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
RelativeFloorShock Floor relative to original value (e.g. cannot decrease by more than delta)
ParameterShock Scalar parameter shocks

See Also