Skip to content

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()