【GISからPythonへ】到達圏解析に挑戦してみた
はじめに
こんにちは、D2Cデータサイエンティストの久保田です。
この記事は、Pythonを使って到達圏解析を行う方法を検討したことについて記載します。オープンソースライブラリを活用し、距離に基づいた到達圏を算出して地図上での視覚化を試みました。
今回は、東京都町田市のコンビニエンスストアを対象に400m到達圏を作成しました。到達圏解析に必要なデータはすべてOpenStreetMapから取得しました。
開発環境
- AWS EC2 (インスタンスタイプ: r5.2xlarge)
- Python 3.10.12
- pandas 2.2.0
- geopandas 1.0.1
- requests 2.32.3
- json 2.0.9
- shapely 2.0.6
- osmnx 2.0.0
- networkx 3.2.1
- folium 0.18.0
到達圏解析とは
到達圏解析とは、ある地点から指定した距離や時間内に到達可能な道路をすべて包含する領域(到達圏)を計算し、可視化する手法です。
マーケティング分野では、新規店舗の立地選定や既存店舗の影響範囲、アクセシビリティの評価など、また広告キャンペーンの最適化といった場面でも利用されています。
Pythonで到達圏解析を行う意味
一般的に到達圏解析は、GISソフトを用いて行われることが多いです。その主な理由のひとつはGUIベースで操作できることです。GISソフトはドラッグ&ドロップやマウス操作で簡単に解析を進めることができます。また、リアルタイムで解析結果を地図上で確認することも可能です。
一方でPythonを用いることには、以下のようなメリットが考えられます。
- 自動化
一連の解析プロセスをスクリプト化できます。これにより、同じ処理を何度も手作業で行う必要がなくなり、自動化が可能です。 - プロセスの透明性
GISソフトでは、標準搭載されている解析ツールを使うことで、設定を選ぶだけで解析が完了します。簡単に解析できる一方で、処理内容がブラックボックス化してしまいます。
Pythonを使う場合、コードとして全ての処理内容を明示的に記述する必要があるため、プロセスが透明化されます。 - 大規模データの処理
Pythonでは、DaskやPySparkなどを活用して大規模なデータセットを分散処理できるため、GISソフトが苦手とする大規模データや複雑な解析にも対応可能です。 - 汎用性の向上
PythonはGISソフトと比較して汎用性が高く、幅広い分野で活用されています。豊富なコミュニティとリソースにより、GIS関連の学習や問題解決が容易で、他分野(機械学習、Web開発など)との連携も可能です。
また、多様なライブラリを自由に組み合わせてカスタマイズができ、柔軟なツールチェーンを構築できることに加えて、多くのライブラリがオープンソースであるため、機能の改善が継続的に進みます。
OpenStreetMap(OSM)とは
OpenStreetMap(OSM)とは、誰でも利用できるように作成され、公開されている無料の地図サービスです(OSM財団が提供、品質は無保証)。
世界中の有志作業者が建物データ(構造物の形状、種類、用途等)や道路データ(道路の種類・規模など)を入力・編集することで作成されています。
Overpass APIを実装することでデータの取得を行うことができます。
実装
1.コンビニのポイントデータを取得
まずは、町田市のコンビニのポイントデータをOverpass APIを使ってOSMから取得します。
# Overpass API クエリ(町田市内のコンビニを取得)
query = """
[out:json];
// 町田市のエリアを指定
area["name"="町田市"]["boundary"="administrative"]->.searchArea;
// コンビニのノードを検索
(
node["shop"="convenience"](area.searchArea);
);
// 結果を出力
out body;
>;
out skel qt;
"""
Overpass API用のクエリはこちらです。
areaとして町田市を指定し、shopがconvenienceのnode(ポイント)のみ抽出しています。
import requests
import json
import geopandas as gpd
from shapely.geometry import Point
# Overpass API エンドポイント
url = "https://overpass-api.de/api/interpreter"
# リクエスト送信
response = requests.get(url, params={'data': query})
# OSM オブジェクトリストを取得
data = response.json()
nodes = data['elements']
# ノードデータを抽出し、GeoDataFrameに変換
names = []
branches = []
coords = []
for node in nodes:
if node['type'] == 'node':
names.append(node['tags'].get('name', 'Unnamed'))
branches.append(node['tags'].get('branch', ''))
coords.append(Point(node['lon'], node['lat']))
gdf = gpd.GeoDataFrame({'name': names, 'branch': branches, 'geometry': coords}, crs='EPSG:4326')
# 緯度と経度を抽出してリストを作成
machida_coord = [(point.y, point.x) for point in gdf.geometry]
次に、こちらのコードで先ほどのクエリを基にリクエストを送信して、データを取得します。さらに扱いやすいようにDataFrameに変換します。(machida_coordは到達圏を算出する際に使用します)
これで、町田市のコンビニのポイントデータを取得できました。
import folium
# Foliumで地図を作成
# 中心座標をポイントデータの最初の地点に設定
center_lat = gdf.geometry.iloc[0].y
center_lon = gdf.geometry.iloc[0].x
folium_map = folium.Map(location=[center_lat, center_lon], zoom_start=14)
# ポイントデータを地図に追加
for _, row in gdf.iterrows():
lat = row.geometry.y
lon = row.geometry.x
name = row['name']
folium.CircleMarker(
location=[lat, lon],
radius=8, # 円の半径(ピクセル単位)
color='red', # 枠線の色
fill=True, # 塗りつぶし
fill_color='red', # 塗りつぶしの色
fill_opacity=0.6 # 塗りつぶしの透明度
).add_to(folium_map)
# 地図を表示
folium_map
試しに、こちらのコードでポイントデータを地図上に示してみます。可視化にはfoliumというライブラリを使います。
foliumはleaflet.jsというJavascriptで使用することのできるマップをPythonライブラリ化したものです。これにより、Pythonで簡易的に地図を用いてデータの可視化を行えます。
地図はOSMが標準として使用できます。
出力はこちらです。コンビニが赤いシンボルで示されています。
駅の周辺にコンビニが集中していることがわかります。
2.町田市の道路データを取得
次に、町田市の道路データをosmnxというライブラリを用いて、OSMから取得します。
osmnxはOSMから地理空間情報をダウンロードし、モデル化, 分析, 可視化が可能なライブラリです。
import osmnx as ox
# 町田市の歩行者通行可能な道路情報(グラフネットワーク)を取得
place_name = "Machida, Japan"
G = ox.graph_from_place(place_name, network_type='walk')
こちらのコードで道路データを取得します。
今回は徒歩での移動を想定して、network_typeをwalkにしています。
network_typeは、どの種類の道路のグラフネットワークを取得するかを指定する引数で、
以下のような種類があります。
- drive : 運転可能な公道(ただし、整備道路は取得しない)
- drive_service : 整備道路を含む運転可能な道路
- walk : 歩行者が使用できるすべての道路と小道(一方通行は無視する)
- bike : サイクリストが使用できるすべての道路と小道
- all : すべての私道でないOSMの道路と小道
- all_private : 私道を含むすべてのOSMの道路と小道
# グラフをGeoDataFrameに変換
nodes, edges = ox.graph_to_gdfs(G)
# Foliumで地図を作成
# ノードのGeoDataFrameから地図の中心を計算
center_lat = nodes["y"].mean() # 緯度(y座標)の平均
center_lon = nodes["x"].mean() # 経度(x座標)の平均
folium_map = folium.Map(location=[center_lat, center_lon], zoom_start=12)
# エッジ(道路)を地図に追加
for _, row in edges.iterrows():
# 各エッジのジオメトリを取得
geom = row["geometry"]
if geom.geom_type == "LineString":
# 単一ラインの場合
folium.PolyLine(
locations=[(lat, lon) for lon, lat in geom.coords],
color="blue", # 線の色
weight=2, # 線の太さ
).add_to(folium_map)
elif geom.geom_type == "MultiLineString":
# 複数ラインの場合
for linestring in geom:
folium.PolyLine(
locations=[(lat, lon) for lon, lat in linestring.coords],
color="blue", # 線の色
weight=2, # 線の太さ
).add_to(folium_map)
# 地図を表示
folium_map
道路データも試しに地図上に示してみます。コードはこちらです。
緯度と経度の平均値から地図の中心を決定していますが、foliumではインタラクティブな地図を作成するので、地図の中心をどこに設定しても大きな問題はありません。
出力はこちらです。これで町田市の道路データが取得できました。
3.到達圏を算出
最後に到達圏を算出します。
今回はコンビニから400m(徒歩5分程度)の到達圏を作成します。
import networkx as nx
import pandas as pd
# 空のリストを作成して、ポリゴン(到達圏)とエッジ(到達圏内の道路)を格納する準備
all_polygons = []
all_edges = []
meters = 400 # 到達圏の距離を設定
for coord in machida_coord:
# 座標(コンビニ)に最も近い道路上の1地点を取得
nearest_node = ox.distance.nearest_nodes(G, coord[1], coord[0])
# 取得した1地点から指定した道のり(meters)内の道路のサブグラフを取得
subgraph = nx.ego_graph(G, nearest_node, radius=meters, distance='length')
# サブグラフからノード(無数にある道路上の点)とエッジ(道路)のジオデータフレームを取得
nodes, edges = ox.graph_to_gdfs(subgraph)
# ノードのジオメトリを使ってポリゴン(到達圏)を作成(ノードが形成する領域の外枠を描く)
convex_hull = nodes.geometry.union_all().convex_hull
# 作成したポリゴンをリストに追加
all_polygons.append({'geometry': convex_hull})
# サブグラフのエッジをリストに追加
all_edges.append(edges[['geometry']])
# ポリゴンとエッジのジオデータフレームを作成
polygons_gdf = gpd.GeoDataFrame(all_polygons, crs='EPSG:4326')
edges_gdf = gpd.GeoDataFrame(pd.concat(all_edges, ignore_index=True), crs='EPSG:4326')
こちらのコードで、コンビニの緯度経度が入ったリスト(machida_coord)と町田市の道路のグラフネットワーク(G)から、到達圏が算出できます。
ざっくりプロセスの説明をすると、それぞれのコンビニにおいて、
- コンビニに最も近い道路上の1地点を取得する
- 取得した1地点から指定した道のり(400m)の範囲内の道路を抽出する
- 抽出した道路をすべて囲うように多角形(到達圏)を描く
from folium import GeoJson
# 中心座標をポイントデータの最初の地点に設定
center_lat = gdf.geometry.iloc[0].y
center_lon = gdf.geometry.iloc[0].x
folium_map = folium.Map(location=[center_lat, center_lon], zoom_start=14)
# ポリゴンを地図に追加
for _, row in polygons_gdf.iterrows():
GeoJson(row['geometry'], style_function=lambda x: {
'fillColor': 'blue', 'color': 'blue', 'weight': 2, 'fillOpacity': 0.4
}).add_to(folium_map)
# エッジ(道路データ)を地図に追加
for _, row in edges_gdf.iterrows():
GeoJson(row['geometry'], style_function=lambda x: {
'color': 'black', 'weight': 1
}).add_to(folium_map)
# ポイントデータを地図に追加
for _, row in gdf.iterrows():
lat = row.geometry.y
lon = row.geometry.x
name = row['name']
folium.CircleMarker(
location=[lat, lon],
radius=8,
color='red',
fill=True,
fill_color='red',
fill_opacity=0.6,
).add_to(folium_map)
# htmlに保存
folium_map.save("rfolium_map.html")
# 地図を表示
folium_map
最後にこちらのコードで到達圏を可視化します。
foliumは作成した地図をhtmlファイルに出力することもできます。
これで、町田市のコンビニエンスストアの400m到達圏が可視化できました。
コンビニ、到達圏、到達圏内の道路がそれぞれ赤、青、黒で示されています。
到達圏の大きさや形状は道路に依存し、アクセスの良さを示します。一般的に都市部では到達圏が広がりやすく、郊外や地方では狭まる傾向にあります。
また、コンビニは必ず到達圏の中心に位置するわけではなく、河川や線路の向こう側には到達圏が広がりづらい傾向にあります。
これは河川や線路をはさむと、その向こう側には影響力を及ぼしづらいことを示唆します。
まとめ
今回は、Pythonを使って到達圏解析を行ってみました。
少ないコードで比較的簡単に到達圏を作成できることがわかりました。また、foliumを使うことで簡単に可視化することができました。
今回は道路をすべて一様なものとして、単純な到達圏を作成しましたが、道幅、交通量、傾斜などを基に道路にコストを付与して到達圏解析することもできます。こちらの方がより現実的な結果を得ることが可能です。
本記事が、地理空間情報を扱う分析に興味を持っていただくきっかけになれば幸いです。
最後までお読みいただきありがとうございました。
参考
採用情報
- D2Cグループ採用サイト
- D2C問い合わせフォーム
株式会社D2C d2c.co.jp のテックブログです。 D2Cは、NTTドコモと電通などの共同出資により設立されたデジタルマーケティング企業です。 ドコモの膨大なデータを活用した最適化を行える広告配信システムの開発をしています。
Discussion