零基础入门数据挖掘 - 数据的特征工程


数据和特征决定了机器学习的上限,而模型和算法只是在尽力逼近这个上限,因此特征工程是机器学习成功的关键。文章背景来自天池实验室的数据挖掘比赛 零基础入门数据挖掘 - 二手车交易价格预测。本文将在之前 赛题理解EDA数据探索性分析工作的基础上,进一步对特征进行分析,并对数据进行处理,为后续的模型搭建做准备。

常见的特征工程

该部分主要包括数据预处理和特征工程部分,数据预处理包括缺失值,异常值的处理,连续值与离散值的选择,数值型特征的归一化/标准化以及类别型特征的encode。特征工程部分包括了特征的构造、特征筛选、特征降维提取等,目的在与尽可能用小的特征集发挥特征的最大价值。
在这里插入图片描述
在这里插入图片描述

1、异常处理

首先完成数据的基本准备工作

#coding:utf-8
import warnings #导入warnings包,利用过滤器来实现忽略警告语句。
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
#import missingno as msno 
from operator import itemgetter
%matplotlib inline
## 1) 载入训练集和测试集;
path = 'C:/Users/Kingfish/Desktop/TianChi/'
train = pd.read_csv(path+'train.csv', sep=' ')   
test = pd.read_csv(path+'testA.csv', sep=' ') 
## 2) 简略观察训练街和测试集数据(head()+shape)
train.head().append(train.tail())
test.head().append(test.tail())
train.shape
test.shape
## 3) 通过describe()来熟悉数据的相关统计量
train.describe()
test.describe()
## 4) 通过info()来熟悉数据类型
train.info()
test.info()
train.columns
test.columns

异常值可以通过箱线图(或 3-Sigma)分析删除异常值,或者长尾截断。这里是一个包装好了的异常值处理的代码,"iqr = box_scale * (data_ser.quantile(0.75) - data_ser.quantile(0.25))"含义是定义iqr为(上四分位数-下四分位数)*scale

# 这里是一个包装好了的异常值处理的代码,可以直接调用。
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))
        val_low = data_ser.quantile(0.25) - iqr
        val_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_outliers(data_series, box_scale=scale)
    index = np.arange(data_series.shape[0])[rule[0] | rule[1]]
    print("Delete number is: {}".format(len(index)))
    data_n = data_n.drop(index)
    data_n.reset_index(drop=True, inplace=True)
    print("Now column number is: {}".format(data_n.shape[0]))
    index_low = np.arange(data_series.shape[0])[rule[0]]
    outliers = data_series.iloc[index_low]
    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 = outliers_proc(train, 'v_5', scale=3)

以数值变量V_5为例,输出的结果包括修正之前和修正之后的箱线图。
在这里插入图片描述
注意点:
1、此处使用的数据只有训练集,不是整体数据即不包括验证集。
2、训练集的分布和整个数据集的分布不一致,因为验证集依然存在异常值
在这里插入图片描述
在这里插入图片描述
所以通常会不直接剔除异常值,而是截断,之后再对数值型特征取对数变化、归一化等。

2、缺失值处理

缺失值常用的处理方式主要3种,此处不做赘述。
(1)不处理(针对类似 XGBoost 等树模型);
(2)删除(缺失数据太多);
(3)插值补全,包括均值/中位数/众数/建模预测/多重插补/压缩

3、特征构造

本文这里介绍了三种特殊构造:
(1)时间特征。
(2)地理信息。
(3)构造统计量特征,报告计数、求和、比例、标准差等。
当然也可以通过非线性变换,包括 log/ 平方/ 根号等进行特征构造。

# 训练集和测试集放在一起,方便构造特征
train['train']=1
test['train']=0
data = pd.concat([train, 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'))
# 看一下空数据,有 15k 个样本的时间是有问题的,我们可以选择删除,也可以选择放着。
# 但是这里不建议删除,因为删除缺失数据占总样本量过大,7.5%
# 我们可以先放着,因为如果我们 XGBoost 之类的决策树,其本身就能处理缺失值,所以可以不用管;
data['used_time'].isnull().sum()
# 从邮编中提取城市信息
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])
# 计算某品牌的销售统计量,同学们还可以计算其他特征的统计量
# 这里要以 train 的数据计算统计量
train_gb = train.groupby("brand")
all_info = {}
for kind, kind_data in train_gb:
    info = {}
    kind_data = kind_data[kind_data['price'] > 0]
    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_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"})
data = data.merge(brand_fe, how='left', on='brand')

上文代码中利用终止时间和起始时间之差构造了使用时长特征;通过邮编提取出二手车所在区域的城镇位置信息,增加了先验知识;考虑到不同二手车品牌对价格的影响显著,构造了品牌价格的平均值、标准差等。

4、数据分桶

数据分桶可以等频分桶、等距分桶、Best-KS 分桶(类似利用基尼指数进行二分类、卡方分桶等方式。

  1. 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
  2. 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的
    3 LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能
  3. 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了
  4. 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
    当然还有很多原因,LightGBM 在改进 XGBoost 时就增加了数据分桶,增强了模型的泛化性
bin = [i*10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels=False)
data[['power_bin', 'power']].head()
# 利用好了,就可以删掉原始数据了
data = data.drop(['creatDate', 'regDate', 'regionCode'], axis=1)
print(data.shape)
data.columns
# 目前的数据其实已经可以给树模型使用了,所以我们导出一下
data.to_csv('data_for_tree.csv', index=0)

5、数值型特征归一化/标准化

标准化是指转换为标准正态分布,归一化是指抓换到 [0,1] 区间;

train['power'].plot.hist()
test['power'].plot.hist()
# 我们对其取 log,在做归一化
from sklearn import preprocessing
train['power'] = np.log(train['power'] + 1)
train['power'] = ((train['power'] - np.min(train['power'])) / (np.max(train['power']) - np.min(train['power'])))
train['power'].plot.hist()
# km 的比较正常,应该是已经做过分桶了
data['kilometer'].plot.hist()
# 所以我们可以直接做归一化
data['kilometer'] = ((data['kilometer'] - np.min(data['kilometer'])) /
(np.max(data['kilometer']) - np.min(data['kilometer'])))
data['kilometer'].plot.hist()

这里通常取对数加1,例如“train[‘power’] = np.log(train[‘power’] + 1)”,"round(kind_data.price.sum() / (len(kind_data) + 1), 2)"是为了防止分母为0。
对于power变量是长尾异常值截断,取对数之后归一化处理。
在这里插入图片描述
km变量应该是已经分桶过了,可以直接进行归一化处理。
在这里插入图片描述

# 除此之外 还有我们刚刚构造的统计量特征:
# 'brand_amount', 'brand_price_average', 'brand_price_max',
# 'brand_price_median', 'brand_price_min', 'brand_price_std',
# 'brand_price_sum'
# 这里不再一一举例分析了,直接做变换,
def max_min(x):
    return (x - np.min(x)) / (np.max(x) - np.min(x))

data['brand_amount'] = ((data['brand_amount'] - np.min(data['brand_amount'])) /
                        (np.max(data['brand_amount']) - np.min(data['brand_amount'])))
data['brand_price_average'] = ((data['brand_price_average'] - np.min(data['brand_price_average'])) /
                               (np.max(data['brand_price_average']) - np.min(data['brand_price_average'])))
data['brand_price_max'] = ((data['brand_price_max'] - np.min(data['brand_price_max'])) /
                           (np.max(data['brand_price_max']) - np.min(data['brand_price_max'])))
data['brand_price_median'] = ((data['brand_price_median'] - np.min(data['brand_price_median'])) /
                              (np.max(data['brand_price_median']) - np.min(data['brand_price_median'])))
data['brand_price_min'] = ((data['brand_price_min'] - np.min(data['brand_price_min'])) /
                           (np.max(data['brand_price_min']) - np.min(data['brand_price_min'])))
data['brand_price_std'] = ((data['brand_price_std'] - np.min(data['brand_price_std'])) /
                           (np.max(data['brand_price_std']) - np.min(data['brand_price_std'])))
data['brand_price_sum'] = ((data['brand_price_sum'] - np.min(data['brand_price_sum'])) /
                           (np.max(data['brand_price_sum']) - np.min(data['brand_price_sum'])))

6、类别特征encode

# 对类别特征进行 OneEncoder
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',
                                   'gearbox', 'notRepairedDamage', 'power_bin'])

7、特征筛选

在一个数据集中,存在大量的特征可使用,但实际上可能部分特征携带的信息丰富,部分特征之间信息有重叠,部分特征对预测目标没有相关性,如果不对特征筛选地全部作为训练特征,经常会出现维度灾难问题,甚至会降低模型的准确性。因此,特征筛选可以排除无效/冗余的特征,进一步挑选出有价值的特征参与模型训练。常用的特征筛选的方式主要有三种,包括过滤式(filter)、包裹式(wrapper)、嵌入式 (Embedded)。

7.1 Filter方法(过滤式)

过滤式(filter):先对数据进行特征选择,然后再训练学习器,征选择的过程与学习器无关,相当于先对特征进行过滤操作,然后用特征子集来训练分类器。
主要思想:对每一维特征“打分”,即给每一维的特征赋予权重,这样的权重就代表着该特征的重要性,然后依据权重排序。
主要方法:
Chi-squared test(卡方检验)
Information gain(信息增益)
Correlation coefficient scores(相关系数)
优点:运行速度快,是一种非常流行的特征选择方法。
缺点:无法提供反馈,特征选择的标准/规范的制定是在特征搜索算法中完成,学习算法无法向特征搜索算法传递对特征的需求。另外,可能处理某个特征时由于任意原因表示该特征不重要,但是该特征与其他特征结合起来则可能变得很重要。

#过滤式特征筛选
# 相关性分析
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)

皮尔森相关系数是一种最简单的,能帮助理解特征和响应变量之间关系的方法,该方法衡量的是变量之间的线性相关性。主要用于连续型特征的筛选,不适用于离散型特征的筛选。此处没有用皮尔森相关系数是因为皮尔逊相关系数需要变量服从正态分布,所以采用斯皮尔曼相关系数。根据相关系数绝对值的结果可以判断相关性并进行选择,也可以进一步的可视化,不过个人认为数值看起来更加直观。

7.2 Wrapper方法(封装式)

包裹式(wrapper)主要思想:将子集的选择看作是一个搜索寻优问题,生成不同的组合,对组合进行评价,再与其他的组合进行比较。这样就将子集的选择看作是一个优化问题。例如SFS,SBS方法。
优点:对特征进行搜索时围绕学习算法展开的,对特征选择的标准/规范是在学习算法的需求中展开的,能够考虑学习算法所属的任意学习偏差,从而确定最佳子特征,真正关注的是学习问题本身。由于每次尝试针对特定子集时必须运行学习算法,所以能够关注到学习算法的学习偏差/归纳偏差,因此封装能够发挥巨大的作用。
缺点:运行速度远慢于过滤算法,实际应用用封装方法没有过滤方法流行。

!pip install mlxtend
# 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()
7.3 Embedded方法(嵌入式)

将特征选择嵌入到模型训练当中,例如lasso回归、随机森林算法中部分特征集的选择,其训练可能是相同的模型,但是特征选择完成后,还能给予特征选择完成的特征和模型训练出的超参数,再次训练优化。
优点:对特征进行搜索时围绕学习算法展开的,能够考虑学习算法所属的任意学习偏差。训练模型的次数小于Wrapper方法,比较节省时间。
缺点:运行速度慢。

总结

(1)不同模型对数据集的要求不同,特征工程的任务也不尽相同。树模型通常会需要考虑处理异常值、特征构造、数据分桶。LR、NN模型需要对数值型特征进一步归一化/标准化处理,分类特征的encode。
(2)由于树模型的兼容性比较好,类似xgboost不用异常值处理,当然为了提升模型的效率、稳健性可以对缺失值进行删除或者填充处理。
(3)特征选择的原则是获取尽可能小的特征子集,不显著降低分类精度、不影响分类分布以及特征子集应具有稳定、适应性强等特点。
部分参考:
1、特征筛选的原理与实现(上)
2、Datawhale 零基础入门数据挖掘-Task3 特征工程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值