From 55c7bc2c72563a1dabdd1f95dc3d5e63724b83fb Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 20 Jun 2026 14:59:05 +0300 Subject: [PATCH] fix(estimator): anchor low-confidence gate (#audit-1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Якорь с confidence=low ИЛИ (ngate_max_fsd) не заменяет headline — fallback на radius-median. Дефолты (min_n=3, max_fsd=0.20) не режут здоровые якоря (n≥4, FSD<0.15 проходят). За флагом estimate_sb_low_conf_gate_enabled (дефолт True). Логируется с причиной. --- tradein-mvp/backend/app/core/config.py | 29 +++++++++++++++++++ tradein-mvp/backend/app/services/estimator.py | 22 ++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/tradein-mvp/backend/app/core/config.py b/tradein-mvp/backend/app/core/config.py index 543276e8..05366fd1 100644 --- a/tradein-mvp/backend/app/core/config.py +++ b/tradein-mvp/backend/app/core/config.py @@ -134,6 +134,35 @@ class Settings(BaseSettings): asking_to_sold_haircut: float = 0.05 # дефолтная asking→sold скидка (banded по ppm²) estimate_fsd_k: float = 1.65 # множитель FSD → полуширина диапазона + # ── #audit-1: anchor low-confidence gate ───────────────────────────────── + # Якорь с низкой уверенностью (confidence="low" ИЛИ n < min_n И FSD > max_fsd) + # НЕ заменяет headline — fallback на radius-median. Дефолты подобраны так, что + # здоровые якоря (n≥4 с FSD<0.15) проходят без изменений. + # estimate_sb_gate_min_n=3 : при n<3 И FSD>max_fsd гейт срабатывает + # estimate_sb_gate_max_fsd=0.20: FSD>0.20 при малом n → ненадёжный якорь + # Отдельный флаг: False → точно старое поведение (гейта нет). + estimate_sb_low_conf_gate_enabled: bool = True + estimate_sb_gate_min_n: int = 3 + estimate_sb_gate_max_fsd: float = 0.20 + + # ── #audit-3: price_trend freshness filter ──────────────────────────────── + # Исключать items старше N месяцев из price_trend (house_placement_history). + # Дефолт 6 (консервативно); аудит предложил 3 — конфигурируемо. + estimate_price_trend_max_age_months: int = 6 + + # ── #audit-4: MAD-clip after similarity-weighting ───────────────────────── + # True = clip происходит ПОСЛЕ similarity-weighting (на взвешенных ppm²). + # False = clip ДО weighting (старое поведение). Дефолт True. + estimate_sb_clip_after_weight: bool = True + + # ── #audit-5: data-age guards ───────────────────────────────────────────── + # sber_index_max_age_days: максимальный допустимый возраст последнего месяца + # СберИндекс-серии (дней). Если latest месяц старее — логируем warning. + sber_index_max_age_days: int = 35 + # avito_imv_thin_market_threshold: если market_count < порога — IMV-оценка + # на тонком рынке (thin_market=True в AvitoImvSummary) + warning. + avito_imv_thin_market_threshold: int = 10 + # ── #764: per-cadastral-quarter price index correction ─────────────────── # Gap-correction: квартальный индекс применяется ТОЛЬКО в pure-radius пути # (когда same-building anchor и IMV-blend не сработали). Корректирует РАЗРЫВ diff --git a/tradein-mvp/backend/app/services/estimator.py b/tradein-mvp/backend/app/services/estimator.py index 7566cd7a..75b915e9 100644 --- a/tradein-mvp/backend/app/services/estimator.py +++ b/tradein-mvp/backend/app/services/estimator.py @@ -2321,6 +2321,28 @@ async def estimate_quality( int(gate_threshold), ) anchor = None + # #audit-1: low-confidence gate — якорь с низкой уверенностью или малым n + # при высоком FSD НЕ заменяет headline; fallback на radius-median. + # Tier A (n≥4, FSD<0.15) проходит без изменений при дефолтных порогах. + if anchor is not None and settings.estimate_sb_low_conf_gate_enabled: + gate_low = anchor["confidence"] == "low" + gate_thin = ( + anchor["n"] < settings.estimate_sb_gate_min_n + and anchor["fsd"] > settings.estimate_sb_gate_max_fsd + ) + if gate_low or gate_thin: + logger.info( + "sb_anchor low-conf gate #audit-1: tier=%s n=%d fsd=%.3f conf=%s" + " → suppressed (gate_low=%s gate_thin=%s) → radius fallback", + anchor_tier, + anchor["n"], + anchor["fsd"], + anchor["confidence"], + gate_low, + gate_thin, + ) + anchor = None + anchor_tier = None if anchor is not None: # #694: якорь мутирует headline — UI-аналоги должны отражать ЭТИ комплы. anchor_comps_used = comps