15. Une première carte - Métro 4#

Nous avons à notre disposition les positions GPS des stations de métro de la ligne 4. Nous souhaitons afficher sur un carte ces stations. Pour cela, allons réaliser un programme Python. Nous veillerons à ce que l’organisation soit la plus correcte et notre code aussi clair que possible.

Nous allons suivre le shéma suivant.

  1. Lire le fichier de données

  2. Préparer les données Python

  3. Créer la carte

La première partie consiste à lire un fichier de données et les convertir en quelque chose d’interprétable pour Python. La deuxième partie sera nécessaire pour préparer les données, c’est à dire, filter celles qui ne sont pas pertinentes et convertir les données au format le plus adapté. Nous pourrons alors réaliser la troisième partie dédiée à la création de la carte.

Pour les plus motivé.e.s, on découvrira comment améliorer notre projet en complétant les données corrompues plutôt qu’en les filtrant.

Le schéma suivant récapitule le processus qui nous attend. Il vous représente la nature des données que l’on traite après l’appel de chacune des fonctions.

Schéma directeur

15.1. Préparation du projet#

Commençons par créer un nouveau projet Replit et à y ajouter un répertoire datas pour y stocker le fichier contenant les données : ligne4.txt. Ouvrez le fichier ligne4.txt et regardez comment il est constitué.

Il est constitué de lignes. Chaque ligne correspond à une station. Elle est divisée en trois colonnes avec la latitude, la longitude et le nom de la station. Les colonnes sont séparées par des virgules. Avec un peu d’attention, vous devriez remarquer que certaines lignes sont mal justifiées et d’autres sont même corrompues : des données manquent, au maximum une par ligne. Gardons cela en tête.

Tout le code que nous allons créer devra être contenu dans le fichier main.py. Nous tacherons de le garder aussi propre que possible. Si vous avez des commentaires à écrire ou des notes pour le cours, faites le dans un autre fichier. N’utilisez ce fichier que pour le code, rien de plus.

Passons à la première partie, la lecture des données.

15.2. Partie 1, lire un fichier texte#

Afin de ne pas trop se noyer dans les informations, je vous propose de considérer la fonction read_file(name) comme une boite noire. C’est une fonction qui lit un fichier et renvoie une liste qui contient les lignes contenues dans le fichier. Nous détaillerons plus tard dans le cours son fonctionnement.

Notons tout de même que le paramètre name permet de désigner le fichier, c’est le chemin d’accès vers le fichier. L’origine de ce chemin est le répertoire contenant le fichier main.py. Dans notre cas, pour lire le fichier il faudra donc saisir l’instruction read_file("datas/ligne4.txt").

def read_file(name):
    lines = ""
    with open(name) as f:
        lines = f.readlines()
    return lines
    
lines = read_file("datas/ligne4.txt")

Question : Copier/coller dans main.py cette fonction et tester là.

Question : La première fonction que l’on va coder permet de découper une ligne. Créez une fonction split_line(line). Cette fonction prend en paramètre une ligne et la décompose pour renvoyer une liste de trois valeurs.

Ainsi,

split_line('48.828566875440806     , 2.3272997082613656, Alésia \n')

doit nous renvoyer une la liste suivante :

[48.828566875440806     ', ' 2.3272997082613656', ' Alésia \n']

def split_line(l):
    pass

split_line('48.828566875440806     , 2.3272997082613656, Alésia \n')

Voici une correction possible.

def split_line(l):
    elts = l.split(",")
    return elts

Question : Améliorons cette fonction en supprimant les caractères blancs au début et à la fin des chaines de caractères que l’on obtient.

Ainsi,

split_line('48.828566875440806     , 2.3272997082613656, Alésia \n')

doit nous renvoyer une la liste suivante :

[48.828566875440806', '2.3272997082613656', 'Alésia']

def split_line(l):
    pass

split_line('48.828566875440806     , 2.3272997082613656, Alésia \n')

Voici une correction possible.

#version brutale
def split_line(l):
    elts = l.split(",")
    elts[0] = elts[0].strip()
    elts[1] = elts[1].strip()
    elts[2] = elts[2].strip()
    return elts

#version badass
def split_line(l):
    return [e.strip() for e in l.split(",")]

Question : Nous savons séparer correctement une ligne, nous allons maintenant réaliser l’oppération pour toutes les lignes du fichier. Faites une fonction clear_lines(lines), cette fonction prend en paramètre une liste de lignes (celle produite par la fonction read_file) et applique la fonction split_line sur chaque ligne.

Partant de

['48.80350127856915, 2.3174291790534154, Bagneux - Lucie Aubrac \n',
 '48.80991722995029, 2.317686671122904, Barbara \n',
 '48.81862126138922, 2.319703692309543, Mairie de Montrouge \n',
 "48.823057475209445, 2.32596933256901, Porte d'Orléans \n",
 '48.828566875440806     , 2.3272997082613656, Alésia \n',
 ...
  '48.893987070119195, 2.3478658138599524, Simplon\n',
 '48.898311313785584, 2.345121624270384, Porte de Clignancourt\n']

On veut obtenir une liste de listes

[['48.80350127856915', '2.3174291790534154', 'Bagneux - Lucie Aubrac'], ['48.80991722995029', '2.317686671122904', 'Barbara'], ['48.81862126138922', '2.319703692309543', 'Mairie de Montrouge'], ['48.823057475209445', '2.32596933256901', "Porte d'Orléans"], ['48.828566875440806', '2.3272997082613656', 'Alésia'], 
...
['48.893987070119195', '2.3478658138599524', 'Simplon'], ['48.898311313785584', '2.345121624270384', 'Porte de Clignancourt']]
def clear_lines(lines):
    pass

lines = read_file("datas/ligne4.txt")
lines = clear_lines(lines)

Voici une correction possible.

def clear_lines(lines):
    clines = []
    for l in lines:
        clines.append(split_line(l))
    return clines

Réorganisez votre code : Nous avons maintenant un bon début. Réorganisez votre fichier. Vous ne devez conserver que le minimum. Au début du fichier vous devez avoir toutes les définitions de fonctions et à la toute fin, le code exécuté.

Il est important de ne pas garder les tests intermédiaires. Les commentaires doivent être brefs, rien de supperflus. Il faut que ce soit simple à lire.

Voici ce que devrait contenir votre fichier main.py à ce stade.

# Lecture d'un fichier de données texte.
def read_file(name):
    lines = ""
    with open(name) as f:
        lines = f.readlines()
    return lines
    

# Séparation des données d'une ligne et suppression des caractères blancs.
def split_line(l):
    return [e.strip() for e in l.split(",")]
    

# Création d'une liste contenant toutes les informations des stations.
def clear_lines(lines):
    clines = []
    for l in lines:
        clines.append(split_line(l))
    return clines
    
lines = read_file("datas/ligne4.txt")
lines = clear_lines(lines)
print(lines)

15.3. Partie 2, préparer les données#

Certaines données sont corrompues. Dans un premier temps, nous allons nous contenter de les supprimer.

Question : Réalisez une fonction filter_incomplete_lines(lines) qui prend en paramètre une liste de stations et renvoie une nouvelle liste, contenant uniquement les stations pour lesquelles toutes les informations sont complètes. Autrement dit, une station de ce type ['', '2.329089014624251', 'Vavin'] doit être supprimée car la latitude n’est pas renseignée.

Il devrait rester 21 stations après cette oppération.

def filter_incomplete_lines(lines):
    pass

lines = read_file("datas/ligne4.txt")
lines = clear_lines(lines)
lines = filter_incomplete_lines(lines)

Voici une correction possible.

#version simple
def filter_incomplete_lines(lines):
    clines = []
    for l in lines:
        if l[0] != "" and l[1] != "" and l[2] != "":
            clines.append(l)
    return clines

#version badass
def filter_incomplete_lines(lines):
    return [l for l in lines if l[0] != "" and l[1] != "" and l[2] != ""]

Parmi les données concernant les stations, la latitude et la longitude sont des nombres à virgule. Actuellement les données sont stockées sous forme de chaines de caractères, il est nécessaire de les convertir.

Question : Réalisez une fonction convert_to_float(lines) qui prend en paramètre une liste de stations (comme celle obtenue après la fonction filter_incomplete_lines) et qui renvoie une nouvelle liste de stations dans laquelle, la latitude et la longitude ont été transformé en nombres à virgule.

def convert_to_float(lines):
    pass

lines = read_file("datas/ligne4.txt")
lines = clear_lines(lines)
lines = filter_incomplete_lines(lines)
lines = convert_to_float(lines)

Voici une correction possible.

def convert_to_float(lines):
    clines = []
    for l in lines:
        clines.append([float(l[0]), float(l[1]), l[2]])
    return clines

Réorganisez votre code : voici à quoi devrait ressembler votre code.

# Lecture d'un fichier de données texte.
def read_file(name):
    lines = ""
    with open(name) as f:
        lines = f.readlines()
    return lines


# Séparation des données d'une ligne et suppression des caractères blancs.
def split_line(l):
    return [e.strip() for e in l.split(",")]


# Création d'une liste contenant toutes les informations des stations.
def clear_lines(lines):
    clines = []
    for l in lines:
        clines.append(split_line(l))
    return clines

# Suppression des lignes incomplètes.
def filter_incomplete_lines(lines):
    return [l for l in lines if l[0] != "" and l[1] != "" and l[2] != ""]

# Conversion de la latitude et longitude en float.
def convert_to_float(lines):
    clines = []
    for l in lines:
        clines.append([float(l[0]), float(l[1]), l[2]])
    return clines

lines = read_file("datas/ligne4.txt")
lines = clear_lines(lines)
lines = filter_incomplete_lines(lines)
lines = convert_to_float(lines)

print(lines)

15.4. Partie 3, création de la carte#

C’est le moment d’utiliser folium et donc d’importer le module. La bonne pratique veut que les imports soient fait dans les toutes premières lignes d’un fichier Python. Ajoutez donc l’import import folium. Exécutez votre programme. La première fois avec Replit, vous devriez voir l’installation du package.

Si l’installation du module fonctionne correctement, nous allons pouvoir tester folium avec les trois lignes suivantes. Un fichier ligne4.html devrait être créé. Téléchargez et ouvrez le avec navigateur. Il devrait contenir la carte de Paris.

import folium
m = folium.Map((48.85381285231611, 2.3449980166801163), zoom_start=13)
m.save("ligne4.html")

Nous allons tenter d’ajouter un unique marqueur sur la carte pour le moment.

Question : Réalisez une fonction add_marker(l, map) qui prend en paramètre la liste des informations concernant une station, ainsi qu’une carte folium et ajoute sur celle-ci un marqueur aux bonnes coordonnées GPS et avec le nom de la station (dans le popup).

Voici pour mémoire la manière dont on ajoute un marqueur. Bien qu’on ne comprenne pas encore les subtilités de cette fonction, vous devriez pouvoir l’adapter pour votre usage.

folium.Marker(
    location=(48, 2),
    popup="nom",
    icon=folium.Icon(color="blue"),
  ).add_to(map)
def add_marker(l, m):
    pass

m = folium.Map((48.85381285231611, 2.3449980166801163), zoom_start=13)
add_marker(lines[0], m)
m.save("ligne4.html")

Voici une correction possible.

def add_marker(l, m):
    lat = l[0]
    lon = l[1]
    name = l[2]
    folium.Marker(
        location=(lat, lon),
        popup=name,
        icon=folium.Icon(color="blue"),
    ).add_to(m)

Nous avons tout ce qu’il nous faut pour ajouter toutes les stations de métro.

Question : Créez une fonction add_markers(lines, m) qui prend en paramètre une liste de stations ainsi qu’une carte folium. La fonction ajoute tous les marqueurs de stations sur la carte.

def add_markers(lines, m):
    pass

m = folium.Map((48.85381285231611, 2.3449980166801163), zoom_start=13)
add_markers(lines, m)
m.save("ligne4.html")

Voici une correction possible.

def add_markers(lines, m):
    for l in lines:
        add_marker(l, m)

Réorganiser votre code : voici à quoi il devrait ressembler.

import folium

# Lecture d'un fichier de données texte.
def read_file(name):
    lines = ""
    with open(name) as f:
        lines = f.readlines()
    return lines

# Séparation des données d'une ligne et suppression des caractères blancs.
def split_line(l):
    return [e.strip() for e in l.split(",")]

# Création d'une liste contenant toutes les informations des stations.
def clear_lines(lines):
    clines = []
    for l in lines:
        clines.append(split_line(l))
    return clines

# Suppression des lignes incomplètes.
def filter_incomplete_lines(lines):
    return [l for l in lines if l[0] != "" and l[1] != "" and l[2] != ""]

# Conversion de la latitude et longitude en float.
def convert_to_float(lines):
    clines = []
    for l in lines:
        clines.append([float(l[0]), float(l[1]), l[2]])
    return clines

# Ajout du marqueur d'une station
def add_marker(l, m):
    lat = l[0]
    lon = l[1]
    name = l[2]
    folium.Marker(
        location=(lat, lon),
        popup=name,
        icon=folium.Icon(color="blue"),
    ).add_to(m)

# Ajout de tous les marqueurs
def add_markers(lines, m):
    for l in lines:
        add_marker(l, m)

lines = read_file("datas/ligne4.txt")
lines = clear_lines(lines)
lines = filter_incomplete_lines(lines)
lines = convert_to_float(lines)

m = folium.Map((48.85381285231611, 2.3449980166801163), zoom_start=13)
add_markers(lines, m)
m.save("ligne4.html")

15.5. Partie 4, compléter les données manquantes#

Pour terminer, nous allons dans la mesure du possible, tenter de compléter les données manquantes.

Question : Proposez une fonction complete_lines pour remplacer la fonction filter_incomplete_lines.

Par exemple, plutôt que de supprimer les stations pour lesquels on ne connait pas le nom, on pourrait compléter le nom en indiquant « Station inconnue ».

Proposez une solution pour ce cas, et réfléchissez pour trouver une solution qui serait suffisamment satisfaisantes pour les latitudes et longitudes manquantes.

def complete_lines_(lines):
    pass

lines = read_file("datas/ligne4.txt")
lines = clear_lines(lines)
lines = complete_lines(lines)
lines = convert_to_float(lines)

Voici une correction possible.

#version de base
def complete_lines_(lines):
    clines = []
    for l in lines:
        if l[0] != "" and l[1] != "":
            clines.append(l)
            if l[2] == "":
                clines[-1][2] = "Station inconnue"
    return clines


#version badass, Interpolation des positions manquantes à partir des deux stations voisines
# (quand cela est possible)
def complete_lines(lines):
    clines = []
    for l in lines:
        clines.append(l)
        if l[2] == "":
            clines[-1][2] = "Station inconnue"

    for i in range(len(clines)):
        if clines[i][0] == "":
            if i > 0 and clines[i - 1][0] != "":
                if i < len(clines) - 1 and clines[i + 1][0] != "":
                    clines[i][0] = (float(clines[i - 1][0]) +
                                    float(clines[i + 1][0])) / 2

    for i in range(len(clines)):
        if clines[i][1] == "":
            if i > 0 and clines[i - 1][1] != "":
                if i < len(clines) - 1 and clines[i + 1][1] != "":
                    clines[i][1] = (float(clines[i - 1][1]) +
                                    float(clines[i + 1][1])) / 2

    return clines

Réorganiser votre code : voici à quoi il devrait ressembler au final. On a ajouté une fonction main qui regroupe tout le comportement du programme et est appelée une unique fois.

import folium

# Lecture d'un fichier de données texte.
def read_file(name):
    lines = ""
    with open(name) as f:
        lines = f.readlines()
    return lines

# Séparation des données d'une ligne et suppression des caractères blancs.
def split_line(l):
    return [e.strip() for e in l.split(",")]

# Création d'une liste contenant toutes les informations des stations.
def clear_lines(lines):
    clines = []
    for l in lines:
        clines.append(split_line(l))
    return clines

# Suppression des lignes incomplètes.
def filter_incomplete_lines(lines):
    return [l for l in lines if l[0] != "" and l[1] != "" and l[2] != ""]

# Interpolation des positions manquantes à partir des deux stations voisines (quand cela est possible)
def complete_lines(lines):
    clines = []
    for l in lines:
        clines.append(l)
        if l[2] == "":
            clines[-1][2] = "Station inconnue"

    for i in range(len(clines)):
        if clines[i][0] == "":
            if i > 0 and clines[i - 1][0] != "":
                if i < len(clines) - 1 and clines[i + 1][0] != "":
                    clines[i][0] = (float(clines[i - 1][0]) +
                                    float(clines[i + 1][0])) / 2

    for i in range(len(clines)):
        if clines[i][1] == "":
            if i > 0 and clines[i - 1][1] != "":
                if i < len(clines) - 1 and clines[i + 1][1] != "":
                    clines[i][1] = (float(clines[i - 1][1]) +
                                    float(clines[i + 1][1])) / 2

    return clines

# Conversion de la latitude et longitude en float.
def convert_to_float(lines):
    clines = []
    for l in lines:
        clines.append([float(l[0]), float(l[1]), l[2]])
    return clines

# Ajout du marqueur d'une station
def add_marker(l, m):
    lat = l[0]
    lon = l[1]
    name = l[2]
    folium.Marker(
        location=(lat, lon),
        popup=name,
        icon=folium.Icon(color="blue"),
    ).add_to(m)

# Ajout de tous les marqueurs
def add_markers(lines, m):
    for l in lines:
        add_marker(l, m)

def main():
    lines = read_file("../_static/datas/ligne4.txt")
    lines = clear_lines(lines)
    lines = complete_lines(lines)
    lines = convert_to_float(lines)

    m = folium.Map((48.85381285231611, 2.3449980166801163), zoom_start=13)
    add_markers(lines, m)
    m.save("ligne4.html")

main()
import folium

# Lecture d'un fichier de données texte.
def read_file(name):
    lines = ""
    with open(name) as f:
        lines = f.readlines()
    return lines

# Séparation des données d'une ligne et suppression des caractères blancs.
def split_line(l):
    return [e.strip() for e in l.split(",")]

# Création d'une liste contenant toutes les informations des stations.
def clear_lines(lines):
    clines = []
    for l in lines:
        clines.append(split_line(l))
    return clines

# Suppression des lignes incomplètes.
def filter_incomplete_lines(lines):
    return [l for l in lines if l[0] != "" and l[1] != "" and l[2] != ""]

# Interpolation des positions manquantes à partir des deux stations voisines (quand cela est possible)
def complete_lines(lines):
    clines = []
    for l in lines:
        clines.append(l)
        if l[2] == "":
            clines[-1][2] = "Station inconnue"

    for i in range(len(clines)):
        if clines[i][0] == "":
            if i > 0 and clines[i - 1][0] != "":
                if i < len(clines) - 1 and clines[i + 1][0] != "":
                    clines[i][0] = (float(clines[i - 1][0]) +
                                    float(clines[i + 1][0])) / 2

    for i in range(len(clines)):
        if clines[i][1] == "":
            if i > 0 and clines[i - 1][1] != "":
                if i < len(clines) - 1 and clines[i + 1][1] != "":
                    clines[i][1] = (float(clines[i - 1][1]) +
                                    float(clines[i + 1][1])) / 2

    return clines

# Conversion de la latitude et longitude en float.
def convert_to_float(lines):
    clines = []
    for l in lines:
        clines.append([float(l[0]), float(l[1]), l[2]])
    return clines

# Ajout du marqueur d'une station
def add_marker(l, m):
    lat = l[0]
    lon = l[1]
    name = l[2]
    folium.Marker(
        location=(lat, lon),
        popup=name,
        icon=folium.Icon(color="blue"),
    ).add_to(m)

# Ajout de tous les marqueurs
def add_markers(lines, m):
    for l in lines:
        add_marker(l, m)

def main():
    lines = read_file("../_static/datas/ligne4.txt")
    lines = clear_lines(lines)
    lines = complete_lines(lines)
    lines = convert_to_float(lines)
    print(lines)

    m = folium.Map((48.85381285231611, 2.3449980166801163), zoom_start=13)
    add_markers(lines, m)
    m.save("ligne4.html")

#main()