Flujos de trabajo y automatización
Overview
Teaching: 40 min
Exercises: 50 minQuestions
¿Puedo automatizar operaciones en Python?
¿Qué son las función y por qué debería usarlas?
Objectives
Describir por qué se usan los bucles en Python.
Usar bucles for para automatizar el análisis de datos.
Escribir nombres de archivo únicos en Python.
Construir código reusable en Python.
Escribir funciones usando condicionales (if, then, else).
Hasta este momento, hemos usado Python y la librería Pandas para explorar y manipular datasets a mano, tal y como lo haríamos en una planilla de cálculo. Sin embargo la belleza de usar un lenguaje de programación como Python viene de la posibilidad de automatizar el procesamiento de los datos a través del uso de bucles y funciones.
Bucles for
Los bucles nos permiten repetir un flujo de trabajo (o una serie de acciones) cierto número dado de veces o mientras una condición es cierta. Podríamos usar un bucle para procesar automáticamente la información que está contenida en múltiples archivos (valores diarios con un archivo por año, por ejemplo). Los bucles nos alivian nuestra tarea al hacer tareas repetitivas sin que tengamos que involucrarnos directamente, y hace menos probable que introduzcamos errores al equivocarnos mientras procesamos cada archivo manualmente.
Escribamos un bucle for sencillo que simule lo que un niño podría ver durante una visita al zoológico:
animals = ['lion', 'tiger', 'crocodile', 'vulture', 'hippo']
print(animals)
['lion', 'tiger', 'crocodile', 'vulture', 'hippo']
for creature in animals:
print(creature)
lion
tiger
crocodile
vulture
hippo
La línea que define el bucle debe comenzar con un for y terminar con el caracter dos puntos, y el cuerpo del bucle debe estar indentado.
Eh este ejemplo, creature
es la variable del bucle que toma el valor de la
siguiente entrada en animals
cada vez que el bucle hace una iteración. Podemos
darle a la variable del bucle el nombre que querramos. Después que se termina el bucle,
la variable del bucle continuará existiendo y tendrá el valor de la última entrada en
la colección.
animals = ['lion', 'tiger', 'crocodile', 'vulture', 'hippo']
for creature in animals:
pass
print('The loop variable is now: ' + creature)
The loop variable is now: hippo
Acá no le estamos pidiendo a Python que imprima el valor de la variable del
bucle, pero el bucle todavía corre y el valor de creature
cambia en cada
iteración. La palabra clave pass
en el cuerpo del bucle significa solamente
“no hagas nada”.
Desafío - Bucles
¿Qué pasa si no incluimos la palabra clave
pass
?Reescribe el bucle de tal forma que los animales estén separados por comas y no por una línea nueva. (Pista: Puedes concatenar cadenas de caracteres usando el signo más. Por ejemplo,
print(string1 + string2)
resulta en ‘string1string2’).
Automatizando el procesamiento de datos usando bucles For
El archivo que hemos estado usando hasta este momento, surveys.csv
, contiene
25 años de información y es muy grande. Nos encantaría separar esta información
por años y guardar un archivo por cada año.
Comencemos por crear un nuevo directorio en nuestra carpeta data
para guardar
todos estos archivos usando el módulo os
import os
os.mkdir('data/yearly_files')
El comando os.mkdir
es equivalente a escribir mkdir
en la terminal. Solo
para estar seguros, podemos verificar que nuestro nuevo directorio fue creado en
la carpeta data
:
os.listdir('data')
['plots.csv',
'portal_mammals.sqlite',
'species.csv',
'survey2001.csv',
'survey2002.csv',
'surveys.csv',
'surveys2002_temp.csv',
'yearly_files']
El comando os.listdir
es equivalente a usar ls
en la terminal.
En episodios anteriores, vimos cómo usar la librería Pandas para cargar en memoria, a través de DataFrame, información sobre las especies; vimos cómo seleccionar un subconjunto de esos datos usando ciertos criterios, y vimos también cómo escribir esa información en un archivo CSV. Escribamos un script que realiza esos tres pasos en secuencia para el año 2002:
import pandas as pd
# Cargamos los datos en un DataFrame
surveys_df = pd.read_csv('data/surveys.csv')
# Seleccionamos solo los datos del año 2002
surveys2002 = surveys_df[surveys_df.year == 2002]
# Escribimos el nuevo DataFrame en un archivo CSV
surveys2002.to_csv('data/yearly_files/surveys2002.csv')
Para crear los archivos con los datos anuales, podemos repetir estos últimos dos comandos una y otra vez, una vez por cada año de información. Sin embargo, repetir código no es ni elegante ni práctico, y hace muy probable que introduzcamos errores en nuestro código. Queremos convertir lo que acabamos de escribir en un bucle que repita estos últimos dos comandos para cada año en nuestro dataset.
Comencemos con un bucle que solamente imprima los nombres de los archivos que queremos crear - El dataset que estamos usando va desde 1977 hasta 2002, y vamos a crear un archivo por separado para cada año. Listar los nombres de los archivos es una buena estrategia, porque así podemos confirmar que nuestro bucle se está comportando como esperamos.
Hemos visto que podemos iterar sobre una lista de elementos, entonces necesitamos una lista de años sobre la cual iterar. Podemos obtener los años en nuestro DataFrame con:
surveys_df['year']
0 1977
1 1977
2 1977
3 1977
...
35545 2002
35546 2002
35547 2002
35548 2002
pero queremos solamente años únicos, y esto lo podemos obtener usando el método
unique
que ya hemos visto.
surveys_df['year'].unique()
array([1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987,
1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998,
1999, 2000, 2001, 2002], dtype=int64)
Escribiendo esto en un bucle for obtenemos
for year in surveys_df['year'].unique():
filename='data/yearly_files/surveys' + str(year) + '.csv'
print(filename)
data/yearly_files/surveys1977.csv
data/yearly_files/surveys1978.csv
data/yearly_files/surveys1979.csv
data/yearly_files/surveys1980.csv
data/yearly_files/surveys1981.csv
data/yearly_files/surveys1982.csv
data/yearly_files/surveys1983.csv
data/yearly_files/surveys1984.csv
data/yearly_files/surveys1985.csv
data/yearly_files/surveys1986.csv
data/yearly_files/surveys1987.csv
data/yearly_files/surveys1988.csv
data/yearly_files/surveys1989.csv
data/yearly_files/surveys1990.csv
data/yearly_files/surveys1991.csv
data/yearly_files/surveys1992.csv
data/yearly_files/surveys1993.csv
data/yearly_files/surveys1994.csv
data/yearly_files/surveys1995.csv
data/yearly_files/surveys1996.csv
data/yearly_files/surveys1997.csv
data/yearly_files/surveys1998.csv
data/yearly_files/surveys1999.csv
data/yearly_files/surveys2000.csv
data/yearly_files/surveys2001.csv
data/yearly_files/surveys2002.csv
Ahora podemos agregar el resto de pasos que necesitamos para crear archivos separados:
# Load the data into a DataFrame
surveys_df = pd.read_csv('data/surveys.csv')
for year in surveys_df['year'].unique():
# Select data for the year
surveys_year = surveys_df[surveys_df.year == year]
# Write the new DataFrame to a CSV file
filename = 'data/yearly_files/surveys' + str(year) + '.csv'
surveys_year.to_csv(filename)
Mira dentro del directorio yearly_files
y verifica un par de los archivos que
acabaste de crear para confirmar que todo funcionó como esperabas.
Escribiendo nombres de archivo únicos
Nota que en el código anterior creamos un nombre de archivo único para cada año.
filename = 'data/yearly_files/surveys' + str(year) + '.csv'
Descompongamos las partes de este nombre:
- La primera parte es simplemente un texto que especifica el directorio en el que
vamos a guardar nuestro archivo (data/yearly_files/) y la primera parte del
nombre del archivo (surveys):
'data/yearly_files/surveys'
- Podemos concatenar esto con el valor de una variable, en este caso
year
, al usar el signo más y la variable que le queremos añadir al nombre del archivo:+ str(year)
- Por último añadimos la extensión del archivo usando otra cadena de caracteres:
+ '.csv'
Nota que usamos comillas sencillas para añadir las cadenas de caracteres y que la
variable no está entre comillas. Este código produce la cadena de caracteres
data/yearly_files/surveys2002.csv
que contiene tanto el path como el
nombre del archivo.
Desafío - Modificando bucles
Algunas de las encuestas que guardaste tienen datos faltantes (tienen valores nulos que salen como
NaN
- No es un número (en inglés) - en los DataFrames y no salen en los archivos de texto). Modifica el bucle for para que las entradas que tengan valores nulos no sean incluidas en los archivos anuales.Supongamos que solo quieres revisar los datos cada cierto múltiplo de años. ¿Cómo modificarías el bucle para generar un archivo de datos cada cinco años comenzando desde 1977?
En vez de separar la información por años, un colega tuyo quiere hacer el análisis separando por especies. ¿Cómo escribirías un único archivo CSV por cada especie?
Construyendo código modular y reusable usando funciones
Supón que separar archivos enormes en archivos anuales individuales es una tarea que tenemos que realizar frecuentemente. Podríamos escribir un bucle for como el que hicimos arriba cada vez que lo necesitemos, pero esto tomaría mucho tiempo y podría introducir errores en el código. Una solución más elegante sería crear una herramienta reusable que realice esta tarea con el mínimo esfuerzo del usuario. Para hacerlo, vamos a convertir el código que ya escribimos en una función.
Las funciones son piezas de código reusable y autónomo que pueden ser llamadas mediante un solo comando. Están diseñadas para aceptar argumentos como entrada y retornar valores, pero no necesitan hacerlo necesariamente. Las variables declaradas adentro de las funciones solo existen mientras la función se está ejecutando, y si una variable adentro de una función (una variable local) tiene el mismo nombre de otra variable en alguna parte del código, la variable local no sobrescribe a la otra.
Todo método usado en Python (como por ejemplo print
) es una función, y las
librerías que importamos (pandas
, por ejemplo) son una colección de funciones.
Solo usaremos las funciones que están contenidas en el mismo código que las
usan, pero es sencillo también escribir funciones que puedan ser usadas por
programas diferentes.
Las funciones se declaran usando la siguiente estructura general:
def this_is_the_function_name(input_argument1, input_argument2):
# El cuerpo de la función está indentado
# Esta función imprime los dos argumentos en pantalla
print('The function arguments are:', input_argument1, input_argument2, '(this is done inside the function!)')
# And returns their product
return input_argument1 * input_argument2
La declaración de la función comienza con la palabra clave def
, seguida del
nombre de la función y los argumentos entre paréntesis, y termina con un dos
puntos. El cuerpo de la función está indentado justo como ocurría con los
bucles. Si la función retorna algo al ser llamada, entonces incluimos la palabra
clave return
al final.
Así es como llamamos a la función:
product_of_inputs = this_is_the_function_name(2,5)
The function arguments are: 2 5 (this is done inside the function!)
print('Their product is:', product_of_inputs, '(this is done outside the function!)')
Their product is: 10 (this is done outside the function!)
Desafío - Funciones
- Cambia los valores de los argumentos en la función y mira su salida.
- Intenta llamar a la función usando la cantidad equivocada de argumentos (es decir, diferente de 2) o sin asignar la llamada de la función a una variable (sin poner
product_of_inputs =
).- Declara una variable dentro de una función y prueba a encontrar en dónde existe (Pista: ¿puedes imprimirla desde fuera de la función?)
- Explora qué sucede cuando una variable tiene el mismo nombre adentro y afuera de la función. ¿Qué le ocurre a la variable global cuando cambias el valor de la variable local?
Ahora podemos convertir el código para guardar archivos con datos anuales en una función. Hay muchas “partes” de este código que podemos convertir en funciones, y podríamos inclusive crear funciones que llaman a otras funciones adentro de ellas. Comencemos escribiendo una función que separa los datos para un año y los guarda en un archivo:
def one_year_csv_writer(this_year, all_data):
"""
Escribe un archivo csv con los datos para un año dado.
this_year --- el año del que vamos a extraer los datos
all_data --- DataFrame con datos de múltiples años
"""
# Seleccionamos los datos para el año
surveys_year = all_data[all_data.year == this_year]
# Escribimos el nuevo DataFrame a un archivo csv
filename = 'data/yearly_files/function_surveys' + str(this_year) + '.csv'
surveys_year.to_csv(filename)
El texto que está entre los dos grupos de tres comillas dobles se llama el docstring y contiene la documentación de la función. No hace nada al ejecutar la función y por tanto no es necesario, pero es una excelente práctica incluir docstrings para recordar y explicar qué hace el código. Los docstrings en las funciones también se vuelven parte de la documentación “oficial”:
one_year_csv_writer?
one_year_csv_writer(2002, surveys_df)
Cambiamos el nombre del archivo CSV para diferenciarlo del que escribimos
anteriormente. Busca en el directorio yearly_files
el archivo que creamos.
¿Hizo la función lo que esperabas que hiciera?
Sin embargo, lo que nos encantaría hacer es crear archivos para múltiples años
sin tener que pedirlos uno a uno. Escribamos otra función que reemplace el bucle
for simplemente iterando a través de la secuencia de años y llamando repetidamente a la
función que acabamos de escribir, one_year_csv_writer
:
def yearly_data_csv_writer(start_year, end_year, all_data):
"""
Escribe archivos CSV separados para cada año de datos.
start_year --- el primer año de datos que queremos
end_year --- el último año de datos que queremos
all_data --- DataFrame con datos de varios años
"""
# "end_year" es el último año que queremos extraer, entonces iteramos hasta end_year+1
for year in range(start_year, end_year+1):
one_year_csv_writer(year, all_data)
Como la gente esperará naturalmente que el año final (end_year
) sea el último,
el bucle for adentro de la función termina en end_year + 1
. Al escribir el
bucle entero en la función, hemos hecho una herramienta reusable para cuando
necesitemos partir un archivo de datos grande en archivos anuales. Como podemos
especificar el primer y el último año para los cuales queremos crear archivos,
podemos inclusive usar esta función para crear archivos para un subconjunto de
los años disponibles. Así llamaríamos la función:
# Cargamos los datos en un DataFrame
surveys_df = pd.read_csv('data/surveys.csv')
# Creamos los archivos CSV
yearly_data_csv_writer(1977, 2002, surveys_df)
¡TEN CUIDADO! Si estás usando Jupyter Notebooks y estás modicando la función, DEBES volver a ejecutar la celda para que la función cambiada esté disponible para el resto del código. Nada cambiará visualmente cuando hagas esto, porque definir una función sin ejecutarla no produce ninguna salida. Toda otra celda que use la función (ahora cambiada) también tendrá que ser re-ejecutada para cambiar su salida.
Challenge- Más funciones
- Añade dos argumentos a las funciones que escribimos que tomen el path del directorio donde los archivos serán escritos y el root del nombre del archivo. Crea un nuevo conjunto de archivos con un nombre diferente en un directorio diferente.
- ¿Cómo podrías usar la función
yearly_data_csv_writer
para crear un archivo CSV para solo un año? (Pista: piensa sobre la sintaxis pararange
)- Haz que las funciones retornen una lista de los archivos que escribieron. Hay muchas formas en las que puedes hacer esto (¡y deberías intentarlas todas!): cualquiera de las dos funciones podría imprimir algo en pantalla, cualquiera podría usar
return
para retornar números o cadenas de caracteres cada vez que se llaman, o podrías hacer una combinación de estas dos estrategias. Podrías también intentar usar la libreríaos
para listar los contenidos de directorios.- Explora qué sucede cuando las variables son declaradas dentro de cada una de las funciones versus en el cuerpo principal de tu código (lo que está sin indentar). ¿Cuál es el alcance de las variables (es decir, dónde son visibles)?, ¿qué ocurre si tienen el mismo nombre pero valores diferentes?
Las funciones que escribimos exigen que les demos un valor para cada argumento.
Idealmente, nos gustaría que estas funciones fuesen tan flexibles e
independientes como fuera posible. Modifiquemos la función
yearly_data_csv_writer
para que start_year
y end_year
sean por defecto el
rango completo de los datos si no son dados por el usuario. Se le pueden asignar
a los argumentos valores por defecto usando el signo igual a la hora de declarar
la función. Todos los argumentos en la función que no tengan un valor por
defecto (como aquí all_data
) serán argumentos requeridos y DEBERÁN ir antes de
los argumentos que tengan valores por defecto (y que son opcionales al llamar la
función).
def yearly_data_arg_test(all_data, start_year = 1977, end_year = 2002):
"""
Modificación de yearly_data_csv_writer para probar argumentos con valores
por defecto!
start_year --- el primer año de datos que queremos --- por defecto: 1977
end_year --- el último año de datos que queremos --- por defecto: 2002
all_data --- DataFrame con datos de varios años
"""
return start_year, end_year
start,end = yearly_data_arg_test (surveys_df, 1988, 1993)
print('Both optional arguments:\t', start, end)
start,end = yearly_data_arg_test (surveys_df)
print('Default values:\t\t\t', start, end)
Both optional arguments: 1988 1993
Default values: 1977 2002
Los “\t” en print
son tabulaciones, y son usadas para alinear el texto y
facilitar la lectura.
Pero ¿qué sucede si nuestro dataset no comienza en 1977 ni termina en 2002? Podemos modificar la función de tal forma que ella misma mire cuál es el primer y cuál es el último año si estos argumentos no son provistos por el usuario:
def yearly_data_arg_test(all_data, start_year = None, end_year = None):
"""
Modificación de yearly_data_csv_writer para probar argumentos con valores
por defecto!
start_year --- el primer año de datos que queremos --- por defecto: None - revisar all_data
end_year --- el último año de datos que queremos --- por defecto: None - revisar all_data
all_data --- DataFrame con datos de varios años
"""
if start_year is None:
start_year = min(all_data.year)
if end_year is None:
end_year = max(all_data.year)
return start_year, end_year
start,end = yearly_data_arg_test (surveys_df, 1988, 1993)
print('Both optional arguments:\t', start, end)
start,end = yearly_data_arg_test (surveys_df)
print('Default values:\t\t\t', start, end)
Both optional arguments: 1988 1993
Default values: 1977 2002
Ahora los valores por defecto de los argumentos start_year
y end_year
en la
función yearly_data_arg_test
son None
. Esta es una constante incorporada en
Python que indica la ausencia de un valor - esencialmente indica que la variable
existe en el directorio de nombres de variables (el namespace) de la función pero que
no corresponde a ningún objeto existente.
Challenge - Variables
¿Qué tipo de objeto corresponde a una variable declarada como
None
? (Pista: crea una variable con el valorNone
y usa la funcióntype()
)Compara el comportamiento de la función
yearly_data_arg_test
cuando los argumentos tienenNone
como valor por defecto y cuando no tienen valores por defecto.¿Qué ocurre si solo incluimos un valor para
start_year
al llamar a la función?, ¿puedes escribir una llamada a la función con solo un valor paraend_year
? (Pista: piensa en cómo la función debe estar asignándole valores a cada uno de sus argumentos - ¡esto está relacionado con la necesidad de poner los argumentos que no tienen valores por defecto antes de los que sí tienen valores por defecto en la definición de la función!)
Sentencias if
El cuerpo de la función anterior ahora tiene dos condicionales if que
revisan los valores de start_year
y end_year
. Los condicionales if ejecutan
un segmento de código si una condición dada es cierta. Usualmente lucen así:
a = 5
if a<0: # ¿es cierta esta primera condición?
# si a ES menor que cero
print('a is a negative number')
elif a>0: # La primera condición no es cierta, ¿la segunda?
# si a NO ES menor que cero y ES mayor que cero
print('a is a positive number')
else: # No se cumplieron las dos condiciones
# si a NO ES menor que cero y NO ES mayor que cero
print('a must be zero!')
lo cual retornaría:
a is a positive number
Cambia los valores de a
para ver cómo funciona este código. La palabra clave
elif
significa “sino” (en inglés, “else if”), y todos los condicionales deben
terminar con un dos puntos.
Los condicionales if en la función yearly_data_arg_test
verifican si hay algún
objeto asociado a los nombres start_year
y end_year
. Si estas variables son
None
, los condicionales if retornan el booleano True
y ejecutan cualquier
cosa que esté en su cuerpo. Por otra parte, si los nombres están asociados a
algún valor (es decir, recibieron un número al ser llamada la función), los
condicionales if retornarán False
y no ejecutan su cuerpo. El condicional
opuesto, que retornaría True
si las variables estuvieran asociadas con
objetos (es decir, si hubieran recibido valores al llamarse la función), sería
if start_year
y if end_year
.
Tal y como la hemos escrito hasta este momento, la función
yearly_data_arg_test
asocia los valores que le pasamos cuando la llamamos con
los argumentos en la definición de la función solo basados en su orden. Si la
función recibe solo dos valores al ser llamada, el primero será asociado con
all_data
y el segundo con start_year
, sin importar cuál era nuestra
intención. Podemos solucionar este problema al llamar la función usando
argumentos keyword, en donde cada uno de los argumentos en la definición de
la función está asociado con una keyword y al llamar la función pasamos
valores a los parámetros usando estas keywords:
start,end = yearly_data_arg_test (surveys_df)
print('Default values:\t\t\t', start, end)
start,end = yearly_data_arg_test (surveys_df, 1988, 1993)
print('No keywords:\t\t\t', start, end)
start,end = yearly_data_arg_test (surveys_df, start_year = 1988, end_year = 1993)
print('Both keywords, in order:\t', start, end)
start,end = yearly_data_arg_test (surveys_df, end_year = 1993, start_year = 1988)
print('Both keywords, flipped:\t\t', start, end)
start,end = yearly_data_arg_test (surveys_df, start_year = 1988)
print('One keyword, default end:\t', start, end)
start,end = yearly_data_arg_test (surveys_df, end_year = 1993)
print('One keyword, default start:\t', start, end)
Default values: 1977 2002
No keywords: 1988 1993
Both keywords, in order: 1988 1993
Both keywords, flipped: 1988 1993
One keyword, default end: 1988 2002
One keyword, default start: 1977 1993
Desafío - Modificando funciones
Reescribe las funciones
one_year_csv_writer
yyearly_data_csv_writer
para que tengan argumentos keyword con valores por defecto.Modifica las funciones de tal forma que no creen archivos para un año si éste no está en los datos y que muestre una alerta al usuario (Pista: usa condicionales para esto. Si quieres un reto más, ¡usa
try
!)Este código verifica si un directorio existe, sino lo crea. Añade un poco de código a la función que escribe los archivos CSV para verificar si existe el directorio al que piensas escribir.
if 'dir_name_here' in os.listdir('.'): print('Processed directory exists') else: os.mkdir('dir_name_here') print('Processed directory created')
- El código que has escrito hasta este momento usando el bucle for está bastante bien, pero no necesariamente es reproducible con datasets diferentes. Por ejemplo, ¿qué pasa con el código si tenemos datos para más años? Usando las herramientas que aprendiste en las actividades anteriores, crea una lista de todos los años representados en los datos. Después crea un bucle para procesar tu información, comenzando desde el primer año y terminando en el último usando la lista. (Pista: puedes crear un bucle con la lista así:
for years in year_list:
)
Key Points
Los bucles nos permiten repetir una serie de acciones un número dado de veces o mientras una condición es cierta.
Podemos automatizar tareas que se deben repetir un número predefinido de veces utilizando bucles
for
.Una tarea de automatización típica en programación es generar secuencias de archivos con nombres distinos que siguen un patrón, esto se puede realizar fácilmente manipulando cadenas de caracteres con el nombre de los archivos dentro de bucles de repetición a medida que se van creando.
Es conveniente definir funciones para establecer bloques de código reutilizables. Las funciones se pueden diseñar para que acepten argumentos de entradas para generalizar su funcionalidad y devolver distintos tipos de resultados.
Las sentencias
if
permiten elegir cuales bloques de código ejecutar según según se cumplan o no distintas condiciones.