feat(site-finder): geo-radius market-price calibration in /analyze (#1881 follow-up) #1899

Merged
bot-backend merged 1 commit from feat/geo-radius-price-calibration into main 2026-06-25 07:59:56 +00:00
Collaborator

Проблема

Цена продажи в финмодели /analyze калибруется по Objective-данным новостроек. Текущий путь — name-match админ-района, но только 4 из 9 районов ЕКБ совпадают по имени (Академический, Ленинский, Кировский, Орджоникидзевский). Остальные 5 (Чкаловский, Верх-Исетский, Железнодорожный, Октябрьский + «не определён») проваливались в district_reference/class_norm — без реальной рыночной цены. objective_lots не имеет геометрии; координаты — через complex_id → complexes(latitude, longitude).

Фикс

Новый ценовой tier objective_geo_radius: ST_DWithin-медиана objective_lots.price_per_m2_rub (join complexes по complex_id) в радиусе 3 км от центроида участка. Вставлен в priority-chain между квартальной MV и district_reference:

  1. mv_quarter_price_per_m2 (сделки квартала) → objective_district_median
  2. NEW гео-радиус Objective → objective_geo_radius
  3. district name-match → district_reference
  4. class_norm

Порог: n≥10 лотов И ≥2 ЖК (один ЖК не задаёт цену).

Прод-валидация (читалка, реальные локации в 5 «гео»-районах)

Район было (district_reference) гео-радиус медиана лотов ЖК
Верх-Исетский NULL 146 387 9 700 19
Железнодорожный NULL 129 543 13 579 16
Октябрьский NULL 158 292 16 497 25
Чкаловский NULL 140 034 11 344 17

Производительность (важно — hot-path #1850)

ST_DWithin(...::geography) не покрывался плоским complexes_geom_idx. Добавлен функциональный GIST-индекс data/sql/168 на (ST_SetSRID(ST_MakePoint(longitude,latitude),4326)::geography). Прод EXPLAIN: Seq Scan 114 мс → Index Scan 28 мс. Без CONCURRENTLY (complexes ~2023 строки, deploy применяет миграции в транзакции). Миграция применяется до compose up → schema-first.

Честность

  • Guard вырожденного центроида: при degenerate/NULL-геометрии центроид падает в центр ЕКБ — гео-радиус тогда пропускается (source=no_data_centroid_fallback), чтобы не выдать центр-города медиану за калиброванную цену участка.
  • ptica-adapt: price_source → честные RU-подписи («медиана рынка в радиусе 3 км» и т.д.), не сырой токен.

Изменения

  • parcels.py — гео-радиус SQL-блок (begin_nested + try/except, degrade→no_data) + centroid-guard + wire в synthesize_parcel_financial + в /analyze-ответ.
  • parcel_financial.py — новый geo_radius_price_median kwarg (back-compat) + priority-chain.
  • data/sql/168 — GIST-индекс.
  • ptica-adapt.ts — RU-подписи price_source.
  • test_parcel_financial.py — +4 теста (4-tier chain). 21 passed.

Без schema-change (price_source = plain str) → codegen не нужен. Deep-review (SQL coord-path, SAVEPOINT, priority-chain, индекс прод-verified).

Refs #1881

## Проблема Цена продажи в финмодели /analyze калибруется по Objective-данным новостроек. Текущий путь — name-match админ-района, но **только 4 из 9 районов ЕКБ совпадают по имени** (Академический, Ленинский, Кировский, Орджоникидзевский). Остальные 5 (Чкаловский, Верх-Исетский, Железнодорожный, Октябрьский + «не определён») проваливались в `district_reference`/`class_norm` — без реальной рыночной цены. `objective_lots` не имеет геометрии; координаты — через `complex_id → complexes(latitude, longitude)`. ## Фикс Новый ценовой tier `objective_geo_radius`: `ST_DWithin`-медиана `objective_lots.price_per_m2_rub` (join `complexes` по `complex_id`) в радиусе **3 км** от центроида участка. Вставлен в priority-chain **между квартальной MV и district_reference**: 1. `mv_quarter_price_per_m2` (сделки квартала) → `objective_district_median` 2. **NEW** гео-радиус Objective → `objective_geo_radius` 3. district name-match → `district_reference` 4. `class_norm` Порог: **n≥10 лотов И ≥2 ЖК** (один ЖК не задаёт цену). ### Прод-валидация (читалка, реальные локации в 5 «гео»-районах) | Район | было (district_reference) | гео-радиус медиана | лотов | ЖК | |---|---|---|---|---| | Верх-Исетский | NULL | 146 387 | 9 700 | 19 | | Железнодорожный | NULL | 129 543 | 13 579 | 16 | | Октябрьский | NULL | 158 292 | 16 497 | 25 | | Чкаловский | NULL | 140 034 | 11 344 | 17 | ## Производительность (важно — hot-path #1850) `ST_DWithin(...::geography)` не покрывался плоским `complexes_geom_idx`. Добавлен функциональный GIST-индекс `data/sql/168` на `(ST_SetSRID(ST_MakePoint(longitude,latitude),4326)::geography)`. **Прод EXPLAIN: Seq Scan 114 мс → Index Scan 28 мс.** Без `CONCURRENTLY` (complexes ~2023 строки, deploy применяет миграции в транзакции). Миграция применяется до `compose up` → schema-first. ## Честность - **Guard вырожденного центроида:** при degenerate/NULL-геометрии центроид падает в центр ЕКБ — гео-радиус тогда **пропускается** (`source=no_data_centroid_fallback`), чтобы не выдать центр-города медиану за калиброванную цену участка. - `ptica-adapt`: `price_source` → честные RU-подписи (`«медиана рынка в радиусе 3 км»` и т.д.), не сырой токен. ## Изменения - `parcels.py` — гео-радиус SQL-блок (`begin_nested` + try/except, degrade→no_data) + centroid-guard + wire в `synthesize_parcel_financial` + в /analyze-ответ. - `parcel_financial.py` — новый `geo_radius_price_median` kwarg (back-compat) + priority-chain. - `data/sql/168` — GIST-индекс. - `ptica-adapt.ts` — RU-подписи price_source. - `test_parcel_financial.py` — +4 теста (4-tier chain). 21 passed. **Без schema-change** (`price_source` = plain `str`) → codegen не нужен. Deep-review ✅ (SQL coord-path, SAVEPOINT, priority-chain, индекс прод-verified). Refs #1881
bot-backend added 1 commit 2026-06-25 07:47:24 +00:00
feat(site-finder): geo-radius market-price calibration in /analyze (#1881 follow-up)
All checks were successful
CI / frontend-tests (pull_request) Successful in 1m0s
CI / backend-tests (pull_request) Successful in 11m36s
CI / openapi-codegen-check (pull_request) Successful in 1m54s
CI / changes (pull_request) Successful in 7s
282843f241
Цена продажи в финмодели калибровалась по name-match админ-района Objective —
но только 4 из 9 районов ЕКБ совпадают по имени, 5 проваливались в class_norm.
Добавлен ценовой tier "objective_geo_radius": ST_DWithin-медиана цен новостроек
Objective (objective_lots ⋈ complexes по lat/lon) в радиусе 3 км от центроида
участка, между квартальной MV и district_reference. Порог n≥10 лотов и ≥2 ЖК
(один ЖК не задаёт цену). Прод: 5 «гео»-районов теперь дают реальную медиану
(129–158k ₽/м²) вместо NULL.

- data/sql/168: функциональный GIST-индекс complexes((ST_MakePoint(lon,lat))::geography)
  — без него ST_DWithin делает Seq Scan (+~90мс на /analyze, у которого уже
  таймаут-давление #1850). Прод EXPLAIN: Seq Scan 114мс → Index Scan 28мс.
- guard: при вырожденном центроиде (fallback на центр ЕКБ) гео-радиус
  пропускается (source=no_data_centroid_fallback) — не выдаём центр-города
  медиану за калиброванную цену участка.
- price_source="objective_geo_radius" (plain str, без schema-change).
- ptica-adapt: price_source → честные RU-подписи (не сырой токен).
- +4 теста (4-tier priority chain). deep-review .

Refs #1881
bot-backend merged commit dc659da655 into main 2026-06-25 07:59:56 +00:00
bot-backend deleted branch feat/geo-radius-price-calibration 2026-06-25 07:59:57 +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#1899
No description provided.