Skip to content

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:

  1. The building blocks - Individual shock operations (multiply, add, clip, etc.)
  2. How to filter - Apply shocks to specific segments or time periods
  3. How to compose - Chain operations together for complex transformations
  4. 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