library(knitr) # load knitr to enable options
library(respR) # load respR

opts_chunk$set(collapse = TRUE, 
               comment = "#>", 
               cache = FALSE, 
               tidy = FALSE, 
               highlight = TRUE, 
               fig.width = 10, 
               fig.height = 5,
               fig.align = "center",
               R.options = list(scipen = 999, 
                                digits = 3))

Introduction

Here we describe a typical workflow for a closed-chamber respirometry experiment. Whether or not this is the type of experiment you are conducting, this is a good starting point to understand the general respR workflow, functions and how they work together.

The example data used here is urchins.rd, where the first column of the data frame is time data in minutes, while the remaining 18 columns are dissolved oxygen data in mg/L. Columns 18 and 19 (b1 and b2) contain background recordings (i.e. from empty or "blank" control chambers).

head(urchins.rd)

A typical respR workflow

The typical workflow in respR is to process a data frame containing paired values of numeric time and oxygen through several functions, saving the output object each time and inputting it into the next function. Much of this is optional; most respR functions also accept numeric values, vectors and data.frame objects, depending on the input or function. However, the object-oriented approach allows for several benefits, such as data integrity checks and reducing the need for additional inputs, and we would strongly recommend using it whenever possible.

A typical workflow involves some or all of the following functions:

| | | | -- | -- | | inspect() | Visualise the data and check it for common issues| | calc_rate() & auto_rate() | Extract rates from the entire dataset or regions of it, manually or automatically | | adjust_rate() | Adjust the rate values for background oxygen consumption or production | | convert_rate() | Convert the adjusted rate(s) to any common units of oxygen consumption or production | | select_rate() | If there are multiple rates, select according to various criteria which to report or how to arrive at a final rate |

Importing and preparing data

respR has a very simple structural data requirement: data must be in the form of paired values of numeric time and oxygen amount in a data.frame. To our knowledge, all oxygen sensing equipment or probe systems output to formats (e.g. .csv, .txt) that can be imported into this structure in R using generic functions such as read.csv().

See vignette("importing") for a guide to importing and preparing data to this form.

inspect - visualise and check for errors

Once data is in the form of a data.frame of paired time and oxygen values, we use inspect() to visualise the data and to check for common issues:

See vignette("inspecting") for a full description and examples of what these checks entail, the implications of them failing or producing warnings, and other inputs in the function.

Inspect entire dataset

By default, inspect assumes the first column of the data frame is time, while the second column is oxygen. However, we can use the time and oxygen inputs to select different columns, including multiple columns. You can use either numbers or the names of the columns. Here, we inspect all columns without <- assigning (i.e. saving) the result.

inspect(urchins.rd, time = 1, oxygen = 2:19)

This is chiefly exploratory functionality to allow for a quick overview of a dataset. Here the plot lets us see which are the specimen columns, which might have data anomalies, and which are controls.

The data checks tell us all the columns pass the various checks, except one. There is a warning that the time data are not evenly spaced. This is a common warning, and in this case can be safely ignored. It results from using decimalised minutes as the time metric, which happen to be numerically unevenly spaced, but are perfectly usable as the time metric in respR.

Rather than make assumptions that rows represent evenly spaced datapoints, the functions in respR use actual time values for analyses and rate calculations, and so even irregularly spaced data are analysed correctly. Such warnings are for informative purposes: to make the user aware of unusual data gaps, and also to remind users that if they use row numbers for manual operations such as subsetting, the same row width in different parts of the data may not necessarily represent the same time period.

Inspect individual columns

To extract rates, it is best to assign each time-oxygen column pair individually as a separate inspect object. Using the time and oxygen inputs we can select particular columns either by the column number or, as shown here, by name.

urchin <- inspect(urchins.rd, time =  "time.min", oxygen = "n")

Note how the data is plotted against both time (bottom blue axis) and row index (top red axis). From this plot, we can see irregularities in these data near the end of the timeseries (in this case the specimen had interfered with the oxygen sensor). A linear regression across the entire data series would therefore not be a representative estimate of the true routine respiration rate. However, the bottom plot, a rolling rate across a moving window of 10% of the data, shows that over the initial stages of the experiment oxygen uptake rate is consistent at around -0.02. In this experiment this region would be therefore be suitable for extracting rates, and we would not want to use any data after approximately the 29 minutes timepoint.

For now, the data is saved as an object, urchin which contains the original data columns we selected coerced into a new data frame, and various other metadata.

Again, note that using inspect() is optional. Functions in respR will generally accept regular R data structures (e.g. data frames, tibbles, vectors, etc.). inspect() is a quality control and exploratory step that helps highlight potential issues about the data before analysis. We use this particular example with an obvious anomaly to illustrate the point that you should always visualise and explore your data before analysis. respR has been designed to make this straightforward.

calc_rate - calculate oxygen uptake rate

Defaults

Using the inspect object urchin that we just created in calc_rate() with no additional inputs, will prompt the function to perform a linear regression on the entire data series.

calc_rate(urchin) 

Note how the function recognises the inspect() object, with no other inputs necessary. Alternatively, you can input a data.frame object containing raw data, in which case the function will automatically consider the first column as time data, and the second column as oxygen data (if they are not in the first two columns, they should be processed via inspect() or otherwise put into this structure).

Other options

In many cases, we want to select the region over which the rate is determined. For example, we may want to exclude initial stages of instability at the start of an experiment, determine the rate over an exact time duration, or within a threshold of oxygen concentrations. Equipment interference or other factors may cause irregularities in the data we want to exclude. calc_rate allows us to specify exact data regions and allow us to work around such issues.

Using the from, to, and by inputs, a user may use calc_rate() to specify data ranges in several ways:

These inputs do not need to be overly precise; for oxygen and time if input values do not match exactly to a value in the data, the function will identify the closest matching values and use these for calculations. Similarly, if from or to values are beyond the maximum or minimum values in the data, the function will use the maximum or minimum value instead. For example, the above experiment is 45 minutes long. If we used to = 60 as the upper time range, the function would simply apply a to value of 45 instead.

Calculate urchin rate

Here, to calculate our rate we'll select a 25 minute period before the interference occurred.

urch_rate <- calc_rate(urchin, from = 4, to = 29, by = "time")

Plotting the output provides a series of diagnostic plots of the data subset that was analysed.

plot(urch_rate)

The saved object can also be explored using generic S3 R methods.

print(urch_rate)

summary(urch_rate)

The rate, which at this stage is unitless, can be seen as the final column, and other summary data and model coefficients are saved in the object. In this case the rsq is 0.99, so this appears to be a very good estimate of this urchin's routine respiration rate.

Note how the rate value is negative. In respR oxygen uptake rates are represented by negative values because they represent a negative slope of oxygen against time. By contrast, oxygen production rates would be positive. These uptake rates will typically be reported as positive values when you come to write-up the results.

Two-point rate

The output also includes a rate.2pt. This is the rate determined by simple two-point calculation of difference in oxygen divided by difference in time. For almost all analyses, the $rate should be used. See vignette("twopoint") for an explanation of this output and when it might be useful.

calc_rate.bg - calculate background oxygen

The presence of micro-organisms and their oxygen use may be a potential source of experimental bias, and we usually want to account for background respiration rates during experiments by conducting empty or "blank" control experiments with no specimens to quantify it.

These control experiments are routinely conducted alongside, or before and after specimen experiments, or at an entirely different time. Whenever they are conducted, it is always important they are done under the exact same conditions using the same or equivalent equipment. Essentially these should be identical to regular experiments, except for the absence of the specimen you are interested in.

Since the oxygen signal from blank experiments can be noisy and trends easily influenced by outliers, often the background rates from several controls are averaged to obtain a more accurate estimate of the background rate. Once a background rate is determined, specimen rates are then adjusted by it. See vignette("adjust_rate") for detailed examples of the many types of adjustments that can be performed using respR.

The function calc_rate.bg() is used to extract background rates from control recordings. These must share the same units of time and oxygen as the experimental rates they will be used to adjust. It can also process multiple background oxygen recordings, so that an average background rate will be applied in the adjust_rate function, or other adjustments requiring multiple recordings can be performed. In the urchins.rd data, background respiration was recorded and saved in columns 18 and 19.

We will determine background rates using calc_rate.bg(). Unlike calc_rate, which can calculate rates from multiple regions of a single oxygen column, this function can calculate rates across single region of multiple columns.

Here, we inspect the two data columns, and because they have no anomalies use the entire data to calculate a background rate (if we wanted to only use part of it we could pass it through subset_data() first). We save the output as a separate object.

bg_insp <- inspect(urchins.rd, time = 1, oxygen = 18:19)
bg_rate <- calc_rate.bg(bg_insp)
bg_rate

The bg_rate object contains both individual background rates for each data column ($rate.bg), and an averaged rate ($rate.bg.mean). We will determine how these are applied as an adjustment to the specimen rate in adjust_rate().

adjust_rate - adjust for background respiration

See vignette("adjust_rate") for detailed examples of the types of adjustments that can be performed using adjust_rate. Here the adjustment is relatively straightforward. We will adjust the urch_rate using the bg_rate object we saved in the previous section.

The rate input to be adjusted can be an object of class calc_rate or auto_rate, or any numeric value (or multiple values). The by adjustment value can be a calc_rate.bg object, calc_rate object, or numeric value or vector. adjust_rate has several methods determining how the by is applied, but the default one is "mean", which is the one we want here, since we want to apply the average of the two background rates we just calculated.

urch_rate_adj <- adjust_rate(urch_rate, by = bg_rate, method = "mean")
urch_rate_adj

The urchin rate has been adjusted to a slightly lower uptake rate because, as we found by looking at the controls, some of this uptake was due to respiration by micro-organisms.

Background adjustments can also be entered manually. Care should be taken to include the correct sign. In respR oxygen uptake rates are negative since they represent a negative slope of oxygen against time. Background rates are usually (though not always) also negative. In this case, the default "mean" method will not alter the by value.

urch_rate_adj_num <- adjust_rate(-0.0218, by = -0.000833)
urch_rate_adj_num

We can see this is essentially the same result as above. The small mismatch in values is simply due to the lower precision of entered values compared to internal ones (the number of decimal places printed in the console depends on your own R options setting).

convert_rate - convert the results

Note, that until this point respR has not required units of time or oxygen to be specified. Now we convert calculated, unitless rates to specified output units.

convert_rate() can be used to convert the unitless rate values we have dealt with up to now to these reportable metrics:

Conversion requires the units of the original raw data (time.unit, oxy.unit), and the volume of fluid in the respirometer in Litres ($L$). Mass-specific rates require the mass of the specimen in kilograms ($kg$), and area-specific rates require the area of the specimen in metres squared ($m^2$). Lastly, an output.unit appropriate to the inputs should be specified. This should be in the correct order: "oxygen/time" or "oxygen/time/mass" or "oxygen/time/area".

Note: the volume is volume of fluid in the respirometer or respirometer loop, not the volume of the respirometer. That is, it represents the effective volume. A specimen occupies space in the respirometer, and so displaces some proportion of the water volume, which depending on its size might be significant. Therefore the volume of water entered here should equal the total volume of the respirometer minus the volume of the specimen. There are several approaches to determine the effective volume; calculating the specimen volume geometrically or via water displacement in a separate vessel, or calculated from the mass and density (e.g. for fish it is often assumed they have an equal density as water, that is ~1000 kg/m^3). Water volume could also be determined directly by pouring out the water at the end of the experiment, or by weighing the respirometer after the specimen has been removed. See the respfun respirometry utilities package for several functions to assist with determining the effective volume.

Convert adjusted urchin rate

For an example of absolute oxygen uptake rate, we can convert the output of adjust_rate() to oxygen consumed by the whole urchin in mg per hour:

convert_rate(urch_rate_adj,         # urchin rate adjusted for background
             oxy.unit = "mg/L",     # oxygen units of the original raw data
             time.unit = "min",     # time units of the original raw data
             output.unit = "mg/h",  # output unit
             volume = 1.09)         # effective volume of the respirometer
convert_rate(urch_rate_adj,         # urchin rate adjusted for background
             oxy.unit = "mg/L",     # oxygen units of the original raw data
             time.unit = "min",     # time units of the original raw data
             output.unit = "mg/h",  # output unit
             volume = 1.09)         # effective volume of the respirometer

We can also convert to a mass-specific rate by adding a specimen mass and specifying a mass-specific output.unit:

convert_rate(urch_rate_adj, 
             oxy.unit = "mg l-1", 
             time.unit = "m", 
             output.unit = "mg/s/kg",
             volume = 1.09, 
             mass = 0.19)
convert_rate(urch_rate_adj, 
             oxy.unit = "mgl-1", 
             time.unit = "m", 
             output.unit = "mg/s/kg",
             volume = 1.09, 
             mass = 0.19)

Note how we have changed the format of the time and oxygen units. A "fuzzy" string matching algorithm automatically recognises such variations, allowing natural, intuitive input. For example, "ml/l", "mL/L", "ml L-1", "milliliter/liter", and "millilitre/litre" are all recognised as ml/L. Unit delimiters can be any combination of a space, dot (.), forward-slash (/), or the "per" unit (-1). Thus, "ml/kg", "mL / kg", "mL /kilogram", "ml kg-1" or "ml.kg-1" are equally recognised as mL/kg. To see what units are available to use in various functions, see unit_args().

unit_args()

Note that some units of oxygen require temperature, salinity and atmospheric pressure to perform the conversion. One handy tip: you may want to enter these even if they are not required for conversions because they are saved in the $summary table, and this can help in keeping track of results across different experiments.

This time we will save (i.e. assign) the result to an object.

urch_rate_final <- convert_rate(urch_rate_adj, 
                                oxy.unit = "mg/L", 
                                time.unit = "mins", 
                                output.unit = "ml/h/kg",
                                volume = 1.09, 
                                mass = 0.19,
                                t = 20,
                                S = 30,
                                P = 1.01)
print(urch_rate_final)

summary(urch_rate_final)

Final rate

The final rate can be seen above in these console outputs. It can be extracted for further use from the saved object where it is $rate.output:

urch_rate_final$rate.output

Alternatively, use the summary function with export = TRUE to save the summary table as a separate data frame which contains all rate regression parameters and data locations, adjustments (if applied), units, and more. This is a great way of exporting all the relevant data for your final results.

urch_rate_final_df <- summary(urch_rate_final, export = TRUE)

Check the result

A final and important step in respirometry analyses, particularly after the first rate from a new set of experiments or specimens has been calculated, is to check the result against rates from the same or similar species of the same size in as close as possible to the same conditions. This could be from previous analyses by the same user or group, or rates from one or more published studies.

Dealing with so many different units and conversions it is very easy to introduce a small conversion or transcription error that will have a huge effect on the final calculated rate. These types of error occasionally make it into published papers. This is why we would also recommend you check the result against more than one independent publication.

In the present example, at the time the experiments were done there were no published respirometry studies on this particular species of sea urchin. However, a quick Google Scholar search led to other papers from which rates of other species of sea urchin at around the same size and temperature could be found. In Moulin et al. (2015). E. mathaei has a respiration rate of around 0.2 to 0.5 umol/h/g at 25°C, and in Kurihara et al. (2013) H. pulcherrimus it is around 0.6 umol/h/g at 22°C.

New in respR v2.3 is the convert_MR() function which converts between different units of oxygen uptake. This works on numeric values but also convert_rate objects, in which case we simply need to specify a different output unit via to.

convert_MR(urch_rate_final, 
           to = "umol/h/g",
           t = 20,
           S = 30,
           P = 1.01)
convert_MR(urch_rate_final, 
           to = "umol/h/g",
           t = 20,
           S = 30,
           P = 1.01)

Generally speaking, if there is a order of magnitude difference or more it is a good indication that there may be a problem. Here, we can see our rate value of -0.225 umol/hr/g is broadly consistent with these others from the literature. Any difference is easily within what would be expected given variation between species and difference in temperature. Therefore, we can be fairly confident that our urchin rate is in the right ballpark and we have not made a conversion error.

Summary

This is an example of a fairly straightforward analysis of a closed-chamber respirometry experiment. This entire analysis can be documented and shared in only a few lines of code, making it easily reproducible if the original data file is included:

# import and inspect
urchin <- inspect(urchins.rd, time = 1, oxygen = 15)

# Background
bg_insp <- inspect(urchins.rd, time = 1, oxygen = 18:19)
bg_rate <- calc_rate.bg(bg_insp)

# Specimen rate
urch_rate <- calc_rate(urchin, from = 4, to = 29, by = "time")

# Adjust rate
urch_rate_adj <- adjust_rate(urch_rate, bg_rate)

# Convert to final rate units
urch_rate_final <- convert_rate(urch_rate_adj,
                                oxy.unit = "mgl-1", 
                                time.unit = "m", 
                                output.unit = "mg/s/kg", 
                                volume = 1.09, 
                                mass = 0.19)

# Extract full results for archiving or further analysis
urch_rate_final_summary <- summary(urch_rate_final, export = TRUE)

Using pipes, either the new native |> pipes introduced in R v4.1 or %>% dplyr pipes, can condense this even further:

urch_rate <- urchins.rd |>                                   # Using the urchins data,
  inspect(1, 15) |>                                          # inspect, then
  calc_rate(from = 4, to = 29, by = "time") |>               # calculate rate, then
  print() |>                                                 # print for quick look,
  adjust_rate(  
    calc_rate.bg(urchins.rd, time = 1,                       # calculate the background and
                 oxygen = 18:19)) |>                         # adjust the rate, then
  print() |>                                                 # print for another check,
  convert_rate(oxy.unit = "mgl-1", time.unit = "m",     
               output.unit = "mg/s/kg", volume = 1.09, mass = 0.19) |>  # then convert
  summary(export = TRUE)                                     # and finally save


januarharianto/respR documentation built on April 20, 2024, 4:34 p.m.