fix(estimator): enable split-home disclosure at validated threshold 1.2 (audit #1871) #1887

Merged
bot-backend merged 1 commit from fix/finmodel-split-disclosure-threshold into main 2026-06-23 20:55:45 +00:00
3 changed files with 42 additions and 13 deletions

View file

@ -191,17 +191,21 @@ class Settings(BaseSettings):
# ENV: ESTIMATE_RADIUS_DEDUP_ENABLED. # ENV: ESTIMATE_RADIUS_DEDUP_ENABLED.
estimate_radius_dedup_enabled: bool = True estimate_radius_dedup_enabled: bool = True
# ── #1871 P2: split-дома wide-corridor disclosure (default OFF) ──────────── # ── #1871 P2: split-дома wide-corridor disclosure (default ON, порог 1.2) ──
# Tier A (same-building) матчит по address-regex (намеренно НЕ house_id — дом # Tier A (same-building) матчит по address-regex (намеренно НЕ house_id — дом
# дробится на несколько house_id). На split-доме разной этажности comp_min..max # дробится на несколько house_id). На split-доме разной этажности comp_min..max
# растягивается через несколько ценовых режимов → коридор range_low/high # растягивается через несколько ценовых режимов → коридор range_low/high
# 148%/170%. Коридор честно широкий, но юзер видит 170% без объяснения. True + # 148%/170%. Коридор честно широкий, но юзер видит 170% без объяснения. True +
# Tier A + corridor_pct > threshold → понижаем confidence на ступень и дописываем # Tier A + corridor_pct > threshold → понижаем confidence на ступень и дописываем
# disclosure в explanation. НЕ трогает point/median/range. default False # disclosure в explanation. НЕ трогает point/median/range.
# (no-regress, включаем осознанно). ENV: ESTIMATE_WIDE_CORRIDOR_DISCLOSURE_ENABLED. # ENV: ESTIMATE_WIDE_CORRIDOR_DISCLOSURE_ENABLED.
estimate_wide_corridor_disclosure_enabled: bool = False estimate_wide_corridor_disclosure_enabled: bool = True
# Порог ширины коридора (range_high-range_low)/median_price для disclosure (60%). # Порог ширины коридора (range_high-range_low)/median_price для disclosure.
estimate_wide_corridor_threshold: float = 0.6 # 1.2 (120%): по prod-данным corridor_pct median≈0.48, p90≈0.93 — порог 0.6
# фаерил бы на ~31% оценок (широкий коридор ≠ split-дом, ложная атрибуция).
# Genuine split-дома из аудита = 148-170% (1.48-1.70) → 1.2 ловит только
# экстремальный хвост (>p99), не трогая нормальную оценочную неопределённость.
estimate_wide_corridor_threshold: float = 1.2
# ── Mera-audit fix-1: Cian valuation sanity bounds ──────────────────────── # ── Mera-audit fix-1: Cian valuation sanity bounds ────────────────────────
# API-ответ Cian иногда возвращает garbage-значения (999_999 или 9_999_999_999). # API-ответ Cian иногда возвращает garbage-значения (999_999 или 9_999_999_999).

View file

@ -2507,9 +2507,9 @@ async def estimate_quality(
if corridor_pct > settings.estimate_wide_corridor_threshold: if corridor_pct > settings.estimate_wide_corridor_threshold:
confidence = _downgrade_confidence(confidence) confidence = _downgrade_confidence(confidence)
explanation = (explanation or "") + ( explanation = (explanation or "") + (
" Широкий ценовой диапазон: дом разбит на секции разной " " Очень широкий ценовой диапазон по дому (вероятно, дом "
"этажности — оценка ориентировочная, уточните по конкретной " "разбит на секции разной этажности или разнородный фонд) — "
"секции." "оценка ориентировочная, уточните по конкретной секции."
) )
# ── #651: IMV / Yandex BLEND (killer accuracy fix) — SECONDARY, Tier D only ── # ── #651: IMV / Yandex BLEND (killer accuracy fix) — SECONDARY, Tier D only ──

View file

@ -67,12 +67,15 @@ _DISCLOSURE_MARKER = "разбит на секции"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_disclosure_flag_default_off() -> None: def test_disclosure_flag_default_on() -> None:
assert settings.estimate_wide_corridor_disclosure_enabled is False # Включено после валидации порога на prod-данных (corridor_pct median≈0.48).
assert settings.estimate_wide_corridor_disclosure_enabled is True
def test_threshold_default_0_6() -> None: def test_threshold_default_1_2() -> None:
assert settings.estimate_wide_corridor_threshold == 0.6 # 1.2: 0.6 фаерил бы на ~31% оценок (широкий коридор ≠ split-дом);
# genuine split-дома аудита = 148-170% → 1.2 ловит только экстремальный хвост.
assert settings.estimate_wide_corridor_threshold == 1.2
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -98,6 +101,28 @@ def test_flag_off_no_change() -> None:
assert _DISCLOSURE_MARKER not in (off.confidence_explanation or "") assert _DISCLOSURE_MARKER not in (off.confidence_explanation or "")
# Boundary-тесты вокруг ПРОД-порога: _WIDE_SPLIT_COMPS даёт коридор ≈1.045
# (estimator MAD-фильтрует более широкие спреды, поэтому проверяем границу
# через сам threshold, а не пытаемся сконструировать коридор >1.2).
def test_corridor_fires_when_above_threshold() -> None:
"""corridor_pct (≈1.045) > threshold (1.0) → disclosure фаерит (механизм работает)."""
on = _run(flag=True, anchor_tier="A", threshold=1.0)
pct = (on.range_high_rub - on.range_low_rub) / on.median_price_rub
assert pct > 1.0
assert _DISCLOSURE_MARKER in (on.confidence_explanation or "")
def test_moderate_corridor_no_fire_at_prod_threshold_1_2() -> None:
"""Прод-дефолт 1.2: умеренный коридор (0.6<pct<1.2) НЕ фаерит — нет ложной
атрибуции split-дома на ~31% оценок, которую дал бы старый порог 0.6."""
on = _run(flag=True, anchor_tier="A", threshold=1.2) # _WIDE_SPLIT_COMPS ≈ 1.045
pct = (on.range_high_rub - on.range_low_rub) / on.median_price_rub
assert 0.6 < pct < 1.2, "moderate-fixture должен быть между 0.6 и 1.2"
assert _DISCLOSURE_MARKER not in (on.confidence_explanation or "")
def test_does_not_touch_point_median_range() -> None: def test_does_not_touch_point_median_range() -> None:
"""Disclosure НЕ трогает point/median/range — только confidence+explanation.""" """Disclosure НЕ трогает point/median/range — только confidence+explanation."""
off = _run(flag=False, anchor_tier="A") off = _run(flag=False, anchor_tier="A")