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 rejects 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. 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