pandas 3.1 表格处理:向量化快 35 倍,四个默认行为要当心
几百行到几千万行的表格,要筛选、分组、求和、和维表拼接、按时间聚合,用 csv 模块加 for 循环写一遍要几十行,改一次逻辑再改一遍。pandas 把这类操作收成 groupby、merge、resample 几个方法,配上带标签的索引,筛选和派生列都是一行。它的代价是内存:默认整列读进内存,几 GB 的 CSV 会直接把机器压住;数据量超过内存时该用 DuckDB 或 Polars,或者先落成 Parquet 再按列读。
安装与实际版本
python -m venv D:\ai\cache\temp\py-pandas
D:\ai\cache\temp\py-pandas\Scripts\python.exe -m pip install pandas
Name: pandas
Version: 3.1.1
Location: D:\ai\cache\temp\py-pandas\Lib\site-packages
import numpy as np
import pandas as pd
print("pandas", pd.__version__)
print("numpy", np.__version__)
pandas 3.1.1
numpy 2.4.6
3.x 有两个和 2.x 不一样的默认行为会体现在输出里:字符串列的 dtype 直接是 str(不再是 object),Copy-on-Write 常开。下面的示例都会看到。
最小可运行示例
七行订单数据,涵盖读入、派生列、分组、缺失值、时间聚合:
import io
import pandas as pd
csv = """order_id,region,sku,qty,unit_price,created_at
1001,华东,SKU-1,2,19.90,2026-11-01
1002,华北,SKU-1,1,19.90,2026-11-01
1003,华东,SKU-2,5,7.50,2026-11-02
1004,华南,SKU-3,3,120.00,2026-11-02
1005,华东,SKU-2,1,7.50,2026-11-03
1006,华北,SKU-3,2,120.00,2026-11-03
1007,,SKU-8,1,7.50,2026-11-04
"""
df = pd.read_csv(io.StringIO(csv), parse_dates=["created_at"])
print(df.dtypes)
print("shape:", df.shape)
df["amount"] = df["qty"] * df["unit_price"]
print(df.loc[df["amount"] > 50, ["order_id", "region", "amount"]])
g = (
df.groupby("region", as_index=False)
.agg(orders=("order_id", "count"), amount=("amount", "sum"))
.sort_values("amount", ascending=False)
)
print(g.to_string(index=False))
order_id int64
region str
sku str
qty int64
unit_price float64
created_at datetime64[us]
dtype: object
shape: (7, 6)
order_id region amount
3 1004 华南 360.0
5 1006 华北 240.0
region orders amount
华南 1 360.0
华北 2 259.9
华东 3 84.8
region 那行是空的(第 7 条),它没有出现在分组结果里,这是第一个要当心的地方,后面单独说。时间列的分辨率是 datetime64[us](微秒),不是 2.x 里常见的 datetime64[ns]。
常用 API 与参数
| 名称 | 作用 | 要点 |
|---|---|---|
pd.read_csv() | 读 CSV | parse_dates、dtype、usecols、na_values 都能避免后续返工 |
df.loc[] / df.iloc[] | 按标签/按位置取值 | 布尔筛选用 loc;直接 df[...] 有时返回视图有时返回副本 |
df.assign() / 直接赋列 | 派生列 | 赋值整列是安全的;对切片赋值会触发 Copy-on-Write 告警 |
df.groupby(by, as_index=False) | 分组 | dropna 默认 True;observed 影响分类列 |
.agg(新列=("源列", "函数")) | 命名聚合 | 一次算多个指标,结果列名可控 |
df.merge(other, on=, how=) | 关联合并 | 键重复会放大行数;validate="many_to_one" 可提前报错 |
df.fillna() / dropna() | 缺失值 | dropna(subset=[...]) 只按指定列判断 |
Series.resample() | 时间重采样 | 需要 DatetimeIndex |
df.to_csv() / to_parquet() | 导出 | Parquet 保留 dtype,读回来不用再转 |
四个坑
第一个坑是链式赋值在 Copy-on-Write 下直接不生效。d2["amount"][mask] = 0 这种写法在 2.x 里给 SettingWithCopyWarning,3.x 里升级成错误:
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Amount 被改为 0 的行数: 0 (目标是 3 行)
一行都没改到,但程序照常往下跑。正确写法是 d2.loc[mask, "amount"] = 0,一次定位到行列。
第二个坑是 groupby 默认把 NaN 组丢掉。同一个 DataFrame,只改 dropna:
dropna=True : {'华东': 84.8, '华北': 259.9, '华南': 360.0}
dropna=False: {'华东': 84.8, '华北': 259.9, '华南': 360.0, nan: 7.5}
region 为空的那条 7.5 元订单,在默认设置下从报表里消失了。做收入对账时这类"少了一点点"最难查,groupby(..., dropna=False) 或者先把缺失值填成"未知"再分组,两种都行。
第三个坑是前导零。带前导零的单号、邮编、行政区划码,read_csv 会当成整数:
默认: int64 [123, 456]
dtype=str: ['00123', '00456']
00123 变成 123,再拼回去就对不上了。读的时候就写 dtype={"code": str},比事后 zfill 可靠。
第四个坑是 merge 的键不唯一。左表 2 行、右表 3 行,合并出 3 行:
left 2 right 3 -> merge 3
看起来没放大,其实是 A 在右表出现两次,A 这一条被拆成 2 行,B 保持 1 行,总数 3。如果右表是"一个 SKU 一条"的维表,这种重复说明维表有脏数据,金额会被重复计入。merge(..., validate="many_to_one") 能让它在合并时就抛错,而不是等到汇总数字对不上。
向量化与逐行的差距
同一份 2 万行的两列相乘,三种写法:
20000 行相乘:向量化 0.05 ms / 列表推导 1.83 ms / 逐行 1.92 ms
加速比 逐行/向量化: 35x
向量化走的是 NumPy 的循环,没有 Python 层的对象创建;for a, b in s1.items() 每次都要拆箱成 Python float。真实业务里这个差距会被 I/O 和分组摊薄,但循环体里再套一个函数调用,几十倍很常见。
顺带一个 3.x 的提示:pd.options.mode.copy_on_write 这个选项还在,但已经不起作用了。
Pandas4Warning: The 'mode.copy_on_write' option is deprecated. Copy-on-Write can no longer be disabled (it is always enabled with pandas >= 3.0), and setting the option has no impact. This option will be removed in pandas 4.0.
按 2.x 的经验去关掉 CoW 来"修复"链式赋值,在 3.x 上不会有效果。
小结
pandas 的写法可以归纳成一句:整列操作,别逐行。2 万行相乘,向量化 0.05ms、逐行 1.92ms,35 倍差距全在解释器开销上。
四个坑都关于默认值:链式赋值不生效但不报错、groupby 丢 NaN 组、read_csv 猜类型猜错、merge 键重复放大行数。它们的共同点是不会抛异常,只会让结果少一点或者多一点,所以核对行数和总额这一步不能省。
数据来源与检索时间
检索时间 2026-11-10。版本、dtype、耗时与告警文本均为本机实测。
| 来源 | URL | 对应数据 |
|---|---|---|
| PyPI《pandas》项目页 | https://pypi.org/project/pandas/ | 当前版本 3.1.1(2026-11-04 发布);依赖 NumPy;CSV/Excel/Parquet 等 I/O 支持 |
| pandas 官方文档《10 minutes to pandas》 | https://pandas.pydata.org/docs/user_guide/10min.html | Series/DataFrame 的数据结构;loc/iloc 与布尔索引;dropna/fillna 语义;merge 在键重复时产生笛卡尔展开的示例;groupby 的分组-聚合流程;resample 用法;3.x 中字符串列为 str dtype、时间列为 datetime64[us] |
| 本机运行记录 | D:\ai\cache\temp\py-pandas | 3.1.1 / numpy 2.4.6;7 行订单的 dtype、分组、缺失值、重采样;2 万行三种写法耗时;ChainedAssignmentError、Pandas4Warning 原文 |
核验说明
- 虚拟环境
D:\ai\cache\temp\py-pandas,Python 3.11.9,pip install pandas装到 3.1.1,同时带进 numpy 2.4.6。 - 真跑的示例:7 行订单 CSV 的读入与 dtype、派生列与布尔筛选、
groupby().agg()、merge维表、缺失值统计、resample("D")、2 万行三种写法的耗时对照、链式赋值的告警、dropna开关对照、前导零读入、键重复的merge行数、copy_on_write选项的弃用告警。 - 没测的部分:Parquet/Excel 读写、
MultiIndex、pivot_table、分类列(Categorical)、apply与map的耗时对比、多线程/多进程加速。几 GB 级数据的表现也没有测,本机只用了 2 万行。 - 耗时数字是单次测量,没有做多轮取中位数;绝对值会随机器波动,能复现的是数量级关系。