From 369b5a4706240632db50d2460e27119f36a48f3e Mon Sep 17 00:00:00 2001 From: lekss361 Date: Mon, 11 May 2026 22:04:22 +0300 Subject: [PATCH] feat(db): v_bucket_success_score view (success-driven mix #25) --- data/sql/86_v_bucket_success_score.sql | 102 +++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 data/sql/86_v_bucket_success_score.sql diff --git a/data/sql/86_v_bucket_success_score.sql b/data/sql/86_v_bucket_success_score.sql new file mode 100644 index 00000000..2d9aaded --- /dev/null +++ b/data/sql/86_v_bucket_success_score.sql @@ -0,0 +1,102 @@ +-- 86_v_bucket_success_score.sql +-- Context: Success-driven bucket ranking view for recommend_mix (issue #25). +-- Per-(district × class × room_bucket) aggregates from domrf_kn_flats + domrf_kn_objects +-- over the last 24 months, with Z-scores normalised per (district, class) group. +-- Used in recommend_mix to bias recommendations toward buckets with high velocity + price. +-- +-- Dependencies: +-- domrf_kn_flats — total_area, rooms, price_per_m2, obj_id, region_cd, snapshot_date +-- domrf_kn_objects — obj_id, district_name, obj_class, region_cd, snapshot_date +-- domrf_kn_sale_graph — obj_id, report_month, contracted, snapshot_date +-- +-- Deploy order: standalone, no prior migration needed. +-- Idempotent: CREATE OR REPLACE VIEW. + +BEGIN; + +CREATE OR REPLACE VIEW v_bucket_success_score AS +WITH +-- Latest snapshot per object (both tables are time-series) +latest_obj AS ( + SELECT DISTINCT ON (obj_id) + obj_id, district_name, obj_class, region_cd + FROM domrf_kn_objects + WHERE district_name IS NOT NULL + ORDER BY obj_id, snapshot_date DESC +), + +bucket_aggs AS ( + SELECT + o.district_name, + COALESCE(o.obj_class, 'Comfort') AS obj_class, + CASE + WHEN f.rooms = 0 OR f.total_area < 30 THEN 'Студии 15-30' + WHEN f.total_area < 45 THEN '1-к 30-45' + WHEN f.total_area < 60 THEN '2-к 45-60' + WHEN f.total_area < 80 THEN '3-к 60-80' + ELSE '80+ м²' + END AS bucket, + AVG(NULLIF(f.price_per_m2, 0)) AS avg_price_per_m2, + AVG(NULLIF(f.total_area, 0)) AS avg_area_m2, + COUNT(*) AS n_deals, + -- Velocity proxy: avg monthly contracted units per object, over last 24 months + AVG( + (SELECT AVG(sg.contracted) + FROM domrf_kn_sale_graph sg + WHERE sg.obj_id = o.obj_id + AND sg.report_month > NOW() - INTERVAL '24 months') + ) AS avg_velocity_per_month + FROM domrf_kn_flats f + JOIN latest_obj o ON o.obj_id = f.obj_id + WHERE f.total_area BETWEEN 15 AND 200 + AND f.price_per_m2 BETWEEN 30000 AND 500000 + AND o.region_cd = 66 + AND f.snapshot_date > CURRENT_DATE - INTERVAL '24 months' + GROUP BY 1, 2, 3 + HAVING COUNT(*) >= 30 -- статистически значимая выборка +), + +z_scores AS ( + SELECT + district_name, obj_class, bucket, + n_deals, avg_price_per_m2, avg_area_m2, avg_velocity_per_month, + -- Z-score нормированный по группе (district × class) + (avg_velocity_per_month + - AVG(avg_velocity_per_month) OVER (PARTITION BY district_name, obj_class)) + / NULLIF(STDDEV(avg_velocity_per_month) OVER (PARTITION BY district_name, obj_class), 0) + AS velocity_z, + (avg_price_per_m2 + - AVG(avg_price_per_m2) OVER (PARTITION BY district_name, obj_class)) + / NULLIF(STDDEV(avg_price_per_m2) OVER (PARTITION BY district_name, obj_class), 0) + AS price_z, + (avg_area_m2 + - AVG(avg_area_m2) OVER (PARTITION BY district_name, obj_class)) + / NULLIF(STDDEV(avg_area_m2) OVER (PARTITION BY district_name, obj_class), 0) + AS area_z + FROM bucket_aggs +) + +SELECT + district_name, + obj_class, + bucket, + n_deals, + avg_price_per_m2, + avg_area_m2, + avg_velocity_per_month, + COALESCE(velocity_z, 0) * 0.5 + + COALESCE(price_z, 0) * 0.3 + - COALESCE(area_z, 0) * 0.2 AS success_score, + COALESCE(velocity_z, 0) AS velocity_z, + COALESCE(price_z, 0) AS price_z, + COALESCE(area_z, 0) AS area_z +FROM z_scores +ORDER BY district_name, obj_class, success_score DESC; + +COMMENT ON VIEW v_bucket_success_score IS + 'Success-driven bucket ranking per (district, class). ' + 'Z-scores: velocity (+) приоритет, price (+) приоритет, area (-) приоритет. ' + 'Min 30 сделок в группе. ' + 'Используется recommend_mix для смещения рекомендации (issue #25).'; + +COMMIT;