fix(tradein): свести высоту потолков к одной колонке ceiling_height_m #2779

Merged
bot-backend merged 2 commits from fix/2699-ceiling-height into main 2026-08-07 09:18:49 +00:00
Collaborator

Замер с прода (2026-08-07, 94 373 объявления)

source  |  всего | ceiling_height | ceiling_height_m | обе   | расходятся
avito   | 48 592 |              0 |            7 149 |     0 |          0
cian    | 21 951 |            855 |                0 |     0 |          0
yandex  | 16 854 |          7 699 |            7 675 | 7 675 |          0
domklik |  6 594 |              0 |                0 |     0 |          0
n1      |    382 |              0 |                0 |     0 |          0

Главная предпосылка опровергнута: расхождений НЕТ. Там, где заполнены обе колонки (7 675 строк), значения совпадают до последнего знака. Это не «две конфликтующие правды», а одна правда в двух ящиках. Задача — не «неверная цена», а лишняя колонка и потерянное покрытие: 855 циановских + 24 яндексовых значения не видит ни один потребитель.

Мусор в колонке, которую читает эстиматор: 26 строк > 6 м (максимум 29.90) и 84 < 2 м (83 ровных 0.00) — все avito. У ceiling_height — 2 строки cian вне диапазона.

Денежный эффект: 0 ₽

Замерен, а не предположен. Backtest (scripts/backtest_estimator, 200 сделок, полный ценовой спайн, --resolve-house-id, прод-БД read-only) в трёх конфигурациях:

конфигурация сделок изменило оценку
A флаг OFF — прод сегодня
B флаг ON, текущие расщеплённые данные 0 / 200 vs A
C флаг ON, данные после этого PR 0 / 200 vs B

Причина структурная, а не «повезло на выборке»: сигнал _adjust_relevance_by_pool_deviation только переупорядочивает аналоги, а медиана ₽/м² к порядку безразлична. Состав пула меняется лишь когда он перевалит за 50 (_stratify_candidates[:50]). Медиана размера пула в выборке — 2, порог 50 достигнут у 1 из 200 сделок, и даже там состав не изменился (сверено посписочно по source_url: порядок отличается у 32 сделок, состав — у 0).

Ловушка «включат флаг и молча поедет цена» из issue тоже, таким образом, опровергнута — при нынешней плотности аналогов сам флаг ничего не двигает. Правка нужна ради чистоты данных и снятия дубля, а не ради цены. Направление правки безопасное по построению: она не может увеличить оценки, потому что не меняет их вовсе.

Выбранная колонка: ceiling_height_m

По источнику данных, не по удобству:

  1. Единицы в имени. Так этот признак назван везде в проекте — domrf_kn_flats.ceiling_height_m, domrf_kn_objects.ceiling_height_m, фронтовый ObjectSpecsTable. listings.ceiling_height — единственное исключение.
  2. Её читает единственный потребитель, влияющий на цену (эстиматор).
  3. numeric(5,2) против numeric(3,2): у 019-колонки потолок 9.99, и запись out-of-range роняет весь батч DataError'ом (per-lot SAVEPOINT ловит только IntegrityError) — это уже приходилось обходить инлайн-гейтом в yandex SERP.
  4. После переноса — надмножество: 15 591 против 8 554.

Что со второй

Писателей у ceiling_height больше нет, значения перенесены, из coverage-дашборда убрана, в схеме помечена COMMENT ... DEPRECATED. DROP COLUMN намеренно не в этом PR: сначала прод должен подтвердить, что колонку никто не пишет и не читает. Снос — отдельным шагом за database-expert.

Изменения

  • новый scraper_kit/ceiling_height.py — единый гейт правдоподобия 2.0–6.0 м. Гейт был инлайн только у yandex SERP, поэтому avito detail (_parse_height_m брал первое число строки) нагнал мусор в канон. Корень починен там, где сходятся все писатели.
  • base.save_listings больше не пишет ceiling_height (писал ОДИН param в ОБЕ колонки — источник дубля).
  • cian/detail.py, yandex/detail.py → пишут канон.
  • мигр. 238 — чистка невозможных значений (110 строк) + перенос 877 уникальных + COMMENT.
  • HOUSE_FIELD_PRIORITY["ceiling_height"] удалён: колонки с таким именем в houses нет (прод: 0 колонок LIKE '%ceiling%'), правило не могло сработать ни разу. Существовавший тест на него зеленел, проверяя фантом, — заменён на проверку отсутствия.
  • LISTING_FIELD_PRIORITY — ключ переименован в имя реальной колонки, источники расширены до трёх (карта — документация, в боевом merge-пути не вызывается, см. её собственный docstring).

Test plan

  • tests/test_ceiling_height_unify_2699.py9 тестов красные на origin/main (прогнано: исходники откачены на main, тесты оставлены новые → 9 failed), зелёные после.
  • Полный прогон tradein-mvp/backend: 3979 passed, 15 skipped.
  • ruff check + ruff format (конфиг репо, v0.7.4).

Прод-верификация (критерий записан ДО факта)

Расписания ждать не надо: tradein-mvp/backend/data/sql/NN_*.sql применяется автоматически на деплое (_schema_migrations, strict). Проверять сразу после деплоя.

Критерий приёмки (посчитан симуляцией миграции на проде 2026-08-07, до мержа):

SELECT count(ceiling_height_m)                                            AS canon,       -- ожидание 15 591 (было 14 824)
       count(*) FILTER (WHERE ceiling_height_m NOT BETWEEN 2.0 AND 6.0)   AS junk,        -- ожидание 0 (было 110)
       count(ceiling_height)                                              AS deprecated   -- ожидание 8 552 (было 8 554) и НЕ РАСТЁТ
FROM listings;

Второй прогон, через сутки после деплоя: count(ceiling_height) не изменился — значит писателей у устаревшей колонки действительно нет и её можно сносить отдельным PR.

Refs #2699

## Замер с прода (2026-08-07, 94 373 объявления) ``` source | всего | ceiling_height | ceiling_height_m | обе | расходятся avito | 48 592 | 0 | 7 149 | 0 | 0 cian | 21 951 | 855 | 0 | 0 | 0 yandex | 16 854 | 7 699 | 7 675 | 7 675 | 0 domklik | 6 594 | 0 | 0 | 0 | 0 n1 | 382 | 0 | 0 | 0 | 0 ``` **Главная предпосылка опровергнута: расхождений НЕТ.** Там, где заполнены обе колонки (7 675 строк), значения совпадают до последнего знака. Это не «две конфликтующие правды», а одна правда в двух ящиках. Задача — не «неверная цена», а лишняя колонка и потерянное покрытие: 855 циановских + 24 яндексовых значения не видит ни один потребитель. Мусор в колонке, которую читает эстиматор: 26 строк > 6 м (максимум 29.90) и 84 < 2 м (83 ровных 0.00) — все avito. У `ceiling_height` — 2 строки cian вне диапазона. ## Денежный эффект: 0 ₽ Замерен, а не предположен. Backtest (`scripts/backtest_estimator`, 200 сделок, полный ценовой спайн, `--resolve-house-id`, прод-БД read-only) в трёх конфигурациях: | | конфигурация | сделок изменило оценку | |---|---|---| | A | флаг OFF — прод сегодня | — | | B | флаг ON, текущие расщеплённые данные | **0 / 200** vs A | | C | флаг ON, данные после этого PR | **0 / 200** vs B | Причина структурная, а не «повезло на выборке»: сигнал `_adjust_relevance_by_pool_deviation` только **переупорядочивает** аналоги, а медиана ₽/м² к порядку безразлична. Состав пула меняется лишь когда он перевалит за 50 (`_stratify_candidates` → `[:50]`). Медиана размера пула в выборке — **2**, порог 50 достигнут у **1 из 200** сделок, и даже там состав не изменился (сверено посписочно по `source_url`: порядок отличается у 32 сделок, состав — у 0). Ловушка «включат флаг и молча поедет цена» из issue тоже, таким образом, опровергнута — при нынешней плотности аналогов сам флаг ничего не двигает. Правка нужна ради чистоты данных и снятия дубля, а не ради цены. Направление правки безопасное по построению: она не может увеличить оценки, потому что не меняет их вовсе. ## Выбранная колонка: `ceiling_height_m` По источнику данных, не по удобству: 1. Единицы в имени. Так этот признак назван **везде** в проекте — `domrf_kn_flats.ceiling_height_m`, `domrf_kn_objects.ceiling_height_m`, фронтовый `ObjectSpecsTable`. `listings.ceiling_height` — единственное исключение. 2. Её читает единственный потребитель, влияющий на цену (эстиматор). 3. `numeric(5,2)` против `numeric(3,2)`: у 019-колонки потолок 9.99, и запись out-of-range роняет **весь батч** `DataError`'ом (per-lot SAVEPOINT ловит только `IntegrityError`) — это уже приходилось обходить инлайн-гейтом в yandex SERP. 4. После переноса — надмножество: **15 591** против 8 554. ## Что со второй Писателей у `ceiling_height` больше нет, значения перенесены, из coverage-дашборда убрана, в схеме помечена `COMMENT ... DEPRECATED`. **DROP COLUMN намеренно не в этом PR**: сначала прод должен подтвердить, что колонку никто не пишет и не читает. Снос — отдельным шагом за database-expert. ## Изменения - **новый** `scraper_kit/ceiling_height.py` — единый гейт правдоподобия 2.0–6.0 м. Гейт был инлайн только у yandex SERP, поэтому avito detail (`_parse_height_m` брал первое число строки) нагнал мусор в канон. Корень починен там, где сходятся все писатели. - `base.save_listings` больше не пишет `ceiling_height` (писал ОДИН param в ОБЕ колонки — источник дубля). - `cian/detail.py`, `yandex/detail.py` → пишут канон. - **мигр. 238** — чистка невозможных значений (110 строк) + перенос 877 уникальных + `COMMENT`. - `HOUSE_FIELD_PRIORITY["ceiling_height"]` **удалён**: колонки с таким именем в `houses` нет (прод: 0 колонок `LIKE '%ceiling%'`), правило не могло сработать ни разу. Существовавший тест на него зеленел, проверяя фантом, — заменён на проверку отсутствия. - `LISTING_FIELD_PRIORITY` — ключ переименован в имя реальной колонки, источники расширены до трёх (карта — документация, в боевом merge-пути не вызывается, см. её собственный docstring). ## Test plan - [x] `tests/test_ceiling_height_unify_2699.py` — **9 тестов красные на `origin/main`** (прогнано: исходники откачены на main, тесты оставлены новые → 9 failed), зелёные после. - [x] Полный прогон `tradein-mvp/backend`: 3979 passed, 15 skipped. - [x] `ruff check` + `ruff format` (конфиг репо, v0.7.4). ## Прод-верификация (критерий записан ДО факта) Расписания ждать не надо: `tradein-mvp/backend/data/sql/NN_*.sql` применяется автоматически на деплое (`_schema_migrations`, strict). Проверять **сразу после деплоя**. **Критерий приёмки** (посчитан симуляцией миграции на проде 2026-08-07, до мержа): ```sql SELECT count(ceiling_height_m) AS canon, -- ожидание 15 591 (было 14 824) count(*) FILTER (WHERE ceiling_height_m NOT BETWEEN 2.0 AND 6.0) AS junk, -- ожидание 0 (было 110) count(ceiling_height) AS deprecated -- ожидание 8 552 (было 8 554) и НЕ РАСТЁТ FROM listings; ``` Второй прогон, через сутки после деплоя: `count(ceiling_height)` не изменился — значит писателей у устаревшей колонки действительно нет и её можно сносить отдельным PR. Refs #2699
bot-backend added 1 commit 2026-08-07 08:56:14 +00:00
fix(tradein): свести высоту потолков к одной колонке ceiling_height_m
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 4m17s
869079a86f
Прод-замер 2026-08-07 (94 373 объявления): признак жил в двух колонках,
источники разложены по ним не пересекаясь.

  source  |  всего | ceiling_height | ceiling_height_m | обе   | расходятся
  avito   | 48 592 |              0 |            7 149 |     0 |          0
  cian    | 21 951 |            855 |                0 |     0 |          0
  yandex  | 16 854 |          7 699 |            7 675 | 7 675 |          0

Расхождений НЕТ: где заполнены обе (7 675 строк), значения совпадают до
последнего знака. Значит это не «две правды», а одна правда в двух ящиках:
855 циановских + 24 яндексовых значения не видел ни один потребитель.

Канон — ceiling_height_m: единицы в имени (так этот признак назван везде
в проекте: domrf_kn_flats/objects, фронт), её читает эстиматор,
numeric(5,2) против numeric(3,2) у 019-колонки, чей потолок 9.99 роняет
весь батч DataError'ом на out-of-range.

Что сделано:
- scraper_kit.ceiling_height.plausible_ceiling_m — единый гейт 2.0–6.0 м.
  Раньше гейт был инлайн только у yandex SERP, поэтому avito detail нагнал
  26 значений > 6 м (максимум 29.90) и 83 ровных 0.00 в колонку, которую
  читает эстиматор. Корень — _parse_height_m брал первое число строки.
- base.save_listings больше не пишет ceiling_height (писал один param в обе
  колонки — источник дубля); cian_detail и yandex_detail переведены на канон.
- Миграция 238: чистка невозможных значений + перенос 879 уникальных.
  DROP COLUMN намеренно НЕ здесь — сначала прод должен подтвердить, что
  колонку никто не пишет; снос отдельным шагом.
- HOUSE_FIELD_PRIORITY["ceiling_height"] удалён: колонки с таким именем в
  houses нет, правило не могло сработать ни разу. Тест на него зеленел,
  проверяя фантом.
- Coverage-дашборд показывает одну строку вместо двух.

Денежный эффект замерен, а не предположен: backtest (200 сделок, полный
ценовой спайн, --resolve-house-id) в трёх конфигурациях — флаг OFF (прод),
флаг ON на текущих данных, флаг ON на данных после этой правки. Оценка не
сдвинулась НИ У ОДНОЙ сделки. Причина структурная: сигнал только
переупорядочивает аналоги, а медиана ₽/м² к порядку безразлична; состав
меняется лишь когда пул перевалит за 50 (медиана пула — 2, у 1 из 200).

Refs #2699
Light1YT added 1 commit 2026-08-07 08:57:02 +00:00
docs(tradein): уточнить ожидаемые числа мигр. 238 по симуляции на проде
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 3m51s
8df66ad740
877 перенесённых (855 cian − 2 мусорных + 24 yandex), итог 15 591 непустых
ceiling_height_m. Прежние 879/15 703 были посчитаны в уме без вычета мусора,
а критерий приёмки, записанный ДО факта, обязан быть точным.

Refs #2699
bot-backend closed this pull request 2026-08-07 09:04:08 +00:00
bot-backend reopened this pull request 2026-08-07 09:04:12 +00:00
bot-backend merged commit 306fd82836 into main 2026-08-07 09:18:49 +00:00
bot-backend deleted branch fix/2699-ceiling-height 2026-08-07 09:18:49 +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#2779
No description provided.