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
3 changed files with 35 additions and 4 deletions
Showing only changes of commit 024fadac73 - Show all commits

View file

@ -325,7 +325,14 @@ def _entity_to_lot(
total_floors = int(floors_total_raw) if floors_total_raw is not None else None
ceiling_height = entity.get("ceilingHeight")
ceiling_height_m = float(ceiling_height) if ceiling_height is not None else None
# Колонка ceiling_height — numeric(3,2), max 9.99. Yandex SERP отдаёт мусор
# (видели 18 м) → запись out-of-range уронила бы весь батч DataError'ом
# (per-lot SAVEPOINT ловит только IntegrityError). Берём только правдоподобный
# диапазон 2.06.0 м, иначе None; сырое значение остаётся в raw_payload. (#2007)
_ceiling_raw = float(ceiling_height) if ceiling_height is not None else None
ceiling_height_m = (
_ceiling_raw if _ceiling_raw is not None and 2.0 <= _ceiling_raw <= 6.0 else None
)
building = entity.get("building") or {}
year_built_raw = building.get("builtYear")

View file

@ -13,9 +13,14 @@
-- MONOLIT->monolith, BRICK->brick, PANEL->panel, MONOLIT_BRICK->monolith_brick,
-- BLOCK->block, WOOD->wood.
--
-- Idempotency: после первого прогона SCREAMING-значений не остаётся, WHERE-фильтр
-- пуст → повторный apply = no-op (0 rows). Scope строго source='yandex' —
-- cian/avito не трогаем (их канон чинится в коде / отдельных PR, напр. #2008).
-- Плюс 'other' (не-канон легаси-значение, 299 строк) → NULL: паритет с
-- normalize_house_type (unknown→None). В estimator soft-penalty NULL нейтрально,
-- а 'other' != target всегда даёт ложный штраф. Лоуэркейс 'brick'/'panel' уже
-- каноничны — не трогаем.
--
-- Idempotency: после первого прогона SCREAMING/'other'-значений не остаётся,
-- WHERE-фильтры пусты → повторный apply = no-op (0 rows). Scope строго
-- source='yandex' — cian/avito не трогаем (канон в коде / отдельных PR, напр. #2008).
BEGIN;
@ -32,4 +37,10 @@ UPDATE listings
WHERE source = 'yandex'
AND house_type IN ('MONOLIT', 'BRICK', 'PANEL', 'MONOLIT_BRICK', 'BLOCK', 'WOOD');
-- 'other' → NULL (паритет с normalize_house_type: unknown→None, нейтрально в estimator).
UPDATE listings
SET house_type = NULL
WHERE source = 'yandex'
AND house_type = 'other';
COMMIT;

View file

@ -239,6 +239,19 @@ def test_entity_to_lot_full_entity():
assert lot.raw_payload["raw_building_type"] == "MONOLIT" # сырой SCREAMING сохранён
def test_entity_to_lot_ceiling_out_of_range_dropped():
"""ceiling_height — numeric(3,2) (max 9.99); неправдоподобный yandex SERP мусор
(видели 18 м) отсекаем в None, иначе DataError уронил бы весь батч. Сырое
значение остаётся в raw_payload. (#2007)"""
lot_hi = _entity_to_lot({**_ENTITY_FULL, "ceilingHeight": 18})
assert lot_hi is not None
assert lot_hi.ceiling_height_m is None
assert lot_hi.raw_payload["ceiling_height"] == 18 # сырое сохранено
lot_lo = _entity_to_lot({**_ENTITY_FULL, "ceilingHeight": 1.6})
assert lot_lo is not None
assert lot_lo.ceiling_height_m is None
def test_entity_to_lot_segment_vtorichka_default():
"""Default new_flat='NO' -> listing_segment='vtorichka'."""
lot = _entity_to_lot(_ENTITY_FULL)