Comparing three or more groups · Choose a method
Test for trend across doses: when one trend test beats comparing each dose with control
If you expect the response to change steadily with dose, test the trend once with a linear contrast or a regression on dose, decided in advance. Comparing each dose with control has less power. Python and R code.
On this page
Quick answer. Decide before the experiment which question you are asking. “Does the response change steadily as the dose rises?” Test it once, with a linear trend contrast across the ordered groups (or a regression on dose), and report the change per dose step (per step in the dose order, not per mg/kg) with its 95% confidence interval. “Which doses differ from control?” Compare each dose with control using Dunnett’s test. The trend test uses all groups at once, so when the effect changes roughly in step with the dose order it has more power: in the simulation below (6 mice per group, a steady fall of 2.2 units per step, SD 4), 82% against 67% for “any dose differs from vehicle”. For ranks or scores use the Jonckheere–Terpstra test (for positive, right-skewed measurements, analyse logs first); for proportions, the Cochran–Armitage test.
Terms used below. Vehicle: the control group given the solvent without the compound (dose 0). Ordered groups: groups with a natural order, such as vehicle, low, medium and high dose. Monotone: moving in one direction only as dose rises, with no reversals. Linear trend contrast: a single comparison that weights the ordered group means (here −3, −1, 1, 3) to test for a steady rise or fall. Dunnett’s test: a test that compares each group with one control and adjusts for the several comparisons. Power: the chance of detecting an effect that is really there. α: the false-positive threshold, here 0.05. df (degrees of freedom): a number printed with the test that reflects the amount of independent data. 95% confidence interval (CI): the range of effects compatible with the data. Dose scores: the numbers given to the ordered groups for the trend test, for example 0, 1, 2, 3 (equal steps) or the log doses. Contrast weights: the dose scores centred so they sum to zero (0, 1, 2, 3 become −3, −1, 1, 3 after doubling to keep whole numbers). One-way ANOVA: the model comparing several group means, which supplies the pooled within-group variance (the average spread inside the groups) used by the contrast. t: the test statistic. Distribution shape: the spread and skew of the values within a group. Repeated-measures analysis: a model for data where the same animal is measured under several conditions, such as a mixed model with animal as a random effect. Step-down trend test: a sequence of trend tests that drops the highest dose at each step to find the lowest effective dose.
Which test for your question
| Your question | Test | Note |
|---|---|---|
| Does the response change steadily with dose? (continuous outcome) | Linear trend contrast in a one-way ANOVA, or a regression on dose or log dose | Choose the dose scores (equal steps, actual doses or log doses) in advance; with a zero-dose vehicle, equal steps are the simplest default, and log doses suit fold-spaced doses without a zero group |
| The same, for ranks or scores | Jonckheere–Terpstra test (in R, for example PMCMRplus::jonckheereTest) |
Rank-based test for ordered groups that assumes similar distribution shapes (Jonckheere 1954); for positive, right-skewed measurements, analyse logs with the contrast instead (Kruskal–Wallis vs ANOVA) |
| The same, for a yes/no outcome per animal (tumour present, died) | Cochran–Armitage test for trend (base R: prop.trend.test(events, totals)) |
Tests whether proportions rise or fall across ordered groups (Armitage 1955) |
| Which doses differ from control? | Dunnett’s test | Dunnett (1955); no assumption about the shape; less power for a steady trend |
| What is the lowest dose that differs from control, assuming a monotone response? | Williams’ test (in R, for example PMCMRplus::williamsTest), or a step-down trend test |
Williams (1971); assumes the response moves in one direction only |
| A full dose–response curve with an IC50 or EC50 | Curve fitting | See how to compare IC50 values |
OECD guidance on toxicity data states the reason plainly: “Procedures that employ step-down trend tests have more power than procedures that rely on multiple pairwise comparisons when there is a monotone dose-response” (OECD 2006, §5.1.3, para. 137). Armitage made the same point for proportions: with ordered groups, “the question usually asked is then not so much whether the proportions differ significantly, but whether they show a significant trend” (Armitage 1955).
Worked example: no single dose differs clearly, the trend is clear
Constructed data: vehicle and three doses (1, 3 and 10 mg/kg), 6 mice per group. The data were simulated with a true fall of 2.2 units per dose step (SD 4); by chance, the observed means rise slightly at the first step (1 mg/kg is just above vehicle), and the estimated change is 1.98 per step. This dataset was chosen from simulated ones so that no dose differs clearly from control on its own; the simulation below shows how often each analysis detects the effect. Stored code and data: doses.csv, analysis.py, analysis.R and the analysis output; both languages print the same output.
| Analysis | Result |
|---|---|
| Group means (vehicle, 1, 3, 10 mg/kg) | 99.12, 100.85, 94.87, 94.52 |
| Dunnett, each dose vs vehicle | p = 0.69, 0.09, 0.07 |
| Linear trend contrast (weights −3, −1, 1, 3) | t = −3.27, df = 20, p = 0.0038; change per dose step −1.98 (95% CI −3.24 to −0.72). The contrast value itself, −19.78, is in weight units (10 × the per-step change) and is not the effect to report |
As separate comparisons, the results show no dose clearly different from vehicle, although the higher doses are lower on average. As one question about dose, they show the response falling across the dose range, though not at every step (1 mg/kg is slightly above vehicle). “Per dose step” here means per step in the ordering (vehicle → 1 → 3 → 10 mg/kg), not per mg/kg; with these unequal doses, a regression on log dose would give a slope per tenfold dose instead, but the vehicle group (0 mg/kg) has no logarithm, so it would have to be left out of that regression or given a score decided in advance. The trend test was the right analysis here only because a steady change with dose was the question decided in advance; choosing it after seeing the Dunnett results would be a second look at the data and would inflate the false-positive rate. In 10,000 simulated experiments per scenario with this design (6 mice per group, SD 4, α = 0.05), each analysis on its own kept the false-positive rate at 5.1% when there was no effect; when the response fell by 2.2 per dose step, the trend contrast detected it in 82.2% of experiments and Dunnett’s “any dose differs from vehicle” in 67.4% (simulation.py, the simulation output).
# Condensed: the full script, which also prints the group means and Dunnett results, is analysis.py
import numpy as np, pandas as pd
from scipy import stats
d = pd.read_csv("doses.csv") # columns: dose_mg_kg, level (0-3), response
g = [d.response[d.level == k].to_numpy() for k in range(4)]
c = np.array([-3, -1, 1, 3]) # equal-step linear weights for 4 ordered groups
df = sum(len(x) for x in g) - len(g)
mse = sum(((x - x.mean()) ** 2).sum() for x in g) / df
est = (c * [x.mean() for x in g]).sum()
t = est / np.sqrt(mse * (c ** 2 / [len(x) for x in g]).sum())
scores = np.array([0, 1, 2, 3]); se = est / t
k = (c * scores).sum() # converts the contrast to change per step
tq = stats.t.ppf(0.975, df)
print(t, df, 2 * stats.t.sf(abs(t), df), est / k, (est - tq * se) / k, (est + tq * se) / k) # t, df, p, change per step, 95% CI
library(multcomp)
d <- read.csv("doses.csv")
d$level_f <- factor(d$level, levels = 0:3) # control first; Dunnett compares with the first level
set.seed(1) # Dunnett p-values use numerical integration with a random element
fit <- aov(response ~ level_f, data = d)
K <- rbind(trend = c(-3, -1, 1, 3)) # equal-step linear weights
g <- glht(fit, linfct = mcp(level_f = K))
summary(g) # linear trend contrast: t, p
confint(g, calpha = univariate_calpha())$confint / sum(K * 0:3) # change per dose step, 95% CI
summary(glht(fit, linfct = mcp(level_f = "Dunnett"))) # each dose vs vehicle
With unequal group sizes or doses that are not equally spaced, either use weights that match the dose scores you chose, or fit a regression of the response on dose (or log dose) and report the slope; both test a linear change with dose, but they are not the same test (the contrast uses the variation within groups, a regression the scatter around the line), so choose one in advance.
Before you test a trend
- Decide the question and the dose scores in advance. Equal steps (0, 1, 2, 3), actual doses or log doses give different tests; write the choice in your plan.
- Look at the group means. A trend test detects steady change; if the response rises and then falls, the linear contrast can miss it and a curve or the separate Dunnett comparisons describe it better.
- Check that the spread is similar across groups. The contrast above uses the pooled within-group variance. If the spread grows clearly with dose, analyse logs or use a method that allows unequal spread.
- Use independent animals, one value each. Different animals in each group; average technical replicates within each animal first (what counts as n). If the same animals received every dose, use a repeated-measures analysis instead.
- Report both if you planned both. If your plan asked both “Is there a trend?” and “Which doses differ?”, report both results and say which was primary; the 5% false-positive rate applies to each analysis separately, so treat the secondary one as supporting evidence. Step-down trend tests (see the table above) keep the overall false-positive rate at 5% when the question is the lowest effective dose.
Check what an AI assistant ran
- Separate t-tests of each dose against control, with no adjustment. Use Dunnett’s test, or the trend test if a trend was the question.
- A one-way ANOVA F-test reported as evidence of a dose response. The overall F-test ignores the order of the doses; use the same ANOVA model for its error variance, but report the trend contrast.
- A trend test chosen after the pairwise tests came out non-significant. Report the analysis you planned.
- Doses treated as unordered categories in a chi-square test for a yes/no outcome. Use the Cochran–Armitage test.
Report it
Methods:
The primary analysis, specified in advance, was a test for linear trend across the four ordered groups (vehicle, 1, 3 and 10 mg/kg), using contrast weights −3, −1, 1, 3 in a one-way ANOVA (n = [6] mice per group; R [4.3.3]). Each dose was also compared with vehicle by Dunnett’s test.
Results:
The response fell with dose (linear trend, t = [−3.27], df = [20], p = [0.0038]; [1.98] units lower per step in dose order, 95% CI [0.72] to [3.24] units lower). No single dose differed clearly from vehicle (Dunnett, p = [0.69], [0.09] and [0.07]).
Replace the bracketed values with your own.
References
- OECD (2006). Current approaches in the statistical analysis of ecotoxicity data: a guidance to application. OECD Series on Testing and Assessment No. 54. doi:10.1787/9789264085275-en
- Armitage P (1955). Tests for linear trends in proportions and frequencies. Biometrics 11:375–386. doi:10.2307/3001775
- Dunnett CW (1955). A multiple comparison procedure for comparing several treatments with a control. Journal of the American Statistical Association 50:1096–1121. doi:10.1080/01621459.1955.10501294
- Jonckheere AR (1954). A distribution-free k-sample test against ordered alternatives. Biometrika 41:133–145. doi:10.1093/biomet/41.1-2.133
- Williams DA (1971). A test for differences between treatment means when several dose levels are compared with a zero dose control. Biometrics 27:103–117. doi:10.2307/2528930
How this page was made
- Written and published by Licklider. Recommendations cite peer-reviewed methods papers and OECD guidance 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 with multcomp 1.4-25), 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.