機械学習による不動産評価: Zestimate のようなシステムの構築
2006 年、Zillow は初の自動評価システムである Zestimate を発売しました。 全国規模の不動産。現在、米国では 1 億 3,500 万以上の住宅が評価されており、 Zestimate は、PropTech セクター全体の参照ベンチマークとなっています。しかし、どうやって システムが実際に機能する 自動評価モデル (AVM)?どれ 機械学習アルゴリズムは最良の結果を生み出しますか?そして何よりも、それがどのように構築されているか 信頼性があり、解釈可能で、差別禁止規制に準拠したシステムですか?
この記事では、データ収集とクリーニングから機能まで、完全な AVM を構築します。 勾配ブースティングモデルのトレーニングから実稼働展開までの高度なエンジニアリング REST API を使用し、SHAP 解釈可能性テクニックとモニタリングを通過 時間の経過とともにモデルがドリフトする。
何を学ぶか
- 不動産の自動評価モデル (AVM) のエンドツーエンドのアーキテクチャ
- 高度な特徴エンジニアリング: 物理的属性、場所、比較対象、マクロ経済
- 比較した ML モデル: XGBoost、LightGBM、CatBoost、ランダム フォレスト、ニューラル ネットワーク
- 個々の評価を説明するSHAP値による解釈性
- ヘドニック価格設定モデルと比較アプローチ (CMA)
- FastAPI を使用した導入と Evidently AI を使用したモデル ドリフト監視
- アルゴリズムのバイアスの管理と公正な住宅規制の遵守
2025年のAVM市場
世界の自動評価モデル市場は 2024 年に 23% 成長すると予想されており、 住宅ローンプロセスのデジタル化とハイブリッドモデルの採用によって推進されています。 AI と人間の評価を組み合わせます。主要なプレーヤーには、Zillow (Zestimate)、CoreLogic、 Black Knight、HouseCanary、Hometrack (英国)。
最新の AVM の平均精度は約 1 中央値絶対パーセント誤差 (MdAPE) 3% ~ 6% 高密度市場の住宅用不動産向け データの。これは、300,000 ユーロのアパートの場合、一般的な誤差は次のとおりであることを意味します。 9,000 ~ 18,000 ユーロの間で、多くの状況において、結果は 標準プロパティの人間の評価者。
AVMシステムアーキテクチャ
エンタープライズ AVM システムは 5 つの主要なレイヤーで構成され、それぞれが役割を担っています。 明確に定義された特定のレイテンシー要件。
# 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 │
└─────────────────────────────────────────────────────┘
データセットと特徴エンジニアリング
効果的な AVM の核心は特徴エンジニアリングです。 Zillowの研究者は、 ML チームはさまざまなアルゴリズムの実験に数週間を無駄にすることがよくあると文書化されています。 本当の競争上の優位性は、機能ではなく機能の品質にある場合 モデルの複雑さ。
機能は 4 つのマクロ カテゴリに分類されます。
| カテゴリ | 主な特長 | データソース | AVM への影響 |
|---|---|---|---|
| 物理的属性 | 表面積、部屋、バスルーム、建設年、床、保存状態 | MLS 土地登記所 | 高 (35-45%) |
| 位置 | GPS 座標、近所、近くの学校、交通機関、洪水の危険性 | オープンストリートマップ、ISTAT、PCN | 非常に高い (40-50%) |
| 市場 | 比較可能な価格、エリア傾向、販売日数、吸収率 | MLS、公証人取引 | 中 (10-20%) |
| マクロ経済 | 住宅ローン金利、インフレ、建設指数、国内総生産(GDP) | ECB、ISTAT、イタリア銀行 | 低~中 (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
評価用の機械学習モデル
Zillow、CoreLogic、学術研究者が公開したベンチマークは次のことを示しています 勾配ブースティング モデル (XGBoost、LightGBM、CatBoost) が一貫して優勢であること 表形式の不動産データの精度ランキング。ニューラルネットワークはお金を稼ぐ 物件の画像がフィーチャとして利用可能な場合に土地を取得します。
| モデル | MdAPE の典型的な | トレーニング速度 | 解釈可能性 | ベストフォー |
|---|---|---|---|---|
| XGブースト | 3.8~5.2% | 中くらい | 高 (SHAP) | バランスの取れたデータセット、重要な機能 |
| ライトGBM | 3.5~4.9% | 非常に速い | 高 (SHAP) | 大規模なデータセット、カテゴリ特徴 |
| キャットブースト | 3.6~5.0% | 中くらい | 高 (SHAP) | エンコードなしのカテゴリ特徴量 |
| ランダムフォレスト | 4.5~6.5% | 遅い | 平均 | 堅牢なベースライン、異常値耐性 |
| ニューラル ネットワーク (表形式) | 4.0~5.5% | 非常に遅い | 低い | 複雑な機能、画像の統合 |
| アンサンブル(スタッキング) | 3.2~4.5% | - | 平均 | 生産、最高の精度 |
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),
}
ヘドニック価格モデルと比較可能な市場分析
純粋な ML モデルに加えて、プロダクション AVM には、次の 2 つの相補的なアプローチが統合されています。 の ヘドニック価格モデル (HPM) そして 比較可能な市場分析 (CMA)。 HPM は、不動産の価格をそれぞれの暗黙の価値の合計として扱います。 特性(各平方メートルは X の価値があり、追加のバスルームはそれぞれ Y の価値があるなど)。 ただし、CMA は近くで最近販売された同様の不動産を探し、調整します。 違いの価格。
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),
},
}
FastAPI を使用した評価 API
モデルの提供は、FastAPI を使用した REST API 経由で行われます。 1 分あたり数千のリクエストを 200 ミリ秒未満のレイテンシーで管理します。
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")
Eviously AI を使用したモデルドリフト監視
不動産評価モデルは特にドリフトの影響を受けやすい 時間の経過とともに、価格は変化し、近隣地域は変化し、新しいインフラストラクチャが追加されます。 構築されています。実稼働環境の AVM システムは常に監視する必要があります 予測の品質とデータ分布の変化。
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(),
}
PropTech におけるアルゴリズムのバイアスの管理
2024 年 5 月に、HUD は、 公正住宅法は自動評価システムにも適用される。 領域内で体系的に低い評価を生成する AVM モデル 少数民族の蔓延は法律違反となる可能性がある。 たとえ差別的意図がなかったとしても。
必須の措置:
- 人種、民族、国籍、宗教を特徴から積極的に除外する
- 郵便番号ごとに評価を追跡し、人口構成と比較します
- CI/CD パイプラインの一部として公平性テスト (異種の影響、均等化されたオッズ) を実装する
- 実行されたすべての評価の完全な監査ログを維持する
- ユーザーのリクエストに応じて、SHAP 値を使用して個々の推定値を説明します
ベストプラクティスとアンチパターン
実稼働環境における AVM のベスト プラクティス
- ターゲットのログ変換: 不動産価格は対数正規分布に従います。 log1p() をターゲットに適用すると、外れ値の影響が軽減され、トレーニングが改善されます。
- 厳密な時間間隔: 特徴量エンジニアリングでは、予測日よりも将来のデータを決して使用しないでください (漏洩)。常に使用する
before_date比較クエリで。 - 必須の信頼区間: 信頼範囲のない点推定値を決して返さないでください。ユーザーはモデルがどれほど不確実であるかを知る必要があります。
- CMA でのフォールバック: ML 信頼度が低い (<0.5) 場合は、CMA を一次推定値として使用するか、信頼度に比例した重みを付けて 2 つを組み合わせます。
- 毎月の再トレーニング: AVM モデルは、不安定な市場ではすぐに劣化します。 24 か月のローリング データに対して毎月の再トレーニングをスケジュールします。
- モデルのバージョン管理: 各予測は、それを生成したモデルの特定のバージョンまで追跡可能である必要があります (監査準拠)。
重大なアンチパターン
- 機能の漏洩: 評価日以降のトランザクションのデータを含めると、トレーニングでは人為的に正確でも実稼働では役に立たないモデルが生成されます。
- 郵便番号のオーバーフィット: 平滑化を行わずに郵便番号をカテゴリ特徴量として使用すると、データが少ない地域では不安定になります。
- 外れ値を無視する: 高級不動産、オークション、ポートフォリオの処分はトレーニングを歪めます。濾過するか、別々に計量します。
- 検証せずに予測する: 監視システムのない AVM は、変動する市場で数か月経過すると信頼性が低くなります。
結論と次のステップ
信頼性の高い AVM を構築するには、優れた ML アルゴリズム以上のものが必要です。 厳密な特徴量エンジニアリング、アルゴリズムによるバイアス管理、解釈可能性 SHAP 値と継続的な監視システムを通じて。勾配ブースティングモデル (XGBoost、LightGBM) は不動産表形式データとしては依然として最先端ですが、 CMA と信頼層を備えたアンサンブルが本番 AVM の特徴です 簡単な学術演習から。
PropTech シリーズの他の記事を探す
- 記事 00 - Scala の不動産プラットフォームのアーキテクチャ
- 記事 02 - BIM ソフトウェア アーキテクチャ: AEC の 3D モデリング
- 記事 03 - スマート ビルディング IoT: センサー統合とエッジ コンピューティング
- 第 09 条 - PropTech におけるプライバシーとコンプライアンス: 公正な住宅と偏見







