Streamlit on Snowflakeで地図を使った可視化

estieでは不動産を扱うため、社内で地図を使った可視化によく取り組みます。estieではSnowflakeを使っており、Streamlit on Snowflakeを活用したいのですが、Streamlitでの地図を使った可視化の定番であるstreamlit_foliumはSnowflakeのセキュリティポリシーで制限されたコンポーネントを使っているため、Streamlit on Snowflakeでは使えません。

Streamlit on Snowflakeでstreamlit_foliumを使おうとすると出るエラー。
Unsupported component error: We removed the component, st.components.v1.html, that appeared here in keeping with our security policy.


そこで、この記事ではpydeckを使ったStreamlit on Snowflakeでも地図にマーカー(点)やパス(線)やポリゴン(面)を描画する方法を説明します。

準備

SnowflakeでStreamlitが使える環境を整えてください。今回の記事ではスコープ外とします。 本記事ではpydeckを使います。エディタ左上のPackagesからpydeckを探し、クリックしてください。

このpydeckをクリックするだけでimportできるようになる


マーカー(点)

ScatterplotLayerIconLayerを使う例を紹介します。まだ下のプログラムでは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は地理空間関数を効率的に実行し、建物が含まれるポリゴンの計算などを比較的高速に行ってくれます。"上手に"クエリを書くことで真価が発揮され、事業にも可視化にも効果を発揮します。このあたりはよくわかっていないので詳しい人はぜひ入社して教えてください。お気軽にお話できるようカジュアル面談を設けておりますので、興味のある方ご応募お待ちしております!

hrmos.co

© 2019- estie, inc.