[P2] Кросс-source дедуп: ключ схлопывает лишь 13.5% дублей — канонизация по house_id_fk (follow-up #2215) #2269

Closed
opened 2026-07-03 06:41:45 +00:00 by lekss361 · 3 comments
Owner

Из адверсариальной верификации закрытий эпика #2200 (2026-07-03, read-only SQL на проде).

Факт

Механизм _dedup_cross_source (#2215, estimator.py:4586) смержен, включён (estimate_dedup_analogs_enabled=True) и вызывается до outlier/медианы/n_analogs — но малоэффективен на реальных данных:

  • Ключ = «кадастр-или-raw-адрес», а 76.3% активных листингов без building_cadastral_number; raw-адреса разных источников нормализуются в разные строки.
  • По метрике самого issue (house_id_fk + floor + round(area) + price_bucket): ключ склеивает 859 из 6366 кросс-source дублей (13.5%) → 86.5% двойного счёта переживает дедуп.
  • Живой пример: свежайшая прод-оценка d5fc3841 (02.07 21:29 UTC, ПОСЛЕ включения флага) — в итоговых analogs одна квартира ×3 источника (Сыромолотова 11, эт. 3, 34 м², 4 000 000 ₽ на cian+yandex+domklik) и вторая ×2 (Рассветная 11А, эт. 11).

Т.е. acceptance-пункт #2215 «n_analogs и confidence не раздуты дублями» на реальных данных не достигнут — сам механизм корректен, неудачен выбор ключа.

Предложение

  • Перевести ключ дедупа на канонический дом: house_id_fk (уже есть в listings и покрывает большинство активных строк) + floor + round(area) + price_bucket; фолбэк на текущий ключ там, где house_id_fk IS NULL.
  • Замер до/после на проде (read-only SQL): доля переживших дедуп кросс-source дублей должна упасть с 86.5% к <20%.
  • Backtest-гейт: scripts/backtest_estimator.py до/после (дедуп меняет пул аналогов → MAPE/coverage могут сдвинуться; frozen baseline не трогать).
  • Sanity: не схлопывать РАЗНЫЕ квартиры одного дома/этажа/метража с разной ценой (price_bucket остаётся в ключе).

Refs #2215, #2200

Из адверсариальной верификации закрытий эпика #2200 (2026-07-03, read-only SQL на проде). ## Факт Механизм `_dedup_cross_source` (#2215, `estimator.py:4586`) смержен, включён (`estimate_dedup_analogs_enabled=True`) и вызывается до outlier/медианы/n_analogs — но **малоэффективен на реальных данных**: - Ключ = «кадастр-или-raw-адрес», а **76.3% активных листингов без `building_cadastral_number`**; raw-адреса разных источников нормализуются в разные строки. - По метрике самого issue (house_id_fk + floor + round(area) + price_bucket): ключ склеивает **859 из 6366** кросс-source дублей (13.5%) → **86.5% двойного счёта переживает дедуп**. - Живой пример: свежайшая прод-оценка `d5fc3841` (02.07 21:29 UTC, ПОСЛЕ включения флага) — в итоговых analogs одна квартира ×3 источника (Сыромолотова 11, эт. 3, 34 м², 4 000 000 ₽ на cian+yandex+domklik) и вторая ×2 (Рассветная 11А, эт. 11). Т.е. acceptance-пункт #2215 «n_analogs и confidence не раздуты дублями» на реальных данных не достигнут — сам механизм корректен, неудачен выбор ключа. ## Предложение - [ ] Перевести ключ дедупа на канонический дом: `house_id_fk` (уже есть в listings и покрывает большинство активных строк) + floor + round(area) + price_bucket; фолбэк на текущий ключ там, где house_id_fk IS NULL. - [ ] Замер до/после на проде (read-only SQL): доля переживших дедуп кросс-source дублей должна упасть с 86.5% к <20%. - [ ] Backtest-гейт: `scripts/backtest_estimator.py` до/после (дедуп меняет пул аналогов → MAPE/coverage могут сдвинуться; frozen baseline не трогать). - [ ] Sanity: не схлопывать РАЗНЫЕ квартиры одного дома/этажа/метража с разной ценой (price_bucket остаётся в ключе). Refs #2215, #2200
Author
Owner

⚠️ Пересечение: параллельное окно смержило PR #2273 (issue #2265, эпик #2262) — «кросс-source дедуп street+house union-find» — это удар ровно в описанную здесь слабость ключа (raw-адреса не унифицируются между источниками). Перед началом работы по этому issue: перемерить метрику на проде после деплоя #2273 (read-only SQL из описания: доля переживших дедуп кросс-source дублей по house_id_fk+floor+round(area)+price_bucket, было 86.5%) и живую оценку. Если union-find закрыл gap — закрыть этот issue как superseded by #2273; если остаток существенный — сузить scope до остатка.

⚠️ Пересечение: параллельное окно смержило PR #2273 (issue #2265, эпик #2262) — «кросс-source дедуп street+house union-find» — это удар ровно в описанную здесь слабость ключа (raw-адреса не унифицируются между источниками). Перед началом работы по этому issue: **перемерить** метрику на проде после деплоя #2273 (read-only SQL из описания: доля переживших дедуп кросс-source дублей по house_id_fk+floor+round(area)+price_bucket, было 86.5%) и живую оценку. Если union-find закрыл gap — закрыть этот issue как superseded by #2273; если остаток существенный — сузить scope до остатка.
Collaborator

Перезамер после #2273 (2026-07-03, прод, живой код дедупа из контейнера)

Деплой #2273 подтверждён (union-find в контейнере, флаг включён). Метрика — воспроизведением РЕАЛЬНОГО ключа (импорт _dedup_cross_source/_parse_street_house из прод-образа, union-find в SQL не воспроизводим):

до #2273 после
Схлопывается 13.5% 61.2%
Переживает дедуп 86.5% 38.8% (2 497 лишних из 6 429)

Вердикт: НЕ superseded — сузить scope. Живой пример после деплоя: оценка 23b8ada1 — domklik «Рассветная улица, 6 к1» + cian «улица Рассветная, 6к1» (эт.13, ~38.5 м², 6 599 000 ₽ оба) НЕ склеены.

Root-cause остатка_DEDUP_STREET_NAME_RE требует <тип> <название> и молчит на:

  1. имя ПЕРЕД типом: «Рассветная улица, 6 к1» (формат domklik — 48% его листингов без токена, кадастр у domklik всегда пуст → ноль дедуп-сигнала у 1 691/3 504);
  2. тип-суффикс без пробела: «Рассветная ул.,6к1» (avito, 447);
  3. адрес без уличного типа: «…, Светлый микрорайон, 10» (микрорайонная застройка).

118 групп с конфликтом кадастров — корректный блок разных зданий, из scope исключены.

Суженный scope: расширить _parse_street_house на форматы (1)-(3) + микрорайон-fallback. Acceptance тот же: прод-перезамер (цель <20% выживших), backtest-гейт, frozen baseline не трогать.

## Перезамер после #2273 (2026-07-03, прод, живой код дедупа из контейнера) Деплой #2273 подтверждён (union-find в контейнере, флаг включён). Метрика — воспроизведением РЕАЛЬНОГО ключа (импорт `_dedup_cross_source`/`_parse_street_house` из прод-образа, union-find в SQL не воспроизводим): | | до #2273 | после | |---|---|---| | Схлопывается | 13.5% | **61.2%** | | Переживает дедуп | 86.5% | **38.8%** (2 497 лишних из 6 429) | **Вердикт: НЕ superseded — сузить scope.** Живой пример после деплоя: оценка `23b8ada1` — domklik «Рассветная улица, 6 к1» + cian «улица Рассветная, 6к1» (эт.13, ~38.5 м², 6 599 000 ₽ оба) НЕ склеены. **Root-cause остатка** — `_DEDUP_STREET_NAME_RE` требует `<тип> <название>` и молчит на: 1. имя ПЕРЕД типом: «Рассветная улица, 6 к1» (формат domklik — 48% его листингов без токена, кадастр у domklik всегда пуст → ноль дедуп-сигнала у 1 691/3 504); 2. тип-суффикс без пробела: «Рассветная ул.,6к1» (avito, 447); 3. адрес без уличного типа: «…, Светлый микрорайон, 10» (микрорайонная застройка). 118 групп с конфликтом кадастров — корректный блок разных зданий, из scope исключены. **Суженный scope:** расширить `_parse_street_house` на форматы (1)-(3) + микрорайон-fallback. Acceptance тот же: прод-перезамер (цель <20% выживших), backtest-гейт, frozen baseline не трогать.
Collaborator

Post-deploy перезамер (PR #2290, образ 5e68a403e9a1) — ЦЕЛЬ ДОСТИГНУТА

Этап % выживших кросс-source дублей
до #2273 86.5%
после #2273 (union-find) 38.8%
после #2290 (этот issue) 10.9% (705 из 6 442) — цель была <20%

Street-token работает: пара Рассветной 6к1 по токену теперь совпадает (обе стороны → «рассветная»). Backtest нейтрален-в-плюс (MAPE 13.55→13.55, coverage +0.36 п.п., 4+комн MAPE −1.9 п.п.).

Остаток 705 — by-design (127 кадастр-конфликтов) + четыре НОВЫХ отдельных класса, вынесены в follow-up issue (крупнейший: mkr-fallback перехватывает район при голом имени улицы без типа-слова — 334; ё/е-фолд ~60; area-bucket straddle 139 — именно он, а не токен, держит живую пару Рассветной: round(38.5)=38 vs round(39.0)=39).

Закрываю по достигнутому acceptance.

## Post-deploy перезамер (PR #2290, образ 5e68a403e9a1) — ЦЕЛЬ ДОСТИГНУТА | Этап | % выживших кросс-source дублей | |---|---| | до #2273 | 86.5% | | после #2273 (union-find) | 38.8% | | **после #2290 (этот issue)** | **10.9%** (705 из 6 442) — цель была <20% | Street-token работает: пара Рассветной 6к1 по токену теперь совпадает (обе стороны → «рассветная»). Backtest нейтрален-в-плюс (MAPE 13.55→13.55, coverage +0.36 п.п., 4+комн MAPE −1.9 п.п.). Остаток 705 — by-design (127 кадастр-конфликтов) + четыре НОВЫХ отдельных класса, вынесены в follow-up issue (крупнейший: mkr-fallback перехватывает район при голом имени улицы без типа-слова — 334; ё/е-фолд ~60; area-bucket straddle 139 — именно он, а не токен, держит живую пару Рассветной: round(38.5)=38 vs round(39.0)=39). Закрываю по достигнутому acceptance.
Sign in to join this conversation.
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#2269
No description provided.