Skip to content
Skip to lesson
Back to Topics
LearnML Algorithms & EvaluationCausal Inference and Counterfactuals
📊MediumEvaluation & Benchmarks

Causal Inference and Counterfactuals

Separate correlation from intervention, diagnose Simpson's reversal, estimate adjusted treatment effects, and inspect overlap before trusting observational AI evaluations.

29 min read
Learning path
Step 44 of 197 in the full curriculum

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 decision?

Experiment Design and A/B Testing Foundations introduced fair comparisons. 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.

Assistant tickets are 10% low risk and 90% high risk; control tickets are 90% low risk and 10% high risk. Raw success is 54% versus 76%. Standardizing both workflows to 70% low risk and 30% high risk gives 78% versus 68%.
The raw arms have opposite case mixes. Giving both workflows the target mix changes their success-rate comparison; it doesn't by itself establish causality.

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 riskAssistant successControl successObserved difference
Low9/10 = 90%72/90 = 80%+10 percentage points
High45/90 = 50%4/10 = 40%+10 percentage points
Naive aggregate54/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 : 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%.

inspect-confounded-ticket-outcomes.py
groups = {
    "low": {"assistant": (9, 10), "control": (72, 90)},
    "high": {"assistant": (45, 90), "control": (4, 10)},
}

for risk, counts in groups.items():
    assistant_success, assistant_total = counts["assistant"]
    control_success, control_total = counts["control"]
    difference = assistant_success / assistant_total - control_success / control_total
    print(f"{risk:>4} risk difference: {100 * difference:+.0f} percentage points")

assistant_rate = (9 + 45) / (10 + 90)
control_rate = (72 + 4) / (90 + 10)
print(f"naive aggregate difference: {100 * (assistant_rate - control_rate):+.0f} percentage points")
Output
low risk difference: +10 percentage points
high risk difference: +10 percentage points
naive aggregate difference: -22 percentage points

The 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

A predictor trained on observational logs can learn P(Y∣X)P(Y \mid X) without learning what changing XX would do. Routing and customer selection help determine which examples appear in those logs. Pearl's causal hierarchy separates three kinds of question: association, intervention, and counterfactuals.[2] A system's training objective and causal assumptions matter; “machine learning” doesn't restrict every model to one rung.

Rung 1: association (seeing)

Observational queries ask: Among tickets assigned to the assistant, what fraction resolved? In the log, P^(Y=1∣A=1)=0.54\hat P(Y=1 \mid A=1)=0.54. This describes the assistant-assigned cases. It combines the workflow's performance, their risk mix, and any other selection or measurement effects; it doesn't isolate the assistant's causal contribution.

Rung 2: intervention (doing)

Interventional queries ask: What fraction would resolve if a specified population were routed to the assistant? Write this as P(Y=1∣do⁡(A=1))P(Y=1 \mid \operatorname{do}(A=1)). In a structural causal model, this intervention replaces the equation assigning AA with A=1A=1 and removes its incoming arrows. It leaves the other mechanisms unchanged. Define the population too: our deployment target has 70% low-risk tickets.

Conditioning and intervention generally give different answers under confounding, but their numerical values can coincide through cancellation. The do-notation names the query; it doesn't identify its value from a dashboard. Identification needs a justified causal model or experimental evidence. If routing every ticket changes the shared queue, that effect must be part of the intervention model.

Rung 3: counterfactuals (imagining)

Counterfactual queries combine evidence from the actual world with an alternative action: Given that a control-assigned ticket failed, would it have resolved under the assistant? The corresponding probability is P(Y(1)=1∣A=0,Y=0)P(Y(1)=1 \mid A=0,Y=0). Conditioning only on those two facts describes a group of failed control tickets; a question about ticket #102 can condition on its other recorded features too.

Even knowing both intervention success rates doesn't determine that counterfactual probability. It needs information about how the two potential outcomes are paired within units. Causal assumptions can help identify or bound such queries; asking an LLM to imagine the alternative doesn't supply the missing evidence.

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 A=1A=1 for assistant assignment and A=0A=0 for control. The Y(1)Y(1) and Y(0)Y(0) are that ticket's binary resolution outcomes under the two workflows. Their difference Y(1)−Y(0)Y(1)-Y(0) is its individual treatment effect.

At this ticket's original decision point, we observe its outcome under one actual assignment. Here Y(1)=1Y(1)=1, but the Y(0)Y(0) 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 doesn't reveal this ticket's missing outcome. Replaying a case later changes its history; a simulated replay answers a question about the simulator. This limitation is the Fundamental Problem of Causal Inference.[3]

These potential outcomes refer to well-defined workflows and the same 24-hour horizon. Record the assistant policy, model and prompt versions, available tools, and control procedure. A stochastic assistant can be a defined policy; consistency doesn't require identical text on every run. The binary response-type example below treats potential outcomes as fixed for each hypothetical ticket.

Causal estimands: ATE and ATT

The averages those individual differences across a specified target population:

ATE⁡=E[Y(1)−Y(0)]\operatorname{ATE} = \mathbb{E}[Y(1) - Y(0)]

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):

ATT⁡=E[Y(1)−Y(0)∣A=1]\operatorname{ATT} = \mathbb{E}[Y(1) - Y(0) \mid A = 1]

If assignment is independent of potential outcomes with a common treatment probability, these population estimands coincide. A finite randomized run can still have different realized average effects in its treated subset by chance. Risk-dependent randomization doesn't automatically give marginal independence.

With selective routing and heterogeneous effects, ATT can differ from ATE. If difficult tickets benefit more and are overrepresented among treated cases, ATT can exceed the log-population ATE. In our displayed table both estimated subgroup differences are +10 points, so changing the risk mix leaves that estimated difference unchanged.

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:

  1. Always Resolves (Immune): Y(1)=1,Y(0)=1Y(1)=1, Y(0)=1. The ticket succeeds under either workflow.
  2. Helped (Beneficiary): Y(1)=1,Y(0)=0Y(1)=1, Y(0)=0. The assistant succeeds where legacy control fails.
  3. Harmed (Adverse): Y(1)=0,Y(0)=1Y(1)=0, Y(0)=1. Legacy control succeeds, but the assistant fails.
  4. Never Resolves (Doomed): Y(1)=0,Y(0)=0Y(1)=0, Y(0)=0. The ticket fails under both workflows.
Potential outcome counterfactual matrix and latent population types. Top: The Fundamental Problem of Causal Inference shows that for any single ticket, either Y(1) or Y(0) is observed while the counterfactual outcome is missing. Bottom: Two hypothetical populations have identical average treatment effect (+10 percentage points: 78% assistant vs 68% control), but Population A has zero tickets harmed (10 helped, 0 harmed) while Population B has ten tickets harmed (20 helped, 10 harmed).
Two populations with identical average treatment effects (+10 points) can hide vastly different individual outcomes: Population A helps 10 tickets without harming any, while Population B harms 10 tickets per hundred.

Consider two hypothetical populations of 100 tickets, with both potential outcomes filled in for illustration:

Potential-outcome typeY(1)Y(1)Y(0)Y(0)Population APopulation B
Resolves under either workflow116858
Helped by assistant101020
Harmed by assistant01010
Resolves under neither002212

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 identifying both marginal outcome rates can't determine their within-ticket pairing. Under these fixed binary potential outcomes, a no-harm assumption Yi(1)≥Yi(0)Y_i(1) \ge Y_i(0) would rule out Population B and identify the proportions of the four types, but not which tickets belong to them. The data's positive average doesn't establish that assumption or safety on other metrics.

The next snippet verifies both populations from their underlying counts:

same-average-different-counterfactuals.py
import numpy as np

# Columns are Y(1), Y(0); rows match the four types in the table.
potential_outcomes = np.array([[1, 1], [1, 0], [0, 1], [0, 0]])
populations = {"A": np.array([68, 10, 0, 22]), "B": np.array([58, 20, 10, 12])}

for name, counts in populations.items():
    rates = counts @ potential_outcomes / counts.sum()
    effect = rates[0] - rates[1]
    print(
        f"{name}: assistant={rates[0]:.0%}, control={rates[1]:.0%}, "
        f"ATE={100 * effect:+.0f} points; helped={counts[1]}, harmed={counts[2]}"
    )
Output
A: assistant=78%, control=68%, ATE=+10 points; helped=10, harmed=0
B: assistant=78%, control=68%, ATE=+10 points; helped=20, harmed=10

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 .

A causal directed acyclic graph (DAG) records assumed direct causal relationships. It can accompany structural equations, but the graph alone doesn't specify their functions or effect sizes. Arrows aren't correlations inferred from the table. A DAG is acyclic; a feedback system may need variables indexed by time.

d-separation describes path blocking, using three elementary junctions.[3] An open path permits dependence rather than guaranteeing a nonzero association: effects can cancel. Under the causal Markov assumption, d-separation implies conditional independence. Claims in the reverse direction need a faithfulness assumption.

Three isolated DAG junctions. The chain A to M to Y and fork A from C to Y are open without conditioning; conditioning on M or C respectively blocks that path. The collider A to K from Y is blocked unless K or a descendant is conditioned on. No direct A-to-Y arrow is shown.
These rules apply to individual paths. To establish d-separation, check every path between the variables. Conditioning on a collider or its descendant can open a selection path.

1. The chain (mediator): A→M→YA \to M \to Y

In this assumed graph, treatment AA changes diagnostic summary MM, which influences resolution YY.

  • Without conditioning, this directed path is open.
  • Conditioning on MM blocks this path. It removes the mediated route from a total-effect comparison.

That operation doesn't automatically identify a direct causal effect. Other paths and mediator-outcome confounding matter, and conditioning on MM differs from intervening to fix it. The isolated chain shown has no direct A→YA \to Y arrow.

2. The fork (confounder): A←C→YA \leftarrow C \to Y

A pre-treatment common cause CC influences assignment and resolution. Risk can play that role in the triage system.

  • Without conditioning, the path A←C→YA \leftarrow C \to Y is open.
  • Conditioning on CC blocks this backdoor path.

Risk adjustment supports causal identification only if it blocks the relevant noncausal paths and the other identification assumptions hold. The Simpson reversal alone doesn't establish that risk is the only confounder.

3. The collider (inverted fork): A→K←YA \to K \leftarrow Y

Two arrows meet at their common effect KK. A variable is a collider on a particular path; the two causes needn't be independent through every other path.

  • Without conditioning on KK or its descendants, this path is blocked.
  • Conditioning on KK, or a descendant of KK, opens it. Other non-colliders can still block a longer path.

To expose the direction of bias, use a fresh fixture in which assistant assignment has no effect on resolution: each workflow resolves 50 of 100 tickets. Audit a ticket if the assistant was used or it failed, so K=A∨(1−Y)K=A \lor (1-Y). All assistant tickets enter the audit, but only control failures enter. Predict the assistant advantage in the filtered log.

filter-on-a-collider.py
# Independent A and Y in this balanced, no-effect fixture.
records = [(a, y) for a in (0, 1) for y in (0, 1) for _ in range(50)]
audited = [(a, y) for a, y in records if a == 1 or y == 0]

for label, subset in (("all tickets", records), ("audited only", audited)):
    rates = {}
    for a in (0, 1):
        outcomes = [y for assigned, y in subset if assigned == a]
        rates[a] = sum(outcomes) / len(outcomes)
    print(
        f"{label}: assistant={rates[1]:.0%}, control={rates[0]:.0%}, "
        f"difference={100 * (rates[1] - rates[0]):+.0f} pp"
    )
Audit selection invents an advantage
all tickets: assistant=50%, control=50%, difference=+0 pp
audited only: assistant=50%, control=0%, difference=+50 pp

Filtering creates a positive association between assistant use and successful resolution in this example. An arrow Y→KY \to K doesn't specify a positive effect: success suppresses the failure-triggered audit. Different audit rules can produce a different sign. This is selection bias, not evidence that the assistant helps.

The backdoor criterion

The backdoor criterion gives a sufficient graphical condition for an adjustment set ZZ:[3]

A covariate set ZZ satisfies the backdoor criterion for (A,Y)(A,Y) when no member is a descendant of AA and conditioning on ZZ blocks every path from AA to YY that starts with an arrow into AA.

For a causal model represented by that DAG, with consistency and positivity over the target support, this sufficient criterion licenses adjustment:

P(Y=y∣do⁡(A=a))=∑zP(Y=y∣A=a,Z=z)P(Z=z)P(Y = y \mid \operatorname{do}(A = a)) = \sum_{z} P(Y = y \mid A = a, Z = z) P(Z = z)

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 0.7(0.90)+0.3(0.50)=0.780.7(0.90)+0.3(0.50)=0.78; the control rate becomes 0.7(0.80)+0.3(0.40)=0.680.7(0.80)+0.3(0.40)=0.68. Their standardized difference is:

Δ^standardized=0.7(0.90−0.80)+0.3(0.50−0.40)=0.10\widehat{\Delta}_{\text{standardized}} = 0.7(0.90-0.80) + 0.3(0.50-0.40) = 0.10

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.[3]

The next snippet continues the earlier session and applies one shared population distribution to both arms:

standardize-ticket-treatment-effects.py
target_weights = {"low": 0.7, "high": 0.3}
standardized = {"assistant": 0.0, "control": 0.0}

for risk, population_weight in target_weights.items():
    for workflow in standardized:
        successes, total = groups[risk][workflow]
        standardized[workflow] += population_weight * successes / total

adjusted_effect = standardized["assistant"] - standardized["control"]
for workflow, rate in standardized.items():
    print(f"{workflow} at the target mix: {rate:.0%}")
print(f"standardized difference: {100 * adjusted_effect:+.0f} percentage points")
assert abs(adjusted_effect - 0.10) < 1e-12
Output
assistant at the target mix: 78%
control at the target mix: 68%
standardized difference: +10 percentage points

The arithmetic answers a target-population comparison. It estimates an identified causal effect only under explicit assumptions:

Identification assumptionMeaning in the ticket example
Conditional exchangeabilityWithin measured risk groups, no unrecorded common cause still changes both routing and resolution: (Y(1),Y(0))⊥A∣R(Y(1), Y(0)) \perp A \mid R.
Positivity or overlapBoth assistant and control assignments occur for every target risk stratum: 0<P(A=1∣R=r)<10 < P(A=1 \mid R=r) < 1.
ConsistencyTreatment policies are well-defined and the outcome matches the policy received: Y=AY(1)+(1−A)Y(0)Y = A Y(1) + (1-A) Y(0).
No interferenceOne ticket's assignment doesn't alter another's outcome, such as through queue spillover. Together with no consequential hidden treatment versions, this is the usual SUTVA requirement.
Transfer to targetThe 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.

How uncertain is the +10-point standardized difference? For independent tickets, fixed target weights, and a conditional binomial model within each workflow/risk cell, a rough plug-in calculation adds the stratum variances with squared weights:

SE^(Δ^)=∑rwr2[p^1r(1−p^1r)n1r+p^0r(1−p^0r)n0r]\widehat{SE}(\widehat\Delta)= \sqrt{\sum_r w_r^2 \left[ \frac{\hat p_{1r}(1-\hat p_{1r})}{n_{1r}}+ \frac{\hat p_{0r}(1-\hat p_{0r})}{n_{0r}} \right]}
uncertainty-after-standardization.py
from math import sqrt

variance_of_difference = 0.0
for risk, weight in target_weights.items():
    for workflow in ("assistant", "control"):
        successes, total = groups[risk][workflow]
        rate = successes / total
        variance_of_difference += weight**2 * rate * (1 - rate) / total

se = sqrt(variance_of_difference)
low, high = adjusted_effect - 1.96 * se, adjusted_effect + 1.96 * se
print(f"plug-in standard error: {100 * se:.2f} pp")
print(f"rough normal interval: [{100 * low:.1f}, {100 * high:.1f}] pp")
Adjustment doesn't remove sampling noise
plug-in standard error: 8.77 pp
rough normal interval: [-7.2, 27.2] pp

The point estimate looks promising, but these few tickets leave broad uncertainty. This normal approximation can have poor coverage with sparse cells such as 9/10; select an interval method suitable for the sampling design before making a claim. Clustering or estimated deployment weights need additional uncertainty treatment. More data reduce sampling variance under a sound design; they don't remove unmeasured confounding.

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 is the conditional assignment probability e(r)=P(A=1∣R=r)e(r) = P(A=1 \mid R=r), not the probability of successful resolution.[3]

Estimate it from the log: low-risk tickets went to the assistant 10 times out of 100, so e^(low)=0.10\hat{e}(\text{low}) = 0.10. High-risk tickets went 90 times out of 100, so e^(high)=0.90\hat{e}(\text{high}) = 0.90. An assistant ticket gets weight 1/e^(r)1/\hat{e}(r); a control ticket gets 1/(1−e^(r))1/(1-\hat{e}(r)).

IPTW balances measured risk in this log. Rare assignments have weight 10 and common assignments weight 10/9. Each arm has total weight 200 split equally by risk, but still contains only 100 actual tickets. Weighted assistant and control rates are 70% and 60%. Hidden confounding is not removed by balancing risk.
These empirical stratum weights balance recorded risk exactly. They don't change the actual routing mechanism, balance hidden variables, or create new independent tickets.

The ten low-risk assistant tickets each get weight 10, for total weight 100. The ninety high-risk assistant tickets each get weight 1/0.91/0.9, 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:

weight-routing-propensity.py
import numpy as np

totals = np.array([[10, 90], [90, 10]], dtype=float)
successes = np.array([[9, 72], [45, 4]], dtype=float)

def assignment_weights(counts):
    if np.any(counts <= 0):
        raise ValueError("No within-risk comparison: a workflow has zero tickets")
    propensity = counts[:, 0] / counts.sum(axis=1)
    return propensity, np.column_stack([1 / propensity, 1 / (1 - propensity)])

propensity, weights = assignment_weights(totals)
weighted_totals = totals * weights
rates = (successes * weights).sum(axis=0) / weighted_totals.sum(axis=0)
print("assignment propensity (low, high):", propensity)
print("weighted counts, rows=low/high; columns=assistant/control:")
print(np.round(weighted_totals, 1))
print(f"50/50 mix: assistant={rates[0]:.0%}, control={rates[1]:.0%}")
print(f"weighted difference: {100 * (rates[0] - rates[1]):+.0f} percentage points")
effective_n = weighted_totals.sum(axis=0)**2 / (totals * weights**2).sum(axis=0)
print("weight-only ESS diagnostic (assistant, control):", np.round(effective_n, 1))

try:
    assignment_weights(np.array([[10, 90], [100, 0]]))
except ValueError as error:
    print("unsupported comparison:", error)
Output
assignment propensity (low, high): [0.1 0.9]
weighted counts, rows=low/high; columns=assistant/control:
[[100. 100.]
 [100. 100.]]
50/50 mix: assistant=70%, control=60%
weighted difference: +10 percentage points
weight-only ESS diagnostic (assistant, control): [36. 36.]
unsupported comparison: No within-risk comparison: a workflow has zero tickets

Positivity violations and extreme weights

If the routing policy always sends high-risk tickets to the assistant, e(high)=1e(\text{high})=1. That is a structural positivity violation for a target effect comparing both workflows on high-risk tickets. Weighting can't recover the missing control rate. Restrict the target to supported cases, collect exploration data, or disclose the extra assumptions used to extrapolate.

A zero cell in a finite sample doesn't by itself prove that the underlying assignment probability is zero. It can occur by chance even with a supported policy. The snippet rejects direct within-stratum arithmetic on either kind of empty cell rather than silently extrapolating.[3]

Near-zero probabilities can still make estimates unstable: an assignment probability of 0.005 gives weight 200. Our pseudo-counts total 200 per arm, but each arm has only 100 tickets. A common weight-only diagnostic is neff=(∑iwi)2/∑iwi2n_{eff}=(\sum_i w_i)^2/\sum_i w_i^2; here it's just 36 per arm. This diagnoses weight concentration, not an exact outcome-specific variance or a confidence interval.

Three choices are often confused:[3]

  • Hájek normalization estimates an arm mean as ∑i:Ai=awiYi/∑i:Ai=awi\sum_{i:A_i=a}w_iY_i/\sum_{i:A_i=a}w_i, as the code does. It's a ratio estimator, generally not exactly unbiased in finite samples.
  • Stabilized weights use a numerator such as P(A=a)/P(A=a∣R)P(A=a)/P(A=a\mid R). Here the numerator is 0.5 in each arm. That constant cancels in a normalized arm mean, leaving the same estimate and weight concentration.
  • Clipping weights caps large values and can trade variance for bias. The bias needn't be small, and the cap changes the effective weighted comparison. Predeclare it and examine sensitivity.

Check balance and support after fitting a propensity model. A misspecified model needn't balance measured covariates in a finite sample. Neither normalization nor stabilization repairs unmeasured confounding or unsupported actions.

Instrumental variables when unmeasured confounders lurk

Suppose hidden urgency UU leaves an open common-cause path A←U→YA\leftarrow U\to Y even after risk adjustment. Conditional exchangeability given risk then fails under the assumed graph. Risk-only standardization and IPTW don't remove this residual confounding. An unrecorded variable doesn't necessarily cause residual confounding if the recorded adjustment set already blocks its relevant paths.

An instrumental variable (IV) offers another identification route, with demanding assumptions. For our binary randomized encouragement ZZ, first justify:[3]

  1. Relevance: ZZ changes the probability of receiving the assistant. The first-stage difference is nonzero.
  2. Exclusion: Changing ZZ has no effect on a ticket's resolution except through the workflow it receives. Removing a direct Z→YZ\to Y arrow is insufficient if another bypassing path remains.
  3. Independence: Randomized ZZ is independent of potential workflow uptake and outcomes. Independence from one named confounder UU alone wouldn't establish this.

Also define the instrument and workflows consistently and address interference. Checking the first stage doesn't prove exclusion or independence.

Diagram showing Instrument Z (Gateway canary flag), Treatment A (Assistant assignment), Unmeasured confounder U (Customer frustration), and Outcome Y (Resolution).
Instrument Z (Gateway canary flag), Treatment A (Assistant assignment), Unmeasured confounder U (Customer frustration), and Outcome Y (Resolution).

Consider an invisible randomized gateway encouragement that changes only the chance of assistant routing. Treat its validity as a design assumption to audit. A latency experiment is a poor substitute: delay can change resolution directly even if it doesn't change customer frustration. A canary flag that also changes model versions, timeouts, or staff behavior has the same exclusion problem.

Wald estimator and local average treatment effects

With binary instrument and workflow uptake, the Wald ratio divides the outcome difference across instrument groups by their workflow-uptake difference. Hats emphasize that sample means produce an estimate:

τ^Wald=YˉZ=1−YˉZ=0AˉZ=1−AˉZ=0\widehat{\tau}_{\text{Wald}} = \frac{\bar Y_{Z=1}-\bar Y_{Z=0}}{\bar A_{Z=1}-\bar A_{Z=0}}

Suppose setting canary flag Z=1Z=1 raises assistant usage from 30% to 70% (ΔA=0.40\Delta A = 0.40), while overall ticket resolution rises from 68% to 72% (ΔY=0.04\Delta Y = 0.04). The Wald estimate is:

τ^Wald=0.72−0.680.70−0.30=0.040.40=0.10(+10 percentage points)\widehat{\tau}_{\text{Wald}} = \frac{0.72 - 0.68}{0.70 - 0.30} = \frac{0.04}{0.40} = 0.10 \quad (+10\text{ percentage points})

Under the instrument assumptions and monotonicity, A(1)≥A(0)A(1)\ge A(0) for every ticket, the population ratio identifies the local average treatment effect (LATE) on compliers: tickets with A(1)=1,A(0)=0A(1)=1,A(0)=0.[3] No defiers means encouragement never makes a ticket receive control when it would otherwise receive the assistant. It doesn't mean every ticket follows the encouragement.

The example allows assistant use at 30% even under Z=0Z=0, so it isn't one-sided noncompliance. Under monotonicity, the uptake rates imply 30% always-takers, 40% compliers, and 30% never-takers. LATE can apply with this two-sided noncompliance; it needn't equal the whole-population ATE. Another instrument may move a different subset.

A small first-stage denominator makes the ratio unstable and magnifies exclusion violations. Estimate uncertainty for the ratio with a method appropriate to instrument strength. The displayed numbers are a hypothetical calculation, not evidence that our gateway flag is valid.

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)

For a one-step routing policy π(a∣s)\pi(a\mid s), offline policy evaluation estimates its value from contexts, actions, and rewards logged under π0(a∣s)\pi_0(a\mid s).[4] Here ss is pre-routing risk and aa chooses between the two fixed workflows. The target policy can choose the assistant more often without changing what “assistant” means.

This requires supported target actions, observed context sufficient for unconfounded action selection, consistent action/reward definitions, and stable relevant mechanisms. A new prompt that changes the assistant's behavior is a new action version; logs of the old workflow don't automatically identify its value. The inverse-propensity estimator (IPS) weights logged rewards by the policy ratio:

V^IPS(π)=1N∑i=1Nπ(ai∣si)π0(ai∣si)ri\hat{V}_{\text{IPS}}(\pi) = \frac{1}{N} \sum_{i=1}^N \frac{\pi(a_i \mid s_i)}{\pi_0(a_i \mid s_i)} r_i

For a multi-step trajectory, an importance ratio can involve ∏t=1Tπ(at∣ht)/π0(at∣ht)\prod_{t=1}^T\pi(a_t\mid h_t)/\pi_0(a_t\mid h_t), where hth_t records the relevant history. Under suitable assumptions, trajectory-wise or per-decision weighting accounts for changed action and state distributions. Variance can grow exponentially with horizon, but isn't guaranteed to do so; identical policies give ratio 1.[5]

Return to one-step routing. A doubly robust (DR) estimator combines a reward model Q^(s,a)\hat Q(s,a) with a weighted residual. Define Q^(s,π)=∑aπ(a∣s)Q^(s,a)\hat Q(s,\pi)=\sum_a\pi(a\mid s)\hat Q(s,a):

V^DR(π)=1N∑i=1N[Q^(si,π)+π(ai∣si)e^(ai∣si)(ri−Q^(si,ai))]\hat{V}_{\text{DR}}(\pi) = \frac{1}{N} \sum_{i=1}^N \left[ \hat{Q}(s_i, \pi) + \frac{\pi(a_i \mid s_i)}{\hat e(a_i \mid s_i)} \left( r_i - \hat{Q}(s_i, a_i) \right) \right]

Why two models? Let e(a∣s)e(a\mid s) be the true logging propensity, e^\hat e its estimate, and QQ the true conditional reward mean. For fixed nuisance models evaluated on independent data, the conditional bias is:

∑aπ(a∣s)(1−e(a∣s)e^(a∣s))(Q^(s,a)−Q(s,a))\sum_a \pi(a\mid s) \left(1-\frac{e(a\mid s)}{\hat e(a\mid s)}\right) \left(\hat Q(s,a)-Q(s,a)\right)

It vanishes if either model is exactly correct on the supported target actions.[4] With estimated models, correct specification and consistent fitting can give consistency under suitable regularity conditions; they're not automatic exact finite-sample unbiasedness. Split data or cross-fit nuisance models, evaluate a learned target policy on held-out data, and use suitable uncertainty. Neither model substitutes for causal identification or overlap.

The formula above is for a one-step contextual bandit. A multi-step agent needs a sequential estimator with future value and per-step corrections; applying this immediate-reward formula to every tool call doesn't evaluate its trajectory return.[5]

The next snippet reconstructs the 200 observed tickets. Its “matched” models reproduce the empirical stratum rates and assignment fractions. Predict whether the residual correction repairs the deliberately shifted reward model when the propensity also misses the risk-dependent routing. Exact agreement in this grouped fixture verifies algebra, not repeated-sample unbiasedness or causal identification.

offline-policy-evaluation.py
import numpy as np

# Reconstruct individual records from the 200 tickets
# Stratum: risk (0: low, 1: high), action A (1: assistant, 0: control), outcome Y
records = []
records.extend([(0, 1, 1)] * 9 + [(0, 1, 0)] * 1)    # Low assistant: 9/10
records.extend([(0, 0, 1)] * 72 + [(0, 0, 0)] * 18)  # Low control: 72/90
records.extend([(1, 1, 1)] * 45 + [(1, 1, 0)] * 45)  # High assistant: 45/90
records.extend([(1, 0, 1)] * 4 + [(1, 0, 0)] * 6)    # High control: 4/10

data = np.array(records)
risk, a_obs, y_obs = data[:, 0], data[:, 1], data[:, 2]

# Empirical logging fractions for assistant assignment.
pi_0 = np.where(risk == 0, 0.10, 0.90)

# Evaluate candidate policy pi_target: always route to assistant (action 1)
pi_target = 1.0
w = np.where(a_obs == 1, pi_target / pi_0, 0.0)

# Reward model deliberately shifted from the observed stratum rates.
matched_q_1 = np.where(risk == 0, 0.90, 0.50)
q_hat_1 = matched_q_1 + 0.05
q_hat_obs = np.where(a_obs == 1, q_hat_1, 0.50)

v_ips = np.mean(w * y_obs)
v_direct = np.mean(q_hat_1)
v_dr = np.mean(q_hat_1 + w * (y_obs - q_hat_obs))

print(f"Direct model (+5 pp): {v_direct:.1%}")
print(f"IPS (matched routing): {v_ips:.1%}")
print(f"DR (matched routing, shifted reward): {v_dr:.1%}")

wrong_weights = np.where(a_obs == 1, 1 / 0.40, 0.0)
matched_q_obs = np.where(a_obs == 1, matched_q_1, 0.50)
dr_matched_reward = np.mean(matched_q_1 + wrong_weights * (y_obs - matched_q_obs))
dr_both_wrong = np.mean(q_hat_1 + wrong_weights * (y_obs - q_hat_obs))
print(f"DR (wrong routing, matched reward): {dr_matched_reward:.1%}")
print(f"DR (both models wrong): {dr_both_wrong:.1%}")
assert abs(v_dr - 0.70) < 1e-12
Output
Direct model (+5 pp): 75.0%
IPS (matched routing): 70.0%
DR (matched routing, shifted reward): 70.0%
DR (wrong routing, matched reward): 70.0%
DR (both models wrong): 68.8%

The matched-model results target the log's 50/50 risk mix: 70% assistant resolution. They aren't the 78% value at the separate deployment mix. The two wrong models leave a weighted-residual error here; double robustness doesn't protect against their simultaneous failure. This is still an adjusted association unless the identification assumptions hold.

When possible, log the actual action probability and policy version at each decision rather than infer them from aggregate counts. Include eligibility, reward windows, missing outcomes, and relevant context. If a hidden urgency score drives both action and reward, a propensity fitted only to displayed risk doesn't restore exchangeability.

Recommendation and search debiasing

Search clicks reflect exposure as well as relevance. Under the simple position-based model, an item is clicked only if it's examined and relevant. Then P(click)=P(examine∣rank) P(relevant)P(click)=P(examine\mid rank)\,P(relevant) under the model's independence assumptions. Joachims et al. use observation propensities to correct a ranking objective.[6]

Position is part of the presentation mechanism; calling it a confounder isn't itself a justification for weighting. Estimate and validate the examination or click model, ensure positive observation probabilities, and account for violations such as trust or attractiveness effects. Correcting position bias under that model doesn't automatically recover all “true preferences” or remove popularity and selection bias.

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?

A trace's order isn't a causal DAG by itself. State, tool results, random choices, and environment changes determine downstream behavior. In a controlled replay, snapshot the environment before call 2, replace its return value, and rerun later steps with a declared randomness policy. Recompute downstream state that depends on the replaced result; freezing it would block the mechanism being tested.

Paired replays can measure the effect of that replacement inside the replay environment. Match or repeat random draws, prevent outside state drift, and specify the alternative result. Intervening on a tool's return value differs from changing its implementation, including its side effects. One successful replay doesn't prove the historical root cause or provide uniquely identified reinforcement-learning credit. Those conclusions need a justified causal model and a clear estimand.[2]

Decide what the comparison supports

Evaluation methodIdentification requirementsCore advantagesPrimary failure modes
Standardization (g-formula)Exchangeability, positivity, consistency, suitable conditional-outcome estimationDirect estimation across target population distributionsUnmeasured confounding; outcome estimation errors
Inverse Weighting (IPTW)Exchangeability, positivity, consistency, justified propensity estimationTargets an adjusted comparison without fitting outcome meansHidden confounding, concentrated weights, unsupported actions
Instrumental Variables (IV)Relevance, independence, exclusion, consistency, non-interference; monotonicity for this LATECan identify a complier effect despite treatment-outcome confoundingInvalid or weak instrument; complier effect may not transfer to everyone
Doubly Robust (one-step DR / OPE)Identification and overlap; consistent fitting of at least one nuisance model with suitable regularityTwo chances to control model error; can improve precisionBoth models wrong, weak overlap, invalid causal assumptions, evaluation leakage
Randomized A/B experimentKnown randomization, well-defined policies, suitable outcome measurement and interference modelAssignment needn't depend on baseline potential outcomesQueue interference, missing outcomes, limited external validity, no individual pairing

A marginally randomized design makes assignment independent of potential outcomes; a conditionally randomized design does so conditional on its randomization strata. Baseline arm balance holds in expectation, not exactly in every realized sample. Neither design automatically repairs missing outcomes, interference, or measurement errors. For tickets sharing an agent pool, assigning one to the assistant can free staff time for another. Define whether you want an individual routing effect, a queue-level policy effect, or another estimand, then design the comparison accordingly.

For this release report, the raw -22-point difference compared unequal case mixes. The +10-point standardized difference addresses the visible imbalance, but needs an uncertainty calculation and credible identification and transfer assumptions before supporting deployment. It doesn't prove every ticket benefited. A well-run randomized evaluation can address routing confounding for its defined population; validate outcomes and queue effects too.

Before recommending a rollout, write a short evidence receipt: target population, two workflow definitions, resolution horizon, routing probabilities, adjustment set and causal graph, overlap diagnostics, effect with uncertainty, missing-outcome handling, and queue/interference assumptions. Mark each claim as observed, identified under assumptions, or unresolved. That's the useful next artifact from this 200-ticket log.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.Why did the AI assistant show a higher observed success rate in both risk strata while appearing worse in the naive aggregate?
2.How does Pearl's Ladder of Causation distinguish between observational conditioning P(Y|X) and intervention P(Y|do(X))?
3.What does the potential outcome Y(0) represent for a ticket that was actually assigned to the AI assistant (A=1)?
4.Two hypothetical populations of 100 tickets each have assistant success 78% and control success 68%. Population A has 10 helped and 0 harmed; B has 20 helped and 10 harmed, where harm means Y(1)=0 and Y(0)=1. What can the shared +10-point ATE establish?
5.In a no-effect fixture, each workflow resolves 50 of 100 tickets. A ticket is audited if the assistant was used OR the ticket failed. What does restricting evaluation to audited tickets show?
6.Assume the graph A -> M -> Y, where assistant assignment A changes a diagnostic summary M that influences resolution Y. Why is adjusting for M inappropriate for this total-effect comparison?
7.What fundamental causal identification failure occurs if a triage policy strictly routes 100% of high-risk tickets to the assistant?
8.The assistant log has 10 low-risk and 90 high-risk tickets, with estimated assignment propensities 0.10 and 0.90. What does inverse-propensity weighting give for this arm?
9.A randomized gateway flag raises assistant uptake from 30% to 70%, but also adds a delay that affects resolution independently of workflow choice. What fails if the flag is proposed as an instrument?
10.A one-step policy evaluation has overlap and valid identification assumptions. Nuisance models are fit separately from evaluation data. What protection does double robustness provide under suitable regularity conditions?

10 questions remaining.

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.