RSS
菜单
全部文章快讯开发科技深度热点

第 43 章 常见报错与排查大全(一):文件读取与索引类报错

内容摘要

本篇是 pandas 系列第一个"报错排查"专题,聚焦两类最高频的报错——**文件读取类**(FileNotFoundError、UnicodeDecodeError、ParserError、EmptyDataError、缺 openpyxl 等)和**索引类**(KeyError、IndexError、IndexingError、loc/iloc 用反、SettingWithCopyWarning 等)。前置知识:第 4 章(数据读取与写入)、第 6 章(数据选择与索引)、第 18 章(视图副本与 SettingWithCopyWarning)。学完后你看到任何一长串红色 traceback,都能在 30 秒内定位到"哪一行、为什么、怎么改",不再靠"把代码删了重写一遍"碰运气。

📖 本篇简介:本篇是 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 转换失败、日期解析报错等"数据进来之后才崩"的高频坑。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 43 章 常见报错与排查大全(一):文件读取与索引类报错》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。