公众号:尤而小屋
作者:Peter
编辑:Peter
大家好,我是Peter~
决定模型好坏的一个重要工作就是:特征工程
机器学习在本质还是特征,数据和特征决定了机器学习的上限,模型和算法知识逼近这个上限而已。
特征工程介于数据和算法之间,常见的特征工程分为:
- 数据预处理
- 特征转换
- 特征提取
- 特征选择
数据预处理
缺失处处理
缺失值的表现为NaN,NA,None
,还有其他用于表示数值缺失的特殊数值。
如果是少量可接受的比例,可以考虑直接删除;一般是使用填充方式:
- 对于类别型特征:填充众数;或者直接填充一个新类别
- 针对数值特征:可以填充均值、众数、中位数等
- 针对有序数据:可以填充相邻值(next或者previous)
- 模型预测填充:通过回归模型进行预测填充
异常值处理
- 定位异常值:可视化方法、统计分析等方法
- 处理异常值:
-
删除异常值
-
将异常值视为缺失值
-
填充均值或者中位数
-
不处理,使用异常值直接建模
-
优化内存
- python的内存回收机制:通过gc.collect来释放内存
- 数值类型优化:将pandas读取的数据转成numpy数组;使用不同的数值类型,比如float16,float32,float64等
# 使用np.iinfo查看每个int类型的最小值和最大值
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
np.iinfo(np.int8).min
-128
np.iinfo(np.int8).max
127
通过特征的最大值和最小值来判断其所属的字类型:
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)
特征转换
连续变量无量纲化
无量纲化:将不同规格的数据转换到同一个规格,两种方法:标准化和缩放法
标准化:前提是特征值服从正态分布;标准化后,特征值服从标准正态分布。
xnew=x−μδx_{new}=frac{x-mu}{delta}
缩放法:利用边界值信息,将特征值缩放某个范围,比如[0,1]
或者[-1,1]
等
Xnorm=X−XminXmax−XminX_{norm} = frac{X-X_{min}}{X_{max} – X_{min}}
连续变量数据转换
- log转换
取对数log转换可以将倾斜数据变得接近正态分布,一般是使用log(x+1),其中加1是防止数据等于0,同时保证x是正的。
取对数不会改变数据的性质和相关关系。但是压缩了变量的尺度,不仅数据更加平稳,还削弱了模型的共线性、异方差性等。
- cbox-cox变换
cbox-cox
变换:自动寻找最佳正态分布变换函数的方法
- 连续变量离散化
离散化后的特征对异常数据有很强的鲁棒性。比如年龄的离散化:将年龄大于30岁视为1,否则视为0。如果没有离散化,数据中异常值300(可能是录入错误)对模型造成很大干扰。
离散化的两种方式:有监督和无监督
- 无监督的离散化:分桶操作将连续数据离散化,使得数据更加平滑,降低噪声数据的影响:等频和等距
- 有监督的离散化:常用的是使用树模型返回叶子节点来进行离散化。
类别型特征转换
对离散型的特征进行编码,2种常见方式:
- 自然数编码(特征有意义):比如衣服的S、M、L、XL等尺码大小,本身就存在一定的大小顺序
- 独热码(特征无意义):比如红黄绿的颜色类别;类别无顺序
# 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折交叉验证的实现:
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函数,考虑每个属性在这个特征的出现频次。
特征选择
增加了新特征后,需要判断它们对提高模型效果是否有用。特征选择算法用于从数据中识别并删除不需要、不相关以及冗余的特征。主要方法:
- 基于先验的特征关联性分析
- 基于后验的特征重要性分析
特征关联性分析
特征关联性分析是使用统计量来为特征之间的相关性进行评分;按照分数的高低来进行排序,选择部分特征。
关联性分析通常是针对单个变量,忽略了变量和变量之间的关系。常用方法:
- 皮尔逊相关系数
- 卡方检验
- 互信息法
- 信息增益
皮尔逊相关系数
- 可以衡量变量和变量间的相关性,解决多重共线性问题
- 可以衡量变量和标签间的相关性
# 提取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]
卡方检验
检验特征变量和因变量的关系。对于分类问题,一般假设与标签独立的特征为无关特征,而卡方检验刚好可以进行独立性检验。
如果检验的结果是某个特征和标签独立,则可以删除该特征。
X2=∑(A−E)2EX^2=sum frac{(A-E)^2} {E}
互信息法
互信息是对一个联合分布中两个变量之间相互影响的度量,也可以用来评价两个变量间的相关性。从两个角度解释互信息:基于KL散度和互信息增益。
互信息越大说明变量相关性越高
MI(xi,y)=∑xi∈{0,1}∑y∈{0,1}p(xi,y)log(p(xi,y)p(xi)p(y))MI(x_i,y) = sum_{x_i in {{0,1}}} sum_{y in {{0,1}}} p(x_i,y)log(frac{p(x_i,y)}{p(x_i)p(y)})
特征重要性分析
基于树模型评估特征的重要性分数。比如使用XGBoost模型评估重要性的3种计算方法:weight、gain、cover
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
封装方法
封装方法是一个比较耗时的特征选择方法:将一组特征视为一个搜索问题,通过准备、评估不同的组合并对这些组合进行比较,从而找出最优的特征子集。搜索过程可以是系统性的(最佳优先搜索),也可以是随机的(随机爬山算法),或者元启发式方法(通过向前或者向后搜索来添加和删除特征,类似剪枝算法)。
常用的方法:
- 启发式方法
- 递归特征消除法RFE
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
特征选择方式。
特征工程案例实战
针对特征工程部分
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
.datatable table.frame { margin-bottom: 0; }
.datatable table.frame thead { border-bottom: none; }
.datatable table.frame tr.coltypes td { color: #FFFFFF; line-height: 6px; padding: 0 0.5em;}
.datatable .bool { background: #DDDD99; }
.datatable .object { background: #565656; }
.datatable .int { background: #5D9E5D; }
.datatable .float { background: #4040CC; }
.datatable .str { background: #CC4040; }
.datatable .time { background: #40CC40; }
.datatable .row_index { background: var(–jp-border-color3); border-right: 1px solid var(–jp-border-color0); color: var(–jp-ui-font-color3); font-size: 9px;}
.datatable .frame tbody td { text-align: left; }
.datatable .frame tr.coltypes .row_index { background: var(–jp-border-color0);}
.datatable th:nth-child(2) { padding-left: 12px; }
.datatable .hellipsis { color: var(–jp-cell-editor-border-color);}
.datatable .vellipsis { background: var(–jp-layout-color0); color: var(–jp-cell-editor-border-color);}
.datatable .na { color: var(–jp-cell-editor-border-color); font-size: 80%;}
.datatable .sp { opacity: 0.25;}
.datatable .footer { font-size: 9px; }
.datatable .frame_dimensions { background: var(–jp-border-color3); border-top: 1px solid var(–jp-border-color0); color: var(–jp-ui-font-color3); display: inline-block; opacity: 0.6; padding: 1px 10px 1px 5px;}
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
ntrain = train.shape[0]
ntest = test.shape[0]
数据预处理
缺失值处理
pd.concat([train,test],axis=0, sort=False)
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)
df.shape
(2919, 77)
# 2、对object类型特征进行填充unknow
object_df = df.select_dtypes(include=["object"])
numerical_df = df.select_dtypes(exclude=["object"])
object_df = object_df.fillna("unknow") # 填充指定值
# 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%,此时可以考虑是否删除该特征
plt.figure(figsize=(8,6))
sns.countplot(df["Street"])
plt.show()
在Street特征中,Pave属性远高于Grvl属性,可以考虑删除。
plt.figure(figsize=(8,6))
sns.countplot(df["Heating"])
plt.show()
plt.figure(figsize=(8,6))
sns.countplot(df["RoofMatl"])
plt.show()
plt.figure(figsize=(8,6))
sns.countplot(df["Condition2"])
plt.show()
plt.figure(figsize=(8,6))
sns.countplot(df["Utilities"])
plt.show()
我们直接删除上面的几个特征,属性分布极不均衡:
object_df = object_df.drop(['Heating','RoofMatl','Condition2','Street','Utilities'],axis=1)
特征提取
从多个角度进行特征构造,构造的特征具有实际意义:
基本特征构造
发现数据中存在异常值:销售日期YrSold 小于建造日期YearBuilt(不符合常理),属于异常。
# 异常值处理
numerical_df.loc[numerical_df['YrSold'] > numerical_df['YearBuilt'], 'YrSold'] = 2009
# 构造特征:房屋年龄
numerical_df["Age_House"] = numerical_df["YrSold"] - numerical_df["YearBuilt"]
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
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}
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']
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)
PavedDrive = {"N":0,"P":1,"Y":2}
object_df["PavedDrive"] = object_df["PavedDrive"].map(PavedDrive)
# 选择剩余的object特征
rest_object_columns = object_df.select_dtypes(include=["object"])
# 实施独热码
object_df = pd.get_dummies(object_df,columns=rest_object_columns.columns)
合并分类型和数值型特征的数据:
df = pd.concat([numerical_df,object_df],axis=1,sort=False)
特征选择:基于相关系数
基于相关性评估的方式进行特征选择,过滤掉相似性大于一定阈值的特征,减少特征冗余。
# 相关性评估的辅助函数
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)
# 相关系数矩阵
# 除去SalePrice,每个两个特征之间的相关系数
corr_features
{'Age_House',
'Exterior2nd_CmentBd',
'Exterior2nd_MetalSd',
'Exterior2nd_VinylSd',
'Exterior2nd_unknow',
'GarageFinish_unknow',
'LandSlope_Mod',
'RoofStyle_Hip',
'SaleCondition_Partial',
'TotalBath',
'TotalBsmtBath'}