Skip to content

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_id
  • principal
  • annual_rate
  • term_months
  • start_date
  • status
  • fico_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, and status
  • a loan history panel with loan_id, fund_date, and as_of_date

If you pass a square matrix, it should:

  • use the same states on rows and columns
  • include a Charged Off state (or legacy Default)
  • 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 distribution
  • summarize_lifetime_loss(...) returns a compact reserve summary with:
  • total_portfolio_balance
  • estimated_lifetime_loss
  • reserve_ratio
  • lgd_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 will reject invalid transition assumptions.
  • forecast_portfolio_states(...) still requires an explicit square matrix even though forecast_lifetime_loss(...) and summarize_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. In that case, move up to the simulation or Rollforward workflow rather than stretching this baseline too far.

Run the packaged demo end-to-end with:

python -m cranalytics.examples.core_lifetime_loss