一、概述
1.1问题介绍与分析
随着社会经济的迅猛发展,房地产开发建设的速度越来越快,二手房市场迅猛发展,对二手房房产价格评估的需求也随之增大.因此,对二手房房价预测与分析是必要的.详细文档与代码资料保存在我的百度云盘。
数据收集:在链家网站上收集广州二手房数据,把数据分为训练集、测试集。训练和测试数据是一些二手房信息以及价格状况,要尝试根据它生成合适的模型并预测其他二手房的价格状况。这是一个回归问题,很多回归算法都可以解决。
收集工具:八爪鱼
1.2总体思路
首先从数据清洗开始,对缺失值的多维度处理、对离群点的剔除方法以及对字符、空格等的处理;其次进行特征工程,包括类别特征编码、连续特征编码等;再次进行特征选择,采用了xgboost,xgboost的训练过程即对特征重要性的排序过程;最后一部分是模型设计与分析,采用了岭回归模型、xgboost,取得了不错的效果,此外还介绍了模型融合方法。
二、 数据清洗
2.1缺失值的多维度处理
(1)通常遇到缺值的情况,我们会有几种常见的处理方式
①如果缺值的样本占总数比例极高,我们可能就直接舍弃了,作为特征加入的话,可能反倒带入noise,影响最后的结果了
②如果缺值的样本适中,而该属性非连续值特征属性(比如说类目属性),那就把NaN作为一个新类别,加到类别特征中
③如果缺值的样本适中,而该属性为连续值特征属性,有时候我们会考虑给定一个step(比如这里的age,我们可以考虑每隔2/3岁为一个步长),然后把它离散化,之后把NaN作为一个type加到属性类目中。
(2)有些情况下,缺失的值个数并不是特别多,那我们也可以试着根据已有的值,拟合一下数据,补充上。
2.2数据预处理
#载入数据:
train=pd.read_csv('train.csv')
test= pd.read_csv('test.csv')
#检视源数据
print(train.head())
#合并数据
这么做主要是为了用DF进行数据预处理的时候更加方便。等所有的需要的预处理进行完之后,我们再把他们分隔开。
首先,价格作为我们的训练目标,只会出现在训练集中,不会在测试集中。所以,我们先把价格这一列给拿出来。
#可视化价格特征
prices= pd.DataFrame({"price":train["价格"],"log(price + 1)":np.log1p(train["价格"])})
prices.hist()
#可见,label本身并不平滑。为了分类器的学习更加准确,首先把label给“平滑化”(正态化)
#这里使用log1p, 也就是 log(x+1),避免了复值的问题。值得注意的是,如果这里把数据都给平滑化了,那么最后算结果的时候,要把预测到的平滑数据给变回去。按照“怎么来的怎么去”原则,log1p()就需要expm1(); 同理,log()就需要exp()
y_train= np.log1p(train.pop('价格'))
#然后把剩下的部分合并起来
all_df= pd.concat((train, test), axis=0)
三、特征工程
#变量转换
all_df["楼龄"]=2017-all_df["楼龄"]
#有一些数据是缺失的,根据缺失值的多少进行相应的处理
#查看缺失情况
print(all_df.isnull().sum().sort_values(ascending=False).head(10))
#可以看到,缺失最多的column是装修情况。由于装修情况的值缺失太多就删掉这一列
all_df.pop("装修情况")
#接着处理楼龄的缺失值,由于缺失不多,这里我用平均值进行填充
mean_col= all_df["楼龄"].mean()
all_df["楼龄"]=all_df["楼龄"].fillna(mean_col)
#由于变量交易权属 建筑类型 配备电梯的绝大多数的值相同,区分的效果不会很明显这里就删掉了
all_df.pop("建筑类型")
all_df.pop("配备电梯")
all_df.pop("交易权属")
#该特征很多都不相同,这里简单处理就删掉了
all_df.pop("房型")
#看看是不是没有空缺了?
print(all_df.isnull().sum().sum())
#把category的变量转变成numerical表达形式
#当我们用numerical来表达categorical的时候,要注意,数字本身有大小的含义所以乱用数字会给之后的模型学习带来麻烦。于是我们可以用One-Hot的方法来表达category。pandas自带的get_dummies方法可以帮你一键做到One-Hot。
pd.get_dummies(all['区域'], prefix='区域')
#此刻区域被分成了5个column,每一个代表一个category。是就是1,不是就是0。
#同理,我们把所有的category数据,都给One-Hot了
all_dummy_df= pd.get_dummies(all_df)
#标准化numerical数据
#这一步并不是必要,但是得看你想要用的分类器是什么。一般来说,regression的分类器最好是把源数据给放在一个标准分布内。不要让数据间的差距太大。这里,我们当然不需要把One-Hot的那些0/1数据给标准化。我们的目标应该是那些本来就是numerical的数据:
#先来看看哪些是numerical的:
numeric_cols= all_df.columns[all_df.dtypes != 'object']
#这里也是用log1p()方法进行平滑
all_dummy_df["楼龄"]=np.log1p(all_dummy_df["楼龄"])
all_dummy_df["建筑面积"]=np.log1p(all_dummy_df["建筑面积"])
四、 特征选择
在特征工程部分,这么多维特征一方面可能会导致维数灾难,另一方面很容易导致过拟合,需要做降维处理,降维方法常用的有如 PCA,t-SNE 等,这类方法的计算复杂度比较高。并且根据以往经验,PCA 或t-SNE 效果往往不好。除了采用降维算法之外,也可以通过特征选择来降低特征维度。特征选择的方法很多:最大信息系数(MIC)、皮尔森相关系数(衡量变量间的线性相关性)、正则化方法(L1,L2)、基于模型的特征排序方法。比较高效的是最后一种,即基于学习模型的特征排序方法,这种方法有一个好处:模型学习的过程和特征选择的过程是同时进行的,因此我们采用这种方法,基于xgboost 来做特征选择,xgboost 模型训练完成后可以输出特征的重要性,据此我们可以保留Top N 个特征,从而达到特征选择的目的。
五、 模型设计与分析
5.1单个分类器
从训练数据中随机划分部分数据作为验证集,剩下作为训练集,供模型调参使用。在训练集上采用十折交叉验证进行模型调参。得到多个单模型结果后,再在验证集上做进一步的模型融合。
#把数据集分回训练/测试集
dummy_train_df= all_dummy_df[:110]
dummy_test_df= all_dummy_df[110:-1]
X_train= dummy_train_df.values
X_test= dummy_test_df.values
(1)随机森林
max_features= [.1, .3, .5, .7, .9, .99]
test_scores= []
formax_feat in max_features:
clf =RandomForestRegressor(n_estimators=200, max_features=max_feat)
test_score = np.sqrt(-cross_val_score(clf,X_train, y_train, cv=5, scoring='neg_mean_squared_error'))
test_scores.append(np.mean(test_score))
plt.plot(max_features,test_scores)
plt.title("MaxFeatures vs CV Error");
#用RF的最优值达到了0.194
(2)这里我们用一个Stacking的思维来汲取两种或者多种模型的优点首先,我们把最好的parameter拿出来,
做成我们最终的model
ridge= Ridge(alpha=1)
rf= RandomForestRegressor(n_estimators=500, max_features=.3)
ridge.fit(X_train,y_train)
rf.fit(X_train,y_train)
#上面提到了,因为最前面我们给label做了个log(1+x), 这里我们需要把predit的值给exp回去,并且减掉那个"1"
所以就是我们的expm1()函数。
y_ridge= np.expm1(ridge.predict(X_test))
y_rf= np.expm1(rf.predict(X_test))
#一个正经的Ensemble是把这群model的预测结果作为新的input,再做一次预测。
y_final= 0.8*y_ridge + 0.2*y_rf
print(y_final)
要判定一下当前模型所处状态(欠拟合or过拟合)
有一个很可能发生的问题是,我们不断地做feature engineering,产生的特征越来越多,用这些特征去训练模型,会对我们的训练集拟合得越来越好,同时也可能在逐步丧失泛化能力,从而在待预测的数据上,表现不佳,也就是发生过拟合问题。从另一个角度上说,如果模型在待预测的数据上表现不佳,除掉上面说的过拟合问题,也有可能是欠拟合问题,也就是说在训练集上,其实拟合的也不是那么好。而在机器学习的问题上,对于过拟合和欠拟合两种情形。我们优化的方式是不同的。
对过拟合而言,通常以下策略对结果优化是有用的:
- 做一下feature selection,挑出较好的feature的subset来做training
- 提供更多的数据,从而弥补原始数据的bias问题,学习到的model也会更准确
而对于欠拟合而言,我们通常需要更多的feature,更复杂的模型来提高准确度。
著名的learning curve可以帮我们判定我们的模型现在所处的状态。我们以样本数为横坐标,训练和交叉验证集上的错误率作为纵坐标,两种状态分别如下两张图所示:过拟合,欠拟合
著名的learningcurve可以帮我们判定我们的模型现在所处的状态。我们以样本数为横坐标,训练和交叉验证集上的错误率作为纵坐标
在实际数据上看,我们得到的learning curve没有理论推导的那么光滑,但是可以大致看出来,训练集和交叉验证集上的得分曲线走势还是符合预期的。目前的曲线看来,我们的model并不处于overfitting的状态(overfitting的表现一般是训练集上得分高,而交叉验证集上要低很多,中间的gap比较大)。
5.2 Bagging
一般来说,单个分类器的效果真的是很有限。我们会倾向于把N多的分类器合在一起,做一个“综合分类器”以达到最好的效果。模型融合可以比较好地缓解,训练过程中产生的过拟合问题,从而对于结果的准确度提升有一定的帮助。Bagging把很多的小分类器放在一起,每个train随机的一部分数据,然后把它们的最终结果综合起来(多数投票制)。Sklearn已经直接提供了这套构架,我们直接调用就行:在这里,我们用CV结果来测试不同的分类器个数对最后结果的影响。注意,我们在部署Bagging的时候,要把它的函数base_estimator里填上你的小分类器
(1)用bagging自带的DT
params= [1, 10, 15, 20, 25, 30, 40]
test_scores= []
forparam in params:
clf= BaggingRegressor(n_estimators=param)
test_score = np.sqrt(-cross_val_score(clf,X_train, y_train, cv=10, scoring='neg_mean_squared_error'))
test_scores.append(np.mean(test_score))
plt.plot(params,test_scores)
plt.title("n_estimatorvs CV Error");
#用bagging自带的DT,结果是0.196
(2)ridge + bagging
ridge= Ridge(1)
params= [1, 10, 15, 20, 25, 30, 40]
test_scores= []
forparam in params:
clf = BaggingRegressor(n_estimators=param,base_estimator=ridge)
test_score = np.sqrt(-cross_val_score(clf,X_train, y_train, cv=10, scoring='neg_mean_squared_error'))
test_scores.append(np.mean(test_score))
plt.plot(params,test_scores)
print(test_scores)
plt.title("n_estimatorvs CV Error");
#使用25个小ridge分类器的bagging,达到了0.163的结果.
#learning_curve曲线,准确率比较高,基本没有过拟合
5.3Boosting
#Boosting比Bagging理论上更高级点,它也是揽来一把的分类器。但是把他们线性排列。下一个分类器把上一个分类器分类得不好的地方加上更高的权重,这样下一个分类器就能在这个部分学得更加“深刻”。
(1)Adaboost+Ridge
params= [34,35,36,45,50,55,60,65]
test_scores= []
forparam in params:
clf = AdaBoostRegressor(n_estimators=param,base_estimator=ridge)
test_score = np.sqrt(-cross_val_score(clf,X_train, y_train, cv=10, scoring='neg_mean_squared_error'))
test_scores.append(np.mean(test_score))
plt.plot(params,test_scores)
plt.title("n_estimatorvs CV Error");
#Adaboost+Ridge在这里,55个小分类器的情况下,达到了0.163的结果。其learning_curve曲线如下:
(2)使用Adaboost自带的DT
params= [10, 15, 20, 25, 30, 35, 40, 45, 50]
test_scores= []
forparam in params:
clf = AdaBoostRegressor(n_estimators=param)
test_score = np.sqrt(-cross_val_score(clf,X_train, y_train, cv=10, scoring='neg_mean_squared_error'))
test_scores.append(np.mean(test_score))
plt.plot(params,test_scores)
plt.title("n_estimatorvs CV Error");
#test_scores达到0.198的结果
5.3XGBoost
#这依旧是一个Boosting框架的模型,但是却做了很多的改进。用Sklearn自带的cross validation方法来测试模型
params= [1,2,3,4,5,6]
test_scores= []
forparam in params:
clf = XGBRegressor(max_depth=param)
test_score = np.sqrt(-cross_val_score(clf,X_train, y_train, cv=10, scoring='neg_mean_squared_error'))
test_scores.append(np.mean(test_score))
plt.plot(params,test_scores)
plt.title("max_depthvs CV Error");
#当params值为6时,test_scores达到0.185
#轻微过拟合
5.4 GBM
params= [80,90,100,110,120,130,140,150]
test_scores= []
forparam in params:
clf =GradientBoostingRegressor(n_estimators=param)
test_score = np.sqrt(-cross_val_score(clf,X_train, y_train, cv=10, scoring='neg_mean_squared_error'))
test_scores.append(np.mean(test_score))
plt.plot(params,test_scores)
plt.title("max_depthvs CV Error");
#当params值为90时,test_scores达到0.188,从其学习率曲线可以看出,模型过拟合挺严重
#综上,Bagging + Ridge 与Adaboost+Ridge模型的准确率和泛化能力是比较好。
六 使用ridge + bagging模型预测房价
ridge= Ridge(1)
br=BaggingRegressor(n_estimators=param, base_estimator=ridge)
br.fit(X_train,y_train)
#房价预测
y_br= np.expm1(br.predict(X_test))
features={'楼龄' , '建筑面积' , '区域_增城' , '区域_天河' , '区域_海珠’, '区域_番禺' , '区域_白云' , '楼层_中楼层' , '楼层_低楼层' , '楼层_独栋' , '楼层_高楼层' , '朝向_东' , '朝向_东北' , '朝向_东南' , '朝向_东西' , '朝向_北' , '朝向_南' , '朝向_南北' , '朝向_西' , '朝向_西北' , '朝向_西南' , '房屋用途_别墅' , '房屋用途_普通住宅'}
#使用GBM模型进行特征选择
gbr= GradientBoostingRegressor(loss='ls', n_estimators=100, max_depth=3,verbose=1, warm_start=True)
gbr_fitted= gbr.fit(X_train, y_train)
#查看特征重要度,根据特征重要度可以进行特征选择,选取重要度高的特征,删除不重要的特征以减少维度降低过拟合
print(zip(features,list(gbr.feature_importances_)))
#可见建筑面积是影响房屋价格的最重要特征。