Shock Operations¶
Gaspatchio provides a composable shock system for actuarial stress testing. Think of shocks as lego bricks - simple operations you can combine to build complex regulatory scenarios.
This page teaches you:
- The building blocks - Individual shock operations (multiply, add, clip, etc.)
- How to filter - Apply shocks to specific segments or time periods
- How to compose - Chain operations together for complex transformations
- How to build regulatory scenarios - Solvency II SCR as a worked example
The Building Blocks¶
Every shock transforms assumption values. The basic operations are:
| Operation | What it does | Example use case |
|---|---|---|
multiply |
Scale by a factor | "Increase mortality by 20%" |
add |
Add a constant | "Shift rates up 50bps" |
set |
Replace with a value | "Assume zero lapses" |
clip |
Cap/floor values | "Lapse cannot exceed 100%" |
Multiplicative Shocks¶
Scale values by a factor. Use for percentage changes.
from gaspatchio_core.scenarios import MultiplicativeShock
# Increase mortality by 20%
shock = MultiplicativeShock(factor=1.2, table="mortality")
# Decrease lapse rates by 10%
shock = MultiplicativeShock(factor=0.9, table="lapse")
Config syntax:
{"table": "mortality", "multiply": 1.2}
Additive Shocks¶
Add a constant delta. Use for basis point shifts and absolute changes.
from gaspatchio_core.scenarios import AdditiveShock
# Add 50bps to discount rates
shock = AdditiveShock(delta=0.005, table="discount_rates")
# Subtract 1% from expense loading
shock = AdditiveShock(delta=-0.01, table="expenses")
Config syntax:
{"table": "discount_rates", "add": 0.005}
Override Shocks¶
Replace all values with a constant. Use for extreme scenarios and boundary testing.
from gaspatchio_core.scenarios import OverrideShock
# Set lapse rates to zero
shock = OverrideShock(value=0.0, table="lapse")
# Override discount rate to flat 5%
shock = OverrideShock(value=0.05, table="discount_rates")
Config syntax:
{"table": "lapse", "set": 0.0}
Clip Shocks¶
Cap and/or floor values to a range. Essential for keeping shocked values actuarially valid.
from gaspatchio_core.scenarios import ClipShock
# Cap lapse rates at 100%
shock = ClipShock(max_value=1.0, table="lapse")
# Floor mortality at 0.1%
shock = ClipShock(min_value=0.001, table="mortality")
# Clip to a valid range
shock = ClipShock(min_value=0.0, max_value=1.0, table="rates")
Config syntax:
{"table": "lapse", "clip": {"max": 1.0}}
{"table": "mortality", "clip": {"min": 0.001}}
{"table": "rates", "clip": {"min": 0.0, "max": 1.0}}
Syntactic sugar - use [min, max] arrays with null for unbounded:
{"table": "lapse", "clip": [null, 1.0]}
{"table": "rates", "clip": [0.0, 1.0]}
Filtering Shocks¶
Sometimes you need shocks that only apply to specific segments or time periods.
WHERE Clause: Dimension Filters¶
Apply shocks only to rows matching conditions on table dimensions.
from gaspatchio_core.scenarios import FilteredShock, MultiplicativeShock
# Increase early-duration lapse by 25%
shock = FilteredShock(
shock=MultiplicativeShock(factor=1.25),
where={"duration": {"lte": 3}},
table="lapse",
)
# Mortality shock for elderly lives
shock = FilteredShock(
shock=MultiplicativeShock(factor=1.15),
where={"attained_age": {"gte": 65}},
table="mortality",
)
# Complex filter with multiple conditions (AND logic)
shock = FilteredShock(
shock=AdditiveShock(delta=0.02),
where={"sex": "F", "smoker_status": "S"},
table="mortality",
)
Config syntax:
{"table": "lapse", "multiply": 1.25, "where": {"duration": {"lte": 3}}}
{"table": "mortality", "multiply": 1.15, "where": {"attained_age": {"gte": 65}}}
Supported filter operators:
| Operator | Meaning | Example |
|---|---|---|
eq |
Equals | {"sex": {"eq": "F"}} or just {"sex": "F"} |
ne |
Not equals | {"product": {"ne": "TERM"}} |
gt |
Greater than | {"age": {"gt": 60}} |
gte |
Greater than or equal | {"duration": {"gte": 5}} |
lt |
Less than | {"age": {"lt": 30}} |
lte |
Less than or equal | {"duration": {"lte": 3}} |
between |
Range (inclusive) | {"age": {"between": [30, 50]}} |
in |
In list | {"product": {"in": ["TERM", "WL"]}} |
not_in |
Not in list | {"status": {"not_in": ["LAPSED", "DEATH"]}} |
WHEN Clause: Time Conditions¶
Apply shocks only at specific projection times.
from gaspatchio_core.scenarios import TimeConditionalShock, AdditiveShock
# Mass lapse at t=0 (40% immediate surrender)
shock = TimeConditionalShock(
shock=AdditiveShock(delta=0.40),
when={"t": {"eq": 0}},
table="lapse",
)
# Expense shock for first 5 years only
shock = TimeConditionalShock(
shock=MultiplicativeShock(factor=1.10),
when={"t": {"lte": 5}},
table="expenses",
)
Config syntax:
{"table": "lapse", "add": 0.40, "when": {"t": {"eq": 0}}}
{"table": "expenses", "multiply": 1.10, "when": {"t": {"lte": 5}}}
Combining WHERE and WHEN¶
You can use both filters together:
{
"table": "lapse",
"multiply": 1.5,
"where": {"product": "TERM"},
"when": {"t": {"lte": 5}}
}
This reads: "Increase lapse by 50% for TERM products during the first 5 years."
Composing Operations¶
Complex regulatory scenarios often need multiple operations applied in sequence.
Pipeline Shocks¶
Chain operations together. Each step's output becomes the next step's input.
from gaspatchio_core.scenarios import PipelineShock, MultiplicativeShock, ClipShock
# Solvency II lapse up: multiply by 1.5, then cap at 100%
shock = PipelineShock(
shocks=[
MultiplicativeShock(factor=1.5),
ClipShock(max_value=1.0),
],
table="lapse",
)
Config syntax:
{
"table": "lapse",
"pipeline": [
{"multiply": 1.5},
{"clip": {"max": 1.0}}
]
}
Syntactic sugar - combine operation + clip in one config:
{"table": "lapse", "multiply": 1.5, "clip": [null, 1.0]}
This is equivalent to the pipeline above but more concise.
Max Shocks¶
Take the maximum (less severe) of two transformations. Essential for Solvency II lapse down.
from gaspatchio_core.scenarios import MaxShock, MultiplicativeShock, AdditiveShock
# Solvency II lapse down: max(lapse × 0.5, lapse - 0.2)
# This means: reduce by 50% OR reduce by 20pp, whichever is less severe
shock = MaxShock(
shock_a=MultiplicativeShock(factor=0.5),
shock_b=AdditiveShock(delta=-0.2),
table="lapse",
)
Config syntax:
{"table": "lapse", "max": [{"multiply": 0.5}, {"add": -0.2}]}
Min Shocks¶
Take the minimum (more severe) of two transformations.
from gaspatchio_core.scenarios import MinShock, MultiplicativeShock, OverrideShock
# Cap mortality at 10% even after stress
shock = MinShock(
shock_a=MultiplicativeShock(factor=1.5),
shock_b=OverrideShock(value=0.1),
table="mortality",
)
Config syntax:
{"table": "mortality", "min": [{"multiply": 1.5}, {"set": 0.1}]}
Parameter Shocks¶
Some model inputs are scalar parameters, not assumption tables. Use ParameterShock for these.
from gaspatchio_core.scenarios import ParameterShock
# Add 1% to expense inflation
shock = ParameterShock(param="expense_inflation", operation="add", value=0.01)
# Apply in model code
base_inflation = 0.02
shocked_inflation = shock.apply(base_inflation) # Returns 0.03
Config syntax:
{"param": "expense_inflation", "add": 0.01}
{"param": "discount_spread", "multiply": 1.5}
{"param": "commission_rate", "set": 0.05}
Parameter shocks are different
ParameterShock is NOT a Shock subclass. It doesn't generate Polars expressions -
instead, it provides an apply(base_value) method for your model code to use.
Configuration Syntax¶
The shock system supports LLM-friendly JSON/dict configs that can be generated from natural language.
For the why (auditability, reproducibility, and no regenerated assumption tables) plus richer actuarial examples, see Natural Language → Executable Configs.
Parsing Configs¶
from gaspatchio_core.scenarios import parse_shock_config, parse_scenario_config
# Parse a single shock
config = {"table": "mortality", "multiply": 1.2}
shock = parse_shock_config(config)
# Returns: MultiplicativeShock(factor=1.2, table="mortality")
# Parse a full scenario configuration
config = [
{"id": "BASE"},
{
"id": "STRESS",
"shocks": [
{"table": "mortality", "multiply": 1.2},
{"table": "lapse", "multiply": 0.8},
],
},
]
scenarios = parse_scenario_config(config)
# Returns: {"BASE": [], "STRESS": [MultiplicativeShock(...), MultiplicativeShock(...)]}
Full Config Schema¶
{
"id": "SCENARIO_NAME",
"shocks": [
{
"table": "table_name",
"column": "optional_column",
"pipeline": [
{"multiply": 1.5},
{"clip": {"max": 1.0}}
],
"where": {"dimension": {"operator": "value"}},
"when": {"t": {"operator": "value"}}
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
table |
string | Yes* | Target assumption table |
param |
string | Yes* | Target scalar parameter (*use one of table/param) |
column |
string | No | Specific column within table |
pipeline |
array | No | Sequence of operations |
where |
object | No | Dimension filter conditions |
when |
object | No | Time filter conditions |
| Operation | varies | Yes | One of: multiply, add, set, clip, max, min |
Putting It Together: Solvency II SCR¶
Here's how the building blocks combine into real regulatory scenarios.
The Solvency II Lapse Shocks¶
| Shock | Requirement | Config |
|---|---|---|
| Lapse Up | min(lapse × 1.5, 1.0) |
Pipeline: multiply then clip |
| Lapse Down | max(lapse × 0.5, lapse - 0.2) |
Max of two transformations |
| Mass Lapse | 40% surrender at t=0 | Time-conditional additive |
from gaspatchio_core.scenarios import (
parse_scenario_config,
describe_scenarios,
)
# Define Solvency II lapse scenarios
config = [
{"id": "BASE"},
{
"id": "LAPSE_UP",
"shocks": [
{
"table": "lapse",
"pipeline": [
{"multiply": 1.5},
{"clip": {"max": 1.0}}
]
}
]
},
{
"id": "LAPSE_DOWN",
"shocks": [
{"table": "lapse", "max": [{"multiply": 0.5}, {"add": -0.2}]}
]
},
{
"id": "MASS_LAPSE",
"shocks": [
{"table": "lapse", "add": 0.40, "when": {"t": {"eq": 0}}}
]
},
]
# Parse into shock objects
scenarios = parse_scenario_config(config)
# Generate audit trail
print(describe_scenarios(scenarios, output_format="markdown"))
Output:
## Scenario: BASE
No shocks applied.
## Scenario: LAPSE_UP
- pipeline to lapse: multiply by 1.5 → clip max=1.0
## Scenario: LAPSE_DOWN
- max(multiply by 0.5, add -0.2) to lapse
## Scenario: MASS_LAPSE
- add +0.4 to lapse WHEN t = 0
Complete Working Example¶
import polars as pl
from gaspatchio_core import ActuarialFrame
from gaspatchio_core.assumptions import Table
from gaspatchio_core.scenarios import (
with_scenarios,
parse_scenario_config,
)
# Sample lapse table
lapse_data = pl.DataFrame({
"duration": [1, 2, 3, 4, 5],
"lapse_rate": [0.08, 0.06, 0.05, 0.04, 0.03],
})
lapse_table = Table(
name="lapse",
source=lapse_data,
dimensions={"duration": "duration"},
value="lapse_rate",
)
# Model points
af = ActuarialFrame({
"policy_id": ["P001", "P002", "P003"],
"duration": [1, 3, 5],
})
# Expand across scenarios
scenarios = ["BASE", "LAPSE_UP", "LAPSE_DOWN"]
af = with_scenarios(af, scenarios)
# Define shocks
shock_config = [
{"id": "BASE"},
{
"id": "LAPSE_UP",
"shocks": [{"table": "lapse", "multiply": 1.5, "clip": [None, 1.0]}]
},
{
"id": "LAPSE_DOWN",
"shocks": [{"table": "lapse", "max": [{"multiply": 0.5}, {"add": -0.2}]}]
},
]
parsed_scenarios = parse_scenario_config(shock_config)
# Look up lapse rates (base values)
af.base_lapse = lapse_table.lookup(duration=af.duration)
# Apply shocks based on scenario_id
# In practice, you'd use Table.from_shocks() or apply shocks in the lookup
# This example shows the concept
print(af.collect())
Framework-Agnostic Patterns¶
While we used Solvency II as an example, these patterns apply to any regulatory framework:
| Framework | Common Shocks | Gaspatchio Pattern |
|---|---|---|
| Solvency II | Lapse up/down, mass lapse, expense inflation | Pipeline + clip, max, time-conditional |
| IFRS 17 | Risk adjustment scenarios | Multiplicative on all decrements |
| US RBC | C1-C4 factors | Multiplicative, often filtered by product |
| ORSA | Management actions, stress scenarios | Combined where + when filters |
| ALM | Interest rate shocks | Additive on curve tables |
The building blocks are universal - only the specific factors and combinations change.
API Quick Reference¶
See the Scenarios API for full function signatures.
| Function/Class | Purpose |
|---|---|
parse_shock_config() |
Parse single shock dict → Shock object |
parse_scenario_config() |
Parse scenario list → dict of shocks |
describe_scenarios() |
Generate human-readable audit trail |
with_scenarios() |
Expand ActuarialFrame across scenarios |
sensitivity_analysis() |
Generate scenario sweep configs |
batch_scenarios() |
Memory-efficient batching for large runs |
Shock Classes:
| Class | Operation |
|---|---|
MultiplicativeShock |
Scale by factor |
AdditiveShock |
Add constant |
OverrideShock |
Replace with value |
ClipShock |
Cap/floor values |
PipelineShock |
Chain operations |
FilteredShock |
WHERE clause |
TimeConditionalShock |
WHEN clause |
MaxShock |
Maximum of two shocks |
MinShock |
Minimum of two shocks |
ParameterShock |
Scalar parameter shocks |
See Also¶
- What-If Analysis - Simple question → config translation
- Table Sensitivities - Python API for applying shocks to tables
- Scenarios Overview - High-level scenario concepts
- Performance - Optimizing large scenario runs