带你读《Python数据分析与数据化运营(第2版)》之三:10条数据化运营不得不知道的数据预处理经验-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

带你读《Python数据分析与数据化运营(第2版)》之三:10条数据化运营不得不知道的数据预处理经验

简介: 这是一本将数据分析技术与数据使用场景深度结合的著作,从实战角度讲解了如何利用Python进行数据分析和数据化运营。作者是有10余年数据分析与数据化运营的大数据专家,书中对50余个数据工作流知识点、14个数据分析与挖掘主题、4个数据化运营主题、8个综合性案例进行了全面的讲解,能让数据化运营结合数据使用场景360°落地。

点击查看第一章
点击查看第二章

第3章

10条数据化运营不得不知道的数据预处理经验
数据预处理是数据化运营过程中的重要环节,它直接决定了后期所有数据工作的质量和价值输出。从数据预处理的主要内容看,包括数据清洗、转换、归约、聚合、抽样等。本章将摒弃理论和方法说教,直接介绍预处理本身可能遇到的问题及应对方法。

3.1 数据清洗:缺失值、异常值和重复值的处理

在数据清洗过程中,主要处理的是缺失值、异常值和重复值。所谓清洗,是对数据集通过丢弃、填充、替换、去重等操作,达到去除异常、纠正错误、补足缺失的目的。

3.1.1 数据列缺失的4种处理方法

数据缺失分为两种:一种是行记录的缺失,这种情况又称数据记录丢失;另一种是数据列值的缺失,即由于各种原因导致的数据记录中某些列的值空缺。不同的数据存储和环境中对于缺失值的表示结果也不同,例如,数据库中是Null,Python返回对象是None,Pandas或Numpy中是NaN。
在极少数情况下,部分缺失值也会使用空字符串来代替,但空字符串绝对不同于缺失值。从对象的实体来看,空字符串是有实体的,实体为字符串类型;而缺失值其实是没有实体的,即没有数据类型。
丢失的数据记录通常无法找回,这里重点讨论数据列类型缺失值的处理思路。通常有4种思路。
1.丢弃
这种方法简单明了,直接删除带有缺失值的行记录(整行删除)或者列字段(整列删除),减少缺失数据记录对总体数据的影响。但丢弃意味着会消减数据特征,以下任何一种场景都不宜采用该方法。

  • 数据集总体中存在大量的数据记录不完整情况且比例较大,例如超过10%,删除这些带有缺失值的记录意味着会损失过多有用信息。
  • 带有缺失值的数据记录大量存在着明显的数据分布规律或特征,例如带有缺失值的数据记录的目标标签(即分类中的Label变量)主要集中于某一类或几类,如果删除这些数据记录将使对应分类的数据样本丢失大量特征信息,导致模型过拟合或分类不准确。

2.补全
相对丢弃而言,补全是更加常用的缺失值处理方式。通过一定的方法将缺失的数据补上,从而形成完整的数据记录,对于后续的数据处理、分析和建模至关重要。常用的补全方法如下。

  • 统计法:对于数值型的数据,使用均值、加权均值、中位数等方法补足;对于分类型数据,使用类别众数最多的值补足。
  • 模型法:更多时候我们会基于已有的其他字段,将缺失字段作为目标变量进行预测,从而得到最为可能的补全值。如果带有缺失值的列是数值变量,采用回归模型补全;如果是分类变量,则采用分类模型补全。
  • 专家补全:对于少量且具有重要意义的数据记录,专家补足也是非常重要的一种途径。
  • 其他方法:例如随机法、特殊值法、多重填补等。

3.真值转换法
在某些情况下,我们可能无法得知缺失值的分布规律,并且无法对于缺失值采用上述任何一种补全方法做处理;或者我们认为数据缺失也是一种规律,不应该轻易对缺失值随意处理,那么还有一种缺失值处理思路—真值转换。
该思路的根本观点是,我们承认缺失值的存在,并且把数据缺失也作为数据分布规律的一部分,将变量的实际值和缺失值都作为输入维度参与后续数据处理和模型计算中。但是变量的实际值可以作为变量值参与模型计算,而缺失值通常无法参与运算,因此需要对缺失值进行真值转换。
以用户性别字段为例,很多数据库集都无法对会员的性别进行补足,但又舍不得将其丢弃掉,那么我们将选择将其中的值,包括男、女、未知从一个变量的多个值分布状态转换为多个变量的真值分布状态。

  • 转换前:性别(值域:男、女、未知)。
  • 转换后:性别_男(值域1或0)、性别_女(值域1或0)、性别_未知(值域1或0)。

然后将这3列新的字段作为输入维度替换原来的1个字段参与后续模型计算。有关真值转换的具体方法和知识话题,会在3.2节中具体介绍。
4.不处理
在数据预处理阶段,对于具有缺失值的数据记录不做任何处理,也是一种思路。这种思路主要看后期的数据分析和建模应用,很多模型对于缺失值有容忍度或灵活的处理方法,因此在预处理阶段可以不做处理。常见的能够自动处理缺失值的模型包括:KNN、决策树和随机森林、神经网络和朴素贝叶斯、DBSCAN(基于密度的带有噪声的空间聚类)等。这些模型对于缺失值的处理思路是:

  • 忽略,缺失值不参与距离计算,例如KNN。
  • 将缺失值作为分布的一种状态,并参与到建模过程,例如各种决策树及其变体。
  • 不基于距离做计算,因此基于值的距离做计算本身的影响就消除了,例如DBSCAN。

在数据建模前的数据归约阶段,有一种归约的思路是降维,降维中有一种直接选择特征的方法。假如我们通过一定方法确定带有缺失值(无论缺少字段的值缺失数量有多少)的字段对于模型的影响非常小,那么我们根本就不需要对缺失值进行处理。因此,后期建模时的字段或特征的重要性判断也是决定是否处理字段缺失值的重要参考因素之一。
对于缺失值的处理思路是先通过一定方法找到缺失值,接着分析缺失值在整体样本中的分布占比,以及缺失值是否具有显著的无规律分布特征,然后考虑后续要使用的模型中是否能满足缺失值的自动处理,最后决定采用哪种缺失值处理方法。在选择处理方法时,注意投入的时间、精力和产出价值,毕竟,处理缺失值只是整个数据工作的冰山一角而已。
在数据采集时,可在采集端针对各个字段设置一个默认值。以MySQL为例,在设计数据库表时,可通过default指定每个字段的默认值,该值必须是常数。在这种情况下,假如原本数据采集时没有采集到数据,字段的值应该为Null,虽然由于在建立库表时设置了默认值会导致“缺失值”看起来非常正常,但本质上还是缺失的。对于这类数据需要尤其注意。

3.1.2 不要轻易抛弃异常数据

异常数据是数据分布的常态,处于特定分布区域或范围之外的数据通常会被定义为异常或“噪音”。产生数据“噪音”的原因很多,例如业务运营操作、数据采集问题、数据同步问题等。对异常数据进行处理前,需要先辨别出到底哪些是真正的数据异常。从数据异常的状态看分为两种:

  • 一种是“伪异常”,这些异常是由于业务特定运营动作产生的,其实是正常反映业务状态,而不是数据本身的异常规律。
  • 一种是“真异常”,这些异常并不是由于特定的业务动作引起的,而是客观地反映了数据本身分布异常的分布个案。

大多数数据挖掘或数据工作中,异常值都会在数据的预处理过程中被认为是噪音而剔除,以避免其对总体数据评估和分析挖掘的影响。但在以下几种情况下,我们无须对异常值做抛弃处理。
1.异常值正常反映了业务运营结果
该场景是由业务部门的特定动作导致的数据分布异常,如果抛弃异常值将导致无法正确反馈业务结果。
例如:公司的A商品正常情况下日销量为1000台左右。由于昨日举行优惠促销活动导致总销量达到10000台,由于后端库存备货不足导致今日销量又下降到100台。在这种情况下,10000台和100台都正确地反映了业务运营的结果,而非数据异常案例。
2.异常检测模型
异常检测模型是针对整体样本中的异常数据进行分析和挖掘,以便找到其中的异常个案和规律,这种数据应用围绕异常值展开,因此异常值不能做抛弃处理。
异常检测模型常用于客户异常识别、信用卡欺诈、贷款审批识别、药物变异识别、恶劣气象预测、网络入侵检测、流量作弊检测等。在这种情况下,异常数据本身是目标数据,如果被处理掉将损失关键信息。
3.包容异常值的数据建模
如果数据算法和模型对异常值不敏感,那么即使不处理异常值也不会对模型本身造成负面影响。例如在决策树中,异常值本身就可以作为一种分裂节点。

image.png

3.1.3 数据重复就需要去重吗

数据集中的重复值包括以下两种情况:

  • 数据值完全相同的多条数据记录。这是最常见的数据重复情况。
  • 数据主体相同但匹配到的唯一属性值不同。这种情况多见于数据仓库中的变化维度表,同一个事实表的主体会匹配同一个属性的多个值。

去重是重复值处理的主要方法,主要目的是保留能显示特征的唯一数据记录。但当遇到以下几种情况时,请慎重(不建议)执行数据去重。

  • 1.重复的记录用于分析演变规律

以变化维度表为例。例如在商品类别的维度表中,每个商品对应的同1个类别的值应该是唯一的,例如苹果iPhone7属于个人电子消费品,这样才能将所有商品分配到唯一类别属性值中。但当所有商品类别的值重构或升级时(大多数情况下随着公司的发展都会这么做),原有的商品可能被分配了类别中的不同值。如

image.png

此时,我们在数据中使用Full join做跨重构时间点的类别匹配时,会发现苹果iPhone7会同时匹配到个人电子消费品和手机数码2条记录。对于这种情况,需要根据具体业务需求处理。

  • 如果跟业务沟通,两条数据需要做整合,那么需要确定一个整合字段用来涵盖2条记录。其实就是将2条数据再次映射到一个类别主体中。
  • 如果跟业务沟通,需要同时保存两条数据,那么此时不能做任何处理。后续的具体处理根据建模需求而定。

image.png

2.重复的记录用于样本不均衡处理
在开展分类数据建模工作时,样本不均衡是影响分类模型效果的关键因素之一。解决分类方法的一种方法是对少数样本类别做简单过采样,通过随机过采样,采取简单复制样本的策略来增加少数类样本。经过这种处理方式后,也会在数据记录中产生相同记录的多条数据。此时,我们不能对其中的重复值执行去重操作。
有关样本不均衡的相关内容将在3.4节中介绍。
3.重复的记录用于检测业务规则问题
对于以分析应用为主的数据集而言,存在重复记录不会直接影响实际运营,毕竟数据集主要是用来做分析的。但对于事务型的数据而言,重复数据可能意味着重大运营规则问题,尤其当这些重复值出现在与企业经营中与金钱相关的业务场景时,例如:重复的订单、重复的充值、重复的预约项、重复的出库申请等。
这些重复的数据记录通常是由于数据采集、存储、验证和审核机制的不完善等问题导致的,会直接反映到前台生产和运营系统。以重复订单为例,假如前台的提交订单功能不做唯一性约束,那么在一次订单中重复点击提交订单按钮,就会触发多次重复提交订单的申请记录,如果该操作审批通过后,会联动带动运营后端的商品分拣、出库、送货,如果用户接收重复商品则会导致重大损失;如果用户退货则会增加反向订单,并影响物流、配送和仓储相关的各个运营环节,导致运营资源无端消耗、商品损耗增加、仓储物流成本增加等问题。
因此,这些问题必须在前期数据采集和存储时就通过一定机制解决和避免。如果确实产生了此类问题,那么数据工作者或运营工作者可以基于这些重复值来发现规则漏洞,并配合相关部门,最大限度地降低由此而带来的运营风险。

3.1.4 代码实操:Python数据清洗

1.缺失值处理
在缺失值的处理上,主要配合使用sklearn.preprocessing中的Imputer类、Pandas和Numpy。其中由于Pandas对于数据探索、分析和探查的支持较为良好,因此围绕Pandas的缺失值处理较为常用。
第1部分为导入库,该代码示例中用到Pandas、Numpy和sklearn。
import pandas as pd  # 导入Pandas库
import numpy as np  # 导入Numpy库
from sklearn.preprocessing import Imputer  # 导入sklearn.preprocessing中的Imputer库
第2部分生成缺失数据。
# 生成缺失数据
df = pd.DataFrame(np.random.randn(6, 4), columns=['col1', 'col2', 'col3', 'col4'])     # 生成一份数据
df.iloc[1:2, 1] = np.nan       # 增加缺失值
df.iloc[4, 3] = np.nan  # 增加缺失值
print(df)
通过Pandas生成一个6行4列,列名分别为'col1'、'col2'、'col3'、'col4'的数据框。同时,数据框中增加两个缺失值数据。除了示例中直接通过pd.DataFrame来直接创建数据框外,还可以使用数据框对象的df.from_records、df.from_dict、df.from_items来从元组记录、字典和键值对对象创建数据框,或使用pandas.read_csv、pandas.read_table、pandas.read_clipboard等方法读取文件或剪贴板创建数据框。该代码段执行后返回了定义含有缺失值的数据框,结果如下:
image.png

image.png

第3部分判断缺失值。
# 查看哪些值缺失
nan_all = df.isnull() # 获得所有数据框中的N值
print(nan_all) # 打印输出
# 查看哪些列缺失
nan_col1 = df.isnull().any() # 获得含有NA的列
nan_col2 = df.isnull().all() # 获得全部为NA的列
print(nan_col1) # 打印输出
print(nan_col2) # 打印输出
通过df.null()方法找到所有数据框中的缺失值(默认缺失值是NaN格式),然后使用any()或all()方法来查找含有至少1个或全部缺失值的列,其中any()方法用来返回指定轴中的任何元素为True,而all()方法用来返回指定轴的所有元素都为True。该代码段执行后返回如下结果。
判断元素是否是缺失值(第2行第2列和第5行第4列):
col1 col2 col3 col4
0 False False False False
1 False True False False
2 False False False False
3 False False False False
4 False False False True
5 False False False False
列出至少有一个元素含有缺失值的列(该示例中为col2和col4):
col1 False
col2 True
col3 False
col4 True
dtype: bool
列出全部元素含有缺失值的列(该示例中没有):
col1 False
col2 False
col3 False
col4 False
dtype: bool
第4部分丢弃缺失值。
df2 = df.dropna() # 直接丢弃含有NA的行记录
print(df2) # 打印输出
通过Pandas默认的dropna()方法丢弃缺失值,返回无缺失值的数据记录。该代码段执行后返回如下结果(第2行、第5行数据记录被删除):
image.png

第5部分通过sklearn的数据预处理方法对缺失值进行处理。
nan_model = Imputer(missing_values='NaN', strategy='mean', axis=0) # 建立替换规则:将值为NaN的缺失值以均值做替换
nan_result = nan_model.fit_transform(df) # 应用模型规则
print(nan_result) # 打印输出
首先通过Imputer方法创建一个预处理对象,其中missing_values为默认缺失值的字符串,默认为NaN;示例中选择缺失值替换方法是均值(默认),还可以选择使用中位数和众数进行替换,即strategy值设置为median或most_frequent;后面的参数axis用来设置输入的轴,默认值为0,即使用列做计算逻辑。然后使用预处理对象的fit_transform方法对df(数据框对象)进行处理,该方法是将fit和transform组合起来使用。代码执行后返回如下结果:
image.png

代码中的第2行第2列和第5行第4列分别被各自列的均值替换。为了验证,我们手动计算一下各自列的均值,通过使用df['col2'].mean()和df['col4'].mean()分别获得这两列的均值为-0.4494679289032068和-0.16611331259664791,与sklearn返回的结果一致。
第6部分使用Pandas做缺失值处理。
nan_result_pd1 = df.fillna(method='backfill') # 用后面的值替换缺失值
nan_result_pd2 = df.fillna(method='bfill', limit=1) # 用后面的值替代缺失值,限制每列只能替代一个缺失值
nan_result_pd3 = df.fillna(method='pad') # 用前面的值替换缺失值
nan_result_pd4 = df.fillna(0) # 用0替换缺失值
nan_result_pd5 = df.fillna({'col2': 1.1, 'col4': 1.2}) # 用不同值替换不同列的缺失值
nan_result_pd6 = df.fillna(df.mean()['col2':'col4']) # 用各自列的平均数替换缺失值
# 打印输出
print(nan_result_pd1) # 打印输出
print(nan_result_pd2) # 打印输出
print(nan_result_pd3) # 打印输出
print(nan_result_pd4) # 打印输出
print(nan_result_pd5) # 打印输出
print(nan_result_pd6) # 打印输出
Pandas对缺失值的处理方法是df.fillna(),该方法中最主要的两个参数是value和method。前者通过固定(或手动指定)的值替换缺失值,后者使用Pandas提供的默认方法替换缺失值。以下是method支持的方法。

  • pad和ffill:使用前面的值替换缺失值,示例中nan_result_pd3使用了pad方法。
  • backfill和bfill:使用后面的值替换缺失值,示例中nan_result_pd1和nan_result_pd2使用了该方法。
  • None:无。

在示例中,nan_result_pd4、nan_result_pd5、nan_result_pd6分别使用0、不同的值、平均数替换缺失值。需要注意的是,如果要使用不同具体值替换,需要使用scalar、dict、Series或DataFrame的格式定义。
上述代码执行后返回如下结果。
用后面的值(method='backfill')替换缺失值:

   col1      col2      col3      col4

0 -0.112415 -0.768180 -0.084859 0.296691
1 -1.777315 1.892790 -0.166615 -0.628756
2 -0.629461 1.892790 -1.850006 0.157567
3 0.544860 -1.230804 0.836615 -0.945712
4 0.703394 -0.764552 -1.214379 0.289643
5 1.928313 -1.376593 -1.557721 0.289643
用后面的值(method='bfill', limit = 1)替换缺失值:

   col1      col2      col3      col4

0 -0.112415 -0.768180 -0.084859 0.296691
1 -1.777315 1.892790 -0.166615 -0.628756
2 -0.629461 1.892790 -1.850006 0.157567
3 0.544860 -1.230804 0.836615 -0.945712
4 0.703394 -0.764552 -1.214379 0.289643
5 1.928313 -1.376593 -1.557721 0.289643
用前面的值替换缺失值(method='pad'):

   col1      col2      col3      col4

0 -0.112415 -0.768180 -0.084859 0.296691
1 -1.777315 -0.768180 -0.166615 -0.628756
2 -0.629461 1.892790 -1.850006 0.157567
3 0.544860 -1.230804 0.836615 -0.945712
4 0.703394 -0.764552 -1.214379 -0.945712
5 1.928313 -1.376593 -1.557721 0.289643
用0替换缺失值:

   col1      col2      col3      col4

0 -0.112415 -0.768180 -0.084859 0.296691
1 -1.777315 0.000000 -0.166615 -0.628756
2 -0.629461 1.892790 -1.850006 0.157567
3 0.544860 -1.230804 0.836615 -0.945712
4 0.703394 -0.764552 -1.214379 0.000000
5 1.928313 -1.376593 -1.557721 0.289643
手动指定两个缺失值分布为1.1和1.2:

   col1      col2      col3      col4

0 -0.112415 -0.768180 -0.084859 0.296691
1 -1.777315 1.100000 -0.166615 -0.628756
2 -0.629461 1.892790 -1.850006 0.157567
3 0.544860 -1.230804 0.836615 -0.945712
4 0.703394 -0.764552 -1.214379 1.200000
5 1.928313 -1.376593 -1.557721 0.289643
用平均数代替,选择各自列的均值替换缺失值:

   col1      col2      col3      col4

0 -0.112415 -0.768180 -0.084859 0.296691
1 -1.777315 -0.449468 -0.166615 -0.628756
2 -0.629461 1.892790 -1.850006 0.157567
3 0.544860 -1.230804 0.836615 -0.945712
4 0.703394 -0.764552 -1.214379 -0.166113
5 1.928313 -1.376593 -1.557721 0.289643
以上示例中,直接指定method的方法适用于大多数情况,较为简单直接;但使用value的方法则更为灵活,原因是可以通过函数的形式将缺失值的处理规则写好,然后直接赋值即可。限于篇幅,不对所有方法做展开讲解。
另外,如果是直接替换为特定值的应用,也可以考虑使用Pandas的replace功能。本示例的df(原始数据框)可直接使用df.replace(np.nan,0),这种用法更加简单粗暴,但也能达到效果。当然,replace的出现是为了解决各种替换应用的,缺失值只是其中的一种应用而已。
上述过程中,主要需要考虑的关键点是缺失值的替换策略,可指定多种方法替换缺失值,具体根据实际需求而定,但大多数情况下均值、众数和中位数的方法较为常用。如果场景固定,也可以使用特定值(例如0)替换。
在使用不同的缺失值策略时,需要注意以下几个问题:
1)缺失值的处理的前提是已经可以正确识别所有缺失值字段,关于识别的问题在使用Pandas读取数据时可通过设置na_values的值指定。但是如果数据已经读取完毕并且不希望再重新读取,那可以使用Pandas的replace功能将指定的字符串(或列表)替换为NaN。更有效的是,如果数据中的缺失值太多而无法通过列表形式穷举时,replace还支持正则表达式的写法。
2)当列中的数据全部为空值时,任何替换方法都将失效,任何基于中位数、众数和均值的策略都将失效。除了可以使用固定值替换外(这种情况下即使替换了该特征也没有实际参与模型的价值),最合理的方式是先将全部为缺失值的列删除,然后再做其他处理。
3)当列中含有极大值或极小值的inf或-inf时,会使得mean()这种方法失效,因为这种情况下将无法计算出均值。应对思路是使用median中位数做兜底策略,只要列中有数据,就一定会有中位数。
2.异常值处理
有关异常值的确定有很多规则和方法,这里使用Z标准化得到的阈值作为判断标准:当标准化后的得分超过阈值则为异常。完整代码如下。
示例代码分为3个部分。
第1部分导入本例需要的Pandas库。
import pandas as pd # 导入Pandas库
第2部分生成异常数据。
df = pd.DataFrame({'col1': [1, 120, 3, 5, 2, 12, 13],

               'col2': [12, 17, 31, 53, 22, 32, 43]})

print(df) # 打印输出
直接通过DataFrame创建一个7行2列的数据框,打印输出结果如下:
col1 col2
0 1 12
1 120 17
2 3 31
3 5 53
4 2 22
5 12 32
6 13 43
第3部分为通过Z-Score方法判断异常值。
df_zscore = df.copy() # 复制一个用来存储Z-score得分的数据框
cols = df.columns # 获得数据框的列名
for col in cols: # 循环读取每列

df_col = df[col]                  # 得到每列的值
z_score = (df_col - df_col.mean()) / df_col.std()  # 计算每列的Z-score得分
df_zscore[col] = z_score.abs() > 2.2        # 判断Z-score得分是否大于2.2,如果是则为True,否则为False

print(df_zscore) # 打印输出
本过程中,先通过df.copy()复制一个原始数据框的副本,用来存储Z-Score标准化后的得分,再通过df.columns获得原始数据框的列名,接着通过循环判断每一列中的异常值。在判断逻辑中,对每一列的数据进行使用自定义的方法做Z-Score值标准化得分计算,然后与阈值2.2做比较,如果大于阈值则为异常。标准化的计算还有更多自动化的方法和场景,有关数据标准化的话题,将在3.9节中具体介绍。本段代码返回结果如下:

col1   col2

0 False False
1 True False
2 False False
3 False False
4 False False
5 False False
6 False False
在本示例方法中,阈值的设定是确定异常与否的关键,通常当阈值大于2.2时,就是相对异常的表现值。
第4部分删除带有异常值所在的记录行。
df_drop_outlier = df[df_zscore['col1'] == False]
print(df_drop_outlier)
本段代码里我们直接使用了Pandas的选择功能,即只保留在df_zscore中异常列(col1)为False的列。完成后在输出的结果中可以看到,删除了index值为1的数据行。
col1 col2
0 1 12
2 3 31
3 5 53
4 2 22
5 12 32
6 13 43
上述过程中,主要需要考虑的关键点是:如何判断异常值。
对于有固定业务规则的可直接套用业务规则,而对于没有固定业务规则的,可以采用常见的数学模型进行判断:基于概率分布的模型(例如正态分布的标准差范围)、基于聚类的方法(例如KMeans)、基于密度的方法(例如LOF)、基于分类的方法(例如KNN)、基于统计的方法(例如分位数法)等。异常值的定义带有较强的主观判断色彩,具体需要根据实际情况选择。
3.重复值处理
有关重复值的处理代码分为4个部分。
第1部分为导入用到的Pandas库。
import pandas as pd # 导入Pandas库
第2部分生成重复数据。
data1, data2, data3, data4 = ['a', 3], ['b', 2], ['a', 3], ['c', 2]
df = pd.DataFrame([data1, data2, data3, data4], columns=['col1', 'col2'])
print(df)
在代码中,我们在一列中直接给4个对象赋值,也可以拆分为4行分别赋值。该数据是一个4行2列数据框,数据结果如下:
col1 col2
0 a 3
1 b 2
2 a 3
3 c 2
第3部分判断重复数据。
isDuplicated = df.duplicated() # 判断重复数据记录
print(isDuplicated) # 打印输出
判断数据记录是否为重复值,返回每条数据记录是否重复结果,取值为True或False。判断方法为df.duplicated(),该方法中两个主要的参数是subset和keep。

  • subset:要判断重复值的列,可以指定特定列或多个列。默认使用全部列。
  • keep:当重复时不标记为True的规则,可设置为第1个(first)、最后一个(last)和全部标记为True(False)。默认使用first,即第1个重复值不标记为True。

结果如下:
0 False
1 False
2 True
3 False
dtype: bool
第4部分删除重复值。
print(df.drop_duplicates()) # 删除数据记录中所有列值相同的记录
print(df.drop_duplicates(['col1'])) # 删除数据记录中col1值相同的记录
print(df.drop_duplicates(['col2'])) # 删除数据记录中col2值相同的记录
print(df.drop_duplicates(['col1', 'col2'])) # 删除数据记录中指定列(col1/col2)值相同的记录
该操作的核心方法是df.drop_duplicates(),该方法的作用是基于指定的规则判断为重复值之后,删除重复值,其参数跟df.duplicated()完全相同。在该部分方法示例中,依次使用默认规则(全部列相同的数据记录)、col1列相同、col2列相同以及指定col1和col2完全相同4种规则进行去重。返回结果如下。
删除数据记录中所有列值相同的记录,index为2的记录行被删除:
col1 col2
0 a 3
1 b 2
3 c 2
删除数据记录中col1值相同的记录,index为2的记录行被删除:
col1 col2
0 a 3
1 b 2
3 c 2
删除数据记录中col2值相同的记录,index为2和3的记录行被删除:
col1 col2
0 a 3
1 b 2
删除数据记录中指定列(col1和col2)值相同的记录,index为2的记录行被删除:
col1 col2
0 a 3
1 b 2
3 c 2
由于数据是通过随机数产生,因此读者操作的结果可能与上述示例的数据结果不同。
除了可以使用Pandas来做重复值判断和处理外,也可以使用Numpy中的unique()方法,该方法返回其参数数组中所有不同的值,并且按照从小到大的顺序排列。Python自带的内置函数set方法也能返回唯一元素的集合。
上述过程中,主要需要考虑的关键点是:如何对重复值进行处理。重复值的判断相对简单,而判断之后如何处理往往不是一个技术特征明显的工作,而是侧重于业务和建模需求的工作。
代码实操小结:本节示例中,主要用了几个知识点:

  • 通过pd.DataFrame新建数据框。
  • 通过df.iloc[]来选择特定的列或对象。
  • 使用Pandas的isnull()判断值是否为空。
  • 使用all()和any()判断每列是否包含至少1个为True或全部为True的情况。
  • 使用Pandas的dropna()直接删除缺失值。
  • 使用sklearn.preprocessing中的Imputer方法对缺失值进行填充和替换,支持3种填充方法。
  • 使用Pandas的fillna填充缺失值,支持更多自定义的值和常用预定义方法。
  • 通过copy()获得一个对象副本,常用于原始对象和复制对象同时进行操作的场景。
  • 通过for循环遍历可迭代的列表值。
  • 自定义代码实现了Z-Score计算公式。
  • 通过Pandas的duplicated()判断重复数据记录。
  • 通过Pandas的drop_duplicates()删除数据记录,可指定特定列或全部。

3.2 将分类数据和顺序数据转换为标志变量

分类数据和顺序数据是常见的数据类型,这些值主要集中在围绕数据实体的属性和描述的相关字段和变量中。

3.2.1 分类数据和顺序数据是什么

在数据建模过程中,很多算法无法直接处理非数值型的变量。例如,KMeans算法基于距离的相似度计算,而字符串则无法直接计算距离。另外,即使算法本身支持,很多算法实现包也无法直接基于字符串做矩阵运算,例如Numpy以及基于Numpy的sklearn。虽然这些库允许直接使用和存储字符串型变量,但却无法发挥矩阵计算的优势。这些类型的数据变量可以分为两类。
1)分类数据:分类数据指某些数据属性只能归于某一类别的非数值型数据,例如性别中的男、女就是分类数据。分类数据中的值没有明显的高、低、大、小等包含等级、顺序、排序、好坏等逻辑的划分,只是用来区分两个或多个具有相同或相当价值的属性。例如:性别中的男和女,颜色中的红、黄和蓝,它们都是相同衡量维度上的不同属性分类而已。
2)顺序数据:顺序数据只能归于某一有序类别的非数值型数据,例如用户的价值度分为高、中、低,学历分为博士、硕士、学士,这些都属于顺序数据。在顺序数据中,有明显的排序规律和逻辑层次的划分。例如:高价值的用户就比低价值的用户价值高(业务定义该分类时已经赋予了这样的价值含义)。

image.png

3.2.2 运用标志方法处理分类和顺序变量

分类数据和顺序数据要参与模型计算,通常都会转化为数值型数据。当然,某些算法是允许这些数据直接参与计算的,例如分类算法中的决策树、关联规则等。将非数值型数据转换为数值型数据的最佳方法是:将所有分类或顺序变量的值域从一列多值的形态转换为多列只包含真值的形态,其中的真值可用True、False或0、1的方式来表示。这种标志转换的方法有时候也称为真值转换。以用户性别变量举例,原有的用户数据如表3-2所示。

image.png

经过转换后的数据如表3-3所示。

image.png

为什么不能直接用数字来表示不同的分类和顺序数据,而一定要做标志转换?这是因为在用数字直接表示分类和顺序变量的过程中,无法准确还原不同类别信息之间的信息差异和相互关联性。

  • 针对分类数据:性别变量的属性值是男和女,无论用什么值来表示都无法表达出两个值的价值相等且带有区分的含义。如果用1和2区分,那么1和2本身已经带有距离为1的差异,但实际上二者是不具有这种差异性的,其他任意数字都是如此;如果用相同的数字来表示,则无法达到区分的目的。
  • 针对顺序数据:学历变量的属性值是博士、硕士和学士,可以用3-2-1来表示顺序和排列关系,那么如何表示3个值之间的差异是3-2-1而不是30-20-10或者1000-100-2呢?因此,任何一个有序数字的排序也都无法准确表达出顺序数据的差异性。

3.2.3 代码实操:Python标志转换

在本示例中,将模拟有两列数据分别出现分类数据和顺序数据的情况,并通过自定义代码及sklearn代码分别进行标志转换。
第1部分导入库。
本示例使用Pandas库和sklearn,sklearn中的OneHotEncoder用来将数值型类别变量转换为0-1的标志型变量,LabelEncoder用来将字符串型变量转换为数值型变量。
import pandas as pd # 导入Pandas库
from sklearn.preprocessing import OneHotEncoder, LabelEncoder # 导入库
第2部分生成原始数据。
df = pd.DataFrame({'id': [3566841, 6541227, 3512441],

               'sex': ['male', 'Female', 'Female'],
               'level': ['high', 'low', 'middle'],
               'score': ['1', '2', '3']})

print(df) # 打印输出原始数据框
数据为3行3列的数据框,分别包含id、sex和level列,其中的id为模拟的用户ID,sex为用户性别(英文),level为用户等级(分别用high、middle和low代表3个等级)。该段代码输出原始数据框如下:

          id     sex   level score

0 3566841 male high 1
1 6541227 Female low 2
2 3512441 Female middle 3

image.png

第3部分使用sklearn.preprocessing中的OneHotEncoder方法进行标志转换。
1)拆分ID和数据列。
id_data = df[['id']] # 获得ID列
raw_convert_data = df.iloc[:, 1:] # 指定要转换的列
print(raw_convert_data)
这里直接使用数据框的指定列名来拆分ID列,使用iloc方法拆分出字符串和数值型的分类变量。

image.png

原始字符串型数据列的输出如下:

level     sex

0 high male
1 low Female
2 middle Female

image.png

2)对数据列中的字符串列数据做转换。
model_enc = OneHotEncoder() # 建立标志转换模型对象(也称为哑编码对象)
df_new2 = model_enc.fit_transform(raw_convert_data).toarray() # 标志转换
在该过程中,先建立一个LabelEncoder对象model_LabelEncoder,然后使用model_Label-Encoder做fit_transform转换,转换后的值直接替换上一步创建的副本transform_data_copy,然后使用toarray方法输出为矩阵。

image.png

3)合并数据。
df_all = pd.concat((id_data, pd.DataFrame(df_new2)), axis=1) # 重新组合为数据框
print(df_all) # 打印输出转换后的数据框
转换完成后,使用Pandas的concat方法,将ID列与转换后的列拼接为完整主体。Df_new2是一个numpy数组,如果不转换为DataFrame则无法直接与其他两个数据框合并。程序执行得到的结果如下:

    id    0    1    2    3    4    5    6    7

0 3566841 0.0 1.0 1.0 0.0 0.0 1.0 0.0 0.0
1 6541227 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0
2 3512441 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0

image.png

第4部分使用Pandas的get_dummies做标志转换。
df_new3 = pd.get_dummies(raw_convert_data)
df_all2 = pd.concat((id_data, pd.DataFrame(df_new3)), axis=1) # 重新组合为数据框
print(df_all2)    # 打印输出转换后的数据框
该过程中,用到了Pandas的get_dummies方法。该方法的主要参数如下。

  • data:要转换的对象,可以是类数组、Series或DataFrame。
  • prefix:转换后列名的前缀,可以是字符串或None,或由字符串组成的列表、字典等。
  • prefix_sep:前缀分隔符,字符串,默认是_(下划线)。
  • dummy_na:增加一列表示NA值,布尔型,默认为False。如果是False就忽略NA值。
  • columns:要转换的列名,类列表,默认为None,表示所有类型为object和category类型的列都将被转换。
  • sparse:是否为稀疏矩阵,布尔型,默认为False。

该部分代码完成后的输出结果如下:

    id  score  sex_Female  sex_male  level_high  level_low  level_middle

0 3566841 1 0 1 1 0 0
1 6541227 2 1 0 0 1 0
2 3512441 3 1 0 0 0 1

image.png

上述过程中,主要需要考虑的关键点是:

  • 如何判断要转换的数据是分类或顺序变量。
  • 分类型变量或顺序意义的变量不一定都是字符串类型。

代码实操小结:本小节示例中,主要用了以下几个知识点。

  • 通过pd.DataFrame构建新的数据框。
  • 通过Pandas中的df[[col_name]]和iloc[]进行数据切片或字段选择。
  • 通过Pandas中的drop()方法删除特定列,当然也可以用于删除行。
  • 通过Pandas的dtype获得对象的dtype类型,df.dtypes也能实现所有对象的类型。
  • 通过unique()方法获得唯一值。
  • 通过字符串组合(示例中直接使用的+)创建一个新的字符串。
  • 直接使用DataFrame和Series对象而无须遍历每个值进行矩阵比较和数值计算。
  • 通过Pandas的df_new[col_name_new]方法直接新增列值。
  • 使用OneHotEncoder将数值型分类向量转换为标志变量。
  • 使用Pandas的concat方法合并多个数据框。
  • 使用Pandas的get_dummies方法做标志转换。

3.3 大数据时代的数据降维

数据降维就是降低数据的维度数量,数据降维是维数归约的一个重要课题。

3.3.1 需要数据降维的情况

数据降维可以降低模型的计算量并减少模型运行时间,降低噪音变量信息对于模型结果的影响,便于通过可视化方式展示归约后的维度信息,并减少数据存储空间。因此,大多数情况下,当我们面临高维数据时,都需要对数据做降维处理。是否进行降维主要考虑以下方面:
1)维度数量。降维的基本前提是高维,假如模型只有几个维度,那就不一定需要降维,具体取决于维度本身的重要性、共线性以及其他排除关系,而不是出于高维的考虑。
2)建模输出是否必须保留原始维度。某些场景下,我们需要完整保留参与建模的原始维度并在最终建模输出时能够得以分析、解释和应用,这种情况下不能进行转换方式降维,只能选择特征筛选的方式降维。
3)对模型的计算效率与建模时效性有要求。当面临高维数据建模时,数据模型的消耗将呈几何倍数增长,这种增长带来的结果便是运算效率慢、耗时长。如果对建模时间和时效性有要求,那么降维几乎是必要步骤。
4)是否要保留完整数据特征。数据降维的基本出发点是在尽量(或最大化)保留原始数据特征的前提下,降低参与建模的维度数。在降维过程中,无论未被表示出来的特征是噪音还是正常分布,这部分信息都无法参与建模。如果某些场景下需要所有数据集的完整特征,那么通常不选择降维。
数据降维只是处理高维数据的思路和方法之一,除此之外,国内外学者也在研究其他高维数据建模的方法。以高维数据聚类为例,除了可以通过降维来应对之外,其他思路还包括基于超图的聚类,基于子空间的聚类、联合聚类等,这些都是非降维的方法。

3.3.2 基于特征选择的降维

基于特征选择的降维指的是根据一定规则和经验,直接选取原有维度的一部分参与后续的计算和建模过程,用选择的维度代替所有维度,整个过程不产生新的维度。
基于特征选择的降维方法有4种思路。

  • 经验法:根据业务专家或数据专家的以往经验、实际数据情况、业务理解程度等进行综合考虑。业务经验依靠的是业务背景,从众多维度特征中选择对结果影响较大的特征;而数据专家则依靠的是数据工作经验,基于数据的基本特征及对后期数据处理和建模的影响来选择或排除维度,例如去掉缺失值较多的特征。
  • 测算法:通过不断测试多种维度选择参与计算,通过结果来反复验证和调整,并最终找到最佳特征方案。
  • 基于统计分析的方法:通过相关性分析不同维度间的线性相关性,在相关性高的维度中进行人工去除或筛选;或者通过计算不同维度间的互信息量,找到具有较高互信息量的特征集,然后去除或留下其中一个特征。
  • 机器学习算法:通过机器学习算法得到不同特征的特征值或权重,然后再根据权重来选择较大的特征。如图3-1所示是通过CART决策树模型得到不同变量的重要性,然后根据实际权重值进行选择。

这种数据降维方法的好处是,在保留了原有维度特征的基础上进行降维,既能满足后续数据处理和建模需求,又能保留维度原本的业务含义,以便于业务理解和应用。对于业务分析型的应用而言,模型的可理解性、可解释性和可应用性在很多时候的优先级要高于模型本身的准确率、效率等技术类指标,要知道,如果没有业务的理解和支持,再好的数据、模型和算法都无法落地。

image.png

例如,通过决策树得到的特征规则,可以作为选择用户样本的基础条件,而这些特征规则便是基于输入的维度产生。假如我们在决策树之前将原有维度用表达式(例如PCA的主成分)方法进行转换,那么即使得到了决策树规则,也无法直接提供给业务应用。

3.3.3 基于特征转换的降维

基于特征转换的降维是按照一定的数学变换方法,把给定的一组相关变量(特征)通过数学模型将高维空间的数据点映射到低维度空间中,然后利用映射后变量的特征来表示原有变量的总体特征。这种方式是一种产生新维度的过程,转换后的维度并非原有维度的本体,而是其综合多个维度转换或映射后的表达式。
通过数据维度变换进行降维是非常重要的降维方法,这种降维方法分为线性降维和非线性降维两种,其中常用的代表算法包括独立成分分析(ICA)、主成分分析(PCA)、因子分析(Factor Analysis,FA)、线性判别分析(LDA,也叫Fisher线性判别FLD)、局部线性嵌入(LLE)、核主成分分析(Kernel PCA)等。
(1)PCA(主成分分析)
主成分分析的基本方法是按照一定的数学变换方法,把给定的一组相关变量(维度)通过线性变换转成另一组不相关的变量,这些新的变量按照方差依次递减的顺序排列。在数学变换中保持变量的总方差不变,使第1变量具有最大的方差,称为第1主成分,第2变量的方差次大,并且和第1变量不相关,称为第2主成分。依次类推,I个变量就有I个主成分。
例如,假设原始数据集中有10个维度分别是tenure、cardmon、lncardmon、cardten、lncardten、wiremon、lnwiremon、wireten、lnwireten、hourstv,现在用主成分分析进行降维,降维后选择具有显著性的前3个主成分示例。
方程式用于 主成分-1

0.006831 * tenure +
0.007453 * cardmon +
0.1861 * lncardmon +
0.0001897 * cardten +
0.1338 * lncardten +
+ -4.767

方程式用于 主成分-2

-0.4433 * lnwiremon +
-0.0001222 * wireten +
-0.1354 * lnwireten +
0.008099 * hourstv +
+ -0.272

方程式用于 主成分-3

-0.01809 * tenure +
0.0124 * cardmon +
0.00002565 * wireten +
-0.1644 * lnwireten +
0.03984 * hourstv +
+ -4.076

上述转换结果就是主成分分析后提取的3个能基本代表原始10个维度的新“维度”。通过上述结果发现,主成分(也就是新的“维度”)是一个多元一次方程,其含义无法直接体现和理解。但对于不关注每个维度业务含义的系统级应用和集成来讲是没有关系的,因为后续算法不需要知道每个变量的具体业务含义。
为了更形象地解释映射关系,现在使用一个序列图来表示整个映射过程。如图3-2所示为原始数据集在经过顺时针旋转(映射变换)后的可视化特征变化过程。从图①到图⑧的序列中可以看出,原始数据的分布呈现明显的曲线分布特征,而旋转后的数据样本则呈现直线分布特征。

image.png

PCA主要适用的应用场景如下:

  • 非监督式类型的数据集。它是一种非监督式的降维方法,因此适用于不带有标签的数据集;而对于带有标签的数据集则可以采用LDA。
  • 根据方差自主控制特征数量。最大的主成分的数量≤特征的数量,这意味着,PCA也可以输出数量完全相同的特征,具体取决于选择特征中解释的方差比例。
  • 更少的正则化处理。选择较多的主成分将导致较少的平滑,因为我们将能够保留更多的数据特征,从而减少正则化。
  • 数据量较大的数据集。数据量大包括数据记录多和数据维度多两种情况,PCA对大型数据集的处理效率较高。
  • 数据分布是位于相同平面上(非曲面),数据中存在线性结构。

(2)FA(因子分析)
因子分析(Factor Analysis)是指研究从变量群中提取共性因子的统计技术,这里的共性因子指的是不同变量之间内在的隐藏因子。例如,一个学生的英语、数据、语文成绩都很好,那么潜在的共性因子可能是智力水平高。因此,因子分析的过程其实是寻找共性因子和个性因子并得到最优解释的过程。
FA与PCA对比,二者的相同点如下:

  • PCA和FA都是数据降维的重要方法,都对原始数据进行标准化处理,都消除了原始指标的相关性对综合评价所造成的信息重复的影响。
  • 二者构造综合评价时所涉及的权数具有客观性,在原始信息损失不大的前提下,减少了后期数据挖掘和分析的工作量。
  • 二者由于侧重点都是进行数据降维,因此很少单独使用,大多数情况下都会有一些模型组合使用。

之所以大多数情况下,很难感性地区分因子分析和主成分分析,原因是二者的降维结果都是对原有维度进行一定的处理,在处理的结果上都偏离了原有基于维度的认识。但只要清楚二者的逻辑一个是基于变量的线性组合,一个是基于因子的组合,便能很好地进行区分。
FA与PCA对比,二者的主要区别如下:

  • 原理不同。主成分分析的基本原理是利用降维(线性变换)的思想,在损失很少信息的前提下把多个指标转化为几个不相关的主成分,每个主成分都是原始变量的线性组合;而因子分析基本原理是从原始变量相关矩阵内部的依赖关系出发,把因子表达成能表示成少数公共因子和仅对某一个变量有作用的特殊因子的线性组合。因子分析是主成分的推广,相对于主成分分析,更倾向于描述原始变量之间的相关关系。
  • 假设条件不同。主成分分析不需要有假设,而因子分析需要假设各个共同因子之间不相关,特殊因子(specificfactor)之间也不相关,共同因子和特殊因子之间也不相关。
  • 求解方法不同。主成分分析的求解方法从协方差阵出发,而因子分析的求解方法包括主成分法、主轴因子法、极大似然法、最小二乘法、a因子提取法等。
  • 降维后的“维度”数量不同,即因子数量和主成分的数量。主成分分析的数量最多等于维度数;而因子分析中的因子个数需要分析者指定(SPSS和SAS根据一定的条件自动设定,只要是特征值大于1的因子可进入分析),指定的因子数量不同而结果也不同。

综合来看,因子分析在实现中可以使用旋转技术,因此可以得到更好的因子解释,这一点比主成分占优势。因此如果后续数据挖掘或处理过程需要解释或者通过原始变量的意义去应用,那么选择因子分析更合适。另外,因子分析不需要舍弃原有变量,而是将原有变量间的共性因子作为下一步应用的前提,其实就是由表及里去发现内在规律。但是,主成分分析由于不需要假设条件,并且可以最大限度地保持原有变量的大多数特征,因此适用范围更广泛,尤其是宏观的未知数据的稳定度更高。
(3)LDA(线性判别分析)
判别分析(Discriminant Analysis)是一种分类方法,它通过一个已知类别的“训练样本”来建立判别准则,并通过预测变量来为未知类别的数据进行分类。线性判别式分析(Linear Discriminant Analysis,简称为LDA)是其中一种,也是模式识别的经典算法,在1996年由Belhumeur引入模式识别和人工智能领域。
基本思想是将高维的模式样本投影到最佳鉴别矢量空间,以达到抽取分类信息和压缩特征空间维数的效果。投影后保证模式样本在新的子空间有最大的类间距离和最小的类内距离,即模式在该空间中有最佳的可分离性。
PCA与LDA相比有以下不同:

  • 出发思想不同。PCA主要是从特征的协方差角度,去找到比较好的投影方式,即选择样本点投影具有最大方差的方向;而LDA则更多地考虑了分类标签信息,寻求投影后不同类别之间数据点距离更大化以及同一类别数据点距离最小化,即选择分类性能最好的方向。
  • 学习模式不同。PCA属于无监督式学习,因此大多场景下只作为数据处理过程的一部分,需要与其他算法结合使用,例如与聚类、判别分析、回归分析等组合使用;LDA是一种监督式学习方法,本身除了可以降维外,还可以进行预测应用,因此既可以组合其他模型一起使用,也可以独立使用。
  • 降维后可用维度数量不同。LDA降维后最多可生成C-1维子空间(分类标签数-1),因此LDA与原始维度数量无关,只有数据标签分类数量有关;而PCA最多有n维度可用,即最大可以选择全部可用维度。

从直接可视化的角度,以二维数据降维为例,PCA和LDA的区别如图3-3所示。

image.png

图3-3左侧是PCA的降维思想,它所做的只是将整组数据整体映射到最方便表示这组数据的坐标轴上,映射时没有利用任何数据内部的分类信息。因此,虽然PCA后的数据在表示上更加方便(降低了维数并能最大限度地保持原有信息),但在分类上也许会变得更加困难;图3-3右侧是LDA的降维思想,可以看到,LDA充分利用了数据的分类信息,将两组数据映射到了另外一个坐标轴上,使得数据更易区分了(在低维上就可以区分,减少了运算量)。
线性判别分析LDA算法由于其简单有效性在多个领域都得到了广泛应用,是目前机器学习、数据挖掘领域经典且热门的一个算法。但是算法本身仍然存在一些局限性。

  • 当样本数量远小于样本的特征维数时,样本与样本之间的距离变大使得距离度量失效,使LDA算法中的类内、类间离散度矩阵奇异,不能得到最优的投影方向,在人脸识别领域中表现得尤为突出。
  • LDA不适合对非高斯分布的样本进行降维。
  • LDA在样本分类信息依赖方差而不是均值时,效果不好。
  • LDA可能过度拟合数据。

LDA是一个经典的机器学习算法,它是判别分析中的线性分类器,在很多应用情况下会面临数据稀疏的问题,尤其是在面部识别的场景。
数据的维度很可能大于数据的样本量,甚至可能呈几倍的差异。此时,LDA的预测准确率会表现较差,当维度数/样本量达到4倍时,准确率会只有50%左右,解决方法之一是对LDA算法进行收缩,Python的sklearn中的LDA算法支持这一收缩规则。默认情况下,solver的值被设定为“svd”,这在大数据量下的表现很好,但不支持收缩规则;当面临数据稀疏时,我们需要使用“lsqr”或“eigen”。另外,与之配合的shrinkage参数需要设置成“auto”,以便于算法自动调整收缩值。当然也可以自己凭借经验将值设定在0~1之间(越大收缩越厉害:0时不收缩,1时意味着对角线方差矩阵将被用作协方差矩阵值的估计),效果如图3-4所示。

image.png

(4)ICA(独立成分分析)
传统的降维方法,包括PCA、LDA等都是以观测数据点呈高斯分布模型为基本假设前提的,在已经先验经验知道观测数据集为非高斯分布模型的前提下,PCA和LDA的降维效果并不好。而ICA将适用于非高斯分析数据集,它是主成分分析(PCA)和因子分析(Factor Analysis)的一种有效扩展。
独立成分分析(Independent Component Analysis,简称ICA)是一种利用统计原理进行计算的方法,它是一个线性变换,这个变换把数据或信号分离成统计独立的非高斯的信号源的线性组合。
独立成分分析的最重要的假设就是信号源统计独立,并且这个假设在大多数盲信号分离(blind signal separation)的情况中符合实际情况;即使当该假设不满足时,仍然可以用独立成分分析来把观察信号统计独立化,从而进一步分析数据的特性。

image.png

ICA应用前提很简单:数据信号源是独立的且数据非高斯分布(或者信号源中最多只有一个成分是高斯分布)。另外观测信号源的数目不能少于源信号数目(为了方便一般要求二者相等即可)。如图3-5所示,原始观测信号源有3种独立信号源混合(正弦、方形和锯齿波形),通过ICA可以较好地分离出3种信号源。


image.png

独立成分分析法最初是用来解决“鸡尾酒会”的问题,ICA基于信号高阶统计特性的分析方法,经ICA分解出的各信号成分(或者叫分量)之间是相互独立的。正是因为这一特点,ICA在信号处理领域受到了广泛的关注。除了经典的盲源分离外,它的应用领域还包括以下这些:

  • 图像识别,去除噪音信息。
  • 语言识别,分离音源并去除噪音(如去除噪音,只保留输入语音)。
  • 通信、生物医学信号处理,从这些信号中单独区分某些信号(如区分胎儿和孕妇的心电信号)。
  • 故障诊断,去除非自然信息。
  • 特征提取和降维。
  • 自然信息处理,如地震声音分离。

3.3.4 基于特征组合的降维

基于特征的组合降维,实际上是将输入特征与目标预测变量做拟合的过程,它将输入特征经过运算,并得出能对目标变量做出很好解释(预测性)的复合特征,这些特征不是原有的单一特征,而是经过组合和变换后的新特征。从这一点来讲,原理类似于特征转换,更准确地来讲是类似于特征转换中的LDA(有监督式的机器学习)。
经过特征组合后形成的新特征,具有以下优点:

  • 在一定程度上解决了单一特征的离散和稀疏的问题,新组合特征对目标变量的解释能力增加。
  • 降低原有特征中噪音信息的干扰,使得模型鲁棒性更强。
  • 降低了模型的复杂性并提高模型效率,数据建模可基于能表达原有信息的组合特征展开。
  • 如果将原有特征与新组合特征共同加入到训练集中,能有效兼顾全局特征(原有单一特征表达的信息)和个性化特征(新组合特征所表达的信息),在很多场景下能有效提高准确率。

特征的组合方法有以下多种形式:

  • 基于单一特征离散化后的组合。这种方式下先将连续型特征离散化,然后基于离散化后的特征组合成新的特征。常见的RFM模型就是其中一种,这种方式先将R、F、M分别离散化,然后做加权或直接组合,生成新的RFM等分。有关该方法的更多信息,会在5.7节中介绍。
  • 基于单一特征的运算后的组合。这种方式下,对单一列基于不同条件下获得的数据记录做求和、均值、最大值、最小值、中位数、分位数、标准差、偏度、峰度等计算,从而获得新的特征。
  • 基于多个特征的运算后的组合。这种方式下,将对多个单一特征直接做复合计算,而计算一般都是基于数值型特征的,常见方式包括加、减、乘、除、取余、对数、正弦、余弦等操作,从而形成新的特征。
  • 基于模型的特征最优组合。这种方式下,特征间的组合将不再是简单的数学运算,而是基于输入特征与目标变量,在特定的优化函数的前提下做模型迭代计算,以达到满足模型最优的解。常见的方式包括:基于多项式的特征组合、基于GBDT的特征组合、基于基因工程的特征组合(这3种方法会在3.3.5节的代码实操中具体介绍)。

但是,特征组合的方法,很多时候其实并不能减少特征的数量,反而可能会增加特征。因此从严格意义上,特征组合不属于降维的过程,而是特征工程中与降维并行的模块。有关特征组合后的数量控制,会在下面代码实操中具体介绍。

image.png

在实际工作中,我们可以选择人工复杂特征+简单模型的思路,也可以选择简单特征+复杂模型的思路,在操作得当的前提下二者都会有较好的效果表现。
本节介绍的特征选择、转换和组合其实都是在做人工选择复杂特征的过程,实施的还是第1种思路。现在,在深度学习和神经网络大行其道的时代,很多公司也在探索不做复杂特征工程,直接将简单处理的数据放到神经网络或其他复杂模型中,模型会自动从中提取特征并进行计算。在这方面最典型的应用领域是图像识别。

3.3.5 代码实操:Python数据降维

本示例中,将分别使用sklearn的DecisionTreeClassifier来判断变量重要性并选择变量,通过PCA进行维度转换。数据源文件data1.txt和data5.txt位于“附件-chapter3”中。
第1部分导入库。
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn import feature_selection
from sklearn.svm import SVC
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.preprocessing import PolynomialFeatures as plf
from sklearn.ensemble import GradientBoostingClassifier as GBDT
from gplearn.genetic import SymbolicTransformer
from sklearn import datasets
本节用到的库较多,除了我们常用的Numpy外,还包括sklearn以及gplearn。其中gplearn是我们本书中新用到的库,该库可通过!pip3 install gplearn直接安装。各个库的主要作用如下。

  • Numpy:基本的数据读取和预处理。
  • DecisionTreeClassifier:决策树分类器,用于结合SelectFromModel提取特征。
  • SVC:支持向量机分类器,用于结合RFE提取特征。
  • PolynomialFeatures:从多项式模型中提取特征。
  • GradientBoostingClassifier:使用默认的GBDT的方法提取组合特征。
  • SymbolicTransformer:从遗传基因中的符号方法中提取组合特征。

第2部分导入数据文件。
data = np.loadtxt('data1.txt') # 读取文本数据文件
x, y = data[:, :-1], data[:, -1] # 获得输入的x和目标变量y
print(x[:3]) # 打印输出x的前3条记录
该部分的数据文件为具有分类特征的数据集。通过Numpy的loadtxt方法读取数据文件得到矩阵,然后对矩阵进行切割,得到要输入变量集x和目标变量y。输出x的前3条记录如下:
[[ 1.88622997 1.31785876 -0.16480621 0.56536882 -1.11934542 -0.53218995
-0.6843102 1.24149827 1.00579225 0.45485041]
[ 0.45016257 0.67080853 -1.16571355 1.16653938 3.27605586 -0.87270624
-0.31067627 -0.94946505 -0.33194209 -2.94399437]
[ 0.48158666 0.33524676 0.72210929 -2.01794519 -0.4255258 -0.98050463
1.57086924 1.46919579 -1.68387822 1.44933243]]
该记录可用于与下面特征选择的记录做对比。
第3部分基于sklearn的feature_selection做特征选择。
该部分用到了SelectPercentile、VarianceThreshold、RFE、SelectFromModel四种方法。
使用SelectPercentile选择特征:
selector_1 = feature_selection.SelectPercentile(percentile=30)
sel_features1 = selector_1.fit_transform(x, y) # 训练并转换数据
print(sel_features1.shape) # 打印形状
print(sel_features1[:3]) # 打印前3条记录
该部分先构建了一个选择器模型对象selector_1,设置选择总体30%的特征。然后基于selector_1做训练和转换,最后输出转换后的形状和数据。结果如下:
(1000, 3)
[[-1.11934542 -0.6843102 0.45485041]
[ 3.27605586 -0.31067627 -2.94399437]
[-0.4255258 1.57086924 1.44933243]]
原数据中的x是一个(1000, 10)的数据集,经过处理后的特征为(1000, 3)。对比第2部分输出的原始记录,原始列索引为4/6/9的3个特征被保留下来。
使用VarianceThreshold选择特征:
selector_2 = feature_selection.VarianceThreshold(1)
sel_features2 = selector_2.fit_transform(x) # 训练并转换数据
print(sel_features2.shape) # 打印形状
print(sel_features2[:3]) # 打印前3条记录
该过程的实现与上面的步骤类似,仅在初始化VarianceThreshold时设置了阈值为1,即方差高于1的特征才能保留下来。上述代码输出结果如下:
(1000, 7)
[[ 1.31785876 0.56536882 -1.11934542 -0.53218995 1.24149827 1.00579225
0.45485041]
[ 0.67080853 1.16653938 3.27605586 -0.87270624 -0.94946505 -0.33194209
-2.94399437]
[ 0.33524676 -2.01794519 -0.4255258 -0.98050463 1.46919579 -1.68387822
1.44933243]]
原始特征中有7个特征符合条件,保留了列索引值为1/3/4/5/7/8/9的特征。
使用RFE选择特征:
model_svc = SVC(kernel="linear")
selector_3 = feature_selection.RFE(model_svc, 3)
sel_features3 = selector_3.fit_transform(x, y) # 训练并转换数据
print(sel_features3.shape) # 打印形状
print(sel_features3[:3]) # 打印前3条记录
在上述过程中,相对于前两种方法,增加了一个指定模型的过程。由于我们的数据带有分类标志,因此我们设置SVC(支持向量机分类器)为基础模型,并设置线性内核。在selector_3的设置中,我们指定保留3个得分最高的特征。最终输出结果如下:
(1000, 3)
[[-1.11934542 -0.6843102 0.45485041]
[ 3.27605586 -0.31067627 -2.94399437]
[-0.4255258 1.57086924 1.44933243]]
上述结果得出最终的特征集保留了3个特征,原始列索引为4/6/9的3个特征被保留下来,该结果与使用SelectPercentile得到的结果一致,但SelectPercentile的默认函数为f_classif。
使用SelectFromModel选择特征:
model_tree = DecisionTreeClassifier(random_state=0) # 建立分类决策树模型对象
selector_4 = feature_selection.SelectFromModel(model_tree)
sel_features4 = selector_4.fit_transform(x, y) # 训练并转换数据
print(sel_features4.shape) # 打印形状
print(sel_features4[:3]) # 打印前3条记录
在该过程中,我们指定了基础模型器为决策树,但这里我们不设置特征重要性的过滤阈值,使用默认值None(原因是在不确定特征重要性的分布前提下,很难得到较好的分割阈值)。得到结果如下:
(1000, 3)
[[ 0.56536882 -1.11934542 -0.6843102 ]
[ 1.16653938 3.27605586 -0.31067627]
[-2.01794519 -0.4255258 1.57086924]]
上述结果显示了默认的参数下我们得到了3个显著性特征,原始列索引为3/4/6的特征被保留下来。

image.png
image.png

第4部分使用sklearn的LDA进行维度转换。
model_lda = LDA() # 建立LDA模型对象
model_lda.fit(x, y) # 将数据集输入模型并训练
convert_features = model_lda.transform(x) # 转换数据
print(convert_features.shape) # 打印形状
print(model_lda.explained_variance_ratio_) # 获得各成分解释方差占比
print(convert_features[:3]) # 打印前3条记录
有数据集中带有Label(分类标志),因此这里我们选择使用LDA方法做特征转换。上述的代码过程比较简单,输出结果如下:
(1000, 1)
[ 1.]
[[-1.08138044]
[ 2.89531581]
[-0.11256936]]
上述结果显示,转换后的特征只有1个特征,后面输出了方差比例及新特征的前3条数据。LDA转换后的特征已经不是原有特征,而是经过变换后新的特征。

image.png

LDA方法转换后的特征数量小于等于目标Label唯一值的个数。假如原始数据中的目标变量y的唯一值数量有n个,那么可指定的特征个数最多为n-1。我们通过set(y)可以看到,y的唯一值域只有2个值,因此LDA的成分最多只能有1个;如果y中有10个唯一值,那么最多可能指定为9。
LDA模型转换后的成分的解释方法比例为100%。如果有多个成分,那么这里会展示每个成分解释方差的比例。如果没有设置n_components,所有的成分都会被展示出来,并且其总和为1。
第5部分使用sklearn的GBDT方法组合特征。
model_gbdt = GBDT()
model_gbdt.fit(x, y)
conbine_features = model_gbdt.apply(x)[:, :, 0]
print(conbine_features.shape) # 打印形状
print(conbine_features[0]) # 打印第1条记录
在上述过程中,我们建立GBDT模型对象model_gbdt并做训练之后,使用了apply做特征提取,该方法返回的是叶子的索引,默认情况下,其形状是[n_samples, n_estimators, n_classes]。如果是二分类的情况下,n_classes为1。一般情况下,在sklearn中的算法和模型用到的输入数据都是二维空间矩阵(即shape = [n_samples, n_features]),因此这里只取n_samples和n_estimators。
上述代码输出的结果如下:
(1000, 100)
[ 4. 3. 4. 3. 5. 4. 5. 4. 4. 7. 7. 7. 7. 7. 7.

                            1. 7.
                            1. 12.
                            1. 9.
                            1. 14.
                            1. 14.
                    1. 14.]

其中的(1000, 100)是GBDT提取后的索引节点的形状,我们看到新的组合特征是100个;而通过下面的数据输出我们看到,第1条数据包含的100个特征。对于该组合特征,本质上已经根据最佳分裂节点做离散化处理了,因此后面可以接其他模型做处理。例如:接OneHotEncode+LR做分类建模。

image.png
image.png

在我们的模型中,都是采用默认的GBDT参数,参数的默认值如下:
GradientBoostingClassifier(criterion='friedman_mse', init=None,

learning_rate=0.1, loss='deviance', max_depth=3,
max_features=None, max_leaf_nodes=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, n_estimators=100,
presort='auto', random_state=None, subsample=1.0, verbose=0,
warm_start=False)

在当前数据集及默认参数下,我们获得了100个特征。读者可自行设置n_estimators的值,会发现,最终的特征数量等于设置的n_estimators(小树)的个数。
第6部分使用sklearn的PolynomialFeatures方法组合特征。
model_plf = plf(2)
plf_features = model_plf.fit_transform(x, y)
print(plf_features.shape) # 打印形状
print(plf_features[0]) # 打印第1条数据
在代码中,指定多项式的项数为2(degree的值为2),建立多项式对象model_plf后,我们直接使用fit_transform方法,然后输出转换后的特征形状和第1条数据。结果如下:
(1000, 66)
[ 1. 1.88622997 1.31785876 -0.16480621 0.56536882 -1.11934542
-0.53218995 -0.6843102 1.24149827 1.00579225 0.45485041 3.55786351
2.4857847 -0.31086242 1.06641562 -2.11134288 -1.00383263 -1.2907664
2.34175125 1.89715548 0.85795248 1.73675172 -0.21719131 0.74507626
-1.47513917 -0.70135119 -0.90182419 1.63611938 1.32549213 0.5994286
0.02716109 -0.0931763 0.18447508 0.08770821 0.11277857 -0.20460663
-0.16576081 -0.07496217 0.31964191 -0.632843 -0.30088361 -0.38688765
0.70190442 0.56864358 0.25715824 1.25293417 0.59570438 0.76597948
-1.3896654 -1.12582894 -0.50913473 0.28322614 0.36418301 -0.6607129
-0.53527252 -0.24206682 0.46828045 -0.84956993 -0.68827389 -0.31125878
1.54131796 1.24868934 0.564696 1.01161804 0.45748502 0.2068889 ]
上述输出了组合特征的个数是66个以及第1行组合特征结果。我们可以通过model_plf.get_feature_names()方法可以获得每个特征的名称,结果如下(为了节省版面,这里我将多行合并为一行,且只列出其中一部分名称,因此版式跟实际不一致):
['1', 'x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', 'x0^2', 'x0 x1', 'x0 x2', 'x0 x3', 'x0 x4', 'x0 x5', 'x0 x6', 'x0 x7', 'x0 x8', 'x0 x9', 'x1^2', 'x1 x2',...'x8 x9', 'x9^2']
由于我们指定的是二项式,通过特征名称可以看到其中有如下规律:

  • 第1列特征的值全为1(读者可通过plf_features[:,0]查看)。
  • 第2列到第10列为原始特征的值(读者可通过x[0]对比查看这10列值)。
  • 后面的列都是原始10个特征两两(包含自身)之间的乘积。

image.png

第7部分使用gplearn的genetic方法组合特征。
在本部分示例中,我们将使用一个新的方法来做特征组合应用。Gplearn的库主要有以下2个:

  • SymbolicRegressor(符号回归)是一种机器学习技术,旨在识别最能描述关系的基础数学表达式。它首先建立一个简单随机公式的数量来表示已知的自变量与其因变量目标之间的关系,以预测新数据。它可以利用遗传算法得到的公式,直接预测目标变量的值,因此属于回归应用的一种方法。
  • SymbolicTransformer(符号转换器)是一种监督式的特征处理技术,它首先建立一组简单的随机公式来表示关系,然后通过从群体中选择最适合的个体进行遗传操作,最终找出最适合彼此相关性最小的个体。本例就用了该方法,它是在特征工程过程中,非常有效的做特征转换和组合的方法。

本示例完整代码如下:
raw_data = datasets.load_boston() # 加载数据集
代码中,我们分别进行了如下操作:
第1行代码我们使用sklearn的demo数据集,该数据集为波士顿房价数据。

image.png
image.png

x, y = raw_data.data, raw_data.target # 分割形成x和y
第2行代码根据列索引将原始数据分割为x和y,用于下面的特征处理。需要注意的是,这里的y是一个数值型而非分类型字段。
print(x.shape) # 查看x的形状
print(x[0]) # 查看x的第1条数据
第3、第4行直接打印原始数据集x的形状和第1条数据,结果如下:
(506, 13)
[ 6.32000000e-03 1.80000000e+01 2.31000000e+00 0.00000000e+00
5.38000000e-01 6.57500000e+00 6.52000000e+01 4.09000000e+00
1.00000000e+00 2.96000000e+02 1.53000000e+01 3.96900000e+02
4.98000000e+00]
数据集x中有506条数据,13个特征。
第5行代码使用SymbolicTransformer方法建立方法组合对象model_symbolic。
model_symbolic = SymbolicTransformer(n_components=5, generations=18, function_set=('add', 'sub', 'mul', 'div', 'sqrt', 'log', 'abs', 'neg', 'inv','max', 'min'), max_samples=0.9, metric='pearson', random_state=0, n_jobs=2)
n_components设置组合后的特征为5个,该值设置的是组合后的特征数量,为整数型数值。该值要小于等于参数hall_of_fame的值(该参数本示例中未设置,默认值为100)。
generations设置了演变迭代的次数。
function_set设置的是特征组合函数,这些函数会对一个或多个数组做对应的计算操作,默认功能函数包括add、sub、mul、div。所有的功能函数对应的逻辑如下。

  • add:两个变量(或数组)相加求和,基于np.add计算。
  • sub:两个变量(或数组)相减求差,基于np.subtract计算。
  • mul:两个变量(或数组)相乘求积,基于np.multiply计算。
  • div:两个变量(或数组)相除求商,基于np.divide计算。
  • sqrt:特定变量(或数组)绝对值的平方根,基于np.sqrt计算。
  • log:特定变量(或数组)绝对值的平方根,基于np.log计算。
  • abs:特定变量(或数组)绝对值,基于np.abs计算。
  • neg:特定变量(或数组)相反数,基于np.negative计算。
  • inv:特定变量(或数组)的倒数,基于1/x计算。
  • max:特定变量(或数组)最大值,基于np.maximum计算。
  • min:特定变量(或数组)最小值,基于np.minimum计算。
  • sin:特定变量(或数组)正弦,基于np.sin计算。
  • cos:特定变量(或数组)余弦,基于np.cos计算。
  • tan:特定变量(或数组)正切,基于np.tan计算。

max_samples设置从x中每次抽取的样本的比例。
meric设置的是拟合指标,该指标可设置为pearson(默认)或spearman,当组合的特征后面使用树分类器(例如随机森林、GBDT等)时,建议设置为spearman;如果后面使用线性模型做拟合时,建议设置为pearson。
random_state用来控制每次随机时使用相同的初始化种子,避免由于初始随机的不同导致结果的差异,这在测试过程中经常用到。
n_jobs用来控制参与计算的CPU核心的个数,该参数在skleran的很多算法中都能用到,其值可根据计算机情况做设置,为了使用全部CPU资源,可直接设置为-1。
第6行代码对model_symbolic对象做fit训练。fit对象除了设置x和y之外,还可以通过设置sample_weight指定每个样本的权重。
model_symbolic.fit(x, y) # 训练数据
第7行代码对x做特征转换,返回一个形状为[n_samples(与原始x的输入记录数一致),n_components(自定义的组合对象数量)]的数组对象。
symbolic_features = model_symbolic.transform(x) # 转换数据
print(symbolic_features.shape) # 打印形状
print(symbolic_features[0]) # 打印第1条数据
最后两行打印形状和第1条数据。结果如下:
(506, 5)
[ 1.2132401 0.83252613 -1.84617541 0.11456174 0.48038727]
上述结果显示了数据集最后保留了5个组合特征。如果我们想要查看这些特定是通过什么公式组合生成的,可以直接使用print(model_symbolic),可输出如下结果:
[inv(sqrt(inv(sqrt(log(sqrt(inv(mul(X10, X12)))))))),
sqrt(inv(sqrt(sqrt(log(mul(X10, X12)))))),
inv(log(inv(sqrt(sqrt(sqrt(mul(X10, X12))))))),
inv(sqrt(mul(X10, X12))),
inv(sqrt(log(mul(X10, X12))))]
通过上述公式发现:生成的特征主要是基于第11和13个原始特征,并做复杂的公式组合生成的,这些公式的复杂性不在于公式本身,而在于其组合的过程略显复杂,公式间的组合值域会呈指数级增长。

image.png

另外,最后生成的组合特征并不一定是唯一的。在本节代码后面有一段注释的代码,代码中引用了data5.txt数据文件,使用跟上面的示例完全相同的设置和操作步骤,最后的输出会发现5个特征是完全相同的,这意味着这些特征都是重复的;并且无论n_components如何设置,都将得到相同的结果。
有关符号回归的gplearn还有很多知识,限于篇幅本书无法一一介绍,更多知识请读者查阅http://gplearn.readthedocs.io/en/stable/intro.html
在本节中,我们使用了多种方法做数据降维,其中,需要读者重点考虑的关键点如下:

  • 根据不同的场景选择最佳降维方法,该过程需要经验积累并需要做数据测试。
  • 将通过特征选择、转换以及组合后的复合特征与后续的算法模型相结合,尤其是找到与后续模型能搭配使用的方法。

代码实操小结:本节示例中,主要用了以下几个知识点。

  • 通过Numpy的loadtxt读取数据文件。
  • 使用sklearn的dataset读取demo数据集。
  • 对Numpy矩阵进行切片。
  • 使用sklearn的feature_selection中的多种方法做特征选择,主要包括SelectPercentile、VarianceThreshold、RFE、SelectFromModel。
  • 使用sklearn的LDA进行维度转换、降维。
  • 使用sklearn的GBDT方法、PolynomialFeatures方法组合特征。
  • 使用gplearn的genetic方法组合特征。

3.4 解决样本类别分布不均衡的问题

所谓的不均衡指的是不同类别的样本量差异非常大。样本类别分布不均衡主要出现在与分类相关的建模问题上。样本类别分布不均衡从数据规模上可以分为大数据分布不均衡和小数据分布不均衡两种。

  • 大数据分布不均衡;这种情况下整体数据规模大,只是其中的小样本类的占比较少。但是从每个特征的分布来看,小样本也覆盖了大部分或全部的特征。例如,在拥有1000万条记录的数据集中,其中占比50万条的少数分类样本便于属于这种情况。
  • 小数据分布不均衡;这种情况下整体数据规模小,并且占据少量样本比例的分类数量也少,这会导致特征分布的严重不均衡。例如,拥有1000条数据样本的数据集中,占有10条样本的分类,其特征无论如何拟合也无法实现完整特征值的覆盖,此时属于严重的数据样本分布不均衡。

样本分布不均衡将导致样本量少的分类所包含的特征过少,并很难从中提取规律。即使得到分类模型,也容易产生过度依赖于有限的数据样本而导致过拟合的问题。当模型应用到新的数据上时,模型的准确性和健壮性将很差。
样本分布不均衡主要在于不同类别间的样本比例差异。以笔者的工作经验看,如果不同分类间的样本量差异超过10倍就需要引起警觉,并应考虑处理该问题,超过20倍就一定要想法解决了。

3.4.1 哪些运营场景中容易出现样本不均衡

在数据化运营过程中,以下场景中会经常出现样本分布不均衡的问题:

  • 异常检测场景。大多数企业中的异常个案都是少量的,比如恶意刷单、黄牛订单、信用卡欺诈、电力窃电、设备故障等。这些数据样本所占的比例通常是整体样本中很少的一部分。以信用卡欺诈为例,刷实体信用卡欺诈的比例一般在0.1%以内。
  • 客户流失场景。大型企业的流失客户相对于整体客户通常是少量的,尤其对于具有垄断地位的行业巨擘,例如电信、石油、网络运营商等更是如此。
  • 罕见事件的分析。罕见事件与异常检测类似,都属于发生个案较少的情况;但不同点在于异常检测通常都有是预先定义好的规则和逻辑,并且大多数异常事件都对会企业运营造成负面影响,因此针对异常事件的检测和预防非常重要;但罕见事件则无法预判,并且也没有明显的积极和消极影响倾向。例如,由于某网络大V无意中转发了企业的一条趣味广告,导致用户流量明显提升便属于此类。
  • 发生低频率的事件。这种事件是预期或计划性事件,但是发生频率非常低。例如,每年一次的“双11”购物节一般都会产生较高的销售额,但放到全年来看,这一天的销售额占比很可能只有不到1%,尤其对于很少参与活动的公司而言,这种情况更加明显。这种就属于典型的低频率事件。

3.4.2 通过过抽样和欠抽样解决样本不均衡

抽样是解决样本分布不均衡相对简单且常用的方法,包括过抽样和欠抽样两种。
1)过抽样:又称上采样(over-sampling),其通过增加分类中少数类样本的数量来实现样本均衡,最直接的方法是简单复制少数类样本以形成多条记录。这种方法的缺点是,如果样本特征少则可能导致过拟合的问题。经过改进的过抽样方法会在少数类中加入随机噪声、干扰数据,或通过一定规则产生新的合成样本,例如SMOTE算法。
2)欠抽样:又称下采样(under-sampling),其通过减少分类中多数类样本的数量来实现样本均衡,最直接的方法是随机去掉一些多数类样本来减小多数类的规模。缺点是会丢失多数类样本中的一些重要信息。
总体上,过抽样和欠抽样更适合大数据分布不均衡的情况,尤其是过抽样方法,应用极为广泛。

3.4.3 通过正负样本的惩罚权重解决样本不均衡

通过正负样本的惩罚权重解决样本不均衡的问题的思想是:在算法实现过程中,对于分类中不同样本数量的类别分别赋予不同的权重(一般思路分类中的小样本量类别权重高,大样本量类别权重低),然后进行计算和建模。
使用这种方法时,不需要对样本本身做额外处理,只需在算法模型的参数中进行相应设置即可。很多模型和算法中都有基于类别参数的调整设置,以scikit-learn中的SVM为例,通过在class_weight : {dict, 'balanced'}中针对不同类别来手动指定权重,如果使用其默认的方法balanced,那么SVM会将权重设置为与不同类别样本数量呈反比的权重来进行自动均衡处理,计算公式如下:
n_samples / (n_classes * np.bincount(y))
如果算法本身支持,这种思路是更加简单且高效的方法。

3.4.4 通过组合/集成方法解决样本不均衡

组合/集成方法指的是在每次生成训练集时使用所有分类中的小样本量,同时从分类中的大样本量中随机抽取数据来与小样本量合并构成训练集,这样反复多次会得到很多训练集和训练模型。最后在应用时,使用组合方法(例如投票、加权投票等)产生分类预测结果。
例如,数据集中的正、负例的样本分别为100条和10000条,比例为1:100。此时可以将负例样本(类别中的大量样本集)随机分为100份(当然也可以分更多),每份100条数据;然后每次形成训练集时使用所有的正样本(100条)和随机抽取的负样本(100条)形成新的数据集。如此反复可以得到100个训练集和对应的训练模型。
这种解决问题的思路类似于随机森林。在随机森林中,虽然每个小决策树的分类能力很弱,但是通过大量的“小树”组合形成的“森林”具有良好的模型预测能力。
如果计算资源充足,并且对于模型的时效性要求不高,这种方法比较合适。

3.4.5 通过特征选择解决样本不均衡

上述几种方法都是基于数据行的操作,通过多种途径可使不同类别的样本数据行记录均衡。除此以外,还可以考虑使用或辅助于基于列的特征选择方法。
一般情况下,样本不均衡也会导致特征分布不均衡,但如果小类别样本量具有一定的规模,那么意味着其特征值的分布较为均匀,可通过选择具有显著型的特征配合参与解决样本不均衡问题,也能在一定程度上提高模型效果。
上述几种方法的思路都是基于分类问题解决的。实际上,这种从大规模数据中寻找罕见数据的情况,也可以使用非监督式的学习方法,例如使用One-class SVM进行异常检测。分类是监督式方法,前期是基于带有标签(Label)的数据进行分类预测;而采用非监督式方法,则是使用除了标签以外的其他特征进行模型拟合,这样也能得到异常数据记录。所以,要解决异常检测类的问题,先是考虑整体思路,然后再考虑方法模型。

3.4.6 代码实操:Python处理样本不均衡

本示例中,我们主要使用一个新的专门用于不平衡数据处理的Python包imbalanced-learn。除此之外,我们还会使用sklearn的SVM在算法中通过调整类别权重来处理样本不均衡问题。本示例使用的数据源文件data2.txt位于“附件-chapter3”中。
第1部分导入库。
import pandas as pd
from imblearn.over_sampling import SMOTE # 过抽样处理库SMOTE
from imblearn.under_sampling import RandomUnderSampler # 欠抽样处理库
RandomUnderSampler
from sklearn.svm import SVC # SVM中的分类算法SVC本示例中用到了第三方库imbalanced-learn实现主要的样本不均衡处理,而Pandas的引入主要用于解释和说明不同处理方法得到的结果集样本的分布情况,sklearn.svm中的SVC主要用于说明SVM如何在算法中自动调整分类权重。
第2部分导入数据文件。
df = pd.read_table('data2.txt', sep=' ', names=['col1', 'col2', 'col3', 'col4', 'col5', 'label'])    # 读取数据文件
x, y = df.iloc[:, :-1],df.iloc[:, -1] # 切片,得到输入x,标签y
groupby_data_orgianl = df.groupby('label').count() # 对label做分类汇总
print(groupby_data_orgianl) # 打印输出原始数据集样本分类分布
该过程中使用Pandas的read_table读取本地文件,为了更好地区别不同的列,通过names指定列名;对数据框做切片分割得到输入的x和目标变量y;通过Pandas的groupby()方法按照label列做分类汇总,汇总方式是使用count()函数计数。输入原始数据集样本分类分布如下:

   col1  col2  col3  col4  col5

label
0.0 942 942 942 942 942
1.0 58 58 58 58 58
输出结果显示,原始数据集中,正样本(label为1)的数量仅有58个,占总样本量的5.8%,属于严重不均衡分布。
第3部分使用SMOTE方法进行过抽样处理。
model_smote = SMOTE() # 建立SMOTE模型对象
x_smote_resampled, y_smote_resampled = model_smote.fit_sample(x, y)

     \# 输入数据并做过抽样处理

x_smote_resampled = pd.DataFrame(x_smote_resampled, columns=['col1', 'col2', 'col3', 'col4', 'col5']) # 将数据转换为数据框并命名列名
y_smote_resampled = pd.DataFrame(y_smote_resampled, columns=['label'])

    \# 将数据转换为数据框并命名列名

smote_resampled = pd.concat([x_smote_resampled, y_smote_resampled], axis=1)

    \# 按列合并数据框

groupby_data_smote = smote_resampled.groupby('label').count() # 对label做分类汇总
print(groupby_data_smote) # 打印输出经过SMOTE处理后的数据集样本分类分布
该过程中首先建立SMOTE模型对象,并直接应用fit_sample对数据进行过抽样处理,如果要获得有关SMOTE的具体参数信息,可先使用fit(x,y)方法获得模型信息,并得到模型不同参数和属性;从fit_sample方法分别得到对x和y过抽样处理后的数据集,将两份数据集转换为数据框然后合并为一个整体数据框;最后通过Pandas提供的groupby()方法按照label类做分类汇总,汇总方式是使用count()函数计数。经过SMOTE处理后的数据集样本分类分布如下:

   col1  col2  col3  col4  col5

label
0.0 942 942 942 942 942
1.0 942 942 942 942 942
通过对比第2部分代码段的原始数据集返回结果发现,该结果中的正样本(label为1)的数量增加,并与负样本数量相同,均为942条,数据分类样本得到平衡。
第4部分使用RandomUnderSampler方法进行欠抽样处理。
model_RandomUnderSampler = RandomUnderSampler() # 建立RandomUnderSampler模型对象
x_RandomUnderSampler_resampled, y_RandomUnderSampler_resampled = model_RandomUnderSampler.fit_sample( x, y) # 输入数据并做欠抽样处理
x_RandomUnderSampler_resampled = pd.DataFrame(x_RandomUnderSampler_resampled, columns=['col1', 'col2', 'col3', 'col4', 'col5']) # 将数据转换为数据框并命名列名
y_RandomUnderSampler_resampled = pd.DataFrame(y_RandomUnderSampler_resampled, columns=['label']) # 将数据转换为数据框并命名列名
RandomUnderSampler_resampled = pd.concat([x_RandomUnderSampler_resampled, y_RandomUnderSampler_resampled], axis=1) # 按列合并数据框
groupby_data_RandomUnderSampler = RandomUnderSampler_resampled.groupby('label').count() # 对label做分类汇总
print(groupby_data_RandomUnderSampler) # 打印输出经过RandomUnderSa-mpler处理后的数据集样本分类分布
该过程与第3部分步骤完全相同,在此略过各模块介绍,用途都已在代码备注中注明。经过RandomUnderSampler处理后的数据集样本分类分布如下:

   col1  col2  col3  col4  col5

label
0.0 58 58 58 58 58
1.0 58 58 58 58 58
对比第2部分代码段的原始数据集返回的结果,该结果中的负样本(label为0)的数量减少,并跟正样本相同,均为58条,样本得到平衡。
第5部分使用SVM的权重调节处理不均衡样本。
model_svm = SVC(class_weight='balanced',gamma='scale') # 创建SVC模型对象并指定类别权重
model_svm.fit(x, y) # 输入x和y并训练模型
该过程主要通过SVC中的class_weight参数和值的设置来处理样本权重,该参数可设置为字典、None或字符串balanced 3种模式。

  • 字典:通过手动指定的不同类别的权重,例如{1:10,0:1}。
  • None:代表类别的权重相同。
  • balanced:代表算法将自动调整与输入数据中的类频率成反比的权重,具体公式如下。
    n_samples /(n_classes * np.bincount(y))
  • 程序示例中使用了该方法。

经过设置后,算法自动处理样本分类权重,无须用户做其他处理。要对新的数据集做预测,只需要调用model_svm模型对象的predict方法即可。
上述过程中,主要需要考虑的关键点是:如何针对不同的具体场景选择最合适的样本均衡解决方案,选择过程中既要考虑每个类别样本的分布情况以及总样本情况,又要考虑后续数据建模算法的适应性,以及整个数据模型计算的数据时效性。
代码实操小结:本节示例中,主要用了以下几个知识点。

  • 通过Pandas的read_table方法读取文本数据文件,并指定列名。
  • 对数据框做切片处理。
  • 通过Pandas提供的groupby()方法配合count()做分类汇总。
  • 使用imblearn.over_sampling中的SMOTE做过抽样处理。
  • 使用imblearn.under_sampling中的RandomUnderSampler做欠抽样处理。
  • 使用sklearn.svm 中的SVC自动调整算法设置不同类别的权重。

image.png

3.5 数据化运营要抽样还是全量数据

抽样是从整体样本中通过一定的方法选择一部分样本。抽样是数据处理的基本步骤之一,也是科学实验、质量检验、社会调查普遍采用的一种经济有效的工作和研究方法。

3.5.1 什么时候需要抽样

抽样工作在数据获取较少或处理大量数据比较困难的时期非常流行,这主要有以下几方面原因:

  • 数据计算资源不足。计算机软硬件的限制是导致抽样产生的基本原因之一,尤其是在数据密集的生物、科学工程等领域,不抽样往往无法对海量数据进行计算。
  • 数据采集限制。很多时候抽样从数据采集端便已经开始,例如做社会调查必须采用抽样方法进行研究,因为根本无法对所有人群做调查。
  • 时效性要求。抽样带来的是以局部反映全局的思路,如果方法正确,可以以极小的数据计算量来实现对整体数据的统计分析,在时效性上会大大增强。

如果存在上述条件限制或有类似强制性要求,那么抽样工作仍然必不可少。但是在当前数据化运营的大背景下,数据计算资源充足、数据采集端可以采集更多的数据并且可以通过多种方式满足时效性的要求,抽样工作是否就没有必要了?其实不是的,即使上述限制条件都满足,还有很多场景依然需要通过抽样方法来解决具体问题。

  • 通过抽样来实现快速的概念验证。数据工作中可能会包括创新性或常识性项目,对于这类项目进行快速验证、迭代和交付结论往往是概念验证的关键,通过抽样方法带来的不仅是计算效率的提升,还有前期数据准备、数据预处理、算法实现等各个方面的开发,以及服务器、硬件的配套方案的部署等内容的可行性、简单化和可操作性。
  • 通过抽样来解决样本不均衡问题。在3.4节中我们提到,通过欠抽样、过抽样以及组合/集成的方法解决不均衡的问题,这个过程就用到了抽样方法。
  • 无法实现对全部样本覆盖的数据化运营场景。典型场景包括市场研究、客户线下调研分析、产品品质检验、用户电话满意度调查等,在这些场景下无法实现对所有样本的采集、分析、处理和建模。
  • 定性分析的工作需要。在定性分析工作中,通常不需要定量分析时的完整假设、精确数据和复杂统计分析过程,更多的是采用访问、观察和文献法收集资料并通过主观理解和定性分析找到问题答案,该过程中主要依靠人自身的能力而非密集的计算机能力来完成研究工作。如果不使用抽样方法,那么定性分析将很难完成。

3.5.2 如何进行抽样

抽样方法从整体上分为非概率抽样和概率抽样两种。非概率抽样不是按照等概率的原则进行抽样,而是根据人类的主观经验和状态进行判断;概率抽样则是以数学概率论为基础,按照随机的原则进行抽样。本节以下内容介绍的抽样方法属于概率抽样。
(1)简单随机抽样
该抽样方法是按等概率原则直接从总样本中抽取n个样本,这种随机抽样方法简单、易于操作,但是它并不能保证样本能完美代表总体。这种抽样的基本前提是所有样本个体都是等概率分布的,但真实情况却是多数样本都不是或无法判断是否是等概率分布的。在简单随机抽样中,得到的结果是不重复的样本集,还可以使用有放回的简单随机抽样,这样得到的样本集中会存在重复数据。该方法适用于个体分布均匀的场景。
(2)等距抽样
等距抽样是先将总体中的每个个体按顺序编号,然后计算出抽样间隔,再按照固定抽样间隔抽取个体。这种操作方法易于理解、简便易行,但当总体样本的分布呈现明显的分布规律时容易产生偏差,例如增减趋势、周期性规律等。该方法适用于个体分布均匀或呈现明显的均匀分布规律,无明显趋势或周期性规律的数据。
(3)分层抽样
分层抽样是先将所有个体样本按照某种特征划分为几个类别,然后从每个类别中使用随机抽样或等距抽样的方法选择个体组成样本。这种操作方法能明显降低抽样误差,并且便于针对不同类别的数据样本进行单独研究,因此是一种较好的实现方法。该方法适用于带有分类逻辑的属性、标签等特征的数据。
(4)整群抽样
整群抽样是先将所有样本分为几个小群体集,然后随机抽样几个小群体集来代表总体。这种操作方法与之前的3种方法的差异点在于该方法抽取的是小群体集,而不是每个数据个体本身。该方法虽然简单易行,但是样本的分布受限于小群体集的划分,抽样误差较大。这种方法适用于小群体集的特征差异比较小的数据,并且对划分小群体集有更高要求。

3.5.3 抽样需要注意的几个问题

1.数据抽样要能反映运营背景
数据能正确反映运营背景,这看起来非常简单,但实际上需要数据工作者对于运营环节和流程非常熟悉才有可能实现。以下是常见的抽样不能反映运营背景的情况。

  • 数据时效性问题:使用过时的数据(例如1年前的数据)来分析现在的运营状态。
  • 缺少关键因素数据:没有将运营分析涉及的主要因素所产生的数据放到抽样数据中,导致无法根据主要因素产生有效结论,模型效果差,例如抽样中没有覆盖大型促销活动带来的销售增长。
  • 不具备业务随机性:有意/无意多抽取或覆盖特定数据场景,使得数据明显趋向于特定分布规律,例如在做社会调查时使用北京市的抽样数据来代表全国。
  • 没有考虑业务增长性:在成长型公司中,公司的发展不都是呈现线性趋势的,很多时候会呈现指数趋势。
    这时需要根据这种趋势来使业务满足不同增长阶段的分析需求,而不只是集中于增长爆发区间。
  • 没有考虑数据来源的多样性:只选择某一来源的数据做抽样,使得数据的分布受限于数据源。例如在做各分公司的销售分析时,仅将北方大区的数据纳入其中做抽样,而忽视了其他大区的数据,其结果必然有所偏颇。
  • 业务数据可行性问题:很多时候,由于受到经费、权限、职责等方面的限制,在数据抽样方面无法按照数据工作要求来执行,此时要根据运营实际情况调整。这点往往被很多数据工作者忽视。

2.数据抽样要能满足数据分析和建模需求
数据抽样必须兼顾后续的其他数据处理工作,尤其是分析和建模需求。这时需要注意以下几个方面的问题。
(1)抽样样本量的问题
对于大多数数据分析建模而言,数据规模越大,模型拟合结果越准确。但到底如何定义数据量的大小,笔者根据不同类型的数据应用总结为以下几个维度:

  • 以时间为维度分布的,至少包含一个能满足预测的完整业务周期。例如,做月度销售预测的,至少包含12个月的数据;做日销售预测的,至少包含30天的数据,如果一天中包含特定周期,则需要重复多个周期。同时,时间性特征的要充分考虑季节性、波动性、节假日等特殊规律,这些都要尽量包含在抽样数据中。
  • 做预测(包含分类和回归)分析建模的,需要考虑特征数量和特征值域(非数值型)的分布,通常数据记录数要同时是特征数量和特征值域的100倍以上。例如数据集有5个特征,假如每个特征有2个值域,那么数据记录数需要至少在1000(100×5×2)条以上。
  • 做关联规则分析建模的,根据关联前后项的数量(每个前项或后项可包含多个要关联的主体,例如品牌+商品+价格关联),每个主体需要至少1000条数据。例如只做单品销售关联,那么单品的销售记录需要在1000条以上;如果要同时做单品+品牌的关联,那么需要至少2000条数据。
  • 对于异常检测类分析建模的,无论是监督式还是非监督式建模,由于异常数据本来就是小概率分布的,因此异常数据记录一般越多越好。

以上的数据记录数不是固定的,在实际工作时,如果没有特定时间要求,笔者一般会选择一个适中的样本量做分析,此时应综合考虑特征数、特征值域分布数、模型算法适应性、建模需求等;如果是面向机器计算的工作项目,一般会选择尽量多的数据参与计算,而有关算法实时性和效率的问题会让技术和运维人员配合实现,例如提高服务器配置、扩大分布式集群规模、优化底层程序代码、使用实时计算的引擎和机制等。
(2)抽样样本在不同类别中的分布问题
做分类分析建模问题时,不同类别下的数据样本需要均衡分布,有关数据样本均衡分布的更多话题参见3.4节。
抽样样本能准确代表全部整体特征:

  • 非数值型的特征值域(例如各值频数相对比例、值域范围等)分布需要与总体一致。
  • 数值型特征的数据分布区间和各个统计量(如均值、方差、偏度等)需要与整体数据分布区间一致。
  • 缺失值、异常值、重复值等特殊数据的分布要与整体数据分布一致。

异常检测类数据的处理:

  • 对于异常检测类的应用要包含全部异常样本。对于异常检测类的分析建模,本来异常数据就非常稀少,因此抽样时要优先将异常数据包含进去。
  • 对于需要去除非业务因素的数据异常,如果有类别特征需要与类别特征分布一致;如果没有类别特征,属于非监督式的学习,则需要与整体分布一致。

3.5.4 代码实操:Python数据抽样

本示例中,将使用random包以及自定义代码实现抽样处理。数据源文件data2.txt、data3.txt和data4.txt位于“附件-chapter3”中。
整个示例代码分为5部分。
第1部分导入需要的库。
import random # 导入标准库
import numpy as np # 导入第三方库
这里用到了Python内置标准库random以及第三方库Numpy,前者用于做随机抽样,后者用于读取文件并做数据切片使用。
第2部分实现了简单随机抽样。
data = np.loadtxt('data3.txt') # 导入普通数据文件
data_sample = data[random.sample([i for i in range(len(data))], 2000)]

image.png

第3部分实现了等距抽样。
data = np.loadtxt('data3.txt') # 导入普通数据文件
sample_count = 2000 # 指定抽样数量
record_count = data.shape[0] # 获取最大样本量
width = record_count / sample_count # 计算抽样间距
data_sample = [] # 初始化空白列表,用来存放抽样结果数据
i = 0 # 自增计数以得到对应索引值
while len(data_sample) <= sample_count and i * width <= record_count - 1:

    # 当样本量小于等于指定抽样数量并且矩阵索引在有效范围内时
data_sample.append(data[int(i \* width)])    # 新增样本
i += 1                     # 自增长

print(data_sample[:2]) # 打印输出前2条数据
print(len(data_sample)) # 打印输出样本数量
首先使用Numpy的loadtxt方法读取数据文件;然后指定抽样样本量为2000,并通过读取原始数据的形状找到最大样本量边界,这可以用来作为循环的终止条件之一;接着通过最大样本量除抽样样本量得到抽样间距;建立一个空列表用于存储最终抽样结果数据,通过一个变量i做循环增长并用来做索引递增,然后进入抽样条件判断过程。

  • 当样本量小于等于指定抽样数量并且矩阵索引在有效范围内时做处理,这里需要注意的是索引从0开始,因此最大数量值减去1得到循环边界,否则会报索引溢出错误。
  • 通过列表的append方法不断追加通过间距得到的新增样本,在本节后面的方法中还会提到列表追加的extend方法,前者用于每次追加1个元素,后者用于批量追加多个元素。
  • i += 1指的是每次循环都增加1,可以写成i = i + 1。
  • 最后打印输出前2条数据和抽样样本量。

返回结果如下:
[array([-3.08057779, 8.09020329, 2.02732982, 2.92353937, -6.06318211]), array([-2.11984871, 7.74916701, 5.7318711 , 4.75148273, -5.68598747])]
2000
第4部分实现了分层抽样。
data2 = np.loadtxt('data2.txt')    # 导入带有分层逻辑的数据
each_sample_count = 200    # 定义每个分层的抽样数量
label_data_unique = np.unique(data2[:, -1])    # 定义分层值域
sample_data = []    # 定义空列表,用于存放最终抽样数据
sample_dict = {}    # 定义空字典,用来显示各分层样本数量
for label_data in label_data_unique:    # 遍历每个分层标签

sample_list = []                     # 定义空列表,用于存放临时分层数据
for data_tmp in data2:                 # 读取每条数据
    if data_tmp[-1] == label_data:             # 如果数据最后一列等于标签
        sample_list.append(data_tmp)        # 将数据加入分层数据中
each_sample_data = random.sample(sample_list, each_sample_count)
    # 对每层数据都随机抽样

sample_data.extend(each_sample_data)    # 将抽样数据追加到总体样本集

sample_dict[label_data] = len(each_sample_data)  # 样本集统计结果

print(sample_dict)    # 打印输出样本集统计结果
首先使用Numpy的loadtxt方法导入带有分层逻辑的数据。在该示例中,读取的数据文件中包含了分类标签,放在最后一列。该列分类标签用于做分层抽样的标识。接着通过unique方法获取分层(分类标签)的值域,用于后续做循环处理。然后分别定义了用于存放临时分层数据、最终抽样数据、显示各分层样本数量的空列表和空字典。
下面进入正式的主循环过程,实现分层抽样:

  • 遍历每个分层标签,用来做数据的分层划分,数据一共分为2类标签(0和1)。
  • 读取每条数据并判断数据的分层标签是否与分层标签相同,如果是则将数据加入各分层数据列表中。
  • 当每个分层标签处理完成后会得到该分层标签下的所有数据,此时使用Python内置的random库的sample方法进行抽样。由于抽样结果是一个列表,因此这里使用extend(而不是append)批量追加到最终抽样数据列表中。然后将每个分层标签得到的样本数量,通过len方法对列表长度进行统计,并打印输出各个分层对应的样本数量。结果是每个分层都按照指定数量抽取样本,输出如下:

{0.0: 200, 1.0: 200}
第5部分实现了整群抽样。
data3 = np.loadtxt('data4.txt')   # 导入已经划分好整群的数据集
label_data_unique = np.unique(data3[:, -1])   # 定义整群标签值域
print(label_data_unique)   # 打印输出所有整群标签
sample_label = random.sample(set(label_data_unique), 2) # 随机抽取2个整群
sample_data = []   # 定义空列表,用来存储最终抽样数据
for each_label in sample_label:   # 遍历每个整群标签值域

for data_tmp in data3:                # 遍历每个样本
    if data_tmp[-1] == each_label:            # 判断样本是否属于抽样整群
        sample_data.append(data_tmp)          # 样本添加到最终抽样数据集

print(sample_label)   # 打印输出样本整群标签
print(len(sample_data))   # 打印输出总抽样数据记录条数
首先使用Numpy的loadtxt方法导入已经划分好整群的数据集。在该示例中,读取的数据文件中的最后一列存放了不同整群的标识,整群一共被划分为4个群组,标识分别为0、1、2、3。接着通过unique方法获取整群标签的值域,用于基于整群的抽样。打印输出结果如下:
[ 0. 1. 2. 3.]
然后使用Random的sample方法从整群标签中进行抽样,这里定义抽取2个整群。最后将所有属于抽取到的整群下的数据进行读取和追加,并得到最终样本集,打印输出样本集的整群标签和总样本数量,结果如下:
[3.0, 1.0]
502

image.png

上述过程中,需要考虑的关键点是:如何根据不同的数据特点、建模需求、业务背景综合考虑抽样方法,得到最适合的结果
代码实操小结:本节示例中,主要用了几个知识点:

  • 使用Numpy的loadtxt方法读取数据文件。
  • 使用内置标准库Random库中的sample方法做数据抽样。
  • 对列表通过索引做截取、通过len方法做长度统计、通过append和extend做追加等操作。
  • 字典赋值操作。
  • 使用Numpy的unique方法获得唯一值。
  • 通过for和while循环,遍历一个可迭代的对象。
  • if条件语句的使用,尤其是单条件和多条件判断。

3.6 解决运营数据的共线性问题

所谓共线性(又称多重共线性)问题指的是输入的自变量之间存在较高的线性相关度。共线性问题会导致回归模型的稳定性和准确性大大降低,另外,过多无关的维度参与计算也会浪费计算资源和时间。
共线性问题是否常见取决于具体业务场景。常见的具有明显的共线性的维度或变量如下:

  • 访问量和页面浏览量。
  • 页面浏览量和访问时间。
  • 订单量和销售额。
  • 订单量和转化率。
  • 促销费用和销售额。
  • 网络展示广告费用和访客数。

导致出现变量间共线性的原因可能包括:

  • 数据样本不够,导致共线性存在偶然性,这其实反映了缺少数据对于数据建模的影响,共线性仅仅是影响的一部分。
  • 多个变量都基于时间,有共同或相反的演变趋势,例如,春节期间的网络销售量和销售额都相对于正常时间有下降趋势。
  • 多个变量间存在一定的推移关系,但总体上变量间的趋势一致,只是发生的时间点不一致,例如品牌广告费用和销售额之间,通常是品牌广告先进行大范围的曝光和信息推送,经过一定时间传播之后,销售额才能爆发出来。
  • 多个变量间存在近似线性的关系。例如,用y代表访客数,用x代表展示广告费用,那么二者的关系很可能是y=2*x+b,即每投放1元钱,可以带来大概2~3个访客。

3.6.1 如何检验共线性

共线性一般通过容忍度、方差膨胀因子、特征值这几个特征数据来做检验。

  • 容忍度(Tolerance):容忍度是每个自变量作为因变量对其他自变量进行回归建模时得到的残差比例,大小用1减得到的决定系数来表示。容忍度的值介于0和1之间,值越小,说明这个自变量与其他自变量间存在共线性问题的可能性越大。
  • 方差膨胀因子(Variance Inflation Factor,VIF):VIF是容忍度的倒数,值越大则共线性问题越明显,通常以10作为判断边界。VIF<10,不存在多重共线性;10≤VIF<100,存在较强的多重共线性;VIF≥100,存在严重多重共线性。
  • 特征值(Eigenvalue):该方法实际上就是对自变量进行主成分分析,如果多个维度的特征值等于0,则可能有比较严重的共线性。

除此以外,还可以使用相关系数辅助判断,当相关系数R>0.8时,表示可能存在较强的相关性。有关相关性的更多话题会在3.8节中探讨。通常这些方法得到的结果都是相关的,即满足其中某个特征后,其他特征也基本满足。因此可以通过多种方法共同验证。

3.6.2 解决共线性的5种常用方法

解决共线性的5种常用方法如下。
(1)增大样本量
通过增加样本量来消除由于数据量不足而出现的偶然共线性现象,在可行的前提下这种方法是需要优先考虑的。但即使增加了样本量,可能也无法解决共线性问题,原因是很可能变量间确实存在这个问题。
(2)岭回归法
岭回归(Ridge Regression)分析是一种专用于共线性问题的有偏估计回归方法,实质上是一种改良的最小二乘估计法。它通过放弃最小二乘法的无偏性,以损失部分信息、降低精度为代价来获得更实际和可靠性更强的回归系数。因此岭回归在存在较强共线性的回归应用中较为常用。
(3)逐步回归法
逐步回归法(Stepwise Regression)是每次引入一个自变量并进行统计检验,然后逐步引入其他变量,同时对所有变量的回归系数进行检验。如果原来引入的变量由于后面变量的引入而变得不再显著,那么就将其剔除,逐步得到最优回归方程。
(4)主成分回归(Principal Components Regression)
通过主成分分析,将原始参与建模的变量转换为少数几个主成分,每个主成分是原变量的线性组合,然后基于主成分做回归分析,这样也可以在不丢失重要数据特征的前提下避开共线性问题。
(5)人工去除
直接结合人工经验,对参与回归模型计算的自变量进行删减,也是一个较为常用的方法。但这种方法需要操作者对于业务、模型和数据都有相对深入的理解,才有可能做出正确的操作。从专业角度分析,如果缺少上述3个方面中的任何一个,那么人工去除的方式都有可能产生偏差,导致结果不准确。

image.png

3.6.3 代码实操:Python处理共线性问题

本示例中,将通过sklearn进行共线性处理。源文件data5.txt位于“附件-chapter3”中。
在自动化工作(尤其是以Python为代表的智能化数据工作)中,通常不会通过人工的方法参与算法结果观察、调优、选择等。同样,在解决共线性的方法中,通过程序的方式自动选择或规避是最佳解决方法。在本示例中,将主要使用岭回归和主成分回归解决共线性问题。
完整代码如下。
第1部分导入相关库。
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.decomposition import PCA
from sklearn.linear_model import LinearRegression
示例中用到了Numpy、sklearn中的Ridge(岭回归)、PCA(主成分分析)和LinearRegres-sion(普通线性回归)。由于本书用到的Python库中没有直接集成主成分回归方法,因此这里将通过PCA+LinearRegression的形式组合实现。
第2部分导入数据。
data = np.loadtxt('data5.txt', delimiter='t') # 读取数据文件
x,y = data[:, :-1],data[:, -1] # 切分自变量和预测变量
使用Numpy的loadtxt方法读取数据文件,数据文件以tab分隔,共1000条数据,有9个自变量和1个因变量。因变量处于最后一列,使用Numpy矩阵进行数据切分。
第3部分使用岭回归算法进行回归分析。
model_ridge = Ridge(alpha=1.0) # 建立岭回归模型对象
model_ridge.fit(x, y) # 输入x、y训练模型
print(model_ridge.coef_) # 打印输出自变量的系数
print(model_ridge.intercept_) # 打印输出截距
该过程中,先建立岭回归模型对象,指定alpha值为0.1,接着通过fit方法将x和y分别输入模型做训练,然后打印输出回归方程中自变量的系数和截距。结果如下:
[ 8.50164360e+01 -1.18330186e-03 9.80792921e-04 -8.54201056e-04
2.10489064e-05 2.20180449e-04 -3.00990875e-06 -9.30084240e-06
-2.84498824e-08]
-7443.98652868
上述结果中包含了各个输入变量的系数以及截距,假设因变量为y,自变量为x1、x2、…、x9,那么该方程可以写成:
y = 85.016436×x1+(-0.00118330186)×x2+0.000980792921×x3+(-0.000854201056)×x4+0.0000210489064×x5+0.000220180449×x6+(-0.00000300990875)×x7+(-0.0000093008424)× x8+(-0.0000000284498824)×x9-7443.98652868

image.png

第4部分使用主成分回归进行回归分析。
model_pca = PCA() # 建立PCA模型对象
data_pca = model_pca.fit_transform(x) # 对x进行主成分分析
ratio_cumsm = np.cumsum(model_pca.explained_variance_ratio_)

    # 得到所有主成分方差占比的累积数据

print(ratio_cumsm) # 打印输出所有主成分方差占比累积
rule_index = np.where(ratio_cumsm > 0.8) # 获取方差占比超过0.8的所有索引值
min_index = rule_index0 # 获取最小索引值
data_pca_result = data_pca[:, :min_index + 1] # 根据最小索引值提取主成分
model_liner = LinearRegression() # 建立回归模型对象
model_liner.fit(data_pca_result, y) # 输入主成分数据和预测变量y并训练模型
print(model_liner.coef_) # 打印输出自变量的系数
print(model_liner.intercept_) # 打印输出截距
该过程主要分为以下步骤:

  • 先建立PCA模型对象,然后使用fit_transform方法直接进行主成分转换。这里没有指定具体主成分的数量,原因是在得到主成分的方差贡献率之前,无法知晓到底选择多少个合适,后续会通过指定阈值的方式自动实现成分选择。
  • 通过PCA模型对象的explained_variance_ratio_得到各个主成分的方差贡献率(方差占比),然后使用Numpy的cumsum方法计算累积占比。得到如下输出结果:

[ 0.9028 0.98570494 0.99957412 0.99995908 0.99999562 0.99999939
0.99999999 1. 1. ]
直观上分析,前2个主成分基本已经完全代表所有成分参与模型计算。但是如果自动化实现,我们不可能每次先中断程序并打印出结果,再使用人工判断。因此我们需要一个阈值,当方差贡献达到阈值时,就自动选择前n个主成分作为最终转换维度。

  • 通过使用Numpy的where方法找到主成分方差占比累积>0.8的值索引,通常0.8就已经能代表大部分的数据特征,本示例较为特殊,第1个主成分就已经非常明显。该方法返回的是一个元组,从元组中得到最小的索引值(越往后累积占比越大,索引值也越大,因此第1个符合条件的索引就是我们要取出的最后一个主成分)。
  • 在获得最小索引值之后,根据最小索引值提取主成分。之所以切片时在索引列值上加1,是由于默认索引值的右侧是不包含的,例如[0:1]只取出第0列,而不是我们需要的0和1两列。
  • 接着进入线性回归的环节。与岭回归的步骤类似,建立回归模型对象并输入x和y做模型训练,最后打印输出回归方程中自变量的系数和截距。结果如下:

[ 1.26262171e-05]
1058.52726
上述结果中包含了输入变量的系数以及截距,假设因变量为y,满足自变量方差占比大于0.9的主成分为第1个主成分。假设其为x1,那么该方程可以写成:
y = 0.0000126262171×x1+1058.52726

image.png

3.7 有关相关性分析的混沌

相关性分析是指对多个具备相关关系的变量进行分析,从而衡量变量间的相关程度或密切程度。相关性可以应用到所有数据的分析过程中,任何事物之间都是存在一定的联系。相关性用R(相关系数)表示,R的取值范围是[-1, 1]。

3.7.1 相关和因果是一回事吗

相关性不等于因果。用x1和x2作为两个变量进行解释,相关意味着x1和x2是逻辑上的并列相关关系,而因果联系可以解释为因为x1所以x2(或因为x2所以x1)的逻辑关系,二者是完全不同的。
用一个运营示例来说明二者的关系:做商品促销活动时,通常都会以较低的价格进行销售,以此来实现较高的商品销量;随着商品销售的提升,也给线下物流配送体系带来了更大的压力,在该过程中通常会导致商品破损量的增加。
本案例中,商品低价与破损量增加并不是因果关系,即不能说因为商品价格低所以商品破损量增加;二者的真实关系是都是基于促销这个大背景下,低价和破损量都是基于促销产生的。
相关性的真实价值不是用来分析“为什么”的,而是通过相关性来描述无法解释的问题背后真正成因的方法。相关性的真正的价值是能知道“是什么”,即无论通过何种因素对结果产生影响,最终出现的规律就是二者会一起增加或降低等。
仍然是上面的案例,通过相关性分析我们可以知道,商品价格低和破损量增加是相伴发生的,这意味着当价格低的时候(通常是做销售活动,也有可能产品质量问题、物流配送问题、包装问题等),我们就想到破损量可能也会增加。但是到底由什么导致的破损量增加,是无法通过相关性来得到的。

3.7.2 相关系数低就是不相关吗

R(相关系数)低就是不相关吗?其实不是。
1)R的取值可以为负,R= -0.8代表的相关性要高于R=0.5。负相关只是意味着两个变量的增长趋势相反,因此需要看R的绝对值来判断相关性的强弱。
2)即使R的绝对值低,也不一定说明变量间的相关性低,原因是相关性衡量的仅仅是变量间的线性相关关系,变量间除了线性关系外,还包括指数关系、多项式关系、幂关系等,这些“非线性相关”的相关性不在R(相关性分析)的衡量范围之内。

3.7.3 代码实操:Python相关性分析

本示例中,将使用Numpy进行相关性分析。源文件data5.txt位于“附件-chapter3”中。
import numpy as np # 导入库
data = np.loadtxt('data5.txt', delimiter='t') # 读取数据文件
x = data[:, :-1] # 切分自变量
correlation_matrix = np.corrcoef(x, rowvar=0) # 相关性分析
print(correlation_matrix.round(2)) # 打印输出相关性结果
示例中实现过程如下:
1)先导入Numpy库;
2)使用Numpy的loadtxt方法读取数据文件,数据文件以tab分隔;
3)矩阵切片,切分出自变量用来做相关性分析;
4)使用Numpy的corrcoef方法做相关性分析,通过参数rowvar = 0控制对列做分析;
5)打印输出相关性矩阵,使用round方法保留2位小数。结果如下:
[[ 1. -0.04 0.27 -0.05 0.21 -0.05 0.19 -0.03 -0.02]
[-0.04 1. -0.01 0.73 -0.01 0.62 0. 0.48 0.51]
[ 0.27 -0.01 1. -0.01 0.72 0. 0.65 0.01 0.02]
[-0.05 0.73 -0.01 1. 0.01 0.88 0.01 0.7 0.72]
[ 0.21 -0.01 0.72 0.01 1. 0.02 0.91 0.03 0.03]
[-0.05 0.62 0. 0.88 0.02 1. 0.03 0.83 0.82]
[ 0.19 0. 0.65 0.01 0.91 0.03 1. 0.03 0.03]
[-0.03 0.48 0.01 0.7 0.03 0.83 0.03 1. 0.71]
[-0.02 0.51 0.02 0.72 0.03 0.82 0.03 0.71 1. ]]
相关性矩阵的左侧和顶部都是相对的变量,从左到右、从上到下依次是列1到列9。从结果看出:

  • 第5列和第7列相关性最高,系数达到0.91。
  • 第4列和第6列相关性较高,系数达到0.88。
  • 第8列和第6列相关性较高,系数达到0.83。

为了更好地展示相关性结果,我们可以配合Matplotlib展示图像。代码如下:
fig = plt.figure() # 调用figure创建一个绘图对象
ax = fig.add_subplot(111) # 设置1个子网格并添加子网格对象
hot_img = ax.matshow(np.abs(correlation_matrix), vmin=0, vmax=1)

    # 绘制热力图,值域从0到1

fig.colorbar(hot_img) # 为热力图生成颜色渐变条
ticks = np.arange(0, 9, 1) # 生成0~9,步长为1
ax.set_xticks(ticks) # 生成x轴刻度
ax.set_yticks(ticks) # 设置y轴刻度
names = ['x' + str(i) for i in range(x.shape[1])] # 生成坐标轴标签文字
ax.set_xticklabels(names) # 生成x轴标签
ax.set_yticklabels(names) # 生成y轴标签
上述代码的功能都已经在注释中注明。有以下几点需要注意:

  • 由于相关性结果中看的是绝对值的大小,因此需要对correlation_matrix做取绝对值操作,其对应的值域会变为[0, 1]。
  • 原始数据中由于没有列标题,因此这里使用列表推导式生成从x0到x8代表原始的9个特征。

展示结果如图3-7所示。

image.png

从图像中配合颜色可以看出:颜色越亮(彩色颜色为越黄),则相关性结果越高,因此从左上角到右下角呈现一条黄色斜线;而颜色较亮的第5列和第7列、第4列和第6列及第8列和第6列分别对应x4和x6、x3和x5、x7和x5。
上述过程中,主要需要考虑的关键点是:如何理解相关性和因果关系的差异,以及如何应用相关性。相关性分析除了可以用来分析不同变量间的相关伴生关系以外,也可以用来做多重共线性检验。有关共线性的问题请参照3.7节。
代码实操小结:本节示例中,主要用了如下几个知识点。

  • 通过Numpy的loadtxt方法读取文本数据文件,并指定分隔符;
  • 对Numpy矩阵做切块处理;
  • 使用Numpy中的corrcoef做相关性分析;
  • 使用round方法保留2位小数;
  • 使用np.abs取绝对值;
  • 使用列表推导式生成新列表;
  • 使用Matplotlib的热力图配合相关性结果做图像展示。

3.8 标准化,让运营数据落入相同的范围

数据标准化是一个常用的数据预处理操作,目的是处理不同规模和量纲的数据,使其缩放到相同的数据区间和范围,以减少规模、特征、分布差异等对模型的影响。除了用作模型计算,标准化后的数据还具有直接计算并生成复合指标的意义,是加权指标的必要步骤。

3.8.1 实现中心化和正态分布的Z-Score

Z-Score标准化是基于原始数据的均值和标准差进行的标准化,假设原转换的数据为x,新数据为x′,那么x'=(x-mean)/std,其中mean和std为x所在列的均值和标准差。
这种方法适合大多数类型的数据,也是很多工具的默认标准化方法。标准化之后的数据是以0为均值,方差为1的正态分布。但是Z-Score方法是一种中心化方法,会改变原有数据的分布结构,不适合对稀疏数据做处理。

image.png

3.8.2 实现归一化的Max-Min

Max-Min标准化方法是对原始数据进行线性变换,假设原转换的数据为x,新数据为x′,那么x'=(x-min)/(max-min),其中min和max为x所在列的最小值和最大值。
这种标准化方法的应用非常广泛,得到的数据会完全落入[0, 1]区间内(Z-Score则没有类似区间)。这种方法能使数据归一化而落到一定的区间内,同时还能较好地保持原有数据结构。

3.8.3 用于稀疏数据的MaxAbs

最大值绝对值标准化(MaxAbs)即根据最大值的绝对值进行标准化。假设原转换的数据为x,新数据为x′,那么x'=x/|max|,其中max为x所在列的最大值。
MaxAbs方法与Max-Min用法类似,也是将数据落入一定区间,但该方法的数据区间为[-1, 1]。MaxAbs也具有不破坏原有数据分布结构的特点,因此也可以用于稀疏数据、稀疏的CSR或CSC矩阵。

image.png

3.8.4 针对离群点的RobustScaler

某些情况下,假如数据集中有离群点,我们可以使用Z-Score进行标准化,但是标准化后的数据并不理想,因为异常点的特征往往在标准化之后容易失去离群特征。此时,可以使用RobustScaler针对离群点做标准化处理,该方法对数据中心化和数据的缩放鲁棒性有更强的参数控制。

3.8.5 代码实操:Python数据标准化处理

本示例中,将使用Numpy、sklearn进行标准化相关处理。源文件data6.txt位于“附件-chapter3”中。完整代码如下。
第1部分导入库。
import numpy as np
from sklearn import preprocessing
import matplotlib.pyplot as plt
示例中用到了Numpy做数据读取,sklearn中的preprocessing模块做标准化处理,matp-lotlib.pyplot模块做可视化图形展示。
第2部分使用Numpy的loadt方法导入数据文件,文件以tab分隔。
data = np.loadtxt('data6.txt', delimiter='t') # 读取数据
第3部分进行Z-Score标准化。
zscore_scaler = preprocessing.StandardScaler() # 建立StandardScaler对象
data_scale_1 = zscore_scaler.fit_transform(data) # StandardScaler标准化处理
通过preprocessing.StandardScaler建立模型,然后应用fit_transform方法进行转换。除了StandardScaler类外,还可直接使用preprocessing.scale(X)做标准化处理,二者的应用差别是preprocessing.StandardScaler方法既可以满足一次性的标准化处理,又能将转换数据集的特征保存下来,分别通过fit和transform对多个数据集(例如测试集和训练集)做相同规则的转换。该方法针对训练集、测试集和应用集分别应用时非常有效。
第4部分进行Max-Min标准化。
minmax_scaler = preprocessing.MinMaxScaler() # 建立MinMaxScaler模型对象
data_scale_2 = minmax_scaler.fit_transform(data) # MinMaxScaler标准化处理
通过preprocessing. MinMaxScaler建立模型,然后应用fit_transform方法进行转换。其中的fit_transform方法可以分别使用fit然后使用transform做多数据集的应用。该方法也可以使用没有面向对象API的等效函数sklearn.preprocessing.minmax_scale。

image.png

第5部分进行MaxAbsScaler标准化。
maxabsscaler_scaler = preprocessing.MaxAbsScaler() # 建立MaxAbsScaler对象
data_scale_3 = maxabsscaler_scaler.fit_transform(data) # MaxAbsScaler标准化处理
通过preprocessing. MaxAbsScaler建立模型,然后应用fit_transform方法进行转换。也可以使用没有面向对象API的等效函数sklearn.preprocessing. maxabs_scale。
第6部分进行RobustScaler标准化。
robustscalerr_scaler = preprocessing.RobustScaler() # 建立RobustScaler标准化对象
data_scale_4 = robustscalerr_scaler.fit_transform(data) # RobustScaler标准化处理
通过preprocessing. RobustScaler建立模型,然后应用fit_transform方法进行转换。也可以使用没有面向对象API的等效函数sklearn.preprocessing. robust_scale。
第7部分展示原始数据和4种标准化的结果。
data_list = [data, data_scale_1, data_scale_2, data_scale_3, data_scale_4]

    # 创建数据集列表

color_list = ['black', 'green', 'blue', 'yellow', 'red'] # 创建颜色列表
merker_list = ['o', ',', '+', 's', 'p'] # 创建样式列表
title_list = ['source data', 'zscore_scaler', 'minmax_scaler', 'maxabsscaler_scaler', 'robustscalerr_scaler']   # 创建标题列表
plt.figure(figsize=(16, 3))
for i, data_single in enumerate(data_list): # 循环得到索引和每个数值

plt.subplot(1, 5, i + 1)                      # 确定子网格
plt.scatter(data_single[:, :-1], data_single[:, -1], s=10, marker=merker_list[i], c=color_list[i])                    # 自网格展示散点图
plt.title(title_list[i])                         # 设置自网格标题

plt.suptitle("raw data and standardized data") # 设置总标题
在该部分功能中,将原始数据以及通过上述4种方法得到的结果数据统一对比展示。主要步骤如下:

  • 先创建4个列表,用于存储原始数据和4个标准化后的数据、颜色、样式、标题等。
  • 通过for循环结合enumerate将索引值和4份数据循环读出;通过plt.figure来设置图像尺寸,通过plt.subplot为不同的数据设置不同的网格,该网格为1行5列;在每个网格中通过plt.scatter画出散点图,并通过索引将列表中对应的值作为参数传给scatter;然后通过title方法为每个子网格设置标题。
  • 最后设置整个图像的总标题并展示图像,如图3-8所示。

image.png

上述过程中,主要需要考虑的关键点是如何根据不同的数据分布特征和应用选择合适的标准化方式。具体来说包含如下几点:

  • 如果要做中心化处理,并且对数据分布有正态需求,那么使用Z-Score方法;
  • 如果要进行0-1标准化或将要指定标准化后的数据分布范围,那么使用Max-Min标准化或MaxAbs标准化方式是比较好的选择,尤其是前者;
  • 如果要对稀疏数据进行处理,Max-Min标准化或MaxAbs标准化仍然是理想方法;
  • 如果要最大限度地保留数据集中的异常,那么使用RobustScaler方法更佳。

代码实操小结:本节示例中,主要用了如下几个知识点。

  • 通过Numpy的loadtxt方法读取文本数据文件,并指定分隔符;
  • 使用sklearn.preprocessing的StandardScaler方法做Z-Score标准化处理;
  • 使用sklearn.preprocessing的MinMaxScaler方法做Max-Min标准化处理;
  • 使用sklearn.preprocessing的MaxAbsScaler方法做最大值绝对值标准化处理;
  • 使用sklearn.preprocessing的RobustScaler方法做针对异常数据的处理;
  • 通过matplotlib.pyplot画图,并在一幅大图中设置使用subplot设置多个子网格,使用scatter画出散点图并设置颜色、样式、大小,使用title和suptitle设置子网格和整体图像标题展示图像。

3.9 离散化,对运营数据做逻辑分层

所谓离散化,就是把无限空间中有限的个体映射到有限的空间中。数据离散化操作大多是针对连续数据进行的,处理之后的数据值域分布将从连续属性变为离散属性,这种属性一般包含2个或2个以上的值域。离散化处理的必要性如下:

  • 节约计算资源,提高计算效率。
  • 算法模型(尤其是分类模型)的计算需要。虽然很多模型,例如决策树可以支持输入连续型数据,但是决策树本身会先将连续型数据转化为离散型数据,因此离散化转换是一个必要步骤。
  • 增强模型的稳定性和准确度。数据离散化之后,处于异常状态的数据不会明显地突出异常特征,而是会被划分为一个子集中的一部分,因此异常数据对模型的影响会大大降低,尤其是基于距离计算的模型(例如K均值、协同过滤等)效果明显。
  • 特定数据处理和分析的必要步骤,尤其在图像处理方面应用广泛。大多数图像做特征检测(以及其他基于特征的分析)时,都需要先将图像做二值化处理,二值化也是离散化的一种。
  • 模型结果应用和部署的需要。如果原始数据的值域分布过多,或值域划分不符合业务逻辑,那么模型结果将很难被业务理解并应用。

image.png

3.9.1 针对时间数据的离散化

针对时间数据的离散化主要用于以时间为主要特征的数据集中和粒度转换,离散化处理后将分散的时间特征转换为更高层次的时间特征。
在带有时间的数据集中,时间可能作为行记录的序列,也可能作为列(维度)记录数据特征。常见的针对时间数据的离散化操作有以下两类:

  • 针对一天中的时间离散化。一般是将时间戳转换为秒、分钟、小时或上下午。
  • 针对日粒度以上数据的离散化。一般是将日期转化为周数、周几、月、工作日或休息日、季度、年等。

针对时间数据的离散化可以将细粒度的时间序列数据离散化为粗粒度的3类数据:

  • 离散化为分类数据,例如上午、下午。
  • 离散化为顺序数据,例如周一、周二、周三等。
  • 离散化为数值型数据,例如一年有52个周,周数是数值型数据。

3.9.2 针对多值离散数据的离散化

针对多值离散数据的离散化指的是要进行离散化处理的数据本身不是数值型数据,而是分类或顺序数据。
例如,用户收入变量的值原来可能划分为10个区间,根据新的建模需求,只需要划分为4个区间,那么就需要对原来的10个区间进行合并。
多值离散数据要进行离散化还有可能是划分的逻辑有问题,需要重新划分。这种问题通常都是由于业务逻辑的变更,导致在原始数据中存在不同历史数据下的不同值域定义。
例如,用户活跃度变量的值,原来分为高价值、中价值和低价值3个类别。根据业务发展的需要,新的用户活跃度变量的值定义为:高价值、中价值、低价值和负价值。此时需要对不同类别的数据进行统一规则的离散化处理。

3.9.3 针对连续数据的离散化

针对连续数据的离散化是主要的离散化应用,在分类或关联分析中应用尤其广泛。这些算法的结果以类别或属性标识为基础,而非数值标记。例如,分类规则的典型结果逻辑如下:
如果 变量1 = 值1 并且 变量2 = 值2
那么 目标变量(T)
连续数据的离散化结果可以分为两类:一类是将连续数据划分为特定区间的集合,例如{(0,10], (10, 20], (20, 50], (50, 100]};一类是将连续数据划分为特定类,例如类1、类2、类3。
常见实现针对连续数据化离散化的方法如下。

  • 分位数法:使用四分位、五分位、十分位等分位数进行离散化处理,这种方法简单易行。
  • 距离区间法:可使用等距区间或自定义区间的方式进行离散化,这种操作更加灵活且能满足自定义需求。另外该方法(尤其是等距区间)可以较好地保持数据原有的分布。
  • 频率区间法:将数据按照不同数据的频率分布进行排序,然后按照等频率或指定频率离散化,这种方法会把数据变换成均匀分布。好处是各区间的观察值是相同的,不足是已经改变了原有数据的分布状态。
  • 聚类法:例如使用K均值将样本集分为多个离散化的簇。
  • 卡方:通过使用基于卡方的离散化方法,找出数据的最佳临近区间并合并,形成较大的区间。

3.9.4 针对连续数据的二值化

在很多场景下,我们可能需要将变量特征进行二值化操作:每个数据点跟阈值比较,大于阈值设置为某一固定值(例如1),小于阈值设置为某一固定值(例如0),然后得到一个只拥有两个值域的二值化数据集。

image.png

二值化应用的前提是数据集中所有的属性值所代表的含义相同或类似,例如读取图像所获得的数据集是颜色值的集合(具体颜色模式取决于读取图像时的模式设置,例如灰度、RGB等),因此每一个数据点都代表颜色,此时可对整体数据集做二值化处理。某些情况下,也可能只针对特定列做二值化,这样不同列的属性虽然不同,但同一列内产生的二值化结果却仍然具有比较和分类意义。

3.9.5 代码实操:Python数据离散化处理

本示例中,将使用Pandas、sklearn进行离散化相关处理。数据源文件data7.txt位于“附件-chapter3”中。
示例代码分为6个部分。
第1部分导入库。
import pandas as pd
from sklearn.cluster import KMeans
from sklearn import preprocessing
代码中用到了Pandas和sklearn。前者主要用来做文件读取、切块、时间处理、关联合并和部分离散化操作;后者主要用来做二值化和聚类建模离散化。
第2部分使用Pandas的read_table方法读取数据文件,并指定列名。
df = pd.read_table('data7.txt', names=['id', 'amount', 'income', 'datetime', 'age']) # 读取数据文件
print(df.head(5))   # 打印输出前5条数据
数据集为100行5列的数据框,包含id、amount、income、datetime和age五个字段。原始数据前5条数据如下:

  id  amount  income             datetime    age

0 15093 1390 10.40 2017-04-30 19:24:13 0-10
1 15062 4024 4.68 2017-04-27 22:44:59 70-80
2 15028 6359 3.84 2017-04-27 10:07:55 40-50
3 15012 7759 3.70 2017-04-04 07:28:18 30-40
4 15021 331 4.25 2017-04-08 11:14:00 70-80
第3部分针对时间数据的离散化。
df['datetime'] = list(map(pd.to_datetime,df['datetime'])) # 将时间转换为datetime格式
df['datetime'] = [i.weekday() for i in df['datetime']] # 离散化为周几
print(df.head(5)) # 打印输出前5条数据
该过程中,先直接使用map配合pandas的to_datetime方法将字符串转换为datetime格式,由于Python 3的map方法返回的是一个map对象,需要用list方法做转换才能正确处理;然后列表推导式配合weekday方法获取周几,新获取的周几的数据直接替换原数据框的时间戳。最后打印输出离散化后的结果。

  id  amount  income datetime    age

0 15093 1390 10.40 6 0-10
1 15062 4024 4.68 3 70-80
2 15028 6359 3.84 3 40-50
3 15012 7759 3.70 1 30-40
4 15021 331 4.25 5 70-80
从结果中看到,datetime列的值由原来的日期时间格式,转换为由0~6组成的周几值,0代表周一,6代表周日。
第4部分针对多值离散数据的离散化。
map_df = pd.DataFrame(

[['0-10', '0-40'], ['10-20', '0-40'], ['20-30', '0-40'], ['30-40', '0-40'], ['40-50', '40-80'], ['50-60', '40-80'], ['60-70', '40-80'], ['70-80', '40-80'], ['80-90', '>80'], ['>90', '>80']],
columns=['age', 'age2'])     # 定义一个要转换的新区间

df_tmp = df.merge(map_df, left_on='age', right_on='age', how='inner')

                # 数据框关联匹配

df = df_tmp.drop('age', 1) # 丢弃名为age的列
print(df.head(5)) # 打印输出前5条数据
该过程中先通过dp.Dataframe定义一个新的转换区间,用来将原数据映射到新区间来;然后通过merge方法将原数据框和新定义的数据框进行关联,关联的两个key(left_on和right_on)分别是age和age2,关联模式为inner(内关联);接着我们通过drop方法去除原始数据框中的age列,只保留新的转换后的区间。得到如下结果:

  id  amount  income datetime  age2

0 15093 1390 10.40 6 0-40
1 15064 7952 4.40 0 0-40
2 15080 503 5.72 5 0-40
3 15068 1668 3.19 5 0-40
4 15019 6710 3.20 0 0-40
上述返回结果中,age2列是转换后新的列,原来的分类区间已经被映射到新的类别区间。
第5部分针对连续数据的离散化。该部分包含3种常用方法。
方法1:自定义分箱区间实现离散化。
bins = [0, 200, 1000, 5000, 10000] # 自定义区间边界
df['amount1'] = pd.cut(df['amount'], bins) # 使用边界做离散化
print(df.head(5)) # 打印输出前5条数据
首先定义一个自定义区间边界列表,用来将数据做划分;然后使用Pandas的cut方法做离散化;并将结果生成一个新的名为amount1的列追加到原数据框。打印输出结果如下:

  id  amount  income datetime  age2        amount1

0 15093 1390 10.40 6 0-40 (1000, 5000]
1 15064 7952 4.40 0 0-40 (5000, 10000]
2 15080 503 5.72 5 0-40 (200, 1000]
3 15068 1668 3.19 5 0-40 (1000, 5000]
4 15019 6710 3.20 0 0-40 (5000, 10000]
上述返回结果中,amount1列是转换后产生的列,每行对应的区间左侧是区间开始值(不包含),右侧是区间结束值(包含)。除了显示区间外,cut方法还可以通过自定义labels(值为列表的形式,用来表示不同分类区间的标签)用标签代替上述区间,例如labels = ['bad','medium','good','awesome'],那么显示在amount1里面的数据就是对应lables里面的字符串。这里没有将原始amount列删除,原因是下面的方法中还会用到该列原始数据。
方法2:使用聚类法实现离散化。
data = df['amount'] # 获取要聚类的数据,名为amount的列
data_reshape = data.values.reshape((data.shape[0], 1)) # 转换数据形状
model_kmeans = KMeans(n_clusters=4, random_state=0)   # 创建KMeans模型并指定要聚类数量
keames_result = model_kmeans.fit_predict(data_reshape) # 建模聚类
df['amount2'] = keames_result   # 新离散化的数据合并到原数据框
print(df.head(5))   # 打印输出前5条数据
该过程使用了sklearn.cluster的KMeans算法实现。首先通过指定数据框的列名获得要建模的数据列;然后将数据的形状进行转换,否则算法会认为该数据只有1行(通过Pandas指定列名获得的数据默认都没有列值,例如示例中的data的形状是(100,),因此大多数场景下作为输入变量都需要做形状转换);接着创建KMeans模型并指定要聚类数量为4表,并设置初始化随机种子为固定值0(否则每次得到的聚类结果很可能都不一样),并使用fit_predict方法直接建模输出结果;最后将新的结果追加到原始数据框中。最终打印输出前5条结果如下:

  id  amount  income datetime  age2        amount1  amount2

0 15093 1390 10.40 6 0-40 (1000, 5000] 2
1 15064 7952 4.40 0 0-40 (5000, 10000] 1
2 15080 503 5.72 5 0-40 (200, 1000] 2
3 15068 1668 3.19 5 0-40 (1000, 5000] 2
4 15019 6710 3.20 0 0-40 (5000, 10000] 1
上述返回结果中,amount2列是转换后产生的列,列的值域是0、1、2,代表3类数据。
方法3:使用4分位数实现离散化。
df['amount3'] = pd.qcut(df['amount'], 4, labels=['bad', 'medium', 'good', 'awesome']) # 按四分位数进行分隔
df = df.drop('amount', 1)   # 丢弃名为amount的列
print(df.head(5))   # 打印输出前5条数据
该过程中,使用了Pandas的qcut方法指定做4分位数分隔,同时设置不同四分位得到的区间的标签分别为['bad','medium','good','awesome']);将得到的结果以列名为amount3追加到原始数据框中;然后通过drop方法丢弃名为amount的列。打印输出结果如下:

  id  income datetime  age2        amount1  amount2  amount3

0 15093 10.40 6 0-40 (1000, 5000] 2 bad
1 15064 4.40 0 0-40 (5000, 10000] 1 awesome
2 15080 5.72 5 0-40 (200, 1000] 2 bad
3 15068 3.19 5 0-40 (1000, 5000] 2 bad
4 15019 3.20 0 0-40 (5000, 10000] 1 awesome
上述返回结果中,amount3列是转换后产生的列,其结果构成与方法1完全相同,差异仅在于区间边界不同。

image.png

第6部分做特征二值化处理。
binarizer_scaler = preprocessing.Binarizer(threshold=df['income'].mean())

                              # 建立Binarizer模型对象

income_tmp = binarizer_scaler.fit_transform(df[['income']]) # Binarizer标准化转换
income_tmp.resize(df['income'].shape)   # 转换数据形状
df['income'] = income_tmp   # Binarizer标准化转换
print(df.head(5))   # 打印输出前5条数据
先建立Binarizer模型对象,然后使用fit_transform方法进行二值化转换,阈值设置为该列的均值;转换后得到的数据的形状是(1, 100),通过resize更改为与原始数据框income列相同的尺寸,并将结果直接替换原始列的值。最后打印输出前5条数据结果如下:

  id  income datetime  age2        amount1  amount2  amount3

0 15093 1.0 6 0-40 (1000, 5000] 2 bad
1 15064 1.0 0 0-40 (5000, 10000] 1 awesome
2 15080 1.0 5 0-40 (200, 1000] 2 bad
3 15068 0.0 5 0-40 (1000, 5000] 2 bad
4 15019 0.0 0 0-40 (5000, 10000] 1 awesome
上述返回结果中,income列的值已经离散为由0和1组成的二值化数据。
上述过程中,主要需要考虑的关键点是:如何根据不同的数据特点和建模需求选择最合适的离散化方式,因为离散化方式是否合理会直接影响后续数据建模和应用效果。
除了本节介绍的相关类型的转换离散化制约外,不同模型对于离散化的约束如下:

  • 使用决策树时往往倾向于少量的离散化区间,原因是过多的离散化将使得规则过多受到碎片区间的影响。
  • 关联规则需要对所有特征一起离散化,原因是关联规则关注的是所有特征的关联关系,如果对每个列单独离散化将失去整体规则性。

代码实操小结:本节示例中,主要用了几个知识点:

  • 通过Pandas的read_table方法读取文本数据文件,并指定列名;
  • 通过Pandas的to_datetime方法将字符串转换为datetime格式,使用weekday提取周几的数据;
  • 使用map函数做批量数据处理;
  • 使用Pandas的head方法只展示前n条数据;
  • 通过Pandas的merge方法合并多个数据框,实现类似SQL的数据关联查询;
  • 使用Pandas的drop方法丢弃特定数据列;
  • 使用Pandas的cut和qcut方法实现基于自定义区间和分位数方法的数据离散化;
  • 使用sklearn.cluster的KMeans方法实现聚类分析;
  • 使用shape方法获取矩阵形状,并使用resize方法对矩阵实现形状转换;
  • 使用sklearn.preprocessing的Binarizer方法做二值化处理。

3.10 内容延伸:非结构化数据的预处理

3.10.1 网页数据解析

本节通过一个稍微复杂一点的示例,来演示如何抓取并解析网页数据。之所以说复杂,是因为本节中会出现几个本书中未曾提及的知识和方法,从代码数量来看也会比之前的示例稍微长一点。
本示例中,将使用requests、bs4、re、time、pandas库进行网页数据读取、解析和相关处理。
由于网页抓取需要对页面本身做分析,而笔者撰写时的网页环境很可能与读者应用时不一致,导致代码不可用。因此,本节将使用笔者自己的网站做抓取和解析应用,该网站的结构不会发生变化。
网页抓取中,舆情分析是常见应用场景,例如在特定论坛抓取用户的发帖数据,然后做用户评价倾向分析(喜欢、不喜欢还是中立),提取用户喜好特征,用户对品牌商品等的印象关键字等。本节的目标是抓取目标网页的发帖的标题、时间、文章分类以及关键字。
在抓取和解析网页数据之前,首先要做的是网页内容分析。

  • 要抓取的内容格式:文本、图像或其他文件。
  • 是否存在重定向:重定向往往根据User-Agent来判断,例如手机端、台式计算机端看到的页面信息不同。
  • 是否需要验证:很多网页的爬虫都需要用户登录、验证码等。
  • 目标数据是否具有统一标签规则:要抓取的数据是否具有统一的HTML标签,便于后期处理。
  • URL规则:大多数情况下网页抓取都不只有一个页面,而是多个页面,因此需要了解不同页面的URL规则,尤其是带有条件查询的,需要了解具体参数。
  • 业务常识性分析:根据实际要抓取的数据,分析可能会产生哪些字段,会有哪些冲突和包含关系以及关联性影响等。

进入笔者网站http://www.dataivy.cn/ ,调出开发者工具,例如Chrome,按快捷键F12打开。打开开发者工具(或者单击右上角image.png,在弹出的菜单中选择“更多工具–开发者工具”),单击image.png切换到查看页面元素视图。鼠标单击开发者工具栏左侧的image.png
通过单击页面上的标题、发布时间、分类、标签等内容发现如下规律:

  • 所有的文章都以article标签开始,因此一个article内是一篇文章,如图3-9所示。

image.png

  • 发布时间、分类、标签都分别在span标签下,并通过不同的class可以区分,如图3-10所示。

image.png

上面的定位还是有点泛,需要再继续拆分:

  • 发布时间位于span标签里面time标签中,如图3-11所示。

image.png

  • 分类内容位于a标签中,如图3-12所示。

image.png

  • 上面两个值都可以直接从tag标签的值中取出,但是tag标签却存在多标签的情况,因此它是一个列表,内容位于内层,如图3-13所示。

image.png

image.png

URL规则分析:
由于我们要抓取所有页面,因此需要找到不同页面的URL特点,这样在构造URL的时候就能通过程序实现。我们点击下一页面以及不同页面,发现URL的基本特征是http://www.dataivy.cn/page/2/ ,其中page后面通过不同的数字代表页面的序号值。
到此为止我们基本确定了抓取思路:先构造一个URL,然后获得所有的article,在每个article中获得对应的标签并解析出目标值。
在本节中,我将使用面向对象的编程方式。

image.png

这两类编程思想都是常用的编程思路,本例将通过面向对象的编程来实现网页解析。清楚了上述逻辑后,我们开始编写代码。完整代码如下。
第1部分导入库,具体用途在注释中已经注明。
import requests # 用于发出HTML请求
from bs4 import BeautifulSoup # 用于HTML格式化处理
import re # 用于解析HTML配合查找条件
import time # 用于文件名保存
import pandas as pd # 格式化数据
第2部分,我们通过class函数来创建名为WebParse的对象,后续的所有应用都将基于该对象创建的实例使用。
class WebParse:
# 初始化对象

def __init__(self, headers):
    self.headers = headers
    self.article_list = []
    self.home_page = 'http://www.dataivy.cn/'
    self.nav_page = 'http://www.dataivy.cn/page/{0}/'
    self.art_title = None
    self.art_time = None
    self.art_cat = None
    self.art_tags = None

# 获取页面数量

def get_max_page_number(self):
    res = requests.get(self.home_page, headers=self.headers)  # 发送请求
    html = res.text                      # 获得请求中的返回文本信息
    html_soup = BeautifulSoup(html, "html.parser")  # 建立soup对象
    page_num_code = html_soup.findAll('a', attrs={"class": "page-numbers"})
    num_sets = [re.findall(r'(\d+)', i.text)
                for i in page_num_code]              # 获得页面字符串类别
    num_int = [int(i[0]) for i in num_sets if len(i) > 0]      # 获得数值页码
    return max(num_int)                     # 最大页码

# 获得文章列表

def find_all_articles(self, i):
    url = self.nav_page.format(i)
    res = requests.get(url, headers=headers)      # 发送请求
    html = res.text                      # 获得请求中的返回文本信息
    html_soup = BeautifulSoup(html, "html.parser")  # 建立soup对象,用于处理HTML
    self.article_list = html_soup.findAll('article')

# 解析单文章

def parse_single_article(self, article):
    self.art_title = article.find('h2', attrs={"class": "entry-title"}).text
    self.art_time = article.find('time', attrs={"class": {"entry-date published", "entry-date published updated"}}).text
    self.art_cat = article.find('a', attrs={"rel": "category tag"}).text
    tags_code = article.find('span', attrs={"class": "tags-links"})
    self.art_tags = '' if tags_code is None else  WebParse._parse_tags(self, tags_code)

# 内部用解析tag函数

def _parse_tags(self, tags_code):
    tag_strs = ''
    for i in tags_code.findAll('a'):
        tag_strs = tag_strs + '/' + i.text
    return tag_strs

# 格式化数据

def format_data(self):
    return self.art_title, self.art_time, self.art_cat, self.art_tags

在对象定义中,每个def都是一个功能模块。
首先要做的是初始化对象属性,我们通过__init__来实现。__init__的意思是初始化对象,所有的WebParse的属性都以self为开头,它们支持通过不同的方式传值。

  • 固定值,例如self.home_page,代表了我们要抓取的初值目标页面。
  • 通过字符串占位符占位,后续再通过format传值,例如self.nav_page,该属性是所有带有页面导航页码的URL地址。
  • 空值,例如self.art_title、self.art_time、self.art_cat、self.art_tags和self.article_list,前4个用于存储每篇文章的标题、发布时间、类别和标签,第5个值是文章列表。这些值后续会在不同的function中更新,并用于其他方法。
  • 外部传值,例如self.headers,代表的是页面headers,用于request请求时使用,在初始化的时候传入。

第2个get_max_page_number模块的作用是,获取页面数量,即有多少个文章页面。具体实现过程如下:

  • 用requests.get方法发出请求并返回对象res,一般情况下网页请求多用get和post方法,具体方法以网页的请求方式为准,具体可通过http工具查看。为了防止被屏蔽,我们通过传入headers信息,来模拟用户访问情况。
  • 用res.text返回信息中的文本html,除了text文本外,res返回的信息还包括status_code、cookies、encoding、headers等多种信息,这些信息可以用来判断信息返回状态等其他用途。
  • 用BeautifulSoup建立针对html的解析对象html_soup,解析中指定的解析器为html.parser,这是默认的解析器,在后面的文章中我们还会用到其他自定义解析器。
  • 从html_soup中使用findAll方法找到所有的a标签,并且其class为page-numbers,这些标签都是文章底部的导航功能。
  • 在num_sets的实现中我们用到了列表推导式,配合正则表达式re的findall方法,来找到所有的页面代码中的数字字符串。注意这里返回的仍然是字符串,而不是页码数字本身。
  • num_int的实现也是基于列表推导式,基本逻辑是从上面的num_sets中获取每个元素,如果元素>0,即元素有数字字符串,那么将其数字取出来并转换为int类型。这样,我们就得到了由页码数字组成的列表。
  • 最后的return返回的是列表中的最大值,通过max实现。后续应用时,会通过一个对象来承接该值。

回顾上面的过程,基本逻辑是,发出请求→获得返回文本→建立bs解析对象→找到符合特定条件的标签→解析标签中的目标元素值,该步骤可能要通过多个功能组合实现。下面所有的请求和解析都是同样的逻辑。
第3个find_all_articles的目标是获得所有的文章列表,作为下面解析用的列表。该模块的实现与get_max_page_number基本一致,其中主要差异点如下:

  • 在方法的参数中,self用来获得初始化时的所有属性,i则可以通过外部传值。
  • 最后我们并没有返回对象,而是通过self.article_list = html_soup.findAll('article')的方式,对初始化的self.article_list做更新,更新后,当对象的其他方法在使用该对象时,则会用到其最新的值,而不是初始值。

第4个parse_single_article的目标是解析单文章里面的不同元素,包括文章、发布时间、分类和标签。
这段方法中,传入的article是一个bs建立的对象,而不是一个字符串,因此我们直接用find方法找到在本节开始时确定的每个元素的规则。但在查找tag标签时,我们发现某些文章没有标签,会导致无法解析出文本(也就是目标标签值),所以这里用了一个条件表达式来实现:如果tags_code的值为空,那么self.art_tags的值为'',否则调用解析tag标签的功能_parse_tags。

image.png

第5个_parse_tags的目标是解析tags并返回字符串型的tag列表。tags_code应用findAll(而不是find)方法找到所有的tag列表,然后将列表中的文字组合为新的字符串,以“/”分割。
第6个format_data的目标是返回组合后的格式化列表格式的数据,功能简单。
第3部分是主程序应用。
if __name__ == '__main__':

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) Apple-WebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36'}
                    \# 定义头信息
data_cols = ['title', 'time', 'cat', 'tags']
app = WebParse(headers)
max_num = app.get_max_page_number()
data_list = []
for ind in range(max_num):
    app.find_all_articles(ind + 1)     # ind从0开始,因此要加1
    for article in app.article_list:
        app.parse_single_article(article)
        data_list.append(app.format_data())
data_pd = pd.DataFrame(data_list,columns=data_cols)
print(data_pd.head(2))

if__name__ == '__main__'中__name__是当前模块名,当模块被直接运行时模块名为__main__,这段代码的意思就是,当模块被直接运行时(例如通过Python直接执行或在编译器中执行),以下代码块将被运行;当模块是被导入时(例如使用import导入该模块时),代码块不被运行。接下来分别获得总页面数量,读取每个页面的标题和价格并写入文件。

  • headers定义了公用的http请求的头信息。
  • data_cols定义了后续数据框的头信息。
  • app = WebParse(headers)的作用是将对象初始化为实例,实例名为app,后续就用app来代替WebParse,包括调用其属性和方法。
  • app.get_max_page_number()的作用是返回页码最大值,并赋值给max_num。在这里我们看到,app.get_max_page_number()方法本身可直接从app实例中调取使用。
  • 下面的循环代码段实现了针对每篇文章的解析。这里用到了两层for循环,外层循环用来遍历所有页面,内层循环用来找到所有的文章。文章解析完成之后传到data_list,再基于data_list创建数据框,并打印输出。

上述代码执行后返回结果如下:
image.png

大多数情况下,通过网络爬虫获取数据都是辅助方式,原因是现在基本上所有的网站都有防爬虫的意识和方式,这导致数据爬取因受到外部很多因素的影响而导致数据质量低下。基于爬虫的主要工作内容包括舆情监测、市场口碑、用户情绪、市场营销等方面,属于外部属性较强的“附加”工作。这些工作其实都不是公司的核心运营内容,这就会导致这些工作看似有趣并且有价值,但真正对企业来讲价值很难实际体现。

image.png

上述过程中,主要需要考虑的关键点是:如何根据不同网页的实际特点,尤其是对于反爬虫的应对来正确读取到网页源代码,读取后源代码的解析往往不是主要问题。
本节示例中,主要用了几个知识点:

  • 通过requests库发送带有自定义head信息的网络请求;
  • 通过requests返回对象的text方法获取源代码文本信息;
  • 使用bs4的BeautifulSoup库配合find和findAll方法进行目标标签查找和解析,并通过其text方法获得标签文本信息;
  • 通过re的正则表达式功能,实现对于特定数字规律的查找;
  • 通过定义function函数来实现特定功能或返回特定结果;
  • 通过定义对象来实现特定功能;
  • 通过for循环读取数据列表;
  • 通过if条件表达式赋值;
  • 对文本文件的读写操作;
  • 使用time的localtime、time、strftime方法进行日期获取及格式化操作。

3.10.2 网络用户日志解析

网络用户日志属于非结构化数据的一种,其解析方法根据配合的服务器和跟踪实施的不同而需要自定义模块。本节将用一个示例来演示如何进行日志解析。
本示例中,将使用正则表达式配合自定义函数模块实现日志解析功能。数据源文件traffic_log_for_dataivy位于“附件-chapter3”中,默认工作目录为“附件-chapter3”。
背景:我们在这个网站上部署了Google Analytics的代码用来监测用户的行为,由于该工具是SAAS工作模式,数据采集之后直接发送到Google云端服务器。我们通过一定的方式将每次发送给谷歌的数据同时“备份”了一条保存为本地服务器日志。其中日志请求内容的部分以image.png 开头便是日志记录。我们的目标是找到这些日志,然后对日志做初步的解析,并保存为本地文件,便于后期做进一步数据解析和应用。
要实现这一目标,基本思路是:先从日志文件夹中读取日志文件列表(本示例中仅有1个文件,因此省略该步骤);然后依次读取每个日志文件中的数据;接着对日志文件中的每条数据进行判断,符合条件才能成为我们要的目标数据;最后将数据做初步解析并写到文件里面。
本节我们依然以函数的方式来撰写各个功能模块,如下是完整代码。
第1部分导入库。
import gzip
import re
import time
import pandas as pd

  • 在之前的读写文件中,我们都是直接基于原始数据做操作,此次我们直接对gzip压缩文件操作。Python默认提供了gzip库,可用于读写gz压缩包文件。
  • re库用来做正则表达式匹配。读者可以发现,凡是涉及文本类内容处理的基本上都离不开该库。
  • time库用来给文件命名。
  • pandas用来格式化输出文件。

image.png

第2部分判断日志中是否为爬虫日志。在日志中,很多搜索引擎公司的网络爬虫在抓取页面时会产生日志数据,除了谷歌外,百度、搜狗、雅虎、有道、必应等都有类似的爬虫(或者称为机器人)。代码如下:
def is_spider(log_record,spiders):

detect_result = [True if log_record.find(spider) == -1 else False for spider in spiders]
is_exist = True if all(detect_result) else False
return is_exist

代码中我们先使用了一个列表推导式,目的是检测当前log日志中是否存在spider元素,如果不存在则为False,否则为True;接着通过条件赋值的方式来判断,如果全部为True(没有任何一个spider元素),则为True,否则为False。

image.png

第3部分判断是否为UA记录。
def is_ua_record(log_record):

is_ua = True if log_record.find(![image.png](https://ucc.alicdn.com/pic/developer-ecology/1e72a359fa684c8aacee5e3a1d562d9b.png)) != -1 else False
return is_ua

这里的实现是基于条件表达式的,如果日志记录中包含“GET /__ua.gif?”则为UA记录,返回True,否则为False。
第4部分解析每条日志数据。

image.png

该模块定义了日志下所有字段的分割规则,首先我们针对日志记录中要解析的每个数据字段定义了一个正则表达式构成的规则集,以用于目标数据的解析;接着将分散的规则通过re库的compile方法合成一个匹配模式;然后基于匹配模式使用match方法匹配每条数据记录,并由此返回匹配的字段值列表,最后解析出所有定义的字段值并返回给其他函数使用。
第5部分读取日志数据。
def get_ua_data(file,spiders):

ua_data = []
with gzip.open(file, 'rt') as fn:      # 打开要读取的日志文件对象
    content = fn.readlines()      # 以列表形式读取日志数据
for single_log in content:          # 循环判断每天记录
    rule1 = is_spider(single_log,spiders)
    rule2 = is_ua_record(single_log)
    if rule1 and rule2:          # 如果同时符合2条规则,则执行
        ua_data.append(split_ua_data(single_log))
ua_pd = pd.DataFrame(ua_data)
return ua_pd

这里涉及基于with方法的上下文管理,先通过gzip.open方法以文本只读模式打开日志文件,然后通过readlines方法以列表的形式读取日志记录,读取完成之后关闭日志文件对象。

image.png

通过一个for循环将列表中的每条日志读取出来,然后定义了两条规则用于判断日志是否符合条件。两个条件同时满足时,解析该条日志并将结果追加到列表ua_data。最后基于ua_data形成新的数据框并返回。
第6部分是主要程序模块。这部分功能的意义是将前文提到的功能整合到该模块中。
#主程序
if name == '__main__':

file = 'dataivy.cn-Feb-2018.gz'      # 定义原始日志的文件名
ua_data = split_ua_data(file)      # 读取非结构化文本数据
spiders = [
'AhrefsBot',
'archive.org_bot',
'baiduspider',
'Baiduspider',
'bingbot',
'DeuSu',
'DotBot',
'Googlebot',
'iaskspider',
'MJ12bot',
'msnbot',
'Slurp',
'Sogou web spider',
'Sogou Push Spider',
'SputnikBot',
'Yahoo! Slurp China',
'Yahoo! Slurp',
'YisouSpider',
'YodaoBot',
'bot.html'

]

ua_pd = get_ua_data(file,spiders)
ua_pd.columns = ['ip_add','requet_time','request_info','status','bytes_info','referral','ua']
print(ua_pd.head(2))
output_file = 'ua_result_{0}.xlsx'.format(time.strftime('%Y%m%d%H%M%S',time.localtime(time.time())))
ua_pd.to_excel(output_file, index=False)

整个代码实现功能如下:

  • file定义了要读取的gzip压缩包文件。
  • Spiders定义的是搜索引擎关键字列表,读者可根据自己需要增加。
  • 使用get_ua_data对日志文件做解析,并获得数据框ua_pd。
  • ua_pd.columns代码段实现了对数据框重命名,这样便于识别不同字段的含义。
  • output_file为输入的文件名,这里定义了一个Excel文件,文件名中通过时间戳的方式增加后缀,便于识别。后缀名使用time.strftime方法,含义是获取当前执行的时间戳,并按照年月日时分秒的格式转换为字符串。
  • ua_pd.to_excel方法实现了导出为Excel文件,设置index=False的意思是不导出index值。

代码执行后,会在当前执行目录下生成一个Excel结果文件,如图3-14所示。

image.png

总结:上述过程看似略显复杂,但每个函数功能比较简单、易懂且逻辑清晰。当然,后续应用可能需要再做二次解析,例如从reqeust_info中解析出URL地址,从request_time中解析出时区、年月日,基于IP地址映射出不同城市、省份等,后续需要做很多应用适配。
上述过程中,主要需要考虑的关键点如下:

  • 如何根据不同的服务器日志配置以及前端代码跟踪实施的具体情况,编写日志过滤规则。
  • 有关爬虫数据的排除也是需要额外注意的信息点。

本节示例中,主要用了如下几个知识点:

  • 对文本文件的读写操作;
  • 使用gzip库读取压缩包文件;
  • 通过find方法查找符合条件的字符串;
  • 通过if条件表达式实现赋值;
  • 通过re的正则表达式功能,实现对于特定字段的查找和匹配;
  • 通过定义function函数来实现特定功能或返回特定结果;
  • 通过for循环读取数据列表;
  • 将Pandas数据框导出为Excel;
  • 基于time.strftime将特定时间转换为字符串。

3.10.3 图像的基本预处理

本示例中,将使用OpenCV来做图像基本预处理操作,基本处理内容包括图像缩放、平移、旋转、透视变换、图像色彩模式转换、边缘检测、二值化操作、平滑处理、形态学处理。
数据源文件sudoku.png、j.png位于“附件-chapter3”中。
代码分为12个部分,涵盖了日常图像处理的常用操作。
第1部分为导入库,本代码中除了OpenCV库外,还有Numpy用于定义图像处理的内核、Matplotlib用于展示多图图像。
import cv2 # 导入图像处理库
import numpy as np # 导入Numpy库
from matplotlib import pyplot as plt # 导入展示库
第2部分定义了一个函数,用来做单图像展示。
def img_show(img_name, img):

cv2.imshow(img_name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()

下面的每个功能模块,当只有一个图像做展示时会直接调用该模块,而无须重复写展示功能代码。cv2.show()方法必须与cv2.waitKey()、cv2.destroyAllWindows()一起使用,才能保证图像正常展示及关闭功能。
第3部分读取原始图像并展示。
img_file = 'sudoku.png' # 定义原始数据文件
img = cv2.imread(img_file) # 以彩色模式读取图像文件
rows, cols, ch = img.shape # 获取图像形状
img_show('raw img', img) # 展示彩色图像
通过cv2的imread方法以彩色模式读取图像,然后获得彩色图像的长、高和通道形状,最终调用img_show做图像展示,结果如图3-15中①所示。
第4部分图像缩放处理。
img_scale = cv2.resize(img, None, fx=0.6, fy=0.6, interpolation=cv2.INTER_CUBIC) # 图像缩放
img_show('scale img', img_scale) # 展示缩放后的图像
直接使用cv2.resize方法设置缩放比例、方法等,并展示输出为原图像60%的新图像,结果如图3-15中②所示。
第5部分图像平移处理。
M = np.float32([[1, 0, 100], [0, 1, 50]]) # 定义平移中心
img_transform = cv2.warpAffine(img, M, (cols, rows)) # 平移图像
img_show('transform img', img_transform) # 展示平移后的图像
先定义图像平移中心,然后使用cv2.warpAffine方法根据平移中心移动图像,移动后的图像如图3-15中③所示。
第6部分图像旋转处理。
M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 45, 0.6) # 定义旋转中心
img_rotation = cv2.warpAffine(img, M, (cols, rows))

    # 第1个参数为旋转中心,第2个参数为旋转角度,第3个参数为旋转后的缩放因子

img_show('rotation img', img_rotation) # 展示旋转后的图像
与图像平移类似,先定义旋转中心,然后使用cv2.warpAffine进行旋转,同时设置旋转角度为45度、缩放因子为0.6,结果如图3-15中④所示。
第7部分透视变换处理。
pts1 = np.float32([[76, 89], [490, 74], [37, 515], [520, 522]]) # 定义变换前的4个校准点
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]]) # 定义变换后的4个角点
M = cv2.getPerspectiveTransform(pts1, pts2) # 定义变换中心点
img_perspective = cv2.warpPerspective(img, M, (300, 300)) # 透视变换
img_show('perspective img', img_perspective) # 展示透视变换后的图像
先定义变换前的4个校准点,然后定义变换后的4个角点,可用来控制图像大小。接着定义变换中心点,并应用cv2.warpPerspective进行透视变换,结果如图3-15中⑤所示。
第8部分转换为灰度图像。
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 图像转灰度
img_show('gray img', img_gray) # 展示灰度图像
使用cv2.cvtColor将BRG模式转为GRAY模式,结果如图3-15中⑥所示。
第9部分边缘检测处理。
img_edges = cv2.Canny(img, 100, 200) # 检测图像边缘
img_show('edges img', img_edges) # 展示图像边缘
使用cv2.Canny检测图像边缘,结果如图3-16所示。

image.png

image.png

第10部分图像二值化处理。
ret, th1 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY)  # 简单阈值
th2 = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)     # 自适应均值阈值
th3 = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)    # 自适应高斯阈值
titles = ['Gray Image', 'Global Thresholding (v = 127)', 'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']    # 定义图像标题
images = [img_gray, th1, th2, th3] # 定义图像集
for i in range(4):

plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')    # 以灰度模式展示每个子网格的图像
plt.title(titles[i])                      # 设置每个子网格标题
plt.xticks([]), plt.yticks([])                  # 设置x轴和y轴标题

这里分别应用了简单阈值、自适应均值阈值、自适应高斯阈值3种方法做二值化处理,并使用Matplotlib做多网格图像,同时展示原始图像和3种阈值下二值化图像处理结果,如图3-17所示。

image.png

第11部分图像平滑处理。
kernel = np.ones((5, 5), np.float32) / 25 # 设置平滑内核大小
img_smoth_filter2D = cv2.filter2D(img, -1, kernel) # 2D卷积法
img_smoth_blur = cv2.blur(img, (5, 5)) # 平均法
img_smoth_gaussianblur = cv2.GaussianBlur(img, (5, 5), 0) # 高斯模糊
img_smoth_medianblur = cv2.medianBlur(img, 5) # 中值法
titles = ['filter2D', 'blur', 'GaussianBlur', 'medianBlur'] # 定义标题集
images = [img_smoth_filter2D, img_smoth_blur, img_smoth_gaussianblur, img_smoth_medianblur]    # 定义图像集
for i in range(4):

plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')
    \# 以灰度模式展示每个子网格的图像
plt.title(titles[i])                      # 设置每个子网格标题
plt.xticks([]), plt.yticks([])                  # 设置x轴和y轴标题

先设置平滑内核大小,然后分别使用cv2.filter2D(3D卷积)、cv2.blur(平均法)、cv2.GaussianBlur(高斯模糊)、cv2.medianBlur(中值法)进行平滑结果对比,如图3-18所示。

image.png

第12部分形态学处理。
img2 = cv2.imread('j.png', 0) # 以灰度模式读取图像
kernel = np.ones((5, 5), np.uint8) # 设置形态学处理内核大小
erosion = cv2.erode(img2, kernel, iterations=1) # 腐蚀
dilation = cv2.dilate(img2, kernel, iterations=1) # 膨胀
plt.subplot(1, 3, 1), plt.imshow(img2, 'gray') # 设置子网格1图像
plt.subplot(1, 3, 2), plt.imshow(erosion, 'gray') # 设置子网格2图像
plt.subplot(1, 3, 3), plt.imshow(dilation, 'gray') # 设置子网格3图像
这里重新以灰度模式读取一个图像,定义处理内核之后,通过cv2.erode和cv2.dilate分别实现腐蚀和膨胀操作。原图和腐蚀、膨胀处理后的图像对比如图3-19所示。

image.png

上述过程中,主要需要考虑的关键点是:如何根据不同的图像处理需求,实现图像的基本预处理任务,尤其是对于每种方法下参数的具体设置,需要根据实际情况加以选择。另外,在程序自动化过程中,是不可能依靠人工参与每次边界调整、阈值优化等具体过程,而应该通过一定专用的参数优化模型来实现。例如,对于透视图像的处理,首先要做的是识别出透视图像的矫正参照点,而该过程就是一个融合了业务场景、图像处理技术、数学知识和计算方法等多学科知识的建模过程。
本节示例中,主要用了几个知识点:

  • 通过cv2.imread对图像文件的数据读取,并分别以彩色和灰度模式读取图像;
  • 通过cv2.imshow、cv2.waitKey()、cv2.destroyAllWindows()实现图像展示;
  • 通过Matplotlib库实现多子网格图的展示;
  • 通过cv2.resize实现图像缩放;
  • 通过cv2.warpAffine实现图像平移;
  • 通过cv2.warpAffine和cv2.getRotationMatrix2D实现图像旋转;
  • 通过cv2. warpPerspective和cv2.getPerspectiveTransform实现图像透视变换;
  • 通过cv2.cvtColor实现图像颜色模式转换;
  • 通过cv2.Canny实现图像边缘检测;
  • 通过cv2.threshold实现图像二值化处理,并通过简单阈值、自适应均值阈值、自适应高斯阈值等方法寻找最佳阈值;
  • 通过cv2.filter2D、cv2.blur、cv2.GaussianBlur、cv2.medianBlur等方法实现图像平滑处理;
  • 通过for循环做数据循环输出;
  • 通过cv2.erode、cv2.dilate等方法实现图像腐蚀、膨胀等形态学处理。

3.10.4 自然语言文本预处理

与数据库中的结构化数据相比,文本具有有限的结构,某些类型的数据源甚至没有数据结构。因此,预处理就是要对半结构化或非结构化的文本进行格式和结构的转换、分解和预处理等,以得到能够用于进一步处理的基础文本。不同环境下,文本所需的预处理工作内容有所差异,大体上分为以下几个部分。
1.基本处理
根据不同的文本数据来源,可能涉及的基本文本处理包括去除无效标签、编码转换、文档切分、基本纠错、去除空白、大小写统一、去标点符号、去停用词、保留特殊字符等。

  • 去除无效标签:例如从网页源代码获取的文本信息中包含HTML标签,此时要提取特定标签内容并去掉标签。
  • 编码转换:不同编码转换对于中文处理具有较大影响,例如UTF-8、UTF-16、GBK、GB2312等之间的转换。
  • 文档切分:如果要获得的单个文档中包含多个文件,此时需要进行单独切分以将不同的文档拆分出来。
  • 基本纠错:对于文本中明显的人名、地名等常用语和特定场景用语的错误进行纠正。
  • 去除空白:去除文本中包含的大量空格、空行等。
  • 大小写统一:将文本中的英文统一为大写或小写。
  • 去标点符号:去除句子中的标点符号、特殊符号等。
  • 去停用词:常见的停用词包括the、a、an、and、this、those、over、under、above、on等。
  • 保留特殊字符:某些场景下可能需要只针对汉字、英文或数字进行处理,其他字符都需要过滤掉。

2.分词
分词是将一系列连续的字符串按照一定逻辑分割成单独的词。在英文中,单词之间是以空格作为自然分界符的;而中文只有字、句和段能通过明显的分界符来简单划界,而作为词则没有形式上的分界符。因此,中文分词要比英语等语种分词困难和复杂得多。对于复杂的中文分词而言,常用的分词方法包括最大匹配法、逆向最大匹配法、双向匹配法、最佳匹配法、联想-回溯法等。
3.文本转向量(word to vector)
人们通常采用向量空间模型来描述文本向量,即将文档作为行,将分词后得到的单词(单词在向量空间模型里面被称为向量,也被称为特征、维度或维)作为列,而矩阵的值则是通过词频统计算法得到的值。这种空间向量模型也称为文档特征矩阵,其表示方法如表3-6所示。

image.png

本示例中,将仅对自然语言文本做分词和word to vector处理,更多有关文本分析的内容,例如词性标注、关键字提取、词频统计、文本聚类、相似关键字分析等将在第4章介绍。数据源文件text.txt位于“附件-chapter3”中。完整代码如下。
第1部分导入库。本代码中用到了jieba做中文分词,sklearn用来做word to vector转换,Pandas用于做格式化输出。
import pandas as pd
import jieba # 结巴分词
from sklearn.feature_extraction.text import TfidfVectorizer # 基于TF-IDF的词频转向量库
第2部分建立一个分词函数。
def jieba_cut(string):

return list(jieba.cut(string)) # 精确模式分词

该函数直接调用jieba.cut做中文分词,用于下面的中文分词配合map函数使用。jieba.cut返回是一个分词对象,使用list方法转换为列表。
第3部分读取自然语言文件和停用词。
with open('text.txt', encoding='utf8') as fn1, open('stop_words.txt', encoding= 'utf8') as fn2:

string_lines = fn1.readlines()
stop_words = fn2.read()

stop_words = stop_words.split('n')
stop_words.append('n')
读取文件时基于with方法做上下文管理,同时使用Python标准方法open方法打开两个文件对象,使用read方法分别读取自然文本以及停用词。由于使用read方法读取的是字符串,我们需要将其转换为列表,这里分别使用split方法对字符串做拆分,分割符为“n”。
第4部分中文分词。
seg_list = list(map(jieba_cut,string_lines)) # 存储所有分词结果
for i in range(5): # 打印输出第1行的前5条数据

print(seg_list[1][i])

直接使用map方法建立seg_list,批量对上面的文本列表string_lines用jieba_cut做分词,然后将map对象转换为list列表。最后通过循环打印输出第2条数据的前3个词,如下:
对于
数据

第6部分word to vector。
vectorizer = TfidfVectorizer(stop_words=stop_words, tokenizer=jieba_cut)

                            # 创建词向量模型

vector_value = vectorizer.fit_transform(string_lines).toarray()

                            # 将文本数据转换为向量空间模型

vector = vectorizer.get_feature_names() # 获得词向量
vector_pd = pd.DataFrame(vector_value, columns=vector) # 创建用于展示的数据框
print(vector_pd.head(1)) # 打印输出第1条数据
先定义一个要去除的停用词库,然后使用TfidfVectorizer方法创建词向量模型,使用fit_transform方法对输入分词后的列表做转换,并使用toarray方法将向量结果转换为数组。最后通过词向量的get_feature_names获得向量名称,再通过数据框做数据格式化。打印输出第1条数据如下:

image.png

在本示例中,没有涉及更多的自然语言文本预处理环节,例如无效标签、编码转换、文档切分、基本纠错、去除空白、大小写统一、去标点符号、去停用词、保留特殊字符等,原因是这些内容都是针对不同案例展开的,而本示例仅做功能演示使用,具体应对不同文本时,很难具有通用性和可复制性。另外,由于测试文件为本书的部分文字内容,文本规模本身有限,因此可能难以提取出真正有价值的数据和规律,尤其是基于向量化的分词由于数据量的限制以及作为扩展内容,无法做更多有价值的探索。
上述过程中,主要需要考虑的关键点是:如何根据不同自然语言的来源特点、应用场景、语言语法、目标应用做综合的文本处理,文本处理中用到的针对文字内容的过滤、筛选、去除、替换等主要基于字符串的操作居多。
本节示例中,主要用了几个知识点:

  • 对文本文件的读写操作;
  • 使用jieba.cut做中文分词,并可设置不同的分词模式;
  • 通过append方法对列表追加元素;
  • 使用字符串的split方法,将其分割为多个字符串列表;
  • 通过for循环读取数据列表;
  • 使用with方法管理多个文件对象;
  • 通过sklearn.feature_extraction.text的TfidfVectorizer方法做word to vector处理;
  • 通过词向量的get_feature_names获得向量名称;
  • 通过转换后的向量使用toarray方法将向量结果转换为数组;
  • 使用pandas.DataFrame建立数组,并通过head方法输出前n条数据。

3.11 本章小结

内容小结:本章介绍了10个有关数据化运营过程中的数据预处理经验,涵盖了常见的数据清洗、标志转换、数据降维、样本不均衡、数据源冲突、抽样、共线性、相关性分析、数据标准化、数据离散化等内容,并在最后提出了运营业务对于数据处理的影响和应对措施。在扩展内容中简单介绍了有关网页、日志、图像、自然语言的文本预处理工作,作为拓展知识阅读。本章涉及技术的部分都有对应示例代码,该代码可在“附件-chapter3”中的名为chapter3_code.ipynb的文件中找到。
数据的预处理其实包含了基本的数据清洗、加工以及对特征的处理,其中有关特征工程的部分是整个预处理的核心,包括特征的选择、转换、组合、重构等都是为了更好地描述数据本身,一定程度上,特征的质量决定了后续模型结果的质量,因此特征工程本身是既复杂又重要的关键过程。
重点知识:客观上讲,本章的每一节内容都非常重要,原因是所有的内容都没有唯一答案,都需要读者根据不同的场景进行判别,然后选择最合适的方法进行处理。因此,掌握每种方法的适用条件以及如何辨别其应用前提是关键知识。
外部参考:限于篇幅,本书涉及很多内容无法一一展开介绍,以下给出更多外部参考资源供读者学习:

  • Python有一个第三方库Featuretools,它能自动实现对特征的加工和处理,例如基于深度特征综合创建新特征。有兴趣的读者可以学习一下,对于特征的派生思路很有帮助。
  • Python第三方库imblearn提供了非常多的样本不均衡处理方法,尤其是SMOTE、组合/集成方法的应用非常广泛。读者可在https://github.com/scikit-learn-contrib/imbalanced-learn 中找到更多信息。
  • 本书中多次引用了sklearn中的processing库,里面还有更多有关数据处理的方法,读者可查阅http://scikit-learn.org/stable/modules/preprocessing.html 了解更多。
  • 关于数据的预处理,Pandas库真的非常好用,尤其是对于交互式或探索式的数据分析而言,Pandas更是利器,推荐读者更加深入地了解和学习,在http://pandas.pydata.org/pandas-docs/stable/ 中可查看更多。
  • 对于自然语言处理而言,语料库的丰富程度决定了可供训练的“原材料”的多寡,在本章的附录中,将有单独的部分提供外部语料库的参考信息。

应用实践:本章几乎每节都带有示例代码,读者可直接使用附件中的示例数据进行模拟操作,以了解实现方法。同时,推荐读者从自己所在环境中找到一些真实数据,针对每个模块进行操作练习。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接