The IQR is Q3 − Q1: the width of the middle 50% of sorted data. It is robust to outliers because the extreme 25% on each end are excluded. Sort, compute the two quartile positions (exact integers for n=9), read sv[pos], subtract. With scipy, scipy.stats.iqr(x) uses linear interpolation (matching np.percentile) by default.

By hand

Same n=9 data as median-and-quartiles. Positions pos_q1=2, pos_q3=6 are exact integers — no interpolation. q1=sv[2]=3, q3=sv[6]=7, iqr=7−3=4.

naive.py
Replay: real traced execution (multi-file project)
values = [4, 7, 2, 9, 1, 8, 5, 6, 3]
sv = sorted(values)
n = len(sv)
pos_q1 = int((n - 1) * 0.25)
pos_q3 = int((n - 1) * 0.75)
q1 = sv[pos_q1]
q3 = sv[pos_q3]
iqr = q3 - q1
print('RESULT:', iqr)
  1. values ← [4, 7, 2, 9, 1, 8, 5, 6, 3]

    1values = [4, 7, 2, 9, 1, 8, 5, 6, 3]2sv = sorted(values)
    values this step[4, 7, 2, 9, 1, 8, 5, 6, 3]values
  2. sv ← [1, 2, 3, 4, 5, 6, 7, 8, 9]

    1values = [4, 7, 2, 9, 1, 8, 5, 6, 3]2sv = sorted(values)3n = len(sv)
    values this step[1, 2, 3, 4, 5, 6, 7, 8, 9]sv
  3. n ← 9

    2sv = sorted(values)3n = len(sv)4pos_q1 = int((n - 1) * 0.25)
    values this step9n
  4. pos_q1 ← 2

    3n = len(sv)4pos_q1 = int((n - 1) * 0.25)5pos_q3 = int((n - 1) * 0.75)
    values this step2pos_q1
  5. pos_q3 ← 6

    4pos_q1 = int((n - 1) * 0.25)5pos_q3 = int((n - 1) * 0.75)6q1 = sv[pos_q1]
    values this step6pos_q3
  6. q1 ← 3

    5pos_q3 = int((n - 1) * 0.75)6q1 = sv[pos_q1]7q3 = sv[pos_q3]
    values this step3q1
  7. q3 ← 7

    6q1 = sv[pos_q1]7q3 = sv[pos_q3]8iqr = q3 - q1
    values this step7q3
  8. iqr ← 4

    7q3 = sv[pos_q3]8iqr = q3 - q19print('RESULT:', iqr)
    values this step4iqr
  9. stdout ← RESULT: 4

    8iqr = q3 - q19print('RESULT:', iqr)
    values this stepRESULT: 4stdout

With the library

scipy.stats.iqr(x) defaults to rng=(25, 75) and linear interpolation, matching np.percentile. With exact-integer positions the result is the same as the naive subtraction. Showing Q1 and Q3 separately makes the denominator and subtraction visible.

library.py
import numpy as np
from scipy.stats import iqr
from dalib.display import set_display
set_display()

x = [4, 7, 2, 9, 1, 8, 5, 6, 3]
q1, q3 = np.percentile(x, [25, 75])
result = iqr(x)
print('Q1:', int(q1))
print('Q3:', int(q3))
print('RESULT:', int(result))
Q1: 3
Q3: 7
RESULT: 4

Implementation notes

  • IQR is robust: adding a large outlier shifts Q1/Q3 only if it pushes into the middle 50%, which is rare. Compare to std, which grows with every outlier.
  • scipy.stats.iqr accepts a rng parameter if you want a different percentile spread (e.g. rng=(10, 90) for the interdecile range).
  • Cross-reference: median-and-quartiles (this chapter) for the quartile positions; iqr-outlier-flags (python-data-cleaning ch06) for using IQR to flag outliers via the 1.5×IQR fence rule.