eqlx.aiOpen research

eqlx.org / eqlx-credit

Python library · v0.2 · Apache-2.0

PD model validation, the way supervisors ask for it.

A bank's rating model gives every borrower a PD, the chance of default within a year, and once a year someone must check that model against what really happened. eqlx-credit computes those checks, the same way every time, each one from a public source.

What goes in, what comes out

No language model inside. Same input, same answer. For people who build, validate or review credit-risk models, and for their agents.

You give

  • a rating scale: borrowers and defaults per grade
  • the PD each grade had at the start of the year
  • default rates of earlier years
  • your thresholds

eqlx-credit computes

  • how well the model ranks, with its margin of error
  • defaults seen against defaults expected
  • a traffic light per grade
  • drift over the years and in the mix

You get

  • every figure with its inputs
  • observed and expected values side by side
  • rows ready for a report table
  • the public source behind each test

One yearly check, start to finish

A made-up corporate portfolio of 6,350 borrowers in six grades. It ships in the repository, with two problems planted in it.

1You enter the rating scale

One row per grade, safest first. PD is what the model promised at the start of the year.

GradePDBorrowersDefaults
10.12 %8201
20.35 %1,4504
30.70 %1,91011
41.40 %1,32019
53.00 %64034
67.00 %21012

2You set the policy

Your institution's choices. The library does not pick them.

A result counts as a finding below5 %
How strongly defaults move together0.12
Traffic light turns yellow at95 %
and red at99.9 %
Ranking score when the model was approved0.860
Shift in the mix: watch / material0.10 / 0.25

3Does the model still rank well?

AUC: how often the model rates a defaulter as riskier than a non-defaulter. 0.5 is a coin flip, 1 is perfect.

At approval
0.860
This year0.748 to 0.841
0.795
Coin flip
0.5
p = 0.003 the drop is more than chance

4Defaults per grade: seen against expected

Expected = borrowers × PD. The bar is what happened.

Grade 1expected 1.0
1
Grade 2expected 5.1
4
Grade 3expected 13.4
11
Grade 4expected 18.5
19
Grade 5expected 19.2
34
Grade 6expected 14.7
12

5Is the PD of grade 5 too low?

Jeffreys test, the one the ECB asks for: how well do the defaults fit the PD? Run for every grade.

PD promised
3.00 %
Defaulted
5.31 %
p = 0.0009 below 5 %: a finding. The other five grades pass.

6Traffic light per grade

Read two ways: as if every default were separate, and allowing for bad years when defaults rise together.

GradeSeparateTogether (0.12)
1 to 4greengreen
5yellowgreen
6greengreen

Grade 5 is yellow only in the first reading. Bad year or wrong PD? That is for the validator to judge.

7Right over several years?

Share of the whole portfolio that defaulted each year, against a PD of 1.10 %.

2021
1.05 %
2022
1.20 %
2023
0.98 %
2024
1.41 %
2025
1.28 %
p = 0.139 average 1.18 %: no finding

8Has the mix of borrowers moved?

PSI: one number for how far the spread over grades shifted since last year. 0 means no change.

This portfolio
0.0041
Your watch level
0.10
Your material level
0.25
stable far below the watch level
4 flagsagainst your thresholds

What comes back

The ranking score fell from 0.860 to 0.795 (p = 0.003). Grade 5 had 34 defaults against 19.2 expected (p = 0.0009) and is yellow if defaults are separate. A test across all grades at once, Hosmer-Lemeshow, gives p = 0.0437. No opinion is given: that stays with the validator.

Evidence, not a verdict

Each figure above is the library's output for this portfolio. Both planted problems, the weaker ranking and grade 5, were found.

What it covers

Grouped by the question a validator asks.

QuestionFunctionsPublic source
Does the model still rank well?auc_ci, auc_ci_from_counts, auc_decline_test, gini, ksDeLong et al. (1988); ECB instructions for IRB validation reporting (2019)
Are the grade PDs right this year?jeffreys_test, binomial_test(rho=), traffic_light, hosmer_lemeshowECB (2019); BCBS Working Paper 14 (2005); Tasche (2003)
Are they right over several years, or per borrower?normal_test, spiegelhalterBCBS WP14 (2005); Spiegelhalter (1986)
What were the default rates?cohort_default_rates, long_run_averageEBA/GL/2017/16
Has the portfolio moved or bunched up?psi, hhi(exposure=)Siddiqi (2006)
How do I set the PD level and curve?anchor_to_central_tendency, fit_ct_ar, apply_pd_floor, master scaleTasche (2013); Basel II, CRR, Basel III floors
Too few defaults to estimate a PD?pluto_tasche, van_der_burgtPluto & Tasche (2005); van der Burgt (2008)
What capital does it need?risk_weight, capital_requirementBasel II ¶272–330 and Annex 5

Capital: the Basel risk weight for any PD

The risk weight is the share of a loan a bank must count when it works out its capital. Loss rate 45 % in both charts.

It rises with the PD

Loans to large companies.

It depends on the kind of loan

The same PD of 0.50 % in five loan classes.

All twelve figures are the library's output and equal the figures printed in Basel II Annex 5.

Too few defaults to trust the count

Six grades with 8 defaults among 1,750 borrowers. The Pluto-Tasche method gives a cautious upper limit for each PD: 90 % confidence, defaults moving together at 0.12.

A grade with no defaults still gets a PD above zero. The limits never fall as the grades get riskier.

How an agent uses it

The agent finds the data, picks the test and writes the summary. It does not produce a number itself: a Gini or a risk weight written by a language model can look right and be wrong.

Get the inputOne grade table and your policy
RunEvery test in one pass
ReadFlags raised against your thresholds
DraftThe report, in your language
Hand overOpinion and signature left empty

These are the steps of the Agent Skill annual-pd-validation in the repository; the example above is its bundled portfolio. Today eqlx-credit is a Python library with structured results that any agent framework can call. An MCP server, so agents can call it directly, is next.

How it was tested

Every function is compared with something it was not derived from.

Published figures, reproduced

✓8 of 8 columnsBasel II Annex 5 risk weights · 152 figures, none off by more than 0.01
✓0.29 %, 0.33 %, 0.76 %Pluto and Tasche 2005 · the paper's worked example
✓94.5 % of 400simulated portfolios where the 95 % interval of the AUC held the true value
✓29 Februarydefault rates per year on hand-built cases, including the one-year boundary

Why "defaults move together" matters

500 borrowers, PD 1 %, 12 defaults. How likely is a year this bad if the PD is right?

0.5 %If every default is treated as separateflags too often
10.5 %The library, with defaults moving together at 0.12matches the simulation
10.5 %200,000 simulated years, counted one by oneindependent check

And on every change

62 testsagainst closed forms, published tables, brute force and simulation
Brute forcethe margin of error of the AUC (DeLong) matches a slow borrower-by-borrower calculation
2 dependenciesNumPy and SciPy; Python 3.10 or later

What it does not do

No sign-offIt computes the evidence. The validation opinion stays with your validators.
No choosing your thresholdsTest levels, traffic-light limits and PSI readings are policy choices you set.
PD models firstLoss-rate (LGD) and credit-line (CCF) back-tests, rating migration and override analysis come in version 0.3.
Still version 0.2Function names and arguments may still change.
pip install eqlx-credit