--- title: "ProteinBatcher workflow" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{ProteinBatcher workflow} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` # Overview ProteinBatcher is a downstream analysis package for label-free quantitative proteomics. It does **not** perform upstream quantification: it operates on a protein-level abundance matrix (it specifically targets DIA-NN `pg_matrix` outputs) together with a sample-annotation table, and turns the sequence of *filtering → imputation → differential abundance testing → effect organization → visualization* into a single, reproducible function call, `run_proteomics_pipeline()`. The design goal is **standardization and automation of a downstream workflow built on top of established Bioconductor methods**, rather than the introduction of new statistical methodology or new data containers. Concretely, ProteinBatcher contributes: 1. A turnkey pipeline that enforces a fixed input schema and a consistent output structure, so that experiments with similar structure but different designs can be analysed with the same code. 2. A systematic separation of each contrast into *main*, *common* (batch- or factor-independent) and *interaction* (factor-dependent) effects, each returned as a `SummarizedExperiment`. 3. Interpretation-oriented visualizations, in particular the **deregulogram**, which compares the full effect of a contrast across the two levels of an interaction factor and emphasises effect concordance and magnitude rather than statistical significance alone. All intermediate and final results are stored in `SummarizedExperiment` objects, and all linear modelling is delegated to `limma`. # Relationship with the Bioconductor ecosystem Bioconductor already provides mature infrastructure for the individual steps of a proteomics analysis, and several packages support fully integrated, design-aware differential-abundance workflows. ProteinBatcher is intended to sit *alongside* this infrastructure, not to replace it. It is built around two established pillars: - **`SummarizedExperiment`**, used as the single container for the assay, feature metadata (`rowData`) and sample metadata (`colData`); and - **`limma`**, used for linear modelling and empirical-Bayes moderation, including multi-factor designs, interactions, and correlation-aware models via `duplicateCorrelation()`. ## How ProteinBatcher differs from msqrob2 The reviewer specifically asked how ProteinBatcher relates to [msqrob2](https://www.bioconductor.org/packages/msqrob2). The two packages overlap in purpose (differential abundance in MS-based proteomics) but occupy clearly different design points, and they are complementary rather than competing. `msqrob2` contributes **statistical methodology**. It implements a robust linear (mixed) model framework whose parameter estimates can be stabilised by ridge regression, empirical-Bayes variance estimation and robust M-estimation, and it offers a hurdle workflow that handles missing values without requiring imputation. It is built on the `QFeatures` infrastructure, so it can model the data while consistently linking the PSM, peptide and protein levels, and can start either from raw peptide intensities or from summarised protein values. Its value lies in the modelling engine itself. `ProteinBatcher`, by contrast, does **not** introduce new statistical methods. It orchestrates *established* `limma` modelling into a fixed, reproducible downstream pipeline that operates on a plain protein-level `SummarizedExperiment`. Its distinctive contributions are operational rather than statistical: a single-call pipeline with a fixed annotation schema, a specific organisation of results into main / common / interaction effects, and dedicated interpretation plots (most notably the deregulogram). In short, `msqrob2` is primarily a *method* (and a peptide-to-protein modelling framework), whereas ProteinBatcher is primarily a *standardised wrapper and visualisation layer* around protein-level `limma` analysis. We therefore do not claim that flexible designs, interaction testing or integrated workflows are unavailable elsewhere in Bioconductor: `msqrob2`, among others, supports them. The motivation for ProteinBatcher is to provide a convention-driven, low-boilerplate route for teams that repeatedly run protein-level `limma` analyses with the same input/output conventions and that want the specific effect-separation and deregulogram outputs described below. # Input data and workflow parameters ProteinBatcher needs two input datasets and a set of workflow parameters. The inputs define *what* is analysed; the parameters control *how* the analysis is performed. ## Input data 1. **Quantitative matrix.** A protein-by-sample matrix of quantitative measurements, as produced by DIA-NN (`pg_matrix`). Rows are protein groups and columns are samples. 2. **Sample annotation table.** A tab-delimited table describing the experimental design and linking each sample to its biological and technical metadata. Both are shipped with the package as example files. The chunk below locates them with `system.file()` and reads the annotation table with base R (`read.delim()`); no external reader package is required. We also record whether the example files are available, so that the rest of the vignette degrades gracefully if they are not. ```{r paths} # Load package library(ProteinBatcher) # Input files shipped with the package path_annotation <- system.file("extdata", "annotation_HaCaT.tsv", package = "ProteinBatcher") path_pgmatrix <- system.file( "extdata", "2024MK017_HaCaT_Stimulation_1to24_Astral_report.pg_matrix.tsv", package = "ProteinBatcher") path_output <- tempdir() # Inspect annotation structure (base R, sin readr) ann <- utils::read.delim(path_annotation, check.names = FALSE) ``` The annotation table read above has one row per sample. We can confirm its shape and look at the first rows: ```{r show-annotation} dim(ann) head(ann) ``` ## Sample annotation structure For the high-level `run_proteomics_pipeline()` entry point, the annotation table is validated against a **fixed set of column names**. These names are *required* (not merely suggested): the input validator stops with an informative error if any of them is missing, because the pipeline locates each piece of metadata by name when it builds `colData` and when it assembles the `limma` design. The required columns are: - **`file`** — raw-data file identifier for each sample (used as the matching label against the quantitative matrix columns). - **`sample`** and **`sample_name`** — sample identifiers; `sample_name` must be unique and becomes the sample id in `colData`. - **`condition`** — the main experimental factor of interest (the factor that the `tests` contrasts compare). - **`replicate`** — repeated-measurement identifier, used by paired / correlation-aware designs. - **`batch`** — a batch or sample-preparation factor (e.g. acquisition day), used as a covariate and as the interaction factor in the examples below. - **`donor_id`** — biological replicate / donor identifier, used as the blocking factor in paired or repeated-measures models. The set is intentionally fixed so that the same pipeline code can be reused across experiments without per-experiment column-mapping: the conventions are the contract. Which of these factors actually enters the model is then determined by the design `formula` and by the `paired` / `block_effect` parameters; columns that are present but not referenced by the formula simply remain available in `colData` for inspection. ## Workflow parameters In addition to the two inputs, the pipeline takes parameters that control filtering, modelling and output generation. The most important are: - **`percent_missing`** — missingness threshold (in %) for feature filtering. - **`tests`** — main `condition` contrasts to evaluate, written as `"_vs_"`. - **`tests_interaction`** — interaction coefficients to evaluate, or `"NA"` to disable interaction testing. - **`formula`** — the `limma` design formula (main effects and interactions). - **`reference_condition`** — the reference level of `condition`. - **`paired`** / **`block_effect`** — options for paired and correlation-aware models. - **`ldv_source`** — strategy for left-censored (LDV) imputation (`"global"` or `"per-condition"`). - **`plots`** — whether to also write result tables and PDF figures to `path_output`. # The example data set The shipped example is a HaCaT keratinocyte stimulation experiment (`2024MK017`). It contains `r if (exists("ann")) nrow(ann) else 24` samples organised as a two-factor design: - **`condition`** with four levels: `NoTreated` (reference), `IL13`, `IL22`, and `COMBO` (the IL13 + IL22 combination); - **`batch`** with two levels: `day1` and `day2` (the acquisition day). Each condition is measured in three samples on each of the two days (4 conditions × 2 days × 3 = 24 samples), and all samples come from the same cell line (`donor_id = 1`). This balanced design is what makes the `condition:batch` interaction estimable: it lets us ask not only whether a treatment changes protein abundance on average, but also whether that change differs between the two acquisition days. The cross-tabulation of the design is: ```{r design-table} table(condition = ann$condition, batch = ann$batch) ``` # Example 1: workflow with interaction testing (condition-by-batch) In this first example we model the four conditions together with the batch factor and their interaction, `~ 0 + condition + batch + condition:batch`. We request the three treatment-vs-control contrasts and the matching condition-by-day interaction coefficients. The chunk below only defines the parameters; nothing is computed yet. ```{r ex1-params} experiment <- "HaCaT" percent_missing <- 50 formula <- formula(~ 0 + condition + batch + condition:batch) tests <- c( "IL13_vs_NoTreated", "IL22_vs_NoTreated", "COMBO_vs_NoTreated" ) tests_interaction <- c( "IL13.batchday2", "IL22.batchday2", "COMBO.batchday2" ) reference_condition <- "NoTreated" ``` We now run the full pipeline. With `plots = FALSE` the function performs filtering, imputation, differential testing and effect organisation in memory and returns a single list; it does not write any files. (Set `plots = TRUE` to additionally export the tables and PDF figures shown later in this vignette.) ```{r ex1-run} res <- run_proteomics_pipeline( path_pgmatrix = path_pgmatrix, path_annotation = path_annotation, path_output = path_output, tests = tests, tests_interaction = tests_interaction, formula = formula, reference_condition = reference_condition, percent_missing = percent_missing, ldv_source = "per-condition", experiment = experiment, plots = FALSE ) ``` The returned object is a named list. `se_raw`, `se_filt` and `se_imp` are the `SummarizedExperiment` objects after import, filtering and imputation respectively; `removed` is a `data.frame` of the proteins dropped by the missingness filter; and `effects` holds the organised results. Inspecting the imputed object shows the protein-by-sample assay together with the metadata the pipeline attached during imputation: ```{r ex1-structure} names(res) res$se_imp ``` ## Accessing the organised effects `effects` is a list **keyed by the main contrast**, so a given contrast can be retrieved directly with `$`. Each element is itself a list of three `SummarizedExperiment` objects — `all_common_effect`, `common_effect` and `interaction_effect` — and the per-contrast statistics (`log2FC`, `CI.L`, `CI.R`, `p.val`, `p.adj`) are stored in their `rowData()` under the `"_"` naming convention (for example `log2FC_IL13_vs_NoTreated`). This is the structure consumed by the downstream visualisation helpers. ```{r ex1-effects} # Contrasts available, and direct access with `$` names(res$effects) il13 <- res$effects$IL13_vs_NoTreated names(il13) # Each slot is a SummarizedExperiment; statistics live in rowData() il13$all_common_effect head(colnames(SummarizedExperiment::rowData(il13$all_common_effect))) ``` ## Filtering and imputation summary tables When `plots = TRUE`, the pipeline also writes plain-text (CSV) summaries of the preprocessing steps so that they are fully transparent and can be re-read with base R. The package ships precomputed copies of these tables as example outputs. The chunk below reads them with `read.csv()` if they are present: - `HaCaT_filtered_out_proteins.csv` lists the proteins removed by the missingness filter, so users can verify exactly which features were excluded. - `HaCaT_Imputation_before.csv` and `HaCaT_Imputation_after.csv` give the protein-by-sample matrix immediately before and after imputation, allowing a direct before/after comparison for quality control. ```{r ex1-tables} path_filtered <- system.file( "extdata", "HaCaT_filtered_out_proteins.csv", package = "ProteinBatcher" ) path_before <- system.file( "extdata", "HaCaT_Imputation_before.csv", package = "ProteinBatcher" ) path_after <- system.file( "extdata", "HaCaT_Imputation_after.csv", package = "ProteinBatcher" ) if (nzchar(path_filtered)) { filtered_df <- read.csv(path_filtered, check.names = FALSE, stringsAsFactors = FALSE) head(filtered_df) } ``` ```{r ex1-tables-imp} if (nzchar(path_before) && nzchar(path_after)) { before_df <- read.csv(path_before, check.names = FALSE, stringsAsFactors = FALSE) after_df <- read.csv(path_after, check.names = FALSE, stringsAsFactors = FALSE) # First proteins, first few columns, before vs after imputation head(before_df[, seq_len(min(5, ncol(before_df)))]) head(after_df[, seq_len(min(5, ncol(after_df)))]) } ``` The "before" table still contains missing values (`NA`); in the "after" table these have been replaced either by a within-condition mean (when a protein is only partially missing in a condition) or by a left-censored low-value draw (when it is essentially absent), which is why the imputed matrix has no missing entries. ## Differential effects: IL13 vs NoTreated From here we focus on the `IL13_vs_NoTreated` contrast to illustrate how the three effect types are read. The figures below are the PNG outputs shipped with the package; each chunk renders the figure only if it is present. ### Main effect (all proteins) The main-effect volcano shows the *average* IL13 effect across both batch days, ignoring the interaction term. Every tested protein is included regardless of whether its response depends on the day. ```{r ex1-fig-main, echo = FALSE, out.width = "80%"} p_main <- system.file( "extdata", "HaCaT_MainEffect_AllProteins_IL13_vs_NoTreated.png", package = "ProteinBatcher" ) if (nzchar(p_main)) knitr::include_graphics(p_main) ``` The x-axis is the estimated log2 fold change (IL13 vs NoTreated) and the y-axis is statistical significance (−log10 adjusted p-value); points away from the centre and high up are strongly and significantly regulated. This view answers "which proteins are affected by IL13 on average?", but it does not yet separate stable effects from day-dependent ones. ### Common effect (batch-independent proteins) The common-effect volcano restricts attention to proteins whose IL13 response is **not** significantly modulated by batch, i.e. those that fail the interaction test and are therefore consistent across days. ```{r ex1-fig-common, echo = FALSE, out.width = "80%"} p_common <- system.file( "extdata", "HaCaT_CommonEffect_IL13_vs_NoTreated.png", package = "ProteinBatcher" ) if (nzchar(p_common)) knitr::include_graphics(p_common) ``` Because their direction and magnitude reproduce across batches, these proteins are usually the most robust candidates for biological interpretation. This view answers "which IL13-responsive proteins behave consistently across days?". ### Interaction effect (batch-dependent proteins) The interaction volcano highlights proteins whose IL13 response *differs* between day1 and day2. Here significance is driven by the interaction coefficient, not by the average effect, so a protein can look weak in the main-effect plot yet be strongly significant here if its response changes between days. ```{r ex1-fig-interaction, echo = FALSE, out.width = "80%"} p_inter <- system.file( "extdata", "HaCaT_InteractionEffect_IL13.batchday2.png", package = "ProteinBatcher" ) if (nzchar(p_inter)) knitr::include_graphics(p_inter) ``` This view answers "which proteins show day-specific IL13 effects?". ### Deregulogram: interpreting batch-dependent effects The deregulogram compares the *full* IL13 effect between the two batch days. The y-axis is the full IL13 effect in the reference day (day1) and the x-axis is the full effect in the other day (day2), computed as main effect + interaction effect. ```{r ex1-fig-dereg, echo = FALSE, out.width = "80%"} p_dereg <- system.file( "extdata", "HaCaT_Deregulogram_IL13_vs_NoTreated_batch.png", package = "ProteinBatcher" ) if (nzchar(p_dereg)) knitr::include_graphics(p_dereg) ``` Proteins on the diagonal behave the same on both days; proteins off the diagonal are regulated differently between days. Highlighted points are those for which the interaction is both statistically significant and large enough to pass an effect-size threshold. The deregulogram is deliberately more restrictive than the interaction volcano: it emphasises interpretable, direction-changing or magnitude-shifting effects, so its highlighted set is expected to be a stricter subset of the significant interactions rather than a one-to-one match. # Example 2: workflow without interaction testing When the goal is only a global condition effect — for instance with a single experimental factor, with well-balanced and negligible batches, or when interaction testing would be underpowered — the interaction terms can be dropped from the model and `tests_interaction` set to `"NA"`. The parameters below specify a condition-only model: ```{r ex2-params} experiment <- "HaCaT" percent_missing <- 50 formula <- ~ 0 + condition tests <- c( "IL13_vs_NoTreated", "IL22_vs_NoTreated", "COMBO_vs_NoTreated" ) tests_interaction <- "NA" reference_condition <- "NoTreated" ``` Running the pipeline with these parameters proceeds exactly as before, but only the main condition effects are estimated: ```{r ex2-run} res_no_int <- run_proteomics_pipeline( path_pgmatrix = path_pgmatrix, path_annotation = path_annotation, path_output = path_output, tests = tests, tests_interaction = tests_interaction, formula = formula, reference_condition = reference_condition, percent_missing = percent_missing, ldv_source = "per-condition", experiment = experiment, plots = FALSE ) ``` With `tests_interaction = "NA"` only main effects are estimated and reported, all proteins are treated as responding homogeneously across samples, and no common-versus-interaction separation is performed. The `effects` list is still keyed by contrast, so the results are accessed the same way; the `interaction_effect` slot is simply empty. This configuration answers "which proteins are differentially abundant between conditions on average?": ```{r ex2-structure} names(res_no_int$effects) res_no_int$effects$IL13_vs_NoTreated$all_common_effect ``` # Session info ```{r session} sessionInfo() ```