Introduction
This vignette describes the pipeline for computing daily and weekly Fitbit activity and sleep summary scores. The pipeline processes minute-level epoch data through a series of heart rate quality control steps before aggregating to daily and then weekly summaries.
All the relevant code for computing these scores can be found in the
R/scores_nt_fitbit.R file in the ABCDscores
package, which has the following structure.
Exported functions
There are six main exported functions, four for daily and two for weekly summaries.
The four functions that compute the daily summaries are:
Weekly summaries are computed from the daily outputs using:
The above two weekly summary functions are wrappers around a primary
function that computes weekly summaries,
compute_fitbit_weekly_summary().
Input Data
The activity functions take two input data frames:
-
data_activity: eitherfitbit_raw_activityorfitbit_covid_raw_activity, and -
data_daily: eitherfitbit_raw_metricsorfitbit_covid_raw_metrics
While the sleep functions additionally require:
-
data_sleep_combined: eitherfitbit_raw_sleeporfitbit_covid_raw_sleep
These files are available in the ABCD 7.0 release and later.
data_activity
either
fitbit_raw_activityorfitbit_covid_raw_activity
Minute-level epoch data. Required columns:
-
participant_id: Participant identifier -
session_id: Session identifier -
dtt: POSIXct timestamp -
hrate: Heart rate (bpm) -
steps: Step count -
intnst: Activity intensity (0–3) -
mets: Metabolic equivalent of task -
is_slp: Logical, whether epoch was classified as sleep -
day: Study day index (integer; day 0 = pre-study, day 1 = first study day) -
main_slp: Logical, whether epoch belongs to main sleep period
Data Exclusions using Heart Rate QC
Before any scores are computed, each pipeline invokes a QC step to identify and exclude problematic or erroneous epochs (Wing et al., 2022) based on QC of the heart rate data using two internal functions:
identify_hr_exclusions()
Flags implausible or poor-quality heart rate epochs based on four criteria:
-
Low heart rate (
hrate_low): heart rate belowhr_low(default 50 bpm) -
High heart rate (
hrate_hi): heart rate abovehr_high(optional;NULLby default) -
Missing heart rate (
hrate_no):NAvalues inhrate -
Plateau / repeated values
(
hrate_repeat_actv,hrate_repeat_slp): consecutive identical values exceedinghr_rep_day(default 10 epochs) during wake, orhr_rep_sleep(default 30 epochs) during sleep
A conservative bookend fill is applied before plateau detection: runs of missing values bounded by identical values on both sides are temporarily filled for the purpose of plateau detection only.
Exclusion flags follow a strict priority hierarchy: repeat exclusions take precedence over all others, ensuring plateau detection is not masked by co-occurring low, high, or missing flags.
The composite flag hrate_excluded is TRUE
if any exclusion criterion is met. Two domain-specific flags —
hr_exc_day (wake) and hr_exc_night (sleep) —
are also retained internally to support downstream detection of
implausible sleep structure (flg_slp), which relies on
distinguishing HR issues that occurred during wake from those that
occurred during sleep.
identify_hr_recovery()
Identifies heart rate exclusions at the midnight boundary. When a
sleep run with missing heart rate ends at 23:59 and is immediately
followed by a contiguous sleep run with valid heart rate, the earlier
run is marked recover_hr = TRUE. The
min_extra_nohrate_slp column in the sleep table counts
these flagged minutes, giving analysts visibility into how much excluded
sleep time may have been valid. These epochs remain excluded from all
summary scores.
Understanding Released Scores
Choosing Between Standard and Extended Scores
The standard functions
(compute_fitbit_activity_table(),
compute_fitbit_sleep_table()) use Fitbit’s built-in sleep
classification directly.
The extended functions
(compute_fitbit_activity_table_ext(),
compute_fitbit_sleep_table_ext()) add an episode-based
sleep reconstruction step before computing summaries. Sleep epochs are
grouped into continuous blocks using create_block_ranges(),
with short gaps between epochs bridged within the same episode. Heart
rate exclusions are then recomputed on the revised sleep classification.
This approach can produce different results from the standard functions
when Fitbit’s built-in sleep classification produces fragmented or
discontinuous sleep blocks.
Daily Activity Table
Standard: compute_fitbit_activity_table()
activity_day <- compute_fitbit_activity_table(
data_activity = fitbit_raw_activity,
data_daily = fitbit_raw_metrics,
hr_low = 50,
hr_high = NULL,
hr_rep_day = 10,
hr_rep_sleep = 30,
bin_minutes = 1440,
append_daily_fitbit_qc = TRUE,
qc_threshold = .8,
offset = "12:00",
min_actv_minutes = 600
)Key parameters:
-
bin_minutes: Aggregation window in minutes. Use1440for calendar-day summaries (default). Smaller values (e.g.,60) produce sub-daily bins. -
offset: Time of day (HH:MM) used as the day boundary for sleep-aligned dates — timestamps after this time are assigned to the next day’ssleep_date. Also passed tomake_offset()to computeflg_30sec(the 30-second sleep presence indicator). Whenoffset = NULL,bin_minutesis used instead. The two are effectively mutually exclusive. -
min_actv_minutes: Threshold for theqc_{n}minquality control flag. A day passes QC ifmin_actv >= min_actv_minutes. Passing600createsqc_600min; passing480createsqc_480min, etc. -
append_daily_fitbit_qc: If TRUE, appends Fitbit-reported step counts and computesqc_stepsandpcnt_steps_fitb. Only available whenbin_minutes= 1440. -
qc_threshold: Minimum proportion of Fitbit-reported steps that computed active steps must meet for qc_steps to pass. Defaults to 0.8. qc_steps isTRUEifsteps_actv >= qc_threshold * steps_fitb,FALSEif below that threshold, andNAif steps_fitb is zero or missing.pcnt_steps_fitbgives the underlying ratio (steps_actv / steps_fitb), set to 1 when both values are equal, and NA when steps_fitb is zero or missing.
Output columns include:
- Identifiers:
participant_id,session_id,wk,day,dt,dt_day,dt_wknd - Minutes:
min_total,min_actv,min_slp,min_nap_slp - Steps:
steps_total,steps_actv,steps,steps_fitb - Intensity:
mets,mets_actv,min_intnst_sed_*,min_intnst_light_*,min_intnst_mod_*,min_intnst_vigor_*(total and active variants) - HR exclusions:
excl_min_total_actv,excl_min_lowhrate_actv,excl_min_nohrate_actv,excl_min_repeathrate_actv(and sleep equivalents) - QC:
qc_{n}min,qc_steps,pcnt_steps_fitb - Flags:
flg_30sec,flg_any
Day numbering: Day 0 (pre-study data) is excluded
from the output via filter(day != 0). Study week is derived
as wk = (day - 1) %/% 7 + 1.
Extended: compute_fitbit_activity_table_ext()
activity_day_ext <- compute_fitbit_activity_table_ext(
data_activity = fitbit_raw_activity,
data_daily = fitbit_raw_metrics,
hr_low = 50,
hr_rep_day = 10,
hr_rep_sleep = 30,
bin_minutes = 1440,
append_daily_fitbit_qc = TRUE,
offset = "12:00",
min_actv_minutes = 600,
main_duration = 180,
gap = 90
)The extended pipeline adds an episode-based sleep reconstruction step
before computing activity scores. Sleep epochs are grouped into
continuous blocks using create_block_ranges(), with gaps up
to gap minutes (default 90) bridged within the same
episode. Heart rate exclusions are then recomputed on the revised sleep
classification. Additional parameters:
-
gap: Maximum allowed gap in minutes between sleep epochs before starting a new episode. -
main_duration: Minimum episode duration in minutes to qualify as main sleep (default 180).
Daily Sleep Table
Standard: compute_fitbit_sleep_table()
sleep_day <- compute_fitbit_sleep_table(
data_activity = fitbit_raw_activity,
data_sleep_combined = fitbit_raw_sleep,
data_daily = fitbit_raw_metrics,
hr_low = 50,
hr_high = NULL,
hr_rep_sleep = 30,
bin_minutes = NULL,
offset = "12:00",
min_slp_minutes = 300
)Key parameters:
-
bin_minutes: IfNULL(default), sleep is aligned usingoffsetand sleep timing variables (dtt_start_slp,dtt_end_slp, WASO) are computed. If a numeric value is supplied, data is binned by that interval instead and timing variables are not calculated. -
offset: Noon-to-noon boundary (default"12:00"). Sleep occurring after noon is assigned to the following day’ssleep_date. -
min_slp_minutes: Threshold for theqc_{n}minQC flag. A day passes ifmin_total_slp >= min_slp_minutes. Passing300createsqc_300min.
Output columns include:
- Identifiers:
participant_id,session_id,wk,day,dt,dt_day,dt_wknd - Timing:
dtt_start_bed,dtt_end_bed,dtt_start_slp,dtt_end_slp - Duration:
min_total_slp,min_asleep_slp,min_restless_slp,min_light_slp,min_deep_slp,min_rem_slp,min_nap_slp,min_wake - WASO:
min_waso,n_waso - HR by stage:
hrate_awake_slp,hrate_restless_slp,hrate_asleep_slp,hrate_light_slp,hrate_deep_slp,hrate_rem_slp,hrate_nap_slp,hrate_rest_fitb - HR exclusions:
excl_min_total_slp,excl_min_lowhrate_slp,excl_min_nohrate_slp,excl_min_repeathrate_slp,min_extra_nohrate_slp - QC:
qc_{n}min - Flags:
flg_slp,flg_any
Sleep date alignment: Sleep dates are aligned to
study day numbers using data_activity rather than the raw
sleep data day column, since the offset may shift
sleep_date forward by one calendar day.
Implausible sleep flagging:
flg_slp = TRUE when a run of HR-flagged awake minutes
(>= 30 minutes by default) immediately follows a sleep run with no HR
issues, suggesting device removal during a recorded sleep period. This
flag is only computed when bin_minutes = NULL.
Extended: compute_fitbit_sleep_table_ext()
sleep_day_ext <- compute_fitbit_sleep_table_ext(
data_activity = fitbit_raw_activity,
data_sleep_combined = fitbit_raw_sleep,
data_daily = fitbit_raw_metrics,
hr_low = 50,
hr_rep_day = 10,
hr_rep_sleep = 30,
bin_minutes = NULL,
offset = "12:00",
min_slp_minutes = 300,
main_duration = 180,
gap = 90
)The extended sleep pipeline reconstructs sleep episodes from
30-second epochs before computing summaries. This is the same
episode-based approach as the extended activity table — sleep blocks are
formed using create_block_ranges(), and main sleep is
redefined based on block duration. HR exclusion thresholds are doubled
during the second pass (hr_rep_day * 2,
hr_rep_sleep * 2) because the sleep data operates on
30-second epochs rather than the minute-level epochs used in the
activity pipeline, so twice as many consecutive identical values are
expected for the same duration of flat HR signal. Pass the same
(un-doubled) values you would use for minute-level data; the function
doubles them internally.
Weekly Summaries
Weekly summaries are computed from the daily tables using
compute_fitbit_activity_week() and
compute_fitbit_sleep_week(). Before aggregation, days are
filtered using the filter_expr parameter to retain only
those passing quality control. The default filters are:
- Activity:
qc_600min&qc_steps— retains days with at least 600 valid active minutes and step count concordance with Fitbit-reported totals - Sleep:
qc_300min— retains days with at least 300 valid sleep minutes
activity_week <- compute_fitbit_activity_week(
df = activity_day,
filter_expr = qc_600min & qc_steps
)
sleep_week <- compute_fitbit_sleep_week(
df = sleep_day,
filter_expr = qc_300min
)Important: filter_expr must reference columns that exist
in the daily table. If append_daily_fitbit_qc = FALSE was
passed to compute_fitbit_activity_table(), the
qc_steps column will be absent and the default
filter_expr will fail. Update filter_expr to
drop qc_steps in that case. Similarly, if min_actv_minutes
or min_slp_minutes were changed from their defaults, the
qc_{n}min column name changes accordingly and
filter_expr must match — see Valid Minute Thresholds.
Each function produces three parallel summaries per
participant-session-week, identified by wk_type:
QC flag (qc_wk): A week is considered
valid if it meets minimum day count thresholds (defaults: 3 weekdays, 1
weekend day). For whole-week estimates, both thresholds must be met. For
weekday/weekend strata, only the relevant threshold applies. These
thresholds can be adjusted by calling
compute_fitbit_weekly_summary() directly with
wkdy_min and wknd_min values that match your
coverage requirements.
Filtering: Only days passing the
filter_expr are included before aggregation. Day counts
(n_day, n_wkdy, n_wknd) reflect
valid days only and are used to compute qc_wk.
Heart rate aggregation in sleep: Stage-specific heart rate values are computed as duration-weighted means rather than simple means, e.g.:
Circular mean for timing variables: Bed/sleep onset and offset times are averaged using circular mean arithmetic to correctly handle times that span midnight.
Re-computing Scores
Key Parameters to Change
bin_minutes and offset
When bin_minutes = NULL (default), days are defined
using offset — a noon-to-noon boundary where sleep
occurring after noon is assigned to the following day’s
sleep_date. This alignment better captures the natural
sleep period, which typically spans midnight. In this mode, sleep timing
variables (dtt_start_bed, dtt_end_bed,
dtt_start_slp, dtt_end_slp), WASO
(min_waso, n_waso), and implausible sleep
flagging (flg_slp) are all computed.
When bin_minutes is set to a numeric value, data is
aggregated into fixed time bins of that length (e.g., 1440
for full calendar days). The offset is ignored in this mode
and sleep timing variables, WASO, and flg_slp are not
computed — those columns will be empty in the output.
# Default: noon-to-noon sleep day alignment
# Sleep timing and WASO variables are computed
compute_fitbit_sleep_table(
...,
bin_minutes = NULL, # sleep timing and WASO are computed
offset = "12:00" # sleep after noon assigned to next day
)
# Binned: calendar-day aggregation, no timing variables
compute_fitbit_sleep_table(
...,
bin_minutes = 1440, # aggregate to full calendar days
offset = NULL # offset is ignored when bin_minutes is set
)Use bin_minutes = 1440 only if sleep timing and WASO
variables are not needed and a simpler calendar-day aggregation is
sufficient. Use smaller values (e.g., 60) only if sub-daily
summaries are needed.
Valid Minute Thresholds
min_actv_minutes and min_slp_minutes
thresholds control the name and value of the QC flag column in the daily
summary output. Changing the value changes the column name:
# Produces column: qc_600min
compute_fitbit_activity_table(..., min_actv_minutes = 600)
# Produces column: qc_480min
compute_fitbit_activity_table(..., min_actv_minutes = 480)
# Produces column: qc_300min
compute_fitbit_sleep_table(..., min_slp_minutes = 300)Important: The
filter_exprincompute_fitbit_activity_week()must match the threshold used in the daily function. If you changemin_actv_minutes, updatefilter_expraccordingly — otherwise the filter will fail to find the column.
# Consistent: both use 600 minutes
activity_day <- compute_fitbit_activity_table(
...,
min_actv_minutes = 600 # creates qc_600min
)
activity_week <- compute_fitbit_activity_week(
df = activity_day,
filter_expr = qc_600min & qc_steps # matches above
)
# If you change the threshold, update filter_expr too
activity_day <- compute_fitbit_activity_table(
...,
min_actv_minutes = 480 # creates qc_480min
)
activity_week <- compute_fitbit_activity_week(
df = activity_day,
filter_expr = qc_480min & qc_steps # updated to match
)Heart Rate Thresholds
The default heart rate exclusion parameters follow recommendations
from (Wing et al., 2022). Heart rate
values below hr_low (default 50 bpm) or above
hr_high (default NULL, i.e. no upper bound)
are flagged as invalid, and all data for those minutes — including
steps, METs, and intensity — are excluded from summary scores. The
repeat thresholds (hr_rep_day, hr_rep_sleep)
control how many consecutive identical heart rate values are tolerated
before flagging a plateau — lower values are more aggressive in
excluding flat HR signals. All parameters can be adjusted to suit user
needs.
compute_fitbit_activity_table(
...,
hr_low = 50, # exclude epochs with HR below 50 bpm
hr_high = NULL, # set a value (e.g., 200) to exclude high HR
hr_rep_day = 10, # flag runs of identical HR > 10 epochs during wake
hr_rep_sleep = 30 # flag runs of identical HR > 30 epochs during sleep
)Setting any parameter to NULL disables that check
entirely. For example, hr_high = NULL (default) applies no
upper bound on heart rate, and hr_rep_day = NULL disables
plateau detection during wake epochs. Note that in
compute_fitbit_sleep_table() or
compute_fitbit_sleep_table_ext(), hr_rep_day
and hr_rep_sleep should not be NULL as plateau
detection is needed to correctly identify implausible sleep structures
(flg_slp).
Daily Metrics
Daily resting heart rate values (hrate_rest_fitb) are
appended to both sleep and activity daily score summaries from
data_daily. Note that when data is binned to sub-daily
intervals (bin_minutes < 1440), the same resting heart
rate value associated with that calendar date will be repeated across
all bins within a day.
Fitbit-reported step counts (steps_fitb) and the
corresponding step count quality control variables
(qc_steps, pcnt_steps_fitb) are optionally
appended to activity summaries by setting
append_daily_fitbit_qc = TRUE (the default).
qc_steps flags days where computed steps are at least 80%
of Fitbit-reported steps; pcnt_steps_fitb gives the
underlying percentage. When append_daily_fitbit_qc = FALSE,
these columns are omitted from the output.
Example: Re-run Full Pipeline
The recommended end-to-end workflow processes daily summaries first, then passes them to the weekly aggregation functions. The following code runs the full pipeline using default parameters, which follow recommendations from (Wing et al., 2022).
# Step 1: Compute daily activity scores
activity_day <- compute_fitbit_activity_table(
data_activity = fitbit_raw_activity,
data_daily = fitbit_raw_metrics,
hr_low = 50,
hr_high = NULL,
hr_rep_day = 10,
hr_rep_sleep = 30,
bin_minutes = 1440,
append_daily_fitbit_qc = TRUE,
qc_threshold = .8,
offset = "12:00",
min_actv_minutes = 600
)
# Step 2: Compute daily sleep scores
sleep_day <- compute_fitbit_sleep_table(
data_activity = fitbit_raw_activity,
data_sleep_combined = fitbit_raw_sleep,
data_daily = fitbit_raw_metrics,
hr_low = 50,
hr_high = NULL,
hr_rep_day = 10,
hr_rep_sleep = 30,
bin_minutes = NULL,
offset = "12:00",
min_slp_minutes = 300
)
# Step 3: Aggregate to weekly activity summaries
# filter_expr must reference the qc column created in Step 1
activity_week <- compute_fitbit_activity_week(
df = activity_day,
filter_expr = qc_600min & qc_steps
)
# Step 4: Aggregate to weekly sleep summaries
sleep_week <- compute_fitbit_sleep_week(
df = sleep_day
)Example: Relaxed Quality Control Pipeline
The default pipeline requires 600 valid active minutes per day and 300 valid sleep minutes. These thresholds, along with the heart rate exclusion parameters, can be adjusted to support alternative quality control criteria. The following example lowers the valid minute thresholds, adjusts the heart rate exclusion parameters, and updates filter_expr to remain aligned with the revised thresholds.
# Step 1: Compute daily activity scores
activity_day <- compute_fitbit_activity_table(
data_activity = fitbit_raw_activity,
data_daily = fitbit_raw_metrics,
hr_low = 40, # changed: more permissive lower HR bound
hr_high = 250, # changed: added upper HR bound
hr_rep_day = 15, # changed: tolerate longer flat HR runs during wake
hr_rep_sleep = 40, # changed: tolerate longer flat HR runs during sleep
bin_minutes = 1440,
append_daily_fitbit_qc = TRUE,
qc_threshold = .5, # changed: tolerate lower accordance between device step count and step score
offset = "17:00", # changed: shifted day boundary to better align with later sleep onset times
min_actv_minutes = 480 # changed: require 480 rather than 600 valid minutes
)
# Step 2: Compute daily sleep scores
sleep_day <- compute_fitbit_sleep_table(
data_activity = fitbit_raw_activity,
data_sleep_combined = fitbit_raw_sleep,
data_daily = fitbit_raw_metrics,
hr_low = 40, # changed: must match activity pipeline
hr_high = 250, # changed: must match activity pipeline
hr_rep_day = 15, # changed: must match activity pipeline
hr_rep_sleep = 40, # changed: must match activity pipeline
bin_minutes = NULL,
offset = "17:00", # changed: must match activity pipeline
min_slp_minutes = 240 # changed: require 240 rather than 300 valid sleep minutes
)
# Step 3: Aggregate to weekly activity summaries
activity_week <- compute_fitbit_activity_week(
df = activity_day,
filter_expr = qc_480min & qc_steps # changed: matches min_actv_minutes = 480 above
)
# Step 4: Aggregate to weekly sleep summaries
sleep_week <- compute_fitbit_sleep_week(
df = sleep_day,
filter_expr = qc_240min # changed: matches min_slp_minutes = 240 above
)Two reminders when re-computing scores:
-
bin_minutesandoffsetare mutually exclusive in the sleep pipeline. Providing a non-NULLbin_minutestriggers a warning thatoffsetwill be ignored, and sleep timing/WASO variables will be empty. - The
qc_{n}mincolumn name is dynamically generated from themin_actv_minutes/min_slp_minutesparameter, so passing600producesqc_600min, passing300producesqc_300min, and so on.