Fork me on GitHub

机器学习算法竞赛实战-特征工程

机器学习算法竞赛实战:特征工程

决定模型好坏的一个重要工作就是:特征工程

机器学习在本质还是特征,数据和特征决定了机器学习的上限,模型和算法知识逼近这个上限而已。

特征工程介于数据和算法之间,常见的特征工程分为:

  • 数据预处理
  • 特征转换
  • 特征提取
  • 特征选择

数据预处理

缺失处处理

缺失值的表现为NaN,NA,None,还有其他用于表示数值缺失的特殊数值。

如果是少量可接受的比例,可以考虑直接删除;一般是使用填充方式:

  • 对于类别型特征:填充众数;或者直接填充一个新类别
  • 针对数值特征:可以填充均值、众数、中位数等
  • 针对有序数据:可以填充相邻值(next或者previous)
  • 模型预测填充:通过回归模型进行预测填充

异常值处理

  1. 定位异常值:可视化方法、统计分析等方法

  2. 处理异常值:

    • 删除异常值
    • 将异常值视为缺失值
    • 填充均值或者中位数
    • 不处理,使用异常值直接建模

优化内存

  1. python的内存回收机制:通过gc.collect来释放内存
  2. 数值类型优化:将pandas读取的数据转成numpy数组;使用不同的数值类型,比如float16,float32,float64等
1
2
3
4
5
6
7
# 使用np.iinfo查看每个int类型的最小值和最大值

import numpy as np
import pandas as pd

import warnings
warnings.filterwarnings("ignore")
1
np.iinfo(np.int8).min
-128
1
np.iinfo(np.int8).max
127

通过特征的最大值和最小值来判断其所属的字类型:

1
2
3
4
5
c_min = df[col].min()
c_max = df[col].max()

if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)

特征转换

连续变量无量纲化

无量纲化:将不同规格的数据转换到同一个规格,两种方法:标准化和缩放法

标准化:前提是特征值服从正态分布;标准化后,特征值服从标准正态分布。

$$x_{new}=\frac{x-\mu}{\delta}$$

缩放法:利用边界值信息,将特征值缩放某个范围,比如[0,1]或者[-1,1]

$$X_{norm} = \frac{X-X_{min}}{X_{max} - X_{min}}$$

连续变量数据转换

  1. log转换

取对数log转换可以将倾斜数据变得接近正态分布,一般是使用log(x+1),其中加1是防止数据等于0,同时保证x是正的。

取对数不会改变数据的性质和相关关系。但是压缩了变量的尺度,不仅数据更加平稳,还削弱了模型的共线性、异方差性等。

  1. cbox-cox变换

cbox-cox变换:自动寻找最佳正态分布变换函数的方法

  1. 连续变量离散化
    离散化后的特征对异常数据有很强的鲁棒性。比如年龄的离散化:将年龄大于30岁视为1,否则视为0。如果没有离散化,数据中异常值300(可能是录入错误)对模型造成很大干扰。

离散化的两种方式:有监督和无监督

  • 无监督的离散化:分桶操作将连续数据离散化,使得数据更加平滑,降低噪声数据的影响:等频和等距
  • 有监督的离散化:常用的是使用树模型返回叶子节点来进行离散化。

类别型特征转换

对离散型的特征进行编码,2种常见方式:

  1. 自然数编码(特征有意义):比如衣服的S、M、L、XL等尺码大小,本身就存在一定的大小顺序
  2. 独热码(特征无意义):比如红黄绿的颜色类别;类别无顺序
1
2
3
4
5
6
7
8
9
10
# 1、调用sklearn函数
from sklearn import preprocessing
for col in columns:
le = preprocessing.LabelEncoder() # 实例化
le.fit(df[col])

# 2、自定义函数
for c in columns:
df[c] = df[c].fillna(-999)
df[c] = df[c].map(dict(zip(df[c].unique(), range(0,df[f].nunique()))))

不规则特征

比如身份证信息等(个人信息属于隐私,仅用于举例)

身份证号码 = 6位数字地址码 + 8位数字出生日期码 + 3位数字顺序码 + 1位数字校验码

特征提取(特征衍生)

通过对数据集的分析和理解来创建一些新特征来帮助模型学习。

类别相关的统计特征

  • 构造目标编码
  • count/nunique/ratio等特征
  • 特征交叉组合等

构造目标编码

构造目标编码:使用目标变量(标签)的统计量来对类别特征进行编码;回归问题,可以统计均值、中位数等,分类问题,可以统计正负样本数量和比例等

基于5折交叉验证的实现:

1
2
3
4
5
6
7
8
9
folds = KFold(n_splits=5,shuffle=True,random_state=2023)
for col in columns:
colname = col + "_kflod"
for fold_, (trn_index, val_index) in enumerate(folds.split(train, train)):
tmp = train.iloc[trn_index] # 交叉验证集下的训练集tmp
order_label = tmp.groupby([col])["label"].mean()
train[colname] = train[col].map(order_label) # col字段映射新特征colname
order_label = train.groupby([col])["label"].mean() # 重点:使用训练集的均值来填充测试集
test[colname] = test[col].map(order_label)

count、nunique、ratio

count:用于统计类别特征的出现频次
nunique、ratio:多个特征的联合构造

类别特征交叉组合

交叉组合能够描述更细粒度的内容,比如年龄_性别组合。

简单来说,就是对两个特征进行笛卡尔积的操作,产生新的特征。

数值相关的统计特征

  • 特征之间的交叉组合
  • 类别特征和数值特征的交叉组合
  • 按行统计相关特征

时间特征

将给定的时间戳属性转成年月日时分秒等单个属性;还可以构造时间差等

多值特征

某列中包含多个属性的情况,这就是多值特征。多值特征的常见处理方式:完全展开,将特征的n个属性展开成n维稀疏矩阵。使用sklearn中的CountVectorizer函数,考虑每个属性在这个特征的出现频次。

特征选择

增加了新特征后,需要判断它们对提高模型效果是否有用。特征选择算法用于从数据中识别并删除不需要、不相关以及冗余的特征。主要方法:

  • 基于先验的特征关联性分析
  • 基于后验的特征重要性分析

特征关联性分析

特征关联性分析是使用统计量来为特征之间的相关性进行评分;按照分数的高低来进行排序,选择部分特征。

关联性分析通常是针对单个变量,忽略了变量和变量之间的关系。常用方法:

  • 皮尔逊相关系数
  • 卡方检验
  • 互信息法
  • 信息增益

皮尔逊相关系数

  • 可以衡量变量和变量间的相关性,解决多重共线性问题
  • 可以衡量变量和标签间的相关性
1
2
3
4
5
6
7
8
9
10
11
12
# 提取top300的特征

def feature_select_pearson(train, features):
featureselect = features[:] # 副本
# 皮尔逊相关系数
corr = []
for f in featureselect:
corr.append(abs(train[[f, "target"]].fillna(0).corr().values[0][1])) # 指定特征f和目标target间的相关系数
se = pd.Series(corr, index=featureselect).sort_values(ascending=False)
feature_select = se[:300].index.tolist() # 前300个特征
# 返回特征选择后的训练集
return train[feature_select]

卡方检验

检验特征变量和因变量的关系。对于分类问题,一般假设与标签独立的特征为无关特征,而卡方检验刚好可以进行独立性检验。

如果检验的结果是某个特征和标签独立,则可以删除该特征

$$X^2=\sum \frac{(A-E)^2} {E}$$

互信息法

互信息是对一个联合分布中两个变量之间相互影响的度量,也可以用来评价两个变量间的相关性。从两个角度解释互信息:基于KL散度和互信息增益

互信息越大说明变量相关性越高

$$M I\left(x_{i}, y\right)=\sum_{x_{i} \in{0,1}} \sum_{y \in{0,1}} p\left(x_{i}, y\right) \log \left(\frac{p\left(x_{i}, y\right)}{p\left(x_{i}\right) p(y)}\right)$$

特征重要性分析

基于树模型评估特征的重要性分数。比如使用XGBoost模型评估重要性的3种计算方法:weight、gain、cover

1
2
3
4
5
6
7
8
9
10

params = {"max_depth":10,
"subsample":1,
"verbose_eval":True,
"seed":12,
"obejective":"binary:logistic"}

xgtrain = xgb.DMatrix(x,label=y)
bst = xgb.train(params, xgtrain,num_boost_round=10)
importance = bst.get_score(fmap="",importance_type="weight") # weight gain cover

封装方法

封装方法是一个比较耗时的特征选择方法:将一组特征视为一个搜索问题,通过准备、评估不同的组合并对这些组合进行比较,从而找出最优的特征子集。搜索过程可以是系统性的(最佳优先搜索),也可以是随机的(随机爬山算法),或者元启发式方法(通过向前或者向后搜索来添加和删除特征,类似剪枝算法)。

常用的方法:

  1. 启发式方法
  2. 递归特征消除法RFE
1
2
3
4
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression

RFE(estimator=LogisticRegression(), n_features_to_select=2).fit_transform(data,target)

使用封装方法的时候,应该先对数据进行采样,再对小数据使用封装方法。

上面三种方法建议使用顺序:特征重要性、特征关联性分析、封装方法

其他不常见的特征选择方法:kaggle上经典的null importance特征选择方式。

https://www.kaggle.com/code/ogrellier/feature-selection-with-null-importances/notebook

特征工程案例实战

针对特征工程部分

1
2
3
4
5
6
7
8
9
10
11
12
import pandas as pd
import numpy as np

from sklearn.model_selection import KFold # K折交叉验证
from sklearn.metrics import mean_squared_error #评价指标mse
from sklearn.preprocessing import OneHotEncoder # 独热码

import lightgbm as lgb # lgb模型

import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
1
2
3
4
5
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")

ntrain = train.shape[0]
ntest = test.shape[0]

数据预处理

缺失值处理

1
pd.concat([train,test],axis=0, sort=False)

1
2
3
4
5
df = pd.concat([train,test],axis=0, sort=False)

# 1、删除缺失值比例大于50%的特征
missing_cols = [c for c in df if df[c].isna().mean() * 100 > 50]
df = df.drop(missing_cols, axis=1)
1
df.shape
(2919, 77)
1
2
3
4
5
6
# 2、对object类型特征进行填充unknow

object_df = df.select_dtypes(include=["object"])
numerical_df = df.select_dtypes(exclude=["object"])

object_df = object_df.fillna("unknow") # 填充指定值
1
2
3
4
5
6
7
# 3、对数值型特征填充中位数

# 确定存在缺失值的数值型字段
missing_cols = [c for c in numerical_df if numerical_df[c].isna().sum() > 0]
# 填充中位数
for c in missing_cols:
numerical_df[c] = numerical_df[c].fillna(numerical_df[c].median())

删除属性分布不均衡的特征

某些特征中的属性分布极不均衡,比如某个属性占比超过95%,此时可以考虑是否删除该特征

1
2
3
plt.figure(figsize=(8,6))
sns.countplot(df["Street"])
plt.show()

在Street特征中,Pave属性远高于Grvl属性,可以考虑删除。

1
2
3
plt.figure(figsize=(8,6))
sns.countplot(df["Heating"])
plt.show()

1
2
3
plt.figure(figsize=(8,6))
sns.countplot(df["RoofMatl"])
plt.show()

1
2
3
plt.figure(figsize=(8,6))
sns.countplot(df["Condition2"])
plt.show()

1
2
3
plt.figure(figsize=(8,6))
sns.countplot(df["Utilities"])
plt.show()

我们直接删除上面的几个特征,属性分布极不均衡:

1
object_df = object_df.drop(['Heating','RoofMatl','Condition2','Street','Utilities'],axis=1)

特征提取

从多个角度进行特征构造,构造的特征具有实际意义:

基本特征构造

发现数据中存在异常值:销售日期YrSold 小于建造日期YearBuilt(不符合常理),属于异常。

1
2
3
4
# 异常值处理
numerical_df.loc[numerical_df['YrSold'] > numerical_df['YearBuilt'], 'YrSold'] = 2009
# 构造特征:房屋年龄
numerical_df["Age_House"] = numerical_df["YrSold"] - numerical_df["YearBuilt"]
1
2
3
numerical_df["TotalBsmtBath"] = numerical_df["BsmtFullBath"] + numerical_df["BsmtHalfBath"] * 0.5  # 浴池 + 半浴池
numerical_df["TotalBath"] = numerical_df["FullBath"] + numerical_df["HalfBath"] * 0.5 # 全浴 + 半浴
numerical_df["TotalSA"] = numerical_df["TotalBsmtSF"] + numerical_df["1stFlrSF"] + numerical_df["2ndFlrSF"] # 1楼 + 2楼 + 地下室面积求和

特征编码

不同的分类型特征采用不同的方式:

  • 本身存在大小关系的序数特征:进行自然编码,0-N的自然数
  • 没有大小关系的特征:独热码one-hot;或者频次编码count
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bin_map = {"TA":2,
"Gd":3,
"Fa":1,
"Ex":4,
"Po":1,
"None":0,
"Y":1,
"N":0,
"Reg":3,
"IR1":2,
"IR2":1,
"IR3":0,
"None":0,
"No":2,
"Mn":2,
"Av":3,
"Gd":4,
"Unf":1,
"LwQ":2,
"Rec":3,
"BLQ":4,
"ALQ":5,
"GLQ":6}
1
sorted(object_df.columns)
['BldgType',
 'BsmtCond',
 'BsmtExposure',
 'BsmtFinType1',
 'BsmtFinType2',
 'BsmtQual',
 'CentralAir',
 'Condition1',
 'Electrical',
 'ExterCond',
 'ExterQual',
 'Exterior1st',
 'Exterior2nd',
 'FireplaceQu',
 'Foundation',
 'Functional',
 'GarageCond',
 'GarageFinish',
 'GarageQual',
 'GarageType',
 'HeatingQC',
 'HouseStyle',
 'KitchenQual',
 'LandContour',
 'LandSlope',
 'LotConfig',
 'LotShape',
 'MSZoning',
 'MasVnrType',
 'Neighborhood',
 'PavedDrive',
 'RoofStyle',
 'SaleCondition',
 'SaleType']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
columns_list = [
'BsmtCond',
'BsmtExposure',
'BsmtFinType1',
'BsmtFinType2',
'BsmtQual',
'CentralAir',
'ExterCond',
'ExterQual',
'FireplaceQu',
'GarageCond',
'GarageQual',
'HeatingQC',
'KitchenQual',
'LotShape'
]

for column in columns_list:
object_df[column] = object_df[column].map(bin_map)
1
2
PavedDrive = {"N":0,"P":1,"Y":2}
object_df["PavedDrive"] = object_df["PavedDrive"].map(PavedDrive)
1
2
3
4
# 选择剩余的object特征
rest_object_columns = object_df.select_dtypes(include=["object"])
# 实施独热码
object_df = pd.get_dummies(object_df,columns=rest_object_columns.columns)

合并分类型和数值型特征的数据:

1
df = pd.concat([numerical_df,object_df],axis=1,sort=False)

特征选择:基于相关系数

基于相关性评估的方式进行特征选择,过滤掉相似性大于一定阈值的特征,减少特征冗余。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 相关性评估的辅助函数

def correlation(data, threshold):
col_corr = set() # 集合

corr_matrix = data.corr()

for i in range(len(corr_matrix.columns)): # 行方向
for j in range(i): # 列方向
if abs(corr_matrix.iloc[i,j]) > threshold: # 定位到具体的相似性分数和阈值对比
colname = corr_matrix.columns[i] # 获取列名
col_corr.add(colname) # 往集合中添加元素

return col_corr,corr_matrix # 列名 + 相关系数矩阵

all_cols = [c for c in df.columns if c not in ["SalePrice"]]
corr_features, corr_matrix = correlation(df[all_cols], 0.9)
1
2
3
4
# 相关系数矩阵
# 除去SalePrice,每个两个特征之间的相关系数

corr_features
{'Age_House',
 'Exterior2nd_CmentBd',
 'Exterior2nd_MetalSd',
 'Exterior2nd_VinylSd',
 'Exterior2nd_unknow',
 'GarageFinish_unknow',
 'LandSlope_Mod',
 'RoofStyle_Hip',
 'SaleCondition_Partial',
 'TotalBath',
 'TotalBsmtBath'}

本文标题:机器学习算法竞赛实战-特征工程

发布时间:2023年03月01日 - 21:03

原始链接:http://www.renpeter.cn/2023/03/01/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AE%97%E6%B3%95%E7%AB%9E%E8%B5%9B%E5%AE%9E%E6%88%98-%E7%89%B9%E5%BE%81%E5%B7%A5%E7%A8%8B.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Coffee or Tea