SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 5.1.1 Sample spaces, events and the axioms of probabilityConcept · 30 min
      2. 5.1.2 CountingConcept · 30 min
      3. 5.1.3 Conditional probability and independenceConcept · 30 min
      4. 5.1.4 Bayes' ruleConcept · 25 min
      5. 5.1.5 Practice set: probabilityPractice set · 25 min
      6. 5.1.6 Case study: default and rating-migration probabilitiesCase study · 35 min

Level 5, Probability and statistics 5.1 Probability basics Lesson 6 of 6

Case studyLesson 5.1.6 in Level 5, Probability and statistics. About 35 minutes.

Case study: default and rating-migration probabilities

The transition matrix

A credit rating grades how likely a borrower is to default, that is, to miss a payment it owes. This case study uses three ratings, A (strongest), B and C, and a fourth state, D, for default.

A one-year transition matrix PP, also called a migration matrix, lists the chance of each move in a year. Its entry pijp_{ij} is the conditional probability (lesson 5.1.3) of being rated jj in a year, given rating ii now. Rows are the rating now; columns are the rating in a year.

now, in a yearABCD
A0.900.080.0150.005
B0.050.850.080.02
C0.010.090.800.10
D0001

In a year a borrower has exactly one of the four ratings: the four outcomes are disjoint and cover every case. So by the axioms (lesson 5.1.1), each row adds up to 1. The last row says a defaulted borrower stays in default: D is absorbing.

Read off the table:

  • Default within a year is the last column: 0.5% for A, 2% for B and 10% for C.
  • A downgrade is a move to any lower state. Those moves are disjoint, so their chances add: for B, 0.08+0.02=0.100.08 + 0.02 = 0.10. A and C have 10% too.
  • An upgrade for B is 5%, and for C, 0.01+0.09=10%0.01 + 0.09 = 10\%.

Try it

P[1] is row B, counting from 0, and P[:, 3] the default column. np.allclose checks that every row adds up to 1, allowing for float noise.

P[1, 2:].sum() adds the moves from B to C and D.

⌘/Ctrl + Enter runs the cell

Two years: total probability as a matrix product

To default within two years, a B borrower passes through some rating at the end of year one. Those ratings form a partition, so the law of total probability (lesson 5.1.3) adds over them:

P(D after 2 years∣B now)=∑jP(j after 1∣B now)×P(D after 2∣j after 1)\begin{aligned} &P(\text{D after 2 years} \mid \text{B now}) \\ &= \sum_j P(j \text{ after 1} \mid \text{B now}) \\ &\qquad \times P(\text{D after 2} \mid j \text{ after 1}) \end{aligned}

The second factor needs an assumption. The move in year two depends only on the rating at the start of that year, through the same table. Then it is pjDp_{j\text{D}}, the default column of PP.

rating after year 1, jjABCD
P(B→j)P(\text{B} \to j)0.050.850.080.02
P(j→D)P(j \to \text{D})0.0050.020.101
product0.000250.0170.0080.02

The products add up to 0.04525. That sum is row B of PP dotted with column D of PP: the entry (B,D)(\text{B}, \text{D}) of P2P^2, by the rule of lesson 3.2.1. So the matrix product gives every two-year probability at once:

now, in 2 yearsABCD
A0.814150.141350.03190.0126
B0.08830.73370.132750.04525
C0.02150.14930.647350.18185
D0001

The rows of P2P^2 add up to 1 again. A B borrower is rated C or in default after two years with chance 0.13275+0.04525=0.1780.13275 + 0.04525 = 0.178: the chance of ending the two years below B.

Why more than 3.96%? A steady 2% a year treats the borrower as a B in both years. In fact 8% are C after one year and then face 10%: they add 0.008. The 5% upgraded to A face only 0.5%, which does not make up for it.

Try it

The first line is the table of products and their total. P @ P is the two-year matrix, and its rows still add up to 1. Its entries have at most 5 decimals, so np.round(P2, 5) shows them exactly.

The last lines simulate 100,000 B borrowers for two years, with the seed 12. Each year, a uniform number u picks the next rating from the borrower's row: the first state whose running total of the row passes u.

⌘/Ctrl + Enter runs the cell

The seeded run gives 0.0446 for default and 0.1772 for C or D, against the exact 0.04525 and 0.178.

Five and ten years: matrix powers

The same step repeats every year, so kk years of moves are the power PkP^k (lesson 3.6.2). Because default is absorbing, being in D after kk years means having defaulted at some point within them. So column D of PkP^k is the chance of default within kk years.

years kk123510
A0.0050.01260.022690.049450.14223
B0.020.045250.073640.134430.2809
C0.10.181850.249680.354960.51839

Case study 2.5.5 gave a borrower a constant hazard rate. At 2% a year that meant 9.52% within 5 years, and a steady 2% each year gives 9.61%. The rating matrix says 13.44% for B, because B borrowers drift down.

For C it goes the other way. A steady 10% gives 40.95% within 5 years, but the matrix only 35.50%, because C borrowers drift up.

Default year by year

The chance of default in year kk, given no default before, is a conditional probability (lesson 5.1.3):

P(D within k)−P(D within k−1)1−P(D within k−1)\frac{P(\text{D within } k) - P(\text{D within } k - 1)}{1 - P(\text{D within } k - 1)}

The top is the chance of defaulting in year kk itself; the bottom shrinks the sample space to the borrowers still standing. For B it rises from 2% in year 1 to 3.69% in year 10. For A it rises from 0.5% to 2.33%, while for C it falls from 10% to 5.02%.

Default within kk years, and in year kk alone for borrowers still standing. Strong ratings get riskier as their survivors drift down; weak ones get safer, since the survivors are increasingly those that improved.

A single constant rate cannot show either shape. In this table every borrower defaults in the end: after 100 years, column D of P100P^{100} is above 0.95 for every rating. Lesson 5.11.2 studies such long runs as a Markov chain.

Try it

matrix_power(P, k) is PkP^k. The loop prints column D for A, B and C, then the year-by-year conditional chance for each rating.

The last line compares B and C with a steady 2% and 10% a year over 5 years.

⌘/Ctrl + Enter runs the cell

A loan book

A bank's book of loans is a mix of ratings. Write the shares as a row vector, s=(0.4,0.4,0.2,0)\mathbf{s} = (0.4, 0.4, 0.2, 0): 40% A, 40% B, 20% C and none in default. By the law of total probability, next year's mix is the row vector times the matrix:

(sP)j=∑isi pij(\mathbf{s}P)_j = \sum_i s_i\, p_{ij}

The default share after one year is 0.4×0.005+0.4×0.02+0.2×0.10=0.030.4 \times 0.005 + 0.4 \times 0.02 + 0.2 \times 0.10 = 0.03. After five years it is the last entry of sP5\mathbf{s}P^5, 0.1445.

Lesson 3.6.2's two towns used the other layout: the columns of MM added up to 1, and MM multiplied a column of shares. Here the rows add up to 1, and the row of shares multiplies from the left. The two are transposes: P⊤P^\top is lesson 3.6.2's form.

Try it

s @ P is the next year's mix and s @ P5 the mix after five years. The last line works in lesson 3.6.2's layout: (P⊤)2(P^\top)^2 times the column for a B borrower gives row B of P2P^2.

⌘/Ctrl + Enter runs the cell
Check yourself
Why is a B borrower's chance of default within two years 4.525%, above the 3.96% of a steady 2% a year?

An early-warning signal: Bayes' rule

Banks watch for warning signs: late payments, a broken loan condition, a falling share price. Say the bank's signal has been tested on past borrowers.

Of those that defaulted within the year, 70% had been flagged at its start: the hit rate. Of those that did not, 10% were flagged: the false-alarm rate.

A B borrower is flagged. Its prior is the table's 2%, and Bayes' rule (lesson 5.1.4) gives

P(D∣flag)=0.7×0.020.7×0.02+0.1×0.98=0.0140.112=0.125\begin{aligned} P(\text{D} \mid \text{flag}) &= \frac{0.7 \times 0.02}{0.7 \times 0.02 + 0.1 \times 0.98} \\ &= \frac{0.014}{0.112} = 0.125 \end{aligned}

A table of counts checks it. Of 10,000 B borrowers, 200 default, and 140 of those were flagged. Of the 9,800 others, 980 were flagged, so 140 of the 1,120 flags are real: 12.5%.

In odds (lesson 5.1.4), the likelihood ratio is 0.70.1=7\frac{0.7}{0.1} = 7. The odds go from 298\frac{2}{98} to 1498=17\frac{14}{98} = \frac{1}{7}, a probability of 18\frac{1}{8}.

The flag makes this B borrower riskier than the table's C, at 10%. No flag works the other way: the likelihoods are 1−0.7=0.31 - 0.7 = 0.3 and 1−0.1=0.91 - 0.1 = 0.9, and the chance falls to 0.676%.

ratingpriorflaggednot flagged
A0.0050.0340.00167
B0.020.1250.00676
C0.100.43750.03571

The base-rate trap of lesson 5.1.4 shows up for A. A flagged A borrower still has only a 3.4% chance of default: most flags on strong borrowers are false alarms.

Try it

posterior is Bayes' rule with a hit rate and a false-alarm rate. Without a flag, both are replaced by their complements.

Then a million B borrowers with the seed 11: each defaults with probability 0.02 and is flagged with probability 0.7 or 0.1.

⌘/Ctrl + Enter runs the cell

In the seeded run 111,993 borrowers were flagged, and a share 0.1266 of them defaulted, against the exact 0.125. Among the rest the share was 0.00676.

Check yourself
The signal flags an A borrower, whose one-year default chance in the table is 0.5%. What is it now?

Try it

The first line renormalises a rounded row that adds up to 1.0018. The second shows a typo caught by the row check: B's default entry typed as 0.03.

⌘/Ctrl + Enter runs the cell
Exercise

Default probabilities from a rating table

Write two functions.

  1. default_probability(P, start, years) returns the chance that a borrower in state start is in default after years years. P is a square transition matrix (a NumPy array or nested lists), rows the state now, with default as the last state. Return entry (start, last) of PyearsP^{\text{years}}, so 0 years gives 0, or 1 if start is the default state. Raise ValueError if P is not square, has an entry outside [0,1][0, 1], or has a row that does not add up to 1 within 10−910^{-9}. Raise it too if years is negative or not a whole number.
  2. flagged_default(prior, hit, false_alarm, flagged=True) returns the chance of default after the signal, by Bayes' rule. Without a flag (flagged=False), use 1−hit1 - \text{hit} and 1−false_alarm1 - \text{false\_alarm} as the likelihoods. Raise ValueError if any input is outside [0,1][0, 1], or if the evidence has probability 0.

The tests use the table of this case study: 0.04525 for B over 2 years, 0.125 for a flagged B.

See the tests
import numpy as np

P = [
    [0.90, 0.08, 0.015, 0.005],
    [0.05, 0.85, 0.08, 0.02],
    [0.01, 0.09, 0.80, 0.10],
    [0.00, 0.00, 0.00, 1.00],
]


def _raises(f, *args, what=""):
    try:
        f(*args)
    except ValueError:
        return
    raise AssertionError(f"expected ValueError for {what}")


def test_b_two_years():
    """A B borrower defaults within 2 years with probability 0.04525"""
    expect_close(default_probability(P, 1, 2), 0.04525, what="default_probability(P, 1, 2)", rel=1e-9)


def test_one_year_is_the_table():
    """After 1 year the answer is the table's default column: 0.005, 0.02, 0.10"""
    for start, expected in [(0, 0.005), (1, 0.02), (2, 0.10)]:
        expect_close(default_probability(P, start, 1), expected, what=f"default_probability(P, {start}, 1)", rel=1e-12)


def test_longer_horizons():
    """B within 5 years is about 0.1344, A within 10 years about 0.1422"""
    expect_close(default_probability(P, 1, 5), 0.13442818405, what="default_probability(P, 1, 5)", rel=1e-9)
    expect_close(default_probability(P, 0, 10), 0.1422328059941972, what="default_probability(P, 0, 10)", rel=1e-9)


def test_zero_years_and_default_start():
    """After 0 years nobody has defaulted, and a defaulted borrower stays in default"""
    expect_close(default_probability(P, 1, 0), 0.0, what="default_probability(P, 1, 0)")
    expect_close(default_probability(P, 3, 7), 1.0, what="default_probability(P, 3, 7)")


def test_other_sizes_and_arrays():
    """A two-state table works too: 1 - 0.9 cubed after 3 years, from a NumPy array"""
    expect_close(default_probability(np.array([[0.9, 0.1], [0.0, 1.0]]), 0, 3), 0.271, what="two-state table", rel=1e-9)


def test_bad_tables():
    """A row adding up to 1.01, a negative entry or a non-square table raises ValueError"""
    typo = [row[:] for row in P]
    typo[1][3] = 0.03
    _raises(default_probability, typo, 1, 2, what="a row adding up to 1.01")
    negative = [[1.1, -0.1], [0.0, 1.0]]
    _raises(default_probability, negative, 0, 1, what="a negative entry")
    _raises(default_probability, [[0.5, 0.5, 0.0], [0.0, 0.5, 0.5]], 0, 1, what="a 2 x 3 table")


def test_bad_years():
    """Negative or fractional years raise ValueError"""
    _raises(default_probability, P, 1, -1, what="years = -1")
    _raises(default_probability, P, 1, 2.5, what="years = 2.5")


def test_flag_on_b():
    """A flagged B borrower (prior 2%, hit 70%, false alarm 10%) defaults with probability 0.125"""
    expect_close(flagged_default(0.02, 0.7, 0.1), 0.125, what="flagged_default(0.02, 0.7, 0.1)", rel=1e-9)


def test_no_flag_on_b():
    """Without a flag the same borrower's chance falls to about 0.00676"""
    expect_close(flagged_default(0.02, 0.7, 0.1, flagged=False), 0.006756756756756757, what="flagged_default(..., flagged=False)", rel=1e-9)


def test_edge_cases():
    """An uninformative signal leaves the prior; no false alarms give 1; a prior of 0 stays 0"""
    expect_close(flagged_default(0.3, 0.6, 0.6), 0.3, what="hit = false alarm", rel=1e-12)
    expect_close(flagged_default(0.01, 0.9, 0.0), 1.0, what="false_alarm = 0", rel=1e-12)
    expect_close(flagged_default(0.0, 0.9, 0.05), 0.0, what="prior = 0")


def test_bad_probabilities():
    """Inputs outside [0, 1], or evidence with probability 0, raise ValueError"""
    _raises(flagged_default, 2.0, 0.7, 0.1, what="prior = 2")
    _raises(flagged_default, 0.02, 70, 0.1, what="hit = 70 (a percentage, not a decimal)")
    _raises(flagged_default, 0.02, 0.0, 0.0, what="a signal that never fires")

Summary card

  • A transition matrix holds conditional probabilities pij=P(j in a year∣i now)p_{ij} = P(j \text{ in a year} \mid i \text{ now}); each row adds up to 1, and default is absorbing.
  • Two-year probabilities come from the law of total probability over the rating after one year, which is the matrix product P2P^2. For B, 4.525% against 3.96% at a steady 2%.
  • Column D of PkP^k is the chance of default within kk years. The chance in each year, given survival, rises for strong ratings and falls for weak ones.
  • A loan book's mix moves as a row vector times PP; lesson 3.6.2's layout is the transpose.
  • A warning signal updates the default chance by Bayes' rule: a flagged B goes from 2% to 12.5%, a flagged A only to 3.4%.

Free further reading

Optional. This lesson covers what you need; these add more examples and practice. All are free to read.