ML in Production & Ethics

Deploying a model is the beginning of its lifecycle, not the end. Senior Manager DS at Capital One are expected to design monitoring pipelines, respond to model degradation, ensure fair lending compliance, and shepherd models through SR 11-7 independent validation — all while keeping business stakeholders informed.

Key Takeaways

  • Data drift = input distribution changed; concept drift = relationship between inputs and outcome changed
  • PSI < 0.10: stable; 0.10–0.25: investigate; ≥ 0.25: retrain
  • SHAP values are the industry standard for explaining individual predictions — know TreeExplainer for XGBoost/RF
  • Disparate impact ratio < 0.80 (the "4/5 rule") triggers fair lending review under ECOA
  • SR 11-7 requires independent model validation and ongoing monitoring for all Capital One models
  • Champion/Challenger: route a small % of traffic to the new model and compare live performance before full promotion

1. Model Monitoring: Drift Detection

Models degrade in production because the world changes. Two distinct types of drift require different responses.

Drift Type What Changed Detection Method Response
Data drift (covariate shift) Input feature distribution $P(X)$ changed PSI on each feature; KS test; Jensen-Shannon divergence Retrain on recent data; investigate pipeline
Concept drift Relationship $P(Y|X)$ changed Gini / AUC degradation; calibration curve shift; SHAP importance drift Redesign features; retrain; consider new model architecture
Label drift Outcome base rate changed Track positive rate over time Re-calibrate threshold; retrain if persistent

Population Stability Index (PSI)

PSI quantifies how much a feature's distribution has shifted between training time and scoring time:

$$PSI = \sum_{i=1}^{n} \left( P_{\text{score},i} - P_{\text{train},i} \right) \times \ln\!\left(\frac{P_{\text{score},i}}{P_{\text{train},i}}\right)$$

Thresholds: PSI < 0.10 — stable; 0.10–0.25 — investigate; ≥ 0.25 — retrain or escalate.

import numpy as np

def psi(expected: np.ndarray, actual: np.ndarray, n_bins: int = 10) -> float:
    """
    Compute Population Stability Index.
    expected: training-time distribution (1-D array of values or bin proportions)
    actual:   scoring-time distribution
    """
    # Bin using training-time quantiles
    breakpoints = np.percentile(expected, np.linspace(0, 100, n_bins + 1))
    breakpoints[0]  = -np.inf
    breakpoints[-1] =  np.inf

    expected_pcts = np.histogram(expected, bins=breakpoints)[0] / len(expected)
    actual_pcts   = np.histogram(actual,   bins=breakpoints)[0] / len(actual)

    # Clip to avoid log(0)
    expected_pcts = np.clip(expected_pcts, 1e-6, None)
    actual_pcts   = np.clip(actual_pcts,   1e-6, None)

    return float(np.sum((actual_pcts - expected_pcts) * np.log(actual_pcts / expected_pcts)))

# Usage
psi_income = psi(train_df['annual_income'].values, score_df['annual_income'].values)
print(f"PSI (annual_income): {psi_income:.4f}")
if psi_income >= 0.25:
    print("WARNING: significant drift — consider retraining")
elif psi_income >= 0.10:
    print("CAUTION: moderate drift — investigate")
else:
    print("OK: distribution is stable")

2. Model Explainability with SHAP

SHAP (SHapley Additive exPlanations) decomposes any model's prediction into the marginal contribution of each feature, grounded in cooperative game theory. It is the industry standard for both global model explanation and individual prediction explanation in regulated industries.

SHAP Explainer Model Type Speed
TreeExplainer XGBoost, LightGBM, Random Forest, decision trees Very fast — exact computation via tree traversal
LinearExplainer Logistic regression, linear SVM Fast — closed-form solution
KernelExplainer Any model (model-agnostic) Slow — sampling-based approximation
import shap
import xgboost as xgb
import pandas as pd
import matplotlib.pyplot as plt

# Train model
model = xgb.XGBClassifier(n_estimators=200, max_depth=4, random_state=42)
model.fit(X_train, y_train)

# Create explainer and compute SHAP values
explainer   = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)   # shape: (n_samples, n_features)

# --- Global explanation: summary plot ---
shap.summary_plot(shap_values, X_test, plot_type='bar')   # feature importance
shap.summary_plot(shap_values, X_test)                    # beeswarm (direction + magnitude)

# --- Individual explanation: waterfall / force plot ---
sample_idx = 0
shap.waterfall_plot(shap.Explanation(
    values    = shap_values[sample_idx],
    base_values = explainer.expected_value,
    data      = X_test.iloc[sample_idx],
    feature_names = X_test.columns.tolist()
))

# --- SHAP drift monitoring ---
# Average absolute SHAP value per feature — track over time
mean_shap_train = np.abs(explainer.shap_values(X_train)).mean(axis=0)
mean_shap_score = np.abs(shap_values).mean(axis=0)
shap_drift = pd.DataFrame({
    'feature':    X_test.columns,
    'shap_train': mean_shap_train,
    'shap_score': mean_shap_score,
    'ratio':      mean_shap_score / np.clip(mean_shap_train, 1e-8, None)
}).sort_values('ratio', ascending=False)
print(shap_drift.head(10))

3. Fairness and Fair Lending

Under ECOA (Equal Credit Opportunity Act) and the Fair Housing Act, Capital One credit models must not have a disparate impact on protected classes (race, gender, national origin, age, etc.) or treat applicants differently based on protected characteristics (disparate treatment).

Disparate Impact Ratio (4/5 Rule)

The approval rate for a protected group divided by the approval rate for the most-approved group. A ratio below 0.80 triggers fair lending review.

$$DIR = \frac{\text{Approval Rate (protected group)}}{\text{Approval Rate (reference group)}} \geq 0.80$$
import pandas as pd

def disparate_impact_ratio(df: pd.DataFrame,
                            decision_col: str,
                            group_col: str,
                            reference_group: str) -> pd.DataFrame:
    """Compute DIR for all groups relative to the reference group."""
    approval_rates = df.groupby(group_col)[decision_col].mean()
    ref_rate       = approval_rates[reference_group]
    dir_table      = (approval_rates / ref_rate).reset_index()
    dir_table.columns = [group_col, 'DIR']
    dir_table['flag'] = dir_table['DIR'].apply(
        lambda x: 'REVIEW REQUIRED' if x < 0.80 else 'OK'
    )
    return dir_table.sort_values('DIR')

result = disparate_impact_ratio(
    df            = decisions_df,
    decision_col  = 'approved',
    group_col     = 'race_category',
    reference_group = 'White'
)
print(result)

Fairness Metrics and the Impossibility Theorem

Metric Definition Trade-off
Equal Opportunity Equal TPR (Recall) across groups May allow unequal FPR
Equalized Odds Equal TPR AND FPR across groups Stricter; often infeasible when base rates differ
Calibration Predicted probabilities equal observed rates within groups Incompatible with equal opportunity when base rates differ (Chouldechova)
Disparate Impact Approval rate ratio ≥ 0.80 Legal standard under ECOA; does not guarantee equal error rates

Chouldechova's impossibility theorem: When base rates differ across groups, it is mathematically impossible to simultaneously satisfy calibration, equal opportunity, and equalized odds. Capital One must choose which criterion to prioritize based on legal requirements and business context — and document the choice explicitly for SR 11-7.

4. Champion/Challenger and Deployment

Champion/Challenger (C/C) is the standard production deployment strategy for high-stakes ML models at Capital One. It routes a controlled percentage of live traffic to the challenger model while the champion handles the remainder, enabling real-world performance comparison before full promotion.

Strategy Traffic Split Best For
Champion/Challenger 90% / 10% (typical) Validating a new model with live outcomes; lagged labels (default takes 12+ months)
A/B Test 50% / 50% Fast outcomes; acceptable to expose 50% of customers to the new model
Shadow mode 100% / 0% (no impact) Validating data pipeline and score distribution before going live
Canary release 1–5% / 95–99% High-risk changes; minimize exposure to unproven behavior

SMOTE and Data Leakage

SMOTE (Synthetic Minority Over-sampling Technique) generates synthetic minority class samples by interpolating between existing minority samples. A critical error is applying SMOTE before train/test split.

Why this causes leakage: Synthetic samples are interpolations of real samples. If applied to the full dataset before splitting, synthetic test samples may be near-neighbors of real training samples — the model has effectively seen the test distribution during training, producing optimistically biased evaluation metrics.

from sklearn.model_selection import train_test_split
from imblearn.over_sampling import SMOTE
import xgboost as xgb
from sklearn.metrics import roc_auc_score

# CORRECT: split first, then apply SMOTE only to training data
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

smote = SMOTE(random_state=42, k_neighbors=5)
X_train_res, y_train_res = smote.fit_resample(X_train, y_train)
# X_test is never touched by SMOTE — it reflects real-world distribution

model = xgb.XGBClassifier(n_estimators=200, random_state=42, eval_metric='logloss')
model.fit(X_train_res, y_train_res,
          eval_set=[(X_test, y_test)],
          verbose=False)

auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1])
print(f"AUC on unmodified test set: {auc:.4f}")

# WRONG (never do this):
# smote = SMOTE(random_state=42)
# X_res, y_res = smote.fit_resample(X, y)          # leaks test distribution
# X_train, X_test, ... = train_test_split(X_res, y_res, ...)  # too late

Practice Questions

Q1: Your credit risk model's Gini coefficient has dropped from 0.65 to 0.52 over 6 months. What are the possible causes and how do you investigate?

Two primary causes:

  1. Concept drift — the relationship between features and default has changed (e.g., new economic policy changed credit behavior). Investigate by checking if calibration has degraded and whether SHAP feature importances have shifted relative to training-time importances.
  2. Covariate shift — input feature distributions have changed (new applicant segments, policy changes, data pipeline issues). Check PSI for all features; flag any with PSI ≥ 0.10.

Additional causes to rule out: data pipeline issues (feature values are wrong due to upstream changes), label definition changed, population mix shifted (different product or channel mix).

Remediation path: (1) If PSI flags specific features → investigate data pipeline; (2) If calibration degraded without PSI flags → concept drift likely → retrain on recent data; (3) If structural break (recession, policy change) → consider redesigning the model with new relevant features.

Q2: What is the difference between equal opportunity and equalized odds as fairness metrics?

Equal Opportunity requires equal True Positive Rate (TPR / Recall) across protected groups — both groups have the same probability of being correctly classified as positive given they are truly positive.

Equalized Odds is stricter: it requires equal TPR and equal FPR across groups. It constrains both types of errors simultaneously.

Chouldechova's impossibility theorem proves these metrics cannot all be satisfied simultaneously when base rates differ across groups. If the default rate differs between demographic groups (which it does in credit), satisfying calibration (predicted probabilities match observed rates within groups) makes it mathematically impossible to achieve equal opportunity and equalized odds simultaneously.

Capital One must choose which fairness criterion to prioritize based on legal requirements (ECOA, fair lending) and business context — and document the rationale explicitly for SR 11-7 independent validation.

Q3: Describe the champion/challenger framework. When would you use it instead of a full A/B test?

Champion/Challenger routes a small percentage of live traffic (e.g., 5–10%) to the challenger model while the champion handles the remaining traffic. Both models' decisions are tracked for business outcomes.

Use champion/challenger instead of A/B testing when:

  1. You need to validate a new model with real business outcomes before committing, but cannot justify exposing 50% of customers to an unproven model.
  2. The outcome variable is lagged — credit defaults take 12+ months to materialize; C/C lets you collect early performance signals without waiting for full A/B results.
  3. You want continuous model improvement as an ongoing process rather than a discrete one-time comparison.
  4. Regulatory requirements (SR 11-7) require live validation evidence before full deployment of a new model.
Q4: Explain why you should apply SMOTE only to the training set and never before train/test split.

SMOTE generates synthetic minority samples by interpolating between existing minority samples using k-nearest neighbors. If applied before splitting:

  1. Synthetic samples may be near-neighbors of real test samples — the model has effectively "seen" the test distribution during training.
  2. Evaluation metrics become optimistically biased — the model appears to generalize better than it actually will on truly unseen data.
  3. The test set no longer represents the real-world distribution, defeating its purpose as an honest estimate of production performance.

Correct procedure:

  1. Split into train/test first (stratify on the target to preserve class ratios).
  2. Apply SMOTE only to the training set.
  3. Evaluate on the original, unmodified test set — this is the only honest estimate of real-world performance.

Action Items

  • Implement the PSI function from scratch, apply it to a real feature, and interpret the result using the 0.10/0.25 thresholds.
  • Fit an XGBoost model and generate a SHAP waterfall plot for the 5 highest-risk predictions — explain each in plain English.
  • Compute disparate impact ratios for a sample credit decision dataset across at least two demographic groups; flag any groups below 0.80.
  • Implement a correct SMOTE pipeline (split first, SMOTE on train only) and compare AUC to the incorrect order (SMOTE first) — document the difference.