Avez-vous déjà essayé de rejoindre Besançon depuis Nancy en train, ou plus précisément les gares de Nancy-Ville et Besançon-Viotte ? Ces deux gares, distantes de 160km, ne bénéficient pas d’une ligne ferroviaire directe.
La faute à la chaîne des Vosges qui limite les traversées possibles entre Lorraine et Franche-Comté et à l’enclavement de la Haute-Saône qui ne dispose que de peu de lignes encore actives.
En effet, les grandes lignes autour (projets LGV Est et LGV Rhin-Rhône) ne font que contourner ce département qui se retrouve isolé et mal desservi.
Plusieurs possibilités se présentent alors :
- Le tracé le plus direct, avec correspondances à Épinal et Belfort, en un peu moins de 4h de trajet pour les plus courts.
- Le plus rapide, qui peut se faire en 3h30 en contournant par l’ouest avec correspondance à Dijon.
- Le plus confortable, un TGV direct de Nancy à Besançon TGV (puis correspondance TER), bénéficiant de la LGV à partir de Strasbourg qui permet de faire le trajet en 4h.
- Quelques variantes à ces précédents choix (passage par Metz, correspondance à Strasbourg et Mulhouse, liaison Dijon - Besançon TGV)
J’ai volontairement omis les trajets qui proposent de passer par Paris.
Bref, c’est assez compliqué et ça emprunte différents types de trains, difficile alors de savoir synthétiquement quels peuvent en être les horaires.
Mon objectif était donc de pouvoir générer une fiche horaire Nancy-Besançon me présentant ces différentes possibilités.
- Passer ma route
- Parsons vite
- C'est quand qu'on va où ?
- Colore le monde
- Chaque jour de plus
- J’entends siffler le train
- Ne partez pas sans moi
- On avance, on avance, on avance
Passer ma route
Pour arriver à générer mes fiches horaires idéales, j’avais donc quelques contraintes à remplir.
Il fallait d’abord que je puisse agréger les données de plusieurs trains, la partie assez ennuyante du travail.
Je ne me suis pas intéressé aux API proposées, j’ai « simplement » utilisé le site oui.sncf pour explorer les trajets possibles suivant les jours et les reporter dans un fichier CSV.
Je notais donc une ligne pour chaque train (ex: TER 835013), avec gares et heures de départ/arrivée, ainsi que les jours de circulation.
Ce fichier comprenait alors les données brutes des trains, à partir desquelles je voulais élaborer moi-même les trajets, en groupant entre-elles les correspondances et en les ordonnant par heure de départ.
Il est aussi nécessaire de les filtrer par jours, pour n’avoir que des trajets cohérents selon le jour de la semaine.
Je voulais aussi garder une certaine liberté de choix et ainsi ne pas imposer une correspondance en TGV quand un TER était possible sur le même trajet dans les mêmes heures. Ça faisait surtout partie du travail de sélection des trains à mettre dans le fichier CSV, mais c’était aussi une contrainte à prendre en compte : plusieurs trains pour un même trajet peuvent faire partie d’un même groupe de correspondances.
Enfin, je souhaitais obtenir une « belle » présentation.
Alors entendons-nous sur le mot : il s’agit juste d’avoir les données correctement représentées, sous la forme d’un tableau HTML ou markdown, afin que les fiches soient lisibles.
Donc une colonne correspondant à chaque arrêt et les arrêts proprement ordonnés.
Parsons vite
J’aurais pu faire appel à une API pour exécuter les requêtes et agréger directement les données liées aux différents trajets… J’aurais pu.
Mais ce n’était pas mon but, je ne voulais pas commencer à me perdre dans ces documentations et ne jamais venir à bout de mon projet.
Alors j’allais utiliser une technique plus artisanale : faire une recherche pour chaque jour de la semaine sur chacun des trajets, et inscrire tout ça dans un fichier CSV.
Il me fallait connaître les gares et heures de départ et d’arrivée, mais aussi les intermédiaires (au cas où d’autres correspondances seraient possibles), ainsi que les jours de la semaine où le train circulait.
J’enregistrais aussi le type de train et son numéro pour me permettre de le retrouver ensuite.
J’avais choisi d’ignorer toute contrainte liée aux jours fériés, aux vacances scolaires ou au travaux, je considérais qu’un train roulerait toujours de la même manière tous les lundis de l’année par exemple.
Je me suis donc retrouvé avec un fichier CSV assez conséquent, où j’avais déjà pré-filtré les trajets ne menant à rien (je n’y faisais pas figurer tous les Nancy-Épinal en sachant qu’il n’y avait pas de correspondance derrière) dont voici un extrait :
TER,88503,S,nancy,06:50,metz,07:27
TGV,9877,S,metz,07:58,strasbourg,09:05,mulhouse,10:01,besancon_tgv,10:46
TER,894518,LMaMeJVS,besancon_tgv,11:04,besancon_viotte,11:19
TER,835013,LMaMeJV,nancy,07:14,strasbourg,08:42
La suite consistait donc à parser ce fichier et à construire les données liées au train.
J’implémentais pour ça un type Train
reprenant toutes les caractéristiques listées au-dessus, qui représentait une liste d’arrêts avec horodatage.
J’y avais ajouté quelques raccourcis pour accéder directement au départ et au terminus.
Le parsing en lui-même était assez trivial : module csv
, format de l’heure et correspondance des jours.
Mais maintenant j’avais sous la main une liste de trains exploitables avec leurs arrêts.
C'est quand qu'on va où ?
Une fiche horaire présente les trains sous la forme d’un tableau, et dispose donc d’une liste d’arrêts. Cette liste est facile à obtenir dans le cas d’une unique ligne puisque les arrêts sont déjà linéaires, mais ce n’est pas du tout le cas de la carte que je présente en introduction.
Comment linéariser les différents arrêts entre des lignes de train qui partent dans différentes directions ?
L’idée qui me vint était de trier les arrêts selon leur « distance » par rapport au point de départ.
Le point de départ était facilement identifiable : c’est le seul arrêt où n’arrive aucun train (ils ne peuvent qu’en partir).
Partant de là, je calculais la distance comme le temps minimum (le nombre d’arrêts et le temps total) pour rejoindre une gare depuis le point de départ, de proche en proche.
Je procédais pour ça en deux temps. D’abord en identifiant pour chaque gare le trajet minimal qui permettait de s’y rendre depuis une autre gare (quelle qu’elle soit). Puis je normalisais cela pour calculer la distance depuis le point de départ plutôt que depuis la gare précédente. Je ne tenais pas compte des temps de correspondance.
Il fallait faire attention aux temps d’arrivée qui pouvaient parfois être le lendemain matin, et donc à une heure antérieure à celle de départ. Cela se gérait bien avec une condition sur ce cas particulier.
J’obtenais donc une liste triée de mes arrêts mais je remarquais vite un petit problème : les listes pour le trajet aller et retour n’étaient pas cohérentes. J’aurais pensé que l’une serait simplement l’inverse de l’autre, mais c’aurait justement été trop simple.
J’ai assez vite identifié le problème, il s’agissait des temps de trajet pour Dijon et Mulhouse.
En effet, il faut moins de temps depuis Nancy pour se rendre à Dijon qu’à Mulhouse… mais c’est vrai aussi depuis Besançon !
Mulhouse se retrouvait donc systématiquement après Dijon dans la liste des arrêts, à l’aller comme au retour.
Pour corriger le problème, j’ai donc calculé les « distances » minimales jusqu’à l’arrivée en plus des distances depuis le départ, et je combinais les deux en les soustrayant. Le point d’arrivée était lui aussi facilement identifiable puisqu’aucun train n’en partait.
Cette fois-ci, j’avais une liste ordonnée et cohérente1 des arrêts !
Nancy |
Metz |
Épinal |
Strasbourg |
Belfort Ville |
Mulhouse |
Dijon |
Besancon TGV |
Besancon Viotte |
- Cohérente dans le sens où elle reste similaire dans un sens et dans l’autre. On remarque toujours quelques « bizarreries » comme le fait que Belfort apparaisse avant Mulhouse.↩
Colore le monde
Je pouvais maintenant entrer dans le vif du sujet, à savoir grouper les trajets entre-eux pour obtenir des itinéraires Nancy↔Besançon.
Je me souvenais pour cela d’un algorithme de construction de labyrinthe qui consistait à colorer les cases puis à les fusionner en groupes au fur et à mesure que des murs étaient ouverts : https://fr.wikipedia.org/wiki/Mod%C3%A9lisation_math%C3%A9matique_de_labyrinthe#Fusion_al%C3%A9atoire_de_chemins
Je pourrais utiliser le même principe pour mes trains, en les fusionnant par groupe avec les autres trains environnants dans les mêmes gares.
Il s’agissait en fait de l’algorithme Union-Find dont j’ai fait une implémentation naïve.
Je voulais néanmoins en faire quelque chose de générique que je pourrais réutiliser dans un autre contexte, et j’ai donc mis en place un objet grouper
.
Cet objet, je pourrais le manipuler pour ajouter de nouveaux éléments (chaque élément appartement à un nouveau groupe) et pour fusionner des groupes.
Je pourrais aussi lui demander la liste des groupes et les éléments présents dans chaque groupe.
J’allais pour ça me heurter aux limites de Python car je voulais pouvoir stocker tous types d’objets (même des mutables) dans des ensembles.
Il me fallait alors mettre en place un wrapper (HashInstance
) pour rendre hashable tout objet.
Je pouvais ensuite attribuer des « couleurs » à chaque train et les fusionner avec les trains précédent/suivant (dans chaque gare) pour former des groupe de correspondances. Les trains ne pourraient être mis en correspondance qu’avec des trains du même groupe.
Dans chaque groupe, les trains étaient ordonnés selon leurs heures d’arrivée et de départ, et les groupes entre-eux étaient triés selon leurs heures de départ. La clé d’un groupe correspondant à l’heure de départ minimale dans ce groupe.
Chaque jour de plus
Mais je n’avais pas fini de grouper. Après avoir groupé par correspondances je devais grouper par jours.
En effet, les trains peuvent circuler certains jours et pas les autres et je voulais avoir un affichage condensé de tout ça. J’allais alors identifier les groupes de jours, c’est-à-dire les jours pour lesquels les trains seraient identiques.
Je calculais donc toutes les intersections possibles entre les jours de circulation des différents trains, et j’en déduisais les groupes distincts de jours identiques.
J’entends siffler le train
Avec tout ça bout à bout, je les avais mes horaires ! Je n’avais plus qu’à afficher un beau tableau, gérer quelques arguments et coder la glue autour.
Pour le tableau, je choisissais de pouvoir gérer à la fois un export HTML et un export Markdown et j’implémentais mes fonctions par rapport à ça, sans vouloir les rendre trop spécifiques pour un format particulier.
C’est pourquoi je gardais une fonction iter_table
à part qui ne ferait que produire les lignes du tableau sous forme de listes.
Le choix du format de sortie se retrouvait aussi côté arguments de la ligne de commande où il pouvait être précisé. J’ajoutais une autre option pour unifier les jours, c’est-à-dire présenter sous un même tableau tous les trains quels que soient leurs jours de circulation.
L’ensemble du code de ce projet peut être trouvé sur le dépôt suivant : https://github.com/entwanne/horaires_trains
Ne partez pas sans moi
Après tout ça, je peux maintenant vous montrer les belles fiches horaires que j’ai obtenues. Et comme on le voit, ça laisse assez peu de possibilités pour faire le trajet rapidement.
Nancy-Ville → Besançon-Viotte
Lundi, Mardi, Mercredi, Jeudi, Vendredi
Train | nancy | metz | epinal | strasbourg | belfort_ville | mulhouse | dijon | besancon_tgv | besancon_viotte |
---|---|---|---|---|---|---|---|---|---|
TER 835013 | 07:14 | 08:42 | |||||||
TGV 9877 | 09:05 | 10:01 | 10:46 | ||||||
TER 894518 | 11:04 | 11:19 | |||||||
TER 836380 | 07:54 | 10:29 | |||||||
TER 894213 | 11:09 | 12:05 | |||||||
TER 835015 | 08:14 | 09:41 | |||||||
TER 832361 | 10:21 | 11:14 | |||||||
TGV 6704 | 11:58 | 12:43 | |||||||
TER 894566 | 13:39 | 13:54 | |||||||
TER 835755 | 08:55 | 09:53 | |||||||
TER 894609 | 09:59 | 11:25 | |||||||
TER 894026 | 11:36 | 12:46 | |||||||
TER 839161 | 11:00 | 12:33 | |||||||
TGV 9879 | 13:03 | 13:59 | 14:44 | ||||||
TER 894528 | 14:54 | 15:09 | |||||||
TGV 5537 | 12:10 | 13:36 | 14:51 | 15:39 | |||||
TER 894575 | 15:48 | 16:01 | |||||||
TER 834024 | 12:55 | 13:53 | |||||||
TGV 2571 | 14:05 | 14:47 | |||||||
TER 894619 | 14:59 | 16:25 | |||||||
TER 894040 | 16:36 | 17:46 | |||||||
TER 835021 | 14:15 | 15:41 | |||||||
TGV 9580 | 16:14 | 17:09 | 17:55 | ||||||
TER 894538 | 18:07 | 18:22 | |||||||
TER 835775 | 17:55 | 18:53 | |||||||
TER 894627 | 18:59 | 20:25 | |||||||
TER 894062 | 20:36 | 21:49 | |||||||
TER 836382 | 16:54 | 19:27 | |||||||
TGV 9896 | 19:46 | 20:12 | |||||||
TER 894267 | 19:50 | 20:55 | |||||||
TER 894560 | 20:22 | 20:37 |
Samedi
Train | nancy | metz | epinal | strasbourg | belfort_ville | mulhouse | dijon | besancon_tgv | besancon_viotte |
---|---|---|---|---|---|---|---|---|---|
TER 88503 | 06:50 | 07:27 | |||||||
TGV 9877 | 07:58 | 09:05 | 10:01 | 10:46 | |||||
TER 894518 | 11:04 | 11:19 | |||||||
TER 836380 | 07:58 | 10:29 | |||||||
TER 894213 | 11:09 | 12:05 | |||||||
TGV 6741 | 11:36 | 12:09 | 12:20 | ||||||
TGV 5537 | 12:10 | 13:36 | 14:51 | 15:39 | |||||
TER 894575 | 15:48 | 16:01 | |||||||
TER 835819 | 13:20 | 14:18 | |||||||
TER 894619 | 14:59 | 16:25 | |||||||
TER 894034 | 17:04 | 18:28 | |||||||
TER 835771 | 16:20 | 17:18 | |||||||
TER 894627 | 18:59 | 20:25 | |||||||
TER 894062 | 20:36 | 21:49 | |||||||
Dimanche
Train | nancy | metz | epinal | strasbourg | belfort_ville | mulhouse | dijon | besancon_tgv | besancon_viotte |
---|---|---|---|---|---|---|---|---|---|
TER 835041 | 08:16 | 09:41 | |||||||
TER 96217 | 10:51 | 11:44 | |||||||
TGV 6704 | 12:01 | 12:47 | |||||||
TER 894566 | 13:39 | 13:54 | |||||||
TER 835043 | 11:16 | 12:41 | |||||||
TGV 9879 | 13:03 | 13:59 | 14:44 | ||||||
TER 894528 | 14:54 | 15:09 | |||||||
TGV 5537 | 12:27 | 13:47 | 14:51 | 15:39 | |||||
TER 894575 | 15:48 | 16:01 | |||||||
TER 835045 | 14:15 | 15:43 | |||||||
TGV 9580 | 16:14 | 17:09 | 17:55 | ||||||
TER 894538 | 18:07 | 18:22 | |||||||
TER 835775 | 17:55 | 18:53 | |||||||
TER 894627 | 18:59 | 20:25 | |||||||
TER 894062 | 20:36 | 21:49 | |||||||
TER 836382 | 16:54 | 19:27 | |||||||
TGV 9896 | 19:46 | 20:12 | |||||||
TER 894267 | 19:50 | 20:55 | |||||||
TER 894560 | 20:20 | 20:35 |
Besançon-Viotte → Nancy-Ville
Lundi, Mardi, Mercredi, Jeudi, Vendredi
Train | besancon_viotte | besancon_tgv | dijon | mulhouse | belfort_ville | strasbourg | epinal | metz | nancy |
---|---|---|---|---|---|---|---|---|---|
TER 894517 | 09:55 | 10:10 | |||||||
TGV 9898 | 10:34 | 11:23 | 12:27 | 13:24 | |||||
TER 88526 | 13:32 | 14:11 | |||||||
TER 894208 | 09:56 | 10:50 | |||||||
TER 836385 | 11:00 | 13:29 | |||||||
TER 894521 | 11:40 | 11:52 | |||||||
TGV 9583 | 12:03 | 12:55 | 13:43 | ||||||
TER 835020 | 14:18 | 15:44 | |||||||
TER 894563 | 13:38 | 13:55 | |||||||
TGV 5516 | 14:11 | 15:01 | 16:00 | 17:30 | |||||
TER 894031 | 15:11 | 16:24 | |||||||
TER 894624 | 17:05 | 18:34 | |||||||
TER 834026 | 18:43 | 19:40 | |||||||
TER 894226 | 18:56 | 19:50 | |||||||
TER 836389 | 20:05 | 22:31 | |||||||
Samedi
Train | besancon_viotte | besancon_tgv | dijon | mulhouse | belfort_ville | strasbourg | epinal | metz | nancy |
---|---|---|---|---|---|---|---|---|---|
TER 894517 | 10:12 | 10:27 | |||||||
TGV 9898 | 10:34 | 11:23 | 12:27 | 13:24 | |||||
TER 88526 | 13:32 | 14:11 | |||||||
TER 894208 | 09:56 | 10:50 | |||||||
TER 836385 | 11:00 | 13:29 | |||||||
TER 894521 | 11:40 | 11:52 | |||||||
TGV 9583 | 12:03 | 12:55 | 13:43 | ||||||
TER 835034 | 15:19 | 16:44 | |||||||
TER 894563 | 13:38 | 13:55 | |||||||
TGV 5516 | 14:11 | 15:01 | 16:00 | 17:16 | |||||
TER 894535 | 17:35 | 17:48 | |||||||
TGV 5500 | 18:23 | 19:12 | 20:23 | 21:38 | |||||
TER 88616 | 22:34 | 23:11 | |||||||
TER 894559 | 19:28 | 19:41 | |||||||
TGV 9896 | 20:15 | 21:03 | 22:00 | 22:57 | |||||
BUS 35441 | 23:39 | 01:16 |
Dimanche
Train | besancon_viotte | besancon_tgv | dijon | mulhouse | belfort_ville | strasbourg | epinal | metz | nancy |
---|---|---|---|---|---|---|---|---|---|
TER 894521 | 11:40 | 11:52 | |||||||
TGV 9583 | 12:03 | 12:55 | 13:43 | ||||||
TER 894569 | 12:23 | 12:36 | |||||||
TGV 6705 | 13:31 | 14:17 | |||||||
TER 832326 | 14:34 | 15:39 | |||||||
TGV 2588 | 15:50 | 17:12 | |||||||
TER 839174 | 16:19 | 17:45 | |||||||
TER 894563 | 13:38 | 13:55 | |||||||
TGV 5516 | 14:11 | 15:01 | 16:00 | 17:33 | |||||
TER 894264 | 18:12 | 19:15 | |||||||
TGV 6764 | 18:36 | 18:52 | 19:22 | ||||||
TER 836389 | 20:05 | 22:31 | |||||||
TER 894559 | 19:28 | 19:41 | |||||||
TGV 9896 | 20:15 | 21:03 | 22:00 | 22:57 | |||||
BUS 35441 | 23:39 | 01:16 |
On avance, on avance, on avance
Est-ce que ce projet est fini ? Je dirais que oui parce que je n’ai plus l’intention d’y toucher, d’où ce billet.
Est-ce que j’aurais pu aller plus loin ? Oui aussi.
Déjà il faudrait encore déboguer un coup, je peux avoir sur certains jours des trains qui apparaissent sans correspondances, parce qu’ils figurent dans un groupe dont les correspondances ne sont disponibles que pour d’autres jours.
Ensuite, il faudrait aussi gérer correctement les horaires qui s’étalent sur plusieurs jours. Le problème ne se pose pas trop car il n’y a pas beaucoup de trains qui roulent autour de minuit, mais j’en ai tout de même un dans mes résultats, et son bon fonctionnement est plus dû à un fix un peu crade qu’autre chose.
Enfin, je pourrais utiliser une API pour connaître les itinéraires et télécharger les horaires plutôt que d’avoir à faire ça manuellement, tout en filtrant les trains inutiles (sans correspondance possible).
Mais bon, j’ai eu ce que je voulais et je m’en contente. Ça me fait de beaux tableaux.
Comme on le constate, ordonner correctement des horaires de train n’est pas quelque chose d’aussi facile qu’on aurait pu le penser.
La SNCF elle-même a parfois quelques loupés.
Ressources complémentaires :
- Lignes ferroviaires de Haute-Saône, anciennes et actuelles :
- Railway Routing pour visualiser les lignes de train :