赞
踩
在实际的建模过程中,我们经常会遇到数据存在缺失值、异常值或重复值的情况。如果在没有对数据进行良好的预处理的情况下直接应用各种算法进行建模,会导致模型的运行速度变慢,预测效果大大降低。数据的预处理包括两个主要方面,即数据预处理和特征工程。本文将首先介绍数据预处理的内容,其中包括数据的无量纲化、缺失值的处理以及分类型和连续型变量的编码。
在真实数据中,我们经常会有将不同规格的数据转换为同一规格,或不同分布的数据转换为某个特定分布的需求,这种需求统称为将数据“无量纲化”。无量纲化可以加快模型运行速度,提高模型精度,避免噪声对模型的影响,不过对于决策树和随机森林而言我们不需要无量纲化,它们能很好地处理任何数据。
数据的无量纲化既可以是线性的,也可以是非线性的,包含中心化和缩放处理,中心化是指将所有样本点减去某一值,即平移某个单位,缩放处理是指将所有样本点除以一个固定值,将数据固定在某个范围之中,取对数也算一种缩放处理。
数据归一化(Normalization)是将所有样本点按最小值中心化,再按极差(极大值-极小值)缩放,最终所有样本点会收敛到[0,1]之见的过程。此外,归一化后的样本点服从正态分布,公式如下:
在sklearn当中,我们使用preprocessing.MinMaxScaler来实现这个操作,MinMaxScaler有一个重要参数feature_range用于控制归一化后的区间,默认值是[0,1]。实际操作代码如下:
from sklearn.preprocessing import MinMaxScaler
import pandas as pd
import numpy as np
data = [[-1, 2], [-0.5, 6], [0, 10], [1, 18]]
print('第一种归一化方式')
# 数据归一化
scaler = MinMaxScaler(feature_range=[5, 10]) # 实例化
scaler = scaler.fit(data) # 在这里本质是生成min(x)和max(x)
result = scaler.transform(data)
# 可以一步到位
result_ = scaler.fit_transform(data)
print(result_)
如下是我们创建的数据:
这是归一化后的结果:
如图,可以发现两列数据其实是一样。此外,模型的训练和导出既可以分步也可以同时操作,并且通过接口inverse_transform逆转结果,具体如下:
print(scaler.inverse_transform(result_)) # 将归一化后的结果逆转
注意:当特征数量非常多的时候,fit会报错并表示,数据量太大了我计算不了,此时使用partial_fit作为训练接口,即:
scaler = scaler.partial_fit(data)
实际上,对于特征数量少的数据我们也可以用numpy更快地实现归一化,代码如下:
print("第二种归一化方式")
# 用numpy来实现归一化
x = np.array([[-1, 2], [-0.5, 6], [0, 10], [1, 18]])
# 归一化的x‘= x - min/ 极差(max - min)
print(x.min())
print(x.min(axis=0)) # 返回每列的最小值,axis=1则返回每一行的最小值
x_nor = (x - x.min(axis=0))/(x.max(axis=0)-x.min(axis=0))
print(x_nor)
# 逆转归一化
x_returned = x_nor*(x.max(axis=0)-x.min(axis=0))+x.min(axis=0)
print(x_returned)
结果与第一种归一化的结果一致,不过numpy处理的速度不如MinMaxScaler,在低维数据中两者差不多,在高维数据中最好使用MinMaxScaler。
数据标准化(Standardization)是指将所有样本点按0中心化,再按标准差缩放,最终服从标准正态分布的过程,公式如下:
我们使用preprocessing.StandardScaler来实现这个操作,代码如下:
from sklearn.preprocessing import StandardScaler # 标准化 # x'= (x-u)/o data = [[-1, 2], [-0.5, 6], [0, 10], [1, 18]] scaler = StandardScaler() # 实例化 scaler.fit(data) # fit, 本质是生成均值和方差 print(scaler.mean_) # 查看均值的属性mean_ print(scaler.var_) # 查看方差的属性var_ x_std = scaler.transform(data) # 查看结果 print(x_std) print(x_std.mean()) print(x_std.std()) print(scaler.fit_transform(data)) print(scaler.inverse_transform(x_std)) # 逆转标准化
数组的属性mean_和var_可以查看均值和方差,此外Standardization也可以用接口fit_transform一步到位。
如下则是标准化后的结果:
此时查看下其均值和方差可以发现分别为0和1,服从标准正态分布。
同样,我们也可以用接口inverse_transform将标准化结果逆转
至此,数据的无量纲化告一段段落,如上是一些常用的无量纲化方法,还有一些其他的函数也可以做到无量纲化,在此就不多赘述了。此外,对于方法的选择,我们经常使用标准化来对数据无量纲化,因为归一化容易受到极端数据的影响,从而可能导致模型表现不够好。
本文此处使用的缺失值来自泰坦尼克号数据集,这个数据有三个特征,一个数值型,两个字符型,有需要的还请从Kaggle自行下载,下载链接:https://www.kaggle.com/competitions/titanic/data,如若遇到账号问题,b站有详细教程,还请自行解决。
本文将借助sklearn的impute.SimpleImuter函数填补缺失值,如下是数据集的信息以及前5行样本:
可以发现数据类型不统一,特征Age和Embarked存在缺失值,这里首先处理Age,显然中位数会比较适合填充,具体代码如下:
from sklearn.impute import SimpleImputer
age = data.loc[:, "Age"].values.reshape(-1, 1) # Series由索引和值组成,values获取值,reshape(-1, 1)升维
imp_median = SimpleImputer(strategy='median') # 用中位数填补
imp_median = imp_median.fit_transform(age)
data.loc[:, "Age"] = imp_median
注意SimpleImputer只能识别二维数组,因此需要做升维操作。
再来看船舱等级Embarked,由于普通的船舱最多,这里我们使用众数来填补,具体代码如下:
# 使用众数填补Embarked
Embarked = data.loc[:, "Embarked"].values.reshape(-1, 1)
imp_mode = SimpleImputer(strategy='most_frequent')
data.loc[:, "Embarked"] = imp_mode.fit_transform(Embarked)
不过这里Embarked的缺失值只有两个,也可以直接删除其缺失值所在行来消除缺失值,代码如下:
data_ = data.copy()
data_.loc[:, "Agwoe"] = data_.loc[:, 'Age'].fillna(data_.loc[:, "Age"].median()) # fillna在DateFrame里面直接进行填补
data_.dropna(axis=0, inplace=True)
# axis=0删除所有有缺失值的行,1则是删除列,inplace=True会覆盖原数据
由于为了完整性,我们这里还是使用Embarked填充众数的数据集,现在我们的缺失值已经没有了。
在机器学习当中大多数算法都要求数据集数据类型为数值型,譬如逻辑回归,支持向量机SVM,k近邻算法等都只能处理数值型数据,不过决策树和普斯贝叶斯可以处理文字,但是sklearn中规定必须导入数值型,然而现实中很多特征以及标签都不能以数字来表示,因此为了让算法认识我们的数据,我们必须编码我们的数据,即,将文字型数据转换为数值型。
在sklearn当中,我们将使用.preprocessing.LabelEncoder来处理我们的分类标签,这是标签专用的函数,也就是说不要求标签是二维数组,具体代码如下:
from sklearn.preprocessing import LabelEncoder # 分类标签编码 # 将非数值标签编码 data_1 = data.copy() # 不破坏原数据 survived = data_1.iloc[:, 0] # 获取标签 le = LabelEncoder() # 实例化 le = le.fit(survived) # 导入数据 label = le.transform(survived) print(label) # le.fit_transform(survived) 也可以直接fit_transform # le.inverse_transform(label) # 使用inverse_transform可以逆转 print(le.classes_) # 属性.classes_查看标签中究竟有多少类型 data_1.iloc[:, 1] = label print(data.head()) # data_1.iloc[:, 1] = LabelEncoder().fit_transform(data_1.iloc[:, 1]) # 也可以一步到位
下图是编码后标签的情况:
由于只有Yes和No,LabelEncoder就将其转换成对应的1和0,虽然没有空值存在,但我们还是用属性categories_查看下标签的类型吧。
可以清楚地发现只有No和Yes两种类型,我们的标签被很好地编码成功了。
preprocessing.OrdinalEncoder是特征专用的编码函数,它用于编码分类型特征,要求数据类型必须是二维数组,我们将用它来编码性别和船舱等级,具体代码如下:
from sklearn.preprocessing import OrdinalEncoder # 分类特征编码,特征专用,即不能导入一维数组
# 特征专用
data_2 = data.copy() # 还是一样,不改变我们原来的数据
print(OrdinalEncoder().fit(data_2.iloc[:, 1::2]).categories_) # 属性categories_返回特征矩阵中的每个特征有多少类型
data_2.iloc[:, 1:] = OrdinalEncoder().fit_transform(data_2.iloc[:, 1:])
print(data_2.info())
print(data_2.head())
使用属性categories_查看每个特征的类型,编码后的数值也是这个顺序
如上图,特征已经全被编码成功,都是float64类型,但这里有个问题,其实编码方式与我们的特征和标签不匹对,这里的特征和标签是属于名义变量,接下来我来介绍下字符变量。常用的字符变量分为名义变量、有序变量和有距变量:
名义变量,顾名思义,是表示名义的变量,它们不存在任何数学运算关系,而是相互独立的变量,比如支付方式包含[‘支付宝’,‘微信’,‘现金’],它们压根不存在比如说支付宝>微信>现金的关系,也不存在支付宝=现金+微信的关系,它们是相互独立的名词,对于名义变量应该使用独热编码创建哑变量的方式才能尽可能让算法认识到数据是什么模样。下面是我用笔记软件描述的正常变量和哑变量的图像:
此时哑变量类似与我们检索空值用isnull得到的布尔矩阵一样,1则是说明该位置是该列头字符,反之,0则说明不是该列头字符。
有序变量,则是指存在顺序关系的一组字符变量,比如学历包含[‘小学’,‘高中’,‘大学’]是有一个学历的大小关系在里面的,对于有序变量,我们才应该用LabelEncoder和OrdinalEncoder来编码。
有距变量,就是存在距离的一组变量,相互之间可以计算的,比如体重,120kg-45kg=90kg,分类之间可以通过数学计算互相转换。
此外,本文将使用独热编码来创建该哑变量,在sklearn当中,preprocessing.OneHotEncoder则是独热编码的函数,具体代码如下:
from sklearn.preprocessing import OneHotEncoder # 独热编码,创建哑变量 s = data.iloc[:, 1::2] enc = OneHotEncoder(categories='auto').fit(s) result = enc.transform(s).toarray() print(result) # OneHotEncoder(categories='auto').fit_transorm(s).to_array() # 一步到位 print(pd.DataFrame(enc.inverse_transform(result))) # 同样可以逆转结果 print(enc.get_feature_names_out()) # axis=1,表示跨行进行合并 new_data = pd.concat([data,pd.DataFrame(result)],axis=1) # print(new_data.head()) new_data.drop(['Sex','Embarked'], axis=1, inplace=True) new_data.columns = ["Survived", "Age", "Female", "Male", "Embarked_C","Embarked_Q", "Embarked_S"] print(new_data.head())
OneHotEncoder有一个重要参数categories用于确认特征类型,这里设置auto,可以让算法自动寻找类型。这里我们对性别和船舱等级创建哑变量,如下:
我们将使用方法get_feature_names_out()获取哑变量特征名称,其对应类型如下:
然后将哑变量与原本的数据合并。
此时需要删除Sex,Embarked特征,并为新特征命名:
这里标签Survived也可以做哑变量,但我们还是用标签编码创建正常变量吧。
至此,分类型特征已经全被处理好了,数据预处理已经做得差不多了,回顾下,我们处理了缺失值,用中位数填充了Age,众数填充了Embarked,使用标签编码函数LabelEncoder编码了标签,使用独热编码函数OneHotEncoder编码了Embarked和Sex.就剩连续型标签还没处理了,接下来本文将最后的一个内容,处理连续型特征。
处理连续性特征的方法有两种,一是设置阈值将数据二值化,小于阈值的映射为0,反之则为1。二则是分箱,将数据按一定规则分成几个箱子,每个箱子包含一定量的数据。
在sklearn当中我们用preprocessing.Binarizer进行二值化,threshold是其重要参数,用于设置阈值,这里我们将年龄以30作为阈值二值化,具体代码如下:
from sklearn.preprocessing import Binarizer # 二值化
# 将年龄二值化,设置阈值
data_3 = data.copy()
f = data_2.iloc[:, 2].values.reshape(-1, 1) # 类为特征专用,所有不能使用一维数组
transformer = Binarizer(threshold=30).fit_transform(f)
# print(transformer)
当然,对于年龄来说,分成一个个年龄段才更有利于算法的运行,下面将介绍分箱。
在sklearn当中,我们将使用preprocessing.KBinsDiscretizer来进行数据分箱,n_bins,encode和strategy是其重要参数:
n_bins,用于控制箱子数目。
encode,是编码的方式,默认“onehot”.其参数如下:
“onehot”,做哑变量,之后返回和一个稀疏矩阵,即01矩阵,每一列是一个特征的类别,含有该类别的样本的表示为1,反之则为0。
“ordinal”,每个箱被编码为一个整数,返回每一列是一个特征,每个特征下含有不同整数编码的箱的矩阵。
“onehot_dense”,做哑变量,返回一个密集矩阵,不常用。
strategy,用来定义箱宽的方式,默认"quantile",其参数如下:
“uniform”,表示等宽分箱。
“quantile”,表示等位分箱,每个特征中的每个箱内的样本数量相同。
“kmeans”,表示按聚类分箱,每个箱中的值到最近的一维k均值聚类的簇心的距离都相同。
具体代码如下:
from sklearn.preprocessing import KBinsDiscretizer # 分箱
b = data.iloc[:, 2].values.reshape(-1, 1)
est = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform')
print(est.fit_transform(b))
print(set(est.fit_transform(b).ravel())) # 降维去重
有0,1,2,说明年龄被成功分成了三个箱子,我们去重再确认下:
至此,数据预处理就讲完了,下节将介绍特征工程,敬请期待。
ps:此处声明下,所有内容皆是来自网上资源,如有侵犯,还请私聊,本文创作的目的只是为了记录以及传播。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。