Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Rating assignment

credit_tools.rating.assign_rating maps borrowers to rating buckets given:

  • a borrower_id, ml_score, and observed defaulted outcome per borrower, and

  • a rating_scale: a {rating: expected_default_rate} dictionary.

It fits a monotonic realized-default-rate curve against ml_score (higher score = higher risk), then maps each borrower’s calibrated default probability to the rating with the closest expected default rate. The scale itself is just data — Moody’s idealized default rates ship as a bundled resource, but any {rating: edr} mapping works.

import random

from credit_tools.rating import assign_rating
from credit_tools.resources import load_rating_scale

random.seed(0)

Load a rating scale

load_rating_scale reads any bundled resources/<name>.json file.

scale = load_rating_scale("moody")
dict(list(scale.items())[:5])
{'Aaa': 0.0001, 'Aa1': 0.0002, 'Aa2': 0.0003, 'Aa3': 0.0004, 'A1': 0.0006}

Simulate a borrower portfolio

In practice ml_scores and defaulted come from your model’s predictions and observed outcomes. Here we simulate them so the notebook is self-contained.

n = 2000
borrower_ids = [f"b{i}" for i in range(n)]
ml_scores = [random.random() for _ in range(n)]
defaulted = [random.random() < score for score in ml_scores]

Assign ratings

ratings = assign_rating(borrower_ids, ml_scores, defaulted, scale)
{borrower_ids[i]: ratings[borrower_ids[i]] for i in range(5)}
{'b0': 'C', 'b1': 'C', 'b2': 'Ca', 'b3': 'Ca', 'b4': 'C'}

Check calibration

For each rating, the realized default rate among the borrowers assigned to it should track the target expected default rate from the scale.

from collections import defaultdict

realized = defaultdict(list)
for i, borrower_id in enumerate(borrower_ids):
    realized[ratings[borrower_id]].append(defaulted[i])

for rating in sorted(realized, key=lambda r: scale[r]):
    outcomes = realized[rating]
    print(
        f"{rating:5s} target_edr={scale[rating]:.4f}  n={len(outcomes):4d}  "
        f"realized_dr={sum(outcomes) / len(outcomes):.4f}"
    )
Aaa   target_edr=0.0001  n=  48  realized_dr=0.0000
B1    target_edr=0.0270  n=  88  realized_dr=0.0227
Caa1  target_edr=0.0900  n=  56  realized_dr=0.0893
Caa2  target_edr=0.1300  n= 216  realized_dr=0.1389
Ca    target_edr=0.3000  n= 465  realized_dr=0.3032
C     target_edr=0.5000  n=1127  realized_dr=0.7098