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.

Megjegyzés

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 GroupBy műveleteknél, kevesebb felesleges allokációval.
  • Bővített LazyFrame API, több string- és lista-műveletet lehet közvetlenül a lazy pipeline-ban használni anélkül, hogy explicit collect()-et kellene hívni köztes lépésekben.
  • Stabilizált IntoLazy implementá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.
Tipp

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:

  1. 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).
  2. Arrow-alapú memóriaelrendezés: kevesebb másolás, jobb cache-locality.
  3. Lazy query optimizer: a felesleges munkát (pl. nem használt oszlopok beolvasását) egyszerűen el sem végzi.
  4. Nincs GIL: a natív Rust kód valódi párhuzamosságot tud kihasználni szálak között.
Figyelem

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.

Jó tudni

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.