数据分析师的一天: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
记住三条原则
- 先观察,再动手。
df.describe()和df.head(20)是你的好朋友,别急着改数据。 - 保留原始数据。清洗前永远复制一份
df_clean = df.copy(),出了问题能回溯。 - 写清洗日志。每步操作记下来:删了多少行、填了多少值。下次遇到同样格式的数据直接复用。
数据清洗不是技术活,是耐心活。掌握这些套路,以后谁再扔给你一坨脏数据,你都能面不改色地三分钟搞定。