汽车价格离群值检测案例

《数据科学导引》汽车价格离群值检测案例

第二章案例4(评论可以私发数据表)



前言

离群点(Outliers),简单而言就是离其余数据点非常远的数据点。它们会极大的影响后续的分析结果,甚至产生有误导的分析结果。

Vast向3个行业的出版商、市场和搜索引擎提供数据,这三个行业包括汽车、房地产和休闲、住宿和旅游。Vast的系统通过白标签集成,并在一些非常受欢迎的消费应用程序(Southwest GetAway Finder,AOL Travel,Yahoo! Travel,Car and Driver等等)中提供搜索结果、产品建议和特别优惠。

Vast的汽车数据是由成千上万的二手汽车卖家提供,并且向市场公布。由于这些数据是由用户手工录入,因此容易受到人为失误的影响,比如用户在错误的字段中提交值,或者无意中发生错误或胖手指值。 对于8岁的车辆,里程表读数为100,000英里。 直觉告诉我们,100,000美元是大多数小型车的不寻常的价格。 一个上市的42000美元是合理的,比如2013年的凯迪拉克ATS豪华版,而对另一个(例如1997年的别克莱斯布雷)来说,这可能是意料之外的。

检测离群值有利于纠正错误,向用户提供卓越和合适的产品。


一、数据集描述

数据集包括训练数据集和测试数据集:

  • accord_sedan_testing.csv
  • accord_sedan_training.csv

测试数据集accord_sedan_training.csv包含了417辆本田雅阁轿车的信息列表。各个特征如下:

特征名称含义类型取值示例
price价格int14995
mileage已行驶英里数int67697
year上市年份int2006
trim档次strexl:高配款且带皮革内饰 ; ex:高配款 ; lx:低配款
engine引擎缸数int4 Cyl:4缸 ; 6 Cyl:6缸
transmission换挡方式strManual:手动挡 ; Automatic:自动挡

二、导入数据集并切分

import pandas as pd  #数据分析包##读取数据表并进行基于DataFrame(交互式展示表格)结构的操作
import numpy as np #函数库
import seaborn as sns #绘图库
from matplotlib import pyplot as plt #调用matplotlib库中的pyplot功能包并重命名为plt(绘图)
from sklearn import preprocessing #数据预处理
from sklearn.feature_extraction import DictVectorizer #特征抽取(字典特征抽取)
from sklearn.linear_model import LinearRegression #引入线性回归模型
#内嵌画图
%matplotlib inline 

import warnings
warnings.filterwarnings('ignore') #不显示warning信息
pd.options.display.width = 900 #Dataframe显示宽度设置

train = pd.read_csv('accord_sedan_training.csv')
print 'The shape is',train.shape
train.head(7)

# 将 train 划分为 x 和 y
train_x = train.drop('price',1)#剔除price,axis=1表示二维数组
train_y = train['price']

三、特征提取并构建线性回归模型

使用sklearn.feature_extraction中的DictVectorizer将名义型变量实现One-Hot编码,得到一个10维向量空间,包括:

  • engine=4 Cyl
  • engine=6 Cyl
  • mileage
  • price
  • transmission=Automatic
  • transmission=Manual
  • trim=ex
  • trim=exl
  • trim=lx
  • year

注意:在异常值检测时需要利用到 price 特征。

# One-Hot编码:使用`DictVectorizer`实现
dv = DictVectorizer()
dv.fit(train.T.to_dict().values())#调用fit函数,导入特征提取化中的字典向量化#sparse=True,即稀疏矩阵
print ('Dimension is',len(dv.feature_names_))
dv.feature_names_

在这里插入图片描述
也可以使用pandas的get_dummies函数实现one-hot编码:

# One-Hot编码:使用`pandas`的`get_dummies`函数实现
nomial_var = ['engine','trim','transmission']#名词解释变量
multi_dummies = [] # 存储三个 DataFrame
train_x_dummies = train_x[['mileage','year']]
for col in nomial_var:#for...in...循环结构
    dummies = pd.get_dummies(train_x[col], prefix = col)#pd.get_dummies特征提取,其中参数prefix表示自定义新列名称,前缀可以是将列名映射到前缀的字典。
    train_x_dummies = pd.concat([train_x_dummies,dummies],axis=1)#将编码结果与非编码特征水平拼接起来。axis = 1表示行对齐,将不同列名称的两张表合并
train_x_dummies.head()

在这里插入图片描述

##构建线性回归模型
train_x_array = dv.transform(train_x.T.to_dict().values())
# train_x_array = train_x_dummies.values #也可以使用get_dummies得到的结果

LR = LinearRegression().fit(train_x_array,train_y)#创建线性回归模型,拟合数据
' + '.join([format(LR.intercept_, '0.2f')] #format字符串格式化函数,%0.2f保留小数点后两位
           + list(map(lambda x: "(%0.2f %s)" % (x[1],x[0]), 
               zip(dv.feature_names_, LR.coef_))))
#map函数和lambda函数连用用于指定对列表中每一个元素的共同操作。一个返回值为保留小数点后两位,另一个返回值为字符串
#这里由于python2与python3的兼容性,对map和lambda函数进行了更新,解决办法如上。

在这里插入图片描述
即拟合得到的模型为:

price≈12084.24−337.20(engine=4Cyl)+337.20(engine=6Cyl)−0.05(mileage)+420.68(transmission=Automatic)−420.67(transmission=Manual)+208.93(trim=ex)+674.60(trim=exl)−883.53(trim=lx)+2.23(year)

四、离群值检测

由测试集拟合得到的模型,我们可以预测测试集中的价格,计算每个样本的绝对误差,并得出

#离群值检测
pred_y = LR.predict(dv.transform(train.T.to_dict().values()))#由测试集拟合得到的模型,我们可以预测测试集中的价格
train_error = abs(pred_y - train_y) # 计算绝对误差
np.percentile(train_error,[75,90,95,99]) # 计算绝对误差数据的百分位数

##np.percentile用法
##np.percentile(a, q, axis=None, out=None, overwrite_input=False, interpolation='linear', keepdims=False) 

在这里插入图片描述
我们也可以画出 train_error 的盒图来观察离群值。

sns.boxplot(x = train_error,palette = "Set2")#train_error的箱线图,参数palette即调色板,控制图像的色调

在这里插入图片描述

可以看到测试集中存在部分离群值。

在本案例中,我们设定置信水平为0.95,即认为超过95%百分位数的train_error为离群值。下面我们在二维空间中画出正常值(蓝色)与离群值(红色):

outlierIndex = train_error >= np.percentile(train_error, 95)#置信水平为0.95,超过95%的为离群值
inlierIndex = train_error < np.percentile(train_error, 95)#置信水平小于95%的不是离群值

# 得到train_error最大的index值,即极端离群值
most_severe = train_error[outlierIndex].idxmax() 

fig = plt.figure(figsize=(7,7))#绘图函数plt.figure,参数figsize指定宽和高
indexes = [inlierIndex, outlierIndex, most_severe]#行名
color = ['#2d9ed8','#EE5150','#a290c4']
label = ['normal points', 'outliers', 'extreme outliers']
for i,c,l in zip(indexes,color,label):#并行遍历
    plt.scatter(train['mileage'][i], 
                train_y[i], 
                c=c,#颜色
                marker='^',#正三角形
                label=l)
plt.legend(loc = 'upper right',#图例位置:右上角
           frameon=True,#显示图例边框
           edgecolor='k',
           framealpha=1,#控制图例框的透明度
           fontsize = 12)#字体大小
plt.xlabel('$mileage$')
plt.ylabel('$price$')
plt.grid('on')#网格线设置
sns.set_style('dark')#主题风格darkgrid,whitegrid,dark,white,ticks

在这里插入图片描述
我们来看看离群值的数量有多少?

outlierIndex.value_counts()#显示离群值的数量

在这里插入图片描述
上图结果也符合我们的经验理解,二手车的行驶公里数越高,它卖出去的价格就应该越低,所以对于处在右上和左下区域的点可能是一些离群值(对于同一款车而言)。比如左下区域的点,一些行驶里程数低,价格也比较低的车辆,有可能该车辆是事故车辆或者有损坏,而右上区域的离群值有可能是真实的离群值,相对来讲不容易有合理的解释,可能是输入失误或者胖手指输入造成。

本案例中的数据只有400多条,如果数据再多一些,则检测的结果会更加可靠。

五、标准化对离群值检测的影响

通常情况下,为了避免不同尺度的影响。我们在进行线性回归模型拟合之前,需要对各个特征进行标准化。常见的标准化有z-score标准化、0-1标准化等,这里我们选择z-score标准化来观察标准化对离群值检测的影响。

##选择z-score标准化来观察标准化对离群值检测的影响
# 利用 preprocessing.scale函数将特征标准化
##函数用法sklearn.preprocessing.scale(X, axis=0, with_mean=True, with_std=True, copy=True)

columns = train_x_dummies.columns#列名
train_x_zscore = pd.DataFrame(preprocessing.scale(train_x_dummies),columns = columns)
#train_y_zscore = pd.DataFrame(preprocessing.scale(pd.DataFrame(train_y,columns=['price'])),columns = ['price'])
#其中第一个参数是存放在DataFrame里的数据,第二个参数columns是列名。

# 线性模型拟合(释义参考前面)
LR_zscore = LinearRegression().fit(train_x_zscore.values,train_y)
' + '.join([format(LR_zscore.intercept_, '0.2f')] 
           + list(map(lambda x: "(%0.2f %s)" % (x[1], x[0]), 
               zip(dv.feature_names_, LR_zscore.coef_))))

在这里插入图片描述

pred_y_zscore = LR_zscore.predict(train_x_zscore)#由测试集拟合得到的模型来预测测试集中标准化后的价格
train_error_zscore = abs(pred_y_zscore - train_y) # 计算绝对误差
np.percentile(train_error_zscore,[75,90,95,99]) # 计算绝对误差数据的百分位数

在这里插入图片描述

outlierIndex_zscore = train_error_zscore >= np.percentile(train_error_zscore, 95)#置信水平超过95%为离群值
inlierIndex_zscore = train_error_zscore < np.percentile(train_error_zscore, 95)
diff = (outlierIndex_zscore != outlierIndex)#diff用于存储标准化前后的离群值检测结果不同的index(行名)
#!=即不等于,用于比较两个对象是否不相等
diff.value_counts()

在这里插入图片描述

# 画出标准化前后的检测差异点
fig = plt.figure(figsize=(7,7))

# rep_inlierIndex为标准化前后都为正常值的index
rep_inlierIndex = (inlierIndex == inlierIndex_zscore)

indexes = [rep_inlierIndex, outlierIndex, outlierIndex_zscore]
color = ['#2d9ed8','#EE5150','#a290c4']
markers = ['^','<','>']#正三角形,左三角形,右三角形
label = ['inliers', 'outliers before z-score', 'outliers after z-score']
for i,c,m,l in zip(indexes,color,markers,label):
    plt.scatter(train['mileage'][i], 
                train_y[i], 
                c=c,
                marker=m,
                label=l)
plt.xlabel('$mileage$')
plt.ylabel('$price$')
plt.grid('on')
plt.legend(loc = 'upper right',
           frameon=True,
           edgecolor='k',
           framealpha=1,
           fontsize = 12)
sns.set_style('dark')

在这里插入图片描述
从结果可以看到,绝大多数样本的检测结果一致。有两个样本存在差别,其中一个样本在标准化之前会被检测为离群值,另外一个样本在标准化之后会被检测为离群值。虽然在本例中,标准化前后的检测效果差异不是很大,我们仍然建议在线性建模之前对特征进行标准化。

六、测试集的验证

我们先以 mileage 为横坐标, price 为纵坐标画出训练集和测试集的所有样本点。

#测试集的验证
test = pd.read_csv('accord_sedan_testing.csv')

datasets = [train,test]
color = ['#2d9ed8','#EE5150']
label = ['training set', 'testing set']#标签
fig = plt.figure(figsize=(7,7))
for i,c,l in zip(range(len(datasets)),color,label):#遍历所有的点
    plt.scatter(datasets[i]['mileage'], 
                datasets[i]['price'], 
                c=c,
                marker='^',
                label=l)
plt.xlabel('$mileage$')
plt.ylabel('$price$')
plt.grid('on')
plt.legend(loc = 'upper right',
           frameon=True,
           edgecolor='k',
           framealpha=1,
           fontsize = 12)
sns.set_style('dark')

在这里插入图片描述

我们来看看利用在训练集上训练得到的模型在测试集上的泛化效果:

pred_y_test = LR.predict(dv.transform(test.T.to_dict().values()))#由测试集拟合得到的模型,预测测试集中的价格
test_error = abs(pred_y_test - test['price'])#计算绝对误差

# 使用分布图观察测试集误差
fig = plt.figure(figsize=(7,7))
sns.distplot(test_error,kde=False)#kde不显示核密度
plt.xlabel('$test\_error$')
plt.ylabel('$count$')
plt.grid('on')

在这里插入图片描述

# 找出极端离群值
most_severe_test = test_error.idxmax()
test.iloc[most_severe_test]#iloc指定行名和列名

在这里插入图片描述
从分布图中可以看到,我们的模型对测试集上其中一个样本的预测表现非常差。该样本是一个极端的离群样本。该车是一个6缸高配版的车,并且其已行驶英里数只有~60,000英里左右,但是其卖出的价格才$2612。

根据经验,我们猜测这个离群样本出现的两种可能:

  1. 在网站里填写时出错;
  2. 该车辆有车体的损伤或者有汽车所有权问题(偷来的或者劫来的)

七、在测试集上使用LOF进行离群值检测

test = pd.read_csv('accord_sedan_testing.csv')
fig = plt.figure(figsize=(7,7))
plt.scatter(test['mileage'], 
            test['price'], 
            c='#EE5150',
            marker='^',
            label='testing set')
plt.xlabel('$mileage$')
plt.ylabel('$price$')
plt.grid('on')
plt.legend(loc = 'upper right',
           frameon=True,
           edgecolor='k',
           framealpha=1,
           fontsize = 12)
sns.set_style('dark')

在这里插入图片描述
可达密度计算公式:
l r d k ( x ) = ( 1 k ∑ y ∈ N k ( x ) r d k ( x , y ) ) − 1 lrd_k(x)=(\frac{1}{k} \sum_{y∈N_k(x)}rd_k(x,y))^{-1} lrdk(x)=(k1yNk(x)rdk(x,y))1
其中 r d k ( x , y ) rd_k(x,y) rdk(x,y)样本点 x 到样本点 y 的第 k 可达距离

LOF因子计算公式:
l o f k ( x ) = 1 k ∑ y ∈ N k ( x ) l r d k ( y ) l r d k ( x ) lof_k(x)=\frac{1}{k} \sum_{y∈N_k(x)}\frac{lrd_k(y)}{lrd_k(x)} lofk(x)=k1yNk(x)lrdk(x)lrdk(y)
LOF算法的一般流程可以描述为:

  1. 初始化 k ,用于后续计算第 k 距离;
  2. 计算每个样本点与其他点的距离,并对其排序;
  3. 计算每个样本点的第 k 距离,第 k 领域;
  4. 计算每个样本点的可达密度以及LOF值;
  5. 对所有样本点的LOF值进行排序,与1作比较,越大于1,越可能是离群值。
data = test[['mileage','price']]
# 导入sklearn用于计算最近邻相关数据的方法 NearestNeighbors
from sklearn.neighbors import NearestNeighbors
import numpy as np

# 定义函数计算第k可达距离
def k_Distance(data, k):
    neigh = NearestNeighbors(k)
    model = neigh.fit(data)#fit()函数输入数据,数据类型为numpy,用于返回训练过程的数据记录
    
    nums = data.shape[0]#shape[0]即读取数据第一维度的长度

    k_distance = []
    neighbor_info = []#近邻信息
    
    for index in range(nums):
        
        # K个距离
        dist = neigh.kneighbors([data[index]], n_neighbors = k + 1)
        # 最大的dist
        k_distance.append(dist[0][-1][-1])
        
        # neighbor为k个邻居的索引,dists为相应的距离
        dists, neighbor = neigh.radius_neighbors([data[index]], radius = k_distance[index])
        
        # 排除自身
        mask = neighbor[0] != index     
        neighbor_info.append([neighbor[0][mask], dists[0][mask]])
    return k_distance, neighbor_info

# 定义函数计算局部可达密度
def reach_density(data, k_distance, neighbor_info):
    
    nums = data.shape[0]
    density_list = []
    
    for index in range(nums):
        
        neighbors, dists = neighbor_info[index]
        nums_neigh = len(neighbors)
    
        sum_dist = 0
        
        for item in range(nums_neigh):
            
            k_dist_o = k_distance[neighbors[item]] 
    
            direct_dist = dists[item]
        
            reach_dist_p_o = max(k_dist_o, direct_dist)#可达距离
        
            sum_dist += reach_dist_p_o
           
        density_list.append(nums_neigh/sum_dist)
    
    return density_list

# 定义函数计算LOF因子
def cal_lof(index, data, neighbor_info, lrd_list):
    
    point_p = data[index]
    neighbors, _ = neighbor_info[index]
    
    nums_neigh = len(neighbors)
    sum_density = 0
    
    for item in range(nums_neigh):
        
        sum_density += lrd_list[item]

    return sum_density/(nums_neigh*lrd_list[index])


dists, neighbor_info = k_Distance(data.values, 2)

lrd_list = reach_density(data, dists, neighbor_info)

nums = data.shape[0]

lof_list = []

for index in range(nums):
    
    lof = cal_lof(index, data.values, neighbor_info, lrd_list)
    lof_list.append(lof)

boolean_array = [item > 5 for item in lof_list]

indicy = []
for key, value in enumerate(boolean_array):
    if value:
        indicy.append(key)

print (indicy)

在这里插入图片描述

#画出离群值
fig = plt.figure(figsize=(7,7))
for i in data.index:
    if i not in indicy:
        plt.scatter(data.iloc[i]['mileage'], 
                data.iloc[i]['price'], 
                c='#2d9ed8',
                s=50,
                marker='^',
                label='inliers')
    else:
        plt.scatter(data.iloc[i]['mileage'], 
                data.iloc[i]['price'], 
                c='#EE5150',
                s=50,
                marker='^',
                label='outliers')
        plt.xlabel('$mileage$')
        plt.ylabel('$price$')
        plt.grid('on')

在这里插入图片描述
另外一种比较乱的写法,代码如下:

test_2d = test[['mileage','price']]
from sklearn.neighbors import NearestNeighbors
import numpy as np
neigh = NearestNeighbors(5) # 默认为欧式距离
model = neigh.fit(test_2d)

data = test_2d
# dist为每个样本点与第k距离邻域内的点的距离(包括自身),neighbor为第k距离邻域点的编号(包括自身)
dist, neighbor=neigh.kneighbors(test_2d,n_neighbors=6)

k_distance_p = np.max(dist,axis=1)

nums = data.shape[0]
lrdk_p = []
lof = []
for p_index in range(nums):
    rdk_po = []
    neighbor_p = neighbor[p_index][neighbor[p_index]!=p_index]
    for o_index in neighbor_p:
        rdk_po.append(max(k_distance_p[o_index],int(dist[p_index][neighbor[p_index]==o_index])))
    lrdk_p.append(float(len(neighbor_p))/sum(rdk_po))

for p_index in range(nums):
    lrdk_o=[]
    neighbor_p = neighbor[p_index][neighbor[p_index]!=p_index]
    for o_index in neighbor_p:
        lrdk_o.append(lrdk_p[o_index])
    lof.append(float(sum(lrdk_o))/(len(neighbor_p)*(lrdk_p[p_index])))    

fig = plt.figure(figsize=(7,7))

for index,size in zip(range(nums),lof):
    if index in indicy:
        plt.scatter(data['mileage'][index], 
                    data['price'][index], 
                    s=np.exp(lof[index])*50,
                    c='#efab40',
                    alpha=0.6,
                    marker='o')
        plt.text(data['mileage'][index]-np.exp(lof[index])*50, 
                 data['price'][index]-np.exp(lof[index])*50,
                str(round(lof[index],2)))
    else:
        plt.scatter(data['mileage'][index], 
                    data['price'][index], 
                    s=np.exp(lof[index])*50,
                    c='#5dbe80',
                    alpha=0.6,
                    marker='o')
        plt.text(data['mileage'][index]-np.exp(lof[index])*50, 
                 data['price'][index]-np.exp(lof[index])*50,
                str(round(lof[index],2)),
                fontsize=7)

plt.xlabel('mileage')
plt.ylabel('price')
plt.grid('off')

在这里插入图片描述
参考资料:
Python机器学习笔记:异常点检测算法——LOF(Local Outiler Factor)
链接: link.

  • 5
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
[入门数据分析的第一堂课]这是一门为数据分析小白量身打造的课程,你从网络或者公众号收集到很多关于数据分析的知识,但是它们零散不成体系,所以第一堂课首要目标是为你介绍:Ø  什么是数据分析-知其然才知其所以然Ø  为什么要学数据分析-有目标才有动力Ø  数据分析的学习路线-有方向走得更快Ø  数据分析的模型-分析之道,快速形成分析思路Ø  应用案例及场景-分析之术,掌握分析方法[哪些同学适合学习这门课程]想要转行做数据分析师的,零基础亦可工作中需要数据分析技能的,例如运营、产品等对数据分析感兴趣,想要更多了解的[你的收获]n  会为你介绍数据分析的基本情况,为你展现数据分析的全貌。让你清楚知道自己该如何在数据分析地图上行走n  会为你介绍数据分析的分析方法和模型。这部分是讲数据分析的道,只有学会底层逻辑,能够在面对问题时有自己的想法,才能够下一步采取行动n  会为你介绍数据分析的数据处理和常用分析方法。这篇是讲数据分析的术,先有道,后而用术来实现你的想法,得出最终的结论。n  会为你介绍数据分析的应用。学到这里,你对数据分析已经有了初步的认识,并通过一些案例为你展现真实的应用。[专享增值服务]1:一对一答疑         关于课程问题可以通过微信直接询问老师,获得老师的一对一答疑2:转行问题解答         在转行的过程中的相关问题都可以询问老师,可获得一对一咨询机会3:打包资料分享         15本数据分析相关的电子书,一次获得终身学习
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值