文章目录

  • 常见方法
  • 1.异常处理
  • 2.特征归一化/标准化
  • 3.数据分桶
  • 4.缺失值处理
  • 5.特征构造
  • 6.特征筛选
  • 7.特征降维
  • 8.代码演练
  • 9.经验总结


常见方法

1.异常处理

  • 通过箱线图分析删除异常值
  • BOX-COX转换(处理有偏分布):
     是统计建模中常用的一种数据变换,用于连续的响应变量不满足正态分布的情况。Box-Cox变换之后,可以一定程度上减小不可观测的误差和预测变量的相关性。
  • 长尾截断

2.特征归一化/标准化

  • 标准正态变化,最大最小变换等
  • 针对幂律分布,可以采用公式: 长尾数据回归问题_长尾数据回归问题

3.数据分桶

  对于LR,NN,SVM等距离度量模型,有时候需要对连续变量离散化,离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1,否则0。如果特征没有离散化,一个异常数据“年龄300岁”会给模型造成很大的干扰;

  • 等频分桶:按照每个区间相同样本数量将连续变量划分为不同区间
  • 等距分桶:按照相同变量值间隔划分为不同区间
  • Best-KS 分桶(类似利用基尼指数进行二分类);
  • 卡方分桶;

4.缺失值处理

  • 基于树模型的可以不对缺失值处理,其内部能自动处理带缺失值的样本集。如通过对未缺失的样本按照规则划分后,再将缺失的样本复制两份送入左右子树
  • 删除(缺失数据太多);可将缺失数据的那个样本删除
  • 插值补全,包括均值/中位数/众数/建模预测/多重插补/压缩感知补全/矩阵补全等;
  • 分箱,缺失值一个箱;

5.特征构造

  • 构造统计量特征,报告计数、求和、比例、标准差等;如此次二手车价格预测实例的brand均值、最大最小值等
  • 时间特征,包括相对时间和绝对时间,节假日,双休日等;如此次实例的使用时间构造
  • 地理信息,包括分箱,分布编码等方法;如此次车牌信息构造城市信息特征
  • 非线性变换,包括 log/ 平方/ 根号等;
  • 特征组合,特征交叉;
  • 仁者见仁,智者见智。

6.特征筛选

  • 过滤式(filter):先对数据进行特征选择,然后在训练学习器,常见的方法有 Relief/方差选择发/相关系数法/卡方检验法/互信息法;
  • 包裹式(wrapper):直接把最终将要使用的学习器的性能作为特征子集的评价准则,常见方法有 LVM(Las Vegas Wrapper) ;
  • 嵌入式(embedding):结合过滤式和包裹式,学习器训练过程中自动进行了特征选择,常见的有 lasso 回归;

7.特征降维

  • PCA/ LDA/ ICA;
  • 特征选择也是一种降维。

8.代码演练

  • 1.导入数据
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
from operator import itemgetter #python 运算符模块

path='./data/'
train=pd.read_csv(path+'used_car_train.csv',sep=' ') #默认即是空格
test=pd.read_csv(path+'used_car_testA.csv',sep=' ')
  • 2.通过箱线图分析数据并剔除异常值
    箱线图通过计算数据四分位值v1和四分之三位值v2计算IQR=v2-v1,并通过IQR判定上边缘 up=v2+nIQR,和下边缘 down=v1+nIQR
def outliers_proc(data, col_name, scale=3):
    """
    用于清洗异常值,默认用 box_plot(scale=3)进行清洗
    :param data: 接收 pandas 数据格式
    :param col_name: pandas 列名
    :param scale: 尺度
    :return:
    """

    def box_plot_outliers(data_ser, box_scale):
        """
        利用箱线图去除异常值
        :param data_ser: 接收 pandas.Series 数据格式
        :param box_scale: 箱线图尺度,
        :return:
        """
        iqr = box_scale * (data_ser.quantile(0.75) - data_ser.quantile(0.25)) #计算iqr
        val_low = data_ser.quantile(0.25) - iqr #计算下边界
        val_up = data_ser.quantile(0.75) + iqr  #计算上边界
        rule_low = (data_ser < val_low)        #得出比下边界还低的bool类型数组
        rule_up = (data_ser > val_up)          #得到比上边界还高的bool类型数组
        return (rule_low, rule_up), (val_low, val_up)

    data_n = data.copy()               
    data_series = data_n[col_name]      #选定要清洗的列
    rule, value = box_plot_outliers(data_series, box_scale=scale)     
    index = np.arange(data_series.shape[0])[rule[0] | rule[1]] #生成需要清除的行的Index的集合
    print("Delete number is: {}".format(len(index)))
    data_n = data_n.drop(index)         #将index集合里面索引对应的行删除
    data_n.reset_index(drop=True, inplace=True)      #重新排列DataFrame的index
    print("Now column number is: {}".format(data_n.shape[0]))
    index_low = np.arange(data_series.shape[0])[rule[0]]   #生成比下界低的值的index的集合
    outliers = data_series.iloc[index_low]     #取出这些值生成一个Series
    print("Description of data less than the lower bound is:")
    print(pd.Series(outliers).describe())
    index_up = np.arange(data_series.shape[0])[rule[1]]
    outliers = data_series.iloc[index_up]
    print("Description of data larger than the upper bound is:")
    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

train_proc= outliers_proc(train, 'power', scale=3)

长尾数据回归问题_数据挖掘_02


左图为原始箱型图,分析可以看出由于偏离中位值的异常大点很多,导致箱型被压扁,所以通过上边缘剔除异常值后的箱型图就如右图较正常了。

  • 3.分析特征的关系,构造新特征
# 训练集和测试集放在一起,方便构造特征
train_proc['train']=1
test['train']=0
data = pd.concat([train_proc, test], ignore_index=True, sort=False)

# 使用时间:data['creatDate'] - data['regDate'],反应汽车使用时间,一般来说价格与使用时间成反比
# 不过要注意,数据里有时间出错的格式,所以我们需要 errors='coerce'
data['used_time'] = (pd.to_datetime(data['creatDate'], format='%Y%m%d', errors='coerce') - 
                            pd.to_datetime(data['regDate'], format='%Y%m%d', errors='coerce')).dt.days
# 看一下空数据,有 15k 个样本的时间是有问题的,我们可以选择删除,也可以选择放着。
# 但是这里不建议删除,因为删除缺失数据占总样本量过大,7.5%
# 我们可以先放着,因为如果我们 XGBoost 之类的决策树,其本身就能处理缺失值,所以可以不用管;
data['used_time'].isnull().sum()

# 从邮编中提取城市信息,因为是德国的数据,所以参考德国的邮编,相当于加入了先验知识
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])
#apply 函数 pandas中可以自定义对DataFrame中每一行series处理的函数,返回每一行处理后结果的series



# 计算某品牌的销售统计量,同学们还可以计算其他特征的统计量
# 这里要以 train 的数据计算统计量,带有price特征

train_gb = train.groupby("brand")
#将训练数据以brand特征分组,得到group对象,可对这个对象处理快速获取分组的统计信息
#train_gb为一个字典,键是分组特征的具体名,值是一个子DataFrame
all_info={}
for kind,kind_data in train_gb: #kind_data为brand特征分支的每一个值对应的一个DataFrame
    info={} #存储brand特征每一个分支的统计量特征的列表
    kind_data=kind_data[kind_data['price']>0] #通过对DataFrame中的某一列特征的筛选构造一个新的DataFrame
    info['brand_amount'] = len(kind_data)
    info['brand_price_max'] = kind_data.price.max()
    info['brand_price_median'] = kind_data.price.median()
    info['brand_price_min'] = kind_data.price.min()
    info['brand_price_sum'] = kind_data.price.sum()
    info['brand_price_std'] = kind_data.price.std()
    info['brand_price_average'] = round(kind_data.price.sum() / (len(kind_data) + 1), 2)
    all_info[kind] = info  #构造以brand特征子值为键,info信息为值的all_info列表

brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"}) #T为转置,并将index列命名为brand
data = data.merge(brand_fe, how='left', on='brand') #pd.merge() 合并pandas,方式是左连接,on:用于连接的列名
  • 4.数据分桶
    对powe做数据分桶,因为可以分析得到功率比较小的变化比如100和105应该对售价的影响不大。而且数据分桶后再通过独热编码可提高LR等模型的表达能力,加快计算,而且引入非线性,还可以进一步构造交叉特征
##对Power做分桶
bin=[i*10 for i in range(31)] #设置分桶的距离,等距分桶?pd.cut
data['power_bin']=pd.cut(data['power'],bin,labels=False)
  • 5.长尾分布的处理
    针对power有很多很大的离群点,也可以对其做log变化使得长尾分布偏向正态分布
from sklearn import preprocessing
min_max_scaler = preprocessing.MinMaxScaler()
data['power'] = np.log(data['power'] + 1) 
#最大最小归一化
data['power'] = ((data['power'] - np.min(data['power'])) / (np.max(data['power']) - np.min(data['power'])))
data['power'].plot.hist()

剔除异常值后的Power直方图

长尾数据回归问题_长尾数据回归问题_03


log转化并归一化后的power统计直方图,可以看到效果接近与正态分布了

长尾数据回归问题_数据挖掘_04

  • 6.对类别特征进行独热编码
     类别特征为什么要进行独热编码呢,转换成离散数字特征不就行了吗?我的理解是对于树模型这样就可以了,但是对于距离度量的模型如LR,损失函数需要用到参数和特征的值相乘得到预测值。那么对于类别特征只编码成数字1,2,3,4,5,的话,5这个特征乘参数会比1这个特征乘参数大得多,这是不对的,因为5跟1只是一个类别的不同,2跟1也只是一个类别的不同,但是他们的相差就不一致,这就引起了Bias。所以需要独热编码,同时独热编码让一个变量变成了多个变量,就引入了非线性等等好处。
# 对类别特征进行 OneEncoder
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',
                                     'gearbox', 'notRepairedDamage', 'power_bin'])
#pd.dummies 即是one_Hot编码中的哑变量编码,即全0也代表一个特征
  • 7.特征筛选
    直接上代码
#1.过滤式,利用相关系数,直接得到,或者画图

print(data['power'].corr(data['price'], method='spearman'))
print(data['kilometer'].corr(data['price'], method='spearman'))
print(data['brand_amount'].corr(data['price'], method='spearman'))
print(data['brand_price_average'].corr(data['price'], method='spearman'))
print(data['brand_price_max'].corr(data['price'], method='spearman'))
print(data['brand_price_median'].corr(data['price'], method='spearman'))
# 当然也可以直接看图
data_numeric = data[['power', 'kilometer', 'brand_amount', 'brand_price_average', 
                     'brand_price_max', 'brand_price_median']]
correlation = data_numeric.corr()

f , ax = plt.subplots(figsize = (7, 7))
plt.title('Correlation of Numeric Features with Price',y=1,size=16)
sns.heatmap(correlation,square = True,  vmax=0.8)


a=data.head()





#2 包裹式

# k_feature 太大会很难跑,没服务器,所以提前 interrupt 了
#from mlxtend.feature_selection import SequentialFeatureSelector as SFS
#from sklearn.linear_model import LinearRegression
#sfs = SFS(LinearRegression(),
#           k_features=10,
#           forward=True,
#           floating=False,
#           scoring = 'r2',
#           cv = 0)
#x = data.drop(['price'], axis=1)
#x = x.fillna(0)
#y = data['price']
#sfs.fit(x, y)
#sfs.k_feature_names_ 


# 画出来,可以看到边际效益
#from mlxtend.plotting import plot_sequential_feature_selection as plot_sfs
#import matplotlib.pyplot as plt
#fig1 = plot_sfs(sfs.get_metric_dict(), kind='std_dev')
#plt.grid()
#plt.show()


# 下一章介绍,Lasso 回归和决策树可以完成嵌入式特征选择
# 大部分情况下都是用嵌入式做特征筛选

各个特征之间以及他们和price间的相关系数热点图,分析热点图即可知道相关性强弱

长尾数据回归问题_缺失值_05


边界效益图

长尾数据回归问题_长尾数据回归问题_06

9.经验总结

特征工程是比赛中最至关重要的的一块,特别的传统的比赛,大家的模型可能都差不多,调参带来的效果增幅是非常有限的,但特征工程的好坏往往会决定了最终的排名和成绩。

特征工程的主要目的还是在于将数据转换为能更好地表示潜在问题的特征,从而提高机器学习的性能。比如,异常值处理是为了去除噪声,填补缺失值可以加入先验知识等。

特征构造也属于特征工程的一部分,其目的是为了增强数据的表达。

有些比赛的特征是匿名特征,这导致我们并不清楚特征相互直接的关联性,这时我们就只有单纯基于特征进行处理,比如装箱,groupby,agg 等这样一些操作进行一些特征统计,此外还可以对特征进行进一步的 log,exp 等变换,或者对多个特征进行四则运算(如上面我们算出的使用时长),多项式组合等然后进行筛选。由于特性的匿名性其实限制了很多对于特征的处理,当然有些时候用 NN 去提取一些特征也会达到意想不到的良好效果。

对于知道特征含义(非匿名)的特征工程,特别是在工业类型比赛中,会基于信号处理,频域提取,丰度,偏度等构建更为有实际意义的特征,这就是结合背景的特征构建,在推荐系统中也是这样的,各种类型点击率统计,各时段统计,加用户属性的统计等等,这样一种特征构建往往要深入分析背后的业务逻辑或者说物理原理,从而才能更好的找到 magic。

当然特征工程其实是和模型结合在一起的,这就是为什么要为 LR NN 做分桶和特征归一化的原因,而对于特征的处理效果和特征重要性等往往要通过模型来验证。

总的来说,特征工程是一个入门简单,但想精通非常难的一件事。

———这段话来自DataWhale 阿泽