Reescribí un flujo de trabajo de datos reales en Polars. Los pandas no tuvieron ninguna posibilidad.

— No estaba buscando activamente polares.

Últimamente he estado en un viaje de optimización de Pandas. Primero, escribí sobre por qué deberías dejar de escribir bucles en Pandas y pensar en columnas.

Luego profundicé en la creación de perfiles de flujos de trabajo reales, corrigí errores de vectorización y terminé reduciendo una canalización de 61 segundos a 0,33 segundos usando nada más que mejores Pandas y NumPy. Eso me sorprendió incluso a mí.

Así que estaba en un buen lugar con los pandas. Sentí que finalmente entendí cómo usarlo correctamente.

Entonces alguien dejó un comentario en una de mis publicaciones. Algo como: "¿Has probado Polars? Está diseñado exactamente para este tipo de cosas".

Había visto el nombre flotando en la comunidad de datos. Hubo rumores en torno a esto: algo sobre la velocidad, sobre una forma completamente diferente de pensar sobre los canales de datos. Pero en realidad nunca lo había tocado.

Ese comentario fue suficiente para llevarme al límite.

Entonces hice lo que siempre hago. Sentí curiosidad, lo instalé y reescribí exactamente el mismo flujo de trabajo de mi último artículo, el que ya había optimizado en Pandas, en una herramienta que nunca había usado antes.

Lo que encontré me sorprendió. No solo las cifras de velocidad, sino lo que Polars te enseña silenciosamente sobre cómo funcionan realmente las canalizaciones de datos.

¿No son suficientes los pandas?

Pregunta justa.

En mi último artículo, tomé una canalización lenta de Pandas y la optimicé hasta 0,33 segundos. Operaciones vectorizadas, tipos de datos correctos, sin copias innecesarias. Honestamente, los resultados fueron mejores de lo que esperaba.

Entonces, ¿por qué hablamos siquiera de los polares?

Aquí está la cosa. Todo lo que hice en ese artículo fue realizar la optimización manualmente. Tenía que saber qué operaciones eran lentas, por qué lo eran y cómo solucionarlas. Polars piensa mucho de eso automáticamente, incluso antes de ejecutar su código.

Además de eso, Polars se basa en una base completamente diferente a la de Pandas. Utiliza todos los núcleos de su CPU de forma predeterminada. Gestiona la memoria de forma diferente. E introduce una forma de escribir canalizaciones de datos que, una vez que hace clic, cambia la forma de pensar sobre todo el proceso.

Pandas optimizado es impresionante. Pero todavía tiene un techo. Este artículo trata sobre lo que hay al otro lado.

El flujo de trabajo

Para mantener la coherencia y la utilidad para cualquiera que haya seguido mi último artículo. Estoy usando el mismo conjunto de datos sintéticos de comercio electrónico. Un millón de filas. Nada exótico. Justo el tipo de datos que realmente encontrarías en la naturaleza.

Si desea generarlo usted mismo, aquí está el código de configuración:

importar pandas como pd importar numpy como np tiempo de importación 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)

El proceso que estamos ejecutando es sencillo. El tipo de cosas que aparecen todo el tiempo en los flujos de trabajo reales:

Corregir tipos de datos por adelantado Calcular ingresos netos por pedido Marcar pedidos de alto valor Agregar ingresos netos totales por región

Simple. Familiar. Y ya optimizado en Pandas desde el último artículo, lo que lo convierte en el punto de referencia perfecto para Polars.

La versión pandas

No voy a mostrarles el código ingenuo de Pandas aquí. Ya lo hice en mi último artículo: la versión con tres llamadas .apply() que tardaron 61 segundos en este mismo conjunto de datos. Si no lo has leído, vale la pena echarle un vistazo.

Lo que estoy mostrando aquí es la versión optimizada. Lo mejor que los pandas pueden hacer en este oleoducto.

importar pandas como pd importar numpy como np importar tiempo df = pd.read_csv('large_sales_data.csv') start = time.time() # Corregir tipos de datos por adelantado df['region'] = df['region'].astype('category') df['category'] = df['category'].astype('category') df['status'] = df['status'].astype('category') # Cálculo de ingresos vectorizados df['net_revenue'] = df['sales'] * df['quantity'] * (1 – 0.075) # Marcado vectorizado df['order_flag'] = np.where(df['net_revenue'] > 50000, 'high', 'low') # Resultado de agregación = df.groupby('región')['net_revenue'].sum() end = time.time() print(f"Tiempo de ejecución de Pandas: {fin – inicio:.2f} segundos") print(resultado)

Estos son pandas limpios. Operaciones vectorizadas, tipos de datos correctos, sin columnas intermedias innecesarias. Todo lo que aprendí al bajar por esa madriguera del conejo.

¿Y el resultado?

Tiempo de ejecución de Pandas: 0,31 segundos

Eso ya es muy bueno. Realmente impresionante por un millón de filas.
Pero aquí está la pregunta con la que seguí sentado después de ver ese número: ¿cómo se ve cuando la herramienta realiza la optimización por usted, en lugar de que usted lo haga manualmente?

Eso es lo que estamos a punto de descubrir.

Instalación de polares y primeras impresiones

Comenzar fue sencillo. Si estás en Google Colab como yo, una línea es todo lo que necesitas:

!pip instalar polares importar polares como pl print(pl.__version__) 1.35.2

Hecho. Sin dolores de cabeza ambientales, sin conflictos de dependencia. Sólo eso fue un buen comienzo.

Pero luego abrí la documentación de Polars e inmediatamente noté algo. La sintaxis parecía bastante familiar (DataFrames, columnas, filtrado), pero la forma en que se supone que debes pensar sobre las operaciones parecía diferente.

En Pandas trabajas con tus datos paso a paso. Haces algo, almacenas el resultado, haces otra cosa. En Polars, usted describe lo que desea como una sola expresión y Polars descubre cómo ejecutarlo.

No lo entendí del todo todavía. Pero estaba a punto de hacerlo.

La otra cosa que me llamó la atención de inmediato fue este concepto de ejecución perezosa versus ejecución entusiasta. En Pandas, cada línea de código se ejecuta en el momento en que la escribes. Polars te da una opción: puedes ejecutar con entusiasmo como Pandas, o puedes crear primero un plan de consulta completo y dejar que Polars lo optimice antes de ejecutar cualquier cosa.

Todavía no sabía lo que eso significaba en la práctica. Pero seguí viéndolo en todas partes en los documentos. Así que decidí que la mejor manera de entenderlo era simplemente reescribir mi canal y ver qué pasaba.

La versión ansiosa

importar polares como pl importar hora inicio = time.time() resultado = ( pl.read_csv('large_sales_data.csv') .with_columns([ (pl.col('sales') * pl.col('cantidad') * (1 – 0.075)).alias('net_revenue') ]) .with_columns([ pl.when(pl.col('net_revenue') > 50000) .then(pl.lit('high')) .otherwise(pl.lit('low')) .alias('order_flag') ]) .group_by('region') .agg(pl.col('net_revenue').sum()) ) end = time.time() print(f"Tiempo de ejecución de Polars Eager: {end – start:.2f} segundos") print(resultado) Polars Tiempo de ejecución ansioso: 0,83 segundos

Lo primero que noté fue el método de encadenamiento. En Pandas, hacía tareas separadas, hacía una cosa, almacenaba el resultado y hacía la siguiente.

Aquí todo fluye como una única expresión de principio a fin. Estás describiendo todo el proceso a la vez.

Permítanme desglosar la sintaxis rápidamente ya que parte de ella le resultará desconocida:

pl.read_csv(): lee el CSV, el mismo concepto que pd.read_csv(). Nada sorprendente. .with_columns([…]) — así es como Polars agrega o transforma columnas. El equivalente de df['new_col'] = … en Pandas. Puede calcular varias columnas en una sola llamada. pl.col('sales'): así es como se hace referencia a una columna en Polars. En lugar de df['sales'], escribe pl.col('sales'). No estás recopilando los datos directamente, sino que estás describiendo una operación en esa columna. Esa distinción importa más de lo que parece. .alias('net_revenue'): simplemente nombra el resultado. Como decir "llame a esta nueva columna ingresos_netos". pl.when(…).then(…).otherwise(…) — Versión polar de np.where(). Se lee casi como en inglés: cuando esta condición es verdadera, devuelve este valor; de lo contrario, devuelve ese. .group_by('region').agg(…) — el mismo concepto que Pandas .groupby(). Agrupe los datos y luego defina su agregación. Diferente sintaxis, misma idea.

Ahora aquí está la cuestión. Esa versión entusiasta se ejecutó en 0,83 segundos. En realidad, eso es más lento que nuestros Pandas optimizados con 0,31 segundos. Si me hubiera detenido aquí, habría descartado por completo a Polars.

Pero seguí leyendo los documentos. Y encontré algo llamado evaluación perezosa.

La versión perezosa

inicio = time.time() resultado = ( pl.scan_csv('large_sales_data.csv') .with_columns([ (pl.col('ventas') * pl.col('cantidad') * (1 – 0.075)).alias('ingresos_netos') ]) .with_columns([ pl.when(pl.col('ingresos_netos') > 50000) .then(pl.lit('high')) .otherwise(pl.lit('low')) .alias('order_flag') ]) .group_by('region') .agg(pl.col('net_revenue').sum()) .collect() ) end = time.time() print(f"Polars Lazy runtime: {end – start:.2f} segundos") imprimir (resultado) Polars Tiempo de ejecución diferido: 0,20 segundos

Encuentra las dos diferencias con la versión entusiasta. pl.read_csv() se convirtió en pl.scan_csv(): esto le dice a Polars que no cargue nada todavía, simplemente comience a crear un plan de consulta. Y se agregó .collect() al final; ahí es donde le dices a Polars "está bien, ahora ejecuta todo".

Dos cambios. Eso es todo.

Y así, 0,83 segundos se convirtieron en 0,20 segundos.

Polars lazy es un 35% más rápido que nuestro canal Pandas ya optimizado. Sin que yo haga ninguna optimización manual. Sin que yo perfile nada. Sin que yo supiera de antemano qué operaciones eran cuellos de botella.

Los polares lo descubrieron por sí solos.

Fue entonces cuando comencé a prestar atención.

Cambio de modelo mental n.º 1: ejecución perezosa o ansiosa

Este es el que cambió mi forma de pensar sobre las canalizaciones de datos.
En Pandas, cada línea de código se ejecuta en el momento en que la escribes. Usted asigna una columna y se ejecuta. Filtra un DataFrame y se ejecuta. Usted agrupa y agrega: se ejecuta.

Cada operación es independiente, inmediata y ajena a lo que viene antes o después.

Esa es una ejecución entusiasta. Es intuitivo. Se siente natural porque coincide con nuestra forma de pensar acerca de escribir código paso a paso.

Polars te da una opción.

Cuando usas pl.scan_csv() en lugar de pl.read_csv(), le estás diciendo a Polars: no ejecutes nada todavía. Simplemente comience a construir un plan. Cada .with_columns(), cada .filter(), cada .group_by() que encadenes después no se está ejecutando, se está registrando. Estás describiendo lo que quieres, no provocándolo.

Luego, cuando llamas a .collect() al final, Polars toma todo el plan, lo analiza como un todo, lo optimiza y luego lo ejecuta en una sola pasada eficiente.

Piénselo de esta manera:

Pandas es como seguir una receta paso a paso. Picar las cebollas. Ponlos en la sartén. Agrega el ajo. Cada instrucción ocurre inmediatamente, en orden, una a la vez. Polars Lazy es como un chef que primero lee la receta completa, descubre la forma más eficiente de preparar todo y luego la ejecuta en el orden óptimo. Mismo resultado. Menos esfuerzo desperdiciado.

Ese paso de optimización es la clave. Antes de que Polars ejecute una sola línea de su canalización, pregunta: ¿qué columnas necesitamos realmente? ¿Qué filas podemos eliminar anticipadamente? ¿Qué operaciones se pueden paralelizar? ¿Qué trabajo se puede omitir por completo?

De hecho, puede ver el plan de consulta que crea Polars antes de ejecutarlo.

Ejecute esto:

lazy_query = ( pl.scan_csv('large_sales_data.csv') .with_columns([ (pl.col('ventas') * pl.col('cantidad') * (1 – 0.075)).alias('ingresos_netos') ]) .with_columns([ pl.when(pl.col('ingresos_netos') > 50000) .then(pl.lit('high')) .otherwise(pl.lit('low')) .alias('order_flag') ]) .group_by('region') .agg(pl.col('net_revenue').sum()) ) print(lazy_query.explain())

Esa llamada .explain() le muestra exactamente lo que Polars planea hacer antes de hacerlo. Es el pensamiento del optimizador hecho visible.
Esto es algo que Pandas simplemente no tiene.

En Pandas, la optimización es tu responsabilidad. Tienes que saber qué operaciones son costosas, perfilar tu código y reestructurarlo manualmente, que es exactamente lo que hice en mi último artículo. En el modo diferido de Polars, el optimizador se encarga de eso por usted.

Esa no es una pequeña diferencia. Esa es una relación fundamentalmente diferente entre usted y su canal de datos.

Cambio de modelo mental n.º 2: optimización de consultas

Una vez que entendí la evaluación perezosa, comencé a preguntarme: está bien, pero ¿qué está optimizando exactamente Polars? ¿Qué hace realmente diferente bajo el capó?

Resulta que hay dos importantes que vale la pena conocer. Y una vez que los comprende, comienza a ver por qué Polars es más rápido no solo en este proceso, sino en casi cualquier flujo de trabajo no trivial.

Empuje de predicado

Supongamos que agrega un filtro a nuestra canalización; solo desea pedidos completados:

resultado = ( pl.scan_csv('large_sales_data.csv') .with_columns([ (pl.col('sales') * pl.col('cantidad') * (1 – 0.075)).alias('net_revenue') ]) .filter(pl.col('status') == 'completado') .group_by('región') .agg(pl.col('ingresos_netos').sum()) .collect() )

En el modo ansioso (Pandas o Polars ansiosos), esto es lo que sucede: cargar el millón de filas, calcular los ingresos netos para todas ellas y luego filtrar hasta los pedidos completados. Hiciste un trabajo costoso en hileras que de todos modos ibas a desechar.

Polars Lazy hace algo más inteligente. Mira todo el plan de consulta y dice: aquí hay un filtro. Permítanme aplicar ese filtro lo antes posible, idealmente antes de cargar datos o inmediatamente después. De esa manera solo hago los costosos cálculos en las filas que realmente importan.

Eso es presión predicada. Empuje el filtro hacia abajo hasta el punto más temprano posible de la tubería. Menos datos procesados. Menos memoria utilizada. Resultado más rápido.

Poda de proyección

La misma idea, pero para columnas en lugar de filas.

Nuestro CSV tiene ocho columnas. Pero nuestro resultado final sólo necesita ventas, cantidad, región y estado. Polars analiza el plan de consulta, determina qué columnas son realmente necesarias para producir el resultado final y solo las carga. El resto se ignora por completo.

En Pandas, primero cargas todo y descubres lo que necesitas después. Polars descubre lo que necesita antes de cargar algo.
Estas dos optimizaciones (empuje de predicados y poda de proyección) son lo que hace un optimizador de consultas de base de datos. Si alguna vez escribió SQL y se preguntó por qué la base de datos es rápida incluso en tablas masivas, esta es una gran parte del motivo.

Y ese es el cambio mental aquí. Cuando escribes un proceso diferido de Polars, en realidad ya no estás escribiendo un guión. Estás escribiendo una consulta. Estás describiendo lo que quieres y Polars, como un motor de base de datos, descubre la forma más eficiente de conseguirlo.

Esa es una forma diferente de pensar. Y una vez que hace clic, comienza a abordar los canales de datos de manera diferente. En lugar de preguntar “¿qué debo hacer a continuación?”, empiezas a preguntar “¿qué necesito realmente al final?” y trabajando hacia atrás desde allí.

Cambio de modelo mental n.º 3: memoria columnar

Hay una pieza más que explica por qué Polars es rápido. Y vive en un nivel por debajo de su código.

Pandas almacena datos en un formato orientado a filas bajo el capó. Imagine una hoja de cálculo donde cada fila se almacena junta en la memoria: todos los valores del orden 1, luego todos los valores del orden 2, y así sucesivamente. Eso funciona bien para buscar registros individuales.

Pero cuando desea calcular algo en una columna completa, como sumar todos los valores de ingresos, Pandas tiene que recorrer la memoria y seleccionar el valor relevante de cada fila, uno a la vez.

Polars utiliza un formato de memoria en columnas llamado Apache Arrow. En lugar de almacenar filas juntas, almacena columnas juntas. Todos los valores de ingresos se encuentran uno al lado del otro en la memoria. Todos los valores de la región se encuentran uno al lado del otro. Cuando necesitas calcular algo en una columna, todo lo que necesitas ya está en un bloque contiguo de memoria.

¿Por qué importa eso? Dos razones.

En primer lugar, las CPU modernas están diseñadas para procesar bloques de memoria contiguos de manera extremadamente eficiente. Cuando sus datos están dispuestos en columnas, la CPU puede dividirlos mediante instrucciones vectorizadas, procesando múltiples valores en una sola operación. El almacenamiento orientado a filas rompe esa optimización.

En segundo lugar, el almacenamiento en columnas significa que Polars solo toca las columnas que necesita. Si su DataFrame tiene 20 columnas pero su operación solo necesita 3, Polars funciona con esas 3 columnas en la memoria. Pandas carga todo independientemente.

No tienes que hacer nada para obtener este beneficio. Así es como se construye Polars.

Piénselo de esta manera. Pandas es como un archivador donde cada cajón contiene todo lo relacionado con un cliente: su nombre, sus pedidos, su historial de pagos, todo junto.

Polars es como un archivador donde un cajón tiene todos los nombres, otro tiene todos los pedidos, otro tiene todos los historiales de pagos. Si necesita analizar los historiales de pagos de todos los clientes, Polars abre un cajón. Pandas abre todos y cada uno de ellos.

Ese es el modelo de memoria columnar. Y combinado con la evaluación lenta y la optimización de consultas, es la razón por la que Polars puede hacer en 0,20 segundos lo que a Pandas le toma 0,31 segundos, incluso después de que Pandas haya sido cuidadosamente optimizado a mano.

Donde los pandas siguen ganando

Quiero ser sincero contigo aquí. Este artículo no es un obituario de Pandas.

Después de realizar todo este ejercicio, todavía hay situaciones en las que alcanzaría Pandas sin dudarlo.

Para una exploración rápida y un análisis ad hoc, Pandas es difícil de superar. La sintaxis es familiar, el ecosistema es enorme y cuando simplemente estás husmeando en un conjunto de datos tratando de entenderlo, la diferencia de rendimiento no importa. No estás ejecutando un oleoducto, estás pensando en voz alta.

Para conjuntos de datos pequeños, las ventajas de las polares desaparecen en gran medida. La sobrecarga de crear un plan de consultas solo vale la pena cuando hay suficientes datos para que valga la pena la optimización. En unos pocos miles de filas, simplemente use Pandas.

Para visualización e integraciones, Pandas está profundamente integrado en el ecosistema de datos de Python. Matplotlib, Seaborn, Scikit-learn, Statsmodels: todos hablan Pandas de forma nativa. Los polares se están poniendo al día, pero los pandas siguen siendo el lenguaje común.

Y, sinceramente, para los equipos y colaboradores, la familiaridad es importante. Si todos en tu equipo conocen Pandas y nadie conoce Polars, presentarlo tiene un costo real.

No voy a reemplazar a los Pandas. Me estoy volviendo más intencional cuando lo uso.

Pequeño conjunto de datos, exploración rápida, visualización, familiaridad con el equipo: Pandas. Gran conjunto de datos, canalización repetida, flujo de trabajo de producción, el rendimiento importa: Polars.

Ambas herramientas. Situaciones correctas.

Conclusión

Esto es lo que esperaba: una comparación de velocidades. Ejecute el mismo código en dos bibliotecas, muestre los números y listo.

Esto es lo que realmente obtuve: una forma diferente de pensar sobre los datos.

Las cifras de velocidad son reales: pasamos de una tubería de Pandas rota de 61 segundos en mi último artículo a 0,31 segundos con Pandas optimizados y a 0,20 segundos con una evaluación diferida de Polars. Ese es un viaje que vale la pena documentar.

Pero lo más duradero es lo que Polars te enseña silenciosamente a lo largo del camino. Que los filtros deben aplicarse lo antes posible. Que sólo debes cargar lo que necesitas. Describir lo que desea y dejar que un optimizador descubra cómo es una forma legítima y poderosa de escribir canalizaciones de datos.

Esas no son sólo ideas de Polars. Son buenas ideas de ingeniería de datos. Y no me habría sentado a entenderlos realmente si un comentario en una de mis publicaciones no me hubiera empujado hacia una herramienta que nunca antes había usado.

Los pandas no empeoraron por nada de esto. Mis expectativas simplemente se hicieron mayores.

Si tiene un flujo de trabajo que parece más lento de lo que debería, incluso después de haber limpiado el código, podría valer la pena preguntarse si la herramienta en sí tiene un techo. A veces, el siguiente paso no es escribir un código mejor. Está escribiendo el mismo código en algo construido de manera diferente.

¿Qué flujos de trabajo está ejecutando y que valdría la pena reescribir?