---
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.