--- title: "Statistical Analysis of Cell-Based Assays" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Statistical Analysis of Cell-Based Assays} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} resource_files: - figures/logo.svg - figures/logo-dark.svg --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4, fig.align = "center") options(width = 84) library(cellreportR) ``` ## Why the level of aggregation matters In a microscopy-based cell assay each treatment condition is measured across several wells, and each well holds many cells. Cells within a well are not independent: they share staining, focus and exposure. Treating every cell as an independent replicate inflates the sample size and, with it, the confidence of the answer. cellreportR supports both resolutions and asks you to say which one you mean: * **Cell-level** tests treat each cell as a data point. Use them for distributional questions — is there a subpopulation shift, does a marker separate two conditions. * **Replicate-level** tests aggregate cells to one number per well and test wells. Use them for the primary inference about whether a compound has an effect. Run both, report the effect size with its interval at the replicate level, and let the difference between the two be visible rather than assumed. ## A worked example ```{r workflow} exp <- cr_example_experiment(seed = 2, n_cells_per_well = 60) res <- cr_test(exp, channel = "marker_1", treatment = "CompoundA_high", control = "Untreated", test = "mann_whitney", level = "both") res$cell_level res$rep_level ``` Same data, same contrast. The cell-level test is run on nearly two thousand observations and the replicate-level test on thirty-two, and the p-values differ by orders of magnitude. The extra cells did not add independent information; they only made the arithmetic behave as though they had. `cr_test()` also returns effect sizes, computed on the cells: ```{r effect-sizes} res$effect_sizes ``` Those intervals are cell-level intervals whatever `level` was asked for, so they are narrow for exactly the reason the cell-level p-value is small. For an interval that respects the unit of replication, use `cr_effect_grid()` with `unit` set — `vignette("end-to-end")` puts the two side by side and measures the gap. ## Effect size interpretation `cr_effect_size()` computes Cohen's *d*, Hedges' *g*, Cliff's delta, rank-biserial correlation and Glass's delta, each with a confidence interval. The verbal magnitude attached to an estimate uses the conventional benchmarks: | Magnitude | Cohen's *d*, Hedges' *g*, Glass's delta | Cliff's delta, rank-biserial | |---|---|---| | negligible | \|est\| < 0.2 | \|est\| < 0.147 | | small | 0.2 -- 0.5 | 0.147 -- 0.33 | | medium | 0.5 -- 0.8 | 0.33 -- 0.474 | | large | ≥ 0.8 | ≥ 0.474 | ```{r effectsize} set.seed(1) cr_effect_size(rnorm(100, 1), rnorm(100, 0)) ``` Bootstrap intervals are available where the analytic ones do not apply: ```{r effectsize-boot} set.seed(1) cr_effect_size(rnorm(60, 0.8), rnorm(60, 0), method = "cliffs_delta", ci_method = "bootstrap", n_boot = 200, seed = 1) ``` ## ROC and AUC for discriminability "Does this marker tell a treated cell from a control cell?" is a different question from "did the group means move", and it has a different answer. Fit a univariate logistic regression and read the area under the curve: ```{r roc} logit <- cr_logistic(exp, "marker_1", "CompoundA_high", "Untreated") cr_auc(logit) cr_plot_roc(logit) ``` ```{r confusion} cr_confusion_matrix(logit, threshold = 0.5) ``` ## Multiple testing `cr_test_all()` runs every treatment level against the control and reports the adjustments **alongside** the unadjusted p-value, never in place of it: ```{r adjust} all_res <- cr_test_all(exp, "marker_1", "Untreated", level = "replicate") attr(all_res, "summary")[, c("treatment", "log2_fc", "p_value", "p_bonferroni", "p_BH", "interpretation")] ``` The `interpretation` column combines the adjusted p-value with the effect size: a contrast is `"weak"`, `"moderate"` or `"strong"` only when `p_adj < 0.05`, and `"no evidence"` otherwise. ## Power and sample size `cr_power_analysis()` simulates the two-sample t-test that a replicate-level analysis actually runs: `n_replicates` replicate means per arm, each the average of `n_cells_per_rep` draws, tested against each other. ```{r power} cr_power_analysis(effect_size = 0.3, n_replicates = 4, n_cells_per_rep = 20, n_sim = 200, seed = 1) ``` Read the number for what the simulation models. The only source of spread between replicate means here is the averaging itself, so power rises steeply with `n_cells_per_rep`. Real units also differ from one another for reasons that more cells cannot average away — staining, focus, seeding density — and that component is not in this model. This is an idealized independent-cell scenario, not a formal upper bound or a validated prediction of laboratory power. A design decision needs an explicit between-unit variance model or an effect and variance estimated at the independent experimental-unit level. For sizing a follow-up study from effects you have actually observed rather than from an assumed one, use `cr_power_grid()`, which solves from the confidence bound nearer the null and so inherits the between-unit variability of the data it was given — see `vignette("end-to-end")`. ## Where this fits * `vignette("getting-started")` — the short path on one plate. * `vignette("end-to-end")` — the same choice of level made across a whole screen, with `cr_effect_grid()` and `cr_compare_levels()` quantifying the gap between the two.