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