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
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)

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.