Artefacts
Reproducible artefacts
A model is only as trustworthy as the evidence behind it — and I’d rather reproduce a result than take it on trust. The figure on the front page is generated from the code below; here is the maths, so you can check it yourself.
Pluto–Tasche PD upper bound
For a low-default portfolio — a rating grade with very few or zero observed defaults — the naïve PD estimate is zero, which is useless for capital. Pluto and Tasche (2005) instead take the most-prudent PD still consistent with the data at a chosen confidence level. With n obligors and no defaults, the independent-obligor bound has the closed form PD = 1 − (1 − γ)^(1/n); the single-factor version adds asset correlation. Fewer names, or a higher required confidence, push the conservative PD sharply up — exactly what the front-page chart shows.
\"\"\"
Pluto-Tasche most-prudent PD upper bound for low-default portfolios.
Reference: Pluto, K. and Tasche, D. (2005),
\"Estimating Probabilities of Default for Low Default Portfolios\".
For a rating grade with n obligors and ZERO observed defaults, the most-prudent
PD estimate at confidence level gamma is the largest PD still consistent with
observing no defaults at that confidence:
independent obligors:
(1 - PD)^n = 1 - gamma => PD = 1 - (1 - gamma)^(1 / n)
single systematic factor (asset correlation rho):
E_Y[ (1 - N( (N^-1(PD) - sqrt(rho) Y) / sqrt(1 - rho) ))^n ] = 1 - gamma
solved for PD, with Y ~ N(0, 1).
\"\"\"
import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq
from scipy.integrate import quad
import matplotlib.pyplot as plt
def pd_bound_independent(gamma, n):
\"\"\"Closed-form PD upper bound assuming independent obligors, zero defaults.\"\"\"
return 1.0 - (1.0 - gamma) ** (1.0 / n)
def _prob_zero_defaults(pd, n, rho):
\"\"\"P(0 defaults) for n obligors under a single Gaussian factor.\"\"\"
def integrand(y):
cond_pd = norm.cdf((norm.ppf(pd) - np.sqrt(rho) * y) / np.sqrt(1.0 - rho))
return (1.0 - cond_pd) ** n * norm.pdf(y)
return quad(integrand, -8.0, 8.0)[0]
def pd_bound_factor(gamma, n, rho):
\"\"\"PD upper bound with asset correlation rho (root of the zero-default equation).\"\"\"
return brentq(lambda pd: _prob_zero_defaults(pd, n, rho) - (1.0 - gamma),
1e-9, 1.0 - 1e-9)
if __name__ == \"__main__\":
gammas = np.linspace(0.50, 0.999, 400)
fig, ax = plt.subplots(figsize=(7, 6))
for n in (50, 200, 1000, 5000):
ax.plot(gammas, [pd_bound_independent(g, n) * 100 for g in gammas],
label=f\"n = {n}\")
ax.set_xlabel(\"Confidence level\")
ax.set_ylabel(\"PD upper bound (%)\")
ax.set_title(\"Pluto-Tasche PD upper bound - zero-default portfolio\")
ax.legend()
fig.tight_layout()
fig.savefig(\"pluto_tasche.png\", dpi=200)
The structural (Merton) and portfolio (Vasicek / Basel IRB) figures are built the same way — from the model, in code. Back to home