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},