¿Por qué falla mi pipeline de datos? Una guía amigable para el perfilado de memoria en Python
1. El misterio de la RAM que desaparece
Pasar tres horas construyendo un pipeline de datos, ¿solo para verlo fallar al 99%? Lo pruebas con una pequeña muestra de 1,000 filas y funciona bien. Sin embargo, cuando le pasas el dataset completo — tu computadora empieza a ralentizarse, el cursor se congela y luego la terminal escupe un MemoryError. O tu kernel de Jupyter simplemente muere sin decir una palabra.
Parece una traición. La reacción natural es “solo necesito una computadora más potente”. Pero más RAM suele ser un parche. El código ineficiente consumirá 32GB con la misma rapidez que consumió 8GB. El objetivo no es tener más espacio; es ver exactamente a dónde se va tu memoria.
Consideremos un escenario típico. Cargamos un CSV que ocupa aproximadamente 500MB en disco. Podrías pensar que eso consume 500MB de RAM. No exactamente.
import pandas as pd
import numpy as np
import os
# Let's create a dummy CSV file of about 500MB
def create_huge_file(filename="large_data.csv"):
df = pd.DataFrame({
'id': range(10000000),
'val1': np.random.randn(10000000),
'val2': np.random.randn(10000000)
})
df.to_csv(filename, index=False)
print(f"File {filename} created.")
# This is where the trouble starts
def load_and_process(filename):
print("Loading data...")
# Pandas often uses 3x to 10x the memory of the raw file size
df = pd.read_csv(filename)
# Creating a copy for 'processing' doubles the usage
df2 = df.copy()
return df2.sum()
# create_huge_file() # Uncomment to run locally
# load_and_process("large_data.csv")
range(10000000)— Elrangede Python es una secuencia perezosa (lazy). Genera enteros bajo demanda, por lo que crear un rango de diez millones casi no cuesta nada en memoria. Pandas luego lo consume de inmediato al construir el DataFrame.np.random.randn(10000000)— Esto asigna de inmediato un bloque contiguo de memoria NumPy para 10 millones de valores float64 (~80 MB cada uno). Dos columnas significan ~160 MB de datos de matriz en bruto, pero el envoltorio del DataFrame y la maquinaria de tipos elevan la huella en RAM.df.to_csv(filename, index=False)— Serializar el DataFrame a CSV es un formato de texto; cada flotante se convierte en una cadena de caracteres ASCII, lo que puede inflar temporalmente la memoria durante la escritura antes de que el archivo llegue al disco.pd.read_csv(filename)— Al leer el CSV de vuelta, Pandas debe analizar el texto, inferir los dtypes de las columnas y construir objetos DataFrame. La representación en memoria resultante suele ser de 3 a 10× el tamaño del archivo en disco porque la sobrecarga de los objetos de Python, la promoción de dtype y los búferes internos se acumulan.df2 = df.copy()—.copy()crea una copia profunda completa de todo el DataFrame. En este momento, tantodfcomodf2existen simultáneamente en RAM, duplicando la huella de memoria. Esta es la clásica trampa del “Dirty Bowl” que describe el artículo.df2.sum()— Devuelve una pequeña Series con las sumas por columna. El objeto intermedio costoso (df2) sigue vivo en el ámbito de quien lo llama, por lo que aún no se libera nada.
Ejecuta eso, y el archivo de 500MB puede inflarse a 2GB o 3GB en memoria. Los objetos de Python son “pesados”: llevan consigo información adicional. Si no sabes cómo rastrear esto, solo estás adivinando.
2. Piensa en la memoria como un mostrador de cocina
Para entender la memoria sin la jerga informática, imagina tu RAM como un mostrador de cocina. Tu disco duro es la despensa.
Cuando quieres cocinar (procesar datos), sacas los ingredientes de la despensa y los pones en el mostrador. Un mostrador enorme te permite colocar todo a la vez. La mayoría de nosotros trabajamos con menos.
Las variables intermedias —esas pequeñas variables df2 o temp_list que creamos— son como tazones sucios que olvidaste lavar. Los usaste para un paso. Ahora solo están ahí en el mostrador, ocupando espacio. Si sigues tomando tazones nuevos sin limpiar los viejos, te quedarás sin espacio para cortar tus verduras.
El pico de memoria es el momento en que tu mostrador está más abarrotado. No importa si limpias después de la comida. Si en algún momento el mostrador estuvo tan lleno que no cabía ni una sola cebolla más, todo el proceso se detiene. Por eso tu pipeline falla.
3. El método ‘Snapshot’: viendo el panorama completo
La forma más fácil de empezar a limpiar tu cocina es encontrar qué paso causa el mayor desastre. memory_profiler ayuda con esto. Recorre tu código línea por línea y muestra cuánta memoria añade cada línea al montón.
En Jupyter, usarás el comando %mprun. Instálalo primero: pip install memory_profiler.
# In a real scenario, you'd put your function in a separate .py file
# For this example, imagine we are profiling this function:
# @profile # This decorator tells the profiler which function to watch
def process_data_inefficiently():
import pandas as pd
import numpy as np
# Line 1: Load data
a = pd.DataFrame(np.random.randn(1000000, 10))
# Line 2: Make a copy (The 'Dirty Bowl' trap)
b = a.copy()
# Line 3: Do a calculation
c = b.describe()
return c
@profile— Este es el decorador dememory_profiler. Cuando lo colocas sobre una función, el profiler instrumenta cada línea dentro de esa función, midiendo la memoria antes y después de que se ejecute cada línea. La diferencia entre esas dos mediciones es la columna Incremento en la salida. Nota:%mprunen Jupyter requiere que la función objetivo resida en un archivo.pyreal, no en una celda del notebook, porque recarga el módulo desde el disco con el decorador inyectado.np.random.randn(1000000, 10)— Asigna de forma anticipada un arreglo de 1,000,000 × 10 de valores float64. Eso son 80 millones de bytes (~76 MB) de datos crudos del arreglo, antes de que Pandas lo envuelva.pd.DataFrame(np.random.randn(1000000, 10))— Envolver el arreglo de NumPy en un DataFrame añade sobrecarga de objetos: objetos índice, etiquetas de columna, búferes internos. La columnaIncrementmostrará aproximadamente 76 MiB para esta línea.b = a.copy()— Crea una copia profunda completa del DataFrame. La columnaIncrementmostrará otro pico de ~76 MiB aquí, porque tantoacomobahora coexisten en la RAM. Esta es la línea que el profiler marcará como el “Tazón sucio” — duplicando la memoria sin razón.b.describe()— Calcula estadísticas descriptivas (media, desviación estándar, mínimo, máximo, cuartiles). El resultado (c) es un pequeño DataFrame de 4 filas × 10 columnas, por lo que el incremento es pequeño, peroaybsiguen activos, por lo que el pico de memoria se mantiene alto.return c— Soloc(diminuto) escapa del ámbito de la función. Peroaybsolo se liberan después de que la función retorna, por lo que el pico de memoria ya se alcanzó duranteb.describe().
Ejecuta el profiler y obtendrás una tabla. La columna más importante es Incremento.
- Un incremento de 500 MiB significa que esa línea acaba de consumir medio gigabyte de RAM.
- Un incremento de 0 MiB significa que no utilizó memoria extra.
Así que si detectas un enorme incremento en una línea que solo copia una variable, ya encontraste al culpable. Estás desperdiciando espacio al mantener dos copias de los mismos datos en tu “mesada”.
4. El profiler ‘Fil’: encontrando el pico
A veces memory_profiler es demasiado lento, o no detecta el pico porque el aumento de memoria ocurre dentro de una biblioteca como NumPy o Pandas. Fil resuelve esto. Está diseñado para científicos de datos.
Fil no rastrea cada asignación: rastrea el Uso máximo de memoria. Genera un gráfico de flama, un diagrama que muestra exactamente qué función fue la responsable cuando la memoria alcanzó su punto más alto.
Para usarlo, ejecuta tu script así en tu terminal:
fil-profile run my_script.py
Fil abre una ventana del navegador con el gráfico. Busca los bloques más anchos en la parte inferior. Un bloque ancho etiquetado como read_csv significa que tus datos son demasiado grandes para cargarlos de una sola vez. Un bloque ancho concat significa que estás creando demasiadas copias temporales durante una combinación.
Fil también detecta la memoria utilizada por las extensiones de C. Pandas está escrito en C por debajo, por lo que las herramientas estándar de Python a veces pasan por alto lo que asigna. Fil no se lo pierde.
¿Qué herramienta de memoria deberías usar? Elige según la tarea, no por costumbre.
| Herramienta | Lo que hace | Ideal para | Dónde se queda corto |
|---|---|---|---|
sys.getsizeof(obj) | Informa el tamaño en bytes de un solo objeto de Python. Sin configuración, biblioteca estándar. | Una revisión rápida de 5 segundos en una variable: “¿Esta lista es de 10 KB o 10 MB?” | Solo mide la sobrecarga del propio objeto, no lo que referencia internamente. Una lista de 10 millones de referencias al mismo arreglo se reporta como diminuta. Inútil para rastrear fugas a través de los límites de las funciones. |
tracemalloc | Rastreador de asignaciones de la biblioteca estándar. Toma instantáneas de todas las asignaciones activas, con atribución de archivo/línea, y calcula las diferencias entre instantáneas. | Cuando necesitas ver qué línea asignó más memoria entre dos puntos de control — y no quieres instalar paquetes de terceros. | Ralentiza tu programa (interceptar cada asignación tiene una sobrecarga real). No “ve a través” de las interioridades de las extensiones de C (búferes de C de NumPy y Pandas) tan limpiamente como lo hace Fil. |
memory_profiler (%mprun) | Incrementos de memoria línea por línea por función, a través del decorador @profile. Tabla limpia con una columna de “Incremento”. | Cuando ya sabes qué función es el problema y quieres profundizar en qué línea específica es la culpable. | Ralentiza la ejecución significativamente (sondea la memoria en cada línea). Pierde picos que aparecen y se liberan dentro de una sola línea. En Jupyter, %mprun necesita que la función objetivo esté en un archivo .py, no en una celda del notebook. |
| Fil | Genera perfiles de la memoria pico y produce un gráfico de flama que muestra la pila de llamadas en el momento de uso máximo. | Cuando todo el script se bloquea con MemoryError y no sabes qué función culpar — Fil te dirige directamente al pico. | Requiere instalación de terceros. El resultado es un gráfico de flama basado en el navegador, no una celda del notebook. Es excesivo para scripts pequeños donde ya conoces la respuesta. |
Regla general: Comienza con sys.getsizeof para una revisión de 5 segundos. Si eso no es suficiente, acude a tracemalloc (sin instalación, biblioteca estándar) para ver las diferencias de asignación por asignación. Si necesitas granularidad línea por línea en una función conocida, usa memory_profiler. Si todo el script se bloquea y no puedes saber dónde, usa Fil.
5. La parte más difícil: por qué la memoria no siempre ‘desaparece’
Esta es la parte más difícil de la gestión de memoria de aceptar: Eliminar una variable no siempre libera la memoria.
Python tiene un recolector de basura. Piénsalo como un compañero de cuarto que recoge los tazones sucios de la mesada — eventualmente. No limpia en el momento en que terminas de comer. Espera hasta que tiene ganas, o hasta que la mesada está llena.
Luego está la trampa de la ‘Copia Oculta’. En versiones anteriores de Pandas, casi cada operación creaba una copia completamente nueva de tus datos. Pandas 2.0+ usa “Copy-on-Write”, lo cual ayuda, pero la trampa no ha desaparecido por completo.
import pandas as pd
import sys
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
# This might look like you're just looking at a slice...
subset = df[['a']]
# But sometimes Pandas creates a whole new object in memory.
# 'del df' might not free memory if 'subset' is still pointing
# to the original data hidden inside it!
import sys—sysse importa para que puedas llamar asys.getsizeof(df)y verificar el tamaño reportado del objeto, aunque no se llama en este fragmento. Incluso si lo hiciera,getsizeofsolo reportaría la sobrecarga del propio objeto Python del DataFrame, no los datos completos del arreglo NumPy subyacente a los que hace referencia — un error clásico al usargetsizeofen objetos complejos.subset = df[['a']— Esto es un slice de columna. En versiones anteriores de Pandas,df[['a']]creaba un nuevo DataFrame con su propia copia de los datos. En Pandas 2.0+ con Copy-on-Write habilitado,subsetinicialmente comparte la memoria subyacente condf— pero tan pronto como se modificadfosubset, Pandas crea una copia detrás de escena (esa es la parte de “Copy-on-Write”). De cualquier manera,subsetpuede mantener una referencia al mismo gestor de bloques subyacente quedf.del df—delelimina el enlace de nombredfdel espacio de nombres. Pero sisubsetaún hace referencia a los datos subyacentes (directamente o a través de componentes internos compartidos), el recolector de basura ve que todavía hay una referencia activa y no liberará esa memoria. El objeto sobrevive; solo el nombre desaparece.- La solución práctica — No confíes en
delpara recuperar memoria. Si no necesitas el original después de una transformación, sobrescribe la misma variable:df = df.drop(columns=['b'])en lugar de creardf2 = df.drop(...). Cuando eldfantiguo se reenlaza, y ninguna otra variable (comosubset) mantiene una referencia, el recolector de basura finalmente puede reclamar el bloque antiguo.
Lo que esto significa en la práctica: no confíes en del. Evita crear la variable en primer lugar. Si no necesitas los datos originales después de una transformación, sobrescríbelos — df = df.drop(columns=['unnecessary']) en lugar de df2 = df.drop(...).
6. Tu nuevo flujo de trabajo: una lista de verificación de 3 pasos
Ahora que la intuición encaja, aquí te mostramos cómo manejar tu próximo pipeline que se cae:
- Paso 1: Usa
%memit. Verifica el uso total de memoria de tu función. ¿Es realmente más alto de lo que esperabas? (por ejemplo, “Espera, ¿por qué este archivo de 100MB está usando 2GB de RAM?”) - Paso 2: Usa Fil. Encuentra el ‘pico’. Identifica la línea exacta de código que empuja tu memoria al límite.
- Paso 3: Divídelo en fragmentos. Si tus datos son demasiado grandes para la encimera, no los pongas todos en la encimera de una vez. Usa el parámetro
chunksizeen Pandas.
Aquí está el “Antes y Después” con chunking:
# THE OLD WAY (Crashes on large files)
def process_everything(filename):
df = pd.read_csv(filename)
return df['val1'].sum()
# THE NEW WAY (Uses almost zero memory regardless of file size)
def process_in_chunks(filename):
total = 0
# We only bring 100,000 rows onto the 'counter' at a time
for chunk in pd.read_csv(filename, chunksize=100000):
total += chunk['val1'].sum()
# When the loop moves to the next chunk, the old chunk
# is cleared off the counter automatically!
return total
# The result: The second function can process a 100GB file
# on a laptop with 8GB of RAM.
pd.read_csv(filename)(forma antigua) — Carga con avidez el archivo completo en un único DataFrame. Para un archivo de 100 GB, esto intentaría asignar 300–600 GB de RAM (tras la inflación de 3–10× de Pandas). Resultado:MemoryErrorinmediato.pd.read_csv(filename, chunksize=100000)(nueva forma) — El argumentochunksizehace queread_csvdevuelva un iterador perezoso (TextFileReader) en lugar de un DataFrame. Solo lee 100,000 filas a la vez, produce un fragmento, luego lee el siguiente lote en la próxima iteración. Esta es la misma filosofía de “generador” que usarías en un generador personalizado de Python — produce un elemento, deja que el consumidor lo procese, luego produce el siguiente.for chunk in ...— Cadachunkes un DataFrame regular de Pandas de 100,000 filas. Cuando el cuerpo del bucle termina y pasa a la siguiente iteración, la variablechunkanterior se reasigna, reduciendo su contador de referencias a cero. El Recolector de Basura (Garbage Collector) libera entonces la memoria de ese fragmento antes de cargar el siguiente — así que solo un DataFrame de 100,000 filas está vivo a la vez.total += chunk['val1'].sum()— Esta es la idea clave:sum()es una reducción conmutativa y asociativa. Sumar 1,000 fragmentos de 100,000 filas cada uno y agregar las sumas por fragmento da el mismo resultado que sumar 100,000,000 filas a la vez. El acumuladortotales un único entero (o float), con un costo esencialmente nulo.- Por qué esto no funcionaría para ordenar — El chunking solo funciona para operaciones que pueden descomponerse en cálculos independientes por fragmento.
sum(),mean(),count(), ymin()/max()son todas reducciones “vergonzosamente paralelas”. Perosort_values()requiere ver todas las filas a la vez para determinar el orden global — no puedes ordenar fragmentos de 100,000 filas de forma independiente y luego unirlos. Para eso, necesitarías una estrategia de ordenamiento externo (por ejemplo, DuckDB o Dask), no el chunking de Pandas.
Lo que cubrimos:
- La RAM es una encimera de cocina: Tienes espacio limitado, así que no dejes tazones sucios (variables sin usar) tirados por ahí.
- Pandas es pesado: A menudo usa mucha más memoria que el tamaño del archivo en disco.
- Usa las herramientas correctas:
memory_profilerpara verificaciones línea por línea, yFilpara encontrar el punto pico de caída. - El chunking funciona: Si los datos no caben, procésalos en pequeños fragmentos.
La próxima vez que tu pipeline se caiga, no saques tu tarjeta de crédito para comprar más RAM. Toma un profiler y revisa qué está pasando realmente bajo el capó.
Comprueba tu comprensión
Las preguntas a continuación van desde el simple recuerdo hasta el diseño abierto, siguiendo aproximadamente la taxonomía de Bloom.
Recordar ¿Qué es el “Peak Memory” y por qué el artículo dice que un pipeline puede fallar incluso si el uso promedio de memoria parece estar bien?
Comprender Con tus propias palabras, explica por qué un archivo CSV de 500MB puede aumentar a 2-3GB en la RAM una vez cargado en Pandas, usando la explicación de “objetos pesados” del artículo.
Aplicar
Usando el patrón de salida de memory_profiler del artículo (la columna “Increment”), si una línea que solo copia un DataFrame (df2 = df.copy()) muestra un incremento de 500 MiB, ¿qué te dice específicamente sobre dónde enfocar tu optimización?
Analizar
El ejemplo de fragmentación del artículo procesa un archivo en piezas de 100,000 filas y afirma que el fragmento antiguo “se limpia del contador automáticamente”. Analiza paso a paso por qué sumar chunk['val1'].sum() a un total acumulado funciona para esta agregación específica, pero no funcionaría si la tarea fuera en cambio “ordenar todo el archivo por val1”: ¿qué propiedad de la operación hace que la fragmentación sea viable o no?
Evaluar
El artículo advierte que del df podría no liberar memoria si subset todavía apunta a datos ocultos dentro de él. Critica la solución del artículo (“sobrescribe la variable en lugar de crear una nueva,” p. ej., df = df.drop(columns=[...])) como una solución completa: ¿reasignar df garantiza que se libere la memoria del DataFrame antiguo, o eso todavía depende de si algo más (como subset) mantiene una referencia a él?
Crear
Diseña un plan de depuración de memoria (siguiendo la lista de verificación de 3 pasos del artículo) para un nuevo escenario: un pipeline que combina tres archivos CSV de 2GB, agrega diez columnas calculadas y falla con MemoryError en una máquina de 16GB. Detalla qué herramienta usarías primero, qué buscarías en su salida y qué solución intentarías según lo que encuentres.
Artículos relacionados
- Generadores de Python: Cómo procesar conjuntos de datos masivos — El procesamiento por fragmentos con
pd.read_csv(chunksize=...)es simplemente el generador integrado de Pandas. Aprende la mecánica subyacente deyieldpara que puedas construir tus propios pipelines eficientes en memoria desde cero. - Polars vs Pandas: Una guía de migración práctica — Si el inflado de memoria de 3–10× de Pandas es la causa raíz, el modelo de memoria Arrow basado en Rust de Polars a menudo utiliza 2–4× menos RAM para la misma carga de trabajo. Vale la pena revisarlo antes de reescribir tu pipeline en fragmentos.
Referencias y lecturas adicionales
- Documentación de
tracemallocde Python — Referencia oficial del trazador de asignación de memoria de la biblioteca estándar, incluyendotake_snapshot(),compare()y estadísticas basadas en filtros. - Documentación del profilador Fil — Documentación oficial del profilador Fil, enfocada en gráficos de flama de memoria máxima para ciencia de datos y compatibilidad con extensiones de C.
Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .
«Aplica lo que aprendiste» es para suscriptores Supporter e Insider.
Suscríbete para desbloquear los ejercicios de este artículo.
Ver planesArtículos relacionados
- Ingeniería en Python En revisión
Generadores de Python: Cómo procesar conjuntos de datos masivos sin colapsar tu computadora
Aprende cómo los generadores de Python y la palabra clave yield te permiten procesar en flujo conjuntos de datos masivos con un consumo de memoria constante, evitando el MemoryError sin tener que cargar todo en la RAM.
- Ingeniería en Python En revisión
Deja de hacer sopa de variables: una guía para el encadenamiento de métodos en Pandas
Reemplaza los dataframes intermedios desordenados con cadenas de métodos limpias de Pandas usando .assign(), .pipe() y .query() para construir pipelines de datos legibles y mantenibles.
- Ingeniería en Python En revisión
¿Por qué mi código de Pandas es tan lento? Una guía práctica para la vectorización
Aprende por qué los ciclos fila por fila hacen que Pandas sea dolorosamente lento y cómo las operaciones vectorizadas, np.select y groupby ofrecen una aceleración de 10,000x con cambios mínimos en el código.
- Estadística En revisión
Pearson vs Spearman vs Kendall: Cómo elegir la correlación adecuada para tus datos
Aprende cuándo usar la correlación de Pearson, Spearman o Kendall en Python — y por qué un puntaje bajo puede significar que simplemente elegiste la herramienta incorrecta para tus datos.
¿Buscas otra cosa?
Busca en todos los artículos por título, resumen o tema.