第四章 数据清洗:用Pandas把脏数据收拾利索
说实话,干这行这么多年,我最怕的不是算法调不出参数,而是数据根本没法用。你想想看,从各种数据库、Excel、API里拉回来的药物数据,缺胳膊少腿是常态。今天咱们就聊聊怎么用Pandas把这堆烂摊子收拾干净。
4.1 数据清洗,到底是洗什么?
我习惯把数据清洗分成三个层面:缺失值、重复值、数据类型。你看下面这张图,基本就是我们的作战路线。
这张图就是咱们这章要干的事。别被吓到,一个一个来不难。
4.2 缺失值处理——药不能停,数据也不能缺
我记得第一次拿到医院处方数据时,有个字段叫“用法用量”,将近30%是空值。要是直接建模,结果肯定跑偏。Pandas里处理缺失值就那么几板斧。
4.2.1 先摸清家底:isnull 和 info
import pandas as pd
df = pd.read_csv('drug_prescriptions.csv')
# 看看每列缺多少
print(df.isnull().sum())
# 或者直接用info,一目了然
print(df.info())
我个人习惯先跑info()。它不仅显示非空数量,还把数据类型一起列出来,一举两得。
| 方法 | 作用 | 常用场景 |
|---|---|---|
| isnull().sum() | 每列的缺失值总数 | 快速概览 |
| info() | 数据类型 + 非空计数 | 综合检查 |
| isna() / notna() | 返回布尔矩阵 | 条件筛选 |
4.2.2 填空还是删行?
遇到缺失值,无非两条路:要么填,要么扔。扔数据挺心疼的,但有时候没办法。
# 方法一:直接扔掉缺失比例>50%的列
thresh = len(df) * 0.5
df_clean = df.dropna(axis=1, thresh=thresh)
# 方法二:用均值/中位数填充
df['dosage'].fillna(df['dosage'].median(), inplace=True)
# 方法三:用前一个值填充(时间序列常用)
df['time'] = df['time'].fillna(method='ffill')
4.2.3 高级玩法:插值
如果数据有顺序,比如按时间排列的用药记录,用interpolate()做线性插值效果不错。
# 线性插值,自动填补缺失的中间值
df['concentration'].interpolate(method='linear', inplace=True)
4.3 重复值处理——拒绝注水数据
重复数据在药物数据里特别常见。比如同一个患者同一天的处方,被录入系统两次。这种重复如果不清理,统计结果会严重失真。
4.3.1 发现重复
# 查看全部重复的行
duplicates = df[df.duplicated()]
print(f"重复行数: {len(duplicates)}")
# 只看某几列是否有重复
df.duplicated(subset=['patient_id', 'drug_code', 'prescription_date'])
你想想看,如果只按patient_id检查,那一个人多次用药记录都会被标记为重复,这不对。所以我建议subst参数要选对——要加上时间戳或用药ID。
4.3.2 删除重复
# 保留第一次出现的,去掉后面的
df.drop_duplicates(inplace=True)
# 保留最后一次出现的
df.drop_duplicates(keep='last', inplace=True)
# 完全不要重复行
df.drop_duplicates(keep=False, inplace=True)
我在项目中遇到过一种情况:同一个医嘱有两次录入,但其中一次有完整的检验结果,另一次没有。这时候keep='last'可能更好——通常后录入的那条信息更全。
4.4 数据类型转换——让Pandas理解你的数据
药物数据里最头疼的就是类型不对。明明是数值型的“剂量”,读进来成了object;明明是日期,成了字符串。机器不认识,你分析的逻辑就跑偏。
4.4.1 用astype转换常见类型
# 把剂量列转为浮点数
df['dosage'] = df['dosage'].astype(float)
# 把患者ID转为字符串
df['patient_id'] = df['patient_id'].astype(str)
但有个坑——如果列里混了非数值字符(比如“50mg”),astype(float)会直接报错。我一般先用pd.to_numeric加errors='coerce'。
# 安全转换,非法值变成NaN
df['dosage'] = pd.to_numeric(df['dosage'], errors='coerce')
4.4.2 日期时间处理——最容易翻车的环节
说实话,日期这块我翻车过好多次。各种格式:2023-01-15、01/15/2023、15-Jan-2023……还好Pandas有万能工具。
# 自动识别常见日期格式
df['prescription_date'] = pd.to_datetime(df['prescription_date'])
# 处理混合格式,遇到解析失败转为NaT
df['prescription_date'] = pd.to_datetime(df['prescription_date'], errors='coerce')
4.4.3 类别数据优化
药物分类、剂型这类重复值多的列,用category类型能省内存、提速。
df['drug_category'] = df['drug_category'].astype('category')
print(df['drug_category'].cat.categories) # 查看所有类别
| 转换函数 | 适用场景 | 注意 |
|---|---|---|
| astype(float) | 纯数字列 | 先清理非数字字符 |
| pd.to_numeric | 含非法值的数字列 | errors='coerce' |
| pd.to_datetime | 日期/时间列 | 指定format或dayfirst |
| astype('category') | 低基数类别列 | 确保值有限且稳定 |
4.5 组合拳:一个真实的清洗流水线
最后给你看一个我在课程项目中真正用过的清洗流程,把今天讲的串起来。
def clean_drug_data(df):
# 1. 移除全空的行和列
df.dropna(how='all', inplace=True)
df.dropna(axis=1, how='all', inplace=True)
# 2. 日期统一
df['rx_date'] = pd.to_datetime(df['rx_date'], errors='coerce')
# 3. 剂量转数值,非法变NaN
df['dosage'] = pd.to_numeric(df['dosage'], errors='coerce')
# 4. 用分组中位数填充缺失的剂量
df['dosage'] = df.groupby('drug_code')['dosage'].transform(
lambda x: x.fillna(x.median())
)
# 5. 去重:同一天同患者同药品只保留一条
df.drop_duplicates(subset=['patient_id', 'drug_code', 'rx_date'], keep='last', inplace=True)
# 6. 去掉日期还是None的记录
df.dropna(subset=['rx_date'], inplace=True)
return df.reset_index(drop=True)
这段代码你完全可以拿去用。但记住,每步之后最好print(df.shape)看看数据量,我习惯每改一下就来个计数,心里有底。
4.6 本章小结要点
- 缺失值:先看分布,再选策略——填空、插值还是删行,取决于业务场景。
- 重复值:明确重复的定义,用subset精确控制;keep参数决定保留哪一条。
- 类型转换:数值用pd.to_numeric,日期用pd.to_datetime,类别用astype('category')。
- 清洗不是一次性的——每步检查,随时回退。
嗯,数据清洗看着琐碎,但如果你在这一章下够功夫,后面的建模会顺很多。我自己就是吃了不少亏才养成了这些习惯,希望你少走弯路。