fix(tradein/imv): счётчики прогона в total_seen/new_count + лог дрейфа ремонта (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (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
CI Trade-In / backend-tests (pull_request) Successful in 2m51s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (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
CI Trade-In / backend-tests (pull_request) Successful in 2m51s
По ревью PR #2675. 1. counters прогона не заполняли выделенные колонки. _column_counts (scrape_runs.py) берёт total_seen из ключей total_seen|lots_fetched и new_count из new_count|lots_inserted — ни одного из них в дикте не было, поэтому все 39 прогонов этого source лежат в БД с total_seen=0. А mark_done по этой же колонке шлёт алерт «3 подряд done с нулевым результатом» (#2625): даже идеальный прогон с 50 сохранёнными считался бы нулевым и через три дня выстрелил бы ложной тревогой про капчу. Добавлены total_seen=checked и new_count=saved. Трейд-офф назван в комментарии: на исчерпанной очереди checked=0 три дня подряд тоже даст алерт — но пустая очередь при ежедневном расписании это и правда сигнал. 2. _map_renovation_type молча схлопывал в 'cosmetic' любое незнакомое непустое значение. Сегодня в проде ровно четыре канонических, живого эффекта нет, но дрейф вокабуляра реален (70950 строк listings с пустым нормализованным ремонтом). Добавлен logger.debug на случай «непустое, но не в карте» — паритет с house_type_normalizer, который такой лог уже пишет. 3. Обоснование дефолта 'cosmetic' в докстринге заменено на более сильное по данным: это одновременно МОДА и МЕДИАННАЯ категория популяции (standard 7984 / good 7116 / needs_repair 4738 / excellent 2562; кумулятивно needs_repair 21.2%, +standard 56.8%), то есть наилучшая одиночная догадка, а не просто «не край шкалы». Там же названа асимметрия: поштучный путь эстиматора при неизвестном ремонте IMV вообще не зовёт, а домовой дефолтит — решение осознанное (иначе теряем ещё ~32% домов очереди), чтобы следующий читатель не принял это за недосмотр. Refs #2674
This commit is contained in:
parent
0815319e1c
commit
4b4ab8b34c
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