fix(sf-21b): _INLINE_VELOCITY_SQL weighted AVG (P0 follow-up) #291

Merged
lekss361 merged 1 commit from fix/sf-21b-inline-velocity-weighted into main 2026-05-17 14:02:44 +00:00
Owner

🔴 P0 follow-up к #21 (PR #290)

backend/app/services/site_finder/best_layouts.py:121-122 (_INLINE_VELOCITY_SQL) использовал тот же broken unweighted AVG pattern напрямую на objective_corpus_room_month — тот же class bug что и в mv_layout_velocity.

Без этого fix — даже после merge PR #290 (mv fix) inline-path (best_layouts.py compute_velocity) всё равно возвращает broken numbers (например studio = 11 м² вместо 28 м²).

Changes

backend/app/services/site_finder/best_layouts.py, _INLINE_VELOCITY_SQL:

  • Было:
    AVG(crm.deals_total_avg_area_m2) AS avg_area_m2,
    AVG(crm.deals_total_avg_price_thousand_rub_per_m2) * 1000.0 AS avg_price_per_m2_rub
    
  • Стало:
    COALESCE(SUM(crm.deals_total_avg_area_m2 * crm.deals_total_count)
             / NULLIF(SUM(crm.deals_total_count), 0), 0)::numeric(10, 2) AS avg_area_m2,
    COALESCE(SUM(crm.deals_total_avg_price_thousand_rub_per_m2 * crm.deals_total_count)
             / NULLIF(SUM(crm.deals_total_count), 0), 0)::numeric(12, 2) * 1000.0 AS avg_price_per_m2_rub
    

COALESCE(..., 0) чтобы при absence-data не сломать downstream Python (раньше при NULL Python мог crash на .toFixed() или Decimal arithmetic).

Verify after deploy

https://gendsgn.ru/site-finder?cad=66:41:0204016:10 → «Рассчитать» в BestLayouts → studio ~28 м² (не 12), 3-к ~81 м² (не 22).

Related: P0 #21b · parent fix PR #290 (SF#21 mv) · epic #271

## 🔴 P0 follow-up к #21 (PR #290) `backend/app/services/site_finder/best_layouts.py:121-122` (`_INLINE_VELOCITY_SQL`) использовал тот же broken unweighted AVG pattern напрямую на `objective_corpus_room_month` — тот же class bug что и в `mv_layout_velocity`. **Без этого fix** — даже после merge PR #290 (mv fix) inline-path (best_layouts.py compute_velocity) **всё равно возвращает broken numbers** (например studio = 11 м² вместо 28 м²). ## Changes `backend/app/services/site_finder/best_layouts.py`, `_INLINE_VELOCITY_SQL`: - Было: ```sql AVG(crm.deals_total_avg_area_m2) AS avg_area_m2, AVG(crm.deals_total_avg_price_thousand_rub_per_m2) * 1000.0 AS avg_price_per_m2_rub ``` - Стало: ```sql COALESCE(SUM(crm.deals_total_avg_area_m2 * crm.deals_total_count) / NULLIF(SUM(crm.deals_total_count), 0), 0)::numeric(10, 2) AS avg_area_m2, COALESCE(SUM(crm.deals_total_avg_price_thousand_rub_per_m2 * crm.deals_total_count) / NULLIF(SUM(crm.deals_total_count), 0), 0)::numeric(12, 2) * 1000.0 AS avg_price_per_m2_rub ``` `COALESCE(..., 0)` чтобы при absence-data не сломать downstream Python (раньше при NULL Python мог crash на `.toFixed()` или Decimal arithmetic). ## Verify after deploy `https://gendsgn.ru/site-finder?cad=66:41:0204016:10` → «Рассчитать» в BestLayouts → studio ~28 м² (не 12), 3-к ~81 м² (не 22). Related: P0 #21b · parent fix PR #290 (SF#21 mv) · epic #271
lekss361 added 1 commit 2026-05-17 13:58:41 +00:00
Replace unweighted AVG(deals_total_avg_area_m2) and AVG(deals_total_avg_price_thousand_rub_per_m2)
with SUM(x * count) / NULLIF(SUM(count), 0) pattern in _INLINE_VELOCITY_SQL.
Months with zero deals no longer dilute the weighted mean 2-4x.
P0 follow-up to PR #290 (mv fix, issue #21).
lekss361 merged commit 9f7224de33 into main 2026-05-17 14:02:44 +00:00
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE.

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

  • Weighted AVG формула SUM(value * count) / NULLIF(SUM(count), 0) корректна, идентична паттерну PR #290.
  • ::numeric(N, M) применяется к column expression, не к bind param — psycopg v3 safe (не CAST trap).
  • Bind params (:cad_quarter, :window_interval, :competitor_obj_ids) сохранены — no injection vector.
  • Algorithm preserved (LEFT JOIN, time_window filter, room_bucket normalization студия→studio из PR #275).
  • _INLINE_VELOCITY_SQL используется только в compute_velocity, schema колонок неизменна — downstream callers safe.

Sequence: PR #290 (MV fix, data/sql/100_*.sql) + этот PR #291 (inline path) активируются вместе → consistent UI.

Post-deploy verify: https://gendsgn.ru/site-finder?cad=66:41:0204016:10 → BestLayouts → studio ~28 м² (не 12), 3-к ~81 м² (не 22).

Note: existing assertions в test_best_layouts.py на avg_area_m2 / avg_price_per_m2_rub могут expect старые (broken) numbers — flag для апдейта если CI красный.

Follow-up: code/patterns/Pattern_Weighted_Aggregates.md (TBD per vault note Bug_MvLayoutVelocity_UnweightedAvg_May17).

Merged via deep-code-reviewer — verdict APPROVE. Что проверено: - Weighted AVG формула `SUM(value * count) / NULLIF(SUM(count), 0)` корректна, идентична паттерну PR #290. - `::numeric(N, M)` применяется к column expression, не к bind param — psycopg v3 safe (не CAST trap). - Bind params (`:cad_quarter`, `:window_interval`, `:competitor_obj_ids`) сохранены — no injection vector. - Algorithm preserved (LEFT JOIN, time_window filter, room_bucket normalization студия→studio из PR #275). - `_INLINE_VELOCITY_SQL` используется только в `compute_velocity`, schema колонок неизменна — downstream callers safe. Sequence: PR #290 (MV fix, `data/sql/100_*.sql`) + этот PR #291 (inline path) активируются вместе → consistent UI. Post-deploy verify: `https://gendsgn.ru/site-finder?cad=66:41:0204016:10` → BestLayouts → studio ~28 м² (не 12), 3-к ~81 м² (не 22). Note: existing assertions в `test_best_layouts.py` на `avg_area_m2` / `avg_price_per_m2_rub` могут expect старые (broken) numbers — flag для апдейта если CI красный. Follow-up: `code/patterns/Pattern_Weighted_Aggregates.md` (TBD per vault note `Bug_MvLayoutVelocity_UnweightedAvg_May17`).
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#291
No description provided.