Spaces:
Running
Running
| """ | |
| 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") | |