MENU

数据的探索性(EDA)分析

March 24, 2020 • Read: 705 • 数据挖掘与机器学习阅读设置

该文章基于天池二手车交易价格预测比赛进行学习

大纲如下:

  • 数据初识(对数据做初步探索,查看字段类型,相关统计量等)
  • 数据感知(在初识的基础上进一步挖掘信息,主要包括数据的确实情况和异常情况)
  • 数据不惑(在前两个基础上进一步挖掘,包括查看预测值的分布和字段的类型判断)
  • 数据洞玄(对数值特征和类别特征分开挖掘,包括类别偏斜,类别分布可视化,数值相关等各种可视化技巧)
  • 数据知命(介绍pandas_profiling数据探索性分析的神器,通过这个神器可以对上面的信息进行整合)
  • 总结

首先导入一些包和数据集

import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
import seaborn as sns
import missingno as msno # 缺失值的可视化处理

"""导入数据集"""
train_data = pd.read_csv('./dataset/used_car_train_20200313.csv', sep=' ') # 指定分隔符为空格
test_data = pd.read_csv('./dataset/used_car_testA_20200313.csv', sep=' ')

1. 数据初识

这里主要是对读取的数据有一个大致的了解,包括简单了解数据的行列信息,数据的统计特征等

# 查看数据的形状(行数和列数)
print('train_data shape :', train_data.shape) # (150000, 31)
print('test_data shape :', test_data.shape) # (50000, 30)

# 数据简要概览
train_data.head().append(train_data.tail()) # 将开头5行和结尾5行拼接起来展示,head()和tail()默认值是5
test_data.head().append(test_data.tail())

从形状可以看出训练集共150000个样本,30个特征,1列价格;测试集共50000个样本,30个特征

# 数据信息的查看 .info()可以看到每列的type,以及NAN缺失值的信息
train_data.info()

通过info()可以发现几点信息,首先就是字段的类型,有一个object(后面需要单独处理)。其次有一些字段有空值,清洗的时候需要处理

通过info()了解数据每列的type,还有助于了解是否存在除了nan以外的特殊符号异常


# 通过 .columns 查看列名
train_data.columns

# 数据的统计信息概览
train_data.describe()

describe()中有每列的统计值,包括:个数count、平均值mean、方差std、最小值min、下四分位数25%、中位数50%、上四分位数75%、以及最大值max。查看这些信息可以瞬间掌握数据的大概范围以及异常值判断,例如999999,-1这些值其实是nan的另一种表达方式,有时候需要注意下

注意看左下角,提示有30列,但是刚才输出shape的时候明明提示有31列。注意,describe()是不包括object类型字段的统计信息的,毕竟不是数值类型。当然你也可以单独用describe()看看

train_data['notRepairedDamage'].describe()

# 结果:
count     150000
unique         3
top          0.0
freq      111361
Name: notRepairedDamage, dtype: object

可以看出,object类型的统计信息和数值类型的不太一样

2. 数据感知

数据感知是在数据初识的基础上,进一步挖掘数据的信息,主要包括数据的确实值和异常值等

# 查看每列存在nan的情况
train_data.isnull().sum()
# test_data.isnull().sum()

可以看出,modelbodyTypefuelTypegearbox有缺失值。还可以对nan进行可视化,看的更加明显

missing = train_data.isnull().sum()
missing = missing[missing > 0]
missing.sort_values(inplace=True)
missing.plot.bar()

可视化nan的个数主要目的在于,查看nan存在的个数是否真的很大,如果很小一般选择填充,如果使用lgb等树模型可以直接让树自己去优化,但如果nan存在的过多,可以考虑删掉

下面是可视化缺失值

# 可视化缺失值
msno.matrix(train_data.sample(250)) # sample(250)表示抽取250个样本
# msno.matrix(test_data.sample(250))

# msno.bar(train_data.sample(1000))
# msno.bar(test_data.sample(1000))

下图是代码运行后得到的结果,白线越多,代表缺失值越多(fuleType缺失的最多)

对数据持着怀疑的角度审视,尤其是object字段

# 看看object这个字段的取值情况
train_data['notRepairedDamage'].value_counts()

# 结果
0.0    111361
-       24324
1.0     14315
Name: notRepairedDamage, dtype: int64

这个字段里面居然有个-值,如果单看比赛给的字段描述:0代表有未修复的损害,1代表没有,如果我们不持着怀疑的态度,很难发现这里还有个-,这个也代表缺失,因为很多模型可以对nan直接处理,所以这里我们先将-替换为nan

train_data['noRepairedDamage'].replace('-', np.nan, inplace=True)
# test_data['noRepairedDamage'].replace('-', np.nan, inplace=True)

3. 数据不惑

通过初识和感知,我们不仅认识了数据,害发现了一些异常和缺失,下面我们进一步挖掘数据信息,主要包括查看预测值的分布以及 将字段分成数值型和类别型,后面分开查看和处理

3.1 了解预测值的分布

"""查看预测值的频数"""
train_data['price'].value_counts()

# 直方图可视化 自动划分10(默认值)个价格区间 统计每个区间的频数
plt.hist(train_data['price'], orientation='vertical', histtype='bar', color='red')
plt.show()

查看频数,发现价格大于20000的值极少,其实这里也可以把这些值当作特殊值(或异常值)直接删掉,不过直接删掉不太好,毕竟这是个回归问题

"""总体分布概况(无界约翰逊分布等)"""
import scipy.stats as st
y = train_data['price']

plt.figure(figsize=(20, 5))
plt.subplot(1, 3, 1)
plt.title('Johnson SU')
sns.distplot(y, kde=False, fit=st.johnsonsu)

plt.subplot(1, 3, 2)
plt.title('normal')
sns.distplot(y, kde=False, fit=st.norm)

plt.subplot(1, 3, 3)
plt.title('Log Normal')
sns.distplot(y, kde=False, fit=st.lognorm)

可以发现,价格不服从正态分布,所以在进行回归之前,必须将它进行转换。最佳拟合的是无界约翰逊分布

# log变换之后的分布会变得比较均匀,可以进行log变换进行预测,这也是预测问题常用的trick
plt.hist(np.log(train_data['price']), orientation='vertical', histtype='bar', color='red')
plt.show()

对预测标签做log转换,使其更加服从正态分布

"""查看偏度和峰度"""
sns.distplot(train_data['price'])
print('Skewness : %f' % train_data['price'].skew())
print('Kurtosis : %f' % train_data['price'].kurt())

# 结果
Skewness : 3.346487
Kurtosis : 18.995183

# train_data.skew(), train_data.kurt()
plt.figure(figsize=(20, 5))
plt.subplot(121)
sns.distplot(train_data.skew(), color='blue', axlabel='Skewness')
plot.subplot(122)
sns.distplot(train_data.kurt(), color='orange', axlabel='Kurtness')

峰度Kurt代表数据分布的尖锐程度,偏度简单来说就是数据的不对称程度,skew、kurt说明参考这篇博客

3.2 把字段分为数值字段和类别字段

"""先分离出label值"""
y_train = train_data['price']

# 数值特征
# numeric_features = train_data.select_dtypes(include=[np.number])
# numeric_features.columns

# 类别特征
# categorical_features = train_data.select_dtypes(include=[np.object])
# categorical_features.columns

上面是自动选取的方式,也可以人为设定

"""人为设定"""
numeric_features = ['power', 'kilometer'].extend(['v_'+str(i) for i in range(15)])

# 这里我感觉这个name和预测值没有关系,所以虽然是类别,可以先去掉看看, 日期的也去掉
categorical_features = ['model', 'brand', 'bodyType', 'fuelType', 'gearbox', 
                        'notRepairedDamage','regionCode', 'seller', 'offerType']

4. 数据洞玄

前面的工作我们已经分析了预测值的分布,从分布中我们看到,如果把预测值进行对数变化一下,效果可能更好。然后我们又把特征字段拆分为数值型和类别型。接下来我们主要对数值特征和类别特征进一步挖掘信息,包括类别偏斜,类别分布可视化,数值可视化等

4.1 类别特征的探索

类别特征主要是看一下每个类别字段的取值和分布,会用到箱型图、小提琴图、柱状图等各种可视化技巧

"""类别偏斜处理"""
for cat_fea in categorical_features:
    print(cate_fea + '特征分布如下:')
    print('{}特征有{}不同的值'.format(cate_fea, len(train_data[cat_fea].unique())))
    print(train_data[cat_fea].value_counts())
    print()

这里主要是重点查看一下类别特征有没有数量严重偏斜的情况(由于太多,不在这里显示),这样的情况一般对预测没有什么帮助

train_data['seller'].value_counts()

0    149999
1         1
Name: seller, dtype: int64
train_data['offerType'].value_counts()

0    150000
Name: offerType, dtype: int64

sellerofferType字段偏斜就比较严重,直接删除这些字段

del train_data['seller']
del train_data['offerType']
del test_data['seller']
del test_data['offerType']

categorical_features.remove('seller')
categorical_features.remove('offerType')

下面看一下每个字段,有多少类(unique)

"""类别的unique分布"""
for cat in categorical_features:
    print(len(train_data[cat].unique()))

# 结果
249
40
9
8
3
3
7905

# 因为regionCode的类别太稀疏了,所以先去掉,因为后面要可视化,不画稀疏的
categorical_features.remove('regionCode')

下面使用各种可视化方式,可视化类别特征

"""类别特征箱型图可视化"""
for c in categorical_features:
    train_data[c] = train_data[c].astype('category')
    if train_data[c].isnull().any():
        train_data[c] = train_data[c].cat.add_categories(['MISSING'])
        train_data[c] = train_data[c].fillna('MISSING')

def boxplot(x, y, **kwargs):
    sns.boxenplot(x=x, y=y)
    x = plt.xticks(rotation=90)

f = pd.melt(train_data, id_vars=['price'], value_vars=categorical_features)
g = sns.FacetGrid(f, col="variable",  col_wrap=3, sharex=False, sharey=False, size=5)
g = g.map(boxplot, "value", "price")

"""类别特征的小提琴图可视化, 小提琴图类似箱型图,比后者高级点,图好看些"""
catg_list = categorical_features
target = 'price'
for catg in catg_list :
    sns.violinplot(x=catg, y=target, data=train_data)
    plt.show()

"""类别特征的柱形图可视化"""
def bar_plot(x, y, **kwargs):
    sns.barplot(x=x, y=y)
    x=plt.xticks(rotation=90)

f = pd.melt(train_data, id_vars=['price'], value_vars=categorical_features)
g = sns.FacetGrid(f, col="variable",  col_wrap=3, sharex=False, sharey=False, size=5)
g = g.map(bar_plot, "value", "price")

看一下柱形图的结果(小提琴的不在这展示)

"""类别特征的每个类别频数可视化(count_plot)"""
def count_plot(x,  **kwargs):
    sns.countplot(x=x)
    x=plt.xticks(rotation=90)

f = pd.melt(train_data,  value_vars=categorical_features)
g = sns.FacetGrid(f, col="variable",  col_wrap=3, sharex=False, sharey=False, size=5)
g = g.map(count_plot, "value")

这里面用到了一个melt函数,这是个转换函数,参考这篇博客

4.2 数值特征的探索

数值特征的探索我们要分析相关性等,也会学习各种相关性可视化的技巧

numeric_train_data = train_data[numeric_features]

# 把price这一列加上,这个也是数值
numeric_train_data['price'] = Y_train

"""相关性分析"""
correlation = numeric_train_data.corr()
print(correlation['price'].sort_values(ascending=False), '\n')   # 与price相关的特征排序

.corr()可以看到每个特征与price的相关性,并且排了个序。下面进行相关性可视化,使用热力图比较合适

# 可视化
f, ax = plt.subplots(figsize=(10,10))
plt.title('Correlation of Numeric Features with Price', y=1, size=16)
sns.heatmap(correlation, square=True, vmax=0.8)

# 删除price
del numeric_train_data['price']

"""查看几个数值特征的偏度和峰度"""
for col in numeric_train_data.columns:
     print('{:15}'.format(col), 
          'Skewness: {:05.2f}'.format(numeric_train_data[col].skew()) , 
          '   ' ,
          'Kurtosis: {:06.2f}'.format(numeric_train_data[col].kurt())  
         )

"""每个数字特征得分布可视化"""
f = pd.melt(train_data, value_vars=numeric_features)
g = sns.FacetGrid(f, col="variable",  col_wrap=5, sharex=False, sharey=False)
g = g.map(sns.distplot, "value")

数值特征的分布可视化,从这里可以看到数值特征的分布情况,其中匿名特征的分布相对均匀

"""数字特征相互之间的关系可视化"""
sns.set()
columns = ['price', 'v_12', 'v_8' , 'v_0', 'power', 'v_5',  'v_2', 'v_6', 'v_1', 'v_14']
sns.pairplot(train_data[columns],size = 2 ,kind ='scatter',diag_kind='kde')
plt.show()

这里面会看到有些特征之间是相关的, 比如v_1和v_6

"""多变量之间的关系可视化"""
fig, ((ax1, ax2), (ax3, ax4), (ax5, ax6), (ax7, ax8), (ax9, ax10)) = plt.subplots(nrows=5, ncols=2, figsize=(24, 20))
# ['v_12', 'v_8' , 'v_0', 'power', 'v_5',  'v_2', 'v_6', 'v_1', 'v_14']
v_12_scatter_plot = pd.concat([Y_train,train_data['v_12']],axis = 1)
sns.regplot(x='v_12',y = 'price', data = v_12_scatter_plot,scatter= True, fit_reg=True, ax=ax1)

v_8_scatter_plot = pd.concat([Y_train,train_data['v_8']],axis = 1)
sns.regplot(x='v_8',y = 'price',data = v_8_scatter_plot,scatter= True, fit_reg=True, ax=ax2)

v_0_scatter_plot = pd.concat([Y_train,train_data['v_0']],axis = 1)
sns.regplot(x='v_0',y = 'price',data = v_0_scatter_plot,scatter= True, fit_reg=True, ax=ax3)

power_scatter_plot = pd.concat([Y_train,train_data['power']],axis = 1)
sns.regplot(x='power',y = 'price',data = power_scatter_plot,scatter= True, fit_reg=True, ax=ax4)

v_5_scatter_plot = pd.concat([Y_train,train_data['v_5']],axis = 1)
sns.regplot(x='v_5',y = 'price',data = v_5_scatter_plot,scatter= True, fit_reg=True, ax=ax5)

v_2_scatter_plot = pd.concat([Y_train,train_data['v_2']],axis = 1)
sns.regplot(x='v_2',y = 'price',data = v_2_scatter_plot,scatter= True, fit_reg=True, ax=ax6)

v_6_scatter_plot = pd.concat([Y_train,train_data['v_6']],axis = 1)
sns.regplot(x='v_6',y = 'price',data = v_6_scatter_plot,scatter= True, fit_reg=True, ax=ax7)

v_1_scatter_plot = pd.concat([Y_train,train_data['v_1']],axis = 1)
sns.regplot(x='v_1',y = 'price',data = v_1_scatter_plot,scatter= True, fit_reg=True, ax=ax8)

v_14_scatter_plot = pd.concat([Y_train,train_data['v_14']],axis = 1)
sns.regplot(x='v_14',y = 'price',data = v_14_scatter_plot,scatter= True, fit_reg=True, ax=ax9)

v_13_scatter_plot = pd.concat([Y_train,train_data['v_13']],axis = 1)
sns.regplot(x='v_13',y = 'price',data = v_13_scatter_plot,scatter= True, fit_reg=True, ax=ax10)

关于可视化的更多学习,可以参考这篇博客

5. 数据知命

这里会综合上面的这些过程,用pandas_profiling这个包使用函数ProfileReport生成一份数据探索性报告, 在这里面会看到:

  • 总体的数据信息(首先是数据集信息:变量数(列)、观察数(行)、数据缺失率、内存;数据类型的分布情况)
  • 警告信息

    • 类型,唯一值,缺失值
    • 分位数统计量,如最小值,Q1,中位数,Q3,最大值,范围,四分位数范围
    • 描述性统计数据,如均值,模式,标准差,总和,中位数绝对偏差,变异系数,峰度,偏度
  • 单变量描述(对每一个变量进行描述)
  • 相关性分析(皮尔逊系数和斯皮尔曼系数)
  • 采样查看等
# 两行简单的代码即可搞定上面的这些信息
pfr = ppf.ProfileReport(train_data)
pfr.to_file("./EDA.html")

6. 总结

今天通过围绕着二手车价格预测的比赛,从五个维度整理了一下数据探索性分析的相关知识,下面根据思维导图进行回顾

Archives Tip
QR Code for this page
Tipping QR Code