fix(tradein): Mera-audit fix-2 — _is_plausible_deal floor edge + area/price guards
Ужесточает валидацию ДКП-сделок в _is_plausible_deal: - floor < 1 или floor=0 → drop (floor=-5/0/999 из битого парсера) - area_m2 задана и <= 0 → drop (нулевая/отрицательная площадь из битого парсера) - price_rub задана и <= 0 → drop (нерыночная/техническая сделка) - floor=None → допустимо (graceful, нечем судить) Все новые параметры area_m2/price_rub keyword-only с дефолтом None → backward-compatible для существующих вызовов. Caller _fetch_deals обновлён для передачи area_m2/price_rub из данных сделки. Тесты: 16 новых тест-кейсов в test_deals_sanitize.py (floor=-5, floor=999, floor=None ok, area_m2=0 drop, price_rub=-1 drop и пр.).
This commit is contained in:
parent
50a72f030a
commit
81ee864d80
2 changed files with 95 additions and 6 deletions
|
|
@ -4000,12 +4000,21 @@ def _fetch_analogs(
|
||||||
|
|
||||||
|
|
||||||
def _is_plausible_deal(
|
def _is_plausible_deal(
|
||||||
price_per_m2: float | None, floor: int | None, total_floors: int | None
|
price_per_m2: float | None,
|
||||||
|
floor: int | None,
|
||||||
|
total_floors: int | None,
|
||||||
|
area_m2: float | None = None,
|
||||||
|
price_rub: float | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""#699: True если ДКП-сделка правдоподобна (не выброс по ppm²/этажу).
|
"""#699 + Mera-audit fix-2: True если ДКП-сделка правдоподобна (не выброс).
|
||||||
|
|
||||||
Абсолютные guard-bands (см. DEAL_* константы). None-поля не судим (keep —
|
Абсолютные guard-bands (см. DEAL_* константы). None-поля не судим (keep —
|
||||||
нечем сравнивать). Этаж > total_floors физически невозможен → drop.
|
нечем сравнивать). Проверки:
|
||||||
|
- price_per_m2 вне [DEAL_MIN_PPM2, DEAL_MAX_PPM2] → drop
|
||||||
|
- floor < 1 или floor > DEAL_MAX_FLOOR → drop (битый парсер: floor=-5/999)
|
||||||
|
- floor > total_floors физически невозможен → drop
|
||||||
|
- area_m2 задана и <= 0 → drop (битый парсер)
|
||||||
|
- price_rub задана и <= 0 → drop (нерыночная/технческая сделка)
|
||||||
"""
|
"""
|
||||||
if price_per_m2 is not None and not (DEAL_MIN_PPM2 <= price_per_m2 <= DEAL_MAX_PPM2):
|
if price_per_m2 is not None and not (DEAL_MIN_PPM2 <= price_per_m2 <= DEAL_MAX_PPM2):
|
||||||
return False
|
return False
|
||||||
|
|
@ -4014,6 +4023,10 @@ def _is_plausible_deal(
|
||||||
return False
|
return False
|
||||||
if total_floors is not None and total_floors > 0 and floor > total_floors:
|
if total_floors is not None and total_floors > 0 and floor > total_floors:
|
||||||
return False
|
return False
|
||||||
|
if area_m2 is not None and area_m2 <= 0:
|
||||||
|
return False
|
||||||
|
if price_rub is not None and price_rub <= 0:
|
||||||
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -4055,13 +4068,19 @@ def _fetch_deals(
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
# #699: отсекаем ДКП-выбросы (битый этаж / нерыночный ppm²) до выдачи в
|
# #699 + Mera-audit fix-2: отсекаем ДКП-выбросы (битый этаж / нерыночный ppm²
|
||||||
# actual_deals и expected_sold — иначе floor:100 / 39.7К-м² шумят демо.
|
# / нулевая площадь / нулевая цена) до выдачи в actual_deals и expected_sold.
|
||||||
deals = [dict(r) for r in rows]
|
deals = [dict(r) for r in rows]
|
||||||
clean = [
|
clean = [
|
||||||
d
|
d
|
||||||
for d in deals
|
for d in deals
|
||||||
if _is_plausible_deal(d.get("price_per_m2"), d.get("floor"), d.get("total_floors"))
|
if _is_plausible_deal(
|
||||||
|
d.get("price_per_m2"),
|
||||||
|
d.get("floor"),
|
||||||
|
d.get("total_floors"),
|
||||||
|
d.get("area_m2"),
|
||||||
|
d.get("price_rub"),
|
||||||
|
)
|
||||||
]
|
]
|
||||||
if len(clean) < len(deals):
|
if len(clean) < len(deals):
|
||||||
logger.info("deals sanitize #699: %d → %d (dropped outliers)", len(deals), len(clean))
|
logger.info("deals sanitize #699: %d → %d (dropped outliers)", len(deals), len(clean))
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,73 @@ def test_null_fields_not_judged() -> None:
|
||||||
assert _is_plausible_deal(None, None, None) is True
|
assert _is_plausible_deal(None, None, None) is True
|
||||||
assert _is_plausible_deal(None, 100, None) is False # floor всё равно судим
|
assert _is_plausible_deal(None, 100, None) is False # floor всё равно судим
|
||||||
assert _is_plausible_deal(39_700, None, None) is False # ppm² всё равно судим
|
assert _is_plausible_deal(39_700, None, None) is False # ppm² всё равно судим
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Mera-audit fix-2: floor edge cases + area_m2/price_rub guards
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_drops_floor_negative() -> None:
|
||||||
|
# floor=-5 из битого парсера → drop.
|
||||||
|
assert _is_plausible_deal(150_000, -5, 9) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_drops_floor_zero() -> None:
|
||||||
|
# floor=0 — не существует (1-indexed) → drop.
|
||||||
|
assert _is_plausible_deal(150_000, 0, 9) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_drops_floor_999() -> None:
|
||||||
|
# floor=999 из битого парсера → drop (> DEAL_MAX_FLOOR=60).
|
||||||
|
assert _is_plausible_deal(150_000, 999, None) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_floor_none_graceful() -> None:
|
||||||
|
# floor=None → допустимо, не судим.
|
||||||
|
assert _is_plausible_deal(150_000, None, None) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_floor_1_valid() -> None:
|
||||||
|
assert _is_plausible_deal(150_000, 1, 9) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_floor_at_max() -> None:
|
||||||
|
assert _is_plausible_deal(150_000, DEAL_MAX_FLOOR, None) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_drops_area_m2_zero() -> None:
|
||||||
|
# area_m2=0 из битого парсера → drop.
|
||||||
|
assert _is_plausible_deal(150_000, 5, 9, area_m2=0.0) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_drops_area_m2_negative() -> None:
|
||||||
|
# area_m2=-10 → drop.
|
||||||
|
assert _is_plausible_deal(150_000, 5, 9, area_m2=-10.0) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_area_m2_none() -> None:
|
||||||
|
# area_m2=None → не судим (graceful).
|
||||||
|
assert _is_plausible_deal(150_000, 5, 9, area_m2=None) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_area_m2_valid() -> None:
|
||||||
|
assert _is_plausible_deal(150_000, 5, 9, area_m2=55.0) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_drops_price_rub_zero() -> None:
|
||||||
|
# price_rub=0 → нерыночная/техническая сделка → drop.
|
||||||
|
assert _is_plausible_deal(150_000, 5, 9, price_rub=0.0) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_drops_price_rub_negative() -> None:
|
||||||
|
assert _is_plausible_deal(150_000, 5, 9, price_rub=-1.0) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_price_rub_none() -> None:
|
||||||
|
# price_rub=None → не судим (graceful).
|
||||||
|
assert _is_plausible_deal(150_000, 5, 9, price_rub=None) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_keeps_price_rub_valid() -> None:
|
||||||
|
assert _is_plausible_deal(150_000, 5, 9, price_rub=8_000_000.0) is True
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue