AI Underwriting: Feature Engineering en risicoscores in moderne verzekeringen
Accepteren is het kloppende hart van elke verzekeringsmaatschappij: en het proces waardoor dit gebeurt beslist of hij een risico aanvaardt, tegen welke prijs en onder welke voorwaarden. Dit proces is al tientallen jaren gaande het hing af van de expertise van menselijke verzekeraars die papieren documenten analyseerden en aanvraagden actuariële regels vastgelegd in tabellen. Het resultaat? Beslissing binnen 3-5 werkdagen, kosten hoge bedrijfskosten en subjectieve variabiliteit onder verzekeraars.
Kunstmatige intelligentie herschrijft deze regels radicaal. Volgens McKinsey is de mondiale investeringen in AI-oplossingen voor verzekeringen zullen de verwachtingen overtreffen 6 miljard dollar in 2025, waarbij BCG schat dat de 36% van de totale waarde van de verzekering AI is juist geconcentreerd op de acceptatiefunctie. De operationele cijfers zijn even indrukwekkend: de gemiddelde acceptatietijd is gedaald van 3 tot 5 dagen 12,4 minuten voor hen standaardbeleid, waarbij de nauwkeurigheid van de risicobeoordeling op 99,3% wordt gehandhaafd.
Maar hoe werkt een AI-acceptatiesysteem eigenlijk? Deze gids deconstrueert het geheel technische stapel: van het verzamelen en ontwikkelen van functies tot risicoscoremodellen, tot tot interpreteerbaarheid en biasmanagement – met productieklare echte codevoorbeelden.
Wat je gaat leren
- Architectuur van een end-to-end AI-acceptatiesysteem
- Functie-engineering specifiek voor het verzekeringsdomein
- ML-modellen voor risicoscores: XGBoost, twee frequentie-/ernststadia
- Interpreteerbaarheid met SHAP voor controleerbare en compliance-ready beslissingen
- Detectie van bias en beperking van eerlijkheid in de EU-regelgevingscontext
- MLOps voor acceptatiemodellen in productie met MLflow
- Monitoring van datadrift met Population Stability Index (PSI)
Het acceptatieproces: van Legacy naar AI-Native
Voordat we een AI-systeem ontwerpen, is het essentieel om de traditionele workflow waarmee we te maken hebben te begrijpen automatiseren. Het acceptatieproces is verdeeld in vier fundamentele fasen:
- Informatieverzameling: De aanvrager verstrekt gegevens over zichzelf en het risico (vragenlijst, documenten, eventuele fysieke inspectie van het asset)
- Risicoanalyse: De verzekeraar evalueert de waarschijnlijkheid en de ernst van eventuele toekomstige claims
- Prijzen: Bepaling van de premie op basis van het ingeschatte risico en de combined ratio-doelstellingen van de portefeuille
- Beslissing: Aanvaarding, afwijzing of aanvaarding met voorwaarden (uitsluitingen, franchise, premie)
Een AI-native systeem elimineert deze fasen niet, maar transformeert ze diepgaand: dataverzameling risicoanalyse wordt automatisch vanuit heterogene bronnen (open data, telematica, kredietbureau). en uitgevoerd door ML-modellen in milliseconden, de prijs is voor elk dynamisch en gepersonaliseerd aanvrager, en de beslissing wordt geautomatiseerd voor standaardzaken met menselijk toezicht voor i complexe of grensgevallen.
Regelgevingskader: AI Act EU en acceptatie
De Europese AI-wet (volledig van kracht vanaf augustus 2027) classificeert systemen scoren krediet en verzekering als AI met hoog risico (bijlage III). Dit impliceert specifieke verplichtingen: transparantie van geautomatiseerde besluiten, recht op menselijke toetsing, gedetailleerde technische documentatie en conformiteitsbeoordeling vóór het op de markt brengen. Het ontwerp van AI-acceptatiesystemen moeten deze vereisten rechtstreeks vanuit de architectuur integreren, niet hoe daaropvolgende renovatie.
Feature Engineering voor verzekeringsacceptatie
De kwaliteit van feature engineering is de factor die een acceptatiemodel het meest onderscheidt uitstekend van middelmatig. In tegenstelling tot domeinen zoals computervisie, waar de functies aanwezig zijn automatisch geëxtraheerd uit convolutionele lagen, waarvoor verzekeringstabelgegevens nodig zijn diepgaande handmatige engineering gebaseerd op actuariële domeinkennis.
De kenmerken voor de automobielsector zijn gegroepeerd in vijf hoofdcategorieën:
- Demografische kenmerken: leeftijd, burgerlijke staat, type woning
- Rijeigenschappen: aantal jaren rijbewijs, leeftijd waarop het voor het eerst werd behaald, geschiedenis van ongevallen en overtredingen
- Voertuigeigenschappen: merk, model, jaar, waarde, vermogen, jaar van registratie
- Geografische kenmerken: stedelijke dichtheid, misdaadindex van het gebied, weersrisico
- Economische kenmerken: kredietscore, type verzekeringscontract vereist
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)
),
}
Risicoscoremodellen: benaderingen en afwegingen
Bij de keuze van het machine learning-model voor risicoscores moet de nauwkeurigheid in evenwicht zijn voorspellend, interpreteerbaarheid (fundamenteel voor naleving), snelheid van gevolgtrekking en gemak van onderhoud. Dit zijn de belangrijkste benaderingen van de verzekeringssector:
Vergelijking van verzekeringsrisicoscoremodellen
| Model | Nauwkeurigheid | Interpreteerbaarheid | Ideale gebruikscasus |
|---|---|---|---|
| GLM (Poisson/Gamma) | Gemiddeld | Zeer hoog | Actuariële basislijn, acceptatie door regelgeving |
| Willekeurig bos | Hoog | Gemiddeld | Functiebelang, robuustheid voor uitschieters |
| XGBoost / LightGBM | Zeer hoog | Gemiddeld | Standaardproductie, SOTA op tabelgegevens |
| Tabellarisch neuraal netwerk | Hoog | Laag | Complexe functies met categorische inbedding |
De meest gevestigde aanpak in de branche is de tweetrapsmodel: een model frequentie (kans op minstens één ongeval) en ernst (verwachte kosten van het ongeval). gegeven dat het voorkomt). De verwachte zuivere premie bedraagt: Frequentie x ernst.
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)
Interpreteerbaarheid met SHAP: controleerbare beslissingen
In een gereguleerde context zoals die van de verzekeringen is een black-box-model niet voldoende. De wetgeving vereist dat acceptatiebeslissingen verklaarbaar zijn: voor de klant (toch bij de AVG-uitleg), voor verzekeraars (beoordeling van grensgevallen) en voor toezichthouders (Solvabiliteit II Pijler 3, ORSA). SHAP (SHapley Additive exPlanations) is het referentiehulpmiddel industrie voor post-hoc interpreteerbaarheid van 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"
Detectie van eerlijkheid en vooringenomenheid in de EU-context
Het gebruik van proxyvariabelen (postcode, kredietscore) kan indirecte discriminatie introduceren bij wet verboden. In Europa is de richtlijn gendergelijkheid (bevestigd door Test-Aankoop uitspraak van het Hof van Justitie van de EU uit 2011) verbiedt het gebruik van gender voor prijsbepaling verzekering. De AI-wet voegt beperkingen toe voor systemen met een hoog risico die zijn geclassificeerd in bijlage III, verplichte nalevingsbeoordelingen vereisen vóór implementatie.
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 en monitoring in productie
Acceptatiemodellen zijn onderworpen aan concept drift frequent: het profiel veranderingen in het aantal aanvragers (nieuwe modellen elektrische voertuigen, demografische veranderingen), kosten van de reparaties lijdt onder inflatie, extreme weersomstandigheden veranderen de verliespatronen. Een continu monitoringsysteem met Bevolkingsstabiliteitsindex (PSI) e essentieel om te bepalen wanneer het model opnieuw moet worden gesleept.
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)))
Beste praktijken en antipatronen
Best practices voor AI-acceptatie
- Tweetrapsarchitectuur (frequentie/ernst): en de actuariële standaard en levert een nauwkeurigere prijsstelling op dan een enkel model voor het claimbedrag
- Verplichte tijdsverdeling: verzekeringsgegevens zijn in de loop van de tijd autogecorreleerd; Gebruik nooit willekeurige shuffle voor de trein-/testsplitsing
- Belichting als offset: gebruik altijd de polisduur (blootstelling in jaren) als compensatie in het Poisson-model om het aantal claims te normaliseren
- Handhaaf een basis-GLM: gegeneraliseerde lineaire modellen kunnen gemakkelijker worden gevalideerd door toezichthouders en bieden benchmarks om de toegevoegde waarde van ML te evalueren
- Schaduwmodus vóór go-live: Voer het model gedurende 30 tot 90 dagen parallel uit met menselijke acceptatie, waarbij u beslissingen vergelijkt voordat u automatiseert
- PSI wekelijks monitoren: Er is sprake van verschuivingen in de autosector als gevolg van nieuwe voertuigmodellen, inflatie van reparatiekosten en veranderingen in de regelgeving
Antipatronen die u moet vermijden
- Eigenschap lekkage: Gebruik nooit variabelen die pas na de claim beschikbaar zijn (claimbedrag, reserve) als trainingsfuncties van het frequentiemodel
- Alleen AUC optimaliseren: in de verzekeringssector zijn de relevante maatstaven Gini-coëfficiënt, Combined Ratio en Lift in het hoogste deciel van risico
- Modellen met meer dan 500 functies: onmogelijk om actuarieel te valideren en te rechtvaardigen tegenover de toezichthouder; geven de voorkeur aan een strenge selectie van kenmerken (max. 40-60 kenmerken)
- Portefeuilleconcentratie negeren: een model dat alleen zeer lage risicoprofielen accepteert, creëert antiselectie en een onevenwichtige portefeuille
- Proxy-discriminatie: variabelen zoals postcode kunnen proxy's zijn voor etniciteit; Controleer altijd de uiteenlopende impact voordat u deze inzet
Conclusies en volgende stappen
AI-acceptatie vervangt de menselijke underwriter niet, maar versterkt deze: beslissingen voor Standaardbeleid (80-90% van het volume) kan nauwkeurig en volledig worden geautomatiseerd hoger dan het menselijk gemiddelde, waardoor specialisten vrijkomen voor complexe gevallen waarbij de ervaring van heerschappij en onvervangbaar.
De sleutels tot een succesvol systeem zijn: op kennis gebaseerde deep feature engineering actuarieel, tweetraps frequentie/ernstarchitectuur, SHAP-interpreteerbaarheid voor compliance, verplichte eerlijkheidsaudit en voortdurende monitoring met PSI voor driftbeheer.
Het volgende artikel in de serie onderzoekt declaimautomatisering met Computer Vision en NLP: van digitale FNOL tot automatische fotografische schadetaxatie, tot versnelde end-to-end afwikkeling.
InsurTech Engineering-serie
- 01 - Verzekeringsdomein voor ontwikkelaars: producten, actoren en datamodellen
- 02 - Cloud-native beleidsbeheer: API-First-architectuur
- 03 - Telematicapijplijn: UBI-gegevensverwerking op schaal
- 04 - AI Underwriting: Feature Engineering en risicoscores (dit artikel)
- 05 - Claimautomatisering: Computer Vision en NLP
- 06 - Fraudedetectie: grafiekanalyse en gedragssignaal
- 07 - ACORD Standard en Insurance API-integratie
- 08 - Compliance Engineering: Solvency II en IFRS 17







