gendesign/backend/app/schemas/recommend.py
bot-backend 72e9b24f2c
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m24s
Deploy / build-worker (push) Successful in 2m33s
Deploy / deploy (push) Successful in 1m4s
feat(recommend): horizon-aware recommend_mix opt-in overlay (#982, 953-A) (#1014)
2026-06-03 07:40:33 +00:00

102 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""IO contracts for the rule-based квартирография recommender.
POST /api/v1/analytics/recommend/mix
"""
from typing import Any, Literal
from pydantic import BaseModel, Field
ClassLiteral = Literal["Comfort", "Comfort+", "Business", "Elite"]
class RecommendMixInput(BaseModel):
district_name: str = Field(..., min_length=2, max_length=80)
area_total_m2: float | None = Field(default=None, ge=100, le=500_000)
target_class: ClassLiteral | None = None
months_window: int = Field(default=12, ge=3, le=24)
# Velocity / pricing scenario knobs (live-tuned client-side; backend just
# ships base coefficients so frontend can recompute without round-trips).
# 0.01..3.0 = -99%..+200% к рынку. min=0.01 (а не 0) чтобы избежать
# деления на 0 / pf^elasticity = ∞ при «бесплатной» цене.
price_factor: float = Field(default=1.0, ge=0.01, le=3.0)
target_months: int | None = Field(default=None, ge=3, le=120)
# #982 (953-A) forecast-overlay opt-in. Оба None по умолчанию → живой микс
# БАЙТ-в-БАЙТ как раньше (overlay не добавляется). horizon_months задан →
# advisory overlay под scope["forecast"]; cad_num задан → demand_supply-режим
# (с supply/конкурентами участка), иначе demand_only (только темп спроса).
horizon_months: int | None = Field(default=None, ge=3, le=24)
cad_num: str | None = Field(default=None, max_length=40)
class RecommendBucket(BaseModel):
bucket: str
share_pct: float
deal_count: int
area_avg_m2: float
area_median_m2: float
price_median_per_m2: float
price_p25_per_m2: float
price_p75_per_m2: float
units_planned: int | None = None
revenue_planned_rub: float | None = None
# Velocity baseline (units/month for THIS project allocated to this bucket
# at price_factor=1.0). Frontend scales by price_factor^elasticity for live
# what-if recompute.
velocity_per_month: float | None = None
months_to_sellout: float | None = None
# Success-driven mix flag (issue #25): bucket has top success_score in district
is_top_success: bool = False
class RecommendComparable(BaseModel):
obj_id: int
comm_name: str | None = None
dev_name: str | None = None
obj_class: str | None = None
flat_count: int | None = None
sold_perc: float | None = None
cad_quarter: str | None = None
lat: float | None = None
lon: float | None = None
buildings_n: int | None = None
class RecommendForecastSegment(BaseModel):
"""Одна ранжированная ячейка §9.7 forecast-overlay (#982). ADVISORY.
`deficit_index`: в demand_supply-режиме ∈ [1,+1] (>0 строить / <0 затоварка,
supply-based); в demand_only-режиме ∈ (0,1] = ПРОКСИ относительной силы спроса
(NOT supply-based — геометрии участка нет). `balance_units` (demandsupply)
None в demand_only (предложение неизмеримо без cad_num).
"""
bucket: str
obj_class: str | None = None
deficit_index: float
balance_units: float | None = None
confidence: Literal["high", "medium", "low"]
class RecommendForecastOverlay(BaseModel):
"""§9.7 СОВЕТУЮЩИЙ forecast-overlay поверх живого микса (#982, 953-A).
Кладётся ТОЛЬКО под scope["forecast"] при заданном horizon_months; живой микс
(4 поля RecommendMixOutput) НЕ затрагивается. `advisory` ВСЕГДА True (наследует
advisory-статус §9.x — не основание для инвест-решения). `mode`: demand_supply
(cad_num задан — supply/конкуренты учтены) или demand_only (cad_num=None — только
темп спроса; см. warnings).
"""
horizon_months: int
mode: Literal["demand_supply", "demand_only"]
advisory: bool
ranked_segments: list[RecommendForecastSegment]
warnings: list[str]
class RecommendMixOutput(BaseModel):
scope: dict[str, Any]
buckets: list[RecommendBucket]
summary: dict[str, Any]
comparables: list[RecommendComparable]