Readable Models and Stories About Models
Interpretability against explainability, and the accuracy gap between them
Week 11 ended on a system refusing somebody. The obvious next question is why, and for most modern models that question is much harder to answer than it sounds.
import pandas as pd
from sklearn.model_selection import train_test_split
rng = np.random.RandomState(0)
n = 4000
history = np.clip(rng.normal(6, 2.5, n), 0, None)
income = rng.normal(32, 9, n)
missed = rng.poisson(1.1, n)
noise = rng.normal(0, 1, n)
# a copy of income, to show what correlated features do to explanations
salary = income + rng.normal(0, 1.0, n)
risk = (-1.4 * missed + 0.06 * income + 0.10 * history
+ rng.normal(0, 0.8, n))
repays = (risk > np.median(risk)).astype(int)
df = pd.DataFrame({'history': history, 'income': income,
'salary': salary, 'missed': missed,
'noise': noise, 'repays': repays})
FEATURES = ['history', 'income', 'salary', 'missed', 'noise']
train, test = train_test_split(df, test_size=0.3, random_state=0,
stratify=df['repays'])
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
forest = RandomForestClassifier(n_estimators=250, random_state=0,
min_samples_leaf=3)
forest.fit(train[FEATURES], train['repays'])
linear = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
linear.fit(train[FEATURES], train['repays'])
print('%-24s %10s' % ('', 'accuracy'))
print('%-24s %10.4f' % ('logistic regression',
linear.score(test[FEATURES], test['repays'])))
print('%-24s %10.4f' % ('random forest',
forest.score(test[FEATURES], test['repays'])))
print()
print('the forest contains %d trees' % len(forest.estimators_))
print('the first has %d nodes'
% forest.estimators_[0].tree_.node_count)
logistic regression 0.8442
random forest 0.8442
the forest contains 250 trees
the first has 489 nodes
Two models at similar accuracy. One is five numbers you can print. The other is two hundred and fifty trees with thousands of nodes each, and no person will ever read it. When the accuracies are this close, the readable one is often the better engineering choice for reasons that have nothing to do with accuracy.
The readable model, read
import pandas as pd
from sklearn.model_selection import train_test_split
rng = np.random.RandomState(0)
n = 4000
history = np.clip(rng.normal(6, 2.5, n), 0, None)
income = rng.normal(32, 9, n)
missed = rng.poisson(1.1, n)
noise = rng.normal(0, 1, n)
# a copy of income, to show what correlated features do to explanations
salary = income + rng.normal(0, 1.0, n)
risk = (-1.4 * missed + 0.06 * income + 0.10 * history
+ rng.normal(0, 0.8, n))
repays = (risk > np.median(risk)).astype(int)
df = pd.DataFrame({'history': history, 'income': income,
'salary': salary, 'missed': missed,
'noise': noise, 'repays': repays})
FEATURES = ['history', 'income', 'salary', 'missed', 'noise']
train, test = train_test_split(df, test_size=0.3, random_state=0,
stratify=df['repays'])
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
forest = RandomForestClassifier(n_estimators=250, random_state=0,
min_samples_leaf=3)
forest.fit(train[FEATURES], train['repays'])
linear = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000))
linear.fit(train[FEATURES], train['repays'])
import numpy as np
clf = linear.named_steps['logisticregression']
for name, w in sorted(zip(FEATURES, clf.coef_[0]),
key=lambda t: -abs(t[1])):
print('%-10s %+8.3f' % (name, w))
print()
print('these are the whole model: multiply, add, done')
income +1.107
history +0.531
salary +0.114
noise +0.021
these are the whole model: multiply, add, done