Pandas Bar Charts: Compare Categories, Groups and Sales

Use DataFrame.plot.bar() to compare numeric values across categories. Start with student marks, then build sales reports with grouped, stacked and percentage bars. Learn how to choose colours, label values and save a chart you can use in a report.

Python Pandas Plot horizontal or vertical Bar graph by using DataFrame with options & save as image

Watch the demonstration, then run the examples in the notebook. The student and sales datasets are synthetic and included in the code.

Create Your First Bar Chart

A bar chart compares values across categories. Here each student name is a category and MARK is the numeric value. Select x="NAME" and y="MARK" explicitly so an unrelated numeric ID column would not become another plotted series. Pandas returns a Matplotlib Axes that you can use to adjust labels or save the figure.

import pandas as pd
import matplotlib.pyplot as plt

def student_data():
    return pd.DataFrame({
        "NAME": ["Ravi", "Raju", "Alex", "Ron", "Geek", "Kim"],
        "MARK": [20, 30, 40, 30, 40, 50],
        "ENGLISH": [30, 20, 30, 40, 20, 40]
    })

df = student_data()
ax = df.plot.bar(x="NAME", y="MARK", title="Student marks",
                 figsize=(6, 3), rot=0, legend=False)
ax.set_ylabel("Marks")
plt.tight_layout()
plt.show()

What to check: Six bars: Ravi 20, Raju 30, Alex 40, Ron 30, Geek 40 and Kim 50.

pandas plot bar

Titles, Figure Size and x/y Selection

Use title for a meaningful heading and figsize=(width, height) for dimensions in inches. x supplies category labels; y selects numeric columns. Make the figure wider when labels would otherwise overlap. The DataFrame tutorial explains the table structure; Python tuples supply the size pair.

df = student_data()
ax = df.plot.bar(x="NAME", y="MARK", title="Student marks by name",
                 figsize=(6, 3), rot=0, legend=False)
ax.set_xlabel("Student")
ax.set_ylabel("Marks")
plt.tight_layout()
plt.show()

use_index: Choose Index Labels or Positions

Without x, the index supplies category labels. Set the names as the index to compare use_index=True and use_index=False. The second chart labels positions 0 through 5. If you want a particular column as the labels, use x explicitly.

df = student_data().set_index("NAME")
fig, axes = plt.subplots(1, 2, figsize=(10, 3))
df.plot.bar(y="MARK", use_index=True, ax=axes[0],
            title="Index labels", rot=0, legend=False)
df.plot.bar(y="MARK", use_index=False, ax=axes[1],
            title="Row positions", rot=0, legend=False)
plt.tight_layout()
plt.show()
Bar chart: use_index true
Bar chart: use_index false

Add a Useful Grid

A light horizontal grid helps readers estimate values. Keep the bars in front of the grid. grid=True enables a grid through the plotting options; the Axes methods give more control over direction and appearance.

df = student_data()
ax = df.plot.bar(x="NAME", y="MARK", grid=False,
                 legend=False, rot=0, figsize=(6, 3))
ax.yaxis.grid(True, alpha=0.25)
ax.set_axisbelow(True)
ax.set_ylabel("Marks")
plt.tight_layout()
plt.show()
Bar chart: grid true
Bar chart: grid false

Show a Legend When It Explains the Series

One series already named on the axis may not need a legend. Multiple series need clear labels so readers can identify them. legend=False hides the legend; legend=True shows it.

df = student_data()
ax = df.plot.bar(x="NAME", y=["MARK", "ENGLISH"], legend=True,
                 rot=0, figsize=(7, 3), title="Two subject scores")
ax.set_ylabel("Marks")
plt.tight_layout()
plt.show()
Bar chart: legend true
Bar chart: legend false

Set One Colour Using RGB or a Named Colour

An RGB tuple contains red, green and blue components between 0 and 1. A named colour or hexadecimal string also works. This one-item list assigns the same colour to the MARK series. Use colours consistently across related charts.

df = student_data()
my_colors = [(0.9, 0.4, 0.6)]
ax = df.plot.bar(x="NAME", y="MARK", color=my_colors,
                 figsize=(5, 3), legend=False, rot=0,
                 title="Student marks")
plt.tight_layout()
plt.show()
Bar chart: color1

Assign Different Colours to Different Series

For DataFrame.plot.bar(), a colour list maps to the plotted columns. With only one numeric series, only the first list colour is used. Plot two subjects to use both RGB colours. A dictionary keyed by column name can make that mapping explicit.

df = student_data()
my_colors = [(0.9, 0.4, 0.6), (0.1, 0.3, 0.8)]
ax = df.plot.bar(x="NAME", y=["MARK", "ENGLISH"], color=my_colors,
                 rot=0, figsize=(7, 3), title="Colours by subject")
plt.tight_layout()
plt.show()
Bar chart: color2

Colour Individual Bars in One Series

Use a Series bar plot when you want a colour for each category. The colour list below has one RGB tuple per student. Different colours can highlight categories, but a single colour is often clearer when colour does not encode additional information.

df = student_data()
my_colors = [(position / 10.0, position / 20.0, 0.9)
             for position in range(len(df))]
ax = df.set_index("NAME")["MARK"].plot.bar(
    color=my_colors, rot=0, figsize=(6, 3), title="Colours by student")
ax.set_ylabel("Marks")
plt.tight_layout()
plt.show()
Bar chart: color3

Rotate Labels or Use a Horizontal Chart

rot controls tick-label rotation. Try 45 degrees for longer names; 180 degrees makes labels upside down and is rarely readable. Keep 0 degrees for short labels. If names are long, horizontal bars often work better.

df = student_data()
ax = df.plot.bar(x="NAME", y="MARK", rot=45,
                 figsize=(6, 3), legend=False, title="Rotated labels")
plt.tight_layout()
plt.show()
Bar chart: rot 45
Bar chart: rot 180

Compare Subjects with Grouped Bars

Grouped bars place series side by side within each category. This makes MARK and ENGLISH easier to compare for each student. Both measurements use the same units, so one y-axis works. Choose the numeric columns explicitly.

df = student_data()
ax = df.plot.bar(x="NAME", y=["MARK", "ENGLISH"], stacked=False,
                 color={"MARK": "#2563a6", "ENGLISH": "#df8b22"},
                 rot=0, figsize=(7, 3), title="Grouped subject scores")
ax.set_ylabel("Marks")
plt.tight_layout()
plt.show()
Bar chart: stacked false

Use Stacked Bars for Parts of a Meaningful Total

stacked=True places series on top of each other. It emphasises totals and composition, while making individual segments harder to compare. Stack values only when adding them answers a meaningful question. Here the total is the combined score across two subjects.

df = student_data()
ax = df.plot.bar(x="NAME", y=["MARK", "ENGLISH"], stacked=True,
                 figsize=(7, 3), rot=0, title="Combined subject scores")
ax.set_ylabel("Combined marks")
plt.tight_layout()
plt.show()

What to check: Kim has a combined score of 90; Geek has 60.

Bar chart: stacked true

Separate Series into Subplots

subplots=True creates a separate chart for each numeric series. A list of titles supplies one title per subplot. This is useful when a shared chart would be crowded. Shared y-axis limits help readers compare subjects with the same units.

df = student_data()
axes = df.plot.bar(x="NAME", y=["MARK", "ENGLISH"], subplots=True,
                   title=["MARK scores", "ENGLISH scores"],
                   layout=(1, 2), sharey=True, rot=0, legend=False,
                   figsize=(10, 3))
plt.tight_layout()
plt.show()

Logarithmic Scales: Use Only for a Clear Reason

logy=True makes the numeric axis logarithmic. This can show multiplicative differences when positive values span orders of magnitude. Zero and negative values do not work on a standard log scale. Because bar lengths normally imply a zero baseline, a log-scale line or point chart is often easier to interpret. logx and loglog are plotting options, but they are generally unsuitable for a categorical bar axis.

df = student_data()
ax = df.plot.bar(x="NAME", y="MARK", logy=True,
                 rot=0, legend=False, title="Marks on a log scale")
ax.set_ylabel("Marks (log scale)")
plt.tight_layout()
plt.show()

# A line plot is another way to demonstrate the numeric log scale.
ax = df.plot.line(x="NAME", y="MARK", logy=True,
                  legend=False, title="Log-scale comparison")
plt.tight_layout()
plt.show()
Bar chart: log

secondary_y: Plot a Series on the Right Axis

A secondary axis uses a different scale. Moving all series to it with secondary_y=True is possible, but placing one identified series on the right is usually more useful. Dual axes can make unrelated changes appear comparable; separate charts are often clearer. Always label both units.

df = pd.DataFrame({"category": ["Stationery", "Office", "Books"],
                   "revenue": [450, 900, 300], "orders": [9, 6, 12]})
ax = df.plot.bar(x="category", y=["revenue", "orders"],
                 secondary_y="orders", rot=0, figsize=(7, 3),
                 title="Revenue and recorded orders")
ax.set_ylabel("Revenue (sample currency units)")
ax.right_ax.set_ylabel("Recorded orders")
plt.tight_layout()
plt.show()
Bar chart: secondary_y true
Bar chart: secondary_y false

mark_right: Identify the Secondary-Axis Series

By default the legend adds (right) to a secondary-axis series. mark_right=False removes that suffix, so make the axis association clear another way. The first example below moves MARK to the right axis; the second hides the suffix and names the axis explicitly.

df = student_data()
ax = df.plot.bar(x="NAME", y="MARK", secondary_y=True,
                 mark_right=True, rot=0, figsize=(6, 3),
                 title="MARK on the right axis")
plt.tight_layout()
plt.show()

ax = df.plot.bar(x="NAME", y="MARK", secondary_y=True,
                 mark_right=False, rot=0, figsize=(6, 3),
                 title="MARK: right-axis scale")
ax.set_ylabel("MARK (right axis)")
plt.tight_layout()
plt.show()
Bar chart: mark_right true
Bar chart: mark_right false

Practical Sales Data: Aggregate Before Plotting

Repeated categories need a summary before plotting: Pandas draws bars for rows, not automatically for unique categories. This synthetic dataset contains recorded revenue by product category and channel. All combinations are observed; the revenue values are non-negative and have the same units.

def sales_data():
    return pd.DataFrame({
        "category": ["Stationery", "Office", "Books", "Stationery", "Office", "Books"],
        "channel": ["Online", "Online", "Online", "Store", "Store", "Store"],
        "revenue": [120, 300, 180, 80, 250, 220]
    })

sales = sales_data()
totals = sales.groupby("category")["revenue"].sum().sort_values(ascending=False)
print(totals)
assert totals.sum() == sales["revenue"].sum()
assert totals.to_dict() == {"Office": 550, "Books": 400, "Stationery": 200}

Expected output

category
Office        550
Books         400
Stationery    200
Name: revenue, dtype: int64

Plot Sorted Category Totals with Value Labels

Use groupby() to calculate category totals before drawing the chart. Sorting makes the ranking easier to read. Value labels show the exact totals; keep a zero baseline for this non-negative bar chart so bar lengths remain comparable.

sales = sales_data()
totals = sales.groupby("category")["revenue"].sum().sort_values(ascending=False)
ax = totals.plot.bar(color="#2563a6", rot=0, figsize=(7, 4),
                      title="Recorded revenue by category")
ax.set_xlabel("Product category")
ax.set_ylabel("Revenue (sample currency units)")
ax.set_ylim(0, totals.max() * 1.18)
ax.bar_label(ax.containers[0], fmt="%.0f", padding=3)
ax.yaxis.grid(True, alpha=0.2)
ax.set_axisbelow(True)
plt.tight_layout()
plt.show()
Plot Sorted Category Totals with Value Labels
Office 550, Books 400 and Stationery 200; total recorded revenue is 1,150.

What to check: Office 550, Books 400 and Stationery 200; total recorded revenue is 1,150.

Compare Sales Channels with Grouped Bars

Build a category-by-channel table with pivot_table(). Each channel becomes a numeric series. Use a stable colour for each channel. If a real dataset has absent combinations, decide whether they mean zero sales or missing records before filling them.

sales = sales_data()
report = sales.pivot_table(index="category", columns="channel",
                           values="revenue", aggfunc="sum")
print(report)
ax = report.plot.bar(stacked=False, rot=0, figsize=(7, 4),
                      color={"Online": "#2563a6", "Store": "#df8b22"},
                      title="Recorded revenue by channel")
ax.set_ylabel("Revenue (sample currency units)")
ax.legend(title="Sales channel")
plt.tight_layout()
plt.show()

Expected output

channel     Online  Store
category                 
Books          180    220
Office         300    250
Stationery     120     80
Compare Sales Channels with Grouped Bars
For Books, Store revenue is 220 and Online revenue is 180.

What to check: For Books, Store revenue is 220 and Online revenue is 180.

Show Channel Contributions with Stacked Bars

Stacking makes category totals visible while retaining channel contributions. These series represent parts of the same recorded revenue total, so adding them is meaningful. Use grouped bars instead when comparing Online values with Store values is the main task.

sales = sales_data()
report = sales.pivot_table(index="category", columns="channel",
                           values="revenue", aggfunc="sum")
ax = report.plot.bar(stacked=True, rot=0, figsize=(7, 4),
                      color={"Online": "#2563a6", "Store": "#df8b22"},
                      title="Channels contributing to category revenue")
ax.set_ylabel("Revenue (sample currency units)")
ax.legend(title="Sales channel")
plt.tight_layout()
plt.show()
assert report.sum().sum() == sales["revenue"].sum()
Show Channel Contributions with Stacked Bars
Stack heights are Books 400, Office 550 and Stationery 200.

What to check: Stack heights are Books 400, Office 550 and Stationery 200.

Compare Composition with 100% Stacked Bars

Divide each category row by its total and multiply by 100. This compares channel shares rather than revenue amounts. A category with no positive total needs separate handling. A 100% chart hides total size, so present the category-total chart alongside it when amounts matter.

sales = sales_data()
report = sales.pivot_table(index="category", columns="channel",
                           values="revenue", aggfunc="sum")
totals = report.sum(axis=1)
assert totals.gt(0).all()
percentages = report.div(totals, axis=0).mul(100)
print(percentages.round(1))
ax = percentages.plot.bar(stacked=True, rot=0, figsize=(7, 4),
                           color={"Online": "#2563a6", "Store": "#df8b22"},
                           title="Share of category revenue by channel")
ax.set_ylim(0, 100)
ax.set_ylabel("Share of recorded revenue (%)")
plt.tight_layout()
plt.show()
assert percentages.sum(axis=1).round(6).eq(100).all()

Expected output

channel     Online  Store
category                 
Books         45.0   55.0
Office        54.5   45.5
Stationery    60.0   40.0
Compare Composition with 100% Stacked Bars
Stationery is 60% Online and 40% Store; every bar totals 100%.

What to check: Stationery is 60% Online and 40% Store; every bar totals 100%.

Save the Chart as a PNG Image

Save through the returned Axes figure, before closing it. dpi controls raster resolution and bbox_inches="tight" helps include labels. In Colab the file is created in the runtime, not automatically in your Drive: download it from the Files sidebar. See the Colab guide for working with notebook files.

sales = sales_data()
totals = sales.groupby("category")["revenue"].sum().sort_values(ascending=False)
ax = totals.plot.bar(rot=0, color="#2563a6", figsize=(7, 4),
                      title="Recorded revenue by category")
ax.set_ylabel("Revenue (sample currency units)")
plt.tight_layout()
ax.figure.savefig("pandas_category_sales.png", dpi=150, bbox_inches="tight")
plt.show()
print("Saved pandas_category_sales.png")

Expected output

Saved pandas_category_sales.png

Choose a Chart That Answers the Question

Use vertical bars for a small number of categories and horizontal bars for long names. Use line plots for an ordered trend, histograms for a numeric distribution and scatter plots for relationships between two numeric variables. Grouped bars compare series; stacked bars explain a meaningful total; 100% stacked bars compare proportions. Do not mix different units in one stack.

Common Bar-Chart Problems

No numeric data to plot: check the selected column dtype and validate text-to-number conversion before plotting. Repeated category labels: aggregate first; use value_counts() when you need counts. Only one list colour appears: DataFrame colour lists map to series; use the Series example for individual bars. Labels overlap: increase figure width, rotate labels or choose horizontal bars. Blank saved image: save the Axes figure before closing it. Missing observations: investigate them before applying fillna(); unknown values are not automatically zero. Misleading comparisons: use clear units, a suitable baseline and consistent category order.

Exercise: Chart Total Revenue by Channel

Calculate total recorded revenue for Online and Store, draw a bar chart and label both values. Predict the totals before running the solution. Verify that the two bars add up to the original revenue sum. Expected totals: Online 600 and Store 550.

Exercise Solution

Group by channel, sum the recorded revenue and display the exact values above the bars. The assertion verifies that the aggregation did not lose any of the six source records.

sales = sales_data()
totals = sales.groupby("channel")["revenue"].sum()
print(totals)
assert totals.to_dict() == {"Online": 600, "Store": 550}
assert totals.sum() == sales["revenue"].sum()
ax = totals.plot.bar(rot=0, color="#2563a6", figsize=(6, 4),
                      title="Recorded revenue by channel")
ax.set_ylabel("Revenue (sample currency units)")
ax.set_ylim(0, totals.max() * 1.18)
ax.bar_label(ax.containers[0], fmt="%.0f", padding=3)
plt.tight_layout()
plt.show()

Expected output

channel
Online    600
Store     550
Name: revenue, dtype: int64

Practice in Google Colab

Open in Google Colab View on GitHub
Run the examples, change category values and practise choosing grouped or stacked bars. Both sample datasets are included in the notebook. Save a copy in Drive to keep your changes.

Continue with the Pandas Plot guide and plotting exercises. Explore pie, box, density, area and hexbin plots for other questions.

References: Pandas bar-plot documentation and Matplotlib bar labels.




Subscribe to our YouTube Channel here



plus2net.com







Python Video Tutorials
Python SQLite Video Tutorials
Python MySQL Video Tutorials
Python Tkinter Video Tutorials
✖
We use cookies to improve your browsing experience. . Learn more
HTML MySQL PHP JavaScript ASP Photoshop Articles Contact us
© 2000-2026 plus2net.com All rights reserved worldwide Privacy Policy Disclaimer