Actilife software procedures to get vector magnitude counts: are the computations exactly the same?

actigraph
actilife
counts
vector magnitude
Exploring how vector magnitude is computed by Actilife software
Author

Pierre-Yves de Müllenheim

Published

July 1, 2022

Introduction

When we measure movement using an ActiGraph accelerometer, we can access two kinds of signals (with GT1M devices and more recent generations of devices): the raw acceleration signal, now expressed in G force units; and the activity counts signal, that describes the amount of body acceleration performed over defined epochs (e.g., 1-s epochs) (Chen & Bassett, 2005; John & Freedson, 2012).

Activity counts have been commonly used in physical activity studies to estimate time spent in different movement intensities or total amount of movement performed during the day (Bassett et al., 2015). In general, the counts of interest are related to the vertical axis or the vector magnitude (VM), VM being the norm of the movement vector defined as \(VM = \sqrt{x^2 + y^2 + z^2}\), with \(x\), \(y\), and \(z\) the counts related to each accelerometer axis for each epoch, respectively.

When using an ActiGraph accelerometer, VM can be obtained when exporting data from Actilife software to a spreadseet, as for example when exporting the whole dataset to DataTable format, or when exporting the detected wear time epochs (after wear time analysis) to a simple .csv file. Other software programs allow getting VM data, such as R software along with the PhysicalActivity R package (Choi et al., 2021). The PhysicalActivity package is well known for its implementation of a nonwear time detection algorithm (Choi et al., 2012), but this package also contains a specific function (the readActigraph() function) to read .agd files and then to compute VM.

An interesting thing with R packages is that code is open, allowing to know exactly what computations are performed when a function returns a result. For example, the readActigraph() function from the PhysicalActivity package actually uses a ceiling() function on the computed VM data. This means that from each inital VM value computed with the function, the function returns at the end the smallest integer that is not lower than the considered value (e.g., for an initial VM value of 4.33, the function will actually return the value 5). Thus, the VM data that are finally obtained from the readActigraph() function are a little transformed. When using Actilife software, it is however more complicated to know what computations are exactly performed when getting VM values. At the end of a personal analysis, I was surprised to see that VM values could be slightly different depending on the procedure used to export data to spreadsheets from Actilife software. Thus, I have performed some analyses to understand how VM values provided by Actilife software are exactly computed and to know to what extent Actilife computation procedures may differ from each other and from other software ressources, specifically the readActigraph() function from the PhysicalActivity R package.

Methods

Data used in this post were initially obtained following a personnal mesurement of physical behaviour performed using an ActiGraph wGT3X-BT device (sampling rate: 90 Hz) worn at the hip during two weeks. Once the measurement completed, data were downloaded from the device using Actilife software v6.13.4 and two .agd files were created by accumulating activity counts using either 60-s epochs or 1-s epochs, and using the normal filter for both files. Finally, I have produced four different datasets for both 60-s epochs and 1-epochs:

  • A dataset containing VM data computed in R using the VM formula on the .agd file data and a rounding function (2 digits) on the VM values (this procedure is called basic thereafter).
  • A dataset containing VM data computed in R using the readActigraph() function from the PhysicalActivity R package on the .agd file data (this procedure is called choi thereafter).
  • A dataset containing VM data computed by Actilife software when exporting data to DataTable format (this procedure is called acti_datatable thereafter).
  • A dataset containing VM data computed by Actilife software when exporting data after wear time analysis (this procedure is called acti_weartime thereafter).

Then, for all datasets containing 60-s epochs and 1-epochs, I have applied in R the wearingMarking() function from the PhysicalActivity R package to finally keep only the wear time epochs from Day 2. This allowed me to make all the datasets comparable for a given epoch length and to reduce their size for faster analysis. The 60-s epoch datasets had 1055 observations each, while the 1-s epoch datasets had 63300 observations each.

To explore potential differences in VM values depending on the software procedure that was used, I have performed the followings: scatter plots for bivariate analyses using all the VM values; bar plots to look at the differences in total VM counts; table view to look at a short sample of VM values obtained from each software procedure.

The R code written to produce data and results can be viewed when clicking on the Code buttons in the remaining part of the post.

Code
# Packages & functions ---------------------------------------------------------
## Packages
library(actigraph.sleepr)
library(PhysicalActivity)
library(dplyr)
library(readr)
library(tidyr)
library(lubridate)
library(ggplot2)
library(ggforce)
library(patchwork)
library(forcats)
library(colorblindr)
library(reactable)
library(SimDesign)

## Below a function to correctly read accelerometer data exported to 'DataTable'
## format from Actilife software. This function solves the problem that data are
## exported from Actilife with quotes for the 'DataTable' format (the function 
## removes the quotes).
read_datatable <- function(file) {
  my_data <- read_csv(file, skip = 10, quote = "")
  my_data2 <- lapply(my_data, function(x) { gsub(",", ".", x) }) %>% as.data.frame()
  my_data3 <- lapply(my_data2, function(x) { gsub("\"", "", x) }) %>% 
    as.data.frame() %>% 
    mutate(Vector.Magnitude = as.numeric(Vector.Magnitude))

  return(my_data3)
}

# Get data
## Datasets with VM obtained using the basic formula of VM (values are rounded 
## with 2 digits)
basic_60s <- 
  quiet(
    actigraph.sleepr::read_agd("data_60sec.agd") %>%
    rename(TimeStamp = timestamp) %>%
    as.data.frame() %>%
    mutate(vm = round(sqrt(axis1^2 + axis2^2 + axis3^2), 2)) %>% 
    wearingMarking(cts = "vm", frame = 90, streamFrame = 30, allowanceFrame = 2) %>% 
    filter(days == "2" & wearing == "w")
  )

basic_1s <-
 quiet(
   actigraph.sleepr::read_agd("data_1sec.agd") %>%
   rename(TimeStamp = timestamp) %>%
   as.data.frame() %>%
   mutate(vm = round(sqrt(axis1^2 + axis2^2 + axis3^2), 2)) %>% 
   wearingMarking(cts = "vm", frame = 90, streamFrame = 30, allowanceFrame = 2) %>% 
   filter(days == "2" & wearing == "w")
 )

## Datasets with VM obtained using the readActigraph() function from the 
## PhysicalActivity R package
choi_60s <-
  quiet(
    readActigraph("data_60sec.agd") %>% 
    wearingMarking(cts = "vm", frame = 90, streamFrame = 30, allowanceFrame = 2) %>% 
    filter(days == "2" & wearing == "w")
   )

choi_1s <- 
  quiet(
    readActigraph("data_1sec.agd") %>% 
    wearingMarking(cts = "vm", frame = 90, streamFrame = 30, allowanceFrame = 2) %>% 
    filter(days == "2" & wearing == "w")
   )

## Datasets with VM obtained after exporting data from Actilife to 'DataTable' 
## format
dataTable_60s <- 
 quiet(
    read_datatable("data_DataTable_60sec.csv") %>%
    unite(TimeStamp, Date, Time, sep = " ") %>%
    mutate(TimeStamp = lubridate::dmy_hms(TimeStamp),
           TimeStamp = as.character(TimeStamp)) %>%
    wearingMarking(cts = "Vector.Magnitude", frame = 90, streamFrame = 30, allowanceFrame = 2) %>% 
    filter(days == "2" & wearing == "w")
   )

dataTable_1s <- 
 quiet(
   read_datatable("data_DataTable_1sec.csv") %>%
   unite(TimeStamp, Date, Time, sep = " ") %>%
   mutate(TimeStamp = lubridate::dmy_hms(TimeStamp),
          TimeStamp = as.character(TimeStamp)) %>%
   wearingMarking(cts = "Vector.Magnitude", frame = 90, streamFrame = 30, allowanceFrame = 2) %>% 
   filter(days == "2" & wearing == "w")
   )

## Datasets with VM obtained after exporting data from Actilife following wear 
## time analysis
WearTimeAnalysis_60s <-
 quiet(
   read_csv("data_WearTimeAnalysis_60sec.csv", skip = 1) %>%
   unite(TimeStamp, date, epoch, sep = " ") %>%
   mutate(TimeStamp = lubridate::dmy_hms(TimeStamp),
          TimeStamp = as.character(TimeStamp)) %>%
   as.data.frame() %>%
   wearingMarking(cts = "vm", frame = 90, streamFrame = 30, allowanceFrame = 2) %>% 
   filter(days == "2" & wearing == "w")
   )

WearTimeAnalysis_1s <- 
 quiet(
   read_csv("data_WearTimeAnalysis_1sec.csv", skip = 1) %>%
   unite(TimeStamp, date, epoch, sep = " ") %>%
   mutate(TimeStamp = lubridate::dmy_hms(TimeStamp),
          TimeStamp = as.character(TimeStamp)) %>%
   as.data.frame() %>%
   wearingMarking(cts = "vm", frame = 90, streamFrame = 30, allowanceFrame = 2) %>% 
   filter(days == "2" & wearing == "w")
   )

Results & Discussion

Figure 1 shows the bivariate relationships between the VM values obtained from the different software procedures. The graphics placed in the diagonale of the figure from the top-left corner to the bottom-right corner show the distributions of the counts values for the corresponding software procedures without using the y-scale of the figure. Each procedure seems perfectly correlated with each other.

Code
# Scatter plots analysis
## Gather all VM variables into a single dataset
all_60s <-
  data.frame(
    basic = basic_60s$vm,
    choi = choi_60s$vm,
    acti_datatable = dataTable_60s$Vector.Magnitude,
    acti_weartime = WearTimeAnalysis_60s$vm
  )

all_1s <-
  data.frame(
    basic = basic_1s$vm,
    choi = choi_1s$vm,
    acti_datatable = dataTable_1s$Vector.Magnitude,
    acti_weartime = WearTimeAnalysis_1s$vm
  )

## Create scatter plots
g1 <- 
  ggplot(all_60s, aes(x = .panel_x, y = .panel_y)) + 
  geom_point(alpha = 0.4, shape = 16, size = 2) + 
  geom_smooth(method = "lm", se = FALSE, size = 1) + 
  geom_autodensity() +
  geom_rect(aes(
    xmin = -Inf, 
    xmax = Inf, 
    ymin = -Inf, 
    ymax = Inf), 
    fill = "grey90"
    ) +
  facet_matrix(
    vars(everything()), 
    layer.lower = c(1,2), 
    layer.diag = 3, 
    layer.upper = 4, 
    grid.y.diag = FALSE) +
  labs(title = "60-s epoch VM counts")

g2 <- 
  ggplot(all_1s, aes(x = .panel_x, y = .panel_y)) + 
  geom_point(alpha = 0.4, shape = 16, size = 2) + 
  geom_smooth(method = "lm", se = FALSE, size = 1) + 
  geom_autodensity() +
  geom_rect(aes(
    xmin = -Inf, 
    xmax = Inf, 
    ymin = -Inf, 
    ymax = Inf), 
    fill = "grey90"
    ) +
  facet_matrix(
    vars(everything()), 
    layer.lower = c(1,2), 
    layer.diag = 3, 
    layer.upper = 4, 
    grid.y.diag = FALSE) +
  labs(title = "\n1-s epoch VM counts")
Figure 1: Relationships between the VM values obtained from the different software procedures

Figure 2 (top panel) shows that total VM counts are different between the tested software procedures. Indeed, total VM counts ranged from 415,312 counts (acti_weartime procedure) to 415,600 counts (choi procedure) for the 60-s epoch datasets, and from 461,795 counts (acti_weartime procedure) to 466,017 counts (choi procedure) for the 1-s epoch datasets. Figure 2 (bottom panel) also shows that the differences between the procedures for total VM counts were higher when using 1-s epochs in comparison with the use of 60-s epochs. This last result may be put into relation with the fact that there were more observations when using 1-epochs compared to 60-s epochs, thus letting more room for total differences. Of note, the basic procedure and the acti_datatable procedure led to the same results, suggesting that Actilife software uses the VM formula and then a rounding procedure (2 digits) to provide VM values when exporting to DataTable-format spreadsheets.

Code
# Bar plots analysis
## Build datasets with total VM counts per day and differences between procedures
## with the 'acti_weartime' procedure as reference
sum_60s <-
  all_60s %>%
  pivot_longer(cols= c(basic:acti_weartime), names_to = "factor", values_to = "vm") %>%
  group_by(factor) %>%
  summarise(total_counts = sum(vm)) %>%
  mutate(
    epoch = "60-s epoch",
    diff = total_counts - min(.$total_counts)
  )

sum_1s <-
  all_1s %>%
  pivot_longer(cols= c(basic:acti_weartime), names_to = "factor", values_to = "vm") %>%
  group_by(factor) %>%
  summarise(total_counts = sum(vm)) %>%
  mutate(
    epoch = "1-s epoch",
    diff = total_counts - min(.$total_counts)
  )

all_data <-
  bind_rows(sum_60s, sum_1s) %>%
  mutate(epoch = fct_relevel(as.factor(epoch), "60-s epoch", "1-s epoch"))

## Create bar plots
g3 <- 
  ggplot(data = all_data, aes(x = fct_reorder(factor, total_counts), y = total_counts, fill = factor)) +
  geom_bar(stat  = "identity") +
  geom_text(aes(label = format(round(total_counts, 2), big.mark = ",", scientific = FALSE)), nudge_y = 20000, size = 7) +
  labs(
    title = "Total VM counts",
    fill = "Computation method",
    y = "Total VM counts"
  ) +
  facet_wrap(.~ epoch) +
  scale_fill_OkabeIto() +
  theme(
    axis.title = element_blank(),
    axis.text.y = element_blank(),
    axis.ticks.y = element_blank(),
    legend.title = element_text(face = "bold")
  )

g4 <-
  ggplot(data = all_data, aes(x = fct_reorder(factor, diff), y = diff, fill = factor)) +
  geom_bar(stat  = "identity") +
  geom_text(aes(label = ifelse(factor == "acti_weartime", "Reference",
                               round(diff, 2))), nudge_y = 150, size = 7) +
  labs(
    title = "\nDifferences in total VM counts from the acti_weartime procedure",
    fill = "Computation method",
    y = "Diff. VM counts"
  ) +
  facet_wrap(.~ epoch) +
  scale_fill_OkabeIto() +
  theme(
    axis.title = element_blank(),
    axis.text.y = element_blank(),
    axis.ticks.y = element_blank(),
    legend.title = element_text(face = "bold")
  ) 
Figure 2: Comparison of total VM counts between the different software procedures

Now, it remains to investigate what computations may be related to the acti_weartime procedure and why there were such differences between the various computation procedures. To do this, let’s take a look at a sample of VM values from the different datasets with 60-s epochs (Figure 3).

Code
table <- (all_60s %>% select(acti_weartime, acti_datatable, basic, choi)) [21:30, ]
orange_pal <- function(x) rgb(colorRamp(c("#ffe4cc", "#ff9500"))(x), maxColorValue = 255)
reactable(table,
      defaultColDef = colDef(align = "center", width = 180),
      columns = list(
        acti_weartime = colDef(style = function() list(background = "#56B4E9")),
        acti_datatable = colDef(style = function() list(background = "#E69F00")),
        basic = colDef(style = function() list(background = "#009E73")),
        choi = colDef(style = function() list(background = "#F0E442"))
       )
)
Figure 3: Sample of VM values from the 60-s epoch datasets obtained with the different software procedures

From Figure 3, we can guess that Actilife software actually uses two different rounding methods when computing VM values with the acti_weartime procedure, these methods depending on the decimal part of the initial VM value. Assuming the values from the basic procedure can be used as reference to provide an idea of what should be initial VM values, we can describe these two methods as follows:

  • Method 1: When the decimal part of the initial VM value is >= 0.5, the value of the integer directly above the initial value of VM is returned;
  • Method 2: When the decimal part of the initial VM value is < 0.5, the value of the integer directly below the initial value of VM is returned.

Thus, we can understand why the choi procedure provides total VM counts that are clearly larger than those from the other procedures: while the basic and acti_datatable procedures will keep the decimal part of the initial VM values, the choi procedure will always return values larger than the initial values of VM if the decimal part is above 0, while the acti_weartime procedure will return either larger VM values or smaller VM values than the initial VM values depending on if the decimal part of the initial VM value is >=0.5 or not, respectively.

Conclusion

In this post, we have seen that two procedures related to Actilife software to export VM data provide different values from each other and also differ from the readActigraph() function retrieved from the PhysicalActivity R package:

  • The acti_datatable procedure (DataTable format from Actilife software) only rounds (2 digits) the VM values obtained using the basic VM formula.
  • The acti_weartime procedure (data obtained after wear time analysis from Actilife software) returns the integer directly above the initial VM value when the decimal part of the initial VM value is >= 0.5, otherwise it returns the integer directly below the initial VM value.
  • The choi procedure always returns the integer directly above the initial VM value when the decimal part of the initial VM value is >0.

While the two tested Actilife procedures provide quite similar total VM counts values, the readActigraph() function seems to provide clearly larger total VM counts values at the end of the day. However, one could still consider such a difference as trivial. Indeed, the absolute difference of 4222 counts between the choi procedure and the acti_weartime procedure, when using 1-s epochs, represents only 0.9% of the total VM counts related to the acti_weartime procedure over the whole day. We could have the same perspective on the differences observed between total VM counts from the 60-s epoch datasets and 1-s epoch datasets. These differences are clear, but could be considered as trivial compared to the expected absolute value of total VM counts over a whole day. That’s being said, if there was no rationale to create such different procedures, it could be good thing to make all of these procedures similar from a computational point of view, for example by keeping the simple rounding procedure of the VM values to not distort the inital data.

References

Bassett, D. R., Troiano, R. P., Mcclain, J. J., & Wolff, D. L. (2015). Accelerometer-based physical activity: Total volume per day and standardized measures. Med Sci Sports Exerc, 47(4), 833–838. https://doi.org/10.1249/MSS.0000000000000468
Chen, K. Y., & Bassett, D. R. (2005). The technology of accelerometry-based activity monitors: Current and future. Med Sci Sports Exerc, 37(11), S490–S500. https://doi.org/10.1249/01.mss.0000185571.49104.82
Choi, L., Beck, C., Liu, Z., Moore, R., Matthews, C. E., & Buchowski, M. S. (2021). PhysicalActivity: Process accelerometer data for physical activity measurement (Version 0.2-4). https://CRAN.R-project.org/package=PhysicalActivity
Choi, L., Ward, S. C., Schnelle, J. F., & Buchowski, M. S. (2012). Assessment of wear/nonwear time classification algorithms for triaxial accelerometer. Med Sci Sports Exerc, 44(10), 2009–2016. https://doi.org/10.1249/MSS.0b013e318258cb36
John, D., & Freedson, P. (2012). ActiGraph and Actical physical activity monitors: A peek under the hood. Med Sci Sports Exerc, 44, S86–S89. https://doi.org/10.1249/MSS.0b013e3182399f5e

Share