Corso
Capita spesso di dover analizzare, modificare e processare rapidamente grandi quantità di file, sia per numero che per dimensioni. Spesso, però, si tratta di file di testo in formato CSV (Comma Separated Values). Aprirli con applicazioni foglio di calcolo come Excel, LibreOffice o OpenOffice sovraccarica la memoria della macchina. Inoltre, elaborare un gran numero di file in un unico batch spesso fallisce dopo qualche ora a causa di qualche anomalia imprevista in uno dei file.
Finisci inevitabilmente per passare molto tempo tra schermate bloccate, riavvii e lunghe attese.
Eppure, la maggior parte di queste operazioni può essere svolta con poche righe di codice. Ancora meglio, un po' di dimestichezza con alcune semplici righe di comando della shell può farti risparmiare tempo e ridurre la frustrazione.
Questo post offre una panoramica di alcuni comandi della shell che uso quasi ogni giorno. Spero ti siano utili.
Note rapide
Nota che esistono diversi tipi di shell (bash, zsh, ...), con bash la più comune poiché è la shell predefinita su OS X e sulle principali distribuzioni linux. Zsh (e Oh My Zsh) è un'alternativa popolare e potente. I comandi della shell inclusi in questo post sono stati testati su bash su OS X (macOS) e dovrebbero funzionare anche con altre shell e ambienti.
Tutti gli esempi seguenti si basano sul dataset adult del repository UCI Machine Learning, noto anche come dataset "Census Income" e disponibile qui. Questo dataset è comunemente usato per prevedere se il reddito supera i \$50K/anno sulla base di dati censuari. Con 48842 righe e 14 attributi, non è affatto un dataset grande, ma è sufficiente per illustrare gli esempi. Sebbene i dati abbiano estensione .data, si tratta di un file CSV ben formattato. Sentiti libero di scaricare il dataset per seguire passo passo!
Conta con wc
Dato un nuovo file di testo, vuoi sapere quante righe contiene. Puoi farlo con il comando di conteggio parole wc -l:
$ wc -l adult.data
32562 adult.data
Che ti dice che il file adult.data contiene 32562 righe. Il flag -l dice a wc di contare le righe. Puoi però usare wc anche per contare le parole usando il flag -w.
$ wc -w adult.data
488415 adult.data
Il file adult.data contiene quasi 500mila parole.
Il comando wc può anche contare il numero di file in una directory usando come input l'output di un semplice comando ls -l. Usare l'output di un comando come input di un altro comando mediante la pipe | è uno schema utile della shell chiamato pipelining. Lo vedrai in diversi esempi lungo questo post.
Supponi ora di avere una cartella con molti file. Il comando seguente ti dirà esattamente quanti file contiene:
$ ls -l <folder> | wc -l
508
Ci sono molte altre applicazioni per wc se inizi ad aggiungere wildcard e sottocartelle.
Torniamo al tuo file adult.data e guardiamo le prime righe con il comando head. Per impostazione predefinita, head stampa 10 righe; puoi limitare l'output alle prime 2 righe usando il flag -n.
$ head -n 2 adult.data
39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, Not-in-family, White, Male, 2174, 0, 40, United-States, <=50K
50, Self-emp-not-inc, 83311, Bachelors, 13, Married-civ-spouse, Exec-managerial, Husband, White, Male, 0, 0, 13, United-States, <=50K
E noti che il file non ha una riga di intestazione. I nomi delle colonne non sono presenti nel file. Puoi aggiungere quella riga di intestazione concatenando due file con il comando cat.
Concatena file con cat
Il comando cat stampa il contenuto di un file sullo standard output (cioè il tuo terminale). Può anche essere usato per concatenare una serie di file. Il comando cat file_1.csv file_2.csv > target_file.csv unirà il contenuto di file_1.csv e file_2.csv in target_file.csv, aggiungendo file_2.csv alla fine di file_1.csv.
Il file di intestazione non è nel dataset originale e devi crearlo. Per farlo, fai l'echo dell'elenco di nomi di colonne separati da virgola in un file header.csv.
$ echo "age,workclass,fnlwgt,education,education-num,marital-status,occupation,relationship,race,sex,capital-gain,capital-loss,native-country,class" > header.csv
In questo esempio, hai usato il carattere di redirezione della shell > per riversare l'output di cat nel file adult.csv. Il > creerà un file o ne sostituirà completamente il contenuto se il file esiste già. Raddoppiando il simbolo >> aggiungerai il nuovo contenuto a un file già esistente senza cancellarne il contenuto.
Aggiungiamo ora il file header.csv all'inizio del file adult.data. Allo stesso tempo, rinomini adult.data in adult.csv visto che, in fin dei conti, è un file in formato CSV.
$ cat header.csv adult.data > adult.csv
Controlla che la prima riga di adult.csv contenga i nomi delle colonne con:
$ head -n 1 adult.csv
age,workclass,fnlwgt,education,education-num,marital-status,occupation,relationship,race,sex,capital-gain,capital-loss,native-country,class
39, State-gov, 77516, Bachelors, 13, Never-married, Adm-clerical, Not-in-family, White, Male, 2174, 0, 40, United-States, <=50K
Modifica un file con sed
Un altro scenario frequente nel data mining si verifica quando un file è corrotto o formattato male, ad esempio con caratteri non UTF-8 o una virgola fuori posto. Puoi correggere quel file senza nemmeno aprirlo usando il comando sed.
Il pattern generico di sed è
$ sed "s/<string to replace>/<string to replace it with>/g" <source_file> > <target_file>.
Il dataset adult.csv usa il carattere ? per indicare un valore mancante. Puoi sostituire quel carattere con un valore di default più adatto usando sed. La stringa vuota è preferibile come indicazione di un valore mancante, poiché verrà interpretata come valore NaN quando carichi i dati in un DataFrame di Pandas.
$ grep ", ?," adult.csv | wc -l
2399
Questo ti restituisce un conteggio di 2399 righe con almeno una colonna con un valore mancante indicato da ?. Il comando seguente sostituirà tutte le colonne con ? con una stringa vuota. Tutte le celle che contengono solo ?, seguito da uno spazio, ora saranno veramente vuote.
$ sed "s/, ?,/,,/g" adult.csv > adult.csv
Nota che usi il delimitatore di colonna , nelle stringhe sorgente e destinazione per evitare di sostituire punti interrogativi legittimi che potrebbero essere presenti altrove nel dataset.
Sottocampionare un file grande
Immagina ora di avere a che fare con un file con oltre 30 milioni di righe. Mentre scrivi il tuo script in Python o R, vorrai testare il workflow solo su un campione di quel file originale di grandi dimensioni, senza dover caricare l'intero dataset in memoria. Puoi usare una combinazione di head e tail per creare un sottoinsieme del dataset originale.
headstampa la prima parte dei file;tailstampa l'ultima parte dei file
Come hai visto prima, questi comandi accettano un flag -n, abbreviazione di --lines, che limita il numero di righe nell'output. tail accetta anche un flag -f, dove f sta per --follow, che stampa i dati man mano che nuove righe vengono aggiunte alla fine del file. Questo è particolarmente comodo per monitorare file di log mentre i tuoi script sono in esecuzione usando tail -f <logfile>.
Combinando la pipe | con head e tail, puoi estrarre un certo numero di righe dall'interno di un file sorgente ed esportarne il contenuto in un file di subset. Ad esempio, per estrarre 20 righe a partire dalla riga 100:
$ head -n 120 adult.csv | tail -n 20 > adult_sample.csv
Nota che prima prendi le first_line + number_of_lines con head e poi con tail prendi le number_of_lines. Il comando generico per il sottocampionamento è:
$ head -n <total_lines> <source_file> | tail -n <number_of_lines> > <target_file>
dove total_lines = first_line + number_of_lines. Tuttavia, il tuo nuovo file campione non contiene più la riga di intestazione. Sappiamo già come aggiungere l'intestazione nel file di subset usando cat per concatenare il file di subset e il file header.csv creato prima:
$ cat adult_sample.csv header.csv > adult_sample_with_header.csv
Nota che non hai usato il nome del file sorgente adult\_sample.csv come nome del file di destinazione, ma hai invece creato un nuovo file chiamato adult\_sample\_with\_header.csv. Avere lo stesso nome di uno dei sorgenti e della destinazione mentre usi cat porterà a contenuti di file imprevisti. È meglio creare un nuovo file come output del comando cat.
Trovare i duplicati con uniq
Con il comando uniq puoi trovare righe ripetute adiacenti in un file. uniq accetta diversi flag, i più utili sono:
uniq -c: aggiunge il conteggio di ripetizione a ogni riga;uniq -d: stampa solo le righe duplicate; euniq -u: stampa solo le righe uniche.
Tuttavia, uniq non è un comando "intelligente". Le righe ripetute non verranno rilevate se non sono adiacenti. Questo significa che prima devi sort il file. Questo comando conta il numero di righe duplicate in adult.csv.
$ sort adult.csv | uniq -d | wc -l
23
e mostra che ci sono 23 duplicati. Il comando successivo prende l'output di tutte le righe con i conteggi di ripetizione aggiunti, ordina in ordine inverso e stampa i primi 3 duplicati:
$ sort adult.csv | uniq -c | sort -r | head -n 3
3 25, Private, 195994, 1st-4th, 2, Never-married, ...
2 90, Private, 52386, Some-college, 10, Never-married, ...
2 49, Self-emp-not-inc, 43479, Some-college, 10, Married-civ-spouse, ...
Ci sono molte opzioni potenti che puoi ottenere combinando sort e uniq con diversi flag. Usa man sort e man uniq per esplorare ulteriormente questi comandi.
Selezionare colonne con cut
Il bello dei file CSV e dei comandi della shell è che puoi lavorare anche a livello di colonna usando cut per selezionare una colonna particolare. cut accetta due flag principali: -d per specificare il delimitatore di colonna e -f per specificare le colonne su cui vuoi lavorare. Nell'esempio seguente, usi cut per trovare il numero di valori unici assunti dalla variabile categorica workclass (colonna 2).
Per prima cosa seleziona la colonna workclass e manda in pipe a head per verificare di avere la colonna giusta:
$ cut -d "," -f 2 adult.csv | head -3
workclass
State-gov
Self-emp-not-inc
Ora, per contare gli unici, ordina l'output di cut e manda il risultato in pipe a uniq -c, così:
$ cut -d "," -f 2 adult.csv | sort | uniq -c
1837
960 Federal-gov
2093 Local-gov
7 Never-worked
22696 Private
1116 Self-emp-inc
2541 Self-emp-not-inc
1298 State-gov
14 Without-pay
1 workclass
Che ti dice, per esempio, che hai 1837 valori nulli e che la classe principale è di gran lunga la classe Private con 22969 occorrenze.
La riga di comando sopra è simile al metodo value_counts() applicato a un DataFrame contenente i dati di adult.csv.
Loop
Finora hai lavorato principalmente su un file. Per rinominare, elaborare o trasferire grandi quantità di file, dovresti aggiungere i loop al tuo toolkit di shell. Il formato generico di un loop nella shell bash è:
while true; do
_do something_ ;
done
Mettiamolo al lavoro. Immagina di avere 1000 file con spazi nel nome e di voler sostituire ogni spazio con un trattino basso "_".
replace_source=' '
replace_target='_'
for filename in ./*.csv; do
new_filename=${filename//$replace_source/$replace_target}
mv "$filename" "$new_filename"
done
In questo esempio,
- per prima cosa dichiari due variabili:
replace_sourceereplace_targetcome segnaposto per lo spazio e il carattere di underscore - iteri su tutti i file
*.csvnella cartella corrente - per ogni
filename, crei unnew_filenamesostituendo ogni spazio con un underscore - e rinomini il file dal nome attuale al nuovo nome con il comando
mv
In effetti, non solo hai usato la shell per ciclare attraverso i file in una directory, ma hai anche creato variabili.
Variabili
Concludiamo questa introduzione ai comandi della shell con le variabili. La creazione di variabili in shell si fa semplicemente così
$ varname='<a string>'
$ varname=a number
Per esempio:
$ varname='Hello world'
$ varname=123.4
Nota che non ci sono spazi attorno al segno '='. var = '<a string>' non funzionerebbe. Per restituire il valore della variabile, fai semplicemente echo:
$ echo $varname
123.4
E per usarla in uno script, incapsulala tra virgolette come visto prima:
$ mv "$filename" "$new_filename"
Scrivere loop e usare variabili apre la strada a manipolazioni di file più complesse ed è un punto di accesso alla scrittura di script di shell, che va oltre questo articolo introduttivo. Tuttavia, la shell script è abbastanza semplice se inizi in piccolo e migliori man mano che cresce la tua confidenza.
Conclusione
Le righe di comando della shell sono estremamente utili nel lavoro quotidiano da data scientist. Ci sono molti altri esempi e casi d'uso che si potrebbero esplorare. E come vedrai, spesso esistono diversi modi per ottenere un determinato risultato usando un'ampia gamma di comandi della shell e relativi flag. In questi casi, mantenere le cose semplici è sempre la strategia vincente.
Speriamo che gli esempi presentati qui ti aiutino a integrare i comandi della shell nel tuo toolkit di elaborazione dati. Sentiti libero di contattarmi e condividere i tuoi trucchi di shell su Twitter: @alexip.
Dai un'occhiata al nostro tutorial Configura un ambiente di data science sul tuo computer.

