Subscriere AI: Ingineria caracteristicilor și scorul de risc în asigurările moderne
Subscrierea este inima oricărei companii de asigurări: și procesul prin care aceasta decide dacă acceptă un risc, la ce preț și în ce condiții. De zeci de ani, acest proces depindea de expertiza asiguratorilor umani care analizau documentele pe hârtie și aplicau reguli actuariale codificate în tabele. Rezultatul? Decizii in 3-5 zile lucratoare, costuri costuri de operare ridicate și variabilitate subiectivă în rândul asigurătorilor.
Inteligența artificială rescrie radical aceste reguli. Potrivit lui McKinsey, Investițiile globale în soluții AI pentru asigurări vor depăși 6 miliarde de dolari în 2025, cu BCG estimand că 36% din valoarea totală a asigurării AI este concentrată tocmai în funcţia de subscriere. Numerele operaționale sunt la fel de impresionante: timpul mediu de decizie de subscriere a scăzut de la 3-5 zile la 12,4 minute pentru ei politici standard, menținând o rată de acuratețe în evaluarea riscului de 99,3%.
Dar cum funcționează de fapt un sistem de subscriere AI? Acest ghid deconstruiește totul stiva tehnică: de la colecția și inginerie de caracteristici, la modele de punctare a riscului, până la la interpretabilitate și managementul părtinirii — cu exemple de cod reale pregătite pentru producție.
Ce vei învăța
- Arhitectura unui sistem de subscriere AI end-to-end
- Inginerie caracteristică specifică domeniului asigurărilor
- Modele ML pentru scorarea riscului: XGBoost, două etape de frecvență/severitate
- Interpretabilitate cu SHAP pentru decizii auditabile și pregătite pentru conformitate
- Detectarea părtinirii și atenuarea echității în contextul de reglementare al UE
- MLOps pentru subscrierea modelelor în producție cu MLflow
- Monitorizarea derivării datelor cu Indicele de stabilitate a populației (PSI)
Procesul de subscriere: de la Legacy la AI-Native
Înainte de a proiecta un sistem AI, este esențial să înțelegem fluxul de lucru tradițional cu care avem de-a face automatizarea. Procesul de subscriere este împărțit în patru etape fundamentale:
- Colectarea informațiilor: Solicitantul furnizează date despre sine și despre risc (chestionar, documente, posibilă inspecție fizică a activului)
- Analiza riscului: Asigurătorul evaluează probabilitatea și gravitatea oricăror daune viitoare
- Preț: Determinarea primei pe baza riscului evaluat și a obiectivelor raportului combinat al portofoliului
- Decizie: Acceptare, respingere sau acceptare cu condiții (excluderi, franciză, premium)
Un sistem nativ AI nu elimină aceste faze, ci le transformă profund: colectarea datelor analiza riscului devine automată din surse eterogene (date deschise, telematică, birou de credit). și realizat de modelele ML în milisecunde, prețul este dinamic și personalizat pentru fiecare solicitant, iar decizia este automatizată pentru cazurile standard cu supraveghere umană pentru i cazuri complexe sau limită.
Cadrul de reglementare: AI Act EU și Underwriting
Actul European AI (în vigoare în totalitate din august 2027) clasifică sistemele punctare credit si asigurare ca IA cu risc ridicat (Anexa III). Aceasta implică obligații specifice: transparența deciziilor automate, dreptul la revizuire umană, documentație tehnică detaliată și evaluarea conformității înainte de introducere pe piață. Designul de Sistemele de subscriere AI trebuie să încorporeze aceste cerințe chiar din arhitectură, nu cum modernizarea ulterioară.
Inginerie caracteristică pentru subscrierea asigurărilor
Calitatea ingineriei caracteristicilor este factorul care diferențiază cel mai mult un model de subscriere excelent din mediocru. Spre deosebire de domenii precum viziunea computerizată, unde sunt caracteristicile extrase automat din straturi convoluționale, datele tabelare de asigurare necesită inginerie manuală profundă bazată pe cunoștințe în domeniul actuarial.
Caracteristicile sectorului auto sunt grupate în cinci categorii principale:
- Caracteristici demografice: vârsta, starea civilă, tipul de reședință
- Caracteristici de conducere: ani de permis de conducere, vârsta primei obțineri, istoric de accidente și încălcări
- Caracteristicile vehiculului: marca, modelul, anul, valoarea, puterea, anul de inmatriculare
- Caracteristici geografice: densitatea urbană, indicele de criminalitate al zonei, riscul meteorologic
- Caracteristici economice: scor de credit, tip de contract de asigurare necesar
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)
),
}
Modele de evaluare a riscurilor: abordări și compromisuri
Alegerea modelului de învățare automată pentru evaluarea riscului trebuie să echilibreze acuratețea predictiv, interpretabilitate (fundamental pentru conformitate), viteza de inferență și ușurință de întreținere. Iată principalele abordări ale industriei asigurărilor:
Comparația modelelor de punctare a riscurilor de asigurare
| Model | Precizie | Interpretabilitate | Caz de utilizare ideal |
|---|---|---|---|
| GLM (Poisson/Gamma) | Medie | Foarte sus | Linie de bază actuarială, acceptare reglementară |
| Pădurea aleatorie | Ridicat | Medie | Importanța caracteristicilor, robustețe la valori aberante |
| XGBoost / LightGBM | Foarte sus | Medie | Producție standard, SOTA pe date tabulare |
| Rețeaua neuronală tabulară | Ridicat | Scăzut | Caracteristici complexe cu înglobări categorice |
Cea mai consacrată abordare în industrie este model în două etape: un model de frecvență (probabilitatea de a avea cel puțin un accident) și unul de gravitate (costul așteptat al accidentului dat fiind că se produce). Prima pură așteptată este: Frecvență x Severitate.
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)
Interpretabilitate cu SHAP: decizii auditabile
Într-un context reglementat precum cel de asigurare, un model cutie neagră nu este suficient. Legislația cere ca deciziile de subscriere să fie explicabile: pentru client (drept la explicația GDPR), pentru asiguratori (revizuirea cazurilor limită) și pentru autoritățile de reglementare (Solvabilitate II Pilonul 3, ORSA). SHAP (SHapley Additive exPlanations) este instrumentul de referință industria pentru interpretabilitatea post-hoc a modelelor de ansamblu.
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"
Detectarea corectitudinii și a părtinirii în contextul UE
Utilizarea variabilelor proxy (codul poștal, scorul de credit) poate introduce discriminare indirectă interzis de lege. În Europa, directiva privind egalitatea de gen (confirmată de Hotărârea Test-Achats a Curții de Justiție a UE din 2011) interzice utilizarea genului pentru stabilirea prețurilor asigurare. Legea AI adaugă constrângeri pentru sistemele cu risc ridicat clasificate în anexa III, care necesită evaluări obligatorii de conformitate înainte de implementare.
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 și monitorizare în producție
Modelele de subscriere sunt supuse deriva conceptului frecvente: profilul modificări ale solicitanților (modele noi de vehicule electrice, schimbări demografice), costuri de reparații suferă inflație, evenimentele climatice extreme modifică modelele de pierderi. Un sistem de monitorizare continuă cu Indicele de stabilitate a populației (PSI) e esențial pentru a identifica momentul în care modelul trebuie retractat.
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)))
Cele mai bune practici și anti-modele
Cele mai bune practici pentru AI Underwriting
- Arhitectură în două etape (frecvență/severitate): și standardul actuarial și produce prețuri mai precise decât un singur model pentru valoarea creanței
- Împărțirea timpului obligatorie: datele de asigurare sunt autocorelate în timp; nu utilizați niciodată amestecarea aleatorie pentru împărțirea trenului/testului
- Expunerea ca compensare: utilizați întotdeauna durata poliței (expunerea în ani) ca o compensare în modelul Poisson pentru a normaliza numărul de cereri
- Mențineți un GLM de bază: modelele liniare generalizate sunt mai ușor validate de autoritățile de reglementare și oferă repere pentru a evalua valoarea adăugată a ML
- Modul umbră înainte de lansare: Rulați modelul în paralel cu subscrierea umană timp de 30-90 de zile, comparând deciziile înainte de automatizare
- Monitorizați PSI săptămânal: deriva în sectorul auto este frecventă din cauza noilor modele de vehicule, a creșterii costurilor de reparații și a modificărilor de reglementare
Anti-modele de evitat
- Scurgere caracteristică: nu utilizați niciodată variabile disponibile numai după revendicare (suma revendicarii, rezervă) ca caracteristici de antrenament ale modelului de frecvență
- Optimizați numai AUC: în sectorul asigurărilor, valorile relevante sunt coeficientul Gini, raportul combinat și creșterea în decilul superior al riscului
- Modele cu peste 500 de caracteristici: imposibil de validat actuarial și justificat în fața autorității de reglementare; preferă selecția riguroasă a caracteristicilor (maxim 40-60 de caracteristici)
- Ignorând concentrarea portofoliului: un model care acceptă doar profiluri de risc foarte scăzute creează anti-selecție și un portofoliu dezechilibrat
- Discriminare prin proxy: variabile precum codul poștal pot fi proxy pentru etnie; verificați întotdeauna impactul disparat înainte de implementare
Concluzii și pașii următori
Subscrierea AI nu înlocuiește asiguratorul uman, ci îl amplifică: decizii pentru Politicile standard (80-90% din volum) pot fi complet automatizate cu acuratețe mai mare decât media umană, eliberând specialiştii pentru cazuri complexe în care experienţa de stăpânire și de neînlocuit.
Cheile pentru un sistem de succes sunt: ingineria profundă a caracteristicilor bazată pe cunoștințe actuarială, arhitectură frecvență/severitate în două etape, interpretabilitate SHAP pentru conformitate, audit obligatoriu de corectitudine și monitorizare continuă cu PSI pentru managementul derivei.
Următorul articol din serie exploreazărevendică automatizarea cu Computer Vision și NLP: de la FNOL digital la evaluarea automată a daunelor fotografice, până la decontare accelerată de la capăt la capăt.
Seria InsurTech Engineering
- 01 - Domeniul de asigurare pentru Dezvoltatori: Produse, Actori și Modele de Date
- 02 - Cloud-Native Policy Management: API-First Architecture
- 03 - Telematics Pipeline: UBI Data Processing la scară
- 04 - Subscriere AI: Ingineria caracteristicilor și scorul de risc (acest articol)
- 05 - Automatizarea revendicărilor: Computer Vision și NLP
- 06 - Detectarea fraudelor: analiză grafică și semnal comportamental
- 07 - Integrare ACORD Standard și Insurance API
- 08 - Inginerie de conformitate: Solvabilitate II și IFRS 17







