本文的数据探索定义包括各类数据预处理、各类数据可视化、各种数据潜在规律的发现与记录等等,但是不包括特征选择、特征衍生等这部分。这部分单独列出来免得都写一块看起来太乱了也不好用手机翻阅。
为了方便说明,我们以经典的泰坦尼克数据集为来进行说明
第一步,确定问题,确认输入输出变量以及数据类型;
首先,你需要确认你的输入变量(特征矩阵X)和你的输出变量(标签y)是什么,接着,你需要确认数据的种类和分类。
import numpy as np
import pandas as pd
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
先简单概览一下数据全貌。
iseesee=train.head(100)
isee=test.head(100)
一般来说,原始的数据通过pandas或者numpy进来要不就是这种彩色标记的number类型的,要不就是非number类型的——包括object、datetime、category等,这里统一归纳为非number数据类型。
这里提前预览一下数据,看看有没有出现连续值中因为存在部分异常值而被判定为类型特征的情况,或者是其它的一些需要提前注意和笔记的地方,比如这里的Name特征,我们通过概览可以发现,”miss“和”Mrs“的存在可以看出某个女人是已婚还是未婚。
所以,概览一下数据,对于敏感的算法工程师来说是必不可少的步骤,敏锐的感觉很容易就能从中发现一些猫腻。
第二步,缺失值处理
这一步之所以放在第二步很多时候我们要画图也好、进行描述性统计也好,缺失值的存在会带来很多不必要的麻烦,所以为了后续处理方便最好提前把缺失值处理干净。
说到缺失值数据的处理方法,实际上细致一点来讨论可以出一本书了,事实上也确实有专门写缺失值数据处理的书——《flexible imputation of missing data》,没有中文版,后续有空会慢慢来翻译。(真是博大精深擦),这里我们就简单处理一下。
train.isnull().sum()
经过第一步,数据整体没有什么奇怪的影响后续分析的现象了,我们来查看缺失值的情况:age缺失了177个,Cabin缺失了687个,Embarked缺失了2个,当然也可以根据总样本数量确定缺失值率,一般来说大部分情况下缺失值率超过80%左右考虑删除,不超过80%的酌情考虑。
另外根据缺失的是连续型特征还是类别型特征or日期特征等要根据实际情况进行处理,但是考虑到缺失值处理如果使用均值会受极值影响,使用中位数可能受到极度有偏数据的影响,所以一般在这一步我是暂时先不处理统一使用np.nan来代替。
另外,在实际情况中,缺失值很多时候不一定是以nan的形式出现的,一般来说,连续型特征的缺失值都是以np.nan的形式为主,不会出现其它的什么幺蛾子,但是对于非数值型特征则缺失值的符号可能就是五花八门了,比如如果是name这种特征,可能是以‘missing’这种关键字来表示名字缺失,如果是日期特征,常常以‘0000-00-00’这类形式来表示缺失,对于属性值,可能会以‘-’这样的形式表示缺失,另外根据pandas的尿性,如果连续特征中出现了一些“missing”,‘NAN’之类的文本特征,则一整列都会被当成非number数据处理,这也是一开始要进行data.head()数据概览的重要原因。
(当然因为这里的篇幅原因,缺失值的详细处理我就不介绍了,实际上缺失值处理真的是很不简单的事情,举个例子,有的特征存在缺失值可能是因为这个缺失值根本没办法采集到,有的特征存在缺失值可能是因为人为因素等,根据不同的数据缺失的原因我们需要采取不同的缺失值处理的解决方式才是比较合理的,最好能够结合业务来进行处理最稳妥。)
最后,有些时候统计缺失值比例也可能是一个潜在的不错的特征。
处理完缺失值后可以先做一个简单的统计描述。
train.describe()
第三步 number数据和非number数据
先说一下number型数据,一般比较常见的是两种情况:float型和int型,一般来说int型数据大部分情况下属于有序离散特征,比如常见的年级,1~6年级从小到大,这类特征会比较特殊一些,因为你既可以把它当作number型的连续特征也可以当作非number型的类别特征来处理,具体展开与否取决于使用的算法、特征取值的数量大小等,对于逻辑回归这类算法比较常见的做法是展开增强lr的表达能力,高维空间更容易线性可分。
(至于为什么维度越高越容易线性可分,注意这里说的是维度越高,线性可分的概率越大,不是说维度越高就一定越线性可分,https://en.wikipedia.org/wiki/Cover%27s_theorem 计算学习理论中的cover定理证明了维度越高则线性可分的概率越高,假设一个极端的情况就是n个样本当我们扩展到n-1维的时候则必然存在线性可分平面)
先来看看怎么处理float的连续型特征:
一、下面写介绍单特征的数据探索和两个特征的数据探索,超过两个的特征之间的交互探索会麻烦一些单独列一块来写
(1)、异常值的观察与处理:常见的就是用箱形图和小提琴图来肉眼观察,具体的教程和箱型图绘制原理可见
Seaborn入门系列(三)——boxplot和violinplotwww.jianshu.comimport matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(10,5))
sns.boxplot(x=train.Fare,data=train,fliersize=2.5,width=0.3,orient="v")
这里很明显可以看出,当Fare的值超过大概90的时候就属于异常值了。
另外我们还可以根据类别特征来观察不同类别对应的异常值的情况,例如:
plt.figure(figsize=(10,5))
sns.boxplot(x=train.Survived,y=train.Fare,fliersize=2.5,width=0.3,orient="v")
这样我们就可以看出不同类别特征下的异常情况,当然这里类别特征用的是标签的二分类值,如果换成Pclass。
plt.figure(figsize=(10,5))
sns.boxplot(x=train.Pclass,y=train.Fare,fliersize=2.5,width=0.3,orient="v")
明显可以看出,Pclass中的类别1的异常情况比较严重,根据这条线索我们可以继续深入挖掘,这就是数据探索的魅力,帮助我们发现模型无法发现的知识然后再将其表达成模型可以学习的形式。
关于异常值处理,实际上也有一本专门的教材进行了非常完整的介绍。。。。我还花了100块钱翻译成中文了呢。。有空介绍吧。异常值处理的问题之所以没有大范围的推广主要是现有的算法精度不够,不好支持业务决策,商业化价值并不是很高,大部分情况下还是用于数据预处理这块会多一些。有空我再搬到知乎上来吧。不得不说,翻译狗是真的贵,效果也很不好,不知道各位看观有没有知道什么免费翻译外文书籍的软件,免费的那种,可以一次性翻译全部的那种。。。。
目前来说,对于异常值的处理,主要有:
1、不处理,例如对于gbdt这类对异常值不敏感的算法来说不太需要处理;
2、把异常值的处理用缺失值的处理的思路来处理,比如均值、众数插补之类的;
3、分箱,风控系统中,使用lr的时候很常用的处理手段;
4、最稳妥的办法还是根据业务知识和实际情况来处理,比如有时候异常值很可能是错误的导入导致的,这个时候最好是去数据源重新导入一份正常的数据等;
5、有待补充,目前就想到这么多。
(2)、连续值自身的分布情况以及相对于目标值的分布情况:以前是直接用plt.hist,然后直方图数设置的很大,后来发现了seaborn有更好的处理方案
fig = plt.figure(figsize=(10,5))
ax=sns.kdeplot(train.Fare ,
color='gray',
shade=True,
label='Pclass')
我一开始很纳闷这么平滑的曲线是怎么画出来的,后来查了一下原来原理是:
当然,我们需要挖掘的更加细致一点,比如这里是分类问题,我们可以分别换一下两种类别的Fare的分布情况:(补充:用sns.displot也可以,多了直方图的形状)
fig = plt.figure(figsize=(10,5))
ax=sns.kdeplot(train.Fare ,
color='gray',
shade=True,
label='Fare')
ax=sns.kdeplot(train[train.Survived==0].Fare ,
color='red',
shade=True,
label='Not Survived Fare')
ax=sns.kdeplot(train[train.Survived==1].Fare ,
color='green',
shade=True,
label='Survived Fare')
从上图我们可以看到,not survived的人里面,买低价的Fare的人占比很高,而survived的人里面则占比相对要低,说明买低价的Fare的人相对来说更容易死,和《泰坦尼克》电影结合起来思考好像也确实是,有钱人优先求生,没钱的人等死,黑暗的世道啊。
因为这里的target是分类的离散值,如果是回归问题可以画二者的分布图,这里我们用年龄来代替画一下试试,考虑到年龄中有缺失值,seaborn不会把这些点画出来:
fig = plt.figure(figsize=(10,5))
sns.scatterplot(train.Age,train.Fare)
如果为了看的更清楚可以用:
fig = plt.figure(figsize=(10,5))
sns.regplot(train.Age,train.Fare)
自动回归,不过这里很明显没有什么线性的关系。
二、下面介绍两个以上的特征之间的交互探索:
用seaborn实现起来也是无比简单,多一个‘hue’的参数就可以了。例如
fig = plt.figure(figsize=(10,5))
sns.scatterplot('Age','Fare',hue='Survived',data=train)
相对于之前写的单纯的Fare和Age之间的关系,这种3特征交互图更加直观,我们可以看出,活下来的人大都是票价不低的人,而死去的人大都是票价低的人,这个结论和我们之前得到的下图的结论是类似的:
另外一个比较常见的就是热力图:
fig = plt.figure(figsize=(10,5))
sns.heatmap(data=train.corr())
接下来我们看看怎么处理int的离散型特征以及类别特征:
有序的int特征很多时候也可以像上面的float型特征那样处理这里就不赘述了。
fig = plt.figure(figsize=(10,5))
sns.scatterplot('Sex','Fare',hue='Survived',data=train)
我们来谈一谈关于这类离散的特征的一些不同的处理方案:
fig = plt.figure(figsize=(10,5))
sns.countplot('Sex',hue='Survived',data=train)
当然,如果画成和标签值相关是最好的了。
fig = plt.figure(figsize=(10,5))
sns.countplot('Sex',hue='Survived',data=train)
可以看到,男人明显比女人容易死多了。。。。。太惨了。
另外针对于scatter的问题有改进的方案:
fig = plt.figure(figsize=(10,5))
sns.stripplot('Sex','Fare',hue='Survived',data=train)
可以看出,男人中买低价票的人数的占比很高,女人则相对好一点,一般女人都对自己比较好,男人都对自己比较狠,所以这么来看好像很符合逻辑。
更复杂一些的计数图
fig = plt.figure(figsize=(10,5))
sns.barplot('Pclass','Fare',hue='Survived',data=train)
暂时就先写这些,这样整理一下以后数据探索就知道该做点啥了,要不然每次都比较萌比,另外: