こんな人にオススメ
以前の記事にてmatplotlib.pyplot
、plt
で複数のグラフを1つの画像に描くコードは解説してたけど、plotly
の場合はどういう風にコードが書けるの?
ということで、今回は以前の記事でplt
で作成したsubplot
のグラフをplotly
でも描いてみようという記事。基本的にはplt
の時と同じ配置のグラフを作成する予定だが、一部再現が無理だった部分があるのでそこはご了承。
plt
と同様、使用するデータは1次関数から4次関数。plt
の時の記事は以下。
-
-
【plt&subplot】pythonのpltで複数グラフを1つの画像に描く
続きを見る
python環境は以下。
- Python 3.9.4
- plotly 4.14.3
- plotly-orca 3.4.2
- numpy 1.20.3
subplotを使わない場合
1つのグラフに複数のプロットを入れると、縦軸の値が極端に異なっていたりしたら見づらくなる。2軸にすれば2種類は解決するが、3種類以上だと対応しにくい。
また、全グラフをバラバラに作成してしまうとその分だけ画像の数が増えてしまって見づらく管理も大変になる。
全てのプロットを1つのグラフに
全てのグラフを1つのグラフに入れる場合はシンプルにfor
でプロットを回せばいいが、今回のように1次関数から4次関数までだと、その値の差が大きすぎて1次関数と2次関数の形状の違いがわかりにくい。
もちろん次数が上がると極端に数値が大きくなるということを言いたいのであればこれでいいのだが、パッと見でグラフの値を比較したい時には不向き。
2軸グラフで対処
2軸グラフを使用すればある程度の対処は可能。しかし、今回のように1次関数と2次関数、3次関数と4次関数でそれぞれ極端に値が異なるとこちらも見づらいグラフになる。
さらに、このグラフだと赤と紫、青と緑がそれぞれのセットに見えるが実はそうではない。凡例を見ればわかるが、実は青と赤、緑と橙がセットのグラフなのだ。
このままだと誤解を与えかねないので、2軸グラフでも適切とはいえないだろう。
もはや全プロットをバラバラのグラフで作成
もはやバラバラで作成するパターン。この場合は大きなグラフで閲覧することは可能だが、グラフの数だけファイルが作成されるので管理も大変だしそれぞれを閲覧するのも大変。
ということで、これらを解消するための一つの案としてsubplot
を紹介する。
下準備
import plotly.graph_objects as go import plotly.io as pio from plotly.subplots import make_subplots import numpy as np x = np.arange(10) # グラフの枠線が色付けできないので背景色を変更 paper_bgcolor = 'rgba(245, 245, 245, 1)',
まずは下準備としてのimport
関連と横軸の値。今回、特別に使用するモジュールがfrom plotly.subplots import make_subplots
で書いてあるmake_subplots
。これでsubplot
の設定を行う。
グラフ保存のorca
については以下参照。
-
-
【plotly&orca】plotlyで静止画保存(orca)
続きを見る
また、plt
の場合はグラフに枠をつけることができたが、plotly
でグラフに枠をつける方法がわからなかったので今回はプロット部分ではない部分(軸ラベルとかある部分)の色を灰色にすることで、グラフのサイズを明示しておく。
そのために、paper_bgcolor
変数を導入、色合いは(R, G, B, alpha) = (245, 245, 245, 1)
として明るい灰色にした。
シンプルに2つのプロットを並べる
まずはシンプルに2種類のグラフを横もしくは縦に並べる。色々コードは書き方があるが今回はmake_subplots
とfig.append_trace
で行うこととする。
左右に並べる
左側に1次関数、右側に2次関数を配置した。マーカーはそれぞれ円形と三角形で作成。make_subplots
の引数で以下を設定。
rows
: グラフ全体の行の数cols
: グラフ全体の列の数subplot_titles
: 各グラフのタイトル
subplot
への登録方法は、fig.append_trace((プロット情報), (行番号), (列番号))
で行う。また、グラフ全体のタイトルはいつも通りgo.Layout
でtitle
を設定すればいい。今回はtext
変数に座標情報を格納して、最後に.join
で結合した。
最後にLayout
をfig
に追加してshow
して完了。
# 各グラフのタイトルを初めに設定 fig = make_subplots( rows=1, cols=2, # 初めに各グラフのタイトルを設定 subplot_titles=('y1', 'y2'), ) symbols = ['circle', 'triangle-up'] text = [] # グラフ全体のタイトルに、各グラフの座標を追加 for order, symbol in enumerate(symbols, 1): y = x ** order name = f"y{order}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) # プロットデータ, グラフの何行目にプロットか, グラフの何列目にプロットか # 行・列ともに1からスタート fig.append_trace(d, 1, order) text.append(f"(1, {order})") layout = go.Layout( title=dict(text=f"subplot(left & right): (row, col)={', '.join(text)}"), xaxis=dict(title='x'), xaxis2=dict(title='x'), # 2つ目のグラフの横軸ラベル yaxis=dict(title='y1'), yaxis2=dict(title='y2'), # 2つ目のグラフの縦軸ラベル # グラフの枠線が色付けできないので背景色を変更 paper_bgcolor=paper_bgcolor, ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_lr.html') pio.write_image(fig, 'subplot_lr.png')
上下に並べる
続いては上側に1次関数、下側に2次関数を配置した。先ほどとは異なり横長なのでグラフの印象がガラリと変わる。もちろん使用しているデータは同じ。基本的には書き方は同じだが、make_subplot
のrows
, cols
とfig.append_trace
の引数の値が異なるので注意。
fig = make_subplots( rows=2, cols=1, subplot_titles=('y1', 'y2'), ) symbols = ['circle', 'triangle-up'] text = [] for order, symbol in enumerate(symbols, 1): y = x ** order name = f"y{order}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) # 行・列ともに1からスタート fig.append_trace(d, order, 1) text.append(f"({order}, 1)") layout = go.Layout( title=dict(text=f"subplot(top & bottom): (row, col)={', '.join(text)}"), xaxis=dict(title='x'), xaxis2=dict(title='x'), yaxis=dict(title='y1'), yaxis2=dict(title='y2'), paper_bgcolor=paper_bgcolor, ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_tb.html') pio.write_image(fig, 'subplot_tb.png')
各グラフのタイトルをプロットデータ作成中に設定
plotly
のグラフでは各グラフのタイトルは初めに設定しておかないといけない。ただ、個人的にはこれは美しくないと思う。各データの作成時に一括でプロットもタイトルなども設定した方が好みなので加工してみる。
まず、シンプルに1行2列のsubplot
を設定してみるとfig
は上のような構造となる。基本的にはlistとdictの形式で値の参照や取り出しが可能。
make_subplot
にsubplot_titles
を設定するとfig
の値が一気に変化する。
layout
にannotations
が追加され、そこのtext
が入力の1
に該当する。だから予めグラフの数だけ適当にタイトルを設定する必要がある。もし設定していないと後から設定できないと思う。
予め適当でもsubplot_titlesを設定しておくと、あとは各プロットでこの値を上書きすればいい。
タイトルの変更はfig['layout']['annotations'][(何番目のグラフか(インデックスなので0スタート))]['text']
で可能。タイトルを変更した後のfig
の値は以下。ちゃんと上書きされていることがわかる。
4分割で四方に並べる
続いては4分割にして四方に並べる。イメージは左上、右上、左下、右下。
1から4の順番に並べる
2×2の行列なのでrows=2, cols=2
と設定。(左上), (右上), (左下), (右下) = (1, 1), (1, 2), (2, 1), (2, 2)
となるので、行・列ともに値を変更する必要がある。
plt
の時は設定するための変数を展開できたが、plotly
では展開できなさそう。もちろん1グラフずつバラバラでコードを書いてもいいが今回は上手にfor
ループで回したい。そこで使えるのが「切り捨て」「余り(剰余)」。
fig = make_subplots( rows=2, cols=2, subplot_titles=('y1', 'y2', 'y3', 'y4'), ) symbols = ['circle', 'triangle-up', 'square', 'diamond'] text = [] for order, symbol in enumerate(symbols, 1): row = (order - 1) // 2 + 1 # 切り捨てで行を計算 col = (order - 1) % 2 + 1 # 余りで列を計算 y = x ** order name = f"y{order}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) # プロットデータ, グラフの何行目にプロットか, グラフの何列目にプロットか fig.append_trace(d, row, col) text.append(f"({row}, {col})") layout = go.Layout( title=dict(text=f"subplot: (row, col)={', '.join(text)}"), xaxis=dict(title='x'), xaxis2=dict(title='x'), xaxis3=dict(title='x'), xaxis4=dict(title='x'), yaxis=dict(title='y1'), yaxis2=dict(title='y2'), yaxis3=dict(title='y3'), yaxis4=dict(title='y4'), paper_bgcolor=paper_bgcolor, ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_4.html') pio.write_image(fig, 'subplot_4.png')
2での切り捨ては以下のように0
, 0
, 1
, 1
の順番で出力される。したがって、これを+1
してあげれば行の番号にできる。
0
:0 // 2
は0
1
:1 // 2
は0
2
:2 // 2
は1
3
:3 // 2
は1
一方で2で割った時の余りは以下のように0, 1, 0, 1
の順番で出力される。したがって、これを+1
してあげれば列の番号にできる。
0
:0 % 2
は0
1
:1 % 2
は1
2
:2 % 2
は0
3
:3 % 2
は1
また、それぞれのグラフの軸ラベルについてはそれぞれxaxis2
やyaxis4
のようにして指定する。
プロットの順番はバラバラでもOK
もちろん描画する順番に決まりはない。上のグラフでは初めにy4
を描画し、その後y1
, y2
, y3
を描画した。描画の位置はこれまで通りにしている。
しかし、描画の順番を変更したのでプロットの色が変更されている。今回の色は指定せず自動で決まっているから色が変わっているが、もちろん色の固定も可能。
fig = make_subplots( rows=2, cols=2, subplot_titles=('y1', 'y2', 'y3', 'y4'), # 全体のタイトルはそのまま ) symbols = ['diamond', 'circle', 'triangle-up', 'square'] row_lst = [2, 1, 1, 2] # y4, y1, y2, y3の順番に描くために行の順番を調節 col_lst = [2, 1, 2, 1] # y4, y1, y2, y3の順番に描くために列の順番を調節 order_lst = [4, 1, 2, 3] text = [] for order, symbol in enumerate(symbols, 1): now_order = order_lst[order - 1] # yのオーダーを指定 row = row_lst[order - 1] # ループごとの行の番号 col = col_lst[order - 1] # ループごとの列の番号 y = x ** now_order name = f"y{order_lst[order - 1]}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) # プロットデータ, グラフの何行目にプロットか, グラフの何列目にプロットか fig.append_trace(d, row, col) text.append(f"({row}, {col})") # 軸ラベルの出力が面倒なので一旦辞書に置き換え xdct, ydct = {}, {} for key in [1, 2, 3, 4]: xdct[f"xaxis{key}"] = dict(title='x') ydct[f"yaxis{key}"] = dict(title=f"y{key}") layout = go.Layout( title=dict(text=f"subplot: (row, col)={', '.join(text)}"), **xdct, # 辞書で作成した軸ラベルを展開して代入 **ydct, # 辞書で作成した軸ラベルを展開して代入 paper_bgcolor=paper_bgcolor, ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_4_position.html') pio.write_image(fig, 'subplot_4_position.png')
また、グラフの数が増えてくると各グラフの軸ラベルをつけるのが面倒になる。そこで今回は以下のコードで一旦、軸ラベルを別で作成した。
# 軸ラベルの出力が面倒なので一旦辞書に置き換え xdct, ydct = {}, {} for key in [1, 2, 3, 4]: xdct[f"xaxis{key}"] = dict(title='x') ydct[f"yaxis{key}"] = dict(title=f"y{key}")
その後、go.Layout
内でこれを展開して適用させた。辞書型の展開になるので、*
は2つ必要だ。
layout = go.Layout( title=dict(text=f"subplot: (row, col)={', '.join(text)}"), **xdct, # 辞書で作成した軸ラベルを展開して代入 **ydct, # 辞書で作成した軸ラベルを展開して代入 paper_bgcolor=paper_bgcolor, )
4分割して3枚しか描かない
もちろん4枚分の領域を用意したからと言って必ずしも4枚のグラフを表示する必要はない。ここではy1
, y2
, y3
の3つのグラフのみを用意してグラフを作成した。
ここで、make_subplots
のsubplot_titles
にy4
のタイトル'y4'
だけ入れるとタイトルだけが表示される。ここのタイトルを消すことでy4
という文字もなくなる。
fig = make_subplots( rows=2, cols=2, subplot_titles=('y1', 'y2', 'y3', 'y4'), ) symbols = ['circle', 'triangle-up', 'square'] text = [] for order, symbol in enumerate(symbols, 1): row = (order - 1) // 2 + 1 # 切り捨てで行を計算 col = (order - 1) % 2 + 1 # 余りで列を計算 y = x ** order name = f"y{order}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) # プロットデータ, グラフの何行目にプロットか, グラフの何列目にプロットか fig.append_trace(d, row, col) text.append(f"({row}, {col})") xdct, ydct = {}, {} for key in [1, 2, 3]: xdct[f"xaxis{key}"] = dict(title='x') ydct[f"yaxis{key}"] = dict(title=f"y{key}") layout = go.Layout( title=dict(text=f"subplot: (row, col)={', '.join(text)}"), **xdct, **ydct, paper_bgcolor=paper_bgcolor, ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_4_del3.html') pio.write_image(fig, 'subplot_4_del3.png')
片側は大きなグラフで他方を小さく
作成するグラフをどこまで広げるかを設定することで、グラフごとにそのサイズを設定することが可能。今回は縦長のy1
の右にy2
とy4
を配置した。
# 引数specでグリッド状にグラフを作成可能 fig = make_subplots( rows=2, cols=2, subplot_titles=('y1', 'y2', 'y4'), specs=[ [{'rowspan': 2}, {}], [None, {}], ], print_grid=True, # 作成したグリッドを表示してくれる ) # This is the format of your plot grid: # ⎡ (1,1) x,y ⎤ [ (1,2) x2,y2 ] # ⎣ : ⎦ [ (2,2) x3,y3 ] # 「:」は同上の意味 symbols = ['circle', 'triangle-up', 'square', 'diamond'] text = [] order_lst = [1, 2, 4] # yの次数 row_lst = [1, 1, 2] # 列番号 col_lst = [1, 2, 2] # 行番号 for num, order in enumerate(order_lst): row = row_lst[num] col = col_lst[num] y = x ** order name = f"y{order}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) fig.append_trace(d, row, col) text.append(f"({row}, {col})") xdct, ydct = {}, {} for key in [1, 2, 4]: xdct[f"xaxis{key}"] = dict(title='x') ydct[f"yaxis{key}"] = dict(title=f"y{key}") layout = go.Layout( title=dict(text=f"subplot: (row, col)={', '.join(text)}"), **xdct, **ydct, paper_bgcolor=paper_bgcolor, ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_124.html') pio.write_image(fig, 'subplot_124.png')
グラフサイズの変更はmake_subplots
に引数spec
を追加することで可能になる。今回使用したspec
の内容は以下。
- 2次元の
list
で作成 rowspan
: 書いた位置を起点に左に何列だけ使用したグラフを作成するかcolspan
: 書いた位置を起点に下に何行だけ使用したグラフを作成するかNone
:rowspan
やcolspan
で埋められた領域にはこれを書く{}
: 1×1の領域でグラフを作成
注意点はNone
だけ{}
で括らないということ。執筆者は{None}
としてエラーを吐かれまくった。また、print_grid=True
でグラフの外観イメージを表示してくれる。
軸を共有
subplot
でグラフを作成していると、軸の値が同じな場合がある。そんな時は軸の共有で軸の値を省略することが可能。
横・縦軸どちらも共有
まずは横・縦軸ともに共有した場合。共有しているので、図の左側と下側にのみラベルが振られるようになる。plt
と異なる点は、上下のグラフ群で縦軸の値が異なるという点。
plt
では一番y
の値が大きいy4
に合わせて全てのグラフの縦軸の値が決められていたが、今回はy1
, y2
で1つのグループ、y3
, y4
で1つのグループとして扱われる。
# shared_xaxesで横軸の共有。shared_yaxesで縦軸の共有 fig = make_subplots( rows=2, cols=2, subplot_titles=('y1', 'y2', 'y3', 'y4'), shared_xaxes=True, shared_yaxes=True, ) symbols = ['circle', 'triangle-up', 'square', 'diamond'] text = [] for order, symbol in enumerate(symbols, 1): row = (order - 1) // 2 + 1 # 切り捨てで行を計算 col = (order - 1) % 2 + 1 # 余りで列を計算 y = x ** order name = f"y{order}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) fig.append_trace(d, row, col) text.append(f"({row}, {col})") xdct, ydct = {}, {} for key in [1, 2, 3, 4]: xdct[f"xaxis{key}"] = dict(title='x') ydct[f"yaxis{key}"] = dict(title=f"y{key}") layout = go.Layout( title=dict(text=f"subplot(x, y share): (row, col)={', '.join(text)}"), **xdct, **ydct, paper_bgcolor=paper_bgcolor, ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_share_xy.html') pio.write_image(fig, 'subplot_share_xy.png')
軸の共有はshared_xaxes
とshared_yaxes
で行う。共有しているので値が極端に異なる場合はその違いがぱっと見ではわかりにくい。そんな時は横軸だけ共有のように片側だけ共有することももちろん可能。
横軸だけ共有
横軸のみを共有したい場合はshared_xaxes=True
とすればいい。
fig = make_subplots( rows=2, cols=2, subplot_titles=('y1', 'y2', 'y3', 'y4'), shared_xaxes=True, ) symbols = ['circle', 'triangle-up', 'square', 'diamond'] text = [] for order, symbol in enumerate(symbols, 1): row = (order - 1) // 2 + 1 # 切り捨てで行を計算 col = (order - 1) % 2 + 1 # 余りで列を計算 y = x ** order name = f"y{order}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) fig.append_trace(d, row, col) text.append(f"({row}, {col})") xdct, ydct = {}, {} for key in [1, 2, 3, 4]: xdct[f"xaxis{key}"] = dict(title='x') ydct[f"yaxis{key}"] = dict(title=f"y{key}") layout = go.Layout( title=dict(text=f"subplot(x share): (row, col)={', '.join(text)}"), **xdct, **ydct, paper_bgcolor=paper_bgcolor, ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_share_x.html') pio.write_image(fig, 'subplot_share_x.png')
グラフの余白や間隔を調節
plt
のグラフのギチギチ感と異なり、plotly
のグラフは余白がありまくりのガバガバ設計。なので、ここではグラフの余白やグラフ同士の間隔を設定することでより見やすいグラフにする。
数字で設定
今回は以下の項目でグラフの調節を行った。
horizontal_spacing
: グラフの近いプロット部の左右端同士の間隔vertical_spacing
: グラフの近いプロット部の上下端同士の間隔t
: グラフ全体の上端から、上側のグラフのプロット部上端までの間隔b
: グラフ全体の下端から、下側のグラフの横軸ラベル下端までの間隔l
: グラフ全体の左端から、左側のグラフの横軸ラベル左端までの間隔r
: グラフ全体の右端から、凡例右端までの間隔(デフォルトの凡例位置の場合)
horizontal_spacing
, vertical_spacing
はmake_subplots
内で設定するが、t
, b
, l
, r
についてはgo.Lauout
内で設定することに注意。ちょっとギチギチにしすぎた感はある。なお。記事上では自動でグラフのサイズが変わる(はず)なので、見ている環境ではかなりギチギチ。
fig = make_subplots( rows=2, cols=2, subplot_titles=('y1', 'y2', 'y3', 'y4'), horizontal_spacing=0.04, # 左右のグラフの間隔 vertical_spacing=0.08, # 上下のグラフの間隔 ) symbols = ['circle', 'triangle-up', 'square', 'diamond'] text = [] for order, symbol in enumerate(symbols, 1): row = (order - 1) // 2 + 1 # 切り捨てで行を計算 col = (order - 1) % 2 + 1 # 余りで列を計算 y = x ** order name = f"y{order}" d = go.Scatter( mode='lines+markers', x=x, y=y, name=name, marker=dict(symbol=symbol), ) fig.append_trace(d, row, col) text.append(f"({row}, {col})") xdct, ydct = {}, {} for key in [1, 2, 3, 4]: xdct[f"xaxis{key}"] = dict(title='x') ydct[f"yaxis{key}"] = dict(title=f"y{key}") layout = go.Layout( title=dict(text=f"subplot: (row, col)={', '.join(text)}"), **xdct, **ydct, paper_bgcolor=paper_bgcolor, margin=dict(t=30, b=20, l=20, r=0), # 上下左右の余白 ) fig['layout'].update(layout) fig.show() pio.write_html(fig, 'subplot_adjust.html') pio.write_image(fig, 'subplot_adjust.png')
自動調節
いちいち値を調節するのが面倒な時は、margin=dict(autoexpand=True)
で自動調節すればいい。まあ、autoexpand=True
はデフォルトなので明示する必要がない場合はいらないかな。
spec
で各グラフサイズをバラバラにする
最後に各グラフのサイズをバラバラに作成するということについて解説する。簡単なグラフに関しては既に上で解説した通りだが、ここではさらに複雑なものを作成してみる。
例1
4行3列で領域を区切り以下のようにグラフを設定。
y1
: 1行2列y2
: 1行1列y3
: 3行1列y4
: 2行2列
例2
4行3列で領域を区切り以下のようにグラフを設定。
y1
: 1行2列y2
: 1行1列y3
: 3行2列y4
: 2行1列
例3
3行2列で領域を区切り以下のようにグラフを設定。
y1
: 1行1列y2
: 1行1列y3
: なしy4
: 2行1列
やっぱりplotly
だと長くなりがち
今回は以前、plt
で作成したsubplot
のグラフをplotly
で作成してみるという内容だった。plotly
の方ができることが多い分やはり設定項目が増えてコードが長くなりがち。
fo
文で短くはしているが、それでもかなりの行数を使用してしまっている。なので、plotly
を使用する際には予め関数とかで定義しておく必要がありそう。
plotly
でplt
でもsubplot
を使用することでパッと見のグラフの印象は大きく変わる。今回と以前のsubplot
の記事でよりグラフ描画のレパートリーを増やしていただければ幸いだ。
関連記事
-
-
【plt&subplot】pythonのpltで複数グラフを1つの画像に描く
続きを見る
-
-
【plotly&table+scatter】subplotsで表と散布図を同時にグラフ化
続きを見る
-
-
【plotly&subplot】goでアニメーションのサブプロット
続きを見る
-
-
【plotly&scatter_matrix】複数種の軸を持つsubplotをpxで描画
続きを見る