AI Underwriting: Feature Engineering e Risk Scoring nell'Assicurazione Moderna
L'underwriting e il cuore pulsante di qualsiasi compagnia assicurativa: e il processo con cui si decide se accettare un rischio, a quale prezzo e con quali condizioni. Per decenni, questo processo ha dipeso dall'esperienza di underwriter umani che analizzavano documenti cartacei e applicavano regole attuariali codificate in tabelle. Il risultato? Decisioni in 3-5 giorni lavorativi, costi operativi elevati e variabilità soggettiva tra gli underwriter.
L'intelligenza artificiale sta riscrivendo queste regole in modo radicale. Secondo McKinsey, gli investimenti globali in soluzioni AI per l'assicurazione supereranno i 6 miliardi di dollari nel 2025, con BCG che stima che il 36% del valore totale dell'AI assicurativa si concentra proprio nella funzione di underwriting. I numeri operativi sono altrettanto impressionanti: il tempo medio di decisione underwriting e sceso da 3-5 giorni a 12,4 minuti per le polizze standard, mantenendo un tasso di accuratezza nella valutazione del rischio del 99,3%.
Ma come funziona concretamente un sistema di AI underwriting? Questa guida decostruisce l'intero stack tecnico: dalla raccolta e ingegnerizzazione delle feature, ai modelli di risk scoring, fino all'interpretabilita e alla gestione del bias - con esempi di codice reali pronti per la produzione.
Cosa Imparerai
- Architettura di un sistema AI underwriting end-to-end
- Feature engineering specifico per il dominio assicurativo
- Modelli ML per risk scoring: XGBoost, due stadi frequenza/severita
- Interpretabilita con SHAP per decisioni auditabili e compliance-ready
- Bias detection e fairness mitigation nel contesto normativo EU
- MLOps per modelli underwriting in produzione con MLflow
- Data drift monitoring con Population Stability Index (PSI)
Il Processo di Underwriting: da Legacy ad AI-Native
Prima di progettare un sistema AI, e essenziale capire il workflow tradizionale che stiamo automatizzando. Il processo di underwriting si articola in quattro fasi fondamentali:
- Raccolta informazioni: Il richiedente fornisce dati su se stesso e sul rischio (questionario, documenti, eventuale ispezione fisica del bene)
- Analisi del rischio: L'underwriter valuta la probabilità e la severita di eventuali sinistri futuri
- Pricing: Determinazione del premio in base al rischio valutato e agli obiettivi di combined ratio del portfolio
- Decisione: Accettazione, rifiuto o accettazione con condizioni (esclusioni, franchise, sovrappremio)
Un sistema AI-native non elimina queste fasi, ma le trasforma profondamente: la raccolta dati diventa automatica da fonti eterogenee (open data, telematica, credit bureau), l'analisi del rischio e eseguita da modelli ML in millisecondi, il pricing e dinamico e personalizzato per ciascun richiedente, e la decisione e automatizzata per i casi standard con supervisione umana per i casi complessi o borderline.
Quadro Normativo: AI Act EU e Underwriting
L'AI Act europeo (pienamente in vigore da agosto 2027) classifica i sistemi di scoring creditizio e assicurativo come AI ad alto rischio (Allegato III). Questo implica obblighi specifici: trasparenza delle decisioni automatizzate, diritto di revisione umana, documentazione tecnica dettagliata e valutazione della conformità ante-mercato. Il design dei sistemi di AI underwriting deve incorporare questi requisiti fin dall'architettura, non come retrofit successivo.
Feature Engineering per l'Underwriting Assicurativo
La qualità del feature engineering e il fattore che più differenzia un modello di underwriting eccellente da uno mediocre. A differenza di domini come la computer vision, dove le feature sono estratte automaticamente da layer convoluzionali, i dati tabulari assicurativi richiedono un'ingegneria manuale profonda basata sulla conoscenza attuariale del dominio.
Le feature per il ramo auto si raggruppano in cinque categorie principali:
- Feature demografiche: eta, stato civile, tipo di residenza
- Feature di guida: anni di patente, eta al primo conseguimento, storico sinistri e infrazioni
- Feature veicolo: marca, modello, anno, valore, potenza, anno di immatricolazione
- Feature geografiche: densita urbana, indice di criminalita dell'area, rischio meteo
- Feature economiche: credit score, tipo di contratto assicurativo richiesto
import pandas as pd
import numpy as np
from typing import Dict, Optional
from dataclasses import dataclass
from datetime import date
@dataclass
class PolicyApplicant:
"""Rappresenta i dati grezzi di un richiedente polizza auto."""
applicant_id: str
birth_date: date
license_date: date
zip_code: str
vehicle_make: str
vehicle_year: int
vehicle_value: float
annual_mileage: int
claims_3yr: int
violations_3yr: int
credit_score: Optional[int] = None
marital_status: str = "single"
housing_type: str = "tenant"
class AutoInsuranceFeatureEngineer:
"""
Feature engineering per underwriting auto.
Produce 40+ feature da dati grezzi del richiedente,
includendo feature derivate, interazioni e encoding
domain-specific.
"""
VEHICLE_MAKE_RISK: Dict[str, int] = {
"Ferrari": 5, "Lamborghini": 5, "Porsche": 4,
"BMW": 3, "Mercedes": 3, "Audi": 3,
"Toyota": 1, "Honda": 1, "Volkswagen": 2,
"Ford": 2, "Fiat": 2, "Renault": 2,
}
def __init__(self, reference_date: Optional[date] = None):
self.reference_date = reference_date or date.today()
def engineer_features(self, applicant: PolicyApplicant) -> Dict[str, float]:
features: Dict[str, float] = {}
features.update(self._demographic_features(applicant))
features.update(self._driving_experience_features(applicant))
features.update(self._vehicle_features(applicant))
features.update(self._claims_features(applicant))
features.update(self._geographic_features(applicant))
if applicant.credit_score is not None:
features.update(self._credit_features(applicant))
features.update(self._interaction_features(features))
return features
def _demographic_features(self, applicant: PolicyApplicant) -> Dict[str, float]:
age = (self.reference_date - applicant.birth_date).days / 365.25
return {
"age": age,
"age_squared": age ** 2,
"age_under_25": float(age < 25),
"age_over_70": float(age > 70),
"age_risk_young": max(0.0, (25 - age) / 25) if age < 25 else 0.0,
"age_risk_senior": max(0.0, (age - 70) / 20) if age > 70 else 0.0,
"is_married": float(applicant.marital_status == "married"),
"is_homeowner": float(applicant.housing_type == "owner"),
}
def _driving_experience_features(self, applicant: PolicyApplicant) -> Dict[str, float]:
years_licensed = (self.reference_date - applicant.license_date).days / 365.25
age = (self.reference_date - applicant.birth_date).days / 365.25
age_at_license = age - years_licensed
return {
"years_licensed": years_licensed,
"years_licensed_squared": years_licensed ** 2,
"age_at_first_license": age_at_license,
"late_license_ratio": max(0.0, (age_at_license - 18) / 10),
"is_new_driver": float(years_licensed < 2),
"is_experienced_driver": float(years_licensed > 10),
}
def _vehicle_features(self, applicant: PolicyApplicant) -> Dict[str, float]:
vehicle_age = self.reference_date.year - applicant.vehicle_year
make_risk = self.VEHICLE_MAKE_RISK.get(applicant.vehicle_make, 2)
return {
"vehicle_age": float(vehicle_age),
"vehicle_value": applicant.vehicle_value,
"vehicle_value_log": np.log1p(applicant.vehicle_value),
"vehicle_make_risk_score": float(make_risk),
"is_high_performance": float(make_risk >= 4),
"is_new_vehicle": float(vehicle_age <= 2),
"is_old_vehicle": float(vehicle_age > 10),
"annual_mileage": float(applicant.annual_mileage),
"annual_mileage_log": np.log1p(applicant.annual_mileage),
"high_mileage": float(applicant.annual_mileage > 20000),
}
def _claims_features(self, applicant: PolicyApplicant) -> Dict[str, float]:
claims = applicant.claims_3yr
violations = applicant.violations_3yr
return {
"claims_3yr": float(claims),
"violations_3yr": float(violations),
"has_any_claim": float(claims > 0),
"has_multiple_claims": float(claims > 1),
"has_violations": float(violations > 0),
# Score combinato ponderato: sinistri pesano 3x rispetto a infrazioni
"incident_score": claims * 3.0 + violations * 1.5,
"claims_x_violations": float(claims * violations),
}
def _geographic_features(self, applicant: PolicyApplicant) -> Dict[str, float]:
# In produzione: lookup su DB geografici (ISTAT, OpenStreetMap, criminalita)
zip_hash = hash(applicant.zip_code) % 100
urban_score = (zip_hash % 5) / 4.0
crime_index = (zip_hash % 3) / 2.0
weather_risk = (zip_hash % 4) / 3.0
return {
"urban_density_score": urban_score,
"area_crime_index": crime_index,
"area_weather_risk": weather_risk,
"composite_geo_risk": (urban_score + crime_index + weather_risk) / 3,
}
def _credit_features(self, applicant: PolicyApplicant) -> Dict[str, float]:
score = applicant.credit_score or 0
return {
"credit_score": float(score),
"credit_score_normalized": (score - 300) / (850 - 300),
"poor_credit": float(score < 580),
"fair_credit": float(580 <= score < 670),
"good_credit": float(670 <= score < 740),
"excellent_credit": float(score >= 740),
}
def _interaction_features(self, features: Dict[str, float]) -> Dict[str, float]:
return {
# Giovane + auto sportiva = rischio molto alto
"young_high_perf": (
features.get("age_risk_young", 0) *
features.get("is_high_performance", 0)
),
# Sinistri + area ad alto crimine amplificano il rischio
"claims_urban": (
features.get("claims_3yr", 0) *
features.get("urban_density_score", 0)
),
# Mileage alto + veicolo vecchio = rischio meccanico aumentato
"mileage_old_vehicle": (
features.get("annual_mileage_log", 0) *
features.get("is_old_vehicle", 0)
),
}
Modelli di Risk Scoring: Approcci e Trade-off
La scelta del modello di machine learning per il risk scoring deve bilanciare accuratezza predittiva, interpretabilita (fondamentale per compliance), velocità di inferenza e facilità di manutenzione. Ecco i principali approcci dell'industria assicurativa:
Confronto Modelli per Insurance Risk Scoring
| Modello | Accuratezza | Interpretabilita | Use Case Ideale |
|---|---|---|---|
| GLM (Poisson/Gamma) | Media | Molto Alta | Baseline attuariale, regulatory acceptance |
| Random Forest | Alta | Media | Feature importance, robustezza agli outlier |
| XGBoost / LightGBM | Molto Alta | Media | Produzione standard, SOTA su dati tabulari |
| Neural Network Tabulare | Alta | Bassa | Feature complesse con embedding categoriali |
L'approccio più consolidato nell'industria e il modello a due stadi: un modello di frequenza (probabilità di avere almeno un sinistro) e uno di severita (costo atteso del sinistro dato che si verifica). Il premio puro atteso e: Frequenza x Severita.
import xgboost as xgb
from sklearn.metrics import mean_absolute_error, mean_squared_error
import numpy as np
import pandas as pd
from typing import Dict, Optional
class TwoStageRiskScorer:
"""
Modello a due stadi per pricing assicurativo auto.
Stage 1: Frequency model (Poisson regression con XGBoost)
Target = numero sinistri per polizza
Stage 2: Severity model (Tweedie/Gamma con XGBoost)
Target = importo sinistro, addestrato solo su polizze con sinistri
Pure Premium = E[Frequency] * E[Severity | has_claim]
"""
FREQUENCY_PARAMS: Dict = {
"objective": "count:poisson",
"eval_metric": "poisson-nloglik",
"max_depth": 6,
"learning_rate": 0.05,
"n_estimators": 500,
"min_child_weight": 50, # stabilità attuariale: min sinistri per leaf
"subsample": 0.8,
"colsample_bytree": 0.8,
"reg_alpha": 0.1,
"reg_lambda": 1.0,
"tree_method": "hist",
"early_stopping_rounds": 50,
}
SEVERITY_PARAMS: Dict = {
"objective": "reg:tweedie",
"tweedie_variance_power": 1.5, # 1=Poisson, 2=Gamma
"eval_metric": "tweedie-nloglik@1.5",
"max_depth": 5,
"learning_rate": 0.05,
"n_estimators": 300,
"min_child_weight": 30,
"subsample": 0.8,
"colsample_bytree": 0.7,
"reg_alpha": 0.1,
"reg_lambda": 1.0,
"tree_method": "hist",
"early_stopping_rounds": 30,
}
def __init__(self) -> None:
self.frequency_model = xgb.XGBRegressor(**self.FREQUENCY_PARAMS)
self.severity_model = xgb.XGBRegressor(**self.SEVERITY_PARAMS)
self.feature_names: list = []
def fit(
self,
X: pd.DataFrame,
y_claims: pd.Series,
y_amounts: pd.Series,
exposure: pd.Series,
eval_fraction: float = 0.2,
) -> "TwoStageRiskScorer":
"""
Addestra entrambi i modelli.
IMPORTANTE: usa split temporale, non random shuffle.
I dati assicurativi sono autocorrelati nel tempo.
"""
self.feature_names = X.columns.tolist()
split_idx = int(len(X) * (1 - eval_fraction))
X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]
freq_train = y_claims.iloc[:split_idx]
freq_val = y_claims.iloc[split_idx:]
# Stage 1: Frequency
self.frequency_model.fit(
X_train, freq_train,
sample_weight=exposure.iloc[:split_idx],
eval_set=[(X_val, freq_val)],
verbose=50,
)
# Stage 2: Severity - solo su polizze con sinistri
has_claim = y_amounts > 0
X_sev = X[has_claim]
y_sev = y_amounts[has_claim]
sev_split = int(len(X_sev) * (1 - eval_fraction))
self.severity_model.fit(
X_sev.iloc[:sev_split], y_sev.iloc[:sev_split],
eval_set=[(X_sev.iloc[sev_split:], y_sev.iloc[sev_split:])],
verbose=30,
)
return self
def predict_pure_premium(
self, X: pd.DataFrame, exposure: float = 1.0
) -> np.ndarray:
"""Calcola il pure premium: E[Freq] * E[Severity]."""
freq = self.frequency_model.predict(X) * exposure
sev = self.severity_model.predict(X)
return freq * sev
def evaluate(self, X: pd.DataFrame, y_claims: pd.Series) -> Dict[str, float]:
pred = self.frequency_model.predict(X)
mae = mean_absolute_error(y_claims, pred)
rmse = float(np.sqrt(mean_squared_error(y_claims, pred)))
gini = self._gini_coefficient(y_claims.values, pred)
lift = self._lift_at_decile(y_claims.values, pred, 0.1)
return {
"mae": round(mae, 6),
"rmse": round(rmse, 6),
"gini_coefficient": round(gini, 4),
"lift_top_decile": round(lift, 4),
}
def _gini_coefficient(self, actual: np.ndarray, predicted: np.ndarray) -> float:
"""Gini coefficient: metrica attuariale standard per modelli di frequenza."""
idx = np.argsort(predicted)
cum = np.cumsum(actual[idx])
cum_norm = cum / cum[-1]
n = len(actual)
lorenz_area = float(np.sum(cum_norm)) / n
return 2 * (lorenz_area - 0.5)
def _lift_at_decile(
self, actual: np.ndarray, predicted: np.ndarray, decile: float
) -> float:
k = max(1, int(len(actual) * decile))
top_idx = np.argsort(predicted)[-k:]
base_rate = actual.mean()
if base_rate == 0:
return 0.0
return float(actual[top_idx].mean() / base_rate)
Interpretabilita con SHAP: Decisioni Auditabili
In un contesto regolamentato come quello assicurativo, un modello black-box non e sufficiente. La normativa richiede che le decisioni di underwriting siano spiegabili: per il cliente (diritto alla spiegazione GDPR), per gli underwriter (revisione dei casi borderline) e per i regulators (Solvency II Pillar 3, ORSA). SHAP (SHapley Additive exPlanations) e lo strumento di riferimento dell'industria per l'interpretabilita post-hoc di modelli ensemble.
import shap
import pandas as pd
import numpy as np
from typing import Dict, List, Tuple
class UnderwritingExplainer:
"""
Spiegazioni SHAP per decisioni underwriting.
Genera output a tre livelli: cliente, underwriter, compliance.
"""
FEATURE_LABELS: Dict[str, str] = {
"age": "eta del guidatore",
"years_licensed": "anni di patente",
"claims_3yr": "sinistri negli ultimi 3 anni",
"violations_3yr": "infrazioni negli ultimi 3 anni",
"vehicle_make_risk_score": "categoria rischio veicolo",
"vehicle_age": "anzianita del veicolo",
"vehicle_value": "valore del veicolo",
"annual_mileage": "chilometraggio annuo dichiarato",
"composite_geo_risk": "rischio della zona geografica",
"credit_score": "score creditizio",
"young_high_perf": "combinazione giovane + veicolo sportivo",
}
def __init__(self, model, feature_names: List[str]) -> None:
self.feature_names = feature_names
self.explainer = shap.TreeExplainer(model)
def explain(
self, X_row: pd.DataFrame, risk_score: float
) -> Dict:
"""Spiegazione completa per una singola valutazione."""
shap_values = self.explainer.shap_values(X_row)
impacts: List[Tuple[str, float]] = sorted(
zip(self.feature_names, shap_values[0]),
key=lambda x: abs(x[1]),
reverse=True
)
return {
"risk_score": round(risk_score, 2),
"decision": self._score_to_decision(risk_score),
"customer_message": self._customer_message(impacts, risk_score),
"top_risk_factors": [
{
"name": name,
"label": self.FEATURE_LABELS.get(name, name),
"direction": "aumenta rischio" if shap > 0 else "riduce rischio",
"magnitude": round(abs(shap), 4),
}
for name, shap in impacts[:5]
],
"audit_trail": {
"base_expected_value": float(self.explainer.expected_value),
"all_shap_values": {
n: round(float(s), 6)
for n, s in zip(self.feature_names, shap_values[0])
},
"input_features": X_row.to_dict(orient="records")[0],
},
}
def _customer_message(
self, impacts: List[Tuple[str, float]], score: float
) -> str:
high = [(n, v) for n, v in impacts if abs(v) > 0.1]
if not high:
return "Il tuo profilo rientra nella fascia di rischio standard."
positivi = [self.FEATURE_LABELS.get(n, n) for n, v in high[:3] if v < 0]
negativi = [self.FEATURE_LABELS.get(n, n) for n, v in high[:3] if v > 0]
parts = []
if negativi:
parts.append(f"Fattori che aumentano il profilo di rischio: {', '.join(negativi)}.")
if positivi:
parts.append(f"Fattori a tuo favore: {', '.join(positivi)}.")
return " ".join(parts)
def _score_to_decision(self, score: float) -> str:
if score < 30:
return "ACCEPT_PREFERRED"
elif score < 60:
return "ACCEPT_STANDARD"
elif score < 80:
return "ACCEPT_SUBSTANDARD"
return "DECLINE_OR_MANUAL_REVIEW"
Fairness e Bias Detection nel Contesto EU
L'uso di variabili proxy (codice postale, credit score) può introdurre discriminazioni indirette vietate dalla normativa. In Europa, la direttiva sull'uguaglianza di genere (confermata dalla sentenza Test-Achats della Corte di Giustizia UE del 2011) vieta l'uso del genere per il pricing assicurativo. L'AI Act aggiunge vincoli per i sistemi ad alto rischio classificati in Allegato III, richiedendo valutazioni di conformità obbligatorie prima del deployment.
import pandas as pd
import numpy as np
from sklearn.metrics import confusion_matrix
from typing import Dict, List
class FairnessAuditor:
"""
Auditor di fairness per modelli underwriting (EU-compliant).
Metriche implementate:
- Disparate Impact (regola 80%)
- Demographic Parity Gap
- Equal Opportunity (TPR parity)
- Calibration by group
"""
DISPARATE_IMPACT_THRESHOLD = 0.8 # EEOC 80% rule
MAX_DP_GAP = 0.1 # linee guida EIOPA
def __init__(
self,
predictions: np.ndarray,
true_labels: np.ndarray,
sensitive_df: pd.DataFrame,
) -> None:
self.predictions = predictions
self.true_labels = true_labels
self.sensitive_df = sensitive_df
def full_audit(self) -> Dict:
results: Dict = {}
for attr in self.sensitive_df.columns:
groups = self.sensitive_df[attr].unique()
attr_results: Dict = {}
for group in groups:
mask = self.sensitive_df[attr] == group
g_pred = self.predictions[mask]
g_true = self.true_labels[mask]
attr_results[str(group)] = {
"count": int(mask.sum()),
"acceptance_rate": float((g_pred < 0.6).mean()),
"avg_score": round(float(g_pred.mean()), 4),
"tpr": self._tpr(g_true, g_pred),
}
di = self._disparate_impact(attr_results)
dp = self._dp_gap(attr_results)
attr_results["_metrics"] = {
"disparate_impact": round(di, 4),
"demographic_parity_gap": round(dp, 4),
"passes_di_rule": di >= self.DISPARATE_IMPACT_THRESHOLD,
"passes_dp_rule": dp <= self.MAX_DP_GAP,
"overall_fair": di >= self.DISPARATE_IMPACT_THRESHOLD and dp <= self.MAX_DP_GAP,
}
results[attr] = attr_results
return results
def _tpr(self, labels: np.ndarray, preds: np.ndarray, thr: float = 0.5) -> float:
if len(labels) < 10:
return float("nan")
binary = (preds >= thr).astype(int)
try:
tn, fp, fn, tp = confusion_matrix(labels, binary, labels=[0, 1]).ravel()
return round(tp / (tp + fn), 4) if (tp + fn) > 0 else 0.0
except ValueError:
return float("nan")
def _disparate_impact(self, groups: Dict) -> float:
rates = [v["acceptance_rate"] for k, v in groups.items()
if not k.startswith("_") and isinstance(v, dict)]
if not rates or max(rates) == 0:
return 1.0
return min(rates) / max(rates)
def _dp_gap(self, groups: Dict) -> float:
rates = [v["acceptance_rate"] for k, v in groups.items()
if not k.startswith("_") and isinstance(v, dict)]
return (max(rates) - min(rates)) if rates else 0.0
MLOps e Monitoring in Produzione
I modelli di underwriting sono soggetti a concept drift frequente: il profilo dei richiedenti cambia (nuovi modelli di veicoli elettrici, variazioni demografiche), i costi di riparazione subiscono inflazione, eventi climatici estremi alterano i pattern di sinistro. Un sistema di monitoring continuo con Population Stability Index (PSI) e essenziale per identificare quando il modello deve essere retrainato.
from scipy import stats
import numpy as np
import pandas as pd
from typing import Dict, List
from datetime import datetime
class DriftMonitor:
"""
Monitora data drift per modelli underwriting.
Usa PSI (Population Stability Index) come metrica primaria.
PSI interpretation:
- PSI < 0.1: Nessun cambiamento significativo
- PSI 0.1-0.25: Cambiamento moderato, monitorare
- PSI > 0.25: Cambiamento significativo, retraining consigliato
"""
def __init__(self, reference_df: pd.DataFrame, features: List[str]) -> None:
self.reference_df = reference_df
self.features = features
def check_drift(self, current_df: pd.DataFrame) -> Dict:
feature_results: Dict = {}
critical_features = []
for feat in self.features:
if feat not in current_df.columns:
continue
psi = self._psi(self.reference_df[feat], current_df[feat])
ks_stat, ks_p = stats.ks_2samp(
self.reference_df[feat].dropna(),
current_df[feat].dropna()
)
status = "ok" if psi < 0.1 else ("warning" if psi < 0.25 else "critical")
feature_results[feat] = {
"psi": round(psi, 4),
"ks_statistic": round(ks_stat, 4),
"ks_pvalue": round(ks_p, 4),
"status": status,
}
if status == "critical":
critical_features.append(feat)
avg_psi = float(np.mean([v["psi"] for v in feature_results.values()]))
return {
"checked_at": datetime.now().isoformat(),
"overall_psi": round(avg_psi, 4),
"retraining_recommended": avg_psi > 0.1,
"critical_features": critical_features,
"feature_details": feature_results,
}
def _psi(self, ref: pd.Series, cur: pd.Series, bins: int = 10) -> float:
ref_clean = ref.dropna().values
cur_clean = cur.dropna().values
edges = np.percentile(ref_clean, np.linspace(0, 100, bins + 1))
edges = np.unique(edges)
ref_counts, _ = np.histogram(ref_clean, bins=edges)
cur_counts, _ = np.histogram(cur_clean, bins=edges)
ref_pct = (ref_counts + 1e-10) / len(ref_clean)
cur_pct = (cur_counts + 1e-10) / len(cur_clean)
return float(np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct)))
Best Practices e Anti-pattern
Best Practices per AI Underwriting
- Architettura due stadi (frequenza/severita): e lo standard attuariale e produce pricing più accurato rispetto a un modello singolo sull'importo sinistro
- Split temporale obbligatorio: i dati assicurativi sono autocorrelati nel tempo; mai usare random shuffle per train/test split
- Esposizione come offset: usare sempre la durata della polizza (exposure in anni) come offset nel modello Poisson per normalizzare il claim count
- Mantieni un GLM baseline: i modelli lineari generalizzati sono più facilmente validabili dai regulators e forniscono benchmark per valutare il valore aggiunto del ML
- Shadow mode prima del go-live: esegui il modello in parallelo all'underwriting umano per 30-90 giorni, confrontando le decisioni prima di automatizzare
- Monitora PSI settimanalmente: il drift nel ramo auto e frequente a causa di nuovi modelli di veicoli, inflazione costi riparazione, variazioni normative
Anti-pattern da Evitare
- Feature leakage: non usare mai variabili disponibili solo post-sinistro (claim amount, reserve) come feature di training del modello di frequenza
- Ottimizzare solo AUC: nel ramo assicurativo le metriche rilevanti sono Gini coefficient, Combined Ratio e Lift nel top decile di rischio
- Modelli con 500+ feature: impossibili da validare attuarialmente e da giustificare al regulator; preferire feature selection rigorosa (max 40-60 feature)
- Ignorare la concentrazione del portfolio: un modello che accetta solo profili a bassissimo rischio crea anti-selezione e portfolio non bilanciato
- Discriminazione proxy: variabili come il codice postale possono essere proxy di etnia; verificare sempre il disparate impact prima del deploy
Conclusioni e Next Steps
L'AI underwriting non sostituisce l'underwriter umano, ma lo amplifica: le decisioni per polizze standard (80-90% del volume) possono essere completamente automatizzate con accuratezza superiore alla media umana, liberando gli specialist per i casi complessi dove l'esperienza di dominio e insostituibile.
Le chiavi per un sistema di successo sono: feature engineering profondo basato su conoscenza attuariale, architettura a due stadi frequenza/severita, interpretabilita SHAP per compliance, audit di fairness obbligatorio e monitoring continuo con PSI per la gestione del drift.
Il prossimo articolo della serie esplora l'automazione dei sinistri con Computer Vision e NLP: dalla FNOL digitale all'assessment automatico dei danni fotografici, fino al settlement accelerato end-to-end.
Serie InsurTech Engineering
- 01 - Dominio Assicurativo per Developer: Prodotti, Attori e Data Model
- 02 - Policy Management Cloud-Native: Architettura API-First
- 03 - Pipeline Telematica: Processing Dati UBI su Scala
- 04 - AI Underwriting: Feature Engineering e Risk Scoring (questo articolo)
- 05 - Automazione Sinistri: Computer Vision e NLP
- 06 - Fraud Detection: Graph Analytics e Behavioral Signal
- 07 - Standard ACORD e API Integration Assicurative
- 08 - Compliance Engineering: Solvency II e IFRS 17

Commenti
Caricamento commenti...
Accedi per commentare