tradein/estimate: высота потолков в двух колонках — эстиматор читает одну, карта приоритетов называет каноном другую, 855 значений Циана не видит … #2699

Closed
opened 2026-08-06 05:52:45 +00:00 by bot-backend · 4 comments
Collaborator

Найдено при проверке предпосылки «высота потолков Циана не разбирается» (эпик #2674). Предпосылка опровергнута — разбор работает. Но при проверке вскрылось другое: высота потолков живёт в двух колонках, источники разложены по ним не пересекаясь, а потребитель читает только одну.

Замер с прода

источник | всего  | ceiling_height | ceiling_height_m
avito    | 48 222 |              0 |            7 147
cian     | 21 799 |            855 |                0
yandex   | 16 721 |          7 582 |            7 558
domklik  |  6 594 |              0 |                0
n1       |    382 |              0 |                0

Авито пишет только в _m. Циан — только в бессуффиксную. Яндекс дублирует в обе.

Кто что читает

Эстиматор (app/services/estimator.py:4815, а также SELECT'ы на :4833, :5177, :5242, :5331) читает только ceiling_height_m. Все 855 циановских значений не видит никто.

При этом собственная карта приоритетов проекта объявляет каноном ровно ту колонку, которую эстиматор не читает:

# app/services/matching/conflict_resolution.py:91
LISTING_FIELD_PRIORITY["ceiling_height"] = ["cian_detail"]

То есть «правильным» источником назначен единственный источник, попадающий в невидимую колонку. Две части системы согласованно смотрят в разные стороны.

Почему это не горит сегодня

estimate_ceiling_height_signal_enabled на проде не выставлен → по умолчанию False. Сигнал сейчас выключен, живого искажения оценки нет. Ловушка сработает в момент включения флага: сигнал молча включится на неполном покрытии (7 147 авито-строк вместо 8 002 с Цианом) и на источнике, который карта приоритетов каноном не считает.

Два сопутствующих дефекта

1. HOUSE_FIELD_PRIORITY["ceiling_height"] (conflict_resolution.py:78) объявляет приоритет для домового поля. Колонки ceiling_height в таблице houses не существует (проверено: 0 колонок с %ceiling%). Правило не может сработать никогда.

2. Единицы у Авито не приведены. В ceiling_height_m у Авито 21 строка больше 10 метров, максимум 29.90, минимум 1.70. У Яндекса в той же колонке максимум 6.00. Похоже на непереведённые сантиметры или мис-парс формата — то есть колонка, которую читает эстиматор, содержит заведомо невозможные значения.

Что предлагается

Свести к одной колонке до того, как флаг включат: выбрать канон по тому, что уже читает эстиматор либо что назначено картой приоритетов (это разные ответы — решение надо принять явно, а не унаследовать), перевести писателей, привести исторические значения, отфильтровать невозможные. Плюс убрать домовое правило, ссылающееся на несуществующую колонку.

Связано: #2674, #2012 (откуда сигнал), #2435.

Найдено при проверке предпосылки «высота потолков Циана не разбирается» (эпик #2674). Предпосылка **опровергнута** — разбор работает. Но при проверке вскрылось другое: высота потолков живёт в **двух колонках**, источники разложены по ним не пересекаясь, а потребитель читает только одну. ## Замер с прода ``` источник | всего | ceiling_height | ceiling_height_m avito | 48 222 | 0 | 7 147 cian | 21 799 | 855 | 0 yandex | 16 721 | 7 582 | 7 558 domklik | 6 594 | 0 | 0 n1 | 382 | 0 | 0 ``` Авито пишет только в `_m`. Циан — только в бессуффиксную. Яндекс дублирует в обе. ## Кто что читает Эстиматор (`app/services/estimator.py:4815`, а также SELECT'ы на `:4833`, `:5177`, `:5242`, `:5331`) читает **только `ceiling_height_m`**. Все **855** циановских значений не видит никто. При этом собственная карта приоритетов проекта объявляет каноном ровно ту колонку, которую эстиматор не читает: ```python # app/services/matching/conflict_resolution.py:91 LISTING_FIELD_PRIORITY["ceiling_height"] = ["cian_detail"] ``` То есть «правильным» источником назначен единственный источник, попадающий в невидимую колонку. Две части системы согласованно смотрят в разные стороны. ## Почему это не горит сегодня `estimate_ceiling_height_signal_enabled` на проде не выставлен → по умолчанию `False`. Сигнал сейчас выключен, живого искажения оценки нет. **Ловушка сработает в момент включения флага**: сигнал молча включится на неполном покрытии (7 147 авито-строк вместо 8 002 с Цианом) и на источнике, который карта приоритетов каноном не считает. ## Два сопутствующих дефекта **1. `HOUSE_FIELD_PRIORITY["ceiling_height"]`** (`conflict_resolution.py:78`) объявляет приоритет для домового поля. Колонки `ceiling_height` в таблице `houses` **не существует** (проверено: 0 колонок с `%ceiling%`). Правило не может сработать никогда. **2. Единицы у Авито не приведены.** В `ceiling_height_m` у Авито **21 строка больше 10 метров**, максимум **29.90**, минимум 1.70. У Яндекса в той же колонке максимум 6.00. Похоже на непереведённые сантиметры или мис-парс формата — то есть колонка, которую читает эстиматор, содержит заведомо невозможные значения. ## Что предлагается Свести к одной колонке до того, как флаг включат: выбрать канон по тому, что уже читает эстиматор либо что назначено картой приоритетов (это разные ответы — решение надо принять явно, а не унаследовать), перевести писателей, привести исторические значения, отфильтровать невозможные. Плюс убрать домовое правило, ссылающееся на несуществующую колонку. Связано: #2674, #2012 (откуда сигнал), #2435.
Author
Collaborator

Прод-верификация после деплоя (PR #2779 смержен, мигр. 238 применена 2026-08-07 09:25 UTC).

Критерий был записан в PR ДО мержа — совпал точно:

было ожидание стало
count(ceiling_height_m) 14 824 15 591 15 591
значений вне 2.0–6.0 в каноне 110 0 0
count(ceiling_height) (DEPRECATED) 8 554 8 552 8 552
source  | ceiling_height_m | ceiling_height
yandex  |             7699 |           7699
avito   |             7039 |              0
cian    |              853 |            853

Циан из невидимых 855 стал видимым 853 (2 отсеяны как невозможные). Аvito потерял 110 мусорных из 7 149.

Код в живом контейнере (проверено выполнением внутри tradein-scraper, не по метке релиза):

gate module live: None 2.7      # plausible_ceiling_m(29.9) → None
cian пишет канон: True
base не пишет 019: True
avito gate: None 2.7            # _parse_height_m("29,9 м") → None

Комментарии колонок на месте: ceiling_height = DEPRECATED (#2699...), ceiling_height_m = КАНОН (#2699).

Что осталось и когда проверять. ceiling_height оставлена в схеме намеренно. Критерий для сноса, записанный сейчас: через сутки (2026-08-08) count(ceiling_height) по-прежнему 8 552 — значит писателей действительно нет. После этого DROP COLUMN отдельным PR за database-expert.

Отдельно — что оказалось неправдой в исходной постановке. Денежного искажения не было и при включении флага не появилось бы: замер бэктестом (200 сделок, полный ценовой спайн) в трёх конфигурациях дал 0 изменившихся оценок во всех парах. Сигнал только переупорядочивает аналоги, а медиана ₽/м² к порядку безразлична; состав пула меняется лишь выше порога 50, а медиана размера пула — 2.

**Прод-верификация после деплоя (PR #2779 смержен, мигр. 238 применена 2026-08-07 09:25 UTC).** Критерий был записан в PR ДО мержа — совпал точно: | | было | ожидание | стало | |---|---|---|---| | `count(ceiling_height_m)` | 14 824 | **15 591** | **15 591** ✅ | | значений вне 2.0–6.0 в каноне | 110 | **0** | **0** ✅ | | `count(ceiling_height)` (DEPRECATED) | 8 554 | 8 552 | **8 552** ✅ | ``` source | ceiling_height_m | ceiling_height yandex | 7699 | 7699 avito | 7039 | 0 cian | 853 | 853 ``` Циан из невидимых 855 стал видимым 853 (2 отсеяны как невозможные). Аvito потерял 110 мусорных из 7 149. Код в живом контейнере (проверено выполнением внутри `tradein-scraper`, не по метке релиза): ``` gate module live: None 2.7 # plausible_ceiling_m(29.9) → None cian пишет канон: True base не пишет 019: True avito gate: None 2.7 # _parse_height_m("29,9 м") → None ``` Комментарии колонок на месте: `ceiling_height` = `DEPRECATED (#2699...)`, `ceiling_height_m` = `КАНОН (#2699)`. **Что осталось и когда проверять.** `ceiling_height` оставлена в схеме намеренно. Критерий для сноса, записанный сейчас: **через сутки (2026-08-08) `count(ceiling_height)` по-прежнему 8 552** — значит писателей действительно нет. После этого DROP COLUMN отдельным PR за database-expert. **Отдельно — что оказалось неправдой в исходной постановке.** Денежного искажения не было и при включении флага не появилось бы: замер бэктестом (200 сделок, полный ценовой спайн) в трёх конфигурациях дал **0 изменившихся оценок** во всех парах. Сигнал только переупорядочивает аналоги, а медиана ₽/м² к порядку безразлична; состав пула меняется лишь выше порога 50, а медиана размера пула — 2.
Author
Collaborator

Working on the tail of this in PR #2799 — снос DEPRECATED-колонки listings.ceiling_height (238 её намеренно оставила до прод-подтверждения).

Критерий «писателей нет», записанный заранее, выполнен: count(ceiling_height) = 8552 на 07.08, 09.08 16:54, 16:57 и 17:16 UTC. Контроль живости замера: за те же минуты count(ceiling_height_m) вырос 16133 → 16147 → 16153, а last_seen_at строк с непустым ceiling_height обновлялся в минуту замера — UPDATE идут по этим строкам прямо сейчас и колонку не трогают.

Потребители сверены на origin/main: обращений к колонке нет (в grep только имена полей датаклассов, имя бинд-параметра :ceiling_height, который присваивается ceiling_height_m, комментарии и тесты, утверждающие отсутствие). Зависимости в схеме — все нули, потери данных 0 (строк «ceiling_height есть, канона нет» — 0).

Остальные два дефекта из тела issue (HOUSE_FIELD_PRIORITY на несуществующую колонку houses.ceiling_height и непереведённые единицы у Авито) закрыты раньше — 238 и PR #2779.

Working on the tail of this in PR #2799 — снос DEPRECATED-колонки listings.ceiling_height (238 её намеренно оставила до прод-подтверждения). Критерий «писателей нет», записанный заранее, выполнен: count(ceiling_height) = 8552 на 07.08, 09.08 16:54, 16:57 и 17:16 UTC. Контроль живости замера: за те же минуты count(ceiling_height_m) вырос 16133 → 16147 → 16153, а last_seen_at строк с непустым ceiling_height обновлялся в минуту замера — UPDATE идут по этим строкам прямо сейчас и колонку не трогают. Потребители сверены на origin/main: обращений к колонке нет (в grep только имена полей датаклассов, имя бинд-параметра :ceiling_height, который присваивается ceiling_height_m, комментарии и тесты, утверждающие отсутствие). Зависимости в схеме — все нули, потери данных 0 (строк «ceiling_height есть, канона нет» — 0). Остальные два дефекта из тела issue (HOUSE_FIELD_PRIORITY на несуществующую колонку houses.ceiling_height и непереведённые единицы у Авито) закрыты раньше — 238 и PR #2779.
Author
Collaborator

Прод-верификация хвоста, PR #2799 смержен 17:51:18 UTC.

Запись в _schema_migrations: 251_listings_drop_ceiling_height.sql — applied_at 2026-08-09 17:56:13 UTC.

Схема: в listings осталась одна колонка по маске %ceiling%ceiling_height_m. attisdropped 4 → 5, живых колонок 92 → 91 (метаданные, heap не переписывался). COMMENT на каноне обновлён.

Канон цел: count(ceiling_height_m) = 16 153 на момент сноса, min 2.00 / max 6.00 — гейт правдоподобия из 238 держится, ни одного значения вне 2.0-6.0. Чтение таблицы работает, sanity-count совпадает.

Писатели против новой схемы — проверено отдельно. Снос пришёлся на окно, когда скрейпинг молчал (последняя запись в listings была в 16:58, за час до), поэтому сразу после деплоя утверждать «писатели работают» было нельзя — только «ошибок нет». Дождался ближайшего запланированного свипа (18:51 UTC):

строк с last_seen_at > 17:56:13 (момент применения) ... 289
из них с ceiling_height_m ......................... 255
count(ceiling_height_m) ................ 16 153 → 16 191
всего строк ............................ 97 576 → 97 602
схемных ошибок в логах tradein-scraper ................ 0

То есть после сноса колонки скрейпер и вставляет новые объявления, и обновляет старые, и продолжает писать высоту потолков в канон.

Невалидных индексов: 0.

Все три дефекта из тела issue закрыты: две колонки сведены к одной (238 + это), домовое правило на несуществующую houses.ceiling_height убрано (#2779), непереведённые единицы Авито отфильтрованы гейтом plausible_ceiling_m. Со стороны БД задача исчерпана — закрытие оставляю владельцу issue.

Прод-верификация хвоста, PR #2799 смержен 17:51:18 UTC. **Запись в _schema_migrations:** `251_listings_drop_ceiling_height.sql` — applied_at 2026-08-09 17:56:13 UTC. **Схема:** в `listings` осталась одна колонка по маске `%ceiling%` — `ceiling_height_m`. attisdropped 4 → 5, живых колонок 92 → 91 (метаданные, heap не переписывался). COMMENT на каноне обновлён. **Канон цел:** count(ceiling_height_m) = 16 153 на момент сноса, min 2.00 / max 6.00 — гейт правдоподобия из 238 держится, ни одного значения вне 2.0-6.0. Чтение таблицы работает, sanity-count совпадает. **Писатели против новой схемы — проверено отдельно.** Снос пришёлся на окно, когда скрейпинг молчал (последняя запись в listings была в 16:58, за час до), поэтому сразу после деплоя утверждать «писатели работают» было нельзя — только «ошибок нет». Дождался ближайшего запланированного свипа (18:51 UTC): ``` строк с last_seen_at > 17:56:13 (момент применения) ... 289 из них с ceiling_height_m ......................... 255 count(ceiling_height_m) ................ 16 153 → 16 191 всего строк ............................ 97 576 → 97 602 схемных ошибок в логах tradein-scraper ................ 0 ``` То есть после сноса колонки скрейпер и вставляет новые объявления, и обновляет старые, и продолжает писать высоту потолков в канон. **Невалидных индексов:** 0. Все три дефекта из тела issue закрыты: две колонки сведены к одной (238 + это), домовое правило на несуществующую houses.ceiling_height убрано (#2779), непереведённые единицы Авито отфильтрованы гейтом plausible_ceiling_m. Со стороны БД задача исчерпана — закрытие оставляю владельцу issue.
Author
Collaborator

Прод 12.08: снос доехал, канон живой — закрываю

Хвост, оставленный за владельцем 09.08, проверен на сегодняшнем проде.

Схема. В listings по маске %ceiling% осталась одна колонка — ceiling_height_m. Обе миграции в _schema_migrations:

238_listings_ceiling_height_unify.sql   applied_at 2026-08-07 09:25:21
251_listings_drop_ceiling_height.sql    applied_at 2026-08-09 17:56:13

Канон жив и растёт: 16 665 непустых из 99 155 строк (было 16 153 в момент сноса), min 2.00 / max 6.00 — гейт правдоподобия из 238 держится третьи сутки под боевой записью, ни одного значения вне 2.0-6.0. Невалидных индексов по всей БД — 0.

Три дефекта из тела задачи, поштучно:

  1. две колонки, эстиматор читает не ту — сведены к одной (238 свёл, 251 снёс). 855 циановских значений, которых «не видел никто», теперь лежат в той колонке, которую читает эстиматор;
  2. HOUSE_FIELD_PRIORITY["ceiling_height"] на несуществующую houses.ceiling_height — запись удалена, на её месте комментарий с обоснованием (conflict_resolution.py:78-81); в LISTING_FIELD_PRIORITY ключ теперь ceiling_height_m с тремя источниками, а не одним;
  3. непереведённые единицы Авито (21 строка >10 м, max 29.90) — отсекаются plausible_ceiling_m, что и подтверждается сегодняшним max 6.00 при живой записи.

В коде на origin/main бессуффиксная ceiling_height встречается только в комментариях, объясняющих её отсутствие.

Отдельно из старого разбора остаётся в силе и не требует действий: денежного искажения не было и при включении флага не появилось бы — бэктест 200 сделок в трёх конфигурациях дал 0 изменившихся оценок.

## Прод 12.08: снос доехал, канон живой — закрываю Хвост, оставленный за владельцем 09.08, проверен на сегодняшнем проде. **Схема.** В `listings` по маске `%ceiling%` осталась **одна** колонка — `ceiling_height_m`. Обе миграции в `_schema_migrations`: ``` 238_listings_ceiling_height_unify.sql applied_at 2026-08-07 09:25:21 251_listings_drop_ceiling_height.sql applied_at 2026-08-09 17:56:13 ``` **Канон жив и растёт**: 16 665 непустых из 99 155 строк (было 16 153 в момент сноса), **min 2.00 / max 6.00** — гейт правдоподобия из 238 держится третьи сутки под боевой записью, ни одного значения вне 2.0-6.0. Невалидных индексов по всей БД — 0. **Три дефекта из тела задачи, поштучно:** 1. *две колонки, эстиматор читает не ту* — сведены к одной (238 свёл, 251 снёс). 855 циановских значений, которых «не видел никто», теперь лежат в той колонке, которую читает эстиматор; 2. *`HOUSE_FIELD_PRIORITY["ceiling_height"]` на несуществующую `houses.ceiling_height`* — запись удалена, на её месте комментарий с обоснованием (`conflict_resolution.py:78-81`); в `LISTING_FIELD_PRIORITY` ключ теперь `ceiling_height_m` с тремя источниками, а не одним; 3. *непереведённые единицы Авито (21 строка >10 м, max 29.90)* — отсекаются `plausible_ceiling_m`, что и подтверждается сегодняшним max 6.00 при живой записи. В коде на `origin/main` бессуффиксная `ceiling_height` встречается только в комментариях, объясняющих её отсутствие. Отдельно из старого разбора остаётся в силе и не требует действий: денежного искажения не было и при включении флага не появилось бы — бэктест 200 сделок в трёх конфигурациях дал 0 изменившихся оценок.
Sign in to join this conversation.
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#2699
No description provided.