Identification des points chauds thématiques dans les zones urbaines

Identification des hotspots thématiques dans les zones urbaines

Des lieux branchés à Budapest.

Un cadre générique utilisant OpenStreetMap et la clusterisation spatiale DBSCAN pour capturer les zones urbaines les plus en vogue

Dans cet article, je présente une méthodologie rapide et facile à utiliser capable d’identifier les hot spots d’un intérêt donné basé sur les Points d’intérêt (POI) collectés à partir de OpenStreetMap (OSM) en utilisant l’algorithme DBSCAN de sklearn. Tout d’abord, je collecte les données brutes de POI appartenant à quelques catégories que j’ai trouvées sur ChatGPT, et j’ai supposé qu’elles sont caractéristiques du mode de vie parfois appelé “hyp-lifestyle” (par exemple, les cafés, les bars, les marchés, les studios de yoga) ; après avoir converti ces données en un GeoDataFrame pratique, je réalise la clusterisation géospatiale et évalue finalement les résultats en fonction de la manière dont les différentes fonctionnalités urbaines se mêlent dans chaque cluster.

Alors que le choix du thème que j’appelle “hipster” et des catégories de POI qui y sont liées est quelque peu arbitraire, ils peuvent facilement être remplacés par d’autres sujets et catégories – la méthode de détection automatique des hot spots reste la même. Les avantages d’une telle méthode facile à adopter vont de l’identification des pôles d’innovation locaux soutenant la planification de l’innovation à la détection de sous-centres urbains soutenant les initiatives de planification urbaine, à l’évaluation de différentes opportunités de marché pour les entreprises, à l’analyse des opportunités d’investissement immobilier ou à la capture des hauts lieux touristiques.

Toutes les images ont été créées par l’auteur.

1. Acquérir les données d’OSM

Tout d’abord, je récupère le polygone administratif de la ville cible. Comme Budapest est ma ville natale, à des fins de validation facile sur le terrain, j’utilise cela. Cependant, comme je n’utilise que la base de données mondiale de OSM, ces étapes peuvent facilement être reproduites pour n’importe quelle autre partie du monde couverte par OSM. En particulier, j’utilise le package OSMNx pour obtenir facilement les limites administratives.

import osmnx as ox # version: 1.0.1city = 'Budapest'admin = ox.geocode_to_gdf(city)admin.plot()

Le résultat de ce bloc de code :

Les limites administratives de Budapest.

Maintenant, utilisez l’API OverPass pour télécharger les POI qui tombent dans la zone englobante des limites administratives de Budapest. Dans la liste amenity_mapping, j’ai compilé une liste de catégories de POI que j’associe au mode de vie hipster. Je dois également noter ici qu’il s’agit d’une catégorisation vague et non basée sur l’expertise, et avec les méthodes présentées ici, n’importe qui peut mettre à jour la liste des catégories en conséquence. De plus, on peut incorporer d’autres sources de données de POI contenant une catégorisation multi-niveaux plus précise pour une caractérisation plus précise du sujet donné. En d’autres termes, cette liste peut être modifiée de n’importe quelle manière que vous jugez appropriée, que ce soit pour mieux couvrir les choses branchées ou pour réajuster cet exercice à tout autre sujet de catégorisation (par exemple, les food courts, les zones commerçantes, les hauts lieux touristiques, etc).

Note : comme le téléchargeur OverPass renvoie tous les résultats dans une zone englobante, à la fin de ce bloc de code, je filtre les POI qui se trouvent en dehors des limites administratives en utilisant la fonction overlay de GeoPandas.

import overpy # version: 0.6from shapely.geometry import Point # version: 1.7.1import geopandas as gpd # version: 0.9.0# démarrer l'apiapi = overpy.Overpass()# obtenir la zone englobanteminx, miny, maxx, maxy = admin.to_crs(4326).bounds.T[0]bbox = ','.join([str(miny), str(minx), str(maxy), str(maxx)])# définir les catégories OSM d'intérêtamenity_mapping = [    ("amenity", "cafe"),    ("tourism", "gallery"),    ("amenity", "pub"),    ("amenity", "bar"),    ("amenity", "marketplace"),    ("sport", "yoga"),    ("amenity", "studio"),    ("shop", "music"),    ("shop", "second_hand"),    ("amenity", "foodtruck"),    ("amenity", "music_venue"),    ("shop", "books"),]# itérer sur toutes les catégories, appeler l'API overpass, # et ajouter les résultats à la liste des données poipoi_data  = []for idx, (amenity_cat, amenity) in enumerate(amenity_mapping):    query = f"""node["{amenity_cat}"="{amenity}"]({bbox});out;"""    result = api.query(query)    print(amenity, len(result.nodes))        for node in result.nodes:        data = {}        name = node.tags.get('name', 'N/A')        data['name'] = name        data['amenity'] = amenity_cat + '__' + amenity        data['geometry'] = Point(node.lon, node.lat)        poi_data.append(data)         # transformer les résultats en GeoDataFramegdf_poi = gpd.GeoDataFrame(poi_data)print(len(gdf_poi))gdf_poi = gpd.overlay(gdf_poi, admin[['geometry']])gdf_poi.crs = 4326print(len(gdf_poi))

Le résultat de ce bloc de code est la distribution de fréquence de chaque catégorie de points d’intérêt téléchargée :

La distribution de fréquence de chaque catégorie de points d'intérêt téléchargée.

2. Visualiser les données des points d’intérêt

Maintenant, visualisez les 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)

Le résultat de cette cellule de code :

Budapest avec tous les POI téléchargés étiquetés par leurs catégories.

Ce graphique est assez difficile à interpréter, à part le fait que le centre-ville est très fréquenté, alors passons à un outil de visualisation interactif, Folium.

import foliumimport branca.colormap as cm# obtenir le centroïde de la ville et configurer la cartex, 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']# transformer le gdf_poiamenity_colors = {}unique_amenities = gdf_poi['amenity'].unique()for i, amenity in enumerate(unique_amenities):    amenity_colors[amenity] = colors[i % len(colors)]# visualiser les pois avec un graphique de dispersionfor idx, row in gdf_poi.iterrows():    amenity = row['amenity']    lat = row['geometry'].y    lon = row['geometry'].x    color = amenity_colors.get(amenity, 'gray')  # gris par défaut si pas dans la coloration        folium.CircleMarker(        location=[lat, lon],        radius=3,          color=color,        fill=True,        fill_color=color,        fill_opacity=1.0,  # Pas de transparence pour les marqueurs à points        popup=amenity,    ).add_to(m)# afficher la mapm

La vue par défaut de cette carte (que vous pouvez facilement changer en ajustant le paramètre zoom_start=12) :

Budapest avec tous les POI téléchargés étiquetés par leurs catégories — version interactive, premier paramètre de zoom.

Ensuite, il est possible de modifier le paramètre de zoom et de retracer la carte, ou simplement de zoomer en utilisant la souris :

Budapest avec tous les POI téléchargés étiquetés par leurs catégories — version interactive, deuxième paramètre de zoom.

Ou zoomer complètement :

Budapest avec tous les POI téléchargés étiquetés par leurs catégories — version interactive, troisième paramètre de zoom.

3. Regroupement spatial

Maintenant que j’ai tous les points d’intérêt nécessaires en main, je passe à l’algorithme DBSCAN, en commençant par écrire une fonction qui prend les points d’intérêt et effectue le regroupement. Je vais uniquement affiner le paramètre eps de DBSDCAN, qui quantifie essentiellement la taille caractéristique d’un cluster, c’est-à-dire la distance entre les points d’intérêt à regrouper. De plus, je transforme les géométries en un système de référence local (EPSG:23700) pour travailler en unités SI. Plus d’informations sur les conversions de système de référence ici.

from sklearn.cluster import DBSCAN # version: 0.24.1
from collections import Counter

# effectuer le regroupement
def appliquer_regroupement_dbscan(gdf_poi, eps):
    feature_matrix = gdf_poi['geometry'].apply(lambda geom: (geom.x, geom.y)).tolist()
    dbscan = DBSCAN(eps=eps, min_samples=1)  # Vous pouvez ajuster min_samples en fonction de vos besoins
    cluster_labels = dbscan.fit_predict(feature_matrix)
    gdf_poi['cluster_id'] = cluster_labels
    return gdf_poi

# transformation en CRS local
gdf_poi_filt = gdf_poi.to_crs(23700)

# effectuer le regroupement
eps_value = 50
clustered_gdf_poi = appliquer_regroupement_dbscan(gdf_poi_filt, eps_value)

# Afficher le GeoDataFrame avec les identifiants de cluster
print('Nombre de clusters trouvés : ', len(set(clustered_gdf_poi.cluster_id)))
clustered_gdf_poi

Le résultat de cette cellule :

Aperçu du GeoDataFrame des points d'intérêt où chaque point est étiqueté par son identifiant de cluster.

Il y a 1237 clusters – cela semble être un peu trop si nous ne cherchons que des endroits confortables et branchés. Jetons un coup d’œil à leur distribution en terme de taille, puis fixons un seuil de taille – considérer un cluster avec deux points d’intérêt comme un endroit branché n’est probablement pas vraiment valide de toute façon.

clusters = clustered_gdf_poi.cluster_id.tolist()
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("Taille du cluster", fontsize=14)
ax.set_ylabel("Nombre de clusters", fontsize=14)

Le résultat de cette cellule :

Distribution des tailles de cluster.

En nous basant sur l’écart dans l’histogramme, conservons les clusters avec au moins 10 points d’intérêt ! Pour l’instant, il s’agit d’une hypothèse de travail assez simple. Cependant, cela pourrait également être travaillé de manière plus sophistiquée, par exemple en tenant compte du nombre de types de points d’intérêt différents ou de la surface géographique couverte.

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)

Ce morceau de code montre qu’il y a 15 clusters qui satisfont aux critères de filtrage.

Une fois que nous avons les 15 vrais clusters branchés, mettez-les sur une carte :

import folium
import random

# obtenir le centroïde de la ville et configurer la carte
min_longitude, min_latitude, max_longitude, max_latitude = clustered_gdf_poi.total_bounds
m = folium.Map(location=[(min_latitude+max_latitude)/2, (min_longitude+max_longitude)/2], zoom_start=14, tiles='CartoDB Dark_Matter')

# obtenir des couleurs uniques et aléatoires pour chaque cluster
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}

# visualiser les points d'intérêt
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]

    # créer un marqueur
    folium.CircleMarker(
        location=[lat, lon],
        radius=3,
        color=color,
        fill=True,
        fill_color=color,
        fill_opacity=0.9,
        popup=row['amenity'],
    ).add_to(m)

# afficher la carte
m
Clusters de POI Hipster - premier niveau de zoom.
Clusters de POI Hipster - deuxième niveau de zoom.
Clusters de POI Hipster - troisième niveau de zoom.

4. Comparaison des clusters

Chaque cluster est considéré comme un cluster à la mode et branché – cependant, ils doivent tous être uniques d’une manière ou d’une autre, n’est-ce pas ? Voyons à quel point ils sont uniques en comparant le portefeuille de catégories de POI qu’ils ont à offrir.

Tout d’abord, visez la diversité et mesurez la variété/diversité des catégories de POI dans chaque cluster en calculant leur entropie.

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# créer un dictionnaire où chaque cluster a sa propre liste d'aménagementsclusters_amenities = clustered_gdf_poi.groupby(by = 'cluster_id')['amenity'].apply(list).to_dict()# calculer et enregistrer les scores d'entropieentropy_data = []for cluster, amenities in clusters_amenities.items():    E = get_entropy_score(amenities)    entropy_data.append({'cluster' : cluster, 'taille' :len(amenities), 'entropie' : E})    # ajouter les scores d'entropie à une dataframeentropy_data = pd.DataFrame(entropy_data)entropy_data

Résultat de cette cellule :

La diversité (entropie) de chaque cluster basée sur son profil POI.

Et une rapide analyse de corrélation sur ce tableau :

entropy_data.corr()
La corrélation entre les caractéristiques des clusters.

Après avoir calculé la corrélation entre l’ID du cluster, la taille du cluster et l’entropie du cluster, il existe une corrélation significative entre la taille et l’entropie ; cependant, cela explique loin d’être toute la diversité. Apparemment, en effet, certains points chauds sont plus diversifiés que d’autres – tandis que d’autres sont quelque peu plus spécialisés. Dans quoi sont-ils spécialisés ? Je répondrai à cette question en comparant les profils POI de chaque cluster à la distribution globale de chaque type de POI au sein des clusters et en choisissant les trois catégories de POI les plus typiques pour un cluster par rapport à la moyenne.

# regrouper les profils de poi dans des dictionnairesclusters = 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()}# calcul de la fréquence relative de chaque catégorie# et ne garder que les candidats au-dessus de la moyenne (> 1) et les trois premiersclusters_top_profile = {}pour cluster dans les 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 = []    pour 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 = triés(clusters_top_amenities, 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] pour c in clusters_top_amenities]    # print, for each cluster, its top categories:pour cluster, top_amenities in clusters_top_profile.items():    print(cluster, top_amenities)

Le résultat de ce bloc de code:

L'empreinte unique des équipements de chaque cluster.

Les descriptions des catégories supérieures montrent déjà certaines tendances. Par exemple, le cluster 17 est clairement destiné à la consommation de boissons, tandis que le cluster 19 mélange également la musique, peut-être pour faire la fête. Le cluster 91, avec des librairies, des galeries et des cafés, est certainement un endroit propice à la détente en journée, tandis que le cluster 120, avec de la musique et une galerie, peut être une excellente préparation pour une tournée des bars. À partir de la distribution, on peut également voir que se rendre dans un bar est toujours approprié (ou, en fonction du cas d’utilisation, nous devrions envisager d’autres normalisations basées sur les fréquences des catégories) !

Remarques finales

En tant que résident local, je peux confirmer que ces clusters ont beaucoup de sens et représentent très bien le mélange souhaité de fonctionnalités urbaines malgré la méthodologie simple. Bien sûr, il s’agit d’un pilote rapide qui peut être enrichi et amélioré de plusieurs manières, telles que:

  • Se fier à une catégorisation et une sélection plus détaillées des POI (points d’intérêt)
  • Prendre en compte les catégories de POI lors de la réalisation du clustering (clustering sémantique)
  • Enrichir les informations sur les POI avec, par exemple, les avis et les notes des médias sociaux

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

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

AI

Application Web de Composeur de Courrier Électronique Linguistique Utilisant OpenAI et Langchain

Introduction Dans cet article, nous verrons comment construire une application web en utilisant OpenAI avec l’a...

AI

Top 10 Outils d'IA pour l'Analyse de Données

À mesure que les données commerciales deviennent de plus en plus compliquées chaque jour, des méthodes avancées sont ...

Actualités sur l'IA

Les Grammy Awards interdisent l'utilisation de l'IA les créateurs humains prennent le devant de la scène.

L’Académie d’enregistrement, responsable du prestigieux Grammy Awards, a décidé d’interdire l’...

AI

Vous pouvez garder votre emploi, mais ce ne sera pas le même emploi

Alors que l'IA empiète sur nos compétences en programmation, elle n'a en aucun cas surpassé le langage humain. C'est ...

AI

11 Générateurs de Vidéo IA à Utiliser en 2023 Transformer le Texte en Vidéo

Une des manifestations les plus remarquables de l’IA est l’émergence des générateurs de vidéos IA, qui on...