4

I want to plot a figure whose xtick values grow at the power of 2.

For example,

import pandas as pd
data = pd.DataFrame({
    'x': [2, 4, 8, 16, 32, 64],
    'y': [1, 2, 3, 4, 5, 6]
})

What I expect is a figure like this, enter image description here

For tidy data like the sample above, I can just create an auxiliary column x2 of 1~6 in this dataframe and then set the xticklabels of the figure to 2^1~2^6. However, this workaround doesn't apply to situations with other values like 3, 7 or 30.

It seems that matplotlib only supports a log scale. How can I achieve a tick with a power scale of 2?

M_x
  • 782
  • 1
  • 8
  • 26
mori
  • 55
  • 4

1 Answers1

2

This is a particularly trick question (which I did not expected to be ^^).

OK, let's start with a few hints for reading: You want to set the x/y scale: .matplotlib.axes.Axes.set_yscale(). While there are a couple of standard scales (the default is obviously 'liner', one can set a custom scale. Here are some nice examples.

Basically, you define two functions with the forward transformation and with the inverse of it. Afterwards you need to set the ticks correctly (because you apply the transformation after plotting, the ticks remain the same (but not at the same position due to the transformation). One has two options for this:

  • setting the ticks manually matplotlib.axes.Axes.set_xticks(), or
  • by setting the locator of the axis: matplotlib.axes.Axes.xaxis.set_major_locator(). This is recommended if you use grids. But as my knowledge is limited, I appreciate a more detailed explanation (because now I am also curious about this feature ^^)

And now comes the tricky part: formatting the tick-labels to represent a '2^x'. I didn't came across a better idea than setting them explicitly as strings. It seems that one can change the general format only within restricted limits, see matplotlib.pyplot.ticklabel_format(), where one can choose if and when a scientific notation should be used (i.e. displaying a '10^x' at the bottom right). Let me know if there is a more generic solution for this.

import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.ticker import FixedLocator

# create dummy data
df = pd.DataFrame({
    'x': [2**x for x in range(1,10)],
    'y': list(range(1,10))
})

def forward(x):
    return np.log2(x)


def inverse(x):
    return 2**x

# open figure
fig, ax = plt.subplots(2,2)
axs = ax.flatten()
for i in range(0,4):
    # plot data
    axs[i].plot(df['x'],df['y'])
    if i > 0:
        # set scale function
        axs[i].set_xscale('function', functions=(forward,inverse))
    if i > 1:
        # set ticks
        # - OPTION 1
        axs[i].set_xticks(df['x'])
        # - OPTION 2
      axs[i].xaxis.set_major_locator(FixedLocator(2**np.arange(1,10)))
    if i > 2:
        # est tick labels
        axs[i].set_xticklabels( [f"2^{j:.0f}" for j in np.log2(df['x'])] )

plt.show()
Benjamin Loison
  • 3,782
  • 4
  • 16
  • 33
max
  • 3,915
  • 2
  • 9
  • 25