Vectorization: por qué debes abandonar los bucles for
Al finalizar este tema
Podrás explicar estructuralmente por qué la vectorización es más rápida y desarrollarás el hábito de reemplazar los bucles for por operaciones vectoriales de NumPy/pandas.
Intuición — mismo resultado, diferencia de 100 veces
Dos formas de elevar al cuadrado un millón de números:
import numpy as npimport time
data = list(range(1_000_000))arr = np.array(data)
# Método 1: bucle forstart = time.time()result_loop = [x ** 2 for x in data]print(f"Bucle for: {time.time() - start:.3f} segundos")
# Método 2: operación vectorial de NumPystart = time.time()result_vec = arr ** 2print(f"Vectorización: {time.time() - start:.3f} segundos")Bucle for: 0,142 segundos
Vectorización: 0,002 segundos ← unas 70 veces más rápidaLos resultados son los mismos. El código es más corto. La velocidad es decenas de veces mayor.
¿Por qué existe esta diferencia?
La razón por la que el bucle for de Python es lento es porque en cada iteración, el intérprete de Python interviene:
Bucle for (Python):
Iteración 1: comprobar tipo → crear objeto → operar → guardar objeto
Iteración 2: comprobar tipo → crear objeto → operar → guardar objeto
... (un millón de iteraciones)
Operación vectorial (NumPy):
Una vez: el código C procesa en bloque datos contiguos en memoria → devuelve el arrayNumPy ejecuta los bucles internamente mediante código C/Fortran. No tiene la sobrecarga del intérprete de Python y los datos se almacenan de forma contigua en la memoria, lo que permite un uso eficiente de la caché de la CPU.
Patrón 1 — Operaciones aritméticas
import numpy as np
prices = np.array([1000, 2500, 3000, 1500, 4000])tax_rate = 0.1
# ❌ Bucle fortotal_loop = []for p in prices: total_loop.append(p * (1 + tax_rate))
# ✅ Vectorizacióntotal_vec = prices * (1 + tax_rate)print(total_vec) # [1100. 2750. 3300. 1650. 4400.]prices * (1 + tax_rate) — Realiza una operación en todo el arreglo a la vez.
Patrón 2 — Filtrado por condiciones
scores = np.array([85, 42, 91, 67, 55, 78, 93, 38])
# ❌ Bucle forpassed = []for s in scores: if s >= 60: passed.append(s)
# ✅ Indexación booleanapassed = scores[scores >= 60]print(passed) # [85 91 67 78 93]scores >= 60 crea un arreglo booleano [True, False, True, True, False, True, True, False], y al usarlo como índice, solo se extraen los elementos que son True.
Patrón 3 — Lo mismo en pandas
import pandas as pd
df = pd.DataFrame({ "name": ["A", "B", "C", "D", "E"], "revenue": [500, 800, 300, 1200, 900], "cost": [200, 600, 150, 700, 400]})
# ❌ Bucle forprofits = []for _, row in df.iterrows(): profits.append(row["revenue"] - row["cost"])df["profit_loop"] = profits
# ✅ Vectorizacióndf["profit_vec"] = df["revenue"] - df["cost"]Recorrer las filas una por una con df.iterrows() es casi siempre un enfoque incorrecto en pandas. Las operaciones a nivel de columna se vectorizan.
Patrón 4 — Asignación condicional de valores
# ❌ Bucle forfor i, row in df.iterrows(): if row["profit_vec"] > 500: df.at[i, "grade"] = "A" else: df.at[i, "grade"] = "B"
# ✅ np.wheredf["grade"] = np.where(df["profit_vec"] > 500, "A", "B")np.where(condición, si_verdadero, si_falso) — aplica if/else a todo el array de una sola vez.
Condiciones múltiples más complejas:
conditions = [ df["profit_vec"] > 500, df["profit_vec"] > 200, df["profit_vec"] > 0]choices = ["A", "B", "C"]df["grade"] = np.select(conditions, choices, default="D")Cuando no es posible la vectorización
No todas las operaciones pueden ser vectorizadas:
# Cálculo acumulativo que depende del valor de la fila anterior# No puede paralelizarse porque cada fila debe consultar el resultado anteriorresult = [data[0]]for i in range(1, len(data)): result.append(result[-1] + data[i])En estos casos, busque funciones especializadas como np.cumsum(); si no existen, utilice un bucle for pero solo dentro del ámbito mínimo necesario.
Reglas de benchmarking
| Tamaño de datos | Bucle for | Vectorización | Diferencia |
|---|---|---|---|
| 1,000 | ~0.2ms | ~0.01ms | ~20x |
| 100,000 | ~20ms | ~0.1ms | ~200x |
| 10,000,000 | ~2s | ~20ms | ~100x |
Cuanto mayores son los datos, mayor es la ventaja de la vectorización.
Resumen clave
| Situación | Utilizar |
|---|---|
| Operaciones aritméticas | arr * 2, arr + arr2 |
| Filtrado condicional | arr[arr > 0] |
| Asignación condicional | np.where(), np.select() |
| Agregación | arr.sum(), arr.mean() |
| Recorrido por filas | ❌ En lugar de iterrows(), usar operaciones por columna |
apply() — un punto intermedio entre la vectorización y los bucles for
Para lógicas complejas que no pueden expresarse mediante operaciones vectoriales, utilice apply():
def categorize(row): if row["revenue"] > 1000 and row["profit_vec"] > 300: return "Excelente" elif row["profit_vec"] > 0: return "Normal" else: return "Pérdida"
df["category"] = df.apply(categorize, axis=1)apply() es más rápido que iterrows(), pero más lento que las operaciones vectoriales puras. Para lógica compleja se recomienda apply(), mientras que para operaciones simples la vectorización es el principio fundamental.
Vectorización de cadenas — Acceso .str
names = pd.Series(["Alice Smith", "Bob Jones", "Charlie Brown"])
# ❌ Bucle forupper_names = []for name in names: upper_names.append(name.upper())
# ✅ Vectorizaciónupper_names = names.str.upper()first_names = names.str.split(" ").str[0]has_e = names.str.contains("e", case=False)
print(first_names) # ["Alice", "Bob", "Charlie"]El accesor .str de pandas vectoriza los métodos de cadena. Realiza la mayoría de las operaciones con cadenas, como split, replace, contains y extract, sin necesidad de bucles for.
Lista de verificación para refactorización de transformaciones
Al encontrar un bucle for en el código existente, verifique en este orden:
- ¿Es una operación aritmética/de comparación? → Use directamente los operadores (
arr + 1,arr > 0) - ¿Es una bifurcación condicional? → Utilice
np.where()onp.select() - ¿Es procesamiento de cadenas? → Use el accesor
.str - ¿Es una agregación? → Utilice
.sum(),.mean(),.groupby() - ¿Es lógica compleja? → Utilice
apply() - ¿Depende de la fila anterior? → Utilice
cumsum(),shift()o busque funciones especializadas - Ninguna de las anteriores aplica → Use un bucle for (alcance mínimo)
groupby — Operaciones vectorizadas por grupo
df = pd.DataFrame({ "department": ["Ventas", "Desarrollo", "Ventas", "Desarrollo", "Desarrollo"], "salary": [5000, 6000, 4500, 7000, 5500]})
# ❌ Media por departamento con un bucle fordepartments = df["department"].unique()for dept in departments: mask = df["department"] == dept print(f"{dept}: {df.loc[mask, 'salary'].mean()}")
# ✅ Operación vectorial con groupbyprint(df.groupby("department")["salary"].mean())groupby() aplica operaciones vectoriales por grupo. Está optimizado internamente en C, lo que lo hace más rápido y conciso que los bucles for.
Consideraciones sobre dtype
La velocidad de las operaciones vectoriales también se ve afectada por el dtype:
# dtype object, columna con cadenas mezcladas → no vectorizablemixed = pd.Series([1, "two", 3]) # dtype: object → lentoclean = pd.Series([1, 2, 3]) # dtype: int64 → rápido
# Comprueba el dtypeprint(df.dtypes)# Si una columna numérica se detecta como object, debe convertirsedf["price"] = pd.to_numeric(df["price"], errors="coerce")Al leer un archivo CSV, si una sola celda de una columna numérica contiene una cadena vacía, el tipo de dato completo se convierte en object, lo que impide realizar operaciones vectoriales. Conviértalo a pd.to_numeric() antes de utilizarlo.
El hábito de verificar si existe un método vectorizado antes de escribir un bucle 'for' puede mejorar significativamente el rendimiento del procesamiento de datos.