Kaggle竞赛入门实战——机器学习预测房屋价格

共 7572字,需浏览 16分钟

 ·

2021-03-17 11:29

这篇文章是介绍一个完整的机器学习小项目——预测房屋价格[1],它是Kaggle竞赛中入门级的题目,和我们比较熟悉的泰坦尼克号生存预测处于同一等级。

本文会全面地介绍完成一个小项目的流程,如何在科学分析的辅助下预测出我们需要的目标值。

(数据集和源码下载见文末)

在分析之前我们应该提前明确我们的目的,中途可能需要处理的问题,可以归纳成以下几点:

  • 了解标签变量:可以通过目标变量大致分析出解决问题是需要分类算法还是回归算法。
  • 粗略了解特征:因为特征标签都为英文,所以理解标签含义是最基本的,观察每个特征的特点,是数值型的、还是字符型的;是离散的、还是连续的;粗略联想一下各个特征与目标变量间的联系。
  • 数据预处理:针对性处理数据集中的缺失数据和异常数据。
  • 研究主要特征:数据集中可能只有一部分特征与目标变量间相关性极强,重点分析这些特征与目标变量间的联系。
  • 选择性处理其他特征:除主要特征外,可能主观上认为某些特征也会影响目标变量,也可以选择性分析一下。
  • 建模工作:选出最适合该问题的模型,进行建模、调参等操作。

由于每个人的习惯不同,所以处理问题时的先后顺序、选择的方法自然也不同,比如说处理缺失值的方法就有很多种,本文提及的流程、方法只是个人的一点小思路,希望给伙伴们参考,但绝不局限于此,不论算法还是模型不都应该向更优处发展嘛。

首先一定要大致了解一下测试数据集,上图只是数据集的一小部分,共80个特征+一个目标变量“SalePrice”,共有1460个样本。很明显这个问题是需要通过房子的一些特征预测出相应的价格,所以建模可以选择回归类算法。

缺失值处理

这个数据集许多特征都或多或少的含有一些缺失值,针对缺失值个数的多少,采取的处理方式也不同,像一些特征的缺失值占样本个数超过三分之二,那么这些特征的意义也不大,所以选择舍去这些特征:

# 很多特征还不及数据个数的三分之一,所以选择舍去
data.drop(columns = ['Alley','FireplaceQu','PoolQC','Fence','MiscFeature'],axis = 1,inplace = True)

而对于缺失值较少的数值型特征,根据情况可以选择填充众数、中位数和平均数:

#填补数值型缺失值
data['LotFrontage'].fillna(data['LotFrontage'].median(),inplace = True)
data['MasVnrArea'].fillna(data['MasVnrArea'].mean(),inplace = True)

而数据集中本身含有很多字符型特征,而对于这类特征的缺失值,是没有中位数和平均数一说的,可以随机填充或者填充出现频率最多的元素,这里我选择了后者:

#获得含有缺失值的字符型特征标签
miss_index_list = data.isnull().any()[data.isnull().any().values == True].index.tolist()
miss_list = [] #存元素
for i in miss_index_list: #注意需要reshape规格
    miss_list.append(data[i].values.reshape(-1,1))

这里填充缺失值的方式可以用上述方式,但前提是需要另写一个函数计算特征的众数,另一种方式就是利用sklearn中自带的API进行填充:

from sklearn.impute import SimpleImputer
#用众数填补数值型变量
for i in range(len(miss_list)):
    imp_most = SimpleImputer(strategy='most_frequent'#实例化,参数选择众数
    imp_most = imp_most.fit_transform(miss_list[i]) #训练
    data.loc[:,miss_index_list[i]] = imp_most #替换原来的一列

当然这种方式也适用于数值型,其中参数strategy也是可选的,像均值、中位数、众数和自定义这几种,代码中most_frequent就代表众数。对于填充缺失值的方式,numpy和pandas应用较多,但这种利用API填充也比较便利,可以当成一次拓展,自己了解一下。

处理字符型特征

对于某些回归类算法,比如线性回归,是通过计算继而预测出最后的目标变量,所以训练时传入字符型元素是不合法的。但如果利用随机森林可以避免,因为由决策树构成的随机森林只注重样本特征的分布,但现在我们并不知道哪一种模型更适合该问题,因为最后我们要从中挑选出一个最优的,那么我们在处理数据时就要兼顾我们将要选择的所有模型。

这里选择利用哑变量(也称独热编码)处理字符型数据,可能有的人比较陌生,但介于篇幅问题不在过多阐述,可以自行查询一下,或者过段时间也会写一篇文章单独讲一下变量处理的相关知识。

我们只针对字符型特征进行哑变量转化,所以需要索引出字符型类的特征:

data_ = data.copy() #在一个新的数据集上操作
ob_features = data_.select_dtypes(include=['object']).columns.tolist()

然后就可以通过sklearn中自带的API对字符型特征进行哑变量转换:

#哑变量/独热编码
from sklearn.preprocessing import OneHotEncoder
OneHot = OneHotEncoder(categories='auto')#实例化
result = OneHot.fit_transform(data_.loc[:,ob_features]).toarray()#训练

打印一下result输出如下:

这个矩阵中的每一列可以看成一个新的特征,而每个特征只包含0和1两个元素,其中1代表有、0代表没有。比如Street特征中包含两个元素Grvl和Pave,而这两个元素就可以通过哑变量转化形成两个新的特征。Street特征中为Grvl的样本在Gral新一列中就为1,在Pave新一列中就为0,其他特征也是如此。

#获取特征名
OneHotnames = OneHot.get_feature_names().tolist()
OneHotDf = pd.DataFrame(result,columns=OneHotnames)

利用get_feature_names方法可以获取特征的标签,将上述矩阵转化为一个DataFrame:

过滤方差

因为转化为哑变量之后许多特征都是由0和1组成的,仍然用Street举例,会不会有种可能就是所有样本的Street中的元素都为Grvl呢?如果这样哑变量转化后的Grvl一列皆为1,而Pave一列皆为0,那么这两个新的特征对于最后的目标变量的预测是没有一点用处的,是可以直接删去的,所以利用方差过滤掉类似的情况,将阈值设为0.1,方差低于0.1的特征都将被过滤掉。

from sklearn.feature_selection import VarianceThreshold
#过滤掉方差小于0.1的特征
transfer = VarianceThreshold(threshold=0.1)
new_data1 = transfer.fit_transform(data_)
#get_support得到的需要留下的下标索引
var_index = transfer.get_support(True).tolist()
data1 = data_.iloc[:,var_index]

最后利用该方法过滤掉了188个特征,原本的270个特征还剩下82个特征。

相关性过滤

82个特征仍然太多了,一定还有一些无关紧要的特征,需要继续特征降维,如果一个特征对于目标变量的预测有帮助,那么这个特征与目标变量间一定有某种联系,那么这个特征与目标变量之间的相关性一定是比较高的,所以可以通过特征与目标变量间的相关系数过滤一些特征。

#皮尔逊相关系数
from scipy.stats import pearsonr
pear_num = [] #存系数 
pear_name = [] #存特征名称
feature_names = data1.columns.tolist()
#得到每个特征与SalePrice间的相关系数
for i in range(0,len(feature_names)-1):
    print('%s和%s之间的皮尔逊相关系数为%f'%(feature_names[i],feature_names[-1],pearsonr(data1[feature_names[i]],data1[feature_names[-1]])[0]))
    if (abs(pearsonr(data1[feature_names[i]],data1[feature_names[-1]])[0])>0.5):
        pear_num.append(pearsonr(data1[feature_names[i]],data1[feature_names[-1]])[0])
        pear_name.append(feature_names[i])

我们设定阈值为0.5,留下相关系数绝对值大于0.5的特征,因为不止正相关有影响,负相关也是对预测有一定作用的。

经过滤后只剩下13个特征,上述计算的是特征与目标变量之间的相关性,我们可以再从特征与特征之间的相关性找一找联系,可不可以再降几维呢?

  • TotalBsmtSF(房间总数)和GrLivArea(地面以上居住面积)相关系数0.83;
  • GarageCars(车库可装车辆个数)和GarageArea(车库面积)相关系数0.88;
  • TotalBsmtSF(地下室总面积)和1stFlrSF(第一层面积)相关系数0.82;

个人觉得可以将TotalBsmtSF和GrLivArea都保留,因为房间面积和房间总数不会过于冲突,可能在房间面积相同的情况下,房间个数越多的房子价格会更高一些呢?而剩下的四个特征从意思上就有一些冲突,可以取GarageArea和otalBsmtSF两个特征。

可视化分析

我们可以通过可视化验证一下过滤后的特征与目标变量之间是否存在上文我们预想的某种联系,这里推荐用箱线图绘制数据较为离散的特征、用散点图绘制数据较为连续的特征,前者方便比较,后者方便观察分布,利于找出联系。

可以看到OverallQual(总体评价)对于价格的影响真的巨大,所以买卖口碑真的是非常重要呀,其他离散特征与目标变量间的联系也是具有很明显的趋势的。

散点图可视化出的结果也是非常明显,所以我们选取的这几个特征都是不错的,但是这不代表其他特征与目标变量之间没有联系,之前也提及过可以选取一些主观上感觉有联系的特征再进行二次分析。我自己猜测Neighborhood(地理位置)和HeatingQC(加热质量)对于价格应该会有影响,但是可视化出的结果显示联系并不大。

选模、调参

模型测试我选择用线性回归和随机森林,由于线性回归对传入数据的要求以及要通过均方误差判断哪一个模型更优,所以需要先对训练集和测试集进行标准化,然后计算两个模型对应的均方误差,代码如下:

#计算模型的均方误差
clfs = {'rfr':RandomForestRegressor(),
      'LR':LinearRegression()}
for clf in clfs:
        clfs[clf].fit(x_train_sta,y_train_sta)
        prediction = clfs[clf].predict(x_test_sta)
        print(clf + " RMSE:" + str(np.sqrt(metrics.mean_squared_error(y_test_sta,prediction))))
'''
rfr RMSE:0.38292937665826626
LR RMSE:0.5534982168680332
'''
     

在不调整参数的情况下,rfr的均方误差小于LR,随机森林在该问题上是要优于线性回归的,所以选定随机森林作为最后建模要用的算法,然后利用网格搜索或者学习曲线对算法进行调参。

这里省略很多很多调参步骤。

最后我自己尝试调出的最佳参数如下,调参之后的模型得分大约为84%,前后相比大约提高了大约2%。

rf = RandomForestRegressor(n_estimators=300,max_depth =20,
                          max_features =5,min_samples_leaf =2,
                          min_samples_split=2)
grid = GridSearchCV(rf,param_grid=param_grid,cv = 5)
grid.fit(x_train,y_train)
rf_reg = grid.best_estimator_#最佳模型

可以在模型的基础上分析一下特征重要程度,其中OverallQual(总体评价)和GrLivArea(居住面积)两者重要程度占比就已经超过五成,反观x32_Unf(未完成地下室面积)和x29_TA(厨房质量)影响甚微。

需要对测试集做与训练集一致的操作,例如缺失值处理、哑变量转化等,然后索引出测试集中这些重要特征并传入建好的模型中,得出最终的预测结果"SalePrice",然后将"Id"和"SalePrice"导出一个名为submission的csv文件,最后需要在Kaggle需要上传这份文件,就可以得到自己的排名啦。

submission_Id = pd.read_csv("house_test.csv",usecols=['Id'])
SalePrice = pd.DataFrame(test_value_y,columns=['SalePrice'])
Submission = pd.concat([submission_Id,SalePrice],axis=1)
Submission.to_csv("submission.csv",index = False)

总结

综上可以发现完成一个小项目五成的时间用来处理分析数据、四成的时间用来调参、最后一成时间用来选模建模,而最终模型的好坏五成取决数据本身、四成取决于你的特征工程、最后一成取决于你的调参,所以需要把更多时间更多精力放在数据处理、特征工程上才能得到让自己满意的一个结果。




如何获取文中数据集和源代码?
1、点击下面名片,关注【码工小熊
2、在码工小熊后台会话栏回复关键字房屋


● 想和陈老师讨论具体工作遇到的问题?
● 想看海量行业资料?
● 想学习陈老师的思维方式?
● 想在求职、转行、转岗道路上得到十年数据人陈老师的一对一亲自指导?
 ↓ ↓ ↓扫描二维码,加入陈老师的知识星球 ↓ ↓ ↓

浏览 59
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报