From e6e2a2cdc4dca6740fb5594bb4195894040b72f8 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 30 May 2026 18:07:44 +0300 Subject: [PATCH] fix(tradein): confidence_explanation no longer contradicts counters (#695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the same-building/micro-radius anchor built the headline, the explanation CONCATENATED three fragments describing different populations: the radius base text («Найдено N аналогов из M разных адресов»), the analog_tier note, and the anchor sentence («построено по anchor['n'] аналогам из того же дома»). Result: contradictory counts (n_analogs≠anchor['n'], e.g. «4 … по 5») and tiers («разных адресов» vs «того же дома»). Fix: when the anchor fires, the anchor sentence becomes authoritative — replace the explanation with it (+ repair_note, which is orthogonal) instead of appending to the radius base + tier_note. Radius path unchanged. Refs #695 --- tradein-mvp/backend/app/services/estimator.py | 12 +++++++--- .../tests/test_same_building_anchor.py | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tradein-mvp/backend/app/services/estimator.py b/tradein-mvp/backend/app/services/estimator.py index 810fb78e..7476c21d 100644 --- a/tradein-mvp/backend/app/services/estimator.py +++ b/tradein-mvp/backend/app/services/estimator.py @@ -1808,10 +1808,16 @@ async def estimate_quality( range_high = new_range_high confidence = anchor["confidence"] tier_label = "того же дома" if anchor_tier == "A" else "ближайшего окружения (≤500 м)" - explanation = (explanation or "") + ( - f" Оценка построена по {anchor['n']} аналогам из {tier_label}" + # #695: когда якорь построил headline, explanation описывает ИМЕННО якорные + # комплы (anchor['n'] из tier_label). Радиусный base-текст («Найдено N из M + # разных адресов») и analog_tier tier_note относятся к ДРУГОЙ выборке и + # противоречат якорю (n_analogs≠anchor['n']; «разных адресов» vs «того же + # дома») — поэтому ЗАМЕНЯЕМ, а не конкатенируем. repair_note сохраняем + # (ортогонален — поправка на ремонт). confidence уже = anchor["confidence"]. + explanation = ( + f"Оценка построена по {anchor['n']} аналогам из {tier_label}" f"{' (топ-уровень в доме)' if anchor['used_uplift'] else ''}." - ) + ) + repair_note # ── #651: IMV / Yandex BLEND (killer accuracy fix) — SECONDARY, Tier D only ── # Радиусная медиана системно занижает премиум/видовые квартиры (нет class/ diff --git a/tradein-mvp/backend/tests/test_same_building_anchor.py b/tradein-mvp/backend/tests/test_same_building_anchor.py index a308d7b4..6d55e99b 100644 --- a/tradein-mvp/backend/tests/test_same_building_anchor.py +++ b/tradein-mvp/backend/tests/test_same_building_anchor.py @@ -925,3 +925,25 @@ def test_estimate_precise_geo_tier_c_not_downgraded() -> None: # default geo = '…ул. Хохрякова, 48' → есть номер дома → не coarse ) assert _APPROX_FLAG not in (est.confidence_explanation or "") + + +# ── #695: explanation согласован при сработавшем якоре (без радиус-противоречия) ── + + +def test_explanation_anchor_no_radius_contradiction() -> None: + """#695: якорь дома построил headline → explanation описывает якорные комплы + (anchor['n']=3 из того же дома), БЕЗ противоречащего радиусного «Найдено N из M + разных адресов» и без analog_tier-note про «нет аналогов в том же доме».""" + est = _run_estimate(anchor_comps=_SB_COMPS_PREMIUM, anchor_tier="A") + exp = est.confidence_explanation or "" + assert "Оценка построена по 3 аналогам из того же дома" in exp + assert "Найдено" not in exp # радиусный base-текст не конкатенируется + assert "разных адресов" not in exp # нет противоречия «того же дома» vs «разных» + + +def test_explanation_radius_path_keeps_base_text() -> None: + """#695 контроль: без якоря (radius-путь) base-текст «Найдено N…» сохраняется.""" + est = _run_estimate(anchor_comps=[], anchor_tier=None) + exp = est.confidence_explanation or "" + assert "Найдено" in exp + assert "Оценка построена по" not in exp # якорной фразы нет на чистом радиусе -- 2.45.3