Migration: Adding Scenarios to an Existing Model¶
To add scenario support to an existing model:
Step 1: Identify scenario-varying assumptions¶
| Assumption | Varies by scenario? |
|---|---|
| Discount rates | Yes |
| Fund returns | Yes |
| Mortality rates | Usually no |
| Lapse rates | Usually no |
| Expenses | Sometimes (inflation scenarios) |
Step 2: Update scenario-varying tables¶
Add scenario_id dimension to tables that vary:
# BEFORE: Single scenario
disc_rate_table = gs.Table(
source="discount_rates.parquet",
dimensions={"year": "year"},
value="disc_rate_ann"
)
# AFTER: Multiple scenarios
disc_rate_table = gs.Table(
source="discount_rates.parquet",
dimensions={
"scenario_id": "scenario_id", # <-- Added
"year": "year"
},
value="disc_rate_ann"
)
# OR use from_scenario_files if you have separate files
disc_rate_table = gs.Table.from_scenario_files(
scenario_files={
"BASE": "rates_base.parquet",
"UP": "rates_up.parquet",
},
scenario_column="scenario_id",
dimensions={"year": "year"},
value="disc_rate_ann"
)
Step 3: Update lookups¶
Change hardcoded scenario values to use af.scenario_id:
# BEFORE (hardcoded scenario)
af.disc_rate = disc_rate_table.lookup(
scenario=pl.lit("BASE"), # <-- Hardcoded
year=af.year
)
# AFTER (dynamic scenario)
af.disc_rate = disc_rate_table.lookup(
scenario_id=af.scenario_id, # <-- From expanded frame
year=af.year
)
Non-scenario-varying lookups stay unchanged:
# This doesn't change - mortality doesn't vary by scenario
af.mort_rate = mort_table.lookup(age=af.age, duration=af.duration)
Step 4: Add scenario expansion¶
Wrap your model execution:
# BEFORE
af = ActuarialFrame(pl.read_parquet("model_points.parquet"))
result = run_projection(af)
# AFTER
af = ActuarialFrame(pl.read_parquet("model_points.parquet"))
af = with_scenarios(af, ["BASE", "UP", "DOWN"]) # <-- Added
result = run_projection(af) # Same model code
Step 5: Update aggregation¶
Aggregate by scenario:
# BEFORE (single scenario)
df = result.collect()
total_pv = df["pv_net_cf"].sum()
# AFTER (multiple scenarios)
df = result.collect()
by_scenario = df.group_by("scenario_id").agg([
pl.col("pv_net_cf").sum().alias("total_pv")
])
# Calculate risk metrics
reserves = by_scenario.sort("total_pv", descending=True)["total_pv"]
cte_98 = reserves.head(int(0.02 * len(reserves))).mean()