2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

大量のデータを画像化して描写するDatashader使ってみる

Last updated at Posted at 2024-08-12

大規模なデータをラスタライズし、可視化・要約を行うHolovizDatashaderで地図描写試してみたという話。

この記事の要約

  • datashaderでは、ver0.16からgeopandasを読みこむことが出来る
  • 画面のピクセルサイズを先に決めてしまい、データをピクセルに対応付け・集計する
  • plotlyで利用する時は、画像が対応する緯度経度を取り出してmapboxレイヤー作る

これは何するの?

下のようなことができます。

  • 公式トップページ

With code just like the above, you can plot 300 million points of data (one per person in the USA) from the 2010 census without any parameter tuning:
(訳)上記のようなコードを使用するとパラメーターを調整せずに、2010年の国勢調査から3億ポイントのデータ(米国の一人ひとり) をプロットできます。

https://datashader.org/#

This dataset contains over 40 million rows of data, each row including several cell tower features in addition to the latitude/longitude location of the tower. To handle this fairly large dataset, this app makes use of Dask for parallel processing, and Datashader for server side rendering.
(訳)このデータセットには4000万行を超えるデータが含まれており、各行には基地局の緯度/経度の位置に加えて、いくつかの付随する情報が含まれています。このかなり大きなデータセットを処理するために、このアプリは並列処理にDaskを使用し、サーバー側のレンダリングにDatashaderを使用します。

https://github.com/plotly/dash-world-cell-towers

やっていることとしては、画面のピクセルサイズを先に決めてしまい、データをピクセルに対応付け・集計を行っているみたいです。

集約関数は複数用意されており、目的に応じて利用できます。

↓この辺に書いてあります。

Plotlyのmapbox系プロットでとりあえず使ってみる

公式の説明の通り。

Plotly and Datashader in PythonMapbox Map Layers in Python
How to use datashader to rasterize large datasets, and visualize the generated raster data with plotly.
https://plotly.com/python/datashader/

datashaderでは、ver0.16からgeopandasを読みこむことが出来る。

そのver0.16がpython 3.8などまだ割と使われているバージョンに対応していないことに留意。

kaggle Taxi Trajectory Dataを可視化してみる

このデータは、ポルトガルを走るタクシーの軌跡データで、約170万!!の移動軌跡が収録されています。

描写対象として、軌跡の長さが、2より大きい166万行、点の数に直すと、8341万点に対して描写しています。

前処理で死にそうなほどメモリ食ってますが、datashaderの処理自体は割とサクサク動きます。

描写してしまえば、画像なので言わずもがなです。

前処理


import polars as pl
import geopandas as gpd
import shapely

import datashader as ds
from colorcet import fire
import plotly.express as px

from IPython.display import Image
df = pl.read_csv(
    'TaxiTrajectoryData.csv',
    schema=dict(
        TRIP_ID=pl.Int64,
        CALL_TYPE=pl.Utf8,
        ORIGIN_CALL=pl.Utf8,
        ORIGIN_STAND=pl.Int16,
        TAXI_ID=pl.Int32,
        TIMESTAMP=pl.Int64,
        DAY_TYPE=pl.Utf8,
        MISSING_DATA=pl.Boolean,
        POLYLINE=pl.Utf8
    )
)
df
TRIP_ID CALL_TYPE ORIGIN_CALL ORIGIN_STAND TAXI_ID TIMESTAMP DAY_TYPE MISSING_DATA POLYLINE
i64 str str i16 i32 i64 str bool str
1372636858620000589 "C" "" null 20000589 1372636858 "A" false "[[-8.618643,41.141412],[-8.618…
1372637303620000596 "B" "" 7 20000596 1372637303 "A" false "[[-8.639847,41.159826],[-8.640…
1372636951620000320 "C" "" null 20000320 1372636951 "A" false "[[-8.612964,41.140359],[-8.613…
1372636854620000520 "C" "" null 20000520 1372636854 "A" false "[[-8.574678,41.151951],[-8.574…
1372637091620000337 "C" "" null 20000337 1372637091 "A" false "[[-8.645994,41.18049],[-8.6459…
1404171463620000698 "C" "" null 20000698 1404171463 "A" false "[[-8.612469,41.14602],[-8.6124…
1404171367620000670 "C" "" null 20000670 1404171367 "A" false "[[-8.610138,41.140845],[-8.610…
1388745716620000264 "C" "" null 20000264 1388745716 "A" false "[]"
1404141826620000248 "B" "" 12 20000248 1404141826 "A" false "[[-8.630712,41.154885],[-8.630…
1404157147620000079 "B" "" 34 20000079 1404157147 "A" false "[[-8.615538,41.140629],[-8.615…
# 死ぬほどメモリ喰うので注意

# ポイント=行として変換する例
#ddf = df\
#     .drop(['ORIGIN_STAND','DAY_TYPE'])\
#     .with_columns(
#         (pl.col('TIMESTAMP') * 10 ** 6).cast(pl.Datetime),
#         pl.col('POLYLINE').str.json_decode()
#     )\
#     .with_columns(
#         pl.int_ranges(0,pl.col('POLYLINE').list.len()).alias('SEC')
#     )\
#     .explode(['POLYLINE','SEC'])\
#     .with_columns(
#         pl.col('TIMESTAMP')+pl.duration(seconds=(pl.col('SEC') * 15))
#     )

# ラインストリングにとして扱うための下準備
ddf = df\
    .drop(['ORIGIN_STAND','DAY_TYPE','MISSING_DATA'])\
    .with_columns(
        (pl.col('TIMESTAMP') * 10 ** 6).cast(pl.Datetime),
        pl.col('POLYLINE').str.json_decode()
    )\
    .filter(pl.col('POLYLINE').list.len() > 2)
%%time
# 死ぬほどメモリ喰うので注意

t=ddf.to_pandas()
t['POLYLINE'] = t['POLYLINE'].apply(lambda x: shapely.LineString(x))

gdf = gpd.GeoDataFrame(t,geometry='POLYLINE')

# ポイントとして整形する例
# a=ddf.to_pandas()
# gdf = gpd.GeoDataFrame(
#     a,
#     geometry=gpd.points_from_xy(
#             a['POLYLINE'].apply(lambda x: x[0]),
#             a['POLYLINE'].apply(lambda x: x[1])
#     )
# )

CPU times: user 2min 42s, sys: 8.94 s, total: 2min 51s
Wall time: 2min 51s

gdf
TRIP_ID CALL_TYPE ORIGIN_CALL TAXI_ID TIMESTAMP POLYLINE
0 1372636858620000589 C 20000589 2013-07-01 00:00:58 LINESTRING (-8.61864 41.14141, -8.6185 41.1413...
1 1372637303620000596 B 20000596 2013-07-01 00:08:23 LINESTRING (-8.63985 41.15983, -8.64035 41.159...
2 1372636951620000320 C 20000320 2013-07-01 00:02:31 LINESTRING (-8.61296 41.14036, -8.61338 41.140...
3 1372636854620000520 C 20000520 2013-07-01 00:00:54 LINESTRING (-8.57468 41.15195, -8.5747 41.1519...
4 1372637091620000337 C 20000337 2013-07-01 00:04:51 LINESTRING (-8.64599 41.18049, -8.64595 41.180...
... ... ... ... ... ... ...
1666761 1388660427620000585 C 20000585 2014-01-02 11:00:27 LINESTRING (-8.60697 41.16228, -8.60697 41.162...
1666762 1404171463620000698 C 20000698 2014-06-30 23:37:43 LINESTRING (-8.61247 41.14602, -8.61249 41.145...
1666763 1404171367620000670 C 20000670 2014-06-30 23:36:07 LINESTRING (-8.61014 41.14084, -8.61017 41.140...
1666764 1404141826620000248 B 20000248 2014-06-30 15:23:46 LINESTRING (-8.63071 41.15488, -8.63073 41.154...
1666765 1404157147620000079 B 20000079 2014-06-30 19:39:07 LINESTRING (-8.61554 41.14063, -8.61542 41.140...

描写

重要なのは、agg = canvas.line(source=gdf, geometry="POLYLINE",agg=ds.count())img = ds.tf.shade(agg, cmap=fire)[::-1].to_pil()の部分。
agg=ds.count()でどうゆう集約をするか?どの列を参照するか?決定する。この例だと、「何回ピクセル上にマッピングされたか?」のカウント。

参考: Datashader - api #reductions

# From https://plotly.com/python/datashader/
canvas = ds.Canvas(plot_width=2000, plot_height=2000)
agg = canvas.line(source=gdf, geometry="POLYLINE",agg=ds.count())

# agg is an xarray object, see http://xarray.pydata.org/en/stable/ for more details
coords_lat, coords_lon = agg.coords['y'].values, agg.coords['x'].values

# Corners of the image, which need to be passed to mapbox
# 要するに上の行と合わせ、作成した画像がどのに位置するのか?エリア(lon,lat)と画像の頂点(角)の対応を取るために必要
coordinates = [[coords_lon[0], coords_lat[0]],
               [coords_lon[-1], coords_lat[0]],
               [coords_lon[-1], coords_lat[-1]],
               [coords_lon[0], coords_lat[-1]]]

# import datashader.transfer_functions as tf
img = ds.tf.shade(agg, cmap=fire)[::-1].to_pil()

# Trick to create rapidly a figure with mapbox axes
# トリックを使用しないとこうなるらしい https://github.com/plotly/dash-world-cell-towers/blob/18fe0d3e2966a3e0b8b8fb03fe40e8daae565687/dash_opencellid/app.py#L720
x,y = gdf.POLYLINE.iloc[0].xy
fig = px.scatter_mapbox(
    gdf.iloc[0],
    lon=x,
    lat=y,
    zoom=7,
    # height=800,
    # width=800
    opacity=0,

    )
# Add the datashader image as a mapbox layer image

fig.update_layout(
                 mapbox_style="carto-darkmatter",
                # mapbox_style="carto-positron",
                 mapbox_layers = [{
                    "sourcetype": "image",
                    "source": img,
                    "coordinates": coordinates
                }]
)
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
# fig.show()
Image(fig.to_image())

image.png

町丁ポリゴンを塗り分けてみる

山形県の全域(!!)の町丁目ポリゴンを適当に塗り分けてプロットします。

4428のポリゴンがあります。数としては少ないですが、複雑な形状も含まれます。普通に描写したらplotlyだとまともに動かんです…

ちなみに、乱数で塗り分けているので、隣り合うポリゴンが違う色になる保証はないです。

(N100・12GBメモリのノートでも難なく動いたので、試すにはこちらのほうが向いているかも…)

データの出典: e-stat - 境界データダウンロード 山形県全域

ほとんど同じなのでGistのみ貼っておきます。(こっちは、agg=ds.by('r')のように集約しています)

↓こんな具合に描写できる

image.png

HoloViewsでの描写

holoviewsで可視化する場合、データの座標系をWEBメルカトル法にする必要があるみたいです。

座標系の変換にはややこい計算が必要ですが、pyprojとか使うと簡単ですgeopandas.to_crsしたほうが簡単だとあとから気づいた人

(この例ではデータ量が多すぎるので1万件に絞っています)

その他はチュートリアル通り。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?