4.2 数据准备与预处理 第四章:LightGBM 实战应用领域 - 4.2 数据准备与预处理详解 4.2.1 数据准备的重要性 数据准备阶段的目标是将原始数据转化为适合 LightGBM 模型训练的格式。这通常包括以下几个关键方面: 数据理解: 深入了解数据的来源、含义、特征类型、数据分布等,这有助于我们选择合适的预处理方法。 数据质量评估: 识别数据中的缺失值、异常值、重复值等问题,并制定相应的处理策略。 数据转换: 将数据转换成 LightGBM 可以有效处理的格式,例如数值型编码、特征缩放等。 特征工程: 基于业务理解和数据分析,创造新的特征,提升模型的预测能力。 数据集划分: 合理划分训练集、验证集和测试集,保证模型评估的可靠性和泛化能力。
数据准备阶段的目标是将原始数据转化为适合 LightGBM 模型训练的格式。这通常包括以下几个关键方面:
数据理解: 深入了解数据的来源、含义、特征类型、数据分布等,这有助于我们选择合适的预处理方法。
数据质量评估: 识别数据中的缺失值、异常值、重复值等问题,并制定相应的处理策略。
数据转换: 将数据转换成 LightGBM 可以有效处理的格式,例如数值型编码、特征缩放等。
特征工程: 基于业务理解和数据分析,创造新的特征,提升模型的预测能力。
数据集划分: 合理划分训练集、验证集和测试集,保证模型评估的可靠性和泛化能力。
一个完善的数据准备流程可以显著提升 LightGBM 模型的性能,具体体现在:
提高模型精度: 清洗后的数据可以减少噪声干扰,特征工程可以挖掘数据中的深层信息,从而提升模型的预测准确率。
加快模型训练速度: 预处理后的数据可以减少数据维度,降低模型复杂度,从而加快训练速度。
增强模型鲁棒性: 处理异常值和缺失值可以提高模型的鲁棒性,使其在面对新数据时表现更稳定。
提升模型可解释性: 清晰的数据和特征可以帮助我们更好地理解模型的决策过程,提升模型的可解释性。
数据预处理是一个迭代的过程,需要根据具体的数据情况和模型需求进行调整。以下是 LightGBM 实战应用中常用的数据预处理步骤,我们将逐一进行详细讲解并提供代码示例。
1. 数据清洗 (Data Cleaning)
数据清洗是预处理的第一步,旨在处理数据中的噪声和错误,提高数据质量。常见的清洗操作包括:
缺失值是实际数据中常见的问题,可能由于数据采集错误、信息遗漏等原因造成。处理缺失值的方法有很多,常见的包括:
* **删除缺失值:** 如果缺失值比例较小,且缺失值是随机分布的,可以考虑直接删除包含缺失值的样本或特征列。 * **填充缺失值:** 使用统计量(如均值、中位数、众数)或模型预测值填充缺失值。对于类别型特征,可以使用众数或新类别填充。 * **不处理:** 对于树模型(如 LightGBM),在某些情况下,可以不显式处理缺失值,模型本身可以学习如何处理缺失值。
代码实践 (Python - Pandas, Scikit-learn):
import pandas as pd from sklearn.impute import SimpleImputer # 示例数据 data = {'feature1': [1, 2, None, 4, 5], 'feature2': ['A', 'B', 'A', None, 'C'], 'target': [0, 1, 0, 1, 0]} df = pd.DataFrame(data) print("原始数据:\n", df) # 缺失值统计 print("\n缺失值统计:\n", df.isnull().sum()) # 1. 删除缺失值 (样本) df_dropna = df.dropna(axis=0) # 删除包含缺失值的行 print("\n删除缺失值后的数据 (样本):\n", df_dropna) # 2. 填充缺失值 (均值/中位数/众数) - 数值型特征 imputer_mean = SimpleImputer(strategy='mean') # 均值填充 df['feature1_mean_imputed'] = imputer_mean.fit_transform(df[['feature1']]) print("\n均值填充后的数据 (数值型特征):\n", df) imputer_mode = SimpleImputer(strategy='most_frequent') # 众数填充 df['feature2_mode_imputed'] = imputer_mode.fit_transform(df[['feature2']]) print("\n众数填充后的数据 (类别型特征):\n", df) # 3. 填充缺失值 (常量) df['feature2_constant_imputed'] = df['feature2'].fillna('Unknown') # 用 'Unknown' 填充类别型缺失值 print("\n常量填充后的数据 (类别型特征):\n", df)
代码详解:
df.isnull().sum(): 统计每列的缺失值数量。
df.dropna(axis=0): 删除包含缺失值的行 (样本)。axis=1 则删除包含缺失值的列 (特征)。
SimpleImputer(strategy='mean'): Scikit-learn 提供的缺失值填充器,strategy 参数可以设置为 'mean' (均值), 'median' (中位数), 'most_frequent' (众数), 'constant' (常量)。
imputer.fit_transform(df[['feature1']]): 先用 fit 方法学习填充策略(例如计算均值),然后用 transform 方法进行填充。fit_transform 将这两个步骤合并。
df['feature2'].fillna('Unknown'): Pandas 提供的 fillna 方法,可以直接用指定值填充缺失值。
异常值处理 (Handling Outliers)
异常值是指与其他观测值明显不同的数据点,可能是数据采集错误、实验误差或真实存在的极端情况。异常值会影响模型的训练,降低模型的泛化能力。常见的异常值处理方法包括:
* **删除异常值:** 如果异常值是明显的错误数据,可以考虑直接删除。 * **替换异常值:** 用合理的数值(如均值、中位数、边界值)替换异常值。 * **缩尾处理 (Winsorization):** 将超出一定范围的异常值替换为边界值,例如将 99% 分位数以上的值替换为 99% 分位数。 * **不处理:** 对于树模型,在某些情况下,异常值对模型的影响较小,可以不处理。
代码实践 (Python - Pandas, NumPy):
import pandas as pd import numpy as np # 示例数据 data = {'feature': [10, 20, 15, 12, 100, 18, 22, -50, 16, 25]} df_outlier = pd.DataFrame(data) print("原始数据 (含异常值):\n", df_outlier) # 1. 基于 IQR (四分位距) 的异常值检测和删除 Q1 = df_outlier['feature'].quantile(0.25) Q3 = df_outlier['feature'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR df_no_outlier_iqr = df_outlier[(df_outlier['feature'] >= lower_bound) & (df_outlier['feature'] <= upper_bound)] print("\n基于 IQR 删除异常值后的数据:\n", df_no_outlier_iqr) # 2. 缩尾处理 (Winsorization) lower_percentile = 0.05 upper_percentile = 0.95 lower_threshold = df_outlier['feature'].quantile(lower_percentile) upper_threshold = df_outlier['feature'].quantile(upper_percentile) df_winsorized = df_outlier['feature'].clip(lower=lower_threshold, upper=upper_threshold) print("\n缩尾处理后的数据:\n", df_winsorized) # 3. 替换异常值 (例如用均值) - 这里简单演示替换大于 50 的值 df_replaced_outlier = df_outlier['feature'].mask(df_outlier['feature'] > 50, df_outlier['feature'].mean()) print("\n替换异常值后的数据 (大于 50 的值替换为均值):\n", df_replaced_outlier)
代码详解:
df_outlier['feature'].quantile(0.25): 计算特征列的 25% 分位数 (Q1)。
IQR = Q3 - Q1: 计算四分位距。
lower_bound, upper_bound: 基于 IQR 定义的异常值边界。
df_outlier[(df_outlier['feature'] >= lower_bound) & (df_outlier['feature'] <= upper_bound)]: 筛选出在正常范围内的样本,删除异常值。
df_outlier['feature'].clip(lower=lower_threshold, upper=upper_threshold): 使用 clip 方法进行缩尾处理,将值限制在指定范围内。
df_outlier['feature'].mask(df_outlier['feature'] > 50, df_outlier['feature'].mean()): 使用 mask 方法,将满足条件 (大于 50) 的值替换为均值。
重复值处理 (Handling Duplicates)
重复值是指完全相同的样本记录,可能是数据采集或合并过程中产生的。重复值会影响模型的训练,导致模型过拟合。处理重复值的方法通常是:
* **删除重复值:** 保留第一个或最后一个重复样本,删除其余重复样本。
代码实践 (Python - Pandas):
import pandas as pd # 示例数据 data = {'feature1': ['A', 'B', 'A', 'A', 'C'], 'feature2': [1, 2, 1, 1, 3], 'target': [0, 1, 0, 0, 1]} df_duplicate = pd.DataFrame(data) print("原始数据 (含重复值):\n", df_duplicate) # 检测重复值 print("\n重复值检测 (所有列完全相同):", df_duplicate.duplicated()) print("\n重复值检测 (指定列 'feature1', 'feature2'):", df_duplicate.duplicated(subset=['feature1', 'feature2'])) # 删除重复值 df_no_duplicate = df_duplicate.drop_duplicates() # 删除所有列完全相同的重复行,保留第一个 print("\n删除重复值后的数据 (所有列):\n", df_no_duplicate) df_no_duplicate_subset = df_duplicate.drop_duplicates(subset=['feature1', 'feature2'], keep='last') # 基于指定列删除重复行,保留最后一个 print("\n基于指定列删除重复值后的数据:\n", df_no_duplicate_subset)
代码详解:
df_duplicate.duplicated(): 检测所有列完全相同的重复行,返回布尔 Series。
df_duplicate.duplicated(subset=['feature1', 'feature2']): 检测指定列 ('feature1', 'feature2') 组合相同的重复行。
df_duplicate.drop_duplicates(): 删除所有列完全相同的重复行,默认保留第一个出现的行。
df_duplicate.drop_duplicates(subset=['feature1', 'feature2'], keep='last'): 基于指定列删除重复行,keep='last' 表示保留最后一个出现的行,keep='first' (默认) 表示保留第一个。
2. 特征工程 (Feature Engineering)
特征工程是预处理的核心环节,旨在基于原始数据创建新的、更有意义的特征,提升模型的预测能力。特征工程的方法非常多样,需要结合业务理解和数据分析,常见的包括:
特征构造 (Feature Creation): 基于现有特征进行组合、变换、聚合等操作,生成新的特征。例如:
多项式特征: 将特征进行平方、立方等操作,捕捉特征之间的非线性关系。
交叉特征: 将不同特征进行组合(例如相乘、相除),捕捉特征之间的交互作用。
聚合特征: 对分组数据进行统计聚合(例如求和、均值、最大值、最小值),提取统计信息。
时间特征: 从日期时间类型数据中提取年、月、日、小时、星期几等特征。
特征选择 (Feature Selection): 从众多特征中选择出对模型预测最有用的特征,降低特征维度,提高模型效率和泛化能力。例如:
过滤式 (Filter Methods): 基于特征的统计指标(如方差、相关系数)进行选择,与模型无关。
包裹式 (Wrapper Methods): 将特征子集的选择看作搜索问题,使用模型性能作为评价指标,例如递归特征消除 (RFE)。
嵌入式 (Embedded Methods): 将特征选择融入模型训练过程中,例如 L1 正则化 (Lasso)、树模型的特征重要性。
代码实践 (Python - Pandas, Scikit-learn):
import pandas as pd from sklearn.preprocessing import PolynomialFeatures, StandardScaler from sklearn.feature_selection import SelectKBest, f_regression # 示例数据 data_fe = {'feature1': [1, 2, 3, 4, 5], 'feature2': [2, 4, 5, 4, 6], 'timestamp': ['2023-10-26 10:00:00', '2023-10-26 11:00:00', '2023-10-27 09:00:00', '2023-10-27 14:00:00', '2023-10-28 12:00:00'], 'target': [0, 1, 0, 1, 0]} df_fe = pd.DataFrame(data_fe) # 1. 特征构造 - 多项式特征 poly = PolynomialFeatures(degree=2, include_bias=False) # 生成 2 次多项式特征,不包含 bias 列 poly_features = poly.fit_transform(df_fe[['feature1', 'feature2']]) poly_feature_names = poly.get_feature_names_out(['feature1', 'feature2']) df_poly_features = pd.DataFrame(poly_features, columns=poly_feature_names) print("\n多项式特征:\n", df_poly_features) # 2. 特征构造 - 时间特征 df_fe['timestamp'] = pd.to_datetime(df_fe['timestamp']) df_fe['hour'] = df_fe['timestamp'].dt.hour df_fe['dayofweek'] = df_fe['timestamp'].dt.dayofweek # 0: Monday, 6: Sunday print("\n时间特征:\n", df_fe[['timestamp', 'hour', 'dayofweek']]) # 3. 特征选择 - 基于方差过滤 (示例,实际应用中需要结合业务和模型选择更合适的特征选择方法) from sklearn.feature_selection import VarianceThreshold selector = VarianceThreshold(threshold=0.5) # 方差阈值设置为 0.5 selector.fit(df_poly_features) selected_features_variance = df_poly_features.columns[selector.get_support()] print("\n基于方差过滤选择的特征:\n", selected_features_variance) # 4. 特征选择 - 基于单变量特征选择 (示例,使用 f_regression 评估回归任务的特征相关性) X = df_poly_features y = df_fe['target'] selector_f_regression = SelectKBest(score_func=f_regression, k=3) # 选择 top 3 特征 selector_f_regression.fit(X, y) selected_features_f_regression = X.columns[selector_f_regression.get_support()] print("\n基于单变量特征选择 (f_regression) 选择的特征:\n", selected_features_f_regression)
代码详解:
PolynomialFeatures(degree=2, include_bias=False): Scikit-learn 提供的多项式特征生成器,degree 参数指定多项式次数,include_bias=False 移除 bias 列 (常数项)。
poly.fit_transform(df_fe[['feature1', 'feature2']]): 生成多项式特征。
poly.get_feature_names_out(['feature1', 'feature2']): 获取生成的多项式特征的名称。
pd.to_datetime(df_fe['timestamp']): 将字符串时间戳转换为 Pandas datetime 类型。
df_fe['timestamp'].dt.hour, df_fe['timestamp'].dt.dayofweek: 从 datetime 类型数据中提取小时和星期几特征。
VarianceThreshold(threshold=0.5): Scikit-learn 提供的方差过滤特征选择器,移除方差低于阈值的特征。
selector.fit(df_poly_features): 训练方差过滤选择器。
selector.get_support(): 返回布尔数组,指示哪些特征被选择。
SelectKBest(score_func=f_regression, k=3): Scikit-learn 提供的单变量特征选择器,score_func 参数指定评分函数 (这里使用 f_regression 用于回归任务的特征选择),k 参数指定选择的特征数量。
selector_f_regression.fit(X, y): 训练单变量特征选择器。
3. 特征编码 (Feature Encoding)
对于类别型特征,LightGBM 虽然可以直接处理类别特征,但在某些情况下,将其转换为数值型编码仍然可以提升模型性能或满足特定需求。常见的类别型特征编码方法包括:
标签编码 (Label Encoding): 将类别标签转换为连续的整数,例如将 ['A', 'B', 'C', 'A'] 编码为 [0, 1, 2, 0]。适用于有序类别特征。
独热编码 (One-Hot Encoding): 为每个类别创建一个新的二元特征,例如将 ['A', 'B', 'C', 'A'] 编码为:
A: [1, 0, 0, 1] B: [0, 1, 0, 0] C: [0, 0, 1, 0]
适用于无序类别特征,可以避免模型误解类别之间的顺序关系。
序数编码 (Ordinal Encoding): 类似于标签编码,但可以自定义类别标签的顺序,适用于有序类别特征。
频率编码 (Frequency Encoding): 用类别的频率或占比替换类别标签,适用于类别数量较多的特征。
目标编码 (Target Encoding): 用类别的目标变量统计量(如均值、中位数)替换类别标签,适用于高基数类别特征,但容易导致过拟合,需要谨慎使用。
代码实践 (Python - Pandas, Scikit-learn):
import pandas as pd from sklearn.preprocessing import LabelEncoder, OneHotEncoder, OrdinalEncoder # 示例数据 data_encode = {'category_feature': ['A', 'B', 'C', 'A', 'B', 'D'], 'ordered_feature': ['Low', 'Medium', 'High', 'Medium', 'Low', 'High'], 'target': [0, 1, 0, 1, 0, 1]} df_encode = pd.DataFrame(data_encode) print("原始数据 (类别型特征):\n", df_encode) # 1. 标签编码 (Label Encoding) label_encoder = LabelEncoder() df_encode['category_encoded'] = label_encoder.fit_transform(df_encode['category_feature']) print("\n标签编码后的数据 (无序类别特征):\n", df_encode) # 2. 独热编码 (One-Hot Encoding) onehot_encoder = OneHotEncoder(sparse_output=False) # sparse=False 返回 dense array onehot_encoded_features = onehot_encoder.fit_transform(df_encode[['category_feature']]) onehot_feature_names = onehot_encoder.get_feature_names_out(['category_feature']) df_onehot_encoded = pd.DataFrame(onehot_encoded_features, columns=onehot_feature_names) print("\n独热编码后的数据 (无序类别特征):\n", df_onehot_encoded) # 3. 序数编码 (Ordinal Encoding) ordinal_encoder = OrdinalEncoder(categories=[['Low', 'Medium', 'High']]) # 指定类别顺序 df_encode['ordered_encoded'] = ordinal_encoder.fit_transform(df_encode[['ordered_feature']]) print("\n序数编码后的数据 (有序类别特征):\n", df_encode) # 4. Pandas get_dummies 实现独热编码 (更简洁常用) df_dummies = pd.get_dummies(df_encode, columns=['category_feature'], prefix='category') # prefix 指定前缀 print("\nPandas get_dummies 独热编码后的数据:\n", df_dummies)
代码详解:
LabelEncoder(): Scikit-learn 提供的标签编码器。
OneHotEncoder(sparse=False): Scikit-learn 提供的独热编码器,sparse=False 返回 NumPy dense array,而不是 sparse matrix。
onehot_encoder.get_feature_names_out(['category_feature']): 获取独热编码后的特征名称。
OrdinalEncoder(categories=[['Low', 'Medium', 'High']]): Scikit-learn 提供的序数编码器,categories 参数指定每个特征的类别顺序。
pd.get_dummies(df_encode, columns=['category_feature'], prefix='category'): Pandas 提供的 get_dummies 函数,可以快速进行独热编码,columns 参数指定要编码的列,prefix 参数指定编码后特征名称的前缀。
4. 特征缩放 (Feature Scaling)
特征缩放是将不同特征的值缩放到相似的范围,避免某些特征值过大或过小对模型训练产生不利影响。对于 LightGBM 这种树模型,特征缩放并非绝对必要,因为树模型对特征的尺度不敏感。然而,在以下情况下,特征缩放仍然可能是有益的:
使用基于距离的算法: 如果模型中包含基于距离的算法(例如 KNN),特征缩放是必要的,否则尺度大的特征会主导距离计算。
加速梯度下降: 对于某些梯度下降优化算法,特征缩放可以加速收敛。
提升模型稳定性: 特征缩放可以提高模型的数值稳定性,避免因特征尺度差异过大而导致梯度爆炸或消失。
特征重要性解释: 特征缩放后,特征重要性排序可能更合理。
常见的特征缩放方法包括:
标准化 (Standardization): 将特征值转换为均值为 0,标准差为 1 的分布,也称为 Z-score 标准化。
归一化 (Normalization): 将特征值缩放到 [0, 1] 或 [-1, 1] 区间,也称为 Min-Max 缩放。
RobustScaler: 基于四分位距进行缩放,对异常值更鲁棒。
代码实践 (Python - Scikit-learn):
import pandas as pd from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler # 示例数据 data_scale = {'feature1': [10, 20, 30, 40, 50], 'feature2': [100, 200, 150, 250, 300], 'target': [0, 1, 0, 1, 0]} df_scale = pd.DataFrame(data_scale) print("原始数据 (特征尺度差异大):\n", df_scale) # 1. 标准化 (StandardScaler) scaler_standard = StandardScaler() scaled_features_standard = scaler_standard.fit_transform(df_scale[['feature1', 'feature2']]) df_scaled_standard = pd.DataFrame(scaled_features_standard, columns=['feature1_scaled_standard', 'feature2_scaled_standard']) print("\n标准化后的数据:\n", df_scaled_standard) # 2. 归一化 (MinMaxScaler) scaler_minmax = MinMaxScaler() scaled_features_minmax = scaler_minmax.fit_transform(df_scale[['feature1', 'feature2']]) df_scaled_minmax = pd.DataFrame(scaled_features_minmax, columns=['feature1_scaled_minmax', 'feature2_scaled_minmax']) print("\n归一化后的数据:\n", df_scaled_minmax) # 3. RobustScaler scaler_robust = RobustScaler() scaled_features_robust = scaler_robust.fit_transform(df_scale[['feature1', 'feature2']]) df_scaled_robust = pd.DataFrame(scaled_features_robust, columns=['feature1_scaled_robust', 'feature2_scaled_robust']) print("\nRobustScaler 缩放后的数据:\n", df_scaled_robust)
代码详解:
StandardScaler(): Scikit-learn 提供的标准化缩放器。
MinMaxScaler(): Scikit-learn 提供的归一化缩放器。
RobustScaler(): Scikit-learn 提供的 RobustScaler 缩放器。
scaler.fit_transform(df_scale[['feature1', 'feature2']]): 先用 fit 方法学习缩放参数(例如均值和标准差),然后用 transform 方法进行缩放。
5. 数据集划分 (Data Splitting)
在模型训练之前,需要将数据集划分为训练集、验证集和测试集。
训练集 (Training Set): 用于模型训练,模型从训练数据中学习参数。
验证集 (Validation Set): 用于模型调参和选择,在训练过程中评估模型性能,调整超参数,防止过拟合。
测试集 (Test Set): 用于最终评估模型性能,在模型训练完成后,用测试集评估模型的泛化能力。
常见的数据集划分方法包括:
简单划分 (Train-Test Split): 将数据集随机划分为训练集和测试集,或者训练集、验证集和测试集。
交叉验证 (Cross-Validation): 将训练集划分为 K 个 folds,轮流将其中一个 fold 作为验证集,其余 folds 作为训练集,进行 K 次训练和验证,得到 K 个验证结果的平均值,更可靠地评估模型性能。常见的交叉验证方法包括 K-Fold 交叉验证、Stratified K-Fold 交叉验证 (适用于类别不平衡数据)。