From 780dd5640cd171028c4e8d445433679aa2dfb7c1 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Sat, 13 Jun 2026 13:09:22 +0500 Subject: [PATCH] fix(analytics): _district_market_saturation latest-snapshot filter (#1221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit domrf_kn_objects/sales_agg хранят ~3 snapshot'а на obj_id (weekly UPSERT, retention=none). JOIN без latest-фильтра считал каждый ЖК N раз: sold_median тянулся старыми perc → sat_factor занижен, гейт n>=5 проходил уже при 2 ЖК (n=6), sat_n завышен ~×N в scope. Сданные ЖК просачивались через старые снапшоты со статусом 'Строящиеся'. Patch: AND a.snapshot_date = (SELECT MAX(snapshot_date) FROM ...) для обеих таблиц (зеркало #1210/#1212). COUNT(*) → COUNT(DISTINCT a.obj_id) defensive. 29 recommend_mix + 8 saturation тестов зелёные. Closes #1221 --- backend/app/services/analytics_queries.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/app/services/analytics_queries.py b/backend/app/services/analytics_queries.py index 2fcb98d8..2e0c35c9 100644 --- a/backend/app/services/analytics_queries.py +++ b/backend/app/services/analytics_queries.py @@ -1414,12 +1414,21 @@ def _district_market_saturation(db: Session, *, district_name: str) -> tuple[flo Возвращает (median_pct, n_objects). None если <5 ЖК с perc. """ + # #1221: domrf_kn_objects/domrf_kn_sales_agg хранят ~3 snapshot'а на obj_id + # (UNIQUE(obj_id,[type,]snapshot_date), weekly UPSERT, ретенции нет). Без + # latest-фильтра один ЖК входил в медиану N раз → sold_median тянется старыми + # perc (sold% растёт во времени → sat_factor занижен), гейт n>=5 проходил уже + # при 2 ЖК, sat_n завышен ~×N. Фильтруем оба JOIN-партнёра MAX(snapshot_date) + # (зеркало #1210/#1212, ср. строки 1200/1208 и latest_obj-CTE стр. ~2780 + # «иначе comparables дублируются»). COUNT(DISTINCT a.obj_id) — defensive: + # после latest-snapshot пары (obj_id, type='apartments') уникальны, но явный + # DISTINCT исключает регрессию, если кто-то ослабит UNIQUE-constraint. row = ( db.execute( text( """ SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY a.perc) AS sold_median, - COUNT(*) AS n + COUNT(DISTINCT a.obj_id) AS n FROM domrf_kn_sales_agg a JOIN domrf_kn_objects o ON o.obj_id = a.obj_id @@ -1428,6 +1437,12 @@ def _district_market_saturation(db: Session, *, district_name: str) -> tuple[flo AND a.perc IS NOT NULL AND o.district_name = :dn AND o.site_status = 'Строящиеся' + AND a.snapshot_date = ( + SELECT MAX(snapshot_date) FROM domrf_kn_sales_agg + ) + AND o.snapshot_date = ( + SELECT MAX(snapshot_date) FROM domrf_kn_objects + ) """ ), {"dn": district_name},