A data science világa hosszú ideig szinte kizárólagosan Python-terep volt: pandas, NumPy, Jupyter — ez volt (és sokaknál ma is ez) a alapfelállás. Az elmúlt két-három évben viszont egyre erősebb kihívóként tűnt fel a Polars, egy Rust-ban írt, de Python (és Node.js) bindingokkal is rendelkező DataFrame könyvtár. Most megjelent a 0.30-as verziója, ami elég sok apró, de fontos újítást hoz — ideje alaposabban megnézni.
Mi az a Polars, és miért fontos a Rust ökoszisztémának
A Polars lényegében egy oszlop-orientált (columnar) DataFrame könyvtár, ami az Apache Arrow memóriaformátumát használja belül. Ez azt jelenti, hogy az adatok cache-barát módon, tömören tárolódnak, a lekérdezések pedig SIMD-vektorizált, multi-threaded módon futnak — méghozzá anélkül, hogy a GIL-hez hasonló korlátokkal kellene küzdeni, mert az egész motor natív Rust kód.
A Rust ökoszisztéma szempontjából ez azért óriási dolog, mert a Polars bebizonyítja: egy performance-kritikus, komplex domain-t (query optimalizáció, lazy evaluation, expression engine) lehet úgy megírni Rust-ban, hogy közben a fejlesztői élmény is rendben marad. A projekt mögött álló csapat (Ritchie Vink és a közösség) folyamatosan tolja előre a határokat, és a 0.30-as release is ezt igazolja.
A Polars natívan is használható Rust projektekből, nem csak Python bindingként. Ha egy backend szolgáltatásodban gyors, in-memory adatfeldolgozásra van szükséged, simán bekötheted a polars crate-et a Cargo.toml-ba.
Mi újdonság a 0.30-ban
A release egyik legszembetűnőbb változása az expression API további bővítése: több string- és dátum-műveletet lehet most már lusta (lazy) kifejezésekként megfogalmazni, ami azt jelenti, hogy a query optimizer jobban tudja átrendezni és összevonni a műveleteket végrehajtás előtt.
Emellett érkeztek:
- Jobb memóriakezelés a
GroupByműveleteknél, kevesebb felesleges allokációval. - Bővített
LazyFrameAPI, több string- és lista-műveletet lehet közvetlenül a lazy pipeline-ban használni anélkül, hogy explicitcollect()-et kellene hívni köztes lépésekben. - Stabilizált
IntoLazyimplementációk több adatforrás-típusra, így egységesebb az API felület eager és lazy módban. - Számos hibajavítás a Parquet és CSV I/O réteg körül, ami különösen nagy fájloknál számít.
Ha eddig eager API-val (DataFrame közvetlen metódusai) dolgoztál, érdemes átgondolni a lazy (LazyFrame) megközelítést — a query optimizer sok esetben jelentős sebességnövekedést hoz ingyen, csak a .lazy() hívást kell hozzáadni.
Gyors start: Cargo.toml és az első DataFrame
Kezdjük egy egyszerű példával. A Cargo.toml-ban a következő függőségre lesz szükség:
[dependencies]
polars = { version = "0.30", features = ["lazy", "csv-file"] }
Ezután már írhatunk is egy alap DataFrame-et a df! makróval:
use polars::prelude::*;
fn main() -> PolarsResult<()> {
let df = df! [
"nev" => ["Anna", "Bela", "Cecil", "Dora"],
"kor" => [23, 45, 31, 29],
"varos" => ["Budapest", "Szeged", "Pecs", "Budapest"],
]?;
println!("{}", df);
// Szűrés: csak a budapestiek, kor szerint rendezve
let szurt = df
.clone()
.lazy()
.filter(col("varos").eq(lit("Budapest")))
.sort("kor", SortOptions::default())
.collect()?;
println!("{}", szurt);
Ok(())
}
Ez a kód a mai (2023.05.03-i) Rust 1.69-cel és Polars 0.30-cal simán fordul és lefut. Figyeld meg, hogy a .lazy() hívás után col() és lit() segítségével építjük fel a kifejezéseket — ez a Polars expression API lényege, és ez az, amit a query optimizer aztán hatékonyan tud átalakítani.
GroupBy és aggregáció lazy módban
A valódi erő a csoportosításban és aggregációban mutatkozik meg. Nézzünk egy tipikus "átlagár termékkategóriánként" jellegű lekérdezést:
use polars::prelude::*;
fn atlag_kategorianta(df: DataFrame) -> PolarsResult<DataFrame> {
let eredmeny = df
.lazy()
.groupby([col("kategoria")])
.agg([
col("ar").mean().alias("atlag_ar"),
col("ar").max().alias("max_ar"),
col("kategoria").count().alias("darabszam"),
])
.sort("atlag_ar", SortOptions {
descending: true,
..Default::default()
})
.collect()?;
Ok(eredmeny)
}
Ez a fajta pipeline-építés nagyon hasonlít a pandas groupby().agg() szintaxisára, de a háttérben a Polars lusta módon optimalizálja a végrehajtási tervet — például eldöntheti, hogy a szűrést a groupby elé tolja, vagy hogy melyik oszlopokat egyáltalán nem kell beolvasnia a lemezről, ha azok nem szükségesek az eredményhez (ún. projection pushdown).
Teljesítmény: Polars vs pandas
A sokat emlegetett benchmarkok (pl. a H2O.ai adatbázis-jellegű groupby tesztek) rendszeresen azt mutatják, hogy a Polars nagyobb adathalmazokon (több millió sor felett) 2-10x gyorsabb tud lenni a pandas-nál, különösen groupby és join műveleteknél, többek között az alábbi okok miatt:
- Multi-threading alapból: a Polars a rendelkezésre álló CPU magokat automatikusan kihasználja, míg a pandas alapból single-threaded (hacsak nem vonsz be külön eszközt, mint a Dask).
- Arrow-alapú memóriaelrendezés: kevesebb másolás, jobb cache-locality.
- Lazy query optimizer: a felesleges munkát (pl. nem használt oszlopok beolvasását) egyszerűen el sem végzi.
- Nincs GIL: a natív Rust kód valódi párhuzamosságot tud kihasználni szálak között.
A benchmark-számok mindig kontextusfüggőek. Kis adathalmazoknál (néhány ezer sor) a különbség gyakran elhanyagolható, sőt a pandas ökoszisztéma (matplotlib, seaborn integrációk) érettsége miatt sok esetben még mindig praktikusabb választás lehet — főleg exploratory data analysis (EDA) fázisban.
Érdemes megemlíteni azt is, hogy a Polars hibakezelése tipikusan Result-alapú, ami Rust-ban természetes módon illeszkedik a nyelv filozófiájába. Egy gyors CSV-beolvasás hibakezelése például így nézhet ki, kihasználva a let-else szintaxist (ami az 1.65 óta stabil):
use polars::prelude::*;
fn biztonsagos_beolvasas(path: &str) -> Option<DataFrame> {
let Ok(df) = CsvReader::from_path(path).and_then(|r| r.finish()) else {
eprintln!("Nem sikerult beolvasni a fajlt: {path}");
return None;
};
Some(df)
}
Ez a minta jól mutatja, hogy a Rust nyelvi feature-ök (mint a let-else) mennyire jól passzolnak a Polars API stílusához — kevesebb boilerplate, tisztább hibakezelés.
Merre tart a Rust a data science világában
A Polars sikere nem elszigetelt jelenség. Egyre több Rust-alapú eszköz jelenik meg az adattudomány perifériáján is: a ndarray a NumPy-szerű tömbműveletekhez, a linfa gépi tanulási algoritmusokhoz, vagy éppen a arrow-rs az Apache Arrow natív implementációjaként. Ezek együtt kezdenek egy olyan ökoszisztémát alkotni, ami hosszú távon valódi alternatívát jelenthet a Python-stack egyes részeihez — különösen ott, ahol a teljesítmény és a memóriahatékonyság kritikus.
Fontos ugyanakkor tisztán látni: a Rust nem fogja "lecserélni" a Pythont a data science-ben rövid távon. A Python ökoszisztéma érettsége, a vizualizációs eszközök, a Jupyter notebook-alapú munkafolyamat, és a hatalmas közösségi tudásbázis olyan előnyök, amiket nehéz gyorsan pótolni. Amit viszont látunk, az egy egyre erősödő hibrid modell: Python a magas szintű orchestration-höz és vizualizációhoz, Rust (Polars-on keresztül) pedig a nehéz adatfeldolgozási munkához a háttérben.
Ha Rust fejlesztőként szeretnél belekóstolni az adatelemzésbe, a Polars kiváló belépési pont, mert nem kell Python-t tanulnod hozzá — a teljes API natívan elérhető Rust-ból, típusbiztos, és a cargo ökoszisztéma minden előnyét élvezheted (verziókezelés, tesztelés, dokumentáció generálás).
Összefoglalás
A Polars 0.30 egy szolid, inkrementális lépés a projekt fejlődésében: bővült az expression API, javult a memóriakezelés, és stabilizálódtak a lazy evaluation körüli funkciók. Ami igazán izgalmas, az a nagyobb kép: a Rust lassan, de biztosan beépül a data science eszköztárba, és a Polars az egyik legjobb bizonyíték arra, hogy egy performance-kritikus, komplex domain-specifikus könyvtárat lehet úgy megírni Rust-ban, hogy közben a fejlesztői élmény is versenyképes maradjon a Python-alapú alternatívákkal szemben. Ha még nem próbáltad, most van itt az ideje — a cargo add polars --features lazy,csv-file egy jó kiindulópont egy délutáni kísérletezéshez.