LoginSignup
21
22

More than 1 year has passed since last update.

”エコ”にダッシュボードをつくろう ~Panelライブラリの紹介~

Last updated at Posted at 2021-11-14

今回はJupyter上に簡単にダッシュボードのようなアプリを作成できるPanelというライブラリを布教(紹介)します。

こんなのつくってみます

以下のようなアプリの作成過程をチュートリアルっぽく紹介出来たら、と思います。

ファイルを読み込むとデータとカラム名が取得されて、軸の項目として選択できるようになり
Panel紹介用_1.gif

ボタンを押すと選択した軸に沿ってグラフが描画される、というシンプルなアプリです。

Panel紹介用_2.gif

ちなみに過去にはこんなのもつくっています
 ⇒ 国会議事録を可視化するツールをつくってみる

HoloVizとPanel

 Panel公式   HoloViz公式

Panelは、HoloVizというパッケージに含まれるライブラリの一つです。
HoloVizは「Pythonでのデータ可視化において、ソフトウェア開発といった余計なことにエネルギーを使わず、本質であるデータと対話することにエネルギーを費やそう」という思想(この考え方をHolovizでは”エコシステム”と呼んでいます)のもと開発がすすめられたライブラリパッケージです。
過去に紹介したHoloViewsもHoloVizに含まれるライブラリの一つです。ちなみに「Holo」はギリシャ語で”全体”を意味する言葉だそうです。

Panelはボタンなどのウィジェットを組み合わせ、ダッシュボードのようなアプリを簡単に構築することができるライブラリです。
特徴は、HoloVizの設計思想である”エコ”なコーディングです。簡潔なPython構文で記述できることに重点が置かれており、各ライブラリやCSS等の深い知識がなくても、多機能かつ精錬されたツールを作成することができます。

また、Dashやstreamlitといった類似のライブラリと比較して、JupyterNotebookと親和性が高いことも特徴の一つだそうです。

ライセンスもBSDという形態で、非商用・商用ともに自由に使用できるとのこと。
(BSDライセンスについては勉強不足なので、詳しい方がいれば是非教えていただきたいです!)

つくってみよう

では、冒頭の動画のようなアプリを作ってみたいと思います。作成過程を
0. 準備
1. ウィジェットの定義
2. ボタン押下時のイベント規定
3. ウィジェットの配置
4. 見た目を整える
の順で説明していきます。

0. 準備

環境

今回実装に用いた環境は以下の通りです。

  • Windows10
  • Python3.8.2
  • jupyterlab==3.1.13
  • panel==0.12.1

ちなみにGoogleColaboratoryでもPanelが動作することは確認しています。
確認していませんがAnacondaでも使えるとのこと。

インストール

pip install panel

でインストールするだけです。
(GoogleColaboratoryではインストール不要)

インポートとおまじない

import panel as pn
# おまじない
pn.extension()

インポートするモジュール名はpanelです。よくpnという略称でインポートされるようです。
pn.extension()は、表示モードを拡張したりできるようですが、指定しないとエラーになるので、
とりあえずここではおまじないのようなものだと理解しておきます。

1. ウィジェットの定義

ユーザーのアクションを受け取るウィジェットには、様々な種類が存在します。
今回は使う機会の多そうなウィジェットをチョイスしていますが、他にどんなウィジェットがあるか知りたい方は公式のリファレンスギャラリーを覗いてみてください。

2021/12/22追記 いくつか記事にまとめました。
 ダッシュボード作成ライブラリPanelのウィジェット紹介 ~入力系編~
 ダッシュボード作成ライブラリPanelのウィジェット紹介 ~出力系編~

入力系ウィジェット

まずはユーザーが入力する情報を受け取るウィジェットについてです。
ウィジェットの定義と表示は下記の例のようにします。

# テキスト(ファイルパス)を入力するウィジェットの定義
filename_input = pn.widgets.TextInput(name= "CSVファイルパス入力", placeholder= "ここにCSVファイルのパスを入力してください")
# 表示
filename_input

 ↓
01_テキストインプット.JPG

pn.widgets.ウィジェット名(オプション)でインスタンスを定義し、格納した変数名を書くだけウィジェットを表示することができます。
今回はとりあえず読み込みたいcsvのフルパスを入力させるためTextInputを使っています。
(ファイルを選択するウィジェットもありますが、説明が増えるので割愛)
オプションのplaceholderは未入力の場合にどんな文字列を表示するか、という文字入力系ウィジェット特有のオプションです。

ウィジェットの各種オプションは、以下のように後から変更することも可能です。

filename_input.placeholder = "HogeHoge FugaFuga"
filename_input

 ↓
02_ウィジェットのオプション変更.JPG

入力したファイルパスを読み込むボタン型のウィジェットも書き方は大体同じです。

# ファイル読み込みボタンの定義
load_file_but = pn.widgets.Button(name= "ファイル読み込み", button_type= "primary")
# 表示
load_file_but

 ↓
03_ファイル読み込みボタン.JPG

button_typeはボタンの色を指定するオプションです。primary(青)の他、success(緑)やerrordanger(赤)等があります。(2021/12/22訂正)
ボタンを押した際のアクションの指定については後述します。

読み込まれたcsvのカラム名からグラフの軸となる項目を選択したいので、セレクトボックスウィジェットとそれをグラフに反映させるボタンを定義します。

# X軸、Y軸を選択するセレクトウィジェットの定義
x_axis_sel = pn.widgets.Select(name= "X軸")
y_axis_sel = pn.widgets.Select(name= "Y軸")
# グラフを描画するボタンの定義
display_graph_but = pn.widgets.Button(name= "グラフ描画", button_type= "primary", disabled= True)

Selectは予め用意しておいた選択肢からプルダウンでユーザーに選択させるウィジェットです。
本来ならoptionsにリスト形式で選択肢を渡します。しかし、この選択肢は読み込んだCSVのカラム名を表示させたいので、最初の時点では指定しないでおきます。
display_graph_butは押したらグラフを表示させる、というボタンにする予定です。そのため最初はdisabledをTrueにして無効化しておきます。
表示は後ほど複数のウィジェットを表示する方法を説明するので、ここでは割愛します。

出力系ウィジェット

先ほどは入力系のウィジェットについて紹介しましたが、今度は出力をメインとしたウィジェットについて紹介します。
といっても書き方はそれほど変わりません。

# HoloViewsインポート
import holoviews as hv
# HoloViewsのバックエンド指定(bokeh)
hv.extension("bokeh")
# データ未入力だとウィジェットエラー出るので仮のグラフデータ格納
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

出力系はpn.pane.ウィジェット名()でインスタンスを生成し、変数名で表示します。
グラフを表示するウィジェットはいくつかありますが、今回は同じHoloVizからHoloViewsを使用しています。
HoloViewsの書き方については今回は割愛しますが、過去に紹介したことがあるので興味がある人はそちらを見てみてください。
 ⇒ HoloViewsとBokehでぐりぐり動かせるグラフを描く

グラフは特定のボタンを押すと描画されるようにしたいので、最初は非表示にしておきます。visibleオプションをFalseにすると非表示にできます。
また、グラフの中身も表示のタイミングで呼び出すのですが、最初に何かしらのグラフデータを与えていないとエラーになるので、hv.Scatter((1, 1))で1,1座標にある散布図を作っておいて、これをウィジェット定義時に指定しています。

仮のグラフですが、一応表示してみます。

# 確認のため一時的にvisibleをTrueに
pn.pane.HoloViews(blank_hv, visible = True)

 ↓
04_仮グラフの表示.JPG

2. ボタン押下時のイベント規定

先ほど定義した2つのボタンにそれぞれ押下したときのイベントを規定します。
イベントの指定は イベント関数定義 → ボタンウィジェットへのイベント登録 の流れで行ないます。

まず、入力されたCSVのファイルパスを読み込み、Selectウィジェットに項目名を設定するイベント関数を定義します。

def load_file(event):
    # 関数外でも使用するCSVカラム名とDataFrameをglobal宣言
    global col_name
    global df
    # csvファイルをデータフレームとして読み込み
    df = pd.read_csv(filename_input.value)
    # 読み込んだCSVデータフレームの列名を取得
    col_name = list(df.columns)
    # 軸項目の選択肢を設定
    x_axis_sel.options = col_name
    y_axis_sel.options = col_name
    # グラフ描画ボタンの無効化を解除
    display_graph_but.disabled = False 
    # ボタンの色を変更
    load_file_but.button_type = "success"

defで引数にeventをした関数を定義し、その中にイベント内容を記述します。

ちなみにこのイベント関数内でエラーが発生した場合、Jupyter上でエラーの表示が出ません。
なので、何かボタン押しても動作している気配がないな、と思ったらイベントの中身をどこか別のセルにコピーして動作チェックしてみることをオススメします。

上記のようにイベント定義したら、このイベント関数をボタンウィジェットに登録します。

# ファイル読み込みボタンへのイベント登録
load_file_but.on_click(load_file)

このようにボタンウィジェット名.on_click(イベント関数名)でイベント指定してあげるだけです。

同様にグラフ出力ボタンを押下したときのイベントも規定・登録しておきます。

def display_graph(event):
    # 関数外でも使用するグラフデータをglobal宣言
    global scatter
    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims= [x_axis_sel.value], vdims= [y_axis_sel.value])
    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True
    # ボタンの色を変更
    display_graph_but.button_type = "success"

# グラフ出力ボタンへのイベント登録
display_graph_but.on_click(display_graph)

HoloViewsならDynamicMapを使えば、グラフの項目のみの変更もできそうですが、複雑になりそう。
なので素直にボタンが押されるたびに新しくグラフオブジェクトを生成し、graph_pane.objectに代入しています。

番外編 ウィジェットが変更されるたびにアクションさせたい

今回の作例では、ボタンをトリガーにイベント実行させていました。
しかし、それ以外にウィジェットが変更されたタイミングでイベント実行させたいケースもあるかと思います。
ウィジェット変更をトリガーにするにはpn.dependsデコレータを使用します。(depends:依存する、の意)

# ウィジェット生成
season = pn.widgets.Select(name="季節", options=["春", "夏", "秋", "冬"], width= 80 )

# ウィジェットの値に依存してStrパネルを返す関数
@pn.depends(season.param.value)
def sele(s):
    return pn.pane.Str(s, width=100, margin= 20)  # width固定した方が文字数変わったとき対応できる

# 表示
pn.Row(season, sele, width=100)

 ↓
Panel紹介用_dependsデコレータ使用例.gif

上記例ではウィジェットの値が変更されるたびに右の文字が変更されています。
書き方は、@pn.depends(ウィジェット.param.value)でウィジェットの値が変動する度に実行させたい関数でデコレートしてあげます。関数の引数にはvalueの値を受け取る変数を指定します。
関数のreturnにパネルやウィジェットを指定すれば、この関数がそのままインスタンスとして使用できます。

3. ウィジェットの配置

定義したウィジェットを表示したい場合、変数名を入力するだけで表示可能です。しかし複数のウィジェットを表示したい場合、Jupyterの一セルに一つのウィジェットしか表示できません。
では、複数のウィジェットをまとめて表示する場合はどうするのかというと、pn.Rowやpn.Columnといったレイアウト用メソッドを使用します。
pn.Row()は行方向、つまり横方向にウィジェットを配置します。

# 行方向への配置
pn.Row(x_axis_sel, y_axis_sel, display_graph_but)

 ↓
10_Row配置例.JPG

pn.Column()は列方向、つまり縦方向への配置です。

# 列方向への配置
pn.Column(x_axis_sel, y_axis_sel, display_graph_but)

 ↓
11_Column配置例.JPG

さらにこれらの表示は組み合わせて使うことができます。
下記の例では、Columnで縦に並べたウィジェット群と別のウィジェットをRowで横に並べています。

# RowとColumnの組み合わせ
pn.Row(pn.Column(x_axis_sel, y_axis_sel), display_graph_but)

 ↓
12_RowとColumnの組み合わせ配置例.JPG

4. 見た目を整える

最低限動作するアプリに必要なものを用意することができました。
せっかくなので、見た目も整えてツールとしての完成度を高めていきます。

まず、ボタンやらセレクトボックスがやたら横長なので、幅を整えていきます。
同時にグラフパネルもサイズを指定しておきます。

# ボタンサイズの指定
load_file_but.width = display_graph_but.width = 150
# セレクトボックスのサイズ指定
x_axis_sel.width = y_axis_sel.width = 200
# グラフパネルのサイズ指定
graph_pane.width = 480
graph_pane.height = 360

# 表示
graph_pane.visible = True  # 一時的に変更
pn.Row(load_file_but, display_graph_but, x_axis_sel, y_axis_sel, graph_pane)

 ↓
20_サイズを整えた結果.JPG

widthで幅を指定し、heightで高さを指定します。
heightはButtonなどの一部ウィジェットには使用できないので注意。

また、文字入力に使用しているTextInputは幅が狭くなると見づらくなるので、文字入力には改行可能+高さを指定できるTextAreaInputウィジェットに変更しますTextInputでも高さ変更可能でした(2021/12/22訂正)。

# ファイルパスを入力するウィジェットとしてTextAreaInputを使用する
filename_input = pn.widgets.TextAreaInput(name= "CSVファイルパス入力", placeholder= "ここにCSVファイルのパスを入力してください", 
                                         width= 200, height= 80)
# 表示
filename_input

 ↓
21_テキストエリアインプット.JPG

ウィジェットボックス

ウィジェットのレイアウトに関連したメソッドにウィジェットボックスがあります。
例えるとコンテナのようなもので、ウィジェットボックスを使用すると比較的簡単に見た目を整えることができます。
まずpn.WidgetBox()の中に各種ウィジェットを格納します。

# ウィジェットボックスwb1へファイル読み込み関するウィジェットを格納
wb1 = pn.WidgetBox("### ファイル読み込み", filename_input, load_file_but)
# 表示
wb1

 ↓
22_ウィジェットボックスとりあえず表示.JPG

一つ目の引数に指定しているように、ウィジェット以外に文字列も直接格納出来るようです。マークダウンにも対応。

ウィジェットボックスをはじめとしたコンテナ系ウィジェットでは、
objectsメソッド(末尾のsに注意)で格納されたウィジェットをイテレータとして取り出せます。
これでfor文ですべてのウィジェットに一括でオプションの設定が可能になるので体裁を整えたい際に便利です。

# wb1のウィジェットすべてもしくは一部に書式設定
for i, object in enumerate(wb1.objects):
    # すべてのオブジェクトの余白を指定
    object.margin = [0, 20, 0, 20]
    # 先頭を除いてオブジェクトの水平・垂直方向位置を指定
    if i != 0:
        object.align = ("center","start")
    # 最後のオブジェクトだけ余白を再指定
    if i == len(wb1.objects) - 1:
        object.margin = [20, 20, 25, 20]

# 表示
wb1

 ↓
23_ウィジェットボックス体裁ととのえ.JPG

今回のケースでは、全体へのマージン設定の他に、最初と最後のオブジェクトの書式を変更したかったので、enumerate関数を使用しています。
marginはウィジェットの周囲の余白を規定します。HTMLやCSSと同じで、上・右・下・左の順に値をタプルもしくはリスト指定します。(リストでなく値を指定した場合は、すべての方向に同じ値の余白を指定します)
alignはウィジェットの水平・垂直方向への位置決めで使用します。値にはタプル形式で水平・垂直方向の順に"start","center","end"を指定します。今回の ("center","start")を与えた場合は、水平方向には真ん中・垂直方向には上寄せになります。

同様にグラフ描画に関連するウィジェットを格納したウィジェットボックスを作成します。

# ウィジェットボックスwb2へグラフ読み込みに関するウィジェットを格納
wb2 = pn.WidgetBox("### グラフ出力", x_axis_sel, y_axis_sel, display_graph_but, visible= True)

# wb2のウィジェットすべてもしくは一部に書式設定
for i, object in enumerate(wb2.objects):
    # すべてのオブジェクトの余白を指定
    object.margin = [0, 20, 0, 20]
    # 先頭を除いてオブジェクトの水平・垂直方向位置を指定
    if i != 0:
        object.align = ("center","start")
    # 最後のオブジェクトだけ余白を再指定
    if i == len(wb2.objects) - 1:
        object.margin = [20, 20, 30, 20]

# 表示
wb2

(画像は割愛)

完成

これで必要なすべてのパーツの準備が整いました。
あとはこれらウィジェットボックスとグラフを配置するだけです。

# 出力
pn.Row(pn.Column(wb1, wb2), graph_pane)

 ↓
30_完成.JPG

できました!
最終的なコード全文は下記のような感じです。(長いので折りたたみました)

クリックしてコードを表示
import panel as pn
import pandas as pd
import holoviews as hv
# おまじない
pn.extension()
# HoloViewsのバックエンド指定(bokeh)
hv.extension("bokeh")

# テキスト(ファイルパス)を入力するウィジェットの定義
# ファイルパスを入力するウィジェットとしてTextAreaInputを使用する
filename_input = pn.widgets.TextAreaInput(name= "CSVファイルパス入力", placeholder= "ここにCSVファイルのパスを入力してください", 
                                         width= 200, height= 80)

# ファイル読み込みボタンの定義
load_file_but = pn.widgets.Button(name= "ファイル読み込み", button_type= "primary")

# X軸、Y軸を選択するセレクトウィジェットの定義
x_axis_sel = pn.widgets.Select(name= "X軸")
y_axis_sel = pn.widgets.Select(name= "Y軸")

# グラフを描画するボタンの定義
display_graph_but = pn.widgets.Button(name= "グラフ描画", button_type= "primary", disabled= True)

# グラフ(HoloViews)描画パネルの定義
# データ未入力だとウィジェットエラー出るので仮のグラフデータ格納
blank_hv = hv.Scatter((1, 1))
graph_pane = pn.pane.HoloViews(blank_hv, visible = False)

# ファイル読み込みのイベント関数規定
def load_file(event):
    # 関数外でも使用するCSVカラム名とDataFrameをglobal宣言
    global col_name
    global df
    # csvファイルをデータフレームとして読み込み
    df = pd.read_csv(filename_input.value)
    # 読み込んだCSVデータフレームの列名を取得
    col_name = list(df.columns)
    # 軸項目の選択肢を設定
    x_axis_sel.options = col_name
    y_axis_sel.options = col_name
    # グラフ描画ボタンの無効化を解除
    display_graph_but.disabled = False 
    # ボタンの色を変更
    load_file_but.button_type = "success"

# グラフ描画のイベント関数規定
def display_graph(event):
    # 関数外でも使用するグラフデータをglobal宣言
    global scatter
    # 描画するグラフデータの生成
    scatter = hv.Scatter(df, kdims= [x_axis_sel.value], vdims= [y_axis_sel.value])
    # ウィジェットで描画するグラフデータの指定と表示
    graph_pane.object = scatter
    graph_pane.visible = True
    # ボタンの色を変更
    display_graph_but.button_type = "success"

# ファイル読み込みボタンへのイベント登録
load_file_but.on_click(load_file)
# グラフ出力ボタンへのイベント登録
display_graph_but.on_click(display_graph)

# ボタンサイズの指定
load_file_but.width = display_graph_but.width = 150
# セレクトボックスのサイズ指定
x_axis_sel.width = y_axis_sel.width = 200
# グラフパネルのサイズ指定
graph_pane.width = 480
graph_pane.height = 360

# ウィジェットボックスwb1へファイル読み込み関するウィジェットを格納
wb1 = pn.WidgetBox("### ファイル読み込み", filename_input, load_file_but)
# ウィジェットボックスwb2へグラフ読み込みに関するウィジェットを格納
wb2 = pn.WidgetBox("### グラフ出力", x_axis_sel, y_axis_sel, display_graph_but, visible= True)

# wb1のウィジェットすべてもしくは一部に書式設定
for i, object in enumerate(wb1.objects):
    # すべてのオブジェクトの余白を指定
    object.margin = [0, 20, 0, 20]
    # 先頭を除いてオブジェクトの水平・垂直方向位置を指定
    if i != 0:
        object.align = ("center","start")
    # 最後のオブジェクトだけ余白を再指定
    if i == len(wb1.objects) - 1:
        object.margin = [20, 20, 25, 20]

# wb2のウィジェットすべてもしくは一部に書式設定
for i, object in enumerate(wb2.objects):
    # すべてのオブジェクトの余白を指定
    object.margin = [0, 20, 0, 20]
    # 先頭を除いてオブジェクトの水平・垂直方向位置を指定
    if i != 0:
        object.align = ("center","start")
    # 最後のオブジェクトだけ余白を再指定
    if i == len(wb2.objects) - 1:
        object.margin = [20, 20, 30, 20]

# 出力
pn.Row(pn.Column(wb1, wb2), graph_pane)

今後やりたいこと

  • とりあえず色々なアプリをつくって、Panelで何をどこまでできるのかを把握したい。
  • 学習負荷が低いとは思うが、各ウィジェットのオプションをどのように指定するかは結構調べる必要があった。これらウィジェットに関する情報について整理しておきたい。
     ⇒ (2021/12/22追記)記事にまとめました。
      ダッシュボード作成ライブラリPanelのウィジェット紹介 ~入力系編~
      ダッシュボード作成ライブラリPanelのウィジェット紹介 ~出力系編~

  • 職場での展開を検討したい。その時、Python(Jupyter)を導入していないユーザーにどのように展開していくかが課題。FlaskでWebアプリとしても展開できることは確認しているので、Pyinstallerと組み合わせてexe化して配布できるか確認したい。
21
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
22