Risk management project · Credit risk · SQL, Python and R

Which card borrowers miss next month's payment, and how much capital the book needs.

30,000 credit card accounts from Taiwan, with six months of bills, payments and repayment status. The data is checked and segmented in SQL, a probability of default (PD) scorecard is built and validated in Python, the whole book is run through expected loss, Basel IRB capital and a Monte Carlo loss model, and the key numbers are recomputed independently in R.

Accounts30,00022.1% defaulted the next month
Test AUC0.769Gini 0.54 · KS 0.42 on 9,000 held out accounts
Expected lossNT$478M14.5% of NT$3.29B exposure at default
Loss at 99.9%NT$940MMonte Carlo; IRB formula capital NT$444M
Python vs RAgreeLargest gap 1.2e−9 on a coefficient

01 / How it is built

Three languages, one pipeline, every number traceable.

One command, python3 run.py, rebuilds the database from the raw file, fits and validates the model, runs the loss model, calls R to recheck it, and writes the figures on this page. Every file is published as it runs.

Python

Model, validate and size the loss

WoE binning, feature selection, logistic scorecard, validation, expected loss, IRB capital, Monte Carlo and stress tests.

R

Check it independently

Refits the model with glm() and recomputes expected loss and capital in base R. The run stops if the answers disagree.

02 / The data, checked in SQL

Know what is wrong with the data before modelling it.

Default of Credit Card Clients, UCI Machine Learning Repository (id 350). Repayment history April to September 2005; default measured in October 2005. Each row is one account: credit limit, six months of repayment status, bills and payments, and whether the next payment was missed. The checks below come from sql/02_quality.sql.

CheckAccountsHow it is handled
Accounts30,000Matches the UCI documentation
Defaults in the following month6,636Matches the documentation: 22.1% of accounts
Duplicate account rows35Kept. The source copy has no account ID, so identical rows can be separate accounts
Education code not in the documentation (0, 5, 6)345Not used: demographic fields stay out of the model
Marital status code not in the documentation (0)54Not used: demographic fields stay out of the model
September status coded -2 (undocumented)2,759Kept as its own group, commonly read as no card use; the data sets its risk
September status coded 0 (undocumented)14,737Kept as its own group, commonly read as revolving credit
Negative September bill (credit balance)590Drawn balance floored at zero when measuring exposure
September bill above the credit limit2,115Kept: over limit is a risk signal. Undrawn limit floored at zero
No bill and no payment in all six months795Kept and disclosed; see the next row
Of which flagged as default next month29937.6% of inactive accounts are labelled default, so the label does not always mean a missed payment on a balance

03 / Where default concentrates

Payment history separates risk far better than limit or utilisation.

Default rate by segment, from sql/04_segments.sql. The dashed line is the book average of 22.1%. A segment holding more of the defaults than of the accounts is where risk sits.

Default rate by repayment status in September

SegmentAccountsDefault rateShare of accountsShare of defaults
Credit limit, NT$
Under 50k4,31136.1%14.4%23.4%
50k to 150k10,97925.2%36.6%41.7%
150k to 300k9,59316.8%32.0%24.3%
300k and over5,11713.7%17.1%10.5%
Months delayed, last six
None19,93111.7%66.4%35.2%
1 to 26,32532.5%21.1%31.0%
3 to 42,10553.8%7.0%17.1%
5 to 61,63968.0%5.5%16.8%
Utilisation, September
None or credit2,59824.8%8.7%9.7%
Under 30%12,19416.9%40.6%31.1%
30% to 70%5,68624.0%18.9%20.6%
70% to 100%7,40726.0%24.7%29.0%
Over limit2,11530.1%7.0%9.6%

Accounts two or more months late in September are 10.4% of the book and 32.8% of next month's defaults. Smaller limits default more often, which is consistent with lenders already giving riskier borrowers less credit. High utilisation matters, but less than payment history.

04 / PD scorecard

7 candidate features in, 4 kept, every decision written down.

Each feature is binned and converted to Weight of Evidence (WoE) on a 70% training sample. Rules set before fitting: Information Value (IV) of at least 0.02, drop the weaker of any pair whose WoE correlates above 0.7, and every coefficient must have the expected sign. Sex, age, education and marital status are never offered to the model.

FeatureIVDecision
Repayment status, September0.878Kept
Months delayed, last six0.860Dropped: WoE correlation 0.76 with Repayment status, September, which has higher IV
Worst status, last six months0.773Kept
Share of August bill paid in September0.191Dropped: positive coefficient after fitting (unstable sign)
Credit limit0.182Kept
Average utilisation, six months0.123Kept
Utilisation, September0.077Dropped: WoE correlation 0.72 with Average utilisation, six months, which has higher IV
Logistic regression on WoECoefficientStd. errorp-value
Intercept-1.2500.019< 0.001
Repayment status, September-0.6520.025< 0.001
Worst status, last six months-0.4560.028< 0.001
Credit limit-0.4550.046< 0.001
Average utilisation, six months-0.3850.055< 0.001

Every coefficient is negative, as it should be: a higher WoE means a safer bin, so it lowers the PD. Repayment status has an IV of 0.88, above the 0.5 level usually treated as suspicious. It is not leakage: September status is known before the October payment falls due, and being two months behind is simply one step from missing the next payment.

Scorecard points by bin

Scaled so 600 points means odds of 50 good accounts to 1 bad, and every 20 points doubles the odds. A borrower's score is the sum of one bin from each feature; test scores run from 443 to 561. Counts are training accounts.

Feature and binAccountsDefault rateWoEPoints
Repayment status, September
No use (-2)1,97213.3%0.62142
Paid duly (-1)3,97316.9%0.33137
Revolving (0)10,34212.8%0.66143
1 month late2,52233.9%-0.59120
2 months late1,87469.4%-2.0892
3+ months late31772.2%-2.2189
Worst status, last six months
No use in any month1,50413.6%0.59139
Paid duly at worst2,16314.7%0.50137
Revolving at worst10,32010.9%0.84142
1 month late1,16624.9%-0.15129
2 months late5,00543.6%-1.00118
3+ months late84262.1%-1.75108
Credit limit, NT$
Up to 30k2,86836.1%-0.69122
30k to 50k2,58327.1%-0.27127
50k to 70k1,08326.9%-0.26127
70k to 100k2,24825.2%-0.17129
100k to 140k1,97923.2%-0.06130
140k to 180k2,32617.6%0.29135
180k to 210k1,74416.9%0.33135
210k to 270k2,03915.7%0.42136
270k to 360k2,41014.8%0.49137
Over 360k1,72012.3%0.71140
Average utilisation, six months
Up to 0.4%2,10025.9%-0.21128
0.4% to 1.7%2,10019.7%0.15132
1.7% to 5.1%2,10014.3%0.53137
5.1% to 13.3%2,10014.3%0.53137
13.3% to 28.4%2,10016.1%0.39135
28.4% to 45.2%2,10019.3%0.17133
45.2% to 61.4%2,10024.6%-0.14129
61.4% to 76.0%2,10025.2%-0.17129
76.0% to 88.9%2,10029.4%-0.38127
Over 88.9%2,10032.4%-0.52125

05 / Validation

Tested on 9,000 accounts the model never saw.

Three questions a validator asks. Does it rank risky borrowers above safe ones (AUC, Gini, KS)? Are the PDs the right size (calibration, Brier, Hosmer-Lemeshow)? Is the score stable between samples (PSI)?

MeasureTrainingTestReading
Accounts21,0009,000Stratified 70%/30% split, fixed seed
Default rate22.1%22.1%Same mix in both samples
AUC0.7650.769Test matches training: no sign of overfitting
Gini0.5300.5382 × AUC − 1
KS0.4080.419Largest gap between bad and good score distributions
Brier score–0.13770.1723 if every account got the average PD
Hosmer-Lemeshow–22.7p = 0.0037: rejects exact calibration
PSI, train to test–0.001Under 0.1 is stable by the usual rule

Predicted vs observed default rate, by tenth of test accounts

Score band, riskiest firstScore rangeAccountsDefault rateDefaults captured so far
Band 1443 to 47890068.6%31.0%
Band 2478 to 50990041.7%49.8%
Band 3509 to 52190027.2%62.1%
Band 4521 to 53690018.2%70.4%
Band 5536 to 54190017.8%78.4%
Band 6541 to 54490012.4%84.0%
Band 7544 to 54790012.7%89.8%
Band 8547 to 5509009.4%94.0%
Band 9550 to 5549008.2%97.7%
Band 10554 to 5619005.0%100.0%

The riskiest tenth of test accounts by score holds 31% of the defaults and the riskiest three tenths hold 62%, so the score is useful for limits and collections. Calibration is close at the top but not exact: Hosmer-Lemeshow rejects it, mainly because tenth 6 is predicted at 14.1% against 17.3% observed and tenth 1 at 8.0% against 5.0%. Before using these PDs for pricing or provisions, a bank would recalibrate them.

06 / Expected loss and capital

From each borrower's PD to a loss distribution for the whole book.

Expected loss = PD × LGD × EAD for each account, summed. Capital comes two ways: the Basel IRB formula for revolving retail, and a Monte Carlo of 20,000 scenarios using the one-factor model that formula is built on. LGD and CCF are not in the data, so they are stated assumptions and stressed in section 07.

InputValueBasis
PDModel PD per accountFloored at 0.1%, the Basel floor for revolving retail
Loss given default (LGD)80%Assumption for unsecured card balances
Credit conversion factor (CCF)50%Assumption: share of the unused limit drawn before default
Asset correlation0.04Basel value for qualifying revolving retail exposures
Confidence level99.9%Basel IRB standard
Credit limitsNT$5.02B
Drawn balances, SeptemberNT$1.54B
Exposure at default (drawn + 50% of unused limit)NT$3.29B
Exposure weighted PD18.1%
Expected lossNT$478M
IRB capital, formulaNT$444M
Simulated loss at 99.9%NT$940M
Unexpected loss at 99.9% (simulated, less mean)NT$463M
Expected shortfall beyond 99.9%NT$985M

Simulated portfolio loss, share of exposure, 20,000 scenarios

Repayment status, SeptemberShare of exposureShare of expected loss
No use (-2)10.9%6.2%
Paid duly (-1)18.8%13.5%
Revolving (0)52.3%32.9%
1 month late9.9%18.2%
2 months late7.2%25.4%
3+ months late1.0%4.0%

Late accounts are 18.1% of the exposure but 47.5% of the expected loss. The IRB formula and the simulation agree within 4% on unexpected loss: the formula assumes an infinitely large book, while the simulation keeps the randomness of 30,000 individual accounts, and only 20 of its scenarios lie beyond the 99.9% point, so the tail estimate carries sampling error. Expected loss is what pricing and provisions should cover; capital covers the bad year on top of it.

07 / Stress tests

Correlation moves capital more than anything else.

Each case changes one input, except the combined case, and reruns the whole book. Stress simulations use 10,000 scenarios each.

ScenarioExpected lossIRB capitalLoss at 99.9%vs base
BaseNT$478MNT$444MNT$940M–
PD × 1.5NT$711MNT$480MNT$1.19B+27%
LGD 90%NT$538MNT$500MNT$1.04B+10%
CCF 100% (full limit drawn)NT$697MNT$679MNT$1.38B+47%
Correlation 0.15NT$478MNT$992MNT$1.45B+54%
Combined: PD × 1.5, LGD 90%, correlation 0.15NT$800MNT$1.15BNT$1.94B+106%

Raising the correlation to 0.15 leaves expected loss unchanged but takes formula capital from NT$444M to NT$992M: when borrowers default together, the average is the same and the bad year is much worse. Higher PDs mostly move expected loss (NT$478M to NT$711M). The combined case reaches NT$1.94B at 99.9%, 59% of exposure.

08 / Independent check in R

Same inputs, different code, same answers.

crosscheck.R reads the inputs Python used, refits the model with glm() and recomputes expected loss and capital from the formula. run.py stops if any gap is above its tolerance. R version 4.3.3 (2024-02-29).

What is comparedLargest gap
Model coefficients1.2e−9
PD of any account3.5e−10
Test AUC4.7e−13
Expected loss (relative)4.8e−13
IRB capital (relative)1.0e−12

09 / Limitations

What this model cannot tell you.

One month, not one yearThe target is a missed payment next month. Basel PDs cover twelve months, so the capital figures show the method, not a regulatory number for this book.
One market, one periodTaiwan, 2005, a period of heavy card defaults, which helps explain why 22.1% of accounts defaulted. There is no later sample, so the split is random rather than out of time.
Assumed LGD and CCFThe data has no recoveries or drawdown history. Both are stated, kept fixed per account, and stressed.
CalibrationHosmer-Lemeshow rejects exact calibration on the test sample. Ranking is reliable; the PD levels would be recalibrated before use.
Undocumented codesStatus codes −2 and 0 are not defined in the documentation. They are kept as separate groups so the data sets their risk, and their labels are the common reading, not a fact.
Approved accounts onlyEvery account was already approved, so the model has not seen rejected applicants. No reject inference is attempted.