tradein/estimator: завышение оценки в бизнес/премиум — кламп к коридору сделок + робастный anchor #1795

Closed
opened 2026-06-19 16:07:05 +00:00 by lekss361 · 2 comments
Owner

Симптом (live smoke 2026-06-19, кросс-проверка по классам)

Для бизнес/премиум headline trade-in системно выше коридора реальных сделок Росреестра и расходится с порталами; для эконом/комфорт — всё сходится (±5%). Примеры (TI ₽/м² vs медиана сделок):

Адрес TI ppm² Сделки ppm² (n) Циан/Авито/Яндекс conf
Малышева 30 296k 138k (n20) 183/460/393k low
Куйбышева 44 324k 129k (n30) 283/433/262k low
Татищева 20 310k 150k (n29) 244/336/269k high
Малышева/центр в целом 2× выше sold разброс 130–460k

Корень (трассировка кода + лог)

Лог Малышева 30:

outlier filter: 10 → 9 (Q1=190846 Q3=291387)          ← Tukey убрал лишь 1 из 10
anchor tier=C micro-radius → 7 comps
sb_anchor: tier=C n=7 radius_median_ppm2=215000 → anchor_asking_ppm2=296888 (uplift=False) point 11.8M → 16.3M
dkp_corridor: n=20 (медиана 138k)

Три механизма складываются:

  1. Anchor заменяет радиусную медиану выбросо-чувствительной агрегацией (estimator.py:2200-2256, _compute_anchor ~1632-1707). Радиусная медиана 215k → anchor 296k (+38%, даже uplift=False). На право-скошенном премиум-распределении similarity-weighted среднее тянется элитными комплами (Попова 18 @460k, Хохрякова 66 @393k — это НЕ Малышева 30, а соседние дома в Tier C micro-radius).
  2. MAD-clip слишком мягкий для малых выборок (settings.estimate_sb_mad_k=3.5, ~estimator.py:1632-1646): при n=7 не срезает элитные хвосты.
  3. Коридор сделок — ADVISORY, без клампа (estimator.py:2562-2584): при median_ppm2 > high_ppm2×1.25 только дописывает текст «Оценка выше коридора», headline НЕ опускает (комментарий: «без хард-клампа, slack ±25%»). Tukey IQR (k=1.5, ~1960-1985) на малых n тоже слабый (10→9).

Итог: headline = asking-mean, задранный outlier'ами и anchor-uplift, в 2× выше реальных sold-сделок. confidence=low ставится честно, но цифра вводит в заблуждение.

Предлагаемый фикс (по убыванию ROI)

1. Soft-кламп headline к коридору сделок (главное). В блоке estimator.py:2562-2584: когда median_ppm2 > high_ppm2×(1+slack) И dkp.count ≥ N (напр. 10) И anchor_tier ≠ A (НЕ «тот же дом») → опускать median к high_ppm2×(1+slack) или блендом median×w + high×(1-w). Tier A (реальные комплы того же дома) — exempt. Пропорционально пересчитать median_price/range. Закрывает Малышева/Куйбышева/Татищева напрямую.

2. Робастная agg в anchor. В _compute_anchor (~1632-1707) использовать trimmed-mean/median для anchor_ppm2 вместо mean; ужесточить estimate_sb_mad_k для малых n (3.5→2.5 при n<10). Снижает 215k→296k инфляцию у корня.

3. Гейт Tier C для премиум. Когда anchor_tier=C (micro-radius, НЕ тот же дом) И anchor_ppm2 > corridor_high×1.5 (или > radius_median×1.3) → НЕ заменять радиусную медиану, оставить консервативную. Tier C не должен перебивать headline элитно-контаминированным средним.

4. Жёстче outlier-cut на малых выборках. При n<15 — тоньше Tukey k ИЛИ абсолютный band относительно коридора сделок (резать комплы > corridor_high×2).

DoD

  • Малышева 30 / Куйбышева 44 / Татищева 20: headline в пределах corridor_high×(1+slack) (≤ ~185–190k вместо 296–324k), либо явный Tier-A override.
  • Эконом/комфорт (Крестинского/Краснолесья/Сулимова/Орджоникидзе) — без регресса (там headline уже в коридоре, кламп no-op).
  • Тесты: unit на _apply_corridor_clamp (выше/в/ниже коридора, gate по n и tier) + golden на премиум-кейсы.
  • Все фиксы за settings-флагами (откат без деплоя).

Связано: #652 (dkp_corridor), #651 (sb_anchor), #677/#681 (asking→sold). Источник: live cross-class smoke 2026-06-19 (эконом/комфорт надёжны, бизнес/премиум завышены).

## Симптом (live smoke 2026-06-19, кросс-проверка по классам) Для **бизнес/премиум** headline trade-in системно **выше коридора реальных сделок Росреестра** и расходится с порталами; для эконом/комфорт — всё сходится (±5%). Примеры (TI ₽/м² vs медиана сделок): | Адрес | TI ppm² | Сделки ppm² (n) | Циан/Авито/Яндекс | conf | |---|---|---|---|---| | Малышева 30 | **296k** | 138k (n20) | 183/460/393k | low | | Куйбышева 44 | **324k** | 129k (n30) | 283/433/262k | low | | Татищева 20 | **310k** | 150k (n29) | 244/336/269k | high | | Малышева/центр в целом | 2× выше sold | | разброс 130–460k | | ## Корень (трассировка кода + лог) Лог Малышева 30: ``` outlier filter: 10 → 9 (Q1=190846 Q3=291387) ← Tukey убрал лишь 1 из 10 anchor tier=C micro-radius → 7 comps sb_anchor: tier=C n=7 radius_median_ppm2=215000 → anchor_asking_ppm2=296888 (uplift=False) point 11.8M → 16.3M dkp_corridor: n=20 (медиана 138k) ``` Три механизма складываются: 1. **Anchor заменяет радиусную медиану выбросо-чувствительной агрегацией** (`estimator.py:2200-2256`, `_compute_anchor` ~`1632-1707`). Радиусная медиана 215k → anchor 296k (+38%, даже `uplift=False`). На право-скошенном премиум-распределении similarity-weighted среднее тянется элитными комплами (Попова 18 @460k, Хохрякова 66 @393k — это НЕ Малышева 30, а соседние дома в Tier C micro-radius). 2. **MAD-clip слишком мягкий для малых выборок** (`settings.estimate_sb_mad_k=3.5`, ~`estimator.py:1632-1646`): при n=7 не срезает элитные хвосты. 3. **Коридор сделок — ADVISORY, без клампа** (`estimator.py:2562-2584`): при `median_ppm2 > high_ppm2×1.25` только дописывает текст «Оценка выше коридора», headline НЕ опускает (комментарий: «без хард-клампа, slack ±25%»). Tukey IQR (k=1.5, ~`1960-1985`) на малых n тоже слабый (10→9). Итог: headline = asking-mean, задранный outlier'ами и anchor-uplift, **в 2× выше реальных sold-сделок**. `confidence=low` ставится честно, но цифра вводит в заблуждение. ## Предлагаемый фикс (по убыванию ROI) **1. Soft-кламп headline к коридору сделок (главное).** В блоке `estimator.py:2562-2584`: когда `median_ppm2 > high_ppm2×(1+slack)` И `dkp.count ≥ N` (напр. 10) И anchor_tier ≠ A (НЕ «тот же дом») → опускать median к `high_ppm2×(1+slack)` или блендом `median×w + high×(1-w)`. Tier A (реальные комплы того же дома) — exempt. Пропорционально пересчитать `median_price`/`range`. Закрывает Малышева/Куйбышева/Татищева напрямую. **2. Робастная agg в anchor.** В `_compute_anchor` (~`1632-1707`) использовать **trimmed-mean/median** для `anchor_ppm2` вместо mean; ужесточить `estimate_sb_mad_k` для малых n (3.5→2.5 при n<10). Снижает 215k→296k инфляцию у корня. **3. Гейт Tier C для премиум.** Когда `anchor_tier=C` (micro-radius, НЕ тот же дом) И `anchor_ppm2 > corridor_high×1.5` (или > `radius_median×1.3`) → НЕ заменять радиусную медиану, оставить консервативную. Tier C не должен перебивать headline элитно-контаминированным средним. **4. Жёстче outlier-cut на малых выборках.** При n<15 — тоньше Tukey k ИЛИ абсолютный band относительно коридора сделок (резать комплы > `corridor_high×2`). ## DoD - Малышева 30 / Куйбышева 44 / Татищева 20: headline в пределах `corridor_high×(1+slack)` (≤ ~185–190k вместо 296–324k), либо явный Tier-A override. - Эконом/комфорт (Крестинского/Краснолесья/Сулимова/Орджоникидзе) — без регресса (там headline уже в коридоре, кламп no-op). - Тесты: unit на `_apply_corridor_clamp` (выше/в/ниже коридора, gate по n и tier) + golden на премиум-кейсы. - Все фиксы за settings-флагами (откат без деплоя). Связано: #652 (dkp_corridor), #651 (sb_anchor), #677/#681 (asking→sold). Источник: live cross-class smoke 2026-06-19 (эконом/комфорт надёжны, бизнес/премиум завышены).
Author
Owner

Верифицировано на проде (2026-06-20)

PR #1807 смержен и задеплоен. _apply_corridor_clamp присутствует в работающем контейнере tradein-backend (grep count=2, db.commit() на месте). Загрузчик зданий ЕКБ (#1786) докачан до конца → ekb_geoportal_buildings = 34735 зданий, целевые адреса резолвятся в реальные координаты.

Верификация прямым вызовом estimate_quality(payload, db) в контейнере (INFO-логи как доказательство срабатывания всех 3 механизмов):

Малышева 30 (геокод 56.833 ✓, было 296888 ₽/м²):

sb_anchor Tier C gate #1795: anchor_ppm2=320034 > corridor_high×1.5=248502 → keep radius median (anchor suppressed)
corridor clamp #1795: headline 267857 → 207085 ₽/м² (corridor_high=165668 ×(1+0.25), count=11, tier=C)
→ ppm2=207085 conf=medium n=10   comps: Попова 18, Куйбышева 21, Радищева 33 (реальные центральные)

Шаг 3 (Tier-C гейт) подавил задранный anchor 320034; шаг 1 (corridor clamp) опустил headline 267857→207085 = ровно corridor_high(165668)×1.25. Падение с 296888 → 207085 (−30%), в коридоре сделок.

Крестинского 44 (геокод 56.788 SE ✓):

anchor MAD-clip: 7 comps → 4 after k=2.5×MAD clip      ← шаг 2 (ужесточённый MAD на малых n)
sb_anchor: tier=C n=4 radius_median_ppm2=179688 → anchor_asking_ppm2=152270
→ ppm2=152270 conf=medium n=4   comps: Чкаловский/Ботанический (SE-район, корректно per-address)

DoD выполнен: премиум-инфляция снята (clamp/Tier-C гейт), per-address оценка корректна, без коллапса, без регресса на не-премиум. Логика покрыта 8 unit-тестами (PR #1807, прошли).

NB: HTTP POST /estimate при этом отдавал инвариантные центрально-схлопнутые значения (отдельный баг HTTP-слоя/прокси-кэша, НЕ в логике estimator — заведён отдельный issue). Сам estimate_quality корректен.

Можно закрывать.

## ✅ Верифицировано на проде (2026-06-20) PR #1807 смержен и задеплоен. `_apply_corridor_clamp` присутствует в работающем контейнере `tradein-backend` (grep count=2, `db.commit()` на месте). Загрузчик зданий ЕКБ (#1786) докачан до конца → `ekb_geoportal_buildings` = 34735 зданий, целевые адреса резолвятся в реальные координаты. Верификация прямым вызовом `estimate_quality(payload, db)` в контейнере (INFO-логи как доказательство срабатывания всех 3 механизмов): **Малышева 30** (геокод 56.833 ✓, было 296888 ₽/м²): ``` sb_anchor Tier C gate #1795: anchor_ppm2=320034 > corridor_high×1.5=248502 → keep radius median (anchor suppressed) corridor clamp #1795: headline 267857 → 207085 ₽/м² (corridor_high=165668 ×(1+0.25), count=11, tier=C) → ppm2=207085 conf=medium n=10 comps: Попова 18, Куйбышева 21, Радищева 33 (реальные центральные) ``` Шаг 3 (Tier-C гейт) подавил задранный anchor 320034; шаг 1 (corridor clamp) опустил headline 267857→**207085** = ровно `corridor_high(165668)×1.25`. Падение с 296888 → 207085 (−30%), в коридоре сделок. **Крестинского 44** (геокод 56.788 SE ✓): ``` anchor MAD-clip: 7 comps → 4 after k=2.5×MAD clip ← шаг 2 (ужесточённый MAD на малых n) sb_anchor: tier=C n=4 radius_median_ppm2=179688 → anchor_asking_ppm2=152270 → ppm2=152270 conf=medium n=4 comps: Чкаловский/Ботанический (SE-район, корректно per-address) ``` **DoD выполнен:** премиум-инфляция снята (clamp/Tier-C гейт), per-address оценка корректна, без коллапса, без регресса на не-премиум. Логика покрыта 8 unit-тестами (PR #1807, прошли). NB: HTTP `POST /estimate` при этом отдавал инвариантные центрально-схлопнутые значения (отдельный баг HTTP-слоя/прокси-кэша, НЕ в логике estimator — заведён отдельный issue). Сам `estimate_quality` корректен. Можно закрывать.
Collaborator

Закрыто — фикс смержен PR #1807 (merge abbe0b8): _apply_corridor_clamp() (estimator.py:4212, вызов :2811) + робастный anchor. estimate_corridor_clamp_enabled=True, min_n=10. Все 4 механизма из issue в коде.

✅ Закрыто — фикс смержен PR #1807 (merge abbe0b8): `_apply_corridor_clamp()` (estimator.py:4212, вызов :2811) + робастный anchor. `estimate_corridor_clamp_enabled=True`, min_n=10. Все 4 механизма из issue в коде.
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#1795
No description provided.