|
|
|
|
|
|
|
import joblib |
|
import pandas as pd |
|
import numpy as np |
|
from fastapi import FastAPI, HTTPException |
|
from pydantic import BaseModel |
|
import warnings |
|
warnings.filterwarnings('ignore') |
|
|
|
|
|
|
|
|
|
app = FastAPI( |
|
title="IGUDAR AI Valuation API", |
|
description="An API to serve the trained property valuation model for Moroccan real estate.", |
|
version="1.0" |
|
) |
|
|
|
|
|
|
|
try: |
|
model = joblib.load("./models/valuation_model.joblib") |
|
preprocessing = joblib.load("./models/preprocessing_objects.joblib") |
|
|
|
|
|
scaler = preprocessing['scaler'] |
|
label_encoders = preprocessing['label_encoders'] |
|
feature_names = preprocessing['feature_names'] |
|
|
|
print("✅ Models and preprocessing objects loaded successfully.") |
|
|
|
except FileNotFoundError: |
|
print("❌ ERROR: Model or preprocessing files not found. Ensure they are in the /models directory.") |
|
model = None |
|
|
|
|
|
|
|
|
|
|
|
class PropertyFeatures(BaseModel): |
|
size_m2: float |
|
bedrooms: int |
|
bathrooms: int |
|
age_years: int |
|
property_type: str |
|
city: str |
|
infrastructure_score: float |
|
economic_score: float |
|
lifestyle_score: float |
|
investment_score: float |
|
neighborhood_tier: int |
|
total_amenities: int |
|
data_quality: float = 0.9 |
|
has_coordinates: bool = True |
|
|
|
|
|
|
|
@app.post("/valuation") |
|
def predict_valuation(property_data: PropertyFeatures): |
|
""" |
|
Predicts the value of a property based on its features. |
|
Accepts a JSON object with property details and returns a prediction. |
|
""" |
|
if model is None: |
|
raise HTTPException(status_code=500, detail="Model is not loaded. Check server logs.") |
|
|
|
|
|
data_dict = property_data.dict() |
|
|
|
|
|
features = {name: 0 for name in feature_names} |
|
|
|
|
|
|
|
|
|
features.update({ |
|
'size_m2': data_dict.get('size_m2', 100), |
|
'bedrooms': data_dict.get('bedrooms', 2), |
|
'bathrooms': data_dict.get('bathrooms', 1), |
|
'age_years': min(data_dict.get('age_years', 5), 50), |
|
'infrastructure_score': data_dict.get('infrastructure_score', 50), |
|
'economic_score': data_dict.get('economic_score', 50), |
|
'lifestyle_score': data_dict.get('lifestyle_score', 50), |
|
'investment_score': data_dict.get('investment_score', 50), |
|
'neighborhood_tier': data_dict.get('neighborhood_tier', 3), |
|
'total_amenities': data_dict.get('total_amenities', 20), |
|
'data_quality': data_dict.get('data_quality', 0.8) |
|
}) |
|
|
|
|
|
features['room_density'] = min((features['bedrooms'] + features['bathrooms']) / features['size_m2'], 0.2) |
|
features['amenity_density'] = min(features['total_amenities'] / features['size_m2'], 2) |
|
features['location_quality'] = (features['infrastructure_score'] * 0.4 + |
|
features['economic_score'] * 0.3 + |
|
features['lifestyle_score'] * 0.3) |
|
features['investment_attractiveness'] = ((5 - features['neighborhood_tier']) * 20 + |
|
features['location_quality'] * 0.5 + |
|
(10 if data_dict.get('has_coordinates', True) else 0) + |
|
(features['data_quality'] * 20)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for col, le in label_encoders.items(): |
|
encoded_col_name = f"{col}_encoded" |
|
if encoded_col_name in features: |
|
try: |
|
|
|
value = data_dict.get(col) |
|
encoded_value = le.transform([value])[0] |
|
features[encoded_col_name] = encoded_value |
|
except Exception as e: |
|
|
|
print(f"Warning: Could not encode '{value}' for feature '{col}'. Defaulting to 0. Error: {e}") |
|
features[encoded_col_name] = 0 |
|
|
|
|
|
df = pd.DataFrame([features])[feature_names] |
|
|
|
|
|
df_scaled = scaler.transform(df) |
|
|
|
|
|
prediction = model.predict(df_scaled)[0] |
|
|
|
|
|
predicted_price = round(max(200000, prediction), 0) |
|
|
|
return { |
|
"predicted_price_mad": predicted_price, |
|
"predicted_price_per_m2": round(predicted_price / data_dict['size_m2'], 0), |
|
"model_used": "igudar_valuation_v1_xgboost" |
|
} |
|
|
|
@app.get("/") |
|
def read_root(): |
|
return {"message": "Welcome to the IGUDAR AI Valuation API. Use the /docs endpoint to test."} |