~/teaching · machine learning · reference
A credit default scoring project, step by step: the reproducible repository behind the UCI Default of Credit Card Clients data
One repository, two commands, thirteen tests, and the question every committee asks: why a model?
Abstract
This is the summary of a reference deck of the Machine Learning course at Centrale Méditerranée (option DDEFI, 2026-2027). It walks, screenshot by screenshot, through the running-thread project that every team receives at the start of the course: a credit default score built on the UCI Default of Credit Card Clients dataset (Yeh and Lien, 2009, CC BY 4.0), delivered as a complete repository with its data script, frozen split, two baseline models, tests, continuous integration, guide notebooks and documentation templates.
The note is useful beyond the course to anyone who wants to see what a small but serious machine learning repository looks like: what to version and what to regenerate, what a machine learning test checks, why the decision threshold lives in a configuration file, how to show that a model beats the rule a bank already applies, and which robustness properties to write before looking at the numbers.
It pairs with the session on the decision threshold and the reproducible repository, with the tools note on Git, GitHub and Python, and with the three milestone notes: baseline, champion and delivery.
Plan of the deck
- The repository and how it arrives. The file map, the history in the order of the course, and the first pull request each team reviews and merges.
- The commands.
make reproducerebuilds data, split, models and metrics identically;make testchecks thirteen properties in seconds. - The guide notebook. Seven steps, one per session, each closed by a list of things not to do.
- Why a model, and does it hold?. The model against business rules in euros, the dependence on costs, six robustness tests and a drift simulation.
- Working as a team. Short branches, reviewed pull requests, a CI that reproduces twice, a history with one commit per idea, a thin interface.
- What is left to do. Three milestones, a heavy model left deliberately empty, and five repository rules with their guardians.
The project and the repository
The task is to estimate, for each of 30,000 card holders, the probability of defaulting next month, so that a credit analyst can decide; the repository makes that estimate rebuildable by anyone.
The data are the UCI Default of Credit Card Clients table: 30,000 clients of a Taiwanese bank in 2005, with their credit limit, demographic fields, six months of repayment status, bill and payment amounts, and a binary target, default the following month. The raw file is never committed: a script downloads it and compares its SHA-256 fingerprint with the one stored in the repository, so a silent change of the source stops the pipeline.
The split is done once, stratified, 60% training, 20% validation, 20% test, with a fixed seed, and the fingerprint of each fold is stored too. Two models come with the repository: the base rate, which predicts the same probability for everyone, and a logistic regression inside a scikit-learn Pipeline, calibrated on the validation fold. The heavier model is left to the teams.
| Place | Role |
|---|---|
src/credit/ | Six importable modules (data, split, features, model, evaluate, compare): the only place where logic lives |
batch/ | Scripts for prepare, train, predict and evaluate; they call src/credit, they never copy it |
notebooks/ | A guide, an exploration, and a notebook on why a model and how robust it is |
tests/ | Thirteen property tests, run locally and in CI |
config.yaml | Seed, error costs, threshold, excluded columns |
app/app.py | A minimal interface: one client in, one decision out |
| Root documents | DECISIONS.md (decision log) and the templates for the model card, datasheet, ablation study, monitoring plan and client memo |
The rule is that nothing regenerable is versioned: no raw data, no folds, no trained model. The .gitignore refuses them and the commands rebuild them. The commit history follows the course, from the skeleton and the framing through data, split and models up to the tests, the notebooks, the milestone templates and the interface, so reading it is reading the project. Each team receives the whole thing as a pull request in its own repository: one member reads the changes, another merges, and everyone then runs the two commands.
Two commands: make reproduce and make test
If the same command does not give the same numbers twice, then the data, the code, the seed or the environment has moved, and you need to know which.
make reproduce # download and fingerprint, split, train, metrics make test # thirteen property tests, a few seconds make compare # milestone 2 only, reads the test fold once make app # one client, one decision
make reproduce chains four targets. The data step stops if the fingerprint changed, because data are not modified without an entry in the decision log. The split step writes the folds and their fingerprints, which a test then freezes. The training step fits the two provided models. The metrics step measures on the validation fold only and writes a metrics.json. The test fold is not read here at all: it is opened once, by make compare, at the second milestone.
| Model | AUC | Log-loss | Brier | Cost per file |
|---|---|---|---|---|
| Base rate | 0.5 | 0.5284 | 0.1723 | €467.3 |
| Calibrated logistic | 0.7232 | 0.4666 | 0.146 | €468.6 |
The base rate is 22.12% in each fold, by construction of the stratified split. Its log-loss is not arbitrary: predicting the base rate for everyone gives exactly the entropy of the target, which is the floor any useful model must go below.
make test runs thirteen properties in a few seconds. A machine learning test is not "the AUC is good": it is something that must hold whatever happens. The schema contract (columns, row count, bounds, a 0/1 target, missing values); a split whose fingerprints never change and where no client appears in two folds; a model that reaches an AUC of 1 on separable data, otherwise the training code is broken; predictions unchanged by permuting rows; a reloaded model whose predictions match the trained one up to in absolute terms; a naive model equal to the base rate; a logistic that beats it. Plus six robustness properties, described below.
The guide notebook: the threshold is a decision, leaks are the enemy
The notebook imports the repository's code instead of copying it, and it shows that the threshold comes from costs, not from the model.
The guide notebook has seven sections, one per session: framing, the frozen split, the naive model then the logistic, the threshold, then the three milestones. Its first cell imports src/credit. A notebook that pastes its own copy of the code soon presents something other than what runs. Each section ends with a short list of things not to do, which together form a catalogue of data leakage: computing correlations on the full dataset before splitting, re-splitting with another seed, standardising before the split, tuning hyperparameters on the validation fold, peeking at the test fold.
Two guard-rails are visible in the logistic step. An assertion checks that the SEX column is not among the features; it is a principle, not a performance choice. And standardisation sits inside the Pipeline, so its means and variances are learned on the training fold only, which removes the most common leak of the course.
With calibrated probabilities, the threshold that minimises the expected cost of a refuse/accept decision depends only on the two error costs:
On the validation fold, the empirical minimum of the cost curve falls at 0.045, far from 0.1176, and the curve is almost flat between the two. That is the honest diagnosis: the baseline sorts low risks poorly. The repository keeps the theoretical threshold, which can be defended, writes it in config.yaml and justifies it in the decision log. A threshold edited by hand in a batch script or in the interface would be a decision nobody took.
Why a model? Against business rules, in euros
Before any machine learning, a bank already refuses clients who are late on payments, so the model has to beat that rule in money, not only in AUC.
| Decision | Cost per file |
|---|---|
| Refuse if latest repayment delay ≥ 1 month | €541 |
| Accept all applicants | €995 |
| Refuse all applicants | €467 |
| Calibrated logistic at | €469 |
With these costs, the calibrated logistic does no better than the bluntest policy imaginable, since turning down nearly every applicant costs about as much. With 22% of defaults and a default 7.5 times more expensive than a lost client, an AUC of 0.72 does not separate low risks well enough. This is a fact about these costs and this model, not a verdict on machine learning, and it is the starting point of the second milestone.
| Cost of a default | Rule (delay ≥ 1) | Logistic | Saving |
|---|---|---|---|
| €600 | €133 | €112 | €21 |
| €1,500 | €227 | €226 | €1 |
| €3,000 | €384 | €374 | €10 |
| €4,500 | €541 | €460 | €81 |
Worked example: when both errors cost the same
- Gap per file. euros in favour of the logistic, that is less than the rule.
- Per year. On 6,000 files, about euros; the deck, working from unrounded costs, quotes about 130,000 euros.
- With its uncertainty. The validation AUC of 0.723 has a 95% bootstrap interval running from 0.706 up to 0.740, so a rival at 0.735 would not be established as better.
The lesson for a client memo: costs are assumptions given by the business, and the value of a model moves with them. The argument has to be shown across cost levels, with its interval.
Six robustness tests, written before looking
A robust model is one for which you have written down, in advance, what it must do when the data move, and turned each statement into an automatic test.
- Interval. The bootstrap AUC is clearly away from 0.5 and its interval is narrower than 0.06.
- Noise. A 1% perturbation of the amounts moves no predicted probability by more than 2 points.
- Monotonicity. One more month of repayment delay never lowers the predicted risk.
- Absurd inputs. A credit limit of one billion or an age of 200 still yields an output in .
- Stability. Five resamples of the training fold give AUCs within 0.01 of each other.
- Slices. No marital-status group falls below an AUC of 0.65.
Each property is both a test in tests/test_robustness.py and a notebook cell that explains it. The tests are frank about their limits: for a client with a one-billion limit, the model outputs a probability of 0.0, which is inside the bounds and still not a sensible answer. In production, the schema contract should reject such a row before the model sees it.
The last cell simulates a drift: every credit limit doubles, and the mean predicted risk falls by 1.4 points. Nobody can say whether that is right, because the model never saw such clients. That is precisely why inputs are monitored after deployment, the subject of the session on drift and conformal prediction.
Working as a team: branches, pull requests, CI, interface
The main branch is the version that works; every change goes through a short branch, a pull request reviewed by someone else, and a CI that reproduces the numbers.
The cycle the branches guide describes
- Start from main. Switch to
main, pull, and create a short branch named after one task. - One commit per idea. Present-tense messages that say what and why, so that
git logreads as the project log; never data, models, secrets or notebook outputs in a commit. - Pull request. Three lines: what it changes, how to check it, what is left; reviewed by someone other than the author.
- Merge with a merge commit. Not a squash, so that the history keeps the trace of each branch.
- Tag the hand-in. The last commit pushed before a deadline counts; a tag such as
jalon-1marks it.
The continuous integration file installs the pinned versions, runs make reproduce and make test, then computes the metrics a second time and compares the two metrics.json. If a single line differs, the check is red and the pull request does not merge. This is exactly the entry criterion of the first milestone: another team clones your repository and gets your numbers.
The interface, app/app.py, is a Streamlit page of about sixty lines: fields for the variables, a button, the probability and the decision. It loads the same Pipeline as the batch and reads the threshold from config.yaml, and it contains no business logic of its own. A computation changed only in the interface would silently diverge from the nightly batch, which does the same thing on a whole CSV file.
What is left: three milestones, a heavy model, five rules
The repository is a starting point; three milestones complete it, and the final project reuses the same structure on another subject.
| Milestone | What must exist in the repository |
|---|---|
| 1. Defensible baseline (November) | A green CI on another team's machine; an ablation study pre-registered in an issue, then computed; ten entries in the decision log; from-scratch OLS and logistic notebooks matching scikit-learn to |
| 2. Documented champion (February) | The heavy model written; the champion rule in the decision log before make compare is run, once; a model card with a fairness audit on marital status; a corrected paired test |
| 3. Delivered project (March) | The same structure on the team's own subject: monitoring plan, client memo, report, slides, interface, individual sheet, then the defence |
The heavy model is deliberately empty: the function in src/credit/model.py lists what to do (gradient boosting in a pipeline with a fixed seed, hyperparameters by cross-validation on the training fold, the same calibration as the baseline, the champion rule written first, a Nadeau and Bengio corrected paired test) and raises an error until it is written. make compare also refuses to run while the decision log lacks the champion rule. Writing a boosting model takes seconds with an assistant; explaining every hyperparameter, and showing the interval that separates the champion from the logistic, is the actual work.
- Notebooks import
src/creditand never paste a copy of it. - All fitting, scaling and encoding use the training fold only, never the full dataset.
- The test fold is read once, by
make compare, after the champion rule is written. SEXnever enters the model; marital status is allowed, with a fairness audit.- Code may be written with an AI assistant, but each member must be able to explain every line orally.
Four of the five rules have an automatic guardian: the feature function refuses forbidden columns, the Pipeline carries preprocessing, make compare checks the decision log, the tests freeze the split. The fifth is checked in person.
Key takeaways
- Version the code, the configuration and the fingerprints; regenerate the data, the folds and the models with one command.
- A machine learning test checks a property that must always hold, such as a frozen split or a perfect AUC on separable data, not a good score.
- The decision threshold follows from the error costs and lives in configuration; on these costs it is 600/5,100, about 0.118.
- A model must beat the rule already in place, in euros and across cost levels, with an interval; here the logistic does so only modestly.
- Write robustness properties before looking at the numbers, and admit what they cannot settle, such as behaviour under unseen drift.
- Short branches, reviewed pull requests and a CI that reproduces twice keep the main branch trustworthy.
Glossary
- Train / validation / test split
- The three disjoint folds: one to fit, one to calibrate and choose the threshold, one read once for the final comparison.
- Random seed
- The fixed number that makes the split and the training repeatable.
- Data leakage
- Any use of information from validation or test data, or from the future, while fitting the model.
- Base rate
- The share of defaults in the data, 22.12% here; the prediction of the naive model.
- Brier score
- The mean squared gap between predicted probability and outcome; lower is better.
- Expected cost
- The average cost per file of a decision rule, weighting each error by its price.
- Property-based test
- A test that checks a property expected to hold whatever the data, rather than a particular score.
- Schema contract
- The declared columns, types and bounds that every input row must respect.
- Continuous integration (CI)
- Automatic reproduction and testing of the repository at every push and pull request.
- Pull request
- A proposed change on a branch, reviewed by someone else before it is merged into main.
- Milestone
- A dated hand-in, identified by the last commit pushed before the deadline and usually a tag.
- Model card
- A document stating what a model is for, how it was evaluated, and its limits.
- Ablation study
- Removing a group of features, pre-registered, to measure what it really contributes.
The other notes of the course
One public summary per deck, in the order of the course.
Previous: Defending a machine learning projectNext: Asset returns: XGBoost and sentiment
- IntroductionAI winters, jobs and method
- ToolsKaggle, GitHub, Python: the tools
- Session 1Project framing and least squares
- Session 2Maximum likelihood and logistic regression
- Session 3Gradient descent, ridge and lasso
- Session 4Decision threshold and reproducible ML
- HomeworkA defensible baseline model
- Session 5Kernels, SVM and decision trees
- Session 6Forests, boosting, honest comparison
- Session 7SHAP, fairness, PCA and clustering
- HomeworkA documented champion model
- Session 8Neural networks, drift, conformal prediction
- HomeworkA delivered machine learning project
- Session 9Defending a machine learning project
- ReferenceCredit default scoring project
- ReferenceAsset returns: XGBoost and sentiment
- ReferencePython code for every ML model
- ReferenceDeep learning essentials
- ReferenceOpen data ML projects by sector
- ReferenceData and AI engineer handbook
- ReferenceData engineering, SRE and cloud
- ReferencePanorama of machine learning models
About these notes
Written by the lecturer from the slides of the deck, as a public summary: the key ideas, the formulas and one worked example. The exercises, their solutions, the tests and the grading stay in the deck, for the students of the course.
Revision: .