МЕРА: срок продажи показываем окном «от–до» по снятым объявлениям и сохраняем его вместе с оценкой #3573
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3573
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/days-on-market-window"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
#2898 — срок экспозиции окном p25–p75 вместо одного числа
Что было
est_days_on_market— одно число: медианаdays_on_marketу активных аналогов, то есть сколько висят объявления, которые ещё продаются (выборка цензурирована).deals.days_on_marketпуст, так что сверить было не с чем.trade_in_estimatesзначение не сохранялось.GET /estimate/{id}и/r/{token}(платная ссылка) всегда отдавалиnull./docsв описании полного отчёта было обещание «прогноз срока».Улики (прод, 17.09, только чтение)
house_placement_history: срок экспозиции заполнен у avito_imv 44 094/44 094 и yandex_valuation 51 583/56 675. За 24 мес. снятых объявлений со сроком 30 462.exposure_days: у yandex это ровноremoved_date − publish_date + 1(разброс p10..p90 = 1). У avito то же самое, только срок округлён до периода размещения: 3 691 из 20 986 (17.6%) ровно 31 день, дальше пики на 62 и 93. Это срок до снятия объявления, а не продажи. Подписи в UI говорят именно так.Что сделано
_estimate_days_on_marketудалена. Новая_fetch_exposure_daysберёт срок экспозиции из снятых объявлений: те же комнаты, площадь ±15% (AREA_TOLERANCE), дома вsearch_radius_m(радиус подбора аналогов), снятие за последние 24 мес. и не позже сегодняшнего дня. Запрос обёрнут в SAVEPOINT, при ошибке окна просто нет._exposure_windowсчитает квартили так же, какpercentile_cont::int. Если объявлений меньшеEXPOSURE_WINDOW_MIN_N = 30, окна нет.est_days_p25/p50/p75/nвtrade_in_estimates. ADD COLUMN без DEFAULT,lock_timeout 5s, бэкфилла нет. Колонки пишутся в основной INSERT и в UPDATE при ревайвле мёртвой строки. GET читает их из колонок, без пересчёта, так что отчёт по ссылке показывает то окно, что было в момент расчёта.exposure_window {p25_days, p50_days, p75_days, n} | null.est_days_on_marketостаётся и теперь равно p50 окна. У старых строк оноnull, как и было на GET. Публичная бесплатная модель не меняется: она собрана по белому списку, а поле добавлено в_PAID_FIELDSтеста.HeroSummary: вместо «Срок продажи N дн.» теперь «До снятия объявления p25–p75 дн.», в подсказке n и оговорка, что снятие ≠ продажа. Без окна показываем «нет данных», старое число в UI больше не выводится./docs: «прогноз срока» заменён на «срок экспозиции похожих объявлений». Та же формулировка уже стоит в TwoPathsV3. Экрана платного отчёта в B2C пока нет,/r/{token}отдаёт JSON, и окно туда теперь попадает.Проверка по значению
percentile_cont::intна 4 когортах, например d0260644: n=228, окно 27/61/133, в SQL 27/61/133.tests/test_2898_exposure_window.py— 8 тестов. Квартили на выборке 2..31 посчитаны руками по формуле percentile_cont: (9, 17, 24). 17 проверяет округление .5 вверх, банковское дало бы 16. Порог: 29 значений — окна нет. POST пишет в INSERT ровно (9, 17, 24, 30), а при 29 — четыре NULL.test_estimate_idor.py— GET отдаёт окно из колонок, а на старой строкеnull.test_estimate_revival.py— UPDATE при ревайвле пишет окно.HeroSummaryExposureWindow.test.tsx— на экране «До снятия объявления29–146 дн.». Без окна «нет данных», а «48 дн.» не появляется.Тесты
pytest tests/test_2898_exposure_window.py tests/test_estimate_idor.py tests/test_public_mera_estimate.py tests/test_estimate_revival.py tests/test_estimate_consent_gate.py→ 91 passed, rc=0.test_3466_corridor_tier_a.py(2) иtest_estimator_radius_floor.py(2), причина'Settings' object has no attribute 'estimate_corridor_clamp_slack' / 'estimate_corridor_clamp_min_n'. На чистомorigin/maina130303cв отдельном worktree те же 4 падают так же (4 failed, 6 passed). К этой ветке они не относятся.ruff check app tests→ All checks passed.ruff format --checkпо изменённым файлам → 7 files already formatted.npx tsc --noEmitrc=0,npx vitest run→ 36 files, 312 tests passed, rc=0.Фальсификация
p50_days=round(p50),EXPOSURE_WINDOW_MIN_N = 20, в INSERTest_days_p75: None. В API на GETest_days_on_market=None. Результат: 6 failed, 42 passed, rc=1:est_days_p50: None→E assert [31, None, 121, 44] == [31, 58, 121, 44], rc=1.est_days_on_marketбез окна и показ одного p50 → 2 failed, rc=1:Каждый раз исходники восстанавливал из копии и сверял через
diff -q.Деплой
lock_timeout 5s.scrape_runs.status='running') и отдельно tradein-frontend. Перед мержем проверитьSELECT count(*) FROM scrape_runs WHERE status='running': 17.09 09:45 UTC было 0.Приёмка на проде (после деплоя, срок — 18.09.2026)
SELECT column_name FROM information_schema.columns WHERE table_name='trade_in_estimates' AND column_name LIKE 'est_days_%'→ 4 строки.created_atпосле деплоя, с координатами в ЕКБ)est_days_n >= 30примерно в 40% случаев. Все четыре колонки либо заполнены, либо NULL:count(*) FILTER (WHERE (est_days_p25 IS NULL) <> (est_days_n IS NULL)) = 0.GET /api/v1/trade-in/estimate/{id}по одной из них:exposure_windowсовпадает с колонками,est_days_on_market = est_days_p50.curl https://meraocenka.ru/docs— строки «прогноз срока» нет.Не сделано / вне scope
trade_in_pdf._days_on_market_range) «срок экспозиции» до сих пор берётся как min–maxdays_on_marketу аналогов, то есть по возрасту активных объявлений. Это отдельная правка.est_days_on_market. На поведение не влияют.Closes #2898
🤖 Generated with Claude Code
Было: est_days_on_market — одно число, медиана days_on_market активных аналогов (возраст висящего объявления, цензурированная выборка), в trade_in_estimates не сохранялось, GET по ссылке отдавал null. Стало: - эстиматор берёт exposure_days из house_placement_history: те же комнаты, площадь ±15%, дома в радиусе подбора аналогов, снятые за 24 мес.; квартили как percentile_cont; при выборке < 30 окна нет; - миграция 324: est_days_p25/p50/p75/n в trade_in_estimates, пишутся в INSERT и при ревайвле, поднимаются на GET (/estimate/{id} и /r/{token}); - API: exposure_window {p25_days, p50_days, p75_days, n}; старое поле est_days_on_market оставлено и равно p50 окна, у старых строк null; - B2B hero: «До снятия объявления N–M дн.» вместо «Срок продажи»; - /docs: убран «прогноз срока» из описания полного отчёта. Порог 30 — замер 17.09 на 51 прод-когорте: ошибка края окна при 20 лотах 24%, при 30 — 18%. Окно есть у 94 из 217 недавних оценок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>