From c799dbf0abbf42bd04a5b9332f28631902574ff6 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 13 Aug 2026 16:55:41 +0500 Subject: [PATCH] =?UTF-8?q?fix(ptica):=20=D0=BD=D0=B5=D1=82=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=BE=D0=BA=20=D0=B7=D0=B0=20=D0=BE=D0=BA=D0=BD?= =?UTF-8?q?=D0=BE=20=E2=80=94=20=D1=86=D0=B5=D0=BD=D0=B0=20=D0=BD=D0=B5=20?= =?UTF-8?q?0=20=E2=82=BD/=D0=BC=C2=B2,=20=D0=B0=20=C2=AB=D0=BD=D0=B5=D1=82?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deals_per_bucket оборачивал деление в COALESCE(...,0), поэтому пустая комнатность получала среднюю цену 0 вместо NULL. Схема УЖЕ обещала честный ответ (TopLayoutRow.avg_price_per_m2_rub: float | None, комментарий «NULL если objective не покрывает obj»), и Python-потребитель уже умеет None — пропускает такую строку во взвешенном роллапе. COALESCE делал эту ветку недостижимой: обещание в контракте без реализации. Замер 13.08, окно 6 месяцев. Сработает ноль или нет — зависит от того, сколько замапленных проектов попало в радиус, поэтому по слоям: 616 проектов, 2083 пары (проект × комнатность), пустых 635; хотя бы одна пустая комнатность у 323 проектов, все пустые — у 80. При объединении по два пустых 255 из 1267, по всему городу — ноль. avg_area_m2 намеренно не трогаю: там контракт float (не Optional), нужна правка схемы + типов фронта + решение по area_bin — отдельно, #2867. Refs #2464 --- .../app/services/site_finder/best_layouts.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/app/services/site_finder/best_layouts.py b/backend/app/services/site_finder/best_layouts.py index 9c6b73e9..aaee692b 100644 --- a/backend/app/services/site_finder/best_layouts.py +++ b/backend/app/services/site_finder/best_layouts.py @@ -192,15 +192,30 @@ _INLINE_VELOCITY_SQL = text(""" SELECT a.room_bucket, SUM(a.deals_window) AS deals_window, + -- Здесь COALESCE(...,0) ОСТАЁТСЯ намеренно: TopLayoutRow.avg_area_m2 + -- объявлен как float (не Optional), и NULL ронял бы контракт API. + -- Пустые комнатности получают площадь 0 м², и это тоже неправда — но + -- честный NULL требует правки схемы + перегенерации типов фронта + -- и решения, что писать в area_bin. Отдельным заходом: #2867. COALESCE( SUM(a.area_weighted_sum) / NULLIF(SUM(a.deals_window), 0), 0 )::numeric(10, 2) AS avg_area_m2, - COALESCE( + -- #2464-B: БЕЗ COALESCE(...,0). Сделок за окно нет → делитель NULL → + -- средней цены нет, и это NULL, а не «0 ₽/м²». Схема так и объявлена + -- (TopLayoutRow.avg_price_per_m2_rub: float | None), и Python ниже уже + -- умеет None (пропускает строку во взвешенном роллапе) — но COALESCE + -- делал эту ветку недостижимой. + -- Замер 13.08 по проду, окно 6 месяцев. Сработает ноль или нет — зависит + -- от того, сколько замапленных проектов попало в радиус, поэтому цифры + -- по слоям: у 616 проектов 2083 пары (проект × комнатность), пустых 635; + -- 323 проекта имеют хотя бы одну пустую комнатность, 80 — пустые ВСЕ. + -- При объединении по два пустых остаётся 255 из 1267, по всему городу — + -- ноль. То есть чем беднее окрестность участка, тем чаще выдумывался 0. + ( SUM(a.price_weighted_sum) - / NULLIF(SUM(a.deals_window), 0), - 0 + / NULLIF(SUM(a.deals_window), 0) )::numeric(12, 2) * 1000.0 AS avg_price_per_m2_rub, array_agg(DISTINCT a.project_name) AS matched_project_names, MIN(a.window_start) AS window_start, -- 2.45.3