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

CATE and uplift

6.23 is a mean, and at Kestrelmark it is the mean of four very different numbers. The re-engagement email is worth +29.09 to a customer who lapsed 30 to 90 days ago, +2.10 to one still active, +1.04 to one dormant a year, and -8.87 to one who has already cancelled. One observed field, recency, explains the individual effect with an R-squared of 0.986. Tonight is about how much of that a model can recover from the table, exactly where the recovery stops, and which three recipes to run to find out.

🔴 Hardest Data scientists and analysts 45 min
0-3 · Welcome 3-22 · The average is a summary of a distribution 22-40 · Build-along: three meta-learners 40-45 · Q&A
Part 0

From one number to a function

Everything up to session 6 produced a single figure for the whole list. That figure is what a policy needs when the policy is "send it or do not". The moment the policy becomes "send it to some people", the object of interest stops being a number and becomes a function of the customer, and almost every practical property changes with it: what can be validated, what can be plotted, and what a wrong answer costs.

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

★ What you walk out with tonight. Three working meta-learners on the Kestrelmark table, each with its documented weakness demonstrated rather than described. The one distinction that decides how much any of them can deliver: the segment effect here is almost fully recoverable, at an R-squared of 0.986 from a single field, and the individual effect is not recoverable at all, because inside every segment there is still a 3.0 coin flip no feature predicts. And the reason a covariate can belong in a CATE model without belonging in the adjustment set.

LiveWhy the average stopped being enough3 min▶

A budget that covers the whole list makes heterogeneity a curiosity. A budget that covers a fifth of it makes heterogeneity the entire problem, because the question is no longer whether the campaign works but which fifth to spend it on. Those two questions can have opposite answers: a campaign with a healthy average can be worth running on half the list and worth withholding from the other half, and the average will never say so.

Kestrelmark is that case, measurably. The file carries a recency segment, and the email is worth a different amount in each of its four values: +2.10 to a customer who is still active, +29.09 to one who lapsed 30 to 90 days ago, +1.04 to one dormant a year, and -8.87 to one who has already cancelled once. Those four average to the 6.23 the bench recovers, and the 6.23 describes none of them.

There is a second layer underneath, and it is the one that sets the ceiling on every model tonight. Inside each of those four groups the effect still takes one of two values, 3.0 apart, according to whether the email is what tipped that person into opening. Nothing observed predicts which. So the honest shape of the answer is: the group is knowable, the person is not.

Real world

The version that reaches a planning meeting is a proposal to cut a campaign whose average lift has fallen. Half the time the average fell because a large low-response segment was added to the list, and the original segment is performing exactly as it always did. Nothing in an average distinguishes those two stories.

Self-studyThe vocabulary, kept straight3 min▶
  • ATE - the average treatment effect over the whole population. 6.23 here.
  • CATE - the average effect among customers sharing a given set of covariate values. A function of those covariates. At Kestrelmark, conditioning on recency alone gives +2.10, +29.09, +1.04 and -8.87.
  • Individual effect - one person's own difference between two worlds. Session 6, and it needs the structural equations.
  • Effect modifier - a variable that changes the size of the effect. It need not cause the treatment, and if it does not, it is not a confounder. recency is the one here.
  • Uplift - the industry word for a predicted CATE used for targeting. Same object, different room.
  • Meta-learner - a recipe that turns any ordinary regression algorithm into a CATE estimator by arranging what it is fitted on.
  • Base learner - the ordinary algorithm doing the fitting inside a meta-learner. Gradient boosting, ridge, a forest: the choice matters and it is a separate literature.

The distinction that causes the most trouble in practice is the second against the third. A CATE describes a group defined by covariates and can be estimated from data. An individual effect describes a person and cannot. A targeting system that quietly slides from one to the other is making rung-3 claims on rung-2 evidence.

Part 1 · covers what a CATE is, what it recovers here, and where it stops

Conditional average treatment effects

9 min live

The definition is short: the average treatment effect conditional on covariates. That makes it a function rather than a number, and the average effect of 6.23 is its mean over the population. On this data the function is far from flat, and one field carries almost all of it.

The segment is recoverable: recency alone explains the individual effect, R-squared 0.986 gains, worth paying for barely moves harmed by the email Sure thing still active +2.10 segment CATE 22.4% of the file inside the segment 1.0 4.0 a 3.0 coin flip apart Persuadable lapsed 30-90 days +29.09 segment CATE 20.4% of the file inside the segment 28.0 31.0 a 3.0 coin flip apart Lost cause dormant 12 months +1.04 segment CATE 49.3% of the file inside the segment 0.0 3.0 a 3.0 coin flip apart Sleeping dog cancelled once -8.87 segment CATE 7.9% of the file inside the segment -10.0 -7.0 a 3.0 coin flip apart The four segment effects average to 6.23, and 6.23 is not one of them. A model can say which card a customer is on. It cannot say which of the two numbers on it is hers.
🔍 Click to zoom - four recoverable segments, and one irreducible coin flip inside each of them.
LiveA function, not a number3 min▶

Write the estimand out and the change is obvious: instead of one difference of means over the whole list, there is a difference of means inside every covariate cell, and the collection of those differences is the object. Averaging the function over the population returns the 6.23 from the bench, which is the first and cheapest check any CATE model should pass.

  • The covariates that define the cells are a choice. Condition on season alone and you get two numbers. Condition on recency alone and you get the four that matter here. The finer the conditioning, the closer to an individual and the noisier every cell.
  • The rule is not "only what you adjusted for". It is "nothing that was forbidden". Colliders, mediators and anything downstream of the treatment stay out, for exactly the reasons session 2 gave. A pre-treatment variable that does not cause the treatment was never forbidden, and here it turns out to be the whole story.
  • The function can be flat, and it is worth knowing that in advance. A finding that the effect does not vary with anything you measured is a result, not a failed project. On this data it is emphatically not flat: the widest gap between two segments is 29.09 against -8.87.

Check the mean before looking at anything else. If the average of your predicted effects is far from the average effect you already trust, the per-person numbers are not going to rescue it. That check costs one line and it catches most wiring mistakes.

LiveAn effect modifier is not a confounder3 min▶

This is the point that decides what goes into the feature list, and it is the one most often got wrong in both directions. recency shifts a customer's baseline spend and it sets how much the email is worth to her. What it does not do is influence whether she was sent the email. No arrow runs from recency into the treatment, so it opens no back door, and the adjustment set defended in session 3 is unchanged: {season, tenure}.

That is the difference between the two jobs a covariate can hold. Closing a back door is about identification and the set is forced on you. Describing how the effect varies is about the estimand you chose, and the set there is a modelling decision. A variable can hold the second job without the first.

Two different jobs: season closes a back door, recency sizes the effect CONFOUNDER · season, tenure season email spend Causes both the email and spend, so it opens a back door. It must be in the adjustment set, and session 3 says why. EFFECT MODIFIER · recency recency email spend sizes this arrow No arrow into the email, so it is not a confounder at all. It sizes the effect, so the CATE model conditions on it. The back-door set is still season and tenure. Adding recency changes what the model can say, not what it may claim.
🔍 Click to zoom - one variable closes a path, the other sets a slope, and only the first is forced on you.
LiveThe target is never observed, for anybody4 min▶

This is the property that makes the whole area strange, and it is worth saying slowly. In ordinary supervised learning every training row carries the answer: the price the house sold for, the class of the image. A model is fitted to that answer and scored against held-out copies of it.

There is no such column here. A row records what the customer spent under the treatment she received, and the treatment effect is a difference between that and what she would have spent under the other one. No row contains both. The quantity being modelled has never been observed for a single customer in the table, and it never will be.

  • So there is no loss function on the target. Every method builds an indirect construction: a difference of two fitted surfaces, a transformed outcome, a pseudo-effect. What gets minimised is always a proxy.
  • And there is no honest held-out score. A validation set has the same missing column, so the usual habit of trusting the leaderboard does not transfer. Session 8 covers what can be done instead by ranking rather than by scoring.
  • Which is why model comparison here is unusually hard, and why running three learners and comparing their disagreement is not a luxury. Disagreement is one of the few signals available.
Real world

The most expensive version of this mistake is an uplift model reported with an R-squared on spend. The number is computed against observed outcomes, not against effects, so it describes how well the outcome was predicted and says nothing at all about the targeting. Models with excellent fit routinely rank customers no better than chance.

LiveHow far the recovery goes, and exactly where it stops4 min▶

Because Kestrelmark is generated from a written model, the per-person effect can be computed rather than estimated, which is what session 6 did. Doing it for all 5,000 customers gives eight values and no others. Every one of them is a segment's own direct effect, plus 3.0 when the email is the thing that tipped that person into opening.

Individual effectCustomersSegment
-10.0245sleeping dog
-7.0148sleeping dog
0.01,614lost cause
+1.0709sure thing
+3.0851lost cause
+4.0411sure thing
+28.0650persuadable
+31.0372persuadable

Regress that column on recency alone and the R-squared is 0.986. Almost all of the variation in who the email helps is one field in the warehouse, which is unusually kind and is the reason a CATE model has anything to find here at all. What is left over is the part no feature can reach.

  • The segment effect is the ceiling. A perfect model, given every observed column, converges on +2.10, +29.09, +1.04 and -8.87. That is the best any of tonight's three learners could possibly do, and it is enough for every decision session 8 makes.
  • The individual effect is out of reach, and not for want of data. Two persuadables with identical covariates get identical predictions, while one of them truly gets 28.0 and the other 31.0. What separates them is whether the email happened to be the thing that made them open it, and that is an unobserved draw rather than a missing column.
  • So the right unit of a targeting promise is a group. "Customers who lapsed 30 to 90 days ago respond at 29.09" is defensible. "This customer will respond at 29.09" is not, and the second sentence is what a ranked file with a name against every score invites people to say.

Say the ceiling out loud before you build. Ask what mechanism in the world would make the effect vary, and whether any column you hold could capture it. Here the answer was a field the business already understood. When the answer is "nothing obvious", a flat function is the likely finding and a beautiful predicted spread would be the model describing itself.

Self-studyWhat the four numbers are worth, and where that argument goes3 min▶

Estimating the effect per person is tonight. Turning it into a send list is session 8. It is still worth seeing why the estimate matters, because the gap is not marginal.

Leader session 5 puts Kestrelmark's fully loaded cost at 7.00 per send, in the same units as spend. Against that bar, only the persuadable segment clears: 29.09 against 7.00. Sure things at 2.10, lost causes at 1.04 and sleeping dogs at -8.87 all lose money, the last group twice over. A blanket send to the whole file is worth -3,848; sending only to the 20.4 percent persuadable segment is worth +22,578.

That comparison is the payoff of estimating a function instead of a number, and it is the reason to get the segments right rather than merely to get a ranking. How to choose the depth of the send, how to score one ranking against another, and how to read the curve that finds the stopping point, all belong to session 8, which consumes the per-customer predictions this session produces.

Part 2 · covers the three meta-learners and their published weaknesses

Three meta-learners and their documented weaknesses

10 min live

The three below are the standard family (Künzel and co-authors, 2019). They are recipes rather than algorithms: each one says how to arrange the data and what to fit on it, and any ordinary regression can sit inside. Each also comes with a weakness that is documented rather than folklore, and the weaknesses do not overlap, which is exactly why running all three is informative.

S-learner · one model features + T one model score at T=1, then at T=0 the difference is the CATE Documented weakness regularisation biases the effect toward zero; a treatment that is weak against the rest is dropped T-learner · two models treated model control model subtract one from the other each arm fitted on its own rows Documented weakness cannot miss a weak effect, but is still regularised and now fits two models on smaller samples X-learner · impute and weight 1 · impute the missing arm 2 · fit on the pseudo-effects 3 · weight the two by propensity built for imbalanced arms Documented weakness leans on an imputation step and a weighting model, so there are two more things to get wrong All three inherit the identification assumptions. A bad adjustment set personalises a wrong answer.
🔍 Click to zoom - one model, two models, two models plus imputation and weighting.
LiveThe S-learner, and the effect it can lose entirely3 min▶

Fit one model on the covariates with the treatment included as an extra feature, then score every row twice, once with that feature set to 1 and once with it set to 0. The difference is the predicted effect. It is the simplest recipe, it uses all the rows for one fit, and it is usually the first thing anybody writes.

  • The documented weakness is regularisation. Because the base learner is normally regularised, the estimated treatment effect is biased toward zero, and the treatment column is just one feature competing with all the others for a share of the penalty budget.
  • If the treatment is weak relative to the other features, it can be ignored entirely. A tree-based learner may never split on it; a penalised linear one may shrink its coefficient to nothing. The output is then a predicted effect of zero everywhere, which looks like a clean finding of no heterogeneity.
  • It is worst exactly where it matters. A strong treatment survives the penalty; a marginal one, which is the case where targeting could actually help, is the one that disappears.

The failure is silent in the same way a mediator is silent in session 2. Nothing errors, the outcome fit looks respectable, and the targeting recommendation is "it makes no difference who you send to".

LiveThe T-learner, and what it trades for that safety3 min▶

Fit one model on the treated rows only and another on the control rows only, then difference their predictions for every customer. The treatment can no longer be shrunk away, because it is never a feature: it is the reason there are two models.

  • It avoids missing a weak effect, which is the specific fault it exists to fix, and that is enough reason to always run it alongside an S-learner.
  • It still suffers regularisation, now in both fits at once, and the two penalties do not cancel. Their difference can wander in ways neither surface would on its own.
  • And it fits two models on smaller samples. Each learner sees only its own arm, so where one arm is thin the surface there is poorly estimated and the difference inherits that noise directly.

Look at the arm sizes before choosing, segment by segment. A campaign sent to 15 percent of the list gives the treated model a seventh of the rows, and the T-learner's predictions will be worst precisely in the covariate regions the campaign has rarely reached, which are the regions a targeting exercise is about to expand into. Sleeping dogs are 7.9 percent of Kestrelmark's file to begin with, so their cells are thin in both arms.

LiveThe X-learner, built for the imbalance that is normal outside experiments4 min▶

Three moves. First, impute the counterfactual outcome for every row using the other arm's model: for a treated customer, what the control surface says she would have spent. Second, take the difference between her observed outcome and that imputed one, which gives a pseudo-effect for every row, and regress on those. Third, combine the two resulting fits, weighting them by propensity so that each region leans on the arm that has more data there.

  • It is particularly suited to treated and control imbalance, which is the norm outside experiments. Observational campaign data is almost never balanced, and this is the recipe that was designed around that fact.
  • The propensity model here is a component, not a topic. It appears as a weight between two fits. Fitting one as an estimator in its own right, with inverse probability weighting or doubly robust estimation on top, belongs to learn-experimentation-with-phoebe and is deliberately not done here.
  • The cost is two extra moving parts. The imputation is a model, the weighting is a model, and an error in either propagates into every per-person number. More structure buys more places to be wrong.

Fitting any of these three well, choosing base learners, tuning them without a validation target, and getting honest variance out of them, is a modelling exercise with its own substantial literature; what matters here is knowing which recipe fails in which direction.

LiveAll three inherit every assumption from sessions 2 and 33 min▶

Nothing in any of these recipes revisits identification. They take an adjustment set as given and produce a function on top of it, which means the covariate set on the inside is the same object that sessions 2 and 3 spent their time defending. Put the wrong columns in and the model does not fail loudly; it personalises the failure.

  • A collider in the feature set is still a collider. Adding the loyalty score moves the average from 6.56 to 5.26, and there is no reason it improves per segment. It biases every cell and the biases need not point the same way.
  • A mediator changes the estimand at every level of granularity. Including the open flag drops the average to 5.48, close to the mean direct effect of 5.16, so every predicted number becomes a conditional direct effect and the whole surface answers the subject-line question rather than the campaign question.
  • A confounder left out is left out everywhere. The naive comparison reads 10.54 against a truth of 6.23, and that +4.31 of leakage does not distribute itself evenly; it lands in whichever segments are most affected by season and tenure, which is the shape of a targeting rule built on nothing.
  • Anything downstream of the treatment is the worst of them. Adding the support ticket takes the average to 2.59. A post-treatment column in a feature list is a targeting model conditioning on the future.
Real world

The reason this is worth insisting on is that per-person output looks far more authoritative than an average. A single lift figure invites the question "compared with what". A ranked list of 5,000 customers with a score against each name invites action, and the assumption set that produced it never travels with the file.

Self-studyWhat a negative predicted segment does and does not mean3 min▶

Every learner will produce some customers with negative predicted effects, and the two available mistakes are equal and opposite: naming a harmed segment that is only variance, and dismissing a real one as variance. Kestrelmark punishes the second. The sleeping dogs are 7.9 percent of the file at -8.87 each, and their individual effects are -10.0 or -7.0. A negative region here is a finding, and a model that smooths it away has lost the most valuable thing in the data.

  • The first check is whether the negative region survives a change of learner. If the S-, T- and X-learners nominate different customers, there is no segment, only variance. If all three land on the same covariate values, that is about as much validation as this problem allows.
  • The second check is whether a mechanism exists. Harm needs a story: an unsubscribe, an irritation, a cannibalised purchase. Here the story is in the segment definition itself, since these are customers who have already cancelled once and are being reminded of it.
  • The third is sample size in the cell. Small cells produce extreme predictions in both directions, and the most extreme predictions in any uplift model are usually its least reliable ones. The smallest true segment is also the harmed one, which is the awkward combination.

So the discipline is not scepticism about negatives, it is the same discipline as everywhere else: a named group, a stated mechanism, and a number that holds up when the recipe changes.

Build-along

Three meta-learners on the same table

★ 18 min · everyone builds

All three learners, on the back-door set plus the effect modifier, checked against the known average of 6.23 before anything else happens. Then the segment recovery against the four true values, then the two segments where the learners disagree.

Fix the feature set first, in writing, and say why each column is there. Season and tenure because they close the back doors. Recency because the effect varies with it and nothing forbids it. Loyalty, opened and ticket stay out for the same reasons they stayed out of the average, and writing that line before any code prevents the feature-selection habit from quietly reopening session 2.

Hold out 1,500 rows and do not touch them. Session 8 scores a ranking on those rows and every learner tonight must be blind to them, or the curve next session is measuring memory.

Build the S-learner. One base learner on the covariates plus the treatment column, scored twice per row. Keep the fit ordinary; the point is not a good model, it is a comparable one.

Build the T-learner. Two fits, one per arm, differenced over the whole table. Note how many rows each arm actually has, because that number explains most of what you see later.

Build the X-learner. Impute the other arm for every row, form the pseudo-effects, fit on them, then weight the two fits by propensity. Three more lines than the T-learner and two more chances to be wrong.

Sanity check every one against 6.23 before looking at any individual number. The mean of the predicted effects should land near the average effect you already trust. A learner that fails this has a wiring problem, not a heterogeneity finding.

Score the segments, not the customers. Group the predictions by recency and compare the four means against +2.10, +29.09, +1.04 and -8.87. That is the only honest scoreboard available tonight, and it is available only because this data is generated.

Make the S-learner's documented weakness visible. Add noise columns, turn the regularisation up, and watch the S-learner's mean slide toward zero while the T-learner's holds. That drift is the published failure mode reproduced on your own screen.

Find the disagreement segment and say what you would do about it. Write it into the record with the covariate values that define it. A disagreement segment is a request for more data, not a targeting list.

★ Try it now - the three learners, on the back-door set plus the effect modifier import numpy as np, pandas as pd from sklearn.ensemble import GradientBoostingRegressor as GBR from sklearn.linear_model import LogisticRegression df = pd.read_csv("kestrelmark.csv") # 5,000 rows, one per customer TRUTH = 6.23 # the population average, known by construction # season and tenure close the back doors. recency is an EFFECT MODIFIER: it does # not cause the email, so it was never needed for identification, and the effect # varies with it, so the CATE model needs it. loyalty is a pre-treatment collider, # opened is a mediator, ticket is a post-treatment collider: none of the three # become safe by being used to describe a segment. FEATURES = ["season", "tenure", "recency"] held = df.iloc[-1500:] # session 8 scores on these. nothing below sees them. train = df.iloc[: -len(held)] X = train[FEATURES].to_numpy(float) t = train["email"].to_numpy(int) y = train["spend"].to_numpy(float) print("treated rows", t.sum(), "control rows", len(t) - t.sum()) def s_learner(X, t, y, base=GBR): """One model with the treatment as a feature. Score each row at 1 and at 0.""" m = base(random_state=0).fit(np.column_stack([X, t]), y) one, zero = np.ones(len(X)), np.zeros(len(X)) return m, m.predict(np.column_stack([X, one])) - m.predict(np.column_stack([X, zero])) def t_learner(X, t, y, base=GBR): """Separate models per arm, differenced. The treatment cannot be shrunk away.""" m1 = base(random_state=0).fit(X[t == 1], y[t == 1]) m0 = base(random_state=0).fit(X[t == 0], y[t == 0]) return (m1, m0), m1.predict(X) - m0.predict(X) def x_learner(X, t, y, base=GBR): """Impute the missing arm, fit on the pseudo-effects, weight by propensity. The propensity model is a component here, not a topic of its own.""" m1 = base(random_state=0).fit(X[t == 1], y[t == 1]) m0 = base(random_state=0).fit(X[t == 0], y[t == 0]) d1 = y[t == 1] - m0.predict(X[t == 1]) # treated: observed minus imputed control d0 = m1.predict(X[t == 0]) - y[t == 0] # control: imputed treated minus observed g1 = base(random_state=0).fit(X[t == 1], d1) g0 = base(random_state=0).fit(X[t == 0], d0) ps = LogisticRegression(max_iter=1000).fit(X, t).predict_proba(X)[:, 1] return (g1, g0, ps), ps * g0.predict(X) + (1.0 - ps) * g1.predict(X) _, s_hat = s_learner(X, t, y) _, t_hat = t_learner(X, t, y) xlearner, x_hat = x_learner(X, t, y) cate = {"S": s_hat, "T": t_hat, "X": x_hat} # the first check, before any per-person number is looked at for name, c in cate.items(): print(f"{name}-learner mean {c.mean():6.2f} against a truth of {TRUTH}")

The mean is a wiring check, not a result. Landing near 6.23 says the plumbing is right and says nothing about whether the per-customer spread is real. Landing far from it says stop, because no amount of segment analysis repairs a learner that cannot recover the number you already know.

★ Try it now - the only honest scoreboard: the four segments # the four true segment effects, from the model the data was generated by CATE_TRUE = {0: 2.10, 1: 29.09, 2: 1.04, 3: -8.87} NAMES = {0: "sure thing", 1: "persuadable", 2: "lost cause", 3: "sleeping dog"} seg = train["recency"].to_numpy(int) for r in [0, 1, 2, 3]: row = " ".join(f"{k} {cate[k][seg == r].mean():7.2f}" for k in ["S", "T", "X"]) print(f"recency {r} {NAMES[r]:<13} true {CATE_TRUE[r]:7.2f} {row}") # and the ceiling on all of it. the per-person effect is computable only because # kestrelmark is generated: session 6 built it from the structural equations. tau = df["true_effect"].to_numpy(float) # eight distinct values, no others d = pd.get_dummies(df["recency"], drop_first=True).to_numpy(float) d = np.column_stack([np.ones(len(d)), d]) fit = d @ np.linalg.lstsq(d, tau, rcond=None)[0] print("R-squared of recency on the individual effect:", round(1 - ((tau - fit) ** 2).sum() / ((tau - tau.mean()) ** 2).sum(), 3)) # 0.986 # 0.986, and what is left over is not a missing column. within every segment the # effect is still two values 3.0 apart, decided by whether the email was the thing # that made that person open it. no feature in any warehouse predicts that draw.

Report the segment, promise the segment. The four means above are what a learner can be held to. The per-row numbers underneath them are the same four means with noise on top, and the moment they are exported with a customer id beside each one, somebody downstream will read them as a prediction about a person.

★ Try it now - reproduce the S-learner's regularisation weakness from sklearn.linear_model import Ridge # give the treatment 40 competitors and a penalty to compete against. this is # the documented failure: the treatment is one feature among many, and a # regularised fit is happy to shrink it toward zero. the T-learner never puts # the treatment in a penalty, so it cannot lose it the same way. rng = np.random.default_rng(0) noise = rng.normal(size=(len(X), 40)) X_big = np.column_stack([X, noise]) for alpha in [1.0, 100.0, 10000.0]: ridge = lambda random_state=0: Ridge(alpha=alpha) s = s_learner(X_big, t, y, base=ridge)[1].mean() tt = t_learner(X_big, t, y, base=ridge)[1].mean() print(f"alpha {alpha:>8} S {s:6.2f} T {tt:6.2f}") # watch the S column slide toward zero as the penalty rises while the T column # holds. a predicted effect of zero everywhere is not a finding of no # heterogeneity; here it would be the penalty eating the treatment column, on # data where the true segments run from -8.87 to +29.09.
★ Try it now - the distributions, and where the three disagree import matplotlib.pyplot as plt P = np.column_stack([cate["S"], cate["T"], cate["X"]]) fig, ax = plt.subplots(figsize=(7, 4), dpi=300) for i, name in enumerate(["S", "T", "X"]): ax.hist(P[:, i], bins=40, histtype="step", label=f"{name}-learner") ax.axvline(TRUTH, linestyle="--", label="average 6.23") for r, v in CATE_TRUE.items(): ax.axvline(v, linestyle=":", label=f"{NAMES[r]} {v}") ax.set_xlabel("predicted individual effect"); ax.legend() fig.savefig("cate-distributions.png", bbox_inches="tight") # the truth is four spikes, and the learners should show four clumps. a single # smooth hill centred near 6.23 means the model never found the segments. spread = P.max(axis=1) - P.min(axis=1) messy = spread >= np.quantile(spread, 0.90) print("where the three disagree most:", train.loc[messy, FEATURES].mean().round(2).to_dict()) # hand the held-out rows and the fitted x-learner to session 8, which turns a # per-customer number into a treat or do-not-treat decision. held.to_csv("kestrelmark_holdout.csv", index=False)

Write the segments down by their covariate values, not by customer id. A list of ids is a snapshot that ages out within a quarter and cannot be argued with. "Lapsed 30 to 90 days" is a rule, it can be checked next quarter, and somebody can disagree with it in a review, which is the only thing a written rule is for.

Homework

Before session 8

30 min
  1. List the covariates you would put in a CATE model for one intervention you own, and split the list in two: the ones that are there because they close a back door, and the ones that are there because you believe the effect varies with them. Anything that fits neither column comes out.
  2. For each variable in the second list, write the one sentence of mechanism that says why the effect would be larger or smaller there. A variable with no sentence is a variable you are hoping about.
  3. Run all three learners with the adjustment set you defended in session 3, and record each one's mean predicted effect beside the average effect you already trust.
  4. Reproduce the S-learner drift on your own data: raise the regularisation in three steps and record what happens to the S mean and the T mean.
  5. Count the rows in each arm, within each candidate segment rather than overall. The segment with the thinnest treated cell is where all three learners will be least trustworthy, and it is often the small harmed group that matters most.
  6. Name one segment your model puts below zero. Say whether you believe it, and give the mechanism or the reason you do not. Bring it to session 8, where the targeting rule gets scored against sending at random.
Check yourself

Three questions

An S-learner with a heavily regularised base learner returns a predicted effect of almost exactly zero for every customer, while the T-learner on the same covariates returns a mean near 6.23. What is the most likely explanation?

The treatment is one column among many inside an S-learner, so regularisation biases the effect toward zero. The T-learner never puts the treatment in a penalty because it is the reason there are two models, which is exactly the fault it exists to avoid. On this data a flat zero would be badly wrong: the true segments run from -8.87 to +29.09.

A learner recovers Kestrelmark's four segment effects almost exactly. Two persuadable customers with identical covariates get the same prediction, but one truly gains 28.0 and the other 31.0. What should be concluded?

Inside every segment the effect takes one of two values 3.0 apart, according to whether the email is what tipped that person into opening. No observed feature predicts that draw, so identical covariates must give identical predictions. The group is the unit a targeting promise can be made about; the person is not.

A team adds the loyalty score to the covariate set of an X-learner, reasoning that a segment description can use any column since nothing is being adjusted away. What follows?

A meta-learner takes the adjustment set as given and builds a function on top of it. Every identification assumption from sessions 2 and 3 is inherited, and a bad set does not fail loudly here; it distributes the failure across a ranked list of names. Note the contrast with recency, which is safe precisely because it causes nothing about the treatment.

Sources covered

What tonight maps to

✓ CATE as a function rather than a number The effect conditional on covariates, with the average effect of 6.23 as its mean over the population, and that mean as the first wiring check.
✓ Effect modification against confounding Recency sizes the effect and shifts baseline spend but does not cause the email, so the back-door set stays as season and tenure while the CATE model conditions on recency as well.
✓ How far recovery goes, and where it stops Recency alone explains the individual effect at an R-squared of 0.986. Inside every segment the effect is still two values 3.0 apart that nothing observed predicts.
✓ The fundamental difficulty The target is never observed for anybody, so there is no label to fit against and no honest held-out score. Every method is an indirect construction.
✓ S-, T- and X-learners with their documented weaknesses Künzel and co-authors, 2019. Regularisation toward zero and a lost weak effect; two fits on smaller samples; imputation plus propensity weighting for imbalanced arms.
✓ Inherited identification assumptions A CATE model built on a bad adjustment set personalises the bias. Adding the loyalty score moves the average from 6.56 to 5.26 and does not improve per segment.
◐ Qini curves, AUUC and who to treat Session 8. Tonight produces the per-customer numbers and the held-out rows; scoring a ranking, choosing how deep to send, and the cost arithmetic at 7.00 all happen there.
◐ Individual counterfactuals Session 6. The eight true per-person effects come from the structural model, which is why the R-squared ceiling can be measured here at all.
◐ Base learner choice, tuning and honest variance A modelling exercise with its own literature. What is fixed here is which recipe fails in which direction, not how to fit any of them well.
· Propensity scores as an estimator, IPW, doubly robust, difference-in-differences, synthetic control, DoWhy Not covered by design, in any session. Propensity appears tonight only as the weight inside the X-learner; estimating effects from observational data belongs to learn-experimentation-with-phoebe.

Session 7 cheat sheet

A function, not a numberThe effect conditional on covariates. 6.23 is its mean, and none of the four segments is 6.23.
The segment is recoverableRecency alone explains the individual effect at an R-squared of 0.986. +2.10, +29.09, +1.04, -8.87.
The person is notInside every segment the effect is two values 3.0 apart, decided by an unobserved draw. No feature reaches it.
Effect modifier, not confounderRecency sizes the effect without causing the email, so it enters the model and not the back-door set.
S-learnerOne model, treatment as a feature. Regularisation biases it toward zero and can drop a weak effect entirely.
T-learnerTwo models differenced. Cannot lose a weak effect, still regularised, two fits on smaller samples.
X-learnerImpute, fit on pseudo-effects, weight by propensity. Built for the arm imbalance that is normal outside experiments.
Assumptions are inheritedA collider in the covariate set personalises the bias: loyalty moves the average from 6.56 to 5.26.