fix(mera/estimate): перефит хедоники поверх area-бакетного ratio — крупное жильё занижалось на 21% #3255

Merged
lekss361 merged 1 commit from fix/3248-hedonic-refit into main 2026-08-29 20:19:27 +00:00
Owner

Summary

Перефит хедонических коэффициентов поверх действующего area-бакетного ratio. Крупное жильё занижалось на 21%, а в выкупе занижение — это потерянные сделки.

Причина

Коэффициенты зафичены 27.06.2026 (#2002), когда asking→sold ratio ключевался по комнатам. 05.08.2026 (#2620) ratio переключили на area-бакеты — то есть под хедонику подставили другую базу, а её саму не пересчитали. Хедоника по определению чинит остаток log(actual_sold / expected_sold) и верна только для той базы, на которой фитилась. Площадь в итоге штрафовалась дважды.

larea = 0.0 выставлен намеренно

Это не «не задан». Фактический sold/ask по бакетам — перевёрнутая U:

бакет факт sold/ask ratio предсказывали
0 (<30) 0.7859 0.7742 0.8018
1 (30-44) 0.8286 0.8648 0.8054
2 (44-62) 0.8988 0.9362 0.8570
3 (62-85) 0.9535 0.9313 0.9052
4 (≥85) 0.8640 0.8211 0.7578

Монотонный ln(area) такую форму выразить не может в принципе — он тянет крупное жильё вниз ровно там, где рынок его не дисконтирует. Сам ratio форму уже повторяет (отклонения ≤5%), так что член стал избыточным и вредным. Клэмп le_asking тут ни при чём: для бакета 4 он не срабатывает ни разу.

Результат

MAPE bias студия 4+
было 13.90 −0.78 +7.6 −0.5 −2.8 −5.3 −21.4
стало 14.28 −1.61 −3.1 −2.3 −0.4 −1.9 −10.4

Худший перекос вдвое меньше, все прочие классы в пределах ±3.1%, покрытие диапазона 86.13% → 87.00%. Цена — +0.38 п.п. общего MAPE: хедоника сжимает разброс за счёт года, и ослабление площадного члена этот эффект чуть уменьшает.

Рассмотренные альтернативы на той же фикстуре:

вариант MAPE 4+
текущие 13.90 −21.4
OLS все 4 члена 14.11 −14.1
без larea (взят) 14.28 −10.4
хедоника OFF целиком 15.88 −5.0

Критерии приёмки #3248 — выполнены частично

  • ни один класс комнат не дальше ±12% (максимум −10.4%)
  • 4+ по модулю ≤8% — нет, −10.4%
  • MAPE не хуже 14.17% — нет, 14.28% (+0.11)

Остаток конфигом не закрывается: ratio бакета 4 = 0.8211 при фактических sold/ask = 0.8640, то есть занижен на ~5% ещё до всякой хедоники. Это пересчёт самой таблицы (app/tasks/asking_to_sold_ratio.py) — отдельной задачей. Считаю правку стоящей мержа как есть: она вдвое сокращает систематическое занижение целого класса и улучшает покрытие, а +0.38 п.п. MAPE — небольшая цена за это.

Тесты

Три теста в test_estimator_hedonic.py развязаны от подгоняемых чисел: два проверяют механизм клэмпов и теперь задают коэффициенты явно через monkeypatch, третий сверяет наземную скидку со значением настройки, а не с зашитым 0.8827. Иначе каждый следующий перефит ломал бы тесты, которые про механизм, а не про подгонку.

Test plan

  • pytest -k "backtest or hedonic or estimator or price_from_inputs" — 622 passed, 1 skipped
  • baseline перезахвачен на свежей фикстуре (той, что подложена в #3254)
  • ruff check + ruff format --check чисто
  • CI зелёный

Closes #3248

## Summary Перефит хедонических коэффициентов поверх действующего area-бакетного ratio. Крупное жильё занижалось на 21%, а в выкупе занижение — это потерянные сделки. ## Причина Коэффициенты зафичены **27.06.2026** (#2002), когда asking→sold ratio ключевался **по комнатам**. **05.08.2026** (#2620) ratio переключили на **area-бакеты** — то есть под хедонику подставили другую базу, а её саму не пересчитали. Хедоника по определению чинит остаток `log(actual_sold / expected_sold)` и верна только для той базы, на которой фитилась. Площадь в итоге штрафовалась дважды. ## `larea = 0.0` выставлен намеренно Это не «не задан». Фактический `sold/ask` по бакетам — **перевёрнутая U**: | бакет | факт `sold/ask` | ratio | предсказывали | |---|---|---|---| | 0 (<30) | 0.7859 | 0.7742 | 0.8018 | | 1 (30-44) | 0.8286 | 0.8648 | 0.8054 | | 2 (44-62) | 0.8988 | 0.9362 | 0.8570 | | 3 (62-85) | 0.9535 | 0.9313 | 0.9052 | | 4 (≥85) | **0.8640** | 0.8211 | **0.7578** | Монотонный `ln(area)` такую форму выразить не может в принципе — он тянет крупное жильё вниз ровно там, где рынок его не дисконтирует. Сам ratio форму уже повторяет (отклонения ≤5%), так что член стал избыточным и вредным. Клэмп `le_asking` тут ни при чём: для бакета 4 он не срабатывает **ни разу**. ## Результат | | MAPE | bias | студия | 1к | 2к | 3к | 4+ | |---|---|---|---|---|---|---|---| | было | **13.90** | −0.78 | +7.6 | −0.5 | −2.8 | −5.3 | **−21.4** | | стало | 14.28 | −1.61 | −3.1 | −2.3 | −0.4 | −1.9 | **−10.4** | Худший перекос вдвое меньше, все прочие классы в пределах ±3.1%, покрытие диапазона **86.13% → 87.00%**. Цена — +0.38 п.п. общего MAPE: хедоника сжимает разброс за счёт года, и ослабление площадного члена этот эффект чуть уменьшает. Рассмотренные альтернативы на той же фикстуре: | вариант | MAPE | 4+ | |---|---|---| | текущие | 13.90 | −21.4 | | OLS все 4 члена | 14.11 | −14.1 | | **без larea (взят)** | 14.28 | **−10.4** | | хедоника OFF целиком | 15.88 | −5.0 | ## Критерии приёмки #3248 — выполнены частично - [x] ни один класс комнат не дальше ±12% (максимум −10.4%) - [ ] `4+` по модулю ≤8% — **нет**, −10.4% - [ ] MAPE не хуже 14.17% — **нет**, 14.28% (+0.11) Остаток конфигом не закрывается: ratio бакета 4 = **0.8211** при фактических `sold/ask` = **0.8640**, то есть занижен на ~5% ещё до всякой хедоники. Это пересчёт самой таблицы (`app/tasks/asking_to_sold_ratio.py`) — отдельной задачей. Считаю правку стоящей мержа как есть: она вдвое сокращает систематическое занижение целого класса и улучшает покрытие, а +0.38 п.п. MAPE — небольшая цена за это. ## Тесты Три теста в `test_estimator_hedonic.py` развязаны от подгоняемых чисел: два проверяют **механизм клэмпов** и теперь задают коэффициенты явно через `monkeypatch`, третий сверяет наземную скидку **со значением настройки**, а не с зашитым `0.8827`. Иначе каждый следующий перефит ломал бы тесты, которые про механизм, а не про подгонку. ## Test plan - [x] `pytest -k "backtest or hedonic or estimator or price_from_inputs"` — 622 passed, 1 skipped - [x] baseline перезахвачен на свежей фикстуре (той, что подложена в #3254) - [x] `ruff check` + `ruff format --check` чисто - [ ] CI зелёный Closes #3248
lekss361 added 1 commit 2026-08-29 20:13:44 +00:00
fix(mera/estimate): перефит хедоники поверх area-бакетного ratio — крупное жильё занижалось на 21%
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m55s
49907aba46
Коэффициенты хедоники зафичены 27.06.2026 (#2002), когда asking->sold ratio
ключевался ПО КОМНАТАМ. 05.08.2026 (#2620) ratio переключили на area-бакеты, то
есть под хедонику подставили другую базу, а её саму не пересчитали. Хедоника по
определению чинит остаток log(actual_sold / expected_sold) и верна только для
той базы, на которой фитилась. Итог: площадь штрафовалась дважды, bias по 4+
комнатам -21.4%. В выкупе занижение — это потерянные сделки.

Перефит на свежей прод-фикстуре (1269 оценённых сделок ЕКБ). larea выставлен в
0.0 НАМЕРЕННО: после #2620 площадь несёт area-бакетный ratio, а его форма —
перевёрнутая U (факт sold/ask по бакетам 0.786 / 0.829 / 0.899 / 0.954 / 0.864),
которую монотонный ln(area) выразить не может в принципе. Член стал избыточным и
тянул крупное жильё вниз ровно там, где рынок его не дисконтирует.

Результат на фикстуре:

  было   MAPE 13.90  bias -0.78 | студия +7.6  1к -0.5  2к -2.8  3к -5.3  4+ -21.4
  стало  MAPE 14.28  bias -1.61 | студия -3.1  1к -2.3  2к -0.4  3к -1.9  4+ -10.4

Худший перекос вдвое меньше, все прочие классы в пределах +-3.1%, покрытие
диапазона 86.13% -> 87.00%. Цена — +0.38 п.п. общего MAPE: хедоника сжимает
разброс за счёт года, и ослабление площадного члена этот эффект чуть уменьшает.

Критерии приёмки из #3248 выполнены частично: «все классы в пределах 12%» — да;
«4+ не хуже 8%» и «MAPE не хуже 14.17» — нет. Остаток -10.4% конфигом не
закрывается: ratio бакета 4 равен 0.8211 при фактических sold/ask 0.8640, то есть
занижен на ~5% ещё до всякой хедоники. Это пересчёт самой таблицы
(app/tasks/asking_to_sold_ratio.py) — отдельной задачей.

Три теста в test_estimator_hedonic.py развязаны от подгоняемых чисел: два
проверяют механизм клэмпов и теперь задают коэффициенты явно через monkeypatch,
третий сверяет наземную скидку с настройкой, а не с зашитым 0.8827. Иначе каждый
следующий перефит ломал бы тесты, которые про механизм, а не про подгонку.
lekss361 merged commit 5da226a271 into main 2026-08-29 20:19:27 +00:00
lekss361 deleted branch fix/3248-hedonic-refit 2026-08-29 20:19:28 +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#3255
No description provided.