数据分析(二)数据预处理

数据预处理的重要性:

  • 由于数据采集技术的局限、传输过程的错误等原因,采集的原始数据通常存在不完整、含噪(包含错误或离群值)、不一致(编码不同、属性与内容对不上)、冗余(样本重复、属性之间可相互推导)等问题
  • 模型输入数据的质量直接影响了建模的效果,尤其是在追求最后的一丢丢优化的时候

广义上的数据预处理还包含了【数据质量管理】这一过程,但对于个人而言感觉这概念太抽象了,所以本文只包含了数据预处理的常规步骤:数据清洗,数据集成,数据转换和数据归约。下面先用一个表格来说明这四个步骤的任务和常见的处理技术之间的关系。

步骤 主要任务 相关技术
数据清洗 补充缺失,平滑噪声,识别或删除离群点/异常值,解决数据不一致问题 缺失值处理,异常值检测,错误发现与修复
数据集成 集成多个数据库、数据立方或文件 实体识别,冗余和相关分析,数据仓库(大数据专用)
数据转换 规范化和聚集 变量离散化,变量标准化
数据归约 特征选择和采样 特征选择,特征提取,数据抽样,数据过滤

注意:在实际的处理过程上,不一定4个步骤都需要;而且如果你看过数据分析(一)的话你会很容易发现其实不同步骤之间存在交集。这不是重点,分析步骤的划分只是为了便于学习理解,在实际处理过程中通常是一个交织循环的过程。问题来了:怎么知道该如何“交织循环”呢?无它,多逛逛优秀的博客,看看大牛们是怎么做的吧。

从上面的表格你也许已经看出来了,【步骤】是提纲挈领用的,【主要任务】是提醒分析目标用的,【相关技术】才是技术人的根基。所以接下来的内容是就主要的技术部分进行介绍。

缺失值处理

数据的缺失就是指 完全没有采集到数据 ,至于数据乱码、数据错误等不算在此列,而是归在异常检测和错误修复部分。处理缺失有两种通用的方法:删除法填补法

删除法

  • 删除样本:将存在数据缺失的样本删除
    – 适用于数据对象有多个属性发生数据缺失、且删除的数据对象占样本总量比例非常小的情况
    – 样本太少的情况下可能数据偏离,引导出错误的学习(训练)成果
  • 删除属性:将存在数据缺失的属性删除
    – 适用于变量缺失值较多,且对研究目标影响不大的情况
    – 可能出现误删重要属性的情况
''' 可用 pandas.DataFrame.dropna 清除缺失值,更多用法请查看官方文档 '''
# 删除有缺失的样本,此处可用 thresh 参数来限制仅删除非缺失值少于 thresh 个的对象
data['有缺失的列'] = data['有缺失的列'].dropna(axis='index', thresh=5)
# 删除有缺失的属性
data['有缺失的列'] = data['有缺失的列'].dropna(axis='columns')

填补法

  • 单一填补
    • 均值填补:对于非数值型数据,使用众数填补;对于数值数据,使用算术平均值
      – 适用于缺失值完全随机的情况
      – 能保证总体均值无偏,但容易使均值集中在均值附近造成“尖峰”,低估方差
      – 调整策略:将样本总体按某种方式划分为多个子集,对每个子集使用局部均值填补
    • 热平台填补:在样本总体中找到与缺失值最相似的变量进行填充
      – 可以保持数据类型,且变量值与填充前可能很接近
      – 只能利用已有信息,无法覆盖样本总体未采集到或未反应的信息
    • 冷平台填补:与热平台类似,但是是从历史数据中找到最相似的变量
      – 适用于保留有历史数据的情况
      – 不能消除估计的偏差
    • KNN填补:与热平台相似,但是是根据某种距离函数,找到k个最相似的样本,取其均值或众数估计缺失的数据
      – 引入了自相关性,容易加剧模型分析的偏差
  • 随机填补
    • 贝叶斯Bootstrap:从无缺失的数据中,随机抽取一个变量进行填补
    • 随机贝叶斯Bootstrap:类似贝叶斯Bootstrap,但是先在样本总体中【有放回地】抽取k*个样本建立子集,在子集中随机抽取
  • 基于模型填补:使用分类或回归模型,将无缺失的属性作为自变量、有缺失的属性当作标签或预测目标,构建模型进行估计
''' 在 python 中,可用 pandas.DataFrame.fillna 或 interpolate 进行填充,注意在 DataFrame 中,缺失值为 NaN 值(可用 numpy.nan 表示) '''
# 用均值
data['有缺失的列'] = data['有缺失的列'].fillna(data['有缺失的列'].mean())
# 用众数
data['有缺失的列'] = data['有缺失的列'].fillna(data['有缺失的列'].mode())
# 用 KNN,k 取 4
data['有缺失的列'] = data['有缺失的列'].interpolate(method='nearest', order=4)
# 或者用这种
data['有缺失的列'].interpolate(method='nearest', order=4, inplace=True)

# 至于热平台冷平台就和具体的作法用关了;而随机填补方面,个人尚未了解到有既成的库可以调用,自己按需 def 一个吧,随机抽取部分的代码可以参考下面:
import random

chosen = random.randint(0, data.shape[0] - 1)
# 或者
chosen = random.randrange(0, data.shape[0])

异常值检测

这里推荐 一篇总结比较全面的文章 ,建议与本文相互对照下

基于统计

  • 3 σ \sigma 探测法:基于正态分布的特性,有超过95%的面积在平均数左右两个标准差的范围内。因此可能通过适当设置 α \alpha 分位点,将极小概率出现的数值作为异常值剔除,在如下示意图中,若 Z0.025=1.96,那么通过仅保留 x μ σ < 1.96 {\lvert \frac {x - \mu}{\sigma} \rvert < 1.96} 的样本,就可以剔除出现概率仅为 5% 的样本:
    正态分布分位点
    – 适用于数值型数据
  • 画散点图:适用于数值型数据,直观地(根据个人经验)发现异常点
  • 四分位法——箱形图观察:对每个属性画出对应的箱形图,将上下截断点之外的值视为异常值
    – 适用于数值型的属性
    – 相较方差和极差,受极值影响小,处理大规模数据效果不错;但对于小规模数据略显粗糙
    – 记上分位点为Q3,下分位点为Q1,IQR = Q3 - Q1,则通常取上截断点 = Q3 + 1.5IQR,下截断点 = Q1 - 1.5IQR
    箱形图四分位点示意图
''' 在 python 中可尝试用 Dataframe.boxplot 实现这一处理过程,不过这一做法要求所有属性值都已处理为数值型,如下 '''
# boxplot 中自动记录了箱形图的基本统计信息,所以 return_type 必须设置为 dict 型
box = data.boxplot(return_type='dict')
col = data.columns
# fliers 记录了每个属性下的异常值,所以不难理解 len(box['fliers']) = 属性个数
for i in range(len(box['fliers'])):
	# boxplot 中把截断点到分位点之间的连线形象地称为胡须(即 whiskers),box['whiskers'] 总共有(2 * 属性个数)个 values,下面的这个调用结果是自动按上面说的公式计算的结果。至于 get_ydata 还是 get_xdata 好像不同版本不同,需要的时候就一个个 print 出来确认下吧
	lower_bound = box['whiskers'][i * 2].get_ydata()[1]
	upper_bound = box['whikders'][i * 2 + 1].get_ydata()[1]
	# 通常的手段是先将异常值设置为缺失值,然后再用处理缺失值的方法完成后续流程
	data[col[i]] = data[col[i]].apply(lambda x: np.nan if (x < lower_bound) | (x > upper_bound) else x)
# 后续的处理缺失值过程自行脑补
' ...... '

基于距离

  • 局部异常因子算法( LOF 算法,Local Outlier Factor)
    – 基本思想:通过定义某种距离度量方法,比较每个点 p (样本)和其邻域点的密度,p 密度越小,越可能被认为是异常点
    – 适用于二维或高维坐标体系(即至少要有2个属性)
    – 先定义一些符号:
符号 含义
d(A, B) 点 A 与 B 之间的距离
dk(A) 点 A 的第 k 距离,即以 A 为圆心,半径从 0 不断加大,接触到第 k 个点时的【半径长度】
Nk(A) 点 A 的第 k 距离以内的所有点,包括第 k 距离对应的点在内
reach-distancek(B, A) max { dk(A), d(A, B) } (可参考下面第一张图
lrdk(A) 点 A 的局部可达密度(local reachability density),计算公式为 l r d k ( A ) = 1 / O N k ( A ) r e a c h d i s t a n c e k ( A , O ) N k ( A ) lrd_k(A) = 1/ \frac{\sum_{O\in {N_k(A)} reach-distance_k(A, O)}}{\lvert N_k(A) \rvert} ,其中,分母表示点 A 的第 k 邻域内到点 A 的平均距离,取其倒数即为密度的定义
LOFk(A) 局部离群因子(local outlier factor),计算公式为 L O F k ( A ) = O N k ( A ) l r d k ( O ) l r d k ( A ) N k ( A ) = O N k ( A ) l r d k ( O ) N k ( A ) / l r d k ( A ) LOF_k(A) = \frac {\sum_{O \in N_k(A)}{\frac {lrd_k(O)}{lrd_k(A)}}}{\lvert N_k(A) \rvert} = \frac {\sum_{O \in N_k(A)} {lrd_k(O)}}{\lvert N_k(A) \rvert} / lrd_k(A) ,即点 A 的第 k 邻域内所有点的【平均局部可达密度】与点 A 的局部可达密度之比 (可参考下面第二张图

距离示意图
 LOF 示意图

  • 确定异常值的方法:
    – LOFk(A) 值接近1,说明点 A 与邻域点密度相近,可能属于同一簇;
    – LOFk(A) 值越小于1,说明点 A 的密度越高于邻域中其他点,点 A 为密集点;
    LOFk(A) 值越大于1,说明点 A 的密度越低于邻域中其他点,越可能是异常点
''' 可用 sklearn.neighbors.LocalOutlierFactor 完成 '''
from sklearn.neighbors import LocalOutlierFactor as LOF
# 默认参数下,剔除 LOF 值最大的 10%
lof = LOF()
lof.fit(data)
# sklearn 中根据 LOF 计算出了每个样本的评估分数,分数越低越可能被认为是异常值
scores = lof.negative_outlier_factor
# lof 模型不会自动将异常值剔除,只是把每个样本的分数、以及 10% 的剔除阈值计算出来
# 如下面的操作不管三七二十一,把这分数最低的 10% 的特征所在的样本全丢弃了(呵,不负责的 cou 男人)
data[scores > lof.threshold_].reset_index(drop=True)

错误发现与修复

  • 格式内容清洗
    – 显示格式不一致:最常见的是日期的格式不一致,尤其是数据来源不同时
    – 内容中存在非法字符:如电话中包含标点,身份中包括字母(X除外)
    – 属性与内容不符:如属性“性别”下的值是“小明”
  • 逻辑错误清洗
    – 去重:可用实体识别技术实现
    – 去除不合理值:如年龄2000岁;这类检测主要依赖于属性值本身的约束
    – 修正矛盾内容:可相互验证的内容出现矛盾,如某个样本中,属性“电话区号”是020(广州),但所在城市是北京

变量离散化

离散化的缘由:

  • 将连续型数值分段,可能将异常值直接划入相应区段,从而强化了模型对异常值的鲁棒性
  • 离散化后每个取值均对应一个有明确含义的区段/区号,可解释性变强了
  • 最重要的是【特征的取值个数】大大减少了,对于模型存储空间和运行效率都有改善

无监督离散化:根据数据本身的分布特征

  • 等距离散化:根据变量的取值,将其均匀地划分为 n 个区间,直接将数据划入相应区间即可
    – 倾向于将数据【不均匀】地划入区间中,容易导致离散化后分布不均,且对异常值很敏感
  • 等频离散化:根据变量取值的总个数,将其均匀划分为 n 份,使得每份中的数据个数相同(或近似相同)
    – 倾向于将相同取值的数据划入不同的区间内,容易导致离散化后连续的区间之间有相似性
  • 基于聚类分析的离散化:对需要离散化的变量用聚类算法如( K-Means,EM 算法),依据变量的分布特征划分;有需要的话根据聚类的结果进一步划分(自上而下的分裂策略 VS 自下而上的合并策略)
    – 建议先参考 这篇文章
''' 比如可以考虑用 sklearn.cluster.KMeans 完成 '''
# 建议先定义一个通用函数方便调用
def cls_cut(d, k, data, col):
	from sklearn.cluster import KMeans as km
	import pandas as pd
	# 此处选用了改进版本的算法——k-means++
	model = km(n_clusters=k, n_jobs=4, init='k-means++')
	model.fit(d.values.reshape(len(d), 1))
	# 记录聚类中心,并以两两中心之间的中点作为区间的划分点
	cls = pd.DataFrame(model.cluster_centers_).sort_values(0)
	border = cls.rolling(2).mean().iloc[1:]
	border = [d.min()] + list(border[0]) + [d.max()]
	data_discrete = pd.cut(d, border, labels=cls)
	data[col] = data_discrete
	# 可以再定义一个函数将结果可视化
	cls_plot(data_discrete, k, data[col])
	return True

def cls_plot(d, k, data):
	import matplotlib.pyplot as plt
	# 解决一些符号无法显示的问题,个人习惯
	plt.rcParams['axes.unicode_minus'] = False
	plt.figure(figsize=(8, 3))
	for i in range(k):
		plt.plot(data[d==i], [i for item in d[d==i]], 'o')
	plt.show()
	return True

# 具体用法
k = 6
col = data.columns
index = [3, 4, 6, 7]
# for i in range(len(col) - 1):
# 可以对所有特征离散化,也可以指定若干个特征
for i in index:
	d = data[col[i]]
	cls_cus(d, k, data, col[i])

下图是个人在某个大作业中运行上述代码的部分可视化效果图:
基于聚类分析的离散化

  • 基于正态 3 σ \sigma 的离散化:简单粗暴地依据正态分布的特性,将变量划入下图的8个区间中
    – 在实际应用中,往往需要先对变量进行 log10 的转化
    离散化

有监督离散化:根据真实的数据类别信息

这一类方法似乎在 R 语言中使用更方便,代码示例也相对容易找些。

  • 基于信息增益的离散化:属于自上而下的分裂策略,灵感源于决策树的建立,不同地是此处为【单变量】决策树:对于单个变量,迭代地使用二分法找到若干个使信息增益最大的分割点,要求先给定叶子结点的个数(即划分的区间个数)
    – 建议先参考 这篇论文
    – 基本思想:记单个变量的所有取值为 v 1 , v 2 , . . . , v k {v_1, v_2, ..., v_k} ,依次取其中点,即 m i = v i + v i + 1 2 ( i = 1 , . . . , k 1 ) {m_i = \frac {v_i + v_{i+1}}{2} (i = 1, ..., k-1)} ;记预期的分割点为 a 1 , a 2 , . . . , a n ( n &lt; k ) {a_1, a_2, ..., a_n}(n &lt; k) ,对于每个 a i {a_i} ,迭代地从 m 1 , . . . , m k 1 {m_1, ..., m_{k-1}} 中(无放回地)选出使分裂前后信息增益最大的一个,重复 n 次。最终根据得到的 a 1 , . . . , a n {a_1, ..., a_n} 就是所求的离散化分割点。
    – 信息增益的定义: G a i n s p l i t = H ( p ) i = 1 k n i n H ( i ) Gain_{split} = H(p) - \sum_{i=1}^{k} {\frac {n_i}{n}H(i)} 其中 n 是父节点 p 的样本数量,ni 是子节点 i 的样本数量
    – 节点 t 的熵(不纯度): H ( t ) = j p ( j t ) log p ( j t ) H(t) = - \sum_{j} {p(j|t)\log {p(j|t)}}
    其中 p ( j t ) {p(j|t)} 是类 j 在节点 t 的相对频率
    – 这个在 python 无直接的实现,在 这篇文章 中用 sklearn 实现了 ID3 算法,其中用到了信息增益作为分裂标准,可以以此为参考尝试下对单个变量的类似操作
    – 根据决策树的特点来看,这种方法在叶子结点过多时容易过拟合
  • 基于卡方的离散化:属于自下而上的分裂策略,通过不断计算每一对相邻区间的卡方统计量,将足够相似的相邻区间合并,直至卡方统计量超过预设的阈值为止,要求先给定卡方统计量的阈值
    – 有兴趣可以看下 这篇用 R 语言实现的文章
    – 基本思想(Chi-Merge):将单变量中所有取值看作独立的区间,并对每一对相邻区间计算卡方统计量;在卡方统计量未超过给定阈值的前提下,迭代地合并邻居——优先合并卡方统计量小的邻居
    – 卡方统计量: χ 2 = i = 1 2 j = 1 k ( A i j E i j ) 2 E i j \chi^2=\sum_{i=1}^{2}{\sum_{j=1}^{k}{\frac {(A_{ij} - E_{ij})^2}{E_{ij}}}}
    – 缺点是每次循环只能归并两个区间,速度较慢;阈值过高易过度离散化,过低离散化不足

变量标准化

变量标准化的缘由:

  • 许多机器学习算法要求输入变量为标准化形式,如 SVM 中的 RBF 核函数,线性模型中的 L1正则项,往往假设其变量均值在 0 附近且方差齐次
  • 量纲问题:若某个属性的量级较大,通常也会对应着较大的方差,使得算法模型难以学习到量级较小的其他变量对因变量的影响

Z-Score 标准化

  • 将每个数据按照公式 x i = x i μ σ x_i^* = \frac {x_i - \mu}{\sigma} 处理成近似符合标准正态分布的形式

0-1 标准化

  • 又称离差标准化或最大-最小值标准化(MinMax)
  • 将每个数据按照公式 x i = x i m i n m a x m i n x_i^* = \frac {x_i - min}{max - min} 线性变换到区间 [0, 1] 内

小数定标标准化

  • 通过移动数据的小数点进行,即按照公式 x i = x i 1 0 j x_i^* = \frac {x_i}{10^j} 处理,其中 j 可取为对应属性下最大绝对值的位数

Logistic 标准化

  • 利用 Sigmoid 函数 x i = 1 1 + e x i x_i^* = \frac {1}{1 + e^{-x_i}} 将数据转化为区间 [0, 1] 之间的数

用表格比较个四种方法的优缺点和适用的范围如下:

标准化方法 优点 缺点 适用范围
Z-Score 转化为标准正态分布,无需数据的最值 需要记录均值和方差 数据中最值未知,且数据系列分布离散
0-1 线性变换,保留原始数据间的关系 若有新的数据加入导致新数据集的最值发生变化,需要重新计算 需要保留原始数据间的关系,且最值固定
小数定标 简单实用,易于还原 若数据的(绝对值的)最大值发生变化,需要重新计算 数据系列分布比较离散,尤其是遍布多个数量级
Logistic 简单易用,单一的映射方式 对分布离散且远离零点的数据处理效果不佳 数据系列分布比较集中,且均匀分布于零点两侧

这里提供我用的一种方法供你参考:

# 用 Z-scroe
from sklearn.preprocessing import StandardScaler
features_scale_std = StandardScaler()
features_train = features_scale_std.fit_transform(features_train)
features_test = features_scale_std.transform(features_test)
'''
# 用归一化(0-1)
from sklearn.preprocessing import  MinMaxScaler
features_scale_mm = MinMaxScaler()
features_train = features_scale_mm.fit_transform(features_train)
features_test = features_scale_mm.transform(features_test)
'''

数据抽样和过滤

一方面可能是为了降低数据量,使其能在相应的设备上处理;另一方面也是常用来处理数据不均衡问题的对策。

数据抽样

  • 随机抽样:从总体 N 个个体中,随机按照某种概率分布(比如均匀分布)抽取 n 个样本;样本多时效率低下
  • 系统抽样:将总体分成 n 个小组,每组有 N / n {\lfloor {N / n} \rfloor} 个样本,且都从数字 1 开始编号(可能出现不整除的情况,随机将多出的部分剔除即可),随机挑选一个数字,从 n 个小组中各取出对应数字的样本
  • 分层抽样:将总体人为地划分为多个类别,在每个类别中按照比例 n / N 进行随机抽样
  • 加权抽样:根据研究目的的不同,为每个样本确定不同的权重值再进行抽样
  • 整群抽样:将总体划分为若干个不重合的集合(群),以群为单位进行随机抽样,常用于大数据抽样(如调查大学生就业情况)

数据过滤

  • 选取满足某种条件的数据
  • 抽样 VS 过滤
    – 抽样主要依赖于随机化技术
    – 过滤是依据限制条件、仅保留符合要求的数据

由于实体识别、数据立方、数据仓库、特征工程等又是另外一个比较大的体系了,不适合放在同一篇文章中完成,后续想起来再看有没有能力吹一波吧 后边再单独写一篇 。

如果你有兴趣的话可以看下 系列(一),了解下数据分析领域的主要知识点,也非常欢迎留言告诉我你对此的见解,共同来完善这个系列~

猜你喜欢

转载自blog.csdn.net/weixin_42527725/article/details/86471888