feat(tradein/yandex): normalize house_type + promote kitchen/ceiling to columns (#2007) #2016

Merged
lekss361 merged 2 commits from feat/tradein-2007-yandex-house-type-promote into main 2026-06-27 17:45:12 +00:00
Owner

Summary

CLASS A — реальный фикс точности оценки: yandex SERP писал house_type сырым SCREAMING (MONOLIT/BRICK/...) вместо канона (monolith/...). estimator soft-penalty (house_type <> :target) никогда не матчил → ~70% yandex-аналогов получали ложный штраф. Прод: 2765 MONOLIT + 1044 BRICK + ... — 0% в каноне.

  • Новый shared house_type_normalizer.py (канон panel/brick/monolith/monolith_brick/block/wood, сверен с estimator._IMV_HOUSE_TYPE_MAP:146; case-insensitive; unknown/other/None→NULL — нейтрально в estimator; cian-ветка monolithBrickmonolith_brick готова к reuse в #2008).
  • yandex_realty._entity_to_lot: buildingTypenormalize_house_type (сырое в raw_payload['raw_building_type']).
  • Backfill-миграция 140 (идемпотентна, auto-apply strict, scope source='yandex'): SCREAMING→канон существующих строк + 'other'(299)→NULL. Без неё фикс работал бы только на новые скрейпы.

CLASS B — promote для дашборда/матчинга (НЕ valuation, см. follow-up #2012): kitchenSpace.valuekitchen_area_m2, ceilingHeightceiling_height_m. Добавлены поля в ScrapedLot + проведены через save_listings (INSERT + оба upsert-пути, COALESCE — SERP-NULL не затирает detail-enriched).

  • ceiling пишется в обе колонки (ceiling_height 019 — читает coverage-дашборд admin.py; ceiling_height_m 111 — живая avito). Cap 2.0–6.0 м перед записью: yandex SERP отдаёт мусор (видели 18 м), а ceiling_heightnumeric(3,2) (max 9.99) → out-of-range уронил бы весь батч DataError'ом (SAVEPOINT ловит только IntegrityError).

Test plan

  • ruff-100 clean, no :x::type CAST trap
  • pytest: test_house_type_normalizer (43), test_yandex_realty_serp (67, вкл. cap out-of-range), test_base_save_listings (dual-column + None-passthrough) — all pass
  • code-reviewer (⚠️ 2 minor → оба исправлены: ceiling cap + 'other'→NULL backfill)
  • post-deploy (миграция авто-применится strict): SELECT house_type, count(*) FROM listings WHERE source='yandex' GROUP BY 1 — SCREAMING/other исчезают; до/после fill-rate kitchen/ceiling

Refs #2007

## Summary **CLASS A — реальный фикс точности оценки:** yandex SERP писал `house_type` сырым SCREAMING (`MONOLIT`/`BRICK`/...) вместо канона (`monolith`/...). estimator soft-penalty (`house_type <> :target`) никогда не матчил → ~70% yandex-аналогов получали ложный штраф. Прод: 2765 `MONOLIT` + 1044 `BRICK` + ... — 0% в каноне. - Новый shared `house_type_normalizer.py` (канон `panel/brick/monolith/monolith_brick/block/wood`, сверен с `estimator._IMV_HOUSE_TYPE_MAP:146`; case-insensitive; unknown/`other`/None→NULL — нейтрально в estimator; cian-ветка `monolithBrick`→`monolith_brick` готова к reuse в #2008). - `yandex_realty._entity_to_lot`: `buildingType` → `normalize_house_type` (сырое в `raw_payload['raw_building_type']`). - **Backfill-миграция `140`** (идемпотентна, auto-apply strict, scope `source='yandex'`): SCREAMING→канон существующих строк + `'other'`(299)→NULL. Без неё фикс работал бы только на новые скрейпы. **CLASS B — promote для дашборда/матчинга (НЕ valuation, см. follow-up #2012):** `kitchenSpace.value`→`kitchen_area_m2`, `ceilingHeight`→`ceiling_height_m`. Добавлены поля в `ScrapedLot` + проведены через `save_listings` (INSERT + оба upsert-пути, COALESCE — SERP-NULL не затирает detail-enriched). - ceiling пишется в **обе** колонки (`ceiling_height` 019 — читает coverage-дашборд `admin.py`; `ceiling_height_m` 111 — живая avito). **Cap 2.0–6.0 м** перед записью: yandex SERP отдаёт мусор (видели 18 м), а `ceiling_height` — `numeric(3,2)` (max 9.99) → out-of-range уронил бы весь батч DataError'ом (SAVEPOINT ловит только IntegrityError). ## Test plan - [x] ruff-100 clean, no `:x::type` CAST trap - [x] pytest: `test_house_type_normalizer` (43), `test_yandex_realty_serp` (67, вкл. cap out-of-range), `test_base_save_listings` (dual-column + None-passthrough) — all pass - [x] code-reviewer ✅ (⚠️ 2 minor → оба исправлены: ceiling cap + `'other'`→NULL backfill) - [ ] post-deploy (миграция авто-применится strict): `SELECT house_type, count(*) FROM listings WHERE source='yandex' GROUP BY 1` — SCREAMING/`other` исчезают; до/после fill-rate kitchen/ceiling Refs #2007
lekss361 added 2 commits 2026-06-27 17:44:04 +00:00
house_type-нормализация = реальный фикс estimator soft-penalty: yandex SERP
писал building.buildingType как есть (SCREAMING — MONOLIT/BRICK/PANEL/...),
а estimator штрафует аналог с house_type != target. 'MONOLIT' никогда не равен
каноничному 'monolith' → ~70% yandex-аналогов получали ложный штраф и фактически
выпадали из скоринга. Новый модуль house_type_normalizer.normalize_house_type
приводит yandex SCREAMING и cian camelCase (reuse в #2008) к канону
{panel, brick, monolith, monolith_brick, block, wood}; unknown/'other'/'' → None
(NULL нейтрально для estimator, не штрафуется). Backfill существующих yandex-строк
миграцией 140 (идемпотентный UPDATE под тот же маппинг).

kitchen_area_m2 / ceiling_height_m promote из raw_payload в колонки — для
coverage-дашборда и matching, НЕ valuation-фича (estimator их пока не читает —
отдельный follow-up). ceiling пишется в ОБЕ колонки (ceiling_height 019 — её
читает coverage-дашборд admin.py + yandex_detail/cian_detail; ceiling_height_m
111 — живая avito-колонка) через COALESCE в ON CONFLICT, чтобы SERP re-scrape
без полей не затирал detail-enrichment (avito поведение сохранено).
fix(tradein/yandex): cap ceiling to numeric(3,2) range + backfill 'other'->NULL
All checks were successful
CI / changes (pull_request) Successful in 8s
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
024fadac73
Pre-push review (#2007):
- ceiling_height_m: clamp to plausible 2.0-6.0 m. yandex SERP emits garbage (seen 18 m)
  -> out-of-range write to ceiling_height numeric(3,2) raised DataError failing the whole
  batch (per-lot SAVEPOINT only catches IntegrityError). Raw value still in raw_payload.
- migration 140: also NULL out legacy 'other' (299 rows) for parity with
  normalize_house_type (unknown->None; NULL neutral in estimator vs 'other' false penalty).
lekss361 merged commit 440bb2ca51 into main 2026-06-27 17:45:12 +00:00
lekss361 deleted branch feat/tradein-2007-yandex-house-type-promote 2026-06-27 17:45:12 +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#2016
No description provided.