以下の記事で、4/4までの感染者数のデータをMatplotlibで可視化しました。
今回は、同じデータをPlotlyを使用して可視化して行きたいと思います。(データの内容の考察は今回はしません。)
使用するデータは、ジョンズ・ホプキンズ大学で公開している以下のデータを使用しています。
https://github.com/CSSEGISandData/COVID-19
Plotlyとは?
Plotlyはインタラクティブなグラフを作成できるツールです。
「インタラクティブなグラフ」とは画面からの操作により表示するデータ、範囲、レイアウトなどを変更できるグラフの事です。
この記事ではPythonを使っていますが、RやJavascript用のライブラリも用意されています。
環境について
今回からJupyter NotebookではなくJupyter Labを使用する事にしました。
本記事で紹介するコードはJupyter Lab+Plotlyの環境で動作しています。
環境が整ってない方は、以下の公式ページを参照してインストールしてください。
https://plotly.com/python/getting-started/
なお重要なポイントとしてPlotlyはバージョン4系を使用しています。
Plotlyはバージョンが3系と4系で概念・コーディング方法が大きく変わりました。
ググって検索して出てくる情報は、ほぼほぼバージョン3系の情報なので注意が必要です。
最も大きい違いは、バージョン3系で「オンラインモード」「オフラインモード」とされていたものが、バージョン4系はオフラインモードのみになり(オフラインモードという言い方すらしない)、オンラインモードと呼ばれていたものは、chart-studioというパッケージに移行された事です。
詳細は、以下をご参照ください。(英語ですが・・)
https://plotly.com/python/v4-migration/
バージョン3でのオフラインモードでの記載方法はバージョン4でも使用できますが、新しく作成するコードでは、バージョン4の書き方をした方が良いでしょう。
Plotlyの基本的な概念
Plotlyの基本的な概念として、「Figure」「Trace」「Layout」「Frame」というものがあります。
- Figure
描画する図全体のオブジェクト - Trace
プロットするデータ(Figureに対して1〜N) - Layout
図のレイアウト(Figureに対して1) - Frame
データの描画フレーム(アニメーションの”コマ”)(Figureに対して0〜N)
ちょっと乱暴ですが図に書くと以下のようなイメージになります。
図では省略しましたがFrameの中にもTraceとLayoutが紐づいています。
アニメーションありの場合は、ベースとなるTraceとLayoutをFrameのTraceとLayoutで更新していくようなイメージになります。
サンプルコード
アニメーションなしの場合の簡単なサンプルコードと結果を以下に掲載します。
まずは必要なライブラリをインポートします。
import numpy as np #サンプルでのみ使用
import plotly.io as pio #サンプルでのみ使用
import plotly.graph_objects as go
以下はnumpyで適当なデータを生成して可視化するソースコードです。
Traceにデータを設定して、Layoutにレイアウト情報を設定して、Figureのコンストラクタに渡しています。
x = np.array([1,2,3,4,5,6,7])
y0 = x
y1 = 0.5 * np.square(x)
y2 = 20*np.sin(x)
#trace
trace0 = go.Scatter(x=x,y=y0,name="linear")
trace1 = go.Scatter(x=x,y=y1,name="Square")
trace2 = go.Scatter(x=x,y=y2,name="Sin")
traces = [trace0,trace1,trace2]
#layout
layout = go.Layout(title=(dict(text="test title",x=0.5)),
xaxis=(dict(title="x")),
yaxis=(dict(title="y")))
#figure
fig = go.Figure(data=traces,layout=layout)
#表示
fig.show()
#htmlとして出力
fig.write_html("plotly-test.html",
include_plotlyjs = "cdn", #jsはcdnを利用
full_html = False) #<html><body>は出力しない
実行した結果をHTMLとして出力して他のHTMLに埋め込めば、ブラウザ上で操作する事ができます。
なお、以下、全てのインタラクティブなグラフは、一応、レスポンシブ対応がされていますが、スマフォの縦画面だとちょっとキツイ(潰れる)です。スマフォの横画面、もしくはタブレット・PCで操作する事をお勧めします。(そもそも指だとめちゃくちゃ操作しにくいですが・・)
以下、この記事に埋め込んだ結果です。
[get_content path=’https://dodotechno.com/wp-content/uploads/2020/04/plotly-test.html’]データの値の箇所にカーソルをあてると値が表示されます。
凡例をクリックするとクリックしたデータの表示/非表示が切り替わり、ダブルクリックするとクリックしたデータ以外の国の表示/非表示が切り替わります。
また、ある範囲を囲むとその箇所がズームされます。
このように画面上からグラフに対して、色々な操作を行う事ができます。
なお同じ動作を実現するコードは何パターンかあります。(何パターンもあると初心者は帰って混乱するのですが・・)
例えば以下のように全てをディクショナリー形式で指定する事もできます。この場合、figは単なるディクショナリーなので、表示・HTML出力にはplotly.io(pioとしてインポート)を使用します。
x = np.array([1,2,3,4,5,6,7])
y0 = x
y1 = 0.5 * np.square(x)
y2 = 20*np.sin(x)
#trace
trace0 = dict(x=x,y=y0,name="linear")
trace1 = dict(x=x,y=y1,name="Square")
trace2 = dict(x=x,y=y2,name="Sin")
traces = [trace0,trace1,trace2]
#layout
layout = dict(title=(dict(text="test title",x=0.5)),
xaxis=(dict(title="x")),
yaxis=(dict(title="y")))
#figure
fig = dict(data=traces,layout=layout)
#表示
pio.show(fig)
#htmlとして出力
pio.write_html(fig,"plotly-test.html",
include_plotlyjs = "cdn", #jsはcdnを利用
full_html = False) #<html><body>は出力しない
また、ディクショナリーの階層を記載するのが面倒臭い場合は、コンストラクタに渡すときに、以下のような”Magic Underscore Notation”という方法を使用して表記を簡略化する事もできます。(出力結果は同じです。)
x = np.array([1,2,3,4,5,6,7])
y0 = x
y1 = 0.5 * np.square(x)
y2 = 20*np.sin(x)
#trace
trace0 = go.Scatter(x=x,y=y0,name="linear")
trace1 = go.Scatter(x=x,y=y1,name="Square")
trace2 = go.Scatter(x=x,y=y2,name="Sin")
traces = [trace0,trace1,trace2]
#figure
fig = go.Figure(data=traces,
#layout
layout_title_text="test title",
layout_title_x=0.5,
layout_xaxis_title="x",
layout_yaxis_title="y")
#表示
fig.show()
#htmlとして出力
fig.write_html("plotly-test.html",
include_plotlyjs = "cdn", #jsはcdnを利用
full_html = False) #<html><body>は出力しない
個人的に”Magic Underscore Notation”が好みなので、積極的に使用しています。
コロナ感染者数を可視化
それではコロナ感染者数を可視化していきましょう。
なお、データの取得から加工までは前述のコロナ感染者数をMatplotlibで可視化した記事に記載していますのでそちらをご参照ください。
リンク先の記事の「データの取得から前加工まで」で生成したpandasのデータフレーム形式の累計データ(covid_data_total)と日別データ(covid_data_daily)を以下で使用します。
アニメーションなしの場合
まずはアニメーションなしの場合でインタラクティブなグラフを作成します。
縦軸がコロナの週別感染者数、横軸がコロナの累計感染者数の推移となっており、各国のデータを比較するために両対数グラフとしています。
ソースコードは以下となります。
def plot_covid_trajectory_plotly(covid_data_total,
covid_data_daily,
period="W",
period_title="週別",
title_date_format="%Y年%m月%d日週"):
#累計は最大値でリサンプル
covid_data_total = covid_data_total.resample(period, label='left', closed='left').max()
#期間別は期間内の総和でリサンプル
covid_data_daily = covid_data_daily.resample(period, label='left', closed='left').sum()
#データ
data = []
for column_name, item in covid_data_total.iteritems():
confirmed_data = go.Scatter(
x = item,
y = covid_data_daily[column_name],
text = covid_data_daily.index.strftime(title_date_format),
name = column_name,
)
data.append(confirmed_data)
#プロット
fig = go.Figure(
data = data,
#タイトル
layout_title_text = f"COVID-19 感染者数({period_title}vs累計)",
layout_title_x = 0.5,
layout_title_xref = "paper",
#マージン
layout_margin_l = 0,
layout_margin_r = 0,
layout_margin_t = 50,
layout_margin_b = 0,
#x軸
layout_xaxis_title = "感染者数(累計)",
layout_xaxis_type = "log",
layout_xaxis_range = [1.7, 5.7],#範囲 (log50〜log500000)
#y軸
layout_yaxis_title = f"感染者数({period_title})",
layout_yaxis_type = "log",
layout_yaxis_range = [1.7, 5.7],#範囲 (log50〜log500000)
#凡例
layout_legend_x = 0.03,
layout_legend_y = 0.95
)
#htmlファイルwの出力
fig.write_html("./covid-19-tragectory-plotly.html",
include_plotlyjs = "cdn",
full_html = False)
fig.show()
#実行
plot_covid_trajectory_plotly(covid_data_total,covid_data_daily)
実行した結果、出力させたHTMLを以下に埋め込んでいます。
[get_content path=’https://dodotechno.com/wp-content/uploads/2020/04/covid-19-tragectory-plotly.html’]
パラメータの数が多いので複雑に見えますが、やってることはサンプルと同じで、Traceを作成して、Layoutを作成して、Figureに渡しているだけです。
パラメータの説明は以下をご参照ください。(膨大なので探すのは結構、一苦労ですが・・)
https://plotly.com/python-api-reference/
アニメーションありの場合
次にアニメーションありの場合のインタラクティブなグラフを作成します。
ソースコードは以下となります。
def plot_covid_trajectory_plotly_animation(covid_data_total,
covid_data_daily,
period="W",
period_title="週別",
title_date_format="%m/%d週"):
#累計は最大値でリサンプル
covid_data_total = covid_data_total.resample(period, label='left', closed='left').max()
#期間別は期間内の総和でリサンプル
covid_data_daily = covid_data_daily.resample(period, label='left', closed='left').sum()
#ベースタイトル
title = f"COVID-19 感染者数({period_title}vs累計)"
data = []
frames = []
#データ(各トレースの属性だけ定義)
for column_name, _ in covid_data_total.iteritems():
data.append(go.Scatter(text = covid_data_daily.index.strftime(title_date_format),
textposition = "bottom right",
name = column_name,
mode = "lines+markers+text"))
#フレーム
frammes = []
#スライダーステップ
slider_steps = []
#時系列(日付,週etc)分のフレームを作成
for i,date in enumerate(covid_data_total.index,1):
format_date = date.strftime(title_date_format)
frame_data = []
#フレームごとに該当時系列までのデータを描画
covid_data_total_tmp = covid_data_total[0:i]
covid_data_daily_tmp = covid_data_daily[0:i]
for column_name, item in covid_data_total_tmp.iteritems():
frame_data.append(go.Scatter(x = item,
y = covid_data_daily_tmp[column_name]))
frame = go.Frame(data = frame_data,
name = format_date,
layout_title_text = title)
frames.append(frame)
#スライダーステップを時系列分定義
slider_step = dict(args = [[format_date],
dict(fromcurrent=True,mode = "immediate",
frame=dict(duration=1000,redraw=True),
transition=dict(duration=500),
)],
label = format_date,
method = "animate")
slider_steps.append(slider_step)
#スライダー
layout_sliders = [dict(len=0.9,x=0.1,
transition=dict(duration=0),steps = slider_steps)]
#ボタンの定義
#再生ボタン
play_button = dict(args=[None,dict(fromcurrent=True,mode = "immediate",
frame=dict(duration=500,redraw=True),
transition=dict(duration=500),
)],
label = "再生",
method = "animate")
#停止ボタン
stop_button = dict(args=[[None],dict(mode="immediate",frame=dict(redraw=True))],
label = "停止",
method = "animate")
#2つのボタンのレイアウトを定義
buttons = dict(buttons=[play_button,stop_button],
direction = "left",
pad = dict(t=35,r=10),
x = 0.1, xanchor = "right",
y = 0, yanchor = "top",
type = "buttons")
#スケール用ドロップダウンの定義
#対数スケールドロップダウン
log_dropdown = dict(args=[dict(xaxis=dict(type="log",range=[1.7, 5.7]),
yaxis=dict(type="log",range=[1.7, 5.7]))],
label = "対数スケール",
method = "relayout") #layoutの変更
#線形スケールドロップダウン
linear_dropdown = dict(args=[dict(xaxis=dict(type="linear",range=[50, 350000]),
yaxis=dict(type="linear",range=[50, 250000]))],
label = "線形スケール",
method = "relayout") #layoutの変更
#ドロップダウンのレイアウトを定義
scale_dropdown = dict(buttons=[log_dropdown,linear_dropdown],
direction = "down",
pad = dict(b=10),
x = 1, xanchor = "right",
y = 1, yanchor = "bottom",
type = "dropdown")
#日付表示ボタンの定義
#日付表示ドボタン
anno_visible_dropdown = dict(args=["mode","lines+markers+text"],
label = "日付表示",
method = "restyle") #dataの変更
#注釈非表示ドロップダウン
anno_invisible_dropdown = dict(args=["mode","lines+markers"],
label = "日付非表示",
method = "restyle") #dataの変更
#注釈用ドロップダウンのレイアウトを定義
anno_dropdown = dict(buttons=[anno_visible_dropdown,anno_invisible_dropdown],
direction = "down",
pad = dict(b=10),
x = 0, xanchor = "left",
y = 1, yanchor = "bottom",
type = "dropdown")
#画像生成
fig = go.Figure(data = data,
frames = frames,
#タイトル
layout_title_text = f"COVID-19 感染者数({period_title}vs累計)",
layout_title_x = 0.5,
layout_title_xref = "paper",
#マージン
layout_margin_l = 10,
layout_margin_r = 10,
layout_margin_t = 100,
layout_margin_b = 100,
#x軸
layout_xaxis_title = "感染者数(累計)",
layout_xaxis_type = "log",
layout_xaxis_range = [1.7, 5.7],
#y軸
layout_yaxis_title = f"感染者数({period_title})",
layout_yaxis_type = "log",
layout_yaxis_range = [1.7, 5.7],
#ボタン
layout_updatemenus = [buttons,scale_dropdown,anno_dropdown],
#スライダー
layout_sliders = layout_sliders,
layout_legend_font_size = 8,
#凡例
layout_legend_x = 0.01,
layout_legend_y = 0.98,
#大きさ
layout_height = 600,
)
#htmlファイルwの出力
fig.write_html("./covid-19-tragectory-plotly-animation.html",
include_plotlyjs = "cdn",
full_html = False)
#プロット
fig.show()
plot_covid_trajectory_plotly_animation(covid_data_total,covid_data_daily)
急に長くて複雑になったように見えますが、単にパラメータが多くなっただけで、結局、Trace,Layout,Frameを定義して最後にFigureに突っ込んでいるだけです。
実行した結果、出力させたHTMLを以下に埋め込んでいます。
[get_content path=’https://dodotechno.com/wp-content/uploads/2020/04/covid-19-tragectory-plotly-animation.html’]
再生ボタンにより時系列でグラフを描写し、停止ボタンで停止できます。
またスライダーで該当する週を選択する事で、特定の週の情報を表示する事ができます。
なお、お遊びで、スケールを対数と線形でドロップダウンで切り替えられるようにしたり、日付をプロット毎に表示/非表示を切り替えられるようにしました。
ブログへの埋め込み
上記でさらっとHTMLを埋め込んだと記載しておりますが、WordPressに埋め込む場合は注意が必要です。
たとえ出力したHTMLを「テキスト」タブで貼り付けても、WordPressは勝手に<p>タグとかつけやがるので、うまくいきません。
全体的に<p>タグを勝手につけないようにするカスタマイズもできるのですが、今まで作成した記事にも影響が出ると困るので、ショートコードを利用してサーバーサイドでHTMLをページにインクルードする方法で解決しました。
ショートコードはphpの関数を使ってwordpressの編集画面から実行できるようにする機能です。
ショートコードはfunction.phpに追記します。
function.phpを修正するためには、ダッシュボードのメニューの「外観」→「テーマエディタ」から「テーマファイル」(テーマのための関数(function.php)」を開いて編集するか、サーバにアクセスにて、以下を開いて編集します。
<サイトのホームディレクトリ>/public_html/wp-content/themes/jin-child/function.php
function.phpに以下のソースコードを追加して保存します。
function get_content($args) {
if (isset($args['path'])) {
return file_get_contents($args['path']);
}
}
add_shortcode('get_content', 'get_content');
追加したショートコードを利用するためには、記事の編集画面で以下のように記載します。
[[get_content path='https://dodotechno.com/wp-content/uploads/2020/04/covid-19-tragectory-plotly-animation.html']]
なおサーバーにhtmlファイルをアップロードしても良いのですが、私は横着して管理画面からアップロードしたので、引数のpathはアップロードしたパス(URL)を記載しています。(サーバーサイドでファイルパスでインクルードした方が本当はいいのですが・・)
まとめ
以上、plotlyでインタラクティブなグラフを生成して、記事に埋め込むまでの方法について記載しました。
パラメータの詳細なども記載しようと思ったのですが、力尽きたので(すいません)、わずかなソースコードのコメント+公式のリファレンス(https://plotly.com/python-api-reference/)をご参照ください。
細かい調整をしようとすると結構、面倒臭いですが、まあ、慣れでしょうね・・
また「plotly.express」という高レベルのパッケージもあり、あまり細かい事をしなければ、そちらを利用した方が楽にデータを描画できます。
使いこなせば、かなりリッチなダッシュボードが作れるようになるので、matplotlibしか使った事ないような方も是非活用してみてください。