The initial release of analyse_kinetics() and family of
kinetics modelling functions!
analyse_kinetics()analyse_kinetics() fits oxygenation response
kinetics with parametric and non-parametric methods. It accepts a single
“mnirs” data frame, a list of data frames, or a grouped data
frame, analyses nirs_channels in each interval, and returns
a formatted table of results.
See ?analyse_kinetics for the canonical
(i.e. human-verified) documentation of all methods, arguments, and
returned objects.
analyse_kinetics(
data,
nirs_channels = c(smo2_left, smo2_right),
method = "monoexponential",
use_TD = TRUE, ## use time delay parameter
direction = "auto" ## auto-detect response direction
) |>
print() |> ## the formatted table prints the coefficients
plot() ## plot observations & fitted datamethod argument selects the kinetics model. Each has
its own additional arguments:
"response_time" — non-parametric fractional
(e.g. 50%) response time.
"peak_slope" — peak rolling linear least-squares
regression slope.
"monoexponential" — 3- or 4-parameter exponential
curve fit via stats::nls().
"exponential_drift" — two-phase fast monoexponential
primary response plus slow linear secondary drift.
"biexponential" — two-phase fast primary and slow
secondary exponential phases.
"sigmoidal" — 4-parameter symmetric generalised
logistic or Gompertz-family curve.
"sigmoidal_drift" — two-phase fast sigmoidal primary
response plus slow linear secondary drift.
Most arguments can be supplied globally or per-channel and
per-interval. However, method itself currently only accepts
a single global model for all channels.
Results are returned as a structured list of class “mnirs_kinetics”, containing:
method: the selected kinetics model.model: the lm or nls
objects.coefficients: resultant model parameters.data: the input data augmented with
*_fitted columns per nirs_channel.interval_times: start_times and
end_times of the analysed intervals.diagnostics: fit quality and model validation
parameters used to evaluate and compare model fits.channel_args: selected per-channel and per-interval
args.warnings: any warning and error messages generated
during fitting.call: the matched call.print.mnirs_kinetics() returns a formatted
coefficients table, and plot.mnirs_kinetics() displays the
observed data overlaid with fitted curves for each channel and
interval.
The individual fitting methods called by
analyse_kinetics() can be called directly outside of the
“mnirs” data structure:
response_time() and peak_slope() estimate
kinetics directly from a numeric vector x over
t, and return a named list of coefficients (with the
lm model object for peak_slope()).peak_slope(x, t, width = 5, direction = "auto")
response_time(x, t, response_fraction = c(0.5, 0.632))monoexponential(), exponential_drift(),
biexponential(), logistic(),
gompertz(), gompertz_left(), and
sigmoidal_drift() contain the parametric equations for each
model response curves. They can be used to construct a pure curve from
explicit parameters, to simulate data, or plotting a fitted model.t <- 1:100
monoexponential(t, A = 10, B = 100, tau = 8, TD = 15)
sigmoidsl(t, A = 10, B = 100, xmid = 30, slope = 4)SSmonoexponential(),
SSexponential_drift(), SSbiexponential(),
SSlogistic(), SSgompertz(),
SSgompertz_left(), and SSsigmoidal_drift() are
the matching self-starting (selfStart) wrappers, which
generate their own initial parameter estimates and can be fit directly
with stats::nls().nls(x ~ SSmonoexponential(t, A, B, tau, TD), data = data)
nls(x ~ SSlogistic(t, A, B, xmid, slope), data = data)An emerging method using in mNIRS research, a series of repeated
brief occlusions can be used to estimate the recovery rate of muscle
oxygen uptake from NIRS channels, as a proxy for muscle oxidative
capacity. This method can be performed in {mnirs} using
recursive calls to analyse_kinetics():
A sequence of data frames containing occlusion intervals
(i.e. extracted with extract_intervals()) can be passed to
analyse_kinetics(method = "peak_slope") with appropriate
arguments.
The result can be passed directly to another call of
analyse_kinetics(), with resulting coefficients supplied
explicitly as time_channel and nirs_channels.
method will usually be selected as
"monoexponential" to determine the rate constant
(k) of mV̇O2 recovery (also see Articles
below).
## fit an exponential through the peak slopes of successive occlusions
analyse_kinetics(
occlusion_intervals,
nirs_channels = hhb,
method = "peak_slope",
span = 3,
) |>
print() |> ## print intermediate results and pass along
analyse_kinetics(
nirs_channels = slope,
time_channel = peak_slope_time,
method = "monoexponential",
group_intervals = list(trial1 = 1:10, trial2 = 11:20)
)correct_blood_volume() is used to normalise NIRS
components signals — i.e. oxy[haem] and deoxy[haem] — for
changes in total[haem], which is a proxy for local blood
volume/perfusion. This can be done before further analysis, to isolate
metabolic O2 from mechanical haemodynamics. See
?correct_blood_volume.“Analysing muscle oxidative capacity with mnirs” walks through a full arterial occlusion OxCap analysis: correcting for blood volume, extracting occlusion intervals, finding peak deoxy[haem] slopes, and fitting a monoexponential through the slope estimates to estimate the mV̇O2 recovery rate constant k.
“Reading and analysing PIONIRS data with mnirs”
demonstrates reading new TD-NIRS .ftn &
.ftn2 files, and compares different fit methods to
occlusion reoxygenation kinetics.
read_mnirs()Reading all files, in particular .csv, is faster and
allocates less memory.
Files can now be read from PIONIRS NIRSBOX, an advanced time-domain TD-NIRS device.
PIONIRS explors file types .ftn and
.ftn2 for single- and dual-channel TD-NIRS,
respectively.
read_mnirs() will automatically detect channels
StO2, Time, and
TagLabel.
Example file pionirs_occlusion.ftn2 can be called
with example_mnirs() (Thanks to Marianna, Dr. Porcelli, and
PIONIRS for the demo files).
example_mnirs("pionirs")
#> [1] "<R library>/mnirs/inst/extdata/pionirs_occlusion.ftn2"Artinis Oxysoft file exports are now automatically read more consistently, using the file metadata and Legend to rename channels:
“(Sample number)” (column 1) is renamed
“sample” with a derived “time” column which is set to
time_channel.
“(Event)” (the last numbered column with event markers)
is renamed “event” and is set as event_channel.
The trailing un-numbered column with event labels is renamed
“labels”, and can be explicitly renamed:
event_channel = c(labels = "labels").
All other channels in the Legend are renamed and
returned as nirs_channels by default with clean, lower case
names (e.g. “Rx1 - Tx1 O2Hb” is renamed as
“rx1_tx1_o2hb”).
Channels can be renamed from either their literal Legend
names; e.g. nirs_channels = c(o2hb = 2),
c(o2hb = "rx1_tx1_o2hb"), or
c(o2hb = "Rx1 - Tx1 O2Hb").
create_mnirs_data() can now rename
nirs_channels, time_channel, and
event_channel and add the renamed column names to
metadata.
df <- create_mnirs_data(
PIONIRS_ftn2,
nirs_channels = c(o2hb = "O2Hb(CH1)", hhb = "HHb(CH1)", thb = "THb(CH1)"),
time_channel = c(time = "Time"),
event_channel = c(labels = "TagLabel")
)
attr(df, "nirs_channels")
# [1] "o2hb" "hhb" "thb" extract_intervals() now accepts a list of multiple
start and/or end values with mixed
by_time(), by_label(), by_lap(),
or by_sample(). Intervals are matched by user-specified
order.## combine multiple specification types for one boundary
extract_intervals(
data,
start = list(by_lap(2), by_time(400)),
end = list(by_lap(3), by_label("10-min marker"))
)extract_intervals() Also now properly retains
event_channel column in ensemble-averaged intervals
(group_intervals = "ensemble" or custom groups).
plot_mnirs(): small adjustments to plot spacing
& point sizes.
palette_mnirs() now returns unnamed colours, which
was disrupting use with
ggplot2::scale_colour_manual().
custom_colours <- c(
smo2_left_vl = palette_mnirs("pink"),
smo2_right_vl = palette_mnirs("light blue"),
smo2_left_rf = palette_mnirs("purple"),
smo2_right_rf = palette_mnirs("dark blue")
)
# smo2_left_vl smo2_right_vl smo2_left_rf smo2_right_rf
# "#ff80ff" "#0080ff" "#9f79ee" "#00468Bff"
plot(result) +
scale_colour_manual(
values = c(
smo2_left_vl = palette_mnirs("pink"),
smo2_right_vl = palette_mnirs("light blue"),
smo2_left_rf = palette_mnirs("purple"),
smo2_right_rf = palette_mnirs("dark blue")
)
)print.mnirs() now returns its object invisibly, so can
be called incrementally within a function pipeline (which I just learned
was possible!).read_mnirs(...) |>
print() |> ## intermediate view data frame
extract_intervals(...) |>
print() |> ## view returned list of data frames
plot() ## and plot those results“README” and “Reading and Cleaning Data with mnirs” vignette edited with updated functionality and consistent formatting.
Included example “moxy_intervals.csv” modified
“Lap” column coincides with intervals start & end, for
testing with extract_intervals().
shift_mnirs() now properly excludes partial windows
at data edges, where fewer samples can bias calculation of
“min” or “max” shift values on noise.
replace_mnirs(), shift_mnirs(), and
filter_moving_average() should gain a serious performance
improvement when calculating rolling means, at the cost of negligible
loss of precision on the order of ± ~1e-11.
Lists of data frames exported from core functions now contain
class = "mnirs" and should now plot()
properly.
This minor version update includes mostly internal refactoring, but
enough user-facing changes and a few breaking deprecations, so that it’s
more than just a patch. This version lays the foundation for the
incoming (hopefully soon) mnirs analyse_kinetics
suite of functions.
Core processing functions (resample_mnirs(),
replace_mnirs(), filter_mnirs(),
shift_mnirs(), and rescale_mnirs()) now accept
as the data input either a single data frame, a list of
data frames, or a grouped data frame (requires {dplyr}).
Single data frames are processed and returned directly, as previously.
Listed and grouped data frames are each processed independently and
returned as a named list.
extract_intervals() now also accepts a list of data
frames or a grouped data frame and returns one flattened, named list of
extracted intervals.
replace_mnirs(
data = list(df1, df2),
outlier_cutoff = 3,
span = 5
)
#> $interval_1
#> # A tibble:
#> time smo2 o2hb
#> <dbl> <dbl> <dbl>
#> 1 0 42.8 -0.0289
#> 2 0.1 42.8 -0.0524
#> 3 0.2 42.8 -0.0916
#> 4 0.3 42.9 -0.138
#> 5 0.4 43.2 -0.205
#>
#> $interval_2
#> # A tibble:
#> time smo2 o2hb
#> <dbl> <dbl> <dbl>
#> 1 9.9 51.7 -2.29
#> 2 10 51.7 -2.32
#> 3 10.1 51.8 -2.31
#> 4 10.2 52.2 -2.22
#> 5 10.3 52.4 -2.12extract_intervals(),
shift_mnirs(), and rescale_mnirs() gain
argument group_channels, which separates channel selection
from channel grouping. nirs_channels selects channels; the
new group_channels argument channels specifies
“distinct”, “ensemble”, or custom channel groupp
construction.shift_mnirs(
data,
nirs_channels = c(smo2, o2hb),
group_channels = "ensemble",
to = 0,
span = 5
)filter_mnirs() and replace_mnirs() now
accept named lists for channel-specific processing arguments, in
addition to a single global value as previously.
rescale_mnirs() and shift_mnirs() accept
processing arguments specified by channel or group. This way each
nirs_channel can be processed with unique parameters.filter_mnirs(
data,
nirs_channels = c(smo2, o2hb),
method = list(smo2 = "moving_average", o2hb = "butterworth"),
span = 5, ## only used by "moving_average" channels
order = 2, ## only used by "butterworth" channels (and below args)
W = 0.02,
type = "low",
na.rm = TRUE
)filter_butterworth() and
filter_moving_average() are now the renamed canonical
functions. filter_butter() and filter_ma()
remain available as aliases.
BREAKING CHANGE:
extract_intervals() new argument
group_intervals replaces deprecated
event_groups. This is where interval grouping is specified
for “distinct” or “ensemble”-averaged, or custom group
list construction.
interval_list <- extract_intervals(
data,
nirs_channels = c(smo2_left, smo2_right),
group_intervals = "ensemble",
start = by_time(368, 1084),
span = c(-20, 90)
)
#> $ensemble
#> # A tibble:
#> time smo2_left smo2_right
#> <dbl> <dbl> <dbl>
#> 1 -20 56.3 59.2
#> 2 -19.9 56.1 59.2
#> 3 -19.8 56.1 59.2
#> 4 -19.7 56.2 58.9
#> 5 -19.6 56.4 58.9read_mnirs().txt files.nirs_channel now identified before type conversion,
ensuring coercion to numeric.nirs_channels become
missing during numeric coercion.time_channel column.sample_rate detection and warnings for
irregular samples.create_mnirs_data()resample_mnirs()data arg now accepts a list or grouped data frame and
returns one processed data frame per data frame.replace_mnirs()data arg now accepts a list or grouped data frame and
returns one processed data frame per data frame.nirs_channels as named
lists.filter_mnirs()data arg now accepts a list or grouped data frame and
returns one processed data frame per data frame.nirs_channels as named
lists.spar, W,
fc, width, and span are now
explicit function arguments, making available options easier to
discover.filter_butterworth() and
filter_moving_average() are now the main worker functions;
filter_butter() and filter_ma() remain
available as aliases.shift_mnirs()data arg now accepts a list or grouped data frame and
returns one processed data frame per data frame.nirs_channels as named
lists, or per-named group via group_channels (see
below).group_channels. Supplying a list to
nirs_channels is deprecated.nirs_channels now defaults to NULL,
allowing channels to be taken from mnirs metadata as elsewhere
in the package.rescale_mnirs()data arg now accepts a list or grouped data frame and
returns one processed data frame per data frame.nirs_channels as named
lists, or per-named group via group_channels (see
below).group_channels. Supplying a list to
nirs_channels is deprecated.nirs_channels now defaults to NULL,
allowing channels to be taken from mnirs metadata as elsewhere
in the package.extract_intervals()data arg now accepts a list or grouped data frame and
returns one named list with interval names that identify their
source data frame and interval sequence number.nirs_channels as named
lists.group_intervals,
renamed from event_groups. event_groups is now
deprecated.group_channels. Supplying a list to
nirs_channels is deprecated.plot.mnirs()data arg accepts a list or grouped data frame and
returns facetted plots per data frame (existing functionality since
mnirs 0.6.3, documenting for clarity).mm:ss for data
lasting less than one hour and h:mm:ss for longer
data.as_data_list.R containing shared support for
processing data argument from one data frame, a list of
data frames, or a grouped data frame. Existing mnirs metadata
is kept for each interval.channel_args.R containing shared support for
setting processing options separately for each nirs_channel
or per-group (for shift_mnirs(),
replace_mnirs(), and extract_intervals()) as a
list named by nirs_channels. One (vector) value can still
be supplied to all channels by default.read_mnirs()read_mnirs() can now handle
c(".txt", ".tsv") files via the same
data.table::fread() pathway as ".csv". There
are occasionally still odd file formats where columns will not be
properly detetcted, usually where files are designed to be
human-readible not machine-readable.
read_mnirs(nirs_channels = NULL) now automatically
returns all channels starting with “SmO2” (case
insensitive), which is the most common NIRS channel name for wearable
mNIRS devices. Previously, only the first detected nirs channel was
returned.
## read an mNIRS file with two "smo2" channels
df <- read_mnirs(file_path = example_mnirs("moxy_ramp"))
attr(df, "nirs_channels")
#> [1] "SmO2 Live" "SmO2 Live(2)"extract_intervals() helper function
by_label() matches character strings as regular expressions
(regex) by default. Now more clearly documents this, and accepts two
additional arguments for better handling character strings, which are
both FALSE be default and must be explicitly opted into
(see ?grep):
by_label(ignore_case = TRUE) ignore case when
evaluating strings.
by_label(fixed = TRUE) treat labels as fixed strings
rather than regular expressions. Useful when labels contain regex
metacharacters (., *, (,
etc.).
plot.mnirs() internal update to pass additional
arguments n.breaks to y-axis breaks, and
breaks to x-axis breaks.
format_hmmss() can now display fractional seconds
values.
replace_mnirs(), resample_mnirs(), and
some other functions return more informative error message when
time_channel has irregularly sampled values (time series
must be monotonically increasing, non NA).
resample_mnirs.
Downsampling 100k rows now allocates ~11MB rather than ~4.5 GB.plot.mnirs() ncan now be called on a list of
“mnirs” data frames. Each data frame will be printed as a
facet. This is primarily useful for printing a list of interval data
frames exported from extract_intervals():read_mnirs() |>
extract_intervals() |>
plot()
## returns a plot with a facet for each intervalextract_intervals() now returns a
list of data frames with class = "mnirs". Otherwise, to
manually plot a list of data frames, it will need to have
class(list) <- c("mnirs", class(list)) edited
manually.read_mnirs() |>
extract_intervals() |>
class()
#> [1] "mnirs" "list"print.mnirs() generic created to
avoid displaying extra NextMethod() calls when printing
lists with class = "mnirs".extract_intervals():
When specifying start and end with
by_lap(), start will only refer to the first
sample of the lap, and end the last sample.
With previous behaviour, start = by_lap() would
include the entire specified lap(s). But this resulted in less control
over displaying only parts of a lap.
Updated behaviour is now more consistent with
by_time, by_label, and
by_sample() methods, which reference start and
end from a single sample. This allows extraction of
e.g. only the first 60-sec of lap:
read_mnirs() |>
extract_intervals(
start = by_lap(1, 3),
span = c(0, 60),
)
## returns a list of two intervals with the first 60-sec of laps 1 and 3, respectively.read_mnirs(): Fix detection issue with
“PerfPro” file formats, and another small bug fix to improve
timestamp parsing.
create_mnirs_data() now properly accepts tidy
evaluation for nirs_channels, time_channel,
and event_channel:
create_mnirs_data(df, nirs_channels = c(o2hb, hhb))resample_mnirs():
Now resamples to an inclusive time range around existing data, rounded to the nearest resampled rate. Better handles edge cases where the last sample was being dropped in certain rounding conditions.
Fix an edge case error when sample_rate was
mis-specified higher than the actual sample_rate of the data. Now more
robustly fills non-numeric columns.
read_mnirs():
Now properly parses time_channel as fractional unix
values; i.e. timestamp (e.g. “hh:mm:ss”) values are saved by Excel in
all its infinite wisdom as numeric fractional Unix timestamps. Will now
be properly coerced to numeric and POSIXct timestamp values can be
returned.
Timestamps should now be returned in the user’s local time zone.
resample_mnirs(): Update default
method = "none". Less opinionated default to force users to
explicitly opt-in to specifying either “linear” or “locf” methods to
fill/interpolate across new samples. Updated package
documentation.
replace_invalid(), replace_outliers(),
replace_missing(): Remove bypass_checks arg
intended for internal use only, to bypass redundant checks when calling
from replace_mnirs().
plot.mnirs(): No longer coerces to long format data
behind the scenes.
plot.mnirs(): y-axis title changed from “signal” to
“mNIRS”.
Fix lap marker inconsistency in
train.red_intervals.csv. Updated relevant interval times in
documentation.
README.md & “reading-mnirs-data.qmd”
vignette updates.
Update recommended core processing sequence:
read_mnirs() -> resample_mnirs() ->
replace_mnirs() -> …
Update extract_intervals() vignette to
train.red_intervals.csv end-interval reoxygenation
events.
Small documentation changes.
Core functions updated to accept nirs_channel args
as a list.
An info message will be displayed when a list is not required, instead of erroring.
Additional info messages will be displayed for
shift_mnirs(), rescale_mnirs(), and
extract_intervals() when nirs_channels has not
been specified as a list, nor retrieved from metadata. As a reminder of
the grouping requirements in those functions.
Core functions now properly update nirs_channels
metadata when re-specified.
nirs_channels in a function
would only add any additional column name strings to the existing
metadata rather than overwrite it. Meaning nirs_channels
could only be removed from metadata by using
create_mnirs_data(nirs_channels = "..."). The updated
behaviour should mean channels need to be re-specified less often.filter_mnirs(): Fixed an error matching
method arguments when left blank.
README and “Reading and Cleaning Data with {mnirs}” vignette updated to fix typos and small edits.
NEWS.md is truncated to the most recent relevant release
updates. The full NEWS.md remains in the dev
branch.
Some development functions have been omitted from the package
build in expectation of CRAN review. They are still present in
dev branch and can be installed with
pak::pak("jemarnold/mnirs").
filter_mnirs(): Method-specific arguments
(e.g. order for method = "butterworth", or
width for method = "moving_average") removed
from the generic function call. Continue to be passed to the appropriate
method via .... Documentation and info/warning/abort
messages updated.
filter_ma() better separates effects of
partial and na.rm args:
partial = FALSE by default returns NA at edges where
insufficient number of samples are available compared to the specified
width or span.
partial = TRUE calculates mean values at edges, as
long as one valid non-NA sample is available.
na.rm = FALSE by default behaves as expected with
mean(na.rm = FALSE), propagating any NAs in
the local window to the calculated mean with a warning.
na.rm = FALSE in filter_butter(), which errors
if there are any internal NAs present. This has not been
changed.na.rm = TRUE ignores NAs and calculates
local means as long as one valid sample is present.
plot.mnirs()
Add new arg: points = TRUE will plot points in
addition to lines, as a useful quick shortcut.
Update na.omit now omits non-valid
c(NA, NaN, Inf, -Inf) values from plotting, not just
NA.
palette_mnirs() can now accept either a single
numeric value specifying the number of colours to return, or any number
of (valid) character colour names.
read_mnirs() now has better automated channel
detection logic for “Artinis” / “Oxysoft” file formats.
nirs_channels and time_channel can be left
blank to automatically “sample”, “time” (from
sample_rate), and nirs channel “2”.
replace_outliers() now accepts numeric
outlier_cutoff values for more precise outlier detection
thresholds (previously was integers only). Documentation also
updated.
read_mnirs() expands event_channel to
work with integer “lap” numbers, or character event label as
previous.
Should now work on more .csv file formats; previously read errors may have occured where the file contained header rows above the data table, resulting in improper detection of columns.
event_channel can now be specified as an integer
lap column, in addition to a character column as
previous.
Other {mnirs} functions may expect event_channel to
be either character or integer-ish.
extract_intervals():
Function arguments start and end are
used to specify one or both of a start and end point to the target
interval.
Specify start/end values with helper
functions by_time(), by_label(),
by_lap(), and by_sample().
Numeric values are automatically coerced to “time” values;
Explicit integer values (e.g. 2L) are coerced to “lap”;
Character strings are coerced to event “label”.
dev
branch: https://github.com/jemarnold/mnirs/blob/dev/NEWS.md