Compare commits

...

2 commits

Author SHA1 Message Date
95e3b209ce docs(tradein/estimator): докстринг якоря говорит правду о таблице, а код-фикс отдан #2816 (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m21s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Правка ремонта якоря, добавленная первым коммитом ветки, — дубль уже открытого
PR #2816 (`_anchor_repair_factor`, #2677, открыт 10.08, mergeable). Свой код снят,
чтобы не мержить одно и то же дважды; независимая перепроверка чисел #2816 —
в описании PR.

Остаётся то, чего не чинит ни один открытый PR: докстринг `_fetch_house_imv_anchor`
обещал «house_imv_evaluations популирована (~2951 домов, fresh)». Неправда по обоим
пунктам, и #2816 её тоже не тронул — прочитал и оставил. Заменено на замер с датой:
2680 строк, 2633 (98.2%) старше 40 суток, 2366 — один прогон 24.05, свежих 47;
дореформенные спрошены с литералом renovation_type='cosmetic' (мода дома совпала
у 972, разошлась у 1217) и с дефолтом house_type='panel' (тип разошёлся у 74).

Плюс записаны два отвергнутых замером решения, чтобы их не предлагали заново:
гейт эры снимает якорь у 224 оценок из 224 (все band-совместимые строки на проде —
дореформенные), массовая переоценка упирается в 1628 домов уже в imv_status='ok'
при очереди 7144 и темпе ~25/прогон, и корень всё равно не в свежести.

Refs #2674, #2677
2026-08-13 01:09:08 +05:00
81980da4d4 fix(tradein/estimator): домовой IMV-якорь и медиана сравниваются в одной базе ремонта (#2674)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m29s
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Порог блендa `anchor > median × 1.15` сравнивал два числа в РАЗНЫХ базах.
`median_price` к этому моменту уже умножена на `_repair_coefficient(repair_state)`
(блок 4b), то есть описывает квартиру С РЕМОНТОМ ЦЕЛИ. Якорь — цена квартиры в том
классе отделки, в котором строку спросили у Авито: 2633 из 2680 строк
house_imv_evaluations спрошены с литералом renovation_type='cosmetic' (до фикса
параметров 06.08), свежие 47 — в модальном ремонте ДОМА. Ни то, ни другое не есть
ремонт цели.

Знак ошибки детерминирован ремонтом цели, и это видно на проде (1081 оценка,
из них 127 Tier-D с band-совместимым якорем):
  - цель needs_repair: медиана ×0.94, якорь нет → порог берётся «на халяву».
    6 из 14 живых блендов — такие; 5 из 6 после приведения к общей базе исчезают.
  - цель good/excellent (23 оценки): медиана ×1.05/×1.10 → порог недостижим,
    бленд не срабатывал НИ РАЗУ.

Множитель — отношение уже существующих `_REPAIR_COEF`, обратное отображение
вокабуляра Авито — существующий `_IMV_REPAIR_MAP`. Второй таблицы не заводим.
Незнакомый/пустой класс с любой стороны → 1.0 (прежнее поведение). Сводка
AvitoImvSummary продолжает показывать СЫРУЮ цифру Авито — она так и подписана.

`_REPAIR_COEF` проверен данными (прод 2026-08-12, парное сравнение ppm² ВНУТРИ
дома, база standard): needs_repair 0.946 при коэффициенте 0.94, good 1.039 при
1.05, excellent 1.113 при 1.10 (61/289/84 домов-пар) — эвристика попадает
в измеренное, отдельный множитель не нужен.

Гейт «не брать дореформенные строки» рассмотрен и ОТКЛОНЁН замером: он снимает
якорь у 224 оценок из 224 (все 224 сидят на дореформенных строках), чтобы убрать
5 ложных срабатываний — 14 блендов и +16.6 млн ₽ коррекции превращаются в 0.
Массовая переоценка тоже не чинит дефект: даже при идеально свежих строках,
спрошенных в модальном ремонте дома, бленд у целей good/excellent срабатывает
2 раза из 23 вместо 0 — база всё равно не та.

Докстринг `_fetch_house_imv_anchor` обещал «~2951 домов, fresh» — неправда по
обоим пунктам. Заменён на замер с датой: 2680 строк, 2633 (98.2%) старше 40 суток,
2366 — один прогон 24.05; естественное обновление их не чинит, потому что 1628 из
2633 домов уже в imv_status='ok', а батч выбирает pending/transient_error, где
сейчас 7144 дома при темпе ~25/прогон.

Тесты двусторонние: на origin/main цель excellent даёт 6 600 000 вместо 7 425 000
(бленд подавлен), цель needs_repair — 6 120 000 вместо 5 640 000 (ложный бленд).

Refs #2674
2026-08-13 01:03:50 +05:00

View file

@ -1158,11 +1158,34 @@ def _fetch_house_imv_anchor(
"""Достаёт РЕАЛЬНУЮ Avito IMV-оценку target-дома из `house_imv_evaluations`.
В отличие от `avito_imv_evaluations` (keyed estimate_id пустая, on-demand
скрейп), `house_imv_evaluations` популирована (~2951 домов, fresh) и keyed по
house_id. Резолвим строку: WHERE house_id = target_house_id, предпочитаем
запись с ближайшими rooms+area (минимизируем |Δrooms|*10 + |Δarea%|), иначе
самую свежую (fetched_at DESC). Best-effort: None при любой ошибке / отсутствии
house_id / пустой таблице estimator продолжает на гео-tier'ах (no regress).
скрейп), `house_imv_evaluations` keyed по house_id. Резолвим строку:
WHERE house_id = target_house_id + band-guard по rooms/area. Best-effort:
None при любой ошибке / отсутствии house_id / пустой таблице estimator
продолжает на гео-tier'ах (no regress).
СОСТОЯНИЕ ТАБЛИЦЫ, замер 2026-08-12 (прежний текст обещал «популирована
~2951 домов, fresh» неправда по обоим пунктам, #2674):
2680 строк, одна на дом (house_imv_eval_house_uniq_idx UNIQUE(house_id)
поэтому ORDER BY ниже выбирает из 1 кандидата). 2633 из них (98.2%)
старше 40 суток, 2366 один прогон 2026-05-24; свежих, после фикса
параметров 2026-08-06, всего 47.
Дореформенные 2633 спрошены у Авито с ЛИТЕРАЛОМ renovation_type='cosmetic'
и с дефолтом house_type='panel' при нераспознанном типе: мода ремонта по
объявлениям того же дома совпала лишь у 972 (36.9%), разошлась у 1217
(46.2%), у 444 моды нет; тип дома разошёлся у 74 из 2633 (46 раз ушла
«панель» вместо «монолита»).
ЧТО С ЭТИМ РЕШЕНО (замер на проде, 1081 персистированная оценка):
· Гейт «не брать дореформенные строки» ОТКЛОНЁН: band-совместимая строка
находится у 224 оценок, и все 224 дореформенные. Гейт снял бы якорь у
224 из 224, то есть просто выключил бы механизм.
· Массовая переоценка не чинит корень и упирается в очередь: 1628 из
2633 домов уже в imv_status='ok', а батч берёт только pending/
transient_error, где сейчас 7144 дома при темпе ~25/прогон.
· Корень не свежесть, а РАЗНЫЕ БАЗЫ РЕМОНТА: median_price к моменту
блендa уже умножена на _repair_coefficient(repair_state), а якорь стоит
в том классе отделки, в котором строку спросили. Чинится приведением
якоря к базе цели (#2677).
Returns dict {recommended_price, lower_price, higher_price, market_count,
rooms, area_m2} или None.
@ -3266,8 +3289,7 @@ def _price_from_inputs(
)
if same_quarter_ratio > settings.estimate_quarter_match_skip_ratio:
logger.info(
"quarter_index: Guard-2 skip (same-quarter ratio=%.2f > %.2f)"
" for %s",
"quarter_index: Guard-2 skip (same-quarter ratio=%.2f > %.2f) for %s",
same_quarter_ratio,
settings.estimate_quarter_match_skip_ratio,
target_quarter,
@ -3319,7 +3341,7 @@ def _price_from_inputs(
analogs_with_qi,
)
explanation = (explanation or "") + (
f" Учтена локация квартала" f" (индекс цен квартала ×{qi_factor:.2f})."
f" Учтена локация квартала (индекс цен квартала ×{qi_factor:.2f})."
)
sources_used_pre = sorted(set(sources_used_pre) | {"quarter_index"})
@ -6766,7 +6788,7 @@ def _enforce_zero_analog_low(
"""
if n_analogs == 0 and confidence != "low":
logger.warning(
"ghost_anchor_guard #1871: forcing confidence 'low' (was %s, " "median=%s, sources=%s)",
"ghost_anchor_guard #1871: forcing confidence 'low' (was %s, median=%s, sources=%s)",
confidence,
median_price,
sources_used,