Saltar a contenido

Pandas vs Polars

Flights Dataset

Enlace descarga

Polar Expressions

Enlace de Descarga

pip install polars
import pandas as pd
import polars as pl
import time

Pandas test

start = time.time()
df = pd.read_csv('flights.csv', dtype={"TAIL_NUMBER": "string", "ORIGIN_AIRPORT": "string", "DESTINATION_AIRPORT": "string"})
df = df[(df['MONTH'] == 12) & (df['ORIGIN_AIRPORT'] == 'SEA') & (df['DESTINATION_AIRPORT'] == 'DFW')]
end = time.time()
print(end - start)
df
6.601531744003296
YEAR MONTH DAY DAY_OF_WEEK AIRLINE FLIGHT_NUMBER TAIL_NUMBER ORIGIN_AIRPORT DESTINATION_AIRPORT SCHEDULED_DEPARTURE ... ARRIVAL_TIME ARRIVAL_DELAY DIVERTED CANCELLED CANCELLATION_REASON AIR_SYSTEM_DELAY SECURITY_DELAY AIRLINE_DELAY LATE_AIRCRAFT_DELAY WEATHER_DELAY
5339849 2015 12 1 2 AA 1230 N3KSAA SEA DFW 5 ... 538.0 -17.0 0 0 NaN NaN NaN NaN NaN NaN
5339922 2015 12 1 2 AA 1228 N3AMAA SEA DFW 500 ... 1056.0 2.0 0 0 NaN NaN NaN NaN NaN NaN
5341203 2015 12 1 2 AS 658 N302AS SEA DFW 640 ... 1210.0 -10.0 0 0 NaN NaN NaN NaN NaN NaN
5341275 2015 12 1 2 AA 1308 N3BDAA SEA DFW 645 ... 1234.0 -4.0 0 0 NaN NaN NaN NaN NaN NaN
5343046 2015 12 1 2 AA 143 N3FCAA SEA DFW 830 ... 1425.0 2.0 0 0 NaN NaN NaN NaN NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
5810434 2015 12 31 4 AA 1489 N3HTAA SEA DFW 1015 ... 1633.0 28.0 0 0 NaN 6.0 0.0 22.0 0.0 0.0
5811917 2015 12 31 4 AA 1402 N3GEAA SEA DFW 1155 ... 1743.0 1.0 0 0 NaN NaN NaN NaN NaN NaN
5813075 2015 12 31 4 AS 662 N469AS SEA DFW 1310 ... 1902.0 12.0 0 0 NaN NaN NaN NaN NaN NaN
5813463 2015 12 31 4 AA 1512 N3DLAA SEA DFW 1340 ... 1923.0 -7.0 0 0 NaN NaN NaN NaN NaN NaN
5819038 2015 12 31 4 AS 660 N407AS SEA DFW 2350 ... 536.0 6.0 0 0 NaN NaN NaN NaN NaN NaN

319 rows Γ— 31 columns

Polars test

El mΓ©todo read_csv() utiliza el modo de ejecuciΓ³n inmediata lo que significa que cargarΓ‘ directamente todo el conjunto de datos antes de realizar cualquier filtrado. En este aspecto, este bloque de cΓ³digo es similar al que utiliza Pandas pero mucho mΓ‘s eficiente

start = time.time()
df = pl.read_csv('flights.csv').filter(
        (pl.col('MONTH') == 12) & (pl.col('ORIGIN_AIRPORT') == 'SEA') & (pl.col('DESTINATION_AIRPORT') == 'DFW'))
end = time.time()
print(end - start)
display(df)
0.6479246616363525
YEARMONTHDAYDAY_OF_WEEKAIRLINEFLIGHT_NUMBERTAIL_NUMBERORIGIN_AIRPORTDESTINATION_AIRPORTSCHEDULED_DEPARTUREDEPARTURE_TIMEDEPARTURE_DELAYTAXI_OUTWHEELS_OFFSCHEDULED_TIMEELAPSED_TIMEAIR_TIMEDISTANCEWHEELS_ONTAXI_INSCHEDULED_ARRIVALARRIVAL_TIMEARRIVAL_DELAYDIVERTEDCANCELLEDCANCELLATION_REASONAIR_SYSTEM_DELAYSECURITY_DELAYAIRLINE_DELAYLATE_AIRCRAFT_DELAYWEATHER_DELAY
i64i64i64i64stri64strstrstri64i64i64i64i64i64i64i64i64i64i64i64i64i64i64i64stri64i64i64i64i64
20151212"AA"1230"N3KSAA""SEA""DFW"594101923020919416605335555538-1700nullnullnullnullnullnull
20151212"AA"1228"N3AMAA""SEA""DFW"500459-116515234237198166010332310541056200nullnullnullnullnullnull
20151212"AS"658"N302AS""SEA""DFW"64064441265622020619016601206412201210-1000nullnullnullnullnullnull
20151212"AA"1308"N3BDAA""SEA""DFW"645640-524704233234197166012211312381234-400nullnullnullnullnullnull
20151212"AA"143"N3FCAA""SEA""DFW"830830025855233235200166014151014231425200nullnullnullnullnullnull
201512314"AA"1489"N3HTAA""SEA""DFW"10151037222110582302362011660161914160516332800null602200
201512314"AA"1402"N3GEAA""SEA""DFW"11551154-113120722722920716601734917421743100nullnullnullnullnullnull
201512314"AS"662"N469AS""SEA""DFW"13101308-2161324220234209166018539185019021200nullnullnullnullnullnull
201512314"AA"1512"N3DLAA""SEA""DFW"13401340015135523022320316601918519301923-700nullnullnullnullnullnull
201512314"AS"660"N407AS""SEA""DFW"23502352211322022420616605297530536600nullnullnullnullnullnull

En la siguiente versiΓ³n sustituye el mΓ©todo read_csv() por scan_csv() que sΓ­ utiliza ejecuciΓ³n diferida.
scan_csv() retrasa la ejecuciΓ³n hasta que se llama al mΓ©todo collect(), analiza todas las operaciones intermedias e intenta optimizar la operaciΓ³n

start = time.time()
df = pl.scan_csv('flights.csv').filter(
        (pl.col('MONTH') == 11) & (pl.col('ORIGIN_AIRPORT') == 'SEA') & (pl.col('DESTINATION_AIRPORT') == 'DFW')).collect()
end = time.time()
print(end - start)
display(df)
0.7065532207489014
YEARMONTHDAYDAY_OF_WEEKAIRLINEFLIGHT_NUMBERTAIL_NUMBERORIGIN_AIRPORTDESTINATION_AIRPORTSCHEDULED_DEPARTUREDEPARTURE_TIMEDEPARTURE_DELAYTAXI_OUTWHEELS_OFFSCHEDULED_TIMEELAPSED_TIMEAIR_TIMEDISTANCEWHEELS_ONTAXI_INSCHEDULED_ARRIVALARRIVAL_TIMEARRIVAL_DELAYDIVERTEDCANCELLEDCANCELLATION_REASONAIR_SYSTEM_DELAYSECURITY_DELAYAIRLINE_DELAYLATE_AIRCRAFT_DELAYWEATHER_DELAY
i64i64i64i64stri64strstrstri64i64i64i64i64i64i64i64i64i64i64i64i64i64i64i64stri64i64i64i64i64
20151117"AS"658"N307AS""SEA""DFW"640640016656220223195166012111212201223300nullnullnullnullnullnull
20151117"AA"1308"N3CSAA""SEA""DFW"645643-21565823821118916601207712431214-2900nullnullnullnullnullnull
20151117"AA"143"N3JXAA""SEA""DFW"828826-21283823720418816601346414251350-3500nullnullnullnullnullnull
20151117"AA"1489"N3LRAA""SEA""DFW"100817424541617582422041831660230151610230641600null0004160
20151117"AA"1402"N3BWAA""SEA""DFW"11521237451312502362071841660175410174818041600null005110
201511301"AA"1489"N3DCAA""SEA""DFW"10151012-3151027230227201166015481116051559-600nullnullnullnullnullnull
201511301"AA"1402"N3AXAA""SEA""DFW"1155151319814152722722820216602049121742210119900null1091890
201511301"AS"662"N487AS""SEA""DFW"13051301-4121313225223198166018311318501844-600nullnullnullnullnullnull
201511301"AA"1512"N3GDAA""SEA""DFW"13401500801815182302271951660203314193020477700null007610
201511301"AA"2310"N3NAAA""SEA""DFW"15211518-313153122422220016602051921052100-500nullnullnullnullnullnull

Formato Parquet

La verdadera potencia de LazyEval se pone de manifiesto cuanto mayores son los conjuntos de datos y, especialmente, si estΓ‘n un un formato columnar. Apache Parquet es un formato de archivo de almacenamiento en columnas optimizado para su uso en escenarios de procesamiento masivo de Big Data. A diferencia de los formatos de almacenamiento basados en filas, como CSV, Parquet almacena los datos en columnas, lo que lo hace muy eficiente para operaciones con gran volumen de lectura, especialmente cuando se trata de grandes conjuntos de datos

Este formato en columnas permite una mejor compresiΓ³n y codificaciΓ³n, ya que los tipos de datos similares se almacenan juntos. Por ejemplo, si se tiene un conjunto de datos con millones de filas y solo se necesita consultar unas pocas columnas, Parquet permite leer solo las columnas relevantes (Predicate Pushdown), lo que reduce significativamente las operaciones de E/S y mejora el rendimiento. Parquet tambiΓ©n admite estructuras de datos anidadas complejas, lo que lo hace ideal para datos semiestructurados como JSON

Ejemplo: poda del Γ‘rbol de bΓΊsqueda (Predicate Pushdown) en Polars

lf = pl.scan_parquet("events/*.parquet")
fast = (
    lf.filter(pl.col("country") == "IN")
      .filter(pl.col("ts") >= pl.datetime(2025, 9, 1))
      .select(["ts", "country", "path"])
      .collect()
)

Ejemplo con datos particionados (Polars Hive Partitioning)

NYC Taxi Dataset

El dataset ocupa 325Mb. Scan multiples ficheros con lazy evaluation
TLC Trip Record Data

# Planificar acceso
q1 = (
    pl.scan_parquet("Parquet/**/*.parquet")
    .filter(pl.col("passenger_count") > 0) # Filter
    .with_columns([
        # Derive date/time features
        (pl.col("tpep_dropoff_datetime") - pl.col("tpep_pickup_datetime"))
        .dt.total_minutes()
        .alias("trip_duration_minutes")
    ])
    .group_by("passenger_count")
    .agg([
        pl.mean("trip_duration_minutes"),
        pl.mean("total_amount").alias("avg_fare")
    ])
    .sort("passenger_count")
)

# Ejecutar consulta
r1 = q1.collect()
print(r1)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ passenger_count ┆ trip_duration_minutes ┆ avg_fare   β”‚
β”‚ ---             ┆ ---                   ┆ ---        β”‚
β”‚ i64             ┆ f64                   ┆ f64        β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ═══════════════════════β•ͺ════════════║
β”‚ 1               ┆ 15.547899             ┆ 27.008371  β”‚
β”‚ 2               ┆ 17.243722             ┆ 30.352485  β”‚
β”‚ 3               ┆ 16.934745             ┆ 29.482887  β”‚
β”‚ 4               ┆ 17.764046             ┆ 32.436805  β”‚
β”‚ 5               ┆ 20.555213             ┆ 26.760919  β”‚
β”‚ 6               ┆ 23.465055             ┆ 26.841099  β”‚
β”‚ 7               ┆ 18.3                  ┆ 83.639     β”‚
β”‚ 8               ┆ 9.911765              ┆ 102.342059 β”‚
β”‚ 9               ┆ 11.666667             ┆ 62.724444  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Particionado estructurado

Si la estructura de particionado es conocida y acorde con los patrones de bΓΊsqueda mΓ‘s frecuentes la poda mejora los tiempos de respuesta drΓ‘sticamente debido a que la bΓΊsqueda se focaliza ΓΊnicamente en las particiones conteniendo los datos

lf = pl.scan_parquet("path/YYYY=2025/MM=10/*/*.parquet")
today = lf.filter(pl.col("DD") == "08").collect()

Join de datos en diferentes formatos: parquet y csv

El siguiente ejemplo incluye un join que muestra la capacidad para analizar datasets en diferentes formatos

q2 = (
    pl.scan_parquet("Parquet/*/*/*.parquet")
    .join(pl.scan_csv("taxi_zone_lookup.csv"), left_on="PULocationID", right_on="LocationID")
    .filter(pl.col("total_amount") > 25)
    .group_by("Zone")
    .agg(
        (pl.col("total_amount") /
        (pl.col("tpep_dropoff_datetime") - pl.col("tpep_pickup_datetime")).dt.total_minutes()
        ).mean().alias("cost_per_minute")
    ).sort("cost_per_minute",descending=True)
)

# Collect the results (executes the query)
r2 = q2.collect()
print(r2)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Zone                            ┆ cost_per_minute β”‚
β”‚ ---                             ┆ ---             β”‚
β”‚ str                             ┆ f64             β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ═════════════════║
β”‚ Co-Op City                      ┆ inf             β”‚
β”‚ Whitestone                      ┆ inf             β”‚
β”‚ UN/Turtle Bay South             ┆ inf             β”‚
β”‚ Eltingville/Annadale/Prince's … ┆ inf             β”‚
β”‚ Pelham Bay Park                 ┆ inf             β”‚
β”‚ …                               ┆ …               β”‚
β”‚ New Dorp/Midland Beach          ┆ 3.958707        β”‚
β”‚ Governor's Island/Ellis Island… ┆ 3.238929        β”‚
β”‚ Rikers Island                   ┆ 2.802784        β”‚
β”‚ Green-Wood Cemetery             ┆ 1.832354        β”‚
β”‚ Crotona Park                    ┆ 1.514066        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

ConversiΓ³n de datos en diferentes formatos: Parquet y CSV

El siguiente ejemplo muestra una transformación CSV→Parquet con dos singularidades esenciales:

  1. No requiere de almacenamiento intermedio alguno
  2. Tipos de datos explΓ­citos (minimiza problemas tΓ­picos de los procesos de migraciΓ³n/integraciΓ³n)
lf = pl.scan_csv("raw/*.csv", dtypes={"ts": pl.Datetime, "user_id": pl.Int64})
(
    lf.filter(pl.col("ts") >= pl.datetime(2025, 10, 1))
      .with_columns(pl.col("path").str.slice(0, 64).alias("path_64"))
      .sink_parquet("curated/2025-10.parquet")