决定模型好坏的一个重要工作就是:特征工程
机器学习在本质还是特征,数据和特征决定了机器学习的上限,模型和算法知识逼近这个上限而已。
特征工程介于数据和算法之间,常见的特征工程分为:
缺失值的表现为NaN,NA,None
,还有其他用于表示数值缺失的特殊数值。
如果是少量可接受的比例,可以考虑直接删除;一般是使用填充方式:
# 使用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)
无量纲化:将不同规格的数据转换到同一个规格,两种方法:标准化和缩放法
标准化:前提是特征值服从正态分布;标准化后,特征值服从标准正态分布。
缩放法:利用边界值信息,将特征值缩放某个范围,比如[0,1]
或者[-1,1]
等
取对数log转换可以将倾斜数据变得接近正态分布,一般是使用log(x+1),其中加1是防止数据等于0,同时保证x是正的。
取对数不会改变数据的性质和相关关系。但是压缩了变量的尺度,不仅数据更加平稳,还削弱了模型的共线性、异方差性等。
cbox-cox
变换:自动寻找最佳正态分布变换函数的方法
离散化的两种方式:有监督和无监督
对离散型的特征进行编码,2种常见方式:
# 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位数字校验码
通过对数据集的分析和理解来创建一些新特征来帮助模型学习。
构造目标编码:使用目标变量(标签)的统计量来对类别特征进行编码;回归问题,可以统计均值、中位数等,分类问题,可以统计正负样本数量和比例等
基于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:多个特征的联合构造
交叉组合能够描述更细粒度的内容,比如年龄_性别
组合。
简单来说,就是对两个特征进行笛卡尔积的操作,产生新的特征。
将给定的时间戳属性转成年月日时分秒等单个属性;还可以构造时间差等
某列中包含多个属性的情况,这就是多值特征。多值特征的常见处理方式:完全展开,将特征的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]
检验特征变量和因变量的关系。对于分类问题,一般假设与标签独立的特征为无关特征,而卡方检验刚好可以进行独立性检验。
如果检验的结果是某个特征和标签独立,则可以删除该特征。
互信息是对一个联合分布中两个变量之间相互影响的度量,也可以用来评价两个变量间的相关性。从两个角度解释互信息:基于KL散度和互信息增益。
互信息越大说明变量相关性越高
基于树模型评估特征的重要性分数。比如使用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
封装方法是一个比较耗时的特征选择方法:将一组特征视为一个搜索问题,通过准备、评估不同的组合并对这些组合进行比较,从而找出最优的特征子集。搜索过程可以是系统性的(最佳优先搜索),也可以是随机的(随机爬山算法),或者元启发式方法(通过向前或者向后搜索来添加和删除特征,类似剪枝算法)。
常用的方法:
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
针对特征工程部分
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
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楼 + 地下室面积求和
不同的分类型特征采用不同的方式:
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'}