机器学习特征工程

type
Post
status
Published
summary
数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。所以机器学习的大部分时间都是在处理数据的过程中,掌握好特征工程的思路和方法,有更高的概率能获得高质量数据。
slug
machine-learning-feature-engineering
date
Nov 6, 2023
tags
特征工程
机器学习
数据预处理
category
机器学习
password
icon
URL
Property
Feb 28, 2024 12:29 PM

一、预处理

1.1、整体分析探索

拿到数据先看数据情况
  • 数据缺失情况
  • 数据大小、形状
  • 数据是否有重复样本
  • 如果是分类任务,则看样本分布情况,看标签是否有缺失
  • 根据业务看数据:如有些指标不能为负数,看看有没有负数;有些指标可能严重异常,年龄几百岁,入网时长超过运营商存在的时长,等等
根据经验分析重点指标,一般是画图分析。绘图请看 Matplotlib & Seaborn

1.2、缺失值

除非是一些非常标准的练习数据或者比赛数据,否则实际成产过程中遇到的数据大多是有或多或少的缺失的。处理缺失值的方法一般有两种,删除和填充。

1.2.1、删除

在我的工作中,数据量一般都是千万级别的,字段也都是上百。所以我的处理方法一般是将缺失 80% 以上的行或者列删除(其他数据应该视情况而定)
data.dropna(axis=0, thresh=round(len(data.columns) * 0.2, 0), inplace=True) data.dropna(axis=1, thresh=round(len(data.index) * 0.2, 0), inplace=True)

1.2.2、统计值填充

  • 均值:适用连续型字段
  • 中位数:适用连续型字段
  • 众数:类别型数据
for col in data.columns: data[col] = data[col].fillna(data[col].mean()) data[col] = data[col].fillna(data[col].mode()) data[col] = data[col].fillna(data[col].median())

1.2.3、特殊值填充

有时候我们可以简单用 0 填充,但需要视情况而定;
另外我们还可以用一个特定值来填充,并且创建一个二元指示器(也就是一个新的布尔类型字段)用来标识是否有值缺失。

1.2.4、插值填充

线性插值:对于有序数据,可以使用线性插值方法,根据已知数据点的线性关系来估计缺失值。
模型预测:通过建立模型,根据其他特征的信息来预测缺失值。
多重补插:可以使用回归、随机森林等模型进行预测,然后多次进行填充,得到多个完整的数据集。

1.3、特征转换

浮点数或整数的特征是可以直接使用的

1.3.1、字符串类型

这种类型的数据一般是类别型数据,
  • 如果只有两类,那就直接将值映射成 0,1 即可;
  • 如果是多类,且具有顺序关系:可以直接进行编号编码。衣服尺寸(大中小),学历(高中,本科,研究生)等
  • 如果是多类,但没有顺序关系:则需要先将字符串映射成数字编号,然后进行独热编码(当然也可以直接进行独热编码,先转换成数字编号主要是为了避免汉字出现)
for col in cat_cols: col_dum = pd.get_dummies(data[col]) data.join(col_dum) data.drop(col, inplace=True)
当字符串表示的类别太多时(成百上千),再进行独热编码就会导致特征空间过大。这时可以使用:
  • 频率编码:统计每个类别的频率作为数据
  • 目标编码:将类别值替换为它们与目标变量的统计相关性(如平均值)。
  • 特征交叉:将目标特征与其他特征组合在一起,尝试新特征
  • 获取枚举值中出现次数较多的值组成类别,出现次数少的值归类到unknown中,然后进行独热编码。

1.3.2、日期数据

  • 计算出所有样本的时间到某一个未来时间之间的数值差距,从而将时间特征转化为连续值。
  • 将一个时间特征转化为若干个离散特征:年,月,日,星期几,小时数。这种方法在分析具有明显时间趋势的问题比较好用。
  • 根据时间的新旧得到一个权重值。比如商品,最近购买的权重大,很早之前购买的权重小。
  • 将日期的年月日拆分成不同的字段、当前日期在一年中的第几天、当前日期在一个月的第几天、当前周在一年中的第几周、当前日期在一周中是第几天等等。

1.3.3、连续特征离散化

分段处理

1.3.4、文本数据

  • 词袋模型:统计词的数量(重点在于词典的构建)
  • 词嵌入:word2vec等
  • 预训练模型:bert等

1.3.5、图片和视频

  • 预训练模型

1.4、异常值

1.5、样本平衡

二、标准化&归一化

只对连续型数据进行标准化或归一化

2.1、z-score标准化

具体的方法是求出样本特征x的均值mean和标准差std,然后用(x-mean)/std来代替原特征。这样特征就变成了均值为0,方差为1了。
可以自己实现也可以调用 sklearn 的接口,在sklearn中,可以用StandardScaler来做z-score标准化。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() scaler.fit_transform(df) # 返回的是 array,不是 df

2.2、max-min标准化

也称为离差标准化,预处理后使特征值映射到[0,1]之间。具体的方法是求出样本特征x的最大值max和最小值min,然后用(x-min)/(max-min)来代替原特征。如果我们希望将数据映射到任意一个区间[a,b],而不是[0,1],那么也很简单。用(x-min)(b-a)/(max-min)+a来代替原特征即可。在sklearn中,可以用MinMaxScaler来做max-min标准化。
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() scaler.fit_transform(df) # 返回的是 array,不是 df
这种方法的问题就是如果测试集或者预测数据里的特征有小于min,或者大于max的数据,会导致max和min发生变化,需要重新计算。所以实际算法中, 除非你对特征的取值区间有需求,否则max-min标准化没有 z-score标准化好用。

2.3、L1/L2范数标准化

如果我们只是为了统一量纲,那么通过L2范数整体标准化也是可以的,具体方法是求出每个样本特征向量的L2范数,然后用 每个样本特征向量 除以 L2范数 代替原样本特征即可,L1范数同理。通常情况下,范数标准化首选L2范数标准化。在sklearn中,可以用Normalizer来做L1/L2范数标准化。
from sklearn.preprocessing import Normalizer scaler = Normalizer() scaler.fit_transform(df) # 返回的是 array,不是 df

2.4、数据标准化的使用时机

1.首先,当使用梯度下降法寻求最优解时,很有可能走“之字型”路线(垂直等高线走),从而导致需要迭代很多次才能收敛;在梯度下降进行求解时能较快的收敛。所以,使用梯度下降法求解最优解的模型,归一化就非常重要!knn,logistc回归,gbdt,xgboost,adaboost 2.有一些模型是基于距离的,所以量纲对模型影响较大,就需要归一化数据,处理特征之间的权重问题,这样可以提高计算精度。比如,knn,svm,kmeans,k近邻,主成分分析; 3.神经网络对数据分布本无要求,但归一化可以加快训练数据; 4.那么不需要归一化处理的模型,决策树,随机森林。他们因为它们不关心变量的值,而是关心变量的分布和变量之间的条件概率

2.5、参考文章

 

连续特征和离散特征处理

1. 连续特征的处理:归一化 vs. 标准化

归一化(Normalization) 和 标准化(Standardization) 是处理连续特征的两种常见方法。
  • 归一化:将特征缩放到一个固定的范围(通常是 [0, 1])。常用于特征之间的尺度不一致或数值范围相差较大的情况。
  • 标准化:将特征值转换为均值为 0,方差为 1 的标准正态分布。
适合使用归一化的模型
  • K-近邻算法(KNN):因为 KNN 依赖距离度量,特征的尺度会对结果产生重大影响,因此需要归一化。
  • 支持向量机(SVM):特别是使用 RBF 核的 SVM,对特征的尺度较为敏感。
  • 神经网络(例如深度学习):为了使梯度下降更快收敛,一般会将输入特征归一化。
适合使用标准化的模型
  • 线性回归 和 逻辑回归:标准化可以使梯度下降更快收敛。
  • 朴素贝叶斯:标准化数据更适合模型的假设(例如高斯朴素贝叶斯假设数据呈正态分布)。
  • 线性判别分析(LDA):标准化有助于提升模型的稳定性。
  • K-means 聚类:标准化后有助于各特征对距离的贡献一致。
  • PCA(主成分分析):标准化数据后各个方向的方差一致,有助于模型更好地找到主成分。

2. 离散特征的处理

离散特征的常见编码方法有独热编码和数字编码(Label Encoding),但在类别较多时,这些方法可能不理想。以下是一些处理大量类别特征的方法:
  • 目标编码(Target Encoding):将类别特征编码为目标变量的均值,适用于有监督学习,但需注意防止数据泄漏。
  • 频率编码(Frequency Encoding):将类别编码为它们出现的频率,有助于减少稀疏性。
  • 嵌入编码(Embeddings):使用神经网络将高维类别特征映射到低维空间,尤其适用于深度学习模型。
  • 哈希编码(Hashing Encoding):将类别特征哈希到固定大小的空间,可以处理大量类别,但可能存在哈希冲突。
可以直接处理数字编码特征的模型
  • 树模型(如决策树、随机森林、梯度提升树 XGBoost、LightGBM、CatBoost 等):这些模型对特征的数值大小不敏感,可以直接处理数字编码的特征,并且对类别之间的顺序无假设。
  • 朴素贝叶斯:对数字编码的特征可以接受,因为它们主要关注概率计算。
归一化和标准化之后通常会选择删除原始列,这是因为归一化的目的是将特征值调整到相同的尺度,以提高模型的训练效果,而原始列的存在可能导致尺度不一致的问题。

三、特征变换

3.1、特征衍生

3.1.1、数学函数变换(单一特征)

  • 对数变换
  • 指数变换
  • 平方根变换

3.1.2、衍生统计特征

当数据中有多个相似特征时,可以进行一些统计特征衍生。如平均值、方差、最大值、最小值等。
如我接触的数据中通常会有:上月 ARPU、上上月 ARPU、上上上月 ARPU。通常一个指标会统计进三个月的值,这时就可以进行适当的衍生

3.1.2、衍生交互特征

除了常规的统计特征,还可以尝试:
  • 趋势值:要求特征之间有时序属性
    • def trendline(data, order=1): # order表示是正序还是反序 index = [i for i in range(len(data),0,-1)] coeffs = np.polyfit(index, list(data), order) return float(coeffs[0])
  • 权重组合:根据不同的权重对多个特征进行加权

3.2、特征降维

主要用到的是 主成分分析 PCA

四、特征选择

特征选择是在尽可能多地保留信息的同时,选择最重要特征子集的过程。特征选择能剔除不相关或冗余的特征,从而达到减少特征个数,提高模型精确度,减少运行时间的目的。
特征选择方法三类:
  • 第一类过滤法,根据某种指标设定一个阈值,进行筛选特征。
    • 方差过滤:方差越大的特征,那么可以认为它是比较有用的。如果方差较小,比如小于1,那么这个特征可能对我们的算法作用没有那么大。最极端的,如果某个特征方差为0,即所有的样本该特征的取值都是一样的,那么它对我们的模型训练没有任何作用,可以直接舍弃。
    • 相关系数:分别计算所有训练集中各个特征与输出值之间的相关系数,设定一个阈值,选择相关系数较大的部分特征。主要用于输出连续值的监督学习算法
    • 卡方检验:卡方检验可以检验某个特征分布和输出值分布之间的相关性。给定卡方值阈值, 选择卡方值较大的部分特征。
    • 互信息:从信息熵的角度分析各个特征和输出值之间的关系评分。互信息值越大,说明该特征和输出值之间的相关性越大,越需要保留。
    • 没有什么思路的 时候,可以优先使用卡方检验和互信息来做特征选择
  • 第二类是包装法,根据目标函数,通常是预测效果评分,每次选择部分特征,或者排除部分特征。
  • 第三类嵌入法则稍微复杂一点,它先使用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,根据权值系数从大到小来选择特征。

4.1、过滤法Filter

4.1.1、方差选择法

特征的方差越大,说明该特征内的值分布的越分散,对区分类别起到的作用越大。
这个方法不需要计算特征与标签之间的相关性,而是计算各个特征的数据方差,然后根据设定的阈值来选择方差较大特征。
# 调包处理 from sklearn.feature_selection import VarianceThreshold var_m = VarianceThreshold(threshold=0) # threshold:设定阈值 var_m.fit_transform(train_data) # 返回的是根据阈值选择过后的数据 # 直接计算方差 var_data = train_data_bool.apply(lambda x: x.var(),axis=0) var_data = pd.DataFrame(var_data,columns=['var']) var_data

4.1.2、相关系数法

Pearson相关系数发衡量的是变量之间的线性相关性,常用来衡量各个特征与标签之间的相关性
结果的取值区间为[-1,1] , -1 表示完全的负相关(这个变量下降,那个就会上升), +1 表示完全的正相关, 0 表示没有线性相关性。
import numpy as np from scipy.stats import pearsonr # Scipy的pearsonr方法能够同时计算相关系数和p-value from sklearn.feature_selection import SelectKBest sel_m = SelectKBest(lambda X,Y: array(list(map(lambda x: pearsonr(x,Y),X.T))).T[0],k=69) # 加一个[0],选择相关系数得分,去掉p-value,不然报错 sel_m.fit_transform(train_data_cont,train_data_tag) # 返回选择好的数据
  • 适合的数据类型:只对线性关系敏感,如果关系是非线性的,即便两个变量具有一一对应的关系,Pearson相关性也可能会接近 0
  • 优缺点:

4.1.3、卡方检验

卡方检验是检验自变量对因变量的相关性。要求数据非负,可以配合极值标准化使用,速度快
from sklearn.datasets import load_digits from sklearn.feature_selection import chi2 # 卡方检验 from sklearn.feature_selection import SelectKBest # 按个数选择器 from sklearn.feature_selection import SelectPercentile # 按比例选择器 X, y = load_digits(return_X_y=True) X.shape X_new = SelectPercentile(chi2, percentile=10).fit_transform(X, y) # 使用卡方检验选择最好的10%的特征 X_new.shape
sklearn API 详细解析

4.1.4、互信息

互信息衡量的也是类别型变量对类别型变量的相关性。数据量太大的话耗时更久
from sklearn.datasets import load_digits from sklearn.feature_selection import mutual_info_classif # 互信息,分类问题 from sklearn.feature_selection import mutual_info_regression # 互信息,回归问题 from sklearn.feature_selection import SelectKBest # 按个数选择器 from sklearn.feature_selection import SelectPercentile # 按比例选择器 X, y = load_digits(return_X_y=True) X.shape X_new = SelectPercentile(mutual_info_classif, percentile=70).fit_transform(X, y) # 使用卡方检验选择最好的10%的特征 X_new.shape
sklearn API 详细解析

4.1、包装法Wrapper

最常用的包装法是递归消除特征法(recursive feature elimination,以下简称RFE)。递归消除特征法使用一个机器学习模型来进行多轮训练,每轮训练后,消除若干权值系数的对应的特征,再基于新的特征集进行下一轮训练。
from sklearn.feature_selection import RFE from sklearn.feature_selection import RFECV # 含交叉验证 from sklearn.svm import SVR from sklearn.datasets import make_friedman1 """ RFECV(estimator, *, step=1, min_features_to_select=1, cv=None, scoring=None, verbose=0, n_jobs=None, importance_getter='auto') - estimator:监督学习估计器 - step: - 大于1:每次迭代要删除的特征个数 - 小于1:每次迭代要删除的特征比例 - min_features_to_select:最少要选择的特征数量 - cv:交叉验证折数 - scoring:评分函数,详见 - verbose:控制输出的详细程度 """ X, y = make_friedman1(n_samples=50, n_features=10, random_state=0) estimator = SVR(kernel="linear") selector = RFECV(estimator, step=1, cv=5) selector = selector.fit(X, y) selector.support_ # 所选特征的掩码,返回一个长度与原字段数相同的布尔值数组,对应值为1,则特征被保留,否则被删除 selector.ranking_ # 特征排序,ranking_[i] 对应第i个特征的排名位置

4.3、嵌入法Embedded

最常用的是使用L1正则化和L2正则化来选择特征。正则化惩罚项越大,那么模型的系数就会越小。当正则化惩罚项大到一定的程度的时候,部分特征系数会变成0,当正则化惩罚项继续增大到一定程度时,所有的特征系数都会趋于0. 但是我们会发现一部分特征系数会更容易先变成0,这部分系数就是可以筛掉的。也就是说,我们选择特征系数较大的特征。常用的L1正则化和L2正则化来选择特征的基学习器是逻辑回归。(L1惩罚项降维的原理在于保留多个对目标值具有同等相关性的特征中的一个,所以没选到的特征不代表不重要。故,可结合L2惩罚项来优化。具体操作为:若一个特征在L1中的权值为1,选择在L2中权值差别不大且在L1中权值为0的特征构成同类集合,将这一集合中的特征平分L1中的权值,故需要构建一个新的逻辑回归模型)
from sklearn.feature_selection import SelectFromModel from sklearn.linear_model import LogisticRegression """ SelectFromModel(estimator, *, threshold=None, prefit=False, norm_order=1, max_features=None, importance_getter='auto') - estimato:估计器 - threshold:选择特征的阈值 - max_features:要选择的最大特征数 """ from sklearn.feature_selection import SelectFromModel from sklearn.linear_model import LogisticRegression X = [[ 0.87, -1.34, 0.31 ], [-2.79, -0.02, -0.85 ], [-1.34, -0.48, -2.55 ], [ 1.92, 1.48, 0.65 ]] y = [0, 1, 0, 1] selector = SelectFromModel(estimator=LogisticRegression()).fit(X, y) selector.estimator_.coef_ selector.threshold_ selector.get_support() selector.transform(X)
 
💡
随机森林可以不进行显式的特征选择
  • 在构建每个决策树时,随机森林会从所有特征中随机选择一部分特征进行训练,这样每个决策树的特征选择都是不同的。
  • 随机森林模型可以通过计算各个特征的重要性指标,来评估每个特征对模型的预测能力的贡献大小。
  • 所以随机森林无需再进行显式的特征选择。

4.4、推荐阅读

If you have any questions, please contact me.