Matplotlib in Python
Press Next → or use ← → arrow keys
Why Matplotlib? The Anatomy of a Figure
Matplotlib underpins almost every scientific visualisation tool in Python. Seaborn and Plotly are great for quick exploration — but when you need pixel-precise control (custom tick formatters, dual axes, shared-axis subplots), you always drop down to matplotlib.
The Figure is the outer canvas; each Axes is a single plot with its own title, x/y labels, ticks, spines and legend. Understanding this hierarchy is the key to every advanced layout.
Global Setup & the Two Interfaces
Set your defaults once at the top of the notebook via rcParams — it signals professionalism and keeps every chart consistent.
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np
# Apply a global style sheet
plt.style.use('seaborn-v0_8-darkgrid')
plt.rcParams['figure.figsize'] = (10, 5)
plt.rcParams['figure.dpi'] = 120
plt.rcParams['axes.titlesize'] = 14
plt.rcParams['lines.linewidth'] = 2
fig, ax = plt.subplots() gives you explicit handles to every element. Recommended for anything real.
Stateful plt.plot() calls are fine for a throwaway chart, but cause confusing state bugs in multi-axes figures.
Line Chart — Trends Over Time
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(months, revenue, color='#60a5fa', marker='o', label='Revenue')
ax.plot(months, target, color='#f59e0b', linestyle='--', label='Target')
ax.fill_between(months, revenue, target, where=[r > t for r, t in zip(revenue, target)],
alpha=0.15, color='#34d399', label='Above target')
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f'₹{x:.0f}k'))
Bar Chart — Comparing Categories
x = np.arange(len(categories)); width = 0.35
b1 = ax.bar(x - width/2, male_avg, width, label='Male', color='#60a5fa')
b2 = ax.bar(x + width/2, female_avg, width, label='Female', color='#f87171')
ax.bar_label(b1, fmt='₹%.0fk', padding=3) # value labels on bars
ax.barh(np.array(cats)[np.argsort(rev)], np.sort(rev)) # sorted horizontal
Scatter Plot — Up to Four Dimensions
sc = ax.scatter(df['age'], df['purchase'],
c=df['rating'], # colour = 3rd variable
s=df['delivery_days'] * 8, # size = 4th variable
cmap='viridis', alpha=0.6, edgecolors='white')
plt.colorbar(sc, ax=ax).set_label('Customer Rating')
ax.annotate('High-value outlier', xy=(58, 24800), xytext=(45, 23000),
arrowprops=dict(arrowstyle='->', color='#f87171'))
Histogram & KDE — Distribution Shape
n, bins, patches = ax.hist(data, bins=35, density=True, color='#60a5fa', alpha=0.6)
kde = gaussian_kde(data) # overlay a smooth KDE curve
ax.plot(x_range, kde(x_range), color='#f59e0b', label='KDE')
for patch, edge in zip(patches, bins): # colour bars by region
if edge > data.mean(): patch.set_facecolor('#f59e0b')
ax.axvline(data.mean(), color='#f59e0b', linestyle='--')
Subplots — Many Charts, One Figure
plt.subplots(nrows, ncols) returns a grid of Axes for dashboards and side-by-side comparisons.
Heatmap with imshow() — Correlations
ax.imshow() renders any 2D array as a coloured grid — correlation matrices, pivot tables, confusion matrices.
im = ax.imshow(corr, cmap='coolwarm',
vmin=-1, vmax=1)
plt.colorbar(im, ax=ax)
# annotate every cell
for i in range(n):
for j in range(n):
c = 'white' if abs(corr[i,j])>.6 else 'black'
ax.text(j, i, f'{corr[i,j]:.2f}',
ha='center', color=c)
Switch annotation colour to white on saturated cells and black on pale ones so every value stays readable — a small touch that separates amateur from publication-ready heatmaps.
Boxplot & Violin — Summary vs Full Shape
A boxplot shows median, quartiles, whiskers and outliers (add notch=True for a 95% CI around the median). A violin plot overlays a KDE to expose the full shape — including bimodal humps a boxplot would hide.
Styling & Saving Publication-Quality Figures
# swap the whole look in one line
plt.style.use('seaborn-v0_8-whitegrid')
plt.style.use('dark_background')
plt.style.use('ggplot')
plt.style.use('bmh')
# export at print quality
fig.savefig('chart.png', dpi=300,
bbox_inches='tight')
fig.savefig('chart.svg') # vector
fig.savefig('chart.pdf') # vector
| Format | Best for |
|---|---|
| PNG dpi=300 | Screen & web display |
| PDF / SVG | Print — vector, resizes with no quality loss |
| EPS | LaTeX & legacy academic docs |
Dual Axes — Two Scales on One Chart
When two series share an x-axis but live on wildly different scales — revenue in ₹ vs conversion rate in % — a single y-axis crushes one of them flat. ax.twinx() gives the second series its own right-hand axis.
fig, ax = plt.subplots()
ax.bar(months, revenue, color='#60a5fa') # left y-axis
ax2 = ax.twinx() # share x, new right y-axis
ax2.plot(months, conversion, color='#f87171', marker='o')
ax.set_ylabel('Revenue (₹)', color='#60a5fa')
ax2.set_ylabel('Conversion %', color='#f87171') # colour-match each axis!
Choosing the Right Colormap
A colormap is a data-encoding decision, not decoration. Prefer perceptually uniform maps where equal steps in data look like equal steps in colour — and stay colour-blind safe.
Sequential (viridis, plasma) for low→high magnitudes. Diverging (coolwarm) when a midpoint like 0 matters. Qualitative (tab10) for unordered categories.
jet has bright bands that invent features that aren't in the data and vanish in greyscale or for colour-blind readers. Viridis fixes all three.
Matplotlib in the ML Workflow
Beyond EDA, matplotlib is how you read your model — learning curves diagnose over/underfitting, and a confusion matrix shows exactly where a classifier fails.
A wide train–val gap means high variance (overfitting → regularise or get more data); both curves high and flat means high bias (underfitting). On the matrix, a bright off-diagonal cell is exactly the error class to fix.
Common Pitfalls & Their Fixes
| Symptom | The trap | The fix |
|---|---|---|
| Blank image file | savefig() after show() | Save before plt.show() — show() clears the figure |
| Cropped labels | Relying on defaults | plt.tight_layout() or bbox_inches='tight' |
| RAM climbs in a loop | Figures never released | plt.close(fig) after each save |
| Blurry export | Screen-DPI raster | dpi=300, or vector .svg/.pdf |
| Misleading colours | cmap='jet' | Perceptually uniform 'viridis' |
| Only last plot shows | Stateful plt. on many axes | Explicit fig, ax = plt.subplots() |
| Dates overlap | Raw datetime ticks | fig.autofmt_xdate() + a date locator |
The Everyday Cheat-Sheet
fig, ax = plt.subplots(figsize=(w,h))plt.subplots(r, c, sharex=True)ax.twinx()ax.plot / bar / barh / scatterax.hist / boxplot / violinplotax.imshow / fill_betweenax.set_title / set_xlabelax.legend(framealpha=.3)ax.annotate(...)ax.set_xlim / set_ylimax.set_xticks / set_xticklabelsax.xaxis.set_major_formatter(...)plt.style.use('...')plt.rcParams.update({...})ax.spines[[...]].set_visible(False)plt.tight_layout()fig.savefig('f.png', dpi=300)plt.close(fig)subplots → plot → label → style → tight_layout → save. Internalise that six-step loop and any figure, however complex, becomes routine.
The Golden Rules — 1 to 4
fig, ax = plt.subplots() — never rely on stateful plt.plot() beyond a throwaway chart. State machines cause silent bugs in multi-axes figures.plt.tight_layout(). Do it before every show or save — otherwise axis labels and titles routinely get cropped.ax.spines[['top','right']].set_visible(False) — the default box adds weight without adding information.The Golden Rules — 5 to 7 & Takeaway
dpi=300, bbox_inches='tight' for print; .svg/.pdf when the figure must resize without pixelation.ax.annotate() to label peaks and anomalies — a chart that needs a paragraph of caption to explain has already failed.rcParams globally. Configure fonts, DPI and grids once at the top of the notebook instead of styling each chart by hand.Higher-level libraries fail the moment you hit a custom requirement — and the answer is always "drop down to matplotlib." Learn it deeply and you'll never be blocked by a chart requirement again.
📊 End of tutorial · Press ← to review, or click Restart