Python之ML–数据预处理
机器学习算法最终学习结果的优劣取决于两个主要因素:数据的质量和数据中蕴含的有用信息的数量.因此,在将数据集应用于学习算法之前,对其进行检验及预处理是至关重要的
主要知识点如下:
- 数据集中缺少数据的删除和填充
- 数据格式化
- 模型格式化
- 模型构建中的特征选择
一.缺少数据的处理
from IPython.display import Image
我们见到的缺失值是数据表中的空值,或者是类似于NaN(Not A Number,非数字)的占位符
我们通过一个CSV(comma-separed values)以逗号为分隔符的数值)文件构造一个简单的例子
import numpy as np
import pandas as pd
from io import StringIO
csv_data='''
A,B,C,D
1.0,2.0,3.0,4.0
5.0,6.0,,8.0
0.0,11.0,12.0,
'''
df=pd.read_csv(StringIO(csv_data))
print(df)
A B C D
0 1.0 2.0 3.0 4.0
1 5.0 6.0 NaN 8.0
2 0.0 11.0 12.0 NaN
有两个缺失值由NaN替代,StringIO函数在此仅起到演示作用:如果我们的数据时存储在硬盘上的CSV文件,就可以通过此函数以字符串的方式从文件中读取数据,并将其转换成DataFrame的格式赋值给csv_data
对于大的DataFrame来说,手工搜索缺失值是及其繁琐的,在此情况下,我们可以使用代码中的isnull方法返回一个布尔型的DataFrame值,若DataFrame的元素单元包含数字型数值则返回值为假(False),若数据值缺失则返回值为真(True).通过sum方法,我们可以得到如下所示的每列中的缺失值数量
df.isnull().sum()
A 0
B 0
C 1
D 1
dtype: int64
在使用sklearn处理数据之前,可以通过DataFrame的value属性来访问相关的Numpy数组
df.values
array([[ 1., 2., 3., 4.],
[ 5., 6., nan, 8.],
[ 0., 11., 12., nan]])
1.将存在缺失值的特征或样本删除
处理缺失数据最简单的方法就是:将包含确实数据的特征(列)或者样本(行)从数据集中删除.可通过dropna方法来删除数据集中包含缺失值的行
df.dropna()
A | B | C | D | |
---|---|---|---|---|
0 | 1.0 | 2.0 | 3.0 | 4.0 |
类似地,我们可以将axis参数设为1,以删除数据集中至少包含一个NaN值的列
df.dropna(axis=1)
A | B | |
---|---|---|
0 | 1.0 | 2.0 |
1 | 5.0 | 6.0 |
2 | 0.0 | 11.0 |
df.dropna(subset=['C'])
A | B | C | D | |
---|---|---|---|---|
0 | 1.0 | 2.0 | 3.0 | 4.0 |
2 | 0.0 | 11.0 | 12.0 | NaN |
2.缺失数据填充
使用不同的插值技术,通过数据集中其他训练样本的数据来估计缺失值.最常用的插值技术之一就是均值插补(meaneinputation),即使用相应的特征均值来替换缺失值.我们使用scikit-learn中的Impute类方便地实现此方法
from sklearn.preprocessing import Imputer
imr=Imputer(missing_values='NaN',strategy='mean',axis=0)
imr=imr.fit(df)
imputed_data=imr.transform(df.values)
imputed_data
E:Anacondaenvsmytensorflowlibsite-packagessklearnutilsdeprecation.py:58: DeprecationWarning: Class Imputer is deprecated; Imputer was deprecated in version 0.20 and will be removed in 0.22. Import impute.SimpleImputer from sklearn instead.
warnings.warn(msg, category=DeprecationWarning)
array([[ 1. , 2. , 3. , 4. ],
[ 5. , 6. , 7.5, 8. ],
[ 0. , 11. , 12. , 6. ]])
如果我们把参数axis=0改为axis=1,则用每行的均值来进行相应的替换
3.理解scikit-learn预估器的API
在上一节,我们使用了scikit-learn中的Imputer类来填充我们数据集中的缺失值Imputer类属于scikit-learn中的转换器类,主要用于数据的转换.这些类中常用的两个方法是fit和transform.其中,fit方法用于对数据集中的参数进行识别并构建相应的数据补齐模型,而transform方法则使用刚构建的数据补齐模型对数据集中相应参数的缺失值进行补齐所有待补齐数据的维度应该与数据补齐模型中其他数据的维度相同
在监督学习中,我们额外提供了类标用于构建模型,而模型可通过predict方法对新的样本数据展开预测
二.处理类别数据
当目前为止,我们仅学习了处理数值型数据的方法.然而,在真实数据集中,经常会出现一个或多个类别数据的特征列.我们在讨论类别数据时,又可以进一步将它们划分为标称特征(nominal feature)和有序特征(ordinal feature)
在探索类别数据的处理技巧之前,我们先构造一个数据集来用来描述问题
import pandas as pd
df=pd.DataFrame([
['green','M',10.1,'class1'],
['red','L',13.5,'class2'],
['blue','XL',15.3,'class1']
])
df.columns=['color','size','price','classlabel']
df
color | size | price | classlabel | |
---|---|---|---|---|
0 | green | M | 10.1 | class1 |
1 | red | L | 13.5 | class2 |
2 | blue | XL | 15.3 | class1 |
我们新构造的DataFrame分别包含了一个标称特征(颜色),一个有序特征(尺寸)以及一个数值特征(价格).类标存储在最后一列
1.有序特征的映射
为了确保学习算法可以正确地使用有序特征,我们需要将类别字符串转换为整数.但是,没有一个适当的方法可以自动将尺寸特征转换为正确的顺序.因此需要我们手工定义相应的映射.如:XL=L+1=M+2
size_mapping={'XL':3,
'L':2,
'M':1
}
df['size']=df['size'].map(size_mapping)
df
color | size | price | classlabel | |
---|---|---|---|---|
0 | green | 1 | 10.1 | class1 |
1 | red | 2 | 13.5 | class2 |
2 | blue | 3 | 15.3 | class1 |
如果在后续过程中需要将整数值还原为有序字符串,可以简单地定义一个逆映射字典:inv_size_mapping={v:k for k,v in size_mapping.items()}
2.类标的编码
我们可以简单地以枚举的方式从0开始设定类标
import numpy as np
class_mapping={label:idx for idx,label in enumerate(np.unique(df['classlabel']))}
class_mapping
{'class1': 0, 'class2': 1}
接下来,我们可以使用映射字典将类标转换为整数
df['classlabel']=df['classlabel'].map(class_mapping)
df
color | size | price | classlabel | |
---|---|---|---|---|
0 | green | 1 | 10.1 | 0 |
1 | red | 2 | 13.5 | 1 |
2 | blue | 3 | 15.3 | 0 |
我们可以通过下列代码将映射字典中的键-值对倒置,以将换转过的类标还原原始的字符串表示
inv_class_mapping={v:k for k,v in class_mapping.items()}
df['classlabel']=df['classlabel'].map(inv_class_mapping)
df
color | size | price | classlabel | |
---|---|---|---|---|
0 | green | 1 | 10.1 | class1 |
1 | red | 2 | 13.5 | class2 |
2 | blue | 3 | 15.3 | class1 |
使用scikit-learn的LabelEncoder类可以更加方便地完成对类标的整数编码工作
from sklearn.preprocessing import LabelEncoder
class_le=LabelEncoder()
y=class_le.fit_transform(df['classlabel'].values)
y
array([0, 1, 0])
请注意:fit_transform方法相当于分别调用fit和transform方法的快捷方式,我们还可以使用inverse_transform方法将整数类标还原原始的字符串表示
class_le.inverse_transform(y)
array(['class1', 'class2', 'class1'], dtype=object)
3.标称特征上的独热编码
X=df[['color','size','price']].values
color_le=LabelEncoder()
X[:,0]=color_le.fit_transform(X[:,0])
X
array([[1, 1, 10.1],
[2, 2, 13.5],
[0, 3, 15.3]], dtype=object)
执行上述代码,Numpy数组X的第一列现在被赋予新的color值,具体编码如下
- blue—>0
- green—>1
- red—>2
独热编码(one-hot encoding)技术的理念就是创建一个新的虚拟特征(dummy feature),虚拟特征的每一列各代表标称数据的一个值
from sklearn.preprocessing import OneHotEncoder
ohe=OneHotEncoder(categorical_features=[0])
ohe.fit_transform(X).toarray()
E:Anacondaenvsmytensorflowlibsite-packagessklearnpreprocessing\_encoders.py:371: FutureWarning: The handling of integer data will change in version 0.22. Currently, the categories are determined based on the range [0, max(values)], while in the future they will be determined based on the unique values.
If you want the future behaviour and silence this warning, you can specify "categories='auto'".
In case you used a LabelEncoder before this OneHotEncoder to convert the categories to integers, then you can now use the OneHotEncoder directly.
warnings.warn(msg, FutureWarning)
E:Anacondaenvsmytensorflowlibsite-packagessklearnpreprocessing\_encoders.py:392: DeprecationWarning: The 'categorical_features' keyword is deprecated in version 0.20 and will be removed in 0.22. You can use the ColumnTransformer instead.
"use the ColumnTransformer instead.", DeprecationWarning)
array([[ 0. , 1. , 0. , 1. , 10.1],
[ 0. , 0. , 1. , 2. , 13.5],
[ 1. , 0. , 0. , 3. , 15.3]])
另外,我们可以通过pandas中的get_dummies方法,更加方便地实现独热编码技术中的虚拟特征.当应用于DataFrame数据时,get_dummies方法只对字符串进行转换,而其他列保持不变
pd.get_dummies(df[['price','color','size']])
price | size | color_blue | color_green | color_red | |
---|---|---|---|---|---|
0 | 10.1 | 1 | 0 | 1 | 0 |
1 | 13.5 | 2 | 0 | 0 | 1 |
2 | 15.3 | 3 | 1 | 0 | 0 |
三.划分训练数据集和测试数据集
葡萄酒数据集(Wine Dataset)是另一个开源数据集,可以通过UCI机器学习样本数据库获得(https://archive.ics.uci.edu/ml/datasets/Wine),它包含178个葡萄酒样本,每个样本通过13个特征对其化学特征进行描述
df_wine=pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data',header=None)
df_wine.columns=['Class label','Alcohol',
'Malic acid','Ash',
'Alcalinity of ash','Magnesium',
'Total phenols','Flavanoids',
'Nonflavanoid phenols',
'Proanthocyanins',
'Color intensity','Hue',
'OD280/OD315 of diluted wines',
'Proline'
]
print("Class labels:",np.unique(df_wine['Class label']))
Class labels: [1 2 3]
葡萄酒样本库通过13个不同的特征,对178个葡萄酒样本的化学特征做出描述,如下表
df_wine.head()
Class label | Alcohol | Malic acid | Ash | Alcalinity of ash | Magnesium | Total phenols | Flavanoids | Nonflavanoid phenols | Proanthocyanins | Color intensity | Hue | OD280/OD315 of diluted wines | Proline | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 14.23 | 1.71 | 2.43 | 15.6 | 127 | 2.80 | 3.06 | 0.28 | 2.29 | 5.64 | 1.04 | 3.92 | 1065 |
1 | 1 | 13.20 | 1.78 | 2.14 | 11.2 | 100 | 2.65 | 2.76 | 0.26 | 1.28 | 4.38 | 1.05 | 3.40 | 1050 |
2 | 1 | 13.16 | 2.36 | 2.67 | 18.6 | 101 | 2.80 | 3.24 | 0.30 | 2.81 | 5.68 | 1.03 | 3.17 | 1185 |
3 | 1 | 14.37 | 1.95 | 2.50 | 16.8 | 113 | 3.85 | 3.49 | 0.24 | 2.18 | 7.80 | 0.86 | 3.45 | 1480 |
4 | 1 | 13.24 | 2.59 | 2.87 | 21.0 | 118 | 2.80 | 2.69 | 0.39 | 1.82 | 4.32 | 1.04 | 2.93 | 735 |
from sklearn.model_selection import train_test_split
X,y=df_wine.iloc[:,1:].values,df_wine.iloc[:,0].values
X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.3,random_state=0)
四.将特征的值缩放到相同的区间
特征缩放(feature scaling)是数据预处理过程中至关重要的一步.决策树和随机森林是机器学习算法中为数不多的不需要进行特征缩放的算法
不同的特征缩放到相同的区间有两个常用的方法:归一化(normalization)和标准化(standardization).多数情况下,归一化指的是将特征的值缩放到区间[0,1],它是最小-最大缩放的一个特例.为了对数据进行规范化处理,我们可以简单地在每个特征列上使用min-max缩放
from sklearn.preprocessing import MinMaxScaler
mms=MinMaxScaler()
X_train_norm=mms.fit_transform(X_train)
X_test_norm=mms.transform(X_test)
ex = pd.DataFrame([0, 1, 2, 3, 4, 5])
# standardize
ex[1] = (ex[0] - ex[0].mean()) / ex[0].std(ddof=0)
# Please note that pandas uses ddof=1 (sample standard deviation)
# by default, whereas NumPy's std method and the StandardScaler
# uses ddof=0 (population standard deviation)
# normalize
ex[2] = (ex[0] - ex[0].min()) / (ex[0].max() - ex[0].min())
ex.columns = ['input', 'standardized', 'normalized']
ex
input | standardized | normalized | |
---|---|---|---|
0 | 0 | -1.46385 | 0.0 |
1 | 1 | -0.87831 | 0.2 |
2 | 2 | -0.29277 | 0.4 |
3 | 3 | 0.29277 | 0.6 |
4 | 4 | 0.87831 | 0.8 |
5 | 5 | 1.46385 | 1.0 |
与MinMaxScaler类似,scikit-learn也已经实现了标准化类
from sklearn.preprocessing import StandardScaler
std=StandardScaler()
X_train_std=std.fit_transform(X_train)
X_test_std=std.transform(X_test)
需要再次强调的是:我们只是使用了StandardScaler对训练数据进行拟合,并使用相同的拟合参数来完成对测试集以及未知数据的转换
五.选择有意义的特征
如果一个模型在训练数据集上的表现比在测试数据集上好很多,这意味着模型过拟合(overfitting)于训练数据.过拟合是指模型参数对于训练数据集的特定观测值拟合得非常近,但训练数据集的分布与真实数据并不一致–我们称之为模型具有较高的方差.产生过拟合的原因是建立在给定训练数据集上的模型过于复杂,而常用的降低泛化误差的方案有:
- 收集更多的训练数据
- 通过正则化引入罚项
- 选择一个参数相对较小的简单模型
- 降低数据的维度
1.使用L1正则化满足数据稀疏化
L2正则化是通过加大的权重增加罚项以降低模型复杂度的一种方法.与L2正则化不同,L1正则化可生成稀疏的特征向量,且大多数的权值为0
对于scikit-learn中支持L1的正则化模型,我们可以通过将penalty参数设定为"l1"来进行简单的数据稀疏处理:
from sklearn.linear_model import LogisticRegression
LogisticRegression(penalty='l1')
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='warn',
n_jobs=None, penalty='l1', random_state=None, solver='warn',
tol=0.0001, verbose=0, warm_start=False)
将其应用于经过标准化处理的葡萄酒数据,通过L1正则化的逻辑斯谛回归模型可以产生如下稀疏结果:
lr=LogisticRegression(penalty='l1',C=0.1)
lr.fit(X_train_std,y_train)
print('Training accuracy:',lr.score(X_train_std,y_train))
print('Test accuracy:',lr.score(X_test_std,y_test))
Training accuracy: 0.9838709677419355
Test accuracy: 0.9814814814814815
E:Anacondaenvsmytensorflowlibsite-packagessklearnlinear_modellogistic.py:433: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
FutureWarning)
E:Anacondaenvsmytensorflowlibsite-packagessklearnlinear_modellogistic.py:460: FutureWarning: Default multi_class will be changed to 'auto' in 0.22. Specify the multi_class option to silence this warning.
"this warning.", FutureWarning)
通过lr.intercept_属性得到截距项后,可以看到反悔了包含三个数值的数组
lr.intercept_
array([-0.38378888, -0.1580707 , -0.70040061])
由于我们在多类别分类数据集上使用了LogisticRegression对象,它默认使用一对多(One-vs-Rest,OvR)的方法.其中,第一个截距项为类别1相对于类别2,3的匹配结果,第二个和第三个类似道理
lr.coef_
array([[ 0.28037545, 0. , 0. , -0.02800529, 0. ,
0. , 0.71026114, 0. , 0. , 0. ,
0. , 0. , 1.23582752],
[-0.64399895, -0.06874575, -0.05719592, 0. , 0. ,
0. , 0. , 0. , 0. , -0.92665435,
0.06023692, 0. , -0.37107354],
[ 0. , 0.06143555, 0. , 0. , 0. ,
0. , -0.63661729, 0. , 0. , 0.49836655,
-0.35810582, -0.5706355 , 0. ]])
通过lr.coef_属性得到的权重系数包含三个权重系数向量,每一个权重向量对应一个分类
import matplotlib.pyplot as plt
fig=plt.figure()
ax=plt.subplot(111)
colors=['blue','green','red','cyan','magenta','yellow','black','pink','lightgreen','lightblue','gray','indigo','orange']
weights,params=[],[]
for c in np.arange(-4,6,dtype=float):
lr=LogisticRegression(penalty='l1',C=10**c,random_state=0)
lr.fit(X_train_std,y_train)
weights.append(lr.coef_[1])
params.append(10**c)
weights=np.array(weights)
for column,color in zip(range(weights.shape[1]),colors):
plt.plot(params,weights[:,column],label=df_wine.columns[column+1],color=color)
plt.axhline(0,color='black',linestyle='--',linewidth=3)
plt.xlim([10**(-5),10**5])
plt.ylabel('weight coefficient')
plt.xlabel('C')
plt.xscale('log')
plt.legend(loc='upper left')
plt.legend(loc='upper left',bbox_to_anchor=(1.38,1.03),ncol=1,fancybox=True)
plt.show()
通过上图,我们能对L1正则化有个深入的认识.在强的正则化参数(C<0.1)作用下,罚项使得所有的特征权重都趋进于0,这里C是正则化参数γ的倒数
2.序列特征选择算法
另外一种降低模型复杂度从而解决过拟合问题的方法是通过特征选择进行降维(dimensionality reduction),该方法对未经正则化处理的模型特别有效.降维技术主要分为两个大类:特征选择和特征提取.通过特征选择,我们可以选出原始特征的一个子集.而在特征提取中,通过对现有的特征信息进行推演
一个经典的序列特征选择算法是序列后向选择算法(Sequential Backward Selection,SBS),其目的是在分类性能衰减最小的约束下,降低原始特征空间上的数据维度,以提高计算效率
python实现SBS算法
from sklearn.base import clone
from itertools import combinations
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
class SBS():
def __init__(self,estimator,k_features,scoring=accuracy_score,test_size=0.25,random_state=1):
self.scoring=scoring
self.estimator=clone(estimator)
self.k_features=k_features
self.test_size=test_size
self.random_state=random_state
def fit(self,X,y):
X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=self.test_size,random_state=self.random_state)
dim=X_train.shape[1]
self.indices_=tuple(range(dim))
self.subsets_=[self.indices_]
score=self._calc_score(X_train,y_train,X_test,y_test,self.indices_)
self.scores_=[score]
while dim>self.k_features:
scores=[]
subsets=[]
for p in combinations(self.indices_,r=dim-1):
score=self._calc_score(X_train,y_train,X_test,y_test,p)
scores.append(score)
subsets.append(p)
best=np.argmax(scores)
self.indices_=subsets[best]
self.subsets_.append(self.indices_)
dim-=1
self.scores_.append(scores[best])
self.k_score_=self.scores_[-1]
return self
def transform(self,X):
return X[:,self.indices_]
def _calc_score(self,X_train,y_train,X_test,y_test,indices):
self.estimator.fit(X_train[:,indices],y_train)
y_pred=self.estimator.predict(X_test[:,indices])
score=self.scoring(y_test,y_pred)
return score
我们实现的SBS应用于KNN分类器的效果
from sklearn.neighbors import KNeighborsClassifier
import matplotlib.pyplot as plt
knn=KNeighborsClassifier(n_neighbors=2)
sbs=SBS(knn,k_features=1)
sbs.fit(X_train_std,y_train)
<__main__.SBS at 0x280934eb550>
k_feat=[len(k) for k in sbs.subsets_]
plt.plot(k_feat,sbs.scores_,marker='o')
plt.ylim([0.7,1.1])
plt.ylabel('Accuracy')
plt.xlabel('Number of features')
plt.grid()
plt.show()
通过上图可以看到:当我们适当减少了特征的数量后,KNN分类器在验证数据集上的准确率提高了
现在我们看一下是哪五个特征在验证数据集上有如此好的表现
k5=list(sbs.subsets_[8])
print(df_wine.columns[1:][k5])
Index(['Alcohol', 'Malic acid', 'Alcalinity of ash', 'Hue', 'Proline'], dtype='object')
使用上述代码,我们从sb_subsets_的第9列中获取了五个特征子集的列标,并通过以pandas的DataFrame格式存储的葡萄酒数据对应的索引中提取到了相应的特征名称
下面我们验证一下KNN分类器在原始测试集上的表现
knn.fit(X_train_std,y_train)
print('Training accuracy:',knn.score(X_train_std,y_train))
print('Testing accuracy:',knn.score(X_test_std,y_test))
Training accuracy: 0.9838709677419355
Testing accuracy: 0.9444444444444444
让我们在选定的五个特征集上看一下KNN的性能
knn.fit(X_train_std[:,k5],y_train)
print('Training accuracy:',knn.score(X_train_std[:,k5],y_train))
print('Testing accuracy:',knn.score(X_test_std[:,k5],y_test))
Training accuracy: 0.9596774193548387
Testing accuracy: 0.9629629629629629
六.通过随机森林判定特征的重要性
from sklearn.ensemble import RandomForestClassifier
feat_labels=df_wine.columns[1:]
forest=RandomForestClassifier(n_estimators=10000,random_state=0,n_jobs=-1)
forest.fit(X_train,y_train)
importances=forest.feature_importances_
indices=np.argsort(importances)[::-1]
for f in range(X_train.shape[1]):
print("%2d) %-*s %f"%(f+1,30,feat_labels[f],importances[indices[f]]))
1) Alcohol 0.182483
2) Malic acid 0.158610
3) Ash 0.150948
4) Alcalinity of ash 0.131987
5) Magnesium 0.106589
6) Total phenols 0.078243
7) Flavanoids 0.060718
8) Nonflavanoid phenols 0.032033
9) Proanthocyanins 0.025400
10) Color intensity 0.022351
11) Hue 0.022078
12) OD280/OD315 of diluted wines 0.014645
13) Proline 0.013916
plt.title('Feature Importances')
plt.bar(range(X_train.shape[1]),importances[indices],color='lightblue',align='center')
plt.xticks(range(X_train.shape[1]),feat_labels,rotation=90)
plt.xlim([-1,X_train.shape[1]])
plt.tight_layout()
plt.show()
根据特征在葡萄酒数据集中的相对重要性,绘制出根据特征重要性排序的图,请注意,这些特征重要性经过归一化处理,其和为1.0
在完成对模型的拟合后,scikit-learn还实现了一个transform方法,可以在用户设定阈值的基础上进行特征选择
例如:将阈值设为0.15,我们可以使用下列代码将数据集压缩到三个最重要的特征:Alcohol,Malic acid和Ash
from sklearn.feature_selection import SelectFromModel
sfm = SelectFromModel(forest, threshold=0.15, prefit=True)
X_selected = sfm.transform(X_train)
X_selected.shape
(124, 3)
进行查看三个最重要的特征详细信息
for f in range(X_selected.shape[1]):
print("%2d) %-*s %f" % (f + 1, 30, feat_labels[indices[f]], importances[indices[f]]))
1) Color intensity 0.182483
2) Proline 0.158610
3) Flavanoids 0.150948