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

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.

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