fix(forecasting): wrap long lines, guard window_months=0, correct base_pace fallback comment (#1593 review)
Some checks failed
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / changes (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled

- Add window_months > 0 guard to vel_by_room dict comprehension (mirrors _monthly_rate)
- Correct outer comment in recommendation.py: honest-zero for known-zero buckets,
  fallback only when velocity_by_room=None or bucket absent from _FORECAST_TO_METRIC_BUCKETS
- Add comment in as_dict() noting velocity_by_room is intentionally not serialized
  (internal pipeline attr consumed directly by recommendation.py)
This commit is contained in:
bot-backend 2026-06-17 21:07:55 +03:00
parent cc6ef80d07
commit ec0d80c1e9
2 changed files with 7 additions and 3 deletions

View file

@ -637,8 +637,9 @@ def _demand_only_overlay(
# §9.2 #1593: per-bucket velocity из velocity_by_room (objective_lots # §9.2 #1593: per-bucket velocity из velocity_by_room (objective_lots
# per комнатность). _FORECAST_TO_METRIC_BUCKETS даёт список metric-ключей # per комнатность). _FORECAST_TO_METRIC_BUCKETS даёт список metric-ключей
# для данного forecast_bucket ("80+ м²" = "4"+"5+"). Суммируем ед./мес по # для данного forecast_bucket ("80+ м²" = "4"+"5+"). Суммируем ед./мес по
# бакетам. Если velocity_by_room отсутствует ИЛИ все нужные бакеты нулевые # бакетам. Честный 0 при известных бакетах — НЕ fallback. Fallback на
# — fallback на агрегатный base_pace (лучше, чем 0-сигнал). # агрегатный base_pace только когда velocity_by_room=None (пустая
# выборка) или bucket неизвестен в _FORECAST_TO_METRIC_BUCKETS.
metric_keys = _FORECAST_TO_METRIC_BUCKETS.get(forecast_bucket, []) metric_keys = _FORECAST_TO_METRIC_BUCKETS.get(forecast_bucket, [])
if vel_by_room is not None and metric_keys: 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) 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), "demand_concentration": _round_or_none(self.demand_concentration, 3),
"price_sensitivity": _round_or_none(self.price_sensitivity, 4), "price_sensitivity": _round_or_none(self.price_sensitivity, 4),
"price_sensitivity_source": self.price_sensitivity_source, "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 — ед./мес по каждой комнатности. Ключи зеркалят # #1593: per-bucket velocity — ед./мес по каждой комнатности. Ключи зеркалят
# _room_bucket() ("студия","1","2","3","4","5+"). При has_sample=False нет # _room_bucket() ("студия","1","2","3","4","5+"). При has_sample=False нет
# смысла делить 0 лотов → None (graceful, зеркало unit_velocity поведения). # смысла делить 0 лотов → None (graceful, зеркало unit_velocity поведения).
# window_months > 0 защищает от ZeroDivisionError (зеркало _monthly_rate()).
vel_by_room: dict[str, float] | None = ( vel_by_room: dict[str, float] | None = (
{bkt: float(cnt) / float(window_months) for bkt, cnt in sold_by_room.items()} {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 else None
) )