かみのメモ

コンピュータビジョン・プログラムな話題中心の勉強メモ

Plotlyでレポート・論文に使えるグラフを描こう

最近注目を集めているグラフツールPlotly。

久しぶりに調べてみれば、なんとベクター画像出力が追加されているじゃないですか! これは論文用の作図もmatplotlibからPlotlyに移行できるぞ! ということで、Plotlyの基本的な使い方を復習ついでにまとめてみました。

この記事では、Pythonからグラフを画像として保存する方法と、自分が資料用のグラフを作るときによく使うレイアウト調整の方法を紹介していきます。

スポンサーリンク

ちなみに、以下の記事で紹介している小技を組み合わせればPlotlyの活用範囲がより広がるかなと思います。 興味があればこちらも読んでみてください。

kamino.hatenablog.com

執筆時のバージョン情報

Python : 3.7.2
plotly : 3.7.0
orca : 1.2.1
psutil : 5.5.0

もくじ

1. ツールのインストール

最初にplotly, psutil, electron, orcaをインストールしておく必要があります。 plotlypsutilはpipからインストールできます。 electronorcaはNode.jsのパッケージとして公開されているので、npmからインストールします。

pip install plotly psutil
npm install -g electron@1.8.4 orca

他のインストール方法については公式ドキュメントを参照してください

2. テンプレートを選ぶ

まずは、https://plot.ly/python/からグラフのテンプレートを選んでコピペしてきましょう。

テンプレートのコードのままではオンライン版Plotlyのワークスペースにデプロイされてしまうので、ローカルにグラフを保存するようコードを書き換えます。

冒頭に以下の2行を追加し、

import plotly.offline as po
import plotly.io as pio

末尾のpy.iplot(fig, filename='xxxxx')を次のように書き換えましょう。

po.plot(fig, filename='xxxxx.html')  # HTMLとして保存
pio.write_image(fig, 'xxxxx.png')    # PNGとして保存(PNG/JPEG/WebP/PDF/SVG/EPS が選択可能)

これで、描画したグラフがHTMLと画像形式で保存されるはずです。 Jupyter Notebookを使っている場合は、冒頭にplotly.offline.init_notebook_mode(connected=True)を追加し、po.plot()po.iplot()にすれば、実行結果の部分にグラフが描画されます。

例として、折れ線グラフのテンプレートを保存してみます。

サンプルコード

import plotly.plotly as py
import plotly.graph_objs as go
import plotly.offline as po
import plotly.io as pio

# Add data
month = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
         'August', 'September', 'October', 'November', 'December']
high_2000 = [32.5, 37.6, 49.9, 53.0, 69.1, 75.4, 76.5, 76.6, 70.7, 60.6, 45.1, 29.3]
low_2000 = [13.8, 22.3, 32.5, 37.2, 49.9, 56.1, 57.7, 58.3, 51.2, 42.8, 31.6, 15.9]
high_2007 = [36.5, 26.6, 43.6, 52.3, 71.5, 81.4, 80.5, 82.2, 76.0, 67.3, 46.1, 35.0]
low_2007 = [23.6, 14.0, 27.0, 36.8, 47.6, 57.7, 58.9, 61.2, 53.3, 48.5, 31.0, 23.6]
high_2014 = [28.8, 28.5, 37.0, 56.8, 69.7, 79.7, 78.5, 77.8, 74.1, 62.6, 45.3, 39.9]
low_2014 = [12.7, 14.3, 18.6, 35.5, 49.9, 58.0, 60.0, 58.6, 51.7, 45.2, 32.2, 29.1]

# Create and style traces
trace0 = go.Scatter(
    x = month,
    y = high_2014,
    name = 'High 2014',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        width = 4)
)
trace1 = go.Scatter(
    x = month,
    y = low_2014,
    name = 'Low 2014',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        width = 4,)
)
trace2 = go.Scatter(
    x = month,
    y = high_2007,
    name = 'High 2007',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        width = 4,
        dash = 'dash') # dash options include 'dash', 'dot', and 'dashdot'
)
trace3 = go.Scatter(
    x = month,
    y = low_2007,
    name = 'Low 2007',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        width = 4,
        dash = 'dash')
)
trace4 = go.Scatter(
    x = month,
    y = high_2000,
    name = 'High 2000',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        width = 4,
        dash = 'dot')
)
trace5 = go.Scatter(
    x = month,
    y = low_2000,
    name = 'Low 2000',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        width = 4,
        dash = 'dot')
)
data = [trace0, trace1, trace2, trace3, trace4, trace5]

# Edit the layout
layout = dict(title = dict(text = 'Average High and Low Temperatures in New York'),
              xaxis = dict(title = 'Month'),
              yaxis = dict(title = 'Temperature (degrees F)'),
              )

fig = dict(data=data, layout=layout)
po.plot(fig, filename='styled-line.html')
pio.write_image(fig, 'styled-line.png')

f:id:kamino-dev:20190313165032p:plain
保存されたグラフ

ちゃんと画像になっていますね。 この画像はPNGですが、ベクター画像(SVG/PDF/EPS)として保存することもできます。

3. Plotlyのデータ形式

さて、ここからテンプレートを少しずつ変更して自分好みのフォーマットに調整していくのですが、その前にPlotlyに渡すデータの形式を確認しておきましょう。

print(fig)してみるとわかりますが、figの実体はただのdictionaryです。 少し複雑に見えるテンプレートでも、結局は所定の形式のdictionaryを作成してplot()を呼び出しているだけなのです。 つまり、このツリー構造さえ把握しておけば、グラフの書式を自由に変更できるようになります。

今回は、この中からよく使うプロパティを抜き出してみました。 すべてのプロパティ(キー)の一覧はhttps://plot.ly/python/reference/で確認できます。

※ 各プロパティの値は基本的にPlotlyのデフォルト値を書いています。

fig = dict(
  data = [
    # Trace(グラフ化するデータを格納するオブジェクト)のリスト
    # グラフの種類に応じて
    #   Scatter/Scattergl/Bar/Box/Pie/Area/Heatmap
    #   Contour/Histogram/Histogram2D/Histogram2Dcontour
    #   Ohlc/Candlestick/Table/Scatter3D/Surface/Mesh3D
    # のいずれかが入る
    go.Scatter(  # Scatterは折れ線・2D点群プロット
      x = [0, 1], y = [10, 20],  # データ(グラフの種類によっては文字列が入ったり、zがあったりする)
      mode = 'lines',
      line = dict(     # 線の書式
        width = 2,
      ),
      marker = dict(   # データ点の書式
        symbol = 'circle',
        size = 6,
      ),
      showlegend = True,  # 凡例にこのTraceを表示するかどうか
    ),
  ],
  layout = dict(
    font = dict(       # グローバルのフォント設定
      family = '"Open Sans", verdana, arial, sans-serif',
      size = 12,
      color = '#444',
    ),
    title = dict(
      text = '',       # グラフのタイトル
      font = dict(),   # タイトルのフォント設定
    ),
    width = 700,       # 全体のサイズ
    height = 450,
    autosize = True,   # HTMLで表示したときページに合わせてリサイズするかどうか
    margin = dict(     # グラフ領域の余白設定
      l = 80, r = 80, t = 100, b = 80,
      pad = 0,         # グラフから軸のラベルまでのpadding
      autoexpand = True,  # LegendやSidebarが被ったときに自動で余白を増やすかどうか
    ),
    xaxis = dict(      # 2Dグラフ用のx軸の設定
      title = dict(text = '', font = dict()),  # x軸のラベル
      type = '-',      # 'linear'、'log'、'date'などに設定可能
      autorange = True,
      range = [0, 1],
      scaleanchor = 'x1', scaleratio = 1,  # 他の軸とのスケールを固定したいときに使う
      tickmode = 'auto',  # 目盛りの刻み方(目盛り関連の設定項目は他にもいくつかあります)
    ),
    yaxis = dict(),    # 2Dグラフ用のy軸の設定、だいたいx軸と一緒
    scene = dict(      # 3Dグラフ用の設定
      camera = dict(),
      xaxis = dict(), yaxis = dict(), zaxis = dict()
    ),
    geo = dict(),      # グラフに地図を表示させるときの設定
    showlegend = True, # 凡例を表示するかどうか
    legend = dict(
      font = dict(),   # 凡例のフォント設定
      x = 1.02, xanchor = 'left',  # 凡例の表示場所の設定
      y = 1, yanchor = 'auto',
      bordercolor = '#444', borderwidth = 0,  # 凡例を囲む枠線の設定
    ),
    annotations = [],  # グラフ中に注釈を入れたいときに使う
    shapes = [],       # グラフ中に線や塗りつぶしを挿入したいときに使う
  ),
)

結構多いですね…。 抑えるべき点は以下の3つだと思います。

1つ目に、グラフに描画するデータの情報はTraceというオブジェクトの中に格納されます。 このTraceをplotly.graph_objs内のどのクラスから作成するかによって、円グラフや棒グラフなどグラフの種類が変わります。 また、マーカーや線など、データのプロットに関する設定もTraceの中に含まれています。 Traceはリスト形式にまとめられfig['data']に格納されます。

2つ目に、グラフ全体のフォーマットに関する設定はfig['layout']に格納されます。 タイトルや軸の設定、凡例の書式などはこの中に含まれています。 ただし、軸の設定は2Dのときと3Dのときで設定すべきプロパティが変わるので注意が必要です。

3つ目に、グラフの上に矢印などの注釈(アノテーション)や追加の線を記入したいときはfig['layout']['annotaions']fig['layout']['shapes']に情報を入れます。

他にも、マウスカーソルを当てたときに出てくる情報(hoverlabel)の設定やスライダーの挿入など設定次第で色々と遊べるのですが、今回は静的なグラフを出力することが目的ですので省略しています。

ともあれ、figの中にグラフのすべての情報がKey-Value形式で入っている、という構造はとてもわかりやすいですね。 このツリー構造さえ頭に入れておけばhttps://plot.ly/python/reference/を読みながらサクサク作図できるはずです(ホンマか?)。

4. よく使うレイアウト調整

さて、ここからは個人的によく使うレイアウト調整の小技をまとめてみます。

4.1. グラフエリアのサイズを指定したい

グラフエリアのサイズを直接指定することはできません。 一応、layout=dict(margin=dict(l=50, r=50, t=100, b=50, autoexpand=False), height=450, width=700)のように指定すれば、height/width から t+b/l+r を引いたものがグラフエリアのサイズになるはずです。 値を調整してみてください。

4.2. 軸の向きを反転させたい

xaxis/yaxis=dict(autorange='reversed')とするか、xaxis/yaxis=dict(range=[100, 0])のように手動で逆順の範囲を指定しましょう。

4.3. 軸を0始まりにしたい

値の範囲が10~80だけど、グラフ上では0の軸を表示させたい、というときはxaxis/yaxis = dict(rangemode='tozero')

ただし、値が正負にまたがっている場合、'tozero'は無視されます。 負の値を無視して正の範囲だけ描画したいときは'nonnegative'にしましょう。

その他の場合は、xaxis/yaxis の range を手動で設定しましょう。

4.4. 軸のスケール比を固定したい

2DグラフでY軸をX軸の5倍に拡大して表示したい場合は、layout=dict(yaxis=dict(scaleanchor='x', scaleratio=5))

f:id:kamino-dev:20190313232301p:plain
Y軸のスケールをX軸の5倍にした例

Y軸の上下が余ってしまっているのが気になる場合は、yaxis=dict(constrain='domain')を追加しておけば自動で切り詰めてくれます。

3Dグラフの場合は、グラフ領域のアスペクト比と各軸のrange幅を調整することで間接的に軸のスケールを調整します。

XYZのrange幅が同じなら、layout=dict(scene=dict(aspectmode='cube'))とすれば全ての軸のスケールが等倍になります。 range幅が違ったり、等倍以外のスケールに設定したい場合は、layout=dict(scene=dict(aspectmode='manual', aspectratio=dict(x=1, y=2, z=3), xaxis=dict(range=[0, 100]), ....))のようにグラフのアスペクト比を固定した上でrangeを調整しましょう。

4.5. グラフエリアを枠で囲みたい

軸を描画させるlinewidthとそれを反対側に転写させるmirrorを組み合わせて、layout=dict(xaxis=dict(linewidth=1, mirror=True), yaxis=dict(linewidth=1, mirror=True))です。

f:id:kamino-dev:20190314001247p:plain
グラフエリアを枠で囲う

4.6. グリッドを消したい&目盛りを追加したい

グリッドを消すときはxaxis/yaxis=dict(showgrid=False)。 目盛りを追加するときはxaxis/yaxis=dict(ticks='inside')もしくは'outside'(目盛りの生える方向が変わります)。

f:id:kamino-dev:20190314002237p:plain
グリッドを消して目盛りと軸線を追加

4.7. グリッド/目盛りをN刻みor手動設定したい

5刻みにするときはxaxis/yaxis=dict(dtick=5)、手動設定する場合はxaxis/yaxis=dict(tickvals=[5, 15, 25, 70])のように値をリストで渡してあげればよいです。

4.8. 凡例の位置を変えたい

凡例の位置のデフォルト設定はlegend=dict(x=1.02, xanchor='left', y=1, yanchor='auto')です。 ここではx,yをグラフの左下を(0,0)、右上を(1,1)としたときの座標系でカウントするので、デフォルトでは凡例ボックスの左エッジがグラフの右端の少し隣にくるよう設定されていることになります。 この値を適当に変えてやればOKです。

4.9. 検定結果(有意差)のマークを書き込みたい

割と使う機会の多い機能のはずなんですが、公式ではまだサポートされていません。 一応、annotationsshapesでお絵描きすれば表現できます。

サンプルコード

import plotly.offline as po
import plotly.graph_objs as go
import plotly.io as pio
import numpy as np

y0 = np.random.randn(50)-1
y1 = np.random.randn(50)+1

trace0 = go.Box(y=y0)
trace1 = go.Box(y=y1)
data = [trace0, trace1]

layout = dict(
    yaxis=dict(range=[-4, 4]),
    annotations=[
        dict(showarrow=False, text='*', x=0.5, y=3.75),
    ],
    shapes=[
        dict(type='line', x0=0, y0=3.5, x1=1, y1=3.5),
        dict(type='line', x0=0, y0=3.25, x1=0, y1=3.5),
        dict(type='line', x0=1, y0=3.25, x1=1, y1=3.5),
    ],
)
fig = dict(data=data, layout=layout)

po.plot(fig, filename='box.html')
pio.write_image(fig, 'box.png')

f:id:kamino-dev:20190314010417p:plain
有意差マークを書き込む


Plotlyたのしいですね。 デフォルトの配色がきれいですし、エラーメッセージも読みやすいのでサクサク使える感じがします。

また便利な設定を発見したら追記していきます。