机器学习特征工程指南:特征缩放、编码、转换、生成与选择,提升模型性能,避免数据泄露。
原文标题:机器学习特征工程,全面指南!(下)
原文作者:数据派THU
冷月清谈:
怜星夜思:
2、文章提到了多种特征编码方式,在实践中如何选择合适的编码方式?
3、如何避免在特征工程过程中引入数据泄露?
原文内容
本文约11200字,建议阅读20分钟本指南是初学者的简明参考,提供了最简单但广泛使用的特征工程和选择技术。
4 特征工程
4.1 特征缩放
定义:特征缩放是一种用于标准化数据自变量或特征范围的方法。在数据处理中,它也被称为数据归一化,通常在数据预处理步骤中执行。
4.1.1 为什么特征缩放很重要
如果输入范围发生变化,在某些算法中,目标函数将无法正常工作。梯度下降在完成特征缩放后收敛得更快。
梯度下降是一种常用的优化算法,用于逻辑回归、支持向量机、神经网络等。
涉及距离计算的算法,如KNN、聚类,也受到特征大小的影响。只需考虑欧几里德距离的计算方法:取观测值之间平方差之和的平方根。这种距离会受到变量之间尺度差异的极大影响。方差较大的变量对这种度量的影响比方差较小的变量大。
注意:基于树的算法几乎是唯一不受输入量影响的算法,因为我们可以很容易地从树的构建方式中看出。在决定如何进行分割时,树算法会寻找诸如“特征值X是否大于3.0”之类的决策,并在分割后计算子节点的纯度,因此特征的规模并不重要。
4.1.2 如何处理特征缩放
面对异常值时三种方法的比较:
正如我们所看到的,归一化-标准化和最小-最大方法会将大多数数据压缩到一个较窄的范围,而鲁棒缩放器在保持数据分布方面做得更好,尽管它不能从处理结果中删除异常值。记住删除/输入异常值是数据清理中的另一个主题,应该提前完成。
关于如何选择特征缩放方法的经验:
-
如果你的特征不是高斯分布,比如,具有偏斜分布或异常值,那么归一化-标准化不是一个好的选择,因为它会将大多数数据压缩到一个狭窄的范围。
-
然而,我们可以将特征转换为高斯分布,然后使用归一化 - 标准化。特征转换将在第3.4节中讨论。
-
在进行距离或协方差计算(如聚类、PCA和LDA等算法)时,最好使用归一化-标准化,因为它会消除尺度对方差和协方差的影响。
-
Min-Max缩放与Normalization-Standardization具有相同的缺点,并且新数据可能不会限制在[0,1],因为它们可能超出原始范围。一些算法,例如一些深度学习网络,更喜欢在0-1范围内输入,因此这是一个不错的选择。
-
当面对偏斜变量时,三种方法的比较可以在这里找到。
-
关于特征缩放的深入研究可以在这里找到。
-
通过将具有相似预测强度的相似属性进行分组,有助于提高模型性能
-
引入非线性,从而提高模型的拟合能力
-
通过分组值增强可解释性
-
尽量减少极端值/很少反转模式的影响
-
防止数值变量可能出现的过拟合
-
允许连续变量之间的特征交互(第4.5.5节)
注意:如果我们在线性回归中使用one-hot编码,我们应该保留k-1个二进制变量以避免多重共线性。这对于在训练期间同时查看所有特征的任何算法都是如此。包括SVM、神经网络和聚类。另一方面,基于树的算法需要整个二进制变量集来选择最佳分割。
-
与结果Y之间存在线性关系
-
多元正态性
-
无或很少多重共线性
-
同方差性
-
计数/求和
-
平均值/中值/众数
-
最大值/最小值/标准偏差/方差/范围/四分位数间距/变异系数
-
时间跨度/间隔
-
时间
-
区域
-
业务类型
# pandas自带的聚合函数
mean(): Compute mean of groups
sum(): Compute sum of group values
size(): Compute group sizes
count(): Compute count of group
std(): Standard deviation of groups
var(): Compute variance of groups
sem(): Standard error of the mean of groups
first(): Compute first of group values
last(): Compute last of group values
nth() : Take nth value, or a subset if n is a list
min(): Compute min of group values
max(): Compute max of group values自定义函数
def median(x):
return np.median(x)
def variation_coefficient(x):
mean = np.mean(x)
if mean != 0:
return np.std(x) / mean
else:
return np.nan
def variance(x):
return np.var(x)
def skewness(x):
if not isinstance(x, pd.Series):
x = pd.Series(x)
return pd.Series.skew(x)
def kurtosis(x):
if not isinstance(x, pd.Series):
x = pd.Series(x)
return pd.Series.kurtosis(x)
def standard_deviation(x):
return np.std(x)
def large_standard_deviation(x):
if (np.max(x)-np.min(x)) == 0:
return np.nan
else:
return np.std(x)/(np.max(x)-np.min(x))
def variation_coefficient(x):
mean = np.mean(x)
if mean != 0:
return np.std(x) / mean
else:
return np.nan
def variance_std_ratio(x):
y = np.var(x)
if y != 0:
return y/np.sqrt(y)
else:
return np.nan
def ratio_beyond_r_sigma(x, r):
if x.size == 0:
return np.nan
else:
return np.sum(np.abs(x - np.mean(x)) > r * np.asarray(np.std(x))) / x.size
def range_ratio(x):
mean_median_difference = np.abs(np.mean(x) - np.median(x))
max_min_difference = np.max(x) - np.min(x)
if max_min_difference == 0:
return np.nan
else:
return mean_median_difference / max_min_differencedef has_duplicate_max(x):
return np.sum(x == np.max(x)) >= 2
def has_duplicate_min(x):
return np.sum(x == np.min(x)) >= 2
def has_duplicate(x):
return x.size != np.unique(x).size
def count_duplicate_max(x):
return np.sum(x == np.max(x))
def count_duplicate_min(x):
return np.sum(x == np.min(x))
def count_duplicate(x):
return x.size - np.unique(x).size
def sum_values(x):
if len(x) == 0:
return 0
return np.sum(x)
def log_return(list_stock_prices):
return np.log(list_stock_prices).diff()
def realized_volatility(series):
return np.sqrt(np.sum(series2))
def realized_abs_skew(series):
return np.power(np.abs(np.sum(series3)),1/3)
def realized_skew(series):
return np.sign(np.sum(series3))*np.power(np.abs(np.sum(series3)),1/3)
def realized_vol_skew(series):
return np.power(np.abs(np.sum(series6)),1/6)
def realized_quarticity(series):
return np.power(np.sum(series4),1/4)
def count_unique(series):
return len(np.unique(series))
def count(series):
return series.size
#drawdons functions are mine
def maximum_drawdown(series):
series = np.asarray(series)
if len(series)<2:
return 0
k = series[np.argmax(np.maximum.accumulate(series) - series)]
i = np.argmax(np.maximum.accumulate(series) - series)
if len(series[:i])<1:
return np.NaN
else:
j = np.max(series[:i])
return j-k
def maximum_drawup(series):
series = np.asarray(series)
if len(series)<2:
return 0series = - series
k = series[np.argmax(np.maximum.accumulate(series) - series)]
i = np.argmax(np.maximum.accumulate(series) - series)
if len(series[:i])<1:
return np.NaN
else:
j = np.max(series[:i])
return j-k
def drawdown_duration(series):
series = np.asarray(series)
if len(series)<2:
return 0
k = np.argmax(np.maximum.accumulate(series) - series)
i = np.argmax(np.maximum.accumulate(series) - series)
if len(series[:i]) == 0:
j=k
else:
j = np.argmax(series[:i])
return k-j
def drawup_duration(series):
series = np.asarray(series)
if len(series)<2:
return 0
series=-series
k = np.argmax(np.maximum.accumulate(series) - series)
i = np.argmax(np.maximum.accumulate(series) - series)
if len(series[:i]) == 0:
j=k
else:
j = np.argmax(series[:i])
return k-j
def max_over_min(series):
if len(series)<2:
return 0
if np.min(series) == 0:
return np.nan
return np.max(series)/np.min(series)
def mean_n_absolute_max(x, number_of_maxima = 1):
“”" Calculates the arithmetic mean of the n absolute maximum values of the time series.“”"
assert (
number_of_maxima > 0
), f" number_of_maxima={number_of_maxima} which is not greater than 1"
n_absolute_maximum_values = np.sort(np.absolute(x))[-number_of_maxima:]
return np.mean(n_absolute_maximum_values) if len(x) > number_of_maxima else np.NaN
def count_above(x, t):
if len(x)==0:
return np.nan
else:
return np.sum(x >= t) / len(x)
def count_below(x, t):
if len(x)==0:
return np.nan
else:
return np.sum(x <= t) / len(x)
#number of valleys = number_peaks(-x, n)
def number_peaks(x, n):
“”"
Calculates the number of peaks of at least support n in the time series x. A peak of support n is defined as a
subsequence of x where a value occurs, which is bigger than its n neighbours to the left and to the right.
“”"
x_reduced = x[n:-n]
res = None
for i in range(1, n + 1):
result_first = x_reduced > _roll(x, i)[n:-n]
if res is None:
res = result_first
else:
res &= result_first
res &= x_reduced > _roll(x, -i)[n:-n]
return np.sum(res)
def mean_abs_change(x):
return np.mean(np.abs(np.diff(x)))
def mean_change(x):
x = np.asarray(x)
return (x[-1] - x[0]) / (len(x) - 1) if len(x) > 1 else np.NaN
def mean_second_derivative_central(x):
x = np.asarray(x)
return (x[-1] - x[-2] - x[1] + x[0]) / (2 * (len(x) - 2)) if len(x) > 2 else np.NaN
def root_mean_square(x):
return np.sqrt(np.mean(np.square(x))) if len(x) > 0 else np.NaN
def absolute_sum_of_changes(x):
return np.sum(np.abs(np.diff(x)))
def longest_strike_below_mean(x):
if not isinstance(x, (np.ndarray, pd.Series)):
x = np.asarray(x)
return np.max(_get_length_sequences_where(x < np.mean(x))) if x.size > 0 else 0
def longest_strike_above_mean(x):
if not isinstance(x, (np.ndarray, pd.Series)):
x = np.asarray(x)
return np.max(_get_length_sequences_where(x > np.mean(x))) if x.size > 0 else 0
def count_above_mean(x):
m = np.mean(x)
return np.where(x > m)[0].size
def count_below_mean(x):
m = np.mean(x)
return np.where(x < m)[0].size
def last_location_of_maximum(x):
x = np.asarray(x)
return 1.0 - np.argmax(x[::-1]) / len(x) if len(x) > 0 else np.NaN
def first_location_of_maximum(x):
if not isinstance(x, (np.ndarray, pd.Series)):
x = np.asarray(x)
return np.argmax(x) / len(x) if len(x) > 0 else np.NaN
def last_location_of_minimum(x):
x = np.asarray(x)
return 1.0 - np.argmin(x[::-1]) / len(x) if len(x) > 0 else np.NaN
def first_location_of_minimum(x):
if not isinstance(x, (np.ndarray, pd.Series)):
x = np.asarray(x)
return np.argmin(x) / len(x) if len(x) > 0 else np.NaNTest non-consecutive non-reoccuring values ?
def percentage_of_reoccurring_values_to_all_values(x):
if len(x) == 0:
return np.nan
unique, counts = np.unique(x, return_counts=True)
if counts.shape[0] == 0:
return 0
return np.sum(counts > 1) / float(counts.shape[0])
def percentage_of_reoccurring_datapoints_to_all_datapoints(x):
if len(x) == 0:
return np.nan
if not isinstance(x, pd.Series):
x = pd.Series(x)
value_counts = x.value_counts()
reoccuring_values = value_counts[value_counts > 1].sum()
if np.isnan(reoccuring_values):
return 0
return reoccuring_values / x.size
def sum_of_reoccurring_values(x):
unique, counts = np.unique(x, return_counts=True)
counts[counts < 2] = 0
counts[counts > 1] = 1
return np.sum(counts * unique)
def sum_of_reoccurring_data_points(x):
unique, counts = np.unique(x, return_counts=True)
counts[counts < 2] = 0
return np.sum(counts * unique)
def ratio_value_number_to_time_series_length(x):
if not isinstance(x, (np.ndarray, pd.Series)):
x = np.asarray(x)
if x.size == 0:
return np.nan
return np.unique(x).size / x.size
def abs_energy(x):
if not isinstance(x, (np.ndarray, pd.Series)):
x = np.asarray(x)
return np.dot(x, x)
def quantile(x, q):
if len(x) == 0:
return np.NaN
return np.quantile(x, q)crossing the mean ? other levels ?
def number_crossing_m(x, m):
if not isinstance(x, (np.ndarray, pd.Series)):
x = np.asarray(x)From math - Efficiently detect sign-changes in python - Stack Overflow
positive = x > m
return np.where(np.diff(positive))[0].size
def absolute_maximum(x):
return np.max(np.absolute(x)) if len(x) > 0 else np.NaN
def value_count(x, value):
if not isinstance(x, (np.ndarray, pd.Series)):
x = np.asarray(x)
if np.isnan(value):
return np.isnan(x).sum()
else:
return x[x == value].size
def range_count(x, min, max):
return np.sum((x >= min) & (x < max))
def mean_diff(x):
return np.nanmean(np.diff(x.values))
5 特征选择
定义:特征选择是为机器学习模型构建选择相关特征子集的过程。
并非总是存在“数据越多,结果越好”这一真理。包含无关特征(对预测毫无帮助的特征)和冗余特征(与其他特征无关的特征)只会使学习过程不堪重负,并容易导致过拟合。
通过特征选择,我们可以:
-
简化模型,使其更容易解释
-
更短的训练时间和更低的计算成本
-
数据收集成本更低
-
避免维度诅咒
-
通过减少过拟合来增强泛化能力
-
选择变量,不考虑模型
-
计算成本更低
-
通常会带来较低的预测性能
-
使用ML模型对特征子集进行评分
-
在每个子集上训练一个新的模型
-
计算成本非常高
-
通常为给定的机器学习算法提供性能最佳的子集,但可能不适合另一个
-
需要任意定义的停止标准
-
性能提升
-
性能下降
-
达到预定义的特征数量
-
1个特征的所有可能组合
-
2个特征的所有可能组合
-
3个特征的所有可能组合
-
全部 4 个特征
-
将特征选择作为模型构建过程的一部分
-
考虑特征之间的相互作用
-
与 Wrappers 相比,计算成本更低,因为它只训练模型一次
-
通常为给定的机器学习算法提供性能最好的子集,但可能不适用于另一个算
-
L1正则化(Lasso)
-
L2正则化(Ridge)
-
L1/L2(弹性网)
-
最陡梯度法和L1正则化回归:一项综述
-
生物信息学中的正则化特征选择和分类
-
特征选择分类:一项综述
-
机器学习解析:正则化
-
相关特征显示了相似的重要性
-
当构建树时没有与其相关的对应物,相关特征的重要性低于实际重要性
-
高枢椎变量往往显示出更高的重要性
-
根据机器学习算法得出的特征重要性对特征进行排名:可以是树重要性,或 LASSO/Ridge,或线性/逻辑回归系数。
-
删除一个特征(最不重要的特征),并利用剩余的特征构建机器学习算法。
-
计算你选择的性能指标:roc-auc、mse、rmse、准确率。
-
如果指标下降超过任意设置的阈值,则该特征很重要,应该保留。否则,我们可以删除该特征。
-
重复步骤2-4,直到所有特征都被删除(并因此被评估),并且性能下降得到评估。
-
根据机器学习算法得出的特征重要性对特征进行排名:可以是树重要性,或 LASSO/Ridge,或线性/逻辑回归系数。
-
构建一个只有一个特征(最重要的特征)的机器学习模型,并计算模型的性能指标。
-
添加一个最重要的特征,并利用添加的特征和前几轮中的任何特征构建机器学习算法。
-
计算你选择的性能指标:roc-auc、mse、rmse、accuracy。
-
如果指标增加超过任意设置的阈值,则该特征很重要,应该保留。否则,我们可以删除该特征。
-
重复步骤2-5,直到所有特征都已被删除(并因此被评估),并且性能下降得到了评估。