Causal inferenceChapter 11
Regression as linear algebra
In a finite population an OLS coefficient is an algebraic identity about the observed data. The coefficient on a treatment dummy is the difference in means, group dummies return group means, and the fully interacted regression is two within-group fits.
Lean source compiled in CI: lean/MrCLean/Regression.lean, lean/MrCLean/Chapters/Ch11Regression.lean
Contents
At the end of an experiment you run lm(y ~ z) and report the coefficient on
z. The textbook that taught you least squares introduced it as a model,
None of them are available here. In a finite population the potential outcomes
are fixed numbers; there is no
It is a function of the numbers the experiment produced. Given
Regression coefficients do not have properties. Estimators have properties. Regression coefficients inherit them by being equal to estimators.
Matrices in Mathlib
A design matrix is Matrix (Fin n) (Fin k) ℝ: Fin n → Fin k → ℝ; the type Matrix exists so that * can mean matrix
multiplication rather than pointwise multiplication. Matrix.of is the
relabelling that takes you from the function to the matrix, and it is why
simpleMatrix below is written the way it is.
Three notations do all the work. Xᵀ is the transpose. X * Y is matrix
multiplication, so Xᵀ * X is the Gram matrix — the sums of squares and cross
products, X *ᵥ v is Matrix.mulVec, the matrix–vector
product, whose
/-- Three facts about Mathlib's `Matrix` that a statistician needs before reading any of the
regression file.
* `Xᵀ` is the transpose and `X * Y` is matrix multiplication, so `Xᵀ * X` is the Gram matrix,
whose `(j, k)` entry is the inner product of columns `j` and `k` — the "sums of squares and
cross products" matrix.
* `X *ᵥ v` (`Matrix.mulVec`) is the matrix–vector product, whose `i`-th entry is the fitted
value `∑ j, X i j * v j`.
* `*ᵥ` binds **tighter** than `*`. `Xᵀ * X *ᵥ β` therefore parses as `Xᵀ * (X *ᵥ β)`, which
is a matrix times a vector and does not even type-check. The Gram matrix applied to a
coefficient vector must be written `(Xᵀ * X) *ᵥ β`, and that is exactly what `IsOLS` says. -/
theorem gram_apply {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (j l : Fin k) :
(Xᵀ * X) j l = ∑ i, X i j * X i l := by
simp [Matrix.mul_apply, Matrix.transpose_apply]
theorem mulVec_assoc {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (β : Fin k → ℝ) :
(Xᵀ * X) *ᵥ β = Xᵀ *ᵥ (X *ᵥ β) :=
(Matrix.mulVec_mulVec β Xᵀ X).symm
theorem isOLS_unfold {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (y : Fin n → ℝ) (β : Fin k → ℝ) :
IsOLS X y β ↔ (Xᵀ * X) *ᵥ β = Xᵀ *ᵥ y := Iff.rflIff.rfl in the last of those is worth a second look: it says the two sides are
the same proposition up to unfolding definitions, so no proof is needed. It is
Lean’s way of saying “that is literally the definition”.
The precedence trap
*ᵥ binds more tightly than *. The Gram matrix applied to a coefficient
vector must therefore be parenthesized:
-- What you mean:
(Xᵀ * X) *ᵥ β
-- What `Xᵀ * X *ᵥ β` actually parses as, and it does not type-check:
Xᵀ * (X *ᵥ β) -- a matrix times a vector; `HMul` has no such instance
This particular mistake is benign, because it is a type error and Lean stops.
The dangerous version of the same confusion is the one that does type-check —
when Xᵀ *ᵥ X *ᵥ β
and gets an expression that means something, just not the thing they wanted.
Check the parenthesization of every *ᵥ you read.
Small matrices, and one determinant
Literal vectors are ![a, b, …] and literal matrices are !![a, b; c, d],
given row by row. A Matrix.det_fin_two_of, and it
is the only determinant this chapter needs.
/-- Literal matrices and vectors. `![a, b, …]` is a vector (a function out of `Fin k`) and
`!![a, b; c, d]` is a matrix given row by row; `Matrix.of` relabels a plain function
`Fin n → Fin k → ℝ` as a matrix, which is how `simpleMatrix` is built.
The two computations below are the entire content of `simpleMatrix_gram_det` in miniature:
a `2 × 2` determinant is `ad - bc`, and `Matrix.det_fin_two_of` is the lemma that says so. -/
theorem literal_mulVec : (!![(1 : ℝ), 2; 3, 4]) *ᵥ ![1, 1] = ![3, 7] := by
funext i
fin_cases i <;> simp [Matrix.mulVec, dotProduct, Fin.sum_univ_two] <;> norm_num
theorem literal_det : (!![(1 : ℝ), 2; 3, 4]).det = -2 := by
norm_num [Matrix.det_fin_two_of]The inverse is total, and that is a trap
Mathlib’s Matrix.inv is a total function. Every square matrix has an
inverse in the sense that A⁻¹ always elaborates; when A.det is not a unit,
A⁻¹ is defined to be the zero matrix. This is the same design decision as
ExerciseEveryone treated: the estimate is zero
Take z to be all of Finset.univ. Then the treatment column equals the
intercept column, the Gram matrix is singular, and ols is the zero vector.
simpleMatrix_gram_det gives you the determinant;
Matrix.nonsing_inv_apply_not_isUnit converts ¬ IsUnit A.det into A⁻¹ = 0;
Matrix.zero_mulVec finishes. This is a core exercise: it is short, and the
fact it establishes is the reason every later theorem carries a hypothesis.
/-- Exercise (core). **A rank-deficient regression returns zero, not an error.**
If every unit is treated then the treatment column of `simpleMatrix` equals the intercept
column, the Gram matrix `!![n, n; n, n]` is singular, and Mathlib's `Matrix.inv` — which is a
*total* function, defined to be `0` whenever the determinant is not a unit — hands back the
zero matrix. So `ols` is perfectly well defined and perfectly meaningless: it is the zero
vector, and in particular the "estimated treatment effect" is `0`.
Nothing warns you. This is why every theorem about `ols` in the library carries an explicit
`IsUnit (Xᵀ * X).det` hypothesis, and why `simpleMatrix_ols_coeff` is stated for solutions of
the normal equations instead.
Hint: `simpleMatrix_gram_det` computes the determinant, and
`Matrix.nonsing_inv_apply_not_isUnit` turns `¬ IsUnit A.det` into `A⁻¹ = 0`; finish with
`Matrix.zero_mulVec`. -/
theorem ols_simpleMatrix_of_all_treated (y : Fin n → ℝ) :
ols (simpleMatrix (Finset.univ : Assignment n)) y = 0 := by
sorryShow solution
/-- Exercise (core). **A rank-deficient regression returns zero, not an error.**
If every unit is treated then the treatment column of `simpleMatrix` equals the intercept
column, the Gram matrix `!![n, n; n, n]` is singular, and Mathlib's `Matrix.inv` — which is a
*total* function, defined to be `0` whenever the determinant is not a unit — hands back the
zero matrix. So `ols` is perfectly well defined and perfectly meaningless: it is the zero
vector, and in particular the "estimated treatment effect" is `0`.
Nothing warns you. This is why every theorem about `ols` in the library carries an explicit
`IsUnit (Xᵀ * X).det` hypothesis, and why `simpleMatrix_ols_coeff` is stated for solutions of
the normal equations instead.
Hint: `simpleMatrix_gram_det` computes the determinant, and
`Matrix.nonsing_inv_apply_not_isUnit` turns `¬ IsUnit A.det` into `A⁻¹ = 0`; finish with
`Matrix.zero_mulVec`. -/
theorem ols_simpleMatrix_of_all_treated (y : Fin n → ℝ) :
ols (simpleMatrix (Finset.univ : Assignment n)) y = 0 := by
have hdet : ¬ IsUnit ((simpleMatrix (Finset.univ : Assignment n))ᵀ
* simpleMatrix (Finset.univ : Assignment n)).det := by
rw [simpleMatrix_gram_det]
simp
rw [ols, Matrix.nonsing_inv_apply_not_isUnit _ hdet, Matrix.zero_mulVec]Least squares without a model
The formalization does not define OLS by minimization. It defines it by the normal equations.
/-- `IsOLS X y β` says that `β` **solves the normal equations** `XᵀX β = Xᵀy`.
This is the definition of "an ordinary least squares fit" that requires no inverses, no
minimisation, and no rank condition: it is a plain linear equation, and it may have zero, one
or many solutions. (It always has at least one, and it is exactly the first-order condition
for minimising `‖y - Xβ‖²`, but we will not need either fact.)
Everything in this file is phrased in terms of `IsOLS` rather than a formula, precisely
because the interesting theorems — "the coefficient equals the difference in means" — hold
for *any* solution, whether or not the design matrix has full rank. -/
def IsOLS {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (y : Fin n → ℝ) (β : Fin k → ℝ) : Prop :=
(Xᵀ * X) *ᵥ β = Xᵀ *ᵥ yIsOLS X y β is a Prop, not a number: it is the plain linear equation
The equivalent geometric statement is the one most statisticians actually carry
in their heads: the residual is orthogonal to every column of
ExerciseNormal equations = orthogonality (core)
Column fun i => X i j, so orthogonality to it is
Matrix.mulVec_mulVec rewrites
(Xᵀ * X) *ᵥ β as Xᵀ *ᵥ (X *ᵥ β), and funext_iff turns an equality of
vectors into a statement about each entry. Both directions are the same
simp only followed by linarith.
This is the lemma every later proof goes through: each column of the design matrix contributes one scalar equation about sums over units, and we get to choose columns that isolate the treated and control groups.
/-- Exercise (core). **The geometric characterisation of least squares**: `β` solves the
normal equations exactly when the residual vector `y - Xβ` is orthogonal to every column of
`X`.
Column `j` of `X` is the vector `fun i => X i j`, so "orthogonal to column `j`" means
`∑ i, X i j * (y i - (X *ᵥ β) i) = 0`. Reading the normal equations this way is what makes
every later proof possible: each column of the design matrix contributes one scalar equation
about sums over units, and we can then choose columns that isolate the treated and control
groups.
Hint: `Matrix.mulVec_mulVec` turns `(Xᵀ * X) *ᵥ β` into `Xᵀ *ᵥ (X *ᵥ β)`, and `funext_iff`
turns an equality of vectors into a statement about each entry. -/
theorem isOLS_iff_orthogonal {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (y : Fin n → ℝ)
(β : Fin k → ℝ) :
IsOLS X y β ↔ ∀ j, ∑ i, X i j * (y i - (X *ᵥ β) i) = 0 := by
sorryShow solution
/-- Exercise (core). **The geometric characterisation of least squares**: `β` solves the
normal equations exactly when the residual vector `y - Xβ` is orthogonal to every column of
`X`.
Column `j` of `X` is the vector `fun i => X i j`, so "orthogonal to column `j`" means
`∑ i, X i j * (y i - (X *ᵥ β) i) = 0`. Reading the normal equations this way is what makes
every later proof possible: each column of the design matrix contributes one scalar equation
about sums over units, and we can then choose columns that isolate the treated and control
groups.
Hint: `Matrix.mulVec_mulVec` turns `(Xᵀ * X) *ᵥ β` into `Xᵀ *ᵥ (X *ᵥ β)`, and `funext_iff`
turns an equality of vectors into a statement about each entry. -/
theorem isOLS_iff_orthogonal {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (y : Fin n → ℝ)
(β : Fin k → ℝ) :
IsOLS X y β ↔ ∀ j, ∑ i, X i j * (y i - (X *ᵥ β) i) = 0 := by
unfold IsOLS
rw [← Matrix.mulVec_mulVec, funext_iff]
constructor
· intro h j
have hj := h j
simp only [Matrix.mulVec, dotProduct, Matrix.transpose_apply, mul_sub,
Finset.sum_sub_distrib] at hj ⊢
linarith
· intro h j
have hj := h j
simp only [Matrix.mulVec, dotProduct, Matrix.transpose_apply, mul_sub,
Finset.sum_sub_distrib] at hj ⊢
linarithThe textbook formula is a definition in this file, not a theorem, and it is
kept firmly separate from IsOLS.
/-- The **textbook OLS formula** `β̂ = (XᵀX)⁻¹Xᵀy`.
Mathlib's `Matrix.inv` is total: for a singular matrix it returns `0`. So this definition
always type-checks, but it only *means* anything when `XᵀX` is nonsingular; that hypothesis
appears explicitly in every theorem about it below. -/
noncomputable def ols {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (y : Fin n → ℝ) : Fin k → ℝ :=
(Xᵀ * X)⁻¹ *ᵥ (Xᵀ *ᵥ y)Two facts connect them, both short enough to be warm-ups. Existence: when the Gram matrix is invertible the formula does solve the normal equations.
ExerciseThe formula solves the equations (warm-up)
Unfold both definitions, then Matrix.mulVec_mulVec collapses the two
matrix–vector products into one, Matrix.mul_nonsing_inv cancels the inverse,
and Matrix.one_mulVec clears up.
/-- Exercise (warm-up). When the Gram matrix `XᵀX` is invertible, the textbook formula does
solve the normal equations.
Hint: `Matrix.mulVec_mulVec`, `Matrix.mul_nonsing_inv`, `Matrix.one_mulVec`. -/
theorem ols_isOLS {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (y : Fin n → ℝ)
(h : IsUnit (Xᵀ * X).det) : IsOLS X y (ols X y) := by
sorryShow solution
/-- Exercise (warm-up). When the Gram matrix `XᵀX` is invertible, the textbook formula does
solve the normal equations.
Hint: `Matrix.mulVec_mulVec`, `Matrix.mul_nonsing_inv`, `Matrix.one_mulVec`. -/
theorem ols_isOLS {k : ℕ} (X : Matrix (Fin n) (Fin k) ℝ) (y : Fin n → ℝ)
(h : IsUnit (Xᵀ * X).det) : IsOLS X y (ols X y) := by
unfold IsOLS ols
rw [Matrix.mulVec_mulVec, Matrix.mul_nonsing_inv _ h, Matrix.one_mulVec]And uniqueness, which is the no-perfect-collinearity condition doing its job.
ExerciseA nonsingular Gram matrix pins β down (warm-up)
One exact. Matrix.mulVec_injective_of_det_ne_zero says that
A *ᵥ · is injective when A.det ≠ 0, and the two hypotheses give you two
vectors with the same image.
/-- Exercise (warm-up). **Uniqueness**: a nonsingular Gram matrix pins the coefficient
vector down completely.
Statistically this is the no-perfect-collinearity condition. When it fails the coefficient
vector is genuinely not identified — but, as the theorems below show, useful *functions* of
it can still be, which is why we never assume it except where we need a formula.
Hint: `Matrix.mulVec_injective_of_det_ne_zero`. -/
theorem isOLS_unique {k : ℕ} {X : Matrix (Fin n) (Fin k) ℝ} {y : Fin n → ℝ} {β β' : Fin k → ℝ}
(h : (Xᵀ * X).det ≠ 0) (hβ : IsOLS X y β) (hβ' : IsOLS X y β') : β = β' := by
sorryShow solution
/-- Exercise (warm-up). **Uniqueness**: a nonsingular Gram matrix pins the coefficient
vector down completely.
Statistically this is the no-perfect-collinearity condition. When it fails the coefficient
vector is genuinely not identified — but, as the theorems below show, useful *functions* of
it can still be, which is why we never assume it except where we need a formula.
Hint: `Matrix.mulVec_injective_of_det_ne_zero`. -/
theorem isOLS_unique {k : ℕ} {X : Matrix (Fin n) (Fin k) ℝ} {y : Fin n → ℝ} {β β' : Fin k → ℝ}
(h : (Xᵀ * X).det ≠ 0) (hβ : IsOLS X y β) (hβ' : IsOLS X y β') : β = β' := by
exact Matrix.mulVec_injective_of_det_ne_zero h (hβ.trans hβ'.symm)Put together they give isOLS_iff_eq_ols: under IsUnit (Xᵀ * X).det, “solves
the normal equations” and “equals
Simple regression is the difference in means
Regress the observed outcome on a constant and the treatment indicator. Two
columns: ones, and
/-- The design matrix of the **simple regression on a treatment dummy**: column `0` is the
intercept (all ones), column `1` is the treatment indicator `Z z`.
Small matrices are built in Lean with the vector notation `![a, b, …]`: here each *row* of the
matrix is the two-entry vector `![1, Z z i]`, and `Matrix.of` re-labels the function
`Fin n → Fin 2 → ℝ` as a matrix. -/
noncomputable def simpleMatrix (z : Assignment n) : Matrix (Fin n) (Fin 2) ℝ :=
Matrix.of fun i => ![1, Z z i]Its Gram matrix is the two-by-two array with entries
ExerciseThe Gram matrix (core)
ext j k reduces the matrix equation to its four entries and fin_cases j <;> fin_cases k splits them out. Matrix.mul_apply unfolds (A * B) j k into
∑ i, A j i * B i k; then sum_Z and Z_mul_self are the whole content.
/-- Exercise (core). The Gram matrix of the simple design is `!![n, n₁; n₁, n₁]`, where
`n₁ = #z` is the number of treated units.
The only two facts used are `∑ i, Z z i = #z` (`sum_Z`) and `Z z i * Z z i = Z z i`
(`Z_mul_self`): a dummy is its own square, so the `(1,1)` entry is again `n₁`.
`!![a, b; c, d]` is Lean's notation for a literal matrix, and `Matrix.mul_apply` unfolds
`(A * B) j k` into `∑ i, A j i * B i k`. -/
theorem simpleMatrix_gram (z : Assignment n) :
(simpleMatrix z)ᵀ * (simpleMatrix z) = !![(n : ℝ), (#z : ℝ); (#z : ℝ), (#z : ℝ)] := by
sorryShow solution
/-- Exercise (core). The Gram matrix of the simple design is `!![n, n₁; n₁, n₁]`, where
`n₁ = #z` is the number of treated units.
The only two facts used are `∑ i, Z z i = #z` (`sum_Z`) and `Z z i * Z z i = Z z i`
(`Z_mul_self`): a dummy is its own square, so the `(1,1)` entry is again `n₁`.
`!![a, b; c, d]` is Lean's notation for a literal matrix, and `Matrix.mul_apply` unfolds
`(A * B) j k` into `∑ i, A j i * B i k`. -/
theorem simpleMatrix_gram (z : Assignment n) :
(simpleMatrix z)ᵀ * (simpleMatrix z) = !![(n : ℝ), (#z : ℝ); (#z : ℝ), (#z : ℝ)] := by
ext j k
fin_cases j <;> fin_cases k <;>
simp [Matrix.mul_apply, simpleMatrix, Matrix.transpose_apply, sum_Z, Z_mul_self]Its determinant is simpleMatrix_gram_det), so the regression
is non-degenerate exactly when both groups are non-empty — the same condition
under which the difference in means is defined at all. That is not a
coincidence, and the next theorem says why.
Unpacking the normal equations is a genuine ↔; no rank condition is needed to
read the system, only to solve it.
/-- **The two normal equations of the simple regression, written out.**
Orthogonality to the column of ones says the residuals sum to zero over the *whole* sample;
orthogonality to the treatment column says they sum to zero over the *treated* units. In
terms of the coefficients:
* `∑ i, y i = n · β₀ + n₁ · β₁`
* `∑ i ∈ z, y i = n₁ · β₀ + n₁ · β₁`
Everything about simple regression follows from this 2 × 2 system. Note that it is a genuine
`↔`: no rank condition is needed to unpack the normal equations, only to solve them. -/
theorem isOLS_simpleMatrix_iff (z : Assignment n) (y : Fin n → ℝ) (β : Fin 2 → ℝ) :
IsOLS (simpleMatrix z) y β ↔
(∑ i, y i = (n : ℝ) * β 0 + (#z : ℝ) * β 1)
∧ (∑ i ∈ z, y i = (#z : ℝ) * β 0 + (#z : ℝ) * β 1) := by
rw [isOLS_iff_orthogonal, Fin.forall_fin_two]
-- Expand the two column equations into sums of `y` over everybody and over the treated.
have expand0 : ∑ i, (y i - (β 0 + Z z i * β 1))
= (∑ i, y i) - ((n : ℝ) * β 0 + (#z : ℝ) * β 1) := by
rw [Finset.sum_sub_distrib, Finset.sum_add_distrib, Finset.sum_const, Finset.card_univ,
Fintype.card_fin, nsmul_eq_mul, ← Finset.sum_mul, sum_Z]
have step1 : ∀ i, Z z i * (y i - (β 0 + Z z i * β 1))
= Z z i * y i - (Z z i * β 0 + Z z i * β 1) := by
-- the only content is `Z² = Z`
intro i; linear_combination (-(β 1)) * Z_mul_self z i
have expand1 : ∑ i, Z z i * (y i - (β 0 + Z z i * β 1))
= (∑ i ∈ z, y i) - ((#z : ℝ) * β 0 + (#z : ℝ) * β 1) := by
rw [Finset.sum_congr rfl fun i _ => step1 i, Finset.sum_sub_distrib, Finset.sum_add_distrib,
← Finset.sum_mul, ← Finset.sum_mul, sum_Z, ← sum_mem_eq_sum_Z_mul]
simp only [simpleMatrix_apply_zero, simpleMatrix_apply_one, simpleMatrix_mulVec,
one_mul, expand0, expand1]
constructor
· rintro ⟨h0, h1⟩; exact ⟨by linarith, by linarith⟩
· rintro ⟨h0, h1⟩; exact ⟨by linarith, by linarith⟩Orthogonality to the column of ones says the residuals sum to zero over
everybody; orthogonality to the treatment column says they sum to zero over the
treated. Subtract the second from the first and you get
TheoremThe slope on a treatment dummy is the difference in means
If
Read the hypotheses. There is no design, no expectation, no assumption about the
potential outcomes: only 0 < #z, #z < n, and IsOLS. The conclusion holds
for every assignment
/-- **Regressing the observed outcome on a constant and the treatment dummy returns the
difference in means.**
If `β` solves the normal equations of the simple regression of `Yobs P z` on `(1, Z z)`, and
both groups are non-empty, then
* `β 0` is the control-group mean of `y0`, and
* `β 1` is exactly `DiM P z`.
There is no probability here at all: this is an identity that holds for *every* assignment
`z` with `0 < #z < n`. The two normal equations say
`S₁ + S₀ = n β₀ + n₁ β₁` and `S₁ = n₁ β₀ + n₁ β₁`; subtracting gives `S₀ = (n - n₁) β₀`,
i.e. `β₀` is the control mean, and the second then gives `β₁ = S₁/n₁ - β₀`. -/
theorem simpleMatrix_ols_coeff (P : Population n) (z : Assignment n) (β : Fin 2 → ℝ)
(hpos : 0 < #z) (hlt : #z < n) (h : IsOLS (simpleMatrix z) (Yobs P z) β) :
β 0 = meanOn zᶜ P.y0 ∧ β 1 = DiM P z := by
obtain ⟨e0, e1⟩ := (isOLS_simpleMatrix_iff z (Yobs P z) β).mp h
-- cardinalities, moved into `ℝ`
have hcard : (#zᶜ : ℝ) = (n : ℝ) - (#z : ℝ) := by rw [card_compl, Nat.cast_sub hlt.le]
have hz0 : (0 : ℝ) < (#z : ℝ) := by exact_mod_cast hpos
have hzc0 : (0 : ℝ) < (n : ℝ) - (#z : ℝ) := by
have : (#z : ℝ) < (n : ℝ) := by exact_mod_cast hlt
linarith
-- the observed outcomes are `y1` on the treated and `y0` on the controls
have hsplit : (∑ i ∈ z, P.y1 i) + (∑ i ∈ zᶜ, P.y0 i) = ∑ i, Yobs P z i := by
rw [Finset.sum_congr rfl fun i hi => (Yobs_of_mem hi).symm,
Finset.sum_congr rfl fun i hi => (Yobs_of_not_mem (by simpa using hi)).symm]
exact Finset.sum_add_sum_compl z _
rw [Finset.sum_congr rfl fun i hi => Yobs_of_mem hi] at e1
rw [← hsplit] at e0
-- solve the 2 × 2 system
have hb0 : β 0 = (∑ i ∈ zᶜ, P.y0 i) / ((n : ℝ) - (#z : ℝ)) := by
field_simp
linarith
have hb1 : β 1 = (∑ i ∈ z, P.y1 i) / (#z : ℝ) - β 0 := by
field_simp
linarith
exact ⟨by rw [meanOn, hcard, hb0], by rw [DiM, hb1, hb0, hcard]⟩The same holds for the closed form (ols_simpleMatrix_eq_DiM), because with
both groups non-empty the determinant
Inheriting a sampling property
Now, and only now, does a design appear.
/-- **The OLS treatment coefficient is unbiased for the ATE under complete randomisation.**
This is the one theorem in the file with probabilistic content, and it contains no new
argument: on every assignment the design can produce, the regression coefficient equals the
difference in means (`ols_simpleMatrix_eq_DiM`), and the difference in means was already
shown to be unbiased (`DiM_unbiased_completeRandomization`).
That is the whole message of the chapter. Running `lm(y ~ z)` does not invoke a model of the
outcomes, does not require homoskedasticity, and does not require normal errors; under this
design it just recomputes an estimator whose sampling distribution is governed entirely by
the randomisation. -/
theorem ols_simpleMatrix_unbiased_completeRandomization {n₁ : ℕ} (hle : n₁ ≤ n)
(P : Population n) (hpos : 0 < n₁) (hlt : n₁ < n) :
(completeRandomization n n₁ hle).expect
(fun z => ols (simpleMatrix z) (Yobs P z) 1) = P.tau := by
have hsupp : ∀ z, (completeRandomization n n₁ hle).prob z ≠ 0 →
ols (simpleMatrix z) (Yobs P z) 1 = DiM P z := by
intro z hz
have hcard : #z = n₁ := card_eq_of_prob_ne_zero hz
exact ols_simpleMatrix_eq_DiM P (by omega) (by omega)
rw [(completeRandomization n n₁ hle).expect_congr_of_support hsupp]
exact DiM_unbiased_completeRandomization hle P hpos hltThere is no new argument in that proof and there is not supposed to be. On every
assignment the design can produce, card_eq_of_prob_ne_zero), so
both groups are non-empty, so the coefficient equals the difference in means; and
expect_congr_of_support says that two functions agreeing on the support of a
design have the same expectation. Then quote
DiM_unbiased_completeRandomization from chapter 8 and stop.
That is the whole message. Running lm(y ~ z) does not invoke a model of the
outcomes, does not need homoskedasticity, and does not need normal errors. Under
this design it recomputes an estimator whose sampling distribution is governed
entirely by the randomization — and whose unbiasedness you proved three chapters
ago.
Three units, in numbers
Identities are easier to believe once you have watched one happen. Here is a population of three units with both potential outcomes written down, which we can do because this is a thought experiment rather than data.
/-- A finite population of three units, with both potential outcomes written down — which we
can do here because this is a thought experiment, not data.
Unit `0` has `(y₁, y₀) = (4, 1)`, unit `1` has `(6, 2)`, unit `2` has `(9, 3)`. -/
noncomputable def exP : Population 3 := ⟨![4, 6, 9], ![1, 2, 3]⟩
/-- The realised assignment: units `0` and `1` treated, unit `2` control. -/
def exZ : Assignment 3 := {0, 1}
theorem exZ_card : #exZ = 2 := by decide
theorem exZ_compl : exZᶜ = {2} := by decideThe estimand and the estimate:
/-- The estimand. The average treatment effect of this population is `(3 + 4 + 6)/3 = 13/3`,
a number no experiment will ever observe. -/
theorem exP_tau : exP.tau = 13 / 3 := by
rw [Population.tau_eq]
norm_num [exP, Population.effect, Fin.sum_univ_three, Matrix.cons_val_two,
Matrix.head_cons, Matrix.tail_cons]
/-- The estimate. On this assignment the observed outcomes are `(4, 6, 3)`, so the
difference in means is `(4 + 6)/2 - 3/1 = 2`.
`2 ≠ 13/3`: an unbiased estimator is not a correct one, and on a single assignment there is
nothing to be done about that. -/
theorem exP_DiM : DiM exP exZ = 2 := by
rw [DiM, exZ_compl, exZ]
norm_num [exP, Matrix.cons_val_two, Matrix.head_cons, Matrix.tail_cons]ExerciseCheck a coefficient vector by hand (warm-up)
The observed outcomes are isOLS_simpleMatrix_iff are
Start with rw [isOLS_simpleMatrix_iff] and constructor; then Yobs_eq_ite
turns the observed outcome into an if, Fin.sum_univ_three expands the sums
over Fin 3, and norm_num does the arithmetic. Note where the numbers came
from:
/-- Exercise (warm-up). **Check a candidate coefficient vector by hand.**
Claim: `β = (3, 2)` solves the normal equations of the regression of the observed outcome on
`(1, Z)` for the population and assignment above. The two equations of
`isOLS_simpleMatrix_iff` read
* `∑ᵢ yᵢ = n·β₀ + n₁·β₁`, i.e. `4 + 6 + 3 = 3·3 + 2·2 = 13`;
* `∑_{i ∈ z} yᵢ = n₁·β₀ + n₁·β₁`, i.e. `4 + 6 = 2·3 + 2·2 = 10`.
Note what the intercept is: `3` is the control-group mean of `y₀`, and the slope `2` is the
difference in means, exactly as `simpleMatrix_ols_coeff` promises.
Hint: `rw [isOLS_simpleMatrix_iff]`, then `constructor`, then unfold the observed outcome
with `Yobs_eq_ite` and expand the sums with `Fin.sum_univ_three`. -/
theorem exP_isOLS : IsOLS (simpleMatrix exZ) (Yobs exP exZ) ![3, 2] := by
sorryShow solution
/-- Exercise (warm-up). **Check a candidate coefficient vector by hand.**
Claim: `β = (3, 2)` solves the normal equations of the regression of the observed outcome on
`(1, Z)` for the population and assignment above. The two equations of
`isOLS_simpleMatrix_iff` read
* `∑ᵢ yᵢ = n·β₀ + n₁·β₁`, i.e. `4 + 6 + 3 = 3·3 + 2·2 = 13`;
* `∑_{i ∈ z} yᵢ = n₁·β₀ + n₁·β₁`, i.e. `4 + 6 = 2·3 + 2·2 = 10`.
Note what the intercept is: `3` is the control-group mean of `y₀`, and the slope `2` is the
difference in means, exactly as `simpleMatrix_ols_coeff` promises.
Hint: `rw [isOLS_simpleMatrix_iff]`, then `constructor`, then unfold the observed outcome
with `Yobs_eq_ite` and expand the sums with `Fin.sum_univ_three`. -/
theorem exP_isOLS : IsOLS (simpleMatrix exZ) (Yobs exP exZ) ![3, 2] := by
rw [isOLS_simpleMatrix_iff]
constructor <;>
simp [Yobs_eq_ite, exZ, exP, Fin.sum_univ_three] <;> norm_num/-- And the closed-form estimator agrees, because both groups are non-empty and therefore the
Gram matrix `!![3, 2; 2, 2]` (determinant `2`) is invertible.
The proof does not invert anything: `ols_simpleMatrix_eq_DiM` has already done the algebra
once and for all, so all that is left is arithmetic on the observed outcomes. -/
theorem exP_ols : ols (simpleMatrix exZ) (Yobs exP exZ) 1 = 2 := by
rw [ols_simpleMatrix_eq_DiM exP (by rw [exZ_card]; norm_num) (by rw [exZ_card]; norm_num),
exP_DiM]Group indicators return group means
Replace the single dummy by one dummy per group and drop the intercept. This is the saturated, or fixed-effects, regression: with blocks crossed with treatment it is the model a blocked experiment is usually written as.
/-- The design matrix of the **group-indicator regression** (no intercept): entry `(i, k)` is
`1` when unit `i` belongs to group `k` and `0` otherwise.
Every row has exactly one `1`, so the columns are orthogonal — which is why the coefficients
turn out to be the group means with no matrix algebra at all. -/
def groupMatrix {K : ℕ} (g : Fin n → Fin K) : Matrix (Fin n) (Fin K) ℝ :=
Matrix.of fun i k => if g i = k then 1 else 0Every row has exactly one
ExerciseThe fitted value is your own group's coefficient (warm-up)
Unfold Matrix.mulVec and dotProduct; the sum
∑ k, (if g i = k then 1 else 0) * β k collapses by Finset.sum_ite_eq, which
simp will find for you.
/-- Exercise (warm-up). The fitted value of unit `i` under the group-indicator design is
simply the coefficient of `i`'s own group, `β (g i)`.
Hint: unfold `Matrix.mulVec` and `dotProduct`; the sum `∑ k, (if g i = k then 1 else 0) * β k`
collapses by `Finset.sum_ite_eq`. -/
theorem groupMatrix_mulVec {K : ℕ} (g : Fin n → Fin K) (β : Fin K → ℝ) (i : Fin n) :
(groupMatrix g *ᵥ β) i = β (g i) := by
sorryShow solution
/-- Exercise (warm-up). The fitted value of unit `i` under the group-indicator design is
simply the coefficient of `i`'s own group, `β (g i)`.
Hint: unfold `Matrix.mulVec` and `dotProduct`; the sum `∑ k, (if g i = k then 1 else 0) * β k`
collapses by `Finset.sum_ite_eq`. -/
theorem groupMatrix_mulVec {K : ℕ} (g : Fin n → Fin K) (β : Fin K → ℝ) (i : Fin n) :
(groupMatrix g *ᵥ β) i = β (g i) := by
simp [Matrix.mulVec, dotProduct, groupMatrix]ExerciseThe coefficients are the cell means (core)
The normal equation for column eq_meanOn_of_sum_eq converts
Get the column equation with (isOLS_iff_orthogonal _ _ _).mp h k, prove the
hstep rewriting each summand as if g i = k then y i - β k else 0 by
by_cases hi : g i = k, then Finset.sum_filter turns the sum of an if into a
sum over groupOf g k.
/-- Exercise (core). **Regression on group indicators returns the group means.**
If `β` solves the normal equations of the no-intercept regression of `y` on the group
dummies, then for every non-empty group `k`, `β k` is the mean of `y` over that group.
The proof is the general recipe in miniature: the normal equation for column `k` is
`∑ i, 1{g i = k} · (y i - β (g i)) = 0`, the indicator restricts the sum to group `k`, on
which the fitted value is the constant `β k`, and `eq_meanOn_of_sum_eq` finishes.
Statistically this is why a saturated regression is "non-parametric": it imposes nothing, it
merely reports the cell means. It is also the blocked-experiment story — a block-by-treatment
saturated regression reports the within-block treated and control means. -/
theorem groupMatrix_ols_coeff {K : ℕ} (g : Fin n → Fin K) (y : Fin n → ℝ) (β : Fin K → ℝ)
(h : IsOLS (groupMatrix g) y β) (k : Fin K) (hk : (groupOf g k).Nonempty) :
β k = meanOn (groupOf g k) y := by
sorryShow solution
/-- Exercise (core). **Regression on group indicators returns the group means.**
If `β` solves the normal equations of the no-intercept regression of `y` on the group
dummies, then for every non-empty group `k`, `β k` is the mean of `y` over that group.
The proof is the general recipe in miniature: the normal equation for column `k` is
`∑ i, 1{g i = k} · (y i - β (g i)) = 0`, the indicator restricts the sum to group `k`, on
which the fitted value is the constant `β k`, and `eq_meanOn_of_sum_eq` finishes.
Statistically this is why a saturated regression is "non-parametric": it imposes nothing, it
merely reports the cell means. It is also the blocked-experiment story — a block-by-treatment
saturated regression reports the within-block treated and control means. -/
theorem groupMatrix_ols_coeff {K : ℕ} (g : Fin n → Fin K) (y : Fin n → ℝ) (β : Fin K → ℝ)
(h : IsOLS (groupMatrix g) y β) (k : Fin K) (hk : (groupOf g k).Nonempty) :
β k = meanOn (groupOf g k) y := by
have hk' := (isOLS_iff_orthogonal _ _ _).mp h k
have hstep : ∀ i, groupMatrix g i k * (y i - (groupMatrix g *ᵥ β) i)
= if g i = k then y i - β k else 0 := by
intro i
rw [groupMatrix_mulVec]
by_cases hi : g i = k <;> simp [groupMatrix, hi]
rw [Finset.sum_congr rfl fun i _ => hstep i, ← Finset.sum_filter,
show Finset.univ.filter (fun i => g i = k) = groupOf g k from rfl,
Finset.sum_sub_distrib, Finset.sum_const, nsmul_eq_mul] at hk'
refine eq_meanOn_of_sum_eq (Finset.card_pos.mpr hk) ?_
linarithThis is the sense in which a saturated regression is non-parametric: it imposes nothing, it reports the cell means. Combine it with chapter 10 and the story about blocked experiments writes itself — a block-by-treatment saturated regression reports the within-block treated and control means, and the blocked estimator is a particular weighted combination of them.
Lin’s fully interacted regression
Regress the outcome on
The algebra that makes it tick is a change of basis. First, a name for “the
simple regression of
/-- `IsSimpleOLSOn s x y a b` says that the line `a + b·x` solves the normal equations of the
simple regression of `y` on `(1, x)` **restricted to the units in `s`**:
* the residuals sum to zero over `s`;
* the residuals are orthogonal to `x` over `s`.
This is the same pair of equations as `isOLS_simpleMatrix_iff`, written for a subgroup and an
arbitrary regressor instead of a dummy. -/
def IsSimpleOLSOn (s : Finset (Fin n)) (x y : Fin n → ℝ) (a b : ℝ) : Prop :=
(∑ i ∈ s, (y i - (a + b * x i)) = 0) ∧ (∑ i ∈ s, x i * (y i - (a + b * x i)) = 0)ExerciseThe fitted line passes through the center of mass (core)
The intercept of a within-group fit is
Expand Finset.sum_sub_distrib,
Finset.sum_add_distrib, Finset.sum_const and Finset.mul_sum, unfold both
meanOns, and let field_simp and linarith divide by
/-- Exercise (core). **The fitted line passes through the group's centre of mass**: the
intercept of a within-group simple regression is `ȳ - b·x̄`, the group means of `y` and `x`.
Only the *first* normal equation (residuals sum to zero) is used; the slope equation is what
would pin `b` down, and we deliberately never need it.
Hint: expand `∑ i ∈ s, (y i - (a + b * x i)) = 0` with `Finset.sum_sub_distrib`,
`Finset.sum_add_distrib`, `Finset.sum_const` and `Finset.mul_sum`, then divide by `#s`. -/
theorem intercept_eq_of_isSimpleOLSOn {s : Finset (Fin n)} {x y : Fin n → ℝ} {a b : ℝ}
(h : IsSimpleOLSOn s x y a b) (hs : 0 < #s) :
a = meanOn s y - b * meanOn s x := by
sorryShow solution
/-- Exercise (core). **The fitted line passes through the group's centre of mass**: the
intercept of a within-group simple regression is `ȳ - b·x̄`, the group means of `y` and `x`.
Only the *first* normal equation (residuals sum to zero) is used; the slope equation is what
would pin `b` down, and we deliberately never need it.
Hint: expand `∑ i ∈ s, (y i - (a + b * x i)) = 0` with `Finset.sum_sub_distrib`,
`Finset.sum_add_distrib`, `Finset.sum_const` and `Finset.mul_sum`, then divide by `#s`. -/
theorem intercept_eq_of_isSimpleOLSOn {s : Finset (Fin n)} {x y : Fin n → ℝ} {a b : ℝ}
(h : IsSimpleOLSOn s x y a b) (hs : 0 < #s) :
a = meanOn s y - b * meanOn s x := by
have hs' : (0 : ℝ) < (#s : ℝ) := by exact_mod_cast hs
have hexp : ∑ i ∈ s, (y i - (a + b * x i))
= (∑ i ∈ s, y i) - ((#s : ℝ) * a + b * ∑ i ∈ s, x i) := by
rw [Finset.sum_sub_distrib, Finset.sum_add_distrib, Finset.sum_const, nsmul_eq_mul,
← Finset.mul_sum]
have h1 := h.1
rw [hexp] at h1
rw [meanOn, meanOn]
field_simp
linarithAnd the design matrix:
/-- The design matrix of **Lin's fully interacted regression**: the four columns are the
intercept, the treatment dummy, the covariate, and their product. -/
noncomputable def interactedMatrix (z : Assignment n) (x : Fin n → ℝ) :
Matrix (Fin n) (Fin 4) ℝ :=
Matrix.of fun i => ![1, Z z i, x i, Z z i * x i]TheoremDecoupling
The fully interacted regression is two separate within-group regressions.
The columns linear_combination calls are each “multiply
/-- **The decoupling lemma: the fully interacted regression is two separate within-group
regressions.**
`β` solves the four normal equations of the regression of `y` on `(1, Z, x, Z·x)` if and only
if
* the line `(β 0 + β 1) + (β 2 + β 3)·x` is a least-squares fit of `y` on `(1, x)` among the
**treated** units, and
* the line `β 0 + β 2·x` is a least-squares fit among the **control** units.
The reason is a change of basis for the column space: the four columns `1, Z, x, Zx` span the
same space as `Z, Zx, (1-Z), (1-Z)x`, and the last two pairs live on disjoint sets of units,
so the `4 × 4` system is block diagonal. Concretely, the treated block is the pair of
equations indexed by columns `Z` and `Zx`, and the control block is what is left over after
subtracting them from the equations indexed by `1` and `x`.
Every step uses only `Z z i * Z z i = Z z i`. -/
theorem isOLS_interactedMatrix_iff (z : Assignment n) (x y : Fin n → ℝ) (β : Fin 4 → ℝ) :
IsOLS (interactedMatrix z x) y β ↔
IsSimpleOLSOn z x y (β 0 + β 1) (β 2 + β 3) ∧ IsSimpleOLSOn zᶜ x y (β 0) (β 2) := by
rw [isOLS_iff_orthogonal, forall_fin_four]
-- abbreviations for the four column equations' left-hand sides
set r : Fin n → ℝ := fun i => y i - (interactedMatrix z x *ᵥ β) i with hr
-- the treated block: multiplying the residual by `Z` replaces the fit by the treated line
have treated : ∀ i, Z z i * r i = Z z i * (y i - ((β 0 + β 1) + (β 2 + β 3) * x i)) := by
intro i
simp only [hr, interactedMatrix_mulVec]
linear_combination (-(β 1 + β 3 * x i)) * Z_mul_self z i
have treatedx : ∀ i, Z z i * x i * r i
= Z z i * (x i * (y i - ((β 0 + β 1) + (β 2 + β 3) * x i))) := by
intro i
simp only [hr, interactedMatrix_mulVec]
linear_combination (-(x i * (β 1 + β 3 * x i))) * Z_mul_self z i
-- the control block: multiplying by `1 - Z` replaces the fit by the control line
have control : ∀ i, (1 - Z z i) * r i = (1 - Z z i) * (y i - (β 0 + β 2 * x i)) := by
intro i
simp only [hr, interactedMatrix_mulVec]
linear_combination (β 1 + β 3 * x i) * Z_mul_self z i
have controlx : ∀ i, (1 - Z z i) * (x i * r i)
= (1 - Z z i) * (x i * (y i - (β 0 + β 2 * x i))) := by
intro i
simp only [hr, interactedMatrix_mulVec]
linear_combination (x i * (β 1 + β 3 * x i)) * Z_mul_self z i
-- the four subgroup sums, expressed through the four column equations
have bz1 : ∑ i ∈ z, (y i - ((β 0 + β 1) + (β 2 + β 3) * x i)) = ∑ i, Z z i * r i := by
rw [sum_mem_eq_sum_Z_mul]
exact (Finset.sum_congr rfl fun i _ => treated i).symm
have bz2 : ∑ i ∈ z, x i * (y i - ((β 0 + β 1) + (β 2 + β 3) * x i))
= ∑ i, Z z i * x i * r i := by
rw [sum_mem_eq_sum_Z_mul]
exact (Finset.sum_congr rfl fun i _ => treatedx i).symm
have bc1 : ∑ i ∈ zᶜ, (y i - (β 0 + β 2 * x i)) = (∑ i, r i) - ∑ i, Z z i * r i := by
rw [sum_compl_eq_sum_one_sub_Z_mul, ← Finset.sum_sub_distrib]
refine Finset.sum_congr rfl fun i _ => ?_
rw [← control i]; ring
have bc2 : ∑ i ∈ zᶜ, x i * (y i - (β 0 + β 2 * x i))
= (∑ i, x i * r i) - ∑ i, Z z i * x i * r i := by
rw [sum_compl_eq_sum_one_sub_Z_mul, ← Finset.sum_sub_distrib]
refine Finset.sum_congr rfl fun i _ => ?_
rw [← controlx i]; ring
simp only [interactedMatrix_apply_zero, interactedMatrix_apply_one,
interactedMatrix_apply_two, interactedMatrix_apply_three, one_mul, ← hr]
rw [IsSimpleOLSOn, IsSimpleOLSOn, bz1, bz2, bc1, bc2]
constructor
· rintro ⟨h0, h1, h2, h3⟩
exact ⟨⟨h1, h3⟩, ⟨by rw [h0, h1, sub_zero], by rw [h2, h3, sub_zero]⟩⟩
· rintro ⟨⟨t1, t2⟩, ⟨c1, c2⟩⟩
exact ⟨by linarith, t1, by linarith, t2⟩Once you see the decoupling, the coefficient on intercept_eq_of_isSimpleOLSOn each intercept is a
center of mass.
/-- **Lin's coefficient is the difference of the two within-group fitted intercepts.**
For any solution `β` of the interacted normal equations, with both groups non-empty,
`β 1 = (ȳ₁ - b₁·x̄₁) - (ȳ₀ - b₀·x̄₀)`,
where `b₁ = β 2 + β 3` and `b₀ = β 2` are the within-treated and within-control slopes and
the bars denote within-group means.
In words: fit a line to the treated units, fit a line to the control units, and report the
gap between the two fitted *intercepts*. When `x` has been centred so that `x̄ = 0`, the
intercepts are the two fitted lines evaluated at the grand mean of the covariate, and the gap
is exactly Lin's covariate-adjusted estimate of the ATE.
This is a finite-sample algebraic identity, true for every assignment. It says nothing about
bias, consistency, or variance; see `lin_coeff_eq_DiM_sub_adjustment` for the form that makes
the connection to the unadjusted difference in means visible. -/
theorem lin_coeff (z : Assignment n) (x y : Fin n → ℝ) (β : Fin 4 → ℝ)
(hpos : 0 < #z) (hlt : #z < n) (h : IsOLS (interactedMatrix z x) y β) :
β 1 = (meanOn z y - (β 2 + β 3) * meanOn z x) - (meanOn zᶜ y - β 2 * meanOn zᶜ x) := by
obtain ⟨htreat, hctrl⟩ := (isOLS_interactedMatrix_iff z x y β).mp h
have hzc : 0 < #zᶜ := by rw [card_compl]; omega
have h1 := intercept_eq_of_isSimpleOLSOn htreat hpos
have h0 := intercept_eq_of_isSimpleOLSOn hctrl hzc
linarithRearranged, the same identity says what adjustment is doing:
ExerciseDifference in means, minus a covariate correction (stretch)
have := lin_coeff … and a linarith — the exercise is reading the statement,
not proving it.
/-- Exercise (stretch). **Lin's estimator is the difference in means minus a
covariate-imbalance correction.**
Rearranging `lin_coeff`,
`β 1 = (ȳ₁ - ȳ₀) - (b₁·x̄₁ - b₀·x̄₀)`.
The first bracket is the raw difference in the observed group means of `y`; the second is what
the fitted slopes predict the difference *would* be from the covariate imbalance `x̄₁` versus
`x̄₀` alone. If the covariate happens to be perfectly balanced (`x̄₁ = x̄₀ = 0`, which is what
centring plus balance gives you) the correction vanishes and Lin's estimator coincides with
the unadjusted difference in means.
Hint: this is `lin_coeff` plus `ring`/`linarith`. -/
theorem lin_coeff_eq_DiM_sub_adjustment (z : Assignment n) (x y : Fin n → ℝ) (β : Fin 4 → ℝ)
(hpos : 0 < #z) (hlt : #z < n) (h : IsOLS (interactedMatrix z x) y β) :
β 1 = (meanOn z y - meanOn zᶜ y)
- ((β 2 + β 3) * meanOn z x - β 2 * meanOn zᶜ x) := by
sorryShow solution
/-- Exercise (stretch). **Lin's estimator is the difference in means minus a
covariate-imbalance correction.**
Rearranging `lin_coeff`,
`β 1 = (ȳ₁ - ȳ₀) - (b₁·x̄₁ - b₀·x̄₀)`.
The first bracket is the raw difference in the observed group means of `y`; the second is what
the fitted slopes predict the difference *would* be from the covariate imbalance `x̄₁` versus
`x̄₀` alone. If the covariate happens to be perfectly balanced (`x̄₁ = x̄₀ = 0`, which is what
centring plus balance gives you) the correction vanishes and Lin's estimator coincides with
the unadjusted difference in means.
Hint: this is `lin_coeff` plus `ring`/`linarith`. -/
theorem lin_coeff_eq_DiM_sub_adjustment (z : Assignment n) (x y : Fin n → ℝ) (β : Fin 4 → ℝ)
(hpos : 0 < #z) (hlt : #z < n) (h : IsOLS (interactedMatrix z x) y β) :
β 1 = (meanOn z y - meanOn zᶜ y)
- ((β 2 + β 3) * meanOn z x - β 2 * meanOn zᶜ x) := by
have := lin_coeff z x y β hpos hlt h
linarithBoth are stated for an arbitrary response vector y. Feeding them the observed
outcome vector puts the identity in the vocabulary of the rest of the tutorial.
ExerciseGroup means of the observed outcome (warm-up)
On the treated units the observed outcome is meanOn z only looks
at those units, so the two means coincide. The treated case is done for you; the
control case is the same three-step rewrite, except that i ∈ zᶜ has to become
i ∉ z before Yobs_of_not_mem applies. simpa using hi does that.
/-- The treated group's mean of the *observed* outcome is its mean of `y₁`: on the treated
units the observed outcome is `y₁` by definition, and `meanOn z` only looks at those units. -/
theorem meanOn_Yobs_treated (P : Population n) (z : Assignment n) :
meanOn z (Yobs P z) = meanOn z P.y1 := by
rw [meanOn, meanOn, Finset.sum_congr rfl fun i hi => Yobs_of_mem hi]
/-- Exercise (warm-up). The control-group mirror image.
Hint: the same three-step proof as above, except that membership in `zᶜ` has to be turned
into non-membership in `z` before `Yobs_of_not_mem` applies — `simpa using hi` does it. -/
theorem meanOn_Yobs_control (P : Population n) (z : Assignment n) :
meanOn zᶜ (Yobs P z) = meanOn zᶜ P.y0 := by
sorryShow solution
/-- The treated group's mean of the *observed* outcome is its mean of `y₁`: on the treated
units the observed outcome is `y₁` by definition, and `meanOn z` only looks at those units. -/
theorem meanOn_Yobs_treated (P : Population n) (z : Assignment n) :
meanOn z (Yobs P z) = meanOn z P.y1 := by
rw [meanOn, meanOn, Finset.sum_congr rfl fun i hi => Yobs_of_mem hi]
/-- Exercise (warm-up). The control-group mirror image.
Hint: the same three-step proof as above, except that membership in `zᶜ` has to be turned
into non-membership in `z` before `Yobs_of_not_mem` applies — `simpa using hi` does it. -/
theorem meanOn_Yobs_control (P : Population n) (z : Assignment n) :
meanOn zᶜ (Yobs P z) = meanOn zᶜ P.y0 := by
rw [meanOn, meanOn, Finset.sum_congr rfl fun i hi => Yobs_of_not_mem (by simpa using hi)]/-- **Lin's coefficient, written against the potential outcomes.**
`lin_coeff_eq_DiM_sub_adjustment` is stated for an arbitrary response vector `y`. Feeding it
the observed outcome vector turns the two group means of `y` into the difference in means of
the population, so the identity reads
`β₁ = DiM − (b₁·x̄₁ − b₀·x̄₀)`,
with `b₁ = β 2 + β 3` the within-treated slope and `b₀ = β 2` the within-control slope.
Lin's estimator is the difference in means, minus what the two fitted lines say the covariate
imbalance alone would have produced. This is an identity about the realised assignment; it
is not a bias, variance or consistency claim. -/
theorem lin_coeff_Yobs (P : Population n) (z : Assignment n) (x : Fin n → ℝ) (β : Fin 4 → ℝ)
(hpos : 0 < #z) (hlt : #z < n) (h : IsOLS (interactedMatrix z x) (Yobs P z) β) :
β 1 = DiM P z - ((β 2 + β 3) * meanOn z x - β 2 * meanOn zᶜ x) := by
rw [lin_coeff_eq_DiM_sub_adjustment z x (Yobs P z) β hpos hlt h,
meanOn_Yobs_treated, meanOn_Yobs_control, DiM_eq_meanOn_sub_meanOn]ExercisePerfect balance means no adjustment (stretch)
If both within-group covariate means are zero, Lin’s coefficient is the plain
difference in means, whatever the fitted slopes turn out to be. lin_coeff_Yobs
and then rewriting with the two balance hypotheses leaves something ring will
close.
The moral is worth more than the proof: covariate adjustment is not a correction
for a bias that exists on average — under complete randomization there is none.
It is a correction for the imbalance realized in the assignment you actually
drew. centred_covariate_means in the library adds the other half of Lin’s
recommendation: with
/-- Exercise (stretch). **On a perfectly balanced assignment, adjustment changes nothing.**
If the covariate has been centred and happens to be exactly balanced — both group means are
`0` — then Lin's coefficient is the plain difference in means, whatever the fitted slopes
turn out to be.
So covariate adjustment is not a correction for a bias that is there on average; it is a
correction for the imbalance realised in the assignment you actually drew. When there is no
imbalance there is nothing to correct.
Hint: `lin_coeff_Yobs`, then rewrite with the two balance hypotheses and `ring`. -/
theorem lin_coeff_eq_DiM_of_balanced (P : Population n) (z : Assignment n) (x : Fin n → ℝ)
(β : Fin 4 → ℝ) (hpos : 0 < #z) (hlt : #z < n)
(h : IsOLS (interactedMatrix z x) (Yobs P z) β)
(hx1 : meanOn z x = 0) (hx0 : meanOn zᶜ x = 0) :
β 1 = DiM P z := by
sorryShow solution
/-- Exercise (stretch). **On a perfectly balanced assignment, adjustment changes nothing.**
If the covariate has been centred and happens to be exactly balanced — both group means are
`0` — then Lin's coefficient is the plain difference in means, whatever the fitted slopes
turn out to be.
So covariate adjustment is not a correction for a bias that is there on average; it is a
correction for the imbalance realised in the assignment you actually drew. When there is no
imbalance there is nothing to correct.
Hint: `lin_coeff_Yobs`, then rewrite with the two balance hypotheses and `ring`. -/
theorem lin_coeff_eq_DiM_of_balanced (P : Population n) (z : Assignment n) (x : Fin n → ℝ)
(β : Fin 4 → ℝ) (hpos : 0 < #z) (hlt : #z < n)
(h : IsOLS (interactedMatrix z x) (Yobs P z) β)
(hx1 : meanOn z x = 0) (hx0 : meanOn zᶜ x = 0) :
β 1 = DiM P z := by
rw [lin_coeff_Yobs P z x β hpos hlt h, hx1, hx0]
ringReading autoformalized Lean
Three ways this chapter’s results get mis-stated. All three type-check.
Dropping the non-degeneracy hypotheses
-- WRONG: no hypotheses on `#z`.
theorem ols_coeff_is_dim (P : Population n) (z : Assignment n) :
ols (simpleMatrix z) (Yobs P z) 1 = DiM P z := by
sorry
Confusing an identity with unbiasedness
-- WRONG: the estimator is not equal to the estimand.
theorem regression_recovers_the_ate {n₁ : ℕ} (hle : n₁ ≤ n) (P : Population n)
(z : Assignment n) (hz : #z = n₁) :
ols (simpleMatrix z) (Yobs P z) 1 = P.tau := by
sorry
A name that promises more than the statement delivers
-- MISLEADING: the name says efficiency, the statement says algebra.
theorem lin_estimator_is_efficient (z : Assignment n) (x y : Fin n → ℝ)
(β : Fin 4 → ℝ) (hpos : 0 < #z) (hlt : #z < n)
(h : IsOLS (interactedMatrix z x) y β) :
β 1 = (meanOn z y - meanOn zᶜ y)
- ((β 2 + β 3) * meanOn z x - β 2 * meanOn zᶜ x) := by
sorry
The habit this chapter is trying to build is narrower than “read Lean carefully”. It is: when a formalization claims a regression coefficient is an estimator, check the hypotheses that keep both sides non-degenerate; and when it claims a regression coefficient has a property, check that the property is stated about a design, because algebra alone can never give you one.