使用scikit-learn和Keras建立房价估价模型

之前曾写过一篇抓取 搜房网 (fang.com) 房源数据并用 tflean 搭建神经网络进行房价分类的文章

本文算是上面这篇文章的第二个版本:使用 scikit-learn 和 Keras 来对广州二手房房源数据搭建 回归模型 进行房源价格的估价。

数据抓取

相比于第一次抓取时,搜房网对房源页面进行了改版,因此之前所使用的抓取脚本不能使用了。于是又用 beautifulsoup 改了一遍,解析取出每一项信息然后结构化。

然后再对各项非数字内容进行数字映射,如:

regions = {'天河': '1.0', '荔湾': '2.0', '海珠': '3.0', '越秀': '4.0', '白云': '5.0', '番禺': '6.0', '黄埔': '7.0', '增城': '8.0', '花都': '9.0', '从化': '10.0', '南沙': '11.0'}  

最终的部分结果如下:

需要说明的是,我排除了每平米价格在 20万 以上的房源 ( 只有 3 例 ),土豪的世界我并不关心。

跟上一次抓取数据时相比,搜房网目前还能抓到房源是否在哪条地铁线路附近、距离地铁多远。

常识告诉我们,这也是影响房价的一个重要因素,因此也作为了特征值。

数据总行数为 10309 :

In [4]: df.shape[0]  
Out[4]: 10309  

先看下两个图了解下房价数据:

( 广州各区房源比例 )

( 广州各区平均房价,最后一列为全市均价 )

具体均价如下:

{
    '天河': 48545.516516516516,
     '荔湾': 28983.626349892009,
     '海珠': 38201.262574595057,
     '越秀': 47588.640838650863,
     '白云': 27656.089361702128,
     '番禺': 27007.389256198348,
     '黄埔': 25335.879492600423,
     '增城': 17014.85277246654,
     '花都': 12356.367205542725,
     '从化': 12507.666666666666,
     '南沙': 16559.167076167076,
     '全市': 31709.176156756232
 }

上一次抓数据的时候(四月份),全市均价还是 28995.8 ,三个月内广州的房价又上涨了 2713.4 元每平米。

备注:数据收集日期:2017/07/23 - 2017/07/25

导入数据并分为训练数据和测试数据

from pandas import read_csv  
df = read_csv('./dataset.csv')

train_ = df[0:df.shape[0]-1000]  
test_ = df[df.shape[0]-1000:]

predictors = ['房','厅','卫','建筑面积','建筑年代','朝向','楼层','装修','地铁沿线','地铁距离','区','学校','有无电梯']  
X_train = train_[predictors]  
y_train = train_['每平米价格']  
X_test = test_[predictors]  
y_test = test_['每平米价格']  

将最后 1000 条数据作为验证模型正确率而使用的测试数据,其余数据为训练数据。

选择 SVR 模型

使用 scikit-learn 的 SVR() 模型,使用默认的核函数 rbf

class sklearn.svm.SVR(kernel='rbf', degree=3, gamma='auto', coef0=0.0, tol=0.001, C=1.0, epsilon=0.1, shrinking=True, cache_size=200, verbose=False, max_iter=-1)  

gamma 默认值为 1/n,其中 n 为特征值的数量,我们的例子中 n = 13,折中取了 0.1。

另外一个比较重要的参数是 C (惩罚因子,即对误差的宽容度。C 越高,说明越不能容忍出现误差,容易过拟合。C 越小,容易欠拟合)。

具体原理可以参考这篇文章

先简单测试下 C 参数:

from sklearn.svm import SVR

for i in range(0, 7):  
    C = 10**i
    clf = SVR(kernel='rbf', C=C, gamma=0.1)
    clf.fit(X_train, y_train)
    print "C=%d, score=%f" % (C, clf.score(X_train, y_train))

输出如下:

C=1, score=-0.031758  
C=10, score=-0.020476  
C=100, score=0.061383  
C=1000, score=0.295201  
C=10000, score=0.765092  
C=100000, score=0.995138  
C=1000000, score=0.999077  

结果也验证了 C 越大,对于训练集的数据的准确度越高,先选择 C=1000000,至于是否会过拟合,我们等下再用测试集数据来验证下情况。

训练结果

import matplotlib.pyplot as plt

clf = SVR(kernel='rbf', C=1000000, gamma=0.1)  
clf.fit(X_train, y_train)

print clf.score(X_train, y_train)

predict = clf.predict(X_train)  
plt.scatter(predict, y_train, s=2)  
predict_y = clf.predict(X_train.values[4])  
print "predict: %.2f, actually: %.2f" % (predict_y, y_train.values[4])  
plt.plot(predict_y, predict_y, 'ro')  
plt.plot([y_train.min(), y_train.max()], [y_train.min(), y_train.max()], 'k--', lw=2)  

输出结果:

0.999076822472  
predict: 49999.90, actually: 50000.00  

斜线为真实值线,黑点为预测值,从图上可以看到这个模型在训练集上的预测值和真实值基本吻合了。

使用测试数据验证模型准确度

predict_test = clf.predict(X_test)  
print clf.score(X_test, y_test)  
plt.scatter(predict_test, y_test, s=2)  
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'k--', lw=2)  

可以看到数据比较明显地分为了两拨,一拨是比较拟合真实值的斜线,另外一拨是在 x=40000 附近的垂直线。( 不明白为什么会出现这种集中在某个预测值的情况 )

准确率只有 41% ,再用 sklearn 的交叉验证函数跑 10 遍:

from sklearn.model_selection import cross_val_score

y = df['每平米价格']  
X = df[predictors]  
scores = cross_val_score(clf, X, y, cv=10)  
print scores  
print scores.mean()  

cross_val_score 函数会自动将全部数据分为 10 份,然后将其中 9 份数据作为训练集,剩下的一份作为测试集,然后会跑 10 次交叉验证。

( 跑了好久 ... )

输出结果:

[ 0.37906349  0.3947758   0.5191052   0.44455579  0.45646649  0.39464724
  0.4439831   0.4434501   0.41471762  0.39988068]
0.429064549048  

可以看到平均下来的准确度也就 42.9% 。

优化模型

改下惩罚因子试下是否有改善:

clf = SVR(kernel='rbf', C=50000, gamma=0.1)  
clf.fit(X_train, y_train)  
print clf.score(X_train, y_train)  
scores = cross_val_score(clf, X, y, cv=10)  
print scores  
print scores.mean()  

( 又跑了好久 ... )

结果:

0.985096477399  
[ 0.37902499  0.40034578  0.52363507  0.44748625  0.44871473  0.39534226
  0.43854683  0.4470296   0.41939679  0.40100924]
0.43005315467  

嗯,提高了 0.1%。

做下 Grid Search,意思是给出备选参数值,让它自动跑出一个最佳的参数值:

from sklearn.model_selection import GridSearchCV  
parameters = {'C': range(30000, 100000, 10000)}  
svr = SVR(kernel='rbf', gamma=0.1)  
clf = GridSearchCV(svr, parameters, cv=10)  
clf.fit(X, y)  
print clf.best_params_  

( 又跑了好久 ... )

结果:

{'C': 70000}

用 C=70000 重新跑了一次交叉验证:

结果:

[ 0.38117769  0.39773934  0.52183782  0.44646927  0.45482664  0.39650662
  0.44513795  0.44663406  0.41807075  0.4017944 ]
0.431019453481  

又提高了 0.1% ...

看下 scikit-learn 的文档,其中有提到 数据预处理 方面的问题:

个人理解,不太准确,大概的意思是将每个特征值按一定的规则缩放到一个比较小的范围。好比说从 1 到 100,误差可能会较大,但是缩放到从 0 到 1 的范围,误差就相对较小。

预处理之后的模型情况如下:

from sklearn import preprocessing

X = preprocessing.scale(X_train)  
y = y_train  
clf = SVR(kernel='rbf', C=40000, gamma=0.1)  
clf.fit(X, y)  
print "training score: %f" % clf.score(X, y)

X_test = preprocessing.scale(X_test)  
predict_test = clf.predict(X_test)  
print "testing  score: %f" % clf.score(X_test, y_test)  

( 需要注意的是,必须对训练数据和测试数据都进行 scale 操作;之前已做了 Grid Search,scale 之后的训练数据,最佳的 C 参数为 40000 )

结果:

training score: 0.712182291727  
testing  score: 0.575817605735  

可以看到虽然训练集的表现不甚理想,但是测试集的准确度比之前上升了 13% ,达到了 57.6%,看起来效果还是不错的。

对数据做了 scale 之后有一个问题,比如说我有一个新的数据需要预测,这时候我根本不知道应该怎么缩放以适应训练好的模型,所以需要记录下缩放的规则,然后用规则对新数据做同样的 scale 变换,才能进行预测:

scaler = preprocessing.StandardScaler().fit(X_train)  
new_data = [3.0,2.0,1.0,84.0,8.0,6.0,2.0,4.0,3.0,200.0,1.0,1.0,1.0]  
new_data = scaler.transform(new_data)  
print clf.predict(new_data)  

接下来,我们再用所有数据做下交叉验证,看下准确率:

y = df['每平米价格']  
X = preprocessing.scale(df[predictors])

scores = cross_val_score(clf, X, y, cv=10)  
print scores  
print scores.mean()  

结果:

[ 0.55006725  0.54573057  0.61465035  0.52750594  0.59165464  0.57390134
  0.6096038   0.55271578  0.50174115  0.57151666]
0.563908747408  

预测值如下,可以看到不像之前那样分为两拨了:

之后再做了 C 和 gamma 两两组合的 Grid Search,并没有得到比较明显的提升。

Give me more data!

再次收集了近 3000 条数据 (共 12937 条数据),再次按照之前的步骤进行参数选择,重新执行交叉验证,可以看到准确度进一步提升了 2%:

[ 0.58394156  0.57893039  0.61247822  0.54683733  0.59447176  0.62030941
  0.6107769   0.5726037   0.56656728  0.55000072]
0.583691726816  

嗯,考试前多做点习题还是很有用的。

特征选择

特征选择本来应该在训练模型之前进行。特征选择的作用在于分析出影响最大的几个特征值,排除一些次要的特征值,可以提高训练的速度,但是对于准确度来说,可能会提高也可能会降低,说不定。

不过我们在这里做下特征选择,看下影响房价的最重要的五个因素是什么:

from sklearn.feature_selection import RFE  
model = SVR(kernel='linear')  
rfe = RFE(model, 5)  
rfe = rfe.fit(X.values, y.values)  
for i, v in enumerate(rfe.support_):  
    if v:
        print predictors[i]

结果如下:

房
卫
地铁沿线
区
学校

( 说明下,上面的五个特征值的重要性并不是排序的 )

上面说明了这么个结论:在哪个区、是否在地铁附近、有无学位、房间数和卫生间数 是影响房价的最重要的五个因素。

貌似也符合常识。

模型的保存和导入

训练好的模型应当保存下来,方便导入。

from sklearn.externals import joblib  
joblib.dump(clf, './model.pkl')  

导入:

clf = joblib.load('./model.pkl')  

深度学习

接下来尝试用深度学习做回归。

这里使用 Keras 的 scikit-learn 接口包装器 ( keras.wrappers.scikit_learn.KerasRegressor ) 来进行房价的回归估值。

需要导入的模块:

from pandas import read_csv  
import numpy  
from keras.models import Sequential  
from keras.layers import Dense  
from keras.wrappers.scikit_learn import KerasRegressor  
from sklearn import preprocessing  
from sklearn.pipeline import Pipeline  
from sklearn.model_selection import KFold  
from sklearn.model_selection import cross_val_score  
from sklearn.cross_validation import cross_val_predict  
import matplotlib.pyplot as plt  

导入数据、划分训练集和测试集,与之前用 scikit-learn 一样:

train_ = df[0:df.shape[0]-1000]  
test_ = df[df.shape[0]-1000:]

predictors = ['房','厅','卫','建筑面积','建筑年代','朝向','楼层','装修','地铁沿线','地铁距离','区','学校','有无电梯']

y_train = train_['每平米价格']  
X_train = train_[predictors]  
X_test = test_[predictors]  
y_test = test_['每平米价格']  

scale 预处理:

X = preprocessing.scale(X_train)  
y = preprocessing.scale(y_train)

x_scaler = preprocessing.StandardScaler().fit(X_train)  
y_scaler = preprocessing.StandardScaler().fit(y_train)  

( 在使用 SVR 时,我没有做 y 的 scale 操作,因为发现把 y 数据 scale 之后,模型训练了一晚上都没有出结果,不知道是什么原因。)

建立模型:

def nn_model():  
    model = Sequential()
    model.add(Dense(13, input_dim=13, kernel_initializer='normal', activation='relu'))
    model.add(Dense(6, kernel_initializer='normal', activation='linear'))
    model.add(Dense(1, kernel_initializer='normal'))
    model.compile(loss='mae', optimizer='rmsprop')
    return model

Dense 是全连接层,第一层全连接层,我们的数据有 13 个特征值,所以 input_dim 参数为 13。

activation 为激励函数,第一层这里选择了 relu。

然后再加一层全连接层作为隐藏层,激励函数选择 linear,参考这个链接:链接1

损失函数只能选择 mse 或者 mae,我这里用了 mae。

优化器用 rmsprop,当然也可以用 adam 之类的。

接下来使用 KFold 来做交叉验证,参考这个链接:链接2

seed = 7  
numpy.random.seed(seed)  
estimators = []  
estimators.append(('mlp', KerasRegressor(build_fn=nn_model, epochs=200, batch_size=32, verbose=0)))  
pipeline = Pipeline(estimators)  
kfold = KFold(n_splits=5, random_state=seed)  

开始交叉验证:

scores = cross_val_score(pipeline, X, y, cv=kfold)  
print scores  
print scores.mean()

plt.scatter(predicted, y, s=2)  
plt.plot(y, y, 'ro', lw=2)  

结果如下:

需要注意的是,这里的 score 并不像用 scikit-learn 的 SVR 时的准确率,而是 loss 函数的数值,即 mae ( Mean Absolute Error ),因为 accuracy 是不能参考的。参考链接1:

从打印的预测值点图来看,比较发散,并不算理想。

我们尝试改进一下模型,加多一层隐藏层:

def nn_model():  
    model = Sequential()
    model.add(Dense(13, input_dim=13, kernel_initializer='normal', activation='relu'))
    model.add(Dense(6, kernel_initializer='normal', activation='relu'))
    model.add(Dense(3, kernel_initializer='normal', activation='linear'))
    model.add(Dense(1, kernel_initializer='normal'))
    model.compile(loss='mae', optimizer='rmsprop')
    return model

然后将训练轮次增加到 500:

estimators.append(('mlp', KerasRegressor(build_fn=nn_model, epochs=500, batch_size=32, verbose=0)))  

结果:

[ 0.42803601  0.39274284  0.41864015  0.40990664  0.38228733]
0.406322592681  

可以看到 mae 减少了 11%,意味着模型的预测值更加准确了,而预测值点图也说明了这点:

用一个自制的检查准确度的函数来看下,假设预测值与实际值误差不超过 10% 为准确的话:

import math

X_test = test_[predictors]  
y_test = test_['每平米价格']

total = len(y_test)

cnt = 0  
X_test = preprocessing.scale(X_test)  
predicted = model.predict(X_test)  
for i,v in enumerate(predicted):  
    p = y_scaler.inverse_transform(v)
    r = y_test.values[i]
    if math.fabs(r - p)/r < 0.1:
        cnt += 1
print "accuracy: %d/%d" % (cnt, total)  

输出:

accuracy: 268/1000  

额,不到三成 ...

还有一种优化方案,就是链接2中的 Wider Network Topology,加大第一层的神经元数:

def nn_model():  
    model = Sequential()
    model.add(Dense(20, input_dim=13, kernel_initializer='normal', activation='relu'))
    model.add(Dense(10, kernel_initializer='normal', activation='relu'))
    model.add(Dense(5, kernel_initializer='normal', activation='linear'))
    model.add(Dense(1, kernel_initializer='normal'))
    model.compile(loss='mae', optimizer='rmsprop')
    return model

从结果来看确实有所提高准确度:

最后我们捏造两个数值来比较下 Keras 和 SVR 两种模型:

下面两组数据,一个是在天河区靠近地铁三号线 200 米左右有学位的房子;一个是在花都区没挨着地铁没学位的房子,两组数据其他都一样:

房,厅,卫,建筑面积,建筑年代,朝向,楼层,装修,地铁沿线,地铁距离,区,学校,有无电梯
3.0,2.0,2.0,133.0,7.0,2.0,2.0,4.0,3.0,200.0,1.0,1.0,1.0  
3.0,2.0,2.0,133.0,7.0,2.0,2.0,4.0,0.0,0.0,9.0,1.0,1.0  

结果:

| 房源 | Keras    | SVR      |
| 天河 | 80271.16 | 69902.54 |
| 花都 | 11703.23 | 14411.85 |

都正确地预估出天河的房子要比花都的要贵好多。

就这两组数据来说,个人感觉 Keras 比 SVR 更符合现实 ...

搭个页面应用

最后我们用 Flask 搭个简单的单页应用来做个房源估价应用。

使用的是之前训练出来的 SVR 模型。

结论

  • 得益于各种开源项目,目前上手机器学习和深度学习,门槛降低了很多;
  • 房价还在疯狂上涨,我去 ...