如何对齐两个y轴比例尺的网格线?

63

我正在绘制两个具有不同单位的数据集,有没有办法使两个y轴上的刻度和网格线对齐?

第一张图片展示了我得到的结果,第二张图片展示了我想要得到的结果。

这是我用来绘图的代码:

import seaborn as sns
import numpy as np
import pandas as pd

np.random.seed(0)
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax1.plot(pd.Series(np.random.uniform(0, 1, size=10)))
ax2 = ax1.twinx()
ax2.plot(pd.Series(np.random.uniform(10, 20, size=10)), color='r')

Example of unwanted behavior

Example of wanted behavior


2
你在寻找 ax2.set_ylim((10, 20)) 吗? - farenorth
我正在寻找一种通用的方法来实现这个目标。也就是说,如果我得到任何两个数据集,如何设置图形,使网格线重合。 - Artturi Björk
你只需要手动设置限制和刻度间距即可完成此操作。 - Paul H
12
一定有比手动做更好的方法!非常想知道这个问题的通用解决方案! - 8one6
2
对于那些想要在ax1和ax2上对齐某个值的人 - 解决方案在这里:https://code.i-harness.com/en/q/9ff146 - grabantot
8个回答

33

我不确定这是否是最漂亮的方法,但它可以用一行代码修复它:

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd

np.random.seed(0)
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax1.plot(pd.Series(np.random.uniform(0, 1, size=10)))
ax2 = ax1.twinx()
ax2.plot(pd.Series(np.random.uniform(10, 20, size=10)), color='r')

# ADD THIS LINE
ax2.set_yticks(np.linspace(ax2.get_yticks()[0], ax2.get_yticks()[-1], len(ax1.get_yticks())))

plt.show()

11
不想太挑剔,但是蓝色的线现在在格线下方,红色的线在格线上方。所以在ax2.set_yticks行后添加“ax2.grid(None)”,可以得到相同的刻度和网格线,但现在两条线都在格线上方。 - benten
1
这很漂亮,但也可能很危险。当我尝试过这个时,刻度线是对齐的,但是线条没有相应地调整,因此图表最终提供了错误的信息。我不知道这是否只发生在我身上;无论如何,最好再次检查。 - Code Learner
1
但是当调用“linspace()”时,你并不总是能够得到漂亮的间隔值,如果它执行的是“linspace(0,3,10)”,那么刻度看起来会很丑。 - Jason

16
我编写了一个函数,它接受Matplotlib axes对象ax1、ax2和浮点数minresax1 minresax2:
def align_y_axis(ax1, ax2, minresax1, minresax2):
    """ Sets tick marks of twinx axes to line up with 7 total tick marks

    ax1 and ax2 are matplotlib axes
    Spacing between tick marks will be a factor of minresax1 and minresax2"""

    ax1ylims = ax1.get_ybound()
    ax2ylims = ax2.get_ybound()
    ax1factor = minresax1 * 6
    ax2factor = minresax2 * 6
    ax1.set_yticks(np.linspace(ax1ylims[0],
                               ax1ylims[1]+(ax1factor -
                               (ax1ylims[1]-ax1ylims[0]) % ax1factor) %
                               ax1factor,
                               7))
    ax2.set_yticks(np.linspace(ax2ylims[0],
                               ax2ylims[1]+(ax2factor -
                               (ax2ylims[1]-ax2ylims[0]) % ax2factor) %
                               ax2factor,
                               7))

它计算并设置刻度,使得有七个刻度。最低的刻度对应于当前最低的刻度,并增加最高的刻度,使得每个刻度之间的分离是minrexax1或minrexax2的整数倍。
为了使其更通用,您可以通过将所有出现的“7”更改为所需的总刻度数,并将“6”更改为总刻度数减1来设置所需的总刻度数。
我提交了一个拉取请求,将一些内容合并到matplotlib.ticker.LinearLocator中:

https://github.com/matplotlib/matplotlib/issues/6142

在未来(Matplotlib 2.0 或许?)尝试:

import matplotlib.ticker
nticks = 11
ax1.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))
ax2.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))

那应该可以正常工作,并选择方便的刻度来调整两个y轴。

1
如果我们想使用matplotlib.ticker,如何自动获取刻度数量,而不必指定nticks? - Spinor8
非常感谢您的巨大贡献。我可以确认现在在matplotlib中已经可以正常工作了。 - undefined

16

我可以通过在网格的一个轴上取消激活ax.grid(None)来解决它:

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd

fig = plt.figure()
ax1 = fig.add_subplot(111)
ax1.plot(pd.Series(np.random.uniform(0, 1, size=10)))
ax2 = ax1.twinx()
ax2.plot(pd.Series(np.random.uniform(10, 20, size=10)), color='r')
ax2.grid(None)

plt.show()

结果图


21
不幸的是,这会从一个轴上移除网格而不是对齐刻度和网格。在您的示例中,恰好在两个轴上都具有相同位置的刻度,但如果另一个轴上的刻度位置不同,则该轴的刻度将无法与网格对齐。 - Artturi Björk
但它回答了你的问题并以你所期望的方式重现了图形。你可以更具体地提出你的问题。 - arnaldo
5
运行你的代码几次,你就会明白我的意思。 - Artturi Björk

10
我创建了一种方法来对齐多个y轴的刻度(可能超过2个),即使不同轴上的比例也可能不同。
下面是一个示例图: enter image description here 这里有3个y轴,一个在左侧是蓝色,另外两个在右侧一个是绿色一个是红色。三条曲线分别用相应的颜色绘制到y轴上。请注意它们具有非常不同的数量级。
- 左边的图:没有对齐。 - 中间的图:在每个y轴的(大约)下限处对齐。 - 右边的图:在指定的值上对齐:0为蓝色,2.2*1e8为红色,44为绿色。这些是任意选择的。
我的做法是将每个y数组缩放至1-100的范围内,然后将所有缩放后的y值合并成单个数组,从中使用MaxNLocator创建一组新的刻度。 然后使用相应的缩放因子将此新的刻度重新缩放以获得每个轴的新刻度。 如果需要特定的对齐,则在缩放之前移动y数组,然后在缩放后移回来。
完整代码在这里(关键函数是alignYaxes()):
import matplotlib.pyplot as plt
import numpy as np

def make_patch_spines_invisible(ax):
    '''Used for creating a 2nd twin-x axis on the right/left

    E.g.
        fig, ax=plt.subplots()
        ax.plot(x, y)
        tax1=ax.twinx()
        tax1.plot(x, y1)
        tax2=ax.twinx()
        tax2.spines['right'].set_position(('axes',1.09))
        make_patch_spines_invisible(tax2)
        tax2.spines['right'].set_visible(True)
        tax2.plot(x, y2)
    '''

    ax.set_frame_on(True)
    ax.patch.set_visible(False)
    for sp in ax.spines.values():
        sp.set_visible(False)

def alignYaxes(axes, align_values=None):
    '''Align the ticks of multiple y axes

    Args:
        axes (list): list of axes objects whose yaxis ticks are to be aligned.
    Keyword Args:
        align_values (None or list/tuple): if not None, should be a list/tuple
            of floats with same length as <axes>. Values in <align_values>
            define where the corresponding axes should be aligned up. E.g.
            [0, 100, -22.5] means the 0 in axes[0], 100 in axes[1] and -22.5
            in axes[2] would be aligned up. If None, align (approximately)
            the lowest ticks in all axes.
    Returns:
        new_ticks (list): a list of new ticks for each axis in <axes>.

        A new sets of ticks are computed for each axis in <axes> but with equal
        length.
    '''
    from matplotlib.pyplot import MaxNLocator

    nax=len(axes)
    ticks=[aii.get_yticks() for aii in axes]
    if align_values is None:
        aligns=[ticks[ii][0] for ii in range(nax)]
    else:
        if len(align_values) != nax:
            raise Exception("Length of <axes> doesn't equal that of <align_values>.")
        aligns=align_values

    bounds=[aii.get_ylim() for aii in axes]

    # align at some points
    ticks_align=[ticks[ii]-aligns[ii] for ii in range(nax)]

    # scale the range to 1-100
    ranges=[tii[-1]-tii[0] for tii in ticks]
    lgs=[-np.log10(rii)+2. for rii in ranges]
    igs=[np.floor(ii) for ii in lgs]
    log_ticks=[ticks_align[ii]*(10.**igs[ii]) for ii in range(nax)]

    # put all axes ticks into a single array, then compute new ticks for all
    comb_ticks=np.concatenate(log_ticks)
    comb_ticks.sort()
    locator=MaxNLocator(nbins='auto', steps=[1, 2, 2.5, 3, 4, 5, 8, 10])
    new_ticks=locator.tick_values(comb_ticks[0], comb_ticks[-1])
    new_ticks=[new_ticks/10.**igs[ii] for ii in range(nax)]
    new_ticks=[new_ticks[ii]+aligns[ii] for ii in range(nax)]

    # find the lower bound
    idx_l=0
    for i in range(len(new_ticks[0])):
        if any([new_ticks[jj][i] > bounds[jj][0] for jj in range(nax)]):
            idx_l=i-1
            break

    # find the upper bound
    idx_r=0
    for i in range(len(new_ticks[0])):
        if all([new_ticks[jj][i] > bounds[jj][1] for jj in range(nax)]):
            idx_r=i
            break

    # trim tick lists by bounds
    new_ticks=[tii[idx_l:idx_r+1] for tii in new_ticks]

    # set ticks for each axis
    for axii, tii in zip(axes, new_ticks):
        axii.set_yticks(tii)

    return new_ticks

def plotLines(x, y1, y2, y3, ax):

    ax.plot(x, y1, 'b-')
    ax.tick_params('y',colors='b')

    tax1=ax.twinx()
    tax1.plot(x, y2, 'r-')
    tax1.tick_params('y',colors='r')

    tax2=ax.twinx()
    tax2.spines['right'].set_position(('axes',1.2))
    make_patch_spines_invisible(tax2)
    tax2.spines['right'].set_visible(True)
    tax2.plot(x, y3, 'g-')
    tax2.tick_params('y',colors='g')

    ax.grid(True, axis='both')

    return ax, tax1, tax2

#-------------Main---------------------------------
if __name__=='__main__':

    # craft some data to plot
    x=np.arange(20)
    y1=np.sin(x)
    y2=x/1000+np.exp(x)
    y3=x+x**2/3.14

    figure=plt.figure(figsize=(12,4),dpi=100)

    ax1=figure.add_subplot(1, 3, 1)
    axes1=plotLines(x, y1, y2, y3, ax1)
    ax1.set_title('No alignment')

    ax2=figure.add_subplot(1, 3, 2)
    axes2=plotLines(x, y1, y2, y3, ax2)
    alignYaxes(axes2)
    ax2.set_title('Default alignment')

    ax3=figure.add_subplot(1, 3, 3)
    axes3=plotLines(x, y1, y2, y3, ax3)
    alignYaxes(axes3, [0, 2.2*1e8, 44])
    ax3.set_title('Specified alignment')

    figure.tight_layout()
    figure.show()

1
这太棒了,非常感谢!向您致敬! - SimTae
当我使用这个时,第二个轴在Y轴上会被图形压缩。你知道为什么会发生这种情况吗? - ifly6
@ifly6,仅从您的描述中无法判断。 - Jason
我也遇到了@ifly6提出的问题。对于刻度为[0, 2, 4, 6, 8, 10][0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4]的轴,它将第二个轴的范围更改为[0, 10]而不是[0, 5] - undefined
log10 替换为 _log2_,将 10** 替换为 _2**_,可以得到坐标轴范围 [0, 16][0, 4]。我认为这对于这种情况来说更好,但比 [0, 10][0, 5] 差。 - undefined

2

这段代码将确保两个轴的网格对齐,而不必隐藏任何一组的网格线。在本例中,它允许您匹配具有更细网格线的轴。这是基于@Leo的想法构建的。希望能帮到您!

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd

fig = plt.figure()
ax1 = fig.add_subplot(111)
ax1.plot(pd.Series(np.random.uniform(0,1,size=10)))
ax2 = ax1.twinx()
ax2.plot(pd.Series(np.random.uniform(10,20,size=10)),color='r')
ax2.grid(None)

# Determine which plot has finer grid. Set pointers accordingly
l1 = len(ax1.get_yticks())
l2 = len(ax2.get_yticks())
if l1 > l2:
  a = ax1
  b = ax2
  l = l1
else:
  a = ax2
  b = ax1
  l = l2

# Respace grid of 'b' axis to match 'a' axis
b_ticks = np.linspace(b.get_yticks()[0],b.get_yticks()[-1],l)
b.set_yticks(b_ticks)

plt.show()

不需要给变量a赋值。 - Andi

0

如果您正在使用轴标签,Leo的解决方案可能会将它们推到一边,因为刻度中数字的精度。

因此,除了像Leo的解决方案(在此重复)之类的东西之外,

ax2.set_yticks(np.linspace(ax2.get_yticks()[0],ax2.get_yticks()[-1],len(ax1.get_yticks())))

你可以使用在这个答案中提到的autolayout设置;例如,在你的脚本中,你可以更新rcParams
from matplotlib import rcParams
rcParams.update({'figure.autolayout': True})

在一些测试用例中,这似乎产生了预期的结果,具有对齐的刻度线和标签完全包含在输出中。

0
  1. 修复两个轴的限制(从任意数字到任意数字)
  2. 将两个轴分成相同的n个部分
ax1.set_ylim(a,b)
ax1.set_yticks(np.linspace(a,b, n))

ax2.set_ylim(c,d)
ax2.set_yticks(np.linspace(c,d, n))

0

我遇到了同样的问题,只不过这是针对第二个x轴的。我通过将我的第二个x轴设置为主轴的限制来解决了这个问题。下面的示例是没有将第二个轴的限制设置为第一个轴的:ax2 = ax.twiny() enter image description here

一旦我将第二个轴的限制设置为第一个轴ax2.set_xlim(ax.get_xlim()),这就是我的结果: enter image description here


我正在使用twinx(),这是唯一对我有效的答案! - Ragadabing
只有当两个轴之间的比例相同时,此方法才有效。 - CAPSLOCK

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接