Пересчитать хедонические коэффициенты поверх area-бакетного ratio — площадь штрафуется дважды, 4+ комнат занижены на 21% #3248

Closed
opened 2026-08-29 19:36:03 +00:00 by lekss361 · 0 comments
Owner

Симптом

Бэктест по проду (ЕКБ, --sample 1500 --city Екатеринбург --resolve-house-id, n=1191) даёт монотонный градиент ошибки по размеру квартиры:

студия 4+
median bias +9.03% −0.78% −2.67% −5.94% −21.47% (n=44)

Занижение на выкупе = потерянные сделки. Разрез по комнатности, в отличие от разреза по ценовым сегментам, НЕ страдает регрессией к среднему (комнатность независима от исхода), поэтому число настоящее.

Причина

Выкуп считается в _price_from_inputs (backend/app/services/estimator.py, ~стр. 3655):

expected = median_price × ratio[area_bucket(area)] × exp(b0 + 0.1220·yr − 0.1603·ln(area) + ...)

Площадь входит ДВАЖДЫ — и в ratio, и в хедонику. Их калибровали порознь, с разрывом в шесть недель:

механизм когда чем ключевался ratio на тот момент
хедоника estimate_hedonic_larea_coef = −0.1603 (#2002) 2026-06-27 по КОМНАТАМ
area-бакеты в asking→sold (#2620, коммит 90332e98) 2026-08-05 по ПЛОЩАДИ

Хедоника по определению чинит ОСТАТОК log(actual_sold / expected_sold), то есть её коэффициент справедлив только для той базы, на которой фитился. #2620 подменил базу — поставил под хедонику ratio с крутым U-образным площадным профилем — а хедонику не пересчитал.

Живая таблица asking_to_sold_ratios на проде:

бакет площадь ratio скидка n_deals
0 <30 м² 0.7742 −22.6% 2249
1 30-44 0.8648 −13.5% 6243
2 44-62 0.9362 −6.4% 4928
3 62-85 0.9313 −6.9% 2954
4 ≥85 м² 0.8211 −17.9% 2156

Для 108 м² (медиана 4-комнатных в ЕКБ, 100% попадают в бакет 4) получается 0.8211 × ≈0.87 ≈ −28% к asking-медиане. Для 50 м² — 0.9362 × ≈0.99 ≈ −7.5%.

Изоляция причины

Реплей замороженной фикстуры (--from-fixture, ZERO DB, n=277): комплы, якоря и всё прочее идентичны в обеих руках, меняется ТОЛЬКО таблица ratio.

ratio overall bias MAPE студия 4+
захваченные (до #2620) −3.74 12.63 +17.0 −4.6 −11.7 −4.0 −1.3
сегодняшние прод (area-бакеты) +0.75 13.98 +22.1 +7.5 −2.0 −7.5 −18.2

Одна смена ratio, без всего остального, роняет 4+ с −1.3% до −18.2% и поднимает общий MAPE на 1.35 п.п. Второй независимый замер (прогон по БД, n=1191) даёт −21.47% — знак и порядок те же.

Почему нельзя обойтись одной ручкой

Свип estimate_hedonic_larea_coef на сегодняшних ratio:

larea overall MAPE студия 4+
−0.1603 (текущий) 13.98 +22.1 +7.5 −7.5 −18.2
−0.1200 15.14 +32.8 +17.3 −1.1 −1.8
−0.0800 18.40 +47.4 +19.6 +1.6 +3.4
0.0 18.87 +70.3 +20.4 +1.6 +5.1

Чувствительность к коэффициенту растёт с ln(area), поэтому послабление чинит 4+ (+16.4 п.п.), но разгоняет студии (+10.7 п.п.) и общий MAPE. Крутить одну ручку — менять один класс комнат на другой.

Что сделать

  1. Повторить фит #2002log(actual_sold / expected_sold) ~ (year−2000)/20 + ln(area) + [floor==1] — на ТЕКУЩЕЙ базе, где expected_sold уже содержит area-бакетный ratio. Обновить estimate_hedonic_b0 / _year_coef / _larea_coef / _first_floor_coef совместно, не по одному.
  2. Отдельно оценить, не стоит ли сгладить сам U-образный профиль ratio: бакеты 0 и 4 дисконтируются втрое сильнее середины. Это может быть настоящей ликвидностью, а может — тонкими бакетами.
  3. Перезахватить регрессионный baseline: --from-fixture --update-baseline (tests/fixtures/backtest_baseline.json). Сдвиг ожидаем, это не регрессия.

Критерий приёмки

На прогоне по БД (ЕКБ, sample ≥1200, --resolve-house-id):

  • 4+ bias по модулю ≤ 8% (сейчас −21.47%)
  • ни один класс комнат не уходит за ±12% по модулю (сейчас студия +9.03, 4+ −21.47)
  • overall MAPE не хуже текущих 14.17%

Проверять чем

docker exec tradein-backend python -m scripts.backtest_estimator \n  --city 'Екатеринбург' --sample 1500 --since 2025-06-01 --resolve-house-id --json

Плюс офлайн-свип на фикстуре: replay_fixture НЕ читает settings_at_capture, цены считаются на живых дефолтах estimator.settings, поэтому подмена настройки в процессе работает.

Оговорка

Фикстура захвачена до #2620, её комплы отражают то состояние. Изоляция внутри неё корректна (обе руки на одних комплах), но абсолютные уровни — не прод. Несущее число берётся из прогона по БД: 4+ = −21.47% при n=44. Перед правкой стоит перезахватить фикстуру.

Не путать

Это НЕ то же, что «занижение бизнес-сегмента» из estimate_segment_multipliers (#2255). Тот перекос — артефакт группировки ошибок по SOLD ₽/м², то есть по самой предсказываемой величине; контрольный предиктор без перекоса по построению даёт в тех же корзинах −17.04% (бизнес) и −30.06% (элит). Разрез по комнатности этим не затронут.

## Симптом Бэктест по проду (ЕКБ, `--sample 1500 --city Екатеринбург --resolve-house-id`, n=1191) даёт монотонный градиент ошибки по размеру квартиры: | | студия | 1к | 2к | 3к | 4+ | |---|---|---|---|---|---| | median bias | +9.03% | −0.78% | −2.67% | −5.94% | **−21.47%** (n=44) | Занижение на выкупе = потерянные сделки. Разрез по комнатности, в отличие от разреза по ценовым сегментам, НЕ страдает регрессией к среднему (комнатность независима от исхода), поэтому число настоящее. ## Причина Выкуп считается в `_price_from_inputs` (`backend/app/services/estimator.py`, ~стр. 3655): ``` expected = median_price × ratio[area_bucket(area)] × exp(b0 + 0.1220·yr − 0.1603·ln(area) + ...) ``` Площадь входит ДВАЖДЫ — и в `ratio`, и в хедонику. Их калибровали порознь, с разрывом в шесть недель: | механизм | когда | чем ключевался ratio на тот момент | |---|---|---| | хедоника `estimate_hedonic_larea_coef = −0.1603` (#2002) | **2026-06-27** | по КОМНАТАМ | | area-бакеты в asking→sold (#2620, коммит `90332e98`) | **2026-08-05** | по ПЛОЩАДИ | Хедоника по определению чинит ОСТАТОК `log(actual_sold / expected_sold)`, то есть её коэффициент справедлив только для той базы, на которой фитился. #2620 подменил базу — поставил под хедонику ratio с крутым U-образным площадным профилем — а хедонику не пересчитал. Живая таблица `asking_to_sold_ratios` на проде: | бакет | площадь | ratio | скидка | n_deals | |---|---|---|---|---| | 0 | <30 м² | 0.7742 | −22.6% | 2249 | | 1 | 30-44 | 0.8648 | −13.5% | 6243 | | 2 | 44-62 | 0.9362 | −6.4% | 4928 | | 3 | 62-85 | 0.9313 | −6.9% | 2954 | | 4 | ≥85 м² | 0.8211 | **−17.9%** | 2156 | Для 108 м² (медиана 4-комнатных в ЕКБ, 100% попадают в бакет 4) получается `0.8211 × ≈0.87 ≈ −28%` к asking-медиане. Для 50 м² — `0.9362 × ≈0.99 ≈ −7.5%`. ## Изоляция причины Реплей замороженной фикстуры (`--from-fixture`, ZERO DB, n=277): комплы, якоря и всё прочее идентичны в обеих руках, меняется ТОЛЬКО таблица ratio. | ratio | overall bias | MAPE | студия | 1к | 2к | 3к | 4+ | |---|---|---|---|---|---|---|---| | захваченные (до #2620) | −3.74 | **12.63** | +17.0 | −4.6 | −11.7 | −4.0 | **−1.3** | | сегодняшние прод (area-бакеты) | +0.75 | **13.98** | +22.1 | +7.5 | −2.0 | −7.5 | **−18.2** | Одна смена ratio, без всего остального, роняет 4+ с −1.3% до −18.2% и поднимает общий MAPE на 1.35 п.п. Второй независимый замер (прогон по БД, n=1191) даёт −21.47% — знак и порядок те же. ## Почему нельзя обойтись одной ручкой Свип `estimate_hedonic_larea_coef` на сегодняшних ratio: | larea | overall MAPE | студия | 1к | 3к | 4+ | |---|---|---|---|---|---| | −0.1603 (текущий) | 13.98 | +22.1 | +7.5 | −7.5 | **−18.2** | | −0.1200 | 15.14 | +32.8 | +17.3 | −1.1 | **−1.8** | | −0.0800 | 18.40 | +47.4 | +19.6 | +1.6 | +3.4 | | 0.0 | 18.87 | +70.3 | +20.4 | +1.6 | +5.1 | Чувствительность к коэффициенту растёт с `ln(area)`, поэтому послабление чинит 4+ (+16.4 п.п.), но разгоняет студии (+10.7 п.п.) и общий MAPE. Крутить одну ручку — менять один класс комнат на другой. ## Что сделать 1. Повторить фит #2002 — `log(actual_sold / expected_sold) ~ (year−2000)/20 + ln(area) + [floor==1]` — на ТЕКУЩЕЙ базе, где `expected_sold` уже содержит area-бакетный ratio. Обновить `estimate_hedonic_b0` / `_year_coef` / `_larea_coef` / `_first_floor_coef` совместно, не по одному. 2. Отдельно оценить, не стоит ли сгладить сам U-образный профиль ratio: бакеты 0 и 4 дисконтируются втрое сильнее середины. Это может быть настоящей ликвидностью, а может — тонкими бакетами. 3. Перезахватить регрессионный baseline: `--from-fixture --update-baseline` (`tests/fixtures/backtest_baseline.json`). Сдвиг ожидаем, это не регрессия. ## Критерий приёмки На прогоне по БД (ЕКБ, sample ≥1200, `--resolve-house-id`): - `4+` bias по модулю ≤ 8% (сейчас −21.47%) - ни один класс комнат не уходит за ±12% по модулю (сейчас студия +9.03, 4+ −21.47) - overall MAPE не хуже текущих 14.17% ## Проверять чем ``` docker exec tradein-backend python -m scripts.backtest_estimator \n --city 'Екатеринбург' --sample 1500 --since 2025-06-01 --resolve-house-id --json ``` Плюс офлайн-свип на фикстуре: `replay_fixture` НЕ читает `settings_at_capture`, цены считаются на живых дефолтах `estimator.settings`, поэтому подмена настройки в процессе работает. ## Оговорка Фикстура захвачена до #2620, её комплы отражают то состояние. Изоляция внутри неё корректна (обе руки на одних комплах), но абсолютные уровни — не прод. Несущее число берётся из прогона по БД: 4+ = −21.47% при n=44. Перед правкой стоит перезахватить фикстуру. ## Не путать Это НЕ то же, что «занижение бизнес-сегмента» из `estimate_segment_multipliers` (#2255). Тот перекос — артефакт группировки ошибок по SOLD ₽/м², то есть по самой предсказываемой величине; контрольный предиктор без перекоса по построению даёт в тех же корзинах −17.04% (бизнес) и −30.06% (элит). Разрез по комнатности этим не затронут.
lekss361 added the
bug
data
priority/p1
scope/backend
status/ready
tradein
вторичка
labels 2026-08-29 19:36:14 +00:00
Sign in to join this conversation.
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#3248
No description provided.