Answer

What are the assumptions of the Wilcoxon signed-rank test?

Inspired by a question on Cross Validated ·

nonparametrichypothesis testing

The short answer

It assumes independent pairs, differences whose sizes can be ranked, and differences symmetric about their centre. If your null is that the conditions make no difference at all, symmetry about zero holds automatically under it, so the test is valid. Symmetry matters for interpretation: with skewed differences the test is about the pseudomedian, not the median. A symmetry test with p > .05 on 15 pairs proves little.

The short answer

The Wilcoxon signed-rank test is the rank-based alternative to the paired t test. You compute one difference per pair (for example, each person's score in condition B minus their score in condition A), rank the absolute differences, and add up the ranks that belong to the positive differences. If the conditions do not matter, positive and negative differences should get roughly equal shares of the ranks.

It does not assume normality. It does assume three things: the pairs are independent of each other, the differences are measured precisely enough that their sizes can be ranked, and the differences are symmetric about their centre. The third one is the most misunderstood, and it matters less for the validity of the test than for how you read the result.

What each assumption means

Why symmetry matters, and why a symmetry test cannot settle it

Formally, the null hypothesis is that the differences are symmetric about zero. In a paired design where the null is "the condition has no effect at all", the two measurements on each person are interchangeable under that null, so their difference is automatically symmetric about zero. In that case the test is valid without any extra check: its false-positive rate is what it claims to be.

Symmetry becomes important when you want to say what shifted. If the differences are skewed, the test is sensitive to the pseudomedian (the median of all pairwise averages of the differences), not the median. The simulation below shows the consequence: with skewed differences whose median is exactly zero, the signed-rank test still rejects "centre = 0" far more often than 5%, because the pseudomedian is not zero. So a significant result then means "the differences tend to be positive", not "the median difference is not zero".

Running a formal test of symmetry and treating p > .05 as proof does not help. With 15 pairs such tests have very little power, so a non-significant result mostly reflects the small sample. This is the same trap as pre-testing for normality (see Is a normality test worth running on your data?). Choosing the test based on a pre-test also changes the error rates of the whole procedure. Look at a dot plot of the differences, think about how the data arise, and decide what you want to estimate.

See it in R and Python

The code runs the test on 15 made-up paired differences, reports the Hodges-Lehmann estimate with its exact 95% confidence interval and a rank-biserial effect size, and then simulates 10,000 samples of 15 to compare the signed-rank test with the sign test when the differences are symmetric and when they are skewed.

R

set.seed(677151)

# 1. Paired design: 15 people measured under two conditions.
#    d = condition B minus condition A, one difference per person
d <- c(4.2, 7.9, -1.3, 5.6, 3.1, 9.4, 2.7, -2.2, 6.8, 1.9,
       4.9, 8.3, 0.6, 3.8, 5.2)
wt <- wilcox.test(d, mu = 0, conf.int = TRUE)  # same as paired = TRUE on the raw columns
unname(wt$statistic)                           # V = sum of ranks of the positive differences
round(wt$p.value, 4)
round(c(pseudomedian = unname(wt$estimate), wt$conf.int), 2)
round(c(median = median(d), mean = mean(d), sd = sd(d)), 2)

# Matched-pairs rank-biserial correlation (effect size)
rk <- rank(abs(d))
round((sum(rk[d > 0]) - sum(rk[d < 0])) / sum(rk), 3)

# 2. Why symmetry matters: how often is H0 "centre = 0" rejected at 5%?
reject <- function(gen, reps = 10000, n = 15) {
  res <- replicate(reps, {
    x <- gen(n)
    c(signed_rank = wilcox.test(x, mu = 0)$p.value < 0.05,
      sign_test   = binom.test(sum(x > 0), n)$p.value < 0.05)
  })
  rowMeans(res)
}
# Symmetric about 0 with heavy tails: both tests stay at or below 5%
round(reject(function(n) rt(n, df = 3)), 3)
# Skewed, but the median is exactly 0: the signed-rank test rejects too often
round(reject(function(n) rexp(n) - log(2)), 3)

# The signed-rank test targets the pseudomedian, which is not 0 here
x <- rexp(1e6) - log(2)
round(median((x[1:5e5] + x[5e5 + 1:5e5]) / 2), 3)

Python

import numpy as np
from scipy import stats

rng = np.random.default_rng(677151)

# 1. Paired design: 15 people measured under two conditions.
#    d = condition B minus condition A, one difference per person
d = np.array([4.2, 7.9, -1.3, 5.6, 3.1, 9.4, 2.7, -2.2, 6.8, 1.9,
              4.9, 8.3, 0.6, 3.8, 5.2])
n = len(d)
rk = stats.rankdata(np.abs(d))
V = rk[d > 0].sum()                         # sum of ranks of the positive differences
p = stats.wilcoxon(d, method="exact").pvalue
print("V", V, "p", round(p, 4))

# Hodges-Lehmann (pseudomedian) estimate and exact 95% CI, as in R's wilcox.test
walsh = np.sort([(d[i] + d[j]) / 2 for i in range(n) for j in range(i, n)])
counts = np.array([1])                      # exact null distribution of V
for k in range(1, n + 1):
    counts = np.concatenate([counts, np.zeros(k)]) + np.concatenate([np.zeros(k), counts])
cdf = np.cumsum(counts) / 2**n
qu = max(int(np.argmax(cdf >= 0.025 - 1e-10)), 1)
ql = n * (n + 1) // 2 - qu
print("pseudomedian", round(np.median(walsh), 2), "CI", round(walsh[qu - 1], 2), round(walsh[ql], 2))
print("median", round(np.median(d), 2), "mean", round(d.mean(), 2), "sd", round(d.std(ddof=1), 2))

# Matched-pairs rank-biserial correlation (effect size)
print("rank-biserial", round((rk[d > 0].sum() - rk[d < 0].sum()) / rk.sum(), 3))

# 2. Why symmetry matters: how often is H0 "centre = 0" rejected at 5%?
def reject(gen, reps=10000, n=15):
    sr = st = 0
    for _ in range(reps):
        x = gen(n)
        sr += stats.wilcoxon(x, method="exact").pvalue < 0.05
        st += stats.binomtest(int((x > 0).sum()), n).pvalue < 0.05
    return {"signed_rank": round(sr / reps, 3), "sign_test": round(st / reps, 3)}

# Symmetric about 0 with heavy tails: both tests stay at or below 5%
print(reject(lambda n: rng.standard_t(3, n)))
# Skewed, but the median is exactly 0: the signed-rank test rejects too often
print(reject(lambda n: rng.exponential(size=n) - np.log(2)))

# The signed-rank test targets the pseudomedian, which is not 0 here
x = rng.exponential(size=1_000_000) - np.log(2)
print(round(np.median((x[:500_000] + x[500_000:]) / 2), 3))

The figures below come from one seeded run of the R code. The test on the fixed data (part 1) involves no randomness, and the Python version reproduces it exactly. Python's random numbers differ from R's, so its simulated rates differ slightly (for example 7.8% instead of 8.2%), but they show the same pattern.

What to do in practice

Not sure whether a paired test is the right tool at all? Try the test chooser. For more plain-language guides to planning and reporting analyses, see the DASS blog.

How to report a Wilcoxon signed-rank test in APA style (7th edition)

Name the test, give the statistic and the exact p value (or p < .001 when it is smaller), and add an effect size and an estimate of the shift with its confidence interval. Using the example above:

"Scores were higher in condition B than in condition A for 13 of 15 participants (Mdn difference = 4.20). A Wilcoxon signed-rank test showed that this difference was statistically significant, V = 114, p < .001, with a Hodges-Lehmann estimated shift of 4.15, 95% CI [2.15, 6.05], and a matched-pairs rank-biserial correlation of r = .90."

Some software reports the smaller rank sum (T) or a standardised z instead of V; report whichever statistic you used and name it. In the method section, state that you used the exact test (or the normal approximation, and how ties and zero differences were handled) and any correction for multiple comparisons.

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.