「都市部の話題の中心地を特定する」

「注目の話題を中心にした都市部の特定」

ブダペストのヒップスタースポット。

OSMとDBSCAN空間クラスタリングを使用した最も話題のある都市のエリアをキャプチャするための汎用フレームワーク

この記事では、OpenStreeetMap (OSM) から収集した POI (ポイント・オブ・インタレスト) に基づいて、特定の興味に対してホットスポットを識別することができる、簡単で使いやすい手法を紹介します。sklearnのDBSCAN アルゴリズムを使用して、いくつかのカテゴリに属する POI の生データを収集し、それを便利な GeoDataFrame に変換した後、地理空間クラスタリングを行い、最後に各クラスタ内で異なる都市機能がどのように混在しているかに基づいて結果を評価します。

今回「ヒップスター」と呼ぶトピックとそれに関連する POI カテゴリの選択は多少なりとも任意的ですが、自動的なホットスポット検出の方法は同じです。このような簡単に採用できる方法の利点は、イノベーション計画をサポートする地元のイノベーションハブの特定から、都市計画イニシアチブをサポートする都市のサブセンターの検出、ビジネスの異なる市場機会の分析、不動産投資機会の分析、観光のホットスポットのキャプチャにまで及びます。

すべての画像は作者によって作成されました。

1. OSMからデータを取得する

このコードブロックの結果は、ダウンロードされたPOIカテゴリの頻度分布です:

ダウンロードされた各POIカテゴリの頻度分布。

2. POIデータを可視化する

さて、すべての2101のPOIを可視化しましょう:

import matplotlib.pyplot as pltf, ax = plt.subplots(1,1,figsize=(10,10))admin.plot(ax=ax, color = 'none', edgecolor = 'k', linewidth = 2)gdf_poi.plot(column = 'amenity', ax=ax, legend = True, alpha = 0.3)

このコードセルの結果:

ダウンロードされたすべてのPOIがカテゴリ別にラベル付けされたブダペスト。

このプロットは非常に解釈が困難ですが、市中心部が非常に混雑していることがわかりますので、対話型の可視化ツールFoliumを使用してみましょう。

import foliumimport branca.colormap as cm# 都市の重心を取得し、マップをセットアップするx, y = admin.geometry.to_list()[0].centroid.xym = folium.Map(location=[y[0], x[0]], zoom_start=12, tiles='CartoDB Dark_Matter')colors = ['blue', 'green', 'red', 'purple', 'orange', 'pink', 'gray', 'cyan', 'magenta', 'yellow', 'lightblue', 'lime']# gdf_poiを変換してinity_colors = {}unique_amenities = gdf_poi['amenity'].unique()for i, amenity in enumerate(unique_amenities):    amenities[amenity] = colors[i % len(colors)]# 散布図を使用してpoiを可視化for idx, row in gdf_poi.iterrows():    amenity = row['amenity']    lat = row['geometry'].y    lon = row['geometry'].x    color = amenities.get(amenity, 'gray')  # カラーマップにない場合はデフォルトで灰色とする        folium.CircleMarker(        location=[lat, lon],        radius=3,          color=color,        fill=True,        fill_color=color,        fill_opacity=1.0,  # ドットマーカーには透明度なし        ).add_to(m)# マップを表示m

このマップのデフォルトビュー(zoom_start=12パラメータを調整して簡単に変更できます):

ダウンロードされたすべてのPOIがカテゴリ別にラベル付けされたブダペスト — 対話型版、最初のズーム設定。

そして、ズームパラメータを変更してマップを再プロットするか、マウスを使用してズームインすることもできます:

ダウンロードされたすべてのPOIがカテゴリ別にラベル付けされたブダペスト — 対話型版、2番目のズーム設定。

または、完全にズームアウト:

ダウンロードされたすべてのPOIがカテゴリ別にラベル付けされたブダペスト — 対話型版、3番目のズーム設定。

3. 空間クラスタリング

必要なすべてのPOIを手に入れたので、まずはPOIを受け取り、クラスタリングを行う関数を作成します。 DBSDCANのepsパラメータを微調整します。これはクラスタの特徴的なサイズを量子化し、グループ化されるPOI間の距離を表します。さらに、幾何情報をローカルのCRS(EPSG:23700)に変換して、SI単位で作業します。CRSの変換についてはこちらをご覧ください。

from sklearn.cluster import DBSCAN # バージョン: 0.24.1from collections import Counter# クラスタリングを行うdef apply_dbscan_clustering(gdf_poi, eps):    feature_matrix = gdf_poi['geometry'].apply(lambda geom: (geom.x, geom.y)).tolist()    dbscan = DBSCAN(eps=eps, min_samples=1)  # 必要に応じてmin_samplesを調整できます    cluster_labels = dbscan.fit_predict(feature_matrix)    gdf_poi['cluster_id'] = cluster_labels    return gdf_poi# ローカルCRSへの変換gdf_poi_filt = gdf_poi.to_crs(23700)    # クラスタリングを行うeps_value = 50  clustered_gdf_poi = apply_dbscan_clustering(gdf_poi_filt, eps_value)# クラスタIDを持つGeoDataFrameを表示print('クラスタの数: ', len(set(clustered_gdf_poi.cluster_id)))clustered_gdf_poi

このセルの結果:

クラスタIDでラベル付けされたPOI GeoDataFrameのプレビュー

1237個のクラスタが見つかりましたが、それは居心地の良い、ヒップスター的なスポットを見ているだけに過ぎない場合には少し多すぎるようです。それでは、サイズの分布を見て、サイズの閾値を決定しましょう。2つのPOIを持つクラスタをヒップスタースポットと呼ぶのは、あまり妥当ではないでしょう。

clusters = clustered_gdf_poi.cluster_id.to_list()clusters_cnt = Counter(clusters).most_common()f, ax = plt.subplots(1,1,figsize=(8,4))ax.hist([cnt for c, cnt in clusters_cnt], bins = 20)ax.set_yscale('log')ax.set_xlabel('クラスタのサイズ', fontsize = 14)ax.set_ylabel('クラスタの数', fontsize = 14)

このセルの結果:

クラスタのサイズ分布

ヒストグラムのギャップを基に、少なくとも10個のPOIを持つクラスタを残します!これは現時点では十分な作業仮説です。ただし、POIの種類の数や地理的な範囲などを組み込んだ、より洗練された方法も考えられます。

to_keep = [c for c, cnt in Counter(clusters).most_common() if cnt>9]clustered_gdf_poi = clustered_gdf_poi[clustered_gdf_poi.cluster_id.isin(to_keep)]clustered_gdf_poi = clustered_gdf_poi.to_crs(4326)len(to_keep)

このスニペットでは、フィルタリング条件を満たす15個のクラスタがあることが示されています。

15個の本物のヒップスタークラスタがあるので、それらを地図に表示します:

import foliumimport random# シティの重心を取得し、地図を設定するmin_longitude, min_latitude, max_longitude, max_latitude = clustered_gdf_poi.total_boundsm = folium.Map(location=[(min_latitude+max_latitude)/2, (min_longitude+max_longitude)/2], zoom_start=14, tiles='CartoDB Dark_Matter')# 各クラスタにユニークなランダムな色を取得するunique_clusters = clustered_gdf_poi['cluster_id'].unique()cluster_colors = {cluster: "#{:02x}{:02x}{:02x}".format(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) for cluster in unique_clusters}# POIを可視化するfor idx, row in clustered_gdf_poi.iterrows():    lat = row['geometry'].y    lon = row['geometry'].x    cluster_id = row['cluster_id']    color = cluster_colors[cluster_id]        # ドットマーカーを作成     folium.CircleMarker(        location=[lat, lon],        radius=3,         color=color,        fill=True,        fill_color=color,        fill_opacity=0.9,          popup=row['amenity'],     ).add_to(m)# 地図を表示m
ヒップスターPOIクラスター-最初のズームレベル。
ヒップスターPOIクラスター-2番目のズームレベル。
ヒップスターPOIクラスター-3番目のズームレベル。

4. クラスターの比較

各クラスターは洒落た、ヒップスターなクラスターとして数えられますが、それぞれが何らかの形でユニークである必要がありますよね?提供するPOIカテゴリのポートフォリオを比較して、それらがどれだけユニークか見てみましょう。

まず、多様性を追求し、各クラスターのPOIカテゴリの多様性/多様性を計算することによって、各クラスターのエントロピーを測定します。

import mathimport pandas as pddef get_entropy_score(tags):    tag_counts = {}    total_tags = len(tags)    for tag in tags:        if tag in tag_counts:            tag_counts[tag] += 1        else:            tag_counts[tag] = 1    tag_probabilities = [count / total_tags for count in tag_counts.values()]    shannon_entropy = -sum(p * math.log(p) for p in tag_probabilities)    return shannon_entropy# 各クラスターに固有のamenitiesのリストを持つ辞書を作成clusters_amenities = clustered_gdf_poi.groupby(by = 'cluster_id')['amenity'].apply(list).to_dict()# エントロピーのスコアを計算して保存するためのデータフレームentropy_data = []for cluster, amenities in clusters_amenities.items():    E = get_entropy_score(amenities)    entropy_data.append({'cluster' : cluster, 'size' :len(amenities), 'entropy' : E})    # エントロピーのスコアをデータフレームに追加entropy_data = pd.DataFrame(entropy_data)entropy_data

このセルの結果:

POIプロファイルに基づく各クラスターの多様性(エントロピー)

そして、このテーブルのクイック相関分析:

entropy_data.corr()
クラスター特徴量間の相関関係。

クラスターID、クラスターサイズ、およびクラスターエントロピーの間の相関を計算した結果、サイズとエントロピーの間には有意な相関がありますが、すべての多様性を説明するには遠いです。明らかに、いくつかのホットスポットは他よりも多様性があります – 他は少し特化しています。特化しているのは何ですか?各クラスターのPOIプロファイルをクラスター内の各POIタイプの全体的な分布と比較し、クラスターにとって最も典型的なトップ3のPOIカテゴリを選びます。

# poiプロファイルを辞書にパックするclusters = sorted(list(set(clustered_gdf_poi.cluster_id)))amenity_profile_all = dict(Counter(clustered_gdf_poi.amenity).most_common())amenity_profile_all = {k : v / sum(amenity_profile_all.values()) for k, v in amenity_profile_all.items()}# 各カテゴリの相対頻度を計算し、平均よりも上位(>1)の上位3つの候補のみを保持するclusters_top_profile = {}for cluster in clusters:        amenity_profile_cls = dict(Counter(clustered_gdf_poi[clustered_gdf_poi.cluster_id == cluster].amenity).most_common() )    amenity_profile_cls = {k : v / sum(amenity_profile_cls.values()) for k, v in amenity_profile_cls.items()}        clusters_top_amenities = []    for a, cnt in amenity_profile_cls.items():        ratio = cnt / amenity_profile_all[a]        if ratio>1: clusters_top_amenities.append((a, ratio))        clusters_top_amenities = sorted(clusters_top_amenities, key=lambda tup: tup[1], reverse=True)        clusters_top_amenities = clusters_top_amenities[0:min([3,len(clusters_top_amenities)])]    clusters_top_profile[cluster] = [c[0] for c in clusters_top_amenities]    # 各クラスターの上位カテゴリを出力する:for cluster, top_amenities in clusters_top_profile.items():    print(cluster, top_amenities)

このコードブロックの結果:

各クラスターのユニークなアメニティの指紋。

トップカテゴリーの説明には既にいくつかのトレンドが見られます。例えば、クラスター17は明らかに飲み物を楽しむためのものであり、19は音楽と組み合わせて、おそらくパーティーにも適しています。本屋、ギャラリー、カフェがあるクラスター91は、昼間にリラックスする場所として利用されることが確かです。一方、音楽とギャラリーがあるクラスター120は、どんなパブツアーの暖機にも最適です。分布からも、バーへの立ち寄りはいつでも適切であることがわかります(または、利用事例に応じて、カテゴリーの頻度に基づいたさらなる正規化を考える必要があるかもしれません)!

結論

地元の住民として、これらのクラスターが理にかなっており、単純な手法にもかかわらず都市機能のミックスをうまく表していることを確認できます。もちろん、これは素早いパイロット版であり、以下のようにいくつかの方法で充実させたり修正したりすることができます:

  • より詳細なPOIの分類と選択に頼る
  • クラスタリングを行う際にPOIのカテゴリーを考慮する(意味的クラスタリング)
  • POI情報をSNSのレビューや評価などで充実させる

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more