Lesson 02

県内カメラ351点 — folium 多層マップと点配置の空間統計

導入基礎可視化地理空間統計v2-rewrite
所要 90分 / 想定レベル: 基礎〜応用基礎 / データ: 県内のカメラ情報 (DoBoX #1279)

学習目標

使用データ

データ取得手順

論題データセットDL保存先形式サイズ
県内カメラ情報DoBoX #1279ページから DL ボタンdata/camera_list.csvCSV (UTF-8, ヘッダ1行, 351 行)約 70 KB

一括取得(全レッスン共通, 推奨):

cd "2026 DoBoX 教材"
py -X utf8 data\fetch_all.py

fetch_all.py はカタログ・追加データを data/data/extras/ に再現可能ダウンロード。DoBoX のオープンデータは申請不要、商用・非商用とも利用可。本レッスンの .py スクリプトは、データが無ければ自動取得してから処理を始めるよう実装されていますensure_dataset() ヘルパ)。

スクリプト(全体ソースコード)

⬇ L02_camera_map.py

cd "2026 DoBoX 教材"
python -X utf8 lessons/L02_camera_map.py

方法

  1. 多層 folium マップ: MarkerCluster (区分別色) と HeatMap (密度) を 2 レイヤとして重ね、LayerControl で切替可能にする → L02_map.html
  2. k-NN 距離: cKDTree で k+1 近傍 index を高速取得 → 正確な haversine で距離 (km) を確定。k=1, 3, 5 でヒストグラム化
  3. Voronoi 担当面積: 緯度経度 → 局所平面 (km, 等距離近似) に投影 → scipy.spatial.Voronoi → 有限セルだけ Shoelace で面積を出し対数ヒストグラム
  4. Poisson 比較: 同 bounding box 内に同数 (351点) の一様乱数を 50 回生成 → 実 NN 分布と KS 検定 + Clark-Evans R
  5. 管理者別比較: 所管文字列を 国/県/市/その他 に粗分類 → 同区分内の NN 距離分布を箱ひげで比較。短い ⇒ 集中、長い ⇒ 散在

コード解説

L02_camera_map.py 行 480–656

 1
 2
 3
 4
 5
 6
 7
 8
 9
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
import numpy as np, pandas as pd, folium
from folium.plugins import MarkerCluster, HeatMap
from scipy.spatial import Voronoi, cKDTree
from scipy.stats import ks_2samp
from _common import ensure_dataset

# === (0) データ自動取得 ===
ensure_dataset("data/camera_list.csv", dataset_id=1279, label="県内カメラ #1279")
df = pd.read_csv("data/camera_list.csv").dropna(subset=["緯度", "経度"])
LAT, LON = df["緯度"].to_numpy(), df["経度"].to_numpy()

# === (1) folium 多層マップ: MarkerCluster + HeatMap ===
m = folium.Map(location=[LAT.mean(), LON.mean()], zoom_start=9)
cluster_layer = folium.FeatureGroup(name="MarkerCluster", show=True)
mc = MarkerCluster().add_to(cluster_layer)
COLOR = {"道路":"blue","河川":"green","ため池":"red","海岸":"orange","港湾":"purple","その他":"gray"}
for _, r in df.iterrows():
    folium.CircleMarker([r["緯度"], r["経度"]], radius=4,
        color=COLOR.get(r["管理区分"], "gray"), fill=True).add_to(mc)
cluster_layer.add_to(m)
heat_layer = folium.FeatureGroup(name="HeatMap", show=False)
HeatMap(list(zip(LAT, LON)), radius=18).add_to(heat_layer)
heat_layer.add_to(m)
folium.LayerControl().add_to(m)
m.save("assets/L02_map.html")

# === (2) k-NN 距離 (haversine) ===
def haversine_km(lat1, lon1, lat2, lon2):
    R = 6371.0
    p1, p2 = np.radians(lat1), np.radians(lat2)
    dphi = np.radians(lat2 - lat1); dlam = np.radians(lon2 - lon1)
    a = np.sin(dphi/2)**2 + np.cos(p1)*np.cos(p2)*np.sin(dlam/2)**2
    return 2*R*np.arcsin(np.sqrt(a))

def knn_km(lats, lons, k=1):
    # 緯度経度 → 局所平面で kd-tree を組み、index 取得後に haversine で測る
    lat0 = lats.mean()
    x = (lons - lons.mean()) * np.cos(np.radians(lat0))
    y = (lats - lats.mean())
    tree = cKDTree(np.column_stack([x, y]))
    _, idx = tree.query(np.column_stack([x, y]), k=k+1)
    nb = idx[:, k]   # 自分自身は idx[:, 0]
    return haversine_km(lats, lons, lats[nb], lons[nb])

knn1 = knn_km(LAT, LON, k=1)   # 各カメラから一番近い隣カメラまで (km)

# === (3) Voronoi 担当面積 ===
R = 6371.0
lat0 = LAT.mean()
x = R * np.radians(LON - LON.mean()) * np.cos(np.radians(lat0))
y = R * np.radians(LAT - LAT.mean())
vor = Voronoi(np.column_stack([x, y]))
areas = []
for i, ri in enumerate(vor.point_region):
    region = vor.regions[ri]
    if not region or -1 in region:   # 凸包外は除外
        continue
    poly = vor.vertices[region]
    a = 0.5*abs(np.dot(poly[:,0], np.roll(poly[:,1], -1))
              - np.dot(poly[:,1], np.roll(poly[:,0], -1)))
    if 0 < a < 5000:
        areas.append(a)

# === (4) Poisson 配置との比較 (KS + Clark-Evans R) ===
rng = np.random.default_rng(42)
poisson_nn = np.concatenate([
    knn_km(rng.uniform(LAT.min(), LAT.max(), len(LAT)),
           rng.uniform(LON.min(), LON.max(), len(LON)), k=1)
    for _ in range(50)])
ks_stat, ks_p = ks_2samp(knn1, poisson_nn)
density = len(LAT) / 12000.0   # 矩形領域面積 (km^2)
expected_nn = 1 / (2 * np.sqrt(density))
clark_evans = knn1.mean() / expected_nn
print(f"D={ks_stat:.3f} p={ks_p:.2e}  CE-R={clark_evans:.3f}")
# R<1 ⇒ クラスタ的, R≈1 ⇒ ランダム, R>1 ⇒ 規則的

# === (5) 管理者区分別 NN ===
def admin_cat(s):
    s = str(s)
    if "国土交通省" in s: return "国"
    if s.endswith("県"):  return "県"
    if s.endswith("市"):  return "市"
    return "その他"
df["admin_cat"] = df["所管"].map(admin_cat)
for cat in ["国","県","市","その他"]:
    sub = df[df["admin_cat"]==cat]
    nn = knn_km(sub["緯度"].values, sub["経度"].values, k=1)
    print(cat, "n=", len(sub), "NN平均", nn.mean())

結果

1. 多層インタラクティブ地図 (MarkerCluster + HeatMap)

※ 右上のレイヤ切替で MarkerCluster (区分別色付き、ズーム連動でクラスタ展開) と HeatMap (密度) を切り替え可能。各点クリックでカメラ名・所管・ライブ映像URLを表示。

左: 管理区分別カメラ数 (道路131・河川120・ため池70・海岸18・港湾6・その他6) / 右: 経度×緯度散布。沿岸 (南部) と内陸の県中央部に集中
左: 管理区分別カメラ数 (道路131・河川120・ため池70・海岸18・港湾6・その他6) / 右: 経度×緯度散布。沿岸 (南部) と内陸の県中央部に集中
k-NN 距離分布 (k=1,3,5)。中央値が k に対し概ね 1.5〜2 倍程度に伸びるのは局所的にカメラが密集している証拠 (一様分布なら √k スケール)
k-NN 距離分布 (k=1,3,5)。中央値が k に対し概ね 1.5〜2 倍程度に伸びるのは局所的にカメラが密集している証拠 (一様分布なら √k スケール)
左: Voronoi 図 (色 = log10 担当面積)。沿岸の道路・河川カメラは小さなセル、北部山地は大きなセル / 右: 担当面積の対数ヒストグラム。3 桁にまたがり強い右裾分布
左: Voronoi 図 (色 = log10 担当面積)。沿岸の道路・河川カメラは小さなセル、北部山地は大きなセル / 右: 担当面積の対数ヒストグラム。3 桁にまたがり強い右裾分布
左: 実データ NN 距離 vs 同領域 Poisson 配置 NN 距離 / 右: CDF 比較と KS 検定。実データが Poisson より小さい距離側に偏る = クラスタ的。Clark-Evans R が判定指標
左: 実データ NN 距離 vs 同領域 Poisson 配置 NN 距離 / 右: CDF 比較と KS 検定。実データが Poisson より小さい距離側に偏る = クラスタ的。Clark-Evans R が判定指標
管理者区分別 同内 NN 距離分布。国 (国交省) は限られた幹線沿いに集中するため箱が低め、県・市は広域分散
管理者区分別 同内 NN 距離分布。国 (国交省) は限られた幹線沿いに集中するため箱が低め、県・市は広域分散

主要指標サマリ

指標
総カメラ数 (有効緯度経度) 351 台
領域面積 (bounding box) 13454 km²
点密度 0.0261 点/km²
実 NN 平均距離 2.06 km
CSR 期待 NN (1/(2√λ)) 3.10 km
Clark-Evans R 0.665
KS 検定 (実 vs Poisson) D=0.376, p=2.83e-44
Voronoi 担当面積 中央値 17.4 km²
Voronoi 担当面積 平均 63.5 km²

管理者区分別 同内 NN 距離

管理者区分 n NN 平均 (km) NN 中央値 (km)
73 1.514 1.014
171 3.480 2.512
92 2.097 1.384
その他 15 3.750 4.897

考察

発展課題

  1. Ripley K 関数を実装して、距離 r を変えながら「どのスケールでクラスタ的か」を出す。今回の Clark-Evans R は r=NN距離スケールでの 1 点指標にすぎない。
  2. 道路ネットワーク距離での NN 計算: ユークリッド距離ではなく OSMnx で取った道路グラフ上の距離で再計算すると、海越し・山越しの不自然な近接が消える。
  3. 属性条件付き: 「ため池カメラから一番近い河川カメラまで」のような 異種 NN。L09 (避難所×カメラ) と同じ枠組みで多変量化。
  4. 動的 Poisson: 矩形ではなく広島県境界 GeoJSON で領域を取り直し、より厳密な CSR を定義。
  5. 稼働率と組合せ: requests.head で各 公開URL を叩き、応答 200 のカメラだけで NN を再計算。「公開されているカメラのカバレッジ」が見える。
  6. 市区町村別 1人あたりカメラ数: 住所列から市町村抽出 → 国勢調査人口で割って カメラ密度の格差を地図化 (L09 接続)。