fix(tradein): coarse-geo downgrade no longer shielded by Tier C anchor (#693)

QA re-smoke after #718 (fixup 2/3): canonical 'фывапролд 999' → ЕКБ
city-centroid still returned medium. Root cause: the coarse-geo gate guard
was anchor_tier is None, but a coarse geocode landing on the dense city
centroid lets Tier C (micro-radius ≤500 m) collect ≥5 comps → anchor_tier='C'
→ guard blocked the downgrade. Tier C is NOT same-building (#691); it is just
random flats within 500 m of a meaningless centroid — exactly the false
confidence this issue targets. Digit-free garbage ('ёъхзщшг') proves
_geocode_is_coarse already returned True; only the anchor guard blocked it.

Protect ONLY Tier A (real same-building comps); coarse geo + Tier C now
downgrades to low + flag. Add prod-diagnostic log of geo fields (QA request).

Refs #693
This commit is contained in:
bot-backend 2026-05-30 15:27:16 +03:00
parent 406ea8365a
commit 090a5c6e91
2 changed files with 57 additions and 6 deletions

View file

@ -1958,16 +1958,36 @@ async def estimate_quality(
# qc_geo >= 2), PostGIS-радиус крутится вокруг центроида НП и собирает # qc_geo >= 2), PostGIS-радиус крутится вокруг центроида НП и собирает
# аналоги из случайных частей города → уверенная «medium» вводит в # аналоги из случайных частей города → уверенная «medium» вводит в
# заблуждение. Помечаем как ориентировочную (low). КОНСЕРВАТИВНО: # заблуждение. Помечаем как ориентировочную (low). КОНСЕРВАТИВНО:
# 1) есть позитивный сигнал грубости (dadata.qc_geo >= 2); # 1) есть позитивный сигнал грубости (dadata.qc_geo >= 2 ИЛИ _geocode_is_coarse);
# 2) якорь «тот же дом» НЕ сработал (anchor_tier is None) — иначе оценка # 2) НЕ сработал якорь «ТОГО ЖЕ ДОМА» (Tier A) — он стоит на реальных комплах
# стоит на реальных комплах дома (#691 path) и геоцентроид не важен; # дома (#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) — что квалифицировать. # 3) есть радиусное число (median_price > 0) — что квалифицировать.
# ИСТОЧНИКИ сигнала грубости (OR): dadata.qc_geo>=2 (если DaData активна) ИЛИ # ИСТОЧНИКИ сигнала грубости (OR): dadata.qc_geo>=2 (если DaData активна) ИЛИ
# _geocode_is_coarse(geo) — прод-фолбэк, работает БЕЗ DaData (#693 QA-fail: # _geocode_is_coarse(geo) — прод-фолбэк, работает БЕЗ DaData.
# на проде DaData off → qc_geo всегда None → старый гейт не срабатывал).
dadata_coarse = dadata is not None and dadata.qc_geo is not None and dadata.qc_geo >= 2 dadata_coarse = dadata is not None and dadata.qc_geo is not None and dadata.qc_geo >= 2
geo_coarse = _geocode_is_coarse(geo) 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 «населённого пункта». # Уровень грубости берём из DaData (точнее), иначе generic «населённого пункта».
if dadata_coarse: if dadata_coarse:
_coarse_label = {2: "населённого пункта", 3: "города", 4: "региона"}.get( _coarse_label = {2: "населённого пункта", 3: "города", 4: "региона"}.get(

View file

@ -894,3 +894,34 @@ def test_estimate_coarse_geo_with_anchor_no_dadata_not_downgraded() -> None:
geo_result=_make_fake_geo_coarse(), geo_result=_make_fake_geo_coarse(),
) )
assert _APPROX_FLAG not in (est.confidence_explanation or "") 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 "")