Коэффициент asking→sold приводится к сегодняшнему дню #3544

Merged
lekss361 merged 1 commit from fix/ratio-time-adjust into main 2026-09-16 20:05:19 +00:00
Owner

Проблема

Числитель asking_to_sold_ratios — сырые цены сделок Росреестра за трейлинг-12 месяцев (на проде фактическое окно 2025-10…2026-04, центр ≈ декабрь 2025). Знаменатель — медиана объявлений за LISTINGS_FRESH_DAYS, то есть сегодня. На растущем рынке ratio занижен ровно на дрейф рынка за половину окна, и прод отдаёт цену позапрошлого квартала.

Это и есть «систематическая недооценка на свежих сделках» из отчёта тестировщика. Зависимость от возраста сделки (Москва): сделки с 2026-03 → −12.7 %, с 2025-06 → −3.4 %, с 2024-06 → +1.8 % — подпись сдвига по времени, а не перекоса модели.

Отдельно: estimator._fetch_dkp_corridor уже приводит ДКП-коридор к сегодня (#794), а ratio — нет. Две стороны одного расчёта стояли на разных временных шкалах.

Что сделано

Каждая сделка домножается на sber-фактор приведения к последнему месяцу ряда: idx[последний месяц] / idx[месяц сделки], clamp SBER_TIME_FACTOR_MIN/MAX (0.7/1.6). Ряд выбирается той же картой регион→ряд (sber_region_series_name) и тем же приоритетом дашбордов (SBER_COEFF_DASHBOARDS), что у эстиматора — вторая карта не заводится. Применено к обоим путям деривации: ЕКБ (deal_side / deal_global) и региональному гео-взвешенному (deal_geo → весь Ласпейрес проезжает приведение автоматически).

Двойного учёта нет — проверено

_sber_time_factor в эстиматоре зовётся ровно в двух местах, оба внутри _fetch_dkp_corridor (estimator.py:2219, :2291). Коридор оттуда уходит в Tier-C anchor gate (:3569) и в _apply_corridor_clamp (:3989) — то есть работает потолком для ASKING-медианы. Множитель из asking_to_sold_ratios применяется отдельно и позже: expected_sold_per_m2 = median_ppm2 * effective_ratio (estimator.py:4058). Один множитель на одно число дважды не ложится; после правки обе стороны просто оказываются на одной шкале.

quarter_index / _apply_quarter_index — поправка пространственная (кадастровый квартал), к вопросу не относится.

Замер на проде (read-only, 2026-09-16)

Окно сделок 2025-10…2026-04, ряд real_estate_deals своего региона, clamp 0.7/1.6:

регион n сделок sold_median сырой приведённый сдвиг factor min…max
77 Москва 68 731 323 529 360 330 ×1.1137 1.077…1.147
50 область 35 264 162 303 179 314 ×1.1048 1.065…1.138
66 Свердловская 33 907 94 655 99 059 ×1.0465 1.032…1.062

Ожидаемые ratio после первого пересчёта: 0.729 → 0.811 (77), 0.811 → 0.896 (50), 0.728 → 0.762 (66). Все ниже 1.0 — клампа estimate_expected_sold_le_asking не задевают.

Как это увидит бэктест

Бэктест меряет предсказание против СЫРЫХ исторических цен сделок, поэтому его bias после включения уедет в плюс примерно на те же 4-11 %. Это не регресс: прод обязан отдавать цену сегодняшнего рынка, а таргет бэктеста — цена декабря 2025. Инструмент по построению не видит этот дефект, поэтому и не может его подтвердить.

Замороженный регресс-гейт не двигается: фикстура несёт готовый ratio и через этот таск не ходит — проверено прогоном.

Откат и деградация

  • ASKING_RATIO_TIME_ADJUST_ENABLED=false → factor = 1.0 для каждой сделки, байт-в-байт прежнее поведение, без деплоя.
  • Ряда для города нет вовсе → сделка НЕ выбрасывается: factor = 1.0 плюс счётчик sber_time_adjust_regions_missing_series и WARNING. Молчаливой деградации нет.
  • Месяц сделки новее последнего месяца ряда → 1.0, вперёд не экстраполируем (симметрично estimator._sber_time_factor).
  • Точки ровно на месяц сделки нет → ближайший более ранний месяц (то же правило, что в эстиматоре).

Тесты

tests/test_asking_to_sold_ratio.py tests/test_estimator_sber_time_adjust.py tests/test_ratio_geography_match_3529.py tests/test_estimator_ratio_per_region_3512.py tests/test_estimator_pi_by_region.py tests/test_backtest_regression_gate.py tests/test_estimator_requires_analogs.py69 passed. ruff check / ruff format чисто, CAST(:x AS type) соблюдён.

Два структурных теста обновлены под намеренное изменение SQL: нормализатор _drop_sber_time_adjust в проверке «080-сид ⊆ refresh SQL» (снимает новые CTE/JOIN дословной подстановкой констант, не regex) и счётчик exec-вызовов у fake-db (+1 presence-check на регион).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs

## Проблема Числитель `asking_to_sold_ratios` — сырые цены сделок Росреестра за трейлинг-12 месяцев (на проде фактическое окно 2025-10…2026-04, центр ≈ декабрь 2025). Знаменатель — медиана объявлений за `LISTINGS_FRESH_DAYS`, то есть сегодня. На растущем рынке ratio занижен ровно на дрейф рынка за половину окна, и прод отдаёт цену позапрошлого квартала. Это и есть «систематическая недооценка на свежих сделках» из отчёта тестировщика. Зависимость от возраста сделки (Москва): сделки с 2026-03 → −12.7 %, с 2025-06 → −3.4 %, с 2024-06 → +1.8 % — подпись сдвига по времени, а не перекоса модели. Отдельно: `estimator._fetch_dkp_corridor` уже приводит ДКП-коридор к сегодня (#794), а ratio — нет. Две стороны одного расчёта стояли на разных временных шкалах. ## Что сделано Каждая сделка домножается на sber-фактор приведения к последнему месяцу ряда: `idx[последний месяц] / idx[месяц сделки]`, clamp `SBER_TIME_FACTOR_MIN/MAX` (0.7/1.6). Ряд выбирается той же картой регион→ряд (`sber_region_series_name`) и тем же приоритетом дашбордов (`SBER_COEFF_DASHBOARDS`), что у эстиматора — вторая карта не заводится. Применено к обоим путям деривации: ЕКБ (`deal_side` / `deal_global`) и региональному гео-взвешенному (`deal_geo` → весь Ласпейрес проезжает приведение автоматически). ## Двойного учёта нет — проверено `_sber_time_factor` в эстиматоре зовётся ровно в двух местах, оба внутри `_fetch_dkp_corridor` (`estimator.py:2219`, `:2291`). Коридор оттуда уходит в Tier-C anchor gate (`:3569`) и в `_apply_corridor_clamp` (`:3989`) — то есть работает потолком для ASKING-медианы. Множитель из `asking_to_sold_ratios` применяется отдельно и позже: `expected_sold_per_m2 = median_ppm2 * effective_ratio` (`estimator.py:4058`). Один множитель на одно число дважды не ложится; после правки обе стороны просто оказываются на одной шкале. `quarter_index` / `_apply_quarter_index` — поправка пространственная (кадастровый квартал), к вопросу не относится. ## Замер на проде (read-only, 2026-09-16) Окно сделок 2025-10…2026-04, ряд `real_estate_deals` своего региона, clamp 0.7/1.6: | регион | n сделок | sold_median сырой | приведённый | сдвиг | factor min…max | |---|---|---|---|---|---| | 77 Москва | 68 731 | 323 529 | 360 330 | ×1.1137 | 1.077…1.147 | | 50 область | 35 264 | 162 303 | 179 314 | ×1.1048 | 1.065…1.138 | | 66 Свердловская | 33 907 | 94 655 | 99 059 | ×1.0465 | 1.032…1.062 | Ожидаемые ratio после первого пересчёта: **0.729 → 0.811** (77), **0.811 → 0.896** (50), **0.728 → 0.762** (66). Все ниже 1.0 — клампа `estimate_expected_sold_le_asking` не задевают. ## Как это увидит бэктест Бэктест меряет предсказание против СЫРЫХ исторических цен сделок, поэтому его bias после включения уедет в плюс примерно на те же 4-11 %. Это не регресс: прод обязан отдавать цену сегодняшнего рынка, а таргет бэктеста — цена декабря 2025. Инструмент по построению не видит этот дефект, поэтому и не может его подтвердить. Замороженный регресс-гейт не двигается: фикстура несёт готовый ratio и через этот таск не ходит — проверено прогоном. ## Откат и деградация - `ASKING_RATIO_TIME_ADJUST_ENABLED=false` → factor = 1.0 для каждой сделки, байт-в-байт прежнее поведение, без деплоя. - Ряда для города нет вовсе → сделка НЕ выбрасывается: factor = 1.0 плюс счётчик `sber_time_adjust_regions_missing_series` и WARNING. Молчаливой деградации нет. - Месяц сделки новее последнего месяца ряда → 1.0, вперёд не экстраполируем (симметрично `estimator._sber_time_factor`). - Точки ровно на месяц сделки нет → ближайший более ранний месяц (то же правило, что в эстиматоре). ## Тесты `tests/test_asking_to_sold_ratio.py tests/test_estimator_sber_time_adjust.py tests/test_ratio_geography_match_3529.py tests/test_estimator_ratio_per_region_3512.py tests/test_estimator_pi_by_region.py tests/test_backtest_regression_gate.py tests/test_estimator_requires_analogs.py` → **69 passed**. `ruff check` / `ruff format` чисто, `CAST(:x AS type)` соблюдён. Два структурных теста обновлены под намеренное изменение SQL: нормализатор `_drop_sber_time_adjust` в проверке «080-сид ⊆ refresh SQL» (снимает новые CTE/JOIN дословной подстановкой констант, не regex) и счётчик exec-вызовов у fake-db (+1 presence-check на регион). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs
lekss361 added 1 commit 2026-09-16 19:56:48 +00:00
fix(trade-in): привести SOLD-сторону коэффициента asking→sold к сегодняшнему дню
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
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 4m54s
0854fd8da4
Числитель ratio — сырые цены сделок Росреестра за трейлинг-12 месяцев (на проде
фактическое окно 2025-10..2026-04, центр ≈ декабрь 2025), знаменатель — медиана
объявлений за LISTINGS_FRESH_DAYS, то есть сегодня. На растущем рынке ratio
занижен ровно на дрейф за половину окна, и прод отдаёт цену позапрошлого
квартала. Это и есть «систематическая недооценка на свежих сделках».

Каждая сделка домножается на sber-фактор приведения к последнему месяцу ряда —
тот же приём и та же карта регион→ряд (sber_region_series_name), что estimator
уже применяет к ДКП-коридору (#794). До этой правки коридор был приведён к
сегодня, а ratio — нет; теперь обе стороны на одной временной шкале.

Двойного учёта нет: _sber_time_factor в estimator зовётся только в
_fetch_dkp_corridor (estimator.py:2219, 2291) и уходит в clamp ASKING-медианы
(_apply_corridor_clamp, estimator.py:3989); ratio из asking_to_sold_ratios
множится отдельно на estimator.py:4058. Один множитель на одно число дважды
не ложится.

Прод-замер 2026-09-16 (read-only, окно 2025-10..2026-04):
  77 Москва   n=68 731  sold_median 323 529 → 360 330  ×1.1137
  50 область  n=35 264  sold_median 162 303 → 179 314  ×1.1048
  66 Свердл.  n=33 907  sold_median  94 655 →  99 059  ×1.0465
Ожидаемые ratio: 0.729→0.811 (77), 0.811→0.896 (50), 0.728→0.762 (66) — все
ниже 1.0, клампа estimate_expected_sold_le_asking не задевают.

Откат без деплоя — ASKING_RATIO_TIME_ADJUST_ENABLED=false (factor=1.0 для
каждой сделки, байт-в-байт прежнее поведение). Если ряда для города нет вовсе,
сделка не выбрасывается: factor=1.0 плюс счётчик
sber_time_adjust_regions_missing_series и WARNING, а не тишина.

Замороженный регресс-гейт не двигается: фикстура несёт готовый ratio и через
этот таск не ходит.

Claude-Session: https://claude.ai/code/session_01NQb6WeJtagZwZnUsSjDizs
lekss361 merged commit 8ebd63780f into main 2026-09-16 20:05:19 +00:00
lekss361 deleted branch fix/ratio-time-adjust 2026-09-16 20:05:20 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#3544
No description provided.