name_list = ["Lovelace", "Smith", "Pigou", "Babbage"]
for name in name_list:
print(name)Lovelace
Smith
Pigou
Babbage
En Funciones hablamos de lo importante que es reducir la duplicación en tu código creando funciones en lugar de copiar y pegar. Reducir la duplicación de código tiene tres beneficios principales:
Es más fácil ver la intención de tu código, porque tu atención se dirige a lo que cambia, no a lo que permanece igual.
Es más fácil responder a cambios en los requisitos. A medida que cambian tus necesidades, solo tienes que hacer cambios en un lugar, en vez de recordar cambiar cada lugar donde copiaste y pegaste el código.
Probablemente tendrás menos errores, porque cada línea de código se usa en más lugares.
Una herramienta para reducir la duplicación son las funciones, que lo logran identificando patrones de código repetidos y extrayéndolos en piezas independientes que se pueden reutilizar y actualizar fácilmente. Otra herramienta para reducir la duplicación es la iteración, que te ayuda cuando necesitas hacer lo mismo con múltiples entradas: repetir la misma operación en distintas columnas o en distintos datasets.
En este capítulo aprenderás sobre la iteración de tres maneras: iteración explícita, usando bucles for y bucles while; iteración mediante comprehensions (por ejemplo, list comprehensions); e iteración para dataframes de pandas.
Este capítulo usará el paquete de análisis de datos pandas.
Un bucle es una forma de ejecutar un fragmento de código similar una y otra vez de manera parecida.
Un bucle for hace algo mientras se cumple la condición. Por ejemplo,
name_list = ["Lovelace", "Smith", "Pigou", "Babbage"]
for name in name_list:
print(name)Lovelace
Smith
Pigou
Babbage
imprime un nombre hasta que se han impreso todos los nombres.
Todo bucle for tiene tres componentes:
La salida, aquí una instrucción print. Pero puedes imaginar un bucle for que rellene cada entrada de un dataframe o de una lista (aunque siempre deberías crear primero el objeto de Python completo y rellenarlo después, en lugar de cambiar su tamaño dentro del bucle, porque esto último es lento).
La secuencia: for name in name_list:. Esto determina sobre qué se itera: cada ejecución del bucle for asignará a name un valor distinto del iterable name_list. No tiene que ser una lista; sirve cualquier objeto iterable. Es útil pensar en name como un pronombre, como “ello”.
El cuerpo: print(name). Este es el código que hace el trabajo. Se ejecuta repetidamente, cada vez con un valor distinto de name. La primera iteración ejecutará en la práctica print(name_list[0]), la segunda ejecutará print(name_list[1]), y así sucesivamente.
Siempre que tu objeto sea un iterable (es decir, que puedas iterar sobre él), se puede usar de esta forma en un bucle for. Los ejemplos más comunes son las listas y las tuplas, pero también puedes iterar sobre strings (en cuyo caso se selecciona cada carácter por turno). Un error común que debes tener en cuenta es iterar sobre un string, por ejemplo “hello”, en lugar de iterar sobre una lista (o tupla) de strings, por ejemplo ["hello"]. En este último caso, obtienes:
for entry in ["hello"]:
print(entry)
print("---end entry---")hello
---end entry---
Mientras que en el primero obtienes algo bastante distinto y, por lo general, no muy útil:
for entry in "hello":
print(entry)
print("---end entry---")h
---end entry---
e
---end entry---
l
---end entry---
l
---end entry---
o
---end entry---
Escribe un bucle for que imprima “Python for Data Science” de modo que cada palabra se imprima en una iteración sucesiva.
Un truco útil con los bucles for es la palabra clave enumerate(), que recorre un índice que lleva la cuenta de la posición de los elementos en una lista:
name_list = ["Lovelace", "Smith", "Hopper", "Babbage"]
for i, name in enumerate(name_list):
print(f"The name in position {i} is {name}")The name in position 0 is Lovelace
The name in position 1 is Smith
The name in position 2 is Hopper
The name in position 3 is Babbage
Recuerda que Python indexa desde 0, así que la primera entrada de i será cero. Pero, si quieres indexar desde otro número, puedes hacerlo:
for i, name in enumerate(name_list, start=1):
print(f"The name in position {i} is {name}")The name in position 1 is Lovelace
The name in position 2 is Smith
The name in position 3 is Hopper
The name in position 4 is Babbage
Otro patrón útil al hacer bucles for con diccionarios es iterar sobre pares clave-valor. Aprenderemos más sobre los diccionarios muy pronto, pero por ahora lo importante es que asocian una clave a un valor; por ejemplo, “apple” podría asociarse a “fruit”. Tomemos nuestro ejemplo anterior que asociaba ciudades a temperaturas. Si quisiéramos iterar sobre ambos, claves y valores, podemos escribir un bucle for así:
cities_to_temps = {"Paris": 28, "London": 22, "Seville": 36, "Wellesley": 29}
for key, value in cities_to_temps.items():
print(f"In {key}, the temperature is {value} degrees C today.")In Paris, the temperature is 28 degrees C today.
In London, the temperature is 22 degrees C today.
In Seville, the temperature is 36 degrees C today.
In Wellesley, the temperature is 29 degrees C today.
Observa que añadimos .items() al final del diccionario. Y observa que no teníamos que llamar a la clave key ni al valor value: estos se definen por su posición. Pero parte de las buenas prácticas al escribir código es que no haya sorpresas, y escribir key, value deja muy claro que estás usando valores de un diccionario.
Escribe un diccionario que asocie cuatro ciudades que conozcas con sus respectivos países e imprime los resultados usando el truco de iteración key, value.
Otro tipo útil de bucle for lo proporciona la función zip(). Puedes pensar en la función zip() como una cremallera, que va uniendo por turnos elementos de dos iteradores distintos. Aquí tienes un ejemplo:
first_names = ["Ada", "Adam", "Grace", "Charles"]
last_names = ["Lovelace", "Smith", "Hopper", "Babbage"]
for forename, surname in zip(first_names, last_names):
print(f"{forename} {surname}")Ada Lovelace
Adam Smith
Grace Hopper
Charles Babbage
La función zip es muy útil en la práctica.
Combina con zip los nombres de pila de arriba con esta lista desordenada de apellidos: ['Babbage', 'Hopper', 'Smith', 'Lovelace'].
(Pista: antes en el capítulo has visto un truco para ayudar a reorganizar listas).
Hay una segunda forma de hacer bucles en Python y, en la mayoría de los casos, aunque no en todos los casos, se ejecuta más rápido. Más importante aún, y esta es la razón por la que es buena práctica usarlas cuando sea posible, son muy legibles. Se llaman list comprehensions (comprensiones de listas).
Las list comprehensions pueden combinar lo que hacen un bucle for y (si hace falta) una condition en una sola línea de código. Primero, veamos un bucle for que suma uno a cada valor, hecho como list comprehension (NB: en la práctica, usaríamos los rapidísimos arrays de numpy para este tipo de operación):
num_list = range(50, 60)
[1 + num for num in num_list][51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
El patrón general se parece un poco al del bucle for, pero hay algunas diferencias. No hay dos puntos ni sangría. La sintaxis es “haz algo con x” y luego for x in iterable. Por último, la expresión se envuelve entre [ y ] para que la salida sea una lista.
Ten en cuenta que las listas no son el único envoltorio que puedes dar a este tipo de estructura. Con ( y ) se crea un generador (no te preocupes por qué es esto por ahora), con { y } se crea un conjunto (un objeto que solo contiene valores únicos), ¡e incluso es posible crear un diccionario a partir de una comprehension! Las list comprehensions son las más comunes, así que si solo recuerdas un tipo, recuerda ese.
Crea una list comprehension que multiplique por 5 los números del rango de 1 a 10.
¿Acertaste con el rango?
Veamos ahora cómo incluir una condición dentro de una list comprehension. Supongamos que tenemos una lista de números y queremos filtrarla según si los números son divisibles por 3 o no, usando el operador módulo:
number_list = range(1, 40)
divide_list = [x for x in number_list if x % 3 == 0]
print(divide_list)[3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39]
La sintaxis aquí es: haz algo con x para x en algo si x cumple alguna condición.
Aquí tienes otro ejemplo que selecciona solo los nombres que incluyen ‘Smith’:
names_list = ["Joe Bloggs", "Adam Smith", "Sandra Noone", "leonara smith"]
smith_list = [x for x in names_list if "smith" in x.lower()]
print(smith_list)['Adam Smith', 'leonara smith']
Observa cómo usamos ‘smith’ en lugar de ‘Smith’ y luego usamos lower() para asegurarnos de encontrar los nombres sin importar si están escritos en mayúsculas o minúsculas.
Incluso podemos hacer una construcción completa if … else dentro de una list comprehension:
names_list = ["Joe Bloggs", "Adam Smith", "Sandra Noone", "leonara smith"]
smith_list = [x if "smith" in x.lower() else "Not Smith!" for x in names_list]
print(smith_list)['Not Smith!', 'Adam Smith', 'Not Smith!', 'leonara smith']
Muchas de las construcciones que hemos visto se pueden combinar. Por ejemplo, no hay razón para no tener una list comprehension anidada o repetida usando zip() y, quizá más sorprendente, ¡a veces son útiles!
first_names = ["Ada", "Adam", "Grace", "Charles"]
last_names = ["Lovelace", "Smith", "Hopper", "Babbage"]
names_list = [x + " " + y for x, y in zip(first_names, last_names)]
print(names_list)['Ada Lovelace', 'Adam Smith', 'Grace Hopper', 'Charles Babbage']
Un uso aún más extremo de las list comprehensions puede producir estructuras anidadas:
first_names = ["Ada", "Adam"]
last_names = ["Lovelace", "Smith"]
names_list = [[x + " " + y for x in first_names] for y in last_names]
print(names_list)[['Ada Lovelace', 'Adam Lovelace'], ['Ada Smith', 'Adam Smith']]
Esto da una estructura anidada que (en este caso) itera primero sobre first_names y luego sobre last_names. (¡Observa que este objeto es una lista de listas de strings!)
Veamos ahora una dictionary comprehension. Se parecen un poco a las set comprehensions porque usan { y } en los extremos, pero se diferencian en que llevan dos puntos que separan las claves de los valores:
{key: value for key, value in zip(first_names, last_names)}{'Ada': 'Lovelace', 'Adam': 'Smith'}
Crea una list comprehension anidada que produzca una lista de listas de strings igual a [['a0', 'b0', 'c0'], ['a1', 'b1', 'c1'], ['a2', 'b2', 'c2']] (es decir, una combinación de los tres primeros enteros y las tres primeras letras del alfabeto). Puede que necesites convertir números a strings usando str(x) para hacerlo.
Si quieres aprender más sobre las list comprehensions, echa un vistazo a estos breves videotutoriales.
Los bucles while siguen ejecutando código hasta que su expresión condicional se evalúa como False. (Por supuesto, si se evalúa como True para siempre, tu código simplemente seguirá ejecutándose…)
n = 10
while n > 0:
print(n)
n -= 1
print("execution complete")10
9
8
7
6
5
4
3
2
1
execution complete
NB: por si te preguntas qué hace -=, es una asignación compuesta que hace que el lado izquierdo sea igual al lado izquierdo menos el lado derecho.
Puedes usar la palabra clave break para salir de un bucle while, por ejemplo si ha alcanzado cierto número de iteraciones sin converger.
Usando import string y luego string.ascii_lowercase para obtener los caracteres del alfabeto, escribe un bucle while que recorra el alfabeto hacia atrás (empezando por “z”) antes de imprimir “execution complete”.
Los bucles for, los bucles while y las comprehensions funcionan con dataframes de pandas, pero en general son una mala forma de hacer las cosas porque son lentos y no son eficientes en memoria. Para los casos en que se necesita iterar, pandas tiene métodos integrados de iteración según lo que necesites hacer.
Estos métodos integrados de iteración se solapan con lo que vimos en Transformación de datos, pero aquí profundizaremos un poco más en assign()/las operaciones de asignación, apply() y eval().
assignUna asignación es una instrucción que asigna el valor de la derecha al objeto de la izquierda, con un signo igual en medio.
Imaginemos que tenemos un dataframe como este:
import numpy as np
import pandas as pd
df = pd.DataFrame(np.random.normal(size=(6, 4)), columns=["a", "b", "c", "d"])
df| a | b | c | d | |
|---|---|---|---|---|
| 0 | -1.811118 | 0.419058 | -0.075656 | -0.800214 |
| 1 | -2.212602 | 0.112817 | -1.323197 | -0.441031 |
| 2 | 0.391113 | 2.369127 | 1.358430 | 1.220568 |
| 3 | 0.888842 | -0.161434 | -0.696840 | 0.544608 |
| 4 | 0.771047 | 0.707021 | -1.019502 | -0.359522 |
| 5 | -1.369215 | -0.011079 | 0.516956 | 1.207807 |
pandas tiene muchas funciones integradas que ya están preparadas para iterar sobre filas y columnas; por ejemplo, para calcular la mediana de las filas o de las columnas, respectivamente:
df.median(axis="rows") # can also use axis=1a -0.489051
b 0.265938
c -0.386248
d 0.092543
dtype: float64
df.median(axis="columns") # can also use axis=00 -0.437935
1 -0.882114
2 1.289499
3 0.191587
4 0.173750
5 0.252938
dtype: float64
En estos casos, y en otros que usan funciones integradas, la iteración está oculta. Pero ¿qué pasa si queremos hacer algo que no es una función integrada y tampoco es una agregación? Tomemos el ejemplo de sumar cinco a cada entrada. Podríamos hacerlo iterando explícitamente fila por fila y luego repitiéndolo para cada columna, es decir
# Do not do this!
def add_five_slow(df):
for i in range(len(df)):
for j in range(len(df.columns)):
df.iloc[i, j] = df.iloc[i, j] + 5
%timeit add_five_slow(df)273 μs ± 565 ns per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Pero para hacer esto hay que acceder a cada celda individual y operar sobre ella, así que es muy lento: tarda milisegundos. pandas tiene formas mucho más rápidas de realizar la misma operación. Para operaciones sencillas sobre dataframes con un tipo de dato uniforme, basta con sumar cinco a todo el dataframe:
%timeit df + 56.65 μs ± 19.5 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Esto tardó decenas de microsegundos, mucho más rápido.
Esto también funciona columna por columna, así que puedes hacer df["a"] = df["a"] + 5, etc.
Estas operaciones tienen equivalentes con el operador assign(), que permite el encadenamiento de métodos (method chaining), es decir, enlazar varias operaciones. La versión con el operador assign() de df["new_a"] = df["a"] + 5 sería
df = df.assign(new_a=lambda x: x["a"] + 5)¿Qué pasa si quieres iterar con una función más complicada? Aquí es donde entra apply() de pandas, que se puede usar junto con la asignación. apply() también puede usarse a lo largo de filas o columnas. Al igual que assign(), puede combinarse con una función lambda y usarse con todo el dataframe o solo con una columna (en cuyo caso no hace falta especificar axis=).
df.apply(lambda x: x["a"] - x["new_a"].mean() * x["c"] / x["b"], axis=1)0 -4.505251
1 -3.563891
2 -3.989228
3 -4.464514
4 -3.273261
5 -5.528083
dtype: float64
Ten en cuenta que esto es solo un ejemplo: ¡podrías hacer toda esta operación sin usar apply! Pero a veces te encontrarás con casos en los que sí necesitas usarlo.
Apply también funciona con funciones, incluidas las definidas por el usuario:
def complicated_function(x):
return x - x.mean()
df = df.apply(complicated_function, axis=1)
df| a | b | c | d | new_a | |
|---|---|---|---|---|---|
| 0 | -1.995308 | 0.234868 | -0.259847 | -0.984404 | 3.004692 |
| 1 | -1.997279 | 0.328140 | -1.107874 | -0.225708 | 3.002721 |
| 2 | -1.754957 | 0.223057 | -0.787640 | -0.925502 | 3.245043 |
| 3 | -0.403961 | -1.454237 | -1.989644 | -0.748196 | 4.596039 |
| 4 | -0.402971 | -0.466997 | -2.193520 | -1.533540 | 4.597029 |
| 5 | -2.164265 | -0.806130 | -0.278095 | 0.412756 | 2.835735 |
eval() evalúa un string que describe operaciones sobre columnas de un DataFrame para crear nuevas columnas. Opera solo sobre columnas, no sobre filas ni elementos. Aquí tienes un ejemplo:
df["ratio"] = df.eval("a / new_a")
df| a | b | c | d | new_a | ratio | |
|---|---|---|---|---|---|---|
| 0 | -1.995308 | 0.234868 | -0.259847 | -0.984404 | 3.004692 | -0.664064 |
| 1 | -1.997279 | 0.328140 | -1.107874 | -0.225708 | 3.002721 | -0.665156 |
| 2 | -1.754957 | 0.223057 | -0.787640 | -0.925502 | 3.245043 | -0.540812 |
| 3 | -0.403961 | -1.454237 | -1.989644 | -0.748196 | 4.596039 | -0.087893 |
| 4 | -0.402971 | -0.466997 | -2.193520 | -1.533540 | 4.597029 | -0.087659 |
| 5 | -2.164265 | -0.806130 | -0.278095 | 0.412756 | 2.835735 | -0.763212 |
Evaluate también puede usarse para crear nuevas columnas boolean usando, por ejemplo, un string "a > 0.5" en el ejemplo anterior.