Assumption Table Examples in Gaspatchio¶
Working with Mortality Tables¶
This guide walks through using the 2015 VBT Female Smoker Mortality Table (ANB) as an example to demonstrate how to set up and use assumption tables in Gaspatchio.
Understanding the Table Structure¶
The 2015 VBT table is structured as follows: - Rows represent issue ages (18-95) - Columns represent policy durations (1-25 plus "Ultimate") - Values represent mortality rates per 1,000
Here's a small sample from the table:
Issue Age | Duration 1 | Duration 2 | Duration 3 | Duration 4 | Duration 5 | Ultimate | Attained Age |
---|---|---|---|---|---|---|---|
30 | 0.20 | 0.25 | 0.31 | 0.38 | 0.45 | 4.84 | 55 |
31 | 0.21 | 0.26 | 0.34 | 0.42 | 0.51 | 5.35 | 56 |
32 | 0.22 | 0.28 | 0.37 | 0.47 | 0.58 | 5.93 | 57 |
33 | 0.23 | 0.31 | 0.42 | 0.53 | 0.65 | 6.59 | 58 |
34 | 0.25 | 0.35 | 0.48 | 0.61 | 0.73 | 7.31 | 59 |
Loading the Assumption Table¶
Loading assumption tables is straightforward with the dimension-based API. Gaspatchio provides tools to analyze table structure and configure dimensions:
import gaspatchio_core as gs
# First, analyze the table structure (optional but helpful)
df = pl.read_csv("2015-VBT-FSM-ANB.csv")
schema = gs.analyze_table(df)
print(schema.suggest_table_config())
# Load the mortality table with dimension configuration
vbt_table = gs.Table(
name="vbt_2015_female_smoker",
source="2015-VBT-FSM-ANB.csv",
dimensions={
"Issue Age": "Issue Age", # Simple data dimension
"duration": gs.MeltDimension(
columns=[str(i) for i in range(1, 26)] + ["Ultimate"],
name="duration",
overflow=gs.ExtendOverflow("Ultimate", to_value=200)
)
},
value="mortality_rate"
)
The API explicitly configures: - Data dimensions (like Issue Age) that map directly from columns - Melt dimensions that transform wide columns (1-25, Ultimate) into long format - Overflow strategies that expand "Ultimate" values to higher durations - The value column name for the melted rates
After loading, the internal data looks like this:
Issue Age | duration | mortality_rate |
---|---|---|
30 | 1 | 0.20 |
30 | 2 | 0.25 |
30 | 3 | 0.31 |
30 | 4 | 0.38 |
30 | 5 | 0.45 |
30 | 26 | 4.84 |
30 | 27 | 4.84 |
30 | 150 | 4.84 |
... | ... | ... |
Using the Assumption Table in ActuarialFrame¶
Now we can use this table for lightning-fast lookups:
# Create a simple policy dataset
policy_data = pl.DataFrame({
"policy_id": ["A001", "A002", "A003", "A004"],
"issue_age": [30, 35, 40, 45],
"duration": [1, 3, 5, 10]
})
# Convert to ActuarialFrame
af = gs.ActuarialFrame(policy_data)
# Look up mortality rates using the table's lookup method
af = af.with_columns(
vbt_table.lookup({
"Issue Age": af["issue_age"],
"duration": af["duration"]
}).alias("mortality_rate")
)
print(af)
Result:
shape: (4, 4)
┌──────────┬───────────┬──────────┬───────────────┐
│ policy_id ┆ issue_age ┆ duration ┆ mortality_rate │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 ┆ f64 │
╞══════════╪═══════════╪══════════╪═══════════════╡
│ A001 ┆ 30 ┆ 1 ┆ 0.20 │
│ A002 ┆ 35 ┆ 3 ┆ 0.54 │
│ A003 ┆ 40 ┆ 5 ┆ 1.15 │
│ A004 ┆ 45 ┆ 10 ┆ 4.10 │
└──────────┴───────────┴──────────┴───────────────┘
Working with Overflow Durations¶
The beauty of the API is that overflow handling is completely transparent. Even extreme durations work instantly:
# Test with durations beyond the table (> 25)
extreme_data = pl.DataFrame({
"policy_id": ["X001", "X002"],
"issue_age": [30, 40],
"duration": [50, 100] # Way beyond table max of 25!
})
af_extreme = gs.ActuarialFrame(extreme_data)
af_extreme = af_extreme.with_columns(
vbt_table.lookup({
"Issue Age": af_extreme["issue_age"],
"duration": af_extreme["duration"]
}).alias("mortality_rate")
)
print(af_extreme)
Result:
shape: (2, 4)
┌──────────┬───────────┬──────────┬────────────────┐
│ policy_id ┆ issue_age ┆ duration ┆ mortality_rate │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 ┆ f64 │
╞══════════╪═══════════╪══════════╪════════════════╡
│ X001 ┆ 30 ┆ 50 ┆ 4.84 │
│ X002 ┆ 40 ┆ 100 ┆ 9.32 │
└──────────┴───────────┴──────────┴────────────────┘
Both policies get the "Ultimate" rate because the ExtendOverflow
strategy pre-expanded the overflow during loading.
Projecting Multiple Periods¶
Gaspatchio's vector-based approach works seamlessly with the API:
# Create a policy with projection over multiple durations
policy_projection = pl.DataFrame({
"policy_id": ["B001"],
"issue_age": [30],
"duration": [[1, 2, 3, 4, 5, 25, 26, 50, 100]] # Mix of regular and overflow
})
af_proj = gs.ActuarialFrame(policy_projection)
# Look up mortality rates for all durations at once
af_proj = af_proj.with_columns(
vbt_table.lookup({
"Issue Age": af_proj["issue_age"],
"duration": af_proj["duration"]
}).alias("mortality_rate")
)
# Explode for visualization
result = af_proj.explode(["duration", "mortality_rate"])
print(result)
Result:
shape: (9, 4)
┌──────────┬───────────┬──────────┬───────────────┐
│ policy_id ┆ issue_age ┆ duration ┆ mortality_rate │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 ┆ f64 │
╞══════════╪═══════════╪══════════╪═══════════════╡
│ B001 ┆ 30 ┆ 1 ┆ 0.20 │
│ B001 ┆ 30 ┆ 2 ┆ 0.25 │
│ B001 ┆ 30 ┆ 3 ┆ 0.31 │
│ B001 ┆ 30 ┆ 4 ┆ 0.38 │
│ B001 ┆ 30 ┆ 5 ┆ 0.45 │
│ B001 ┆ 30 ┆ 25 ┆ 4.12 │
│ B001 ┆ 30 ┆ 26 ┆ 4.84 │
│ B001 ┆ 30 ┆ 50 ┆ 4.84 │
│ B001 ┆ 30 ┆ 100 ┆ 4.84 │
└──────────┴───────────┴──────────┴───────────────┘
Loading Simple Curves¶
For 1-dimensional tables (like lapse rates by age), the API is even simpler:
# Load a simple age → lapse rate curve
lapse_table = gs.Table(
name="lapse_2025",
source="lapse_curve.csv",
dimensions={
"age": "age" # Simple string shorthand
},
value="lapse_rate"
)
# Use it immediately
af = af.with_columns(
lapse_table.lookup({"age": af["age"]}).alias("lapse_rate")
)
Advanced Features¶
For more complex scenarios, you have full control with the dimension-based API:
# Multi-dimensional table with selective column loading
mortality_table = gs.Table(
name="mortality_by_gender",
source="mortality_m_f.csv",
dimensions={
"age": "age",
"gender": gs.MeltDimension(
columns=["Male", "Female"],
name="gender"
)
},
value="mortality_rate"
)
# Table with custom overflow limits
salary_table = gs.Table(
name="salary_scale",
source="salary_by_service.csv",
dimensions={
"grade": "grade",
"service": gs.MeltDimension(
columns=[str(i) for i in range(1, 21)] + ["20+"],
name="service",
overflow=gs.ExtendOverflow("20+", to_value=50)
)
},
value="scale_factor"
)
# Using computed dimensions
complex_table = gs.Table(
name="complex_assumptions",
source=df,
dimensions={
"issue_age": "issue_age",
"policy_year": "policy_year",
"attained_age": gs.ComputedDimension(
pl.col("issue_age") + pl.col("policy_year") - 1,
"attained_age"
)
},
value="assumption_value"
)
Using the TableBuilder Pattern¶
For step-by-step table construction, use the fluent TableBuilder
API:
# Build a complex mortality table
mortality_table = (
gs.TableBuilder("mortality_select_ultimate")
.from_source("mortality_su.csv")
.with_data_dimension("issue_age", "IssueAge")
.with_data_dimension("gender", "Gender")
.with_melt_dimension(
"duration",
columns=[f"Dur{i}" for i in range(1, 16)] + ["Ultimate"],
overflow=gs.ExtendOverflow("Ultimate", to_value=100),
fill=gs.LinearInterpolate() # Interpolate any gaps
)
.with_value_column("qx_rate")
.build()
)
# The table is ready for lookups
af = af.with_columns(
mortality_table.lookup({
"issue_age": af["age"],
"gender": af["sex"],
"duration": af["policy_duration"]
}).alias("mortality_rate")
)
Metadata and Table Discovery¶
Tables can include metadata for documentation and discovery:
# Create table with rich metadata
vbt_table = gs.Table(
name="vbt_2015_complete",
source="vbt_2015_all.csv",
dimensions={
"age": "Age",
"gender": "Gender",
"smoking": "Smoker",
"duration": gs.MeltDimension(
columns=duration_columns,
name="duration",
overflow=gs.ExtendOverflow("Ultimate", to_value=120)
)
},
value="mortality_rate",
metadata={
"source": "2015 Valuation Basic Table",
"basis": "ANB",
"version": "2015",
"effective_date": "2015-01-01",
"description": "Industry standard mortality table",
"tags": ["mortality", "vbt", "2015", "standard"]
}
)
# Discover tables
all_tables = gs.list_tables()
print(f"Available tables: {all_tables}")
# Get metadata for a specific table
metadata = gs.get_table_metadata("vbt_2015_complete")
print(f"Table metadata: {metadata}")
# List all tables with metadata
tables_info = gs.list_tables_with_metadata()
for name, meta in tables_info.items():
print(f"{name}: {meta.get('description', 'No description')}")