Sans inspecter ces messages, auriez-vous soupçonné qu'il manquent tant de données pour la Corse, par exemple? Et si je n'avais pas fait attention à les rendre faciles à inspecter, l'auriez-vous fait quand-même?
On peut d'ailleurs se demander comment il est possible d'avoir tant de points manquants dans les données régionales, mais un seul point manquant dans les données nationales qui devraient, en théorie, être simplement la somme sur les régions. Mais c'est une question que seul le Réseau Sentinelles peut répondre.
* Les limites des workflows pilotés par les fichiers
Snakemake fait partie d'une grande famille de gestionnaires de workflow dont l'ancêtre commun est l'outil =make= qui date de 1976. Le principe de fonctionnement de cette famille est que l'exécution des tâches est pilotée par les noms et les dates de modification des fichiers qui contiennent les données. C'est un principe simple qui permet beaucoup de fléxibilité dans son application, comme par exemple la parallélisation automatique. Mais ce principe a aussi des limitations, et nous en avons rencontré une: la nécessité de fournir une liste explicite des régions dans notre =Snakefile=.
À priori, la fichier de données téléchargé contient la liste exhaustive des régions qu'il faut traiter. On s'attend donc à pouvoir simplement dire "je veux appliquer mes règles à toutes les régions réferencées dans ce fichier". On s'attend bien sûr aussi à devoir fournir du code pour extraire la liste des région dudit fichier. Mais le problème est plus fondamental. Snakemake utilise le =Snakefile= et les fichiers de données réferencées par celui-ci pour déduire quelles tâches il faut exécuter et dans quel ordre. La liste des tâches ne peut donc pas dépendre du /contenu/ d'un tel fichier, et encore moins du contenu d'un fichier qui n'est même pas disponible avant l'exécution de la toute première tâche qui est =download=! Autrement dit, la limitation fondamentale de snakemake est le fait que le graphe de dépendance des tâches est établi avant toute exécution.
Les versions récentes de Snakemake proposent une façon de contourner cette limitation, et je vais vous la montrer. Le principe est qu'il faut dire à Snakemake de reconstruire le graphe des tâches après en avoir exécuté certaines.
Je vais d'abord créer un nouveau repertoire pour ce troisième workflow, et copier les scripts du deuxième qui ne seront pas modifiés:
#+begin_src sh :session *snakemake3* :results output :exports both
# déjà fait: mkdir incidence_syndrome_grippal_par_region_v2
La règle =split_by_region= devient un "checkpoint", ce qui veut dire que Snakemake reconstruit son graphe de tâches /après/ son exécution:
#+begin_src :exports both :tangle incidence_syndrome_grippal_par_region_v2/Snakefile
checkpoint split_by_region:
input:
"data/weekly-incidence-all-regions.csv"
output:
directory("data/weekly-incidence-by-region")
script:
"scripts/split-by-region.py"
#+end_src
La particularité d'un checkpoint est que ses fichiers de sortie ne sont pas connus d'avance. On donne donc seulement le nom d'un répertoire. C'est le répertoire entier qui est consideré le résultat de la tâche. C'est donc le script qui doit le créer:
#+begin_src python :exports both :tangle incidence_syndrome_grippal_par_region_v2/scripts/split-by-region.py
import os
# Read the CSV file into memory
data = open(snakemake.input[0], 'rb').read()
# Decode the Latin-1 character set,
# remove white space at both ends,
# and split into lines.
lines = data.decode('latin-1') \
.strip() \
.split('\n')
# Separate header from data table
comment = lines[0]
header = lines[1]
table = [line.split(',') for line in lines[2:]]
# Find all the regions mentioned in the table
regions = set(record[-1] for record in table)
# Create the output directory
directory = snakemake.output[0]
if not os.path.exists(directory):
os.makedirs(directory)
# Write CSV files for each region
for region in regions:
filename = os.path.join(directory, region + '.csv')
with open(filename, 'w') as output_file:
# The other scripts expect a comment in the first line,
# so write a minimal one to make them happy.
output_file.write('#\n')
output_file.write(header)
output_file.write('\n')
for record in table:
# Write only the records for right region
if record[-1] == region:
output_file.write(','.join(record))
output_file.write('\n')
#+end_src
Cette réorganisation des fichiers nécessite une petite modification des entrées de la règle =preprocess=:
#+begin_src :exports both :tangle incidence_syndrome_grippal_par_region_v2/Snakefile
Enfin, c'est la règle =peak_years= qui doit changer parce qu'elle doit construire la liste des fichiers d'entrées à partir des sorties du checkpoint =split_by_regions=. Ceci nécessite du code, mais snakemake permet de définir des fonctions Python dans le =Snakefile=:
#+begin_src :exports both :tangle incidence_syndrome_grippal_par_region_v2/Snakefile
Ceci nécessite quelques explications. Sous "input", j'ai donné le nom d'une fonction à la place d'une liste de fichiers. Cette fonction retourne la liste dont snakemake a besoin. Pour construire la liste, elle utilise =expand= que nous avons déjà vu avant. Mais la liste des noms de région n'est plus une constante définie dans le =Snakefile=. Elle est le résultat d'une recherche de fichiers qui correspondent à "{region}.csv", dans le répertoire peuplé par la règle =split_by_region=.
Nous voyons donc que le prix à payer pour laisser le workflow trouver la liste des régions est une plus grande complexité du workflow. Il faut décider au cas par cas si c'est réellement avantageux.
Pour terminer, lançons ce troisième workflow et comparons s'il donne les mêmes résultats que le deuxième!
#+begin_src sh :session *snakemake3* :results output :exports both