之前的应用中,把数据塞进模型前的部分统统简单粗暴地称之为“洗数据”。在这次的学习中,初步接触了“探索性学习”和“特征工程”两个概念。在我的理解中,探索性学习偏向数据本身,目的性较弱,主要是为了认识、了解数据本身的一些特性。而特征工程则和业务知识理解结合较为紧密,目的明确,一方面基于业务理解,尽可能可能找出对因变量有影响的所有自变量;另一方面基于前期对数据的探索,对原始数据进行加工,通过清洗、变换、组合、筛选,优化原始数据的特征,产生新的、相关性更高的特征,从而提高预测的准确性
在开始本次学习之前,一直认为机器学习的重点在于选择、使用合适的模型,根据模型和训练反馈选用、调整参数。但是随着学习的深入,发现特别在比赛中,大家使用的模型都差不太多,调参水平也差不太多,再加上融合,基本上决定分数的关键就变成了特征工程。当然,在实际数据分析项目中,貌似机器学习方法一直被传统数学建模碾压,期待通过学习,能够更好地将机器学习与实际项目结合起来。
import numpy as np
import pandas as pd
np.set_printoptions(suppress=True)
pd.set_option("max_columns",1000,"display.max_colwidth",20)
path = "./tianchi/"
Train_data = pd.read_csv(path+"used_car_train_20200313.csv",sep = " ")
Test_data = pd.read_csv(path+"used_car_testA_20200313.csv",sep = " ")
Datawhale入门手册的优秀已无须多言,群上各路大佬们也基本上把前进路上的雷排了个干净。于是我一路行云流水地完成了删除异常值和构造“汽车使用时间”特征,来到了“提取城市信息”阶段。
#从邮编中提取城市信息,相当于加入了先验知识
Train_data['city'] = Train_data['regionCode'].apply(lambda x : str(x)[:-3])
print(Train_data['city'].value_counts())
36680
1 31886
2 26481
3 20545
4 14969
5 10047
6 6304
7 2986
8 102
Name: city, dtype: int64
什么?这个“city”是个什么鬼?于是我又翻开了天池官方说明页,上面清楚地写着:“regionCode”,地区编码,已脱敏。既然不是原始的邮编,这样直接切片有业务知识依据么?我再次打开了天池官方说明。很遗憾,这次什么有用的信息都没有查找到。
于是抱着探索的心态,我又掉转头来开始观察数据。
print(Train_data['regionCode'].value_counts().sort_index())
print(Train_data['regionCode'].value_counts().shape[0])
0 63
1 17
2 26
3 30
4 65
5 39
6 40
7 7
8 25
9 7
10 45
11 35
12 43
13 31
14 28
15 12
16 62
17 51
18 47
19 11
20 19
…
8072 1
8073 2
8077 1
8078 2
8081 1
8082 1
8084 1
8086 1
8087 1
8088 2
8090 1
8092 1
8093 1
8094 1
8097 1
8100 1
8102 1
8103 2
8104 1
8105 1
8106 1
8107 1
8109 1
8112 1
8113 1
8117 1
8120 1
Name: regionCode, dtype: int64
7905
通过对“regionCode”字段的数据进行去重和排序,惊讶地发现它几乎是一列连续的整数!当然,通过行数的查看我们发现,并不是完全连续的,最大编号8120,但是只有7905行,中间缺了两百多个编号,占比2.5%,不知道这个比例对数据连续性影响大不大,暂且当作连续数据来考虑吧。
根据我脑子里的陈年地理常识和生活常识,觉得邮编貌似不应该这么连续流畅,而应该是有一套映射规则的。面对这列如此连续的数据,我深深地怀疑那个取“city”的操作取出来的9个分类具体代表了什么意义。。。直接拿SaleID,不也能划出个分类吗?对我们的预测有意义吗?
但是,这么稀疏的高维矩阵,直接拿来预测,似乎很难操作。接下来该怎么办呢?有人提出聚类。嗯,好像很有道理的样子。先来观察一下数据:
import matplotlib.pyplot as plt
plt.scatter(Train_data['regionCode'], Train_data['price'])
plt.show()
plt.close()
Emmm,这么密实的一坨,真是kmeans看了会沉默,DBSCAN看了会流泪。。。
兜兜转转一圈,还是没有找到好的分类方式。。。好在小雨大佬终于睡醒了,费劲地看了我辞不达意的描述,想了想,建议我先画个图看看,按照city的分类,价格是否有明显差异。听上去好有道理,于是我一顿操作。。。等等,好像被大佬带偏了,我要问的难道不是“为什么这个regionCode第三位能代表区域”的吗?SaleID也是顺序编号,用它来划分一下,和regionCode有什么不同呢?带着这个疑问,我给大佬画了个对比图。。。
plt.figure(figsize=(14,6),dpi=80)
#将city处理为int型,空格置0,方便坐标轴表示
Train_data['city'] = Train_data['regionCode'].apply(lambda x : int(str(x)[:-3]) if len(str(x)) > 3 else 0)
ax1 = plt.subplot(121)
plt.scatter(Train_data['city'], Train_data['price'])
#引入SaleID(交易流水号),同样切块,作为分布参照
Train_data['SB'] = Train_data['SaleID'].apply(lambda x : str(x)[1:2])
ax2 = plt.subplot(122)
plt.scatter(Train_data['SB'], Train_data['price'])
plt.show()
plt.close()
小雨大佬终于明白了我的意思,一脸云淡风轻地表示,这个就是随便猜分析出来的。。。但是同时表示,大部分稀疏数据这样处理是没有意义的,但是像邮编这样有一定先验知识的数据,可以试着取前1位、前2位,这样猜一猜。是吗?终于解锁了入门手册中的隐藏技能——猜?
本着严谨务实的态度,按照小雨大佬的指示精神,我又不辞辛劳地把regionCode的4位数细细地切了一遍,又顺手捎上了SaleID做对照。嗯,对销售编号进行value_counts()的操作过于白痴,为了避免看起来就是宛如一个智障,这里把它删掉了。。。
#分别取regionCode和SaleID倒数第4位
Train_data['city1'] = Train_data['regionCode'].apply(lambda x : int(str(x)[:-3]) if len(str(x)) > 3 else 0)
Train_data['SB1'] = Train_data['SaleID'].apply(lambda x : str(x)[-4:-3])
#为了方便对城市分类的交易数量统计可视化,需要将Series转换为DataFrame,并将索引还原为列
city1_counts = Train_data['city1'].value_counts().to_frame().reset_index()
#为了显示分析的严谨性,在这里查看一下相关系数,注意.corr()得到的是一个np数组类型
city1_corr = float(Train_data['city1'].corr(Train_data['price'], method='spearman'))
SB1_corr = Train_data['SB1'].corr(Train_data['price'], method='spearman')
plt.figure(figsize=(21,6),dpi=80)
ax1 = plt.subplot(131)
plt.scatter(Train_data['city1'], Train_data['price'])
plt.title('city1,corr:' + str(city1_corr))
ax2 = plt.subplot(132)
plt.scatter(Train_data['SB1'], Train_data['price'])
plt.title('SB1,corr:' + str(SB1_corr))
ax3 = plt.subplot(133)
plt.bar(city1_counts['index'], city1_counts['city1'])
plt.title('city1_amount')
plt.show()
plt.close()
#分别取regionCode和SaleID倒数第3位
Train_data['city2'] = Train_data['regionCode'].apply(lambda x : int(str(x)[-3:-2]) if len(str(x)) > 3 else 0)
Train_data['SB2'] = Train_data['SaleID'].apply(lambda x : str(x)[-3:-2])
#为了方便对城市分类的交易数量统计可视化,需要将Series转换为DataFrame,并将索引还原为列
city2_counts = Train_data['city2'].value_counts().to_frame().reset_index()
#为了显示分析的严谨性,在这里查看一下相关系数,注意.corr()得到的是一个np数组类型
city2_corr = float(Train_data['city2'].corr(Train_data['price'], method='spearman'))
SB2_corr = Train_data['SB2'].corr(Train_data['price'], method='spearman')
plt.figure(figsize=(21,6),dpi=80)
ax1 = plt.subplot(131)
plt.scatter(Train_data['city2'], Train_data['price'])
plt.title('city2,corr:' + str(city2_corr))
ax2 = plt.subplot(132)
plt.scatter(Train_data['SB2'], Train_data['price'])
plt.title('SB2,corr:' + str(SB2_corr))
ax3 = plt.subplot(133)
plt.bar(city2_counts['index'], city2_counts['city2'])
plt.title('city2_amount')
plt.show()
plt.close()
#分别取regionCode和SaleID倒数第2位
Train_data['city3'] = Train_data['regionCode'].apply(lambda x : int(str(x)[-2:-1]) if len(str(x)) > 3 else 0)
Train_data['SB3'] = Train_data['SaleID'].apply(lambda x : str(x)[-2:-1])
#为了方便对城市分类的交易数量统计可视化,需要将Series转换为DataFrame,并将索引还原为列
city3_counts = Train_data['city3'].value_counts().to_frame().reset_index()
#为了显示分析的严谨性,在这里查看一下相关系数,注意.corr()得到的是一个np数组类型
city3_corr = float(Train_data['city3'].corr(Train_data['price'], method='spearman'))
SB3_corr = Train_data['SB3'].corr(Train_data['price'], method='spearman')
plt.figure(figsize=(21,6),dpi=80)
ax1 = plt.subplot(131)
plt.scatter(Train_data['city3'], Train_data['price'])
plt.title('city3,corr:' + str(city3_corr))
ax2 = plt.subplot(132)
plt.scatter(Train_data['SB3'], Train_data['price'])
plt.title('SB3,corr:' + str(SB3_corr))
ax3 = plt.subplot(133)
plt.bar(city3_counts['index'], city3_counts['city3'])
plt.title('city3_amount')
plt.show()
plt.close()
#分别取regionCode和SaleID倒数第1位
Train_data['city4'] = Train_data['regionCode'].apply(lambda x : int(str(x)[-1:]) if len(str(x)) > 3 else 0)
Train_data['SB4'] = Train_data['SaleID'].apply(lambda x : str(x)[-1:])
#为了方便对城市分类的交易数量统计可视化,需要将Series转换为DataFrame,并将索引还原为列
city4_counts = Train_data['city4'].value_counts().to_frame().reset_index()
#为了显示分析的严谨性,在这里查看一下相关系数,注意.corr()得到的是一个np数组类型
city4_corr = float(Train_data['city4'].corr(Train_data['price'], method='spearman'))
SB4_corr = Train_data['SB4'].corr(Train_data['price'], method='spearman')
plt.figure(figsize=(21,6),dpi=80)
ax1 = plt.subplot(131)
plt.scatter(Train_data['city4'], Train_data['price'])
plt.title('city4,corr:' + str(city4_corr))
ax2 = plt.subplot(132)
plt.scatter(Train_data['SB4'], Train_data['price'])
plt.title('SB4,corr:' + str(SB4_corr))
ax3 = plt.subplot(133)
plt.bar(city4_counts['index'], city4_counts['city4'])
plt.title('city4_amount')
plt.show()
plt.close()
感觉自己像一个老农,辛勤地一行一行刨着地。貌似应该自定义一个函数,用1-2个for循环来搞定?呃,其实我还没想出来怎么实现我们的重点不在这里,大家将就看看吧。
开始观察结果数据!有些尴尬。。。从散点图和交易数的柱状图来看,貌似取倒数第4位的区分度是最高的,但是看看相关系数。。。为了看得更直观,把它们塞到同一张图里。
x_city = ['city1_corr','city2_corr','city3_corr','city4_corr']
y_city = [city1_corr,city2_corr,city3_corr,city4_corr]
plt.bar(x_city,y_city,width = 0.8,color = 'c',label = 'city')
x_SB = ['SB1_corr','SB2_corr','SB3_corr','SB4_corr']
y_SB = [SB1_corr,SB2_corr,SB3_corr,SB4_corr]
plt.bar(x_SB,y_SB,width = 0.8,color = 'orange',label = 'SB')
plt.show()
plt.close()
看起来regionCode的后面两位和价格更相关?可是这相关性,貌似也低得可怜。所以,是不是应该放弃这个数据?感觉比找对象更困难的,是找到有用特征。。。
总结:
1. 相比数据挖掘的其它过程,特征工程是最没有现成套路,最体现脑洞的。其间充满了艰辛的探索过程,和许多愚蠢的失败。
2. 和同伴一起讨论,思考,确实可以学习到很多知识和思路。当然,这些属于碎片化的学习,还需要通过自己的动手实践,来真正掌握它,将其整合到自己的知识体系中。
3. 向大佬请教问题,一定要事先准备好代码和图,以免鸡同鸭讲,暴露大佬智商浪费大佬时间。
感谢:
1. 感谢Ivan,他对数据的敏锐感觉和探索思路给了我许多启发,文中的问题是由他首先提出的,我只是进行了后面的探索部分。
2. 感谢Crazy、闫钟峰两位同学,和我一起探讨这个问题,给了我许多好的建议。文中某行代码的操作小技巧来自Crazy。
3. 感谢小雨姑娘,他(你没有看错!是“他”,不是“她”!)让我打开了数据分析的新思路,得以一窥数据特征处理的更高境界。
4. 感谢Datawhale,让我遇见这些志同道合的小伙伴,让我的学习探索之旅更加顺利。
by 六一