From 77a3d3f75a05c998f187a69c63878f6c26c5b3f6 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Wed, 24 Jun 2026 01:54:58 +0500 Subject: [PATCH] fix(estimator): enable split-home disclosure at validated threshold 1.2 (audit #1871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Включает wide-corridor disclosure (PR #1880, был default OFF) — но с ИСПРАВЛЕННЫМ порогом по prod-данным. Проверка на trade_in_estimates (60д): corridor_pct = (range_high-range_low)/ median_price имеет median≈0.48, p90≈0.93, p99≈1.38; **30.7% оценок > 0.6**. Старый порог 0.6 прилепил бы caveat «дом разбит на секции» к ~трети оценок — большинство НЕ split-дома (широкий коридор бывает от разных причин). Это была бы ложная атрибуция (дезинформация). - estimate_wide_corridor_threshold 0.6 → 1.2: genuine split-дома из аудита = 148-170% (1.48-1.70) → 1.2 ловит только экстремальный хвост (>p99), не трогая нормальную оценочную неопределённость. - estimate_wide_corridor_disclosure_enabled False → True (включаем после валидации). ENV-override сохранён (kill-switch). - Формулировка смягчена: «дом разбит на секции» → «вероятно, дом разбит на секции разной этажности ИЛИ разнородный фонд» — мы ИНФЕРИМ split по ширине коридора, не доказываем структурно. - Логика не тронута: фаерит только Tier A + corridor>threshold, только понижает confidence + дописывает explanation; point/median/range не трогает. Тесты: обновлены default-ассерты (флаг ON, порог 1.2) + 2 behavioral boundary- теста (фаерит при pct>threshold, НЕ фаерит при умеренном 1.045<1.2 — доказывает что raise убрал false-positive flood). 13 passed. Refs #1871 --- tradein-mvp/backend/app/core/config.py | 16 +++++---- tradein-mvp/backend/app/services/estimator.py | 6 ++-- .../test_estimator_split_corridor_1871.py | 33 ++++++++++++++++--- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/tradein-mvp/backend/app/core/config.py b/tradein-mvp/backend/app/core/config.py index 95ee6372..0cd30304 100644 --- a/tradein-mvp/backend/app/core/config.py +++ b/tradein-mvp/backend/app/core/config.py @@ -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). diff --git a/tradein-mvp/backend/app/services/estimator.py b/tradein-mvp/backend/app/services/estimator.py index 3c8f14c6..15acb693 100644 --- a/tradein-mvp/backend/app/services/estimator.py +++ b/tradein-mvp/backend/app/services/estimator.py @@ -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 ── diff --git a/tradein-mvp/backend/tests/test_estimator_split_corridor_1871.py b/tradein-mvp/backend/tests/test_estimator_split_corridor_1871.py index 27cc8265..bc7214af 100644 --- a/tradein-mvp/backend/tests/test_estimator_split_corridor_1871.py +++ b/tradein-mvp/backend/tests/test_estimator_split_corridor_1871.py @@ -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 None: """Disclosure НЕ трогает point/median/range — только confidence+explanation.""" off = _run(flag=False, anchor_tier="A")