This vignette introduces
run_pipeline_native(), the Vallim pipeline
— a sleep classification layer developed by Julia Ribeiro da Silva
Vallim that addresses limitations of the base CSPD pipeline in detecting
and classifying sleep episodes from actigraphy data.
The Condor CSPD pipeline (run_pipeline()) detects sleep
periods using an adaptive median filter and returns one row per period.
It works well for clear-cut recordings but can misclassify episodes in
several edge cases:
The Vallim pipeline keeps the same off-wrist detection and CSPD epoch
scorer as run_pipeline() but replaces the final
classification step with a 7-rule adaptive system ported from Julia
Vallim’s Python notebook
(vs_condor_py_pipeline_fix29_jrsv.ipynb).
The classification follows this fixed execution order (Fix 27):
| Step | Fix/Rule | Description |
|---|---|---|
| 1 | Fix 25 | Exclude truncated episodes at the recording end |
| 2 | Fix 26a | Infer adaptive nocturnal window from wrist temperature and light |
| 3 | Fix 29 / Rule 2 | Split TBT 14–16 h episodes; exclude TBT > 16 h |
| 4 | Fix 26c | Recover fragmented sleep nights missed by the scorer |
| 5 | Rules 3–5 | Classify as main or secondary using the nocturnal window |
| 6 | Fix 26b | Resolve sleep-date collisions from the noon threshold |
| 7 | Rule 6 | Keep the longest main episode per sleep date |
| 8 | Rule 7 | Exclude all episodes on days with no main sleep |
The nocturnal window (Fix 26a) is inferred adaptively per participant rather than fixed at 18:00–06:00. Episodes where wrist temperature ≥ 28 °C and ambient light ≤ 5 lux are used as candidates; the circular mean of their onsets becomes the window anchor (±4 h).
FILE <- system.file("extdata", "input1.txt", package = "zeitR")
TZ <- "America/Sao_Paulo"
result <- run_pipeline_native(FILE, tz = TZ, quiet = TRUE)
#> ℹ Reading 'input1.txt' ...
#> ✔ [input1] Done. 52 main night(s), 0 secondary episode(s).
result
#>
#> ── zeitr_result: input1 ────────────────────────────────────────────────────────
#> • Source: /tmp/RtmpHIeM0o/Rinst107b1a078e73/zeitR/extdata/input1.txt
#> • Epochs: 76196
#> • Nights: 52
#> • Issues: 5The nights table has the same schema as
run_pipeline() but adds a sleep_type
column:
result$nights |>
mutate(
bed_time = format(bed_time, "%Y-%m-%d %H:%M"),
get_up_time = format(get_up_time, "%Y-%m-%d %H:%M"),
tbt_h = round(tbt / 60, 1),
tst_h = round(tst / 60, 1),
eff_pct = round(eff * 100, 1)
) |>
select(night, sleep_type, bed_time, get_up_time, tbt_h, tst_h, waso, sol, eff_pct) |>
head(10) |>
knitr::kable(
col.names = c("Night", "Type", "Bed time", "Get-up time",
"TBT (h)", "TST (h)", "WASO (min)", "SOL (min)", "Eff (%)"),
align = "clllrrrrr"
)| Night | Type | Bed time | Get-up time | TBT (h) | TST (h) | WASO (min) | SOL (min) | Eff (%) |
|---|---|---|---|---|---|---|---|---|
| 1 | main | 2021-05-27 23:42 | 2021-05-28 06:53 | 7.2 | 6.9 | 18 | 0 | 95.8 |
| 2 | main | 2021-05-28 23:30 | 2021-05-29 07:15 | 7.8 | 7.3 | 25 | 0 | 94.4 |
| 3 | main | 2021-05-30 22:18 | 2021-05-31 07:57 | 9.7 | 9.2 | 27 | 0 | 95.3 |
| 4 | main | 2021-05-31 23:24 | 2021-06-01 07:13 | 7.8 | 7.6 | 12 | 0 | 97.0 |
| 5 | main | 2021-06-02 00:10 | 2021-06-02 07:18 | 7.2 | 6.8 | 23 | 0 | 94.6 |
| 6 | main | 2021-06-02 23:05 | 2021-06-03 07:46 | 8.7 | 8.2 | 30 | 0 | 94.3 |
| 7 | main | 2021-06-04 00:01 | 2021-06-04 08:28 | 8.5 | 7.8 | 39 | 0 | 92.3 |
| 8 | main | 2021-06-04 23:36 | 2021-06-05 05:19 | 5.7 | 5.3 | 4 | 9 | 93.3 |
| 9 | main | 2021-06-06 00:34 | 2021-06-06 06:19 | 5.8 | 5.7 | 7 | 0 | 98.0 |
| 10 | main | 2021-06-07 00:13 | 2021-06-07 06:58 | 6.8 | 6.7 | 6 | 0 | 98.5 |
Running both pipelines on the same recording lets you see exactly what the Vallim rule set changes:
cspd <- run_pipeline(FILE, tz = TZ, quiet = TRUE)
#> ℹ Reading 'input1.txt' ...
#> ✔ [input1] Done. 55 night(s) detected.
native <- run_pipeline_native(FILE, tz = TZ, quiet = TRUE)
#> ℹ Reading 'input1.txt' ...
#> ✔ [input1] Done. 52 main night(s), 0 secondary episode(s).
cat("CSPD nights: ", nrow(cspd$nights), "\n")
#> CSPD nights: 55
cat("Vallim nights:", nrow(native$nights), "\n")
#> Vallim nights: 52
cat(" main: ", sum(native$nights$sleep_type == "main"), "\n")
#> main: 52
cat(" secondary: ", sum(native$nights$sleep_type == "secondary"), "\n")
#> secondary: 0The Vallim pipeline consolidates duplicate-date episodes (Rule 6) and discards dates where no valid main sleep exists (Rule 7), resulting in a cleaner set of main nights.
circ_mean_h() and circ_sd_h() compute
circular statistics for clock-time variables. Using standard arithmetic
mean for times like sleep onset (which often straddles midnight)
inflates variability estimates and produces biased means; the circular
mean handles the wrap correctly.
main <- native$nights |> filter(sleep_type == "main")
# Decimal hours of bed time and get-up time
onset_h <- as.numeric(format(main$bed_time, "%H", tz = TZ)) +
as.numeric(format(main$bed_time, "%M", tz = TZ)) / 60
offset_h <- as.numeric(format(main$get_up_time, "%H", tz = TZ)) +
as.numeric(format(main$get_up_time, "%M", tz = TZ)) / 60
cat(sprintf("Mean sleep onset: %05.2f h (SD = %.2f h)\n",
circ_mean_h(onset_h), circ_sd_h(onset_h)))
#> Mean sleep onset: 23.80 h (SD = 0.89 h)
cat(sprintf("Mean sleep offset: %05.2f h (SD = %.2f h)\n",
circ_mean_h(offset_h), circ_sd_h(offset_h)))
#> Mean sleep offset: 07.73 h (SD = 0.77 h)The circ_mean_h() function returns decimal hours in [0,
24). To display as HH:MM:
fmt_h <- function(h) sprintf("%02d:%02d", as.integer(h) %% 24L,
as.integer((h %% 1) * 60))
cat("Mean sleep onset: ", fmt_h(circ_mean_h(onset_h)), "\n")
#> Mean sleep onset: 23:47
cat("Mean get-up time: ", fmt_h(circ_mean_h(offset_h)), "\n")
#> Mean get-up time: 07:43For custom workflows, extract_sleep_episodes() and
classify_sleep_episodes() expose the two stages
independently.
# Step 1: read, prepare, detect off-wrist and CSPD-score epochs
rec <- read_acttrust(FILE, tz = TZ)
prep <- prepare_actigraphy(rec)
prep <- detect_offwrist_bimodal(prep)
prep <- detect_sleep_crespo(prep)
# Step 2: extract per-episode statistics from the CSPD state
episodes_raw <- extract_sleep_episodes(prep)
# Step 3: apply the Vallim rule set
episodes <- classify_sleep_episodes(
episodes = episodes_raw,
data = prep,
verbose = TRUE # print each fix as it fires
)
episodes |> filter(sleep_type == "main")The verbose = TRUE argument prints a trace of each
rule:
[Fix 26a] anchor=23.8 h -> nocturnal window 19.8 h-3.8 h
[Fix 26c] No fragmented episodes recovered.
[Fix 26b] No sleep-date collisions detected.
run_pipeline_native_batch() works identically to
run_pipeline_batch():
results <- run_pipeline_native_batch(
"recordings/",
tz = "America/Sao_Paulo",
quiet = TRUE
)
# Combine all main nights
main_all <- bind_rows(
lapply(names(results), function(id) {
results[[id]]$nights |>
filter(sleep_type == "main") |>
mutate(subject_id = id)
})
)vignette("sleep-analysis") — CSPD pipeline walkthrough
with actogram?export_hypnogram — export results to
hypnoR for sleep architecture metrics?compute_sleep_metrics,
?compute_cpd_metrics — day-type sleep summary and
chronotype?classify_sleep_episodes — full parameter reference for
the rule set?circ_mean_h, ?circ_sd_h — circular
statistics for clock-time variables?acttrust_params — device parameter defaults