Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
An AI assistant resolves 54% of its tickets, while the old workflow resolves 76%. A release report calls the assistant harmful and recommends pulling the plug. Then the tickets are split by pre-routing risk: the assistant's observed resolution rate is ten percentage points higher inside both easy and difficult groups. Which comparison should drive the rollout decision?
Experiment Design and A/B Testing Foundations introduced fair comparisons. Causal inference asks whether an action caused an observed outcome, then makes the assignment assumptions behind that conclusion mathematically explicit.[1] You can't rely on the unadjusted aggregate label on the report.

Watch the aggregate reverse the group results
Work with a synthetic log of 200 tickets. Treatment means routing a ticket to a fixed assistant workflow; control means routing it to the existing human workflow. Success means resolution within 24 hours, measured the exact same way in both arms. Risk is recorded before routing, not inferred from the assistant's response.
Within each risk group, the assistant's observed success rate is ten percentage points higher. Before looking at the totals, predict what the aggregate would say if both arms had the same risk mix.
| Ticket risk | Assistant success | Control success | Observed difference |
|---|---|---|---|
| Low | 9/10 = 90% | 72/90 = 80% | +10 percentage points |
| High | 45/90 = 50% | 4/10 = 40% | +10 percentage points |
| Naive aggregate | 54/100 = 54% | 76/100 = 76% | -22 percentage points |
The totals compare different populations. The assistant receives 90 difficult tickets; control receives 90 easy tickets. That unequal mix flips the aggregate direction.
This reversal is Simpson's paradox: group composition changes the direction of the aggregated association. Observational counts alone still don't establish a causal improvement.
The next snippet computes both within-group differences and the reversed aggregate. It reports percentage points, not relative percentage changes: 90% minus 80% is 10 points, whereas the relative increase is 10/80 = 12.5%.
1groups = {
2 "low": {"assistant": (9, 10), "control": (72, 90)},
3 "high": {"assistant": (45, 90), "control": (4, 10)},
4}
5
6for risk, counts in groups.items():
7 assistant_success, assistant_total = counts["assistant"]
8 control_success, control_total = counts["control"]
9 difference = assistant_success / assistant_total - control_success / control_total
10 print(f"{risk:>4} risk difference: {100 * difference:+.0f} percentage points")
11
12assistant_rate = (9 + 45) / (10 + 90)
13control_rate = (72 + 4) / (90 + 10)
14print(f"naive aggregate difference: {100 * (assistant_rate - control_rate):+.0f} percentage points")1low risk difference: +10 percentage points
2high risk difference: +10 percentage points
3naive aggregate difference: -22 percentage pointsThe reversal disappears once both workflows are compared against the same risk distribution. That repairs the visible mix, but it still leaves a deeper question: for one assistant-handled ticket, what would control have done?
Pearl's ladder: association, intervention, and counterfactuals
Standard machine learning models optimize conditional expectations like . When evaluating systems, this observational conditioning mixes the effect of the model with the behavior of upstream routers, triage heuristics, and customer selection. Judea Pearl organized causal queries into three distinct levels known as the Ladder of Causation.[1]
Rung 1: association (seeing)
Observational queries ask: If we see event , what's our belief about ? In notation, this is . This is the native language of statistical learning, correlation, and passive monitoring. In the support log, . This number answers what happened to tickets observed under the assistant, but it reflects the difficult queue the assistant was assigned to rather than the tool's standalone capability.
Rung 2: intervention (doing)
Interventional queries ask: What happens to if we actively set variable to value ? Pearl denotes this operation with the operator: . Intervening isn't passive conditioning. An intervention cuts the natural causal arrows flowing into , replacing the upstream routing rule with an external mandate. If we force every incoming ticket to the assistant regardless of risk, the resolution rate is , which doesn't equal whenever confounding exists.
Rung 3: counterfactuals (imagining)
Counterfactual queries ask retrospective questions about specific units: Given that ticket #102 received the legacy control workflow and failed to resolve within 24 hours, what would have happened if we had routed that exact ticket to the assistant? In notation, this queries . Rung 3 lives in hypothetical alternative worlds. It enables individual credit assignment, root-cause debugging, and algorithmic fairness audits that observational data alone can't answer.
A dashboard shows that users who trigger an LLM code explanation feature retain 15% better than users who don't. Does this justify rolling out the feature to all users?
Answer
No. That comparison sits on Rung 1: P(Retain | Feature = 1) vs P(Retain | Feature = 0). Highly motivated users might use the feature more frequently. Deciding a rollout requires an interventional query on Rung 2: P(Retain | do(Feature = 1)), which separates user motivation from feature impact.
Separate observed outcomes from missing alternatives
Take one difficult ticket that received the assistant and resolved within 24 hours. Its observed outcome is 1. Define for assistant assignment and for control. The potential outcomes and are that ticket's binary resolution outcomes under the two workflows. Their difference is its individual treatment effect.
Only one potential outcome is ever observed for a given unit. Here , but the counterfactual could be 0 or 1. The assistant may have caused this success, or the ticket may have resolved either way. Another control ticket can help estimate an average, but can't reveal this ticket's missing outcome. This limitation is known as the Fundamental Problem of Causal Inference.[2]
Causal estimands: ATE and ATT
The average treatment effect averages those individual differences across the entire population:
For binary resolution, an ATE of 0.10 means ten additional resolutions per 100 tickets on average under assistant assignment versus control.
Engineers also inspect the Average Treatment Effect on the Treated (ATT):
When treatment assignment is completely randomized, ATE and ATT coincide. When routing policies select specific subgroups, they can diverge sharply. If the assistant provides a larger boost on difficult tickets than on easy tickets, and the triage system routes mostly difficult tickets to the assistant, ATT will exceed ATE.
Latent response types and the counterfactual matrix
Because individual counterfactuals are hidden, every unit in a binary-outcome setting belongs to one of four latent response types:
- Always Resolves (Immune): . The ticket succeeds under either workflow.
- Helped (Beneficiary): . The assistant succeeds where legacy control fails.
- Harmed (Adverse): . Legacy control succeeds, but the assistant fails.
- Never Resolves (Doomed): . The ticket fails under both workflows.

Consider two hypothetical populations of 100 tickets, with both potential outcomes filled in for illustration:
| Potential-outcome type | Population A | Population B | ||
|---|---|---|---|---|
| Resolves under either workflow | 1 | 1 | 68 | 58 |
| Helped by assistant | 1 | 0 | 10 | 20 |
| Harmed by assistant | 0 | 1 | 0 | 10 |
| Resolves under neither | 0 | 0 | 22 | 12 |
Both populations produce 78 assistant successes and 68 control successes. In Population A, ten tickets benefit and zero are harmed. In Population B, twenty benefit and ten are harmed. The net gain is ten points in both cases. Even an ideal randomized experiment estimating marginal averages can't reveal which pairing of potential outcomes is true without structural assumptions like monotonicity ( for all ).
The next snippet verifies both populations from their underlying counts:
1import numpy as np
2
3# Columns are Y(1), Y(0); rows match the four types in the table.
4potential_outcomes = np.array([[1, 1], [1, 0], [0, 1], [0, 0]])
5populations = {"A": np.array([68, 10, 0, 22]), "B": np.array([58, 20, 10, 12])}
6
7for name, counts in populations.items():
8 rates = counts @ potential_outcomes / counts.sum()
9 effect = rates[0] - rates[1]
10 print(
11 f"{name}: assistant={rates[0]:.0%}, control={rates[1]:.0%}, "
12 f"ATE={100 * effect:+.0f} points; helped={counts[1]}, harmed={counts[2]}"
13 )1A: assistant=78%, control=68%, ATE=+10 points; helped=10, harmed=0
2B: assistant=78%, control=68%, ATE=+10 points; helped=20, harmed=10Why can't the control group's 76% aggregate success rate serve as the missing outcome for every assistant ticket?
Answer
The control group contains a much larger share of easy tickets, so its aggregate doesn't provide a comparable average for the assistant-assigned cases. Even a perfectly comparable control group identifies an average, not the missing binary outcome of each individual ticket.
Causal graphs, d-separation, and the three canonical junctions
The routing table shows an imbalanced mix, but not why routing produced it. Suppose the triage system sends difficult tickets to the assistant more often. Risk exists before routing and influences both assignment and resolution, making it a confounder.
Directed acyclic graphs (DAGs) encode causal assumptions as non-parametric structural equations. Arrows represent direct causal influence, not correlations. To understand how statistical dependencies flow through graphs, Pearl developed the rules of d-separation (directional separation), built from three elementary three-node junctions.[1]

1. The chain (mediator):
Treatment causes an intermediate variable , which in turn causes outcome . For example, the assistant () produces an automated diagnostic summary (), which helps staff resolve the ticket ().
- Unconditioned state: The path is active. Association flows from to , transmitting the causal effect.
- Conditioned state: Conditioning on mediator blocks the path. Holding the summary fixed isolates any direct effect , but blocks the indirect mechanism through which the assistant works. If you adjust for a mediator when estimating the total effect, you wipe out the very benefit you're trying to measure.
2. The fork (confounder):
A pre-treatment common cause influences both treatment assignment and outcome . In our triage system, baseline ticket risk () determines routing () and changes resolution difficulty ().
- Unconditioned state: The path is active. Association flows through the backdoor , creating a non-causal association that produces Simpson's reversal.
- Conditioned state: Conditioning on confounder blocks the backdoor path. Stratifying by risk allows within-stratum comparisons to isolate the true causal effect.
3. The collider (inverted fork):
Two independent variables point to a shared effect . For instance, suppose an automated quality audit flag () is triggered if the assistant was used () or if the ticket failed to resolve ().
- Unconditioned state: The path is naturally blocked. No association flows between and through .
- Conditioned state: Conditioning on collider (such as filtering an evaluation dashboard to only tickets that triggered quality audit flags) opens an artificial path between and . This phenomenon is Berkson's bias or selection bias. If an audited ticket didn't fail, it probably used the assistant; this induces an artificial negative correlation between assistant usage and ticket resolution!
The backdoor criterion
Pearl formalized when a set of observed covariates is sufficient to identify the causal effect of on :[1]
A set of variables satisfies the backdoor criterion relative to an ordered pair of variables in a DAG if:
- No node in is a descendant of .
- blocks every path between and that contains an arrow pointing into (backdoor paths).
If satisfies this criterion, the causal effect is identified by the backdoor adjustment formula:
Ticket risk is recorded before routing, while an escalation note is created by the assistant after routing. Which variable can support backdoor adjustment, and why can't the other replace it?
Answer
Pre-treatment risk can block the common-cause fork when it captures the relevant assignment and outcome differences. The assistant-generated escalation note occurs after treatment, so conditioning on it can block the mediator path or act as a collider, introducing new selection bias.
Standardize both arms to one population
Suppose the planned deployment has 70% low-risk and 30% high-risk tickets. What would each workflow's resolution rate be at that same mix? The assistant rate becomes ; the control rate becomes . Their standardized difference is:
The hat marks an estimate from observed data. Both within-group differences are ten points, so this weighted difference is ten points for any shared risk mix. The individual arm rates depend on the mix, and unequal subgroup differences would make the estimated effect depend on it too. Calling this difference an identified causal effect requires explicit identification conditions.[2]
The next snippet continues the earlier session and applies one shared population distribution to both arms:
1target_weights = {"low": 0.7, "high": 0.3}
2standardized = {"assistant": 0.0, "control": 0.0}
3
4for risk, population_weight in target_weights.items():
5 for workflow in standardized:
6 successes, total = groups[risk][workflow]
7 standardized[workflow] += population_weight * successes / total
8
9adjusted_effect = standardized["assistant"] - standardized["control"]
10for workflow, rate in standardized.items():
11 print(f"{workflow} at the target mix: {rate:.0%}")
12print(f"standardized difference: {100 * adjusted_effect:+.0f} percentage points")
13assert abs(adjusted_effect - 0.10) < 1e-121assistant at the target mix: 78%
2control at the target mix: 68%
3standardized difference: +10 percentage pointsThe arithmetic answers a target-population comparison. It estimates an identified causal effect only under explicit assumptions:
| Identification assumption | Meaning in the ticket example |
|---|---|
| Conditional exchangeability | Within measured risk groups, no unrecorded common cause still changes both routing and resolution: . |
| Positivity or overlap | Both assistant and control assignments occur for every target risk stratum: . |
| Consistency | The recorded outcome matches the specific treatment received: . |
| No interference (SUTVA) | One ticket's assignment doesn't alter another ticket's outcome (no queue spillover). |
| Transfer to target | The 70% and 30% weights describe deployment, and risk-specific potential-outcome averages apply there too. |
The smallest strata contain only ten tickets. If one low-risk assistant success became a failure, its rate would fall from 90% to 80%, and the standardized difference would fall from ten points to three. That's a sensitivity check, not a formal confidence interval, but it shows why the estimate needs uncertainty analysis before an engineering rollout. More data reduce sampling variance; they don't remove unmeasured confounding.
Both observed risk groups show a ten-point advantage, but a hidden customer-tier field affects both assistant routing and resolution. Does standardizing only by recorded risk identify the causal effect?
Answer
Not necessarily. The arithmetic still returns a risk-adjusted ten-point association, but an unmeasured common cause violates conditional exchangeability. Without measuring and adjusting for customer tier, or switching to a randomized design, that association isn't guaranteed to be causal.
Weight the assignment, then check its support
Standardization averages group-specific outcomes at a chosen mix. Inverse Probability of Treatment Weighting (IPTW) instead weights each observation by the inverse of the probability of receiving the treatment it actually received. The propensity score is the conditional assignment probability , not the probability of successful resolution.[2]
Estimate it from the log: low-risk tickets went to the assistant 10 times out of 100, so . High-risk tickets went 90 times out of 100, so . An assistant ticket gets weight ; a control ticket gets .

The ten low-risk assistant tickets each get weight 10, for total weight 100. The ninety high-risk assistant tickets each get weight , also totaling 100. The control arm gets the same 100/100 weighted mix. These weights recover the logged population's 50%/50% risk mix, not the planned deployment's 70%/30% mix. Weighting and standardization agree when they target the same population.
This snippet works directly with the grouped counts:
1import numpy as np
2
3totals = np.array([[10, 90], [90, 10]], dtype=float)
4successes = np.array([[9, 72], [45, 4]], dtype=float)
5
6def assignment_weights(counts):
7 if np.any(counts <= 0):
8 raise ValueError("No within-risk comparison: a workflow has zero tickets")
9 propensity = counts[:, 0] / counts.sum(axis=1)
10 return propensity, np.column_stack([1 / propensity, 1 / (1 - propensity)])
11
12propensity, weights = assignment_weights(totals)
13weighted_totals = totals * weights
14rates = (successes * weights).sum(axis=0) / weighted_totals.sum(axis=0)
15print("assignment propensity (low, high):", propensity)
16print("weighted counts, rows=low/high; columns=assistant/control:")
17print(np.round(weighted_totals, 1))
18print(f"50/50 mix: assistant={rates[0]:.0%}, control={rates[1]:.0%}")
19print(f"weighted difference: {100 * (rates[0] - rates[1]):+.0f} percentage points")
20
21try:
22 assignment_weights(np.array([[10, 90], [100, 0]]))
23except ValueError as error:
24 print("unsupported comparison:", error)1assignment propensity (low, high): [0.1 0.9]
2weighted counts, rows=low/high; columns=assistant/control:
3[[100. 100.]
4 [100. 100.]]
550/50 mix: assistant=70%, control=60%
6weighted difference: +10 percentage points
7unsupported comparison: No within-risk comparison: a workflow has zero ticketsPositivity violations and extreme weights
If high-risk tickets always receive the assistant, then : no high-risk control outcomes can appear in the logs. This violates positivity (overlap). Without examples from both actions, no reweighting can identify the counterfactual without extra parametric assumptions.
Even non-zero probabilities can destabilize estimators. A rare assignment probability of 0.005 yields an inverse weight of 200, allowing a single noisy ticket to dominate the estimate. Production systems inspect weight distributions and employ stabilized weights (the Hajek estimator, normalizing weights to sum to one within each arm) or apply weight clipping, accepting a small amount of bias to rein in variance.
The weighted assistant rate is 70%, while the 70/30 standardized assistant rate was 78%. Did one calculation fail?
Answer
No. The inverse-probability weights here target the log's 50/50 risk mix: 0.5(0.90) + 0.5(0.50) = 0.70. Deployment standardization targets 70/30: 0.7(0.90) + 0.3(0.50) = 0.78. The differences between arms happen to agree because both risk groups share the same ten-point difference.
Instrumental variables when unmeasured confounders lurk
What happens when an unmeasured confounder (like customer technical frustration or urgency) affects both treatment routing and resolution ? Conditional exchangeability fails because isn't recorded in the logs. Backdoor adjustment and IPTW can't eliminate the bias.
An instrumental variable (IV) offers an alternative identification route. A valid instrument satisfies three conditions:[1]
- Relevance: causally affects treatment assignment ().
- Exclusion Restriction: affects outcome only through treatment (no direct arrow ).
- Exogeneity / Independence: shares no common causes with ().

In an AI support platform, could be a randomized client gateway latency experiment or canary flag that stochastically pushes incoming requests toward the assistant without affecting customer frustration.
Wald estimator and local average treatment effects
When treatment is binary and compliance is imperfect, the Wald estimator computes the causal effect by taking the ratio of the intention-to-treat outcome difference to the assignment compliance difference:
Suppose setting canary flag raises assistant usage from 30% to 70% (), while overall ticket resolution rises from 68% to 72% (). The Wald estimate is:
Under one-sided non-compliance and monotonicity (no defiers who choose the opposite of their assignment), this identifies the Local Average Treatment Effect (LATE): the causal effect on compliers whose routing was changed by the instrument.
Why can't an engineer use the ticket's word count as an instrument for assistant routing?
Answer
Ticket word count correlates with issue complexity, which directly affects resolution time. This violates the exclusion restriction: word count has a direct causal path to resolution that doesn't pass through assistant routing.
Causal inference in production AI systems
Causal principles are fundamental to machine learning and AI agent engineering, particularly when models interact with environments, user behavior, and multi-step tool calls.
Offline policy evaluation (OPE)
When testing a new agent policy (such as a redesigned triage prompt), deploying it live to production traffic can degrade user experience. Instead, teams evaluate on historical interaction logs collected under a legacy logging policy .
This problem matches observational causal inference. The action is the treatment, state is the covariate context, and reward is the outcome. The standard importance-sampling estimator (IPS) weights logged rewards by the policy probability ratio:
For multi-step agents, importance ratios compound exponentially over the trajectory horizon : , causing massive variance.
To tame this variance, teams turn to the Doubly Robust (DR) estimator. DR fits a baseline regression model and uses importance weights only to correct the residual:
The Doubly Robust estimator is unbiased if either the propensity model is accurate OR the reward model is accurate.
The next snippet verifies how the Doubly Robust estimator corrects a biased direct model using importance-weighted residuals from our 200-ticket log:
1import numpy as np
2
3# Reconstruct individual records from the 200 tickets
4# Stratum: risk (0: low, 1: high), action A (1: assistant, 0: control), outcome Y
5records = []
6records.extend([(0, 1, 1)] * 9 + [(0, 1, 0)] * 1) # Low assistant: 9/10
7records.extend([(0, 0, 1)] * 72 + [(0, 0, 0)] * 18) # Low control: 72/90
8records.extend([(1, 1, 1)] * 45 + [(1, 1, 0)] * 45) # High assistant: 45/90
9records.extend([(1, 0, 1)] * 4 + [(1, 0, 0)] * 6) # High control: 4/10
10
11data = np.array(records)
12risk, a_obs, y_obs = data[:, 0], data[:, 1], data[:, 2]
13
14# Logging policy pi_0(A=1 | risk)
15pi_0 = np.where(risk == 0, 0.10, 0.90)
16
17# Evaluate candidate policy pi_target: always route to assistant (action 1)
18pi_target = 1.0
19w = np.where(a_obs == 1, pi_target / pi_0, 0.0)
20
21# Direct model Q_hat with deliberate positive bias (+5 percentage points)
22q_hat_1 = np.where(risk == 0, 0.95, 0.55) # True rates: 0.90, 0.50
23q_hat_obs = np.where(a_obs == 1, q_hat_1, 0.50)
24
25v_ips = np.mean(w * y_obs)
26v_direct = np.mean(q_hat_1)
27v_dr = np.mean(q_hat_1 + w * (y_obs - q_hat_obs))
28
29print(f"Direct model (biased): {v_direct:.1%}")
30print(f"IPS estimator (unbiased): {v_ips:.1%}")
31print(f"Doubly Robust (corrected): {v_dr:.1%}")
32assert abs(v_dr - 0.70) < 1e-121Direct model (biased): 75.0%
2IPS estimator (unbiased): 70.0%
3Doubly Robust (corrected): 70.0%Recommendation and search debiasing
In recommendation engines and search rankers, user interaction logs suffer from severe position bias and popularity bias. Users click the top-ranked item because it's displayed first, not necessarily because it's the most relevant. Training ranking models directly on raw clicks creates a self-reinforcing feedback loop. By treating presentation position as an observed confounder and weighting training losses by inverse examination probabilities, models learn true user preferences rather than interface artifacts.
Counterfactual reasoning in LLM agents
When an autonomous agent executes a sequence of tool calls (e.g. database query, code generation, test execution) and fails at step 5, diagnosing which step caused the failure is non-trivial. Did tool call 2 return subtly corrupted data, or did tool call 4 fail on valid input?
By constructing a causal DAG of the execution trace, developers can perform counterfactual interventions: holding the agent's intermediate state fixed up to step 2, intervening on tool call 2's return value, and re-running downstream generation. This isolates the causal contribution of individual agent steps and provides principled credit assignment for agent reinforcement learning.
Decide what the comparison supports
| Evaluation method | Identification requirements | Core advantages | Primary failure modes |
|---|---|---|---|
| Standardization (g-formula) | Exchangeability, positivity, consistency, correctly specified outcome model | Direct estimation across target population distributions | Unmeasured confounding; model misspecification in high dimensions |
| Inverse Weighting (IPTW) | Exchangeability, positivity, correctly modeled assignment propensity | Balances entire covariate distribution without outcome modeling | Extreme weights explode variance; positivity violations |
| Instrumental Variables (IV) | Relevant instrument, exclusion restriction, exogeneity | Identifies causal effects even when unmeasured confounders exist | Weak instruments amplify standard errors; identifies only LATE |
| Doubly Robust (DR / OPE) | Overlap, and either propensity model or reward model correctly specified | Minimum variance and double protection against misspecification | Multi-step horizon compounding; simultaneous model failure |
| Randomized A/B Experiment | Unconfounded in expectation; non-interference (SUTVA) | Gold standard; breaks all pre-treatment confounding paths | Queue interference; attrition; inability to observe individual counterfactuals |
A properly implemented randomized design makes assignment independent of potential outcomes in expectation. It balances baseline characteristics across arms, but it doesn't automatically repair post-assignment attrition, interference, or measurement errors. For support tickets sharing an agent pool, assigning one ticket to an AI assistant can free staff time for another, violating the no-interference assumption.
For this release report, the raw -22-point difference compared unequal case mixes. The +10-point standardized difference answers a better-specified comparison, but remains an estimate conditioned on routing, measurement, and interference assumptions. It doesn't prove that every ticket benefited. A randomized evaluation with sufficient tickets in both risk strata resolves routing bias; consistent measurement horizons and queue designs ensure the resulting numbers can be trusted.