63 lines
2.2 KiB
Python
63 lines
2.2 KiB
Python
"""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)
|
||
|
||
|
||
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 RecommendMixOutput(BaseModel):
|
||
scope: dict[str, Any]
|
||
buckets: list[RecommendBucket]
|
||
summary: dict[str, Any]
|
||
comparables: list[RecommendComparable]
|