title

Une étude de cas d’intelligence de localisation pour localiser un restaurant de Cuisine Française dans la ville de Lyon

L’une des traditions les plus réputées de Lyon est sans doute sa gastronomie. Depuis 1935 et grâce à Curnonsky, célèbre critique culinaire, la ville porte le titre de « capitale mondiale de la gastronomie ». Dès le XIXe siècle, quand on vient à Lyon, on veut « manger » : d’abord chez les Mères, ces cuisinières d’excellence qui ont contribué à faire de la cuisine lyonnaise une véritable institution puis le paysage gastronomique lyonnais s’est diversifié et s’ouvre dorénavant à de nouvelles tendances (1), il convient donc d’étudier les possibilités offertes par les emplacements de nouveaux restaurants français.

Dans ce projet, l’idée est de trouver un emplacement optimal pour un restaurant de spécialités françaises à Lyon, basé sur des algorithmes d'apprentissage automatiques tirés du cours "The Battle of Neighborhoods: Coursera Capstone Project" (2).

Comme il y a beaucoup de restaurants à Lyon, nous allons essayer de détecter les emplacements en fonction de la définition de facteurs qui vont influencer notre décision :

1- Les endroits qui ne sont pas déjà bondés de restaurants.

2- Les zones avec peu ou pas de restaurants français à proximité.

3- Proches si possible du centre-ville, en supposant que les deux premières conditions soient remplies.

Source de données

Les sources de données suivantes seront nécessaires pour extraire / générer les informations requises:

1.- Les centres des zones candidates seront générés par algorithme et les adresses approximatives des centres de ces zones seront obtenues à l'aide de l'un des paquetages Geopy Geocoders. (3)

2-Le nombre de restaurants, leur type et leur emplacement dans chaque quartier seront obtenus à l'aide de l'API Foursquare. (4)

Les données seront utilisées dans les scénarios suivants:

1- pour peupler la densité de tous les restaurants et restaurants français à partir des données extraites.

2- pour identifier les zones peu encombrées et peu compétitives

3- pour calculer les distances entre les restaurants concurrents.

Candidats de Quartier

La zone cible sera celle qui se trouve vers le centre-ville, où les attractions touristiques sont plus nombreuses comparées aux autres lieux. Créez une grille de cellules couvrant la zone d’intérêt qui est approximativement. Kilomètres 12x12 centrés autour du centre-ville de Lyon.

In [16]:
import requests

from geopy.geocoders import Nominatim


address = '10 rue Marc Antoine Petit, 69002 Lyon, France'
geolocator = Nominatim(user_agent="lyon_explorer")
location = geolocator.geocode(address)
lat = location.latitude
lng = location.longitude
lyon_center = [lat, lng]
print('Coordinate of {}: {}'.format(address, lyon_center), ' location : ', location)
Coordinate of 10 rue Marc Antoine Petit, 69002 Lyon, France: [45.7448525, 4.8253553]  location :  10, Rue Marc-Antoine Petit, Perrache, Lyon 2e Arrondissement, Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, France, 69002, France

Nous créons une grille de zones candidates équidistantes, centrées autour du centre-ville et à environ 6 km de Lyon et calculons les distances dont nous avons besoin pour créer notre grille d'emplacements dans un système de coordonnées cartésiennes 2D qui nous permet de calculer des distances en mètres.

Ensuite, nous projetterons ces coordonnées en degrés de latitude / longitude à afficher sur les cartes avec Mapbox et Folium (5).

In [17]:
#!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('Vérification de la transformation de coordonnées')
print('-------------------------------')
print('Lyon center longitude={}, latitude={}'.format(lyon_center[1], lyon_center[0]))
x, y = lonlat_to_xy(lyon_center[1], lyon_center[0])
print('Lyon center UTM X={}, Y={}'.format(x, y))
lo, la = xy_to_lonlat(x, y)
print('Lyon center longitude={}, latitude={}'.format(lo, la))
Vérification de la transformation de coordonnées
-------------------------------
Lyon center longitude=4.8253553, latitude=45.7448525
Lyon center UTM X=-291337.8191532027, Y=5116288.719500631
Lyon center longitude=4.8253553, latitude=45.74485249999999

Créons une grille hexagonale de cellules: nous décalons toutes les lignes et ajustons l'espacement des lignes verticales de manière à ce que chaque centre de cellule soit à égale distance de tous ses voisins.

In [18]:
lyon_center_x, lyon_center_y = lonlat_to_xy(lyon_center[1], lyon_center[0]) # City center in Cartesian coordinates

k = math.sqrt(3) / 2 # Vertical offset for hexagonal grid cells
x_min = lyon_center_x - 6000
x_step = 600
y_min = lyon_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(lyon_center_x, lyon_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), 'Centres de quartier candidats générés.')
364 Centres de quartier candidats générés.

Visualisons les données dont nous disposons jusqu’à présent: emplacement du centre-ville et centres de quartier candidats:

In [19]:
import folium
In [20]:
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_lyon = folium.Map(location=lyon_center, zoom_start=12, tiles=tileset, attr=attribution)
folium.Marker(lyon_center, popup='City of Lyon').add_to(map_lyon)
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=300, color='purple', fill=False).add_to(map_lyon)
    #folium.Marker([lat, lon]).add_to(map_lyon)
map_lyon
Out[20]:

A ce stade, nous avons maintenant les coordonnées des centres de quartiers / zones à évaluer, à égale distance (la distance entre chaque point et ses voisins est exactement la même) et à environ 6 km de Lyon.

In [21]:
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(lyon_center[0], lyon_center[1], addr)) 
print(type(location[0]))
Reverse geocoding check
-----------------------
Address of [45.7448525, 4.8253553] is: 10, Rue Marc-Antoine Petit, Perrache, Lyon 2e Arrondissement, Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, France, 69002, France
<class 'str'>
In [22]:
print('Obtaining location addresses: ', end='')
addresses = []
for lat, lon in zip(latitudes, longitudes):
    address = get_address(lat, lon)
    if address is None:
        address = 'NO ADDRESS'
    address = address.replace(', France', '') # We don't need country part of address
    addresses.append(address)
    print(' .', end='')
print(' done.')
Obtaining location addresses:  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . done.
In [23]:
import pandas as pd

df_locations = pd.DataFrame({'Address': addresses,
                             'Latitude': latitudes,
                             'Longitude': longitudes,
                             'X': xs,
                             'Y': ys,
                             'Distance from center': distances_from_center})

df_locations.head()
Out[23]:
Address Latitude Longitude X Y Distance from center
0 nothing found 45.692167 4.811886 -293137.819153 5.110573e+06 5992.495307
1 14, Rue de Serrières, Zone industrielle La Mou... 45.692850 4.819470 -292537.819153 5.110573e+06 5840.376700
2 A 450, Pierre-Bénite, Lyon, Métropole de Lyon,... 45.693533 4.827055 -291937.819153 5.110573e+06 5747.173218
3 Station d'épuration - Installation d'Incinérat... 45.694216 4.834640 -291337.819153 5.110573e+06 5715.767665
4 Pont Pierre-Bénite, Saint-Fons, Lyon, Métropol... 45.694898 4.842225 -290737.819153 5.110573e+06 5747.173218
In [24]:
df_locations.shape
Out[24]:
(364, 6)
In [25]:
df_locations.to_pickle('./Dataset/lyon_locations.pkl')    

Foursquare

Nous avons utilisé l’API Foursquare pour explorer le nombre de restaurants disponibles dans un rayon de 300 m du centre de Lyon et nous avons limité la recherche aux catégories d’aliments afin de récupérer les résultats des restaurants, ainsi que leur latitude et leur longitude.

In [26]:
client_id = 'xxxxxx'
client_secret = 'xxxx'
VERSION = 'xxxxx'

Nous utilisons l’API Foursquare pour explorer le nombre de restaurants disponibles à moins de 6 km du centre de Lyon et limitons la recherche à tous les locaux associés à la catégorie de restaurants et plus particulièrement ceux qui correspondent à la cuisine française (Brasserie,Bouchons, Estaminet, etc).

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

french_restaurant_categories = ['52e81612bcbc57f1066b79f1', '52e81612bcbc57f1066b79f4', '4bf58dd8d48988d16c941735',
                                 '52e81612bcbc57f1066b79f2', '52e81612bcbc57f1066b7a09', '4bf58dd8d48988d120951735',
                                 '4bf58dd8d48988d10c941735', '57558b36e4b065ecebd306b6', '57558b36e4b065ecebd306b8',
                                 '57558b36e4b065ecebd306bc', '57558b36e4b065ecebd306b0','57558b36e4b065ecebd306c5', 
                                 '57558b36e4b065ecebd306c0', '57558b36e4b065ecebd306cb', '57558b36e4b065ecebd306ce',
                                 '57558b36e4b065ecebd306d1', '57558b36e4b065ecebd306b4', '57558b36e4b065ecebd306b2',
                                 '57558b35e4b065ecebd306ad', '57558b36e4b065ecebd306d4', '57558b36e4b065ecebd306d7',
                                 '57558b36e4b065ecebd306da', '57558b36e4b065ecebd306ba', '4bf58dd8d48988d155941735'] # 'Food' Catégorie de restaurants français
In [28]:
def is_restaurant(categories, specific_filter=None):
    restaurant_words = ['bouchons', 'brasserie', 'restaurant', 'café', 'lyonesse']
    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 'fast food' 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(', France', '')
    address = address.replace(', France', '')
    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 [29]:
# 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 = {}
    french_restaurants = {}
    location_restaurants = []

    print('Obtention de sites autour des sites candidats:', 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_french = is_restaurant(venue_categories, specific_filter=french_restaurant_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_french, x, y)
                if venue_distance<=300:
                    area_restaurants.append(restaurant)
                restaurants[venue_id] = restaurant
                if is_french:
                    french_restaurants[venue_id] = restaurant
        location_restaurants.append(area_restaurants)
        print(' .', end='')
    print(' done.')
    return restaurants, french_restaurants, location_restaurants

# Essayez de charger à partir du système de fichiers local au cas où nous l'avions déjà fait
restaurants = {}
french_restaurants = {}
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/french_restaurants_350.pkl', 'rb') as f:
        french_restaurants = pickle.load(f)
        print('french Restaurant data loaded.')
    with open('/Dataset/location_restaurants_350.pkl', 'rb') as f:
        location_restaurants = pickle.load(f)
        print('location Restaurant data loaded.')
    loaded = True
except:
    print('Données du restaurant chargées.')
    pass

# Si le chargement échoue, utilisez l'API Foursquare pour obtenir les données.
if not loaded:
    restaurants, french_restaurants, location_restaurants = get_restaurants(latitudes, longitudes)
    
Données du restaurant chargées.
Obtention de sites autour des sites candidats: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . done.
In [30]:
import numpy as np
print('**Les résultats**',)
print('Nombre total de Restaurants:', len(restaurants))
print('Nombre total de Restaurants Français:', len(french_restaurants))
print('Pourcentage de Restaurants Français: {:.2f}%'.format(len(french_restaurants) / len(restaurants) * 100))
print('Nombre moyen de Restaurants dans le quartier:', np.array([len(r) for r in location_restaurants]).mean())
**Les résultats**
Nombre total de Restaurants: 734
Nombre total de Restaurants Français: 303
Pourcentage de Restaurants Français: 41.28%
Nombre moyen de Restaurants dans le quartier: 1.7692307692307692
In [31]:
print('Liste de tous les restaurants')
print('-----------------------')
for r in list(restaurants.values())[:10]:
    print(r)
print('...')
print('Total:', len(restaurants))
Liste de tous les restaurants
-----------------------
('4ec6477677c8be492126c73a', 'Le Chineur', 45.694131642544384, 4.844251800402608, 'France', 179, True, -290591.0222608857, 5110467.682494704)
('5245fd57498ec19841af8fd4', 'Capri', 45.69619430298283, 4.817199073156698, '75 Chemin De La Mouche, Saint Genis Laval', 210, False, -292666.82421045285, 5110966.986744515)
('4fc5f505e4b0d1962f0128f8', 'Les Deux Platanes', 45.69653473628246, 4.821817788493687, '134 Rue Henri Barbusse, 69230\u200e Saint-Genis-Laval', 147, True, -292302.5519560282, 5110958.598738363)
('58e398749343e06385e00f62', "Bull'it Café", 45.696189, 4.820461, '134 Rue Henri Barbusse, 69230 Saint-Genis-Laval', 234, False, -292413.06639224105, 5110933.772935502)
('53da1f39498e724b4f937690', 'Le Canastel', 45.69783012801924, 4.826271801848619, '198 Rue des Martyrs de la Libération, 69310 Pierre Bénite', 299, False, -291937.48049620213, 5111057.923293267)
('5105491ae4b05ec85fc245c0', 'Kevser Kebap', 45.704534, 4.867112, 'France', 346, False, -288664.2744663686, 5111395.037822498)
('4f969cfbe4b08e75f2fd5450', "L'Auberge Savoyarde", 45.7032978, 4.8234443, '3 Rue de la République, 69310 Pierre-Bénite', 161, True, -292079.4937664536, 5111693.352618956)
('5346826e498ef5d622b677be', 'Osteria Italiana', 45.704161, 4.832133, '4, Rue Voltaire, Pierre Bénite', 104, False, -291391.1470583045, 5111702.384821587)
('4cf38b9d6c29236a3c5471a2', 'Courtepaille', 45.70563678709475, 4.831392955537238, 'Chemin de la Lone (Autoroute A7), 69310 Pierre-Bénite', 330, True, -291427.686768492, 5111873.657951516)
('5148572ee4b0e01e8daafcc3', 'Snack Anatolia', 45.70857594534838, 4.855010522406425, 'France', 350, False, -289548.34582979, 5111964.405218408)
...
Total: 734
In [32]:
print('Liste des restaurants français')
print('---------------------------')
for r in list(french_restaurants.values())[:10]:
    print(r)
print('...')
print('Total:', len(french_restaurants))
Liste des restaurants français
---------------------------
('4ec6477677c8be492126c73a', 'Le Chineur', 45.694131642544384, 4.844251800402608, 'France', 179, True, -290591.0222608857, 5110467.682494704)
('4fc5f505e4b0d1962f0128f8', 'Les Deux Platanes', 45.69653473628246, 4.821817788493687, '134 Rue Henri Barbusse, 69230\u200e Saint-Genis-Laval', 147, True, -292302.5519560282, 5110958.598738363)
('4f969cfbe4b08e75f2fd5450', "L'Auberge Savoyarde", 45.7032978, 4.8234443, '3 Rue de la République, 69310 Pierre-Bénite', 161, True, -292079.4937664536, 5111693.352618956)
('4cf38b9d6c29236a3c5471a2', 'Courtepaille', 45.70563678709475, 4.831392955537238, 'Chemin de la Lone (Autoroute A7), 69310 Pierre-Bénite', 330, True, -291427.686768492, 5111873.657951516)
('55f2b960498e7dade80ae8f8', 'Café des Arts', 45.706571, 4.878024, 'France', 110, True, -287786.35392994806, 5111512.6907795975)
('5c40628c65cdf8002cf08008', 'Six Pieds Sur Terre', 45.711561, 4.802183, '69600 Oullins', 209, True, -293615.61085299484, 5112823.724406202)
('54e74d40498e1c4563966105', 'Café Charmant', 45.71689046228136, 4.812333379301939, '1 Rue Louis Aulagne, 69600 Oullins', 166, True, -292749.82436070475, 5113313.902338774)
('4f195c60e4b02a5a23e34003', 'boite a sardines', 45.7151785541558, 4.820850797712636, '26 Quai Pierre Semard, 69350 La Mulatière, 69350 La Mulatière', 181, True, -292111.7131021753, 5113038.59621686)
('4dd3a9fc7d8b6704c7a9db0a', 'La terrasse de Marie', 45.720052, 4.859009, 'Rue du professeur Roux, Vénissieux', 164, True, -289074.0812424299, 5113198.943277162)
('5131f275e4b02d07dbba6b88', 'Les Frères Barbet', 45.71998903944859, 4.800653763204484, 'France', 148, True, -293614.0282938194, 5113774.943133002)
...
Total: 303
In [33]:
print('Restaurants autour de l´emplacement')
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))
Restaurants autour de l´emplacement
---------------------------
Restaurants around location 101: 
Restaurants around location 102: 
Restaurants around location 103: 
Restaurants around location 104: 
Restaurants around location 105: 
Restaurants around location 106: 
Restaurants around location 107: Jols
Restaurants around location 108: 
Restaurants around location 109: Le Martin, Le Karachi Indien Restaurant
Restaurants around location 110: Lyon Frans Royal Restaurant

Tous les restaurants de la ville de Lyon sont indiqués en gris et ceux associés à la cuisine française sont surlignés en rouge.

In [34]:
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_lyon = folium.Map(location=lyon_center, zoom_start=13, tiles=tileset, attr=attribution)
folium.Marker(lyon_center, popup='City of Lyon').add_to(map_lyon)
for res in restaurants.values():
    lat = res[2]; lon = res[3]
    is_french = res[6]
    color = 'red' if is_french else 'grey'
    folium.CircleMarker([lat, lon], radius=3, color=color, fill=True, fill_color=color, fill_opacity=1).add_to(map_lyon)
map_lyon
Out[34]:

Analyse

Analyse de données explicatives et quelques informations supplémentaires à partir de données brutes. Comptez le nombre de restaurants dans chaque zone candidate :

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

df_locations['Restaurants dans la zone'] = location_restaurants_count

print('Nombre moyen de restaurants dans chaque zone avec rayon = 300m:', np.array(location_restaurants_count).mean())

df_locations.head(10)
Nombre moyen de restaurants dans chaque zone avec rayon = 300m: 1.7692307692307692
Out[35]:
Address Latitude Longitude X Y Distance from center Restaurants dans la zone
0 nothing found 45.692167 4.811886 -293137.819153 5.110573e+06 5992.495307 0
1 14, Rue de Serrières, Zone industrielle La Mou... 45.692850 4.819470 -292537.819153 5.110573e+06 5840.376700 0
2 A 450, Pierre-Bénite, Lyon, Métropole de Lyon,... 45.693533 4.827055 -291937.819153 5.110573e+06 5747.173218 0
3 Station d'épuration - Installation d'Incinérat... 45.694216 4.834640 -291337.819153 5.110573e+06 5715.767665 0
4 Pont Pierre-Bénite, Saint-Fons, Lyon, Métropol... 45.694898 4.842225 -290737.819153 5.110573e+06 5747.173218 1
5 Saint-Fons, Lyon, Métropole de Lyon, Circonscr... 45.695579 4.849811 -290137.819153 5.110573e+06 5840.376700 0
6 Collège Alain, 1, Rue de Valence, Saint-Fons, ... 45.696260 4.857397 -289537.819153 5.110573e+06 5992.495307 0
7 Saint-Thomas d'Aquin-Veritas, Rue Francisque D... 45.695744 4.799664 -294037.819153 5.111093e+06 5855.766389 0
8 Chemin du Grand Revoyet, Favier, Saint-Genis-L... 45.696428 4.807248 -293437.819153 5.111093e+06 5604.462508 0
9 nothing found 45.697112 4.814833 -292837.819153 5.111093e+06 5408.326913 1

Calculons maintenant la distance du restaurant français le plus proche de chaque centre candidat de la région (non seulement ceux situés à moins de 300 m - nous voulons une distance jusqu'au centre le plus proche, quelle que soit sa distance).

In [36]:
distances_to_french_restaurant = []

for area_x, area_y in zip(xs, ys):
    min_distance = 100
    for res in french_restaurants.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_french_restaurant.append(min_distance)

df_locations['Distance to restaurant french'] = distances_to_french_restaurant
In [37]:
df_locations.head(10)
Out[37]:
Address Latitude Longitude X Y Distance from center Restaurants dans la zone Distance to restaurant french
0 nothing found 45.692167 4.811886 -293137.819153 5.110573e+06 5992.495307 0 100.0
1 14, Rue de Serrières, Zone industrielle La Mou... 45.692850 4.819470 -292537.819153 5.110573e+06 5840.376700 0 100.0
2 A 450, Pierre-Bénite, Lyon, Métropole de Lyon,... 45.693533 4.827055 -291937.819153 5.110573e+06 5747.173218 0 100.0
3 Station d'épuration - Installation d'Incinérat... 45.694216 4.834640 -291337.819153 5.110573e+06 5715.767665 0 100.0
4 Pont Pierre-Bénite, Saint-Fons, Lyon, Métropol... 45.694898 4.842225 -290737.819153 5.110573e+06 5747.173218 1 100.0
5 Saint-Fons, Lyon, Métropole de Lyon, Circonscr... 45.695579 4.849811 -290137.819153 5.110573e+06 5840.376700 0 100.0
6 Collège Alain, 1, Rue de Valence, Saint-Fons, ... 45.696260 4.857397 -289537.819153 5.110573e+06 5992.495307 0 100.0
7 Saint-Thomas d'Aquin-Veritas, Rue Francisque D... 45.695744 4.799664 -294037.819153 5.111093e+06 5855.766389 0 100.0
8 Chemin du Grand Revoyet, Favier, Saint-Genis-L... 45.696428 4.807248 -293437.819153 5.111093e+06 5604.462508 0 100.0
9 nothing found 45.697112 4.814833 -292837.819153 5.111093e+06 5408.326913 1 100.0
In [38]:
print('Distance moyenne en mètres du restaurant français le plus proche de chaque centre:', df_locations['Distance to restaurant french'].mean())
Distance moyenne en mètres du restaurant français le plus proche de chaque centre: 97.84663066364439

Nous avons utilisé des cartes heatmaps de Folium avec Mapbox pour visualiser la densité des restaurants dans le rayon sélectionné à partir du centre de Lyon.

In [39]:
lyon_boroughs_url = 'https://transcode.geo.data.gouv.fr/services/5bfd5598cb932f8781e55ede/feature-types/ms:adr_voie_lieu.adrarrond?format=GeoJSON&projection=WGS84'
lyon_boroughs = requests.get(lyon_boroughs_url).json()

def boroughs_style(feature):
    return { 'color': 'blue', 'fill': False }
In [40]:
restaurant_latlons = [[res[2], res[3]] for res in restaurants.values()]

french_latlons = [[res[2], res[3]] for res in french_restaurants.values()]
In [41]:
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_lyon = folium.Map(location=lyon_center, zoom_start=13, tiles=tileset, attr=attribution)
HeatMap(restaurant_latlons).add_to(map_lyon)
folium.Marker(lyon_center).add_to(map_lyon)
folium.Circle(lyon_center, radius=1000, fill=False, color='white').add_to(map_lyon)
folium.Circle(lyon_center, radius=2000, fill=False, color='blue').add_to(map_lyon)
folium.Circle(lyon_center, radius=3000, fill=False, color='red').add_to(map_lyon)
map_lyon
Out[41]:

à présent, nous créons une autre carte heatmap montrant uniquement heatmap / densité des restaurants français .

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

D'après les cartes ci-dessus, il a été constaté que la plupart des restaurants sont dispersés du côté nord du centre-ville et que leur densité est plus présente après avoir consulté la carte thermique. La ville de Lyon, qui a été désignée comme le centre et ses régions du nord, a été occupée principalement par des bureaux administratifs comptant moins de population résidentielle. La partie occidentale a un attrait touristique important mais est déjà surchargée de restaurants. Notre objectif va être la partie ouest et sud du centre de Lyon qui a une bonne population résidentielle ainsi que des attractions touristiques.

In [43]:
roi_x_min = lyon_center_x - 2000
roi_y_max = lyon_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_lyon = folium.Map(location=lyon_center, zoom_start=13, tiles=tileset, attr=attribution)
HeatMap(restaurant_latlons).add_to(map_lyon)
folium.Marker(lyon_center).add_to(map_lyon)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.4).add_to(map_lyon)
map_lyon
Out[43]:

Créons également une nouvelle grille plus dense de candidats localisés limitée à notre nouvelle région d'intérêt

In [44]:
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), 'centres de quartier candidats générés.')
2120 centres de quartier candidats générés.

Calculez deux choses les plus importantes pour chaque emplacement candidat: le nombre de restaurants à proximité (nous utiliserons un rayon de 250 mètres) et la distance jusqu'au restaurant français le plus proche.

In [45]:
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_french_distances = []

print('Generating data on location candidates... ', 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, french_restaurants)
    roi_french_distances.append(distance)
print('done.')
Generating data on location candidates... done.
In [46]:
df_roi_locations = pd.DataFrame({'Latitude':roi_latitudes,
                                 'Longitude':roi_longitudes,
                                 'X':roi_xs,
                                 'Y':roi_ys,
                                 'Restaurants nearby':roi_restaurant_counts,
                                 'Distance to french restaurant':roi_french_distances})


df_roi_locations.sort_values(by=['Restaurants nearby'], ascending=False, inplace=True)

df_roi_locations.head(5)
Out[46]:
Latitude Longitude X Y Restaurants nearby Distance to french restaurant
1878 45.763271 4.834195 -290387.819153 5.118246e+06 57 49.830007
1879 45.763384 4.835461 -290287.819153 5.118246e+06 54 27.450325
1915 45.764095 4.834687 -290337.819153 5.118332e+06 52 70.434322
1875 45.762929 4.830398 -290687.819153 5.118246e+06 52 157.249881
1840 45.762560 4.834969 -290337.819153 5.118159e+06 50 99.813433
In [47]:
df_roi_locations.shape
Out[47]:
(2120, 6)

Laissez-nous maintenant filtrer ces lieux: nous nous intéressons uniquement aux emplacements ne comptant pas plus de deux restaurants dans un rayon de 250 mètres et aucun restaurant français à un rayon de 400 mètres .

In [48]:
good_res_count = np.array((df_roi_locations['Restaurants nearby']<=2))
print('Emplacements avec pas plus de deux restaurants à proximité:', good_res_count.sum())

good_ind_distance = np.array(df_roi_locations['Distance to french restaurant']>=400)
print('Emplacements sans restaurants français à moins de 400m:', good_ind_distance.sum())

good_locations = np.logical_and(good_res_count, good_ind_distance)
print('Lieux avec les deux conditions remplies:', good_locations.sum())

df_good_locations = df_roi_locations[good_locations]
Emplacements avec pas plus de deux restaurants à proximité: 1394
Emplacements sans restaurants français à moins de 400m: 475
Lieux avec les deux conditions remplies: 458
In [49]:
good_latitudes = df_good_locations['Latitude'].values
good_longitudes = df_good_locations['Longitude'].values

good_locations = [[lat, lon] for lat, lon in zip(good_latitudes, good_longitudes)]
map_lyon = folium.Map(location=lyon_center, zoom_start=14, tiles=tileset, attr=attribution)
HeatMap(restaurant_latlons).add_to(map_lyon)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.6).add_to(map_lyon)
folium.Marker(lyon_center).add_to(map_lyon)
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_lyon) 
map_lyon
Out[49]:
In [50]:
map_lyon = folium.Map(location=lyon_center, zoom_start=14, tiles=tileset, attr=attribution)
HeatMap(good_locations, radius=25).add_to(map_lyon)
folium.Marker(lyon_center).add_to(map_lyon)
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_lyon)
map_lyon
Out[50]:

Laissez-nous maintenant Cluster ces emplacements pour créer des centres de zones contenant de bons emplacements . Ces zones, leurs centres et adresses seront le résultat final de notre analyse.

In [52]:
from sklearn.cluster import KMeans

number_of_clusters = 16

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_lyon = folium.Map(location=lyon_center, zoom_start=14, tiles=tileset, attr=attribution)
HeatMap(restaurant_latlons).add_to(map_lyon)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.4).add_to(map_lyon)
folium.Marker(lyon_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_lyon) 
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_lyon)
map_lyon
Out[52]:

Voyons ces zones ouest et sud sur ville avec une carte thermique, en utilisant des zones ombrées pour indiquer nos clusters:

In [53]:
map_lyon = folium.Map(location=lyon_center, zoom_start=14, tiles=tileset, attr=attribution)
folium.Marker(lyon_center).add_to(map_lyon)
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_lyon)
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_lyon)
for lon, lat in cluster_centers:
    folium.Circle([lat, lon], radius=500, color='white', fill=False).add_to(map_lyon) 
map_lyon
Out[53]:

Zoomons sur les zones candidates

In [54]:
candidate_area_addresses = []
print('==============================================================')
print('Adresses des centres de zones recommandées')
print('==============================================================\n')
for lon, lat in cluster_centers:
    addr = get_address(lat, lon)
    addr = addr.replace(', France', '')
    addr = addr.replace(', Lyon', '')
    addr = addr.replace(', FR', '')
    addr = addr.replace(', Lyon', '')
    addr = addr.replace("'", '')
    candidate_area_addresses.append(addr)    
    x, y = lonlat_to_xy(lon, lat)
    d = calc_xy_distance(x, y, lyon_center_x, lyon_center_y)
    print('{}{} => {:.1f}km from City of Lyon'.format(addr, ' '*(50-len(addr)), d/1000))
    
==============================================================
Adresses des centres de zones recommandées
==============================================================

nothing found                                      => 2.5km from City of Lyon
Cité Scolaire Internationale de Lyon, 2, Place de Montréal, Gerland 7e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69007 => 2.0km from City of Lyon
Etablissement scolaire provisoire, Esplanade Blandan, La Guillotière 7e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69007 => 2.2km from City of Lyon
nothing found                                      => 1.2km from City of Lyon
Chemin de la Demi-Lune, Champvert 5e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69005 => 2.6km from City of Lyon
Rue Croix Barret, Gerland 7e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69007 => 2.0km from City of Lyon
Lycée technologique privé ICOF, Avenue Debrousse, Saint-Georges, Saint-Irénée 5e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69005 => 1.1km from City of Lyon
BRASSART Lyon, Avenue du Château de Gerland, Gerland 7e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69007 => 2.1km from City of Lyon
87, Rue Pierre Audry, Saint-Georges, Saint-Just 5e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69005 => 2.4km from City of Lyon
Terrain de Sports Marius Bourrat, Avenue du Maréchal Foch, Sainte-Foy-lès-Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69110 => 1.7km from City of Lyon
37, Rue du Commandant Charcot, Point du Jour 5e Arrondissement, Sainte-Foy-lès-Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69005 => 2.0km from City of Lyon
24, Rue Denuzière, Confluence 2e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69002 => 0.4km from City of Lyon
Chemin de Fontanières, La Mulatière, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69350 => 1.7km from City of Lyon
Avenue Berthelot 7e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69007 => 1.5km from City of Lyon
Hôpital gériatrique de Fourvière, Allée Louise et Rose Faurite, Saint-Georges, Fourvière 5e Arrondissement, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69005 => 2.1km from City of Lyon
7 bis, Rue du Pensionnat, La Mulatière, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, 69350 => 2.1km from City of Lyon

Result

In [55]:
map_lyon = folium.Map(location=lyon_center, zoom_start=13.4, tiles=tileset, attr=attribution)
folium.Circle(lyon_center, radius=50, color='red', fill=True, fill_color='red', fill_opacity=1).add_to(map_lyon)
for lonlat, addr in zip(cluster_centers, candidate_area_addresses):
    folium.Marker([lonlat[1], lonlat[0]], popup=addr).add_to(map_lyon)     
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_lyon)
map_lyon
Out[55]:

Les emplacements ci-dessus sont assez proches du centre de Lyon et chacun de ces emplacements ne compte pas plus de deux restaurants dans un rayon de 250 m et aucun Restaurant français ne se trouve à moins de 400 m. N'importe lequel de ces établissements est un candidat potentiel pour un nouveau Restaurant Français, du moins en raison de la concurrence à proximité. L'algorithme K-means d'apprentissage non supervisé permet de regrouper les 15 emplacements avec un choix adéquat pour les parties prenantes à choisir parmi les résultats présentés ci-dessous.

Les résultats

Le but de ce projet était d'identifier les secteurs lyonnais proches du centre avec un faible nombre de restaurants (en particulier des restaurants français) afin d'aider les parties prenantes à réduire la recherche d'un emplacement optimal pour un nouveau restaurant français.

En calculant la distribution de densité de restaurants à partir de données Foursquare, il est possible de générer une vaste collection de lieux qui répondent à certaines exigences de base.

Un regroupement de ces donnés en utilisant des algorithmes d'apprentissage machineemplace,ments a ensuite été effectué afin de créer des zones d'intérêt majeures (contenant le plus grand nombre d'emplacements potentiels) et les adresses de ces centres de zone ont été créées pour servir de points de départ à l'exploration finale par les parties prenantes.

Les parties prenantes prendront la décision finale quant à l'emplacement optimal des restaurants en fonction des caractéristiques spécifiques des quartiers et des emplacements dans chaque zone recommandée, en tenant compte de facteurs supplémentaires tels que l'attractivité de chaque emplacement (proximité d'un parc ou de l'eau), des niveaux de bruit / des routes principales. , disponibilité immobilière, prix, dynamique sociale et économique de chaque quartier, etc.

Enfin, une analyse plus complète et les travaux futurs devraient intégrer des données provenant d'autres bases de données externes.

Références