← All posts · Michael Baffour Awuah

R Shiny app + Python CLI · open source

Single-cell morphometry, with the statistics done right.

Measuring cell shape over time is the easy half. CellMorphR does the honest half — summarise each replicate first, then test at the level the design supports — so the insight in your figure survives review instead of quietly resting on pseudoreplication.

Per-replicate summaries by default No pseudoreplication Colourblind-safe figures
Median cell area SuperPlot · N = 3
Area (µm²) Control Infected
Small translucent dots are individual cells; the large dots are the three replicate medians the inference actually rests on. You see the biology and the honest N in one glance.
The core idea

Hundreds of cells from three dishes are not hundreds of data points.

The cells in one dish shared a culture, an infection event, an imaging session — they are correlated. Count each as independent and you inflate N roughly two-hundred-fold, manufacturing significance from differences that may be biologically trivial. CellMorphR refuses to let you.

The tempting mistake

Test every cell as independent

n = 4,800 p < 0.0001
✗ inflated The p-value is not the one your experiment earned — and reviewers know it.
What CellMorphR does

Collapse to replicate means, then test

N = 3 dishes two-way ANOVA
✓ honest Your true N is the number of dishes. The interaction test is the one your design supports.
What it does

Everything you need to quantify shape — and defend the numbers.

CellMorphR is built around one conviction: with hierarchical microscopy data, the analysis you reach for by default is usually the wrong one. It fixes that first, then hands you figures a reviewer will trust.

Anti-pseudoreplication by default

Hundreds of cells per dish are not independent data points. Each biological replicate collapses to one summary before any test runs — so your true N is the number of dishes, not the number of cells. This is the whole point.

Two-way ANOVA on replicate summaries

A factorial ANOVA over Condition × Time on per-replicate medians. The Condition × Time interaction is the key test — it tells you whether the effect grows over time. Partial η² is reported for every term.

Linear mixed model alternative

For more power, fit Measurement ~ Condition * Time + (1|Rep) over all cell-level data. A random intercept absorbs within-dish correlation; Satterthwaite p-values come from lmerTest. Replicate IDs are made unique automatically to avoid a silent modelling error.

Replicate-level trajectories

The most important figure: each replicate's summary plotted over time, per condition, with loess trends and an SEM ribbon computed at the correct level. Diverging trajectories are the visual signature of the interaction you're reporting.

Seven distribution styles, SuperPlots included

Violin, strip, sina, box, ridge, ECDF, and a replicate-only dot plot (Lord et al., 2020). Every style overlays per-replicate medians, so statistical power is shown honestly, not hidden by a smooth curve.

Welch pairwise tests, Holm-corrected

Where the interaction is significant, pairwise Welch's t-tests on replicate medians pinpoint when conditions diverge. Holm–Bonferroni controls the family-wise error rate, and the caption names the test, the N, and the correction.

Effect sizes, independent of p-values

Absolute difference, percent change from control, and replicate-level Cohen's d — plotted over time. With N = 3 a real 30% enlargement can miss p < 0.05; effect sizes say how big it is regardless. “125% larger at 120 min” communicates biology directly.

Assumption diagnostics, run for you

Shapiro–Wilk on residuals and Levene's test with median centering (the same variant Prism uses) run automatically and print into the Statistics tab. If an assumption is badly violated, you'll know before you submit.

Okabe–Ito colourblind-safe palettes

The default palette is Okabe–Ito (viridis optional), scaling to any number of conditions — legible to readers with colour-vision deficiency, not just to you. Selectable in the app and via --palette in the CLI.

Unit-aware axis titles

Set a unit once and axis titles render as Area (µm²), Median Area (µm²), and so on across every tab and panel. Leave it blank and nothing changes — the kind of small thing a copy-editor otherwise sends back.

Composite publication figures

Assemble panels A (distributions), B (trajectories) and C (effect sizes) into a single labelled, multi-panel figure with patchwork. Panel letters follow journal conventions, with a global title and subtitle above the row.

Five export formats, journal-ready

PDF, SVG and EPS vectors, plus PNG and LZW-compressed TIFF raster — at configurable DPI on every tab. Defaults (300 DPI, sans-serif, 11 pt) already meet Nature / Science / Cell specs.

ECDFs — assumption-free comparison

One panel per time point, one step per cell — no binning, no bandwidth. A rightward shift means larger cells; a tail-only divergence reveals a filamenting subpopulation the summary statistics would hide.

R and Python give identical numbers

Both implementations run the same ANOVA, Welch tests and Holm correction, agreeing to at least three decimals on the same data. Explore in Shiny; batch it in a pipeline with the CLI. The choice of tool never changes the result.

Machine-readable stats output

Save the full ANOVA and pairwise results as text, plus a pairwise_*.csv for downstream use. Demo data with a known ground-truth effect ships in both tools so you can validate your pipeline before trusting it.

~4,800
cells collapse to the N that actually matters
7
distribution styles, SuperPlots included
2
interfaces — R Shiny app & Python CLI
5
export formats: PDF, SVG, EPS, PNG, TIFF
Why this exists

A picture wasn't enough. So I built the analysis.

By Michael Baffour Awuah · on turning microscopy into insight you can defend

While writing a paper, I did a lot of microscopy. Phase-contrast image after phase-contrast image, cells over time, conditions side by side. And at some point I hit a wall I didn't expect: the pictures were beautiful, but a picture on its own often couldn't tell the story I was trying to tell. You can see that the infected cells look bigger. Convincing a reader — and a reviewer — that they are bigger, and by how much, is a different job entirely.

So I needed a way to get real insight out of the images — segment them, pull out areas and perimeters and Feret diameters, and turn that beautiful-but-mute picture into a number I could stand behind. That part felt tractable. The part that quietly worried me was the statistics.

Because here's the trap. After segmentation you have thousands of cells in a spreadsheet — hundreds per dish, a few time points, three biological replicates. The temptation is almost irresistible: drop the whole column into a t-test, watch the software report p < 0.0001, and feel bulletproof. It's the most natural thing in the world to do. And it's wrong.

Hundreds of cells from three dishes are not hundreds of independent data points. The p-value that comes out is not the one your experiment earned.

That's pseudoreplication, and it's the half of the job that quietly breaks papers. The cells in one dish shared a culture, an infection event, an imaging session — they're correlated. Counting each as a separate observation inflates your sample size roughly two-hundred-fold and manufactures significance out of differences that may be biologically trivial. The statistical unit is the dish, not the cell — and it took me an embarrassingly long time to really internalise that.

What I wanted was a tool that did the right thing by default — one where the correct analysis was the path of least resistance. So CellMorphR does three things in a fixed order, and the order is the point. It displays every cell — violins, strips, sina plots, ECDFs — so you can see the heterogeneity that makes single-cell data worth collecting. It summarises at the replicate level, collapsing hundreds of cells to one median per dish. And it tests on those summaries, so the p-values reflect the sample size the experiment truly has.

The distribution plots overlay the per-replicate medians on the cell cloud, following the SuperPlots idea — so in one glance you see both the shape of the data and the handful of independent points the inference rests on. And it ships two ways because I work two ways: an R Shiny app for exploring, a Python CLI for pipelines, verified to give identical results on the same data.

I should be honest about how it got built. I'm a scientist who's gotten good at building tools with AI — not a career software engineer. The statistical thinking and the frustration that started the whole thing are mine; the implementation I did in close collaboration with Claude. And weirdly, building it is what made me understand pseudoreplication properly — when you have to decide, in actual code, what a “replicate” is and at what level a test runs, you can't hand-wave anymore.

It isn't finished, and it has gaps I know about. But it runs, both interfaces agree, and it has already kept my own analyses honest. That feels like the right bar for whether something was worth building — and it turns out you can just build the thing your work needs.

A note on how it's built The statistical decisions and the anti-pseudoreplication core are the part worth protecting, and they're deliberate. The plumbing — Shiny reactives, the ANOVA and mixed-model code, keeping the R and Python outputs identical — was written collaboratively with Claude. Building tools this way lets a specific lab frustration become something other people can use, faster than working alone.
How it works

Display, summarise, test — in that order.

The sequence isn't cosmetic. Show the cells, collapse them to replicate summaries, then run inference at the level the design supports. One tidy file in; a defensible figure out.

Display

Every cell is shown — violins, strips, sina plots, ECDFs — so the biological heterogeneity that makes single-cell data worth collecting stays visible, never averaged away.

Summarise

Each biological replicate collapses to a single median. Technical replicates from the same dish pool automatically, so one dish contributes exactly one point.

Test

Two-way ANOVA on replicate summaries, or a linear mixed model over all cells with replicate as a random effect. The p-value reflects the N you truly have.

1

Shape your data

One row per cell, with Condition, Time_min and Replicate columns plus any numeric measurements. All conditions and time points in one file.

2

Label replicates honestly

Every image from the same dish gets the same Replicate number. Technical replicates pool — that's what makes N the count of dishes.

3

Load it, pick a measure

Drop the file into the Shiny app or point the CLI at it. Choose the measurement variable and control. No data? Load the built-in demo with a known effect.

4

Read the trajectories

Distributions with per-replicate medians overlaid, then the trajectory plot — diverging lines are the interaction you'll report.

5

Run the statistics

Two-way ANOVA or mixed model. Read the Condition × Time row, check the Welch pairwise comparisons, glance at the auto-run diagnostics.

6

Export the figure

Compose a labelled A/B/C panel figure and export as PDF, SVG, EPS, PNG or TIFF at your target DPI — with the stats saved as text and CSV alongside.

Free & open source

Keep your inference as honest as your images.

Available as both an R Shiny app and a Python CLI, with a built-in demo dataset. Bring your morphometry spreadsheet and get figures a reviewer will trust.