Answer

What counts as a family when you correct for multiple comparisons?

Inspired by a question on Cross Validated ·

multiple comparisonshypothesis testingp-values

The short answer

A family is the set of tests that jointly support one conclusion. Fix it in advance from the claims you will make. Correct each outcome separately when each answers its own question; pool the tests when any significant result would back one overall claim. In one simulation, per-outcome Holm let a false positive through somewhere in 12.6% of studies; one pooled Holm correction held that to 4.0%.

The short answer

A familywise correction such as Bonferroni or Holm guarantees that, within a chosen set of tests, the chance of even one false positive stays at or below α. It says nothing about tests outside that set. So the real decision is which tests go in the same set, and the answer comes from your conclusions, not from your spreadsheet: a family is the group of tests whose results jointly feed a single claim, so that one false positive would make that claim wrong.

This page is about how to draw the boundary of a family. For the separate question of whether to correct at all, and how Bonferroni, Holm and Benjamini-Hochberg compare, see when to use a Bonferroni correction.

See it in R and Python

The code has two parts. Part 1 takes nine p-values from a design with three pairwise comparisons on each of three outcomes and applies Holm two ways: within each outcome, and to all nine together. Part 2 simulates 2,000 studies with three groups of 30 and three outcomes that correlate at .5, where every null hypothesis is true. For each rule it records how often a study produced at least one false positive within a single outcome, and anywhere among the nine tests.

R

set.seed(677506)

# 1. Nine p-values: 3 pairwise comparisons x 3 outcomes (no randomness)
p <- c(0.004, 0.030, 0.210,   # outcome A
       0.012, 0.048, 0.600,   # outcome B
       0.020, 0.090, 0.330)   # outcome C
outcome <- rep(c("A", "B", "C"), each = 3)
tab <- cbind(raw         = p,
             per_outcome = ave(p, outcome, FUN = function(x) p.adjust(x, "holm")),
             all_nine    = p.adjust(p, "holm"))
round(tab, 3)
colSums(tab < 0.05)   # tests significant at .05

# 2. Simulation: 3 groups (n = 30 each), 3 outcomes correlated at .5,
#    every null hypothesis true, so every rejection is a false positive
n <- 30; rho <- 0.5; reps <- 2000
g <- rep(1:3, each = n)
pairs <- combn(3, 2)
res <- replicate(reps, {
  z <- rnorm(3 * n)                     # shared person-level component
  Y <- sapply(1:3, function(k) sqrt(rho) * z + sqrt(1 - rho) * rnorm(3 * n))
  p <- as.vector(apply(Y, 2, function(y) apply(pairs, 2, function(pr)
         t.test(y[g == pr[1]], y[g == pr[2]], var.equal = TRUE)$p.value)))
  out <- rep(1:3, each = 3)
  rej <- list(none        = p < 0.05,
              per_outcome = ave(p, out, FUN = function(x) p.adjust(x, "holm")) < 0.05,
              all_nine    = p.adjust(p, "holm") < 0.05)
  sapply(rej, function(r) c(within_one_outcome = any(r[out == 1]),
                            anywhere           = any(r)))
})
round(apply(res, c(1, 2), mean), 3)   # rates of at least one false positive

Python

import numpy as np
from scipy import stats
from itertools import combinations

rng = np.random.default_rng(677506)

# Holm adjustment, same definition as R's p.adjust(p, "holm")
def holm(p):
    p = np.asarray(p, float); m = len(p); o = np.argsort(p)
    out = np.empty(m)
    out[o] = np.minimum(1, np.maximum.accumulate((m - np.arange(m)) * p[o]))
    return out

def per_outcome_holm(p, k=3):
    return np.concatenate([holm(p[i:i + k]) for i in range(0, len(p), k)])

# 1. Nine p-values: 3 pairwise comparisons x 3 outcomes (no randomness)
p = np.array([0.004, 0.030, 0.210,    # outcome A
              0.012, 0.048, 0.600,    # outcome B
              0.020, 0.090, 0.330])   # outcome C
tab = np.column_stack([p, per_outcome_holm(p), holm(p)])
print("raw  per_outcome  all_nine")
print(np.round(tab, 3))
print("significant at .05:", (tab < 0.05).sum(axis=0))

# 2. Simulation: 3 groups (n = 30 each), 3 outcomes correlated at .5,
#    every null hypothesis true, so every rejection is a false positive
n, rho, reps = 30, 0.5, 2000
z = rng.normal(size=(reps, 1, 3 * n))               # shared person-level component
Y = np.sqrt(rho) * z + np.sqrt(1 - rho) * rng.normal(size=(reps, 3, 3 * n))
groups = [Y[:, :, i * n:(i + 1) * n] for i in range(3)]
# p-values ordered as in R: outcome 1 (pairs 12, 13, 23), outcome 2, outcome 3
pv = np.stack([stats.ttest_ind(groups[a], groups[b], axis=2).pvalue
               for a, b in combinations(range(3), 2)], axis=2).reshape(reps, 9)
rules = {"none": lambda r: r < 0.05,
         "per_outcome": lambda r: per_outcome_holm(r) < 0.05,
         "all_nine": lambda r: holm(r) < 0.05}
for name, rule in rules.items():
    rej = np.array([rule(row) for row in pv])
    print(name, "within_one_outcome", round(float(rej[:, :3].any(axis=1).mean()), 3),
          "anywhere", round(float(rej.any(axis=1).mean()), 3))
        raw per_outcome all_nine
 [1,] 0.004       0.012    0.036
 [2,] 0.030       0.060    0.180
 [3,] 0.210       0.210    0.630
 [4,] 0.012       0.036    0.096
 [5,] 0.048       0.096    0.240
 [6,] 0.600       0.600    0.660
 [7,] 0.020       0.060    0.140
 [8,] 0.090       0.180    0.360
 [9,] 0.330       0.330    0.660
        raw per_outcome    all_nine 
          5           2           1 
                    none per_outcome all_nine
within_one_outcome 0.138       0.052    0.017
anywhere           0.307       0.126    0.040

The simulated rates come from one seeded run of the R code. Python's random numbers differ from R's, so its simulated rates differ slightly (for example 12.2% instead of 12.6% for a false positive anywhere under per-outcome Holm), but they show the same pattern. Part 1 involves no randomness, and Python reproduces those adjusted p-values exactly.

What the numbers show

Questions that settle the grouping

Whatever you decide, write it down before looking at the results, apply the same rule to every outcome, and report every test in the family. If you also need to choose between planned and post hoc comparisons, see planned contrasts vs post hoc tests; our test chooser and the DASS blog cover choosing and reporting tests more broadly.

How to report adjusted p-values in APA style (7th edition)

State the families, the correction and the number of tests in each family in the method section, then give adjusted p-values. Using the per-outcome result from the example above:

"Pairwise comparisons among the three groups were adjusted with the Holm method separately for each outcome (three families of three comparisons), because each outcome addressed a distinct research question. For outcome A, one comparison remained significant (adjusted p = .012), as did one comparison for outcome B (adjusted p = .036); no comparison for outcome C was significant (all adjusted ps ≥ .060)."

If you pooled the tests instead: "To control the familywise error rate across all nine comparisons, p-values were adjusted with the Holm method; one comparison remained significant (adjusted p = .036)." In a table, list raw and adjusted p-values side by side and name the method and family size in a table note.

Related tools and guides

More answered questions

Working with your own data?

General answers only go so far. Send us your situation and we'll reply by email within two business days.

Ask your question

Written by AskStats with AI assistance. This is general information, not advice for your specific data or study. When the results matter, check your approach with a qualified statistician.