chore(mv): drop redundant migration — weighted-AVG fix already shipped in 100 (#295)
This commit is contained in:
parent
1f30e3d234
commit
3c30942cf7
2 changed files with 1 additions and 93 deletions
|
|
@ -76,7 +76,7 @@ SUM(avg_value * cnt) / NULLIF(SUM(cnt), 0)
|
||||||
и возвращает `NULL` вместо фейкового `0`.
|
и возвращает `NULL` вместо фейкового `0`.
|
||||||
- Без весов: `AVG()` равноправно учитывает «пустые» месяцы → занижение.
|
- Без весов: `AVG()` равноправно учитывает «пустые» месяцы → занижение.
|
||||||
|
|
||||||
Reference: fix #295 (`120_fix_mv_layout_velocity_weighted_avg.sql`),
|
Reference: fix #295 (`100_fix_mv_layout_velocity_weighted_avg.sql`),
|
||||||
тест `backend/tests/sql/test_mv_layout_velocity_weighted_avg.py`.
|
тест `backend/tests/sql/test_mv_layout_velocity_weighted_avg.py`.
|
||||||
|
|
||||||
## Запреты
|
## Запреты
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
-- 120_fix_mv_layout_velocity_weighted_avg.sql
|
|
||||||
-- Fix #295 — mv_layout_velocity used naive AVG() over pre-aggregated per-month rows.
|
|
||||||
-- objective_corpus_room_month has rows even for months with 0 deals; those zero rows
|
|
||||||
-- dragged the average down 2-10x depending on how sparse the project's deal history is.
|
|
||||||
-- (e.g. a project with 1 deal in 1 month out of 24 reported months: AVG divided by 24,
|
|
||||||
-- weighted AVG divides by 1 → correct value.)
|
|
||||||
--
|
|
||||||
-- Fix: replace AVG() with count-weighted formula
|
|
||||||
-- SUM(val * deals_total_count) / NULLIF(SUM(deals_total_count), 0)
|
|
||||||
-- applied to both avg_area_m2 and avg_price_thousand_rub_per_m2.
|
|
||||||
--
|
|
||||||
-- Prod verification (2026-05-28, Vitamin-квартал на Титова, last 24 mo):
|
|
||||||
-- room | buggy_area | wtd_area | buggy_price | wtd_price
|
|
||||||
-- studio| 19.11 | 26.03 | 129.83 | 175.83
|
|
||||||
-- 1-rm | 31.72 | 37.15 | 145.68 | 174.21
|
|
||||||
-- 2-rm | 41.44 | 55.89 | 98.52 | 134.97
|
|
||||||
-- 3-rm | 27.14 | 81.43 | 45.40 | 136.20
|
|
||||||
-- Weighted values match expected (studio ≈28m², 1-rm ≈38m², 2-rm ≈57m², 3-rm ≈81m²,
|
|
||||||
-- prices 110-180k ₽/m²). Sparse 3-rm (5 deals / 15 months) had 3x undercount.
|
|
||||||
--
|
|
||||||
-- All indexes recreated identically to 94_mv_layout_velocity.sql:
|
|
||||||
-- mv_layout_velocity_pk (UNIQUE on obj_id, room_bucket) — required for REFRESH CONCURRENTLY
|
|
||||||
-- mv_layout_velocity_obj_idx (btree on obj_id)
|
|
||||||
--
|
|
||||||
-- CASCADE check (2026-05-28): no dependent views or tables found → no extra DDL needed.
|
|
||||||
--
|
|
||||||
-- Deploy: auto-applied by deploy.yml via _schema_migrations tracking.
|
|
||||||
-- After apply: REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity is safe
|
|
||||||
-- (MV is populated WITH DATA in this migration).
|
|
||||||
--
|
|
||||||
-- WARN: Re-applying this file will DROP + recreate the MV (CASCADE-safe as no dependents),
|
|
||||||
-- but will cause a brief data gap. Normally prevented by _schema_migrations tracking.
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
DROP MATERIALIZED VIEW IF EXISTS mv_layout_velocity CASCADE;
|
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW mv_layout_velocity AS
|
|
||||||
WITH last24mo AS (
|
|
||||||
SELECT
|
|
||||||
ocm.project_name,
|
|
||||||
CASE
|
|
||||||
WHEN ocm.room_bucket = 'студия' THEN 'studio'
|
|
||||||
ELSE ocm.room_bucket
|
|
||||||
END AS room_bucket,
|
|
||||||
ocm.deals_total_count,
|
|
||||||
ocm.deals_total_avg_area_m2,
|
|
||||||
ocm.deals_total_avg_price_thousand_rub_per_m2,
|
|
||||||
ocm.deals_total_vol_m2,
|
|
||||||
ocm.report_month
|
|
||||||
FROM objective_corpus_room_month ocm
|
|
||||||
WHERE ocm.report_month >= (NOW() - INTERVAL '24 months')::date
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
cm.domrf_obj_id AS obj_id,
|
|
||||||
l.room_bucket,
|
|
||||||
SUM(l.deals_total_count)::int AS total_deals_24mo,
|
|
||||||
-- Count-weighted average area: avoids zero-deal months dragging value down
|
|
||||||
(SUM(l.deals_total_avg_area_m2 * l.deals_total_count)
|
|
||||||
/ NULLIF(SUM(l.deals_total_count), 0))::numeric(10, 2) AS avg_area_m2,
|
|
||||||
-- Count-weighted average price: same reasoning
|
|
||||||
(SUM(l.deals_total_avg_price_thousand_rub_per_m2 * l.deals_total_count)
|
|
||||||
/ NULLIF(SUM(l.deals_total_count), 0))::numeric(12, 2) AS avg_price_thousand_rub_per_m2,
|
|
||||||
SUM(l.deals_total_vol_m2)::numeric(12, 2) AS total_vol_m2,
|
|
||||||
MIN(l.report_month) AS window_start,
|
|
||||||
MAX(l.report_month) AS window_end,
|
|
||||||
COUNT(DISTINCT l.report_month)::int AS months_with_data
|
|
||||||
FROM last24mo l
|
|
||||||
JOIN objective_complex_mapping cm
|
|
||||||
ON cm.objective_complex_name = l.project_name
|
|
||||||
WHERE l.room_bucket IS NOT NULL
|
|
||||||
AND cm.domrf_obj_id IS NOT NULL
|
|
||||||
AND cm.objective_group = 'Екатеринбург' -- защита от cross-region Cartesian при future multi-city
|
|
||||||
GROUP BY cm.domrf_obj_id, l.room_bucket
|
|
||||||
WITH DATA;
|
|
||||||
|
|
||||||
-- UNIQUE index required for REFRESH CONCURRENTLY (periodic refreshes via layout_velocity_refresh.py)
|
|
||||||
CREATE UNIQUE INDEX mv_layout_velocity_pk
|
|
||||||
ON mv_layout_velocity (obj_id, room_bucket);
|
|
||||||
|
|
||||||
-- Lookup index used by /best-layouts endpoint
|
|
||||||
CREATE INDEX mv_layout_velocity_obj_idx
|
|
||||||
ON mv_layout_velocity (obj_id);
|
|
||||||
|
|
||||||
COMMENT ON MATERIALIZED VIEW mv_layout_velocity IS
|
|
||||||
'Per-(obj_id, room_bucket) deals aggregation за last 24 months. '
|
|
||||||
'Source: objective_corpus_room_month × objective_complex_mapping (EKB only). '
|
|
||||||
'avg_area_m2 and avg_price_thousand_rub_per_m2 are COUNT-WEIGHTED to avoid '
|
|
||||||
'zero-deal months skewing the average (fix #295). '
|
|
||||||
'Refresh via layout_velocity_refresh.py (concurrently=True — MV is pre-populated).';
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
Loading…
Add table
Reference in a new issue