estieでは不動産を扱うため、社内で地図を使った可視化によく取り組みます。estieではSnowflakeを使っており、Streamlit on Snowflakeを活用したいのですが、Streamlitでの地図を使った可視化の定番であるstreamlit_foliumはSnowflakeのセキュリティポリシーで制限されたコンポーネントを使っているため、Streamlit on Snowflakeでは使えません。
そこで、この記事ではpydeckを使ったStreamlit on Snowflakeでも地図にマーカー(点)やパス(線)やポリゴン(面)を描画する方法を説明します。
準備
SnowflakeでStreamlitが使える環境を整えてください。今回の記事ではスコープ外とします。
本記事ではpydeckを使います。エディタ左上のPackagesからpydeckを探し、クリックしてください。
マーカー(点)
ScatterplotLayerやIconLayerを使う例を紹介します。まだ下のプログラムではSnowflakeを使っていませんがこのように描画できます。
import pandas as pd import pydeck as pdk import streamlit as st plot_data = pd.DataFrame({ 'lat': [0, 10, 20], 'lng': [0, 10, 30], }) layer = pdk.Layer( 'ScatterplotLayer', plot_data, get_position=['lng', 'lat'], get_radius=100000, ) st.pydeck_chart(pdk.Deck(map_style=None, layers=[layer]))
せっかくSnowflakeで動かすので、データベースから取得したデータを使う方法も紹介します。下のプログラムはestieが所在していた建物を描画するプログラムです。便利な可視化にするために、開いた時点で世界地図ではなく東京付近の表示とし、マーカーをestieの色にし、大きさをビルの基準階面積に比例させてみました。33,34行目のカラム名を大文字にする必要があります(私は少しこれにハマりました…)。
import pandas as pd import pydeck as pdk import streamlit as st from snowflake.snowpark.context import get_active_session session = get_active_session() # Snowflakeからデータの取得 estie_office_histories = pd.DataFrame(session.sql(f""" select name, address, latitude, longitude, sqrt(standard_floor_area)*10 as radius from {{データベース}} where {{estieが所在していたビル}}; """).collect()) # 地図の初期表示位置・拡大率の設定 initial_view_state=pdk.ViewState( latitude=35.66, longitude=139.73, zoom=12, ) # 描画する点 layer = pdk.Layer( 'ScatterplotLayer', estie_office_histories, get_position=['LONGITUDE', 'LATITUDE'], # 大文字にすること get_radius=['RADIUS'], get_color=[25, 79, 255], # 点の色。estieのロゴの色 ) st.pydeck_chart( pdk.Deck( map_style=None, layers=[layer], initial_view_state=initial_view_state ) )
パス(線)
PathLayerを使う例を紹介します。PathLayerでは、例えば[経度, 緯度]のリストが地図に描画できます。
import pandas as pd import pydeck as pdk import streamlit as st from snowflake.snowpark.context import get_active_session session = get_active_session() # ここからScatterPlot # Snowflakeからデータの取得 estie_office_histories = pd.DataFrame(session.sql(f""" select name, address, latitude, longitude, sqrt(standard_floor_area)*10 as radius from {{データベース}} where {{estieが所在していたビル}}; """).collect()) # 地図の初期表示位置・拡大率の設定 initial_view_state=pdk.ViewState( latitude=35.67, longitude=139.743, zoom=14, ) # 描画する点 point_layer = pdk.Layer( 'ScatterplotLayer', estie_office_histories, get_position=['LONGITUDE', 'LATITUDE'], # 大文字にすること get_radius=['RADIUS'], get_color=[25, 79, 255], # 点の色。estieのロゴの色 ) # ここからPath # カラーコードから10進に変換 def hex_to_rgb(h): h = h.lstrip("#") if len(h) == 6: return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) return tuple(int(h[i], 16)*17 for i in (0, 1, 2)) # オフィスとその近隣駅の座標ペアを取得 building_stations = pd.DataFrame(session.sql(f""" select b.latitude as building_latitude, b.longitude as building_longitude, sl.latitude as station_latitude, sl.longitude as station_longitude, l.colorcode as line_color, l.name from {{ビルと近隣駅や路線のデータベースをjoin}} where {{estieが所在していたビルから徒歩5分以内の駅の情報}}; """).collect()) # Layer用に成形 def make_path(row): return [ [row["BUILDING_LONGITUDE"],row["BUILDING_LATITUDE"]], [row["STATION_LONGITUDE"],row["STATION_LATITUDE"]] ] building_stations["PATH"] = building_stations.apply(make_path, axis=1) building_stations["LINE_COLOR"] = building_stations["LINE_COLOR"].apply(hex_to_rgb) path_layer = pdk.Layer( "PathLayer", data=building_stations, pickable=True, get_color="LINE_COLOR", width_scale=20, width_min_pixels=2, get_path="PATH" ) st.pydeck_chart( pdk.Deck( map_style=None, layers=[point_layer, path_layer], initial_view_state=initial_view_state ) )
今回は駅と建物の座標のみを入れたため直線で結ばれていますが、中間の座標もあれば道に沿った経路も表示できます。SnowflakeはGEOGRAPHYのLineStringなどの型に対応していますが、今回はfloatの数値からStreamlitで描画する例を書きました。GEOGRAPHY型からの変換は、後述するポリゴンで説明します。
ポリゴン(面)
PolygonLayerを使う例を紹介します。pydeckのdataはstrを受け取るので、polygonをjson形式で渡すことで描画できます。
Layer Overview and Examples — pydeck 0.8.0b4 documentation
import json import pandas as pd import pydeck as pdk import streamlit as st from snowflake.snowpark.context import get_active_session session = get_active_session() ward_polygons = pd.DataFrame(session.sql(f""" select ST_ASGEOJSON(polygon):coordinates::varchar as POLYGON from dmg.data_catalog_prd.address_lod where code in (13101) """).collect()) ward_polygons["POLYGON"] = ward_polygons["POLYGON"].apply(json.loads) # 地図の初期表示位置・拡大率の設定 initial_view_state=pdk.ViewState( latitude=35.69, longitude=139.75, zoom=12, ) layer = pdk.Layer( type="PolygonLayer", data=ward_polygons, stroked=True, get_polygon="POLYGON", get_fill_color=[0, 255, 0], filled=True, get_line_color=[0, 0, 0], getLineWidth="3", pickable=True, opacity=0.05, ) st.pydeck_chart(pdk.Deck(layers=[layer], initial_view_state=initial_view_state, tooltip={"text": "{name}"}, map_style=None))
千代田区は飛び地がなく描画も簡単ですが、自治体には様々な形があります。Snowflakeには連結なPolygon以外にも、連結を仮定しないMultiPolygonやGeometryCollectionといった型もあります。説明しませんでしたが、PointにはMultiPointが、LineStringにはMultiLineStringがあります。そしてこれらを要素とするGeometryCollectionもいます。MultiPolygonやGeometryCollectionはPolygonとは形式が違うため、下記のクエリのようにPolygonへと分解してPolygonLayerで描画できます。
SELECT name, f.value::varchar AS POLYGON, FROM {city_polygon_database}, LATERAL FLATTEN(ST_ASGEOJSON(polygon):coordinates) f WHERE {{港区}}
全てを合わせると下のプログラムで点・線・面が描画できます。
import json import pandas as pd import pydeck as pdk import streamlit as st from snowflake.snowpark.context import get_active_session session = get_active_session() # ここからScatterPlot # Snowflakeからデータの取得 estie_office_histories = pd.DataFrame(session.sql(f""" select name, address, latitude, longitude, sqrt(standard_floor_area)*10 as radius from {{データベース}} where {{estieが所在していたビル}}; """).collect()) # 地図の初期表示位置・拡大率の設定 initial_view_state=pdk.ViewState( latitude=35.67, longitude=139.743, zoom=14, ) # 描画する点 point_layer = pdk.Layer( 'ScatterplotLayer', estie_office_histories, get_position=['LONGITUDE', 'LATITUDE'], # 大文字にすること get_radius=['RADIUS'], get_color=[25, 79, 255], # 点の色。estieのロゴの色 ) # ここからPath # カラーコードから10進に変換 def hex_to_rgb(h): h = h.lstrip("#") if len(h) == 6: return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) return tuple(int(h[i], 16)*17 for i in (0, 1, 2)) # オフィスとその近隣駅の座標ペアを取得 building_stations = pd.DataFrame(session.sql(f""" select b.latitude as building_latitude, b.longitude as building_longitude, sl.latitude as station_latitude, sl.longitude as station_longitude, l.colorcode as line_color, l.name from {{ビルと近隣駅や路線のデータベースをjoin}} where {{estieが所在していたビルから徒歩5分以内の駅の情報}}; """).collect()) # Layer用に成形 def make_path(row): return [ [row["BUILDING_LONGITUDE"],row["BUILDING_LATITUDE"]], [row["STATION_LONGITUDE"],row["STATION_LATITUDE"]] ] building_stations["PATH"] = building_stations.apply(make_path, axis=1) building_stations["LINE_COLOR"] = building_stations["LINE_COLOR"].apply(hex_to_rgb) # ここからPolygon ward_polygons = pd.DataFrame(session.sql(f""" select f.value::varchar as POLYGON, from {{市区町村ポリゴンデータ}}, lateral flatten(st_asgeojson(polygon):coordinates) f where {{西新橋か赤坂}} """).collect()) ward_polygons["POLYGON"] = ward_polygons["POLYGON"].apply(json.loads) polygon_laler = pdk.Layer( type="PolygonLayer", data=ward_polygons, stroked=True, get_polygon="POLYGON", get_fill_color=[0, 255, 0], filled=True, get_line_color=[0, 0, 0], getLineWidth="3", pickable=True, opacity=0.05, # 不透過率 ) st.pydeck_chart( pdk.Deck( map_style=None, layers=[point_layer, path_layer, polygon_laler], initial_view_state=initial_view_state ) )
おわりに
今回はStreamlit on Snowflakeで地図上に可視化する手法を説明しました。今回紹介したプログラムに改善の余地はありますが、データベースに入っていそうな形からの可視化を説明することで、一筋縄ではなく変換処理が必要なパターンも説明できたと思っています。自分の勉強用と見返す用でもありますが、参考になれば嬉しいです。
pydeckにはまだまだたくさんのLayerがあり、目的に応じて使い分けています。estieは不動産を扱っているため緯度経度の座標はもちろん、建物とインターチェンジの経路をLineStringで持ったり自治体や用途地域をMultiPolygonで持ったりと様々な地理系データを扱っていますし、これからもたくさんの地理系データを収集・修正・公開し、プロダクトでも使っていきます。
Snowflakeは地理空間関数を効率的に実行し、建物が含まれるポリゴンの計算などを比較的高速に行ってくれます。"上手に"クエリを書くことで真価が発揮され、事業にも可視化にも効果を発揮します。このあたりはよくわかっていないので詳しい人はぜひ入社して教えてください。お気軽にお話できるようカジュアル面談を設けておりますので、興味のある方ご応募お待ちしております!