未经处理的数据,就像未经打磨的璞玉,只有经过精心雕琢,才能绽放光彩。
你是否曾经兴致勃勃地跑完一个机器学习模型,却发现准确率惨不忍睹?你是否遇到过明明算法选得没错,可结果就是不如人意?问题很可能出在数据预处理上!
俗话说“垃圾进,垃圾出”,机器学习模型对数据的质量极其敏感。现实世界中的数据往往是脏乱差的:有缺失值、有异常点、量纲不统一……今天,我们就来系统地梳理机器学习中的数据预处理方法,并附上可直接运行的Python代码,让你从数据小白进阶为预处理达人!
一、数据预处理概述
数据预处理,简单来说就是把原始数据转换成适合模型输入的格式。主要包括以下任务:
数据清洗:处理缺失值、重复值、异常值
特征工程:特征缩放、编码、构造、选择
数据变换:偏态处理、离散化、降维
数据集划分:训练/验证/测试集划分
通常,我们用pandas进行数据探索,用scikit-learn完成大部分预处理操作。开始前,先导入必要的库:
import pandas as pdimport numpy as npimport matplotlib.pyplot as pltimport seaborn as snsfrom sklearn.model_selection import train_test_splitfrom sklearn.preprocessing import (StandardScaler, MinMaxScaler, OneHotEncoder, LabelEncoder, PolynomialFeatures, PowerTransformer)from sklearn.impute import SimpleImputerfrom sklearn.feature_selection import (VarianceThreshold, SelectKBest, chi2, mutual_info_classif)from sklearn.decomposition import PCA# 设置随机种子np.random.seed(42)
二、数据清洗:让数据“干净”起来
1. 处理缺失值
缺失值是家常便饭,处理方法主要有两种:删除和填充。
来看代码示例:
# 创建示例数据df = pd.DataFrame({ 'A': [1, 2, np.nan, 4, 5], 'B': [10, np.nan, 30, 40, 50], 'C': ['cat', 'dog', 'cat', None, 'dog'], 'target': [0, 1, 0, 1, 0]})print("原始数据:\n", df)# 1. 删除缺失值所在行df_dropna = df.dropna()print("\n删除缺失行后:\n", df_dropna)# 2. 填充缺失值# 数值列用中位数填充imputer_num = SimpleImputer(strategy='median')df[['A', 'B']] = imputer_num.fit_transform(df[['A', 'B']])# 分类列用众数填充imputer_cat = SimpleImputer(strategy='most_frequent')df['C'] = imputer_cat.fit_transform(df[['C']]).ravel()print("\n填充后数据:\n", df)
除了SimpleImputer,还可以用pandas自带的fillna方法进行更灵活的填充,比如向前填充(method='ffill')或向后填充(method='bfill')。2. 处理重复值
重复数据会让模型对某些样本过拟合,必须去重。
# 检测重复行print(df.duplicated().sum()) # 输出重复行数# 删除重复行(保留第一个)df = df.drop_duplicates()
3. 处理异常值
异常值可能是测量误差或真实极端情况,需要用统计方法识别并处理。
常用方法:
# 用IQR法识别异常值Q1 = df['A'].quantile(0.25)Q3 = df['A'].quantile(0.75)IQR = Q3 - Q1lower_bound = Q1 - 1.5 * IQRupper_bound = Q3 + 1.5 * IQRoutliers = df[(df['A'] < lower_bound) | (df['A'] > upper_bound)]print("异常值:\n", outliers)# 处理方式:删除或替换为边界值df['A'] = df['A'].clip(lower_bound, upper_bound) # 截断
三、特征工程:打造模型喜欢的特征
1. 特征缩放
不同特征往往具有不同的量纲和取值范围,很多模型(如SVM、神经网络、KNN)对特征尺度敏感,需要进行缩放。
# 创建示例数据X = np.array([[1, 100], [2, 200], [3, 300], [4, 400]])# 标准化scaler_std = StandardScaler()X_std = scaler_std.fit_transform(X)print("标准化后:\n", X_std)# 归一化scaler_minmax = MinMaxScaler()X_minmax = scaler_minmax.fit_transform(X)print("归一化后:\n", X_minmax)
注意:对训练集进行fit_transform后,要用相同的scaler对测试集进行transform,避免数据泄露。2. 特征编码
机器学习模型只能处理数值,类别型特征需要转换成数值形式。
标签编码(Label Encoding):将类别映射为0,1,2...适合有序类别。
独热编码(One-Hot Encoding):创建二进制虚拟变量,适合无序类别。
# 标签编码le = LabelEncoder()df['C_encoded'] = le.fit_transform(df['C'])print("标签编码后:\n", df[['C', 'C_encoded']])# 独热编码df_encoded = pd.get_dummies(df, columns=['C'], prefix='C')print("独热编码后:\n", df_encoded)
使用sklearn的OneHotEncoder可以保留特征名称,方便后续处理。
3. 特征构造
有时候现有特征不足以表达复杂关系,需要构造新特征,比如多项式特征、组合特征等。
# 多项式特征(生成交互项和幂次项)poly = PolynomialFeatures(degree=2, include_bias=False)X_poly = poly.fit_transform(X)print("原始特征:\n", X)print("多项式特征:\n", X_poly)
还可以根据业务知识构造,比如从日期中提取星期几、是否为节假日等。4. 特征选择
特征太多会导致维度灾难,且可能包含冗余特征。特征选择可以降维、减少过拟合、提升模型效率。
常用方法:
方差过滤:删除方差低于阈值的特征(通常先归一化)。
统计检验:卡方检验、互信息等,选择与目标变量最相关的K个特征。
基于模型的选择:用随机森林等模型的特征重要性进行选择。
# 假设X为特征,y为目标X = df[['A', 'B']] # 示例特征y = df['target']# 方差过滤selector_var = VarianceThreshold(threshold=0.1)X_var = selector_var.fit_transform(X)# 卡方检验(适用于分类,特征非负)selector_chi2 = SelectKBest(chi2, k=1)X_chi2 = selector_chi2.fit_transform(X, y)print("卡方选择后特征:", X_chi2)# 互信息selector_mi = SelectKBest(mutual_info_classif, k=1)X_mi = selector_mi.fit_transform(X, y)
四、数据变换:让分布更“友好”
1. 偏态处理
许多模型假设特征服从正态分布,偏态严重的特征需要变换。常用对数变换、Box-Cox变换。
# 生成偏态数据skewed_data = np.exp(np.random.randn(1000) * 0.5) # 正偏态plt.hist(skewed_data, bins=30)plt.title("原始偏态分布")plt.show()# 对数变换log_data = np.log1p(skewed_data) # log(1+x)plt.hist(log_data, bins=30)plt.title("对数变换后")plt.show()# Box-Cox变换(要求数据>0)pt = PowerTransformer(method='box-cox')boxcox_data = pt.fit_transform(skewed_data.reshape(-1, 1))plt.hist(boxcox_data, bins=30)plt.title("Box-Cox变换后")plt.show()
2. 离散化
将连续变量分段,变成有序类别,有助于处理非线性关系。
等宽离散化:将数据分成k个等宽的区间。
等频离散化:每个区间包含相同数量的样本。
聚类离散化:用KMeans聚类得到区间。
# 等宽离散化df['A_binned'] = pd.cut(df['A'], bins=3, labels=['low', 'mid', 'high'])# 等频离散化df['A_qcut'] = pd.qcut(df['A'], q=3, labels=['Q1', 'Q2', 'Q3'])
3. 降维
当特征数量巨大时,可以用PCA等算法将特征投影到低维空间。
# PCA降维pca = PCA(n_components=2) # 降到2维X_pca = pca.fit_transform(X)print("降维后数据形状:", X_pca.shape)print("解释方差比:", pca.explained_variance_ratio_)
五、数据集划分:训练、验证、测试
永远不要用测试数据来指导预处理或模型调参!正确做法是先划分数据集,再分别预处理。
X = df.drop('target', axis=1)y = df['target']# 先划分训练集和测试集X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y)# 再划分训练集和验证集(可选)X_train, X_val, y_train, y_val = train_test_split( X_train, y_train, test_size=0.25, random_state=42, stratify=y_train)print(f"训练集大小:{X_train.shape}")print(f"验证集大小:{X_val.shape}")print(f"测试集大小:{X_test.shape}")
预处理时的注意事项:
对训练集进行fit(计算均值、方差等)和transform
对验证/测试集只进行transform(使用训练集的参数)
避免使用任何未来信息(如用整个数据集的均值填充缺失值,这是常见错误)
六、完整预处理流程示例(以泰坦尼克号数据集为例)
下面我们用Kaggle的Titanic数据集,演示一个完整的预处理流程。
# 加载数据train = pd.read_csv('train.csv') # 假设文件在当前目录test = pd.read_csv('test.csv')# 初步探索print(train.info())print(train.describe())# 选择特征(简化处理)features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']X = train[features]y = train['Survived']# 划分训练集和测试集(这里用全部训练集做训练,测试集留给最终提交)X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42)# 预处理流水线(手工逐步实现)# 1. 缺失值处理# Age用中位数填充age_median = X_train['Age'].median()X_train['Age'].fillna(age_median, inplace=True)X_test['Age'].fillna(age_median, inplace=True)# Embarked用众数填充embarked_mode = X_train['Embarked'].mode()[0]X_train['Embarked'].fillna(embarked_mode, inplace=True)X_test['Embarked'].fillna(embarked_mode, inplace=True)# 2. 特征编码# Sex: male->0, female->1X_train['Sex'] = X_train['Sex'].map({'male': 0, 'female': 1})X_test['Sex'] = X_test['Sex'].map({'male': 0, 'female': 1})# Embarked: 独热编码X_train = pd.get_dummies(X_train, columns=['Embarked'], prefix='Emb')X_test = pd.get_dummies(X_test, columns=['Embarked'], prefix='Emb')# 3. 特征缩放(对数值特征标准化)num_cols = ['Age', 'SibSp', 'Parch', 'Fare']scaler = StandardScaler()X_train[num_cols] = scaler.fit_transform(X_train[num_cols])X_test[num_cols] = scaler.transform(X_test[num_cols])# 4. 可选:特征构造(如家庭大小)X_train['FamilySize'] = X_train['SibSp'] + X_train['Parch'] + 1X_test['FamilySize'] = X_test['SibSp'] + X_test['Parch'] + 1# 查看预处理后的数据print(X_train.head())# 现在可以用X_train, y_train训练模型了
七、总结与注意事项
数据预处理没有固定的公式,需要根据数据和模型灵活调整。以下是几点提醒:
先理解数据,再动手预处理:可视化、描述性统计能帮你发现很多问题。
避免数据泄露:所有从训练集计算得到的参数(均值、缩放因子等)都要应用到测试集上,但不能让测试集参与fit。
保存预处理参数:对于部署模型,你需要保存训练时的scaler、imputer等对象,以便对新数据做相同处理。
尝试多种方法:不同的预处理组合可能会带来模型性能的差异,可以用交叉验证来评估。
注意模型对数据的假设:树模型不需要特征缩放,线性模型需要;KNN对尺度敏感等。
掌握数据预处理,你就已经走在了正确的机器学习道路上。希望这篇攻略能帮你少踩坑,多出成果!如果你有独到的预处理技巧,欢迎在评论区分享交流~
代码部分可直接复制运行,但建议根据实际数据调整参数。如果你喜欢这篇文章,记得点赞、在看、分享三连哦!