Lifetime Loss Forecasting Tutorial
This guide shows the baseline transition-matrix workflow for estimating portfolio lifetime loss.
Problem
Use this workflow when you already have:
- a loan-level portfolio snapshot
- transition input that describes monthly state migration
- an LGD assumption you want to apply across the book
This is the right starting point for reserve estimation prototypes, scenario baselines, and sanity checks before moving into simulation or rollforward-based workflows.
Inputs
Your portfolio DataFrame should include:
loan_idprincipalannual_rateterm_monthsstart_datestatusfico_score
Your transition input can be one of:
- a square transition matrix with the same states on rows and columns
- a transition ledger with
loan_id,period, andstatus - a loan history panel with
loan_id,fund_date, andas_of_date
If you pass a square matrix, it should:
- use the same states on rows and columns
- include a
Charged Offstate (or legacyDefault) - have each row sum to 1
Code
import pandas as pd
from cranalytics import (
forecast_lifetime_loss,
forecast_portfolio_states,
summarize_lifetime_loss,
)
portfolio_df = pd.DataFrame(
{
"loan_id": ["L101", "L102", "L103"],
"principal": [10000.0, 15000.0, 8000.0],
"annual_rate": [0.08, 0.10, 0.09],
"term_months": [36, 36, 24],
"start_date": [
pd.Timestamp("2024-01-01"),
pd.Timestamp("2024-02-01"),
pd.Timestamp("2024-01-15"),
],
"status": ["Current", "Delinquent", "Current"],
"fico_score": [720, 640, 700],
}
)
states = ["Current", "Delinquent", "Charged Off"]
migration_matrix = pd.DataFrame(
[[0.95, 0.04, 0.01], [0.10, 0.80, 0.10], [0.00, 0.00, 1.00]],
index=states,
columns=states,
)
initial_states = (
portfolio_df["status"]
.value_counts()
.reindex(migration_matrix.index, fill_value=0)
.astype(float)
)
state_forecast = forecast_portfolio_states(
migration_matrix,
initial_states,
n_periods=12,
)
print(state_forecast.head())
lifetime_loss = forecast_lifetime_loss(
portfolio_df,
migration_matrix,
lgd=0.55,
as_of_date=pd.Timestamp("2024-02-01"),
)
print(f"Estimated lifetime loss: ${lifetime_loss:,.2f}")
summary = summarize_lifetime_loss(
portfolio_df,
migration_matrix,
lgd=0.55,
as_of_date=pd.Timestamp("2024-02-01"),
)
print(summary)
# The same API also accepts transition ledgers or loan history panels:
history_df = pd.DataFrame(
{
"loan_id": ["L101", "L101", "L102", "L102"],
"fund_date": [
pd.Timestamp("2023-12-01"),
pd.Timestamp("2023-12-01"),
pd.Timestamp("2023-12-15"),
pd.Timestamp("2023-12-15"),
],
"as_of_date": [
pd.Timestamp("2024-01-01"),
pd.Timestamp("2024-02-01"),
pd.Timestamp("2024-01-01"),
pd.Timestamp("2024-02-01"),
],
"source_status": ["Current", "Current", "Current", "Charged Off"],
"current_balance": [10000.0, 9800.0, 15000.0, 0.0],
"annual_rate": [0.08, 0.08, 0.10, 0.10],
"term_months": [36, 36, 36, 36],
}
)
loss_from_history = forecast_lifetime_loss(
portfolio_df,
history_df,
lgd=0.55,
as_of_date=pd.Timestamp("2024-02-01"),
)
print(f"Estimated lifetime loss from history: ${loss_from_history:,.2f}")
Output
You should expect two useful views:
forecast_portfolio_states(...)returns a period-by-period state distributionsummarize_lifetime_loss(...)returns a compact reserve summary with:total_portfolio_balanceestimated_lifetime_lossreserve_ratiolgd_assumption
This workflow is intentionally simple. It is best used as a baseline, not as a full operating loss engine.
Common errors
- Matrix states and portfolio statuses do not match. If your portfolio contains extra labels that are not in the matrix, normalize or filter them first.
- The matrix is not square or its rows do not sum to 1. The validator rejects invalid transition assumptions.
forecast_portfolio_states(...)still requires an explicit square matrix even thoughforecast_lifetime_loss(...)andsummarize_lifetime_loss(...)also accept ledgers and loan history.- You forget
as_of_date. If omitted, the current date is used, which changes remaining term calculations. - You need cashflow-level behavior, amortization effects, or dynamic risk assumptions. Move up to simulation or Rollforward instead of stretching this baseline too far.
Run the packaged demo end-to-end with:
python -m cranalytics.examples.core_lifetime_loss