From a79cf7d5e01e6018a4bb1dc710b2995a9b17e649 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 30 May 2026 12:34:11 +0000 Subject: [PATCH] fix(tradein): coarse-geo downgrade no longer shielded by Tier C anchor (#693) (#720) Co-authored-by: bot-backend Co-committed-by: bot-backend --- tradein-mvp/backend/app/services/estimator.py | 32 +++++++++++++++---- .../tests/test_same_building_anchor.py | 31 ++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/tradein-mvp/backend/app/services/estimator.py b/tradein-mvp/backend/app/services/estimator.py index 58fb01c5..dbb14528 100644 --- a/tradein-mvp/backend/app/services/estimator.py +++ b/tradein-mvp/backend/app/services/estimator.py @@ -1958,16 +1958,36 @@ async def estimate_quality( # qc_geo >= 2), PostGIS-радиус крутится вокруг центроида НП и собирает # аналоги из случайных частей города → уверенная «medium» вводит в # заблуждение. Помечаем как ориентировочную (low). КОНСЕРВАТИВНО: - # 1) есть позитивный сигнал грубости (dadata.qc_geo >= 2); - # 2) якорь «тот же дом» НЕ сработал (anchor_tier is None) — иначе оценка - # стоит на реальных комплах дома (#691 path) и геоцентроид не важен; + # 1) есть позитивный сигнал грубости (dadata.qc_geo >= 2 ИЛИ _geocode_is_coarse); + # 2) НЕ сработал якорь «ТОГО ЖЕ ДОМА» (Tier A) — он стоит на реальных комплах + # дома (#691 path) и геоцентроид не важен. ВАЖНО (#693 QA-fail 2/3): Tier C + # (micro-radius ≤500 м) — это НЕ «тот же дом», а случайные квартиры в 500 м + # вокруг геокода. Если геокод грубый (мусор → ЕКБ-центроид), Tier C набирает + # ≥5 комплов в плотном центре и РАНЬШЕ блокировал downgrade (guard был + # `anchor_tier is None`) → канонический кейс «фывапролд 999» оставался medium. + # Теперь защищаем ТОЛЬКО Tier A; Tier C при грубом геокоде — тоже понижаем. # 3) есть радиусное число (median_price > 0) — что квалифицировать. # ИСТОЧНИКИ сигнала грубости (OR): dadata.qc_geo>=2 (если DaData активна) ИЛИ - # _geocode_is_coarse(geo) — прод-фолбэк, работает БЕЗ DaData (#693 QA-fail: - # на проде DaData off → qc_geo всегда None → старый гейт не срабатывал). + # _geocode_is_coarse(geo) — прод-фолбэк, работает БЕЗ DaData. dadata_coarse = dadata is not None and dadata.qc_geo is not None and dadata.qc_geo >= 2 geo_coarse = _geocode_is_coarse(geo) - if (dadata_coarse or geo_coarse) and anchor_tier is None and median_price > 0: + if (dadata_coarse or geo_coarse) and median_price > 0: + # #693 QA diag (2/3): на проде QA просил залогировать сигналы геокода для + # негеокодируемых адресов — фиксируем, что увидел гейт. + logger.info( + "coarse-geo gate #693: dadata_coarse=%s geo_coarse=%s anchor_tier=%s " + "median=%s geo.provider=%s geo.confidence=%s geo.full_address=%r geo=(%.5f,%.5f)", + dadata_coarse, + geo_coarse, + anchor_tier, + median_price, + geo.provider, + geo.confidence, + geo.full_address, + geo.lat, + geo.lon, + ) + if (dadata_coarse or geo_coarse) and anchor_tier != "A" and median_price > 0: # Уровень грубости берём из DaData (точнее), иначе generic «населённого пункта». if dadata_coarse: _coarse_label = {2: "населённого пункта", 3: "города", 4: "региона"}.get( diff --git a/tradein-mvp/backend/tests/test_same_building_anchor.py b/tradein-mvp/backend/tests/test_same_building_anchor.py index 890c706c..a308d7b4 100644 --- a/tradein-mvp/backend/tests/test_same_building_anchor.py +++ b/tradein-mvp/backend/tests/test_same_building_anchor.py @@ -894,3 +894,34 @@ def test_estimate_coarse_geo_with_anchor_no_dadata_not_downgraded() -> None: geo_result=_make_fake_geo_coarse(), ) assert _APPROX_FLAG not in (est.confidence_explanation or "") + + +# ── #693 fixup 2/3: Tier C micro-radius must NOT shield a coarse-geo estimate ─ + + +def test_estimate_coarse_geo_tier_c_downgrades() -> None: + """#693 QA-fail 2/3 (canonical 'фывапролд 999' → ЕКБ-центроид): грубый геокод + + Tier C micro-radius (≤500 м) набрал комплы в плотном центре. РАНЬШЕ guard + `anchor_tier is None` блокировал downgrade → medium. Теперь защищаем ТОЛЬКО + Tier A; coarse-геокод + Tier C → low + флаг «ориентировочная».""" + est = _run_estimate( + anchor_comps=_SB_COMPS_PREMIUM, + anchor_tier="C", + dadata_result=None, + geo_result=_make_fake_geo_coarse(), + ) + assert est.median_price_rub > 0 # Tier C дал headline — было что квалифицировать + assert est.confidence == "low" + assert _APPROX_FLAG in (est.confidence_explanation or "") + + +def test_estimate_precise_geo_tier_c_not_downgraded() -> None: + """#693 no false-downgrade: ТОЧНЫЙ геокод (есть дом) + Tier C → НЕ понижаем + (Tier C — легитимный fallback для реального адреса; центроид-сигнала нет).""" + est = _run_estimate( + anchor_comps=_SB_COMPS_PREMIUM, + anchor_tier="C", + dadata_result=None, + # default geo = '…ул. Хохрякова, 48' → есть номер дома → не coarse + ) + assert _APPROX_FLAG not in (est.confidence_explanation or "")