Repository navigation
Add support for repeated measurements in the Evaluation module (multiple nights per person) - #279
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #279 +/- ##
==========================================
+ Coverage 98.61% 98.72% +0.10%
==========================================
Files 13 13
Lines 3167 3361 +194
Branches 431 456 +25
==========================================
+ Hits 3123 3318 +195
Misses 23 23
+ Partials 21 20 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…luation Both EpochByEpochAgreement and SleepStatsAgreement take a new `subjects` argument. get_sleep_stats() carries the subject IDs over as an index level, so SleepStatsAgreement(sstats) picks them up automatically. EpochByEpochAgreement: summary() and get_confusion_matrix_proportional() average within subject first, then across subjects, and the bootstrap resamples subjects. The proportional matrix gains an `n_subjects` column. SleepStatsAgreement: the variance of the differences is decomposed into between- and within-subject components with the one-way ANOVA of Bland & Altman (2007), exposed in a new `variance_components` property. The LoA use the total SD. The parametric CIs use the model-based SE of the bias and the MOVER method of Zou (2013) for the LoA, the LoA half-width and the Euser slope. The regressions behind the proportional-bias and heteroscedasticity tests are random-intercept mixed models (statsmodels), with Wald CIs using a t(n_subjects - 2) quantile for all `ci_method` values, since a cluster bootstrap over few subjects under-covers the slope. The bootstrap of the bias, LoA, half-width and Euser slope is a cluster bootstrap over subjects. plot_blandaltman(hue="subject_id") colors the nights of each subject alike. statsmodels is a new optional dependency (`yasa[stats]`, in `full`), pinned to 0.14.5 in the minimum-versions CI job. The tutorial gets a new section on several nights per subject. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Behavior-preserving cleanup of the repeated-measures support in SleepStatsAgreement, after a reuse/simplification/efficiency/altitude review. All outputs (summary, assumptions, variance components, Euser CIs and seeded bootstrap) are unchanged. - Compute the bias SE, critical t, bias CI and t-test once for both the independent and repeated cases (only the SE differs). - Return the degrees of freedom of the coefficient CIs from `_regress` instead of re-deriving them from the "method" label. - Replace `_linregr_dict` / `_regr_cols` (positional subject column) with explicit `_regr_frame` / `_vc_frame` helpers built on `_per_stat`. - Vectorize the log-transform (Euser) loop over all log-transformed stats. - Add `_total_sd`, unify the half-width computation in the bootstrap statistic, copy the Wald CIs with a single concat, drop the unused `n_pairs` key, and let `_subjects_to_series` handle the subject index level. - Tests: share the sleep_id -> subject mapping as a module constant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Fall back to SD / sqrt(n) for the bias SE of a sleep stat with no repeated nights after NaN removal, instead of returning NaN CIs and t-test p-value (which flagged the stat as biased). - Use a t-test on n_subjects - 2 degrees of freedom for the mixed-model slope p-value instead of the z-test of statsmodels, consistent with the Wald CIs. The z-test was anti-conservative with few subjects. - Warn when the mixed model is at the boundary (no between-subject variance) and falls back to OLS, and use OLS with fewer than 3 subjects. - Document when sd_between / sd_within are NaN in variance_components. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rewrite the "Several nights per subject" section for readability and add the cited repeated-measures papers to the references - Reproduce the repeated-measures outputs in docs/tutorials/evaluation.py - Remove redundant or non-functional repeated-measures tests and merge overlapping ones (coverage of evaluation.py unchanged) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reword the docstrings of the `subjects` arguments, the SleepStatsAgreement notes on several nights per subject, `variance_components`, and the related `summary`, `report` and `plot_blandaltman` parameters for readability. No change in behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each statistical idea (subject-then-group averaging, cluster bootstrap, ANOVA variance components, MOVER intervals, Wald CIs for the mixed-model coefficients) is now explained once in its natural home and referenced elsewhere. Also simplify the tutorial wording, merge the duplicated by_stage branches in EpochByEpochAgreement.summary and use the same warning in both classes when each subject has a single session. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Merge the two attribute tests, drop the redundant ndarray input case and the simulation sanity check, simplify the cluster-bootstrap assertions and keep only the hue/legend part of the plot test. Coverage of evaluation.py is unchanged (99%, all remaining partial branches predate the PR). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
817c990 to
796ea1d
Compare
Comparison with
|
| Dataset | Component | YASA | statsmodels REML | matrixCorr |
|---|---|---|---|---|
| Balanced, 10 subjects × 4 nights | sd_between | 12.58 | 12.58 | 11.91 |
| sd_within | 4.45 | 4.45 | 4.45 | |
| Unbalanced, 8 subjects, 23 nights (the test fixture of this PR) | sd_between | 10.13 | 10.28 | 0.00 |
| sd_within | 8.48 | 8.50 | 12.84 | |
| bias 95% CI | 3.6 to 23.1 | 3.7 to 23.0 | 8.1 to 18.6 |
On the unbalanced dataset, matrixCorr's EM collapsed the subject variance to zero although the data were simulated with a strong subject offset (sd_between = 20). Its bias CI is then about half as wide as the REML one, because the 23 nights are treated as independent. YASA matches REML. Only two datasets were run, so this is a warning sign rather than a characterization of matrixCorr.
Differences in what is estimated
- Bias. matrixCorr reports the GLS intercept, which down-weights subjects with many nights when the subject variance is large. YASA reports the plain mean of all differences, as in Bland & Altman (2007). The two coincide for balanced designs or when the subject variance is zero.
- Confidence intervals. matrixCorr uses Wald intervals with a normal quantile and a delta-method propagation through a numerical Hessian of the profiled REML likelihood. YASA uses a t quantile with n_subjects - 1 degrees of freedom for the bias and the MOVER method of Zou (2013) for the LoA, and also offers a cluster bootstrap. With 8 to 20 subjects, the normal quantile is noticeably anticonservative.
- Proportional bias. matrixCorr can add a slope on the paired mean (classic Bland-Altman convention) and keeps horizontal LoA. YASA regresses on the reference value (Menghini et al. convention, since PSG is the gold standard), fits it as a mixed model, and derives regression-based bias and LoA lines plus a heteroscedasticity test.
Features only one side has
- matrixCorr: optional AR(1) correlation between consecutive nights of a subject, and pairwise matrices for three or more methods. YASA treats the nights of a subject as exchangeable and compares exactly two scorers.
- YASA: assumption tests adapted to the clustering, log-transform and Euser slope, calibration, Bland-Altman plots with a subject hue, bootstrap CIs, the between- and within-subject SD as a user-facing output (
variance_components), and the epoch-by-epoch half.
Takeaway
Nothing here suggests changing the implementation. The closed-form ANOVA is exact for balanced data, within 1 to 2% of REML for unbalanced data, and was more robust than matrixCorr's EM in the one unbalanced case tested. One design choice worth keeping in mind: the bias is the unweighted mean of all differences rather than the GLS estimate.
🤖 Generated with Claude Code
…fferences Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The model-based SE of the bias is the variance of the mean of all differences under the one-way random-effects model, not a formula from Zou (2013), who uses the mean of the subject means. Bland & Altman (2007) do not discuss clipping a negative between-subject variance. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Validation against the full text of the referencesI read the five methods papers cited by this PR (Bland & Altman 2007, Zou 2013, Parker et al. 2016, Myles & Cui 2007, Euser et al. 2008) and checked every formula the implementation relies on, then fed the worked examples of Bland & Altman and Zou into the code. Worked examples are reproduced
The last row differs only by the rounding of the printed inputs. One caveat on the Bland & Altman data: in the author-produced PDF (White Rose repository), the subject labels of the second column of Table 1 are shifted by one row, so subjects 5 to 8 have 6, 4, 4 and 6 pairs rather than the printed 5, 4, 4 and 7. With the labels as printed, Σm_i² is 314 instead of the paper's 312 and the ANOVA table cannot be reproduced. With the shift corrected, every number matches. Method-by-method
Corrections made (1200303)Two code comments cited papers for statements the papers do not make:
The corresponding cell of the summary table in the PR description was updated as well. No formula change was needed. 🤖 Generated with Claude Code |
Summary
This PR adds native support for repeated measures in the Evaluation module. Prior to this PR, the Evaluation module treated all nights as fully independent. When several nights come from the same person, they are not independent, and treating them as separate overstates the sample size: the bias and LoA are barely affected, but their confidence intervals and the p-values of the assumption tests become too small.
This PR adds a
subjectsargument toEpochByEpochAgreementandSleepStatsAgreement. With one night per subject, the results are unchanged (a warning says thatsubjectshas no effect).API
subjectscan be a list, array, dict or Series, aligned with the hypnograms or sleep sessions.EpochByEpochAgreement.get_sleep_stats()adds the subject IDs as an index level, soSleepStatsAgreement(ebe.get_sleep_stats())picks them up automatically.n_subjectsattribute, also shown in the repr of both classes.SleepStatsAgreement.variance_componentsproperty with the between-, within-subject and total SD of the differences for each sleep stat.plot_blandaltman(hue="subject_id")colors the nights of each subject alike.What changes when
subjectsis passedEpochByEpochAgreementget_agreement(),get_agreement_bystage(),get_confusion_matrix(),plot_hypnograms()summary()descriptive statisticssupportis the mean number of epochs per night of each subjectsummary(ci_method="boot")get_confusion_matrix_proportional()n_subjectscolumn. Parametric CI uses t(n_subjects - 1)get_sleep_stats()(scorer, sleep_id)(scorer, subject_id, sleep_id)SleepStatsAgreementlog_transform=True)MixedLM, REML). Slope p-value and Wald CIs with t(n_subjects - 2). Residuals from the fixed effects only. Falls back to OLS with a warning if the fit has no between-subject varianceci_method="boot". In simulations, a bootstrap over 8-20 subjects covered the true slope only 85-90% of the time at the 95% levelvariance_componentssd_betweenandsd_withinare NaN,sd_totalis the sample SDn_subjects,n_sessionscalibrate(),plot_blandaltman()hue="subject_id"optionAveraging the nights of each subject before the analysis is not a valid alternative: the bias would be unchanged but the LoA would be too narrow, because averaging removes part of the night-to-night error.
Dependencies
statsmodelsis a new optional dependency (pip install yasa[stats], included infull). It is required only whenSleepStatsAgreementgets repeated measures.docs/index.rst.Docs and tests
SleepStatsAgreementdocstring.subjects, invalid inputs, the equivalence of thesubjectsargument and thesubject_idindex level, the cluster bootstrap, and the single-night, no-repeated-nights and boundary fallbacks.References
AI assistance
Model used: Claude Fable 5.1 via Claude Code
Checklist
master, and I checked open PRs for conflictsIf AI-assisted:
/simplifywas run (preferred, Claude Code)🤖 Generated with Claude Code