Getting started with MRManalyzeR

Scope

MRManalyzeR processes reviewed targeted LC-MS/MS results, assesses data quality and generates reports. A curated workbook and a YAML configuration define the analysis, so a run is described by two files and can be repeated from them.

It reads Waters TargetLynx exports, Skyline exports, and plain sample-by-analyte matrices from anything else. It does not do peak detection, integration or calibration-curve fitting: those decisions stay with you, in the vendor software.

Everything is carried in a struct DatasetExperiment: the assay matrix, the sample metadata and the feature metadata, kept aligned in one object. Any package that produces or consumes that class can exchange data with this one — see Working with other packages.

Installation

if(!requireNamespace("BiocManager", quietly = TRUE))
  install.packages("BiocManager")
BiocManager::install("MRManalyzeR")

Then load it:

library(MRManalyzeR)

Example

Run example

The bundled example of oxylipins in mouse lung writes to a directory you choose:

res <- mrmRunExample(render = FALSE, open = FALSE)
list.files(res$output_directory)
#> [1] "example_config.yml"               "example_data_ng_mL__measured.RDS"
#> [3] "example_data_ng_mL_.RDS"          "example_data_ng_mL_.txt"         
#> [5] "example_data_ng_mL_.xlsx"

This is what a successful run produces. render = FALSE skips the two HTML reports, which is what makes it quick here; plain mrmRunExample() writes those too.

Data format

One workbook with three sheets:

Sheet One row per Holds
lcms_data see below the vendor export, as reviewed
sample_metadata injection which injections to include, group, batch, amounts
feature_metadata compound which compounds to report, class and pathway annotation

The data sheet’s layout depends on the data source, and the configuration names which reader to use:

Source lcms_data holds Configured under
Waters TargetLynx the export as saved: stacked compound blocks, one per compound, each with its own header row tl_data:
Skyline one row per molecule, one column per injection skyline_data:
Anything else a plain matrix of samples by analytes, or its transpose matrix_data:, with orientation:

The two metadata sheets are user curated, MRManalyzeR never writes them. sample_metadata needs an injection name matching the export, an inclusion flag, and whatever factors you want to analyse by:

Name Sample_ID Include Sample_type Treatment
20260505_07 CTRL_1 YES Sample PBS
20260505_08 CTRL_2 YES Sample PBS
20260505_09 HDM_1 YES Sample HDM

Columns beyond those are needed only by the steps that use them, and only when you switch those steps on: injection and vial volumes for adjust_conc:, a protein or tissue-weight column for normalize:, a batch column for batch_correction:, an injection-order column for the drift plots, and a sample-type column wherever QCs or blanks are involved. A run with every processing step off needs only the columns above.

Class and pathway annotations in feature_metadata group and colour compounds across plots.

A configuration to edit

newConfig() writes a skeleton YAML to fill for a project. The short form carries the keys a run cannot do without; minimal = FALSE writes the fully commented reference instead, which documents every key the package reads.

cfg <- file.path(tempdir(), "project_config.yml")
newConfig(cfg, source = "targetlynx")
#> project:
#>   name: my_study
#>   researcher: ""
#> 
#>   # Where the workbook is, and where results go. fn is the workbook name
#>   # inside data_dir, without the .xlsx.
#>   paths:
#>     project_dir: &project_dir "C:/path/to/study"
#>     data_dir:    &data_dir    !concat_path [*project_dir, "data"]
#>     result_dir:  &result_dir  !concat_path [*project_dir, "results"]
#>     fn: &fn my_workbook
#>     suffix: _run1                 # appended to every output name
#> 
#>   PeakMatrixProcessing:
#>     execute: True                 # False = reuse the stored RDS, re-render only
#> 
#>     # 1. Data source
#>     tl_data:
#>       enabled: True

Open that file and work down this list:

  1. paths: – project_dir, data_dir, result_dir, and fn, the workbook’s name without .xlsx.
  2. The data-source block – data_tab_names must name your data sheet, and datatype: the value to report (tl_data:), or signal_filter: the floor to apply (skyline_data:).
  3. sample_meta_tab and its columns – name_col, include_col, include_value.
  4. feature_meta_tab and its columns – compound_col, processing_name_col, report_col, report_value.
  5. The processing steps you want, each off until you set it.
  6. The report blocks, where sample_type_head, qc_label and sample_labels have to match your sample-type column.

Nothing is guessed from the data, so a key that does not match stops the run with a message naming it – useful confirmation that you have the list above right, rather than a substitute for working through it.

The run

res <- runMRManalyzeR(cfg)

Outputs

Outputs of MRManalyzeR are stored at paths.result_dir, named after the workbook, the datatype and your suffix:

File Holds
<name>.xlsx processed matrix, both metadata sheets, and the compounds removed with the reason
<name>_stats.xlsx tests, correlations, linear models, ratios, group summaries, and a key sheet defining every column
<name>.RDS the dataset, for re-rendering reports without reprocessing
<name>_measured.RDS the same before imputation – written only when replace_MVs: is set
<name>.txt the parameters as they resolved for this run
up to two .html reports data quality, and statistics, each written only if its block has execute: True

<name>_stats.xlsx likewise carries only the analyses that were enabled: a run with comparisons: off and correlations: on has no stats sheet.

The term measured means processed but not imputed: the values as they come out of the filters, batch correction, normalisation and concentration adjustment, with the gaps still gaps. It does not mean the untouched instrument output.

In <name>.xlsx, the two sheets to read first lead with the answer. After the compound name, feature_metadata gives Lowest_measured, and removed_features gives Failed_step, naming what took the compound out (User exclude, Detection filter, Signal filter (LOD), Never measured, and so on).

Lowest_measured is the smallest value for that compound across the biological samples – QCs and blanks excluded – taken after all processing but before imputation, in the units the run reports (Area, ng/mL, and after normalisation or concentration adjustment where those are on). It is rounded to the nearest whole number.

The return value holds the same objects:

res$datasetExperiment   # processed, imputed if replace_MVs was set
res$measured            # processed, never imputed
res$stats_tables        # what went into the stats workbook

Reading the configuration

Four blocks, in the order they run.

project:
  paths:                  # workbook in, results out
  PeakMatrixProcessing:   # filters, batch correction, normalisation, imputation
  data_quality_report:    # %RSD, drift, normality, QC PCA
  stats_report:           # comparisons, PCA, volcano, heatmaps, ratios

Two points decide how a run reads, and both are recorded in the output:

Processing order is fixed. Therefore, blank filter, then detection filters, then batch correction, then normalisation and concentration adjustment, and imputation last: batch correction works on the instrument signal and imputation on the corrected signal.

Imputed values stay out of the statistics as default. With replace_MVs: set, the exported matrix is filled and the measured values are written beside it. The tests, boxplots and CVs use the measured ones; only the PCA uses the filled matrix, because a PCA cannot take a gap. use_imputed: True in a report block changes that for the whole report.

A compound reaching the reports carries its own record: how many values were filled in, whether it could be batch-corrected, the fraction of samples it was detected in. The vignette vignette("processing-details", package = "MRManalyzeR") goes through each step, what it decides and how to read what it recorded.

The two reports

Data quality answers whether the data is usable: %RSD for QCs and for samples and their ratio, signal against injection order, per-compound normality, a QC PCA coloured by batch, order and sample type, and every compound that was dropped with the reason.

Statistics answers what the study found: group comparisons with the test named per comparison, boxplots and summaries, PCA with loadings coloured by pathway, volcano, heatmaps blocked by pathway, feature correlations and metabolite ratios.

Both are single self-contained HTML files, and each states which matrix it used and how missing values were handled.

Several data acquisition panels in one study

When a study runs more than one panel — oxylipins, phospholipids, sphingolipids — each is processed on its own, then merged by sample ID:

cfg <- newConfig("combine.yml", template = "combine")
res <- runMRManalyzeRCombine("combine.yml")

The merge reads each panel’s measured values where they were stored, prefixes the compounds by panel so names cannot collide, and runs one statistics report across the lot.

Three things decide what the merged dataset contains:

  • Samples are intersected. Only sample_id_col values present in every panel survive; a sample one panel missed is dropped from the merge rather than carried with gaps. No samples in common causes an error.
  • Duplicate sample IDs are an error by default, naming the offending IDs. duplicate_samples: first | mean | median resolves them instead, and a re-injection is better marked Include: "NO" before processing.
  • Metadata columns are intersected too. feature_meta_cols: and sample_meta_cols: default to the columns every panel shares; name them explicitly to keep a particular set. Panels annotated differently therefore merge down to their common columns.

Working with other packages

A DatasetExperiment is struct’s container, which enables the MRManalyzeR output to be used in other packages, for instance for statistics. res$datasetExperiment is a DatasetExperiment, and a DatasetExperiment is also a SummarizedExperiment, so packages built on either reach the same data.

Taking the processed dataset into structToolbox for a PLS-DA. That model is fitted by pls, which structToolbox suggests rather than requires, so the chunk below runs only where both are installed:

library(structToolbox)

de      <- loadDataset(system.file("extdata", "example_synthetic.RDS",
                                   package = "MRManalyzeR"))
samples <- subsetDataset(de, conditions = list(Sample_type = "Sample"))

# structToolbox wants the response as a factor
sm <- as.data.frame(samples$sample_meta)
sm$Treatment <- factor(sm$Treatment)
samples$sample_meta <- sm

M <- model_apply(PLSDA(number_components = 2, factor_name = "Treatment"),
                 samples)
round(as.data.frame(M$scores$data)[1:3, ], 2)
#>            LV1   LV2
#> SYN_002 -12.65 -0.04
#> SYN_003 -20.05  9.41
#> SYN_004  -9.81 15.18

The scores come back as a DatasetExperiment too, so the exchange runs both ways: anything holding one – a structToolbox model output, a pmp matrix wrapped in one – can be handed straight back to any step here, or to runStats(). A plain matrix with its two metadata frames goes through assembleDataset() first.

Session info

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] structToolbox_1.25.0 MRManalyzeR_0.99.7   struct_1.25.0       
#> [4] BiocStyle_2.41.0    
#> 
#> loaded via a namespace (and not attached):
#>  [1] SummarizedExperiment_1.43.0 gtable_0.3.6               
#>  [3] xfun_0.61                   bslib_0.12.0               
#>  [5] ggplot2_4.0.3               httr2_1.3.0                
#>  [7] htmlwidgets_1.6.4           Biobase_2.73.2             
#>  [9] lattice_0.23-1              vctrs_0.7.3                
#> [11] tools_4.6.1                 generics_0.1.4             
#> [13] stats4_4.6.1                tibble_3.3.1               
#> [15] pkgconfig_2.0.3             Matrix_1.7-6               
#> [17] RColorBrewer_1.1-3          S7_0.2.2                   
#> [19] S4Vectors_0.51.10           lifecycle_1.0.5            
#> [21] farver_2.1.2                compiler_4.6.1             
#> [23] stringr_1.6.0               janitor_2.2.1              
#> [25] Seqinfo_1.3.2               snakecase_0.11.1           
#> [27] htmltools_0.5.9             sys_3.4.3                  
#> [29] buildtools_1.0.0            sass_0.4.10                
#> [31] yaml_2.3.12                 pillar_1.11.1              
#> [33] jquerylib_0.1.4             tidyr_1.3.2                
#> [35] DT_0.34.0                   DelayedArray_0.39.8        
#> [37] cachem_1.1.0                abind_1.4-8                
#> [39] pls_2.9-0                   tidyselect_1.2.1           
#> [41] zip_3.0.2                   digest_0.6.39              
#> [43] stringi_1.8.9               dplyr_1.2.1                
#> [45] purrr_1.2.2                 ggthemes_7.0.0             
#> [47] maketools_1.3.2             fastmap_1.2.0              
#> [49] grid_4.6.1                  cli_3.6.6                  
#> [51] SparseArray_1.13.4          magrittr_2.0.5             
#> [53] S4Arrays_1.13.2             withr_3.0.3                
#> [55] scales_1.4.0                sp_2.2-4                   
#> [57] lubridate_1.9.5             timechange_0.4.0           
#> [59] rmarkdown_2.32              XVector_0.53.0             
#> [61] matrixStats_1.5.0           otel_0.2.0                 
#> [63] gridExtra_2.3.1             openxlsx_4.2.9             
#> [65] evaluate_1.0.5              knitr_1.52                 
#> [67] GenomicRanges_1.65.4        IRanges_2.47.5             
#> [69] rlang_1.3.0                 Rcpp_1.1.2                 
#> [71] glue_1.8.1                  BiocManager_1.30.27        
#> [73] BiocGenerics_0.59.12        jsonlite_2.0.0             
#> [75] R6_2.6.1                    MatrixGenerics_1.25.0