此文为翻译转载
- 项目文献
- 原文代码
2.正文
机器学习模型只能从我们给定的数据中学习,所以构造一个和任务相关的特征是至关重要的,参见优质论文《A Few Useful Things to Know about Machine Learning》。
然而,人工特性工程是一项冗长乏味的任务,并且受到人类想象力的限制——我们可以思考创建的特性只有这么多,而且随着时间的推移,创建新特性需要大量的时间。理想情况下,应该有一个客观的方法来创建一系列不同的候选新特性,然后我们可以将这些特性用于机器学习任务。这个过程的目的不是替换数据科学家,而是使她的工作更容易,并允许她使用自动工作流补充领域知识。
本文,我们将介绍使用Featuretools(一个开源的Python库)实现自动创建特性(数据位于结构化表中)。
数据
clients: 客户的基本信息。
loans: 客户历史贷款记录。
payments:客户历史还款记录。
import pandas as pd
import numpy as np
from datetime import datetime
import random
rand_dates = []
for _ in range(1000):
year = random.choice(range(2000, 2015))
month = random.choice(range(1, 13))
day = random.choice(range(1, 29))
rdate = datetime(year, month, day)
rand_dates.append(rdate)
clients = pd.DataFrame(columns = ['client_id', 'joined', 'income', 'credit_score'])
for _ in range(25):
clients = clients.append(pd.DataFrame({'client_id': np.random.randint(25000, 50000, size = 1)[0], 'joined': random.choice(rand_dates),
'income': np.random.randint(30500, 240000, size = 1)[0], 'credit_score': np.random.randint(500, 850, size = 1)[0]},
index = [0]), ignore_index = True)
clients.head()
loans = pd.DataFrame(columns = ['client_id', 'loan_type', 'loan_amount', 'repaid',
'loan_id', 'loan_start', 'loan_end', 'rate'])
for client in clients['client_id'].unique():
for _ in range(20):
time_created = pd.datetime(np.random.randint(2000, 2015, size = 1)[0],
np.random.randint(1, 13, size = 1)[0],
np.random.randint(1, 30, size = 1)[0])
time_ended = time_created + pd.Timedelta(days = np.random.randint(500, 1000, size = 1)[0])
loans = loans.append(pd.DataFrame({'client_id': client, 'loan_type': random.choice(['cash', 'credit', 'home', 'other']),
'loan_amount': np.random.randint(500, 15000, size = 1)[0],
'repaid': random.choice([0, 1]),
'loan_id': np.random.randint(10000, 12000, size = 1)[0],
'loan_start': time_created,
'loan_end': time_ended,
'rate': round(abs(4 * np.random.randn(1)[0]), 2)}, index = [0]), ignore_index = True)
payments = pd.DataFrame(columns = ['loan_id', 'payment_amount',
'payment_date', 'missed'])
for _, row in loans.iterrows():
time_created = row['loan_start']
payment_date = time_created + pd.Timedelta(days = 30)
loan_amount = row['loan_amount']
loan_id = row['loan_id']
payment_id = np.random.randint(10000, 12000, size = 1)[0]
for _ in range(np.random.randint(5, 10, size = 1)[0]):
payment_id += 1
payment_date += pd.Timedelta(days = np.random.randint(10, 50, size = 1)[0])
payments = payments.append(pd.DataFrame({'loan_id': loan_id,
'payment_amount': np.random.randint(int(loan_amount / 10), int(loan_amount / 5), size = 1)[0],
'payment_date': payment_date, 'missed': random.choice([0, 1])}, index = [0]), ignore_index = True)
clients = clients.drop_duplicates(subset = 'client_id')
loans = loans.drop_duplicates(subset = 'loan_id')
clients.to_csv('clients.csv', index = False)
loans.to_csv('loans.csv', index = False)
payments.to_csv('payments.csv', index = False)
import pandas as pd
import numpy as np
import featuretools as ft
# Read in the data
clients = pd.read_csv('data/clients.csv', parse_dates = ['joined'])
loans = pd.read_csv('data/loans.csv', parse_dates = ['loan_start', 'loan_end'])
payments = pd.read_csv('data/payments.csv', parse_dates = ['payment_date'])
clinets.head(5)
index | client_id | joined | income | credit_score |
---|---|---|---|---|
0 | 46109 | 2002-04-16 | 172677 | 527 |
1 | 49545 | 2007-11-14 | 104564 | 770 |
2 | 41480 | 2013-03-11 | 122607 | 585 |
3 | 46180 | 2001-11-06 | 43851 | 562 |
4 | 25707 | 2006-10-06 | 211422 | 621 |
loans.head(5)
index | client_id | loan_type | loan_amount | repaid | loan_id | loan_start | loan_end | rate |
---|---|---|---|---|---|---|---|---|
0 | 46109 | home | 13672 | 0 | 10243 | 2002-04-16 | 2003-12-20 | 2.15 |
1 | 46109 | credit | 9794 | 0 | 10984 | 2003-10-21 | 2005-07-17 | 1.25 |
2 | 46109 | home | 12734 | 1 | 10990 | 2006-02-01 | 2007-07-05 | 0.68 |
3 | 46109 | cash | 12518 | 1 | 10596 | 2010-12-08 | 2013-05-05 | 1.24 |
4 | 46109 | credit | 14049 | 1 | 11415 | 2010-07-07 | 2012-05-21 | 3.13 |
payments.sample(5)
index | loan_id | payment_amount | payment_date | missed |
---|---|---|---|---|
258 | 10425 | 1448 | 2002-03-16 | 1 |
2104 | 10116 | 937 | 2001-10-06 | 1 |
2280 | 11546 | 1149 | 2003-02-03 | 0 |
2123 | 10198 | 1709 | 2012-11-07 | 0 |
2703 | 11481 | 1660 | 2009-04-24 | 1 |
使用pandas对loans进行变量衍生,然后和clients合并
# Groupby client id and calculate mean, max, min previous loan size
stats = loans.groupby('client_id')['loan_amount'].agg(['mean', 'max', 'min'])
stats.columns = ['mean_loan_amount', 'max_loan_amount', 'min_loan_amount']
# Merge with the clients dataframe
clients.merge(stats, left_on = 'client_id', right_index=True, how = 'left').head(5)
index | client_id | joined | income | credit_score | mean_loan_amount | max_loan_amount | min_loan_amount |
---|---|---|---|---|---|---|---|
0 | 46109 | 2002-04-16 | 172677 | 527 | 8951.60 | 14049 | 559 |
1 | 49545 | 2007-11-14 | 104564 | 770 | 10289.30 | 14971 | 3851 |
2 | 41480 | 2013-03-11 | 122607 | 585 | 7894.85 | 14399 | 811 |
3 | 46180 | 2001-11-06 | 43851 | 562 | 7700.85 | 14081 | 1607 |
4 | 25707 | 2006-10-06 | 211422 | 621 | 7963.95 | 13913 | 1212 |
同样,我们可以对payments进行做变量衍生,然后合并到clients中。
很明显,这个手工特性工程的过程会因为有许多列和多个表而变得非常乏味,我当然不希望必须手工完成这个过程!幸运的是,featuretools可以自动执行整个过程,并且会创建比我们想象的更多的特性。
Featuretools
Featuretools要理解的第一部分是entity。entity可以是一个表:DataFrame。将多个entity合并到一个名为EntitySet的单一对象中,这是一个由许多单独的实体以及它们之间的关系组成的大型数据结构。
EntitySet
#创建一个空的实体集
es = ft.EntitySet(id="clients")
entity
一个entity就是一个表,如:dataframe。
每个entity必须有一个唯一标识列,称为索引。对于clients,索引就是client_id,因为每个client_id只在数据中出现一次。
在贷款dataframe中,client_id不是索引,因为每个id可能出现不止一次,它索引是loan_id。
在featuretools中创建entity时,我们必须标识dataframe的哪个列是索引。如果数据没有唯一的索引,我们可以通过传入make_index = True并为索引指定名称来创建索引。如果数据也有唯一的标识时间索引,我们可以将其作为time_index参数传入。
Featuretools将自动推断数据中列的变量类型(数字、类别、日期时间),但我们也可以传入特定的数据类型以覆盖此行为。举个例子,尽管payments中的 repaid 被表示为一个整数,但是我们可以告诉featuretools这是一个类别特性,因为它只能接受两个离散值。这是使用一个整数实现的,变量作为键,特征类型作为值。
在下面的代码中,我们创建了三个entity并将它们添加到EntitySet中。语法相对简单,只有几条注释:对于payments,我们需要建立一个索引,对于loans,我们指定repaid是一个类别特性,对于payments,我们指定missed是一个类别特性。
#clients指定索引为client_id,时间索引为joined
es = es.entity_from_dataframe(entity_id = 'clients', dataframe = clients,
index = 'client_id', time_index = 'joined')
#payments建议一个索引payment_id,指定missed是一个类别特性,时间索引为payment_date。
es = es.entity_from_dataframe(entity_id = 'payments',
dataframe = payments,
variable_types = {'missed': ft.variable_types.Categorical},
make_index = True,
index = 'payment_id',
time_index = 'payment_date')
#loans指定索引为loan_id,repaid是一个类别特性,时间索引为loan_start
es = es.entity_from_dataframe(entity_id = 'loans', dataframe = loans,
variable_types = {'repaid': ft.variable_types.Categorical},
index = 'loan_id',
time_index = 'loan_start')
Relationships
在定义了EntitySet中的entity之后,我们现在需要告诉featuretools,entity是如何关联的。
最直观的理解关系的方法是使用父类对子类的类比:父类对子类的关系是一对多关系,因为对于每个父类,可能有多个子类。因此,clients是loans的父类,因为clients中每个client_id只有一行,但每个client在loans可能有多个贷款。同样,loans是payments的父类,因为每个贷款都有多笔支付。
这些关系允许我们使用aggregation(聚合)对数据池进行分组,然后创建新的特性。例如,我们可以将与一个客户相关的所有贷款进行分组,并找到平均贷款金额。
要定义关系,我们需要指定父变量和子变量。这个变量将两个实体连接在一起。在我们的示例中,clients和loans由client_id连接在一起。
我们通过用featuretools语言编写关系,来指定父类和子类。在创建关系之后,我们将它添加到EntitySet中。
#前是父类,后为子类
es=es.add_relationship(ft.Relationship(es["clients"]["client_id"],es["loans"]["client_id"]))
r_payments = ft.Relationship(es['loans']['loan_id'],
es['payments']['loan_id'])
es = es.add_relationship(r_payments)
现在我们在EntitySet有了entity以及Relationships。现在,我们可以开始从所有表中创建新特性。
Feature Primitives
特征基元:对数据进行操作来创建特性。这些非常简单的计算,也可以叠加在一起创建出复杂的特性。
特征基元分为两类:
Aggregation(聚合):对每个父类的子类数据进行分组,然后计算统计数据(如平均值、最小值、最大值或标准差)的函数。
如:计算每个客户的最大贷款额。
Transformation(转换):应用于单个表中的一个或多个列的操作。例如,从日期中提取日期,或者在一个表中找出两列之间的差异。
我们可以查看Feature Primitives:
primitives = ft.list_primitives()
pd.options.display.max_colwidth = 100
primitives[primitives['type'] == 'aggregation'].head(10)
index | name | type | description |
---|---|---|---|
0 | mode | aggregation | Finds the most common element in a categorical feature. |
1 | any | aggregation | Test if any value is ‘True’. |
2 | last | aggregation | Returns the last value. |
3 | all | aggregation | Test if all values are ‘True’. |
4 | median | aggregation | Finds the median value of any feature with well-ordered values. |
5 | min | aggregation | Finds the minimum non-null value of a numeric feature. |
6 | avg_time_between | aggregation | Computes the average time between consecutive events. |
7 | max | aggregation | Finds the maximum non-null value of a numeric feature. |
8 | n_most_common | aggregation | Finds the N most common elements in a categorical feature. |
9 | percent_true | aggregation | Finds the percent of ‘True’ values in a boolean feature. |
如果Feature Primitives中没有合适的工具,我们也可以自己创建。
在下面的示例中,使用创建的EntitySet,clients作为目标entity,因为我们希望用指定的一些聚合和转换工具为每个客户创建新的特性。
# Create new features using specified primitives
features, feature_names = ft.dfs(entityset = es, target_entity = 'clients',
agg_primitives = ['mean', 'max', 'percent_true', 'last'],
trans_primitives = ['years', 'month', 'subtract', 'divide'])
Deep Feature Synthesis
当堆叠Primitives以获得深层特性时,使用功能工具的主要好处就出现了,特性的深度仅仅是创建特性所需的Primitives的数量。
因此,一个依赖于单个聚合的特性是一个深度为1的特性,一个叠加了两个Primitives的特性的深度为2,以此类推。
要阅读更多关于深度特征合成的信息,请阅读Max Kanter等人的原始论文。
在手动指定Primitives的dataframe中,也可以看到特性深度的概念。例如,MEAN(loans.loan_amount)特性的深度为1,因为它是通过应用单个Primitives来实现的。表示客户平均贷款金额。
# Show a feature with a depth of 1
pd.DataFrame(features['MEAN(loans.loan_amount)'].head(10))
深度为2的特性。例如,LAST(loans.)(MEAN(payments.payment_amount))具有深度= 2,因为它是通过叠加两个实现Primitives的,首先是aggregation,然后是Transformation。这个特性表示每个客户最近一次贷款的平均支付金额。
# Show a feature with a depth of 2
pd.DataFrame(features['LAST(loans.MEAN(payments.payment_amount))'].head(10))
我们可以通过叠加更多的Primitives来创建任意深度的特性。然而,当我使用特性工具时,我从来没有超过2的深度!
超过2层之后,要理解这些特性就变得非常复杂了。
Automated Deep Feature Synthesis
除了手动指定聚合和转换之外,还可以自动生成许多新特性。我们通过使用相同的ft.dfs函数调用来实现这一点,但是没有传递任何Primitives。我们只设置max_depth参数,featuretools将会自动地尝试多个Primitives组合到有序的深度。
当在大型数据集上运行时,这个过程可能需要相当长的时间,但是对于我们的示例数据,它将相对快速。
对于这个调用,我们只需要指定entityset、target_entity(clients)和max_depth。
# Perform deep feature synthesis without specifying primitives
features, feature_names = ft.dfs(entityset=es, target_entity='clients',
max_depth = 2)
3.总结
在这个笔记本中,我们看到了如何将featuretools应用到一个示例数据集。这是一种强大的方法,它允许我们克服人类对时间和想象力的限制,从多个数据表中创建许多新特性。特性工具基于深度特性合成的思想,将多个简单的Primitives(聚合和转换)叠加起来创建新的特性。featuretools允许我们将跨多个表的信息合并到一个单独的dataframe中,然后我们可以使用这个名称进行机器学习模型训练。创建所有这些特性后的下一步是确定哪些特性是重要的