数据获取与清洗
上一课说"八成时间洗数据",这一课就把这八成摊开给你看。学完它,你能用 pandas 把一份满是缺漏、重复和离谱值的"脏数据",清洗到可以建模的程度——并且每一步都说得出为什么。
先记住一句话:垃圾进,垃圾出。模型救不了脏数据,你洗多干净,模型就多聪明。
一、数据从哪来
| 来源 | 长什么样 | 怎么拿 |
|---|---|---|
| 文件 | CSV、Excel、JSON | pd.read_csv / pd.read_json,最常见 |
| 数据库 | 老站 SQL 课里建过的那些表 | SQL 查询结果直接转成 DataFrame |
| API | 网站开放的数据接口 | 发请求拿 JSON(老站 F12 课的 Network 面板就是找接口的地方) |
| 爬虫 | 别人网页上的公开数据 | 自己写脚本抓,注意遵守 robots 协议、控制频率,红线在老站的法律红线一课 |
来源五花八门,但进了 pandas 之后都是同一个东西,所以本课只管"进来之后"的事。
二、pandas 的两个核心对象
python
import pandas as pd
# Series:一列,带着标签的一串数据
s = pd.Series([86, 92, 78], index=["语文", "数学", "英语"])
# DataFrame:一张表,由多列 Series 拼成
df = pd.DataFrame({
"姓名": ["阿呆", "小问", "阿呆"], # 注意:阿呆出现了两次,为后面的去重埋的伏笔
"语文": [86, 92, 86],
"数学": [70, 85, 70]
})🧠 记忆锚点:Series 是一列,DataFrame 是一张表。 后面所有操作,本质都在回答两个问题:选哪些行、动哪些列。
拿到任何一份新数据,先做"三板斧"体检,别急着分析:
| 方法 | 作用 | 你想从它那听到什么 |
|---|---|---|
df.head() | 看前 5 行 | 数据长这样吗?列名对吗? |
df.info() | 每列的类型和非空数量 | 哪几列缺了?类型对不对? |
df.describe() | 数值列的统计摘要 | 最大最小离不离谱?均值中位数差多远? |
三、拿到一份脏数据
把下面整段复制成 dirty.py 运行。这是奶茶店 10 位会员的登记表,我往里面埋了三类问题:缺失值、重复行、离谱值——先跑三板斧,亲眼把它们找出来:
python
import io
import pandas as pd
csv_text = """会员ID,年龄,月均消费,月均到店次数,备注
M01,19,180,8,
M02,25,240,10,
M03,300,50,2,
M04,,210,9,
M05,31,320,12,
M06,22,,6,
M07,28,150,5,
M05,31,320,12,
M08,35,90,3,
M09,,60,2,
M10,41,410,13,
"""
df = pd.read_csv(io.StringIO(csv_text))
print(df.info()) # 哪几列非空数量少于 10?
print(df.describe()) # 年龄的最大值是多少?我埋的三类问题:M03 年龄 300(录入错误)、M04 / M09 年龄缺失、M06 消费缺失、M05 整行重复。运行之后:
info() 会告诉你:年龄列只有 8 个非空、月均消费列 9 个——缺失暴露了;describe() 会告诉你:年龄最大值 300——离谱值也暴露了。再加上肉眼可见的 M05 整行重复,三类问题全部定位。
💡 动手实验 1:再加一行
print(df[df.duplicated()]),把那条重复行打印出来。注意duplicated()返回的是一列 True/False,用它去"框" DataFrame 就能筛出重复的行——布尔筛选是 pandas 最常用的姿势,后面还会反复用。
四、处理缺失值:删、填、还是留着
先找出来,再选策略:
python
print(df.isna().sum()) # 每列缺了几个| 策略 | 代码 | 什么时候用 |
|---|---|---|
| 删行 | df.dropna(subset=["年龄"]) | 缺得很少、且这行没法补救(比如连会员 ID 都缺) |
| 填中位数/均值 | df["年龄"] = df["年龄"].fillna(df["年龄"].median()) | 数值列,缺失比例不高;中位数比均值扛离谱值 |
| 填众数 | df["备注"] = df["备注"].fillna(df["备注"].mode()[0]) | 文本/类别列 |
| 留成标记 | 缺失本身就藏着信息("没填备注"也许就是一种行为) | 先加一列 是否缺失 再填,把信息保住 |
本份数据里年龄缺 2 个、占两成,用中位数填最稳:
python
df["年龄"] = df["年龄"].fillna(df["年龄"].median())🧠 记忆锚点:删是干脆,填是推断,留是心机。 三种策略没有对错,只有和不合场景——但无论选哪个,都要在结果里写明你做了什么,别让下游的人以为数据天生就这么干净。
五、处理重复值
python
df = df.drop_duplicates() # 整行完全相同才算重复,删掉后多出来的那份
df = df.reset_index(drop=True) # 删完行号会跳,重新排一下一个值得停留十秒的问题:为什么 M05 的两条记录只有一条算"重复"? 因为 drop_duplicates 默认把第一条当原件、后面的当复印件。但如果会员真的一月来登记了两次(数据更新了呢)?那就得靠业务规则判断——"重复"从来不只是技术判断,还是业务判断。
六、处理异常值
describe() 里年龄最大 300,最小 19。怎么判"异常"?两个常用标准:
- 3σ 原则:正态分布下,99.7% 的数据落在「均值 ± 3 倍标准差」里,出去的就是稀有值
- 箱线图 IQR 法:低于
Q1 - 1.5×IQR或高于Q3 + 1.5×IQR的算异常(Q1/Q3 是四分位数)
300 岁不需要算也知道是录入错误(手滑多打了 0),直接处理:
python
# 只保留合理的年龄范围
df = df[(df["年龄"] >= 10) & (df["年龄"] <= 90)]⚠️ 异常值别见一个删一个。先问它是"错误"还是"事实":300 岁是录入错误,删;而"一位月消费 2 万的土豪"虽然离谱,却是真金白银的事实,删了你的报表就对不上一笔真实收入。错误要修,事实要留——哪怕它扎眼。
七、串起来:清洗前后对比
把上面的动作连成完整脚本,然后对比清洗前后的 describe():
python
import io
import pandas as pd
csv_text = """会员ID,年龄,月均消费,月均到店次数,备注
M01,19,180,8,
M02,25,240,10,
M03,300,50,2,
M04,,210,9,
M05,31,320,12,
M06,22,,6,
M07,28,150,5,
M05,31,320,12,
M08,35,90,3,
M09,,60,2,
M10,41,410,13,
"""
df = pd.read_csv(io.StringIO(csv_text))
df["年龄"] = df["年龄"].fillna(df["年龄"].median()) # ① 补缺失
df["月均消费"] = df["月均消费"].fillna(df["月均消费"].median())
df = df.drop_duplicates().reset_index(drop=True) # ② 去重复
df = df[(df["年龄"] >= 10) & (df["年龄"] <= 90)] # ③ 删错误异常
print(df.describe())
print("最终行数:", len(df))对比一眼就能发现变化:年龄的最大值从 300 变回正常范围,均值也从"被 300 拖飞"的状态回落到真实水平。一个离谱值就足以把整列的均值带偏——这就是为什么它必须处理。
💡 动手实验 2:把第 ① 步改成
df.dropna()(直接删掉带缺失的行)再跑一遍,看最终行数变成了多少。然后想清楚一个问题:如果这份表有 10 万行、缺失只占 0.1%,你选删还是选填?如果缺失占 40% 呢?——策略跟着缺失比例和数据量走。
💡 动手实验 3:清洗只能靠人眼盯
describe吗?试试df.plot(kind="box")(需要pip install matplotlib),箱线图会把异常值画成一串孤零零的点,一眼定位。
小结
- 数据来源:文件、数据库、API、爬虫;进了 pandas 之后一律是 DataFrame
- 新数据到手先跑三板斧:
head看长相、info查缺失、describe抓离谱 - 缺失值三策略:删是干脆,填是推断,留是心机;数值填中位数比均值扛离谱值
drop_duplicates把第一条当原件;"重复"是技术判断更是业务判断- 异常值先问"错误还是事实":错误要修,事实要留;3σ 和 IQR 是两个常用标准
- 3σ 原则与 IQR 法之外,眼睛和业务常识永远是第一道筛查
数据洗干净了,下一课开始真正"挖":用 分类与聚类 把会员分成三六九等、预测谁会流失。