pythonmatplotliblogarithmmatplotlibpyplot

PyPlot's ylim and yticks change for no reason


I am following some online code to add major yticks to a bar chart. However, I'm finding that the major yticks and the ylim changes for no reason, even though I don't add new major yticks:

import pandas as pd
import matplotlib.pyplot as plt
plt.close('all')
ax = pd.Series([600,1e3,1e4],index=['A','B','C']).plot.bar()
plt.yscale('log')

# This causes ylim and major yticks to change
if False: ax.set_yticks( ax.get_yticks(minor=False) )

I am using:

enter image description here

Afternote 2025-06-16 15:28 ET: I found that I can force the yticks and ylim back by saving ylim (ylim=plt.ylim()) before calling set_yticks, then restoring it afterward (plt.ylim(ylim)), but why is this necessary at all? In other words, why does calling set_yticks run different heuristics for determining ylim and yticks?

ANNEX: Exploration of sharey

A suggestion was made to set the plt.subplots argument sharey to True. This only makes sense if you have 2 subplots in the same figure and both figures have identical y-axes. Here is an example where they don't occur in the same figure and the y-axes are not identical, yet the yticks of the 2nd graph is set based on the log transformation of the yticks from the 1st graph. I was curious to see whether sharey=True could somehow be made to work:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import weibull_min

# Create 3 "Bin"'s of "Linear" data, 1000 points each
bins=['Alpha','Brave','Charlie']
scales=[1,10,100] # 1 scaling of the Weibull curve per bin
n_points = 1000 # Number of data points per bin
shape=0.5 # Curve shape
df = pd.concat([
    pd.DataFrame({
        'Bin':[bins[i_bin]]*n_points ,
        'Linear':weibull_min(c=shape,scale=scales[i_bin]).rvs(size=n_points)
    })
    for i_bin in range(3)
])

# Linear box plot, then yscaled as logarithmic
plt.close('all')
fig,(ax1,ax2) = plt.subplots(1,2,layout='constrained',sharey=True)
df.boxplot('Linear',by='Bin',ax=ax1)
plt.yscale('log')

# Box plot of log_10 transformation of the data
df['Log10'] = np.log10( df.Linear )
df.boxplot('Log10',by='Bin',ax=ax2)

This is random data so exact box plots extremities may vary.

enter image description here

So far, I haven't come up with a way to make it work. The boxes (if not the whiskesr) should look like this. The problem is that the 2nd plot box and whiskers are calculated based on logarithmic'd data (out of necessity, as per here and here) and won't present correctly on a scale for un-logarithmic'd data. Hence, we certainly don't want sharey=True.

Furthermore, in a more realistic usage, we want to plot the linear data and log transform it only to harvest the yticks. We then clear the axes and boxplot the log-transformed data to get properly calculated whiskers. So we don't necessarily have multiple subplots, and sharey is not applicable.


Solution

  • The order in which Matplotlib sets the properties of the y-axis is important, because calling set_ylim does not reset the y-ticks, while on the contrary calling set_yticks resets the y-axis limits.

    two sublots showing order is important

    import matplotlib.pyplot as plt
    
    fig, (ax1, ax2) = plt.subplots(1, 2)
    
    ax1.plot()
    ax1.set_yscale("log")
    ax1.set_xticks((-1, 0, 1))
    ax1.set_yticks([1.0e01, 1.0e02, 1.0e03, 1.0e04, 1.0e05, 1.0e06])
    ax1.set_ylim((100, 12000))
    print(ax1.get_yticks()) # [1.e+01 1.e+02 1.e+03 1.e+04 1.e+05 1.e+06]
    
    ax2.plot()
    ax2.set_yscale("log")
    ax2.set_xticks((-1, 0, 1))
    ax2.set_ylim((100, 12000))
    ax2.set_yticks([1.0e01, 1.0e02, 1.0e03, 1.0e04, 1.0e05, 1.0e06])
    print(ax2.get_ylim()) # (np.float64(10.0), np.float64(1000000.0))
    
    plt.show()