ActiSleep estimates daily sleep duration from wrist or hip accelerometer data. The package implements the Pruned Dynamic Programming (PDP) algorithm described in Baek et al. (2021).
PDP solves the penalised segmentation problem: find the K-segment partition of an activity time series that minimises a cost function plus a penalty proportional to the number of breakpoints. The pruning step discards candidate breakpoints that cannot be optimal for any future index, reducing worst-case complexity from O(n²K) to O(nK).
Sleep estimation proceeds in four steps:
threshold_pct) are zeroed.no_activity_cutoff zero-count epochs) are merged into
candidate sleep windows.The package includes one subject’s data in
AccelData:
data("AccelData")
str(AccelData)
#> spc_tbl_ [1,351 × 11] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
#> $ date : chr [1:1351] "7/12/2016 17:00" "7/12/2016 17:01" "7/12/2016 17:02" "7/12/2016 17:03" ...
#> $ axis1 : num [1:1351] 848 1381 0 1567 2975 ...
#> $ axis2 : num [1:1351] 889 1197 0 1126 2157 ...
#> $ axis3 : num [1:1351] 1418 1074 2 1140 3195 ...
#> $ steps : num [1:1351] 6 12 0 13 23 23 23 24 39 17 ...
#> $ lux : num [1:1351] 0 31 0 0 0 29 71 20 171 315 ...
#> $ inclineOff : num [1:1351] 0 0 0 0 0 0 0 0 0 0 ...
#> $ inclineStanding: num [1:1351] 20 30 0 20 40 60 60 60 60 60 ...
#> $ inclineSitting : num [1:1351] 0 0 0 0 0 0 0 0 0 0 ...
#> $ inclineLying : num [1:1351] 40 30 60 40 20 0 0 0 0 0 ...
#> $ VM : num [1:1351] 1876 2120 2 2241 4869 ...
#> - attr(*, "spec")=List of 3
#> ..$ cols :List of 11
#> .. ..$ date : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
#> .. ..$ axis1 : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ axis2 : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ axis3 : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ steps : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ lux : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ inclineOff : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ inclineStanding: list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ inclineSitting : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ inclineLying : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> .. ..$ VM : list()
#> .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
#> ..$ default: list()
#> .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
#> ..$ delim : chr ","
#> ..- attr(*, "class")= chr "col_spec"
#> - attr(*, "problems")=<pointer: (nil)>
head(AccelData)
#> # A tibble: 6 × 11
#> date axis1 axis2 axis3 steps lux inclineOff inclineStanding inclineSitting
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 7/12/… 848 889 1418 6 0 0 20 0
#> 2 7/12/… 1381 1197 1074 12 31 0 30 0
#> 3 7/12/… 0 0 2 0 0 0 0 0
#> 4 7/12/… 1567 1126 1140 13 0 0 20 0
#> 5 7/12/… 2975 2157 3195 23 0 0 40 0
#> 6 7/12/… 3467 2392 2785 23 29 0 60 0
#> # ℹ 2 more variables: inclineLying <dbl>, VM <dbl>The date column contains minute-level timestamps and
VM is the vector magnitude (activity count).
Call estimate_sleep() with the data frame and the name
of the activity column. estimate_sleep() automatically
detects and parses common date-time formats.
result <- estimate_sleep(AccelData, activity_col = "VM")
result
#> ActiSleep Sleep Estimation
#> ----------------------------------------
#> Day of week : Tue
#> Cost model : poisson
#> Threshold pct : 0.4
#> Seg/hour : 3
#> Valid accel : yes
#> ----------------------------------------
#> Sleep segments found: 3
#> [1] 2016-07-13 00:51:00 -> 2016-07-13 01:28:00 (37 min, 79% zero)
#> [2] 2016-07-13 01:31:00 -> 2016-07-13 03:53:00 (142 min, 97% zero)
#> [3] 2016-07-13 03:54:00 -> 2016-07-13 09:08:00 (314 min, 90% zero)
#> ----------------------------------------Use the three S3 methods to inspect the result:
# Formatted per-segment summary
print(result)
#> ActiSleep Sleep Estimation
#> ----------------------------------------
#> Day of week : Tue
#> Cost model : poisson
#> Threshold pct : 0.4
#> Seg/hour : 3
#> Valid accel : yes
#> ----------------------------------------
#> Sleep segments found: 3
#> [1] 2016-07-13 00:51:00 -> 2016-07-13 01:28:00 (37 min, 79% zero)
#> [2] 2016-07-13 01:31:00 -> 2016-07-13 03:53:00 (142 min, 97% zero)
#> [3] 2016-07-13 03:54:00 -> 2016-07-13 09:08:00 (314 min, 90% zero)
#> ----------------------------------------
# Aggregate statistics
summary(result)
#> ActiSleep Result Summary
#> ----------------------------------------
#> Sleep segments : 3
#> Total sleep time : 493 min (8.2 hr)
#> Mean % zero epochs : 88.5%
#> ----------------------------------------
# Full data frame
df <- as.data.frame(result)
df
#> sleep_onset sleep_offset duration_min pct_zero_activity
#> 6 2016-07-13 00:51:00 2016-07-13 01:28:00 37 mins 0.7894737
#> 8 2016-07-13 01:31:00 2016-07-13 03:53:00 142 mins 0.9650350
#> 10 2016-07-13 03:54:00 2016-07-13 09:08:00 314 mins 0.9015873
#> no_activity_cutoff is_sleep is_no_activity diary_overlap diary_bedtime
#> 6 0.7 1 1 1 2016-07-12 22:00:00
#> 8 0.7 1 1 1 2016-07-12 22:00:00
#> 10 0.7 1 1 1 2016-07-12 22:00:00
#> diary_waketime n_sleep_segments
#> 6 2016-07-13 08:00:00 3
#> 8 2016-07-13 08:00:00 3
#> 10 2016-07-13 08:00:00 3Each row in the segments data frame represents one candidate sleep episode:
sleep_onset / sleep_offset — start and end
timestampsduration_min — duration in minutespct_zero_activity — proportion of zero-count epochs
(higher = more inactive)is_sleep — 1 if the segment meets all criteria for
sleepdiary_overlap — 1 if the segment overlaps the diary (or
default) windowSet detect_nonwear = TRUE to apply the accelerometry
non-wear algorithm before segmentation. Days with insufficient wear time
are flagged with valid_accel = 0 and return NA
segments.
result_nw <- estimate_sleep(
AccelData,
activity_col = "VM",
detect_nonwear = TRUE,
min_wear_minutes = 120
)
print(result_nw)
#> ActiSleep Sleep Estimation
#> ----------------------------------------
#> Day of week : Tue
#> Cost model : poisson
#> Threshold pct : 0.4
#> Seg/hour : 3
#> Valid accel : yes
#> ----------------------------------------
#> Sleep segments found: 3
#> [1] 2016-07-13 00:51:00 -> 2016-07-13 01:28:00 (37 min, 79% zero)
#> [2] 2016-07-13 01:31:00 -> 2016-07-13 03:53:00 (142 min, 97% zero)
#> [3] 2016-07-13 03:54:00 -> 2016-07-13 09:08:00 (314 min, 90% zero)
#> ----------------------------------------When self-reported bed and wake times are available, pass them as a
data.frame with columns bed and
wake.
data("SleepDiary1Day")
diary <- data.frame(
bed = SleepDiary1Day$bed,
wake = SleepDiary1Day$wake
)
result_diary <- estimate_sleep(
AccelData,
activity_col = "VM",
cost_model = "normal",
threshold_pct = 0,
detect_nonwear = TRUE,
segments_per_hour = 2,
no_activity_cutoff = 0.45,
min_sleep_minutes = 5,
use_diary = TRUE,
diary = diary
)
print(result_diary)
#> ActiSleep Sleep Estimation
#> ----------------------------------------
#> Day of week : Tue
#> Cost model : normal
#> Threshold pct : 0
#> Seg/hour : 2
#> Valid accel : yes
#> ----------------------------------------
#> Sleep segments found: 1
#> [1] 2016-07-13 00:17:00 -> 2016-07-13 10:21:00 (604 min, 86% zero)
#> ----------------------------------------Diary columns may be POSIXct objects or character
strings; estimate_sleep() parses them automatically using
common formats ("YYYY-MM-DD HH:MM",
"MM/DD/YYYY HH:MM", etc.).
Wrap estimate_sleep() in lapply() to
process a list of subjects:
# subject_list: list of named lists with $accel (data.frame) and $id (string)
results <- lapply(subject_list, function(subj) {
estimate_sleep(
data = subj$accel,
subject_id = subj$id,
activity_col = "VM"
)
})
# Combine all segments into one data frame
all_segments <- do.call(rbind, lapply(results, as.data.frame))ActiSleep supports five PDP cost functions:
cost_model |
Distribution | When to use |
|---|---|---|
"poisson" (default) |
Poisson | Non-negative integer counts — typical for raw actigraphy |
"normal" |
Normal | Continuous or approximately symmetric data |
"negative_binomial" |
Negative binomial | Overdispersed integer counts |
"variance" |
Variance change | Constant-mean series with changing variance |
"exponential" |
Exponential | Positive continuous inter-event times |
Integer codes 1–5 are also accepted.
threshold_pctControls how aggressively low-activity epochs are zeroed before segmentation.
segments_per_hourControls the temporal resolution of the segmentation.
no_activity_cutoffMinimum proportion of zero-count epochs to label a segment as “inactive”.
min_sleep_minutesSegments shorter than this are not classified as sleep. Raise this value if short spurious segments are being detected (e.g. during long inactive rest periods while awake).
ActiGraph devices produce .agd files (SQLite databases).
Use read_agd() to extract the device settings and raw
accelerometer data:
agd <- read_agd("subject01.agd", tz = "America/New_York")
head(agd$raw.data) # date, axis1, axis2, axis3, steps, lux, ...
agd$settings # device metadataThe returned raw.data data frame can be passed directly
to estimate_sleep() after selecting the appropriate
activity column.
Baek, J., Banker, M., Jansen, E. C., She, X., Peterson, K. E., Pitchford, E. A., & Song, P. X. K. (2021). An efficient segmentation algorithm to estimate sleep duration from actigraphy data. Statistics in Biosciences. https://doi.org/10.1007/s12561-021-09309-3