Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ae790eb
update notebook
jkoestner Apr 21, 2026
22f26fb
Merge branch 'main' into dev
jkoestner Apr 21, 2026
834c4bd
adding error toast
jkoestner Apr 22, 2026
8952dfb
add export_csv functionality
jkoestner Apr 24, 2026
07ee2c7
add total line option in chart
jkoestner Apr 25, 2026
5acaeae
add cdc weekly chart and create a common_build for chart/table
jkoestner Apr 27, 2026
eebbb01
dashboard enhancements and design structures
jkoestner Apr 29, 2026
d722ab5
update dashboard logic
jkoestner Apr 29, 2026
ae2a0f5
update cdc page to be more informative
jkoestner Apr 30, 2026
141a260
fix the slider input boxes in dashboard
jkoestner May 4, 2026
1912634
enhancing cdc page, update credibility to return series
jkoestner May 11, 2026
08d7d54
add variance and summarize data
jkoestner May 13, 2026
98f5bad
small updates
jkoestner May 13, 2026
4194082
adding claude skills and cdc pages
jkoestner May 23, 2026
30d531f
adding claude model review and mortality table skills, add amount to …
jkoestner May 31, 2026
6322d32
small change
jkoestner May 31, 2026
bc17368
add small but meaningful updates to neural (on early stopping the bes…
jkoestner May 31, 2026
db00a76
small change
jkoestner Jun 1, 2026
d3ffb1e
small change
jkoestner Jun 1, 2026
52fbfae
small change
jkoestner Jun 1, 2026
febfcb8
add deterministic and device parameter
jkoestner Jun 3, 2026
9c8d6f1
add neural methods
jkoestner Jun 6, 2026
bb4059b
update notebooks
jkoestner Jun 7, 2026
9a29862
add target_loss
jkoestner Jun 7, 2026
08ede97
add cdc new data check
jkoestner Jun 7, 2026
dc487ab
changelog update
jkoestner Jun 7, 2026
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
138 changes: 138 additions & 0 deletions .claude/skills/_shared/actuarial-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Actuarial Reference for Mortality Model Work

Shared reference for the `model-review`, `nn-review`, and `mortality-table-build` skills.

## Key Terminology

### A/E Ratio (Actual to Expected)
- **A/E = 1.00**: Model perfectly predicts mortality
- **A/E < 1.00**: Model over-predicts (actual deaths less than expected)
- **A/E > 1.00**: Model under-predicts (actual deaths more than expected)

### Select vs Ultimate Mortality
- **Select Period**: First 15-25 policy years after underwriting
- **Select Mortality**: Lower mortality due to recent underwriting (healthy lives)
- **Ultimate Mortality**: Stable mortality after select period wears off
- **Selection Wear-off**: Gradual increase in mortality as select advantage fades

### VBT 2015
- **VBT**: Valuation Basic Table (SOA industry mortality table)
- **qx_vbt15**: Mortality rate from VBT 2015
- Used as benchmark for model comparisons

## Data Dimensions

### Insurance Plans
| Plan | Description | Mortality Characteristics |
|------|-------------|--------------------------|
| Term | Temporary coverage | Generally better mortality (more underwriting) |
| Perm | Permanent whole life | Stable, traditional product |
| UL | Universal Life | Flexible premium, moderate mortality |
| ULSG | UL with Secondary Guarantee | Often older ages, different risk profile |
| VL | Variable Life | Investment component, varied demographics |
| VLSG | VL with Secondary Guarantee | Similar to ULSG |

### Preferred Classes
- **Number of Preferred Classes**: 1-4 tiers of underwriting
- **Preferred Class**: Specific tier (1=best, 4=standard)
- **class_enh**: Combined field (e.g., "2_1" = 2 classes, tier 1)

### Face Amount Bands
Higher face amounts typically indicate more rigorous underwriting, higher
socioeconomic status, and better mortality experience.

## Expected Patterns

### By Duration
| Duration | Expected A/E Pattern | Reason |
|----------|---------------------|--------|
| 1-5 | Lower (0.60-0.80) | Strong select effect |
| 6-10 | Rising (0.80-0.95) | Select wearing off |
| 11-15 | Near 1.00 | Approaching ultimate |
| 16+ | Stable (~1.00) | Ultimate mortality |

### By Age
| Age Band | Considerations |
|----------|---------------|
| 30-50 | Lower deaths, higher variance |
| 51-70 | Core mortality experience |
| 71-80 | Increasing mortality rates |
| 81+ | High mortality, credibility concerns |

### By Smoker Status
| Status | Expected Pattern |
|--------|-----------------|
| NS (Non-Smoker) | Better mortality, majority of exposure |
| S (Smoker) | Higher mortality, smaller population |

## Model Quality Benchmarks

### A/E Tolerance by Segment Size
| Exposure Level | Acceptable A/E Range |
|----------------|---------------------|
| Very High (>$100B) | 0.95 - 1.05 |
| High ($10B-$100B) | 0.90 - 1.10 |
| Medium ($1B-$10B) | 0.85 - 1.15 |
| Low (<$1B) | 0.75 - 1.25 |

### Overfitting Indicators
- Train A/E significantly better than Test A/E (>0.05 difference)
- Very low training loss but high validation loss
- Model performs well on seen data but poorly on holdout

## Standard Review Dimensions

When grouping A/E for review, use these dimensions (binning shown where relevant):

- `sex` (M/F)
- `smoker_status` (NS/S)
- `attained_age` (binned: 30-40, 41-50, 51-60, 61-70, 71-80, 81+)
- `duration` (binned: 1-5, 6-10, 11-15, 16-20, 21+)
- `observation_year`
- `insurance_plan` (Term, Perm, UL, ULSG, VL, VLSG)
- `face_amount_band` or `binned_face`
- `class_enh` (preferred class combination)

## Feature Engineering Best Practices

### Splines for Age
- Use B-splines with 6-10 knots for `attained_age`
- Quantile-based knots capture data distribution
- Degree 3 (cubic) provides smooth curves

### Encoding by Model Family
| Feature | GLM | GAM | CatBoost | Neural |
|---------|-----|-----|----------|--------|
| sex | OHE | OHE | native cat | OHE/ordinal |
| smoker_status | OHE | OHE | native cat | OHE/ordinal |
| insurance_plan | OHE | OHE | native cat | Embedding |
| face_amount_band | ordinal | spline | native cat | Embedding |
| preferred_class | OHE | OHE | native cat | Embedding |
| attained_age | binned/poly | spline | numeric | spline or numeric |

### Interaction Terms to Consider
- `duration × preferred_class` (select effect varies by underwriting)
- `attained_age × smoker_status` (age-mortality slope differs)
- `duration × insurance_plan` (select patterns vary by product)

## Model-Family Notes

### CatBoost
- Handles categoricals natively; no OHE needed
- Use built-in feature importance (`PredictionValuesChange`, `LossFunctionChange`)
- SHAP via TreeExplainer (fast, exact)

### GLM (`morai.models.core.GLM`)
- Coefficients are log-odds (logit link) or log-rates (log link) — interpret carefully
- Use `calc_likelihood_ratio` to compare nested models
- Watch for unstable coefficients (large SE) on sparse categories

### GAM (`morai.models.r.GAMR`)
- Inspect smooth term plots — non-monotonic curves at the tails often indicate
over-flexible splines
- Effective degrees of freedom (EDF) per term signals complexity

### Neural (`morai.models.neural.Neural`)
- Embeddings for high-cardinality categoricals
- SHAP via KernelExplainer (slow, sample 100 background / 100 explain)
- Inspect training/validation loss curves for overfitting
150 changes: 150 additions & 0 deletions .claude/skills/model-review/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
name: model-review
description: Review a fitted mortality model (GLM, GAM, CatBoost, or Neural) focused on actual-vs-expected (A/E) analysis. Use when evaluating model fit, segment-level A/E, rate comparisons vs VBT, partial dependence, and overfitting checks. Model-family-agnostic; branches into family-specific diagnostics in the addendum section.
allowed-tools: Read, Bash, Write, Edit, Glob, Grep
---

# Mortality Model Review (A/E Focused)

Generalized review framework for any fitted mortality model exposing a
scikit-learn-style `.predict(X)` interface. Works for GLM, GAM, CatBoost, and
Neural models in this codebase.

**This skill reviews pre-trained models. It does not train them.**

For terminology, tolerance bands, expected A/E patterns, and standard review
dimensions, read `../_shared/actuarial-reference.md` before beginning.

## Inputs

- A fitted model (typically loaded via `joblib.load("files/models/<name>.joblib")`)
- Train and test datasets with at minimum: `death_claim_amount`, `amount_exposed`,
`qx_raw`, `qx_vbt15`, and the standard review dimensions
- The `mapping` and preprocessing artifacts used to encode features for the model

## Workflow

### 1. Sanity Checks
- Confirm model type (`type(model).__name__`) and identify the family (GLM, GAM,
CatBoost, Neural)
- Confirm predictions run end-to-end on a small slice
- Compute `expected_claims = model.predict(X) * amount_exposed`

### 2. Overall A/E
- Train A/E and Test A/E
- Flag overfitting if `|Train A/E - Test A/E| > 0.05` OR if Train A/E is in band
but Test A/E is out of band

### 3. A/E by Dimension
Group by each dimension in the shared reference's "Standard Review Dimensions"
list. Report on both train and test:

```
| Dimension Value | Actual | Expected | Exposure | A/E |
```

Apply tolerance bands from the shared reference (sized by exposure level).
Flag any segment outside band.

### 4. Rate Comparison Charts
Use `morai.experience.charters.compare_rates` to plot weighted-average
(`amount_exposed`-weighted) rates for `qx_raw`, `qx_vbt15`, and `qx_model`
across age, duration, and other key dimensions.

### 5. Partial Dependence (model-agnostic)
Use `morai.experience.charters.pdp` — it calls `model.predict` so it works for
all families.

Priority features:
1. `duration` — line color by `insurance_plan`
2. `attained_age` — line color by `class_enh` or `binned_face`
3. `insurance_plan` — overall effect

Settings: `weight="amount_exposed"`, `secondary="death_count"`, `center="per_x"`.

### 6. Family-Specific Addendum

Run only the section matching the model's family.

#### GLM (`morai.models.core.GLM`)
- Coefficient table (estimate, SE, p-value) — flag |p| > 0.05 on retained terms
- Likelihood ratio vs a reduced model via `calc_likelihood_ratio`
- Sparse-category check: coefficients with very large SE

#### GAM (`GAMPy` / `GAMStats`)
- Smooth term plots — call out non-monotonic tails
- EDF per term
- Compare in-sample deviance to a comparable GLM if available

#### CatBoost
- Built-in importance: `PredictionValuesChange` and `LossFunctionChange`
- SHAP via TreeExplainer (fast, exact) — bar + summary
- Categorical handling: confirm `cat_features` were declared at fit time

#### Neural (`morai.models.neural.Neural`)
- Defer to the `nn-review` skill for SHAP (KernelExplainer), embedding cosine /
PCA, and loss-curve inspection

## Output Template

```markdown
## Model Review — <model name> (<family>)

### Overall
- Train A/E: X.XX
- Test A/E: X.XX
- Overfitting risk: Low / Medium / High

### A/E by Dimension
<tables, with out-of-band rows flagged>

### Rate Comparison Highlights
<bullets on where qx_model diverges from qx_raw and qx_vbt15>

### Family-Specific Findings
<from the addendum section run>

### Key Findings
1. ...
2. ...

### Recommendations
1. ...
2. ...
```

## Common Code Patterns

### A/E Calculation
```python
ae = df.groupby(dim).agg(
actual=("death_claim_amount", "sum"),
expected=("expected_claims", "sum"),
exposure=("amount_exposed", "sum"),
)
ae["A/E"] = ae["actual"] / ae["expected"]
```

### Model-Agnostic PDP
```python
from morai.experience import charters
charters.pdp(
model=model,
df=md_encoded,
x_axis="duration",
line_color="insurance_plan",
weight="amount_exposed",
secondary="death_count",
mapping=mapping,
)
```

### Rate Comparison
```python
charters.compare_rates(
df=md,
rates=["qx_raw", "qx_vbt15", "qx_model"],
x_axis="attained_age",
weight="amount_exposed",
)
```
Loading
Loading