Simulation & Stress Testing Decision Policies

9 min read Module 8 of 9 Lesson 24 of 25

What you'll learn

  • Explain what simulation adds beyond off-policy evaluation and experiments
  • Build a simple agent-based simulator of customer responses and calibrate it to historical data
  • Stress test policies against scenarios: base-rate shocks, budget cuts, response decay and adversarial behaviour
  • Validate simulators against held-out reality and manage the sim-to-real gap
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

Every evaluation method so far looks at the past. Holdouts and experiments measure what happened. Off-policy evaluation reweights what happened. But decision owners also ask about things that haven’t happened: What if churn doubles? What if finance cuts the budget by half mid-quarter? What if customers learn to game the cancel-page offer? What happens after a year of this policy, not just a month?

Those are questions for simulation: a model of the world, not just of one outcome, that you can run policies against under conditions you choose.

What simulation adds

Simulation earns its place in three situations:

  • Counterfactual conditions. Shocks that never occurred in the logs (a demand collapse, a fraud attack, a budget cut), so neither OPE nor past experiments cover them.
  • Long-horizon dynamics. Effects that build over many decisions: offer fatigue, customers learning to wait for discounts, inventory carrying over between weeks, credit exposure accumulating. Single-step methods miss these.
  • Rare, high-cost events. Tail scenarios you’d never want to discover in production.

Simulation does not replace experiments. A simulator is itself a model, built from assumptions, and its conclusions are only as good as those assumptions. Treat it as a stress-testing and screening tool.

Kinds of simulators

  • Replay with perturbation. Take historical data and modify it: scale base rates, remove a share of budget, inject a shock. The cheapest option, limited to changes that don’t require modeling behaviour.
  • Agent-based simulators. Model each customer (or driver, applicant, store) as an agent with a state and a response model: how likely they are to churn, how they respond to each offer, how their state changes over time. Run many agents through many periods under each policy.
  • Digital twins. Detailed operational simulators of a supply chain, warehouse or network, often built with discrete-event simulation. Common in operations decisions.

For most customer-facing decisions, a modest agent-based simulator is the sweet spot.

A simple customer simulator

Here’s the skeleton of an agent-based simulator for the retention example. Each month, every customer has a churn hazard depending on their state; offers reduce it according to an uplift model; repeated discounts lose effect (fatigue); and a share of customers learn to visit the cancel page to get offers.

import numpy as np
from dataclasses import dataclass

@dataclass
class World:
    base_hazard_scale: float = 1.0      # stress: >1 means more churn
    offer_effect_scale: float = 1.0     # stress: <1 means offers work less
    fatigue_per_offer: float = 0.35     # each past discount cuts effect by 35%
    gaming_rate: float = 0.0            # share of offered customers who learn to game

def simulate(policy, customers, world: World, months=12, seed=0):
    """customers: dict of arrays (features, monthly_margin, baseline hazard
    and per-offer uplift from the fitted models). policy(state) -> action array.
    Returns total margin net of offer costs, for one simulated year."""
    rng = np.random.default_rng(seed)
    n = len(customers["margin"])
    active = np.ones(n, bool)
    past_offers = np.zeros(n)
    gamer = np.zeros(n, bool)
    total = 0.0
    for m in range(months):
        state = {**customers, "active": active, "past_offers": past_offers, "gamer": gamer}
        action = policy(state)                          # 0 = none, else offer id
        offered = (action > 0) & active
        effect = customers["uplift"][np.arange(n), action] * world.offer_effect_scale
        effect *= (1 - world.fatigue_per_offer) ** past_offers   # fatigue
        hazard = customers["hazard"] * world.base_hazard_scale - np.where(offered, effect, 0)
        hazard = np.where(gamer & ~offered, hazard * 1.5, hazard)  # gamers churn if ignored
        churn = active & (rng.random(n) < np.clip(hazard, 0, 1))
        active &= ~churn
        total += customers["margin"][active].sum()
        total -= customers["offer_cost"][np.arange(n), action][offered & active].sum()
        past_offers += offered
        gamer |= offered & (rng.random(n) < world.gaming_rate)
    return total

Every number in this simulator is an assumption: the hazard model, the uplift model, the fatigue rate, the gaming behaviour. The first two come from the fitted models of Modules 2 and 4. The others come from evidence where it exists (past repeat-offer data for fatigue) and from explicit, documented guesses where it doesn’t.

Calibrate, then validate

A simulator must reproduce reality before it can say anything about counterfactuals:

  1. Calibrate to history: simulate the historical policy over a past period and tune unknown parameters until simulated churn rates, offer acceptance and margin match what actually happened, overall and by key segments.
  2. Validate out of sample: simulate a period, or better a past experiment, the simulator wasn’t calibrated on. If a real A/B test showed policy B beat A by 4%, does the simulator show something similar? Reproducing known policy differences is the most convincing evidence that the simulator can rank new ones.
  3. Document every assumption, its source, and how sensitive results are to it.

A simulator that can’t reproduce a known experiment shouldn’t be used to pick policies.

Stress testing

With a validated simulator, run each candidate policy across a grid of scenarios, with many random seeds per scenario:

scenarios = {
    "base": World(),
    "churn_shock_x2": World(base_hazard_scale=2.0),
    "offers_weaken": World(offer_effect_scale=0.5),
    "fast_fatigue": World(fatigue_per_offer=0.6),
    "gaming": World(gaming_rate=0.15),
}
results = {
    (pname, sname): np.mean([simulate(p, customers, w, seed=s) for s in range(30)])
    for pname, p in policies.items()
    for sname, w in scenarios.items()
}

Read the results as a policy × scenario table. What you’re looking for:

  • Fragile winners: policies that win in the base case but lose badly in a plausible stress scenario. A 3% base-case gain isn’t worth a 20% loss if offers decay faster than expected, unless you can detect that scenario quickly and switch.
  • Robust choices: policies that are rarely best but never bad. Often the right choice under uncertainty, in the same spirit as Lesson 9.
  • Breaking points: at what churn shock, budget cut or gaming rate does a policy stop being profitable? Those thresholds become monitoring alerts (Lesson 21).
  • Long-term divergence: policies that look similar over one month but diverge over twelve, typically because of fatigue or gaming. Exactly what single-step OPE can’t see.

The sim-to-real gap

The temptation, once a simulator exists, is to optimize policies inside it, perhaps with reinforcement learning. Be careful. An optimizer trained against a simulator will exploit the simulator’s inaccuracies, exactly as Lesson 8’s optimizer exploited prediction errors. The policy learns to win in the simulated world.

Use simulation to generate and screen candidates and to stress test them, then validate with OPE on real logs and with real experiments. Keep the simulator honest by re-validating it against every new experiment result: each one is a free test of whether its predictions hold.

Synthetic customers generated by LLMs (“simulate how this customer persona responds to the offer email”) are sometimes proposed as a shortcut. They can help brainstorm scenarios and message variants, but their responses aren’t calibrated to your customers’ real behaviour. Never use them as evidence of effect size.

With simulation, the toolkit is complete. The capstone puts every piece together into one system.

Knowledge Check

4 questions to test your understanding

1 Which question can simulation answer that off-policy evaluation on historical logs cannot?

2 A simulator was calibrated on 2024-2025 data. How should you check it's trustworthy before using it to choose policies?

3 Policy A beats policy B by 3% in the base scenario but loses by 20% when the response to offers decays faster than expected. What should the decision owner take from this?

4 Why is it risky to train a policy purely inside a simulator and deploy it directly?

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan