23 August 2020

Задача двух осей и четырех графиков c Plotly

В части Plotly теперь у нас есть все, чтобы решить задачу двух осей и четырех графиков. Рассмотрим как это сделать.

Сначала создаем холст с двумя графиками по вертикали, указывая, что каждый будет иметь по две оси:

fig = make_subplots(rows=2, cols=1 , 

  subplot_titles = ('Upper', 'Lower'),
  specs=[[{"secondary_y":True}],[{"secondary_y":True}]])


Теперь создаем четрые точечных диаграммы - две для модельных данных и две для реальных данных:

trc1 = go.Scatter(
    x = x, y = y,
    name = 'Scatter sample',
    line = dict(color= 'green'),
    marker_symbol ='x-open',
    mode ='markers+lines'
)

trc2 = go.Scatter(
    x = x, y = y2,
    name = 'Scatter sample 2',
    line = dict(color= 'blue'),
    marker_symbol ='x-open',
    mode ='markers+lines'
)

trc1r = go.Scatter(
    x = x, y = yr,
    name = 'Scatter sample real',
    line = dict(color= 'red', dash='dot'),
    marker_symbol ='diamond-open',
    mode ='markers+lines'
)

trc2r = go.Scatter(
    x = x, y = yr2,
    name = 'Scatter sample 2 real',
    line = dict(color= 'black', dash='dot'),
    marker_symbol ='diamond-open',
    mode ='markers+lines'
)

Далее добавляем диаграммы на верхний график, указывая для второго набора ( y2 ) вторую ось:

fig.add_trace ( trc1 , row = 1, col =1, secondary_y=False )
fig.add_trace ( trc2 , row = 1, col =1, secondary_y=True )
fig.add_trace ( trc1r , row = 1, col =1, secondary_y=False )
fig.add_trace ( trc2r , row = 1, col =1, secondary_y=True )


А для реальных сделаем наоборот - укажем вторую ось для первого набора ( y ) :

fig.add_trace ( trc2 , row = 2, col =1, secondary_y=False )
fig.add_trace ( trc1 , row = 2, col =1, secondary_y=True )
fig.add_trace ( trc2r , row = 2, col =1, secondary_y=False )
fig.add_trace ( trc1r , row = 2, col =1, secondary_y=True )


Подписываем оси для верхнего графика:

fig.update_xaxes( title = 'Upper X', row =1 , col =1 )
fig.update_yaxes( title = 'Upper Primary Y' , secondary_y=False , row =1 , col =1  )
fig.update_yaxes( title = 'Upper Secondary Y' , secondary_y=True  , row =1 , col =1  )


Подписываем оси для нижнего графика:

fig.update_xaxes( title = 'Lower X', row =2 , col =1 )
fig.update_yaxes( title = 'Lower Primary Y' , secondary_y=False , row =2 , col =1  )
fig.update_yaxes( title = 'Lower Secondary Y' , secondary_y=True  , row =2 , col =1  )


А вот и результат:

Как можно видеть масштаб основных осей ( как и дополнительных осей ) на первом и втором графике - разный. А вот легенда дублируется - потому что мы ее не меняли при добавлении на нижний график.
Одноко легко убедиться, что легенды представлены по графикам - именно по этому их два набора.
То есть, скажем, если мы уберем верхнюю:

 то она убертся только на верхнем графике, а на нижнем она останется на месте. И чтобы ее убрать надо убрать вторую легенду.
Одиним словом, с помощью Plotly два графика с двумя осями также легко  отображаются.

15 August 2020

Plotly: вторая ось

Для задания второй оси в Plotly используется несколько другой подход, нежели в matplotlib - там ось не клонируется ( вызовом  twinx ), а задается ее наличии при создании холста.
Ранее мы использовали метод go.Figure для его создания, но он не принимает аргументов для определения второй оси. Поэтому надо использовать другой метод make_subplots:

from plotly.subplots import make_subplots
fig = make_subplots(specs=[[{"secondary_y":True}]])


Как можно видеть, задание второй оси регулируется параметром secondary_y, но он оборачивается в хитрый тип - список списков. Зачем так сделали разработчки Plotly - непонятно, но именно так это работает.

Далее мы также создаем две точечных диаграммы:

trc1 = go.Scatter(
    x = x, y = y,
    name = 'Scatter sample',
    line = dict(color= 'green', dash='dot'),
    marker_symbol ='diamond-open',
    mode ='markers+lines'
)

trc2 = go.Scatter(
    x = x, y = y2,
    name = 'Scatter sample 2',
    line = dict(color= 'blue', dash='dot'),
    marker_symbol ='diamond-open',
    mode ='markers+lines'
)


И при добавлении указываем на какую ось добавлять.
Первый - на основную:

fig.add_trace ( trc1 , secondary_y=False )


После этого добавления у нас появляется данные первой диаграммы:

Как можно видеть - второй оси пока еще нет.
Теперь добавляем вторую диаграмму на вторую ось:

fig.add_trace ( trc2 ,  secondary_y=True )

И вот тут ось уже появляется:

Для подписи оси X используется тот же метод: 

fig.update_xaxes( title = 'My new X axis' )

А вот для подписи осей Y уже надо указывать какую:

fig.update_yaxes( title = 'Primary Y' , secondary_y=False  )
fig.update_yaxes( title = 'Secondary Y' , secondary_y=True  )


Вот результат:


Таким образом создание второй оси в Plotly делается несколько по-другому, но тоже несложно.

13 August 2020

Юпитер и интерактивность: matplotlib наносит отетный удар

Ранее в посте Юпитер и интерактивность - темная строна я указал, что в простом случае matplotlib не работает нормально интерактивно. Однако чтобы это вернуть достаточно установить ipympl. Чтобы с гарантией это было в Юпитере - ставим через конду:

conda install -c conda-forge ipympl

Теперь это есть в системе:

Берем код из ранее упомянутого примера и добавляем в самое начало немного магии:

%matplotlib widget

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

button = widgets.Button(description='My Button')

sF = widgets.FloatSlider(
         value=1,
         min=0,
         max=10.0,
         step=0.1,)
sPhase = widgets.FloatSlider(
         value=1,
         min=0,
         max=10.0,
         step=0.1,)


Теперь осталось сделать обработчик кнопки, привязать его к кнопке и разместить элементы. Для размещения я использую Vbox, то есть в нем элементы будут размещаться последовательно вертикально. Туда же в качестве виджета я помещаю и вывод - widgets.Output() :

outt = widgets.Output()

def on_butt_clicked(b):
    with outt:
        clear_output()
        global sF,sPhase
        plot_f(sF.value, sPhase.value)
       
button.on_click(on_butt_clicked)
widgets.VBox([sF, sPhase, button, outt ])


И вот теперь все начинает работать:

Меняем значение ползунка и еще раз нажимаем кнопку:

Как можно видеть, заголовок у графика поменялся с Figure 3 на Figure 4. Однако картинка осталась на месте, правильно отображенная и не стала клонироваться. Более того, как и в случае режима notebook, задаваемого магическими командами, у графика есть такая же панель инструментов, только слева, а не внизу.
Правда, если изменить масштаб/сдвинуть график, то после нажатия кнопки это все пропадет в силу того, что картинка полностью перерисовывается. Тем не менее путем доустановки отсутствующего в штатной комплктации ipympl можно использовать интерактивные возможности Юпитера и с matplotlib.

12 August 2020

Matplotlib и Юпитер: немного о магии

Решение задачи двух осей и четырех графиков всем хорошо, кроме как внешним видом - графики получились маленькие:

Варианта разрешить этот недостаток ровно два - явно поменять размер графиков или использовать магические (magic) команды. Рассмотрим их по порядку.

Чтобы изменить размер графика надо изменить параметр figsize у холста.
Если делать это без сохранения объекта холста ( figure ) то вызов будет такой:

plt.rcParams["figure.figsize"] = [10,6]

Здесь я изменил размеры по горизонтали и вертикали на 10 и 6 соответственно, когда по умолчанию эти размеры 6.4 и 4.8.
В случае использования объекта холста можно сделать это прямо при вызове subplots вот так:

fig, ( ax_up, ax_btm) = plt.subplots(2,1,figsize=(10,6))

Делать это надо до вызовов рисования. Результат будет один и тот же - график увеличится в размерах:

Эмпирическим путем можно подобрать нужный размер графика.
Второй способ подразумевает использование магических команд, которые начинаются с символа процента, для указаний работы реализации. Для библиотеки Matplotlib они начинаются так:

%matplotlib [что-то тут]

Два основных варианта это inline и notebook, то есть так:

%matplotlib inline
%matplotlib notebook


Случай inline - это как если бы мы ничего не указывали - неинтерактивный график. Нужен разве что если мы использовали notebook и хотим вернуться обратно.
А вот notebook добавляет интерактивности:

 Как можно видеть сверху появился заголовок Figure 1 с кнопкой. Эта кнопка - выключает интерактивность и если ее выключить, то более уже к ней не вернуться.

Внизу справа ( на нее показывет красная стрелка ) - символ в виде треугольника - это для изменения размеров - если на на него встать, нажать мышь и двигать, то размер будет меняться.
Внизу слева - меню управления.
Слева направо:

  • Символ в виде дома - вернуться к исходному отображению ( Home )
  • Стрелка влево - вернуться на шаг изменения назад ( Back, аналог Undo )
  • Стрелка вправо - сделать изменени вперед ( после возравщения на шаг назад, Forward, аналог Redo )
  • Крест со стрелками - смещение ( Pan )
  • Пустой квадрат - масштаб ( Zoom )
  • Символ дискеты - сохранить изображение ( Download )


Изменение размеров и меню слева работают незвисимо - то есть после изменения размера кнопка назад не вернет его предыдущее значение.
У левого меню основные операции это смещение и масштаб - их можно отменять или возвращать.
Для изменения масштаба надо выбрать прямоугльник и пойти на нужный график - там курсор превратится в крестик, можно выбрать прямоугольную область и она будет показана во весь график: 

Вот график после такого масштабирования:

При помощи сдвига можно сдвинуть график к интересующей его части - тогда курсор меняется на руку и при удержании кнопки мыши и ее перемещении двигается и сам график:

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

Сохранение изображения оиткрывает картинку в новой вкладке в браузере и его можно сохранить как файл. Одноко оно работает с ошибками - прямо после масштабирования и/или сдвига картинка показывается криво:

Исправить это можно слегка изменив размер картинки иконкой справа  и снова нажав сохранение:

Таким образом в режиме notebook можно интерактивно просматривать более детально части графиков, хотя он и отстает от возможностей Plotly.

Дополнительно можно посмотеть информацию тут Using matplotlib in jupyter notebooks - comparing methods and some tips
 

11 August 2020

Задача двух осей и четырех графиков с matplotlib: решение

До конца задачи осталось совсем немного - надо просто сложить методы для двух областей с методами для двух графиков вместе.
Если это решать последовательным методом то вот код:

# верхняя область
plt.subplot(211)
plt.plot( x,y, label='Model', marker='x')
plt.plot( x,yr ,color = 'green', marker='D', linestyle='dashed', label='Real')
plt.xlabel('X')
plt.ylabel('Y', color='red', fontsize=14)
plt.title('First Subplot', color='green', fontsize=14)
plt.legend(loc ='lower right')

plt.gca().twinx()
plt.plot( x,y2,color = 'black',label='Model Y2', marker='x')
plt.plot( x,yr2 ,color = 'red', marker='o', linestyle='dashed', label='Real Y2')
plt.ylabel('Y2')
plt.legend(loc = 'upper center')

# нижняя область
plt.subplot(212)
plt.plot( x,y, label='Model 2 ', marker='x')
plt.plot( x,yr ,color = 'green', marker='D', linestyle='dashed', label='Real 2')
plt.xlabel('X')
plt.ylabel('Y', color='red', fontsize=14)
plt.title('Second Subplot', color='green', fontsize=14)
plt.legend(loc ='lower right')

plt.gca().twinx()
plt.plot( x,y2,color = 'black',label='Model 2 Y2', marker='x')
plt.plot( x,yr2 ,color = 'red', marker='o', linestyle='dashed', label='Real 2 Y2')
plt.ylabel('Y2')
plt.legend(loc = 'upper center')
plt.subplots_adjust(hspace=0.70)


Результат выполнения:

 

Если это решать путем создания переменных для холста и для осей то вот код:

fig, ( ax_up, ax_btm) = plt.subplots(2,1)
# верхняя область
ax_up.plot( x,yr ,color = 'green', marker='D', linestyle='dashed', label='Real')
ax_up.plot( x,y, label='Model', marker='x')

ax_up.set_xlabel('X')
ax_up.set_ylabel('Y', color='red', fontsize=14)
ax_up.set_title('First Subplot', color='green', fontsize=14)
ax_up.legend(loc ='lower right')

ax_up2 = ax_up.twinx()
ax_up2.plot( x,y2,color = 'black',label='Model Y2', marker='x')
ax_up2.plot( x,yr2 ,color = 'red', marker='o', linestyle='dashed', label='Real Y2')
ax_up2.set_ylabel('Y2')
ax_up2.legend(loc = 'upper center')

# нижняя область
ax_btm.plot( x,yr ,color = 'green', marker='D', linestyle='dashed', label='Real')
ax_btm.plot( x,y, label='Model', marker='x')

ax_btm.set_xlabel('X')
ax_btm.set_ylabel('Y', color='red', fontsize=14)
ax_btm.set_title('Second Subplot', color='green', fontsize=14)
ax_btm.legend(loc ='lower right')

ax_btm2 = ax_btm.twinx()
ax_btm2.plot( x,y2,color = 'black',label='Model Y2', marker='x')
ax_btm2.plot( x,yr2 ,color = 'red', marker='o', linestyle='dashed', label='Real Y2')
ax_btm2.set_ylabel('Y2')
ax_btm2.legend(loc = 'upper center')

plt.subplots_adjust(hspace=0.70)

При этом получается такая же картинка.
Задача решена.

Задача двух осей и четырех графиков: две оси с matplotlib

Следующий этап этой задачи - создание второй оси. В matplotlib это достигается вызовом метода оси twinx, который тоже возвращает ось, только вторую. В простом случае ее можно и не запоминать - она становится текущей. Однако, чтобы ее получить в простом случае надо получить саму ось вызовом gca. При этом matplotlib не допускает интерактивности в части построения частей сложного графика.
В самом деле, давайте рассмотрим пример. Сначала создадим пару графиков - модельный и реальный - для одной оси:

plt.plot( x,y, label='Model', marker='x')
plt.plot( x,yr ,color = 'green', marker='D', linestyle='dashed', label='Real')
plt.xlabel('X')
plt.ylabel('Y', color='red', fontsize=14)
plt.title('First Subplot', color='green', fontsize=14)


Вот вывод такой части кода:

А теперь попробуем создать вторую ось в другой ячейке. Вот что мы увидим: 

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

plt.gca().twinx()
plt.plot( x,y2,color = 'black',label='Model Y2', marker='x')
plt.plot( x,yr2 ,color = 'red', marker='o', linestyle='dashed', label='Real Y2')
plt.ylabel('Y2')


то мы увидим только их - предыдущие данные для первой оси пропадут:

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

Теперь попробуем добавить легенды графиков - их придется делать два раза ибо каждый вызов будет относиться к отдельной оси. Ранее я использовал вызов plt.legend().set_visible(True) но оказывается можно и проще - просто plt.legend(). Вот что получим:

Как можно видеть, подписи легенд перехлестнулись с графиками. Ровно как и в случае с подписями областей тут, увы, придется править руками. В вызов plt.legend() можно поставить параметр loc и указать место расположения строкой:

Более детально о подобном форматировании можно почитать тут Customizing Plot Legends

Второй вариант создания второй оси - явный вызов методов у осей. В этом случае надо создать холст и ось вызовом subplots() не передавая ему никаких параметров для разбивки по областям.
Вызов twinx в этом случае производится непосредственно у оси, далее новая ось запоминается и вызов рисования идет ее методами:

fig, ax  = plt.subplots()
ax.plot( x,yr ,color = 'green', marker='D', linestyle='dashed', label='Real')
ax.plot( x,y, label='Model', marker='x')

ax.set_xlabel('X')
ax.set_ylabel('Y', color='red', fontsize=14)
ax.set_title('First Subplot', color='green', fontsize=14)
ax.legend(loc ='lower right')

ax2 = ax.twinx()
ax2.plot( x,y2,color = 'black',label='Model Y2', marker='x')
ax2.plot( x,yr2 ,color = 'red', marker='o', linestyle='dashed', label='Real Y2')
ax2.set_ylabel('Y2')
ax2.legend(loc = 'upper center')


Данный код рисует в точности такой же график с двумя осями, как и выше.

09 August 2020

Plotly-области: немного о подписях графиков

Ранее в посте Plotly : области. Делаем два графика мы сделали два графика.

Подписи для каждого из них мы задавали прямо при разбиении областей:

fig = make_subplots(rows=2, cols=1 , subplot_titles = ('Upper', 'Lower') )

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

fig.update_yaxes( title = 'My second Y axis' , row =2 , col =1 )
fig.update_layout( title = 'My New Chart' )


Однако с подписями областей такой трюк не пройдет.  Для начала, давайте поймем где же они хранятся. Это можно сделать печатью содержимого разметки фигуры, т.е. свойства layout:

Как можно видеть, они находятся под ключом annotations в виде списка словарей, где ключом является  text. Поэтому логично, что если мы хотим изменить подпись верхнего графика, то надо брать нулевой элемент:

 

Однако Plotly предоставляет и иной способ ее поменять - по свойству элемента.

Если посмотреть описание по документации plotly.graph_objects.Figure :
 

 

то, при том что там как бы есть парамеры row/col, все равно ничего на выйдет - все указанные методы и попытки поменять подпись таким образом, не работают:

# попытка 1 - не работает
fig.update_annotations( patch= dict(text = 'Second upper test ') , row =1 , col =1)

# попытка 2 - не работает
fig.update_annotations( text = 'Second upper test ' , row =1 , col =1)

# попытка 3 - не работает
fig.update_annotations( dict(text = 'Second upper test ') , row =1 , col =1)


Однако удивительно, что работает ссылка на элемент через селектор - т.е. ему надо указать словарь со свойствами желательного элемента. Как можно видеть наиболее яркое, кроме ключей x и y, у нас только свойство text. Так и попробуем:

fig.update_annotations( text = 'Second upper test' , selector = dict(text= 'Test Upper') )

И вуаля! - это работает:

 

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


07 August 2020

Plotly : области. Делаем два графика.

 Следующим шагом мы сделаем две области, то есть построим два графика на одном холсте.
В Plotly функция для создания областей в общем-то схожая.

Для начала ее экспортируем:

from plotly.subplots import make_subplots


Далее создаем две области по вертикали, то есть одна колонка и две строчки. При создании областей можно сразу их подписать:

fig = make_subplots(rows=2, cols=1 , subplot_titles = ('Upper', 'Lower') )

Создаем две точечных диаграммы:

trc1 = go.Scatter(
    x = x, y = y,
    name = 'Scatter sample',
    line = dict(color= 'green', dash='dot'),
    marker_symbol ='diamond-open',
    mode ='markers+lines'
)
trc2 = go.Scatter(
    x = x, y = y2,
    name = 'Scatter sample 2',
    line = dict(color= 'green', dash='dot'),
    marker_symbol ='diamond-open',
    mode ='markers+lines'
)


Добавляем первую диаграмму:

fig.add_trace ( trc1 , row = 1, col =1 )


Сразу же после исполнения появляется график, но с одной диаграммой: 

Добавляем вторую диаграмму:

fig.add_trace ( trc2 , row = 2, col =1 )

Теперь отображаются уже два графика:

Осталось только подписать оси и сам график уже известным способом. Разница только в том, что для подписей осей используется ссылка на строку и колонку:

fig.update_xaxes( title = 'My new X axis' , row =1 , col =1 )
fig.update_yaxes( title = 'My new Y axis' , row =1 , col =1 )
fig.update_xaxes( title = 'My second X axis' , row =2 , col =1 )
fig.update_yaxes( title = 'My second Y axis' , row =2 , col =1 )
fig.update_layout( title = 'My New Chart' )


Вот и результат:

 

Таким образом по сравнению с matplotlib использование Plotly не сильно сложнее  - разве что создание самого графика несколько более длиннее в записи  go.Scatter по сравению с plot.
C другой стороны не требуется настройка вертикальных отступов посредством subplots_adust и библиотека отступы сама отлично настраивает.

04 August 2020

Plotly : шаблоны

В предыдущем посте Plotly : подписи у графика и осей мы нашли нужные нам ссылки по смыслу в параметре template т.е. в шаблоне.
А что если попробовать применить сам шаблон ?

Для изменения, к примеру, вызовем вот так:

fig.layout.update( template = 'plotly_dark')
fig


Вот что получится в результате:



А если вызвать другой:

fig.layout.update( template = 'gridon')
fig


То будет вот так:


Таким образом можно регулировать стили отображения графиков.
Более полный список шаблонов можно посмотреть тут Theming and templates in Python