[site-finder] EPIC 5: Полная модель релевантности конкурентов + расширенные рыночные метрики #949

Closed
opened 2026-06-02 16:32:04 +00:00 by Light1YT · 3 comments
Owner

Часть #944 (Site Finder v2 / ТЗ ГенГеныч).

Статус: EXTEND · Приоритет: P0 · ТЗ §9.1, 9.2

Scope

Расширить competitors.py весом релевантности §9.1 (гео+класс+цена+срок+квартирография+инфра+бренд+стадия-на-дату-выхода). Добавить метрики §9.2: absorption rate, months-of-supply, liquidity, overstock, demand concentration, price sensitivity. Измерительный слой под прогноз/индексы.

Зависимости:
Уже есть (EXTEND): competitors.py, velocity.py, analytics_queries.py

**Часть #944 (Site Finder v2 / ТЗ ГенГеныч).** Статус: **EXTEND** · Приоритет: **P0** · ТЗ §9.1, 9.2 ## Scope Расширить competitors.py весом релевантности §9.1 (гео+класс+цена+срок+квартирография+инфра+бренд+стадия-на-дату-выхода). Добавить метрики §9.2: absorption rate, months-of-supply, liquidity, overstock, demand concentration, price sensitivity. Измерительный слой под прогноз/индексы. **Зависимости:** — **Уже есть (EXTEND):** competitors.py, velocity.py, analytics_queries.py
Light1YT added the
analytics
GG-форсайт
site-finder
labels 2026-06-02 16:32:04 +00:00
Owner

📊 Ground-truth 2026-06-02 (прод-БД + код)

Маппинг Объектив↔domrf = 34.8%, а НЕ ~2.5% как помечено риском в vault. На проде: objective_lots.complex_id заполнен у 303 677 / 872 508 лотов (350 distinct ЖК, 416 проектов).
→ Первая задача эпика: сверить, что competitors.py джойнит именно через complex_id (а не более слабый ключ, дающий 2.5%) и замерить, сколько из 35% реально доходят до конкурентного набора. Возможно, «киллер-риск точности» снимается почти бесплатно.

Метрики §9.2 (absorption, months-of-supply, price/area velocity, overstock, «что зависает») требуют point-in-time ряда склада. objective_lots_history сейчас = 2 снапшота (17 и 19 мая) — для velocity/exposure недостаточно. Два источника закрытия:

  • продажи — глубоко из objective_lots.contract_date (107 мес) → absorption/sell-through считаются;
  • живой склад/экспозиция — из листингового слоя (listings_snapshots подневно + houses_price_dynamics 7 мес) после cross-load из tradein, см. #950.

База уже есть: analytics_queries.py (_district_market_saturation, _velocity_baseline, _active_competitors_count, _competitors_two_dim, _elasticity_coef). Нет именно named-метрик absorption/MoS/overstock и формального веса релевантности §9.1.

## 📊 Ground-truth 2026-06-02 (прод-БД + код) **Маппинг Объектив↔domrf = 34.8%, а НЕ ~2.5%** как помечено риском в vault. На проде: `objective_lots.complex_id` заполнен у **303 677 / 872 508** лотов (350 distinct ЖК, 416 проектов). → Первая задача эпика: **сверить, что `competitors.py` джойнит именно через `complex_id`** (а не более слабый ключ, дающий 2.5%) и замерить, сколько из 35% реально доходят до конкурентного набора. Возможно, «киллер-риск точности» снимается почти бесплатно. **Метрики §9.2 (absorption, months-of-supply, price/area velocity, overstock, «что зависает»)** требуют point-in-time ряда склада. `objective_lots_history` сейчас = **2 снапшота (17 и 19 мая)** — для velocity/exposure недостаточно. Два источника закрытия: - продажи — глубоко из `objective_lots.contract_date` (107 мес) → absorption/sell-through считаются; - живой склад/экспозиция — из листингового слоя (`listings_snapshots` подневно + `houses_price_dynamics` 7 мес) после cross-load из tradein, см. #950. База уже есть: `analytics_queries.py` (`_district_market_saturation`, `_velocity_baseline`, `_active_competitors_count`, `_competitors_two_dim`, `_elasticity_coef`). Нет именно named-метрик absorption/MoS/overstock и формального веса релевантности §9.1.
lekss361 added this to the Site Finder v2 — Foresight (GG-форсайт) milestone 2026-06-02 19:02:51 +00:00
lekss361 reopened this issue 2026-06-03 17:59:32 +00:00
Owner

🔴 Reopen — CRITICAL: market-metrics переоценивает продажи ~14×, выдаёт невозможные absorption/velocity с confidence='high'. Валидация #997 + #1000 против прод-БД.

#997 backend/app/services/site_finder/market_metrics.py _SALES_WINDOW_SQL (CTE hist+first_sold). «Продажи за окно» считаются как лот, помеченный (is_sold IS TRUE OR contract_date IS NOT NULL) в ЛЮБОМ снапшоте окна + DISTINCT ON (objective_lot_id). Снапшоты переносят состояние «продано» вперёд → считается весь накопленный already-sold inventory, а НЕ новые продажи; transition unsold→sold не детектится.
Замер (Автовокзал, окно 6 мес): units_sold_window = 33 245 против реальных продаж по contract_date = 2 355 (~14×). Следствие: unit_velocity≈5 540/мес, absorption_rate≈0.235/мес (235% стока в месяц — физически невозможно), months_of_supply≈4.2 — мусор, и всё помечено confidence='high' (n_lots=64 694). Эти метрики потребляются downstream #950/#952 → заражают весь прогноз.
Fix: считать по contract_date в окне ЛИБО детектить transition между снапшотами.
🟠 Доп. (HIGH): objective_lots_history по району — всего ~3 снапшота / ~18 дней, но 6-мес метрика помечена confidence='high'. Гейтинг confidence игнорирует реальную глубину истории.

#1000 competitors.py _AVG_PRICE_SQL (latent, MEDIUM). Новый COUNT(*) FILTER (… sold …) GROUP BY obj_id (flats_sold) без фильтра snapshot_date → fan-out по 6 снапшотам domrf_kn_flats (verified obj 63312: 1316 flats × 4 снап = 5264+ строк). Сейчас 0 объектов с >1 снап И sold → значения верны, но первый же weekly-снапшот ломает: инфляция sold_pct/available либо срабатывает guard raw_sold<=flats_total и сигнал молча отбрасывается. (AVG был duplication-invariant; копия паттерна в COUNT — нет.) Fix: latest snapshot per obj перед COUNT. Доп. LOW: ветка LIKE '%прод%' мёртвая (статусы англ. free/sold/booked), комментарий называет её основной — неверно.

Хелперы релевантности (#1000) корректны: веса∑=1.0, sub-scores клампятся [0,1], нейтраль 0.5 на missing.

🔴 **Reopen — CRITICAL: market-metrics переоценивает продажи ~14×, выдаёт невозможные absorption/velocity с confidence='high'.** Валидация #997 + #1000 против прод-БД. **#997 `backend/app/services/site_finder/market_metrics.py` `_SALES_WINDOW_SQL` (CTE `hist`+`first_sold`).** «Продажи за окно» считаются как лот, помеченный `(is_sold IS TRUE OR contract_date IS NOT NULL)` в ЛЮБОМ снапшоте окна + `DISTINCT ON (objective_lot_id)`. Снапшоты переносят состояние «продано» вперёд → считается весь накопленный already-sold inventory, а НЕ новые продажи; transition unsold→sold не детектится. Замер (Автовокзал, окно 6 мес): `units_sold_window` = **33 245** против реальных продаж по `contract_date` = **2 355** (~14×). Следствие: `unit_velocity`≈5 540/мес, `absorption_rate`≈0.235/мес (**235% стока в месяц — физически невозможно**), `months_of_supply`≈4.2 — мусор, и всё помечено `confidence='high'` (n_lots=64 694). Эти метрики потребляются downstream #950/#952 → заражают весь прогноз. **Fix:** считать по `contract_date` в окне ЛИБО детектить transition между снапшотами. 🟠 Доп. (HIGH): `objective_lots_history` по району — всего ~3 снапшота / ~18 дней, но 6-мес метрика помечена `confidence='high'`. Гейтинг confidence игнорирует реальную глубину истории. **#1000 `competitors.py` `_AVG_PRICE_SQL` (latent, MEDIUM).** Новый `COUNT(*) FILTER (… sold …) GROUP BY obj_id` (`flats_sold`) **без фильтра snapshot_date** → fan-out по 6 снапшотам `domrf_kn_flats` (verified obj 63312: 1316 flats × 4 снап = 5264+ строк). Сейчас 0 объектов с >1 снап И sold → значения верны, но первый же weekly-снапшот ломает: инфляция `sold_pct`/`available` либо срабатывает guard `raw_sold<=flats_total` и сигнал молча отбрасывается. (`AVG` был duplication-invariant; копия паттерна в COUNT — нет.) **Fix:** latest snapshot per obj перед COUNT. Доп. LOW: ветка `LIKE '%прод%'` мёртвая (статусы англ. `free/sold/booked`), комментарий называет её основной — неверно. _Хелперы релевантности (#1000) корректны: веса∑=1.0, sub-scores клампятся [0,1], нейтраль 0.5 на missing._
Collaborator

FIXED + verified on prod (#1036).

Root cause (deeper than 'counts already-sold'): _SALES_WINDOW_SQL counted window-sales from objective_lots_history, but history is only ~17 days deep -> every currently-sold lot had a sold-snapshot in window -> window-sales = whole cumulative sold stock.

Fix: count window-sales by objective_lots.contract_date in window (100% reliable on sold lots).

PROD compute_market_metrics(Автовокзал) AFTER deploy:
absorption_rate: 2.35 -> 0.0163 (1.6%/mo, realistic)
unit_velocity: ~5540 -> 384.67 (= 2308 contract_date sales / 6mo)
months_of_supply: finite 61.2
n_sold (cumulative): 41,161 (unchanged, correct)
Closing on prod ground-truth.

FIXED + verified on prod (#1036). Root cause (deeper than 'counts already-sold'): _SALES_WINDOW_SQL counted window-sales from objective_lots_history, but history is only ~17 days deep -> every currently-sold lot had a sold-snapshot in window -> window-sales = whole cumulative sold stock. Fix: count window-sales by objective_lots.contract_date in window (100% reliable on sold lots). PROD compute_market_metrics(Автовокзал) AFTER deploy: absorption_rate: 2.35 -> 0.0163 (1.6%/mo, realistic) unit_velocity: ~5540 -> 384.67 (= 2308 contract_date sales / 6mo) months_of_supply: finite 61.2 n_sold (cumulative): 41,161 (unchanged, correct) Closing on prod ground-truth.
Sign in to join this conversation.
No project
No assignees
3 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#949
No description provided.