分享

深度特征合成:自动生成机器学习中的特征

 大邓的Python 2021-02-23

英文原文标题:Automated Feature Engineering in Python
How to automatically create machine learning features

原文链接: https:///automated-feature-engineering-in-python-99baf11cc219

作者:William Koehrsen
译者:大邓

特征工程,也叫做特征生成,是对数据集中的数据进行处理构建多个特征,并让这些特征数据来训练机器学习模型。在机器学习流程中,特征工程模型选择更加的重要,而且特征工程要根据机器学习任务,选择并构建相关的特征。

通常来说,特征工程要依赖数据科学家拥有一定的领域知识、直觉来对数据操作,因此是一个繁杂冗长的工作。自动特征工程(automated feature engineering)旨在通过自动从数据集生成大量可用的候选特征来帮助数据科学家减少低端琐碎重复的劳动。

在本文中,我们会通过一个自动特征工程的例子来学习featuretools库

安装

打开命令行,输入下列命令回车。

pip3 install featuretools

或者如果你使用anaconda作为开发环境,可以使用下列命令

conda install -c featuretools featuretools 
数据集

我伪造了数据来方便大家学习,该数据集包含三个表:

  • clients: 关于用户信用的表

  • loans: 关于每个用户历史贷款情况

  • payments: 关于每笔贷款的偿还情况

在数据分析过程中,经常数据会分布在不同的表中(或者不同的文件,不同的数据库中)。例如,互联网公司中消费者数据可能分布在个人信息、浏览数据、消费数据,这些数据只有通过用户id(如果学过计算机的朋友们,可能知道这相当于数据库中的外键)才能将彼此串联整合起来。

机器学习也属于数据分析的一种,所以也需要将多个表的数据进行特征生成,并用特征数据去训练模型。

读取数据

首先我们要读取数据

import pandas as pd
import numpy as np

#忽略pandas的错误提示
import warnings
warnings.filterwarnings('ignore')

#parse_dates参数可将日期数据以日期格式读取,如果没有这个参数,那么读取的数据仅仅是字符串或者数字
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'])

读取client表中的前5行

clients.head()

#读取loans表中的前10行
loans.sample(10)

#读取payments表中的前10行
payments.sample(10)

手动进行特征工程

这里只简单演示下过去传统的特征工程,需要我们数据科学家手动进行操作。例如,在这里我们根据clients表中joined这列构建join_month特征变量。把日期类型转化为整数类型数据,方便进行比较和计算。

根据clients表中income这列构建log_income特征变量,避免数据量纲差异过大。

这两个特征(变量)构建在featuretools库中相当于转换(transformation)(因为这种操作是在单一表中对列进行的操作)

#在clients表中创建一个新特征变量(新列)
clients['join_month'] = clients['joined'].dt.month

#在clients表中创建log_income列
clients['log_income'] = np.log(clients['income'])

clients.head()

为了整合其他表的信息,我们使用pandas中的df.groupby方法,这里需要有pandas基础知识,推荐大家看我之前分享的pandas文章-《使用Pandas更好的做数据科学》。例如我们计算clients表中每个用户之前贷款loan平均值、最大值、最小值。因为我们使用了多个表来整个信息,在featuretools库中,这种操作被称为聚合(aggregation)

# 对loans表中的依据用户(client_id)进行聚合,并计算出每个用户贷款的均值、最大值、最小值
stats = loans.groupby('client_id').loan_amount.agg(['mean''max''min'])
#等同于stats = loans.groupby('client_id')['loan_amount'].agg(['mean', 'max', 'min'])

#给生成的stats三个列进行命名
stats.columns = ['mean_loan_amount''max_loan_amount''min_loan_amount']
stats.head()

从loans表中得到的stats数据整合进clients表中。

left_on='client_id' : 使用左侧dataframeclient_id作为左链接键。 clients在stats的左边【clients.merge(stats】,left_on表示根据clients表中的client_id进行整合

right_index=True:  使用右则dataframe中的行索引做为连接键

how = 'left':左外连接。符合连接条件和查询条件的数据行并返回左表中不符合连接条件但符合查询条件的数据行。

这块比较难理解,直接记住这么用就行。或者以后直接用clients.merge(stats)即可。

clients.merge(stats, left_on = 'client_id', right_index=True, how = 'left').head(10)

更进一步,在clients表中包含有关payments的信息。 为此,我们必须通过loan_idpayments进行分组,将其与loans表进行合并。然后我们再根据loans表中的client_id信息,将其合并到clients表中。 这让新的clients表中含有贷款信息及用户还贷信息。

显然,这种手动特征工程的过程可能会因很多个列和很多个表而变得相当繁琐,我们当然不希望手动完成这个过程! 幸运的是,featuretools库可以自动执行整个过程,并创建比我们想象的更多的功能。 虽然我喜欢pandas库,但我不愿意忍受这么多的手动数据操作!

Featuretools库

现在我们开始使用featuretools库来摆脱手动特征工程,在featuretools中被称为深度特征合成(Deep Feature Synthesis),但这里并没有用到深度学习。深度特征合成采用基本模块来操作和生成特征,具体的工作原理我们不需要知道,会用就行,特征的深度(depth of feature)一般不超过2。

我扔出了几个概念,不用担心,稍后我们会做详细的代码演示和说明。

我们首先要理解featuretools中的 三种重要的对象:Entitiy 、EntitiySet、Relationship

Entitiy

一个Entity相当于一个表,在pandas中就是一个dataframe。你可以简单的理解为一个类似于excell文件样式的数据。每一个entity必须拥有一个独一无二的列(外键)作为索引。例如,在clients表中的client_id列是独一无二的列,相当于人的身份证号码,具有唯一标示作用。而在loans表中,client_id不是唯一的,因为在loans表中,client_id可以出现多次,但loan_id可以作为该表的唯一标示。

当我们使用featuretools创建了entity后,我们必须指明dataframe中的哪一列作为索引index。如果dataframe中没有唯一标示列,我们可以使用make_index=True来让程序自动生成唯一标示列。如果dataframe中的时间列具有唯一性(即任意两个用户数据间的时间是不同的),那么我们可以将时间列传给time_index参数。

featuretools会自动推断dataframe中各个变量的类型(数字型、类别型、日期型),但我们也可以对变量的类型进行自主设定。例如loans表中的rapaid列数据是0和1的整数型,但实际上只有0和1两种,这应该是类别型变量。

在下面的代码中,我们创建了三个entity,并将其加入到entitySet中。

import featuretools as ft

#创建entityset。注意这里的id哦
es = ft.EntitySet(id = 'new_clients')

#从client表中创建entity,并加到entityset中。
#time_index: 将joined列作为时间列索引
es = es.entity_from_dataframe(entity_id = 'clients', dataframe = clients, 
                              index = 'client_id', time_index = 'joined')

#从loans表中创建entity,并加到entityset中。
#将loans中的repaid列(变量)定义为分类变量
#time_index: 将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')

# Create an entity from the payments dataframe
# make_index = True  payments表中没有唯一标识列,所以这里需要自动生成标识列。
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')

es

运行结果

    Entityset: new_clients
      Entities:
        clients [Rows: 25, Columns: 6]
        loans [Rows: 443, Columns: 8]
        payments [Rows: 3456, Columns: 5]
      Relationships:
        No relationships

EntitySet以字典形式存储信息,我们使用字典方法就可以获取每个表的具体信息。

#查看loans表的信息,传入的实际上是entity_id参数,写法如 es[entity_id]
es['loans']

运行结果

    Entity: loans
      Variables:
        client_id (dtype: numeric)
        loan_type (dtype: categorical)
        loan_amount (dtype: numeric)
        loan_start (dtype: datetime_time_index)
        loan_end (dtype: datetime)
        rate (dtype: numeric)
        repaid (dtype: categorical)
        loan_id (dtype: index)
      Shape:
        (Rows: 443, Columns: 8)

Relationships

当我们在entityset中定义了entity时,我们需要告诉featuretools各个entity之间的关系。在数据库中,这种关系常被称作父表-子表。比如clientsloans的父表, loanspayments的父表。

例如clientsloans父子表可以通过client_id来衔接起来,而loanspayments可以通过loan_id衔接起来。多个表之间的这种关系能让我们将其整合起来创建新数据。

下面我们分别对两对父子表进行关系的建立

#通过client_id这个索引来建立clients和loans之间关系
r_client_previous = ft.Relationship(es['clients']['client_id'],
                                    es['loans']['client_id'])

#将这种关系加入到entityset中,更新entityset对象
es = es.add_relationship(r_client_previous)


#通过loan_id这个索引来建立loans和payments之间关系
r_payments = ft.Relationship(es['loans']['loan_id'],
                                      es['payments']['loan_id'])

# 将这种关系加入到entityset中,更新entityset对象
es = es.add_relationship(r_payments)

es

运行结果

    Entitysetnew_clients
      Entities:
        clients [Rows: 25, Columns: 6]
        loans [Rows: 443, Columns: 8]
        payments [Rows: 3456, Columns: 5]
      Relationships:
        loans.client_id -clients.client_id
        payments.loan_id -loans.loan_id

现在的entityset已经拥有了三个表数据,并将表之间的关系进行了构建。现在我们可以开始使用entityset这种数据,使用深度特征合成来构建新特征。在此之前,我们先要了解两种概念 :聚合(aggregation)和转化(transformation)。

元特征(Feature Primitives)

元特征是最基本的特征粒度,据此我们还可以构建基于元特征的新的特征。元特征相当于金融市场中的基础金融产品(如房产债券、股票),基于基础金融产品可以打包衍生出更多的金融衍生品,如期权、期货理财产品。通俗点解释,元特征是一切特征之母,一切特征的源泉。而元特征产生有两种来源:

  • 聚合Aggregation: 产生过程涉及多个表的整合,如父子表,产生出的变量。比如我们结合clients和loans两个表,计算用户的贷款的最大值、最小值、均值。

  • 转换Transformation: 仅仅在一个表中进行操作产生的特征变量,叫做转换。

下面我们代码,看看featuretools中的两种元特征来源

#元特征
primitives = ft.list_primitives()

#dataframe中列宽只显示100长度
pd.options.display.max_colwidth = 100

#统计featuretools中元特征方法信息.一共有62种方法
primitives.describe()

只显示type为aggregation的元特征方法

#只显示type为aggregation的元特征方法,前10种
primitives[primitives['type'] == 'aggregation'].head(10)

只显示type为transform的元特征方法

#只显示type为transform的元特征方法
primitives[primitives['type'] == 'transform'].head(10)

如果featuretools中没有我们需要的特征生成方法,我们也可以自己定义。具体可查阅featuretools库的官方文档官方文档

为了理解元特征生成,我们先在我们的数据上试试。这里要用到ft.dfs,也就是featuretools的深度特征合成。在这个函数中,我们指定了我们要使用的entiysettarget_entity(新生成的特征最终要汇总到某个表)agg_primitives(使用哪些 聚合方法 生成元特征)trans_primitives(使用哪些 转换方法 生成元特征)

接下来的例子,我们使用已经创建好的entityset,采用多种特征生成方法生成特征,并指定将生产的元特征汇数据汇总到clients表中。

# 使用指定的特征生成方法,来生成特征
features, feature_names = ft.dfs(entityset = es, target_entity = 'clients'
                                 agg_primitives = ['mean''max''percent_true''last'],
                                 trans_primitives = ['years''month''subtract''divide'])

#查看生成的元特征数目
len(feature_names)

运行结果显示

797

这里我们只显示前20个特征

feature_names[:20]

前20个特征有

    [<Feature: income>,
     <Feature: credit_score>,
     <Feature: join_month>,
     <Feature: log_income>,
     <Feature: MEAN(loans.loan_amount)>,
     <Feature: MEAN(loans.rate)>,
     <Feature: MAX(loans.loan_amount)>,
     <Feature: MAX(loans.rate)>,
     <Feature: LAST(loans.loan_type)>,
     <Feature: LAST(loans.loan_amount)>,
     <Feature: LAST(loans.rate)>,
     <Feature: LAST(loans.repaid)>,
     <Feature: MEAN(payments.payment_amount)>,
     <Feature: MAX(payments.payment_amount)>,
     <Feature: LAST(payments.payment_amount)>,
     <Feature: LAST(payments.missed)>,
     <Feature: MONTH(joined)>,
     <Feature: join_month - income>,
     <Feature: credit_score - income>,
     <Feature: credit_score - join_month>]

展示MEAN(loans.loan_amount)特征的前5条数据

pd.DataFrame(features['MEAN(loans.loan_amount)'].head())

#展示MONTH(joined)特征的前5条数据
pd.DataFrame(features['MONTH(joined)'].head())

#显示生成的所有特征数据中前5条。
features.head()

我们看到featuretools生成了庞大的特征数(797),很多特征名中是包含着操作数据列及操作方法。这引出了新的问题,深度特征合成中计算深度的问题。

深度特征合成( Deep Feature Synthesis)

虽然元特征是有用的,但是深度特征合成的主要优点是可以更深入的去挖掘数据中的特征。 特征的深度是计算特征所采用方法的复合数量。一般深度不超过2。

例如MEAN(loans.loan_amount)特征的深度是1,因为只对loan_amount进行了一种操作-计算平均数

通过浏览所有的特征,我们发现也有深度为2的特征。如,基于LAST(loans.(MEAN(payments.payment_amount)))这个特征,又进行了LAST 方法操作。

# 特征深度2
pd.DataFrame(features['LAST(loans.MEAN(payments.payment_amount))'].head(10))

自动深度特征合成( Automated Deep Feature Synthesis)

一般特征深度不超过2,因为超过2之后,生成的特征我们无法理解更很难去解读该特征的现实意义。所以我们还要通过  max_depth=2,限制特征深度。

# Perform deep feature synthesis without specifying primitives
features, feature_names = ft.dfs(entityset=es, target_entity='clients'
                                 max_depth = 2)


len(feature_names)

通过限制深度,最后一共生成了94个特征。相比于原来的797,大大的减少了!

    94

显示前5行,后90列的数据

features.iloc[:, 4:].head()

深度特征合成已经从现有数据中创建了90个新特征! 虽然我们可以手动创建所有这些,但我很高兴不必手动编写所有代码。 featuretools的主要好处是它创建的特征没有任何主观的人为偏见。 即使是具有相当领域知识的人,在生成新特征时常常受到想象力、精力和个人经验偏好的限制,从而带有很强的主观性。 自动化特征工程不受这些因素的限制(相反,它受到计算时间的限制),并为机器学习创建了良好的起点。 这个过程可能不会完全消除人类对特征工程的贡献,因为人类仍然可以使用领域知识和机器学习专业知识来选择最重要的特征

不足之处

虽然自动特征生成解决了人工特征繁琐的工作,但是却给机器学习引入了大量的特征。虽然很难说出哪些特征对于给定的机器学习任务很重要,但很可能并非所有的特征都给模型训练提高了性能。 事实上,具有太多特征是机器学习中的一个重要问题,训练速度变得更慢,使训练变得更加困难。这个问题常被称作维度诅咒或者维度爆炸,这个问题需要用PCA压缩数据中的特征数。之前写过一篇文章-《应用PCA降维加速模型训练》

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多