from https://zhuanlan.zhihu.com/p/368421880
前言
本文我们将使用功能工具库介绍如何将自动功能工程应用到家庭信用违约风险数据集。Featuretools是一个开源Python包,用于从多个结构化的相关数据表自动创建新特征。自动特征工程旨在通过从数据集中自动构建数百或数千个新特征来帮助竞赛/机器学习玩家解决特征创建问题。
# pandas and numpy for data manipulation
import pandas as pd
import numpy as np
# featuretools for automated feature engineering
import featuretools as ft
# matplotlit and seaborn for visualizations
import matplotlib.pyplot as plt
plt.rcParams['font.size'] = 22
import seaborn as sns
# Suppress warnings from pandas
import warnings
warnings.filterwarnings('ignore')
问题
该案例是一个监督分类机器学习任务。目的是使用历史财务和社会经济数据来预测申请人是否有能力偿还贷款。这是一个标准的二分类的监督任务:
监督:标签包含在训练数据中,目标是训练一个模型来学习从特征中预测标签
分类:标签为二元变量,0(会按时还款),1(还款困难)
数据集
这些数据是由Home Credit提供的(文末获取),这是一项专门为没有银行账户的人群提供信贷(贷款)的服务,有7个不同的数据文件:
- application_train/application_test:包含Home Credit每个贷款申请信息的主要训练和测试数据。每笔贷款都有自己的一条记录,并由SK_ID_CURR标识。TARGET附带的训练数据指示0:已偿还贷款,1:未偿还贷款。
- bureau:客户在其他金融机构的信用记录。每个以前的信用在bureau中有它自己的记录,由SK_ID_BUREAU标识,数据中的每个贷款可以有多个以前的信用。
- Bureau_balance:每月在该局的信用数据。每一行是一个月以前的信用,一个以前的信用可以有多个行,一个行对应一个月的信用长度。
- previous_application:申请数据中有贷款的客户以前在Home Credit的贷款申请。申请数据中的每个当前贷款可以有多个以前的贷款,由特征SK_ID_PREV标识。
- POS_CASH_BALANCE:每月的数据,以前的销售点或现金贷款客户,每一行是前一个销售点或现金贷款的一个月,并且单个前一个贷款可以有许多行。
- credit_card_balance:关于客户在Home credit的信用卡月度数据。每一行是一个月的信用卡余额,一张信用卡可以有许多行。
- installments_payment: Home Credit以前贷款的支付历史。每一次付款都有一行,每一次未付款都有一行。
下面的图表(官方提供)显示了表格之间的关系。当我们需要在特征工具中定义关系时,这将非常有用。
- 读取数据,按SK_ID_CURR排序,只保留前1000行(为了计算演示方便)。
# Read in the datasets and limit to the first 1000 rows (sorted by SK_ID_CURR)
# This allows us to actually see the results in a reasonable amount of time!
app_train = pd.read_csv('../input/home-credit-default-risk/application_train.csv').sort_values('SK_ID_CURR').reset_index(drop = True).loc[:1000, :]
app_test = pd.read_csv('../input/home-credit-default-risk/application_test.csv').sort_values('SK_ID_CURR').reset_index(drop = True).loc[:1000, :]
bureau = pd.read_csv('../input/home-credit-default-risk/bureau.csv').sort_values(['SK_ID_CURR', 'SK_ID_BUREAU']).reset_index(drop = True).loc[:1000, :]
bureau_balance = pd.read_csv('../input/home-credit-default-risk/bureau_balance.csv').sort_values('SK_ID_BUREAU').reset_index(drop = True).loc[:1000, :]
cash = pd.read_csv('../input/home-credit-default-risk/POS_CASH_balance.csv').sort_values(['SK_ID_CURR', 'SK_ID_PREV']).reset_index(drop = True).loc[:1000, :]
credit = pd.read_csv('../input/home-credit-default-risk/credit_card_balance.csv').sort_values(['SK_ID_CURR', 'SK_ID_PREV']).reset_index(drop = True).loc[:1000, :]
previous = pd.read_csv('../input/home-credit-default-risk/previous_application.csv').sort_values(['SK_ID_CURR', 'SK_ID_PREV']).reset_index(drop = True).loc[:1000, :]
installments = pd.read_csv('../input/home-credit-default-risk/installments_payments.csv').sort_values(['SK_ID_CURR', 'SK_ID_PREV']).reset_index(drop = True).loc[:1000, :]
- 训练集和测试集拼接在一起,加一个训练/测试的标识来区别:
# Add identifying column
app_train['set'] = 'train'
app_test['set'] = 'test'
app_test["TARGET"] = np.nan
# Append the dataframes
app = app_train.append(app_test, ignore_index = True)
Featuretools
Featuretools是一个开源Python库,它可以使用一种称为深度特征合成的技术,从一组相关的表中自动创建特征(就是自动化特征工程),为了更好的利用,讨论一些想法和概念,帮助理解featuretools的使用。
- 实体和实体集合
- 表之间的关系
- 特征基础:聚合与转化
- 深度特征创建
实体和实体集合
一个实体就是一个表,或者在Pandas中是一个数据框。观察的结果(数据属性)在行中,特征在列中。featuretools中的实体必须有一个唯一的索引,其中没有重复的元素。目前,只有app、bureau和previous有惟一索引(分别为SK_ID_CURR、SK_ID_BUREAU和SK_ID_PREV)。对于其他数据框,我们必须传入make_index = True,然后指定索引的名称。实体还可以有时间索引,其中每一条记录由唯一的时间标识。(上面数据中都没有日期时间,但有相对时间,以月或日为单位,我们可以考虑将其作为时间变量)。
EntitySet是表及其之间关系的集合。这可以看作是具有自己的方法和属性的数据结构。使用EntitySet允许我们将多个表组合在一起,并比操作单个表快得多。
首先,我们将创建一个名为clients的空实体集来跟踪所有数据。
# Entity set with id applications
es = ft.EntitySet(id = 'clients')
现在我们定义每个实体或数据表。如果数据有索引,则需要传入索引;如果没有,则传入make_index = True。Featuretools会自动推断变量的类型,但是如果需要,也可以更改它们。例如,如果我们有一个用整数表示的分类变量,我们可能希望让featuretools知道正确的类型。
# Entities with a unique index
es = es.entity_from_dataframe(entity_id = 'app', dataframe = app, index = 'SK_ID_CURR')
es = es.entity_from_dataframe(entity_id = 'bureau', dataframe = bureau, index = 'SK_ID_BUREAU')
es = es.entity_from_dataframe(entity_id = 'previous', dataframe = previous, index = 'SK_ID_PREV')
# Entities that do not have a unique index
es = es.entity_from_dataframe(entity_id = 'bureau_balance', dataframe = bureau_balance,
make_index = True, index = 'bureaubalance_index')
es = es.entity_from_dataframe(entity_id = 'cash', dataframe = cash,
make_index = True, index = 'cash_index')
es = es.entity_from_dataframe(entity_id = 'installments', dataframe = installments,
make_index = True, index = 'installments_index')
es = es.entity_from_dataframe(entity_id = 'credit', dataframe = credit,
make_index = True, index = 'credit_index')
表之间的关系
这里举一个简单明了的例子:父母是单个个体,但可以有多个孩子。然后,这些孩子可以拥有多个自己的孩子。在父表中,每个个体都有一行。父表中的每个个体在子表中可以有多行,可以联系数据库的关系去理解。
例如,application数据框对于每个客户端(SK_ID_CURR)有一行,而bureau的数据框对于每个客户端(SK_ID_CURR)有多个以前的贷款(SK_ID_PREV)。因此,bureau数据框是application数据框的子框。而bureau数据框又是bureau_balance的父类,因为每笔贷款在bureau中有一行,但在bureau_balance中有多个月度记录。
注⚠️:建议把表下下来,先熟悉表结构。
print('Parent: app, Parent Variable: SK_ID_CURR\n\n', app.iloc[:, 111:115].head())
print('\nChild: bureau, Child Variable: SK_ID_CURR\n\n', bureau.iloc[10:30, :4].head())
—-output
Parent: app, Parent Variable: SK_ID_CURR
SK_ID_CURR TARGET TOTALAREA_MODE WALLSMATERIAL_MODE
0 100002 1.0 0.0149 Stone, brick
1 100003 0.0 0.0714 Block
2 100004 0.0 NaN NaN
3 100006 0.0 NaN NaN
4 100007 0.0 NaN NaN
Child: bureau, Child Variable: SK_ID_CURR
SK_ID_CURR SK_ID_BUREAU CREDIT_ACTIVE CREDIT_CURRENCY
10 100002 6158905 Closed currency 1
11 100002 6158906 Closed currency 1
12 100002 6158907 Closed currency 1
13 100002 6158908 Closed currency 1
14 100002 6158909 Active currency 1
SK_ID_CURR“100002”;在父表中有一行,在子表中有多行。
两个表通过一个共享变量连接起来。application和bureau数据框通过SK_ID_CURR变量连接,而bureau和bureau_balance数据框通过SK_ID_BUREAU链接。定义关系相对简单,官方提供的图表有助于查看关系。对于每个关系,我们需要指定父变量和子变量。表之间总共有6种关系。下面我们指定所有6个关系,然后将它们添加到EntitySet中。
# Relationship between app and bureau
r_app_bureau = ft.Relationship(es['app']['SK_ID_CURR'], es['bureau']['SK_ID_CURR'])
# Relationship between bureau and bureau balance
r_bureau_balance = ft.Relationship(es['bureau']['SK_ID_BUREAU'], es['bureau_balance']['SK_ID_BUREAU'])
# Relationship between current app and previous apps
r_app_previous = ft.Relationship(es['app']['SK_ID_CURR'], es['previous']['SK_ID_CURR'])
# Relationships between previous apps and cash, installments, and credit
r_previous_cash = ft.Relationship(es['previous']['SK_ID_PREV'], es['cash']['SK_ID_PREV'])
r_previous_installments = ft.Relationship(es['previous']['SK_ID_PREV'], es['installments']['SK_ID_PREV'])
r_previous_credit = ft.Relationship(es['previous']['SK_ID_PREV'], es['credit']['SK_ID_PREV'])
# Add in the defined relationships
es = es.add_relationships([r_app_bureau, r_bureau_balance, r_app_previous,
r_previous_cash, r_previous_installments, r_previous_credit])
# Print out the EntitySet
es
—-output
Entityset: clients
Entities:
app [Rows: 2002, Columns: 123]
bureau [Rows: 1001, Columns: 17]
previous [Rows: 1001, Columns: 37]
bureau_balance [Rows: 1001, Columns: 4]
cash [Rows: 1001, Columns: 9]
installments [Rows: 1001, Columns: 9]
credit [Rows: 1001, Columns: 24]
Relationships:
bureau.SK_ID_CURR -> app.SK_ID_CURR
bureau_balance.SK_ID_BUREAU -> bureau.SK_ID_BUREAU
previous.SK_ID_CURR -> app.SK_ID_CURR
cash.SK_ID_PREV -> previous.SK_ID_PREV
installments.SK_ID_PREV -> previous.SK_ID_PREV
credit.SK_ID_PREV -> previous.SK_ID_PREV
注意⚠️: 我们需要小心不要创建一个从父到子有多条路径的菱形图。
- SK_ID_CURR直接连接app和cash;
- previous和cash通过SK_ID_PREV;
- app和previous通过SK_ID_CURR,
然后我们创建了从app到cash的两条路径,这会导致模糊性,所以我们必须采取的方法是将app与cash通过previous,我们使用SK_ID_PREV在previous(父)和cash(子)之间建立关系,然后我们使用SK_ID_CURR在app(父类)和previous(现在是子类)之间建立关系,然后功能工具将能够通过堆叠多个原语,在app上创建从previous和cash派生的功能。
实体中的所有实体都可以相互关联。理论上,允许我们计算任何实体的功能,但实际上,我们将只计算app数据框的功能,因为我们最后是用于训练/测试。
特征基础:聚合与转换
特征基础是应用于一个表或一组表来创建特征的操作。这些代表了简单的计算,其实我们已经在手工特征工程中经常使用,可以堆叠在一起创建复杂的特征。特征基础分为两类:
- Aggregation(聚合)
- Transformation(转换)
看下面的具体应用
# List the primitives in a dataframe
primitives = ft.list_primitives()
pd.options.display.max_colwidth = 100
primitives[primitives['type'] == 'aggregation'].head(10)
primitives[primitives['type'] == 'transform'].head(10)
深度特征创建
深度特征创建(Deep Feature Synthesis, DFS)是特征工具用来生成新特征的过程。DFS堆叠功能原话是:形成具有“深度”的功能。例如:
- 如果我们取客户以前贷款的最大值(比如MAX(previous.loan_amount)),这是一个“深度特征”;深度为1。
- 要创建一个深度为2的特征,我们可以通过取客户上一次贷款的平均每月付款的最大值(例如MAX(previous(MEAN(installments.payment)))).))。
关于使用深度特征创建的自动特征工程的论文大家有时间可以看看:https://dai.lids.mit.edu/wp-content/uploads/2017/10/DSAA_DSM_2015.pdf
- 为了在featuretools中执行DFS,我们使用DFS函数传递一个实体集、target_entity(我们想要创建特征的地方)、要使用agg_primitives、要使用trans_primitives和特征的max_depth。
- 这里我们将使用默认的Aggregation和Transformation(转换),最大深度为2,并为应用实体计算原图。因为这个过程的计算开销很大,所以可以使用features_only = True来运行函数,只返回一个特性列表,而不计算特征本身。
使用默认的DFS
# Default primitives from featuretools
default_agg_primitives = ["sum", "std", "max", "skew", "min", "mean", "count", "percent_true", "num_unique", "mode"]
default_trans_primitives = ["day", "year", "month", "weekday", "haversine", "numwords", "characters"]
# DFS with specified primitives
feature_names = ft.dfs(entityset = es, target_entity = 'app',
trans_primitives = default_trans_primitives,
agg_primitives=default_agg_primitives,
max_depth = 2, features_only=True)
print('%d Total Features' % len(feature_names))
—-output
1697 Total Features
- 生成功能的子集
# DFS with default primitives
feature_matrix, feature_names = ft.dfs(entityset = es, target_entity = 'app',
trans_primitives = default_trans_primitives,
agg_primitives=default_agg_primitives,
max_depth = 2, features_only=False, verbose = True)
pd.options.display.max_columns = 1700
feature_matrix.head(10)
—-output
Built 1697 features
Elapsed: 00:40 | Remaining: 00:00 | Progress: 100%|██████████| Calculated: 11/11 chunks
自定义DFS
有了特征工具,我们可以在几行代码中从121个原始特性增加到将近1700个特征。虽然我们在featuretools中获得了很多特征,但是这个函数调用并不是很通用。我们只是简单地使用了默认聚合,而没有考虑哪些聚合是“重要的”聚合。我们最终获得了许多特征,但它们可能并不都与我们的问题相关。过多的无关特征会淹没重要特性,从而降低性能。
下一个调用将指定更小的特征集合。仍然没有使用太多领域相关的知识,但是这个特征集合将更易于管理。接下来的步骤就是改进我们实际构建的特征并执行特征选择。
# Specify the aggregation primitives
feature_matrix_spec, feature_names_spec = ft.dfs(entityset = es, target_entity = 'app',
agg_primitives = ['sum', 'count', 'min', 'max', 'mean', 'mode'],
max_depth = 2, features_only = False, verbose = True)
—-output
Built 884 features
Elapsed: 00:15 | Remaining: 00:00 | Progress: 100%|██████████| Calculated: 11/11 chunks
这“仅仅”为我们提供了884个特性(在完整的数据集上运行大约需要12个小时)。
为了确定我们的功能工具的基本实现是否有用,我们可以看几个结果:
- 相关性:特征和TARGET之间,以及特征本身之间
- 特征重要性: 可以通过树模型输出
特征性能实验(分离特征效果)
为了比较机器学习任务的一些不同的特征集,我设置了几个实验。为了分离特征的效果,使用相同的模型来测试多个不同的特征集。LightGBM算法,5折交叉验证进行训练和验证;
- 对照组:仅使用来自app数据集的数据
- 测试一:仅使用app、bureau和bureau_balance数据的手动特征工程
- 测试二:使用所有数据集的手动特征工程
- 测试三:featuretools默认(在feature_matrix中)
- 测试四:featuretools自定义(在feature_matrix_spec中)
- 测试五:特征工具自定义的特征与手工特征工程相结合
实验对比结果:
可视化相关变量的分布
features_sample = pd.read_csv('../input/home-credit-default-risk-feature-tools/feature_matrix.csv', nrows = 20000)
features_sample = features_sample[features_sample['set'] == 'train']
features_sample.head()
def kde_target_plot(df, feature):
"""Kernel density estimate plot of a feature colored
by value of the target."""
# Need to reset index for loc to workBU
df = df.reset_index()
plt.figure(figsize = (10, 6))
plt.style.use('fivethirtyeight')
# plot repaid loans
sns.kdeplot(df.loc[df['TARGET'] == 0, feature], label = 'target == 0')
# plot loans that were not repaid
sns.kdeplot(df.loc[df['TARGET'] == 1, feature], label = 'target == 1')
# Label the plots
plt.title('Distribution of Feature by Target Value')
plt.xlabel('%s' % feature); plt.ylabel('Density');
plt.show()
kde_target_plot(features_sample, feature = 'MAX(previous_app.MEAN(credit.CNT_DRAWINGS_ATM_CURRENT))')
这里演示一个,可以跑多个特征,看其中的关系。
共线特征
高度相关的特征,即共线特征,寻找相关的特征对,删除超过阈值的特征。
threshold = 0.9
correlated_pairs = {}
# Iterate through the columns
for col in correlations:
# Find correlations above the threshold
above_threshold_vars = [x for x in list(correlations.index[correlations[col] > threshold]) if x != col]
correlated_pairs[col] = above_threshold_vars
plt.plot(features_sample['MEAN(credit.AMT_PAYMENT_TOTAL_CURRENT)'], features_sample['MEAN(previous_app.MEAN(credit.AMT_PAYMENT_CURRENT))'], 'bo')
plt.title('Highly Correlated Features');
这些变量之间的相关性都是0.99,几乎是完全正线性的。将它们全部包含在模型中是不必要的,因为这将编码冗余信息,我们可能想要删除一些高度相关的变量,以帮助模型更好地学习。
特征重要性
由基于树模型返回的特征重要性表示将特征包含在模型中所带来impurity减少。虽然重要性的绝对值很难解释,但是查看重要性的相对值可以让我们比较特征的重要性。
- 前十五的重要性特征:
# Read in the feature importances and sort with the most important at the top
fi = pd.read_csv('../input/home-credit-default-risk-feature-tools/spec_feature_importances_ohe.csv', index_col = 0)
fi = fi.sort_values('importance', ascending = False)
fi.head(15)
- featuretools创建的最重要的特征是客户向另一个机构申请贷款前的最大天数。(这个特征最初被记录为负,所以最大值最接近于零)。
kde_target_plot(features_sample, feature = 'MAX(bureau.DAYS_CREDIT)')
- 可以计算出由featuretools的前100特征的数量。
# List of the original features (after one-hot)
original_features = list(pd.get_dummies(app).columns)
created_features = []
# Iterate through the top 100 features
for feature in fi['feature'][:100]:
if feature not in original_features:
created_features.append(feature)
print('%d of the top 100 features were made by featuretools' % len(created_features))
- 可视化15个最重要的特征。
import matplotlib.pyplot as plt
plt.rcParams['font.size'] = 22
def plot_feature_importances(df):
"""
Plot importances returned by a model. This can work with any measure of
feature importance provided that higher importance is better.
Parameters
--------
df : dataframe
feature importances. Must have the features in a column
called `features` and the importances in a column called `importance
Return
-------
shows a plot of the 15 most importance features
df : dataframe
feature importances sorted by importance (highest to lowest)
with a column for normalized importance
"""
# Sort features according to importance
df = df.sort_values('importance', ascending = False).reset_index()
# Normalize the feature importances to add up to one
df['importance_normalized'] = df['importance'] / df['importance'].sum()
# Make a horizontal bar chart of feature importances
plt.figure(figsize = (14, 10))
ax = plt.subplot()
# Need to reverse the index to plot most important on top
ax.barh(list(reversed(list(df.index[:15]))),
df['importance_normalized'].head(15),
align = 'center', edgecolor = 'k')
# Set the yticks and labels
ax.set_yticks(list(reversed(list(df.index[:15]))))
ax.set_yticklabels(df['feature'].head(15))
# Plot labeling
plt.xlabel('Normalized Importance'); plt.title('Feature Importances')
plt.show()
return df
fi = plot_feature_importances(fi)
- featuretools创建的最重要的特征是MAX(bureau.DAYS_CREDIT)。DAYS_CREDIT表示申请人向另一个信贷机构申请贷款的当前申请之前的天数。因此,这个值(超过以前的贷款)的最大值由这个特征表示;
- 其他几个重要的特征,它们的深度为2,比如MEAN(previous_app.MIN(installments.AMT_PAYMENT)),它是客户的贷款的平均值,是之前的信用申请分期付款的最小值。
- 特征的重要性可以用于降维。它们也可以用来帮助我们更好地理解问题。例如,在评估一笔潜在贷款时,我们可以使用最重要的特征来专注于客户的这些方面,从特征集中删除的0重要性特性的数量。
print('There are %d features with 0 importance' % sum(fi['importance'] == 0.0))
删除低重要性的特征
删除任何只有一个唯一值或全部为null的特征;Featuretools在selection模块中有一个默认的方法。
from featuretools import selection
# Remove features with only one unique value
feature_matrix2 = selection.remove_low_information_features(feature_matrix)
print('Removed %d features' % (feature_matrix.shape[1]- feature_matrix2.shape[1]))
—-output
Removed 159 features
结论
本文从家庭信用违约风险数据集出发,并从实体和实体集合、表之间的关系、特征基础:聚合与转化、深度特征创建四个方面,介绍了Featuretools工具包的使用,通过使用Featuretools开源库大大节省了我们手动工程的时间,可能存在的问题是所构建的特征有些可能是无用的,同时避免维度灾难,我们通过特征重要性选择,剔除无用特征,实现完成的特征工程。
在后面的文章中,将会介绍一些从业务角度出发构建的特征工程。
更多资料:
https://towardsdatascience.com/automated-feature-engineering-in-python-99baf11cc219