Causal inferenceChapter 10
Blocking and clustering
Pushforward designs, stratified randomization, blocked estimators, and cluster randomization.
Contents
The designs considered so far assign treatment directly to individual units. Two common alternatives are:
- Blocking. Split the units into
strata by something you know in advance — clinic, school, pre-treatment risk decile — and run a separate completely randomized experiment inside each one. Balance on the blocking variable stops being a matter of luck. - Clustering. You cannot treat half a village, so you randomize villages and every unit inside a treated village is treated.
Both designs can reuse the Horvitz–Thompson theorem from chapter 8. The new work is to construct each design and compute its inclusion probabilities. Unbiasedness then follows from the general theorem.
A design on one space induces a design on another
Start with the general tool, because clustering is nothing but a special case of it and blocking borrows its proof idea.
Suppose
noncomputable def map (f : Assignment m → Assignment n) (D : Design m) : Design n where
prob z := ∑ w ∈ univ.filter (fun w => f w = z), D.prob w
nonneg _ := Finset.sum_nonneg fun w _ => D.nonneg w
sum_one := (Finset.sum_fiberwise univ f D.prob).trans D.sum_oneA Design has three fields — the pmf, its nonnegativity, and that it sums to
one — so defining one means supplying three things.
prob z := ∑ w ∈ univ.filter (fun w => f w = z), D.prob wThe pmf.
univ.filter (fun w => f w = z)is the fibre written as aFinset, so the probability the pushforward assigns to is the total upstream probability of the assignments that map to .nonneg _ := Finset.sum_nonneg fun w _ => D.nonneg wNonnegativity, which is free:
Finset.sum_nonnegsays a sum of nonnegative terms is nonnegative, and every term is nonnegative becauseDis already a design. The underscore is the assignment , which this argument never looks at.sum_one := (Finset.sum_fiberwise univ f D.prob).trans D.sum_oneSumming to one, the only real obligation, and it is a Mathlib lemma.
Finset.sum_fiberwise univ f D.probstates : summing over the fibres of and then over the values of recovers the sum over the whole domain. That is the definition of a partition into fibres, and it will show up twice more in this chapter..transchains that equation withD.sum_one, which says the right-hand side is .
The one property we need of the pushforward is the change-of-variables formula.
Proposition (change of variables). Let
Write
Inside the fibre
theorem expect_map (f : Assignment m → Assignment n) (D : Design m) (g : Assignment n → ℝ) :
(D.map f).expect g = D.expect (fun w => g (f w)) := by
have key : ∀ z : Assignment n,
(D.map f).prob z * g z = ∑ w ∈ univ.filter (fun w => f w = z), D.prob w * g (f w) := by
intro z
rw [map_prob, Finset.sum_mul]
exact Finset.sum_congr rfl fun w hw => by rw [(Finset.mem_filter.mp hw).2]
calc (D.map f).expect g
= ∑ z : Assignment n, ∑ w ∈ univ.filter (fun w => f w = z), D.prob w * g (f w) :=
Finset.sum_congr rfl fun z _ => key z
_ = D.expect (fun w => g (f w)) := Finset.sum_fiberwise univ f _Read the statement first. (D.map f).expect g is an expectation over
assignments of D.expect (fun w => g (f w)) is an expectation over
assignments of
The Lean follows the informal proof exactly: have key is the per-calc block — Lean’s way of writing a chain of
equalities with a justification for each link — is the two displayed equations.
intro zFix the downstream assignment
; the claim ofkeyis that one term of the outer sum, the probability of times , is the fibre sum with evaluated upstream.Goalf : Assignment m → Assignment n D : Design m g : Assignment n → ℝ z : Assignment n ⊢ (Design.map f D).prob z * g z = ∑ w with f w = z, D.prob w * g (f w)rw [map_prob, …]The first of the two rewrites:
map_probis the@[simp]lemma recording that the pushforward’s pmf is the fibre sum, so the left-hand side becomes(∑ w ∈ fibre, D.prob w) * g z.Goal⊢ (∑ w with f w = z, D.prob w) * g z = ∑ w with f w = z, D.prob w * g (f w)rw […, Finset.sum_mul]The second:
Finset.sum_mul, which states , pushes the factorg zinside the sum. The two sides now differ only in whether is applied tozor tof w.Goal⊢ ∑ i with f i = z, D.prob i * g z = ∑ w with f w = z, D.prob w * g (f w)exact Finset.sum_congr rfl fun w hw => by rw [(Finset.mem_filter.mp hw).2]Finset.sum_congr rflreduces an equality of two sums over the sameFinsetto an equality of their terms, giving onewin the fibre at a time.Finset.mem_filter.mp hwsplits membership inuniv.filter (f · = z)into “inuniv” and the filter condition, and.2is the second half,f w = z; rewriting with it turnsg zintog (f w). This is the step where the fibre does its work.= ∑ z : Assignment n, ∑ w ∈ univ.filter (fun w => f w = z), D.prob w * g (f w) := Finset.sum_congr rfl fun z _ => key zFirst
calclink: the expectation is by definition , so applyingkeytermwise turns it into a double sum over fibres. AgainFinset.sum_congr rfldoes the termwise part._ = D.expect (fun w => g (f w)) := Finset.sum_fiberwise univ f _Second link, and the same partition lemma that made
mapa design in the first place:Finset.sum_fiberwisecollapses “over the values of , then over the fibre” into “over the whole domain once”.
Stratified complete randomization
Fix a blocking function b : Fin n → Fin K: unit b i. The
block itself is the fibre.
def block (b : Fin n → Fin K) (k : Fin K) : Finset (Fin n) :=
univ.filter (fun i => b i = k)Writing the block as univ.filter (fun i => b i = k) rather than as a preimage
or an image is deliberate: it is syntactically what Finset.sum_fiberwise
produces, so “group this sum by block” is a rewrite rather than an induction.
Write
The experimenter fixes the quotas n₁ : Fin K → ℕ in advance, and the design
is uniform on the assignments meeting all
def stratSupport (b : Fin n → Fin K) (n₁ : Fin K → ℕ) : Finset (Assignment n) :=
univ.filter (fun z => ∀ k, #(z ∩ block b k) = n₁ k)Counting: an assignment is a tuple of within-block choices
Here is the one place where blocking costs real work. We need to know how many
admissible assignments there are, and the answer — one
Lemma (blockwise counting). For each
The map
theorem card_blockwise (b : Fin n → Fin K) (t : Fin K → Finset (Assignment n))
(ht : ∀ k, ∀ T ∈ t k, T ⊆ block b k) :
#(univ.filter (fun S : Assignment n => ∀ k, S ∩ block b k ∈ t k)) = ∏ k, #(t k) := by
classical
rw [← Fintype.card_piFinset t]
refine Finset.card_nbij' (fun S k => S ∩ block b k) (fun g => univ.biUnion g)
?_ ?_ ?_ ?_
· intro S hS
simp only [Finset.coe_filter, Set.mem_ofPred_eq, Finset.mem_univ, true_and] at hS
simpa only [Finset.mem_coe, Fintype.mem_piFinset] using hS
· intro g hg
simp only [Finset.mem_coe, Fintype.mem_piFinset] at hg
have hsub : ∀ k, g k ⊆ block b k := fun k => ht k (g k) (hg k)
simp only [Finset.coe_filter, Set.mem_ofPred_eq, Finset.mem_univ, true_and]
exact fun k => by rw [inter_block_biUnion hsub k]; exact hg k
· exact fun S _ => biUnion_inter_block b S
· intro g hg
simp only [Finset.mem_coe, Fintype.mem_piFinset] at hg
exact funext fun k => inter_block_biUnion (fun k => ht k (g k) (hg k)) kThe statement is more general than we need, and that is what makes it easy:
nothing is assumed about the admissible families t k except the hypothesis
ht, that each of their members sits inside its own block.
classicalMakes every proposition decidable, so that no instance search behind a
filteror abiUnioncan fail. Defensive rather than necessary: the predicate∀ k, S ∩ block b k ∈ t kis decidable anyway and the proof goes through without the line. It changes nothing mathematically either way.rw [← Fintype.card_piFinset t]Fintype.card_piFinset tstates , whereFintype.piFinset tis theFinsetof dependent tuples with . Used right-to-left it replaces the product on the right by that single cardinality, so the goal becomes an equality of two counts — the shape a bijection can close.Goalb : Fin n → Fin K t : Fin K → Finset (Assignment n) ht : ∀ (k : Fin K), ∀ T ∈ t k, T ⊆ block b k ⊢ #{S | ∀ (k : Fin K), S ∩ block b k ∈ t k} = #(Fintype.piFinset t)refine Finset.card_nbij' (fun S k => S ∩ block b k) (fun g => univ.biUnion g) ?_ ?_ ?_ ?_Finset.card_nbij'equates two cardinalities from a pair of mutually inverse maps. The forward map is and the backward map is ; the four?_holes are its four obligations, in order: each map lands in the right set, and each composite is the identity on its own side.Goalcase refine_1 ⊢ Set.MapsTo (fun S k => S ∩ block b k) ↑{S | ∀ (k : Fin K), S ∩ block b k ∈ t k} ↑(Fintype.piFinset t) case refine_2 ⊢ Set.MapsTo (fun g => univ.biUnion g) ↑(Fintype.piFinset t) ↑{S | ∀ (k : Fin K), S ∩ block b k ∈ t k} case refine_3 ⊢ Set.LeftInvOn (fun g => univ.biUnion g) (fun S k => S ∩ block b k) ↑{S | ∀ (k : Fin K), S ∩ block b k ∈ t k} case refine_4 ⊢ Set.RightInvOn (fun g => univ.biUnion g) (fun S k => S ∩ block b k) ↑(Fintype.piFinset t)intro S hS; simp only [Finset.coe_filter, Set.mem_ofPred_eq, Finset.mem_univ, true_and] at hSFirst obligation, forward map lands in the tuple set. The hypothesis
hSsays is in the coercedfilter;Finset.coe_filterturns that coercion into aSetcomprehension,Set.mem_ofPred_equnfolds membership in it to the predicate, andFinset.mem_univ/true_anddiscard the vacuousS ∈ univconjunct. What is left is exactly∀ k, S ∩ block b k ∈ t k.simpa only [Finset.mem_coe, Fintype.mem_piFinset] using hSAnd that is the goal, once membership in the tuple set is unfolded the same way:
Finset.mem_coestrips the coercion andFintype.mem_piFinsetsays a tuple lies inpiFinset texactly when each component lies in itst k.intro g hg; simp only [Finset.mem_coe, Fintype.mem_piFinset] at hg; have hsub : ∀ k, g k ⊆ block b k := fun k => ht k (g k) (hg k)Second obligation, backward map lands in the filter set. Unfolding
hgthe same way gives∀ k, g k ∈ t k, and feeding that to the hypothesishtgiveshsub: every piece of the tuple sits inside its own block. That is the only thing the reassembly argument needs.simp only [Finset.coe_filter, Set.mem_ofPred_eq, Finset.mem_univ, true_and]; exact fun k => by rw [inter_block_biUnion hsub k]; exact hg kUnfold the goal’s membership to
∀ k, (univ.biUnion g) ∩ block b k ∈ t k, theninter_block_biUnion hsub k— the disjointness half of “the blocks partition the population” — rewrites as , andhg ksays .exact fun S _ => biUnion_inter_block b SThird obligation, cut then reassemble is the identity:
biUnion_inter_block b Sstates , which is the covering half of the same partition fact.intro g hg; simp only [Finset.mem_coe, Fintype.mem_piFinset] at hg; exact funext fun k => inter_block_biUnion (fun k => ht k (g k) (hg k)) kFourth obligation, reassemble then cut is the identity. The two tuples are equal when they agree at every index, which is what
funextreduces the goal to, and at index that isinter_block_biUnionagain.
Specializing to
Corollary. The blocked design’s support — the assignments treating exactly
elements.
An assignment
theorem card_stratSupport (b : Fin n → Fin K) (n₁ : Fin K → ℕ) :
#(stratSupport b n₁) = ∏ k, (#(block b k)).choose (n₁ k) := by
rw [stratSupport_eq_filter,
card_blockwise b (fun k => (block b k).powersetCard (n₁ k))
(fun k T hT => (Finset.mem_powersetCard.mp hT).1)]
exact Finset.prod_congr rfl fun k _ => Finset.card_powersetCard _ _rw [stratSupport_eq_filter, …]stratSupport_eq_filteris the restatement in the shape the lemma expects: it trades the condition#(z ∩ block b k) = n₁ kforz ∩ block b k ∈ (block b k).powersetCard (n₁ k), whereFinset.powersetCard m sis theFinsetof -element subsets ofs. The two are equivalent becauseFinset.inter_subset_rightsupplies the containment for free — that is the one-line iff in its own proof.Goalb : Fin n → Fin K n₁ : Fin K → ℕ ⊢ #{S | ∀ (k : Fin K), S ∩ block b k ∈ powersetCard (n₁ k) (block b k)} = ∏ k, (#(block b k)).choose (n₁ k)rw […, card_blockwise b (fun k => (block b k).powersetCard (n₁ k)) (fun k T hT => (Finset.mem_powersetCard.mp hT).1)]Apply the lemma at
the -subsets of . Its hypothesishtis discharged inline:Finset.mem_powersetCard.mp hTsplits “Tis ann₁ k-subset ofblock b k” into the containment and the count, and.1is the containment. The count is now a product of cardinalities of powerset-card families.Goal⊢ ∏ k, #(powersetCard (n₁ k) (block b k)) = ∏ k, (#(block b k)).choose (n₁ k)exact Finset.prod_congr rfl fun k _ => Finset.card_powersetCard _ _Finset.prod_congr rflcompares the two products factor by factor, andFinset.card_powersetCardis the binomial coefficient itself: .
So the design has
The design
Definition (stratified complete randomization). Fix quotas
noncomputable def stratifiedDesign (b : Fin n → Fin K) (n₁ : Fin K → ℕ)
(h : ∀ k, n₁ k ≤ #(block b k)) : Design n where
prob z := if (∀ k, #(z ∩ block b k) = n₁ k) then (#(stratSupport b n₁) : ℝ)⁻¹ else 0
nonneg _ := by
split
· positivity
· exact le_refl 0
sum_one := by
have hne : ((#(stratSupport b n₁) : ℕ) : ℝ) ≠ 0 :=
Nat.cast_ne_zero.mpr (card_stratSupport_pos h).ne'
calc ∑ z : Assignment n,
(if (∀ k, #(z ∩ block b k) = n₁ k) then (#(stratSupport b n₁) : ℝ)⁻¹ else 0)
= ∑ _z ∈ stratSupport b n₁, (#(stratSupport b n₁) : ℝ)⁻¹ := by
rw [← Finset.sum_filter]; rfl
_ = ((#(stratSupport b n₁) : ℕ) : ℝ) * (#(stratSupport b n₁) : ℝ)⁻¹ := by
rw [Finset.sum_const, nsmul_eq_mul]
_ = 1 := mul_inv_cancel₀ hneNote what this is not. There is no product of designs here, and there could
not be: a Design n is a pmf on Finset (Fin n), and card_blockwise. The two descriptions
agree because an admissible assignment is a tuple of within-block choices.
The hypothesis h : ∀ k, n₁ k ≤ #(block b k) is what makes the support
nonempty; without it there would be no uniform distribution to normalize. The
three fields again:
prob z := if (∀ k, #(z ∩ block b k) = n₁ k) then (#(stratSupport b n₁) : ℝ)⁻¹ else 0The pmf: the reciprocal of the number of admissible assignments on the admissible ones, zero elsewhere. Note that the normalizing constant is written as a cardinality, not as
; the product form is the corollary above, and keeping the count abstract here is what lets the definition go through before any combinatorics is done.nonneg _ := by split · positivity · exact le_refl 0Nonnegativity, one branch of the
ifat a time.splitproduces both branches; in the first the value is the inverse of a natural-number cast, whichpositivityhandles, and in the second it is literally .have hne : ((#(stratSupport b n₁) : ℕ) : ℝ) ≠ 0 := Nat.cast_ne_zero.mpr (card_stratSupport_pos h).ne'Summing to one begins with the fact that makes the normalization legal at all.
card_stratSupport_pos his where the hypothesishis used: the count is a product of , each positive byNat.choose_posbecause ..ne'turns into andNat.cast_ne_zero.mprmoves that from to — the denominator is nonzero, so the reciprocal is a real number and not Lean’s .Goalhne : ↑(#(stratSupport b n₁)) ≠ 0 ⊢ (∑ z, if ∀ (k : Fin K), #(z ∩ block b k) = n₁ k then (↑(#(stratSupport b n₁)))⁻¹ else 0) = 1= ∑ _z ∈ stratSupport b n₁, (#(stratSupport b n₁) : ℝ)⁻¹ := by rw [← Finset.sum_filter]; rflFirst
calclink: throw away the zero terms.Finset.sum_filtersays that summing over the members of that satisfy is the same as summingif p a then f a else 0over all of , so used right-to-left it turns the sum of anifover all assignments into a sum over the filtered set.rflthen identifies that filtered set withstratSupport b n₁, which is whatstratSupportis defined to be.Goal⊢ ∑ a with ∀ (k : Fin K), #(a ∩ block b k) = n₁ k, (↑(#(stratSupport b n₁)))⁻¹ = ∑ _z ∈ stratSupport b n₁, (↑(#(stratSupport b n₁)))⁻¹_ = ((#(stratSupport b n₁) : ℕ) : ℝ) * (#(stratSupport b n₁) : ℝ)⁻¹ := by rw [Finset.sum_const, nsmul_eq_mul]Second link: the summand no longer depends on
, soFinset.sum_constreplaces the sum by#s • c, the cardinality acting on the constant by repeated addition.nsmul_eq_mulconverts that -action into an honest multiplication by the cast.Goal⊢ #(stratSupport b n₁) • (↑(#(stratSupport b n₁)))⁻¹ = ↑(#(stratSupport b n₁)) * (↑(#(stratSupport b n₁)))⁻¹_ = 1 := mul_inv_cancel₀ hneThird link:
for , which is exactly whathnesupplies. The design is normalized.
Balance is then exact, not approximate:
theorem stratified_card_inter_block {z : Assignment n}
(hz : (stratifiedDesign b n₁ h).prob z ≠ 0) (k : Fin K) :
#(z ∩ block b k) = n₁ k := by
by_contra hc
refine hz ?_
rw [stratifiedDesign_prob]
exact ite_eq_right (fun hall => hc (hall k))by_contra hcby_contraassumes the negation of the goal — herehc, that the count in block is not — and asks for a contradiction. The proof is by contradiction because the design’s pmf is anif, and the natural thing to say about anifis which branch it took.Goalhz : (stratifiedDesign b n₁ h).prob z ≠ 0 k : Fin K hc : ¬#(z ∩ block b k) = n₁ k ⊢ Falserefine hz ?_hzsays the probability of is nonzero, i.e. it is a function from “the probability is ” toFalse. Applying it turns the goalFalseinto the goal(stratifiedDesign b n₁ h).prob z = 0.rw [stratifiedDesign_prob]Unfold the pmf with its
@[simp]lemma, so the goal is that theifexpression evaluates to — that is, that its condition is false.Goalhc : ¬#(z ∩ block b k) = n₁ k ⊢ (if ∀ (k : Fin K), #(z ∩ block b k) = n₁ k then (↑(#(stratSupport b n₁)))⁻¹ else 0) = 0exact ite_eq_right (fun hall => hc (hall k))ite_eq_rightstates thatif c then t else eequalsewhen¬c, so it wants a refutation of the condition∀ k, #(z ∩ block b k) = n₁ k. Given such anhall, instantiating it at our block contradictshc— one instance is enough to kill the universal.
Under complete randomization
Inclusion probabilities and a non-constant propensity
Because the design is uniform on its support, every inclusion probability is a ratio of two counts, and the numerator factorizes over blocks by the same counting lemma.
Lemma (inclusion counts factorize). Let
The constraint
theorem card_stratSupport_filter_subset (b : Fin n → Fin K) (n₁ : Fin K → ℕ)
(S : Assignment n) :
#((stratSupport b n₁).filter (fun z => S ⊆ z))
= ∏ k, #(((block b k).powersetCard (n₁ k)).filter (fun T => S ∩ block b k ⊆ T)) := by
have hsub : ∀ k, ∀ T ∈ ((block b k).powersetCard (n₁ k)).filter (fun T => S ∩ block b k ⊆ T),
T ⊆ block b k :=
fun k T hT => (Finset.mem_powersetCard.mp (Finset.mem_filter.mp hT).1).1
have hset : (stratSupport b n₁).filter (fun z => S ⊆ z)
= univ.filter (fun z : Assignment n => ∀ k,
z ∩ block b k ∈ ((block b k).powersetCard (n₁ k)).filter
(fun T => S ∩ block b k ⊆ T)) := by
ext z
simp only [Finset.mem_filter, mem_stratSupport, Finset.mem_univ, true_and,
Finset.mem_powersetCard, subset_iff_forall_inter_block b S z]
constructor
· rintro ⟨hc, hS⟩ k
exact ⟨⟨Finset.inter_subset_right, hc k⟩, hS k⟩
· exact fun hall => ⟨fun k => (hall k).1.2, fun k => (hall k).2⟩
rw [hset, card_blockwise b _ hsub]The Lean proof is that argument with the set identity done explicitly, because
the two descriptions of the same collection of assignments are not syntactically
the same Finset.
have hsub : ∀ k, ∀ T ∈ ((block b k).powersetCard (n₁ k)).filter (fun T => S ∩ block b k ⊆ T), T ⊆ block b k := fun k T hT => (Finset.mem_powersetCard.mp (Finset.mem_filter.mp hT).1).1The hypothesis the counting lemma will need, discharged first.
Finset.mem_filter.mp hTsplits membership in the filtered family into “an -subset of ” and “contains ”;.1keeps the former,Finset.mem_powersetCard.mpsplits that into containment and count, and the outer.1is the containment .ext zNow
hset, the set identity. TwoFinsets are equal when they have the same members, andext zreduces the goal to that biconditional for an arbitrary .Goalz : Assignment n ⊢ z ∈ {z ∈ stratSupport b n₁ | S ⊆ z} ↔ z ∈ {z | ∀ (k : Fin K), z ∩ block b k ∈ {T ∈ powersetCard (n₁ k) (block b k) | S ∩ block b k ⊆ T}}simp only [Finset.mem_filter, mem_stratSupport, Finset.mem_univ, true_and, Finset.mem_powersetCard, subset_iff_forall_inter_block b S z]Unfold both sides into elementary statements about the blocks.
Finset.mem_filterandFinset.mem_univ/true_andstrip the filters,mem_stratSupportreplaces membership in the support by∀ k, #(z ∩ block b k) = n₁ k,Finset.mem_powersetCardsplits the powerset-card membership into containment and count, andsubset_iff_forall_inter_blockis the mathematical content: iff for every . What is left is a purely logical rearrangement — a pair of s on the left, one over a conjunction on the right.Goal⊢ ((∀ (k : Fin K), #(z ∩ block b k) = n₁ k) ∧ ∀ (k : Fin K), S ∩ block b k ⊆ z ∩ block b k) ↔ ∀ (k : Fin K), (z ∩ block b k ⊆ block b k ∧ #(z ∩ block b k) = n₁ k) ∧ S ∩ block b k ⊆ z ∩ block b kconstructor · rintro ⟨hc, hS⟩ k exact ⟨⟨Finset.inter_subset_right, hc k⟩, hS k⟩Left to right.
rintro ⟨hc, hS⟩names the two halves,kfixes a block, and the tuple is reassembled at that block; the one piece not in the hypotheses is , which isFinset.inter_subset_right.· exact fun hall => ⟨fun k => (hall k).1.2, fun k => (hall k).2⟩Right to left, pure projection: from the conjunction at each block take the count (
.1.2) to rebuild the support condition and the containment (.2) to rebuild . The stray containment is simply dropped.rw [hset, card_blockwise b _ hsub]Rewrite with the identity just proved and apply the counting lemma at the filtered families, with
hsubas its hypothesis and the family itself left to unification. That closes the goal.
Take
TheoremThe propensity score of a blocked design
A unit’s chance of treatment depends on its own block and nothing else:
The only hypothesis is h, that each block can supply the units asked of it.
And note what the statement does not say: it does not say
theorem stratified_propensity (i : Fin n) :
(stratifiedDesign b n₁ h).propensity i
= (n₁ (b i) : ℝ) / (#(block b (b i)) : ℝ) := by
have hcount : (stratifiedDesign b n₁ h).propensity i
= (#((stratSupport b n₁).filter (fun z => i ∈ z)) : ℝ)
* ((#(stratSupport b n₁) : ℕ) : ℝ)⁻¹ := by
rw [Design.propensity_eq_inclusion, stratified_inclusion, ← stratSupport_filter_mem_eq]
have hQ : ((∏ k ∈ univ.erase (b i), (#(block b k)).choose (n₁ k) : ℕ) : ℝ) ≠ 0 :=
Nat.cast_ne_zero.mpr (Finset.prod_pos fun k _ => Nat.choose_pos (h k)).ne'
have hcprod : (#(stratSupport b n₁) : ℕ)
= (#(block b (b i))).choose (n₁ (b i))
* ∏ k ∈ univ.erase (b i), (#(block b k)).choose (n₁ k) := by
rw [card_stratSupport]
exact (Finset.mul_prod_erase univ _ (Finset.mem_univ (b i))).symm
rw [hcount, ← div_eq_mul_inv, card_stratSupport_filter_mem b n₁ i, hcprod,
Nat.cast_mul, Nat.cast_mul, mul_div_mul_right _ _ hQ]
rcases Nat.eq_zero_or_pos (n₁ (b i)) with hzero | hpos
· rw [hzero, card_filter_mem_powersetCard_zero]
simp only [Nat.cast_zero, zero_div]
· rw [card_filter_mem_powersetCard (mem_block_self b i) hpos]
exact_mod_cast choose_ratio_one hpos (h (b i))The design is uniform on its support, so for any unit
and both counts factorize over blocks by the previous lemma, taken at
the last step being the identity
The Lean is that computation with the cancellation and the degenerate case written out.
have hcount : … = (#((stratSupport b n₁).filter (fun z => i ∈ z)) : ℝ) * ((#(stratSupport b n₁) : ℕ) : ℝ)⁻¹ := by rw [Design.propensity_eq_inclusion, stratified_inclusion, ← stratSupport_filter_mem_eq]The first of three
haves: the propensity is a ratio of counts.Design.propensity_eq_inclusionsays is the inclusion probability of the singleton ;stratified_inclusionevaluates any inclusion probability of this design as (count of admissible assignments containing ) , which is what “uniform on its support” means quantitatively; andstratSupport_filter_mem_eq, used right-to-left, trades{i} ⊆ zback for the more readablei ∈ z.Goal⊢ (stratifiedDesign b n₁ h).inclusion {i} = ↑(#({z ∈ stratSupport b n₁ | i ∈ z})) * (↑(#(stratSupport b n₁)))⁻¹have hQ : ((∏ k ∈ univ.erase (b i), (#(block b k)).choose (n₁ k) : ℕ) : ℝ) ≠ 0 := Nat.cast_ne_zero.mpr (Finset.prod_pos fun k _ => Nat.choose_pos (h k)).ne'The second: the product over the other blocks is nonzero.
Finset.prod_posreduces positivity of a product to positivity of each factor, andNat.choose_pos (h k)supplies it because , which is what licenses the cancellation two steps below.have hcprod : (#(stratSupport b n₁) : ℕ) = (#(block b (b i))).choose (n₁ (b i)) * ∏ k ∈ univ.erase (b i), (#(block b k)).choose (n₁ k) := by rw [card_stratSupport]; exact (Finset.mul_prod_erase univ _ (Finset.mem_univ (b i))).symmThe third: the denominator, split into
‘s own block times the rest.card_stratSupportis the corollary proved above, andFinset.mul_prod_erasestates for ; reversed, it peels the factor belonging to off the front of the product.card_stratSupport_filter_mem, quoted in the next step, does the same peeling for the numerator.rw [hcount, ← div_eq_mul_inv, card_stratSupport_filter_mem b n₁ i, hcprod, Nat.cast_mul, Nat.cast_mul, …]Assemble.
hcountputs the propensity in count form anddiv_eq_mul_inv, reversed, rewrites as ;card_stratSupport_filter_memfactorizes the numerator into ( ‘s block’s count) (the other blocks’ ), which is where the previous lemma is actually consumed;hcproddoes the denominator; and the twoNat.cast_muls push the cast inside both products so that the common factor is literally the same real number top and bottom.Goal⊢ ↑(#({T ∈ powersetCard (n₁ (b i)) (block b (b i)) | i ∈ T})) * ↑(∏ k ∈ univ.erase (b i), (#(block b k)).choose (n₁ k)) / (↑((#(block b (b i))).choose (n₁ (b i))) * ↑(∏ k ∈ univ.erase (b i), (#(block b k)).choose (n₁ k))) = ↑(n₁ (b i)) / ↑(#(block b (b i)))rw […, mul_div_mul_right _ _ hQ]The cancellation:
mul_div_mul_rightstates for , andhQis the . Every block but ‘s has now disappeared, and what is left is a within-block ratio of binomial coefficients.Goal⊢ ↑(#({T ∈ powersetCard (n₁ (b i)) (block b (b i)) | i ∈ T})) / ↑((#(block b (b i))).choose (n₁ (b i))) = ↑(n₁ (b i)) / ↑(#(block b (b i)))rcases Nat.eq_zero_or_pos (n₁ (b i)) with hzero | hposThe case split the informal proof also has to make:
choose_ratio_oneneeds , so the empty quota is handled separately. The theorem itself carries no lower bound on the quota, which is the point of doing this here rather than assuming it away.· rw [hzero, card_filter_mem_powersetCard_zero]; simp only [Nat.cast_zero, zero_div]The degenerate branch. With the quota
the only admissible within-block set is , which does not contain , so the numerator is — that iscard_filter_mem_powersetCard_zero. Both sides are then divided by something, whichNat.cast_zeroandzero_divfinish.· rw [card_filter_mem_powersetCard (mem_block_self b i) hpos]The real branch.
card_filter_mem_powersetCardcounts the -subsets of a set containing a fixed element of it: there are of them. Its two hypotheses aremem_block_self b i(a unit lies in its own block) andhpos.Goalhpos : n₁ (b i) > 0 ⊢ ↑((#(block b (b i)) - 1).choose (n₁ (b i) - 1)) / ↑((#(block b (b i))).choose (n₁ (b i))) = ↑(n₁ (b i)) / ↑(#(block b (b i)))exact_mod_cast choose_ratio_one hpos (h (b i))And that ratio is the quota over the block size, by
choose_ratio_onefrom chapter 7 — the same lemma that gave complete randomization its propensity.exact_mod_castcloses a goal that matches the lemma up toℕ → ℝcoercions, which is most of the friction in this library.
And now the punchline, which is a sentence of statistics rather than of Lean:
Why plain difference in means goes wrong
In chapter 8 the argument for
DiM_unbiased_completeRandomization was: on the support of the design the
denominators
Under blocking the first half of that still holds — the total number treated is
Proposition. Under stratified complete randomization with quotas
On every assignment the design can produce,
where the second equality replaces each restricted sum by an
indicator-weighted sum over the whole population. Two functions that agree
wherever the design puts positive probability have the same expectation, so we
may take the expectation of the right-hand side instead. It is a fixed linear
combination of the indicators, and by linearity of expectation with
theorem expect_DiM_stratified {b : Fin n → Fin K} {n₁ : Fin K → ℕ}
(h : ∀ k, n₁ k ≤ #(block b k)) (P : Population n) :
(stratifiedDesign b n₁ h).expect (DiM P)
= (∑ i, (stratifiedDesign b n₁ h).propensity i * P.y1 i) / ((∑ k, n₁ k : ℕ) : ℝ)
- (∑ i, (1 - (stratifiedDesign b n₁ h).propensity i) * P.y0 i)
/ ((n : ℝ) - ((∑ k, n₁ k : ℕ) : ℝ)) := by
have hle : ∑ k, n₁ k ≤ n := sum_n₁_le h
have hsup : ∀ z, (stratifiedDesign b n₁ h).prob z ≠ 0 →
DiM P z = (∑ i, Z z i * P.y1 i) / ((∑ k, n₁ k : ℕ) : ℝ)
- (∑ i, (1 - Z z i) * P.y0 i) / ((n : ℝ) - ((∑ k, n₁ k : ℕ) : ℝ)) := by
intro z hz
have hcard : #z = ∑ k, n₁ k := stratified_card_eq_of_prob_ne_zero hz
have hcompl : ((#zᶜ : ℕ) : ℝ) = (n : ℝ) - ((∑ k, n₁ k : ℕ) : ℝ) := by
rw [card_compl, hcard, Nat.cast_sub hle]
rw [DiM, sum_mem_eq_sum_Z_mul, sum_compl_eq_sum_one_sub_Z_mul, hcard, hcompl]
rw [(stratifiedDesign b n₁ h).expect_congr_of_support hsup]
simp only [Design.expect_sub, Design.expect_div_const, Design.expect_sum,
Design.expect_mul_const, Design.expect_Z, Design.expect_const]If
have hle : ∑ k, n₁ k ≤ n := sum_n₁_le hThe quotas fit inside the population — needed only so that the cast
from to can be taken without a truncated subtraction. It is the first warm-up exercise below.intro z hz; have hcard : #z = ∑ k, n₁ k := stratified_card_eq_of_prob_ne_zero hzNow
hsup, the “on the support” rewriting. Fix an assignment with nonzero probability;stratified_card_eq_of_prob_ne_zero(a core exercise below) says the overall number treated is the sum of the quotas — the fixed total that the first half of chapter 8’s argument needed.Goalhcard : #z = ∑ k, n₁ k ⊢ DiM P z = (∑ i, Z z i * P.y1 i) / ↑(∑ k, n₁ k) - (∑ i, (1 - Z z i) * P.y0 i) / (↑n - ↑(∑ k, n₁ k))have hcompl : ((#zᶜ : ℕ) : ℝ) = (n : ℝ) - ((∑ k, n₁ k : ℕ) : ℝ) := by rw [card_compl, hcard, Nat.cast_sub hle]And so is the control count.
card_complgives in ,hcardsubstitutes the total, andNat.cast_sub hleis what lets the truncated -subtraction be pushed through the cast as an honest real subtraction; that is the only workhledoes.rw [DiM, sum_mem_eq_sum_Z_mul, sum_compl_eq_sum_one_sub_Z_mul, hcard, hcompl]Unfold the estimator and rewrite it into indicator form:
sum_mem_eq_sum_Z_mulstates andsum_compl_eq_sum_one_sub_Z_mulits control-side twin . Thenhcardandhcomplreplace the two random denominators by constants. This is the displayed equation in the informal proof.rw [(stratifiedDesign b n₁ h).expect_congr_of_support hsup]expect_congr_of_supportis the formal version of “two functions that agree on the support have the same expectation”, so the goal is now about the indicator-form function, whose denominators are constants.Goal⊢ ((stratifiedDesign b n₁ h).expect fun z => (∑ i, Z z i * P.y1 i) / ↑(∑ k, n₁ k) - (∑ i, (1 - Z z i) * P.y0 i) / (↑n - ↑(∑ k, n₁ k))) = (∑ i, (stratifiedDesign b n₁ h).propensity i * P.y1 i) / ↑(∑ k, n₁ k) - (∑ i, (1 - (stratifiedDesign b n₁ h).propensity i) * P.y0 i) / (↑n - ↑(∑ k, n₁ k))simp only [Design.expect_sub, Design.expect_div_const, Design.expect_sum, Design.expect_mul_const, Design.expect_Z, Design.expect_const]Linearity, applied outside in:
expect_subsplits the difference of the two terms,expect_div_constpulls each constant denominator out of the expectation,expect_sumexchanges the expectation with the sum over units, andexpect_mul_constpulls each or out of its term. What is left inside is on the treated side, whichexpect_Zreplaces by — that is whatDesign.propensityis defined to be — and on the control side, whichexpect_subsplits a second time so thatexpect_constgives the andexpect_Zthe . Note that there are no positivity hypotheses anywhere: at both sides use Lean’s and the identity still holds.
That formula makes the bias concrete. Five units, blocks
def bEx : Fin 5 → Fin 2 := ![0, 0, 1, 1, 1]
def n₁Ex : Fin 2 → ℕ := ![1, 2]
theorem hEx : ∀ k, n₁Ex k ≤ #(block bEx k) := by decide
def PEx : Population 5 := ⟨fun _ => 0, fun i => if bEx i = 0 then 1 else 0⟩
theorem DiM_biased_stratified :
(stratifiedDesign bEx n₁Ex hEx).expect (DiM PEx) ≠ PEx.tau := by
have hprop : ∀ i : Fin 5, (stratifiedDesign bEx n₁Ex hEx).propensity i
= (n₁Ex (bEx i) : ℝ) / (#(block bEx (bEx i)) : ℝ) := stratified_propensity
have hN : (∑ k, n₁Ex k) = 3 := by decide
have hc : ∀ i : Fin 5, #(block bEx (bEx i)) = if bEx i = 0 then 2 else 3 := by decide
have hm : ∀ i : Fin 5, n₁Ex (bEx i) = if bEx i = 0 then 1 else 2 := by decide
have hp : ∀ i : Fin 5, (stratifiedDesign bEx n₁Ex hEx).propensity i
= ((if bEx i = 0 then 1 else 2 : ℕ) : ℝ) / ((if bEx i = 0 then 2 else 3 : ℕ) : ℝ) :=
fun i => by rw [hprop i, hc i, hm i]
have b0 : bEx 0 = 0 := by decide
have b1 : bEx 1 = 0 := by decide
have b2 : bEx 2 = 1 := by decide
have b3 : bEx 3 = 1 := by decide
have b4 : bEx 4 = 1 := by decide
rw [expect_DiM_stratified hEx PEx, hN, Population.tau_eq_mean_sub_mean, mean, mean,
Fin.sum_univ_five, Fin.sum_univ_five, Fin.sum_univ_five, Fin.sum_univ_five,
hp 0, hp 1, hp 2, hp 3, hp 4]
simp only [PEx, b0, b1, b2, b3, b4]
norm_numHere
The proof is a chain of small evaluations rather than an argument; it is worth walking once because every numeric example in this book has this shape.
have hprop : ∀ i : Fin 5, (stratifiedDesign bEx n₁Ex hEx).propensity i = (n₁Ex (bEx i) : ℝ) / (#(block bEx (bEx i)) : ℝ) := stratified_propensityInstantiate the propensity theorem at this design. Nothing here is specific to the example; it is the general theorem with the concrete
bExandn₁Exsubstituted in.have hN : (∑ k, n₁Ex k) = 3 := by decideThe total quota.
decideproves a decidable arithmetic fact aboutFinandFinsetby evaluating it in the kernel, which is exactly what a claim about five explicit units is.have hc : ∀ i : Fin 5, #(block bEx (bEx i)) = if bEx i = 0 then 2 else 3 := by decideThe block sizes, as a function of the unit rather than of the block index — two units in block
, three in block . Writing it as anifonbEx iis what lets the next few steps stay uniform in .have hm : ∀ i : Fin 5, n₁Ex (bEx i) = if bEx i = 0 then 1 else 2 := by decideLikewise the quotas: one treated unit in block
, two in block .have hp : ∀ i : Fin 5, (stratifiedDesign bEx n₁Ex hEx).propensity i = ((if bEx i = 0 then 1 else 2 : ℕ) : ℝ) / ((if bEx i = 0 then 2 else 3 : ℕ) : ℝ) := fun i => by rw [hprop i, hc i, hm i]Combine the three: each unit’s propensity is
in the first block and in the second. This is the disproportionality that will produce the bias, now in a form the arithmetic can chew on.have b0 : bEx 0 = 0 := by decideFive
decides of this shape record which block each unit is in. They are needed because theif bEx i = 0conditions above have to be resolved unit by unit beforenorm_numcan see numbers.rw [expect_DiM_stratified hEx PEx, hN, Population.tau_eq_mean_sub_mean, mean, mean, Fin.sum_univ_five, Fin.sum_univ_five, Fin.sum_univ_five, Fin.sum_univ_five, hp 0, hp 1, hp 2, hp 3, hp 4]The one substantive line.
expect_DiM_stratifiedreplaces the left-hand expectation by the closed form just proved,hNputs in for ,Population.tau_eq_mean_sub_meanrewrites as , the twomeans unfold to sums over , the fourFin.sum_univ_fives expand the four sums overFin 5into five explicit terms each, and the fivehps substitute the propensities.simp only [PEx, b0, b1, b2, b3, b4]Substitute the population’s two outcome vectors and resolve every
if bEx i = 0using the five block facts. After this the goal is a statement about explicit rational numbers.norm_numAnd
norm_numdoes that arithmetic: .
The cure is visible in the same formula. If the quotas are proportional —
The blocked estimator
The fix is the one every applied paper uses: estimate within each block, then average the block estimates with weights proportional to block size.
noncomputable def blockDiM (b : Fin n → Fin K) (P : Population n) (z : Assignment n)
(k : Fin K) : ℝ :=
(∑ i ∈ z ∩ block b k, P.y1 i) / (#(z ∩ block b k) : ℝ)
- (∑ i ∈ zᶜ ∩ block b k, P.y0 i) / (#(zᶜ ∩ block b k) : ℝ)
noncomputable def blockedDiM (b : Fin n → Fin K) (P : Population n) (z : Assignment n) : ℝ :=
∑ k, ((#(block b k) : ℝ) / (n : ℝ)) * blockDiM b P z kwhere on the support the two denominators are the known constants
And now the payoff is a repeat of chapter 8’s, one level up. On the support of
the design, the blocked estimator is Horvitz–Thompson: the within-block
denominators are the constants sum_eq_sum_block, i.e.
Finset.sum_fiberwise again) reassembles exactly the block-weighted difference
in means. That is blockedDiM_eq_HT_of_mem_support, the stretch exercise
posed at the end of the chapter. Given it, unbiasedness is a short
corollary:
TheoremThe blocked estimator is unbiased
If every block contains at least one treated and at least one control unit, then
The expectation is a finite sum over the
theorem blockedDiM_unbiased (P : Population n)
(hpos : ∀ k, 0 < n₁ k) (hlt : ∀ k, n₁ k < #(block b k)) :
(stratifiedDesign b n₁ h).expect (blockedDiM b P) = P.tau := by
have hprop : ∀ i : Fin n, (stratifiedDesign b n₁ h).propensity i
= (n₁ (b i) : ℝ) / (#(block b (b i)) : ℝ) := stratified_propensity
have hsupport : ∀ z, (stratifiedDesign b n₁ h).prob z ≠ 0 →
blockedDiM b P z = HT (stratifiedDesign b n₁ h) P z := fun z hzprob =>
blockedDiM_eq_HT_of_mem_support P hpos hlt (stratified_card_inter_block hzprob)
rw [(stratifiedDesign b n₁ h).expect_congr_of_support hsupport]
refine HT_unbiased _ P (fun i => ?_) (fun i => ?_)
· rw [hprop i]
exact div_pos (by exact_mod_cast hpos (b i))
(by exact_mod_cast block_card_pos b i)
· rw [hprop i]
exact (div_lt_one (by exact_mod_cast block_card_pos b i)).mpr
(by exact_mod_cast hlt (b i))On the support of the design the blocked estimator equals the
Horvitz–Thompson estimator, by the identity just described. Two estimators that
agree wherever the design puts positive probability have the same expectation.
And Horvitz–Thompson is unbiased for
Nothing about blocking is re-derived; all that changed was the propensity score.
have hprop : ∀ i : Fin n, (stratifiedDesign b n₁ h).propensity i = (n₁ (b i) : ℝ) / (#(block b (b i)) : ℝ) := stratified_propensityThe propensity theorem, named so that the two positivity goals at the end can rewrite with it. This
haverestates a fact already proved; it buys readability, not content.have hsupport : ∀ z, (stratifiedDesign b n₁ h).prob z ≠ 0 → blockedDiM b P z = HT (stratifiedDesign b n₁ h) P z := fun z hzprob => blockedDiM_eq_HT_of_mem_support P hpos hlt (stratified_card_inter_block hzprob)The “on the support” fact, assembled from two pieces: the stretch exercise
blockedDiM_eq_HT_of_mem_supportneeds to know that every block hits its quota on , andstratified_card_inter_block— the exact-balance theorem above — extracts that from the mere assumption that has nonzero probability.rw [(stratifiedDesign b n₁ h).expect_congr_of_support hsupport]Swap the estimator for Horvitz–Thompson under the expectation. The goal is now the general Horvitz–Thompson theorem’s conclusion verbatim.
Goalhsupport : ∀ (z : Assignment n), (stratifiedDesign b n₁ h).prob z ≠ 0 → blockedDiM b P z = HT (stratifiedDesign b n₁ h) P z ⊢ (stratifiedDesign b n₁ h).expect (HT (stratifiedDesign b n₁ h) P) = P.taurefine HT_unbiased _ P (fun i => ?_) (fun i => ?_)Apply
HT_unbiasedfrom chapter 8 and leave its two hypotheses open, one per?_: the propensity is positive, and it is below one. Both are now goals about a single unit .Goali : Fin n ⊢ 0 < (stratifiedDesign b n₁ h).propensity irw [hprop i]Replace the propensity by the ratio. In the first goal this leaves
; in the second, after the same rewrite, .Goali : Fin n ⊢ 0 < ↑(n₁ (b i)) / ↑(#(block b (b i)))exact div_pos (by exact_mod_cast hpos (b i)) (by exact_mod_cast block_card_pos b i)Positivity:
div_poswants both numerator and denominator positive.hpos (b i)gives andblock_card_pos b igives because a unit lies in its own block;exact_mod_castmoves each from to .exact (div_lt_one (by exact_mod_cast block_card_pos b i)).mpr (by exact_mod_cast hlt (b i))And below one:
div_lt_one, given a positive denominator, says iff , andhlt (b i)is . This is where the “a control in every block” hypothesis finally earns its place — it is also handed on to the stretch exercise in step 2 — and the callout below is about what it costs.
What is missing: the variance
The curriculum promised the variance of the blocked estimator, and the library does not have it. Informally it is
i.e. Neyman’s formula from chapter 9 applied inside each
block and combined with squared weights, with no cross-block covariance
terms. Every ingredient is already available, and I want to be precise about
what is unproven: the missing step is the second-order inclusion probabilities
of the blocked design. Within a block, card_stratSupport_filter_subset with choose_ratio_two
for the within-block case. What was not written is the case analysis:
This is a genuinely good exercise for a reader who has got this far, and it is the one place in this chapter where you should not believe a formula because I wrote it down.
Cluster randomization
Now the map c : Fin n → Fin K groups units into clusters, and treatment is
assigned to whole clusters. The unit-level design is the pushforward of a
cluster-level design.
def clusterAssignment (c : Fin n → Fin K) (T : Assignment K) : Assignment n :=
univ.filter (fun i => c i ∈ T)
noncomputable def clusterDesign (c : Fin n → Fin K) (Dc : Design K) : Design n :=
Dc.map (clusterAssignment c)Everything else follows from one identity: a unit’s treatment indicator is its
cluster’s treatment indicator,
theorem Z_clusterAssignment (c : Fin n → Fin K) (T : Assignment K) (i : Fin n) :
Z (clusterAssignment c T) i = Z T (c i) := by
unfold Z
by_cases hi : c i ∈ T <;> simp [hi]unfold ZReplace both indicators by their definition,
if i ∈ z then 1 else 0. The goal is now an equality of twoifs, whose conditions are “unit is treated” and “cluster is treated”.Goalc : Fin n → Fin K T : Assignment K i : Fin n ⊢ (if i ∈ clusterAssignment c T then 1 else 0) = if c i ∈ T then 1 else 0by_cases hi : c i ∈ T <;> simp [hi]Split on whether
‘s cluster is treated. In each branchsimp [hi]resolves both conditions at once, becausemem_clusterAssignment— a@[simp]lemma — saysclusterAssignment c Tiff , which is the same condition. Both branches are then or .
From it, the defining property of cluster randomization: two units of the same cluster always get the same treatment.
theorem cluster_all_or_nothing {c : Fin n → Fin K} {Dc : Design K} {z : Assignment n}
(hz : (clusterDesign c Dc).prob z ≠ 0) {i j : Fin n} (hij : c i = c j) :
Z z i = Z z j := by
obtain ⟨T, hT, -⟩ := Design.exists_of_map_prob_ne_zero hz
subst hT
rw [Z_clusterAssignment, Z_clusterAssignment, hij]obtain ⟨T, hT, -⟩ := Design.exists_of_map_prob_ne_zero hzDesign.exists_of_map_prob_ne_zerosays an assignment in the support of a pushforward comes from some upstream assignment: if the fibre sum is nonzero then some term of it is.obtainnames that cluster-level assignmentTand the equationhT : clusterAssignment c T = z; the third component, thatTitself has nonzero probability, is discarded with-because we do not need it.Goalhij : c i = c j T : Assignment K hT : clusterAssignment c T = z ⊢ Z z i = Z z jsubst hTReplace
zbyclusterAssignment c Teverywhere. This is the step that makes the argument possible: the hypothesis only said was in the support, and now is displayed as coming from a cluster-level assignment.Goalhij : c i = c j ⊢ Z (clusterAssignment c T) i = Z (clusterAssignment c T) jrw [Z_clusterAssignment, Z_clusterAssignment, hij]Both indicators are now cluster-level by the previous lemma, applied once on each side, and
hij : c i = c jmakes the two cluster indices the same. The goal closes as a reflexivity.
Proposition. Under cluster randomization, a unit’s propensity is its
cluster’s:
By the change-of-variables formula, the expectation of any statistic of the
unit-level assignment is the expectation of the same statistic composed with
“treated clusters
theorem cluster_propensity (c : Fin n → Fin K) (Dc : Design K) (i : Fin n) :
(clusterDesign c Dc).propensity i = Dc.propensity (c i) := by
rw [clusterDesign, Design.map_propensity]
exact Dc.expect_congr fun T => Z_clusterAssignment c T irw [clusterDesign, …]Unfold
clusterDesignto what it is: the pushforward of the cluster-level design alongclusterAssignment c. Nothing about clusters is left in the goal except that map.Goal⊢ (Design.map (clusterAssignment c) Dc).propensity i = Dc.propensity (c i)rw […, Design.map_propensity]Design.map_propensityis the change-of-variables formula specialized to the statistic : the propensity of a pushforward is an expectation computed upstream — the average, over cluster-level assignments , of the indicator that unit is treated when the treated clusters are exactly .Goal⊢ (Dc.expect fun w => Z (clusterAssignment c w) i) = Dc.propensity (c i)exact Dc.expect_congr fun T => Z_clusterAssignment c T iDc.expect_congrsays two statistics that agree at every assignment have the same expectation, so it is enough to compare integrands: at each , the previous lemma turns into , and the expectation of that is the cluster’s propensity by definition.
There is no dilution and no inflation. So a cluster design has exactly the same first-order inclusion probabilities as the cluster-level design it came from, and unbiasedness transfers with no new counting at all:
theorem clusterHT_unbiased (c : Fin n → Fin K) {K₁ : ℕ} (hle : K₁ ≤ K) (P : Population n)
(hpos : 0 < K₁) (hlt : K₁ < K) :
(clusterDesign c (completeRandomization K K₁ hle)).expect
(HT (clusterDesign c (completeRandomization K K₁ hle)) P) = P.tau := by
have hKR : (0 : ℝ) < (K : ℝ) := by exact_mod_cast Nat.lt_of_lt_of_le hpos hle
have hK1R : (0 : ℝ) < (K₁ : ℝ) := by exact_mod_cast hpos
have hltR : (K₁ : ℝ) < (K : ℝ) := by exact_mod_cast hlt
have hprop : ∀ i : Fin n,
(clusterDesign c (completeRandomization K K₁ hle)).propensity i = (K₁ : ℝ) / (K : ℝ) :=
fun i => by rw [cluster_propensity, completeRandomization_propensity]
refine HT_unbiased _ P (fun i => ?_) (fun i => ?_)
· rw [hprop i]; exact div_pos hK1R hKR
· rw [hprop i]; exact (div_lt_one hKR).mpr hltRhave hKR : (0 : ℝ) < (K : ℝ) := by exact_mod_cast Nat.lt_of_lt_of_le hpos hleThree real-number facts first, because
div_posanddiv_lt_onebelow want inequalities in and the hypotheses are in . This one is , chained from and .have hK1R : (0 : ℝ) < (K₁ : ℝ) := by exact_mod_cast hpos : at least one cluster is treated.have hltR : (K₁ : ℝ) < (K : ℝ) := by exact_mod_cast hltAnd
: at least one cluster is not. These are the cluster-level analogues of “both groups non-empty”, stated about clusters rather than units: a first hint that it is the number of clusters that matters here, not the number of units.GoalhKR : 0 < ↑K hK1R : 0 < ↑K₁ hltR : ↑K₁ < ↑K ⊢ (clusterDesign c (completeRandomization K K₁ hle)).expect (HT (clusterDesign c (completeRandomization K K₁ hle)) P) = P.tauhave hprop : ∀ i : Fin n, (clusterDesign c (completeRandomization K K₁ hle)).propensity i = (K₁ : ℝ) / (K : ℝ) := fun i => by rw [cluster_propensity, completeRandomization_propensity]The whole content of the theorem, in one line:
cluster_propensityreduces a unit’s propensity to its cluster’s, andcompleteRandomization_propensity(chapter 7) evaluates that as . Every unit has the same propensity, whatever the cluster sizes are.refine HT_unbiased _ P (fun i => ?_) (fun i => ?_)Apply the general theorem and leave its two side conditions open.
· rw [hprop i]; exact div_pos hK1R hKR from the two positivity facts.· rw [hprop i]; exact (div_lt_one hKR).mpr hltRAnd
from with .
Read that proof and notice what is absent. There is no combinatorics, no
Nat.choose, no bijection. cluster_propensity reduces the propensity to
completeRandomization_propensity, which was proved in chapter 7, and
HT_unbiased finishes. Clustering costs precision, never validity — and the
precision loss is invisible at this level, because it lives entirely in the
joint propensities: two units of the same cluster have
Exercises
ExerciseThe quotas fit (warm-up)
A blocked design cannot ask for more treated units than exist: the block quotas add up to at most the population size.
theorem sum_n₁_le {b : Fin n → Fin K} {n₁ : Fin K → ℕ} (h : ∀ k, n₁ k ≤ #(block b k)) :
∑ k, n₁ k ≤ n := by
sorryHint
A calc with two links: Finset.sum_le_sum (fed fun k _ => h k, the
hypothesis at each block) bounds sum_card_block says that sum is
Show solution
theorem sum_n₁_le {b : Fin n → Fin K} {n₁ : Fin K → ℕ} (h : ∀ k, n₁ k ≤ #(block b k)) :
∑ k, n₁ k ≤ n := by
calc ∑ k, n₁ k ≤ ∑ k, #(block b k) := Finset.sum_le_sum fun k _ => h k
_ = n := sum_card_block bExerciseCounting the survivors (warm-up)
The five-unit blocked design of the biased example has
theorem card_stratSupport_example : #(stratSupport bEx n₁Ex) = 6 := by
sorryHint
rw [card_stratSupport] turns the count into a product of binomial
coefficients; decide evaluates it in the kernel.
Show solution
theorem card_stratSupport_example : #(stratSupport bEx n₁Ex) = 6 := by
rw [card_stratSupport]
decideExerciseProportional allocation makes the propensity constant (core)
The hypothesis ∀ k, n₁ k * n = (∑ l, n₁ l) * #(block b k) is the
cross-multiplied form of hbal that the stretch exercise below assumes.
theorem stratified_propensity_of_proportional {b : Fin n → Fin K} {n₁ : Fin K → ℕ}
(h : ∀ k, n₁ k ≤ #(block b k)) (hprop : ∀ k, n₁ k * n = (∑ l, n₁ l) * #(block b k))
(i : Fin n) :
(stratifiedDesign b n₁ h).propensity i = ((∑ k, n₁ k : ℕ) : ℝ) / (n : ℝ) := by
sorryHint
Rewrite with stratified_propensity i, then div_eq_div_iff — whose two side
conditions are the positivity of block_card_pos
and Fin.pos i cast to ℝ, since a unit lives in a nonempty block and in a
nonempty population — leaves the cross-multiplied form, which is hprop (b i) up
to ℕ → ℝ coercions; exact_mod_cast closes it.
Show solution
theorem stratified_propensity_of_proportional {b : Fin n → Fin K} {n₁ : Fin K → ℕ}
(h : ∀ k, n₁ k ≤ #(block b k)) (hprop : ∀ k, n₁ k * n = (∑ l, n₁ l) * #(block b k))
(i : Fin n) :
(stratifiedDesign b n₁ h).propensity i = ((∑ k, n₁ k : ℕ) : ℝ) / (n : ℝ) := by
have hN : (0 : ℝ) < (#(block b (b i)) : ℝ) := by exact_mod_cast block_card_pos b i
have hn : (0 : ℝ) < (n : ℝ) := by exact_mod_cast Fin.pos i
rw [stratified_propensity i, div_eq_div_iff hN.ne' hn.ne']
exact_mod_cast hprop (b i)ExerciseThe price of clustering, in one covariance (core)
For two units of the same cluster,
theorem cluster_cov_Z_same (c : Fin n → Fin K) (Dc : Design K) {i j : Fin n} (hij : c i = c j) :
(clusterDesign c Dc).cov (fun z => Z z i) (fun z => Z z j)
= (clusterDesign c Dc).propensity i * (1 - (clusterDesign c Dc).propensity i) := by
sorryHint
Design.cov_eq_expect_mul_sub_mul turns the covariance into
have is closed by rfl; then cluster_propensity (twice, with hij) makes
the two propensities agree, cluster_jointPropensity_same replaces
ring finishes.
Show solution
theorem cluster_cov_Z_same (c : Fin n → Fin K) (Dc : Design K) {i j : Fin n} (hij : c i = c j) :
(clusterDesign c Dc).cov (fun z => Z z i) (fun z => Z z j)
= (clusterDesign c Dc).propensity i * (1 - (clusterDesign c Dc).propensity i) := by
have hjoint : (clusterDesign c Dc).expect (fun z => Z z i * Z z j)
= (clusterDesign c Dc).jointPropensity i j := rfl
have hi : (clusterDesign c Dc).expect (fun z => Z z i)
= (clusterDesign c Dc).propensity i := rfl
have hj : (clusterDesign c Dc).expect (fun z => Z z j)
= (clusterDesign c Dc).propensity j := rfl
have hp : (clusterDesign c Dc).propensity j = (clusterDesign c Dc).propensity i := by
rw [cluster_propensity, cluster_propensity, hij]
rw [Design.cov_eq_expect_mul_sub_mul, hjoint, hi, hj, hp,
cluster_jointPropensity_same c Dc hij]
ringExerciseWhen difference in means survives blocking (stretch)
The converse reading of the expectation formula: if every unit has the same
propensity DiM by itself; unequal treated fractions do.
theorem expect_DiM_stratified_of_balanced {b : Fin n → Fin K} {n₁ : Fin K → ℕ}
(h : ∀ k, n₁ k ≤ #(block b k)) (P : Population n)
(hpos : 0 < ∑ k, n₁ k) (hlt : ∑ k, n₁ k < n)
(hbal : ∀ i, (stratifiedDesign b n₁ h).propensity i = ((∑ k, n₁ k : ℕ) : ℝ) / (n : ℝ)) :
(stratifiedDesign b n₁ h).expect (DiM P) = P.tau := by
sorryHint
Rewrite with expect_DiM_stratified and Population.tau_eq_mean_sub_mean, then
simp only with hbal, ← Finset.mul_sum (to pull the constant propensity out
of each sum) and mean, and let field_simp clear the denominators — which is
why you need haves first.
Show solution
theorem expect_DiM_stratified_of_balanced {b : Fin n → Fin K} {n₁ : Fin K → ℕ}
(h : ∀ k, n₁ k ≤ #(block b k)) (P : Population n)
(hpos : 0 < ∑ k, n₁ k) (hlt : ∑ k, n₁ k < n)
(hbal : ∀ i, (stratifiedDesign b n₁ h).propensity i = ((∑ k, n₁ k : ℕ) : ℝ) / (n : ℝ)) :
(stratifiedDesign b n₁ h).expect (DiM P) = P.tau := by
have hnR : (0 : ℝ) < (n : ℝ) := by exact_mod_cast Nat.lt_of_le_of_lt (Nat.zero_le _) hlt
have hN1 : (0 : ℝ) < ((∑ k, n₁ k : ℕ) : ℝ) := by exact_mod_cast hpos
have hN0 : (0 : ℝ) < (n : ℝ) - ((∑ k, n₁ k : ℕ) : ℝ) := by
have : ((∑ k, n₁ k : ℕ) : ℝ) < (n : ℝ) := by exact_mod_cast hlt
linarith
rw [expect_DiM_stratified h P, P.tau_eq_mean_sub_mean]
simp only [hbal, ← Finset.mul_sum, mean]
field_simpSeven more come from the library itself, which poses its own exercises inline.
ExerciseDistinct blocks are disjoint (warm-up)
Distinct blocks are disjoint — a unit has only one value of b i. This is half
of “the blocks partition the population”.
theorem block_disjoint (b : Fin n → Fin K) {k l : Fin K} (hkl : k ≠ l) :
Disjoint (block b k) (block b l) := by
sorryHint
Finset.disjoint_left reduces disjointness to: no i is in both. A unit in
block b k has b i = k (mem_block), so being in two blocks makes k = l,
contradicting hkl.
Show solution
theorem block_disjoint (b : Fin n → Fin K) {k l : Fin K} (hkl : k ≠ l) :
Disjoint (block b k) (block b l) := by
refine Finset.disjoint_left.mpr fun i hik hil => ?_
exact hkl ((mem_block.mp hik).symm.trans (mem_block.mp hil))ExerciseThe block sizes add up (core)
The other half: the block sizes add up to the population size,
theorem sum_card_block (b : Fin n → Fin K) : ∑ k, #(block b k) = n := by
sorryHint
Finset.card_eq_sum_card_fiberwise is the statement that counting a set by
fibres of a function gives the same answer; instantiate it at f := b with
s = t = univ, then simpa only with Finset.card_univ, Fintype.card_fin and
block lines the two sides up (the lemma states the equation the other way
round, so use .symm).
Show solution
theorem sum_card_block (b : Fin n → Fin K) : ∑ k, #(block b k) = n := by
have hcount := Finset.card_eq_sum_card_fiberwise (f := b) (s := (univ : Finset (Fin n)))
(t := (univ : Finset (Fin K))) (fun i _ => Finset.mem_univ (b i))
simpa only [Finset.card_univ, Fintype.card_fin, block] using hcount.symmExerciseHorvitz–Thompson under a blocked design (warm-up)
Under a blocked design the Horvitz–Thompson estimator is unbiased too. It is the
same statement as blockedDiM_unbiased in disguise, since the two estimators
agree on the support.
theorem stratifiedHT_unbiased (P : Population n)
(hpos : ∀ k, 0 < n₁ k) (hlt : ∀ k, n₁ k < #(block b k)) :
(stratifiedDesign b n₁ h).expect (HT (stratifiedDesign b n₁ h) P) = P.tau := by
sorryHint
The last two lines of blockedDiM_unbiased, run on their own. refine HT_unbiased _ P (fun i => ?_) (fun i => ?_), then in each goal rw [stratified_propensity i]: div_pos, and div_lt_one, with the hpos, hlt, block_card_pos) discharged by exact_mod_cast.
Show solution
theorem stratifiedHT_unbiased (P : Population n)
(hpos : ∀ k, 0 < n₁ k) (hlt : ∀ k, n₁ k < #(block b k)) :
(stratifiedDesign b n₁ h).expect (HT (stratifiedDesign b n₁ h) P) = P.tau := by
refine HT_unbiased _ P (fun i => ?_) (fun i => ?_)
· rw [stratified_propensity i]
exact div_pos (by exact_mod_cast hpos (b i)) (by exact_mod_cast block_card_pos b i)
· rw [stratified_propensity i]
exact (div_lt_one (by exact_mod_cast block_card_pos b i)).mpr
(by exact_mod_cast hlt (b i))ExerciseThe total treated count is fixed too (core)
On the support, the overall number treated is
theorem stratified_card_eq_of_prob_ne_zero {z : Assignment n}
(hz : (stratifiedDesign b n₁ h).prob z ≠ 0) : #z = ∑ k, n₁ k := by
sorryHint
First establish ∑ k, #(z ∩ block b k) = #z as a have: it is
Finset.sum_fiberwise applied to the indicator fun i => if i ∈ z then 1 else 0, cleaned up by simpa only with Finset.sum_ite_mem,
Finset.card_eq_sum_ones, block, Finset.univ_inter and Finset.inter_comm.
Rewrite the goal with it backwards, and Finset.sum_congr rfl with
stratified_card_inter_block hz k handles each block.
Show solution
theorem stratified_card_eq_of_prob_ne_zero {z : Assignment n}
(hz : (stratifiedDesign b n₁ h).prob z ≠ 0) : #z = ∑ k, n₁ k := by
have hfib : ∑ k, #(z ∩ block b k) = #z := by
have := Finset.sum_fiberwise (M := ℕ) univ b (fun i => if i ∈ z then 1 else 0)
simpa only [Finset.sum_ite_mem, Finset.card_eq_sum_ones, block, Finset.univ_inter,
Finset.inter_comm z] using this
rw [← hfib]
exact Finset.sum_congr rfl fun k _ => stratified_card_inter_block hz kExerciseJoint propensities pass to the clusters (warm-up)
The joint propensity of two units is the joint propensity of their two clusters: nothing is lost or gained by passing to the unit level.
theorem cluster_jointPropensity_diff (c : Fin n → Fin K) (Dc : Design K) (i j : Fin n) :
(clusterDesign c Dc).jointPropensity i j = Dc.jointPropensity (c i) (c j) := by
sorryHint
Unfold Design.jointPropensity and clusterDesign, push the expectation through
the pushforward with Design.expect_map, unfold Design.jointPropensity again
on the cluster side, and finish with Dc.expect_congr and two rewrites by
Z_clusterAssignment.
Show solution
theorem cluster_jointPropensity_diff (c : Fin n → Fin K) (Dc : Design K) (i j : Fin n) :
(clusterDesign c Dc).jointPropensity i j = Dc.jointPropensity (c i) (c j) := by
rw [Design.jointPropensity, clusterDesign, Design.expect_map, Design.jointPropensity]
exact Dc.expect_congr fun T => by rw [Z_clusterAssignment, Z_clusterAssignment]ExerciseTwo units of one cluster (core)
theorem cluster_jointPropensity_same (c : Fin n → Fin K) (Dc : Design K) {i j : Fin n}
(hij : c i = c j) :
(clusterDesign c Dc).jointPropensity i j = (clusterDesign c Dc).propensity i := by
sorryHint
The same rewrite chain as the previous exercise — cluster_propensity,
Design.jointPropensity, clusterDesign, Design.expect_map, then
Design.propensity on the cluster side — with one extra step inside
Dc.expect_congr: after two Z_clusterAssignment rewrites and hij turn both
indicators into the same one, Z_mul_self says an indicator times itself is
itself.
Show solution
theorem cluster_jointPropensity_same (c : Fin n → Fin K) (Dc : Design K) {i j : Fin n}
(hij : c i = c j) :
(clusterDesign c Dc).jointPropensity i j = (clusterDesign c Dc).propensity i := by
rw [cluster_propensity, Design.jointPropensity, clusterDesign, Design.expect_map,
Design.propensity]
exact Dc.expect_congr fun T => by
rw [Z_clusterAssignment, Z_clusterAssignment, hij, Z_mul_self]ExerciseThe blocked estimator is Horvitz–Thompson (stretch)
The one that carries the chapter: on the support of the blocked design, the
blocked estimator is the Horvitz–Thompson estimator. It is the blocked
analogue of DiM_eq_HT_of_card, and the lemma blockedDiM_unbiased is a
corollary of.
theorem blockedDiM_eq_HT_of_mem_support (P : Population n)
(hpos : ∀ k, 0 < n₁ k) (hlt : ∀ k, n₁ k < #(block b k))
{z : Assignment n} (hz : ∀ k, #(z ∩ block b k) = n₁ k) :
blockedDiM b P z = HT (stratifiedDesign b n₁ h) P z := by
sorryHint
Work one summand at a time first: for i ∈ block b k, stratified_propensity i
and mem_block turn the Horvitz–Thompson summand into Z z i * y1 i / n₁ k - (1 - Z z i) * y0 i / (N_k - n₁ k), with
div_div_eq_mul_div, one_sub_div and field_simp doing the algebra. Summing
that over a block (Finset.mul_sum, Finset.sum_sub_distrib, Finset.sum_div,
sum_inter_eq_sum_Z_mul, sum_compl_inter_eq_sum_one_sub_Z_mul) gives
N_k * blockDiM b P z k, once hz k and card_compl_inter with Nat.cast_sub
identify the two denominators. Then unfold HT blockedDiM, regroup the
Horvitz–Thompson sum with ← sum_eq_sum_block b, and ring closes each block’s
term; the nonvanishing of haves throughout.
Show solution
theorem blockedDiM_eq_HT_of_mem_support (P : Population n)
(hpos : ∀ k, 0 < n₁ k) (hlt : ∀ k, n₁ k < #(block b k))
{z : Assignment n} (hz : ∀ k, #(z ∩ block b k) = n₁ k) :
blockedDiM b P z = HT (stratifiedDesign b n₁ h) P z := by
have hNne : ∀ k, ((#(block b k) : ℕ) : ℝ) ≠ 0 := fun k =>
Nat.cast_ne_zero.mpr (Nat.lt_of_le_of_lt (Nat.zero_le _) (hlt k)).ne'
have hn1ne : ∀ k, ((n₁ k : ℕ) : ℝ) ≠ 0 := fun k => Nat.cast_ne_zero.mpr (hpos k).ne'
have hdiff : ∀ k, ((#(block b k) : ℕ) : ℝ) - ((n₁ k : ℕ) : ℝ) ≠ 0 := fun k => by
have : ((n₁ k : ℕ) : ℝ) < ((#(block b k) : ℕ) : ℝ) := by exact_mod_cast hlt k
linarith
have hcompl : ∀ k, ((#(zᶜ ∩ block b k) : ℕ) : ℝ)
= ((#(block b k) : ℕ) : ℝ) - ((n₁ k : ℕ) : ℝ) := by
intro k
rw [card_compl_inter, hz k, Nat.cast_sub ((hlt k).le)]
have hterm : ∀ (k : Fin K) (i : Fin n), i ∈ block b k →
Z z i * P.y1 i / (stratifiedDesign b n₁ h).propensity i
- (1 - Z z i) * P.y0 i / (1 - (stratifiedDesign b n₁ h).propensity i)
= ((#(block b k) : ℕ) : ℝ)
* (Z z i * P.y1 i / (n₁ k : ℝ)
- (1 - Z z i) * P.y0 i / (((#(block b k) : ℕ) : ℝ) - (n₁ k : ℝ))) := by
intro k i hi
have hbi : b i = k := mem_block.mp hi
rw [stratified_propensity i, hbi]
rw [div_div_eq_mul_div, one_sub_div (hNne k)]
field_simp
have hblock : ∀ k : Fin K,
∑ i ∈ block b k, (Z z i * P.y1 i / (stratifiedDesign b n₁ h).propensity i
- (1 - Z z i) * P.y0 i / (1 - (stratifiedDesign b n₁ h).propensity i))
= ((#(block b k) : ℕ) : ℝ) * blockDiM b P z k := by
intro k
rw [Finset.sum_congr rfl fun i hi => hterm k i hi, ← Finset.mul_sum,
Finset.sum_sub_distrib, ← Finset.sum_div, ← Finset.sum_div,
← sum_inter_eq_sum_Z_mul, ← sum_compl_inter_eq_sum_one_sub_Z_mul]
rw [blockDiM, hz k, hcompl k]
unfold HT blockedDiM
rw [← sum_eq_sum_block b, Finset.sum_congr rfl fun k _ => hblock k, Finset.mul_sum]
exact Finset.sum_congr rfl fun k _ => by ringReading autoformalized Lean
Three ways to get this chapter wrong while compiling. The first two are statements that look like the results above and are not; the third is a statement that is true and says nothing.
The blocking function that does nothing
noncomputable def stratifiedDesign' (b : Fin n → Fin K) (n₁ : Fin K → ℕ)
(h : ∀ k, n₁ k ≤ #(block b k)) : Design n where
prob z := if #z = ∑ k, n₁ k then (n.choose (∑ k, n₁ k) : ℝ)⁻¹ else 0
nonneg := ...
sum_one := ...
theorem stratified'_propensity (i : Fin n) :
(stratifiedDesign' b n₁ h).propensity i = (∑ k, n₁ k : ℝ) / n := ...
Fill in the two elided proofs and everything type-checks, the theorem is true,
and b appears in the signature three times. It appears nowhere in the body
of the definition. The support constrains only the total number treated, so
this is completeRandomization n (∑ k, n₁ k) with a blocking function bolted
on for decoration — and the
propensity comes out constant, which is the tell. A real blocked design has
∀ k, #(z ∩ block b k) = n₁ k, one constraint per block; the chapter’s
stratified_card_eq_of_prob_ne_zero shows that the total-count condition is a
consequence of it, never the other way round.
Clusters that pretend to be independent
theorem cluster_jointPropensity' (c : Fin n → Fin K) (Dc : Design K) (i j : Fin n) :
(clusterDesign c Dc).jointPropensity i j
= Dc.propensity (c i) * Dc.propensity (c j) := ...
This is the statement everybody wants to be true, because it is what makes the
variance of a cluster experiment look like the variance of a unit experiment.
It is false, and it is false in exactly the case that matters: when
cluster_jointPropensity_same and cluster_jointPropensity_diff
— and neither claims factorization; factorization is a property of the
cluster-level design, and only bernoulli_jointPropensity actually has it.
A hypothesis that is satisfied by nothing
Finally, a milder version of the same disease. Consider
blockedDiM_unbiased’s hypothesis ∀ k, n₁ k < #(block b k). If any block is
empty this says n₁ k < 0 in ℕ, which is unsatisfiable, so for such a b the
theorem is vacuously true and tells you nothing. The theorem is not wrong — an
empty stratum identifies nothing, and refusing to talk about it is correct — but
“this compiles” and “this applies to my experiment” are different claims, and
the second one requires you to check that the hypotheses can be met by the
design you actually ran.