KI-Underwriting: Feature Engineering und Risikobewertung in modernen Versicherungen
Das Underwriting ist das schlagende Herz eines jeden Versicherungsunternehmens: und der Prozess, mit dem es durchgeführt wird entscheidet, ob, zu welchem Preis und zu welchen Bedingungen ein Risiko eingegangen wird. Seit Jahrzehnten läuft dieser Prozess Es hing von der Fachkenntnis menschlicher Underwriter ab, die Papierdokumente analysierten und beantragten in Tabellen kodifizierte versicherungsmathematische Regeln. Das Ergebnis? Entscheidungen in 3-5 Werktagen, Kosten hohe Betriebskosten und subjektive Schwankungen zwischen den Underwritern.
Künstliche Intelligenz schreibt diese Regeln radikal neu. Laut McKinsey ist das Die weltweiten Investitionen in KI-Lösungen für Versicherungen werden das übersteigen 6 Milliarden Dollar im Jahr 2025, wobei BCG schätzt, dass die 36 % des Gesamtwerts der Versicherungs-KI konzentriert sich gerade auf die Underwriting-Funktion. Ebenso beeindruckend sind die Betriebszahlen: Die durchschnittliche Entscheidungszeit für das Underwriting ist von 3–5 Tagen auf gesunken 12,4 Minuten für sie Standardrichtlinien, wobei eine Genauigkeitsrate bei der Risikobewertung von 99,3 % aufrechterhalten wird.
Doch wie funktioniert eigentlich ein KI-Underwriting-System? Dieser Leitfaden dekonstruiert das Ganze Technischer Stack: von der Sammlung und Entwicklung von Funktionen über Risikobewertungsmodelle bis hin zu bis hin zu Interpretierbarkeit und Bias-Management – mit produktionsreifen echten Codebeispielen.
Was Sie lernen werden
- Architektur eines End-to-End-KI-Underwriting-Systems
- Feature-Engineering speziell für den Versicherungsbereich
- ML-Modelle für die Risikobewertung: XGBoost, zwei Häufigkeits-/Schweregradstufen
- Interpretierbarkeit mit SHAP für überprüfbare und konformitätsgerechte Entscheidungen
- Verzerrungserkennung und Fairnessminderung im EU-Regulierungskontext
- MLOps zum Underwriting von Modellen in der Produktion mit MLflow
- Datendriftüberwachung mit Population Stability Index (PSI)
Der Underwriting-Prozess: vom Legacy zum AI-Native
Bevor Sie ein KI-System entwerfen, ist es wichtig, den traditionellen Arbeitsablauf zu verstehen, mit dem wir es zu tun haben automatisieren. Der Underwriting-Prozess ist in vier grundlegende Phasen unterteilt:
- Informationssammlung: Der Antragsteller stellt Daten zu seiner Person und dem Risiko zur Verfügung (Fragebogen, Dokumente, eventuelle physische Besichtigung des Vermögenswerts).
- Risikoanalyse: Der Underwriter beurteilt die Wahrscheinlichkeit und Schwere künftiger Schadensfälle
- Pricing: Determinazione del premio in base al rischio valutato e agli obiettivi di combined ratio del portfolio
- Entscheidung: Annahme, Ablehnung oder Annahme mit Auflagen (Ausschlüsse, Franchise, Prämie)
Ein KI-natives System eliminiert diese Phasen nicht, sondern verändert sie grundlegend: die Datenerfassung Die Risikoanalyse erfolgt automatisch aus heterogenen Quellen (offene Daten, Telematik, Kreditauskunftei). und von ML-Modellen in Millisekunden durchgeführt, ist die Preisgestaltung dynamisch und für jeden individuell Antragsteller, und die Entscheidung erfolgt automatisiert für Standardfälle unter menschlicher Aufsicht für i komplexe oder grenzwertige Fälle.
Quadro Normativo: AI Act EU e Underwriting
Das europäische KI-Gesetz (ab August 2027 vollständig in Kraft) klassifiziert Systeme punkten Kredit und Versicherung als KI mit hohem Risiko (Anhang III). Dies impliziert spezifische Verpflichtungen: Transparenz automatisierter Entscheidungen, Recht auf menschliche Überprüfung, detaillierte technische Dokumentation und Konformitätsbewertung vor dem Inverkehrbringen. Das Design von KI-Underwriting-Systeme müssen diese Anforderungen bereits in der Architektur berücksichtigen, nicht wie nachträgliche Nachrüstung.
Feature Engineering für das Versicherungs-Underwriting
Die Qualität des Feature-Engineerings ist der Faktor, der ein Underwriting-Modell am meisten auszeichnet ausgezeichnet von mittelmäßig. Im Gegensatz zu Bereichen wie Computer Vision, wo die Funktionen vorhanden sind Automatisch aus Faltungsschichten extrahiert, erfordert die Versicherung tabellarische Daten tiefgreifendes manuelles Engineering basierend auf versicherungsmathematischem Fachwissen.
Die Features für die Automobilbranche sind in fünf Hauptkategorien unterteilt:
- Demografische Merkmale: Alter, Familienstand, Art des Wohnsitzes
- Fahreigenschaften: Jahre des Besitzes des Führerscheins, Alter beim ersten Erwerb des Führerscheins, Vorgeschichte von Unfällen und Verstößen
- Fahrzeugmerkmale: Marke, Modell, Jahr, Wert, Leistung, Jahr der Zulassung
- Geografische Merkmale: städtische Dichte, Kriminalitätsindex der Region, Wetterrisiko
- Wirtschaftsmerkmale: Kreditwürdigkeit, Art des gewünschten Versicherungsvertrags
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)
),
}
Risikobewertungsmodelle: Ansätze und Kompromisse
Bei der Wahl des maschinellen Lernmodells für die Risikobewertung muss die Genauigkeit im Gleichgewicht sein Vorhersagekraft, Interpretierbarkeit (grundlegend für Compliance), Schlussfolgerungsgeschwindigkeit und Benutzerfreundlichkeit der Wartung. Hier sind die wichtigsten Ansätze der Versicherungsbranche:
Vergleichsmodelle für die Bewertung von Versicherungsrisiken
| Modello | Accuratezza | Interpretabilita | Use Case Ideale |
|---|---|---|---|
| GLM (Poisson/Gamma) | Media | Sehr hoch | Baseline attuariale, regulatory acceptance |
| Random Forest | Alta | Media | Feature importance, robustezza agli outlier |
| XGBoost / LightGBM | Sehr hoch | Media | Standardproduktion, SOTA auf tabellarischen Daten |
| Neural Network Tabulare | Alta | Bassa | Komplexe Features mit kategorialen Einbettungen |
Der etablierteste Ansatz in der Branche ist der zweistufiges Modell: ein Modell der Häufigkeit (Wahrscheinlichkeit, mindestens einen Unfall zu haben) und der Schwere (erwartete Kosten des Unfalls). vorausgesetzt, dass es auftritt). Die erwartete reine Prämie beträgt: Häufigkeit x Schweregrad.
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, # stabilita 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)
Interpretierbarkeit mit SHAP: Überprüfbare Entscheidungen
In einem regulierten Kontext wie dem der Versicherung reicht ein Black-Box-Modell nicht aus. Der Gesetzgeber verlangt, dass Underwriting-Entscheidungen erklärbar sind: für den Kunden (rechts). zur DSGVO-Erklärung), für Underwriter (Prüfung von Grenzfällen) und für Aufsichtsbehörden (Solvency II Säule 3, ORSA). SHAP (SHapley Additive exPlanations) ist das Referenztool Industrie für die Post-hoc-Interpretierbarkeit von Ensemblemodellen.
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 und Bias-Erkennung im EU-Kontext
Die Verwendung von Proxy-Variablen (Postleitzahl, Kreditwürdigkeit) kann zu indirekter Diskriminierung führen gesetzlich verboten. In Europa gilt die Gleichstellungsrichtlinie (bestätigt durch Das Test-Achats-Urteil des EU-Gerichtshofs von 2011 verbietet die Verwendung des Geschlechts bei der Preisgestaltung Versicherung. Das KI-Gesetz fügt Einschränkungen für in Anhang III eingestufte Hochrisikosysteme hinzu. Vor der Bereitstellung sind obligatorische Compliance-Bewertungen erforderlich.
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
Underwriting-Modelle unterliegen Konzeptdrift häufig: das Profil der Bewerber Veränderungen (neue Modelle von Elektrofahrzeugen, demografische Veränderungen), Kosten der Reparaturen leiden unter der Inflation, extreme Klimaereignisse verändern die Schadensmuster. Ein kontinuierliches Überwachungssystem mit Bevölkerungsstabilitätsindex (PSI) e Dies ist wichtig, um zu erkennen, wann das Modell erneut abgeschleppt werden muss.
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 für KI-Underwriting
- Zweistufige Architektur (Häufigkeit/Schweregrad): und dem versicherungsmathematischen Standard und führt zu einer genaueren Preisgestaltung als ein einzelnes Modell zur Schadenshöhe
- Obligatorischer Zeitsplit: Versicherungsdaten werden im Laufe der Zeit automatisch korreliert; Verwenden Sie niemals zufälliges Mischen für die Zug-/Testaufteilung
- Belichtung als Offset: Verwenden Sie im Poisson-Modell immer die Vertragslaufzeit (Engagement in Jahren) als Offset, um die Schadenszahl zu normalisieren
- Pflegen Sie ein Basis-GLM: Verallgemeinerte lineare Modelle können von den Regulierungsbehörden leichter validiert werden und bieten Benchmarks zur Bewertung des Mehrwerts von ML
- Schattenmodus vor dem Go-Live: Führen Sie das Modell 30–90 Tage lang parallel zum menschlichen Underwriting aus und vergleichen Sie Entscheidungen vor der Automatisierung
- Überwachen Sie den PSI wöchentlich: Aufgrund neuer Fahrzeugmodelle, steigender Reparaturkosten und regulatorischer Änderungen kommt es im Automobilsektor häufig zu Driften
Zu vermeidende Anti-Muster
- Funktionsleck: Verwenden Sie niemals Variablen, die erst nach dem Schadensfall verfügbar sind (Schadensbetrag, Reserve), als Trainingsmerkmale des Frequenzmodells
- Nur AUC optimieren: Im Versicherungssektor sind die relevanten Kennzahlen Gini-Koeffizient, Combined Ratio und Lift im obersten Risikodezil
- Modelle mit über 500 Funktionen: versicherungsmathematische Validierung und Begründung gegenüber der Aufsichtsbehörde unmöglich; Bevorzugen Sie eine strenge Funktionsauswahl (maximal 40–60 Funktionen).
- Portfoliokonzentration ignorieren: Ein Modell, das nur sehr niedrige Risikoprofile akzeptiert, führt zu Antiselektion und einem unausgewogenen Portfolio
- Proxy-Diskriminierung: Variablen wie die Postleitzahl können Stellvertreter für die ethnische Zugehörigkeit sein; Überprüfen Sie vor der Bereitstellung immer die unterschiedlichen Auswirkungen
Fazit und nächste Schritte
KI-Underwriting ersetzt nicht den menschlichen Underwriter, sondern verstärkt ihn: Entscheidungen für Standardrichtlinien (80–90 % des Volumens) können vollständig und präzise automatisiert werden höher als der menschliche Durchschnitt, wodurch Spezialisten für komplexe Fälle frei werden, bei denen die Erfahrung von Herrschaft und unersetzlich.
Der Schlüssel zu einem erfolgreichen System ist: wissensbasiertes Deep Feature Engineering versicherungsmathematische, zweistufige Häufigkeits-/Schweregradarchitektur, SHAP-Interpretierbarkeit für Compliance, obligatorisches Fairness-Audit und kontinuierliches Monitoring mit PSI für Driftmanagement.
Der nächste Artikel der Reihe befasst sich mit demSchadensautomatisierung mit Computer Vision und NLP: von der digitalen FNOL über die automatische fotografische Schadensbeurteilung bis hin zu beschleunigte End-to-End-Abwicklung.
Serie InsurTech Engineering
- 01 - Dominio Assicurativo per Developer: Prodotti, Attori e Data Model
- 02 - Policy Management Cloud-Native: Architettura API-First
- 03 – Telematik-Pipeline: UBI-Datenverarbeitung im großen Maßstab
- 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







