机器学习python入门之特征工程
- Baseline model
- 加载数据Load the data
- 准备目标列Prepare the target column
- 转换时间戳Convert timestamps
- Prep categorical variables
- Create training, validation, and test splits
- Train a model
- Make predictions & evaluate the model
- 分类编码Categorical Encodings
- Count Encoding
- Target Encoding
- CatBoost Encoding
- 特征生成Feature Generation
- 交互 Interaction
- Number of projects in the last week
- Time since the last project in the same category
- Transforming numerical features
- 特征选择Feature Selection
- 单变量特征选择Univariate Feature Selection
- L1 regularization
Baseline model
构建一个基线模型作为特性工程的起点。
加载数据Load the data
我们将使用来自Kickstarter项目的数据。数据的前几行如下所示:
import pandas as pd
ks = pd.read_csv('../input/kickstarter-projects/ks-projects-201801.csv',
parse_dates=['deadline', 'launched'])
ks.head(6)
ID | name | category | main_category | currency | deadline | goal | launched | pledged | state | backers | country | usd pledged | usd_pledged_real | usd_goal_real | |
0 | 1000002330 | The Songs of Adelaide & Abullah | Poetry | Publishing | GBP | 2015-10-09 | 1000.0 | 2015-08-11 12:12:28 | 0.0 | failed | 0 | GB | 0.0 | 0.0 | 1533.95 |
1 | 1000003930 | Greeting From Earth: ZGAC Arts Capsule For ET | Narrative Film | Film & Video | USD | 2017-11-01 | 30000.0 | 2017-09-02 04:43:57 | 2421.0 | failed | 15 | US | 100.0 | 2421.0 | 30000.00 |
2 | 1000004038 | Where is Hank? | Narrative Film | Film & Video | USD | 2013-02-26 | 45000.0 | 2013-01-12 00:20:50 | 220.0 | failed | 3 | US | 220.0 | 220.0 | 45000.00 |
3 | 1000007540 | ToshiCapital Rekordz Needs Help to Complete Album | Music | Music | USD | 2012-04-16 | 5000.0 | 2012-03-17 03:24:11 | 1.0 | failed | 1 | US | 1.0 | 1.0 | 5000.00 |
4 | 1000011046 | Community Film Project: The Art of Neighborhoo… | Film & Video | Film & Video | USD | 2015-08-29 | 19500.0 | 2015-07-04 08:35:03 | 1283.0 | canceled | 14 | US | 1283.0 | 1283.0 | 19500.00 |
5 | 1000014025 | Monarch Espresso Bar | Restaurants | Food | USD | 2016-04-01 | 50000.0 | 2016-02-26 13:38:27 | 52375.0 | successful | 224 | US | 52375.0 | 52375.0 | 50000.00 |
state列显示了项目的结果。
print('Unique values in `state` column:', list(ks.state.unique()))
output:
Unique values in `state` column: ['failed', 'canceled', 'successful', 'live', 'undefined', 'suspended']
利用这些数据,我们如何使用项目类别 category、货币currency、融资目标goal和国家country等特征来预测Kickstarter项目是否会成功?
准备目标列Prepare the target column
首先,我们将把state列转换为可以在模型中使用的目标。数据清理不是当前的重点,所以我们将简化这个例子:
- Dropping projects that are "live"删除“实时”的项目
- Counting “successful” states as outcome = 1将“成功”状态计数为结果 =1
- Combining every other state as outcome = 0结所有合其他状态为结果= 0
# Drop live projects
ks = ks.query('state != "live"')
# Add outcome column, "successful" == 1, others are 0
ks = ks.assign(outcome=(ks['state'] == 'successful').astype(int))
转换时间戳Convert timestamps
接下来,我们将launched 特征转换为可以在模型中使用的分类特征。由于我们将列加载为时间戳数据,所以我们通过时间戳列的.dt属性访问日期和时间值。
注:如果不熟悉分类特征和标签编码,请查看机器学习入门(二)。
ks = ks.assign(hour=ks.launched.dt.hour,
day=ks.launched.dt.day,
month=ks.launched.dt.month,
year=ks.launched.dt.year)
Prep categorical variables
现在对于分类变量——类别category、货币currency和国家country——我们需要将它们转换为整数,以便我们的模型能够使用数据。为此,我们将使用scikit-learn的标签编码器LabelEncoder。这将为分类特征的每个值分配一个整数。
from sklearn.preprocessing import LabelEncoder
cat_features = ['category', 'currency', 'country']
encoder = LabelEncoder()
# Apply the label encoder to each column
encoded = ks[cat_features].apply(encoder.fit_transform)
我们将所有这些特征收集到一个新的数据框dataframe 中,用于训练模型。
# Since ks and encoded have the same index and I can easily join them
data = ks[['goal', 'hour', 'day', 'month', 'year', 'outcome']].join(encoded)
data.head()
goal | hour | day | month | year | outcome | category | currency | country | |
0 | 1000.0 | 12 | 11 | 8 | 2015 | 0 | 108 | 5 | 9 |
1 | 30000.0 | 4 | 2 | 9 | 2017 | 0 | 93 | 13 | 22 |
2 | 45000.0 | 0 | 12 | 1 | 2013 | 0 | 93 | 13 | 22 |
3 | 5000.0 | 3 | 17 | 3 | 2012 | 0 | 90 | 13 | 22 |
4 | 19500.0 | 8 | 4 | 7 | 2015 | 0 | 55 | 13 | 22 |
Create training, validation, and test splits
我们需要为训练、验证和测试创建数据集。我们将使用一种相当简单的方法,使用切片来分割split 数据。我们将使用10%的数据作为验证集,10%用于测试,其余80%用于训练。
valid_fraction = 0.1
valid_size = int(len(data) * valid_fraction)
train = data[:-2 * valid_size]
valid = data[-2 * valid_size:-valid_size]
test = data[-valid_size:]
Train a model
在本节中,我们将使用LightGBM模型。这是一种基于树的模型,通常提供最好的性能,甚至与XGBoost相比也是如此。而且它的训练速度也相对较快。
我们不会做超参数优化,因为那不是本节的目标。所以,我们的模型不会是你能得到的最好的性能。但是你仍然会看到模型性能的提高,就像我们做特性工程一样。
import lightgbm as lgb
feature_cols = train.columns.drop('outcome')
dtrain = lgb.Dataset(train[feature_cols], label=train['outcome'])
dvalid = lgb.Dataset(valid[feature_cols], label=valid['outcome'])
param = {'num_leaves': 64, 'objective': 'binary'}
param['metric'] = 'auc'
num_round = 1000
bst = lgb.train(param, dtrain, num_round, valid_sets=[dvalid], early_stopping_rounds=10, verbose_eval=False)
Make predictions & evaluate the model
最后,让我们对模型的测试集进行预测,看看它执行得如何。需要记住的重要一点是,你可能会过度拟合验证数据。这就是为什么我们需要一个模型在最终评估之前不会看到的测试集。
from sklearn import metrics
ypred = bst.predict(test[feature_cols])
score = metrics.roc_auc_score(test['outcome'], ypred)
print(f"Test AUC score: {score}")
output:
Test AUC score: 0.747615303004287
分类编码Categorical Encodings
为建模对分类数据进行编码有很多种方法。
既然已经构建了一个基线模型,就可以用一些巧妙的方法改进它来处理分类变量了。
你已经熟悉了最基本的编码:独热编码和标签编码。在本小节中,你将学习计数编码count encoding、目标编码** target encoding和CatBoost encoding**。
我们首先运行第一个小节中的代码来重新构建基线模型。
import pandas as pd
from sklearn.preprocessing import LabelEncoder
ks = pd.read_csv('../input/kickstarter-projects/ks-projects-201801.csv',
parse_dates=['deadline', 'launched'])
# Drop live projects
ks = ks.query('state != "live"')
# Add outcome column, "successful" == 1, others are 0
ks = ks.assign(outcome=(ks['state'] == 'successful').astype(int))
# Timestamp features
ks = ks.assign(hour=ks.launched.dt.hour,
day=ks.launched.dt.day,
month=ks.launched.dt.month,
year=ks.launched.dt.year)
# Label encoding
cat_features = ['category', 'currency', 'country']
encoder = LabelEncoder()
encoded = ks[cat_features].apply(encoder.fit_transform)
data_cols = ['goal', 'hour', 'day', 'month', 'year', 'outcome']
data = ks[data_cols].join(encoded)
# Defining functions that will help us test our encodings
import lightgbm as lgb
from sklearn import metrics
def get_data_splits(dataframe, valid_fraction=0.1):
valid_fraction = 0.1
valid_size = int(len(dataframe) * valid_fraction)
train = dataframe[:-valid_size * 2]
# valid size == test size, last two sections of the data
valid = dataframe[-valid_size * 2:-valid_size]
test = dataframe[-valid_size:]
return train, valid, test
def train_model(train, valid):
feature_cols = train.columns.drop('outcome')
dtrain = lgb.Dataset(train[feature_cols], label=train['outcome'])
dvalid = lgb.Dataset(valid[feature_cols], label=valid['outcome'])
param = {'num_leaves': 64, 'objective': 'binary',
'metric': 'auc', 'seed': 7}
bst = lgb.train(param, dtrain, num_boost_round=1000, valid_sets=[dvalid],
early_stopping_rounds=10, verbose_eval=False)
valid_pred = bst.predict(valid[feature_cols])
valid_score = metrics.roc_auc_score(valid['outcome'], valid_pred)
print(f"Validation AUC score: {valid_score:.4f}")
# Train a model (on the baseline data)
train, valid, test = get_data_splits(data)
train_model(train, valid)
output:
Validation AUC score: 0.7467
Count Encoding
计数编码将每个分类值替换为它在数据集中出现的次数。例如,如果值“GB”在country特性中出现了10次,那么每个“GB”将被数字10替换。
我们将使用分类编码包categorical-encodings package来获得这种编码。编码器本身是可用的CountEncoder。这种编码器和其他分类编码的工作方式类似于scikit-learn 具有.fit 和.transform 方法的 transformers 。
import category_encoders as ce
cat_features = ['category', 'currency', 'country']
# Create the encoder
count_enc = ce.CountEncoder()
# Transform the features, rename the columns with the _count suffix, and join to dataframe
count_encoded = count_enc.fit_transform(ks[cat_features])
data = data.join(count_encoded.add_suffix("_count"))
# Train a model
train, valid, test = get_data_splits(data)
train_model(train, valid)
output:
Validation AUC score: 0.7486
添加计数编码特性可以将验证分数从0.7467提高到0.7486,但改进不大。
Target Encoding
目标编码将分类值替换为该特征值的目标平均值。例如,给定国家值“CA”,你将计算country == 'CA’的所有行的平均结果,大约为0.28。这通常与整个数据集上的目标概率混合在一起,以减少很少出现的值的方差。
这种技术使用目标创建新特征。因此,在目标编码中包含验证或测试数据将是目标泄漏的一种形式。相反,你应该只从训练数据集学习目标编码,并将其应用于其他数据集。
category_encoders包为目标编码提供了TargetEncoder。实现类似于CountEncoder。
# Create the encoder
target_enc = ce.TargetEncoder(cols=cat_features)
target_enc.fit(train[cat_features], train['outcome'])
# Transform the features, rename the columns with _target suffix, and join to dataframe
train_TE = train.join(target_enc.transform(train[cat_features]).add_suffix('_target'))
valid_TE = valid.join(target_enc.transform(valid[cat_features]).add_suffix('_target'))
# Train a model
train_model(train_TE, valid_TE)
output:
Validation AUC score: 0.7491
验证分数再次更高,从0.7467到0.7491。
CatBoost Encoding
最后,我们将讨论CatBoost Encoding。这与目标编码相似,因为它基于给定值的目标概率。但是对于CatBoost,对于每一行,目标概率仅从它之前的行计算。
# Create the encoder
target_enc = ce.CatBoostEncoder(cols=cat_features)
target_enc.fit(train[cat_features], train['outcome'])
# Transform the features, rename columns with _cb suffix, and join to dataframe
train_CBE = train.join(target_enc.transform(train[cat_features]).add_suffix('_cb'))
valid_CBE = valid.join(target_enc.transform(valid[cat_features]).add_suffix('_cb'))
# Train a model
train_model(train_CBE, valid_CBE)
output:
Validation AUC score: 0.7492
这比目标编码稍微好一点。
特征生成Feature Generation
这是一种常用的情况,可以将多行数据组合成有用的特征。
从原始数据创建新特征是改进模型的最佳方法之一。例如,在使用Kickstarter的数据时,你可以计算出最近一周的项目总数以及筹款的持续时间。你创建的特征对于每个数据集都是不同的,因此需要一些创造性和实验。这里有一点限制,因为我们只使用一个表。通常,你可以访问多个包含相关数据的表,你可以使用这些数据创建新特征。
但是你仍然可以看到如何使用分类特征来创建新的特征,然后是一些生成数字特征的例子。
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()
from sklearn.preprocessing import LabelEncoder
ks = pd.read_csv('../input/kickstarter-projects/ks-projects-201801.csv',
parse_dates=['deadline', 'launched'])
# Drop live projects
ks = ks.query('state != "live"')
# Add outcome column, "successful" == 1, others are 0
ks = ks.assign(outcome=(ks['state'] == 'successful').astype(int))
# Timestamp features
ks = ks.assign(hour=ks.launched.dt.hour,
day=ks.launched.dt.day,
month=ks.launched.dt.month,
year=ks.launched.dt.year)
# Label encoding
cat_features = ['category', 'currency', 'country']
encoder = LabelEncoder()
encoded = ks[cat_features].apply(encoder.fit_transform)
data_cols = ['goal', 'hour', 'day', 'month', 'year', 'outcome']
baseline_data = ks[data_cols].join(encoded)
交互 Interaction
创建新特性的最简单方法之一是结合分类变量。例如,如果一个记录的国家是“CA”,类别是“Music”,那么你可以创建一个新值“CA_Music”。这是一个新的分类特征,可以提供关于分类变量之间的相关性的信息。这种类型的特性通常称为交互interaction。
通常,你将从所有分类特征对构建交互特性。你也可以从三个或更多的功能中进行交互,但是你会得到递减的回报。
Pandas让我们像普通的Python字符串一样简单地将字符串列添加到一起。
interactions = ks['category'] + "_" + ks['country']
print(interactions.head(5))
output:
0 Poetry_GB
1 Narrative Film_US
2 Narrative Film_US
3 Music_US
4 Film & Video_US
dtype: object
然后,我们可以标签编码的交互特征,并添加到我们的数据。
label_enc = LabelEncoder()
data_interaction = baseline_data.assign(category_country=label_enc.fit_transform(interactions))
data_interaction.head()
goal | hour | day | month | year | outcome | category | currency | country | category_country | |
0 | 1000.0 | 12 | 11 | 8 | 2015 | 0 | 108 | 5 | 9 | 1900 |
1 | 30000.0 | 4 | 2 | 9 | 2017 | 0 | 93 | 13 | 22 | 1630 |
2 | 45000.0 | 0 | 12 | 1 | 2013 | 0 | 93 | 13 | 22 | 1630 |
3 | 5000 | .0 3 | 17 | 3 | 2012 | 0 | 90 | 13 | 22 | 1595 |
4 | 19500.0 | 8 | 4 | 7 | 2015 | 0 | 55 | 13 | 22 | 979 |
Number of projects in the last week
接下来,我们将计算每个记录在之前一周启动的项目的数量。我将在以“launch”列作为索引的系列上使用.rolling方法。我将用ks创建这个series。用ks.launched作为索引和ks.index作为值,然后排序时间。使用时间序列作为索引允许我们以小时、天、周等来定义滚动 rolling窗口的大小。
# First, create a Series with a timestamp index
launched = pd.Series(ks.index, index=ks.launched, name="count_7_days").sort_index()
launched.head(20)
output:
launched
1970-01-01 01:00:00 94579
1970-01-01 01:00:00 319002
1970-01-01 01:00:00 247913
1970-01-01 01:00:00 48147
1970-01-01 01:00:00 75397
1970-01-01 01:00:00 2842
1970-01-01 01:00:00 273779
2009-04-21 21:02:48 169268
2009-04-23 00:07:53 322000
2009-04-24 21:52:03 138572
2009-04-25 17:36:21 325391
2009-04-27 14:10:39 122662
2009-04-28 13:55:41 213711
2009-04-29 02:04:21 345606
2009-04-29 02:58:50 235255
2009-04-29 04:37:37 98954
2009-04-29 05:26:32 342226
2009-04-29 06:43:44 275091
2009-04-29 13:52:03 284115
2009-04-29 22:08:13 32898
Name: count_7_days, dtype: int64
有七个项目的发布日期 launch dates明显是错误的,但我们将忽略它们。同样,这是清理数据时需要处理的问题,但这不是这个小节的重点。
使用timeseries索引,你可以使用.rolling选择时间段作为窗口。例如launched.rolling(‘7d’)创建一个滚动窗口,其中包含前7天的所有数据。该窗口包含当前记录,因此如果我们想计算所有以前的项目,而不是当前的项目,我们需要减去1。我们将把结果画出来,以确保它看起来是正确的。
count_7_days = launched.rolling('7d').count() - 1
print(count_7_days.head(20))
# Ignore records with broken launch dates
plt.plot(count_7_days[7:]);
plt.title("Number of projects launched over periods of 7 days");
output:
launched
1970-01-01 01:00:00 0.0
1970-01-01 01:00:00 1.0
1970-01-01 01:00:00 2.0
1970-01-01 01:00:00 3.0
1970-01-01 01:00:00 4.0
1970-01-01 01:00:00 5.0
1970-01-01 01:00:00 6.0
2009-04-21 21:02:48 0.0
2009-04-23 00:07:53 1.0
2009-04-24 21:52:03 2.0
2009-04-25 17:36:21 3.0
2009-04-27 14:10:39 4.0
2009-04-28 13:55:41 5.0
2009-04-29 02:04:21 5.0
2009-04-29 02:58:50 6.0
2009-04-29 04:37:37 7.0
2009-04-29 05:26:32 8.0
2009-04-29 06:43:44 9.0
2009-04-29 13:52:03 10.0
2009-04-29 22:08:13 11.0
Name: count_7_days, dtype: float64
现在我们有了计数,我们需要调整索引,以便将其与其他训练数据连接起来。
count_7_days.index = launched.values
count_7_days = count_7_days.reindex(ks.index)
count_7_days.head(10)
0 1409.0
1 957.0
2 739.0
3 907.0
4 1429.0
5 1284.0
6 1119.0
7 1391.0
8 1043.0
9 3199.0
Name: count_7_days, dtype: float64
现在使用.join再次将新特性与其他数据连接起来,因为我们已经匹配了索引。
baseline_data.join(count_7_days).head(10)
goal | hour | day | month | year | outcome | category | currency | country | count_7_days | |
0 | 1000.0 | 12 | 11 | 8 | 2015 | 0 | 108 | 5 | 9 | 1409.0 |
1 | 30000.0 | 4 | 2 | 9 | 2017 | 0 | 93 | 13 | 22 | 957.0 |
2 | 45000.0 | 0 | 12 | 1 | 2013 | 0 | 93 | 13 | 22 | 739.0 |
3 | 5000.0 | 3 | 17 | 3 | 2012 | 0 | 90 | 13 | 22 | 907.0 |
4 | 19500.0 | 8 | 4 | 7 | 2015 | 0 | 55 | 13 | 22 | 1429.0 |
5 | 50000.0 | 13 | 26 | 2 | 2016 | 1 | 123 | 13 | 22 | 1284.0 |
6 | 1000.0 | 18 | 1 | 12 | 2014 | 1 | 58 | 13 | 22 | 1119.0 |
7 | 25000.0 | 20 | 1 | 2 | 2016 | 0 | 41 | 13 | 22 | 1391.0 |
8 | 125000.0 | 18 | 24 | 4 | 2014 | 0 | 113 | 13 | 22 | 1043.0 |
9 | 65000.0 | 21 | 11 | 7 | 2014 | 0 | 39 | 13 | 22 | 3199.0 |
Time since the last project in the same category
同一类别的项目是否会竞争捐助者?如果你想投资一款电子游戏,而另一个游戏项目刚刚启动,你可能不会得到那么多钱。我们可以通过计算自上次在同一类别中启动项目以来的时间来获取这一点。
在组内执行操作的一种方便的方法是使用.groupby 然后 .transform。.transform方法接受一个函数,然后为每个组向该函数传递一个序列或数据框架。这将返回一个与原始dataframe具有相同索引的dataframe。在我们的例子中,我们将对“category”执行groupby,并使用transform来计算每个category的时间差。
def time_since_last_project(series):
# Return the time in hours
return series.diff().dt.total_seconds() / 3600.
df = ks[['category', 'launched']].sort_values('launched')
timedeltas = df.groupby('category').transform(time_since_last_project)
timedeltas.head(20)
launched | |
94579 | NaN |
319002 | NaN |
247913 | NaN |
48147 | NaN |
75397 | NaN |
2842 | 0.000000 |
273779 NaN | |
169268 | NaN |
322000 | NaN |
138572 | NaN |
325391 | NaN |
122662 | 137.130833 |
213711 | NaN |
345606 | 145.941111 |
235255 | NaN |
98954 | 344715.626944 |
342226 | NaN |
275091 | NaN |
284115 | NaN |
32898 | NaN |
我们在这里为在其类别中的第一个-category获得NAN。这里需要填入均值或中位数。我们还需要重置索引,以便将其与其他数据连接起来。
# Final time since last project
timedeltas = timedeltas.fillna(timedeltas.median()).reindex(baseline_data.index)
timedeltas.head(20)
launched | |
0 | 18.606111 |
1 | 5.592778 |
2 | 1.313611 |
3 | 0.635000 |
4 | 16.661389 |
5 | 2.629722 |
6 | 0.367500 |
7 | 12.286111 |
8 | 14.243611 |
9 | 0.174722 |
10 | 1.372222 |
11 | 8.524444 |
12 | 0.015833 |
13 | 9.884444 |
14 | 1.725556 |
15 | 3.806111 |
16 | 2.654167 |
17 | 26.531667 |
18 | 12.273611 |
19 | 9.288889 |
Transforming numerical features
“goal”中的价值分布表明,大多数项目的目标都在5000美元以下。然而,达到10万美元的目标有一个长尾巴。当特征是正态分布时,有些模型工作得更好,所以它可能有助于目标值的转换。通常的选择是平方根和自然对数。这些转换还可以帮助约束异常值。
plt.hist(ks.goal, range=(0, 100000), bins=50);
plt.title('Goal');
plt.hist(np.sqrt(ks.goal), range=(0, 400), bins=50);
plt.title('Sqrt(Goal)');
plt.hist(np.log(ks.goal), range=(0, 25), bins=50);
plt.title('Log(Goal)');
由于基于树的模型是比例不变的,因此对数变换对我们的模型没有帮助。然而,如果我们有一个线性模型或神经网络,这应该会有所帮助。
其他的变换包括平方,幂,指数,等等。这些可能有助于模型区分,就像支持向量机的内核技巧一样。同样,我们也需要做一些实验来看看什么是有效的。一种方法是创建一堆新的特征,然后用特征选择算法选出最好的特征。
特征选择Feature Selection
在各种编码和特征生成之后,通常会有成百上千个特征。这可能导致两个问题。首先,你拥有的特征越多,就越有可能过度适应训练和验证集。这将导致你的模型在归纳新数据时性能变差。
其次,你拥有的特征越多,训练你的模型和优化超参数所需的时间就越长。另外,在构建面向用户的产品时,你需要尽可能快地做出推断。使用较少的特征可以加速推理,但代价是降低预测性能。
为了帮助解决这些问题,你需要使用特征选择技术来为你的模型保留信息最丰富的特征。
%matplotlib inline
import itertools
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.preprocessing import LabelEncoder
from sklearn import metrics
ks = pd.read_csv('../input/kickstarter-projects/ks-projects-201801.csv',
parse_dates=['deadline', 'launched'])
# Drop live projects
ks = ks.query('state != "live"')
# Add outcome column, "successful" == 1, others are 0
ks = ks.assign(outcome=(ks['state'] == 'successful').astype(int))
# Timestamp features
ks = ks.assign(hour=ks.launched.dt.hour,
day=ks.launched.dt.day,
month=ks.launched.dt.month,
year=ks.launched.dt.year)
# Label encoding
cat_features = ['category', 'currency', 'country']
encoder = LabelEncoder()
encoded = ks[cat_features].apply(encoder.fit_transform)
data_cols = ['goal', 'hour', 'day', 'month', 'year', 'outcome']
baseline_data = ks[data_cols].join(encoded)
cat_features = ['category', 'currency', 'country']
interactions = pd.DataFrame(index=ks.index)
for col1, col2 in itertools.combinations(cat_features, 2):
new_col_name = '_'.join([col1, col2])
# Convert to strings and combine
new_values = ks[col1].map(str) + "_" + ks[col2].map(str)
label_enc = LabelEncoder()
interactions[new_col_name] = label_enc.fit_transform(new_values)
baseline_data = baseline_data.join(interactions)
launched = pd.Series(ks.index, index=ks.launched, name="count_7_days").sort_index()
count_7_days = launched.rolling('7d').count() - 1
count_7_days.index = launched.values
count_7_days = count_7_days.reindex(ks.index)
baseline_data = baseline_data.join(count_7_days)
def time_since_last_project(series):
# Return the time in hours
return series.diff().dt.total_seconds() / 3600.
df = ks[['category', 'launched']].sort_values('launched')
timedeltas = df.groupby('category').transform(time_since_last_project)
timedeltas = timedeltas.fillna(timedeltas.max())
baseline_data = baseline_data.join(timedeltas.rename({'launched': 'time_since_last_project'}, axis=1))
def get_data_splits(dataframe, valid_fraction=0.1):
valid_fraction = 0.1
valid_size = int(len(dataframe) * valid_fraction)
train = dataframe[:-valid_size * 2]
# valid size == test size, last two sections of the data
valid = dataframe[-valid_size * 2:-valid_size]
test = dataframe[-valid_size:]
return train, valid, test
def train_model(train, valid):
feature_cols = train.columns.drop('outcome')
dtrain = lgb.Dataset(train[feature_cols], label=train['outcome'])
dvalid = lgb.Dataset(valid[feature_cols], label=valid['outcome'])
param = {'num_leaves': 64, 'objective': 'binary',
'metric': 'auc', 'seed': 7}
print("Training model!")
bst = lgb.train(param, dtrain, num_boost_round=1000, valid_sets=[dvalid],
early_stopping_rounds=10, verbose_eval=False)
valid_pred = bst.predict(valid[feature_cols])
valid_score = metrics.roc_auc_score(valid['outcome'], valid_pred)
print(f"Validation AUC score: {valid_score:.4f}")
return bst
单变量特征选择Univariate Feature Selection
最简单和最快的方法是基于单变量统计检验。对于每个特征,使用统计检验(如或方差分析ANOVA)来衡量目标对特征的依赖程度。
来自scikiti - learning feature selection模块的feature_selection.SelectKBest返回给定评分函数的K个最佳特性。对于我们的分类问题,该模块提供了三种不同的得分函数: 、方差分析F值ANOVA F-value、交互信息得分the mutual information score。F值度量特征变量和目标之间的线性依赖关系。这意味着,如果特征和目标之间的关系是非线性的,那么得分可能会低估它们之间的关系。交互信息评分是非参数的,因此可以捕捉非线性关系。
使用SelectKBest,我们根据得分函数的分数来定义要保留的特征数量。通过使用.fit_transform(features, target),我们得到一个仅包含选中特征的数组。
from sklearn.feature_selection import SelectKBest, f_classif
feature_cols = baseline_data.columns.drop('outcome')
# Keep 5 features
selector = SelectKBest(f_classif, k=5)
X_new = selector.fit_transform(baseline_data[feature_cols], baseline_data['outcome'])
X_new
output:
array([[2015., 5., 9., 18., 1409.],
[2017., 13., 22., 31., 957.],
[2013., 13., 22., 31., 739.],
...,
[2010., 13., 22., 31., 238.],
[2016., 13., 22., 31., 1100.],
[2011., 13., 22., 31., 542.]])
但是,我做错了一些事情。统计检验是用所有的数据计算出来的。这意味着来自验证和测试集的信息可能会影响我们保留的特征,从而引入泄漏源。这意味着我们应该只用一个训练集来选择特征。
feature_cols = baseline_data.columns.drop('outcome')
train, valid, _ = get_data_splits(baseline_data)
# Keep 5 features
selector = SelectKBest(f_classif, k=5)
X_new = selector.fit_transform(train[feature_cols], train['outcome'])
X_new
output:
array([[2.015e+03, 5.000e+00, 9.000e+00, 1.800e+01, 1.409e+03],
[2.017e+03, 1.300e+01, 2.200e+01, 3.100e+01, 9.570e+02],
[2.013e+03, 1.300e+01, 2.200e+01, 3.100e+01, 7.390e+02],
...,
[2.011e+03, 1.300e+01, 2.200e+01, 3.100e+01, 5.150e+02],
[2.015e+03, 1.000e+00, 3.000e+00, 2.000e+00, 1.306e+03],
[2.013e+03, 1.300e+01, 2.200e+01, 3.100e+01, 1.084e+03]])
你应该注意到,所选择的特征与我使用整个数据集时不同。现在我们有了选择的特征,但它只是训练集的特征值。要从验证集和测试集删除被拒绝的特征,我们需要知道SelectKBest保留了数据集中的哪些列。为此,我们可以使用.inverse_transform返回具有原始数据形状的数组。
# Get back the features we've kept, zero out all other features
selected_features = pd.DataFrame(selector.inverse_transform(X_new),
index=train.index,
columns=feature_cols)
selected_features.head()
goal | hour | day | month | year | category | currency | country | category_currency | category_country | currency_country | count_7_days | time_since_last_project | |
0 | 0.0 | 0.0 | 0.0 | 0.0 | 2015.0 | 0.0 | 5.0 | 9.0 | 0.0 | 0.0 | 18.0 | 1409.0 | 0.0 |
1 | 0.0 | 0.0 | 0.0 | 0.0 | 2017.0 | 0.0 | 13.0 | 22.0 | 0.0 | 0.0 | 31.0 | 957.0 | 0.0 |
2 | 0.0 | 0.0 | 0.0 | 0.0 | 2013.0 | 0.0 | 13.0 | 22.0 | 0.0 | 0.0 | 31.0 | 739.0 | 0.0 |
3 | 0.0 | 0.0 | 0.0 | 0.0 | 2012.0 | 0.0 | 13.0 | 22.0 | 0.0 | 0.0 | 31.0 | 907.0 | 0.0 |
4 | 0.0 | 0.0 | 0.0 | 0.0 | 2015.0 | 0.0 | 13.0 | 22.0 | 0.0 | 0.0 | 31.0 | 1429.0 | 0.0 |
这将返回一个具有与训练集相同的索引和列的DataFrame,但是所有被删除的列都用0填充。我们可以通过选择方差非零的特征来找到所选的列。
# Dropped columns have values of all 0s, so var is 0, drop them
selected_columns = selected_features.columns[selected_features.var() != 0]
# Get the valid dataset with the selected features.
valid[selected_columns].head()
year | currency | country | currency_country | count_7_days | |
302896 | 2015 | 13 | 22 | 31 | 1534.0 |
302897 | 2013 | 13 | 22 | 31 | 625.0 |
302898 | 2014 | 5 | 9 | 18 | 851.0 |
302899 | 2014 | 13 | 22 | 31 | 1973.0 |
302900 | 2014 | 5 | 9 | 8 | 2163.0 |
为了找到K的最佳值,可以用K的递增值来拟合多个模型,然后选择验证得分超过某个阈值或其他标准的最小K。一个很好的方法是循环K的值,并记录每次迭代的验证得分。
L1 regularization
单变量方法在做选择决策时只考虑一个特征。相反,我们可以通过L1正则化的线性模型来使用所有的特征来进行选择。这种类型的正则化(有时称为Lasso)惩罚系数的绝对值,与L2(岭)回归惩罚系数的平方。
随着正则化强度的增加,将对预测目标不那么重要的特征设置为0。这使得我们可以通过调整正则化参数来进行特征选择。我们选择参数的方法是在一个保留的集合上找到最佳性能,或者提前决定保留多少特征。
对于回归问题,可以使用sklearn.linear_model.Lassor,或sklearn.linear_model.LogisticRegression来进行分类。这些可以与sklearn.feature_selection.SelectFromModel来选择非零系数。否则,代码类似于单变量测试。
from sklearn.linear_model import LogisticRegression
from sklearn.feature_selection import SelectFromModel
train, valid, _ = get_data_splits(baseline_data)
X, y = train[train.columns.drop("outcome")], train['outcome']
# Set the regularization parameter C=1
logistic = LogisticRegression(C=1, penalty="l1", solver='liblinear', random_state=7).fit(X, y)
model = SelectFromModel(logistic, prefit=True)
X_new = model.transform(X)
X_new
output:
array([[1.000e+03, 1.200e+01, 1.100e+01, ..., 1.900e+03, 1.800e+01,
1.409e+03],
[3.000e+04, 4.000e+00, 2.000e+00, ..., 1.630e+03, 3.100e+01,
9.570e+02],
[4.500e+04, 0.000e+00, 1.200e+01, ..., 1.630e+03, 3.100e+01,
7.390e+02],
...,
[2.500e+03, 0.000e+00, 3.000e+00, ..., 1.830e+03, 3.100e+01,
5.150e+02],
[2.600e+03, 2.100e+01, 2.300e+01, ..., 1.036e+03, 2.000e+00,
1.306e+03],
[2.000e+04, 1.600e+01, 4.000e+00, ..., 9.200e+02, 3.100e+01,
1.084e+03]])
与单变量检验类似,我们得到一个具有所选特性的数组。同样,我们将希望将这些转换为DataFrame,以便获得所选列。
# Get back the kept features as a DataFrame with dropped columns as all 0s
selected_features = pd.DataFrame(model.inverse_transform(X_new),
index=X.index,
columns=X.columns)
# Dropped columns have values of all 0s, keep other columns
selected_columns = selected_features.columns[selected_features.var() != 0]
在L1参数C=1的情况下,我们将删除time_since_last_project列。
一般来说,L1正则化的特征选择比单变量检验更强大,但是当你有很多数据和很多特征时,它也会很慢。单变量检验在大型数据集上会快得多,但也可能执行得更差。