データ分析

コロナ感染数を元にPlotlyでインタラクティブなグラフを作成してみました。

以下の記事で、4/4までの感染者数のデータをMatplotlibで可視化しました。

コロナ可視化
コロナ感染者数をPython+Matplotlibで可視化してみました。(4/4現在)現時点で、収束の兆しが見えていないコロナですが、ジョンズ・ホプキンズ大学では、日々の感染者数についての情報を以下のリンク先で公開していま...

今回は、同じデータを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」というものがあります。

  1. Figure
    描画する図全体のオブジェクト
  2. Trace
    プロットするデータ(Figureに対して1〜N)
  3. Layout
    図のレイアウト(Figureに対して1)
  4. Frame
    データの描画フレーム(アニメーションの”コマ”)(Figureに対して0〜N)

ちょっと乱暴ですが図に書くと以下のようなイメージになります。

plotly構成(アニメーションなし)plotly構成(アニメーションなし)
plotly構成(アニメーションあり)plotly構成(アニメーションあり)

 

図では省略しましたが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で操作する事をお勧めします。(そもそも指だとめちゃくちゃ操作しにくいですが・・)

以下、この記事に埋め込んだ結果です。

データの値の箇所にカーソルをあてると値が表示されます。

凡例をクリックするとクリックしたデータの表示/非表示が切り替わり、ダブルクリックするとクリックしたデータ以外の国の表示/非表示が切り替わります。

また、ある範囲を囲むとその箇所がズームされます。

このように画面上からグラフに対して、色々な操作を行う事ができます。

なお同じ動作を実現するコードは何パターンかあります。(何パターンもあると初心者は帰って混乱するのですが・・)

例えば以下のように全てをディクショナリー形式で指定する事もできます。この場合、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を以下に埋め込んでいます。

 

パラメータの数が多いので複雑に見えますが、やってることはサンプルと同じで、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を以下に埋め込んでいます。

 

再生ボタンにより時系列でグラフを描写し、停止ボタンで停止できます。

またスライダーで該当する週を選択する事で、特定の週の情報を表示する事ができます。

なお、お遊びで、スケールを対数と線形でドロップダウンで切り替えられるようにしたり、日付をプロット毎に表示/非表示を切り替えられるようにしました。

ブログへの埋め込み

上記でさらっと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しか使った事ないような方も是非活用してみてください。