🏠 首页 攻略 Python pandas数据清洗实战:5个高频技巧解决80%脏数据问题

Python pandas数据清洗实战:5个高频技巧解决80%脏数据问题

拿到一份数据,80%的时间花在清洗上?本文总结pandas最常用的5个数据清洗技巧:缺失值处理、重复值去重、异常值检测、日期解析和字段拆分。附完整代码示例,直接复制就能用。

数据分析师的一天:20%分析,80%洗数据

你肯定经历过这种场景:老板甩给你一个CSV文件,说"帮我看下上周的销售数据"。

打开一看:

  • 有5万行,但每行格式都不统一
  • 日期列有的写"2024-01-15",有的写"01/15/24"
  • 手机号列混着身份证号
  • 价格列带着"¥“符号和千分位逗号

这时候别慌。数据清洗占数据分析80%的时间,掌握正确的方法,能把这个比例降到30%。

下面这5个技巧覆盖了日常工作中80%的脏数据场景。每个技巧都附可直接运行的代码。

技巧1:缺失值处理——别急着删

拿到数据第一反应:有缺失值的行删掉。

等等。删之前先看看缺失了多少。

import pandas as pd

df = pd.read_csv('sales_data.csv')

# 先看整体缺失情况
print(df.isnull().sum())
print(f"缺失率:{df.isnull().mean().mean()*100:.2f}%")

如果某列缺失率超过50%,删掉可能比保留更有意义。但如果只有5%缺失,直接删除会损失大量有效数据。

三种处理方式:

# 数值型:用中位数填充(不受极端值影响)
df['salary'].fillna(df['salary'].median(), inplace=True)

# 分类变量:用众数填充
df['city'].fillna(df['city'].mode()[0], inplace=True)

# 时间序列:用前向填充(上一个有效值)
df['price'].fillna(method='ffill', inplace=True)

关键原则:知道为什么缺失,比怎么填充更重要。如果是系统bug导致的随机缺失,填充没问题。如果是某个渠道没上报数据,那这个缺失本身就是一个信号。

技巧2:去重——你以为的唯一值其实不唯一

drop_duplicates() 大家都知道。但很多人只用了默认参数,漏掉了真正的重复。

# 基础去重:所有列完全相同才算重复
df.drop_duplicates(inplace=True)

# 按指定列去重,保留最后一行
df.drop_duplicates(subset=['order_id'], keep='last', inplace=True)

# 找出重复行(不删,先看看有多少)
duplicates = df[df.duplicated(subset=['email'], keep=False)]
print(f"重复邮箱数量:{len(duplicates)}")

实战场景:电商订单表经常有同一笔订单被记录两次的情况。用户刷新页面、网络重试都会产生重复。用order_id去重,保留时间戳最新的那条。

技巧3:异常值检测——别让 outliers 毁掉你的统计

平均值被马云拉高到千亿的故事大家都听过。异常值会让你的统计分析完全失真。

方法一:IQR四分位距法(推荐)

Q1 = df['order_amount'].quantile(0.25)
Q3 = df['order_amount'].quantile(0.75)
IQR = Q3 - Q1

# 超出这个范围的标记为异常
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['order_amount'] < lower_bound) | (df['order_amount'] > upper_bound)]
print(f"发现 {len(outliers)} 个异常值")

# 可选:截断而非删除
df['order_amount'] = df['order_amount'].clip(lower=lower_bound, upper=upper_bound)

方法二:Z-Score法(适合正态分布数据)

from scipy import stats

z_scores = np.abs(stats.zscore(df['age'].dropna()))
df['is_outlier'] = z_scores > 3

IQR法更稳健,不假设数据分布。Z-Score适合已经做过清洗、接近正态分布的数据。

技巧4:日期解析——统一格式是第一步

日期是最常见的脏数据格式之一。同一个字段里混着"2024/01/15”、“Jan 15, 2024”、“20240115”。

# pandas内置解析器,自动识别多种格式
df['date'] = pd.to_datetime(df['date'], errors='coerce')

# 解析不了的会变成 NaT(Not a Time)
print(f"解析失败的数量:{df['date'].isna().sum()}")

# 统一输出格式
df['date_formatted'] = df['date'].dt.strftime('%Y-%m-%d')

# 提取年月日做分组分析
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.day_name()

如果某些日期格式实在解析不了,单独拎出来看:

failed = df[df['date'].isna()]
# 检查是不是有特殊格式,比如"昨天"、"上周"这种非标准写法
print(failed['date'].value_counts().head(20))

技巧5:字符串清洗和字段拆分

Excel里能做的VLOOKUP和分列,pandas一行代码搞定。

# 去掉前后空格
df['name'] = df['name'].str.strip()

# 统一大小写
df['category'] = df['category'].str.lower().str.strip()

# 去掉货币符号和千分位
df['price'] = df['price'].str.replace('¥', '').str.replace(',', '')
df['price'] = df['price'].astype(float)

# 分列:把"北京-朝阳区"拆成城市和区域
df[['city', 'district']] = df['address'].str.split('-', expand=True)

# 正则提取邮箱
df['email'] = df['contact'].str.extract(r'([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)')

一个实战案例: 处理用户手机号字段。

# 统一手机号格式:去掉+86、空格、横杠
df['phone'] = df['phone'].str.replace(r'[+\s\-]', '', regex=True)

# 补全:有些是7位本地号码,有些是11位国际格式
df['phone'] = df['phone'].apply(
    lambda x: '86' + str(x) if len(str(x)) == 7 else str(x)
)

# 验证:应该是11位数字
df['phone_valid'] = df['phone'].str.match(r'^86\d{11}$')

完整清洗流程模板

把上面5个技巧串起来,就是一个通用的数据清洗模板:

def clean_sales_data(filepath):
    df = pd.read_csv(filepath)
    
    # 1. 查看基本信息
    print(f"原始数据:{df.shape}")
    print(f"缺失率:{df.isnull().mean().mean()*100:.2f}%")
    
    # 2. 去重
    before = len(df)
    df.drop_duplicates(inplace=True)
    print(f"去重后:{len(df)}(移除{before - len(df)}条)")
    
    # 3. 日期解析
    df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce')
    
    # 4. 价格清洗
    df['amount'] = df['amount'].str.replace('¥', '').str.replace(',', '').astype(float)
    
    # 5. 异常值处理
    Q1 = df['amount'].quantile(0.25)
    Q3 = df['amount'].quantile(0.75)
    IQR = Q3 - Q1
    df['amount'] = df['amount'].clip(Q1 - 1.5*IQR, Q3 + 1.5*IQR)
    
    # 6. 缺失值填充
    df.fillna({'city': df['city'].mode()[0]}, inplace=True)
    
    print(f"清洗完成:{df.shape}")
    return df

记住三条原则

  1. 先观察,再动手df.describe()df.head(20) 是你的好朋友,别急着改数据。
  2. 保留原始数据。清洗前永远复制一份 df_clean = df.copy(),出了问题能回溯。
  3. 写清洗日志。每步操作记下来:删了多少行、填了多少值。下次遇到同样格式的数据直接复用。

数据清洗不是技术活,是耐心活。掌握这些套路,以后谁再扔给你一坨脏数据,你都能面不改色地三分钟搞定。