📖 本篇简介:本篇是 pandas 系列第一个"报错排查"专题,聚焦两类最高频的报错——文件读取类(FileNotFoundError、UnicodeDecodeError、ParserError、EmptyDataError、缺 openpyxl 等)和索引类(KeyError、IndexError、IndexingError、loc/iloc 用反、SettingWithCopyWarning 等)。前置知识:第 4 章(数据读取与写入)、第 6 章(数据选择与索引)、第 18 章(视图副本与 SettingWithCopyWarning)。学完后你看到任何一长串红色 traceback,都能在 30 秒内定位到"哪一行、为什么、怎么改",不再靠"把代码删了重写一遍"碰运气。
写代码不怕报错,怕的是看不懂报错。pandas 的 traceback 动辄几十行,90% 的信息是无关的调用栈,真正有用的往往只有最后一行。本篇把工程里最常见的报错按"报错原文 → 触发场景 → 原因分析 → 解决方案 → 预防建议"五步拆解,全部基于本机 pandas 3.0.6 实跑,错误信息原样截取。
目录
1. 为什么要专门花一章讲报错
2. 报错速查总表
3. 文件读取类报错:先看全局
4. 报错 1:FileNotFoundError —— 文件不存在 / 路径写错
5. 报错 2:UnicodeDecodeError —— CSV 编码不匹配
6. 报错 3:ParserError —— 分隔符 / 列数 / 引号对不上
7. 报错 4:EmptyDataError —— 空文件或全是注释
8. 报错 5:ImportError —— 读 .xlsx 缺 openpyxl
9. 顺带说:表头乱码与 header / skiprows / names 参数
10. 顺带说:路径含中文或空格怎么写
11. 索引类报错:先看全局
12. 报错 6:KeyError —— 列名不存在 / 大小写 / MultiIndex 取列
13. 报错 7:IndexError —— iloc 位置越界
14. 报错 8:IndexingError —— 布尔 mask 长度 / 索引不对齐
15. 报错 9:loc / iloc 用反 —— 列名行当行取
16. 报错 10:SettingWithCopyWarning 与链式赋值
17. 顺带说:axis 参数用错导致结果"看起来对其实错"
18. 自测清单
19. 练习题
1. 为什么要专门花一章讲报错
写 pandas 代码,90% 的时间不是在"写新逻辑",而是在"把报错跑通":traceback 一长串不知道哪行是自己的;错误信息每个单词都认识连起来不懂;同一行昨天能跑今天换台电脑就崩;搜出来的答案还是 pandas 1.x 时代的。
💡 读 traceback 的口诀:从下往上读。最后一行是真正的错误类型;往上找最近一条File ".../你的脚本.py", line N,那一行才是你要改的地方;中间.../site-packages/pandas/...是内部实现,不用看。
本篇运行环境:Windows / Python 3.14.7 / pandas 3.0.6 / numpy 2.5.3(默认开启 Copy-on-Write、字符串列默认 str、多数方法已移除 inplace)。
2. 报错速查总表
下表覆盖本篇全部报错,按"一句话原因"对号入座。
| 报错名 | 一句话原因 | 本篇小节 |
|--------|------------|----------|
| FileNotFoundError | 文件路径写错、被移动、相对路径基于工作目录 | §4 |
| UnicodeDecodeError | CSV 编码与 encoding= 参数不一致(gbk 文件按 utf-8 读) | §5 |
| ParserError | 分隔符错、某行列数对不上、引号没闭合 | §6 |
| EmptyDataError | 文件是空的,或全是注释/空行 | §7 |
| ImportError: openpyxl | 读 .xlsx 但没装 openpyxl | §8 |
| 列名乱码 / Unnamed | 编码错,或没加 header / skiprows / comment | §9 |
| KeyError: 'xxx' | 列名拼错、大小写错、把索引行当列取、MultiIndex 取内层 | §12 |
| IndexError: out-of-bounds | iloc 下标超过行数 | §13 |
| IndexingError: Unalignable boolean | 布尔 mask 长度不对 / 索引没对齐 | §14 |
| .loc 报 KeyError: '列名' | 把列名传给了行标签位置 | §15 |
| .iloc 报 TypeError: non-integer | iloc 只接受整数位置,传了字符串行标签 | §15 |
| SettingWithCopyWarning | 链式赋值 df[mask]["col"]=x——3.0 CoW 后行为变了 | §16 |
| ValueError: axis 与 index/columns 冲突 | drop 同时传了两种指定方式 | §17 |
3. 文件读取类报错:先看全局
数据分析师的第一行代码通常是 pd.read_xxx(...),崩也崩在这一行。读取类报错的共同特点是:traceback 巨长,但真正的原因只有一行。按现象对号入座:
| 现象 | 第一步检查 | 第二步 |
|------|------------|--------|
| 说找不到文件 | 文件是否真存在?相对路径基于哪个工作目录? | os.path.exists(path) 验证 |
| 说编码错误 | 文件是 utf-8 还是 gbk? | 换 gb18030,或用记事本"另存为"看编码 |
| 说 parser / tokenize | 分隔符是不是逗号?某行多了/少了一列? | on_bad_lines="skip" 跳过坏行 |
| 说 empty | 文件是不是 0 字节?全是 # 注释? | comment="#" 或 skiprows=N |
| 说缺 openpyxl / xlrd | pip install openpyxl | .xlsx 用 openpyxl,.xls 用 xlrd |
4. 报错 1:FileNotFoundError —— 文件不存在 / 路径写错
① 报错原文
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\Administrator\\AppData\\Local\\Temp\\p43\\不存在的文件.csv'
② 触发场景
import pandas as pd
# 文件其实不在这个位置
df = pd.read_csv(r"C:\Users\Administrator\AppData\Local\Temp\p43\不存在的文件.csv")
三种常见触发:文件名拼错(数据.csv vs data.csv);文件被移动但代码路径没改;相对路径踩坑——写 pd.read_csv("data.csv"),但工作目录不是你以为的那个。
③ 原因分析
pandas 就是老老实实 open(path),操作系统说找不到就抛错。问题 100% 出在路径字符串上。相对路径是最隐蔽的坑:"data.csv" 是相对于Python 进程的当前工作目录(启动 python 命令时所在目录),不是脚本文件所在目录。Jupyter、命令行、IDE 点"运行"三者的工作目录都可能不一样。
④ 解决方案
import os, pandas as pd
path = r"C:\Users\Administrator\AppData\Local\Temp\p43\demo.xlsx"
print(os.path.exists(path)) # 先验证路径是否真存在
print("当前工作目录:", os.getcwd()) # 看一眼工作目录
if not os.path.exists(path):
raise FileNotFoundError(f"找不到数据文件:{path}")
df = pd.read_excel(path)
⑤ 预防建议
- 永远用绝对路径,不要写
pd.read_csv("data.csv"); - Windows 路径前加
r,避免\U、\n被转义; - 项目里把数据路径集中到顶部
DATA_DIR = r"D:\资料\豆包\...",不要散落各处。
⚠️ 常见坑:写成pd.read_csv("C:\new\data.csv")——\n在 Python 字符串里是换行!要么r"C:\new\data.csv",要么正斜杠"C:/new/data.csv"。
5. 报错 2:UnicodeDecodeError —— CSV 编码不匹配
① 报错原文
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd0 in position 0: invalid continuation byte
② 触发场景
import pandas as pd
# 这个 CSV 其实是 GBK 编码(Windows 中文版 Excel 默认导出格式)
df = pd.read_csv(r"...\gbk_file.csv", encoding="utf-8") # 强行按 utf-8 读,崩
文件内容(GBK 字节流):姓名,年龄\n张三,20\n李四,25\n。
③ 原因分析
pandas 读 CSV 是字节 → 字符串的解码过程,靠 encoding= 指定规则。中文 Windows 上:Excel 另存 CSV 默认 GBK / GB2312;程序员脚本、Linux 导出默认 UTF-8;政府/老系统常是 GB18030(GBK 超集,含生僻字和少数民族文字)。按 utf-8 解 GBK 字节流,遇到 0xd0("张"的首字节)就崩。
| 编码 | 覆盖范围 | 典型来源 |
|------|----------|----------|
| utf-8 | 全球字符,Python 3 默认 | 代码、Linux、Mac |
| gbk | 简体常用字 | Windows 中文版 Excel |
| gb18030 | GBK 超集,含生僻字 | 政府/金融、老系统 |
💡 小技巧:不确定编码时优先试 gb18030,它兼容 gbk/gb2312;utf-8 文件用 gb18030 读会乱码但不崩,反过来 gbk 文件用 utf-8 读会直接崩。④ 解决方案
import pandas as pd
# 方案 A:换正确编码
df = pd.read_csv(r"...\gbk_file.csv", encoding="gbk")
print(df)
输出:
姓名 年龄
0 张三 20
1 李四 25
# 方案 B:用 gb18030,兼容性更广(推荐用于"不知道什么编码的中文文件")
df = pd.read_csv(r"...\gbk_file.csv", encoding="gb18030")
# 方案 C:兜底——文件里有几个坏字节不想让它崩
# 注意:read_csv 不接受 errors= 参数(pandas 3.0 实跑报 TypeError),
# 正确做法是先用 open() 打开文件对象,把 errors='replace' 传给 open()
with open(r"...\gbk_file.csv", "r", encoding="utf-8", errors="replace") as f:
df = pd.read_csv(f) # 无法解码的字节替换成 U+FFFD(�),数据能读进来
📌 版本提示:早期教程常写pd.read_csv(..., errors="replace"),但errors是 Python 内置open()的参数,从来不是 `read_csv` 的参数。pandas 3.0.6 实跑会TypeError: read_csv() got an unexpected keyword argument 'errors'。
⑤ 预防建议
- 拿到文件先用记事本"另存为"看编码下拉框(ANSI = GBK);
- 项目内统一把 CSV 转成 UTF-8,后续不再扯皮;
- 中文数据读入函数里写死
encoding="gb18030"比utf-8容错率高。
6. 报错 3:ParserError —— 分隔符 / 列数 / 引号对不上
① 报错原文
pandas.errors.ParserError: Error tokenizing data. C error: Expected 3 fields in line 3, saw 4
② 触发场景
import pandas as pd
# 第 2 行只有 2 列,第 3 行却有 4 列
df = pd.read_csv(r"...\bad_cols.csv")
文件内容:
a,b,c
1,2
3,4,5,6
7,8,9
③ 原因分析
ParserError(旧名 CParserError,pandas 2.0 起统一为此名)是"CSV 解析器看不懂你的文件"的总称。常见触发:列数不一致;分隔符错(TSV 按逗号读、欧洲 Excel 用分号);引号没配对(字段里本身带逗号);文件其实是 Excel 但改了 .csv 后缀。
④ 解决方案
import pandas as pd
# 方案 A:坏行跳过(数据清洗阶段最常用)
df = pd.read_csv(r"...\bad_cols.csv", on_bad_lines="skip")
print(df)
输出(坏行被跳过,缺的位置填 NaN):
a b c
0 1 2 NaN
1 7 8 9.0
# 方案 B:分隔符不是逗号时显式指定
# pd.read_csv(..., sep="\t") # TSV
# pd.read_csv(..., sep=";") # 欧洲分号
# pd.read_csv(..., sep=r"\s+") # 任意空白
# 方案 C:on_bad_lines="warn" 让坏行打印出来便于排查
📌 版本提示:pandas 1.3 之前用error_bad_lines=False;该参数在 2.x 起已废弃,3.0 中已移除,现在统一用on_bad_lines="skip"。
⑤ 预防建议
- 读进来先
df.head()+df.shape看列数行数是否符合预期; - 文件来自第三方时,先用文本编辑器打开前 10 行肉眼确认分隔符;
- 数据管道里加
on_bad_lines="warn",坏行既不崩也不静默丢。
7. 报错 4:EmptyDataError —— 空文件或全是注释
① 报错原文
pandas.errors.EmptyDataError: No columns to parse from file
② 触发场景
import pandas as pd
df = pd.read_csv(r"...\empty.csv") # 0 字节,或全是 # 注释
③ 原因分析
文件本身存在(不是 FileNotFoundError),但 pandas 一通切分后一列都没解析出来。典型情况:上游产出 0 字节空文件;文件全是 # 注释但没传 comment="#";表头行本身是空行。
④ 解决方案
import os, pandas as pd
path = r"...\empty.csv"
if os.path.getsize(path) == 0:
print(f"警告:{path} 是空文件,跳过")
df = pd.DataFrame() # 空表兜底,后续逻辑继续跑
else:
df = pd.read_csv(path, comment="#") # 注释文件加 comment
⑤ 预防建议
- 数据管道读文件前先
os.path.getsize(path); - 批处理脚本用
try/except pd.errors.EmptyDataError包一层,单文件挂掉不崩全链路; - 上游任务失败时不要"静默产出空文件",要让它直接报错。
8. 报错 5:ImportError —— 读 .xlsx 缺 openpyxl
① 报错原文
ImportError: `Import openpyxl` failed. Use pip or conda to install the openpyxl package.
(被屏蔽前因:ImportError: No module named 'openpyxl')
② 触发场景
import pandas as pd
df = pd.read_excel(r"C:\data\报表.xlsx") # 本机没装 openpyxl 时崩
③ 原因分析
pandas 本身不实现 Excel 解析,它只是个壳,真正干活的是第三方库。pip install pandas 不会自动装 openpyxl。
| 扩展名 | 需要的库 | 安装命令 |
|--------|----------|----------|
| .xlsx / .xlsm | openpyxl | pip install openpyxl |
| .xls(老格式) | xlrd==1.2.0 | pip install xlrd==1.2.0 |
④ 解决方案
# 在命令行(不是 Python REPL)里执行
pip install openpyxl
# 代码里显式指定 engine 更清晰
import pandas as pd
df = pd.read_excel(r"C:\data\报表.xlsx", engine="openpyxl")
📌 版本提示:pandas 2.0 之后engine="xlrd"已弃用,3.0 中已移除;读老.xls建议先转.xlsx。
⑤ 预防建议
- 项目
requirements.txt把openpyxl和 pandas 列在一起; - 服务器跑批前先
python -c "import openpyxl"验证; - 大文件读写 Excel 慢,能转 CSV/Parquet 就转(见第 4 章"大文件读取优化")。
9. 顺带说:表头乱码与 header / skiprows / names 参数
这一类问题不抛异常,但列名是 Unnamed: 1、员工信息表(2024年度) 这种奇怪东西,后续 df["姓名"] 直接 KeyError。
场景一:Excel 导出的 CSV,第一行是大标题
文件内容:员工信息表(2024年度),,\n姓名,年龄,城市\n张三,20,北京\n李四,25,上海\n。默认 header=0 把大标题行当表头,列名变成 ['员工信息表(2024年度)', 'Unnamed: 1', 'Unnamed: 2']——"姓名/年龄/城市"被当成了数据行。修复:表头在第 2 行(0-based 索引 1):
import pandas as pd
df = pd.read_csv(r"...\raw.csv", header=1)
print(df.columns.tolist()) # ['姓名', '年龄', '城市']
场景二:文件前几行是 # 注释
直接读会 ParserError: Expected 1 fields in line 3, saw 3(默认不识别 #)。修复:pd.read_csv(r"...\raw2.csv", comment="#")。
场景三:手动指定列名
# header=None 表示"文件里没有表头",再用 names 自己起名字
df = pd.read_csv(r"...\raw.csv", skiprows=2, header=None,
names=["name", "age", "city"])
三个参数的分工:
| 参数 | 作用 | 什么时候用 |
|------|------|------------|
| header=N | 第 N 行(0-based)是表头 | 表头不在第一行 |
| skiprows=N | 跳过前 N 行 | 开头是标题/空行/注释 |
| names=[...] | 手动指定列名 | 文件没表头,或表头不能用 |
| comment="#" | 行首 # 当注释 | 文件带元信息注释 |
列名乱码变成 ['濮撳悕', '骞撮緞']
这是典型的"UTF-8 文件按 GBK 读"。修复见 §5:把 encoding 改成 utf-8(或 utf-8-sig 处理带 BOM 的文件)。
10. 顺带说:路径含中文或空格怎么写
pandas 本身完全支持中文路径和带空格的路径(Python 3 字符串是 Unicode,Windows API 也支持 Unicode 路径)。真正容易翻车的是 shell 转义:Python 代码里 pd.read_csv(r"C:\资料\数据 文件\销售明细.csv") 直接写没问题;但在 PowerShell/cmd 里跑脚本 python script.py C:\资料\数据 文件\销售明细.csv 会因空格断词,整条路径要加双引号。实跑验证(路径含中文 + 空格)输出 x = [1, 2],无报错。
💡 Python 内部拼路径用 os.path.join(),不要手搓斜杠。11. 索引类报错:先看全局
能把文件读进来,下一步就是"取列、取行、按条件筛"——即第 6 章讲的 [] / .loc[] / .iloc[]。索引类报错的特点是:解释器不会替你检查列名是否拼对,跑到那一行才炸。
| 你想做的事 | 正确写法 | 错误写法 |
|------------|----------|----------|
| 取一列 | df["age"] | df["Age"](大小写) |
| 按行标签取行 | df.loc["a"] | df.loc["name"](列名行当行) |
| 按位置取行 | df.iloc[0] | df.iloc["a"](传字符串) |
| 布尔筛选 | df[df["age"] > 20] | mask 长度不够 |
| 改筛选后的子集 | sub = df[...].copy(); sub.loc[:, "age"] = 1 | df[...]["age"] = 1(链式) |
12. 报错 6:KeyError —— 列名不存在 / 大小写 / MultiIndex 取列
① 报错原文
KeyError: 'Name'
② 触发场景
import pandas as pd
df = pd.DataFrame({"name": ["张三", "李四", "王五"], "age": [20, 25, 30]})
df["Name"] # 列名其实是小写 name
③ 原因分析
df["xxx"] 在 DataFrame 上就是查 columns 索引,查不到就 KeyError。常见触发:大小写/拼写(Name vs name、user_id vs userId);列名带空格(实际是 "name ");把 index 里的行当列取;MultiIndex 直接取内层名字。
④ 解决方案
import pandas as pd
df = pd.DataFrame({"name": ["张三", "李四", "王五"], "age": [20, 25, 30]})
# 第一步:先看一眼真正的列名
print(df.columns.tolist()) # ['name', 'age']
# 第二步:发现有空格,立刻清理
df.columns = df.columns.str.strip()
# 第三步:用正确列名
print(df["name"])
输出:
['name', 'age']
0 张三
1 李四
2 王五
Name: name, dtype: str
MultiIndex 取列:列是两层 (A, x)、(A, y),直接 mdf["x"] 报 KeyError: 'x'——因为 x 是内层,pandas 默认按第一层 A 查。正确写法:
mdf = pd.DataFrame([[0, 1], [2, 3]],
columns=pd.MultiIndex.from_tuples([("A", "x"), ("A", "y")]))
mdf["A"] # 取第一层 A 下的所有列
mdf[("A", "x")] # 用元组精确取某一列
mdf.xs("x", axis=1, level=1) # 按 level=1 跨层取
⑤ 预防建议
- 读完 CSV 第一件事
print(df.columns.tolist()),别上来就df["某列"]; - 多列取数 prefer
df.loc[:, ["name", "age"]],列名拼错立刻报错; - MultiIndex 用元组
df[(top, sub)],可读性最好。
13. 报错 7:IndexError —— iloc 位置越界
① 报错原文
IndexError: single positional indexer is out-of-bounds
② 触发场景
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3]}) # 只有 3 行
df.iloc[10] # 取第 11 行
③ 原因分析
.iloc[] 是纯位置索引,从 0 开始、左闭右开,位置 ≥ 行数就越界。和 loc 的区别:df.loc[10] 按行标签 10 取,没标签才 KeyError;df.iloc[10] 按位置取第 11 行,没有就 IndexError。
④ 解决方案
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3]})
print(len(df)) # 3
print(df.iloc[-1]) # 取最后一行,用负位置
print(df.iloc[:10]) # 切片越界不报错,只返回能取到的部分
⑤ 预防建议
- 取"最后一行"用
df.iloc[-1],不要硬写df.iloc[len(df)-1]; - 切片
iloc[:N]安全,单点iloc[N]才会越界; - 循环里用
for i in range(len(df)),不要硬编码行数。
14. 报错 8:IndexingError —— 布尔 mask 长度 / 索引不对齐
① 报错原文
pandas.errors.IndexingError: Unalignable boolean Series provided as indexer (index of the boolean Series and of the indexed object do not match).
(伴随 UserWarning:Boolean Series key will be reindexed to match DataFrame index.)
② 触发场景
import pandas as pd
df = pd.DataFrame({"name": ["张三", "李四", "王五"], "age": [20, 25, 30]},
index=["a", "b", "c"])
mask = pd.Series([True, False]) # 长度 2,df 有 3 行
df[mask]
③ 原因分析
布尔索引要求 mask 与被索引对象长度相同、索引对齐。mask 只有 2 个元素,df 有 3 行,pandas 不知道保留哪几行。更隐蔽的情况:mask 长度对,但索引不对齐——比如 mask = df["age"] > 20 经过 .loc[[...]] 切片后索引变了,再拿去筛另一个 DataFrame。
④ 解决方案
import pandas as pd
df = pd.DataFrame({"name": ["张三", "李四", "王五"], "age": [20, 25, 30],
"city": ["北京", "上海", "广州"]}, index=["a", "b", "c"])
# 方案 A:mask 直接由 df 算出来,索引天然对齐
mask = df["age"] > 20
print(df[mask])
输出:
name age city
b 李四 25 上海
c 王五 30 广州
# 方案 B:mask 是外部列表时,列表直接传按位置
df[[True, False, True]]
⑤ 预防建议
- 写
df[df["age"] > 20],不要把 mask 拆出来手动拼; - 多条件用
&|,每个条件加括号:df[(df.age > 20) & (df.city == "北京")]; - 看到
Unalignable boolean Series,第一反应是"mask.index 和 df.index 不一样"。
15. 报错 9:loc / iloc 用反 —— 列名行当行取
两类紧密相关的错误放在一起讲。
① 报错原文
loc 传列名当行标签:KeyError: 'name'
iloc 传字符串行标签:TypeError: Cannot index by location index with a non-integer key
② 触发场景
import pandas as pd
df = pd.DataFrame({"name": ["张三", "李四", "王五"], "age": [20, 25, 30]},
index=["a", "b", "c"])
df.loc["name"] # 错:loc 第一个位置是"行标签","name" 是列名
df.iloc["a"] # 错:iloc 只接受整数位置
③ 原因分析
.loc / .iloc 参数顺序是 `[行, 列]`。新手常见混淆:想取列 name,写成 df.loc["name"]——pandas 把 "name" 当行标签去 index 查,查不到 → KeyError;想取行标签 a,写成 df.iloc["a"]——pandas 要整数,给字符串 → TypeError。
④ 解决方案
# 取列:用 [] 或 .loc[:, 列]
df["name"] # 最简单
df.loc[:, "name"] # 等价
# 取行:
df.loc["a"] # 按标签
df.iloc[0] # 按位置
# 同时取行和列
df.loc["a", "name"] # 标签 + 标签
df.iloc[0, 0] # 位置 + 位置
⑤ 预防建议
- 口诀:loc 认标签,iloc 认位置,先写行再写列;
- 想取整列就用
df["col"],不要绕.loc; - 想取行就明确写
.loc[label, :]或.iloc[i, :]。
16. 报错 10:SettingWithCopyWarning 与链式赋值
这是 pandas 历史上最著名的警告,第 18 章有底层详解。这里聚焦"看到它怎么修"。
① 报错原文(pandas < 2.0 / 未开 CoW 时)
SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
📌 pandas 3.0 重大变化:本机 pandas 3.0.6 默认开启 Copy-on-Write。我们显式 pd.set_option("chained_assignment", "warn") 后跑下面的链式赋值,Warning 不再出现,行为是"改副本、原表不变"。② 触发场景
import pandas as pd
df = pd.DataFrame({"name": ["张三", "李四", "王五"], "age": [20, 25, 30],
"city": ["北京", "上海", "广州"]}, index=["a", "b", "c"])
sub = df[df["age"] > 20]
sub["age"] = 999 # 经典链式赋值
③ 原因分析
df[df["age"] > 20] 返回的到底是视图还是副本?pandas 官方说"不保证"——视图会连带改 df,副本就不会,这种"行为不确定"就是警告存在的原因。pandas 3.0 CoW 之后:所有切片都返回独立对象,链式赋值一定改副本,原表永远不受影响——从根上消除了"视图陷阱",但也意味着你以为在改原表、其实改了个寂寞。
④ 解决方案
实跑对比(pandas 3.0.6):
sub = df[df["age"] > 20]
sub["age"] = 999
print("--- sub ---"); print(sub)
print("--- 原 df ---"); print(df)
输出:
--- sub ---
name age city
b 李四 999 上海
c 王五 999 广州
--- 原 df ---
name age city
a 张三 20 北京
b 李四 25 上海
c 王五 30 广州
安全写法(无论哪个版本都推荐):
sub = df[df["age"] > 20].copy() # 明确要独立副本
sub.loc[:, "age"] = 999
df.loc[df["age"] > 20, "age"] = 999 # 想改原表就一步到位
⑤ 预防建议
- 筛选后要修改:加
.copy(),意图一目了然; - 要改原表:用
df.loc[mask, col] = value一步到位,不要先切片再赋值; - 旧代码里
df[mask]["col"] = x在 3.0 下不警告了,但行为变了——原来可能改原表,现在一定改副本,升级 pandas 后全局搜一下这种写法; - 看 CoW 是否开启:
pd.options.mode.copy_on_write(3.0 默认True)。详见第 18 章。
17. 顺带说:axis 参数用错导致结果"看起来对其实错"
这类问题不报错,但结果是错的,比报错更可怕。
典型:drop 同时传 axis 和 index/columns
import pandas as pd
d = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
d.drop(columns="a", axis=1) # ValueError: Cannot specify both 'axis' and 'index'/'columns'
pandas 3.0 已把这种"两种指定方式混用"升级成显式报错(旧版是静默忽略其中一个)。修复:d.drop(columns="a")(推荐关键字参数)或 d.drop("a", axis=1)。
axis 语义速记
| 写法 | 含义 |
|------|------|
| axis=0(默认) | 沿行方向操作,即"对每一列"做聚合 / 删行 |
| axis=1 | 沿列方向操作,即"对每一行"做聚合 / 删列 |
记忆法:`axis=` 是"我要沿哪个轴往下走"。axis=0 从上往下走(跨行),得到每列一个值;axis=1 从左往右走(跨列),得到每行一个值。例如 d.sum(axis=0) -> a:6, b:15;d.sum(axis=1) -> [5, 7, 9]。
18. 自测清单
读完本章对照下面问题自测:
- [ ] 看到
FileNotFoundError,第一步先用哪个 Python 函数验证路径? - [ ] UTF-8、GBK、GB18030 三者关系?哪个兼容性最好?
- [ ]
read_csv能不能直接传errors="replace"?正确写法是什么? - [ ]
on_bad_lines="skip"是哪个版本之后的参数?旧版error_bad_lines现在还能用吗? - [ ] 读
.xlsx需要装哪个库?读老.xls呢? - [ ] 文件第一行是大标题、第二行才是表头,应该传什么参数?
- [ ]
df["Name"]报 KeyError 但df里有name列,可能原因有哪些? - [ ]
df.iloc[10]报 IndexError,怎么安全取最后一行? - [ ] 布尔 mask 长度和 DataFrame 行数不一致,抛哪个异常?
- [ ]
df.loc["name"]和df.iloc["a"]分别报什么错? - [ ] pandas 3.0 下链式赋值还会触发 SettingWithCopyWarning 吗?和旧版有何不同?
- [ ]
df.drop(columns="a", axis=1)为什么报错?正确写法?
19. 练习题
1. 路径与编码:你拿到 销售.csv,双击能看到中文,但 pd.read_csv("销售.csv") 抛 UnicodeDecodeError: 'utf-8' codec can't decode byte ...。写出:(a) 两步定位编码的方法;(b) 正确的 read_csv 调用;(c) 文件里还有几个坏字节不想崩,怎么写。
2. ParserError:日志文件 app.log 用逗号分隔共 5 列,但每周一归档任务会有 3 行多吐一个逗号。请写代码:(a) 读入时跳过这 3 行不崩;(b) 把 on_bad_lines 设成 "warn",让坏行打印出来。
3. KeyError 排查:df.columns.tolist() 输出是 ['user_id', 'UserName', ' order_amt '](order_amt 前后有空格、UserName 是驼峰)。下面代码会在哪一行报错?怎么改?
df = pd.read_csv("users.csv")
df2 = df[["user_id", "username", "order_amt"]]
4. loc / iloc 用反:df 的 index 是 ["a","b","c"],列是 ["x","y"]。下面五行分别会报什么错、还是正常运行?
df.loc["a"]; df.loc["x"]; df.iloc[0]; df.iloc["a"]; df["x"]
5. SettingWithCopyWarning:在 pandas 3.0 下,下面代码执行后 df 和 sub 的 age 分别是多少?如果是 pandas 1.x 会有什么警告?怎么改成"无论哪个版本行为都一致"的写法?
df = pd.DataFrame({"age": [20, 30, 40]})
sub = df[df["age"] > 20]
sub["age"] = 0
6. 综合:写一个函数 safe_read(path),要求:(a) 文件不存在时返回空 DataFrame 并打印警告;(b) 编码错时自动尝试 utf-8 → gbk → gb18030 三种;(c) 列数对不上时跳过坏行;(d) 读完打印列名和 shape。
下一篇预告:第 44 章 常见报错与排查大全(二):类型与运算类报错 —— 继续排查TypeError、ValueError、dtype不匹配、NaN 参与运算、astype转换失败、日期解析报错等"数据进来之后才崩"的高频坑。
文章回复
0 条公开回复