【Streamlit】PydeckのHexagonLayerを使用してもマップ上に3D棒グラフが表示できない理由と解決策

# 前置き
こんにちはデータエンジニアの山口歩夢です！


こちら基本的な内容から実践的な内容、自分が開発する上で躓いたポイントやTipsなどを書かせていただいているStreamlitの入門書です。

電子版が技術書典にて購入可能なため、是非チェックいただけると幸いです。

https://techbookfest.org/product/t5GBwbFm4kUhXrTPkGpAnY?utm_source=twitter&utm_medium=social&utm_campaign=share



# 本題
Streamlitには、`st.pydeck_chart`と言うPydeckライブラリで作成した図を、地図上に表示させることができる関数が用意されています。

公式ドキュメントに紹介されているような、地図上に3Dグラフが表示されるアプリケーションを作成したいと思い、ドキュメントを真似しながらアプリケーションを構築してみたのですが、なかなかうまくいきませんでした。

その時の解決策を共有させていただきます！
以下が公式ドキュメントで紹介されている図で、こういった3D棒グラフを日本地図にプロットしてみようと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/883573/3e5427cc-f682-9ee5-5be3-f409c6147754.png)


https://docs.streamlit.io/develop/api-reference/charts/st.pydeck_chart

# 具体的にやりたいこと
以下の`latitude`、`longitude`、`sales`と言う3つのカラムがあるデータフレームの`sales`カラムの値を基準に3D棒グラフを地図上にプロットします。

`latitude`と`longitude`の値で3D棒グラフがプロットされる緯度と経度が決まり、`sales`の値が大きければ3D棒グラフの高さが高くなり、低ければ低くなるといったイメージです。
```py
import pandas as pd

# 元のデータフレームに追加します
data = pd.DataFrame({
    'latitude': [35.6895, 34.6937, 35.0116, 35.1802, 43.2203,
                 33.5904, 35.4478, 35.6051, 35.8617, 34.9769,
                 34.3853, 34.6617, 38.2688, 36.6513, 35.2153,
                 36.5947, 34.1856, 33.6248, 31.9111, 26.2124],
    'longitude': [139.6917, 135.5023, 135.7681, 136.9066, 142.8635,
                  130.4017, 139.6425, 140.1233, 139.6486, 138.3831,
                  132.4553, 133.9350, 140.8710, 138.1812, 136.0722,
                  136.5851, 131.4714, 132.8560, 130.6769, 127.6790],
    'sales': [100000, 75000, 50000, 80000, 60000,
              40000, 60000, 30000, 45000, 25000,
              35000, 20000, 30000, 40000, 50000,
              60000, 70000, 80000, 90000, 100000]
})
```


# 結論
今回やりたかったことを実現するためには、**HexagonLayerではなくColumnLayerを使用しなければならなかった**が結論になります。
以下が最終的に出来上がったスクリプトです。
```py
import streamlit as st
import pandas as pd
import pydeck as pdk
import numpy as np

# 元のデータフレームに追加します
data = pd.DataFrame({
    'latitude': [35.6895, 34.6937, 35.0116, 35.1802, 43.2203,
                 33.5904, 35.4478, 35.6051, 35.8617, 34.9769,
                 34.3853, 34.6617, 38.2688, 36.6513, 35.2153,
                 36.5947, 34.1856, 33.6248, 31.9111, 26.2124],
    'longitude': [139.6917, 135.5023, 135.7681, 136.9066, 142.8635,
                  130.4017, 139.6425, 140.1233, 139.6486, 138.3831,
                  132.4553, 133.9350, 140.8710, 138.1812, 136.0722,
                  136.5851, 131.4714, 132.8560, 130.6769, 127.6790],
    'sales': [100000, 75000, 50000, 80000, 60000,
              40000, 60000, 30000, 45000, 25000,
              35000, 20000, 30000, 40000, 50000,
              60000, 70000, 80000, 90000, 100000]  # 売上データ（例）
})

# PyDeckを使用して地図を描画します
st.pydeck_chart(pdk.Deck(
    map_style=None,
    initial_view_state=pdk.ViewState(
        latitude=38.5,
        longitude=137.0,
        zoom=4,
        pitch=50,
    ),
    layers=[
        pdk.Layer(
            'ColumnLayer',
            data=data,
            get_position=['longitude', 'latitude'],
            get_elevation='sales',
            elevation_scale=5,
            radius=10000,
            get_fill_color=[180, 0, 200, 140],
            pickable=True,
            extruded=True,
        ),
    ],
))
```
そして、以下が出来上がった地図です。
ColumnLayerの`get_position`に経度と緯度を設定して、`get_elevation`に棒グラフで可視化したいカラムを指定してあげることで日本地図上に3D棒グラフを可視化することができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/883573/44243547-8152-09c1-c833-6246fce2c143.png)

以上です！

# HexagonLayerで3D棒グラフを可視化できなかった理由
冒頭で紹介した通り、公式ドキュメントではHexagonLayerを使用して地図上に棒グラフをプロットしています。公式ドキュメントでHexagonLayerを使って地図上に3D棒グラフを可視化しているから、HexagonLayerを使用するに違いないという思い込みがつまづいてしまった原因でした。

そもそも、**HexagonLayerは指定したカラムの値に応じて3D棒グラフをプロットすると言う機能がありません**でした。
どうして公式ドキュメントでは3D棒グラフを地図上にプロットできていたのでしょうか。

公式ドキュメントでは、以下のようにDataFrameを用意しています。`lat`カラムと`lon`カラムの2つのカラムしか用意していません。



```py
import streamlit as st
import pandas as pd
import numpy as np

chart_data = pd.DataFrame(
   np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
   columns=['lat', 'lon'])
```
HexagonLayerは、緯度 (lat) と経度 (lon) のデータポイントを基に地図上に六角形をプロットします。そして、その六角形の密度に応じて3D棒グラフの高さを表現しています(六角形を何個も重ねて高さを出しているイメージ)。
そのため、公式ドキュメントでは`lat`カラムと`lon`カラムだけを使用することで、その位置に六角形をプロットして、六角形の密度を基に3D棒グラフをプロットすることができていたのでした。

都道府県ごとに`sales`カラムの値を元に3D棒グラフを作成するスクリプトに関しても、以下のように改良すれば、HexagonLayerでも3D棒グラフを可視化することは可能です。

以下のスクリプトでは、各都道府県の緯度・経度と売上データを使用して、その都道府県の売上に応じた数の行を持つ新しいデータフレームを作成しています。例えば、東京（`latitude:35.6895`, `longitude:139.6917`, `sales:100000`）は、新しいデータフレームに100,000行として追加されます。
```py
import streamlit as st
import pandas as pd
import pydeck as pdk
import numpy as np

# 元のデータフレームに追加します
data = pd.DataFrame({
    'latitude': [35.6895, 34.6937, 35.0116, 35.1802, 43.2203,
                 33.5904, 35.4478, 35.6051, 35.8617, 34.9769,
                 34.3853, 34.6617, 38.2688, 36.6513, 35.2153,
                 36.5947, 34.1856, 33.6248, 31.9111, 26.2124],
    'longitude': [139.6917, 135.5023, 135.7681, 136.9066, 142.8635,
                  130.4017, 139.6425, 140.1233, 139.6486, 138.3831,
                  132.4553, 133.9350, 140.8710, 138.1812, 136.0722,
                  136.5851, 131.4714, 132.8560, 130.6769, 127.6790],
    'sales': [100000, 75000, 50000, 80000, 60000,
              40000, 60000, 30000, 45000, 25000,
              35000, 20000, 30000, 40000, 50000,
              60000, 70000, 80000, 90000, 100000]
})

# 新しいデータフレームを作成します
new_data = pd.DataFrame(columns=['latitude', 'longitude'])
for idx, row in data.iterrows():
    num_sales = row['sales'] if 'sales' in row else 1  # salesがない場合は1とします
    temp_df = pd.DataFrame({
        'latitude': np.repeat(row['latitude'], num_sales),
        'longitude': np.repeat(row['longitude'], num_sales)
    })
    new_data = pd.concat([new_data, temp_df], ignore_index=True)

# PyDeckを使用して地図を描画します
st.pydeck_chart(pdk.Deck(
    map_style=None,
    initial_view_state=pdk.ViewState(
        latitude=38.5,
        longitude=137.0,
        zoom=4,
        pitch=50,
    ),
    layers=[
        pdk.Layer(
            'HexagonLayer',
            data=new_data,
            get_position=['longitude', 'latitude'],
            radius=20000,
            elevation_scale=50,
            elevation_range=[0, 1000],
            extruded=True,
            coverage=1,
        ),
    ],
))
```
上記のスクリプトからは、以下のようなマップが出来上がりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/883573/fcd8b039-feee-0e45-1891-f0cda3a49101.png)

試してみたところ、HexagonLayerで特定のカラムを基準にレコード数を増やして3D棒グラフを可視化しようとすると、アプリケーションの動きが重くなってしまいました。
やはり今回のように特定のカラムの値を元に3D棒グラフを作りたい場合は、ColumnLayerを使うのが良さそうです。


# まとめ
st.pydeck_chartを使用して、データフレームの特定のカラムを基準にマップ上に3D棒グラフをプロットしたい場合は、HexagonLayerではなくColumnLayerを使いましょう！