fix(forecasting): thread room_bucket velocity into format ranking (#1593) #1696

Merged
lekss361 merged 2 commits from fix/base-pace-room-bucket-1593 into main 2026-06-17 18:08:26 +00:00
2 changed files with 7 additions and 3 deletions
Showing only changes of commit ec0d80c1e9 - Show all commits

View file

@ -637,8 +637,9 @@ def _demand_only_overlay(
# §9.2 #1593: per-bucket velocity из velocity_by_room (objective_lots
# per комнатность). _FORECAST_TO_METRIC_BUCKETS даёт список metric-ключей
# для данного forecast_bucket ("80+ м²" = "4"+"5+"). Суммируем ед./мес по
# бакетам. Если velocity_by_room отсутствует ИЛИ все нужные бакеты нулевые
# — fallback на агрегатный base_pace (лучше, чем 0-сигнал).
# бакетам. Честный 0 при известных бакетах — НЕ fallback. Fallback на
# агрегатный base_pace только когда velocity_by_room=None (пустая
# выборка) или bucket неизвестен в _FORECAST_TO_METRIC_BUCKETS.
metric_keys = _FORECAST_TO_METRIC_BUCKETS.get(forecast_bucket, [])
if vel_by_room is not None and metric_keys:
bucket_velocity: float = sum(vel_by_room.get(k, 0.0) for k in metric_keys)

View file

@ -140,6 +140,8 @@ class MarketMetrics:
"demand_concentration": _round_or_none(self.demand_concentration, 3),
"price_sensitivity": _round_or_none(self.price_sensitivity, 4),
"price_sensitivity_source": self.price_sensitivity_source,
# velocity_by_room намеренно не сериализуется: это внутренний
# pipeline-атрибут (потребляется recommendation.py напрямую).
}
@ -416,9 +418,10 @@ def compute_market_metrics(
# #1593: per-bucket velocity — ед./мес по каждой комнатности. Ключи зеркалят
# _room_bucket() ("студия","1","2","3","4","5+"). При has_sample=False нет
# смысла делить 0 лотов → None (graceful, зеркало unit_velocity поведения).
# window_months > 0 защищает от ZeroDivisionError (зеркало _monthly_rate()).
vel_by_room: dict[str, float] | None = (
{bkt: float(cnt) / float(window_months) for bkt, cnt in sold_by_room.items()}
if has_sample and sold_by_room
if has_sample and sold_by_room and window_months > 0
else None
)