fix(tradein/imv): домовая оценка перестаёт врать про ремонт и тип дома (#2674) #2675
3 changed files with 42 additions and 4 deletions
|
|
@ -116,13 +116,26 @@ def _map_renovation_type(repair_state: str | None) -> str:
|
|||
лишь 36%).
|
||||
|
||||
Неизвестный ремонт (498 домов из 2685 — ни одного объявления с repair_state)
|
||||
ОСТАЁТСЯ 'cosmetic', в отличие от неизвестного типа дома: это середина
|
||||
порядковой шкалы (required < cosmetic < euro < designer), а не её край,
|
||||
поэтому системного сдвига цены в одну сторону не даёт.
|
||||
ОСТАЁТСЯ 'cosmetic', в отличие от неизвестного типа дома: 'cosmetic'
|
||||
(=standard) — это одновременно МОДА и МЕДИАННАЯ категория популяции
|
||||
(standard 7984 / good 7116 / needs_repair 4738 / excellent 2562; кумулятивно
|
||||
needs_repair 21.2%, +standard 56.8%), то есть наилучшая одиночная догадка.
|
||||
У типа дома такой догадки нет: 'panel' — почти край шкалы, а не её середина.
|
||||
|
||||
Асимметрия осознанная, а не недосмотр: поштучный путь эстиматора при
|
||||
неизвестном ремонте IMV вообще не зовёт (estimator.py, `imv_renovation is not
|
||||
None`), а домовой дефолтит — иначе теряем ещё ~32% домов очереди поверх тех,
|
||||
что уже отсекает неизвестный тип дома.
|
||||
"""
|
||||
from app.services.estimator import _IMV_REPAIR_MAP # lazy — см. import-блок
|
||||
|
||||
return _IMV_REPAIR_MAP.get(repair_state) or "cosmetic"
|
||||
mapped = _IMV_REPAIR_MAP.get(repair_state)
|
||||
if mapped is None and repair_state:
|
||||
# Непустое, но незнакомое значение — признак дрейфа вокабуляра на ингесте
|
||||
# (сырых repair-значений в listings больше, чем нормализованных). Паритет
|
||||
# с house_type_normalizer, который такой случай уже логирует.
|
||||
logger.debug("house_imv: unmapped repair_state %r — падаем в 'cosmetic'", repair_state)
|
||||
return mapped or "cosmetic"
|
||||
|
||||
|
||||
# ── Region bbox prefix для Avito geocoder ────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -407,6 +407,17 @@ async def _job_house_imv_backfill(
|
|||
"skipped": result.skipped,
|
||||
"errors": result.errors,
|
||||
"duration_sec": int(result.duration_sec),
|
||||
# #2674: _column_counts (scrape_runs.py) берёт выделенные колонки из
|
||||
# ключей total_seen|lots_fetched и new_count|lots_inserted — ни одного
|
||||
# из них тут не было, поэтому все 39 прогонов этого source лежат в БД
|
||||
# с total_seen=0. А mark_done по этой же колонке шлёт алерт «3 подряд
|
||||
# done с нулевым результатом» (#2625) — то есть даже идеальный прогон
|
||||
# с 50 сохранёнными считался бы нулевым и через три дня выстрелил бы
|
||||
# ложной тревогой про капчу.
|
||||
# Трейд-офф: на исчерпанной очереди checked=0 три дня подряд тоже даст
|
||||
# алерт — но пустая очередь при ежедневном расписании это и правда сигнал.
|
||||
"total_seen": result.checked,
|
||||
"new_count": result.saved,
|
||||
}
|
||||
# Честный статус (#2674, тот же класс, что #2670/#2657): успех — это
|
||||
# «сделали то, что собирались», а не «не поймали известное исключение».
|
||||
|
|
|
|||
|
|
@ -204,3 +204,17 @@ async def test_partial_success_stays_done() -> None:
|
|||
"""Анти-оверрич: что-то сохранили — прогон успешен, даже если были ошибки."""
|
||||
calls = await _drive_job(saved=3, errors=7)
|
||||
assert calls[-1][0] == "mark_done"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_counters_feed_total_seen_and_new_count() -> None:
|
||||
"""#2674: без этих ключей _column_counts оставляет колонку total_seen=0,
|
||||
|
||||
и алерт «3 подряд done с нулевым результатом» (#2625) выстрелил бы даже на
|
||||
полностью успешном прогоне. На проде так лежат все 39 прогонов source'а.
|
||||
"""
|
||||
from app.services.scrape_runs import _column_counts
|
||||
|
||||
calls = await _drive_job(saved=50, errors=0)
|
||||
counters = calls[-1][1]
|
||||
assert _column_counts(counters) == (50, 50)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue