Toutes les interventions plénières des eurodéputés, en 100 lignes (de code)

Mise à jour, juin 2015 : si ce billet vous intéresse, allez voir le travail de Derek Greene et James P. Cross, qui ont modélisé les documents décrits ci-dessous de manière plus fine que ce que j’avais codé.

Quelques notes tirées de trois conversations sur les sept derniers jours. C’était plus long à décrire qu’à coder.

Commençons par récupérer la liste des eurodéputés, en trois étapes :

  1. La source : l’index complet est disponible en une seule page, ce qui rend les choses faciles. Le zéro final de l’adresse est le numéro de législature ; il y en a 7 au total, et la dernière possède un index des députés un peu plus détaillé, que l’on utilisera peut-être un autre jour.
  2. La méthode : il y a quatre ans, on m’a fait découvrir XPath, qui est une syntaxe servant à sélectionner des éléments dans un document structuré. XPath, c’est un peu comme les expressions régulières, ou découvrir comment trafiquer un PDF sans laisser de traces dans les métadonnées : ça change la vie1.
  3. La syntaxe : le site Internet du Parlement européen est rédigé en HTML propre (car généré automatiquement), avec des éléments bien imbriqués les uns dans les autres : c’est le cas idéal. La source de l’index, consultable dans tout navigateur, donne la syntaxe exacte des éléments utilisés ci-dessous.

Illustration de l’explication ci-dessus :

Screen Shot 2014-05-19 at 9.26.24 PM

À présent, dans R :

  1. On souhaite juste sauvegarder un « annuaire » minimal des 3 000 et quelques eurodéputés, et le sauvegarder dans un fichier qui servira d’index général où chaque eurodéputé est identifiable par trois éléments : le nom complet, inutilisable car plein d’accents et non unique2, l’URL (l’adresse de sa page personnelle), et dans cette URL, un numéro, 4484 par exemple, qui servira d’identifiant unique plus tard.
  2. La fonction de téléchargement va lire la page HTML, en extraire les éléments pertinents à partir de sa structure, et renvoyer tout ça dans un jeu de données. On sauvegardera les données au format CSV, un format en texte simple qui sauvegardera correctement les accents dans les noms si l’on s’assure que le parser les lit en encodage UTF-8.
  3. Petite nuance : quand vous voulez extraire un lien, la syntaxe XPath vous le permet directement ; mais pour extraire le texte d’un lien, le truc sur lequel on clique pour aller au lien en question, il vous faut la « valeur XML », d’où la fonction xmlValue ci-dessous. Le reste du code demande surtout de se renseigner sur XPath, et ce n’est pas compliqué du tout, surtout si l’on apprend par essai-erreur.

Il va donc vous falloir installer puis charger les librairies (a.k.a packages, extensions, plugins, comme vous voulez) jsonlite, plyr et XML, et 20 lignes plus tard, vous avez tous les eurodéputés dans un fichier CSV.

Plus compliqué, obtenir toutes leurs interventions parlementaires :

  1. Sur la page de chaque eurodéputé (exemple), un lien fournit toutes les interventions en séance plénière (exemple). En lisant encore une fois la source de la page, on trouve le petit bout de Javascript qui génère cette information, et surtout l’URL qui renvoie cette information depuis la base de données utilisée en coulisses du site.
  2. L’information est au format JSON, pour lequel il y a non pas un, ni deux, mais trois packages R qui peuvent assurer la conversion vers le format classique des jeux de données R, data.frame. Jeroen Ooms m’a prouvé en cinq lignes que son package était le meilleur, et j’ai pu vérifier depuis qu’il a entièrement raison3.
  3. Les fonctions conditionnelles de type if else, qui permettent par exemple de ne rien faire si aucune donnée n’a été trouvée, servent à contrôler l’exécution du script. Le code ci-dessous contient deux autres structures apparentées, while et try. Quelques détails chiants si vous ne les utilisez pas fréquemment :
    • while est un itérateur habituellement moins préférable que for (ou, dans R, qu’une vectorisation de type *apply), mais qui marche très bien ici pour lister les interventions de chaque eurodéputé(e)4.
    • try permet de continuer l’exécution même si un problème de connexion Internet empêche ponctuellement de lire une page (erreurs 404, 502, 503… amusez-vous bien)5.

Voilà pour le détail, et 30 lignes plus tard, vous pouvez récupérer les interventions de n’importe quel eurodéputé dans n’importe quelle législature (1 à 7), en utilisant son numéro d’identifiant unique (id)6.

Ça se complique encore un peu mais on y est presque. Le workflow final est le suivant :

  1. Pour chaque eurodéputé dans l’index général, extraire le numéro d’identifiant unique de l’adresse de sa page Internet, puis aller chercher ses interventions publiques dans chacune des 7 législatures parlementaires. La partie « pour chaque député » est purement itérative (for), mais la partie « chacune des législatures » est vectorisée : on va de 1 à 7, et on applique à chaque fois la fonction get_cre de 20 lignes décrite plus haut. Le seul bénéfice avéré de la vectorisation dans ce cas réside dans l’économie d’accolades et de lignes de code à gérer.
  2. Il ne reste que les soucis de format. Le premier est très simple : la partie vectorisée du code renvoie une liste, que l’on va aplatir au format data.frame avec rbind.fill, la fonction la plus utile du monde après paste (si si). Enfin, dernier problème : les fichiers JSON utilisent des listes, mais vos jeux de données sont des tableaux (i.e. des matrices avec des attributs) qui utilisent des cellules. Une liste vide est NULL, alors qu’une cellule vide est NA : welcome to the R Inferno ! Heureusement, une fois le problème détecté, il se règle en une ligne, à appliquer à chaque colonne susceptible de contenir des listes/cellules nulles/vides afin de les « purger » — sinon, pas d’écriture possible au format CSV.
  3. Prévoir, dans quelques heures, environ 1700 fichiers de résultats (car tous les eurodéputés n’interviennent pas, loin de là), pour environ 90 MB d’espace disque. Cet exemple est très « léger » et relativement rapide, mais d’expérience, vous avez vraiment intérêt à toujours penser à la taille des résultats sur disque avant de faire quoi que ce soit — à moins d’avoir la trésorerie pour détruire un disque dur de temps à autre7.

Illustration de l’avertissement ci-dessus, tiré d’un autre projet dont les résultats des modélisations font plusieurs gigaoctets de plus que les données brutes, ce qui est logique quand on utilise des simulations ou, comme ici, un bootstrap :

Screen Shot 2014-05-16 at 1.39.12 PM

On termine en écrivant poliment le nombre d’eurodéputés restants à l’écran, et avec 20 lignes de plus, l’index général est mis à profit pour télécharger, dans un dossier records à créer préalablement, les métadonnées de tout ce que les eurodéputés ont raconté en séance plénière depuis qu’il y a des données disponibles, dans des fichiers nommés UID_cre.csvUID est l’identifiant unique de chaque eurodéputé.

Ça fait 100 lignes en tout, avec quelques trucs en plus.

Le résultat à l’écran ressemble à l’illustration ci-dessous, avec des pseudo-barres de progression quand il y a des fichiers en téléchargement (je montre une exécution avec les résultats déjà présents sur le disque, d’où les « 0 » jamais suivis des « 100, 200, 300… » correspondant au téléchargement de chaque page d’interventions). Le code est exécuté dans TextMate via son plugin R, (qui doit exister pour d’autres éditeurs comme SublimeText).

Screen Shot 2014-05-19 at 9.32.46 PM

Sans avoir regardé en détail, les données intégrales ne semblent concerner que les trois dernières législatures. Le même code peut aussi collecter les opinions prononcées en tant que rapporteur, les motions, les questions écrites etc. avec à peu près autant de facilité, et pour rajouter des variables aux eurodéputés eux-mêmes, il suffit d’extraire plus d’informations de leurs pages personnelles—mais cela dépasserait le temps que je voulais passer sur cet exercice.

Je termine en pensant aux vraies feignasses dans mon genre : le code et les données en une seule archive. Add. : et si ça vous intéresse, j’ai aussi un fichier CSV de 400 MB contenant toutes les interventions orales des eurodéputés au cours des législatures 5–7 (1999–2004, 2004–2009 et 2009-2014).

  1. Les métadonnées, c’est ce que la NSA sait sur vous et sur votre grand-mère. Ça sert aussi à détecter des trucs bizarres dans les copies d’étudiants, ou à savoir qui rédige vraiment les rapports publics que vous trouvez parfois dans la presse en ligne. []
  2. Ce menu détail rend les dernières données de VoteWatch inutiles pour cet exercice, mais voici quand même un guide à leur réutilisation avec R, en trois parties : un, deux, trois. []
  3. La vignette du package explique très bien pourquoi, et en pratique, c’est l’argument flatten = TRUE qui fait la moitié du boulot de virer les listes pour mettre des colonnes, histoire que le résultat ressemble moins à un flux qu’à un tableau de données. L’autre moitié du boulot prend une ligne, lire plus haut. []
  4. La variable idx est lue dans le fichier JSON et correspond à la pagination des interventions publiques, qui sont fournies par tranche de 100 interventions en partant de zéro, parce que Javascript compte à partir de zéro. Elle est égale à 100, 200, 300 ... tant qu’il reste des pages d’interventions publiques dans la pagination du site Internet, -1 quand on se trouve sur la dernière page d’interventions. La variable est directement égale à 0 quand il n’y a aucune intervention publique. []
  5. Vu le grand nombre de pages à couvrir, N eurodéputés * P pages d’environ 100 interventions, le script va nécessairement avoir quelques soucis : il faut le réexécuter quelques fois pour arriver à télécharger tout ce qui est possiblement téléchargeable. Je laisse de côté les fonctions de mise à jour, (il suffit d’effacer les fichiers-cibles en amont). []
  6. Par exemple, le président actuel du Parlement est codé 1911, donc la fonction get_cre(1911, 7) renvoie les interventions (nombreuses, car majoritairement procédurales) de son mandat en cours. []
  7. N’imaginez même pas essayer d’écrire les résultats sur une clé USB : ce n’est pas vraiment un support froid, et je garantis à 200% que tout essai sur une clé USB de plus de 8 GB aura pour seul effet de la rendre complètement et définitivement inutilisable. []