Pedro Hernández
Pedro Hernández Estudiante de ciencias de la computación y administrador de CS Blog.

Docker y Ciencia de Datos

Docker y Ciencia de Datos

En esta publicación se realizará una limpieza de datos de Covid-19 en México utilizando la línea de comandos de Unix y se generará un contenedor de Docker para la reproducibilidad del proceso.

Objetivo

El objetivo de este pequeño tratamiento de datos es obtener el número total de casos positivos y negativos de Covid-19 para cada municipio de Sonora, partiendo de la base de datos federal correspondiente a dicha enfermedad. Estos resultados se almacenarán en un archivo csv. De este modo, tendremos un conjunto de datos limpios sobre los cuales podríamos aplicar técnicas de análisis.

Prerrequisitos

El único requisito es tener instalado Docker en nuestra computadora. Aquí puedes consultar las guías de instalación.

El código fuente de este mini proyecto lo puedes encontrar en este repositorio.

Cómo usarlo

Generar la imagen

Para generar la imagen de Docker necesitamos:

  1. Instalar Docker
  2. Clonar o descargar el repositorio del proyecto
  3. Situarnos en la carpeta del repositorio
  4. Ejecutar el siguiente comando:
1
$ docker build -t <nombre_imagen> .

Recuerda reemplazar la cadena < nombre_imagen > por el nombre que deseas asignar a la imagen que se va a construir.

Crear un contenedor a partir de la imagen generada

Para crear un contenedor persistente con la imagen que acabamos de crear ejecutamos la siguiente instrucción:

1
$ docker run -it --name <nombre_contenedor> <nombre_imagen>

Recuerda reemplazar la cadena < nombre_contenedor > por el nombre que deseas asignar al contenedor que se va a construir.

El nombre de la imagen < nombre_imagen > debe coincidir con el nombre que escribiste al momento de generar la imagen.

En este momento se desplegará la terminal del contenedor y podrás ver algo parecido a lo siguiente:

1
(base) root@3328f252cf7f:/data_cleaning#

Si enlistamos los archivos contenidos en el directorio actual tendremos lo siguiente:

1
2
3
(base) root@3328f252cf7f:/data_cleaning# ls
201128_Catalogos.xlsx	 data_processing.yml  numero_positivos_y_negativos_municipios_sonora.csv
210422COVID19MEXICO.csv  fix_data.py	      positivos_y_negativos_municipios_sonora.csv

El archivo llamado positivos_y_negativos_municipios_sonora.csv es el que almacena el resultado deseado. Podemos echarle un vistazo y confirmar que contiene la sumatoria de los casos positivos y negativos a Covid-19 para cada municipio del estado de Sonora.

1
2
3
4
5
6
7
8
9
10
11
(base) root@3328f252cf7f:/data_cleaning# cat positivos_y_negativos_municipios_sonora.csv | head -n 10
TOTAL,MUNICIPIO_RES,CLASIFICACION_FINAL
17,ACONCHI,CASO DE SARS-COV-2  CONFIRMADO
14,ACONCHI,NEGATIVO A SARS-COV-2
1021,AGUA PRIETA,CASO DE SARS-COV-2  CONFIRMADO
851,AGUA PRIETA,NEGATIVO A SARS-COV-2
178,ALAMOS,CASO DE SARS-COV-2  CONFIRMADO
143,ALAMOS,NEGATIVO A SARS-COV-2
76,ALTAR,CASO DE SARS-COV-2  CONFIRMADO
82,ALTAR,NEGATIVO A SARS-COV-2
21,ARIVECHI,CASO DE SARS-COV-2  CONFIRMADO

Si deseas conocer el procedimiento para obtener dicho resultado te invito a continuar leyendo este post.

Procedimiento

El procedimiento consta de dos sencillos pasos:

  1. Para la descarga y limpieza preliminar de los datos usaremos las herramientas de la línea de comandos de Unix, más específicamente, utilizaremos la distribución de Ubuntu.
  2. Con el paso anterior obtendremos un archivo csv pero cada casilla contendrá las claves de los valores reales. Por lo tanto, es necesaria una segunda etapa para sustituir dichas claves y así obtener un resultado humanamente legible. Para esta etapa nos apoyaremos de un script de Python utilizando la librería Pandas.

Este procedimiento se realizará desde un Dockerfile, por lo tanto, cualquier usuario puede reproducirlo y obtendrá exactamente el mismo resultado; además, nos ahorraremos todos los problemas de configuración de nuestro ambiente de trabajo ¡Qué bonito es Docker!.

Archivos necesarios

Se presenta una breve explicación de la función de cada archivo necesario. Recuerda que todos éstos los puedes encontrar en el repositorio del proyecto.

  • Dockerfile: Archivo que contiene las instrucciones necesarias para la configuración de nuestro ambiente de trabajo y para realizar el procedimiento de limpieza de datos.
  • data_processing.yml: Entorno de Anaconda. Recordemos que necesitamos ejecutar un script de Python y utilizar Pandas. Me di a la tarea de instalar todas las dependencias necesarias y desde el Dockerfile instalaremos Anaconda y exportaremos este archivo a un nuevo entorno.
  • fix_data.py: Script de Python para ejecutar la segunda etapa del procedimiento y obtener un resultado final.
  • 201128_Catalogos.xlsx: Este archivo contiene las claves y valores reales utilizados en la base de datos que vamos a procesar. Es necesario para intercambiar el valor de las claves por sus valores reales.

Los archivos más relevantes son Dockerfile y fix_data.py. A continuación les echaremos un vistazo.

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Tomamos como base la imagen de ubuntu y además instalamos miniconda
FROM ubuntu
FROM continuumio/miniconda3

# Autor
LABEL Pedro Hernandez <pedro.a.hdez.a@gmail.com>

# Creamos un directorio y lo convertimos en nuestro directorio de trabajo
RUN mkdir data_cleaning
WORKDIR /data_cleaning

# Actualizamos todos los paquetes e instalamos los que faltan
RUN apt -y update
RUN apt install -y curl unzip csvkit

# Descargamos los datos
RUN curl -O http://datosabiertos.salud.gob.mx/gobmx/salud/datos_abiertos/datos_abiertos_covid19.zip

# Descomprimimos el archivo y eliminamos el zip
RUN unzip datos_abiertos_covid19.zip && rm datos_abiertos_covid19.zip

# Obtenemos la suma de los negativos y confirmados de covid para cada municipio de Sonora y
# guardamos el resultado en el archivo "numero_positivos_y_negativos_municipios_sonora.csv"
RUN csvcut -c ENTIDAD_RES,MUNICIPIO_RES,CLASIFICACION_FINAL 210422COVID19MEXICO.csv | \
    csvgrep -c ENTIDAD_RES -m "26" | csvcut -c MUNICIPIO_RES,CLASIFICACION_FINAL | \
    csvgrep -c CLASIFICACION_FINAL -r "[37]" | csvsort --no-inference -c 1,2 | uniq -c | \
    tail -n+2 | sed -e 's/\s\+/,/g' | cut -c 2- > numero_positivos_y_negativos_municipios_sonora.csv

RUN sed -i '1s/^/TOTAL,MUNICIPIO_RES,CLASIFICACION_FINAL\n/' numero_positivos_y_negativos_municipios_sonora.csv

CMD ["bash"]

# El archivo anterior tendrá como valores en sus renglones el total, las claves de los municipios
# y la clave de la clasificación. Para mapear estas claves a sus respectivos valores haremos
# uso de un script de python.

# Copiamos el entorno de anaconda, el catálogo de los datos y el script para arreglar los datos
COPY data_processing.yml .
COPY 201128_Catalogos.xlsx .
COPY fix_data.py .

# Creamos el entorno de anaconda y configuramos bash para correr anaconda
RUN conda env create -f data_processing.yml
SHELL ["conda", "run", "-n", "data_processing", "/bin/bash", "-c"]
SHELL ["conda", "run", "--no-capture-output", "-n", "data_processing", "python", "fix_data.py"]

# Corremos el script para arreglar los datos
RUN python fix_data.py

La parte fundamental de este Dockerfile es el tratamiento de los datos (líneas 24-29), explicaré cada pipe que se utilizó:

La base de datos contiene 40 columnas. Nosotros únicamente necesitamos las columnas de ENTIDAD_RES para identificar a Sonora, MUNICIPIO_RES para identificar cada municipio, CLASIFICACION_FINAL para identificar a los casos positivos y negativos a Covid-19. Entonces, en nuestra primer pipe filtramos el archivo para quedarnos únicamente con las columnas que nos interesan.

1
$ csvcut -c ENTIDAD_RES,MUNICIPIO_RES,CLASIFICACION_FINAL 210422COVID19MEXICO.csv

Con este pipe elegimos únicamente los renglones que tengan como valor “26” (la clave de Sonora) en su columna ENTIDAD_RES. Para entender esta clave y las demás es necesario consultar el archivo 201128_Catalogos.xlsx

1
$ csvgrep -c ENTIDAD_RES -m "26"

Aquí deshechamos la columna ENTIDAD_RES, ésta ya no nos interesa porque con el pipe anterior nos aseguramos de que estamos trabajando únicamente con registros de Sonora.

1
$ csvcut -c MUNICIPIO_RES,CLASIFICACION_FINAL

Aplicamos la expresión regular “[37]” a la columna CLASIFICACION_FINAL; es decir, nos quedamos únicamente con los renglones que tengan un “3” o un “7” como valor en la columna mencionada. Esto significa que nos quedamos úncamente con los datos positivos (3) y negativos (7) a Covid-19.

1
$ csvgrep -c CLASIFICACION_FINAL -r "[37]"

Aquí existen dos pipes que van de la mano. Con el primero ordenamos los datos, primero por su columna 1 (MUNICIPIO_RES) y después por la columna 2 (CLASIFICACION_FINAL) y con el segundo contamos las líneas únicas de la base de datos. Haciendo ésto obtenemos el total de positivos y el total de negativos a Covid-19 en cada uno de los municipios de Sonora.

1
$ csvsort --no-inference -c 1,2 | uniq -c

La pipe anterior cuenta cuántas veces se repite una línea en todo el archivo, por lo tanto, también cuenta el encabezado del csv (el renglón que contiene el nombre de las columnas) y le agrega la cantidad de veces que se repite, por lo tanto, este encabezado ya no nos sirve más. Con esta instrucción tomamos todas las líneas del archivo excepto la primera.

1
$ tail -n+2

El comando uniq tiene su propio formato de salida, por lo general utiliza espacios en blanco como separadores y también los agrega al inicio de cada línea. Con esta instrucción sustituímos esos espacios en blanco por una coma.

1
$ sed -e 's/\s\+/,/g'

Debido a que el comando anterior también incluyó comas al inicio de cada renglón del archivo, con este pipe borramos todo hasta llegar al segundo caracter de cada línea, es decir, nos deshacemos de las comas existentes al principio de cada renglón. Finalmente escribimos este resultado en un archivo.

1
$ cut -c 2- > numero_positivos_y_negativos_municipios_sonora.csv

En este punto ya tenemos un archivo con la sumatoria de casos positivos y negativos a Covid-19 para cada municipio del estado de Sonora. Pero todavía nos falta un pequeño detalle. Recordemos que al usar la instrucción uniq -c destruimos el encabezado de nuestro archivo, entonces como última acción necesitamos volver a añadir dicho encabezado, para eso utilizamos la siguiente instrucción:

1
$ sed -i '1s/^/TOTAL,MUNICIPIO_RES,CLASIFICACION_FINAL\n/' numero_positivos_y_negativos_municipios_sonora.csv

¡Listo! Ahora ya tenemos el resultado parcial.

fix_data.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pandas as pd

# Leyendo la hoja de municipios de los catálogos
df_municipios = pd.read_excel(io="201128_Catalogos.xlsx",
                          sheet_name="Catálogo MUNICIPIOS")

# Leyendo la hoja de clasificación final de los catálogos
df_clasificacion = pd.read_excel(io="201128_Catalogos.xlsx",
                          sheet_name="Catálogo CLASIFICACION_FINAL", skiprows=2)

# Leyendo el csv que nosotros obtuvimos
df_datos = pd.read_csv("numero_positivos_y_negativos_municipios_sonora.csv", index_col=None)

# Obtenemos los municipios de sonora (con CLAVE_ENTIDAD == 26) y eliminamos dicha columna
df_municipios = df_municipios.loc[df_municipios['CLAVE_ENTIDAD'] == 26].drop(['CLAVE_ENTIDAD'], axis=1)

# Reemplazamos los datos en nuestro csv de acuerdo al nombre del municipio asignado a cada clave.
# Esta información la obtenemos del catálogo de municipios
df_datos['MUNICIPIO_RES'] = df_datos['MUNICIPIO_RES'].map(df_municipios.set_index('CLAVE_MUNICIPIO')['MUNICIPIO'])

# Reemplazamos la clasificación final en nuestros datos de acuerdo a la clave asignada. Esta
# información la obtenemos del catálogo de clasificaciones finales
df_datos['CLASIFICACION_FINAL'] = df_datos['CLASIFICACION_FINAL'].map(df_clasificacion.set_index('CLAVE')['CLASIFICACIÓN'])

# Guardamos la información en un csv
df_datos.to_csv("positivos_y_negativos_municipios_sonora.csv", index=False)

Este pequeño script es muy sencillo, en parte por su tamaño y también gracias a la simplicidad de la sintaxis de Python.

Si echamos un vistazo a nuestro resultado parcial, nos daremos cuenta que contiene claves en vez de valores que representen algo para nosotros:

1
2
3
4
5
6
7
8
9
10
11
$ cat numero_positivos_y_negativos_municipios_sonora.csv | head -n 10
TOTAL,MUNICIPIO_RES,CLASIFICACION_FINAL
17,001,3
14,001,7
1021,002,3
851,002,7
178,003,3
143,003,7
76,004,3
82,004,7
21,005,3

Sin embargo, al correr el script, sustituimos las claves haciendo uso del archivo 201128_Catalogos.xlsx y guardamos nuestro resultado final en el archivo positivos_y_negativos_municipios_sonora.csv. Ahora, nuestros datos tienen un signifcado:

1
2
3
4
5
6
7
8
9
10
11
$ cat positivos_y_negativos_municipios_sonora.csv | head -n 10
TOTAL,MUNICIPIO_RES,CLASIFICACION_FINAL
17,ACONCHI,CASO DE SARS-COV-2  CONFIRMADO
14,ACONCHI,NEGATIVO A SARS-COV-2
1021,AGUA PRIETA,CASO DE SARS-COV-2  CONFIRMADO
851,AGUA PRIETA,NEGATIVO A SARS-COV-2
178,ALAMOS,CASO DE SARS-COV-2  CONFIRMADO
143,ALAMOS,NEGATIVO A SARS-COV-2
76,ALTAR,CASO DE SARS-COV-2  CONFIRMADO
82,ALTAR,NEGATIVO A SARS-COV-2
21,ARIVECHI,CASO DE SARS-COV-2  CONFIRMADO

De esta forma hemos realizado una pequeña limpieza a una base de datos y al mismo tiempo preparamos un ambiente de trabajo reproducible.

comments powered by Disqus