├── .github
├── .gitignore
├── FUNDING.yml
└── workflows
│ ├── pkgdown.yaml
│ ├── R-CMD-check.yaml
│ └── pr-commands.yaml
├── vignettes
├── .gitignore
├── vo2_kinetics.Rmd
└── incremental.Rmd
├── LICENSE
├── tests
├── testthat.R
└── testthat
│ ├── test-mrt.R
│ ├── test-max.R
│ ├── test-read_data.R
│ ├── test-interpolate.R
│ ├── test-detect_outliers.R
│ ├── test-perform_average.R
│ ├── test-process_data.R
│ ├── test-vo2_kinetics.R
│ ├── test-perform_kinetics.R
│ └── test-incremental.R
├── man
├── figures
│ ├── logo.png
│ └── header.png
├── pipe.Rd
├── print.whippr.Rd
├── plot_outliers.Rd
├── model_diagnostics.Rd
├── plot_incremental.Rd
├── new_whippr_tibble.Rd
├── theme_whippr.Rd
├── normalize_time.Rd
├── work_rate_ramp.Rd
├── normalize_first_breath.Rd
├── get_residuals.Rd
├── interpolate.Rd
├── normalize_transitions.Rd
├── whippr-package.Rd
├── remove_empty.Rd
├── outliers_linear.Rd
├── outliers_anomaly.Rd
├── process_data.Rd
├── run_manual_cleaner.Rd
├── work_rate_step.Rd
├── read_data.Rd
├── predict_bands.Rd
├── predict_bands_transition.Rd
├── predict_bands_baseline.Rd
├── undoHistory.Rd
├── perform_average.Rd
├── perform_kinetics.Rd
├── perform_max.Rd
├── incremental_normalize.Rd
├── detect_outliers.Rd
├── vo2_max.Rd
└── vo2_kinetics.Rd
├── inst
├── ramp_cosmed.xlsx
├── step_cortex.xlsx
├── example_cosmed.xlsx
└── shinyThings
│ └── undoHistory.js
├── pkgdown
├── favicon
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── apple-touch-icon-60x60.png
│ ├── apple-touch-icon-76x76.png
│ ├── apple-touch-icon-120x120.png
│ ├── apple-touch-icon-152x152.png
│ └── apple-touch-icon-180x180.png
└── extra.css
├── .Rbuildignore
├── R
├── utils-pipe.R
├── whippr-package.R
├── plot-theme.R
├── globals.R
├── interpolate.R
├── helpers-read.R
├── utils.R
├── nlstools.R
├── normalize.R
├── helpers-incremental.R
├── averages.R
├── helpers-outliers.R
├── tbl.R
├── addin.R
├── shinyThings.R
├── predict.R
├── max.R
└── incremental.R
├── codecov.yml
├── cran-comments.md
├── whippr.Rproj
├── LICENSE.md
├── DESCRIPTION
├── .gitignore
├── NAMESPACE
├── _pkgdown.yml
├── NEWS.md
├── README.Rmd
└── README.md
/.github/.gitignore:
--------------------------------------------------------------------------------
1 | *.html
2 |
--------------------------------------------------------------------------------
/vignettes/.gitignore:
--------------------------------------------------------------------------------
1 | *.html
2 | *.R
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | YEAR: 2020
2 | COPYRIGHT HOLDER: Felipe Mattioni Maturana
3 |
--------------------------------------------------------------------------------
/tests/testthat.R:
--------------------------------------------------------------------------------
1 | library(testthat)
2 | library(whippr)
3 |
4 | test_check("whippr")
5 |
--------------------------------------------------------------------------------
/man/figures/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/man/figures/logo.png
--------------------------------------------------------------------------------
/inst/ramp_cosmed.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/inst/ramp_cosmed.xlsx
--------------------------------------------------------------------------------
/inst/step_cortex.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/inst/step_cortex.xlsx
--------------------------------------------------------------------------------
/man/figures/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/man/figures/header.png
--------------------------------------------------------------------------------
/tests/testthat/test-mrt.R:
--------------------------------------------------------------------------------
1 | test_that("MRT linear works", {
2 | expect_equal(2 * 2, 4)
3 | })
4 |
--------------------------------------------------------------------------------
/inst/example_cosmed.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/inst/example_cosmed.xlsx
--------------------------------------------------------------------------------
/tests/testthat/test-max.R:
--------------------------------------------------------------------------------
1 | test_that("multiplication works", {
2 | expect_equal(2 * 2, 4)
3 | })
4 |
--------------------------------------------------------------------------------
/pkgdown/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/favicon.ico
--------------------------------------------------------------------------------
/pkgdown/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/pkgdown/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/pkgdown/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/pkgdown/favicon/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/pkgdown/favicon/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/pkgdown/favicon/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/pkgdown/favicon/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/pkgdown/favicon/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fmmattioni/whippr/HEAD/pkgdown/favicon/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^whippr\.Rproj$
2 | ^\.Rproj\.user$
3 | ^LICENSE\.md$
4 | ^README\.Rmd$
5 | ^_pkgdown\.yml$
6 | ^docs$
7 | ^pkgdown$
8 | ^\.github$
9 | ^vignettes$
10 | ^codecov\.yml$
11 | ^CRAN-SUBMISSION$
12 | ^cran-comments\.md$
13 |
--------------------------------------------------------------------------------
/R/utils-pipe.R:
--------------------------------------------------------------------------------
1 | #' Pipe operator
2 | #'
3 | #' See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details.
4 | #'
5 | #' @name %>%
6 | #' @rdname pipe
7 | #' @keywords internal
8 | #' @export
9 | #' @importFrom magrittr %>%
10 | #' @usage lhs \%>\% rhs
11 | NULL
12 |
--------------------------------------------------------------------------------
/R/whippr-package.R:
--------------------------------------------------------------------------------
1 | #' @keywords internal
2 | "_PACKAGE"
3 |
4 | # The following block is used by usethis to automatically manage
5 | # roxygen namespace tags. Modify with care!
6 | ## usethis namespace: start
7 | #' @importFrom tibble tibble
8 | ## usethis namespace: end
9 | NULL
10 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
2 |
3 | coverage:
4 | status:
5 | project:
6 | default:
7 | target: auto
8 | threshold: 1%
9 | informational: true
10 | patch:
11 | default:
12 | target: auto
13 | threshold: 1%
14 | informational: true
15 |
--------------------------------------------------------------------------------
/tests/testthat/test-read_data.R:
--------------------------------------------------------------------------------
1 | test_that("read data works", {
2 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
3 |
4 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
5 |
6 | expect_s3_class(
7 | object = df,
8 | class = "whippr"
9 | )
10 | })
11 |
--------------------------------------------------------------------------------
/man/pipe.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/utils-pipe.R
3 | \name{\%>\%}
4 | \alias{\%>\%}
5 | \title{Pipe operator}
6 | \usage{
7 | lhs \%>\% rhs
8 | }
9 | \description{
10 | See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details.
11 | }
12 | \keyword{internal}
13 |
--------------------------------------------------------------------------------
/tests/testthat/test-interpolate.R:
--------------------------------------------------------------------------------
1 | test_that("interpolation works", {
2 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
3 |
4 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
5 |
6 | df_interpolated <- df %>%
7 | interpolate()
8 |
9 | expect_s3_class(
10 | object = df_interpolated,
11 | class = "whippr"
12 | )
13 | })
14 |
--------------------------------------------------------------------------------
/man/print.whippr.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/tbl.R
3 | \name{print.whippr}
4 | \alias{print.whippr}
5 | \title{Whippr print method}
6 | \usage{
7 | \method{print}{whippr}(x, ...)
8 | }
9 | \arguments{
10 | \item{x}{A tibble with class 'whippr'}
11 |
12 | \item{...}{Extra arguments, not used.}
13 | }
14 | \description{
15 | Whippr print method
16 | }
17 |
--------------------------------------------------------------------------------
/man/plot_outliers.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/outliers.R
3 | \name{plot_outliers}
4 | \alias{plot_outliers}
5 | \title{Plot outliers}
6 | \usage{
7 | plot_outliers(.data)
8 | }
9 | \arguments{
10 | \item{.data}{The data retrieved from \code{detect_outliers()}.}
11 | }
12 | \value{
13 | a patchwork object
14 | }
15 | \description{
16 | Plot outliers
17 | }
18 |
--------------------------------------------------------------------------------
/man/model_diagnostics.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/nlstools.R
3 | \name{model_diagnostics}
4 | \alias{model_diagnostics}
5 | \title{Model diagnostics}
6 | \usage{
7 | model_diagnostics(.residuals_tbl)
8 | }
9 | \arguments{
10 | \item{.residuals_tbl}{The data retrived from \code{get_residuals()}.}
11 | }
12 | \value{
13 | a patchwork object
14 | }
15 | \description{
16 | Plots different model diagnostics for checking the model performance.
17 | }
18 |
--------------------------------------------------------------------------------
/man/plot_incremental.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/incremental.R
3 | \name{plot_incremental}
4 | \alias{plot_incremental}
5 | \title{Plot incremental test work rate}
6 | \usage{
7 | plot_incremental(.data)
8 | }
9 | \arguments{
10 | \item{.data}{data retrieved from \code{incremental_normalize()}.}
11 | }
12 | \value{
13 | a ggplot object
14 | }
15 | \description{
16 | Visualize what was done during the process of deriving the work rate from the incremental test protocol
17 | }
18 |
--------------------------------------------------------------------------------
/man/new_whippr_tibble.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/tbl.R
3 | \name{new_whippr_tibble}
4 | \alias{new_whippr_tibble}
5 | \title{Construct a new tibble with metadata}
6 | \usage{
7 | new_whippr_tibble(.data, metadata)
8 | }
9 | \arguments{
10 | \item{.data}{A data frame}
11 |
12 | \item{metadata}{Metadata to be passed along with the data}
13 | }
14 | \value{
15 | a \link[tibble:tibble-package]{tibble}
16 | }
17 | \description{
18 | Construct a new tibble with metadata
19 | }
20 | \keyword{internal}
21 |
--------------------------------------------------------------------------------
/cran-comments.md:
--------------------------------------------------------------------------------
1 | ## R CMD check results
2 |
3 | 0 errors | 0 warnings | 1 note
4 |
5 | * This is a new release.
6 |
7 | ## Comments from last submission
8 |
9 | Found the following (possibly) invalid URLs:
10 | URL: https://korr.com/go/cardiocoach (moved to
11 | https://korr.com/go/cardiocoach/)
12 | From: README.md
13 | Status: 301
14 | Message: Moved Permanently
15 |
16 | Please change http --> https, add trailing slashes, or follow moved
17 | content as appropriate.
18 |
19 | > All of the above has been corrected accordingly.
20 |
--------------------------------------------------------------------------------
/man/theme_whippr.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/plot-theme.R
3 | \name{theme_whippr}
4 | \alias{theme_whippr}
5 | \title{Whippr ggplot2 theme}
6 | \usage{
7 | theme_whippr(base_size = 14, base_family = "sans")
8 | }
9 | \arguments{
10 | \item{base_size}{base font size, given in pts. Default is \code{14}.}
11 |
12 | \item{base_family}{base font family. Default is \code{sans}.}
13 | }
14 | \value{
15 | a ggplot2 object
16 | }
17 | \description{
18 | This theme was inspired by the plots from the Acta Physiologica Journal
19 | }
20 |
--------------------------------------------------------------------------------
/inst/shinyThings/undoHistory.js:
--------------------------------------------------------------------------------
1 | function toggleHistoryButtonState (state) {
2 | if (state.enable) {
3 | for (var i = 0; i < state.enable.length; i++) {
4 | var btn_e = $('#' + state.enable[i]);
5 | btn_e.prop('disabled', false).removeClass('disabled');
6 | }
7 | }
8 | if (state.disable) {
9 | for (var j = 0; j < state.disable.length; j++) {
10 | var btn_d = $('#' + state.disable[j]);
11 | btn_d.prop('disabled', true).addClass('disabled');
12 | }
13 | }
14 | }
15 |
16 | Shiny.addCustomMessageHandler('undoHistoryButtons', toggleHistoryButtonState);
17 |
--------------------------------------------------------------------------------
/man/normalize_time.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/normalize.R
3 | \name{normalize_time}
4 | \alias{normalize_time}
5 | \title{Normalize time column}
6 | \usage{
7 | normalize_time(.data, protocol_baseline_length)
8 | }
9 | \arguments{
10 | \item{.data}{Breath-by-breath data.}
11 |
12 | \item{protocol_baseline_length}{The length of the baseline (in seconds).}
13 | }
14 | \value{
15 | a \link[tibble:tibble-package]{tibble}
16 | }
17 | \description{
18 | Normalizes the the time column such that the baseline phase has negative time values. Point zero will then represent the start of the transition phase.
19 | }
20 |
--------------------------------------------------------------------------------
/pkgdown/extra.css:
--------------------------------------------------------------------------------
1 | /*
2 | Make only first letter upper case in TOC
3 | https://github.com/r-lib/pkgdown/issues/1302
4 | */
5 | nav[data-toggle='toc'] .nav > li > a {
6 | text-transform: none !important;
7 | }
8 |
9 | .rmdinfo {
10 | padding: 2em 1em 1em 7em;
11 | margin-top: 20px;
12 | margin-bottom: 20px;
13 | border-radius: 25px;
14 | background-color: #e6f0ff;
15 | position:relative;
16 | }
17 |
18 | .rmdinfo:before {
19 | font-family: "Font Awesome 5 Free";
20 | font-weight: 900;
21 | content: "\f129";
22 | top: 50%;
23 | transform: translateY(-50%);
24 | left: 30px;
25 | position:absolute;
26 | font-size: 45px;
27 | }
28 |
--------------------------------------------------------------------------------
/man/work_rate_ramp.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/helpers-incremental.R
3 | \name{work_rate_ramp}
4 | \alias{work_rate_ramp}
5 | \title{Work rate for a ramp-incremental test}
6 | \usage{
7 | work_rate_ramp(.data, baseline_intensity, ramp_increase)
8 | }
9 | \arguments{
10 | \item{.data}{The data with recognized protocol phases}
11 |
12 | \item{baseline_intensity}{The baseline intensity}
13 |
14 | \item{ramp_increase}{The ramp increase, in watts per minute}
15 | }
16 | \value{
17 | a \link[tibble:tibble-package]{tibble}
18 | }
19 | \description{
20 | This function produces the work rate throughout a ramp-incremental test given the procotol
21 | }
22 | \keyword{internal}
23 |
--------------------------------------------------------------------------------
/whippr.Rproj:
--------------------------------------------------------------------------------
1 | Version: 1.0
2 |
3 | RestoreWorkspace: No
4 | SaveWorkspace: No
5 | AlwaysSaveHistory: Default
6 |
7 | EnableCodeIndexing: Yes
8 | UseSpacesForTab: Yes
9 | NumSpacesForTab: 2
10 | Encoding: UTF-8
11 |
12 | RnwWeave: knitr
13 | LaTeX: XeLaTeX
14 |
15 | AutoAppendNewline: Yes
16 | StripTrailingWhitespace: Yes
17 |
18 | BuildType: Package
19 | PackageUseDevtools: Yes
20 | PackageInstallArgs: --no-multiarch --with-keep.source --no-build-vignettes
21 | PackageBuildArgs: --no-build-vignettes --ignore-vignettes
22 | PackageBuildBinaryArgs: --no-build-vignettes --ignore-vignettes
23 | PackageCheckArgs: --no-build-vignettes --ignore-vignettes
24 | PackageRoxygenize: rd,collate,namespace
25 |
26 | UseNativePipeOperator: No
27 |
--------------------------------------------------------------------------------
/tests/testthat/test-detect_outliers.R:
--------------------------------------------------------------------------------
1 | test_that("detection of outliers works", {
2 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
3 |
4 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
5 |
6 | ## detect outliers
7 | data_outliers <- detect_outliers(
8 | .data = df,
9 | test_type = "kinetics",
10 | vo2_column = "VO2",
11 | cleaning_level = 0.95,
12 | cleaning_baseline_fit = c("linear", "exponential", "exponential"),
13 | protocol_n_transitions = 3,
14 | protocol_baseline_length = 360,
15 | protocol_transition_length = 360,
16 | verbose = FALSE
17 | )
18 |
19 | expect_s3_class(
20 | object = data_outliers,
21 | class = "whippr"
22 | )
23 | })
24 |
--------------------------------------------------------------------------------
/man/normalize_first_breath.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/normalize.R
3 | \name{normalize_first_breath}
4 | \alias{normalize_first_breath}
5 | \title{Normalize first breath}
6 | \usage{
7 | normalize_first_breath(.data)
8 | }
9 | \arguments{
10 | \item{.data}{Breath-by-breath data.}
11 | }
12 | \value{
13 | a \link[tibble:tibble-package]{tibble}
14 | }
15 | \description{
16 | This is needed specially when the data gets filtered. For example, if the data file does not only contain
17 | the baseline and transitions performed, we will have to normalize the time column.
18 | This function will make sure that in case the first breath does not start at zero, it will create a zero data point,
19 | duplicating the first breath. This will make sure the data does not get shifted (misalignment).
20 | }
21 |
--------------------------------------------------------------------------------
/tests/testthat/test-perform_average.R:
--------------------------------------------------------------------------------
1 | test_that("bin-averaging works", {
2 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
3 |
4 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
5 |
6 | df_averaged <- df %>%
7 | interpolate() %>%
8 | perform_average(type = "bin", bins = 30)
9 |
10 | expect_s3_class(
11 | object = df_averaged,
12 | class = "whippr"
13 | )
14 | })
15 |
16 | test_that("rolling-averaging works", {
17 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
18 |
19 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
20 |
21 | df_averaged <- df %>%
22 | interpolate() %>%
23 | perform_average(type = "rolling", rolling_window = 30)
24 |
25 | expect_s3_class(
26 | object = df_averaged,
27 | class = "whippr"
28 | )
29 | })
30 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: fmmattioni # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: ['https://www.buymeacoffee.com/XQauwUWGm', 'https://paypal.me/fmmattioni?locale.x=en_US'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/man/get_residuals.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/nlstools.R
3 | \name{get_residuals}
4 | \alias{get_residuals}
5 | \title{Get residuals}
6 | \usage{
7 | get_residuals(.model)
8 | }
9 | \arguments{
10 | \item{.model}{A model of class \code{nls}.}
11 | }
12 | \value{
13 | a \link[tibble:tibble-package]{tibble} containing the data passed to augment, and additional columns:
14 | \item{.fitted}{The predicted response for that observation.}
15 | \item{.resid}{The residual for a particular point.}
16 | \item{standardized_residuals}{Standardized residuals.}
17 | \item{sqrt_abs_standardized_residuals}{The sqrt of absolute value of standardized residuals.}
18 | \item{lag_residuals}{The lag of the \code{.resid} column for plotting auto-correlation.}
19 | }
20 | \description{
21 | Computes residuals from the VO2 kinetics model.
22 | }
23 |
--------------------------------------------------------------------------------
/man/interpolate.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/interpolate.R
3 | \name{interpolate}
4 | \alias{interpolate}
5 | \title{Interpolate data from breath-by-breath into second-by-second}
6 | \usage{
7 | interpolate(.data)
8 | }
9 | \arguments{
10 | \item{.data}{Data retrieved from \code{read_data()}.}
11 | }
12 | \value{
13 | a \link[tibble:tibble-package]{tibble}
14 | }
15 | \description{
16 | This function interpolates the data based on the time column. It takes the breath-by-breath data
17 | and transforms it into second-by-second.
18 | }
19 | \examples{
20 | \dontrun{
21 | ## get file path from example data
22 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
23 |
24 | ## read data
25 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
26 |
27 | df \%>\%
28 | interpolate()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/man/normalize_transitions.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/normalize.R
3 | \name{normalize_transitions}
4 | \alias{normalize_transitions}
5 | \title{Normalize transitions}
6 | \usage{
7 | normalize_transitions(
8 | .data,
9 | protocol_n_transitions,
10 | protocol_baseline_length,
11 | protocol_transition_length
12 | )
13 | }
14 | \arguments{
15 | \item{.data}{Breath-by-breath data.}
16 |
17 | \item{protocol_n_transitions}{Number of transitions performed.}
18 |
19 | \item{protocol_baseline_length}{The length of the baseline (in seconds).}
20 |
21 | \item{protocol_transition_length}{The length of the transition (in seconds).}
22 | }
23 | \value{
24 | a \link[tibble:tibble-package]{tibble}
25 | }
26 | \description{
27 | Recognizes and normalizes the time column of each transition. It will also label the transitions into: 'baseline' or 'transition'.
28 | }
29 |
--------------------------------------------------------------------------------
/tests/testthat/test-process_data.R:
--------------------------------------------------------------------------------
1 | test_that("process data works", {
2 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
3 |
4 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
5 |
6 | ## detect outliers
7 | data_outliers <- detect_outliers(
8 | .data = df,
9 | test_type = "kinetics",
10 | vo2_column = "VO2",
11 | cleaning_level = 0.95,
12 | cleaning_baseline_fit = c("linear", "exponential", "exponential"),
13 | protocol_n_transitions = 3,
14 | protocol_baseline_length = 360,
15 | protocol_transition_length = 360,
16 | verbose = FALSE
17 | )
18 |
19 | ## process data
20 | data_processed <- process_data(
21 | .data_outliers = data_outliers,
22 | protocol_baseline_length = 360,
23 | fit_bin_average = 5
24 | )
25 |
26 | expect_s3_class(
27 | object = data_processed,
28 | class = "whippr"
29 | )
30 | })
31 |
--------------------------------------------------------------------------------
/man/whippr-package.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/whippr-package.R
3 | \docType{package}
4 | \name{whippr-package}
5 | \alias{whippr}
6 | \alias{whippr-package}
7 | \title{whippr: Tools for Manipulating Gas Exchange Data}
8 | \description{
9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}}
10 |
11 | Set of tools for manipulating gas exchange data from cardiopulmonary exercise testing.
12 | }
13 | \seealso{
14 | Useful links:
15 | \itemize{
16 | \item \url{https://fmmattioni.github.io/whippr/}
17 | \item \url{https://github.com/fmmattioni/whippr}
18 | \item Report bugs at \url{https://github.com/fmmattioni/whippr/issues}
19 | }
20 |
21 | }
22 | \author{
23 | \strong{Maintainer}: Felipe Mattioni Maturana \email{felipe.mattioni@med.uni-tuebingen.de} (\href{https://orcid.org/0000-0002-4221-6104}{ORCID})
24 |
25 | }
26 | \keyword{internal}
27 |
--------------------------------------------------------------------------------
/man/remove_empty.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/utils.R
3 | \name{remove_empty}
4 | \alias{remove_empty}
5 | \title{Remove empty rows and/or columns from a data.frame or matrix.}
6 | \usage{
7 | remove_empty(dat, which = c("rows", "cols"), cutoff = 1)
8 | }
9 | \arguments{
10 | \item{dat}{the input data.frame or matrix.}
11 |
12 | \item{which}{one of "rows", "cols", or \code{c("rows", "cols")}. Where no
13 | value of which is provided, defaults to removing both empty rows and empty
14 | columns, declaring the behavior with a printed message.}
15 |
16 | \item{cutoff}{What fraction (>0 to <=1) of rows or columns must be empty to
17 | be removed?}
18 | }
19 | \value{
20 | Returns the object without its missing rows or columns.
21 | }
22 | \description{
23 | Removes all rows and/or columns from a data.frame or matrix that
24 | are composed entirely of \code{NA} values.
25 | }
26 | \keyword{internal}
27 |
--------------------------------------------------------------------------------
/tests/testthat/test-vo2_kinetics.R:
--------------------------------------------------------------------------------
1 | test_that("general vo2 kinetics function works", {
2 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
3 |
4 | ## read data
5 | df <- read_data(path = path_example, metabolic_cart = "cosmed", time_column = "t")
6 |
7 | ## VO2 kinetics analysis
8 | results_kinetics <- vo2_kinetics(
9 | .data = df,
10 | intensity_domain = "moderate",
11 | vo2_column = "VO2",
12 | protocol_n_transitions = 3,
13 | protocol_baseline_length = 360,
14 | protocol_transition_length = 360,
15 | cleaning_level = 0.95,
16 | cleaning_baseline_fit = c("linear", "exponential", "exponential"),
17 | fit_level = 0.95,
18 | fit_bin_average = 5,
19 | fit_phase_1_length = 20,
20 | fit_baseline_length = 120,
21 | fit_transition_length = 240,
22 | verbose = TRUE
23 | )
24 |
25 | expect_s3_class(
26 | object = results_kinetics,
27 | class = "tbl"
28 | )
29 | })
30 |
--------------------------------------------------------------------------------
/R/plot-theme.R:
--------------------------------------------------------------------------------
1 | #' Whippr ggplot2 theme
2 | #'
3 | #' This theme was inspired by the plots from the Acta Physiologica Journal
4 | #'
5 | #' @param base_size base font size, given in pts. Default is `14`.
6 | #' @param base_family base font family. Default is `sans`.
7 | #'
8 | #' @return a ggplot2 object
9 | #' @export
10 | #' @importFrom ggplot2 theme_light theme element_rect element_line element_text
11 | theme_whippr <- function(base_size = 14, base_family = "sans") {
12 | theme_light(base_size = base_size, base_family = base_family) +
13 | theme(
14 | panel.background = element_rect(fill = "#fefeda"),
15 | axis.line = element_line(color = "black"),
16 | axis.ticks = element_line(color = "black", linewidth = 1),
17 | axis.text = element_text(color = "black", face = "bold"),
18 | axis.title = element_text(face = "bold"),
19 | plot.title = element_text(face = "bold"),
20 | panel.border = element_rect(colour = NA)
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/man/outliers_linear.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/helpers-outliers.R
3 | \name{outliers_linear}
4 | \alias{outliers_linear}
5 | \title{Linear method for detecting outliers from an incremental test}
6 | \usage{
7 | outliers_linear(.data, time_column, vo2_column, cleaning_level)
8 | }
9 | \arguments{
10 | \item{.data}{The data retrieved from \code{incremental_normalize()}.}
11 |
12 | \item{time_column}{The name (quoted) of the column containing the time. Depending on the language of your system, this column might not be "t". Therefore, you may specify it here.}
13 |
14 | \item{vo2_column}{The name (quoted) of the column containing the absolute oxygen uptake (VO2) data.}
15 |
16 | \item{cleaning_level}{A numeric scalar between 0 and 1 giving the confidence level for the intervals to be calculated.}
17 | }
18 | \value{
19 | a \link[tibble:tibble-package]{tibble}
20 | }
21 | \description{
22 | Function for internal use only. It will not be exported.
23 | }
24 | \keyword{internal}
25 |
--------------------------------------------------------------------------------
/man/outliers_anomaly.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/helpers-outliers.R
3 | \name{outliers_anomaly}
4 | \alias{outliers_anomaly}
5 | \title{Anomaly method for detecting outliers from an incremental test}
6 | \usage{
7 | outliers_anomaly(.data, time_column, vo2_column, cleaning_level)
8 | }
9 | \arguments{
10 | \item{.data}{The data retrieved from \code{incremental_normalize()}.}
11 |
12 | \item{time_column}{The name (quoted) of the column containing the time. Depending on the language of your system, this column might not be "t". Therefore, you may specify it here.}
13 |
14 | \item{vo2_column}{The name (quoted) of the column containing the absolute oxygen uptake (VO2) data.}
15 |
16 | \item{cleaning_level}{A numeric scalar between 0 and 1 giving the confidence level for the intervals to be calculated.}
17 | }
18 | \value{
19 | a \link[tibble:tibble-package]{tibble}
20 | }
21 | \description{
22 | Function for internal use only. It will not be exported.
23 | }
24 | \keyword{internal}
25 |
--------------------------------------------------------------------------------
/man/process_data.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/kinetics.R
3 | \name{process_data}
4 | \alias{process_data}
5 | \title{Process data for VO2 kinetics fitting}
6 | \usage{
7 | process_data(.data_outliers, protocol_baseline_length, fit_bin_average)
8 | }
9 | \arguments{
10 | \item{.data_outliers}{The data retrived from \code{detect_outliers()}.}
11 |
12 | \item{protocol_baseline_length}{The length of the baseline (in seconds).}
13 |
14 | \item{fit_bin_average}{The bin average to be performed for the final fit.}
15 | }
16 | \value{
17 | a \link[tibble:tibble-package]{tibble} with the time-aligned, ensembled-averaged, and bin-averaged data.
18 | }
19 | \description{
20 | It removes the outliers detected through \code{detect_outliers()}, interpolates each transition,
21 | ensemble-averages all the transitions into one, performs a bin-average, and normalizes the time column
22 | (time zero will indicate the end of baseline and the start of the transition phase).
23 | }
24 | \details{
25 | TODO
26 | }
27 |
--------------------------------------------------------------------------------
/man/run_manual_cleaner.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/addin.R
3 | \name{run_manual_cleaner}
4 | \alias{run_manual_cleaner}
5 | \title{Manual data cleaner}
6 | \usage{
7 | run_manual_cleaner(.data, width = 1200, height = 900)
8 | }
9 | \arguments{
10 | \item{.data}{The data to be manually cleaned. The first column will be always treated as the x-axis.}
11 |
12 | \item{width}{The width, in pixels, of the window.}
13 |
14 | \item{height}{the height, in pixels, of the window.}
15 | }
16 | \value{
17 | The code to reproduce the manual data cleaning.
18 | }
19 | \description{
20 | Usually manual data cleaning should be avoided. However, sometimes in gas exchange data
21 | there is the need to delete a few clear "bad breaths" (noise). In these situations you may use this function.
22 | Although it is encouraged that you use the \code{detect_outliers()} function, you may use this function at your own risk.
23 | This function can also be used to clean other kind of data, like heart rate data.
24 | }
25 |
--------------------------------------------------------------------------------
/man/work_rate_step.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/helpers-incremental.R
3 | \name{work_rate_step}
4 | \alias{work_rate_step}
5 | \title{Work rate for a step-incremental test}
6 | \usage{
7 | work_rate_step(
8 | .data,
9 | baseline_intensity,
10 | step_start,
11 | step_increase,
12 | step_length
13 | )
14 | }
15 | \arguments{
16 | \item{.data}{The data with recognized protocol phases}
17 |
18 | \item{baseline_intensity}{The baseline intensity}
19 |
20 | \item{step_start}{In case the step test started in a different work rate than baseline}
21 |
22 | \item{step_increase}{The step in increase, in watts per step}
23 |
24 | \item{step_length}{The length, in seconds, of each step}
25 | }
26 | \value{
27 | a \link[tibble:tibble-package]{tibble}
28 | }
29 | \description{
30 | This function produces the work rate throughout a step-incremental test given the protocol
31 | This will retrieve both the 'original' work rates, and also will perform a 'linearization' of the steps.
32 | }
33 | \keyword{internal}
34 |
--------------------------------------------------------------------------------
/R/globals.R:
--------------------------------------------------------------------------------
1 | utils::globalVariables(
2 | c(".",
3 | "V1",
4 | "V2",
5 | ".fitted",
6 | ".resid",
7 | ".se.fit",
8 | "bands_data",
9 | "baseline_fit",
10 | "data",
11 | "lag_residuals",
12 | "lwr_pred",
13 | "n_transition",
14 | "outlier",
15 | "phase",
16 | "sqrt_abs_standardized_residuals",
17 | "standardized_residuals",
18 | "transition",
19 | "upr_pred",
20 | "x",
21 | "y",
22 | "n",
23 | "fit",
24 | "lwr",
25 | "upr",
26 | "cleaning_baseline_fit",
27 | "name",
28 | "value",
29 | "info",
30 | "time",
31 | "VO2",
32 | "anomaly",
33 | "data_ramp_outliers",
34 | "formula_model",
35 | "label",
36 | "model",
37 | "model_augmented",
38 | "mrt_fit",
39 | "pred",
40 | "protocol_phase",
41 | "recomposed_l2",
42 | "remainder",
43 | "step",
44 | "step_work_rate",
45 | "t_step",
46 | "unit",
47 | "work_rate",
48 | "desc",
49 | "X1",
50 | "X2"),
51 | package = "whippr",
52 | add = FALSE
53 | )
54 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2020 Felipe Mattioni Maturana
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/testthat/test-perform_kinetics.R:
--------------------------------------------------------------------------------
1 | test_that("core vo2 kinetics function works", {
2 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
3 |
4 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
5 |
6 | ## detect outliers
7 | data_outliers <- detect_outliers(
8 | .data = df,
9 | test_type = "kinetics",
10 | vo2_column = "VO2",
11 | cleaning_level = 0.95,
12 | cleaning_baseline_fit = c("linear", "exponential", "exponential"),
13 | protocol_n_transitions = 3,
14 | protocol_baseline_length = 360,
15 | protocol_transition_length = 360,
16 | verbose = FALSE
17 | )
18 |
19 | ## process data
20 | data_processed <- process_data(
21 | .data_outliers = data_outliers,
22 | protocol_baseline_length = 360,
23 | fit_bin_average = 5
24 | )
25 |
26 | data_kinetics <- perform_kinetics(
27 | .data_processed = data_processed,
28 | intensity_domain = "moderate",
29 | fit_level = 0.95,
30 | fit_phase_1_length = 20,
31 | fit_baseline_length = 120,
32 | fit_transition_length = 240,
33 | verbose = FALSE
34 | )
35 |
36 | expect_s3_class(
37 | object = data_kinetics,
38 | class = "tbl"
39 | )
40 | })
41 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: whippr
2 | Title: Tools for Manipulating Gas Exchange Data
3 | Version: 0.1.4
4 | Authors@R:
5 | person(given = "Felipe",
6 | family = "Mattioni Maturana",
7 | role = c("aut", "cre"),
8 | email = "felipe.mattioni@med.uni-tuebingen.de",
9 | comment = c(ORCID = "0000-0002-4221-6104"))
10 | Description: Set of tools for manipulating gas exchange data from cardiopulmonary exercise testing.
11 | License: MIT + file LICENSE
12 | URL: https://fmmattioni.github.io/whippr/, https://github.com/fmmattioni/whippr
13 | BugReports: https://github.com/fmmattioni/whippr/issues
14 | Encoding: UTF-8
15 | Roxygen: list(markdown = TRUE)
16 | Imports:
17 | readxl (>= 1.3.1),
18 | dplyr (>= 1.0.1),
19 | stringr (>= 1.4.0),
20 | lubridate (>= 1.7.9),
21 | magrittr,
22 | tibble,
23 | zoo,
24 | purrr,
25 | tidyr (>= 1.1.1),
26 | broom (>= 0.7.0),
27 | cli,
28 | ggplot2 (>= 3.4.0),
29 | glue,
30 | minpack.lm,
31 | patchwork (>= 1.0.1),
32 | rlang,
33 | nlstools,
34 | pillar
35 | RoxygenNote: 7.3.2
36 | Suggests:
37 | knitr,
38 | rmarkdown,
39 | fansi,
40 | collapsibleTree,
41 | testthat,
42 | shiny,
43 | miniUI,
44 | datapasta,
45 | rstudioapi,
46 | htmltools,
47 | readr,
48 | anomalize,
49 | ggforce,
50 | ggtext,
51 | forcats
52 |
--------------------------------------------------------------------------------
/R/interpolate.R:
--------------------------------------------------------------------------------
1 | #' Interpolate data from breath-by-breath into second-by-second
2 | #'
3 | #' This function interpolates the data based on the time column. It takes the breath-by-breath data
4 | #' and transforms it into second-by-second.
5 | #'
6 | #' @param .data Data retrieved from \code{read_data()}.
7 | #'
8 | #' @return a [tibble][tibble::tibble-package]
9 | #' @export
10 | #'
11 | #' @importFrom stats approx
12 | #'
13 | #' @examples
14 | #' \dontrun{
15 | #' ## get file path from example data
16 | #' path_example <- system.file("example_cosmed.xlsx", package = "whippr")
17 | #'
18 | #' ## read data
19 | #' df <- read_data(path = path_example, metabolic_cart = "cosmed")
20 | #'
21 | #' df %>%
22 | #' interpolate()
23 | #' }
24 | interpolate <- function(.data) {
25 | ## first make sure data only contains numeric columns
26 | data_num <- .data %>%
27 | dplyr::select_if(is.numeric) %>%
28 | remove_empty(dat = ., which = c("rows", "cols"))
29 |
30 | suppressWarnings({
31 | out <- lapply(data_num, function (i) approx(
32 | x = data_num[[1]],
33 | y = i,
34 | xout = seq(min(data_num[[1]]), max(data_num[[1]], na.rm = TRUE), 1)
35 | )$y
36 | ) %>%
37 | dplyr::as_tibble()
38 | })
39 |
40 |
41 | metadata <- attributes(.data)
42 | metadata$data_status <- "interpolated data"
43 |
44 | out <- new_whippr_tibble(out, metadata)
45 |
46 | out
47 | }
48 |
--------------------------------------------------------------------------------
/.github/workflows/pkgdown.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | push:
5 | branches: [main, master]
6 | pull_request:
7 | release:
8 | types: [published]
9 | workflow_dispatch:
10 |
11 | name: pkgdown.yaml
12 |
13 | permissions: read-all
14 |
15 | jobs:
16 | pkgdown:
17 | runs-on: ubuntu-latest
18 | # Only restrict concurrency for non-PR jobs
19 | concurrency:
20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }}
21 | env:
22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
23 | permissions:
24 | contents: write
25 | steps:
26 | - uses: actions/checkout@v4
27 |
28 | - uses: r-lib/actions/setup-pandoc@v2
29 |
30 | - uses: r-lib/actions/setup-r@v2
31 | with:
32 | use-public-rspm: true
33 |
34 | - uses: r-lib/actions/setup-r-dependencies@v2
35 | with:
36 | extra-packages: any::pkgdown, local::.
37 | needs: website
38 |
39 | - name: Build site
40 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
41 | shell: Rscript {0}
42 |
43 | - name: Deploy to GitHub pages 🚀
44 | if: github.event_name != 'pull_request'
45 | uses: JamesIves/github-pages-deploy-action@v4.5.0
46 | with:
47 | clean: false
48 | branch: gh-pages
49 | folder: docs
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .Rproj.user
2 | .Rhistory
3 | .DS_Store
4 | .RData
5 | docs/
6 | demo.Rmd
7 | dev.R
8 | dev/
9 | CRAN-SUBMISSION
10 |
11 | # Created by https://www.gitignore.io/api/macos,r
12 | # Edit at https://www.gitignore.io/?templates=macos,r
13 | ### macOS ###
14 | # General
15 | .AppleDouble
16 | .LSOverride
17 | # Icon must end with two \r
18 | Icon
19 | # Thumbnails
20 | ._*
21 | # Files that might appear in the root of a volume
22 | .DocumentRevisions-V100
23 | .fseventsd
24 | .Spotlight-V100
25 | .TemporaryItems
26 | .Trashes
27 | .VolumeIcon.icns
28 | .com.apple.timemachine.donotpresent
29 | # Directories potentially created on remote AFP share
30 | .AppleDB
31 | .AppleDesktop
32 | Network Trash Folder
33 | Temporary Items
34 | .apdisk
35 | ### R ###
36 | # History files
37 | .Rapp.history
38 | # Session Data files
39 | .RDataTmp
40 | # User-specific files
41 | .Ruserdata
42 | # Example code in package build process
43 | *-Ex.R
44 | # Output files from R CMD build
45 | /*.tar.gz
46 | # Output files from R CMD check
47 | /*.Rcheck/
48 | # RStudio files
49 | .Rproj.user/
50 | # produced vignettes
51 | vignettes/*.html
52 | vignettes/*.pdf
53 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3
54 | .httr-oauth
55 | # knitr and R markdown default cache directories
56 | *_cache/
57 | /cache/
58 | # Temporary files created by R markdown
59 | *.utf8.md
60 | *.knit.md
61 | ### R.Bookdown Stack ###
62 | # R package: bookdown caching files
63 | /*_files/
64 | # End of https://www.gitignore.io/api/macos,r
65 | inst/doc
66 |
--------------------------------------------------------------------------------
/man/read_data.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/read-data.R
3 | \name{read_data}
4 | \alias{read_data}
5 | \title{Read data from metabolic cart}
6 | \usage{
7 | read_data(
8 | path,
9 | metabolic_cart = c("cosmed", "cortex", "nspire", "parvo", "geratherm", "cardiocoach",
10 | "custom"),
11 | time_column = "t",
12 | work_rate_column = NULL
13 | )
14 | }
15 | \arguments{
16 | \item{path}{Path to read the file from.}
17 |
18 | \item{metabolic_cart}{Metabolic cart that was used for data collection. Currently, 'cosmed', 'cortex', 'nspire', 'parvo', 'geratherm', and 'cardiocoach' are supported. Additionaly, there is an option called 'custom' that supports files that do not have a metabolic cart-specific format.}
19 |
20 | \item{time_column}{The name (quoted) of the column containing the time. Depending on the language of your system, this column might not be "t". Therefore, you may specify it here. Default to "t".}
21 |
22 | \item{work_rate_column}{Default is \code{NULL}. In case your work rate column is coerced as a character column
23 | you can define here the name of this column in your data file. This happens because at the very beginning of the test
24 | the system may input a character like "-" to indicate no work rate. Therefore this is not going to get recognized as a numeric column.
25 | If your work rate column is called \code{WR}, for example, just pass \code{"WR"} to this argument.}
26 | }
27 | \value{
28 | a \link[tibble:tibble-package]{tibble}
29 | }
30 | \description{
31 | It reads the raw data exported from the metabolic cart.
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/R-CMD-check.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | push:
5 | branches: [main, master]
6 | pull_request:
7 |
8 | name: R-CMD-check.yaml
9 |
10 | permissions: read-all
11 |
12 | jobs:
13 | R-CMD-check:
14 | runs-on: ${{ matrix.config.os }}
15 |
16 | name: ${{ matrix.config.os }} (${{ matrix.config.r }})
17 |
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | config:
22 | - {os: macos-latest, r: 'release'}
23 | - {os: windows-latest, r: 'release'}
24 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
25 | - {os: ubuntu-latest, r: 'release'}
26 | - {os: ubuntu-latest, r: 'oldrel-1'}
27 |
28 | env:
29 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
30 | R_KEEP_PKG_SOURCE: yes
31 |
32 | steps:
33 | - uses: actions/checkout@v4
34 |
35 | - uses: r-lib/actions/setup-pandoc@v2
36 |
37 | - uses: r-lib/actions/setup-r@v2
38 | with:
39 | r-version: ${{ matrix.config.r }}
40 | http-user-agent: ${{ matrix.config.http-user-agent }}
41 | use-public-rspm: true
42 |
43 | - uses: r-lib/actions/setup-r-dependencies@v2
44 | with:
45 | extra-packages: any::rcmdcheck
46 | needs: check
47 |
48 | - uses: r-lib/actions/check-r-package@v2
49 | with:
50 | upload-snapshots: true
51 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
52 |
--------------------------------------------------------------------------------
/R/helpers-read.R:
--------------------------------------------------------------------------------
1 | # Get target data
2 | ## these functions could be a single function actually,
3 | ## but perhaps it is better to keep it that way in case we add a system in the future that requires additional steps
4 | target_cortex <- function(.data, time_column){
5 | target_cell <- which(.data == time_column, arr.ind = TRUE)
6 |
7 | ## usually cortex will not export the data at the top of the spreadsheet
8 | ## meaning that the data we are looking for won't have column names recognized here
9 | ## the following is just to make sure that, in case the user's export settings does export the data at the top of the spreadsheet, it will get recognized
10 | if(purrr::is_empty(target_cell))
11 | target_cell <- which(colnames(.data) == time_column, arr.ind = TRUE)
12 |
13 | target_cell
14 | }
15 |
16 | target_cosmed <- function(.data, time_column){
17 | target_cell <- which(colnames(.data) == time_column, arr.ind = TRUE)
18 |
19 | target_cell
20 | }
21 |
22 | target_nspire <- function(.data, time_column){
23 | target_cell <- which(colnames(.data) == time_column, arr.ind = TRUE)
24 |
25 | target_cell
26 | }
27 |
28 | target_parvo <- function(.data, time_column) {
29 | target_cell <- which(.data == time_column, arr.ind = TRUE)
30 |
31 | ## usually cortex will not export the data at the top of the spreadsheet
32 | ## meaning that the data we are looking for won't have column names recognized here
33 | ## the following is just to make sure that, in case the user's export settings does export the data at the top of the spreadsheet, it will get recognized
34 | if(purrr::is_empty(target_cell))
35 | target_cell <- which(colnames(.data) == time_column, arr.ind = TRUE)
36 |
37 | target_cell
38 | }
39 |
--------------------------------------------------------------------------------
/man/predict_bands.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/predict.R
3 | \name{predict_bands}
4 | \alias{predict_bands}
5 | \title{Extract confidence and prediction bands}
6 | \usage{
7 | predict_bands(
8 | .data,
9 | time_column = "t",
10 | vo2_column = "VO2",
11 | cleaning_level = 0.95,
12 | cleaning_baseline_fit = c("linear", "exponential")
13 | )
14 | }
15 | \arguments{
16 | \item{.data}{The nornalized data retrieved from \code{normalize_transitions()}.}
17 |
18 | \item{time_column}{The name (quoted) of the column containing the time. Depending on the language of your system, this column might not be "t". Therefore, you may specify it here. Default to "t".}
19 |
20 | \item{vo2_column}{The name (quoted) of the column containing the absolute oxygen uptake (VO2) data. Default to 'VO2'.}
21 |
22 | \item{cleaning_level}{A numeric scalar between 0 and 1 giving the confidence level for the intervals to be calculated.}
23 |
24 | \item{cleaning_baseline_fit}{A character indicating what kind of fit to perform for each baseline. Either 'linear' or 'exponential'.}
25 | }
26 | \value{
27 | a \link[tibble:tibble-package]{tibble} containing the following columns:
28 | \item{x}{The provided time data.}
29 | \item{y}{The provided VO2 data.}
30 | \item{.fitted}{The predicted response for that observation.}
31 | \item{.resid}{The residual for a particular point.}
32 | \item{lwr_conf}{Lower limit of the confidence band.}
33 | \item{upr_conf}{Upper limit of the confidence band.}
34 | \item{lwr_pred}{Lower limit of the prediction band.}
35 | \item{upr_pred}{Upper limit of the prediction band.}
36 | }
37 | \description{
38 | It extracts confidence and prediction bands from the \code{nls} model. It is used only for data cleaning.
39 | }
40 |
--------------------------------------------------------------------------------
/man/predict_bands_transition.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/predict.R
3 | \name{predict_bands_transition}
4 | \alias{predict_bands_transition}
5 | \title{Extract confidence and prediction bands for the transition phase}
6 | \usage{
7 | predict_bands_transition(
8 | .data,
9 | time_column,
10 | vo2_column,
11 | cleaning_level,
12 | cleaning_model
13 | )
14 | }
15 | \arguments{
16 | \item{.data}{The nornalized data retrieved from \code{normalize_transitions()}. The data should be filtered to only the 'transition' phase before passing to the function.}
17 |
18 | \item{time_column}{The name (quoted) of the column containing the time. Depending on the language of your system, this column might not be "t". Therefore, you may specify it here. Default to "t".}
19 |
20 | \item{vo2_column}{The name (quoted) of the column containing the absolute oxygen uptake (VO2) data. Default to 'VO2'.}
21 |
22 | \item{cleaning_level}{A numeric scalar between 0 and 1 giving the confidence level for the intervals to be calculated.}
23 |
24 | \item{cleaning_model}{The \code{nls} model to retrieve the bands from.}
25 | }
26 | \value{
27 | a \link[tibble:tibble-package]{tibble} containing the following columns:
28 | \item{x}{The provided time data.}
29 | \item{y}{The provided VO2 data.}
30 | \item{.fitted}{The predicted response for that observation.}
31 | \item{.resid}{The residual for a particular point.}
32 | \item{lwr_conf}{Lower limit of the confidence band.}
33 | \item{upr_conf}{Upper limit of the confidence band.}
34 | \item{lwr_pred}{Lower limit of the prediction band.}
35 | \item{upr_pred}{Upper limit of the prediction band.}
36 | }
37 | \description{
38 | Extract confidence and prediction bands for the transition phase
39 | }
40 |
--------------------------------------------------------------------------------
/man/predict_bands_baseline.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/predict.R
3 | \name{predict_bands_baseline}
4 | \alias{predict_bands_baseline}
5 | \title{Extract confidence and prediction bands for the baseline phase}
6 | \usage{
7 | predict_bands_baseline(
8 | .data,
9 | time_column,
10 | vo2_column,
11 | cleaning_level,
12 | cleaning_baseline_fit
13 | )
14 | }
15 | \arguments{
16 | \item{.data}{The nornalized data retrieved from \code{normalize_transitions()}. The data should be filtered to only the 'baseline' phase before passing to the function.}
17 |
18 | \item{time_column}{The name (quoted) of the column containing the time. Depending on the language of your system, this column might not be "t". Therefore, you may specify it here. Default to "t".}
19 |
20 | \item{vo2_column}{The name (quoted) of the column containing the absolute oxygen uptake (VO2) data. Default to 'VO2'.}
21 |
22 | \item{cleaning_level}{A numeric scalar between 0 and 1 giving the confidence level for the intervals to be calculated.}
23 |
24 | \item{cleaning_baseline_fit}{A character indicating what kind of fit to perform for each baseline. Either 'linear' or 'exponential'.}
25 | }
26 | \value{
27 | a \link[tibble:tibble-package]{tibble} containing the following columns:
28 | \item{x}{The provided time data.}
29 | \item{y}{The provided VO2 data.}
30 | \item{.fitted}{The predicted response for that observation.}
31 | \item{.resid}{The residual for a particular point.}
32 | \item{lwr_conf}{Lower limit of the confidence band.}
33 | \item{upr_conf}{Upper limit of the confidence band.}
34 | \item{lwr_pred}{Lower limit of the prediction band.}
35 | \item{upr_pred}{Upper limit of the prediction band.}
36 | }
37 | \description{
38 | Extract confidence and prediction bands for the baseline phase
39 | }
40 |
--------------------------------------------------------------------------------
/man/undoHistory.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/shinyThings.R
3 | \name{undoHistory}
4 | \alias{undoHistory}
5 | \title{Undo/Redo History Buttons}
6 | \usage{
7 | undoHistory(id, value, value_debounce_rate = 500)
8 | }
9 | \arguments{
10 | \item{id}{The module id}
11 |
12 | \item{value}{The reactive expression with the values should be saved for the
13 | user's history. This expression can contain arbitrary data and be of any
14 | structure as long as it returns a single value (or list). Each change in
15 | this value is stored, so the module may not work well for storing large
16 | data sets.}
17 |
18 | \item{value_debounce_rate}{Debounce rate in milliseconds for the \code{value}
19 | reactive expression. To avoid saving spurious changes in \code{value}, the
20 | expression is debounced. See \code{\link[shiny:debounce]{shiny::debounce()}} for more information.}
21 | }
22 | \value{
23 | The \code{undoHistory()} module returns the currently selected history
24 | item as the user moves through the stack, or \code{NULL} if the last update
25 | was the result of user input. The returned value has the same structure as
26 | the reactive \code{value} passed to \code{undoHistory()}.
27 | }
28 | \description{
29 | This is a simple Shiny module for undo/redo history. The Shiny module accepts
30 | an arbitrary reactive data value. Changes in the state of this reactive value
31 | are tracked and added to the user's history. The user can then repeatedly
32 | undo and redo to walk through this stack. The module returns the current
33 | selected value of the reactive from this historical stack, or \code{NULL} when
34 | the app state was changed by the user. Because this reactive can hold
35 | arbitrary data about the state of the Shiny app, it is up to the app
36 | developer to use the returned current value to update the Shiny apps' inputs
37 | and UI elements.
38 | }
39 | \keyword{internal}
40 |
--------------------------------------------------------------------------------
/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | S3method(detect_outliers,incremental)
4 | S3method(detect_outliers,kinetics)
5 | S3method(incremental_normalize,ramp)
6 | S3method(incremental_normalize,step)
7 | S3method(perform_average,bin)
8 | S3method(perform_average,ensemble)
9 | S3method(perform_average,rolling)
10 | S3method(perform_kinetics,heavy)
11 | S3method(perform_kinetics,moderate)
12 | S3method(plot_incremental,ramp)
13 | S3method(plot_incremental,step)
14 | S3method(plot_outliers,incremental)
15 | S3method(plot_outliers,kinetics)
16 | S3method(print,whippr)
17 | S3method(read_data,cardiocoach)
18 | S3method(read_data,cortex)
19 | S3method(read_data,cosmed)
20 | S3method(read_data,custom)
21 | S3method(read_data,geratherm)
22 | S3method(read_data,nspire)
23 | S3method(read_data,parvo)
24 | export("%>%")
25 | export(detect_outliers)
26 | export(get_residuals)
27 | export(incremental_normalize)
28 | export(interpolate)
29 | export(model_diagnostics)
30 | export(normalize_first_breath)
31 | export(normalize_time)
32 | export(normalize_transitions)
33 | export(perform_average)
34 | export(perform_kinetics)
35 | export(perform_max)
36 | export(plot_incremental)
37 | export(plot_outliers)
38 | export(predict_bands)
39 | export(predict_bands_baseline)
40 | export(predict_bands_transition)
41 | export(process_data)
42 | export(read_data)
43 | export(run_manual_cleaner)
44 | export(theme_whippr)
45 | export(vo2_kinetics)
46 | export(vo2_max)
47 | importFrom(ggplot2,element_line)
48 | importFrom(ggplot2,element_rect)
49 | importFrom(ggplot2,element_text)
50 | importFrom(ggplot2,theme)
51 | importFrom(ggplot2,theme_light)
52 | importFrom(magrittr,"%>%")
53 | importFrom(rlang,":=")
54 | importFrom(stats,approx)
55 | importFrom(stats,coef)
56 | importFrom(stats,deriv)
57 | importFrom(stats,lm)
58 | importFrom(stats,predict.lm)
59 | importFrom(stats,qt)
60 | importFrom(stats,vcov)
61 | importFrom(tibble,tibble)
62 | importFrom(utils,head)
63 | importFrom(utils,tail)
64 |
--------------------------------------------------------------------------------
/R/utils.R:
--------------------------------------------------------------------------------
1 | # manual import of janitor::remove_empty() to avoid extra dependency
2 |
3 | #' @title Remove empty rows and/or columns from a data.frame or matrix.
4 | #'
5 | #' @description Removes all rows and/or columns from a data.frame or matrix that
6 | #' are composed entirely of \code{NA} values.
7 | #'
8 | #' @param dat the input data.frame or matrix.
9 | #' @param which one of "rows", "cols", or \code{c("rows", "cols")}. Where no
10 | #' value of which is provided, defaults to removing both empty rows and empty
11 | #' columns, declaring the behavior with a printed message.
12 | #' @param cutoff What fraction (>0 to <=1) of rows or columns must be empty to
13 | #' be removed?
14 | #' @return Returns the object without its missing rows or columns.
15 | #' @keywords internal
16 | remove_empty <- function(dat, which = c("rows", "cols"), cutoff=1) {
17 | if (missing(which) && !missing(dat)) {
18 | message("value for \"which\" not specified, defaulting to c(\"rows\", \"cols\")")
19 | which <- c("rows", "cols")
20 | }
21 | if ((sum(which %in% c("rows", "cols")) != length(which)) && !missing(dat)) {
22 | stop("\"which\" must be one of \"rows\", \"cols\", or c(\"rows\", \"cols\")")
23 | }
24 | if (length(cutoff) != 1) {
25 | stop("cutoff must be a single value")
26 | } else if (!is.numeric(cutoff)) {
27 | stop("cutoff must be numeric")
28 | } else if (cutoff <= 0 | cutoff > 1) {
29 | stop("cutoff must be >0 and <= 1")
30 | } else if (length(which) > 1 & cutoff != 1) {
31 | stop("cutoff must be used with only one of which = 'rows' or 'cols', not both")
32 | }
33 | if ("rows" %in% which) {
34 | # Using different code with cutoff = 1 vs cutoff != 1 to avoid possible
35 | # floating point errors.
36 | mask_keep <-
37 | if (cutoff == 1) {
38 | rowSums(is.na(dat)) != ncol(dat)
39 | } else {
40 | (rowSums(!is.na(dat))/ncol(dat)) > cutoff
41 | }
42 | dat <- dat[mask_keep, , drop = FALSE]
43 | }
44 | if ("cols" %in% which) {
45 | # Using different code with cutoff = 1 vs cutoff != 1 to avoid possible
46 | # floating point errors.
47 | mask_keep <-
48 | if (cutoff == 1) {
49 | colSums(is.na(dat)) != nrow(dat)
50 | } else {
51 | (colSums(!is.na(dat))/nrow(dat)) > cutoff
52 | }
53 | dat <- dat[, mask_keep, drop = FALSE]
54 | }
55 | dat
56 | }
57 |
--------------------------------------------------------------------------------
/man/perform_average.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/averages.R
3 | \name{perform_average}
4 | \alias{perform_average}
5 | \title{Perform average on second-by-second data}
6 | \usage{
7 | perform_average(
8 | .data,
9 | type = c("bin", "rolling", "ensemble"),
10 | bins = 30,
11 | bin_method = c("ceiling", "round", "floor"),
12 | rolling_window = 30
13 | )
14 | }
15 | \arguments{
16 | \item{.data}{The second-by-second data retrieved from \code{interpolate()}.}
17 |
18 | \item{type}{The type of the average to perform. Either \code{bin}, \code{rolling}, or \code{ensemble}.}
19 |
20 | \item{bins}{If bin-average is chosen, here you can specify the size of the bin-average, in seconds. Default to 30-s bin-average.}
21 |
22 | \item{bin_method}{Method for determining bin boundaries when \code{type = "bin"}.
23 | One of \code{"ceiling"} (default), \code{"round"}, or \code{"floor"}.
24 | \code{"ceiling"} is recommended as it ensures no data points are excluded
25 | from the analysis by always rounding up to the next bin boundary.}
26 |
27 | \item{rolling_window}{If rolling-average is chosen, here you can specify the rolling-average window, in seconds. Default to 30-s rolling-average.}
28 | }
29 | \value{
30 | a \link[tibble:tibble-package]{tibble}
31 | }
32 | \description{
33 | This function performs either a bin- or a rolling-average on the interpolated data.
34 | You must specify the \code{type} of the average before continuing.
35 | }
36 | \details{
37 | Ensemble average is used in VO2 kinetics analysis, where a series of transitions from baseline to
38 | the moderate/heavy/severe intensity-domain is ensembled averaged into a single 'bout' for further data processing.
39 |
40 | When using bin averaging, the \code{bin_method} parameter controls how time points are assigned to bins:
41 | \itemize{
42 | \item \code{"ceiling"}: Rounds up to the next bin boundary (recommended)
43 | \item \code{"round"}: Rounds to the nearest bin boundary
44 | \item \code{"floor"}: Rounds down to the previous bin boundary
45 | }
46 | }
47 | \examples{
48 | \dontrun{
49 | ## get file path from example data
50 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
51 |
52 | ## read data
53 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
54 |
55 | ## interpolate and perform 30-s bin-average
56 | df \%>\%
57 | interpolate() \%>\%
58 | perform_average(type = "bin", bins = 30)
59 |
60 | ## interpolate and perform 30-s rolling-average
61 | df \%>\%
62 | interpolate() \%>\%
63 | perform_average(type = "rolling", rolling_window = 30)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/man/perform_kinetics.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/kinetics.R
3 | \name{perform_kinetics}
4 | \alias{perform_kinetics}
5 | \title{Perform VO2 kinetics fitting}
6 | \usage{
7 | perform_kinetics(
8 | .data_processed,
9 | intensity_domain = c("moderate", "heavy", "severe"),
10 | fit_level = 0.95,
11 | fit_phase_1_length,
12 | fit_baseline_length,
13 | fit_transition_length,
14 | verbose = TRUE,
15 | ...
16 | )
17 | }
18 | \arguments{
19 | \item{.data_processed}{The data retrieved from \code{process_data()}.}
20 |
21 | \item{intensity_domain}{The exercise-intensity domain that the test was performed. Either \emph{moderate}, \emph{heavy}, or \emph{severe}.}
22 |
23 | \item{fit_level}{A numeric scalar between 0 and 1 giving the confidence level for the parameter estimates in the final VO2 kinetics fit. Default to \code{0.95}.}
24 |
25 | \item{fit_phase_1_length}{The length of the phase I that you wish to exclude from the final exponential fit, in seconds. See \verb{VO2 kinetics} section in \code{?vo2_kinetics} for more details.}
26 |
27 | \item{fit_baseline_length}{The length the baseline to perform the final linear fit, in seconds. See \verb{VO2 kinetics} section \code{?vo2_kinetics} for more details.}
28 |
29 | \item{fit_transition_length}{The length of the transition to perform the final exponential fit, in seconds. See \verb{VO2 kinetics} section \code{?vo2_kinetics} for more details.}
30 |
31 | \item{verbose}{A boolean indicating whether messages should be printed in the console. Default to \code{TRUE}.}
32 |
33 | \item{...}{Additional arguments when fitting VO2 kinetics in the heavy- or severe-intensity domains. Arguments may be the following:
34 | \describe{
35 | \item{\code{TODO}}{}
36 | }}
37 | }
38 | \value{
39 | a \code{\link[tibble:tibble-package]{tibble}} containing one row and the nested columns:
40 | \item{data_fitted}{The data containing the time and VO2 columns, as well as the fitted data and its residuals for each data point.}
41 | \item{model}{A \code{nls} object. The model used in the VO2 kinetics fitting.}
42 | \item{model_summary}{The tidied summary of the \code{model}.}
43 | \item{model_residuals}{The residuals of the \code{model}.}
44 | \item{plot_model}{The final plot of the fitted \code{model}.}
45 | \item{plot_residuals}{The residuals plot for the \code{model} diagnostics.}
46 | }
47 | \description{
48 | Performs the fitting process for the VO2 kinetics analysis. At this point, the data should already have been cleaned (outliers removed) and processed
49 | (interpolated, time-aligned, ensembled-averaged, and bin-averaged).
50 | }
51 | \details{
52 | See \code{?vo2_kinetics} for details.
53 | }
54 |
--------------------------------------------------------------------------------
/_pkgdown.yml:
--------------------------------------------------------------------------------
1 | destination: docs
2 |
3 | template:
4 | bootstrap: 5
5 | bootswatch: sandstone
6 | bslib:
7 | primary: "#0054AD"
8 | border-radius: 0.5rem
9 | btn-border-radius: 0.25rem
10 | opengraph:
11 | image:
12 | src: man/figures/header.png
13 | alt: "Tools for Manipulating Gas Exchange Data"
14 | twitter:
15 | creator: "@felipe_mattioni"
16 | card: summary_large_image
17 | params:
18 | ganalytics: UA-126092763-1
19 |
20 | authors:
21 | Felipe Mattioni Maturana:
22 | href: https://fmattioni.me
23 |
24 | toc:
25 | depth: 3
26 |
27 | development:
28 | mode: unreleased
29 |
30 | navbar:
31 | title: ~
32 | type: default
33 | left:
34 | - icon: fa-home fa-lg
35 | href: index.html
36 | - icon: fa-book fa-lg
37 | text: "Articles"
38 | menu:
39 | - text: "VO2 kinetics analysis"
40 | href: articles/vo2_kinetics.html
41 | - text: "Incremental test analyses"
42 | href: articles/incremental.html
43 | - text: Reference
44 | icon: fa-balance-scale
45 | href: reference/index.html
46 | - text: News
47 | icon: fa-newspaper
48 | menu:
49 | - text: "Change log"
50 | href: news/index.html
51 | right:
52 | - icon: fa-github fa-lg
53 | href: https://github.com/fmmattioni/whippr
54 | - icon: fa-twitter fa-lg
55 | href: https://twitter.com/felipe_mattioni
56 |
57 | reference:
58 | - title: Read data
59 | desc: >
60 | Read raw data from metabolic cart.
61 | contents:
62 | - read_data
63 | - title: Interpolate data
64 | desc: >
65 | Interpolate data from breath-by-breath into second-by-second
66 | contents:
67 | - interpolate
68 | - title: Perform averages
69 | desc: >
70 | Perform average on second-by-second data
71 | contents:
72 | - perform_average
73 | - title: VO2 kinetics analysis
74 | desc: >
75 | Tools for performing VO2 kinetics analysis
76 | contents:
77 | - vo2_kinetics
78 | - starts_with("normalize")
79 | - detect_outliers
80 | - plot_outliers
81 | - starts_with("predict")
82 | - process_data
83 | - perform_kinetics
84 | - get_residuals
85 | - model_diagnostics
86 | - title: VO2 max analysis
87 | desc: >
88 | Tools for performing incremental test analysis
89 | contents:
90 | - incremental_normalize
91 | - plot_incremental
92 | - detect_outliers
93 | - perform_max
94 | - vo2_max
95 | - title: Additional tools
96 | desc: >
97 | Collection of a few additional helpful tools
98 | contents:
99 | - run_manual_cleaner
100 | - theme_whippr
101 | - print.whippr
102 |
--------------------------------------------------------------------------------
/.github/workflows/pr-commands.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | issue_comment:
5 | types: [created]
6 |
7 | name: pr-commands.yaml
8 |
9 | permissions: read-all
10 |
11 | jobs:
12 | document:
13 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }}
14 | name: document
15 | runs-on: ubuntu-latest
16 | env:
17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
18 | permissions:
19 | contents: write
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - uses: r-lib/actions/pr-fetch@v2
24 | with:
25 | repo-token: ${{ secrets.GITHUB_TOKEN }}
26 |
27 | - uses: r-lib/actions/setup-r@v2
28 | with:
29 | use-public-rspm: true
30 |
31 | - uses: r-lib/actions/setup-r-dependencies@v2
32 | with:
33 | extra-packages: any::roxygen2
34 | needs: pr-document
35 |
36 | - name: Document
37 | run: roxygen2::roxygenise()
38 | shell: Rscript {0}
39 |
40 | - name: commit
41 | run: |
42 | git config --local user.name "$GITHUB_ACTOR"
43 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
44 | git add man/\* NAMESPACE
45 | git commit -m 'Document'
46 |
47 | - uses: r-lib/actions/pr-push@v2
48 | with:
49 | repo-token: ${{ secrets.GITHUB_TOKEN }}
50 |
51 | style:
52 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }}
53 | name: style
54 | runs-on: ubuntu-latest
55 | env:
56 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
57 | permissions:
58 | contents: write
59 | steps:
60 | - uses: actions/checkout@v4
61 |
62 | - uses: r-lib/actions/pr-fetch@v2
63 | with:
64 | repo-token: ${{ secrets.GITHUB_TOKEN }}
65 |
66 | - uses: r-lib/actions/setup-r@v2
67 |
68 | - name: Install dependencies
69 | run: install.packages("styler")
70 | shell: Rscript {0}
71 |
72 | - name: Style
73 | run: styler::style_pkg()
74 | shell: Rscript {0}
75 |
76 | - name: commit
77 | run: |
78 | git config --local user.name "$GITHUB_ACTOR"
79 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
80 | git add \*.R
81 | git commit -m 'Style'
82 |
83 | - uses: r-lib/actions/pr-push@v2
84 | with:
85 | repo-token: ${{ secrets.GITHUB_TOKEN }}
86 |
--------------------------------------------------------------------------------
/tests/testthat/test-incremental.R:
--------------------------------------------------------------------------------
1 | test_that("incremental_normalize ramp works", {
2 | ## get file path from example data
3 | path_example <- system.file("ramp_cosmed.xlsx", package = "whippr")
4 | ## read data from ramp test
5 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
6 | ## normalize incremental test data
7 | ramp_normalized <- df %>%
8 | incremental_normalize(
9 | .data = .,
10 | incremental_type = "ramp",
11 | has_baseline = TRUE,
12 | baseline_length = 240,
13 | work_rate_magic = TRUE,
14 | baseline_intensity = 20,
15 | ramp_increase = 25
16 | )
17 |
18 | expect_s3_class(
19 | object = ramp_normalized,
20 | class = "tbl"
21 | )
22 | })
23 |
24 | test_that("incremental_normalize step works", {
25 | ## get file path from example data
26 | path_example <- system.file("step_cortex.xlsx", package = "whippr")
27 | ## read data from ramp test
28 | df <- read_data(path = path_example, metabolic_cart = "cortex")
29 | ## normalize incremental test data
30 | ramp_normalized <- df %>%
31 | incremental_normalize(
32 | .data = .,
33 | incremental_type = "step",
34 | has_baseline = TRUE,
35 | baseline_length = 120,
36 | work_rate_magic = TRUE,
37 | baseline_intensity = 0,
38 | step_start = 50,
39 | step_increase = 25,
40 | step_length = 180
41 | )
42 |
43 | expect_s3_class(
44 | object = ramp_normalized,
45 | class = "tbl"
46 | )
47 | })
48 |
49 | test_that("plot_incremental ramp works", {
50 | ## get file path from example data
51 | path_example <- system.file("ramp_cosmed.xlsx", package = "whippr")
52 | ## read data from ramp test
53 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
54 | ## normalize incremental test data
55 | ramp_normalized <- df %>%
56 | incremental_normalize(
57 | .data = .,
58 | incremental_type = "ramp",
59 | has_baseline = TRUE,
60 | baseline_length = 240,
61 | work_rate_magic = TRUE,
62 | baseline_intensity = 20,
63 | ramp_increase = 25
64 | )
65 |
66 | ## plot
67 | expect_s3_class(
68 | plot_incremental(ramp_normalized),
69 | "ggplot"
70 | )
71 | })
72 |
73 | test_that("plot_incremental step works", {
74 | ## get file path from example data
75 | path_example <- system.file("step_cortex.xlsx", package = "whippr")
76 | ## read data from ramp test
77 | df <- read_data(path = path_example, metabolic_cart = "cortex")
78 | ## normalize incremental test data
79 | ramp_normalized <- df %>%
80 | incremental_normalize(
81 | .data = .,
82 | incremental_type = "step",
83 | has_baseline = TRUE,
84 | baseline_length = 120,
85 | work_rate_magic = TRUE,
86 | baseline_intensity = 0,
87 | step_start = 50,
88 | step_increase = 25,
89 | step_length = 180
90 | )
91 |
92 | ## plot
93 | expect_s3_class(
94 | plot_incremental(ramp_normalized),
95 | "ggplot"
96 | )
97 | })
98 |
--------------------------------------------------------------------------------
/man/perform_max.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/max.R
3 | \name{perform_max}
4 | \alias{perform_max}
5 | \title{Perform VO2max calculation}
6 | \usage{
7 | perform_max(
8 | .data,
9 | vo2_column = "VO2",
10 | vo2_relative_column = NULL,
11 | heart_rate_column = NULL,
12 | rer_column = NULL,
13 | average_method = c("bin", "rolling"),
14 | average_length = 30,
15 | plot = TRUE,
16 | verbose = TRUE
17 | )
18 | }
19 | \arguments{
20 | \item{.data}{The data retrieved either from \code{incremental_normalize()} or \code{detect_outliers()}.}
21 |
22 | \item{vo2_column}{The name (quoted) of the column containing the absolute oxygen uptake (VO2) data. Default to \code{"VO2"}.}
23 |
24 | \item{vo2_relative_column}{The name (quoted) of the column containing the relative to body weight oxygen uptake (VO2) data. Default to \code{NULL}.}
25 |
26 | \item{heart_rate_column}{The name (quoted) of the column containing the heart rate (HR) data. Default to \code{NULL}. If \code{NULL}, this parameter will not be calculated.}
27 |
28 | \item{rer_column}{The name (quoted) of the column containing the respiratory exchange ratio (RER) data. Default to \code{NULL}. If \code{NULL}, this parameter will not be calculated.}
29 |
30 | \item{average_method}{The average method to be used for VO2max calculation. One of \code{bin} or \code{rolling}.}
31 |
32 | \item{average_length}{The length, in seconds, of the average to be used. For example, if \code{average_method = bin}, and \code{average_length = 30}, it will perform a 30-s bin-average.}
33 |
34 | \item{plot}{A boolean indicating whether to produce a plot with the summary results. Default to \code{TRUE}.}
35 |
36 | \item{verbose}{A boolean indicating whether messages should be printed in the console. Default to \code{TRUE}.}
37 | }
38 | \value{
39 | a tibble
40 | }
41 | \description{
42 | It performs the calculation of VO2max, HRmax, and maximal RER. Additionally, it detects whether a plateau can be identified from your data.
43 | }
44 | \examples{
45 | \dontrun{
46 | ## get file path from example data
47 | path_example <- system.file("ramp_cosmed.xlsx", package = "whippr")
48 |
49 | ## read data from ramp test
50 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
51 |
52 | ## normalize incremental test data
53 | ramp_normalized <- df \%>\%
54 | incremental_normalize(
55 | .data = .,
56 | incremental_type = "ramp",
57 | has_baseline = TRUE,
58 | baseline_length = 240,
59 | work_rate_magic = TRUE,
60 | baseline_intensity = 20,
61 | ramp_increase = 25
62 | )
63 |
64 | ## detect outliers
65 | data_ramp_outliers <- detect_outliers(
66 | .data = ramp_normalized,
67 | test_type = "incremental",
68 | vo2_column = "VO2",
69 | cleaning_level = 0.95,
70 | method_incremental = "linear",
71 | verbose = TRUE
72 | )
73 |
74 | ## analyze VO2max
75 | perform_max(
76 | .data = data_ramp_outliers,
77 | vo2_column = "VO2",
78 | vo2_relative_column = "VO2/Kg",
79 | heart_rate_column = "HR",
80 | rer_column = "R",
81 | average_method = "bin",
82 | average_length = 30,
83 | plot = TRUE,
84 | verbose = FALSE
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/R/nlstools.R:
--------------------------------------------------------------------------------
1 | #' Get residuals
2 | #'
3 | #' Computes residuals from the VO2 kinetics model.
4 | #'
5 | #' @param .model A model of class \code{nls}.
6 | #'
7 | #' @return a [tibble][tibble::tibble-package] containing the data passed to augment, and additional columns:
8 | #' \item{.fitted}{The predicted response for that observation.}
9 | #' \item{.resid}{The residual for a particular point.}
10 | #' \item{standardized_residuals}{Standardized residuals.}
11 | #' \item{sqrt_abs_standardized_residuals}{The sqrt of absolute value of standardized residuals.}
12 | #' \item{lag_residuals}{The lag of the \code{.resid} column for plotting auto-correlation.}
13 | #'
14 | #' @export
15 | get_residuals <- function(.model) {
16 | ## adapted from nlstools::nlsResiduals()
17 |
18 | sigma_model <- summary(.model)$sigma
19 |
20 | model_augmented <- broom::augment(.model)
21 |
22 | mean_residuals <- mean(model_augmented$.resid)
23 |
24 | out <- model_augmented %>%
25 | dplyr::mutate(standardized_residuals = (.resid - mean_residuals) / sigma_model,
26 | sqrt_abs_standardized_residuals = abs(.resid / sigma_model),
27 | sqrt_abs_standardized_residuals = sqrt(sqrt_abs_standardized_residuals),
28 | lag_residuals = dplyr::lag(.resid))
29 |
30 | out
31 | }
32 |
33 | #' Model diagnostics
34 | #'
35 | #' Plots different model diagnostics for checking the model performance.
36 | #'
37 | #' @param .residuals_tbl The data retrived from \code{get_residuals()}.
38 | #'
39 | #' @return a patchwork object
40 | #' @export
41 | model_diagnostics <- function(.residuals_tbl) {
42 | ## Plots inspired both by the 'nlstools' and 'see' packages
43 |
44 | ## P1
45 | p1 <- .residuals_tbl %>%
46 | ggplot2::ggplot(ggplot2::aes(.resid)) +
47 | ggplot2::geom_density(fill = "skyblue", alpha = 0.5) +
48 | ggplot2::labs(
49 | title = "Non-Normality of Residuals",
50 | subtitle = "Distribution should look like a normal curve",
51 | x = "Residuals",
52 | y = "Density"
53 | ) +
54 | theme_whippr()
55 |
56 | ## P2
57 | p2 <- .residuals_tbl %>%
58 | ggplot2::ggplot(ggplot2::aes(sample = standardized_residuals)) +
59 | ggplot2::stat_qq(shape = 21, size = 4, fill = "skyblue") +
60 | ggplot2::stat_qq_line(color = "darkred", linewidth = 1) +
61 | ggplot2::labs(
62 | title = "Non-normality of Residuals and Outliers",
63 | subtitle = "Dots should be plotted along the line",
64 | y = "Standardized Residuals",
65 | x = "Theoretical Quantiles"
66 | ) +
67 | theme_whippr()
68 |
69 | ## P3
70 | p3 <- .residuals_tbl %>%
71 | ggplot2::ggplot(ggplot2::aes(.fitted, .resid)) +
72 | ggplot2::geom_point(shape = 21, size = 4, fill = "skyblue") +
73 | ggplot2::geom_hline(yintercept = 0, lty = "dashed", color = "darkred", linewidth = 1) +
74 | ggplot2::labs(
75 | title = "Homoscedasticity",
76 | subtitle = "Dots should be similar above and below the dashed line",
77 | x = "Fitted values",
78 | y = "Residuals"
79 | ) +
80 | theme_whippr()
81 |
82 | p4 <- .residuals_tbl %>%
83 | tidyr::drop_na(lag_residuals) %>%
84 | ggplot2::ggplot(ggplot2::aes(.resid, lag_residuals)) +
85 | ggplot2::geom_point(shape = 21, size = 4, fill = "skyblue") +
86 | ggplot2::geom_hline(yintercept = 0, lty = "dashed", color = "darkred", linewidth = 1) +
87 | ggplot2::labs(
88 | title = "Autocorrelation",
89 | subtitle = "Dots should be plotted randomly",
90 | x = "Residuals",
91 | y = "Residuals - i"
92 | ) +
93 | theme_whippr()
94 |
95 | out <- patchwork::wrap_plots(p1, p2, p3, p4, ncol = 2)
96 |
97 | out
98 | }
99 |
--------------------------------------------------------------------------------
/man/incremental_normalize.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/incremental.R
3 | \name{incremental_normalize}
4 | \alias{incremental_normalize}
5 | \title{Normalize incremental test data}
6 | \usage{
7 | incremental_normalize(
8 | .data,
9 | incremental_type = c("ramp", "step"),
10 | has_baseline = TRUE,
11 | baseline_length = NULL,
12 | work_rate_magic = FALSE,
13 | baseline_intensity = NULL,
14 | ramp_increase = NULL,
15 | step_start = NULL,
16 | step_increase = NULL,
17 | step_length = NULL,
18 | ...
19 | )
20 | }
21 | \arguments{
22 | \item{.data}{Data retrieved from \code{read_data()}.}
23 |
24 | \item{incremental_type}{The type of the incremental test performed. Either "ramp" or "step".}
25 |
26 | \item{has_baseline}{A boolean to indicate whether the data contains a baseline phase. This is used for an incremental test only. Default to \code{TRUE}.}
27 |
28 | \item{baseline_length}{The baseline length (in seconds) performed.}
29 |
30 | \item{work_rate_magic}{A boolean indicating whether to perform the work rate calculations. When set to \code{TRUE},
31 | it will calculate the work rate throughout a ramp or step test. In the case of a step test, it will also
32 | perform a linear transformation of the work rate.
33 | If set to \code{TRUE}, the arguments below should be given. Default to \code{FALSE}.}
34 |
35 | \item{baseline_intensity}{A numeric atomic vector indicating the work rate of the baseline. If the baseline was performed at rest, indicate \code{0}.}
36 |
37 | \item{ramp_increase}{A numeric atomic vector indicating the ramp increase in watts per minute (W/min). For example, if the ramp
38 | was \verb{30 W/min}, then pass the number \code{30} to this argument.}
39 |
40 | \item{step_start}{In case your baseline was performed at rest, you can set in this parameter at which intensity
41 | the step test started.}
42 |
43 | \item{step_increase}{A numeric atomic vector indicating the step increase, in watts. For example, if the step increase was
44 | \verb{25 W} at each step, then pass the number \code{25} to this argument.}
45 |
46 | \item{step_length}{A numeric atomic vector indicating the length (in seconds) of each step in the step incremental test.}
47 |
48 | \item{...}{Additional arguments. Currently ignored.}
49 | }
50 | \value{
51 | a \link[tibble:tibble-package]{tibble}
52 | }
53 | \description{
54 | Detect protocol phases (baseline, ramp, steps), normalize work rate, and
55 | time-align baseline phase (baseline time becomes negative).
56 | }
57 | \examples{
58 | \dontrun{
59 | ## get file path from example data
60 | path_example <- system.file("ramp_cosmed.xlsx", package = "whippr")
61 |
62 | ## read data from ramp test
63 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
64 |
65 | ## normalize incremental test data
66 | ramp_normalized <- df \%>\%
67 | incremental_normalize(
68 | .data = .,
69 | incremental_type = "ramp",
70 | has_baseline = TRUE,
71 | baseline_length = 240,
72 | work_rate_magic = TRUE,
73 | baseline_intensity = 20,
74 | ramp_increase = 25
75 | )
76 |
77 | ## get file path from example data
78 | path_example_step <- system.file("step_cortex.xlsx", package = "whippr")
79 |
80 | ## read data from step test
81 | df_step <- read_data(path = path_example_step, metabolic_cart = "cortex")
82 |
83 | ## normalize incremental test data
84 | step_normalized <- df_step \%>\%
85 | incremental_normalize(
86 | .data = .,
87 | incremental_type = "step",
88 | has_baseline = TRUE,
89 | baseline_length = 120,
90 | work_rate_magic = TRUE,
91 | baseline_intensity = 0,
92 | step_start = 50,
93 | step_increase = 25,
94 | step_length = 180
95 | )
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/NEWS.md:
--------------------------------------------------------------------------------
1 | # whippr 0.1.4
2 |
3 | * Fixed an issue with the `perform_max()` function that the `aaverage_length` argument was not being correctly considered (#15).
4 | * Fixed issue with reading data from Parvo metabolic cart (#14).
5 | * Modified default bin average method from `round` to `ceiling`.
6 |
7 | # whippr 0.1.3
8 |
9 | * Removed `usethis` dependency.
10 | * Fixed typos.
11 | * Fixed `ggplot2`, `tidyselect`, and `tibble` warnings.
12 | * Adjusted documentation as requested by CRAN.
13 |
14 | # whippr 0.1.2
15 |
16 | * Fixed a bug in `read_data.cosmed()` that made the time column to return `NA` values when the test was longer than one hour.
17 | * Added a `custom` option to `read_data()`.
18 |
19 | # whippr 0.1.1
20 |
21 | * Updated docs with `roxygen 7.2.1`.
22 |
23 | # whippr 0.1.0
24 |
25 | * General cleanup has been performed to reduce dependencies.
26 |
27 | # whippr 0.0.0.9000
28 |
29 | ## Breaking changes
30 |
31 | * The following function calls were simplified:
32 |
33 | * `vo2_kinetics()` and `detect_outliers()` = `time_column` argument not needed anymore (this is automatically taken from `read_data()` now).
34 | * `plot_outliers()` = `test_type` argument not needed anymore (this is automatically taken from `detect_outliers()` now).
35 | * `perform_kinetics()` = `time_column` and `vo2_column` arguments not needed anymore (this is automatically taken from `read_data()` and `detect_outliers()`).
36 |
37 | ## New functions/methods
38 |
39 | * Added function `read_data()` to read data from metabolic cart (COSMED and CORTEX).
40 | * Now you can specify the name of the time column, in case your system is not in English. Default is set to "t".
41 | * Added option to read data from NSpire system (thanks to [@peter__leo](https://twitter.com/peter__leo)).
42 | * Added option to read data from Parvo Medics system (thanks to [@EatsleepfitJeff](https://twitter.com/EatsleepfitJeff)).
43 | * Added option to read data from Geratherm Respiratory system (thanks to [@marcorsini61](https://twitter.com/marcorsini61))
44 |
45 | * Added function `interpolate()` to interpolate breath-by-breath data into second-by-second.
46 |
47 | * Added function `perform_average()` to perform bin- and rolling-averages.
48 |
49 | * Added `run_manual_cleaner()`.
50 |
51 | * Added testing of functions (internal modification only, not visible to the user).
52 |
53 | * Added new print method.
54 |
55 | * Added new functionality for analyzing data from incremental exercise.
56 |
57 | * Added `perform_max()` and `vo2_max()`.
58 |
59 | * Added support for the `CardioCoach` metabolic cart.
60 |
61 | ## Bug fixes
62 |
63 | * Fixed issue with `read_data()` when using the COSMED metabolic cart: previously, character columns were being coerced into `NA`(thanks @Scott-S-Hannah #4).
64 |
65 | * Added extra argument to `read_data()` to automatically fix the issue when the work rate column is coerced as a character column (thanks to [@ThibauxV](https://twitter.com/ThibauxV)).
66 |
67 | * Improved error messages in case `read_data()` cannot find the name of the time column provided.
68 |
69 | * Removed the `time_column` argument from the `interpolate()` and `perform_average()` functions. This is only necessary in `read_data()` now.
70 |
71 | * Make sure that data does not contain rows and cols with only `NA` in `interpolate()` (thanks @Scott-S-Hannah).
72 |
73 | * Fixed issue renaming work rate column in `read_data()` (thanks @Scott-S-Hannah #6).
74 |
75 | ## VO2 kinetics analyses
76 |
77 | * Added a set of tools for VO2 kinetics analyses.
78 |
79 | ## Incremental test analyses
80 |
81 | * Added a set of tools for incremental test analyses: data standardization and normalization, detection of 'bad breaths', mean response time, maximal values (i.e., VO2max, HRmax, maximal RER, etc), and ventilatory thresholds.
82 |
--------------------------------------------------------------------------------
/man/detect_outliers.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/outliers.R
3 | \name{detect_outliers}
4 | \alias{detect_outliers}
5 | \title{Detect outliers}
6 | \usage{
7 | detect_outliers(
8 | .data,
9 | test_type = c("incremental", "kinetics"),
10 | vo2_column = "VO2",
11 | cleaning_level = 0.95,
12 | cleaning_baseline_fit,
13 | protocol_n_transitions,
14 | protocol_baseline_length,
15 | protocol_transition_length,
16 | method_incremental = c("linear", "anomaly"),
17 | verbose = TRUE,
18 | ...
19 | )
20 | }
21 | \arguments{
22 | \item{.data}{Data retrieved from \code{read_data()} for a \strong{kinetics} test, or
23 | the data retrieved from \code{incremental_normalize()} for a \strong{incremental} test.}
24 |
25 | \item{test_type}{The test to be analyzed. Either 'incremental' or 'kinetics'.}
26 |
27 | \item{vo2_column}{The name (quoted) of the column containing the absolute oxygen uptake (VO2) data. Default to \code{VO2}.}
28 |
29 | \item{cleaning_level}{A numeric scalar between 0 and 1 giving the confidence level for the intervals to be calculated. Default to \code{0.95}.}
30 |
31 | \item{cleaning_baseline_fit}{For \strong{kinetics} test only. A vector of the same length as the number in \code{protocol_n_transitions}, indicating what kind of fit to perform for each baseline. Vector accepts characters either 'linear' or 'exponential'.}
32 |
33 | \item{protocol_n_transitions}{For \strong{kinetics} test only. Number of transitions performed.}
34 |
35 | \item{protocol_baseline_length}{For \strong{kinetics} test only. The length of the baseline (in seconds).}
36 |
37 | \item{protocol_transition_length}{For \strong{kinetics} test only. The length of the transition (in seconds).}
38 |
39 | \item{method_incremental}{The method to be used in detecting outliers from the
40 | incremental test. Either 'linear' or 'anomaly'. See \code{Details}.}
41 |
42 | \item{verbose}{A boolean indicating whether messages should be printed in the console. Default to \code{TRUE}.}
43 |
44 | \item{...}{Additional arguments. Currently ignored.}
45 | }
46 | \value{
47 | a \link[tibble:tibble-package]{tibble}
48 | }
49 | \description{
50 | It detects outliers based on prediction bands for the given level of confidence provided.
51 | }
52 | \details{
53 | TODO
54 | }
55 | \examples{
56 | \dontrun{
57 | ## get file path from example data
58 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
59 |
60 | ## read data
61 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
62 |
63 | ## detect outliers
64 | data_outliers <- detect_outliers(
65 | .data = df,
66 | test_type = "kinetics",
67 | vo2_column = "VO2",
68 | cleaning_level = 0.95,
69 | cleaning_baseline_fit = c("linear", "exponential", "exponential"),
70 | protocol_n_transitions = 3,
71 | protocol_baseline_length = 360,
72 | protocol_transition_length = 360,
73 | verbose = TRUE
74 | )
75 |
76 | ## get file path from example data
77 | path_example_ramp <- system.file("ramp_cosmed.xlsx", package = "whippr")
78 |
79 | ## read data from ramp test
80 | df_ramp <- read_data(path = path_example_ramp, metabolic_cart = "cosmed")
81 |
82 | ## normalize incremental test data
83 | ramp_normalized <- df_ramp \%>\%
84 | incremental_normalize(
85 | .data = .,
86 | incremental_type = "ramp",
87 | has_baseline = TRUE,
88 | baseline_length = 240,
89 | work_rate_magic = TRUE,
90 | baseline_intensity = 20,
91 | ramp_increase = 25
92 | )
93 |
94 | ## detect ramp outliers
95 | data_ramp_outliers <- detect_outliers(
96 | .data = ramp_normalized,
97 | test_type = "incremental",
98 | vo2_column = "VO2",
99 | cleaning_level = 0.95,
100 | method_incremental = "linear",
101 | verbose = TRUE
102 | )
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/R/normalize.R:
--------------------------------------------------------------------------------
1 | #' Normalize first breath
2 | #'
3 | #' This is needed specially when the data gets filtered. For example, if the data file does not only contain
4 | #' the baseline and transitions performed, we will have to normalize the time column.
5 | #' This function will make sure that in case the first breath does not start at zero, it will create a zero data point,
6 | #' duplicating the first breath. This will make sure the data does not get shifted (misalignment).
7 | #'
8 | #' @param .data Breath-by-breath data.
9 | #'
10 | #' @return a [tibble][tibble::tibble-package]
11 | #' @export
12 | normalize_first_breath <- function(.data) {
13 | first_breath <- .data %>%
14 | dplyr::select(1) %>%
15 | dplyr::slice(1) %>%
16 | dplyr::pull() %>%
17 | as.integer()
18 |
19 | if(first_breath %% 10 != 0) {
20 | row_to_add <- .data[1, ] %>%
21 | dplyr::mutate_at(1, function(x) x = first_breath - first_breath %% 10)
22 |
23 | out <- .data %>%
24 | dplyr::bind_rows(row_to_add, .)
25 | } else {
26 | out <- .data
27 | }
28 |
29 | out
30 | }
31 |
32 | #' Normalize transitions
33 | #'
34 | #' Recognizes and normalizes the time column of each transition. It will also label the transitions into: 'baseline' or 'transition'.
35 | #'
36 | #' @param .data Breath-by-breath data.
37 | #' @param protocol_n_transitions Number of transitions performed.
38 | #' @param protocol_baseline_length The length of the baseline (in seconds).
39 | #' @param protocol_transition_length The length of the transition (in seconds).
40 | #'
41 | #' @return a [tibble][tibble::tibble-package]
42 | #' @export
43 | normalize_transitions <- function(
44 | .data,
45 | protocol_n_transitions,
46 | protocol_baseline_length,
47 | protocol_transition_length
48 | ) {
49 | ## first step is to get the time that each transition ends (in seconds),
50 | ## and then apply the label to each transition
51 | info_transition <- dplyr::tibble(
52 | n_transition = seq(1, protocol_n_transitions, 1)
53 | ) %>%
54 | dplyr::mutate(
55 | end_transition = (protocol_baseline_length + protocol_transition_length) * n_transition,
56 | label = paste("transition", n_transition, sep = " ")
57 | )
58 |
59 | ## get time column name
60 | time_column <- colnames(.data)[1]
61 |
62 | ## now we normalize the first breath for the whole data
63 | ## for more information see ?normalize_first_breath()
64 | .data %>%
65 | normalize_first_breath() %>%
66 | ## normalize the whole time column in case the test didn't start at zero
67 | dplyr::mutate_at(1, function(x) x = x - min(x)) %>%
68 | ## label transitions
69 | dplyr::mutate(transition = cut(!!rlang::sym(time_column), c(0, info_transition$end_transition), labels = info_transition$label, include.lowest = TRUE)) %>%
70 | ## now we normalize the first breath for each transition
71 | ## for more information see ?normalize_first_breath()
72 | tidyr::nest_legacy(-transition) %>%
73 | dplyr::mutate(data = purrr::map(.x = data, .f = normalize_first_breath)) %>%
74 | tidyr::unnest_legacy() %>%
75 | dplyr::select(2:ncol(.), transition) %>%
76 | dplyr::group_by(transition) %>%
77 | dplyr::mutate_at(1, function(x) x = x - min(x)) %>%
78 | dplyr::ungroup() %>%
79 | dplyr::mutate(phase = ifelse(.[[1]] <= protocol_baseline_length, "baseline", "transition"))
80 | }
81 |
82 | #' Normalize time column
83 | #'
84 | #' Normalizes the the time column such that the baseline phase has negative time values. Point zero will then represent the start of the transition phase.
85 | #'
86 | #' @param .data Breath-by-breath data.
87 | #' @param protocol_baseline_length The length of the baseline (in seconds).
88 | #'
89 | #' @return a [tibble][tibble::tibble-package]
90 | #' @export
91 | normalize_time <- function(.data, protocol_baseline_length) {
92 | out <- .data %>%
93 | dplyr::mutate_at(1, function(x) x = x - protocol_baseline_length)
94 |
95 | out
96 | }
97 |
--------------------------------------------------------------------------------
/R/helpers-incremental.R:
--------------------------------------------------------------------------------
1 | #' Work rate for a ramp-incremental test
2 | #'
3 | #' This function produces the work rate throughout a ramp-incremental test given the procotol
4 | #'
5 | #' @param .data The data with recognized protocol phases
6 | #' @param baseline_intensity The baseline intensity
7 | #' @param ramp_increase The ramp increase, in watts per minute
8 | #'
9 | #' @return a [tibble][tibble::tibble-package]
10 | #' @keywords internal
11 | work_rate_ramp <- function(
12 | .data,
13 | baseline_intensity,
14 | ramp_increase
15 | ) {
16 | model <- lm(work_rate ~ time, data = data.frame(work_rate = c(baseline_intensity, baseline_intensity + ramp_increase), time = c(0, 60)))
17 |
18 | slope <- model$coefficients[2]
19 |
20 | ## get time column name
21 | time_column <- colnames(.data)[1]
22 |
23 | out <- .data %>%
24 | dplyr::mutate(work_rate = dplyr::case_when(
25 | protocol_phase == "baseline" ~ baseline_intensity,
26 | protocol_phase == "ramp" ~ baseline_intensity + slope * !!rlang::sym(time_column)
27 | ))
28 |
29 | out
30 | }
31 |
32 | #' Work rate for a step-incremental test
33 | #'
34 | #' This function produces the work rate throughout a step-incremental test given the protocol
35 | #' This will retrieve both the 'original' work rates, and also will perform a 'linearization' of the steps.
36 | #'
37 | #' @param .data The data with recognized protocol phases
38 | #' @param baseline_intensity The baseline intensity
39 | #' @param step_start In case the step test started in a different work rate than baseline
40 | #' @param step_increase The step in increase, in watts per step
41 | #' @param step_length The length, in seconds, of each step
42 | #'
43 | #' @return a [tibble][tibble::tibble-package]
44 | #' @keywords internal
45 | work_rate_step <- function(
46 | .data,
47 | baseline_intensity,
48 | step_start,
49 | step_increase,
50 | step_length
51 | ) {
52 | ## check if forcats is installed and have a prompt to install it.
53 | rlang::check_installed("forcats")
54 |
55 | if(!is.null(step_start)) {
56 | begin_intensity <- step_start
57 | } else {
58 | begin_intensity <- baseline_intensity
59 | }
60 |
61 | ## this will make sure that the final intensity of each step
62 | ## corresponds to the step intensity
63 | if(begin_intensity == 0) {
64 | # linearization
65 | model <- lm(work_rate ~ time, data = data.frame(work_rate = c(begin_intensity, begin_intensity + step_increase), time = c(0, step_length)))
66 | } else {
67 | # linearization
68 | model <- lm(work_rate ~ time, data = data.frame(work_rate = c(begin_intensity - step_increase, begin_intensity), time = c(0, step_length)))
69 | }
70 |
71 | slope <- model$coefficients[2]
72 |
73 | ## get time column name
74 | time_column <- colnames(.data)[1]
75 |
76 | ## this will make sure that the final intensity of each step
77 | ## corresponds to the step intensity
78 | if(begin_intensity == 0) {
79 | data_linearized <- .data %>%
80 | dplyr::mutate(work_rate = dplyr::case_when(
81 | protocol_phase == "baseline" ~ baseline_intensity,
82 | protocol_phase == "step" ~ begin_intensity + slope * !!rlang::sym(time_column)
83 | ))
84 | } else {
85 | data_linearized <- .data %>%
86 | dplyr::mutate(work_rate = dplyr::case_when(
87 | protocol_phase == "baseline" ~ baseline_intensity,
88 | protocol_phase == "step" ~ begin_intensity - step_increase + slope * !!rlang::sym(time_column)
89 | ))
90 | }
91 |
92 | # steps
93 | data_steps <- data_linearized %>%
94 | dplyr::mutate(step = paste0("step_", (!!rlang::sym(time_column) %/% step_length + 1)),
95 | step = forcats::as_factor(step))
96 |
97 | n_steps <- length(unique(data_steps$step))
98 |
99 | out <- data_steps %>%
100 | tidyr::nest(data = -step) %>%
101 | dplyr::mutate(step_work_rate = c(baseline_intensity, seq(begin_intensity, n_steps * step_increase + baseline_intensity, step_increase))) %>%
102 | tidyr::unnest(cols = data) %>%
103 | dplyr::relocate(step, .after = dplyr::last_col())
104 |
105 | out
106 | }
107 |
--------------------------------------------------------------------------------
/man/vo2_max.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/max.R
3 | \name{vo2_max}
4 | \alias{vo2_max}
5 | \title{VO2max}
6 | \usage{
7 | vo2_max(
8 | .data,
9 | vo2_column = "VO2",
10 | vo2_relative_column = NULL,
11 | heart_rate_column = NULL,
12 | rer_column = NULL,
13 | detect_outliers = TRUE,
14 | average_method = c("bin", "rolling"),
15 | average_length = 30,
16 | mrt,
17 | plot = TRUE,
18 | verbose = TRUE,
19 | ...
20 | )
21 | }
22 | \arguments{
23 | \item{.data}{Data retrieved from \code{read_data()}.}
24 |
25 | \item{vo2_column}{The name (quoted) of the column containing the absolute oxygen uptake (VO2) data. Default to \code{"VO2"}.}
26 |
27 | \item{vo2_relative_column}{The name (quoted) of the column containing the relative to body weight oxygen uptake (VO2) data. Default to \code{NULL}.}
28 |
29 | \item{heart_rate_column}{The name (quoted) of the column containing the heart rate (HR) data. Default to \code{NULL}. If \code{NULL}, this parameter will not be calculated.}
30 |
31 | \item{rer_column}{The name (quoted) of the column containing the respiratory exchange ratio (RER) data. Default to \code{NULL}. If \code{NULL}, this parameter will not be calculated.}
32 |
33 | \item{detect_outliers}{A boolean indicating whether to detect outliers. Default to \code{TRUE}.}
34 |
35 | \item{average_method}{The average method to be used for VO2max calculation. One of \code{bin} or \code{rolling}.}
36 |
37 | \item{average_length}{The length, in seconds, of the average to be used. For example, if \code{average_method = bin}, and \code{average_length = 30}, it will perform a 30-s bin-average.}
38 |
39 | \item{mrt}{A boolean indicating whether to calculate the mean response time. To be implemented soon <- currently ignored.}
40 |
41 | \item{plot}{A boolean indicating whether to produce a plot with the summary results. Default to \code{TRUE}.}
42 |
43 | \item{verbose}{A boolean indicating whether messages should be printed in the console. Default to \code{TRUE}.}
44 |
45 | \item{...}{Additional arguments passed onto \code{incremental_normalize()}, \code{detect_outliers()} if \code{detect_outliers = TRUE}, and \code{incremental_mrt()} if \code{mrt = TRUE}.}
46 | }
47 | \value{
48 | a \link[tibble:tibble-package]{tibble} containing one row and the following columns:
49 | \item{VO2max_absolute}{The absolute VO2max.}
50 | \item{VO2max_relative}{The relative VO2max.}
51 | \item{POpeak}{The peak power output.}
52 | \item{HRmax}{The maximal heart rate.}
53 | \item{RERmax}{The maximal RER.}
54 | \item{plot}{The plot, if \code{plot = TRUE}.}
55 | }
56 | \description{
57 | It performs the whole process of the VO2max data analysis, which includes:
58 | data standardization and normalization according to incremental protocol (\code{incremental_normalize()}),
59 | 'bad breaths' detection (\code{detect_outliers()}),
60 | mean response time calculation (\code{incremental_mrt()}) (currently ignored),
61 | and maximal values calculation (VO2, PO, HR, RER) (\code{perform_max()}).
62 | }
63 | \details{
64 | TODO
65 | }
66 | \examples{
67 | \dontrun{
68 | ## get file path from example data
69 | path_example <- system.file("ramp_cosmed.xlsx", package = "whippr")
70 |
71 | ## read data from ramp test
72 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
73 |
74 | ## normalize incremental test data
75 | ramp_normalized <- df \%>\%
76 | incremental_normalize(
77 | .data = .,
78 | incremental_type = "ramp",
79 | has_baseline = TRUE,
80 | baseline_length = 240,
81 | work_rate_magic = TRUE,
82 | baseline_intensity = 20,
83 | ramp_increase = 25
84 | )
85 |
86 | ## detect outliers
87 | data_ramp_outliers <- detect_outliers(
88 | .data = ramp_normalized,
89 | test_type = "incremental",
90 | vo2_column = "VO2",
91 | cleaning_level = 0.95,
92 | method_incremental = "linear",
93 | verbose = TRUE
94 | )
95 |
96 | ## analyze VO2max
97 | perform_max(
98 | .data = data_ramp_outliers,
99 | vo2_column = "VO2",
100 | vo2_relative_column = "VO2/Kg",
101 | heart_rate_column = "HR",
102 | rer_column = "R",
103 | average_method = "bin",
104 | average_length = 30,
105 | plot = TRUE,
106 | verbose = FALSE
107 | )
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/R/averages.R:
--------------------------------------------------------------------------------
1 | #' Perform average on second-by-second data
2 | #'
3 | #' This function performs either a bin- or a rolling-average on the interpolated data.
4 | #' You must specify the \code{type} of the average before continuing.
5 | #'
6 | #' @param .data The second-by-second data retrieved from \code{interpolate()}.
7 | #' @param type The type of the average to perform. Either \code{bin}, \code{rolling}, or \code{ensemble}.
8 | #' @param bins If bin-average is chosen, here you can specify the size of the bin-average, in seconds. Default to 30-s bin-average.
9 | #' @param bin_method Method for determining bin boundaries when \code{type = "bin"}.
10 | #' One of \code{"ceiling"} (default), \code{"round"}, or \code{"floor"}.
11 | #' \code{"ceiling"} is recommended as it ensures no data points are excluded
12 | #' from the analysis by always rounding up to the next bin boundary.
13 | #' @param rolling_window If rolling-average is chosen, here you can specify the rolling-average window, in seconds. Default to 30-s rolling-average.
14 | #'
15 | #' @return a [tibble][tibble::tibble-package]
16 | #' @export
17 | #'
18 | #' @details
19 | #' Ensemble average is used in VO2 kinetics analysis, where a series of transitions from baseline to
20 | #' the moderate/heavy/severe intensity-domain is ensembled averaged into a single 'bout' for further data processing.
21 | #'
22 | #' When using bin averaging, the \code{bin_method} parameter controls how time points are assigned to bins:
23 | #' \itemize{
24 | #' \item \code{"ceiling"}: Rounds up to the next bin boundary (recommended)
25 | #' \item \code{"round"}: Rounds to the nearest bin boundary
26 | #' \item \code{"floor"}: Rounds down to the previous bin boundary
27 | #' }
28 | #'
29 | #' @examples
30 | #' \dontrun{
31 | #' ## get file path from example data
32 | #' path_example <- system.file("example_cosmed.xlsx", package = "whippr")
33 | #'
34 | #' ## read data
35 | #' df <- read_data(path = path_example, metabolic_cart = "cosmed")
36 | #'
37 | #' ## interpolate and perform 30-s bin-average
38 | #' df %>%
39 | #' interpolate() %>%
40 | #' perform_average(type = "bin", bins = 30)
41 | #'
42 | #' ## interpolate and perform 30-s rolling-average
43 | #' df %>%
44 | #' interpolate() %>%
45 | #' perform_average(type = "rolling", rolling_window = 30)
46 | #' }
47 | perform_average <- function(.data, type = c("bin", "rolling", "ensemble"), bins = 30, bin_method = c("ceiling", "round", "floor"), rolling_window = 30) {
48 | if(missing(type))
49 | stop("You must specify the type of average you would like to perform.", call. = FALSE)
50 |
51 | type <- match.arg(type)
52 | bin_method <- match.arg(bin_method)
53 |
54 | class(.data) <- type
55 |
56 | UseMethod("perform_average", .data)
57 | }
58 |
59 | #' @export
60 | perform_average.bin <- function(.data, type = c("bin", "rolling", "ensemble"), bins = 30, bin_method = c("ceiling", "round", "floor"), rolling_window = 30) {
61 | bin_method <- match.arg(bin_method)
62 | # select the appropriate rounding function
63 | rounding_func <- switch(bin_method,
64 | "ceiling" = ceiling,
65 | "round" = round,
66 | "floor" = floor)
67 | ## first make sure data only contains numeric columns
68 | data_num <- .data %>%
69 | dplyr::select_if(is.numeric)
70 |
71 | out <- data_num %>%
72 | dplyr::group_by_at(1, function(x) rounding_func(x / bins) * bins) %>%
73 | dplyr::summarise_all(mean, na.rm = TRUE)
74 |
75 | metadata <- attributes(.data)
76 | metadata$data_status <- glue::glue("averaged data - {bins}-s bins")
77 |
78 | out <- new_whippr_tibble(out, metadata)
79 |
80 | out
81 | }
82 |
83 | #' @export
84 | perform_average.rolling <- function(.data, type = c("bin", "rolling", "ensemble"), bins = 30, bin_method = c("ceiling", "round", "floor"), rolling_window = 30) {
85 | ## first make sure data only contains numeric columns
86 | data_num <- .data %>%
87 | dplyr::select_if(is.numeric)
88 |
89 | out <- data_num %>%
90 | zoo::rollmean(x = ., k = rolling_window) %>%
91 | dplyr::as_tibble()
92 |
93 | metadata <- attributes(.data)
94 | metadata$data_status <- glue::glue("averaged data - {rolling_window}-s rolling average")
95 |
96 | out <- new_whippr_tibble(out, metadata)
97 |
98 | out
99 | }
100 |
101 | #' @export
102 | perform_average.ensemble <- function(.data, type = c("bin", "rolling", "ensemble"), bins = 30, bin_method = c("ceiling", "round", "floor"), rolling_window = 30) {
103 | ## first make sure data only contains numeric columns
104 | data_num <- .data %>%
105 | dplyr::select_if(is.numeric)
106 |
107 | out <- data_num %>%
108 | dplyr::group_by_at(1) %>%
109 | dplyr::summarise_all(mean, na.rm = TRUE)
110 |
111 | metadata <- attributes(.data)
112 | metadata$data_status <- "ensemble-averaged data"
113 |
114 | out <- new_whippr_tibble(out, metadata)
115 |
116 | out
117 | }
118 |
--------------------------------------------------------------------------------
/R/helpers-outliers.R:
--------------------------------------------------------------------------------
1 | #' Linear method for detecting outliers from an incremental test
2 | #'
3 | #' Function for internal use only. It will not be exported.
4 | #'
5 | #' @param .data The data retrieved from `incremental_normalize()`.
6 | #' @param time_column The name (quoted) of the column containing the time. Depending on the language of your system, this column might not be "t". Therefore, you may specify it here.
7 | #' @param vo2_column The name (quoted) of the column containing the absolute oxygen uptake (VO2) data.
8 | #' @param cleaning_level A numeric scalar between 0 and 1 giving the confidence level for the intervals to be calculated.
9 | #'
10 | #' @return a [tibble][tibble::tibble-package]
11 | #' @keywords internal
12 | outliers_linear <- function(
13 | .data,
14 | time_column,
15 | vo2_column,
16 | cleaning_level
17 | ) {
18 | ## set linear formula for each phase
19 | bsln_formula <- glue::glue("{vo2_column} ~ 1")
20 | incremental_formula <- glue::glue("{vo2_column} ~ {time_column}")
21 |
22 | out <- .data %>%
23 | ## separate each phase
24 | tidyr::nest(data = -protocol_phase) %>%
25 | dplyr::mutate(
26 | ## set the formula
27 | formula_model =
28 | dplyr::case_when(
29 | protocol_phase == "baseline" ~ bsln_formula,
30 | TRUE ~ incremental_formula
31 | ),
32 | ## set the model
33 | model = purrr::map2(
34 | .x = formula_model,
35 | .y = data,
36 | .f = ~ lm(formula = .x, data = .y)
37 | ),
38 | model_augmented = purrr::map(
39 | .x = model,
40 | .f = ~ broom::augment(.x)
41 | )
42 | ) %>%
43 | ## column not needed anymore
44 | dplyr::select(-formula_model) %>%
45 | ## delete additional columns to needed in augmented data
46 | dplyr::mutate(
47 | model_augmented = purrr::map(
48 | .x = model_augmented,
49 | .f = ~ dplyr::select(.x, -dplyr::any_of(c(time_column, vo2_column)))
50 | ),
51 | model_augmented = purrr::map2(
52 | .x = data,
53 | .y = model_augmented,
54 | .f = ~ dplyr::mutate(.y, x = .x[[time_column]], y = .x[[vo2_column]]) %>% dplyr::select(x, y, dplyr::everything())
55 | ),
56 | ## get confidence bands
57 | conf = purrr::map(
58 | .x = model,
59 | .f = ~ predict.lm(object = .x, interval = "confidence", level = cleaning_level) %>%
60 | dplyr::as_tibble() %>%
61 | dplyr::select(-1) %>%
62 | dplyr::rename_all(~ c("lwr_conf", "upr_conf"))
63 | ),
64 | ## get prediction bands
65 | pred = purrr::map(
66 | .x = model,
67 | .f = ~ suppressWarnings(predict.lm(object = .x, interval = "prediction", level = cleaning_level)) %>%
68 | dplyr::as_tibble() %>%
69 | dplyr::select(-1) %>%
70 | dplyr::rename_all(~ c("lwr_pred", "upr_pred"))
71 | )
72 | ) %>%
73 | dplyr::select(-model) %>%
74 | tidyr::unnest(cols = c(data:pred)) %>%
75 | dplyr::select(2:ncol(.), protocol_phase) %>%
76 | ## if VO2 is above or below the prediction bands, classify the breath as an outlier
77 | dplyr::mutate(
78 | outlier = ifelse(y >= lwr_pred & y <= upr_pred, "no", "yes")
79 | )
80 |
81 | out
82 | }
83 |
84 | #' Anomaly method for detecting outliers from an incremental test
85 | #'
86 | #' Function for internal use only. It will not be exported.
87 | #'
88 | #' @param .data The data retrieved from `incremental_normalize()`.
89 | #' @param time_column The name (quoted) of the column containing the time. Depending on the language of your system, this column might not be "t". Therefore, you may specify it here.
90 | #' @param vo2_column The name (quoted) of the column containing the absolute oxygen uptake (VO2) data.
91 | #' @param cleaning_level A numeric scalar between 0 and 1 giving the confidence level for the intervals to be calculated.
92 | #'
93 | #' @return a [tibble][tibble::tibble-package]
94 | #' @keywords internal
95 | outliers_anomaly <- function(
96 | .data,
97 | time_column,
98 | vo2_column,
99 | cleaning_level
100 | ) {
101 | ## check if anomalize is installed
102 | rlang::check_installed("anomalize")
103 |
104 | ## set alpha
105 | alpha <- 1 - cleaning_level
106 |
107 | outliers_df <- .data %>%
108 | dplyr::mutate_at(1, function(x) x = lubridate::as_date(x)) %>%
109 | anomalize::time_decompose(target = !!rlang::sym(vo2_column), method = "stl", message = FALSE) %>%
110 | anomalize::anomalize(target = remainder, method = "iqr", alpha = alpha) %>%
111 | anomalize::time_recompose() %>%
112 | dplyr::select(anomaly:recomposed_l2) %>%
113 | dplyr::rename_all(~ c("outlier", "lwr_pred", "upr_pred")) %>%
114 | dplyr::mutate(outlier = stringr::str_to_lower(outlier))
115 |
116 | ## set time and vo2 columns as x and y
117 | ## this is to make simpler to plot it later on
118 | x_and_y <- .data %>%
119 | dplyr::select(!!rlang::sym(time_column), !!rlang::sym(vo2_column)) %>%
120 | dplyr::rename_all(~ c("x", "y"))
121 |
122 | out <- dplyr::bind_cols(.data, x_and_y, outliers_df)
123 |
124 | out
125 | }
126 |
--------------------------------------------------------------------------------
/README.Rmd:
--------------------------------------------------------------------------------
1 | ---
2 | output: github_document
3 | ---
4 |
5 |
6 |
7 | ```{r, include = FALSE}
8 | knitr::opts_chunk$set(
9 | collapse = TRUE,
10 | comment = "#>",
11 | fig.path = "man/figures/README-",
12 | out.width = "100%"
13 | )
14 | ```
15 |
16 | # whippr
17 |
18 |
19 | [](https://lifecycle.r-lib.org/articles/stages.html#stable)
20 | [](https://CRAN.R-project.org/package=whippr)
21 | [](https://app.codecov.io/gh/fmmattioni/whippr?branch=master)
22 | [](https://github.com/fmmattioni/whippr/actions/workflows/R-CMD-check.yaml)
23 |
24 |
25 | The goal of `whippr` is to provide a set of tools for manipulating gas exchange data from cardiopulmonary exercise testing.
26 |
27 | ## Why `whippr`?
28 |
29 | The name of the package is in honor of [Prof. Brian J Whipp](https://erj.ersjournals.com/content/39/1/1) and his invaluable contribution to the field of exercise physiology.
30 |
31 | ## Installation
32 |
33 | You can install the development version of `whippr` from [Github](https://github.com/fmmattioni/whippr) with:
34 |
35 | ``` r
36 | # install.packages("remotes")
37 | remotes::install_github("fmmattioni/whippr")
38 | ```
39 |
40 | ## Use
41 |
42 | ### Read data
43 |
44 | ```{r}
45 | library(whippr)
46 |
47 | ## example file that comes with the package for demonstration purposes
48 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
49 |
50 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
51 |
52 | df
53 | ```
54 |
55 | ### Interpolate
56 |
57 | ```{r}
58 | df %>%
59 | interpolate()
60 | ```
61 |
62 | ### Perform averages
63 |
64 | #### Bin-average
65 |
66 | ```{r}
67 | ## example of performing 30-s bin-averages
68 | df %>%
69 | interpolate() %>%
70 | perform_average(type = "bin", bins = 30)
71 | ```
72 |
73 | #### Rolling-average
74 |
75 | ```{r}
76 | ## example of performing 30-s rolling-averages
77 | df %>%
78 | interpolate() %>%
79 | perform_average(type = "rolling", rolling_window = 30)
80 | ```
81 |
82 |
83 | ### Perform VO2 kinetics analysis
84 |
85 | ```{r}
86 | results_kinetics <- vo2_kinetics(
87 | .data = df,
88 | intensity_domain = "moderate",
89 | vo2_column = "VO2",
90 | protocol_n_transitions = 3,
91 | protocol_baseline_length = 360,
92 | protocol_transition_length = 360,
93 | cleaning_level = 0.95,
94 | cleaning_baseline_fit = c("linear", "exponential", "exponential"),
95 | fit_level = 0.95,
96 | fit_bin_average = 5,
97 | fit_phase_1_length = 20,
98 | fit_baseline_length = 120,
99 | fit_transition_length = 240,
100 | verbose = TRUE
101 | )
102 | ```
103 |
104 | ### Perform VO2max analysis
105 |
106 | ```{r}
107 | df_incremental <- read_data(path = system.file("ramp_cosmed.xlsx", package = "whippr"), metabolic_cart = "cosmed")
108 |
109 | vo2_max(
110 | .data = df_incremental, ## data from `read_data()`
111 | vo2_column = "VO2",
112 | vo2_relative_column = "VO2/Kg",
113 | heart_rate_column = "HR",
114 | rer_column = "R",
115 | detect_outliers = TRUE,
116 | average_method = "bin",
117 | average_length = 30,
118 | plot = TRUE,
119 | verbose = TRUE,
120 | ## arguments for `incremental_normalize()`
121 | incremental_type = "ramp",
122 | has_baseline = TRUE,
123 | baseline_length = 240, ## 4-min baseline
124 | work_rate_magic = TRUE, ## produce a work rate column
125 | baseline_intensity = 20, ## baseline was performed at 20 W
126 | ramp_increase = 25, ## 25 W/min ramp
127 | ## arguments for `detect_outliers()`
128 | test_type = "incremental",
129 | cleaning_level = 0.95,
130 | method_incremental = "linear"
131 | )
132 | ```
133 |
134 | ## Metabolic carts currently supported
135 |
136 | * [COSMED](https://www.cosmed.com/en/)
137 | * [CORTEX](https://cortex-medical.com/EN)
138 | * [NSpire](https://www.pressebox.de/pressemitteilung/nspire-health-gmbh/ZAN-100-Diagnostische-Spirometrie/boxid/745555)
139 | * Parvo Medics
140 | * [Geratherm Respiratory](https://www.geratherm-respiratory.com/product-groups/cpet/)
141 | * [CardioCoach](https://korr.com/go/cardiocoach/)
142 |
143 | ## Online app
144 |
145 | Would you like to perform VO2 kinetics analyses but don't know R? No problem! You can use our online app: [VO2 Kinetics App](https://exphyslab.com/kinetics/)
146 |
147 | ## Code of Conduct
148 |
149 | Please note that this project is released with a [Contributor Code of Conduct](https://www.contributor-covenant.org/version/1/0/0/code-of-conduct.html).
150 | By participating in this project you agree to abide by its terms.
151 |
152 |
5 |
6 |
7 |
8 | [](https://lifecycle.r-lib.org/articles/stages.html#stable)
10 | [](https://CRAN.R-project.org/package=whippr)
12 | [](https://app.codecov.io/gh/fmmattioni/whippr?branch=master)
14 | [](https://github.com/fmmattioni/whippr/actions/workflows/R-CMD-check.yaml)
15 |
16 |
17 | The goal of `whippr` is to provide a set of tools for manipulating gas
18 | exchange data from cardiopulmonary exercise testing.
19 |
20 | ## Why `whippr`?
21 |
22 | The name of the package is in honor of [Prof. Brian J
23 | Whipp](https://erj.ersjournals.com/content/39/1/1) and his invaluable
24 | contribution to the field of exercise physiology.
25 |
26 | ## Installation
27 |
28 | You can install the development version of `whippr` from
29 | [Github](https://github.com/fmmattioni/whippr) with:
30 |
31 | ``` r
32 | # install.packages("remotes")
33 | remotes::install_github("fmmattioni/whippr")
34 | ```
35 |
36 | ## Use
37 |
38 | ### Read data
39 |
40 | ``` r
41 | library(whippr)
42 |
43 | ## example file that comes with the package for demonstration purposes
44 | path_example <- system.file("example_cosmed.xlsx", package = "whippr")
45 |
46 | df <- read_data(path = path_example, metabolic_cart = "cosmed")
47 |
48 | df
49 | #> # Metabolic cart: COSMED
50 | #> # Data status: raw data
51 | #> # Time column: t
52 | #> # A tibble: 754 × 119
53 | #> t Rf VT VE VO2 VCO2 O2exp CO2exp `VE/VO2` `VE/VCO2` `VO2/Kg`
54 | #>