这篇文章是介绍一个完整的机器学习小项目——预测房屋价格,它是Kaggle竞赛中入门级的题目,和我们比较熟悉的泰坦尼克号生存预测处于同一等级。在之前介绍KNN算法时,曾用过这个数据集,但只是通过简单的建模帮助理解KNN的思想,本文会更加全面地介绍完成一个小项目的流程,如何在科学分析的辅助下预测出我们需要的目标值。
在分析之前我们应该提前明确我们的目的,中途可能需要处理的问题,可以归纳成以下几点:
由于每个人的习惯不同,所以处理问题时的先后顺序、选择的方法自然也不同,比如说处理缺失值的方法就有很多种,本文提及的流程、方法只是个人的一点小思路,希望给伙伴们参考,但绝不局限于此,不论算法还是模型不都应该向更优处发展嘛。
首先一定要大致了解一下测试数据集,上图只是数据集的一小部分,共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都保留,因为房间面积和房间总数不会过于冲突,可能在房间面积相同的情况下,房间个数越多的房子价格会更高一些呢?而剩下的四个特征从意思上就有一些冲突,可以取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)
综上可以发现完成一个小项目五成的时间用来处理分析数据、四成的时间用来调参、最后一成时间用来选模建模,而最终模型的好坏五成取决数据本身、四成取决于你的特征工程、最后一成取决于你的调参,所以需要把更多时间更多精力放在数据处理、特征工程上才能得到让自己满意的一个结果。
公众号【奶糖猫】回复"房价预测"可获取源码供参考