查看原文
其他

Python数据科学:全栈技术详解5-特征工程(上)

作者:Ben,多本数据科学畅销书作家,先后在亚信、德勤、百度等企业从事电信、金融行业数据挖掘工作。

配套学习教程:数据科学实战:Python篇 https://edu.hellobi.com/course/270


  前文传送门:

  Python数据科学:全栈技术详解1-个人贷款违约预测模型

  Python数据科学:全栈技术详解2-客户分群

  Python数据科学:全栈技术详解3-长尾理论

  Python数据科学:全栈技术详解4-推荐算法


本章从商业数据分析和挖掘的角度详细地介绍了特征工程及其使用的响应方法:数据预处理、特征构造、特征抽取以及特征选择。系统性地说明了用于构建分析用的结构化数据的过程。

5.1 特征工程概述

在商业数据的分析挖掘当中,最常用的数据是结构化数据,其呈现为二维表的结构,数据可以用装载到二维数组当中,其中的每个数可以使用行与列进行索引。结构化数据中的每一行称为记录,也可称为样本或实例(视不同的学科而定),每一列则被称为字段,也可称为变量或特征(同样视学科领域而定),其中“特征(Feature)”这一叫法在机器学习及模式识别当中被广泛使用,在应用当中只要不引起歧义,可以不区分它们的叫法。

特征(feature)是一个被观察到的现象的可测量的属性,结构化数据中,特征通常以列的形式出现,用于描述记录在某些方面的属性;非结构化数据同样如此,例如对于一个文档,短语或单词的计数就可以是其一个特征。

特征是模型的输入,而不同的模型对输入有不同的要求。正因如此,原始数据往往需要通过一定的处理和转换才能在模型中使用,而为了提升模型的表现,又需要对所有可用的特征进行一定的筛选,既保证重要的特征进入模型,又要保证不会选择过多的特征。所有这些在建模前对数据进行处理、转换、筛选的工作被称为特征工程(Feature Engineering),其本质上是对原始数据的再加工,目的是产生进入模型的特征。“特征工程”这一名称在机器学习与模式识别领域被大量使用,在数据挖掘标准流程CRISP-DM当中,“数据准备”(Data Preparation)具有与特征工程相同的作用和类似的方法,因此在应用当中,无需严格区分,本章当中使用“特征工程”这一名称。

特征工程分为数据预处理(Data Preprocessing)、特征构造(Feature Construction)、特征抽取(Feature Extraction)和特征选择(Feature Selection)等几个步骤,每个步骤间没有明显的顺序之分,往往需要根据需求反复执行,甚至也没有严格区分的概念边界,例如特征构造可能会与数据预处理使用同样的数据变换技术等等。

特征工程在整个数据分析的流程当中占用最多的工作时间,而且具有重要的地位,可以认为,数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。

此外,为了演示特征工程的案例,本章采用pandas与scikit-learn进行数据的处理。因为商业数据通常是结构化的,pandas包可以很好地对这类数据进行读取、统计、汇总、变换,也可以进行一些简单的可视化,因此数据的预处理可以使用pandas中的函数和方法。同时scikit-learn包作为流行的机器学习框架,对机器学习的多种方法都有支持,包括了对数据预处理、特征选择等建模前步骤的方法支持。因此部分案例既使用了pandas,也使用了scikit-learn进行实现,目的是为了使读者能更深入地了解它们的使用方法和异同点。

下面通过对一份手机用户通话账单进行处理来演示特征工程的各步骤,原始数据包含了1个属性,包括了ID、是否流失、入网时间、是否VIP、套餐编号、月消费、月通话时长、月流量消耗、用户状态及手机串号,数据集如下所示:

其中"display.max_columns"设置了在notebook中显示的最大列数,当显示的数据超过10列时中间的部分数据会使用省略号表示,这仅仅是为了展示方便而已,如果该值被设置为"none",则会显示所有列,超出页面宽度时会出现相应滚动条。

5.2 数据预处理(Data Preprocessing)

数据预处理是为了满足数据后续处理与建模的基本步骤要求,包括对噪声数据的清洗,以及为满足模型需求的基本数据变换。其中噪声包括了错误值、异常值等类型;基本数据变换除了常用的函数变换,还包括二值化、哑编码、标准化、规范化等内容。

5.2.1错误值处理(Wrong Value Processing)

错误值是指数据集中出现的数值、格式、类型等错误,导致错误的原因包括:

(1)输入错误:录入信息时缺失、错误输入、类型或格式不符等。

(2)设备故障:例如采集设备故障、数据分析工具不够健壮等。

(3)其他:因为对数据理解不够导致数据处理失误,比如造成数据重复、错误截断数据等。

错误值产生的原因多种多样,有些错误值是显而易见的,有些则不那么明显,需要结合对业务的理解以及对多种数据源之间互相对照来发现。要修正错误,需要明确错误产生的原因,这就需要对业务与数据有深入了解,常用的处理错误值的方法包括以下几种。

(1)修正

补充正确信息:例如重新导入原始的正确数据。

对照其他信息源:例如使用CRM系统中正确的用户年龄替换手工填报的错误年龄。

视为空值:将数据清空,等待下一步处理。

(2)删除

删除记录。

删除特征。

如果通过删除来处理错误值,那么要保证该处理不会对后续工作造成重要影响,删除的记录过多会影响数据的分布,删除的特征也不能是那些重要的字段。

以下是一个通过业务规则发现错误值的例子。

在用户消费的流量“traffic”字段中,有一些是超过5000MB的,而根据业务规则,电信运营商限制了单用户每月消费的流量不得超过5000MB(有些套餐设置不得超过15GB),设置这个规则是为了避免用户在不知情的情况下消耗过多流量而引起投诉。这项规则一般可以通过联系服务商而关闭,而一些用于内部测试的号码则没有这项规则的限制。如果筛选出来的数据经确认是错误的,或者是测试号所为,则可以删除它们,因为分析的目标不应当包含测试号这类特殊用户。因此,删除(或筛选)处理方式如下。

新的数据集“data”将不包含流量消费大于5000MB的用户。

对于分类变量,可以采用一定的技术手段去识别错误值,因为错误值通常极少出现,因此可以通过对比分类变量各分类水平之间的数量差异去识别。比如对用户状态“state”进行汇总,代码如下。

通过汇总,发现用户状态为“2”和“4”的记录非常少,则这两类数据很有可能是错误的。为了更直观地展现数量差异,可以使用饼图来描述数据:

通过图形可以非常容易发现“2”与“4”这两种状态的用户很少,如果对照业务规则,发现这两种状态是不应当出现的,那么可以删除这些记录。另外一种可选的方式是先把这两种状态清空,后期再用合适的方法填补,代码如下。

 

当然了,根据业务规则,实际上这两种状态是确实存在的,而且是较少出现的。实际建模中,我们往往难以区分分类变量中的某些很少的编码到底是这个对应的业务发生地少,还是填报错误,因此对都作为错误进行处理。去除这类出现较少的分类有两方面考虑:1)属于这一类的客户太少,单独分类意义不大;2)数量极少的组在抽样时会出现抽样分布不均匀现象,造成模型无法使用。上例中分组为“2”的客户只有9个,有可能这9个样本在建模时都被随机抽样的到测试数据集,而在训练数据集中没有出现。我们知道分类变量在构建模型时要使用哑编码,因此训练的模型中没有“2”对应的编码,而在打分时,测试数据集有这个编码,因此模型出现报错。

5.2.2异常值处理(Outlier Processing)

异常值是指那些远远偏离多数样本的数据点。商业数据挖掘中,一般多数样本聚集在中心附近,而少数记录偏离它们,形态上接近正态分布或对数正态分布(对连续变量而言),我们仅讨论这种情况。

根据数据偏离程度的不同,可将异常值分为极端值与离群值(其中极端值偏离得更远一些)。从单变量的角度看,识别异常值可以采用平均值法、四分位数法等方法;从多变量角度看,可以使用聚类来识别异常值(某些记录可以在单个变量分布上并无异常),聚类方法会在其他章节进行介绍。

异常值有时意味着错误,有时不是。无论是否代表错误,异常值都应当被处理。通常可以根据对业务的理解来设置异常值的边界,也可以使用一定的技术手段辅助识别异常值。常用的识别方法包括以下两种。

(1)平均值法:对于正态分布来说,三倍标准差之外的样本仅占所有样本的1%,因此,设置平均值±3×标准差之外的数据为离群值,极端值被定义为平均值±5×标准差之外数据。

(2)四分位数法:设置1.5倍四分位距以外的数据为异常值,即IQR = Q3–Q1,IQR为四分位距,Q3和Q1分别为第三和第一四分位点,定义正常数据在Q1–1.5 ´ IQR ~ Q3 + 1.5 ´ IQR 之间。

例如:绘制月消费额箱线图。

其中上面单独的横线位置代表离开第三个四分位点1.5倍四分位距的数据,超出以外的数据就是异常值。

对异常值的处理方法通常包括以下几种。

(1)视为空值:待后期对空值进行处理,如果后期的处理策略是对空值进行填补,则视填补方式的不同会不同程度地改变原有数据的分布。

(2)盖帽法:异常值被重新设定为数据的边界,这种方法同样会改变原有数据的分布,但由于异常值往往是少数,因此可以采用。

(3)变量转换:通过一定的变换改变原有数据的分布,使得异常值不再“异常”,常用的转换是对数变换,这对那些严重右偏的数据非常有用,变换后的数据能够更接近正态分布,如图15-3所示。

 

图17-3

(4)考虑分段建模或离散化:分段建模是将那些偏离中心的样本单独进行分段建模,其隐含的商业逻辑为离群点的内在规律与多数不同,例如收入很高的人群,其行为与普通人群必然不同,其行为所反映的内在规律必然也有较大的差异,应当单独进行分析;离散化是通过设置分割点将连续型变量转换为离散型,例如将年龄这一字段离散化,可以通过设置分割点划分为儿童、少年、青年、中年、老年,这样年龄很大的人会被作为单独一类进行分析。值得一提的是,连续型变量往往是模型不稳定的原因,离散化之后通常能提高模型的泛化能力。

使用盖帽法进行异常值处理的示例如下。

通过将离开中心点三倍标准差之外的数据重新赋值,用户月消费额被限制在了250元以内,尽管新数据使用四分位法或平均值法分析后,又可能出现了“异常值”,但一般不会再次处理。

对于月消费额这样的数据绘制直方图,可以发现其明显右偏,代码如下。

这类数据也可以通过对数转换使其更接近于正态分布,同时消除异常值,由于对数函数的定义域为(0,+∞),而消费额这样的数据往往包括0,因此可以对数据进行加1操作,再进行对数转换,这样的转换可以保证原来为0的数据转换后依然为0,新数据都是非负的,示例代码如下。

上述代码中使用了np.log函数,传入的参数正是原始数据+1后的结果。numpy中定义了一个函数log1p可以产生同样的效果。

除了使用对数函数直接转换数据之外,scikit-learn提供了preprocessing这样的预处理模块,可以利用其中定义的函数转换器达到同样的效果,示例如下:

可以看到使用scikit-learn中的函数转换可以一次性对多个字段进行处理。

对数转换后发现存在大量的0值,这些0消费额的账单比较特殊,因此完全可以单独提取出来进行分析。同时,去掉了0值的数据更加接近正态分布,因此对于后续的分析工作更加有利。

5.2.3缺失值填补(Missing Data Imputation)

缺失值在页面上显示为NaN,意味着Not a Number,等价于numpy当中的空值NaN。缺失值的出现是非常普遍的,在pandas当中,对DataFrame进行count计数,不会统计其中的缺失值,因此可以使用该方法来判别哪些字段上有缺失值以及缺失的数量(除非所有字段都有缺失值,这种情况不多见,一般ID识别号是不会有缺失的),如下所示:

为了更加准确的判断缺失值的数量,可以使用isnull函数,如果数值为空返回True,否则为False,再进行求和(True被作为1,False被作为0),就可以统计每个字段缺失值的数量。同样可以使用notnull函数进行类似分析,其返回结果与isnull相反。示例如下:

缺失值的数量会影响缺失值处理的策略,同时,缺失值的处理需要根据字段代表的业务内涵来进行,常用的方法包括:

  • 通过对比其他数据源填补空值;

  • 对缺失不多的连续型字段填补中位数或均值;

  • 对缺失较多的连续型字段,生成一个新字段用于标示哪些记录是被填补的(比如用1表示缺失记录,用0表示非空记录),原字段不再使用;

  • 对缺失不多(比如少于20%)的离散型字段填补众数或填补为“未知”这样的新分类;

  • 对缺失较多(比如多于20%并且少于50%)的离散型字段填补为“未知”这样的新分类,同时生成一个新的字段用于标示哪些记录是被填补的(比如用1表示填补的记录,用0表示之前非空的记录);

  • 对缺失很多(比如多于50%并且少于80%)的离散型变量,直接使用新字段标示哪些记录存在缺失,原字段不再使用;

  • 对缺失非常多(比如多于80%)的字段(包括离散型与连续型)直接删除;

  • 对缺失值过多的记录直接删除;

  • 通过建模从其他字段“预测”目标字段的缺失值应当为多少。

由于业务的多样性,上述方法只是一个参考,以下是一个填补缺失值的例子(见下图):

 

缺失值的处理可以非常灵活,比如对“营销次数”的填补可以使用众数填补,也可以填为0(如果缺失是因为在营销数据库中没有该记录造成的则填0)。此外,营销活动通常是对满足一定条件的用户开展的,因此也可以通过建模“预测”该用户“被营销”的次数。

以下是对之前的data数据集按照缺失值数量进行记录删除的一个例子。

上例使用了dropna方法,对"state"和"IMEI"这两个字段,如果缺失值超过1个(即参数thresh),则删除该条记录,即删除这两个字段都缺失的记录,因此新数据集的记录数减少了。当然了,因为数据集共有10个字段,仅仅两个字段缺失就删除有点过于冒失,因此,这个例子仅为演示代码所用,无需实际保留该结果。

除了按条件删除记录,pandas也提供了对缺失值进行填补的方法fillna,一个使用该方法填补缺失值的例子如下:

在上例中,使用0来填补手机串码"IMEI"的缺失值,用账户状态"state"的众数来填补自身的缺失值,其中mode方法用于求众数,由于众数可以有多个,因此该方法会返回一个数组,包含所有众数,使用索引取得第一个众数用于填补。fillna中的参数inplace用于指定是否直接替换原缺失值,如果设置为True,会对数据就地进行修改,否则会生成一个填补后的新dataframe对象。

在scikit-learn的preprocessing模块中,也提供了用于缺失值填补的类Imputer,其使用方法如下所示:

这种方法可以指定用于表示缺失的值,strategy参数则指定了填补策略,比如示例中使用的众数,其它策略包括均值、中位数等,具体可参考帮助或文档。

5.2.4二值化(Binarization)

二值化,顾名思义就是将一个字段转换为仅有两个可能值。二值化通过设定一个阈值,原字段大于阈值的被设定为1,否则为0,即转换函数为指示函数:


对于示例数据集当中的"IMEI"字段是手机串码(原本为15位),一部移动设备对应一个,是全球唯一的,这样的字段与ID字段一样无法进入模型。通过观察,发现有大量的记录IMEI为空值(现已填补为0),因此可以将有IMEI的设置为1,没有的设置为0。在scikit-learn的preprocessing中,使用Binarizer可以将字段二值化,示例如下:



除了使用Binarizer,在可以使用pandas.cut对数据进行离散化,这种方式通过传入一个用于表示阈值的"bins"参数,将数据进行分割,如下所示:

需要注意的是传入的阈值列表是包含了上下界的,切割的区间为左开右闭,即切割后的两个区间为(-∞,0]和(0,+∞)。如果要切割成多个区域,只需把所有阈值以列表形式传入"bins"参数即可。

5.2.5离散化(Discretization)

连续型特征经常成为模型不稳定的来源,此外,连续型特征可能与目标变量呈现复杂的相关性,而将连续型特征根据一定方法转化为离散特征后可能带来模型效果的提升。常用的离散化方法包括等宽离散、等分离散、人工离散等,二值化(Binarization)可以看做是离散化的特殊情况。

离散化的基本原理是将连续数据划分至不同的区间(也称为“分箱”),使用区间的编码来代替原始数据即可。例如:等宽离散的区间是按照数据的极差进行平均划分的,其每个区间的宽度相等;等分离散化是按照数据的分位数划分的,其每个区间内的样本量相等(或接近相等);人工离散化则是根据业务理解进行区间划分的。一个针对年龄进行不同离散化的例子如下图所示:


为了演示离散化,我们将示例数据集中的“入网时间”转化为“在网时长”:

至此,我们“在网时长”属于连续型特征,我们可以对其进行离散化。

1. 等宽离散

使用Pandas进行等宽离散的例子如下:

离散化后,可以看到(1.892, 36.913] 区间内的样本量最多,(141.453, 176.3]区间内样本量最少,在不同的区间内用户离网率(mean字段)呈现有规律的变化。这里可以注意一下,pandas等宽离散的每个区间都是左开右闭的。

pandas提供的cut方法可以通过传输label参数,将每个离散化后的区间用指定的编码进行表示,并且新生成的编码是有序的。例如使用0到4的整数来表示等宽离散的每个区间如下:

可见,原始的在网时长字段可以被用0到4来替代,函数同时返回了划分区间的每个阈值。

2. 等分离散

pandas提供的qcut方法可用于进行等分离散,但有bug,所以这里重新定义一个qcut函数:

该函数使用分位数确定某一列特征的划分区间,再使用pandas.cut将数据按照划分区间进行离散,这里的pandas.cut函数因为指定了区间,将不再按照默认的等宽方式离散了。

使用自定义的函数可以方便地对数据进行等分离散:

可以看到,离散化后每个区间内的样本量基本相等,各区间内的用户离网率呈现出一定的规律。也正是因为每个区间的样本量基本相等,这个离网率显得更有可信度。如果采用其他离散化方法,可能造成某些区间的样本量过少,该区间的离网率自然很难有说服力。本例中我们将离网率按等分离散的区间绘制柱状图如下:

从图中,离散化后的离网率规律一目了然。

3. 人工离散

仅需要人工指定区间划分的阈值就可以很容易地使用pandas.cut进行人工离散化,如下例:

本例中,为了保证能将所有数据都包含进来,分别使用正负无穷大作为最大与最小值。

5.2.6哑编码(Dummy coding)

对于无序的分类变量,许多模型不支持其运算(在R中许多模型,例如回归模型,会自动将因子变量转换为哑变量,省去了很多麻烦),可以先生成哑变量,再进行深入分析。哑变量被认为是量化了的分类变量,因此应用非常广泛。

哑变量又称虚拟变量或虚设变量,一般使用0和1来表示分类变量的值是否处于某一分类水平,例如账户ID1~4的办理渠道如图18-1左图所示,进行哑编码后用0和1来表示各记录是的具体取值情况,如图18-1右图所示:

 

图18-1

一般有m个分类水平的变量经过哑编码之后,会生成m个哑变量,各个哑变量之间两两互斥,应用中一般仅保留其中m-1个进入模型,留下1个作为对照组,对照组可以通过其他m-1个哑变量完全还原出来。在使用哑变量建模时,一般需要保证由这m-1个哑变量都能进入模型,或者都不进入模型。

在pandas中可以使用get_dummies函数来对分类变量进行哑编码,新生成的哑变量与原变量的分类水平数相等,对照组可自行选择,get_dummies使用方法如下所示:

另外,scikit-learn的preprocessing当中,可以使用OneHotEncoder对数据进行哑编码,同样会生成与原变量水平数相等的哑变量,代码如下:

5.2.8标准化(Standardization)

在商业分析中,不同的变量存在单位不同的问题,比如流量使用MB作为单位,而金额使用元作为单位,因此流量、金额无法直接进行比较;同时,单一指标采用不同单位也会无法直接比较,比如收入用元作单位和用万元作单位会在数值上相差很大。这样的差异性会强烈影响模型的结果,比如下例中的聚类方案(见图18-2)。

 

图18-2

同样的样本点,如果流量采用MB为单位,则A\B为一个簇,C\D为一个簇;如果使用GB为单位,则A\C一个簇,B\D一个簇。可见因为单位不同对模型的结果会产生颠覆性的影响。

为了消除字段间因为单位差异而导致模型不稳定的情况,需要将变量的单位消除,使得它们都是在一个“标准”的尺度上进行比较分析。因此需要采用标准化的技术,常用的方法包括极值标准化与学生标准化:

极差标准化

 

学生标准化


极值标准化将数据映射到[0,1]之间,受极端值影响较大;学生标准化更多应用在对称分布的字段上(因为对称分布的均值有代表性),受极端值的影响较小,是多数模型的默认标准化方法。另外,这两种标准化方法都不会改变原字段分布图形的形态,仅仅是对数据进行了线性缩放而已。

使用pandas提供的函数可以很方便地对字段进行标准化,使用scikit-learn也可以达到同样效果,而且在应用中可能更加方便,比如我们对已进行了对数转换的收入、通话时长、流量消费(接近正态分布)进行学生标准化的例子如下:

使用preprocessing.StandardScaler可以对多个字段一次性进行学生标准化,而且fit之后的转换器std_scaler可以重复使用。比如,使用训练集数据fit的转换器可以对测试集进行相同的转换(使用训练集的均值和标准差进行学生标准化)。

将标准化后的数据添加到数据集当中:


极值标准化的方法也类似:

在很多软件中,会在建模前默认对数据进行标准化,但并不是所有软件都如此,比如scikit-learn这样的开源工具。在scikit-learn中,部分模型会有用于标准化的参数选项(比如各种降维),另一些则需要手动进行标准化(例如逻辑回归、SVM等)。此外,并不是所有模型都需要预先进行数据标准化(比如决策树),而即便是同样的模型,运用在不同业务问题上也会有不同的标准化要求,这就要求我们对业务与模型都要深入了解才行。

5.2.9规范化(Normalization)

标准化方法应用于列,规范化或称归一化一般应用于行。规范化是为了数据样本向量在做点乘运算或其他核函数计算相似性时,拥有统一的标准。如果将表中的每一行作为向量来看,规范化就是将其缩放成单位向量。

对于有n条记录m个字段的数据集,使用x代表行记录形成的m维向量,使用L2范数(这里L2范数就是欧几里德距离)进行规范化的公式为:

scikit-learn当中可以使用preprocessing.Normalizer对每一行进行规范化,仅仅是为了演示代码,我们将通信账单数据集的后三列提取出来,使用规范化方法进行转换如下:

可以发现转换后每个行向量都是单位长度,只是具有不同的角度。需要再次强调的是,本例仅为演示代码,因为对行执行规范化后,每列的分布形态会发生改变,因此不适用于本数据集。

老师的文章配套免费课程

立刻扫码或点击阅读原文学习吧~


点击
阅读原文即可免费学习本文配套的Python数据科学实战课程

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存