0.回顾
上一次打卡探索性数据分析,是我们与数据的第一次会面,需要从整体到局部来认识,为数据清洗和特征工程打下基础。
我一般会使用pandas,sklearn和matplotlib等库进行数据EDA,首先看一下各特征是什么意思,大概预测一下相关性,然后就需要详细看各特征的分布,缺失,特点。
import pandas as pd
from sklearn.preprocessing import StandardScaler
data=open('E:\data\\test\loan.csv',encoding='utf-8')
loan_data=pd.read_csv(data)
loan_data.head()
#查看数据缺失比例,ascending表示是否升序
check_null=loan.isnull().sum().sort_values(ascending=False)/float(len(loan))
#series 也可以用[搜查条件]
print(type(check_null),check_null[check_null>0.2])
thresh_count=len(loan)*0.9
#删除缺失比例大于阀值的列,dropna方法,thresh参数
loan=loan.dropna(thresh=thresh_count,axis=1)
print(loan.shape)
这里推荐使用Missingno库来快速评价数值缺失情况
import missingno as msno
import numpy as np
import matplotlib.pyplot as plt
msno.matrix(loans[objectColumns])
plt.show()
结果类似于这样:
1.概述
特征工程个人觉得包括两部分,一个是特征构造,一个是特征筛选。特征构造包括异常值处理、缺失值处理、数据分桶、特征处理、特征构造。
特征工程是机器学习的重要内容。机器学习有两种提升原始数据表达的方法,特征工程和特征学习。
1.特征学习,自动学习有用的特征
2.特征工程,对数据进行人为处理,有时候也叫洗数据。
特征工程,其实是个经验活,对预测指标要有业务上的熟悉,然后不断将组合的特征,拿到模型中训练,筛选特征,最后取得效果好的特征。
1.1常见的训练数据预处理:
- 异常处理:
通过箱线图(或 3-Sigma)分析删除异常值;
BOX-COX 转换(处理有偏分布);
长尾截断;
特征归一化/标准化:
标准化(转换为标准正态分布);
归一化(抓换到 [0,1] 区间);
针对幂律分布,可以采用公式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rkcdwola-1600702814933)(attachment:image.png)]
- 缺失值处理:
不处理(针对类似 XGBoost 等树模型);
删除(缺失数据太多);
插值补全,包括均值/中位数/众数/建模预测/多重插补/压缩感知补全/矩阵补全等;
分箱,缺失值一个箱;
1.2 常见的特征工程:
- 数据分桶:
等频分桶;
等距分桶;
Best-KS 分桶(类似利用基尼指数进行二分类);
卡方分桶;
- 特征构造:
构造统计量特征,报告计数、求和、比例、标准差等;
时间特征,包括相对时间和绝对时间,节假日,双休日等;
地理信息,包括分箱,分布编码等方法;
非线性变换,包括 log/ 平方/ 根号等;
特征组合,特征交叉;
- 特征筛选
过滤式(filter):先对数据进行特征选择,然后在训练学习器,常见的方法有 Relief/方差选择发/相关系数法/卡方检验法/互信息法;
包裹式(wrapper):直接把最终将要使用的学习器的性能作为特征子集的评价准则,常见方法有 LVM(Las Vegas Wrapper) ;
嵌入式(embedding):结合过滤式和包裹式,学习器训练过程中自动进行了特征选择,常见的有 lasso 回归;
降维
PCA/ LDA/ ICA;
特征选择也是一种降维。
接下来分享一些做的好的特征工程
2. 特征工程分享
首先是异常值处理
#异常值处理
def outliers_proc(data,col_name,scale=3):
'''
用于清洗异常值,默认用box_plot(scale=3)进行清洗
:param data:接受 pandas 数据格式
:param col_name:列名
:param scale:尺度
:return:'''
def box_plot_outlier(data_ser,box_scale):
'''
利用箱线图去除异常值
:param data_ser: 接受pandas.series 数据格式
:param box_scdale:箱线图尺度
:return:'''
iqr=box_scale*(data_ser.quantile(0.75)-data_ser.quantile(0.25))
#极差
val_low=data_ser.quantile(0.25)-iqr
val_up= data_up=data_ser.quantile(0.75)+iqr
rule_low=(data_ser<val_low)
rule_up=(data_ser>val_up)
return (rule_low,rule_up),(val_low,val_up)
data_n=data.copy()
data_series=data_n[col_name]
rule,value=box_plot_outlier(data_series,box_scale=scale)
index=np.arange(data_series.shape[0])[rule[0]|rule[1]]
print('删除的数字是:{}'.format(len(index)))
data_n=data_n.drop(index)
data_n.reset_index(drop=True,inplace=True)
print('现在这列有的数字:{}'.format(data_n.shape[0]))
index_low=np.arange(data_series.shape[0])[rule[0]]
outliers=data_series.iloc[index_low]
print('大于上限的数据的描述是:')
print(pd.Series(outliers).describe())
fig, ax = plt.subplots(1, 2, figsize=(10, 7))
sns.boxplot(y=data[col_name], data=data, palette="Set1", ax=ax[0])
sns.boxplot(y=data_n[col_name], data=data_n, palette="Set1", ax=ax[1])
return data_n
然后是数据分桶
也就是把连续的数据进行离散化切分,一类一类的。
为什么要做数据分桶呢,原因有很多
- 1 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
- 2 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
- 3 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;
- 4 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
分享一下其他人的特征工程,首先是天池某大佬的,亮点在于特征的二阶交叉。
from scipy.stats import entropy
feat_cols = []
### count编码
for f in tqdm([
'regDate', 'creatDate', 'regDate_year',
'model', 'brand', 'regionCode'
]):
df[f + '_count'] = df[f].map(df[f].value_counts())
feat_cols.append(f + '_count')
### 用数值特征对类别特征做统计刻画,随便挑了几个跟price相关性最高的匿名特征
for f1 in tqdm(['model', 'brand', 'regionCode']):
g = df.groupby(f1, as_index=False)
for f2 in tqdm(['v_0', 'v_3', 'v_8', 'v_12']):
feat = g[f2].agg({
'{}_{}_max'.format(f1, f2): 'max', '{}_{}_min'.format(f1, f2): 'min',
'{}_{}_median'.format(f1, f2): 'median', '{}_{}_mean'.format(f1, f2): 'mean',
'{}_{}_std'.format(f1, f2): 'std', '{}_{}_mad'.format(f1, f2): 'mad'
})
df = df.merge(feat, on=f1, how='left')
feat_list = list(feat)
feat_list.remove(f1)
feat_cols.extend(feat_list)
### 类别特征的二阶交叉
for f_pair in tqdm([
['model', 'brand'], ['model', 'regionCode'], ['brand', 'regionCode']
]):
### 共现次数
df['_'.join(f_pair) + '_count'] = df.groupby(f_pair)['SaleID'].transform('count')
### n unique、熵
df = df.merge(df.groupby(f_pair[0], as_index=False)[f_pair[1]].agg({
'{}_{}_nunique'.format(f_pair[0], f_pair[1]): 'nunique',
'{}_{}_ent'.format(f_pair[0], f_pair[1]): lambda x: entropy(x.value_counts() / x.shape[0])
}), on=f_pair[0], how='left')
df = df.merge(df.groupby(f_pair[1], as_index=False)[f_pair[0]].agg({
'{}_{}_nunique'.format(f_pair[1], f_pair[0]): 'nunique',
'{}_{}_ent'.format(f_pair[1], f_pair[0]): lambda x: entropy(x.value_counts() / x.shape[0])
}), on=f_pair[1], how='left')
### 比例偏好
df['{}_in_{}_prop'.format(f_pair[0], f_pair[1])] = df['_'.join(f_pair) + '_count'] / df[f_pair[1] + '_count']
df['{}_in_{}_prop'.format(f_pair[1], f_pair[0])] = df['_'.join(f_pair) + '_count'] / df[f_pair[0] + '_count']
feat_cols.extend([
'_'.join(f_pair) + '_count',
'{}_{}_nunique'.format(f_pair[0], f_pair[1]), '{}_{}_ent'.format(f_pair[0], f_pair[1]),
'{}_{}_nunique'.format(f_pair[1], f_pair[0]), '{}_{}_ent'.format(f_pair[1], f_pair[0]),
'{}_in_{}_prop'.format(f_pair[0], f_pair[1]), '{}_in_{}_prop'.format(f_pair[1], f_pair[0])
])
然后是统计编码,最大会利用数值特征。
from sklearn.model_selection import KFold
train_df = df[~df['price'].isnull()].reset_index(drop=True)
test_df = df[df['price'].isnull()].reset_index(drop=True)
### target encoding目标编码,回归场景相对来说做目标编码的选择更多,不仅可以做均值编码,还可以做标准差编码、中位数编码等
enc_cols = []
stats_default_dict = {
'max': train_df['price'].max(),
'min': train_df['price'].min(),
'median': train_df['price'].median(),
'mean': train_df['price'].mean(),
'sum': train_df['price'].sum(),
'std': train_df['price'].std(),
'skew': train_df['price'].skew(),
'kurt': train_df['price'].kurt(),
'mad': train_df['price'].mad()
}
### 暂且选择这三种编码
enc_stats = ['mean', 'std', 'mad']
skf = KFold(n_splits=5, shuffle=True, random_state=2020)
for f in tqdm(['model', 'brand', 'regionCode']):
enc_dict = {
}
for stat in enc_stats:
enc_dict['{}_target_{}'.format(f, stat)] = stat
train_df['{}_target_{}'.format(f, stat)] = 0
test_df['{}_target_{}'.format(f, stat)] = 0
enc_cols.append('{}_target_{}'.format(f, stat))
for i, (trn_idx, val_idx) in enumerate(skf.split(train_df, train_df['price'])):
trn_x, val_x = train_df.iloc[trn_idx].reset_index(drop=True), train_df.iloc[val_idx].reset_index(drop=True)
enc_df = trn_x.groupby(f, as_index=False)['price'].agg(enc_dict)
val_x = val_x[[f]].merge(enc_df, on=f, how='left')
test_x = test_df[[f]].merge(enc_df, on=f, how='left')
for stat in enc_stats:
val_x['{}_target_{}'.format(f, stat)] = val_x['{}_target_{}'.format(f, stat)].fillna(stats_default_dict[stat])
test_x['{}_target_{}'.format(f, stat)] = test_x['{}_target_{}'.format(f, stat)].fillna(stats_default_dict[stat])
train_df.loc[val_idx, '{}_target_{}'.format(f, stat)] = val_x['{}_target_{}'.format(f, stat)].values
test_df['{}_target_{}'.format(f, stat)] += test_x['{}_target_{}'.format(f, stat)].values / skf.n_splits
cols = cate_cols + date_cols + num_cols + feat_cols + enc_cols
sub = test_df[['SaleID']].copy()
test_df = test_df[cols]
labels = train_df['price'].values
train_df = train_df[cols]
print(train_df.shape)
train_df.head()
二次项特征构造,这个也是套路之一。二次项使用sklearn.preprocessing.PolynomialFeatures来进行特征的构造。它是使用多项式的方法来进行的,如果有a,b两个特征,那么它的2次多项式为(1,a,b,a^2,ab, b^2),这个多项式的形式就是是使用poly的效果。
# 二元项变换
feature_cols = [col for col in data.columns if col not in ['SaleID','price']]
poly_data = data[feature_cols]
poly = PolynomialFeatures(2)
poly_data_ndarray = poly.fit_transform(poly_data)
poly_data_final = pd.DataFrame(poly_data_ndarray)
poly_data_final.columns = poly.get_feature_names(poly_data.columns)
# 将二次转换过后的数据拼接到原数据集上
Merge_cols = ['price','SaleID']
poly_data_merge = data[Merge_cols]
data = poly_data_merge.merge(poly_data_final, how='left',right_index=True,left_index=True)
变量间的交互统计
# 构建统计量函数
def Group_Statistic(df,df2,feature,Target):
Train_gb = df.groupby(feature)
all_info = {
}
for kind, kind_data in Train_gb:
info = {
}
kind_data = kind_data[kind_data[Target] > 0]
#info[feature + '_unique'] = len(kind_data['feature'].unique())
#info[feature + '_amount'] = len(kind_data)
info[feature + '_' + Target + '_max'] = kind_data[Target].max()
info[feature + '_' + Target + '_median'] = kind_data[Target].median()
info[feature + '_' + Target + '_min'] = kind_data[Target].min()
info[feature + '_' + Target + '_sum'] = kind_data[Target].sum()
info[feature + '_' + Target + '_std'] = kind_data[Target].std()
info[feature + '_' + Target + '_average'] = round(kind_data[Target].sum() / (len(kind_data) + 1), 2)
all_info[kind] = info
brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={
'index': feature})
return df2.merge(brand_fe, how='left', on=feature)
# 多个特征建的交互
data_final = Group_Statistic(train_set,data_final,'regionCode','years')
data_final = Group_Statistic(train_set,data_final,'regionCode','months')
data_final = Group_Statistic(train_set,data_final,'regionCode','days')
data_final = Group_Statistic(train_set,data_final,'regionCode','kilometer')
data_final = Group_Statistic(train_set,data_final,'regionCode','power')
data_final = Group_Statistic(train_set,data_final,'name','years')
data_final = Group_Statistic(train_set,data_final,'name','months')
data_final = Group_Statistic(train_set,data_final,'name','days')
data_final = Group_Statistic(train_set,data_final,'name','kilometer')
data_final = Group_Statistic(train_set,data_final,'name','power')
data_final = Group_Statistic(train_set,data_final,'model','years')
data_final = Group_Statistic(train_set,data_final,'model','months')
data_final = Group_Statistic(train_set,data_final,'model','days')
data_final = Group_Statistic(train_set,data_final,'model','kilometer')
data_final = Group_Statistic(train_set,data_final,'model','power')
data_final = Group_Statistic(train_set,data_final,'brand','years')
data_final = Group_Statistic(train_set,data_final,'brand','months')
data_final = Group_Statistic(train_set,data_final,'brand','days')
data_final = Group_Statistic(train_set,data_final,'brand','kilometer')
data_final = Group_Statistic(train_set,data_final,'brand','power')