在我们之前对 Lasso、Ridge 和 ElasticNet 等惩罚回归模型的研究中,我们展示了这些模型如何有效处理多重共线性,使我们能够利用更广泛的特征来增强模型性能。在此基础上,我们现在着手处理数据预处理的另一个关键方面——处理缺失值。如果处理不当,缺失数据会严重损害模型的准确性和可靠性。本文将探讨各种插补策略来处理缺失数据,并将其嵌入到我们的流程中。这种方法通过纳入先前排除的特征,使我们能够进一步提高预测精度,从而充分利用我们丰富的数据集。
通过我的书《进阶数据科学》启动您的项目。它提供了带有可运行代码的自学教程。
让我们开始吧。

填补空白:机器学习中插补技术的比较指南
照片由 lan deng 提供。部分权利保留。
概述
这篇博文分为三部分;它们是:
- 使用 SimpleImputer 重建手动插补
- 使用 IterativeImputer 推进插补技术
- 利用 KNN 插补的邻域洞察
使用 SimpleImputer 重建手动插补
在本文的第一部分,我们将使用 SimpleImputer
重温并重建我们之前的手动插补技术。我们之前对 Ames Housing 数据集的探索为 使用数据字典 处理缺失数据提供了基础性见解。我们展示了针对不同数据类型量身定制的手动插补策略,考虑了领域知识和数据字典的细节。例如,数据集中缺失的分类变量通常表示该特征的缺失(例如,缺失的“PoolQC”可能意味着没有泳池),指导我们将这些变量填充为“None”以保持数据集的完整性。同时,数值特征则通过诸如均值插补等技术进行了不同的处理。
现在,通过使用 scikit-learn 的 SimpleImputer
自动化这些过程,我们提高了可重复性和效率。我们的流程不仅包括插补,还包括特征缩放和编码,为 Lasso、Ridge 和 ElasticNet 等回归模型做准备。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# 导入必要的库 import pandas as pd from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer from sklearn.linear_model import Lasso, Ridge, ElasticNet from sklearn.model_selection import cross_val_score # 加载数据集 Ames = pd.read_csv('Ames.csv') # 从特征中排除 'PID' 和 'SalePrice',并专门处理 'Electrical' 列 numeric_features = Ames.select_dtypes(include=['int64', 'float64']).drop(columns=['PID', 'SalePrice']).columns categorical_features = Ames.select_dtypes(include=['object']).columns.difference(['Electrical']) electrical_feature = ['Electrical'] # 特别处理 'Electrical' 列 # 用于填充缺失分类数据的“None”的辅助函数 def fill_none(X): return X.fillna("None") # 数值特征的流程:先插补缺失值,再进行缩放 numeric_transformer = Pipeline(steps=[ ('impute_mean', SimpleImputer(strategy='mean')), ('scaler', StandardScaler()) ]) # 通用分类特征的流程:用“None”填充缺失值,然后应用独热编码 categorical_transformer = Pipeline(steps=[ ('fill_none', FunctionTransformer(fill_none, validate=False)), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]) # 'Electrical' 的特定转换器,使用众数进行插补 electrical_transformer = Pipeline(steps=[ ('impute_electrical', SimpleImputer(strategy='most_frequent')), ('onehot_electrical', OneHotEncoder(handle_unknown='ignore')) ]) # 结合数值、通用分类和电气数据的预处理器 preprocessor = ColumnTransformer( transformers=[ ('num', numeric_transformer, numeric_features), ('cat', categorical_transformer, categorical_features), ('electrical', electrical_transformer, electrical_feature) ]) # 目标变量 y = Ames['SalePrice'] # 所有特征 X = Ames[numeric_features.tolist() + categorical_features.tolist() + electrical_feature] # 定义带有预处理器和回归器的模型流程 models = { 'Lasso': Lasso(max_iter=20000), 'Ridge': Ridge(), 'ElasticNet': ElasticNet() } results = {} for name, model in models.items(): pipeline = Pipeline(steps=[ ('preprocessor', preprocessor), ('regressor', model) ]) # 执行交叉验证 scores = cross_val_score(pipeline, X, y) results[name] = round(scores.mean(), 4) # 输出交叉验证得分 print("使用 Simple Imputer 的交叉验证得分:", results) |
此实现的输出显示了简单插补如何影响模型准确性,并为后面讨论的更复杂的方法建立了基准。
1 |
使用 Simple Imputer 的交叉验证得分:{'Lasso': 0.9138, 'Ridge': 0.9134, 'ElasticNet': 0.8752} |
从手动步骤过渡到使用 scikit-learn 的流程方法可以增强数据处理的多个方面。
- 效率和错误减少:手动插补值耗时且容易出错,尤其是随着数据复杂性的增加。流程自动化了这些步骤,确保了一致的转换并减少了错误。
- 可重用性和集成:手动方法的可重用性较低。相比之下,流程封装了整个预处理和建模步骤,使其易于重用并无缝集成到模型训练过程中。
- 数据泄露预防:手动插补存在数据泄露的风险,因为它在计算值时可能包含测试数据。流程通过 fit/transform 方法来预防这种风险,确保计算仅来自训练集。
此框架以 SimpleImputer
为例,展示了一种灵活的数据预处理方法,可以轻松地适应各种插补策略。在接下来的部分,我们将探讨其他技术,评估它们对模型性能的影响。
想开始学习进阶数据科学吗?
立即参加我的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
使用 IterativeImputer 推进插补技术
在第二部分,我们尝试使用 IterativeImputer
,这是一种更高级的插补技术,它以循环方式将缺失值特征建模为其他特征的函数。与使用均值或中位数等通用统计数据的简单方法不同,IterativeImputer
将缺失值特征建模为回归的因变量,并以数据集中的其他特征为依据。此过程会迭代进行,利用所有可用的特征交互来优化缺失值的估计。这种方法可以揭示简单插补方法无法捕捉到的细微数据模式和依赖关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# 导入必要的库 import pandas as pd from sklearn.pipeline import Pipeline from sklearn.experimental import enable_iterative_imputer # IterativeImputer 需要此行 from sklearn.impute import SimpleImputer, IterativeImputer from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer from sklearn.linear_model import Lasso, Ridge, ElasticNet from sklearn.model_selection import cross_val_score # 加载数据集 Ames = pd.read_csv('Ames.csv') # 从特征中排除 'PID' 和 'SalePrice',并专门处理 'Electrical' 列 numeric_features = Ames.select_dtypes(include=['int64', 'float64']).drop(columns=['PID', 'SalePrice']).columns categorical_features = Ames.select_dtypes(include=['object']).columns.difference(['Electrical']) electrical_feature = ['Electrical'] # 特别处理 'Electrical' 列 # 用于填充缺失分类数据的“None”的辅助函数 def fill_none(X): return X.fillna("None") # 数值特征的流程:先迭代插补,再进行缩放 numeric_transformer_advanced = Pipeline(steps=[ ('impute_iterative', IterativeImputer(random_state=42)), ('scaler', StandardScaler()) ]) # 通用分类特征的流程:用“None”填充缺失值,然后应用独热编码 categorical_transformer = Pipeline(steps=[ ('fill_none', FunctionTransformer(fill_none, validate=False)), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]) # 'Electrical' 的特定转换器,使用众数进行插补 electrical_transformer = Pipeline(steps=[ ('impute_electrical', SimpleImputer(strategy='most_frequent')), ('onehot_electrical', OneHotEncoder(handle_unknown='ignore')) ]) # 结合数值、通用分类和电气数据的预处理器 preprocessor_advanced = ColumnTransformer( transformers=[ ('num', numeric_transformer_advanced, numeric_features), ('cat', categorical_transformer, categorical_features), ('electrical', electrical_transformer, electrical_feature) ]) # 目标变量 y = Ames['SalePrice'] # 所有特征 X = Ames[numeric_features.tolist() + categorical_features.tolist() + electrical_feature] # 定义带有预处理器和回归器的模型流程 models = { 'Lasso': Lasso(max_iter=20000), 'Ridge': Ridge(), 'ElasticNet': ElasticNet() } results_advanced = {} for name, model in models.items(): pipeline = Pipeline(steps=[ ('preprocessor', preprocessor_advanced), ('regressor', model) ]) # 执行交叉验证 scores = cross_val_score(pipeline, X, y) results_advanced[name] = round(scores.mean(), 4) # 输出高级插补的交叉验证得分 print("使用 Iterative Imputer 的交叉验证得分:", results_advanced) |
尽管 IterativeImputer
相对于 SimpleImputer
的准确性提升不大,但它们凸显了数据插补的一个重要方面:数据集中的复杂性和相互依赖性可能并不总是能带来更复杂方法带来的显著更高的得分。
1 |
使用 Iterative Imputer 的交叉验证得分:{'Lasso': 0.9142, 'Ridge': 0.9135, 'ElasticNet': 0.8746} |
这些微小的改进表明,虽然 IterativeImputer
可以提高我们模型的精度,但其影响程度可能因数据集的特征而异。在我们进入本文的第三个也是最后一个部分时,我们将探索 KNNImputer
,这是一种替代的高级技术,它利用最近邻方法,可能为处理各种数据集中的缺失数据提供不同的见解和优势。
利用 KNN 插补的邻域洞察
在本文的最后一部分,我们探索 KNNImputer
,它使用训练集中找到的 k 个最近邻的平均值来插补缺失值。此方法假定相似的数据点可以在特征空间中找到接近的点,因此对于这些假设成立的数据集非常有效。KNN 插补在数据点特征相似的可能性很高的场景中特别强大。我们检查它对相同预测模型的影响,提供了不同插补方法如何影响回归分析结果的完整谱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# 导入必要的库 import pandas as pd from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer, KNNImputer from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer from sklearn.linear_model import Lasso, Ridge, ElasticNet from sklearn.model_selection import cross_val_score # 加载数据集 Ames = pd.read_csv('Ames.csv') # 从特征中排除 'PID' 和 'SalePrice',并专门处理 'Electrical' 列 numeric_features = Ames.select_dtypes(include=['int64', 'float64']).drop(columns=['PID', 'SalePrice']).columns categorical_features = Ames.select_dtypes(include=['object']).columns.difference(['Electrical']) electrical_feature = ['Electrical'] # 特别处理 'Electrical' 列 # 用于填充缺失分类数据的“None”的辅助函数 def fill_none(X): return X.fillna("None") # 数值特征的流程:先 K-Nearest Neighbors 插补,再进行缩放 numeric_transformer_knn = Pipeline(steps=[ ('impute_knn', KNNImputer(n_neighbors=5)), ('scaler', StandardScaler()) ]) # 通用分类特征的流程:用“None”填充缺失值,然后应用独热编码 categorical_transformer = Pipeline(steps=[ ('fill_none', FunctionTransformer(fill_none, validate=False)), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]) # 'Electrical' 的特定转换器,使用众数进行插补 electrical_transformer = Pipeline(steps=[ ('impute_electrical', SimpleImputer(strategy='most_frequent')), ('onehot_electrical', OneHotEncoder(handle_unknown='ignore')) ]) # 结合数值、通用分类和电气数据的预处理器 preprocessor_knn = ColumnTransformer( transformers=[ ('num', numeric_transformer_knn, numeric_features), ('cat', categorical_transformer, categorical_features), ('electrical', electrical_transformer, electrical_feature) ]) # 目标变量 y = Ames['SalePrice'] # 所有特征 X = Ames[numeric_features.tolist() + categorical_features.tolist() + electrical_feature] # 定义带有预处理器和回归器的模型流程 models = { 'Lasso': Lasso(max_iter=20000), 'Ridge': Ridge(), 'ElasticNet': ElasticNet() } results_knn = {} for name, model in models.items(): pipeline = Pipeline(steps=[ ('preprocessor', preprocessor_knn), ('regressor', model) ]) # 执行交叉验证 scores = cross_val_score(pipeline, X, y) results_knn[name] = round(scores.mean(), 4) # 输出 KNN 插补的交叉验证得分 print("使用 KNN Imputer 的交叉验证得分:", results_knn) |
使用 KNNImputer
的交叉验证结果显示,与 SimpleImputer
和 IterativeImputer
相比,准确性有非常小的提升:
1 |
使用 - KNN Imputer 的 交叉验证 得分: {'Lasso': 0.9146, 'Ridge': 0.9138, 'ElasticNet': 0.8748} |
这种细微的增强表明,对于某些数据集,KNNImputer
的基于邻近的方法——它考虑了数据点之间的相似性——在捕获和保留数据结构方面可能更有效,从而可能带来更准确的预测。
进一步阅读
API
教程
资源
总结
本文指导您从手动到自动化插补技术的演变过程,首先通过 SimpleImputer
复制基本的手动插补以建立基准。然后,我们使用 IterativeImputer
探索了更复杂的策略,该策略将缺失值特征建模为依赖于其他特征,并最后使用 KNNImputer
,利用数据点的邻近性来填充缺失值。有趣的是,在我们的案例中,这些复杂技术并没有比基本方法有大的改进。这表明,虽然可以使用高级插补方法来处理缺失数据,但其有效性可能因所涉及的数据集的特定特征和结构而异。
具体来说,你学到了:
- 如何使用
SimpleImputer
复制和自动化手动插补过程。 IterativeImputer
的复杂性如何不总是能证明其预测性能的提高是合理的。KNNImputer
如何展示利用数据结构进行插补的潜力,尽管在我们的数据集中它也仅显示出适度的改进。
您有任何问题吗?请在下面的评论中提出您的问题,我将尽力回答。
暂无评论。