Chapter 96 · Solutions
Chapter 96 · Solutions
Five challenges, each verified in code.
Solutions to the five challenges from Chapter 96, with statsmodels and scikit-learn.
In [1]:
import numpy as np, pandas as pd, matplotlib.pyplot as plt
from scipy import stats
import seaborn as sns # seaborn = high-level statistical plots (heatmaps, regplots, pairplots)
import statsmodels.api as sm
from statsmodels.formula.api import ols
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.nonparametric.smoothers_lowess import lowess
from sklearn.linear_model import LinearRegression, Ridge, Lasso, LogisticRegression
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import r2_score, mean_squared_error
ORG="#ea580c"; DEEP="#c2410c"; LIGHT="#fdba74"; INK="#1a2138"; GRID="#e6e9f2"; GREEN="#059669"; RED="#ef4444"; AMBER="#d97706"; BLUE="#2563eb"; PUR="#9333ea"; GREY="#94a3b8"; SLATE="#475569"
plt.rcParams.update({"figure.facecolor":"white","axes.facecolor":"white","figure.dpi":110,"font.size":11,
"axes.edgecolor":GRID,"axes.grid":True,"grid.color":GRID,"axes.axisbelow":True,"axes.spines.top":False,
"axes.spines.right":False,"axes.titlesize":12,"axes.titleweight":"bold","legend.frameon":False})
sns.set_style("whitegrid")
BASE="https://raw.githubusercontent.com/johnfisher-ai/Statistics-Data-Science-AI-Visual-Book/main/data/"
In [2]:
import warnings; warnings.filterwarnings('ignore')
import statsmodels.formula.api as smf
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (roc_auc_score, roc_curve, confusion_matrix, classification_report, recall_score, RocCurveDisplay)
try:
loans = pd.read_excel('../../data/logistic-regression--loans.xlsx', sheet_name='Loans')
except FileNotFoundError:
loans = pd.read_excel(BASE + 'logistic-regression--loans.xlsx', sheet_name='Loans')
CHALLENGE 1
Fit and read an odds ratio
credit-score OR per 50 points.
In [3]:
m = smf.logit('default ~ credit_score + dti', loans).fit(disp=0)
orr = np.exp(m.params['credit_score']*50)
print(f'Each +50 credit points multiplies the odds of default by {orr:.2f} (a {100*(1-orr):.0f}% reduction).')
Each +50 credit points multiplies the odds of default by 0.40 (a 60% reduction).
CHALLENGE 2
Confusion matrix at 0.5
accuracy, precision, recall.
In [4]:
p = m.predict(loans); yhat = (p>=0.5).astype(int)
print(confusion_matrix(loans.default, yhat))
print(classification_report(loans.default, yhat, digits=2, target_names=['repaid','default']))
[[267 30]
[ 55 68]]
precision recall f1-score support
repaid 0.83 0.90 0.86 297
default 0.69 0.55 0.62 123
accuracy 0.80 420
macro avg 0.76 0.73 0.74 420
weighted avg 0.79 0.80 0.79 420
CHALLENGE 3
ROC, AUC, better threshold
threshold for >=75% recall.
In [5]:
auc = roc_auc_score(loans.default, p); print(f'AUC = {auc:.3f}')
fpr,tpr,thr = roc_curve(loans.default, p)
i = np.where(tpr>=0.75)[0][0]
print(f'A threshold of ~{thr[i]:.2f} catches {tpr[i]:.0%} of defaults (FPR {fpr[i]:.0%}).')
AUC = 0.819 A threshold of ~0.31 catches 76% of defaults (FPR 27%).
CHALLENGE 4
The accuracy paradox
baseline vs class-weighted recall.
In [6]:
print(f'predict-all-repaid accuracy = {(loans.default==0).mean():.2f}')
X = loans[['credit_score','dti','loan_amount']]; y = loans.default
bal = LogisticRegression(max_iter=1000, class_weight="balanced").fit(X,y)
print(f'balanced recall on defaults = {recall_score(y, bal.predict(X)):.2f}')
predict-all-repaid accuracy = 0.71 balanced recall on defaults = 0.78
CHALLENGE 5
Break it: perfect separation
statsmodels fails, penalized logistic is fine.
In [7]:
sep = loans.copy(); sep['leak'] = sep['default']*10 + np.random.default_rng(1).normal(0,0.01,len(sep))
try:
smf.logit('default ~ leak', sep).fit(disp=0, maxiter=100); print('converged (unexpected)')
except Exception as e:
print('MLE fails on separation:', type(e).__name__)
reg = LogisticRegression(max_iter=1000).fit(sep[['leak']], sep.default)
print(f'regularized coefficient stays finite: {reg.coef_[0][0]:.2f}')
converged (unexpected) regularized coefficient stays finite: 1.43
Statistics, Data Science and AI: A Visual Handbook · © 2026 John Fisher