wristpy

DOI

wristpy

A Python package for wrist-worn accelerometer data processing.

Build codecov Ruff stability-experimental LGPL--2.1 License pages

Welcome to wristpy, a Python library designed for processing and analyzing wrist-worn accelerometer data. This library provides a set of tools for loading sensor information, calibrating raw accelerometer data, calculating physical activity metrics (ENMO derived) and sleep metrics (angle-Z derived), finding non-wear periods, and detecting sleep periods (onset and wakeup times). Additionally, we provide access to other sensor data that may be recorded by the watch, including; temperature, luminosity, capacitive sensing, battery voltage, and all metadata.

Supported formats & devices

The package currently supports the following formats:

Format Manufacturer Device Implementation status
GT3X Actigraph wGT3X-BT
BIN GENEActiv GENEActiv

Special Note The idle_sleep_mode for Actigraph watches will lead to uneven sampling rates during periods of no motion (read about this here). Consequently, this causes issues when implementing wristpy's non-wear and sleep detection. As of this moment, we fill in the missing acceleration data with the assumption that the watch is perfeclty idle in the face-up position (Acceleration vector = [0, 0, -1]). The data is filled in at the same sampling rate as the raw acceleration data. In the special circumstance when acceleration samples are not evenly spaced, the data is resampled to the highest effective sampling rate to ensure linearly sampled data.

Processing pipeline implementation

The main processing pipeline of the wristpy module can be described as follows:

  • Data loading: sensor data is loaded using actfast, and a WatchData object is created to store all sensor data
  • Data calibration: A post-manufacturer calibration step can be applied, to ensure that the acceleration sensor is measuring 1g force during periods of no motion. There are three possible options: None, gradient, ggir.
  • Data imputation In the special case when dealing with the Actigraph idle_sleep_mode == enabled, the gaps in acceleration are filled in after calibration, to avoid biasing the calibration phase.
  • Metrics Calculation: Calculates various metrics on the calibrated data, namely ENMO (Euclidean norm , minus one) and angle-Z (angle of acceleration relative to the x-y axis).
  • Non-wear detection: We find periods of non-wear based on the acceleration data. Specifically, the standard deviation of the acceleration values in a given time window, along each axis, is used as a threshold to decide wear or not wear.
  • Sleep Detection: Using the HDCZ1 and HSPT2 algorithms to analyze changes in arm angle we are able to find periods of sleep. We find the sleep onset-wakeup times for all sleep windows detected.
  • Physical activity levels: Using the enmo data (aggreagated into epoch 1 time bins, 5 second default) we compute activity levels into the following categories: inactivity, light activity, moderate activity, vigorous activity. The default threshold values have been chosen based on the values presented in the Hildenbrand 2014 study3.

Installation

Install this package from PyPI via :

pip install wristpy

Quick start

wristpy provides three flexible interfaces: a command-line tool for direct execution, an importable Python library, and a Docker image for containerized deployment.

Using Wristpy through the command-line:

Run single files:

wristpy /input/file/path.gt3x -o /save/path/file_name.csv -c gradient

Run entire directories:

wristpy /path/to/files/input_dir -o /path/to/files/output_dir -c gradient -O .csv

Using Wristpy through a python script or notebook:

Running single files:

from wristpy.core import orchestrator

# Define input file path and output location
# Support for saving as .csv and .parquet
input_path = '/path/to/your/file.gt3x'
output_path = '/path/to/save/file_name.csv'

# Run the orchestrator
results = orchestrator.run(
    input=input_path,
    output=output_path,
    calibrator='gradient',  # Choose between 'ggir', 'gradient', or 'none'
)

#Data available in results object
enmo = results.enmo
anglez = results.anglez
physical_activity_levels = results.physical_activity_levels
nonwear_array = results.nonwear_epoch
sleep_windows = results.sleep_windows_epoch

Running entire directories:

from wristpy.core import orchestrator

# Define input file path and output location

input_path = '/path/to/files/input_dir'
output_path = '/path/to/files/output_dir'

# Run the orchestrator
# Specify the output file type, support for saving as .csv and .parquet
results_dict = orchestrator.run(
    input=input_path,
    output=output_path,
    calibrator='gradient',  # Choose between 'ggir', 'gradient', or 'none'
    output_filetype = '.csv'
)


#Data available in dictionry of results.
subject1 = results_dict['subject1']

enmo = subject1.enmo
anglez = subject1.anglez
physical_activity_levels = subject1.physical_activity_levels
nonwear_array = subject1.nonwear_epoch
sleep_windows = subject1.sleep_windows_epoch

Using Wristpy Through Docker

  1. Install Docker: Ensure you have Docker installed on your system. Get Docker

  2. Pull the Docker image:

    docker pull cmidair/wristpy:main
    
  3. Run the Docker image with your data:

    docker run -it --rm \
      -v "/local/path/to/data:/data" \
      -v "/local/path/to/output:/output" \
      cmidair/wristpy
    

    Replace /local/path/to/data with the path to your input data directory and /local/path/to/output with where you want results saved.

    To run a single file, we simply need to modify the mounting structure for the docker call slightly: bash docker run -it --rm \ -v "/local/path/to/data/file.bin:/data/file.bin" \ -v "/local/path/to/output:/output" \ cmidair/wristpy

Customizing the Pipeline:

The Docker image supports multiple input variables to customize processing. You can set these by simply chaining these inputs as you would for the CLI input:

docker run -it --rm \
  -v "/local/path/to/data/file.bin:/data/file.bin" \
  -v "/local/path/to/output:/output" \
  cmidair/wristpy /data --output /output --epoch-length 5 --nonwear-algorithm ggir --nonwear-algorithm detach --thresholds 0.1 0.2 0.4

For more details on available options, see the orchestrator documentation.

References

  1. van Hees, V.T., Sabia, S., Jones, S.E. et al. Estimating sleep parameters using an accelerometer without sleep diary. Sci Rep 8, 12975 (2018). https://doi.org/10.1038/s41598-018-31266-z
  2. van Hees, V. T., et al. A Novel, Open Access Method to Assess Sleep Duration Using a Wrist-Worn Accelerometer. PLoS One 10, e0142533 (2015). https://doi.org/10.1371/journal.pone.0142533
  3. Hildebrand, M., et al. Age group comparability of raw accelerometer output from wrist- and hip-worn monitors. Medicine and Science in Sports and Exercise, 46(9), 1816-1824 (2014). https://doi.org/10.1249/mss.0000000000000289

Wristpy Tutorial

Introduction

Wristpy is a Python library designed for processing and analyzing wrist-worn accelerometer data. This tutorial will guide you through the basic steps of using Wristpy to analyze your accelerometer data. Specifically, we will cover the following topics through a few examples:

  • running the default processor, analyzing the output data, and visualizing the results.
  • loading data and plotting the raw signals.
  • how to calibrate the data, computing ENMO and angle-z from the calibrated data and then plotting those metrics.
  • how to obtain non-wear windows and visualize them.
  • how to obtain sleep windows and visualize them.
    • how we can filter sleep windows that overlap with non-wear periods.

Example 1: Running the default processor

The orchestrator module of wristpy contains the default processor that will run the entire wristpy processing pipeline. This can be called as simply as:

from wristpy.core import orchestrator

results = orchestrator.run(
   input = '/path/to/your/file.gt3x',
   output = 'path/to/save/file_name.csv'
)

This runs the processing pipeline with all the default arguments, creates an output .csv file, and will create a results object that contains the various output metrics (namely, enmo, angle-z, physical activity values, non-wear detection, sleep detection).

The orchestrator can also process entire directories. The call to the orchestrator remains largely the same but now output is expected to be a directory and the desired filetype for the saved files must be specified:

from wristpy.core import orchestrator

results = orchestrator.run(
    input = '/path/to/input/dir',
    output = '/path/to/output/dir',
    output_filetype = ".csv"
)

We can visualize some of the outputs within the results object, directly, with the following scripts:

Plot the ENMO across the entire data set:

from matplotlib import pyplot as plt
plt.plot(results.enmo.time, results.enmo.measurements)

Example of the ENMO result

Plot the sleep windows with normalized angle-z data:

from matplotlib import pyplot as plt

plt.plot(results.anglez.time, results.anglez.measurements/90)
plt.plot(results.sleep_windows_epoch.time, results.sleep_windows_epoch.measurements)
plt.legend(['Angle Z', 'Sleep Windows'])
plt.show()

Example of the Sleep and Anglez

We can also view and process these outputs from the saved .csv output file:

import polars as pl
import matplotlib.pyplot as plt
output_results = pl.read_csv('path/to/save/file_name.csv', try_parse_dates=True)
phys_activity = output_results['physical_activity_levels'].cast(pl.Categorical).to_physical()

plt.plot(output_results['time'], phys_activity)

Example of plotting physical activity levels from csv

It is also possible to do some analysis on these output variables, for example, if we want to find the percent of time spent inactive, or in light, moderate, or vigorous physical activity:

inactivity_count = sum(output_results['physical_activity_levels'] == 0)
light_activity_count = sum(output_results['physical_activity_levels'] == 1)
moderate_activity_count = sum(output_results['physical_activity_levels'] == 2)
vigorous_activity_count = sum(output_results['physical_activity_levels'] == 3)
total_activity_count = len(output_results['physical_activity_levels'])

print(f'Light activity percent: {light_activity_count*100/total_activity_count}')
print(f'Moderate activity percent: {moderate_activity_count*100/total_activity_count}')
print(f'Vigorous activity percent: {vigorous_activity_count*100/total_activity_count}')
print(f'Inactivity percent: {inactivity_count*100/total_activity_count}')
Light activity percent: 12.394840157038699
Moderate activity percent: 1.1030099083940923
Vigorous activity percent: 0.031158471988533682
Inactivity percent: 86.47099146257868

Example 2: Loading data and plotting the raw signals

In this example we will go over the built-in functions to directly read the raw accelerometer and light data, and how to quickly visualize this information.

The built in readers module can be used to load all the sensor and metadata from one of the support wristwatches (.gt3x or .bin), the reader will automatically select the appropriate loading methodology.

from wristpy.io.readers import readers

watch_data = readers.read_watch_data('/path/to/geneactive/file.bin')

We can then visualize the raw accelerometer and light sensor values very easily as follows:

Plot the raw acceleration along the x-axis:

plt.plot(watch_data.acceleration.time, watch_data.acceleration.measurements[:,0])

Plot raw acceleration data from watch_data

Plot the light data:

plt.plot(watch_data.lux.time, watch_data.lux.measurements)

Plot the light data

Example 3: Plot the epoch1 level measurements

In this example we will expand on the skills learned in Example 2: we will load the sensor data, calibrate, and then calculate the ENMO and angle-z data in 5s windows (epoch 1 data).

from wristpy.io.readers import readers
from wristpy.processing import calibration, metrics
from wristpy.core import computations

watch_data = readers.read_watch_data('/path/to/geneactive/file.bin')

#Calibration phase
calibrator_object = calibration.ConstrainedMinimizationCalibration()
calibrated_data = calibrator_object.run_calibration(watch_data.acceleration)

#Compute the desired metrics
enmo = metrics.euclidean_norm_minus_one(calibrated_data)
anglez = metrics.angle_relative_to_horizontal(calibrated_data)

#Obtain the epoch1 level data
enmo_epoch1 = computations.moving_mean(enmo)
anglez_epoch1 = computations.moving_mean(anglez)

We can then visualize the epoch1 measurements as:

fig, ax1 = plt.subplots()


ax1.plot(enmo_epoch1.time, enmo_epoch1.measurements, color='blue')
ax1.set_ylabel('ENMO', color='blue')

ax2 = ax1.twinx()
ax2.plot(anglez_epoch1.time, anglez_epoch1.measurements, color='red')
ax2.set_ylabel('Anglez', color='red')

plt.show()

Plot the epoch1 data

Example 4: Visualize the detected non-wear times

In this example we will build on Example 3 by also solving for the non-wear periods, as follows:

from wristpy.io.readers import readers
from wristpy.processing import calibration, metrics


watch_data = readers.read_watch_data('/path/to/geneactive/file.bin')
calibrator_object = calibration.ConstrainedMinimizationCalibration()
calibrated_data = calibrator_object.run_calibration(watch_data.acceleration)

#Find non-wear periods
non_wear_array = metrics.detect_nonwear(calibrated_data)

We can then visualize the non-wear periods, in comparison to movement (ENMO at the epoch1 level):

from wristpy.core import computations

enmo = metrics.euclidean_norm_minus_one(calibrated_data)
enmo_epoch1 = computations.moving_mean(enmo)


plt.plot(enmo_epoch1.time, enmo_epoch1.measurements)
plt.plot(non_wear_array.time, non_wear_array.measurements)

plt.legend(['ENMO Epoch1', 'Non-wear'])

Plot the nonwear periods compared to the ENMO data

Example 5: Find and filter the sleep windows

The following script will obtain the sleep window pairs (onset,wakeup):

from wristpy.io.readers import readers
from wristpy.processing import analytics, calibration, metrics


watch_data = readers.read_watch_data('/path/to/geneactive/file.bin')
calibrator_object = calibration.ConstrainedMinimizationCalibration()
calibrated_data = calibrator_object.run_calibration(watch_data.acceleration)
anglez = metrics.angle_relative_to_horizontal(calibrated_data)

sleep_detector = analytics.GgirSleepDetection(anglez)
sleep_windows = sleep_detector.run_sleep_detection()

We can then visualize the sleep periods in comparison to the angle-z data and the non-wear periods, where sleep periods are visualized by a horizontal blue line, and non-wear periods are visualized with a green trace and the angle-z data with the semi-transparent red trace:

import matplotlib.pyplot as plt

fig, ax1 = plt.subplots()

# Plot each sleep window as a horizontal line
for sw in sleep_windows:
    if sw.onset is not None and sw.wakeup is not None:
        plt.hlines(1, sw.onset, sw.wakeup, colors='blue', linestyles='solid')

plt.plot(non_wear_array.time, non_wear_array.measurements, color='green')
ax2 = ax1.twinx()
ax2.plot(anglez.time, anglez.measurements, color='red', alpha=0.5)
ax2.set_ylabel('Anglez Epoch1', color='red')

ax1.set_ylabel('Sleep Period/Non-wear')
ax1.set_ylim(0, 1.5)

plt.show()

Plot the sleep periods compared to angelz data.

The filtered sleep windows can easily be obtained with the following function. This removes all sleep periods that have any overlap with non-wear time.

filtered_sleep_windows = analytics.remove_nonwear_from_sleep(non_wear_array, sleep_windows )

And these can be visualized and compared to angle-z and the non-wear periods as previously:

import matplotlib.pyplot as plt

fig, ax1 = plt.subplots()

for sw in filtered_sleep_windows:
    if sw.onset is not None and sw.wakeup is not None:
        plt.hlines(1, sw.onset, sw.wakeup, colors='blue', linestyles='solid')

plt.plot(non_wear_array.time, non_wear_array.measurements, color='green')
ax2 = ax1.twinx()
ax2.plot(anglez.time, anglez.measurements, color='red', alpha=0.5)
ax2.set_ylabel('Anglez Epoch1', color='red')

ax1.set_ylabel('Sleep Period/Non-wear')
ax1.set_ylim(0, 1.5)

plt.show()

Plot the filtered sleep windows.

1""".. include:: ../../README.md
2.. include:: ../../docs/wristpy_tutorial.md
3"""  # noqa: D205, D415