Curso
Hay muchas situaciones en las que necesitas analizar, modificar y procesar rápidamente gran cantidad de archivos, tanto en número como en tamaño. A menudo, además, son archivos de texto en formato CSV (valores separados por comas). Abrirlos con hojas de cálculo como Excel, LibreOffice u OpenOffice satura la memoria del equipo. Procesar muchos archivos en un solo lote también suele fallar tras unas horas por alguna anomalía inesperada.
Al final acabas perdiendo tiempo entre bloqueos, reinicios y esperas eternas.
Sin embargo, la mayoría de estas tareas podrían resolverse con unas pocas líneas de código. Es más, dominar unas cuantas órdenes básicas de shell puede ahorrarte mucho tiempo y frustración.
En este post te presento un resumen de varios comandos de shell que uso casi a diario. Espero que te resulten útiles.
Notas rápidas
Ten en cuenta que existen diferentes tipos de shell (bash, zsh, ...). Bash es el más común, ya que es el shell predeterminado en OS X y en las principales distribuciones de Linux. Zsh (y Oh My Zsh) es una alternativa muy popular y potente. Los comandos de shell incluidos en este artículo se han probado en bash sobre OS X (macOS) y deberían funcionar en otros shells y entornos.
Todos los ejemplos siguientes se basan en el conjunto de datos adult del repositorio de UCI Machine Learning, también conocido como "Census Income" y disponible aquí. Suele emplearse para predecir si los ingresos superan los \$50K/año a partir de datos censales. Con 48.842 filas y 14 atributos no es un dataset grande, pero es suficiente para ilustrar los ejemplos. Aunque los datos se guardan con la extensión .data, es un CSV bien formateado. ¡Descárgalo si quieres seguir el tutorial paso a paso!
Cuenta con wc
Ante un archivo de texto nuevo, quizá quieras saber cuántas líneas tiene. Puedes hacerlo con el comando de conteo wc -l:
$ wc -l adult.data
32562 adult.data
Esto indica que el archivo adult.data contiene 32.562 filas. La opción -l le dice a wc que cuente líneas. También puedes usar wc para contar palabras con la opción -w.
$ wc -w adult.data
488415 adult.data
El archivo adult.data contiene casi 500k palabras.
El comando wc también puede contar el número de archivos en un directorio usando como entrada la salida de un simple ls -l. Usar la salida de un comando como entrada de otro mediante la tubería | es un patrón muy útil llamado pipelining. Lo verás varias veces en este post.
Imagina ahora que tienes una carpeta con muchos archivos. El siguiente comando te dirá exactamente cuántos contiene:
$ ls -l <folder> | wc -l
508
Hay muchas más aplicaciones de wc si empiezas a usar comodines y subcarpetas.
Volvamos a tu archivo adult.data y mira las primeras líneas con head. Por defecto, head muestra 10 líneas; puedes limitar la salida a las 2 primeras con la opción -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
Y verás que el archivo no tiene fila de cabecera: los nombres de columna no están incluidos. Puedes añadirla concatenando dos archivos con el comando cat.
Concatena archivos con cat
El comando cat imprime el contenido de un archivo en la salida estándar (tu terminal). También sirve para concatenar varios archivos. El comando cat file_1.csv file_2.csv > target_file.csv fusionará el contenido de file_1.csv y file_2.csv en target_file.csv, añadiendo file_2.csv al final de file_1.csv.
El archivo de cabecera no está en el dataset original, así que tienes que crearlo. Para ello, haz echo de la lista de nombres de columna separados por comas en un archivo header.csv.
$ echo "age,workclass,fnlwgt,education,education-num,marital-status,occupation,relationship,race,sex,capital-gain,capital-loss,native-country,class" > header.csv
En este ejemplo, usaste la redirección de shell > para volcar la salida de cat al archivo adult.csv. El símbolo > crea el archivo o reemplaza por completo su contenido si ya existe. Si lo duplicas >>, se añadirá el nuevo contenido al final sin borrar lo anterior.
Ahora añade el archivo header.csv al principio de adult.data. De paso, renombra adult.data a adult.csv, ya que es, al fin y al cabo, un CSV.
$ cat header.csv adult.data > adult.csv
Comprueba que la primera fila de adult.csv contiene los nombres de columna 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 archivo con sed
Otro caso típico de minería de datos ocurre cuando un archivo está corrupto o mal formateado, por ejemplo con caracteres no UTF-8 o comas fuera de sitio. Puedes corregirlo sin abrirlo con el comando sed.
El patrón genérico de sed es
$ sed "s/<string to replace>/<string to replace it with>/g" <source_file> > <target_file>.
El dataset adult.csv usa el carácter ? para indicar valores ausentes. Puedes sustituirlo por un valor por defecto mejor usando sed. La cadena vacía es preferible para marcar valores ausentes, ya que se interpretará como NaN al cargar los datos en un DataFrame de Pandas.
$ grep ", ?," adult.csv | wc -l
2399
Esto te da un recuento de 2.399 líneas con al menos una columna con un valor ausente indicado por ?. El siguiente comando sustituirá todas las columnas con ? por una cadena vacía. Todas las celdas que solo contienen ? seguido de un espacio quedarán realmente vacías.
$ sed "s/, ?,/,,/g" adult.csv > adult.csv
Fíjate en que usas el delimitador de columna , en las cadenas origen y destino para evitar reemplazar signos de interrogación válidos que puedan aparecer en otras partes del dataset.
Crear un subconjunto de un archivo grande
Imagina ahora que trabajas con un archivo de más de 30 millones de filas. Mientras preparas tu script en Python o R, querrás probar el flujo con una muestra del archivo original y no cargar todo en memoria. Puedes combinar head y tail para crear una submuestra del dataset original.
headmuestra la primera parte de los archivos;tailmuestra la última parte
Como ya viste, estos comandos aceptan la opción -n (de --lines) para limitar el número de líneas en la salida. tail también acepta -f (de --follow) para mostrar en tiempo real los datos que se van añadiendo al final del archivo. Es muy útil para monitorizar logs mientras se ejecutan tus scripts con tail -f <logfile>.
Combinando la tubería | con head y tail, puedes extraer un número concreto de líneas de un archivo y exportarlas a un archivo de subconjunto. Por ejemplo, para extraer 20 líneas empezando en la línea 100:
$ head -n 120 adult.csv | tail -n 20 > adult_sample.csv
Primero tomas con head first_line + number_of_lines y luego aplicas tail para quedarte con number_of_lines. El comando genérico es:
$ head -n <total_lines> <source_file> | tail -n <number_of_lines> > <target_file>
donde total_lines = first_line + number_of_lines. Eso sí, tu muestra ya no tiene la fila de cabecera. Sabemos añadirla de nuevo en el subconjunto con cat concatenando el archivo de muestra y el header.csv que creamos antes:
$ cat adult_sample.csv header.csv > adult_sample_with_header.csv
Fíjate en que no usaste el nombre fuente adult\_sample.csv como nombre de destino, sino que creaste adult\_sample\_with\_header.csv. Usar cat con un destino que coincide con una de las fuentes puede producir resultados inesperados. Mejor crea un archivo nuevo como salida de cat.
Encontrar duplicados con uniq
Con el comando uniq puedes detectar líneas repetidas adyacentes en un archivo. uniq tiene varias opciones útiles:
uniq -c: añade el recuento de repeticiones a cada línea;uniq -d: muestra solo las líneas duplicadas; yuniq -u: muestra solo las líneas únicas.
Eso sí, uniq no es "listo": no detecta repeticiones si no son adyacentes. Por tanto, primero hay que sort el archivo. Este comando cuenta el número de líneas duplicadas en adult.csv:
$ sort adult.csv | uniq -d | wc -l
23
y muestra que hay 23 duplicados. El siguiente comando toma todas las líneas con su recuento, ordena en reversa y muestra los 3 duplicados principales:
$ 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, ...
Hay muchas combinaciones potentes si mezclas sort y uniq con distintas opciones. Usa man sort y man uniq para explorar más.
Seleccionar columnas con cut
Lo mejor de los CSV y los comandos de shell es que puedes trabajar a nivel de columna usando cut para seleccionar la que te interese. cut acepta dos opciones principales: -d para indicar el delimitador de columnas y -f para especificar las columnas. En el siguiente ejemplo, usas cut para conocer el número de valores únicos que toma la variable categórica workclass (columna 2).
Primero selecciona la columna workclass y encadena a head para verificar que es la correcta:
$ cut -d "," -f 2 adult.csv | head -3
workclass
State-gov
Self-emp-not-inc
Ahora, para contar únicos, ordena la salida de cut y encadénala a uniq -c así:
$ 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
Esto te indica, por ejemplo, que hay 1.837 valores nulos y que la clase mayoritaria es Private con 22.969 apariciones.
La línea anterior es similar al método value_counts() aplicado a una Serie con los datos de adult.csv.
Bucles
Hasta ahora, has trabajado sobre un único archivo. Para renombrar, procesar o transferir muchos archivos, añade bucles a tu caja de herramientas de shell. El formato genérico de un bucle en bash es:
while true; do
_do something_ ;
done
Pongámoslo en práctica. Imagina que tienes 1000 archivos con espacios en su nombre y quieres sustituir cada espacio por un guion bajo "_".
replace_source=' '
replace_target='_'
for filename in ./*.csv; do
new_filename=${filename//$replace_source/$replace_target}
mv "$filename" "$new_filename"
done
En este ejemplo:
- primero declaras dos variables,
replace_sourceyreplace_target, para el espacio y el guion bajo; - recorres todos los archivos
*.csvde la carpeta actual; - para cada
filename, creas unnew_filenamesustituyendo cada espacio por un guion bajo; - y renombras el archivo del nombre actual al nuevo con
mv.
De hecho, no solo has usado shell para iterar por archivos en un directorio, sino que también has creado variables.
Variables
Terminamos esta introducción a los comandos de shell con variables. Crear variables en shell es tan simple como:
$ varname='<a string>'
$ varname=a number
Por ejemplo:
$ varname='Hello world'
$ varname=123.4
Observa que no hay espacios alrededor del signo '='. var = '<a string>' no funcionaría. Para ver el valor, simplemente haz echo:
$ echo $varname
123.4
Y para usarla en un script, enciérrala entre comillas como ya viste:
$ mv "$filename" "$new_filename"
Escribir bucles y usar variables abre la puerta a manipulaciones de archivos más complejas y es la antesala del scripting en shell, que va más allá de este artículo introductorio. Aun así, el scripting en shell es accesible si empiezas poco a poco y vas ganando confianza.
Conclusión
Las líneas de comando de shell son tremendamente útiles en tu día a día como profesional de datos. Hay muchos más ejemplos y casos de uso por explorar. Y, como verás, casi siempre hay distintas formas de lograr un mismo resultado combinando comandos y opciones. En esos casos, mantenerlo simple suele ser la mejor estrategia.
Ojalá estos ejemplos te ayuden a integrar los comandos de shell en tu kit de procesamiento de datos. Si te apetece, comparte tus trucos de shell conmigo en Twitter: @alexip.
Echa un vistazo a nuestro tutorial Configura un entorno de data science en tu ordenador.


