cassionData Analysis

Back to the lessonLesson 2 of 8The counterfactual

A standardised difference of 0.85, where 0.1 is the limit

The same deck as the downloads, rendered as a page. Start the slideshow to present it full screen — arrow keys or a click advance one slide, Escape leaves.

Slides · PDFSlides · PowerPoint

  1. Slide 1 / 26

    What this lesson covers

    • Build the balance table before anything else
    • What the standardised difference is, and why not a p-value
    • What randomisation would have bought
    • When randomisation is refused, and what to say
    • The three questions to ask before an evaluation design
    • Report it whole
    • What comes next
    Speaker notes
    Fifteen schools have the feeding programme and nine do not. They differ on baseline literacy by 0.85 standard deviations and on school size by 0.68, and Centre runs five programme schools to one without. This is what a comparison group looks like when nobody randomised.
  2. Slide 2 / 26

    Build the balance table before anything else — In Python (cont.)

    import pandas as pd
    import numpy as np
    
    enrolment = pd.read_csv("school-enrolment-2024.v1.csv")
    current = enrolment[(enrolment["school_year"] == 2024)
                        & (enrolment["age_years"] <= 20)]   # drop the age outliers
    roster = pd.read_csv("school-roster-2024.v1.csv").drop_duplicates(
        "student_id", keep="last")
    joined = current.merge(roster[["student_id", "school_id", "feeding_programme"]],
                           on="student_id", suffixes=("", "_r"))
    joined["over_age"] = joined["age_years"] > joined["grade"] + 5
    joined["displaced"] = joined["displacement_status"] != "resident"
    
    by = joined.groupby("school_id")
    schools = pd.DataFrame({
        "feeding_programme": by["feeding_programme"].first(),
  3. Slide 3 / 26

    Build the balance table before anything else — In Python (cont.)

        "baseline": by["baseline"].mean(),      # from the assessment file
        "age_years": by["age_years"].mean(),
        "size": by.size(),
        "over_age": by["over_age"].mean(),
        "disability": by["disability_reported"].mean(),
        "displaced": by["displaced"].mean(),
        "district": by["admin2"].first(),
    }).reset_index()
    
    def std_diff(treated, control):
        pooled = np.sqrt((treated.var(ddof=1) + control.var(ddof=1)) / 2)
        return (treated.mean() - control.mean()) / pooled
    
    for column in ["baseline", "age_years", "size", "over_age",
                   "disability", "displaced"]:
        t = schools[schools["feeding_programme"]][column].dropna()
  4. Slide 4 / 26

    Build the balance table before anything else — In Python (cont.)

        c = schools[~schools["feeding_programme"]][column].dropna()
        print(f"{column:12} {t.mean():8.4f} {c.mean():8.4f}"
              f"  std.diff {std_diff(t, c):+.3f}")
  5. Slide 5 / 26

    Build the balance table before anything else — In R

    library(dplyr)
    
    schools |>
      summarise(across(c(baseline, age_years, size, over_age), mean),
                .by = feeding_programme)
  6. Slide 6 / 26

    Build the balance table before anything else

    CharacteristicFeeding (15)No programme (9)Standardised difference
    Baseline literacy54.10%51.05%+0.85
    Mean age9.659.42+0.79
    School size50.344.8+0.68
    Disability reported7.6%9.7%−0.52
    Displaced or returnee23.7%26.9%−0.51
    Over-age39.2%36.8%+0.43
  7. Slide 7 / 26

    Build the balance table before anything else

    • The conventional threshold is 0.10, and every one of the six is above it — four of them by a factor of five or more
    Speaker notes
    The conventional threshold is 0.10, and every one of the six is above it — four of them by a factor of five or more.
  8. Slide 8 / 26

    Build the balance table before anything else — In Python

    print(pd.crosstab(schools["district"], schools["feeding_programme"]))
  9. Slide 9 / 26

    Build the balance table before anything else — In R

    table(schools$district, schools$feeding_programme)
  10. Slide 10 / 26

    Build the balance table before anything else

    DistrictNo programmeFeeding
    Artibonite33
    Centre15
    Nord-Ouest15
    Sud42
  11. Slide 11 / 26

    Build the balance table before anything else

    • Two districts are five-to-one in favour of the programme and one is two-to-four against — Whatever else differs between…
    Speaker notes
    Two districts are five-to-one in favour of the programme and one is two-to-four against. Whatever else differs between Centre and Sud rides along with every comparison of fed against unfed schools.
  12. Slide 12 / 26

    What the standardised difference is, and why not a p-value — In Python

    t = schools[schools["feeding_programme"]]["baseline"]
    c = schools[~schools["feeding_programme"]]["baseline"]
    from scipy import stats
    print(f"std.diff {std_diff(t, c):+.3f}   p = {stats.ttest_ind(t, c).pvalue:.3f}")
  13. Slide 13 / 26

    What the standardised difference is, and why not a p-value — In R

    # Both, and only one of them is the right tool.
  14. Slide 14 / 26

    What the standardised difference is, and why not a p-value

    • Standardised difference +0.85, p = 0.064 — A reviewer reading only the p-value would conclude the schools are balanced…
    • A balance test's p-value confounds imbalance with sample size — which is the opposite of what a balance check needs
    • Report the standardised difference, which does not depend on n — Anything above 0.10 is imbalance you have to handle;…
    Speaker notes
    Standardised difference +0.85, p = 0.064. A reviewer reading only the p-value would conclude the schools are balanced on baseline literacy. They are not; the sample is 24 schools and the test has almost no power. A balance test's p-value confounds imbalance with sample size, which is the opposite of what a balance check needs. Twenty-four units will pass every balance test ever written and two thousand will fail some by chance. Report the standardised difference, which does not depend on n. Anything above 0.10 is imbalance you have to handle; above 0.25 is imbalance that regression adjustment will not rescue.
  15. Slide 15 / 26

    What randomisation would have bought

    • That last clause is the whole value — Adjustment can fix imbalance in baseline literacy because it is in the file
    Speaker notes
    Not balance on everything — balance in expectation, on everything, including the variables you did not measure. That last clause is the whole value. Adjustment can fix imbalance in baseline literacy because it is in the file. It cannot fix imbalance in how motivated the head teacher is, whether the school was chosen because someone advocated for it, or how far it is from the district office — none of which is recorded anywhere.
  16. Slide 16 / 26

    What randomisation would have bought — In Python

    rng = np.random.default_rng(20260729)
    draws = []
    for _ in range(2000):
        assigned = rng.permutation(schools["feeding_programme"].values)
        t = schools["baseline"][assigned]
        c = schools["baseline"][~assigned]
        draws.append(std_diff(t, c))
    print(f"under random assignment, |std.diff| > 0.85 in "
          f"{np.mean(np.abs(draws) > 0.85):.1%} of draws")
  17. Slide 17 / 26

    What randomisation would have bought — In R

    # Permute the assignment 2,000 times and see how unusual the real split is.
  18. Slide 18 / 26

    What randomisation would have bought

    • Randomly reassigning the same 24 schools produces an imbalance this large in 5.3% of 2,000 draws — So the observed…
    Speaker notes
    Randomly reassigning the same 24 schools produces an imbalance this large in 5.3% of 2,000 draws. So the observed split is unusual but not impossible — which is exactly the wrong conclusion to draw from it. The question is not whether this imbalance could have arisen by chance; it is that it did not, because nobody randomised.
  19. Slide 19 / 26

    When randomisation is refused, and what to say

    ObjectionThe honest response
    "We cannot deny a service to needy schools"Randomise the order of a phased roll-out; everyone is served, the sequence is random
    "The ministry chooses the schools"Randomise within the ministry's eligible list, or evaluate the eligibility rule as a threshold design
    "It is already running"Say so, use a comparison design, and report the balance table
    "The donor wants results this year"An underpowered randomised trial and a well-argued difference-in-differences are both defensible; a before-after is not
    Speaker notes
    It usually is, and the reasons are mostly good ones.
  20. Slide 20 / 26

    When randomisation is refused, and what to say

    • A phased roll-out is the most under-used design in this sector — Every programme that expands over three years has a…
    • Where none of that is possible, the balance table is what you owe the reader — It is not a formality: it is the…
    Speaker notes
    A phased roll-out is the most under-used design in this sector. Every programme that expands over three years has a randomisable order, and randomising it costs nothing and forecloses none of the objections above. Where none of that is possible, the balance table is what you owe the reader. It is not a formality: it is the evidence on which they decide how much of the adjustment to believe.
  21. Slide 21 / 26

    The three questions to ask before an evaluation design

    • Who decided who gets the programme, and on what? — If the answer is a rule, you may have a threshold design
    • Is there anything measured before the programme started? — Without a baseline the difference-in-differences of the next…
    • How many units were assigned? — Not how many people were measured — how many schools, clinics, villages, camps
    Speaker notes
    Who decided who gets the programme, and on what? If the answer is a rule, you may have a threshold design. If it is a person's judgement, you have confounding you cannot enumerate. Is there anything measured before the programme started? Without a baseline the difference-in-differences of the next lesson is unavailable and you are left with a cross-section. How many units were assigned? Not how many people were measured — how many schools, clinics, villages, camps. That number sets the precision, and lesson 6 shows what 24 buys.
  22. Slide 22 / 26

    Report it whole — Example (cont.)

    Comparability of feeding and non-feeding schools
    
      15 schools with the programme, 9 without. Assignment was not randomised.
    
      Standardised differences at baseline:
        Baseline literacy     +0.85      School size        +0.68
        Mean age              +0.79      Disability         -0.52
        Displacement          -0.51      Over-age           +0.43
    
      All six characteristics exceed the 0.10 conventional threshold and all six
      exceed 0.25. Programme schools are concentrated in Centre and
      Nord-Ouest (5 of 6 schools in each) and scarce in Sud (2 of 6).
    
      Balance tests are not reported as p-values: with 24 units they would show
      balance regardless. The standardised difference does not depend on n.
    
  23. Slide 23 / 26

    Report it whole — Example (cont.)

      The comparison is adjusted for baseline literacy and district. Adjustment
      cannot address unmeasured differences in why these schools were selected,
      and no result in this report should be read as an effect of the programme
      alone.
  24. Slide 24 / 26

    Report it whole

    • The last paragraph is where an evaluation earns or loses a reviewer — and its length is right: two sentences, stating…
    Speaker notes
    The last paragraph is where an evaluation earns or loses a reviewer, and its length is right: two sentences, stating the limit without apologising for it.
  25. Slide 25 / 26

    What comes next

    • If the two groups differed at baseline, the way to use the baseline is to subtract it.
    Speaker notes
    If the two groups differed at baseline, the way to use the baseline is to subtract it. The next lesson does that formally, gets an estimate of −0.96 points, and spends most of its length on the assumption that makes it meaningful — which two rounds of data cannot test.
  26. Slide 26 / 26

    Where this goes next

    Read the full lesson, with runnable code Back to the lesson