特征工程是构建机器学习模型的关键步骤之一,如果用对了特征,模型性能会得到很大提升。
很多情况下,我们可以避免使用大规模的、复杂的模型,转而使用经过特征工程的简单模型。
特征工程最佳实践时机是你拥有该问题的领域知识,并对数据有深刻认识。
当然,特征工程也有一些通用的技巧,你可以基于数值特征或分类特征来创建新特征。
特征工程不仅是根据数据创建新特征,还包括各种归一化和数据变换。
上一章里,我们尝试了结合不同分类变量的创建新特征的方法,包括将分类变量转为统计量、target encoding 和 entity embedding 。
这三种方法几乎是特征工程里处理分类特征的全部方法,因此,本章我们重点关注数值特征与数值、分类混合特征。
让我们先从一个简单但被广泛使用的特征工程技术开始。
假如你在处理日期与时间数据,即 pandas DataFrame 里有一列 datetime 数据,那么通过该列可以得到以下特征:
-
Year
-
Week of year
-
Month
-
Day of week
-
Weekend
-
Hour
-
And many more
代码实现如下。
df.loc[:, 'year'] = df.datetime_column.dt.year
df.loc[:, 'weekofyear'] = df.datetime_column.dt.weekofyear
df.loc[:, 'month'] = df.datetime_column.dt.month
df.loc[:, 'dayofweek'] = df.datetime_column.dt.dayofweek
df.loc[:, 'weekend'] = (df.datetime_column.dt.weekday >= 5).astype(int)
df.loc[:, 'hour'] = df.datetime_column.dt.hour
下边是一个具体的例子,可以通过时间序列创建的一系列特征。
import pandas as pd
s = pd.date_range('2023-1-1', '2023-12-31', freq='10H').to_series()
features = {
'dayofweek': s.dt.dayofweek.values,
'dayofyear': s.dt.dayofyear.values,
'hour': s.dt.hour.values,
'is_leap_year': s.dt.is_leap_year.values,
'quarter': s.dt.quarter.values
}
pd.DataFrame(features)
对于一个时间序列,可以创建一系列特征,我们可以在任何 pandas 数据上使用该方法。
对时间序列特征进行处理非常重要,例如你要预测一个商店的销量,这时基于时间窗口对特征进行聚合会给你带来不同的数据视野。
假如有如下代码生成的数据。
import numpy as np
import pandas as pd
dt = pd.date_range('2022-1-1', '2022-12-31', freq='1MS')
size = dt.shape[0]
df = pd.DataFrame({
'date': dt,
'user_id': np.random.randint(0, 100000, size=(size)),
'cat_1': np.random.randint(0, 6, size=(size)),
'cat_2': np.random.randint(0, 4, size=(size)),
'cat_3': np.random.randint(0, 3, size=(size)),
'num_1': np.random.randn(size),
})
df
该数据有一列时间序列,一列用户 ID,三列类别特征,一列数值特征。
基于该数据,我们可以生成一系列特征:
-
用户最活跃的月份是哪个月?
-
每个用户的 cat_1/cat_2/cat_3 总量?
-
指定时间窗口内,每个用户的 cat_1/cat_2/cat_3 总量?
-
每个用户的 num_1 平均值?
通过 pandas 的聚合方法,这些特征很容易计算。
def generate_features(df):
df.loc[:, 'year'] = df.date.dt.year
df.loc[:, 'month'] = df.date.dt.month
df.loc[:, 'dayofweek'] = df.date.dt.dayofweek
df.loc[:, 'weekend'] = (df.dayofweek >= 5).astype(int)
aggs = {}
aggs['month'] = ['unique', 'mean']
aggs['num_1'] = ['sum', 'max', 'min', 'mean']
aggs['user_id'] = ['size', 'unique']
agg_df = df.groupby('user_id').agg(aggs)
return agg_df
generate_features(df)
得到聚合结果如下。
接下来,我们便可以根据 user_id 拼接聚合结果与原始 DataFrame。
有时在处理时间序列问题时,在某个特征里,其数据可能是一个数组的值。这种情况下,我们也可以创建不同类型的特征:在分类特征是做聚合运算,通常会得到这种值为序列的特征,接着可以对该序列做如下统计运算,算出新特征。
-
Mean
-
Max
-
Min
-
Unique
-
Skew
-
Kurtosis
-
Kstat
-
Percentile
-
Quantile
-
Peak to peak
对这样的数组做统计,numpy 提供了很多方法。
import numpy as np
x = np.random.randint(0, 1000, size=(1000))
print(f'Mean: {np.mean(x)}')
print(f'Max: {np.max(x)}')
print(f'Min: {np.min(x)}')
print(f'std: {np.std(x)}')
print(f'var: {np.var(x)}')
print(f'ptp: {np.ptp(x)}')
print(f'percentile-80: {np.percentile(x, 80)}')
print(f'quantile-95: {np.quantile(x, 0.95)}')
对于时间序列,也可以使用 tsfresh 进行特征计算。
import numpy as np
import pandas as pd
from tsfresh.feature_extraction import feature_calculators as fc
size = 1000
date = pd.date_range('2022-1-1', '2022-12-31', periods=size)
s = pd.Series(np.random.randint(0, 1000, size=(size)), index=date)
print(fc.abs_energy(s))
print(fc.count_above_mean(s))
print(fc.count_below_mean(s))
print(fc.mean_abs_change(s))
print(fc.mean_change(s))
tsfresh 提供了上百种特征,你可以在时间序列上使用该库来快速计算特征。
另一种基于数值特征生成新特征的方法是多项式特征 polynomial features,例如,如果你要在 a、b 特征上生成多项式特征,你将会得到 a、b、ab、aa、bb 特征。
实现代码如下。
import numpy as np
import pandas as pd
from sklearn import preprocessing
df = pd.DataFrame(
np.random.randn(100, 2),
columns=[f'f_{i}' for i in range(1, 3)]
)
pf = preprocessing.PolynomialFeatures(
degree=2,
interaction_only=False,
include_bias=False
)
feats = pf.fit_transform(df)
df = pd.DataFrame(
feats,
columns=[f'f_{i}' for i in range(1, feats.shape[1]+1)]
)
df
使用 PolynominalFeatures 方法生成新特征。
如果有三个特征,那么就会生成 9 个 polynomial 特征。输入的特征数量越多,得到的 polynomial 特征就越多,在数据集数量较多时,这种创建特征的方法耗时不短。
另一种处理数值特征的方式是,将数值特征转化为分类特征,通常被称为分箱 binning。在 pandas 中,cut 方法可以快速完成分箱操作。
import numpy as np
import pandas as pd
df = pd.DataFrame(
np.random.randn(100, 2),
columns=[f'f_{i}' for i in range(1, 3)]
)
df['f_bin_10'] = pd.cut(df.f_1, bins=10, labels=False)
df['f_bin_100'] = pd.cut(df.f_1, bins=100, labels=False)
df
另一种处理数值特征的方式是,将数值做对数转换,通常被称为 log transformation,通过对特征值做 log(1+x) 计算新特征。
在数据分布范围过大时,使用对数变化可以缩小特征值的取值范围,同时可以使特征值分布更平滑。
与该操作相反的操作是,指数转换。
例如,当你使用基于对数的评估指标 RMSLE 时,可以通过指数运算将该值转换为原始预测目标值。
大多数时候,这些数值特征的创建是基于直觉的,没有固定公式,通常需要结合领域知识和一些灵感。
在处理分类变量和数值变量时,通常会遇见缺失值。
我们在上一章里看到了如何处理分类变量的缺失值,数值变量里的缺失值也需要做处理。
对数据缺失值的处理也算是特征工程的一部分。
对分类变量的缺失值进行处理很简单,只需要把缺失值当作一个新类型即可,这种做法几乎从来没出过问题。
一种填充数值数据缺失值的方法是选择一个在该特征取值范围内从来没出现过的一个值,并使用该值进行填充。例如,如果 0 没有在该特征值中出现过,那么可以用 0 来填充所有的缺失值。但该方法可能不够有效。
比填充 0 更有效的方法是填充均值,类似地,也可以填充中位数或者填充出现频率最高的值。
比较理想的填充方法可能是 knn 方法,选择一个带有缺失值的样本,计算该样本的最近邻,然后对这些邻居取均值,填充样本。
使用 KNNImputer 的代码如下。
import numpy as np
from sklearn import impute
x = np.random.randint(1, 15, size=(10, 6)).astype(float)
x.ravel()[np.random.choice(x.size, 10, replace=False)] = np.nan
print(x)
knn_imputer = impute.KNNImputer(n_neighbors=2)
print(knn_imputer.fit_transform(x))
还可以在其他列上训练回归模型预测当前列的缺失值。即把当前列作为 target,把其他所有列做为训练的特征。
到目前为止我们了解了许多通用的创建特征值的方法。
现在,假如你要基于商店过去销售情况预测之后的销售情况,你可以根据这些数据,使用上述方法创建新特征,然而,你还得根据领域知识、业务逻辑去创建一些特征,你得探索数据,根据需求寻找合适的特征。
还有重要的一点,在使用线性模型如逻辑回归模型 SVM 时,要注意对特征数值的放缩与归一化。而基于树的模型不需要对特征归一化。