feat(tradein): позиция квартиры внутри когорты аналогов — перцентиль (#2899) #2926
Merged
bot-backend
merged 2 commits from 2026-08-19 10:08:19 +00:00
feat/2899-market-percentile into main
2 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 9f17983689 |
fix(tradein): миграция 267 — lock_timeout, как требует гейт #2752
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m36s
Гейт `check-migration-lock-timeout.py` в ci.yml поймал правильно: `ADD COLUMN` без `SET LOCAL lock_timeout` — блокирующий DDL. Мой комментарий в первой версии был вдобавок обманчив: «блокировки не берёт, ADD COLUMN без DEFAULT не переписывает таблицу». Переписывания действительно нет, но ACCESS EXCLUSIVE берётся, и опасно не удержание лока, а ОЖИДАНИЕ его выдачи — ждущий DDL встаёт в очередь перед новыми запросами приложения к той же таблице. Так миграция 250 простояла 29 минут за чужой psql-сессией. Переписал комментарий под то, что происходит на самом деле. Refs #2899, #2752 |
|||
| 2c852bd13b |
feat(tradein): позиция квартиры внутри когорты аналогов (перцентиль)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Failing after 8s
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 4m31s
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
Макет показывает бейдж «Верх рынка» — место квартиры в распределении аналогов. Поля не было ни в схеме, ни в расчёте. ГДЕ СЧИТАЕТСЯ В estimate_quality, в блоке display_pool — по той же когорте, что дала headline (anchor_comps_used либо ценовые listings_clean), и по УЖЕ финальной median_ppm2. Внутри _price_from_inputs считать нельзя: там цена ещё не финальная (восемь мутаторов — repair_coef, anchor override, IMV-blend, quarter index, corridor clamp, radius floor, deals fallback, segment multiplier — отрабатывают позже), а n_analogs после точки расчёта перезаписывается трижды. Перцентиль разошёлся бы с показанной ценой у двух третей оценок: 733 из 1086 прод-строк идут якорным путём. Пул берётся ДО _dedup_display_lots: дедуп режет выборку под показ (на проде 15 карточек из 528 усечены именно им), а позиция должна считаться по популяции, из которой взята цена. Формула — перцентиль РАНГА (midrank: доля дешевле плюс половина равных), а не _percentile: тот решает обратную задачу «какая цена стоит на заданной доле». Края зажаты в 1..99 — «0-й перцентиль» читался бы как «дешевле всех на свете», хотя означает лишь край выборки из двадцати лотов. ПОРОГ 15, А НЕ 5 Замер 19.08 (Монте-Карло на 54 прод-когортах размера 62..125, с поправкой на конечную популяцию): k=5 → ошибка ранга 13.4 пп и ярлык перепутан в 26.4% случаев; k=15 → 7.6 пп; k=20 → 6.7 пп ценой ещё 10 пп покрытия. Второй сигнал: шаг перцентиля 100/n против ценового разрыва между соседями — при n≈7 один соседний лот двигает ярлык на целую категорию. HEADLINE_LISTINGS_MIN_N=5 переиспользовать нельзя: он отвечает на другой вопрос — устойчива ли МЕДИАНА, а она устойчива там, где ранг ещё пляшет. Ниже порога поле остаётся None и UI не рисует ничего. Отдельную надпись «мало данных» не добавляю — это уже говорят reliability и relaxations. ПЕРСИСТ ОБЯЗАТЕЛЕН Пересчитать на GET нельзя: когорты в БД нет, в analogs лежит top-10 (прод: max(jsonb_array_length(analogs))=10 при max(n_analogs)=294). Отсюда миграция 267 и проброс через ОБА SELECT'а — у GET и PDF списки колонок разные и живут в разных функциях. getattr в конструкторах — тот же defensive-идиом, что у relaxations/reliability рядом: строка без колонки деградирует в None, а не роняет ответ. ЧЕМ ЭТО НЕ ЯВЛЯЕТСЯ Не location_index_pct (тот про район против медианы города, живёт на другой ручке и в цену не идёт), не квантили ДКП-коридора, не price_premium_label. ТЕСТЫ Двусторонние: против main падают импорт нового модуля тестов (константы нет) и test_get_estimate_surfaces_market_percentile. Контроли зелёные с обеих сторон: nullable-поле проходит как null, лоты без цены не засчитываются в порог. Хунк форматирования — не мой: pre-commit ruff v0.7.4 против 0.15.12 (#2864). Refs #2899 |