Identification des points chauds thématiques dans les zones urbaines
Identification des hotspots thématiques dans les zones urbaines

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.
- Considérations pratiques dans la conception d’application RAG
- Émissions de carbone d’une équipe d’ingénierie ML
- Examiner l’impact du boom de l’IA sur les services cloud
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 :

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 :

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 :

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) :

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

Ou zoomer complètement :

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 :

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 :

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



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 :

Et une rapide analyse de corrélation sur ce tableau :
entropy_data.corr()

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:

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!
Was this article helpful?
93 out of 132 found this helpful
Related articles
- Exploiter les super pouvoirs du TPN un tutoriel étape par étape sur l’affinage Hugging Face
- De 2D à 3D Améliorer la cohérence de génération de texte en 3D avec des présomptions géométriques alignées
- Déverrouillage de la transparence de l’IA Comment le regroupement des fonctionnalités d’Anthropic améliore l’interprétabilité des réseaux neuronaux.
- Optimisation fine LLM Optimisation fine efficiente des paramètres (PEFP) — LoRA et QLoRA — Partie 1
- Oracle présente sa vision d’un avenir axé sur l’IA et le Cloud
- La bulle de l’IA générative éclatera bientôt
- Décrypter la signification statistique Le guide du professionnel du marketing