Matplotlib Log Scale and Log-Log Plots (plt.loglog, semilogx, semilogy)

Matplotlib sets a log scale with one call: plt.loglog() for both axes, plt.semilogx() or plt.semilogy() for one, or ax.set_xscale("log") on a plot you’ve already drawn:

import matplotlib.pyplot as plt

plt.loglog(x, y)              # both axes
plt.semilogy(x, y)            # y axis only
ax.set_xscale("log")          # switch an existing axis

This guide covers log-log plots, power-law fits, the base, zeros, error bars and histograms. The charts are real windows from Python 3.14.7, Matplotlib 3.11.2 and NumPy 2.5.3.

Set a log scale on the x axis, y axis or both

Here’s the same data, y = x², with each of the three options side by side:

import matplotlib.pyplot as plt
import numpy as np

x = np.logspace(0, 3, 40)          # 1 to 1,000
y = x ** 2

fig, axes = plt.subplots(1, 3, figsize=(12, 3.8), layout="constrained")
axes[0].semilogx(x, y); axes[0].set_title("semilogx: log x axis")
axes[1].semilogy(x, y); axes[1].set_title("semilogy: log y axis")
axes[2].loglog(x, y);   axes[2].set_title("loglog: both axes")
for ax in axes:
    ax.grid(True, which="both", alpha=0.3)
plt.show()

for ax in axes:
    print(f"{ax.get_title():<22} x={ax.get_xscale():<7} y={ax.get_yscale()}")

Output:

semilogx: log x axis   x=log     y=linear
semilogy: log y axis   x=linear  y=log
loglog: both axes      x=log     y=log
Matplotlib log scale comparison showing the same data with semilogx semilogy and loglog
Only the log-log version turns y = x² into a straight line.

semilogx logs the x axis, semilogy logs the y axis, and loglog logs both. They’re shortcuts for plot() followed by set_xscale and set_yscale.

Which one you want depends on the data. Exponential growth is straight on semilogy, while a power law like x² is straight only on loglog.

When only the y axis needs a log scale, setting the Matplotlib y axis to log scale goes deeper, including symlog for data that crosses zero.

Log scale in a pandas plot

If your data is in a DataFrame, you don’t need Matplotlib calls at all. df.plot() takes the log switches directly:

import matplotlib
matplotlib.use("Agg")
import numpy as np
import pandas as pd

days = pd.date_range("2026-01-01", periods=6, freq="MS")
signups = pd.DataFrame({"signups": [120, 480, 1900, 7600, 30000, 121000]}, index=days)

for kwargs in ({"logy": True}, {"logx": True}, {"loglog": True}):
    ax = signups.plot(**kwargs)
    print(f"df.plot({list(kwargs)[0]}=True) -> x {ax.get_xscale():<7} y {ax.get_yscale()}")

Output:

df.plot(logy=True) -> x linear  y log
df.plot(logx=True) -> x log     y linear
df.plot(loglog=True) -> x log     y log

logy=True suits growth data like these monthly sign-ups, which multiply by about four each month. On a log y axis that steady multiplication becomes a straight line you can actually read.

Make a log-log plot with plt.loglog

plt.loglog() takes the same arguments as plt.plot(), so markers, labels and colors all work as usual:

import matplotlib.pyplot as plt
import numpy as np

requests = np.array([1, 5, 10, 50, 100, 500, 1000, 5000])
latency_ms = 3.0 * requests ** 0.8

lines = plt.loglog(requests, latency_ms, marker="o", label="API server")
plt.xlabel("Requests per second")
plt.ylabel("Latency (ms)")
plt.title("Latency vs load, 2026 benchmark")
plt.legend()
plt.show()

print("plt.loglog returned:", type(lines).__name__, "of", type(lines[0]).__name__)

Output:

plt.loglog returned: list of Line2D
Matplotlib log-log plot made with plt.loglog showing latency against requests per second
A load test on log-log axes: three orders of magnitude on each axis fit comfortably.

Like plot(), it returns a list of line objects, one per line drawn. ax.loglog() is the object-oriented version and does exactly the same on a specific axes.

On linear axes, the small request counts would be squashed into one corner. The log scale gives each factor of ten the same amount of space.

Why a power law is a straight line on a log-log plot

That straight line isn’t a coincidence, and it’s the main reason scientists and engineers reach for log-log plots:

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)
x = np.logspace(0, 3, 30)
y = 3.0 * x ** 1.5 * rng.lognormal(0, 0.08, x.size)

slope, intercept = np.polyfit(np.log10(x), np.log10(y), 1)
print(f"fitted exponent:    {slope:.3f}   (true value 1.5)")
print(f"fitted coefficient: {10 ** intercept:.3f}   (true value 3.0)")

fig, ax = plt.subplots(figsize=(7, 4.5), layout="constrained")
ax.loglog(x, y, "o", label="measurements")
ax.loglog(x, 10 ** intercept * x ** slope, "-", label=f"fit: y = {10 ** intercept:.2f} x^{slope:.2f}")
ax.grid(True, which="both", alpha=0.3)
ax.legend()
plt.show()

Output:

fitted exponent:    1.506   (true value 1.5)
fitted coefficient: 2.947   (true value 3.0)
Matplotlib log-log plot of power law data with a fitted straight line and the fitted exponent in the legend
Fit a straight line to the logs, and its slope is the exponent of the power law.

Taking the log of y = a · xb gives log y = log a + b · log x, which is a straight line. So fitting a line to the logs recovers the exponent as the slope.

np.polyfit on log10 of both arrays did exactly that here, landing very close to the true exponent of 1.5. I use this constantly for scaling tests: plot, fit, read the slope.

Change the log base in Matplotlib

Base 10 is the default. For data that doubles, like memory sizes, base 2 reads much better:

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

memory_mb = [1, 2, 4, 8, 16, 32, 64, 128]
fig, ax = plt.subplots()
ax.plot(memory_mb, range(len(memory_mb)), "o-")
ax.set_xscale("log", base=2)
fig.canvas.draw()
labels = [t.get_text() for t in ax.get_xticklabels()][1:-1]
print("tick labels:", [s.replace("$\\mathdefault{", "").replace("}$", "").replace("{", "").replace("}", "") for s in labels])

Output:

tick labels: ['2^0', '2^1', '2^2', '2^3', '2^4', '2^5', '2^6', '2^7']

set_xscale("log", base=2) labels the ticks as powers of two. loglog() and semilogx() accept the same base argument.

Older tutorials use basex=2, which current Matplotlib rejects with a TypeError. The next section shows the exact message.

What happens to zero and negative values on a log scale?

A log scale can’t show zero or negative numbers, since their logarithm doesn’t exist. What Matplotlib does about it surprises people:

import warnings
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

def try_plot(label, xs):
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        fig, ax = plt.subplots()
        ax.plot(xs, range(len(xs)), "o")
        ax.set_xscale("log")
        fig.canvas.draw()
        low, high = ax.get_xlim()
        print(f"{label:<28} x range {low:.2f} to {high:.2f} | warnings: {len(caught)}")
        for w in caught[:1]:
            print("   ", w.message)
        plt.close(fig)

try_plot("[0, 1, 10, 100]", [0, 1, 10, 100])
try_plot("[-5, 1, 10, 100]", [-5, 1, 10, 100])
try_plot("[0, -1, -10] (none positive)", [0, -1, -10])
print()

fig, ax = plt.subplots()
for kwargs in ({"basex": 2}, {"base": 1}):
    try:
        ax.set_xscale("log", **kwargs)
    except (TypeError, ValueError) as err:
        print(f"set_xscale('log', {kwargs}) -> {type(err).__name__}: {err}")

Output:

[0, 1, 10, 100]              x range 0.79 to 125.89 | warnings: 0
[-5, 1, 10, 100]             x range 0.79 to 125.89 | warnings: 0
[0, -1, -10] (none positive) x range 0.89 to 11.22 | warnings: 2
    Data has no positive values, and therefore cannot be log-scaled.

set_xscale('log', {'basex': 2}) -> TypeError: LogScale.__init__() got an unexpected keyword argument 'basex'. Did you mean 'base'?
set_xscale('log', {'base': 1}) -> ValueError: The log base cannot be <= 0 or == 1
Command Prompt showing Matplotlib dropping zero and negative values on a log scale and the base TypeError and ValueError
Zeros and negatives vanish without a warning, unless no value at all is positive.

The zero and the -5 were simply left out. No error, no warning, just a missing point, which is easy to miss in a busy chart.

You only get a warning when every value is non-positive: Data has no positive values, and therefore cannot be log-scaled. Filter your data first if a missing point would matter.

The bottom two lines of the screenshot are the base errors: basex became base, and a base of 1 or less is refused. For data that genuinely crosses zero, symlog is covered in the y axis log scale guide.

Set axis limits on a log scale

The same rule applies when you set the limits yourself. A limit of zero can’t exist on a log axis:

import warnings
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.loglog([1, 10, 100, 1000], [1, 10, 100, 1000])

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    ax.set_xlim(0, 1000)
low, high = ax.get_xlim()
print(f"set_xlim(0, 1000)   -> ({low:.2f}, {high:.0f})")
for w in caught:
    print("   ", w.message)

ax.set_xlim(0.5, 2000)
low, high = ax.get_xlim()
print(f"set_xlim(0.5, 2000) -> ({low:.2f}, {high:.0f})")

Output:

set_xlim(0, 1000)   -> (0.71, 1000)
    Attempt to set non-positive xlim on a log-scaled axis will be ignored.
set_xlim(0.5, 2000) -> (0.50, 2000)

Matplotlib warns and ignores the zero, keeping its old lower limit instead. Pick a small positive number like 0.5 when you want the axis to start below your first point.

Log-log plot with error bars and grid lines

Error bars and grid lines both behave a little differently on log axes:

import matplotlib.pyplot as plt
import numpy as np

sizes = np.array([10, 100, 1000, 10000])
times = np.array([0.8, 6.5, 70, 690])
errors = np.array([1.0, 2.0, 15, 120])     # the first lower bar would go below zero

fig, ax = plt.subplots(figsize=(7, 4.5), layout="constrained")
ax.errorbar(sizes, times, yerr=errors, fmt="o-", capsize=4)
ax.set_xscale("log")
ax.set_yscale("log")
ax.grid(True, which="both", alpha=0.35)
ax.set_xlabel("Input size")
ax.set_ylabel("Run time (ms)")
plt.show()

print(f"lowest error bar would reach {times[0] - errors[0]:.1f} - clipped at the bottom of the axis, no warning")

Output:

lowest error bar would reach -0.2 - clipped at the bottom of the axis, no warning