FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

Snowflake x Streamlit でデータを可視化する

Snowflake x Streamlit でデータを可視化しよう

こんにちは、データチームにてデータ基盤開発を行なっています、井山です。

データチームでは、データ基盤に関係する各種運用メタデータ(wfの処理時間やデータ格納時間等々)をSnowflake上に格納、Tableau上で可視化しているのですが、半年程前にStreamlitがSnowflakeによって買収され、今後機能として追加されていきそうなので、今のうちに試しておこうと思います。

www.snowflake.com

Streamlit とは

streamlit.io

データアプリケーションを手軽に作成することができるオープンソースのWebアプリケーションフレームワークです。 フロントエンドの経験がなくともPythonのみで記述ができ、手軽に試すことができます。

前準備

今回は下記のdevcontainerを利用します。

https://github.com/jroes/streamlit-getting-started-m1

基本Readmeに従って、こちらのリポジトリをCloneして立ち上げます。 VsCodeのターミナルに以下を入力すると

のような表示がされると思うので、ブラウザで開くを押下し、以下のような表示がされればOKです。

Snowflakeへの接続

ここからはSnowflakeに実際に接続して、Snowflake側のデータを参照して可視化していきたいと思います。 今回利用している環境ではSnowflake接続のためのパッケージが足りないのでインストールしておきます。

conda install snowflake-connector-python

前もってDockerfileを編集しておくでもOK

RUN conda install snowflake-connector-python

上記ができたら以下のドキュメントを参考にSnowflakeへの接続を確認します。

※Snowflake環境はトライアル環境を利用します。

ドキュメントに従って、データベースとテーブルを作成します。

CREATE DATABASE PETS;

CREATE TABLE MYTABLE (
    NAME            varchar(80),
    PET             varchar(80)
);

INSERT INTO MYTABLE VALUES ('Mary', 'dog'), ('John', 'cat'), ('Robert', 'bird');

SELECT * FROM MYTABLE;

今回はCloudの方のStreamlitではなくローカルで試す形なので、 .streamlit 配下に secret.toml を作成し、そこに認証情報を記載します。

# .streamlit/secrets.toml

[snowflake]
user = "xxx"
password = "xxx"
account = "xxx"
warehouse = "xxx"
database = "xxx"
schema = "xxx"

ここまでできたら下記のコマンドを実行し、実際に接続できているかを確認します。

streamlit run streamlit_app.py 

テスト用データの準備

せっかくなので何かしら可視化用のデータが欲しいので、Snowflakeのハンズオンでも利用されているCITIBIKEのデータをImportします。

※CITIBIKEについて.

ニューヨークにあるレンタル自転車バイクシェアプログラム S3に関連するデータを公開してくれています。 https://ride.citibikenyc.com/system-data

データのインポートについては特に変わったことはしないので省略します。

今回は下記のS3に入っているデータをインポートしました。(一部CSVファイルでエラーが出るので ON_ERROR = SKIP_FILE でエラーが出たファイルは飛ばしてインポート)

s3://snowflake-workshop-lab/citibike-trips

SCHEMA 情報

データプレビュー

実際に可視化してみる

いったん簡単な可視化から試してみます。 可視化の際に選べるチャートの種類は以下にある通りです、幾つか試してみたいと思います。

docs.streamlit.io

Table

import streamlit as st
import pandas as pd
import snowflake.connector

@st.experimental_singleton
def init_connection():
    return snowflake.connector.connect(
        **st.secrets["snowflake"], client_session_keep_alive=True
    )

conn = init_connection()

# Table
used_count = pd.read_sql("SELECT START_STATION_NAME, COUNT(*) AS USE_COUNT FROM TRIPS WHERE STARTTIME BETWEEN '2017-01-01' AND '2018-01-01' GROUP BY START_STATION_NAME ORDER BY COUNT(*) DESC LIMIT 10;", conn)

st.header('利用回数が多い駅TOP10')
st.table(used_count)

line_chart

import streamlit as st
import pandas as pd
import snowflake.connector

@st.experimental_singleton
def init_connection():
    return snowflake.connector.connect(
        **st.secrets["snowflake"], client_session_keep_alive=True
    )

conn = init_connection()

# LineChart
duration_per_days = pd.read_sql("SELECT to_date(STARTTIME) AS USE_DATE, SUM(TRIPDURATION) AS SUM_TRIPDURATION FROM TRIPS WHERE STARTTIME BETWEEN '2017-01-01' AND '2018-01-01' GROUP BY to_date(STARTTIME) ORDER BY USE_DATE;", conn)
duration_per_days = duration_per_days.set_index(['USE_DATE'])

st.header('日別 TRIP DURATION')
st.line_chart(duration_per_days)

bar_chart

import streamlit as st
import pandas as pd
import snowflake.connector

@st.experimental_singleton
def init_connection():
    return snowflake.connector.connect(
        **st.secrets["snowflake"], client_session_keep_alive=True
    )

conn = init_connection()

# bar_chart
count_per_start_station = pd.read_sql("SELECT to_date(STARTTIME) AS USE_DATE, START_STATION_NAME, COUNT(*) AS USE_COUNT FROM TRIPS WHERE STARTTIME BETWEEN '2017-01-01' AND '2017-02-01' AND START_STATION_NAME IN (SELECT START_STATION_NAME FROM TRIPS WHERE STARTTIME BETWEEN '2017-01-01' AND '2018-01-01' GROUP BY START_STATION_NAME ORDER BY COUNT(*) DESC LIMIT 3) GROUP BY to_date(STARTTIME), START_STATION_NAME;", conn)
count_per_start_station = count_per_start_station.set_index(['START_STATION_NAME'])

st.header("日別・スタート駅別 利用回数")
st.bar_chart(count_per_start_station, x="USE_DATE", y="USE_COUNT")

基本的なチャートの描画はすぐ出来る感じですが、もう少しリッチな感じにしたい場合は altair_chart等 を利用するとよさそうです。

docs.streamlit.io

altair-viz.github.io

altair_chart

import streamlit as st
import pandas as pd
import altair as alt
import snowflake.connector

@st.experimental_singleton
def init_connection():
    return snowflake.connector.connect(
        **st.secrets["snowflake"], client_session_keep_alive=True
    )

conn = init_connection()

count_per_start_station = pd.read_sql("SELECT to_date(STARTTIME) AS USE_DATE, START_STATION_NAME, COUNT(*) AS USE_COUNT FROM TRIPS WHERE STARTTIME BETWEEN '2017-01-01' AND '2017-02-01' AND START_STATION_NAME IN (SELECT START_STATION_NAME FROM TRIPS WHERE STARTTIME BETWEEN '2017-01-01' AND '2018-01-01' GROUP BY START_STATION_NAME ORDER BY COUNT(*) DESC LIMIT 3) GROUP BY to_date(STARTTIME), START_STATION_NAME;", conn)

c = alt.Chart(count_per_start_station).mark_bar().encode(
    x='USE_DATE', y='USE_COUNT', color='START_STATION_NAME', tooltip=['USE_DATE', 'USE_COUNT', 'START_STATION_NAME'])

st.header("日別・スタート駅別 利用回数")
st.altair_chart(c, use_container_width=True)

他のグラフライブラリにも対応しているので自身が行いたい可視化に合わせて使えば良い感じかなーと思います。

  • Vega-Lite
  • Plotly
  • Bokeh
  • PyDeck
  • dagre-d3

snowparkでやってみる

Snowflakeには snowpark というものがあります。

docs.snowflake.com

Snowparkライブラリは、データパイプライン内のデータをクエリおよび処理するための直感的な API を提供します。このライブラリを使用すると、アプリケーションコードが実行されるシステムにデータを移動することなく、Snowflakeでデータを処理するアプリケーションを構築できます。Snowparkには、他のクライアントライブラリとの差別化をもたらすいくつかの機能があります。

Snowflake上で動作するアプリケーションを開発できる機能で現在は Java、Scala、 Python に対応しています。 せっかくなので Snowpark for Python を利用して試してみたいと思います。

docs.snowflake.com

devcontainer を少しいじる

デフォルトの devcontainer の環境だと snowflake-snowpark-python が入っていないので少し弄ります。 devconatiner の conda の python バージョンが 3.10 だったので、snowflake-snowpark-python の対応バージョンに合わせるため 3.8.13 に変更し、その後 snowflake-snowpark-python をインストールします。

conda install python=3.8.13
conda install -c main snowflake-snowpark-python

Snowpark での Snowflake への接続確認

from snowflake.snowpark import Session

connection_parameters = {
  "account": "XXXX",
  "user": "XXXX",
  "password": "XXXX",
  "role": "XXXX",
  "warehouse": "XXX",
  "database": "XXXX",
  "schema": "XXX"
}

session = Session.builder.configs(connection_parameters).create()
df_table = session.table("TRIPS")
df_table.show()

session.close()

実行結果

2022-10-21 03:07:29.329 query: [SELECT  *  FROM ( SELECT  *  FROM (TRIPS)) LIMIT 10 OFFSET 0]
2022-10-21 03:07:29.686 query execution done
2022-10-21 03:07:29.686 Number of results in first chunk: 10
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|"TRIPDURATION"  |"STARTTIME"          |"STOPTIME"           |"START_STATION_ID"  |"START_STATION_NAME"           |"START_STATION_LATITUDE"  |"START_STATION_LONGITUDE"  |"END_STATION_ID"  |"END_STATION_NAME"        |"END_STATION_LATITUDE"  |"END_STATION_LONGITUDE"  |"BIKEID"  |"MEMBERSHIP_TYPE"             |"USERTYPE"  |"BIRTH_YEAR"  |"GENDER"  |
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|832             |2018-02-11 21:11:17  |2018-02-11 21:25:09  |435                 |W 21 St & 6 Ave                |40.74173969               |-73.99415556               |448               |W 37 St & 10 Ave          |40.75660359             |-73.9979009              |32963     |Annual Membership             |Subscriber  |1976          |1         |
|395             |2018-02-11 21:11:18  |2018-02-11 21:17:53  |433                 |E 13 St & Avenue A             |40.72955361               |-73.98057249               |439               |E 4 St & 2 Ave            |40.7262807              |-73.98978041             |15472     |Annual Membership - Save 15%  |Subscriber  |1993          |2         |
|354             |2018-02-11 21:11:20  |2018-02-11 21:17:14  |3140                |1 Ave & E 78 St                |40.77140426               |-73.9535166                |3288              |E 88 St & 1 Ave           |40.778301               |-73.9488134              |18199     |Annual Membership             |Subscriber  |1967          |2         |
|193             |2018-02-11 21:11:30  |2018-02-11 21:14:44  |3632                |E 12 St & Avenue B             |40.728048571              |-73.978811502              |266               |Avenue D & E 8 St         |40.72368361             |-73.97574813             |32922     |Annual Membership             |Subscriber  |1991          |1         |
|1389            |2018-02-11 21:11:37  |2018-02-11 21:34:47  |447                 |8 Ave & W 52 St                |40.76370739               |-73.9851615                |2021              |W 45 St & 8 Ave           |40.75929124             |-73.98859651             |32298     |Annual Membership             |Subscriber  |1976          |1         |
|223             |2018-02-11 21:11:44  |2018-02-11 21:15:27  |3304                |6 Ave & 9 St                   |40.668127                 |-73.98377641               |3365              |3 St & 7 Ave              |40.6703837              |-73.97839676             |28643     |Annual Membership             |Subscriber  |NULL          |0         |
|965             |2018-02-11 21:11:46  |2018-02-11 21:27:52  |405                 |Washington St & Gansevoort St  |40.739323                 |-74.008119                 |325               |E 19 St & 3 Ave           |40.73624527             |-73.98473765             |31174     |Annual Membership             |Subscriber  |1986          |2         |
|968             |2018-02-11 21:11:52  |2018-02-11 21:28:01  |405                 |Washington St & Gansevoort St  |40.739323                 |-74.008119                 |325               |E 19 St & 3 Ave           |40.73624527             |-73.98473765             |32687     |Annual Membership             |Subscriber  |1986          |1         |
|1141            |2018-02-11 21:11:52  |2018-02-11 21:30:54  |3518                |Lenox Ave & W 126 St           |40.808442                 |-73.9452087                |2006              |Central Park S & 6 Ave    |40.76590936             |-73.97634151             |30204     |Annual Membership             |Subscriber  |1987          |1         |
|463             |2018-02-11 21:11:53  |2018-02-11 21:19:36  |280                 |E 10 St & 5 Ave                |40.73331967               |-73.99510132               |3263              |Cooper Square & Astor Pl  |40.729514962            |-73.990752697            |15975     |Annual Membership             |Subscriber  |1969          |1         |
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

ちゃんとデータが確認できました。

DF の操作については以下のドキュメントにまとまっているので、必要なものを利用します。

docs.snowflake.com

Streamlit に渡して描画してみる

from snowflake.snowpark import Session
from snowflake.snowpark.functions import avg, sum, col,lit
import streamlit as st
import pandas as pd

st.header("snowpark")

connection_parameters = {
  "account": "xxxx",
  "user": "xxxx",
  "password": "xxxx",
  "role": "xxxx",
  "warehouse": "xxxx",
  "database": "xxxx",
  "schema": "xxxx"
}

session = Session.builder.configs(connection_parameters).create()

df = session.table("TRIPS").select(col("TRIPDURATION"), col("START_STATION_ID") ,col("STARTTIME"), col("STOPTIME")).filter(col("START_STATION_ID") == 342)
pd_df = df.to_pandas()
st.table(pd_df)

session.close()

まとめ

Snowflake上にあるデータをサクッと可視化したい時などある場合に使い慣れたpythonで簡単に可視化が可能でとても便利な印象です。 また詰まりやすい環境構築がSnowflake上で解決できる可能性もあるので、今後どのようにSnowflakeの機能として追加されるのかが楽しみです!(Snowflake上でホスト出来たりしないかなーと妄想しています)

利用したドキュメント等