--- title: "2. Getting started: from two CSVs to a SummarizedExperiment" author: "Abdelkarim Hani Ghrieb" date: "`r Sys.Date()`" output: BiocStyle::html_document: toc: true toc_float: true vignette: > %\VignetteIndexEntry{2. Getting started: from two CSVs to a SummarizedExperiment} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r getting-started-setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") ``` # Motivation rnaSentry operates on a `SummarizedExperiment`: the expression matrix in the assay slot and the sample metadata in `colData`. Most wet-lab and clinical users, however, start with two plain text files: - a **counts matrix** (genes in rows, samples in columns), and - a **clinical table** (one row per sample, with follow-up time and event plus any batch, stage, or treatment covariates). This vignette shows the exact bridge between those two files and the pipeline: read the files, keep only the samples present in both (the single most common pitfall), validate the intake with `load_counts()`, audit the design with `design_audit()`, and run the full pipeline. The example below writes two small synthetic files to a temporary directory as stand-ins for *your* `counts.csv` and `clinical.csv`; replace the file paths and column names with your own and the block works unchanged. # Build two example CSV files ```{r build-csv} library(rnaSentry) library(SummarizedExperiment) set.seed(42) n_genes <- 300 n_samples <- 30 genes <- paste0("GENE", seq_len(n_genes)) samples <- paste0("S", seq_len(n_samples)) # Raw integer counts (bulk RNA-seq style), genes x samples. cnt <- matrix(rpois(n_genes * n_samples, lambda = 200), nrow = n_genes, ncol = n_samples, dimnames = list(genes, samples)) # Clinical metadata, one row per sample. clin <- S4Vectors::DataFrame( time = round(rexp(n_samples, rate = 0.08), 1), event = rbinom(n_samples, size = 1, prob = 0.5), batch = factor(rep(c("B1", "B2", "B3"), length.out = n_samples)), stage = factor(sample(c("I", "II", "III"), n_samples, replace = TRUE)), row.names = samples ) # --- Deliberately introduce the two classic file problems ------------------- # A sample present in the counts file but not the clinical file, cnt <- cbind(cnt, S31 = cnt[, "S2"]) # and a clinical row with no matching counts column. clin <- rbind(clin, S32 = clin["S1", ]) # Plus two intake issues load_counts() should catch: a non-integer count... cnt["GENE1", "S1"] <- cnt["GENE1", "S1"] + 0.5 # ...and a gene with no counts anywhere. cnt["GENE300", ] <- 0L td <- tempfile("csvdemo") dir.create(td) write.csv(cnt, file.path(td, "counts.csv")) write.csv(clin, file.path(td, "clinical.csv")) list.files(td) ``` Every reader sees the same two files a real user exports from their pipeline: `counts.csv` (genes in rows, samples in columns) and `clinical.csv` (one row per sample). # Step 1: read both files ```{r read-csv} counts <- as.matrix(read.csv(file.path(td, "counts.csv"), row.names = 1)) clinical <- read.csv(file.path(td, "clinical.csv"), row.names = 1) dim(counts) dim(clinical) ``` Note the `as.matrix()`: `load_counts()` expects a matrix, and `read.csv()` returns a `data.frame`. # Step 2: intersect the sample IDs The most common pitfall is sample IDs that do not match between the two files: one file may contain samples the other lacks (a failed RNA extraction, a sample re-labeled, or a version mismatch). Passing them to `load_counts()` un-checked produces a hard error or, worse, silently mis-aligned columns. The fix is a single `intersect()` that keeps only samples found in **both** files: ```{r intersect-samples} n_counts_before <- ncol(counts) n_clin_before <- nrow(clinical) common <- intersect(colnames(counts), rownames(clinical)) counts <- counts[, common, drop = FALSE] clinical <- clinical[common, , drop = FALSE] data.frame( "counts columns" = c(before = n_counts_before, after = ncol(counts)), "clinical rows" = c(before = n_clin_before, after = nrow(clinical)), row.names = c("before intersect", "after intersect") ) ``` This example planted one sample in each file that the other lacks, so the 30 truly shared samples survive and `S31`/`S32` are dropped. Always check the dimensions after intersecting: if you lose hundreds of samples, suspect a version or delimiter mismatch between the two files. # Step 3: build and validate the SummarizedExperiment `load_counts()` constructs the object and records every intake issue it finds in a flag ledger stored in `metadata(se)$flags`: ```{r load-counts} se <- load_counts(counts, clinical) se ``` ```{r load-flags} S4Vectors::metadata(se)$flags ``` The ledger flags the two problems we planted (the non-integer count and the all-zero gene) and carries them into the rendered report's audit trail. Duplicate sample IDs, missing metadata, and Ensembl-style gene IDs are caught the same way. The pipeline still runs on a flagged object -- the ledger's job is to make sure you saw the issue first. # Step 4: audit the design before modeling `design_audit()` tests every candidate covariate against the leading expression principal component and flags potential confounders, before any per-gene modeling is invested in: ```{r design-audit} da <- design_audit(se, design_vars = c("batch", "stage")) da ``` # Step 5: run the full pipeline `run_rnaSentry()` orchestrates design audit, PCA audit, signature construction with repeated cross-validation, and the survival-modeling stages on the same object. It needs a numeric follow-up `time` column and a `0/1` `event` indicator in `colData`: ```{r run-pipeline, warning = TRUE} run <- run_rnaSentry(se, time_col = "time", event_col = "event", top_n = 5, repeats = 1, folds = 2, seed = 42, design_vars = c("batch", "stage"), render_report = FALSE) run ``` On 30 samples the `events_per_parameter` guardrail warns that cross-validated concordance is unstable -- exactly as it should: this cohort is a demonstration, not a discovery set. See `vignette("case-study-small-cohort")` for what that warning means and `?generate_report` for how to render the audited HTML report of a run. The run locked the discovered signature: `run_rnaSentry()` is single-use per session, so a second call in the same session is refused until the lock is released. This is the reproducibility guardrail against silently re-selecting signature genes after survival analysis. ```{r unlock-signature} run$stages$build_signature <- lock_signature(run$stages$build_signature, lock = FALSE) ``` # Assay naming: `counts` vs `logcounts` (`exprs` for microarray) `load_counts()` stores your matrix under the assay name `counts`, and the pipeline computes `log2(counts + 1)` when it runs. If your input file is *already normalized* (CPM, TPM, VST), store it under the name `logcounts` instead so it is used as-is and not double-logged. For microarray log2 intensities, use the conventional assay name `exprs`, which the pipeline likewise uses as-is: ```{r assay-naming} se_norm <- se SummarizedExperiment::assayNames(se_norm) <- "logcounts" ``` Either way the bridge is identical; only the assay name tells the pipeline which scale your data is in. # Session info ```{r session-info} sessionInfo() ```