Perfilar con timeit y memory_profiler
Mida el tiempo de ejecución con %timeit y la memoria máxima con memory_profiler para identificar las partes más lentas de su pipeline.
Perfilar con timeit y memory_profiler es una lección gratuita de Pandas & NumPy Academy en CoddyKit. Esta es la lección 1 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.
¿Por qué crear un perfil antes de optimizar?
Un error habitual es optimizar el código de forma intuitiva: reescribir una función que parece lenta sin medir si realmente es el cuello de botella. En las canalizaciones de datos, a menudo el 90 % del tiempo de ejecución procede del 10 % del código. La creación de perfiles identifica exactamente dónde se consumen el tiempo y la memoria, para que pueda centrar los esfuerzos de optimización donde sean relevantes. La regla de oro es: mida primero y optimice después. Herramientas como timeit miden el tiempo y memory_profiler mide la RAM.
import pandas as pd
import numpy as np
import timeit
# Naive approach vs vectorised approach — which is faster?
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(100000), 'b': np.random.randn(100000)})
t1 = timeit.timeit(lambda: df['a'] + df['b'], number=100)
t2 = timeit.timeit(lambda: [a + b for a, b in zip(df['a'], df['b'])], number=10)
print(f'Vectorised (100 runs): {t1:.3f}s')
print(f'List comprehension (10 runs): {t2:.3f}s')
print(f'Per-run speedup: ~{(t2/10)/(t1/100):.0f}x')timeit: el temporizador integrado de Python
El módulo timeit mide el tiempo de ejecución ejecutando una instrucción muchas veces y devolviendo el tiempo total transcurrido. timeit.timeit(stmt, number=n) ejecuta stmt n veces y devuelve el total en segundos. Divida entre number para obtener el tiempo por ejecución. Para obtener mediciones precisas, elija number de modo que la ejecución total dure al menos 0,1 segundos: una sola ejecución de una operación rápida no es representativa debido al ruido del sistema de planificación.
import timeit
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(10000)})
# Compare three ways to square a column
time_a = timeit.timeit(lambda: df['x'] ** 2, number=1000)
time_b = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=100)
time_c = timeit.timeit(lambda: np.square(df['x']), number=1000)
print(f'Operator **2 per run: {time_a/1000*1000:.3f} ms')
print(f'.apply(v**2) per run: {time_b/100*1000:.3f} ms')
print(f'np.square() per run: {time_c/1000*1000:.3f} ms')La magia %timeit en Jupyter
En los notebooks de Jupyter, la magia %timeit selecciona automáticamente el número de repeticiones y ejecuciones para proporcionar una estimación estadísticamente estable y, después, informa de los tiempos mejor y medio con su desviación estándar. Es la forma más práctica de realizar rápidamente una evaluación comparativa de una sola línea. Utilice %%timeit (doble signo de porcentaje) para medir una celda completa. El formato de salida es: X ns/μs/ms ± Y ns por bucle (media ± desv. est. de 7 ejecuciones, N bucles cada una).
# In Jupyter notebook:
# %timeit df['x'] ** 2
# Output: 47.3 us +- 1.2 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)
# Multi-line cell timing:
# %%timeit
# result = df['x'] ** 2
# total = result.sum()
# In a regular Python script, use timeit.timeit() instead:
import timeit
import pandas as pd, numpy as np
df = pd.DataFrame({'x': np.random.randn(10000)})
result = timeit.repeat(lambda: df['x']**2, number=1000, repeat=7)
print(f'Best run: {min(result)/1000*1000:.3f} ms per call')cProfile para crear perfiles a nivel de función
Mientras que timeit mide una sola expresión, cProfile crea un perfil de toda la pila de llamadas: muestra cuánto tiempo se emplea en cada función llamada durante la ejecución. Esto es esencial para identificar el cuello de botella en una canalización compleja. Ejecute python -m cProfile -s cumulative script.py desde la línea de comandos o utilice cProfile.run('function()') en un script. En Jupyter, utilice %prun function().
import cProfile
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(50000), 'b': np.random.randn(50000)})
def slow_pipeline(df):
result = []
for _, row in df.iterrows():
result.append(row['a'] * row['b'])
return pd.Series(result)
# Profile the function
print('Profile output (top 5 functions by cumulative time):')
cProfile.run('slow_pipeline(df.head(1000))', sort='cumulative')Medición del uso de memoria
Utilice df.memory_usage(deep=True) para consultar cuánta RAM consume cada columna. El argumento deep=True mide la memoria real utilizada por las columnas de tipo object (cadenas), que de otro modo solo informarían del tamaño del puntero. Utilice .sum() para obtener la memoria total del DataFrame en bytes. Divida entre 1e6 para obtener MB o entre 1e9 para obtener GB. Este es el punto de partida para optimizar la memoria: no puede reducir lo que no ha medido.
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({
'id': range(100000),
'name': ['user_' + str(i) for i in range(100000)],
'score': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D'], 100000)
})
mem = df.memory_usage(deep=True)
print('Memory per column (bytes):')
print(mem)
print(f'\nTotal: {mem.sum() / 1e6:.2f} MB')memory_profiler para medir la memoria línea por línea
El paquete memory_profiler (pip install memory-profiler) mide el uso de RAM línea por línea dentro de una función. Decore una función con @profile y ejecútela con python -m memory_profiler script.py. En Jupyter, utilice la magia %memit (de la extensión de ipython) para medir la memoria máxima de una sola expresión. Esto permite identificar qué estructuras de datos provocan picos repentinos de memoria, a menudo por concatenar dentro de un bucle en lugar de recopilar los datos y concatenarlos una sola vez.
# Install: pip install memory-profiler
# Usage as a decorator:
# from memory_profiler import profile
# @profile
# def process_data():
# import pandas as pd, numpy as np
# df = pd.DataFrame({'x': np.random.randn(1000000)})
# result = df['x'].rolling(100).mean() # <-- sees if this spikes RAM
# return result
# Run: python -m memory_profiler script.py
# In Jupyter (after %load_ext memory_profiler):
# %memit pd.DataFrame({'x': np.random.randn(1000000)}).rolling(100).mean()
print('memory_profiler decorates functions for line-by-line RAM measurement.')
print('Typical output: Line # Mem usage Increment Line Contents')sys.getsizeof frente a memory_usage para objetos
sys.getsizeof(obj) devuelve el tamaño de memoria de un objeto de Python, pero en los DataFrame de pandas solo informa de la sobrecarga del contenedor (unos cientos de bytes), no de los datos reales. Utilice siempre df.memory_usage(deep=True).sum() para los DataFrame. Para objetos individuales de Python, como listas y diccionarios, sys.getsizeof es preciso, pero no recorre los objetos anidados; utilice la biblioteca pympler (asizeof) para obtener el tamaño profundo de los objetos.
import sys
import pandas as pd
import numpy as np
df = pd.DataFrame({'x': np.random.randn(100000)})
# sys.getsizeof reports only the container metadata — misleading!
print('sys.getsizeof(df):', sys.getsizeof(df), 'bytes (misleading!)')
# memory_usage gives the true memory
true_mem = df.memory_usage(deep=True).sum()
print('df.memory_usage(deep=True).sum():', true_mem, 'bytes')
print(f'True memory: {true_mem/1e6:.2f} MB')Patrón de gestor de contexto para medir tiempos
Para pasos de canalización más largos en los que timeit no resulta adecuado (por ejemplo, consultas a bases de datos o E/S de archivos), utilice un gestor de contexto sencillo con time.perf_counter(). Envuelva cada paso de la canalización en un contexto temporizado para registrar su duración. Esto resulta más práctico que timeit en código de producción, porque registra el tiempo real transcurrido, incluida la espera de E/S; timeit también la mide, pero es más difícil de integrar con marcos de registro.
import time
import contextlib
import pandas as pd
import numpy as np
@contextlib.contextmanager
def timer(name):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f'[{name}] {elapsed*1000:.1f} ms')
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(500000), 'y': np.random.randn(500000)})
with timer('rolling mean'):
result1 = df['x'].rolling(30).mean()
with timer('groupby mean'):
df['group'] = df['x'].round(0)
result2 = df.groupby('group')['y'].mean()Creación sistemática de perfiles de operaciones de Pandas
Un flujo de trabajo sistemático para crear perfiles de rendimiento: 1) Comience con una muestra representativa de sus datos (10.000 filas son suficientes para comparar tiempos). 2) Identifique los pasos más lentos mediante un contexto de temporización o cProfile. 3) Reescriba el paso más lento utilizando un enfoque más rápido (vectorización, tipo Categorical, extensión de C). 4) Verifique la corrección con sentencias assert. 5) Vuelva a medir el tiempo para confirmar la mejora de velocidad. 6) Repita el proceso con el siguiente cuello de botella hasta que el flujo de procesamiento cumpla su SLA.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'value': np.random.randn(100000),
'category': np.random.choice(['A', 'B', 'C', 'D', 'E'], 100000)
})
# Benchmark two groupby implementations
t1 = timeit.timeit(lambda: df.groupby('category')['value'].mean(), number=100)
df2 = df.copy()
df2['category'] = df2['category'].astype('category')
t2 = timeit.timeit(lambda: df2.groupby('category')['value'].mean(), number=100)
print(f'Object dtype groupby: {t1/100*1000:.2f} ms per call')
print(f'Categorical dtype: {t2/100*1000:.2f} ms per call')
print(f'Speedup: {t1/t2:.1f}x')Cuándo crear perfiles de rendimiento: umbrales prácticos
No todos los scripts necesitan un análisis de rendimiento. Invierta en este análisis cuando: un flujo de procesamiento tarde más de 30 segundos en ejecutarse y se ejecute con frecuencia (diariamente o más), una función procese más de 100.000 filas o el uso de memoria supere el 50 % de la RAM disponible. Para los scripts que se ejecutan una sola vez o tardan unos segundos, la legibilidad es más importante que la optimización a nivel de microsegundos. El coste de mantener código muy optimizado y poco claro suele superar el tiempo ahorrado por la optimización.
Resumen de las herramientas de análisis de rendimiento
Referencia rápida:
- timeit.timeit — mide el tiempo de una sola instrucción (script)
- %timeit / %%timeit — mide el tiempo en Jupyter (repite automáticamente)
- %prun / cProfile — analiza la pila de llamadas a nivel de función
- df.memory_usage(deep=True) — mide la RAM utilizada por cada columna
- memory_profiler @profile — mide la RAM utilizada línea por línea
- time.perf_counter() — temporizador manual para los pasos del flujo de procesamiento
Comprobación rápida
Compruebe sus conocimientos sobre las herramientas de análisis de rendimiento de esta lección.
Recapitulación de la lección
En esta lección ha aprendido que timeit.timeit y %timeit miden el tiempo de ejecución de operaciones específicas, df.memory_usage(deep=True) mide con precisión la RAM utilizada por cada columna y cProfile / memory_profiler ofrecen un análisis más profundo para flujos de procesamiento complejos. Analice siempre el rendimiento antes de optimizar y verifique la corrección después de cada cambio. A continuación, veremos cómo evitar los bucles lentos con iterrows y sustituirlos por operaciones vectorizadas.
Preguntas frecuentes
¿La lección «Perfilar con timeit y memory_profiler» es gratis?
Sí — el texto completo de «Perfilar con timeit y memory_profiler» 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 «Perfilar con timeit y memory_profiler»?
Mida el tiempo de ejecución con %timeit y la memoria máxima con memory_profiler para identificar las partes más lentas de su pipeline. 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 1 de 4.
¿Cuánto tiempo toma la lección «Perfilar con timeit y memory_profiler»?
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
- Perfilar con timeit y memory_profiler
- Evitar iterrows y los bucles de Python
- Tipos de datos eficientes para reducir la memoria
- Lectura por bloques de archivos grandes