Commit 91dc1d7a authored by Konrad Hinsen's avatar Konrad Hinsen

Nouvelle partie 2 du tutoriel snakemake

parent 34dddc74
rule all:
input:
"data/peak-year-all-regions.txt"
rule download:
output:
"data/weekly-incidence-all-regions.csv"
shell:
"wget -O {output} http://www.sentiweb.fr/datasets/incidence-RDD-3.csv"
checkpoint split_by_region:
input:
"data/weekly-incidence-all-regions.csv"
output:
directory("data/by-region")
script:
"scripts/split-by-region.py"
rule preprocess:
input:
"data/by-region/weekly-incidence-{region}.csv"
output:
data="data/preprocessed-weekly-incidence-{region}.csv",
errorlog="data/errors-from-preprocessing-{region}.txt"
script:
"scripts/preprocess.py"
rule plot:
input:
"data/preprocessed-weekly-incidence-{region}.csv"
output:
"data/weekly-incidence-plot-{region}.png",
"data/weekly-incidence-plot-last-years-{region}.png"
script:
"scripts/incidence-plots.R"
rule annual_incidence:
input:
"data/preprocessed-weekly-incidence-{region}.csv"
output:
"data/annual-incidence-{region}.csv"
script:
"scripts/annual-incidence.R"
def annual_incidence_files(wildcards):
directory = checkpoints.split_by_region.get().output[0]
pattern = os.path.join(directory, "weekly-incidence-{region}.csv")
print("Pattern:", pattern)
return expand("data/annual-incidence-{region}.csv",
region=glob_wildcards(pattern).region)
rule peak_years:
input:
annual_incidence_files
output:
"data/peak-year-all-regions.txt"
script:
"scripts/peak-years.py"
# Read in the data and convert the dates
data = read.csv(snakemake@input[[1]])
# Plot the histogram
png(filename=snakemake@output[[1]])
hist(data$incidence,
breaks=10,
xlab="Annual incidence",
ylab="Number of observations",
main="")
dev.off()
# Read in the data and convert the dates
data = read.csv(snakemake@input[[1]])
names(data) <- c("date", "incidence")
data$date <- as.Date(data$date)
# A function that extracts the peak for year N
yearly_peak = function(year) {
start = paste0(year-1,"-08-01")
end = paste0(year,"-08-01")
records = data$date > start & data$date <= end
sum(data$incidence[records])
}
# The years for which we have the full peak
years <- 1986:2018
# Make a new data frame for the annual incidences
annual_data = data.frame(year = years,
incidence = sapply(years, yearly_peak))
# write output file
write.csv(annual_data,
file=snakemake@output[[1]],
row.names=FALSE)
# Read in the data and convert the dates
data = read.csv(snakemake@input[[1]])
data$week_starting <- as.Date(data$week_starting)
# Plot the complete incidence dataset
png(filename=snakemake@output[[1]])
plot(data, type="l", xlab="Date", ylab="Weekly incidence")
dev.off()
# Zoom on the last four years
png(filename=snakemake@output[[2]])
plot(tail(data, 4*52), type="l", xlab="Date", ylab="Weekly incidence")
dev.off()
# Libraries used by this script:
import csv # for reading CSV files
import os # for path manipulations
with open(snakemake.output[0], 'w') as result_file:
for filename in snakemake.input:
region = '-'.join(os.path.splitext(filename)[0].split('-')[2:])
with open(filename, 'r') as csv_file:
csv_reader = csv.reader(csv_file)
csv_reader.__next__()
peak_year = None
peak_incidence = 0
for year, incidence in csv_reader:
incidence = int(incidence)
if incidence > peak_incidence:
peak_incidence = incidence
peak_year = year
result_file.write(region)
result_file.write(', ')
result_file.write(peak_year)
result_file.write('\n')
# Libraries used by this script:
import datetime # for date conversion
import csv # for writing output to a CSV file
# Read the CSV file into memory
data = open(snakemake.input[0], 'r').read()
# Remove white space at both ends,
# and split into lines.
lines = data.strip() \
.split('\n')
# Split each line into columns
table = [line.split(',') for line in lines]
# Remove records with missing data and write
# the removed records to a separate file for inspection.
with open(snakemake.output.errorlog, "w") as errorlog:
valid_table = []
for row in table:
missing = any([column == '' for column in row])
if missing:
errorlog.write("Missing data in record\n")
errorlog.write(str(row))
errorlog.write("\n")
else:
valid_table.append(row)
# Extract the two relevant columns, "week" and "inc"
week = [row[0] for row in valid_table]
assert week[0] == 'week'
del week[0]
inc = [row[2] for row in valid_table]
assert inc[0] == 'inc'
del inc[0]
data = list(zip(week, inc))
# Check for obviously out-of-range values
with open(snakemake.output.errorlog, "a") as errorlog:
for week, inc in data:
if len(week) != 6 or not week.isdigit():
errorlog.write("Suspect value in column 'week': {week}\n")
if not inc.isdigit():
errorlog.write("Suspect value in column 'inc': {inc}\n")
# Convert year/week by date of the corresponding Monday,
# then sort by increasing date
converted_data = \
[(datetime.datetime.strptime(year_and_week + ":1" , '%G%V:%u').date(), inc)
for year_and_week, inc in data]
converted_data.sort(key = lambda record: record[0])
# Check that consecutive dates are seven days apart
with open(snakemake.output.errorlog, "a") as errorlog:
dates = [date for date, _ in converted_data]
for date1, date2 in zip(dates[:-1], dates[1:]):
if date2-date1 != datetime.timedelta(weeks=1):
errorlog.write(f"{date2-date1} between {date1} and {date2}\n")
# Write data to a CSV file with two columns:
# 1. the date of the Monday of each week, in ISO format
# 2. the incidence estimate for that week
with open(snakemake.output.data, "w") as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow(["week_starting", "incidence"])
for row in converted_data:
csv_writer.writerow(row)
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:
# Some region names contain spaces which are awkward in filenames
region_name = '-'.join(region.split(' '))
filename = os.path.join(directory, 'weekly-incidence-' + region_name + '.csv')
with open(filename, 'w') as output_file:
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')
...@@ -137,7 +137,7 @@ rule preprocess: ...@@ -137,7 +137,7 @@ rule preprocess:
script: script:
"scripts/preprocess.py" "scripts/preprocess.py"
#+end_src #+end_src
Il y a donc un fichier d'entrée, qui est le résultat de la tâche /download/. Et il y a les deux fichiers de sortie. Enfin, pour faire le travail, j'ai opté pour un script Python cette fois. =snakemake= reconnaît le langage par l'extension =.py=. Il y a donc un fichier d'entrée, qui est le résultat de la tâche /download/. Et il y a les deux fichiers de sortie, un pour les résultats et un pour les messages d'erreur. Enfin, pour faire le travail, j'ai opté pour un script Python cette fois. =snakemake= reconnaît le langage par l'extension =.py=.
Le contenu de ce script est presque un copier-coller d'un document computationnel du module 3, plus précisément du document que j'ai montré dans le parcours Emacs/Org-Mode: Le contenu de ce script est presque un copier-coller d'un document computationnel du module 3, plus précisément du document que j'ai montré dans le parcours Emacs/Org-Mode:
#+begin_src python :exports both :tangle incidence_syndrome_grippal_snakemake/scripts/preprocess.py #+begin_src python :exports both :tangle incidence_syndrome_grippal_snakemake/scripts/preprocess.py
...@@ -909,176 +909,133 @@ Finished job 0. ...@@ -909,176 +909,133 @@ Finished job 0.
Complete log: /home/hinsen/projects/RR_MOOC/repos-session02/mooc-rr-ressources/module6/ressources/incidence_syndrome_grippal_snakemake/.snakemake/log/2019-08-29T153130.204927.snakemake.log Complete log: /home/hinsen/projects/RR_MOOC/repos-session02/mooc-rr-ressources/module6/ressources/incidence_syndrome_grippal_snakemake/.snakemake/log/2019-08-29T153130.204927.snakemake.log
#+end_example #+end_example
* Vers la gestion de données plus volumineuses * Vers la gestion de données plus volumineuses
Le workflow que je viens de montrer produit 7 fichiers. Ce n'est pas beaucoup. On peut les nommer à la main, un par un, sans difficulté. Dans la vraie vie, par exemple en bioinformatique, un workflow peut facilement gérer des centaines ou milliers de fichiers, par exemple un fichier par séquence d'acides aminés dans une étude de protéomique. Dans une telle situation, il faut définir un schéma pour nommer les fichiers de façon systématique, et introduire des boucles dans le workflow dont les itérations seront idéalement exécutées en parallèle. Je vais illustrer ceci avec une autre décomposition de l'analyse de l'incidence du syndrome grippal. Le workflow que je viens de montrer produit 7 fichiers. Ce n'est pas beaucoup. On peut les nommer à la main, un par un, sans difficulté. Dans la vraie vie, par exemple en bioinformatique, un workflow peut facilement gérer des centaines ou milliers de fichiers, par exemple un fichier par séquence d'acides aminés dans une étude de protéomique. Dans une telle situation, il faut définir un schéma pour nommer les fichiers de façon systématique, et introduire des boucles dans le workflow dont les itérations seront idéalement exécutées en parallèle. Je vais illustrer ceci avec une variante de l'analyse de l'incidence du syndrome grippal. Elle utilise une forme plus détaillée des données brutes dans laquelle les incidence sont repertoriées par région plutôt que pour la France entière. Il faut donc répéter le calcul de l'incidence annuelle 13 fois, une fois pour chaque région. Pour simplifier un peu, le résultat principal de ce nouveau workflow sera un fichier qui contient, pour chaque région, l'année dans laquelle l'incidence était la plus élevée. Il n'y a donc pas d'histogramme.
Dans cette nouvelle décomposition en tâches, je vais calculer l'incidence annuelle pour chaque année séparément. Comme nous avons les données pour 33 ans, ceci fait 33 tâches à la place d'une seule dans la version que j'ai montrée avant. Pour ce calcul trivial, qui fait simplement la somme d'une cinquantaine d'entiers, ça n'a aucun sense. On va même perdre en efficacité, malgré le parallélisme. Mais il est facile d'imaginer un calcul plus compliqué à la place de cette simple sommme. Mon but ici est de montrer les techniques pour définir le workflow, qui serviront aussi pour mieux comprendre comment fonctionne =snakemake=. Pour cette deuxième version, je crée un nouveau répertoire, et j'y fais une copie de tous les scripts, car la plupart ne nécessite pas de modification:
Pour cette deuxième version, je crée un nouveau répertoire, et j'y fais une copie du script de pré-traitement, qui reste identique:
#+begin_src sh :session *snakemake2* :results output :exports both #+begin_src sh :session *snakemake2* :results output :exports both
mkdir incidence_syndrome_grippal_snakemake_parallele mkdir incidence_syndrome_grippal_par_region_snakemake
cd incidence_syndrome_grippal_snakemake_parallele cd incidence_syndrome_grippal_par_region_snakemake
mkdir data mkdir data
mkdir scripts cp -r ../incidence_syndrome_grippal_snakemake/scripts .
cp ../incidence_syndrome_grippal_snakemake/scripts/preprocess.py scripts/
#+end_src #+end_src
#+RESULTS: #+RESULTS:
Et puis je vais vous montrer le =Snakefile=, tout de suite en entier, que je vais commenter après. Et puis je vais vous montrer le =Snakefile=, tout de suite en entier, que je vais commenter après.
#+begin_src :exports both :tangle incidence_syndrome_grippal_snakemake_parallele/Snakefile #+begin_src :exports both :tangle incidence_syndrome_grippal_par_region_snakemake/Snakefile
rule all: rule all:
input: input:
"data/annual-incidence-histogram.png" "data/peak-year-all-regions.txt"
rule download: rule download:
output: output:
"data/weekly-incidence.csv" "data/weekly-incidence-all-regions.csv"
shell: shell:
"wget -O {output} http://www.sentiweb.fr/datasets/incidence-PAY-3.csv" "wget -O {output} http://www.sentiweb.fr/datasets/incidence-RDD-3.csv"
REGIONS = ["AUVERGNE-RHONE-ALPES",
"BOURGOGNE-FRANCHE-COMTE",
"BRETAGNE",
"CENTRE-VAL-DE-LOIRE",
"CORSE",
"GRAND EST",
"HAUTS-DE-FRANCE",
"ILE-DE-FRANCE",
"NORMANDIE",
"NOUVELLE-AQUITAINE",
"OCCITANIE",
"PAYS-DE-LA-LOIRE",
"PROVENCE-ALPES-COTE-D-AZUR"]
rule split_by_region:
input:
"data/weekly-incidence-all-regions.csv"
output:
expand("data/weekly-incidence-{region}.csv",
region=REGIONS)
script:
"scripts/split-by-region.py"
rule preprocess: rule preprocess:
input: input:
rules.download.output, "data/weekly-incidence-{region}.csv"
"scripts/preprocess.py"
output: output:
data="data/preprocessed-weekly-incidence.csv", data="data/preprocessed-weekly-incidence-{region}.csv",
errorlog="data/errors-from-preprocessing.txt" errorlog="data/errors-from-preprocessing-{region}.txt"
script: script:
"scripts/preprocess.py" "scripts/preprocess.py"
rule extract_one_year: rule plot:
input: input:
rules.preprocess.output.data, "data/preprocessed-weekly-incidence-{region}.csv"
"scripts/extract_one_year.py"
params:
year="{year}"
output: output:
"data/{year}.csv" "data/weekly-incidence-plot-{region}.png",
"data/weekly-incidence-plot-last-years-{region}.png"
script: script:
"scripts/extract_one_year.py" "scripts/incidence-plots.R"
rule annual_incidence: rule annual_incidence:
input: input:
"data/{year}.csv", "data/preprocessed-weekly-incidence-{region}.csv"
"scripts/annual_incidence.py"
output: output:
"data/{year}-incidence.txt" "data/annual-incidence-{region}.csv"
script: script:
"scripts/annual_incidence.py" "scripts/annual-incidence.R"
rule histogram: rule peak_years:
input: input:
expand("data/{year}-incidence.txt", year=range(1986, 2019)), expand("data/annual-incidence-{region}.csv",
"scripts/annual-incidence-histogram.R" region=REGIONS)
output: output:
"data/annual-incidence-histogram.png" "data/peak-year-all-regions.txt"
script: script:
"scripts/annual-incidence-histogram.R" "scripts/peak-years.py"
#+end_src #+end_src
Commençons en haut: j'ai mis la règle =all= au début pour pouvoir être paresseux à l'exécution: la simple commande =snakemake= déclenchera l'ensemble des calculs. Et =all=, c'est simplement l'histogramme des incidences annuelles ici. Tous les autres fichiers sont des résultats intermédiaires. Par simplicité, j'ai décidé de ne plus faire les plots de la suite chronologiques - vous l'avez vu assez de fois maintenant. Dans la règle =all=, je n'ai pas écrit le nom du fichier image de l'histgramme, mais à la place j'ai utilisé une référence indirecte, =rules.histogram.output=, ce qui veut dire tous les fichiers de sortie de la règle =histogram=. Ceci évite d'écrire le même nom deux fois, et potentiellement introduire des erreurs en le faisant. Commençons en haut: j'ai mis la règle =all= au début pour pouvoir être paresseux à l'exécution: la simple commande =snakemake= déclenchera l'ensemble des calculs. Et =all=, c'est simplement le fichier qui résume les années du pic maximal pour chaque région.
Les deux règles suivantes, =download= et =preprocess=, sont les mêmes qu'avant, à un détail de notation près: dans =preprocess=, j'ai également utilisé la référence indirecte =rules.download.output= à la place du nom du fichier en question. Et j'ai rajouté le fichier du script comme fichier d'entrée pour que =snakemake= refasse le calcul après chaque modification du script.
Les trois règles finales sont la partie vraiment intéressante. Commençons par la dernière, =histogram=. Elle produit un fichier image, comme avant. Elle le fait en appelant un script en R, comme avant. Mais à l'entrée, elle réclame un fichier par an. L'expression =expand(...)= produit une liste de noms de fichier en remplaçant ={year}= dans le modèle donné par les éléments de la liste =range(1986, 2019)=, qui contient les entiers de =1986= à =2018=. Si cela vous rappele le langage Python, vous avez raison - le nom =snakemake= n'est pas une coïncidence, c'est écrit en Python !
La règles =histogram= réclame donc les fichiers =data/1986-incidence.txt=,
=data/1987-incidence.txt=, =data/1988-incidence.txt=, etc. Comme ces fichiers n'existent pas au départ, Une autre règle doit les produire. Et c'est la règle =annual_incidence=.qui le fait. En fait, elle s'applique à la production de tout fichier dont le nom à la forme =data/{year}-incidence.txt=. Quand =histogram= réclame le fichier =data/1986-incidence.txt=, =snakemake= trouve que la règle =annual_incidence= est applicable si on remplace ={year}= par =1986=. Il faut donc exécuter le script =scripts/annual_incidence.py= avec le fichier d'entrée =data/1986.csv=. Sauf que... ce fichier n'existe pas non plus. Pas grave, car la règle =extract_one_year= peut le produire! Il suffit d'appeler le script =scripts/extract_one_year.py= avec à l'entrée le fichier =rules.preprocess.output.data=, autrement dit le fichier =data/preprocessed-weekly-incidence.csv=, que fournit la règles =preprocess=.
La boucle qui fait le calcul pour chaque année est donc contenue dans la spécification des entrées de la règle =histogram=, qui en consomme le résultat. Et si vous regardez bien, c'est le principe de fonctionnement de =snakemake= partout: on demande un résultat, =snakemake= cherche la règle qui le produit, et applique cette règle après avoir assuré l'existence de ses entrée par exactement le même mécanisme. =snakemake= traite donc le workflow en commençant par la fin, les résultats, et en remontant vers les données d'entrée.
Il ne reste plus qu'à regarder les trois scripts réferencés dans les règles. Le premier, =scripts/extract_one_year.py=, lit le fichier des données pré-traitées et en extrait la part d'une année. Comme expliqué dans le module 3, la part de l'année N va du 1er août de l'année N-1 jusqu'au 31 juillet de l'année N et inclut ainsi le pic de l'année N qui se situe en janvier ou février. L'année est passée comme paramètre, défini dans la section =params= du =Snakefile= et récupéré en Python comme =snakemake.params=.
Un point important est la vérification de l'année. J'ai utilisé le nom suggestif =year= pour la partie variable des noms de fichier dans la règle =extract_one_year=, mais pour =snakemake=, ce n'est qu'un nom. Si je demande =snakemake data/foo.csv=, la même règle va être appliquée avec =foo= comme valeur de =year= ! Il faut donc que le script vérifie la validité du paramètre. Dans la règle =download=, seul le nom du fichier de données a changé par rapport à avant. J'ai trouvé le nom du fichier "par région" sur le site Web du Réseau Sentinelles. C'est après qu'il y a le plus grand changement: la définition d'une variable =REGIONS=, qui est une liste des 13 régions administratives, dont les noms sont écrits exactement comme dans le fichier des données. On devrait récupérer cette liste du fichier de façon automatique, et je montrerai plus tard comment faire. Pour l'instant, je préfère copier la liste manuellement dans le =Snakefile= afin de ne pas introduire trop de nouveautés d'aun seul coup. La variable =REGIONS= est utilisée immédiatement après, pour définir les fichiers de sortie de la règle =split_by_region=. La fonction =expand= produit une liste des noms de fichier en insérant le nom de la région au bon endroit dans le modèle.
#+begin_src python :exports both :tangle incidence_syndrome_grippal_snakemake_parallele/scripts/extract_one_year.py
# Libraries used by this script:
import csv # for reading and writing CSV files
import os # for filename manipulation
# Read the CSV file into memory
with open(snakemake.input[0], "r") as csvfile:
data = []
csv_reader = csv.reader(csvfile)
for row in csv_reader:
data.append(row)
assert data[0] == ['week_starting', 'incidence']
del data[0]
# Get the year from the parameter object.
# Check that it is a valid year number, i.e. a four-digit integer.
year = snakemake.params.year
assert len(year) == 4
assert str(int(year)) == year
year = int(year)
# Check that we have data for that year.
# The dates are in ISO format, meaning that string
# comparison is equivalent to date comparison.
# There is thus no need to convert the date string
# to date objects first!
start = "%4d-08-01" % (year-1)
end = "%4d-08-01" % year
assert start >= data[0][0]
assert end <= data[-1][0]
# Write a CSV output file for the requested year.
with open(snakemake.output[0], "w") as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow(["week_starting", "incidence"])
number_of_weeks = 0
for row in data:
if row[0] >= start and row[0] < end:
csv_writer.writerow(row)
number_of_weeks += 1
assert number_of_weeks in [51, 52, 53]
#+end_src
Le script =scripts/annual_incidence.py= fonctionnent d'après les mêmes principes. Il n'a pas besoin d'un paramètre pour indiquer l'année, car il n'en a pas besoin. Il lit un fichier CSV et fait la somme des nombres dans la deuxième colonne, c'est tout. Le rôle de la règle =split_by_region= est de découper les données téléchargées en un fichier par région, afin de pouvoir traiter les régions en parallèle et avec les même scripts que nous avons déjà. Le script appliqué par la règle est assez simple:
#+begin_src python :exports both :tangle incidence_syndrome_grippal_snakemake_parallele/scripts/annual_incidence.py #+begin_src python :exports both :tangle incidence_syndrome_grippal_par_region_snakemake/scripts/split-by-region.py
# Libraries used by this script: import os
import csv # for reading CSV files
# Read the CSV file into memory # Read the CSV file into memory
with open(snakemake.input[0], "r") as csvfile: data = open(snakemake.input[0], 'rb').read()
data = [] # Decode the Latin-1 character set,
csv_reader = csv.reader(csvfile) # remove white space at both ends,
for row in csv_reader: # and split into lines.
data.append(row) lines = data.decode('latin-1') \
assert data[0] == ['week_starting', 'incidence'] .strip() \
del data[0] .split('\n')
# Compute total incidence
incidence = sum(int(row[1]) for row in data)
# Write the output file
with open(snakemake.output[0], "w") as output_file:
output_file.write(str(incidence))
output_file.write("\n")
#+end_src
Reste le script R qui fait l'histogramme. Rien à signaler, autre que peut-être la façon de lire tous les fichiers d'entrée avec une seule ligne de code avec la fonction =lapply=. A noter que =snakemake@input= est la liste des tous les fichiers d'entrée, y compris le nom du script lui-même, qu'il faut supprimer bien sûr.
#+begin_src R :exports both :tangle incidence_syndrome_grippal_snakemake_parallele/scripts/annual-incidence-histogram.R
# Read in the data. The last input file is the name of the script,
# so it needs to ne removed from the list before reading all the files.
files = snakemake@input
datafiles = files[-length(files)]
data = as.numeric(lapply(datafiles, function(fn) read.table(fn)[[1]]))
# Plot the histogram # Separate header from data table
png(filename=snakemake@output[[1]]) comment = lines[0]
hist(data, header = lines[1]
breaks=10, table = [line.split(',') for line in lines[2:]]
xlab="Annual incidence",
ylab="Number of observations", # Find all the regions mentioned in the table
main="") regions = set(record[-1] for record in table)
dev.off()
# Write CSV files for each region
for region in regions:
filename = 'data/weekly-incidence-' + 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 #+end_src
Je pourrais maintenant taper =snakemake= et voir une longue liste de calculs défiler devant mes yeux. Je me retiens encore un peu pour illustrer ce que j'ai expliqué avant. En fait, je vais d'abord demander juste l'incidence annuelle de 2008: Avant de continuer, voyons déjà ce que ça donne:
#+begin_src sh :session *snakemake2* :results output :exports both #+begin_src sh :session *snakemake2* :results output :exports both
snakemake data/2008-incidence.txt snakemake split_by_region
#+end_src #+end_src
#+RESULTS: #+RESULTS:
...@@ -1089,77 +1046,106 @@ Provided cores: 1 ...@@ -1089,77 +1046,106 @@ Provided cores: 1
Rules claiming more threads will be scaled down. Rules claiming more threads will be scaled down.
Job counts: Job counts:
count jobs count jobs
1 annual_incidence
1 download 1 download
1 extract_one_year 1 split_by_region
1 preprocess 2
4
[Fri Aug 30 17:58:48 2019] [Tue Sep 24 12:13:37 2019]
rule download: rule download:
output: data/weekly-incidence.csv output: data/weekly-incidence-all-regions.csv
jobid: 3 jobid: 1
--2019-08-30 17:58:48-- http://www.sentiweb.fr/datasets/incidence-PAY-3.csv --2019-09-24 12:13:37-- http://www.sentiweb.fr/datasets/incidence-RDD-3.csv
Resolving www.sentiweb.fr (www.sentiweb.fr)... 134.157.220.17 Resolving www.sentiweb.fr (www.sentiweb.fr)... 134.157.220.17
Connecting to www.sentiweb.fr (www.sentiweb.fr)|134.157.220.17|:80... connected. Connecting to www.sentiweb.fr (www.sentiweb.fr)|134.157.220.17|:80... connected.
HTTP request sent, awaiting response... 200 OK HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/csv] Length: unspecified [text/csv]
Saving to: 'data/weekly-incidence.csv' Saving to: 'data/weekly-incidence-all-regions.csv'
] 0 --.-KB/s data/weekly-inciden [ <=> ] 79.88K --.-KB/s in 0.04s ] 0 --.-KB/s data/weekly-inciden [ <=> ] 1.06M --.-KB/s in 0.09s
2019-08-30 17:58:48 (1.84 MB/s) - 'data/weekly-incidence.csv' saved [81800] 2019-09-24 12:13:44 (11.3 MB/s) - 'data/weekly-incidence-all-regions.csv' saved [1112021]
[Fri Aug 30 17:58:48 2019] [Tue Sep 24 12:13:44 2019]
Finished job 3. Finished job 1.
) done ) done
[Fri Aug 30 17:58:48 2019] [Tue Sep 24 12:13:44 2019]
rule preprocess: rule split_by_region:
input: data/weekly-incidence.csv, scripts/preprocess.py input: data/weekly-incidence-all-regions.csv
output: data/preprocessed-weekly-incidence.csv, data/errors-from-preprocessing.txt output: data/weekly-incidence-AUVERGNE-RHONE-ALPES.csv, data/weekly-incidence-BOURGOGNE-FRANCHE-COMTE.csv, data/weekly-incidence-BRETAGNE.csv, data/weekly-incidence-CENTRE-VAL-DE-LOIRE.csv, data/weekly-incidence-CORSE.csv, data/weekly-incidence-GRAND EST.csv, data/weekly-incidence-HAUTS-DE-FRANCE.csv, data/weekly-incidence-ILE-DE-FRANCE.csv, data/weekly-incidence-NORMANDIE.csv, data/weekly-incidence-NOUVELLE-AQUITAINE.csv, data/weekly-incidence-OCCITANIE.csv, data/weekly-incidence-PAYS-DE-LA-LOIRE.csv, data/weekly-incidence-PROVENCE-ALPES-COTE-D-AZUR.csv
jobid: 2 jobid: 0
[Fri Aug 30 17:58:48 2019] [Tue Sep 24 12:13:44 2019]
Finished job 2. Finished job 0.
) done ) done
Complete log: /home/hinsen/projects/RR_MOOC/repos-session02/mooc-rr-ressources/module6/ressources/incidence_syndrome_grippal_par_region_snakemake/.snakemake/log/2019-09-24T121337.313929.snakemake.log
#+end_example
[Fri Aug 30 17:58:48 2019] Et les fichiers sont bien là où il faut:
rule extract_one_year: #+begin_src sh :session *snakemake2* :results output :exports both
input: data/preprocessed-weekly-incidence.csv, scripts/extract_one_year.py ls data
output: data/2008.csv #+end_src
jobid: 1
wildcards: year=2008
[Fri Aug 30 17:58:48 2019] #+RESULTS:
Finished job 1. #+begin_example
) done weekly-incidence-AUVERGNE-RHONE-ALPES.csv
weekly-incidence-BOURGOGNE-FRANCHE-COMTE.csv
weekly-incidence-BRETAGNE.csv
weekly-incidence-CENTRE-VAL-DE-LOIRE.csv
weekly-incidence-CORSE.csv
weekly-incidence-GRAND EST.csv
weekly-incidence-HAUTS-DE-FRANCE.csv
weekly-incidence-ILE-DE-FRANCE.csv
weekly-incidence-NORMANDIE.csv
weekly-incidence-NOUVELLE-AQUITAINE.csv
weekly-incidence-OCCITANIE.csv
weekly-incidence-PAYS-DE-LA-LOIRE.csv
weekly-incidence-PROVENCE-ALPES-COTE-D-AZUR.csv
weekly-incidence-all-regions.csv
#+end_example
[Fri Aug 30 17:58:48 2019] Les trois règles suivantes, =preprocess=, =plot=, et =annual_incidence= sont presques les mêmes qu'avant. Ce qui a changé, c'est la partie =-{region}= dans les noms des fichiers. Il faut interpréter le mot entre les accolades ("region") comme un nom de variable. La règle =preprocess=, par exemple, peut produire tout fichier qui a la forme "data/preprocessed-weekly-incidence-{region}.csv" si on lui donne le fichier "data/weekly-incidence-{region}.csv" avec la même valeur pour ={region}=. Etant donné les fichiers que nous avons obtenu par =split_by_region=, nous pouvons donc demander à snakemake le fichier "data/preprocessed-weekly-incidence-CORSE.csv", et snakemake va appliquer la règle =preprocess= au fichier d'entrée "data/weekly-incidence-CORSE.csv" que nous avons déjà. Faison-le:
rule annual_incidence:
input: data/2008.csv, scripts/annual_incidence.py #+begin_src sh :session *snakemake2* :results output :exports both
output: data/2008-incidence.txt snakemake data/preprocessed-weekly-incidence-CORSE.csv
#+end_src
#+RESULTS:
#+begin_example
Building DAG of jobs...
Using shell: /bin/bash
Provided cores: 1
Rules claiming more threads will be scaled down.
Job counts:
count jobs
1 preprocess
1
[Tue Sep 24 12:14:02 2019]
rule preprocess:
input: data/weekly-incidence-CORSE.csv
output: data/preprocessed-weekly-incidence-CORSE.csv, data/errors-from-preprocessing-CORSE.txt
jobid: 0 jobid: 0
wildcards: year=2008 wildcards: region=CORSE
[Fri Aug 30 17:58:48 2019] [Tue Sep 24 12:14:02 2019]
Finished job 0. Finished job 0.
) done ) done
Complete log: /home/hinsen/projects/RR_MOOC/repos-session02/mooc-rr-ressources/module6/ressources/incidence_syndrome_grippal_snakemake_parallele/.snakemake/log/2019-08-30T175848.265842.snakemake.log Complete log: /home/hinsen/projects/RR_MOOC/repos-session02/mooc-rr-ressources/module6/ressources/incidence_syndrome_grippal_par_region_snakemake/.snakemake/log/2019-09-24T121402.098618.snakemake.log
#+end_example #+end_example
On peut bien suivre l'exécution des tâches: d'abord =download=, puis =preprocess=, =extract_one_year=, et =annual_incidence=. Regardons le contenu de ce petit fichier:
#+begin_src sh :session *snakemake2* :results output :exports both #+begin_src sh :session *snakemake2* :results output :exports both
cat data/2008-incidence.txt ls data/preprocessed*
#+end_src #+end_src
#+RESULTS: #+RESULTS:
: 2975925 : data/preprocessed-weekly-incidence-CORSE.csv
Si maintenant je demande une autre année, seulement les tâches =extract_one_year= et =annual_incidence= devraient s'afficher, car les deux première sont déjà faites. Voyons: Le script =preprocess.py= n'a d'ailleurs pas changé du tout. Un workflow permet donc de séparer la logistique de la gestion des données du code qui fait les calculs.
Le même mécanisme permet de demander l'incidence annuelle pour la Corse:
#+begin_src sh :session *snakemake2* :results output :exports both #+begin_src sh :session *snakemake2* :results output :exports both
snakemake data/1992-incidence.txt snakemake data/annual-incidence-CORSE.csv
#+end_src #+end_src
#+RESULTS: #+RESULTS:
...@@ -1171,35 +1157,59 @@ Rules claiming more threads will be scaled down. ...@@ -1171,35 +1157,59 @@ Rules claiming more threads will be scaled down.
Job counts: Job counts:
count jobs count jobs
1 annual_incidence 1 annual_incidence
1 extract_one_year 1
2
[Fri Aug 30 18:01:41 2019]
rule extract_one_year:
input: data/preprocessed-weekly-incidence.csv, scripts/extract_one_year.py
output: data/1992.csv
jobid: 1
wildcards: year=1992
[Fri Aug 30 18:01:41 2019]
Finished job 1.
) done
[Fri Aug 30 18:01:41 2019] [Tue Sep 24 12:14:13 2019]
rule annual_incidence: rule annual_incidence:
input: data/1992.csv, scripts/annual_incidence.py input: data/preprocessed-weekly-incidence-CORSE.csv
output: data/1992-incidence.txt output: data/annual-incidence-CORSE.csv
jobid: 0 jobid: 0
wildcards: year=1992 wildcards: region=CORSE
[Fri Aug 30 18:01:41 2019] During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
[Tue Sep 24 12:14:13 2019]
Finished job 0. Finished job 0.
) done ) done
Complete log: /home/hinsen/projects/RR_MOOC/repos-session02/mooc-rr-ressources/module6/ressources/incidence_syndrome_grippal_snakemake_parallele/.snakemake/log/2019-08-30T180141.597756.snakemake.log Complete log: /home/hinsen/projects/RR_MOOC/repos-session02/mooc-rr-ressources/module6/ressources/incidence_syndrome_grippal_par_region_snakemake/.snakemake/log/2019-09-24T121413.475153.snakemake.log
#+end_example #+end_example
Ça marche ! Je peux alors attaquer la totale, mais je vais supprimer l'affichage de tous les détails de l'exécution (option =-q=), pour éviter de voir les années défiler devant mes yeux! Snakemake nous dit d'ailleurs explicitement quelle règle a été appliquée (=annual_incidence=), avec quel fichier d'entrée (=data/preprocessed-weekly-incidence-CORSE.csv=), et avec quel fichier de sortie (=data/annual-incidence-CORSE.csv=).
A la fin du workflow, il y a une nouvelle règle, =peak_years=, qui extrait l'année du pic maximal de chaque fichier d'incience annuelle, et produit un fichier résumant ces années par région. Sa seule particularité est la spécification des fichiers d'entrée, qui utilise la fonction =expand= exactement comme on l'a vu pour les fichiers résultats de la règle =split_by_region=. Le script Python associé est assez simple:
#+begin_src python :exports both :tangle incidence_syndrome_grippal_par_region_snakemake/scripts/peak-years.py
# Libraries used by this script:
import csv # for reading CSV files
import os # for path manipulations
with open(snakemake.output[0], 'w') as result_file:
for filename in snakemake.input:
region = '-'.join(os.path.splitext(filename)[0].split('-')[2:])
with open(filename, 'r') as csv_file:
csv_reader = csv.reader(csv_file)
csv_reader.__next__()
peak_year = None
peak_incidence = 0
for year, incidence in csv_reader:
incidence = int(incidence)
if incidence > peak_incidence:
peak_incidence = incidence
peak_year = year
result_file.write(region)
result_file.write(', ')
result_file.write(peak_year)
result_file.write('\n')
#+end_src
Dans ce workflow, nous avons donc introduit une boucle sur les régions en jouant avec les noms des fichiers. Chaque fichier du workflow précédent a été remplacé par une version "régionalisée", avec le suffix =-{region}= dans le nom. Ce qui déclenche la boucle, c'est la fonction =expand= dans notre =Snakefile=. Le grand avantage d'une telle boucle, par rapport à une boucle standard en Python ou R, est la parallélisation automatique. Sur une machine avec suffisamment de processeurs, toutes les 13 régions seront traitées en même temps. Mon ordinateur portable n'a qu'un processeur à deux coeurs, donc =snakemake= traite seulement deux régions à la foi.
Je vais maintenant lancer le calcul total - avec une petite précaution, l'option =-q= ("quiet") qui dit à snakemake d'être moins bavard:
#+begin_src sh :session *snakemake2* :results output :exports both #+begin_src sh :session *snakemake2* :results output :exports both
snakemake -q snakemake -q
#+end_src #+end_src
...@@ -1209,28 +1219,181 @@ snakemake -q ...@@ -1209,28 +1219,181 @@ snakemake -q
Job counts: Job counts:
count jobs count jobs
1 all 1 all
33 annual_incidence 12 annual_incidence
33 extract_one_year 1 peak_years
1 histogram 12 preprocess
68 26
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages: During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C" 1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C" 2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C" 3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C" 4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
#+end_example
En regardant bien le début du rapport que snakemake a fourni, on voit que =preprocess= et =annual_incidence= sont comptés 12 fois: une fois par région, moins la Corse que j'ai déjà traitée à la main. Une fois =all= et =peak_years=, ça a l'air bon. Et le résultat est là:
#+begin_src sh :session *snakemake2* :results output :exports both
cat data/peak-year-all-regions.txt
#+end_src
#+RESULTS:
#+begin_example
AUVERGNE-RHONE-ALPES, 2009
BOURGOGNE-FRANCHE-COMTE, 1986
BRETAGNE, 1996
CENTRE-VAL-DE-LOIRE, 1996
CORSE, 1989
GRAND EST, 2000
HAUTS-DE-FRANCE, 2013
ILE-DE-FRANCE, 1989
NORMANDIE, 1990
NOUVELLE-AQUITAINE, 1989
OCCITANIE, 2013
PAYS-DE-LA-LOIRE, 1989
PROVENCE-ALPES-COTE-D-AZUR, 1986
#+end_example
Un dernier détail à noter: la règle =plot= est bien dans mon =Snakefile=, mais elle n'a jamais été appliquée, et il n'y a aucun plot. C'est simplement parce que la règle =all= ne réclame que la production du fichier =data/peak-year-all-regions.txt=. J'aurais pu rajouter les plots, mais je ne l'ai pas fait. Ceci ne m'empêche pas de les demander explicitement:
#+begin_src sh :session *snakemake2* :results output :exports both
snakemake data/weekly-incidence-plot-last-years-CORSE.png
#+end_src
#+RESULTS:
#+begin_example
Building DAG of jobs...
Using shell: /bin/bash
Provided cores: 1
Rules claiming more threads will be scaled down.
Job counts:
count jobs
1 plot
1
[Tue Sep 24 12:20:52 2019]
rule plot:
input: data/preprocessed-weekly-incidence-CORSE.csv
output: data/weekly-incidence-plot-CORSE.png, data/weekly-incidence-plot-last-years-CORSE.png
jobid: 0
wildcards: region=CORSE
During startup - Warning messages:
1: Setting LC_COLLATE failed, using "C"
2: Setting LC_TIME failed, using "C"
3: Setting LC_MESSAGES failed, using "C"
4: Setting LC_MONETARY failed, using "C"
Warning message:
Y-%m-%d", tz = "GMT") :
unknown timezone 'zone/tz/2019b.1.0/zoneinfo/Europe/Paris'
null device
1
null device null device
1 1
[Tue Sep 24 12:20:52 2019]
Finished job 0.
) done
Complete log: /home/hinsen/projects/RR_MOOC/repos-session02/mooc-rr-ressources/module6/ressources/incidence_syndrome_grippal_par_region_snakemake/.snakemake/log/2019-09-24T122052.352596.snakemake.log
#+end_example #+end_example
Regardons le résultat final, l'histogramme: [[file:incidence_syndrome_grippal_par_region_snakemake/data/weekly-incidence-plot-last-years-CORSE.png]]
[[file:incidence_syndrome_grippal_snakemake_parallele/data/annual-incidence-histogram.png]]
Enfin, je vais tenter de produire le dessin du graphe des tâches, m'attendant à une graphique un peu disproportionnée à cause du grand nombre de tâches: Enfin, je vais tenter de produire le dessin du graphe des tâches, comme je l'ai fait avant pour un workflow nettement plus simple. Voyons...
#+begin_src sh :session *snakemake2* :results output :exports both #+begin_src sh :session *snakemake2* :results output :exports both
snakemake -q --forceall --dag all | dot -Tpng > graph.png snakemake -q --forceall --dag all | dot -Tpng > graph.png
#+end_src #+end_src
#+RESULTS: #+RESULTS:
file:incidence_syndrome_grippal_par_region_snakemake/graph.png
[[file:incidence_syndrome_grippal_snakemake_parallele/graph.png]] On voit bien la structure du calcul, y compris le traitement des régions en parallèle.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment