fix(tradein/estimator): city-scope ДКП-коридора + oblast city-tokens/bbox #2488

Merged
lekss361 merged 2 commits from fix/tradein-oblast-c2-estimator-scope into main 2026-07-12 17:21:56 +00:00
Owner

Что

Чинит доказанную кросс-город контаминацию ДКП-коридора: _fetch_dkp_corridor матчил сделки по street-name БЕЗ city → тянул одноимённые улицы ВСЕХ городов области. Пример (verified): «Нижний Тагил, Ленина» → коридор 171 сделка, но у Н.Тагила только 34 — остальное Лесной/Краснотурьинск/Серов/ЕКБ/Первоуральск. Медианы всех городов схлопывались в 56-60k.

Изменения

  • _resolve_target_city(address) — word-boundary поверх geocoder.SVERDLOVSK_OBLAST_CITIES (~30 городов, F), с district-guard («мкр Заречный» = микрорайон, не город-ЗАТО Заречный — единственная ЕКБ-коллизия).
  • _fetch_dkp_corridor: +param cityAND LOWER(d.city) = CAST(:target_city AS text). Проводка: estimate_quality (precedence dadata.canonical_address > geo.full_address) + trade_in.py GET rehydrate. city=None → прежнее поведение (fallback).
  • _CITY_TOKENS + _DEDUP_ADDR_NOISE_WORDS → derive из SVERDLOVSK_OBLAST_CITIES (было 6 EKB-агломерация городов) → same-house dedup работает для non-EKB.
  • bbox fast-path → geocoder.is_within_oblast66_bbox.
  • docstring исправлен: коридор НЕ чисто advisory — при n>=clamp_min_n(10) клампит headline + radius-floor + Tier-C anchor gate.

EKB инвариант

Regression-гейт байт-зелёный (коридор/токены upstream от frozen _price_from_inputs fixture replay — не гоняются в реплее). Deep-review проверил: EKB-адреса резолвятся корректно (F-коллизии Серова/Ирбитская/Асбестовский → екб, word-boundary rigor есть); SQL injection-safe (static-фрагмент + bind); perf ok (LOWER(city) — residual поверх уже street-фильтрованного rowset).

Проверка

  • deep-code-review: ⚠️ APPROVE, без 🔴/🟠 (2×🟡 de-risk применены: заречный-guard, docstring). ё-form: deals.city хранит без ё (Березовский/Артемовский) → не проблема.
  • Полный tradein backend suite: 2390 passed (+ pre-existing test_search_cache_hit). ruff clean.

Follow-ups (task-tracked)

  • DB-derived city-set (Лесной 1103 uncovered остаётся контаминирован — graceful fallback).
  • asking_to_sold_ratio.py city-scope (тот же класс бага).
  • backtest city-parity (backtest_estimator.py:1519 не передаёт city) → делать в D.
## Что Чинит **доказанную кросс-город контаминацию** ДКП-коридора: `_fetch_dkp_corridor` матчил сделки по street-name БЕЗ city → тянул одноимённые улицы ВСЕХ городов области. Пример (verified): «Нижний Тагил, Ленина» → коридор 171 сделка, но у Н.Тагила только 34 — остальное Лесной/Краснотурьинск/Серов/ЕКБ/Первоуральск. Медианы всех городов схлопывались в 56-60k. ## Изменения - `_resolve_target_city(address)` — word-boundary поверх `geocoder.SVERDLOVSK_OBLAST_CITIES` (~30 городов, F), с district-guard («мкр Заречный» = микрорайон, не город-ЗАТО Заречный — единственная ЕКБ-коллизия). - `_fetch_dkp_corridor`: +param `city` → `AND LOWER(d.city) = CAST(:target_city AS text)`. Проводка: estimate_quality (precedence `dadata.canonical_address > geo.full_address`) + trade_in.py GET rehydrate. `city=None` → прежнее поведение (fallback). - `_CITY_TOKENS` + `_DEDUP_ADDR_NOISE_WORDS` → derive из `SVERDLOVSK_OBLAST_CITIES` (было 6 EKB-агломерация городов) → same-house dedup работает для non-EKB. - bbox fast-path → `geocoder.is_within_oblast66_bbox`. - docstring исправлен: коридор **НЕ чисто advisory** — при `n>=clamp_min_n(10)` клампит headline + radius-floor + Tier-C anchor gate. ## EKB инвариант Regression-гейт байт-зелёный (коридор/токены upstream от frozen `_price_from_inputs` fixture replay — не гоняются в реплее). Deep-review проверил: EKB-адреса резолвятся корректно (F-коллизии `Серова`/`Ирбитская`/`Асбестовский` → екб, word-boundary rigor есть); SQL injection-safe (static-фрагмент + bind); perf ok (LOWER(city) — residual поверх уже street-фильтрованного rowset). ## Проверка - deep-code-review: ⚠️ **APPROVE**, без 🔴/🟠 (2×🟡 de-risk применены: заречный-guard, docstring). ё-form: deals.city хранит без ё (Березовский/Артемовский) → не проблема. - Полный tradein backend suite: 2390 passed (+ pre-existing `test_search_cache_hit`). ruff clean. ## Follow-ups (task-tracked) - DB-derived city-set (Лесной 1103 uncovered остаётся контаминирован — graceful fallback). - `asking_to_sold_ratio.py` city-scope (тот же класс бага). - backtest `city`-parity (backtest_estimator.py:1519 не передаёт city) → делать в D.
lekss361 added 1 commit 2026-07-12 17:11:40 +00:00
fix(tradein/estimator): city-scope ДКП-коридора + oblast city-tokens/bbox
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
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
CI Trade-In / backend-tests (pull_request) Successful in 53s
b93d26990b
Чинит доказанную кросс-город контаминацию коридора: street-name-матч без city тянул одноимённые улицы всех городов обл. («Ленина» = Н.Тагил+Лесной+Серов+ЕКБ...).

- _resolve_target_city(address) — word-boundary поверх geocoder.SVERDLOVSK_OBLAST_CITIES (F, ~30 городов), district-guard («мкр Заречный» = район, не город-ЗАТО).
- _fetch_dkp_corridor: +param city → AND LOWER(d.city)=CAST(:target_city AS text). Проводка в estimate_quality (dadata.canonical > geo) + trade_in.py GET rehydrate. city=None → прежнее поведение (fallback).
- _CITY_TOKENS + _DEDUP_ADDR_NOISE_WORDS → derive из SVERDLOVSK_OBLAST_CITIES (было 6 EKB-городов) → same-house dedup для non-EKB.
- bbox fast-path → geocoder.is_within_oblast66_bbox.
- docstring: коридор НЕ чисто advisory (клампит headline + radius-floor при n>=clamp_min_n).

EKB regression-гейт байт-зелёный (коридор upstream от frozen _price_from_inputs replay). Deep-review ⚠️APPROVE (no блокеров). Follow-ups (task #10): DB-derived city-set (Лесной 1103 uncovered), asking_to_sold_ratio city-scope, backtest city-parity.
bot-backend added 1 commit 2026-07-12 17:14:23 +00:00
Merge remote-tracking branch 'refs/remotes/forgejo/main' into fix/tradein-oblast-c2-estimator-scope
All checks were successful
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 50s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
4a579e74e0
# Conflicts:
#	tradein-mvp/backend/app/services/estimator.py
lekss361 merged commit 5d7c3e1c30 into main 2026-07-12 17:21:56 +00:00
lekss361 deleted branch fix/tradein-oblast-c2-estimator-scope 2026-07-12 17:21:57 +00:00
Sign in to join this conversation.
No reviewers
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#2488
No description provided.