cassionData Analysis

Lesson 1 of 8

Unit · A model is a comparison

The coefficient is the difference

Fit attendance against a single feeding dummy. The intercept is 85.21% — the mean of schools with no programme. The coefficient is 4.94 points — the difference between the two means, to four decimal places. A regression with one dummy is a two-group comparison and nothing more.

PythonR150 minSMART surveyUNICEF indicator definitions

The same comparison, written twice

The statistics course compared attendance in schools with a feeding programme against schools without one, and got 90.15% against 85.21%. Fit that as a regression and look at what comes back.

import pandas as pd
import numpy as np
import statsmodels.formula.api as smf

attendance = pd.read_csv("school-attendance-2024.v1.csv")
roster = pd.read_csv("school-roster-2024.v1.csv").drop_duplicates(
    "student_id", keep="last")          # two never-de-registered transfers

MARKS = {"true": True, "Y": True, "false": False, "N": False}
marked = attendance[attendance["present"].isin(MARKS)].copy()
marked["attended"] = marked["present"].map(MARKS)

per_student = (marked.groupby("student_id")["attended"].mean()
               .rename("rate").reset_index()
               .merge(roster, on="student_id"))

model = smf.ols("rate ~ feeding_programme", data=per_student).fit()
print(model.summary().tables[1])
library(dplyr)

per_student |> lm(rate ~ feeding_programme, data = _) |> summary()
Term Coefficient SE t
Intercept 0.8521 0.0070 121.30
feeding_programme[True] 0.0494 0.0088 5.63
means = per_student.groupby("feeding_programme")["rate"].mean()
print(means)
print(f"difference: {means[True] - means[False]:.4f}")
per_student |> summarise(rate = mean(rate), .by = feeding_programme)

Mean without a programme: 0.8521. Mean with: 0.9015. Difference: 0.0494.

The intercept is the first number and the coefficient is the third, exactly. This is not an approximation or a coincidence — with one binary predictor and nothing else, least squares has no freedom to do anything but reproduce the two group means.

What every term means

Three sentences cover the whole of reading a coefficient table, and they hold for every model in this course.

The intercept is the fitted value when every predictor is zero. Here that is feeding_programme = False, so the intercept is the mean of schools with no programme. It is not “the average attendance” and it is not a baseline in any programme sense — it is a specific group’s mean, and which group depends entirely on how the predictors were coded.

A coefficient is the change in the outcome for a one-unit change in that predictor, with the others held fixed. For a dummy, “one unit” is False → True, so the coefficient is a difference between two groups.

A standard error is the same standard error the last course computed. The t column is coefficient over standard error, and the interval is the coefficient plus or minus about two standard errors. Nothing new is being introduced.

coef = model.params["feeding_programme[T.True]"]
se = model.bse["feeding_programme[T.True]"]
print(f"{coef:+.4f}  95% CI [{coef - 1.96*se:+.4f}, {coef + 1.96*se:+.4f}]")
confint(model)

+0.0494, 95% CI +0.0322 to +0.0665 — the same interval, arrived at from the same data by a different route.

Where the two routes differ, and by how much

The regression t is 5.63 and the Welch t-test in the statistics course gave 5.41. That difference is not an error in either.

Ordinary least squares assumes one residual variance for both groups. It is the pooled-variance t-test written in matrix form. Welch’s test does not assume it, which is why the last course recommended Welch by default.

from scipy import stats

fed = per_student[per_student["feeding_programme"]]["rate"]
unfed = per_student[~per_student["feeding_programme"]]["rate"]
print(f"variances: {fed.var():.5f} and {unfed.var():.5f}")
print("pooled t:", stats.ttest_ind(fed, unfed).statistic.round(2))
print("Welch  t:", stats.ttest_ind(fed, unfed, equal_var=False).statistic.round(2))
t.test(rate ~ feeding_programme, data = per_student, var.equal = TRUE)
t.test(rate ~ feeding_programme, data = per_student)

Regression reproduces the pooled t exactly. When the group variances are close the two barely differ; here they are 0.019 and 0.025, and the gap is the reason Welch exists. A regression cannot be told “use Welch” — the fix is a robust standard error, which lesson 6 introduces for a related reason.

Three predictors, three readings

A regression’s usefulness starts when the predictor is not binary. Each type reads differently and each is a comparison.

Predictor Coefficient reads as
Binary (feeding_programme) The difference between the two groups
Continuous (age_years) The change per one year
Categorical with k levels k−1 coefficients, each a difference against the omitted level
enrolment = pd.read_csv("school-enrolment-2024.v1.csv")
current = enrolment[enrolment["school_year"] == 2024]
joined = per_student.merge(
    current[["student_id", "sex", "age_years", "admin2"]], on="student_id")

print(smf.ols("rate ~ admin2", data=joined).fit().params.round(4))
print(joined.groupby("admin2")["rate"].mean().round(4))
lm(rate ~ admin2, data = joined) |> coef()
joined |> summarise(rate = mean(rate), .by = admin2)

A categorical predictor produces one coefficient per level except one, and every coefficient is a comparison against that omitted level. Change which level is omitted and every number in the column changes while the model is identical — which is the first sign that a coefficient means nothing without knowing what it is being compared against.

Say the reference level in the table caption. “Against Artibonite” is four words and it is the difference between a readable table and one a reader has to guess at.

What the model does not say

Two things a coefficient never carries, both of which readers supply for themselves.

It is not an effect. The 4.9-point coefficient is the difference between two sets of schools that were not randomised into having a feeding programme. Every regression in this course is a comparison of groups that already differed; lesson 5 is about which of those differences you can and cannot remove.

It is not a prediction worth making. Regression is taught elsewhere as a prediction machine, and for programme data that framing does damage: it invites model comparison by fit rather than by whether the comparison is the one the report needs. Fit the model your question implies, then read the coefficient it was fitted for, and treat R² as a diagnostic rather than a score — lesson 8 is about a model with an R² of 0.03 that is the most useful result in its report.

Report it whole

Attendance and school feeding, February to April 2024

  Linear model, one predictor, 1,200 students.

  Intercept (no programme)   85.21%
  Feeding programme          +4.94 points   95% CI +3.22 to +6.65

  The coefficient is the difference between the two group means and is
  reported as such. Schools were not randomised into the programme; the
  comparison is observational.

  The standard error assumes independent students. It is not, and lesson 6
  gives the corrected version.

The last line is a promissory note the course pays off, and writing it is better than not knowing it is owed. A model reported with a caveat you can name is in better shape than one reported without.

What comes next

One predictor reproduces a comparison you could have made without a model. The next lesson adds the second predictor, which is where a regression starts earning its keep — and where “adjusting for” gets a precise meaning that is narrower than most reports assume.

Teach this lesson

The lesson as a slide deck, with the prose kept in the speaker notes rather than on the slide. Generated from this page, so it cannot fall out of step with it.

Start the slideshowRead the slides

The PDF needs no software and projects from any machine. The PowerPoint file is there to be edited — add your organisation's branding, cut a section for a shorter session, or merge two lessons into a workshop.