fix(tradein/sale-share): гео-фильтр числителя ≤300м — отсечь mis-bucketed листинги #2080

Merged
lekss361 merged 1 commit from feat/tradein-sale-share-geofilter into main 2026-06-28 20:25:49 +00:00
Owner

Проблема (нашёл при QA пользователь)

Числитель «доли квартир в продаже» загрязнён: листинги примагничиваются к НЕ ТОМУ дому (кривой house_id_fk). Прод-замер: 373 дома (5.5%) / 1685 листингов (7.2%) дальше 300м от своего дома.

Пример — house 131237 «Мамина-Сибиряка 126» (8-эт., ЖКХ=51 кв — знаменатель верный): из 20 «активных» листингов реально его лишь ~2, остальные — Свердлова 32/32Б, Азина 25/31, Мамина 36Б/132 (разброс до 2285м). → ложная доля 39% вместо ~4%. Такие «мусорные вёдра» всплывают в ТОП высоких долей.

Фикс (митигация на уровне view + детали)

  • мигр.150 v_building_sale_share: числители (active_secondary/listings_45d) + медианы считают только листинги ≤300м от geom дома (ST_DistanceSphere; листинг/дом без geom — keep). Знаменатель (flat_count_effective, плаузибилити-гейты, ЖКХ-приоритет) не изменён. Только CTE listing_agg правится — top-level SELECT byte-identical 149.
  • buildings_query.build_listings_query: тот же гео-фильтр в детальной выдаче (карточка дома больше не показывает чужие листинги).

Порог 300м обоснован распределением: p90=97м, 91% листингов ≤150м (геокодинг ок), хвост-мусор p95=856м/p99=5.7км.

Импакт (симуляция на проде до мержа)

  • Меняется 351 дом, обнуляется 164 (чистые «мусорные вёдра»), общий числитель −8.3% (18519→16973).
  • Мамина-126: active 20 → 2 → доля 39% → ~4%.

Природа бага

Системная (кривой house_id_fk в данных), не во view. Это митигация на уровне представления — делает метрику и карточку честными сейчас. Корневой фикс (матчер листинг→дом: app/tasks/cadastral_geo_match.py, services/matching/houses.py, save-time + ре-backfill house_id_fk) — отдельным заходом, крупнее.

Проверки

  • ruff clean; pytest test_buildings_api.py — 24 green (обновлён test_listings_query_shape).
  • Прод-симуляция логики подтверждена (Мамина 20→2, 351 дом).
  • Мигр.150 идемпотентна (CREATE OR REPLACE VIEW), деплой после 149.

После мержа

  1. Deploy авто-применит мигр.150.
  2. Verify: Мамина-126 доля ~4% (не 39%); карточка без чужих листингов.
  3. Замер нового ≥5%-распределения.
## Проблема (нашёл при QA пользователь) Числитель «доли квартир в продаже» **загрязнён**: листинги примагничиваются к НЕ ТОМУ дому (кривой `house_id_fk`). Прод-замер: **373 дома (5.5%) / 1685 листингов (7.2%)** дальше 300м от своего дома. Пример — house 131237 «Мамина-Сибиряка 126» (8-эт., ЖКХ=51 кв — **знаменатель верный**): из 20 «активных» листингов реально его лишь ~2, остальные — Свердлова 32/32Б, Азина 25/31, Мамина 36Б/132 (разброс до **2285м**). → ложная доля 39% вместо ~4%. Такие «мусорные вёдра» всплывают в ТОП высоких долей. ## Фикс (митигация на уровне view + детали) - **мигр.150** `v_building_sale_share`: числители (`active_secondary`/`listings_45d`) + медианы считают **только листинги ≤300м от `geom` дома** (`ST_DistanceSphere`; листинг/дом без geom — keep). Знаменатель (`flat_count_effective`, плаузибилити-гейты, ЖКХ-приоритет) **не изменён**. Только CTE `listing_agg` правится — top-level SELECT byte-identical 149. - **`buildings_query.build_listings_query`**: тот же гео-фильтр в детальной выдаче (карточка дома больше не показывает чужие листинги). Порог 300м обоснован распределением: p90=97м, **91% листингов ≤150м** (геокодинг ок), хвост-мусор p95=856м/p99=5.7км. ## Импакт (симуляция на проде до мержа) - Меняется **351 дом**, обнуляется **164** (чистые «мусорные вёдра»), общий числитель **−8.3%** (18519→16973). - Мамина-126: active **20 → 2** → доля 39% → ~4%. ## Природа бага **Системная** (кривой `house_id_fk` в данных), не во view. Это **митигация** на уровне представления — делает метрику и карточку честными сейчас. **Корневой фикс** (матчер листинг→дом: `app/tasks/cadastral_geo_match.py`, `services/matching/houses.py`, save-time + ре-backfill `house_id_fk`) — отдельным заходом, крупнее. ## Проверки - ruff clean; `pytest test_buildings_api.py` — 24 green (обновлён `test_listings_query_shape`). - Прод-симуляция логики подтверждена (Мамина 20→2, 351 дом). - Мигр.150 идемпотентна (CREATE OR REPLACE VIEW), деплой после 149. ## После мержа 1. Deploy авто-применит мигр.150. 2. Verify: Мамина-126 доля ~4% (не 39%); карточка без чужих листингов. 3. Замер нового ≥5%-распределения.
lekss361 added 1 commit 2026-06-28 20:24:55 +00:00
fix(tradein/sale-share): гео-фильтр числителя ≤300м — отсечь mis-bucketed листинги
All checks were successful
CI / changes (pull_request) Successful in 9s
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
f617c323bd
Листинги примагничиваются к НЕ ТОМУ дому (кривой house_id_fk): прод-замер 373
дома (5.5%) / 1685 листингов (7.2%) дальше 300м от своего дома. Пример — house
131237 «Мамина-Сибиряка 126» (ЖКХ=51 кв, знаменатель ВЕРНЫЙ): из 20 активных
листингов реально его ~2, остальные — Свердлова 32 / Азина 31 (до 2.2км) →
ложная доля 39% вместо ~4%.

- мигр.150: v_building_sale_share — числители (active/45д) + медианы считают
  только листинги ≤300м от geom дома (ST_DistanceSphere; листинг/дом без geom —
  keep). Знаменатель не тронут.
- buildings_query.build_listings_query: тот же гео-фильтр в детальной выдаче
  (карточка дома больше не показывает чужие листинги).

Симуляция на проде: меняется 351 дом, обнуляется 164 (чистые «мусорные вёдра»),
числитель −8.3%. Мамина-126: 20→2. Это митигация на уровне view; корневой фикс
house_id_fk-матчера — отдельно. ruff clean, pytest 24 green.
lekss361 merged commit bd3562b6bd into main 2026-06-28 20:25:49 +00:00
lekss361 deleted branch feat/tradein-sale-share-geofilter 2026-06-28 20:25:49 +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#2080
No description provided.