# Transwell migration and invasion assay statistics: fields, inserts, percent of control and what counts as n

> Average fields within inserts and inserts within experiments, then compare conditions as a log ratio per experiment. Fields as n, or a Student's t-test against a control fixed at 100%, give false positives. Python and R code.

Canonical: https://www.licklider.ai/guides/transwell-migration-invasion/
Section: Guides › Normalised data and fold change
Author: Licklider · Published 2026-11-12 · Last reviewed 2026-11-12 (UTC)

> **Quick answer.** In a transwell (Boyden chamber) assay, average the cell counts of the fields within each insert, average the inserts of each condition within each **independent experiment**, and compare conditions with one number per experiment: the **log ratio** treated ÷ control, tested against 0 with a **one-sample t-test** (n = experiments; every mean must be above 0). Normalising to the control of the same experiment removes the large day-to-day differences in counts. In a simulation of 4 experiments (2 inserts, 5 fields each) with no real effect, two common shortcuts gave false positives: treating **fields as n** (11.9%, where 5% is expected) and comparing **percent of control against a control column fixed at 100%** with a Student's t-test (8.9%). The log ratio gave 4.8%, and percent of control tested against 100 with a one-sample t-test gave 4.6%.

**Terms used below.** *Transwell (Boyden chamber) assay*: cells seeded in an insert with a porous membrane; those that cross towards a chemoattractant in the lower well are stained and counted. *Migration*: crossing an uncoated membrane. *Invasion*: crossing a membrane coated with an extracellular matrix (ECM) such as Matrigel. *Invasion index*: invaded ÷ migrated cells for one condition. *Field*: one microscope image of the membrane's underside. *Insert*: one membrane; treatments are applied per insert, but inserts within an experiment are subsamples, not n. *Independent experiment*: cells prepared, seeded and stained on a separate occasion. *Percent of control* (% of control): each value divided by the control mean of the same experiment, × 100. *Log ratio*: the natural logarithm of treated ÷ control, converted back to a ratio for reporting; a ratio of 0.8 and one of 1.25 are equally far from 1 on this scale. *One-sample t-test*: tests whether the mean of the per-experiment values differs from a fixed number (0 for log ratios, 100 for percentages); for log ratios it gives the same answer as a paired t-test on the log means. *Student's and Welch t-tests*: two-group t-tests that assume equal or allow unequal spread. *Standard error*: the uncertainty of a mean, its standard deviation ÷ √n. *Multiple-comparison correction*: widens intervals (adjusted CIs) when several treatments are compared with one control. *Mixed model*: a regression with a random effect for each experiment. *Degrees of freedom (df)*: roughly, how many independent values the test uses to estimate the spread. *95% confidence interval (CI)*: the range of values compatible with the data. *Power*: the chance of detecting an effect that is really there. *False positive*: a "significant" result when there is no real effect.

## Which analysis for your question

| Question | Analysis | Report |
| --- | --- | --- |
| Does a treatment change migration? | Mean cells per field per insert, mean of inserts per condition per experiment; log ratio treated ÷ control per experiment, one-sample t-test against 0 | Ratio with 95% CI, n experiments |
| Several treatments vs one control | Log ratio of each treatment vs control per experiment; one-sample t-tests with a multiple-comparison correction, or a mixed model with experiment as a random effect ([nested data](/guides/nested-data-mixed-model/)) | Ratios with adjusted CIs |
| Does a treatment change invasion beyond its effect on migration? | Coated and uncoated inserts in every experiment; average invaded and migrated counts separately per condition and experiment, log(invaded ÷ migrated) per condition, then treated − control per experiment, one-sample t-test; do not pair individual inserts | Ratio of invasion indices with 95% CI |
| Absorbance after dye elution (stain washed off the membrane and read in a plate reader) | Absorbance minus the reading of a cell-free blank, per insert; mean per condition per experiment, log ratio, one-sample t-test; every value must stay above 0 | Ratio with 95% CI |
| Assay longer than about 24 h (hours) | Log ratio per experiment as above, plus a proliferation inhibitor or a parallel proliferation measurement | Ratio, with the proliferation control |
| One insert per condition per experiment | The per-experiment log ratio still works; the inserts' own variation is then inside the experiment-to-experiment spread | Ratio with 95% CI; state the design |

The log ratio needs every per-experiment mean to be above 0 and assumes the treatment changes migration by a factor rather than by a fixed number of cells. If a condition has no migrated cells in an experiment, count more of the membrane or use a count model ([count data](/guides/statistical-tests-for-count-data/)).

Lazic notes that "cells in the same flask or well of a cell-culture experiment are not independent", and that when "the whole procedure is often repeated three or more times and reported as three 'independent replicate experiments'", then "n is the number of experiments" ([Lazic 2010](https://doi.org/10.1186/1471-2202-11-5)). Pijuan and colleagues counted cells with ImageJ ("Select Process → Find Maxima to measure automatically the total number of cells in each image") and, in their example figure, counted cells "from 10 random microscope fields for each sample in 3 independent experiments" ([Pijuan et al. 2019](https://doi.org/10.3389/fcell.2019.00107)). For invasion, Corning's protocol advises "to set up a few wells without an ECM barrier to determine the percent of cells that exhibit chemotaxis when protein degradation is not required" ([Sherman and Rothenberg, Corning CLS-AN-211](https://www.scientificlabs.ie/handlers/libraryFiles.ashx?filename=White_Paper_3_3428_F.pdf)).

## Worked example: a knockdown with no real effect

Constructed data: migrated cells counted in 5 fields per insert, 2 inserts per condition (control and knockdown), in **4 independent experiments** (80 fields). By design, **the knockdown does not change migration on average**. Counts differ a lot between experiments (as seeding, cell state and staining vary; standard deviation 0.35 on the log scale), the knockdown's apparent effect varies a little between experiments (0.12), inserts differ (0.15), and cells are spread unevenly over the membrane. The data's random seed was the first one tried and was not chosen for its result. Stored code and data: [transwell.csv](/guides/transwell-migration-invasion/transwell.csv), [model.py](/guides/transwell-migration-invasion/model.py), [make_data.py](/guides/transwell-migration-invasion/make_data.py), [analysis.py](/guides/transwell-migration-invasion/analysis.py), [analysis.R](/guides/transwell-migration-invasion/analysis.R) and [the analysis output](/guides/transwell-migration-invasion/analysis_output.txt); analysis.py and analysis.R print the same output.

| Experiment | Control, cells per field | Knockdown, cells per field | Knockdown, % of control |
| --- | --- | --- | --- |
| 1 | 66.3 | 57.8 | 87% |
| 2 | 63.7 | 53.2 | 84% |
| 3 | 52.2 | 33.6 | 64% |
| 4 | 40.7 | 55.9 | 137% |

| Analysis, knockdown vs control | Result |
| --- | --- |
| A. Fields as n (40 vs 40), Welch t-test | ratio 0.90, p = 0.26 |
| B. Inserts as n (8 vs 8), raw counts, Welch t-test | ratio 0.90, p = 0.45 |
| C. Percent of control vs a control column fixed at 100% (4 vs 4), Student's t-test | 93%, p = 0.67 |
| D. Percent of control, one-sample t-test against 100 (n = 4) | 93%, p = 0.69 |
| **E. Log ratio per experiment, one-sample t-test (n = 4)** | **0.90 (95% CI 0.54 to 1.48), p = 0.53** |

All five analyses give p > 0.05 in this example, so the difference between them shows only across many studies. The interval from E shows how much 4 experiments can say: the data are compatible with anything from a 46% drop to a 48% rise. Control counts ranged from 40.7 to 66.3 cells per field across experiments, which is why analyses that do not pair by experiment (A and B) are noisy. Across 5,000 simulated studies of this design ([simulation.py](/guides/transwell-migration-invasion/simulation.py), [the simulation output](/guides/transwell-migration-invasion/simulation_output.txt)):

| Scenario | A. Fields as n | B. Inserts as n | C. % of control vs fixed 100% | D. % of control, one-sample | E. Log ratio, one-sample |
| --- | --- | --- | --- | --- | --- |
| No effect, 4 experiments (false positives) | **11.9%** | 1.5% | **8.9%** | 4.6% | 4.8% |
| 30% less migration, 4 experiments (power) | 78.4% | 32.9% | 62.2% | 47.8% | 39.5% |
| 30% less migration, 8 experiments (power) | 95.6% | 64.5% | 85.7% | 82.3% | **85.2%** |

These percentages describe this simulated design (5 fields, 2 inserts, the variation above), not every transwell study. Fields as n treats 40 counts per condition as independent and finds effects that are not there. Fixing the control at 100% and running a Student's t-test pools the knockdown's spread with a column that has no spread: the standard error comes out the same as in a one-sample test, but the degrees of freedom double (6 instead of 3), so the p-values are too small. Normalising to percent of control is fine; the fix is to test the percentages against 100 with a one-sample t-test (D). D and E both kept false positives near 5%; the log ratio treats a halving and a doubling alike, while percentages detected a drop slightly more often with 4 experiments. Inserts as n ignores the pairing within experiments: here it was too cautious (1.5%) and weak, but when the treatment effect varies more between experiments it can give false positives. With 4 experiments, a 30% drop was detected in fewer than half of studies; with 8, about 85%. With 3 experiments the test still runs, but the interval is even wider. The higher power of A and C is the other face of their false positives.

The code expects one row per field with columns experiment, condition, insert, field and cells, with the condition labels exactly `control` and `knockdown` (lowercase); rename your columns and labels to match, and run the code in the folder that holds the CSV file. The code compares one treatment with one control; for several treatments, run it once per treatment and adjust the p-values (for example with Holm's method); for invasion, compute the experiment-level means of invaded and of migrated cells for each condition as the code does, then take log(invaded ÷ migrated) per condition and the treated − control difference per experiment. Python needs NumPy, pandas and SciPy; the R code uses base R only. The first rows of the example file:

```text
experiment,condition,insert,field,cells
1,control,1,1,44
1,control,1,2,93
```

```python
import numpy as np, pandas as pd
from scipy import stats

d = pd.read_csv("transwell.csv")
ins = d.groupby(["experiment", "condition", "insert"], as_index=False).cells.mean()   # mean per insert
ex = ins.groupby(["experiment", "condition"]).cells.mean().unstack()                  # mean per condition per experiment
r = np.log(ex["knockdown"] / ex["control"]); t = stats.ttest_1samp(r, 0); ci = t.confidence_interval()
print(f"ratio {np.exp(r.mean()):.2f} (95% CI {np.exp(ci.low):.2f} to {np.exp(ci.high):.2f}), p = {t.pvalue:.2g}")
# ratio 0.90 (95% CI 0.54 to 1.48), p = 0.53
```

```r
d <- read.csv("transwell.csv")
ins <- aggregate(cells ~ experiment + condition + insert, data = d, FUN = mean)      # mean per insert
ex <- tapply(ins$cells, list(ins$experiment, ins$condition), mean)                    # mean per condition per experiment
t <- t.test(log(ex[, "knockdown"] / ex[, "control"]))                                 # one-sample t-test of log ratios
round(c(ratio = exp(unname(t$estimate)), lower = exp(t$conf.int[1]), upper = exp(t$conf.int[2]), p = t$p.value), 2)
# ratio 0.90, lower 0.54, upper 1.48, p 0.53
```

## Before you analyse

1. **Plan the number of independent experiments for the effect you care about.** In our simulation, 4 experiments found a 30% drop in fewer than half of studies and 8 found it in about 85%. Run each condition in at least two inserts per experiment.
2. **Count the same fields on every membrane, blind to condition**: for example 5 to 10 fields in fixed positions (centre and four quadrants), or the whole membrane. More fields make each insert's mean more precise; only more experiments add n. An automated count (for example ImageJ's Find Maxima) applied the same way to every image avoids choosing fields.
3. **Average fields within inserts and inserts within experiments before testing**, and normalise within each experiment ([fold change](/guides/statistics-on-fold-change/)).
4. **For invasion, include uncoated inserts in every experiment** so that a change in invasion can be separated from a change in migration.
5. **For assays longer than about 24 hours, control for proliferation**, with an inhibitor or a parallel cell count, so that more cells are not read as more migration.

## Check what an AI assistant ran

1. **The assistant ran a t-test on all fields.** Ask for one value per condition per experiment.
2. **The assistant compared a "% of control" column with a column of 100s.** Ask for a one-sample test of the percentages against 100, or of the log ratios against 0.
3. **The assistant pooled inserts from all experiments without pairing.** Ask for the per-experiment ratio.
4. **The assistant reported invasion without the matched migration inserts.** Ask how a change in migration was ruled out.

## Report it

> Migrated cells were counted in [5] fields per insert (ImageJ, [Find Maxima]), blind to condition, in [2] inserts per condition in each of [4] independent experiments. Counts were averaged within inserts and then across inserts within experiments; the knockdown ÷ control ratio was log-transformed and compared with 0 by a one-sample t-test across experiments (R [4.3.3]).

> The knockdown did not clearly change migration (knockdown ÷ control [0.90], 95% CI [0.54] to [1.48]; p = [0.53], n = [4] experiments); with [4] experiments the result is inconclusive for changes smaller than about [50]%.

Replace the bracketed values with your own.

## References

- Lazic SE (2010). The problem of pseudoreplication in neuroscientific studies: is it affecting your analysis? *BMC Neuroscience* 11:5. [doi:10.1186/1471-2202-11-5](https://doi.org/10.1186/1471-2202-11-5)
- Pijuan J, Barceló C, Moreno DF, et al. (2019). In vitro cell migration, invasion, and adhesion assays: from cell imaging to data analysis. *Frontiers in Cell and Developmental Biology* 7:107. [doi:10.3389/fcell.2019.00107](https://doi.org/10.3389/fcell.2019.00107)
- Sherman H, Rothenberg M. Cell migration, chemotaxis and invasion assay using staining. Corning Life Sciences protocol CLS-AN-211 (undated). [Corning CLS-AN-211 (PDF)](https://www.scientificlabs.ie/handlers/libraryFiles.ashx?filename=White_Paper_3_3428_F.pdf)

## Related

- [Scratch wound healing assay](https://www.licklider.ai/guides/scratch-wound-healing-assay/)
- [Statistics on fold change and normalised data](https://www.licklider.ai/guides/statistics-on-fold-change/)
- [What counts as n?](https://www.licklider.ai/guides/what-counts-as-n/)
- [Statistical tests for count data](https://www.licklider.ai/guides/statistical-tests-for-count-data/)

## How this page was made

- Written and published by Licklider. Recommendations cite peer-reviewed methods papers and a manufacturer protocol listed under References; cited passages were checked against the source text.
- The worked example uses constructed data. Its numbers were produced by stored code (Python 3.13 with SciPy 1.18.1; R 4.3.3, base R), and the two languages print identical output; the simulation was run in Python.
- Separate review passes checked the statistics, the citations and readability before publication.
