2414 字
约 8 分钟
18
S10-X02 · Pandas 与数据处理入门

Pandas 的学习曲线断点几乎不在 API 数量上,而在心智模型:很多人把它当成"能放多种类型的二维数组",于是写出 df[0] 这种按位置取列的代码,然后在列名是字符串时拿到 KeyError。另一种情况是把 SettingWithCopyWarning 当成噪音忽略,最后发现赋值没生效。这篇按"模型 → 读取 → 选择 → 清洗 → 聚合 → 合并 → 时间 → 性能边界"的顺序把主干走一遍。


1. 心智模型:带标签的二维表

Series = 一维带标签数组;DataFrame = 共享同一套行索引的 Series 字典。关键认知:索引是标签,不是位置。

import pandas as pd

df = pd.DataFrame({"uid": [1, 2, 3], "amt": [10.0, 20.0, 30.0]})
df.index = ["a", "b", "c"]      # 行标签换成字符串
print(df.loc["b", "amt"])       # 典型输出:20.0
print(df.iloc[1, 1])            # 典型输出:20.0
概念 是什么 常见误解
index 行标签,可重复、可非数值 以为等于行号
columns 列标签 以为可以像 numpy 那样按位置取列
dtype 每列的物理类型 以为整张表只有一种类型
视图/副本 切片可能返回视图 以为赋值一定写回原表

2. 读取:read_csv 与 read_sql

df = pd.read_csv(
    "orders.csv",
    dtype={"uid": "int64", "phone": "string"},   # 显式类型,禁用推断
    encoding="utf-8",                            # 或 "gb18030"
    parse_dates=["created_at"],
    na_values=["", "NULL", "-"],
    keep_default_na=True,
)

import sqlite3
with sqlite3.connect("app.db") as conn:
    df = pd.read_sql("select * from orders where created_at >= ?", conn, params=("2026-01-01",))
参数 典型坑 处理
dtype 工号/手机号/邮编被推断成 int64,前导零丢失 显式指定 "string"
encoding Windows 导出的 GBK 文件读成乱码或 UnicodeDecodeError 先试 utf-8,再试 gb18030
parse_dates 日期留成字符串,后续比较变成字典序 读取时就解析
na_values 业务用 "NULL"/"-" 表示空,pandas 不认 显式声明
usecols / nrows 百 MB 文件全量读进来才发现只要三列 先探结构再读

read_sql 的坑不在 pandas 而在驱动:SQL 里做聚合比读进来再用 pandas 聚合快得多,尤其当表在远端数据库上。


3. 选择:loc、iloc 与布尔索引

df.loc["a":"b", ["uid", "amt"]]      # 标签切片,含右端点
df.iloc[0:2, 0:2]                    # 位置切片,不含右端点
df[df["amt"] > 15]                   # 布尔索引
df.loc[df["amt"] > 15, "amt"]        # 布尔 + 列选择,最推荐
方式 依据 右端点 返回
loc 标签 包含 行/列按标签
iloc 位置 不包含 行/列按位置
[] 列名(字符串)或布尔 Series —— 视输入而定,语义含糊
at / iat 单个标量 —— 标量,最快

两个必须记住的差异:loc 切片含右端点("a":"b" 取到 b),iloc 沿用 Python 习惯不含。以及 df[布尔 Series] 是行过滤,df["col"] 是列选择,df[列名列表] 才是多列——同一个方括号三种语义,所以生产代码建议一律写 loc/iloc。


4. 缺失值:isna / fillna / dropna

df.isna().sum()                       # 每列缺失数
df["amt"].fillna(df["amt"].median())  # 用中位数填
df.dropna(subset=["uid"])             # 只按关键列删行
df["amt"].fillna(0, inplace=False)    # 显式不原地改
方法 语义 何时用
dropna() 删含缺失的行 缺失比例极低且随机
fillna(常数) 填固定值 业务上缺失等价于某个默认值
fillna(统计量) 填中位数/均值 数值特征,缺失与大小无关
ffill / bfill 前/后向填充 时间序列,前提是"值不变"
isna() 布尔掩码 先统计再决定策略

"缺失"和"零"是两种语义。金额缺失表示"没发生交易",填 0 会把"未发生"和"发生了但金额为 0"混成一类,直接污染 mean()。同理,fillna(0) 之后 isna() 再也救不回来——清洗前先备份原始列或整表。


5. 分组聚合:agg 与 transform

g = df.groupby("city", dropna=False)

g["amt"].agg(["count", "sum", "mean"])            # 每组一个标量,行数变少
g["amt"].transform("mean")                        # 每组一个标量,回填到原行
df.assign(share=df["amt"] / g["amt"].transform("sum"))   # 组内占比
方法 输出形状 典型用途
agg 每组一行 汇总报表
transform 与原表同形 组内标准化、占比、去均值
apply 由函数决定 复杂逻辑,最慢,兜底用
filter 原表子集 过滤掉不满足条件的组
size / count 每组一行 前者含 NaN,后者不含

agg 和 transform 混用是最高频的错误来源:需要"回填到原表"时用 agg 会得到形状不匹配的赋值错误,需要"每组一行"时用 transform 会得到一堆重复值。


6. 合并:四种 how 与重复键放大

out = orders.merge(users, on="uid", how="left", validate="many_to_one")
how 保留 缺失侧填充
inner 两表都有的键 无
left 左表全部 右表缺的补 NaN
right 右表全部 左表缺的补 NaN
outer 并集 两侧都可能补 NaN

重复键导致的笛卡尔放大是 merge 最危险的行为:左表某 uid 出现 3 次、右表同 uid 出现 2 次,合并后该用户变成 6 行,金额被重复计三次。防御手段是 validate=:

validate 取值 含义
"one_to_one" 两侧键都唯一
"one_to_many" 左侧唯一
"many_to_one" 右侧唯一(最常见)
"many_to_many" 不检查,默认行为

合并前后各查一次行数(len(before) <= len(after) 未必成立,但放大倍数应可解释),是比事后对账便宜得多的习惯。


7. 排序、去重与时间序列基础

df.sort_values(["city", "amt"], ascending=[True, False])
df.drop_duplicates(subset=["uid"], keep="last")

df["created_at"] = pd.to_datetime(df["created_at"], errors="coerce", utc=True)
daily = df.set_index("created_at").resample("D")["amt"].sum()
操作 关键参数 坑
sort_values ascending 可为列表 多列排序时长度必须对齐
drop_duplicates subset、keep 不写 subset 按整行比对,几乎删不掉
to_datetime errors="coerce" 脏值变 NaT 而不是抛错,需事后统计
resample "D"/"W"/"M" 必须先把时间列设为索引
dt 访问器 .dt.floor("h") 非 datetime dtype 会报错

resample 的语义是"按时间桶聚合",空桶默认补 0(对 sum)而不补 NaN,做"日均单量"时要意识到这一点。


8. 链式写法与 pipe

def only_valid(df):
    return df[df["amt"] > 0]

out = (
    df.pipe(only_valid)
      .assign(month=lambda d: d["created_at"].dt.to_period("M"))
      .groupby("month", as_index=False)["amt"].sum()
      .sort_values("amt", ascending=False)
)

链式的价值是让每一步都可单独摘出来测试;pipe 的作用是让自定义函数也能插进链里(df.pipe(f) 等价于 f(df))。代价是中途无法打印中间结果——调试时把链拆成几段变量,别在链里塞 print。


9. 排错

现象 根因 处理
SettingWithCopyWarning 在切片结果上赋值,可能是副本 先 .copy(),或改用 .loc[行, 列] = 值
数值列变成 object 混入字符串或 None pd.to_numeric(errors="coerce") 后查 NaN
inplace=True 之后原变量没变 链式调用返回副本,原地改的是临时对象 改成 df = df.dropna() 显式赋值
merge 后行数暴涨 键重复 加 validate=,检查键唯一性
KeyError: 列名 列名带空格或大小写不一致 df.columns.str.strip() 后核对
大文件读取 OOM 全量载入内存 usecols/chunksize/dtype 降精度

inplace 的误用值得单独说:df.dropna(inplace=True) 有效,但 df.dropna(inplace=True).sort_values(...) 里第二个调用作用在 None 上会直接报错;而 df["a"].fillna(0, inplace=True) 在部分版本上根本改不到原表。统一风格——要么全用赋值,要么全用 inplace,不要混。


10. 边界:什么时候别用 pandas

场景 更合适的选择 理由
数据超过内存 DuckDB / polars lazy 流式或列式的内存模型
只是过滤聚合后入库 SQL 让数据库做它擅长的事
单机多核大表计算 polars 原生并行、无索引开销
强类型管线、需要 schema polars / Pydantic + Arrow 显式 schema 更可控
交互式探索小表(万行级) pandas 生态最全,够用
生产服务里的逐行处理 纯 Python / numpy pandas 单行访问开销大

判断阈值不用记精确数字:当你在 pandas 里写 iterrows() 或反复 apply 时,说明该换工具或换写法了。pandas 的强项是整列向量化操作,逐行逻辑是它最慢的路径。


11. 自测

  1. df.loc["a":"b"] 与 df.iloc[0:1] 在"是否含右端点"上有什么区别?
  2. 金额列缺失填 0,会让后续 mean() 产生什么偏差?为什么这属于语义错误而不是技术错误?
  3. agg 与 transform 的输出形状分别是什么?什么场景必须用后者?
  4. 两表按 uid 合并后行数变成原来的 6 倍,最可能的原因是什么?用什么参数提前拦住?
  5. SettingWithCopyWarning 出现时,为什么"加 .copy()"有时也不是正解?
  6. 什么信号出现时,说明这份数据处理该从 pandas 换成 polars 或 SQL?
S10-X02 · Pandas 与数据处理入门
https://www.clxhxhhr.top/posts/5744/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。