fix(market_metrics): disambiguate ROLLUP grand-total via GROUPING() (#1214) #1258

Merged
bot-backend merged 1 commit from fix/market-metrics-rollup-grand-total-disambiguate into main 2026-06-13 05:57:20 +00:00
Collaborator

Summary

Closes #1214 (P2 correctness — silent data corruption в форсайте). ROLLUP grand-total путали с NULL-комнатной группой → units_total затирался частичным счётом.

Bug

_SALES_WINDOW_SQL делает GROUP BY ROLLUP (rooms_int), rooms_int nullable (ETL пишет NULL для «неопределённого типа», sales_series.py:399 явно обрабатывает None).

Один проданный лот с rooms_int IS NULL даёт ДВЕ строки с rooms_int IS NULL (NULL-группа + grand-total итог), неразличимые в Python (if r["rooms_int"] is None).

MixedAggregate-план PG16 эмитит grand-total ПЕРВЫМ среди hash-строк, NULL-группа после → loop затирает units_total частичным счётом (живой тест на PG16: 2000 → 200).

Эффект: unit_velocity, absorption_rate занижены, months_of_supply завышен → base_pace в demand_supply_forecast неверный → reports/scoring врёт.

Fix

  • SQL: GROUPING(rooms_int) AS is_total (=1 для grand-total, =0 иначе).
  • Python: ветвить по is_total, NULL-комнатную группу класть в by_room['unknown'] (отдельный бакет), аккумулировать через += вместо assign (защита от будущих NULL-вариантов).
  • Тесты: моки получили is_total поле.

Test plan

  • pytest -k market_metrics71 passed, 1 skipped.
  • ruff check → clean.
  • Post-deploy: smoke на районе с NULL-rooms лотами → units_total корректно ≈ полная сумма; by_room['unknown'] > 0 если есть такие лоты.

Closes #1214

## Summary Closes #1214 (P2 correctness — silent data corruption в форсайте). ROLLUP grand-total путали с NULL-комнатной группой → `units_total` затирался частичным счётом. ## Bug `_SALES_WINDOW_SQL` делает `GROUP BY ROLLUP (rooms_int)`, `rooms_int` nullable (ETL пишет NULL для «неопределённого типа», `sales_series.py:399` явно обрабатывает None). Один проданный лот с `rooms_int IS NULL` даёт ДВЕ строки с `rooms_int IS NULL` (NULL-группа + grand-total итог), **неразличимые** в Python (`if r["rooms_int"] is None`). MixedAggregate-план PG16 эмитит grand-total **ПЕРВЫМ** среди hash-строк, NULL-группа после → loop затирает `units_total` частичным счётом (живой тест на PG16: 2000 → 200). Эффект: `unit_velocity`, `absorption_rate` занижены, `months_of_supply` завышен → `base_pace` в `demand_supply_forecast` неверный → reports/scoring врёт. ## Fix - SQL: `GROUPING(rooms_int) AS is_total` (=1 для grand-total, =0 иначе). - Python: ветвить по `is_total`, NULL-комнатную группу класть в `by_room['unknown']` (отдельный бакет), аккумулировать через `+=` вместо assign (защита от будущих NULL-вариантов). - Тесты: моки получили `is_total` поле. ## Test plan - [x] `pytest -k market_metrics` → **71 passed, 1 skipped**. - [x] `ruff check` → clean. - [ ] Post-deploy: smoke на районе с NULL-rooms лотами → `units_total` корректно ≈ полная сумма; `by_room['unknown']` > 0 если есть такие лоты. Closes #1214
bot-backend added 1 commit 2026-06-13 05:44:13 +00:00
fix(market_metrics): disambiguate ROLLUP grand-total via GROUPING() (#1214)
All checks were successful
CI / changes (push) Successful in 6s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m22s
CI / backend-tests (pull_request) Successful in 6m24s
c295979c01
_SALES_WINDOW_SQL делал GROUP BY ROLLUP (rooms_int), rooms_int nullable
(ETL пишет NULL для «неопределённого типа», sales_series.py:399 явно
обрабатывает None). Проданный лот с rooms_int IS NULL даёт ДВЕ строки
rooms_int IS NULL (NULL-группа + grand-total итог), неразличимые в
Python (оба if r["rooms_int"] is None).

MixedAggregate-план PG16 эмитит grand-total ПЕРВЫМ (среди hash-строк),
NULL-группа после → loop затирает units_total частичным счётом (живой
тест на PG16: 2000 → 200). Эффект: unit_velocity / absorption_rate
занижены, months_of_supply завышен → base_pace в demand_supply_forecast
неверный (recommendation.py:586) → reports/scoring врёт.

Patch:
- SQL: добавить GROUPING(rooms_int) AS is_total (=1 для grand-total).
- Python: ветвить по is_total, NULL-комнатную группу класть в
  by_room['unknown'] (отдельный бакет), аккумулировать через +=
  вместо assign (защита от будущих NULL-вариантов).
- Тесты: моки получили "is_total" поле (1 для grand-total, 0 иначе).

71/71 market_metrics тестов зелёные. ruff clean.

Closes #1214
bot-backend merged commit bc2d393b05 into main 2026-06-13 05:57:20 +00:00
bot-backend deleted branch fix/market-metrics-rollup-grand-total-disambiguate 2026-06-13 05:57:20 +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#1258
No description provided.