Spaces:
Running
Running
File size: 11,348 Bytes
2e8a71f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | """
THAMAN API β Pydantic request/response schemas
"""
from pydantic import BaseModel, ConfigDict, Field
from typing import Optional, List
# ββ Borough mapping βββββββββββββββββββββββββββββββββββββββββββββββββββ
# 1 = Manhattan, 2 = Bronx, 3 = Brooklyn, 4 = Queens, 5 = Staten Island
BOROUGH_NAMES = {1: "Manhattan", 2: "Bronx", 3: "Brooklyn", 4: "Queens", 5: "Staten Island"}
BLDGCLASS_DESCRIPTIONS = {
"A0": "Cape Cod",
"A1": "Two story detached - small or moderate",
"A2": "One story - permanent living quarters",
"A3": "Large suburban residence",
"A4": "City residence - one family",
"A5": "City residence - attached (rowhouse)",
"A6": "Summer residence",
"A7": "Mansion type or town house",
"A8": "Bungalow colony",
"A9": "Single family residence NEC",
"B1": "Two family brick",
"B2": "Two family frame",
"B3": "Two family conversion",
"B9": "Two family NEC",
"C0": "Three families",
"C1": "Over six families without stores",
"C2": "Five to six families",
"C3": "Four families",
"C4": "Old law tenements",
"C5": "Converted dwellings - rooming houses",
"C6": "Cooperative",
"C7": "Walk-up apartments with stores",
"C8": "Walk-up apartment with offices",
"C9": "Walk-up apartment NEC",
"D0": "Elevator cooperative",
"D1": "Elevator apartment (semi-fireproof)",
"D2": "Elevator apartment (artists)",
"D3": "Elevator apartment (fireproof - subsidized)",
"D4": "Elevator apartment building (full building)",
"D5": "Converted elevator building",
"D6": "Elevator apartment with stores",
"D7": "Elevator apartment with offices",
"D8": "Elevator apartment NEC",
"D9": "Elevator apartment NEC",
"R1": "Condo residential unit in elevator building",
"S0": "Primarily one-family with two stores or offices",
"S1": "Primarily one-family with one store or office",
"S2": "Primarily two-family with one store or office",
"S4": "Primarily mixed use (3+ families + commercial)",
"S5": "Converted from one/two family",
}
# ββ Request model βββββββββββββββββββββββββββββββββββββββββββββββββββββ
class PredictRequest(BaseModel):
# Required: Location
latitude: float = Field(..., ge=40.4, le=40.95, description="Property latitude (NYC: 40.4β40.95)")
longitude: float = Field(..., ge=-74.3, le=-73.7, description="Property longitude (NYC: -74.3 β -73.7)")
# Required: Property basics
gross_square_feet: float = Field(..., gt=0, description="Total building gross square footage")
building_age: int = Field(..., ge=0, le=200, description="Age of building in years")
bldgclass: str = Field(..., description="NYC building class code (e.g. 'A1', 'D4', 'R1')")
borough: int = Field(..., ge=1, le=5, description="Borough: 1=Manhattan, 2=Bronx, 3=Brooklyn, 4=Queens, 5=Staten Island")
numfloors: float = Field(..., gt=0, description="Number of floors")
residential_units: int = Field(..., ge=0, description="Number of residential units")
# Optional property attributes
land_square_feet: Optional[float] = Field(None, ge=0, description="Land area in sq ft")
# Optional: building type flags (auto-inferred from bldgclass if not set)
has_elevator: Optional[int] = Field(None, ge=0, le=1, description="1 if building has elevator")
is_condo: Optional[int] = Field(None, ge=0, le=1, description="1 if condo (R-class)")
is_multifamily: Optional[int] = Field(None, ge=0, le=1, description="1 if multi-family elevator (D-class)")
is_single_fam: Optional[int] = Field(None, ge=0, le=1, description="1 if single family (A-class)")
is_mixed_use: Optional[int] = Field(None, ge=0, le=1, description="1 if mixed-use (S-class)")
# Optional: FAR/Zoning (from PLUTO; defaults to 0 if unknown)
builtfar: Optional[float] = Field(None, ge=0, description="Built Floor Area Ratio")
residfar: Optional[float] = Field(None, ge=0, description="Residential allowable FAR")
commfar: Optional[float] = Field(None, ge=0, description="Commercial allowable FAR")
facilfar: Optional[float] = Field(None, ge=0, description="Community facility allowable FAR")
maxallwfar: Optional[float] = Field(None, ge=0, description="Maximum allowable FAR")
far_utilization: Optional[float] = Field(None, ge=0, description="FAR utilization ratio (builtfar/maxallwfar)")
# Optional: ACRIS prior sale data
prior_sale_price: Optional[float] = Field(None, ge=0, description="Prior recorded sale price (from ACRIS)")
price_appreciation: Optional[float] = Field(None, description="Price appreciation ratio since prior sale")
years_since_prior_sale: Optional[float] = Field(None, ge=0, description="Years since prior sale")
has_prior_sale: Optional[int] = Field(0, ge=0, le=1, description="1 if property has a prior sale record")
is_flip: Optional[int] = Field(0, ge=0, le=1, description="1 if resold within 2 years")
# Optional: Renovation / landmark status
renovated_since_2018: Optional[int] = Field(0, ge=0, le=1, description="1 if major permit since 2018")
years_since_renovation: Optional[float] = Field(0.0, ge=0, description="Years since last major renovation")
# Optional: Time context (defaults to current year/month)
sale_year: Optional[int] = Field(None, ge=2010, le=2030, description="Year of hypothetical sale")
sale_month: Optional[int] = Field(None, ge=1, le=12, description="Month of hypothetical sale")
model_config = ConfigDict(json_schema_extra={
"example": {
"latitude": 40.6892,
"longitude": -73.9442,
"gross_square_feet": 1800,
"building_age": 55,
"bldgclass": "A1",
"borough": 3,
"numfloors": 2,
"residential_units": 1,
"land_square_feet": 2000,
}
})
# ββ Response models βββββββββββββββββββββββββββββββββββββββββββββββββββ
class FeatureDriver(BaseModel):
feature: str = Field(description="Feature name")
value: float = Field(description="Feature value for this property")
impact: float = Field(description="SHAP value (log-price units)")
direction: str = Field(description="'positive' or 'negative'")
description: str = Field(description="Human-readable feature description")
class AvmQc(BaseModel):
"""2026 AVM Quality Control block β confidence score, hit rate, and QC flags."""
confidence_score: int = Field(description="0β100 AVM reliability score (100 β segment MedAPE)")
confidence_grade: str = Field(description="Letter grade: A (β₯85) / B (β₯75) / C (β₯65) / D (<65)")
segment_medape_pct: float = Field(description="Segment-specific MedAPE used for this prediction")
comparables_found: int = Field(description="Training-set sales within 800m (hit rate signal)")
comparables_radius_m: int = Field(default=800, description="Radius used for comparable count")
sparse_market: bool = Field(description="True if fewer than 5 comparables found within 800m")
qc_flags: List[str] = Field(default_factory=list,
description="SPARSE_MARKET | LUXURY_SEGMENT | HIGH_UNCERTAINTY | METRO_CORE")
class SpatialFeatures(BaseModel):
dist_subway_m: float
dist_bus_m: float
dist_express_subway_m: float
nearest_station_is_express: int
dist_school_m: float
dist_elem_school_m: float
dist_park_m: float
dist_hospital_m: float
dist_waterfront_m: float
dist_bike_lane_m: float
poi_count_500m: float
airbnb_count_500m: float
crime_rate_nta: float
noise_density_nta: float
livability_complaint_rate: float
population_2020: float
median_income_nta: float
school_district: float
district_avg_score: float
mortgage_rate_30yr: float
class PredictResponse(BaseModel):
# Core prediction
predicted_price: int
confidence_low: int
confidence_high: int
confidence_note: str = "Β±18.98% MedAPE confidence interval"
# Model metadata
model: str
r2_test: float
medape_pct: float
# Context
borough_name: str
bldgclass_description: str
# Auto-computed spatial features
spatial_features: dict
# SHAP explanations
top_drivers: List[FeatureDriver]
# AVM Quality Control (2026 standard) β always present
avm_qc: Optional[AvmQc] = None
# Resolved NTA code (for transparency / debugging)
nta_code: Optional[str] = None
# ββ Riyadh predict schemas ββββββββββββββββββββββββββββββββββββββββββββ
RIYADH_TYPE_LABELS = {
"Ψ΄ΩΨ©": "Apartment / Ψ΄ΩΨ©",
"ΩΩΩΨ§": "Villa / ΩΩΩΨ§",
"ΩΨ·ΨΉΨ© Ψ£Ψ±ΨΆ-Ψ³ΩΩΩ": "Residential Plot / ΩΨ·ΨΉΨ© Ψ£Ψ±ΨΆ Ψ³ΩΩΩΨ©",
"ΨΉΩ
Ψ§Ψ±Ψ©": "Building / ΨΉΩ
Ψ§Ψ±Ψ©",
}
class RiyadhPredictRequest(BaseModel):
latitude: float = Field(..., ge=23.5, le=26.0, description="Property latitude (Riyadh bbox)")
longitude: float = Field(..., ge=45.5, le=48.0, description="Property longitude (Riyadh bbox)")
property_type: str = Field(..., description="Property type: Ψ΄ΩΨ© | ΩΩΩΨ§ | ΩΨ·ΨΉΨ© Ψ£Ψ±ΨΆ-Ψ³ΩΩΩ | ΨΉΩ
Ψ§Ψ±Ψ©")
area_sqm: float = Field(..., gt=0, description="Property area in square meters")
year: Optional[int] = Field(None, ge=2018, le=2030)
quarter: Optional[int] = Field(None, ge=1, le=4)
model_config = ConfigDict(json_schema_extra={
"example": {
"latitude": 24.7500,
"longitude": 46.6800,
"property_type": "Ψ΄ΩΨ©",
"area_sqm": 150.0,
"year": 2025,
"quarter": 2,
}
})
class RiyadhPredictResponse(BaseModel):
predicted_price_sqm: int = Field(description="Predicted price per sqm (SAR/mΒ²)")
predicted_total_sar: int = Field(description="Estimated total price (SAR) for the given area")
confidence_low_sqm: int
confidence_high_sqm: int
confidence_low_sar: int
confidence_high_sar: int
area_sqm: float
property_type: str
district_ar: Optional[str] = None
model: str
r2_test: float
medape_pct: float
spatial_features: dict
top_drivers: List[FeatureDriver] = Field(default_factory=list, description="Top SHAP feature drivers")
# Asking-price overlay (Bayut listing median for matched district)
asking_price_psqm: Optional[int] = Field(None, description="Bayut median asking price per sqm (SAR/mΒ²)")
asking_price_total: Optional[int] = Field(None, description="Estimated Bayut asking total for given area (SAR)")
asking_spread_pct: Optional[float] = Field(None, description="Asking-price premium over THAMAN transaction estimate (%)")
asking_price_source: Optional[str] = Field(None, description="Source platform for asking-price data")
|