Wycena nieruchomości za pomocą uczenia maszynowego: budowanie systemu podobnego do Zestimate
W 2006 roku firma Zillow uruchomiła Zestimate, pierwszy automatyczny system oceny nieruchomości w skali kraju. Obecnie, po ocenie ponad 135 milionów domów w USA, Zestimate stał się punktem odniesienia dla całego sektora PropTech. Ale jak system faktycznie działa Zautomatyzowany model wyceny (AVM)? Które Czy algorytmy uczenia maszynowego dają najlepsze wyniki? A przede wszystkim to, jak jest zbudowany system rzetelny, dający się zinterpretować i zgodny z przepisami antydyskryminacyjnymi?
W tym artykule zbudujemy kompletny AVM: od gromadzenia danych i czyszczenia po funkcje zaawansowana inżynieria, od modeli zwiększania gradientu szkoleniowego po wdrożenie produkcyjne z REST API, przechodząc przez techniki interpretowalności i monitorowanie SHAP dryf modelu w czasie.
Czego się nauczysz
- Kompleksowa architektura Automatycznego Modelu Wyceny (AVM) dla nieruchomości
- Zaawansowana inżynieria cech: atrybuty fizyczne, lokalizacja, elementy porównawcze, makroekonomia
- Porównanie modeli ML: XGBoost, LightGBM, CatBoost, Random Forest, Neural Networks
- Interpretowalność za pomocą wartości SHAP w celu wyjaśnienia każdej indywidualnej oceny
- Hedoniczny model wyceny i podejście porównawcze (CMA)
- Wdrożenie za pomocą FastAPI i monitorowanie dryfu modelu za pomocą Evidently AI
- Zarządzanie stronniczością algorytmiczną i zgodnością z przepisami Fair Housing
Rynek AVM w 2025 roku
Globalny rynek modeli automatycznej wyceny wzrośnie o 23% w 2024 roku, napędzane cyfryzacją procesu udzielania kredytów hipotecznych i przyjęciem modeli hybrydowych, które łączą sztuczną inteligencję i ocenę człowieka. Główni gracze to Zillow (Zestimate), CoreLogic, Black Knight, HouseCanary i Hometrack (Wielka Brytania).
Średnia dokładność nowoczesnych AVM wynosi około jednego mediana bezwzględnego błędu procentowego (MdAPE) od 3% do 6% dla nieruchomości mieszkalnych na rynkach o dużym zagęszczeniu danych. Oznacza to, że w przypadku mieszkania za 300 000 euro typowym błędem jest od 9 000 do 18 000 euro, co w wielu kontekstach przekracza dokładność rzeczoznawcy-ludzi dla standardowych właściwości.
Architektura systemu AVM
Korporacyjny system AVM składa się z pięciu głównych warstw, z których każda ma określone obowiązki dobrze zdefiniowane i specyficzne wymagania dotyczące opóźnień.
# 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 │
└─────────────────────────────────────────────────────┘
Zbiory danych i inżynieria cech
Sercem skutecznego AVM jest inżynieria funkcji. Naukowcy z Zillow tak mają udokumentowano, że zespoły ML często marnują tygodnie na eksperymentowanie z różnymi algorytmami, gdy prawdziwa przewaga konkurencyjna leży w jakości funkcji, a nie w złożoność modelu.
Funkcje są podzielone na cztery makrokategorie:
| Kategoria | Główne cechy | Źródło danych | Wpływ AVM |
|---|---|---|---|
| Atrybuty fizyczne | powierzchnia, pokoje, łazienki, rok budowy, piętro, stan zachowania | Rejestr gruntów, MLS | Wysoka (35-45%) |
| Lokalizacja | Współrzędne GPS, okolica, pobliskie szkoły, transport, ryzyko powodzi | OpenStreetMap, ISTAT, PCN | Bardzo wysoki (40-50%) |
| Rynek | porównywalne ceny, tendencje obszarowe, dni obecności na rynku, stopień absorpcji | MLS, transakcje notarialne | Średni (10-20%) |
| Makroekonomiczne | oprocentowanie kredytów hipotecznych, inflacja, wskaźniki budownictwa, lokalny PKB | EBC, ISTAT, Bank Włoch | Niski-Średni (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 uczenia maszynowego do oceny
Wyniki testów porównawczych opublikowane przez Zillow, CoreLogic i badaczy akademickich pokazują że modele wzmacniające gradient (XGBoost, LightGBM, CatBoost) niezmiennie dominują rankingi dokładności tabelarycznych danych dotyczących nieruchomości. Sieci neuronowe zarabiają pieniądze land, gdy obrazy nieruchomości są dostępne jako funkcje.
| Model | MdAPE Typowe | Szybkość treningu | Interpretowalność | Najlepsze dla |
|---|---|---|---|---|
| XGBoost | 3,8 - 5,2% | Średni | Wysoka (KSZTAŁT) | Zrównoważone zbiory danych, ważne funkcje |
| LekkiGBM | 3,5 - 4,9% | Bardzo szybko | Wysoka (KSZTAŁT) | Duże zbiory danych, cechy kategoryczne |
| CatBoost | 3,6 - 5,0% | Średni | Wysoka (KSZTAŁT) | Funkcje kategoryczne bez kodowania |
| Losowy las | 4,5 - 6,5% | Powolny | Przeciętny | Solidna linia bazowa, odstający opór |
| Sieć neuronowa (tabela) | 4,0 - 5,5% | Bardzo powolny | Niski | Złożone funkcje, integracja obrazu |
| Zespół (układanie) | 3,2 - 4,5% | - | Przeciętny | Produkcja, maksymalna dokładność |
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),
}
Hedoniczny model cenowy i analiza rynku porównawczego
Oprócz czystych modeli ML produkcyjne AVM integruje dwa uzupełniające się podejścia: the Hedoniczny model cenowy (HPM) e la Porównywalna analiza rynku (CMA). HPM traktuje cenę nieruchomości jako sumę sugerowanych wartości każdej z nich charakterystyka (każdy metr kwadratowy jest wart X, każda dodatkowa łazienka jest warta Y itp.). CMA szuka jednak podobnych nieruchomości niedawno sprzedanych w pobliżu i dostosowuje się cena za różnice.
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 ewaluacyjne z FastAPI
Obsługa modeli odbywa się poprzez REST API z FastAPI, przeznaczone dla zarządzaj tysiącami żądań na minutę z opóźnieniem mniejszym niż 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")
Monitorowanie dryfu modelu za pomocą sztucznej inteligencji
Modele wyceny nieruchomości są szczególnie wrażliwe na dryf z biegiem czasu: zmieniają się ceny, przekształcają się dzielnice, nowa infrastruktura są zbudowane. System AVM w produkcji musi stale monitorować jakość prognoz i zmiany w dystrybucji danych.
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(),
}
Zarządzanie błądami algorytmicznymi w PropTech
W maju 2024 r. HUD opublikował wytyczne, które wyraźnie wyjaśniają, w jaki sposób Ustawa o godziwym mieszkalnictwie ma również zastosowanie do automatycznych systemów oceny. Model AVM, który generuje systematycznie niższe oceny w obszarach przewaga mniejszości etnicznych może stanowić naruszenie prawa, nawet w przypadku braku zamiaru dyskryminacyjnego.
Środki obowiązkowe:
- Aktywnie wykluczaj rasę, pochodzenie etniczne, narodowość i religię z funkcji
- Śledź oceny według kodu pocztowego i porównuj je ze strukturą demograficzną
- Wdrażaj testy uczciwości (różny wpływ, wyrównane szanse) w ramach rurociągu CI/CD
- Prowadź pełne dzienniki audytu każdej przeprowadzonej oceny
- Użyj wartości SHAP, aby wyjaśnić każde indywidualne oszacowanie na żądanie użytkownika
Najlepsze praktyki i anty-wzorce
Najlepsze praktyki AVM w produkcji
- Transformacja logu celu: Ceny nieruchomości mają rozkład logarytmiczno-normalny. Zastosowanie log1p() do celu zmniejsza wpływ wartości odstających i usprawnia szkolenie.
- Ścisła separacja czasowa: Nigdy nie używaj przyszłości danych niż data przewidywania w inżynierii cech (wyciek). Zawsze używaj
before_datew zapytaniach porównawczych. - Obowiązkowy przedział ufności: Nigdy nie zwracaj oszacowania punktowego bez zakresu ufności. Użytkownicy muszą wiedzieć, jak niepewny jest model.
- Powrót do CMA: Jeżeli pewność ML jest niska (<0,5), należy zastosować CMA jako główne oszacowanie lub połączyć je z wagą proporcjonalną do ufności.
- Comiesięczne ponowne szkolenie: Modele AVM szybko ulegają degradacji na niestabilnych rynkach. Zaplanuj comiesięczne ponowne szkolenie na podstawie danych kroczących z 24 miesięcy.
- Wersjonowanie modelu: Każda prognoza musi być powiązana z konkretną wersją modelu, który ją wygenerował (kontrola zgodności).
Krytyczne anty-wzorce
- Wyciek funkcji: Uwzględnienie danych z transakcji po dacie oceny tworzy modele, które są sztucznie dokładne w szkoleniu, ale bezużyteczne w produkcji.
- Overfit na kodzie pocztowym: Używanie kodu pocztowego jako cechy kategorycznej bez wygładzania prowadzi do niestabilności w obszarach z małą ilością danych.
- Ignoruj wartości odstające: Luksusowe nieruchomości, aukcje, sprzedaż portfeli zakłócają szkolenie. Przefiltruj lub zważ osobno.
- Przewiduj bez sprawdzania: AVM bez systemu monitorowania staje się zawodny po kilku miesiącach na ruchomych rynkach.
Wnioski i dalsze kroki
Zbudowanie niezawodnego AVM wymaga czegoś więcej niż dobrego algorytmu ML: wymaga Rygorystyczna inżynieria funkcji, algorytmiczne zarządzanie odchyleniami, interpretowalność poprzez wartości SHAP i system ciągłego monitorowania. Modele wzmacniania gradientowego (XGBoost, LightGBM) pozostają najnowocześniejsze w zakresie danych tabelarycznych dotyczących nieruchomości, ale zespół z CMA i warstwa pewności siebie to cechy, które wyróżniają produkcyjne AVM z prostego ćwiczenia akademickiego.
Zapoznaj się z innymi artykułami z serii PropTech
- Artykuł 00 - Architektura Platformy Nieruchomości w Scali
- Artykuł 02 - Architektura oprogramowania BIM: Modelowanie 3D dla AEC
- Artykuł 03 - Inteligentny budynek IoT: integracja czujników i przetwarzanie brzegowe
- Artykuł 09 – Prywatność i zgodność w PropTech: uczciwe warunki mieszkaniowe i stronniczość







