Create a Box and Whisker Chart in Excel with Python
Averages hide the shape of a distribution; a box plot shows it. For delivery times, call durations or order values, the box (the middle half of the data), the line inside it (the median) and the whiskers (the spread) tell a reader in one glance which region is consistently slow and which one is usually fine but has a long tail. Excel 2016 added a box-and-whisker chart type, but it uses a newer chart format that neither openpyxl nor XlsxWriter can write. The good news is that a classic chart type already draws exactly those shapes: a stock chart. Its up-down bars become the boxes and its high-low lines the whiskers. This page builds an editable box plot that way, with the statistics computed in pandas. It belongs to Creating Charts in Excel with openpyxl.
Prerequisites
pip install pandas xlsxwriter pillow
delivery_days.xlsx has 160 deliveries — 40 per region — with the number of days each took. LibreOffice (soffice) and pdftoppm render the chart in Step 6; they are optional.
Step 1 — Compute the five numbers per group
import pandas as pd
deliveries = pd.read_excel("delivery_days.xlsx")
order = ["North", "South", "East", "West"]
def box_stats(s):
q1, med, q3 = s.quantile([0.25, 0.5, 0.75])
iqr = q3 - q1
low_fence, high_fence = q1 - 1.5 * iqr, q3 + 1.5 * iqr
inside = s[(s >= low_fence) & (s <= high_fence)]
return pd.Series({"Q1": q1, "Median": med, "Q3": q3,
"LowWhisker": inside.min(), "HighWhisker": inside.max(),
"Outliers": int(((s < low_fence) | (s > high_fence)).sum())})
stats = deliveries.groupby("Region")["Days"].apply(box_stats).unstack().loc[order]
print(stats.round(2))
The quartiles use pandas' default method, which matches Excel's QUARTILE.INC — the difference from the "exclusive" method is explained in MEDIAN, MODE and QUARTILE Equivalents in pandas. The whiskers follow the usual Tukey rule: they reach the most extreme data point within 1.5 times the interquartile range of the box; anything beyond is an outlier, counted here and listed in Step 5. If your audience expects whiskers from the minimum to the maximum, use s.min() and s.max() instead and say so in the chart title.
Step 2 — Map the numbers onto a stock chart
A stock chart expects four series per category in a fixed order — open, high, low, close — and draws a vertical line from low to high plus a bar between open and close. Feed it the box plot's numbers in that order:
import xlsxwriter
book = xlsxwriter.Workbook("delivery_boxplot.xlsx")
sheet = book.add_worksheet("Box plot")
headers = ["Region", "Q1 (open)", "High whisker (high)", "Low whisker (low)", "Q3 (close)", "Median", "Outliers"]
sheet.write_row(0, 0, headers)
for r, (region, row) in enumerate(stats.iterrows(), start=1):
sheet.write_row(r, 0, [region, row["Q1"], row["HighWhisker"], row["LowWhisker"], row["Q3"],
row["Median"], row["Outliers"]])
n = len(stats)
cats = ["Box plot", 1, 0, n, 0]
Mapping Q1 to "open" and Q3 to "close" makes the up-down bar span exactly the box, and the whisker ends as "high" and "low" make the high-low line span the whiskers. Writing the numbers to the sheet — rather than embedding them in the chart — keeps the chart linked to visible, checkable cells.
Step 3 — Boxes from up-down bars, whiskers from high-low lines
box = book.add_chart({"type": "stock"})
for col in range(1, 5):
box.add_series({"name": ["Box plot", 0, col], "categories": cats,
"values": ["Box plot", 1, col, n, col],
"line": {"none": True}, "marker": {"type": "none"}})
box.set_high_low_lines({"line": {"color": "#334155", "width": 1.25}})
same = {"fill": {"color": "#E0E7FF"}, "border": {"color": "#4338CA"}}
box.set_up_down_bars({"up": same, "down": same})
The four series themselves are invisible — no line, no marker — because only their derived shapes matter. set_high_low_lines draws the whiskers. set_up_down_bars draws a bar from open to close; normally "up" and "down" bars are coloured differently (rising and falling prices), but Q3 is always above Q1, so giving both the same style keeps every box identical.
Step 4 — Add the median and finish the axes
The median is a fifth number that a stock chart has no slot for, so add it as a separate line series with a dash marker and combine it with the stock chart:
median = book.add_chart({"type": "line"})
median.add_series({"name": "Median", "categories": cats, "values": ["Box plot", 1, 5, n, 5],
"line": {"none": True},
"marker": {"type": "long_dash", "size": 16, "fill": {"color": "#4338CA"},
"border": {"color": "#4338CA"}}})
box.combine(median)
box.set_title({"name": "Delivery days by region (whiskers: 1.5 × IQR)"})
box.set_legend({"none": True})
box.set_y_axis({"min": 0, "name": "Days", "major_gridlines": {"visible": True, "line": {"color": "#E5E7EB"}}})
box.set_size({"width": 620, "height": 340})
sheet.insert_chart("A8", box)
The median dash sits across each box. The title states the whisker rule, because readers interpret box plots differently. A fixed axis minimum of zero keeps the boxes comparable; the maximum is left automatic so long whiskers are never cut off. Stock-chart details are in Create a Stock Chart with openpyxl.
Step 5 — List the outliers beside the chart
Points beyond the whiskers are not drawn by this construction, so list them in a small table — which is often more useful than dots anyway, because each one is a delivery someone may want to look up:
outliers = []
for region, s in deliveries.groupby("Region")["Days"]:
q1, q3 = s.quantile([0.25, 0.75])
fence = q3 + 1.5 * (q3 - q1)
outliers += [(region, d) for d in s[s > fence].sort_values(ascending=False)]
sheet.write_row(0, 9, ["Region", "Outlier (days)"])
for r, (region, d) in enumerate(outliers, start=1):
sheet.write_row(r, 9, [region, d])
sheet.set_column("A:A", 10)
sheet.set_column("B:G", 12)
book.close()
print(f"{len(outliers)} outliers:", outliers)
Only high outliers occur here, because delivery times cannot go far below zero; the same loop with s < q1 - 1.5 * iqr finds low ones. Finding outliers in general is covered in Detect Outliers in Excel Data with pandas.
Step 6 — Check the rendered chart
import subprocess
from PIL import Image
subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "delivery_boxplot.xlsx"], check=True, capture_output=True)
subprocess.run(["pdftoppm", "-png", "-r", "60", "-f", "1", "-l", "1", "delivery_boxplot.pdf", "boxplot"], check=True)
counts = {c: n for n, c in Image.open("boxplot-1.png").convert("RGB").getcolors(maxcolors=1_000_000)}
for label, rgb in {"boxes": (0xE0, 0xE7, 0xFF), "medians": (0x43, 0x38, 0xCA), "whiskers": (0x33, 0x41, 0x55)}.items():
print(f"{label:9} {'drawn' if counts.get(rgb, 0) > 20 else 'MISSING'}")
Counting pixels of each colour confirms that the boxes, median dashes and whiskers were all drawn. That check matters for this chart in particular: an earlier construction of the same plot, using stacked columns with custom error bars for the whiskers, rendered boxes without any whiskers in LibreOffice, which does not draw custom error-bar values from these files. The stock-chart construction uses only standard elements and renders the same in both applications.
Performance notes
The statistics are a group-by over the raw data and take milliseconds even for millions of rows; the chart itself only ever holds five numbers per group, so it stays small and fast however much data sits behind it. That is a real advantage over pasting raw data into Excel's own box-plot type, which recalculates from every point. Keep the raw data in its own sheet (or out of the workbook entirely) and write only the summary table the chart needs.
Common pitfalls
- Expecting a native box-plot type. The Excel 2016 box-and-whisker chart cannot be written by openpyxl or XlsxWriter; build it from a stock chart.
- Wrong series order. Stock charts need open, high, low, close in that order; anything else draws the wrong shapes.
- Different up and down colours. Style both identically, since Q3 is always above Q1.
- Unstated whisker rule. Say whether whiskers are min–max or 1.5 × IQR.
- Custom error bars for whiskers. They do not render in every application; high-low lines do.
Conclusion
Compute Q1, median, Q3 and the whisker ends per group with pandas, write them to the sheet as open (Q1), high (upper whisker), low (lower whisker) and close (Q3), and build a stock chart with invisible series: up-down bars draw the boxes and high-low lines the whiskers. Add the median as a line series with dash markers, state the whisker rule in the title, list outliers in a table beside the chart, and render it once to confirm every element appears.
Frequently asked questions
Can openpyxl or XlsxWriter create Excel's box and whisker chart? No. The box-and-whisker type added in Excel 2016 uses a newer chart format that these libraries do not write. A stock chart with up-down bars and high-low lines produces an equivalent, editable box plot.
How do I make a box plot from a stock chart? Use four series in the order open, high, low, close with Q1, upper whisker, lower whisker and Q3. Up-down bars then draw the box from Q1 to Q3 and high-low lines draw the whiskers.
How do I add the median to the box plot? Add the median values as a line series with no line and a long-dash marker, and combine that line chart with the stock chart so the dash sits across each box.
How are box plot whiskers calculated? Commonly with Tukey's rule: each whisker reaches the most extreme data point within 1.5 times the interquartile range from the box. Points beyond are outliers. Some charts use the minimum and maximum instead, so state the rule.
Related
- Up one level: Creating Charts in Excel with openpyxl — native charts from Python.
- Create a Histogram in Excel with Python — another view of a distribution.
- Create a Bullet Chart in Excel with Python — combining series into a custom chart.
- Embed a matplotlib Chart in an Excel Report — box plots with outlier dots as pictures.
More on charts & images
- How-toBuild a Dashboard Sheet with Charts from Multiple Tabs
- How-toBuild a Rolling 12-Month Chart on an Excel Dashboard
- How-toKeep Charts and Images When Filling an Excel Template
- How-toAdd a Chart to an Excel File with xlsxwriter
- How-toAdd Sparklines to an Excel Report with xlsxwriter
- How-toBuild a Waterfall Chart in Excel with Python