Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 46 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,44 @@

**When should actuaries trust neural networks for mortality forecasting?**

A comprehensive benchmark of 14 classical and neural mortality models with a practical decision framework, an original hybrid model (LC-ResNet), and an actuarial case study quantifying model risk in EUR.
A reproducible comparison of 14 mortality-forecasting implementations, including a proposed hybrid (LC-ResNet) and an actuarial case study that translates model risk into EUR.

---

## Highlights

- **14 models** compared: 8 classical (implemented from scratch) + 6 neural (PyTorch)
- **14 implementations**: 5 classical structural models + 2 simple baselines + 6 neural models + 1 hybrid
- **8 countries** from the Human Mortality Database (1950–2023, including COVID)
- **Rolling-origin validation** with actuarial metrics (e₀, annuity ä₆₅)
- **Decision framework**: "which model to use when" based on data length, horizon, age group, interpretability
- **Original hybrid model (LC-ResNet)**: Lee-Carter Poisson skeleton + neural residual correction with horizon shrinkage
- **Actuarial case study**: annuity pricing for 1,000 pensioners, Solvency II longevity shock, model risk in EUR
- **Rolling-origin validation** with log-rate and age-100-truncated actuarial metrics (e₀, annuity ä₆₅)
- **Objective-specific guidance** based on data length, horizon, age group, and transparency constraints
- **LC-ResNet**: Poisson Lee-Carter skeleton + neural residual correction with horizon shrinkage
- **Reproducible case study**: 1,000 French pensioners, 2033 rates, Solvency II longevity shock
- **Interactive dashboard** (Streamlit)

## Models

| Classical (from scratch) | Neural (PyTorch) | Hybrid (ours) |
| Structured / baseline | Neural (PyTorch) | Hybrid |
|---|---|---|
| Lee-Carter (SVD) | LSTM on κ_t | **LC-ResNet** |
| Lee-Miller | GRU on κ_t | |
| Booth-Maindonald-Smith | Bi-LSTM on κ_t | |
| Poisson Lee-Carter | Transformer on κ_t | |
| CBD (Cairns-Blake-Dowd) | FFNN with embeddings | |
| Hyndman-Ullah (FDA) | CNN on mortality surface | |
| Random walk with drift | | |
| Poisson Lee-Carter | Bi-LSTM on κ_t | |
| CBD (ages 60–100) | Transformer on κ_t | |
| H-U-style functional model | FFNN with embeddings | |
| Random walk with drift | CNN on mortality surface | |
| Frozen rates | | |

The H-U implementation uses spline smoothing, six functional principal components, and random-walk-with-drift score forecasts; it is a simplified H-U-style model rather than the full robust automatic-ARIMA procedure.

## Main findings

- The age-specific random walk with drift has the lowest full-grid log-rate RMSE at every tested horizon.
- LC-ResNet has the lowest one-year error among structured models, but Lee-Miller is better at 20 years.
- H-U has the lowest 20-year life-expectancy RMSE; the FFNN has the lowest 20-year annuity-factor RMSE.
- Frozen rates lead the cumulative 2020–2022 COVID stress test and the 20-year-history scenario.
- The five-model French case study produces a **€6.26M provision spread**; among the four full-grid models, the spread is €1.22M.

These are results for this data design and loss functions—not universal model rankings.

## Key Questions Answered

- Which model works best with **short historical data** (20 vs 50 years)?
Expand All @@ -55,7 +66,10 @@ python scripts/download_hmd.py
# 3. Run benchmark (quick mode: 1 country, reduced origins)
python scripts/run_benchmark.py --countries FRATNP --quick

# 4. Launch dashboard
# 4. Reproduce the French 2033 case study
python scripts/run_case_study.py

# 5. Launch dashboard
streamlit run dashboard/app.py
```

Expand All @@ -77,13 +91,13 @@ mortality-benchmark/
├── src/mortality/
│ ├── data/ # HMD loader and preprocessing
│ ├── models/
│ │ ├── classical/ # Lee-Carter, Lee-Miller, BMS, Poisson LC, CBD, H-U, baselines
│ │ ├── classical/ # Lee-Carter, Lee-Miller, Poisson LC, CBD, H-U, baselines
│ │ ├── neural/ # LSTM, GRU, BiLSTM, Transformer, FFNN, CNN
│ │ └── hybrid/ # LC-ResNet (our contribution)
│ ├── evaluation/ # Rolling-origin, metrics, Diebold-Mariano, scenarios, decision framework
│ ├── evaluation/ # Rolling-origin, metrics, scenarios, decision framework
│ ├── actuarial/ # Life tables, e₀, annuities ä₆₅, Solvency II shock
│ └── viz/ # Heatmaps, fan charts, result tables
├── scripts/ # run_benchmark.py, download_hmd.py
├── scripts/ # benchmark, case-study, and HMD download entry points
├── dashboard/ # Streamlit interactive app
├── report/ # French report + CV kit
├── config/ # YAML configs (data, models, evaluation)
Expand All @@ -93,22 +107,31 @@ mortality-benchmark/

## The LC-ResNet Hybrid Model

Our original contribution combines:
The proposed hybrid combines:

1. A **Poisson Lee-Carter skeleton** (aₓ, bₓ, κ_t) — interpretable, stable at long horizons
2. A **small residual neural network** that learns the structured residuals (non-linearities, cohort effects)
1. A **Poisson Lee-Carter skeleton** (aₓ, bₓ, κ_t) — transparent and stable at long horizons
2. A **small residual neural network** that learns nonlinear age-time residual structure
3. **Horizon-dependent shrinkage**: the neural correction is multiplied by exp(−λh), so at long horizons the model reverts to stable LC extrapolation

This preserves interpretability while capturing patterns that LC misses at short/medium horizons.
The LC skeleton remains reportable, but the neural correction is opaque; LC-ResNet is therefore only partially interpretable.

## Manuscript and reproducibility

- Manuscript: [`paper/main.pdf`](paper/main.pdf)
- Stored benchmark rows: [`results/benchmark.csv`](results/benchmark.csv)
- Case-study output: [`results/case_study.csv`](results/case_study.csv)

Each horizon-`h` result is an RMSE over the complete forecast path from year 1 through year `h`, averaged without weighting over valid country–origin pairs. Age-band scenarios refit each model within the band. HMD data are not included because their licence does not permit redistribution.

## References

1. Lee & Carter (1992). *Modeling and forecasting U.S. mortality.* JASA.
2. Brouhns, Denuit & Vermunt (2002). *A Poisson log-bilinear regression approach.* IME.
3. Cairns, Blake & Dowd (2006). *A two-factor model for stochastic mortality.* NAAJ.
3. Cairns, Blake & Dowd (2006). *A two-factor model for stochastic mortality.* Journal of Risk and Insurance.
4. Hyndman & Ullah (2007). *Robust forecasting of mortality and fertility rates.* CSDA.
5. Richman & Wüthrich (2019). *A neural network extension of the Lee-Carter model.* AAS.
6. Perla et al. (2021). *A brief review of deep learning methods in mortality forecasting.* AAS.
5. Richman & Wüthrich (2021). *A neural network extension of the Lee-Carter model.* Annals of Actuarial Science.
6. Barigou et al. (2023). *Bayesian model averaging for mortality forecasting using leave-future-out validation.* IJF.
7. Li, Li & Panagiotelis (2025). *Boosting domain-specific models with shrinkage.* IJF.

## License

Expand Down
2 changes: 1 addition & 1 deletion config/data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ages:
start: 0
end: 100

sexes: [Male, Female]
sexes: [Total]

paths:
raw: data/raw
Expand Down
6 changes: 1 addition & 5 deletions config/evaluation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ metrics:
- rmse_e0
- rmse_annuity_65

confidence:
level: 0.95
n_simulations: 1000

scenarios:
short_history:
train_lengths: [20, 30, 50]
Expand All @@ -26,4 +22,4 @@ scenarios:
working: [20, 64]
elderly: [65, 100]

seeds: [42, 123, 456, 789, 1024]
seed: 42
8 changes: 1 addition & 7 deletions config/models.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ classical:
kt_model: rwd
lee_miller:
adjust_e0: true
bms:
linearity_test_alpha: 0.05
poisson_lc:
max_iter: 500
tol: 1.0e-6
Expand All @@ -21,14 +19,10 @@ classical:
neural:
common:
seed: 42
n_ensemble: 5
epochs: 200
patience: 20
batch_size: 32
lr: 0.001
weight_decay: 1.0e-4
mc_dropout_rate: 0.1
mc_dropout_samples: 100

lstm_kt:
hidden_size: 64
Expand Down Expand Up @@ -58,4 +52,4 @@ hybrid:
lc_resnet:
residual_hidden: [64, 32]
shrinkage_lambda: 0.1
multi_population: true
multi_population: false
43 changes: 29 additions & 14 deletions dashboard/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@
fig2, ax2 = plt.subplots(figsize=(12, 4))
ax2.plot(data["years"], e0_series, linewidth=2)
ax2.set_xlabel("Year")
ax2.set_ylabel("e0")
ax2.set_title(f"Life expectancy at birth — {COUNTRIES[country_code]}")
ax2.set_ylabel("e0 (truncated at age 100)")
ax2.set_title(
f"Age-100-truncated life expectancy — {COUNTRIES[country_code]}"
)
ax2.grid(True, alpha=0.3)
st.pyplot(fig2)
plt.close()
Expand Down Expand Up @@ -133,7 +135,7 @@

try:
data = load_country("FRATNP")
models_to_compare = ["lee_carter", "poisson_lc", "random_walk", "lc_resnet"]
models_to_compare = ["lee_carter", "poisson_lc", "random_walk", "lc_resnet", "cbd"]
mx_by_model = {}

for name in models_to_compare:
Expand All @@ -142,7 +144,15 @@
model.fit(data["log_mx"], data["ages"], data["years"],
data.get("exposures"), data.get("deaths"))
fc = model.forecast(10)
mx_by_model[name] = np.exp(fc[:, -1])
if name == "cbd":
# CBD forecasts ages 60--100. Younger rates are immaterial to an
# annuity starting at 65, but a full 0--100 vector is required.
mx = data["mx"][:, -1].copy()
positions = np.searchsorted(data["ages"], model.forecast_ages)
mx[positions] = np.exp(fc[:, -1])
mx_by_model[name] = mx
else:
mx_by_model[name] = np.exp(fc[:, -1])

if mx_by_model:
pricing = price_annuity_portfolio(mx_by_model)
Expand All @@ -161,25 +171,30 @@

with tab4:
st.subheader("Model Recommendation Engine")
col1, col2, col3 = st.columns(3)
col1, col2, col3, col4 = st.columns(4)
with col1:
hist_len = st.slider("History length (years)", 15, 70, 50)
with col2:
fc_horizon = st.slider("Forecast horizon (years)", 1, 30, 10)
with col3:
age_focus = st.selectbox("Age focus", ["all", "young", "working", "elderly"])
with col4:
objective = st.selectbox(
"Primary objective", ["log_rates", "life_expectancy", "annuity"]
)

interp = st.checkbox("Needs regulatory interpretability?")
interp = st.checkbox("Restrict to models with a transparent statistical skeleton?")

recommended = recommend_model(hist_len, fc_horizon, age_focus, interp)
recommended = recommend_model(
hist_len, fc_horizon, age_focus, interp, objective=objective
)
st.success(f"**Recommended model: {recommended}**")

st.markdown("""
**Decision logic:**
- Short history (<25 years) → Random walk (neural networks need more data)
- Elderly ages → CBD (designed for 60+, used by regulators)
- Short horizon (≤5 years) → LC-ResNet (neural correction helps most)
- Medium horizon (≤10 years) → Poisson Lee-Carter
- Long horizon (>10 years) → Lee-Carter (stable extrapolation)
- If interpretability required → LC-ResNet at short horizons, LC otherwise
**Decision logic:** recommendations reproduce the empirical winners in this
benchmark and depend on the selected loss. The random walk leads full-grid
log-rate accuracy; H-U leads 20-year life-expectancy accuracy; the FFNN leads
20-year annuity-factor accuracy; and LC-ResNet leads structured models at one
year. A transparent-skeleton restriction excludes simple baselines and fully
neural models. These are benchmark-specific recommendations, not universal rules.
""")
Loading
Loading