Matplotlib log scale tick label number formatting
Asked Answered
O

6

76

With matplotlib when a log scale is specified for an axis, the default method of labeling that axis is with numbers that are 10 to a power eg. 10^6. Is there an easy way to change all of these labels to be their full numerical representation? eg. 1, 10, 100, etc.

Note that I do not know what the range of powers will be and want to support an arbitrary range (negatives included).

Odd answered 20/2, 2014 at 21:42 Comment(0)
O
78

Sure, just change the formatter.

For example, if we have this plot:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.axis([1, 10000, 1, 100000])
ax.loglog()

plt.show()

enter image description here

You could set the tick labels manually, but then the tick locations and labels would be fixed when you zoom/pan/etc. Therefore, it's best to change the formatter. By default, a logarithmic scale uses a LogFormatter, which will format the values in scientific notation. To change the formatter to the default for linear axes (ScalarFormatter) use e.g.

from matplotlib.ticker import ScalarFormatter
for axis in [ax.xaxis, ax.yaxis]:
    axis.set_major_formatter(ScalarFormatter())

enter image description here

Ostrogoth answered 20/2, 2014 at 22:5 Comment(12)
Thanks, exactly what I wanted. Surprised none of the other answers on here were this simple.Odd
What if I wanted to change the numbers to, 1, 5, 10, 20?Gravedigger
@Joe Kington: I would like to add ticks in between, like 50,200, etc.., How can I do that? I tried, set_xticks[50.0,200.0] but that doesn't seem to work!Pfennig
But with ax.axis([1, 100, 1, 100]), ScalarFormatter gives 1.0, 10.0, ... which is not what I desire. I want it to give integers...Antitragus
I also had to use set_powerlimits() to avoid the scientific notation.Christi
@DonKirkby can you provide a snippet of code? I can't seem to get set_powerlimits or set_scientific to produce what I want.Aq
According to the docs, @BrandonDube, formatter.set_powerlimits((-3, 4)) sets the pre-2007 default in which scientific notation is used for numbers less than 1e-3 or greater than 1e4. In this answer's snippet, you'll have to assign the formatter to local variable, set power limits, then assign it on the axis.Christi
Sure - the lines aren't right but here's a snippet with semicolons for linebreaks: fig, ax = plt.subplots(); ax.plot(x,y); tick = ticker.ScalarFormatter(); ticket.set_powerlimits((-3,20)); ax.xaxis.set_major_formatter(ticket); ax.set(xscale='log'); plt.show(); <- yields major ticks in scientific notation.Aq
And I had to use xaxis.set_minor_formatter(..) too.Orthopterous
@DonKirkby: You can use .set_scientific(False) instead of set_powerlimits() to disable the scientific notation.Orthopterous
Thanks, @Timmmm, I posted a summary with your hint and the others in a new answer.Christi
How to set the decimal place with this oneJarrad
W
62

I've found that using ScalarFormatter is great if all your tick values are greater than or equal to 1. However, if you have a tick at a number <1, the ScalarFormatter prints the tick label as 0.

enter image description here

We can use a FuncFormatter from the matplotlib ticker module to fix this issue. The simplest way to do this is with a lambda function and the g format specifier (thanks to @lenz in comments).

import matplotlib.ticker as ticker

ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda y, _: '{:g}'.format(y)))

Note in my original answer I didn't use the g format, instead I came up with this lambda function with FuncFormatter to set numbers >= 1 to their integer value, and numbers <1 to their decimal value, with the minimum number of decimal places required (i.e. 0.1, 0.01, 0.001, etc). It assumes that you are only setting ticks on the base10 values.

import matplotlib.ticker as ticker
import numpy as np

ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda y,pos: ('{{:.{:1d}f}}'.format(int(np.maximum(-np.log10(y),0)))).format(y)))

enter image description here

For clarity, here's that lambda function written out in a more verbose, but also more understandable, way:

def myLogFormat(y,pos):
    # Find the number of decimal places required
    decimalplaces = int(np.maximum(-np.log10(y),0))     # =0 for numbers >=1
    # Insert that number into a format string
    formatstring = '{{:.{:1d}f}}'.format(decimalplaces)
    # Return the formatted tick label
    return formatstring.format(y)

ax.yaxis.set_major_formatter(ticker.FuncFormatter(myLogFormat))
Wapentake answered 19/10, 2015 at 11:26 Comment(2)
This follow-up is exactly what I needed, thanks! One suggestion: wouldn't the g format specifier do the trick as well? With FuncFormatter(lambda y, _: '{:g}'.format(y)) I got the same output.Artefact
Yes I think you're right. I learnt about the 'g' formatter after I wrote this answer :)Wapentake
C
22

I found Joe's and Tom's answers very helpful, but there are a lot of useful details in the comments on those answers. Here's a summary of the two scenarios:

Ranges above 1

Here's the example code like Joe's, but with a higher range:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.axis([1, 10000, 1, 1000000])
ax.loglog()

plt.show()

That shows a plot like this, using scientific notation: Default plot with scientific notation

As in Joe's answer, I use a ScalarFormatter, but I also call set_scientific(False). That's necessary when the scale goes up to 1000000 or above.

import matplotlib.pyplot as plt
from matplotlib.ticker import ScalarFormatter

fig, ax = plt.subplots()
ax.axis([1, 10000, 1, 1000000])
ax.loglog()
for axis in [ax.xaxis, ax.yaxis]:
    formatter = ScalarFormatter()
    formatter.set_scientific(False)
    axis.set_major_formatter(formatter)

plt.show()

Plot with integer ticks

Ranges below 1

As in Tom's answer, here's what happens when the range goes below 1:

import matplotlib.pyplot as plt
from matplotlib.ticker import ScalarFormatter

fig, ax = plt.subplots()
ax.axis([0.01, 10000, 1, 1000000])
ax.loglog()
for axis in [ax.xaxis, ax.yaxis]:
    formatter = ScalarFormatter()
    formatter.set_scientific(False)
    axis.set_major_formatter(formatter)

plt.show()

That displays the first two ticks on the x axis as zeroes.

Plot with ticks labelled as zero

Switching to a FuncFormatter handles that. Again, I had problems with numbers 1000000 or higher, but adding a precision to the format string solved it.

import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter

fig, ax = plt.subplots()
ax.axis([0.01, 10000, 1, 1000000])
ax.loglog()
for axis in [ax.xaxis, ax.yaxis]:
    formatter = FuncFormatter(lambda y, _: '{:.16g}'.format(y))
    axis.set_major_formatter(formatter)

plt.show()

enter image description here

Christi answered 15/3, 2018 at 18:16 Comment(2)
I found your answer very helpful and I've used it in this partial answer to my own question. However my question asks why I am getting a specific behavior. Do you have any thoughts on that? Thanks!Countersubject
Need to add axis.set_minor_formatter(formatter) as well for minor ticks. Also, the 16g sometimes causes labels like 0.700000000000001. It helps to reduce the number of digits.Whatnot
D
7

regarding these questions

What if I wanted to change the numbers to, 1, 5, 10, 20?
– aloha Jul 10 '15 at 13:26

I would like to add ticks in between, like 50,200, etc.., How can I do that? I tried, set_xticks[50.0,200.0] but that doesn't seem to work! – ThePredator Aug 3 '15 at 12:54

But with ax.axis([1, 100, 1, 100]), ScalarFormatter gives 1.0, 10.0, ... which is not what I desire. I want it to give integers... – CPBL Dec 7 '15 at 20:22

you can solve those issue like this with MINOR formatter:

ax.yaxis.set_minor_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_minor_formatter(matplotlib.ticker.FormatStrFormatter("%.8f"))
ax.set_yticks([0.00000025, 0.00000015, 0.00000035])

in my application I'm using this format scheme, which I think solves most issues related to log scalar formatting; the same could be done for data > 1.0 or x axis formatting:

plt.ylabel('LOGARITHMIC PRICE SCALE')
plt.yscale('log')
ax.yaxis.set_major_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter("%.8f"))
ax.yaxis.set_minor_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_minor_formatter(matplotlib.ticker.FormatStrFormatter("%.8f"))
#####################################################
#force 'autoscale'
#####################################################
yd = [] #matrix of y values from all lines on plot
for n in range(len(plt.gca().get_lines())):
        line = plt.gca().get_lines()[n]
        yd.append((line.get_ydata()).tolist())
yd = [item for sublist in yd for item in sublist]
ymin, ymax = np.min(yd), np.max(yd)
ax.set_ylim([0.9*ymin, 1.1*ymax])
#####################################################
z = []
for i in [0.0000001, 0.00000015, 0.00000025, 0.00000035,
          0.000001, 0.0000015, 0.0000025, 0.0000035,
          0.00001,  0.000015, 0.000025, 0.000035,
          0.0001, 0.00015, 0.00025, 0.00035,
          0.001, 0.0015, 0.0025, 0.0035,
          0.01, 0.015, 0.025, 0.035,
          0.1, 0.15, 0.25, 0.35]:

    if ymin<i<ymax:
        z.append(i)
        ax.set_yticks(z)                

for comments on "force autoscale" see: Python matplotlib logarithmic autoscale

which yields:

enter image description here

then to create a general use machine:

# user controls
#####################################################
sub_ticks = [10,11,12,14,16,18,22,25,35,45] # fill these midpoints
sub_range = [-8,8] # from 100000000 to 0.000000001
format = "%.8f" # standard float string formatting

# set scalar and string format floats
#####################################################
ax.yaxis.set_major_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter(format))
ax.yaxis.set_minor_formatter(matplotlib.ticker.ScalarFormatter())
ax.yaxis.set_minor_formatter(matplotlib.ticker.FormatStrFormatter(format))

#force 'autoscale'
#####################################################
yd = [] #matrix of y values from all lines on plot
for n in range(len(plt.gca().get_lines())):
        line = plt.gca().get_lines()[n]
        yd.append((line.get_ydata()).tolist())
yd = [item for sublist in yd for item in sublist]
ymin, ymax = np.min(yd), np.max(yd)
ax.set_ylim([0.9*ymin, 1.1*ymax])

# add sub minor ticks
#####################################################
set_sub_formatter=[]
for i in sub_ticks:
    for j in range(sub_range[0],sub_range[1]):
        set_sub_formatter.append(i*10**j)
k = []
for l in set_sub_formatter:
    if ymin<l<ymax:
        k.append(l)
ax.set_yticks(k)
#####################################################

yields:

enter image description here

Disrate answered 12/3, 2017 at 15:37 Comment(0)
D
2

The machinery outlined in the accepted answer works great, but sometimes a simple manual override is easier. To get ticks at 1, 10, 100, 1000, for example, you could say:

ticks = 10**np.arange(4)
plt.xticks(ticks, ticks)

Note that it is critical to specify both the locations and the labels, otherwise matplotlib will ignore you.

This mechanism can be used to obtain arbitrary formatting. For instance:

plt.xticks(ticks, [ f"{x:.0f}" for x in ticks ])

or

plt.xticks(ticks, [ f"10^{int(np.log10(x))}" for x in ticks ])

or

plt.xticks(ticks, [ romannumerals(x) for x in ticks ])

(where romannumerals is an imagined function that converts its argument into Roman numerals).

As an aside, this technique also works if you want ticks at arbitrary intervals, e.g.,

ticks = [1, 2, 5, 10, 20, 50, 100]

etc.

Daina answered 24/10, 2021 at 16:18 Comment(0)
S
1
import matplotlib.pyplot as plt

plt.rcParams['axes.formatter.min_exponent'] = 2
plt.xlim(1e-5, 1e5)
plt.loglog()

plt.show()

This will become default for all plots in a session.

See also: LogFormatter tickmarks scientific format limits

Schriever answered 20/6, 2022 at 21:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.