Scales and limits
Log axes, zero baselines, and why twin axes can prove anything
ax.set_xlimax.set_yscalesymlogax.twinxax.marginssharexWatch it happen
Play it through, or step back and forth yourself.
ax.margins(x=0)line touches both edgesax.margins(0.1)10% padding both axesax.autoscale(enable=False)freeze itax.relim(); ax.autoscale_view()recompute after adding dataBy default matplotlib fits the data and adds 5% breathing room on each side. That padding is ax.margins(), and setting ax.margins(x=0) makes a time series touch both edges of the frame, which usually looks better.
The idea
Limits and scales are where a chart stops being a rendering of your data and starts being an argument about it. Most of this lesson is about not making that argument by accident.
Autoscale and margins
By default matplotlib fits the data and adds 5% padding on each side. That padding is ax.margins():
ax.margins(x=0) # line touches both edges — usually right for time series
ax.margins(0.1) # 10% on both axesSetting limits
ax.set_ylim(0, 100)
ax.set_ylim(bottom=0) # pin one end, let the other float
ax.set_ylim(100, 0) # reversed — for depth, or rank where 1 is best
ax.get_ylim() # read it backSetting a limit turns autoscaling off for that axis. That's usually what you want in a report — it stops the chart resizing itself when next month's data arrives — and occasionally a trap, when you set limits early and then add a series that falls outside them.
The zero question
There is a real answer here, and it depends on what the chart encodes. Bars encode value as length, so cutting the baseline changes the length without changing the number: the chart is then simply wrong. Lines encode position, and the reader is looking at the shape of the movement — forcing a stock price down to zero can flatten the very thing you're showing.
So: bars start at zero, always. Lines may be zoomed, as long as the axis is labelled and you aren't dressing up noise as a trend.
Log scales
ax.set_yscale("log")Use it when values span orders of magnitude — populations, file sizes, case counts. On a log axis equal distances mean equal ratios, so exponential growth straightens into a line and the slope is the growth rate. On a linear axis the same data looks like nothing happened for months and then everything happened at once.
Two catches, one of them silent:
- A log axis cannot show zero or negative values. They don't error and they don't leave a gap; they're simply not drawn. If your counts include zeros, you have lost rows and nothing told you.
- Readers miss log axes. Put it in the label —
"cases (log scale)"— because somebody who reads it as linear will misjudge every gap on the chart.
ax.set_yscale("symlog") handles data crossing zero: linear near the origin, log beyond it. There's also "logit" for probabilities.
Twin axes
fig, ax = plt.subplots()
ax.plot(when, cups, color="C0")
ax2 = ax.twinx()
ax2.plot(when, temp, color="C1")
len(fig.axes) # 2 — twinx adds a real AxesThis is the standard way to show two quantities in different units against the same x. It's also the easiest way in matplotlib to produce a chart that is readable and false.
Both y ranges are your choice, and so the crossing points are too. Slide one scale and the same two series can be made to look like they move together, or opposite, or lead each other by a week. Nothing in the chart tells the reader which arrangement you picked.
Two honest alternatives:
# stacked, sharing the x axis
fig, (a, b) = plt.subplots(2, 1, sharex=True)
# or put both on one scale as % change from the start
pct = s / s.iloc[0] * 100Use twinx when the two units are genuinely fixed and familiar — °C and mm of rain — and say in the caption which axis is which. Otherwise, stack them.
Sharing
fig, axes = plt.subplots(2, 1, sharex=True) # zoom one, both move
fig, axes = plt.subplots(1, 3, sharey=True) # one y scale, comparable panelssharey across a row of panels is the quiet hero of small multiples: without it each panel scales itself and the comparison you built the layout for becomes meaningless.
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
tryadding a zero to `growth` and watching that point vanish from the log panel.
Your turn
3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Plot delhi and pin the y axis to exactly 0 to 100. Return list(ax.get_ylim()).
Plot delhi on a log y axis, then return ax.get_yscale() to confirm it took.
Plot delhi, then add a second y scale with twinx() and plot mumbai on it. Return len(fig.axes) — the point is that a twin is another Axes.
