Polars vs Pandas: Welke Python Library Wint de Data-Analyse Strijd?
Als je met Python en data werkt, ken je Pandas waarschijnlijk als je beste vriend. Miljoenen data scientists, analisten en engineers gebruiken het dagelijks — en eerlijk gezegd, het heeft die reputatie ook verdiend. Maar er is een nieuwkomer die steeds meer aandacht trekt: Polars, een razendsnelle DataFrame-bibliotheek die onder de motorkap in Rust is geschreven.
Ik moet toegeven, toen ik voor het eerst over Polars hoorde, was ik sceptisch. Weer een nieuwe library die belooft alles beter te doen? Maar na het draaien van een paar benchmarks op mijn eigen datasets veranderde ik snel van mening.
In deze gids vergelijken we Polars en Pandas op alle fronten: architectuur, prestaties, syntax en wanneer je welke moet kiezen. Inclusief praktische codevoorbeelden die je direct kunt gebruiken.
Waarom Polars in 2026 op Je Radar Moet Staan
De datasets waarmee we tegenwoordig werken worden elk jaar groter. Waar we vijf jaar geleden nog prima uit de voeten konden met een CSV-bestandje van een paar megabytes, is het nu heel normaal om gigabytes aan data op je laptop te verwerken. En daar begint Pandas — oorspronkelijk ontworpen in 2008 — tegen zijn limieten aan te lopen.
Waarom zou je als data professional Polars moeten kennen?
- Pandas is single-threaded: Het maakt kopieën van data in het geheugen, waardoor het bij grote datasets traag en geheugenintensief wordt. Herkenbaar?
- Moderne hardware wordt niet benut: Je laptop heeft waarschijnlijk 8+ cores, maar Pandas gebruikt er maar eentje. Polars pakt ze standaard allemaal.
- De adoptie groeit explosief: Steeds meer bedrijven en open-source projecten stappen over. De community is ontzettend actief.
- Lazy evaluation: Polars kan queries optimaliseren voordat ze worden uitgevoerd — vergelijkbaar met hoe SQL-databases werken. Best slim.
- Minder geheugengebruik: Dankzij Apache Arrow gebruikt Polars aanzienlijk minder werkgeheugen. Dat merk je echt bij grotere datasets.
Goed, laten we eens kijken wat Polars precies onder de motorkap heeft.
Wat is Polars? Een Technisch Overzicht
Polars is een open-source DataFrame-bibliotheek die van de grond af aan is ontworpen voor maximale snelheid en geheugenefficiëntie. Waar Pandas in Python en C is geschreven, is Polars gebouwd in Rust — een taal die bekendstaat om zijn snelheid én geheugenveiligheid.
De Drie Pilaren van Polars
De architectuur rust op drie fundamentele principes:
- Geschreven in Rust: Rust biedt prestaties vergelijkbaar met C/C++, maar dan met ingebouwde geheugenveiligheid. Geen geheugenlekken, geen buffer overflows. De Python-interface is eigenlijk gewoon een dunne wrapper rondom die Rust-kern.
- Multi-threaded uitvoering: Polars gebruikt standaard alle beschikbare CPU-cores. Filteren, sorteren, groeperen — het wordt allemaal automatisch geparallelliseerd. Je hoeft er niets voor te configureren.
- Apache Arrow geheugenmodel: In plaats van NumPy's geheugenformaat (wat Pandas gebruikt), draait Polars op Apache Arrow. Dat is een columnar formaat dat efficiënter omgaat met geheugen, zero-copy interoperabiliteit biedt met andere tools, en betere cache-prestaties levert op moderne processoren.
Polars Installeren
Installeren is simpel:
# Polars installeren
pip install polars
# Of met alle optionele dependencies
pip install polars[all]
# Versie controleren
import polars as pl
print(pl.__version__)
Meer is het niet. Je kunt binnen een minuut aan de slag.
Architecturale Verschillen: Waarom Polars Sneller Is
Om te begrijpen waarom Polars zoveel sneller is, moeten we even onder de motorkap kijken. De architecturale verschillen verklaren niet alleen de prestatiekloof, maar ook waarom de syntax soms anders is.
Geheugenmodel
Pandas gebruikt NumPy-arrays als onderliggende datastructuur. Elke kolom is een NumPy-array, en het DataFrame is eigenlijk een dictionary van die arrays. Dat klinkt prima, maar het heeft nadelen: het is bij bepaalde bewerkingen rij-georiënteerd, strings worden inefficiënt opgeslagen als Python-objecten, en transformaties vereisen vaak kopieën van de data.
Polars gebruikt Apache Arrow-arrays die kolom-georiënteerd zijn. Data van hetzelfde type wordt aaneengesloten in het geheugen opgeslagen, wat veel betere cache-prestaties oplevert. Daarnaast ondersteunt Arrow native string-types, null-waarden en geneste datatypen veel efficiënter.
Eager vs. Lazy Uitvoering
Pandas voert alles eager uit — elke operatie wordt direct uitgevoerd. Meerdere transformaties achter elkaar? Elke stap wordt apart berekend met tussentijdse geheugenallocatie. Dat is niet ideaal.
Polars ondersteunt zowel eager als lazy uitvoering. In lazy modus bouwt het eerst een query-plan op en optimaliseert dit voordat het wordt uitgevoerd. Onnodige berekeningen? Die worden gewoon geëlimineerd.
Threading en Parallellisme
Pandas zit vast aan single-threaded uitvoering door de beruchte Python GIL. Sommige NumPy-bewerkingen zijn wel multi-threaded, maar de meeste Pandas-operaties gebruiken maar één core.
Polars omzeilt de GIL volledig — de kern draait in Rust en gebruikt Rayon (een work-stealing threadpool) om bewerkingen over alle cores te verdelen. Bij grote datasets levert dit enorme winst op.
Overzichtstabel
| Eigenschap | Pandas | Polars |
|----------------------|---------------------|-------------------------|
| Taal kern | C / Python | Rust |
| Geheugenmodel | NumPy arrays | Apache Arrow |
| Threading | Single-threaded | Multi-threaded |
| Evaluatie | Eager | Eager + Lazy |
| Index | Ja (rij-index) | Nee (geen index) |
| Null-handling | NaN / None (mixed) | Native null-type |
| String-type | Object (traag) | Native UTF-8 (snel) |
| Geheugengebruik | Hoog | Laag |
Benchmarks: Hoe Veel Sneller Is Polars Echt?
Oké, genoeg theorie. Laten we naar de cijfers kijken. De volgende benchmarks zijn uitgevoerd op een dataset van 10 miljoen rijen (vergelijkbaar met een e-commerce transactiedataset) op een machine met 8 CPU-cores en 16 GB RAM.
Spoiler alert: de resultaten zijn best indrukwekkend.
CSV Laden: 5x Sneller
Het laden van een groot CSV-bestand is vaak de eerste stap. En meteen zie je al een flink verschil.
import pandas as pd
import polars as pl
import time
# Pandas: CSV laden
start = time.time()
df_pandas = pd.read_csv("transacties_10m.csv")
pandas_tijd = time.time() - start
print(f"Pandas CSV laden: {pandas_tijd:.2f} seconden")
# Resultaat: ~12.4 seconden
# Polars: CSV laden
start = time.time()
df_polars = pl.read_csv("transacties_10m.csv")
polars_tijd = time.time() - start
print(f"Polars CSV laden: {polars_tijd:.2f} seconden")
# Resultaat: ~2.5 seconden
print(f"Polars is {pandas_tijd / polars_tijd:.1f}x sneller")
De uitkomst:
- Pandas: 12.4 seconden, geheugengebruik ~1.4 GB
- Polars: 2.5 seconden, geheugengebruik ~179 MB
- Verschil: 5x sneller en 87% minder geheugen
Dat verschil in geheugengebruik (179 MB vs 1.4 GB!) komt doordat Polars het Arrow-formaat gebruikt, dat strings veel efficiënter opslaat dan het NumPy object-type. Bij datasets met veel tekstkolommen kan het verschil nóg groter zijn.
Filteren: 4.6x Sneller
Rijen filteren op basis van condities — je doet het waarschijnlijk tientallen keren per dag.
# Pandas: Filteren
start = time.time()
result_pd = df_pandas[
(df_pandas["bedrag"] > 100) &
(df_pandas["categorie"] == "Elektronica") &
(df_pandas["status"] == "voltooid")
]
pandas_filter = time.time() - start
print(f"Pandas filteren: {pandas_filter:.3f} seconden")
# Resultaat: ~0.92 seconden
# Polars: Filteren
start = time.time()
result_pl = df_polars.filter(
(pl.col("bedrag") > 100) &
(pl.col("categorie") == "Elektronica") &
(pl.col("status") == "voltooid")
)
polars_filter = time.time() - start
print(f"Polars filteren: {polars_filter:.3f} seconden")
# Resultaat: ~0.20 seconden
print(f"Polars is {pandas_filter / polars_filter:.1f}x sneller")
Resultaat: Polars is 4.6x sneller dankzij multi-threaded filtering. Bij 0.20 seconden versus 0.92 seconden merk je het misschien niet bij één keer draaien, maar in een pipeline die duizenden keren draait? Dat tikt aan.
Aggregaties: 2.6x Sneller
GroupBy-aggregaties zijn de ruggengraat van de meeste data-analyses. Hier een typische vergelijking:
# Pandas: GroupBy aggregatie
start = time.time()
result_pd = df_pandas.groupby("categorie").agg(
totaal_bedrag=("bedrag", "sum"),
gem_bedrag=("bedrag", "mean"),
aantal=("bedrag", "count"),
max_bedrag=("bedrag", "max")
)
pandas_agg = time.time() - start
print(f"Pandas aggregatie: {pandas_agg:.3f} seconden")
# Resultaat: ~1.56 seconden
# Polars: GroupBy aggregatie
start = time.time()
result_pl = df_polars.group_by("categorie").agg(
pl.col("bedrag").sum().alias("totaal_bedrag"),
pl.col("bedrag").mean().alias("gem_bedrag"),
pl.col("bedrag").count().alias("aantal"),
pl.col("bedrag").max().alias("max_bedrag")
)
polars_agg = time.time() - start
print(f"Polars aggregatie: {polars_agg:.3f} seconden")
# Resultaat: ~0.60 seconden
print(f"Polars is {pandas_agg / polars_agg:.1f}x sneller")
Uitkomst: 1.56 seconden vs 0.60 seconden — Polars is 2.6x sneller. Niet het meest spectaculaire verschil, maar het telt op.
Sorteren: 11.7x Sneller
Bij sorteren wordt het verschil pas echt dramatisch. Polars gebruikt een geoptimaliseerd parallel sort-algoritme, en dat merk je.
# Pandas: Sorteren op meerdere kolommen
start = time.time()
result_pd = df_pandas.sort_values(
by=["categorie", "bedrag"],
ascending=[True, False]
)
pandas_sort = time.time() - start
print(f"Pandas sorteren: {pandas_sort:.3f} seconden")
# Resultaat: ~7.02 seconden
# Polars: Sorteren op meerdere kolommen
start = time.time()
result_pl = df_polars.sort(
by=["categorie", "bedrag"],
descending=[False, True]
)
polars_sort = time.time() - start
print(f"Polars sorteren: {polars_sort:.3f} seconden")
# Resultaat: ~0.60 seconden
print(f"Polars is {pandas_sort / polars_sort:.1f}x sneller")
Resultaat: 7.02 seconden versus 0.60 seconden. Dat is 11.7x sneller. Eerlijk gezegd was dit het moment waarop ik echt overtuigd raakte.
Joins: 13.75x Sneller
Het samenvoegen van datasets is een van de meest rekenintensieve bewerkingen. En hier schittert Polars het meest.
# Tweede dataset voor de join
# klanten_df heeft 1 miljoen unieke klant-records
# Pandas: Inner join
start = time.time()
result_pd = df_pandas.merge(
klanten_pd,
on="klant_id",
how="inner"
)
pandas_join = time.time() - start
print(f"Pandas join: {pandas_join:.3f} seconden")
# Resultaat: ~5.50 seconden
# Polars: Inner join
start = time.time()
result_pl = df_polars.join(
klanten_pl,
on="klant_id",
how="inner"
)
polars_join = time.time() - start
print(f"Polars join: {polars_join:.3f} seconden")
# Resultaat: ~0.40 seconden
print(f"Polars is {pandas_join / polars_join:.1f}x sneller")
Resultaat: 5.50 seconden versus 0.40 seconden — 13.75x sneller. Dat is geen klein verschil meer.
Benchmark Samenvatting
| Bewerking | Pandas | Polars | Speedup |
|-----------------|------------|------------|----------|
| CSV laden | 12.4s | 2.5s | 5.0x |
| Filteren | 0.92s | 0.20s | 4.6x |
| Aggregaties | 1.56s | 0.60s | 2.6x |
| Sorteren | 7.02s | 0.60s | 11.7x |
| Joins | 5.50s | 0.40s | 13.75x |
Polars is consistent sneller in alle geteste bewerkingen, met snelheidswinsten van 2.6x tot 13.75x. De grootste winst zie je bij bewerkingen die sterk profiteren van parallellisatie, zoals sorteren en joins.
Lazy Evaluation: Het Geheime Wapen van Polars
Dit is waar het écht interessant wordt. Een van de krachtigste features van Polars is lazy evaluation. Het concept is simpel maar briljant: in plaats van elke bewerking direct uit te voeren, bouwt Polars eerst een query-plan op en optimaliseert dit voordat er ook maar iets wordt berekend.
Hoe Werkt Het?
Bij lazy evaluation worden bewerkingen niet direct uitgevoerd. Polars bouwt een logisch query-plan op — een soort boomstructuur van alle bewerkingen die je wilt doen. Pas wanneer je collect() aanroept, wordt het plan geoptimaliseerd en uitgevoerd.
import polars as pl
# Lazy evaluation: het query-plan wordt opgebouwd maar NIET uitgevoerd
lazy_result = (
pl.scan_csv("transacties_10m.csv") # Lazy CSV reader
.filter(pl.col("bedrag") > 100) # Filter wordt gepland
.filter(pl.col("categorie") == "Elektronica") # Nog een filter
.group_by("regio") # GroupBy wordt gepland
.agg(
pl.col("bedrag").sum().alias("totaal"),
pl.col("bedrag").mean().alias("gemiddelde"),
pl.col("klant_id").n_unique().alias("unieke_klanten")
)
.sort("totaal", descending=True) # Sortering wordt gepland
)
# Op dit punt is er NOG NIETS berekend!
print(type(lazy_result)) #
# Nu pas wordt alles geoptimaliseerd en uitgevoerd
result = lazy_result.collect()
print(result)
Best cool, toch? Je schrijft je hele analyse, en Polars zoekt zelf de meest efficiënte manier om het uit te voeren.
Predicate Pushdown
Predicate pushdown is een van de slimste optimalisaties. Polars verplaatst filteroperaties zo vroeg mogelijk in het query-plan. Als je eerst een CSV laadt en daarna filtert, past Polars het filter toe tijdens het laden — zodat onnodige rijen nooit in het geheugen komen.
# Zonder optimalisatie zou dit ALLE rijen laden en dan filteren
# Met predicate pushdown filtert Polars tijdens het lezen
result = (
pl.scan_csv("transacties_10m.csv")
.filter(pl.col("datum") >= "2025-01-01") # Dit filter wordt
.filter(pl.col("bedrag") > 500) # naar beneden geduwd
.group_by("categorie")
.agg(pl.col("bedrag").sum())
.collect()
)
# Polars leest alleen rijen die aan BEIDE filtercondities voldoen
# Dit bespaart enorm veel geheugen en rekentijd
Projection Pushdown
Projection pushdown zorgt ervoor dat alleen de kolommen worden geladen die je daadwerkelijk nodig hebt. Heeft je CSV 50 kolommen maar gebruik je er maar 3? Polars leest alleen die 3.
# Dit CSV-bestand heeft 20 kolommen, maar we gebruiken er maar 3
result = (
pl.scan_csv("grote_dataset.csv")
.select(["klant_id", "bedrag", "categorie"]) # Projection pushdown
.filter(pl.col("bedrag") > 100)
.group_by("categorie")
.agg(pl.col("bedrag").sum())
.collect()
)
# Polars leest ALLEEN de kolommen klant_id, bedrag en categorie
# De overige 17 kolommen worden volledig genegeerd
Het Query-Plan Inspecteren
Wil je zien wat Polars onder de motorkap doet? Je kunt het geoptimaliseerde query-plan bekijken:
lazy_query = (
pl.scan_csv("transacties_10m.csv")
.filter(pl.col("bedrag") > 100)
.select(["klant_id", "bedrag", "categorie"])
.group_by("categorie")
.agg(pl.col("bedrag").sum())
)
# Bekijk het geoptimaliseerde query-plan
print(lazy_query.explain())
# Output toont hoe filters en projecties naar beneden zijn geduwd:
# AGGREGATE
# [col("bedrag").sum()] BY [col("categorie")]
# CSV SCAN transacties_10m.csv
# PROJECT 3/20 COLUMNS <-- Projection pushdown
# SELECTION: col("bedrag") > 100 <-- Predicate pushdown
Lazy evaluation kan bij complexe analyses op grote bestanden oplopen tot 10-100x sneller dan eager evaluation. Simpelweg omdat er véél minder data wordt gelezen en verwerkt.
Praktische Codevoorbeelden: Pandas vs Polars Zij aan Zij
Genoeg uitleg — laten we code vergelijken. Hieronder de meest voorkomende data-bewerkingen in beide bibliotheken, zodat je precies ziet wat de syntaxverschillen zijn.
Data Laden en Verkennen
# ============ PANDAS ============
import pandas as pd
# CSV laden
df_pd = pd.read_csv("verkopen.csv")
# Eerste rijen bekijken
print(df_pd.head())
# Structuur en datatypes
print(df_pd.info())
print(df_pd.describe())
# Kolom selecteren
omzet = df_pd["omzet"]
subset = df_pd[["product", "omzet", "datum"]]
# ============ POLARS ============
import polars as pl
# CSV laden (eager)
df_pl = pl.read_csv("verkopen.csv")
# Of lazy laden (aanbevolen voor grote bestanden)
df_pl_lazy = pl.scan_csv("verkopen.csv")
# Eerste rijen bekijken
print(df_pl.head())
# Structuur en datatypes
print(df_pl.schema)
print(df_pl.describe())
# Kolom selecteren
omzet = df_pl.select("omzet")
subset = df_pl.select(["product", "omzet", "datum"])
De syntax lijkt op het eerste gezicht best wel op elkaar. Het grootste verschil: Polars gebruikt pl.col() expressions in plaats van directe kolomtoegang.
Filteren van Data
# ============ PANDAS ============
# Eenvoudig filter
dure_producten = df_pd[df_pd["prijs"] > 100]
# Meerdere condities
resultaat = df_pd[
(df_pd["prijs"] > 100) &
(df_pd["categorie"] == "Laptop") &
(df_pd["voorraad"] > 0)
]
# Filter met isin
geselecteerd = df_pd[df_pd["merk"].isin(["Apple", "Samsung", "Dell"])]
# Filter met string methoden
apple_prod = df_pd[df_pd["naam"].str.contains("MacBook")]
# ============ POLARS ============
# Eenvoudig filter
dure_producten = df_pl.filter(pl.col("prijs") > 100)
# Meerdere condities
resultaat = df_pl.filter(
(pl.col("prijs") > 100) &
(pl.col("categorie") == "Laptop") &
(pl.col("voorraad") > 0)
)
# Filter met is_in
geselecteerd = df_pl.filter(
pl.col("merk").is_in(["Apple", "Samsung", "Dell"])
)
# Filter met string methoden
apple_prod = df_pl.filter(
pl.col("naam").str.contains("MacBook")
)
GroupBy Operaties
# ============ PANDAS ============
# Eenvoudige groepering
per_categorie = df_pd.groupby("categorie")["omzet"].sum()
# Meerdere aggregaties
samenvatting = df_pd.groupby("categorie").agg(
totaal_omzet=("omzet", "sum"),
gemiddelde_prijs=("prijs", "mean"),
aantal_verkopen=("order_id", "count"),
max_korting=("korting", "max")
).reset_index()
# Groeperen op meerdere kolommen
per_regio_cat = df_pd.groupby(["regio", "categorie"]).agg(
omzet=("omzet", "sum"),
orders=("order_id", "nunique")
).reset_index()
# ============ POLARS ============
# Eenvoudige groepering
per_categorie = df_pl.group_by("categorie").agg(
pl.col("omzet").sum()
)
# Meerdere aggregaties
samenvatting = df_pl.group_by("categorie").agg(
pl.col("omzet").sum().alias("totaal_omzet"),
pl.col("prijs").mean().alias("gemiddelde_prijs"),
pl.col("order_id").count().alias("aantal_verkopen"),
pl.col("korting").max().alias("max_korting")
)
# Groeperen op meerdere kolommen
per_regio_cat = df_pl.group_by(["regio", "categorie"]).agg(
pl.col("omzet").sum().alias("omzet"),
pl.col("order_id").n_unique().alias("orders")
)
Merk op: in Polars hoef je geen .reset_index() te doen na een group_by. Er ís namelijk geen index. Dat scheelt een stap (en een hoop verwarring).
Window Functies met .over()
Dit is naar mijn mening een van de elegantste features van Polars. De .over() methode maakt window functies bijna pijnlijk eenvoudig vergeleken met Pandas' transform().
# ============ PANDAS ============
# Gemiddelde omzet per categorie toevoegen aan elke rij
df_pd["gem_omzet_categorie"] = df_pd.groupby("categorie")["omzet"].transform("mean")
# Rang binnen categorie
df_pd["rang_in_categorie"] = df_pd.groupby("categorie")["omzet"].rank(
ascending=False, method="dense"
)
# Cumulatieve som per klant
df_pd = df_pd.sort_values("datum")
df_pd["cum_omzet"] = df_pd.groupby("klant_id")["omzet"].cumsum()
# Verschil met vorige verkoop per klant
df_pd["verschil"] = df_pd.groupby("klant_id")["omzet"].diff()
# Percentage van categorie-totaal
df_pd["pct_van_categorie"] = (
df_pd["omzet"] /
df_pd.groupby("categorie")["omzet"].transform("sum") * 100
)
# ============ POLARS ============
# Gemiddelde omzet per categorie toevoegen aan elke rij
df_pl = df_pl.with_columns(
pl.col("omzet").mean().over("categorie").alias("gem_omzet_categorie")
)
# Rang binnen categorie
df_pl = df_pl.with_columns(
pl.col("omzet").rank(descending=True, method="dense")
.over("categorie")
.alias("rang_in_categorie")
)
# Cumulatieve som per klant (gesorteerd op datum)
df_pl = df_pl.sort("datum").with_columns(
pl.col("omzet").cum_sum().over("klant_id").alias("cum_omzet")
)
# Verschil met vorige verkoop per klant
df_pl = df_pl.with_columns(
pl.col("omzet").diff().over("klant_id").alias("verschil")
)
# Percentage van categorie-totaal
df_pl = df_pl.with_columns(
(pl.col("omzet") / pl.col("omzet").sum().over("categorie") * 100)
.alias("pct_van_categorie")
)
Zie je hoe leesbaar die Polars syntax is? Bereken iets over een groepering — de intentie is direct duidelijk. In Pandas moet je die omslachtige groupby().transform() constructie gebruiken, en die is eerlijk gezegd een stuk minder intuïtief.
Joins
# ============ PANDAS ============
# Inner join
result = pd.merge(
verkopen_pd,
klanten_pd,
on="klant_id",
how="inner"
)
# Left join met meerdere sleutels
result = pd.merge(
verkopen_pd,
producten_pd,
left_on=["product_code", "variant"],
right_on=["code", "variant_id"],
how="left"
)
# Anti-join (klanten zonder bestellingen)
# Pandas heeft geen native anti-join, dus workaround nodig
klanten_zonder = klanten_pd[
~klanten_pd["klant_id"].isin(verkopen_pd["klant_id"])
]
# ============ POLARS ============
# Inner join
result = verkopen_pl.join(
klanten_pl,
on="klant_id",
how="inner"
)
# Left join met meerdere sleutels
result = verkopen_pl.join(
producten_pl,
left_on=["product_code", "variant"],
right_on=["code", "variant_id"],
how="left"
)
# Anti-join (klanten zonder bestellingen) - native support!
klanten_zonder = klanten_pl.join(
verkopen_pl.select("klant_id").unique(),
on="klant_id",
how="anti"
)
Die native anti-join ondersteuning in Polars is echt een verademing. In Pandas moet je altijd een workaround verzinnen — in Polars zeg je gewoon how="anti" en klaar.
Kolommen Toevoegen en Transformeren
# ============ PANDAS ============
# Nieuwe kolom toevoegen
df_pd["winst"] = df_pd["omzet"] - df_pd["kosten"]
df_pd["marge_pct"] = (df_pd["winst"] / df_pd["omzet"]) * 100
# Conditionele kolom
df_pd["segment"] = pd.cut(
df_pd["omzet"],
bins=[0, 100, 500, float("inf")],
labels=["klein", "medium", "groot"]
)
# Meerdere kolommen tegelijk
df_pd["jaar"] = pd.to_datetime(df_pd["datum"]).dt.year
df_pd["maand"] = pd.to_datetime(df_pd["datum"]).dt.month
# ============ POLARS ============
# Nieuwe kolom toevoegen
df_pl = df_pl.with_columns(
(pl.col("omzet") - pl.col("kosten")).alias("winst"),
((pl.col("omzet") - pl.col("kosten")) / pl.col("omzet") * 100)
.alias("marge_pct")
)
# Conditionele kolom met when/then/otherwise
df_pl = df_pl.with_columns(
pl.when(pl.col("omzet") <= 100).then(pl.lit("klein"))
.when(pl.col("omzet") <= 500).then(pl.lit("medium"))
.otherwise(pl.lit("groot"))
.alias("segment")
)
# Meerdere kolommen tegelijk (in een enkele with_columns)
df_pl = df_pl.with_columns(
pl.col("datum").str.to_datetime().dt.year().alias("jaar"),
pl.col("datum").str.to_datetime().dt.month().alias("maand")
)
De when/then/otherwise syntax in Polars vind ik persoonlijk veel leesbaarder dan pd.cut(). Het leest bijna als gewoon Engels (of in ons geval, logica).
Een Complete Analyse-Pipeline
Nu een realistisch voorbeeld: een complete pipeline die meerdere bewerkingen combineert. Dit is waar Polars echt tot zijn recht komt.
# ============ POLARS: Complete analyse-pipeline ============
import polars as pl
# Lazy laden en complete analyse in een keer
resultaat = (
pl.scan_csv("verkopen_2025.csv")
# Datatypes casten
.with_columns(
pl.col("datum").str.to_datetime(),
pl.col("bedrag").cast(pl.Float64),
)
# Filteren op relevante data
.filter(
(pl.col("datum").dt.year() == 2025) &
(pl.col("status") == "voltooid") &
(pl.col("bedrag") > 0)
)
# Nieuwe kolommen berekenen
.with_columns(
pl.col("datum").dt.quarter().alias("kwartaal"),
pl.col("datum").dt.month().alias("maand"),
(pl.col("bedrag") * pl.col("aantal")).alias("totaal_bedrag"),
)
# Window functie: percentage van categorie-totaal
.with_columns(
(pl.col("totaal_bedrag") / pl.col("totaal_bedrag").sum().over("categorie") * 100)
.round(2)
.alias("pct_categorie")
)
# Groeperen en aggregeren
.group_by(["categorie", "kwartaal"])
.agg(
pl.col("totaal_bedrag").sum().alias("omzet"),
pl.col("totaal_bedrag").mean().alias("gem_orderwaarde"),
pl.col("klant_id").n_unique().alias("unieke_klanten"),
pl.col("order_id").count().alias("aantal_orders"),
)
# Sorteren
.sort(["categorie", "kwartaal"])
# Uitvoeren
.collect()
)
print(resultaat)
Deze hele pipeline wordt geoptimaliseerd voordat er ook maar één byte data wordt gelezen. Predicate pushdown, projection pushdown — Polars regelt het allemaal automatisch. Probeer dat maar eens in Pandas.
Wanneer Kies Je Pandas en Wanneer Polars?
Ondanks alle indrukwekkende benchmarks is Polars niet altijd de beste keuze. Serieus. Hier is een eerlijk besliskader.
Kies Pandas wanneer:
- Je datasets klein zijn (< 100 MB): Bij kleine datasets is het verschil verwaarloosbaar, en Pandas' vertrouwde syntax kan gewoon productiever zijn.
- Je afhankelijk bent van het ecosysteem: Bibliotheken als scikit-learn, statsmodels en seaborn verwachten Pandas DataFrames. Conversie is simpel, maar het is wel een extra stap.
- Je team Pandas door en door kent: De leercurve weegt niet altijd op tegen de prestatiewinst, zeker bij kleinere datasets.
- Tijdreeksanalyse je ding is: Pandas heeft een volwassen API met
resample(),rolling(), en uitgebreide DatetimeIndex-functionaliteit die Polars nog niet volledig matcht. - Je aan het prototypen bent: Voor snelle, interactieve exploratie in Jupyter notebooks is Pandas nog steeds uitstekend.
- Er veel bestaande code is: Miljoenen regels productiecode draaien op Pandas. Herschrijven puur voor snelheid is lang niet altijd gerechtvaardigd.
Kies Polars wanneer:
- Je datasets groot zijn (> 1 GB): Hier worden de verschillen echt significant. Van minuten naar seconden — dat is geen overdrijving.
- Geheugen een bottleneck is: Polars gebruikt fors minder geheugen. Cruciaal als je op een machine met beperkt RAM werkt of meerdere datasets tegelijk verwerkt.
- Je data-pipelines bouwt: Lazy evaluation en query-optimalisatie maken Polars ideaal voor ETL-pipelines in productie.
- Snelheid er echt toe doet: Real-time dashboards, API-endpoints, batch-verwerking — als elke seconde telt, kies Polars.
- Je een nieuw project start: Zonder legacy Pandas-code is er weinig reden om niet met Polars te beginnen. De API is modern en goed gedocumenteerd.
- Je met streaming data werkt: Via
collect(streaming=True)kan Polars datasets verwerken die te groot zijn voor het geheugen.
De Hybride Aanpak (vaak het slimst)
In de praktijk is een combinatie vaak de beste keuze. Gebruik Polars voor het zware rekenwerk en converteer naar Pandas waar nodig voor ML of visualisatie:
import polars as pl
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
import matplotlib.pyplot as plt
# Stap 1: Zware data-verwerking in Polars (snel!)
df_verwerkt = (
pl.scan_csv("grote_dataset.csv")
.filter(pl.col("jaar") >= 2024)
.with_columns(
pl.col("omzet").log().alias("log_omzet"),
pl.col("categorie").cast(pl.Categorical),
)
.group_by(["klant_id", "categorie"])
.agg(
pl.col("omzet").sum().alias("totaal_omzet"),
pl.col("omzet").mean().alias("gem_omzet"),
pl.col("order_id").count().alias("aantal_orders"),
)
.collect()
)
# Stap 2: Converteer naar Pandas voor scikit-learn
df_pandas = df_verwerkt.to_pandas()
# Stap 3: Machine learning met scikit-learn
X = df_pandas[["totaal_omzet", "gem_omzet", "aantal_orders"]]
y = df_pandas["categorie"]
model = RandomForestClassifier()
model.fit(X, y)
# Stap 4: Visualisatie met matplotlib
fig, ax = plt.subplots(figsize=(10, 6))
df_pandas.groupby("categorie")["totaal_omzet"].sum().plot(kind="bar", ax=ax)
plt.title("Totale Omzet per Categorie")
plt.tight_layout()
plt.savefig("omzet_per_categorie.png")
Migratietips: Van Pandas naar Polars
Besloten om (gedeeltelijk) over te stappen? Hier zijn de belangrijkste dingen om in je achterhoofd te houden.
1. Er Is Geen Index
Polars heeft bewust geen rij-index. Dit is een fundamenteel ontwerpbesluit — het maakt de code eenvoudiger en de prestaties beter. Maar als je Pandas-code zwaar leunt op de index, moet je wat herstructureren.
# Pandas: Gebruik van index
df_pd = df_pd.set_index("datum")
waarde = df_pd.loc["2025-06-15", "omzet"]
# Polars: Gebruik filter in plaats van index
waarde = df_pl.filter(
pl.col("datum") == "2025-06-15"
).select("omzet")
# Pandas: Index-gebaseerde operaties
df_pd = df_pd.sort_index()
df_pd = df_pd["2025-01":"2025-06"] # Slice op index
# Polars: Expliciete filter- en sorteerbewerkingen
df_pl = df_pl.sort("datum")
df_pl = df_pl.filter(
(pl.col("datum") >= "2025-01-01") &
(pl.col("datum") < "2025-07-01")
)
2. Kolommen Selecteren en Hernoemen
# Pandas
df_pd = df_pd.rename(columns={"oude_naam": "nieuwe_naam"})
df_pd = df_pd[["kolom_a", "kolom_b"]]
# Polars
df_pl = df_pl.rename({"oude_naam": "nieuwe_naam"})
df_pl = df_pl.select(["kolom_a", "kolom_b"])
Subtiel verschil, maar Polars is iets beknopter hier.
3. Expressions in Plaats van Apply
In Pandas is apply() een veelgebruikte maar notoir trage methode. In Polars gebruik je native expressions — die zijn vele malen sneller.
# Pandas: Traag met apply
df_pd["categorie_label"] = df_pd.apply(
lambda row: f"{row['categorie']}_{row['regio']}", axis=1
)
# Polars: Snel met expressions
df_pl = df_pl.with_columns(
(pl.col("categorie") + "_" + pl.col("regio")).alias("categorie_label")
)
# Pandas: Traag met apply voor conditionele logica
df_pd["korting_type"] = df_pd["korting"].apply(
lambda x: "hoog" if x > 20 else ("medium" if x > 10 else "laag")
)
# Polars: Snel met when/then/otherwise
df_pl = df_pl.with_columns(
pl.when(pl.col("korting") > 20).then(pl.lit("hoog"))
.when(pl.col("korting") > 10).then(pl.lit("medium"))
.otherwise(pl.lit("laag"))
.alias("korting_type")
)
4. Geen In-Place Operaties
Pandas moedigt in-place mutaties aan met inplace=True. Polars werkt met onveranderlijke DataFrames — elke bewerking geeft een nieuw DataFrame terug. Dat maakt je code voorspelbaarder en veiliger, vooral bij parallelle verwerking.
# Pandas: In-place mutatie
df_pd.drop(columns=["temp_kolom"], inplace=True)
df_pd.fillna(0, inplace=True)
# Polars: Altijd een nieuw DataFrame
df_pl = df_pl.drop("temp_kolom")
df_pl = df_pl.fill_null(0)
5. Stapsgewijze Migratie
Je hoeft echt niet alles in één keer te migreren. Een praktische aanpak:
- Begin met nieuwe code: Schrijf nieuwe analyses en pipelines in Polars.
- Identificeer bottlenecks: Zoek de traagste delen van je bestaande Pandas-code en migreer die eerst.
- Gebruik conversie-functies:
df_polars.to_pandas()enpl.from_pandas(df_pandas)maken wisselen tussen beide heel eenvoudig. - Test grondig: Vergelijk de resultaten om te controleren of de migratie correct is.
# Conversie tussen Pandas en Polars
import polars as pl
import pandas as pd
# Pandas DataFrame naar Polars
df_pandas = pd.DataFrame({
"naam": ["Jan", "Piet", "Klaas"],
"leeftijd": [30, 25, 35],
"stad": ["Amsterdam", "Rotterdam", "Utrecht"]
})
df_polars = pl.from_pandas(df_pandas)
# Polars DataFrame naar Pandas
df_terug_naar_pandas = df_polars.to_pandas()
# Parquet: Ideaal tussenformaat voor beide bibliotheken
df_polars.write_parquet("data.parquet")
df_pandas_geladen = pd.read_parquet("data.parquet")
df_polars_geladen = pl.read_parquet("data.parquet")
6. Stap Over naar Parquet
Ongeacht welke bibliotheek je gebruikt: overweeg serieus om van CSV naar Parquet te gaan. Het is een binair, kolom-georiënteerd formaat dat sneller laadt en minder schijfruimte inneemt. Zowel Pandas als Polars werken er uitstekend mee.
# Parquet is sneller EN kleiner dan CSV
# CSV: 10 miljoen rijen -> ~2.1 GB, laden: 12.4s (Pandas)
# Parquet: 10 miljoen rijen -> ~0.4 GB, laden: 1.2s (Pandas)
# Polars met Parquet en lazy evaluation
result = (
pl.scan_parquet("verkopen.parquet") # Extreem snel!
.filter(pl.col("bedrag") > 100)
.group_by("categorie")
.agg(pl.col("bedrag").sum())
.collect()
)
Geavanceerde Polars Functies
Naast de basis biedt Polars een aantal geavanceerde functies die het extra aantrekkelijk maken voor professioneel gebruik.
Streaming Mode voor Enorme Datasets
Dataset te groot voor je geheugen? Geen probleem — Polars kan het in chunks verwerken:
# Streaming: verwerk data die groter is dan je RAM
result = (
pl.scan_csv("enorme_dataset_50gb.csv")
.filter(pl.col("land") == "Nederland")
.group_by("provincie")
.agg(
pl.col("omzet").sum(),
pl.col("klant_id").n_unique()
)
.collect(streaming=True) # Verwerk in chunks!
)
50 GB aan data verwerken op een machine met 16 GB RAM? Met streaming mode kan het gewoon.
SQL Interface
Meer een SQL-persoon? Polars heeft je gedekt met een ingebouwde SQL-interface:
import polars as pl
# DataFrame aanmaken
df = pl.DataFrame({
"product": ["Laptop", "Telefoon", "Tablet", "Laptop", "Telefoon"],
"omzet": [1200, 800, 500, 1500, 900],
"regio": ["Noord", "Zuid", "Noord", "Zuid", "Noord"]
})
# SQL query uitvoeren op het DataFrame
result = pl.SQLContext(frame=df).execute("""
SELECT
regio,
SUM(omzet) as totaal_omzet,
AVG(omzet) as gem_omzet,
COUNT(*) as aantal
FROM frame
GROUP BY regio
ORDER BY totaal_omzet DESC
""").collect()
print(result)
Handig als je vanuit een SQL-achtergrond komt en de expression-syntax nog aan het leren bent.
Expressieve Data Transformaties
import polars as pl
# Complexe transformatie met geneste expressies
resultaat = df.with_columns(
# String manipulatie
pl.col("naam").str.to_uppercase().alias("naam_upper"),
# Conditionele berekening met meerdere niveaus
pl.when(pl.col("omzet") > pl.col("omzet").quantile(0.9))
.then(pl.lit("top_10_pct"))
.when(pl.col("omzet") > pl.col("omzet").median())
.then(pl.lit("bovengemiddeld"))
.otherwise(pl.lit("ondergemiddeld"))
.alias("prestatie_categorie"),
# Rolling window berekening
pl.col("omzet")
.rolling_mean(window_size=7)
.over("product")
.alias("voortschrijdend_gemiddelde_7d"),
# Lag en lead functies
pl.col("omzet").shift(1).over("product").alias("vorige_omzet"),
pl.col("omzet").shift(-1).over("product").alias("volgende_omzet"),
# Procentuele verandering
((pl.col("omzet") - pl.col("omzet").shift(1).over("product"))
/ pl.col("omzet").shift(1).over("product") * 100)
.round(2)
.alias("pct_verandering"),
)
Veelvoorkomende Valkuilen bij de Migratie
Even een heads-up over de meest voorkomende valkuilen als je overstapt van Pandas naar Polars. Deze hebben mij (en veel anderen) al eens gepakt:
- Kolomnamen zijn hoofdlettergevoelig:
"Naam"en"naam"zijn in Polars twee verschillende kolommen. Klinkt logisch, maar na jaren Pandas kan het je verrassen. - Geen automatische type-conversie: Polars is strenger met datatypes. Je moet explicieter casten met
.cast(). Dat is eigenlijk wel een goede zaak — het voorkomt stille bugs. - GroupBy-resultaten zijn ongesorteerd: Anders dan bij Pandas garandeert Polars geen volgorde na
group_by(). Voeg altijd een.sort()toe als de volgorde belangrijk is. - Null vs NaN: Polars maakt een duidelijk onderscheid tussen
null(ontbrekend) enNaN(niet-een-nummer, alleen voor floats). Pandas gooit deze concepten nogal eens door elkaar. - Geen chained indexing:
df["kolom"][0]werkt anders. Gebruikdf.select("kolom").item(0, 0)of directe Python indexing op de Series.
Conclusie: Welke Kies Jij?
De vergelijking tussen Polars en Pandas is geen simpel verhaal van "de ene is beter dan de andere". Het gaat over evolutie in het Python data-ecosysteem. Pandas heeft de weg vrijgemaakt en blijft uitstekend voor veel toepassingen. Maar Polars is de volgende generatie: sneller, zuiniger met geheugen, en beter afgestemd op moderne hardware.
Wat we geleerd hebben:
- Polars is consistent sneller: Met winsten van 2.6x tot 13.75x is het de duidelijke winnaar qua prestaties. Bij grote datasets betekent dit het verschil tussen minuten en seconden.
- Geheugenefficiëntie maakt het verschil: 179 MB versus 1.4 GB voor dezelfde dataset — dat is niet niks, vooral op machines met beperkt RAM.
- Lazy evaluation is een gamechanger: Query-optimalisatie met predicate en projection pushdown maakt Polars bijzonder geschikt voor complexe pipelines.
- De leercurve valt mee: De concepten zijn vergelijkbaar. De meeste Pandas-gebruikers zijn binnen een paar dagen productief met Polars.
- Hybride werkt het best: Polars voor het zware werk, Pandas waar het ecosysteem dit vereist. Simpel te combineren dankzij eenvoudige conversie.
Of je nu een ervaren data scientist bent of net begint met data-analyse in Python — kennis van beide bibliotheken is in 2026 echt een waardevolle investering. De trend is duidelijk: Polars groeit snel en wordt steeds breder geadopteerd.
Mijn advies? Installeer Polars, laad een van je bestaande datasets, en ervaar zelf het snelheidsverschil. De kans is groot dat je niet meer terug wilt.