PyGWalkerダッシュボードを簡単に保存・共有するアプリを作りました
はじめに
以前PyGWalkerをStreamlitで使ってみる記事を書きましたが、その時に思いついたアイディアを実践してみました。
やりたいことの背景
前回の記事で試してわかったこととして、PyGWalkerは作成したグラフを、「export_code」ボタンからコードをコピーして、それを起動時に読み込ませることで、グラフの再利用ができることがわかりました。
ただし、これを行うためには、出力したコードをPythonコードに貼り付けて再実行する必要があり、Pythonの環境とスキルが必要です。
自分一人で使う分にはこれでも問題ないのですが、そもそも自分で使うだけならExcelのPower Queryとピボットグラフを使った方が、できることも多いですし、より簡単に行えます。
せっかくStreamlitでWebアプリとして公開できるのですから、その強みを活かして、複数人(Pythonスキルの無い人含む)で共有できるようにしたいと思いました。
作ったもの
起動方法
以下のリポジトリにコードを置いています。
dockerを使用するので、事前にdocker desktop等をインストールしておいてください。
こちらをクローンして、以下のコマンドで起動します。
docker compose up -d
ビルドが終わったら、以下のURLにアクセスしてください。
使い方
テンプレートの起動
アプリを開くと、以下のような画面が表示されます。
まだ何も作成していないので、「既存のダッシュボードを開く」の方には何も表示されません。
「新規ダッシュボードを作成」をクリックすると、以下のような画面が表示されます。
まず、この画面でダッシュボードのベースとなるテンプレートを選択します。
テンプレートとして以下の二つを用意しています。
- template_1_csv.py : 1つのCSVファイルを読み込んで、グラフを作成するテンプレート
- template_2_csv.py : 2つのCSVファイルを読み込んで、マージしたデータを作成し、グラフを作成するテンプレート
まずは、template_1_csv.pyを選択して、「テンプレートを開く」をクリックします。
すると、以下のような画面が表示されます。
グラフの作成
ここに適当なCSVファイルをアップロードすると、PyGWalkerが起動するのでグラフを作成します。
(ここではリポジトリにあるsample_data/test_data_1.csvを使っています)
とりあえず以下のように2つグラフを作成しました。
ダッシュボードの保存
グラフができたので「export_code」ボタンを押し、表示されたウィンドウの「Copy to Clipboard」ボタンを押します。
サイドバーにある「ダッシュボードの保存」に必要項目を入力し、「保存」ボタンを押します。
(アプリ上では作成したグラフ群を「ダッシュボード」と呼んでいます)
- カテゴリ名 : ダッシュボードを分類するためのカテゴリ名
- ダッシュボード名 : 作成したダッシュボードの名前
- ダッシュボードの説明 : 作成したダッシュボードの説明
- グラフのコード : 先ほどコピーしたコードを貼り付け
今回は以下のように入力しました。
- カテゴリ名 :
実績
- ダッシュボード名 :
販売数
- ダッシュボードの説明 :
販売実績のCSVデータをアップロードすると、「店舗別」「品目別」でそれぞれ集計したグラフが表示されます。
- グラフのコード :
vis_spec = r"""{"config":[{"config":{"defaultAggregated":true,"geoms":["auto"],"coordSystem":"generic","limit":-1,"timezoneDisplayOffset":0},"encodings":{"dimensions":[{"dragId":"gw_DJJS","fid":"日付","name":"日付","basename":"日付","semanticType":"temporal","analyticType":"dimension","offset":0},{"dragId":"gw_duH3","fid":"品目","name":"品目","basename":"品目","semanticType":"nominal","analyticType":"dimension","offset":0},{"dragId":"gw_is6J","fid":"店舗","name":"店舗","basename":"店舗","semanticType":"nominal","analyticType":"dimension","offset":0},{"dragId":"gw_mea_key_fid","fid":"gw_mea_key_fid","name":"Measure names","analyticType":"dimension","semanticType":"nominal"}],"measures":[{"dragId":"gw_Zl_c","fid":"個数","name":"個数","basename":"個数","semanticType":"quantitative","analyticType":"measure","offset":0},{"dragId":"gw_count_fid","fid":"gw_count_fid","name":"Row count","analyticType":"measure","semanticType":"quantitative","aggName":"sum","computed":true,"expression":{"op":"one","params":[],"as":"gw_count_fid"}},{"dragId":"gw_mea_val_fid","fid":"gw_mea_val_fid","name":"Measure values","analyticType":"measure","semanticType":"quantitative","aggName":"sum"}],"rows":[{"dragId":"gw_DJZh","fid":"個数","name":"個数","basename":"個数","semanticType":"quantitative","analyticType":"measure","offset":0,"aggName":"sum"}],"columns":[{"dragId":"gw_jh0h","fid":"店舗","name":"店舗","basename":"店舗","semanticType":"nominal","analyticType":"dimension","offset":0,"sort":"descending"}],"color":[],"opacity":[],"size":[],"shape":[],"radius":[],"theta":[],"longitude":[],"latitude":[],"geoId":[],"details":[],"filters":[],"text":[]},"layout":{"showActions":false,"showTableSummary":false,"stack":"stack","interactiveScale":false,"zeroScale":true,"size":{"mode":"full","width":320,"height":200},"format":{},"geoKey":"name","resolve":{"x":false,"y":false,"color":false,"opacity":false,"shape":false,"size":false}},"visId":"gw_NSKd","name":"店舗別"},{"config":{"defaultAggregated":true,"geoms":["auto"],"coordSystem":"generic","limit":-1,"timezoneDisplayOffset":0},"encodings":{"dimensions":[{"dragId":"gw_DJJS","fid":"日付","name":"日付","basename":"日付","semanticType":"temporal","analyticType":"dimension","offset":0},{"dragId":"gw_duH3","fid":"品目","name":"品目","basename":"品目","semanticType":"nominal","analyticType":"dimension","offset":0},{"dragId":"gw_is6J","fid":"店舗","name":"店舗","basename":"店舗","semanticType":"nominal","analyticType":"dimension","offset":0},{"dragId":"gw_mea_key_fid","fid":"gw_mea_key_fid","name":"Measure names","analyticType":"dimension","semanticType":"nominal"}],"measures":[{"dragId":"gw_Zl_c","fid":"個数","name":"個数","basename":"個数","semanticType":"quantitative","analyticType":"measure","offset":0},{"dragId":"gw_count_fid","fid":"gw_count_fid","name":"Row count","analyticType":"measure","semanticType":"quantitative","aggName":"sum","computed":true,"expression":{"op":"one","params":[],"as":"gw_count_fid"}},{"dragId":"gw_mea_val_fid","fid":"gw_mea_val_fid","name":"Measure values","analyticType":"measure","semanticType":"quantitative","aggName":"sum"}],"rows":[{"dragId":"gw_DJZh","fid":"個数","name":"個数","basename":"個数","semanticType":"quantitative","analyticType":"measure","offset":0,"aggName":"sum"}],"columns":[{"dragId":"gw_QMuf","fid":"品目","name":"品目","basename":"品目","semanticType":"nominal","analyticType":"dimension","offset":0,"sort":"descending"}],"color":[],"opacity":[],"size":[],"shape":[],"radius":[],"theta":[],"longitude":[],"latitude":[],"geoId":[],"details":[],"filters":[],"text":[]},"layout":{"showActions":false,"showTableSummary":false,"stack":"stack","interactiveScale":false,"zeroScale":true,"size":{"mode":"full","width":320,"height":200},"format":{},"geoKey":"name","resolve":{"x":false,"y":false,"color":false,"opacity":false,"shape":false,"size":false}},"visId":"gw_dO62","name":"品目別"}],"chart_map":{},"workflow_list":[{"workflow":[{"type":"view","query":[{"op":"aggregate","groupBy":["店舗"],"measures":[{"field":"個数","agg":"sum","asFieldKey":"個数_sum"}]}]}]},{"workflow":[{"type":"view","query":[{"op":"aggregate","groupBy":["品目"],"measures":[{"field":"個数","agg":"sum","asFieldKey":"個数_sum"}]}]}]}],"version":"0.4.7"}"""
「保存しました」と表示されたら、サイドバーの一番上にある「Homeに戻る」ボタンから、最初の画面に戻ります。
すると、以下のように「既存のダッシュボードを開く」に、先ほど作成したダッシュボードが表示されます。
保存したダッシュボードを開く
「ダッシュボードを開く」ボタンを押すと、先ほどのテンプレートを開いた時と同じような画面が表示されます。
違いとしては、「使い方」として先ほど保存したダッシュボードの説明が表示されていることです。
この画面でCSVデータをアップロードすると、先ほど作成したグラフが表示され、保存したダッシュボードが再利用できることがわかります。
これにより、販売実績のCSVデータから、店舗別、品目別のグラフを簡単に作成できるようになりました。
当然ですが、先ほどアップロードしたCSVファイルと異なる形式のCSVファイルをアップロードすると、エラーが表示されます。
ダッシュボードに変更を加えたい場合は、変更後に再度「export_code」ボタンを押し、コードをコピーして、サイドバーの「ダッシュボードの保存」から保存し直すことで、変更を反映できます。
別のテンプレートを使う
さすがにこれだけだと、Excelとあまり差が無いので、もう一つのテンプレートを使ってみます。
「Homeに戻る」ボタンを押し、再度「新規ダッシュボードを作成」をクリックし、今度はtemplate_2_csv.pyを選択して、「テンプレートを開く」をクリックします。
すると、以下のような画面が表示されます。
先ほどのファイルに加えて、sample_data/sample_data/test_data_2.csvをアップロードします。
すると以下のようにエラー画面が表示されます。
template_2_csv.pyは、2つのCSVファイルをマージしてグラフを作成するテンプレートなのですが、マージの起点となる選択中のカラムの内容が、まったく一致しないため、エラーが表示されているだけです。
sample_data/test_data_2.csvは、品目ごとの単価が記載されているので、カラムの選択を両方とも「品目」に変更し、マージの方法を「left」に変更します。
(left joinを行うことで、test_data_1.csvにあるデータを全て残しつつ、test_data_2.csvにあるデータを結合します)
こうすることでエラーが解消され、グラフが表示されます。
結合後のデータを使って、先ほどのグラフに売上金額を追加したものを作成しました。
売上金額の算出方法ですが、「Add Computed Field」ボタンを押し、以下のように設定しました。
このダッシュボードも、先ほどと同様に保存します。
- カテゴリ名 :
実績
- ダッシュボード名 :
販売数・売上金額
- ダッシュボードの説明 :
1つ目のファイルは販売実績をアップロードしてください。2つ目のファイルは品目ごとの単価データをアップロードしてください。「店舗別」「品目別」でそれぞれ集計した販売数と売上金額のグラフが表示されます。
- グラフのコード :
vis_spec = r"""{"config":[{"config":{"defaultAggregated":true,"geoms":["auto"],"coordSystem":"generic","limit":-1,"timezoneDisplayOffset":0},"encodings":{"dimensions":[{"dragId":"gw_qpjp","fid":"日付","name":"日付","basename":"日付","semanticType":"temporal","analyticType":"dimension","offset":0},{"dragId":"gw_WEjn","fid":"品目","name":"品目","basename":"品目","semanticType":"nominal","analyticType":"dimension","offset":0},{"dragId":"gw_bt65","fid":"店舗","name":"店舗","basename":"店舗","semanticType":"nominal","analyticType":"dimension","offset":0},{"dragId":"gw_mea_key_fid","fid":"gw_mea_key_fid","name":"Measure names","analyticType":"dimension","semanticType":"nominal"}],"measures":[{"dragId":"gw_lzWR","fid":"個数","name":"個数","basename":"個数","semanticType":"quantitative","analyticType":"measure","offset":0},{"dragId":"gw_WGGb","fid":"単価","name":"単価","basename":"単価","analyticType":"measure","semanticType":"quantitative","aggName":"sum","offset":0},{"analyticType":"measure","dragId":"gw_pXj4","fid":"gw_pXj4","name":"売上","semanticType":"quantitative","computed":true,"aggName":"sum","expression":{"op":"expr","as":"gw_pXj4","params":[{"type":"sql","value":"\"単価\" * \"個数\""}]}},{"dragId":"gw_count_fid","fid":"gw_count_fid","name":"Row count","analyticType":"measure","semanticType":"quantitative","aggName":"sum","computed":true,"expression":{"op":"one","params":[],"as":"gw_count_fid"}},{"dragId":"gw_mea_val_fid","fid":"gw_mea_val_fid","name":"Measure values","analyticType":"measure","semanticType":"quantitative","aggName":"sum"}],"rows":[{"dragId":"gw_-pLJ","fid":"個数","name":"個数","basename":"個数","semanticType":"quantitative","analyticType":"measure","offset":0,"aggName":"sum"},{"analyticType":"measure","dragId":"gw_AQdt","fid":"gw_pXj4","name":"売上","semanticType":"quantitative","computed":true,"aggName":"sum","expression":{"op":"expr","as":"gw_pXj4","params":[{"type":"sql","value":"\"単価\" * \"個数\""}]}}],"columns":[{"dragId":"gw_iZzS","fid":"店舗","name":"店舗","basename":"店舗","semanticType":"nominal","analyticType":"dimension","offset":0}],"color":[],"opacity":[],"size":[],"shape":[],"radius":[],"theta":[],"longitude":[],"latitude":[],"geoId":[],"details":[],"filters":[],"text":[]},"layout":{"showActions":false,"showTableSummary":false,"stack":"stack","interactiveScale":false,"zeroScale":true,"size":{"mode":"full","width":320,"height":200},"format":{},"geoKey":"name","resolve":{"x":false,"y":false,"color":false,"opacity":false,"shape":false,"size":false}},"visId":"gw_F7-x","name":"店舗別"},{"config":{"defaultAggregated":true,"geoms":["auto"],"coordSystem":"generic","limit":-1,"timezoneDisplayOffset":0},"encodings":{"dimensions":[{"dragId":"gw_qpjp","fid":"日付","name":"日付","basename":"日付","semanticType":"temporal","analyticType":"dimension","offset":0},{"dragId":"gw_WEjn","fid":"品目","name":"品目","basename":"品目","semanticType":"nominal","analyticType":"dimension","offset":0},{"dragId":"gw_bt65","fid":"店舗","name":"店舗","basename":"店舗","semanticType":"nominal","analyticType":"dimension","offset":0},{"dragId":"gw_mea_key_fid","fid":"gw_mea_key_fid","name":"Measure names","analyticType":"dimension","semanticType":"nominal"}],"measures":[{"dragId":"gw_lzWR","fid":"個数","name":"個数","basename":"個数","semanticType":"quantitative","analyticType":"measure","offset":0},{"dragId":"gw_WGGb","fid":"単価","name":"単価","basename":"単価","analyticType":"measure","semanticType":"quantitative","aggName":"sum","offset":0},{"analyticType":"measure","dragId":"gw_pXj4","fid":"gw_pXj4","name":"売上","semanticType":"quantitative","computed":true,"aggName":"sum","expression":{"op":"expr","as":"gw_pXj4","params":[{"type":"sql","value":"\"単価\" * \"個数\""}]}},{"dragId":"gw_count_fid","fid":"gw_count_fid","name":"Row count","analyticType":"measure","semanticType":"quantitative","aggName":"sum","computed":true,"expression":{"op":"one","params":[],"as":"gw_count_fid"}},{"dragId":"gw_mea_val_fid","fid":"gw_mea_val_fid","name":"Measure values","analyticType":"measure","semanticType":"quantitative","aggName":"sum"}],"rows":[{"dragId":"gw_-pLJ","fid":"個数","name":"個数","basename":"個数","semanticType":"quantitative","analyticType":"measure","offset":0,"aggName":"sum"},{"analyticType":"measure","dragId":"gw_AQdt","fid":"gw_pXj4","name":"売上","semanticType":"quantitative","computed":true,"aggName":"sum","expression":{"op":"expr","as":"gw_pXj4","params":[{"type":"sql","value":"\"単価\" * \"個数\""}]}}],"columns":[{"dragId":"gw_I630","fid":"品目","name":"品目","basename":"品目","semanticType":"nominal","analyticType":"dimension","offset":0,"sort":"descending"}],"color":[],"opacity":[],"size":[],"shape":[],"radius":[],"theta":[],"longitude":[],"latitude":[],"geoId":[],"details":[],"filters":[],"text":[]},"layout":{"showActions":false,"showTableSummary":false,"stack":"stack","interactiveScale":false,"zeroScale":true,"size":{"mode":"full","width":320,"height":200},"format":{},"geoKey":"name","resolve":{"x":false,"y":false,"color":false,"opacity":false,"shape":false,"size":false}},"visId":"gw_Ll4h","name":"品目別"}],"chart_map":{},"workflow_list":[{"workflow":[{"type":"transform","transform":[{"key":"gw_pXj4","expression":{"op":"expr","as":"gw_pXj4","params":[{"type":"sql","value":"(\"単価\" * \"個数\")"}]}}]},{"type":"view","query":[{"op":"aggregate","groupBy":["店舗"],"measures":[{"field":"個数","agg":"sum","asFieldKey":"個数_sum"},{"field":"gw_pXj4","agg":"sum","asFieldKey":"gw_pXj4_sum"}]}]}]},{"workflow":[{"type":"transform","transform":[{"key":"gw_pXj4","expression":{"op":"expr","as":"gw_pXj4","params":[{"type":"sql","value":"(\"単価\" * \"個数\")"}]}}]},{"type":"view","query":[{"op":"aggregate","groupBy":["品目"],"measures":[{"field":"個数","agg":"sum","asFieldKey":"個数_sum"},{"field":"gw_pXj4","agg":"sum","asFieldKey":"gw_pXj4_sum"}]}]}]}],"version":"0.4.7"}"""
Homeに戻ると、先ほど保存したダッシュボードが一覧に表示されていることがわかります。
このように、テンプレートの工夫次第で、様々なダッシュボードを作成し、保存・共有することができます。
売上金額のカラムのように、追加したカラムも保存されていますので、何度も同じ作業を繰り返す必要がありません。
仕組みの概要
新規ダッシュボードの作成から保存を行うと、以下の情報がDBに保存されます。
(DBにはSQLiteを使用しています)
- カテゴリ名
- ダッシュボード名
- ダッシュボードの説明
- グラフのコード
- templateのファイル名
- パラメータ
カテゴリ名とダッシュボード名は、ダッシュボードを一意に特定するための情報です。
ダッシュボードの説明は、使い方を記載するための情報です。
グラフのコードは、export_codeボタンを押した際にコピーしたコードです。
templateのファイル名は、ダッシュボードを作成した際に選択したテンプレートのファイル名です。
パラメータは、templateのファイル名によって異なりますが、template_2_csv.pyの場合は、マージの起点となるカラムの選択とマージ方法の選択が保存されます。
保存されたダッシュボードを開く際は、まずこれらの情報をDBから取得し、st.session_stateに保存します。
次にtemplateのファイル名に対応するページに遷移し、st.session_stateに保存された情報を使って、ダッシュボードを作成します。
この時テンプレートとしてページが開かれたのか、保存されたダッシュボードが開かれたのかを判別するために、st.session_state.template_modeに、True/Falseを保存しています。
遷移したtemplateページは、st.session_stateに保存されている情報を使い、新規作成か既存のダッシュボードを開くかを判別し、それぞれの処理を行います。
templateの作り方
このアプリはtemplateを追加することで、利便性が大幅に向上すると思っています。
例えば、DBにSQLを発行してデータを取得するテンプレートや、クラウド上のデータを取得するテンプレートなど、データが集まっているところへのアクセス部分をテンプレートとして作成することで、データ分析の効率が大幅に向上すると思います。
自分としては、業務でAWSを使用しているため、AWSのCloudWatch Logs Insightsのデータを取得するテンプレートを作成してみたいと思っています。
ログデータを取得してその場で分析できると、障害対応などが大幅に効率化できますし、それらを共有することでチーム全体の効率化・スキル平準化にもつながります。
リポジトリに_template_base.pyというファイルを用意していますので、これをコピーして、新しいテンプレートを作成してみてください。
中身は以下のようになっています。
from pygwalker.api.streamlit import init_streamlit_comm
import pandas as pd
import streamlit as st
from utils import get_pyg_renderer, save_dashboard
st.set_page_config(page_title="PyGWalker WorkSpace", layout="wide")
# Homeに戻るリンクを追加
st.sidebar.page_link(page="Home.py", label="Homeに戻る")
# PyGWalkerとStreamlitの通信を確立する
init_streamlit_comm()
if st.session_state.template_mode:
# テンプレートモードの場合、specをデフォルトに設定する
spec = "./gw_config.json"
else:
# テンプレートモードでない場合、specをセッションから取得し、使い方を表示する
spec = st.session_state.spec
st.markdown(f"### 使い方\n\n{st.session_state.discription}")
# ==============================================
# ここにPyGWalkerに渡すdfを作成するコードを記述する。
# ==============================================
renderer = get_pyg_renderer(df, spec)
# データ探索インターフェースをレンダリングする。
renderer.render_explore()
# ダッシュボードの保存UIを表示
# ダッシュボードの表示に必要なパラメータが有れば、それをdbに保存する
st.session_state.params = {
"param1": param1,
"param2": param2,
"param3": param3,
}
save_dashboard(st.session_state.params)
template_1_csv.pyとtemplate_2_csv.pyを参考にしてもらえれば、どのように作成すれば良いかがわかると思います。
もしわからないことがあれば、コメントで聞いていただければ、お力になれるかもしれません。
まとめ
PyGWalkerのダッシュボードを簡単に保存・共有するアプリを作成しました。
突貫工事なところも多いですが、割と業務でも使えるものだと思うので、もし興味があれば試してみてください。
Discussion