当前位置: 首页 > news >正文

072、Pandas 数据清洗:缺失值处理、类型转换、字符串操作、apply 家族

072、Pandas 数据清洗:缺失值处理、类型转换、字符串操作、apply 家族

上周五晚上十一点,我正盯着一个客户的生产环境数据报表发呆。那个CSV文件有三十万行,客户说“数据很干净,就是跑个统计”。结果我pd.read_csv一加载,发现“年龄”列里混着“25岁”、“三十”、“NaN”、“None”,甚至还有一行写着“保密”。更离谱的是,“收入”列里有些数字带逗号,有些带“元”,还有几个单元格直接是空字符串。那一刻我意识到,所谓“干净数据”只是数据清洗前的幻觉。

缺失值处理:别让NaN悄悄搞崩你的模型

缺失值处理是数据清洗的第一步,也是最容易翻车的地方。很多人一上来就df.dropna(),结果把有用的行也删了。我见过最惨的案例:一个同事删掉了80%的数据,就因为某列有少量缺失。

先摸清缺失的底细

importpandasaspdimportnumpyasnp# 模拟一份真实数据——别笑,这就是生产环境常见的样子df=pd.DataFrame({'姓名':['张三','李四',np.nan,'王五','赵六'],'年龄':[25,np.nan,30,'保密',28],'收入':['12,000元','15000',np.nan,'20000元',None],'城市':['北京','上海','广州',np.nan,'深圳']})# 这里踩过坑:直接用isnull()只能看到NaN,None和空字符串会被忽略print(df.isnull().sum())# 只统计NaN,不统计None和空字符串# 正确姿势:把常见缺失值统一替换df=df.replace(['None','','保密','未知'],np.nan)print(df.isnull().sum())# 现在统计准确了

处理策略:别一刀切

缺失值处理没有银弹,得看业务场景。我一般按这个优先级来:

# 1. 数值列:用中位数填充(比均值更抗异常值)# 别这样写:df['年龄'].fillna(df['年龄'].mean()) # 均值会被极端值带偏df['年龄']=pd.to_numeric(df['年龄'],errors='coerce')# 先把非数字转成NaNdf['年龄']=df['年龄'].fillna(df['年龄'].median())# 中位数更稳健# 2. 分类列:用众数填充,或者单独标记为"未知"df['城市']=df['城市'].fillna('未知城市')# 保留缺失信息,比随便填一个城市好# 3. 如果缺失比例超过50%,考虑直接删除该列ifdf['收入'].isnull().sum()/len(df)>0.5:df=df.drop(columns=['收入'])else:# 这里有个小技巧:用前后值填充,适合时间序列df['收入']=df['收入'].fillna(method='ffill')# 前向填充

真实场景的坑:有一次我处理销售数据,用均值填充了缺失的销售额,结果模型训练出来偏差很大。后来发现,缺失的销售额都是周末的数据,而周末销售额本来就低。所以,填充前一定要分析缺失模式

类型转换:Pandas的隐式类型推断是个坑

Pandas读取数据时,会自动推断类型,但经常翻车。比如“年龄”列里有字符串,它就把整列变成object类型。更坑的是,有些列看起来是数字,但因为有逗号或单位,也被当成字符串。

# 模拟一个典型的生产数据df=pd.DataFrame({'日期':['2024-01-01','2024/01/02','2024.01.03','2024-01-04'],'销售额':['12,000','15,000','20,000','18,000'],'折扣率':['0.1','0.15','0.2','0.25'],'客户ID':['C001','C002','C003','C004']})# 这里踩过坑:直接astype会报错# df['销售额'] = df['销售额'].astype(float) # 报错!逗号没处理# 正确姿势:先清洗再转换df['销售额']=df['销售额'].str.replace(',','').astype(float)df['折扣率']=df['折扣率'].astype(float)# 日期转换:统一格式df['日期']=pd.to_datetime(df['日期'],format='mixed')# 自动识别多种格式# 别这样写:df['日期'] = pd.to_datetime(df['日期']) # 遇到混合格式会报错# 客户ID保持为字符串,但去掉前导零df['客户ID']=df['客户ID'].str.lstrip('C').astype(int)# 变成1,2,3,4

类型转换的黄金法则:永远先检查dtypes,再转换。我习惯在数据加载后立即打印df.info(),看看哪些列类型不对。

字符串操作:正则表达式是你的瑞士军刀

字符串清洗是数据清洗中最耗时的部分。一个地址字段可能包含“北京市海淀区”、“北京海淀”、“海淀区北京”等各种写法。这时候,正则表达式就是你的救星。

# 模拟脏数据df=pd.DataFrame({'地址':['北京市海淀区中关村大街1号','上海浦东新区陆家嘴','广州天河区体育西路','深圳南山区科技园'],'电话':['138-1234-5678','13912345678','010-12345678','02112345678'],'邮箱':['zhangsan@qq.com','lisi@163.com','wangwu@gmail.com','zhaoliu@126.com']})# 1. 提取城市信息df['城市']=df['地址'].str.extract(r'([\u4e00-\u9fa5]{2,3}(?:市|区))')# 匹配中文城市名# 这里有个坑:extract只返回第一个匹配组,如果没匹配到会返回NaN# 2. 清洗电话号码:统一格式df['电话_清洗']=df['电话'].str.replace(r'[^\d]','',regex=True)# 只保留数字# 别这样写:df['电话'].str.replace('-', '') # 只能处理一种分隔符# 3. 提取邮箱域名df['邮箱域名']=df['邮箱'].str.extract(r'@(.+)$')# 提取@后面的部分# 4. 字符串分割:把地址拆分成省市区df[['省','市','区']]=df['地址'].str.extract(r'(.{2,3}?省)?(.{2,3}?市)?(.{2,3}?[区县])?')# 这个正则有点复杂,但很实用。注意问号表示非贪婪匹配

字符串操作的性能优化:如果数据量超过10万行,str操作会变慢。这时候可以考虑用向量化操作或者numba加速。我一般会先做一次小样本测试,确认正则没问题再跑全量。

apply家族:从入门到放弃再到精通

apply家族是Pandas里最灵活但也最容易被滥用的工具。很多人一遇到复杂操作就apply,结果代码又慢又难读。其实,大部分场景都有更好的替代方案。

# 模拟数据df=pd.DataFrame({'姓名':['张三','李四','王五','赵六'],'年龄':[25,30,35,28],'收入':[12000,15000,20000,18000],'部门':['技术部','市场部','技术部','财务部']})# 1. applymap:对整个DataFrame应用函数(已废弃,别用了)# 别这样写:df.applymap(lambda x: str(x).upper()) # 效率低,且已废弃# 正确做法:用向量化操作df=df.map(lambdax:str(x).upper()ifisinstance(x,str)elsex)# 2. apply:对行或列应用函数# 计算收入等级defincome_level(row):ifrow['收入']>15000:return'高收入'elifrow['收入']>10000:return'中等收入'else:return'低收入'# 这里踩过坑:apply默认是按列遍历,要指定axis=1才是按行df['收入等级']=df.apply(income_level,axis=1)# 3. 更高效的替代方案:用np.selectconditions=[df['收入']>15000,df['收入']>10000]choices=['高收入','中等收入']df['收入等级_v2']=np.select(conditions,choices,default='低收入')# 4. apply的高级用法:分组后apply# 计算每个部门的收入中位数defdept_median(group):returngroup['收入'].median()dept_stats=df.groupby('部门').apply(dept_median)# 但这里其实有更简单的写法:df.groupby('部门')['收入'].median()

apply的性能陷阱:apply本质上是Python循环,数据量大时很慢。我测试过,100万行数据,apply比向量化操作慢50倍以上。所以,能用向量化操作就别用apply,能用np.select就别用自定义函数

实战经验总结

写了这么多年数据清洗代码,我总结了几条血泪教训:

  1. 永远先备份原始数据。我见过太多人直接修改原DataFrame,结果清洗错了回不去。养成习惯:df_clean = df.copy()。

  2. 分步验证,不要一次性跑完所有清洗逻辑。每做一步清洗,就打印df.head()看看效果。特别是正则表达式,很容易匹配错。

  3. 缺失值处理要记录日志。我习惯在清洗后打印一份报告:原始行数、删除行数、填充列数、类型转换列数。这样出了问题能快速定位。

  4. 类型转换前先检查异常值。比如年龄列,如果出现负数或者超过150的值,先处理异常值再转换类型。

  5. apply家族要慎用。能用向量化操作解决的问题,绝对不要用apply。如果实在要用,考虑用swifter库加速。

  6. 字符串操作注意内存。str.replace和str.extract会生成新的Series,如果数据量大,内存会暴涨。可以考虑用chunksize分批处理。

最后,数据清洗不是一次性的工作。我一般会写一个清洗函数,每次加载数据都调用它。这样既保证了代码复用,也避免了重复踩坑。记住,数据清洗花的时间,会在模型训练和业务分析中加倍还回来

http://www.jsqmd.com/news/1086371/

相关文章:

  • 从“边界”视角重识C++ set的lower_bound与upper_bound
  • OMPL中BIT*算法核心流程与关键模块解析
  • Steam游戏自动破解器:终极指南与完整解决方案
  • JSON转Excel实际应用场景案例
  • HIS医院信息系统:微服务架构实践与医疗数字化转型方案
  • ENVI实战:为无地理参考的栅格影像精准注入空间坐标
  • PostgreSQL数据文件损坏:从“read only 0 of 8192 bytes”错误到精准修复
  • Fast DDS之Domain隔离与Participant通信机制
  • LSI MegaRAID实战:从零配置硬RAID到系统挂载
  • 国内各大招聘平台分类汇总|HR选型全指南,附低成本直聘渠道推荐
  • 550+免费RPG Maker插件库:从新手到专家的完整游戏开发解决方案
  • 终极WPF界面开发解决方案:HandyControls控件库完整实战指南
  • 明日方舟自动化终极指南:3分钟掌握Arknights-Mower智能基建管理
  • 微信好友检测终极指南:3分钟发现谁已悄悄删除你
  • 售前方案能不能用Codex和Claude半自动生成?客户需求到报价说明实战
  • AI私域电商品牌实测排行:2026年七大维度对比与场景适配
  • 如何高效解包Godot游戏资源:专业PCK文件提取工具完整实战指南
  • Ubuntu 20.04下Gazebo仿真环境搭建与SLAM建图导航实战
  • PID公式拆解:从连续到离散的数学之旅
  • 【Vitis/Vivado】单机多板调试实战:利用端口隔离与多实例管理FPGA集群
  • 数据分析转大模型:真实项目中的关键步骤
  • Rust Unsafe 编程:裸指针抽象与编译期防护的工程实践
  • ENVI5.3.1实战:基于Landsat 8影像的区域无缝镶嵌与精准裁剪
  • 软考证书能加多少分?官方未公开的“分级赋分模型”首次还原:高级/中级/初级对应岗位差异达4.2分
  • 英飞凌AURIX平台嵌入式开发实战:从资源获取到多环境移植
  • AOSP基础(TODO)
  • 如何利用code2flow可视化动态语言代码调用关系
  • 3步完成HS2-HF Patch安装:新手快速打造完美HoneySelect2体验
  • SD-PPP:为什么这款Photoshop AI插件能让你3分钟完成AI创作?
  • 如何在Windows系统获得Apple触控板完美体验:mac-precision-touchpad驱动终极指南