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:
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.
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
#> # A tibble: 1 × 8
#> level test statistic p_value n_x n_y median_x median_y
#> <chr> <chr> <dbl> <dbl> <int> <int> <dbl> <dbl>
#> 1 cell mann_whitney 904417 0 969 941 4143. 553.
res$rep_level
#> # A tibble: 1 × 8
#> level test statistic p_value n_x n_y median_x median_y
#> <chr> <chr> <dbl> <dbl> <int> <int> <dbl> <dbl>
#> 1 replicate mann_whitney 256 0.00000154 16 16 4175. 550.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:
res$effect_sizes
#> # A tibble: 4 × 5
#> method estimate ci_low ci_high magnitude
#> <chr> <dbl> <dbl> <dbl> <chr>
#> 1 cohens_d 6.59 4.71 8.48 large
#> 2 hedges_g 6.43 4.59 8.26 large
#> 3 cliffs_delta 1 NA NA large
#> 4 rank_biserial 1 NA NA largeThose 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.
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 |
set.seed(1)
cr_effect_size(rnorm(100, 1), rnorm(100, 0))
#> # A tibble: 5 × 5
#> method estimate ci_low ci_high magnitude
#> <chr> <dbl> <dbl> <dbl> <chr>
#> 1 cohens_d 1.23 0.930 1.54 large
#> 2 hedges_g 1.23 0.927 1.53 large
#> 3 cliffs_delta 0.616 0.481 0.723 large
#> 4 rank_biserial 0.616 0.481 0.723 large
#> 5 glass_delta 1.20 0.872 1.52 largeBootstrap intervals are available where the analytic ones do not apply:
“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:
logit <- cr_logistic(exp, "marker_1", "CompoundA_high", "Untreated")
cr_auc(logit)
#> # A tibble: 1 × 4
#> auc ci_low ci_high method
#> <dbl> <dbl> <dbl> <chr>
#> 1 0.992 0.989 0.995 delong
cr_plot_roc(logit)cr_test_all() runs every treatment level against the
control and reports the adjustments alongside the
unadjusted p-value, never in place of it:
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")]
#> # A tibble: 5 × 6
#> treatment log2_fc p_value p_bonferroni p_BH interpretation
#> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 PosControl 3.10 0.00000154 0.00000772 0.00000386 strong
#> 2 CompoundA_low 0.916 0.0000312 0.000156 0.0000520 strong
#> 3 CompoundA_high 2.92 0.00000154 0.00000772 0.00000386 strong
#> 4 CompoundB 0.314 0.109 0.546 0.109 no evidence
#> 5 CompoundC 0.578 0.0000433 0.000216 0.0000541 moderateThe 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.
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.
cr_power_analysis(effect_size = 0.3,
n_replicates = 4,
n_cells_per_rep = 20,
n_sim = 200,
seed = 1)
#> # A tibble: 1 × 5
#> effect_size n_replicates n_cells_per_rep alpha power
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 0.3 4 20 0.05 0.355Read 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").
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.