Ομολογώ, την πρώτη φορά που έπιασα στα χέρια μου το Polars ήμουν λίγο σκεπτικός — άλλη μια βιβλιοθήκη DataFrame που υπόσχεται να «νικήσει» το Pandas; Έχω ξανακούσει αυτή την ιστορία. Δύο χρόνια μετά, έχω πια ξεγράψει το Pandas από τα μισά μου pipelines και δεν το έχω μετανιώσει ούτε στιγμή. Γραμμένο σε Rust πάνω στο Apache Arrow, το Polars τρέχει 5 με 30 φορές πιο γρήγορα, καίει λιγότερη μνήμη και υποστηρίζει εγγενώς lazy evaluation και streaming για datasets που δεν χωράνε στη RAM.
Σε αυτόν τον οδηγό θα δούμε γιατί το 2026 περίπου οι μισές data teams έχουν ήδη υιοθετήσει το Polars, σε τι διαφέρει αρχιτεκτονικά από το Pandas, πώς γράφεις εκφραστικό και βελτιστοποιημένο κώδικα, και — ίσως το πιο κρίσιμο — πώς κάνεις σταδιακή μετάβαση χωρίς να σπάσεις pipelines που τρέχουν σε production.
Τι είναι το Polars και γιατί ξεχωρίζει το 2026
Το Polars είναι μια στηλοκεντρική (columnar) βιβλιοθήκη DataFrame, σχεδιασμένη αποκλειστικά για αναλυτικά φορτία εργασίας. Δεν είναι πια το «πειραματικό project» του 2023. Από την έκδοση 1.0 το καλοκαίρι του 2024 ωρίμασε ραγδαία και σήμερα τρέχει σε production σε χρηματοοικονομικά συστήματα, ML έρευνα και ETL μεγάλης κλίμακας.
Σε σύγκριση με το Pandas, αυτά είναι τα βασικά πλεονεκτήματα:
- Παραλληλισμός από προεπιλογή: αξιοποιεί όλους τους πυρήνες της CPU σας χωρίς να γράψετε ούτε μία γραμμή έξτρα κώδικα.
- Lazy API με αυτόματη βελτιστοποίηση query plan (predicate και projection pushdown).
- Streaming engine για datasets μεγαλύτερα από τη διαθέσιμη RAM σας.
- Apache Arrow ως μορφή μνήμης — μειώνει σημαντικά τη χρήση RAM (συνήθως 2–3×) και επιτρέπει zero-copy ανταλλαγή με DuckDB, PyArrow και άλλα Arrow-native εργαλεία.
- Predictable API: χωρίς index, χωρίς
SettingWithCopyWarning, χωρίς ασυνέπειες μεταξύ.locκαι.iloc(επιτέλους).
Για να βάλουμε νούμερα: σε benchmarks 100 εκατομμυρίων γραμμών το 2026, το Polars φιλτράρει σε 1,89 δευτερόλεπτα (5× πιο γρήγορα από Pandas), εκτελεί joins σε 2,59 δευτερόλεπτα (3,6× πιο γρήγορα) και κάνει aggregation σε 1,58 δευτερόλεπτα. Σε ένα CSV των 2 GB, το Pandas θέλει γύρω στα 10 GB RAM ενώ το Polars τα βγάζει πέρα με μόλις 4. Αυτή η διαφορά μνήμης, στην πράξη, σημαίνει ότι μπορείτε να τρέξετε στον φορητό σας πράγματα που πριν χρειάζονταν cluster.
Εγκατάσταση και πρώτα βήματα
Η εγκατάσταση είναι μία απλή εντολή pip. Σας συστήνω την έκδοση 1.x — είναι σταθερή και υποστηρίζει πλήρως το νέο streaming engine.
pip install "polars>=1.20"
# Προαιρετικά: integration με PyArrow και Pandas
pip install pyarrow pandas
Ξεκινάμε δημιουργώντας ένα DataFrame απευθείας από Python dict, ακριβώς όπως στο Pandas, αλλά με σαφές typed schema:
import polars as pl
df = pl.DataFrame({
"πελάτης": ["Α", "Β", "Α", "Γ", "Β"],
"πωλήσεις": [120.0, 85.5, 200.0, 30.0, 150.0],
"ημερομηνία": ["2026-01-05", "2026-01-06", "2026-01-07",
"2026-01-08", "2026-01-09"],
})
print(df)
print(df.schema)
Το print(df.schema) θα σας δείξει ότι η στήλη ημερομηνία παραμένει String. Σε αντίθεση με το Pandas, το Polars δεν προσπαθεί να μαντέψει τύπους — προτιμά την προβλεψιμότητα από τη μαγεία. Παρακάτω θα δούμε πώς γίνεται η μετατροπή με expressions.
Το Expression API: η καρδιά του Polars
Η μεγαλύτερη εννοιολογική αλλαγή σε σχέση με το Pandas είναι η εξής: όλοι οι μετασχηματισμοί γράφονται με expressions. Μια expression περιγράφει τι θέλετε να γίνει σε μια στήλη, χωρίς όμως να εκτελείται άμεσα. Αυτός ο διαχωρισμός είναι που επιτρέπει στο Polars να παραλληλοποιεί και να βελτιστοποιεί τον κώδικά σας.
import polars as pl
df = pl.read_csv("sales.csv")
# Πολλαπλές expressions εκτελούνται παράλληλα
result = df.select([
pl.col("revenue").sum().alias("total_revenue"),
pl.col("revenue").mean().alias("avg_revenue"),
pl.col("customer_id").n_unique().alias("unique_customers"),
(pl.col("revenue") / pl.col("quantity")).mean().alias("avg_unit_price"),
])
print(result)
Παρατηρήστε ότι μέσα στο select έχουμε 4 ανεξάρτητες expressions· το Polars θα τις εκτελέσει σε διαφορετικά threads αυτόματα. Στο Pandas θα χρειαζόμασταν χωριστά passes ή agg dictionaries — και πάντα κάποιος θα ξεχνούσε ένα.
Conditional logic με when/then/otherwise
Ξεχάστε το df.apply(lambda x: ...). Στο Polars δουλεύουμε με διανυσματικές εκφράσεις:
df = df.with_columns(
pl.when(pl.col("revenue") > 1000)
.then(pl.lit("high"))
.when(pl.col("revenue") > 100)
.then(pl.lit("medium"))
.otherwise(pl.lit("low"))
.alias("segment")
)
Αυτή η προσέγγιση είναι 50–100× πιο γρήγορη από ένα Python lambda στο Pandas, επειδή τρέχει εξ ολοκλήρου σε Rust χωρίς να επιστρέφει στον Python interpreter για κάθε γραμμή. Στην πράξη, σε ένα από τα δικά μας pipelines, μετατρέψαμε ένα τέτοιο apply και ο χρόνος έπεσε από 4 λεπτά σε περίπου 3 δευτερόλεπτα.
Lazy API και query optimization
Η lazy API είναι ο σημαντικότερος λόγος που το Polars υπερτερεί σε production pipelines. Αντί να εκτελείται κάθε γραμμή κώδικα άμεσα, χτίζετε έναν logical plan τον οποίο το Polars βελτιστοποιεί πριν την εκτέλεση. Οι κύριες βελτιστοποιήσεις:
- Predicate pushdown: τα φίλτρα μετακινούνται όσο πιο κοντά γίνεται στο read, ώστε να διαβάζονται λιγότερες γραμμές από το δίσκο.
- Projection pushdown: φορτώνονται μόνο οι στήλες που πραγματικά χρησιμοποιούνται από το CSV/Parquet.
- Common subplan elimination: επαναλαμβανόμενοι υπολογισμοί συγχωνεύονται σε έναν.
- Slice pushdown: όταν χρειάζεστε μόνο τα πρώτα Ν αποτελέσματα, η εκτέλεση σταματά νωρίς.
Στην πράξη, αντί για read_csv ξεκινάμε με scan_csv ή scan_parquet:
import polars as pl
q = (
pl.scan_parquet("logs/*.parquet") # δεν διαβάζει ακόμη τίποτα
.filter(pl.col("status") == 200) # predicate
.filter(pl.col("ts") >= pl.lit("2026-04-01")) # δεύτερο predicate
.with_columns(
(pl.col("response_ms") / 1000).alias("response_s")
)
.group_by("endpoint")
.agg([
pl.col("response_s").mean().alias("avg_s"),
pl.col("response_s").quantile(0.95).alias("p95_s"),
pl.len().alias("requests"),
])
.sort("p95_s", descending=True)
)
# Δείτε το βελτιστοποιημένο σχέδιο εκτέλεσης
print(q.explain())
# Εκτέλεση
result = q.collect()
print(result.head(10))
Αν καλέσετε q.explain(), θα δείτε ότι τα δύο φίλτρα έχουν συγχωνευτεί και έχουν πιεστεί στο επίπεδο σάρωσης Parquet. Αποτέλεσμα: το Polars διαβάζει μόνο τα αρχεία και τις σειρές που πραγματικά χρειάζονται. Αυτή η μία βελτιστοποίηση από μόνη της μειώνει συχνά τον χρόνο εκτέλεσης κατά μία τάξη μεγέθους.
Streaming για datasets μεγαλύτερα της RAM
Στο Polars 1.x ενεργοποιείτε streaming με την παράμετρο engine="streaming" στο collect(). Σε αυτή τη λειτουργία τα δεδομένα επεξεργάζονται σε batches και η κατανάλωση μνήμης παραμένει σταθερή — ακόμη και για αρχεία 100+ GB.
q = (
pl.scan_csv("transactions_2026.csv")
.filter(pl.col("amount") > 0)
.group_by(["country", "category"])
.agg(pl.col("amount").sum().alias("total"))
)
# Ενεργοποίηση του streaming engine
result = q.collect(engine="streaming")
Μια σημείωση: ο streaming engine χαρακτηρίζεται ακόμη επίσημα ως unstable, αλλά στην πράξη είναι αρκετά αξιόπιστος για τις περισσότερες περιπτώσεις aggregation και join. Για κρίσιμο production κώδικα, καλό είναι να καρφώνετε την έκδοση Polars στο requirements.txt και να μην βασίζεστε σε auto-upgrades.
Joins, group_by και window functions
Τα joins στο Polars γίνονται με τη μέθοδο .join() και υποστηρίζουν inner, left, full, semi, anti, cross και asof joins. Δείτε ένα παράδειγμα asof join — ιδιαίτερα χρήσιμο σε χρονοσειρές:
trades = pl.DataFrame({
"ts": ["2026-04-01 10:00:00", "2026-04-01 10:00:05",
"2026-04-01 10:00:12"],
"price": [100.1, 100.4, 100.9],
}).with_columns(pl.col("ts").str.to_datetime())
quotes = pl.DataFrame({
"ts": ["2026-04-01 10:00:00", "2026-04-01 10:00:03",
"2026-04-01 10:00:08", "2026-04-01 10:00:11"],
"bid": [100.0, 100.2, 100.5, 100.8],
}).with_columns(pl.col("ts").str.to_datetime())
# Ταιριάζει κάθε trade με το πιο πρόσφατο quote
joined = trades.join_asof(quotes, on="ts", strategy="backward")
print(joined)
Για window functions η σύνταξη είναι πολύ κοντά στην SQL: δηλώνετε over() πάνω σε μια έκφραση.
df = df.with_columns([
pl.col("revenue").sum().over("customer_id").alias("customer_total"),
pl.col("revenue").rank().over("region").alias("rank_in_region"),
pl.col("revenue").mean().over(["region", "month"]).alias("avg_region_month"),
])
Migration από Pandas: αντιστοιχίες και παγίδες
Η μετάβαση από Pandas σε Polars δεν χρειάζεται να γίνει με μία κίνηση. Μπορείτε να μετατρέπετε ελεύθερα DataFrames και προς τις δύο κατευθύνσεις με pl.from_pandas() και .to_pandas(). Ο παρακάτω πίνακας συνοψίζει τις πιο συχνές μεταφράσεις:
| Pandas | Polars |
|---|---|
df.loc[df.x > 0, "y"] | df.filter(pl.col("x") > 0).select("y") |
df.merge(other, on="id") | df.join(other, on="id") |
df.fillna(0) | df.fill_null(0) |
df.dropna() | df.drop_nulls() |
df["x"].apply(f) | pl.col("x").map_elements(f) ή expression |
df.groupby("k").agg({"v":"sum"}) | df.group_by("k").agg(pl.col("v").sum()) |
pd.to_datetime(df["d"]) | pl.col("d").str.to_datetime() |
df.sort_values("x", ascending=False) | df.sort("x", descending=True) |
df.iloc[:10] | df.head(10) ή df[:10] |
Συνηθισμένες παγίδες
- Δεν υπάρχει index: αν στο Pandas στηρίζεστε σε
set_indexκαιreset_index, στο Polars αντιμετωπίζετε απλώς τις στήλες ως δεδομένα. Πιστέψτε με, μετά από λίγο δεν θα σας λείψει. - Όχι in-place mutation: το
df["x"] = ...δεν δουλεύει· χρησιμοποιείτεdf.with_columns()που επιστρέφει νέο DataFrame. - Strict types: οι μικτοί τύποι σε στήλη επιστρέφουν error αντί για object dtype όπως στο Pandas.
- Διαφορετική σημασιολογία NaN/null: το Polars διαχωρίζει null (απουσία τιμής) από NaN (αριθμητική ειδική τιμή). Χρησιμοποιείτε
fill_nullγια το πρώτο καιfill_nanγια το δεύτερο.
Πρακτικό παράδειγμα: ETL pipeline για logs
Ας τα συνδυάσουμε όλα σε ένα πραγματικό ETL pipeline. Θα διαβάσουμε log files Parquet, θα φιλτράρουμε σφάλματα, θα υπολογίσουμε SLO metrics ανά endpoint και θα γράψουμε το αποτέλεσμα σε νέο Parquet:
import polars as pl
from datetime import datetime, timedelta
def build_slo_report(input_glob: str, output_path: str, days: int = 7):
cutoff = datetime.utcnow() - timedelta(days=days)
pipeline = (
pl.scan_parquet(input_glob)
.filter(pl.col("ts") >= cutoff)
.filter(pl.col("status") < 600)
.with_columns([
(pl.col("status") >= 500).alias("is_error"),
(pl.col("response_ms") > 1000).alias("is_slow"),
])
.group_by("endpoint")
.agg([
pl.len().alias("requests"),
pl.col("is_error").mean().alias("error_rate"),
pl.col("is_slow").mean().alias("slow_rate"),
pl.col("response_ms").quantile(0.50).alias("p50_ms"),
pl.col("response_ms").quantile(0.95).alias("p95_ms"),
pl.col("response_ms").quantile(0.99).alias("p99_ms"),
])
.with_columns(
pl.when(pl.col("error_rate") > 0.01)
.then(pl.lit("breach"))
.when(pl.col("p95_ms") > 800)
.then(pl.lit("warn"))
.otherwise(pl.lit("ok"))
.alias("slo_status")
)
.sort("p95_ms", descending=True)
)
pipeline.sink_parquet(output_path)
if __name__ == "__main__":
build_slo_report("logs/2026/*.parquet", "reports/slo_weekly.parquet")
Προσέξτε τη χρήση του sink_parquet αντί για collect().write_parquet(). Το sink_parquet χρησιμοποιεί streaming και γράφει σταδιακά στον δίσκο, οπότε η κατανάλωση μνήμης μένει χαμηλή ακόμη και για τεράστια output.
Συνεργασία με DuckDB και PyArrow
Polars, DuckDB και PyArrow μοιράζονται την ίδια αναπαράσταση μνήμης (Apache Arrow). Πρακτικά, αυτό σημαίνει ότι μπορούν να ανταλλάσσουν δεδομένα χωρίς αντιγραφή. Ένα ισχυρό μοτίβο για το 2026 είναι το hybrid stack: DuckDB για περίπλοκα SQL joins, Polars για expression-based μετασχηματισμούς, Pandas για ML integration στο τελευταίο στάδιο.
import duckdb
import polars as pl
# DuckDB κάνει το βαρύ join σε Parquet δεδομένα
con = duckdb.connect()
arrow_table = con.execute("""
SELECT o.*, c.country
FROM 'orders/*.parquet' o
JOIN 'customers.parquet' c USING (customer_id)
WHERE o.amount > 0
""").arrow()
# Zero-copy μετατροπή σε Polars για περαιτέρω feature engineering
df = pl.from_arrow(arrow_table)
features = (
df.lazy()
.with_columns([
pl.col("amount").log().alias("log_amount"),
pl.col("ts").dt.weekday().alias("weekday"),
])
.group_by(["country", "weekday"])
.agg(pl.col("log_amount").mean())
.collect()
)
Best practices για production
- Προτιμήστε lazy: ξεκινάτε πάντα με
scan_*και κλείνετε μεcollect()ήsink_*. Παίρνετε δωρεάν query optimization. - Φιλτράρετε νωρίς: τοποθετήστε τα
filterόσο πιο νωρίς γίνεται, ώστε να ενεργοποιηθεί το predicate pushdown. - Επιλέξτε μόνο τις απαραίτητες στήλες: το
select/with_columnsνωρίς ενεργοποιεί projection pushdown. - Αποφύγετε το
map_elements: όταν αναγκαστείτε να καλέσετε Python function, τα οφέλη του Rust εξανεμίζονται. Προσπαθήστε πρώτα να εκφράσετε τη λογική με expressions. - Καρφώστε την έκδοση: το API εξελίσσεται γρήγορα. Στο
pyproject.tomlήrequirements.txtγράψτεpolars==1.x.y. - Χρησιμοποιήστε
explain()καιprofile(): το πρώτο για να δείτε το βελτιστοποιημένο σχέδιο, το δεύτερο για να μετρήσετε χρόνους ανά node. - Δοκιμάστε
collect(engine="streaming")για datasets που πλησιάζουν τη RAM σας — συχνά αλλάζει εντελώς τη συμπεριφορά μνήμης.
Συχνές ερωτήσεις (FAQ)
Πρέπει να εγκαταλείψω εντελώς το Pandas;
Όχι. Η σύσταση των περισσότερων ομάδων για το 2026 είναι hybrid προσέγγιση: Polars για ETL και heavy data prep, Pandas για ML integration με scikit-learn ή για διαδραστική εξερεύνηση μικρών datasets. Με τη μέθοδο .to_pandas() η ανταλλαγή είναι ανώδυνη.
Πόσο πιο γρήγορο είναι πραγματικά το Polars από το Pandas;
Εξαρτάται από τη λειτουργία και το μέγεθος. Σε datasets κάτω από 1 GB συχνά δεν θα δείτε τεράστια διαφορά. Σε 10 GB+ workloads, το Polars είναι τυπικά 5–30× πιο γρήγορο, ενώ σε streaming σενάρια όπου το Pandas αποτυγχάνει εντελώς λόγω μνήμης, η σύγκριση γίνεται μάλλον άνευ νοήματος.
Υποστηρίζει το Polars αναπαραστάσεις χωρίς index όπως το Pandas;
Το Polars δεν έχει καθόλου index — κάθε γραμμή προσδιορίζεται από τη θέση της. Αν χρειάζεστε labels, κρατήστε τα ως κανονική στήλη και χρησιμοποιήστε φίλτρα ή joins. Αυτό κάνει τον κώδικα πιο προβλέψιμο και αποφεύγει σιωπηρά bugs που εμφανίζονται με Pandas indexes.
Είναι ώριμο το streaming engine για production;
Η Polars team το χαρακτηρίζει ακόμη ως unstable, αλλά στις περισσότερες περιπτώσεις aggregation, group_by και join δουλεύει σταθερά. Συνιστάται να καρφώνετε την έκδοση Polars, να γράφετε ολοκληρωμένα tests στο pipeline σας και να παρακολουθείτε το changelog για βελτιώσεις.
Πώς ενσωματώνεται το Polars με Jupyter notebooks και visualization;
Τα Polars DataFrames εμφανίζονται μορφοποιημένα στο Jupyter με τύπους ανά στήλη. Για visualization, τα plotly.express και altair δέχονται κατευθείαν Polars DataFrames. Αν χρειαστείτε matplotlib ή seaborn, μπορείτε να καλέσετε .to_pandas() πριν τη γραφική παράσταση.
Μπορώ να χρησιμοποιήσω Polars με scikit-learn;
Από το scikit-learn 1.4+ υπάρχει υποστήριξη για Polars DataFrames ως input. Για παλαιότερες εκδόσεις και για τα περισσότερα estimators, η ασφαλέστερη οδός είναι να καλέσετε .to_numpy() ή .to_pandas() ακριβώς πριν το fit. Στις περισσότερες περιπτώσεις το βαρύ feature engineering γίνεται σε Polars και μόνο το τελικό βήμα μετατρέπεται.
Συμπέρασμα
Το Polars δεν είναι απλώς «γρηγορότερο Pandas». Είναι ένα διαφορετικό φιλοσοφικά εργαλείο που σας βάζει να σκέφτεστε σε όρους query plan και expressions — λίγο σαν να γράφετε SQL, αλλά σε Python. Η σταδιακή υιοθέτησή του το 2026 σας δίνει σημαντικά οφέλη σε επιδόσεις και κατανάλωση μνήμης, ενώ η συνεργασία του με DuckDB και PyArrow ανοίγει νέα μοτίβα αρχιτεκτονικής. Αν χτίζετε ETL pipelines, αναλυτικά reports ή προετοιμασία features για ML, η επένδυση στο Polars αποπληρώνεται γρήγορα. Πραγματικά γρήγορα.