Two different questions that get answered in the wrong order
Identification asks whether the quantity you want can be written as a function of things you can observe, given the graph. Estimation asks how accurately you can compute that function from a finite sample. The first is a question about assumptions and has a yes or a no. The second is a question about data and has an error bar. Running them in the wrong order produces a well-tuned answer to something that was never answerable.
★ What you walk out with tonight. The back-door criterion in its exact form, and a check you can run on a whiteboard against any graph you have drawn. A working script that enumerates every path between two nodes, marks the back-door ones, and tests a candidate set against both conditions. And the response set for the case that nobody wants: what to do when the honest answer is that this data cannot support the claim.
LiveWhy identification comes first3 min▶
An identification failure and an estimation failure look nothing alike from the inside. A small sample announces itself: wide intervals, unstable coefficients, results that move when you refit. An unidentified estimand announces nothing at all. The model converges, the fit is respectable, the number is stable across bootstrap replicates, and it is measuring the wrong thing with great precision.
That asymmetry is why the order matters. Estimation quality is visible in the output; identification is only visible in the graph. Ten minutes with a pen answers a question that no amount of diagnostics on the fitted model will ever raise.
Kestrelmark's first attempt at the campaign question spent three weeks on feature engineering and cross-validation before anybody drew the graph. The features were good, the validation was honest, and the estimand was the total effect while the covariate list contained the mediator. Three weeks of careful work on a quantity nobody had asked for.
Self-studyThe vocabulary this session runs on3 min▶
- Estimand - the quantity you want. Here, the total effect of the email on spend.
- Identified - the estimand can be written in terms of observable distributions, given the graph. A property of the graph plus the column list, not of the sample.
- Adjustment set Z - the variables held fixed. The criterion tests a whole set, not its members individually.
- Descendant of X - any node reachable from X by following arrows forwards. Includes X's children, their children, and so on.
- Blocked path - a path along which no association can flow, given what has been conditioned on. One blocked node is enough to block the whole path.
- Back-door path - a path from X to Y whose first arrow points into X. These are the ones that need blocking.
The back-door criterion
11 min liveStated formally: a set Z satisfies the back-door criterion relative to an ordered pair (X, Y) in a causal diagram if (i) no node in Z is a descendant of X, and (ii) Z blocks every path between X and Y that contains an arrow into X. Two conditions, both mechanical, both answerable from the picture alone.
LiveCondition (i): no node in Z is a descendant of X3 min▶
The first condition is a single sweep forwards from the treatment. Follow the arrows out of X, then out of everything you reach, and collect the lot. Nothing in that collection may appear in Z.
What makes this worth stating separately is how much it disposes of at once. Session 2 argued mediators out on one set of grounds and post-treatment colliders out on another; condition (i) removes both in one stroke, along with every proxy, every engineered feature built from a descendant, and every variable somebody added because it correlated beautifully with the outcome for reasons downstream of the treatment.
- At Kestrelmark the descendants of email are opened and ticket. Neither may enter Z, and neither may any feature computed from them.
- It is a purely structural test. No judgement about whether the variable is "important" or "predictive" enters into it.
- The graph does the work, so lineage matters. A feature whose definition touches a descendant is a descendant, whatever the column is called.
Run condition (i) first, always. It is a reachability query, it takes seconds, and it usually shrinks the candidate list enough that condition (ii) becomes easy. It also fails loudly, which the second condition does not.
LiveCondition (ii): Z blocks every path with an arrow into X4 min▶
The second condition is the one that decides what Z has to contain rather than what it must not. List every path between X and Y, ignoring arrow directions while you walk. Keep the ones whose first arrow points into X: those are the back-door paths, and they are the routes along which association arrives without the treatment having caused anything. Every one of them must be blocked by Z.
Together the two conditions pin Z down from both sides. Condition (i) says Z may not contain consequences of X. Condition (ii) says Z must contain enough to shut every back door. What survives is the confounding structure and nothing else, which is why the criterion is a definition of "the right set" rather than a heuristic for building one.
- Paths, not variables. A single variable can appear on several paths and block some while leaving others open. The unit of the test is the path.
- Blocking is per path, not per node. One blocked node anywhere along a path blocks the whole path.
- Front-door paths are left alone. A path whose first arrow points out of X carries the effect you are measuring. Blocking one is the mediator mistake from session 2.
LiveThe blocking rules, in the order you apply them4 min▶
Walk a path node by node, looking at each intermediate node's two adjacent arrows. There are only three arrangements, and the third behaves backwards from the other two.
- Chain,
i -> m -> j. Open by default. Blocked by conditioning on m. - Fork,
i <- m -> j. Open by default. Blocked by conditioning on m. - Collider,
i -> m <- j. Blocked by default. Opened by conditioning on m, or on any descendant of m.
Read the third line twice. Every intuition that says "conditioning tidies things up" is built on the first two arrangements, and the collider inverts it: doing nothing is what keeps the path shut, and touching it is what breaks the analysis. This is the same rule session 2 measured at -0.97 on the loyalty score, which precedes the email, and at -3.64 on the ticket flag, which the email helps cause: the downstream collider is by far the more expensive of the two.
The descendant clause is where real graphs bite. A collider does not have to be in Z to be conditioned on. If Z contains anything caused by the collider, the path opens anyway, which is why the check has to run on the graph rather than on the covariate list.
LiveThe Kestrelmark check, worked through4 min▶
Estimand: the total effect of email on spend. Candidate set: {season, tenure}.
Condition (i). Descendants of email are opened, ticket, and spend itself. Season and tenure are neither, so (i) holds.
Condition (ii). The back-door paths, meaning every path whose first arrow points into email:
email <- season -> spend. A fork at season. Blocked, because season is in Z.email <- tenure -> spend. A fork at tenure. Blocked, because tenure is in Z.email <- u1 -> loyalty <- u2 -> spend. A collider at loyalty. Blocked by default, and it stays blocked precisely because loyalty is not in Z.email -> ticket <- spend. Not a back-door path at all: the first arrow points out of email. It is also a collider, so it is blocked, and adding ticket to Z would open it.
Every back-door path is blocked and no element of Z is a descendant of email, so {season, tenure} satisfies the criterion. The bench agrees: 6.56 against a true effect of 6.23, leaving +0.33. Refit on five seeds, that leftover reads +0.33, +0.17, -0.38, +0.19 and -0.01, a mean of +0.06. It changes sign, so it is noise around zero rather than bias, and more rows shrink it; every contaminated set, by contrast, keeps the same sign on all five and no amount of data moves it.
Adding device gives 6.56 again, because device sits on no path and blocks nothing, so it neither helps nor hurts. recency is the same kind of passenger as far as this check is concerned: it causes spend but not the email, so it opens no back door and {season, tenure} is still the set. Sessions 7 and 8 are where that column earns its keep.
Notice that the third path was handled by leaving something out. There is no covariate list on which loyalty appears as an absence, which is why a list cannot be reviewed and a graph can. The decision not to include it is a decision, and it is only visible against the graph.
When no valid set exists
7 min liveKestrelmark is a friendly graph: the confounders are measured and the criterion is satisfiable. Most real graphs are not, and the honest handling of that case is a professional skill in its own right rather than a failure to be worked around.
LiveA back door running through the unmeasured3 min▶
Change one thing in the Kestrelmark graph. Suppose the disposition u1, which drives who gets the email, also drove spend directly, so that email <- u1 -> spend is a path. It is a fork, it is a back-door path, and blocking it requires conditioning on u1. There is no u1 column.
Condition (ii) now fails for every subset of the warehouse. Not for the one you tried: for all of them, because the only node that could block that path is not in the data. The effect is not identified from this data, and that conclusion is final until either the graph changes or the data does.
- More rows do not help. Sample size affects the width of an interval, not which quantity the interval surrounds.
- A better estimator does not help. Every observational estimator computes a function of the observed distribution, and the whole finding is that no such function equals the effect.
- A better fit is actively misleading. Predictive accuracy improves as the model absorbs the confounded association, so the diagnostics get healthier while the answer gets worse.
LiveThe four responses, in the order they are usually available3 min▶
- Measure the missing variable. Often the cheapest and almost never considered, because by the time the analysis starts the column list feels fixed. Identification checked at design time is a specification for instrumentation: this field, on this event, from this date.
- Find another identification strategy. A different structure can rescue an effect the back door cannot reach. The front-door criterion is the case where a fully mediating variable is measured even though the confounder is not, and session 4 is that case in full.
- Randomise. An experiment removes the incoming arrows to the treatment by construction, which is why it needs no adjustment set. Designing, powering and analysing one is the territory of
learn-experimentation-with-phoebe, and so is every observational estimator that gets reached for when randomising is impossible. - Report the association honestly. "Emailed customers spent 10.54 more, and we cannot separate the campaign from the season and the tenure" is a defensible sentence. "The campaign drove 10.54 in incremental spend" is not, and the difference between them is one clause.
The fourth response is a result, not a surrender. A team that knows which of its numbers are causal and which are descriptive makes better decisions than one that believes all of them are causal, and it is the only version of the answer that stays true when somebody checks.
LiveTen minutes on paper against a month of modelling3 min▶
The whole check needs a graph, a list of columns and a pen. Sweep forwards from the treatment for condition (i), enumerate the back-door paths for condition (ii), and see whether anything in the warehouse blocks them all. On a graph with a dozen nodes this is a coffee break.
What that buys is a decision made before the expensive part starts. Identified, with the set in hand: proceed, and the estimation work has a target worth hitting. Not identified: stop, and spend the month on instrumenting the missing variable or on designing an experiment instead of on cross-validation.
- Do it at design time, not analysis time. The check tells you what to collect, and that is only actionable before collection starts.
- Put the verdict in writing next to the graph. Identified or not, with the set and the paths it blocks, so the next person inherits the reasoning rather than the covariate list.
- Re-run it whenever an arrow changes. The verdict is a function of the graph, so a disputed arrow that gets added can flip it.
Self-studyThe honest limit: the criterion inherits your graph4 min▶
The criterion is a theorem, and like every theorem it is conditional on its premises. It says: given this graph, this set is sufficient. It says nothing about whether the graph is right, and it cannot, because no procedure operating on the data can tell you that.
So a clean pass through both conditions is worth exactly as much as the arrows it was checked against. Get one arrow wrong and the criterion will certify a set that does not close the back door, with no warning anywhere in the output.
- Missing arrows are the dangerous kind. Session 1 made the point that a missing arrow is the stronger claim, and this is where the bill arrives: an unmeasured common cause you did not draw produces a graph on which the criterion passes.
- The response is not to abandon the method. Every alternative also rests on assumptions; the graph's virtue is that its assumptions are written down and specific enough to argue with.
- The response is to price the doubt. Ask how strong an unmeasured confounder would have to be to overturn the finding, and report that number beside the estimate. Session 9 is that calculation, and it exists because of this limit.
The strongest version of a causal write-up states the estimate, the adjustment set, the graph it was checked against, and the strength of unmeasured confounding that would flip the conclusion. Four items, one paragraph, and it survives a hostile reader in a way that a coefficient and a confidence interval do not.
Check one graph, in code
★ 18 min · everyone buildsSixty lines of plain Python turn the criterion into something you can run against a graph in a text file. No library beyond numpy for the arithmetic at the end, and nothing here is an estimator.
Encode the DAG as an edge list. The same file session 1 put in version control, read into a list of pairs. One line per arrow, and it is deliberately the graph and not the data.
Write the path enumerator. Walk the graph as if it were undirected, remembering the direction of each arrow you cross, and collect every simple path from email to spend. There are only a handful on this graph and printing them all is the point.
Mark the back-door paths. A path is back-door when its first arrow points into the treatment. Everything else is a causal path and must be left alone.
Implement condition (i) as a reachability query. Collect the descendants of email by following arrows forwards, and check that Z is disjoint from that set.
Implement condition (ii) with the three blocking rules. For each intermediate node on a path, decide chain, fork or collider from its two adjacent arrows, then apply the rule, remembering that a collider is opened by Z containing it or any descendant of it.
Run it on four candidate sets: {}, {season}, {season, tenure}, {season, tenure, ticket}. Predict each verdict out loud before you press enter.
Then run the matching regressions and line the two outputs up. The set that passes lands at 6.56 against a truth of 6.23; the one that fails condition (i) lands at 2.59. The paper check predicted the arithmetic.
Point the script at your own graph and record the verdict in the repository beside it. If nothing passes, that sentence is tonight's real output.
Read the failure reasons, not just the verdicts. The two failures are diagnostic and they point at different fixes. A condition (ii) failure names the path that is still open, which tells you which column to go and find. A condition (i) failure names the variable to remove, and removing it costs nothing.
Device is the useful control on this experiment. It passes both conditions, because it is not a descendant of email and it sits on no back-door path, and it leaves the estimate where it was, to two decimal places. The criterion never claimed a valid set has to be minimal, only that it has to be sufficient and free of descendants.
Before session 4
30 min- Run the script against your own graph from session 1. Enumerate every path between your treatment and your outcome and print them; the count is usually larger than expected.
- Write down the smallest set in your data that satisfies both conditions. If there is none, write down which single unmeasured variable would rescue it.
- For that variable, write one sentence on what it would take to instrument it: which event, which field, which system, and roughly what it costs.
- Put the verdict in the repository beside the graph, in the form "identified via {..}" or "not identified from current data because of the path ..". One line, dated.
- If your graph has a back door blocked by an unmeasured node and a fully mediating variable you do measure, flag it and bring it to session 4, where the front door handles exactly that shape.
Three questions
A path from email to spend runs through the ticket flag, which is caused by both. Nobody has conditioned on anything. Is that path transmitting association, and what changes if ticket is added to the adjustment set?
A collider is the one arrangement that starts closed. Chains and forks are open until you condition; a collider is shut until you condition, and the descendant clause means a proxy opens it too. The bench prices this one at -3.64, the most expensive wrong adjustment on the graph.
A team proposes the set {season, tenure, opened} and points out that it blocks every back-door path from email to spend. Does it satisfy the back-door criterion?
The two conditions are both required. Condition (i) removes mediators and post-treatment colliders in one stroke, before any path is examined, and the resulting number here is 5.48, a clean estimate of the direct effect rather than the total.
Every back-door path in a graph runs through a variable that was never logged. What follows?
Identification is a property of the graph plus the column list, and no sample size or estimator changes which quantity is being computed. Finding this out on paper is worth a month of modelling that would have produced a precise wrong answer.