Architecture
Scores loan default risk.Ran on Chameleon Cloud.
Containers pull the public LendingClub loan book, fit one transform on the training rows and save it beside the data. XGBoost trains on those rows, and a FastAPI service rebuilds each uploaded loan with the same saved transform before it scores it.
0.79 ms
median to score one loan row, model in process, over 100 calls
22,607 loans
held out for the stress tests, never seen in training
7 days
on the calendar from the first Dockerfile to the last commit, May 5 to 11, 2025
0.71 AUC
ROC AUC of the served model in 2 fold cross validation
New York --:-- simulated
Hover a part to trace what it touches. Click it for its notes.
- Built by me
- Managed by Google
- Outside system
- Main data path
- On demand
- Credentials cross
When it breaks
What each failure does, and what it does not do.
-
A loan field is blank
The API fills it with the training median or mode saved in the pickle, so one empty field does not sink the request.
fastapi_pt/app.py
-
A category never seen before
One hot columns are rebuilt from the training list and missing ones are added as zeros, so the model always gets the columns it expects.
fastapi_pt/app.py
-
An expected column is gone
The transform stops and names the missing columns before it writes anything.
etl/docker/docker-compose-etl.yaml
What I tuned
Small choices that keep it fast and cheap.
-
Fit once, load everywhere
Medians, modes, encoders and the scaler are fit on training rows only. Validation and the API load the same pickle.
etl/docker/docker-compose-etl.yaml
-
Class weight from the counts
Training held 1,564,054 low risk and 244,480 high risk loans. A scale_pos_weight of 6.4 matches that ratio.
train/train_xgboost_onnx.ipynb
-
Run history on block storage
MLflow keeps runs in Postgres and artifacts in MinIO on mounted block storage, so the history outlives any container.
train/docker/docker-compose-block.yaml
Guards
Rules the code enforces, not rules people remember.
-
Training statistics only
Validation rows are filled, encoded and scaled with values learned from training rows, never from themselves.
etl/docker/docker-compose-etl.yaml
-
Outcome columns dropped
Payment, recovery and settlement fields only exist after a loan ends, so the split drops them before anything trains.
etl/data-split/data-split.py
-
Every score is counted
Each prediction feeds a confidence histogram and a per class counter that Prometheus scrapes.
fastapi_pt/app.py
