Module 5 — Position and boxplot

Data Description | Part II — Summarising one variable

Modified

October 8, 2026

The business question. “The Wandering Fork” has 120 days of daily revenue data per franchise. The owner wants to know: what is a “typical” day, how spread out are the days, and are there any wildly atypical days? This module teaches you to locate specific points within a distribution — the median, quartiles, and percentiles — and to build the boxplot, the single most information-dense chart in descriptive statistics.

Learning objectives

By the end of this module, you will be able to:

  • Compute the median, quartiles, and any percentile of raw or grouped data.
  • Apply linear interpolation to find a quantile in grouped (class-based) continuous data.
  • Build a boxplot from the five-number summary and detect outliers with the fence rule.
  • Use the interactive demo below to see how changing the quartiles reshapes a boxplot.

Time plan

Block Minutes What you do
Theory 60 Median, quartiles, percentiles, IQR, boxplot, outlier fences, grouped-data interpolation
Excel lab 60 QUARTILE.EXC, PERCENTILE.EXC, MIN, MAX, box-and-whisker chart
Exercises 60 Three by-hand and three Excel exercises
Total 180

Theory

Median

NoteDefinition — median

The median \(Me\) is the value that splits an ordered series into two equal halves: at least 50% of observations are \(\leq Me\) and at least 50% are \(\geq Me\). For \(n\) ordered values, if \(n\) is odd the median is the value at position \((n+1)/2\); if \(n\) is even, it is the average of the two values at positions \(n/2\) and \(n/2+1\).

Quartiles (exclusive method)

TipFormula — quartiles (exclusive method)

The three quartiles \(Q_1, Q_2 (=Me), Q_3\) split an ordered series into four equal quarters. Following the exclusive convention used throughout this course (Excel’s QUARTILE.EXC), the position of \(Q_p\) (\(p=0.25, 0.5, 0.75\)) in the ordered data is \[ \text{rank}(Q_p) = p\,(n+1), \] interpolating linearly between the two surrounding observations when the rank is not a whole number.

In plain words: to find \(Q_1\), compute \(0.25 \times (n+1)\); if the result is 7.75, take the value 75% of the way between the 7th and 8th smallest observations. This is exactly the position formula from OpenStax §2.2, and it matches Excel’s QUARTILE.EXC.

Interquartile range and outlier fences

TipFormula — interquartile range and outlier fences

\[ IQR = Q_3 - Q_1, \qquad \text{lower fence} = Q_1 - 1.5\,IQR, \qquad \text{upper fence} = Q_3 + 1.5\,IQR. \] Any observation outside \([\text{lower fence}, \text{upper fence}]\) is flagged as a statistical outlier on the boxplot.

In plain words: the IQR measures the spread of the central 50% of the data. The fences extend 1.5 IQRs beyond the box — anything further out is an outlier.

Boxplot

NoteDefinition — boxplot

A boxplot draws the five-number summary (\(\min, Q_1, Me, Q_3, \max\), or the fences if there are outliers) as a box (from \(Q_1\) to \(Q_3\), split by a line at \(Me\)) with whiskers extending to the most extreme non-outlier values; outliers are marked individually.

Percentiles

TipFormula — percentiles (exclusive method)

For any percentile \(P_p\) (\(0 < p < 1\)), the position in the ordered data is \(\text{rank}(P_p) = p\,(n+1)\), interpolating when the rank is not an integer. In Excel: =PERCENTILE.EXC(range, p).

Grouped-data interpolation

TipFormula — quantile in grouped continuous data

For a continuous variable grouped into classes, we cannot read \(Q_p\) directly from a list of raw values — we interpolate within the class that contains the target cumulative frequency. If class \([L, L+h[\) has lower cumulative count \(N_{\text{before}}\) (from all earlier classes) and count \(n_{\text{class}}\) of its own, and we seek the value at cumulative position \(p\cdot n\), linear interpolation across the class width \(h\) gives \[ Q_p = L + \frac{p\,n - N_{\text{before}}}{n_{\text{class}}}\; h. \]

In plain words: find which class contains the target position, then interpolate proportionally across that class’s width.

Worked example

Using “The Wandering Fork”’s daily_revenue_eur (\(n=30\)), verified in data-model.md:

Statistic Value
Minimum 407.59
\(Q_1\) (exclusive) 506.61
\(Q_2 = Me\) 643.63
\(Q_3\) (exclusive) 729.99
Maximum 886.56
\(IQR = Q_3-Q_1\) 223.39
Lower fence 171.52
Upper fence 1065.07

Step 1 — locate \(Q_1\). With \(n=30\), rank\((Q_1) = 0.25\times 31 = 7.75\): interpolate 75% of the way between the 7th and 8th smallest values of the ordered daily_revenue_eur series: \(500.02 + 0.75\times(508.80-500.02) = 506.605 \approx 506.61\).

Step 2 — locate \(Q_3\). rank\((Q_3) = 0.75\times 31 = 23.25\): interpolate between the 23rd and 24th smallest values: \(725.66 + 0.25\times(742.99-725.66) = 729.9925 \approx 729.99\).

Step 3 — fences and outliers. Keeping the unrounded quartiles, \(IQR = 729.9925-506.605 = 223.3875 \approx 223.39\); lower fence \(= 506.605 - 1.5(223.3875) \approx 171.52\); upper fence \(= 729.9925+1.5(223.3875) \approx 1065.07\). The observed minimum (407.59) and maximum (886.56) both lie inside the fences — “The Wandering Fork” has zero outliers in its daily revenue, itself a useful finding: 30 days of trading, however variable, never produced a wildly atypical day.

Step 4 — draw the boxplot. Box from 506.61 to 729.99, median line at 643.63, whiskers extending to the true min (407.59) and max (886.56) since neither is an outlier.

Step 5 — grouped-data check. Using the class table from Module 3 to estimate \(Q_1\) (target cumulative position \(0.25\times30=7.5\)): cumulative count reaches 6 after [400,500[ and 12 after [500,600[, so \(Q_1\) lies in [500,600[ with \(N_{\text{before}}=6\), \(n_{\text{class}}=6\), \(h=100\): \[ Q_1 \approx 500 + \frac{7.5-6}{6}\times100 = 525. \] This is close to, but not identical to, the raw-data value of 506.61 — the difference is the same class-grouping approximation error discussed in Module 4.

Using Excel

The sheet harbour_30d of wandering-fork.xlsx has the data. The complete list of function names, with their French equivalents, is on the Excel functions page.

Concept Excel function / steps
Median =MEDIAN(range)
Quartiles (exclusive) =QUARTILE.EXC(range, 1) / (range, 2) / (range, 3)
Arbitrary percentile =PERCENTILE.EXC(range, p), e.g. p=0.9 for \(P_{90}\)
IQR =QUARTILE.EXC(range,3)-QUARTILE.EXC(range,1)
Boxplot Select the data → Insert → Statistical Chart → Box and Whisker

Why QUARTILE.EXC? The course follows the \((n+1)\times p\) position formula (OpenStax §2.2), which matches Excel’s exclusive method. QUARTILE.INC (inclusive) gives different values on small samples — do not mix them.

For the full table (120 days per franchise) use the Excel Table tblFork and add the franchise as one more condition, for example =QUARTILE.EXC(IF(tblFork[franchise]="Harbour",tblFork[daily_revenue_eur]),1) (entered as an array formula in older Excel, or use FILTER in newer versions).

Proof / derivation

For a continuous variable grouped into classes, we cannot read \(Q_p\) directly from a list of raw values — we interpolate within the class that contains the target cumulative frequency. If class \([L, L+h[\) has lower cumulative count \(N_{\text{before}}\) (from all earlier classes) and count \(n_{\text{class}}\) of its own, and we seek the value at cumulative position \(p\cdot n\), linear interpolation across the class width \(h\) gives \[ Q_p = L + \frac{p\,n - N_{\text{before}}}{n_{\text{class}}}\; h. \]

Sanity check with our class table (Module 3): to locate the median (\(p=0.5\), target position \(0.5\times30=15\)) in daily_revenue_eur’s classes, the cumulative count reaches 12 after [500,600[ and 20 after [600,700[, so the median lies in [600,700[ with \(N_{\text{before}}=12\), \(n_{\text{class}}=8\), \(h=100\): \[ Me \approx 600 + \frac{15-12}{8}\times 100 = 637.5, \] close to (though not identical to, because this formula uses grouped midpoint approximations while the true value uses the raw 30 observations) the raw-data median of \(643.63\) computed above — the small gap is exactly the information lost when data is grouped into classes.

Visual intuition

The boxplot is the five-number summary drawn as a picture: the box’s width is the IQR (how spread out the central 50% of days are), the line’s position inside the box shows whether the median sits closer to \(Q_1\) or to \(Q_3\) (a first hint of asymmetry, formalised in Module 7), and the whisker lengths show how far the more extreme (but non-outlier) days stretch beyond the box.

Interactive demo — boxplot from quartiles

Drag each slider to set the five-number summary and watch the boxplot redraw. Starting values are “The Wandering Fork”’s verified statistics.

#| standalone: true
#| components: [viewer]
#| viewerHeight: 560

from shiny import App, render, ui
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

BG = '#1C2E22'
FG = '#D2CCC0'

app_ui = ui.page_fluid(
    ui.tags.style(f"""
        body {{ background-color: {BG}; color: {FG}; padding: 12px; font-family: sans-serif; margin:0; }}
        .form-label {{ color: {FG} !important; }}
        .form-range {{ accent-color: steelblue; width: 100%; }}
        .info-box {{
            background: #152119; border-radius: 6px; padding: 10px 14px;
            margin: 8px 0; border-left: 3px solid steelblue;
            font-family: monospace; font-size: 0.9em;
        }}
    """),
    ui.h5("📦 Boxplot from the five-number summary", style=f"color:{FG}"),
    ui.p(
        "Move the sliders (kept in min ≤ Q1 ≤ median ≤ Q3 ≤ max order) "
        "to see the boxplot and IQR/fences update live.",
        style=f"color:{FG}; font-size:0.88em; margin-bottom:6px;",
    ),
    ui.input_slider("vmin", "Minimum:", min=300, max=900, value=407.59, step=1),
    ui.input_slider("q1", "Q1:", min=300, max=900, value=506.61, step=1),
    ui.input_slider("med", "Median:", min=300, max=900, value=643.63, step=1),
    ui.input_slider("q3", "Q3:", min=300, max=900, value=729.99, step=1),
    ui.input_slider("vmax", "Maximum:", min=300, max=900, value=886.56, step=1),
    ui.output_ui("info"),
    ui.output_plot("plot", height="300px"),
)

def server(input, output, session):
    def ordered():
        vals = sorted([input.vmin(), input.q1(), input.med(), input.q3(), input.vmax()])
        return vals

    @output
    @render.ui
    def info():
        vmin, q1, med, q3, vmax = ordered()
        iqr = q3 - q1
        lo_fence = q1 - 1.5 * iqr
        hi_fence = q3 + 1.5 * iqr
        outlier = vmin < lo_fence or vmax > hi_fence
        tag = "⚠️ min/max beyond fences — would be plotted as outliers" if outlier else "✅ no outliers"
        return ui.HTML(f"""
            <div class='info-box'>
                IQR = {iqr:.2f} &nbsp;|&nbsp;
                Lower fence = {lo_fence:.2f} &nbsp;|&nbsp;
                Upper fence = {hi_fence:.2f}<br>{tag}
            </div>
        """)

    @output
    @render.plot
    def plot():
        vmin, q1, med, q3, vmax = ordered()

        fig, ax = plt.subplots(figsize=(7, 2.6))
        fig.patch.set_facecolor(BG)
        ax.set_facecolor(BG)
        for spine in ax.spines.values():
            spine.set_edgecolor(FG)
        ax.tick_params(colors=FG)

        box = ax.bxp(
            [{
                'med': med, 'q1': q1, 'q3': q3,
                'whislo': vmin, 'whishi': vmax,
                'fliers': [],
            }],
            vert=False, widths=0.5, patch_artist=True,
        )
        for patch in box['boxes']:
            patch.set_facecolor('steelblue')
            patch.set_edgecolor(FG)
        for line_group in ('whiskers', 'caps', 'medians'):
            for line in box[line_group]:
                line.set_color(FG)
        ax.set_yticks([])
        ax.set_xlabel("Value", color=FG)
        ax.set_title("Boxplot from the five-number summary", color=FG)
        ax.grid(alpha=0.15, color=FG, axis='x')
        plt.tight_layout()
        return fig

app = App(app_ui, server)

Exercises

For an ordered series of \(n=24\) values, at which position(s) do you read the median?

Solution. \(n=24\) is even, so the median is the average of the values at positions \(n/2=12\) and \(n/2+1=13\).

A single unusually bad day of \(\text{daily\_revenue\_eur} = 100\) is added to “The Wandering Fork”’s data. Using the fences computed above (171.52 / 1065.07, before recomputing the quartiles for the new \(n=31\) sample), would this new day be flagged as an outlier?

Solution. Yes: \(100 < 171.52\) (the lower fence), so it would be flagged as a low outlier — a day that badly underperformed relative to the rest of the trading period.

Using the class table from Module 3, estimate \(Q_1\) for daily_revenue_eur (target cumulative position \(0.25\times30=7.5\)).

Solution. Cumulative count reaches 6 after [400,500[ and 12 after [500,600[, so \(Q_1\) lies in [500,600[ with \(N_{\text{before}}=6\), \(n_{\text{class}}=6\), \(h=100\): \[ Q_1 \approx 500 + \frac{7.5-6}{6}\times100 = 525. \] This is close to, but not identical to, the raw-data value of 506.61 — the difference is the same class-grouping approximation error discussed in this module’s proof.

In wandering-fork.xlsx, filter franchise to “Campus” and build a boxplot for daily_revenue_eur (120 days). What are the five-number summary and fences? Are there outliers?

Solution. Campus 120-day: min = 23.85, Q1 = 236.07, median = 390.39, Q3 = 598.60, max = 1407.59. IQR = 362.54. Lower fence = -307.74, upper fence = 1142.40. Three days exceed the upper fence (1335.48, 1362.93 and 1407.59), so there are three high outliers and no low one — consistent with Campus’s right-skewed revenue (skewness = 1.00).

Compute \(P_{90}\) for Harbour’s 120-day daily_revenue_eur using PERCENTILE.EXC. What does this value mean?

Solution. A filter on the sheet does not change what PERCENTILE.EXC sees, so restrict the range with FILTER:

=PERCENTILE.EXC(FILTER(tblFork[daily_revenue_eur],
                       tblFork[franchise]="Harbour"), 0.9)

This gives approximately 699.63. It means 90% of Harbour trading days earned €699.63 or less; only the top 10% of days exceeded this amount.

Create side-by-side boxplots for Harbour and Campus daily_revenue_eur (120 days each). What visual differences do you see?

Solution. The two medians are almost level (~391 vs ~390), but Harbour’s box is narrower (IQR ~299 vs ~363) and Harbour has a single high outlier (€1,050.00, day 80, above its upper fence of 1016.45). Campus’s box is wider, with a long upper whisker and three high outliers (€1,335.48 to €1,407.59) — visually confirming the right skew and higher variability seen in the numbers.

Common mistakes

WarningWatch out
  • Using QUARTILE.INC instead of QUARTILE.EXC. The course standard is the exclusive method (position \((n+1)p\)); mixing them gives inconsistent quartiles, especially on small samples.
  • Forgetting that percentiles need \(0 < p < 1\) for PERCENTILE.EXC. p=0 or p=1 will error — use MIN/MAX for the extremes.
  • Confusing the fence rule with standard deviations. The 1.5×IQR rule is for boxplots; the “2 standard deviations” rule is for normal distributions — they are different tools.
  • Reading the median position as \(n/2\) instead of \((n+1)/2\). For odd \(n\), \((n+1)/2\) gives the exact middle position; \(n/2\) would be off by 0.5.
  • Using grouped-data interpolation when raw data is available. The grouped formula is an approximation; prefer the raw-data quantile when you have the full dataset.

Further reading

Source Where What it adds Time Verified
OpenStax IBS 2e §2.2 Measures of the Location of the Data (book pages 65–74) Quartiles, percentiles, IQR, boxplot ~9 pages checked against the PDF contents (T046)
Khan Academy Unit Summarizing quantitative data in the Statistics and probability course; look for lessons on quartiles, percentiles, and boxplots Short videos and practice on position indicators and boxplots ~30–45 min (estimate) link check pending (T048)

OpenStax IBS 2e: Alexander Holmes, Barbara Illowsky and Susan Dean, Introductory Business Statistics 2e, OpenStax, Rice University, https://openstax.org/details/books/introductory-business-statistics-2e, licensed under CC BY-NC-SA 4.0. Sections are cited, not reproduced.