前言

在真实的数据挖掘项目中,原始数据往往并不完美——缺失值是最常见的问题之一。如果直接使用含有缺失值的数据训练模型,可能会导致:

  • 模型无法计算(很多算法不支持缺失值)

  • 统计偏差(缺失并非随机)

  • 信息浪费(直接删除行会丢失有用信息)

因此,在建模之前,我们需要对缺失值进行合理的填充或删除。即数据预处理。


一、准备工作:数据与工具

我们从往年的人工智能竞赛上面获取一道真题:
假设我们有一份矿物数据,包含序号、矿物类型(A/B/C/D)以及若干数值型特征。其中部分样本存在空值,我们现在需要对这份矿物数据做数据预处理,包括且不限于去除不合理的数据,填充数据集中空缺的数据。
数据集预览:
在这里插入图片描述
为了对这份数据集进行预处理,我们需要在额外新建一个py文件,用来存放函数。

二、完整代码

import pandas as pd
import matplotlib.pyplot as plt
import fill_data

data = pd.read_excel('矿物数据.xls')
data = data[data['矿物类型']!='E']
null_num = data.isnull()

null_total = null_num.sum()
X_whole = data.drop('矿物类型',axis=1).drop('序号',axis=1)
y_whole = data.矿物类型

label_dict = {'A':0,'B':1,'C':2,'D':3}
encoded_labels = [label_dict[label] for label in y_whole]
y_whole = pd.Series(encoded_labels,name="矿物类型")

for column_name in X_whole.columns:
    X_whole[column_name] = pd.to_numeric(X_whole[column_name],errors='coerce')

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_whole_Z = scaler.fit_transform(X_whole)
X_whole = pd.DataFrame(X_whole_Z,columns=X_whole.columns)

from sklearn.model_selection import train_test_split
x_train_w,x_test_w,y_train_w,y_test_w = \
    train_test_split(X_whole,y_whole,test_size=0.3,random_state=10000)

# #1.只保留完整数据集
# x_train_fill,y_train_fill = fill_data.cca_train_fill(x_train_w,y_train_w)
# x_test_fill,y_test_fill = fill_data.cca_test_fill(x_train_fill,y_train_fill,x_test_w,y_test_w)

# #2.使用平均数填充
# x_train_fill,y_train_fill = fill_data.mean_train_fill(x_train_w,y_train_w)
# x_test_fill,y_test_fill = fill_data.mean_test_fill(x_train_fill,y_train_fill,x_test_w,y_test_w)

# #3.使用中位数填充
# x_train_fill,y_train_fill = fill_data.median_train_fill(x_train_w,y_train_w)
# x_test_fill,y_test_fill = fill_data.medain_teat_fill(x_train_fill,y_train_fill,x_test_w,y_test_w)

# #4.使用众数填充
# x_train_fill,y_train_fill = fill_data.mode_train_fill(x_train_w,y_train_w)
# x_test_fill,y_test_fill = fill_data.mode_teat_fill(x_train_fill,y_train_fill,x_test_w,y_test_w)

# #5.使用逻辑回归填充
# x_train_fill,y_train_fill = fill_data.lr_train_fill(x_train_w,y_train_w)
# x_test_fill,y_test_fill = fill_data.lr_test_fill(x_train_fill,y_train_fill,x_test_w,y_test_w)

#6.使用随机森林填充
#x_train_fill,y_train_fill = fill_data.rf_train_fill(x_train_w,y_train_w)
#x_test_fill,y_test_fill = fill_data.rf_test_fill(x_train_fill,y_train_fill,x_test_w,y_test_w)


from imblearn.over_sampling import SMOTE
oversampler = SMOTE(random_state=42, k_neighbors=1)
os_x_train,os_y_train = oversampler.fit_resample(x_train_fill,y_train_fill)

y_whole = pd.concat([os_y_train,y_test_fill])
# label_count = pd.value_counts(y_whole)
label_count = y_whole.value_counts()
fig, ax =plt.subplots()
bars = ax.bar(label_count.index,label_count.values)
for bar in bars:
    yval = bar.get_height()
    ax.text(bar.get_x()+bar.get_width()/2,yval,
            round(yval,2),
            va = 'bottom',
            ha = 'center',
            fontsize = 10,
            color = 'black'
    )
plt.xlabel('labels')
plt.ylabel('numbers')
plt.title('The number of data for each category after removing empty data')
plt.show()

data_train = pd.concat([os_y_train,os_x_train],axis=1).sample(frac=1,random_state=4)
data_test = pd.concat([y_test_fill,x_test_fill],axis=1)

data_train.to_excel(r'.//temp_data//训练数据集[随机森林填充].xlsx', index=False)
data_test.to_excel(r'.//temp_data//测试数据集[随机森林填充].xlsx', index=False)

可以看到我们有六种方法处理空缺的数据集,本文仅对前三种方式做介绍和讲解。

三、方法1:只保留完整数据

这种方法的主旨是:既然有的数据是空缺的,那么有空缺的数据我们就删除所有包含缺失值的行,仅使用完整的数据。
他的优点是简单快速,不引入额外偏差且保留真实数据分布。
但是缺点也很明显:样本量大幅减少,可能丢失重要信息,会使数据集的大小明显变小。

在我们新建的fill_data.py文件中写入两种函数,一种针对训练集,一种针对测试集。

def cca_train_fill(train_data,train_label):
    data = pd.concat([train_data,train_label],axis=1)
    data = data.reset_index(drop=True)
    df_filled = data.dropna()
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

def cca_test_fill(test_data,test_label):
    data = pd.concat([test_data,test_label],axis=1)
    data = data.reset_index(drop=True)
    df_filled = data.dropna()
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

代码详解

训练集

首先将训练集的标签和特征导入到函数里面

def cca_train_fill(train_data,train_label):

然后我们需要将标签数据和特征数据结合变为完整数据,然后再进行排序(从小到大)

    data = pd.concat([train_data,train_label],axis=1)
    data = data.reset_index(drop=True)

删除所有有空缺值的行,仅保留有完整数据的数据

    df_filled = data.dropna()

返回包含所有完整数据的数据,并将他们分为标签和特征

   return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

测试集

测试集的处理和训练集没什么两样,不过是测试集是提前切分好的数据,比训练集小的多
同样的将测试集的标签和特征导入到函数里面

def cca_test_fill(test_data,test_label):

合并,排序

    data = pd.concat([test_data,test_label],axis=1)
    data = data.reset_index(drop=True)

删除有空缺值的行数据,返回测试机的完整数据并切分为特征和标签。

    df_filled = data.dropna()
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

四、方法2:使用平均数填充

这个方法主旨是用每一列的已有数据的平均数,填充到空缺的位置上。
和只是用完整数据的方法不同的是,我们需要首先需要按照矿物类型分类好,每一类使用各自的平均值,然后将各自的平均值填充到空缺的位置。

def mean_train_method(data):
    fill_values = data.mean()
    return data.fillna(fill_values)

def mean_train_fill(train_data,train_label):
    data = pd.concat([train_data,train_label],axis=1)
    data = data.reset_index(drop=True)

    A = data[data['矿物类型'] == 0]
    B = data[data['矿物类型'] == 1]
    C = data[data['矿物类型'] == 2]
    D = data[data['矿物类型'] == 3]

    A = mean_train_method(A)
    B = mean_train_method(B)
    C = mean_train_method(C)
    D = mean_train_method(D)

    df_filled = pd.concat([A,B,C,D])
    df_filled = df_filled.reset_index(drop=True)
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

代码详解

训练集

首先写入一个计算平均值的函数
计算出平均值后,填充到空缺的位置

def mean_train_method(data):
    fill_values = data.mean()
    return data.fillna(fill_values)

然后我们再写入一个划分各个类型的函数
导入训练集的标签和特征,合并并排序

def mean_train_fill(train_data,train_label):
    data = pd.concat([train_data,train_label],axis=1)
    data = data.reset_index(drop=True)

分别提取出矿物类型为0,1,2,3的数据,后面导入到计算平均值的函数中。

    A = data[data['矿物类型'] == 0]
    B = data[data['矿物类型'] == 1]
    C = data[data['矿物类型'] == 2]
    D = data[data['矿物类型'] == 3]

将提取出的各个类型的数据导入到计算平均值的函数中,进行计算。

    A = mean_train_method(A)
    B = mean_train_method(B)
    C = mean_train_method(C)
    D = mean_train_method(D)

最后将填充好的A,B,C,D数据合并为一个数据,再切分为特征和标签。

    df_filled = pd.concat([A,B,C,D])
    df_filled = df_filled.reset_index(drop=True)
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

测试集

def mean_test_method(train_data,test_data):
    fill_values = train_data.mean()
    return test_data.fillna(fill_values)

def mean_test_fill(train_data,train_label,test_data,test_label):
    train_data_all = pd.concat([train_data,train_label],axis=1)
    train_data_all = train_data_all.reset_index(drop=True)
    test_data_all = pd.concat([test_data,test_label],axis=1)
    test_data_all = test_data_all.reset_index(drop=True)

    A_train = train_data_all[train_data_all['矿物类型'] == 0]
    B_train = train_data_all[train_data_all['矿物类型'] == 1]
    C_train = train_data_all[train_data_all['矿物类型'] == 2]
    D_train = train_data_all[train_data_all['矿物类型'] == 3]

    A_test = test_data_all[test_data_all['矿物类型'] == 0]
    B_test = test_data_all[test_data_all['矿物类型'] == 1]
    C_test = test_data_all[test_data_all['矿物类型'] == 2]
    D_test = test_data_all[test_data_all['矿物类型'] == 3]

    A = mean_test_method(A_train, A_test)
    B = mean_test_method(B_train, B_test)
    C = mean_test_method(C_train, C_test)
    D = mean_test_method(D_train, D_test)

    df_filled = pd.concat([A,B,C,D])
    df_filled = df_filled.reset_index(drop=True)
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

同样的,首先写入一个计算平均值的函数,计算出平均值后,填充到空缺的位置。
不同的是,我们是将训练集的平均值,填充到测试集的空缺位置上,所以形参我们设置为两个。

def mean_test_method(train_data,test_data):
    fill_values = train_data.mean()
    return test_data.fillna(fill_values)

写入一个划分各个类型的函数
导入训练集的标签和特征,和测试集的特征和标签,合并并排序

def mean_test_fill(train_data,train_label,test_data,test_label):
    train_data_all = pd.concat([train_data,train_label],axis=1)
    train_data_all = train_data_all.reset_index(drop=True)
    test_data_all = pd.concat([test_data,test_label],axis=1)
    test_data_all = test_data_all.reset_index(drop=True)

划分出训练集的类型,后面计算平均值要用。

    A_train = train_data_all[train_data_all['矿物类型'] == 0]
    B_train = train_data_all[train_data_all['矿物类型'] == 1]
    C_train = train_data_all[train_data_all['矿物类型'] == 2]
    D_train = train_data_all[train_data_all['矿物类型'] == 3]

划分出测试集的类型,用来被测试集的平均值填充。

    A_test = test_data_all[test_data_all['矿物类型'] == 0]
    B_test = test_data_all[test_data_all['矿物类型'] == 1]
    C_test = test_data_all[test_data_all['矿物类型'] == 2]
    D_test = test_data_all[test_data_all['矿物类型'] == 3]

将同一个类型的训练集和测试集数据导入到计算平均值的函数内,计算训练集的平均值,填充到测试集的空缺位置上。

    A = mean_test_method(A_train, A_test)
    B = mean_test_method(B_train, B_test)
    C = mean_test_method(C_train, C_test)
    D = mean_test_method(D_train, D_test)

最后将填充好的测试数据合并为一个数据,再切分为特征和标签。

    df_filled = pd.concat([A,B,C,D])
    df_filled = df_filled.reset_index(drop=True)
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

五、方法3:使用中位数填充

本质上是和计算平均值是同一个思路去处理,只是将计算平均值的函数替换为计算中位数的函数。

def median_method(data):
    fill_values = data.median()
    return data.fillna(fill_values)

def median_train_fill(train_data,train_label):
    data = pd.concat([train_data,train_label],axis=1)
    data = data.reset_index(drop=True)

    A = data[data['矿物类型'] == 0]
    B = data[data['矿物类型'] == 1]
    C = data[data['矿物类型'] == 2]
    D = data[data['矿物类型'] == 3]

    A = median_method(A)
    B = median_method(B)
    C = median_method(C)
    D = median_method(D)

    df_filled = pd.concat([A,B,C,D])
    df_filled = df_filled.reset_index(drop=True)
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

def medain_teat_meyhod(train_data,test_data):
    fill_values = train_data.median()
    return test_data.fillna(fill_values)

def medain_teat_fill(train_data,train_label,test_data,test_label):
    train_data_all = pd.concat([train_data,train_label],axis=1)
    train_data_all = train_data_all.reset_index(drop=True)
    test_data_all = pd.concat([test_data,test_label],axis=1)
    test_data_all = test_data_all.reset_index(drop=True)

    A_train = train_data_all[train_data_all['矿物类型'] == 0]
    B_train = train_data_all[train_data_all['矿物类型'] == 1]
    C_train = train_data_all[train_data_all['矿物类型'] == 2]
    D_train = train_data_all[train_data_all['矿物类型'] == 3]

    A_test = test_data_all[test_data_all['矿物类型'] == 0]
    B_test = test_data_all[test_data_all['矿物类型'] == 1]
    C_test = test_data_all[test_data_all['矿物类型'] == 2]
    D_test = test_data_all[test_data_all['矿物类型'] == 3]

    A = medain_teat_meyhod(A_train, A_test)
    B = medain_teat_meyhod(B_train, B_test)
    C = medain_teat_meyhod(C_train, C_test)
    D = medain_teat_meyhod(D_train, D_test)

    df_filled = pd.concat([A,B,C,D])
    df_filled = df_filled.reset_index(drop=True)
    return df_filled.drop('矿物类型',axis=1),df_filled.矿物类型

六、三种方法的直观对比

方法 适用场景 优点 缺点
删除法 缺失率<5%,且缺失完全随机 简单,无数据扭曲 样本减少,浪费信息
均值填充 数值型,分布对称,无异常值 保持均值,速度快 受异常值影响,降低方差
中位数填充 数值型,偏态分布或有异常值 稳健,不受极端值影响 同样降低方差,计算略慢

总结

小样本数据:优先考虑中位数或均值填充,避免删除后样本不足。
数据量充足且缺失少:直接删除法最简单可靠。
下一篇文章我们将会介绍更高级的填充方法:众数填充、逻辑回归预测填充和随机森林填充。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐