思维导图:零基础入门数据挖掘的学习路径
1. 写在前面
零基础入门数据挖掘是记录自己在Datawhale举办的数据挖掘专题学习中的所学和所想, 该系列笔记使用理论结合实践的方式,整理数据挖掘相关知识,提升在实际场景中的数据分析、数据清洗,特征工程、建模调参和模型融合等技能。所以这个系列笔记共五篇重点内容, 也分别从上面五方面进行整理学习, 既是希望能对知识从实战的角度串联回忆,加强动手能力的锻炼,也希望这五篇笔记能够帮助到更多喜欢数据挖掘的小伙伴,我们一起学习,一起交流吧。
既然是理论结合实践的方式,那么我们是从天池的一个二手车交易价格预测比赛出发进行学习,既可以学习到知识,又可以学习如何入门一个数据竞赛, 下面我们开始吧。
今天是本系列的第三篇文章数据的清晰和转换技巧,依然是围绕着上面的比赛进行展开。数据清洗和转换在数据挖掘中也非常重要,毕竟数据探索我们发现了问题之后,下一步就是要解决问题,通过数据清洗和转换,可以让数据变得更加整洁和干净,才能进一步帮助我们做特征工程,也有利于模型更好的完成任务。如果看了零基础数据挖掘入门系列(二) - 数据的探索性(EDA)分析, 就会发现数据存在下面的一些问题(当然可能不全,欢迎补充交流)
- 有异常值, 可以用箱线图锁定异常, 但是有时候不建议直接把样本删除,截尾的方式可能会更好一些
- 有缺失值,尤其是类别的那些特征(bodyType, gearbox, fullType, notRepaired的那个)
- 类别倾斜的现象(seller, offtype), 考虑删除
- 类别型数据需要编码
- 数值型数据或许可以尝试归一化和标准化的操作
- 预测值需要对数转换
- power高度偏斜,这个处理异常之后对数转换试试
- 隐匿特征的相关性
- 存在高势集model, 及类别特征取值非常多, 可以考虑使用聚类的方式,然后在独热编码
所以今天的重点是在处理上面的问题上,整理数据清洗和转换的技巧,以方便日后查阅迁移。首先是处理异常数据,通过箱线图捕获异常点,然后截尾处理, 然后是整理一些处理缺失的技巧, 然后是数据分桶和数据转换的一些技巧, 下面我们开始。
大纲如下:
- 异常值处理(箱线图分析删除,截尾,box-cox转换技术)
- 缺失值处理(不处理, 删除,插值补全, 分箱)
- 数据分桶(等频, 等距, Best-KS分桶,卡方分桶)
- 数据转换(归一化标准化, 对数变换,转换数据类型,编码等)
- 知识总结
Ok, let’s go!
准备工作:数据清洗的时候,我这里先把数据训练集和测试集放在一块进行处理,因为我后面的操作不做删除样本的处理, 如果后面有删除样本的处理,可别这么做。 数据合并处理也是一个trick, 一般是在特征构造的时候合并起来,而我发现这个问题中,数据清洗里面训练集和测试集的操作也基本一致,所以在这里先合起来, 然后分成数值型、类别型还有时间型数据,然后分别清洗。
"""把train_data的price先保存好"""
train_target = train_data['price']
del train_data['price']
"""数据合并"""
data = pd.concat([train_data, test_data], axis=0)
data.set_index('SaleID', inplace=True)
"""把数据分成数值型和类别型还有时间型,然后分开处理"""
"""人为设定"""
numeric_features = ['power', 'kilometer']
numeric_features.extend(['v_'+str(i) for i in range(15)])
categorical_features = ['name', 'model', 'brand', 'bodyType', 'fuelType', 'gearbox',
'notRepairedDamage','regionCode', 'seller', 'offerType']
time_features = ['regDate', 'creatDate']
num_data = data[numeric_features]
cat_data = data[categorical_features]
time_data = data[time_features]
trick1: 就是如果发现处理数据集的时候,需要训练集和测试集进行同样的处理,不放将它们合并到一块处理。
trick2: 如果发现特征字段中有数值型,类别型,时间型的数据等,也不妨试试将它们分开,因为数值型,类别型,时间型数据不管是在数据清洗还是后面的特征工程上, 都是会有不同的处理方式, 所以这里将训练集合测试集合并起来之后,根据特征类型把数据分开, 等做完特征工程之后再进行统一的整合(set_index把它们的索引弄成一样的,整合的时候就非常简单了)。
2. 异常值处理
常用的异常值处理操作包括箱线图分析删除异常值, BOX-COX转换(处理有偏分布), 长尾截断的方式, 当然这些操作一般都是处理数值型的数据。
关于box-cox转换,一般是用于连续的变量不满足正态的时候,在做线性回归的过程中,一般线性模型假定, 其中ε满足正态分布,但是利用实际数据建立回归模型时,个别变量的系数通不过。例如往往不可观测的误差 ε 可能是和预测变量相关的,不服从正态分布,于是给线性回归的最小二乘估计系数的结果带来误差,为了使模型满足线性性、独立性、方差齐性以及正态性,需改变数据形式,故应用box-cox转换。具体详情这里不做过多介绍,当然还有很多转换非正态数据分布的方式:
- 对数转换:
- 平方根转换:
- 倒数转换:
- 平方根后取倒数:
- 平方根后再取反正弦:
- 幂转换:, 其中, 参数
在一些情况下(P值<0.003)上述方法很难实现正态化处理,所以优先使用Box-Cox转换,但是当P值>0.003时两种方法均可,优先考虑普通的平方变换。
Box-Cox的变换公式:
具体实现:
from scipy.stats import boxcox
boxcox_transformed_data = boxcox(original_data)
当然,也给出一个使用案例:使用scipy.stats.boxcox完成BoxCox变换
好了,BOX-COX就介绍这些吧, 因为这里处理数据先不涉及这个变换,我们回到这个比赛中来,通过这次的数据介绍一下箱线图筛选异常并进行截尾:
从上面的探索中发现,某些数值型字段有异常点,可以看一下power这个字段:
# power属性是有异常点的
num_data.boxplot(['power'])
结果如下:
所以,我们下面用箱线图去捕获异常,然后进行截尾, 这里不想用删除,一个原因是我已经合并了训练集和测试集,如果删除的话肯定会删除测试集的数据,这个是不行的, 另一个原因就是删除有时候会改变数据的分布等,所以这里考虑使用截尾的方式:
"""这里包装了一个异常值处理的代码,可以随便调用"""
def outliers_proc(data, col_name, scale=3):
"""
用于截尾异常值, 默认用box_plot(scale=3)进行清洗
param:
data: 接收pandas数据格式
col_name: pandas列名
scale: 尺度
"""
data_col = data[col_name]
Q1 = data_col.quantile(0.25) # 0.25分位数
Q3 = data_col.quantile(0.75) # 0,75分位数
IQR = Q3 - Q1
data_col[data_col < Q1 - (scale * IQR)] = Q1 - (scale * IQR)
data_col[data_col > Q3 + (scale * IQR)] = Q3 + (scale * IQR)
return data[col_name]
num_data['power'] = outliers_proc(num_data, 'power')
我们再看一下数据:
是不是比上面的效果好多了?当然,如果想删除这些异常点,这里是来自Datawhale团队的分享代码,后面会给出链接,也是一个模板:
def outliers_proc(data, col_name, scale=3):
"""
用于清洗异常值,默认用 box_plot(scale=3)进行清洗
:param data: 接收 pandas 数据格式
:param col_name: pandas 列名
:param scale: 尺度
:return:
"""
def box_plot_outliers(data_ser, box_scale):
"""
利用箱线图去除异常值
:param data_ser: 接收 pandas.Series 数据格式
:param box_scale: 箱线图尺度,
:return:
"""
iqr = box_scale * (data_ser.quantile(0.75) - data_ser.quantile(0.25))
val_low = data_ser.quantile(0.25) - iqr
val_up = data_ser.quantile(0.75) + iqr
rule_low = (data_ser < val_low)
rule_up = (data_ser > val_up)
return (rule_low, rule_up), (val_low, val_up)
data_n = data.copy()
data_series = data_n[col_name]
rule, value = box_plot_outliers(data_series, box_scale=scale)
index = np.arange(data_series.shape[0])[rule[0] | rule[1]]
print("Delete number is: {}".format(len(index)))
data_n = data_n.drop(index)
data_n.reset_index(drop=True, inplace=True)
print("Now column number is: {}".format(data_n.shape[0]))
index_low = np.arange(data_series.shape[0])[rule[0]]
outliers = data_series.iloc[index_low]
print("Description of data less than the lower bound is:")
print(pd.Series(outliers).describe())
index_up = np.arange(data_series.shape[0])[rule[1]]
outliers = data_series.iloc[index_up]
print("Description of data larger than the upper bound is:")
print(pd.Series(outliers).describe())
fig, ax = plt.subplots(1, 2, figsize=(10, 7))
sns.boxplot(y=data[col_name], data=data, palette="Set1", ax=ax[0])
sns.boxplot(y=data_n[col_name], data=data_n, palette="Set1", ax=ax[1])
return data_n
这个代码是直接删除数据,这个如果要使用,不要对测试集用哈。下面看看power这个特征的分布:
也不错了,所以就没再进一步处理power,至于其他的数值型是不是需要截尾,这个看自己吧。
3. 缺失值处理
关于缺失值处理的方式,这里也是先上方法, 有几种情况:不处理(这是针对xgboost等树模型), 有些模型有处理缺失的机制,所以可以不处理,如果缺失的太多,可以考虑删除该列, 另外还有插值补全(均值,中位数,众数,建模预测,多重插补等), 还可以分箱处理,缺失值一个箱。
下面整理几种填充值的方式:
# 删除重复值
data.drop_duplicates()
# dropna()可以直接删除缺失样本,但是有点不太好
# 填充固定值
train_data.fillna(0, inplace=True) # 填充 0
data.fillna({0:1000, 1:100, 2:0, 4:5}) # 可以使用字典的形式为不用列设定不同的填充值
train_data.fillna(train_data.mean(),inplace=True) # 填充均值
train_data.fillna(train_data.median(),inplace=True) # 填充中位数
train_data.fillna(train_data.mode(),inplace=True) # 填充众数
train_data.fillna(method='pad', inplace=True) # 填充前一条数据的值,但是前一条也不一定有值
train_data.fillna(method='bfill', inplace=True) # 填充后一条数据的值,但是后一条也不一定有值
"""插值法:用插值法拟合出缺失的数据,然后进行填充。"""
for f in features:
train_data[f] = train_data[f].interpolate()
train_data.dropna(inplace=True)
"""填充KNN数据:先利用knn计算临近的k个数据,然后填充他们的均值"""
from fancyimpute import KNN
train_data_x = pd.DataFrame(KNN(k=6).fit_transform(train_data_x), columns=features)
# 还可以填充模型预测的值, 这一个在我正在写的数据竞赛修炼笔记的第三篇里面可以看到,并且超级精彩,还在写
再回到这个比赛中,我们在数据探索中已经看到了缺失值的情况:
上图可以看到缺失情况, 都是类别特征的缺失,notRepaired这个特征的缺失比较严重, 可以尝试填充, 但目前关于类别缺失,感觉上面的方式都不太好,所以这个也是一个比较困难的地方,感觉用模型预测填充比较不错,后期再说吧,因为后面的树模型可以自行处理缺失。 当然OneHot的时候,会把空值处理成全0的一种表示,类似于一种新类型了。
4. 数据分桶
连续值经常离散化或者分离成“箱子”进行分析, 为什么要做数据分桶呢?
- 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
- 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
- LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
- 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;
- 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
当然还有很多原因,LightGBM 在改进 XGBoost 时就增加了数据分桶,增强了模型的泛化性
数据分桶的方式:
- 等频分桶
- 等距分桶
- Best-KS分桶(类似利用基尼指数进行二分类)
- 卡方分桶
最好是数据分桶的特征作为新一列的特征,不要把原来的数据给替换掉, 所以在这里通过分桶的方式做一个特征出来看看,以power为例
"""下面以power为例进行分桶, 当然构造一列新特征了"""
bin = [i*10 for i in range(31)]
num_data['power_bin'] = pd.cut(num_data['power'], bin, labels=False)
当然这里的新特征会有缺失。
这里也放一个数据分桶的其他例子(迁移之用)
# 连续值经常离散化或者分离成“箱子”进行分析。
# 假设某项研究中一组人群的数据,想将他们进行分组,放入离散的年龄框中
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
# 如果按年龄分成18-25, 26-35, 36-60, 61以上的若干组,可以使用pandas中的cut
bins = [18, 25, 35, 60, 100] # 定义箱子的边
cats = pd.cut(ages, bins)
print(cats) # 这是个categories对象 通过bin分成了四个区间, 然后返回每个年龄属于哪个区间
# codes属性
print(cats.codes) # 这里返回一个数组,指明每一个年龄属于哪个区间
print(cats.categories)
print(pd.value_counts(cats)) # 返回结果是每个区间年龄的个数
# 与区间的数学符号一致, 小括号表示开放,中括号表示封闭, 可以通过right参数改变
print(pd.cut(ages, bins, right=False))
# 可以通过labels自定义箱名或者区间名
group_names = ['Youth', 'YonngAdult', 'MiddleAged', 'Senior']
data = pd.cut(ages, bins, labels=group_names)
print(data)
print(pd.value_counts(data))
# 如果将箱子的边替代为箱子的个数,pandas将根据数据中的最小值和最大值计算出等长的箱子
data2 = np.random.rand(20)
print(pd.cut(data2, 4, precision=2)) # precision=2 将十进制精度限制在2位
# qcut是另一个分箱相关的函数, 基于样本分位数进行分箱。 取决于数据的分布,使用cut不会使每个箱子具有相同数据数量的数据点,而qcut,使用
# 样本的分位数,可以获得等长的箱
data3 = np.random.randn(1000) # 正太分布
cats = pd.qcut(data3, 4)
print(pd.value_counts(cats))
结果如下:
5. 数据转换
数据转换的方式, 数据归一化(MinMaxScaler), 标准化(StandardScaler), 对数变换(log1p), 转换数据类型(astype), 独热编码(OneHotEncoder),标签编码(LabelEncoder), 修复偏斜特征(boxcox1p)等。关于这里面的一些操作,我有几篇博客已经描述过了,后面会给出链接, 这里只针对这个问题进行阐述:
- 数值特征这里归一化一下, 因为我发现数值的取值范围相差很大
minmax = MinMaxScaler()
num_data_minmax = minmax.fit_transform(num_data)
num_data_minmax = pd.DataFrame(num_data_minmax, columns=num_data.columns, index=num_data.index)
- 类别特征独热一下
"""类别特征某些需要独热编码一下"""
hot_features = ['bodyType', 'fuelType', 'gearbox', 'notRepairedDamage']
cat_data_hot = pd.get_dummies(cat_data, columns=hot_features)
- 关于高势集特征model,也就是类别中取值个数非常多的, 一般可以使用聚类的方式,然后独热,这里就采用了这种方式:
from scipy.cluster.hierarchy import linkage, dendrogram
#from sklearn.cluster import AgglomerativeClustering
from sklearn.cluster import KMeans
ac = KMeans(n_clusters=3)
ac.fit(model_price_data)
model_fea = ac.predict(model_price_data)
plt.scatter(model_price_data[:,0], model_price_data[:,1], c=model_fea)
cat_data_hot['model_fea'] = model_fea
cat_data_hot = pd.get_dummies(cat_data_hot, columns=['model_fea'])
效果如下:
但是我发现KMeans聚类不好,可以尝试层次聚类试试,并且这个聚类数量啥的应该也会有影响,这里只是提供一个思路,我觉得这个特征做的并不是太好,还需改进。
数据清洗和转换的技巧就描述到这里,但是不能说是结束,因为这一块的知识没有什么固定的套路,我们得学会发散思维,然后不断的试错探索,这里只整理的部分方法。
trick3: 通过上面的方式处理完了数据之后,我们要记得保存一份数据到文件,这样下次再用的时候,就不用再花功夫处理,直接导入清洗后的数据,进行后面的特征工程部分即可。一定要养成保存数据到文件的习惯。
6. 总结
今天主要是整理一些数据清洗和转换的技巧,包括异常处理,缺失处理,数据分箱和数据转换操作, 这些技巧也同样不仅适用于这个比赛,还可以做迁移。依然是利用思维导图把知识进行串联:
关于经验的话,数据清洗和转换这一块只能整理一些方法,然后需要针对具体的数据不断的尝试, 只有亲自尝试才能获得更多的成长,没有什么固定的套路或者说方式,没有什么循规蹈矩的规定,当然也希望不要把思维限定在上面的这些方法中,因为毕竟目前我也是小白,这些只是我目前接触到的一些,所以肯定不会包括所有的方式,希望有大佬继续补充和交流。
另外,我觉得分享本身就是一种成长,因为分享知识在帮助自己加深记忆的同时,也是和别人进行思维碰撞的机会,这个过程中会遇到很多志同道合的人一起努力,一起进步,这样比一个人要好的多。 一个人或许会走的很快,但是一群人才能走的更远,所以希望这个系列能帮助更多的伙伴,也希望学习路上可以遇到更多的伙伴, 你看,天上太阳正晴,我们一起吧 😉
对了,上面的数据清洗过程,再做几个特征,可以让误差降低60多,也算是有点用吧,不过不是太理想,还需要进一步探索这块, 希望和更多的小伙伴一起试错,一起交流,然后一起成长。
参考:
- Datawhale 零基础入门数据挖掘-Task3 特征工程
- 离散数据编码方式总结(OneHotEncoder、LabelEncoder、OrdinalEncoder、get_dummies、DictVectorizer、to_categorical的区别?)
- 真的明白sklearn.preprocessing中的scale和StandardScaler两种标准化方式的区别吗
- 打AI比赛的模板整理
- 类别型数据的预处理方法
- 缺失值填充的几种方法
- 机器学习中的异常值检测和处理
- 特征预处理和特征生成 (三)缺失值的处理
- 机器学习sklearn(13)层次聚类
- 数据清洗与准备