Press "Enter" to skip to content

从原始到精细:数据预处理之旅 —— 第二部分:缺失值

照片来自Unsplash的Holly Stratton

在阅读本文之前,请先阅读系列文章中的特征工程的前一篇文章。

从原始到精细:数据预处理之旅 — 第1部分:特征缩放

有时,我们为机器学习任务接收到的数据并不适合使用Scikit-Learn进行编码…

pub.towardsai.net

为什么要处理缺失值?

大多数现实世界的数据集都至少包含一定比例的缺失值。但是Scikit-Learn的估计器不能处理这样的数据。因此,为了使数据与Scikit-Learn的估计器的要求兼容,必须处理数据中的缺失值。

为什么完全删除带有缺失值的数据记录是一个坏主意?

处理缺失值是数据预处理的关键部分。通常,会删除或填补缺失的数据以解决这个问题。然而,完全删除缺失值并不总是最佳解决方案,特别是当数据集很小的时候。这种方法可能会导致数据集规模进一步减小。此外,缺失数据可能是由数据收集过程中存在的特定条件导致的,或者可能是缺失数据存在特定模式。因此,重要的是考虑其他处理缺失数据的方法。

让我举个例子来解释。想象一下,我们正在调查大学生,其中一个问题是关于他们的体重。在这种情况下,一些女学生可能因社会期望而不愿透露自己的体重。因此,我们可能会在女性参与者中遇到大量缺失体重的回答。这是一个遵循特定模式的缺失数据的例子。

让我们再来看看调查中的另一种情况。如果我们询问与肌肉力量或二头肌大小有关的问题,许多大学年龄的男性可能会犹豫提供他们的答案。这可能是因为他们对自己的体力或力量感到自卑。因此,我们可能会从男性参与者中得到大量缺失的回答。

因此,几乎总是删除带有缺失值的记录是一个坏主意。这是因为通过这样做,我们可能会丧失数据中存在的一些有用信息。处理缺失值的更复杂的方法是使用从其他非缺失数据中获得的值填补缺失的部分。

删除带有缺失值的列仍然可以在大数据集上获得相当好的准确性。因此,让我们看看如何使用代码来做到这一点。

通过删除包含缺失值的特征来处理缺失值问题

为了演示,我们将使用Kaggle上的墨尔本房屋快照数据集。为了简单起见,我们只使用数据的数值特征。

## 导入所需的库import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport seaborn as snsfrom sklearn.model_selection import train_test_splitfrom sklearn.ensemble import RandomForestRegressorfrom sklearn.tree import DecisionTreeRegressorfrom sklearn.metrics import mean_absolute_errorfrom sklearn.experimental import enable_iterative_imputerfrom sklearn.impute import SimpleImputer, IterativeImputer, KNNImputerimport warningswarnings.filterwarnings('ignore')

## 读取数据df = pd.read_csv('data-path')## 检查数据的基本信息df.info()

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第2张

## 找出数值和分类特征的名称num_feat = [feature for feature in df.columns if df[feature].dtypes != 'O']cat_feat = [feature for feature in df.columns if feature not in num_feat]print(f"数值特征:{num_feat}。\n")print(f"分类特征:{cat_feat}。")

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第3张

## 将数据分为因变量('Price')和自变量X,y = df.drop('Price', axis=1), df['Price']
## 为了简化,从数据中删除分类特征。X.drop(cat_feat, axis=1, inplace=True)X.info()

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第4张

## 将数据分为训练数据和验证数据X_train, X_valid, y_train, y_valid = train_test_split(X, y, train_size=0.8, test_size=0.2,                                                      random_state=0)

## 检查每个特征中缺失值的数量missing_values_df = pd.DataFrame()missing_values_df['特征'] = X_train.columnsmissing_values_df['缺失值数量'] = X.isnull().sum().to_numpy()missing_values_df['缺失值百分比 (%)'] = missing_values_df['缺失值数量'].apply(lambda x: np.round((x/X_train.shape[0])*100),2)missing_values_df

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第5张

让我们可视化数据中的缺失值。

import missingno as msgnmsgn.matrix(X_train)

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第6张

在上图中,每条白线表示一个缺失值,黑线表示一个非缺失值。

从上图可以看出,特征Car、BuildArea和YearBuilt包含缺失值。

现在,让我们创建一个函数来检查填充方法的效果。我们对填充后的数据训练回归器,并计算其平均绝对误差值以进行评估。

# 用于比较不同方法的函数def score_dataset(X_train, X_valid, y_train, y_valid):    model = RandomForestRegressor(n_estimators=10, random_state=0)    model.fit(X_train, y_train)    preds = model.predict(X_valid)    return mean_absolute_error(y_valid, preds)

现在最后让我们移除含有缺失值的特征。

# 获取含有缺失值的列名cols_with_missing = [col for col in X_train.columns                     if X_train[col].isnull().any()]# 在训练和验证数据中删除列reduced_X_train = X_train.drop(cols_with_missing, axis=1)reduced_X_valid = X_valid.drop(cols_with_missing, axis=1)print(f"通过删除含有缺失值的列的方法得到的MAE值:{np.round(score_dataset(reduced_X_train, reduced_X_valid, y_train, y_valid),2)}")

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第7张

处理缺失值问题的另一种方法是用某个值填充缺失的部分。

如何通过填充来处理缺失数据

Image by author

主要有两种方法可以使用Scikit-Learn库中的类进行缺失值填充,即单变量特征填充和多变量特征填充。

在单变量特征填充中,填充所需的值是通过单个特征中的数据计算得出的。例如,如果想要填充重量列中的缺失值,则可以使用重量列的非缺失值计算填充值。

在多变量特征填充中,填充所需的值是通过多个特征中的数据计算得出的。例如,要填充重量列中的缺失值,还可以使用其他列(如身高和二头肌大小)的非缺失值。

单变量特征插补

Scikit-Learn的SimpleImputer类用于执行单变量特征插补。SimpleImputer类使我们能够用我们选择的常数值或非缺失数据在同一列中的均值、中位数或众数等统计值替换缺失值。

SimpleImputer参数

Scikit-Learn的SimpleImputer有四个重要的参数,即missing_values、strategy、fill_value和add_indicator。

missing_values:此参数指示数据中缺失值的占位符,通常设置为numpy.nan。

strategy:此参数用于显示用于插补的策略。SimpleImputer类有四个可用的策略,即均值(mean)、中位数(median)、最常见值(most_frequent)和常数(constant)。

fill_value:当strategy = ‘most_frequent’时,我们需要将常数值赋给fill_value参数。

add_indicator:当缺失值显示某种模式时,此参数很有用。如果add_indicator = True,则为每个包含缺失值的列创建一个新列。对于每个新列,在原始列中找到缺失值的索引处放置值1,在其他地方放置值0。

SimpleImputer代码演示

现在,让我们看看如何使用SimpleImputer类执行插补。

si = SimpleImputer(missing_values=np.nan, strategy='mean', add_indicator=True)print(f"插补前X_train中的缺失值数据:\n\n {X_train.head()}\n\n")print(f"插补前X_train的列名:\n\n {X_train.columns.values}\n\n")filled_X_train = pd.DataFrame(si.fit_transform(X_train), columns=si.get_feature_names_out())filled_X_valid = pd.DataFrame(si.transform(X_valid), columns=si.get_feature_names_out())print(f"插补后X_train中的缺失值数据:\n\n {filled_X_train.head()}\n\n")print(f"插补后X_train的列名:\n\n {filled_X_train.columns.values}\n\n")print(f"使用SimpleImputer插补方法的MAE值:{np.round(score_dataset(filled_X_train, filled_X_valid, y_train, y_valid),2)}")

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第9张

请注意,根据我们设置的add_indicator = True,数据集中增加了三列附加列(用黄色标记)。最初有三列缺失值,为了保留这三列中的缺失值模式,创建了三个新列。

选择何种策略

此外,与SimpleImputer相关的另一个重要因素是了解选择哪种策略。策略主要基于数据的类型和数据中是否存在异常值。

当数据是数值型且没有任何异常值时,策略= ‘mean’

当数据是数值型且存在异常值时,策略= ‘median’。

对于分类数据,策略= ‘most_frequent’。

当数据是分类数据或者我们希望使用自定义值填充缺失值时,策略= ‘constant’。

多变量特征插补

在Scikit-Learn中,多变量特征插补有两个选择,即IterativeImputer和KNNImputer。

首先让我们了解IterativeImputer。

IterativeImputer

请注意,此估计器目前处于实验状态。因此,使用IterativeImputer的代码可能会在将来出现问题。

在IterativeImputer中,具有缺失值的特征被视为因变量,而其他所有列被视为自变量。然后,在因变量的非缺失值上训练回归器。训练好的模型将预测因变量中的缺失值。对于每个包含缺失值的列,这个过程会重复进行。

让我们通过一个例子更清楚地理解这个过程。假设我们的训练数据和测试数据如下。

X_train
X_test

如果我们想要填补Feature2中的缺失值,那么IterativeImputer将会创建如下的函数:

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第12张

因此,IterativeImputer将会创建一个以Feature2为‘y’,以Feature1为‘X’的回归器。一旦模型以这种方式进行训练,它就会发现Feature2中的值是Feature1中值的两倍。利用这个逻辑,Feature2中的缺失值将被填充。

同样地,另一个模型将被训练来填补Feature1中的值。

现在,让我们看一下如何实现IterativeImputer。

iterimputer = IterativeImputer(estimator=DecisionTreeRegressor(), missing_values=np.nan, add_indicator=True)print(f"在填补X_train中的缺失值之前的数据:\n\n {X_train.head()}\n\n")print(f"在填补之前的X_train的列名:\n\n {X_train.columns.values}\n\n")filled_X_train = pd.DataFrame(iterimputer.fit_transform(X_train), columns=iterimputer.get_feature_names_out())filled_X_valid = pd.DataFrame(iterimputer.transform(X_valid), columns=iterimputer.get_feature_names_out())print(f"在填补X_train中的缺失值之后的数据:\n\n {filled_X_train.head()}\n\n")print(f"在填补之后的X_train的列名:\n\n {filled_X_train.columns.values}\n\n")print(f"使用IterativeImputer进行填补的均方误差(MAE):{np.round(score_dataset(filled_X_train, filled_X_valid, y_train, y_valid),2)}")

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第13张

请注意,我们还可以使用IterativeImputer的estimator参数来指定要用于填补的回归器。此外,IterativeImputer还有一个add_indicator参数,其工作原理与SimpleImputer中的参数相同。

KNNImputer

该填补器使用K最近邻的概念。计算距离时使用支持缺失值的欧几里得距离nan_euclidean_distances。

让我们看一下nan_euclidean_distances是如何工作的。在计算样本对之间的距离时,这个公式会忽略具有缺失值的特征坐标,并增加剩余坐标的权重。

计算nan_euclidean_distance的公式

在计算距离时,我们只考虑那些值不缺失的坐标,即a1、a2等和b1、b2等都不是缺失值。

现在,让我们举个例子来理解这个算法的工作原理。

考虑以下数据。

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第15张

假设我们想要填补Feature1中第一个索引处的缺失值。

步骤1:

我们将计算每行记录与索引为1处的行记录之间的nan_euclidean_distance。

索引为1处的记录与索引为2处的记录之间的nan_euclidean_distance:

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第16张

同样地,

索引1处记录与索引3处记录之间的nan欧几里德距离:

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第17张

索引1处记录与索引4处记录之间的nan欧几里德距离:

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第18张

索引1处记录与索引5处记录之间的nan欧几里德距离:

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第19张

步骤2:

在这一步中,我们将按升序对这些距离进行排序。

56、128.3、159.8、193.8

步骤3:

在这一步中,我们将按顺序取前K个(K最近邻中的K值)数值。

如果K = 2,则我们将使用顺序中前两个值的平均值填充缺失值,即(56 + 128.3)/ 2 = 92.15

如果K = 3,则我们将使用顺序中前三个值的平均值填充缺失值,即(56 + 128.3 + 159.8)/ 3 = 114.7

以此类推。

现在,我们已经了解了算法的工作原理,让我们看看如何实现它。

knnimputer = KNNImputer(n_neighbors=3, missing_values=np.nan, add_indicator=True)print(f"在X_train中填充缺失值之前的数据:\n\n {X_train.head()}\n\n")print(f"在填充之前的X_train的列名称:\n\n {X_train.columns.values}\n\n")knn_filled_X_train = pd.DataFrame(knnimputer.fit_transform(X_train), columns=knnimputer.get_feature_names_out())knn_filled_X_valid = pd.DataFrame(knnimputer.transform(X_valid), columns=knnimputer.get_feature_names_out())print(f"在X_train中填充缺失值之后的数据:\n\n {knn_filled_X_train.head()}\n\n")print(f"在填充之后的X_train的列名称:\n\n {knn_filled_X_train.columns.values}\n\n")print(f"使用KNNImputer进行缺失值填充的MAE:{np.round(score_dataset(knn_filled_X_train, knn_filled_X_valid, y_train, y_valid),2)}")

从原始到精细:数据预处理之旅 —— 第二部分:缺失值 四海 第20张

请注意,IterativeImputer也有一个add_indicator参数,其用法与SimpleImputer中的相同。

感谢阅读!如果对本文有任何想法,请告诉我。

你在选择下一篇阅读材料时是否感到困惑?别担心,我知道一篇你可能会觉得有趣的文章。

通过交叉验证来增加对机器学习模型的自信

交叉验证是一个常用的工具,用于检验机器学习模型在新数据上的可靠性。这篇文章将介绍…

pub.towardsai.net

还有一篇…

从混乱到有序:利用数据聚类实现增强决策

本文将展示数据聚类方法的重要用例,如何使用这些方法,并展示如何…

pub.towardsai.net

Shivam Shinde

  • 与我联系:LinkedIn
  • 同样地,你也可以在VoAGI上关注我

祝你有个美好的一天!

Leave a Reply

Your email address will not be published. Required fields are marked *