<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Isaac Land</title>
    <description>The latest articles on DEV Community by Isaac Land (@landisaac1).</description>
    <link>https://dev.to/landisaac1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195271%2Fa19dce13-7ab3-47f9-aeca-1992c37c4838.png</url>
      <title>DEV Community: Isaac Land</title>
      <link>https://dev.to/landisaac1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/landisaac1"/>
    <language>en</language>
    <item>
      <title>Regression discontinuity designs break in six common ways</title>
      <dc:creator>Isaac Land</dc:creator>
      <pubDate>Wed, 21 Oct 2026 11:33:56 +0000</pubDate>
      <link>https://dev.to/landisaac1/regression-discontinuity-designs-break-in-six-common-ways-3kjd</link>
      <guid>https://dev.to/landisaac1/regression-discontinuity-designs-break-in-six-common-ways-3kjd</guid>
      <description>&lt;p&gt;Regression discontinuity (RD) is one of the most credible designs for causal inference from observational data. When a rule assigns treatment based on whether a score crosses a cutoff (a scholarship for test scores above 80, a programme for towns under a population threshold, a policy on one side of a border), people just above and just below the cutoff should be comparable. Any jump in outcomes at the cutoff can then be attributed to the treatment.&lt;/p&gt;

&lt;p&gt;But a jump in a plot is not the same thing as a credible effect. This post goes through the mistakes that most often undermine RD analyses and the tools that help avoid them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick refresher
&lt;/h2&gt;

&lt;p&gt;The variable that determines treatment is the &lt;strong&gt;running variable&lt;/strong&gt; (also called the forcing variable or score). RD comes in two forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sharp RD&lt;/strong&gt;: treatment switches on completely at the cutoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzy RD&lt;/strong&gt;: crossing the cutoff only changes the &lt;em&gt;probability&lt;/em&gt; of treatment. The effect is estimated as the jump in the outcome divided by the jump in treatment take-up, which is essentially an instrumental-variables estimate at the cutoff. It needs a non-zero first stage, an exclusion restriction, and monotonicity, and it identifies a local effect for compliers near the cutoff.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key identifying assumption is not that the observed outcome is smooth. It is that the &lt;em&gt;potential outcomes&lt;/em&gt;, meaning what would happen with and without treatment, change smoothly through the cutoff. Lee and Lemieux (2010) give an accessible treatment, and the design itself goes back to Thistlethwaite and Campbell (1960).&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1: fitting high-order global polynomials
&lt;/h2&gt;

&lt;p&gt;A common older practice was to fit a 4th-, 5th-, or 6th-degree polynomial to the whole range of the running variable on each side and read off the gap at the cutoff. Gelman and Imbens (2019), in the &lt;em&gt;Journal of Business &amp;amp; Economic Statistics&lt;/em&gt;, argue strongly against this, for three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global polynomials put surprisingly large and erratic weight on observations far from the cutoff.&lt;/li&gt;
&lt;li&gt;Estimates are sensitive to the polynomial order.&lt;/li&gt;
&lt;li&gt;Confidence intervals tend to be too narrow, so real coverage falls short of the nominal level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Better:&lt;/strong&gt; use local linear (or local quadratic) regression within a bandwidth around the cutoff, with a triangular kernel that gives more weight to points near the threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: choosing the bandwidth by eye (or by result)
&lt;/h2&gt;

&lt;p&gt;How much data on either side of the cutoff should you use? A narrow window reduces bias but increases variance. A wide window does the opposite. Trying several bandwidths and reporting the one with the cleanest result is a garden of forking paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better:&lt;/strong&gt; use a data-driven, MSE-optimal bandwidth selector and report it up front. Then show sensitivity to, say, half and double that bandwidth as a robustness check, not as a menu to choose from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 3: conventional confidence intervals with an optimal bandwidth
&lt;/h2&gt;

&lt;p&gt;Here is a subtle one. MSE-optimal bandwidths are, by construction, wide enough that some bias remains. Conventional confidence intervals ignore that bias, so they under-cover. Calonico, Cattaneo and Titiunik (2014), in &lt;em&gt;Econometrica&lt;/em&gt;, proposed &lt;strong&gt;robust bias-corrected&lt;/strong&gt; confidence intervals that account for this. They are now standard, and implemented in the &lt;code&gt;rdrobust&lt;/code&gt; package for R, Stata, and Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rdrobust&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rdrobust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# local linear, MSE-optimal bandwidth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;# report the robust bias-corrected CI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;rdplot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;# binned scatter with local fits&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Report the point estimate together with the robust confidence interval, not the conventional one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 4: not checking for manipulation of the running variable
&lt;/h2&gt;

&lt;p&gt;If people can precisely control their score (retaking a test until they pass, or misreporting income to stay under a threshold), those just above the cutoff are no longer comparable to those just below. A tell-tale sign is bunching: a spike in the density of the running variable on one side.&lt;/p&gt;

&lt;p&gt;McCrary (2008) introduced the standard density test. A more recent local-polynomial version is available in the &lt;code&gt;rddensity&lt;/code&gt; package. A histogram of the running variable with fine bins is a cheap first look that every RD paper should show.&lt;/p&gt;

&lt;p&gt;Note the asymmetry. A clean density test does not prove the absence of sorting, but clear bunching is a serious warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 5: skipping covariate balance checks
&lt;/h2&gt;

&lt;p&gt;If the design works, pre-treatment characteristics should not jump at the cutoff. Run the same RD estimator with baseline covariates (age, prior outcomes, demographics) as the outcome. A significant jump in a variable the treatment could not have affected suggests sorting or a second policy that shares the same cutoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 6: over-extrapolating the result
&lt;/h2&gt;

&lt;p&gt;An RD estimate is a &lt;strong&gt;local&lt;/strong&gt; effect at the cutoff. A scholarship's effect on students scoring exactly 80 says little about students scoring 50. Reporting it as "the effect of scholarships" stretches the design beyond what it identifies. Be explicit about the population the estimate applies to.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cautionary case: the Huai River heating study
&lt;/h2&gt;

&lt;p&gt;China historically provided free winter heating north of the Huai River and not south of it, which creates a geographic discontinuity. Chen, Ebenstein, Greenstone and Li (2013), in &lt;em&gt;PNAS&lt;/em&gt;, used this border to estimate the effect of air pollution on life expectancy. The paper drew wide attention, and also methodological criticism, notably from Andrew Gelman, centring on the use of a global cubic polynomial in distance from the river and how sensitive the estimate was to that choice. A follow-up study, Ebenstein, Fan, Greenstone, He and Zhou (2017), also in &lt;em&gt;PNAS&lt;/em&gt;, revisited the question with more data and different specifications.&lt;/p&gt;

&lt;p&gt;Whatever your view of the substantive result, the episode is a useful teaching case. It shows why specification choices in RD (polynomial order, bandwidth, what counts as "near" the cutoff) need to be justified and stress-tested rather than picked once.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical RD checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the assignment rule exists independently of the outcome data. Don't go hunting for a kink first and call it a policy cutoff afterwards.&lt;/li&gt;
&lt;li&gt;Plot the raw data with &lt;code&gt;rdplot&lt;/code&gt; or a binned scatter.&lt;/li&gt;
&lt;li&gt;Check the density of the running variable for bunching.&lt;/li&gt;
&lt;li&gt;Check covariate balance at the cutoff.&lt;/li&gt;
&lt;li&gt;Estimate with local linear regression, an MSE-optimal bandwidth, and robust bias-corrected CIs.&lt;/li&gt;
&lt;li&gt;Show sensitivity to bandwidth and kernel; avoid high-order global polynomials.&lt;/li&gt;
&lt;li&gt;For fuzzy RD, report the first stage.&lt;/li&gt;
&lt;li&gt;Run placebo cutoffs where no effect should exist.&lt;/li&gt;
&lt;li&gt;State clearly that the estimate is local.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wrote a longer version of this discussion, with more on the Huai River debate, in &lt;a href="https://metho.scholay.com/news/draft-5b12c767-147a-4c17-a896-ae42dc0cca1d/" rel="noopener noreferrer"&gt;this article on common regression discontinuity mistakes&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Calonico, S., Cattaneo, M. D., &amp;amp; Titiunik, R. (2014). Robust nonparametric confidence intervals for regression-discontinuity designs. &lt;em&gt;Econometrica&lt;/em&gt;, 82(6), 2295–2326.&lt;/li&gt;
&lt;li&gt;Chen, Y., Ebenstein, A., Greenstone, M., &amp;amp; Li, H. (2013). Evidence on the impact of sustained exposure to air pollution on life expectancy from China's Huai River policy. &lt;em&gt;PNAS&lt;/em&gt;, 110(32), 12936–12941.&lt;/li&gt;
&lt;li&gt;Ebenstein, A., Fan, M., Greenstone, M., He, G., &amp;amp; Zhou, M. (2017). New evidence on the impact of sustained exposure to air pollution on life expectancy from China's Huai River Policy. &lt;em&gt;PNAS&lt;/em&gt;, 114(39), 10384–10389.&lt;/li&gt;
&lt;li&gt;Gelman, A., &amp;amp; Imbens, G. (2019). Why high-order polynomials should not be used in regression discontinuity designs. &lt;em&gt;Journal of Business &amp;amp; Economic Statistics&lt;/em&gt;, 37(3), 447–456.&lt;/li&gt;
&lt;li&gt;Lee, D. S., &amp;amp; Lemieux, T. (2010). Regression discontinuity designs in economics. &lt;em&gt;Journal of Economic Literature&lt;/em&gt;, 48(2), 281–355.&lt;/li&gt;
&lt;li&gt;McCrary, J. (2008). Manipulation of the running variable in the regression discontinuity design: A density test. &lt;em&gt;Journal of Econometrics&lt;/em&gt;, 142(2), 698–714.&lt;/li&gt;
&lt;li&gt;Thistlethwaite, D. L., &amp;amp; Campbell, D. T. (1960). Regression-discontinuity analysis: An alternative to the ex post facto test. &lt;em&gt;Journal of Educational Psychology&lt;/em&gt;, 51(6), 309–317.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datascience</category>
      <category>tutorial</category>
      <category>learning</category>
    </item>
    <item>
      <title>EEG classifiers can score well by recognising the person</title>
      <dc:creator>Isaac Land</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:00:22 +0000</pubDate>
      <link>https://dev.to/landisaac1/eeg-classifiers-can-score-well-by-recognising-the-person-2clm</link>
      <guid>https://dev.to/landisaac1/eeg-classifiers-can-score-well-by-recognising-the-person-2clm</guid>
      <description>&lt;p&gt;Many EEG machine-learning papers report accuracies that look too good. One common reason has nothing to do with the brain signal you care about: the model has learned to recognise &lt;em&gt;who&lt;/em&gt; a recording came from. If the same person's data sit in both the training and the test set, the test score answers an easier question than the one the paper claims to answer. This post walks through how that happens, what a recent small replication found, and how to set up the split so your number means what you think it means.&lt;/p&gt;

&lt;h2&gt;
  
  
  How windowing creates the problem
&lt;/h2&gt;

&lt;p&gt;A typical EEG pipeline cuts each recording into many short windows (say, 4 seconds each), extracts features per window, and treats each window as a sample. A study with 36 participants can easily end up with a couple of thousand windows. That looks like plenty of data.&lt;/p&gt;

&lt;p&gt;The trap is the next step. If you shuffle all the windows and do a random 80/20 split, windows from the same person land on both sides. Consecutive windows from one recording are highly similar, and so are different recordings from the same head. A flexible classifier can then learn person-specific boundaries ("for this participant, the alpha power shifts like &lt;em&gt;this&lt;/em&gt; between rest and task") that do not carry over to anyone new.&lt;/p&gt;

&lt;p&gt;The randomisation unit decides what you are estimating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Window-level split&lt;/strong&gt;: how well the model classifies new windows from people it has probably already seen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subject-level split&lt;/strong&gt;: how well the model transfers to people it has never seen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is wrong in itself. The problem is reporting the first and claiming the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a minimal replication showed
&lt;/h2&gt;

&lt;p&gt;In October 2026, Renish Joshi published a deliberately simple replication in &lt;em&gt;Towards Data Science&lt;/em&gt;. He used the public EEGMAT dataset on PhysioNet (EEG during mental arithmetic): 36 healthy students, each with a resting recording and a serial-subtraction recording, sampled at 500 Hz with 10/20 electrode placement (Zyma et al., 2019).&lt;/p&gt;

&lt;p&gt;The pipeline was kept fixed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;19 scalp channels, 1–40 Hz band-pass filter, non-overlapping 4-second windows (2,134 windows in total).&lt;/li&gt;
&lt;li&gt;Welch power spectra, relative power in theta, alpha, beta and low-gamma bands, giving 76 features per window.&lt;/li&gt;
&lt;li&gt;Three classifiers: class-weighted logistic regression, an RBF-kernel SVM, and a 300-tree class-weighted random forest. Scaling sat inside a scikit-learn pipeline, so it was fit on training data only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the evaluation changed. The window-level protocol used 10 stratified random 80/20 splits. The subject-level protocol used 6-fold &lt;code&gt;GroupKFold&lt;/code&gt; with participant ID as the group, so no test participant contributed a single training window.&lt;/p&gt;

&lt;p&gt;Balanced accuracy dropped for every model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Classifier&lt;/th&gt;
&lt;th&gt;Random windows&lt;/th&gt;
&lt;th&gt;Held-out subjects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logistic regression&lt;/td&gt;
&lt;td&gt;0.770&lt;/td&gt;
&lt;td&gt;0.648&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RBF SVM&lt;/td&gt;
&lt;td&gt;0.864&lt;/td&gt;
&lt;td&gt;0.602&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random forest&lt;/td&gt;
&lt;td&gt;0.664&lt;/td&gt;
&lt;td&gt;0.532&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;ROC-AUC moved the same way. Because a linear model, a kernel method and a tree ensemble all showed the pattern, it is hard to blame one model family.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do the features know who you are?
&lt;/h2&gt;

&lt;p&gt;A score drop alone doesn't explain the mechanism, so Joshi ran a second test: use the same 76 spectral features to predict &lt;em&gt;which of the 36 participants&lt;/em&gt; a window came from. Chance is about 2.8%. The identity classifier reached 95.6%. On a class-balanced subset (15 rest and 15 task windows per person, 1,080 windows), it still reached 94.0%, with 99.5% top-3 accuracy.&lt;/p&gt;

&lt;p&gt;He is careful about what this shows. It does not prove the task classifier used identity as its decision rule. It shows the features carry a lot of person-specific information, which is enough to make overlapping splits optimistic. The window-versus-subject gap also persisted on the balanced subset, so class imbalance alone doesn't explain it.&lt;/p&gt;

&lt;p&gt;The averages also hide a wide spread. Per-participant balanced accuracy for the subject-level logistic regression ranged from 0.289 to 0.956, and 5 of 36 participants scored below chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  This isn't a one-off
&lt;/h2&gt;

&lt;p&gt;The replication follows Brookshire et al. (2024) in &lt;em&gt;Frontiers in Neuroscience&lt;/em&gt;, who reran two published convolutional networks. For Alzheimer's disease versus subjective cognitive decline, test accuracy was 99.8% with segment-level splits and 53.0% with subject-level splits, where the confidence interval included chance. For seizure detection on the Siena Scalp EEG database, accuracy fell from 79.1% to 65.1%. In a non-exhaustive review of 63 deep-learning EEG papers on disease identification, they found only 17 (27.0%) that clearly avoided this leakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the split correctly
&lt;/h2&gt;

&lt;p&gt;The rule is simple to state: &lt;strong&gt;match the split to the generalisation claim.&lt;/strong&gt; If you claim the model works on new people, no data from a test person may reach training, and that includes anything learned from data along the way.&lt;/p&gt;

&lt;p&gt;In scikit-learn, that means grouped cross-validation, with every learned step inside the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GroupKFold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cross_val_score&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.pipeline&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_pipeline&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.preprocessing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StandardScaler&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.linear_model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LogisticRegression&lt;/span&gt;

&lt;span class="c1"&gt;# X: (n_windows, n_features), y: labels, groups: participant ID per window
&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nc"&gt;LogisticRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;class_weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;balanced&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_iter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GroupKFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;scoring&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;balanced_accuracy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to check beyond the outer split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tuning&lt;/strong&gt;: if you tune hyperparameters, do it with an inner grouped split inside the training participants only (nested CV), not on the outer test folds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature selection, PCA, ICA, normalisation&lt;/strong&gt;: anything fit on data belongs inside the pipeline so it only sees training participants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report per-subject results&lt;/strong&gt;: show the spread across held-out people, not just the mean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the identity check&lt;/strong&gt;: if your features can name the participant, window-level results are likely to be inflated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Window-level evaluation is still fine when the question really is "new sessions from people already enrolled", for example a personalised model that only ever serves the users it was trained on. Just say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth keeping in mind
&lt;/h2&gt;

&lt;p&gt;Joshi's study is one 36-person dataset with hand-crafted spectral features. The two protocols also differ in resampling structure (10 random splits versus 6 grouped folds), so the gap is descriptive, not a formal causal estimate of leakage. Where the person-specific signal comes from (physiology, electrode placement, session effects or residual artefacts) isn't resolved. And as of mid-October 2026, the article page did not link a public code repository, so the numbers can't yet be rerun independently. Brookshire et al. did publish code in their supplementary material, and EEGMAT is freely downloadable.&lt;/p&gt;

&lt;p&gt;I first wrote up this case in Chinese, with the figures from Brookshire et al., on &lt;a href="https://metho.scholay.com/news/draft-d767e6ec-62a9-4728-b0b9-2f97559a2b4e/" rel="noopener noreferrer"&gt;Scholay's research-methods section&lt;/a&gt;. The same logic applies to any data with repeated samples per person: patients, users, sensors, or schools.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Brookshire, G., Kasper, J., Blauch, N. M., Wu, Y. C., Glatt, R., Merrill, D. A., Gerrol, S., Yoder, K. J., Quirk, C., &amp;amp; Lucero, C. (2024). Data leakage in deep learning studies of translational EEG. &lt;em&gt;Frontiers in Neuroscience&lt;/em&gt;, 18, 1373515. &lt;a href="https://doi.org/10.3389/fnins.2024.1373515" rel="noopener noreferrer"&gt;https://doi.org/10.3389/fnins.2024.1373515&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Joshi, R. (2026, October 17). When EEG models see the same people in training and testing. &lt;em&gt;Towards Data Science&lt;/em&gt;. &lt;a href="https://towardsdatascience.com/when-eeg-models-see-the-same-people-in-training-and-testing/" rel="noopener noreferrer"&gt;https://towardsdatascience.com/when-eeg-models-see-the-same-people-in-training-and-testing/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zyma, I., Seleznov, I., Popov, A., Chernykh, M., &amp;amp; Shpenkov, O. (2018). EEG during mental arithmetic tasks (version 1.0.0). &lt;em&gt;PhysioNet&lt;/em&gt;. &lt;a href="https://doi.org/10.13026/C2JQ1P" rel="noopener noreferrer"&gt;https://doi.org/10.13026/C2JQ1P&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zyma, I., Tukaev, S., Seleznov, I., Kiyono, K., Popov, A., Chernykh, M., &amp;amp; Shpenkov, O. (2019). Electroencephalograms during mental arithmetic task performance. &lt;em&gt;Data&lt;/em&gt;, 4(1), 14. &lt;a href="https://doi.org/10.3390/data4010014" rel="noopener noreferrer"&gt;https://doi.org/10.3390/data4010014&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;scikit-learn developers. Cross-validation: evaluating estimator performance (section on cross-validation iterators for grouped data). &lt;a href="https://scikit-learn.org/stable/modules/cross_validation.html" rel="noopener noreferrer"&gt;https://scikit-learn.org/stable/modules/cross_validation.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cross-validation fails when data leaks between folds</title>
      <dc:creator>Isaac Land</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:44:31 +0000</pubDate>
      <link>https://dev.to/landisaac1/cross-validation-fails-when-data-leaks-between-folds-6f9</link>
      <guid>https://dev.to/landisaac1/cross-validation-fails-when-data-leaks-between-folds-6f9</guid>
      <description>&lt;p&gt;A model that scores well on the data it was trained on has shown that it can memorise. Cross-validation is the standard way to estimate how it will do on data it has never seen. It only works if one rule holds, though: no information from the validation data may reach the model-building process by any route. Break that rule, which is easy to do without noticing, and your estimate turns optimistic. This post covers k-fold and leave-one-out cross-validation and the leakage patterns that most often break them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just score on the training set?
&lt;/h2&gt;

&lt;p&gt;The scikit-learn user guide gives the extreme case: a model that simply repeats the labels of samples it has already seen gets a perfect training score and is useless on new data. That is overfitting in its purest form.&lt;/p&gt;

&lt;p&gt;The obvious fix is a held-out test set. With small datasets, though, holding data out shrinks the training set further, and the score depends heavily on which rows happened to land in the test split. Cross-validation reuses the data more efficiently and gives a more stable estimate.&lt;/p&gt;

&lt;p&gt;The idea is old. Stone (1974), in the &lt;em&gt;Journal of the Royal Statistical Society, Series B&lt;/em&gt;, used cross-validation for both choosing a prediction method and assessing it. &lt;em&gt;The Elements of Statistical Learning&lt;/em&gt; (Hastie, Tibshirani &amp;amp; Friedman, chapter 7) keeps those two jobs separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model selection&lt;/strong&gt;: using CV to pick a model or tune hyperparameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model assessment&lt;/strong&gt;: using CV to report the final model's error on new data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mixing the two in a single CV loop is one of the leakage sources covered below.&lt;/p&gt;

&lt;h2&gt;
  
  
  k-fold and leave-one-out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;k-fold CV&lt;/strong&gt; randomly splits the data into &lt;em&gt;k&lt;/em&gt; roughly equal folds. In each round, one fold is held out for validation and the model is trained on the other &lt;em&gt;k − 1&lt;/em&gt;. Average the &lt;em&gt;k&lt;/em&gt; validation scores and you have the CV estimate. Every observation is used for validation exactly once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leave-one-out CV (LOOCV)&lt;/strong&gt; is the special case &lt;em&gt;k = n&lt;/em&gt;: each round holds out a single observation.&lt;/p&gt;

&lt;p&gt;Choosing &lt;em&gt;k&lt;/em&gt; is a bias–variance trade-off. ESL notes that LOOCV is approximately unbiased for expected prediction error, but its &lt;em&gt;n&lt;/em&gt; training sets are almost identical, so the estimate can have high variance. It can also be expensive. Five- or ten-fold CV is the common default and usually a sensible compromise.&lt;/p&gt;

&lt;p&gt;In scikit-learn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KFold&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.linear_model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LogisticRegression&lt;/span&gt;

&lt;span class="n"&gt;cv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LogisticRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_iter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Leakage pattern 1: preprocessing before splitting
&lt;/h2&gt;

&lt;p&gt;This is the classic mistake. You standardise features, impute missing values, or run feature selection on the &lt;em&gt;whole&lt;/em&gt; dataset, and only then run cross-validation. Every validation fold has now influenced the means, the imputation values, or the selected features.&lt;/p&gt;

&lt;p&gt;Feature selection is the worst offender. ESL section 7.10.2, "The Wrong and Right Way to Do Cross-validation", describes screening thousands of predictors for those most correlated with the label and then cross-validating a classifier on the survivors. Even when labels are pure noise, this procedure can report error rates far below chance. Ambroise and McLachlan (2002) documented the same selection bias in gene-expression studies.&lt;/p&gt;

&lt;p&gt;The fix is to put every data-dependent step inside the CV loop. In scikit-learn, that is what &lt;code&gt;Pipeline&lt;/code&gt; is for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.pipeline&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_pipeline&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.preprocessing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StandardScaler&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.feature_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SelectKBest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_classif&lt;/span&gt;

&lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;StandardScaler&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nc"&gt;SelectKBest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f_classif&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;LogisticRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_iter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# scaler and selector refit per fold
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Leakage pattern 2: tuning and reporting on the same folds
&lt;/h2&gt;

&lt;p&gt;If you try 50 hyperparameter settings and report the best CV score, that score is biased upwards: you picked the winner partly because it got lucky on those folds. Varma and Simon (2006) showed how large this bias can be in small-sample settings.&lt;/p&gt;

&lt;p&gt;The remedy is &lt;strong&gt;nested cross-validation&lt;/strong&gt;. An inner loop tunes hyperparameters and an outer loop estimates the performance of the whole tuning procedure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GridSearchCV&lt;/span&gt;

&lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GridSearchCV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logisticregression__C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;nested_scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Leakage pattern 3: grouped data split row by row
&lt;/h2&gt;

&lt;p&gt;When several rows come from the same unit (multiple scans per patient, many EEG windows per subject, several photos of the same object), a random row-level split puts the same unit in both training and validation. The model can learn to recognise the &lt;em&gt;individual&lt;/em&gt; rather than the pattern you care about. The score then reflects identity matching, not generalisation to new people.&lt;/p&gt;

&lt;p&gt;Split by group instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GroupKFold&lt;/span&gt;

&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cross_val_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subject_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;GroupKFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_splits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful sanity check: if the group-wise score falls sharply compared with the row-wise score, your earlier number was mostly measuring leakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leakage pattern 4: time
&lt;/h2&gt;

&lt;p&gt;For time series, a random split lets the model train on the future and predict the past. Features built with rolling windows or "future-aware" aggregates make this worse. Use forward-chaining splits such as &lt;code&gt;TimeSeriesSplit&lt;/code&gt;, which only ever validate on data that comes after the training window, and add a gap between them if features look back over several periods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leakage pattern 5: features that encode the target
&lt;/h2&gt;

&lt;p&gt;Kaufman, Rosset, Perlich and Stitelman (2012) describe leakage as including information that would not be legitimately available at prediction time. Typical examples are an ID field that correlates with outcome because of how records were created, a "date of treatment" column when predicting diagnosis, or a field that is filled in only after the event you are predicting. No splitting scheme protects you from this. You have to ask, for each feature, "would I actually have this value at the moment of prediction?"&lt;/p&gt;

&lt;h2&gt;
  
  
  It is not just a Kaggle problem
&lt;/h2&gt;

&lt;p&gt;Kapoor and Narayanan (2023), in &lt;em&gt;Patterns&lt;/em&gt;, surveyed published research that used machine learning across many scientific fields. They found leakage to be a widespread cause of overstated results, and proposed "model info sheets" so authors could document how they had ruled it out. If you are using ML in a research setting, it is worth reading their taxonomy before submitting anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every step that learns from data (scaling, imputation, encoding, feature selection, resampling such as SMOTE) sits inside the CV loop.&lt;/li&gt;
&lt;li&gt;Hyperparameter tuning happens in an inner loop, and the reported score comes from an outer loop or a truly untouched test set.&lt;/li&gt;
&lt;li&gt;If rows share a subject, site, or source, split by group.&lt;/li&gt;
&lt;li&gt;If time matters, validate only on the future.&lt;/li&gt;
&lt;li&gt;Each feature is available at prediction time.&lt;/li&gt;
&lt;li&gt;The test set is used once, at the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a broader look at how validation choices affect what a study can claim, I keep notes on research-method topics like this one on &lt;a href="https://scholay.com" rel="noopener noreferrer"&gt;Scholay&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ambroise, C., &amp;amp; McLachlan, G. J. (2002). Selection bias in gene extraction on the basis of microarray gene-expression data. &lt;em&gt;PNAS&lt;/em&gt;, 99(10), 6562–6566.&lt;/li&gt;
&lt;li&gt;Hastie, T., Tibshirani, R., &amp;amp; Friedman, J. (2009). &lt;em&gt;The Elements of Statistical Learning&lt;/em&gt; (2nd ed.), chapter 7. Springer.&lt;/li&gt;
&lt;li&gt;Kapoor, S., &amp;amp; Narayanan, A. (2023). Leakage and the reproducibility crisis in machine-learning-based science. &lt;em&gt;Patterns&lt;/em&gt;, 4(9).&lt;/li&gt;
&lt;li&gt;Kaufman, S., Rosset, S., Perlich, C., &amp;amp; Stitelman, O. (2012). Leakage in data mining: Formulation, detection, and avoidance. &lt;em&gt;ACM Transactions on Knowledge Discovery from Data&lt;/em&gt;, 6(4).&lt;/li&gt;
&lt;li&gt;Stone, M. (1974). Cross-validatory choice and assessment of statistical predictions. &lt;em&gt;Journal of the Royal Statistical Society: Series B&lt;/em&gt;, 36(2), 111–147.&lt;/li&gt;
&lt;li&gt;Varma, S., &amp;amp; Simon, R. (2006). Bias in error estimation when using cross-validation for model selection. &lt;em&gt;BMC Bioinformatics&lt;/em&gt;, 7, 91.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Nested data needs a multilevel model</title>
      <dc:creator>Isaac Land</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:43:16 +0000</pubDate>
      <link>https://dev.to/landisaac1/nested-data-needs-a-multilevel-model-1c01</link>
      <guid>https://dev.to/landisaac1/nested-data-needs-a-multilevel-model-1c01</guid>
      <description>&lt;p&gt;When your data come in groups (students inside classrooms, patients inside hospitals, repeated measurements inside the same person), ordinary regression quietly assumes something false: that every row is independent. This post walks through why that matters, how a multilevel (mixed-effects) model fixes it, and how to fit one in R with &lt;code&gt;lme4&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: rows that know each other
&lt;/h2&gt;

&lt;p&gt;Imagine 3,000 students spread across 100 classrooms. Students in the same classroom share a teacher, a timetable, and often a neighbourhood. Their test scores will be more alike than scores drawn at random from the whole population.&lt;/p&gt;

&lt;p&gt;Ordinary least squares (OLS) does not know that. It treats 3,000 rows as 3,000 independent pieces of information. In reality, part of the information is duplicated within each classroom, so OLS standard errors come out too small, confidence intervals too narrow, and p-values too optimistic. Classroom-level predictors such as "teacher has a master's degree" are hit hardest, because there are really only 100 independent units for that variable, not 3,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the similarity: the intraclass correlation
&lt;/h2&gt;

&lt;p&gt;The standard way to quantify "how alike are members of the same group" is the &lt;strong&gt;intraclass correlation (ICC)&lt;/strong&gt;. Start with an empty (intercept-only) multilevel model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;y_ij = γ00 + u_j + e_ij
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;u_j&lt;/code&gt; is classroom &lt;em&gt;j&lt;/em&gt;'s deviation from the grand mean, with variance τ²&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;e_ij&lt;/code&gt; is student &lt;em&gt;i&lt;/em&gt;'s deviation from their classroom mean, with variance σ²&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ICC = τ² / (τ² + σ²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is the share of total variance that lies &lt;em&gt;between&lt;/em&gt; groups, and it also equals the expected correlation between two observations from the same group.&lt;/p&gt;

&lt;h2&gt;
  
  
  From ICC to the design effect
&lt;/h2&gt;

&lt;p&gt;Survey statisticians have long used the &lt;strong&gt;design effect&lt;/strong&gt; to translate the ICC into lost precision. Following Kish (1965), for groups of average size &lt;em&gt;m&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deff ≈ 1 + (m − 1) × ICC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A purely hypothetical illustration: 100 classrooms of 30 students and an ICC of 0.2 give &lt;code&gt;deff = 1 + 29 × 0.2 = 6.8&lt;/code&gt;. The effective sample size is about 3000 / 6.8 ≈ 441, and naive standard errors would be roughly 1/√6.8 ≈ 0.38 of what they should be. Even a modest ICC of 0.05 gives a design effect of 2.45 with classes of 30. Small ICCs matter when groups are large.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to handle groups
&lt;/h2&gt;

&lt;p&gt;Gelman and Hill (2007) frame the choice as three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Complete pooling.&lt;/strong&gt; Ignore groups and fit one regression. Simple, but it erases between-group variation (which is often the very thing you care about) and gives overconfident standard errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pooling.&lt;/strong&gt; Fit each group separately, or add a dummy variable per group. Small groups get noisy estimates, and group dummies are perfectly collinear with any group-level predictor, so you cannot estimate the effect of, say, school type at the same time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial pooling (multilevel model).&lt;/strong&gt; Each group gets its own parameters, but those parameters are drawn from a common distribution. Groups with little data are "shrunk" towards the overall mean; groups with lots of data mostly keep their own estimates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Partial pooling is the core idea. It is a data-driven compromise between "all groups are the same" and "all groups are unrelated".&lt;/p&gt;

&lt;h2&gt;
  
  
  Random intercepts and random slopes
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;random-intercept&lt;/strong&gt; model lets each group have its own baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;y_ij = γ00 + γ10 x_ij + u0j + e_ij
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;random-slope&lt;/strong&gt; model also lets the effect of a predictor vary by group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;y_ij = γ00 + (γ10 + u1j) x_ij + u0j + e_ij
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, the link between hours of homework and test scores might be steeper in some classrooms than in others. The variance of &lt;code&gt;u1j&lt;/code&gt; tells you how much that slope varies, and the covariance between &lt;code&gt;u0j&lt;/code&gt; and &lt;code&gt;u1j&lt;/code&gt; tells you whether high-baseline groups also have steeper slopes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fitting it in R with lme4
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;lme4&lt;/code&gt; package (Bates, Mächler, Bolker &amp;amp; Walker, 2015) is the workhorse for this in R. It ships with the &lt;code&gt;sleepstudy&lt;/code&gt; dataset: reaction times for 18 participants measured over 10 days of sleep restriction, so measurements are nested in people.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;library&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lme4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;# Complete pooling: one line for everyone&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;m_ols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Reaction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sleepstudy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;# Random intercept per participant&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;m_ri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lmer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Reaction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sleepstudy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;# Random intercept and random slope for Days&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;m_rs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lmer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Reaction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Days&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sleepstudy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things worth comparing yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The standard error of &lt;code&gt;Days&lt;/code&gt; in &lt;code&gt;m_ols&lt;/code&gt; versus &lt;code&gt;m_rs&lt;/code&gt;. The OLS version treats 180 rows as independent.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Subject&lt;/code&gt; variance components in &lt;code&gt;summary(m_ri)&lt;/code&gt;. Plug them into the ICC formula above.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;coef(m_rs)$Subject&lt;/code&gt; against per-subject &lt;code&gt;lm()&lt;/code&gt; fits. You will see shrinkage: participants with extreme individual slopes are pulled towards the average.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;lmer()&lt;/code&gt; uses REML (restricted maximum likelihood) by default, which gives less biased variance estimates than plain ML. If you compare models that differ in their &lt;em&gt;fixed&lt;/em&gt; effects with a likelihood-ratio test, refit with &lt;code&gt;REML = FALSE&lt;/code&gt; first. &lt;code&gt;anova()&lt;/code&gt; in lme4 does this automatically for &lt;code&gt;merMod&lt;/code&gt; objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Too few groups.&lt;/strong&gt; Variance components are estimated from the number of groups, not the number of rows. Maas and Hox (2005) found that with very few groups the standard errors of the group-level variances were noticeably biased. Five schools with 1,000 students each is a weak basis for a random effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring the design and just clustering standard errors.&lt;/strong&gt; Cluster-robust standard errors can fix inference for fixed effects, which is a reasonable choice in some settings. But they do not give you estimates of between-group variation, group-specific predictions, or random slopes. Pick the tool that answers your question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding random slopes for everything.&lt;/strong&gt; Models with many random slopes and correlations often fail to converge or produce singular fits (&lt;code&gt;isSingular()&lt;/code&gt; returns &lt;code&gt;TRUE&lt;/code&gt;). Start from a theoretically motivated structure and simplify when the data cannot support it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting centring.&lt;/strong&gt; With a random slope on &lt;code&gt;x&lt;/code&gt;, the intercept variance depends on where &lt;code&gt;x = 0&lt;/code&gt;. Centring predictors, either at the grand mean or within each group, changes what the intercept means. Within-group centring also separates within-group and between-group effects, which are often different things entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating the ICC as a test of whether you "need" a multilevel model.&lt;/strong&gt; A small ICC can still produce a large design effect when groups are big, and the grouping is part of the design whatever the ICC turns out to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for a multilevel model
&lt;/h2&gt;

&lt;p&gt;Use one when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;observations are grouped, and the grouping comes from how the data were collected (sampling, randomisation, or repeated measurement);&lt;/li&gt;
&lt;li&gt;you care about variation between groups, or about predictors measured at the group level;&lt;/li&gt;
&lt;li&gt;some groups are small and you want stable estimates for them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a longer, education-focused walk-through of the same ideas, including a worked example with classroom data, the &lt;a href="https://metho.scholay.com/news/draft-59e3ef4b-bca5-4afb-a202-8fc22a646d2d/" rel="noopener noreferrer"&gt;original article on nested data and multilevel models&lt;/a&gt; goes into more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bates, D., Mächler, M., Bolker, B., &amp;amp; Walker, S. (2015). Fitting linear mixed-effects models using lme4. &lt;em&gt;Journal of Statistical Software&lt;/em&gt;, 67(1), 1–48.&lt;/li&gt;
&lt;li&gt;Gelman, A., &amp;amp; Hill, J. (2007). &lt;em&gt;Data Analysis Using Regression and Multilevel/Hierarchical Models&lt;/em&gt;. Cambridge University Press.&lt;/li&gt;
&lt;li&gt;Kish, L. (1965). &lt;em&gt;Survey Sampling&lt;/em&gt;. Wiley.&lt;/li&gt;
&lt;li&gt;Maas, C. J. M., &amp;amp; Hox, J. J. (2005). Sufficient sample sizes for multilevel modeling. &lt;em&gt;Methodology&lt;/em&gt;, 1(3), 86–92.&lt;/li&gt;
&lt;li&gt;Raudenbush, S. W., &amp;amp; Bryk, A. S. (2002). &lt;em&gt;Hierarchical Linear Models: Applications and Data Analysis Methods&lt;/em&gt; (2nd ed.). Sage.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datascience</category>
      <category>tutorial</category>
      <category>learning</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
