Polars vs Pandas 演算法交易實戰基準測試

Polars 與 Pandas 在演算法交易任務上的詳細對比:過濾、聚合、滾動訊號計算、I/O 和記憶體消耗的基準測試。Polars + Numba 混合架構實現最大回測效能。

系列文章"回測無幻覺",第9篇

策略回測不僅僅是訊號邏輯和執行模擬。它還是一個數據管道:載入數百萬根K線、重取樣時間框架、計算指標、按條件過濾、按標的分組。當管道執行需要30秒而不是3秒時,這不僅僅是不便。這意味著每小時少做10倍的實驗、10倍更慢的迭代、從想法到生產的路徑延長10倍。

Pandas 是 Python 中處理表格資料的事實標準。但 Pandas 設計於2008年,當時 CPU 核心更慢,資料集更小。Pandas 是單執行緒的,記憶體消耗大,且缺少查詢最佳化器。Polars 是用 Rust 編寫的新一代庫,具有並行執行能力,以 Apache Arrow 為核心,並帶有惰性查詢規劃器。

問題是:Polars 在真實演算法交易任務上到底快多少?不是 README 中的合成基準測試,而是在 tick 過濾、滾動指標計算、按標的分組以及從 Parquet/QuestDB 載入資料的場景中?

本文提供系統的基準測試,包含資料、程式碼和實踐建議。

基準測試方法論

基準測試環境 未來感精密測量實驗室:受控參數的可復現基準測試環境

在比較之前,讓我們定義規則,確保結果可復現且公平。

環境

  • Python 3.11、Pandas 2.2、Polars 1.x(最新穩定版本)
  • 機器:8核、32 GB RAM、NVMe SSD
  • 每個基準測試執行100次,取中位數
  • 預熱(warmup):測量前5次迭代
  • 測量期間停用 GC(gc.disable())

資料

三個規模級別:

  • 小型:10K 行(單個標的,一天,分鐘K線)
  • 中型:1M 行(單個標的,約2年,分鐘K線)
  • 大型:10M+ 行(100個標的,2年,分鐘K線)

額外:真實 NYC Taxi 資料集(1270萬行)用於 ETL 基準測試 — 行業標準基準。

測量內容

import timeit, gc

def bench(fn, n=100, warmup=5):
    """公平基准测试:预热 + n 次运行的中位数。"""
    for _ in range(warmup):
        fn()
    gc.disable()
    times = timeit.repeat(fn, number=1, repeat=n)
    gc.enable()
    return {
        "median_ms": sorted(times)[n // 2] * 1000,
        "p95_ms": sorted(times)[int(n * 0.95)] * 1000,
    }

按操作分類的基準測試:表格

操作基準測試對比 不同資料規模下 filter、groupby、join 和 select 操作的效能對比

小型資料集(10K 行)

操作Pandas (ms)Polars (ms)加速比
Filter0.180.320.56x
GroupBy1.20.751.6x
Join5.50.413.75x
Select0.50.22.5x

在10K行資料上,Pandas 在簡單過濾時有時更快 — 通過 PyO3 呼叫 Polars 函數的開銷與操作本身的時間相當。但在 join 操作上,優勢已經顯現:Polars 的 Rust 雜湊表比 Pandas 快13倍。

中型資料集(1M 行)

操作Pandas (ms)Polars (ms)加速比
Filter12.47.81.6x
GroupBy45.228.61.6x
Join89.014.36.2x
Select21.82.010.9x

在百萬行資料上,Polars 在過濾和分組上穩定快1.6倍。在 select(選擇列子集)上快10.9倍,因為 Arrow 列式格式允許零複製切片。

大型資料集(10M+ 行)

操作Pandas (ms)Polars (ms)加速比
Filter185503.7x
GroupBy8601008.6x
Join145012012.1x
Select240406.0x

在大數據上,Polars 的優勢呈非線性增長:8核上的並行執行和查詢最佳化器產生累積效果。GroupBy 加速8.6倍 — 這是"等待一秒"和"等待100毫秒"之間的區別。

真實資料 ETL(NYC Taxi,1270萬行)

操作Pandas (s)Polars (s)加速比
CSV 載入28.51.1425.0x
Filter + GroupBy + Agg3.80.429.0x
多列轉換2.10.73.0x
完整 ETL 管道34.42.2615.2x

CSV I/O 是最引人注目的結果:Polars 在 Rust 引擎上並行讀取 CSV,快25倍。這對於歷史資料的初始載入至關重要。

官方 PDS-H 基準測試(2025年5月)

PDS-H 基準測試排行榜 DataFrame 庫效能競賽:Polars 和 DuckDB 遙遙領先,Pandas 落後數個數量級

PDS-H(Performance Data Science — Holistic)是 DataFrame 庫的標準基準測試,類似於資料庫的 TPC-H。2025年5月的結果:

  • Pandas 僅參與 SF-10 規模的測試 — 單執行緒,無查詢最佳化器,比領先者慢兩個數量級
  • Polars 和 DuckDB 在 SF-10 和 SF-100 上遙遙領先
  • Polars 的新流式引擎相比記憶體模式提供額外3-7倍加速 — 可以處理不適合 RAM 的資料

對於演算法交易,這意味著:如果您的管道在載入1億+行 tick 資料時遇到記憶體瓶頸 — Polars 流式引擎可以在不增加 RAM 的情況下處理它們。

交易訊號的滾動計算:殺手級特性

Polars vs Pandas rolling speedup comparison

這是演算法交易最重要的基準測試。典型任務:您有100個標的,需要為每個計算滾動均值、滾動標準差、z-score,並據此生成訊號。在 Pandas 中是 groupby().rolling(),在 Polars 中是 group_by().agg(col().rolling_mean())。

Pandas: groupby + rolling

import pandas as pd
import numpy as np

# 1000万行:100个标的 x 10万根K线
df_pd = pd.DataFrame({
    "ticker": np.repeat([f"TICKER_{i}" for i in range(100)], 100_000),
    "close": np.random.randn(10_000_000).cumsum() + 100,
    "volume": np.random.randint(100, 10000, 10_000_000),
})

# 按组计算滚动 z-score
def pandas_rolling_signals(df):
    grouped = df.groupby("ticker")["close"]
    df["ma_20"] = grouped.transform(lambda x: x.rolling(20).mean())
    df["std_20"] = grouped.transform(lambda x: x.rolling(20).std())
    df["zscore"] = (df["close"] - df["ma_20"]) / df["std_20"]
    return df

Polars: group_by + 滾動表示式

import polars as pl

df_pl = pl.DataFrame({
    "ticker": np.repeat([f"TICKER_{i}" for i in range(100)], 100_000),
    "close": np.random.randn(10_000_000).cumsum() + 100,
    "volume": np.random.randint(100, 10000, 10_000_000),
})

# 按组计算滚动 z-score — 单个表达式链
def polars_rolling_signals(df):
    return df.with_columns([
        pl.col("close")
            .rolling_mean(window_size=20)
            .over("ticker")
            .alias("ma_20"),
        pl.col("close")
            .rolling_std(window_size=20)
            .over("ticker")
            .alias("std_20"),
    ]).with_columns(
        ((pl.col("close") - pl.col("ma_20")) / pl.col("std_20"))
            .alias("zscore")
    )

結果

操作Pandas (ms)Polars (ms)加速比
滾動均值,100組 x 10萬行420012350x
滾動標準差,100組 x 10萬行510015340x
Z-score(均值 + 標準差 + 算術運算)1250035357x
滾動均值,1000組 x 1萬行38000113454x

按組滾動計算獲得 10倍到3500倍的加速。這不是筆誤。Pandas 的 groupby().transform(lambda x: x.rolling().mean()) 對每個組建立 Python 迴圈,每次呼叫都有直譯器開銷。Polars 在 Rust 中執行所有操作,跨組並行,沒有中間 Python 物件。

對於需要為100個標的計算10個指標的管道 — 這是2分鐘和0.3秒之間的區別。

技術指標:布林帶、肯特納通道、TTM Squeeze

技術指標視覺化 布林帶和肯特納通道包裹價格序列,TTM Squeeze 區域高亮顯示

讓我們來看交易策略中使用的真實技術指標的計算。

布林帶

Upper=SMA(close,n)+k⋅σ(close,n)\text{Upper} = \text{SMA}(close, n) + k \cdot \sigma(close, n) Lower=SMA(close,n)−k⋅σ(close,n)\text{Lower} = \text{SMA}(close, n) - k \cdot \sigma(close, n)

Pandas 實現

def bollinger_pandas(df, period=20, k=2.0):
    df["bb_mid"] = df["close"].rolling(period).mean()
    df["bb_std"] = df["close"].rolling(period).std()
    df["bb_upper"] = df["bb_mid"] + k * df["bb_std"]
    df["bb_lower"] = df["bb_mid"] - k * df["bb_std"]
    return df

Polars 實現

def bollinger_polars(df, period=20, k=2.0):
    return df.with_columns([
        pl.col("close").rolling_mean(window_size=period).alias("bb_mid"),
        pl.col("close").rolling_std(window_size=period).alias("bb_std"),
    ]).with_columns([
        (pl.col("bb_mid") + k * pl.col("bb_std")).alias("bb_upper"),
        (pl.col("bb_mid") - k * pl.col("bb_std")).alias("bb_lower"),
    ])

肯特納通道

Upper=EMA(close,n)+k⋅ATR(n)\text{Upper} = \text{EMA}(close, n) + k \cdot \text{ATR}(n) Lower=EMA(close,n)−k⋅ATR(n)\text{Lower} = \text{EMA}(close, n) - k \cdot \text{ATR}(n)

其中 ATR(平均真實範圍):

TR=max⁡(high−low,  ∣high−closeprev∣,  ∣low−closeprev∣)\text{TR} = \max(high - low, \; |high - close_{prev}|, \; |low - close_{prev}|)

ATR(n)=EMA(TR,n)\text{ATR}(n) = \text{EMA}(\text{TR}, n)

TTM Squeeze

TTM Squeeze 是一種識別市場從擠壓狀態(低波動性)向擴充狀態過渡的方法。當布林帶位於肯特納通道內部時產生訊號:

squeeze=BBlower>KClower  ∧  BBupper<KCupper\text{squeeze} = \text{BB}_{lower} > \text{KC}_{lower} \;\land\; \text{BB}_{upper} < \text{KC}_{upper}

技術指標基準測試(1M 行,單個標的)

指標Pandas (ms)Polars (ms)加速比
布林帶 (20, 2)8.41.27.0x
肯特納通道 (20, 1.5)14.22.16.8x
TTM Squeeze (完整)28.64.17.0x
RSI (14)6.81.16.2x
MACD (12, 26, 9)5.20.86.5x

單個標的上穩定獲得 約7倍 加速。按組計算(100個標的)時,由於 Pandas groupby 開銷,加速倍數增長到數百倍。

注意:現成的指標包

Pandas 有 pandas-ta — 包含130+指標的庫。Polars 目前還沒有等效的包。這意味著使用 Polars 時,您需要自行實現指標。不過,基本構建塊(rolling_mean、rolling_std、ewm_mean、shift、列算術運算)覆蓋了絕大多數標準指標,而且 Polars 實現通常比想像的更簡短。

I/O 基準測試:CSV、Parquet、資料庫

資料 I/O 管道視覺化 來自 CSV、Parquet 和資料庫的資料流:並行 Rust I/O 對比單執行緒 Python

資料管道從載入資料開始。儲存格式和讀取方式決定了整個管道的基準速度。

CSV

# Pandas
df_pd = pd.read_csv("candles_10m.csv")

# Polars
df_pl = pl.read_csv("candles_10m.csv")

# Polars 惰性模式 — 仅读取需要的列
df_pl_lazy = (
    pl.scan_csv("candles_10m.csv")
    .select(["timestamp", "close", "volume"])
    .filter(pl.col("volume") > 1000)
    .collect()
)

Parquet

# Pandas(通过 PyArrow)
df_pd = pd.read_parquet("candles_10m.parquet")

# Polars(原生 Arrow)
df_pl = pl.read_parquet("candles_10m.parquet")

# Polars 惰性模式 — 谓词下推 + 投影下推
df_pl_lazy = (
    pl.scan_parquet("candles_10m.parquet")
    .select(["timestamp", "close", "volume"])
    .filter(pl.col("volume") > 1000)
    .collect()
)

I/O 結果(1000萬行,6列)

操作Pandas (s)Polars (s)加速比
CSV 讀取28.51.1425.0x
CSV 寫入42.02.815.0x
Parquet 讀取(所有列)0.820.312.6x
Parquet 讀取(6列中的3列)0.540.124.5x
Parquet 寫入0.950.911.04x
Parquet 惰性(過濾 + 選擇)N/A0.08謂詞下推

關鍵結論:

  1. CSV:Polars 快達25倍 — Rust 中的並行解析
  2. Parquet 讀取:Polars 全量讀取快2.6倍,投影下推(僅讀取需要的列)快4.5倍
  3. Parquet 寫入:幾乎相同 — 兩者都使用 PyArrow/Arrow 後端
  4. 惰性掃描:Polars 可以在 Parquet 檔案的行組級別應用過濾器,而無需將資料載入到記憶體中。對於 Pandas,不手動使用 PyArrow 就無法實現這一點

對於 Parquet 快取 — 我們儲存預計算時間框架和指標的主要格式 — Polars 的惰性求值提供了理想的整合:僅載入需要的列和時間段,而無需將整個檔案讀入記憶體。

記憶體消耗與惰性求值

記憶體消耗與惰性求值 急切模式 vs 惰性模式:橙色的冗餘資料副本對比青色的最佳化 Arrow 列式佈局

急切模式 vs 惰性模式

Pandas 僅在急切模式下工作:每個操作立即執行,中間結果被例項化到記憶體中。

# Pandas:每行都在内存中创建新的 DataFrame
df = pd.read_csv("big_file.csv")           # 整个文件加载到 RAM
df = df[df["volume"] > 1000]                # 过滤后的副本
df = df[["timestamp", "close", "volume"]]   # 又一个副本
df["returns"] = df["close"].pct_change()    # 再一个副本

Polars 支援惰性求值 — 查詢構建為計算圖,經過最佳化後一次性執行:

# Polars 惰性模式:单次遍历,最小内存消耗
result = (
    pl.scan_csv("big_file.csv")
    .filter(pl.col("volume") > 1000)
    .select(["timestamp", "close", "volume"])
    .with_columns(
        pl.col("close").pct_change().alias("returns")
    )
    .collect()
)

Polars 最佳化器自動完成:

  • 投影下推:僅讀取3列而非全部
  • 謂詞下推:在讀取時應用 volume > 1000 過濾器,不載入不需要的行
  • 公共子表示式消除:避免重複計算相同內容

記憶體消耗(1000萬行,6個 float64 列)

場景Pandas (GB)Polars 急切 (GB)Polars 惰性 (GB)
CSV 載入0.920.460.46
Filter + Select 3列1.38*0.220.22
5步轉換管道2.76*0.480.48
Parquet 載入(6列中的3列)0.460.230.23

* Pandas 建立中間副本;inplace=True 部分有幫助,但並非對所有操作有效。

Polars 原生使用 Arrow 列式格式:資料按列儲存,行不重複,儘可能使用零複製操作。對於包含多個轉換的管道,Polars 消耗的記憶體少2-6倍。

流式引擎:處理超出 RAM 的資料

對於不適合 RAM 的資料集,Polars 提供流式引擎:

# 在16GB RAM 的机器上处理100GB数据
result = (
    pl.scan_parquet("huge_dataset/*.parquet")
    .filter(pl.col("exchange") == "binance")
    .group_by("ticker")
    .agg([
        pl.col("close").mean().alias("avg_close"),
        pl.col("volume").sum().alias("total_volume"),
    ])
    .collect(engine="streaming")
)

流式引擎分塊處理資料,無需將整個資料集載入到記憶體中。根據 PDS-H 基準測試資料,流式模式在大規模上比記憶體模式快3-7倍 — 得益於更好的快取區域性性和沒有虛擬記憶體壓力。

混合架構:Polars + Numba

Hybrid Polars + Numba architecture data flow

回測由兩個本質不同的部分組成:

  1. 資料管道 — 載入、轉換、指標、過濾。這是大規模並行、面向列的,完美適合 Polars。

  2. 投資組合模擬 — 訂單成交、PnL 計算、倉位管理。這是路徑依賴的:每一步都取決於前一個狀態。這需要對時間序列進行逐元素遍歷。

Pandas 對這兩部分都不擅長。Polars 擅長第一部分,但不擅長第二部分。對於路徑依賴邏輯,最優工具是 Numba(Python 的 JIT 編譯器)或原生 Rust/C++。

架構

┌─────────────────────────────────────────────────────┐
│                     数据管道                          │
│                                                      │
│  Parquet/QuestDB  ──→  Polars LazyFrame             │
│       │                     │                        │
│       │              ┌──────┴──────┐                 │
│       │              │   指标      │                 │
│       │              │   过滤器    │                 │
│       │              │   特征      │                 │
│       │              └──────┬──────┘                 │
│       │                     │                        │
│       │              NumPy 数组                      │
│       │              (从 Arrow 零拷贝)              │
│       ▼                     ▼                        │
│  ┌──────────────────────────────────────────────┐   │
│  │        投资组合模拟 (Numba)                     │   │
│  │                                                │   │
│  │  @njit                                         │   │
│  │  def simulate(prices, signals, params):        │   │
│  │      position = 0.0                            │   │
│  │      pnl = 0.0                                 │   │
│  │      for i in range(len(prices)):              │   │
│  │          if signals[i] > threshold:            │   │
│  │              position = 1.0                    │   │
│  │          elif signals[i] < -threshold:         │   │
│  │              position = -1.0                   │   │
│  │          pnl += position * (prices[i] - ...)   │   │
│  │      return pnl                                │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

示例:完整管道

import polars as pl
import numpy as np
from numba import njit

# ── 第1步:Polars 数据管道 ──────────────────────────
df = (
    pl.scan_parquet("cache_ETHUSDT_2024_2026.parquet")
    .filter(pl.col("timestamp").is_between(start, end))
    .with_columns([
        pl.col("close")
            .rolling_mean(window_size=20)
            .alias("ma_fast"),
        pl.col("close")
            .rolling_mean(window_size=50)
            .alias("ma_slow"),
        pl.col("close")
            .rolling_std(window_size=20)
            .alias("volatility"),
    ])
    .with_columns(
        ((pl.col("ma_fast") - pl.col("ma_slow")) / pl.col("volatility"))
            .alias("signal")
    )
    .collect()
)

# ── 第2步:零拷贝传输到 NumPy ───────────────────────
prices = df["close"].to_numpy()    # 从 Arrow 零拷贝
signals = df["signal"].to_numpy()  # 从 Arrow 零拷贝

# ── 第3步:Numba 投资组合模拟 ───────────────────────
@njit
def simulate_strategy(prices, signals, threshold=1.5, stop_loss=0.02):
    """
    路径依赖模拟:Numba 编译为机器码。
    100万次迭代耗时70-100ms。
    """
    n = len(prices)
    equity = np.empty(n)
    equity[0] = 1.0
    position = 0.0
    entry_price = 0.0

    for i in range(1, n):
        # 检查止损
        if position != 0.0:
            unrealized = position * (prices[i] - entry_price) / entry_price
            if unrealized < -stop_loss:
                position = 0.0

        # 入场信号
        if position == 0.0:
            if signals[i] > threshold:
                position = 1.0
                entry_price = prices[i]
            elif signals[i] < -threshold:
                position = -1.0
                entry_price = prices[i]

        # PnL
        ret = (prices[i] - prices[i - 1]) / prices[i - 1]
        equity[i] = equity[i - 1] * (1.0 + position * ret)

    return equity

equity = simulate_strategy(prices, signals)

為什麼不用 vectorbt?

vectorbt 是一個流行的回測框架,可在70-100ms內處理100萬筆訂單。它基於 Pandas + NumPy + Numba 構建。問題在於:Pandas 是資料管道的瓶頸 — 慢、單執行緒、記憶體消耗大。vectorbt 不得不通過 Numba 繞過 Pandas 的限制來處理關鍵部分,但資料載入和指標計算仍然通過 Pandas 進行。

Polars + Numba 混合架構取兩者之長:

  • Polars 用於資料管道 — 在相同操作上比 Pandas 快5-350倍
  • Numba 用於投資組合模擬 — 與 vectorbt 中的速度相同
  • 沒有中間 Pandas 層 — 資料通過零複製直接從 Arrow 流向 NumPy

遷移:從 Pandas 到 Polars 的關鍵模式

從 Pandas 到 Polars 的遷移 連線遺留程式碼與現代程式碼的橋樑:將 Pandas 模式轉換為 Polars 表示式

如果您的管道是用 Pandas 編寫的,遷移不需要從頭重寫。主要模式可以按模板轉換。

讀取資料

# Pandas
df = pd.read_parquet("data.parquet")
df = pd.read_csv("data.csv", parse_dates=["timestamp"])

# Polars
df = pl.read_parquet("data.parquet")
df = pl.read_csv("data.csv", try_parse_dates=True)

# Polars 惰性模式(推荐)
df = pl.scan_parquet("data.parquet")  # 在 .collect() 之前不读取任何内容

過濾

# Pandas
df_filtered = df[df["volume"] > 1000]
df_filtered = df[(df["close"] > 100) & (df["exchange"] == "binance")]

# Polars
df_filtered = df.filter(pl.col("volume") > 1000)
df_filtered = df.filter(
    (pl.col("close") > 100) & (pl.col("exchange") == "binance")
)

建立列

# Pandas
df["returns"] = df["close"].pct_change()
df["log_returns"] = np.log(df["close"] / df["close"].shift(1))

# Polars
df = df.with_columns([
    pl.col("close").pct_change().alias("returns"),
    (pl.col("close") / pl.col("close").shift(1)).log().alias("log_returns"),
])

GroupBy + 聚合

# Pandas
result = df.groupby("ticker").agg(
    avg_close=("close", "mean"),
    total_volume=("volume", "sum"),
    trade_count=("close", "count"),
)

# Polars
result = df.group_by("ticker").agg([
    pl.col("close").mean().alias("avg_close"),
    pl.col("volume").sum().alias("total_volume"),
    pl.col("close").count().alias("trade_count"),
])

按組滾動計算

# Pandas(慢:Python 循环遍历各组)
df["ma_20"] = df.groupby("ticker")["close"].transform(
    lambda x: x.rolling(20).mean()
)

# Polars(快:Rust 中并行执行)
df = df.with_columns(
    pl.col("close")
        .rolling_mean(window_size=20)
        .over("ticker")
        .alias("ma_20")
)

與 QuestDB 整合

Polars 原生支援 Apache Arrow — 與 QuestDB 用於資料傳輸的格式相同。這意味著接收查詢結果時零複製:

import pyarrow as pa
from questdb.ingress import Sender

# 从 QuestDB 读取 → Arrow → Polars(零拷贝)
arrow_table = questdb_connection.query_arrow(
    "SELECT * FROM candles WHERE ticker = 'ETHUSDT'"
)
df = pl.from_arrow(arrow_table)  # 零拷贝!

# 对比 Pandas:Arrow → Pandas(拷贝 + 类型转换)
df_pd = arrow_table.to_pandas()  # 拷贝 + 类型转换

有關使用 QuestDB 儲存和分析交易資料的更多資訊,請參閱我們的資料架構系列文章。

與 Parquet 快取的整合

Parquet 快取架構 列式 Parquet 快取:通過謂詞下推和投影下推實現選擇性資料載入

在文章聚合 Parquet 快取中,我們描述瞭如何預計算時間框架和指標一次並儲存到 Parquet 檔案中。Polars 使這種方法更加高效:

# 使用 Polars 创建缓存 — 比 Pandas 快5-10倍
cache = (
    pl.scan_parquet("raw_candles_1m.parquet")
    .with_columns([
        # 模拟更高时间框架
        pl.col("close")
            .rolling_mean(window_size=60)
            .alias("ma_1h"),
        pl.col("close")
            .rolling_mean(window_size=240)
            .alias("ma_4h"),
        # 布林带
        pl.col("close")
            .rolling_mean(window_size=20)
            .alias("bb_mid"),
        pl.col("close")
            .rolling_std(window_size=20)
            .alias("bb_std"),
    ])
    .with_columns([
        (pl.col("bb_mid") + 2.0 * pl.col("bb_std")).alias("bb_upper"),
        (pl.col("bb_mid") - 2.0 * pl.col("bb_std")).alias("bb_lower"),
    ])
    .collect()
)

# 保存 — 写入时间大致相同(两者都通过 Arrow)
cache.write_parquet(
    "cache_ETHUSDT_2024_2026.parquet",
    compression="zstd",
    compression_level=3,
)

在大規模最佳化期間 — 當需要執行數千種參數組合時 — 通過 Polars scan_parquet 配合謂詞下推從 Parquet 快取讀取,可以僅載入需要的時間段和列,而無需讀取整個檔案。

與自適應逐層細化的結合:Polars 惰性求值非常適合兩級載入 — 主遍歷使用粗粒度資料,僅在成交模糊區域使用詳細資料(秒級、毫秒級)。

何時使用什麼:實踐建議

Pandas vs Polars 選擇指南 決策矩陣:小規模原型開發與大規模生產管道的不同路徑

Pandas 適用於以下情況:

  • 資料集不超過1M行且您不需要對數百個組進行 GroupBy — Pandas 2.2 和 Polars 之間的差異通常不大(1.5-2倍)
  • 需要 pandas-ta 或其他具有 Pandas API 的庫 — 為一次性研究重寫130個指標不切實際
  • 原型設計 — Pandas API 對大多數人更熟悉,快速假設驗證時速度不是關鍵
  • 與遺留程式碼整合 — 現有的 Pandas 管道正常執行,不需要最佳化

Polars 適用於以下情況:

  • 資料集超過1000萬行 — 數千萬和數億行的 tick 資料、多時間框架快取
  • 按組滾動計算 — 100+標的,每個都需要計算指標:100-3500倍加速
  • ETL 管道 — 載入、清洗、轉換大量資料
  • 有限的 RAM — 惰性求值和流式引擎允許處理不適合記憶體的資料
  • Parquet/QuestDB 技術棧 — 原生 Arrow = 零複製、謂詞下推、投影下推

不應期望什麼

營銷數字"快30倍"是特定操作上的峰值加速。典型管道操作的實際加速:2-10倍。按組滾動計算上 — 顯著更多。在小資料集上 — 有時 Polars 由於開銷甚至更慢。

我們在 marketmaker.cc 的經驗

生產效能儀表盤 生產指標:管道加速 6-8 倍,每小時最佳化迭代次數增加 8 倍

在 marketmaker.cc,我們為回測引擎使用 Polars + Numba 混合架構。整個資料管道 — 從 Parquet 快取載入、計算指標、過濾、特徵工程 — 在 Polars 上執行。投資組合模擬在 Numba 上執行。

在資料管道中從 Pandas 切換到 Polars,在我們的典型資料集(5000萬-1億行,200+標的)上獲得了6-8倍的加速。按組滾動指標計算從幾分鐘降到幾百毫秒。這使我們在不更換硬體的情況下,將每小時最佳化迭代次數從約500次增加到約4000次。

關鍵點:我們沒有在一天內遷移所有程式碼。首先遷移了 I/O(讀取 Parquet),然後是指標計算,然後是過濾和特徵工程。Pandas 僅保留在與期望 pd.DataFrame 的遺留元件的介面中。df.to_pandas() / pl.from_pandas() 轉換隻需幾毫秒,不是瓶頸。

在回測階段計算的指標 — 包括按活躍時間計算的 PnL — 已經在 Polars DataFrame 上計算,這簡化了管道並消除了中間轉換。

結論

架構融合 三大技術流匯聚:Polars、Numba 和 Arrow 融合為統一的最佳化管道

Polars 並非在每個場景中都能替代 Pandas。它是一個不同類別的工具,在嚴肅演算法交易中典型的規模上才能充分發揮:數百萬和數億行、數十和數百個標的、持續的參數最佳化。

關鍵數字:

  • 基本操作:典型管道任務加速2-10倍
  • 按組滾動計算:10-3500倍 — 交易管道的主要殺手級特性
  • CSV I/O:高達25倍 — 對初始資料載入至關重要
  • 記憶體:得益於 Arrow 和惰性求值,節省2-6倍
  • 流式處理:處理不適合 RAM 的資料

推薦的生產回測引擎架構:

  1. Polars — 整個資料管道:載入、指標、過濾、特徵
  2. Numba/Rust — 投資組合模擬:路徑依賴的訂單和倉位邏輯
  3. Arrow — 所有連線點的資料格式:Parquet、QuestDB、Polars、NumPy

沒有中間 Pandas 層。資料從儲存通過 Polars 流入 NumPy 陣列,再進入 Numba 引擎 — 沒有不必要的複製,沒有 GIL,沒有單執行緒瓶頸。


有用的連結

  1. Polars — User Guide
  2. Polars vs Pandas — official benchmark
  3. PDS-H Benchmark — DataFrame libraries comparison
  4. Apache Arrow — columnar format specification
  5. Numba — JIT compiler for Python
  6. vectorbt — backtesting framework
  7. pandas-ta — Technical Analysis Indicators
  8. Ritchie Vink — I wrote one of the fastest DataFrame libraries (Polars origin)
  9. Towards Data Science — Polars vs Pandas: real-world benchmarks
  10. Ernest Chan — Quantitative Trading

引用

@article{soloviov2026polarsvspandas,
  author = {Soloviov, Eugen},
  title = {Polars vs Pandas for Algotrading: Benchmarks on Real Data},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/polars-vs-pandas-algotrading},
  description = {Polars 与 Pandas 在算法交易任务上的详细对比:过滤、聚合、滚动信号计算、I/O 和内存消耗的基准测试。Polars + Numba 混合架构实现最大回测性能。}
}