Matplotlib is the foundation of Python plotting. It's the library that NumPy and pandas talk to on a .plot() call, and the one that seaborn, pandas, and most data-science notebooks ultimately render through. It has two APIs (one inherited from MATLAB, one object-oriented), a handful of core chart types, and many customization options. This lesson covers the object-oriented model, the basic plot types, the common customizations, multi-panel layouts, saving figures, integration with pandas, and when to use a different library.
Matplotlib exposes two ways to make a plot. The pyplot API is state-machine-based: calls like plt.plot(...), plt.title(...), plt.legend() operate on an internally tracked "current figure" and "current axes". It mimics MATLAB and reads compactly for tiny throwaway plots.
The same plot with the object-oriented API is more explicit but scales:
fig is the entire figure (the canvas). ax is one set of axes (one chart inside the canvas). All the customization happens through ax.set_xxx(...) methods. The advantages of the OO API:
fig, (ax1, ax2) = plt.subplots(1, 2) provides two axes objects to draw on. With pyplot's state machine, switching the "current axes" is a constant source of bugs.ax argument and draw on a caller-supplied axes. Pyplot's "current axes" makes this awkward.The rest of this lesson uses the OO API exclusively. fig, ax = plt.subplots() is the entry point.
The diagram shows the figure-axes-elements hierarchy. One figure holds one or more axes; each axes owns its title, labels, ticks, legend, and the actual plotted elements (lines, bars, points). Every customization is a method call on the relevant object.
A line plot shows a continuous quantity changing across an ordered axis. Daily sales, sensor readings over time, prices across order dates.
ax.plot(x, y) is the basic call. marker="o" puts a circle at each data point. color="#00ceff" picks the line color. linewidth=2 thickens the line slightly. ax.grid(True, alpha=0.3) adds a faint grid in the background, which makes values easier to read off the chart.
figsize=(8, 4) sets the figure size in inches. The default is (6.4, 4.8), which is usually too small for presentation. Wide-and-short (8 by 4) suits time series; nearly square (6 by 6) suits scatter plots.
Multiple lines on one axes means multiple ax.plot(...) calls. Each call adds another line:
Each line gets a label=. ax.legend() picks up those labels and renders the legend box. The default placement is "best" (matplotlib picks a corner that doesn't overlap data); pass loc="upper left" or similar to control placement.
A bar chart shows values across discrete categories. Sales by product, headcount by department, ratings per category.
ax.bar(categories, values) draws the bars. A list of colors paints each bar a different color, which can highlight a top category without cluttering the chart. The for loop adds a value label above each bar; ax.text(x, y, text) places a string at the given data coordinates, and ha="center" centers it horizontally.
For horizontal bars (better for long category names), ax.barh(...) is the same call with x and y swapped semantically.
Horizontal bars usually work better when the category names are long enough that vertical-bar labels would overlap or rotate. Reading "Audio" stacked vertically under a thin bar is harder than reading it across.
For grouped bars (comparing two metrics side by side per category), shift the x positions:
Two ax.bar(...) calls with shifted x positions and reduced widths produce the side-by-side bars. ax.set_xticks(x) plus ax.set_xticklabels(categories) puts the category names back where they belong (in the middle of each pair of bars).
A scatter plot shows the relationship between two numeric variables. Each point is one observation. It's useful for spotting correlation, clusters, and outliers.
ax.scatter(x, y) plots one dot per observation. alpha=0.7 makes the dots semi-transparent so overlapping points darken visibly. edgecolors="black" outlines each dot, which improves visibility against a light background.
The relationship visible here is roughly linear and negative: higher prices correlate with fewer units sold. A scatter plot is the first chart for two numeric columns whose relationship is in question.
A third variable can be encoded in the dot size or color:
s=revenue / 20 sizes each dot by revenue (divided by 20 to keep dots reasonable). c=revenue colors each dot by the same value; cmap="viridis" is a perceptually uniform color map, which means equal differences in data correspond to equal perceptual differences in color (and it's colorblind-friendly). plt.colorbar(...) adds the color legend on the right.
Encoding too many variables in one plot gets noisy fast. Two extra dimensions (size and color, or shape and color) is the practical limit for readability.
Scatter plots with millions of points get slow to render and visually saturate (everything overlaps). For very large datasets, switch to ax.hexbin(...) (which bins the plane into hexagons and colors them by count) or ax.hist2d(...) (a 2D histogram). Both summarize density instead of drawing every point.
A histogram shows the distribution of a single numeric variable: how many observations fall into each bin.
ax.hist(values, bins=20) partitions the value range into 20 equal-width bins and counts how many observations fall into each. edgecolor="black" outlines each bar, which makes the bin boundaries visible. ax.axvline(...) draws a vertical line at the mean, with a label that goes into the legend.
The bin count is a real choice. Too few bins (5) hides the shape; too many (100) makes the chart noisy. A rule of thumb is bins = int(np.sqrt(len(data))), but the best value depends on the data. Try 10, 20, 50 and pick the clearest.
For comparing two distributions on the same axes, plot both with alpha:
Both histograms are drawn semi-transparent so they overlap visibly. The shift in the new distribution toward higher ratings becomes visible. For more than two distributions, a density=True parameter normalizes each histogram so the areas sum to 1, making them comparable when the sample sizes differ.
ax.pie(...) draws a pie chart: a circle divided into slices proportional to category values.
labels= is the category names. autopct="%1.1f%%" shows the percentage of each slice (the format string "%1.1f%%" means "one decimal place followed by a literal %"). colors= is the slice palette.
Pie charts have a real readability problem: humans are bad at comparing angles. A bar chart with the same data is faster to read and more precise. Two cases where a pie chart works better: very few slices (2 or 3) where the proportions are themselves the story, and "X is a majority" claims where the visual punch matters more than precision. For everything else, prefer a bar chart.
Every plot needs at least a title and axis labels. Most need a legend, sensible tick formatting, and grid lines. The OO API exposes all of this through methods on ax.
Common customizations:
marker="s" is a square marker. Common values: "o" (circle), "s" (square), "^" (triangle), "d" (diamond), "*" (star), "x" (cross).linewidth controls line thickness; markersize controls marker size.ax.yaxis.set_major_formatter(...) reformats the tick labels (here, adding $ and commas to revenue).ax.grid(True, axis="y", linestyle="--") adds only the horizontal grid lines (along the y-axis), dashed for subtlety.ax.spines["top"].set_visible(False) hides the top border of the plot. Removing top and right spines is a common "tufte-style" cleanup that reduces visual clutter.Color values can be hex codes ("#00ceff"), named colors ("red", "steelblue", "orange"), or RGB tuples ((0.0, 0.8, 1.0)). Matplotlib's full list of named colors covers most needs; the project's brand palette is the hex form from the diagram guidelines.
Line styles: "-" (solid, default), "--" (dashed), ":" (dotted), "-." (dash-dot).
plt.subplots(rows, cols) creates a figure with a grid of axes. The result is (fig, axes), where axes is a 2D array of Axes objects when there's more than one row or column.
plt.subplots(2, 2) returns a figure with four axes in a 2x2 grid. axes[row, col] indexes them. Each axes is independent: its own title, labels, ticks. fig.suptitle(...) adds a title for the entire figure. fig.tight_layout() adjusts spacing so titles and labels don't overlap; without it, the dense grid looks cramped.
For a single row or column of subplots, the result is a 1D array, which can be unpacked directly:
Two axes side by side, unpacked into ax1 and ax2 for clarity. This is the cleanest pattern for two related charts.
When the subplots share an x or y range, sharex=True or sharey=True aligns the axes:
sharex=True means both axes use the same x range and only the bottom one shows tick labels. The vertical alignment makes day-to-day comparisons easy.
fig.savefig(path) writes the figure to disk. The format is inferred from the file extension: .png, .jpg, .pdf, .svg. PNG is the standard raster format for web; SVG is vector for scaling without quality loss; PDF is vector and prints well.
dpi=150 controls pixel density for raster formats. The default is 100, which works for screens; 150-300 is better for print or high-res displays. bbox_inches="tight" trims excess whitespace around the figure, which matters when embedding in documents or web pages.
plt.close(fig) releases the figure's memory. In notebooks, matplotlib usually displays and closes automatically; in long-running scripts that generate many figures, skipping the close call leaks memory.
Common parameters:
| Parameter | Effect |
|---|---|
dpi | Resolution for raster formats (higher = sharper, larger file) |
bbox_inches="tight" | Trim excess whitespace |
transparent=True | PNG with transparent background |
facecolor= | Background color (default white) |
format= | Force a format ignoring the extension |
Pandas DataFrames and Series have a .plot() method that delegates to matplotlib. It's the quickest way to chart pandas data and produces a matplotlib Axes object that can be customized further.
df.plot(x=, y=, kind=, ax=) is the pandas entry point. kind= can be "line", "bar", "barh", "hist", "scatter", "box", "area", "pie", "kde". Passing ax=ax makes the plot draw on a specific axes (the one from plt.subplots()), which keeps the OO API intact and allows further axes customization.
Without ax=, pandas calls plt.subplots() itself and returns the new axes:
For multi-series plots, pandas handles the layout:
secondary_y="orders" puts the orders series on a second y-axis on the right side, which fits when two series have very different scales (revenue in thousands, orders in hundreds). Use it sparingly: dual-axis charts are easy to mislead with.
kind="barh" gives horizontal bars; legend=False hides the legend since there's only one series.
df.plot() is convenient but slightly slower than the direct matplotlib calls because of the wrapping. For one-off charts the difference is invisible; for tight loops generating thousands of small charts, going straight to matplotlib saves time.
Matplotlib fits when the requirements include:
Other libraries handle other cases better:
Seaborn wraps matplotlib with statistical defaults: better automatic palettes, built-in support for distributions (displot, kdeplot), categorical comparisons (boxplot, violinplot, swarmplot), and faceted plots (relplot, catplot). Charts come out looking polished with one call. Internally it produces a matplotlib figure, so further customization is possible. Use seaborn when the plot is a statistical summary and sensible defaults are wanted; use raw matplotlib when precise control matters.
Plotly produces interactive charts: hover tooltips, zoom, pan, click-to-toggle. The output is HTML, which works in notebooks and web pages. Use plotly for exploratory analysis in notebooks and dashboards where interactivity matters. The downside is heavier JavaScript output and a different API to learn.
Bokeh is similar to plotly: interactive, web-targeted, large dataset support. Slightly more verbose for simple plots but powerful for custom interactive dashboards.
Altair is a declarative wrapper around Vega-Lite. The chart is described in terms of data columns and encodings (this column to x, that column to color), and Altair figures out the rest. Concise for standard statistical charts, less flexible for fine-grained custom layouts.
A rough decision tree: matplotlib for static and precise, seaborn for statistical with good defaults, plotly for interactive. Most data-science codebases end up using both matplotlib and seaborn, because seaborn handles the high-level "summarize this distribution" and matplotlib handles the low-level "make it look exactly like this."
9 quizzes