0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

交通事故データを3Dで可視化する — MapLibre × deck.gl による立体分布表現

Posted at

はじめに

MapLibre と deck.gl を使って、
警察庁の交通事故統計オープンデータをもとに事故発生地点を可視化します。

今回は、deck.gl の HexagonLayer を使用し、事故の発生密度を3次元で表現します。

データの準備

交通事故統計オープンデータ(本票CSV)には、
緯度・経度が度分秒(DMS)形式の右詰め数値で記録されています。
以下のスクリプトで 10 進度に変換し、東京都のみを抽出します。

import pandas as pd

# === 設定 ===
CSV_FILE = "honhyo_2024.csv"   # 警察庁オープンデータ
ENCODING = "shift_jis"
PREF_CODE = 30                 # 東京都
OUTPUT_JSON = "accidents_tokyo.json"

# === DMS → 10進度変換関数 ===
def dms_to_deg(value: int | str) -> float:
    s = str(int(value)).rjust(10, "0")  # 緯度9桁→10桁右詰め補完
    sec = int(s[-5:]) / 1000
    minute = int(s[-7:-5])
    deg = int(s[:-7])
    return deg + minute / 60 + sec / 3600

# === CSV 読み込みと変換 ===
df = pd.read_csv(CSV_FILE, encoding=ENCODING)
df = df[df["都道府県コード"] == PREF_CODE]

# 緯度経度カラムの変換
df["lat"] = df["地点 緯度(北緯)"].apply(dms_to_deg)
df["lon"] = df["地点 経度(東経)"].apply(dms_to_deg)

# === 出力 ===
records = df[["lon", "lat"]].to_dict(orient="records")
pd.DataFrame(records).to_json(OUTPUT_JSON, orient="records", force_ascii=False, indent=2)
print(f"{len(records)} 件を {OUTPUT_JSON} に出力しました。")

これで、accidents_tokyo.json が作成されます。

サンプルコード

MapLibre を背景に、deck.gl の HexagonLayer で事故発生地点を 3D 表現します。
最小構成では、背景スタイルを指定し、accidents_tokyo.json を読み込むだけで動作します。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>交通事故データ 3D分布</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- MapLibre -->
    <script src="https://unpkg.com/maplibre-gl@3.6.0/dist/maplibre-gl.js"></script>
    <link href="https://unpkg.com/maplibre-gl@3.6.0/dist/maplibre-gl.css" rel="stylesheet" />
    <!-- deck.gl -->
    <script src="https://unpkg.com/deck.gl@8.9.36/dist.min.js"></script>

    <style>
      html,
      body {
        margin: 0;
        height: 100%;
        overflow: hidden;
      }
      #map {
        position: absolute;
        top: 0;
        bottom: 0;
        width: 100%;
      }
      .maplibregl-canvas {
        filter: brightness(80%) contrast(110%);
      }
    </style>
  </head>
  <body>
    <div id="map"></div>

    <script>
      const map = new maplibregl.Map({
        container: "map",
        style: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
        center: [139.76, 35.68],
        zoom: 10,
        pitch: 45,
        bearing: -15,
      });

      let deckOverlay;

      fetch("accidents_tokyo.json")
        .then((res) => res.json())
        .then((data) => {
          const createLayer = (radius, elevationScale) =>
            new deck.HexagonLayer({
              id: "accidents-hex",
              data,
              getPosition: (d) => [d.lon, d.lat],
              radius, // 六角形の平面サイズ
              elevationScale, // ズームに応じて高さも調整
              elevationRange: [0, 4000],
              extruded: true,
              opacity: 0.75,
              pickable: true,
              parameters: {
                depthTest: true, // 深度テストを有効化
                blend: true,
                blendFunc: [770, 771], // SRC_ALPHA, ONE_MINUS_SRC_ALPHA
                blendEquation: 32774,
              },
              colorRange: [
                [0, 0, 255],
                [0, 128, 255],
                [0, 255, 128],
                [255, 255, 0],
                [255, 128, 0],
                [255, 0, 0],
              ],
              onHover: (info) => {
                map.getCanvas().style.cursor = info.object ? "pointer" : "";
              },
            });

          // 初期設定
          let radius = 400;
          let elevationScale = 5; // 実際に採用される値に合わせる
          let hexLayer = createLayer(radius, elevationScale);

          deckOverlay = new deck.MapboxOverlay({
            interleaved: true,
            layers: [hexLayer],
          });
          map.addControl(deckOverlay);

          // === ズームレベルに応じて radius と elevationScale を連動調整 ===
          map.on("zoom", () => {
            const z = map.getZoom();
            let radius, elevationScale;

            if (z >= 14) {
              radius = 20; // 交差点レベル
              elevationScale = 0.25; // 高さを低く(詳細表示)
            } else if (z >= 12) {
              radius = 100; // 市街レベル
              elevationScale = 1.0; // 中程度の高さ
            } else if (z >= 10) {
              radius = 200; // 区レベル
              elevationScale = 2; // 標準の高さ
            } else {
              radius = 400; // 広域傾向
              elevationScale = 5; // 高く(概要表示)
            }

            const newLayer = createLayer(radius, elevationScale);

            // レイヤーを完全に更新
            deckOverlay.setProps({
              layers: [newLayer],
              // 強制的にレンダリングをリセット
              _getViewState: () => map.transform,
            });

            // 追加:少し遅延させて再描画を確実にする
            setTimeout(() => {
              map.triggerRepaint();
            }, 50);
          });
        });
    </script>
  </body>
</html>

地図を開くと、東京都内の事故発生地点が六角形の立体として描画されます。
radius を調整することで、平面上の密度感を変えることができます。

f06397ea65a20ba8c669-1.png

f06397ea65a20ba8c669-2.png

このコードでは、地図のズーム操作に応じて
六角形のサイズ(radius)と高さ倍率(elevationScale)を自動的に調整しています。
広域では密度の傾向を俯瞰でき、ズームインすると詳細な分布が確認できます。

背景地図には Carto 提供のベクトルスタイル
https://basemaps.cartocdn.com/gl/positron-gl-style/style.json を使用しています。

パラメータ例

HexagonLayer の主なパラメータと、その役割をまとめます。
視認性はブラウザの表示倍率や地図範囲にも左右されるため、
以下の設定値はあくまで一例です。

パラメータ 役割 設定例
radius 六角形の平面サイズ(m) 300
elevationScale 柱の高さ倍率 3
elevationRange 高さの最小・最大範囲 [0, 4000]
opacity 不透明度(背景地図とのバランス調整) 0.75
colorRange 密度に応じた配色。以下のようなRGB指定が可能 6段階グラデーション(青→赤)
[[0,0,255], [0,128,255], [0,255,128], [255,255,0], [255,128,0], [255,0,0]]
depthTest 柱の前後関係を正しく描画する設定 true
blendFunc 透過合成の方式を指定 [SRC_ALPHA, ONE_MINUS_SRC_ALPHA]

配色は [R,G,B] の数値配列で指定します。
低密度域を寒色、高密度域を暖色で表現すると直感的に理解しやすくなります。

まとめ

本記事では、MapLibre と deck.gl を組み合わせて、
警察庁の交通事故統計オープンデータの事故発生場所を 3D 表現しました。

HexagonLayer を用いて事故発生地点の分布を高さで表し、
密度の差を視覚的に把握できるようになります。

表示の見やすさは radiusopacity の調整によって大きく変化します。
まずは最小構成で試し、目的に応じて設定を調整するとよいでしょう。

実際に可視化してみると、
特定の地域で事故が集中する様子を空間的に捉えられ、
データを「数値」ではなく「地理的パターン」として直感的に理解できるようになりました。

また、同じデータでも表示スケールを変えることで
”局所的な多発地点” と ”広域的な傾向” の両方を確認でき、
単一視点では得られない気づきが得られます。

可視化の目的が明確であれば、
3D表現はデータの特徴をより多面的に把握する手段となります。

交通事故統計オープンデータ(警察庁):
https://www.npa.go.jp/publications/statistics/koutsuu/opendata/index_opendata.html

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?