Lycée de secteur
Le problème
On dispose d'une liste qui associe à certaines adresses géographiques un lycée de secteur du département 31 (Haute-Garonne).
Les adresses sont en fait représentées par un couple
(lat, lon)
oùlat
est la latitude de l'adresse etlon
est la longitude de l'adresse. Par exemple, le 7 rue St-Hilaire à Toulouse est représenté par le couple(43.61319, 1.44209)
.
On souhaite rédiger un programme qui, à une adresse donnée (donc un couple latitude et longitude), indique quel est son lycée de secteur, en se basant sur le lycée de secteur de ses voisins.
La liste d'association est stockée sous la forme d'un dictionnaire nommé adresses_lycees
. Voici ci-dessous un extrait de cette liste d'association. Le fichier complet vous sera donné plus tard.
adresses_lycees = {
(43.116227, 0.718689): "Lycée Bagatelle",
(43.592111, 1.459428): "Lycée Marcelin Berthelot",
(43.587685, 1.48444): "Lycée Saint-Sernin",
...
}
On rappelle quelques opérations élémentaires sur les dictionnaires :
for adresse in adresses_lycees: # Permet de parcourir uniquement les adresses existantes, stockées en temps que couple de données.
for (lat, lon) in assoc: # Idem en obtenant directement les valeurs lat et lon au lieu d'un couple (lat, lon).
(lat, lon) in assoc # Renvoie True si l'adresse existe dans le dictionnaire, False sinon
lycee = assoc[(lat, long)] # Enregistre dans la variable lycee le nom du lycée associé à une adresse existante
lycee = assoc[adresse] # Idem avec adresse un couple de coordonnées (lat, lon)
Toutes vos réponses sont à rédiger dans un fichier lycee_de_secteur.py
et les tests présents dans les docstrings peuvent être exécutés en plaçant le code suivant dans votre fichier :
if __name__ == '__main__':
# Tests des fonctions pouvant être testées avec doctest.
# Il ne doit pas y avoir de failure dans l'exécution finale.
import doctest
doctest.testmod()
Question
Rédiger une fonction distance(lat_1, lon_1, lat_2, lon_2)
qui renvoie la distance euclidienne entre deux points (lat_1, lon_1)
et (lat_2, lon_2)
.
def distance(lat_1, lon_1, lat_2, lon_2):
"""
Calcule la distance euclidienne entre deux points.
:param lat_1: Latitude du point 1
:param lon_1: Longitude du point 1
:param lat_2: Latitude du point 2
:param lon_2: Longitude du point 2
>>> distance(43.57801, 1.44627, 43.59577, 1.45256)
0.018840958043584093
>>> distance(43.61125, 1.34041, 43.55486, 1.54005)
0.2074511067697639
"""
pass
Indice
On rappelle la formule de la distance euclidienne : \(distance(A, B)=\sqrt{(x_A-x_B)^2+(y_A-y_B)^2}\)
Indice
Il faut importer la fonction sqrt depuis le module math :
from math import sqrt
# Utilisation :
sqrt(4) # 2
Indice
Nos valeurs A et B dans la formule s'appliquent de cette façon :
\(x_A \rightarrow\)
lat_1
\(x_B \rightarrow\)
lat_2
\(y_A \rightarrow\)
lon_1
\(y_B \rightarrow\)
lon_2
Solution
from math import sqrt
def distance(lat_1, lon_1, lat_2, lon_2):
"""
Calcule la distance euclidienne entre deux points.
:param lat_1: Latitude du point 1
:param lon_1: Longitude du point 1
:param lat_2: Latitude du point 2
:param lon_2: Longitude du point 2
>>> distance(43.57801, 1.44627, 43.59577, 1.45256)
0.018840958043584093
>>> distance(43.61125, 1.34041, 43.55486, 1.54005)
0.2074511067697639
"""
diff_lat = (lat_2 - lat_1) ** 2
diff_lon = (lon_2 - lon_1) ** 2
return sqrt(diff_lat + diff_lon)
Où se trouvent nos données ?
Pour cet exercice, le dictionnaire contenant les données est déjà présent dans le fichier ci-joint :
On peut l'importer en le plaçant dans le même dossier que le fichier Python de cet exercice, et en indiquant dans votre fichier de travail le code suivant :
from donnees_lycees import adresses_lycees
La première valeur donnees_lycees
correspond au nom du fichier sans l'extension .py
et la deuxième adresses_lycees
correspond au nom du dictionnaire dans le fichier.
Question
On souhaite ensuite rédiger une fonction plus_proche(lat, lon, k, assoc)
qui renvoie la liste des k
plus proches adresses de (lat, lon)
dans le dictionnaire assoc
.
On propose pour cela l'algorithme suivant :
Stocker dans une liste
distances
l'ensemble des adresses et de leur distance à(lat, lon)
sous la forme(distance, adresse)
. (voir premier indice si pas compris)Cette liste va permettre d'indiquer, pour chaque adresse, quelle est sa distance avec notre point de calculer
(lat, lon)
passé en paramètre de la fonction.Trier la liste
distances
. Comment trier rapidement ?Cela fonctionne car lorsqu'on trie une liste de tuples, le tri va se faire en priorité sur le premier élément du tuple (ici sur la distance). Ingénieux !
Parcourir les
k
premiers éléments de la listedistances
et conserver les adresses dans une autre listeplus_proches
. (voir deuxième indice si vous êtes vraiment perdus)
Rédiger cet algorithme en Python.
def plus_proche(lat, lon, k, assoc):
"""
Renvoie les k plus proches adresses de (lat, lon)
:param lat: Latitude de l'adresse
:param lon: Longitude de l'adresse
:param k: Nombre de voisins à renvoyer
:param assoc: Dictionnaire qui associe un lycée à chaque adresse
>>> plus_proche(43.61125, 1.34041, 8, adresses_lycees)
[(43.61036, 1.33953), (43.609972, 1.340423), (43.61024, 1.34149), (43.609475, 1.340495), (43.609555, 1.339555), (43.613156, 1.340015), (43.61281, 1.339055), (43.610095, 1.338595)]
>>> plus_proche(43.59577, 1.45256, 5, adresses_lycees)
[(43.597102, 1.45326), (43.596568, 1.454731), (43.598, 1.453246), (43.597306, 1.450786), (43.595723, 1.454907)]
"""
pass
Indice
On souhaite obtenir une liste qui ressemble à cela :
distances = [(0.3, (43.52791, 1.23712)), (0.2, (43.21831, 1.63139))]
Cette liste a deux éléments :
Le premier est l'adresse
(43.52791, 1.23712)
située à distance0.3
de l'adresse fournie en paramètre de la fonction(lat, lon)
Le second est l'adresse
(43.21831, 1.63139)
située à distance0.2
de l'adresse fournie en paramètre de la fonction(lat, lon)
Pour y parvenir, il faut :
parcourir toutes les adresses de assoc (par exemple avec
for (lat_1, lon_1) in assoc:
) ;calculer la distance
d
avec(lat, lon)
;l'ajouter dans la liste
distances
sous la forme(d, (lat_1, lon_1))
Indice
Il suffit de parcourir les indices de 0 à k (exclu) en utilisant la boucle FOR et range(k)
.
Si on utilise i
comme variant de notre boucle for, on peut ensuite récupérer l'adresse avec distances[i][1]
.
Pourquoi 1 ? Parce-que
distances[i]
est un tuple qui contient comme premier élément la distance, et comme deuxième élément l'adresse.
Solution
def plus_proche(lat, lon, k, assoc):
"""
Renvoie les k plus proches adresses de (lat, lon)
:param lat: Latitude de l'adresse
:param lon: Longitude de l'adresse
:param k: Nombre de voisins à renvoyer
:param assoc: Dictionnaire qui associe un lycée à chaque adresse
>>> plus_proche(43.61125, 1.34041, 8, adresses_lycees)
[(43.61036, 1.33953), (43.609972, 1.340423), (43.61024, 1.34149), (43.609475, 1.340495), (43.609555, 1.339555), (43.613156, 1.340015), (43.61281, 1.339055), (43.610095, 1.338595)]
>>> plus_proche(43.59577, 1.45256, 5, adresses_lycees)
[(43.597102, 1.45326), (43.596568, 1.454731), (43.598, 1.453246), (43.597306, 1.450786), (43.595723, 1.454907)]
"""
distances = [] # Contient des éléments sous la forme (d, (lat, lon))
# Parcours de toutes les adresses existantes
for lat_1, lon_1 in assoc:
d = distance(lat, lon, lat_1, lon_1)
distances.append((d, (lat_1, lon_1)))
# Tri des distances
distances.sort()
# Sauvegarde des k plus proches
plus_proches = []
for i in range(k):
plus_proches.append(distances[i][1])
return plus_proches
On approche de la fin
Plus que quelques lignes de code et on pourra connaître le lycée de secteur de n'importe qui en Haute-Garonne !
Il ne reste plus qu'à exploiter les résultats, le plus dur est déjà derrière vous ! Dans la dernière fonction de votre programme, vous allez devoir déterminer quel est le lycée de secteur d'une adresse donnée.
Question
Rédiger une fonction lycee_de_secteur(lat, lon) qui renvoie le nom du lycée de secteur d'une adresse donnée en paramètre.
Vous êtes libres de choisir la valeur qui vous semble la plus appropriée pour k
en paramètre par défaut.
On utilisera max(liste, key=liste.count)
pour connaître la valeur de l'élément le plus fréquent dans la liste liste
. (le deuxième indice vous explique comment ça marche)
Le premier indice déroule l'algorithme à coder en Python.
def lycee_de_secteur(lat, lon, k=5):
"""
Renvoie le nom du lycée de secteur depuis une adresse
:param lat: Latitude de l'adresse
:param lon: Longitude de l'adresse
:param k: Nombre de voisins à prendre en compte
>>> lycee_de_secteur(43.62225, 1.39855, 5)
'Lycée Saint-Exupéry'
>>> lycee_de_secteur(43.46647, 1.06295, 10)
'Lycée Charles de Gaulle'
"""
pass
Indice
Voici le déroulé de notre algorithme :
Récupérer la liste des plus proches voisins grâce à la fonction précédemment codée.
Vous pouvez choisir la valeur de
k
que vous souhaitez en paramètre par défaut (par exemple 5).Vous devez utiliser
adresses_lycees
pour le paramètreassoc
deplus_proche()
.
Créer une liste
lycees
qui contient tous les lycées de secteur de nos voisins.Vous devrez parcourir tous les plus proches voisins (obtenus avec l'étape précédente).
Pour chaque
voisin
, on peut récupérer son lycée de secteur grâce àadresses_lycees[voisin]
.
Rechercher et renvoyer la valeur la plus fréquente dans la liste
lycees
.
Indice
On peut obtenir la valeur la plus fréquente dans une liste grâce à max(liste, key=liste.count)
. Pourquoi ?
La fonction
max()
renvoie la valeur la plus grande dans une liste (ex :max([2,5,3])
renvoie 5).Le paramètre
key=
permet de précisier une fonction a utiliser sur chaque élément avant de le comparer aux autres.La fonction
liste.count
renvoie le nombre d'occurences d'un élément passé en paramètre.Les parenthèses ne sont d'ailleurs pas présentes car on ne veut pas donner à
max()
le résultat de la fonction, mais le nom de la fonction qu'il devra utiliser.
Donc la fonction max()
regarde le nombre d'occurences de chaque élément, et pour celui dont cette valeur est la plus élevée, va renvoyer sa valeur.
Solution
def lycee_de_secteur(lat, lon, k=5):
"""
Renvoie le nom du lycée de secteur depuis une adresse
:param lat: Latitude de l'adresse
:param lon: Longitude de l'adresse
:param k: Nombre de voisins à prendre en compte
>>> lycee_de_secteur(43.62225, 1.39855, 5)
'Lycée Saint-Exupéry'
>>> lycee_de_secteur(43.46647, 1.06295, 10)
'Lycée Charles de Gaulle'
"""
plus_proches = plus_proche(lat, lon, k, adresses_lycees)
# On stocke le nom des lycées de secteur de chaque voisin
lycees = []
for voisin in plus_proches:
lycees.append(adresses_lycees[voisin])
# On renvoie le lycée le plus fréquent
return max(lycees, key=lycees.count)
Pour les plus rapides
Si vous avez tout terminé, bravo ! Le reste des exercices n'est que du bonus, et n'est pas à connaître. Il s'agit de deux questions bonus :
La première consiste à créer une carte sur laquelle apparaissent les voisins d'une couleur différente selon leur lycée de secteur.
La seconde consiste à revoir la fonction de distance pour calculer correctement la distance entre deux adresses géographiques.
Question
Ce premier exercice consiste donc à obtenir le résultat ci-contre.
On doit pouvoir afficher les voisins autour de nous, avec le nom de leur lycée de secteur.
On utilisera pour cela le module TkinterMapView. Il est déjà installé sur les ordinateurs du lycée. Chez vous, il faudra taper pip3 install tkintermapview
dans le terminal.
On dispose également de deux couleurs associées à chaque lycée, toujours dans le fichier donnees_lycees
où couleurs_lycees
est un dictionnaire qui à chaque lycée associe deux couleurs dans un tuple.
from donnees_lycees import adresses_lycees, couleurs_lycees
Compléter la fonction ci-dessous pour afficher les points sur la carte.
from tkinter import Tk
from tkintermapview import TkinterMapView
def afficher_voisins(lat, lon, k=10):
# Création de la fenetre Tkinter
largeur, hauteur = 800, 600
fenetre = Tk(
)
fenetre.geometry(f"{largeur}x{hauteur}")
fenetre.title("Lycées de secteur")
# Ajout de la carte à la fenêtre
carte = TkinterMapView(fenetre, width=largeur, height=hauteur, corner_radius=0)
carte.place(relx=0.5, rely=0.5, anchor=CENTER)
# On récupère les lycées les plus proches
plus_proches = ...
# On ajoute les voisins à la carte
for lat_voisin, lon_voisin in plus_proches:
# On récupère toutes les infos pour l'affichage
lycee = ....
couleur_1, couleur_2 = ...
# On affiche le voisin
carte.set_marker(lat_voisin, lon_voisin, text=lycee, marker_color_circle=couleur_1, marker_color_outside=couleur_2)
# On ajoute l'adresse de recherche sur la carte avec le lycée final
carte.set_position(lat, lon, marker=True, text=lycee_de_secteur(lat, lon, k), marker_color_circle="white", marker_color_outside="gray")
carte.set_zoom(16)
# On affiche la carte
carte.mainloop()
Solution
def afficher_voisins(lat, lon, k=10):
# Création de la fenetre Tkinter
largeur, hauteur = 800, 600
fenetre = Tk()
fenetre.geometry(f"{largeur}x{hauteur}")
fenetre.title("Lycées de secteur")
# Ajout de la carte à la fenêtre
carte = TkinterMapView(fenetre, width=largeur, height=hauteur, corner_radius=0)
carte.place(relx=0.5, rely=0.5, anchor=CENTER)
# On récupère les lycées les plus proches
plus_proches = plus_proche(lat, lon, k, adresses_lycees)
# On ajoute les voisins à la carte
for lat_voisin, lon_voisin in plus_proches:
# On récupère toutes les infos pour l'affichage
lycee = adresses_lycees[(lat_voisin, lon_voisin)]
couleur_1, couleur_2 = couleurs_lycees[lycee]
# On affiche le voisin
carte.set_marker(lat_voisin, lon_voisin, text=lycee, marker_color_circle=couleur_1, marker_color_outside=couleur_2)
# On ajoute l'adresse de recherche sur la carte avec le lycée final
carte.set_position(lat, lon, marker=True, text=lycee_de_secteur(lat, lon, k), marker_color_circle="white", marker_color_outside="gray")
carte.set_zoom(16)
# On affiche la carte
carte.mainloop()
Question
Ce deuxième exercice consiste à réécrire la fonction distance()
en prenant en compte la courbure de la Terre et en calculant la vraie distance en km entre deux points géographiques.
Cette distance est appelée formule de haversine et s'applique à toute sphère. La formule est donnée ci-dessous :
\(d = 2 r \arcsin\left(\sqrt{\sin^2\left(\frac{x_2 - x_1}{2}\right) + \cos(x_1) \cos(x_2)\sin^2\left(\frac{y_2 - y_1}{2}\right)}\right)\)
où \(r\) est le rayon de la sphère (6371 km)
\((x_1, y_1)\) correspond au premier point
\((x_2, y_2)\) correspond au second point
Attention, les coordonnées sont à représenter en radians. Il faudra donc d'abord transformer les différentes coordonnées (exprimées en degrés) en radians.
Vous aurez aussi besoin d'importer quelques fonctions depuis le module math
:
from math import sin, cos, atan2, sqrt, pi
Rédiger la fonction distance_km(lat1, lon1, lat2, lon2)
qui implémente cette fonction de haversine.
def distance_km(lat1, lon1, lat2, lon2):
"""
Calcule la distance de haversine entre deux points.
:param lat_1: Latitude du point 1
:param lon_1: Longitude du point 1
:param lat_2: Latitude du point 2
:param lon_2: Longitude du point 2
>>> distance_km(43.57801, 1.44627, 43.59577, 1.45256)
2.0387675154222005
>>> distance_km(43.61125, 1.34041, 43.55486, 1.54005)
17.259635893940143
"""
pass
Indice
Pour traduire les degrés en radians, on pourra utiliser le code suivant :
def deg2rad(deg):
return deg * (pi / 180)
Solution
def deg2rad(deg):
return deg * (pi / 180)
def distance_km(lat1, lon1, lat2, lon2):
"""
Calcule la distance de haversine entre deux points.
:param lat_1: Latitude du point 1
:param lon_1: Longitude du point 1
:param lat_2: Latitude du point 2
:param lon_2: Longitude du point 2
>>> distance_km(43.57801, 1.44627, 43.59577, 1.45256)
2.0387675154222005
>>> distance_km(43.61125, 1.34041, 43.55486, 1.54005)
17.259635893940143
"""
R = 6371
dLat = deg2rad(lat2 - lat1)
dLon = deg2rad(lon2 - lon1)
a = sin(dLat / 2) * sin(dLat / 2) + cos(deg2rad(lat1)) * cos(deg2rad(lat2)) * sin(dLon / 2) * sin(dLon / 2)
c = atan2(sqrt(a), sqrt(1 - a))
d = 2 * R * c
return d