Polares está arrasando en el mundo gracias a su velocidad, eficiencia de memoria y su hermosa API. Si quieres saber qué tan poderoso es, no busques más que el Puntos de referencia de DuckDB. Y estos ni siquiera utilizan la versión más reciente de Polars.
Sin embargo, a pesar de todas las cosas increíbles que Polars puede hacer, tradicionalmente no ha sido una mejor solución que Pandas para hacer TODOS los cálculos que desee hacer. Hay algunas excepciones en las que Polars no ha obtenido mejores resultados. Sin embargo, con el reciente lanzamiento del sistema de complementos Polars para Rust, es posible que ese ya no sea el caso.
¿Qué es exactamente un complemento polares? Es simplemente una forma de crear sus propias expresiones Polars utilizando Rust nativo y exponiéndolas a expresiones utilizando un espacio de nombres personalizado. Le permite tomar la velocidad de Rust y aplicarla a su Polars DataFrame para realizar cálculos de una manera que aproveche la velocidad y las herramientas integradas que proporciona Polars.
Echemos un vistazo a algunos ejemplos concretos.
Cálculos secuenciales
Un área en la que Polars parece carecer de funcionalidad son las operaciones que requieren un conocimiento del valor anterior de un DataFrame. Los cálculos que son de naturaleza secuencial no siempre son muy fáciles o eficientes de escribir en expresiones polares nativas. Echemos un vistazo a un ejemplo específico.
Tenemos el siguiente algoritmo para calcular el valor acumulado de una matriz de números para una ejecución determinada, definida como un conjunto de números que tienen el mismo signo. Por ejemplo:
┌───────┬───────────┐
│ value ┆ run_value │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═══════╪═══════════╡
│ 1 ┆ 1 │ # First run starts here
│ 2 ┆ 3 │
│ 3 ┆ 6 │
│ -1 ┆ -1 │ # Run resets here
│ -2 ┆ -3 │
│ 1 ┆ 1 │ # Run resets here
└───────┴───────────┘
Entonces queremos tener una suma acumulativa de una columna que se reinicia cada vez que el signo del valor cambia de positivo a negativo o de negativo a positivo.
Comencemos con una versión básica escrita en pandas.
def calculate_runs_pd(s: pd.Series) -> pd.Series:
out = []
is_positive = True
current_value = 0.0
for value in s:
if value > 0:
if is_positive:
current_value += value
else:
current_value = value
is_positive = True
else:
if is_positive:
current_value = value
is_positive = False
else:
current_value += value
out.append(current_value)
return pd.Series(out)
Iteramos sobre una serie, calculamos el valor actual de la ejecución en cada posición y devolvemos una nueva Serie Pandas.
Evaluación comparativa
Antes de continuar, estableceremos algunos puntos de referencia. Vamos a medir tanto la velocidad de ejecución como el consumo de memoria usando punto de referencia de pytest y memoria pytest. Configuraremos el problema de manera que tengamos una columna de entidad, una columna de tiempo y una columna de característica. El objetivo es calcular los valores de ejecución para cada entidad en los datos a lo largo del tiempo. Estableceremos el número de entidades y marcas de tiempo cada una en 1000, lo que nos dará un DataFrame con 1.000.000 de filas.
Cuando ejecutamos nuestra implementación de Pandas en comparación con nuestro punto de referencia utilizando la funcionalidad de aplicación groupby de Pandas, obtenemos los siguientes resultados: