Evaluarea imobiliară cu învățare automată: construirea unui sistem asemănător Zestimate
În 2006, Zillow a lansat Zestimate, primul sistem automat de rating proprietăți la scară națională. Astăzi, cu peste 135 de milioane de case evaluate în SUA, Zestimate a devenit reperul de referință pentru întregul sector PropTech. Dar cum un sistem chiar funcționează Model de evaluare automată (AVM)? Care dintre ele Algoritmii de învățare automată produc cele mai bune rezultate? Și mai presus de toate, cum este construit un sistem fiabil, interpretabil și compatibil cu reglementările anti-discriminare?
În acest articol vom construi un AVM complet: de la colectarea și curățarea datelor până la funcții inginerie avansată, de la formarea modelelor de creștere a gradientului până la implementarea producției cu REST API, trecând prin tehnici de interpretabilitate și monitorizare SHAP deriva modelului in timp.
Ce vei învăța
- Arhitectura end-to-end a unui Model de Evaluare Automatizat (AVM) pentru imobiliare
- Inginerie avansată a caracteristicilor: atribute fizice, locație, comparabile, macroeconomie
- Modele ML comparate: XGBoost, LightGBM, CatBoost, Random Forest, Neural Networks
- Interpretabilitate cu valori SHAP pentru a explica fiecare evaluare individuală
- Modelul hedonic de stabilire a prețurilor și abordarea comparabilelor (CMA)
- Implementare cu FastAPI și monitorizare a deplasării modelului cu Evidently AI
- Gestionarea părtinirii algoritmice și conformitatea cu reglementările privind locuința echitabilă
Piața AVM în 2025
Piața globală a modelelor de evaluare automată va crește cu 23% în 2024, condus de digitalizarea procesului de credit ipotecar și de adoptarea modelelor hibride care ele combină IA și evaluarea umană. Jucătorii majori includ Zillow (Zestimate), CoreLogic, Black Knight, HouseCanary și Hometrack (Marea Britanie).
Precizia medie a AVM-urilor moderne este de aproximativ unu eroare procentuală absolută medie (MdAPE) între 3% și 6% pentru proprietăți rezidențiale pe piețele cu densitate mare de date. Asta înseamnă că pentru un apartament de 300.000 de euro, eroarea tipică este între 9.000 și 18.000 de euro, rezultat care în multe contexte depășește acuratețea evaluatori umani pentru proprietăți standard.
Arhitectura sistemului AVM
Un sistem AVM de întreprindere este format din cinci straturi principale, fiecare cu responsabilități cerințe de latență bine definite și specifice.
# Architettura AVM - Schema a livelli
┌─────────────────────────────────────────────────────┐
│ DATA LAYER │
│ MLS Feed │ Catasto │ Transazioni │ OSM/Maps │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ FEATURE ENGINEERING │
│ Property Features │ Location │ Market │ Temporal │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ MODEL ENSEMBLE │
│ XGBoost │ LightGBM │ Neural Net │ CMA Model │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ INFERENCE & EXPLAINABILITY │
│ Confidence Interval │ SHAP Values │ Comparables │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ SERVING LAYER (FastAPI) │
│ REST API │ Caching │ Monitoring │ Audit Log │
└─────────────────────────────────────────────────────┘
Seturi de date și inginerie a caracteristicilor
Inima unui AVM eficient este ingineria caracteristicilor. Cercetătorii Zillow au a documentat că echipele de ML pierd adesea săptămâni experimentând cu diferiți algoritmi, când avantajul competitiv real constă în calitatea caracteristicilor, nu în complexitatea modelului.
Caracteristicile sunt împărțite în patru macro-categorii:
| Categorie | Caracteristici principale | Sursa datelor | Impactul AVM |
|---|---|---|---|
| Atribute fizice | suprafata, camere, bai, an constructie, etaj, stare de conservare | Cartea funciară, MLS | Ridicat (35-45%) |
| Locaţie | Coordonate GPS, cartier, școli din apropiere, transport, risc de inundații | OpenStreetMap, ISTAT, PCN | Foarte mare (40-50%) |
| Piaţă | prețuri comparabile, tendințe în zonă, zile pe piață, rata de absorbție | MLS, tranzacții notariale | Medie (10-20%) |
| Macroeconomice | rate ipotecare, inflație, indici de construcții, PIB local | BCE, ISTAT, Banca Italiei | Scăzut-Mediu (5-10%) |
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from geopy.distance import geodesic
class PropertyFeatureEngineer:
"""
Feature engineering per AVM immobiliare.
Gestisce attributi fisici, location e market features.
"""
def __init__(self, comparables_db, poi_db):
self.comparables_db = comparables_db
self.poi_db = poi_db
self.scaler = StandardScaler()
def build_physical_features(self, prop: dict) -> dict:
"""Feature relative agli attributi fisici dell'immobile."""
surface = prop['superficie_mq']
rooms = prop['num_vani']
bathrooms = prop['num_bagni']
year_built = prop['anno_costruzione']
return {
'superficie_mq': surface,
'num_vani': rooms,
'num_bagni': bathrooms,
'rapporto_superficie_vani': surface / max(rooms, 1),
'eta_immobile': 2025 - year_built,
'eta_categoria': self._categorize_age(year_built),
'piano_normalizzato': prop.get('piano', 0) / max(prop.get('piani_totali', 1), 1),
'is_ultimo_piano': int(prop.get('piano', 0) == prop.get('piani_totali', 0)),
'has_garage': int(prop.get('garage', False)),
'has_terrazzo': int(prop.get('terrazzo', False)),
'classe_energetica_encoded': self._encode_energy_class(
prop.get('classe_energetica', 'G')
),
}
def build_location_features(self, lat: float, lon: float) -> dict:
"""Feature geografiche e di prossimita ai servizi."""
coords = (lat, lon)
# Distanze dai principali POI
nearest_school = self._nearest_poi(coords, 'scuola')
nearest_metro = self._nearest_poi(coords, 'metro')
nearest_hospital = self._nearest_poi(coords, 'ospedale')
nearest_supermarket = self._nearest_poi(coords, 'supermercato')
city_center = self.poi_db.get_city_center()
return {
'lat': lat,
'lon': lon,
'dist_scuola_km': nearest_school['distance'],
'dist_metro_km': nearest_metro['distance'],
'dist_ospedale_km': nearest_hospital['distance'],
'dist_supermercato_km': nearest_supermarket['distance'],
'dist_centro_km': geodesic(coords, city_center).km,
'zona_istat': self._get_zone_code(lat, lon),
'reddito_medio_zona': self._get_zone_income(lat, lon),
'densita_abitativa': self._get_population_density(lat, lon),
'walk_score': self._calculate_walk_score(lat, lon),
'rischio_idrogeologico': self._get_flood_risk(lat, lon),
}
def build_market_features(self, lat: float, lon: float,
surface: float, reference_date: str) -> dict:
"""Feature di mercato basate su transazioni recenti nell'area."""
# CRITICO: usare solo dati passati rispetto a reference_date
# per evitare data leakage!
comps = self.comparables_db.get_comparables(
lat=lat,
lon=lon,
radius_km=0.5,
before_date=reference_date,
limit=20
)
if len(comps) == 0:
# Fallback su area più ampia
comps = self.comparables_db.get_comparables(
lat=lat, lon=lon, radius_km=2.0,
before_date=reference_date, limit=20
)
prices_per_sqm = [c['prezzo'] / c['superficie_mq'] for c in comps]
return {
'prezzo_medio_mq_zona': np.mean(prices_per_sqm) if prices_per_sqm else 0,
'prezzo_mediano_mq_zona': np.median(prices_per_sqm) if prices_per_sqm else 0,
'prezzo_std_mq_zona': np.std(prices_per_sqm) if prices_per_sqm else 0,
'num_transazioni_6m': len(comps),
'dom_medio_zona': np.mean([c.get('days_on_market', 0) for c in comps]),
'trend_prezzi_12m': self._calculate_price_trend(lat, lon, reference_date),
'absorption_rate': self._calculate_absorption_rate(lat, lon, reference_date),
}
def _categorize_age(self, year_built: int) -> int:
"""Categorizza l'eta dell'immobile in fasce storiche."""
if year_built < 1919: return 0 # Storico
elif year_built < 1945: return 1 # Pre-guerra
elif year_built < 1970: return 2 # Dopoguerra
elif year_built < 1990: return 3 # Anni 70-80
elif year_built < 2000: return 4 # Anni 90
elif year_built < 2010: return 5 # Anni 2000
else: return 6 # Recente
def _encode_energy_class(self, energy_class: str) -> float:
"""Converte la classe energetica in valore numerico."""
mapping = {'A4': 10, 'A3': 9, 'A2': 8, 'A1': 7, 'A': 7,
'B': 6, 'C': 5, 'D': 4, 'E': 3, 'F': 2, 'G': 1}
return mapping.get(energy_class.upper(), 1)
def _calculate_price_trend(self, lat, lon, reference_date) -> float:
"""Calcola il trend % dei prezzi negli ultimi 12 mesi."""
# Prezzi mediani 12m fa vs 6m fa vs oggi
prices_12m = self.comparables_db.get_median_price(lat, lon, reference_date, months_back=12)
prices_6m = self.comparables_db.get_median_price(lat, lon, reference_date, months_back=6)
if prices_12m and prices_12m > 0:
return (prices_6m - prices_12m) / prices_12m * 100
return 0.0
Modele de învățare automată pentru evaluare
Benchmark-urile publicate de Zillow, CoreLogic și cercetătorii academicieni arată că modelele de creștere a gradientului (XGBoost, LightGBM, CatBoost) domină constant clasamentele de acuratețe pentru datele imobiliare tabelare. Rețelele neuronale fac bani teren atunci când imaginile proprietății sunt disponibile ca caracteristici.
| Model | MdAPE Tipic | Viteza de antrenament | Interpretabilitate | Cel mai bun pentru |
|---|---|---|---|---|
| XGBoost | 3,8 - 5,2% | Mediu | Ridicat (SHAP) | Seturi de date echilibrate, caracteristici importante |
| LightGBM | 3,5 - 4,9% | Foarte rapid | Ridicat (SHAP) | Seturi mari de date, caracteristici categorice |
| CatBoost | 3,6 - 5,0% | Mediu | Ridicat (SHAP) | Caracteristici categoriale fără codare |
| Pădurea aleatorie | 4,5 - 6,5% | Lent | Medie | Linie de bază robustă, rezistență excepțională |
| Rețea neuronală (tabulară) | 4,0 - 5,5% | Foarte lent | Scăzut | Caracteristici complexe, integrarea imaginii |
| Ansamblu (stivuire) | 3,2 - 4,5% | - | Medie | Productie, precizie maxima |
import xgboost as xgb
import lightgbm as lgb
from sklearn.ensemble import RandomForestRegressor, StackingRegressor
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score, KFold
from sklearn.metrics import mean_absolute_percentage_error
import shap
import numpy as np
class AVMEnsemble:
"""
Ensemble di modelli per valutazione immobiliare.
Combina XGBoost, LightGBM e Random Forest con meta-learner Ridge.
"""
def __init__(self):
self.xgb_model = xgb.XGBRegressor(
n_estimators=1000,
learning_rate=0.05,
max_depth=6,
subsample=0.8,
colsample_bytree=0.8,
reg_alpha=0.1,
reg_lambda=1.0,
random_state=42,
n_jobs=-1,
early_stopping_rounds=50,
)
self.lgb_model = lgb.LGBMRegressor(
n_estimators=1000,
learning_rate=0.05,
num_leaves=63,
min_child_samples=20,
feature_fraction=0.8,
bagging_fraction=0.8,
bagging_freq=5,
lambda_l1=0.1,
lambda_l2=1.0,
random_state=42,
n_jobs=-1,
verbose=-1,
)
self.rf_model = RandomForestRegressor(
n_estimators=500,
max_depth=None,
min_samples_leaf=5,
n_jobs=-1,
random_state=42,
)
# Meta-learner: combina le previsioni dei base models
self.ensemble = StackingRegressor(
estimators=[
('xgb', self.xgb_model),
('lgb', self.lgb_model),
('rf', self.rf_model),
],
final_estimator=Ridge(alpha=1.0),
cv=5,
n_jobs=-1,
)
self.explainer = None
def train(self, X_train, y_train, X_val=None, y_val=None):
"""Addestramento con cross-validation e early stopping."""
# Early stopping per XGBoost
if X_val is not None:
self.xgb_model.fit(
X_train, np.log1p(y_train), # log-transform per stabilità
eval_set=[(X_val, np.log1p(y_val))],
verbose=False,
)
self.lgb_model.fit(
X_train, np.log1p(y_train),
eval_set=[(X_val, np.log1p(y_val))],
callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)],
)
else:
self.xgb_model.fit(X_train, np.log1p(y_train))
self.lgb_model.fit(X_train, np.log1p(y_train))
self.rf_model.fit(X_train, np.log1p(y_train))
# Fit dell'ensemble finale
self.ensemble.fit(X_train, np.log1p(y_train))
# Inizializza SHAP explainer per interpretabilita
self.explainer = shap.TreeExplainer(self.xgb_model)
return self
def predict(self, X) -> dict:
"""
Restituisce valutazione con intervallo di confidenza.
"""
log_pred = self.ensemble.predict(X)
price_pred = np.expm1(log_pred) # Inverti log-transform
# Calcola incertezza tramite predizioni dei singoli modelli
xgb_pred = np.expm1(self.xgb_model.predict(X))
lgb_pred = np.expm1(self.lgb_model.predict(X))
rf_pred = np.expm1(self.rf_model.predict(X))
predictions = np.stack([xgb_pred, lgb_pred, rf_pred])
uncertainty = np.std(predictions, axis=0) / price_pred
return {
'valuation': price_pred,
'low_estimate': price_pred * (1 - 2 * uncertainty),
'high_estimate': price_pred * (1 + 2 * uncertainty),
'confidence_score': np.clip(1 - uncertainty * 10, 0, 1),
}
def explain(self, X_single) -> dict:
"""
Genera spiegazione SHAP per una singola valutazione.
Mostra quali feature hanno influenzato il prezzo e in che misura.
"""
shap_values = self.explainer.shap_values(X_single)
feature_impacts = [
{
'feature': feat,
'value': float(X_single[feat]),
'impact_euro': float(shap_val * np.expm1(1)), # Approx
'direction': 'positive' if shap_val > 0 else 'negative',
}
for feat, shap_val in zip(X_single.index, shap_values[0])
]
# Ordina per impatto assoluto
feature_impacts.sort(key=lambda x: abs(x['impact_euro']), reverse=True)
return {
'top_features': feature_impacts[:10],
'base_value': float(np.expm1(self.explainer.expected_value)),
'final_value': float(np.expm1(self.explainer.expected_value + shap_values[0].sum())),
}
def evaluate(self, X_test, y_test) -> dict:
"""Metriche di valutazione complete."""
predictions = self.predict(X_test)
pred_prices = predictions['valuation']
mape = mean_absolute_percentage_error(y_test, pred_prices) * 100
mdape = np.median(np.abs((y_test - pred_prices) / y_test)) * 100
# Percentuale previsioni entro 5%, 10%, 20% del valore reale
errors = np.abs((y_test - pred_prices) / y_test)
within_5 = np.mean(errors <= 0.05) * 100
within_10 = np.mean(errors <= 0.10) * 100
within_20 = np.mean(errors <= 0.20) * 100
return {
'mape': round(mape, 2),
'mdape': round(mdape, 2),
'within_5pct': round(within_5, 1),
'within_10pct': round(within_10, 1),
'within_20pct': round(within_20, 1),
}
Modelul hedonic de prețuri și analiza pieței comparabile
Pe lângă modelele ML pur, un AVM de producție integrează două abordări complementare: cel Model hedonic de prețuri (HPM) iar cel Analiza pieței comparabile (CMA). HPM tratează prețul unei proprietăți ca fiind suma valorilor implicite ale fiecăreia caracteristică (fiecare metru pătrat valorează X, fiecare baie suplimentară valorează Y etc.). Cu toate acestea, CMA caută proprietăți similare vândute recent în apropiere și ajustează pretul pentru diferente.
from dataclasses import dataclass
from typing import List
import numpy as np
@dataclass
class Comparable:
id: str
prezzo: float
superficie_mq: float
num_vani: int
num_bagni: int
distanza_km: float
giorni_fa: int
lat: float
lon: float
class ComparableMarketAnalysis:
"""
CMA: stima il valore comparando con immobili simili venduti di recente.
Applica aggiustamenti per differenze nelle caratteristiche.
"""
# Aggiustamenti di mercato (da calibrare per zona)
ADJUSTMENTS = {
'per_mq_extra': 1800, # EUR per mq di differenza
'per_bagno_extra': 8000, # EUR per bagno aggiuntivo
'per_anno_eta': -150, # EUR per anno di eta
'per_piano': 1200, # EUR per piano (es: piano 3 vs piano 1)
'garage_premium': 15000, # EUR per garage incluso
'terrazzo_premium': 8000, # EUR per terrazzo
}
def estimate(self, subject: dict, comparables: List[Comparable]) -> dict:
"""
Stima il valore dell'immobile tramite analisi dei comparables.
"""
if not comparables:
return {'error': 'Nessun comparable disponibile'}
adjusted_prices = []
for comp in comparables:
adjusted_price = comp.prezzo
# Aggiustamento superficie
surface_diff = subject['superficie_mq'] - comp.superficie_mq
adjusted_price += surface_diff * self.ADJUSTMENTS['per_mq_extra']
# Aggiustamento bagni
bath_diff = subject.get('num_bagni', 1) - comp.num_bagni
adjusted_price += bath_diff * self.ADJUSTMENTS['per_bagno_extra']
# Aggiustamento eta (più recente = più valore)
age_diff = (2025 - subject.get('anno_costruzione', 1980)) - \
(2025 - getattr(comp, 'anno_costruzione', 1980))
adjusted_price += age_diff * self.ADJUSTMENTS['per_anno_eta']
# Peso per distanza e freschezza dei dati
distance_weight = 1 / (1 + comp.distanza_km * 2)
recency_weight = 1 / (1 + comp.giorni_fa / 90)
weight = distance_weight * recency_weight
adjusted_prices.append((adjusted_price, weight))
# Media ponderata
total_weight = sum(w for _, w in adjusted_prices)
weighted_value = sum(p * w for p, w in adjusted_prices) / total_weight
return {
'cma_value': round(weighted_value, -3), # Arrotonda a migliaia
'num_comparables': len(comparables),
'comparable_range': {
'min': min(p for p, _ in adjusted_prices),
'max': max(p for p, _ in adjusted_prices),
},
}
API de evaluare cu FastAPI
Servirea modelului se face printr-un API REST cu FastAPI, conceput pentru gestionați mii de solicitări pe minut cu o latență mai mică de 200 ms.
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, Field, field_validator
from typing import Optional
import numpy as np
import logging
app = FastAPI(title="AVM API", version="2.0.0")
logger = logging.getLogger(__name__)
class PropertyInput(BaseModel):
"""Schema di input per la valutazione immobiliare."""
superficie_mq: float = Field(..., gt=10, lt=2000, description="Superficie in mq")
num_vani: int = Field(..., ge=1, le=20)
num_bagni: int = Field(..., ge=1, le=10)
anno_costruzione: int = Field(..., ge=1800, le=2025)
piano: int = Field(default=0, ge=0, le=50)
piani_totali: int = Field(default=1, ge=1, le=50)
classe_energetica: str = Field(default='G')
has_garage: bool = False
has_terrazzo: bool = False
lat: float = Field(..., ge=35.0, le=48.0) # Italia
lon: float = Field(..., ge=6.0, le=19.0)
@field_validator('classe_energetica')
@classmethod
def validate_energy_class(cls, v):
valid = ['A4', 'A3', 'A2', 'A1', 'A', 'B', 'C', 'D', 'E', 'F', 'G']
if v.upper() not in valid:
raise ValueError(f'Classe energetica non valida. Usa: {valid}')
return v.upper()
class ValuationResponse(BaseModel):
"""Risposta della valutazione con range e spiegazione."""
valuation: float
low_estimate: float
high_estimate: float
confidence_score: float
price_per_sqm: float
comparable_value: Optional[float]
top_factors: list
model_version: str
@app.post("/api/v1/valuation", response_model=ValuationResponse)
async def valuate_property(
prop: PropertyInput,
model: AVMEnsemble = Depends(get_model),
feature_eng: PropertyFeatureEngineer = Depends(get_feature_engineer),
):
"""
Valuta un immobile con ML ensemble + CMA.
Restituisce stima puntuale, range e spiegazione.
"""
try:
# Feature engineering
features = {
**feature_eng.build_physical_features(prop.dict()),
**feature_eng.build_location_features(prop.lat, prop.lon),
**feature_eng.build_market_features(
prop.lat, prop.lon, prop.superficie_mq,
reference_date='2025-03-01'
),
}
X = pd.DataFrame([features])
# Predizione ensemble
prediction = model.predict(X)
explanation = model.explain(X.iloc[0])
# CMA come cross-check
comparables = feature_eng.comparables_db.get_comparables(
lat=prop.lat, lon=prop.lon, radius_km=1.0,
before_date='2025-03-01', limit=10
)
cma = ComparableMarketAnalysis().estimate(prop.dict(), comparables)
logger.info(
"Valuation completed",
extra={
"lat": prop.lat, "lon": prop.lon,
"valuation": prediction['valuation'][0],
"confidence": prediction['confidence_score'][0],
}
)
return ValuationResponse(
valuation=round(float(prediction['valuation'][0]), -3),
low_estimate=round(float(prediction['low_estimate'][0]), -3),
high_estimate=round(float(prediction['high_estimate'][0]), -3),
confidence_score=round(float(prediction['confidence_score'][0]), 3),
price_per_sqm=round(float(prediction['valuation'][0]) / prop.superficie_mq, 0),
comparable_value=cma.get('cma_value'),
top_factors=explanation['top_features'][:5],
model_version="avm-v2.1.0",
)
except Exception as e:
logger.error(f"Valuation error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Errore durante la valutazione")
Monitorizarea derivei modelului cu Evident AI
Modelele de evaluare imobiliară sunt deosebit de sensibile la derive în timp: prețurile se modifică, cartierele se transformă, infrastructură nouă sunt construite. Un sistem AVM în producție trebuie să monitorizeze constant calitatea prognozelor și schimbările în distribuția datelor.
from evidently.report import Report
from evidently.metrics import (
DataDriftTable, ColumnDriftMetric,
RegressionQualityMetric, RegressionPredictedVsActualScatter,
)
from evidently.test_suite import TestSuite
from evidently.tests import (
TestColumnDrift, TestValueMeanInNSigmas,
)
import pandas as pd
from datetime import datetime, timedelta
class AVMMonitor:
"""
Monitoraggio continuo della qualità del modello AVM.
Rilevazione data drift e degradazione delle performance.
"""
def __init__(self, reference_data: pd.DataFrame):
self.reference_data = reference_data
def run_quality_report(self, current_data: pd.DataFrame,
predictions: pd.Series,
actuals: pd.Series) -> dict:
"""
Report completo: data drift + qualità regressione.
"""
report = Report(metrics=[
DataDriftTable(),
RegressionQualityMetric(),
RegressionPredictedVsActualScatter(),
ColumnDriftMetric(column_name='prezzo_medio_mq_zona'),
ColumnDriftMetric(column_name='superficie_mq'),
])
# Unisci predictions e actuals al current_data
eval_data = current_data.copy()
eval_data['prediction'] = predictions.values
eval_data['target'] = actuals.values
report.run(
reference_data=self.reference_data,
current_data=eval_data,
)
report_dict = report.as_dict()
# Estrai metriche chiave
metrics = report_dict.get('metrics', [])
drift_detected = any(
m.get('result', {}).get('drift_detected', False)
for m in metrics
)
return {
'drift_detected': drift_detected,
'report_date': datetime.now().isoformat(),
'metrics': report_dict,
}
def run_test_suite(self, current_data: pd.DataFrame) -> dict:
"""
Test automatici: fallisce se il modello degrada oltre soglie.
"""
tests = TestSuite(tests=[
TestColumnDrift(column_name='superficie_mq'),
TestColumnDrift(column_name='prezzo_medio_mq_zona'),
TestValueMeanInNSigmas(
column_name='error_pct',
n=3, # Allerta se media errore supera 3 sigma
),
])
tests.run(
reference_data=self.reference_data,
current_data=current_data,
)
return {
'passed': tests.as_dict()['summary']['all_passed'],
'results': tests.as_dict(),
}
Managementul părtinirii algoritmice în PropTech
În mai 2024, HUD a lansat un ghid care clarifică în mod explicit modul în care Legea privind locuințele echitabile se aplică și sistemelor automate de evaluare. Un model AVM care produce în mod sistematic evaluări mai mici în zone prevalența minorităților etnice poate constitui o încălcare a legii, chiar şi în absenţa intenţiei discriminatorii.
Măsuri obligatorii:
- Excludeți în mod proactiv rasa, etnia, naționalitatea și religia din caracteristici
- Urmăriți evaluările după codul poștal și comparați cu structura demografică
- Implementați teste de corectitudine (impact diferit, cote egalizate) ca parte a conductei CI/CD
- Mențineți jurnalele de audit complete ale fiecărei evaluări efectuate
- Utilizați valorile SHAP pentru a explica fiecare estimare individuală la cererea utilizatorului
Cele mai bune practici și anti-modele
Cele mai bune practici pentru AVM în producție
- Transformarea în jurnal a țintei: Prețurile imobiliare urmează distribuția log-normală. Aplicarea log1p() la țintă reduce impactul valorii aberante și îmbunătățește antrenamentul.
- Separare strictă a timpului: Nu utilizați niciodată date viitoare decât data de predicție în ingineria caracteristicilor (scurgere). Utilizați întotdeauna
before_dateîn interogări comparabile. - Interval de încredere obligatoriu: Nu returnați niciodată o estimare punctuală fără un interval de încredere. Utilizatorii trebuie să știe cât de incert este modelul.
- Alternativ pe CMA: Dacă încrederea ML este scăzută (<0,5), utilizați CMA ca estimare primară sau combinați cele două cu pondere proporțională cu încrederea.
- Recalificare lunară: Modelele AVM se degradează rapid pe piețele volatile. Programați o reinstruire lunară pe date de 24 de luni.
- Versiune model: Fiecare prognoză trebuie să fie trasabilă până la versiunea specifică a modelului care a generat-o (conformitatea auditului).
Anti-modele critice
- Scurgere caracteristică: Includerea datelor din tranzacții după data evaluării produce modele care sunt artificial precise în antrenament, dar inutile în producție.
- Supraadaptare pe codul poștal: Utilizarea codului poștal ca o caracteristică categorială fără netezire duce la instabilitate pentru zonele cu puține date.
- Ignorați valorile aberante: Proprietățile de lux, licitațiile, cedările de portofoliu distorsionează formarea. Filtrați sau cântăriți separat.
- Prezice fără validare: Un AVM fără un sistem de monitorizare devine nefiabil după câteva luni pe piețe în mișcare.
Concluzii și pașii următori
Construirea unui AVM fiabil necesită mai mult decât un algoritm ML bun: necesită Inginerie riguroasă a caracteristicilor, management al părtinirii algoritmice, interpretabilitate prin valorile SHAP și un sistem de monitorizare continuă. Modele de creștere a gradului (XGBoost, LightGBM) rămân de ultimă generație pentru datele tabelare imobiliare, dar ansamblul cu CMA și stratul de încredere sunt ceea ce distinge un AVM de producție dintr-un simplu exerciţiu academic.
Explorați alte articole din seria PropTech
- Articolul 00 - Arhitectura Platformei Imobiliare de la Scala
- Articolul 02 - Arhitectura software BIM: Modelare 3D pentru AEC
- Articolul 03 - Smart Building IoT: Integrarea senzorilor și Edge Computing
- Articolul 09 - Confidențialitate și conformitate în PropTech: locuințe echitabile și părtinire







