背景
在这次Datawhale的组队学习中,我们主要学习数据竞赛的相关知识,其中task3是有关于特征工程的知识。
特征工程
数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。
从上面这句流传已久的说法中,不难看出特征工程在机器学习中起到的巨大作用。下面就来简单介绍一下什么是特征工程。
根据维基百科的介绍(链接在这), 特征工程是利用某个领域的知识,通过数据挖掘技术从原始数据中提取特征的过程。这些特征可以用来提高机器学习算法的性能。特征工程可以看作是应用机器学习算法的过程。
简而言之,就是将原始数据转化为各种特征,用来作为模型的输入。由此看出,在模型相同的情况下,构建好的特征有利于模型得到更好的输出,获得更好的效果。这也体现出特征工程的重要性。
具体可以参考:细说特征工程
特征工程的常见内容
根据Datawhale学习手册中作者阿泽的介绍,常见的内容如下:
1. 异常处理
- 通过箱型图(box plot)分析删除异常值
- BOX-COX转换(处理有偏分布)
- 长尾截断
2. 特征归一化/标准化
- 标准化(近似正态可用,转换为标准正态分布)
- 归一化(转换到 [0, 1] 区间内)
- 针对幂律分布,可以采用公式
3. 数据分桶
- 等频分桶
- 等距分桶
- Best-KS 分桶
- 卡方分桶
4. 缺失值处理
- 不处理 (针对类似于XGBoost等树模型)
- 删除(缺失数据太多)
- 插值补全(均值/ 中位数/ 众数/ 建模预测/ 多重插补/ 压缩感知补全/ 矩阵补全等)
- 分箱,把缺失值分到一个箱
5. 特征构造
- 构造统计量特征,报告计数、求和、比例、标准差等
- 时间特征,包括相对时间和绝对时间,节假日、双休日等
- 地理信息,包括分箱、分布编码等
- 非线性变换,包括 log/ 平方/ 根号等
- 特征组合,特征交叉
- 仁者见仁,智者见智
6. 特征筛选
- 过滤式(filter):先对数据进行特征选择,然后再训练学习器,常见的方法有Relief/ 方差选择法/ 相关系数法/ 卡方检验法/ 互信息法
- 包裹式(wrapper):直接把最终将要使用的学习器的性能作为特征子集的评价准则,常见方法有LVM(Las Vegas Wrapper)
- 嵌入式(embedding):结合过滤式和包裹式,学习器训练过程中自动进行了特征选择,常见的有 lasso回归
7. 降维
- PCA/ LDA/ ICA
- 特征选择也是一种降维
具体代码
Notebook内容链接:Notebook代码
通过df.describe()可以查看数据的概况,但是列数过多可能会省略几列,那么如果要查看全部列的话,可以添加这一句代码
pd.set_option('display.max_columns', None)
而df.head()只能查看前5行,那么通过以下代码可以查看首尾5行(上一篇也有提到过)(train是一个DataFrame):
train.head().append(train.tail())
通过train.columns 可以查看列索引,类型为Index。再调用 tolist()可以转化为列表:
train.columns
Index(['SaleID', 'name', 'regDate', 'model', 'brand', 'bodyType', 'fuelType',
'gearbox', 'power', 'kilometer', 'notRepairedDamage', 'regionCode',
'seller', 'offerType', 'creatDate', 'price', 'v_0', 'v_1', 'v_2', 'v_3',
'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12',
'v_13', 'v_14'],
dtype='object')
train.columns.tolist()
['SaleID', 'name', 'regDate', 'model', 'brand', 'bodyType', 'fuelType', 'gearbox', 'power', 'kilometer',
'notRepairedDamage', 'regionCode', 'seller', 'offerType', 'creatDate', 'price', 'v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6',
'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14']
阿泽大佬在手册中写了一个删除异常值的代码,其原理是利用箱型图中的异常处理机制,将3IQR以外的值删除(又称极端异常值),其中IQR为0.75分位点与0.25分位点的差值。
函数如下,我稍微加了一点注释:
# 异常值处理的函数,用箱型图去除极端异常值(在3IQR(外限)之外)
def processing_outliers(data, col_name, scale=3):
def boxplot_outliers(data_Series, box_scale):
iqr = box_scale * (data_Series.quantile(0.75) - data_Series.quantile(0.25))
val_low = data_Series.quantile(0.25) - iqr
val_up = data_Series.quantile(0.75) + iqr
rule_low = (data_Series < val_low) # 返回的是True 或者 FALSE
rule_up = (data_Series > val_up)
return (rule_low, rule_up), (val_low, val_up)
data_tmp = data.copy()
data_series = data_tmp[col_name]
rule, value = boxplot_outliers(data_series, box_scale=scale)
# 骚操作,用bool序列筛选data_series的值
index = np.arange(data_series.shape[0])[rule[0] | rule[1]]
# index 就是那些异常值的索引,大于up 或者 小于low
print("Total deleted number is: {} ".format(len(index)))
# 打印删除的样本个数
data_tmp = data_tmp.drop(index)
data_tmp.reset_index(drop=True, inplace=True)
print("The remaining number is: {}".format(data_tmp.shape[0]))
# 给超出下界的样本打印一个describe
index_low = np.arange(data_series.shape[0])[rule[0]]
# 7 iloc loc ix https://www.cnblogs.com/smartmsl/p/11161027.html
outliers = data_series.iloc[index_low]
print("Description of the data below the {} IQR bound is:".format(scale))
print(outliers.describe())
index_up = np.arange(data_series.shape[0])[rule[1]]
outliers = data_series.iloc[index_up]
print("Description of data above the {} IQR bound is:".format(scale))
print(outliers.describe())
fig, ax = plt.subplots(1, 2, figsize=(10, 7))
sns.boxplot(y=data[col_name], data=data, ax=ax[0])
ax[0].set_title('before')
sns.boxplot(y=data_tmp[col_name], data=data_tmp, ax=ax[1])
ax[1].set_title('after')
return data_tmp
这段代码有几个值得注意的地方:
筛选条件时,可以对Series取布尔值:
se = pd.Series([1, 2, 3, 4, 5])
a = (se > 2) # 假设规则就是只保留大于2的值
a
返回:
0 False
1 False
2 True
3 True
4 True
dtype: bool
返回的bool值使后续的索引筛选更加方便,这也是numpy数组的一个特点或者优点:
idx = np.arange(3)
d = [True, False, True] # 假设d就是那个处理后的bool序列
idx[d]
array([0, 2])
由此,我们可以根据自己的需要设置不同的规则,十分方便!
另外函数df.reset_index()(重置行索引,尤其是drop删除了几行后)里面的两个参数也值得留意,inplace参数比较好理解,为True的话意思为覆盖原实例,不创建新对象,那么drop=True则可以避免多出一个index列:
默认情况下,drop=False,多了一个原索引列:
设置drop=True后,index列就没有了:
class max_speed
0 bird 389.0
1 bird 24.0
2 mammal NaN
函数最后会输出处理前与处理后的箱型图图像,如图:
可见数据分布得到了优化,箱型图的“箱子”也能看到了!
总结
由于时间原因,目前这篇文章代码部分只展示了一部分成果,还待进一步更新。