查看全课大纲(第 9 / 35 节)
- 1.特征工程简介
- 2.特征工程的处理流程
- 3.数据与特征概述
- 4.数据统计、相似度与数据探索
- 5.数据探索工具 Pandas_profiling
- 6.工具应用:《泰坦尼克》分析报告
- 7.构造一个微型链家租房数据集用于演示
- 8.数据预处理综述
- 9.缺失值处理实操
- 10.异常值处理实操
- 11.连续值特征构建
- 12.类别型数据特征构建
- 13.特征幅度缩放
- 14.特征离散化
- 15.特征编码:让类别数据参与建模
- 16.特征变换
- 17.时间特征
- 18.特征衍生
- 19.自动化特征工具
- 20.特征选择综述
- 21.特征选择方法
- 22.过滤式特征选择方法实操
- 23.包裹式特征选择方法
- 24.嵌入式特征选择
- 25.混序法与混合法特征选择
- 26.特征降维综述
- 27.案例实操:特征降维
- 28.特征监控概述与指标
- 29.Iris 数据集特征漂移检测案例
- 30.特征监控实战:用分布对比检测数据漂移
- 31.实时特征监控
- 32.电商销量预测项目背景介绍
- 33.数据预处理和特征工程(一)
- 34.数据预处理和特征工程(二)
- 35.数据建模
缺失值处理实操
约 10 分钟
小象实战讲义 · 特征工程精讲与实践
现实数据往往存在缺失,直接建模会引入偏差甚至导致模型崩溃。本节将系统学习缺失值处理的常规方法与高级模型方法,通过泰坦尼克号数据集实战,掌握工业级特征工程的预处理基本功。
💡 核心导读
- 缺失值识别:使用 pandas 工具快速定位缺失字段与比例。
- 常规处理六法:删除、标记、常量填充、统计量填充、边界值填充、随机样本填充。
- 高级模型填充:基于 K 最近邻(KNN)与随机森林(MissForest)的智能插补。
- 工程化封装:将常用方法封装为可复用的工具函数,提升特征工程效率。
一、缺失值识别与数据准备
在开始处理之前,我们首先要识别数据中的缺失情况。pandas 提供了便捷的工具,我们也可以将常用检查逻辑封装起来。
import pandas as pd
import numpy as np
# 构造一个包含缺失值的微型泰坦尼克号数据集样例
# 注意:这是一个可手算验证的极小数据集,便于理解原理
data = pd.DataFrame({
'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8],
'Survived': [0, 1, 1, 1, 0, 0, 0, 1],
'Pclass': [3, 1, 3, 1, 3, 3, 1, 3],
'Name': ['Braund', 'Cumings', 'Heikkinen', 'Futrelle', 'Allen', 'Moran', 'McCarthy', 'Palsson'],
'Sex': ['male', 'female', 'female', 'female', 'male', 'male', 'female', 'female'],
'Age': [22.0, 38.0, 26.0, 35.0, None, None, 54.0, 2.0], # 第5、6行年龄缺失
'SibSp': [1, 1, 0, 1, 0, 0, 0, 3],
'Parch': [0, 0, 0, 0, 0, 0, 0, 1],
'Ticket': ['A/5 21171', 'PC 17599', 'STON/O2. 3101282', '113803', '373450', '330877', '17463', '349909'],
'Fare': [7.25, 71.28, 7.93, 53.10, 8.05, 8.46, 51.86, 21.08],
'Cabin': [None, 'C85', None, 'C123', None, None, 'E46', None], # 多个舱位缺失
'Embarked': ['S', 'C', 'S', 'S', 'S', 'Q', 'S', 'S']
})
print("原始数据形状:", data.shape)
print("\n各字段缺失情况统计:")
print(data.isnull().sum())
print("\n各字段缺失比例:")
print((data.isnull().sum() / len(data)).round(4))原始数据形状: (8, 12)
各字段缺失情况统计:
PassengerId 0
Survived 0
Pclass 0
Name 0
Sex 0
Age 2
SibSp 0
Parch 0
Ticket 0
Fare 0
Cabin 5
Embarked 0
dtype: int64
各字段缺失比例:
PassengerId 0.000
Survived 0.000
Pclass 0.000
Name 0.000
Sex 0.000
Age 0.250
SibSp 0.000
Parch 0.000
Ticket 0.000
Fare 0.000
Cabin 0.625
Embarked 0.000
dtype: float64可以看到,Age 字段有 2 个缺失值(占比 25%),Cabin 字段有 5 个缺失值(占比 62.5%)。在实际的泰坦尼克号全量数据中,年龄缺失约 20%,舱位缺失更多。
二、常规缺失值处理方法
1. 删除法
最简单直接的方法是删除含有缺失值的记录或字段。但会损失信息,适用于缺失比例极低或字段重要性不高的情况。
import pandas as pd
import numpy as np
# 自包含数据定义
data_drop = pd.DataFrame({
'Age': [22.0, 38.0, 26.0, 35.0, None, None, 54.0, 2.0],
'Fare': [7.25, 71.28, 7.93, 53.10, 8.05, 8.46, 51.86, 21.08],
'Cabin': [None, 'C85', None, 'C123', None, None, 'E46', None]
})
print("原始数据:")
print(data_drop)
print("\n1. 删除含有缺失值的行(默认axis=0):")
data_drop_rows = data_drop.dropna(axis=0)
print("删除缺失行后的数据形状:", data_drop_rows.shape)
print(data_drop_rows)
print("\n2. 删除含有缺失值的列:")
data_drop_cols = data_drop.dropna(axis=1)
print("删除缺失列后的数据形状:", data_drop_cols.shape)
print("保留的列:", data_drop_cols.columns.tolist())原始数据:
Age Fare Cabin
0 22.0 7.25 NaN
1 38.0 71.28 C85
2 26.0 7.93 NaN
3 35.0 53.10 C123
4 NaN 8.05 NaN
5 NaN 8.46 NaN
6 54.0 51.86 E46
7 2.0 21.08 NaN
1. 删除含有缺失值的行(默认axis=0):
删除缺失行后的数据形状: (3, 3)
Age Fare Cabin
1 38.0 71.28 C85
3 35.0 53.10 C123
6 54.0 51.86 E46
2. 删除含有缺失值的列:
删除缺失列后的数据形状: (8, 1)
保留的列: ['Fare']2. 标记法
构建新字段标识缺失,将缺失本身作为一种信息。这对于某些模型(如树模型)可能是有用的特征。
import pandas as pd
import numpy as np
# 自包含数据与函数定义
def add_var_denote_NA(data, NA_col):
"""
构建一个新的字段用于标识该样本在指定列上是否包含缺失值
1表示有缺失,0表示无缺失
"""
data_copy = data.copy()
for col in NA_col:
if data_copy[col].isnull().sum() > 0:
data_copy[col + '_is_NA'] = np.where(data_copy[col].isnull(), 1, 0)
return data_copy
# 示例数据
data_mark = pd.DataFrame({
'Age': [22.0, 38.0, 26.0, 35.0, None, None, 54.0, 2.0],
'Cabin': [None, 'C85', None, 'C123', None, None, 'E46', None]
})
print("原始数据:")
print(data_mark)
# 标记 Age 字段的缺失情况
data_marked = add_var_denote_NA(data_mark, ['Age'])
print("\n标记 Age 缺失后的数据:")
print(data_marked[['Age', 'Age_is_NA']])原始数据:
Age Cabin
0 22.0 NaN
1 38.0 C85
2 26.0 NaN
3 35.0 C123
4 NaN NaN
5 NaN NaN
6 54.0 E46
7 2.0 NaN
标记 Age 缺失后的数据:
Age Age_is_NA
0 22.0 0
1 38.0 0
2 26.0 0
3 35.0 0
4 NaN 1
5 NaN 1
6 54.0 0
7 2.0 03. 填充法
用特定值替换缺失值,是最常用的处理方法。下面展示几种典型填充策略。
import pandas as pd
import numpy as np
# 自包含数据与函数定义
def impute_NA_with_avg(data, strategy='mean', NA_col=[]):
"""
用统计量(均值/中位数/众数)填充缺失值
"""
data_copy = data.copy()
for col in NA_col:
if data_copy[col].isnull().sum() > 0:
if strategy == 'mean':
fill_value = data[col].mean()
new_col_name = col + '_impute_mean'
elif strategy == 'median':
fill_value = data[col].median()
new_col_name = col + '_impute_median'
elif strategy == 'mode':
fill_value = data[col].mode()[0] if not data[col].mode().empty else np.nan
new_col_name = col + '_impute_mode'
data_copy[new_col_name] = data_copy[col].fillna(fill_value)
return data_copy
# 示例数据
data_fill = pd.DataFrame({
'Age': [22.0, 38.0, 26.0, 35.0, None, None, 54.0, 2.0],
'Embarked': ['S', 'C', 'S', 'S', 'S', 'Q', 'S', 'S']
})
print("原始 Age 数据:")
print(data_fill['Age'].tolist())
print("\nAge 字段统计量:")
print(f"均值:{data_fill['Age'].mean():.2f}")
print(f"中位数:{data_fill['Age'].median():.2f}")
print(f"众数:{data_fill['Embarked'].mode()[0]}") # 用Embarked演示类别众数
# 均值填充
data_mean_fill = impute_NA_with_avg(data_fill, strategy='mean', NA_col=['Age'])
print("\n均值填充结果(Age_impute_mean):")
print(data_mean_fill['Age_impute_mean'].tolist())
# 中位数填充
data_median_fill = impute_NA_with_avg(data_fill, strategy='median', NA_col=['Age'])
print("\n中位数填充结果(Age_impute_median):")
print(data_median_fill['Age_impute_median'].tolist())
# 常量填充(特殊值,如-999)
data_const_fill = data_fill.copy()
data_const_fill['Age_impute_const'] = data_const_fill['Age'].fillna(-999)
print("\n常量填充结果(-999):")
print(data_const_fill['Age_impute_const'].tolist())原始 Age 数据:
[22.0, 38.0, 26.0, 35.0, nan, nan, 54.0, 2.0]
Age 字段统计量:
均值:29.50
中位数:30.50
众数:S
均值填充结果(Age_impute_mean):
[22.0, 38.0, 26.0, 35.0, 29.5, 29.5, 54.0, 2.0]
中位数填充结果(Age_impute_median):
[22.0, 38.0, 26.0, 35.0, 30.5, 30.5, 54.0, 2.0]
常量填充结果(-999):
[22.0, 38.0, 26.0, 35.0, -999.0, -999.0, 54.0, 2.0]其他填充方法如边界值填充(均值±3倍标准差)、随机样本填充等,原理类似。在工业实践中,中位数填充对异常值不敏感,更为稳健;对于类别特征,众数填充是常见选择。
三、高级缺失值处理方法
当数据缺失不是完全随机,且与其他特征存在关系时,可以使用模型进行智能插补。这里介绍两种基于 scikit-learn API 的模型方法。
1. K最近邻(KNN)插补
KNNImputer 通过寻找样本的 k 个最近邻,用这些邻居的特征均值或加权均值来填充缺失值。(原课使用的是 missingpy 库的 KNNImputer,此处采用 scikit-learn 内置的同名实现,API 一致。)
import pandas as pd
import numpy as np
from sklearn.impute import KNNImputer
# 构建一个适合KNN插补的微型数值数据集
# 包含多个特征,便于计算样本间距离
data_knn = pd.DataFrame({
'Pclass': [3, 1, 3, 1, 3, 2, 1, 3],
'Age': [22.0, 38.0, 26.0, 35.0, None, 54.0, 2.0, 27.0], # 第5行缺失
'Fare': [7.25, 71.28, 7.93, 53.10, 8.05, 51.86, 21.08, 11.13],
'SibSp': [1, 1, 0, 1, 0, 0, 1, 0]
})
print("KNN填充前数据:")
print(data_knn)
# 初始化KNN插补器,使用3个最近邻
imputer = KNNImputer(n_neighbors=3, weights='uniform')
# fit_transform 会同时拟合和转换
data_knn_imputed = imputer.fit_transform(data_knn)
print("\nKNN填充后数据(数组形式):")
print(data_knn_imputed)
print("\n第5行(索引4)Age的填充值:", data_knn_imputed[4, 1])KNN填充前数据:
Pclass Age Fare SibSp
0 3 22.0 7.25 1
1 1 38.0 71.28 1
2 3 26.0 7.93 0
3 1 35.0 53.10 1
4 3 NaN 8.05 0
5 2 54.0 51.86 0
6 1 2.0 21.08 1
7 3 27.0 11.13 0
KNN填充后数据(数组形式):
[[ 3. 22. 7.25 1. ]
[ 1. 38. 71.28 1. ]
[ 3. 26. 7.93 0. ]
[ 1. 35. 53.1 1. ]
[ 3. 25. 8.05 0. ]
[ 2. 54. 51.86 0. ]
[ 1. 2. 21.08 1. ]
[ 3. 27. 11.13 0. ]]
第5行(索引4)Age的填充值: 25.0KNNImputer 基于 Pclass、Fare、SibSp 这三个非缺失维度计算样本间的距离,为第5行样本找到了3个最近邻(第0、2、7行样本),然后用这3个邻居的年龄均值((22 + 26 + 27) / 3 = 25.0)完成了填充。
2. 随机森林插补(MissForest)
MissForest 是一个基于随机森林的迭代插补方法,可以处理数值和类别特征,通常比 KNN 更强大。
import pandas as pd
import numpy as np
# 注意:原课使用的 missingpy.MissForest 不在白名单库中
# 此处我们讲解其原理,并用一个简化的迭代思想示例说明
print("MissForest(随机森林插补)原理说明:")
print("1. 首先用均值/众数等简单方法初始化所有缺失值。")
print("2. 对于每个有缺失的字段,将其作为目标变量,其他字段作为特征,训练一个随机森林模型。")
print("3. 用训练好的模型预测该字段的缺失值,并更新填充值。")
print("4. 重复步骤2-3:当相邻两轮插补差异首次增大(delta_new > delta_old)即停止,")
print(" 并以最大迭代次数兜底(这是MissForest原文的停止判据)。")
print("\n优势:")
print("- 可以捕捉特征间的非线性关系")
print("- 同时处理数值和类别变量")
print("- 对数据分布假设较少")
print("\n注:原课使用 missingpy 库的 MissForest 实现,")
print("其API与scikit-learn一致,使用 fit_transform 方法即可完成插补。")MissForest(随机森林插补)原理说明:
1. 首先用均值/众数等简单方法初始化所有缺失值。
2. 对于每个有缺失的字段,将其作为目标变量,其他字段作为特征,训练一个随机森林模型。
3. 用训练好的模型预测该字段的缺失值,并更新填充值。
4. 重复步骤2-3:当相邻两轮插补差异首次增大(delta_new > delta_old)即停止,
并以最大迭代次数兜底(这是MissForest原文的停止判据)。
优势:
- 可以捕捉特征间的非线性关系
- 同时处理数值和类别变量
- 对数据分布假设较少
注:原课使用 missingpy 库的 MissForest 实现,
其API与scikit-learn一致,使用 fit_transform 方法即可完成插补。四、工程实践与选择建议
在实际项目中,缺失值处理需要综合考虑:
- 缺失机制分析:是完全随机缺失(MCAR)、随机缺失(MAR)还是非随机缺失(MNAR)?这会影响方法选择。
- 缺失比例:低于5%可考虑删除;5%-20%需认真填充;高于20%可能要谨慎使用该特征。
- 特征类型:数值特征常用中位数/KNN填充;类别特征常用众数/新类别填充。
- 模型影响:树模型对缺失相对稳健,可考虑标记法;线性模型、神经网络通常需要完整数据。
- 数据划分:所有统计量(均值、中位数等)都必须在训练集上计算,然后用于填充训练集和测试集,避免数据泄漏。
📝 动手练一练
- 概念辨析:在泰坦尼克号数据中,
Cabin字段缺失率超过60%。如果直接删除所有包含Cabin缺失的记录,可能会引入什么偏差?除了删除,还有哪些更合适的处理思路?
👉 点击查看参考答案
潜在偏差:舱位信息缺失可能与乘客的社会经济地位相关(例如三等舱乘客更可能没有记录舱位)。直接删除这些记录会导致样本向高等级舱位乘客倾斜,使模型低估低等级舱位乘客的生存率,影响泛化能力。
其他处理思路:
- 将缺失本身作为信息:创建
Cabin_is_NA标记特征。 - 填充为特殊值:如
'Unknown',作为一个新的舱位类别。 - 利用相关特征推断:例如根据
Pclass(客舱等级)和Fare(票价)分组,用该组的众数填充。 - 对于预测任务,如果
Cabin缺失率过高且难以可靠填充,可以考虑暂时放弃该特征,优先使用其他特征。
- 代码实战:使用上面学到的知识,对以下微型数据集中的
Income字段进行填充。尝试至少两种不同的填充方法(如中位数填充和KNN填充),并比较填充后的数据分布。
import pandas as pd
import numpy as np
from sklearn.impute import KNNImputer
data_exercise = pd.DataFrame({
'Age': [25, 30, 35, 40, 45, 50, 55, 60],
'Education_Years': [12, 16, 18, 16, 12, 20, 18, 16],
'Income': [30000, 50000, 75000, None, 40000, 90000, None, 80000] # 两个缺失
})👉 点击查看参考答案
import pandas as pd
import numpy as np
from sklearn.impute import KNNImputer
# 数据准备
data_exercise = pd.DataFrame({
'Age': [25, 30, 35, 40, 45, 50, 55, 60],
'Education_Years': [12, 16, 18, 16, 12, 20, 18, 16],
'Income': [30000, 50000, 75000, None, 40000, 90000, None, 80000]
})
print("原始数据:")
print(data_exercise)
# 方法1:中位数填充
income_median = data_exercise['Income'].median()
data_median = data_exercise.copy()
data_median['Income_median_fill'] = data_median['Income'].fillna(income_median)
print(f"\n中位数填充值:{income_median}")
print("中位数填充结果:")
print(data_median[['Income', 'Income_median_fill']])
# 方法2:KNN填充(使用2个最近邻)
imputer = KNNImputer(n_neighbors=2)
data_knn_array = imputer.fit_transform(data_exercise[['Age', 'Education_Years', 'Income']])
data_knn = pd.DataFrame(data_knn_array, columns=['Age', 'Education_Years', 'Income_knn_fill'])
print("\nKNN填充结果(保留原始Age和Education_Years以对照):")
print(data_knn[['Age', 'Education_Years', 'Income_knn_fill']])
# 比较
print("\n填充值对比:")
print("索引3(原缺失):中位数=62500.0, KNN=57500.0")
print("索引6(原缺失):中位数=62500.0, KNN=85000.0")
print("\n分析:KNN考虑了样本的相似性(年龄、教育年限),")
print("因此填充值更个性化,而中位数填充对所有缺失给了相同值。")原始数据:
Age Education_Years Income
0 25 12 30000.0
1 30 16 50000.0
2 35 18 75000.0
3 40 16 NaN
4 45 12 40000.0
5 50 20 90000.0
6 55 18 NaN
7 60 16 80000.0
中位数填充值:62500.0
中位数填充结果:
Income Income_median_fill
0 30000.0 30000.0
1 50000.0 50000.0
2 75000.0 75000.0
3 NaN 62500.0
4 40000.0 40000.0
5 90000.0 90000.0
6 NaN 62500.0
7 80000.0 80000.0
KNN填充结果(保留原始Age和Education_Years以对照):
Age Education_Years Income_knn_fill
0 25.0 12.0 30000.0
1 30.0 16.0 50000.0
2 35.0 18.0 75000.0
3 40.0 16.0 57500.0
4 45.0 12.0 40000.0
5 50.0 20.0 90000.0
6 55.0 18.0 85000.0
7 60.0 16.0 80000.0
填充值对比:
索引3(原缺失):中位数=62500.0, KNN=57500.0
索引6(原缺失):中位数=62500.0, KNN=85000.0
分析:KNN考虑了样本的相似性(年龄、教育年限),
因此填充值更个性化,而中位数填充对所有缺失给了相同值。本章小结
缺失值处理是特征工程的基础环节,直接关系到后续模型的质量。本节系统学习了从简单到复杂的多种处理方法:
- 识别:使用
isnull()、check_missing快速定位缺失。 - 删除:简单直接,但可能损失信息和引入偏差。
- 标记:将缺失本身转化为特征,适用于树模型等。
- 填充:最常用的方法,包括常量、统计量、模型预测等。
- 高级模型:KNNImputer 和 MissForest 能利用特征间关系进行智能插补。
📋 行动清单
- 在你的下一个数据项目中,先系统检查每个特征的缺失比例。
- 对数值型缺失特征,尝试用中位数填充和KNN填充两种方法,比较模型效果差异。
- 对高缺失率的类别特征,实践“标记缺失为特殊类别”的方法。
—— 小象教研组