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%).
|
лишь 36%).
|
||||||
|
|
||||||
Неизвестный ремонт (498 домов из 2685 — ни одного объявления с repair_state)
|
Неизвестный ремонт (498 домов из 2685 — ни одного объявления с repair_state)
|
||||||
ОСТАЁТСЯ 'cosmetic', в отличие от неизвестного типа дома: это середина
|
ОСТАЁТСЯ 'cosmetic', в отличие от неизвестного типа дома: 'cosmetic'
|
||||||
порядковой шкалы (required < cosmetic < euro < designer), а не её край,
|
(=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-блок
|
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 ────────────────────────────────────
|
# ── Region bbox prefix для Avito geocoder ────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,17 @@ async def _job_house_imv_backfill(
|
||||||
"skipped": result.skipped,
|
"skipped": result.skipped,
|
||||||
"errors": result.errors,
|
"errors": result.errors,
|
||||||
"duration_sec": int(result.duration_sec),
|
"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): успех — это
|
# Честный статус (#2674, тот же класс, что #2670/#2657): успех — это
|
||||||
# «сделали то, что собирались», а не «не поймали известное исключение».
|
# «сделали то, что собирались», а не «не поймали известное исключение».
|
||||||
|
|
|
||||||
|
|
@ -204,3 +204,17 @@ async def test_partial_success_stays_done() -> None:
|
||||||
"""Анти-оверрич: что-то сохранили — прогон успешен, даже если были ошибки."""
|
"""Анти-оверрич: что-то сохранили — прогон успешен, даже если были ошибки."""
|
||||||
calls = await _drive_job(saved=3, errors=7)
|
calls = await _drive_job(saved=3, errors=7)
|
||||||
assert calls[-1][0] == "mark_done"
|
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