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
  1. Matrices in Mathlib
    1. The precedence trap
    2. Small matrices, and one determinant
    3. The inverse is total, and that is a trap
  2. Least squares without a model
  3. Simple regression is the difference in means
    1. Inheriting a sampling property
  4. Three units, in numbers
  5. Group indicators return group means
  6. Lin’s fully interacted regression
  7. Reading autoformalized Lean
    1. Dropping the non-degeneracy hypotheses
    2. Confusing an identity with unbiasedness
    3. A name that promises more than the statement delivers

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, y=Xβ+ε, with assumptions about ε — mean zero, constant variance, maybe normality — and everything you know about the coefficient’s behavior was derived from those assumptions.

None of them are available here. In a finite population the potential outcomes are fixed numbers; there is no ε, nothing is drawn from a distribution, and the only random object in the room is the assignment. So what is the regression coefficient?

It is a function of the numbers the experiment produced. Given X and y, “the OLS coefficient” is the solution of a linear system: an algebraic object, with no probabilistic content whatsoever. Every theorem in this chapter is therefore an identity that holds for every single assignment, with no expectation anywhere in it. The statistics arrives at the very end, and it is borrowed:

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) ℝ: n rows, one per unit, and k columns, one per regressor. Underneath it is nothing but a function 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, XX. And X *ᵥ v is Matrix.mulVec, the matrix–vector product, whose i-th entry is the fitted value jXijvj.

Gram entries, associativity, and what IsOLS unfolds toNew tabOpens with 1,631 lines of library preamble — the code above is at the bottom of the editor.
/-- 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.rfl

Iff.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 k=n and both readings are matrices, or when someone writes 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 2×2 determinant is Matrix.det_fin_two_of, and it is the only determinant this chapter needs.

Vector and matrix literalsNew tabOpens with 1,653 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 x/0=0 for reals, and it has the same consequence: a statement about (XX)1 is never ill-formed, it is just sometimes about zero.

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.

New tabOpens with 1,668 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,668 lines of library preamble — the code above is at the bottom of the editor.
/-- 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.

The definition of an OLS fitNew tabOpens with 1,074 lines of library preamble — the code above is at the bottom of the editor.
/-- `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ᵀ *ᵥ y

IsOLS X y β is a Prop, not a number: it is the plain linear equation XXβ=Xy. It may have one solution, or — when the columns are collinear — infinitely many. Working with the relation rather than with a formula is the single most useful decision in this file, because the interesting theorems (“the coefficient is the difference in means”) hold for every solution, whether or not the design matrix has full rank. No inverse, no rank condition, no minimization.

The equivalent geometric statement is the one most statisticians actually carry in their heads: the residual is orthogonal to every column of X.

ExerciseNormal equations = orthogonality (core)

Column j of X is fun i => X i j, so orthogonality to it is iXij(yi(Xβ)i)=0. 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.

New tabOpens with 1,087 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,087 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 ⊢
    linarith

The textbook formula is a definition in this file, not a theorem, and it is kept firmly separate from IsOLS.

The closed formNew tabOpens with 1,116 lines of library preamble — the code above is at the bottom of the editor.
/-- 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.

New tabOpens with 1,124 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,124 lines of library preamble — the code above is at the bottom of the editor.
/-- 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.

New tabOpens with 1,133 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,133 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 (XX)1Xy” are the same statement. Notice which direction the dependency runs. The formula is a convenience for computing; the relation is what the theorems are about.

Simple regression is the difference in means

Regress the observed outcome on a constant and the treatment indicator. Two columns: ones, and Z.

The design matrixNew tabOpens with 1,186 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 n,n1 on the first row and n1,n1 on the second, where n1=#z is the number of treated units. The only two facts that go into it are iZi=n1 and Zi2=Zi: a dummy is its own square, which is why the (1,1) entry is n1 and not something else.

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.

New tabOpens with 1,209 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,209 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 n1(nn1) (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, written outNew tabOpens with 1,232 lines of library preamble — the code above is at the bottom of the editor.
/-- **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 izyi=(nn1)β0, so β0 is the control mean; the second equation then hands you β1.

TheoremThe slope on a treatment dummy is the difference in means

If β solves the normal equations of the regression of the observed outcome on (1,Z), and both groups are non-empty, then β0 is the control-group mean of y0 and

β1=1n1izYi1n0izYi=τ^DiM.

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 z with both groups non-empty, including the ones the design would never produce. That is what “algebraic identity” means.

New tabOpens with 1,267 lines of library preamble — the code above is at the bottom of the editor.
/-- **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 n1(nn1) is nonzero.

Inheriting a sampling property

Now, and only now, does a design appear.

The one probabilistic theorem in the fileNew tabOpens with 1,321 lines of library preamble — the code above is at the bottom of the editor.
/-- **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 hlt

There is no new argument in that proof and there is not supposed to be. On every assignment the design can produce, #z=n1 (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 population and a realized assignmentNew tabOpens with 1,693 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 decide

The estimand and the estimate:

τ = 13/3, and the difference in means on this assignment is 2New tabOpens with 1,706 lines of library preamble — the code above is at the bottom of the editor.
/-- 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]

213/3. The estimator is unbiased and the estimate is wrong, which is the normal state of affairs and worth seeing spelled out by a proof assistant at least once.

ExerciseCheck a coefficient vector by hand (warm-up)

The observed outcomes are (4,6,3). Claim: β=(3,2) solves the normal equations. The two equations of isOLS_simpleMatrix_iff are 4+6+3=3β0+2β1 and 4+6=2β0+2β1, i.e. 13=13 and 10=10.

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: 3 is the control mean of y0 and 2 is the difference in means.

New tabOpens with 1,722 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,722 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 agreesNew tabOpens with 1,741 lines of library preamble — the code above is at the bottom of the editor.
/-- 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.

One column per groupNew tabOpens with 1,355 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 0

Every row has exactly one 1, so the columns are orthogonal and no matrix algebra is needed at all.

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.

New tabOpens with 1,363 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,363 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 k is i1{gi=k}(yiβgi)=0. The indicator restricts the sum to group k, on which the fitted value is the constant βk, and eq_meanOn_of_sum_eq converts isyi=#sc into c=y¯s.

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.

New tabOpens with 1,372 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,372 lines of library preamble — the code above is at the bottom of the editor.
/-- 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) ?_
  linarith

This 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 (1,Z,x,Zx) for a covariate x. Lin (2013) recommends centering x first; the coefficient on Z is then his covariate-adjusted estimator.

The algebra that makes it tick is a change of basis. First, a name for “the simple regression of y on (1,x), restricted to the units in s”:

A within-group simple regressionNew tabOpens with 1,412 lines of library preamble — the code above is at the bottom of the editor.
/-- `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 y¯sbx¯s. Only the first normal equation is used — the one that says the residuals sum to zero. The slope equation is what would pin b down, and we deliberately never need it, which is why b appears in the conclusion as a free quantity.

Expand is(yi(a+bxi))=0 with 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 #s.

New tabOpens with 1,423 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,423 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  linarith

And the design matrix:

Four columnsNew tabOpens with 1,445 lines of library preamble — the code above is at the bottom of the editor.
/-- 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. β solves the four normal equations if and only if the line (β0+β1)+(β2+β3)x is a least-squares fit among the treated units and the line β0+β2x is a least-squares fit among the controls.

The columns 1,Z,x,Zx span the same space as Z,Zx,(1Z),(1Z)x, and the last two pairs are supported on disjoint sets of units, so the 4×4 system is block diagonal. Every step of the proof uses only Zi2=Zi; the four linear_combination calls are each “multiply Z2=Z by the right scalar”.

New tabOpens with 1,470 lines of library preamble — the code above is at the bottom of the editor.
/-- **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 Z is visibly the gap between two fitted intercepts, and by intercept_eq_of_isSimpleOLSOn each intercept is a center of mass.

Lin's coefficient is a difference of interceptsNew tabOpens with 1,539 lines of library preamble — the code above is at the bottom of the editor.
/-- **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
  linarith

Rearranged, the same identity says what adjustment is doing:

ExerciseDifference in means, minus a covariate correction (stretch)

β1=(y¯1y¯0)(b1x¯1b0x¯0). The first bracket is the raw difference in observed group means; the second is what the fitted slopes say the covariate imbalance alone would have produced. One have := lin_coeff … and a linarith — the exercise is reading the statement, not proving it.

New tabOpens with 1,565 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,565 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  linarith

Both 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 y1, and 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.

New tabOpens with 1,752 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,752 lines of library preamble — the code above is at the bottom of the editor.
/-- 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 against the potential outcomesNew tabOpens with 1,766 lines of library preamble — the code above is at the bottom of the editor.
/-- **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 ixi=0 the two group means are tied together by n1x¯1+n0x¯0=0, so a single scalar imbalance drives the whole correction.

New tabOpens with 1,785 lines of library preamble — the code above is at the bottom of the editor.
/-- 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
  sorry
Show solution
New tabOpens with 1,785 lines of library preamble — the code above is at the bottom of the editor.
/-- 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]
  ring

Reading 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.