DEV Community

Alan Matthew
Alan Matthew

Posted on

Finding Outliers with the IQR Method: Tukey Fences, Quartile Gotchas, and Code

Outlier detection looks like a solved problem. Sort the data, compute quartiles, multiply by 1.5, done. Then you run the same dataset through Excel, NumPy, and R and get three slightly different sets of fences.

This post covers the IQR (interquartile range) method end to end: the formula, a worked example, why quartile definitions disagree, where the 1.5 comes from, why it beats z-scores in some cases and fails in others, and code you can paste into a notebook.

The five-number summary in 60 seconds

The IQR method is built on order statistics, meaning it only cares about where values rank, not their size:

  • Minimum
  • Q1: the 25th percentile
  • Median (Q2): the 50th percentile
  • Q3: the 75th percentile
  • Maximum

The IQR is the spread of the middle half of your data:

IQR = Q3 − Q1
Enter fullscreen mode Exit fullscreen mode

Because Q1 and Q3 are ranks, an absurdly large maximum can't drag them around. That resistance to extreme values is the whole point. The mean and standard deviation get pulled by outliers, while the quartiles barely notice them.

The Tukey fences

John Tukey introduced this rule in Exploratory Data Analysis (1977). You build fences around the middle half of the data and flag whatever falls outside:

Inner fences:  Q1 − 1.5 × IQR     and     Q3 + 1.5 × IQR
Outer fences:  Q1 − 3.0 × IQR     and     Q3 + 3.0 × IQR
Enter fullscreen mode Exit fullscreen mode
  • Inside the inner fences: normal.
  • Between the inner and outer fences: mild outlier (Tukey called these "outside" values).
  • Beyond the outer fences: extreme outlier (Tukey's "far out" values).

Most box plots draw the whiskers out to the most extreme points still inside the inner fences, and plot everything beyond as individual dots.

Worked example: 12 test scores

55, 62, 68, 70, 72, 75, 78, 80, 85, 88, 92, 120
Enter fullscreen mode Exit fullscreen mode

The data is already sorted, and n = 12.

Median. With an even count, average the 6th and 7th values: (75 + 78) / 2 = 76.5.

Q1. Median of the lower half (55, 62, 68, 70, 72, 75): (68 + 70) / 2 = 69.

Q3. Median of the upper half (78, 80, 85, 88, 92, 120): (85 + 88) / 2 = 86.5.

IQR. 86.5 − 69 = 17.5.

Inner fences:

Lower = 69   − 1.5 × 17.5 = 69   − 26.25 = 42.75
Upper = 86.5 + 1.5 × 17.5 = 86.5 + 26.25 = 112.75
Enter fullscreen mode Exit fullscreen mode

Outer fences:

Lower = 69   − 3 × 17.5 = 16.5
Upper = 86.5 + 3 × 17.5 = 139
Enter fullscreen mode Exit fullscreen mode

Classify. Everything from 55 to 92 sits inside [42.75, 112.75], so it's normal. 120 is above 112.75 but below 139, so it's a mild outlier.

Mild is a useful label. It tells you something is unusual, not that something is wrong. A score of 120 could be a legitimately brilliant exam (if extra credit exists), a typo for 12 or 102, or a different scale. The method flags the value, and you have to find out which of those it is.

The gotcha: there is no single "Q1"

Here is where tools disagree. There are at least nine accepted ways to compute a quartile. For the same 12 scores:

Method Q1 Q3 IQR Inner fences Flagged
Median of halves / Tukey hinges 69 86.5 17.5 42.75 to 112.75 120
Linear interpolation (Excel QUARTILE.INC, NumPy and pandas default, R quantile default) 69.5 85.75 16.25 45.125 to 110.125 120
(n+1) interpolation (Excel QUARTILE.EXC) 68.5 87.25 18.75 40.375 to 115.375 120

Here all three flag 120, so the conclusion survives. But the fences move by several points. Now imagine one student scored 111 instead of 120. The quartiles don't change under any of these methods, but:

  • The linear method's upper fence is 110.125, so 111 is flagged.
  • The hinge method (112.75) and the (n+1) method (115.375) say 111 is fine.

Same data, different verdict, and nobody made an error. Borderline values are exactly where the quartile convention decides the outcome.

For odd n, the "median of halves" approach splits into two camps:

  • Include the median in both halves (Tukey's hinges): for 1 to 11, Q1 = 3.5 and Q3 = 8.5.
  • Exclude the median (the Moore and McCabe textbook method): for 1 to 11, Q1 = 3 and Q3 = 9.

Practical advice: decide on a method, say which one you used, and use the same method every time you compare results across tools. If a classmate's answer differs from yours in the second decimal, check the quartile definition before you assume someone made an arithmetic mistake.

Why 1.5?

It isn't arbitrary, though it isn't sacred either. In a normal distribution, IQR ≈ 1.349σ and Q3 ≈ +0.6745σ. So the upper inner fence lands at:

0.6745σ + 1.5 × 1.349σ ≈ 2.70σ
Enter fullscreen mode Exit fullscreen mode

The two inner fences together flag roughly 0.7% of perfectly normal data. That's a sensible trade-off: rare enough to be interesting, common enough that you'll catch genuine anomalies in a few hundred rows. The outer fences sit at about 4.72σ, which flags roughly 2 in a million observations from a normal distribution. A "far out" value is therefore very unlikely to be an ordinary draw from a normal population.

You can check this by simulation:

import numpy as np

rng = np.random.default_rng(42)
x = rng.normal(0, 1, 100_000)
q1, q3 = np.percentile(x, [25, 75])
iqr = q3 - q1
flagged = ((x < q1 - 1.5 * iqr) | (x > q3 + 1.5 * iqr)).mean()
print(f"{flagged:.2%}")   # about 0.71%
Enter fullscreen mode Exit fullscreen mode

Multipliers other than 1.5 are legitimate. Use 3 if you only want to see extreme cases, and note that you chose it.

IQR vs z-score: the masking problem

The z-score method flags points more than 2 or 3 standard deviations from the mean. It's easy to compute, and it has a structural flaw: the outliers distort the mean and SD they're being measured against. This is called masking.

10, 11, 12, 12, 13, 13, 14, 15, 15, 16, 90, 95
Enter fullscreen mode Exit fullscreen mode

Two obviously bad values, 90 and 95. The mean is 26.33 and the sample SD is 30.97. The z-scores of the two suspects:

z(90) = 2.06
z(95) = 2.22
Enter fullscreen mode Exit fullscreen mode

Neither passes a 3-SD cutoff, and neither even passes 2.5. The outliers inflated the SD enough to hide themselves.

With the IQR method, Q1 = 12, Q3 = 15.25, IQR = 3.25. The inner fences are 7.125 and 20.125, and the outer fences are 2.25 and 25.0. Both 90 and 95 are beyond the outer fence, so both are extreme outliers. The quartiles ignored them, which is exactly what you want.

The original test-score dataset shows a milder version. The z-score of 120 is 2.46 with it included, but 4.03 when you compute it against the other 11 scores. The outlier makes itself look less extreme.

Where z-scores are still fine: large samples from roughly symmetric, near-normal data, and as a quick sanity check. Elsewhere, prefer IQR, or move to a robust version (median and MAD).

Code

Python

import numpy as np

def iqr_outliers(data, k=1.5, method="linear"):
    x = np.sort(np.asarray(data, dtype=float))
    q1, q3 = np.percentile(x, [25, 75], method=method)
    iqr = q3 - q1
    lo_in, hi_in = q1 - k * iqr, q3 + k * iqr
    lo_out, hi_out = q1 - 3 * iqr, q3 + 3 * iqr
    mild = x[((x < lo_in) & (x >= lo_out)) | ((x > hi_in) & (x <= hi_out))]
    extreme = x[(x < lo_out) | (x > hi_out)]
    return {
        "q1": q1, "q3": q3, "iqr": iqr,
        "inner": (lo_in, hi_in), "outer": (lo_out, hi_out),
        "mild": mild.tolist(), "extreme": extreme.tolist(),
    }

scores = [55, 62, 68, 70, 72, 75, 78, 80, 85, 88, 92, 120]

print(iqr_outliers(scores, method="linear"))    # Q1=69.5,  Q3=85.75, fences 45.125 to 110.125
print(iqr_outliers(scores, method="hazen"))     # Q1=69,    Q3=86.5,  fences 42.75 to 112.75
print(iqr_outliers(scores, method="weibull"))   # Q1=68.5,  Q3=87.25, fences 40.375 to 115.375
Enter fullscreen mode Exit fullscreen mode

In all three cases mild is [120.0]. The method argument needs NumPy 1.22 or later (older versions use interpolation=). "hazen" happens to match the median-of-halves answer for this even-length dataset. For odd n, it won't always match the hinges or the exclude-the-median method, so don't treat it as a general substitute.

pandas

import pandas as pd

s = pd.Series(scores)
q1, q3 = s.quantile([0.25, 0.75])
iqr = q3 - q1
outliers = s[(s < q1 - 1.5 * iqr) | (s > q3 + 1.5 * iqr)]
Enter fullscreen mode Exit fullscreen mode

R

x <- c(55, 62, 68, 70, 72, 75, 78, 80, 85, 88, 92, 120)

boxplot.stats(x)$stats   # 55 69 76.5 86.5 92  (whisker, hinge, median, hinge, whisker)
boxplot.stats(x)$out     # 120
Enter fullscreen mode Exit fullscreen mode

Careful here: boxplot.stats() uses Tukey's hinges (fivenum), while quantile() defaults to linear interpolation. Same package, two quartile definitions.

Excel

=QUARTILE.INC(A2:A13, 1)    → 69.5
=QUARTILE.INC(A2:A13, 3)    → 85.75
=QUARTILE.EXC(A2:A13, 1)    → 68.5
=QUARTILE.EXC(A2:A13, 3)    → 87.25
Enter fullscreen mode Exit fullscreen mode

Then compute the IQR and the fences in neighboring cells, and use conditional formatting to highlight cells outside them.

Quick box plot

import matplotlib.pyplot as plt

plt.boxplot(scores, vert=False, whis=1.5)
plt.xlabel("Score")
plt.show()
Enter fullscreen mode Exit fullscreen mode

Matplotlib's default whisker setting is already the 1.5 × IQR rule.

When the IQR rule misleads you

  • Skewed data. The fences are symmetric, but skewed distributions aren't. On a log-normal sample, the rule flags about 8% of observations (run the simulation above with rng.lognormal(0, 1, 10_000)), almost all in the long right tail. Those aren't anomalies. They're just the shape of the distribution. Consider a log transform first, or an adjusted boxplot designed for skew.
  • Lots of tied values. If more than half your values are identical, Q1 = Q3, the IQR is 0, and the fences collapse onto that value. Everything else gets flagged. This is common with discrete counts and heavily rounded data.
  • Very small samples. With 4 to 8 points, the quartiles themselves are unstable, so treat flags as prompts for a closer look.
  • Multimodal data. Two clusters make the "middle half" meaningless as a description of normal behavior.
  • Multivariate and time-series data. A point can look fine on each variable separately and still be an outlier in combination. Seasonality can make normal peaks look like outliers. Use methods that respect the structure.
  • Categorical data. The method applies to numeric data only.

What to do after you flag something

Detection is the easy half. In order:

  1. Check for errors. Typos, unit mix-ups (grams vs kilograms), sentinel values like 999 or -1, and copy-paste glitches.
  2. Check for a legitimate explanation. A different population, a real rare event, a known measurement change.
  3. Decide and document. Correct it, exclude it, keep it, or analyze both ways. Whatever you choose, write it down along with the fence method and multiplier.
  4. Consider robust alternatives instead of deleting: median instead of mean, winsorizing, trimmed means, rank-based tests, or a transformation.

Deleting a point because it's inconvenient is one of the fastest ways to get a result you can't defend.

Skip the arithmetic: a free calculator

If you want a quick answer without opening a notebook, I maintain a free IQR Outlier Calculator. Paste your values (commas, spaces, or newlines), pick the 1.5 or 3 multiplier and a quartile method, and it returns the five-number summary, IQR, inner and outer fences, a box plot, a per-point classification, and the step-by-step working.

A good test for any outlier tool, mine included: paste in the 12 scores from this post. With the standard textbook setting you should see Q1 = 69, Q3 = 86.5, IQR = 17.5, fences at 42.75 and 112.75, and 120 flagged as a mild outlier. If you get slightly different quartiles, check the quartile-method setting first, since that's the usual cause (see the table above).

More from this series: PASTE_CI_POST_URL_HERE and PASTE_CRONBACH_POST_URL_HERE.

FAQ

Is a mild outlier a mistake?
No. It's a value worth a second look. Plenty of mild outliers are real data.

Should I use 1.5 or 3?
1.5 is the convention and a good default for exploration. Use 3 when you only want to hunt for truly extreme values, or when flagging too many points is costly.

Why does my software disagree with my textbook?
Almost always the quartile definition. See the comparison table.

Does the IQR method need normally distributed data?
No. That's its main advantage over z-scores. But symmetric fences work best on roughly symmetric data.

What's the minimum sample size?
Technically four values. Practically, treat results on fewer than about 10 as informal.

Can I apply it per group?
Yes, and often you should. A value that is an outlier overall may be perfectly normal within its group (for example, a heavy weight in a "large breed" subgroup).


Have a dataset where two tools disagreed on which points were outliers? Share the quartile settings in the comments and let's figure out why.

Top comments (0)