Measure monotonic association by ranking each list then applying the rank-difference formula: r = 1 − 6Σd²/(n(n²−1)), valid only when all ranks are distinct (no ties). Assign 1-based ranks via sorted-order lookup, compute d = rx[i] − ry[i] for each pair, sum d². With scipy, spearmanr(x, y).statistic; p-value snapshot-only.

By hand

Values are distinct so no tie-handling needed. Two ranking loops: for each value look up its position in sorted(x) (0-based) and add 1 for 1-based rank. Then one loop: accumulate sum_sq_d via d=rx[i]−ry[i], d²=d*d. For x=[2,4,1,5,3] and y=[1,3,2,5,4]: ranks_x=[2,4,1,5,3], ranks_y=[1,3,2,5,4], d=[1,1,−1,0,−1], d²=[1,1,1,0,1], Σd²=4; r = 1−24/120 = 0.8.

naive.py
Replay: real traced execution (multi-file project)
x = [2, 4, 1, 5, 3]
y = [1, 3, 2, 5, 4]
n = len(x)
sx = sorted(x)
sy = sorted(y)
rx = []
for v in x:
    rx.append(sx.index(v) + 1)
ry = []
for v in y:
    ry.append(sy.index(v) + 1)
sum_sq_d = 0
for i in range(n):
    d = rx[i] - ry[i]
    sum_sq_d = sum_sq_d + d * d
r = 1 - 6 * sum_sq_d / (n * (n ** 2 - 1))
print('RESULT:', round(r, 4))
  1. x ← [2, 4, 1, 5, 3]

    1x = [2, 4, 1, 5, 3]2y = [1, 3, 2, 5, 4]
    values this step[2, 4, 1, 5, 3]x
  2. y ← [1, 3, 2, 5, 4]

    1x = [2, 4, 1, 5, 3]2y = [1, 3, 2, 5, 4]3n = len(x)
    values this step[1, 3, 2, 5, 4]y
  3. n ← 5

    2y = [1, 3, 2, 5, 4]3n = len(x)4sx = sorted(x)
    values this step5n
  4. sx ← [1, 2, 3, 4, 5]

    3n = len(x)4sx = sorted(x)5sy = sorted(y)
    values this step[1, 2, 3, 4, 5]sx
  5. sy ← [1, 2, 3, 4, 5]

    4sx = sorted(x)5sy = sorted(y)6rx = []
    values this step[1, 2, 3, 4, 5]sy
  6. rx ← []

    5sy = sorted(y)6rx = []7for v in x:
    values this step[]rx
  7. v ← 2, rx ← [2]

    pass 1 of 5
    6rx = []7for v in x:8    rx.append(sx.index(v) + 1)9ry = []
    values this step2v[] [2]rx
    All 5 passes — pass 1 is the card above
    passvrx
    12[] [2]
    22 4[2] [2, 4]
    34 1[2, 4] [2, 4, 1]
    41 5[2, 4, 1] [2, 4, 1, 5]
    55 3[2, 4, 1, 5] [2, 4, 1, 5, 3]
  8. for v in x:

    6rx = []7for v in x:8    rx.append(sx.index(v) + 1)
  9. ry ← []

    8    rx.append(sx.index(v) + 1)9ry = []10for v in y:
    values this step[]ry
  10. v ← 1, ry ← [1]

    pass 1 of 5
    9ry = []10for v in y:11    ry.append(sy.index(v) + 1)12sum_sq_d = 0
    values this step3 1v[] [1]ry
    All 5 passes — pass 1 is the card above
    passvry
    13 1[] [1]
    21 3[1] [1, 3]
    33 2[1, 3] [1, 3, 2]
    42 5[1, 3, 2] [1, 3, 2, 5]
    55 4[1, 3, 2, 5] [1, 3, 2, 5, 4]
  11. for v in y:

    9ry = []10for v in y:11    ry.append(sy.index(v) + 1)
  12. sum_sq_d ← 0

    11    ry.append(sy.index(v) + 1)12sum_sq_d = 013for i in range(n):
    values this step0sum_sq_d
  13. i ← 0, d ← 1, sum_sq_d ← 1

    pass 1 of 5
    12sum_sq_d = 013for i in range(n):14    d = rx[i] - ry[i]15    sum_sq_d = sum_sq_d + d * d16r = 1 - 6 * sum_sq_d / (n * (n ** 2 - 1))
    values this step0i1d0 1sum_sq_d
    All 5 passes — pass 1 is the card above
    passidsum_sq_d
    1010 1
    20 11 2
    31 21 -12 3
    42 3-1 0
    53 40 -13 4
  14. for i in range(n):

    12sum_sq_d = 013for i in range(n):14    d = rx[i] - ry[i]
  15. r ← 0.8

    15    sum_sq_d = sum_sq_d + d * d16r = 1 - 6 * sum_sq_d / (n * (n ** 2 - 1))17print('RESULT:', round(r, 4))
    values this step0.8r
  16. stdout ← RESULT: 0.8

    16r = 1 - 6 * sum_sq_d / (n * (n ** 2 - 1))17print('RESULT:', round(r, 4))
    values this stepRESULT: 0.8stdout

With the library

scipy.stats.spearmanr(x, y) computes Spearman r internally (rank then Pearson). .statistic is the r value; .pvalue is the two-tailed p-value for H₀: r=0, shown in snapshot but not in the RESULT comparison.

library.py
from scipy.stats import spearmanr
from dalib.display import set_display
set_display()

x = [2, 4, 1, 5, 3]
y = [1, 3, 2, 5, 4]
result = spearmanr(x, y)
r = result.statistic
p = result.pvalue
print('r:', round(r, 4))
print('p-value:', round(p, 4))
print('RESULT:', round(r, 4))
r: 0.8
p-value: 0.1041
RESULT: 0.8

Honesty

This lesson shows the computation exactly, on a tiny pinned sample. The arithmetic is correct and reproducible, but with a sample this small the result is not a valid statistical finding — it demonstrates the mechanism, not evidence. Real inference needs an adequate sample size and assumption checks (e.g. independence and continuity of the underlying distribution); the p-value / interval here should be read as "how the formula is computed," not as a conclusion about a population.

Implementation notes

  • The formula 1−6Σd²/(n(n²−1)) is algebraically equivalent to Pearson r on the ranks, but only when all ranks are distinct. With ties you must use averaged ranks and Pearson r on those averages — the simple formula no longer holds.
  • Spearman r captures monotonic relationships (x rising as y rises, even non-linearly), not just linear ones. Pearson r can be 0 while Spearman r is non-zero if the relationship is monotonic but curved.
  • Cross-reference: pearson-correlation (this chapter) for the linear counterpart and the formula that Spearman applies to ranks.