title

Un estudio de localización inteligente y algoritmos de machine learning para la seleccionar ubicaciones de una cafetería en la ciudad de Buenos Aires

Las cafeterías de Buenos Aires forman parte de la cultura de la ciudad, de las costumbres de sus habitantes y su circuito turístico. Han sido objeto de estudio de distintos escritores, inspiradores de innumerables creaciones artísticas y tradicional encuentro de unión de los porteños. En este proyecto, la idea es encontrar una ubicación óptima para una nueva cafetería, basados en algoritmos de aprendizaje automático tomados del curso "The Battle of Neighborhoods: Coursera Capstone Project" (1). Partiendo de que existe una asociación de las cafeterías con los restaurantes, primero intentaremos detectar ubicaciones en función de la definición de factores que influirán en nuestra decisión:

1- Lugares que aún no están llenos de restaurantes.

2- Áreas con poco o ninguna cafetería cercana.

3- Cerca del centro, si es posible, suponiendo que se cumplan las dos primeras condiciones.

Con estos sencillos parámetros vamos a programar un algoritmo para descubrir que soluciones se pueden obtener.

Fuente de los Datos

Se necesitarán las siguientes fuentes de datos para extraer y generar la información requerida:

1.- Los centros de las áreas candidatas serán generados automáticamente siguiendo el algoritmo y las direcciones aproximadas de los centros de estas áreas se obtendrán usando uno de los paquetes de Geopy Geocoders. (2) 2-El número de restaurantes, su tipo y ubicación en cada vecindario se obtendrá utilizando la API de Foursquare. (3)

Los datos se utilizarán en los siguientes escenarios:

1- Para descubrir la densidad de todos los restaurantes y las cafeterías a partir de los datos extraídos.

2- Para identificar áreas que no están muy densas y no son muy competitivas.

3- Para calcular las distancias entre restaurantes competidores.

Localizar los candidatos

El área objetivo será el centro de la ciudad, donde las atracciones turísticas son + numerosas en comparación con otros lugares. A partir de ello vamos a crear una cuadrícula de celdas que cubra el área de interés la cual sera unos 12x12 Kilómetros centrados alrededor del centro de la ciudad de Buenos Aires.

In [120]:
import requests

from geopy.geocoders import Nominatim


address = 'Sarmiento 999, Buenos Aires, Argentina'
geolocator = Nominatim(user_agent="lyon_explorer")
location = geolocator.geocode(address)
lat = location.latitude
lng = location.longitude
caba_center = [lat, lng]
print('Coordinate of {}: {}'.format(address, lyon_center), ' location : ', location)
Coordinate of Sarmiento 999, Buenos Aires, Argentina: [-34.604886, -58.3807509]  location :  999, Sarmiento, Microcentro, Comuna 1, San Nicolás, Buenos Aires, Ciudad Autónoma de Buenos Aires, 1035, Argentina

Creamos una cuadrícula de las áreas candidatas equidistantes, centradas alrededor del centro de la ciudad y que sea 6 km alrededor de este punto, para ello calculamos las distancias que necesitamos para crear nuestra cuadrícula de ubicaciones en un sistema de coordenadas cartesianas 2D que nos permitirá luego calcular distancias en metros.

A continuación, proyectaremos estas coordenadas en grados de latitud / longitud para que se muestren en los mapas con Mapbox y Folium (3).

In [121]:
#!pip install shapely
import shapely.geometry

#!pip install pyproj
import pyproj

import math

def lonlat_to_xy(lon, lat):
    proj_latlon = pyproj.Proj(proj='latlong',datum='WGS84')
    proj_xy = pyproj.Proj(proj="utm", zone=33, datum='WGS84')
    xy = pyproj.transform(proj_latlon, proj_xy, lon, lat)
    return xy[0], xy[1]

def xy_to_lonlat(x, y):
    proj_latlon = pyproj.Proj(proj='latlong',datum='WGS84')
    proj_xy = pyproj.Proj(proj="utm", zone=33, datum='WGS84')
    lonlat = pyproj.transform(proj_xy, proj_latlon, x, y)
    return lonlat[0], lonlat[1]

def calc_xy_distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    return math.sqrt(dx*dx + dy*dy)

print('Verificación de las Coordenadas')
print('-------------------------------')
print('Centro de Buenos Aires longitud={}, latitud={}'.format(lyon_center[1], lyon_center[0]))
x, y = lonlat_to_xy(lyon_center[1], lyon_center[0])
print('Centro de Buenos Aires UTM X={}, Y={}'.format(x, y))
lo, la = xy_to_lonlat(x, y)
print('Centro de Buenos Aires longitud={}, latitud={}'.format(lo, la))
Verificación de las Coordenadas
-------------------------------
Centro de Buenos Aires longitud=-58.3807509, latitud=-34.604886
Centro de Buenos Aires UTM X=-6310652.811608684, Y=-7497891.359396329
Centro de Buenos Aires longitud=-58.38075090000346, latitud=-34.604885999999645

Creamos una cuadrícula hexagonal de celdas: desplazamos todas las líneas y ajustamos el espaciado de las líneas verticales para que cada centro de celda sea equidistante de todos sus vecinos.

In [122]:
caba_center_x, caba_center_y = lonlat_to_xy(caba_center[1], caba_center[0]) # City center in Cartesian coordinates

k = math.sqrt(3) / 2 # Vertical offset for hexagonal grid cells
x_min = caba_center_x - 6000
x_step = 600
y_min = caba_center_y - 6000 - (int(21/k)*k*600 - 12000)/2
y_step = 600 * k 

latitudes = []
longitudes = []
distances_from_center = []
xs = []
ys = []
for i in range(0, int(21/k)):
    y = y_min + i * y_step
    x_offset = 300 if i%2==0 else 0
    for j in range(0, 21):
        x = x_min + j * x_step + x_offset
        distance_from_center = calc_xy_distance(caba_center_x, caba_center_y, x, y)
        if (distance_from_center <= 6001):
            lon, lat = xy_to_lonlat(x, y)
            latitudes.append(lat)
            longitudes.append(lon)
            distances_from_center.append(distance_from_center)
            xs.append(x)
            ys.append(y)

print(len(latitudes), 'Grilla del centro de la Ciudad de Buenos Aires - CABA')
364 Grilla del centro de la Ciudad de Buenos Aires - CABA

Veamos los datos que tenemos hasta ahora: ubicación en el centro y los centros vecinales candidatos:

In [123]:
import folium
In [124]:
tileset = r'https://api.mapbox.com'
attribution = (r'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a>'
                ' contributors, Imagery © <a href="http://mapbox.com">MapBox</a>')

map_caba = folium.Map(location=caba_center, zoom_start=14, tiles=tileset, attr=attribution)
folium.Marker(caba_center, popup='Ciudad de Buenos Aires').add_to(map_caba)
for lat, lon in zip(latitudes, longitudes):
    #folium.CircleMarker([lat, lon], radius=2, color='blue', fill=True, fill_color='blue', fill_opacity=1).add_to(map_lyon) 
    folium.Circle([lat, lon], radius=200, color='purple', fill=False).add_to(map_caba)
    #folium.Marker([lat, lon]).add_to(map_caba)
map_caba
Out[124]:

En este punto, ahora tenemos las coordenadas de los centros / áreas locales a evaluar, a la misma distancia (la distancia entre cada punto y sus vecinos es exactamente la misma) y aproximadamente a 4 km del centro de Buenos Aires.

In [125]:
def get_address(lat, lng):
    #print('entering get address')
    try:
        #address = '{},{}'.format(lat, lng)
        address = [lat, lng]
        geolocator = Nominatim(user_agent="lyon_explorer")
        location = geolocator.geocode(address)
        #print(location[0])
        return location[0]
    except:
        return 'nothing found'


addr = get_address(lyon_center[0], lyon_center[1])
print('Reverse geocoding check')
print('-----------------------')
print('Address of [{}, {}] is: {}'.format(caba_center[0], caba_center[1], addr)) 
print(type(location[0]))
Reverse geocoding check
-----------------------
Address of [-34.604886, -58.3807509] is: nothing found
<class 'str'>
In [126]:
print('Obteniendo Ubicaciones: ', end='')
addresses = []
for lat, lon in zip(latitudes, longitudes):
    address = get_address(lat, lon)
    if address is None:
        address = 'NO ADDRESS'
    address = address.replace(', Argentina', '') 
    addresses.append(address)
    print(' .', end='')
print(' done.')
Obteniendo Ubicaciones:  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . done.
In [127]:
import pandas as pd

df_locations = pd.DataFrame({'Dirección': addresses,
                             'Latitud': latitudes,
                             'Longitud': longitudes,
                             'X': xs,
                             'Y': ys,
                             'Distancia desde el centroide': distances_from_center})

df_locations.head()
Out[127]:
Dirección Latitud Longitud X Y Distancia desde el centroide
0 McDonald's, Avenida Rivadavia, Comuna 5, Almag... -34.610689 -58.420397 -6.312453e+06 -7.503607e+06 5992.495307
1 Estacionamiento José, Colombres, Comuna 5, Alm... -34.613645 -58.418537 -6.311853e+06 -7.503607e+06 5840.376700
2 nothing found -34.616602 -58.416676 -6.311253e+06 -7.503607e+06 5747.173218
3 Campus Deportes, México, Comuna 5, Almagro, Bu... -34.619559 -58.414815 -6.310653e+06 -7.503607e+06 5715.767665
4 3302, Estados Unidos, Comuna 5, Boedo, Buenos ... -34.622517 -58.412953 -6.310053e+06 -7.503607e+06 5747.173218
In [128]:
df_locations.shape
Out[128]:
(364, 6)
In [129]:
df_locations.to_pickle('./Dataset/caba_locations.pkl')    

Foursquare

Ahora vamos a utilizar el API de Foursquare para explorar la cantidad de restaurantes disponibles dentro de estas cuadriculas y limitaremos la búsqueda a las categorías de alimentos para recuperar los datos de latitud y longitud de los restaurantes y de las cafeterías.

In [130]:
client_id = 'xxxxx'
client_secret = 'xxxxx'
VERSION = 'xxxxx'

Utilizamos la API de Foursquare para explorar la cantidad de restaurantes disponibles dentro de los 4 km del centro de Buenos Aires y limitar la búsqueda a todos los locales asociados con la categoría de restaurantes y especialmente aquellos que corresponden a las Cafeterías.

In [131]:
food_category = '4d4b7105d754a06374d81259' # 'Food' Catégorie de restaurant

caba_cafe_categories = ['4bf58dd8d48988d143941735', '4bf58dd8d48988d128941735', '4bf58dd8d48988d16d941735', 
                              '4bf58dd8d48988d1e0931735', '4bf58dd8d48988d147941735'] # 'Food' Catégorie de restaurants cafe
In [135]:
def is_restaurant(categories, specific_filter=None):
    restaurant_words = ['restaurant']
    restaurant = False
    specific = False
    for c in categories:
        category_name = c[0].lower()
        category_id = c[1]
        for r in restaurant_words:
            if r in category_name:
                restaurant = True
        if 'Restaurante' in category_name:
            restaurant = False
        if not(specific_filter is None) and (category_id in specific_filter):
            specific = True
            restaurant = True
    return restaurant, specific

def get_categories(categories):
    return [(cat['name'], cat['id']) for cat in categories]

def format_address(location):
    address = ', '.join(location['formattedAddress'])
    address = address.replace(', Argentina', '')
    address = address.replace(', Argentina', '')
    return address

def get_venues_near_location(lat, lon, category, client_id, client_secret, radius=500, limit=1000):
    version = '20180724'
    url = 'https://api.foursquare.com/v2/venues/explore?client_id={}&client_secret={}&v={}&ll={},{}&categoryId={}&radius={}&limit={}'.format(
        client_id, client_secret, version, lat, lon, category, radius, limit)
    try:
        results = requests.get(url).json()['response']['groups'][0]['items']
        venues = [(item['venue']['id'],
                   item['venue']['name'],
                   get_categories(item['venue']['categories']),
                   (item['venue']['location']['lat'], item['venue']['location']['lng']),
                   format_address(item['venue']['location']),
                   item['venue']['location']['distance']) for item in results]        
    except:
        venues = []
    return venues
In [136]:
# Passons maintenant aux emplacements de nos quartiers et trouvons des restaurants à proximité. nous allons également maintenir un dictionnaire de tous les restaurants trouvés et de tous les restaurants français trouvés

import pickle

def get_restaurants(lats, lons):
    restaurants = {}
    caba_cafe = {}
    location_restaurants = []

    print('Obtención de los candidatos', end='')
    for lat, lon in zip(lats, lons):
        # En utilisant rayon = 350 mts, assurez-vous que nous avons des recouvrements / une couverture complète afin que nous ne manquions aucun restaurant (nous utilisons des dictionnaires pour supprimer tout doublon résultant de chevauchements de zones).
        venues = get_venues_near_location(lat, lon, food_category, client_id, client_secret, radius=350, limit=100)
        area_restaurants = []
        for venue in venues:
            venue_id = venue[0]
            venue_name = venue[1]
            venue_categories = venue[2]
            venue_latlon = venue[3]
            venue_address = venue[4]
            venue_distance = venue[5]
            is_res, is_cafe = is_restaurant(venue_categories, specific_filter=caba_cafe_categories)
            if is_res:
                x, y = lonlat_to_xy(venue_latlon[1], venue_latlon[0])
                restaurant = (venue_id, venue_name, venue_latlon[0], venue_latlon[1], venue_address, venue_distance, is_cafe, x, y)
                if venue_distance<=300:
                    area_restaurants.append(restaurant)
                restaurants[venue_id] = restaurant
                if is_cafe:
                    caba_cafe[venue_id] = restaurant
        location_restaurants.append(area_restaurants)
        print(' .', end='')
    print(' done.')
    return restaurants, caba_cafe, location_restaurants

# Essayez de charger à partir du système de fichiers local au cas où nous l'avions déjà fait
restaurants = {}
caba_cafe = {}
location_restaurants = []
loaded = False
try:
    with open('/Dataset/restaurants_350.pkl', 'rb') as f:
        restaurants = pickle.load(f)
        print('Restaurant data loaded.')
    with open('/Dataset/caba_cafe_350.pkl', 'rb') as f:
        caba_cafe = pickle.load(f)
        print('Descargando Datos de las Cafeterías')
    with open('/Dataset/location_restaurants_350.pkl', 'rb') as f:
        location_restaurants = pickle.load(f)
        print('Descargando datos de Restaurantes de la Ciudad de Buenos Aires')
    loaded = True
except:
    print('Datos de Restaurantes Descargandose')
    pass

# Si le chargement échoue, utilisez l'API Foursquare pour obtenir les données.
if not loaded:
    restaurants, caba_cafe, location_restaurants = get_restaurants(latitudes, longitudes)
    
Datos de Restaurantes Descargandose
Obetención de los candidatos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . done.
In [137]:
print('**Resultados**',)
print('Número Total de Restaurantes:', len(restaurants))
print('Número Total de Cafeterías:', len(caba_cafe))
print('Porcentaje de Cafeterías: {:.2f}%'.format(len(caba_cafe) / len(restaurants) * 100))
print('Promedio de locales por cuadrícula:', np.array([len(r) for r in location_restaurants]).mean())
**Resultados**
Número Total de Restaurantes: 1155
Número Total de Cafeterías: 373
Porcentaje de Cafeterías: 32.29%
Promedio de locales por cuadrícula: 6.271978021978022
In [138]:
print('Lista de Todos los Restaurantes')
print('-----------------------')
for r in list(restaurants.values())[:10]:
    print(r)
print('...')
print('Total:', len(restaurants))
Lista de Todos los Restaurantes
-----------------------
('4c2e94b316adc9286f34bc9c', 'El Mirasol de Boedo', -34.61293641469864, -58.41752838470169, 'Av. Boedo 136 (e/ Hipólito Yrigoyen y Don Bosco), C1206AAO Buenos Aires, Buenos Aires C.F.', 292, False, -6311896.774475244, -7503415.082999121)
('4da86f0d6e81162ae7a64092', 'Restaurante Centro Navarro', -34.614651020377565, -58.418541297788536, 'Moreno 3682 (esq. Colombres), C1209ABR Almagro, Buenos Aires C.F.', 276, False, -6311692.551610563, -7503691.280318653)
('4c645ed558810f47e7060c1e', 'Havanna', -34.611218813206236, -58.42009297983823, 'Av. Rivadavia 3826 (entre Colombres y Av. Castro Barros), 1204 Buenos Aires, Buenos Aires C.F.', 343, True, -6312347.25624532, -7503610.973063783)
('513c9faee4b05d5c33103869', 'La Juvenil', -34.61113014375674, -58.419127541711426, 'Av. Rivadavia 3733 (Salguero), Ciudad de Buenos Aires, Buenos Aires', 254, False, -6312295.090981655, -7503476.13011793)
('4b92d17af964a520a11e34e3', 'Murillo', -34.61095445797399, -58.41975486603663, 'Rivadavia 3801, Buenos Aires, Buenos Aires C.F.', 310, False, -6312366.246228285, -7503544.36257964)
('4fd8e404e4b05b85928ae7c0', 'Resto Bar Renacer', -34.614693774388925, -58.42068593840626, 'Castro Barros 209, Buenos Aires, Buenos Aires C.F.', 228, True, -6311833.035241403, -7503977.981550891)
('52828799498eca810e8e6da8', 'Toto Chinese Food', -34.61130035231146, -58.41965142183581, 'Av. Rivadavia 3740, Buenos Aires, Buenos Aires C.F.', 305, False, -6312303.899540054, -7503559.445443048)
('4c37e2b41e06d13a94e2763e', "McDonald's", -34.61126319080611, -58.42041074174844, 'Av. Rivadavia 3855 (e/ Av. Medrano y Jerónimo Salguero), Almagro, Buenos Aires C.F.', 315, False, -6312361.998662292, -7503656.617798919)
('4ba632d3f964a520e33a39e3', 'Spiagge di Napoli', -34.620914957548074, -58.415537077104005, 'Av. Independencia 3527 (e/ Maza y Av. Boedo), C1226AAA Buenos Aires, Buenos Aires C.F.', 296, False, -6310485.997918164, -7503815.114228333)
('5186c6b6498e8f5d0966b9cc', 'El Santo', -34.621254259644445, -58.416666984558105, 'Av. Boedo 709 (e/ Av. Independencia y Estados Unidos), C1218AAD Buenos Aires, Buenos Aires C.F.', 253, True, -6310509.419835732, -7503992.442108145)
...
Total: 1155
In [139]:
print('Lista de todas las Cafeterías')
print('---------------------------')
for r in list(caba_cafe.values())[:10]:
    print(r)
print('...')
print('Total:', len(caba_cafe))
Lista de todas las Cafeterías
---------------------------
('4c645ed558810f47e7060c1e', 'Havanna', -34.611218813206236, -58.42009297983823, 'Av. Rivadavia 3826 (entre Colombres y Av. Castro Barros), 1204 Buenos Aires, Buenos Aires C.F.', 343, True, -6312347.25624532, -7503610.973063783)
('4fd8e404e4b05b85928ae7c0', 'Resto Bar Renacer', -34.614693774388925, -58.42068593840626, 'Castro Barros 209, Buenos Aires, Buenos Aires C.F.', 228, True, -6311833.035241403, -7503977.981550891)
('5186c6b6498e8f5d0966b9cc', 'El Santo', -34.621254259644445, -58.416666984558105, 'Av. Boedo 709 (e/ Av. Independencia y Estados Unidos), C1218AAD Buenos Aires, Buenos Aires C.F.', 253, True, -6310509.419835732, -7503992.442108145)
('5657821c498e715f5fd19080', 'Café Martínez', -34.62275959638465, -58.41639876365661, 'Av. Boedo 819 (esq. Estados Unidos), C1218 Boedo, Buenos Aires C.F.', 316, True, -6310250.649497023, -7504082.086374279)
('597fa357da5e566b24671963', 'Havanna', -34.622437341842925, -58.416645526885986, 'Av. Boedo 787 (esq. Estados Unidos), C1218AAD Buenos Aires, Buenos Aires C.F.', 338, True, -6310319.047208579, -7504087.887739555)
('4f18403fe4b0a0515c30584e', 'Vip 70', -34.62459546828437, -58.40912551603048, 'Av. San Juan 3119 (e/ Gral. Urquiza y 24 de Noviembre), C1233ABB Buenos Aires, Buenos Aires C.F.', 220, True, -6309458.055003345, -7503274.717871552)
('4db9e2534df0cac21ae29cd7', 'Tinta Roja', -34.625365177914226, -58.4073520210512, 'Urquiza 1248 (Barcalá), Buenos Aires, Buenos Aires C.F.', 38, True, -6309213.374075161, -7503104.646196396)
('4d46cceec3e5f04db585b120', 'Bar Urquiza y Pavon', -34.62825278366537, -58.40851112853827, 'Urquiza (Pavon)', 305, True, -6308832.046561152, -7503497.50376062)
('4c0bf6656071a593c90ce232', 'Café Bar La Orquídea', -34.6029183297825, -58.42255847332825, 'Av. Corrientes 4101 (esq. Francisco Acuña de Figueroa), C1195AAA Buenos Aires, Buenos Aires C.F.', 317, True, -6313842.388551223, -7503246.783666756)
('51d038c2498ec2e46980aab6', 'El Símbolo', -34.60343049846772, -58.41782167553902, 'Av. Corrientes 3789 (e/ Bulnes y Mario Bravo), C1194AAD Buenos Aires, Buenos Aires C.F.', 243, True, -6313435.039264028, -7502663.773385202)
...
Total: 373
In [140]:
print('Restaurantes de Autor')
print('---------------------------')
for i in range(100, 110):
    rs = location_restaurants[i][:8]
    names = ', '.join([r[1] for r in rs])
    print('Restaurants around location {}: {}'.format(i+1, names))
Restaurantes de Autor
---------------------------
Restaurants around location 101: Helueni - Delicias Árabes Orientales, Piacere, Havanna, Havanna, Tienda de Café, Bar San Martín
Restaurants around location 102: Helueni - Delicias Árabes Orientales, Doner Kebab, Havanna, Bar San Martín
Restaurants around location 103: Bi Won, Doner Kebab, Havanna, El Bodegon de la calle Ayacucho, Rotisería China Diamante, Maral, Via mont, Yushan Da Jiulou
Restaurants around location 104: Shogun, Sho Gun, Café Due, Cervantes II, Green Life, Havanna, Prosciutto, El Bodegon de la calle Ayacucho
Restaurants around location 105: Café de los Angelitos, La Gran Taberna, Cervantes II, Café Martínez, Bellagamba Bodegón, Havanna, Punta Cuore, Eloisa
Restaurants around location 106: La Gran Taberna, O'Toxo Restaurant, Centro Lalin, Ebro, Victoria, Casablanca, Havanna, Punta Cuore
Restaurants around location 107: Centro Asturiano, Lo Rafael, O'Toxo Restaurant, Ebro, Spiga
Restaurants around location 108: Lo Rafael, Bar Gijón, Gran Café Gardel, Bar Mágico, Balcarce
Restaurants around location 109: Vivaldi Libros Bar, Bar Mágico, Café San José, Mandiyu
Restaurants around location 110: Les Anciens Combattants, Vivaldi Libros Bar, Les ancient combatants, Mandiyu

Todos los restaurantes en la ciudad de Buenos Aires están indicados en gris y aquellos asociados a cafeterías los vamos a resaltar en rojo.

In [141]:
tileset = r'https://api.mapbox.com/styles/v1/roqueleal08/cjyaey84d07zq1crze5r08yg1/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1Ijoicm9xdWVsZWFsMDgiLCJhIjoiY2ppZzl5NWo2MTVmMTNrcGU0enR0ZTU2MyJ9.4ZWYdzUlqvIQwwSIR50xZA'
attribution = (r'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a>'
                ' contributors, Imagery © <a href="http://mapbox.com">MapBox</a>')

map_caba = folium.Map(location=caba_center, zoom_start=13, tiles=tileset, attr=attribution)
folium.Marker(caba_center, popup='Buenos Aires').add_to(map_caba)
for res in restaurants.values():
    lat = res[2]; lon = res[3]
    is_cafe = res[6]
    color = 'red' if is_cafe else 'grey'
    folium.CircleMarker([lat, lon], radius=3, color=color, fill=True, fill_color=color, fill_opacity=1).add_to(map_caba)
map_caba
Out[141]:

Análisis

Vamos a explicar los datos y la información adicional de ellos en bruto. número de restaurantes en cada área candidata:

In [142]:
location_restaurants_count = [len(res) for res in location_restaurants]

df_locations['Restaurantes en las zonas'] = location_restaurants_count

print('Promedio de restaurantes desde el centro de Buenos Aires a un rango de 300m:', np.array(location_restaurants_count).mean())

df_locations.head(10)
Promedio de restaurantes desde el centro de Buenos Aires a un rango de 300m: 6.271978021978022
Out[142]:
Dirección Latitud Longitud X Y Distancia desde el centroide Restaurantes en las zonas
0 McDonald's, Avenida Rivadavia, Comuna 5, Almag... -34.610689 -58.420397 -6.312453e+06 -7.503607e+06 5992.495307 0
1 Estacionamiento José, Colombres, Comuna 5, Alm... -34.613645 -58.418537 -6.311853e+06 -7.503607e+06 5840.376700 5
2 nothing found -34.616602 -58.416676 -6.311253e+06 -7.503607e+06 5747.173218 1
3 Campus Deportes, México, Comuna 5, Almagro, Bu... -34.619559 -58.414815 -6.310653e+06 -7.503607e+06 5715.767665 3
4 3302, Estados Unidos, Comuna 5, Boedo, Buenos ... -34.622517 -58.412953 -6.310053e+06 -7.503607e+06 5747.173218 1
5 3202, Avenida San Juan, Comuna 5, San Cristóba... -34.625474 -58.411091 -6.309453e+06 -7.503607e+06 5840.376700 1
6 3101, Avenida Pavón, Comuna 5, Boedo, Buenos A... -34.628432 -58.409229 -6.308853e+06 -7.503607e+06 5992.495307 2
7 nothing found -34.604922 -58.420092 -6.313353e+06 -7.503088e+06 5855.766389 8
8 Astesiano, Avenida Díaz Vélez, Comuna 5, Almag... -34.607878 -58.418231 -6.312753e+06 -7.503088e+06 5604.462508 5
9 Alakoa, Avenida Rivadavia, Comuna 5, Almagro, ... -34.610835 -58.416370 -6.312153e+06 -7.503088e+06 5408.326913 7

Ahora calculamos la distancia de la cafetería + cercana a cada cuadricula (no sólo aquellos ubicados a menos de 300 m de distancia, ya que deseamos conocer tambien la distancia al centro + cercano.

In [160]:
distances_to_caba_cafe = []

for area_x, area_y in zip(xs, ys):
    min_distance = 100
    for res in caba_cafe.values():
        res_x = res[7]
        res_y = res[8]
        d = calc_xy_distance(area_x, area_y, res_x, res_y)
        if d<min_distance:
            min_distance = d
    distances_to_caba_cafe.append(min_distance)

df_locations['Distancias a la Cafetería'] = distances_to_caba_cafe
In [161]:
df_locations.head(10)
Out[161]:
Dirección Latitud Longitud X Y Distancia desde el centroide Restaurantes en las zonas Distancias a la Cafetería
0 McDonald's, Avenida Rivadavia, Comuna 5, Almag... -34.610689 -58.420397 -6.312453e+06 -7.503607e+06 5992.495307 0 100.0
1 Estacionamiento José, Colombres, Comuna 5, Alm... -34.613645 -58.418537 -6.311853e+06 -7.503607e+06 5840.376700 5 100.0
2 nothing found -34.616602 -58.416676 -6.311253e+06 -7.503607e+06 5747.173218 1 100.0
3 Campus Deportes, México, Comuna 5, Almagro, Bu... -34.619559 -58.414815 -6.310653e+06 -7.503607e+06 5715.767665 3 100.0
4 3302, Estados Unidos, Comuna 5, Boedo, Buenos ... -34.622517 -58.412953 -6.310053e+06 -7.503607e+06 5747.173218 1 100.0
5 3202, Avenida San Juan, Comuna 5, San Cristóba... -34.625474 -58.411091 -6.309453e+06 -7.503607e+06 5840.376700 1 100.0
6 3101, Avenida Pavón, Comuna 5, Boedo, Buenos A... -34.628432 -58.409229 -6.308853e+06 -7.503607e+06 5992.495307 2 100.0
7 nothing found -34.604922 -58.420092 -6.313353e+06 -7.503088e+06 5855.766389 8 100.0
8 Astesiano, Avenida Díaz Vélez, Comuna 5, Almag... -34.607878 -58.418231 -6.312753e+06 -7.503088e+06 5604.462508 5 100.0
9 Alakoa, Avenida Rivadavia, Comuna 5, Almagro, ... -34.610835 -58.416370 -6.312153e+06 -7.503088e+06 5408.326913 7 100.0
In [162]:
print('Distancia promedio en metros desde la Cafetería + cercana a cada centro:', df_locations['Distancias a la Cafetería'].mean())
Distancia promedio en metros desde la Cafetería + cercana a cada centro: 97.68981798608365

Utilizamos HeatMap con Mapbox para visualizar la densidad de restaurantes en el radio seleccionado desde el centro de Buenos Aires.

In [163]:
restaurant_latlons = [[res[2], res[3]] for res in restaurants.values()]

cafe_latlons = [[res[2], res[3]] for res in caba_cafe.values()]
In [164]:
from folium import plugins
from folium.plugins import HeatMap


tileset = r'https://api.mapbox.com/styles/v1/roqueleal08/cjyaey84d07zq1crze5r08yg1/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1Ijoicm9xdWVsZWFsMDgiLCJhIjoiY2ppZzl5NWo2MTVmMTNrcGU0enR0ZTU2MyJ9.4ZWYdzUlqvIQwwSIR50xZA'
attribution = (r'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a>'
                ' contributors, Imagery © <a href="http://mapbox.com">MapBox</a>')

map_caba = folium.Map(location=caba_center, zoom_start=13, tiles=tileset, attr=attribution)
HeatMap(restaurant_latlons).add_to(map_caba)
folium.Marker(caba_center).add_to(map_caba)
folium.Circle(caba_center, radius=1000, fill=False, color='white').add_to(map_caba)
folium.Circle(caba_center, radius=2000, fill=False, color='blue').add_to(map_caba)
folium.Circle(caba_center, radius=3000, fill=False, color='red').add_to(map_caba)
map_lyon
Out[164]:

Ahora presentamos otra visualización con un Heatmap de sólo las cafeterías

In [165]:
map_caba = folium.Map(location=caba_center, zoom_start=13, tiles=tileset, attr=attribution)
HeatMap(cafe_latlons).add_to(map_caba)
folium.Marker(caba_center).add_to(map_caba)
folium.Circle(caba_center, radius=1000, fill=False, color='white').add_to(map_caba)
folium.Circle(caba_center, radius=2000, fill=False, color='blue').add_to(map_caba)
folium.Circle(caba_center, radius=3000, fill=False, color='red').add_to(map_caba)
map_caba
Out[165]:

De los mapas anteriores, se descubrimos que la mayoría de los restaurantes están dispersos en el lado norte del centro del área en estudio. Vamos a enfocarnos en las áreas con una menor densidad para localizar los candidatos.

In [166]:
roi_x_min = caba_center_x - 2000
roi_y_max = caba_center_y + 1000
roi_width = 5000
roi_height = 5000
roi_center_x = roi_x_min + 1900
roi_center_y = roi_y_max - 700
roi_center_lon, roi_center_lat = xy_to_lonlat(roi_center_x, roi_center_y)
roi_center = [roi_center_lat, roi_center_lon]
map_caba = folium.Map(location=caba_center, zoom_start=13, tiles=tileset, attr=attribution)
HeatMap(restaurant_latlons).add_to(map_caba)
folium.Marker(caba_center).add_to(map_caba)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.4).add_to(map_caba)
map_caba
Out[166]:

Construimos nuevamente una grilla para ubicar los candidatos y las principales atracciones turísticas.

In [167]:
k = math.sqrt(3) / 2 
x_step = 100
y_step = 100 * k 
roi_y_min = roi_center_y - 2500

roi_latitudes = []
roi_longitudes = []
roi_xs = []
roi_ys = []
for i in range(0, int(51/k)):
    y = roi_y_min + i * y_step
    x_offset = 50 if i%2==0 else 0
    for j in range(0, 51):
        x = roi_x_min + j * x_step + x_offset
        d = calc_xy_distance(roi_center_x, roi_center_y, x, y)
        if (d <= 2501):
            lon, lat = xy_to_lonlat(x, y)
            roi_latitudes.append(lat)
            roi_longitudes.append(lon)
            roi_xs.append(x)
            roi_ys.append(y)

print(len(roi_latitudes), 'Localidades con posibles candidatos.')
2120 Localidades con posibles candidatos.

Calculamos dos cosas + importantes para cada ubicación candidata: el número de restaurantes cercanos (utilizaremos un radio de 250 metros) y la distancia a la cafetería + cercana.

In [168]:
def count_restaurants_nearby(x, y, restaurants, radius=250):    
    count = 0
    for res in restaurants.values():
        res_x = res[7]; res_y = res[8]
        d = calc_xy_distance(x, y, res_x, res_y)
        if d<=radius:
            count += 1
    return count

def find_nearest_restaurant(x, y, restaurants):
    d_min = 100000
    for res in restaurants.values():
        res_x = res[7]; res_y = res[8]
        d = calc_xy_distance(x, y, res_x, res_y)
        if d<=d_min:
            d_min = d
    return d_min

roi_restaurant_counts = []
roi_cafe_distances = []

print('Generando los datos de los posibles candidatos... ', end='')
for x, y in zip(roi_xs, roi_ys):
    count = count_restaurants_nearby(x, y, restaurants, radius=250)
    roi_restaurant_counts.append(count)
    distance = find_nearest_restaurant(x, y, caba_cafe)
    roi_cafe_distances.append(distance)
print('done.')
Generando los datos de los posibles candidatos... done.
In [171]:
df_roi_locations = pd.DataFrame({'Latitud':roi_latitudes,
                                 'Longitud':roi_longitudes,
                                 'X':roi_xs,
                                 'Y':roi_ys,
                                 'Restaurantes Cercanos':roi_restaurant_counts,
                                 'Distancia  a las Cafeterías cercanas':roi_cafe_distances})


df_roi_locations.sort_values(by=['Restaurantes Cercanos'], ascending=False, inplace=True)

df_roi_locations.head(5)
Out[171]:
Latitud Longitud X Y Restaurantes Cercanos Distancia a las Cafeterías cercanas
1043 -34.615885 -58.372105 -6.308303e+06 -7.497666e+06 19 157.509071
1087 -34.615416 -58.371745 -6.308353e+06 -7.497580e+06 18 184.053404
1016 -34.602584 -58.380499 -6.311003e+06 -7.497666e+06 17 66.727514
1088 -34.615909 -58.371434 -6.308253e+06 -7.497580e+06 17 141.004460
1623 -34.598483 -58.373899 -6.311203e+06 -7.496454e+06 17 51.739216
In [172]:
df_roi_locations.shape
Out[172]:
(2120, 6)

Ahora vamos a filtrar estos lugares: sólo estamos interesados en ubicaciones con no + de dos restaurantes dentro de un radio de 250 metros y sin nunguna cafetería dentro de un perímetro de 400 metros.

In [174]:
good_res_count = np.array((df_roi_locations['Restaurantes Cercanos']<=2))
print('Lugares con no + de dos restaurantes cercanos:', good_res_count.sum())

good_ind_distance = np.array(df_roi_locations['Distancia  a las Cafeterías cercanas']>=400)
print('Cuadriculas sin Cafeterías a menos de 400 m.:', good_ind_distance.sum())

good_locations = np.logical_and(good_res_count, good_ind_distance)
print('Lugares con ambas condiciones cumplidas:', good_locations.sum())

df_good_locations = df_roi_locations[good_locations]
Lugares con no + de dos restaurantes cercanos: 653
Cuadriculas sin Cafeterías a menos de 400 m.: 205
Lugares con ambas condiciones cumplidas: 138
In [177]:
good_latitudes = df_good_locations['Latitud'].values
good_longitudes = df_good_locations['Longitud'].values

good_locations = [[lat, lon] for lat, lon in zip(good_latitudes, good_longitudes)]
map_caba = folium.Map(location=caba_center, zoom_start=14, tiles=tileset, attr=attribution)
HeatMap(restaurant_latlons).add_to(map_caba)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.6).add_to(map_lyon)
folium.Marker(caba_center).add_to(map_caba)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='purple', fill=True, fill_color='blue', fill_opacity=1).add_to(map_caba) 
map_caba
Out[177]:
In [180]:
map_caba = folium.Map(location=caba_center, zoom_start=14, tiles=tileset, attr=attribution)
HeatMap(good_locations, radius=25).add_to(map_caba)
folium.Marker(caba_center).add_to(map_caba)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='purple', fill=True, fill_color='blue', fill_opacity=1).add_to(map_caba)
map_caba
Out[180]:

Ahora vamos agrupar estas ubicaciones utilizando un algoritmo de machine learning en este caso K-medias para crear 6 grupos que contengan buenas ubicaciones. Estas áreas, sus centros y direcciones serán el resultado final de nuestro análisis.

In [188]:
from sklearn.cluster import KMeans

number_of_clusters = 6

good_xys = df_good_locations[['X', 'Y']].values
kmeans = KMeans(n_clusters=number_of_clusters, random_state=0).fit(good_xys)

cluster_centers = [xy_to_lonlat(cc[0], cc[1]) for cc in kmeans.cluster_centers_]

map_caba = folium.Map(location=caba_center, zoom_start=14, tiles=tileset, attr=attribution)
HeatMap(restaurant_latlons).add_to(map_caba)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.4).add_to(map_caba)
folium.Marker(caba_center).add_to(map_lyon)
for lon, lat in cluster_centers:
    folium.Circle([lat, lon], radius=500, color='gray', fill=True, fill_opacity=0.25).add_to(map_caba) 
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='purple', fill=True, fill_color='blue', fill_opacity=1).add_to(map_caba)
map_caba
Out[188]:

Miremos estas áreas al oeste y al sur de la ciudad con un Heatmap, usando áreas sombreadas para indicar los 6 grupos creados:

In [189]:
map_caba = folium.Map(location=caba_center, zoom_start=14, tiles=tileset, attr=attribution)
folium.Marker(caba_center).add_to(map_caba)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.Circle([lat, lon], radius=250, color='#00000000', fill=True, fill_color='#0066ff', fill_opacity=0.07).add_to(map_caba)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='purple', fill=True, fill_color='blue', fill_opacity=1).add_to(map_caba)
for lon, lat in cluster_centers:
    folium.Circle([lat, lon], radius=500, color='white', fill=False).add_to(map_caba) 
map_caba
Out[189]:

Ahora vamos a listar las ubicaciones candidatas

In [193]:
candidate_area_addresses = []
print('==============================================================')
print('Direcciones de las localizaciones recomendadas')
print('==============================================================\n')
for lon, lat in cluster_centers:
    addr = get_address(lat, lon)
    addr = addr.replace(', Argentina', '')
    addr = addr.replace(', Buenos Aires', '')
    addr = addr.replace(', AR', '')
    addr = addr.replace(', BA', '')
    addr = addr.replace("'", '')
    candidate_area_addresses.append(addr)    
    x, y = lonlat_to_xy(lon, lat)
    d = calc_xy_distance(x, y, caba_center_x, caba_center_y)
    print('{}{} => {:.1f}km del centro la ciudad de Buenos Aires'.format(addr, ' '*(50-len(addr)), d/1000))
    
==============================================================
Direcciones de las localizaciones recomendadas
==============================================================

801, México, Microcentro, Comuna 1, Monserrat, Ciudad Autónoma de Buenos Aires, C1042AAB => 1.9km del centro la ciudad de Buenos Aires
Dársena Norte, Cecilia Grierson, Microcentro, Comuna 1, Retiro, Ciudad Autónoma de Buenos Aires, C1107CCC => 2.5km del centro la ciudad de Buenos Aires
302, Trinidad Guevara, Microcentro, Comuna 1, Puerto Madero, Ciudad Autónoma de Buenos Aires, C1107BGA => 2.5km del centro la ciudad de Buenos Aires
Escuela Infantil Diálogos, Moreno, Microcentro, Comuna 1, Monserrat, Ciudad Autónoma de Buenos Aires, 1091 => 1.4km del centro la ciudad de Buenos Aires
Paseo del Bajo, Microcentro, Comuna 1, Puerto Madero, Ciudad Autónoma de Buenos Aires, C1107AAV => 2.2km del centro la ciudad de Buenos Aires
1600, Moreno, Microcentro, Comuna 1, Monserrat, Ciudad Autónoma de Buenos Aires, C1093ABE => 1.8km del centro la ciudad de Buenos Aires

Resultados

In [198]:
map_caba = folium.Map(location=caba_center, zoom_start=14, tiles=tileset, attr=attribution)
folium.Circle(caba_center, radius=50, color='red', fill=True, fill_color='red', fill_opacity=1).add_to(map_caba)
for lonlat, addr in zip(cluster_centers, candidate_area_addresses):
    folium.Marker([lonlat[1], lonlat[0]], popup=addr).add_to(map_caba)     
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.Circle([lat, lon], radius=250, color='#0000ff00', fill=True, fill_color='#0066ff', fill_opacity=0.05).add_to(map_caba)
map_caba
Out[198]:

Las ubicaciones anteriores están bastante cerca del centro de Buenos Aires y cada una de estas ubicaciones no tiene + de dos restaurantes en un radio de 250 m ninguna cafetería a 400 m. Cualquiera de estos establecimientos es un candidato potencial para la nueva cafetería, al menos considerando la competencia cercana. El algoritmo de aprendizaje no supervisado K-medias nos ha permitido agrupar las 15 ubicaciones con una elección adecuada para que las partes interesadas elijan entre los resultados que se presentan a continuación.

Las conclusiones

El objetivo de este proyecto era identificar las áreas de Buenos Aires cercanas al centro, con una pequeña cantidad de restaurantes (especialmente cafeterías) para ayudar a las partes interesadas a reducir la búsqueda de una ubicación óptima para una nueva cafetería.

Al calcular la distribución de la densidad de los restaurantes a partir de los datos del API Foursquare, es posible generar una gran colección de ubicaciones que cumplen ciertos requisitos básicos.

Luego se realizó una agrupación de estos datos utilizando algoritmos de machine learning (K-medias) para crear las principales áreas de interés (que contienen el mayor número de ubicaciones potenciales) y se crearon las direcciones de estos centros de área. Desde esta interpretación podemos tener un punto de partida para la exploración final por parte de los interesados.

Las partes interesadas tomarán la decisión final sobre la ubicación óptima de los restaurantes en función de las características y ubicaciones específicas del vecindario en cada área recomendada, teniendo en cuenta factores adicionales como el atractivo de cada ubicación (proximidad a un parque o agua), niveles de ruido / carreteras principales. disponibilidad inmobiliaria, precio, dinámica social y económica de cada barrio, etc.

Finalmente, un análisis + completo y un trabajo futuro deberían integrar datos de otras fuentes externas.

Referencias