Computes the Sleep Regularity Index (Phillips et al. 2017): a measure of day-to-day consistency in the sleep/wake pattern, based on the probability that an epoch's sleep/wake state matches the state at the same clock time exactly 24 h later (or earlier), averaged across the whole recording. Ranges from -100 (perfectly inverted day-to-day) to +100 (perfectly regular); 0 corresponds to chance-level agreement.
Arguments
- x
A
zeitr_recording/zeitr_result, or a data frame / tibble with at leastdatetimeandstatecolumns (algo = "vallim") ordatetimeandactivitycolumns (algo = "sadeh","ck","scripps", or"roenneberg"). Forstate:state == 1orstate == 7is treated as sleep,state == 4as off-wrist (missing), and any other value as wake – matching the coding already used across zeitR's pipeline output (run_pipeline(),run_pipeline_native(),export_hypnogram()).- epoch_s
numeric(1). Epoch duration in seconds. IfNULL(default), estimated automatically from the median inter-epoch interval.- max_gap_min
numeric(1). Off-wrist gaps of this many minutes or less are interpolated rather than excluded. Default30, matching Fix 30 and Fix 14's 30-minute threshold elsewhere in the pipeline. Only used whenalgo = "vallim".- algo
character(1). Which sleep/wake source and SRI aggregation to use:"vallim"(default, uses the pipeline's ownstatecolumn),"sadeh","ck","scripps", or"roenneberg"(all four score rawactivity). See Details for the precise differences beyond just the scoring algorithm.
Value
A tibble with columns participant_id, sri, n_pairs (number
of valid 24h-apart epoch comparisons used; NA for algo != "vallim",
whose aggregation isn't a single pooled pair count), and
n_epochs. sri is NA if the recording is shorter than 24 h, or
(for algo = "vallim") if no valid pairs remain after off-wrist
exclusion.
Details
algo = "vallim" (default) ports Fix 30 of the Python reference
pipeline (SRI_vallim): sleep/wake comes directly from the epoch-level
state column already produced by zeitR's own pipelines
(run_pipeline() / run_pipeline_native()) – the same classification
compute_sleep_metrics() and compute_cpd_metrics() already treat as
ground truth for this recording. Off-wrist handling mirrors Fix 30
exactly: off-wrist epochs (state == 4) are treated as missing. Gaps of
max_gap_min minutes or less are interpolated (forward-filled from the
last valid epoch, or back-filled from the next valid epoch when the gap
starts at the very beginning of the recording); longer gaps are left as
missing and excluded from the day-to-day comparison entirely, rather than
being counted as a non-match. The aggregation is a single flat average
over every valid 24h-apart epoch pair, pooled across the whole recording:
$$SRI = -100 + 200 \times \frac{1}{M}\sum_{t} \Psi(t, t + 24h)$$
where \(\Psi(t, t+24h) = 1\) if the sleep/wake state at epoch \(t\)
matches the state 24 h later, \(0\) otherwise, and \(M\) is the number
of epoch pairs where both epochs have a valid (non-missing) state. This
matches Julia's own compute_sri_vallim() replica (also a flat pooled
ratio, not a per-time-of-day average – see below).
algo = "sadeh" instead derives sleep/wake from the raw activity
signal via the Sadeh et al. (1994) algorithm, matching pyActigraphy's
actual Sadeh()/SleepRegularityIndex() (Cell 16 of
vs_condor_py_pipeline_fix30_jrsv.ipynb) exactly – both sourced from
pyActigraphy's real code (pyActigraphy/sleep/scoring_base.py,
_sadeh(); pyActigraphy/sleep/scoring/sri.py, sri()), not
reconstructed from documentation. Two precise details that differ from
the "vallim" path above:
Aggregation is a two-step average, not a flat pooled one. pyActigraphy's
sri()first groups epochs by time-of-day (hour, minute, second) across all days, averages day-to-day stability within each time-of-day slot, and only then averages across slots. This is mathematically different from a flat pooled average whenever slots have unequal numbers of valid day-pairs (e.g. a partial first/last day) – both"vallim"and"sadeh"are ported faithfully to their own respective real source, not reconciled to use the same aggregation.No off-wrist handling. Sadeh scores whatever raw activity it is given; there is no off-wrist masking/interpolation step in pyActigraphy's own code for this path.
max_gap_minhas no effect whenalgo = "sadeh".
Sadeh's own edge behaviour is also reproduced exactly: mean_W5/NAT
need a full centered 11-epoch window (5 before, self, 5 after) and are
NA for the first/last 5 epochs; sd_Last6 needs a full trailing
6-epoch window (self + 5 before) and is NA for the first 5 epochs;
logAct uses the following epoch's activity (shift(-1)) and is NA
for the last epoch. Where the resulting PS score is NA, pandas'
NaN > threshold evaluates to False (not propagated as missing), so
that epoch is scored wake (0), not excluded – reproduced here
explicitly, since R's NA > threshold gives NA, not FALSE.
algo = "ck" derives sleep/wake via pyActigraphy's native Cole-Kripke
implementation (.CK(), default settings = "30sec_max_non_overlap")
– a DIFFERENT weight set from the Condor-native ColeKripke class
used elsewhere in zeitR's pipeline (R/cole_kripke.R); the two share an
algorithm family name but are otherwise unrelated. Shares "sadeh"'s
two-step SRI aggregation and lack of off-wrist handling (see above).
Despite the reference notebook resampling to 30-second bins before
calling this, that round-trip is a mathematical no-op on data that was
only ever 1-minute resolution (verified by direct execution – see
?.ck_native_score), so this operates directly on native-resolution
activity with no resampling needed. Also applies Webster's (1982)
rescoring rules afterward, matching pyActigraphy's default. Uses the
opposite threshold polarity from Sadeh (D < threshold = sleep here,
vs PS > threshold = sleep for Sadeh) – both faithful to their own
respective source.
algo = "scripps" derives sleep/wake via pyActigraphy's native Scripps
Clinic implementation (Scripps()/_scripps()). Structurally identical
to algo = "ck" – same centered rolling weighted dot product, same
D < threshold = sleep polarity, same two-step SRI aggregation and
lack of off-wrist handling – just a different scale/window/threshold,
and no Webster rescoring step (pyActigraphy's Scripps() doesn't call
rescore(), unlike CK()).
algo = "roenneberg" derives sleep/wake via pyActigraphy's native
Roenneberg et al. algorithm (roenneberg()) – by far the most
involved of the four: trend extraction (a 24h centered rolling mean,
allowing a partial window down to 12h at the recording's edges), a
15%-of-trend threshold categorization, seed-finding (candidate
sleep-onset runs at least 30 min long), then an iterative
correlation-based bout-cleaning loop (each candidate onset is tested
against a family of triangular "sleep bout ending at position i"
templates over the following 12h, accepting the HIGHEST correlation
peak as the bout's offset – not simply the first qualifying one; see
?.find_highest_peak_idx for a real version mismatch this caught).
Shares the two-step SRI aggregation and lack of off-wrist handling
with the other three raw-activity algorithms above. No rescoring step
(rescoring is specific to CK()).
References
Phillips, A. J. K., Clerx, W. M., O'Brien, C. S., Sano, A., Barger, L. K., Picard, R. W., Lockley, S. W., Klerman, E. B., & Czeisler, C. A. (2017). Irregular sleep/wake patterns are associated with poorer academic performance and delayed circadian and sleep/wake timing. Scientific Reports, 7, 3216. doi:10.1038/s41598-017-03171-4
Sadeh, A., Sharkey, M., & Carskadon, M. A. (1994). Activity-Based Sleep-Wake Identification: An Empirical Test of Methodological Issues. Sleep, 17(3), 201-207. doi:10.1093/sleep/17.3.201
Examples
if (FALSE) { # \dontrun{
result <- run_pipeline_native("recordings/P001.txt", tz = "America/Sao_Paulo")
compute_sri(result)
compute_sri(result, algo = "sadeh")
} # }