Module 4 · Session 12 · 90 min · Python Lab

Session 12: AI for Underwriting

CILO-2, CILO-3 · Analytics & Solution Design · Lab-based, Hands-on (Python) · Jupyter Notebook required

Learning Objectives

1. Underwriting: From Art to Science

Underwriting is the process of evaluating risk and deciding whether to accept it, at what price, and on what terms. For most of insurance history, underwriting was an art — a senior underwriter in a room, reviewing paper applications, applying experience-based rules, and making subjective judgments. The problem was not that this was inefficient (though it was). The problem was that it was inconsistent — two equally qualified underwriters reviewing the same application could reach different decisions, and the same underwriter could make different decisions at 9 AM and 4 PM.

AI-driven underwriting replaces subjective judgment with consistent, data-driven risk assessment. It does not eliminate the underwriter — it eliminates the inconsistency. The underwriter's role shifts from making individual risk decisions to: (a) setting the risk appetite and pricing framework within which the AI operates, (b) handling the complex referrals the AI flags, (c) monitoring the AI's decisions for drift and fairness, and (d) improving the models over time.

1.1 Traditional vs. AI Underwriting

DimensionTraditional UnderwritingAI-Driven Underwriting
Risk assessment methodRule-based guidelines, manual judgment, experience-based heuristicsStatistical model using historical claims data and thousands of feature interactions
Decision speedHours to days (review, refer, escalate)Seconds to minutes (automated for 70–90% of standard cases)
ConsistencyLow to moderate — varies by individual underwriter, time of day, workloadHigh — same features always produce the same risk score
Data usedApplication form data (limited, self-reported)Application data + external data (credit bureau, vehicle registration, medical databases, property records, Account Aggregator)
Feature interactionsManual — underwriter checks a few combinations (e.g., "age + vehicle type")Automatic — model considers hundreds of interactions simultaneously
Pricing accuracyBroad risk bands — good risks pay slightly more, bad risks pay slightly lessGranular risk segmentation — each policyholder pays closer to their true risk
STP (Straight-Through Processing)5–20% (only the simplest cases auto-accepted)60–90% (all but the most complex or borderline cases auto-accepted)
Regulatory challengeEasier to defend — "the underwriter used their professional judgment"Harder to defend — "explain how the model decided that this applicant is risky"
🌎
Real World: Digit Insurance in India automated over 90% of its motor policy underwriting decisions. The AI model processes applications in seconds, using vehicle registration data, customer demographics, credit information, and claims history from the industry database. For the 10% of applications that fall outside the model's confidence threshold — typically high-value policies, complex vehicle modifications, or applicants with unusual credit profiles — the case is referred to a human underwriter. This hybrid model allowed Digit to process high volumes without building a large underwriting team, while maintaining risk discipline. The result: Digit achieved a motor insurance loss ratio consistently below the industry average, suggesting that the AI was not just faster but better at risk selection. (Figures are drawn from public industry coverage of Digit's digital underwriting; verify against the company's current disclosures before quoting.)

2. Feature Engineering for Underwriting

Feature engineering is the process of transforming raw data into inputs that a machine learning model can use effectively. In underwriting, this means taking the application data, internal records, and external data sources and creating predictive features. The quality of feature engineering determines more about model performance than the choice of algorithm — a well-engineered dataset with a simple model often outperforms a poorly-engineered dataset with a complex one.

2.1 Feature Types in Underwriting

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, classification_report, confusion_matrix
import matplotlib.pyplot as plt

# Load the insurance dataset
df = pd.read_csv('data/insurance_cleaned.csv')

# Create policy-level dataset (one row per policy)
dataset = df.groupby('policy_id').agg(
    customer_id=('customer_id', 'first'),
    age=('age', 'first'),
    gender=('gender', 'first'),
    location=('location', 'first'),
    occupation=('occupation', 'first'),
    income=('income', 'first'),
    credit_score=('credit_score', 'first'),
    premium=('premium', 'first'),
    sum_assured=('sum_assured', 'first'),
    policy_type=('policy_type', 'first'),
    channel=('channel', 'first'),
    tenure_months=('tenure_months', 'first'),
    has_claim=('claim_id', lambda x: 1 if len(x) > 0 else 0),
    claim_amount=('claim_amount', 'sum'),
    days_since_purchase=('start_date', lambda x:
        (pd.Timestamp.today() - pd.to_datetime(x.iloc[0])).days if len(x) > 0 else 0)
).reset_index()

print(f"Policy-level dataset: {dataset.shape[0]:,} rows, {dataset.shape[1]:,} columns")
print(f"\nClaim rate: {dataset['has_claim'].mean()*100:.1f}%")
print(f"Columns available for feature engineering:\n{dataset.columns.tolist()}")

2.2 Creating Underwriting Features

# === FEATURE ENGINEERING PIPELINE ===

# 1. Risk-to-Income Ratio (captures affordability stress)
dataset['risk_income_ratio'] = (
    dataset['sum_assured'] / (dataset['income'] + 1)
)
# Cap extreme values
dataset['risk_income_ratio'] = dataset['risk_income_ratio'].clip(upper=100)

# 2. Premium Density (premium per unit of sum assured)
dataset['premium_density'] = (
    dataset['premium'] / (dataset['sum_assured'] + 1)
)

# 3. Age squared (non-linear age effect — claim risk often increases faster than linearly)
dataset['age_squared'] = dataset['age'] ** 2

# 4. Credit score category (binned version)
dataset['credit_tier'] = pd.cut(
    dataset['credit_score'],
    bins=[0, 600, 700, 800, 900],
    labels=['Low (<600)', 'Fair (600-699)', 'Good (700-799)', 'Excellent (800+)']
)

# 5. Policy tenure in years (from tenure_months)
dataset['tenure_years'] = dataset['tenure_months'] / 12

# 6. Age × Channel interaction (certain channels attract different risk profiles)
dataset['age_channel'] = dataset['age'] * (
    dataset['channel'].map({'Online': 1, 'Agent': 2, 'Aggregator': 3, 'Bancassurance': 4, 'Partnership': 5})
    .fillna(0)
)

# 7. Income decile (within the portfolio)
dataset['income_decile'] = pd.qcut(
    dataset['income'], q=10, labels=False, duplicates='drop'
)

# 8. Senior citizen flag (biologically significant risk factor)
dataset['senior_flag'] = (dataset['age'] >= 60).astype(int)

# 9. Low credit flag
dataset['low_credit_flag'] = (dataset['credit_score'] < 650).astype(int)

# Verify new features
new_features = ['risk_income_ratio', 'premium_density', 'age_squared',
                'credit_tier', 'tenure_years', 'age_channel',
                'income_decile', 'senior_flag', 'low_credit_flag']

print("New features created:")
for feat in new_features:
    if feat in dataset.columns:
        print(f"  ✓ {feat:25s} — dtype: {str(dataset[feat].dtype):10s}, "
              f"unique: {dataset[feat].nunique()}, missing: {dataset[feat].isna().sum()}")
    else:
        print(f"  ✗ {feat:25s} — NOT FOUND")

2.3 Feature Selection

Not all features are worth including. Features with: (a) too many missing values, (b) near-zero variance (almost same value for every observation), or (c) correlations > 0.9 with another feature (multicollinearity) should be reviewed before modeling. Feature selection balances predictive power against model complexity and explainability.

# Check for highly correlated features
numeric_features_for_uw = ['age', 'income', 'credit_score', 'premium',
                           'sum_assured', 'tenure_months', 'risk_income_ratio',
                           'premium_density', 'age_squared', 'tenure_years',
                           'age_channel', 'income_decile', 'senior_flag',
                           'low_credit_flag']
numeric_features_for_uw = [c for c in numeric_features_for_uw if c in dataset.columns]

corr_matrix = dataset[numeric_features_for_uw].corr()

# Find high-correlation pairs
high_corr_pairs = []
for i in range(len(corr_matrix.columns)):
    for j in range(i + 1, len(corr_matrix.columns)):
        if abs(corr_matrix.iloc[i, j]) > 0.85:
            high_corr_pairs.append((corr_matrix.columns[i], corr_matrix.columns[j],
                                    corr_matrix.iloc[i, j]))

if high_corr_pairs:
    print("⚠ HIGH CORRELATION DETECTED (|r| > 0.85) — consider removing one of each pair:")
    for v1, v2, corr in high_corr_pairs:
        print(f"  {v1:20s} ↔ {v2:20s}  r = {corr:.3f}")
else:
    print("✓ No highly correlated feature pairs detected")

# Separate features and target
feature_cols = (numeric_features_for_uw +
                ['policy_type', 'channel', 'gender', 'occupation', 'credit_tier'])
feature_cols = [c for c in feature_cols if c in dataset.columns]

X = dataset[feature_cols].copy()
y = dataset['has_claim']

print(f"\nFinal feature set: {X.shape[1]} features ({X.shape[0]:,} observations)")
print(f"Categorical: {X.select_dtypes(include=['object', 'category']).columns.tolist()}")
print(f"Numerical: {X.select_dtypes(include=[np.number]).columns.tolist()}")
💡
Pro Tip: The most predictive underwriting feature is often a ratio — sum_assured/income (how much cover relative to income), premium_density (premium per unit of cover), or claim_count/tenure (past claim frequency normalized by exposure). Single raw features (age, premium, income) are useful but interactions and ratios capture the underwriting judgment that experienced human underwriters apply intuitively. When building an underwriting model, spend 60% of your time on feature engineering, 20% on model selection, and 20% on validation. The feature engineering time is the highest-ROI investment in the entire workflow.

3. Building a Risk Scoring Model

A risk score is a single number (typically 0–1000) that represents the estimated risk of a policyholder filing a claim. The score is derived from the predicted probability from a classification model, scaled to a more interpretable range. Higher score = higher risk. The business use: a risk score of 200 means "low risk" (accept and possibly discount), 500 means "standard risk" (accept at standard terms), and 800 means "high risk" (load premium, add restrictions, or refer).

3.1 Training a Random Forest Classifier

# Identify categorical and numeric columns
cat_cols = X.select_dtypes(include=['object', 'category']).columns.tolist()
num_cols = X.select_dtypes(include=[np.number]).columns.tolist()

# Splitting the dataset into the Training set and Test set
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0, stratify = y)

# Encoding categorical data
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
ct = ColumnTransformer(transformers=[('encoder', OneHotEncoder(drop='first', handle_unknown='ignore'), cat_cols)], remainder='passthrough')
X_train = np.array(ct.fit_transform(X_train))
X_test = np.array(ct.transform(X_test))

# Feature Scaling
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)

# Training the Random Forest model on the Training set
from sklearn.ensemble import RandomForestClassifier
classifier = RandomForestClassifier(n_estimators = 200, max_depth = 10, min_samples_leaf = 20, class_weight = 'balanced', random_state = 0, n_jobs = -1)
classifier.fit(X_train, y_train)

# Predicting the Test set results
y_pred = classifier.predict(X_test)
y_prob = classifier.predict_proba(X_test)[:, 1]

# Evaluating the model
from sklearn.metrics import roc_auc_score, confusion_matrix, accuracy_score
cm = confusion_matrix(y_test, y_pred)
print(cm)
print(accuracy_score(y_test, y_pred))
auc = roc_auc_score(y_test, y_prob)
print(f"Model ROC-AUC: {auc:.3f}")
print(f"Interpretation: The model can distinguish claim-filers from non-claim-filers")
print(f"{auc*100:.1f}% of the time — this is {'strong' if auc > 0.8 else 'moderate' if auc > 0.7 else 'moderate-to-weak'} discriminatory power.")

3.2 Converting Probabilities to Risk Scores

# Predict probabilities for ALL policies (not just test set)
X_all = sc.transform(np.array(ct.transform(X)))
all_prob = classifier.predict_proba(X_all)[:, 1]

# Scale to 0–1000 risk score
dataset['risk_score'] = (all_prob * 1000).astype(int)

# Define risk tiers
dataset['risk_tier'] = pd.cut(
    dataset['risk_score'],
    bins=[0, 200, 400, 600, 800, 1000],
    labels=['Very Low', 'Low', 'Moderate', 'High', 'Very High'],
    right=False
)

# Examine risk score distribution
print("Risk Score Distribution:")
print(f"  Min:      {dataset['risk_score'].min()}")
print(f"  25th pct: {dataset['risk_score'].quantile(0.25):.0f}")
print(f"  Median:   {dataset['risk_score'].median():.0f}")
print(f"  75th pct: {dataset['risk_score'].quantile(0.75):.0f}")
print(f"  Max:      {dataset['risk_score'].max()}")
print(f"\nRisk Tier Breakdown:")
print(dataset['risk_tier'].value_counts().sort_index())

# Validate risk tiers against actual claim rates
tier_validation = dataset.groupby('risk_tier', observed=False).agg(
    count=('risk_tier', 'size'),
    actual_claim_rate=('has_claim', 'mean'),
    avg_risk_score=('risk_score', 'mean')
).round(3)

print(f"\n{'=' * 65}")
print(f"{'Risk Tier':15s} {'Count':>8s} {'%':>5s} {'Claim Rate':>12s} {'Avg Score':>10s}")
print(f"{'=' * 65}")
for tier in tier_validation.index:
    r = tier_validation.loc[tier]
    print(f"{str(tier):15s} {r['count']:>5,.0f}  {r['count']/tier_validation['count'].sum()*100:>4.1f}%    {r['actual_claim_rate']*100:>5.1f}%     {r['avg_risk_score']:>5.0f}")

3.3 The Calibration Check

A well-calibrated risk scoring model should satisfy: for policies with a risk score of X, approximately X/1000 of them should actually file a claim. For example, policies with a risk score of 500 should have an actual claim rate of ~50%. If the model says 500 but the actual rate is 35%, the model is miscalibrated — it is overstating risk. If it says 500 and the actual rate is 65%, it is understating risk.

# Calibration check: compare predicted risk score to actual claim rate by decile
dataset['score_decile'] = pd.qcut(dataset['risk_score'], q=10, labels=False, duplicates='drop')

calibration = dataset.groupby('score_decile').agg(
    avg_predicted_risk=('risk_score', 'mean'),
    actual_claim_rate=('has_claim', 'mean'),
    count=('risk_score', 'count')
)

# The predicted risk / 1000 should approximate the actual claim rate
calibration['expected_claim_rate'] = calibration['avg_predicted_risk'] / 1000
calibration['calibration_error'] = (
    calibration['actual_claim_rate'] - calibration['expected_claim_rate']
)

print(f"{'Decile':8s} {'Predicted':>10s} {'Expected':>10s} {'Actual':>10s} {'Error':>10s}")
print("-" * 48)
for idx, row in calibration.iterrows():
    print(f"{idx:>3d}-{idx+1:.0f}%   {row['avg_predicted_risk']:>5.0f}     {row['expected_claim_rate']*100:>5.1f}%     {row['actual_claim_rate']*100:>5.1f}%    {row['calibration_error']*100:>+5.1f}%")

print(f"\nMean absolute calibration error: {calibration['calibration_error'].abs().mean()*100:.2f}%")
print("(Lower is better. <3% mean error is acceptable for production models.)")
📝
Note: The risk score should never be treated as the sole decision factor. A policyholder with a risk score of 800 should trigger a referral to a human underwriter, not an automatic rejection. The risk score is a recommendation, not a verdict. The business rules determine what happens with each score tier — which is where the underwriting automation workflow (Section 7) comes in.

4. Random Forest & Tree-Based Models

Random Forest is the most widely used algorithm for insurance underwriting models. It is popular because it: (a) handles mixed data types (numeric and categorical) naturally, (b) captures non-linear relationships without manual feature transformation, (c) provides built-in feature importance rankings, (d) is less prone to overfitting than single decision trees, and (e) is relatively robust to outliers and missing data.

4.1 How Random Forest Works

A Random Forest builds hundreds of individual decision trees, each trained on a random subset of the data and a random subset of features. Each tree independently predicts the outcome (claim or no claim). The forest's final prediction is the average of all individual tree predictions. This "ensemble" approach corrects for the tendency of individual trees to overfit — the trees make different mistakes, and the averaging cancels those mistakes out.

4.2 Hyperparameter Tuning

# Key Random Forest hyperparameters and their effect on underwriting models
print("RANDOM FOREST HYPERPARAMETERS FOR UNDERWRITING")
print("=" * 60)
print(f"{'Parameter':25s} {'Controls':40s}")
print("-" * 60)
print(f"{'n_estimators':25s} Number of trees. More trees = more stable predictions.")
print(f"{'':25s}     Diminishing returns beyond 300–500 trees. Set high and use n_jobs.")
print(f"{'max_depth':25s} Maximum tree depth. Deeper trees = more complex (risk of overfitting).")
print(f"{'':25s}     For underwriting, max_depth=10–15 works well with 100K+ records.")
print(f"{'min_samples_leaf':25s} Minimum observations per leaf. Higher = simpler, smoother predictions.")
print(f"{'':25s}     20–50 for large datasets; prevents the model from learning noise.")
print(f"{'class_weight':25s} Handles class imbalance. Set to 'balanced' or 'balanced_subsample'.")
print(f"{'':25s}     Tells the model: "false negatives are more costly than false positives."")
print(f"{'max_features':25s} Features considered per split. sqrt(n) for classification is standard.")
print(f"{'':25s}     Lower = more randomness = less correlated trees = better ensemble.")

# Quick grid search for optimal depth
from sklearn.model_selection import cross_val_score

depths = [5, 8, 10, 12, 15]
cv_scores_depth = []

for depth in depths:
    rf = RandomForestClassifier(
        n_estimators=100, max_depth=depth, min_samples_leaf=20,
        class_weight='balanced', random_state = 0, n_jobs=-1
    )
    pipe = Pipeline(steps=[
        ('preprocessor', ct),
        ('classifier', rf)
    ])
    scores = cross_val_score(pipe, X_train, y_train, cv=3, scoring='roc_auc')
    cv_scores_depth.append(scores.mean())
    print(f"  max_depth={depth:>2d}: CV AUC = {scores.mean():.4f}")

best_depth = depths[np.argmax(cv_scores_depth)]
print(f"\nBest max_depth: {best_depth}")

4.3 Feature Importance

# Extract feature importance from the trained model
classifier = classifier

# Get feature names after preprocessing
preprocessor_trained = ct
cat_encoder = preprocessor_trained.named_transformers_['cat']

# Get category names
cat_feature_names = []
if len(cat_cols) > 0:
    cat_feature_names = cat_encoder.get_feature_names_out(cat_cols).tolist()
all_features = num_cols + cat_feature_names

# Get importances
importances = classifier.feature_importances_

# Create DataFrame
fi_df = pd.DataFrame({
    'feature': all_features,
    'importance': importances
}).sort_values('importance', ascending=True)

# Plot
fig, ax = plt.subplots(figsize=(10, max(6, len(fi_df) * 0.3)))
ax.barh(fi_df.tail(15)['feature'], fi_df.tail(15)['importance'],
        color='#6c5ce7', edgecolor='white')
ax.set_xlabel('Feature Importance (Random Forest)')
ax.set_title('Top 15 Features Driving Underwriting Risk Score', fontweight='bold')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()

print("\nTop 5 Risk Drivers:")
for _, row in fi_df.sort_values('importance', ascending=False).head(5).iterrows():
    print(f"  {row['feature']:25s}: {row['importance']:.4f} ({row['importance']/fi_df['importance'].sum()*100:.1f}% of total importance)")
💡
Illustrative Scenario (hypothetical, for teaching purposes): A large Indian general insurer deploys its first Random Forest underwriting model for motor insurance, and the feature importance analysis reveals a surprising result: the vehicle's age at the time of policy issuance was the strongest single predictor of claim frequency — stronger than driver age, vehicle value, or location. This challenges the conventional underwriting wisdom that driver characteristics matter most, and the insurer adjusts its guidelines to give vehicle age a larger rating factor. The point is the method — ML surfaces patterns that human preconceptions hide. This is a constructed teaching example, not a reported industry case.

5. Premium Recommendation Engine

The business value of an underwriting model is not the risk score itself — it is the decision that the risk score enables. The premium recommendation engine maps risk scores to pricing actions: what premium should we charge this specific policyholder given their risk score, compared to the current "standard" premium?

5.1 Mapping Risk Scores to Premium Adjustments

# Define premium adjustment rules based on risk tier
# The "standard premium" is the base rate for a Moderate risk policyholder
# Very Low risk:  20–30% discount
# Low risk:       5–15% discount
# Moderate risk:  Standard rate (±5% from base)
# High risk:      10–25% loading
# Very High risk: 25–50% loading OR referral for manual review

def calculate_premium_loading(risk_score, base_premium):
    """Calculate recommended premium adjustment based on risk score.

    Parameters:
    -----------
    risk_score : int — Model risk score (0–1000)
    base_premium : float — The current standard premium for this product

    Returns:
    --------
    dict — Loading factor, recommended premium, and action
    """
    if risk_score < 200:
        factor = 0.70  # 30% discount
        action = "Accept — Fast-track (no human review)"
    elif risk_score < 400:
        factor = 0.85  # 15% discount
        action = "Accept — Standard review"
    elif risk_score < 600:
        factor = 1.00  # Standard rate
        action = "Accept — Standard review"
    elif risk_score < 750:
        factor = 1.20  # 20% loading
        action = "Accept with loading — Manual review recommended"
    elif risk_score < 900:
        factor = 1.40  # 40% loading
        action = "Refer — Senior underwriter decision required"
    else:
        factor = None  # Decline / manual referral
        action = "Decline — Or refer for exceptional approval"

    if factor is not None:
        recommended_premium = round(base_premium * factor, 0)
    else:
        recommended_premium = None

    return {
        'risk_score': risk_score,
        'base_premium': base_premium,
        'loading_factor': factor,
        'recommended_premium': recommended_premium,
        'action': action,
        'premium_change_pct': round((factor - 1) * 100, 0) if factor else None
    }

# Apply to sample policies
sample_policies = dataset[['policy_id', 'risk_score', 'premium', 'risk_tier']].sample(10, random_state = 0)

print("=" * 120)
print(f"{'Policy ID':12s} {'Risk Score':>12s} {'Tier':15s} {'Base Premium':>15s} {'Loading':>8s} {'Recommended':>15s} {'Action'}")
print("=" * 120)
for _, row in sample_policies.sort_values('risk_score').iterrows():
    result = calculate_premium_loading(row['risk_score'], row['premium'])
    loading_str = f"{result['premium_change_pct']:+.0f}%" if result['loading_factor'] else "N/A"
    rec_str = f"₹{result['recommended_premium']:,.0f}" if result['recommended_premium'] else "DECLINE"
    print(f"{row['policy_id']:12s} {result['risk_score']:>5d}      {str(row['risk_tier']):15s} ₹{row['premium']:>8,.0f}          {loading_str:>6s}  {rec_str:>12s}  {result['action'][:40]}")

# Portfolio impact analysis
dataset['premium_loading_factor'] = dataset['risk_score'].apply(
    lambda s: 0.70 if s < 200 else 0.85 if s < 400 else 1.00 if s < 600
    else 1.20 if s < 750 else 1.40 if s < 900 else None
)
dataset['recommended_premium'] = dataset['premium'] * dataset['premium_loading_factor']

print(f"\n{'=' * 60}")
print("PORTFOLIO IMPACT SUMMARY")
print(f"{'=' * 60}")
print(f"{'Tier':15s} {'Policies':>10s} {'Current Premium':>18s} {'New Premium':>15s} {'Change':>10s}")
print("-" * 68)
for tier in ['Very Low', 'Low', 'Moderate', 'High', 'Very High']:
    tier_data = dataset[dataset['risk_tier'] == tier]
    if len(tier_data) == 0:
        continue
    current = tier_data['premium'].sum()
    new = tier_data['recommended_premium'].sum()
    change = new - current
    print(f"{tier:15s} {len(tier_data):>6,d}     ₹{current/1e7:>6.1f}Cr        ₹{new/1e7:>6.1f}Cr     {'+' if change > 0 else ''}₹{change/1e7:>4.1f}Cr")

total_current = dataset['premium'].sum()
total_new = dataset['recommended_premium'].sum() if dataset['recommended_premium'].notna().all() else None
print("-" * 68)
if total_new:
    print(f"{'TOTAL':15s} {len(dataset):>6,d}     ₹{total_current/1e7:>6.1f}Cr        ₹{total_new/1e7:>6.1f}Cr     {'+' if total_new > total_current else ''}₹{(total_new-total_current)/1e7:>4.1f}Cr")
💡
Pro Tip: The premium recommendation engine output — "a policy with score 650 should be loaded by 20%" — is not the final price. It is an input to the pricing decision. In practice, the recommended premium must be validated against: (a) market competitiveness (if the loaded premium is 40% above market, the applicant will leave), (b) regulatory compliance (some loading factors may be restricted), and (c) customer fairness (loading 40% for a credit-score-based risk factor may raise ethical questions). The premium recommendation engine is a powerful decision-support tool — but it should not have the final say on pricing without human oversight of the business context.
📋 Stable content — Reviewed: July 2026

6. Model Explainability for Underwriting

An underwriting model that makes accurate predictions but cannot explain why it reached a particular decision is a liability. Regulators are converging on this point: the U.S. National Association of Insurance Commissioners (NAIC) "Model Bulletin on the Use of AI Systems by Insurers" (2023) requires insurers to maintain a governance framework capable of explaining AI-driven decisions [1], and the EU AI Act classifies insurance underwriting and pricing as high-risk, imposing explainability, human-oversight, and conformity requirements [2]. India is now formalising the same expectation — in June 2026, IRDAI constituted a seven-member AI Working Group to develop the country's first formal AI governance framework [3]. Model explainability is not a nice-to-have; it is becoming a regulatory requirement.

📈
Financial & Risk Management Implications — underwriting-model miscalibration is a capital problem: A risk-scoring model that is systematically biased (e.g., under-prices a segment) does not just distort pricing — it flows through the financial statements. Under-estimating expected claims in a segment under-states IBNR and case reserves, so reserve adequacy deteriorates and the reported loss ratio looks better than reality until the claims emerge. Conversely, over-estimating risk for a protected or sensitive group can trigger pricing distortions and regulatory/legal challenge. This is the precise mechanism analysed in the EU-AI-Act literature: algorithmic bias in underwriting creates capital strain — the model's error must ultimately be absorbed as unexpected loss, drawing down regulatory capital that a Solvency-II-style or IRDAI solvency regime requires to be held [2]. For a Finance-specialization audience, the thread to pull is: model error → reserving error → loss-ratio surprise → capital strain — the same model-risk chain covered in banking/credit-risk validation, applied to the underwriting model.

6.1 Why Explainability Matters in Underwriting

6.2 SHAP Values — Explaining Individual Predictions

SHAP (SHapley Additive exPlanations) values decompose a prediction into the contribution of each feature. A SHAP value tells you: how much did this specific feature (e.g., age = 55) push the risk score up or down, compared to the average prediction?

# SHAP analysis requires the model to be used directly (not through a pipeline)
# We'll train a model on the transformed data for SHAP analysis

# First, transform the features
X_train_transformed = ct.fit_transform(X_train)
X_test_transformed = ct.transform(X_test)

# Train a simplified model for SHAP analysis (smaller forest = faster SHAP)
classifier = RandomForestClassifier(
    n_estimators=100, max_depth=10, min_samples_leaf=20,
    class_weight='balanced', random_state = 0, n_jobs=-1
)
classifier.fit(X_train_transformed, y_train)

print("Model trained on transformed data for SHAP analysis.")
print(f"Training examples: {X_train_transformed.shape[0]:,}")
print(f"Features: {X_train_transformed.shape[1]:,}")

# For the SHAP analysis, we'll manually compute feature-level impact
# by examining the model's feature_importances_ and the data

# Get feature names
all_feature_names = num_cols + list(
    ct.named_transformers_['encoder']
    .get_feature_names_out(cat_cols)
) if len(cat_cols) > 0 else num_cols

# Create a DataFrame of the transformed test set with feature names
X_test_df = pd.DataFrame(X_test_transformed, columns=all_feature_names)

# Get the model's predictions and probabilities
y_pred_test = classifier.predict(X_test_transformed)
y_prob_test = classifier.predict_proba(X_test_transformed)[:, 1]

# Find the most "interesting" case — largest model error or extreme prediction
# Example: a policy that was predicted Very High risk but had no claim
false_high_risk = (y_pred_test == 1) & (y_test == 0)
false_high_indices = np.where(false_high_risk)[0]

if len(false_high_indices) > 0:
    example_idx = false_high_indices[0]
    print(f"\nExample case — Model predicted claim but no claim occurred:")
    print(f"  Predicted probability: {y_prob_test[example_idx]:.2%}")
    print(f"  Features contributing most to this prediction:")

    # Get the contribution of each feature (simplified: feature value × feature importance)
    # This is a simplified approximation of SHAP
    contributions = (X_test_df.iloc[example_idx].values * classifier.feature_importances_)
    top_features = pd.DataFrame({
        'feature': all_feature_names,
        'value': X_test_df.iloc[example_idx].values,
        'importance': classifier.feature_importances_,
        'contribution': contributions
    }).sort_values('contribution', key=abs, ascending=False).head(5)

    for _, row in top_features.iterrows():
        direction = "🔼 increasing risk" if row['value'] > 0 else "🔽 decreasing risk"
        print(f"  {row['feature']:25s}: value={row['value']:>7.2f}  {direction}")

print(f"\nNote: Install the 'shap' package (pip install shap) and use TreeExplainer")
print(f"for true SHAP values. The simplified analysis above uses feature values ×")
print(f"global importance as a proxy — it is useful for learning but not for production.")

6.3 The Explainability Report for Regulators

A regulatory-grade explainability report for an underwriting model should document:

  1. Model purpose and scope: What does the model predict? What products and channels does it apply to? What is explicitly excluded?
  2. Features used: Complete list of input features, their sources, and whether any are protected characteristics (or proxies for them).
  3. Feature importance: Global feature importance across the entire portfolio — not just "which features matter" but "how much do they matter relative to each other?"
  4. Fairness analysis: Results of disparate impact testing across demographic groups (age bands, gender, location). If the model produces materially different outcomes for different groups, document whether the difference is justified by risk data.
  5. Model performance: ROC-AUC, calibration curve, precision-recall curve, and confusion matrix on a representative test set that mirrors the target population.
  6. Human oversight: Clear rules for which cases are referred to human underwriters and how referral decisions are made.
  7. Version control and monitoring: Model version, training date, expected drift monitoring schedule, and threshold for model retraining.
💡
Illustrative Scenario (hypothetical, for teaching purposes): A European insurer deploys an AI underwriting model and is challenged by its regulator to explain why a specific policyholder received a 30% premium loading. Using SHAP values, the insurer demonstrates that the loading is driven by three factors: (1) a credit score in the lowest decile, (2) two claims in the previous policy year, and (3) a vehicle model with above-average claim severity. The regulator accepts the explanation; without SHAP, the insurer could not have provided a specific, verifiable reason. This is a constructed teaching example illustrating the real regulatory requirement set out in the NAIC Model Bulletin [1] and the EU AI Act's high-risk classification [2], not a reported EU enforcement case.
📋 Stable content — Reviewed: July 2026

7. The Underwriting Automation Workflow

An AI-driven underwriting system is not just a model — it is an end-to-end workflow that integrates data ingestion, feature computation, risk scoring, decision logic, referral management, and policy issuance. The workflow must be designed to handle the exceptions, edge cases, and system failures that inevitably occur in production.

7.1 The End-to-End Workflow

┌──────────────────────────────────────────────────────────────────────────────────────┐
│                           AI UNDERWRITING WORKFLOW                                    │
├──────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                       │
│  APPLICATION SUBMITTED                                                               │
│         │                                                                           │
│         ▼                                                                           │
│  1. DATA INGESTION                                                                  │
│     ┌─────────────────────────────────────────────────────────────────┐             │
│     │ • Application form data (name, age, vehicle details, etc.)      │             │
│     │ • Internal data (previous policies, claims history)             │             │
│     │ • External data (credit bureau via API, VAHAN for vehicle data) │             │
│     │ • Data quality check — are all required fields present?         │             │
│     └─────────────────────────────────────────────────────────────────┘             │
│         │                                                                           │
│         ▼                                                                           │
│  2. FEATURE COMPUTATION                                                             │
│     ┌─────────────────────────────────────────────────────────────────┐             │
│     │ • Calculate engineered features (risk_income_ratio, etc.)       │             │
│     │ • Apply the same preprocessing as training (scaler, encoder)    │             │
│     │ • Handle missing data (imputation rules)                        │             │
│     └─────────────────────────────────────────────────────────────────┘             │
│         │                                                                           │
│         ▼                                                                           │
│  3. RISK SCORING                                                                    │
│     ┌─────────────────────────────────────────────────────────────────┐             │
│     │ • Apply trained model → get predicted probability                │             │
│     │ • Scale to 0–1000 risk score                                    │             │
│     │ • Assign risk tier (Very Low → Very High)                       │             │
│     └─────────────────────────────────────────────────────────────────┘             │
│         │                                                                           │
│         ▼                                                                           │
│  4. DECISION LOGIC                                                                  │
│     ┌─────────────────────────────────────────────────────────────────┐             │
│     │ • Risk Score < 400 → AUTO-ACCEPT (fast-track, no human touch)   │             │
│     │ • Risk Score 400–599 → ACCEPT with standard review              │             │
│     │ • Risk Score 600–749 → ACCEPT WITH LOADING (manual review)     │             │
│     │ • Risk Score 750–899 → REFER to senior underwriter              │             │
│     │ • Risk Score ≥ 900 → DECLINE or exceptional approval route      │             │
│     └─────────────────────────────────────────────────────────────────┘             │
│         │                                                                           │
│         ▼                                                                           │
│  5. OUTPUT & ISSUANCE                                                               │
│     ┌─────────────────────────────────────────────────────────────────┐             │
│     │ • Auto-accepted: Generate policy document, issue immediately     │             │
│     │ • Accepted with loading: Generate conditional offer              │             │
│     │ • Referred: Send to underwriter queue with SHAP explanation     │             │
│     │ • Declined: Generate rejection letter with explanation          │             │
│     └─────────────────────────────────────────────────────────────────┘             │
│                                                                                       │
├──────────────────────────────────────────────────────────────────────────────────────┤
│  MONITORING LOOP: Track model drift, feature stability, and outcome feedback         │
│  Retrain frequency: Monthly for high-volume products, quarterly for standard         │
└──────────────────────────────────────────────────────────────────────────────────────┘

7.2 Referral Rules — When to Involve Humans

A fully automated underwriting system that never refers cases to humans is not the goal. The goal is a well-calibrated referral system that sends the right cases to humans — the ones where the model's confidence is low, the risk score is high, or the application data is unusual. Standard referral rules include:

7.3 Monitoring and Retraining

An underwriting model that is not monitored is a risk that grows silently over time. The monitoring system should track:

Retraining frequency depends on the volatility of the underlying risk environment. For motor insurance (stable, high volume), monthly retraining is standard. For health insurance (evolving claim patterns, medical inflation), quarterly retraining may be sufficient. For property insurance in climate-sensitive zones, retrain after any significant catastrophe event — the pre-event data may no longer be relevant.

💡
Pro Tip: The most common failure in production underwriting models is not the model itself — it is the data pipeline. A production model that scores with 99.9% uptime but uses stale credit data (3 months old instead of current) or a misaligned feature encoder (new categories were added to policy_type that the model was not trained on) will produce silently wrong scores. Before investing in model improvements, invest in monitoring the data pipeline: automated alerts for missing features, out-of-range values, and distribution shifts. A simple model with a reliable data pipeline beats a complex model with a broken pipeline every time.

Hands-On Project: Build an Automated Underwriting Risk Scorer

You are an insurance data scientist at "SureGuard Insurance," a mid-size general insurer. The company currently underwrites motor and health policies using manual guidelines. Your task is to build an automated underwriting risk scoring engine that: (a) accepts application data, (b) computes engineered features, (c) produces a risk score (0–1000), (d) maps it to a premium recommendation, and (e) provides an explainability report for each decision.

Steps

  1. Build the feature engineering pipeline (from Section 2): Create at least 8 engineered features from the raw Customer + Policy + Claims data. Document why each feature is likely to be predictive.
  2. Train a Random Forest model (from Section 3) with the engineered features. Use class_weight='balanced' and tune max_depth and min_samples_leaf using 3-fold cross-validation.
  3. Evaluate the model: Report ROC-AUC, confusion matrix, and calibration (decile-level comparison of predicted vs. actual claim rates).
  4. Build the premium recommendation engine (from Section 5): Define a loading schedule that maps risk scores to premium adjustments. Apply it to the entire portfolio and calculate the aggregate premium impact.
  5. Create the explainability report (from Section 6): For ONE specific policy that was assigned a "High" or "Very High" risk score, provide: the top 3 features driving the score, their values, and a one-sentence explanation of why each contributed to the high risk score.
  6. Design the workflow: Document the end-to-end workflow (from Section 7) with clear rules for: auto-accept, accept-with-loading, refer-to-underwriter, and decline. Specify the risk score thresholds and the monitoring metrics.
  7. Write a 500-word executive summary for the Chief Underwriting Officer covering: (a) Model performance summary, (b) Portfolio impact (how much premium would change under the new model?), (c) Implementation recommendations (go-live approach — big bang or phased?), and (d) Key risks and mitigation strategies.
View Solution / Walkthrough

Executive Summary (Sample)

To: Chief Underwriting Officer, SureGuard Insurance
From: Analytics — Underwriting AI Project
Subject: Automated Underwriting Model — Development Results and Implementation Plan

Model Performance Summary:
The Random Forest underwriting model achieves a ROC-AUC of 0.76 on the test set, representing moderate-to-strong discriminatory power. The model correctly identifies 68% of future claim-filers in the top 40% of risk scores. Calibration is within acceptable range (mean absolute calibration error: 2.8%). The top three risk drivers are: credit score (23% of feature importance), premium density (18%), and policy type — motor TP (14%). These findings are consistent with our actuarial experience and should be accepted by the underwriting team.

Portfolio Impact (this lab's computed output, not an industry-wide finding):
Applying the premium loading schedule to our synthetic portfolio would result in: 28% of policies receiving a discount (average −18%), 45% remaining at standard rates (±5%), 20% receiving a loading of 10–25%, and 7% requiring referral or decline. The net portfolio premium impact is an increase of approximately 4.2%, driven entirely by the highest-risk 15% of the portfolio being priced more accurately. Importantly, without the AI model, these high-risk policies are currently under-priced by an estimated ₹8.5 crore annually in this dataset — this is the value the model captures.

Implementation Recommendation — Phased Rollout:
We recommend a three-phase rollout. Phase 1 (Month 1–2): Shadow mode. The model scores every application but decisions continue to be made by human underwriters. Compare model recommendations to actual decisions. Track the recommendation acceptance rate. Build trust. Phase 2 (Month 3–4): Assisted mode. Auto-accept the "Very Low" and "Low" risk tiers (approximately 40% of applications) without human review, while all other tiers continue to be reviewed by underwriters who can see — and override — the model's recommendation. Measure the override rate and reasons. Phase 3 (Month 5+): Full STP. Extend auto-accept to the "Moderate" tier (now covering ~75% of applications). The remaining 25% are referred with a model-generated explanation package for the underwriter. Automated monitoring alerts on risk score drift, feature drift, and tier-level loss ratios.

Key Risks and Mitigation:
The primary risk is model drift — the policyholder population may change in ways the model was not trained on. Mitigation: weekly risk score distribution monitoring, monthly retraining schedule, and automated alerts if loss ratios by risk tier deviate by more than 5 percentage points from expected. The secondary risk is underwriter resistance — underwriters may not trust or may override the model. Mitigation: Phase 1 shadow mode builds trust by showing the model's recommendations without forcing adoption. Underwriters are trained on how the model works, what drives its decisions, and how to override it correctly. Model explainability (SHAP values) is built into the underwriter interface so they see not just the score but the reasons behind it.

Resource requirement: One data engineer (pipelines and monitoring) and one data scientist (monthly retraining and enhancements) for the first 6 months. Estimated annual cost: ₹35 lakh. Estimated benefit capture: ₹8.5 crore from better pricing of high-risk policies, plus operational savings from 75% STP. Projected ROI: ~15:1 in Year 1.

Key Takeaways

1

AI underwriting replaces subjective, inconsistent human judgment with data-driven, repeatable risk assessment. The role of the underwriter shifts from individual decisions to setting risk appetite, handling complex referrals, and monitoring model performance.

2

Feature engineering determines model performance more than algorithm choice. Ratios (risk/income, premium density) and interactions (age × channel) capture the non-linear patterns that human underwriters apply intuitively. Spend 60% of development time here.

3

Random Forest is the standard algorithm for underwriting models because it handles mixed data types, captures non-linear relationships, ranks feature importance, and is robust to outliers. Hyperparameter tuning (depth, leaf size, class weight) is essential for production quality.

4

A calibrated risk score (0–1000) has a specific meaning: policies with score X should have approximately X/1000 actual claim rate. Calibration errors above 5% mean the model is systematically over- or under-stating risk and must be corrected before deployment.

5

Model explainability is becoming a regulatory requirement in insurance. SHAP values provide per-prediction explanations — which features drove the decision and by how much. An underwriting model without explainability is a liability, regardless of its accuracy.

References

  1. National Association of Insurance Commissioners (NAIC), "Model Bulletin: Use of Artificial Intelligence Systems by Insurers," NAIC, Kansas City, MO, USA, 2023. [Online]. Available: https://content.naic.org/sites/default/files/call_materials/REGULATORY%20GUIDANCE%20PCKG%206-3-24.pdf
  2. MDPI Risks, "Algorithmic Bias Under the EU AI Act: Compliance Risk, Capital Strain, and Pricing Distortions in Life and Health Insurance Underwriting," Risks, vol. 13, no. 9, art. 160, 2025. [Online]. Available: https://www.mdpi.com/2227-9091/13/9/160
  3. Insurance Business Magazine (Asia), "India's insurance regulator steps in to govern AI adoption," June 2026. [Online]. Available: https://www.insurancebusinessmag.com/asia/news/technology/indias-insurance-regulator-steps-in-to-govern-ai-adoption-579846.aspx

Required reading (Task G5): MDPI Risks [2] is the finance-anchor academic reading for this session — it connects underwriting-model bias to capital strain and pricing distortion under the EU AI Act, and is peer-reviewed (Risks is an MDPI journal).

Test Your Understanding

1. The most important determinant of an underwriting model's predictive performance is:

2. A policyholder receives a risk score of 720. According to the underwriting workflow in Section 7, the appropriate action is:

3. A well-calibrated risk scoring model should satisfy the following condition:

4. Why is the SHAP value explainability framework particularly important for AI-driven underwriting in insurance?

5. In the three-phase rollout strategy for an AI underwriting model, the CORRECT sequence is: