Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Licence CC BY-NC-ND, Thierry Parmentelat

intérêts de ce TP

introduction

les données

on vous a préparé deux jeux de données qui décrivent le métro parisien

Loading...
Loading...

SOURCE : à l’origine la source de ces données est la RATP et plus précisément ceci, prétraité par nos soins

DISCLAIMER comme vous le verrez ces données ne sont pas parfaites, mais dans l’ensemble c’est un niveau de qualité suffisant pour notre propos d’aujourd’hui.

ce qu’on veut en faire

  1. dessiner le réseau sur une carte

  2. s’en servir de support pour visualiser deux algorithmes de parcours de graphe

comment on se propose de le faire

  1. construire une structure de données pour

  1. afficher la carte avec la librairie folium

  2. parcours :

  3. implémenter les 2 parcours de graphe

  4. numéroter les stations (en partant de Chatelet) selon les deux parcours

  5. afficher les résultats

les algorithmes de parcours

Il y a deux algorithmes standard pour parcourir un graphe à partir de l’un de ses sommets :

intuitivement, en partant de cet échantillon (ici un simple DAG - Directed Acyclic Graph) :

DFS donnerait l’énumération suivante (les sauts de ligne ne sont pas significatifs) :

v v1 v11 v111 v112 
v12 v113
v2 v21 v211 v212
v22 v213

alors que BFS verrait au contraire :

v v1 v2
v11 v12 v21 v22
v111 v121 v122 v211 v212 v222

pour simplifier au maximum les deux algorithmes de parcours que l’on veut implémenter, on peut les décrire informellement de la façon suivante :

ce qui est assez remarquable, c’est que les deux ordres de parcours (DFS et BFS) peuvent être toutes les deux réalisés avec ce même algorithme, la seule chose qui change entre les deux étant l’ordre dans lequel waiting_area “rend” les choses qu’on y insère; ainsi

c’est ce que nous visualiserons à la fin de ce TP

structures de données - intro

on va représenter le graphe avec plusieurs classes d’objet :

spécifications

specs (1) : construction du graphe

voici pour commencer le code qu’on aimerait pouvoir écrire

ainsi une fois qu’on aura implémenté la classe Graph (et la classe Node sous-jacente) on pourra construire notre graphe en écrivant

metro = build_graph(stations, hops)

specs (2) : dessin de la carte

pour ce qui est d’afficher le réseau sur une carte, le code qu’on va vouloir écrire ressemble à ceci (on reparlera plus en détail de la librairie folium en temps voulu) :

def build_map(metro: Graph, show_labels=True):

    map = folium.Map(...)

    for node in metro.iter_nodes():
        # si le noeud a un label
        if show_labels and node.label:
            # l'afficher à la position du noeud
            # donc on a besoin d'accéder à
            node.latitude, node.longitude, node.label

    # quand on itère sur les arêtes on retourne
    # un triplet avec le numéro de ligne
    # qu'on utilisera ici pour trouver la couleur attachée à chaque ligne
    for node, neighbour, line in metro.iter_edges():
        # pour tracer un trait entre les deux
        # on a besoin d'accéder à
        (node.latitude, node.longitude),
        (neighbour.latitude, neighbour.longitude)

    return map

specs (3) : parcours

en ce qui concerne les algorithmes de parcours, compte tenu de ce qu’on a vu plus haut, le code qu’on va vouloir écrire aura besoin aussi d’itérer sur les voisins d’un noeud, et ressemblera schématiquement à ceci :

specs : résumé

pour résumer les besoins sur les classes Graph et Node, on va vouloir être en mesure d’écrire ce genre de choses:


# création
node = graph.add_node(station)

graph.add_edge(
   from_station_id, to_station_id, line)

# parcours des noeuds
for node in graph.iter_nodes():
   # accès à partir d'un noeud
   node.station, node.label
   node.latitude, node.longitude

# parcours des arêtes
for node, neighbour, line in graph.iter_edges():
   ...

for neighbour, line in node.iter_edges():
   ...

et on peut ajouter quelques fonctions de confort

# le nombre de noeuds
len(graph) 

# le nombre d'arêtes dans un graphe
graph.nb_edges() 

# trouver un noeud dans le graphe
graph.find_node_from_station_id(station_id)

# le nombre d'arêtes 
node.nb_edges() 

implémentation

le type Station

pour le type Station on n’a rien à écrire, c’est l’intérêt d’avoir séparé la couche ‘données’ (dataframe) de la couche ‘connectivité’ (Graph), un objet de type Station est en fait une instance de pandas.Series

et rappelez-vous, pour accéder à une dataframe on peut utiliser:

du coup, voici comment utiliser les objets de type Station

latitude 48.887086 longitude 2.349370 Name: 2152, dtype: float64
np.float64(48.88708628305045)
OOPS - <class 'AttributeError'> : 'Series' object has no attribute 'station_id'
np.int64(2152)
np.int64(2152)
pandas.Series

le type Node

pour modéliser les relations entre stations, on enrobe le type Station avec un objet Node qui possède une référence vers une station, et aussi vers les noeuds voisins

la première idée consisterait à garder dans un attribut self.neighbours un set d’objets Node (c’est d’ailleurs ce qu’on a représenté sur le diagramme simplifié ci-dessus)

mais il se trouve que dans hops.txt on nous donne aussi le numéro de la ligne de métro qui connecte deux stations, on va donc vouloir attacher à chaque lien ce numéro de ligne, et du coup un ensemble n’est sans doute pas ce qu’il y a de mieux...

construction du graphe

maintenant on devrait pouvoir construire le graphe

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[17], line 3
      1 metro = build_graph(stations, hops)
      2 
----> 3 print(f"notre graphe a {len(metro)} stations et {metro.nb_edges()} liens")

TypeError: object of type 'Graph' has no len()
Ellipsis

dessiner le graphe sur une carte

on va utiliser la librairie folium pour afficher les cartes

pas de code à écrire de votre part dans cette partie, mais vous pouvez prendre le temps de voir comment c’est fait

les couleurs des lignes

{'1': '#f2c931', '2': '#216eb4', '3': '#9a9940', '3B': '#89c7d6', '4': '#bb4d98', '5': '#de8b53', '6': '#79bb92', '7': '#df9ab1', '7B': '#79bb92', '8': '#c5a3ca', '9': '#cdc83f', '10': '#dfb039', '11': '#8e6538', '12': '#328e5b', '13': '#89c7d6', '14': '#67328e'}

afficher les labels

pour mettre en évidence une station (un peu fastidieux en folium)

Chatelet au centre de la carte

ma première carte en folium

On n’aura besoin que des classes suivantes

Loading...

contruire une folium.Map à partir du graphe

Nous voulons visualiser les stations, et les lignes. En option (si show_labels=True), on affichera aussi l’attribut label des objets Node lorsqu’il est défini.

à ce stade si votre code pour Node et Graph est correct vous pouvez voir ici la carte du réseau avec la station Chatelet numérotée 0

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[27], line 2
      1 # on met au moins un label pour voir l'effet
----> 2 metro.find_node_from_station_id(chatelet_station_id).label = '0'
      3 
      4 build_map(metro)

AttributeError: 'NoneType' object has no attribute 'label' and no __dict__ for setting new attributes

parcours : implémentation et illustration

rappel des objectifs

nous voulons écrire un generateur qui implémente les deux stratégies de parcours, à partir d’un sommet du graphe

bien entendu, seuls les sommets accessibles seront parcourus, ce qui dans notre cas n’a pas d’importance puisque le graphe du métro est connexe

nous avons vu qu’on pouvait unifier les deux parcours, en utilisant une file d’attente FIFO ou FILO selon le parcours que l’on veut faire

nous avons simplement besoin dans les deux cas d’un objet Storage qui implémente store() et retrieve() avec la bonne politique; aussi de manière accessoire on a besoin de pouvoir tester si une file est vide ou non, c’est à dire de pouvoir écrire while storage: ...

FIFO / FILO

à vous d’implémenter les deux classes suivantes

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[30], line 5
      1 # pour vérifier
      2 fifo = Fifo()
      3 for i in range(1, 4):
      4     fifo.store(i)
----> 5 while fifo:
      6     print(f"retrieve → {fifo.retrieve()}")

TypeError: 'NoneType' object cannot be interpreted as an integer
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[31], line 5
      1 # pour vérifier
      2 filo = Filo()
      3 for i in range(1, 4):
      4     filo.store(i)
----> 5 while filo:
      6     print(f"retrieve → {filo.retrieve()}")

TypeError: 'NoneType' object cannot be interpreted as an integer

parcours générique

vous avez à présent toutes les informations pour écrire vous-même la fonction suivante, qui va nous servir ensuite à implémenter les deux parcours (voyez les cellules suivantes)

les deux parcours spécifiques

illustration

pour illustrer les deux parcours, on va simplement utiliser l’attribut label des noeuds, et y ranger l’ordre dans lequel se fait le parcours

si tout se passe bien vous allez voir le réseau du métro parisien, avec les stations numérotées selon ces deux algorithmes

depth-first scan

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[35], line 2
      1 # labelling all stations according to a DFS scan
----> 2 for index, node in enumerate(DFS(metro, chatelet_station)):
      3     node.label = str(index)
      4 print(f"index={index}")
      5 build_map(metro)

Cell In[33], line 6, in DFS(metro, station)
      3 def DFS(metro, station):
      4     node = metro.find_node_from_station_id(station.name)
      5     storage = Filo()
----> 6     yield from scan(node, storage)

TypeError: 'NoneType' object is not iterable

breadth-first scan

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[36], line 2
      1 # same with a BFS
----> 2 for index, node in enumerate(BFS(metro, chatelet_station)):
      3     node.label = str(index)
      4 print(f"index={index}")
      5 build_map(metro)

Cell In[34], line 6, in BFS(metro, station)
      3 def BFS(metro, station):
      4     node = metro.find_node_from_station_id(station.name)
      5     storage = Fifo()
----> 6     yield from scan(node, storage)

TypeError: 'NoneType' object is not iterable

un dernier mot : instances hashables

vous avez peut-être utilisé un ensemble pour mémoriser dans la fonction scan() les sommets parcourus; en tous cas c’est assez logique d’y penser

je signale à ce sujet que par défaut, une instance de classe peut être insérée dans un ensemble; ce qui peut sembler contredire un peu ce qu’on a vu au sujet des types prédéfinis, à savoir qu’un ensemble ne peut contenir que des objets immutables

en fait par défaut, lorsqu’on insère une instance dans un ensemble, ce qui sert de clé pour le hachage c’est le résultat de id(), c’est-à-dire en gros l’adresse de l’instance

dans le cas qui nous intéresse ici, on a créé des instances de Node, on peut effectivement les mettre dans un ensemble sans trop de précaution, il y a des chances pour que ça fonctionne tout seul (typiquement si on crée une seule instance de Node par station de métro)

mais bon, si on veut être propre (don’t program by coincidence...), il est intéressant de redéfinir 2 dunder (sur la classe Node donc) pour que les recherches dans l’ensemble soient pertinentes; il s’agit des dunder __hash__ et __eq__ que je vous invite à implémenter si vous ne l’avez pas fait.