Identificar los puntos calientes temáticos en áreas urbanas

Descubriendo los puntos candentes temáticos en áreas urbanas

Los lugares de moda hipster en Budapest.

Un marco genérico que utiliza OpenStreetMap y DBSCAN Spatial Clustering para capturar las áreas urbanas más populares

En este artículo, muestro una metodología rápida y fácil de usar que es capaz de identificar lugares de moda para un interés dado basado en Puntos de interés (POI) recolectados de OpenStreeetMap (OSM) utilizando el algoritmo DBSCAN de sklearn. Primero, recolectaré los datos sin procesar de los POI pertenecientes a un par de categorías que encontré en ChatGPT, y asumí que son características del estilo de vida a veces llamado ‘hyp’ (por ejemplo, cafeterías, bares, mercados, estudios de yoga); después de convertir esos datos en un práctico GeoDataFrame, realizaré la agrupación geoespacial y finalmente evaluaré los resultados en función de cómo se mezclan las diferentes funcionalidades urbanas en cada clúster.

Aunque la elección del tema que llamo ‘hipster’ y las categorías de POI vinculadas a él son algo arbitrarias, se pueden reemplazar fácilmente por otros temas y categorías. El método automático de detección de lugares de moda se mantiene igual. Las ventajas de un método tan fácil de adoptar van desde identificar centros de innovación locales que apoyan la planificación de la innovación, hasta detectar subcentros urbanos que respaldan iniciativas de planificación urbana, evaluar diferentes oportunidades de mercado para empresas, analizar oportunidades de inversión inmobiliaria o capturar lugares turísticos populares.

Todas las imágenes fueron creadas por el autor.

1. Adquirir datos de OSM

Primero, obtengo el polígono administrativo de la ciudad objetivo. Como Budapest es mi ciudad natal, para fines de validación fácil (en el terreno), la utilizo. Sin embargo, como solo estoy utilizando la base de datos global de OSM, estos pasos se pueden reproducir fácilmente para cualquier otra parte del mundo que cubra OSM. En particular, utilizo el paquete OSMNx para obtener los límites administrativos de manera súper fácil.

import osmnx as ox # versión: 1.0.1ciudad = 'Budapest'admin = ox.geocode_to_gdf(ciudad)admin.plot()

El resultado de este bloque de código:

Los límites administrativos de Budapest.

Ahora, utiliza la API de OverPass para descargar los POI que se encuentren dentro de la caja delimitadora de los límites administrativos de Budapest. En la lista amenity_mapping, he compilado una lista de categorías de POI que asocio con el estilo de vida hipster. También tengo que mencionar aquí que esta es una categorización vaga y no basada en expertos, y con los métodos presentados aquí, cualquiera puede actualizar la lista de categorías en consecuencia. Además, uno puede incorporar otras fuentes de datos de POI que contengan una categorización multinivel más precisa para una caracterización más exacta del tema dado. En otras palabras, esta lista se puede cambiar de cualquier manera que consideres adecuada, desde cubrir mejor las cosas hipsters hasta readaptar este ejercicio a cualquier otra categorización de temas (por ejemplo, áreas de comida, áreas comerciales, lugares turísticos, etc).

Nota: como el descargador de OverPass devuelve todos los resultados dentro de una caja delimitadora, al final de este bloque de código, filtro aquellos POI fuera de los límites administrativos utilizando la función de superposición de GeoPandas.

import overpy # versión: 0.6from shapely.geometry import Point # versión: 1.7.1import geopandas as gpd # versión: 0.9.0# iniciar la apia = overpy.Overpass()# obtener la caja delimitadora que envuelve elárea geográfica de la ciudadminx, miny, maxx, maxy = admin.to_crs(4326).bounds.T[0]bbox = ','.join([str(miny), str(minx), str(maxy), str(maxx)])# definir las categorías OSM de interésamenity_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"),]# iterar sobre todas las categorías, llamar a la api de overpass, # y agregar los resultados a la lista de datos poi_datapoi_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)         # transformar los resultados en un 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))

El resultado de este bloque de código es la distribución de frecuencia de cada categoría de POI descargada:

La distribución de frecuencia de cada categoría de POI descargada.

2. Visualizar los datos de POI

Ahora, visualiza todos los 2101 POIs:

import matplotlib.pyplot as plt
f, 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)

El resultado de esta celda de código:

Budapest con todos los POIs descargados etiquetados por sus categorías.

Este gráfico es bastante difícil de interpretar, excepto que el centro de la ciudad está muy congestionado. Así que vamos a usar una herramienta de visualización interactiva, Folium.

import folium
import branca.colormap as cm
# obtener el centroide de la ciudad y configurar el mapa
x, y = admin.geometry.to_list()[0].centroid.xy
m = 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']
# transformar el gdf_poi
amenity_colors = {}
unique_amenities = gdf_poi['amenity'].unique()
for i, amenity in enumerate(unique_amenities):
    amenity_colors[amenity] = colors[i % len(colors)]
# visualizar los pois con un gráfico de dispersión
for idx, row in gdf_poi.iterrows():
    amenity = row['amenity']
    lat = row['geometry'].y
    lon = row['geometry'].x
    color = amenity_colors.get(amenity, 'gray')  # default to gray if not in the colormap
    folium.CircleMarker(
        location=[lat, lon],
        radius=3,
        color=color,
        fill=True,
        fill_color=color,
        fill_opacity=1.0,  # Sin transparencia para los marcadores de puntos
        popup=amenity,
    ).add_to(m)
# mostrar el mapa
m

La vista predeterminada de este mapa (que puedes cambiar fácilmente ajustando el parámetro zoom_start=12):

Budapest con todos los POIs descargados etiquetados por sus categorías - versión interactiva, primera configuración de zoom.

Luego, es posible cambiar el parámetro de zoom y volver a dibujar el mapa, o simplemente acercar usando el mouse:

Budapest con todos los POIs descargados etiquetados por sus categorías - versión interactiva, segunda configuración de zoom.

O hacer zoom completamente hacia fuera:

Budapest con todos los POIs descargados etiquetados por sus categorías - versión interactiva, tercera configuración de zoom.

3. Agrupación espacial

Ahora que tengo todos los POI necesarios a mano, utilizo el algoritmo DBSCAN, primero escribiendo una función que toma los POI y realiza la agrupación. Solo ajustaré el parámetro eps de DBSDCAN, que básicamente cuantifica el tamaño característico de un grupo, la distancia entre los POI que se van a agrupar. Además, transformo las geometrías a un CRS local (EPSG:23700) para trabajar en unidades SI. Más sobre las conversiones de CRS aquí.

from sklearn.cluster import DBSCAN # versión: 0.24.1from collections import Counter# realizar la agrupacióndef 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)  # Puedes ajustar min_samples según sea necesario    cluster_labels = dbscan.fit_predict(feature_matrix)    gdf_poi['cluster_id'] = cluster_labels    return gdf_poi# transformar a local CRSgdf_poi_filt = gdf_poi.to_crs(23700)    # realizar la agrupacióneps_value = 50  clustered_gdf_poi = apply_dbscan_clustering(gdf_poi_filt, eps_value)# Imprimir el GeoDataFrame con los IDs de los gruposprint('Número de grupos encontrados: ', len(set(clustered_gdf_poi.cluster_id)))clustered_gdf_poi

El resultado de esta celda:

Vista previa del GeoDataFrame de los POI donde cada uno está etiquetado con su ID de grupo.

Hay 1237 grupos, eso parece ser un poco demasiado si solo estamos buscando lugares acogedores y modernos. Echemos un vistazo a su distribución de tamaño y luego elijamos un umbral de tamaño: llamar a un grupo con dos POI lugares de moda probablemente no sea muy sólido de todos modos.

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('Tamaño del grupo', fontsize = 14)ax.set_ylabel('Número de grupos', fontsize = 14)

El resultado de esta celda:

Distribución del tamaño del grupo.

¡Basándonos en la brecha en el histograma, mantengamos los grupos con al menos 10 POI! Por ahora, esta es una hipótesis de trabajo lo suficientemente simple. Sin embargo, esto también se podría desarrollar de manera más sofisticada, por ejemplo, incorporando el número de diferentes tipos de POI o el área geográfica cubierta.

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)

Este fragmento muestra que hay 15 grupos que cumplen con el filtro.

Una vez que tenemos los 15 grupos de lugares de moda reales, coloquémoslos en un mapa:

import foliumimport random# obtener el centroide de la ciudad y configurar el mapamin_longitud, min_latitud, max_longitud, max_latitud = clustered_gdf_poi.total_boundsm = folium.Map(location=[(min_latitud+max_latitud)/2, (min_longitud+max_longitud)/2], zoom_start=14, tiles='CartoDB Dark_Matter')# obtener colores únicos y aleatorios para cada grupounique_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}# visualizar los poisfor 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]        # crear un marcador de punto     folium.CircleMarker(        location=[lat, lon],        radius=3,         color=color,        fill=True,        fill_color=color,        fill_opacity=0.9,          popup=row['amenity'],     ).add_to(m)# mostrar el mapam
Acumulación de POI hipster - primer nivel de zoom.
Acumulación de POI hipster - segundo nivel de zoom.
Acumulación de POI hipster - tercer nivel de zoom.

4. Comparando las acumulaciones

Cada acumulación cuenta como una acumulación de moda y estilo de vida, sin embargo, todas deben ser únicas de alguna manera, ¿verdad? Vamos a ver qué tan únicas son comparando el portafolio de categorías de POI que cada una ofrece.

Primero, busquemos la diversidad y midamos la variedad/diversidad de categorías de POI en cada acumulación mediante el cálculo de su entropía.

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# crear un diccionario donde cada acumulación tiene su propia lista de amenidadesclusters_amenities = clustered_gdf_poi.groupby(by = 'cluster_id')['amenity'].apply(list).to_dict()# calcular y almacenar los puntajes de entropíaentropy_data = []for cluster, amenities in clusters_amenities.items():    E = get_entropy_score(amenities)    entropy_data.append({'cluster' : cluster, 'tamaño' :len(amenities), 'entropía' : E})    # agregar los puntajes de entropía a un data frameentropy_data = pd.DataFrame(entropy_data)entropy_data

El resultado de esta celda:

La diversidad (entropía) de cada acumulación basada en su perfil de POI.

Y un análisis rápido de correlación en esta tabla:

entropy_data.corr()
La correlación entre las características de las acumulaciones.

Después de calcular la correlación entre el ID de la acumulación, el tamaño de la acumulación y la entropía de la acumulación, hay una correlación significativa entre el tamaño y la entropía; sin embargo, esto está lejos de explicar toda la diversidad. Aparentemente, algunos puntos de moda son más diversos que otros, mientras que otros están algo más especializados. ¿En qué están especializados? Responderé esta pregunta comparando los perfiles de POI de cada acumulación con la distribución general de cada tipo de POI dentro de las acumulaciones y seleccionaré las tres categorías de POI más típicas de una acumulación en comparación con el promedio.

# empaquetar los perfiles de poi en diccionariosclusters = 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()}# calcular la frecuencia relativa de cada categoría# y mantener solo aquellos por encima del promedio (>1) y los 3 principales candidatosclusters_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]    # imprimir, para cada acumulación, sus principales categorías:for cluster, top_amenities in clusters_top_profile.items():    print(cluster, top_amenities)

El resultado de este bloque de código:

La huella única de cada grupo de servicios.

Las descripciones de las principales categorías ya muestran algunas tendencias. Por ejemplo, el grupo 17 está claramente relacionado con bebidas, mientras que el 19 también tiene música, posiblemente relacionada con fiestas. El grupo 91, con librerías, galerías y cafés, es definitivamente un lugar para relajarse durante el día, mientras que el grupo 120, con música y una galería, puede ser un excelente preámbulo para cualquier recorrido por bares. A partir de la distribución, también podemos ver que ir a un bar siempre es apropiado (o, según el caso de uso, debemos pensar en normalizaciones adicionales basadas en la frecuencia de las categorías)!

Conclusiones

Como residente local, puedo confirmar que estos grupos tienen mucho sentido y representan muy bien la mezcla deseada de funcionalidad urbana a pesar de la metodología simple. Por supuesto, este es un piloto rápido que se puede enriquecer y mejorar de varias formas, como:

  • Depender de una categorización y selección de puntos de interés (POI) más detallada
  • Considerar las categorías de POI al realizar la agrupación (agrupación semántica)
  • Enriquecer la información de los POI con, por ejemplo, reseñas y calificaciones de redes sociales

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

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Por qué los científicos se adentran en el mundo virtual

Un número creciente de investigadores científicos están utilizando la tecnología de realidad virtual (VR) en el labor...

Inteligencia Artificial

Las Nuevas Implicaciones Éticas de la Inteligencia Artificial Generativa

El rápido progreso del IA generativa hace necesario implementar urgentemente salvaguardias éticas contra los riesgos ...

Inteligencia Artificial

ChatGPT Plugins Todo lo que necesitas saber

Aprenda más sobre los complementos de terceros que OpenAI ha lanzado para comprender ChatGPTs en uso en el mundo real.

Inteligencia Artificial

Nueva investigación de IA de KAIST presenta FLASK un marco de evaluación de granularidad fina para modelos de lenguaje basado en conjuntos de habilidades

Increíblemente, los LLM han demostrado estar en sintonía con los valores humanos, brindando respuestas útiles, honest...

Inteligencia Artificial

El debate sobre la seguridad de la IA está dividiendo Silicon Valley

El drama de liderazgo de OpenAI es el último estallido en el acalorado debate entre los tecnócratas que buscan la seg...