fix(tradein/estimator): per-city ценовой band ДКП-сделок для всей Свердловской обл. (66) #2485

Merged
lekss361 merged 1 commit from fix/tradein-oblast-c1-price-band into main 2026-07-12 16:09:05 +00:00
Owner

Что

Глобальный плаузибл-band DEAL_MIN_PPM2=50_000 (estimator.py) молча резал 46.6% не-ЕКБ сделок — легитимные дешёвые города (Североуральск p50=21.7k ₽/м², Новоуральск 36k, Асбест 41k — все ниже 50k). Band стал per-city, чтобы дешёвые сделки выживали, а мусор (доли/ошибки) резался.

Изменения

  • migration 178 deal_city_price_bands (city PK, ppm2_min/max, n_deals, refreshed_at): populate из deals (source=rosreestr, city<>'Екатеринбург', count>=30), ppm2_min=GREATEST(p1,8000), ppm2_max=LEAST(p99,800000), idempotent upsert (ON CONFLICT DO UPDATE), HAVING-guard p99>=8000 (airtight против inverted band). Пример bands: Североуральск [9316, 45668], Серов [14504, 92500], Н.Тагил [16955, 108175].
  • estimator.py оба enforcement-пойнта city-aware, оба с fallback на глобаль:
    • _fetch_dkp_corridor SQL: LEFT JOIN deal_city_price_bands b + price_per_m2 BETWEEN COALESCE(b.ppm2_min, :ppm_min) AND COALESCE(b.ppm2_max, :ppm_max).
    • _is_plausible_deal(city, bands) + _fetch_deals: _load_city_price_bands (TTL-cache, graceful empty), SELECT добавил city.

EKB инвариант

'Екатеринбург' исключён из band-таблицы → оба пути (COALESCE / .get) fallback на глобальные 50k/800k → EKB-сделки byte-identical. Verified: deals спеллит EKB только как 'Екатеринбург' (49 791, без вариантов). Регресс-гейт байт-зелёный (фикстура реплеит _price_from_inputs, который upstream от этих фильтров — они не гоняются в реплее). Nuance (deep-review): interior-EKB оценки идентичны; boundary-EKB (radius <1км от banded-соседа) / common-street corridor могут подтянуть не-ЕКБ сделки — унаследовано от mig-177 (#2482), corridor advisory (display-only, не клампит).

Проверка

  • deep-code-review: 🟡 APPROVE, без 🔴/🟠 блокеров. Follow-ups учтены (inverted-band guard добавлен).
  • Полный tradein backend suite: 2390 passed (+ pre-existing test_search_cache_hit deselect, как в CI). ruff clean.
  • Post-deploy verify нужен: SELECT count(*) FROM deal_city_price_bands (должен быть >0 — не-ЕКБ сделки уже в проде после A2).

Follow-ups (не в этом PR)

  • Periodic refresh band-таблицы (deals растут ночью) — пока ручной re-run 178.
  • City-scoping _fetch_dkp_corridor (street-name collision между городами) → C2.

Зависит от A1+A2 (уже в проде: 96 974 сделки, +47 183 не-ЕКБ, геокод 99.9%).

## Что Глобальный плаузибл-band `DEAL_MIN_PPM2=50_000` (estimator.py) молча резал **46.6% не-ЕКБ сделок** — легитимные дешёвые города (Североуральск p50=21.7k ₽/м², Новоуральск 36k, Асбест 41k — все ниже 50k). Band стал **per-city**, чтобы дешёвые сделки выживали, а мусор (доли/ошибки) резался. ## Изменения - **migration 178** `deal_city_price_bands` (city PK, ppm2_min/max, n_deals, refreshed_at): populate из `deals` (source=rosreestr, `city<>'Екатеринбург'`, `count>=30`), `ppm2_min=GREATEST(p1,8000)`, `ppm2_max=LEAST(p99,800000)`, idempotent upsert (`ON CONFLICT DO UPDATE`), HAVING-guard `p99>=8000` (airtight против inverted band). Пример bands: Североуральск [9316, 45668], Серов [14504, 92500], Н.Тагил [16955, 108175]. - **estimator.py** оба enforcement-пойнта city-aware, оба с fallback на глобаль: - `_fetch_dkp_corridor` SQL: `LEFT JOIN deal_city_price_bands b` + `price_per_m2 BETWEEN COALESCE(b.ppm2_min, :ppm_min) AND COALESCE(b.ppm2_max, :ppm_max)`. - `_is_plausible_deal(city, bands)` + `_fetch_deals`: `_load_city_price_bands` (TTL-cache, graceful empty), SELECT добавил `city`. ## EKB инвариант 'Екатеринбург' **исключён** из band-таблицы → оба пути (COALESCE / `.get`) fallback на глобальные 50k/800k → **EKB-сделки byte-identical**. Verified: `deals` спеллит EKB только как `'Екатеринбург'` (49 791, без вариантов). Регресс-гейт байт-зелёный (фикстура реплеит `_price_from_inputs`, который upstream от этих фильтров — они не гоняются в реплее). Nuance (deep-review): interior-EKB оценки идентичны; boundary-EKB (radius <1км от banded-соседа) / common-street corridor могут подтянуть не-ЕКБ сделки — унаследовано от mig-177 (#2482), corridor advisory (display-only, не клампит). ## Проверка - deep-code-review: 🟡 **APPROVE**, без 🔴/🟠 блокеров. Follow-ups учтены (inverted-band guard добавлен). - Полный tradein backend suite: 2390 passed (+ pre-existing `test_search_cache_hit` deselect, как в CI). ruff clean. - **Post-deploy verify нужен**: `SELECT count(*) FROM deal_city_price_bands` (должен быть >0 — не-ЕКБ сделки уже в проде после A2). ## Follow-ups (не в этом PR) - Periodic refresh band-таблицы (deals растут ночью) — пока ручной re-run 178. - City-scoping `_fetch_dkp_corridor` (street-name collision между городами) → C2. Зависит от A1+A2 (уже в проде: 96 974 сделки, +47 183 не-ЕКБ, геокод 99.9%).
lekss361 added 1 commit 2026-07-12 16:07:35 +00:00
fix(tradein/estimator): per-city ценовой band ДКП-сделок для всей обл. 66
All checks were successful
CI Trade-In / frontend-checks (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 / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 50s
e2502a0134
Глобальный DEAL_MIN_PPM2=50k резал 46.6% не-ЕКБ сделок (легит дешёвые города: Североуральск p50=21.7k, Новоуральск 36k, Асбест 41k). Band стал per-city.

- migration 178: deal_city_price_bands (city PK, ppm2_min/max, n_deals), populate из deals (source=rosreestr, city<>Екатеринбург, count>=30), min=GREATEST(p1,8000) max=LEAST(p99,800000), idempotent upsert; HAVING-guard p99>=8000 (airtight против inverted band).
- estimator.py оба enforcement-пойнта city-aware с fallback на глобаль:
  - _fetch_dkp_corridor SQL: LEFT JOIN deal_city_price_bands + COALESCE(band, :global).
  - _is_plausible_deal(city, bands) + _fetch_deals: _load_city_price_bands (TTL-cache), SELECT city.
- ЕКБ исключён из таблицы → fallback на 50k/800k → EKB-сделки byte-identical (deals спеллит EKB только 'Екатеринбург', 49791).

EKB deal-level инвариант airtight; interior-EKB оценки идентичны. Регресс-гейт байт-зелёный. Follow-ups: periodic refresh band-таблицы; city-scoping _fetch_dkp_corridor (→ C2).
lekss361 merged commit b0b2c94ece into main 2026-07-12 16:09:05 +00:00
lekss361 deleted branch fix/tradein-oblast-c1-price-band 2026-07-12 16:09:06 +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#2485
No description provided.