You finished a clean groupby. The table is right. Then someone says, “Can you make it visual?” You open a charting library, paste three random snippets from the internet, and ship a rainbow bar chart with truncated axes and a title that says “Chart 1.” Nobody can answer a decision from it, but it looks like analytics happened.
This is Part 11 of Python for analytics, the stretch path after the core series. You already load tables, filter, group, join, clean, and hand off results. Plotting is not a new career. It is a communication layer on top of numbers you already trust. We pick matplotlib (with pandas’ built-in plot helpers) as one stack and stick with it so you build muscle memory instead of tool FOMO.
What you’ll learn
- What an analysis chart is for (explore vs explain)
- Why we pick matplotlib for this series and ignore the rest for now
- Bar and line patterns from pandas groupbys you already know
- Titles, labels, and one annotation that carries the takeaway
- Common chart crimes and a Monday practice loop
A chart is a job, not a decoration
Before you import anything, finish this sentence: This chart helps someone decide whether to ___. If you cannot finish it, you are making wallpaper. Wallpaper is fine for personal exploration. It is a problem when it lands in a leadership deck with no claim.
Analysis charts usually do one of three jobs:
- Compare categories (East vs West revenue)
- Show change over time (weekly orders)
- Show composition carefully (share of total, with a warning about pie overload)
If the job is “impress with color,” stop. Go back to the table from Part 5 on groupby and write one honest sentence first. The chart should make that sentence faster to see, not replace it.
This matches the foundations habit from Analytics foundations: question before output. A plot without a question is a screenshot of your curiosity, not a handoff.
One stack: matplotlib (+ pandas plot)
The internet will try to sell you five charting libraries before breakfast. For this series we choose:
- matplotlib as the drawing engine
- pandas
.plot()as the shortcut from a DataFrame
Why not Plotly or Seaborn today? Both are excellent. Plotly shines for interactive dashboards. Seaborn shines for statistical aesthetics. You can learn them later. Right now you need one path that:
- Installs with a single familiar tool (
pip install matplotlib) - Works offline in a notebook or script
- Exports a PNG you can paste into Slack or a slide
- Does not require a frontend mental model
Stick to that stack for a month of real work. Switching libraries every week is how people never learn titles and axis honesty.
Chart anatomy you can defend
Every serious chart needs a few boring parts. Boring is good. Boring is readable at 7 a.m. on a phone.

| Part | Job | Bad version |
|---|---|---|
| Title | States the claim or comparison | “Chart 1” or “Sales” |
| Axes | Units and scale people can trust | No labels, mystery zeros |
| Encoding | Bars/lines for the comparison | 3D pie with 12 slices |
| Annotation | Points at the takeaway | No callout, reader guesses |
| Source note | Where the data came from | Silent screenshot |
Rule of thumb: If someone cannot retell your chart’s message without the slide notes, the chart failed its job.
From groupby to bar chart
Start from a summary table you already trust. Do not plot raw row chaos if the question is “by region.” Aggregate first (Part 5), then plot.
import pandas as pd
import matplotlib.pyplot as plt
sales = pd.DataFrame({
"region": ["East", "West", "East", "West", "East", "North"],
"amount": [42.5, 18.0, 91.25, 33.0, 12.0, 55.0],
"order_date": pd.to_datetime([
"2026-01-03", "2026-01-03", "2026-01-04",
"2026-01-04", "2026-01-05", "2026-01-05",
]),
})
by_region = (
sales.groupby("region", as_index=False)["amount"]
.sum()
.sort_values("amount", ascending=False)
)
print(by_region)
ax = by_region.plot(
kind="bar",
x="region",
y="amount",
legend=False,
color="#c2410c",
figsize=(8, 4.5),
)
ax.set_title("East leads total sales in this sample week")
ax.set_xlabel("Region")
ax.set_ylabel("Sales amount (USD)")
ax.tick_params(axis="x", rotation=0)
plt.tight_layout()
plt.savefig("sales_by_region.png", dpi=160)
plt.show()What that code draws:

Notice what we did on purpose:
- Aggregated before plotting
- Sorted so the eye lands on the leader
- Wrote a title that claims something, not “Bar chart of amount”
- Labeled axes with units
- Saved a PNG for handoff (Part 9 habits still apply)
Lines for time, not for categories
Lines imply continuity. Use them when the x-axis is ordered time (or another ordered sequence). Do not connect “East, West, North” with a line. That invents a path that does not exist.
daily = (
sales.groupby("order_date", as_index=False)["amount"]
.sum()
.sort_values("order_date")
)
ax = daily.plot(
kind="line",
x="order_date",
y="amount",
marker="o",
legend=False,
color="#1e40af",
figsize=(8, 4.5),
)
ax.set_title("Daily sales amount, sample week")
ax.set_xlabel("Order date")
ax.set_ylabel("Sales amount (USD)")
plt.tight_layout()
plt.savefig("sales_daily.png", dpi=160)
plt.show()What that code draws:

If your time series has missing days, decide whether to show gaps or fill zeros. Silence is a choice. A smooth line across missing dates can lie by implication.
A little more control with matplotlib objects
pandas .plot() is great until you need a callout. Then step one level down to the axes object (you already get ax back from many plot calls).
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.bar(by_region["region"], by_region["amount"], color="#c2410c")
ax.set_title("East leads total sales in this sample week")
ax.set_xlabel("Region")
ax.set_ylabel("Sales amount (USD)")
# Call out the top bar
top = by_region.iloc[0]
ax.annotate(
f"Leader: {top['region']}",
xy=(0, top["amount"]),
xytext=(0.4, top["amount"] * 0.85),
arrowprops=dict(arrowstyle="->", color="#292524"),
fontsize=10,
)
fig.tight_layout()
fig.savefig("sales_by_region_annotated.png", dpi=160)
plt.show()What that code draws:

One annotation beats five. If you need five callouts, you probably need two charts or a table.
Honesty checklist (axes and baselines)
Visual example for this checklist: same scores, two scales.

Charts can be technically correct and still misleading. A few defaults keep you out of trouble:
| Temptation | Risk | Prefer |
|---|---|---|
| Start bar axis above zero | Exaggerates small gaps | Zero baseline for bar length comparisons |
| Dual axes for unrelated metrics | Fake correlation vibes | Two charts or indexed series with a note |
| Too many categories | Spaghetti | Top N + “Other,” or a table |
| Rainbow default colors | Hard to read, not colorblind-safe | One strong color + gray for context |
| 3D effects | Perspective lies | Flat 2D always |
This is the same spirit as reading numbers like an adult: rates need denominators, charts need fair scales. If leadership only sees the picture, the picture must not smuggle a conclusion the table does not support.
Explore mode vs explain mode
In a notebook, you will make ugly exploratory charts. That is fine. Mark them as exploration. When you export for others:
- Remove grid clutter you do not need
- Fix the title to a claim
- Set figure size for slides or docs
- Save at enough DPI that text stays sharp (160 is a decent start; go higher for print)
- Keep the underlying CSV or query note nearby (Part 9)
Exploratory mess that ships to executives becomes “the dashboard lied.” Prevent that with a two-folder brain: scratch/ and share/.
Worked example: weekly region story
What that code draws:

Suppose your question is: Which region should get the next support hire if we staff by recent sales volume? You are not making art. You are ranking volume with context.
import pandas as pd
import matplotlib.pyplot as plt
# Pretend this is the output of a clean pipeline (Parts 7 and 8)
weekly = pd.DataFrame({
"region": ["East", "West", "North", "South"],
"orders": [120, 95, 40, 22],
"sales": [18400, 15100, 6200, 3100],
})
weekly = weekly.sort_values("sales", ascending=True) # horizontal bars read well
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.barh(weekly["region"], weekly["sales"], color="#0f766e")
ax.set_title("Sales by region: East and West dominate this week")
ax.set_xlabel("Sales (USD)")
ax.set_ylabel("")
for i, row in weekly.reset_index(drop=True).iterrows():
ax.text(row["sales"] + 200, i, f"{int(row['orders'])} orders", va="center", fontsize=9)
fig.tight_layout()
fig.savefig("region_support_context.png", dpi=160)
plt.show()
print("Share-ready sentence:")
print(
"East and West produced most sales this week; "
"South volume is small even if percent growth looks loud."
)The labels on bars add order counts so sales alone does not hide workload. That is analysis plotting: extra context that answers the decision, not chart junk.
Common mistakes
- Plotting before aggregating. You get a hairball of points nobody asked for.
- Default titles. Rename until a stranger understands the point.
- Pie charts for 8+ slices. Use bars or a table.
- Comparing rates with unlabeled counts. A 50% region with 2 customers is not a strategy.
- Copying Plotly snippets into a matplotlib mental model. Pick one stack this month.
- Forgetting to save the figure and only “seeing” it in a live notebook session.
- Using color as the only encoding without position or labels (accessibility and print fail).
How to practice Monday morning
- Take one trusted summary from last week’s work (CSV is fine).
- Write the decision sentence first.
- Make one bar or one line chart in matplotlib via pandas.
- Add title, axis labels, and at most one annotation.
- Export PNG + keep the table beside it.
- Ask a teammate: “What decision would you make from this alone?” Fix what they miss.
When you are ready to package exploration vs production style, continue to Part 12: Notebooks vs scripts. Charts live in both worlds; the difference is how repeatable the path is.
Quick recap
- Charts answer decisions; decoration is optional and often harmful.
- This series sticks to matplotlib + pandas plotting for muscle memory.
- Aggregate first; bars for categories; lines for ordered time.
- Title, axes, one annotation, honest baseline.
- Export for handoff; keep the table of truth nearby.
Sources
Research and further reading used for this article:
- matplotlib: Quick start guide (figures, axes, savefig)
- pandas: Chart visualization (DataFrame.plot helpers)
- matplotlib annotate (callouts on axes)
- Analytics Made Simple: Python for analytics (series home)
- Analytics Made Simple: Learn (related paths on this site)
