learn-causal-inference-with-phoebe / Practitioner session 2 of 10
Learn Causal Inference with Phoebe · Practitioner session 2 of 10

Confounder, collider, mediator

The Kestrelmark graph from session 1 has six observable columns and there is no way to tell by looking at them which ones belong in the adjustment set. Season and tenure must go in. The loyalty score, the open flag and the ticket flag must stay out, and each one is out for a different reason. Device can go either way and costs nothing. Tonight is the three shapes those distinctions come from, and what the word "conditioning" actually does to each of them.

🟠 Advanced Data scientists and analysts Anyone who has added a column because it improved the fit 45 min
0-3 · Welcome 3-22 · Three shapes, three fates 22-40 · Build-along: classify every column 40-45 · Q&A
Part 0

What conditioning is, before anything is classified

Conditioning on a variable means comparing only within groups that share its value: within winter, within a tenure band, within the customers who raised a ticket. Every adjustment method does this, including plain regression, and it is a strong operation. Held-fixed variables stop transmitting association along some paths and start transmitting it along others, and which of those two happens is decided entirely by the arrow directions around the variable.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. A rule you can apply in ten seconds to any column in any table: name the shape, then act. The reason "it was measured before the treatment" does not make a variable safe, with the price tag on that mistake at Kestrelmark. And a classified column list for one real model, written into a text file beside the graph, so that the next review argues about the shapes rather than about the fit.

LiveThree shapes and nothing else3 min▶

A variable sitting between the treatment and the outcome can be arranged in exactly three ways, and there is no fourth. It can be a common cause of both, in which case arrows point out of it into each. It can be a link in the chain, with an arrow in from the treatment and out to the outcome. Or it can be a common effect, with arrows pointing into it from both sides.

That exhaustiveness is what makes the classification practical. You are not searching an open-ended space of ways a variable could misbehave; you are asking three yes-or-no questions about arrow directions on a graph you already drew. The graph does the hard part, which is why session 1 came first.

Real world

The Kestrelmark covariate list arrived in a pull request as six column names in alphabetical order. Sorted that way, device sits next to email and loyalty next to opened, and the ordering carries no information about which of them is free and which is the most expensive mistake on the list. Sorted by shape, the review takes four minutes.

Self-studyThe six Kestrelmark columns and their verdicts3 min▶
  • season - confounder. Winter sends more email and sells more. Adjust.
  • tenure - confounder. Long accounts get more email and buy more anyway. Adjust.
  • loyalty - collider, and a pre-treatment one. A common effect of two unobserved causes. Do not adjust.
  • opened - mediator. Carries part of the real effect. Adjust only if the direct effect is the question.
  • ticket - collider, post-treatment. A common effect of the email and the spend. Do not adjust.
  • device - bystander. No arrows at all. Free either way, and worth knowing that it is free.

Two adjust, three do not, one does not matter. Nothing about the column names, the data types or the correlations with spend would have produced that split.

The file carries a seventh column, recency, and it is deliberately not on that list because it is not one of the three shapes. It is an effect modifier: it sets how much the email is worth to each customer and it shifts their baseline spend, but it does not cause the email, so it opens no back door and needs no adjustment either way. The back-door set is still season and tenure and every graph on this page stands. Sessions 7 and 8 are where it earns its hour.

Part 1 · covers the three shapes and the effect of conditioning on each

Three shapes, and what conditioning does to each

11 min live

Draw the same triangle three times and change only which way the arrows point. The three pictures below are that triangle, with the treatment bottom left and the outcome bottom right in all three. The verdict under each one is the whole of the practical rule.

Confounder · adjust for it Season Email Spend A common cause of both it opens a back-door path conditioning CLOSES it Mediator · only if you must Opened Email Spend A link in the real chain it carries part of the effect conditioning REMOVES that part Collider · never adjust for it Ticket Email Spend A common effect of both left alone it carries nothing conditioning CREATES a link The same triangle three times. Only the arrow directions differ, and they decide everything.
🔍 Click to zoom - one triangle, three arrow patterns, three opposite instructions.
LiveConfounder: conditioning closes a path that was open3 min▶

The shape is X <- Z -> Y. Season causes the email to be sent and independently causes spend to rise, so emailed customers and non-emailed customers differ in more than the email. Association flows from the treatment up the incoming arrow, across the fork and down into the outcome, and that flow is not the effect of anything the campaign did.

  • Left alone, the path is open and its contribution is added to whatever the treatment really does. At Kestrelmark the two open back doors add 4.31 on a true effect of 6.23.
  • Conditioning closes it. Inside a single season the season cannot vary, so it cannot explain any of the remaining difference in spend. Same for a tenure band.
  • This is the only shape where conditioning is unambiguously the right move, and it is the shape most people have in mind when they say "control for it". The trouble starts when the phrase is applied to the other two.

The test is two questions, not one. Does it affect who gets treated, and does it also affect the outcome through some route other than the treatment? A variable that only predicts the outcome is not a confounder, and a variable that only predicts treatment is not one either.

LiveMediator: conditioning removes real effect3 min▶

The shape is X -> M -> Y. Opening the email is not a nuisance sitting between the treatment and the outcome; it is one of the two routes by which the treatment does its work. Holding it fixed compares emailed and non-emailed customers who ended up with the same open status, which subtracts the mechanism from the answer.

  • The path carries real effect, which makes this the opposite case from a confounder. Closing it is not cleaning, it is deleting.
  • What remains is the direct effect: whatever the treatment does by routes that do not run through the mediator. At Kestrelmark that is the sender name in the inbox.
  • So the verdict is conditional on the question. Total effect, leave it out. Direct effect, put it in and say so in the write-up. There is no default that is right for both.

The failure mode is silent. Nothing errors, the fit improves, and the number that comes back is a perfectly good estimate of a quantity nobody asked for.

LiveCollider: conditioning creates association out of nothing3 min▶

The shape is X -> C <- Y, and it is the one that reverses every intuition built on the first two. Formally: in a structure i -> m <- j, conditioning on m makes i and j dependent when they were independent, a result known in epidemiology as Berkson's paradox.

  • A collider path is closed to begin with. Two arrows meeting head to head do not transmit association. Nothing needs to be done to it.
  • Conditioning opens it. Restricting to a value of the common effect forces its two causes to trade off against each other inside that group.
  • The association it creates is entirely manufactured by the analyst. It was not in the population and it will not replicate under a different selection rule.
Self-studyWhy a collider manufactures a link, in Kestrelmark terms4 min▶

Take the ticket flag. Two things push a customer toward raising one: getting the email, which prompts questions, and spending a lot, which also prompts questions. Now look only at the customers who raised a ticket.

Among those customers, learning that somebody did not get the email tells you they probably spent a lot, because something had to have caused the ticket and the email was not it. Learning that somebody did get the email removes the need for the other explanation, so their spend looks ordinary. Inside the ticket group, email and spend now appear negatively related, and that appearance was created by the decision to look inside the group.

  • Neither variable changed. The population is exactly as it was; only the window has narrowed.
  • Adjustment does this by construction. Putting ticket on the right-hand side of a regression is the same operation as looking within ticket groups, done for every value at once.
  • It also happens without a regression. Any filter that removes rows on a common effect does it: analysing only customers who contacted support, only accounts that survived, only applications that were approved.
Real world

The selection version is more common than the adjustment version and much harder to see, because it happens in the WHERE clause rather than the covariate list. A cohort defined as "customers who raised a ticket in the period" is a conditioned collider whether or not anybody typed the word ticket into a model.

Part 2 · covers the two cases that survive a careful review

The two that fool people

8 min live

A post-treatment collider like the ticket flag is the costliest column here, at -3.64, and it is at least suspicious on sight: it happened after the campaign. The two cases below pass that check and still do damage, which is why they are the ones that end up in shipped models.

Loyalty left alone: the collider blocks the path by default Email u1 Loyalty u2 Spend two arrows in: path blocked here No conditioning. Email and spend share no open route through loyalty, so the set works. Conditioning on loyalty: the collider opens and a back door appears Email u1 Loyalty u2 Spend boxed = conditioned: path OPEN Now open: email <- u1 -> loyalty <- u2 -> spend. On the bench the estimate falls to 5.26. Loyalty was measured before the email. Timing is not a safety argument; shape is.
🔍 Click to zoom - the M shape, closed on top and opened underneath by a single tick box.
LiveThe pre-treatment collider, and why timing proves nothing4 min▶

Kestrelmark's loyalty score is computed from behaviour that all happened before the campaign went out. Every reviewer who has ever been burned by a leaked post-treatment feature will wave it through on that basis, and every one of them is wrong here.

The score is a common effect of two things nobody measured. Call them u1 and u2: u1 is the disposition that makes somebody both a campaign target and a certain kind of loyal, and u2 is whatever makes somebody both loyal and a spender. So u1 -> loyalty <- u2, while u1 -> email and u2 -> spend. The path email <- u1 -> loyalty <- u2 -> spend contains a collider in the middle and is therefore closed while nobody touches it.

  • Conditioning on loyalty opens that path, connecting the email to spend through two variables that are not in the warehouse and cannot be adjusted away.
  • It happens despite loyalty preceding the treatment. The shape is A <- U1 -> C <- U2 -> Y and the middle of an M has nothing to do with when C was recorded.
  • The price is measured. Adding loyalty to the correct set moves the estimate from 6.56 to 5.26, a bias of -0.97 against a true effect of 6.23. Timing says nothing about the size either: the post-treatment collider on the same graph costs -3.64, the biggest single harm of the four additions.

Replace the timing check with a shape check. "Was it measured before the treatment" is a useful screen for leakage and tells you nothing about bias. The question that matters is whether two arrows point into the variable, and that is answered on the graph.

LiveDescendants: a proxy for a collider is still a collider3 min▶

The formal statement includes it explicitly: conditioning on m or any descendant of m makes i and j dependent. Anything caused by a collider inherits most of its information, and conditioning on the child partially conditions on the parent.

  • "We used a proxy instead" is not a fix. A support-contact rate, a satisfaction survey sent to ticket raisers, a churn-risk flag built partly from ticket volume: all of them are downstream of the ticket and all of them leak the same association.
  • Nor is a coarser version. Bucketing a collider into three bands weakens the effect rather than removing it, and the residual bias is unknown without the graph.
  • Nor is a composite score. If any input to an engineered feature is a collider or a descendant of one, the feature is contaminated, and this is how the problem gets into models that never mention the offending column.

The practical consequence is that classification has to run on the lineage, not on the column name. A feature is safe when everything feeding it is safe.

LiveThe mediator is not a mistake, it is a different question3 min▶

Adding the open flag to the correct set moves the estimate from 6.56 to 5.48. That looks like the same kind of failure as the loyalty score, and at -0.75 against that column's -0.97 it is even about the same size. It is not the same thing at all.

The model that generated the data has a mean direct effect of 5.16 written into it: the part of the email's work that does not run through opening. Conditioning on the open removes the mediated part and leaves that direct effect, and 5.48 is a good estimate of 5.16. The arithmetic did what it was asked. The specification asked the wrong thing.

  • Total effect, 6.23 - what happens if the email is sent. This answers "should we run the campaign", and it is the estimand for almost every business decision.
  • Direct effect, mean 5.16 - what happens if the email is sent and nobody opens it. This answers "how much of the lift survives a bad subject line", which is a genuine question for a different team.
  • The bench cannot tell which you wanted. Nor can a reviewer, unless the estimand is written down before the covariate list.
Real world

The commonest route to this is an engagement feature added for accuracy. Open rate, session count, page views: all downstream of the treatment, all improve the fit, all quietly convert a campaign lift number into a direct-effect number that nobody labels as such.

Self-studyThe bystander, and why it matters that it is harmless3 min▶

Device has no arrows in the Kestrelmark graph. Adding it to the correct set leaves the estimate at 6.56 and the bias at +0.33, matching the back-door set to 0.01.

That result is worth as much as the harmful ones, because it removes the escape route of treating every extra covariate as risky. If all additions cost something, "keep the list short" would be sufficient advice and no graph would be needed. They do not. One of the four additions is free, three are expensive, and the four look identical in a schema browser. The classification is what separates them and there is no cheaper substitute.

  • In finite samples a bystander is not quite free - it spends a degree of freedom and can move the estimate. At 5,000 rows here it moves it by 0.01.
  • A variable that predicts the outcome and not the treatment is also generally safe and can tighten things up. The recency segment is exactly that: it moves spend, it does not move who gets the email, and so it carries no adjustment duty. Precision is a different topic from bias, and the bench measures only bias.
Build-along

Classify every column, then price the mistakes

★ 18 min · everyone builds

Kestrelmark's table is one row per customer with the six observable columns plus the outcome. Work through it in whichever of the two languages you live in; the arithmetic is the same and neither one needs a library beyond numpy.

Write the six roles down before running anything. For each column ask the three questions in order: does it cause the email, does it cause spend, is it caused by either. Two causes out means confounder, one in and one out means mediator, two causes in means collider, nothing means bystander.

Get the naive gap in SQL. One query, no model, and it should give 10.54. This is the number a dashboard reports.

Run the back-door regression with season and tenure only. The estimate moves from 10.54 to 6.56, and the bias from +4.31 to +0.33. The +0.33 left over is noise rather than bias, and the way to tell is to change the seed: across five of them the back-door bias comes out +0.33, +0.17, -0.38, +0.19, -0.01, mean +0.06, changing sign. Every contaminated set below keeps its sign in all five.

Add loyalty on its own and record it. 5.26, bias -0.97. Say out loud that this variable was measured before the treatment, because that is the sentence you are unlearning.

Untick loyalty, add opened. 5.48, bias -0.75. Write beside it "direct effect, mean 5.16 by construction" so that the record distinguishes a wrong answer from a different question.

Untick opened, add ticket. 2.59, bias -3.64. Same shape as loyalty, later in time, the most expensive of the four by a wide margin, and the one a reviewer would actually catch.

Add device. 6.56, unchanged. Sit with the fact that the four additions cost 0.97, 0.75, 3.64 and nothing, that the worst of them is the one that arrived last, and that the schema gives no hint which is which.

Write the classification into a text file beside the DAG from session 1, one line per column, role and verdict and a reason. That file is the artifact reviewers should be reading instead of the covariate list.

★ Try it now - the naive gap, and a first look at each column's shape -- 1. the naive comparison: emailed against not emailed. expect 10.54 SELECT AVG(CASE WHEN email = 1 THEN spend END) - AVG(CASE WHEN email = 0 THEN spend END) AS naive_gap FROM kestrelmark.customers; -- 2. a confounder shows up on BOTH sides: it predicts treatment AND outcome SELECT season, COUNT(*) AS customers, AVG(email) AS email_rate, -- season -> email AVG(CASE WHEN email = 0 THEN spend END) AS spend_untreated -- season -> spend FROM kestrelmark.customers GROUP BY season; -- 3. the collider, seen directly. inside the ticket group the sign flips, -- and the flip is manufactured by the WHERE clause, not by the world SELECT ticket, AVG(CASE WHEN email = 1 THEN spend END) - AVG(CASE WHEN email = 0 THEN spend END) AS gap_within_group FROM kestrelmark.customers GROUP BY ticket;

Query 3 is the whole collider lesson in six lines. Splitting by ticket is not an analysis choice that happens to be inconvenient; it is the same operation a regression performs when ticket is on the right-hand side. If the gap inside the groups disagrees with the gap overall, ask which shape you just conditioned on before deciding which number to believe.

★ Try it now - price every adjustment set with plain least squares import numpy as np, pandas as pd df = pd.read_csv("kestrelmark.csv") # 5,000 rows, one per customer TRUTH = 6.23 # known by construction, not estimated def effect(adjust): """OLS coefficient on email. No inference, no standard errors: this is arithmetic, and the seam is deliberate.""" cols = ["email"] + [c for c in adjust if c != "email"] X = np.column_stack([np.ones(len(df))] + [df[c].to_numpy(float) for c in cols]) y = df["spend"].to_numpy(float) beta, *_ = np.linalg.lstsq(X, y, rcond=None) return beta[1] # email is always the first covariate BACKDOOR = ["season", "tenure"] sets = { "nothing": [], "back-door set": BACKDOOR, "+ loyalty (collider, pre-treatment)": BACKDOOR + ["loyalty"], "+ opened (mediator)": BACKDOOR + ["opened"], "+ ticket (collider, post-treatment)": BACKDOOR + ["ticket"], "+ device (bystander)": BACKDOOR + ["device"], } for name, adj in sets.items(): est = effect(adj) print(f"{name:<40} {est:6.2f} bias {est - TRUTH:+.2f}") # nothing 10.54 bias +4.31 # back-door set 6.56 bias +0.33 # + loyalty (collider, pre-treatment) 5.26 bias -0.97 # + opened (mediator) 5.48 bias -0.75 # + ticket (collider, post-treatment) 2.59 bias -3.64 # + device (bystander) 6.56 bias +0.33
★ Try it now - the classification file, saved beside the graph # kestrelmark-roles.txt - one line per observable column # format: column | shape | verdict | reason # estimand: TOTAL effect of email on spend. lives beside kestrelmark-email.dag season | confounder | ADJUST | winter sends more mail and sells more tenure | confounder | ADJUST | old accounts get more mail and buy more loyalty | collider, pre-treat | DO NOT | common effect of u1 and u2; opens an M path opened | mediator | DO NOT | carries real effect; adjusting gives 5.16 direct ticket | collider, post-treat| DO NOT | common effect of email and spend device | bystander | EITHER | no arrows; measured at no cost recency | effect modifier | NO NEED | sets what the email is worth; does not cause it # derived features: check the lineage, not the name. any feature built from # loyalty, opened or ticket inherits the same problem. # measured on 5,000 rows against a truth of 6.23: # season, tenure -> 6.56 (+0.33) # + loyalty -> 5.26 (-0.97) # + opened -> 5.48 (-0.75) # + ticket -> 2.59 (-3.64) # + device -> 6.56 (+0.33)

Keep the roles file in version control next to the DAG. A covariate list changes in a diff that shows one added line and no reason. A roles file forces whoever adds the line to name a shape, and naming a shape is the point at which most bad additions stop.

Homework

Before session 3

30 min
  1. Take the covariate list from a model your team ships and write a roles file for it: column, shape, verdict, reason. Count how many rows you had to mark unknown.
  2. Find one variable in that list that is caused by the treatment. There is usually one and it is usually there because it improved the fit.
  3. Check the lineage of every engineered feature. If any input is a collider or a descendant of one, mark the feature contaminated and note which input did it.
  4. Look at the cohort definition, not just the model. Any filter on a variable caused by both the treatment and the outcome is a conditioned collider hiding in a WHERE clause.
  5. Write one sentence saying whether the estimand is the total effect or the direct effect, and bring it to session 3, where the question becomes whether any set at all can deliver it.
Check yourself

Three questions

A reviewer approves the loyalty score for the adjustment set because it was computed weeks before the campaign went out. Adding it moves the estimate from 6.56 to 5.26. What did the timing argument miss?

The damaging shape is two arrows pointing in, and an arrival timestamp says nothing about arrow directions. The pre-treatment collider costs 0.97 here and the post-treatment one costs 3.64, so timing predicts neither the presence of the damage nor its size.

Adjusting for whether the customer opened the email returns 5.48 against a true total effect of 6.23 and a mean direct effect of 5.16 written into the model. How should that be reported?

The mediator carries real effect, so removing it subtracts a genuine part of the answer rather than contaminating it. The number is right; the question changed, and only a written estimand catches that.

A team drops the ticket flag from the model but keeps a support-contact score built from ticket volume. What happens?

The formal statement covers m or any descendant of m. Classification runs on lineage, not on column names, and a feature is safe only when everything feeding it is safe.

Sources covered

What tonight maps to

✓ The three shapes and what conditioning does to each Confounder closed by conditioning, mediator emptied by it, collider opened by it, on the bench's own six columns.
✓ Colliders and Berkson's paradox In a structure i into m out of j, conditioning on m or any descendant of m makes i and j dependent when they were independent.
✓ M-bias and the pre-treatment collider The shape A from U1 into C from U2 into Y, measured at -0.97 against the post-treatment collider's -3.64, with the timing argument shown to be worthless.
✓ Total against direct effect 5.48 recovered against a mean direct effect of 5.16 written into the model, and why the estimand goes in writing first.
◐ The back-door criterion, stated formally Session 3. Tonight classifies variables one at a time; the criterion decides whether a whole set is sufficient.
◐ Simpson's paradox Session 5. The within-group flip in query 3 is the same machinery seen from the aggregation side.
· Propensity scores, IPW, doubly robust, difference-in-differences, synthetic control, DoWhy Not covered by design, in any session. Deciding what belongs in the adjustment set is this track; fitting an estimator with that set belongs to learn-experimentation-with-phoebe.

Session 2 cheat sheet

Three shapes, no fourthCommon cause, link in the chain, common effect. Name the shape, then act.
Confounder: adjustArrows out into both. The path is open and conditioning closes it.
Mediator: only for directCarries real effect. Conditioning removes it and returns 5.48 against a mean direct 5.16.
Collider: neverArrows in from both. Closed by default, opened by conditioning. Berkson's paradox. The ticket flag costs -3.64, the worst column here.
Pre-treatment is not safeThe loyalty M path costs -0.97 despite being measured before the email.
Descendants countA proxy for a collider does most of the same damage. Check lineage, not names.
Bystanders are freeDevice leaves 6.56 and +0.33 untouched, which is why feel cannot sort the list.
Colliders hide in WHEREA cohort filtered on a common effect is conditioned whether or not a model saw it.