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
Matplotlib log-log plot with error bars and major and minor grid lines using which both
which="both" draws the minor grid, and the error bar that would go below zero is clipped.

The first error bar would reach below zero, which a log axis can’t show, so Matplotlib clips it at the bottom edge. Again there’s no warning, so check any bar that looks cut short.

ax.grid(True) alone draws only the major lines at 10, 100, 1000. On a log axis the minor lines are what let you read values between them, so pass which="both".

Error bars in general, including asymmetric ones, are covered in plotting error bars in Matplotlib.

Log scale histograms: log=True versus log-spaced bins

plt.hist(..., log=True) looks like the answer for a log histogram, but it isn’t always:

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(7)
order_values = rng.lognormal(mean=3, sigma=1.2, size=5000)     # skewed, like real order sizes

fig, (left, right) = plt.subplots(1, 2, figsize=(11, 4), layout="constrained")
counts_log, _, _ = left.hist(order_values, bins=40, log=True)
left.set_title("hist(..., log=True): only the counts are log")

bins = np.logspace(np.log10(order_values.min()), np.log10(order_values.max()), 40)
counts_bins, _, _ = right.hist(order_values, bins=bins)
right.set_xscale("log")
right.set_title("log-spaced bins on a log x axis")
plt.show()

print("left:  x", left.get_xscale(), "| y", left.get_yscale(), "| empty bins", int((counts_log == 0).sum()), "of 40")
print("right: x", right.get_xscale(), "| y", right.get_yscale(), "| empty bins", int((counts_bins == 0).sum()), "of 40")

Output:

left:  x linear | y log | empty bins 26 of 40
right: x log | y linear | empty bins 3 of 40
Two Matplotlib histograms comparing hist log True with log spaced bins on a log x axis
log=True logs the counts; log-spaced bins are what you need for a log x axis.

log=True only puts the counts on a log scale. The x axis stays linear, so skewed data like order values still piles up in the first few bars.

For a log x axis, build the bins with np.logspace and then call set_xscale("log"). Equal-width bins on a log axis leave most of them empty, as the empty-bin counts show.

Log-log scatter plot

Scatter plots take the same two calls. There’s no scatter version of loglog, so set both scales yourself:

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

rng = np.random.default_rng(3)
population = 10 ** rng.uniform(3, 7, 60)                 # towns of 1,000 to 10 million people
stores = 0.002 * population ** 0.9 * rng.lognormal(0, 0.3, 60)

fig, ax = plt.subplots()
ax.scatter(population, stores, s=18)
ax.set_xscale("log")
ax.set_yscale("log")
print("scatter axes:", ax.get_xscale(), ax.get_yscale())
print("x spans", f"{population.min():,.0f} to {population.max():,.0f}")

Output:

scatter axes: log log
x spans 1,014 to 7,831,431

With populations from about a thousand to nearly eight million, a linear axis would pile nearly every town against the left edge. On log axes the whole range spreads out, and a power-law relationship shows up as a straight trend.

Minor ticks, tick labels and colorbars on a log scale

Log axes have their own tick machinery. LogLocator places the ticks, and minor ticks sometimes vanish when the axis spans many decades.

That topic has its own guide: Matplotlib log scale minor ticks and log colorbars, including LogNorm for putting a colorbar on a log scale.

plt.loglog vs semilogx vs set_xscale: which should you use?

You wantUse
Both axes logarithmic, new plotplt.loglog(x, y) or ax.loglog(x, y)
Only the x axisax.semilogx(x, y)
Only the y axisax.semilogy(x, y)
Switch an existing plot, or scatter/bar/histax.set_xscale("log") / ax.set_yscale("log")
Base 2 instead of 10add base=2
Data that crosses zeroset_yscale("symlog")

I default to set_xscale and set_yscale in real projects. They work with every plot type, and they read clearly when someone else opens the code.

A few more Matplotlib guides that pair well with log scales:

Frequently asked questions

How do I set a log scale in Matplotlib?

Call ax.set_xscale("log") and/or ax.set_yscale("log"), or plot with plt.loglog(), plt.semilogx() or plt.semilogy().

What is the difference between plt.loglog and plt.plot?

plt.loglog() is plt.plot() with both axes switched to a log scale. It takes the same arguments.

Why is my data missing on a log scale?

Zero and negative values can’t be shown on a log axis, and Matplotlib drops them without a warning. Filter them out or use symlog.

How do I change the log base in Matplotlib?

Pass base, for example ax.set_xscale("log", base=2). The old basex and basey arguments now raise a TypeError.

How do I make a histogram with a log scale?

hist(data, log=True) logs the counts. For a log x axis, use bins=np.logspace(...) and ax.set_xscale("log").

How do I show minor grid lines on a log plot?

Use ax.grid(True, which="both"). The default draws only the major grid lines.

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

Because log y = log a + b · log x is linear in the logs, with the exponent as the slope. Matplotlib’s own log scale examples show more variations.