Demand: Активные конкуренты — анализ продаж ЖК в радиусе 1км за квартал (cad → топ конкурентов) #112

Closed
opened 2026-05-12 15:36:45 +00:00 by lekss361 · 1 comment
lekss361 commented 2026-05-12 15:36:45 +00:00 (Migrated from github.com)

Эшелон: 🟠 Demand — «Что СЕЙЧАС продаётся рядом»
Source feedback: Макс, 12.05.2026 14:01 (по результатам встречи)

Контекст

Текущий #36 D4 показывает «pipeline планируемых ЖК на 24mo» (future supply). Это другое.

Макс хочет active competitive analysis — что СЕЙЧАС продаётся в радиусе 1 км за последний квартал, кто почём, какие комнатности.

«Радиус поиска конкурентов: настраиваемый, 0-1.5 км (предпочтительно 1 км). Временной срез: продажи за последний квартал. Возможность вручную исключать или добавлять объекты-конкуренты. Динамика цен и продаж по выбранным конкурентам. Детализация: по комнатности (кол-во, цена).»

Это MVP версия competitor analysis от Макса. #36 D4 остаётся как pipeline-метрика; этот issue — активные конкуренты с реальными продажами.

Что сделать

1. Backend — endpoint POST /api/v1/site-finder/{cad}/competitors

Request:

{
  "radius_m": 1000,           // 0..1500, default 1000
  "time_window": "last_quarter", // "last_month" | "last_quarter" | "last_year" | custom range
  "include_obj_ids": [],       // explicitly include (override radius)
  "exclude_obj_ids": [],       // explicitly exclude
  "obj_class_filter": null     // optional: filter "economy" | "comfort" | "business"
}

Response:

{
  "competitors": [
    {
      "obj_id": "domrf_12345",
      "comm_name": "ЖК Брусника. Северный",
      "dev_name": "ООО СЗ Брусника",
      "distance_m": 412,
      "lat": 56.8389, "lng": 60.5975,
      "obj_class": "comfort",
      "stage": "selling",          // "selling" | "completed" | "construction"
      "flats_total": 850,
      "flats_sold": 612,
      "sold_pct": 72.0,
      "velocity_per_month": {
        "last_quarter": 24.3,
        "last_year_avg": 22.1
      },
      "price_per_m2": {
        "current_avg": 142500,
        "last_quarter_dynamics": [138200, 140100, 142500],
        "yoy_change_pct": 8.2
      },
      "by_rooms": {
        "studio":  {"sold_last_q": 18, "avg_price_m2": 156800, "avg_size_m2": 28},
        "1_room":  {"sold_last_q": 25, "avg_price_m2": 145300, "avg_size_m2": 41},
        "2_room":  {"sold_last_q": 19, "avg_price_m2": 138900, "avg_size_m2": 58},
        "3_room":  {"sold_last_q": 8,  "avg_price_m2": 132100, "avg_size_m2": 82},
        "4plus_room": {"sold_last_q": 2, "avg_price_m2": 128400, "avg_size_m2": 110}
      }
    }
  ],
  "summary": {
    "total_competitors": 7,
    "weighted_avg_price_m2": 141800,
    "weighted_avg_velocity": 18.5,
    "best_seller": {"obj_id": "...", "by_metric": "velocity_per_m2"}
  }
}

2. Источники данных

  • domrf_kn_objects — список ЖК с lat/lng + flats_total + obj_class
  • domrf_kn_sale_graph — velocity + price dynamics per month
  • rosreestr_deals — ground truth по ДДУ за квартал (опционально для cross-check)
  • cad_parcels_geom — для distance compute от точки участка

3. Frontend — раздел в /site-finder/{cad}

Карточка «Активные конкуренты»:

  • Слайдер радиуса (0-1500м, default 1000м, с шагом 100м)
  • Toggle временного среза (месяц / квартал / год)
  • Карта Leaflet: точка участка + competitors с цветовой градацией velocity
  • Чекбоксы add/exclude per ЖК
  • Reactive recompute при изменении (TanStack Query mutation)

Таблица сводная (sortable):
| ЖК | Расстояние | Sold% | Темп/мес | Ц/м² | Δ Q-Q |

Drilldown per ЖК (modal или expanded row):

  • Динамика price/m² (line chart per последний год)
  • Распределение sold по комнатностям (stacked bar или table)
  • Кнопка «Исключить» / «Включить только этого»

4. ETL (если данных недостаточно)

  • rosreestr_deals.contract_signed_at >= NOW() - INTERVAL '1 quarter' — индекс
  • Materialized view mv_active_competitors_last_q (refresh daily)

Acceptance

  • Endpoint работает для test cad 66:41:0204016:10 — возвращает ≥3 конкурентов в 1км
  • Per-room breakdown суммируется в реальный flats_sold_last_q
  • UI слайдер радиуса меняет результат с задержкой <500мс
  • Add/exclude UI работает без перезагрузки
  • Динамика цен показывается за последние 4 квартала
  • Cross-check с rosreestr_deals: расхождение <10%

Effort

~4-5 дней (backend 2д + ETL/materialized view 1д + frontend 2д)

Owner

backend-engineer (endpoint + ETL) + frontend-engineer (Leaflet карта + таблица + drilldown)

Dependencies

  • domrf_kn_sale_graph уже есть в БД
  • rosreestr_deals 6.83M ДДУ уже есть
  • cad_parcels_geom (foundation #51 / #94)

Replaces / Supersedes

  • Дополняет #36 D4 (D4 = future pipeline / planned, этот issue = current active sales)
  • Поднимает up scope того что было в закрытом #19
  • Feedback source: Макс (12.05.2026 14:01)
  • Phase 2 follow-up: Layout analysis (отдельный issue) — какие планировки лучше продаются → ТЗ на проектирование
**Эшелон:** 🟠 Demand — «Что СЕЙЧАС продаётся рядом» **Source feedback:** Макс, 12.05.2026 14:01 (по результатам встречи) ## Контекст Текущий **#36 D4** показывает «pipeline планируемых ЖК на 24mo» (future supply). **Это другое.** Макс хочет **active competitive analysis** — что СЕЙЧАС продаётся в радиусе 1 км за последний квартал, кто почём, какие комнатности. > «Радиус поиска конкурентов: настраиваемый, 0-1.5 км (предпочтительно 1 км). Временной срез: продажи за последний квартал. Возможность вручную исключать или добавлять объекты-конкуренты. Динамика цен и продаж по выбранным конкурентам. Детализация: по комнатности (кол-во, цена).» Это **MVP версия** competitor analysis от Макса. **#36 D4** остаётся как pipeline-метрика; этот issue — **активные конкуренты с реальными продажами**. ## Что сделать ### 1. Backend — endpoint `POST /api/v1/site-finder/{cad}/competitors` **Request:** ```json { "radius_m": 1000, // 0..1500, default 1000 "time_window": "last_quarter", // "last_month" | "last_quarter" | "last_year" | custom range "include_obj_ids": [], // explicitly include (override radius) "exclude_obj_ids": [], // explicitly exclude "obj_class_filter": null // optional: filter "economy" | "comfort" | "business" } ``` **Response:** ```json { "competitors": [ { "obj_id": "domrf_12345", "comm_name": "ЖК Брусника. Северный", "dev_name": "ООО СЗ Брусника", "distance_m": 412, "lat": 56.8389, "lng": 60.5975, "obj_class": "comfort", "stage": "selling", // "selling" | "completed" | "construction" "flats_total": 850, "flats_sold": 612, "sold_pct": 72.0, "velocity_per_month": { "last_quarter": 24.3, "last_year_avg": 22.1 }, "price_per_m2": { "current_avg": 142500, "last_quarter_dynamics": [138200, 140100, 142500], "yoy_change_pct": 8.2 }, "by_rooms": { "studio": {"sold_last_q": 18, "avg_price_m2": 156800, "avg_size_m2": 28}, "1_room": {"sold_last_q": 25, "avg_price_m2": 145300, "avg_size_m2": 41}, "2_room": {"sold_last_q": 19, "avg_price_m2": 138900, "avg_size_m2": 58}, "3_room": {"sold_last_q": 8, "avg_price_m2": 132100, "avg_size_m2": 82}, "4plus_room": {"sold_last_q": 2, "avg_price_m2": 128400, "avg_size_m2": 110} } } ], "summary": { "total_competitors": 7, "weighted_avg_price_m2": 141800, "weighted_avg_velocity": 18.5, "best_seller": {"obj_id": "...", "by_metric": "velocity_per_m2"} } } ``` ### 2. Источники данных - `domrf_kn_objects` — список ЖК с lat/lng + flats_total + obj_class - `domrf_kn_sale_graph` — velocity + price dynamics per month - `rosreestr_deals` — ground truth по ДДУ за квартал (опционально для cross-check) - `cad_parcels_geom` — для distance compute от точки участка ### 3. Frontend — раздел в `/site-finder/{cad}` **Карточка «Активные конкуренты»:** - Слайдер радиуса (0-1500м, default 1000м, с шагом 100м) - Toggle временного среза (месяц / квартал / год) - Карта Leaflet: точка участка + competitors с цветовой градацией velocity - Чекбоксы add/exclude per ЖК - Reactive recompute при изменении (TanStack Query mutation) **Таблица сводная (sortable):** | ЖК | Расстояние | Sold% | Темп/мес | Ц/м² | Δ Q-Q | **Drilldown per ЖК (modal или expanded row):** - Динамика price/m² (line chart per последний год) - Распределение sold по комнатностям (stacked bar или table) - Кнопка «Исключить» / «Включить только этого» ### 4. ETL (если данных недостаточно) - `rosreestr_deals.contract_signed_at >= NOW() - INTERVAL '1 quarter'` — индекс - Materialized view `mv_active_competitors_last_q` (refresh daily) ## Acceptance - [ ] Endpoint работает для test cad `66:41:0204016:10` — возвращает ≥3 конкурентов в 1км - [ ] Per-room breakdown суммируется в реальный `flats_sold_last_q` - [ ] UI слайдер радиуса меняет результат с задержкой <500мс - [ ] Add/exclude UI работает без перезагрузки - [ ] Динамика цен показывается за последние 4 квартала - [ ] Cross-check с rosreestr_deals: расхождение <10% ## Effort ~4-5 дней (backend 2д + ETL/materialized view 1д + frontend 2д) ## Owner `backend-engineer` (endpoint + ETL) + `frontend-engineer` (Leaflet карта + таблица + drilldown) ## Dependencies - `domrf_kn_sale_graph` уже есть в БД ✅ - `rosreestr_deals` 6.83M ДДУ уже есть ✅ - `cad_parcels_geom` (foundation #51 / #94) ## Replaces / Supersedes - **Дополняет #36 D4** (D4 = future pipeline / planned, этот issue = current active sales) - **Поднимает up scope** того что было в закрытом #19 ## Links - **Feedback source:** Макс (12.05.2026 14:01) - **Phase 2 follow-up:** Layout analysis (отдельный issue) — какие планировки лучше продаются → ТЗ на проектирование
Owner

Closed — Phase 1 shipped в PR #191

Endpoint live: POST /api/v1/parcels/{cad_num}/competitors

Backend: backend/app/services/site_finder/competitors.py + backend/app/api/v1/parcels.py:2088. 9 mock tests pass, prod-verified.

Phase 1 acceptance

  • Endpoint работает (POST /competitors с radius 0-1500m, time_window last_quarter, include/exclude_obj_ids, obj_class_filter)
  • Per-room breakdown с avg_price_m2 / avg_size_m2 / sold_last_q

Phase 2 follow-up

  • Layout analysis (Phase 2 от Макса) — реализован в #113 (closed)
  • Frontend Leaflet карта + drilldown UI — отдельный issue если потребуется (текущая UI integration через CompetitorTable в MarketTab)

Known carryover bug

competitors.py:143 фильтр WHERE f.status='sold' возвращает 0 (поле 99.8% NULL в domrf_kn_flats на проде). avg_price_per_m2 в response always None. PR #196 (#113 PR C) обходит этот bug — использует objective_corpus_room_month через mv_layout_velocity. Для #112 нужен отдельный fix — рекомендую новый issue «fix(competitors): use objective_corpus snapshot diff вместо kn_flats.status».

## ✅ Closed — Phase 1 shipped в PR #191 **Endpoint live:** `POST /api/v1/parcels/{cad_num}/competitors` Backend: `backend/app/services/site_finder/competitors.py` + `backend/app/api/v1/parcels.py:2088`. 9 mock tests pass, prod-verified. ### Phase 1 acceptance - ✅ Endpoint работает (`POST /competitors` с radius 0-1500m, time_window last_quarter, include/exclude_obj_ids, obj_class_filter) - ✅ Per-room breakdown с avg_price_m2 / avg_size_m2 / sold_last_q ### Phase 2 follow-up - ✅ Layout analysis (Phase 2 от Макса) — реализован в #113 (closed) - Frontend Leaflet карта + drilldown UI — отдельный issue если потребуется (текущая UI integration через `CompetitorTable` в MarketTab) ### Known carryover bug `competitors.py:143` фильтр `WHERE f.status='sold'` возвращает 0 (поле 99.8% NULL в `domrf_kn_flats` на проде). `avg_price_per_m2` в response always None. PR #196 (#113 PR C) обходит этот bug — использует `objective_corpus_room_month` через `mv_layout_velocity`. Для #112 нужен отдельный fix — рекомендую новый issue «fix(competitors): use objective_corpus snapshot diff вместо kn_flats.status».
Sign in to join this conversation.
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#112
No description provided.