<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Education Methods</title>
<link>https://zl1212-ship-it.github.io/education-methods/</link>
<atom:link href="https://zl1212-ship-it.github.io/education-methods/index.xml" rel="self" type="application/rss+xml"/>
<description>Plain, honest notes on credible impact evaluation and measurement in education.</description>
<generator>quarto-1.10.19</generator>
<lastBuildDate>Thu, 22 Oct 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>How I read a What Works Clearinghouse report (and what it can’t tell me)</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/reading-wwc/</link>
  <description><![CDATA[ 




<p>The first time I used the <a href="https://ies.ed.gov/ncee/wwc/">What Works Clearinghouse</a> to check an intervention, I treated the rating like a verdict: positive means buy, nothing means skip. I read the same pages differently now, because I finally understood what question the WWC is actually answering. It is narrower than the name suggests, and knowing exactly how narrow is what makes the reports useful.</p>
<p>The WWC reviews studies of an intervention and rates the <em>evidence</em>, not the intervention. Its reviewers ask: were these studies designed so that the measured effect is believably the program’s doing? That is an internal-validity question, the same one this blog keeps circling: <a href="../../posts/attrition/">randomization held together by low attrition</a>, <a href="../../posts/01-baseline-table/">groups equivalent at baseline</a>, outcomes measured properly. A strong rating means “this evidence would survive a skeptic.” It does not mean “this will work in your district.”</p>
<section id="the-order-i-read-in-now" class="level2">
<h2 class="anchored" data-anchor-id="the-order-i-read-in-now">The order I read in now</h2>
<p><strong>Start at the bottom: how many studies?</strong> This is the number people skip, and it reframes everything else. An intervention can carry a positive rating on the strength of one qualifying study, sometimes with a few hundred students. That is not a scandal; a single good study beats ten bad ones. But “positive, based on one study in one city” and “positive, based on twelve studies across three grade bands” are different bets, and the rating alone won’t tell you which you’re holding.</p>
<p><strong>Then the size, translated.</strong> The WWC reports effects in an “improvement index”: where the average treated student would land in the comparison group’s distribution, expressed in percentile points. Plus four means the median kid moves to the 54th percentile. It is <a href="../../posts/02-effect-sizes-wwc/">an effect size wearing friendlier clothes</a>, and the friendly clothes make it easy to forget that plus four is modest, and that plus four <em>on average</em> contains classrooms where nothing moved.</p>
<p><strong>Then who was studied.</strong> Grade levels, student populations, settings. The report lists them plainly, and the list is where “works” quietly becomes “worked, for someone.” An intervention validated on ninth-grade English learners in urban districts is a hypothesis, not a finding, about your rural fourth grade.</p>
<p><strong>Only then the rating.</strong> By this point it mostly confirms what the study count and design details already told you.</p>
</section>
<section id="what-the-label-cant-carry" class="level2">
<h2 class="anchored" data-anchor-id="what-the-label-cant-carry">What the label can’t carry</h2>
<p>Three things, honestly. Most interventions you look up will have <em>no</em> studies meeting standards, and <a href="../../posts/null-results/">absence of evidence is not evidence of absence</a>; it usually means nobody has funded a rigorous test yet. The reports say little about cost, training burden, or what implementation actually took, which is where programs live or die in practice. And a review can only be as current as the literature; a rating from studies a decade old describes a program as it was then.</p>
<p>None of this is a complaint. A nutrition label doesn’t cook dinner either. The WWC does one hard thing well, filtering evidence through <a href="../../posts/reading-evaluations/">the same checks I’d want to run myself</a>, and reading it with the grain of that one thing, instead of as a verdict, is what it took me embarrassingly long to learn.</p>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for WWC-aligned baseline-equivalence tables, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>impact evaluation</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/reading-wwc/</guid>
  <pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Difference-in-differences and the leap of faith you can photograph</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/parallel-trends/</link>
  <description><![CDATA[ 




<p>If you read education policy studies, you have met this design more than any other, whether or not the paper said its name. A state adopts a policy and its neighbors don’t. Compare the change in the adopting state to the change in the others, and the difference of those differences is the estimate. Two subtractions: the first removes everything permanently different about the state, the second removes everything that happened to everyone that year.</p>
<p>It is older than it looks. In the 1850s, <a href="https://en.wikipedia.org/wiki/John_Snow">John Snow</a> noticed that one London water company had moved its intake upstream of the sewage while a competitor serving the same neighborhoods had not, and compared how cholera deaths <em>changed</em> for the two sets of customers. He didn’t need the two customer groups to be identical. He needed something subtler, and that subtler thing is still the entire game.</p>
<section id="the-assumption-you-cant-test" class="level2">
<h2 class="anchored" data-anchor-id="the-assumption-you-cant-test">The assumption you can’t test</h2>
<p>Difference-in-differences works only if, absent the policy, the two groups would have moved in parallel. Not been equal; moved in parallel. That counterfactual is unobservable by definition, which is worth sitting with for a second: the workhorse design of policy research rests on an assumption no dataset can verify. The first time this clicked for me, I thought I had misread it. A design this popular, resting on a leap of faith?</p>
<p>The redeeming feature is that the leap of faith leaves footprints. If the two groups were moving in parallel for the five years <em>before</em> the policy, believing they would have continued for one more year is a small step. If their gap was already widening, the design is dead on arrival, and no regression rescues it.</p>
</section>
<section id="what-i-look-for-in-order" class="level2">
<h2 class="anchored" data-anchor-id="what-i-look-for-in-order">What I look for, in order</h2>
<p><strong>The picture.</strong> Outcome over time, both groups, adoption year marked. A flat pre-policy gap is the design showing its work, the same way <a href="../../posts/rdd-credibility/">a discontinuity study owes you the scatter plot</a>. A paper that presents the two-by-two table of averages without the trend lines is asking you to take the photographable part on faith.</p>
<p><strong>Why the policy happened.</strong> Policies are not raindrops; states adopt them in response to something. If a state passed a tutoring law <em>because</em> its scores were sliding, its trend was already different from its neighbors’, in exactly the way the design can’t absorb. The question “what prompted this policy, and was it moving the outcome already?” does more work than most robustness tables.</p>
<p><strong>Who is in the comparison.</strong> “All other states” sounds neutral and rarely is. The honest version says why these comparisons and shows the result holds when you swap them, which is the same discipline as <a href="../../posts/matched-on-observables/">asking who got matched to whom</a>.</p>
<p><strong>Staggered adoption, handled or not.</strong> When many states adopt in different years, the naive version quietly compares late adopters to early ones already treated, and the average can bend or even flip. The fixes exist and are standard now; a recent paper that doesn’t mention the issue is dating itself.</p>
<p>None of this needs code to check, only <a href="../../posts/reading-evaluations/">the willingness to look for what’s missing</a>. Parallel trends really is a leap of faith. The good studies photograph the runway before they jump, and hand you the photo. The rest describe the feeling of flight.</p>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for baseline-equivalence tables, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>causal inference</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/parallel-trends/</guid>
  <pubDate>Thu, 08 Oct 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>If colleges were employers, two-thirds of those we can audit would fail the government’s own fairness test</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/four-fifths/</link>
  <description><![CDATA[ 




<p>Since the 1970s, U.S. employment law has used a simple screen for hiring discrimination called the four-fifths rule: if one group’s selection rate is less than 80% of the highest group’s rate, the process is flagged for scrutiny. A flag is not a finding of discrimination. The bar is deliberately low, closer to a smoke alarm than to a verdict.</p>
<p>I applied that same arithmetic to college completion, using public federal graduation data. For every institution with large enough cohorts to audit (at least 30 students in each group), I divided the share of Black students who complete within 150% of normal time by the share of White students who do at the same institution. In 2022, <strong>66% of the 1,518 auditable institutions fall below the four-fifths line for Black students.</strong> The median institution completes Black students at 70% of its White completion rate. This is not one bad year: the failure share sits between 61% and 66% in every year from 2018 to 2022.</p>
<section id="what-the-flag-does-and-does-not-mean" class="level2">
<h2 class="anchored" data-anchor-id="what-the-flag-does-and-does-not-mean">What the flag does and does not mean</h2>
<p>I want to be careful about what this number means, because the temptation to overread it is strong. An institution below the line has not been shown to discriminate. Completion gaps carry everything students bring with them, from prior schooling to money to the hours they can give to coursework. The four-fifths rule was built for hiring decisions, and lifting a threshold out of employment law and dropping it onto a multi-year educational process is the kind of move fairness researchers call a portability trap (<a href="https://doi.org/10.1145/3287560.3287598">Selbst and colleagues, 2019</a>).</p>
<p>What the rule offers is different: a common, transparent trigger. In hiring, a number like this obliges someone to look. In higher education it obliges no one to do anything. That asymmetry is what stayed with me while I was writing the paper.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/four-fifths/fig-four-fifths.png" class="img-fluid figure-img"></p>
<figcaption>Each institution’s ratio of Black to White completion rates in 2022. The dashed line is the 0.80 threshold employment law uses; the dotted line is parity. Four institutions with ratios above 1.6 are drawn at 1.6.</figcaption>
</figure>
</div>
</section>
<section id="what-the-distribution-says" class="level2">
<h2 class="anchored" data-anchor-id="what-the-distribution-says">What the distribution says</h2>
<p>The distribution tells me more than the headline share does. Most institutions are not sitting just under the line: the median is 0.70, a fifth of auditable institutions are below 0.50, and fewer than 7% complete Black students at or above their White rate. The result also survives the checks I could throw at it, including a stricter reference group, an absolute-gap measure instead of a ratio, and different cohort minimums. Some of the concentration in low-completion institutions turns out to be partly a ratio artifact, and the paper says so. The national pattern does not go away.</p>
</section>
<section id="why-borrow-a-hiring-rule-at-all" class="level2">
<h2 class="anchored" data-anchor-id="why-borrow-a-hiring-rule-at-all">Why borrow a hiring rule at all</h2>
<p>Partly because higher education has no agreed threshold of its own. Completion gaps are reported in many dashboards and cut many ways, and none of them comes with a line that says this one warrants a closer look. Employment law adopted a crude but usable line fifty years ago. Borrowing it does not settle what a college owes its students. What it gave me was a single number that shows how this landscape would look if the standard we already apply to hiring applied here too.</p>
<p>The audit is descriptive, not causal, and the data are public: IPEDS graduation rates, 2018 through 2022, via the Urban Institute Education Data Portal.</p>
<p>The full paper: Liang, Y. (2026). A national disparate-impact audit of college completion equity. <em>Educational Policy</em>. Advance online publication. <a href="https://doi.org/10.1177/08959048261489777">https://doi.org/10.1177/08959048261489777</a></p>
<p>Free accepted manuscript: <a href="https://doi.org/10.35542/osf.io/p2dvf_v1">https://doi.org/10.35542/osf.io/p2dvf_v1</a></p>
<p><em>Data: IPEDS 150%-time graduation rates, 2018 to 2022, via the Urban Institute Education Data Portal; cohorts of at least 30 in each group. The article’s data availability statement points to the full replication materials.</em></p>
<p><em>Related, on how the choice of gap measure changes what you see: <a href="../../posts/02-effect-sizes-wwc/">Effect sizes for education: what WWC actually wants</a>. I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a> and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">Subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>audit findings</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/four-fifths/</guid>
  <pubDate>Thu, 01 Oct 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The students who disappeared are the finding</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/attrition/</link>
  <description><![CDATA[ 




<p>Take an evaluation with a healthy gain for the program group, and flip back to the sample table. One group shrank by a tenth and the other by a quarter before the outcome test, and the gain was computed on whoever remained. Nothing about the arithmetic is wrong. The question, which took me a long time to put into words, is what the arithmetic is still measuring.</p>
<section id="a-trial-is-only-randomized-on-day-one" class="level2">
<h2 class="anchored" data-anchor-id="a-trial-is-only-randomized-on-day-one">A trial is only randomized on day one</h2>
<p>Random assignment buys you two groups that are alike, once, at the start. Every departure after that is chosen, by a family that moved, a student who stopped coming, a school that quit the study, and chosen departures re-sort the groups. If the program’s strugglers give up on the study while the control group’s strugglers have nothing to give up on, the surviving program group is now selected for persistence in a way its comparison is not. This is <a href="../../posts/matched-on-observables/">the enrollment problem</a> sneaking back in through the exit door, after randomization was supposed to have solved it.</p>
<p>That is why attrition is not a footnote about sample size. The evidence standards used to review education RCTs treat it as the first gate: <a href="https://ies.ed.gov/ncee/wwc/Handbooks">the What Works Clearinghouse</a> rates a randomized trial down if attrition is high, before anything else about the analysis is considered.</p>
</section>
<section id="two-numbers-and-the-quieter-one-matters-too" class="level2">
<h2 class="anchored" data-anchor-id="two-numbers-and-the-quieter-one-matters-too">Two numbers, and the quieter one matters too</h2>
<p>Attrition comes in two flavors, and reports love to disclose the first while staying vague on the second. <strong>Overall attrition</strong> is how many outcome measurements went missing in total. <strong>Differential attrition</strong> is the gap between the groups, and it is the louder alarm, because it means whatever drove students out operated differently on the two sides.</p>
<p>But equal rates are not automatic safety. Ten percent can leave each group for different reasons, the discouraged leaving one arm and the upwardly mobile leaving the other, and the comparison bends with no differential attrition at all. The rate is observable; the reasons mostly aren’t. Which is why the honest report does more than disclose percentages.</p>
</section>
<section id="what-the-careful-ones-show" class="level2">
<h2 class="anchored" data-anchor-id="what-the-careful-ones-show">What the careful ones show</h2>
<p>Three things, none of them exotic. A flow diagram: how many were assigned, served, measured, in each group, so the reader isn’t reconstructing it from footnotes. A leavers-versus-stayers comparison: <a href="../../posts/01-baseline-table/">the same baseline table</a> the study already built, rerun on who dropped out, so you can see whether the missing tenth was the struggling tenth. And an analysis that keeps everyone assigned in the group they were assigned to, with a sensitivity check that asks how bad the missing outcomes would have to be to change the conclusion.</p>
<p>When a report instead analyzes “completers,” praises their gains, and moves on, <a href="../../posts/reading-evaluations/">question three of my reading checklist</a> has been answered, just not in words. I say this with sympathy; a completers analysis is usually the easiest query to write, and it is easy to forget that it answers a different question.</p>
</section>
<section id="the-thirty-second-version" class="level2">
<h2 class="anchored" data-anchor-id="the-thirty-second-version">The thirty-second version</h2>
<p>Find the number randomized and the number analyzed, per group; the distance between them is where the story lives. Ask whether the leavers looked like the stayers at baseline. And check whether the headline number is about everyone who started or only those who finished, because “the program worked for the students who stayed” is sometimes the program’s success and sometimes its selection mechanism, and the report’s job is to help you tell which.</p>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for baseline-equivalence tables, including the leavers-versus-stayers kind, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>impact evaluation</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/attrition/</guid>
  <pubDate>Thu, 24 Sep 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The two hours that make an evaluation believable</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/preregistration/</link>
  <description><![CDATA[ 




<p>The most convincing before-and-after in the history of research credibility took two decades to show up in the data. Large heart-disease trials funded by one US agency were, until 2000, positive more often than not: <a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC4526697/">17 of 30 published trials, 57%, reported significant benefits</a>. Then a rule changed. Trials had to register their primary outcome, in public, before the data came in. Of the 25 trials published after the rule, <strong>2</strong> reported significant benefits. Eight percent.</p>
<p>The drugs didn’t get worse in 2000. What changed is that researchers could no longer decide, after seeing the data, which outcome the study had “really” been about. That is the entire idea of pre-registration: a dated, public note, written before the outcome data exist, saying what will be measured, how it will be analyzed, and what would count as success.</p>
<section id="why-such-a-small-thing-does-so-much" class="level2">
<h2 class="anchored" data-anchor-id="why-such-a-small-thing-does-so-much">Why such a small thing does so much</h2>
<p>Almost none of what pre-registration prevents is fraud. It prevents something more human: hopeful flexibility. Every evaluation contains dozens of defensible forks (which outcome, which subgroup, which model, which sample cutoff) and a researcher who wants the program to work can wander those forks in good faith until something significant appears. I wrote about the reader’s side of this, <a href="../../posts/null-results/">the subgroup safari and the migrating outcome</a>, and about <a href="../../posts/reading-evaluations/">the two questions that expose it</a>. Pre-registration is the author’s side: you close the forks yourself, in advance, while you still don’t know which answer flatters you.</p>
<p>The first time I tried to write a registration for a small course study, the embarrassing part was discovering how many decisions I had been planning to make “later,” which is to say, after peeking. Closing them in advance took an afternoon and felt like flossing: unglamorous, obviously correct.</p>
<p>It also, quietly, upgrades every null you’ll ever report. A null against a pre-named target is a finding. The same null without the registration is an anecdote that invites the question “but did you check…?”</p>
</section>
<section id="what-it-doesnt-do" class="level2">
<h2 class="anchored" data-anchor-id="what-it-doesnt-do">What it doesn’t do</h2>
<p>A registration is a plan, not a cage, and it is worth saying what it can’t carry. It doesn’t make a weak design strong; a pre-registered comparison with no comparison group is still a description. Deviating from the plan is fine, science requires it, so long as the deviation is disclosed as one. And exploratory analysis remains welcome, labeled as exploration; the registry is what makes the label meaningful.</p>
</section>
<section id="education-has-the-infrastructure-now" class="level2">
<h2 class="anchored" data-anchor-id="education-has-the-infrastructure-now">Education has the infrastructure now</h2>
<p>Medicine had a twenty-year head start, but education is no longer short of places to do this. The <a href="https://sreereg.icpsr.umich.edu/">Registry of Efficacy and Effectiveness Studies</a>, built by SREE and ICPSR specifically for education research, takes randomized trials, discontinuity designs, and quasi-experiments alike; <a href="https://osf.io/registries">OSF Registrations</a> covers everything else. Funders and evidence reviews increasingly ask for it. The cost is real but small: for a typical evaluation, the registration is an afternoon.</p>
</section>
<section id="how-i-use-one-when-reading" class="level2">
<h2 class="anchored" data-anchor-id="how-i-use-one-when-reading">How I use one when reading</h2>
<p>Three steps, thirty seconds each. Find the registration link (its absence is itself information). Check the date against when outcome data could first have been seen. Compare the registered primary outcome to the one in the abstract. When all three line up, most of the darker explanations for a striking result are simply off the table, and I can spend my skepticism on the design instead.</p>
<p>For anyone running an evaluation this year, from a grad student with her first studies still ahead of her: the two hours it takes are the cheapest credibility you will ever buy. You are not tying your hands. You are making it possible, later, for people like me to believe you.</p>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for baseline-equivalence tables, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>impact evaluation</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/preregistration/</guid>
  <pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The almost-experiment hiding in every cutoff</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/rdd-credibility/</link>
  <description><![CDATA[ 




<p>Education runs on cutoffs. Score below this line and you’re assigned to remediation; above that one and you win the scholarship; the class splits in two when enrollment crosses forty. Every one of those lines quietly mass-produces something precious: pairs of students who are nearly identical, one point apart, treated completely differently.</p>
<p>Drawn as a picture, outcome on one axis and a break at the cutoff line, it looks like a card trick, and my first instinct was to hunt for the catch. That is the whole idea of regression discontinuity, and it has been since <a href="https://doi.org/10.1037/h0044319">two psychologists noticed it in 1960</a> while studying National Merit awards. Students at 79 and 81 points didn’t choose their side of the line the way program volunteers <a href="../../posts/matched-on-observables/">choose to enroll</a>; the cutoff chose for them. Right at the line, assignment is close to a coin flip, and a jump in outcomes right there is about as believable as quasi-experimental evidence gets. Evidence clearinghouses agree: it is one of the few designs the What Works Clearinghouse will rate as highly as a randomized trial, when it is done well.</p>
<p>“When it is done well” is carrying weight in that sentence. Here is what I check, in order, when a report says “we used a regression discontinuity.”</p>
<p><strong>Can anyone nudge themselves across the line?</strong> The design dies quietly if people can sort. If students can retake the test, if teachers round a 59 up to a 60, if families know the threshold in advance, then the two sides of the cutoff are no longer alike; the strivers have bunched up on the winning side. There is a standard check, plotting the density of scores around the cutoff to look for suspicious pile-ups, and a report that doesn’t show it is skipping the design’s load-bearing wall.</p>
<p><strong>Does anything else change at the same line?</strong> Cutoffs are administratively convenient, so programs stack onto them. If the same score that assigns tutoring also triggers a parent letter and a schedule change, the jump you see is the bundle, not the tutoring.</p>
<p><strong>Can I see the jump?</strong> The great virtue of this design is that the evidence is a picture: outcome on the y-axis, running score on the x-axis, a visible break at the line. When a paper reports a discontinuity but shows no scatter plot, or the plotted jump is invisible without the fitted lines to guide your eye, my confidence drops accordingly.</p>
<p><strong>Does the effect survive the bandwidth?</strong> The analyst chooses how wide a window around the cutoff to use. Narrow windows compare the most similar students but leave few of them; wide ones buy sample size with comparability. An honest report shows the estimate across several windows. An effect that appears at exactly one bandwidth was found, not estimated.</p>
<p><strong>Who is the effect about?</strong> This is the design’s built-in modesty, and it gets lost in summaries. A discontinuity estimates the effect <em>for students near the cutoff</em>. What remediation does for a student one point below the line may say little about what it does for a student thirty points below, who is the student the policy debate is usually about. The design answers a narrow question extremely well, and the fine print is the word “narrow.”</p>
<p>The catch I was hunting for turned out to be real, and it is the list above; the design is only as magic as its checks. None of them needs software; they need the report to have shown its work, which is the same standard as <a href="../../posts/reading-evaluations/">the rest of my reading checklist</a>. A discontinuity study that shows the density check, the raw picture, and the bandwidth sweep has done something genuinely close to an experiment, without anyone having to randomize a child. One that shows a coefficient and a citation has shown me a line in a table.</p>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for baseline-equivalence tables, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>



 ]]></description>
  <category>causal inference</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/rdd-credibility/</guid>
  <pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>The evaluation came back null. Now what?</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/null-results/</link>
  <description><![CDATA[ 




<p>Here is the most useful fact I know about education evaluations. In 2019, two researchers collected 141 large randomized trials commissioned by the two most serious funders of education evidence in the US and the UK, covering 1.2 million students. The average effect across all of them was <a href="https://journals.sagepub.com/doi/10.3102/0013189X19832850">0.06 standard deviations</a>, roughly nothing, and the confidence intervals were so wide (0.30 standard deviations on average) that many trials couldn’t distinguish “small effect” from “no effect” at all.</p>
<p>So when your evaluation comes back null, the first thing to know is that you are holding the <em>normal</em> result. I find this fact genuinely comforting, the way weather statistics are comforting when your picnic gets rained out in Seattle. The question is not how to apologize for it. The question is what kind of null you have, and I work through that in a fixed order.</p>
<section id="first-is-the-null-informative-or-just-imprecise" class="level2">
<h2 class="anchored" data-anchor-id="first-is-the-null-informative-or-just-imprecise">First: is the null informative, or just imprecise?</h2>
<p>“No detectable effect” covers two different findings. A tight confidence interval around zero says something real: effects big enough to care about probably aren’t there. A wide interval mostly says the study couldn’t answer the question, which is what happened in many of those 141 trials. The way to tell them apart is to <a href="../../posts/02-effect-sizes-wwc/">translate the interval into units a reader can feel</a>: “we can rule out gains larger than a month of learning” and “we can’t rule out anything between harm and half a year of gains” deserve opposite reactions.</p>
</section>
<section id="second-could-the-study-have-hidden-a-real-effect" class="level2">
<h2 class="anchored" data-anchor-id="second-could-the-study-have-hidden-a-real-effect">Second: could the study have hidden a real effect?</h2>
<p>Before concluding the program failed, I check whether the design could have produced a null even if the program worked.</p>
<ul>
<li><strong>The outcome couldn’t see the change.</strong> An unreliable or off-target measure shrinks every effect toward zero, which is why I check <a href="../../posts/03-measurement-before-models/">measurement before models</a>, and why an outcome that was itself <a href="../../posts/text-classifier-error/">machine-classified with error</a> attenuates further.</li>
<li><strong>The score doesn’t mean the same thing in both groups.</strong> A null gap on a scale that <a href="../../posts/measurement-invariance/">functions differently across groups</a> is not evidence of equality, in either direction.</li>
<li><strong>The groups weren’t comparable to start.</strong> If <a href="../../posts/01-baseline-table/">baseline equivalence</a> failed, a real gain in one group can be canceled by a head start in the other. The same worry applies to <a href="../../posts/matched-on-observables/">matched comparisons</a>, where the invisible reasons people enrolled can mask or mimic effects.</li>
<li><strong>The outcome was aggregated away.</strong> Composites hide movement; <a href="../../posts/value-added-scores/">what a summary score actually captures</a> and <a href="../../posts/composite-verdict/">how the weights were set</a> both decide whether a real change surfaces.</li>
</ul>
</section>
<section id="third-is-the-null-the-honest-number-at-last" class="level2">
<h2 class="anchored" data-anchor-id="third-is-the-null-the-honest-number-at-last">Third: is the null the honest number, at last?</h2>
<p>The mirror image matters just as much. Analyses that <a href="../../posts/clustering-independence/">treat clustered students as independent</a> produce stars out of noise, and <a href="../../posts/controls-collider/">piling on control variables</a> can manufacture effects as easily as destroy them. Sometimes a well-analyzed null is not the study failing; it is the first analysis in the room that stopped flattering the program.</p>
</section>
<section id="then-write-it-like-a-result-because-it-is-one" class="level2">
<h2 class="anchored" data-anchor-id="then-write-it-like-a-result-because-it-is-one">Then write it like a result, because it is one</h2>
<p>Lead with the finding, in the first paragraph, in plain words. State what the interval rules out, not just the p-value. Point to what success was defined as in advance, if anything was, since a null against a pre-named target is a result while a null against nothing is an anecdote. Keep subgroup patterns, labeled as exploratory, for the next study to test. And separate the two sentences a decision-maker actually needs: “it probably doesn’t work” and “we couldn’t tell,” one of which should appear, alone, in your summary. If you want to see these habits from the reader’s side, they are <a href="../../posts/reading-evaluations/">questions seven and ten on my checklist</a>.</p>
<p>I’m still in grad school; nobody has yet paid me to deliver bad news to a funder, and I don’t imagine the room feels like a statistics seminar. But a clean null saves someone real money, usually the district about to scale the program. And it compounds: people who watch you report a null clearly will believe your positive finding next year at face value. In a field where the average large trial finds 0.06 standard deviations, the evaluators who can say “nothing happened” gracefully are not the failures. They are the ones telling the truth at the base rate.</p>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for baseline-equivalence tables, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>impact evaluation</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/null-results/</guid>
  <pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>“We matched participants to similar students”: what that buys, and what it can’t</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/matched-on-observables/</link>
  <description><![CDATA[ 




<p>For a long time, the sentence “participants were matched to similar non-participants on grades, attendance, and demographics” quietly ended my skepticism. It sounds like the problem of comparison has been handled. The groups look alike; surely the difference that remains is the program.</p>
<p>What changed my reading was one question, which I now ask about every matched study: <em>if two students look identical in the data, why did only one of them sign up?</em></p>
<section id="what-matching-actually-does" class="level2">
<h2 class="anchored" data-anchor-id="what-matching-actually-does">What matching actually does</h2>
<p>Matching does one thing, and does it honestly: it makes the comparison group resemble the participants <strong>on the variables you matched</strong>. Same prior grades, same attendance band, same school poverty level. That is real progress; an unmatched comparison would be worse. After matching, the observable excuses are gone.</p>
</section>
<section id="what-it-cannot-do" class="level2">
<h2 class="anchored" data-anchor-id="what-it-cannot-do">What it cannot do</h2>
<p>The trouble is that the reason a student enrolls is usually not a column in the file. A tutoring program’s sign-ups differ from non-sign-ups in motivation, in whether a parent pushed, in whether a counselor happened to mention it. Matching on grades cannot touch any of that, because none of it is recorded. The technical phrase is “selection on unobservables”; the plain version is that <strong>matching removes the differences you can see, and the decision to participate is driven partly by differences you can’t.</strong></p>
<p>There is a subtler version of the trap that took me longer to see. Match a signed-up student with low prior grades to a non-participant with the same low grades, and you may have paired the <em>most</em> motivated student in one group (low grades, still sought help) with an ordinary student in the other. Matching on the observable can quietly sharpen the unobservable difference, not shrink it.</p>
</section>
<section id="the-four-things-i-look-for-in-a-matched-study" class="level2">
<h2 class="anchored" data-anchor-id="the-four-things-i-look-for-in-a-matched-study">The four things I look for in a matched study</h2>
<p><strong>A balance table, before and after.</strong> Matching is a claim about achieved similarity, and the claim is checkable: <a href="../../posts/01-baseline-table/">the same baseline-equivalence table any comparison-group study owes</a>, shown for the matched sample. No table, no claim.</p>
<p><strong>The enrollment story.</strong> I try to write one honest sentence about how a student ended up in the program. If the sentence contains a word like “chose,” “volunteered,” or “was referred,” I ask whether two students with identical files could plausibly differ on exactly that word. Usually they could.</p>
<p><strong>Who got dropped.</strong> Matching discards participants with no good counterpart, which changes the question from “did the program work?” to “did it work for the kind of participant who has a look-alike?” A report should say how many were dropped and who they were.</p>
<p><strong>A sensitivity analysis.</strong> The best matched studies ask: how strong would a hidden difference have to be to erase this effect? Reporting that number doesn’t remove the assumption; it prices it. When it’s missing, I price it myself, pessimistically.</p>
</section>
<section id="the-right-amount-of-trust" class="level2">
<h2 class="anchored" data-anchor-id="the-right-amount-of-trust">The right amount of trust</h2>
<p>None of this makes matching a trick. It is an honest tool for a narrower claim than the one usually attached to it, something like: <em>among students who look alike on what we measured, participants did better.</em> Studies that state the claim at that size, show the balance, and price the assumption are the ones I trust most. The reassuring sentence at the top was never the evidence. The table under it is.</p>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for exactly that balance table (WWC-aligned, before and after matching), and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>causal inference</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/matched-on-observables/</guid>
  <pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>How I read an evaluation report I’m inclined to believe</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/reading-evaluations/</link>
  <description><![CDATA[ 




<p>The evaluation reports that fool me are never the sloppy ones. They are the ones whose conclusion I already agree with. I’m a master’s student, so I read a lot of these on assignment, and the routine below is what I have settled on: ten questions in a set order, precisely so that my sympathy for the finding doesn’t get a vote. None of them needs software. Most can be answered from the report itself, and when one can’t, that absence is usually the answer.</p>
<p><strong>1. Compared to whom?</strong> Before any number, I look for the comparison group. “Participants improved” is not a finding; students improve on their own, programs or not. If there is no comparison group at all, I stop expecting a causal claim and read the rest as description.</p>
<p><strong>2. Were the groups similar before the program?</strong> The whole causal claim rests here, and it is checkable from one table. I wrote about <a href="../../posts/01-baseline-table/">what that table has to show</a> and why a p-value is the wrong check; a report that never shows baseline equivalence is asking me to take the hardest part on faith.</p>
<p><strong>3. Who disappeared?</strong> Enrollment counts at the start, outcome counts at the end. If a quarter of the treatment group is missing from the final table, the program’s effect is now entangled with who stuck around, and I want the report to say how the leavers differed, not just how many left.</p>
<p><strong>4. Was the outcome measured the same way for both groups?</strong> Same test, same timing, same raters. A program grades its own homework more often than you’d think, sometimes literally. When groups are compared on a score, I also want to know <a href="../../posts/measurement-invariance/">the score means the same thing in both</a>.</p>
<p><strong>5. How big is the effect, in units I can feel?</strong> “Significant” tells me about sample size as much as anything. I look for the effect in months of learning, points on a real test, or a standardized effect size I can compare, <a href="../../posts/02-effect-sizes-wwc/">the way evidence clearinghouses ask for it</a>.</p>
<p><strong>6. Were students counted as if they were independent?</strong> Twenty-five students in one classroom share a teacher, a curriculum, a snow day. Analyses that ignore this <a href="../../posts/clustering-independence/">make everything look significant</a>. The fix is standard; its absence in 2026 is informative.</p>
<p><strong>7. How many outcomes were measured, and how many reported?</strong> Ten outcomes and one star is a different story from one pre-named outcome and one star. I look for a protocol, a pre-registration, or at least a full outcome table with the nulls left in.</p>
<p><strong>8. What was controlled for, and why?</strong> More controls is not automatically better; <a href="../../posts/controls-collider/">some variables make the estimate worse when you adjust for them</a>. I read the control list asking “is this a cause of enrollment, or a consequence of the program?”</p>
<p><strong>9. Who paid, and who analyzed?</strong> Not as an accusation, as a base rate. Evaluations commissioned by the program’s vendor come out favorable more often, and the mechanism is rarely fraud; it is which outcomes, sites, and cohorts made it into the report. Independence is a design feature like any other.</p>
<p><strong>10. What would failure have looked like?</strong> My favorite question, and the one reports answer least often. If every plausible result could be narrated as success, the evaluation was not a test. When a report states in advance what result would have counted against the program, everything else in it gets more believable.</p>
<section id="what-a-checklist-can-and-cant-do" class="level2">
<h2 class="anchored" data-anchor-id="what-a-checklist-can-and-cant-do">What a checklist can and can’t do</h2>
<p>Ten questions can’t convict a report, and passing them doesn’t make a finding true. Most of them cover mistakes that are easy to make and easy to miss when reading. This is triage: it tells me where to spend skepticism, which is always in shorter supply than I’d like, and it protects me a little from my own preferences. The reports I end up trusting most are rarely the ones with the biggest effects. They are the ones that make questions 1 through 10 easy to answer, because the authors asked themselves first.</p>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for the baseline-equivalence table in question 2, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>impact evaluation</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/reading-evaluations/</guid>
  <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>I ranked the same eight universities two defensible ways. The list nearly reversed.</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/composite-verdict/</link>
  <description><![CDATA[ 




<p>I keep telling people that a school rating depends on the weights inside the formula, and I realized recently that I had never actually watched it happen with numbers I could check. So I built the smallest real ranking I could.</p>
<p>I took the eight public four-year universities in Washington, where I studied, and two numbers per school from the 2021 federal IPEDS collection. The first is the six-year graduation rate for bachelor’s students: of those who start, how many finish. The second is Pell share: what fraction of undergraduates receive a federal Pell grant, a rough marker of how many lower-income students a campus serves. Neither number is obscure and neither is controversial to care about. A public university is supposed to graduate its students, and it is supposed to be open to students without money.</p>
<p>Then I did what every rating system does. I put the two on a common scale, blended them with a weight, and ranked. The only thing I varied was the weight, in five steps from all-graduation to all-Pell.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/composite-verdict/fig-bump.png" class="img-fluid figure-img"></p>
<figcaption>Eight universities, five blends of the same two 2021 numbers. Orange schools move at least four positions between the two ends; the 50/50 column is where the blend, not either single measure, decides the order.</figcaption>
</figure>
</div>
<section id="what-surprised-me" class="level2">
<h2 class="anchored" data-anchor-id="what-surprised-me">What surprised me</h2>
<p>I expected some reshuffling in the middle. I did not expect the list to nearly turn over. UW Seattle is first when the weight is on graduation (84% of its students finish, the highest here) and last when the weight is on Pell share (17% of its undergraduates receive Pell, the lowest here). Evergreen State is the exact mirror: last on one end, first on the other. These aren’t two rankings of quality and its absence. They are two different questions, and each school answers one of them well.</p>
<p>The reason the reversal is so clean is worth stating: in this group of eight, the two measures are strongly negatively related. The campuses that graduate the largest share of students enroll the smallest share of Pell students. I don’t get to conclude much from eight schools about why that is, but it has an uncomfortable implication for anyone building a rating: there is no weight between these two numbers that is neutral. Every blend takes a side, quietly.</p>
</section>
<section id="what-this-little-exercise-cant-say" class="level2">
<h2 class="anchored" data-anchor-id="what-this-little-exercise-cant-say">What this little exercise can’t say</h2>
<p>A lot, honestly. Two indicators is a cartoon; real rating systems use many, and better ones. A graduation rate reflects who enrolls at least as much as what a campus does for them. Pell share is a coarse stand-in for access. And my choices leaked in everywhere: I checked the endpoints of the figure under two other reasonable ways of scaling the numbers and they came out the same, but the 50/50 column did not. Even the scaling is a decision.</p>
<p>Above all, this is not a ranking of Washington’s universities, and none of these schools is “worst.” That is closer to being the point: any single ordered list of them would be hiding a judgment call in the weights.</p>
</section>
<section id="what-i-do-differently-now" class="level2">
<h2 class="anchored" data-anchor-id="what-i-do-differently-now">What I do differently now</h2>
<p>When I see a composite rating, I try to find two things before reacting to any school’s position: what is weighted how much, and whether the publisher shows how the order moves under other defensible weightings. When that sensitivity is reported, I trust the list more, not less.</p>
<p>I’m working on audits of real rating and flagging systems with public data, and I’ll write them up here as they become shareable.</p>
<p><em>Update, October 2026: the first of these is out. <a href="../../posts/four-fifths/">If colleges were employers, two-thirds of those we can audit would fail the government’s own fairness test</a>.</em></p>
<p><em>The data (a <a href="wa_publics_2021.csv">small CSV</a> pulled from the Urban Institute’s public IPEDS API) and the figure script (<a href="make_figure.py"><code>make_figure.py</code></a>) are in this post’s folder; deleting the CSV and rerunning the script re-fetches the numbers.</em></p>
<p><em>Related: <a href="../../posts/value-added-scores/">What a “teacher effectiveness” score actually measures (and what it doesn’t)</a>. I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a> and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>audit findings</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/composite-verdict/</guid>
  <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>More controls ≠ less bias: the variables you should never put in your model</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/controls-collider/</link>
  <description><![CDATA[ 




<p>Okay so. Reviewers often ask for one more control. You add the variable, the coefficient changes, and it is tempting to take the change as proof the variable mattered.</p>
<p>It is not proof the variable mattered. It took me way longer than I’d like to admit to get that.</p>
<p>Adding a control and watching the number jump feels productive. It feels careful. But a number moving only tells you a number moved. It doesn’t tell you whether you got closer to the truth or further from it. That depends on what the variable actually is, and “just throw in more controls” quietly lumps together three things that behave nothing alike.</p>
<section id="three-reasons-a-variable-is-in-your-model" class="level2">
<h2 class="anchored" data-anchor-id="three-reasons-a-variable-is-in-your-model">Three reasons a variable is in your model</h2>
<p>One: it’s a <strong>confounder</strong>. Something that causes both who got treated <em>and</em> the outcome. Pretest scores, if the higher scoring kids were also more likely to get the program. You want these in the model. Leaving them out is the thing everyone’s (rightly) scared of.</p>
<p>Two: it’s a <strong>mediator</strong>. Something the treatment causes, which then moves the outcome. It’s part of how the program works. Control for it and you’re literally subtracting off a piece of the effect you came to measure.</p>
<p>Three: it’s a <strong>collider</strong>. The weird one. It’s a shared <em>consequence</em> of two things, and it sits there harmlessly until you condition on it, at which point it invents a correlation that was never real.</p>
<p>“Control for everything you can” treats all three the same. Two of them make your answer worse.</p>
</section>
<section id="just-draw-the-arrows" class="level2">
<h2 class="anchored" data-anchor-id="just-draw-the-arrows">Just draw the arrows</h2>
<p>This is less philosophical than it sounds. Sketch what causes what (a DAG, if we’re being formal). Then the rule is basically mechanical: block the paths that sneak in <em>through</em> the treatment, and leave alone anything the treatment causes or anything two variables jointly cause.</p>
<p>A reviewer saying “control for X” is making a guess about where X lives on that picture. It’s not a command. Go look at where it actually sits.</p>
</section>
<section id="the-one-that-got-me" class="level2">
<h2 class="anchored" data-anchor-id="the-one-that-got-me">The one that got me</h2>
<p>Say you’re evaluating a tutoring program and your outcome is test scores. Being responsible, you decide to control for student motivation. Reasonable, except you measured motivation <em>after</em> the program ran. Which means the program probably moved it. So now motivation is downstream of the treatment, and controlling for it shaves off exactly the part of the effect that went “kids got more into it, then did better.” You conclude tutoring barely did anything.</p>
<p>It did something. You just deleted the evidence. And it felt like diligence the whole time, because, again, the coefficient moved.</p>
</section>
<section id="watch-it-happen" class="level2">
<h2 class="anchored" data-anchor-id="watch-it-happen">Watch it happen</h2>
<p>I simulated this so it’s not just me asserting things. True total effect of the program: 8.5. There’s a trait <code>C</code> that drives both treatment and the outcome (a real confounder), and <code>M</code> is motivation measured after the program (a mediator).</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Estimand: total effect of `treat` on `Y` (simulated; true value = 8.5)</span></span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">coef</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(Y <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> treat))[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"treat"</span>]          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># naive</span></span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">coef</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(Y <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> treat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> C))[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"treat"</span>]      <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># adjust for the confounder</span></span>
<span id="cb1-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">coef</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(Y <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> treat <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> C <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> M))[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"treat"</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># also adjust for the post treatment mediator</span></span></code></pre></div></div>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Model</th>
<th style="text-align: right;">Estimate</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Naive (<code>Y ~ treat</code>)</td>
<td style="text-align: right;"><strong>11.46</strong></td>
</tr>
<tr class="even">
<td>+ confounder (<code>Y ~ treat + C</code>)</td>
<td style="text-align: right;"><strong>8.61</strong></td>
</tr>
<tr class="odd">
<td>+ mediator too (<code>Y ~ treat + C + M</code>)</td>
<td style="text-align: right;"><strong>3.99</strong></td>
</tr>
</tbody>
</table>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/controls-collider/fig-overcontrol.png" class="img-fluid figure-img"></p>
<figcaption>Three estimates of the program effect; only adjusting for the confounder recovers the truth.</figcaption>
</figure>
</div>
<p>The naive number is too big because treated kids started ahead. Adjust for the confounder and it snaps to 8.61, basically the truth. Then you add the after the fact mediator and it falls to 3.99, because you just controlled away the thing the program works through. The model with the <em>most</em> controls is the most wrong. That’s the whole point.</p>
</section>
<section id="what-i-actually-do-now" class="level2">
<h2 class="anchored" data-anchor-id="what-i-actually-do-now">What I actually do now</h2>
<ul>
<li>Sketch the DAG before I touch the model. A bad drawing on a receipt counts.</li>
<li>Only control for things measured <em>before</em> treatment.</li>
<li>If a variable was measured after the treatment, I treat it as off limits until the picture tells me otherwise.</li>
<li>For every control, I make myself say which of the three it is. If I can’t, it doesn’t go in.</li>
</ul>
<p>I’m not against controls. I’m anti “more controls = more rigor,” because that one’s just false, and it’s the kind of false that looks like care.</p>
<hr>
<p><em>I’m building <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for WWC-aligned baseline equivalence, and a cohort course on running education evaluations you can actually trust. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> if that’s your thing.</em></p>


</section>

 ]]></description>
  <category>causal inference</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/controls-collider/</guid>
  <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Measurement before models: the check most evaluations skip</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/03-measurement-before-models/</link>
  <description><![CDATA[ 




<p>The saddest sentence in program evaluation is “the program didn’t work,” when what actually happened was “we measured it with a broken ruler.” Those two get mixed up constantly, and the second one wears the first one’s clothes.</p>
<p>Here’s the part that stings: teams will spend months on the design, the randomization, the comparison group, the matching, and then about ten minutes on the thing they actually measure. And then they’re surprised by a null result.</p>
<section id="what-a-noisy-outcome-quietly-does" class="level2">
<h2 class="anchored" data-anchor-id="what-a-noisy-outcome-quietly-does">What a noisy outcome quietly does</h2>
<p>Random measurement error in your outcome doesn’t just scatter the points around a bit. It inflates the variance of the outcome, which widens your standard errors, which costs you power. And because the standard deviation in your effect size denominator just got bigger, the standardized effect you can report <em>shrinks</em>. You can run a flawless design on a bad measure and find nothing, not because the program failed, but because your ruler was made of rubber.</p>
<p>So before the model, check the ruler.</p>
</section>
<section id="reliability-what-one-number-hides" class="level2">
<h2 class="anchored" data-anchor-id="reliability-what-one-number-hides">Reliability: what one number hides</h2>
<p>Reliability usually shows up as a single coefficient, Cronbach’s alpha most of the time, and gets a nod on the way to the regression. But that one number is hiding things. Alpha climbs with the number of items, not only their quality, so a long mediocre scale can post a comfortable one. It leans on the items being roughly interchangeable, and when they aren’t it can mislead in either direction (which is why a lot of measurement people reach for McDonald’s omega instead). And a high alpha is not proof your items measure one thing.</p>
<p>A “respectable” reliability can also just be too low for <em>your</em> claim. Detecting a small effect on a moderately unreliable outcome can demand far more sample than you have. The reliability you need is a function of the effect you’re chasing, not a fixed bar you clear once and forget.</p>
</section>
<section id="validity-briefly" class="level2">
<h2 class="anchored" data-anchor-id="validity-briefly">Validity, briefly</h2>
<p>Reliability is consistency. Validity is aboutness. A bathroom scale that reads three pounds heavy every single time is perfectly reliable and perfectly wrong. For an outcome, the evidence worth showing is whether the items cover the construct, whether the data back the dimensions you claim, and whether the measure relates to things it should and not to things it shouldn’t. You don’t need all of it for every study. You do need to stop claiming more than your evidence supports.</p>
</section>
<section id="a-check-you-can-actually-run" class="level2">
<h2 class="anchored" data-anchor-id="a-check-you-can-actually-run">A check you can actually run</h2>
<p>Before you trust an outcome, look at it. On your own data, not invented numbers, look at the item behavior and an internal consistency estimate for each key construct:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># On YOUR data. Do not report invented coefficients.</span></span>
<span id="cb1-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># items &lt;- your_data[, c("q1", "q2", "q3", "q4")]</span></span>
<span id="cb1-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># psych::alpha(items)       # internal consistency + item level diagnostics</span></span>
<span id="cb1-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># psych::omega(items)       # model based reliability</span></span>
<span id="cb1-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># psych::fa.parallel(items) # how many dimensions are actually in there?</span></span></code></pre></div></div>
<p>Read the item level output, not just the headline. An item that barely correlates with the rest, or a “scale” that turns out to be two factors in a trench coat, will tell you more than alpha ever does alone.</p>
</section>
<section id="why-this-goes-before-the-impact-model" class="level2">
<h2 class="anchored" data-anchor-id="why-this-goes-before-the-impact-model">Why this goes before the impact model</h2>
<p>Because everything downstream inherits it. Here’s the same point, simulated. The program has a <em>real</em> effect, true standardized 0.5. The only thing I change is how reliably the outcome is measured:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th style="text-align: right;">Outcome reliability</th>
<th style="text-align: right;">Observed effect</th>
<th style="text-align: right;">Power</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: right;">1.00</td>
<td style="text-align: right;">0.50</td>
<td style="text-align: right;">70%</td>
</tr>
<tr class="even">
<td style="text-align: right;">0.80</td>
<td style="text-align: right;">0.45</td>
<td style="text-align: right;">60%</td>
</tr>
<tr class="odd">
<td style="text-align: right;">0.60</td>
<td style="text-align: right;">0.39</td>
<td style="text-align: right;">48%</td>
</tr>
<tr class="even">
<td style="text-align: right;">0.40</td>
<td style="text-align: right;">0.31</td>
<td style="text-align: right;">34%</td>
</tr>
</tbody>
</table>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/03-measurement-before-models/fig-attenuation.png" class="img-fluid figure-img"></p>
<figcaption>As outcome reliability falls, the observed effect and the power both fall.</figcaption>
</figure>
</div>
<p>Same program. Same design. Same sample. Only the ruler gets noisier, and the effect you’d report slides from 0.50 to 0.31 while your power to see <em>anything</em> falls from 70% to 34%. Half your ability to detect a real effect, gone, because the outcome measure was weak.</p>
<p>Measurement isn’t the boring prelude to the real analysis. On a bad measure, it <em>is</em> the result. Check the ruler before you trust the measurement.</p>
<hr>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a> and am developing a cohort course on credible evaluation and measurement in education, where this is basically a whole week. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>measurement</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/03-measurement-before-models/</guid>
  <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>What a ‘teacher effectiveness’ score actually measures (and what it doesn’t)</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/value-added-scores/</link>
  <description><![CDATA[ 




<p>Somewhere right now a teacher is being handed a number that says she went from the 40th percentile to the 70th in a single year, and a district is quietly deciding what that number says about her. Maybe a bonus. Maybe a “see, the system works.”</p>
<p>I want to talk about how little that number might actually be saying, because the people on the other end of it don’t get to see the error bars.</p>
<section id="what-its-trying-to-do" class="level2">
<h2 class="anchored" data-anchor-id="what-its-trying-to-do">What it’s trying to do</h2>
<p>A value added model starts from a fair idea. Predict each kid’s score from where they started and a few covariates, and credit the teacher with the part that beat the prediction. Net out the head start, reward the gain. I don’t think the goal is wrong.</p>
<p>The problem is that the teacher’s real contribution is <em>small</em> next to everything else shoving those scores around, and the one thing the model can’t fully scrub out is the biggest one: kids are not handed to teachers at random.</p>
</section>
<section id="whats-actually-inside-the-score" class="level2">
<h2 class="anchored" data-anchor-id="whats-actually-inside-the-score">What’s actually inside the score</h2>
<p>The number says “effectiveness,” but it’s carrying a lot of passengers. Some of it is real teaching. Some of it is sorting, because if the stronger or better supported kids keep landing with certain teachers, adjusting for prior scores shrinks that bias but never kills it, and whatever you didn’t measure rides straight through. Some of it is test measurement error, which goes in one end and comes out the other as “effectiveness.” Some of it is just small samples, a teacher judged on one or two classrooms, where a couple of unusual kids swing the whole thing. And some of it is the test’s scale.</p>
<p>Stack all of that up and the year to year stability of these scores tends to be modest, well below what a high stakes ranking quietly assumes.</p>
</section>
<section id="watch-it-happen" class="level2">
<h2 class="anchored" data-anchor-id="watch-it-happen">Watch it happen</h2>
<p>I gave 300 simulated teachers a fixed, unchanging true effectiveness. It does not move. The only thing that changes between year one and year two is noise: small classes, test error, the luck of the draw.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1">vam1 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> mu <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> noise   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># year 1 estimate</span></span>
<span id="cb1-2">vam2 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> mu <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> noise   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># year 2 estimate, same true mu, fresh noise</span></span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cor</span>(vam1, vam2)      <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># year to year reliability</span></span></code></pre></div></div>
<p>The year to year reliability comes out at <strong>0.47</strong>. Of the teachers in the bottom quartile one year, <strong>23% land above the median the next</strong>, same people, same skill. And a representative teacher slides from the <strong>42nd to the 69th percentile</strong> between years without changing a thing.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/value-added-scores/fig-vam-churn.png" class="img-fluid figure-img"></p>
<figcaption>Year 1 vs year 2 percentile ranks scatter widely around the diagonal; true skill is unchanged.</figcaption>
</figure>
</div>
<p>That jump from the low 40s to the high 60s, the one a district would frame as a turnaround? Pure noise. The scatter says it flatly: where a teacher ranks one year barely tells you where they’ll rank the next.</p>
</section>
<section id="and-then-it-gets-worse" class="level2">
<h2 class="anchored" data-anchor-id="and-then-it-gets-worse">And then it gets worse</h2>
<p>Once a number carries stakes, Campbell’s Law shows up: the moment a metric becomes a target, people optimize the metric instead of the thing it was supposed to stand for. Teaching to the test. Quietly steering which kids end up in your room. Narrowing what gets taught. The score stops being even the noisy proxy it started as.</p>
</section>
<section id="what-to-do" class="level2">
<h2 class="anchored" data-anchor-id="what-to-do">What to do</h2>
<p>If you’re going to use these at all: report the uncertainty, and actually look at how wide it is. Shrink the estimates (empirical Bayes pulls noisy small sample numbers toward the average, which is exactly right when most of the spread is noise). Never hang a high stakes decision on a single year. And say out loud what you’re assuming about how kids got sorted to teachers, because the entire causal story rests on it.</p>
<p>These scores can inform. Used as a one number verdict on someone’s career, they mostly measure which children walked through the door.</p>
<hr>
<p><em>This is where methods and policy collide, which is the part of this work I care about most. I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a> and a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>education policy</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/value-added-scores/</guid>
  <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Effect sizes for education: what WWC actually wants</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/02-effect-sizes-wwc/</link>
  <description><![CDATA[ 




<p>“We found a significant effect.” I have a mild allergic reaction to that sentence and I’ve accepted it’s permanent.</p>
<p>It’s not that it’s wrong. It’s that it tells you almost nothing. Significant says <em>we detected something</em>. It says nothing about <em>how big</em>, which is the only thing a school board or a funder or the What Works Clearinghouse actually wants to know. That’s what an effect size is for, and in education that means a standardized mean difference, with a couple of small decisions baked in that quietly move the number.</p>
<section id="the-thing-itself" class="level2">
<h2 class="anchored" data-anchor-id="the-thing-itself">The thing itself</h2>
<p>Strip it down: take the gap between the two group means, divide by a standard deviation. Now the effect is in standard deviation units, so a reading program and a math program can sit on the same ruler. Two studies with the same raw point gain can land at very different standardized effects, because the spread of scores differs. Which is the point. It’s also where the choices start to matter.</p>
</section>
<section id="cohens-d-vs.-hedges-g" class="level2">
<h2 class="anchored" data-anchor-id="cohens-d-vs.-hedges-g">Cohen’s d vs.&nbsp;Hedges’ g</h2>
<p>Cohen’s d puts the pooled within group SD in that denominator. Fine, except it has a known flaw: in small samples it’s biased <em>upward</em>. It overstates. And education studies are small constantly, a few classrooms, a handful of schools, so this is not a hypothetical.</p>
<p>Hedges’ g is just Cohen’s d times a small sample correction. When the sample is big, the correction is basically 1 and the two agree. When it’s small, the correction pulls the estimate back toward the truth. WWC reports g for exactly this reason, and once you see the size of the bias you stop arguing about it.</p>
<p>Here’s the bias, simulated. True standardized effect is 0.5. I draw a pile of small studies and average each estimator:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th style="text-align: right;">Per group n</th>
<th style="text-align: right;">Cohen’s d</th>
<th style="text-align: right;">Hedges’ g</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: right;">3</td>
<td style="text-align: right;"><strong>0.618</strong></td>
<td style="text-align: right;">0.495</td>
</tr>
<tr class="even">
<td style="text-align: right;">5</td>
<td style="text-align: right;">0.572</td>
<td style="text-align: right;">0.517</td>
</tr>
<tr class="odd">
<td style="text-align: right;">10</td>
<td style="text-align: right;">0.519</td>
<td style="text-align: right;">0.497</td>
</tr>
<tr class="even">
<td style="text-align: right;">100</td>
<td style="text-align: right;">0.501</td>
<td style="text-align: right;">0.499</td>
</tr>
</tbody>
</table>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/02-effect-sizes-wwc/fig-bias.png" class="img-fluid figure-img"></p>
<figcaption>Cohen’s d sits above the true 0.5 at small samples; Hedges’ g tracks it.</figcaption>
</figure>
</div>
<p>Three per group and uncorrected d averages 0.618, a 24% overstatement of an effect that is really 0.5. Hedges’ g sits at 0.495. By a hundred per group they shake hands. Small education studies live on the left edge of that plot, which is precisely where skipping the correction costs you.</p>
</section>
<section id="the-little-decisions-people-get-wrong" class="level2">
<h2 class="anchored" data-anchor-id="the-little-decisions-people-get-wrong">The little decisions people get wrong</h2>
<p>Which SD goes in the denominator. Pooled (both groups) is the usual choice and what the WWC convention assumes. control group only is sometimes defensible, but if you pick it without saying so, your effect size isn’t comparable to anyone else’s. State your denominator.</p>
<p>Treating a small sample d as if it were unbiased. If n is small and you skipped the correction, your number is inflated. Use g.</p>
<p>And comparing effect sizes across studies that standardized differently. A 0.30 on a narrow, homogeneous sample is not the same animal as a 0.30 on a broad one. Effect sizes are only comparable when the rulers are.</p>
</section>
<section id="reporting-it-like-you-mean-it" class="level2">
<h2 class="anchored" data-anchor-id="reporting-it-like-you-mean-it">Reporting it like you mean it</h2>
<p>Give the effect size with an interval, not a lonely point estimate; “g = 0.8” with no uncertainty begs to be over trusted. Tie the number to a decision, because “significant” and “meaningful” are different words. And say your choices out loud so the number can be checked.</p>
<p>A standardized mean difference is a tiny calculation carrying a big claim. Get the denominator and the correction right, attach honest uncertainty, translate it into something a human can act on. Skip those and “significant” becomes a word that hides more than it tells.</p>
<hr>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package for WWC-aligned baseline equivalence and effect sizes, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>impact evaluation</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/02-effect-sizes-wwc/</guid>
  <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Your baseline table is doing more work than your p value</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/01-baseline-table/</link>
  <description><![CDATA[ 




<p>For a long time the baseline table was the part of a report I did last, fast, and a little resentfully. Throat clearing before the real analysis, was how I thought about it. Get it out of the way, get to the effect.</p>
<p>I had it exactly backwards about which part was the real analysis.</p>
<p>By the time you’re estimating a program effect, you’ve already placed the bet that matters: that the two groups were comparable to begin with. If they weren’t, you’re not measuring the program. You’re measuring the head start. No model downstream fixes that, and reviewers, funders, and the What Works Clearinghouse all know exactly where to look.</p>
<section id="the-question-its-actually-answering" class="level2">
<h2 class="anchored" data-anchor-id="the-question-its-actually-answering">The question it’s actually answering</h2>
<p>Plain version: before the program started, were these groups similar enough that I can pin later differences on the program instead of on who was in each group?</p>
<p>Notice what that is <em>not</em>. It is not “is the baseline difference statistically significant?” That’s the reflex, run a t test on the pretest, see p &gt; .05, declare the groups fine, move on. But significance is the wrong tool here, for a reason that has nothing to do with the program. With a big sample, a trivial difference turns “significant.” With a small one, a real gap sails through as “not significant.” A p value is mostly telling you about your sample size. Equivalence is a question about <em>size of the gap</em>. Different question.</p>
</section>
<section id="what-the-wwc-asks-for-instead" class="level2">
<h2 class="anchored" data-anchor-id="what-the-wwc-asks-for-instead">What the WWC asks for instead</h2>
<p>For each covariate you report a standardized mean difference (Hedges’ g, which is Cohen’s d with a small sample correction), and you read it against two numbers:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 33%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th>Hedges’ g (absolute value)</th>
<th>Category</th>
<th>What it commits you to</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>≤ 0.05</code></td>
<td>Satisfied</td>
<td>Nothing. The groups are equivalent here.</td>
</tr>
<tr class="even">
<td><code>0.05</code>–<code>0.25</code></td>
<td>Satisfied <em>with adjustment</em></td>
<td>Fine <strong>only if</strong> you adjust for this covariate in the impact model.</td>
</tr>
<tr class="odd">
<td><code>&gt; 0.25</code></td>
<td>Not satisfied</td>
<td>This one can’t establish equivalence, and adjustment won’t save it.</td>
</tr>
</tbody>
</table>
<p>The whole framework is those two numbers, 0.05 and 0.25, applied honestly. The arithmetic isn’t the hard part. Remembering what each category obligates you to do later is.</p>
</section>
<section id="doing-it-without-a-spreadsheet" class="level2">
<h2 class="anchored" data-anchor-id="doing-it-without-a-spreadsheet">Doing it without a spreadsheet</h2>
<p>Here it is on a simulated tutoring evaluation, 400 students, data I bundled into <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a> just for this. One call, whole table:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(baselinr)</span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">data</span>(tutoring)</span>
<span id="cb1-3"></span>
<span id="cb1-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">baseline_equivalence</span>(</span>
<span id="cb1-5">  tutoring, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">treatment =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"treat"</span>,</span>
<span id="cb1-6">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">covariates =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pretest"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"attendance"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"age"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"female"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"frpl"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ell"</span>)</span>
<span id="cb1-7">)</span></code></pre></div></div>
<table class="caption-top table">
<colgroup>
<col style="width: 14%">
<col style="width: 14%">
<col style="width: 19%">
<col style="width: 19%">
<col style="width: 19%">
<col style="width: 14%">
</colgroup>
<thead>
<tr class="header">
<th>Covariate</th>
<th>Type</th>
<th style="text-align: right;">Mean (T)</th>
<th style="text-align: right;">Mean (C)</th>
<th style="text-align: right;">Effect size</th>
<th>WWC category</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>pretest</td>
<td>continuous</td>
<td style="text-align: right;">52.15</td>
<td style="text-align: right;">50.10</td>
<td style="text-align: right;">0.196</td>
<td>satisfied_with_adjustment</td>
</tr>
<tr class="even">
<td>attendance</td>
<td>continuous</td>
<td style="text-align: right;">0.927</td>
<td style="text-align: right;">0.918</td>
<td style="text-align: right;">0.186</td>
<td>satisfied_with_adjustment</td>
</tr>
<tr class="odd">
<td>age</td>
<td>continuous</td>
<td style="text-align: right;">10.04</td>
<td style="text-align: right;">10.03</td>
<td style="text-align: right;">0.020</td>
<td>satisfied</td>
</tr>
<tr class="even">
<td>female</td>
<td>binary</td>
<td style="text-align: right;">0.530</td>
<td style="text-align: right;">0.525</td>
<td style="text-align: right;">0.012</td>
<td>satisfied</td>
</tr>
<tr class="odd">
<td>frpl</td>
<td>binary</td>
<td style="text-align: right;">0.555</td>
<td style="text-align: right;">0.445</td>
<td style="text-align: right;">0.268</td>
<td><strong>not_satisfied</strong></td>
</tr>
<tr class="even">
<td>ell</td>
<td>binary</td>
<td style="text-align: right;">0.250</td>
<td style="text-align: right;">0.150</td>
<td style="text-align: right;">0.385</td>
<td><strong>not_satisfied</strong></td>
</tr>
</tbody>
</table>
<p>Continuous covariates get Hedges’ g; the binary ones get the Cox index, handled for you. And honestly the plot is where it clicks:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">love_plot</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">baseline_equivalence</span>(tutoring, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"treat"</span>,</span>
<span id="cb2-2">          <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">covariates =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pretest"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"attendance"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"age"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"female"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"frpl"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ell"</span>)))</span></code></pre></div></div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/01-baseline-table/fig-loveplot.png" class="img-fluid figure-img"></p>
<figcaption>Love plot of the six covariates against the WWC thresholds.</figcaption>
</figure>
</div>
<p>Age and sex are equivalent, nothing to do. Pretest and attendance sit in the middle band, so you can proceed but you owe the model an adjustment. And then poverty status and language status blow past 0.25, which means the groups differ on two things adjustment can’t fix. That last pair is something you’d want to know <em>before</em> you ever report an effect, not after a reviewer finds it.</p>
</section>
<section id="the-part-that-quietly-leaks-credibility" class="level2">
<h2 class="anchored" data-anchor-id="the-part-that-quietly-leaks-credibility">The part that quietly leaks credibility</h2>
<p>“Satisfied with adjustment” is not a passing grade. It’s a promise. It says: these groups are close enough, <em>as long as</em> I actually control for this covariate in the impact model. And it is genuinely common to see a baseline table that files a covariate under “satisfied with adjustment” and then an impact model that never adjusts for it. The table says one thing, the model does another, and a careful reader catches it instantly. If a covariate lands in the middle band, it has to show up in the model. Every time.</p>
<p>Treat the baseline table like the load bearing wall it is, not the trim you tack on at the end. Get it right and everything downstream has something solid to stand on. Get it wrong and your fanciest model is just measuring the head start.</p>
<hr>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a>, a small R package that produces these tables, and am developing a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>impact evaluation</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/01-baseline-table/</guid>
  <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Before you compare two groups’ survey scores, check this</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/measurement-invariance/</link>
  <description><![CDATA[ 




<p>Here’s a sentence that should make you flinch: “Group A scored higher on the belonging scale, so Group A feels more like they belong.”</p>
<p>I have a soft spot for this trap, mostly because I’ve walked into it myself. It sounds airtight. Five items, two groups, one mean is higher, write it up. But there’s a premise buried in there that nobody says out loud, and the whole conclusion is sitting on top of it.</p>
<p>The premise is that the scale <em>means the same thing</em> to both groups.</p>
<section id="the-lens-thing" class="level2">
<h2 class="anchored" data-anchor-id="the-lens-thing">The lens thing</h2>
<p>I think of a scale as a lens you look at a construct through. Comparing two groups’ scores assumes they’re looking through the <em>same</em> lens. If the lenses are ground differently, then comparing what each group “sees” tells you about the lenses, not the thing you were trying to look at. The technical name for “same lens” is measurement invariance, and it is not a given. It’s a thing you check.</p>
</section>
<section id="how-much-youre-allowed-to-say" class="level2">
<h2 class="anchored" data-anchor-id="how-much-youre-allowed-to-say">How much you’re allowed to say</h2>
<p>Invariance comes in levels, and each one buys you a different comparison. Roughly: if the same items load on the same factor in both groups (configural), you’ve got the basic shape. If the loadings are equal too (metric), you can start comparing <em>relationships</em> across groups. And only if the intercepts are also equal (scalar) are you actually allowed to compare means.</p>
<p>That last one is the level your group comparison quietly assumed it had. You test for it with a multigroup CFA. And when a single item behaves differently across groups, that’s DIF, differential item functioning, which is a fancy way of saying the lens has a chip in it right there.</p>
</section>
<section id="watch-it-happen" class="level2">
<h2 class="anchored" data-anchor-id="watch-it-happen">Watch it happen</h2>
<p>Two groups, a 5-item engagement scale, and here’s the thing: I built them to have <em>identical</em> true engagement. Same distribution, no real difference. Then I made one item (item 5) read systematically lower for group B at the same level of engagement. Textbook DIF. Then I just… compared the groups, the way anyone would, with a standardized mean difference:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">library</span>(baselinr)</span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">hedges_g</span>(theta, group)        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the true latent difference</span></span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">hedges_g</span>(scale_full, group)   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># observed, all 5 items</span></span>
<span id="cb1-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">hedges_g</span>(scale_inv, group)    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># observed, dropping the one DIF item</span></span></code></pre></div></div>
<table class="caption-top table">
<thead>
<tr class="header">
<th>What’s compared</th>
<th style="text-align: right;">Hedges’ g</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>True latent engagement</td>
<td style="text-align: right;"><strong>−0.03</strong> (there is no real difference)</td>
</tr>
<tr class="even">
<td>Observed 5-item scale</td>
<td style="text-align: right;"><strong>−0.25</strong></td>
</tr>
<tr class="odd">
<td>Scale without the DIF item</td>
<td style="text-align: right;"><strong>−0.02</strong></td>
</tr>
</tbody>
</table>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/measurement-invariance/fig-dif.png" class="img-fluid figure-img"></p>
<figcaption>Per item group difference: items 1–4 are flat, item 5 carries the whole gap.</figcaption>
</figure>
</div>
<p>The groups are equal. I promise, I made them that way. And the full scale still reports g = −0.25, which most people would write up as “Group B is less engaged.” The figure shows the crime scene: items 1 through 4 are flat, and item 5 is doing <em>all</em> of the work, off by −1.22 on its own. Pull that one item and the difference evaporates to −0.02.</p>
<p>Nothing about engagement happened here. The ruler happened.</p>
</section>
<section id="what-to-do" class="level2">
<h2 class="anchored" data-anchor-id="what-to-do">What to do</h2>
<p>Test invariance before you compare means, every time you’re comparing a latent thing across groups. If it fails, don’t throw the whole comparison out, go find the items that broke (the DIF ones). And then report partial invariance honestly, anchored on the items that held. It’s less clean than a single triumphant p value. It’s also the difference between a finding about students and a finding about your questionnaire.</p>
<hr>
<p><em>Measurement is the thing I get genuinely nerdy about. I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a> and I’m building a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> if you want in.</em></p>


</section>

 ]]></description>
  <category>measurement</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/measurement-invariance/</guid>
  <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Students aren’t independent: the clustering mistake that makes everything look significant</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/clustering-independence/</link>
  <description><![CDATA[ 




<p>Sixty schools, three thousand students, and a p value with more zeros after the decimal than you have ever earned honestly. It feels amazing. I’ve had that little rush, the one where you think <em>oh, this is going in the abstract.</em></p>
<p>I’m sorry to tell you it’s frequently a lie, and the lie is baked into the very first thing you assumed: that you have three thousand independent pieces of information.</p>
<p>You don’t.</p>
<section id="why-n-3000-is-fiction" class="level2">
<h2 class="anchored" data-anchor-id="why-n-3000-is-fiction">Why “n = 3,000” is fiction</h2>
<p>Kids in the same school resemble each other. Same teachers, same building, same neighborhood, same everything. So each extra student from a school you’ve already sampled tells you less than a brand new, unrelated student would. Regression assumes your rows are independent. When they’re clustered and you shrug and run it anyway, your standard errors come out too small, your test statistics too big, and out the other end pours a parade of “significant” effects that are mostly just the model fooling itself.</p>
<p>How much smaller depends on the intraclass correlation, the ICC, which is the share of the variation that lives <em>between</em> schools rather than within them. A little clustering barely dents you. A lot of it quietly guts your real sample size down toward the number of <em>schools</em> rather than the number of students.</p>
</section>
<section id="the-machinery-quickly" class="level2">
<h2 class="anchored" data-anchor-id="the-machinery-quickly">The machinery, quickly</h2>
<p>ICC measures how much students within a cluster move together. The design effect turns that into how badly your naive standard errors lie. Cluster robust standard errors and multilevel models are the two standard fixes (robust SEs patch the uncertainty; multilevel models patch it <em>and</em> let you study how schools differ). And if you’ve only got a handful of clusters, even robust SEs get shaky, and you reach for a wild cluster bootstrap.</p>
</section>
<section id="watch-it-happen" class="level2">
<h2 class="anchored" data-anchor-id="watch-it-happen">Watch it happen</h2>
<p>This is my favorite part, because it’s so stark. Sixty schools, fifty kids each, a school level treatment, and a true effect of exactly zero. The within school correlation gives an ICC around 0.26, a design effect of roughly 14, so those 3,000 students carry about as much information as 210.</p>
<p>One representative null study:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1">m <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(y <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> treat, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">data =</span> study)        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># student level, ignores clustering</span></span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sqrt</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">diag</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">vcov</span>(m)))[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"treat"</span>]                          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># naive SE</span></span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sqrt</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">diag</span>(sandwich<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">vcovCL</span>(m, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">cluster =</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span>school)))[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"treat"</span>]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># cluster robust SE</span></span></code></pre></div></div>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Standard error</th>
<th style="text-align: right;">SE</th>
<th style="text-align: right;">p value</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Naive (student level)</td>
<td style="text-align: right;">0.043</td>
<td style="text-align: right;"><strong>&lt; 0.0001</strong></td>
</tr>
<tr class="even">
<td>Cluster robust (school)</td>
<td style="text-align: right;">0.157</td>
<td style="text-align: right;"><strong>0.167</strong></td>
</tr>
</tbody>
</table>
<p>Same estimate, same data. The naive version is screaming significance. The honest version shrugs, correctly, because there is nothing there.</p>
<p>And it’s not one unlucky draw. Across 600 simulated null studies:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/clustering-independence/fig-falsepos.png" class="img-fluid figure-img"></p>
<figcaption>Naive analysis calls 57% of null studies significant; cluster robust ~6%.</figcaption>
</figure>
</div>
<p>The naive analysis declares a significant effect in <strong>57%</strong> of studies where the truth is <em>nothing</em>. 57 percent. The cluster robust version sits at 6%, right about the 5% you signed up for. If you ignore clustering, “p &lt; .05” doesn’t mean what you think it means. It barely means anything.</p>
</section>
<section id="what-to-do" class="level2">
<h2 class="anchored" data-anchor-id="what-to-do">What to do</h2>
<p>Cluster at the level treatment was actually assigned (rolled out by school? then school, not student). Report the ICC so your reader knows how much this even matters here. Watch out when you’ve only got a few clusters. And if you genuinely care about how schools differ, not just correcting for it, go multilevel.</p>
<p>Before you celebrate a tiny p value, just ask where the rows came from. In education they’re almost always nested, which means “n” is not the number of students, and significance computed as if it were is significance you didn’t earn.</p>
<hr>
<p><em>I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a> and I’m building a cohort course on running education evaluations you can actually trust. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> to follow along.</em></p>


</section>

 ]]></description>
  <category>impact evaluation</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/clustering-independence/</guid>
  <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Your text classifier is 85% accurate. That error doesn’t vanish when you run the regression.</title>
  <dc:creator>Yuxia Liang</dc:creator>
  <link>https://zl1212-ship-it.github.io/education-methods/posts/text-classifier-error/</link>
  <description><![CDATA[ 




<p>There’s a very specific high that hits when a classifier you’ve been wrestling with for days finally clears 85% on the held out set. It works. You run it over all fifty thousand comments, attach the predicted labels to your dataset, and move on to the analysis you actually cared about.</p>
<p>That’s the exact moment I want to grab your arm.</p>
<p>Because the 15% it got wrong doesn’t politely stay inside the classifier. It rides along into the regression, and it changes your answer. “Accuracy” is hiding how much.</p>
<section id="a-predicted-label-is-a-guess-youre-treating-as-a-fact" class="level2">
<h2 class="anchored" data-anchor-id="a-predicted-label-is-a-guess-youre-treating-as-a-fact">A predicted label is a guess you’re treating as a fact</h2>
<p>That’s the whole problem in one sentence. You didn’t <em>observe</em> “this feedback is specific.” You <em>estimated</em> it, with a model that’s right most of the time and wrong some of the time. The minute you drop those predictions into a regression as if they were the real thing, you’ve imported the model’s mistakes without telling the regression they’re there.</p>
<p>And the phrase “ground truth” makes it so much worse, because it’s not ground truth. It’s the classifier’s training target. In your actual analysis those labels are a noisy stand in, and calling them truth is how the error sneaks past you.</p>
</section>
<section id="what-the-error-does-depends-on-what-kind-it-is" class="level2">
<h2 class="anchored" data-anchor-id="what-the-error-does-depends-on-what-kind-it-is">What the error does depends on what kind it is</h2>
<p>If the mistakes are just random (the classifier is equally clumsy everywhere), and the noisy variable is a predictor, the effect gets <strong>attenuated</strong>. Pulled toward zero. This is the part people get wrong in conversation, by the way. “It’s just noise, it averages out.” A regression coefficient on a mismeasured predictor does not average out. It shrinks.</p>
<p>Differential error is the one that actually scares me. If the classifier is worse for some cases than others, in a way that’s tied to your outcome or your groups, the bias picks up a <em>direction</em>. You’re not just underestimating anymore. You can end up pointing the wrong way.</p>
<p>Your setup is the textbook case: you predict “feedback specificity” from teacher comments, then relate it to student progress. Now suppose the model reads feedback from teachers of multilingual learners a little worse. That error is now braided into exactly the comparison you care about, and it doesn’t wash out. It bends the conclusion.</p>
</section>
<section id="watch-it-happen" class="level2">
<h2 class="anchored" data-anchor-id="watch-it-happen">Watch it happen</h2>
<p>Six thousand simulated comments. The true effect of the text feature on the outcome is 1.0. I feed two classifiers’ <em>predicted</em> labels into the same regression and treat them as truth:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># True effect of Z on Y is 1.0 (simulated).</span></span>
<span id="cb1-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">coef</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(Y <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> Z))[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Z"</span>]         <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># if we had the true labels</span></span>
<span id="cb1-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">coef</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(Y <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> Zhat_nd))        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 85% accurate, error unrelated to anything</span></span>
<span id="cb1-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">coef</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">lm</span>(Y <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span> Zhat_d))         <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 85% accurate overall, but worse for one group</span></span></code></pre></div></div>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Labels used</th>
<th style="text-align: right;">Estimated effect</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>True labels</td>
<td style="text-align: right;"><strong>1.00</strong></td>
</tr>
<tr class="even">
<td>Predicted — nondifferential error</td>
<td style="text-align: right;"><strong>0.68</strong></td>
</tr>
<tr class="odd">
<td>Predicted — differential error</td>
<td style="text-align: right;"><strong>0.72</strong></td>
</tr>
</tbody>
</table>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://zl1212-ship-it.github.io/education-methods/posts/text-classifier-error/fig-attenuation.png" class="img-fluid figure-img"></p>
<figcaption>Both classifiers are 85% accurate, yet the estimated effect is attenuated and the bias differs.</figcaption>
</figure>
</div>
<p>Sit with two things here. First: clean, random, 85%-accurate error drags a true effect of 1.0 down to 0.68. A third of your signal, gone, purely from treating guesses as data. Second, and this is the one that gets me: both classifiers report the <em>same</em> 85%. But the “differential” one is 93% accurate for one group and 78% for the other. The headline number cannot tell you which world you’re in. Only breaking accuracy out by subgroup can.</p>
</section>
<section id="what-to-actually-do" class="level2">
<h2 class="anchored" data-anchor-id="what-to-actually-do">What to actually do</h2>
<p>Keep a human labeled gold set, and not just for tuning. You need it to <em>measure</em> the error so you can correct for it later. Report classifier performance broken out by the groups you’ll compare, because “85% overall” is precisely where differential error hides. And then either propagate that uncertainty into the analysis or use an estimator built for predicted labels (there’s a small but real literature on inference with machine predicted variables now). Whatever you do, stop quietly swapping predictions in for observations and calling it done.</p>
<p>Measure your measurement. Report it by group. Let the error into the model the same way it’s already, invisibly, in your data.</p>
<hr>
<p><em>This exact corner, where text models meet measurement error, is what I work on. I build <a href="https://CRAN.R-project.org/package=baselinr"><code>baselinr</code></a> and I’m putting together a cohort course on credible evaluation and measurement in education. <a href="https://zl1212-ship-it.github.io/education-methods/index.xml">subscribe via RSS</a> if you want to follow along.</em></p>


</section>

 ]]></description>
  <category>measurement</category>
  <guid>https://zl1212-ship-it.github.io/education-methods/posts/text-classifier-error/</guid>
  <pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
