feat(tradein/db): авто-refresh ценовых бэндов по городам + порог для малых городов (#2576) #2579

Merged
lekss361 merged 2 commits from feat/tradein-price-bands-refresh into main 2026-07-31 14:01:35 +00:00
Owner

Эпик #2576, этап B. Тихий дефект: deal_city_price_bands строилась разово (SQL 178) только для городов с ≥30 сделок; остальные падали на глобальный DEAL_MIN_PPM2=50000, откалиброванный по ЕКБ. В Североуральске медиана ~21.7к — весь город молча отбрасывался как «нерыночный».

Измерено на проде (47 253 не-ЕКБ сделки, 369 городов):

Порог Городов Сделок Доля
≥30 (текущий) 80 45 988 97.3%
15–29 21 460 1.0%
10–14 21 247 0.5%
1–9 247 558 1.2%

289 городов (1265 сделок) раньше уходили на екатеринбургский порог — теперь получают честный бэнд.

Трёхуровневая схема (SQL 194, ADD COLUMN tier):

  • full (N≥30) — прежняя деривация из 178, байт-в-байт; ЕКБ и текущие 80 городов не меняются.
  • rough (10≤N<30) — свой нижний порог (p1), потолок фиксирован 800000: p99 на выборке <30 точек статистически нестабилен, одна дорогая сделка исказила бы верх.
  • region_fallback (1≤N<10) — pooled p1 по всей области = 15 263 ₽/м² вместо ЕКБ-калиброванных 50 000.

Плюс SQL 195 (seed расписания, окно 07:00–08:00 UTC — свободный слот, после rosreestr_dkp_import и asking_to_sold_ratio_refresh), задача deal_city_price_bands_refresh.py (по образцу asking_to_sold_ratio.py, без DELETE — множество городов растёт монотонно) и регистрация обработчика.

Валидация на scratch-БД в прод-контейнере: синтетика на границах тиров (N=5/9/10/15/29/30), ЕКБ, не-rosreestr источник, NULL-цены и NULL-город — все исключения отработали; прогон дважды (идемпотентность: tier already exists, skipping, counts стабильны, ровно 1 CHECK-constraint); scratch-БД удалена, боевая только читалась.

Порядок деплоя: 194 → 195. 194 безопасна сама по себе и расширяет покрытие сразу; 195 нужна вместе с Python-кодом (иначе планировщик не найдёт обработчик — но не раньше окна 07:00 UTC).

Эпик #2576, этап B. Тихий дефект: `deal_city_price_bands` строилась разово (SQL 178) только для городов с ≥30 сделок; остальные падали на глобальный `DEAL_MIN_PPM2=50000`, откалиброванный по ЕКБ. В Североуральске медиана ~21.7к — **весь город молча отбрасывался как «нерыночный»**. **Измерено на проде** (47 253 не-ЕКБ сделки, 369 городов): | Порог | Городов | Сделок | Доля | |---|---|---|---| | ≥30 (текущий) | 80 | 45 988 | 97.3% | | 15–29 | 21 | 460 | 1.0% | | 10–14 | 21 | 247 | 0.5% | | 1–9 | 247 | 558 | 1.2% | 289 городов (1265 сделок) раньше уходили на екатеринбургский порог — теперь получают честный бэнд. **Трёхуровневая схема** (SQL 194, `ADD COLUMN tier`): - `full` (N≥30) — прежняя деривация из 178, байт-в-байт; ЕКБ и текущие 80 городов не меняются. - `rough` (10≤N<30) — свой нижний порог (p1), потолок **фиксирован** 800000: p99 на выборке <30 точек статистически нестабилен, одна дорогая сделка исказила бы верх. - `region_fallback` (1≤N<10) — pooled p1 по всей области = **15 263 ₽/м²** вместо ЕКБ-калиброванных 50 000. Плюс SQL 195 (seed расписания, окно 07:00–08:00 UTC — свободный слот, после `rosreestr_dkp_import` и `asking_to_sold_ratio_refresh`), задача `deal_city_price_bands_refresh.py` (по образцу `asking_to_sold_ratio.py`, без DELETE — множество городов растёт монотонно) и регистрация обработчика. **Валидация на scratch-БД** в прод-контейнере: синтетика на границах тиров (N=5/9/10/15/29/30), ЕКБ, не-rosreestr источник, NULL-цены и NULL-город — все исключения отработали; прогон дважды (идемпотентность: `tier already exists, skipping`, counts стабильны, ровно 1 CHECK-constraint); scratch-БД удалена, боевая только читалась. Порядок деплоя: 194 → 195. 194 безопасна сама по себе и расширяет покрытие сразу; 195 нужна вместе с Python-кодом (иначе планировщик не найдёт обработчик — но не раньше окна 07:00 UTC).
lekss361 added 1 commit 2026-07-31 13:35:36 +00:00
feat(tradein/db): авто-refresh ценовых бэндов по городам + порог для малых городов (#2576)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 13s
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 2m21s
f44ed4043c
Миграция 178 засеяла deal_city_price_bands разово (N>=30 сделок), без scheduler'а
на refresh. Город без строки падал на глобальный DEAL_MIN_PPM2=50000 (ЕКБ-калибровка)
— для дешёвых городов области это не anti-outlier guard, а cut-off легитимного рынка
(Североуральск median ~21.7k). Замер по прод-данным: 289 из 369 не-ЕКБ городов
(1265 сделок) не имели строки и падали на ЕКБ-порог.

- 194_deal_city_price_bands_tiers.sql — трёхуровневая схема (tier колонка):
  full (N>=30, own p1/p99, unchanged) / rough (N 10-29, own p1 floor + фикс.
  ceiling 800000) / region_fallback (N 1-9, pooled областной p1=15263 вместо
  ЕКБ-порога). Екатеринбург по-прежнему не в таблице — estimator fallback
  byte-identical.
- 195_scrape_schedules_seed_deal_city_price_bands_refresh.sql — scrape_schedules
  row, окно 07:00-08:00 UTC (после rosreestr_dkp_import + asking_to_sold_ratio_refresh).
- app/tasks/deal_city_price_bands_refresh.py — периодический re-derive (kit-scheduler,
  byte-identical 194 derivation), без DELETE (множество городов монотонно растёт).
- app/services/product_handlers.py — регистрация Handler для нового source.

Валидация: scratch-БД (syntax_check) в прод-контейнере, synthetic данные на
границах тиров (N=9/10/29/30) + Екатеринбург/non-rosreestr/NULL exclusion, оба
файла применены дважды (идемпотентность подтверждена), scratch-БД удалена.
bot-backend added 1 commit 2026-07-31 13:57:07 +00:00
test(tradein): добавить deal_city_price_bands_refresh в паритет-тест планировщика (#2576)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 10s
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 2m15s
3595cb603f
_PRODUCT_SOURCES в test_scraper_kit_scheduler_parity.py — ручной canonical-список
(файл сам предписывает обновлять его при изменении product_handlers.py, #2397 Part C
уже чинил ровно эту протухшесть). Новый source из PR #2579 забыт — без него
test_real_build_product_handlers_covers_all_scheduled_sources не покрывает
регистрацию Handler'а: пропажа entry в product_handlers.py осталась бы незамечена,
а kit-scheduler логировал бы "unknown source, skip" каждый тик без выполнения задачи.

Проверено: тест падает при временном откате Handler-registration (AssertionError
misses source=deal_city_price_bands_refresh), проходит после восстановления.
lekss361 merged commit e1935b609f into main 2026-07-31 14:01:35 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2579
No description provided.