前回は、Foursquare、Overture、OpenStreetMapのPOIデータを比較しました。今回はその続きとして、公開POI(お店や施設などの地点)から街を数値化し、似た街の検索、クラスタリング、地域Embeddingまで試します。
先に結論を書くと、POIから地域を比較する仕組みは作れます。ただし、得られるのは「指定した条件から見た街の一側面」であり、街そのものの性格ではありません。
1地域を1行の表にする
最初に作るのが「地域特徴量」です。これは、複数の地域を同じ物差しで比べるための数値プロフィールです。
今回は東京都内656駅について、駅の代表点を中心とする1km四方を集計しました。保存する表は地域ID、面積、件数なども含む15列で、そのうち比較に使ったのは次の8項目です。
- POI登録密度
- 食事、カフェ、バー、衣料、美容、食品小売の構成比
- 250m格子で見たPOIの局所集中度(最も密集した格子が地域平均の何倍か)
実装のポイントは、範囲内のPOIを数え、どの地域にも同じ列を用意することです。次のコードは、計算部分だけを抜き出しています。
count = len(places)
density = count / area_km2
category_counts = places["display_group"].value_counts()
profile = {
"region_id": region_id,
"poi_density_per_km2": density,
**{
f"{name}_share": category_counts.get(name, 0) / count
for name in category_groups
},
"peak_to_mean_density_250m": peak_density / density,
}
この表では、1行が1地域、1列が1つの特徴です。これで地域同士を同じ形の数値で比べられます。
この地域データは、似た街を探したり、街をグループに分けたり、細かなカテゴリを圧縮したりするのに使います。
数値の距離から「似た街」を探す
列ごとに単位とばらつきが違うため、密度と集中度へlog1p(極端に大きい値の影響を抑える対数変換)を適用し、中央値と四分位範囲(データ中央50%の幅)で数値をそろえました。6つのカテゴリ比率は、その部分だけが距離を支配しないよう重みも調整しています。
values[["density", "concentration"]] = np.log1p(
values[["density", "concentration"]]
)
z = (values - values.median()) / (
values.quantile(0.75) - values.quantile(0.25)
)
z[category_columns] /= np.sqrt(6)
distance = np.linalg.norm(z - z.loc["kichijoji"], axis=1)
この8特徴量で吉祥寺に近かったのは、町田、自由が丘、立川南などでした。
ここでの距離は類似率や正解率ではありません。「今回選んだ8項目が近い順」です。特徴量や重みを変えれば順位も変わります。
基準の街を決めずに分類する
同じ8特徴量をK-means(近いデータ同士を指定した数のグループに分ける手法)へ入力すると、656駅周辺を大きく3群へ整理できました。吉祥寺は「高密度・飲食集積型」と仮称した群に入りました。
ただし、「東京の街は本当に3種類ある」という意味ではありません。3~8群を比較しても評価指標ごとの最良値は一致せず、3群は規模、安定性、説明しやすさから選んだ粗い整理です。
961カテゴリを64次元へ圧縮する
8特徴量では、ATM、駐車場、事業所などを「その他」にまとめています。そこで次は、961カテゴリの件数をTF-IDF(多くの地域にあるカテゴリの重みを下げる方法)で重み付けし、TruncatedSVD(情報をなるべく残しながら列数を減らす線形手法)で64次元(64列の数値)へ圧縮しました。Embeddingとは、対象を比較や検索に使える固定長の数値ベクトルで表すことです。今回はLLMではなく、線形の次元圧縮で作ったものを「地域Embedding」と呼びます。
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.preprocessing import normalize
tfidf = TfidfTransformer(
norm="l2", smooth_idf=True, sublinear_tf=False
).fit_transform(category_counts)
embedding = TruncatedSVD(
n_components=64, n_iter=10, random_state=42
).fit_transform(tfidf)
embedding = normalize(embedding)
8特徴量、961カテゴリ、64次元Embeddingで、吉祥寺の上位10駅に共通したのは5駅でした。Embeddingでは中目黒や学芸大学などが上位へ入りました。
表現を細かくしたことで見える違いは増えましたが、Embeddingにしたから改善したとは言えません。 POIを半分ずつに分けた近傍一致率(上位候補がどれだけ共通したか)は、圧縮前の30.2%に対してEmbeddingは26.8%でした。また、「街の類似」に正解ラベル(正解として比較できる基準データ)がないため、どちらが正しいかも自動では判定できません。
いちばん注意したいのは「どこまでを街とするか」
駅から東西南北500mの範囲を800mへ広げると、3方式すべてで似た駅の順位が変わりました。500m時点の上位10駅のうち、800mでも残った平均駅数は次のとおりです。
| 表現 | 上位10に残った平均駅数 |
|---|---|
| A:8特徴量 | 2.24駅 |
| B:961カテゴリ | 3.31駅 |
| C:64次元Embedding | 3.19駅 |
駅の東側が商業地、西側が住宅地なら、範囲を広げる方向や形でも集計結果は変わります。地理空間分析では、集計区域の大きさや分け方で結果が変わる問題をMAUP(Modifiable Areal Unit Problem)と呼びます。
これは分析が無意味という話ではありません。「街の性格」を漠然と求めるのではなく、用途から範囲を決める必要があるということです。
| 避けたい問い | 比較しやすい問い |
|---|---|
| この駅はどんな性格か | 駅の各出口から徒歩5分で行けるカフェ構成は似ているか |
| 住みやすい街はどこか | 徒歩10分圏にある生活施設の構成は似ているか |
| 東京の街を正しく分類したい | 指定したPOIと範囲で大まかにグループを眺めたい |
まとめ
- 地域特徴量は、街を同じ列で比較するための数値プロフィール
- 類似検索とクラスタリングは、選んだ特徴から候補やまとまりを探す方法
- Embeddingは表現を変える方法であり、自動的に精度を上げる魔法ではない
- データ、特徴量、重み、分析範囲を変えると結果も変わる
- 「誰が、どこから、何を利用するか」を先に決めると、範囲と結果を説明しやすい
今回の一連の実験では、POIから街を数値化する方法だけでなく、その数値をどこまで信頼できるかも確認できました。地域ベクトルは街の本質ではなく、条件を決めて使う比較の道具と考えるのがよさそうです。
動画で見る
本文の前半と後半を図解した動画も用意しました。必要なところから見られます。
前編:地域特徴量と類似検索
後編:クラスタリング、地域Embedding、分析範囲の限界
個人ブログの詳しい解説
各実験の条件、図表、結果、注意点は、個人ブログで詳しく説明しています。
- POIデータから作る「地域特徴量」 Pythonで吉祥寺駅周辺を数値化してみた
- 「吉祥寺」に似た街はどこ? POIデータで東京都内656駅周辺を比べてみた
- POIデータで街をクラスタリング Pythonで東京都内656駅周辺を分類してみた
- 街をベクトルで表すと何が変わる? Pythonで地域Embeddingを試してみた
- POIで街を数値化して分かったこと 分析範囲で「似た街」はどう変わる?
コードと詳しい検証記録
使用したPOIはOverture Maps FoundationのOverture Places release(データ版)2026-09-23.0です。駅には国土交通省「国土数値情報(鉄道データ)N02-25」を加工して使用しました。POIは登録情報であり、実際の営業中店舗数、人流、売上、街の人気、住みやすさを表すものではありません。
※本記事は個人の見解であり、所属組織を代表するものではありません。







