在数据分析流程中,数据清洗与预处理往往比建模本身更耗时。Pandas 提供了一整套 API 来覆盖缺失值、重复值、类型、结构、文本、分箱等环节。本文按“发现问题是哪一类→用哪个函数→参数怎么写”的顺序,把 7 类高频操作整理成可直接复用的代码片段,所有示例均基于 numpy 与 pandas 的常规接口,版本差异影响很小。
一、缺失值处理:优先填充,其次删除
处理缺失值的第一原则是“能填充不删除”。删除只适用于缺失比例极低(经验值小于 5%)且无填充意义的场景,通过 dropna(subset=[...]) 精确指定判断列,避免误删整行。填充又分几档:固定值填充适合有业务默认值的字段(如未知性别填 'unknown');统计值填充中,数值列用均值或中位数,分类列用众数;时序数据用 ffill/bfill 方向填充;需要按维度分别填充时,用 groupby + transform + fillna 组合,保证填充值来自同组而不是全局。
- import pandas as pd
- import numpy as np
- # 构造测试数据
- df = pd.DataFrame({
- '地区': ['北京', '北京', '上海', '上海', '广州', '广州'],
- '销售额': [100, np.nan, 200, 150, np.nan, 300],
- '品类': ['家电', np.nan, '数码', '数码', '家电', '家电']
- })
- # 1. 删除缺失值(仅删除销售额列有缺失的行)
- df_drop = df.dropna(subset=['销售额'])
- # 2. 数值列用分组均值填充(按地区填充该地区销售额均值)
- df['销售额'] = df.groupby('地区')['销售额'].transform(lambda x: x.fillna(x.mean()))
- # 3. 分类列用众数填充
- df['品类'] = df['品类'].fillna(df['品类'].mode()[0])
- # 4. 时序数据方向填充(示例)
- df_time = pd.DataFrame({'温度': [25, np.nan, 27, np.nan, 28]},
- index=pd.date_range('2026-03-01', periods=5))
- df_time['温度'] = df_time['温度'].fillna(method='ffill') # 前向填充
复制代码
这里的关键点是 transform:它返回与原始行数对齐的结果,因此可以直接赋值回原列;如果直接用 groupby().mean() 会得到按地区聚合后的短表,无法回填。
二、重复数据处理:精准识别与保留规则
duplicated() 返回布尔序列用于标记重复行,默认要求全部列相同才算重复,用 subset 参数可以改判重口径(例如只按 ID 判重)。drop_duplicates() 负责真正删除,keep='first' 保留首次出现,keep='last' 保留最后一次。进阶用法是先按业务指标排序再去重,从而实现“同一 ID 只保留金额最大的那条”。
- # 构造测试数据
- df = pd.DataFrame({
- 'ID': [1, 1, 2, 3, 3],
- '姓名': ['张三', '张三', '李四', '王五', '王五'],
- '金额': [100, 200, 150, 300, 300]
- })
- # 1. 检测重复(默认全部列相同才视为重复)
- df['是否重复'] = df.duplicated()
- # 2. 按指定列检测重复(仅 ID 相同即视为重复)
- df['ID重复'] = df.duplicated(subset=['ID'])
- # 3. 删除重复行(保留第一次出现的行)
- df_clean = df.drop_duplicates()
- # 4. 按 ID 去重,保留金额最大的行(进阶用法)
- df_clean = df.sort_values('金额', ascending=False).drop_duplicates(subset=['ID'], keep='first')
复制代码
三、数据类型转换:隐性坑最多的一步
类型必须与业务含义匹配。astype() 用于数值、字符串、分类类型互转;pd.to_numeric() 负责清洗非标准数值,带逗号的 "5,000" 需要先用 str.replace 去掉逗号,异常字符串则可用 errors='coerce' 直接转为 NaN;pd.to_datetime() 能自动识别多种日期格式;低基数列转 astype('category') 可节省内存并加速分组。日期列还能通过 dt 访问器派生年、月、星期等字段,其中 dt.dayofweek 以 0 表示周一、6 表示周日。
- # 构造测试数据
- df = pd.DataFrame({
- '年龄': ['25', '30', '未知'],
- '收入': ['5,000', '8,000', '10,000'],
- '日期': ['2026-03-01', '2026/03/02', '20260303'],
- '性别': ['男', '女', '男']
- })
- # 1. 字符串转数值(处理异常值)
- df['年龄'] = pd.to_numeric(df['年龄'], errors='coerce') # "未知" 转为 NaN
- df['收入'] = pd.to_numeric(df['收入'].str.replace(',', ''))
- # 2. 字符串转日期(自动识别多种格式)
- df['日期'] = pd.to_datetime(df['日期'])
- # 3. 转为分类类型(节省内存,加速分组)
- df['性别'] = df['性别'].astype('category')
- # 4. 日期提取年/月/日(衍生字段)
- df['年'] = df['日期'].dt.year
- df['月'] = df['日期'].dt.month
- df['星期'] = df['日期'].dt.dayofweek
复制代码
四、数据重塑:长表与宽表互换
重塑的目的是让数据结构适配分析视角。melt() 把多列压成“键值对”形式实现宽转长,pivot() 反过来把某个字段的取值展开为列实现长转宽,pivot_table() 则在透视的同时做聚合,用 aggfunc 指定 sum、mean 等聚合方式。三者常配合使用:先 melt 后 pivot 可以验证转换是否可逆。
- # 构造测试数据
- df = pd.DataFrame({
- '地区': ['北京', '北京', '上海', '上海'],
- '品类': ['家电', '数码', '家电', '数码'],
- '2025年': [100, 200, 150, 250],
- '2026年': [120, 220, 180, 280]
- })
- # 1. 宽转长(将年份列转为行)
- df_long = df.melt(
- id_vars=['地区', '品类'], # 保留的标识列
- value_vars=['2025年', '2026年'], # 要转换的列
- var_name='年份', # 原列名放入的新列
- value_name='销售额' # 数值放入的新列
- )
- # 2. 长转宽(恢复宽表)
- df_wide = df_long.pivot(
- index=['地区', '品类'],
- columns='年份',
- values='销售额'
- ).reset_index()
- # 3. 透视表(聚合统计)
- df_pivot = df.pivot_table(
- index='地区',
- columns='品类',
- values=['2025年', '2026年'],
- aggfunc='sum'
- )
复制代码
五、文本数据处理:标准化后再提取
文本清洗的通用套路是先去噪再抽取。str.strip() 去首尾空格,str.lower()/str.upper() 统一大小写,str.replace() 清掉横线、空格、特殊字符;str.split(',', expand=True) 把逗号分隔的多值列拆成多列,再配合 pd.concat(axis=1) 拼回主表;str.extract() 用正则捕获组提取关键片段,str.contains() 用于按内容筛选行。
- # 构造测试数据
- df = pd.DataFrame({
- '姓名': [' 张三 ', '李四 ', ' 王五'],
- '手机号': ['138-1234-5678', '139 9876 5432', '13788889999'],
- '地址': ['北京市朝阳区XX小区', '上海市浦东新区XX路', '广州市天河区XX街'],
- '爱好': ['篮球,足球', '读书,旅游', '跑步,游泳']
- })
- # 1. 格式标准化
- df['姓名'] = df['姓名'].str.strip()
- df['地址'] = df['地址'].str.replace('市', '')
- # 2. 清理特殊字符
- df['手机号'] = df['手机号'].str.replace('-', '').str.replace(' ', '')
- # 3. 拆分多值列
- df_hobby = df['爱好'].str.split(',', expand=True)
- df_hobby.columns = ['爱好1', '爱好2']
- df = pd.concat([df, df_hobby], axis=1)
- # 4. 提取关键信息(从地址提取城市名)
- df['省份'] = df['地址'].str.extract('(北京|上海|广州)')
- # 5. 筛选包含指定内容的行
- df_bj = df[df['地址'].str.contains('北京')]
复制代码
六、数据分箱与离散化
把连续数值转成区间标签,常用于统计报表和特征工程。pd.cut() 是等宽分箱,bins 传整数表示区间个数,也可传自定义边界列表,right=False 表示左闭右开;pd.qcut() 是等频分箱,q 为分位组数,保证每组样本量接近。分箱结果列可直接参与 groupby 做分组统计。
- # 构造测试数据
- df = pd.DataFrame({
- '年龄': [18, 25, 32, 45, 58, 65, 72, 80],
- '收入': [3000, 8000, 15000, 25000, 50000, 80000, 100000, 150000]
- })
- # 1. 等宽分箱(年龄分为 4 个等宽区间)
- df['年龄分档_等宽'] = pd.cut(df['年龄'], bins=4, labels=['青年', '中年', '中老年', '老年'])
- # 2. 等频分箱(收入分为 3 个区间,每个区间样本数相同)
- df['收入分档_等频'] = pd.qcut(df['收入'], q=3, labels=['低收入', '中收入', '高收入'])
- # 3. 自定义分箱(按业务规则分年龄)
- age_bins = [0, 18, 30, 50, 100]
- age_labels = ['未成年', '青年', '中年', '老年']
- df['年龄分档_自定义'] = pd.cut(df['年龄'], bins=age_bins, labels=age_labels, right=False)
- # 4. 分箱后统计(各年龄档的收入均值)
- df.groupby('年龄分档_自定义')['收入'].mean()
复制代码
注意 labels 的长度必须与区间个数一致,否则会直接抛错;自定义边界时 right 参数决定区间开闭,边界值落在哪一档要提前确认清楚。
七、其他高频转换
剩下的零散操作同样日常:列间运算生成新列、map 按字典做值映射、sort_values 多列排序、reset_index(drop=True) 重整不连续索引、rename 重命名列、布尔索引过滤行。
- # 构造测试数据
- df = pd.DataFrame({
- 'A': [1, 2, 3, 4],
- 'B': [10, 20, 30, 40],
- 'C': ['a', 'b', 'a', 'b']
- })
- # 1. 列运算(创建新列)
- df['A+B'] = df['A'] + df['B']
- df['A*B'] = df['A'] * df['B']
- df['B/A'] = df['B'] / df['A']
- # 2. 映射替换(按字典替换值)
- map_dict = {'a': '类别1', 'b': '类别2'}
- df['C_映射'] = df['C'].map(map_dict)
- # 3. 排序(按多列排序)
- df_sorted = df.sort_values(by=['C', 'A'], ascending=[True, False])
- # 4. 重置索引(清洗后索引不连续时)
- df_reset = df_sorted.reset_index(drop=True)
- # 5. 重命名列
- df_rename = df.rename(columns={'A': '列1', 'B': '列2', 'C': '列3'})
- # 6. 过滤行(按条件筛选)
- df_filter = df[df['B'] > 20]
复制代码
执行顺序与工程化建议
这 7 类方法几乎覆盖了日常清洗的绝大多数场景,执行顺序建议固定为:先处理缺失值和重复值,再做类型转换,最后根据业务需求做重塑、分箱、文本提取等进阶操作。原因是缺失值会影响均值、众数等统计结果,重复行会放大分箱与透视的计数,而类型不正确会让日期解析、数值比较直接失败。
工程化角度,复杂清洗链路建议封装成函数,把每一步的输入输出、异常捕获和日志记录一并写进去,既方便复用,也便于出问题时定位是哪一步改变了数据形态。函数内部按“参数化列名 + 显式返回新 DataFrame”的方式实现,可以避免链式赋值带来的副本与视图混淆问题。 |