Causal inferenceChapter 06
Potential outcomes
Potential outcomes, treatment assignments, observed outcomes, and non-identification in a finite population.
Contents
This chapter defines the deterministic part of the design-based model: a finite population of fixed potential outcomes and an assignment that determines which outcome is observed for each unit. Probability enters in Chapter 7. The present definitions are already sufficient to state and prove that the average treatment effect is not identified from a single assignment and outcome vector.
The population is two vectors
There are
DefinitionFinite population
A finite population consists of
structure Population (n : ℕ) where
y1 : Fin n → ℝ
y0 : Fin n → ℝstructure … where declares a record; y1 and y0 are its fields. Each is a
function Fin n → ℝ, and Fin n is the type with exactly n elements, so a
Population 500 is literally a pair of length-500 real vectors. To build one
you supply both fields; to take one apart you write P.y1 and P.y0.
Read what is not there. There is no distribution, no error term, no
superpopulation the units were drawn from, and no random variable of any kind.
P.y1 i is a real number in the same sense that
The average treatment effect is the mean of the individual effects:
def effect (i : Fin n) : ℝ := P.y1 i - P.y0 i
noncomputable def tau : ℝ := mean P.effectwith mean the obvious thing:
noncomputable def mean (v : Fin n → ℝ) : ℝ := (∑ i, v i) / ntau is a noncomputable def because real division has no algorithm.
effect is not, because subtraction does. Neither fact affects a single proof;
Lean just insists you say so.
The identity every applied paper opens with is that the average effect is the difference of the two average outcomes.
Proposition. For every population,
By definition
No hypothesis on
theorem tau_eq_mean_sub_mean : P.tau = mean P.y1 - mean P.y0 :=
mean_sub P.y1 P.y0mean_sub P.y1 P.y0There is no
by: this is a term-mode proof, and the term is the general lemma applied to the two outcome vectors.mean_sub u v : mean (fun i => u i - v i) = mean u - mean vis the whole informal argument in one equation, and it is proved inBasic.leanby the one-linesimp onlyyou are asked to reproduce for the additive version in the exercises below:Finset.sum_sub_distribsplits the sum of differences,sub_divsplits the division. Instantiated atu := P.y1andv := P.y0it readsmean (fun i => P.y1 i - P.y0 i) = mean P.y1 - mean P.y0, and the goal’s left-hand sideP.tauunfolds — throughtau, theneffect— to precisely thatmean. Lean checks a term against the goal up to unfolding definitions, so there is nothing left to do.
Interrogating a definition
Three commands do almost all the work of reading someone else’s Lean.
#check e prints the type of e. #check @f prints it with the implicit
arguments made visible — @ switches off Lean’s habit of filling them in, and
it is usually what you want when you are auditing a statement. #print f
prints the body of f, which is where the content of a definition lives. And
example states a proposition with no name, to be checked and thrown away.
#check @Population
#check @Population.y1
#check @Population.tau
#check @Z
#check @Yobs
#print Yobs
#print Population.effect
example : Assignment 3 = Finset (Fin 3) := rfl
example (P : Population 3) (z : Assignment 3) (i : Fin 3) :
Yobs P z i = Z z i * P.y1 i + (1 - Z z i) * P.y0 i := rflrfl proves an equation that holds by unfolding definitions alone. Both
examples above are proved by rfl, and that is a meaningful check: the first
says Assignment 3 is the same type as Finset (Fin 3) rather than a copy of
it, the second says Yobs is by definition the algebraic expression and not
merely equal to it by some argument.
Nothing here is abstract. A three-unit population can be written down and its
and treating the first and third units — which Lean, indexing from zero, calls
{0, 2} — reveals
def toy : Population 3 where
y1 := ![3, 5, 4]
y0 := ![1, 2, 3]
example : toy.tau = 2 := by
norm_num [Population.tau, mean, Population.effect, toy, Fin.sum_univ_three,
Matrix.cons_val_two, Matrix.tail_cons, Matrix.head_cons]
example : Yobs toy {0, 2} = ![3, 2, 4] := by
funext i
fin_cases i <;> simp [Yobs_eq_ite, toy]![3, 5, 4] is Mathlib’s notation for a vector, i.e. for a function
Fin 3 → ℝ; it is nested, ![a, b, c] being Matrix.vecCons a ![b, c], and
that nesting is why the first proof needs the lemma list it does.
norm_num [Population.tau, mean, Population.effect, toy, Fin.sum_univ_three, Matrix.cons_val_two, Matrix.tail_cons, Matrix.head_cons]One tactic.
norm_num [l₁, l₂, …]issimpwith the named lemmas added, plus a decision procedure for numeric goals; naming a definition in the list unfolds it. The eight entries do three jobs — unfold the definitions, write the three-term sum out, and read entries out of the vector literals — and are taken in that order below.Population.tau, mean, Population.effectThe three definitions. Unfolding them turns
toy.tau = 2into an explicit quotient: the sum overFin 3oftoy.y1 i - toy.y0 i, divided by3.Fin.sum_univ_three∑ i, f i = f 0 + f 1 + f 2forf : Fin 3 → M. There are exactly three units, so the sum is written out. This is the lemma that makes a small design computable at all; the generalncase has no such expansion.toyUnfolding
toyreplacestoy.y1andtoy.y0by the two vector literals, and what is left is arithmetic on entries of literals.Goal⊢ (![3, 5, 4] 0 - ![1, 2, 3] 0 + (![3, 5, 4] 1 - ![1, 2, 3] 1) + (![3, 5, 4] 2 - ![1, 2, 3] 2)) / ↑3 = 2Matrix.cons_val_two, Matrix.tail_cons, Matrix.head_consReading an entry out of
![3, 5, 4]means unwrapping the nesting.Matrix.cons_val_two : (vecCons x u) 2 = vecHead (vecTail u)takes the third entry to the head of the tail, andtail_cons/head_consthen peel one layer each. Entries0and1need no help —Matrix.cons_val_zeroandMatrix.cons_val_oneare alreadysimplemmas — which is exactly why only the index-2lemmas appear in the list. Drop them andnorm_numstops half-way, leaving(8 + ![3, 5, 4] 2 - (3 + ![1, 2, 3] 2)) / 3 = 2.norm_numWith every entry a numeral, the arithmetic half of the tactic finishes:
in .
The second example is the whole chapter in miniature. Treating units 0 and 2
reveals y1 0, y0 1, y1 2 — and the three numbers y0 0, y1 1, y0 2,
which
funext iYobs toy {0, 2}and![3, 2, 4]are functionsFin 3 → ℝ. Two functions are equal when they agree at every argument, andfunextis how you say so: the goal becomes the equation at an arbitrary uniti.Goali : Fin 3 ⊢ Yobs toy {0, 2} i = ![3, 2, 4] ifin_cases ii : Fin 3has exactly three inhabitants, sofin_casesreplaces the variable by each of them in turn, leaving three goals — the equation at unit0, at unit1and at unit2. (Each index arrives wrapped in the redex(fun i => i) ⟨0, _⟩thatfin_casesleaves behind;simpbeta-reduces it without being asked.)<;> simp [Yobs_eq_ite, toy]<;>runs what follows on every goal the previous tactic produced, so onesimpcall discharges all three branches. In each,Yobs_eq_iteturns the observed outcome intoif i ∈ {0, 2} then toy.y1 i else toy.y0 i; membership in a literal set ofFin 3is decidable, sosimpresolves theifby itself —0and2take the treated branch,1the control branch — andtoyplus the matrix-literalsimplemmas evaluate both sides to the same numeral.
An assignment is the treated set
An assignment says who got treated. The obvious encoding is a vector of zeros
and ones, Fin n → Bool. The library uses the set of treated units instead.
DefinitionAssignment
An assignment is a subset
abbrev Assignment (n : ℕ) := Finset (Fin n)This is a design decision worth a paragraph, because it is the one that decides how hard the rest of the library is to prove.
- “Exactly
units are treated” becomesFinset.powersetCard n₁ univ, an object Mathlib already knows how to count. Complete randomization in chapter 7 is then a Mathlib lemma away rather than a bespoke combinatorial argument. - Bernoulli assignment sums become
Finset.prod_add, again already in Mathlib. - The control group is free: it is
zᶜ, and every lemma proved about the treated side transfers to the control side by complementation. Finset (Fin n)is aFintype, so∑ z, …over all assignments is a finite sum that needs no extra bookkeeping. That sum is what an expectation over a design will be.
abbrev rather than def matters: an abbrev is reducible, so Lean will
unfold Assignment n to Finset (Fin n) on its own, and every Finset lemma
and instance applies to an assignment without a word from you.
Nothing downstream pattern-matches on the set. The algebra is written in terms of the indicator, which is real-valued:
def Z (z : Assignment n) (i : Fin n) : ℝ := if i ∈ z then 1 else 0
@[simp] theorem Z_of_mem {z : Assignment n} {i : Fin n} (h : i ∈ z) : Z z i = 1 := ite_eq_left h
@[simp] theorem Z_of_not_mem {z : Assignment n} {i : Fin n} (h : i ∉ z) : Z z i = 0 := ite_eq_right hThe two lemmas beside the definition are the only ways anyone ever gets rid of
the if, and both are one-term proofs.
ite_eq_left hite_eq_left : c → (if c then t else e) = tsays that anifwhose condition holds is its then branch. Applied toh : i ∈ zit proves(if i ∈ z then 1 else 0) = 1, and that isZ z i = 1onceZis unfolded — which Lean does on its own, since a term is checked against the goal up to unfolding definitions.ite_eq_right hThe mirror image:
ite_eq_right : ¬c → (if c then t else e) = epicks the else branch, soh : i ∉ zgivesZ z i = 0. Both lemmas are tagged@[simp], which is what lets a latersimp [hi]clear every indicator out of a goal the momenti’s membership is known.
Z z i is 1 or 0 — the number, not the Boolean and not the proposition.
Design-based inference is algebra with indicators. In Bool you would be
writing a coercion at every occurrence of Z; in Prop you would be reasoning
about propositions rather than computing with them. In ℝ the usual identities
are ordinary equations in a field, which is what lets every later chapter hand
them to ring, field_simp and linarith. The two that everything is built
from have to be proved by hand first, from the case split.
Lemma. For every assignment
theorem Z_mul_self (z : Assignment n) (i : Fin n) : Z z i * Z z i = Z z i := by
unfold Z; split <;> norm_num
theorem Z_sq (z : Assignment n) (i : Fin n) : Z z i ^ 2 = Z z i := by
rw [sq, Z_mul_self]The two theorems in that block are the same fact stated with a product and with a square, and they are proved differently on purpose: the first does the case split, the second reduces to the first.
unfold Zunfold Zreplaces everyZ z iin the goal by its body. Nothing is proved by this; it exposes theif, which is the only thing in the goal a tactic can act on.Goaln : ℕ z : Assignment n i : Fin n ⊢ ((if i ∈ z then 1 else 0) * if i ∈ z then 1 else 0) = if i ∈ z then 1 else 0splitsplitfinds anifin the goal and splits on its condition, producing one goal per branch with the branch already substituted — so both remaining goals are numeric. The hypothesis it introduces is namedh✝, with the dagger marking it inaccessible: neither branch needs to mention it, because the condition has already done its work.Goalcase isTrue n : ℕ z : Assignment n i : Fin n h✝ : i ∈ z ⊢ 1 * 1 = 1 case isFalse n : ℕ z : Assignment n i : Fin n h✝ : i ∉ z ⊢ 0 * 0 = 0<;> norm_num<;>applies what follows to both goalssplitproduced.norm_numevaluates1 * 1 = 1and0 * 0 = 0in and closes each.rw [sq, Z_mul_self]The squared form,
Z_sq, is now two rewrites.sq : a ^ 2 = a * a(a lemma, despite the name) turns the power into a product, leavingZ z i * Z z i = Z z i; that is exactlyZ_mul_self, whose left-hand side rewrites toZ z i, andrwcloses a goal that has becomerfl.
theorem one_sub_Z (z : Assignment n) (i : Fin n) : 1 - Z z i = Z zᶜ i := by
unfold Z
by_cases h : i ∈ z <;> simp [h]unfold ZBoth sides carry an indicator, and they are indicators of different sets, so unfolding leaves two
ifs with different conditions:i ∈ zon the left,i ∈ zᶜon the right.Goaln : ℕ z : Assignment n i : Fin n ⊢ (1 - if i ∈ z then 1 else 0) = if i ∈ zᶜ then 1 else 0by_cases h : i ∈ zTwo goals, one with
h : i ∈ zand one withh : i ∉ z. WhereZ_mul_selfcould usesplit, here it cannot: the twoifs have different conditions, and what is needed is a named hypothesis abouti ∈ zthat can be used to resolve both of them.by_casesis the tactic that supplies one.<;> simp [h]In the treated branch
hsends the leftifto1, andFinset.mem_compl : a ∈ sᶜ ↔ a ∉ s— asimplemma — turns the right condition intoi ∉ z, whichhcontradicts, so the rightifgoes to0;simpthen checks1 - 1 = 0. In the control branch the same two facts point the other way and the goal is1 - 0 = 1.
The second lemma is the complementation trick made precise: 1 - Z z i is not
merely like an indicator, it is the indicator of zᶜ. Every control-side
lemma in the rest of the book is a treated-side lemma with zᶜ written for z.
Counting the two groups
Write
ExerciseThe treated count
Warm-up. The number of treated units is the sum of the indicators:
theorem sum_Z (z : Assignment n) : ∑ i, Z z i = (#z : ℝ) := by
sorryHint
A single simp only. Unfold Z; Finset.sum_ite_mem turns the sum of
if i ∈ z into a sum over univ ∩ z; Finset.univ_inter simplifies that to
z; then Finset.sum_const, nsmul_eq_mul and mul_one turn
∑ i ∈ z, (1 : ℝ) into #z.
Show solution
theorem sum_Z (z : Assignment n) : ∑ i, Z z i = (#z : ℝ) := by
simp only [Z, Finset.sum_ite_mem, Finset.univ_inter, Finset.sum_const, nsmul_eq_mul, mul_one]and the control count follows by complementation, with no new work:
ExerciseThe control count
Warm-up. The control indicators sum to the number of control units:
theorem sum_one_sub_Z (z : Assignment n) : ∑ i, (1 - Z z i) = (#zᶜ : ℝ) := by
sorryHint
Two lines. simp only [one_sub_Z] turns every 1 - Z z i into Z zᶜ i, and
the goal is then exactly sum_Z zᶜ.
Show solution
theorem sum_one_sub_Z (z : Assignment n) : ∑ i, (1 - Z z i) = (#zᶜ : ℝ) := by
simp only [one_sub_Z]
exact sum_Z zᶜThe cast is where it gets annoying. card_compl says #zᶜ = n - #z in ℕ,
where subtraction is truncated, and getting that into ℝ requires knowing that
the subtraction does not truncate:
ExerciseThe control count, over ℝ
Core. The control count as a real number,
theorem card_compl_cast (z : Assignment n) : ((#zᶜ : ℕ) : ℝ) = (n : ℝ) - #z := by
sorryHint
card_compl gives #zᶜ = n - #z in ℕ, and Nat.cast_sub moves the cast
across the subtraction once you hand it a proof of #z ≤ n.
Finset.card_le_univ z gives #z ≤ Fintype.card (Fin n), and simpa reduces
the right-hand side to n.
Show solution
theorem card_compl_cast (z : Assignment n) : ((#zᶜ : ℕ) : ℝ) = (n : ℝ) - #z := by
have h : #z ≤ n := by simpa using Finset.card_le_univ z
rw [MrCLean.card_compl, Nat.cast_sub h]The observed outcome
You see
DefinitionObserved outcome
The observed outcome of unit
def Yobs (P : Population n) (z : Assignment n) (i : Fin n) : ℝ :=
Z z i * P.y1 i + (1 - Z z i) * P.y0 iThe library takes the algebraic form as the definition and proves the if form
as a lemma, rather than the other way round. That ordering is deliberate: the
algebraic form is linear in Z, which is what makes every expectation
computation in chapters 8 and 9 a rewrite rather than a case split.
ExerciseThe two forms agree
Warm-up. The algebraic form of Yobs agrees with the case split: the observed
outcome is y1 i for a treated unit and y0 i for a control unit.
theorem Yobs_eq_ite (P : Population n) (z : Assignment n) (i : Fin n) :
Yobs P z i = if i ∈ z then P.y1 i else P.y0 i := by
sorryHint
unfold Yobs Z replaces both names by their bodies; then by_cases h : i ∈ z
splits on membership and simp [h] closes each branch. Note the <;>: it runs
the tactic on both goals the by_cases produced.
Show solution
theorem Yobs_eq_ite (P : Population n) (z : Assignment n) (i : Fin n) :
Yobs P z i = if i ∈ z then P.y1 i else P.y0 i := by
unfold Yobs Z
by_cases h : i ∈ z <;> simp [h]Once you have that, the two branches follow, and they are the lemmas you will actually reach for.
Lemma. If unit
Substitute
theorem Yobs_of_mem {P : Population n} {z : Assignment n} {i : Fin n} (h : i ∈ z) :
Yobs P z i = P.y1 i := by rw [Yobs_eq_ite, ite_eq_left h]
theorem Yobs_of_not_mem {P : Population n} {z : Assignment n} {i : Fin n} (h : i ∉ z) :
Yobs P z i = P.y0 i := by rw [Yobs_eq_ite, ite_eq_right h]rw [Yobs_eq_ite, ite_eq_left h]Two rewrites, and the goal shown is the state between them.
Yobs_eq_iteis the exercise above, used left to right: it replaces the algebraic form by the case split.ite_eq_left hthen collapses thatifusingh : i ∈ z, leavingP.y1 i = P.y1 i, whichrwcloses automatically. Notice that the argument is the informal proof run backwards — rather than substituting into the algebra, it moves to theifform first, where the hypothesis applies directly.GoalP : Population n z : Assignment n i : Fin n h : i ∈ z ⊢ (if i ∈ z then P.y1 i else P.y0 i) = P.y1 irw [Yobs_eq_ite, ite_eq_right h]The control case, with
ite_eq_rightfor the else branch andh : i ∉ z. Both lemmas take their membership hypothesis implicitly, which is why the statements can be quantified over{i : Fin n}in braces and read like ordinary mathematics at the point of use:Yobs_of_mem hiis “unitiis treated, so it showsy1”.
An exercise in reading Yobs as a regression writes it:
ExerciseThe regression form
Core. The observed outcome written the way a regression writes it: the control outcome plus the individual effect, switched on by the indicator.
theorem Yobs_eq_y0_add_Z_mul_effect (P : Population n) (z : Assignment n) (i : Fin n) :
Yobs P z i = P.y0 i + Z z i * P.effect i := by
sorryHint
simp only [Yobs, Population.effect] unfolds both definitions and then ring
proves the resulting polynomial identity in Z z i, P.y1 i, P.y0 i. There
is no case split: this is an identity, not a fact about indicators.
Show solution
theorem Yobs_eq_y0_add_Z_mul_effect (P : Population n) (z : Assignment n) (i : Fin n) :
Yobs P z i = P.y0 i + Z z i * P.effect i := by
simp only [Yobs, Population.effect]
ringNo interference is a type, not an assumption
In a paper, SUTVA is a numbered assumption you state and then hope nobody
checks. Here there is nothing to state. y1 : Fin n → ℝ takes a unit and
returns a number; it has no argument in which anybody else’s treatment could
appear. A model with interference would need
structure InterferingPopulation (n : ℕ) where
y : Assignment n → Fin n → ℝ
which is a different structure, so the assumption is not something you can forget: you would have to rewrite the model to violate it.
The consequence is provable rather than assumed.
Proposition (no interference). Let
By hypothesis, either
theorem Yobs_congr (P : Population n) (z z' : Assignment n) (i : Fin n)
(h : i ∈ z ↔ i ∈ z') : Yobs P z i = Yobs P z' i := by
rw [Yobs_eq_ite, Yobs_eq_ite]
by_cases hi : i ∈ z
· rw [ite_eq_left hi, ite_eq_left (h.mp hi)]
· rw [ite_eq_right hi, ite_eq_right fun hc => hi (h.mpr hc)]Read the statement: if two assignments agree about unit i then unit i
shows the same observed outcome under both, no matter how differently the other
ite_eq_left / ite_eq_right — the lemmas that collapse an if once you know
which branch you are in — doing the substitution in each case.2
rw [Yobs_eq_ite, Yobs_eq_ite]The lemma name appears twice because
rwchooses its instantiation from the first match it finds. The first pass matchesYobs P z i, fixing the assignment toz, and rewrites every occurrence of that instance; the second pass then picks upYobs P z' i. After both, each side of the goal is anif, and the two conditions are the two memberships the hypothesis relates.GoalP : Population n z z' : Assignment n i : Fin n h : i ∈ z ↔ i ∈ z' ⊢ (if i ∈ z then P.y1 i else P.y0 i) = if i ∈ z' then P.y1 i else P.y0 iby_cases hi : i ∈ zThe case split of the informal proof, on the left assignment. Two goals, one with
hi : i ∈ zand one withhi : i ∉ z, and in each the hypothesishwill transport that membership toz'.· rw [ite_eq_left hi, ite_eq_left (h.mp hi)]The treated branch — the
·is a bullet, which focuses the first remaining goal.ite_eq_left hicollapses the leftiftoP.y1 i. For the rightifthe condition isi ∈ z', andh.mp hisupplies a proof of it:his anIff, soh.mpis its forward direction, and applying it tohigivesi ∈ z'. The goal becomesP.y1 i = P.y1 iandrwcloses it.· rw [ite_eq_right hi, ite_eq_right fun hc => hi (h.mpr hc)]The control branch, and the one place in this chapter where a proof term is built inline.
ite_eq_rightneedsi ∉ z', which is by definitioni ∈ z' → False, so a function is what has to be supplied: givenhc : i ∈ z', the backward directionh.mpr hcturns it intoi ∈ z, and feeding that tohi : i ∉ zyieldsFalse. Both sides becomeP.y0 i.
The fundamental problem of causal inference
Informally: you observe one potential outcome per unit, never both, so
Theoremτ is not identified
Suppose
An analyst who sees
theorem exists_population_tau_eq (hn : 0 < n) (P : Population n) (z : Assignment n) (t : ℝ) :
∃ P' : Population n, Yobs P' z = Yobs P z ∧ P'.tau = t := by
refine ⟨shift P z (t - P.tau), Yobs_shift P z _, ?_⟩
rw [tau_shift hn]
ringPut
Under
The construction is one definition, and it is the display above transcribed:
def shift (P : Population n) (z : Assignment n) (c : ℝ) : Population n where
y1 := fun i => if i ∈ z then P.y1 i else P.y1 i + c
y0 := fun i => if i ∈ z then P.y0 i - c else P.y0 iThe two ifs lean opposite ways for the reason the informal proof gives — the
entry a treated unit hides is its y0, the entry a control unit hides is its
y1 — and that is exactly why nothing observable moves:
theorem Yobs_shift (P : Population n) (z : Assignment n) (c : ℝ) :
Yobs (shift P z c) z = Yobs P z := by
funext i
by_cases h : i ∈ z <;> simp [Yobs_eq_ite, shift, h]funext iThe statement is an equation between two vectors,
Yobs (shift P z c) zandYobs P z. Two functions are equal when they agree everywhere, andfunextis how you say so in Lean: the goal becomes the equation at an arbitrary uniti.GoalP : Population n z : Assignment n c : ℝ i : Fin n ⊢ Yobs (shift P z c) z i = Yobs P z iby_cases h : i ∈ zThe case split of the informal proof, on whether
iwas treated. Both branches are needed becauseshiftis defined by anifon exactly this condition.<;> simp [Yobs_eq_ite, shift, h]One
simpcall closes both goals.Yobs_eq_iteturns each side into anif;shiftunfolds the perturbed population’s two fields;hthen resolves everyifin sight — the twoYobs_eq_iteproduced, one per side, and the one inside whichevershiftfield the first of them selected — and both sides reduce toP.y1 iin the treated branch and toP.y0 iin the control branch. That the unshifted outcome survives is the whole content: the entryshiftmoved is the entry this assignment does not reveal.
The effect moves uniformly, so the average moves with it:
theorem tau_shift (hn : 0 < n) (P : Population n) (z : Assignment n) (c : ℝ) :
(shift P z c).tau = P.tau + c := by
have heff : (shift P z c).effect = fun i => P.effect i + c := by
funext i
by_cases h : i ∈ z <;> simp [Population.effect, shift, h] <;> ring
calc (shift P z c).tau
= mean (fun i => P.effect i + c) := by simp only [Population.tau, heff]
_ = mean P.effect + mean (fun _ : Fin n => c) := mean_add P.effect fun _ => c
_ = P.tau + c := by simp only [mean_const hn, Population.tau]have heff : (shift P z c).effect = fun i => P.effect i + c := byThe pointwise half of the informal proof, isolated as a named fact because the
calcchain below needs it as a rewrite. It says the shifted effect vector is the original plus the constantc— an equation between functions, so its own proof starts withfunext.funext iInside the
have: reduce the vector equation to the equation at one unit.Goal⊢ (shift P z c).effect i = P.effect i + cby_cases h : i ∈ z <;> simp [Population.effect, shift, h] <;> ringSplit on membership, then
simpunfoldseffectandshiftand resolves theifs, and the two goals shown are what it leaves. Both are true by ordinary algebra but neither is syntactically the goal — the treated branch has to move a minus sign across a bracket, the control branch has to reassociate — so a second<;>hands each toring, which proves any identity that holds in a commutative ring. Note that this is the step where the two oppositeifs inshiftpay off: a treated unit gainscby havingy0lowered, a control unit by havingy1raised, and the two branches end up at the same expression.Goalcase pos h : i ∈ z ⊢ P.y1 i - (P.y0 i - c) = P.y1 i - P.y0 i + c case neg h : i ∉ z ⊢ P.y1 i + c - P.y0 i = P.y1 i - P.y0 i + ccalc (shift P z c).tau = mean (fun i => P.effect i + c) := by simp only [Population.tau, heff]The averaging half, as a
calcchain: three steps, each stating an equation whose right-hand side is the next step’s left-hand side. HerePopulation.tauunfolds to the mean of the effect vector, andheffrewrites that vector tofun i => P.effect i + c._ = mean P.effect + mean (fun _ : Fin n => c) := mean_add P.effect fun _ => cThe
_stands for the previous right-hand side. This step is the exercise at the end of the chapter, applied in term mode: the mean of a pointwise sum is the sum of the means. Its two arguments are the two summands,P.effectand the constant functionfun _ => c._ = P.tau + c := by simp only [mean_const hn, Population.tau]mean_const hn : mean (fun _ : Fin n => c) = c— and this is the one placehn : 0 < nis used, because with no units the mean of a constant is , not . Foldingmean P.effectback intoP.taufinishes the chain. With no units at all every population has and nothing can be shifted, which is why the hypothesis is not decoration.
That is everything the theorem needs. Its own proof — the three lines beside the statement at the head of this section — spends the two lemmas just proved and does no mathematics of its own.
refine ⟨shift P z (t - P.tau), Yobs_shift P z _, ?_⟩The goal is an existential with a conjunction inside it, so one anonymous constructor
⟨…⟩supplies all three pieces at once: the witness , which isPshifted byc = t - P.tau; the proof that the observed vectors agree, which isYobs_shiftwith_for the shift amount Lean can read off the goal; and?_, which postpones the last component as a new goal.refineisexactwith holes.rw [tau_shift hn]The remaining goal is
(shift P z (t - P.tau)).tau = t. Rewriting withtau_shift hnreplaces the left-hand side byP.tau + (t - P.tau).ringP.tau + (t - P.tau) = tis an identity in a commutative ring, andringproves it. The cancellation is the reason the witness wast - P.taurather than anything more clever: choosing the shift is choosing the answer.
The form one usually says out loud follows immediately.
Corollary (the fundamental problem). Suppose
Let
theorem fundamental_problem (hn : 0 < n) (z : Assignment n) :
∃ P P' : Population n, Yobs P z = Yobs P' z ∧ P.tau ≠ P'.tau := by
obtain ⟨P', hY, ht⟩ :=
exists_population_tau_eq hn (⟨fun _ => 0, fun _ => 0⟩ : Population n) z 1
refine ⟨⟨fun _ => 0, fun _ => 0⟩, P', hY.symm, ?_⟩
have h0 : (⟨fun _ => 0, fun _ => 0⟩ : Population n).tau = 0 := by
simp [Population.tau, Population.effect, mean]
rw [h0, ht]
norm_numobtain ⟨P', hY, ht⟩ := exists_population_tau_eq hn (⟨fun _ => 0, fun _ => 0⟩ : Population n) z 1Instantiate the theorem at the all-zero population —
⟨fun _ => 0, fun _ => 0⟩is the anonymous constructor again, building aPopulation nout of its two fields — and at . What comes back is an existential, andobtaindestructures it in one move:P'is the witness,hYthe agreement of the observed vectors,htthe fact thatP'.tau = 1.Goaln : ℕ hn : 0 < n z : Assignment n P' : Population n hY : Yobs P' z = Yobs { y1 := fun x => 0, y0 := fun x => 0 } z ht : P'.tau = 1 ⊢ ∃ P P', Yobs P z = Yobs P' z ∧ P.tau ≠ P'.taurefine ⟨⟨fun _ => 0, fun _ => 0⟩, P', hY.symm, ?_⟩Supply the two populations of the corollary, in the order the statement wants them: the all-zero one first,
P'second.hY.symmrather thanhYbecause the theorem producedYobs P' z = Yobs P zand the goal asks for the reverse —.symmon an equation flips it. The inequality is left as?_.GoalhY : Yobs P' z = Yobs { y1 := fun x => 0, y0 := fun x => 0 } z ht : P'.tau = 1 ⊢ { y1 := fun x => 0, y0 := fun x => 0 }.tau ≠ P'.tauhave h0 : (⟨fun _ => 0, fun _ => 0⟩ : Population n).tau = 0 := by simp [Population.tau, Population.effect, mean]The one arithmetic fact the corollary needs: the all-zero population has
. Unfolding the three definitions makes the goal(∑ i, (0 - 0)) / n = 0, andsimpfinishes it — the sum of zeros is zero, and0 / nis0whatevernis, sohnis not needed here.rw [h0, ht]Rewrite both sides of the inequality with the values just computed:
h0replaces the left-hand by0,htthe right-hand one by1. The goal is now(0 : ℝ) ≠ 1.norm_numA numeric fact about
, which is whatnorm_numis for. Note how little of the work is here: the mathematics was the shift construction, and this last theorem only picks a convenient and a convenient .
What the data does pin down is exactly the treated units’ y1 and the control
units’ y0:
ExerciseEverything an experiment tells you
Core. Two populations have the same observed vector under y1 over the treated units and on y0 over the control units. Read
the right-hand side as the exact inventory of what a single experiment can ever
tell you.
theorem Yobs_eq_iff (P P' : Population n) (z : Assignment n) :
Yobs P z = Yobs P' z ↔ (∀ i ∈ z, P.y1 i = P'.y1 i) ∧ (∀ i ∉ z, P.y0 i = P'.y0 i) := by
sorryHint
constructor splits the ↔. Forward: congrFun h i gives the equation at unit
i, and rewriting both sides with Yobs_of_mem hi (or Yobs_of_not_mem hi,
depending on the branch) reads off the answer — rwa is rw followed by
assumption. Backward: funext i, by_cases hi : i ∈ z, and the same two
lemmas together with h1 i hi or h0 i hi.
Show solution
theorem Yobs_eq_iff (P P' : Population n) (z : Assignment n) :
Yobs P z = Yobs P' z ↔ (∀ i ∈ z, P.y1 i = P'.y1 i) ∧ (∀ i ∉ z, P.y0 i = P'.y0 i) := by
constructor
· intro h
refine ⟨fun i hi => ?_, fun i hi => ?_⟩
· have hc := congrFun h i
rwa [Yobs_of_mem hi, Yobs_of_mem hi] at hc
· have hc := congrFun h i
rwa [Yobs_of_not_mem hi, Yobs_of_not_mem hi] at hc
· rintro ⟨h1, h0⟩
funext i
by_cases hi : i ∈ z
· rw [Yobs_of_mem hi, Yobs_of_mem hi, h1 i hi]
· rw [Yobs_of_not_mem hi, Yobs_of_not_mem hi, h0 i hi]None of this is fixed by collecting more units, and none of it is fixed by
measuring more carefully. It is fixed — in the weaker sense of “made estimable
on average” — by making
Exercises
The exercises above are the load-bearing ones. Three more, in increasing order of effort.
ExerciseWarm-up: the mean is additive
The mean of a pointwise sum is the sum of the means.
theorem mean_add (u v : Fin n → ℝ) : mean (fun i => u i + v i) = mean u + mean v := by
sorryHint
One simp only with three lemmas: mean to unfold the definition,
Finset.sum_add_distrib to split the sum, add_div to split the division.
Show solution
theorem mean_add (u v : Fin n → ℝ) : mean (fun i => u i + v i) = mean u + mean v := by
simp only [mean, Finset.sum_add_distrib, add_div]ExerciseWarm-up: everybody treated
If everybody is treated, the observed vector is y1. Note the corollary: even
in this extreme case y0 is entirely unobserved, so exists_population_tau_eq
still applies.
theorem Yobs_univ (P : Population n) : Yobs P Finset.univ = P.y1 := by
sorryHint
funext i reduces to a single unit, and Yobs_of_mem (Finset.mem_univ i) is
the whole proof — Finset.mem_univ i is the proof that every unit belongs to
univ.
Show solution
theorem Yobs_univ (P : Population n) : Yobs P Finset.univ = P.y1 := by
funext i
exact Yobs_of_mem (Finset.mem_univ i)ExerciseStretch: the mean of the observed vector
The mean of the observed vector is the treated units’ y1 and the control
units’ y0, averaged over the whole population. Having proved it, notice that
z appears in the statement and
theorem mean_Yobs (P : Population n) (z : Assignment n) :
mean (Yobs P z) = ((∑ i ∈ z, P.y1 i) + ∑ i ∈ zᶜ, P.y0 i) / n := by
sorryHint
Work on the right-hand side: sum_mem_eq_sum_Z_mul and
sum_compl_eq_sum_one_sub_Z_mul, used left to right, turn the two group sums
into indicator-weighted sums over everybody, and ← Finset.sum_add_distrib
merges them into one sum. Both sides are then the same expression up to
unfolding mean and Yobs, so rfl finishes. Resist simp here: mean and
the two sum lemmas loop against each other, which is what the “possibly looping
simp theorem” warning is telling you.
Show solution
theorem mean_Yobs (P : Population n) (z : Assignment n) :
mean (Yobs P z) = ((∑ i ∈ z, P.y1 i) + ∑ i ∈ zᶜ, P.y0 i) / n := by
rw [sum_mem_eq_sum_Z_mul, sum_compl_eq_sum_one_sub_Z_mul, ← Finset.sum_add_distrib]
rflReading autoformalized Lean
Suppose you asked a model to formalize “the fundamental problem of causal inference” and got one of the following. Each type-checks. None of them says it.
The quantifiers swapped
theorem fundamental_problem (hn : 0 < n) (z : Assignment n) (P P' : Population n) :
Yobs P z = Yobs P' z → P.tau ≠ P'.tau
The hypothesis nobody can satisfy
theorem fundamental_problem' (hn : 0 < n) (z : Assignment n) (y : Fin n → ℝ)
(h : ∀ P : Population n, Yobs P z = y) :
∃ P P' : Population n, Yobs P z = Yobs P' z ∧ P.tau ≠ P'.tau
The definition doing the theorem’s work
def Yobs (P : Population n) (z : Assignment n) (i : Fin n) : ℝ := P.y1 i
Footnotes
-
This is Neyman’s 1923 model, and the reason the tutorial is arranged around it is that its randomness has exactly one source. A Lean development of the superpopulation version would need measure theory on page one; this one needs finite sums. ↩
-
If you have read older Lean 4 code you will know these as
if_posandif_neg. They are deprecated as of v4.34, and Mathlib is mid-migration on several such names. ↩