Step 1: What the model leans on
Harbour Credit Union's data team trained a default model on 6,000 past personal loans. It scores an AUC of 0.997, better than any credit model has a right to be. Before anyone uses it, find out what it relies on.
Permutation importance asks one question per input: shuffle this column, breaking its link to the outcome, and how much worse does the model get? If the model depends on a column, the score drops. It works on any model, because it only needs predictions.
1. Write permutation_importance() in explain.py.
2. Run it on the model trained with every column.
explain.py, the file you edit100 lines
"""Explaining Harbour Credit Union's loan-default model. You write the functions marked TODO, one step at a time."""
from itertools import combinations
from math import factorial
import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
LABEL = "defaulted"
def load():
"""(train, valid): a stratified 70/30 split of loans.csv."""
df = pd.read_csv("loans.csv")
return train_test_split(df, test_size=0.3, random_state=0, stratify=df[LABEL])
def fit(train, features):
return HistGradientBoostingClassifier(random_state=0).fit(train[features], train[LABEL])
def auc(model, data, features):
return float(roc_auc_score(data[LABEL], model.predict_proba(data[features])[:, 1]))
# ---------- Step 1: what does the model lean on ----------
def permutation_importance(model, data, features, repeats=5, seed=0):
"""{feature: mean drop in AUC} when that feature's column is shuffled (rng.permutation, rng =
np.random.default_rng(seed), one shared generator), `repeats` times, the other columns left as they are."""
# TODO (Step 1): base AUC; for each feature, `repeats` times: copy data, shuffle that column
# with rng.permutation, AUC again; the mean drop per feature.
raise NotImplementedError("Step 1: write permutation_importance()")
# ---------- Step 2: a feature from the future ----------
def usable_features(dictionary, decision_point="application"):
"""The columns recorded at the decision point, in dictionary order: what exists when the loan is decided."""
raise NotImplementedError("usable_features() arrives in Step 2")
def single_feature_auc(data, column):
"""How well one column alone separates defaults: roc_auc_score of the raw values, flipped to max(auc, 1 - auc)."""
raise NotImplementedError("single_feature_auc() arrives in Step 2")
# ---------- Step 3: how each input moves the prediction ----------
def ice(model, X, feature, grid):
"""Individual conditional expectation: an array (rows of X, len(grid)) with each row's predicted default
probability when `feature` is set to each grid value and everything else stays as it is."""
raise NotImplementedError("ice() arrives in Step 3")
def partial_dependence(model, X, feature, grid):
"""The average of ice() over the rows: one mean probability per grid value."""
raise NotImplementedError("partial_dependence() arrives in Step 3")
def direction(values, tol=0.005):
""""up" if every step rises by more than -tol and the curve ends higher than it starts, "down" for the mirror
image, else "mixed"."""
raise NotImplementedError("direction() arrives in Step 3")
# ---------- Step 4: Shapley values, exactly ----------
def coalition_value(model, x, background, S):
"""The mean predicted probability over the background rows after setting the columns in S (a tuple of column
positions) to the applicant x's values."""
raise NotImplementedError("coalition_value() arrives in Step 4")
def shapley_values(model, x, background):
"""One value per column: the average, over every coalition S of the other columns, of v(S + {j}) - v(S),
weighted |S|! (n - |S| - 1)! / n!."""
raise NotImplementedError("shapley_values() arrives in Step 4")
# ---------- Step 5: reasons an applicant can act on ----------
REASONS = {
"income": "Income is low for the amount requested",
"debt_to_income": "Existing debt payments are high compared with income",
"loan_amount": "The amount requested is high",
"term_months": "The repayment term is long",
"credit_history_years": "Credit history is short",
"late_payments_12m": "Recent late payments on other credit",
"employment_years": "Time with current employer is short",
}
NOT_STATED = "Employment history was not provided"
def reason_codes(phi, x, features, k=3):
"""The texts of the (at most k) features that pushed this applicant's risk UP the most (positive Shapley value),
largest first. employment_years at 0 means "not stated": use NOT_STATED for it then."""
raise NotImplementedError("reason_codes() arrives in Step 5")data_dictionary.csvloans.csvtry_it.py