Data pipeline e Snakemake

Oggi discuteremo come strutturare la pipeline di analisi dal punto di vista logico, come gestire i dati e come automazzare le analisi con gli strumenti di automazione come Snakemake

Ritorniamo al discorso fatto nella prima lezione, riguardo all'affidabilità delle analisi ed alla loro riproducibilità.

tutto quello che viene fatto a mano è da considerarsi già dimenticato, quindi bisogna automatizzare il più possibile

Questo può essere fatto a più livelli:

  • non modificare i dati manualmente
  • non fare le analisi a occhio
  • non buttare via i risultati delle analisi
  • collegare manualmente i vari step dell'analisi

Nella prima lezione abbiamo trattato come tenere traccia della storia delle nostre analisi con il controllo di versione.

Oggi approfondiremo questi concetti e li espanderemo ad altri livelli.

Struttura dei dati

Per prima cosa, vediamo come strutturare i nostri dati.

In linea di principio abbiamo due macrocategorie di tipi di dati:

  • formati testuali
  • formati binari

formati testuali

Questi formati di dati non sono altro che file di testo formattati in un modo particolare, per contenere informazioni leggibili dal computer.

Questi formati sono tipicamente dispendiosi in termini di spazio e velocità di lettura, ma sono anche tipicamente più robusti alla corruzione e possono essere più facilmente ispezionati da un essere umano.

Questi formati vengono poi spesso spostati in giro in versione compressa, per risparmiare spazio, e dovrebbero essere considerati formati binari. Per comodità di classificazione, farò finta di nulla. Inoltre, molte librerie come Pandas, leggono i formati testuali direttamente anche quando sono compressi.

cvs e tsv

Formati tabulari, separati da virgole o tabulazioni (o punti e virgola), uno dei formati più comuni per rappresentare le tabelle.

nome;età;abitazione
Antonio;32;Bologna
Maria;25;Torino
Francesco;47;Napoli

Permettono di inserire commenti (tipicamente con #) e possono essere aperti ed editati facilmente con programmi come Excel, ma possono contenere solo dati in formato tabella

JSON

formato standard di condivisione dati su internet, è molto utile per rappresentare dati gerarchici. Corrisponde sostanzialmente ad una dizionario di dizionari (in termini di python), ed ha praticamente la stessa sintassi.

{"persone":
    {"antonio": {"età": "32", "abitazione": "Bologna"},
     "maria": {"età": "20", "abitazione": "Cesena"},
     }
}

INI

I dati INI sono nati sotto l'ambiente windows per la gestione delle configurazioni. Sono molto vicini al JSON come livello di espressività e facilità di gestione.

[Sezione]
chiave_con_valore = 2
chiave_booleana

    [sottosezione]
    sottodettaglio = 3

[Nuova sezione]
altro_parametro = "codice hash"

YAML

Formato di configurazione molto ricco. è molto semplice da scrivere per un essere umano, ma il formato è potenzialmente molto complesso e quindi non c'è una garanzia che la libreria lo riesca ad importare correttamente se si usano costrutti un po' elaborati.

Se ci si mantiene sul semplice è però molto comodo

persone:
    Antonio:
        età: 30
        abitazione: Ferrara
        hobby: [pesca, calcio]
    Maria:
        età: 20
        abitazione: Torino
        hobby: [immersioni, paracadutismo]

XML

Formato figlio del vecchio html, si presta bene a rappresentare dati molto complicati, ma è estremamente verboso e non è banale farne il parsing a meno che non sia indicato in un altro file quale sia la sua struttura

<listapersone>
    <persona nome='Antonio'>
        <annonascita>1983</annonascita>
        <cittàresidenza>Bologna</cittàresidenza>
    </persona>
    <persona nome='Maria'>
        <annonascita>1997</annonascita>
        <cittàresidenza>Rimini</cittàresidenza>
    </persona>
</listapersone>

nota sull'XML

molti formati sono in realtà XML, anche se non ce ne rendiamo conto:

  • le immagini vettoriali svg
  • i documenti openoffice e office sono xml compressi
  • le pagine html
  • molti formati specialistici in vari campi

Formati Binari

I formati binari codificano l'informazione in modo molto compresso, sfruttando una codifica diretta dei bit.

Questi formati sono molto spesso specifici di una singola applicazione, ma esistono dei formati generali riconosciuti in vari contesti.

Di seguito ne presenterò alcuni che sono abbastanza comuni o di interesse per il nostro campo

immagini lossless: bmp, png, gif, tiff

Questi formati contengono i valori di colore contenuti in un'immagine con diversi tipi di compressione, senza però perdere alcuna informazione, al contrario di formati come il jpeg, che andrebbe quindi evitato per lo storage di immagini.

Se il vostro dato è un plot e non un'immagine generica, considerate l'uso del formato SVG, che permette modificazioni successive con più facilità.

Dicom

formato medico per lo storage di dati di immagini, come radiologie, TAC, risonanze magnetiche e simili.

Contengono un gran numero di metadati sul paziente e la configurazione del macchinario.

numpy: NPY

Numpy definisce un suo formato binario, adatto a salvare un singolo array in modo molto semplice ed open.

Viste le limitazioni non è un buon formato per lo scambio di dati, ma è ottimo per file temporanei durante le analisi.

HDF5

Questo è un formato generico di storage binario, adatto a dati numerici.

È il formato di default delle versioni moderne di matlab per il salvataggio dei dati (anche se li definiscono file .mat).

Sono degli interi filesystem virtuali in cui si possono contenere un numero arbitrario di matrici numeriche ed una buona quantità di metadati.

Non è adatto nel caso sia necessario fare ripetute riscritture degli stessi dati.

Performances di diversi formati

Performance di diversi formati di dati per la scrittura di una matrice numerica di medie dimensioni (500 x 150'000)

tipo di dato dimensione scrittura lettura compressione post
csv esteso 430 MB 1m 15s 9.78s 132 MB
csv con gzip 132 MB 6m 57s 16.2s
hdf5 non compresso 463 MB 155ms 299ms 154 MB
npy non compresso 462 MB 638ms 393ms 154 MB

hashing dei file

Una operazione molto comune nella data analisi è il cosiddetto hashing.

Questo consiste nella generazione di una stringa alfanumerica (tipicamente di qualche centinaio di caratteri) a partire dal contenuto del file.

Gli algoritmi di hash garantiscono che piccole modifiche del contenuto del file comporti grandi variazioni dell'hash, permettendo di usare queste stringhe per garantire l'integrità dei dati.

Esistono diversi algoritmi per la generazione dell'hash (SHA1, md5, sha512, etc...) per cui l'algoritmo di calcolo dovrebbe essere riportato insieme all'hash

I sistemi di controllo di versione usano internamente gli hash dei file per verificare se questi sono cambiati o meno.

In [2]:
import hashlib

stringa1 = "sono una stringa di testo completamente 1nnocente".encode('utf8')
r = hashlib.md5(stringa1).hexdigest()
print('md5: {}'.format(r))

stringa1 = "sono una stringa di testo completamente lnnocente".encode('utf8')
r = hashlib.md5(stringa1).hexdigest()
print('md5: {}'.format(r))

stringa1 = "sono una stringa di testo completamente innocente".encode('utf8')
r = hashlib.md5(stringa1).hexdigest()
print('md5: {}'.format(r))
md5: 924c9da73082e7ca11ef717a8e65ac7c
md5: 11d1f5de573d53f8f194e56b6de0a047
md5: b8e8b3b5490d600d8451d733d0f135f3
In [4]:
!md5sum ./Snakefile
fcdcdbda6cc42ee8fac608ed9e2d4391  ./Snakefile
In [6]:
def md5(fname):
    """funzione di hash dei file appropriata anche per i big data"""
    hash_md5 = hashlib.md5()
    with open(fname, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()

hash_result = md5("Snakefile")
print(hash_result)
fcdcdbda6cc42ee8fac608ed9e2d4391

Data Pipeline

Ora che abbiamo un'idea di come siano fatti i dati, possiamo discutere di come questi dati possano venir messi insieme in una pipeline.

Per semplicità, li possiamo inserire in una gerarchia di rilevanza:

  1. metadati
  2. raw data
  3. codice sorgente
  4. source data
  5. usage data
  6. intermediate data
  7. temporary data

Questi nomi non sono formali, solo un modo per discuterne fra di noi, ma rispecchiano molto bene la tipica procedura di analisi.

1 - metadati

Dati a proposito dei dati.

Tipicamente un file di testo semplice, che descrive il motivo per cui i dati sono stati presi, chi li ha raccolti, quando e con che metodi.

Può sembrare banale, ma a distanza di qualche anno potreste trovarvi un disco pieno di dati di cui non capite la ragione d'essere.

Un dato di cui non si conosce il motivo di esistere ha lo stesso valore di un dato cancellato.

Spesso riporta anche l'hashing dei dati RAW per garantire di poterne verificare l'integrità.

2 - Raw data

Questi sono i dati originari dei vostri esperimenti. Non li userete direttamente nelle vostre analisi. Potrebbero essere nei formati più strampalati, a seconda dell'output dello strumento di misura.

Questi dati sono SACRI.

Vanno conservati scrupolosamente e non modificati.

Se si dovessero avere nuove versioni (ad esempio una misura è stata ripetuta ed aggiornata) non sovrascriveteli, ma tenete le varie versioni.

Dal punto di vista di FOSSIL per il controllo di versione, questo tipo di dati può essere tenuto nel repository come unversioned, visto che non dovrebbe essere modificato.

3 - Codice sorgente

Può sembrare banale, ma il vostro codice rappresenta comunque informazione a proposito del problema:

  • come vanno letti e manipolati i dati?
  • come correggere gli errori?
  • a cosa bisogna stare attenti?
  • quali sono le analisi già fatte?

Queste informazioni meritano di essere trattate come dati a tutti gli effetti, e sono quasi altrettanto importanti dei raw data.

È per questo che esiste il controllo di versione, usatelo!

4 - Source data

Una volta ottenuti i raw data, processateli tramite uno script in un formato decente. Tramite questo script potrete unire eventuali file frammentati (ad esempio da un file per persona ad un dataset unico), correggere errori nei raw data (non dovete andare a modificarli alla fonte!) e mettere il tutto in una struttura che possa essere mantenuta a lungo. La mia preferenza è per formati di testo come il csv.

Lo script di generazione è più importante dei dati risultanti!

In questa fase dovete mantenere i dati il più possibile aderenti alle informazioni dei file RAW. Non fate detrending, normalizzazioni o altre modifiche, solo trasportarli in un formato ragionevole.

Se i RAW fossero già ben formattati e strutturati e corretti, potete considerarli direttamente dei source. Non capita mai, non sperateci. Quando capita di solito vi stanno mentendo e non sono davvero i RAW ma dei dati già processati.

5 - Usage data

Qui iniziamo effettivamente la nostra analisi.

A partire dai dati source, possiamo comporre i dati nel formato più comodo per le analisi che abbiamo in mente, senza doverci preoccupare di cose come la normalizzazione delle tabelle del database e simili.

Tipicamente questi dati saranno generati solo una volta, a meno che non si riscontrino dei problemi nell'analisi (nel qual caso sarà necessario tornare indietro ai source data e capire cosa sia andato storto).

Diverse analisi potrebbero aver bisogno di diversi usage data.

6 - Intermediate Data

La vostra analisi sarà tipicamente composta da diverse fasi, quali la normalizzazione dei dati, il detrending, e così via.

Dopo ciascuno di questi step è solitamente buona pratica tener traccia dei risultati in dei file intermedi, in modo da poter riprendere l'analisi da qualsiasi step intermedio senza dover ripartire da capo.

Di solito l'unico inconveniente della perdita di questi dati è il tempo necessario per ripetere l'analisi.

7 - Temporary data

Questi dati sono il risultato intermedio delle analisi, ma con il preciso obiettivo di essere rimossi alla fine di ogni step.

Ad esempio potrebbero essere generati da un algoritmo parallelo distribuito che analizza un paziente alla volta, per poi ricomporre i risultati in un'unica tabella che li contiene tutti. Una volta generata la tabella complessiva, non c'è ragione di tenere le tabelle dei singoli pazienti in giro a consumare spazio, e vanno quindi eliminate.

Snakemake

Snakemake è una reimplementazione in python del classico programma UNIX make, usato per la compilazione del codice.

Snakemake rende l'automazione delle pipeline di analisi estremamente comoda e permette di scalare facilmente da sequenziale a parallelismo locale fino a calcolo su cluster.

Vi permette di collegare in modo molto semplice comandi di bash (e quindi programmi generici), script in python ed R (a cui può passare variabili direttamente in memoria) o di mescolare del codice python direttamente nello script.

Snakamake è solo uno dei tanti sistemi di gestione delle pipeline di dati.

È quello che reputo più vicino ai nostri bisogni, ma vi invito a guardare anche gli altri.

Alcuni esempi famosi sono:

  • Luigi
  • Apache Airflow
  • BigDataScript
  • Dask
  • Nextflow

una lista molto comprensiva la trovate alla pagina awesome-pipeline

La struttura fondamentale di Snakemake è una regola, che rappresenta un programma (tipicamente uno script), i file che ha bisogno di avere in input e quelli che restituirà in output.

Ciascuna regola viene eseguita come un nuovo processo python a se stante.

Una regola ha delle sotto sezioni, di cui le più importanti sono:

  • output: la lista dei file che la regola genererà in output (è una promessa, vanno poi effettivamente creati)
  • input: la lista dei file che la regola richiede
  • shell/run: esegue uno o più comandi di shell oppure esegue del codice python arbitrario

Di default, se non si specifica altro, snakemake cerca di eseguire la regola all

Se il file di output della regola esiste già, la regola non viene eseguita (a meno di non constringerlo).

Se il file di input della regola non esiste, snakemake cerca un'altra regola che abbia quel file come output e la esegue prima.

Questa è una struttura chiamata pull (in cui specifico il punto di arrivo), e richiede un po' di tempo per prenderci confidenza (lo standard della programmazione è di tipo push, in cui specifico il punto di partenza).

In [41]:
%%file Snakefile

rule all:
    shell:
        "echo 'hello world' > result.txt"
Overwriting Snakefile
In [42]:
!snakemake
Provided cores: 1
Rules claiming more threads will be scaled down.
Job counts:
	count	jobs
	1	all
	1

rule all:
    jobid: 0

Finished job 0.
1 of 1 steps (100%) done
In [43]:
%ls
result.txt  Snakefile

Se il file di output di una regola esiste già, ed è più recente dei file di input, la regola non viene eseguita.

Questo comportamento è detto idempotenza, e rende l'esecuzione dello script più prevedibile.

È comunque possibile forzare la mano a snakamake in vari modi (se vi servisse, li trovate sul manuale)

In [45]:
%%file Snakefile

rule all:
    output:
        "result.txt"
    shell:
        "echo 'hello world' > {output}"
Overwriting Snakefile
In [46]:
!snakemake
Nothing to be done.
In [47]:
%rm result.txt
In [48]:
!snakemake
Provided cores: 1
Rules claiming more threads will be scaled down.
Job counts:
	count	jobs
	1	all
	1

rule all:
    output: result.txt
    jobid: 0

Finished job 0.
1 of 1 steps (100%) done

Se i fil di input sono specificati e:

  1. non esistono
  2. non esiste una regola che li produca in output

Allora snakemake ritornerà un errore

In [49]:
%%file Snakefile

rule all:
    input:
        "parziali1.txt",
        "parziali2.txt"
    output:
        "result.txt"
    shell:
        "cat {input} > {output}"
Overwriting Snakefile
In [50]:
!snakemake
MissingInputException in line 2 of /home/enrico/lavoro/snakemake_lesson/Snakefile:
Missing input files for rule all:
parziali2.txt
parziali1.txt

Vediamo come appare uno script con due regole distinte, una per creare i due parziali ed una per processarli.

In [53]:
%%file Snakefile

rule all:
    input:
        "parziali1.txt",
        "parziali2.txt"
    output:
        "result.txt"
    shell:
        "cat {input} > {output}"
        
rule crea_parziali:
    output:
        "parziali1.txt",
        "parziali2.txt"
    run:
        for filename in output:
            with open(filename, 'w') as file:
                print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
In [54]:
!snakemake
Provided cores: 1
Rules claiming more threads will be scaled down.
Job counts:
	count	jobs
	1	all
	1	crea_parziali
	2

rule crea_parziali:
    output: parziali1.txt, parziali2.txt
    jobid: 1

Finished job 1.
1 of 2 steps (50%) done

rule all:
    input: parziali1.txt, parziali2.txt
    output: result.txt
    jobid: 0

Finished job 0.
2 of 2 steps (100%) done
In [55]:
%ls
parziali1.txt  parziali2.txt  result.txt  Snakefile
In [56]:
%cat parziali1.txt
risultato di parziali1.txt
In [57]:
%cat parziali2.txt
risultato di parziali2.txt
In [58]:
%cat result.txt
risultato di parziali1.txt
risultato di parziali2.txt

Se i risultati intermedi esistono già, non li esegue di nuovo

In [59]:
%rm result.txt
In [60]:
!snakemake
Provided cores: 1
Rules claiming more threads will be scaled down.
Job counts:
	count	jobs
	1	all
	1

rule all:
    input: parziali1.txt, parziali2.txt
    output: result.txt
    jobid: 0

Finished job 0.
1 of 1 steps (100%) done

In questo script la stessa funzione python crea tutti i file uno alla volta, in modo indipendente.

In realtà potrei eseguirlo in modo concorrente, senza doversi aspettare. Per fare questo posso usare le wildcards.

Possono provocare torsioni della materia grigia, ma atteniamoci al caso più semplice

In [61]:
%%file Snakefile

rule all:
    input:
        "parziali{numero}.txt"
    output:
        "result.txt"
    shell:
        "cat {input} > {output}"
        
rule crea_parziali:
    output:
        "parziali{numero}.txt"
    run:
        with open(output, 'w') as file:
            print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
In [62]:
!snakemake
WorkflowError in line 2 of /home/enrico/lavoro/snakemake_lesson/Snakefile:
Wildcards in input files cannot be determined from output files:
'numero'

Per usare le wildcard devo dare da qualche parte il comando expand, che assegna di vari possibili valori alle wildcards.

Posso avere più wildcards allo stesso momento, l'importante è inizializarle tutte.

Ci sono dei meccanismi per fare inferenza automatica delle wildcard, ma vi consiglio di prendere prima confidenza con la dichiarazione esplicita.

In [71]:
%%file Snakefile

numeri = [1, 2, 3, 4]

rule all:
    input:
        expand("parziali{numero}.txt", numero=numeri)
    output:
        "result.txt"
    shell:
        "cat {input} > {output}"
        
rule crea_parziali:
    output:
        out = "parziali{numero}.txt"
    run:
        filename = output.out
        with open(filename, 'w') as file:
            print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
In [66]:
%rm result.txt 
In [72]:
%ls
parziali1.txt  parziali2.txt  Snakefile
In [73]:
!snakemake
Provided cores: 1
Rules claiming more threads will be scaled down.
Job counts:
	count	jobs
	1	all
	2	crea_parziali
	3

rule crea_parziali:
    output: parziali3.txt
    jobid: 3
    wildcards: numero=3

Finished job 3.
1 of 3 steps (33%) done

rule crea_parziali:
    output: parziali4.txt
    jobid: 1
    wildcards: numero=4

Finished job 1.
2 of 3 steps (67%) done

rule all:
    input: parziali1.txt, parziali2.txt, parziali3.txt, parziali4.txt
    output: result.txt
    jobid: 0

Finished job 0.
3 of 3 steps (100%) done
In [74]:
%cat result.txt
risultato di parziali1.txt
risultato di parziali2.txt
risultato di parziali3.txt
risultato di parziali4.txt
In [75]:
%rm parziali1.txt
%rm result.txt

Snakamake mi permette anche di creare un grafico di flusso che visualizza tutto ciò che deve essere fatto, e ciò che invece è stato già fatto e non ha bisogno di una nuova esecuzione.

In [77]:
!snakemake --dag | dot -Tsvg > dag.svg

visualizzazione pipeline

Un'altra funzione estramamente utile è la creazione di un registro di provenance, che mi indica quali file sono stati creati da quale regola e con che parametri.

Questo permette di tenere traccia dell'origine di ciascun file in modo semplice.

È anche facile impostarlo in modo da appendere la provenance ad un log completo, dando così la storia di tutti i file creati e modificati nel tempo.

In [78]:
!snakemake --detailed-summary > provenance.tsv
In [81]:
import pandas as pd
pd.read_table("provenance.tsv", index_col=0)
Out[81]:
date rule version log-file(s) input-file(s) shellcmd status plan
output_file
result.txt - all - NaN parziali1.txt,parziali2.txt,parziali3.txt,parz... cat parziali1.txt parziali2.txt parziali3.txt ... missing update pending
parziali2.txt Sat Mar 11 12:59:59 2017 crea_parziali - NaN NaN - rule implementation changed no update
parziali1.txt - crea_parziali - NaN NaN - missing update pending
parziali4.txt Sat Mar 11 13:10:21 2017 crea_parziali - NaN NaN - ok no update
parziali3.txt Sat Mar 11 13:10:21 2017 crea_parziali - NaN NaN - ok no update
In [83]:
%rm *.txt
rm: cannot remove ‘*.txt’: No such file or directory
dag.svg  provenance.tsv  Snakefile

Se voglio eseguire più regole in parallelo (ovviamente rispettando l'ordine necessario di esecuzione di ciascun ramo), mi basta dare il comando --cores <N> e snakemake eseguirà in automatico tutto quello che riesce in parallelo.

Esiste un equivalente anche per lanciare la pipeline in un cluster di calcolo, rendendo molto semplice il calcolo distribuito.

In [85]:
!snakemake --cores 6
Provided cores: 6
Rules claiming more threads will be scaled down.
Job counts:
	count	jobs
	1	all
	4	crea_parziali
	5

rule crea_parziali:
    output: parziali1.txt
    jobid: 3
    wildcards: numero=1

rule crea_parziali:
    output: parziali3.txt
    jobid: 4
    wildcards: numero=3

rule crea_parziali:
    output: parziali4.txt
    jobid: 1
    wildcards: numero=4

rule crea_parziali:
    output: parziali2.txt
    jobid: 2
    wildcards: numero=2

Finished job 3.
1 of 5 steps (20%) done
Finished job 1.
2 of 5 steps (40%) done
Finished job 2.
3 of 5 steps (60%) done
Finished job 4.
4 of 5 steps (80%) done

rule all:
    input: parziali1.txt, parziali2.txt, parziali3.txt, parziali4.txt
    output: result.txt
    jobid: 0

Finished job 0.
5 of 5 steps (100%) done
In [91]:
%rm *.txt

Posso anche specificare delle risorse limitate (oltre i processori) in modo che la pipeline non ecceda nell'uso.

Ad esempio, se ho delle regole che richiedono una gran quantità di memoria, posso specificare il livello atteso di occupazione nella regola e poi specificare la memoria disponibile da linea di comando.

In [92]:
%%file Snakefile

numeri = [1, 2, 3, 4]

rule all:
    input:
        expand("parziali{numero}.txt", numero=numeri)
    output:
        "result.txt"
    shell:
        "cat {input} > {output}"
        
rule crea_parziali:
    output:
        out = "parziali{numero}.txt"
    resources: 
        memory = 6
    run:
        filename = output.out
        with open(filename, 'w') as file:
            print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
In [93]:
!snakemake --cores 6 --resources memory=12
Provided cores: 6
Rules claiming more threads will be scaled down.
Provided resources: memory=12
Job counts:
	count	jobs
	1	all
	4	crea_parziali
	5

rule crea_parziali:
    output: parziali2.txt
    jobid: 2
    wildcards: numero=2
    resources: memory=6

rule crea_parziali:
    output: parziali4.txt
    jobid: 1
    wildcards: numero=4
    resources: memory=6

Finished job 2.
1 of 5 steps (20%) done

rule crea_parziali:
    output: parziali3.txt
    jobid: 3
    wildcards: numero=3
    resources: memory=6

Finished job 1.
2 of 5 steps (40%) done

rule crea_parziali:
    output: parziali1.txt
    jobid: 4
    wildcards: numero=1
    resources: memory=6

Finished job 3.
3 of 5 steps (60%) done
Finished job 4.
4 of 5 steps (80%) done

rule all:
    input: parziali1.txt, parziali2.txt, parziali3.txt, parziali4.txt
    output: result.txt
    jobid: 0

Finished job 0.
5 of 5 steps (100%) done

configurazioni

Eventuali parametri di configurazione possono essere dati da linea di comando oppure caricati da un file di configurazione in formato YAML o JSON

In [14]:
%%file Snakefile

numeri = [i for i in range(int(config['number']))]

rule all:
    input:
        expand("parziali{numero}.txt", numero=numeri)
    output:
        "result.txt"
    shell:
        "cat {input} > {output}"
        
rule crea_parziali:
    output:
        out = "parziali{numero}.txt"
    resources: 
        memory = 6
    run:
        filename = output.out
        with open(filename, 'w') as file:
            print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
In [15]:
%rm *.txt
In [16]:
!snakemake --cores 6 --resources memory=12 --config number=4
Provided cores: 6
Rules claiming more threads will be scaled down.
Provided resources: memory=12
Job counts:
	count	jobs
	1	all
	4	crea_parziali
	5

rule crea_parziali:
    output: parziali1.txt
    jobid: 4
    wildcards: numero=1
    resources: memory=6

rule crea_parziali:
    output: parziali2.txt
    jobid: 2
    wildcards: numero=2
    resources: memory=6

Finished job 2.
1 of 5 steps (20%) done

rule crea_parziali:
    output: parziali3.txt
    jobid: 3
    wildcards: numero=3
    resources: memory=6

Finished job 4.
2 of 5 steps (40%) done

rule crea_parziali:
    output: parziali0.txt
    jobid: 1
    wildcards: numero=0
    resources: memory=6

Finished job 3.
3 of 5 steps (60%) done
Finished job 1.
4 of 5 steps (80%) done

rule all:
    input: parziali0.txt, parziali1.txt, parziali2.txt, parziali3.txt
    output: result.txt
    jobid: 0

Finished job 0.
5 of 5 steps (100%) done
In [17]:
%%file config.yaml
number: 4
Overwriting config.yaml
In [18]:
%%file Snakefile

configfile: "./config.yaml"

numeri = [i for i in range(int(config['number']))]

rule all:
    input:
        expand("parziali{numero}.txt", numero=numeri)
    output:
        "result.txt"
    shell:
        "cat {input} > {output}"
        
rule crea_parziali:
    output:
        out = "parziali{numero}.txt"
    resources: 
        memory = 6
    run:
        filename = output.out
        with open(filename, 'w') as file:
            print("risultato di {}".format(filename), file=file)
Overwriting Snakefile
In [19]:
%rm *.txt
In [20]:
!snakemake --cores 6 --resources memory=12
Provided cores: 6
Rules claiming more threads will be scaled down.
Provided resources: memory=12
Job counts:
	count	jobs
	1	all
	4	crea_parziali
	5

rule crea_parziali:
    output: parziali0.txt
    jobid: 4
    wildcards: numero=0
    resources: memory=6

rule crea_parziali:
    output: parziali3.txt
    jobid: 3
    wildcards: numero=3
    resources: memory=6

Finished job 3.
1 of 5 steps (20%) done

rule crea_parziali:
    output: parziali2.txt
    jobid: 1
    wildcards: numero=2
    resources: memory=6

Finished job 4.
2 of 5 steps (40%) done

rule crea_parziali:
    output: parziali1.txt
    jobid: 2
    wildcards: numero=1
    resources: memory=6

Finished job 1.
3 of 5 steps (60%) done
Finished job 2.
4 of 5 steps (80%) done

rule all:
    input: parziali0.txt, parziali1.txt, parziali2.txt, parziali3.txt
    output: result.txt
    jobid: 0

Finished job 0.
5 of 5 steps (100%) done

Esercizio

Nel sito trovate un link a dei file per questa lezione, ciascuno con dentro una semplice tabella che indica una sequenza di versamenti fatti da delle persone.

Ci sarà anche un file che indica gli hash md5 per ciascuno di questi file.

Scrivete una pipeline che li scarichi, controlli che la md5 hash è quella attesa, caricate i dati e scrivete in un file il totale risultante per ciascuna persona.

  • La cartella la trovate all'indirizzo https://chiselapp.com/user/EnricoGiampieri/repository/DataProgrammingCourse/doc/tip/snakemake_exercise/
  • ci sono 50 file chiamati transazioni_{}.tsv con l'indice da 00 a 49
  • il file di controllo degli hash è md5sums.tsv

suggerimenti

  • la funzione di hash può essere implementata in python o con il comando da terminale md5sum
  • i file possono essere scaricati da terminale con wget oppure da python con la libreria requests
  • usate le wildcard per ottenere i file, o non finite più!
In [ ]: