Reiad's Library
Case study · Credit risk · Machine learning

A probability-of-default model, and an honest answer about whether the clever one wins.

The whole pipeline on a real public dataset, fitted in your browser while you read: split, encode, a logistic scorecard, a gradient-boosted ensemble, cross-validation, calibration, and the cut-off where the modelling stops and the lending decision starts. Nothing here is precomputed. Move the split seed and every number on the page is refitted, which is the fastest way to see how much of a result on a thousand rows is real.

Boosted minus scorecard, test AUC

Scorecard AUCHeld-out applicants, of them
Boosted AUCSame applicants, same split
KSThe separation credit teams quote
Cost per applicantAt your cut-off, on the dataset's 5:1 matrix
Approval rateWhat the cut-off actually does
Brier scoreWhether the probability means anything
ROC on the held-out applicantsScorecardBoosted

Where the two models disagree about an applicantone dot per held-out applicant

The data, and what is wrong with it

A thousand loan applications from a German bank, twenty attributes each, labelled good or bad. It is the most-used credit dataset in teaching, which is the reason to use it and also the reason to be careful with it: everything below is a demonstration of method on a set that no longer describes any real lending market.

Information value, before any model is fittedtraining rows only

Information value is the first thing a credit analyst computes and the last thing a machine-learning course mentions. It measures how far an attribute separates the two outcomes, on its own, with no model in the way, and the industry has read it off the same thresholds for thirty years: under 0.02 useless, 0.1 weak, 0.3 medium, above 0.5 strong enough that the honest first reaction is to check for leakage rather than to celebrate.

The pipeline, in the order that matters

1 · Split

Before anything else touches the data

Stratified, so both halves carry the same 30% default rate, and seeded, so it is the same split for every reader. A slice of the training rows is held back again for the boosting curve and the calibration, which keeps the test set for one job only.

2 · Encode

Learned on the training rows, applied to the test rows

Each category becomes a column, except the two the documentation lists and the data never contains. The scorecard drops one level per attribute as its reference and standardises the numbers; the trees take the raw values, because a split point does not care about scale.

3 · Fit

Two models, the same columns

Newton's method for the scorecard, second-order gradient boosting for the ensemble. Neither sees a test row.

4 · Calibrate

A ranking is not yet a probability

Boosted scores rank well and lie about the level. The mapping that fixes that is fitted on held-out rows, because fitting it in-sample makes the answer worse in a way that only shows up later.

5 · Decide

The cut-off is an economic choice

No amount of AUC picks it. The dataset's own cost matrix does: lending to a bad applicant is stated to be five times as expensive as turning away a good one.

6 · Check

Whether the difference was real

Cross-validation and DeLong's test, because a single split of 300 applicants measures an AUC to about three points, and both models are well inside that of each other.

The leak that does not announce itself

Standardise, bin, or compute weights of evidence on all thousand rows before splitting, and the test AUC comes out higher than the model deserves. Nothing errors. The number just quietly includes a little knowledge of the applicants it was supposed to be surprised by, and the model underperforms in production for reasons nobody can reconstruct. Every fitted quantity on this page is learned inside the training set, which is why the encoder is refitted on every fold rather than once.

Model one: the scorecard

Logistic regression, fitted by iteratively reweighted least squares, which is Newton's method on the log-likelihood:

ln( p / (1 − p) ) = β₀ + β₁x₁ + … + βₖxₖ

Every coefficient is a log odds ratio against its reference level, every one has a standard error from the inverse of the Hessian, and the whole thing converts into a points table where the odds double every twenty points. That last part is not decoration: a scorecard is what lets a lender tell a declined applicant which two things cost them the most, which in most places that lend money is a legal requirement rather than a courtesy.

Coefficients, largest effect firstlog odds against the reference level, with 95% intervals

A ridge is on by default. Without it, a category where every applicant happened to default sends its coefficient to infinity, which is called complete separation and looks from the outside exactly like a very confident model.

Model two: gradient boosting

The algorithm XGBoost and LightGBM implement, written out rather than imported, because a page served as static files cannot run a C++ library. Each tree fits the errors of the ones before it, using the second-order approximation to the loss. A split is worth taking when

gain = ½ [ G²ₗ/(Hₗ+λ) + G²ᵣ/(Hᵣ+λ) − G²/(H+λ) ] − γ

is positive, and the value that lands in a leaf is −G/(H+λ), where G and H are the sums of the gradient and the curvature of the loss over the rows that reach it. Features are pre-binned into a histogram before any tree is grown, which is the trick that makes LightGBM fast and costs nothing to do here.

The learning curveTrainingHeld-out slice

The validation curve is drawn on rows carved out of the training set, never on the test set. Choosing the number of trees by watching a test curve is how a test set stops being one, and it is the most common way a model's reported performance ends up better than its real performance.

So does the clever model win?

On one split, usually, by a little. The question is whether that little survives being measured properly, and there are two ways to find out. DeLong's test compares two AUCs measured on the same applicants, which is the right test because the two models agree about the easy cases and differ only on the hard ones. Cross-validation asks the same question by refitting everything, five times, on different folds.

DeLong's testdifference in AUC, with its interval

Five folds, both modelsrefitted from scratch each time

Score distribution by outcomewhere the KS statistic comes from
What a thousand rows can and cannot settle

Where the cut-off goes, and what it costs

Everything above is ranking. None of it is a decision. The decision is a number: above this probability of default, decline. The dataset ships its own cost matrix, and with it the answer follows from arithmetic rather than taste. Declining a good applicant costs 1. Lending to a bad one costs 5. So decline when

p × 5 > (1 − p) × 1, which is p > 1/6 = 0.167

and the whole apparatus of AUC has no opinion about it. A model that ranks better lets you sit at a lower cut-off for the same loss rate, which is where its value actually shows up.

Cost against cut-offthe dataset's own 5:1 matrix

The decision at your cut-off

The lift tableapplicants sorted worst first, in tenths

A ranking is not a probability

AUC only knows about order. A model can rank every applicant perfectly and still say 4% when it means 20%, and for a PD model that is not a detail: the number goes into a provision, a capital calculation and a price. Boosted trees are usually the worst offenders, because the loss they minimise rewards confidence on the training rows.

The fix is a one-variable logistic regression of the outcome on the model's own log odds, fitted on held-out rows. It cannot reorder anybody, so AUC, Gini and KS are untouched by construction. What changes is whether the number means what it says.

Reliabilitypredicted against observed, in tenths of the book

Explaining one decision

A model that cannot say why it declined someone is a model that cannot be deployed in consumer lending. The scorecard answers by construction: the points are additive, so the reasons are the rows with the most negative points. The boosted model has to be taken apart, and for an additive ensemble of trees that can be done exactly rather than approximately: walk each tree, and give every split the change in value it caused. The parts add to the prediction, which the test file checks.

Scorecard
Boosted

The attributes a lender is not allowed to use

This dataset ships sex, age and foreign-worker status, and a model handed all three will use all three. Every jurisdiction that regulates consumer credit restricts at least two of them, so the first thing a real project does is take them out. The toggle above does that, and the interesting part is what happens next: performance barely moves, and the disparity does not disappear, because fifty other columns rebuild what they can from what is left.

Approval rate by group, at your cut-off

What the four-fifths rule is and is not

If one group's approval rate is under four-fifths of another's, that is the point at which a US regulator starts asking questions. It is a screening test rather than a legal standard, it says nothing about whether the difference is justified by risk, and passing it is not a defence. It is on the page because it is the first number any model review computes, and because a page about credit models that skipped this section would be a page about a different job.

What this is not

1 · The dataset

A thousand rows from 1994

A 30% default rate is not a lending book, it is a teaching set, probably oversampled from bad cases. Amounts are in Deutsche Marks. A 2019 re-examination of the source argues parts of the published codebook are wrong. None of that damages a demonstration of method, and all of it would damage a conclusion about lending.

2 · No time

Every row is treated as if it happened at once

Real credit data arrives in vintages, and the split that matters is out-of-time, not random: train on last year, test on this one. A random split flatters every model, because it lets it learn from applicants who came after the ones it is being tested on.

3 · No reject inference

The applicants who were declined are not in the data

A bank only observes the outcome of loans it made. Fitting on approved applicants only and then scoring everyone is the oldest selection problem in credit, and correcting for it is most of the work in a real build.

4 · One threshold, no pricing

Approve or decline, and nothing in between

Real lenders price to risk, set limits, ask for a guarantor, or approve at a shorter term. The cost matrix here has two cells because the dataset's does.

5 · No monitoring

The half of the job that happens after deployment

Population stability, score drift, override rates, back-testing the calibration every quarter. A model is a product with a shelf life, and nothing on this page measures it.

6 · The algorithm, not the library

Gradient boosting written out by hand

Same objective, same split rule, same histogram trick, a few hundred lines instead of a few hundred thousand. What it does not have is the tuning, the categorical handling and the twenty years of edge cases that make XGBoost and LightGBM worth using in production.

Fitted here, not pasted in

Both models, the cross-validation and every metric are computed in the browser when the page loads. There are no stored results to drift from the code that made them, and the split seed is a slider because the honest way to show how much a result depends on luck is to let someone change the luck.

Data with a receipt

A script in the repository downloads the dataset from its archive, records the checksum, and writes the module the page imports. The test file recomputes column totals and level counts against what that script saw, so the data in the repository can be traced to the data at the source.

The comparison is tested, not asserted

DeLong for the difference on one split, repeated stratified folds for the difference in general, and a check in the test file that the gap between the models really is smaller than the spread across folds, so the page cannot go on claiming it after that stops being true.

Closed forms as the referee

Logistic regression on one binary predictor must return the log odds ratio of the two-by-two table and Woolf's standard error, exactly. AUC is computed three ways and they must agree. A boosted leaf must equal the closed-form minimiser of the objective it claims to minimise.

The parts of the answer add up

The scorecard's points sum to its score; the boosted model's per-feature contributions sum to its prediction; the Brier score decomposes into reliability, resolution and uncertainty. Three identities that hold to machine precision or the tests fail.

180 checks on the engine

Including the two bugs they caught while it was being written: a calibration fitted on in-sample scores that made the model worse, and a gradient with its sign the wrong way round, which trains a model that climbs the loss it is meant to descend and returns something that looks like a model.

Working together

Need a model that survives a validation team?

Scorecards, machine-learning challengers, the comparison that says whether the challenger is really better, calibration, cut-off economics and the documentation a model risk function will ask for. In Python or R, with the notebook and the data pipeline handed over.