Python Polars 高性能数据处理引擎深度解析

1. 背景:为什么在Pandas 已经很好用的年代还要学 Polars

1.1 数据工程师的三次觉醒

做工业数采的人都会经历三个阶段:

  1. 第一阶段:数据量小,pd.read_csv() 一把梭,几百 MB 的 CSV 也就等几秒。
  2. 第二阶段 :点位明细越攒越多------一台 CNC 每秒 10 个点位、200 台设备、24 小时不停机,一天就是 1.7 亿行 。Pandas 开始内存暴涨、卡顿、警告刷屏,concat 一次要等半分钟。
  3. 第三阶段 :你发现 Pandas 慢不是因为它懒,而是它的设计哲学决定了它在大数据量 上的天花板:
    • 行式内存布局:按记录连续存储,读取一列要跨过整行所有字段,CPU 缓存利用率低;
    • 单线程执行:现代 CPU 8 核 16 线程摆在那里,Pandas 只用一个核;
    • 急切执行:每写一行代码就立刻算完,中间产生大量临时 DataFrame,内存峰值失控;
    • 函数名混乱:dfdf\['a'>1]、df.query()、df.loc...、df.filter()......同一个操作有四五种写法,可读性灾难。

1.2 Polars 是什么

Polars 是 2020 年起由 Ritchie Vink 发起、用 Rust 编写的开源 DataFrame 库(MIT 协议)。它的核心承诺是:

承诺 实现方式
快 列式存储(Apache Arrow 内存格式)+ SIMD 向量化 + 多线程并行
省 惰性执行(LazyFrame)整棵查询树优化后再执行,无中间临时帧
稳 类型严格(区分 Int32/Int64/Float32/Float64),拒绝「静默隐式转换」
爽 统一表达式 API,一个 .expr() 语法贯穿选择、过滤、聚合、窗口

它与 Pandas 不是替代与被替代,而是 通用分析工具 vs 高性能引擎的分层关系:

维度 Pandas Polars
内存布局 行式(BlockManager) 列式(Arrow)
执行模型 急切(Eager) 急切 + 惰性(Lazy)
并行 单线程为主 多线程(默认全部核心)
类型系统 宽松,隐式转换 严格,类型敏感
表达式 方法分散、写法多 统一表达式体系
适用 探索性分析、机器学习特征工程、生态依赖 大批量数据清洗、ETL、报表聚合、流式处理

2. 核心概念速览

先建立 Polars 的世界观,它只有几个关键概念,但每个都比 Pandas 更较真。

2.1 DataFrame 与 Series(和 Pandas 同名但不同内核)

  • pl.DataFrame:二维表,底层是 Arrow 列式存储,每一列是一个连续内存数组,类型一致;
  • pl.Series:单列,本质是指向 Arrow ChunkedArray 的句柄;
  • 列的类型有严格语义:Int8/16/32/64、UInt*、Float32/64、String、Boolean、Date/Datetime/Duration、List、Struct、Categorical、Enum。

2.2 表达式(Expression):Polars 的灵魂

Pandas 的 API 是方法作用在 DataFrame 上;Polars 的 API 是 表达式作用在列上,再传给上下文。

python 复制代码
# Pandas 风格:方法分散
df[df['ts'].dt.hour == 8]['value'].mean()

# Polars 风格:表达式统一
df.filter(pl.col('ts').dt.hour() == 8).select(pl.col('value').mean())

表达式可以组合、复用、并行求值。一个表达式对象可以在多个上下文里用:select(选列)、filter(过滤)、with_columns(新增列)、group_by(分组聚合)、sort(排序)。

2.3 Eager 与 Lazy 双模式

  • Eager:.collect() 前的 DataFrame 操作,每步立刻执行;
  • Lazy :.lazy() 生成 LazyFrame,只记录查询计划,最终 .collect() 才执行。中间不会产生任何临时帧,且会触发查询优化器。

这是 Polars 性能神话的核心之一:lazy 模式会把你的整条流水线(读文件 → 过滤 → 聚合 → 排序)合并优化后再执行,而不是像 Pandas 一样每一步建一个临时表。

2.4 列式存储为什么快

数据在内存中是按列连续存放的。这意味着:

  • 读取一列 = 顺序扫描一段连续内存,CPU 缓存命中率高;
  • 过滤一列时,其他列完全不碰(Pandas 行式存储要整行读出再丢弃);
  • 数值计算天然向量化(一列数据就是一片同类型数组,可以直接上 SIMD);
  • 压缩友好(同类型数据压缩率高,pl.read_parquet 是主流格式)。

2.5 流式处理(Streaming)

面对「超过内存」的数据,Polars 提供流式执行:.collect(streaming=True),查询引擎会在必要时把中间结果落盘(如排序、聚合),用边读边算代替全量载入内存。


3. 安装与快速上手

复制代码
pip install polars          # 当前稳定版 1.x
pip install pyarrow         # 可选的 Arrow 互操作扩展
pip install polars[timezone]  # 时区支持

Polars 是编译好的二进制 wheel,不需要本地 Rust 环境。导入:

复制代码
import polars as pl

五分钟最小闭环:

复制代码
# 构造
df = pl.DataFrame({
    "device": ["CNC-01", "CNC-01", "CNC-02", "CNC-02"],
    "ts": ["2026-10-23 08:00:00", "2026-10-23 08:00:01",
           "2026-10-23 08:00:00", "2026-10-23 08:00:01"],
    "value": [10.5, 12.1, 8.2, 9.0],
}).with_columns(pl.col("ts").str.to_datetime())

# 过滤 + 聚合(Polars 风格)
result = (
    df.lazy()
    .filter(pl.col("value") > 9)
    .group_by("device")
    .agg(pl.col("value").mean().alias("avg_value"))
    .sort("avg_value", descending=True)
    .collect()
)
print(result)

输出:

复制代码
shape: (2, 2)
┌─────────┬───────────┐
│ device  ┆ avg_value │
╞═════════╪═══════════╡
│ CNC-01  ┆ 11.3      │
│ CNC-02  ┆ 8.2       │
└─────────┴───────────┘

4. API 说明(高频速查)

4.1 创建与 IO

目的 API
从 dict / list / numpy 构造 pl.DataFrame({...})、pl.DataFrame(arr, schema=...)
读 CSV pl.read_csv(path, schema_overrides={...}, try_parse_dates=True)
读 Parquet / Arrow IPC pl.read_parquet(path)、pl.read_ipc(path)
读 JSON / NDJSON pl.read_json(path)、pl.read_ndjson(path)
连接数据库 pl.read_database(uri=..., query=...)
写文件 df.write_csv(...)、df.write_parquet(...)、df.write_json(...)、df.write_ipc(...)
与 Pandas / NumPy 互转 df.to_pandas()、df.to_numpy()、pl.from_pandas(pd_df)、pl.from_numpy(arr)

4.2 选择与操作上下文

上下文 作用 示例
df.select(...) 选列 / 计算列 df.select(pl.col("a"), (pl.col("b")*2).alias("b2"))
df.with_columns(...) 新增 / 覆盖列 df.with_columns(pl.col("v").diff().alias("delta"))
df.filter(...) 行过滤 df.filter(pl.col("q") == 1)
df.group_by(...).agg(...) 分组聚合 df.group_by("device").agg(pl.len(), pl.col("v").mean())
df.sort(...) 排序 df.sort("ts")、df.sort(pl.col("v").rank())
df.unique() 去重 df.unique(subset="device")

4.3 列选择语法

  • pl.col("a")、pl.col("a", "b")、pl.col("^prefix_.*$")(正则)、pl.all()、pl.exclude("ts");
  • df.select(pl.col("a").name.prefix("raw_")) 批量改名;
  • 取子集:df.select("device", "value") 直接传字符串列表也可以。

4.4 常用表达式函数族

类别 表达式
数值 sum/mean/median/std/var/min/max、diff、pct_change、rank、cum_sum、clip、round、abs、log、pow、shift
字符串 str.len_chars、str.contains、str.starts_with、str.to_uppercase、str.split、str.extract、str.replace
时间 dt.year/month/day/hour/minute/second、dt.truncate("1h")、dt.round("5m")、dt.epoch("s")、dt.strftime
条件 pl.when(cond).then(a).otherwise(b)(对标 np.where)、is_in、is_null、is_between
聚合 first/last/n_unique/unique/count/len、quantile(0.95)、arg_max/arg_min、n_unique
列表/结构 list.len、list.first、list.sum、list.eval、struct.field

4.5 窗口函数

over() 是 Polars 的分组窗口语法,对标 SQL 的 PARTITION BY:

python 复制代码
df.with_columns(
    pl.col("value").mean().over("device").alias("device_avg"),      # 组内均值广播
    pl.col("value").rank().over(["device", pl.col("ts").dt.hour()]).alias("rank_in_hour"),
)

4.6 Join

python 复制代码
df1.join(df2, on="device", how="left")        # left/right/inner/outer/anti/semi/cross
df1.join_asof(df2, on="ts", by="device")      # 最近时间匹配,工业时序对齐神器

join_asof 是 Polars(及 DuckDB)独有亮点:两表按时间就近对齐,做设备档案与点位明细关联时不需要先做时间分箱。

4.7 其他高频方法

  • df.describe():统计摘要;
  • df.head()/tail()、df.sample(n=1000):抽样;
  • df.explode("list_col"):把列表列炸开成多行;
  • df.pivot(values="v", index="device", columns="hour"):透视;
  • df.estimated_size():估算内存;
  • df.glimpse():列类型快速预览(类似 df.info())。

5. 详细使用说明(8 组可运行示例)

以下示例围绕工业数采场景组织,全部可直接运行。

5.1 最小闭环:从 CSV 读点位明细并做分钟级聚合

python 复制代码
import polars as pl

# 模拟 1 台设备 3 天的秒级点位(这里直接造数据,实际用 pl.read_csv)
df = pl.DataFrame({
    "device": ["CNC-01"] * 259200,
    "ts": pl.datetime_range(
        start="2026-10-20 00:00:00", end="2026-10-22 23:59:59",
        interval="1s", eager=True,
    ),
    "load": [60 + (i % 40) for i in range(259200)],
}).with_columns(pl.col("ts").dt.cast_time_unit("ms").alias("ts_ms"))

minute_stats = (
    df.lazy()
    .with_columns(pl.col("ts").dt.truncate("1m").alias("minute"))
    .group_by(["device", "minute"])
    .agg(
        pl.col("load").mean().alias("avg_load"),
        pl.col("load").max().alias("max_load"),
        pl.col("load").quantile(0.95).alias("p95_load"),
    )
    .collect()
)
print(minute_stats.head())

要点:dt.truncate("1m") 把时间对齐到分钟;group_by 内一次性算三个指标,比 Pandas 三次 agg 再 concat 省得多。

5.2 惰性查询优化:三步流水线合并为一次执行

python 复制代码
# 先 lzma,再 collect:中间不产生临时 DataFrame
plan = (
    df.lazy()
    .filter(pl.col("load") > 50)
    .with_columns((pl.col("load") - 60).alias("overload"))
    .group_by("device")
    .agg(pl.col("overload").mean())
)
print(plan.explain())        # 查看优化后的执行计划
result = plan.collect()      # 真正执行

explain() 能直接看到:Polars 会把 filter 下推到读取阶段(ProjectionPushdown),整条链路只扫描一次数据。

5.3 窗口函数:设备日均值 + 相对偏差

python 复制代码
df.with_columns(
    pl.col("ts").dt.date().alias("day"),
).with_columns(
    (pl.col("load") / pl.col("load").mean().over(["device", "day"]) - 1).alias("deviation"),
).select("device", "day", "deviation").head()

5.4 多表关联:点位明细 × 设备档案 × 工艺参数

python 复制代码
details = pl.DataFrame({...})          # 秒级点位
devices = pl.DataFrame({               # 设备档案
    "device": ["CNC-01", "CNC-02"],
    "line": ["L1", "L2"],
    "model": ["VMC-850", "VMC-1060"],
})
limits = pl.DataFrame({                # 工艺参数阈值
    "model": ["VMC-850", "VMC-1060"],
    "load_hi": [85, 88],
})

enriched = (
    details
    .join(devices, on="device", how="left")
    .join(limits, on="model", how="left")
    .with_columns(
        pl.when(pl.col("load") > pl.col("load_hi"))
        .then(pl.lit("ALARM")).otherwise(pl.lit("OK")).alias("status")
    )
)

5.5 join_asof:档案变更与点位时序对齐

python 复制代码
events = pl.DataFrame({  # 换刀、改参等事件
    "ts": ["2026-10-22 09:10:00", "2026-10-22 14:30:00"],
    "device": ["CNC-01", "CNC-01"],
    "event": ["tool_change", "param_update"],
}).with_columns(pl.col("ts").str.to_datetime())

aligned = df.join_asof(events, on="ts", by="device", strategy="backward")

每个点位自动带上最近一次发生的事件,做根因回溯非常好用。

5.6 流式处理:处理超过内存的 Parquet

python 复制代码
# 假设 hourly 目录下有 10 GB Parquet 分片
lf = pl.scan_parquet("data/hourly/*.parquet")   # scan_* 系列天然惰性
summary = (
    lf.filter(pl.col("q") == 1)
    .group_by(["device", pl.col("ts").dt.truncate("1h")])
    .agg(pl.col("value").mean())
    .collect(streaming=True)
)

scan_parquet 只读取查询所需的列(列裁剪)+ 只读取满足过滤的行组(谓词下推),配合 streaming=True 可以处理远超内存的数据。

5.7 与 Pandas / Matplotlib 互操作

python 复制代码
# Polars 算,Pandas/Matplotlib 画(或直接 Matplotlib 消费 numpy)
import matplotlib.pyplot as plt

trend = (
    df.lazy()
    .group_by(pl.col("ts").dt.truncate("1h"))
    .agg(pl.col("load").mean())
    .collect()
)
plt.plot(trend["ts"].to_numpy(), trend["load"].to_numpy())

5.8 生产骨架:批量落盘 + 增量追加 + 压缩

python 复制代码
# 每小时把聚合结果追加到 Parquet(用 pyarrow 或直接写分片文件)
def write_hourly(df: pl.DataFrame, base: str):
    hour = df["ts"].min().strftime("%Y%m%d_%H")
    path = f"{base}/agg_{hour}.parquet"
    df.write_parquet(path, compression="zstd", compression_level=19)

6. 底层实现剖析(它为什么这么快)

6.1 Apache Arrow 列式内存格式

Polars 的所有数据都装在 Arrow 的 ChunkedArray 里。每列是一块连续的同类型缓冲,另外还有一个 validity bitmap(位图)表示空值------所以空值不需要单独的 NaN 哨兵,也不占 8 字节。内存布局对 CPU 缓存极度友好:扫描一列 = 顺序读一片连续内存。

6.2 多线程并行

Polars 默认使用机器全部核心。它的并行策略是:

  • 按列并行:select 里多个表达式可以并行求值;
  • 按块并行:单列操作把数据切成 chunk 并行处理,再拼接;
  • 按组并行:group_by 先并行哈希分组,再合并各组结果。

对比 Pandas 的单线程,8 核机器上纯计算任务轻松快 5~20 倍。

6.3 SIMD 向量化

Rust 编译器(LLVM)自动为热循环生成 AVX2/AVX-512 指令,一次处理 8 个 float64 或 16 个 int32。列式布局让「整列是连续同类型数组」成为常态,SIMD 收益最大化。

6.4 查询优化器(Lazy 的秘密)

Polars 的 LazyFrame 会做以下优化(explain() 可见):

  • Predicate Pushdown:把 filter 尽量推到读取/扫描阶段,只读需要的行;
  • Projection Pushdown:只读取/计算需要的列;
  • Slice Pushdown:head/tail 提前截断;
  • CSE(公共子表达式消除):多个表达式重复计算的部分只算一次;
  • Join 重排:小表优先、hash join 等策略选择。

6.5 表达式系统与零中间帧

表达式是 AST(抽象语法树):pl.col("load").mean().over("device") 构建一棵树,执行时由引擎统一调度。正因为 Lazy 模式整棵树一次执行,中间不会物化临时 DataFrame,内存峰值大幅下降------这是 Pandas 急切执行无法避免的痛点。

6.6 内存与缓存策略

  • 大文件读取使用 mmap(scan_parquet 依赖 Arrow 的内存映射);
  • 写入使用批缓冲;collect(streaming=True) 时排序、聚合等算子自动 spill 到磁盘临时文件。

7. 常错点 / 坑(20+ 条实战排雷)

  1. 忘 collect():LazyFrame 上 print() 显示的是查询计划而不是数据;任何 lazy() 链最后必须 .collect()。
  2. collect() 后继续当 Lazy 用:collect 返回的是普通 DataFrame,再调 lazy() 才能继续惰性。
  3. 列名大小写敏感:Polars 列名严格区分大小写,col("Device") 找不到 device 会直接报错(Pandas 也是,但错误信息更友好)。
  4. 隐式类型转换不存在 :pl.col("a") + pl.col("b") 两边类型不一致(如 Int64 + String)直接抛 ComputeError,这是特性不是 bug,逼你显式 .cast()。
  5. 空值传播:mean() 默认跳过 null(与 SQL 一致),但 sum() 也是跳过;需要 null 参与时用 pl.col("v").sum().fill_null(0) 之类显式处理。
  6. when().then().otherwise() 缺 otherwise 返回 null:忘记写 otherwise,条件不满足的行会变 null,而不是保留原值。
  7. 用 Pandas 思维写 Polars:dfdf\["a">1] 在 Polars 里含义完全不同(那是列选择!),必须写 df.filter(pl.col("a") > 1)。
  8. group_by 后忘记 agg:df.group_by("device") 单独调用没有意义,必须跟 .agg(...) 或 .len()。
  9. 聚合中混用 alias 与列名:聚合字段名默认是表达式本身,建议总是显式 .alias(),否则后续 select 引用很别扭。
  10. 时间字符串没有转 Datetime 就做 dt.*:pl.col("ts").dt.hour() 前必须先 str.to_datetime(),否则报错或得到 null。
  11. 时区陷阱:带 +08:00 的字符串直接 str.to_datetime() 可能解析为无时区;建议统一 str.to_datetime(time_zone="Asia/Shanghai") 或先 dt.convert_time_zone。
  12. read_csv 日期不自动解析:Polars 不会像 Pandas 那样自动猜日期列,需要 try_parse_dates=True 或 schema_overrides 手动指定。
  13. 大文件直接 read_csv 爆内存:能 scan_csv 就不要 read_csv;超过内存用流式。
  14. write_parquet 忘记压缩参数:默认 snappy 够用,但冷数据建议 compression="zstd",文件能小一半以上。
  15. join 默认 how 是 inner:需要保留左表全部行时记得写 how="left"。
  16. join 键类型不一致:device 一边是 String 一边是 Categorical 会报错或结果为空,先 cast(pl.String) 统一。
  17. join_asof 的两表都必须按 key 排序:不排序结果乱套,先 .sort("ts")。
  18. over() 分组字段是表达式而非字符串时忘列表包裹:pl.col("v").mean().over("device", pl.col("ts").dt.hour()) 要传列表。
  19. shift 边界行为:df.with_columns(pl.col("v").shift(1)) 首行是 null,不是 0。
  20. unique(subset=...) 与 n_unique 混淆:df.unique(subset="device") 返回去重后的帧;pl.col("device").n_unique() 是计数。
  21. pivot 参数顺序:df.pivot(values=..., index=..., columns=..., aggregate_function="mean"),顺序写反会报错。
  22. List/Struct 列小心:对 List 列做算术要先 list.eval 或 explode,直接 col("list_col") + 1 不可用。
  23. filter 多条件用 & / | 而非 and / or:Python 关键字在表达式里不生效且不报错,静默返回错误结果。
  24. describe() 对非数值列:Polars 会给出统计但行为与 Pandas 不同,别指望 NaN 计数。
  25. to_pandas() 大表转换内存翻倍:能保持 Polars 就保持;转换后注意 Arrow 与 Pandas 的 dtype 映射。
  26. 版本升级破坏性变更:Polars 1.x 之前 API 变动频繁(如 groupby→group_by、apply→map_elements);老教程代码直接跑不通时先看版本。

8. 性能优化清单(10 条)

  1. 默认走 Lazy:所有 ETL 链路从 scan_* / .lazy() 开始,让优化器帮你干活;
  2. 列裁剪:只 select 需要的列,文件读取阶段就少 IO;
  3. 谓词下推:filter 写在最前面(先过滤再计算);
  4. 预聚合:秒级明细先 group_by + truncate 到分钟/小时,再下钻;
  5. Parquet 优先:中间数据一律 Parquet + zstd,别用 CSV 中转;
  6. join_asof 替代自写循环:时序对齐场景用它,不要 Python 循环逐行配对;
  7. 用 pl.len() 计数:agg(pl.len()) 比 agg(pl.col("x").count()) 语义清晰且少读一列;
  8. 避免 map_elements / apply:Python UDF 会打穿性能(每行回调 Python),能用内置表达式就用内置表达式;实在要自定义逻辑,考虑 list.eval 或 Rust 扩展;
  9. collect(streaming=True) 治理内存:大数据量排序/聚合触发 spill,别硬扛内存;
  10. explain() 检查计划:优化器没按预期下推时,八成是写错了上下文(比如在 with_columns 里做了本应 filter 做的事)。

9. 总结五要点

  1. 列式存储 + 多线程 + SIMD 是 Polars 快的基础,不是魔法;
  2. 表达式统一语法 让选、滤、聚、窗口一套思维走天下,代码可读性远超 Pandas 的多种写法;
  3. LazyFrame + 查询优化器 是省内存的钥匙------整棵树一次执行、谓词/投影下推、无中间帧;
  4. 严格类型 + 显式错误 是稳的保障------坑在编译期暴露,而不是线上静默算错;
  5. 工业数采的正确姿势:采集用 C++/Go,存储用 TDengine/Kafka,分析用 Polars------把对的工具用在对的环节。

FAQ(10 条速查)

  1. Polars 会取代 Pandas 吗? 短期不会;它取代的是「Pandas 扛不动大数据量」的痛点,生态互补。
  2. 需要会 Rust 吗? 不需要,pip 装的是编译好的 wheel。
  3. Lazy 一定比 Eager 快吗? 不一定;但 Lazy 的内存峰值一定更低,复杂链路通常更快。
  4. collect(streaming=True) 何时生效? 排序、分组聚合等算子;取决于操作能否流式化,Polars 会自行决定。
  5. 支持 Windows 吗? 支持,官方发布 win x86_64 wheel。
  6. CSV 编码问题? 默认 UTF-8;GBK 文件先 encoding="gbk"(read_csv 支持)。
  7. 和 DuckDB 什么关系? 同是查询引擎思路,DuckDB 偏 SQL、Polars 偏 DataFrame;可互转(pl.from_arrow / duckdb 读 Arrow)。
  8. 支持 GPU 吗? 官方暂未提供 GPU 执行(CUDA 加速不在当前路线图重点),大数据量建议走分布式 + 分片并行。
  9. 时间序列支持如何? 有 dt.* 全套、truncate/round、join_asof、rolling(滚动窗口),时序分析够用。
  10. 生产环境稳定性? 1.x 起 API 趋于稳定,被 Snowflake 等厂商采用(Polars 是 Snowflake 收购的一部分),社区活跃。
相关推荐
初阳7851 小时前
【基于Qt5仿音乐播放器(一)】——项目准备
qt·1024程序员节
CAIE研习社1 小时前
跨专业考证如何帮助求职?从原有专业到AI应用能力的组合思路
1024程序员节
Nuanyt1 小时前
Hot 100 刷题笔记 · 数组与哈希专题(3 题)
java·笔记·算法·leetcode·哈希算法·1024程序员节
Hardworking6661 小时前
2026年10月24日系统集成项目管理工程师上午考试内容
1024程序员节·系统集成项目管理工程师
豆豆1 小时前
网站漏洞应急处置 + 安全加固指南,附安全 CMS 推荐
web安全·网站安全·1024程序员节·漏洞修复·漏洞处置·cms 选型·网站漏洞修复
fpcc1 小时前
并行编程实战—CUDA中的Tile编程之八优化提示
cuda·1024程序员节·并行编程
csdn_aspnet1 小时前
JavaScript 未排序数组中第 k 个最小/最大元素 (预期线性时间)
python
小豆庄1 小时前
视频隐私脱敏:从逐帧标注到识别追踪的工程取舍
目标跟踪·视频·敏捷流程·python3.11·1024程序员节·qt6.3
quantdash_cc1 小时前
如何利用标的信息为研究股票池补充名称和交易所信息?
开发语言·python·pandas·量化交易·1024程序员节·股票数据·quantdash