Бэктест оценщика берёт тип дома из справочника домов, как боевая оценка — штраф за тип дома впервые участвует в замере #3571

Merged
bot-backend merged 2 commits from fix/backtest-house-type into main 2026-09-17 11:22:34 +00:00
Collaborator

Частично по #2862. Issue остаётся открытым: основную его часть (перевод кода материала стен Росреестра в тип дома) без справочника и решения владельца делать нельзя, см. раздел «Что не сделано».

Что было

  • У всех сделок Росреестра пустой тип дома. Замер на проде 17.09: deals (source='rosreestr') — 434 911 строк, house_type заполнен у 0 (регион 50: 0 из 113 351, 66: 0 из 108 623, 77: 0 из 212 937).
  • Бэктест (scripts/backtest_estimator.py) передаёт deal.house_type во все четыре вызова _fetch_analogs. Штраф за несовпадение типа дома в SQL срабатывает только при условии target_house_type IS NOT NULL, поэтому ни в одном прогоне он не включался.
  • Боевой estimate_quality с #3234 берёт тип дома из houses, если его нет в форме: сначала по резолвленному house_id, иначе по ближайшему дому в радиусе 60 м. С #3257 форма ещё и предзаполняется из того же справочника. Бэктест этот шаг не повторял, даже в режиме --resolve-house-id, который и существует для паритета с продом. На этом режиме строились калибровки в config.py и множители вилки в #3540.

Что сделано

  • Под флагом --resolve-house-id, если у сделки нет типа дома, бэктест вызывает тот же _lookup_house_facts, что и боевая оценка, с теми же аргументами: резолвленный house_id, а без него координаты сделки. Если тип дома у сделки уже есть, справочник его не перезаписывает — так же на проде значение из формы главнее houses.
  • В блок house_id_resolution добавлен счётчик house_type_from_houses: сколько сделок получили тип из справочника. Если он равен нулю, штраф в прогоне не участвовал.
  • Год постройки и этажность из houses не подставляются намеренно, хотя боевая оценка берёт их тем же вызовом. deals.total_floors тоже пуст у 0 из 434 911. Если подставить и их, метрики сдвинутся ещё и из-за этажного фактора и когорт по году, и эффект одного типа дома уже не выделить. А issue просит измерить именно его. Этот разрыв с продом остаётся, и его стоит закрыть отдельным PR с отдельным замером.
  • Без флага вывод бэктеста не меняется. Замороженный регресс-гейт (replay_fixture, 277 сделок) не затронут: он воспроизводит уже отобранные аналоги из фикстуры.

Ожидаемый охват невелик. Оценка по той же выборке, что у калибровочных прогонов: регион 66, --since 2025-06-01 --spread scattered --seed 42, первые 300 сделок, только ветка «ближайший дом в 60 м». Дом нашёлся у 110 сделок из 300, тип дома заполнен у 13 (4.3%). Сопоставление по адресу (match_house_readonly) может добавить ещё какое-то число. Точное значение покажет счётчик.

Что не сделано и почему

Колонка во внешней таблице, соответствие кодов, загрузчик, backfill. У источника (gendesign-postgres, rosreestr_deals) материал стен закодирован по справочнику 126-УНСИ: 061001001001, 061001005000, 061001007001 и т.д., плюс составные значения вида 061001006001;061001006002. Сделки ДКП региона 66, топ по числу: пустой код — 70 770, 061001001001 — 40 547, 061001005000 — 38 150, 061001007001 — 19 328, 061001003000 — 13 662. Расшифровки кодов в репозитории нет (git grep 061001 находит только схему). Какой код считать panel, monolith, block или other и как разбирать составные значения — это решение по справочнику, а не по коду. Сам issue прямо запрещает соответствие «на глаз»: неверный тип хуже пустого. Колонку wall_material_code во внешней таблице без такого соответствия заводить не стал: у неё не было бы ни одного потребителя. Номер миграции МЕРА 323 не использован.

Попутная находка (не чинил, касается и прода)

houses.house_type содержит значения не из словаря: Монолитный (6), Панельный (6), Кирпичный (5), Монолитно кирпичный (3), Блочный (1), wireframe (1). У активных объявлений ровно 6 кодов: monolith, brick, panel, monolith_brick, block, wood. Для этих домов проверка house_type <> target истинна у каждого аналога, и штраф бьёт по всем подряд — и на проде, и теперь в бэктесте.

Тесты

  • tests/test_backtest_estimator.py: 71 passed, rc=0. Два новых теста проверяют значения, а не текст:
    • test_full_backtest_takes_house_type_from_houses_like_prod — пустой тип сделки становится panel в каждом вызове _fetch_analogs; свой тип brick у второй сделки сохраняется; справочник запрошен один раз с target_house_id=77, lat, lon; счётчик = 1.
    • test_full_backtest_without_flag_keeps_house_type_untouched — без флага справочник не вызывается, тип остаётся None, блока house_id_resolution в выводе нет.
  • Весь сьют до rebase: 6234 passed, 44 skipped, rc=0.
  • После rebase на свежий main: 6372 passed, 4 failed, 44 skipped. Эти 4 падения уже есть в main и не связаны с этим PR: test_3466_corridor_tier_a.py (2) и test_estimator_radius_floor.py (2) падают с AttributeError: 'Settings' object has no attribute 'estimate_corridor_clamp_min_n'. Причина — #3556 превратил эти настройки в код, а тесты #3554 всё ещё к ним обращаются. Этот PR меняет только два файла бэктеста.
  • ruff check app tests — All checks passed; ruff format --check на обоих файлах — already formatted.

Фальсификация

  1. Убрал подстановку (deal = dataclasses.replace(...)pass), rc=1:
>       assert seen[1] == {"panel"}
E       AssertionError: assert {None} == {'panel'}
FAILED tests/test_backtest_estimator.py::test_full_backtest_takes_house_type_from_houses_like_prod
1 failed, 1 passed, 69 deselected
  1. Справочник перезаписывает собственный тип сделки (if deal.house_type is Noneif True), rc=1:
E       AssertionError: assert {'panel'} == {'brick'}
FAILED tests/test_backtest_estimator.py::test_full_backtest_takes_house_type_from_houses_like_prod

После каждого прогона исходник восстанавливал из копии и проверял через diff -q.

Приёмка на проде (до 2026-09-20)

Проверять не заполненность колонки, а то, сработал ли механизм и сдвинулись ли метрики. После деплоя в tradein-backend запустить два прогона на одном срезе (только чтение из БД): --engine full --resolve-house-id --region 66 --since 2025-06-01 --spread scattered --seed 42 --sample 300 --json — на коде до этого PR и после него.

  1. Механизм: house_id_resolution.house_type_from_houses > 0; по оценке выше ожидается порядка 13 и больше из 300. Ноль означает, что подстановка не исполнялась, и сравнивать метрики бессмысленно.
  2. Эффект: сравнить MAPE и покрытие вилки. Если метрики не сдвинулись при ненулевом счётчике, это тоже результат, его нужно записать в #2862: на покрытых домах признак ничего не решает.

🤖 Generated with Claude Code

Частично по #2862. Issue остаётся открытым: основную его часть (перевод кода материала стен Росреестра в тип дома) без справочника и решения владельца делать нельзя, см. раздел «Что не сделано». ## Что было - У всех сделок Росреестра пустой тип дома. Замер на проде 17.09: `deals` (source='rosreestr') — 434 911 строк, `house_type` заполнен у **0** (регион 50: 0 из 113 351, 66: 0 из 108 623, 77: 0 из 212 937). - Бэктест (`scripts/backtest_estimator.py`) передаёт `deal.house_type` во все четыре вызова `_fetch_analogs`. Штраф за несовпадение типа дома в SQL срабатывает только при условии `target_house_type IS NOT NULL`, поэтому ни в одном прогоне он не включался. - Боевой `estimate_quality` с #3234 берёт тип дома из `houses`, если его нет в форме: сначала по резолвленному house_id, иначе по ближайшему дому в радиусе 60 м. С #3257 форма ещё и предзаполняется из того же справочника. Бэктест этот шаг не повторял, даже в режиме `--resolve-house-id`, который и существует для паритета с продом. На этом режиме строились калибровки в `config.py` и множители вилки в #3540. ## Что сделано - Под флагом `--resolve-house-id`, если у сделки нет типа дома, бэктест вызывает тот же `_lookup_house_facts`, что и боевая оценка, с теми же аргументами: резолвленный house_id, а без него координаты сделки. Если тип дома у сделки уже есть, справочник его не перезаписывает — так же на проде значение из формы главнее `houses`. - В блок `house_id_resolution` добавлен счётчик `house_type_from_houses`: сколько сделок получили тип из справочника. Если он равен нулю, штраф в прогоне не участвовал. - **Год постройки и этажность из `houses` не подставляются намеренно**, хотя боевая оценка берёт их тем же вызовом. `deals.total_floors` тоже пуст у 0 из 434 911. Если подставить и их, метрики сдвинутся ещё и из-за этажного фактора и когорт по году, и эффект одного типа дома уже не выделить. А issue просит измерить именно его. Этот разрыв с продом остаётся, и его стоит закрыть отдельным PR с отдельным замером. - Без флага вывод бэктеста не меняется. Замороженный регресс-гейт (`replay_fixture`, 277 сделок) не затронут: он воспроизводит уже отобранные аналоги из фикстуры. **Ожидаемый охват невелик.** Оценка по той же выборке, что у калибровочных прогонов: регион 66, `--since 2025-06-01 --spread scattered --seed 42`, первые 300 сделок, только ветка «ближайший дом в 60 м». Дом нашёлся у 110 сделок из 300, тип дома заполнен у **13 (4.3%)**. Сопоставление по адресу (`match_house_readonly`) может добавить ещё какое-то число. Точное значение покажет счётчик. ## Что не сделано и почему **Колонка во внешней таблице, соответствие кодов, загрузчик, backfill.** У источника (`gendesign-postgres`, `rosreestr_deals`) материал стен закодирован по справочнику 126-УНСИ: `061001001001`, `061001005000`, `061001007001` и т.д., плюс составные значения вида `061001006001;061001006002`. Сделки ДКП региона 66, топ по числу: пустой код — 70 770, `061001001001` — 40 547, `061001005000` — 38 150, `061001007001` — 19 328, `061001003000` — 13 662. Расшифровки кодов в репозитории нет (`git grep 061001` находит только схему). Какой код считать `panel`, `monolith`, `block` или `other` и как разбирать составные значения — это решение по справочнику, а не по коду. Сам issue прямо запрещает соответствие «на глаз»: неверный тип хуже пустого. Колонку `wall_material_code` во внешней таблице без такого соответствия заводить не стал: у неё не было бы ни одного потребителя. Номер миграции МЕРА 323 не использован. ## Попутная находка (не чинил, касается и прода) `houses.house_type` содержит значения не из словаря: `Монолитный` (6), `Панельный` (6), `Кирпичный` (5), `Монолитно кирпичный` (3), `Блочный` (1), `wireframe` (1). У активных объявлений ровно 6 кодов: monolith, brick, panel, monolith_brick, block, wood. Для этих домов проверка `house_type <> target` истинна у каждого аналога, и штраф бьёт по всем подряд — и на проде, и теперь в бэктесте. ## Тесты - `tests/test_backtest_estimator.py`: 71 passed, rc=0. Два новых теста проверяют значения, а не текст: - `test_full_backtest_takes_house_type_from_houses_like_prod` — пустой тип сделки становится `panel` в каждом вызове `_fetch_analogs`; свой тип `brick` у второй сделки сохраняется; справочник запрошен один раз с `target_house_id=77, lat, lon`; счётчик = 1. - `test_full_backtest_without_flag_keeps_house_type_untouched` — без флага справочник не вызывается, тип остаётся `None`, блока `house_id_resolution` в выводе нет. - Весь сьют до rebase: `6234 passed, 44 skipped`, rc=0. - После rebase на свежий main: `6372 passed, 4 failed, 44 skipped`. Эти 4 падения уже есть в main и не связаны с этим PR: `test_3466_corridor_tier_a.py` (2) и `test_estimator_radius_floor.py` (2) падают с `AttributeError: 'Settings' object has no attribute 'estimate_corridor_clamp_min_n'`. Причина — #3556 превратил эти настройки в код, а тесты #3554 всё ещё к ним обращаются. Этот PR меняет только два файла бэктеста. - `ruff check app tests` — All checks passed; `ruff format --check` на обоих файлах — already formatted. ## Фальсификация 1) Убрал подстановку (`deal = dataclasses.replace(...)` → `pass`), rc=1: ``` > assert seen[1] == {"panel"} E AssertionError: assert {None} == {'panel'} FAILED tests/test_backtest_estimator.py::test_full_backtest_takes_house_type_from_houses_like_prod 1 failed, 1 passed, 69 deselected ``` 2) Справочник перезаписывает собственный тип сделки (`if deal.house_type is None` → `if True`), rc=1: ``` E AssertionError: assert {'panel'} == {'brick'} FAILED tests/test_backtest_estimator.py::test_full_backtest_takes_house_type_from_houses_like_prod ``` После каждого прогона исходник восстанавливал из копии и проверял через `diff -q`. ## Приёмка на проде (до 2026-09-20) Проверять не заполненность колонки, а то, сработал ли механизм и сдвинулись ли метрики. После деплоя в `tradein-backend` запустить два прогона на одном срезе (только чтение из БД): `--engine full --resolve-house-id --region 66 --since 2025-06-01 --spread scattered --seed 42 --sample 300 --json` — на коде до этого PR и после него. 1. Механизм: `house_id_resolution.house_type_from_houses` > 0; по оценке выше ожидается порядка 13 и больше из 300. Ноль означает, что подстановка не исполнялась, и сравнивать метрики бессмысленно. 2. Эффект: сравнить MAPE и покрытие вилки. Если метрики не сдвинулись при ненулевом счётчике, это тоже результат, его нужно записать в #2862: на покрытых домах признак ничего не решает. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-09-17 09:32:20 +00:00
Бэктест берёт тип дома из справочника домов, как боевая оценка (#2862)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 15s
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) Failing after 5m14s
9f6701d3a5
deals.house_type пуст у всех 434 911 сделок Росреестра (прод 17.09), поэтому
в прогоне с --resolve-house-id штраф за несовпадение типа дома в отборе
аналогов не срабатывал ни разу. Боевой estimate_quality с #3234 подставляет
тип из houses (по резолвленному house_id, иначе ближайший дом в 60 м), когда
его нет в форме; бэктест этого не повторял.

Теперь при --resolve-house-id пустой тип сделки дозаполняется тем же
_lookup_house_facts, свой тип сделки главнее. Счётчик house_type_from_houses
в блоке house_id_resolution показывает, скольким сделкам тип подставлен.
Год и этажность из houses намеренно не берутся: они сдвинули бы метрики по
другим признакам и смешали бы замер эффекта типа дома. Без флага вывод прежний.

Основная часть issue (код материала стен Росреестра -> тип дома) не сделана:
нужен справочник 126-УНСИ и решение владельца по соответствию.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Light1YT added 1 commit 2026-09-17 10:49:46 +00:00
Merge remote-tracking branch 'origin/main' into fix/backtest-house-type
All checks were successful
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 / changes (pull_request) Successful in 21s
CI / changes (pull_request) Successful in 33s
CI Trade-In / backend-tests (pull_request) Successful in 8m41s
86629dc9f4
bot-backend merged commit a6a1b7cdfc into main 2026-09-17 11:22:34 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3571
No description provided.