Polars 完整教學 2026:pandas 替代品實測與遷移指南

Polars 是 Rust 寫的 DataFrame 引擎,40,309 顆星、MIT 授權、最新版 2.1.0。本文在 2 vCPU/7 GB 機器上用 500 萬列、226 MB CSV 實測:filter+groupby 快 4.7 倍、排序快 6.8 倍、join 快 3.6 倍、記憶體少三成,但讀 CSV 只快 1.2 倍。附 pandas 3 的 CoW 與 Arrow 字串新行為、12 組 pandas→Polars 寫法對照、LazyFrame 查詢優化實例,以及不該換過來的四種情況。

  • Dennis
  • 8 分鐘閱讀
Polars 完整教學 2026:pandas 替代品實測與遷移指南

Polars 是以 Rust 撰寫、採用 Apache Arrow 記憶體格式的 DataFrame 引擎,靠多執行緒與查詢優化器加速。在 2 vCPU、500 萬列的實測中,它的分組聚合比 pandas 快 4.7 倍、排序快 6.8 倍、記憶體少 32%;但讀 CSV 只快 1.2 倍——這正是 2026 年該重新理解 Polars 的地方。

Polars 是什麼?先看基本資料

項目內容
定位高效能 DataFrame 查詢引擎(Rust 核心)
起源2020 年由 Ritchie Vink 發起
授權MIT(商用友善)
GitHubpola-rs/polars,40,309 ★、3,236 fork(2026-11-10 查詢)
最新版本py-2.1.0(2026-10-25 發布)
官方文件docs.pola.rs
語言綁定Python、Rust、Node.js、R
特殊能力LazyFrame 查詢優化、spill-to-disk、SQL 介面、GPU 加速

安裝就一行:

pip install polars

寫法上,一個最小的 lazy 查詢長這樣(這是官方 README 的範例,直接跑得動):

import polars as pl

df = (
    pl.scan_parquet("orders.parquet")
    .filter(pl.col("status") == "shipped")
    .group_by("customer_id")
    .agg(
        pl.col("amount").sum().alias("total"),
        pl.len().alias("n_orders"),
    )
    .sort("total", descending=True)
    .collect()
)

注意三件事:scan_parquet 是延遲讀取(不會立刻載入)、pl.col() 是表達式(不是字串欄名)、最後的 .collect() 才真正執行。這三個習慣換過來,效能差距才吃得滿。

為什麼快?三個機制

  1. Arrow 欄式記憶體:同一欄的資料在記憶體中連續排列,CPU 快取命中率高,也能走 SIMD 向量化指令;跨工具傳遞可零拷貝。
  2. 真多執行緒:pandas 的核心運算本質是單執行緒(要平行得靠 Dask),Polars 預設吃滿所有核心,分組用 lock-free 雜湊避免鎖競爭。
  3. 查詢優化器:LazyFrame 會先建查詢計畫再最佳化。實際跑 .explain() 看得到它做了什麼:
AGGREGATE[maintain_order: false]
  [col("price").sum()] BY [col("city")]
  FROM
  Parquet SCAN [/tmp/pl_bench.parquet]
  PROJECT 2/7 COLUMNS              ← 只讀 2 欄,不是 7 欄
  SELECTION: col("price") > 1000.0 ← 篩選下推到掃描層
  ESTIMATED ROWS: 5000000

PROJECT 2/7 COLUMNS 是 projection pushdown、SELECTION 那行是 predicate pushdown。意思是:檔案有 7 欄,但只讀你要的 2 欄;5,000 萬列裡不符合條件的列,根本不會進記憶體。這是 pandas 做不到的層級——pandas 一定得先全部載入再篩。

實測:500 萬列、2 vCPU 的對照

測試環境:2 vCPU、7 GB RAM 的 VPS(pl.thread_pool_size() 回報 2 條執行緒)、Python 3.12、pandas 3.1.1、polars 2.1.0、pyarrow 26.0.1。資料為 500 萬列 × 7 欄的模擬訂單表,CSV 226.4 MB、轉 Parquet 後 63.4 MB。每項取三次最佳值,避免冷快取誤差。

任務pandas 3.1.1Polars 2.1.0倍率
讀 CSV(226 MB)0.548 s0.452 sPolars 快 1.2 倍
讀 Parquet(63 MB)0.371 s0.071 sPolars 快 5.2 倍
filter + groupby(記憶體內)0.828 s0.178 sPolars 快 4.7 倍
字串 contains 篩選0.453 s0.112 sPolars 快 4.0 倍
join(500 萬 × 8 列維度表)0.979 s0.271 sPolars 快 3.6 倍
排序後取前 1007.673 s1.131 sPolars 快 6.8 倍
Parquet 讀取+groupby 全流程0.534 s0.099 sPolars 快 5.4 倍
資料在記憶體佔用362.4 MB248.0 MBPolars 少 32%

兩個必須說清楚的觀察:

  • 讀 CSV 幾乎追平了。 2024 年那種「Polars 讀 CSV 快 3–5 倍」的說法,在 pandas 3 之後不成立——因為 pandas 3 預設用 Arrow 支撐的字串型別(pd.Series(['a','b']).dtype 現在回 str,不是 object),C 解析器本身也很快。真正拉開差距的是壓縮格式(Parquet 5.2 倍)與計算(4–7 倍)。
  • 排序的 6.8 倍是單執行緒 vs 多執行緒的直觀結果。 pandas 要 7.67 秒,Polars 1.13 秒;pandas 這邊還得先付出記憶體複製成本。

第三方獨立測試的數字方向一致。Uvik Software 在 2026 年 10 月發布的評測(2 vCPU、8 GB RAM、1,000 萬列)測到:groupby 快 5.7 倍(2.86 s vs 16.39 s)、join 快 3.75 倍、讀 CSV 快 1.28 倍、讀 Parquet 快 2.43 倍;把資料放大到 5,000 萬列並限制記憶體 5.84 GiB 時,pandas 直接 OOM 崩潰,Polars 完成 9/10 題。

有趣的是倍率會隨機器放大:Polars 官方在 96 vCPU 機器上跑 PDS-H 測到比 pandas 快 94 倍,而 8 核筆電的社群實測只有約 3.3 倍。核心數越少,差距越接近本文這組數字——這也是為什麼一台 2 vCPU 的小 VPS 上,Polars 是「有感」而不是「神效」。

一個反直覺的實測:Lazy 不是永遠更快

同一份資料,如果已經在記憶體裡,那 lazy 反而慢:

情境時間
pl.read_csv() 後直接 eager groupby0.178 s
pl.scan_csv() → filter → groupby → collect0.837 s
pl.scan_parquet() + groupby(lazy)0.099 s

原因很簡單:scan_csv 每呼叫一次就要重新解析 226 MB 的 CSV,而 eager 版本只付一次解析成本。lazy 的價值在「一次掃描、多層轉換」與壓縮格式:Parquet 加 lazy 只要 0.099 秒,比 pandas 全流程 0.534 秒快 5.4 倍。

實務判斷原則:

  • 手邊的檔案是 CSV/JSON 且只查一次 → eager read_csv 就好。
  • 檔案是 Parquet/多檔 glob/需要連續 3 個以上轉換 → 用 scan_* + lazy。
  • 資料量大於記憶體 → lazy + spill-to-disk(Polars 2.x 預設啟用,超過 RAM 約 80% 就自動溢出到磁碟)。

2026 年的 pandas 也變了,這會影響你的遷移決策

很多「Polars 教學」還停在 pandas 1.x/2.x 的比較,但 pandas 3 已經改了幾個關鍵行為(以下都在 pandas 3.1.1 實測):

項目pandas 3 行為對遷移的意義
Copy-on-Write全面預設;寫入切片不會改到原 DataFrame(實測原 df['b'] 仍是 [4,5,6])記憶體複製變少,pandas 不再是「到處 copy」的代名詞
鏈式賦值df['b'][mask] = 7 完全失效,數值不會寫回(實測 b 仍是 [0,0,0]),並拋 ChainedAssignmentError舊程式碼升級 pandas 3 就會壞,這與 Polars 無關
字串型別預設 str(Arrow 後端),不再是 objectCSV/字串處理的差距被吃掉一大半
to_numpy()預設唯讀,寫入會 ValueError(實測)共用記憶體的舊技巧會踩雷
表達式新增 pd.col()(實測 hasattr(pd,'col') 為 True)語法正在往 Polars 靠攏
Python 版本要求 3.11 以上舊環境得先升級

結論:如果你只是嫌 pandas 慢,先升到 pandas 3 並裝 pyarrow,可能就解決一半問題;如果你要的是多執行緒計算、遠大於記憶體的資料、或確定性的型別,那才是 Polars 的主場。

pandas → Polars:12 組寫法對照

想做什麼pandasPolars
選欄位df[['a','b']]df.select(['a','b'])
條件篩選df[df.a > 10]df.filter(pl.col('a') > 10)
新增欄位df.assign(x=df.a*10)df.with_columns((pl.col('a')*10).alias('x'))
條件賦值df['a'].mask(cond, df['b'])pl.when(cond).then(pl.col('b')).otherwise(pl.col('a')).alias('a')
分組聚合df.groupby('c').agg({'v':'sum'})df.group_by('c').agg(pl.col('v').sum())
分組筆數df.groupby('c').size()df.group_by('c').agg(pl.len())
排序df.sort_values('v')df.sort('v')
前 N 列df.head(100)df.head(100)
欄位改名df.rename(columns={'a':'b'})df.rename({'a':'b'})
串接pd.concat([df1,df2])pl.concat([df1,df2])
視窗函式df.groupby('c')['v'].transform('sum')pl.col('v').sum().over('c')
轉回 pandas—df.to_pandas()

實際跑過的完整範例(含輸出):

import polars as pl

df = pl.DataFrame({
    'city': ['Taipei','Taipei','Taichung','Tainan','Kaohsiung','Taichung'],
    'month': [1,2,1,1,2,2],
    'revenue': [100, 150, 80, 60, 90, 120],
    'orders': [5, 7, 4, 3, 4, 6],
})

# 1) 新增欄位 + 篩選 + 分組 + 排序,一個 lazy 計畫做完
out = (df.lazy()
   .with_columns((pl.col('revenue') / pl.col('orders')).alias('aov'))
   .filter(pl.col('revenue') > 70)
   .group_by('city')
   .agg(pl.col('revenue').sum().alias('rev'),
        pl.col('aov').mean().round(1).alias('aov'),
        pl.len().alias('rows'))
   .sort('rev', descending=True)
   .collect())

輸出:

shape: (3, 4)
┌───────────┬─────┬──────┬──────┐
│ city      ┆ rev ┆ aov  ┆ rows │
│ str       ┆ i64 ┆ f64  ┆ u32  │
╞═══════════╪═════╪══════╪══════╡
│ Taipei    ┆ 250 ┆ 20.7 ┆ 2    │
│ Taichung  ┆ 200 ┆ 20.0 ┆ 2    │
│ Kaohsiung ┆  90 ┆ 22.5 ┆ 1    │
└───────────┴─────┴──────┴──────┘

視窗函式(算每個城市營收占比)也不必 transform 或 apply:

pct = (df.lazy()
   .with_columns(pl.col('revenue').sum().over('city').alias('city_total'))
   .with_columns((pl.col('revenue') / pl.col('city_total') * 100).round(1).alias('pct_of_city'))
   .collect())

Taipei 兩筆得到 40.0 與 60.0,Tainan 只有一筆就是 100.0——結果與 pandas 的 transform('sum') 一致,但寫法短很多。

也可以直接寫 SQL

Polars 2.x 內建 SQL 介面,能與表達式混用(同一個優化器):

import polars as pl

orders = pl.scan_parquet("orders.parquet")
df = (
    pl.sql("SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id")
    .filter(pl.col("total") > 1_000)
    .collect()
)

對已經熟悉 SQL 的團隊,這條路讓遷移成本大幅下降:先用 SQL 把邏輯搬過來,再逐步改寫成表達式。

四個「先不要換」的情況

Polars 不是萬靈丹,以下情況留在 pandas 更省事:

  1. 資料只有幾萬列。 差距在毫秒級,學習成本遠大於節省的時間。
  2. 下游是 scikit-learn、statsmodels、matplotlib 生態。 這些套件的一等公民仍是 pandas/NumPy,中間得靠 to_pandas() 來回轉,反而多一層成本。
  3. 程式大量依賴 Index。 Polars 沒有 index(實測 hasattr(df,'set_index') 為 False),時間序列的 DatetimeIndex、頻率推斷、resample 這些 pandas 慣用技都得改寫成顯式欄位操作。
  4. 需要 apply 跑 Python 自訂函式。 Polars 的 map_elements 是逐列回退到 Python,慢又容易被官方標為「非慣用」;能用表達式改寫才有價值。

另外三個實務坑:

  • 行序不保證。 Polars 2.x 的 streaming 引擎為了吞吐量,join、group_by、unpivot 預設不維護列順序,要順序就得明寫 maintain_order=True。
  • 錯誤訊息風格不同。 表達式型別錯誤會指向整個 expression 而不是某一欄,一開始會不習慣。
  • 型別嚴格。 整數欄有缺值就是 null(欄位仍是整數),不會像 pandas 一樣偷偷轉成 float——這是優點,但舊報表邏輯要重驗。

選型建議:pandas / Polars / DuckDB 三者怎麼分

情境建議理由
探索性分析、資料量 < 100 萬列pandas生態最完整,AI 助理與範例最多
要接 sklearn/statsmodels 建模pandas中間轉換成本最低
ETL、批次處理、> 1,000 萬列Polars多執行緒+lazy+spill-to-disk
資料是 Parquet 資料湖、要跨多檔查詢Polars 或 DuckDB兩者都能只讀需要的欄位
團隊只會 SQL、不想學新 APIDuckDB直接用 SQL 查 Parquet/CSV,不必搬資料進記憶體
要跑在 GPU 上Polars2.x 支援 NVIDIA GPU 加速引擎

同一個站上已有一篇 DuckDB 完整教學,如果你的場景是「對一堆 Parquet 檔跑 SQL」,DuckDB 往往比 Polars 更省事;Polars 的優勢在需要複雜表達式與 Python 端流程控制時。兩者可混用:DuckDB 讀檔、Polars 做後續轉換。

30 分鐘上手路線

  1. pip install polars,把一份現有 CSV 用 pl.read_csv() 讀進來,比對欄位型別。
  2. 把 df[df.a > 10] 改成 df.filter(pl.col('a') > 10),體會表達式寫法。
  3. 找一段最慢的 groupby 腳本,用 scan_parquet + .explain() 改寫,看 PROJECT 與 SELECTION 有沒有出現。
  4. 用 to_pandas() 接回既有繪圖/建模流程,不必一次全搬。
  5. 量測:改寫前後各跑三次取最佳值,差距小於 20% 就別搬了。

站內延伸閱讀

原始來源連結

(本文效能數字為在 2 vCPU/7 GB RAM 機器上的自測結果,資料為 500 萬列模擬訂單表;你的機器核心數與資料型態不同,倍率會不一樣,建議用同一份資料自行驗證。)

📬 訂閱 most.tw 電子報

每週精選 AI 工具教學與技術乾貨,直接送到你的信箱。免費、隨時可退訂。

訂閱即表示同意收到 most.tw 電子報,隨時可一鍵退訂。

💬 有問題想討論?加 LINE 聯絡我

歡迎透過 LINE 官方帳號直接留言,我會盡快回覆你的問題。

加入 LINE 好友