之前一直在研究数据特征工程,看了一些资料,也自己写了通用的代码,现在将这些内容整理一下。机器学习里有一句名言:数据和特征决定了机器学习的上限,而模型和算法的应用只是让我们逼近这个上限。整个数据挖掘全流程60%以上的时间和精力去做建模前期的数据处理和特征分析,那么面对参差不齐的数据,n多的特征,我们要做的就是step by step抽丝剥茧。首先我们要对数据做清洗,预处理,然后是特征工程。
1.数据预处理
在python中,常用的数据格式是DataFrame,我们将一个DataFrame作为输入,可先将数据框按照数据的类型进行分离,分割成数值型数据框和名义型数据框,然后对这两个数据框分别做不同的处理:
import pandas as pd
def split_dt(dt):
numeric_dt = pd.DataFrame()
nomial_dt = pd.DataFrame()
for i in dt.columns:
if type(dt[i][0]) != str:
numeric_dt[i] = dt[i]
else:
nomial_dt[i] = dt[i]
return numeric_dt, nomial_dt
这一步骤,我将一个输入的DataFrame分割成了numeric_dt和nomial_dt,接下来我们分别对这两个数据框进行处理。
1.1数值型数据预处理
首先我们看看数据涨什么样子,当然这一步也可以在分割之前做,总之就是什么时候看一下都是可以的:
def data_ndes(dt):
samples, features = dt.shape
# ld = dt.iloc[:, 0].size
# vd = dt.columns.size
ds = dt.describe()
return samples, features, dt.head(), ds
返回的是数据的样本及特征数量,数据的前5行做个大概的预览,还有数据的describe,一般就是每个数值型变量的总数(通过这个也可以看出来特征是不是有缺失值)、最小值、1/4分位数,中位数、均值、3/4分位数。之后我们可以用data.drop_duplicate()
来删除重复的数据。如果这个数据中有时间类型的数据,我们还可以对时间类型的数据做些规范,这其中也考虑了时间戳的情况:
# datetime standard
def transform_date(dt_col, format="%Y-%m-%d %H:%M:%S"):
if not type(dt_col[0]) is str:
return pd.to_datetime(pd.to_datetime(round(dt_col), unit='s'), format=format)
else:
return pd.to_datetime(dt_col, format=format)
浮点型数据的规整:包括四舍五入、向上、向下取整、保留n位小数,输入数据的一列,返回规范后的一列
def numerical_round(dt_col):
return round(dt_col)
def numerical_int(dt_col):
return [int(x) for x in dt_col]
def numerical_ceil(dt_col):
return [math.ceil(x) for x in dt_col]
def numerical_round2(dt_col, n):
return round(dt_col, ndigits=n)
接下来我们要做的是离群点和缺失值的检测及处理:
1.1.1缺失值的检测及处理
对于一个数据框检测每一列是否有缺失值,我期望的到的结果是每一列缺失值的个数、缺失值的比例,然后将这些含有缺失值的数据取出来进行观察。
def num_missing(dt):
# isNull = lambda x : any(pd.isnull(x))
# return dt.apply(function=isNull, axis=0)
is_null = []
null_num = []
null_ratio = []
for i in dt.columns:
is_null.append(any(pd.isnull(dt[i])))
null_num.append(sum(pd.isnull(dt[i])))
null_ratio.append(float(round(sum(pd.isnull(dt[i]))/dt.shape[0], 4)))
return is_null, null_num, null_ratio
缺失值的处理我主要用了以下几种方法,一是删除含有缺失值的样本:
def null_drop(dt):
return dt.dropna()
得到的就是不含缺失值的样本,这是我们想到了一个问题,如果某一列含有很多的缺失值,那么就会删除了太多的样本,所以在这一步骤之前,我们通过之前的缺失值检测的结果先将含有很多缺失值的列进行处理,一般情况下如果缺失值超过了30%,那么删除这个特征有时候比做一些填补的效果要好,如果缺失值不多,那我们可以采取均值、中位数、众数(一般是名义型数据处理方式)进行填补,或者如果数据有明显的规则和连续型,那么也可以采用向前或者向后填补 :
# 众数
def dt_mod(dt_col):
a = mode(dt_col, nan_policy='omit')[0][0]
return a
2.mean,median,mod
def null_fill(dt_col, va):
return dt_col.fillna(value=va)
#fill forward / backward number
def null_fillfb(dt_col, way):
return dt_col.fillna(method=way)
1.1.2离群点的检测及处理
1.1.2.1 1.5倍IQR离群点的检测及处理
离群点这部分我主要用了常用的三种检测方法,一是1.5倍IQR,具体看下面的代码就能理解:
import scipy
import numpy as np
def outlier_detect_iqr(dt_col):
outlier_value = []
outlier_index = []
outlier_count = 0
detect_range = scipy.stats.iqr(dt_col, nan_policy='omit')
Q1 = dt_col.quantile(q=0.25)
Q3 = dt_col.quantile(q=0.75)
for ids, values in enumerate(dt_col):
if (values >= Q3+1.5 * detect_range) | (values <= Q1-1.5 * detect_range):
outlier_count += 1
outlier_value.append(values)
outlier_index.append(ids)
return outlier_index, outlier_value, outlier_count
函数的输入的进行检查的数据框的一列,返回离群点的index,离群值以及离群点的总数。对于离群点的处理方式一般有取值的自然对数,为什么要取值的自然对数,下面的例子直观的展示了log的作用:
离群点的处理我采用了三种常用的方式,第二种方法是,输入数据框的一列,输出不含缺失值的一列:
def outlier_iqr_delete(dt_col):
new_value = []
new_index = []
ids = outlier_detect_iqr(dt_col)[0]
for i, vas in enumerate(dt_col):
if i not in ids:
new_value.append(vas)
new_index.append(i)
return new_index, new_value
第三种方法是补均值、中位数、众数:
def outlier_iqr_fillmean(dt_col):
new_value = []
new_index = []
ids = outlier_detect_iqr(dt_col)[0]
for i, vas in enumerate(dt_col):
if i in ids:
new_value.append(dt_col.mean())
new_index.append(i)
else:
new_value.append(vas)
new_index.append(i)
return new_index, new_value
def outlier_iqr_fillmedian(dt_col):
new_value = []
new_index = []
ids = outlier_detect_iqr(dt_col)[0]
for i, vas in enumerate(dt_col):
if i in ids:
new_value.append(dt_col.median())
new_index.append(i)
else:
new_value.append(vas)
new_index.append(i)
return new_index, new_value
def outlier_iqr_fillmode(dt_col):
new_value = []
new_index = []
ids = outlier_detect_iqr(dt_col)[0]
for i, vas in enumerate(dt_col):
if i in ids:
new_value.append(mode(dt_col, nan_policy='omit')[0][0])
new_index.append(i)
else:
new_value.append(vas)
new_index.append(i)
return new_index, new_value
因为我们想要输入一列然后得到不含缺失值的一列,所以函数写起来规范了输入输出比较麻烦。后面根据不同的检测方式会得到不同的离群点,但是处理的方式都是相同的,后面不再赘述。当然离群点也是可以不处理的,python后面也提供了一个针对含有离群点数据的RobustScaler
1.1.2.2 [5%,95%]离群点的检测
第二个检测的方法是把数据的[5%, 95%]之外的点划为离群点:
def outlier_detect_inter(dt_col):
outlier_value = []
outlier_index = []
outlier_count = 0
new = dt_col.sort_values()
for ids, values in enumerate(dt_col):
if (ids <= len(new) * 0.05) | (ids >= len(new) * 0.95):
outlier_count += 1
outlier_value.append(values)
outlier_index.append(ids)
return outlier_index, outlier_value, outlier_count
处理方式也是一样的,可根据自己的需要改变输出,得到离群点的index是为了后续的处理。
1.1.2.3 n倍均值检测(n一般为3)
具体方法看代码就知道啦
def outlier_detect_mean(dt_col, n=3):
outlier_value = []
outlier_index = []
outlier_count = 0
m = dt_col.mean()
for ids, values in enumerate(dt_col):
if (values <= m / n) | (values >= m * n):
outlier_count += 1
outlier_value.append(values)
outlier_index.append(ids)
return outlier_index, outlier_value, outlier_count
目前为止数值型变量的处理可以告一段落了,现在开始处理名义型变量。
1.2 名义型变量的处理
名义型变量一般要查看出现的字符以及出现的次数
import collections
def data_sdes(dt):
for i in dt.columns:
if type(dt[i][0]) == str:
count_nominal = collections.Counter(dt[i])
print(i)
ctd = pd.DataFrame.from_dict(count_nominal, orient='index')
print(ctd)
名义型变量无法直接扔到estimator中做机器学习算法,就需要我们将其转化成数值型变量,有两种方式,一是哑编码,另一种是One-hotEncoder,各有其适用情况及优缺点,后续介绍。经过了数据预处理,数据规范了很多,没有了缺失值,我们可以进行下面的数据特征工程。
2.数据特征工程
有这么一句话在业界广泛流传:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。那特征工程到底是什么呢?就是一项工程活动,目的是最大限度地从原始数据中提取特征以供算法和模型使用。特征工程包括:
特征生成(特征转换)
特征抽取
特征选择
降维
2.1 特征生成
特征生成主要利用python中的sklearn.preprocessing,pandas,numpy,fraction。scikit-learn提供了丰富的特征工程功能。
2.1.1 无量纲化
2.1.1.1 标准化
无量纲化使不同规格的数据转换到同一规格。常见的无量纲化方法有标准化和区间缩放法。标准化的前提是特征值服从正态分布,标准化后,其转换成标准正态分布。区间缩放法利用了边界值信息,将特征的取值区间缩放到某个特点的范围,例如[0, 1]等。标准化需要计算特征的均值和标准差,公式表达为:
from sklearn.preprocessing import StandardScaler
def std_features(dt):
numeric_dt = split_dt(dt)[0]
std_dt = pd.DataFrame(StandardScaler(copy=True,
with_mean=True,
with_std=True)
.fit_transform(numeric_dt.values))
std_dt.columns = 'std_f_' + numeric_dt.columns
return std_dt
输入数据框,先将名义型数据剥离,得到的是数值型数据的标准化数据。
2.1.1.2 区间缩放
区间缩放有很多种方式,最常用的是用两个最值进行缩放,用到sklearn.preprocessing中的MinMaxScaler,当然还有很多种方法,只是python只讲这个方式封装成了函数,需要的话可以自己写,在spark中还有利用最大绝对值进行缩放,具体我会在数据特征工程-spark这篇文章中介绍。
# 区间缩放,返回值为缩放到[0, 1]区间的数据
from sklearn.preprocessing import MinMaxScaler
def minmax(dt):
numeric_dt = split_dt(dt)[0]
minmax_dt = pd.DataFrame(MinMaxScaler(feature_range=(0, 1),
copy=True).
fit_transform(numeric_dt.values))
minmax_dt.columns = 'minmax_'+numeric_dt.columns
return minmax_dt
对于有很多离群点的数据,python提供了一种RobustScaler方法:
# 区间缩放 data with many outliers median not mean
from sklearn.preprocessing import RobustScaler
def scaler_dt_outliers(dt):
numeric_dt = split_dt(dt)[0]
scaler_outliers_dt = pd.DataFrame(RobustScaler(with_centering=True, with_scaling=True,
quantile_range=(25.0, 75.0), copy=True).fit_transform(numeric_dt.values))
scaler_outliers_dt.columns = 'minmax_ols_'+numeric_dt.columns
return scaler_outliers_dt
2.1.2 归一化
标准化是依照特征矩阵的列处理数据,归一化是依照特征矩阵的行处理数据,其目的在于样本向量在点乘运算或其他核函数计算相似性时,拥有统一的标准,也就是说都转化为“单位向量”。规则为l2的归一化公式如下
# 归一化,l2(default),Normalizer,sample
from sklearn.preprocessing import Normalizer
def normalizer_sample(dt, norm='l2'):
numeric_dt = split_dt(dt)[0]
normalizer_dt = pd.DataFrame(Normalizer(norm=norm, copy=True).transform(numeric_dt.values))
normalizer_dt.columns = 'normalize_'+norm+'_sample_'+numeric_dt.columns
return normalizer_dt
为什么要将数据归一化呢,举个简单的例子:学生的分数:[0,100];[0,10], 若不进行归一化,可能导致某些指标被忽视,影响到数据分析的结构。
2.1.3 对定量特征二值化
有时候我们只想知道数据是不是有超过一个值,数据的粒度变粗了,这就是二值化,利用Binarizer进行二值化的代码如下:
from sklearn.preprocessing import Binarizer
def binarizer(dt, threshold):
numeric_dt = split_dt(dt)[0]
binarizer_dt = pd.DataFrame(index=numeric_dt.index, columns=numeric_dt.columns)
for i in numeric_dt.columns:
binarizer_dt = pd.DataFrame(Binarizer(threshold=threshold[i], copy=True).fit_transform(numeric_dt.values))
binarizer_dt.columns = 'binarizer_'+numeric_dt.columns
return binarizer_dt
2.1.4 对定性特征的哑编码
计算机无法识别语义,对定性特征的哑编码方便后续对定性特征的分析,包括词之间距离的计算等。哑编码代码如下:传入数据框的一列,和一个映射的字典,得到的是一列数据,可以将这一列concat到原数据框中,或者将多个列concat成一个数据框,concat时候只要传入dumy_col即可。
def dumy(dt_col, diction):
dumy_col = []
for i in dt_col:
dumy_i = (diction[i])
dumy_col.append(dumy_i)
# dumy_col.name = 'dumy_'+i
return 'dumy_'+dt_col.name, dumy_col
2.1.5 One-hotEncder
哑编码的问题:男性->0,女性->1。各种类别的特征被看成是有序的,不符合实际场景。
one-hotEncoder的算法原理:使用N位状态寄存器对N个状态进行编码,每个状态都有它独立的寄存器位,并且在任意时候,只有一位有效,也就是对于每一个特征,如果有m个值,那么经过one-hot encoder之后就变成了m个二元特征。利用sklearn中的OneHotEncoder。传入的是之前哑编码concat之后的数据框,得到One-HotEncoder数据框
from sklearn.preprocessing import OneHotEncoder
def onehotencoder_dt(dumy_dt):
enc = OneHotEncoder()
enc.fit(dumy_dt)
return pd.DataFrame(enc.transform(dumy_dt).toarray())
算法One-HotEncoder算法的优缺点如下:
优点:
1)在一定程度上扩充了特征(比如性别是一维的,可以扩展成二维)
2)离散特征->欧式空间(回归、分类、聚类等机器学习算法中特征之间距离的计算以及相似度的计算)、更加合理
缺点:
1)数据会变得非常稀疏(维度灾难)
2)无法进行词汇语义的比较运算
2.1.6 数学变换
简单的单纯的数学变换,也不知道怎样的变换会跟我们的目标变量很大的关系。常数次幂、开根号、log,这部分没什么复杂的原理,就是在编程过程中要考虑到剔除掉不可行的情况
def transform_square_root(dt, n, m):
square_root_dt = pd.DataFrame()
for i in dt.columns:
if type(dt[i][0]) != str and min(dt[i]) >= 0:
square_root_dt[i+'**'+str(Fraction(n, m))] = np.power(dt[i], n/m)
return square_root_dt
多项式变换代码如下:
def transform_polynomial(dt, degree=2):
mydata = pd.DataFrame()
# c = []
for i in dt.columns:
if type(dt[i][0]) != str:
mydata[i] = dt[i]
# c.append('poly_'+i)
poly_dt = pd.DataFrame(PolynomialFeatures(degree=degree).fit_transform(mydata.values))
# poly_dt.columns = c
return poly_dt
如果需要也可以将两个变量进行加减乘除。编程时注意减法和除法有限制也有顺序就好。好了,到此我们基本上就做完了特征转换,这是一个生成大量特征的过程,为的是为我们后面的特征选择提供更多的可能性。当然以上介绍的都是结构化数据的特征变化,对于非结构化的数据,sklearn提供了针对文本数据的TF-IDDF, word2vec,更多的就涉及到NLTK等自然语言处理问题,针对图片则是像素的抽取,其实就是n维矩阵的各种操作,个人觉得用处不大,在此不再赘述下面就是抽丝剥茧的过程啦。
2.2 特征选择
特征选择一般按照两个原则来选择,一是看特征是否发散 , 二是考察特征与目标的相关性。