数据预处理的重要性:
- 由于数据采集技术的局限、传输过程的错误等原因,采集的原始数据通常存在不完整、含噪(包含错误或离群值)、不一致(编码不同、属性与内容对不上)、冗余(样本重复、属性之间可相互推导)等问题
- 模型输入数据的质量直接影响了建模的效果,尤其是在追求最后的一丢丢优化的时候
广义上的数据预处理还包含了【数据质量管理】这一过程,但对于个人而言感觉这概念太抽象了,所以本文只包含了数据预处理的常规步骤:数据清洗,数据集成,数据转换和数据归约。下面先用一个表格来说明这四个步骤的任务和常见的处理技术之间的关系。
步骤 | 主要任务 | 相关技术 |
---|---|---|
数据清洗 | 补充缺失,平滑噪声,识别或删除离群点/异常值,解决数据不一致问题 | 缺失值处理,异常值检测,错误发现与修复 |
数据集成 | 集成多个数据库、数据立方或文件 | 实体识别,冗余和相关分析,数据仓库(大数据专用) |
数据转换 | 规范化和聚集 | 变量离散化,变量标准化 |
数据归约 | 特征选择和采样 | 特征选择,特征提取,数据抽样,数据过滤 |
注意:在实际的处理过程上,不一定4个步骤都需要;而且如果你看过数据分析(一)的话你会很容易发现其实不同步骤之间存在交集。这不是重点,分析步骤的划分只是为了便于学习理解,在实际处理过程中通常是一个交织循环的过程。问题来了:怎么知道该如何“交织循环”呢?无它,多逛逛优秀的博客,看看大牛们是怎么做的吧。
从上面的表格你也许已经看出来了,【步骤】是提纲挈领用的,【主要任务】是提醒分析目标用的,【相关技术】才是技术人的根基。所以接下来的内容是就主要的技术部分进行介绍。
数据的缺失就是指 完全没有采集到数据 ,至于数据乱码、数据错误等不算在此列,而是归在异常检测和错误修复部分。处理缺失有两种通用的方法:删除法和填补法。
''' 可用 pandas.DataFrame.dropna 清除缺失值,更多用法请查看官方文档 '''
# 删除有缺失的样本,此处可用 thresh 参数来限制仅删除非缺失值少于 thresh 个的对象
data['有缺失的列'] = data['有缺失的列'].dropna(axis='index', thresh=5)
# 删除有缺失的属性
data['有缺失的列'] = data['有缺失的列'].dropna(axis='columns')
''' 在 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])
这里推荐 一篇总结比较全面的文章 ,建议与本文相互对照下
''' 在 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)
# 后续的处理缺失值过程自行脑补
' ...... '
符号 | 含义 |
---|---|
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} lrdk(A)=1/∣Nk(A)∣∑O∈Nk(A)reach−distancek(A,O),其中,分母表示点 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) LOFk(A)=∣Nk(A)∣∑O∈Nk(A)lrdk(A)lrdk(O)=∣Nk(A)∣∑O∈Nk(A)lrdk(O)/lrdk(A),即点 A 的第 k 邻域内所有点的【平均局部可达密度】与点 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)
离散化的缘由:
- 将连续型数值分段,可能将异常值直接划入相应区段,从而强化了模型对异常值的鲁棒性
- 离散化后每个取值均对应一个有明确含义的区段/区号,可解释性变强了
- 最重要的是【特征的取值个数】大大减少了,对于模型存储空间和运行效率都有改善
''' 比如可以考虑用 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])
这一类方法似乎在 R 语言中使用更方便,代码示例也相对容易找些。
变量标准化的缘由:
- 许多机器学习算法要求输入变量为标准化形式,如 SVM 中的 RBF 核函数,线性模型中的 L1正则项,往往假设其变量均值在 0 附近且方差齐次
- 量纲问题:若某个属性的量级较大,通常也会对应着较大的方差,使得算法模型难以学习到量级较小的其他变量对因变量的影响
用表格比较个四种方法的优缺点和适用的范围如下:
标准化方法 | 优点 | 缺点 | 适用范围 |
---|---|---|---|
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)
'''
一方面可能是为了降低数据量,使其能在相应的设备上处理;另一方面也是常用来处理数据不均衡问题的对策。
由于实体识别、数据立方、数据仓库、特征工程等又是另外一个比较大的体系了,不适合放在同一篇文章中完成,后续想起来再看有没有能力吹一波吧 后边再单独写一篇 。
如果你有兴趣的话可以看下 系列(一),了解下数据分析领域的主要知识点,也非常欢迎留言告诉我你对此的见解,共同来完善这个系列~