Oceňování nemovitostí pomocí strojového učení: Vybudování systému podobného Zestimate
V roce 2006 společnost Zillow spustila Zestimate, první automatický ratingový systém vlastnosti v celostátním měřítku. Dnes je v USA hodnoceno více než 135 milionů domácností Zestimate se stal referenčním měřítkem pro celý sektor PropTech. Ale jak systém skutečně funguje Automatizovaný model oceňování (AVM)? Které Poskytují algoritmy strojového učení nejlepší výsledky? A především, jak je postaven spolehlivý, interpretovatelný a v souladu s antidiskriminačními předpisy?
V tomto článku vytvoříme kompletní AVM: od sběru dat a čištění až po funkce pokročilé inženýrství, od tréninkových modelů zvyšujících gradient až po produkční nasazení s REST API, procházející technikami interpretace SHAP a monitorováním drift modelu v čase.
Co se naučíte
- Komplexní architektura modelu automatického oceňování (AVM) pro nemovitosti
- Pokročilé inženýrství funkcí: fyzikální vlastnosti, umístění, srovnatelnosti, makroekonomie
- Srovnávané modely ML: XGBoost, LightGBM, CatBoost, Random Forest, Neural Networks
- Interpretovatelnost s hodnotami SHAP pro vysvětlení každého jednotlivého hodnocení
- Hedonický cenový model a srovnatelný přístup (CMA)
- Nasazení s FastAPI a monitorování driftu modelu s Evidently AI
- Řízení algoritmického zkreslení a soulad s předpisy Fair Housing
Trh AVM v roce 2025
Globální trh s automatizovanými modely oceňování poroste v roce 2024 o 23 %, řízena digitalizací hypotečního procesu a přijetím hybridních modelů, které kombinují AI a lidské hodnocení. Mezi hlavní hráče patří Zillow (Zestimate), CoreLogic, Black Knight, HouseCanary a Hometrack (UK).
Průměrná přesnost moderních AVM je kolem jedné střední absolutní procentuální chyba (MdAPE) mezi 3 % a 6 % pro rezidenční nemovitosti na trzích s vysokou hustotou dat. To znamená, že u bytu za 300 000 eur je typická chyba mezi 9 000 a 18 000 eur, což je výsledek, který v mnoha kontextech překračuje přesnost lidské oceňování standardních nemovitostí.
Architektura systému AVM
Podnikový systém AVM se skládá z pěti hlavních vrstev, z nichž každá má odpovědnost dobře definované a specifické požadavky na latenci.
# 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 │
└─────────────────────────────────────────────────────┘
Datové sady a Feature Engineering
Srdcem efektivního AVM je inženýrství funkcí. Výzkumníci Zillow ano zdokumentovali, že týmy ML často ztrácejí týdny experimentováním s různými algoritmy, kdy skutečná konkurenční výhoda spočívá v kvalitě funkcí, nikoli v složitost modelu.
Funkce jsou rozděleny do čtyř makro kategorií:
| Kategorie | Hlavní vlastnosti | Zdroj dat | AVM dopad |
|---|---|---|---|
| Fyzické atributy | plocha, pokoje, koupelny, rok výstavby, podlaha, stav zachování | Katastr nemovitostí, MLS | Vysoká (35–45 %) |
| Umístění | GPS souřadnice, okolí, školy v okolí, doprava, riziko povodní | OpenStreetMap, ISTAT, PCN | Velmi vysoká (40–50 %) |
| Trh | srovnatelné ceny, plošné trendy, dny na trhu, míra absorpce | MLS, notářské transakce | Střední (10–20 %) |
| Makroekonomické | sazby hypoték, inflace, stavební indexy, místní HDP | ECB, ISTAT, Bank of Italy | Nízký-Střední (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
Modely strojového učení pro hodnocení
Ukazují srovnávací testy publikované Zillow, CoreLogic a akademickými výzkumníky že modely se zesílením gradientu (XGBoost, LightGBM, CatBoost) trvale dominují žebříčky přesnosti pro tabulkové údaje o nemovitostech. Neuronové sítě vydělávají peníze pozemky, když jsou obrázky nemovitostí k dispozici jako prvky.
| Model | MdAPE Typické | Rychlost tréninku | Interpretovatelnost | BestFor |
|---|---|---|---|---|
| XGBoost | 3,8–5,2 % | Střední | Vysoká (SHAP) | Vyvážené datové sady, důležité funkce |
| LightGBM | 3,5–4,9 % | Velmi rychlé | Vysoká (SHAP) | Velké datové sady, kategorické funkce |
| CatBoost | 3,6–5,0 % | Střední | Vysoká (SHAP) | Kategorické vlastnosti bez kódování |
| Náhodný les | 4,5–6,5 % | Pomalý | Průměrný | Robustní základní linie, extrémní odolnost |
| Neuronová síť (tabulka) | 4,0–5,5 % | Velmi pomalé | Nízký | Komplexní funkce, integrace obrazu |
| Ensemble (stohování) | 3,2–4,5 % | - | Průměrný | Výroba, maximální přesnost |
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),
}
Hédonický cenový model a srovnatelná analýza trhu
Kromě čistě ML modelů integruje produkční AVM dva komplementární přístupy: a Hedonic Pricing Model (HPM) e la Srovnatelná analýza trhu (CMA). HPM považuje cenu nemovitosti za součet implikovaných hodnot každé z nich charakteristika (každý metr čtvereční má hodnotu X, každá další koupelna má hodnotu Y atd.). CMA však hledá podobné nemovitosti nedávno prodané v okolí a přizpůsobuje se cena za rozdíly.
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),
},
}
Evaluation API s FastAPI
Obsluhování modelu se provádí přes REST API s FastAPI, určené pro spravovat tisíce požadavků za minutu s latencí menší než 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")
Monitorování driftu modelu s Evidently AI
Modely oceňování nemovitostí jsou zvláště citlivé na posun v průběhu času: ceny se mění, sousedství se mění, nová infrastruktura jsou postaveny. Systém AVM ve výrobě musí neustále monitorovat kvalita prognóz a změny v distribuci dat.
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(),
}
Správa algoritmického zkreslení v PropTech
V květnu 2024 vydal HUD pokyny, které výslovně objasňují, jak Fair Housing Act se vztahuje i na systémy automatického hodnocení. Model AVM, který v oblastech produkuje systematicky nižší hodnocení převaha etnických menšin může představovat porušení zákona, i bez diskriminačního záměru.
Povinná opatření:
- Proaktivně vyloučit rasu, etnicitu, národnost a náboženství z rysů
- Sledujte hodnocení podle PSČ a porovnejte s demografickým složením
- Implementujte testy poctivosti (různý dopad, vyrovnané šance) jako součást CI/CD potrubí
- Uchovávejte kompletní protokoly auditu o každém provedeném hodnocení
- Použijte hodnoty SHAP k vysvětlení každého jednotlivého odhadu na žádost uživatele
Osvědčené postupy a anti-vzorce
Nejlepší postupy pro AVM ve výrobě
- Log-transformace cíle: Ceny nemovitostí se řídí log-normálním rozdělením. Použití log1p() na cíl snižuje dopad odlehlých hodnot a zlepšuje trénink.
- Přísné časové oddělení: Nikdy nepoužívejte data v budoucnosti, než je datum predikce v inženýrství funkcí (únik). Vždy používejte
before_dateve srovnatelných dotazech. - Povinný interval spolehlivosti: Nikdy nevracejte bodový odhad bez rozsahu spolehlivosti. Uživatelé musí vědět, jak nejistý je model.
- Záloha na CMA: Pokud je spolehlivost ML nízká (<0,5), použijte CMA jako primární odhad nebo oba zkombinujte s váhou úměrnou spolehlivosti.
- Měsíční rekvalifikace: Modely AVM rychle degradují na nestabilních trzích. Naplánujte si měsíční přeškolení na 24měsíční klouzavá data.
- Verze modelu: Každá prognóza musí být sledovatelná ke konkrétní verzi modelu, který ji vygeneroval (shoda s auditem).
Kritické anti-vzory
- Únik funkce: Zahrnutí dat z transakcí po datu vyhodnocení vytváří modely, které jsou uměle přesné ve školení, ale nepoužitelné ve výrobě.
- Přebytek na PSČ: Použití PSČ jako kategorické funkce bez vyhlazování vede k nestabilitě u oblastí s malým množstvím dat.
- Ignorovat odlehlé hodnoty: Luxusní nemovitosti, aukce, likvidace portfolia narušují školení. Filtrujte nebo zvažte samostatně.
- Předvídat bez ověřování: AVM bez monitorovacího systému se po několika měsících na měnících se trzích stává nespolehlivým.
Závěry a další kroky
Vytvoření spolehlivého AVM vyžaduje více než jen dobrý algoritmus ML: vyžaduje Přísné inženýrství funkcí, algoritmické řízení zkreslení, interpretovatelnost prostřednictvím hodnot SHAP a nepřetržitého monitorovacího systému. Modely zesílení přechodu (XGBoost, LightGBM) zůstávají nejmodernější pro tabulková data nemovitostí, ale soubor s CMA a vrstva spolehlivosti jsou tím, co odlišuje produkci AVM z jednoduchého akademického cvičení.
Prozkoumejte další články ze série PropTech
- Článek 00 – Architektura platformy nemovitostí ve Scale
- Článek 02 – Softwarová architektura BIM: 3D modelování pro AEC
- Článek 03 – Smart Building IoT: Integrace senzorů a Edge Computing
- Článek 09 – Soukromí a dodržování předpisů v PropTech: Fair Housing and Bias







