4.1 — Credit Risk Data: Default, EAD, LGD, Feature Engineering, Multiple Regression

Chapter 4 · Statistical Predictive Models

Prof. Xuhu Wan

Section 4.1 · Chapter 4 · Learning Statistics with Python

Credit Risk Data: Default, EAD, LGD, Feature Engineering, Multiple Regression

Statistical Predictive Models

Prof. Xuhu Wan

ISOM, HKUST Business School · 2026 Edition

Credit Risk Data: Default, EAD, LGD, Feature Engineering, Multiple Regression

A lender’s loss on a loan is the product of three things — whether it defaults, how much is still owed when it does, and how little comes back. You will build all three from raw columns, engineer 25 features, and discover how much (and how little) a linear model explains.

How does a lending platform make money?

LendingClub matches borrowers (debt consolidation, big purchases, medical bills) with funding. The platform underwrites each application with a credit-risk model, sets the rate and term, and either keeps the loan or sells it to investors.

Which statement best describes the platform’s economics?

  • It earns only when a borrower defaults
  • It earns a flat fee per application, so risk is irrelevant
  • It earns fees and net interest, so the whole business rests on pricing risk correctly
  • It earns nothing until the loan is fully repaid

Expected loss has three factors

\[\text{EL} = \underbrace{\text{PD}}_{\text{probability of default}} \times \underbrace{\text{EAD}}_{\text{exposure at default}} \times \underbrace{\text{LGD}}_{\text{loss given default}}\]

Which factor calls for a classification model rather than a regression?

  • EAD — it is the exposure in dollars
  • PD — it is the probability of a 0/1 outcome
  • LGD — it is a loss fraction
  • All three are regressions

This section builds EAD and LGD by regression. PD waits for §4.3.

Load 8 000 loans

The catalog says 20 % of loans are charged off. What does the second line print for Charged Off?

print(debt["loan_status"].value_counts(normalize=True).round(3))

0.2

(8000, 29) — a random sample of the 150 000-loan original. Exactly 80 % Fully Paid, 20 % Charged Off. Every loan here has finished its life, so we know the outcome.

Define default, then EAD and LGD from the ledger

Three ledger columns carry the story: funded_amnt (what was lent), total_rec_prncp (principal repaid), recoveries (cash clawed back after charge-off).

For a Fully Paid loan, what is EAD = (funded_amnt − total_rec_prncp) / funded_amnt?

  • 0 — all principal came back
  • 1 — the full amount was exposed
  • Undefined
  • Equal to the interest rate

Build the three targets

Among the 1 604 charged-off loans the mean EAD is 0.698 — on average 70 % of the principal was still outstanding — and mean LGD is 0.892: recoveries claw back barely a tenth. The 1e-4 guards the one loan with EADamount = 0. Six loans recovered more than the outstanding principal (late fees, collection interest), which is why the minimum LGD is −0.177 — real ledgers do this, and we keep the raw number.

Feature Engineering

Raw columns are strings, dates and NaNs. A regression needs numbers with a meaning. You will convert term and employment length, build a credit age, a FICO midpoint, two “time since trouble” clocks, and dummies for three categoricals.

Term and employment length are strings

emp_length holds values like "10+ years", "< 1 year", "3 years" and NaN. What does str.extract(r"(\d+)") return for "10+ years" and for "< 1 year"?

pd.Series(["10+ years", "< 1 year"]).str.extract(r"(\d+)")[0].tolist()

['10', '1']

"< 1 year" would extract as 1, so we override it to 0. The 484 loans with no employment record get employment = 0 and a flag employed = 0 — the flag lets the model treat “unknown” differently from “under a year”.

Credit age, FICO midpoint, instalment burden

earliest_cr_line is like "Jan-1986". Days from 1 Jan 1986 to 29 Dec 2018 (the data’s end date)?

(pd.Timestamp("2018-12-29") - pd.Timestamp("1986-01-01")).days

12050

format="%b-%Y" parses Jan-1986 directly — no guessing, no warnings. Credit histories run from 1 277 to 20 757 days (3.5 to 57 years). install is the monthly instalment as a share of annual income — a burden ratio, not a dollar amount. Three applicants report zero income: dividing by NaN rather than by 0 keeps their ratio missing instead of infinite.

Months since last delinquency — what does NaN mean?

mths_since_last_delinq is NaN for half the loans. What should NaN become?

  • 0 — missing means no information, use the smallest value
  • The column mean
  • A large number such as 360 — NaN means “never delinquent”
  • Drop those rows

balance (instalment-loan balance over income) is NaN when the borrower has no instalment loans — there the honest fill is 0.

Dummies for grade, home ownership, verification

Grade has 7 levels A–G. Why keep only 6 dummy columns in a regression with an intercept?

  • Grade G is too rare to matter
  • All 7 would sum to the intercept column — perfect collinearity
  • statsmodels allows at most 6 dummies
  • To make the coefficients smaller

Home ownership also has ANY and OTHER (one loan each). We keep OWN, RENT, MORTGAGE as the notebook does — and in §4.2 the VIF will show why that was a trap.

Assemble, shuffle, split

Why permute the rows before taking the first 70 % as training data?

  • To make the regression converge faster
  • Because statsmodels requires it
  • The file may be sorted, so an unshuffled split would not be representative
  • To remove duplicates

25 features. Six rows are dropped for a missing dti or revol_util — the three zero-income applicants among them. 5 595 / 2 399 loans train/test; of those, 1 146 / 458 defaulted — the EAD and LGD regressions see only these.

OLS for EAD on the defaulted loans

R² = 0.192. Longer term (t = 7.5) and higher int_rate (t = 3.5) mean more principal is still outstanding when the loan fails; a heavier instalment install (t = −2.7) means the borrower had paid more down before failing. fico and dti add nothing once the rest are in.

Now predict: how well does OLS explain LGD?

Same 25 features, target LGD. The R² will be roughly:

  • Under 0.05 — recoveries are nearly unpredictable from origination data
  • About 0.2, like EAD
  • About 0.5
  • Above 0.8 — LGD is mechanical

R² = 0.039, adjusted 0.018. The 150 000-loan notebook found 0.012. LGD is, for practical purposes, a constant near 0.89 — and a model that knows it is a constant is more useful than one that pretends otherwise.

Judge on data the model never saw

\[R^2_{\text{adj}} = 1 - (1 - R^2)\,\frac{n-1}{n-k-1}, \qquad \text{RMSE} = \sqrt{\tfrac{1}{n}\sum (y_i - \hat y_i)^2}\]

EAD: train adjusted R² 0.174 → test 0.107. LGD: test adjusted R² is negative (−0.080) — worse than predicting the mean. Test RMSE 0.190 for EAD says a typical exposure forecast is off by 19 percentage points of principal.

Your turn: expected loss per dollar lent

You have all three factors on the training data. Put them together.

EL = PD × mean EAD × mean LGD, using the default rate in train and the mean EAD and LGD of the defaulted loans train_d. Fix the EL line.

About 12.7 cents per dollar lent — which is why the average interest rate in this book is 13 %.

What you discovered

  • A lender’s loss is PD × EAD × LGD; PD is a classification, EAD and LGD are regressions on the charged-off subset (1 604 of 8 000 loans).
  • Feature engineering is a sequence of judgements: "< 1 year" → 0, NaN delinquency → 360 (never), missing balance → 0, one dummy dropped per categorical.
  • OLS explains 19 % of EAD on training data, 16 % on test — and 4 % of LGD, which is effectively a constant near 0.89.
  • Adjusted R² and RMSE on the test set are the numbers you report; a negative test adjusted R² means “predict the mean instead”.

Next: §4.2 — can interactions and best-subset search raise that 0.107?