Polars 完全指南

最后更新:2026-03-27 API 参考:https://docs.pola.rs/api/python/stable/reference/ 1. Polars完全指南 · 安装与版本(/polars-wan-quan-zhi-nan/#%E5%AE%89%E8%A3%85%E4%B8%8E%E7%89%88%E6%9C%AC) 2. Polars完全指南 · 核心概念(/polars-wan-quan-zhi-nan/#%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5) 3. Polars完全指南 · 数据创建(/polar

分享

最后更新:2026-03-27

官方文档:https://docs.pola.rs/
适用版本:Polars 1.x(2026-05-08 核实)
API 参考:https://docs.pola.rs/api/python/stable/reference/


目录

  1. Polars完全指南 · 安装与版本
  2. Polars完全指南 · 核心概念
  3. Polars完全指南 · 数据创建
  4. Polars完全指南 · 数据读写
  5. Polars完全指南 · 数据查看
  6. Polars完全指南 · 表达式系统(Expression)
  7. Polars完全指南 · DataFrame 操作
  8. Polars完全指南 · 分组聚合(GroupBy)
  9. Polars完全指南 · 连接操作
  10. Polars完全指南 · 数据重塑
  11. Polars完全指南 · 合并
  12. Polars完全指南 · 字符串操作(str namespace)
  13. Polars完全指南 · 日期时间操作(dt namespace)
  14. Polars完全指南 · 惰性执行(LazyFrame)
  15. Polars完全指南 · 流式处理(Streaming)
  16. Polars完全指南 · 窗口函数
  17. Polars完全指南 · 性能特性
  18. Polars完全指南 · 常用配合库
  19. Polars完全指南 · 最佳实践
  20. Polars完全指南 · 应用场景
  21. Polars完全指南 · 常见陷阱与注意事项

安装与版本

安装

# 基础安装
pip install polars

# 包含所有可选依赖(推荐生产环境)
pip install polars[all]

# 仅包含 pandas 互操作依赖
pip install polars[pandas]

# 仅包含 numpy 依赖
pip install polars[numpy]

# 包含 Excel 读写支持
pip install polars[xlsx2csv,openpyxl]

# 包含数据库连接支持
pip install polars[connectorx]

验证安装

import polars as pl

print(pl.__version__)

Polars vs Pandas 核心对比

维度 Polars Pandas
API 风格 表达式系统(Expression),链式调用 命令式,索引驱动
执行引擎 Rust 编写,原生多线程 Python + NumPy,默认单线程
内存模型 Apache Arrow 列式存储 NumPy 数组,非列式
多线程 自动并行,无需配置 需借助 Dask/Modin 等
惰性求值 内置 LazyFrame,支持查询优化 无原生惰性支持
索引 无行索引(无 index 概念) 有行索引(Index 对象)
内存占用 通常低 30%~60% 较高,有对象列开销
处理速度 大数据集通常快 5~20 倍 小数据集差距不明显
生态成熟度 较新(2021 年),生态仍在完善 成熟,生态完整
缺失值处理 null(整数也可为 null)与 NaN 严格区分 NaN 和 None 混用

核心概念

Series 和 DataFrame(Eager 模式)

Series 是一维数组,DataFrame 是二维表格,均基于 Apache Arrow 内存格式。

Eager 模式下操作立即执行并返回结果,适合探索性分析和小数据集。

import polars as pl

# Series
s = pl.Series("age", [25, 30, 35, 28])
print(s.dtype)    # Int64
print(s.mean())   # 29.5

# DataFrame
df = pl.DataFrame({
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 30, 35],
    "score": [88.5, 92.0, 79.5],
})
print(df)

LazyFrame(Lazy 模式)

LazyFrame 记录操作但不立即执行,调用 .collect() 时才触发计算。Polars 在执行前对查询计划进行优化(谓词下推、投影下推等)。

# 从 DataFrame 转换
lf = df.lazy()

# 构建查询计划(不执行)
result = (
    lf
    .filter(pl.col("age") > 25)
    .select(["name", "score"])
    .sort("score", descending=True)
)

# 查看执行计划
print(result.explain())

# 触发执行
df_result = result.collect()

Expression(表达式系统)

表达式是 Polars 最核心的概念。表达式描述对列的操作,可以任意组合,Polars 自动并行执行多个表达式。

# 表达式描述操作,不立即执行
expr = pl.col("age") * 2 + 1

# 在 select/filter/with_columns 中使用
df.select(expr)
df.with_columns(expr.alias("age_double"))

Apache Arrow 内存模型

Polars 底层使用 Apache Arrow 列式存储:

  • 同一列的数据在内存中连续存储,CPU 缓存命中率高
  • 支持零拷贝与其他 Arrow 兼容库(PyArrow、DuckDB 等)互操作
  • 整数列支持 null(不需要像 pandas 那样将整数列转为 float64 以存储 NaN)

数据创建

pl.Series()

创建一维 Series。

pl.Series(name, values, dtype=None, *, strict=True, nan_to_null=False)
参数 类型 默认值 说明
name str 必填 Series 名称
values array-like 必填 数据,支持 list/numpy/pyarrow 等
dtype PolarsDataType None 指定数据类型,None 则自动推断
strict bool True True 时值不符合 dtype 则报错,False 则尝试转换
nan_to_null bool False 将浮点 NaN 转为 null
s1 = pl.Series("scores", [1, 2, 3, 4, 5])
s2 = pl.Series("prices", [1.5, 2.3, None, 4.1], dtype=pl.Float32)
s3 = pl.Series("flags", [True, False, True])
s4 = pl.Series("names", ["Alice", "Bob", "Charlie"])

pl.DataFrame()

创建二维 DataFrame。

pl.DataFrame(data=None, schema=None, *, schema_overrides=None, strict=True, orient=None, infer_schema_length=100, nan_to_null=False)
参数 类型 默认值 说明
data dict / list / numpy / pandas None 输入数据
schema list[str] / dict[str, dtype] None 列名或列名到类型的映射
schema_overrides dict[str, dtype] None 覆盖部分列的类型推断结果
strict bool True 类型不匹配时是否报错
orient "col" / "row" None data 为嵌套列表时的方向
infer_schema_length int 100 推断 schema 时扫描的行数
nan_to_null bool False 将 NaN 转为 null
# 从字典创建(最常用)
df = pl.DataFrame({
    "id": [1, 2, 3],
    "name": ["Alice", "Bob", "Charlie"],
    "score": [88.5, 92.0, 79.5],
})

# 从列表创建(按行)
df = pl.DataFrame(
    [[1, "Alice", 88.5], [2, "Bob", 92.0]],
    schema=["id", "name", "score"],
    orient="row",
)

# 从列表创建(按列)
df = pl.DataFrame(
    [[1, 2, 3], ["Alice", "Bob", "Charlie"]],
    schema=["id", "name"],
    orient="col",
)

# 指定类型
df = pl.DataFrame(
    {"id": [1, 2, 3], "score": [88.5, 92.0, 79.5]},
    schema={"id": pl.Int32, "score": pl.Float32},
)

# schema_overrides 只覆盖部分列
df = pl.DataFrame(
    {"id": [1, 2, 3], "name": ["Alice", "Bob", "Charlie"], "score": [88, 92, 79]},
    schema_overrides={"score": pl.Float64},
)

从 Pandas 互转

# pl.from_pandas(df, schema_overrides=None, rechunk=True, nan_to_null=True, include_index=False)
参数 类型 默认值 说明
df pandas.DataFrame / pandas.Series 必填 输入的 pandas 对象
schema_overrides dict[str, dtype] None 覆盖部分列的类型
rechunk bool True 重新分配内存为单块,提升后续性能
nan_to_null bool True 将 NaN 转为 null
include_index bool False 是否将 pandas index 作为列导入
import pandas as pd
import polars as pl

pdf = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})

# pandas -> polars
df = pl.from_pandas(pdf)
df = pl.from_pandas(pdf, include_index=True)

# polars -> pandas
pdf2 = df.to_pandas()
pdf2 = df.to_pandas(use_pyarrow_extension_array=True)  # 保留 null(不转 NaN)

从 NumPy 和 Arrow 创建

import numpy as np
import pyarrow as pa

# 从 numpy 创建
arr = np.array([[1, 2, 3], [4, 5, 6]])
df = pl.from_numpy(arr, schema=["a", "b", "c"])

# Series from numpy
s = pl.Series("data", np.array([1.0, 2.0, 3.0]))

# 从 Arrow Table 创建
table = pa.table({"a": [1, 2, 3], "b": ["x", "y", "z"]})
df = pl.from_arrow(table)

# Polars -> Arrow
arrow_table = df.to_arrow()

数据读写

pl.read_csv()

读取 CSV 文件为 DataFrame(Eager 模式)。

pl.read_csv(source, *, has_header=True, columns=None, new_columns=None, separator=",", comment_prefix=None, quote_char='"', skip_rows=0, skip_rows_after_header=0, dtypes=None, schema=None, schema_overrides=None, null_values=None, missing_utf8_is_empty_string=False, ignore_errors=False, try_parse_dates=False, n_threads=None, infer_schema_length=100, batch_size=8192, n_rows=None, encoding="utf8", low_memory=False, rechunk=True, use_pyarrow=False, storage_options=None, skip_rows=0, row_index_name=None, row_index_offset=0, sample_size=1024, eol_char="\n", raise_if_empty=True, truncate_ragged_lines=False, decimal_comma=False)

常用参数:

参数 类型 默认值 说明
source str / Path / bytes 必填 文件路径、URL 或字节内容
has_header bool True 第一行是否为表头
separator str "," 字段分隔符
columns list[str/int] None 只读取指定列(列名或列索引)
new_columns list[str] None 为列重命名(按顺序替换)
dtypes dict[str, dtype] None 指定列的数据类型(旧参数,建议用 schema_overrides)
schema_overrides dict[str, dtype] None 覆盖部分列的推断类型
null_values str / list / dict None 将指定字符串识别为 null
ignore_errors bool False 解析失败的行用 null 填充而非报错
try_parse_dates bool False 尝试自动解析日期列
n_rows int None 只读取前 N 行
skip_rows int 0 跳过文件开头 N 行
skip_rows_after_header int 0 跳过表头后的 N 行
encoding str "utf8" 文件编码,常用 "utf8-lossy" 容错
low_memory bool False 降低内存使用(牺牲速度)
infer_schema_length int 100 推断类型时扫描的行数,None 表示全部
n_threads int None 并行线程数,None 表示自动
rechunk bool True 读取后重整内存块
row_index_name str None 添加行号列,指定列名
row_index_offset int 0 行号起始值
truncate_ragged_lines bool False 列数不足的行截断而不报错
# 基础读取
df = pl.read_csv("data.csv")

# 常用选项
df = pl.read_csv(
    "data.csv",
    separator="\t",                          # Tab 分隔
    columns=["id", "name", "score"],         # 只读指定列
    null_values=["", "NA", "N/A", "null"],   # 多种 null 表示
    try_parse_dates=True,                    # 自动解析日期
    n_rows=10000,                            # 只读前 1 万行
    schema_overrides={"id": pl.Int32},       # 覆盖类型
    encoding="utf8-lossy",                   # 容忍编码错误
)

# 无表头文件
df = pl.read_csv("data.csv", has_header=False, new_columns=["a", "b", "c"])

pl.read_parquet()

读取 Parquet 文件为 DataFrame。

pl.read_parquet(source, *, columns=None, n_rows=None, use_statistics=True, hive_partitioning=None, hive_schema=None, try_convert_dates=False, parallel="auto", row_index_name=None, row_index_offset=0, low_memory=False, storage_options=None, use_pyarrow=False, pyarrow_options=None, memory_map=True, rechunk=False)
参数 类型 默认值 说明
source str / Path / list 必填 文件路径,支持通配符和列表
columns list[str/int] None 只读指定列
n_rows int None 只读前 N 行
use_statistics bool True 利用 Parquet 统计信息加速过滤
parallel "auto" / "columns" / "row_groups" / "none" "auto" 并行读取策略
low_memory bool False 降低内存使用
row_index_name str None 添加行号列
hive_partitioning bool None 是否解析 Hive 分区路径
df = pl.read_parquet("data.parquet")
df = pl.read_parquet("data.parquet", columns=["id", "value"])
df = pl.read_parquet("data/*.parquet")  # 读取多个文件

pl.read_json() / pl.read_ndjson()

# JSON 数组格式:[{...}, {...}]
df = pl.read_json("data.json")

# NDJSON(每行一个 JSON 对象)格式:
# {"a": 1, "b": "x"}
# {"a": 2, "b": "y"}
df = pl.read_ndjson("data.ndjson")
df = pl.read_ndjson("data.ndjson", schema_overrides={"a": pl.Int32})

pl.read_excel()

pl.read_excel(source, *, sheet_id=None, sheet_name=None, engine=None, engine_options=None, read_options=None, schema_overrides=None, infer_schema_length=100, columns=None, raise_if_empty=True)
参数 类型 默认值 说明
source str / Path / bytes 必填 文件路径
sheet_id int None 按索引选择 Sheet(从 1 开始)
sheet_name str None 按名称选择 Sheet
engine str None 解析引擎:"calamine"(推荐)/ "openpyxl" / "xlsx2csv"
schema_overrides dict None 覆盖列类型
infer_schema_length int 100 推断类型扫描行数
columns list None 只读指定列
# 需要安装:pip install polars[xlsx2csv] 或 pip install polars[openpyxl]
df = pl.read_excel("data.xlsx")
df = pl.read_excel("data.xlsx", sheet_name="Sheet2", engine="calamine")

pl.scan_csv() / pl.scan_parquet()

返回 LazyFrame,不立即读取数据,配合惰性执行使用。读取大文件的首选方式。

# scan_csv 参数与 read_csv 基本相同,返回 LazyFrame
lf = pl.scan_csv("large_data.csv")
lf = pl.scan_csv(
    "large_data.csv",
    separator=",",
    null_values=[""],
    try_parse_dates=True,
    schema_overrides={"id": pl.Int32},
)

# scan_parquet 支持通配符和目录
lf = pl.scan_parquet("data.parquet")
lf = pl.scan_parquet("data/*.parquet")
lf = pl.scan_parquet("hive_data/", hive_partitioning=True)

# 配合惰性操作,只读取需要的列和行
result = (
    pl.scan_csv("large_data.csv")
    .filter(pl.col("date") >= "2024-01-01")
    .select(["id", "name", "value"])
    .collect()
)

写出文件

df.write_csv()

df.write_csv(file=None, *, has_header=True, separator=",", line_terminator="\n", quote_char='"', batch_size=1024, datetime_format=None, date_format=None, time_format=None, float_precision=None, null_value="", quote_style="necessary")
参数 类型 默认值 说明
file str / Path / None None 输出路径,None 则返回字符串
has_header bool True 是否写出表头
separator str "," 字段分隔符
null_value str "" null 的字符串表示
float_precision int None 浮点数精度
datetime_format str None 日期时间格式字符串
quote_style str "necessary" 引号策略:"necessary" / "always" / "never" / "non_numeric"
df.write_csv("output.csv")
df.write_csv("output.csv", separator="\t", null_value="NULL")
csv_str = df.write_csv()  # 返回字符串

df.write_parquet()

df.write_parquet(file, *, compression="zstd", compression_level=None, statistics=False, row_group_size=None, data_page_size=None, use_pyarrow=False, pyarrow_options=None)
参数 类型 默认值 说明
file str / Path 必填 输出路径
compression str "zstd" 压缩算法:"lz4" / "uncompressed" / "snappy" / "gzip" / "lzo" / "brotli" / "zstd"
compression_level int None 压缩级别(依算法而定)
statistics bool False 写入列统计信息(加速谓词下推)
row_group_size int None 每个 row group 的行数
df.write_parquet("output.parquet")
df.write_parquet("output.parquet", compression="snappy", statistics=True)

df.write_json() / df.write_ndjson()

df.write_json("output.json")
df.write_json("output.json", pretty=True)

df.write_ndjson("output.ndjson")

数据查看

基础查看方法

df.head(5)        # 前 N 行,默认 5
df.tail(5)        # 后 N 行,默认 5
df.sample(10)     # 随机采样 N 行
df.sample(fraction=0.1)  # 随机采样 10%

sample() 参数表

参数 类型 默认值 说明
n int None 采样行数,与 fraction 二选一
fraction float None 采样比例(0.0~1.0)
with_replacement bool False 是否有放回采样
shuffle bool False 是否打乱顺序
seed int None 随机种子,保证可复现

结构信息

df.shape           # (行数, 列数) 元组
df.columns         # 列名列表
df.dtypes          # 类型列表(与 columns 一一对应)
df.schema          # {列名: 类型} 字典
df.height          # 行数
df.width           # 列数

describe()

生成描述性统计汇总。

df.describe()
df.describe(percentiles=[0.25, 0.5, 0.75])
参数 类型 默认值 说明
percentiles list[float] [0.25, 0.5, 0.75] 要计算的分位数
interpolation str "nearest" 分位数插值方式

glimpse()

Polars 特有方法,纵向展示每列的名称、类型和前几个值,适合列数多的 DataFrame。

df.glimpse()
df.glimpse(max_items_per_column=5, max_colname_length=30)

输出示例:

Rows: 3
Columns: 3
$ id     <i64> 1, 2, 3
$ name   <str> 'Alice', 'Bob', 'Charlie'
$ score  <f64> 88.5, 92.0, 79.5

表达式系统(Expression)

表达式是 Polars 最核心的概念。表达式是对列操作的描述,可自由组合,Polars 会自动并行执行多个无依赖关系的表达式。表达式在 select()filter()with_columns()agg() 等上下文中使用。

pl.col() — 列引用

# 单列
pl.col("name")
pl.col("age")

# 多列(返回多个表达式)
pl.col("age", "score")
pl.col(["age", "score"])

# 正则匹配列名(以 ^ 开头,以 $ 结尾)
pl.col("^score.*$")     # 匹配所有以 score 开头的列

# 按 dtype 选择
pl.col(pl.Int64)         # 所有 Int64 列
pl.col(pl.Utf8)          # 所有字符串列(Utf8 即 String)
pl.col(pl.NUMERIC_DTYPES)  # 所有数值类型列

pl.lit() — 字面量

创建常量表达式。

pl.lit(42)
pl.lit("hello")
pl.lit(3.14)
pl.lit(True)
pl.lit(None)

# 使用场景:与列组合
df.with_columns((pl.col("score") + pl.lit(10)).alias("score_adjusted"))

pl.all() / pl.exclude()

pl.all()                          # 所有列
pl.exclude("id")                  # 排除单列
pl.exclude(["id", "name"])        # 排除多列
pl.exclude(pl.Utf8)               # 排除字符串列

# 示例
df.select(pl.all())               # 等同于 df
df.select(pl.exclude("id"))       # 删除 id 列
df.select(pl.all().sort())        # 对所有列排序

算术表达式

pl.col("price") + pl.col("tax")
pl.col("price") * 1.1
pl.col("score") ** 2           # 乘方
pl.col("value") % 10           # 取模
pl.col("a") // pl.col("b")    # 整除

比较与逻辑表达式

# 比较
pl.col("age") > 25
pl.col("age") >= 25
pl.col("age") == 25
pl.col("age") != 25
pl.col("age").is_between(20, 30)   # 闭区间
pl.col("age").is_between(20, 30, closed="left")  # 左闭右开

# 逻辑(推荐用运算符)
(pl.col("age") > 25) & (pl.col("score") > 80)   # AND
(pl.col("age") > 25) | (pl.col("score") > 90)   # OR
~pl.col("flag")                                   # NOT

# 也可以用方法(更明确)
pl.col("age").gt(25).and_(pl.col("score").gt(80))
pl.col("age").gt(25).or_(pl.col("score").gt(90))
pl.col("flag").not_()

alias() — 别名

pl.col("score").alias("成绩")
(pl.col("price") * 1.1).alias("price_with_tax")
pl.all().name.prefix("new_")    # 为所有列名加前缀
pl.all().name.suffix("_orig")   # 为所有列名加后缀

cast() — 类型转换

pl.col("id").cast(pl.Int32)
pl.col("score").cast(pl.Float32)
pl.col("flag").cast(pl.Boolean)
pl.col("date_str").cast(pl.Date)
参数 类型 默认值 说明
dtype PolarsDataType 必填 目标数据类型
strict bool True True 时转换失败报错,False 时失败返回 null
# 容错转换
pl.col("value").cast(pl.Int64, strict=False)  # 无法转换的变为 null

空值处理

pl.col("score").is_null()          # 是否为 null,返回布尔 Series
pl.col("score").is_not_null()      # 是否非 null
pl.col("value").is_nan()           # 是否为 NaN(仅浮点列)
pl.col("value").is_not_nan()

# fill_null:填充 null
pl.col("score").fill_null(0)                    # 用常量填充
pl.col("score").fill_null(pl.col("score").mean())   # 用均值填充
pl.col("score").fill_null(strategy="forward")   # 前向填充
pl.col("score").fill_null(strategy="backward")  # 后向填充
pl.col("score").fill_null(strategy="mean")      # 用均值填充
pl.col("score").fill_null(strategy="min")
pl.col("score").fill_null(strategy="max")

fill_null 的 strategy 参数:

strategy 说明
"forward" 用前一个非 null 值填充
"backward" 用后一个非 null 值填充
"mean" 用列均值填充
"min" 用列最小值填充
"max" 用列最大值填充
"zero" 用 0 填充
"one" 用 1 填充
# fill_nan:填充浮点 NaN
pl.col("value").fill_nan(0.0)
pl.col("value").fill_nan(None)   # NaN -> null

# drop_nulls:在 DataFrame 层面删除含 null 的行
df.drop_nulls()
df.drop_nulls(subset=["score"])  # 只检查指定列

排序表达式

pl.col("score").sort()                         # 升序
pl.col("score").sort(descending=True)          # 降序
pl.col("score").sort(nulls_last=True)          # null 排在末尾
pl.col("score").arg_sort()                     # 返回排序索引(位置)
pl.col("name").sort_by("score")                # 按另一列排序
pl.col("name").sort_by("score", descending=True)

聚合表达式

pl.col("score").sum()
pl.col("score").mean()
pl.col("score").median()
pl.col("score").std()          # 标准差(ddof=1)
pl.col("score").var()          # 方差(ddof=1)
pl.col("score").min()
pl.col("score").max()
pl.col("id").count()           # 非 null 计数
pl.col("id").len()             # 总行数(包含 null)
pl.col("name").n_unique()      # 唯一值个数
pl.col("name").first()         # 第一个值
pl.col("name").last()          # 最后一个值
pl.col("score").quantile(0.9)  # 分位数
pl.col("score").sum() / pl.col("score").count()  # 自定义聚合

when().then().otherwise() — 条件表达式

Polars 的条件分支,等价于 SQL 的 CASE WHEN,比 pandas apply 快得多。

# 基础用法
pl.when(pl.col("score") >= 90).then(pl.lit("A")).otherwise(pl.lit("B"))

# 多条件链式
grade = (
    pl.when(pl.col("score") >= 90).then(pl.lit("A"))
    .when(pl.col("score") >= 80).then(pl.lit("B"))
    .when(pl.col("score") >= 70).then(pl.lit("C"))
    .otherwise(pl.lit("D"))
)

df = df.with_columns(grade.alias("grade"))

# otherwise 引用其他列
result = (
    pl.when(pl.col("value").is_null())
    .then(pl.col("default_value"))
    .otherwise(pl.col("value"))
    .alias("value_filled")
)

# otherwise 可以省略(null 填充)
pl.when(pl.col("score") > 100).then(pl.lit(100))  # 超过 100 的截断,其余为 null

字符串表达式(str namespace)

详见 Polars完全指南 · 字符串操作(str namespace)

pl.col("name").str.to_uppercase()
pl.col("text").str.contains("python")
pl.col("date_str").str.strptime(pl.Date, "%Y-%m-%d")

日期时间表达式(dt namespace)

详见 Polars完全指南 · 日期时间操作(dt namespace)

pl.col("date").dt.year()
pl.col("datetime").dt.strftime("%Y-%m-%d")

列表表达式(list namespace)

# 假设列为 List[Int64] 类型
pl.col("scores").list.len()           # 每个列表的长度
pl.col("scores").list.sum()           # 每个列表元素求和
pl.col("scores").list.mean()          # 每个列表均值
pl.col("scores").list.max()
pl.col("scores").list.min()
pl.col("scores").list.first()         # 每个列表的第一个元素
pl.col("scores").list.last()
pl.col("scores").list.get(0)          # 按索引取元素
pl.col("scores").list.contains(90)    # 是否包含某值
pl.col("scores").list.sort()          # 排序每个列表
pl.col("scores").list.unique()        # 去重每个列表
pl.col("a").list.concat(pl.col("b"))  # 连接两个列表列

df = pl.DataFrame({"scores": [[1, 2, 3], [4, 5], [6]]})
df.with_columns(pl.col("scores").list.len().alias("n"))

结构体表达式(struct namespace)

# 创建结构体
df.select(pl.struct("a", "b").alias("ab"))

# 访问字段
pl.col("struct_col").struct.field("a")
pl.col("struct_col").struct.rename_fields(["x", "y"])

# 解构
df.with_columns(pl.col("struct_col").struct.unnest())

DataFrame 操作

select() — 列选取

选择列,可传入表达式。只返回选中的列。

df.select(expr, *more_exprs)
参数 类型 默认值 说明
exprs str / Expr / list 必填 列名、表达式或其列表
df.select("name")
df.select(["name", "score"])
df.select(pl.col("name"), pl.col("score") * 2)
df.select(pl.all().sort())                  # 对所有列排序
df.select(pl.col("^score.*$"))             # 正则选列
df.select(pl.col(pl.NUMERIC_DTYPES))       # 选所有数值列
df.select((pl.col("score") - pl.col("score").mean()).alias("score_centered"))

filter() — 行过滤

按条件过滤行,保留满足条件的行。

df.filter(expr)
参数 类型 默认值 说明
exprs Expr / list[Expr] 必填 布尔表达式,多个表达式间为 AND 关系
df.filter(pl.col("age") > 25)
df.filter((pl.col("age") > 25) & (pl.col("score") > 80))

# 多表达式参数等价于 AND
df.filter(pl.col("age") > 25, pl.col("score") > 80)

# is_in 过滤
df.filter(pl.col("city").is_in(["Beijing", "Shanghai"]))

# 字符串过滤
df.filter(pl.col("name").str.starts_with("A"))

with_columns() — 添加或修改列

添加新列或修改已有列。原有列不受影响,返回包含所有原列加上新列的 DataFrame。

df.with_columns(expr, *more_exprs)
参数 类型 默认值 说明
exprs str / Expr / list 必填 表达式列表,alias 决定列名
# 添加新列
df.with_columns((pl.col("price") * 1.1).alias("price_with_tax"))

# 修改已有列(alias 与现有列同名则覆盖)
df.with_columns(pl.col("score").cast(pl.Float32))

# 同时添加多列(并行执行)
df.with_columns(
    (pl.col("price") * 1.1).alias("price_tax"),
    pl.col("name").str.to_uppercase().alias("name_upper"),
    pl.col("score").fill_null(0).alias("score_filled"),
)

rename()

重命名列。

df.rename(mapping)
参数 类型 默认值 说明
mapping dict[str, str] 必填 {旧列名: 新列名} 字典
df.rename({"name": "user_name", "score": "user_score"})

drop()

删除列。

df.drop(columns, *more_columns)
参数 类型 默认值 说明
columns str / list[str] 必填 要删除的列名
df.drop("id")
df.drop(["id", "temp_col"])

sort()

按列排序。

df.sort(by, *, descending=False, nulls_last=False, multithreaded=True, maintain_order=False)
参数 类型 默认值 说明
by str / Expr / list 必填 排序列或表达式
descending bool / list[bool] False 是否降序,可按列分别指定
nulls_last bool False null 值是否排在末尾
multithreaded bool True 是否多线程排序
maintain_order bool False 相同值保持原始顺序(稳定排序,较慢)
df.sort("score")
df.sort("score", descending=True)
df.sort(["score", "name"], descending=[True, False])
df.sort("score", nulls_last=True)

unique()

去除重复行。

df.unique(subset=None, *, keep="any", maintain_order=False)
参数 类型 默认值 说明
subset list[str] None 用于判断重复的列,None 则考虑所有列
keep str "any" 保留哪条:"any"(任意一条)/ "first"(第一条)/ "last"(最后一条)/ "none"(删除所有重复)
maintain_order bool False 保持原始行顺序(较慢)
df.unique()
df.unique(subset=["user_id"])
df.unique(subset=["user_id"], keep="last", maintain_order=True)

slice()

按位置切片。

df.slice(offset, length=None)
参数 类型 默认值 说明
offset int 必填 起始位置(支持负数,从末尾计)
length int None 切片长度,None 则取到末尾
df.slice(10, 20)      # 第 10~29 行
df.slice(-5)          # 最后 5 行

数据提取

# 获取单行(返回 dict)
df.row(0)
df.row(0, named=True)          # 返回 {列名: 值} 的字典

# 获取多行(返回 list of tuple)
df.rows()
df.rows(named=True)            # 返回 list of dict

# 转为 Series
df.to_series(0)                # 按索引
df["name"]                     # 按列名(等同于 df.get_column("name"))

# 转为字典列表
df.to_dicts()                  # [{列名: 值}, ...]

# 转为 numpy
df.to_numpy()                  # 二维 numpy 数组
df["score"].to_numpy()         # 一维数组

分组聚合(GroupBy)

group_by()

按列分组。

df.group_by(by, *more_by, maintain_order=False)
参数 类型 默认值 说明
by str / Expr / list 必填 分组列
maintain_order bool False 保持分组顺序(稍慢),默认结果顺序不保证

.agg() — 聚合

# 基础聚合
df.group_by("category").agg(
    pl.col("score").mean().alias("avg_score"),
    pl.col("score").max().alias("max_score"),
    pl.col("id").count().alias("count"),
)

# 对所有数值列求均值
df.group_by("category").agg(pl.col(pl.NUMERIC_DTYPES).mean())

# 多组聚合
df.group_by(["category", "region"]).agg(
    pl.col("revenue").sum(),
    pl.col("quantity").sum(),
    pl.n_unique("user_id").alias("unique_users"),
)

# 收集列值到列表
df.group_by("user_id").agg(pl.col("action").alias("actions"))

# 保证顺序
df.group_by("category", maintain_order=True).agg(pl.col("score").mean())

group_by_dynamic() — 动态分组(时序)

基于时间窗口的分组,处理时序数据的核心方法。

df.group_by_dynamic(index_column, *, every, period=None, offset=None, label="left", include_boundaries=False, closed="left", group_by=None, start_by="window")
参数 类型 默认值 说明
index_column str 必填 时间索引列(必须已排序)
every str / timedelta 必填 窗口间隔,如 "1d" / "1h" / "30m" / "1w"
period str / timedelta None 窗口大小,默认等于 every(滚动窗口)
offset str / timedelta None 窗口偏移量
label str "left" 窗口标签:"left"(起始)/ "right"(结束)/ "datapoint"
include_boundaries bool False 是否在结果中包含窗口起止列
closed str "left" 窗口边界:"left" / "right" / "both" / "none"
group_by str / list None 额外的分组键
start_by str "window" 窗口起始策略
# 按日聚合
df.sort("timestamp").group_by_dynamic("timestamp", every="1d").agg(
    pl.col("value").sum().alias("daily_sum"),
    pl.col("value").mean().alias("daily_avg"),
)

# 按小时聚合,并按 user_id 额外分组
df.sort("timestamp").group_by_dynamic(
    "timestamp", every="1h", group_by="user_id"
).agg(
    pl.col("event_count").sum(),
)

# 滑动窗口(period > every)
df.sort("date").group_by_dynamic(
    "date", every="1d", period="7d"  # 每天一个 7 天滑动窗口
).agg(pl.col("sales").sum().alias("rolling_7d_sum"))

时间间隔字符串说明:

字符串 含义
"1ns" 1 纳秒
"1us" 1 微秒
"1ms" 1 毫秒
"1s" 1 秒
"1m" 1 分钟
"1h" 1 小时
"1d" 1 天
"1w" 1 周
"1mo" 1 个月
"1q" 1 季度
"1y" 1 年

group_by_rolling()

基于行的滑动窗口分组(已被 group_by_dynamic 的 period 参数覆盖,较少使用)。

df.sort("date").group_by_rolling(
    index_column="date",
    period="7d",
    group_by="category",
).agg(pl.col("sales").sum())

连接操作

join()

DataFrame 连接。

df.join(other, on=None, how="inner", *, left_on=None, right_on=None, suffix="_right", validate="m:m", join_nulls=False, coalesce=None)
参数 类型 默认值 说明
other DataFrame 必填 右侧 DataFrame
on str / list None 两侧同名的连接键,与 left_on/right_on 二选一
how str "inner" 连接类型,见下表
left_on str / list None 左侧连接键(列名不同时使用)
right_on str / list None 右侧连接键
suffix str "_right" 右侧重名列的后缀
validate str "m:m" 键唯一性校验:"1:1" / "1:m" / "m:1" / "m:m"
join_nulls bool False null 是否视为相等的键
coalesce bool None 是否合并同名键列

how 类型说明:

how SQL 等价 说明
"inner" INNER JOIN 只保留两侧都有匹配键的行
"left" LEFT JOIN 保留左侧所有行,右侧无匹配则 null
"right" RIGHT JOIN 保留右侧所有行,左侧无匹配则 null
"full" FULL OUTER JOIN 保留两侧所有行
"semi" LEFT SEMI JOIN 保留左侧中键在右侧出现的行(不附加右侧列)
"anti" LEFT ANTI JOIN 保留左侧中键不在右侧出现的行
"cross" CROSS JOIN 笛卡尔积,所有行组合
df_orders = pl.DataFrame({"order_id": [1, 2, 3], "user_id": [101, 102, 101], "amount": [50.0, 80.0, 120.0]})
df_users = pl.DataFrame({"user_id": [101, 102, 103], "name": ["Alice", "Bob", "Charlie"]})

# 内连接
df_orders.join(df_users, on="user_id")

# 左连接(不同列名)
df_orders.join(df_users, left_on="user_id", right_on="user_id", how="left")

# Anti join:找出没有匹配用户的订单
df_orders.join(df_users, on="user_id", how="anti")

# 列名冲突处理
df1.join(df2, on="id", suffix="_df2")

join_asof()

时序对齐连接(AsOf Join)。按最近的键匹配,而非精确匹配,常用于将事件数据与价格/汇率等时序数据对齐。

df.join_asof(other, *, on=None, left_on=None, right_on=None, by=None, by_left=None, by_right=None, strategy="backward", suffix="_right", tolerance=None, coalesce=True)
参数 类型 默认值 说明
other DataFrame 必填 右侧 DataFrame(必须已按 on 列排序)
on str None 两侧同名的连接键(通常是时间列)
left_on / right_on str None 两侧列名不同时使用
by str / list None 精确匹配的额外分组键(如股票代码)
strategy str "backward" 匹配策略,见下表
tolerance int / float / str None 允许的最大偏差(超过则返回 null)

strategy 说明:

strategy 说明
"backward" 取右侧中小于等于左侧键的最大值(向前看价格,最常用)
"forward" 取右侧中大于等于左侧键的最小值
"nearest" 取右侧中距离最近的键
df_trades = pl.DataFrame({
    "time": [1, 2, 5, 8],
    "trade_id": [1, 2, 3, 4],
}).sort("time")

df_prices = pl.DataFrame({
    "time": [1, 3, 6],
    "price": [100.0, 105.0, 110.0],
}).sort("time")

# 每笔交易匹配最近的(不晚于交易时间的)价格
df_trades.join_asof(df_prices, on="time", strategy="backward")

# 按股票分组的时序对齐
df_trades.join_asof(df_prices, on="time", by="symbol", strategy="backward")

# 设置容忍度:超过 2 秒则不匹配
df_trades.join_asof(df_prices, on="time", strategy="backward", tolerance=2)

数据重塑

unpivot() / melt()

宽格式转长格式(Wide to Long)。melt()unpivot() 的别名,行为相同。

df.unpivot(on=None, *, index=None, variable_name="variable", value_name="value")
参数 类型 默认值 说明
on list[str] None 要转为长格式的列(值列),None 则转除 index 外所有列
index list[str] None 保持不变的标识列(id 列)
variable_name str "variable" 新列名(存放原列名)
value_name str "value" 新列名(存放原值)
df = pl.DataFrame({
    "id": [1, 2],
    "jan": [100, 200],
    "feb": [110, 220],
    "mar": [120, 230],
})

df.unpivot(on=["jan", "feb", "mar"], index="id", variable_name="month", value_name="sales")
# 结果:
# id | month | sales
#  1 | jan   |  100
#  1 | feb   |  110
# ...

pivot()

长格式转宽格式(Long to Wide)。

df.pivot(on, *, index=None, values=None, aggregate_function="first", maintain_order=True, sort_columns=False, separator="_")
参数 类型 默认值 说明
on str / list 必填 值将成为新列名的列
index str / list None 行标识列,None 则用其余所有列
values str / list None 填入值的列,None 则用除 on 和 index 外的列
aggregate_function str "first" 聚合函数:"first" / "last" / "min" / "max" / "sum" / "mean" / "count"
maintain_order bool True 保持行顺序
sort_columns bool False 是否对新生成的列名排序
df_long = pl.DataFrame({
    "id": [1, 1, 2, 2],
    "month": ["jan", "feb", "jan", "feb"],
    "sales": [100, 110, 200, 220],
})

df_long.pivot(on="month", index="id", values="sales")
# 结果:
# id | jan | feb
#  1 | 100 | 110
#  2 | 200 | 220

explode()

将列表列展开为多行。

df.explode(columns, *more_columns)
参数 类型 默认值 说明
columns str / list 必填 要展开的列表列
df = pl.DataFrame({"id": [1, 2], "tags": [["python", "data"], ["sql"]]})
df.explode("tags")
# 结果:
# id | tags
#  1 | python
#  1 | data
#  2 | sql

unnest()

将结构体列(Struct)拆分为多个普通列。

df.unnest(columns, *more_columns)
参数 类型 默认值 说明
columns str / list 必填 要展开的结构体列
df = pl.DataFrame({"id": [1, 2], "info": [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 30}]})
df.unnest("info")
# 结果:id | name | age

合并

pl.concat()

合并多个 DataFrame 或 Series。

pl.concat(items, *, how="vertical", rechunk=False, parallel=True)
参数 类型 默认值 说明
items list[DataFrame/Series/LazyFrame] 必填 要合并的对象列表
how str "vertical" 合并方式,见下表
rechunk bool False 合并后重整内存块(消耗时间,但后续操作更快)
parallel bool True 是否并行读取各分片

how 类型说明:

how 说明
"vertical" 纵向拼接(行合并),列必须相同
"vertical_relaxed" 纵向拼接,类型不同时自动转换
"diagonal" 纵向拼接,列可不同,缺失列填 null
"diagonal_relaxed" 纵向拼接,列可不同,类型不同自动转换
"horizontal" 横向拼接(列合并),行数必须相同
"align" 按公共列对齐后合并
# 纵向合并(行追加)
df_all = pl.concat([df1, df2, df3])

# 列不完全相同的合并(缺失列补 null)
df_all = pl.concat([df1, df2], how="diagonal")

# 横向合并(列追加)
df_wide = pl.concat([df_a, df_b], how="horizontal")

# LazyFrame 合并
lf_all = pl.concat([lf1, lf2])

字符串操作(str namespace)

通过 pl.col("col").str.方法() 调用。

str.contains()

pl.col("text").str.contains(pattern, *, literal=False)
参数 类型 默认值 说明
pattern str 必填 匹配模式(正则或字面量)
literal bool False True 则视为字面量,False 则视为正则
pl.col("text").str.contains("python")
pl.col("text").str.contains(r"py\w+", literal=False)
pl.col("url").str.contains("https://", literal=True)

pl.col("name").str.starts_with("A")
pl.col("email").str.ends_with(".com")

str.replace() / str.replace_all()

pl.col("text").str.replace(pattern, value, *, literal=False, n=1)
pl.col("text").str.replace_all(pattern, value, *, literal=False)
参数 类型 默认值 说明
pattern str 必填 匹配模式
value str 必填 替换内容,支持正则捕获组引用 $1
literal bool False 是否字面量匹配
n int 1 replace() 中替换的最大次数
pl.col("text").str.replace("foo", "bar")
pl.col("text").str.replace_all(r"\s+", " ")   # 合并多个空格
pl.col("text").str.replace_all(r"(\w+)", "[$1]", literal=False)  # 正则替换

str.strip_chars() 系列

pl.col("text").str.strip_chars()              # 去除两端空白
pl.col("text").str.strip_chars(" \t")         # 去除两端指定字符
pl.col("text").str.lstrip_chars()             # 去除左端空白
pl.col("text").str.rstrip_chars()             # 去除右端空白

str.split()

pl.col("text").str.split(by, *, inclusive=False)
参数 类型 默认值 说明
by str 必填 分隔符
inclusive bool False 是否在每个分割结果中保留分隔符
pl.col("csv_line").str.split(",")               # 返回 List[String] 列
pl.col("csv_line").str.split(",").list.get(0)  # 取第一个分割结果

str.extract() / str.extract_groups()

# 提取第一个捕获组
pl.col("text").str.extract(r"(\d+)", group_index=1)

# 提取所有捕获组为结构体
pl.col("text").str.extract_groups(r"(\w+)-(\d+)")
# 返回 Struct{field_0: String, field_1: String}
参数 类型 默认值 说明
pattern str 必填 正则表达式(必须含捕获组)
group_index int 1 extract() 中要提取的捕获组索引

大小写转换

pl.col("text").str.to_uppercase()
pl.col("text").str.to_lowercase()
pl.col("text").str.to_titlecase()

长度计算

pl.col("text").str.len_chars()   # 字符数(Unicode 字符)
pl.col("text").str.len_bytes()   # 字节数

数值转换

pl.col("num_str").str.to_integer(base=10, strict=True)   # 字符串转整数
pl.col("num_str").str.to_decimal(inference_length=100)    # 字符串转 Decimal

str.strptime() — 字符串转日期时间

pl.col("date_str").str.strptime(dtype, format=None, *, strict=True, exact=True, cache=True, use_earliest=None, ambiguous="raise")
参数 类型 默认值 说明
dtype pl.Date / pl.Datetime / pl.Time 必填 目标日期时间类型
format str None 格式字符串(如 "%Y-%m-%d"),None 则自动推断
strict bool True 解析失败时报错(False 则返回 null)
exact bool True 是否要求整串匹配(False 则允许前缀匹配)
cache bool True 是否缓存解析结果(重复值多时加速)
use_earliest bool None 夏令时模糊时段,是否取最早的时间
ambiguous str "raise" 夏令时模糊处理:"raise" / "earliest" / "latest" / "null"
pl.col("date_str").str.strptime(pl.Date, "%Y-%m-%d")
pl.col("dt_str").str.strptime(pl.Datetime, "%Y-%m-%d %H:%M:%S")
pl.col("dt_str").str.strptime(pl.Datetime("us", "Asia/Shanghai"), "%Y-%m-%d %H:%M:%S")
pl.col("date_str").str.strptime(pl.Date, strict=False)  # 解析失败返回 null

日期时间操作(dt namespace)

通过 pl.col("col").dt.方法/属性 调用。

时间属性

pl.col("date").dt.year()
pl.col("date").dt.month()
pl.col("date").dt.day()
pl.col("date").dt.hour()
pl.col("date").dt.minute()
pl.col("date").dt.second()
pl.col("date").dt.millisecond()
pl.col("date").dt.microsecond()
pl.col("date").dt.nanosecond()
pl.col("date").dt.weekday()      # 星期几(1=周一 ... 7=周日)
pl.col("date").dt.week()         # 一年中的第几周(ISO 周)
pl.col("date").dt.day_of_year()  # 一年中的第几天
pl.col("date").dt.quarter()      # 季度(1~4)
pl.col("date").dt.ordinal_day()  # 儒略历天数

dt.strftime()

pl.col("date").dt.strftime(format)
参数 类型 默认值 说明
format str 必填 格式字符串,与 Python strftime 相同
pl.col("datetime").dt.strftime("%Y-%m-%d")
pl.col("datetime").dt.strftime("%Y/%m/%d %H:%M:%S")
pl.col("datetime").dt.strftime("%Y年%m月%d日")

dt.truncate()

截断到指定时间粒度(如将时间截断到小时)。

pl.col("datetime").dt.truncate(every, offset="0ns")
参数 类型 默认值 说明
every str / timedelta 必填 截断粒度,如 "1h" / "1d" / "15m"
offset str / timedelta "0ns" 偏移量
pl.col("datetime").dt.truncate("1h")    # 截断到小时
pl.col("datetime").dt.truncate("1d")    # 截断到天
pl.col("datetime").dt.truncate("15m")   # 截断到 15 分钟

dt.offset_by()

日期偏移运算。

pl.col("date").dt.offset_by(by)
参数 类型 默认值 说明
by str 必填 偏移量,正数向后,负数向前,如 "1d" / "-7d" / "1mo" / "1y"
pl.col("date").dt.offset_by("1d")      # 加 1 天
pl.col("date").dt.offset_by("-7d")     # 减 7 天
pl.col("date").dt.offset_by("1mo")     # 加 1 个月(自动处理月末)
pl.col("date").dt.offset_by("1y")      # 加 1 年

Duration 方法

当列为 Duration 类型时使用。

pl.col("duration").dt.total_seconds()
pl.col("duration").dt.total_milliseconds()
pl.col("duration").dt.total_microseconds()
pl.col("duration").dt.total_nanoseconds()
pl.col("duration").dt.total_minutes()
pl.col("duration").dt.total_hours()
pl.col("duration").dt.total_days()

# Duration 通过两个日期相减得到
df.with_columns((pl.col("end_date") - pl.col("start_date")).alias("duration"))

pl.date_range() / pl.datetime_range()

生成日期/时间序列。

pl.date_range(start, end, interval="1d", *, eager=False, name=None)
pl.datetime_range(start, end, interval="1h", *, time_unit="us", time_zone=None, eager=False, name=None)
参数 类型 默认值 说明
start date / datetime / str / Expr 必填 起始时间
end date / datetime / str / Expr 必填 结束时间(包含)
interval str / timedelta "1d" 间隔
eager bool False True 则立即返回 Series,False 则返回表达式
time_unit str "us" 时间精度:"ns" / "us" / "ms"
time_zone str None 时区,如 "Asia/Shanghai"
import datetime

# 生成日期序列
dates = pl.date_range(
    datetime.date(2024, 1, 1),
    datetime.date(2024, 12, 31),
    interval="1mo",
    eager=True,
)

# 在表达式中使用(用于 with_columns)
df.with_columns(
    pl.datetime_range(pl.col("start"), pl.col("end"), "1d").alias("dates")
)

时区处理

# 为无时区的 Datetime 设置时区(声明,不转换)
pl.col("dt").dt.replace_time_zone("Asia/Shanghai")

# 转换时区(转换,保持绝对时间不变)
pl.col("dt").dt.convert_time_zone("America/New_York")

惰性执行(LazyFrame)

LazyFrame 是 Polars 高性能的核心。所有操作构建执行计划,collect() 时触发优化并执行。

创建 LazyFrame

# 从 DataFrame 转换
lf = df.lazy()

# 直接从文件扫描(最佳实践)
lf = pl.scan_csv("data.csv")
lf = pl.scan_parquet("data.parquet")
lf = pl.scan_ndjson("data.ndjson")

lf.collect()

触发执行,返回 DataFrame。

lf.collect(*, streaming=False, background=False, comm_subplan_elim=True, projection_pushdown=True, predicate_pushdown=True, cluster_with_columns=True, no_optimization=False, slice_pushdown=True, new_streaming=False)
参数 类型 默认值 说明
streaming bool False 启用流式处理,减少内存峰值(部分操作不支持)
no_optimization bool False 禁用所有优化(调试用)
projection_pushdown bool True 是否启用投影下推
predicate_pushdown bool True 是否启用谓词下推
comm_subplan_elim bool True 公共子计划消除
result = lf.collect()
result = lf.collect(streaming=True)  # 流式执行,适合大数据集

查看执行计划

# 文本形式的逻辑计划
print(lf.explain())

# 文本形式的优化后计划
print(lf.explain(optimized=True))

# 可视化执行计划(需要安装 graphviz)
lf.show_graph()
lf.show_graph(optimized=True)

# 收集执行统计信息
result, stats = lf.profile()

谓词下推(Predicate Pushdown)

Polars 自动将 filter() 条件尽量靠近数据源执行,减少读取的数据量。

# Polars 会将 filter 推到 scan_csv 层面,只读取满足条件的行
result = (
    pl.scan_csv("large_data.csv")
    .filter(pl.col("year") == 2024)        # 谓词下推:读 CSV 时就过滤
    .filter(pl.col("amount") > 1000)       # 多个 filter 都会被下推
    .select(["id", "date", "amount"])
    .collect()
)

投影下推(Projection Pushdown)

Polars 自动只读取 select() 中实际用到的列,对 Parquet 等列式格式效果显著。

# 只读取 id 和 amount 两列
result = (
    pl.scan_parquet("large_data.parquet")
    .select(["id", "amount"])              # 投影下推:只读这两列
    .filter(pl.col("amount") > 1000)
    .collect()
)

何时用 LazyFrame,何时用 Eager

场景 推荐模式
数据文件较大(>500MB) Lazy + scan_*
多步操作链 Lazy,collect() 一次触发
交互式探索少量数据 Eager
需要中间结果进行条件分支 在关键点 collect(),再 lazy()
内存受限的大数据集 Lazy + collect(streaming=True)

lf.sink_parquet() / lf.sink_csv()

流式写出,不将结果加载到内存。

lf.sink_parquet(path, *, compression="zstd", compression_level=None, statistics=False, row_group_size=None, data_page_size=None, maintain_order=True)
参数 类型 默认值 说明
path str / Path 必填 输出路径
compression str "zstd" 压缩算法
maintain_order bool True 是否保持行顺序
# 大数据集流式写出,不占用内存
(
    pl.scan_csv("large_input.csv")
    .filter(pl.col("status") == "active")
    .with_columns(pl.col("amount") * 1.1)
    .sink_parquet("output.parquet")
)

(
    pl.scan_parquet("large.parquet")
    .select(["id", "name", "value"])
    .sink_csv("output.csv")
)

流式处理(Streaming)

collect(streaming=True)

result = lf.collect(streaming=True)

流式处理原理:Polars 将数据分批处理,每批处理完后释放内存,峰值内存大幅降低。适合数据集大小超出可用内存的场景。

适用场景

  • 数据集大于可用内存的 50% 时考虑使用
  • ETL 管道:扫描大文件,过滤,写出
  • 配合 sink_parquet() / sink_csv() 实现全程流式处理
# 全程流式,内存占用极低
(
    pl.scan_csv("100gb_data.csv")
    .filter(pl.col("date") >= "2024-01-01")
    .group_by("category")
    .agg(pl.col("value").sum())
    .sink_parquet("result.parquet")
)

流式处理的限制

部分操作目前不支持流式处理,会自动回退到非流式模式:

  • sort()(全局排序需要全量数据)
  • join() 中的部分类型
  • pivot()
  • 某些窗口函数

Polars 会自动降级,不会报错,但内存使用会升高。


窗口函数

over() — 窗口聚合

Polars 的窗口函数语法,等同于 SQL 的 OVER (PARTITION BY ...)。与 group_by 不同,over() 不减少行数,每行保留,但值替换为窗口聚合结果。

pl.col("expr").over(partition_by, *, order_by=None, mapping_strategy="group_to_rows")
参数 类型 默认值 说明
partition_by str / list 必填 分区键,相当于 PARTITION BY
order_by str / Expr None 窗口内排序键(用于 rank/cumsum 等需要顺序的聚合)
mapping_strategy str "group_to_rows" 结果映射策略,一般不需要修改
# 每行添加所在分组的聚合值
df.with_columns(
    pl.col("score").mean().over("department").alias("dept_avg_score"),
    pl.col("score").max().over("department").alias("dept_max_score"),
    pl.col("score").count().over("department").alias("dept_count"),
)

# 组内排名
df.with_columns(
    pl.col("score").rank(descending=True).over("department").alias("dept_rank")
)

# 组内累积和(需要 order_by)
df.with_columns(
    pl.col("sales").cum_sum().over("store", order_by="date").alias("cumulative_sales")
)

# 组内 lag/lead(需要 order_by)
df.with_columns(
    pl.col("price").shift(1).over("symbol", order_by="date").alias("prev_price")
)

rolling() 表达式

在时间窗口内进行滚动计算。

pl.col("value").rolling_mean(window_size, *, weights=None, min_periods=None, center=False)
pl.col("value").rolling_sum(window_size, *, weights=None, min_periods=None, center=False)
pl.col("value").rolling_std(window_size, *, weights=None, min_periods=None, center=False, ddof=1)
pl.col("value").rolling_min(window_size, *, min_periods=None, center=False)
pl.col("value").rolling_max(window_size, *, min_periods=None, center=False)
参数 类型 默认值 说明
window_size int / str 必填 窗口大小(行数或时间字符串)
weights list[float] None 窗口权重
min_periods int None 窗口内最少有效值数,默认等于 window_size
center bool False 是否居中窗口(而非右对齐)
df.with_columns(
    pl.col("price").rolling_mean(7).alias("ma7"),            # 7 日均线
    pl.col("price").rolling_mean(30, min_periods=1).alias("ma30"),  # 允许不足 30 天
    pl.col("price").rolling_std(20).alias("volatility"),
)

性能特性

自动多线程并行

Polars 使用 Rust 的 Rayon 线程池,自动并行化以下操作:

  • 多个无依赖的 with_columns() 表达式并行执行
  • group_by / join 使用多线程
  • 文件读取并行化
  • LazyFrame 查询计划中并行节点同步执行

无需任何配置,自动利用所有 CPU 核心。

Arrow 列式存储优势

  • 同列数据内存连续,SIMD 向量化指令生效,聚合操作极快
  • 列式读取 Parquet 时可跳过不需要的列(零 IO)
  • 与 DuckDB、PyArrow 等 Arrow 生态库零拷贝互操作

与 Pandas 性能对比

操作 Polars 优势说明
groupby + agg 多线程哈希聚合,通常快 5~15 倍
filter 谓词下推 + SIMD,快 3~10 倍
join 并行哈希 join,快 3~10 倍
read_csv 并行解析,快 5~20 倍
sort 并行排序,快 3~8 倍
apply(map_elements) 接近 pandas apply,优势消失

内存使用对比

  • Polars 使用 Arrow 格式,整数列不需要为 null 转为 float64
  • 字符串使用 LargeUtf8 编码,避免对象数组的开销
  • 通常比 pandas 少用 30%~60% 内存
  • LazyFrame + streaming 可处理超出 RAM 的数据集

用 lf.explain() 分析查询计划

lf = (
    pl.scan_parquet("large.parquet")
    .filter(pl.col("year") == 2024)
    .group_by("category")
    .agg(pl.col("value").sum())
)

# 查看未优化计划
print(lf.explain(optimized=False))

# 查看优化后计划(确认谓词下推是否生效)
print(lf.explain(optimized=True))

# 查看执行耗时分布(用于性能调优)
result, profile = lf.profile()
print(profile)

常用配合库

NumPy

import numpy as np

# numpy -> polars Series
arr = np.array([1.0, 2.0, 3.0])
s = pl.Series("data", arr)

# numpy -> polars DataFrame
arr2d = np.random.rand(100, 3)
df = pl.from_numpy(arr2d, schema=["a", "b", "c"])

# polars Series -> numpy
arr = df["score"].to_numpy()                    # 无 null 时效率最高
arr = df["score"].to_numpy(allow_copy=True)     # 有 null 时需要 allow_copy

# polars DataFrame -> numpy
arr2d = df.to_numpy()

Pandas

import pandas as pd

# pandas -> polars
df_pl = pl.from_pandas(pdf)
df_pl = pl.from_pandas(pdf, nan_to_null=True, include_index=True)

# polars -> pandas
pdf = df_pl.to_pandas()
pdf = df_pl.to_pandas(use_pyarrow_extension_array=True)  # 保留 null 语义

PyArrow

import pyarrow as pa

# polars -> arrow
arrow_table = df.to_arrow()

# arrow -> polars
df = pl.from_arrow(arrow_table)

# 零拷贝:polars 和 arrow 共享内存(不需要拷贝数据)

Matplotlib / Seaborn 绘图

Polars 无原生绘图支持,通过转换为 pandas 使用:

import matplotlib.pyplot as plt
import seaborn as sns

# 转换后绘图
pdf = df.to_pandas()
pdf.plot(x="date", y="value")
sns.histplot(pdf["score"])
plt.show()

# 或直接取 numpy 数组
plt.scatter(df["x"].to_numpy(), df["y"].to_numpy())

DuckDB — SQL on Polars

DuckDB 可以直接查询 Polars DataFrame(Arrow 协议,无拷贝):

import duckdb

df = pl.DataFrame({"id": [1, 2, 3], "value": [10, 20, 30]})

# 直接用 SQL 查询 Polars DataFrame
result = duckdb.sql("SELECT id, value * 2 as doubled FROM df").pl()

# 复杂 SQL
conn = duckdb.connect()
result = conn.execute("""
    SELECT category, SUM(amount) as total
    FROM df
    GROUP BY category
    ORDER BY total DESC
""").pl()  # .pl() 返回 Polars DataFrame

Connectorx — 高性能数据库读取

import connectorx as cx

# 高性能从数据库读取,直接返回 Polars DataFrame(Arrow 协议)
df = cx.read_sql(
    "postgresql://user:pass@host:5432/db",
    "SELECT * FROM orders WHERE status = 'active'",
    return_type="polars",
)

最佳实践

优先使用 LazyFrame + collect() 模式

# 不推荐:每步操作立即执行,无法优化
df1 = df.filter(pl.col("year") == 2024)
df2 = df1.select(["id", "value"])
df3 = df2.group_by("id").agg(pl.col("value").sum())

# 推荐:构建完整计划后一次执行,Polars 自动优化
result = (
    df.lazy()
    .filter(pl.col("year") == 2024)
    .select(["id", "value"])
    .group_by("id")
    .agg(pl.col("value").sum())
    .collect()
)

用 select + with_columns 而不是逐列赋值

# 不推荐(pandas 风格,Polars 不支持就地赋值)
# df["new_col"] = df["price"] * 1.1  # Polars 不支持此语法

# 推荐:with_columns 批量添加(多列并行执行)
df = df.with_columns(
    (pl.col("price") * 1.1).alias("price_tax"),
    pl.col("name").str.to_uppercase().alias("name_upper"),
    (pl.col("score") - pl.col("score").mean()).alias("score_norm"),
)

链式表达式风格(Method Chaining)

result = (
    pl.scan_csv("data.csv")
    .filter(pl.col("date") >= "2024-01-01")
    .filter(pl.col("amount") > 0)
    .with_columns(
        (pl.col("amount") * pl.col("quantity")).alias("revenue"),
        pl.col("date").str.strptime(pl.Date, "%Y-%m-%d"),
    )
    .group_by(["category", pl.col("date").dt.month().alias("month")])
    .agg(
        pl.col("revenue").sum().alias("total_revenue"),
        pl.col("amount").mean().alias("avg_amount"),
    )
    .sort("total_revenue", descending=True)
    .collect()
)

用 when/then/otherwise 替代 map_elements

# 不推荐:map_elements 走 Python 循环,极慢
df.with_columns(
    pl.col("score").map_elements(lambda x: "A" if x >= 90 else "B").alias("grade")
)

# 推荐:when/then/otherwise 在 Rust 层执行,极快
df.with_columns(
    pl.when(pl.col("score") >= 90).then(pl.lit("A"))
    .when(pl.col("score") >= 80).then(pl.lit("B"))
    .otherwise(pl.lit("C"))
    .alias("grade")
)

避免 map_elements(相当于 pandas apply)

# 仅在没有原生表达式替代时才使用 map_elements
# 尽量找到对应的原生表达式

# 字符串处理:用 str namespace
pl.col("text").map_elements(lambda x: x.upper())  # 不推荐
pl.col("text").str.to_uppercase()                  # 推荐

# 数学计算:用算术表达式
pl.col("x").map_elements(lambda x: x ** 2)  # 不推荐
pl.col("x") ** 2                             # 推荐

# 条件逻辑:用 when/then/otherwise
# (见上例)

尽量在 LazyFrame 阶段完成 filter

# 推荐:filter 在 collect 之前,利用谓词下推
result = (
    pl.scan_parquet("huge_file.parquet")
    .filter(pl.col("status") == "active")   # 读取时就过滤
    .collect()
)

# 不推荐:collect 后再 filter,已经将全量数据加载到内存
result = pl.read_parquet("huge_file.parquet").filter(pl.col("status") == "active")

读大文件用 scan_* 而不是 read_*

# 不推荐:一次性加载全部数据到内存
df = pl.read_csv("100gb.csv")

# 推荐:惰性扫描,配合过滤和投影只读需要的数据
result = (
    pl.scan_csv("100gb.csv")
    .filter(pl.col("region") == "China")
    .select(["id", "name", "value"])
    .collect()
)

应用场景

大型 CSV 数据分析流水线

result = (
    pl.scan_csv(
        "sales_data.csv",
        try_parse_dates=True,
        null_values=["", "N/A"],
        schema_overrides={"amount": pl.Float64},
    )
    .filter(
        (pl.col("date") >= datetime.date(2024, 1, 1)) &
        (pl.col("status") == "completed")
    )
    .with_columns(
        pl.col("amount").fill_null(0).alias("amount"),
        pl.col("date").dt.month().alias("month"),
        pl.col("date").dt.year().alias("year"),
        (pl.col("amount") * pl.col("quantity")).alias("revenue"),
    )
    .group_by(["year", "month", "category"])
    .agg(
        pl.col("revenue").sum().alias("total_revenue"),
        pl.col("amount").mean().alias("avg_amount"),
        pl.col("id").count().alias("order_count"),
    )
    .sort(["year", "month", "total_revenue"], descending=[False, False, True])
    .collect()
)

时序数据处理

df = pl.read_csv("sensor_data.csv", try_parse_dates=True)

result = (
    df
    .sort("timestamp")
    .group_by_dynamic("timestamp", every="1h", group_by="sensor_id")
    .agg(
        pl.col("temperature").mean().alias("avg_temp"),
        pl.col("temperature").max().alias("max_temp"),
        pl.col("temperature").min().alias("min_temp"),
        pl.col("temperature").std().alias("std_temp"),
        pl.col("value").count().alias("reading_count"),
    )
    .with_columns(
        pl.col("avg_temp").rolling_mean(24).over("sensor_id").alias("ma_24h")
    )
)

ETL 数据转换

result = (
    pl.scan_parquet("raw_orders.parquet")
    .with_columns(
        # 清洗空值
        pl.col("amount").fill_null(0),
        pl.col("customer_name").fill_null("Unknown"),
        # 类型转换
        pl.col("order_date").str.strptime(pl.Date, "%Y-%m-%d"),
        # 派生列
        (pl.col("amount") * pl.col("tax_rate")).alias("tax_amount"),
        # 分类
        pl.when(pl.col("amount") > 10000).then(pl.lit("large"))
        .when(pl.col("amount") > 1000).then(pl.lit("medium"))
        .otherwise(pl.lit("small"))
        .alias("order_size"),
    )
    .filter(pl.col("status").is_in(["active", "completed"]))
    .rename({"customer_name": "name", "order_date": "date"})
    .collect()
)

result.write_parquet("clean_orders.parquet", compression="zstd", statistics=True)

替换 Pandas 的迁移模式

操作 Pandas 写法 Polars 写法
读取 CSV pd.read_csv("f.csv") pl.read_csv("f.csv")
过滤行 df[df["age"] > 25] df.filter(pl.col("age") > 25)
选择列 df"name", "age" df.select(["name", "age"])
添加列 df["new"] = df["a"] + 1 df.with_columns((pl.col("a") + 1).alias("new"))
分组聚合 df.groupby("cat")["val"].sum() df.group_by("cat").agg(pl.col("val").sum())
排序 df.sort_values("col", ascending=False) df.sort("col", descending=True)
去重 df.drop_duplicates(subset=["id"]) df.unique(subset=["id"])
宽转长 df.melt(id_vars=["id"]) df.unpivot(index=["id"])
条件赋值 df["col"] = np.where(cond, a, b) df.with_columns(pl.when(cond).then(a).otherwise(b).alias("col"))
apply df["col"].apply(func) df["col"].map_elements(func)(尽量避免)
fillna df["col"].fillna(0) df.with_columns(pl.col("col").fill_null(0))
merge pd.merge(df1, df2, on="id") df1.join(df2, on="id")
concat pd.concat([df1, df2]) pl.concat([df1, df2])
字符串操作 df["col"].str.upper() pl.col("col").str.to_uppercase()
日期提取 df["date"].dt.year pl.col("date").dt.year()

常见陷阱与注意事项

Polars 没有 index

Polars DataFrame 没有行索引(不像 pandas 有 Index 对象)。无法通过 df.loc[...]df.iloc[...] 访问。用 filter() 过滤行,用 slice() 按位置切片。

# pandas 风格(在 Polars 中无效)
# df.loc[df["name"] == "Alice"]   # 错误

# Polars 正确写法
df.filter(pl.col("name") == "Alice")
df.row(0, named=True)   # 按位置取单行
df.slice(0, 10)         # 按位置切片

列名重复会报错

Polars 不允许列名重复。join 时如果两侧有同名列(非连接键),会自动加 suffix,但如果 with_columns 操作产生同名列则会覆盖(不报错)。

# join 时处理重名列
df1.join(df2, on="id", suffix="_right")

# 主动重命名避免冲突
df2_renamed = df2.rename({"value": "value_right"})

is_in() vs isin()

Polars 使用 is_in(),没有 pandas 的 isin()

pl.col("city").is_in(["Beijing", "Shanghai"])   # Polars
# df["city"].isin(["Beijing", "Shanghai"])       # Pandas(Polars 中不存在)

group_by 结果顺序不保证

默认情况下 group_by() 的结果顺序是不确定的(利用并行哈希表,顺序取决于执行)。如需稳定顺序:

# 方法 1:maintain_order=True(稍慢)
df.group_by("category", maintain_order=True).agg(pl.col("value").sum())

# 方法 2:聚合后 sort(推荐,明确意图)
df.group_by("category").agg(pl.col("value").sum()).sort("category")

map_elements 比原生表达式慢很多

# map_elements 调用 Python 解释器处理每个值,破坏 Polars 的 Rust 并行优化
# 性能通常比原生表达式慢 10~100 倍

# 碰到需要 map_elements 的场景,先思考是否有原生替代:
# - 字符串操作  -> str namespace
# - 数学运算    -> 算术表达式
# - 条件分支    -> when/then/otherwise
# - 日期操作    -> dt namespace

# 实在没有原生替代时,才使用 map_elements,并指定 return_dtype 提升性能
pl.col("text").map_elements(custom_func, return_dtype=pl.String)

时区处理:replace_time_zone vs convert_time_zone

# replace_time_zone:声明时区(不改变时间值,仅附加时区信息)
# 用于:从无时区 Datetime 变为有时区 Datetime
pl.col("dt").dt.replace_time_zone("Asia/Shanghai")

# convert_time_zone:转换时区(改变时间值,保持绝对时间不变)
# 用于:在两个时区之间转换
pl.col("dt").dt.convert_time_zone("America/New_York")

# 典型流程:读入 naive datetime -> 声明时区 -> 转换时区
pl.col("dt").dt.replace_time_zone("UTC").dt.convert_time_zone("Asia/Shanghai")

null vs NaN 的严格区分

Polars 严格区分 null(缺失值)和 NaN(浮点非数值),pandas 通常混用两者。

# null:任何类型都可以有,表示"缺失数据"
pl.col("value").is_null()
pl.col("value").fill_null(0)

# NaN:只有浮点列有,表示"不是数字"(如 0/0 的结果)
pl.col("value").is_nan()
pl.col("value").fill_nan(0.0)
pl.col("value").fill_nan(None)  # NaN -> null

# 从 pandas/numpy 导入时,NaN 默认转为 null(nan_to_null=True)
df = pl.from_pandas(pdf, nan_to_null=True)

# describe() 对 null 和 NaN 的统计行为不同:
# - null 不参与 mean/sum 等聚合
# - NaN 会使 mean/sum 结果为 NaN(需要先 fill_nan)

join 时列名冲突处理

# join 时,右侧与左侧同名但不是连接键的列,会自动加 suffix
df1 = pl.DataFrame({"id": [1, 2], "value": [10, 20], "name": ["a", "b"]})
df2 = pl.DataFrame({"id": [1, 2], "value": [100, 200], "info": ["x", "y"]})

result = df1.join(df2, on="id")
# 结果列:id, value, name, value_right, info
# value_right 是 df2 的 value 列

# 自定义 suffix
result = df1.join(df2, on="id", suffix="_df2")
# 结果列:id, value, name, value_df2, info

其他常见注意事项

# 1. Polars 列名区分大小写
df.select("Name")  # 与 "name" 是不同的列

# 2. 链式操作不修改原 DataFrame(不可变),必须赋值
df.sort("score")            # 不修改 df
df = df.sort("score")       # 正确

# 3. 多个 filter 等价于 AND,不是 OR
df.filter(pl.col("a") > 1, pl.col("b") > 2)  # a > 1 AND b > 2

# 4. 惰性模式下的 collect 是唯一触发点,不要在循环中 collect
for item in items:
    lf = lf.filter(...)    # 只构建计划,不执行
result = lf.collect()       # 最后一次性执行

# 5. LazyFrame 不支持 len() / shape,需要 collect 后获取
lf.collect().shape
# 或用 lf.select(pl.len()).collect().item() 获取行数(不加载全部数据)

参见

Pandas完全指南
SQLAlchemy完全指南
FastAPI完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog