脏数据是模型的头号杀手:缺失、异常、重复、格式混乱……本页用一份 15 行的真实脏数据,带你走完从「体检」到「干净」的完整六步。
下面是一份典型的用户行为表(7 列 × 15 行),集中了表格数据里最常见的 8 类脏问题。先学会「看懂脏在哪」,后面的每一步清洗都是在解决这里标注出来的问题。
🔴 红底行 = 含脏数据的问题行;红色字段 = 具体出问题的单元格。注意 note 列 15 行全空 —— 这是典型的「无效字段」,会在 ⑤ 号章节直接删除。下载 CSV 后可以用 Excel / pandas 打开,跟着后面章节一步步清洗。
几乎所有模型都不接受 NaN。处理缺失值没有银弹,核心是先看缺失比例和字段重要性,再决定「删」还是「填」:
| 策略 | 适用场景 | 本页样本示例 |
|---|---|---|
| 删除行 | 关键字段(如 label)缺失、缺失量很少且随机 | label 缺失的 U010 / U015 |
| 均值填充 | 数值列、近似正态分布 | income(收入近似正态时) |
| 中位数填充 | 数值列、分布偏斜或有异常值 | age(存在 999 这种极端值) |
| 众数填充 | 分类列(城市、性别) | city 缺失时填 Beijing |
| 前向填充 | 时间序列数据(股价、传感器) | 按日期排序后用上一行补 |
import pandas as pd df = pd.read_csv('table_dirty_sample.csv') # ① 先体检:每列各缺失多少? print(df.isna().sum()) # ② 删除:label(训练目标)缺失的行无法补救 → 直接删 df = df.dropna(subset=['label']) # ③ 填充:数值列 / 分类列策略不同 df['age'] = df['age'].fillna(df['age'].median()) # 中位数:抗异常值干扰 df['income'] = df['income'].fillna(df['income'].mean()) # 均值:近似正态分布时 df['city'] = df['city'].fillna(df['city'].mode()[0]) # 众数:分类列专用 # ④ 时间序列场景:前向填充(用上一行的值补上) df = df.ffill()
样本里的 age = -5、age = 999 就是典型异常值。它们会严重拉偏均值和方差,让模型被少数几个点「绑架」。三种主流检测方法:
import numpy as np # 方法① IQR 箱线图法(推荐:对极端值稳健) Q1 = df['age'].quantile(0.25) # 25% 分位数 Q3 = df['age'].quantile(0.75) # 75% 分位数 IQR = Q3 - Q1 # 四分位距 lower = Q1 - 1.5 * IQR # 下限 upper = Q3 + 1.5 * IQR # 上限 df = df[(df['age'] >= lower) & (df['age'] <= upper)] # 方法② 3σ 法则(要求数据近似正态分布) mean, std = df['income'].mean(), df['income'].std() df = df[np.abs(df['income'] - mean) <= 3 * std] # 方法③ 业务规则(最直接:年龄合法范围 0~120) df = df[(df['age'] >= 0) & (df['age'] <= 120)]
样本第 8 行和第 1 行所有字段完全一致,是采集/合并时的重复录入。重复行有两个危害:模型对重复样本「过度学习」;更隐蔽的是,同一条样本同时落进训练集和测试集,会让评估指标虚高。
# ① 先看看哪些行完全重复 print(df[df.duplicated()]) # ② 删除完全重复行(默认保留第一次出现的) df = df.drop_duplicates() # ③ 按关键列去重:同一个 id 只保留一条 df = df.drop_duplicates(subset=['id'], keep='last') # ④ keep 参数:'first' / 'last' / False(重复的全部删掉) # 配合排序可以实现「同一用户只保留最新一条记录」 df = df.sort_values('register_date').drop_duplicates(subset=['id'], keep='last')
不是所有列都值得喂给模型。三类典型「无效字段」,删掉反而让模型更专注:
# ① 常量列 / 全空列:note 15 行全空,没有任何信息量 df = df.drop(columns=['note']) # ② 唯一 ID 列:每行都不一样,模型学不到规律(还会记住噪声) if df['id'].nunique() == len(df): df = df.drop(columns=['id']) # ③ 缺失率超过 80% 的列 threshold = 0.8 drop_cols = df.columns[df.isna().mean() > threshold] print('高缺失列:', list(drop_cols)) df = df.drop(columns=drop_cols)
CSV 读进来的列经常「类型不对」:"8,500" 是字符串,算不了均值;2024/01/03、03-01-2024、2024.02.15 三种日期格式混在一起,无法排序和提取特征;beijing / Beijing / BEIJING 会被当成三个不同城市。统一类型是进模型前的必经之路:
# ① 千分位字符串 → 数值:先去掉逗号,再转类型 df['income'] = df['income'].astype(str).str.replace(',', '') df['income'] = pd.to_numeric(df['income'], errors='coerce') # errors='coerce':转不动的(比如 'abc')一律变成 NaN,交给缺失值流程 # ② 日期统一:显式列出常见格式,逐个尝试 def parse_date(s): for fmt in ('%Y-%m-%d', '%Y/%m/%d', '%d-%m-%Y', '%Y.%m.%d'): try: return pd.to_datetime(s, format=fmt) except (ValueError, TypeError): continue return pd.NaT # 实在认不出 → 置为缺失,走缺失值流程 df['register_date'] = df['register_date'].apply(parse_date) # ③ 文本规范化:去首尾空格 + 统一首字母大写 df['city'] = df['city'].str.strip().str.title() # beijing / BEIJING → Beijing
模型只吃数字。两件事要做:类别 → 数字(编码),量纲统一(缩放)——年龄几十、收入几万,不缩放的话收入会「淹没」年龄的作用。
| 方法 | 适用 | 注意 |
|---|---|---|
| One-Hot | 无序类别(城市) | 类别多则列爆炸 |
| Label Encoding | 有序类别(大/中/小)、树模型 | 会引入虚假大小关系,线性模型慎用 |
| 方法 | 结果 | 适用 |
|---|---|---|
| StandardScaler | 均值 0、方差 1 | 大多数模型的默认选择 |
| MinMaxScaler | 压到 [0, 1] | 神经网络 / 距离类(KNN);怕异常值 |
from sklearn.preprocessing import StandardScaler, MinMaxScaler # ① One-Hot:无序类别(城市)→ 每个类别变成一列 0/1 df = pd.get_dummies(df, columns=['city'], prefix='city') # ② 有序映射 / 标签编码:label 普通→0,高价值→1 df['label'] = df['label'].map({'普通': 0, '高价值': 1}) # ③ 标准化:均值 0、方差 1(逻辑回归 / SVM 等首选) scaler = StandardScaler() df[['age', 'income']] = scaler.fit_transform(df[['age', 'income']]) # ④ 归一化:压缩到 [0, 1](神经网络 / KNN 常用) mm = MinMaxScaler() df[['age', 'income']] = mm.fit_transform(df[['age', 'income']])
把前面六步串起来。点击按钮,每点一次执行一步清洗(数据就是 ① 号章节那 15 行脏数据):左边被删除的行会逐渐淡出,右边实时展示当前的干净数据。
💡 最终剩下 7 行干净数据:age / income 归一化到 [0,1],city 展开为 One-Hot 列,label 变成 0/1 —— 可以直接喂给模型训练。
把 ① 号章节下载的 table_dirty_sample.csv 放到代码同目录,运行下面这段端到端脚本(pandas + sklearn),即可复现上面 demo 的完整六步:(15, 7) → (7, 9),输出可直接建模的 table_clean.csv。
import pandas as pd from sklearn.preprocessing import StandardScaler # ========== 0. 读取脏数据 ========== df = pd.read_csv('table_dirty_sample.csv') print('原始:', df.shape) # (15, 7) # ========== ① 缺失值:关键字段缺失 → 删行 ========== df = df.dropna(subset=['age', 'income', 'label']) print('删缺失后:', df.shape) # (10, 7) # ========== ② 异常值:业务规则,合法年龄 0~120 ========== df = df[(df['age'] >= 0) & (df['age'] <= 120)] print('剔异常后:', df.shape) # (8, 7):-5 和 999 被剔除 # ========== ③ 重复值:完全重复的行只留一条 ========== df = df.drop_duplicates() print('去重后:', df.shape) # (7, 7) # ========== ④ 无效字段:全空列 + 唯一 ID 列 ========== df = df.drop(columns=['note']) # 全空列,无信息量 if df['id'].nunique() == len(df): df = df.drop(columns=['id']) # 每行唯一 → 对模型无意义 # ========== ⑤ 类型转换:数值 / 日期 / 文本规范化 ========== df['income'] = df['income'].astype(str).str.replace(',', '') df['income'] = pd.to_numeric(df['income'], errors='coerce') # "8,500" → 8500 def parse_date(s): # 多格式日期统一 for fmt in ('%Y-%m-%d', '%Y/%m/%d', '%d-%m-%Y', '%Y.%m.%d'): try: return pd.to_datetime(s, format=fmt) except (ValueError, TypeError): continue return pd.NaT df['register_date'] = df['register_date'].apply(parse_date) df['city'] = df['city'].str.strip().str.title() # beijing / BEIJING → Beijing # ========== ⑥ 编码 + 归一化 ========== df = pd.get_dummies(df, columns=['city'], prefix='city') # 无序类别 → One-Hot df['label'] = df['label'].map({'普通': 0, '高价值': 1}) # 标签 → 0/1 scaler = StandardScaler() df[['age', 'income']] = scaler.fit_transform(df[['age', 'income']]) print('清洗完成:', df.shape) # (7, 9) df.to_csv('table_clean.csv', index=False, encoding='utf-8-sig')