Chapter 5: Classification

  1. 2019-2026 Peter C. Bruce, Andrew Bruce, Peter Gedeck
# from statsmodels.genmod.generalized_linear_model import GLMResults
from imblearn.over_sampling import SMOTE, ADASYN
from sklearn.compose import ColumnTransformer
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_score, recall_score
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, KBinsDiscretizer
from sklearn.preprocessing import OrdinalEncoder
from sklearn.tree import DecisionTreeClassifier
import matplotlib.pyplot as plt
import mlba
import numpy as np
import pandas as pd
import pygam
import random
import seaborn as sns
import statsmodels.api as sm
import statsmodels.formula.api as smf

random.seed(123)

# Location of the data files. Adjust this path if you keep the data
# files in a different directory.
from pathlib import Path
DATA_DIR = Path('../data')

Classification

Naive Bayes

The Naive Solution

loan_data = pd.read_csv(DATA_DIR / "loan_data.csv.gz")

predictors = ["purpose_", "home_", "emp_len_"]
outcome = "outcome"
X = pd.get_dummies(loan_data[predictors], prefix="", prefix_sep="", dtype=int)
y = loan_data[outcome]

naive_model = MultinomialNB(alpha=0.01, fit_prior=True)
naive_model.fit(X, y)
new_loan = X.loc[146:146, :]
print("predicted class: ", naive_model.predict(new_loan)[0])

probabilities = pd.DataFrame(naive_model.predict_proba(new_loan),
                             columns=naive_model.classes_)
print("predicted probabilities", probabilities)
predicted class:  default
predicted probabilities     default  paid off
0  0.653696  0.346304

Numeric Predictor Variables

num_cols = ["borrower_score", "payment_inc_ratio"]
cat_cols = ["purpose_", "home_", "emp_len_"]
mixed_naive_model = Pipeline([
    ("pre", ColumnTransformer([
        ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
        ("num", KBinsDiscretizer(n_bins=5, strategy="uniform"), num_cols),
    ])),
    ("clf", MultinomialNB()),
])
X = loan_data[cat_cols + num_cols]
mixed_naive_model.fit(X, loan_data["outcome"])
Pipeline(steps=[('pre',
                 ColumnTransformer(transformers=[('cat',
                                                  OneHotEncoder(handle_unknown='ignore'),
                                                  ['purpose_', 'home_',
                                                   'emp_len_']),
                                                 ('num',
                                                  KBinsDiscretizer(strategy='uniform'),
                                                  ['borrower_score',
                                                   'payment_inc_ratio'])])),
                ('clf', MultinomialNB())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Discriminant Analysis

A Simple Example

loan3000 = pd.read_csv(DATA_DIR / "loan3000.csv")

loan3000.outcome = loan3000.outcome.astype("category")

predictors = ["borrower_score", "payment_inc_ratio"]
outcome = "outcome"

X = loan3000[predictors]
y = loan3000[outcome]

loan_lda = LinearDiscriminantAnalysis()
loan_lda.fit(X, y)
pd.DataFrame(loan_lda.scalings_, index=X.columns)
0
borrower_score 7.175839
payment_inc_ratio -0.099676
pred = pd.DataFrame(loan_lda.predict_proba(loan3000[predictors]),
                    columns=loan_lda.classes_)
pred.head()
default paid off
0 0.553544 0.446456
1 0.558953 0.441047
2 0.272696 0.727304
3 0.506254 0.493746
4 0.609952 0.390048
# Use scalings and center of means to determine decision boundary
center = np.mean(loan_lda.means_, axis=0)
slope = - loan_lda.scalings_[0] / loan_lda.scalings_[1]
intercept = center[1] - center[0] * slope

# payment_inc_ratio for borrower_score of 0 and 20
x_0 = (0 - intercept) / slope
x_20 = (20 - intercept) / slope

lda_df = pd.concat([loan3000, pred["default"]], axis=1)
lda_df.head()

fig, ax = plt.subplots(figsize=(4, 4))
g = sns.scatterplot(x="borrower_score", y="payment_inc_ratio",
                    hue="default", data=lda_df,
                    palette=sns.diverging_palette(240, 10, n=9, as_cmap=True),
                    ax=ax, legend=False)

ax.set_ylim(0, 20)
ax.set_xlim(0.15, 0.8)
ax.plot((x_0, x_20), (0, 20), linewidth=3)
ax.plot(*loan_lda.means_.transpose())

plt.tight_layout()
plt.show()

Logistic Regression

Logistic Regression and the GLM

predictors = ["payment_inc_ratio", "purpose_", "home_", "emp_len_",
              "borrower_score"]
outcome = "outcome"
X = pd.get_dummies(loan_data[predictors], prefix="", prefix_sep="",
                   drop_first=True, dtype=int)
y = loan_data[outcome]

logit_reg = LogisticRegression(C=np.inf, max_iter=1000)
logit_reg.fit(X, y)
/Users/petergedeck/cdd/practical-statistics-for-data-scientists-code-3e/.venv/lib/python3.13/site-packages/sklearn/linear_model/_logistic.py:1170: UserWarning: Setting penalty=None will ignore the C and l1_ratio parameters
  warnings.warn(
LogisticRegression(C=inf, max_iter=1000)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
print("intercept ", logit_reg.intercept_[0])
print("classes", logit_reg.classes_)
pd.DataFrame({"coeff": logit_reg.coef_[0]}, index=X.columns)
intercept  -1.6261242575653818
classes ['default' 'paid off']
coeff
payment_inc_ratio -0.079885
borrower_score 4.610744
debt_consolidation -0.250128
home_improvement -0.411826
major_purchase -0.228901
medical -0.528521
other -0.620839
small_business -1.222285
OWN -0.052460
RENT -0.159205
> 1 Year 0.349254

Predicted Values from Logistic Regression

pred = pd.DataFrame(logit_reg.predict_log_proba(X),
                    columns=logit_reg.classes_)
pred.describe()
default paid off
count 45342.000000 45342.000000
mean -0.758078 -0.760365
std 0.378317 0.390689
min -2.771842 -3.546430
25% -0.986107 -0.977608
50% -0.697477 -0.688836
75% -0.471941 -0.466850
max -0.029251 -0.064588
pred = pd.DataFrame(logit_reg.predict_proba(X),
                    columns=logit_reg.classes_)
pred.describe()
default paid off
count 45342.000000 45342.000000
mean 0.499934 0.500066
std 0.167427 0.167427
min 0.062547 0.028827
25% 0.373026 0.376210
50% 0.497840 0.502160
75% 0.623790 0.626974
max 0.971173 0.937453

Assessing the Model

y_numbers = [1 if yi == "default" else 0 for yi in y]
logit_reg_sm = sm.GLM(y_numbers, X.assign(const=1),
                      family=sm.families.Binomial())
logit_result = logit_reg_sm.fit()
print(logit_result.summary())
                 Generalized Linear Model Regression Results                  
==============================================================================
Dep. Variable:                      y   No. Observations:                45342
Model:                            GLM   Df Residuals:                    45330
Model Family:                Binomial   Df Model:                           11
Link Function:                  Logit   Scale:                          1.0000
Method:                          IRLS   Log-Likelihood:                -28757.
Date:                Sun, 31 May 2026   Deviance:                       57515.
Time:                        18:07:11   Pearson chi2:                 4.54e+04
No. Iterations:                     4   Pseudo R-squ. (CS):             0.1112
Covariance Type:            nonrobust                                         
======================================================================================
                         coef    std err          z      P>|z|      [0.025      0.975]
--------------------------------------------------------------------------------------
payment_inc_ratio      0.0797      0.002     32.058      0.000       0.075       0.085
borrower_score        -4.6126      0.084    -55.203      0.000      -4.776      -4.449
debt_consolidation     0.2494      0.028      9.030      0.000       0.195       0.303
home_improvement       0.4077      0.047      8.747      0.000       0.316       0.499
major_purchase         0.2296      0.054      4.277      0.000       0.124       0.335
medical                0.5105      0.087      5.882      0.000       0.340       0.681
other                  0.6207      0.039     15.738      0.000       0.543       0.698
small_business         1.2153      0.063     19.192      0.000       1.091       1.339
OWN                    0.0483      0.038      1.271      0.204      -0.026       0.123
RENT                   0.1573      0.021      7.420      0.000       0.116       0.199
 > 1 Year             -0.3567      0.053     -6.779      0.000      -0.460      -0.254
const                  1.6381      0.074     22.224      0.000       1.494       1.783
======================================================================================
formula = ("outcome ~ bs(payment_inc_ratio, df=4) + purpose_ + "
           "home_ + emp_len_ + bs(borrower_score, df=4)")
model = smf.glm(formula=formula, data=loan_data, family=sm.families.Binomial())
results = model.fit()
print(results.summary())
                             Generalized Linear Model Regression Results                             
=====================================================================================================
Dep. Variable:     ['outcome[default]', 'outcome[paid off]']   No. Observations:                45342
Model:                                                   GLM   Df Residuals:                    45324
Model Family:                                       Binomial   Df Model:                           17
Link Function:                                         Logit   Scale:                          1.0000
Method:                                                 IRLS   Log-Likelihood:                -28744.
Date:                                       Sun, 31 May 2026   Deviance:                       57487.
Time:                                               18:07:11   Pearson chi2:                 4.54e+04
No. Iterations:                                            5   Pseudo R-squ. (CS):             0.1117
Covariance Type:                                   nonrobust                                         
==================================================================================================
                                     coef    std err          z      P>|z|      [0.025      0.975]
--------------------------------------------------------------------------------------------------
Intercept                          1.8382      0.380      4.836      0.000       1.093       2.583
purpose_[T.debt_consolidation]     0.2491      0.028      9.020      0.000       0.195       0.303
purpose_[T.home_improvement]       0.4138      0.047      8.848      0.000       0.322       0.505
purpose_[T.major_purchase]         0.2401      0.054      4.450      0.000       0.134       0.346
purpose_[T.medical]                0.5183      0.087      5.953      0.000       0.348       0.689
purpose_[T.other]                  0.6295      0.040     15.812      0.000       0.552       0.708
purpose_[T.small_business]         1.2252      0.063     19.303      0.000       1.101       1.350
home_[T.OWN]                       0.0484      0.038      1.273      0.203      -0.026       0.123
home_[T.RENT]                      0.1581      0.021      7.456      0.000       0.117       0.200
emp_len_[T. > 1 Year]             -0.3541      0.053     -6.729      0.000      -0.457      -0.251
bs(payment_inc_ratio, df=4)[0]     0.0049      0.121      0.041      0.967      -0.232       0.241
bs(payment_inc_ratio, df=4)[1]     1.6033      0.142     11.289      0.000       1.325       1.882
bs(payment_inc_ratio, df=4)[2]     1.9033      0.488      3.900      0.000       0.947       2.860
bs(payment_inc_ratio, df=4)[3]    -0.8521      1.929     -0.442      0.659      -4.633       2.929
bs(borrower_score, df=4)[0]       -1.0045      0.476     -2.112      0.035      -1.937      -0.072
bs(borrower_score, df=4)[1]       -2.6411      0.287     -9.209      0.000      -3.203      -2.079
bs(borrower_score, df=4)[2]       -3.6984      0.473     -7.824      0.000      -4.625      -2.772
bs(borrower_score, df=4)[3]       -5.8564      0.525    -11.160      0.000      -6.885      -4.828
==================================================================================================
Analysis of residuals

Evaluating Classification Models

Confusion Matrix

pred = logit_reg.predict(X)
pred_y = logit_reg.predict(X) == "default"
true_y = y == "default"
true_pos = true_y & pred_y
true_neg = ~true_y & ~pred_y
false_pos = ~true_y & pred_y
false_neg = true_y & ~pred_y

conf_mat = pd.DataFrame([[np.sum(true_pos), np.sum(false_neg)],
                         [np.sum(false_pos), np.sum(true_neg)]],
                       index=["Y = default", "Y = paid off"],
                       columns=["Yhat = default", "Yhat = paid off"])
conf_mat
Yhat = default Yhat = paid off
Y = default 14321 8350
Y = paid off 8140 14531
print(confusion_matrix(y_true=y, y_pred=logit_reg.predict(X)))
[[14321  8350]
 [ 8140 14531]]
mlba.classificationSummary(y_true=y, y_pred=logit_reg.predict(X))
Confusion Matrix (Accuracy 0.6363)

         Prediction
  Actual  default paid off
 default    14321     8350
paid off     8140    14531

Precision, Recall, and Specificity

y_pred = logit_reg.predict(X)
print("precision ", precision_score(y, y_pred, pos_label="default"))
print("recall.    ", recall_score(y, y_pred, pos_label="default"))
print("specificity", recall_score(y, y_pred, pos_label="paid off"))
precision  0.6375940519122034
recall.     0.6316880596356579
specificity 0.640950994662785

ROC Curve

fpr, tpr, thresholds = roc_curve(y, logit_reg.predict_proba(X)[:, 0],
                                 pos_label="default")
roc_df = pd.DataFrame({"recall": tpr, "specificity": 1 - fpr})

ax = roc_df.plot(x="specificity", y="recall", figsize=(4, 4), legend=False)
ax.set_ylim(0, 1)
ax.set_xlim(1, 0)
ax.plot((1, 0), (0, 1))
ax.set_xlabel("specificity")
ax.set_ylabel("recall")

plt.tight_layout()
plt.show()

AUC

print(np.sum(roc_df.recall[:-1] * np.diff(1 - roc_df.specificity)))
print(roc_auc_score([1 if yi == "default" else 0 for yi in y],
                    logit_reg.predict_proba(X)[:, 0]))
0.6917057288869074
0.6917058067118191

Strategies for Imbalanced Data

Undersampling

full_train_set = pd.read_csv(DATA_DIR / "full_train_set.csv.gz")

print("percentage of loans in default: ",
      100 * np.mean(full_train_set.outcome == "default"))
percentage of loans in default:  18.894546909248504
predictors = ["payment_inc_ratio", "purpose_", "home_", "emp_len_",
              "dti", "revol_bal", "revol_util"]
outcome = "outcome"
X = pd.get_dummies(full_train_set[predictors], prefix="", prefix_sep="",
                   drop_first=True, dtype=int)
y = full_train_set[outcome]

full_model = LogisticRegression(C=np.inf, max_iter=10_000)
full_model.fit(X, y)
print("percentage of loans predicted to default: ",
      100 * np.mean(full_model.predict(X) == "default"))
/Users/petergedeck/cdd/practical-statistics-for-data-scientists-code-3e/.venv/lib/python3.13/site-packages/sklearn/linear_model/_logistic.py:1170: UserWarning: Setting penalty=None will ignore the C and l1_ratio parameters
  warnings.warn(
percentage of loans predicted to default:  0.3883754073357947

Oversampling and Up/Down Weighting

default_wt = 1 / np.mean(full_train_set.outcome == "default")
wt = [default_wt if outcome == "default" else 1
      for outcome in full_train_set.outcome]

full_model = LogisticRegression(C=np.inf, max_iter=10_000)
full_model.fit(X, y, sample_weight=wt)
print("percentage of loans predicted to default (weighting): ",
      100 * np.mean(full_model.predict(X) == "default"))
/Users/petergedeck/cdd/practical-statistics-for-data-scientists-code-3e/.venv/lib/python3.13/site-packages/sklearn/linear_model/_logistic.py:1170: UserWarning: Setting penalty=None will ignore the C and l1_ratio parameters
  warnings.warn(
percentage of loans predicted to default (weighting):  57.61874203038663

Supplementary Material

Figure 5-2. Graph of the logit function that maps a probability to a scale suitable for a linear model

p = np.arange(0.01, 1, 0.01)
df = pd.DataFrame({
    "p": p,
    "logit": np.log(p / (1 - p)),
    "odds": p / (1 - p),
})

fig, ax = plt.subplots(figsize=(3, 3))
ax.axhline(0, color="grey", linestyle="--")
ax.axvline(0.5, color="grey", linestyle="--")
ax.plot(df["p"], df["logit"])
ax.set_xlabel("Probability")
ax.set_ylabel("logit(p)")

plt.tight_layout()
plt.show()

How to control the order of the classes in Python

# If you have a feature or outcome variable that is ordinal, use
# the scikit-learn `OrdinalEncoder` to replace the categories
# (here, "paid off" and "default") with numbers. In the below code,
# we replace "paid off" with 0 and "default" with 1. This reverses
# the order of the predicted classes and as a consequence, the
# coefficients will be reversed. You will however now need to
# keep track of how the the numbers map back to the classes.
predictors = ["payment_inc_ratio", "purpose_", "home_", "emp_len_",
              "borrower_score"]
outcome = "outcome"
X = pd.get_dummies(loan_data[predictors], prefix="", prefix_sep="",
                   drop_first=True, dtype=int)
enc = OrdinalEncoder(categories=[["paid off", "default"]])
y_enc = enc.fit_transform(loan_data[[outcome]]).ravel()

logit_reg_enc = LogisticRegression(C=np.inf, max_iter=1000)
logit_reg_enc.fit(X, y_enc)

print("intercept ", logit_reg_enc.intercept_[0])
print("classes", logit_reg_enc.classes_)
pd.DataFrame({"coeff": logit_reg_enc.coef_[0]}, index=X.columns)
intercept  1.6262802328204813
classes [0. 1.]
/Users/petergedeck/cdd/practical-statistics-for-data-scientists-code-3e/.venv/lib/python3.13/site-packages/sklearn/linear_model/_logistic.py:1170: UserWarning: Setting penalty=None will ignore the C and l1_ratio parameters
  warnings.warn(
coeff
payment_inc_ratio 0.079896
borrower_score -4.611003
debt_consolidation 0.250206
home_improvement 0.411879
major_purchase 0.228987
medical 0.528678
other 0.620869
small_business 1.222274
OWN 0.052453
RENT 0.159220
> 1 Year -0.349459

Figure 5-3. The relationship between the odds ratio and the log-odds ratio

fig, ax = plt.subplots(figsize=(3, 3))
ax.plot(df["logit"], df["odds"])
ax.set_xlabel("log(odds ratio)")
ax.set_ylabel("odds ratio")
ax.set_xlim(0, 5.1)
ax.set_ylim(-5, 105)

plt.tight_layout()
plt.show()

Figure 5-4. Partial residuals from logistic regression

def partial_residual_plot(model, df, outcome, feature, fig, ax):
    y_actual = [0 if s == "default" else 1 for s in df[outcome]]
    y_pred = model.predict(df)
    org_params = model.params.copy()
    zero_params = model.params.copy()
    # set model parametes of other features to 0
    for i, name in enumerate(zero_params.index):
        if feature in name:
            continue
        zero_params.iloc[i] = 0.0
    model.initialize(model.model, zero_params)
    feature_prediction = model.predict(df)
    ypartial = -np.log(1 / feature_prediction - 1)
    ypartial -= np.mean(ypartial)
    model.initialize(model.model, org_params)
    results = pd.DataFrame({
        "feature": df[feature],
        "residual": -2 * (y_actual - y_pred),
        "ypartial": ypartial / 2,
    })
    results = results.sort_values(by=["feature"])

    ax.scatter(results.feature, results.residual, marker=".", s=72. / fig.dpi)
    ax.plot(results.feature, results.ypartial, color="black")
    ax.set_xlabel(feature)
    ax.set_ylabel(f"Residual + {feature} contribution")
    return ax

formula = ("outcome ~ bs(payment_inc_ratio, df=8) + purpose_ + "
           "home_ + emp_len_ + bs(borrower_score, df=3)")
model = smf.glm(formula=formula, data=loan_data, family=sm.families.Binomial())
results = model.fit()

fig, ax = plt.subplots(figsize=(5, 5))
partial_residual_plot(results, loan_data, "outcome", "payment_inc_ratio", fig, ax)
ax.set_xlim(0, 25)
ax.set_ylim(-2.5, 2.5)


plt.tight_layout()
plt.show()

Figure 5-7. Area under the ROC curve for the loan data

ax = roc_df.plot(x="specificity", y="recall", figsize=(4, 4), legend=False)
ax.set_ylim(0, 1)
ax.set_xlim(1, 0)
ax.set_xlabel("specificity")
ax.set_ylabel("recall")
ax.fill_between(roc_df.specificity, 0, roc_df.recall, alpha=0.3)

plt.tight_layout()
plt.show()

SMOTE

predictors = ["payment_inc_ratio", "purpose_", "home_", "emp_len_",
              "dti", "revol_bal", "revol_util"]
outcome = "outcome"
X = pd.get_dummies(full_train_set[predictors], prefix="", prefix_sep="",
                   drop_first=True, dtype=int)
y = full_train_set[outcome]

X_resampled, y_resampled = SMOTE().fit_resample(X, y)
print("percentage of loans in default (SMOTE resampled): ",
      f"{100 * np.mean(y_resampled == 'default'):.2f}")

full_model = LogisticRegression(C=np.inf, max_iter=10_000)
full_model.fit(X_resampled, y_resampled)
print("percentage of loans predicted to default (SMOTE): ",
      f"{100 * np.mean(full_model.predict(X) == 'default'):.2f}")

X_resampled, y_resampled = ADASYN().fit_resample(X, y)
print("percentage of loans in default (ADASYN resampled): ",
      f"{100 * np.mean(y_resampled == 'default'):.2f}")

full_model = LogisticRegression(C=np.inf, max_iter=10_000)
full_model.fit(X_resampled, y_resampled)
print("percentage of loans predicted to default (ADASYN): ",
      f"{100 * np.mean(full_model.predict(X) == 'default'):.2f}")
percentage of loans in default (SMOTE resampled):  50.00
/Users/petergedeck/cdd/practical-statistics-for-data-scientists-code-3e/.venv/lib/python3.13/site-packages/sklearn/linear_model/_logistic.py:1170: UserWarning: Setting penalty=None will ignore the C and l1_ratio parameters
  warnings.warn(
percentage of loans predicted to default (SMOTE):  29.51
percentage of loans in default (ADASYN resampled):  48.56
/Users/petergedeck/cdd/practical-statistics-for-data-scientists-code-3e/.venv/lib/python3.13/site-packages/sklearn/linear_model/_logistic.py:1170: UserWarning: Setting penalty=None will ignore the C and l1_ratio parameters
  warnings.warn(
percentage of loans predicted to default (ADASYN):  27.37

Figure 5-8. Comparison of the classification rules for four different methods

predictors = ["borrower_score", "payment_inc_ratio"]
outcome = "outcome"
X = loan3000[predictors]
y = loan3000[outcome]

loan_tree = DecisionTreeClassifier(random_state=1, criterion="entropy",
                                   min_impurity_decrease=0.003)
loan_tree.fit(X, y)

loan_lda = LinearDiscriminantAnalysis()
loan_lda.fit(X, y)

logit_reg = LogisticRegression(penalty="l2", solver="liblinear")
logit_reg.fit(X, y)


## model
gam = pygam.LinearGAM(pygam.s(0) + pygam.s(1))
print(gam.gridsearch(X.values, [1 if yi == "default" else 0 for yi in y]))
/Users/petergedeck/cdd/practical-statistics-for-data-scientists-code-3e/.venv/lib/python3.13/site-packages/sklearn/linear_model/_logistic.py:1135: FutureWarning: 'penalty' was deprecated in version 1.8 and will be removed in 1.10. To avoid this warning, leave 'penalty' set to its default value and use 'l1_ratio' or 'C' instead. Use l1_ratio=0 instead of penalty='l2', l1_ratio=1 instead of penalty='l1', and C=np.inf instead of penalty=None.

  warnings.warn(


  0% (0 of 11) |                         | Elapsed Time: 0:00:00 ETA:  --:--:--

 45% (5 of 11) |###########              | Elapsed Time: 0:00:00 ETA:   0:00:00

 90% (10 of 11) |#####################   | Elapsed Time: 0:00:00 ETA:   0:00:00

100% (11 of 11) |########################| Elapsed Time: 0:00:00 Time:  0:00:00

LinearGAM(callbacks=[Deviance(), Diffs()], fit_intercept=True, 
   max_iter=100, scale=None, terms=s(0) + s(1) + intercept, 
   tol=0.0001, verbose=False)
models = {
    "Decision Tree": loan_tree,
    "Linear Discriminant Analysis": loan_lda,
    "Logistic Regression": logit_reg,
    "Generalized Additive Model": gam,
}

fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(5, 5))

xvalues = np.arange(0.25, 0.73, 0.005)
yvalues = np.arange(-0.1, 20.1, 0.1)
xx, yy = np.meshgrid(xvalues, yvalues)
X = pd.DataFrame({
    "borrower_score": xx.ravel(),
    "payment_inc_ratio": yy.ravel(),
})

boundary = {}

for n, (title, model) in enumerate(models.items()):
    ax = axes[n // 2, n % 2]
    predict = model.predict(X)
    if "Generalized" in title:
        Z = np.array([1 if z > 0.5 else 0 for z in predict])
    else:
        Z = np.array([1 if z == "default" else 0 for z in predict])
    Z = Z.reshape(xx.shape)
    boundary[title] = yvalues[np.argmax(Z > 0, axis=0)]
    boundary[title][Z[-1, :] == 0] = yvalues[-1]

    c = ax.pcolormesh(xx, yy, Z, cmap="Blues", vmin=0.1, vmax=1.3, shading="auto")
    ax.set_title(title)
    ax.grid(visible=True)

plt.tight_layout()
plt.show()

Naive Bayes with numerical features

print(f”MixedNB Library Accuracy: {accuracy_mixed:.2f}“)