From 48e2201c7b34e6a070ed6daa9f03d03f38fafcda Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sun, 28 Jun 2026 16:04:56 +0000 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20guard=20=D0=93=D0=90=D0=A0-unde?= =?UTF-8?q?rcount=20=D0=B7=D0=BD=D0=B0=D0=BC=D0=B5=D0=BD=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B2=20v=5Fbuilding=5Fsale=5Fshare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ГАР часто недо-регистрирует помещения (лишь 68k из 919k домов имеют квартиры), из-за чего малые знаменатели раздували % — топ листа был забит «100% = 5 из 5» и 91 дом показывал >100% (active>flats, физически невозможно). Добавляю гейт: sale_share_pct считается только если flat_count_effective >= GREATEST(этажность, 8) И active_secondary <= flat_count_effective. Проверено на проде (rollback): 2769→2596 достоверных (−91 >100% −82 undercount), over100=0, топ — реальные сигналы (Крауля 89А 10/11=90.9%, Ткачей 17 15/19=78.9%). Недостоверные → «н/д». --- ...45_building_sale_share_plausible_denom.sql | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tradein-mvp/backend/data/sql/145_building_sale_share_plausible_denom.sql diff --git a/tradein-mvp/backend/data/sql/145_building_sale_share_plausible_denom.sql b/tradein-mvp/backend/data/sql/145_building_sale_share_plausible_denom.sql new file mode 100644 index 00000000..4211f789 --- /dev/null +++ b/tradein-mvp/backend/data/sql/145_building_sale_share_plausible_denom.sql @@ -0,0 +1,75 @@ +-- 145_building_sale_share_plausible_denom.sql +-- +-- CONTEXT: гейт правдоподобности знаменателя в v_building_sale_share. +-- ГАР/ФИАС часто недо-регистрирует помещения (на проде лишь 68 406 из 919 341 +-- домов региона 66 имеют хоть одно помещение). Малый/неполный flat_count раздувал +-- sale_share_pct: топ листа был забит «5 из 5 = 100%», а 91 дом показывал >100% +-- (active_secondary > flat_count — физически невозможно, явный undercount). +-- +-- Гейт: sale_share_pct вычисляется ТОЛЬКО когда +-- flat_count_effective >= GREATEST(COALESCE(total_floors,0), 8) -- >=1 кв/этаж и не мизер +-- AND active_secondary <= flat_count_effective -- не >100% +-- иначе NULL («н/д» — честно, без выдуманного процента). +-- +-- Проверено на проде (rollback-tx): 2769 → 2596 достоверных домов (−91 >100%, +-- −82 undercount), over100=0; топ — реальные сигналы концентрации продаж +-- (Крауля 89А 10/11=90.9%, Ткачей 17 15/19=78.9%, Походная 63 12/32=37.5%). +-- +-- DEPENDENCIES: 143 (view + houses.gar_flat_count), 144 (canon match populates gar_flat_count). +-- SAFETY: только CREATE OR REPLACE VIEW — идемпотентно, не деструктивно. Колонки/типы +-- не меняются (тот же набор столбцов), меняется лишь условие в CASE sale_share_pct. +-- Deploy order: после 144_gar_canon_addr_match.sql. + +BEGIN; + +CREATE OR REPLACE VIEW v_building_sale_share AS + WITH listing_agg AS ( + SELECT l.house_id_fk AS house_id, + count(*) FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text) AS active_secondary, + percentile_cont(0.5::double precision) WITHIN GROUP (ORDER BY (l.price_rub::double precision)) + FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text AND l.price_rub IS NOT NULL) AS median_price_rub, + percentile_cont(0.5::double precision) WITHIN GROUP (ORDER BY (l.price_per_m2::double precision)) + FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text AND l.price_per_m2 IS NOT NULL) AS median_price_per_m2, + avg(l.days_on_market) + FILTER (WHERE l.is_active AND l.listing_segment = 'vtorichka'::text AND l.days_on_market IS NOT NULL) AS avg_days_on_market + FROM listings l + WHERE l.house_id_fk IS NOT NULL + GROUP BY l.house_id_fk + ) + SELECT h.id AS house_id, + h.short_address, + h.full_address, + h.address, + h.lat, + h.lon, + h.year_built, + h.house_type, + h.total_floors, + h.series_name, + h.is_emergency, + COALESCE(h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0)) AS flat_count_effective, + h.gar_flat_count, + h.gar_match_method, + la.active_secondary, + la.median_price_rub, + la.median_price_per_m2, + la.avg_days_on_market, + CASE + WHEN COALESCE(h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0)) + >= GREATEST(COALESCE(h.total_floors, 0), 8) + AND la.active_secondary <= COALESCE(h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0)) + THEN round(100.0 * la.active_secondary::numeric + / COALESCE(h.gar_flat_count, NULLIF(h.total_units, 0), NULLIF(h.flat_count, 0))::numeric, 1) + ELSE NULL::numeric + END AS sale_share_pct + FROM houses h + JOIN listing_agg la ON la.house_id = h.id + WHERE h.geom IS NOT NULL AND la.active_secondary > 0; + +COMMENT ON VIEW v_building_sale_share IS + 'Per-building rollup вторички для «доли квартир дома в продаже» (мигр. 143; ' + 'знаменатель — ГАР canon-match мигр. 144). sale_share_pct только при правдоподобном ' + 'знаменателе: flat_count_effective >= GREATEST(total_floors,8) AND active<=flat_count ' + '(мигр. 145 — защита от ГАР-undercount); иначе NULL. Фильтр: geom NOT NULL AND active>0.'; + +COMMIT; -- 2.45.3