fix(sf-10): velocity LEFT JOIN + velocity_data_available flag #284

Merged
lekss361 merged 1 commit from fix/sf-10-velocity-left-join-v2 into main 2026-05-17 13:00:19 +00:00
Owner

Summary

SF FixList #10 (Wave 2) — velocity.py Step 2 использовал де-факто INNER JOIN с objective_complex_mapping → ЖК без маппинга полностью выпадали из ответа вместо честного velocity=0. После SF#4 (Малевич mapping) мы знаем что ещё ~91.5% ЖК ЕКБ unmapped — silent dropout.

Audit (postgres MCP, EKB region_cd=66)

mapped unmapped total % unmapped
129 1387 1516 91.5%

Изменения

Файл Что
backend/app/services/site_finder/velocity.py LEFT JOIN: CTE all_competitors через unnest(CAST(:obj_ids AS int[])) + LEFT JOIN с mapped + LEFT JOIN с crm. Поле has_mapping per row. Три случая: no mapping anywhere → velocity=0 + velocity_data_available=False; mapped но нет в окне → то же; нормальный путь → flag=True
backend/app/schemas/parcel.py (через VelocityResult dataclass + as_dict()) velocity_data_available: bool = True (additive backward compat)
frontend/src/types/site-finder.ts Velocity.velocity_data_available?: boolean
frontend/src/components/site-finder/VelocityBlock.tsx Badge bg-slate-100 text-slate-500 text-xs px-2 py-0.5 rounded "нет данных velocity" когда flag=false + скрытие gauge
Vault fixes/Fix_SF_Velocity_LeftJoin_May17.md

Test plan

  • Участок где ни один ЖК в радиусе не маппирован → "нет данных velocity" badge + счётчик конкурентов без gauge
  • 66:41:0702048:27 (есть маппнутые) → velocity показывается как раньше, velocity_data_available=true
  • ruff/prettier — passed на commit
  • sum=100 invariant для recommendation не сломан (velocity=0 не должен влиять)

Related: epic #271 item #10 · Wave 2 · также частично решает silent-dropout от SF#4 audit (Малевич + 13 HIGH + 25 MEDIUM unmapped)

## Summary SF FixList #10 (Wave 2) — `velocity.py` Step 2 использовал де-факто INNER JOIN с `objective_complex_mapping` → ЖК без маппинга **полностью выпадали** из ответа вместо честного `velocity=0`. После SF#4 (Малевич mapping) мы знаем что ещё ~91.5% ЖК ЕКБ unmapped — silent dropout. ## Audit (postgres MCP, EKB region_cd=66) | mapped | unmapped | total | % unmapped | |---|---|---|---| | **129** | **1387** | 1516 | **91.5%** | ## Изменения | Файл | Что | |---|---| | `backend/app/services/site_finder/velocity.py` | LEFT JOIN: CTE `all_competitors` через `unnest(CAST(:obj_ids AS int[]))` + LEFT JOIN с mapped + LEFT JOIN с crm. Поле `has_mapping` per row. Три случая: no mapping anywhere → `velocity=0 + velocity_data_available=False`; mapped но нет в окне → то же; нормальный путь → flag=True | | `backend/app/schemas/parcel.py` (через VelocityResult dataclass + `as_dict()`) | `velocity_data_available: bool = True` (additive backward compat) | | `frontend/src/types/site-finder.ts` | `Velocity.velocity_data_available?: boolean` | | `frontend/src/components/site-finder/VelocityBlock.tsx` | Badge `bg-slate-100 text-slate-500 text-xs px-2 py-0.5 rounded` "нет данных velocity" когда flag=false + скрытие gauge | | Vault | `fixes/Fix_SF_Velocity_LeftJoin_May17.md` | ## Test plan - [ ] Участок где ни один ЖК в радиусе не маппирован → "нет данных velocity" badge + счётчик конкурентов без gauge - [ ] `66:41:0702048:27` (есть маппнутые) → velocity показывается как раньше, `velocity_data_available=true` - [ ] ruff/prettier — passed на commit - [ ] sum=100 invariant для recommendation не сломан (velocity=0 не должен влиять) Related: epic #271 item #10 · Wave 2 · также частично решает silent-dropout от SF#4 audit (Малевич + 13 HIGH + 25 MEDIUM unmapped)
lekss361 added 1 commit 2026-05-17 12:53:21 +00:00
Replace INNER JOIN on objective_complex_mapping with LEFT JOIN approach:
competitors without mapping now return velocity=0 with
velocity_data_available=False instead of being silently dropped.

UI: VelocityBlock shows 'нет данных velocity' badge when flag is False
and hides the gauge (meaningless zero). TS type updated (additive optional
field, backward compat).

Audit (EKB, region_cd=66): mapped=129, unmapped=1387, total=1516.

Closes #271 item #10
lekss361 merged commit e1300b1955 into main 2026-05-17 13:00:19 +00:00
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE.

Что проверено:

  • SQL: LEFT JOIN chain корректен, предикаты report_month >= ... и deals_total_count > 0 в ON (не WHERE) — unmapped строки сохраняются. GROUP BY ac.obj_id, m.obj_id правильный, has_mapping derived из grouped column.
  • CAST(:obj_ids AS int[]) + CAST(:window_interval AS interval) — соответствует backend.md (psycopg v3, не ::type).
  • Caller parcels.py:2095 — additive field в as_dict() backward compat.
  • best_layouts.py использует свой _INLINE_VELOCITY_SQL, не compute_velocity → recommendation pipeline (rb_deals, sum=100) не затронут.
  • Frontend: dataAvailable = velocity.velocity_data_available !== false — корректно обрабатывает undefined для старых ответов.
  • Vault fixes/Fix_SF_Velocity_LeftJoin_May17.md — есть.

MINOR (follow-up, не блокирующее):

  • backend/tests/test_velocity.py мокает sales_rows без ключа has_mapping — следующий pytest CI упадёт с KeyError: 'has_mapping'. Нужен follow-up PR: добавить "has_mapping": True/False в _sales_row(...) fixture.
  • В mapped_conf ветке fallback denominator = ... else ekb_median * 2.0 теперь dead branch (n_with_sales==0 уже отбрасывается раньше) — nit, не влияет на корректность.
  • PR body упоминает schemas/parcel.py, но фактически schema не менялся — изменения через VelocityResult.as_dict(). Косметика описания.

Smoke план:

  1. /analyze для cad где все ЖК unmapped (например далеко от центра ЕКБ) → response: velocity.velocity_data_available=false, badge "нет данных velocity" в UI, gauge скрыт.
  2. 66:41:0702048:27 (есть маппнутые) → velocity_data_available=true, normal gauge + period.
  3. Verify /analyze.recommendation rooms_mix sum=100 invariant не сломан.
Merged via deep-code-reviewer — verdict APPROVE. **Что проверено:** - SQL: LEFT JOIN chain корректен, предикаты `report_month >= ...` и `deals_total_count > 0` в ON (не WHERE) — unmapped строки сохраняются. `GROUP BY ac.obj_id, m.obj_id` правильный, `has_mapping` derived из grouped column. - `CAST(:obj_ids AS int[])` + `CAST(:window_interval AS interval)` — соответствует backend.md (psycopg v3, не `::type`). - Caller `parcels.py:2095` — additive field в `as_dict()` backward compat. - `best_layouts.py` использует свой `_INLINE_VELOCITY_SQL`, не `compute_velocity` → recommendation pipeline (`rb_deals`, `sum=100`) не затронут. - Frontend: `dataAvailable = velocity.velocity_data_available !== false` — корректно обрабатывает undefined для старых ответов. - Vault `fixes/Fix_SF_Velocity_LeftJoin_May17.md` — есть. **MINOR (follow-up, не блокирующее):** - `backend/tests/test_velocity.py` мокает `sales_rows` без ключа `has_mapping` — следующий pytest CI упадёт с `KeyError: 'has_mapping'`. Нужен follow-up PR: добавить `"has_mapping": True/False` в `_sales_row(...)` fixture. - В `mapped_conf` ветке fallback `denominator = ... else ekb_median * 2.0` теперь dead branch (n_with_sales==0 уже отбрасывается раньше) — nit, не влияет на корректность. - PR body упоминает `schemas/parcel.py`, но фактически schema не менялся — изменения через `VelocityResult.as_dict()`. Косметика описания. **Smoke план:** 1. `/analyze` для cad где все ЖК unmapped (например далеко от центра ЕКБ) → response: `velocity.velocity_data_available=false`, badge "нет данных velocity" в UI, gauge скрыт. 2. `66:41:0702048:27` (есть маппнутые) → `velocity_data_available=true`, normal gauge + period. 3. Verify `/analyze.recommendation` rooms_mix sum=100 invariant не сломан.
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#284
No description provided.