2022年のrstudio::confにて発表された、R言語を使ったdashboard作成フレームワークであるshinyをpythonに移植したものです。昔からshinyに触ったことのある方なら、驚きましたよねー😲
2022年8月時点ではまだリリースされたばかりのα版となっており、動作の保証は一切ないばかりか、利用報告も全然見かけません。
今回、一通りコードを書いてshinyapps.ioにアップするところまで成功しましたので、この記事ではその一連の流れについてご紹介しようと思います。最後の方ではstreamlitやR版Shinyなどとも比較してみています💡
shiny-pythonやrsconnect-pythonなどのコマンドは今後使用が変更される可能性がありますので、利用を試す際には最新情報をチェックしましょう⚠️
Shinyは他でもないRstudio社が開発したウェブフレームワークであり、R言語を使ってインタラクティブなウェブページ(Apps)の作成が可能です。歴史はそこそこ長く、初出は2012年、2015年ごろには既にflexdashboard等のアドインも作られ、今とほぼ同じ開発体験が可能となっていました。
バックエンドでRが動作しなければならない等の制約があり、作成したAppの公開先はshinyapps.ioやShinyServerなどと場所を選びますが、当時はかなり先進的なフレームワークとしてもてはやされていました。
しかし今ではGoogleColabを通じたJupyterNotebookの共有や、Plotly社のDash、Streamlit社のStreamlitといったPythonフレームワークの方がShinyよりも人気になっており、私自身もStreamlitを使った社内ツールを活用しています。以前ほどの注目を集めていないShinyがこのタイミングでPythonに移植されることにどれほど意味があるのだろうか🤔と疑問に思われる方も多いでしょうね。
python版shinyのメリットは以下のような点でしょうか。
最大のメリットはWebAssembly(WASM)へのコンパイルによる高速化ではないかと個人的に思いますが、特にそれが売りっていうわけでもないようです。[ref]https://www.rstudio.com/conference/2022/keynotes/past-future-shiny/ Hadleyとのトークで明らかにされています。[/ref]
今回はstreamlitとshinyR、Shiny for Pythonを個人的に比較してみて、改めてWebAppを作る上での私なりの最適解を見いだすことを目的としました。
そこで、①典型的なグラフ作成を含み、②動的なApp機能があり、③キャッチーな内容である、厚生労働省の「データからわかる-新型コロナウイルス感染症情報-」を題材として、一部をクローンすることにしました。
制作方針は、それぞれのアドインなどはフル活用しつつなるべく簡単に美しいページを作成することです。
https://covid19.mhlw.go.jp/extensions/public/index.html
それぞれの善し悪しなどについては後日別記事に遺そうと思います。とりあえず作ったものを列挙すると以下です。
https://snitch0-mhlw-clone-streamlit-app-jaculn.streamlitapp.com/
一番最初に作りました。制作期間は土日含めた三日(約5時間)くらいです。
最近streamlitアプリを業務で運用したりしていたこともあり、いつも通り楽しく制作できました。
stremalitの次にはShinyRで作りました。
昔何度かshinyで簡単なツールを何個か作ったことがあるのですが、shinyRではIN/OUTの仕組みを作るのが面倒だと感じていたのと、バニラの状態では見た目があまり美しくないということを先入観として持っていました。 なので、今回はflexdashboard
パッケージを使用しました。おかげで、制作は思っていたよりもスムーズに進みました。
制作期間は1週間(約10時間)で、レイアウト崩壊問題やデプロイがうまくいかないなどの問題対処にほとんどの時間を持って行かれました。この問題はストレスフルでしたが、まあ順調に制作できたと思います。
https://snitch.shinyapps.io/ipyshiny\_covid1/
これが本題ですが、今回紹介するPython版Shinyは最後に作りました。
まだα版のShiny for Pythonには便利なサードパーティーライブラリが全くといって良いほどなく、さすがに可哀想な見た目なので、わずかばかりのカスタムcssで見た目を補いました。
ここからはShiny for Pythonの使い方についてレポートしていきたいと思います。はじめに注意事項ですが、pythonとライブラリのバージョン依存によるエラーが発生しやすいです。 必ずcondaやvenvといった仮想環境を使ってバージョンコントロールをしましょう。
condaならminiforgeがおすすめです。venvの使い方はググれば沢山出てくると思います。
この記事ではUbuntu20.04 LTS(WSL2)を使用した例を紹介します。 MacOSでも多分可能だとは思いますが、Windowsのconda環境ではうまくrsconnectコマンドを動作させることはできませんでした。※デプロイに使うrsconnect以外はWindowsでも問題無いと思います。多分。
また、minicondaが導入されている前提で話を進めます。pipしか使わないので、python venvでもpipenvでも問題ありません。
まず環境を作ります。どういうわけか、pythonバージョンが違っただけでデプロイに失敗したので、バージョン指定はきちんと行いましょう。
mamba create -n shinyenv python=3.9.13
本筋とは関係ないですが、最近でもtwitterで「condaは環境汚すからヤベェ」というコメントを定期的に見かけますが、そんなに心配することは無いと思います。condaを介してインストールされるpythonライブラリはpipのインストールとは競合しないよう改善されたせいか、最近では環境が乱れる現象をほとんど見かけません。 また、mambaコマンドを使用するとインストール速度が超高速化するため、サクッと環境構築することも可能になります。
次にpythonの必要ライブラリを用意します。
ちゃんと作成したばかりのpipが使われてることを確認しておきましょう。
conda activate shinyenv
which pip3
# ~/mambaforge/envs/shinyenv/bin/pip3
pip install shiny shinywidgets htmltools jupyter rsconnect-python
以下はグラフ作成等に使用するライブラリです。
pip install pandas numpy altair vega
これはオプショナルです。
Pythonの開発環境ならどこでも大丈夫ですが、今回はVSCodeを使ってAppを作成します。便利な拡張機能が作られていますので、インストールしておきましょう。
https://marketplace.visualstudio.com/items?itemName=rstudio.pyshiny
また、公式のガイドの通りにlinterなどもセットアップしておくと良いでしょう。
これで環境がうまく構築できたはずです。試しになんか作って動かしてみます。
shinyコマンドには空のプロジェクトを生成するAPIが用意されているので、使ってみます。
$ shiny create testapp
Created Shiny app at testapp/app.py
非常に簡単なapp.py
が生成されました。内容は以下のShiny Examplesの例と全く同じものです。
Exampleページのプレビューを見るだけでも動作確認が簡単にできますが、VSCodeの拡張を使っても同様の結果を得ることが出来ます。
以下のようなログとともに、ポート8000でAppが立ち上がります。VSCode拡張を使うとVSCode内でAppがシンプルブラウザーを使って立ち上がるので非常に便利です。
> /home/snitch/mambaforge/envs/shinyenv/bin/python -m shiny run --port 8000 --reload "/home/snitch/R/ipyshiny_test/testapp/app.py"
INFO: Will watch for changes in these directories: ['/home/snitch/R/ipyshiny_test/testapp']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [2233] using StatReload
INFO: Started server process [2242]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: 127.0.0.1:51224 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /?vscodeBrowserReqId=1660601926014 HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /require.min.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /bootstrap.min.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /shiny.min.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /css/ion.rangeSlider.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /shiny.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /jquery-3.6.0.min.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /bootstrap.bundle.min.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /js/ion.rangeSlider.min.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /strftime-min.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:51226 - "GET /shiny-autoreload.js HTTP/1.1" 200 OK
INFO: ('127.0.0.1', 51240) - "WebSocket /websocket/" [accepted]
INFO: connection open
続いて、私が作ったCOVIDダッシュボードについてかいつまんで解説します💡
R版のShinyを使ったことがある方ならご存じだと思いますが、Shinyはuiパートとserverパートの二つで構成されており、uiのところにhtmlやデータ入力ウィジェットを配置、serverのとこにデータフレーム変形やグラフ作成等の計算処理を記述します。
UI側はTagクラスのオブジェクトだけを複数内包したオブジェクトであるのに対して、server側は自由な関数定義で構成され、自由な処理が可能です。
uiとserver間でデータをやりとりしたい場合、例えばserver側にあるiris
というデータフレームをuiに表示させたい場合、@render.table
デコレーターを使ってテーブルを描画し、@output
デコレーターでI/Oストリームに乗せます。
この記述方法は非常に良く頑張ってShinyRの書き方に寄せたな、という印象です。デコレーターを使うことで、非常にすっきりとしたAPIになっているのも素晴らしいと思います👍 しかし、オブジェクトをオブジェクトとして受け渡しできないのはなんかpythonらしくない、わかりにくい表現だなあ、と思います。ShinyRもそんな感じの受け渡し方なので、あえて記法をそろえようとしているのかもしれません。
UIパートで実装した主な機能は以下です。
なぜかこの機能だけは充実した実装になっています🙂
ui.navset_pill()
や、ui.navset_pill_list()
, ui.navset_pill_card()
などが用意されており、公式ドキュメントもそこそこ充実しています。
https://shiny.rstudio.com/py/docs/ui-page-layouts.html
今回は以下のようにheader
オプションを利用して都道府県選択を先頭に表示させました。
app_ui = ui.page_fluid(
ui.navset_pill(
ui.nav(
ui.markdown("## Page 1")
# widget群
),
ui.nav(
ui.markdown("## Page 2")
# widget群
),
header=ui.input_select(
id="
prefecture",
label="都道府県ごとに閲覧できます。",
choices=list(pref.keys()),
),
),
)
個人的にPython版Shinyの実装で面白いなーと思ったのが、uiコンポーネントはtagクラスのタプルで構成されている点です。pythonでは末尾のカンマ(通称ケツカンマ)があっても無くても問題なくタプルを定義できるため、若干javascriptと同じようなノリでWebアプリ作成が可能となっていると思いました。
素のShinyはRでもPythonでも高水準なUIを提供してくれません。Streamlitは何もしなくても非常に美しいUIを作成できますし、R版Shinyも拡張機能である{flexdashboard}
などを使えば概ね同じ事ができます。
しかし、今回のPython版Shinyはまだα版ですし、プラグインが全くありません。 さすがに分が悪くて可哀想なのでカスタムcssを使ってその差を埋めてあげることにしました。
とは言っても、cssは厚労省のウィジェットから拝借し、細かいところを調整しただけです。
カスタムcssを”style.css”という名前で保存しておき、これをshiny側で読み込みます。ファイルの保存場所は全部ルート以下にしておきます。
.
├── app.py
├── metrics_box.py
├── plot_figure.py
├── plot_func.py
├── prefecture_dictionary.py
└── style.css
あとはcssの内容をshiny.ui.tags.style()
メソッドに直で入力すればOKです。今回はワンライナーでpathlib.Path.read_text()
を使いました。
from htmltools import head_content
app_ui = ui.page_fluid(
# head
head_content(
ui.tags.meta(charset="UTF-8"),
ui.tags.style(
(Path(__file__).parent / "style.css").read_text()
),
ui.tags.link(rel="stylesheet",
href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css"),
ui.tags.link(rel="stylesheet",
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"),
),
)
その他metaタグや外部cssを読み込むコードもありますが、一般的なHTMLページと同じ内容です。この辺りの書き方は公式galleryの”wordle”を参考にしました。
あとはcssクラスを利用したコンテンツを作って表示したいのですが、「都道府県プルダウン」の入力を受け付けるため、インタラクティブに数値を表示させる必要があります。 インタラクティブな数値は後述のserverパートにて定義し、ここではHTMLタグの内容を記述します。
from shiny.ui import p, span, div, br
def metrics_card_item(str_title: str, num_main: int, num_sub=0):
num_main_str = f'{num_main:,}'
arrow_char = "⬆︎" if num_sub > 0 else "⬇"
if num_sub:
num_sub_str = f'{num_sub:,}'
card = div(
div(
p(str_title),
p(
span(num_main_str, class_="col4-pattern1_num",
id="curSituNewCaseKPI"),
span("人", class_="fontSize3"),
br(),
span("前日比", class_="fontSize4"),
span(arrow_char,
class_="fontSize8",
id="curSituNewCaseArw",
style="color: rgb(204, 0, 0)"),
span(num_sub_str, class_="fontSize6", id="curSituNewCaseDB"),
span("人", class_="fontSize7")
),
class_="col4-pattern1_sub"
), class_="col4-pattern1_item"
)
こんな感じで、shiny.ui
で定義されたp, span, div
のようなメソッドを使います。HTMLの属性をid=****
と指定していくのですが、pythonではclass
がクラス定義の際に使用する予約語となっているため、代わりにclass_
という引数が用意されています。
このように自由なHTMLを作ることができるのは創作の幅が広がって良いですね♪
今回は①都道府県の入力と、②グラフ表示期間の入力の二つをユーザー操作から取得しています。
①の都道府県についてはui.input_select()
を使用し、データの照合にはpythonの辞書型を使ってみました。
def create_pref_dict():
en_list = [
"ALL",
...,
"kumamoto", "oita", "miyazaki", "kagoshima", "okinawa"
]
en_c_list = [s.capitalize() if s.islower() else s for s in en_list]
ja_list = [
"全国",
...,
"熊本", "大分", "宮崎", "鹿児島", "沖縄"
]
pref_dict = {key:[val1, val2] for key, val1, val2 in zip(
ja_list, en_c_list, range(len(ja_list)))}
return pref_dict
以上のような辞書内包表記を使うことで、以下のような辞書型オブジェクトを生成することができ、以下のようにデータの変換・参照が簡単になります。
>>> pref = create_pref_dict()
>>> pref["熊本"]
['Kumamoto', 43]
そんなこんなで作成した都道府県入力と、グラフの期間指定のラヂオボタン入力を使ったデータの受け渡し実装は以下のような感じになりました。
pref = create_pref_dict()
app_ui = ui.page_fluid(
ui.input_select(
id="prefecture",
label="都道府県ごとに閲覧できます。",
choices=list(pref.keys()),
),
ui.input_radio_buttons(
"rb1",
"グラフ表示期間",
{
"week": "1週間",
"month": "1か月",
"3months": "3か月",
"year": "1年"
},
selected="year"
)
)
def server(input, output, session):
@output
@render.plot
def my_ploy():
plot = plot_func.plot_line_cases(
url="https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv",
prefec=pref[input.prefecture()][0],
period=input.rb1()
)
UIパートでオブジェクトの名前をstr型で指定(上記例ではprefectureとrb1)しつつ、serverパートでは`input`のメソッドとして呼び出していることになります。 ここはlike a magicって感じで、pythonらしくはないですが、使いやすいAPIに仕上がっていると思います。
蛇足ですが、驚くことにこの記述でもmypyチェックを問題なく通過します。重要な処理の多くがデコレーターに隠蔽されているのでしょうね🤔
グラフの表示方法はR版Shinyとほとんど同じです。server側で作成したプロットを出力するだけです。
ただし、shinyライブラリで実装されているui.render_plot()
メソッドではmatplotlibのPlotオブジェクトしか受け付けないらしく、matplotlibかseabornライブラリによるプロットしか使えません😥
そこで、py-shinywidgetsライブラリを使用しました。これを使えばplotlyやaltair、vegaといった主要なインタラクティブプロットフレームワークが全て使用可能になります。
from shinywidgets import output_widget, render_widget
import plot_figure
app_ui = ui.page_fluid(
ui.columns(
output_widget(plot1_1)
)
)
def server(input, output, session):
@output
@render_widget
def plot1_1():
return plot_figure.plot_new_cases(
url="https://covid19.mhlw.go.jp/public/opendata/newly_confirmed_cases_daily.csv",
plot_range=input.rb1(),
ytick_space=50000,
color="#fd6262",
prefecture=pref[input.prefecture()][0]
)
plotに関する関数定義は後述しますが、別ファイルにて定義しています。
こちらではinputから値を受け取り、altairグラフを作成し、outputストリームを通じてUIパートにグラフを渡します。
データの受け取り方は前述の通りinput.prefecture()
のようにinput
クラスのメソッドとして受け取りますが、outputはデコレーターで装飾された関数を定義すれば自動的に関数の戻り値が渡される仕様となっています。
今回は使ったことのないaltairグラフを使用して作ってみました。
plotlyは昔何度も使ったことがあるのですが、なんとなく最近はbokeh・vega系の方が人気なイメージがあり、vega系のフレームワークとして有名なaltairに触れてみたかったのです。ちょっと使ってみた感じ、ドキュメントが分かりにくいのは玉に瑕ですが、使いやすくて結構好きでした。
これから業務でインタラクティブプロットを作る際はaltairを採用しよう、と思えるくらいに好きでした。
後で気づきましたが、今回の「期間指定」程度の入力であればShinyの力を借りずともbokehやaltair内だけで完結できました。やったことないので今度挑戦します🦾
今回作成したプロットは以下のようにpandas.DataFrame
を用意し、altairで記述しています。本筋ではないので解説は省略します。
def plot_new_cases(plot_range: str, url: str, ytick_space: int, color: str,
prefecture: str):
df = pd.read_csv(url)
df["Date"] = pd.to_datetime(df["Date"])
df["col"] = color
chart = alt.Chart(filter_df_with_daterange(df, plot_range)).mark_area(
).encode(
alt.Y(prefecture, axis=alt.Axis(
values=[i*ytick_space for i in range(1, 6, 1)])),
x="Date",
color=alt.Color("col", scale=None)
)
return chart
というわけで今回はαリリースされたばかりのPython版Shinyを試してみました。まだ使ってみた系の記事を書いてる人は見かけないので、これからトライしてみる誰かの一助になれば幸いです🖐️
簡単ではあるものの、せっかく一つのまとまったモノを作ったので感想などを述べてみたいと思います。
Python版ShinyとR版との大きな違いはWebAssembly(WASM)へコンパイルされている点でしょう[ref]Shinyのプレゼンテーション(53:45)でJoe Chengが「コレ言って良いんだっけ?」と確認しつつ明かされています。[/ref]。WASMを使うことでアプリケーションは高速化・軽量化しますので、これはもう大きなメリットです。
Pyscriptが発表されたときも話題になりましたが、WASMを取り巻くウェブアプリ業界は今後要チェックですね🧐
また、言うまでも無いですが、Pythonには膨大な量のライブラリという資産があります。Rには存在しないけどPythonには存在する便利なライブラリを活用できるのは大きな強みとなりましょう。
これは今回やってみた私の個人的な感想ですが、R版でもPython版でも、Shinyの開発はあまり楽しくありませんでした😅
理由は以下です。
最初の二つに関してはバグに近い理由なので改善の余地はあるかと思いますが、五年近く前にR版Shinyを使った時と今とでさほど状況は変わらなかったのでイラつきました💢
ただ、VSCodeの拡張はシンプルながら素晴らしいです。streamlitでもVSCodeのシンプルブラウザで開く拡張機能があればいいのに🤔 [ref]でもそれがあったら本当にShinyを使う理由が無くなる・・・[/ref][ref]と思ったら誰かが昨日作ってました。 https://discuss.streamlit.io/t/release-vscode-extension-python-string-markdown-for-streamlit/3378\[/ref\]
私はウェブ系のスキルがあまりなく、そのスキルを伸ばす予定もないため[ref]typescriptは気になってるので勉強したいのですが、優先度としてはRust,C++>Zig>>tsと思ってます。非エンジニアは学び残しが多くてつらいぜ・・・😉[/ref]、HTML・CSS・JSのスキルスタックが多少要求されるフレームワークは抵抗がありあります。
もし貴方が私のように「WebUIに時間をかけずに、グラフや計算等のアプリ作成に注力したい」派の方なら、圧倒的にstreamlitがオススメです。
HTML等の知識が皆無であっても、おそらくほぼ全てのウィジェット・UIレイアウト機能を使いこなすことが出来るはずです。(もちろんmarkdownのレンダリングや、HTMLタグの挿入も可能です)
また、stremalitはインプット・アウトプットのウィジェットが最初から豊富です。既存のものを組み合わせるだけで、やりたいことのほとんどが実現できると思います。コミュニティメイドのウィジェットが正式に採用されたり、pythonライブラリをフォーラムでシェアされていたりするので、ウィジェットに困ることはまず無いはずです。
公式のドキュメント
充実しているとは言い難いが、一通りShiny for Pythonを学ぶことが出来る。
APIリファレンス
https://shiny.rstudio.com/py/api/
shinyapps.ioへのデプロイ方法
https://docs.rstudio.com/shinyapps.io/getting-started.html#working-with-shiny-for-python
py-shinyレポジトリ
https://github.com/rstudio/py-shiny/
rsconnec-pythonレポジトリ
https://github.com/rstudio/rsconnect-python
py-shinywidgetsレポジトリ