Ga naar hoofdinhoud

8 handige shellopdrachten voor data science

Welke shellopdrachten gebruiken data scientists bijna elke dag? Ontdek ze en leer ze gebruiken in deze tutorial!
· 12 min lezen

Verken met AI

Er zijn veel situaties waarin je snel grote bestanden wilt analyseren, wijzigen en verwerken, zowel in aantal als in omvang. Bestanden zijn echter vaak tekstgebaseerde CSV-bestanden (Comma Separated Values). Ze openen met standaard spreadsheet-apps zoals Excel, LibreOffice of OpenOffice overbelast het geheugen van je machine. Ook het in één batch verwerken van een groot aantal bestanden gaat vaak na een paar uur mis door een onverwachte bestandsafwijking.

Je brengt onvermijdelijk veel tijd door met vastgelopen schermen, herstarts en lange wachttijden.

Toch kunnen de meeste van deze taken worden uitgevoerd met een paar regels code. Sterker nog, vertrouwdheid met een paar simpele shellopdrachten kan je veel tijd en frustratie besparen.

In deze post geef ik een overzicht van enkele shellopdrachten die ik bijna elke dag gebruik. Hopelijk heb je er wat aan.

shell data science

Korte notities

Let op dat er verschillende typen shells zijn (bash, zsh, ...), waarbij bash het meest voorkomt omdat het de standaard shell is op OS X en de meeste Linux-distributies. Zsh (en Oh My Zsh) is een populaire en krachtige alternatieve shell. De shellopdrachten in deze blogpost zijn getest op bash op OS X (macOS) en zouden ook in andere shells en omgevingen moeten werken.

Alle onderstaande voorbeelden zijn gebaseerd op de adult dataset uit de UCI Machine Learning-repository, ook wel de "Census Income"-dataset genoemd en hier beschikbaar. Deze dataset wordt vaak gebruikt om te voorspellen of het inkomen hoger is dan \$50K/jaar op basis van censusgegevens. Met 48842 rijen en 14 attributen is het zeker geen grote dataset, maar wel voldoende om de voorbeelden te illustreren. Hoewel de data de extensie .data heeft, is het een goed geformatteerd CSV-bestand. Voel je vrij om de dataset te downloaden en mee te doen!

Tellen met wc

Bij een nieuw tekstbestand wil je weten hoeveel regels het bevat. Dat kan met de word count-opdracht wc -l:

$ wc -l adult.data
  32562 adult.data

Dit vertelt je dat het bestand adult.data 32562 rijen bevat. De vlag -l geeft aan dat wc de regels moet tellen. Maar je kunt met wc ook woorden tellen met de vlag -w.

$ wc -w adult.data
  488415 adult.data

Het bestand adult.data bevat bijna 500k woorden.

De opdracht wc kan ook het aantal bestanden in een map tellen door de uitvoer van een simpele ls -l-opdracht als input voor wc te gebruiken. De uitvoer van de ene opdracht als invoer voor een andere gebruiken via het pipe-symbool | is een handig shellpatroon dat pipelining heet. Je ziet dit in meerdere voorbeelden in deze post.

Stel dat je nu een map met veel bestanden hebt. Met de volgende opdracht zie je precies hoeveel bestanden erin staan:

$ ls -l <folder> | wc -l
 508

Er zijn veel andere toepassingen voor wc als je wildcards en submappen gaat gebruiken.

Laten we teruggaan naar je bestand adult.data en met de opdracht head naar de eerste regels kijken. Standaard geeft head 10 regels weer; je beperkt de uitvoer tot de eerste 2 regels met de vlag -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

En je ziet dat het bestand geen headerregel heeft. De kolomnamen staan niet in het bestand. Je kunt die headerregel toevoegen door twee bestanden te concatenaten met de opdracht cat.

Bestanden samenvoegen met cat

De opdracht cat print de inhoud van een bestand naar de standaarduitvoer (oftewel je terminal). Je kunt er ook een reeks bestanden mee samenvoegen. De opdracht cat file_1.csv file_2.csv > target_file.csv voegt de inhoud van zowel file_1.csv als file_2.csv samen in target_file.csv, waarbij file_2.csv achter file_1.csv wordt geplakt.

Het headerbestand zit niet in de originele dataset en moet je aanmaken. Doe dat door de kommagescheiden lijst met kolomnamen te echoen naar een bestand 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 dit voorbeeld heb je de shell-omleiding > gebruikt om de uitvoer van cat naar het bestand adult.csv te sturen. De > maakt een bestand aan of overschrijft de inhoud volledig als het bestand al bestaat. Door het symbool te verdubbelen >> voeg je nieuwe inhoud toe aan een bestaand bestand zonder de inhoud te verwijderen.

Laten we nu het bestand header.csv aan het begin van adult.data toevoegen. Tegelijkertijd hernoem je adult.data naar adult.csv omdat het tenslotte een CSV-bestand is.

$ cat header.csv adult.data > adult.csv

Controleer of de eerste rij van adult.csv de kolomnamen bevat met:

$ 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

Een bestand aanpassen met sed

Een andere veelvoorkomende datasituatie doet zich voor wanneer een bestand corrupt of slecht geformatteerd is, bijvoorbeeld met niet-UTF-8-tekens of een komma op de verkeerde plek. Je kunt dat bestand corrigeren zonder het te openen met de opdracht sed.

Het generieke sed-patroon is

$ sed "s/<string to replace>/<string to replace it with>/g" <source_file> > <target_file>.

De dataset adult.csv gebruikt het teken ? om een ontbrekende waarde aan te duiden. Je kunt dat teken vervangen door een beter passende standaardwaarde met sed. De lege string is te verkiezen als aanduiding van een ontbrekende waarde, omdat die als NaN wordt geïnterpreteerd bij het inladen van de data in een Pandas DataFrame.

$ grep ", ?," adult.csv | wc -l
2399

Dit geeft je een telling van 2399 regels met minstens één kolom met een ontbrekende waarde aangeduid door ?. De volgende opdracht vervangt alle kolommen met ? door een lege string. Alle cellen die alleen ? bevatten, gevolgd door een spatie, zijn nu echt leeg.

$ sed "s/, ?,/,,/g" adult.csv >  adult.csv

Let op dat je de kolomscheidingsteken , in de bron- en doelstrings gebruikt om te voorkomen dat je legitieme vraagtekens vervangt die elders in de dataset kunnen staan.

Een groot bestand subsetten

Stel je werkt nu met een bestand met meer dan 30 miljoen rijen. Terwijl je je Python- of R-script maakt, wil je je workflow testen op slechts een sample van dat grote oorspronkelijke bestand en niet de hele dataset in het geheugen laden. Je kunt een combinatie van head en tail gebruiken om een subsample van de originele dataset te maken.

  • head geeft het eerste deel van bestanden weer;
  • tail geeft het laatste deel van bestanden weer

Zoals je eerder zag, nemen deze opdrachten een vlag -n, wat staat voor --lines, die het aantal regels in de uitvoer beperkt. tail heeft ook een vlag -f waarbij f staat voor --follow, die nieuwe regels toont zodra ze aan het einde van het bestand worden toegevoegd. Dit is vooral handig om logbestanden te monitoren terwijl je scripts draaien met tail -f <logfile>.

Door het pipe-symbool | te combineren met head en tail, kun je een bepaald aantal regels uit een bronbestand halen en de inhoud exporteren naar een subsetbestand. Bijvoorbeeld, om 20 regels te extraheren vanaf regel 100:

$ head -n 120 adult.csv | tail -n 20 > adult_sample.csv

Let op dat je eerst de first_line + number_of_lines pakt met head en daarna met tail de number_of_lines. De generieke subset-opdracht is:

$ head -n <total_lines> <source_file> | tail -n <number_of_lines> > <target_file>

waarbij total_lines = first_line + number_of_lines. Je nieuwe samplebestand bevat echter niet langer de headerregel. We weten al hoe we de header terugzetten in het subsetbestand met cat om het subsetbestand en het eerder gemaakte header.csv te concatenaten:

$ cat adult_sample.csv header.csv > adult_sample_with_header.csv

Let op dat je de bronbestandsnaam adult\_sample.csv niet als doelbestandsnaam hebt gebruikt, maar in plaats daarvan een nieuw bestand hebt gemaakt met de naam adult\_sample\_with\_header.csv. Dezelfde naam gebruiken als een van de bronnen én als doel bij cat leidt tot onverwachte bestandsinhoud. Maak beter een nieuw bestand als uitvoer van de opdracht cat.

Duplicaten vinden met uniq

Met de opdracht uniq kun je naast elkaar staande herhaalde regels in een bestand vinden. uniq kent meerdere vlaggen, waarbij de meest nuttige zijn:

  • uniq -c: voegt het aantal herhalingen toe aan elke regel;
  • uniq -d: geeft alleen dubbele regels weer; en
  • uniq -u: geeft alleen unieke regels weer.

Maar uniq is geen slimme opdracht. Herhaalde regels worden niet gedetecteerd als ze niet naast elkaar staan. Dat betekent dat je het bestand eerst moet sorten. Deze opdracht telt het aantal dubbele regels in adult.csv.

$ sort adult.csv | uniq -d | wc -l
23

en laat zien dat er 23 duplicaten zijn. De volgende opdracht neemt de uitvoer van alle regels met herhaaltellingen, sorteert omgekeerd en geeft de eerste 3 duplicaten weer:

$ 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, ...

Er zijn veel krachtige mogelijkheden door sort en uniq met verschillende vlaggen te combineren. Gebruik man sort en man uniq om deze opdrachten verder te verkennen.

Kolommen selecteren met cut

Het mooie van CSV-bestanden en shellopdrachten is dat je ook op kolomniveau kunt werken met cut om een bepaalde kolom te selecteren. cut heeft twee hoofdvlaggen: -d om de kolomscheidingsteken te specificeren en -f om de kolommen op te geven waarmee je wilt werken. In het volgende voorbeeld gebruik je cut om het aantal unieke waarden te vinden van de categorische variabele workclass (kolom 2).

Selecteer eerst de kolom workclass en pipe naar head om te controleren of je de juiste kolom hebt:

$ cut -d "," -f 2 adult.csv | head -3
workclass
State-gov
Self-emp-not-inc

Om nu uniques te tellen, sorteer je de uitvoer van cut en pipe je het resultaat naar uniq -c, als volgt:

$  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

Dit vertelt je bijvoorbeeld dat je 1837 null-waarden hebt, en dat de hoofdklasse veruit de Private-klasse is met 22969 voorkomens.

De bovenstaande opdrachtregel lijkt op de value_counts()-methode toegepast op een DataFrame met de adult.csv-data.

Lussen

Tot nu toe heb je vooral met één bestand gewerkt. Om grote aantallen bestanden te hernoemen, verwerken of verplaatsen, voeg je lussen toe aan je shell-toolkit. Het generieke formaat van een lus in bash is:

while true; do
    _do something_ ;
done

Laten we dat toepassen. Stel je hebt 1000 bestanden met spaties in de bestandsnaam en je wilt elke spatie vervangen door een underscore "_".

replace_source=' '
replace_target='_'
for filename in ./*.csv; do
    new_filename=${filename//$replace_source/$replace_target}
    mv "$filename" "$new_filename"
done

In dit voorbeeld:

  • Declareer je eerst twee variabelen: replace_source en replace_target als placeholders voor het spatie- en underscoreteken
  • Loop je over alle *.csv-bestanden in de huidige map
  • maak je voor elke filename een new_filename door elke spatie te vervangen door een underscore
  • en hernoem je het bestand van de huidige bestandsnaam naar de nieuwe bestandsnaam met de opdracht mv

Je hebt hier niet alleen de shell gebruikt om door bestanden in een map te lopen, maar ook variabelen aangemaakt.

Variabelen

We sluiten deze introductie tot shellopdrachten af met variabelen. Een variabele aanmaken in shell doe je simpelweg met

$ varname='<a string>'
$ varname=a number

Bijvoorbeeld:

$ varname='Hello world'
$ varname=123.4

Let op dat er geen spaties rond het '='-teken staan. var = '<a string>' zou niet werken. Om de waarde van de variabele te tonen, echo je die simpelweg:

$ echo $varname
123.4

En om hem in een script te gebruiken, zet je hem tussen aanhalingstekens zoals je eerder zag:

$ mv "$filename" "$new_filename"

Het schrijven van lussen en het gebruiken van variabelen opent de deur naar complexere bestandsmanipulaties en is een opstap naar shellscripting, wat buiten deze introductie valt. Shellscripting is echter prima te doen als je klein begint en verder uitbreidt naarmate je vertrouwen groeit.

Conclusie

Shellopdrachten zijn enorm nuttig in je dagelijkse werk als data scientist. Er zijn nog veel meer voorbeelden en use-cases te verkennen. En zoals je zult zien, zijn er vaak verschillende manieren om een bepaald resultaat te bereiken met een breed scala aan shellopdrachten en bijbehorende vlaggen. In dat geval is het devies: houd het simpel — dat wint altijd.

Hopelijk helpen de voorbeelden hier je om shellopdrachten te integreren in je dataprocestoolkit. Laat gerust iets van je horen en deel je shelltrucs met me op Twitter: @alexip.

Bekijk ook onze tutorial Een data science-omgeving op je computer instellen.

Onderwerpen

Gerelateerd