A Polars 2026-ra valahogy észrevétlenül a Python-alapú adatfeldolgozás de facto nagy teljesítményű alternatívájává vált a Pandas mellett – és őszintén szólva, ennek a sebességnek a titka nem csak a Rust mag, hanem a LazyFrame API. Ha eddig csak pl.DataFrame-et használtál (mint ahogy én is sokáig), akkor a Polars erejének nagyjából a felét hagyod kihasználatlanul.
Ebben az útmutatóban végigmegyünk azon, hogyan működik a Polars lazy execution motorja, milyen optimalizációkat végez automatikusan a hátad mögött, és hogyan írj olyan lekérdezéseket, amelyek memórián túli adathalmazokat is képesek feldolgozni egyetlen laptopon. Igen, a 16 GB RAM-os gépeden is.
Mi az a LazyFrame, és miért gyorsabb?
A klasszikus DataFrame (vagyis az eager API) minden műveletet azonnal végrehajt. Ha kiolvasol egy 5 GB-os CSV-t, az teljes egészében a memóriába kerül, mielőtt akár egyetlen sort is szűrnél. Vicces dolog, ha egy 8 GB-os laptopon csinálod.
A LazyFrame ezzel szemben nem tartalmaz adatot – csak egy query plant, vagyis egy logikai műveleti gráfot, amelyet csak a .collect() vagy .sink_*() hívás idején futtat le.
Ez azt jelenti, hogy a Polars optimalizálója látja a teljes lekérdezést, mielőtt elindítaná, és olyan transzformációkat hajthat végre, amelyek eager módban egyszerűen lehetetlenek lennének.
LazyFrame vs. DataFrame: gyors összehasonlítás
| Tulajdonság | DataFrame (eager) | LazyFrame |
|---|---|---|
| Végrehajtás | Azonnali | Halasztott (collect/sink) |
| Predicate pushdown | Nem | Igen |
| Projection pushdown | Nem | Igen |
| Common subexpression elimination | Nem | Igen |
| Memórián túli adatok | Nem | Igen (streaming) |
| Tipikus gyorsulás összetett pipeline-on | 1× | 2–2.5× |
Telepítés és környezet
2026 májusában a Polars 1.x a stabil verzió, és Python 3.10–3.13 verziókat támogat. A streaming engine alapból bekapcsolt – ezért nem kell külön flag-ekkel babrálni.
pip install "polars>=1.20"
# vagy ha PyArrow integráció kell:
pip install "polars[pyarrow]>=1.20"
Importáld a következőképpen, és gyorsan ellenőrizd a verziót:
import polars as pl
print(pl.__version__) # pl. 1.21.0
LazyFrame létrehozása: scan_* vs. read_*
A legfontosabb szabály, amit szinte minden Polars-tutorial elfelejt rendesen hangsúlyozni: mindig scan_* függvényt használj, ne read_*-ot, ha tényleg a lazy API-t akarod kihasználni.
A scan_csv, scan_parquet, scan_ipc és társaik csak a fájl sémáját olvassák be. Maga az adat csak akkor kerül a memóriába, amikor szükség van rá – és a Polars akkor is csak a szükséges oszlopokat és sorokat olvassa be a lemezről.
import polars as pl
# ROSSZ: az egész fájlt beolvassa, majd "lazy"-vé alakítja
lf_bad = pl.read_csv("nagy_adathalmaz.csv").lazy()
# JÓ: ténylegesen lazy, a pushdownok a fájlolvasásra is hatnak
lf_good = pl.scan_csv("nagy_adathalmaz.csv")
# Parquet esetén a különbség még drámaibb
lf_parquet = pl.scan_parquet("data/*.parquet")
A scan_parquet egyébként támogatja a glob mintákat és a Hive-particionálást is, ami különösen hasznos data lake jellegű adatok feldolgozásához.
A négy fő automatikus optimalizáció
A Polars optimalizálója legalább négy fontos transzformációt végez automatikusan a lekérdezési terveden. Nézzük meg ezeket egyenként, valós példákkal – kezdjük a leggyakrabban emlegetettel.
1. Predicate pushdown
A predicate pushdown azt jelenti, hogy a szűrőfeltételek (a WHERE-ek) a lekérdezési terv lehető legkorábbi pontjára kerülnek. Ideális esetben magába a fájl-olvasásba beépülnek. Így a Polars eleve csak azokat a sorokat olvassa be, amelyekre szükség van.
lf = (
pl.scan_parquet("eladasok.parquet")
.with_columns(pl.col("ar") * 1.27) # ÁFA-s ár
.filter(pl.col("orszag") == "Magyarország") # ezt feljebb tolja
.select(["termek", "ar", "datum"])
)
# Az optimalizált terv a filtert a scan_parquet-be építi
print(lf.explain())
Az explain() kimenete kb. valami ilyesmit mutat:
SELECT [col("termek"), col("ar"), col("datum")] FROM
Parquet SCAN [eladasok.parquet]
PROJECT 3/N COLUMNS
SELECTION: [(col("orszag")) == ("Magyarország")]
Vegyük észre, hogy a SELECTION magában a fájl-scanben van – nem külön FILTER lépésként! Ez az, amitől a Polars úgy repül.
2. Projection pushdown
A projection pushdown az oszlopokra vonatkozó megfelelője. A Polars elemzi, hogy a teljes lekérdezésben mely oszlopokra van valójában szükség, és csak azokat olvassa be a lemezről. Egy 200 oszlopos Parquet fájlnál, ha csak 5 oszlopot használsz, akár 40-szeres I/O csökkenést is elérhetsz – és ezt az interneten alig említi bárki.
lf = (
pl.scan_parquet("nagy_tabla.parquet") # 200 oszlop
.group_by("kategoria")
.agg([
pl.col("bevetel").sum(),
pl.col("mennyiseg").mean(),
])
)
# A Polars csak 3 oszlopot olvas: kategoria, bevetel, mennyiseg
3. Common subexpression elimination (CSE)
Ha ugyanazt a kifejezést többször használod, a Polars csak egyszer számolja ki, és újrafelhasználja az eredményt. Egyszerű, de annyira hasznos:
lf = pl.scan_csv("logok.csv").with_columns([
(pl.col("ar") * pl.col("mennyiseg")).alias("brutto"),
(pl.col("ar") * pl.col("mennyiseg") * 0.27).alias("afa"),
(pl.col("ar") * pl.col("mennyiseg") * 1.27).alias("vegosszeg"),
])
# A "ar * mennyiseg" csak EGYSZER számolódik ki
4. Slice pushdown és párhuzamos végrehajtás
A head(N) és limit(N) műveletek szintén a scan-ig kerülnek, és a Polars több fájlt vagy partíciót automatikusan párhuzamosan olvas a rendelkezésre álló CPU-magokon. Vagyis ingyen kapsz többszálú végrehajtást, anélkül hogy bármit konfigurálnod kellene.
A lekérdezési terv vizualizálása: explain() és show_graph()
Ha tényleg meg akarod érteni, mit csinál a Polars (és nem csak hinni a marketingnek), két alapeszközt kell ismerned: az explain() és a show_graph() metódusokat.
lf = (
pl.scan_csv("rendelesek.csv")
.filter(pl.col("status") == "kifizetve")
.group_by("ugyfel_id")
.agg(pl.col("osszeg").sum().alias("teljes_koltes"))
.sort("teljes_koltes", descending=True)
.head(10)
)
# Szöveges, optimalizált terv
print(lf.explain())
# Az optimalizálás ELŐTTI terv
print(lf.explain(optimized=False))
# Graphviz alapú vizualizáció Jupyter-ben
lf.show_graph()
A nem-optimalizált és optimalizált terv összehasonlítása szerintem az egyik leghasznosabb tanulási eszköz a Polarsban – látni fogod a saját szemeddel, hogyan rendezi át az optimalizáló a műveleteidet.
collect(), collect_all() és streaming
A .collect() az alapvető módja annak, hogy a LazyFrame-et eredménnyel rendelkező DataFrame-re konvertáld:
df = lf.collect()
Ha viszont több független lekérdezés ered ugyanabból az alap-LazyFrame-ből, használj collect_all()-t. A Polars ekkor megosztott alkifejezéseket azonosít és párhuzamosan futtatja őket – ez gyakran 30-50% gyorsulást jelent ingyen:
napi = lf.group_by("datum").agg(pl.col("ar").sum())
havi = lf.group_by(pl.col("datum").dt.month()).agg(pl.col("ar").sum())
napi_df, havi_df = pl.collect_all([napi, havi])
Streaming engine memórián túli adatokra
A Polars 1.x-ben a streaming engine alapból bekapcsolt, és batch-enként dolgozza fel az adatokat, így a RAM-nál nagyobb fájlok is kezelhetők:
# Engine kiválasztása explicit módon
df = lf.collect(engine="streaming")
# Vagy environment változóval
# export POLARS_ENGINE_AFFINITY=streaming
Fontos: ha az adatod elfér a RAM-ban, a streaming valójában lassíthat, mert a batchelés overhead-et okoz. A streaming a 10 GB+ tartományban válik egyértelműen nyereséggé. (Ezt a saját bőrömön tanultam meg, miután egy 200 MB-os CSV-n erőltettem.)
sink_parquet és sink_ipc: írás közvetlenül lemezre
Ha a végeredmény túl nagy ahhoz, hogy a memóriában materializálódjon, használj sink_* metódusokat. Ezek a köztes DataFrame létrehozása nélkül streamelik közvetlenül a lemezre az eredményt – ami gyakorlatilag varázslat:
(
pl.scan_csv("100gb_log.csv")
.filter(pl.col("level") == "ERROR")
.group_by("service")
.agg(pl.col("latency_ms").mean())
.sink_parquet(
"errors_by_service.parquet",
compression="zstd", # legjobb arány/sebesség
)
)
A compression paraméter lehetséges értékei: uncompressed, snappy, gzip, lz4, zstd, brotli. Általános javaslat:
- zstd – legjobb tömörítési arány, modern alapértelmezés
- lz4 – ha a sebesség fontosabb, mint a méret
- snappy – ha régi Parquet olvasókkal kell kompatibilisnek lenned
Multiplexed sinks
Egyetlen lekérdezés eredményét egyszerre több célba is írhatod. Hasznos, ha pl. egy belső pipeline és egy külső audit célra is kell ugyanaz az adat:
q1 = lf.sink_parquet("eredmeny.parquet", lazy=True)
q2 = lf.sink_ipc("eredmeny.arrow", lazy=True)
pl.collect_all([q1, q2])
Profilozás: hol van a szűk keresztmetszet?
A .profile() metódus visszaadja az eredmény DataFrame-et és egy második DataFrame-et, amely megmutatja, hogy az egyes csomópontok mennyi időt vettek igénybe (mikroszekundumban):
df, profile = lf.profile()
print(profile)
# Tipikus kimenet:
# ┌─────────────────────┬──────┬──────┐
# │ node ┆ start┆ end │
# │ --- ┆ --- ┆ --- │
# │ str ┆ u64 ┆ u64 │
# ╞═════════════════════╪══════╪══════╡
# │ parquet ┆ 0 ┆ 1240 │
# │ filter ┆ 1240 ┆ 1340 │
# │ aggregate ┆ 1340 ┆ 4120 │
# └─────────────────────┴──────┴──────┘
Ha a parquet csomópont dominál, IO-bound vagy. Érdemes az adatokat újraparticionálni vagy szűkebb sémát használni. Ha viszont az aggregate a fő bűnös, akkor a CPU a szűk keresztmetszet, és máshol kell keresgélned.
Gyakorlati pipeline példa: webshop bevétel-elemzés
Tegyük fel, hogy van három bemeneti fájlod: orders.parquet (50 GB), customers.parquet (2 GB) és products.csv (50 MB). Ki akarod számolni a top 100 ügyfelet országonként, csak a 2026-os rendelésekre. Ismerős feladat?
import polars as pl
orders = pl.scan_parquet("orders.parquet")
customers = pl.scan_parquet("customers.parquet")
products = pl.scan_csv("products.csv")
result = (
orders
.filter(pl.col("order_date").dt.year() == 2026)
.join(products, on="product_id", how="inner")
.with_columns(
(pl.col("quantity") * pl.col("unit_price")).alias("revenue")
)
.group_by("customer_id")
.agg(pl.col("revenue").sum().alias("total_revenue"))
.join(customers.select(["customer_id", "country"]), on="customer_id")
.sort(["country", "total_revenue"], descending=[False, True])
.group_by("country")
.head(100)
)
# Memórián túl: streamelve írjuk Parquet-be
result.sink_parquet(
"top100_by_country_2026.parquet",
compression="zstd",
)
Ez a teljes pipeline 50+ GB nyers adattal is megbízhatóan fut egy 16 GB RAM-os laptopon, mert:
- A
filter(year == 2026)beépül a Parquet scan-be (predicate pushdown) - Csak a szükséges oszlopok kerülnek beolvasásra (projection pushdown)
- A streaming engine batch-enként dolgozza fel az adatokat
- A
sink_parquetnem materializálja a teljes eredményt a memóriában
Gyakori hibák és buktatók
1. read_csv() használata scan_csv() helyett
A leggyakoribb hiba, és én is elkövettem párszor. A read_csv().lazy() minta egyszerűen nem ad pushdown-előnyöket, mert az adat ekkor már a memóriában van.
2. .collect() iteratív hívása ciklusban
Egy gyakori antimintát látunk: emberek ciklusban hívnak .collect()-et, ami minden iterációban újra végrehajtja az egész tervet. Inkább építsd fel a teljes lekérdezést, és egyszer hívj collect()-et a végén. Hidd el, érdemes.
3. UDF (user-defined function) nem optimalizálható
Ha .map_elements()-et használsz Python függvénnyel, az áttöri az optimalizáló terveket, mert a Polars nem tudja, mit csinál a függvény. Mindig próbálj natív Polars kifejezésekkel megoldani mindent – meg fogsz lepődni, milyen sok mindent megengednek:
# ROSSZ – sem párhuzamos, sem optimalizálható
lf.with_columns(
pl.col("nev").map_elements(lambda x: x.upper(), return_dtype=pl.Utf8)
)
# JÓ – natív kifejezés, optimalizálható
lf.with_columns(pl.col("nev").str.to_uppercase())
4. A streaming nem minden műveletet támogat
Bizonyos műveletek (pl. komplex pivotok, néhány join-típus) még nem futnak streaming módban. Ha az engine visszaesik in-memory módba, és a fájl nagyobb, mint a RAM, OOM hibát fogsz kapni. Mindig explain(streaming=True)-vel ellenőrizd, hogy a tervet streaming módban is le tudja-e futtatni a Polars.
Best practice checklist 2026-ra
- Mindig
scan_*-ot használj, neread_*-ot - Filtert és projection-t a lánc elejére tedd – még ha az optimalizáló úgyis átrendezi, az olvasott kód is érthetőbb lesz
- Kerüld a
map_elements-et; használj natív Polars kifejezéseket - Több párhuzamos lekérdezést
collect_all()-lal futtass - Memórián túli outputot mindig
sink_parquet/sink_ipc-vel írj - Új pipeline írásakor mindig nézd meg az
explain()kimenetet - Élesítés előtt futtasd le a
profile()-t reprezentatív adatmennyiségen
Mikor NE használj LazyFrame-et?
Bár a LazyFrame szinte mindig nyer, van pár eset, amikor az eager API jobb választás:
- Exploratív elemzés Jupyter-ben: ha gyors visszajelzést akarsz minden cellára, a
pl.read_csv+ DataFrame egyszerűbb - Nagyon kis adathalmazok (< 100 MB): a tervezési overhead többe kerül, mint amit megtakarít
- Egyszeri, egylépéses transzformációk, ahol egyszerűen nincs mit optimalizálni
FAQ
Mi a különbség a collect() és a sink_parquet() között?
A collect() a lekérdezést memóriában hajtja végre és egy DataFrame-et ad vissza. A sink_parquet() ezzel szemben streamingben futtatja le a lekérdezést, és közvetlenül lemezre írja az eredményt anélkül, hogy a teljes adatot a memóriában materializálná – így használható memórián túli adathalmazokra is.
Mikor érdemes scan_csv-t használni read_csv helyett?
Lényegében mindig, amikor a lazy API-t akarod kihasználni. A scan_csv csak a sémát olvassa be azonnal, és lehetővé teszi, hogy a Polars optimalizálója a predicate és projection pushdownt magára a fájl-olvasásra alkalmazza – így csak a ténylegesen szükséges oszlopok és sorok kerülnek beolvasásra.
Mennyivel gyorsabb a LazyFrame, mint a DataFrame?
Egyszerű egylépéses műveleteknél elhanyagolható a különbség. Összetett, többlépéses pipeline-okon, különösen nagy adathalmazokon viszont tipikusan 2–2.5×-szeres gyorsulás várható, és a memóriahasználat is jelentősen csökken a pushdownok miatt.
Tudja-e a Polars LazyFrame kezelni a memóriánál nagyobb adatokat?
Igen, a streaming engine segítségével. A .collect(engine="streaming") vagy a sink_* metódusok batchelve dolgozzák fel az adatot, így 100 GB-os fájlok is feldolgozhatók egy 16 GB RAM-os gépen. Megjegyzés: nem minden művelet támogatja a streaminget – mindig ellenőrizd explain()-nel.
Mi az explain() kimenet, és hogyan értelmezzem?
Az explain() a Polars optimalizált logikai tervét mutatja. A legfontosabb dolgok, amiket keresel benne: a SELECTION: sor a fájl-scan-ben (predicate pushdown), a PROJECT N/M COLUMNS (projection pushdown), és hogy a műveletek sorrendje a leghatékonyabb-e. Az explain(optimized=False)-szal összevetve látod, mit változtatott rajta az optimalizáló.
Lehet UDF-eket használni LazyFrame-ben?
Lehet, de erősen visszafogottan érdemes. A map_elements Python-szintű függvényt fut soronként, ami áttöri az optimalizáló terveket és nem párhuzamosítható. Mindig próbáld natív Polars kifejezésekkel megoldani – a pl.col().str.*, pl.when().then().otherwise(), és a beépített aggregátorok kombinációjával a logika legnagyobb része kifejezhető.
Összegzés
A Polars LazyFrame nem csak egy „gyorsabb DataFrame" – egy teljes lekérdezés-fordító, amely automatikusan átalakítja a kódodat hatékony végrehajtási tervvé. A négy fő optimalizáció (predicate pushdown, projection pushdown, CSE, párhuzamosítás) együtt 2–10×-es gyorsulást és töredéknyi memóriahasználatot eredményez nagy adathalmazokon, a streaming engine pedig megnyitja az utat a memórián túli adatfeldolgozás felé.
Ha eddig csak az eager API-t használtad, kezdd azzal, hogy a következő pipeline-odban kicseréled a read_csv-t scan_csv-re, a végén pedig .collect()-et hívsz – aztán nézd meg az explain() kimenetet. Garantáltan meglepetés ér.