desde hace algún tiempo. Aunque nada demasiado loco. Solo limpieza de datos básica, análisis de datos exploratorios y algunas funciones esenciales. También exploré cosas como el encadenamiento de métodos para obtener un código más limpio y organizado, y operaciones que interrumpen silenciosamente el flujo de trabajo de Pandas, sobre las cuales ya he escrito antes.
Realmente nunca pensé en el tiempo de ejecución. Honestamente, si mi código se ejecutó sin errores y me dio el resultado que necesitaba, estaba feliz. Incluso si todas las celdas de mi cuaderno tardaron unos minutos en terminar, no me importó. Sin errores no había problemas, ¿verdad?
Entonces me encontré con el concepto de vectorización. Y algo hizo clic.
Bajé por la madriguera del conejo, como suelo hacer. Cuanto más leo, más me doy cuenta de que "sin errores" y "código eficiente" son dos cosas muy diferentes. Su código Pandas puede ser completamente correcto y aun así ser terriblemente terrible a escala.
Así que este artículo soy yo documentando lo que encontré. Los errores que ralentizan el código de Pandas, por qué ocurren, cómo solucionarlos y cuándo el propio Pandas podría ser el cuello de botella. Si alguna vez utilizó una computadora portátil y asumió que el tiempo de espera era normal, esta es para usted.
Por qué el "código de trabajo" no es lo suficientemente bueno
Hay una razón por la que me tomó un tiempo pensar en esto. Pandas está diseñado para perdonar. Puedes escribir código de una docena de formas diferentes y la mayoría funcionará. Obtiene su resultado, su marco de datos se ve bien y continúa.
Pero esa flexibilidad tiene un costo oculto.
A diferencia de SQL o los sistemas de datos de nivel de producción, Pandas no te obliga a pensar en la eficiencia. No te avisa cuando estás haciendo algo caro. Simplemente… lo hace. A veces, lentamente. Pero lo hace.
Piénselo de esta manera. SQL tiene un optimizador de consultas. Analiza lo que estás pidiendo y descubre la forma más eficiente de conseguirlo. Pandas no tiene eso. Confía en usted para escribir código eficiente. Y si no sabe cómo es la eficiencia, nunca sabrá que se lo está perdiendo.
El resultado es que gran parte del código Pandas disponible es lo que yo llamaría cortésmente ineficiente. Funciona en pequeños conjuntos de datos. Funciona en conjuntos de datos medianos con un poco de paciencia. Pero en el momento en que le arrojas datos del mundo real, algo que tenga unos cientos de miles de filas o más, las grietas comienzan a aparecer. Lo que antes tomaba segundos, ahora toma minutos. Lo que tomó minutos se vuelve inutilizable.
Y lo frustrante es que nada parece estar mal. Sin errores. Sin advertencias. Sólo un cuaderno lento y un cursor giratorio.
Esa es la trampa. Pandas optimiza por conveniencia, no por velocidad. Y la comodidad es excelente, hasta que deja de serlo.
Entonces, el primer cambio es de mentalidad: el código funcional y el código eficiente no son lo mismo. Una vez que eso hace clic, todo lo demás sigue.
Elaboración de perfiles: deje de adivinar, comience a medir
Aquí hay algo que noté mientras bajaba por esta madriguera de conejo. La mayoría de las personas, cuando sienten que su código es lento, hacen una de dos cosas. O reescriben todo desde cero con la esperanza de que algo mejore, o simplemente lo aceptan y esperan.
Ninguno de esos es el movimiento correcto.
El movimiento correcto es medir primero. No puedes optimizar lo que no has identificado. Y la mayoría de las veces, la parte de su código que cree que es lenta no es en realidad el problema.
Pandas te ofrece algunas herramientas sencillas para empezar.
%timeit: sepa cuánto tiempo tardan realmente las cosas
%timeit es un comando mágico de Jupyter que ejecuta una línea de código varias veces y le brinda el tiempo promedio de ejecución. Es la forma más sencilla de comparar dos enfoques y saber, concretamente, cuál es más rápido.
importar pandas como pd importar numpy como np df = pd.DataFrame({ 'ventas': np.random.randint(100, 10000, tamaño=100_000), 'descuento': np.random.uniform(0.0, 0.5, tamaño=100_000) }) # Enfoque A %timeit df.apply(lambda fila: fila['ventas'] * fila['descuento'], eje=1) # Enfoque B %timeit df['ventas'] * df['descuento']
En un conjunto de datos de 100.000 filas, la diferencia no es sutil:
1,91 s ± 228 ms por bucle (media ± desviación estándar de 7 ejecuciones, 1 bucle cada una) 316 μs ± 14 μs por bucle (media ± desviación estándar de 7 ejecuciones, 1000 bucles cada una)
Misma salida. Costo completamente diferente. Ese es el tipo de cosas que nunca notarías con solo ejecutar el celular una vez y seguir adelante.
df.info() y df.memory_usage(): sepa lo que lleva consigo
La velocidad no se trata sólo de computación. La memoria también juega un papel muy importante. Un marco de datos repleto de tipos de datos incorrectos ralentizará todo incluso antes de que hayas escrito una sola transformación.
df.info()
Producción:
RangeIndex: 100000 entradas, 0 a 99999 Columnas de datos (2 columnas en total): # Columna Recuento no nulo Dtype — —— ————– —– 0 ventas 100000 int64 no nulo 1 descuento 100000 no nulo float64 dtypes: float64(1), int64(1) uso de memoria: 1,5 MB
Para comprobar el uso de la memoria
df.memory_usage(profundo=Verdadero)
Producción:
Índice 132 ventas 400000 descuento 800000 tipo d: int64
Aquí podemos ver que el descuento ocupa el doble de espacio. Esto se debe a que el descuento se almacena como un tipo de número "más pesado" (float64), mientras que las ventas se almacenan en un tipo de número "más ligero" (int32).
Esto se vuelve especialmente importante cuando se trabaja con columnas de cadenas o tipos de objetos que consumen memoria en secreto. Volveremos a esto en la siguiente sección.
La mentalidad de elaboración de perfiles
Las herramientas en sí son simples. El cambio está en cómo aborda su código. Antes de optimizar algo, pregúntese: ¿adónde va realmente el tiempo? Mide las partes lentas. Compara alternativas. Deje que los números le digan qué arreglar.
Porque lo que se siente lento y lo que es lento son a menudo dos cosas completamente diferentes.
Error n.º 1: operaciones por filas (el asesino silencioso)
Si hay algo que vi surgir una y otra vez mientras investigaba este tema, fue esto: personas recorriendo los marcos de datos de Pandas fila por fila. Y lo entiendo. Se siente natural. Piensa en sus datos una fila a la vez, por lo que escribe código que los procesa una fila a la vez.
El problema es que Pandas no piensa así.
Cómo funcionan realmente los pandas
Pandas está construido sobre NumPy, que almacena datos en bloques contiguos de memoria, columna por columna. Esto significa que Pandas está muy optimizado para operar en columnas enteras a la vez. Cuando haces eso, ejecuta operaciones vectorizadas rápidas, de bajo nivel bajo el capó.
En cambio, cuando recorre filas, básicamente está omitiendo todo eso. Estás cayendo en Python puro, una fila a la vez, con toda la sobrecarga que conlleva. En un conjunto de datos pequeño nunca lo notarás. En uno grande, tendrás que esperar mucho tiempo.
Hay dos patrones que aparecen constantemente.
.iterrows()
# Calcular un precio con descuento fila por fila precios_descuentos =[]para índice, fila en df.iterrows(): precios_descuentos.append(fila['ventas'] * (1 – fila['descuento'])) df['precio_descuento'] = precios_descuentos
Esto funciona. Te dará la respuesta correcta. Pero en un marco de datos con 100.000 filas, es tremendamente lento.
%timeit [fila['ventas'] * (1 – fila['descuento']) para índice, fila en df.iterrows()]
Producción:
10,2 s ± 1,73 s por bucle (media ± desviación estándar de 7 ejecuciones, 1 bucle cada una)
.aplicar(eje=1)
Este es más astuto porque parece más "parecido a los pandas". Pero aplicar una función a través del eje = 1 significa aplicarla fila por fila, que es esencialmente el mismo problema.
%timeit df.apply(fila lambda: fila['ventas'] * (1 – fila['descuento']), eje=1)
Producción:
1,5 s ± 88,1 ms por bucle (media ± desviación estándar de 7 ejecuciones, 1 bucle cada una)
Más rápido que .iterrows(), pero sigue funcionando fila por fila. Todavía lento.
La solución: operaciones vectorizadas
Aquí está el mismo cálculo, hecho de la manera que Pandas realmente quiere que lo hagas:
df['precio_descuento'] = df['ventas'] * (1 – df['descuento'])
cronometraremos
%timeit df['ventas'] * (1 – df['descuento'])
Producción:
688 μs ± 236 μs por bucle (media ± desviación estándar de 7 ejecuciones, 1000 bucles cada una)
Eso es todo. Una línea. Sin bucle. Sin lambda. Y es aproximadamente 14.800 veces más rápido que .iterrows() y 2.180 veces más rápido que .apply(axis=1).
Lo que sucede aquí es que Pandas pasa la columna completa a NumPy, que ejecuta la operación en el nivel C en toda la matriz a la vez. Sin gastos generales de Python. Sin iteración fila por fila. Sólo cálculo rápido y de bajo nivel.
Cuando .apply() está realmente bien
Para ser justos, .apply() no siempre es el villano. Cuando se aplica una función por columnas (eje=0, que es el valor predeterminado), suele ser perfectamente razonable. El problema es específicamente el eje = 1, que fuerza la ejecución fila por fila.
Y a veces su lógica es lo suficientemente compleja como para que una expresión vectorizada limpia no sea obvia. En esos casos, np.vectorize() o np.where() pueden brindarle algo más cercano al rendimiento vectorizado y al mismo tiempo permitirle expresar la lógica condicional con claridad.
# En lugar de esto df['category'] = df.apply( lambda fila: 'alta' if fila['sales'] > 5000 else 'low', axis=1 ) # Haz esto df['category'] = np.where(df['sales'] > 5000, 'high', 'low') %timeit df.apply(lambda row: 'high' if fila['ventas'] > 5000 más 'bajo', eje = 1) %timeit np.where(df['ventas'] > 5000, 'alto', 'bajo')
Producción:
1,31 s ± 189 ms por bucle (media ± desviación estándar de 7 ejecuciones, 1 bucle cada una) 1,3 ms ± 180 μs por bucle (media ± desviación estándar de 7 ejecuciones, 1000 bucles cada una)
Mismo resultado. Aproximadamente 1000 veces más rápido.
La regla general
Si estás escribiendo un bucle sobre filas en Pandas, detente y pregúntate: ¿se puede expresar esto como una operación de columna? Nueve de cada diez veces la respuesta es sí. Y cuando es así, la diferencia de desempeño es transformadora.
Si estás recorriendo filas, no estás usando Pandas. Estás usando Python con pasos adicionales.
Error nº 2: copias innecesarias y exceso de memoria
Las operaciones por filas reciben mucha atención cuando la gente habla del rendimiento de Pandas. La memoria se vuelve mucho menor. Lo cual es una pena, porque según mi experiencia al leer sobre esto, la memoria inflada es tan responsable de la lentitud de los portátiles como de los malos cálculos.
Aquí está la cosa. Las operaciones de Pandas no siempre modifican su marco de datos. Muchos de ellos crean silenciosamente una nueva copia de sus datos entre bastidores. Haga esto suficientes veces y no solo mantendrá un marco de datos en la memoria. Estás sosteniendo varios, todos a la vez, sin darte cuenta.
El costo oculto de las operaciones encadenadas
Las operaciones encadenadas son un culpable común. Parecen limpios y legibles, pero cada paso puede generar una copia intermedia que permanece en la memoria hasta que la recolección de basura la limpia.
# Cada paso aquí crea potencialmente una nueva copia df2 = df[df['sales'] > 1000] df3 = df2.dropna() df4 = df3.reset_index(drop=True) df5 = df4[['sales', 'discount']]
Cuando llegue a df5, potencialmente tendrá cinco versiones de sus datos flotando en la memoria simultáneamente. En un conjunto de datos pequeño esto es invisible. En uno grande, así es como te quedas sin RAM.
Columnas temporales que se quedan
Otro patrón que devora silenciosamente la memoria es la creación de columnas que sólo necesitas temporalmente.
df['ingresos_brutos'] = df['ventas'] * df['cantidad'] df['impuestos'] = df['ingresos_brutos'] * 0.075 df['ingresos_netos'] = df['ingresos_brutos'] – df['impuestos'] # Pero en realidad solo necesitabas ingresos_netos
ingresos_brutos e impuestos ahora son columnas permanentes en su marco de datos, ocupando memoria para el resto de su computadora portátil a pesar de que solo eran escalones.
La solución es sencilla. O calcule directamente:
df['ingresos_netos'] = (df['ventas'] * df['cantidad']) * (1 – 0,075)
O déjelos tan pronto como haya terminado:
df.drop(columnas=['ingresos_brutos', 'impuestos'], inplace=True)
Los tipos de datos incorrectos son bastante caros
Éste me sorprendió cuando lo encontré. De forma predeterminada, Pandas es bastante generoso con la cantidad de memoria que asigna a cada columna. Las columnas enteras obtienen int64. Las columnas flotantes obtienen float64. Las columnas de cadena se convierten en tipos de objetos, que es uno de los tipos que más memoria consume en Pandas.
Veamos cómo se ve realmente:
df = pd.DataFrame({ 'order_id': np.random.randint(1000, 9999, tamaño=100_000), 'ventas': np.random.randint(100, 10000, tamaño=100_000), 'descuento': np.random.uniform(0.0, 0.5, tamaño=100_000), 'región': np.random.choice(['norte', 'sur', 'este', 'oeste'], tamaño=100_000) }) df.memory_usage(deep=True)
Producción
Índice 132 order_id 400000 ventas 400000 descuento 800000 región 5350066 tipo d: int64
Esa columna de región, que solo tiene cuatro valores posibles, consume 5,3 MB como tipo de objeto. Conviértalo a categórico y observe lo que sucede:
df['región'] = df['región'].astype('categoría') df.memory_usage(deep=True)
Producción:
Índice 132 order_id 400000 ventas 400000 descuento 800000 región 100386 tipo d: int64
Desde 5,3 MB hasta aproximadamente 100 KB. Para una columna. La misma lógica se aplica a las columnas de números enteros donde no necesita el rango int64 completo. Si sus valores se ajustan cómodamente a int32 o incluso a int16, el downcasting ahorra memoria real.
df['ventas'] = df['ventas'].astype('int32') df['order_id'] = df['order_id'].astype('int32') df.memory_usage(deep=True)
Producción:
Índice 128 order_id 400000 ventas 400000 descuento 800000 región 100563 tipo d: int64
Unos pequeños cambios de tipo y su marco de datos ya es significativamente más ligero. Y un marco de datos más liviano significa operaciones más rápidas en todos los ámbitos, porque simplemente hay menos datos para mover.
El hábito de comprobar rápidamente la memoria
Antes de ejecutar cualquier transformación importante, vale la pena saber con qué estás trabajando:
print(f"Uso de memoria: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
Tarda un segundo y le indica exactamente cuánta memoria está consumiendo su marco de datos en ese momento. Conviértalo en un hábito antes y después de transformaciones importantes y rápidamente desarrollará una intuición para saber cuándo algo pesa más de lo que debería ser.
La percepción
El código lento no siempre se trata de computación. A veces su computadora portátil es lenta porque transporta muchos más datos de los necesarios, en formatos que son mucho más caros de lo necesario. Recortar la memoria no es un trabajo glamoroso, pero agrava la situación. Un marco de datos que es más liviano de almacenar es más rápido de filtrar, más rápido de fusionar y más rápido de transformar.
La memoria y la velocidad no son problemas separados. Son el mismo problema.
Error nº 3: abusar de los pandas para todo
Este es un poco diferente a los dos anteriores. No se trata de una función específica o de un mal hábito. Se trata de conocer los límites de tu herramienta.
Pandas es realmente genial. Para la mayoría de las tareas de datos, especialmente en la escala en la que trabaja la mayoría de las personas, es más que suficiente. Pero hay una versión del uso de Pandas que seguí viendo descrita mientras investigaba esto: las personas recurren a Pandas de forma predeterminada, para todo, independientemente de si es la opción adecuada.
Y a cierta escala, eso se convierte en un problema.
El conjunto de datos
Para que esto sea real, generé un conjunto de datos sintéticos de comercio electrónico con 1 millón de filas. Nada exótico, sólo el tipo de datos que realmente encontrarías: pedidos, fechas, regiones, categorías, cifras de ventas, descuentos, cantidades y estados.
importar pandas como pd importar numpy como np np.random.seed(42) n = 1_000_000 regiones = ['norte', 'sur', 'este', 'oeste'] categorías = ['electrónica', 'ropa', 'muebles', 'comida', 'deportes'] estados = ['completado', 'devuelto', 'pendiente', 'cancelado'] df = pd.DataFrame({ 'order_id': np.arange(1000, 1000 + n), 'order_date': pd.date_range(start='2022-01-01', period=n, freq='1min'), 'región': np.random.choice(regions, size=n), 'categoría': np.random.choice(categorías, tamaño=n), 'ventas': np.random.randint(100, 10000, tamaño=n), 'cantidad': np.random.randint(1, 20, tamaño=n), 'descuento': np.round(np.random.uniform(0.0, 0.5, tamaño=n), 2), 'estado': np.random.choice(estados, tamaño=n), }) df.to_csv('large_sales_data.csv', index=False)
Un millón de filas. Guardado en un CSV. Este es el conjunto de datos con el que trabajaremos durante el resto del artículo.
Donde los pandas comienzan a luchar
Pandas carga todo su conjunto de datos en la memoria. Eso está bien cuando sus datos tienen unos cientos de miles de filas. Empieza a resultar incómodo con unos pocos millones. Y más allá de eso, estás luchando contra la herramienta.
El otro escenario son transformaciones complejas y anidadas en las que se acumulan múltiples operaciones, se crean resultados intermedios y, en general, se le pide a Pandas que haga mucho trabajo pesado en secuencia. Cada paso agrega gastos generales. Los costos se acumulan.
A continuación se muestra un ejemplo realista utilizando nuestro conjunto de datos. Supongamos que necesita calcular un promedio móvil de ventas por región, marcar los pedidos por encima de un umbral y luego agregarlos por mes:
# Paso 1: Ordenar df = df.sort_values(['region', 'order_date']) # Paso 2: Promedio móvil por región df['rolling_avg'] = ( df.groupby('region')['sales'] .transform(lambda x: x.rolling(window=7).mean()) ) # Paso 3: Marcar pedidos de alto valor df['high_value'] = df['sales'] > df['rolling_avg'] * 1.5 # Paso 4: Agregación mensual df['month'] = pd.to_datetime(df['order_date']).dt.to_period('M') mensual_summary = df.groupby(['region', 'month'])['sales'].sum()
Esto funciona. Pero observe que el Paso 2 usa .transform(lambda x: …), que conlleva el mismo costo de fila adyacente del que hablamos anteriormente. En 1 millón de filas, este canal se arrastrará. Continúe y mida el tiempo en su máquina y verá exactamente lo que quiero decir.
Qué buscar en su lugar
La buena noticia es que no es necesario que abandones Pandas por completo. Hay algunas opciones dependiendo de la situación.
fragmentación
Si su conjunto de datos es demasiado grande para cargarlo todo a la vez, Pandas le permite procesarlo en fragmentos. En lugar de cargar el millón de filas en la memoria a la vez, carga y procesa una parte a la vez:
tamaño_fragmento = 100_000 resultados =[]para fragmento en pd.read_csv('large_sales_data.csv', chunksize=chunk_size): fragmento['precio_descuento'] = fragmento['ventas'] * (1 – fragmento['descuento']) resultados.append(chunk.groupby('región')['precio_descuento'].sum()) final_result = pd.concat(resultados).groupby(nivel=0).sum() imprimir(resultado_final)
En lugar de pedirle a Pandas que mantenga 1 millón de filas en la memoria simultáneamente, le estás alimentando 100.000 filas a la vez, procesando cada fragmento y reuniendo los resultados al final. No es el patrón más elegante, pero le permite trabajar con datos que de otro modo bloquearían su núcleo.
Cuándo considerar otras herramientas
A veces la respuesta honesta es que Pandas no es la herramienta adecuada para el trabajo. Esto no es una crítica, es sólo alcance. Algunos que vale la pena conocer:
Polars: una biblioteca de marcos de datos moderna construida en Rust, diseñada para la velocidad. Utiliza una evaluación diferida, lo que significa que optimiza toda la consulta antes de ejecutarla. Para conjuntos de datos grandes, puede ser mucho más rápido que Pandas. Dask: amplía Pandas para que funcione en paralelo en varios núcleos o incluso en varias máquinas. Si se siente cómodo con la sintaxis de Pandas, Dask le resultará familiar. DuckDB: le permite ejecutar consultas SQL directamente en sus marcos de datos o archivos CSV con un rendimiento sorprendentemente rápido. Excelente para agregaciones y consultas analíticas sobre grandes datos.
La cuestión no es abandonar a los pandas. Para la mayoría del trabajo de datos cotidiano, es la elección correcta. La cuestión es reconocer cuándo se ha tocado techo y saber que hay buenas opciones al otro lado.
El refactor del mundo real: de 61 segundos a 0,33 segundos
Aquí es donde todo lo que hemos cubierto deja de ser teórico.
Tomé nuestro conjunto de datos de comercio electrónico de 1 millón de filas y escribí el tipo de código Pandas que parece completamente normal. El tipo de cosas que escribirías un martes por la tarde sin pensarlo dos veces.
Luego lo cronometré.
La versión lenta
import time df = pd.read_csv('large_sales_data.csv') start = time.time() # Cálculo de ingresos por filas df['gross_revenue'] = df.apply( lambda fila: fila['sales'] * fila['cantidad'], eje=1 ) df['tax'] = df.apply( lambda fila: fila['gross_revenue'] * 0.075, eje=1 ) df['net_revenue'] = df.apply( lambda fila: fila['gross_revenue'] – fila['tax'], eje=1 ) # Marcado por filas df['order_flag'] = df.apply( lambda fila: 'alto' if fila['net_revenue'] > 50000 else 'bajo', axis=1 ) # Resultado de agregación final = df.groupby('region')['net_revenue'].sum() end = time.time() print(f"Tiempo de ejecución total: {end – start:.2f} segundos")
Producción:
Tiempo de ejecución total: 61,78 segundos
Más de un minuto. Para un oleoducto de cuatro pasos. Y nada parece mal. Analicemos exactamente qué lo hace lento.
Tres errores, todos en un mismo canal:
En primer lugar, nunca se abordan los tipos de datos. Las columnas de región, categoría y estado se cargan como tipos de objetos genéricos, que consumen mucha memoria y son lentos para trabajar. Llevamos ese peso muerto en cada operación. En segundo lugar, hay tres llamadas .apply(axis=1) independientes solo para calcular los ingresos. Cada uno recorre el millón de filas en Python, una a la vez. Ya vimos en la Sección 4 lo caro que es. Aquí lo hacemos tres veces seguidas. En tercer lugar, los ingresos_brutos y los impuestos se crean como columnas permanentes aunque sean solo pasos intermedios. No tienen ningún propósito más allá de ser trampolines hacia los ingresos netos, pero de todos modos permanecen en la memoria para el resto del proceso.
Así es como solucionaría este problema paso a paso.
Paso 1: corrija los tipos de datos por adelantado
Antes que nada, convierta las columnas categóricas obvias:
df['región'] = df['región'].astype('categoría') df['categoría'] = df['categoría'].astype('categoría') df['estado'] = df['estado'].astype('categoría')
Esto por sí solo reduce significativamente el uso de memoria y hace que las operaciones posteriores sean más baratas en todos los ámbitos.
Paso 2: Reemplazar .apply() con operaciones vectorizadas
En lugar de tres llamadas separadas por filas, una expresión vectorizada hace el mismo trabajo:
# Antes: tres llamadas a .apply(), tres pasadas por 1 millón de filas df['gross_revenue'] = df.apply(lambda fila: fila['sales'] * fila['cantidad'], eje=1) df['tax'] = df.apply(lambda fila: fila['gross_revenue'] * 0.075, eje=1) df['net_revenue'] = df.apply(lambda fila: fila['gross_revenue'] – fila['tax'], eje=1) # Después: una expresión vectorizada, sin columnas temporales df['net_revenue'] = df['sales'] * df['cantidad'] * (1 – 0,075)
Paso 3: Reemplace el marcado por filas con np.where()
# Antes de df['order_flag'] = df.apply( lambda fila: 'alta' si fila['net_revenue'] > 50000 else 'bajo', eje=1 ) # Después de df['order_flag'] = np.where(df['net_revenue'] > 50000, 'alta', 'baja')
La misma lógica. Vectorizado. Hecho.
La versión rápida
Póngalo todo junto y la canalización se verá así:
import time df = pd.read_csv('large_sales_data.csv') start = time.time() # Solución 1: corregir los tipos de datos por adelantado df['region'] = df['region'].astype('category') df['category'] = df['category'].astype('category') df['status'] = df['status'].astype('category') # Solución 2: cálculo de ingresos vectorizados, sin columnas temporales df['net_revenue'] = df['sales'] * df['quantity'] * (1 – 0.075) # Solución 3: marcado vectorizado con np.where df['order_flag'] = np.where(df['net_revenue'] > 50000, 'alto', 'bajo') # Resultado de agregación final = df.groupby('región')['net_revenue'].sum() end = time.time() print(f"Tiempo de ejecución total: {fin – inicio:.2f} segundos")
Producción:
Tiempo de ejecución total: 0,33 segundos
61,78 segundos hasta 0,33 segundos. Una reducción del 99,5% en el tiempo de ejecución. Eso es como 187 veces más rápido.
No es un truco. Así es como se supone que se debe usar Pandas.
Antes de ejecutar su próxima computadora portátil
Todo lo que cubrimos se reduce a algunos hábitos básicos. No reglas. No trucos. Simplemente una forma diferente de pensar en tu código antes de escribirlo.
Piense en columnas, no en filas. Si está recorriendo un marco de datos fila por fila, deténgase y pregunte si lo mismo se puede expresar como una operación de columna. Nueve de cada diez veces puede hacerlo. Mida antes de optimizar. No adivinéis de dónde viene la lentitud. Utilice %timeit y df.memory_usage() para dejar que los números le digan qué arreglar. Cuida tu memoria, no solo tu velocidad. Se suman tipos de datos incorrectos, copias innecesarias y columnas temporales. Un marco de datos más ligero es un marco de datos más rápido. Sepa cuándo cambiar de herramienta. Pandas es la elección correcta la mayor parte del tiempo. Pero a cierta escala, la optimización correcta es reconocer que ya la has superado.
Comencé esta madriguera porque seguía viendo surgir la misma conversación en las comunidades de datos. La gente estaba frustrada por las computadoras portátiles lentas, el código que funcionaba bien con datos pequeños y se desmoronaba con datos reales. Quería entender por qué.
Lo que encontré fue que el código no estaba roto. Simplemente no fue construido a escala. Y la brecha entre el código que funciona y el código que funciona eficientemente no tiene que ver con ser un usuario avanzado de Pandas. Se trata de un puñado de hábitos aplicados de forma constante.
Si alguna vez esperó demasiado a que terminara un cuaderno y asumió que era normal, ahora sabe que no tiene por qué ser así.
Si esto cambió su forma de pensar sobre su código Pandas, me encantaría saber con qué cuellos de botella ha estado lidiando. No dudes en saludar en cualquiera de estas plataformas.
Medio
Gorjeo
YouTube