--- title: "WiNN Tutorial: A Reproducible LC-MS Example" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{WiNN Tutorial: A Reproducible LC-MS Example} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4, message = FALSE, warning = FALSE ) library(winn) ``` This vignette runs a single, fully reproducible WiNN workflow on simulated LC-MS data. Every object is created inside the document, so the example can be rerun unchanged during `R CMD check`. To keep the vignette lightweight, it uses only base R plus `winn`, avoids slow parameter-grid searches, and focuses on a single before/after comparison. ## 1. Simulate a small LC-MS dataset We simulate 30 metabolites measured across 96 injections. Every eighth injection is a pooled QC sample, so QC values should be close after technical correction. The observed matrix includes dilution, drift, batch shifts, and random noise layered on top of a known ground-truth signal. ```{r simulate} set.seed(42) n_met <- 30L n_samples <- 96L n_batches <- 4L batch <- rep(seq_len(n_batches), each = n_samples / n_batches) run_order <- seq_len(n_samples) qc_idx <- seq(4, n_samples, by = 8) study_idx <- setdiff(seq_len(n_samples), qc_idx) base_mean <- rnorm(n_met, mean = 7.8, sd = 0.35) true_log <- base_mean + matrix(rnorm(n_met * n_samples, sd = 0.08), nrow = n_met) signal_metabolites <- 1:6 study_signal <- as.numeric(scale(sin(study_idx / 9) + rnorm(length(study_idx), sd = 0.2))) true_log[signal_metabolites, study_idx] <- true_log[signal_metabolites, study_idx] + 0.30 * matrix( study_signal, nrow = length(signal_metabolites), ncol = length(study_idx), byrow = TRUE ) covariate_metabolites <- 7:10 continuous_covariate <- numeric(n_samples) continuous_covariate[study_idx] <- as.numeric(scale(runif(length(study_idx)))) true_log[covariate_metabolites, ] <- true_log[covariate_metabolites, ] + 0.20 * matrix( continuous_covariate, nrow = length(covariate_metabolites), ncol = n_samples, byrow = TRUE ) pooled_qc <- rowMeans(true_log[, study_idx, drop = FALSE]) true_log[, qc_idx] <- pooled_qc make_batch_drift <- function(n, amplitude) { x <- seq(0, 1, length.out = n) amplitude * (0.8 * x - 0.6 * x^2 + 0.15 * sin(2 * pi * x)) } batch_drift <- numeric(n_samples) drift_amplitude <- c(0.22, 0.14, 0.18, 0.10) for (b in seq_len(n_batches)) { idx <- which(batch == b) batch_drift[idx] <- make_batch_drift(length(idx), drift_amplitude[b]) } metabolite_drift_scale <- matrix( 0.05 + 0.25 * runif(n_met), nrow = n_met, ncol = n_samples ) drift_log <- matrix(batch_drift, nrow = n_met, ncol = n_samples, byrow = TRUE) * metabolite_drift_scale batch_shift <- matrix(0, nrow = n_met, ncol = n_samples) batch_centers <- c(-0.18, 0.05, 0.12, -0.08) for (b in seq_len(n_batches)) { idx <- which(batch == b) batch_shift[, idx] <- rnorm(n_met, mean = batch_centers[b], sd = 0.04) } dilution_factor <- exp(rnorm(n_samples, sd = 0.06)) noise_log <- matrix(rnorm(n_met * n_samples, sd = 0.08), nrow = n_met) observed_log <- true_log + matrix(log(dilution_factor), nrow = n_met, ncol = n_samples, byrow = TRUE) + drift_log + batch_shift + noise_log true_intensity <- pmax(expm1(true_log), 0) observed_intensity <- pmax(expm1(observed_log), 0) ``` ## 2. Run WiNN For a vignette, fixed settings are the most practical choice because they are fast and deterministic. The auto-tuning mode is still available for full analyses when you want QC-guided parameter selection. ```{r run-winn} corrected_intensity <- winn( observed_intensity, batch = batch, run_order = run_order, control_samples = qc_idx, parameters = "fixed", fdr_threshold = 0.05, median_adjustment = "shrink", remove_batch_effects = "anova", lag = NULL, scale_by_batch = FALSE ) ``` ## 3. Summarize agreement with the known truth We use three metrics: - Mean per-metabolite Pearson correlation on the `log1p` scale - Mean per-metabolite ICC(A,1) on the `log1p` scale - Mean QC coefficient of variation (QC-CV) on the intensity scale Higher Pearson and ICC indicate stronger agreement with the ground-truth profiles. Lower QC-CV indicates tighter pooled controls. ```{r metrics} mean_qc_cv <- function(x, qc_samples) { qc_values <- x[, qc_samples, drop = FALSE] qc_cvs <- apply(qc_values, 1, function(v) { mu <- mean(v, na.rm = TRUE) if (!is.finite(mu) || abs(mu) < .Machine$double.eps) { return(NA_real_) } sd(v, na.rm = TRUE) / abs(mu) }) mean(qc_cvs, na.rm = TRUE) } icc_a1 <- function(reference, candidate) { keep <- is.finite(reference) & is.finite(candidate) reference <- reference[keep] candidate <- candidate[keep] n <- length(reference) k <- 2L if (n < 2L) { return(NA_real_) } ratings <- cbind(reference, candidate) grand_mean <- mean(ratings) row_means <- rowMeans(ratings) col_means <- colMeans(ratings) ss_rows <- k * sum((row_means - grand_mean)^2) ss_cols <- n * sum((col_means - grand_mean)^2) ss_total <- sum((ratings - grand_mean)^2) ss_error <- ss_total - ss_rows - ss_cols ms_rows <- ss_rows / (n - 1) ms_cols <- ss_cols / (k - 1) ms_error <- ss_error / ((n - 1) * (k - 1)) denom <- ms_rows + (k - 1) * ms_error + (k * (ms_cols - ms_error) / n) if (!is.finite(denom) || denom <= 0) { return(NA_real_) } (ms_rows - ms_error) / denom } calc_metrics <- function(candidate, truth, qc_samples) { candidate_log <- log1p(candidate) truth_log <- log1p(truth) pearson_vals <- vapply(seq_len(nrow(candidate)), function(i) { cor(candidate_log[i, ], truth_log[i, ], use = "pairwise.complete.obs") }, numeric(1)) icc_vals <- vapply(seq_len(nrow(candidate)), function(i) { icc_a1(truth_log[i, ], candidate_log[i, ]) }, numeric(1)) data.frame( mean_pearson = mean(pearson_vals, na.rm = TRUE), mean_icc = mean(icc_vals, na.rm = TRUE), mean_qc_cv = mean_qc_cv(candidate, qc_samples) ) } summary_metrics <- rbind( raw = calc_metrics(observed_intensity, true_intensity, qc_idx), winn = calc_metrics(corrected_intensity, true_intensity, qc_idx) ) knitr::kable( round(summary_metrics, 4), caption = "Before/after metrics for the simulated dataset." ) ``` On this simulated dataset, WiNN should increase agreement with the known truth while reducing pooled-QC variability. ## 4. Visualize a few metabolites The plot below shows four representative metabolites. Grey traces are the observed measurements, dashed black traces are the true underlying signal, and blue traces are the WiNN-corrected values. Orange points mark pooled QC injections. ```{r plot, fig.height=6} met_ids <- c(1, 6, 9, 14) panel_breaks <- (n_samples / n_batches) * seq_len(n_batches - 1) + 0.5 op <- par(mfrow = c(2, 2), mar = c(3.5, 3.5, 2.5, 1)) for (met in met_ids) { y_raw <- log1p(observed_intensity[met, ]) y_true <- log1p(true_intensity[met, ]) y_winn <- log1p(corrected_intensity[met, ]) y_lim <- range(c(y_raw, y_true, y_winn), finite = TRUE) plot( run_order, y_raw, type = "l", col = "grey55", lwd = 1, ylim = y_lim, xlab = "Run order", ylab = "log1p(Intensity)", main = paste("Metabolite", met) ) abline(v = panel_breaks, lty = 3, col = "grey80") lines(run_order, y_true, col = "black", lty = 2, lwd = 1.2) lines(run_order, y_winn, col = "#1b6ca8", lwd = 1.4) points(qc_idx, y_winn[qc_idx], pch = 16, cex = 0.6, col = "#d95f02") } par(op) ``` ## 5. Takeaways - The vignette is self-contained: it generates its own data and reruns without external files or internet access. - A fixed-parameter WiNN run is fast enough for documentation while still showing the full correction workflow. - Pearson, ICC, and QC-CV provide complementary checks: signal recovery, agreement, and QC tightness.