把数据洗干净
3 道练习题·预计 50 分钟·做对一题解锁下一段
前言
上一章结尾说过,这一章要换一份数据。为什么要换呢?因为前面几章用的记账本,是一份整理好的数据:每个日期都是同一种写法,每个金额都是数字,也没有空着的地方。
可真实的数据,很少有这么规矩的。手机上随手记的账,有时金额后面多打了一个「元」,有时手一抖记了两遍,有时忘了选类别,有时又多按了几个 9。这些问题要是不解决,后面算出来的数,就全都不可信了。
把数据收拾干净的过程,叫数据清洗。它不像画图那样好看,却是分析里最花时间、也最见功夫的一步。在第 1 章说的六个步骤里,它排在第三。
这一章要用的,是 ledger_2026_raw.csv :11 月上半月的一份原始账单,一共 64 行。它在网页里已经准备好了,直接读就行。
学这一章之前,需要先学完 数据分析课第 4 章 。
目录
这一章会学到:
- 一、先做个体检——问题都藏在哪儿
- 二、空着的值——删掉还是补上
- 三、重复的行——
duplicated()和drop_duplicates() - 四、统一格式——去掉「元」,把文字变成数字和日期
- 五、明显不对的值——异常值,以及一份完整的清洗代码
- 六、常见坑
- 七、自己查一次文档——
to_datetime()的format='mixed' - 八、小结与回看
学完这一章,拿到一份乱糟糟的数据,你知道该从哪儿下手,一步一步把它收拾干净。
一、先做个体检
拿到一份乱糟糟的数据,先别急着改。就像看病要先体检一样,得先弄清楚问题都藏在哪儿。拿什么来体检呢?第 2 章学的那几招,正好派上用场。先用 info() 看看整体的情况:
import pandas as pd
raw = pd.read_csv('ledger_2026_raw.csv')
raw.info()输出的结果:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 64 entries, 0 to 63
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 date 64 non-null object
1 item 64 non-null object
2 category 61 non-null object
3 cost 62 non-null object
4 payment 64 non-null object
dtypes: object(5)
memory usage: 1.3+ KB一眼就能看出两个问题:
- 一共 64 行,可
category只有 61 个非空值,cost只有 62 个,说明这两列都有空着的地方。 cost的类型是object,不是数字。第 2 章说过,这说明金额这一列里,混进了不是数字的东西。
不过,info() 只告诉我们哪里有问题,却看不出问题具体长什么样。所以再用 head(8) 看看开头 8 行:
import pandas as pd
raw = pd.read_csv('ledger_2026_raw.csv')
print(raw.head(8))输出的结果:
date item category cost payment
0 2026-11-01 房租 居住 2500 银行卡
1 2026-11-01 水电燃气 居住 229.5 支付宝
2 2026-11-01 早饭 餐饮 8 支付宝
3 2026-11-01 午饭 餐饮 22.5元 微信
4 2026-11-01 晚饭 餐饮 27 微信
5 2026-11-02 早饭 餐饮 10 支付宝
6 2026-11-02 午饭 餐饮 29 支付宝
7 2026-11-02 晚饭 NaN 58 微信仔细看,问题就出来了:
- 行号为 3 的那一行,金额写成了
22.5元。 - 行号为 5 的那一行,
10有点歪,因为它前后多了空格。 - 行号为 7 的那一行,类别是
NaN,也就是空着。
光看开头 8 行,就找出了三个问题,后面还有多少呢?一个一个地找就太慢了。所以接下来,咱们一类一类地收拾,先从空着的值开始。
二、空着的值
先来收拾空着的值。第一步,是弄清楚每一列各空了几个。isna() 能判断每个值是不是空的,得到一串 True 和 False 。和第 3 章一样,再 sum() 一下,就知道有几个空的:
import pandas as pd
raw = pd.read_csv('ledger_2026_raw.csv')
print(raw.isna().sum())输出的结果:
date 0
item 0
category 3
cost 2
payment 0
dtype: int64这里是对整张表用 isna() ,再 sum() ,所以每一列有几个空值,一下子全出来了:类别空了 3 个,金额空了 2 个。
光知道个数还不够,还得看看具体是哪几行。把它们挑出来:
import pandas as pd
raw = pd.read_csv('ledger_2026_raw.csv')
print(raw[raw['cost'].isna()])
print(raw[raw['category'].isna()])输出的结果:
date item category cost payment
21 2026-11-06 早饭 餐饮 NaN 微信
52 2026-11-13 地铁 交通 NaN 支付宝
date item category cost payment
7 2026-11-02 晚饭 NaN 58 微信
31 2026-11-08 午饭 NaN 26 支付宝
55 2026-11-14 打车 NaN 29 微信那这些空值该怎么处理呢?其实没有标准答案,要看缺的是什么:
- 金额空了:比如那顿早饭,到底吃了多少钱?没法知道。瞎猜一个数,只会让结果更不准。所以这种就删掉。
- 类别空了:晚饭、午饭肯定是餐饮,打车肯定是交通,这是能推断出来的。所以这种就补上。
先说删。删掉用 dropna() ,再用 subset 说明看哪几列:这几列有空的,才删。
import pandas as pd
raw = pd.read_csv('ledger_2026_raw.csv')
has_cost = raw.dropna(subset=['cost'])
print(len(raw), len(has_cost))输出的结果:
64 6264 行删掉了金额空着的 2 行,剩下 62 行。
再说补。补上用 fillna() 。它可以填同一个值,比如 fillna('未分类') ;也可以拿另一列来填,同一行的值对着填进去。这里先用 map() 按名称查出类别,再拿查出来的结果去填:
import pandas as pd
raw = pd.read_csv('ledger_2026_raw.csv')
guess = raw['item'].map({'午饭': '餐饮', '晚饭': '餐饮', '打车': '交通'})
raw['category'] = raw['category'].fillna(guess)
print(raw.loc[[7, 31, 55]])
print(raw['category'].isna().sum())输出的结果:
date item category cost payment
7 2026-11-02 晚饭 餐饮 58 微信
31 2026-11-08 午饭 餐饮 26 支付宝
55 2026-11-14 打车 交通 29 微信
0这段代码有四个地方要说一下:
map(字典)会拿这一列的每个值去查字典:午饭查到餐饮,打车查到交通,查不到的就是NaN。fillna(guess)只填空着的地方,原来有类别的,它不会动。raw['category'] = ...是把结果放回去,替换掉原来那一列。loc[[7, 31, 55]]方括号里是一个行号的列表,能一次挑出这三行,方便看看补得对不对。
最后输出的 0 说明,三个空着的类别都补上了。
那要是实在推断不出来呢?先做一道练习:把空着的类别都填成「未分类」。
读进 ledger_2026_raw.csv ,把类别(category)空着的地方都填成「未分类」,放回原来那一列,然后打印每一列还有几个空值。
运行后的输出应该是:
date 0
item 0
category 0
cost 2
payment 0
dtype: int64推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai