Как выровнять линии сетки для двух масштабов оси Y с помощью Matplotlib?

Я рисую два набора данных с разными единицами измерения по оси Y. Есть ли способ сделать галочки и линии сетки выровненными по обеим осям 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')

Пример нежелательного поведения

Пример разыскиваемого поведения


person Artturi Björk    schedule 05.11.2014    source источник
comment
Вы ищете ax2.set_ylim((10, 20))?   -  person farenorth    schedule 05.11.2014
comment
Я ищу общий способ сделать это. то есть, если я получу любые два набора данных, как мне настроить график таким образом, чтобы линии сетки совпадали.   -  person Artturi Björk    schedule 05.11.2014
comment
вам просто нужно сделать это вручную, установив лимит и расстояние между галочками.   -  person Paul H    schedule 05.11.2014
comment
О, должен быть лучший способ, чем просто делать это вручную! Очень интересует общее решение этой проблемы!   -  person 8one6    schedule 06.11.2014
comment
Для тех, кто хочет выровнять какое-то значение на ax1 и ax2 — решение здесь: code. i-harness.com/en/q/9ff146   -  person grabantot    schedule 05.01.2019


Ответы (7)


Я не уверен, что это самый красивый способ сделать это, но он исправляет это одной строкой:

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()
person Leo    schedule 09.04.2015
comment
Чтобы не быть слишком придирчивым, но синяя линия теперь находится под сеткой, а красная линия — над сеткой. Поэтому я добавляю `ax2.grid(None)' после строки ax2.set_yticks и получаю те же отметки и линии сетки, но теперь обе линии находятся над сеткой. - person benten; 09.04.2016
comment
Это красиво, но, возможно, и опасно. Когда я попробовал это, тики выровнены, но линии не скорректированы соответствующим образом, поэтому диаграмма в конечном итоге предоставляет неверную информацию. Я не знаю, случается ли это только со мной; в любом случае лучше перепроверить - person West Yang; 10.07.2018
comment
Но вам не всегда везет, когда при вызове linspace() получаются красивые пробелы, а если он выполняет linspace(0,3,10), то галочки будут выглядеть некрасиво. - person Jason; 16.07.2018

Я мог бы решить эту проблему, деактивировав 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()

Рисунок результата

person arnaldo    schedule 17.03.2015
comment
К сожалению, это удаляет сетку с одной из осей, а не выравнивает галочки и сетку. В вашем примере у вас есть отметки в одинаковых положениях на обеих осях, но если бы другая ось была в другом положении, отметки этой оси не выровнялись бы с сеткой. - person Artturi Björk; 19.03.2015
comment
Но он ответит на ваш вопрос и воспроизведет фигуру так, как вы предполагали. Вы могли бы конкретизировать свой вопрос. - person arnaldo; 19.03.2015
comment
запустите свой код несколько раз, и вы поймете, что я имею в виду. - person Artturi Björk; 19.03.2015

Я написал эту функцию, которая принимает объекты осей Matplotlib 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.

person Scott Howard    schedule 09.03.2016
comment
Если мы хотим использовать matplotlib.ticker, как нам автоматически получить количество тиков, а не указывать nticks? - person Spinor8; 21.07.2017

Этот код обеспечит выравнивание сеток по обеим осям относительно друг друга без необходимости скрывать линии сетки из любого набора. В этом примере это позволяет вам сопоставить то, что имеет более тонкие линии сетки. Это основано на идее @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()
person John    schedule 11.07.2017
comment
Не обязательно назначать переменную a. - person Andi; 16.01.2020

Я создал метод для выравнивания меток нескольких осей Y (может быть больше 2), возможно, с разными масштабами по разным осям.

Ниже приведен пример рисунка: введите здесь описание изображения

Есть 3 оси Y, одна синяя слева, зеленая и красная справа. 3 кривые нанесены на ось 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()
person Jason    schedule 04.12.2020

Если вы используете метки осей, решение Лео может оттолкнуть их за пределы из-за к точности чисел в тиках.

Итак, в дополнение к чему-то вроде решения Лео (повторяется здесь),

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})

В нескольких тестовых случаях это, по-видимому, дает ожидаемый результат: как выстроенные галочки, так и метки полностью содержатся в выводе.

person Jonathan W.    schedule 14.03.2016

У меня была такая же проблема, за исключением того, что это было для вторичной оси x. Я решил, установив мою вторичную ось X равной пределу моей основной оси. В приведенном ниже примере не задан предел второй оси, равный первому: ax2 = ax.twiny() введите здесь описание изображения

Как только я установил предел второй оси равным первому ax2.set_xlim(ax.get_xlim()), вот мой результат: введите здесь описание изображения

person Hugo Alain Oliva    schedule 16.02.2018
comment
Я использую twinx(), и это единственный ответ, который мне подходит! - person Ragadabing; 15.08.2019
comment
это работает только в том случае, если масштаб одинаков между двумя осями - person Gio; 16.03.2020