fix(tradein): sync n_analogs to anchor count when anchor drives headline (#695)

QA-fail after #738: that PR de-contradicted the explanation STRING, but
n_analogs stayed = radius count (listings_clean) while the same-building anchor
(#691/#694) replaced the headline + analogs list with the house comps. So every
UI counter («Аналогов N», «Показано N из M», «по N аналогам», histogram) still
read the radius number — producing «Показано 5 из 4» on the Олимп penthouse
(radius=4, anchor=5). #738's Малышева case passed only because radius==anchor
(14=14) — mimicry, not a fix.

Fix: when anchor_tier is not None, set n_analogs = anchor['n'] (the comp count
the estimate is actually built on, = the displayed analogs capped at 10). All
counters now agree. Per #698, for the anchor path that IS the «full count found».
Radius path unchanged.

Refs #695
This commit is contained in:
bot-backend 2026-05-30 18:23:30 +03:00
parent 2b3eb0b23d
commit d688d24a12
2 changed files with 28 additions and 0 deletions

View file

@ -1818,6 +1818,14 @@ async def estimate_quality(
f"Оценка построена по {anchor['n']} аналогам из {tier_label}"
f"{' (топ-уровень в доме)' if anchor['used_uplift'] else ''}."
) + repair_note
# #695 (QA fixup): когда якорь подменяет headline и список аналогов на
# комплы дома, n_analogs ДОЛЖЕН считаться по anchor-популяции, а не по
# радиусу (listings_clean). Иначе все UI-счётчики («Аналогов N»,
# «Показано N из M», «по N аналогам», гистограмма) расходятся: radius=4 vs
# anchor=5 → артефакт «Показано 5 из 4». anchor['n'] = число комплов, на
# которых построена оценка (= показываемый analogs, capped 10). Для anchor-
# пути это и есть «полное число найденных» по контракту n_analogs (#698).
n_analogs = anchor["n"]
# ── #651: IMV / Yandex BLEND (killer accuracy fix) — SECONDARY, Tier D only ──
# Радиусная медиана системно занижает премиум/видовые квартиры (нет class/

View file

@ -947,3 +947,23 @@ def test_explanation_radius_path_keeps_base_text() -> None:
exp = est.confidence_explanation or ""
assert "Найдено" in exp
assert "Оценка построена по" not in exp # якорной фразы нет на чистом радиусе
# ── #695 QA fixup: n_analogs синхронизируется с anchor-популяцией (radius≠anchor) ──
def test_anchor_n_analogs_syncs_to_anchor_count() -> None:
"""#695 QA-fail: anchor подменяет headline+список на комплы дома, но n_analogs
раньше оставался радиусным «Показано 5 из 4». radius=5 anchor=3 намеренно;
n_analogs ДОЛЖЕН стать 3 (anchor), согласуясь с «по 3 аналогам» в explanation."""
radius = [_make_listing(price_per_m2=200_000.0) for _ in range(5)]
est = _run_estimate(anchor_comps=_SB_COMPS_PREMIUM, anchor_tier="A", radius_analogs=radius)
assert est.n_analogs == 3 # = len(_SB_COMPS_PREMIUM), НЕ 5 (радиус)
assert "по 3 аналогам" in (est.confidence_explanation or "")
def test_radius_path_n_analogs_unchanged() -> None:
"""#695 контроль: без якоря n_analogs = радиусный счёт (поведение не меняется)."""
radius = [_make_listing(price_per_m2=200_000.0) for _ in range(5)]
est = _run_estimate(anchor_comps=[], anchor_tier=None, radius_analogs=radius)
assert est.n_analogs == 5