
How to Make a Grouped Bar Chart for a Research Paper (Matplotlib Code + CSV Shortcut)
A complete, copy-paste matplotlib recipe for publication-quality grouped bar charts: long-format CSV, error bars, colorblind-safe colors, column-width sizing, embedded fonts, and fixes for the most common layout problems.
A grouped bar chart (also called a clustered bar chart) is the standard way to compare several methods across several datasets or settings: one group per dataset, one bar per method. It shows up in almost every experiments section, and it is also one of the easiest figures to get subtly wrong. Labels collide, error bars hide the numbers, the legend covers a bar, or the PDF fails the venue's font check.
This guide gives you a complete matplotlib script you can paste and run, explains the parts that matter for a paper, and lists the fixes for the problems you are most likely to hit. At the end there is a shortcut if you would rather start from a CSV and a one-sentence description.
When a grouped bar chart is the right choice
Use a grouped bar chart when:
- You compare a small number of methods (usually 2–5) across a small number of categories (datasets, tasks, model sizes).
- The categories are discrete. If the x-axis is continuous, such as training steps or noise level, a line chart is clearer.
- Readers care about relative differences within each group more than the exact numbers. If exact numbers matter most, a table may be better, or you can add value labels as shown below.
If you have more than about 5 bars per group or more than about 6 groups, consider splitting the figure, switching to a horizontal layout, or using a table.
Step 1: Put your data in long format
Keep one row per measurement. This "long" format is easy to export from experiment logs and easy to reshape:
method,dataset,accuracy,std
Baseline,CIFAR-10,91.2,0.3
Ours,CIFAR-10,94.7,0.2
Ours+Aug,CIFAR-10,95.3,0.2
Baseline,CIFAR-100,68.4,0.5
Ours,CIFAR-100,74.9,0.4
Ours+Aug,CIFAR-100,76.8,0.3
Baseline,Tiny-ImageNet,55.1,0.6
Ours,Tiny-ImageNet,61.8,0.5
Ours+Aug,Tiny-ImageNet,63.5,0.4The numbers above are example data for this tutorial, not real benchmark results.
Step 2: The complete matplotlib script
This script was tested with matplotlib 3.11 and pandas. It writes both a vector PDF for LaTeX and a 300 DPI PNG.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
df = pd.read_csv("results.csv")
datasets = ["CIFAR-10", "CIFAR-100", "Tiny-ImageNet"] # x-axis order
methods = ["Baseline", "Ours", "Ours+Aug"] # bar order in each group
mean = df.pivot(index="dataset", columns="method", values="accuracy").loc[datasets, methods]
std = df.pivot(index="dataset", columns="method", values="std").loc[datasets, methods]
plt.rcParams.update({
"font.family": "serif",
"font.size": 9,
"axes.spines.top": False,
"axes.spines.right": False,
"pdf.fonttype": 42, # embed TrueType fonts (many venues require this)
"ps.fonttype": 42,
})
colors = ["#0072B2", "#E69F00", "#009E73"] # Okabe-Ito, colorblind-safe
hatches = ["", "//", "xx"] # still readable in grayscale
x = np.arange(len(datasets))
n = len(methods)
width = 0.8 / n
fig, ax = plt.subplots(figsize=(3.5, 2.6)) # single-column width in inches
for i, method in enumerate(methods):
offset = (i - (n - 1) / 2) * width
ax.bar(
x + offset, mean[method], width,
yerr=std[method], capsize=2, error_kw={"elinewidth": 0.8},
label=method, color=colors[i], hatch=hatches[i],
edgecolor="black", linewidth=0.5,
)
# Put value labels above the error bar, not on top of it.
for xi, m, s in zip(x + offset, mean[method], std[method]):
ax.text(xi, m + s + 1.5, f"{m:.1f}", ha="center", va="bottom", fontsize=5.5)
ax.set_xticks(x, ["CIFAR-10", "CIFAR-100", "Tiny-\nImageNet"]) # wrap long names
ax.set_ylabel("Top-1 accuracy (%)")
ax.set_ylim(0, 105)
ax.yaxis.grid(True, linestyle=":", linewidth=0.5)
ax.set_axisbelow(True)
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.24), ncol=n, frameon=False)
fig.tight_layout()
fig.savefig("grouped_bar.pdf", bbox_inches="tight") # vector, for LaTeX
fig.savefig("grouped_bar.png", dpi=300, bbox_inches="tight") # raster, 300 DPIThis is the output, rendered at 300 DPI:

What each part is doing
The offset formula. Each method's bars are shifted by (i - (n - 1) / 2) * width, which centers the group on the tick no matter how many methods you have. With width = 0.8 / n, the bars in one group take up 80% of the space between ticks, leaving a visible gap between groups.
Explicit ordering. pivot(...).loc[datasets, methods] fixes the order of groups and bars. Without it, pandas sorts alphabetically, which usually puts your own method in the wrong place.
Colors and hatches. The three colors come from the Okabe–Ito palette, which stays distinguishable for the most common forms of color vision deficiency. The hatches keep the bars distinguishable when the paper is printed in grayscale.
Size in inches, not pixels. figsize=(3.5, 2.6) matches a single column in most two-column templates (roughly 3.3–3.5 inches wide). Designing at the final size means a 9 pt font in matplotlib appears as 9 pt in the PDF, close to the body text. For a full-width figure, use about 7 inches.
Embedded fonts. pdf.fonttype: 42 makes matplotlib embed TrueType fonts instead of Type 3 fonts. Submission checkers for many conferences flag Type 3 fonts, and this setting avoids that problem.
Vector first. Include the PDF in LaTeX with \includegraphics[width=\columnwidth]{grouped_bar.pdf}. Keep the 300 DPI PNG for slides, Word, or journals that ask for raster images.
Common problems and how to fix them
Here is the first version of the same script, before the layout fixes:

Three problems are visible, and they are the ones people hit most often:
- Overlapping x-axis labels. At single-column width, long category names run into each other. Wrap them with
\n, shorten them (and explain the abbreviation in the caption), or rotate them withax.set_xticks(x, labels, rotation=20, ha="right"). - Value labels covering the error bars.
ax.bar_labelplaces text at the top of the bar, which is exactly where the error bar starts. Place labels atmean + stdplus a small margin, as the final script does, or drop value labels and report the numbers in a table. - Crowded numbers. If neighboring labels touch, reduce their font size, use one decimal place, or label only the best result in each group.
Other issues worth checking:
- The legend covers a bar. Move it below the axis with
bbox_to_anchor, as above, or place it above the plot withloc="lower center", bbox_to_anchor=(0.5, 1.0). - The y-axis does not start at zero. Bar length encodes the value, so a truncated axis exaggerates differences. If the differences are small and important, show them with a dot plot or a table instead of cutting the axis.
- The error bars are unexplained. State in the caption whether they show standard deviation, standard error, or a 95% confidence interval, and how many runs they are based on.
- Blurry figures in the PDF. This usually means a low-resolution PNG was scaled up. Use the vector PDF, or export the PNG at the final printed size and at least 300 DPI.
Pre-submission checklist
- Group and bar order is intentional (your method is easy to find).
- Colors are colorblind-safe, and the bars are still distinguishable in grayscale.
- The font size in the figure is close to the caption's font size at the final printed width.
- Axis labels include units, and the y-axis starts at zero.
- The caption says what the error bars mean and how many runs were used.
- Fonts are embedded (no Type 3 fonts in the PDF).
- The same method uses the same color in every figure in the paper.
Prefer seaborn?
If your data is already in long format, seaborn can draw the same grouping in one call:
import seaborn as sns
ax = sns.barplot(data=df, x="dataset", y="accuracy", hue="method",
palette=["#0072B2", "#E69F00", "#009E73"], errorbar=None)With one row per measurement and no repeated runs, set errorbar=None. If your CSV contains one row per run, seaborn can compute the error bars itself (for example, errorbar="sd"). You will still want the sizing, font and export settings from the script above.
Shortcut: from CSV to figure with PaperBanana
If you would rather not write and debug the plotting code yourself, the PaperBanana Paper Visualizer has a Data Plot mode built for this:
- Paste your CSV or JSON data.
- Describe the figure in one sentence, for example: "Grouped bar chart of accuracy by dataset, one bar per method, value labels on top, legend below the x-axis."
- PaperBanana writes matplotlib code, runs it in an isolated sandbox, renders the figure at 300 DPI, and has a critic review the result over one or more rounds.
You can download the image and copy the generated code. That means you can rerun the code locally, export a vector PDF, and adjust anything you like. This is the actual output for the same data used in this guide:

Data Plot mode does not use an image model, so the numbers in the figure come straight from your data. It costs 10 credits per critique round, and rounds that are not needed are refunded automatically. See pricing for details.
FAQ
Should I use a grouped bar chart or a table? Use a chart when the main message is a pattern, such as "our method wins on every dataset" or "the gap grows on harder datasets". Use a table when readers need exact numbers. Many papers do both: a chart in the main text and a full table in the appendix.
How many bars per group is too many? More than about 5 bars per group becomes hard to read at column width. Split the comparison into two figures, or move the less important baselines to the appendix.
Standard deviation or standard error? Either is acceptable if you say which one you used. Standard deviation shows the spread across runs. Standard error and confidence intervals show how precisely the mean is estimated. Always report the number of runs.
Can I make the bars horizontal?
Yes. Replace ax.bar with ax.barh, swap the roles of the x and y axes, and use ax.set_yticks. Horizontal bars work well when category names are long.
For general guidance on paper figures, see How to Create Publication-Quality Figures for Your Research Paper and our comparison of TikZ, matplotlib and AI tools.
Author

Categories
More Posts

TikZ vs Matplotlib vs AI: The Best Tools for Academic Paper Figures in 2026
An honest comparison of tools for creating research paper figures. We compare TikZ, matplotlib, draw.io, Figma, BioRender, and AI generators like PaperBanana across learning curve, output quality, and workflow speed.


PaperBanana is back: GPT Image 2.6, Nano Banana 2.1 and plot mode
PaperBanana returns on November 10, 2026 with two image models, 2K output, isolated 300 DPI plotting, and per-round billing with automatic unused-round refunds.


PaperBanana: Completing the Final Piece of the AI Scientist Puzzle
In the wave of AI reshaping scientific research, PaperBanana fills the visual gap in automated research workflows. Deep dive into the agentic framework, benchmark evaluation, and future implications for fully autonomous AI scientists.

Stay Updated
Join the PaperBanana community
Get notified about new features, research tips, and early access opportunities