2. Polars基础入门
在金融数据处理场景中,数据规模、字段类型和计算链路往往比通用数据分析更复杂:行情快照、逐笔成交、因子矩阵、持仓明细,动辄百万行起步,且需要频繁做时间对齐、分组聚合和滚动窗口计算。Polars 作为基于 Rust 与 Arrow 内存格式的列式 DataFrame 库,正是为这类高吞吐、低延迟任务设计的。本节从安装配置开始,逐步建立对 DataFrame、Series 的直观认识,并厘清它与 Pandas 的关键差异,最后完成一次金融数据的读取与快速预览。
一、安装配置:轻量起步,按需扩展
Polars 的安装非常直接,核心包即可完成绝大多数金融数据清洗任务。若需要与 Pandas、NumPy 互转,或读取 Excel、数据库,再按需安装可选依赖。
# 基础安装(推荐使用虚拟环境)
pip install polars
# 常用扩展:Pandas 互转、Excel 读取、连接器
pip install polars[pandas,excel,connectorx]
# 验证版本与后端
python -c "import polars as pl; print(pl.__version__)"
配置层面,Polars 默认会使用全部可用 CPU 核心并行执行。对于金融回测或批量因子计算,通常不需要额外设置;若在共享服务器上运行,可通过环境变量限制线程数,避免抢占资源:
export POLARS_MAX_THREADS=8
pl.Config.set_tbl_rows(20) 与 pl.Config.set_fmt_str_lengths(50),便于预览宽表与长字符串字段。二、核心数据结构:DataFrame 与 Series
Polars 的两大核心结构是 DataFrame 和 Series。DataFrame 是二维表,由多个命名的 Series 组成;Series 是一维同类型数组,是列式存储的基本单位。理解这一点,是后续使用表达式 API 的基础。
import polars as pl
# 从字典构造 DataFrame
df = pl.DataFrame({
"trade_date": ["2024-01-02", "2024-01-03", "2024-01-04"],
"symbol": ["IF2401", "IF2401", "IF2401"],
"close": [3421.6, 3408.2, 3435.9],
"volume": [81234, 76521, 90112],
})
# 取出一列,得到 Series
close_s = df["close"]
print(type(close_s)) # polars.series.series.Series
# Series 的基本统计
print(close_s.mean(), close_s.max(), close_s.min())
与 Pandas 不同,Polars 的 Series 不依赖索引,行位置由隐式序号决定。这意味着切片、过滤和连接都围绕“列”和“表达式”展开,而不是围绕 index 对齐。对金融数据而言,这反而更贴近实际:行情表的主键通常是 (symbol, trade_date),而不是一个自动生成的整数索引。
DataFrame 的 schema 是强类型的。构造时 Polars 会自动推断类型,也可以显式指定,避免日期被误读为字符串:
df = pl.DataFrame(
{
"trade_date": ["2024-01-02", "2024-01-03"],
"close": [3421.6, 3408.2],
},
schema={"trade_date": pl.Date, "close": pl.Float64},
)
三、与 Pandas 的差异:不只是“更快”
很多学习者会把 Polars 当作“更快的 Pandas”,但两者在 API 哲学上有本质区别。Pandas 以索引为中心,支持链式赋值和就地修改;Polars 以表达式为中心,强调不可变、并行和惰性执行。下表列出金融数据清洗中最常遇到的差异点。
| 对比维度 | Pandas | Polars |
|---|---|---|
| 内存模型 | NumPy 块 + 索引 | Arrow 列式 + 无索引 |
| 执行方式 | 即时执行 | 即时 + 惰性(LazyFrame) |
| 并行能力 | 有限,多需手动优化 | 默认多核并行 |
| 缺失值 | NaN / None 混用 | null 与 NaN 明确区分 |
| 字符串类型 | object,内存开销大 | Utf8,紧凑高效 |
| 分组聚合 | groupby + agg | group_by + 表达式 |
在金融场景中,最直观的差异体现在缺失值处理上。行情数据里“停牌无成交”与“成交量为 0”是两种不同状态,Pandas 中容易混淆,而 Polars 用 null 表示缺失、NaN 表示浮点无效值,语义更清晰。另一个差异是字符串列:Polars 的 Utf8 类型在百万级 symbol 或合约代码上,内存占用通常显著低于 Pandas 的 object。
df.loc[...] = ... 习惯迁移到 Polars。Polars 的 DataFrame 不可变,所有修改都应通过 with_columns、filter、select 等表达式返回新对象。四、金融数据读取与快速预览
Polars 内置了 CSV、Parquet、JSON、IPC 等格式的读写能力,其中 Parquet 是金融数据落盘的首选:列式压缩、读取快、类型保留完整。下面以一份日频行情 CSV 为例,完成读取、类型修正与快速预览。
import polars as pl
# 读取 CSV,指定日期列解析
df = pl.read_csv(
"daily_quotes.csv",
try_parse_dates=True,
)
# 若日期列未被识别,可显式转换
df = df.with_columns(
pl.col("trade_date").str.to_date("%Y-%m-%d")
)
# 快速预览:形状、前几行、schema
print(df.shape)
print(df.head(5))
print(df.schema)
# 描述性统计
print(df.describe())
对于更大的数据集,建议使用惰性读取 pl.scan_csv 或 pl.scan_parquet,先构建查询计划,再通过 collect() 触发执行。这样可以在读取阶段就完成列裁剪和过滤,减少内存占用:
lf = (
pl.scan_parquet("daily_quotes.parquet")
.filter(pl.col("symbol") == "IF2401")
.select(["trade_date", "close", "volume"])
)
result = lf.collect()
print(result.head())
快速预览阶段,除了 head 和 describe,还应关注空值分布与唯一值数量,这对后续清洗策略至关重要:
print(df.null_count())
print(df.select(pl.col("symbol").n_unique()))
五、常见误区与复盘
- 误区一:把 Polars 当 Pandas 用。 直接套用
inplace=True或索引赋值会报错或产生意外结果。应转向表达式链式写法。 - 误区二:忽略惰性执行。 对大数据集直接
read_csv再过滤,内存峰值高。应优先scan_*+collect。 - 误区三:日期列未解析。 字符串日期无法做时间窗口和重采样,读取后应立即检查 schema。
- 误区四:混淆 null 与 NaN。 在收益率、波动率计算中,二者处理方式不同,需显式使用
fill_null或fill_nan。
复盘来看,本节的关键不是记住所有 API,而是建立三个认知:Polars 以列和表达式为中心;DataFrame 不可变,操作返回新对象;惰性执行是处理金融大数据的第一选择。掌握这些后,后续的清洗、因子计算和性能优化都会顺理成章。