learn-python-data-analysis-with-phoebe / Session 6 of 8
Learn Python Data Analysis with Phoebe · Session 6 of 8

Matplotlib fundamentals

You've cleaned the frame and grouped it into answers. Now you make it visible. Matplotlib is the engine under every Python chart - seaborn and pandas .plot both sit on it - so we learn its anatomy once, properly. By the end you'll hand-build a line, bar, histogram, and scatter of the real job market, fully labeled and saved at publication quality.

🟡 Moderate Builders Figure and Axes 45 min live + self-study
0-3 · Welcome 3-18 · Concepts 18-40 · Build-along demos 40-45 · Q&A
Part 0

A chart is an argument

A chart isn't decoration - it's an argument made with ink. "Data roles pay more with seniority." "Postings peaked in spring." A good chart makes that claim in a second and lets the reader check it. The tool that draws it in Python is matplotlib, and here's the thing most people never learn: it's the engine under everything. seaborn calls it. pandas .plot() calls it. Every chart you've ever seen in a Python notebook came out of matplotlib. Learn its anatomy once and every other plotting library becomes a shortcut you understand, not magic you fear.

Live - presented in session Self-study - read after class ★ Build-along demo The project: the data job market
★ What you walk out with today Four hand-built charts of your clean job-market frame - a bar of the top roles, a histogram of salaries, a location bar and a salary distribution side by side - each with a title, axis labels with units, and saved as a 300 dpi PNG for slides plus an SVG that stays sharp anywhere. Real charts you could drop into the Session 8 report.
Part 1 · covers the pyplot vs object-oriented interface

The Figure and Axes anatomy 5 min live

The #1 matplotlib confusion is that there are two ways to draw the same chart. Clear it up now and the docs stop feeling contradictory.

Figure (the whole canvas) Axes (one plot) Postings by role legend Title Legend y Axis + ylabel (count, units) Spines (the frame) x Axis + xlabel (role, ticks) One Figure holds one or more Axes; every label you add hangs off an Axes.
🔍 Click to zoom - a Figure is the canvas; an Axes is one plot with its title, x/y labels, ticks, legend, and spines
LiveTwo interfaces, one picture3 min

Matplotlib gives you two ways in, and mixing them is where beginners get lost:

  • pyplot (stateful) - plt.plot(...), plt.title(...). There's a hidden "current" figure and every command draws onto it. Quick for a one-off throwaway chart, clumsy the moment you want two plots.
  • Object-oriented (explicit) - fig, ax = plt.subplots() hands you the Figure and the Axes as real objects. You call ax.bar(...), ax.set_title(...) on the exact plot you mean. This is what pros use and what every serious codebase looks like.

The anatomy above is the whole vocabulary: a Figure is the canvas, an Axes is a single plot living on it (confusingly not the same as an "axis" - an Axes has an x axis and a y axis), and everything you label - title, ticks, legend, spines - hangs off that Axes.

Self-studyAlways use fig, ax = plt.subplots()2 min read
★ The one pattern to internalizeimport matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(8, 5)) # Figure + one Axes, explicitly ax.bar(roles, counts) # draw on THIS axes ax.set_title("Top data roles by postings") ax.set_xlabel("role") ax.set_ylabel("number of postings") fig.tight_layout()

Why prefer this over plt.bar(...)? Because the moment you want a second plot, a shared legend, or small multiples, the object-oriented style just works - fig, (ax1, ax2) = plt.subplots(1, 2) and you have two named axes. The stateful version leaves you guessing which "current" figure a command lands on. Learn the explicit pattern first and you never unlearn a bad habit.

fig vs ax - who does what The fig owns figure-wide things (size, saving, tight_layout). The ax owns the plot itself (the marks, title, labels, legend). When you're not sure where a method lives, ask: "is this about the whole canvas, or this one plot?"
Part 2 · covers the core plot types

The four charts you need 5 min live

You don't need thirty chart types. You need four, and the skill is matching each to the question it answers. Pick the wrong one and a true dataset tells a confusing story.

Line - trend over time use for: postings per month Bar - compare categories use for: postings by role Histogram - one distribution use for: salary_year spread Scatter - relationship use for: salary vs company size Match the chart to the question first; the code is the easy part.
🔍 Click to zoom - four charts, four questions: trend, comparison, distribution, relationship
LiveWhich chart answers which question3 min
ChartQuestion it answersOn our data
LineHow does one number change over time?postings per month across 2023-2024
BarHow do categories compare?postings by role, or by location
HistogramHow is one numeric column distributed?the spread of salary_year
ScatterDo two numbers move together?salary_year vs company size
Real world

A common mistake: using a bar chart for salary spread. A bar of "average salary by role" hides that half the postings for a role pay wildly more or less than that average. Ask "distribution?" and reach for a histogram - it's the difference between a headline that's true and one that's technically-an-average.

Self-studyThe method names on an Axes2 min read
  • ax.plot(x, y) - line
  • ax.bar(categories, heights) - vertical bars (ax.barh for horizontal, better for long role names)
  • ax.hist(values, bins=30) - histogram
  • ax.scatter(x, y) - scatter

Same four verbs, same object. Once fig, ax = plt.subplots() is muscle memory, switching chart types is just switching the method you call on ax.

Part 3 · covers labels, subplots, and saving

Labels, styles, subplots, saving 4 min live

A chart without labels is a puzzle. Here's the small set of methods that turn a raw plot into something you'd publish - and how to save it right.

LiveLabel it, size it, save it2 min
★ The publish-ready patternfig, ax = plt.subplots(figsize=(9, 5)) # width, height in inches ax.bar(roles, counts, color="#3776AB") ax.set_title("Top 10 data roles by postings") ax.set_xlabel("role") ax.set_ylabel("number of postings") ax.legend(["2023-2024 postings"]) # only if you have series to name fig.tight_layout() # stop labels getting clipped fig.savefig("roles.png", dpi=300, bbox_inches="tight") # 300 dpi = print quality fig.savefig("roles.svg", bbox_inches="tight") # vector - sharp at any size

For more than one plot, fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 5)) gives you a grid of Axes - "small multiples" that let a reader compare panels side by side. Always finish with tight_layout() so nothing overlaps.

Self-studyWhy PNG and SVG, and why 300 dpi2 min read
  • PNG at dpi=300 is print / publication quality - crisp in slides and on LinkedIn, not the blurry 72-dpi screenshot look.
  • SVG is a vector: it stays razor-sharp at any zoom because it stores shapes, not pixels. Perfect when someone blows your chart up on a projector.
  • Saving both is the workspace convention - PNG for the quick paste, SVG for anything that might get resized or printed.
Real world

An unlabeled chart is a lie by omission. A bar climbing from 3 to 5 means nothing until the y axis says "salary in thousands USD" or "postings per month". Every axis gets units before the chart leaves your notebook - that one habit separates a screenshot from a finding people trust.

Demo 1 of 3

A labeled bar of the top roles ★ 8 min · everyone codes

From your clean frame, count postings per job title and keep the top 10.

Build the figure explicitly, draw the bars, rotate the labels so long titles don't collide.

Title it, label both axes, then save a 300 dpi PNG.

★ Top 10 roles, bar chartimport matplotlib.pyplot as plt top = jobs["title_clean"].value_counts().head(10) # from the clean frame fig, ax = plt.subplots(figsize=(10, 5)) ax.bar(top.index, top.values, color="#3776AB") ax.set_title("Top 10 data roles by number of postings") ax.set_xlabel("role") ax.set_ylabel("number of postings") ax.tick_params(axis="x", rotation=45) # rotate crowded labels plt.setp(ax.get_xticklabels(), ha="right") # anchor them neatly fig.tight_layout() fig.savefig("top-roles.png", dpi=300, bbox_inches="tight")
Long labels colliding? Rotate to 45 with ha="right", or flip to a horizontal bar with ax.barh(top.index[::-1], top.values[::-1]) - reversed so the biggest bar sits on top. For role names, horizontal usually reads better.
Demo 2 of 3

A salary histogram with a median line ★ 8 min · everyone codes

Take the salaried subset (the ~30% of postings with a real salary_year) and draw its distribution.

Choose a sensible bin count, then drop a vertical line at the median with axvline to anchor the eye.

Read the shape out loud: is it symmetric, or does a long right tail pull the mean above the median?

★ Distribution of salary_yearsalaried = jobs.dropna(subset=["salary_year"]) median = salaried["salary_year"].median() fig, ax = plt.subplots(figsize=(9, 5)) ax.hist(salaried["salary_year"], bins=40, color="#4B8BBE", edgecolor="white") ax.axvline(median, color="#E6A700", linewidth=2, label=f"median = ${median:,.0f}") ax.set_title("Distribution of advertised yearly salary") ax.set_xlabel("salary_year (USD)") ax.set_ylabel("number of postings") ax.legend() fig.tight_layout() fig.savefig("salary-hist.png", dpi=300, bbox_inches="tight")
Real world

Salary data is almost always right-skewed - a long tail of a few very high offers drags the mean well above the typical role. The histogram makes that visible, and the median line shows where most people actually land. This is exactly the chart that keeps your Session 8 report from claiming a misleadingly high "average" salary.

Demo 3 of 3

Two panels side by side ★ 6 min · everyone codes

Make a 1x2 grid: postings by location on the left, the salary distribution on the right.

Each panel is its own Axes - draw on ax1 and ax2 independently.

tight_layout, then save both a PNG and an SVG for the report.

★ 1x2 small multiplestop_loc = jobs["location_state"].value_counts().head(8) fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(13, 5)) # left panel - postings by location ax1.barh(top_loc.index[::-1], top_loc.values[::-1], color="#3776AB") ax1.set_title("Postings by location (top 8 states)") ax1.set_xlabel("number of postings") # right panel - salary distribution ax2.hist(salaried["salary_year"], bins=40, color="#4B8BBE", edgecolor="white") ax2.set_title("Salary distribution") ax2.set_xlabel("salary_year (USD)") ax2.set_ylabel("number of postings") fig.tight_layout() fig.savefig("market-overview.png", dpi=300, bbox_inches="tight") fig.savefig("market-overview.svg", bbox_inches="tight")
subplots returns a grid With one row and two columns you unpack (ax1, ax2). For a 2x2 you'd get ((ax1, ax2), (ax3, ax4)), or capture the whole thing as axes and index it. Same Figure, many Axes - that's all small multiples are.
After the session

This week ◐ 30 min total

Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Why prefer fig, ax = plt.subplots() over calling plt.plot() directly?

The object-oriented style hands you the Figure and Axes as real objects, so multiple plots, shared legends, and small multiples just work. plt.plot draws onto a hidden "current" figure you have to keep track of.

2 · Which chart shows the distribution of a single numeric column like salary_year?

A histogram bins one numeric column and shows how values spread - skew, clusters, outliers. A bar of the average hides all of that behind a single number.

3 · Why save with savefig(dpi=300) in both PNG and SVG?

300 dpi keeps the raster crisp in print and slides; SVG is a vector that never blurs when resized. Saving both covers the quick paste and the projector blow-up.

Source material

Official sources covered

This session teaches the working content of the official matplotlib docs and Wes McKinney's open-access Python for Data Analysis (3e). Interactive galleries and the deep API stay on those platforms - links provided.

matplotlib pyplot tutorialstateful vs object-oriented, plot / bar / hist / scatter - Parts 1-2
matplotlib Figure / Axes (Artist) layerfig, ax anatomy, set_title / labels / legend, subplots - Parts 1 & 3
McKinney ch9 - Plotting and Visualizationfigures, subplots, saving, styling - all three demos
matplotlib animation + interactive backendsnot covered - static publication charts are the goal here

Session 6 cheat sheet · pin this

Start every chartfig, ax = plt.subplots(figsize=(w, h)) - explicit Figure + Axes, the pattern pros use.
Four verbsax.plot (trend) · ax.bar / barh (compare) · ax.hist (distribution) · ax.scatter (relationship).
Label everythingax.set_title, ax.set_xlabel, ax.set_ylabel, ax.legend - every axis gets units.
Small multiplesplt.subplots(nrows, ncols) -> a grid of axes; draw on each independently.
Never clipfig.tight_layout() before saving so labels and titles don't get cut off.
Save it rightfig.savefig("c.png", dpi=300) + fig.savefig("c.svg") - PNG for slides, SVG stays sharp.