0Pricing
Pandas & NumPy Academy · Lección

Evitar iterrows y los bucles de Python

Sustituya los bucles fila a fila por operaciones vectorizadas sobre columnas, np.where y pd.cut para conseguir aceleraciones de 10 a 100 veces.

Evitar iterrows y los bucles de Python es una lección gratuita de Pandas & NumPy Academy en CoddyKit. Esta es la lección 2 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Pandas & NumPy Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Pandas & NumPy Academy incluye 4 lecciones en total.

El coste de iterar fila por fila

Pandas se basa en NumPy, que procesa matrices completas de una vez mediante código C compilado. Al iterar fila por fila con for _, row in df.iterrows(), se evita este mecanismo y se vuelve a Python puro: cada fila se extrae como un objeto Series y el bucle se ejecuta en el intérprete de Python con un coste aproximadamente entre 100 y 1000 veces superior al de una operación vectorizada equivalente. En un DataFrame de 1 millón de filas, esto puede suponer minutos en lugar de milisegundos.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})

# Slow: iterrows loop
def loop_version(df):
    result = []
    for _, row in df.iterrows():
        result.append(row['a'] + row['b'])
    return pd.Series(result)

# Fast: vectorised
t_loop = timeit.timeit(lambda: loop_version(df), number=5)
t_vec = timeit.timeit(lambda: df['a'] + df['b'], number=500)

print(f'Loop (5 runs):       {t_loop:.3f}s total')
print(f'Vectorised (500 runs): {t_vec:.3f}s total')
print(f'Speedup: ~{(t_loop/5)/(t_vec/500):.0f}x')

Sustituir bucles condicionales con np.where

np.where(condition, value_if_true, value_if_false) es el equivalente vectorizado de un if/else elemento a elemento. En lugar de iterar por las filas y escribir una sentencia if, proporcione la condición y ambos valores de resultado como matrices. np.where realiza este cálculo en C para toda la columna de una vez, por lo que es entre 50 y 200 veces más rápido que un bucle de Python equivalente en DataFrames grandes.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'price': np.random.uniform(10, 100, 100000)})

# Slow: iterrows
def label_loop(df):
    labels = []
    for _, row in df.iterrows():
        if row['price'] > 50:
            labels.append('expensive')
        else:
            labels.append('cheap')
    return labels

# Fast: np.where
def label_vectorised(df):
    return np.where(df['price'] > 50, 'expensive', 'cheap')

# Verify equivalence on a small subset
assert list(label_loop(df.head(100))) == list(label_vectorised(df.head(100)))
print('Results match!')
print('Fast version result sample:', label_vectorised(df)[:5])

Varias condiciones con np.select

Para tres o más condiciones, utilice np.select(condlist, choicelist, default=) en lugar de anidar np.where. Proporcione una lista de matrices booleanas y otra de los valores de salida correspondientes. La primera condición que coincida determina el resultado; las filas que no coincidan reciben el valor default. Así se sustituyen las cadenas complejas de if/elif/else dentro de los bucles por una expresión vectorizada y clara.

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'score': np.random.randint(0, 101, 20)})

conditions = [
    df['score'] >= 90,
    df['score'] >= 75,
    df['score'] >= 60
]
choices = ['A', 'B', 'C']

df['grade'] = np.select(conditions, choices, default='F')
print(df.sort_values('score', ascending=False).head(10))

Operaciones vectorizadas con cadenas mediante .str

La manipulación de cadenas es una fuente habitual de bucles lentos. El accesorio .str de Pandas proporciona equivalentes vectorizados de todos los métodos de cadenas de Python: .str.lower(), .str.strip(), .str.replace(), .str.contains() y muchos más. Utilizar métodos .str es entre 10 y 50 veces más rápido que iterar por las filas y llamar manualmente a métodos de cadenas de Python.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
names = ['Alice Smith', 'BOB JONES', '  carol  ', 'Dave Brown'] * 25000
df = pd.DataFrame({'name': names})

# Slow: loop
def clean_loop(df):
    return [n.strip().title() for n in df['name']]

# Fast: .str accessor
def clean_vectorised(df):
    return df['name'].str.strip().str.title()

t1 = timeit.timeit(lambda: clean_loop(df), number=10)
t2 = timeit.timeit(lambda: clean_vectorised(df), number=50)

print(f'Loop (10 runs): {t1:.3f}s')
print(f'.str (50 runs): {t2:.3f}s')
print(f'Speedup: {(t1/10)/(t2/50):.0f}x')
print('Sample:', clean_vectorised(df).head(4).tolist())

Usar pd.cut y pd.qcut en lugar de bucles

pd.cut() y pd.qcut() vectorizan operaciones de agrupación por intervalos que a menudo se escriben como bucles con varias condiciones if/elif. Si se encuentra escribiendo 'if value < 18: age_group = child elif value < 65: age_group = adult' dentro de un bucle de filas, sustitúyalo por una sola llamada a pd.cut() que procese toda la columna de una vez a velocidad de C.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'age': np.random.randint(0, 90, 100000)})

# Slow: loop with conditionals
def categorise_loop(df):
    result = []
    for age in df['age']:
        if age < 18:
            result.append('child')
        elif age < 65:
            result.append('adult')
        else:
            result.append('senior')
    return result

# Fast: pd.cut
def categorise_cut(df):
    return pd.cut(df['age'], bins=[0, 18, 65, 100],
                  labels=['child', 'adult', 'senior'],
                  right=False)

assert list(categorise_loop(df.head(5))) == list(categorise_cut(df.head(5)).astype(str))
print('Fast version sample:', categorise_cut(df).head(5).tolist())

apply() no siempre es la respuesta

Muchos tutoriales recomiendan sustituir los bucles por .apply(func), pero apply sigue siendo internamente un bucle a nivel de Python: solo es ligeramente más rápido que iterrows y mucho más lento que la vectorización real. Reserve apply para los casos en los que no exista una alternativa vectorizada (por ejemplo, lógica compleja entre varias columnas). Si una función integrada de Pandas o NumPy puede expresar la operación, prefiera siempre esa función a apply.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(100000)})

# These all do the same thing — compare performance
t1 = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=20)
t2 = timeit.timeit(lambda: df['x'] ** 2, number=200)
t3 = timeit.timeit(lambda: np.square(df['x']), number=200)

print(f'apply(v**2): {t1/20*1000:.2f} ms per run')
print(f'** operator: {t2/200*1000:.2f} ms per run')
print(f'np.square(): {t3/200*1000:.2f} ms per run')

Sustituir bucles de groupby con agg y transform

Un patrón habitual consiste en iterar manualmente por los grupos: for name, group in df.groupby('cat'): do_something(group). Esto es lento por las mismas razones que iterrows. Sustitúyalo por groupby().agg() para generar estadísticas resumidas o por groupby().transform() para distribuir las estadísticas de cada grupo en las posiciones de las filas originales.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'cat': np.random.choice(['A','B','C','D'], 100000),
    'val': np.random.randn(100000)
})

# Slow: manual loop to add group mean
def slow_group_mean(df):
    means = {}
    for name, group in df.groupby('cat'):
        means[name] = group['val'].mean()
    return df['cat'].map(means)

# Fast: transform
def fast_group_mean(df):
    return df.groupby('cat')['val'].transform('mean')

t1 = timeit.timeit(lambda: slow_group_mean(df), number=10)
t2 = timeit.timeit(lambda: fast_group_mean(df), number=100)
print(f'Loop:       {t1/10*1000:.1f} ms')
print(f'transform:  {t2/100*1000:.1f} ms')
print(f'Speedup: {(t1/10)/(t2/100):.0f}x')

Cuándo es aceptable usar iterrows

A pesar de ser lentos, iterrows e itertuples tienen usos legítimos: imprimir o registrar información de las filas (el rendimiento es irrelevante), construir cargas útiles para API cuando cada fila desencadena una llamada HTTP (la latencia de red es el factor dominante) y depurar filas concretas con condiciones complejas. Si tiene menos de 1.000 filas y la operación se ejecuta una sola vez, la diferencia entre un bucle y la vectorización es de milisegundos, por lo que no compensa la complejidad adicional del código.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'order_id': [101, 102, 103],
    'product': ['Widget', 'Gadget', 'Doohickey'],
    'amount': [29.99, 49.99, 9.99]
})

# Acceptable use: building a structured log (small data, one-time)
for _, row in df.iterrows():
    print(f'Order {row["order_id"]}: {row["product"]} — ${row["amount"]:.2f}')

itertuples es más rápido que iterrows

Si debe iterar por las filas en Python (porque no existe un equivalente vectorizado), utilice itertuples() en lugar de iterrows(). Devuelve cada fila como una tupla con nombre en lugar de como una Serie de Pandas, evitando el coste de construir la Serie. Por lo general, esto lo hace entre 5 y 10 veces más rápido que iterrows. Acceda a los valores mediante la notación de atributos: row.column_name. Evítelo si los nombres de las columnas contienen espacios (no son nombres de atributo válidos).

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})

t1 = timeit.timeit(
    lambda: [row.a + row.b for row in df.itertuples()], number=10)
t2 = timeit.timeit(
    lambda: [row['a'] + row['b'] for _, row in df.iterrows()], number=10)

print(f'itertuples: {t1/10*1000:.1f} ms')
print(f'iterrows:   {t2/10*1000:.1f} ms')
print(f'itertuples speedup: {t2/t1:.1f}x')

Lista de comprobación de patrones de vectorización

Antes de escribir un bucle, hágase estas preguntas:

  • ¿La operación se realiza elemento a elemento en una columna? → Utilice una expresión aritmética de columna o una ufunc de NumPy.
  • ¿Incluye lógica condicional? → Utilice np.where (2 condiciones) o np.select (3 o más).
  • ¿Agrupa los valores en intervalos? → Utilice pd.cut o pd.qcut.
  • ¿Aplica operaciones con cadenas? → Utilice el accesorio .str.
  • ¿Agrega datos dentro de grupos? → Utilice groupby().agg() o groupby().transform().
Recurra a apply() o itertuples() únicamente si ninguna de las opciones anteriores es aplicable.

Ejemplo de mejora de velocidad: cálculo de precios

A continuación se muestra una refactorización completa, antes y después, de un cálculo empresarial habitual: aplicar descuentos escalonados según la cantidad del pedido. La versión con bucle es legible, pero demasiado lenta para DataFrames grandes. La versión vectorizada que utiliza np.select obtiene el mismo resultado en una décima parte del tiempo.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'price': np.random.uniform(10, 200, 100000),
    'qty': np.random.randint(1, 500, 100000)
})

# BEFORE: loop with conditionals
def slow_discount(df):
    result = []
    for _, row in df.iterrows():
        if row['qty'] >= 100:
            disc = 0.2
        elif row['qty'] >= 50:
            disc = 0.1
        elif row['qty'] >= 10:
            disc = 0.05
        else:
            disc = 0.0
        result.append(row['price'] * (1 - disc))
    return pd.Series(result)

# AFTER: np.select
def fast_discount(df):
    conditions = [df['qty'] >= 100, df['qty'] >= 50, df['qty'] >= 10]
    discounts = [0.20, 0.10, 0.05]
    disc = np.select(conditions, discounts, default=0.0)
    return df['price'] * (1 - disc)

assert slow_discount(df.head(200)).round(4).equals(fast_discount(df.head(200)).reset_index(drop=True).round(4))
print('Both versions agree!')

Comprobación rápida

Compruebe sus conocimientos sobre vectorización de esta lección.

Recapitulación de la lección

En esta lección ha aprendido que iterrows es entre 100 y 1000 veces más lento que las operaciones vectorizadas, np.where y np.select sustituyen los bucles condicionales, el accesorio .str vectoriza las operaciones con cadenas y groupby().transform() sustituye la iteración manual por grupos. Cuando sea necesario iterar, utilice itertuples en lugar de iterrows para obtener una mejora de entre 5 y 10 veces. A continuación, exploraremos tipos de datos eficientes: reducir el tamaño de los tipos numéricos y utilizar Categorical para disminuir la memoria hasta un 70 %.

Preguntas frecuentes

¿La lección «Evitar iterrows y los bucles de Python» es gratis?

Sí — el texto completo de «Evitar iterrows y los bucles de Python» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Pandas & NumPy Academy, actualiza a CoddyKit PRO. El curso de Pandas & NumPy Academy incluye 4 lecciones en total.

¿Qué aprenderé en «Evitar iterrows y los bucles de Python»?

Sustituya los bucles fila a fila por operaciones vectorizadas sobre columnas, np.where y pd.cut para conseguir aceleraciones de 10 a 100 veces. Practicas Pandas & NumPy Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar Pandas & NumPy Academy?

No se requiere experiencia previa. Pandas & NumPy Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 2 de 4.

¿Cuánto tiempo toma la lección «Evitar iterrows y los bucles de Python»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de Pandas & NumPy Academy?

Sí. Cada lección de Pandas & NumPy Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Perfilar con timeit y memory_profiler
  2. Evitar iterrows y los bucles de Python
  3. Tipos de datos eficientes para reducir la memoria
  4. Lectura por bloques de archivos grandes
← Volver a Pandas & NumPy Academy