一般来说,未经处理的原始数据中通常会存在缺失值、离群值等,因此在建模训练之前需要处理好缺失值。
缺失值处理方法一般可分为:删除、统计值填充、统一值填充、前后向值填充、插值法填充、建模预测填充和具体分析7种方法。
理论部分
缺失值最简单的处理方法是删除,所谓删除就是删除属性或者删除样本,删除一般可分为两种情况:
删除属性(特征)
如果某一个特征中存在大量的缺失值(缺失量大于总数据量的40%~50%及以上),
那么我们可以认为这个特征提供的信息量非常有限,这个时候可以选择删除掉这一维特征。
删除样本
如果整个数据集中缺失值较少或者缺失值数量对于整个数据集来说可以忽略不计的情况下,
那么可以直接删除含有缺失值的样本记录。
注意事项:
如果数据集本身数据量就很少的情况下,不建议直接删除缺失值。
代码实现
构造假数据做演示,就上面两种情况进行代码实现删除。
import numpy as np
import pandas as pd
# 构造数据
def dataset():
col1 = [1, 2, 3, 4, 5, 6, 7, 8, 9,10]
col2 = [3, 1, 7, np.nan, 4, 0, 5, 7, 12, np.nan]
col3 = [3, np.nan, np.nan, np.nan, 9, np.nan, 10, np.nan, 4, np.nan]
y = [10, 15, 8, 12, 17, 9, 7, 14, 16, 20]
data = {'feature1':col1, 'feature2':col2, 'feature3':col3, 'label':y}
df = pd.DataFrame(data)
return df
data = dataset()
data
feature1 | feature2 | feature3 | label | |
---|---|---|---|---|
0 | 1 | 3.0 | 3.0 | 10 |
1 | 2 | 1.0 | NaN | 15 |
2 | 3 | 7.0 | NaN | 8 |
3 | 4 | NaN | NaN | 12 |
4 | 5 | 4.0 | 9.0 | 17 |
5 | 6 | 0.0 | NaN | 9 |
6 | 7 | 5.0 | 10.0 | 7 |
7 | 8 | 7.0 | NaN | 14 |
8 | 9 | 12.0 | 4.0 | 16 |
9 | 10 | NaN | NaN | 20 |
data.isnull().sum()
feature1 0
feature2 2
feature3 6
label 0
dtype: int64
# 删除属性
def delete_feature(df):
N = df.shape[0] # 样本数
no_nan_count = df.count().to_frame().T # 每一维特征非缺失值的数量
del_feature, save_feature = [], []
for col in no_nan_count.columns.tolist():
loss_rate = (N - no_nan_count[col].values[0])/N # 缺失率
# print(loss_rate)
if loss_rate > 0.5: # 缺失率大于 50% 时,将这一维特征删除
del_feature.append(col)
else:
save_feature.append(col)
return del_feature, df[save_feature]
del_feature, df11 = delete_feature(data)
print(del_feature)
df11
['feature3']
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | NaN | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | NaN | 20 |
从上面可以看出,feature2 的缺失值较少,可以采取直接删除措施
# 删除样本
def delete_sample(df):
df_ = df.dropna()
return df_
delete_sample(df11)
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
理论部分
对于特征的缺失值,可以根据缺失值所对应的那一维特征的统计值来进行填充。
统计值一般泛指平均值、中位数、众数、最大值、最小值等,具体使用哪一种统计值要根据具体问题具体分析。
注意事项:当特征之间存在很强的类别信息时,需要进行类内统计,效果比直接处理会更好。
比如在填充身高时,需要先对男女进行分组聚合之后再进行统计值填充处理(男士的一般平均身高1.70,女士一般1.60)。
代码实现
使用上面数据帧 df11 作为演示数据集,分别实现使用各个统计值填充缺失值。
# 使用上面 df11 的数据帧作为演示数据
df11
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | NaN | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | NaN | 20 |
# 均值填充
print(df11.mean())
df11.fillna(df11.mean())
feature1 5.500
feature2 4.875
label 12.800
dtype: float64
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.000 | 10 |
1 | 2 | 1.000 | 15 |
2 | 3 | 7.000 | 8 |
3 | 4 | 4.875 | 12 |
4 | 5 | 4.000 | 17 |
5 | 6 | 0.000 | 9 |
6 | 7 | 5.000 | 7 |
7 | 8 | 7.000 | 14 |
8 | 9 | 12.000 | 16 |
9 | 10 | 4.875 | 20 |
# 中位数填充
print(df11.median())
df11.fillna(df11.median())
feature1 5.5
feature2 4.5
label 13.0
dtype: float64
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | 4.5 | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | 4.5 | 20 |
由于众数可能会存在多个,因此返回的是序列而不是一个值所以在填充众数的时候,我们可以 df11[‘feature’].mode()[0],可以取第一个众数作为填充值
# 众数填充
print(df11.mode())
def mode_fill(df):
for col in df.columns.tolist():
if df[col].isnull().sum() > 0: # 有缺失值就进行众数填充
df_ = df.fillna(df11[col].mode()[0])
return df_
mode_fill(df11)
feature1 feature2 label
0 1 7.0 7
1 2 NaN 8
2 3 NaN 9
3 4 NaN 10
4 5 NaN 12
5 6 NaN 14
6 7 NaN 15
7 8 NaN 16
8 9 NaN 17
9 10 NaN 20
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | 7.0 | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | 7.0 | 20 |
# 最大值/最小值填充
df11.fillna(df11.max())
df11.fillna(df11.min())
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | 0.0 | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | 0.0 | 20 |
理论部分
代码实现
任然使用数据帧 df11 进行演示,实现统一值填充缺失值的应用。
df11
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | NaN | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | NaN | 20 |
# 统一值填充
# 自定义统一值常数为 10
df11.fillna(value=10)
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | 10.0 | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | 10.0 | 20 |
理论部分
前后向值填充是指使用缺失值的前一个或者后一个的值作为填充值进行填充。
代码实现
任然使用数据帧 df11 作为演示的数据集,实现前后向值填充。
df11
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | NaN | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | NaN | 20 |
df11.fillna(method='ffill') # 前向填充
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | 7.0 | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | 12.0 | 20 |
从上面的后向填充我们发现明显的 Bug:
如果最后一个是缺失值,那么后向填充无法处理最后一个的缺失值;
如果第一个是缺失值,那么前向填充无法处理第一个的缺失值。
因此在进行前后向值填充时,要根据具体情况来进行填充,
一般同时进行前向填充+后向填充就可以解决上面的问题。
工作原理
所谓的插值法,就是在X范围区间中挑选一个或者自定义一个数值,
然后代进去插值模型公式当中,求出数值作为缺失值的数据。
** 1. 多项式插值**
理论公式及推导
已知n+1个互异的点 P 1 : ( x 1 , y 1 ) , P 2 : ( x 2 , y 2 ) , . . . , P n + 1 : ( x n + 1 , y n + 1 ) P1:(x1,y1),P2:(x2,y2),...,P_{n+1}:(x_{n+1},y_{n+1}) P1:(x1,y1),P2:(x2,y2),...,Pn+1:(xn+1,yn+1),
可以求得经过这n+1个点,最高次不超过n的多项式 Y = a 0 + a 1 X + a 2 X 2 + . . . + a n X n Y=a_0+a_1X+a_2X^2+...+a_nX^n Y=a0+a1X+a2X2+...+anXn,
其中计算系数A的公式如下:
A = [ a 0 , a 1 , . . . , a n ] T = X − 1 Y , 其 中 X − 1 是 X 的 逆 矩 阵 A=[a_0,a_1,...,a_n]^T=X^{-1}Y,其中X^{-1}是X的逆矩阵 A=[a0,a1,...,an]T=X−1Y,其中X−1是X的逆矩阵
(1)其中X,Y形式如下,求待定系数A:
X = [ 1 x 1 x 1 2 . . . x 1 n 1 x 2 x 2 2 . . . x 2 n . . . . . 1 x n + 1 x n + 1 2 . . . x n + 1 n ] , Y = [ y 1 y 2 . y n + 1 ] X=\begin{bmatrix} 1&x_1&x_1^2&...&x_1^n\\ 1&x_2&x_2^2&...&x_2^n\\ .&.&.&.&.\\ 1&x_{n+1}&x_{n+1}^2&...&x_{n+1}^n \end{bmatrix},Y=\begin{bmatrix} y_1&\\ y_2&\\ .&\\ y_{n+1}& \end{bmatrix} X=⎣⎢⎢⎡11.1x1x2.xn+1x12x22.xn+12..........x1nx2n.xn+1n⎦⎥⎥⎤,Y=⎣⎢⎢⎡y1y2.yn+1⎦⎥⎥⎤
(2)进行插值的公式, Y = A X Y=AX Y=AX
工作原理
(1)在事先已知的n+1个P点,可以通过A=X^(-1) Y求解得到待定系数A。
(2)假设有一空值,已知X(test_x)值,但Y值(缺失值的填充词)不知道,
由步骤1求解到的待定系数根据公式Y=AX可以求解出缺失值的数值。
import numpy as np
def Polynomial(x, y, test_x):
'''
test_x 的值一般是在缺失值的前几个或者后几个值当中,挑出一个作为参考值,
将其值代入到插值模型之中,学习出一个值作为缺失值的填充值
'''
# 求待定系数
array_x = np.array(x) # 向量化
array_y = np.array(y)
n, X = len(x), []
for i in range(n): # 形成 X 矩阵
l = array_x ** i
X.append(l)
X = np.array(X).T
A = np.dot(np.linalg.inv(X), array_y) # 根据公式求待定系数 A
# 缺失值插值
xx = []
for j in range(n):
k = test_x ** j
xx.append(k)
xx=np.array(xx)
return np.dot(xx, A)
x, y, test_x = [1, 2, 3, 4], [1, 5, 2, 6], 3.5
Polynomial(x, y, test_x)
2.2499999999999716
2. lagrange插值
工作原理
可以证明,经过n+1个互异的点的次数不超过n的多项式是唯一存在的。
也就是说,无论是否是使用何种基底,只要基底能张成所需要的空间,都不会影响最终结果。 。
理论公式及推导
已知n+1个互异的点 P 1 : ( x 1 , y 1 ) , P 2 : ( x 2 , y 2 ) , . . . , P n + 1 : ( x n + 1 , y n + 1 ) P1:(x1,y1),P2:(x2,y2),...,P_{n+1}:(x_{n+1},y_{n+1}) P1:(x1,y1),P2:(x2,y2),...,Pn+1:(xn+1,yn+1),令
l i ( x ) = ∏ ( j ≠ i ) ( j = 1 ) n + 1 x − x j x i − x j , 公 式 ( 1 ) l_i(x)=\prod_{(j\ne i)(j=1)}^{n+1}\frac{x-x_j}{x_i-x_j},公式(1) li(x)=(j=i)(j=1)∏n+1xi−xjx−xj,公式(1)
作为插值基底,则Lagrange值 L i ( x ) = ∑ i = 1 n + 1 l i ( x ) y i , 公 式 ( 2 ) L_i(x)=\sum \limits_{i=1}^{n+1}l_i(x)y_i,公式(2) Li(x)=i=1∑n+1li(x)yi,公式(2)
工作原理
(1)先求出插值基底值
(2)再求Lagrange拉格朗日值
def Lagrange(x, y, test_x):
'''
所谓的插值法,就是在X范围区间中挑选一个或者自定义一个数值,
然后代进去插值公式当中,求出数值作为缺失值的数据。
'''
n = len(x)
L = 0
for i in range(n):
# 计算公式 1
li = 1
for j in range(n):
if j != i:
li *= (test_x-x[j])/(x[i]-x[j])
# 计算公式 2
L += li * y[i]
return L
Lagrange(x, y, test_x)
2.25
Pandas也自带差值方法
df11['feature2'].interpolate(method="linear")
0 3.0
1 1.0
2 7.0
3 5.5
4 4.0
5 0.0
6 5.0
7 7.0
8 12.0
9 12.0
Name: feature2, dtype: float64
df11['feature2'].interpolate(method="polynomial",order=2)
# order代表多项式的项数
0 3.000000
1 1.000000
2 7.000000
3 7.856922
4 4.000000
5 0.000000
6 5.000000
7 7.000000
8 12.000000
9 NaN
Name: feature2, dtype: float64
理论部分
预测填充思路如下:
(1)把需要填充缺失值的某一列特征(Feature_A)作为新的标签(Label_A)
(2)然后找出与 Label_A 相关性较强的特征作为它的模型特征
(3)把 Label_A 非缺失值部分作为训练集数据,而缺失值部分则作为测试集数据
(4)若 Label_A 的值属于连续型数值,则进行回归拟合;若是类别(离散)型数值,则进行分类学习
(5)将训练学习到评分和泛化能力较好的模型去预测测试集,从而填充好缺失值
代码实现部分
使用 seaborn 模块中内置 IRIS 数据集进行演示,实现使用算法模型进行预测填充。
import seaborn as sns
import numpy as np
import warnings
import matplotlib.pyplot as plt
%matplotlib inline
warnings.filterwarnings('ignore')
dataset = sns.load_dataset('iris')
print(dataset.shape)
print(dataset.isnull().sum())
dataset.head()
(150, 5)
sepal_length 0
sepal_width 0
petal_length 0
petal_width 0
species 0
dtype: int64
sepal_length | sepal_width | petal_length | petal_width | species | |
---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | setosa |
1 | 4.9 | 3.0 | 1.4 | 0.2 | setosa |
2 | 4.7 | 3.2 | 1.3 | 0.2 | setosa |
3 | 4.6 | 3.1 | 1.5 | 0.2 | setosa |
4 | 5.0 | 3.6 | 1.4 | 0.2 | setosa |
# 将特征 petal_width 处理成含有 30 个缺失值的特征
dataset['Label_petal_length'] = dataset['petal_length']
for i in range(0, 150, 5):
dataset.loc[i, 'Label_petal_length'] = np.nan
print(dataset.isnull().sum())
dataset.head()
sepal_length 0
sepal_width 0
petal_length 0
petal_width 0
species 0
Label_petal_length 30
dtype: int64
sepal_length | sepal_width | petal_length | petal_width | species | Label_petal_length | |
---|---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | setosa | NaN |
1 | 4.9 | 3.0 | 1.4 | 0.2 | setosa | 1.4 |
2 | 4.7 | 3.2 | 1.3 | 0.2 | setosa | 1.3 |
3 | 4.6 | 3.1 | 1.5 | 0.2 | setosa | 1.5 |
4 | 5.0 | 3.6 | 1.4 | 0.2 | setosa | 1.4 |
dataset.corr()
sepal_length | sepal_width | petal_length | petal_width | Label_petal_length | |
---|---|---|---|---|---|
sepal_length | 1.000000 | -0.117570 | 0.871754 | 0.817941 | 0.875744 |
sepal_width | -0.117570 | 1.000000 | -0.428440 | -0.366126 | -0.449716 |
petal_length | 0.871754 | -0.428440 | 1.000000 | 0.962865 | 1.000000 |
petal_width | 0.817941 | -0.366126 | 0.962865 | 1.000000 | 0.963768 |
Label_petal_length | 0.875744 | -0.449716 | 1.000000 | 0.963768 | 1.000000 |
可以发现特征 sepal_length、petal_width 与 Label_petal_width 有着强关联,
因此 sepal_length、petal_width 作为 Label_petal_length 的模型特征
data = dataset[['sepal_length', 'petal_width', 'Label_petal_length']].copy()
train = data[data['Label_petal_length'].notnull()]
test = data[data['Label_petal_length'].isnull()]
print(train.shape)
print(test.shape)
(120, 3)
(30, 3)
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
# 将训练集进行切分,方便验证模型训练的泛化能力
x_train, x_valid, y_train, y_valid = train_test_split(train.iloc[:, :2],
train.iloc[:, 2],
test_size=0.3
)
print(x_train.shape, x_valid.shape)
print(y_train.shape, y_valid.shape)
# 使用简单的线性回归进行训练
lr = LinearRegression()
lr.fit(x_train, y_train)
y_train_pred = lr.predict(x_train)
print('在训练集中的表现:', r2_score(y_train_pred, y_train))
y_valid_pred = lr.predict(x_valid)
print('在验证集中的表现:', r2_score(y_valid_pred, y_valid))
(84, 2) (36, 2)
(84,) (36,)
>>>在训练集中的表现: 0.9513919768204693
>>>在验证集中的表现: 0.9375742227297561
y_test_pred = lr.predict(test.iloc[:, :2])
test.loc[:, 'Label_petal_length'] = y_test_pred
df_no_nan = pd.concat([train, test], axis=0)
print(df_no_nan.isnull().sum())
df_no_nan.head()
sepal_length 0
petal_width 0
Label_petal_length 0
dtype: int64
sepal_length | petal_width | Label_petal_length | |
---|---|---|---|
1 | 4.9 | 0.2 | 1.4 |
2 | 4.7 | 0.2 | 1.3 |
3 | 4.6 | 0.2 | 1.5 |
4 | 5.0 | 0.2 | 1.4 |
6 | 4.6 | 0.3 | 1.4 |
上面就是预测填充的代码示例以及详细讲解。
利用knn算法填充,其实是把目标列当做目标标量,利用非缺失的数据进行knn算法拟合,最后对目标列缺失进行预测。(对于连续特征一般是加权平均,对于离散特征一般是加权投票)
df11
feature1 | feature2 | label | |
---|---|---|---|
0 | 1 | 3.0 | 10 |
1 | 2 | 1.0 | 15 |
2 | 3 | 7.0 | 8 |
3 | 4 | NaN | 12 |
4 | 5 | 4.0 | 17 |
5 | 6 | 0.0 | 9 |
6 | 7 | 5.0 | 7 |
7 | 8 | 7.0 | 14 |
8 | 9 | 12.0 | 16 |
9 | 10 | NaN | 20 |
from fancyimpute import KNN
fill_knn = KNN(k=3).fit_transform(df11)
data_2 = pd.DataFrame(fill_knn)
data_2
Imputing row 1/10 with 0 missing, elapsed time: 0.001
0 | 1 | 2 | |
---|---|---|---|
0 | 1.0 | 3.000000 | 10.0 |
1 | 2.0 | 1.000000 | 15.0 |
2 | 3.0 | 7.000000 | 8.0 |
3 | 4.0 | 1.333333 | 12.0 |
4 | 5.0 | 4.000000 | 17.0 |
5 | 6.0 | 0.000000 | 9.0 |
6 | 7.0 | 5.000000 | 7.0 |
7 | 8.0 | 7.000000 | 14.0 |
8 | 9.0 | 12.000000 | 16.0 |
9 | 10.0 | 8.818182 | 20.0 |
上面两次提到具体问题具体分析,为什么要具体问题具体分析呢?因为属性缺失有时并不意味着数据缺失,
缺失本身是包含信息的,所以需要根据不同应用场景下缺失值可能包含的信息进行合理填充。
下面通过一些例子来说明如何具体问题具体分析,仁者见仁智者见智,仅供参考:
手工查看每个变量的缺失值是非常麻烦的一件事情,
missingno提供了一个灵活且易于使用的缺失数据可视化和实用程序的小工具集,可以快速直观地总结数据集的完整性。
import missingno as msno
data
feature1 | feature2 | feature3 | label | |
---|---|---|---|---|
0 | 1 | 3.0 | 3.0 | 10 |
1 | 2 | 1.0 | NaN | 15 |
2 | 3 | 7.0 | NaN | 8 |
3 | 4 | NaN | NaN | 12 |
4 | 5 | 4.0 | 9.0 | 17 |
5 | 6 | 0.0 | NaN | 9 |
6 | 7 | 5.0 | 10.0 | 7 |
7 | 8 | 7.0 | NaN | 14 |
8 | 9 | 12.0 | 4.0 | 16 |
9 | 10 | NaN | NaN | 20 |
如果data太大,需要data.sample(250)
重新随机抽样
msno.matrix(data,labels=True)
我们可以一目了然的看到每个变量的缺失情况,
变量feature1,label数据是完整的,feature2变量中间段和最后部分有缺失,feature3确实较多。
msno.bar(data,labels=True)
msno.heatmap(data,figsize=(16, 7))
msno.dendrogram(data)