# ANOVA assumptions not met: what to do for each one

> Which ANOVA assumption failed decides the fix: fix the unit, use Welch's ANOVA, analyse logs, fit a count or proportion model, or use a robust or rank test. Checks and code in R and Python.

Canonical: https://www.licklider.ai/guides/anova-assumptions-not-met/
Section: Guides › Comparing three or more groups
Author: Licklider · Published 2026-11-10 · Last reviewed 2026-11-10 (UTC)

> **Quick answer.** Find out **which** assumption failed, then use its fix — not one blanket switch to a "non-parametric" test. If one animal contributes several values, fix the unit of analysis first; no test repairs that. If the outcome is a count or a proportion, fit a **count or proportion model** instead of an ANOVA (and do not log-transform counts). If the values are positive, skewed and their spread grows with the mean (concentrations, intensities), **analyse the logarithms** with Welch's ANOVA and report ratios. If the group spreads differ for another reason, use **Welch's ANOVA** with **Games–Howell** pairs. If a few extreme values dominate, use a **trimmed-means** test or a rank test, knowing that the rank test asks a different question. If only the residuals look a little non-normal and the spreads are similar, the F-test's false-positive rate holds up well: in a simulation of three groups with equal variances, the classical F-test's rate stayed between 2.5% and 7.5% in all 1,308 conditions tested (Blanca et al. 2017).

**Terms used below.** *Assumption*: a condition the test's p-value relies on. *Residual*: a value minus its group mean — the part the group does not explain; ANOVA's normality and equal-spread assumptions are about residuals, not raw data. *Spread*: the standard deviation (SD) within a group. *Skewed*: a long tail on one side; for concentrations, towards high values. *Log scale*: analysing the logarithm of each value, which turns ratios into differences. *Geometric mean*: the back-transformed mean of the logs. *False-positive rate*: the share of experiments with p < 0.05 when there is no real difference. *Power*: the share that detects a real difference. *Games–Howell*: a pairwise follow-up test that uses each pair's own spreads. *Count model / proportion model*: a regression built for counts (Poisson, negative binomial) or for "k out of n" data (binomial). *Quasi-binomial*: a binomial model that also allows extra animal-to-animal variation. *Rate ratio / odds ratio*: how many times higher the count rate, or the odds of a positive cell, is in one group. *QQ plot*: residuals plotted against the values a normal distribution would give; a straight line means roughly normal. *Degrees of freedom (df)*: the number that sets which t or F distribution the p-value comes from; Welch's tests give fractional df. *Simultaneous 95% intervals*: intervals adjusted so that all of them together cover the true differences 95% of the time. *Trimmed mean*: the mean after dropping the lowest and highest 20% of values. *Kruskal–Wallis*: a rank-based test of whether values in some groups tend to be higher. *Welch's ANOVA*: the one-way ANOVA that does not assume equal spreads (our default). *CI*: confidence interval. *Heavy tails*: more extreme values than a normal distribution would give. *Non-parametric (rank) test*: a test on the ranks of the values instead of their means. *Mixed model*: a model that allows several values per animal. *Negative binomial*: a count model that allows more spread than a Poisson model; use it when counts vary more between animals than their mean. *Type I error rate*: the false-positive rate. *Pools the spreads*: assumes one common spread for all groups.

## Find which assumption failed, then use its fix

Work down the table and stop at the first row that applies. Decide the scale and the test from your study design and the type of measurement, before you look at the p-values.

| What failed | How you notice | What to do | R | Python |
| --- | --- | --- | --- | --- |
| **Independence**: several values per animal, well or culture counted as separate observations | n is larger than the number of animals you randomised | Average to one value per independent unit, then use the row below that fits; or fit a mixed model ([what counts as n](/guides/what-counts-as-n/), [nested data](/guides/nested-data-mixed-model/)) | `d <- aggregate(y ~ mouse + group, data = d, FUN = mean)` | `df = df.groupby(["mouse", "group"], as_index=False)["y"].mean()` |
| **The outcome is a count or a proportion** (foci per mouse; positive cells out of cells counted) | Whole numbers, many small values or zeros; or k out of n, bounded by 0 and 1 | Quasi-Poisson regression with t-based intervals for counts when groups have fewer than about 20 animals (negative binomial with a likelihood-ratio test from about 20 per group), quasi-binomial regression for proportions, one row per animal ([code](#counts-and-proportions-need-their-own-model)); do not log-transform counts | `glm(y ~ group, family = quasipoisson, data = d)`; `glm(cbind(k, n - k) ~ group, family = quasibinomial, data = d)` | `smf.glm("y ~ group", df, family=sm.families.Poisson()).fit(scale="X2", use_t=True)`; proportions: see the [full code](#counts-and-proportions-need-their-own-model) |
| **Skew in positive values** whose spread grows with the mean (all values above zero) | Long right tail; SD rises in step with the group mean | Analyse **log values** (every value must be above zero) with Welch's ANOVA; report ratios of geometric means with CIs (example below). This row comes before the equal-spreads row because the log scale usually removes the unequal spreads too | `oneway.test(log(y) ~ group)` | `stats.f_oneway(*[np.log(g) for g in groups], equal_var=False)` |
| **Equal spreads**, when the spreads differ for another reason (not skew in positive values) | One group's SD is clearly larger; the residuals-vs-fitted plot fans out | Welch's ANOVA, then Games–Howell for pairs ([Welch's ANOVA](/guides/welch-anova/)) | `oneway.test(y ~ group)` | `stats.f_oneway(*groups, equal_var=False)` |
| **Heavy tails or a few extreme values** that are real measurements | One or two points far from the rest in the QQ plot | Exclude a value only for a recorded reason that does not depend on the value itself, such as a failed injection ([excluding outliers](/guides/when-can-you-exclude-an-outlier/)); otherwise compare 20% trimmed means (`WRS2::t1way`), or use Kruskal–Wallis, which tests whether values in some groups are systematically higher rather than whether means differ ([Kruskal–Wallis vs ANOVA](/guides/kruskal-wallis-vs-anova/)) | Trimmed means: `WRS2::t1way(y ~ group, data = d)`, then `WRS2::lincon(y ~ group, data = d)` for pairs; Kruskal–Wallis: `kruskal.test(y ~ group, data = d)`, then `rstatix::dunn_test(d, y ~ group, p.adjust.method = "holm")` | Trimmed means (pairs only; SciPy has no trimmed-means omnibus test): `stats.ttest_ind(a, b, trim=0.2)` for each pair, then `multipletests(pvals, method="holm")`; Kruskal–Wallis: `stats.kruskal(*groups)`, then `sp.posthoc_dunn(df, val_col="y", group_col="group", p_adjust="holm")` |
| **Residuals not quite normal**, spreads similar, no row above applies | QQ plot bends a little at the ends | Keep Welch's ANOVA, unless the values are positive and strongly skewed with fewer than about 50 per group (then use the log-scale row): Delacre et al. (2019) found Welch's test "will not control Type I error rate when skewness is present" there. For mild departures the evidence is reassuring: with equal variances the classical F's false-positive rate stayed within 2.5–7.5% across the shapes Blanca et al. (2017) simulated, and in our simulation of skewed log-normal data Welch's ANOVA on raw values stayed at 4.1–5.0% | `oneway.test(y ~ group)` | `stats.f_oneway(*groups, equal_var=False)` |

The Python cells need SciPy 1.16 or later (`equal_var` in `f_oneway`; Games–Howell via `tukey_hsd(..., equal_var=False)`); check with `import scipy; print(scipy.__version__)`. The cells are fragments: in R your data are a data frame called `d`, in Python a pandas DataFrame called `df`; `y`, `group`, `mouse`, `k` and `n` stand for your column names; `groups` is the list of per-group arrays and `a`, `b` two of them. Python cells assume `import numpy as np`, `from scipy import stats`, `import statsmodels.formula.api as smf`, `from statsmodels.stats.multitest import multipletests` and `import scikit_posthocs as sp`. Packages: `install.packages(c("MASS", "WRS2", "rstatix"))` in R; `pip install numpy pandas scipy statsmodels scikit-posthocs matplotlib` in Python. Holm's adjustment keeps the chance of any false positive across the pairs at 5%; Dunn's test compares pairs on the ranks Kruskal–Wallis used. Run on the example data below, the robust cells give: Dunn's test with Holm (R and Python agree) high vs vehicle p = 0.0008, low vs vehicle 0.032, high vs low 0.22; trimmed-means pairs high vs vehicle p = 0.017 (`lincon`) or 0.0034 (Yuen tests with Holm) — two different pairwise procedures, so their p-values differ.

Kruskal–Wallis is not a general fix for unequal spreads: its null hypothesis matches the F-test's only when the groups' distributions have the same shape (Blanca et al. 2017, Discussion), so use it when "are values in some groups systematically higher?" is the question you want answered.

**What to do:** name the row that fits your data in your analysis plan, and use that route for the whole analysis.

## Check with residual plots, not a pass/fail test

Normality and equal spread are assumptions about the residuals, and plots show how large a problem is; a test only shows whether it is detectable. Kozak and Piepho (2018) showed with two examples that "residuals should be used for model checking and that residual plots are better for checking ANOVA assumptions than statistical tests." A Shapiro–Wilk test can miss a real problem in small groups and can flag a trivial one in large groups; [should you test for normality first?](/guides/should-you-test-for-normality/) explains this and why using such a test as a gate also changes the error rates.

Fit the one-way model and draw two plots: residuals against fitted values (look for a fan shape: spread growing with the mean) and a normal QQ plot of the residuals (look for curvature or isolated points). Download the [example data](/guides/anova-assumptions-not-met/il6.csv) (one row per mouse: `mouse`, `group`, `il6`) to run these as printed: save it in your working folder (check with `getwd()` in R or `os.getcwd()` in Python) or replace `"il6.csv"` with the file's full path.

```r
d <- read.csv("il6.csv")              # one row per mouse: mouse, group, il6
d$group <- factor(d$group, levels = c("vehicle", "low", "high"))
fit <- lm(il6 ~ group, data = d)
png("residuals_raw_R.png", width = 1200, height = 520, res = 150)
par(mfrow = c(1, 2))
plot(fit, which = 1:2)                # residuals vs fitted; normal QQ plot
invisible(dev.off())
round(tapply(d$il6, d$group, sd), 1)  # SD per group
```

```python
import pandas as pd
import statsmodels.formula.api as smf
from scipy import stats
import matplotlib
matplotlib.use("Agg")                  # save to a file; remove this line in a notebook
import matplotlib.pyplot as plt

df = pd.read_csv("il6.csv")            # one row per mouse: mouse, group, il6
fit = smf.ols("il6 ~ C(group)", data=df).fit()
fig, ax = plt.subplots(1, 2, figsize=(8, 3.5))
ax[0].scatter(fit.fittedvalues, fit.resid)
ax[0].axhline(0, color="grey")
ax[0].set_xlabel("Fitted value (group mean)")
ax[0].set_ylabel("Residual")
stats.probplot(fit.resid, dist="norm", plot=ax[1])   # normal QQ plot of the residuals
fig.tight_layout()
fig.savefig("residuals_raw.png", dpi=150)
print(df.groupby("group", sort=False)["il6"].std().round(1))   # SD per group
```

With small groups the plots are noisy, so let the type of measurement carry most of the decision: for positive measurements such as concentrations and intensities whose spread grows with the mean in pilot or published data, plan the log scale in advance; counts and proportions get their own models whatever the plots show.

**What to do:** keep both plots with your analysis, and state in Methods how you checked the assumptions (ARRIVE 2.0 item 7b asks for this).

## Worked example: skewed concentrations in three groups

A constructed experiment: serum IL-6 (pg/mL) in 8 mice per group, vehicle and two doses, one value per mouse. The data were generated from log-normal distributions with geometric means 40, 60 and 100 pg/mL and the same spread on the log scale; the seed was fixed in advance, not chosen for the result.

| Group | n | Mean (pg/mL) | SD | Geometric mean |
| --- | --- | --- | --- | --- |
| Vehicle | 8 | 41.0 | 16.9 | 36.3 |
| Low dose | 8 | 84.3 | 43.1 | 75.2 |
| High dose | 8 | 120.0 | 54.4 | 111.3 |

On the raw scale the SD grows with the mean (largest/smallest SD = 3.2) and the residuals are right-skewed (skewness 1.19). On the log scale the SD ratio falls to 1.5 and the residuals have no long right tail (skewness −0.77, from one low vehicle value). That pattern — spread proportional to the mean — is the case for the log scale; Keene (1995) recommends that log-transformed analyses "should frequently be preferred to untransformed analyses" for positive continuous data, decided at the design stage.

| Analysis | Result |
| --- | --- |
| Classical ANOVA, raw values | F(2, 21) = 7.36, p = 0.0038 |
| Welch's ANOVA, raw values | F(2, 11.25) = 9.62, p = 0.0037 |
| **Welch's ANOVA, log values** | **F(2, 13.59) = 9.04, p = 0.0032** |
| Kruskal–Wallis, raw values | χ²(2) = 13.72, p = 0.0011 |

All four agree that the groups differ. The scale changes what you can report about the doses:

| Comparison | Games–Howell on raw values: difference (95% CI), p | Games–Howell on log values: ratio of geometric means (95% CI), p |
| --- | --- | --- |
| Low vs vehicle | 43.4 pg/mL (−2.3 to 89.0), p = 0.062 | 2.07 times (0.99 to 4.33), p = 0.053 |
| High vs vehicle | 79.0 pg/mL (22.0 to 136.1), p = 0.010 | **3.06 times (1.54 to 6.08), p = 0.0025** |
| High vs low | 35.6 pg/mL (−29.0 to 100.3), p = 0.34 | 1.48 times (0.81 to 2.70), p = 0.24 |

Both sets of intervals are simultaneous 95% intervals (adjusted for the three comparisons). On the log scale, the high dose raised the geometric mean three-fold (ratio 3.06) — a multiplicative effect, which is how these data were generated — and each comparison uses spreads that are now similar across groups.

```r
d <- read.csv("il6.csv")
d$group <- factor(d$group, levels = c("vehicle", "low", "high"))
oneway.test(log(il6) ~ group, data = d)   # Welch's ANOVA on log values

games_howell <- function(y, g) {          # Games-Howell, as on the Welch's ANOVA page
  lv <- levels(g); k <- length(lv); out <- NULL
  for (i in 1:(k - 1)) for (j in (i + 1):k) {
    a <- y[g == lv[i]]; b <- y[g == lv[j]]
    va <- var(a) / length(a); vb <- var(b) / length(b)
    dd <- mean(b) - mean(a); se <- sqrt(va + vb)
    df <- (va + vb)^2 / (va^2 / (length(a) - 1) + vb^2 / (length(b) - 1))
    crit <- qtukey(0.95, k, df) / sqrt(2)
    out <- rbind(out, data.frame(pair = paste(lv[j], "/", lv[i]), log_diff = dd,
      lwr = dd - crit * se, upr = dd + crit * se,
      p = ptukey(abs(dd) / se * sqrt(2), k, df, lower.tail = FALSE)))
  }
  out
}
gh <- games_howell(log(d$il6), d$group)
data.frame(pair = gh$pair, ratio = round(exp(gh$log_diff), 2),
           lwr = round(exp(gh$lwr), 2), upr = round(exp(gh$upr), 2), p = round(gh$p, 4))
```

```python
import numpy as np
import pandas as pd
from scipy import stats   # f_oneway(equal_var=...) needs SciPy 1.16 or later

df = pd.read_csv("il6.csv")
order = ["vehicle", "low", "high"]
logs = [np.log(df.loc[df.group == g, "il6"]) for g in order]

print(stats.f_oneway(*logs, equal_var=False))          # Welch's ANOVA on log values
gh = stats.tukey_hsd(*logs, equal_var=False)           # Games-Howell on log values
ci = gh.confidence_interval(0.95)
for i, j in [(1, 0), (2, 0), (2, 1)]:
    print(f"{order[i]} / {order[j]}: ratio {np.exp(gh.statistic[i, j]):.2f} "
          f"(95% CI {np.exp(ci.low[i, j]):.2f} to {np.exp(ci.high[i, j]):.2f}), p = {gh.pvalue[i, j]:.4f}")
```

Both give F = 9.04, p = 0.0032 (R also prints the denominator df, 13.59) and the ratios in the table above. The log scale needs every value to be above zero; a value of exactly zero (below detection) is a separate problem, not something to fix by adding 1: report how many values were below the detection limit in each group and analyse them as below-limit (censored) values, with a statistician's help if needed.

**What to do:** plan the log scale for positive, right-skewed measurements, report geometric means and ratios with their intervals, and say so in Methods.

## What the choice of scale does to false positives and power

We simulated 20,000 experiments per scenario with values like the example's (log-normal, the same log-scale spread in every group) and compared four analyses. "No difference" means all three groups came from the same distribution; "third group 1.8 times higher" means its geometric mean was 1.8 times the others'. The table shows the share of experiments with p < 0.05.

| Mice per group | Truth | Classical ANOVA, raw | Welch, raw | Welch, log | Kruskal–Wallis |
| --- | --- | --- | --- | --- | --- |
| 8, 8, 8 | No difference | 4.0% | 4.1% | 4.7% | 4.5% |
| 8, 8, 8 | Third group 1.8 times higher | 41.2% | 29.7% | 41.6% | 41.2% |
| 5, 8, 11 | No difference | 4.4% | 4.9% | 5.1% | 4.4% |
| 5, 8, 11 | Third group (n = 11) 1.8 times higher | 37.8% | 40.3% | 43.8% | 46.4% |
| 11, 8, 5 | No difference | 4.5% | 5.0% | 5.2% | 4.5% |
| 11, 8, 5 | Third group (n = 5) 1.8 times higher | 39.4% | 16.5% | 32.1% | 30.3% |

Skew alone did not break false-positive control: every analysis stayed between 4.0% and 5.2% when nothing differed, consistent with Blanca et al. (2017). The difference is in power. Welch's ANOVA on raw values lost the most when the higher group was also the smallest (16.5% against 32.1% on logs), because on the raw scale that group's large spread is estimated from 5 mice. In that same scenario, part of the classical raw-scale ANOVA's 39.4% comes from being too liberal: the smallest group is also the most variable, the pairing in which Delacre et al. (2019) found the classical F rejects too often. Welch on logs came within 8 percentage points of the most powerful option in every scenario and, unlike the raw-scale classical test, does not rely on equal spreads; Kruskal–Wallis had similar power but answers a different question. Code and full output: [simulation.py](/guides/anova-assumptions-not-met/simulation.py) and [its output](/guides/anova-assumptions-not-met/simulation_output.json) (seed 20261110, fixed in advance).

**What to do:** choose the log scale in advance for concentration-like data; it keeps the false-positive rate and avoids the power loss of Welch's test on raw values.

## Counts and proportions need their own model

A count per animal (foci, colonies, events) and a proportion per animal (positive cells out of cells counted) have spreads tied to their means by construction. Fit a model for that kind of data and report a rate ratio or an odds ratio with its interval. In simulations of ecological counts, O'Hara and Kotze (2010) found that log and square-root transformations "performed poorly, except when the dispersion was small and the mean counts were large", while quasi-Poisson and negative binomial models "consistently performed well"; for binomial proportions, Warton and Hui (2011) found logistic regression had "greater interpretability and higher power" than analysing transformed proportions. Keep one row per animal: the model's unit is still the animal, and the quasi-Poisson and quasi-binomial versions below allow for animal-to-animal variation beyond the Poisson or binomial.

With groups of fewer than about 20 animals, use quasi-Poisson with t-based intervals for counts rather than the negative binomial model's default (Wald) p-values, because those run too small with few animals. In our seeded simulation of this page's design (6 vs 6 mice, negative binomial counts with no true difference, 5,000 runs per setting), the negative binomial Wald test gave 8.7–10.5% false positives and its likelihood-ratio test 7.4–8.4%, while quasi-Poisson with t-based inference stayed at 4.5–4.8%; at 20 per group the negative binomial tests were at 5.5–6.8% ([simulation code and output](/guides/anova-assumptions-not-met/count_simulation.R)).

```r
# Counts: foci per mouse (one value per mouse). Quasi-Poisson with t-based inference
# (df = animals - groups): with small groups its false-positive rate stays near 5%.
counts <- data.frame(group = factor(rep(c("vehicle", "drug"), each = 6), levels = c("vehicle", "drug")),
                     foci = c(3, 7, 2, 5, 9, 4, 12, 8, 15, 6, 20, 11))
qp <- glm(foci ~ group, family = quasipoisson, data = counts)
s <- summary(qp)$coefficients["groupdrug", ]
round(c(rate_ratio = exp(s[["Estimate"]]),
        exp(s[["Estimate"]] + c(lwr = -1, upr = 1) * qt(0.975, qp$df.residual) * s[["Std. Error"]]),
        p = s[["Pr(>|t|)"]], dispersion = summary(qp)$dispersion), 4)

# Proportions: positive cells out of cells counted, one row per mouse
prop <- data.frame(group = factor(rep(c("vehicle", "drug"), each = 5), levels = c("vehicle", "drug")),
                   positive = c(8, 22, 12, 19, 6, 20, 38, 18, 33, 40),
                   counted = c(100, 110, 95, 105, 90, 100, 115, 98, 102, 120))
bi <- glm(cbind(positive, counted - positive) ~ group, family = quasibinomial, data = prop)
s <- summary(bi)$coefficients["groupdrug", ]
round(c(odds_ratio = exp(s[["Estimate"]]),
        exp(s[["Estimate"]] + c(lwr = -1, upr = 1) * qt(0.975, bi$df.residual) * s[["Std. Error"]]),
        p = s[["Pr(>|t|)"]]), 4)
```

```python
import numpy as np
import pandas as pd
import statsmodels.api as sm
import statsmodels.formula.api as smf

# Counts: foci per mouse (one value per mouse). Quasi-Poisson with t-based inference
# (df = animals - groups): with small groups its false-positive rate stays near 5%.
counts = pd.DataFrame({"group": ["vehicle"] * 6 + ["drug"] * 6,
                       "foci": [3, 7, 2, 5, 9, 4, 12, 8, 15, 6, 20, 11]})
qp = smf.glm("foci ~ C(group, Treatment('vehicle'))", data=counts,
             family=sm.families.Poisson()).fit(scale="X2", use_t=True)   # quasi-Poisson
print("Rate ratio drug/vehicle:", round(float(np.exp(qp.params.iloc[1])), 2),
      "95% CI", np.round(np.exp(qp.conf_int().iloc[1].to_numpy()), 2), "p =", f"{qp.pvalues.iloc[1]:.4f}",
      "dispersion =", round(float(qp.scale), 2))

# Proportions: positive cells out of cells counted, one row per mouse
prop = pd.DataFrame({"group": ["vehicle"] * 5 + ["drug"] * 5,
                     "positive": [8, 22, 12, 19, 6, 20, 38, 18, 33, 40],
                     "counted": [100, 110, 95, 105, 90, 100, 115, 98, 102, 120]})
X = sm.add_constant((prop.group == "drug").astype(float).rename("drug"))
bi = sm.GLM(prop.positive / prop.counted, X, family=sm.families.Binomial(),
            var_weights=prop.counted).fit(scale="X2", use_t=True)   # quasi-binomial: allows mouse-to-mouse spread
print("Odds ratio drug/vehicle:", round(float(np.exp(bi.params["drug"])), 2),
      "95% CI", np.round(np.exp(bi.conf_int().loc["drug"].to_numpy()), 2), "p =", f"{bi.pvalues['drug']:.4f}")
```

Both languages give a rate ratio of 2.40 (95% CI 1.27 to 4.54, t(10), p = 0.012; dispersion 1.73) for the counts and an odds ratio of 2.49 (95% CI 1.30 to 4.78, p = 0.012) for the proportions. The same models extend to three or more groups by adding levels to `group`.

**What to do:** report the rate ratio or odds ratio from the model for counts and k-out-of-n data, with one row per animal; with fewer than about 20 animals per group, use the quasi-Poisson model's t-based interval for counts.

## Designs this page does not cover

Two factors (genotype × treatment) follow [two-way ANOVA vs separate t-tests](/guides/two-way-anova-vs-multiple-t-tests/), where the same residual checks apply to the two-way model. Repeated measurements on the same animals and cells within animals follow [nested data](/guides/nested-data-mixed-model/). Normalised values (fold change, % of control) follow [statistics on fold change](/guides/statistics-on-fold-change/).

**What to do:** check whether your design is one of these before using the table above.

## Report it

> We checked the one-way model's assumptions with residuals-versus-fitted and normal QQ plots. Because IL-6 concentrations are positive and their SD increased with the mean (as planned for concentration data), we analysed log-transformed values with Welch's ANOVA, followed by Games–Howell comparisons. Results are reported as ratios of geometric means with simultaneous 95% confidence intervals (high dose vs vehicle 3.06, 95% CI 1.54 to 6.08, p = 0.0025). Analyses used R 4.3.3.

Replace the measurement, the reason and the numbers with yours; keep "as planned" only if your plan or protocol says so. If you changed the analysis after looking at the data, say that instead, and report the original analysis too.

**What to do:** copy the template into Methods and Results and fill in every value from your software output.

## Check what an AI assistant ran

If an assistant tells you "ANOVA is robust, just proceed" or switched you to Kruskal–Wallis, check four things in its code:

1. **Unit.** Does each row hold one animal (or other independent unit)? If wells or cells are rows, go back to the first table row.
2. **Which assumption, which fix.** Did it name the failed assumption and use that row's fix, or did it run a Shapiro–Wilk or Levene test and switch tests on the result?
3. **Scale.** For concentrations, did it analyse logs and back-transform to ratios — or report a raw difference whose interval assumes equal spreads?
4. **Matching follow-up.** Welch → Games–Howell (`tukey_hsd(..., equal_var=False)` in SciPy); log scale → Games–Howell on logs; Kruskal–Wallis → Dunn's test with Holm's adjustment (code in the table above). A Tukey test after Welch's ANOVA pools the spreads again.

**What to do:** ask the assistant to state the unit, the failed assumption and the chosen fix in one sentence each, and compare them with the table above.

## Corrections

- **10 November 2026:** the count example previously used a negative binomial model with Wald inference (rate ratio 2.40, 95% CI 1.45 to 3.96, p = 0.0006). With 6 animals per group that test's p-values run too small (8.7–10.5% false positives in our simulation), so the example now uses quasi-Poisson with t-based inference (95% CI 1.27 to 4.54, p = 0.012), and the table row says when each model fits.

## References

- Blanca MJ, Alarcón R, Arnau J, Bono R, Bendayan R (2017). Non-normal data: Is ANOVA still a valid option? *Psicothema* 29(4):552–557. [doi:10.7334/psicothema2016.383](https://doi.org/10.7334/psicothema2016.383)
- Delacre M, Leys C, Mora YL, Lakens D (2019). Taking parametric assumptions seriously: arguments for the use of Welch's F-test instead of the classical F-test in one-way ANOVA. *International Review of Social Psychology* 32(1):13. [doi:10.5334/irsp.198](https://doi.org/10.5334/irsp.198)
- Keene ON (1995). The log transformation is special. *Statistics in Medicine* 14(8):811–819. [doi:10.1002/sim.4780140810](https://doi.org/10.1002/sim.4780140810)
- Kozak M, Piepho H-P (2018). What's normal anyway? Residual plots are more telling than significance tests when checking ANOVA assumptions. *Journal of Agronomy and Crop Science* 204(1):86–98. [doi:10.1111/jac.12220](https://doi.org/10.1111/jac.12220)
- O'Hara RB, Kotze DJ (2010). Do not log-transform count data. *Methods in Ecology and Evolution* 1(2):118–122. [doi:10.1111/j.2041-210X.2010.00021.x](https://doi.org/10.1111/j.2041-210X.2010.00021.x)
- Percie du Sert N, et al. (2020). The ARRIVE guidelines 2.0, item 7 (statistical methods). [arriveguidelines.org](https://arriveguidelines.org/arrive-guidelines/statistical-methods)
- Warton DI, Hui FKC (2011). The arcsine is asinine: the analysis of proportions in ecology. *Ecology* 92(1):3–10. [doi:10.1890/10-0340.1](https://doi.org/10.1890/10-0340.1)
- R documentation: `oneway.test` (stats, R 4.3.3; `var.equal = FALSE` by default); WRS2 `t1way` (one-way ANOVA on trimmed means, "Homoscedasticity assumption not required"; [reference](https://cran.r-project.org/package=WRS2)).
- SciPy 1.18.1 documentation: `scipy.stats.f_oneway` (`equal_var`), `scipy.stats.tukey_hsd` (`equal_var=False` gives Games–Howell), `scipy.stats.ttest_ind` (`trim`, Yuen's trimmed t-test).

## Related

- [Welch's ANOVA vs one-way ANOVA](https://www.licklider.ai/guides/welch-anova/)
- [Kruskal–Wallis vs ANOVA](https://www.licklider.ai/guides/kruskal-wallis-vs-anova/)
- [Should you test for normality first?](https://www.licklider.ai/guides/should-you-test-for-normality/)
- [Statistical test chooser](https://www.licklider.ai/tools/statistical-test-chooser/)

## How this page was made

- Written and published by Licklider. Every recommendation is cited to peer-reviewed studies or official documentation listed under References; cited passages were checked against the source text, or against the abstract where only the abstract was accessible.
- The worked example uses constructed data, and our simulation was run with stored code (Python with SciPy 1.18.1 and statsmodels 0.15.0; R 4.3.3). Python and R agree on every number shown where both run the same analysis (the trimmed-means pairs use different procedures in the two languages), and every code block on this page was run as printed (table cells on the example data).
- Separate review passes checked the statistics, the citations and readability before publication.
