---
title: "3. Naive analysis vs rnaSentry: what the guardrails prevent"
author: "Abdelkarim Hani Ghrieb"
date: "`r Sys.Date()`"
output:
  BiocStyle::html_document:
    toc: true
    toc_float: true
vignette: >
  %\VignetteIndexEntry{3. Naive analysis vs rnaSentry: what the guardrails prevent}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r impact-setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

# The three-tier structure

The case studies are written as three acts. Tier 1 runs the analysis the way
a standard, unguarded pipeline would, and prints the headline a researcher
would have believed. Tier 2 runs the *same data* through `rnaSentry` and
shows which guardrail fired, with the statistic that justified it. Tier 3
states the counterfactual: the error the guardrails prevented, and the true
signal the guarded analysis still recovered.

Four failure modes -- the ways a prognostic-signature study most often goes
wrong -- are covered by the case studies:

| # | Failure mode | Where it is demonstrated |
|---|---|---|
| 1 | **Wrong direction**: an inverted concordance convention turns a strong signature into a "failure" | `vignette("case-study-brca")` |
| 2 | **Confounded design**: batch-driven genes masquerade as prognostic; collinear terms are double-counted | `vignette("case-study-confounder-audit")` |
| 3 | **Underpowered discovery**: a mean C-index hides coin-flip folds | `vignette("case-study-small-cohort")` |
| 4 | **False transfer**: screening-internal CV optimism is mistaken for cross-cohort validity | README / main vignette (LUAD honest negative, below) |

# The master impact table

| Failure mode | Naive headline | Guarded headline | Guardrails that fired | What a guarded analysis can claim |
|---|---|---|---|---|
| Wrong direction (GSE20685) | "CV C = 0.217, signature fails to generalize (winner's curse)" | held-out C = **0.783**, direction correct, far above the 0.582 random-gene control | `reverse = TRUE` convention; `C + C_rev = 1` parity tests; direction-sensitive regression tests | the signature is not a 0.22 collapse but a directionally correct held-out signal (CV alone does not exceed selection-noise controls; see BRCA Limitations) |
| Confounded design (synthetic batch cohort) | "six batch-driven genes are prognostic; batch and region are two independent risk factors" | `batch`/`region` flagged redundant (Cramer's V = 0.82); `batch` surrogate eta-squared 0.97; minimal design `~ batch`; adjusted screening drops the batch-artifact genes | `redundant_variable`, surrogate-association flags, `adjusted_screening` | the discovered signature is not a batch echo |
| Underpowered discovery (30 samples, 23 events) | "cross-validated C = 0.74 -- decent" | fold range **0.50-1.00**, sd 0.14, 4.6 events/parameter < 5 | `events_per_parameter`, fold-level reporting | the concordance is an average over coin-flip folds; hypothesis-generating only |
| False transfer (GSE31210 -> GSE50081, LUAD) | "CV C = 0.86, the signature is validated" | CV C = 0.862 vs permutation null 0.836 (delta +0.026); external C = **0.540**, log-rank p = **0.266** -- transfer *not* demonstrated | permutation-null calibration; honest-negative external validation; power analysis | no false claim of cross-cohort validity; the negative is a power finding (35 events), not a tool failure |

# Why the first failure mode is worth dwelling on

A sign-convention bug is the one error class that flips a paper's conclusion
without any red flag in the data. `survival::concordance()` defaults to
*larger score means longer survival*; a Cox risk score means the opposite.
Calling the default on a risk score returns `1 - Harrell's C`. The identity
below is what the parity test suite now asserts on every fold:

```{r sign-convention}
library(rnaSentry)
library(SummarizedExperiment)
library(survival)

# Synthetic cohort for the sign-convention demo (real GEO via GEOquery
# shown in case-study-brca; synthetic used here for offline builds).
# NOTE: seed deliberately differs from any GEO accession so future
# `git log -S "set.seed(20685)"` audits stay unambiguous.
set.seed(42)
n_genes <- 3000
n_samples <- 327
genes <- paste0("GENE", seq_len(n_genes))
samples <- paste0("Sample", seq_len(n_samples))
mat <- matrix(rnorm(n_genes * n_samples, mean = 6, sd = 1.5),
              nrow = n_genes, ncol = n_samples,
              dimnames = list(genes, samples))
risk0 <- colMeans(mat[1:5, , drop = FALSE])
event_time <- rexp(n_samples, rate = 0.08 * exp(0.6 * scale(risk0)[, 1]))
censor_time <- rexp(n_samples, rate = 0.04)
time <- pmin(event_time, censor_time)
event <- as.integer(event_time < censor_time)
coldata <- S4Vectors::DataFrame(time = time, event = event,
                                row.names = samples)
se <- SummarizedExperiment(assays = list(logcounts = mat), colData = coldata)
score <- colMeans(assays(se)$logcounts[1:5, , drop = FALSE])
c_default <- as.numeric(concordance(Surv(se$time, se$event) ~
                                    score)$concordance[1])
c_reverse <- as.numeric(concordance(Surv(se$time, se$event) ~ score,
                                    reverse = TRUE)$concordance[1])
c(c_default = c_default, c_reverse = c_reverse, sum = c_default + c_reverse)
```

A naive reading of the default concordance convention reports 0.349 and
would rationalize it as winner's curse; `1 - 0.349 = 0.651` is the correct,
biologically expected value all along. The same class of error has appeared
in published survival literature; rnaSentry pins the convention in code, in
parity tests, and in the rendered report.

## Pipeline execution on the same cohort

Running `rnaSentry` on the synthetic cohort above demonstrates the guardrail
flags in action:

```{r pipeline-demo, warning = TRUE}
run <- run_rnaSentry(se, time_col = "time", event_col = "event",
                     top_n = 5, repeats = 2, folds = 3, seed = 42,
                     render_report = FALSE)
run$stages$build_signature$flags
```

Every flag in the ledger is backed by a statistic. The `events_per_parameter`
warning, the `reverse = TRUE` convention, and the fold-level concordance
distribution are the guardrails that prevent the three failure modes
demonstrated in the case studies above.

```{r unlock-impact}
run$stages$build_signature <-
    lock_signature(run$stages$build_signature, lock = FALSE)
```

# The LUAD honest negative (GSE31210 -> GSE50081)

The fourth failure mode is demonstrated on independent GEO cohorts in the
README and main vignette; the numbers are reproduced via
`validation/repro_live_geo.R` in the GitHub repository. Because the data are
downloaded live, the walkthrough is narrative here rather than a
build-time vignette:

- **Discovery** GSE31210 (LUAD, 35 deaths), **external validation** GSE50081
  (n = 128, 52 deaths).
- Screening-internal CV C = 0.862 against a permutation null of 0.836
  (delta +0.026) -- even survival-permuted data yields a high null CV (about
  0.8 on ~21k genes), so the CV must be calibrated against a matched null.
- External validation: **C = 0.540, log-rank p = 0.266** -- transfer was
  *not* demonstrated, and the tool reports an honest negative rather than a
  "validated signature."
- A power analysis in the simulation study (`validation/simulate_study.R`)
  quantifies why: transfer power < 0.30 at ~35 discovery events, and >= 0.80
  only near ~300 events for a C ~ 0.65 signature.

## Demonstrating the honest-negative concept with synthetic data

The same pattern -- high screening-internal CV but poor external transfer --
can be reproduced on a small synthetic cohort:

```{r luad-null-demo}
set.seed(7)
n_genes <- 120
n_samples <- 80
cnt <- matrix(rpois(n_genes * n_samples, lambda = 500),
              nrow = n_genes, ncol = n_samples,
              dimnames = list(paste0("gene", seq_len(n_genes)),
                              paste0("S", seq_len(n_samples))))
sig_expr <- colMeans(cnt[1:5, , drop = FALSE])
risk <- scale(sig_expr)[, 1] * 0.4
event_time <- rexp(n_samples, rate = 0.03 * exp(0.8 * risk))
censor_time <- rexp(n_samples, rate = 0.02)
cd <- S4Vectors::DataFrame(time = pmin(event_time, censor_time),
                           event = as.integer(event_time < censor_time),
                           row.names = colnames(cnt))
se_null <- SummarizedExperiment(assays = list(counts = cnt), colData = cd)
discovery <- se_null[, seq_len(60)]
external <- se_null[, 61:80]

sig_null <- build_signature(discovery, "time", "event", top_n = 5,
                            repeats = 2, folds = 3, seed = 7)
cat("Screening-internal CV C:", mean(sig_null$cv_results$c_index), "\n")

km_null <- km_curve(sig_null, discovery)
val_null <- validate_external(sig_null, external,
                              cutpoint = km_null$cutpoint)
cat("External C:", val_null$concordance, "\n")
cat("External log-rank p:", val_null$log_rank_p, "\n")
```

The screening-internal CV is optimistic (high C) but the external validation
on the held-out half is poor -- exactly the honest-negative pattern the
tool is designed to surface rather than hide.

# Reading this package's claims

- **Every number above is reproducible.** The breast-cancer case study downloads
  the live GSE20685 series from GEO via `GEOquery` (which caches the series
  matrix locally), with an evaluated synthetic fallback of identical
  structure if the download is unavailable, and the live-GEO LUAD numbers
  are reproduced by `validation/repro_live_geo.R` in the GitHub repository.
- **The guardrails are testable.** Each flag in the report's audit trail is a
  `(check, severity, detail, stage)` record backed by a statistic, and each
  has a regression test asserting the condition that fires it.
- **This is a demonstration, not a clinical claim.** All signatures are
  provisional.

# Session info

```{r session-info}
sessionInfo()
```
