fix(estimator): enable split-home disclosure at validated threshold 1.2 (audit #1871) #1887
3 changed files with 42 additions and 13 deletions
|
|
@ -191,17 +191,21 @@ class Settings(BaseSettings):
|
|||
# ENV: ESTIMATE_RADIUS_DEDUP_ENABLED.
|
||||
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 — дом
|
||||
# дробится на несколько house_id). На split-доме разной этажности comp_min..max
|
||||
# растягивается через несколько ценовых режимов → коридор range_low/high
|
||||
# 148%/170%. Коридор честно широкий, но юзер видит 170% без объяснения. True +
|
||||
# Tier A + corridor_pct > threshold → понижаем confidence на ступень и дописываем
|
||||
# disclosure в explanation. НЕ трогает point/median/range. default False
|
||||
# (no-regress, включаем осознанно). ENV: ESTIMATE_WIDE_CORRIDOR_DISCLOSURE_ENABLED.
|
||||
estimate_wide_corridor_disclosure_enabled: bool = False
|
||||
# Порог ширины коридора (range_high-range_low)/median_price для disclosure (60%).
|
||||
estimate_wide_corridor_threshold: float = 0.6
|
||||
# disclosure в explanation. НЕ трогает point/median/range.
|
||||
# ENV: ESTIMATE_WIDE_CORRIDOR_DISCLOSURE_ENABLED.
|
||||
estimate_wide_corridor_disclosure_enabled: bool = True
|
||||
# Порог ширины коридора (range_high-range_low)/median_price для disclosure.
|
||||
# 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 ────────────────────────
|
||||
# API-ответ Cian иногда возвращает garbage-значения (999_999 или 9_999_999_999).
|
||||
|
|
|
|||
|
|
@ -2507,9 +2507,9 @@ async def estimate_quality(
|
|||
if corridor_pct > settings.estimate_wide_corridor_threshold:
|
||||
confidence = _downgrade_confidence(confidence)
|
||||
explanation = (explanation or "") + (
|
||||
" Широкий ценовой диапазон: дом разбит на секции разной "
|
||||
"этажности — оценка ориентировочная, уточните по конкретной "
|
||||
"секции."
|
||||
" Очень широкий ценовой диапазон по дому (вероятно, дом "
|
||||
"разбит на секции разной этажности или разнородный фонд) — "
|
||||
"оценка ориентировочная, уточните по конкретной секции."
|
||||
)
|
||||
|
||||
# ── #651: IMV / Yandex BLEND (killer accuracy fix) — SECONDARY, Tier D only ──
|
||||
|
|
|
|||
|
|
@ -67,12 +67,15 @@ _DISCLOSURE_MARKER = "разбит на секции"
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_disclosure_flag_default_off() -> None:
|
||||
assert settings.estimate_wide_corridor_disclosure_enabled is False
|
||||
def test_disclosure_flag_default_on() -> None:
|
||||
# Включено после валидации порога на prod-данных (corridor_pct median≈0.48).
|
||||
assert settings.estimate_wide_corridor_disclosure_enabled is True
|
||||
|
||||
|
||||
def test_threshold_default_0_6() -> None:
|
||||
assert settings.estimate_wide_corridor_threshold == 0.6
|
||||
def test_threshold_default_1_2() -> None:
|
||||
# 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 "")
|
||||
|
||||
|
||||
# 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:
|
||||
"""Disclosure НЕ трогает point/median/range — только confidence+explanation."""
|
||||
off = _run(flag=False, anchor_tier="A")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue