1.2 基于scikit-learn实现房价预测
本节将使用scikit-learn来实现房价预测。首先使用决定系数R2Score来做一个小测验;然后使用决策树回归器配合不同的深度值来创建和对比模型;最后使用网格搜索(grid search)来查找最佳模型,以便进行房价预测。
1.2.1 衡量R2值
通过sklearn提供的r2_score()函数来计算拟合优度(Goodness of Fit),R2值越接近1表明拟合的程度越高;反之,R2值越小,则说明回归直线的拟合程度越低。
# 导入r2_score from sklearn.metrics import r2_score # 定义函数,计算目标值和预测值之间的分值 def performance_metric(y_true,y_predict): score=r2_score(y_true,y_predict) return score
这里有一个小测验,假设有5个真实数值和5个预测数值,分别是[3,−0.5,2,7,4.2]和[2.5,0.0,2.1,7.8,5.3],然后通过r2_score()函数来看它的拟合优度的程度值。
# 真实数值 test_y_true=[3,-0.5,2,7,4.2] # 预测数值 test_y_predict=[2.5,0.0,2.1,7.8,5.3] # 通过r2_score函数来计算 score=performance_metric(test_y_true,test_y_predict) print("决定系数,R^2=:{}。".format(score))
输出如下。
决定系数,R^2=:0.9228556485355649。
可以看到,R2值非常接近1,说明相关程度高。
1.2.2 模型性能对比
我们将通过4个不同的深度值创建4个不同的模型,然后来对比这4个模型的性能表现,最后通过图表直观显示。现在,我们把房屋特征和价格的数据进行清洗和分割。
# 从sklearn库里导入train_test_split方法 from sklearn.model_selection import train_test_split # train_test_split()方法用来清洗和分割数据 # 参数1:特征样本,就是房屋特征数据 # 参数2:特征样本对应的目标(房屋)价格 # 参数3:分配给测试集的大小为0.1,也就是10%的数据用于测试、90%的数据用于训练 # 参数4:random_state表示随机数生成器的种子,如果希望第二次调用train_test_split()方法 # 的结果和第一次调用的结果一致,那么就可以设置一个值,多少都可以,生产环境不要设值 X_train, X_test, y_train, y_test = train_test_split(features, prices, test_size=0.1, random_state=50) print("X_train.shape={}, y_train.shape={}.".format(X_train.shape, y_train.shape)) print("X_test.shape={}, y_test.shape={}.".format(X_test.shape, y_test.shape))
输出如下。
X_train.shape=(440,3),y_train.shape=(440,). X_test.shape=(49,3),y_test.shape=(49,).
得到440行的数据用于训练,49行的数据用于测试。接下来,我们定义一个函数来衡量模型的性能,深度值分别是1、3、6、10,然后输出这些模型的图,便于更直观地对比。
# 导入绘图库 import matplotlib.pyplot as plt # 使用matplotlib在Jupyter Notebook中绘图时,需要使用这个 %matplotlib inline # 导入sklearn的清洗分割、学习曲线和决策树回归器的对象和函数 from sklearn.model_selection import ShuffleSplit, learning_curve from sklearn.tree import DecisionTreeRegressor # 定义模型的性能对比函数 # 通过不同大小的深度值来创建for循环里的模型,然后以图的形式展现 def ModelLearningGraphMetrics(X, y): # 清洗和分割数据对象定义 # 参数1:n_splits表示重新清洗和分割数据的迭代次数,默认值为10 # 参数2:test_size=0.2表示有0.2的数据用于测试,也就是20%的数据用于测试,80%的数据用于训练 # 参数3:random_state表示随机数生成器的种子,如果希望第二次调用ShuffleSplit()方法 # 的结果和第一次调用的结果一致,那么就可以设置一个值,多少都可以,生产环境不要设值 cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0) # 生成训练集大小 # 函数np.rint()是计算数组各元素的四舍五入的值到最近的整数 # 函数np.linspace(start_i, stop_i, num)表示从起始值到结束值之间,以均匀的间隔返回指定个数的值。 # 那么这里就是从1开始、以X结束的总行数的80%的数据,数据间隔是9,最后将数据元素都转换成整型 train_sizes = np.rint(np.linspace(1, X.shape[0]*0.8 - 1, 9)).astype(int) # 创建一个绘图窗口,大小10×7,单位是英寸(inch) fig = plt.figure(figsize=(10, 7)) # 根据深度值创建不同的模型 # 这里的深度值就是1、3、6、10这4个 for k, depth in enumerate([1,3,6,10]): # 根据深度(max_depth)值来创建决策树回归器 regressor = DecisionTreeRegressor(max_depth=depth) # 通过学习曲线函数计算训练集和测试集的分值 # 参数1:评估器,这里就是决策树回归器 # 参数2:特征样本,房屋特征 # 参数3:目标标签,房屋价格 # 参数4:训练样本的个数,这是用来生成学习曲线的 # 参数5:交叉验证生成器,或可迭代对象 # 参数6:评分器,是一个可调用对象 sizes, train_scores, test_scores = learning_curve( regressor, X, y, train_sizes=train_sizes, cv=cv, scoring='r2') # 计算训练集分值和测试集分值的标准差 train_std = np.std(train_scores, axis=1) test_std = np.std(test_scores, axis=1) # 计算训练集分值和测试集分值的均值 train_mean = np.mean(train_scores, axis=1) test_mean = np.mean(test_scores, axis=1) # 根据学习曲线值来绘制图,四个图的位置通过k+1来控制 ax = fig.add_subplot(2, 2, k+1) # 绘制训练得分线,plot()方法 # 参数1:X轴方向的值 # 参数2:y轴方向的值 # 参数3:绘制出来的线的样式风格,比如这里的“o”表示一个圆点标记,而“-”表示实线 # 参数4:绘制的线的颜色 # 参数5:图例上的标题 ax.plot(sizes, train_mean, 'o-', color='r', label='Training Score') # 绘制测试得分线 ax.plot(sizes, test_mean, 'o-', color='g', label='Testing Score') # fill_between()方法表示为训练得分线描边 # 参数1:X轴方向的值 # 参数2:y轴方向的覆盖下限 # 参数3:y轴方向的覆盖上限 # 参数4:设置覆盖区域的透明度 # 参数5:设置覆盖区域的颜色 ax.fill_between(sizes, train_mean - train_std, train_mean + train_std, alpha=0.15, color='r') # fill_between()方法表示为测试得分线描边 ax.fill_between(sizes, test_mean - test_std, test_mean + test_std, alpha=0.15, color='g') # 在绘图的窗口上添加标题 ax.set_title('max_depth = {}'.format(depth)) # 设置X轴的标题 ax.set_xlabel('Number of Training Points') # 设置y轴的标题 ax.set_ylabel('Score') # 设置X轴方向的最小值和最大值 ax.set_xlim([0, X.shape[0]*0.8]) # 设置y轴方向的最小值和最大值 ax.set_ylim([-0.05, 1.05]) # 添加图例 ax.legend(bbox_to_anchor=(1.05, 2.05), loc='lower left', borderaxespad=0.) # 添加图形总标题 fig.suptitle('Decision Tree Regressor Learning Performances', fontsize=16, y=1.03) # 自动调整subplot符合图的区域的参数的布局。生产环境中不要使用该函数,因为这是一个实验特性函数 fig.tight_layout() # 显示绘图 fig.show() ModelLearningGraphMetrics(features, prices)
输出的图形如图1.6所示。
图1.6 决策树回归器的模型性能表现
通过上面4个模型的图形对比,我们发现max_depth=3的模型表现不错;max_depth=1的模型有些欠拟合;max_depth=10的模型有些过拟合;而max_depth=6的模型训练集得分高,但是测试集得分有些低,略微过拟合。
我们还可以通过观察max_depth等于1到10的值来进行模型的性能对比,最后通过输出图形来看它们的表现。比如以后在其他场景中,读者也可以使用下述策略,会更好地找到最佳值模型。
# 导入sklearn库的数据清洗与分割函数,验证曲线函数 from sklearn.model_selection import ShuffleSplit, validation_curve # 定义模型在复杂度高的情况下性能表现的函数 # 随着模型复杂度的增加计算它的性能表现 def ModelComplexityPerformanceMetrics(X, y): # 清洗和分割数据对象定义 # 参数1:n_splits表示重新清洗和分割数据的迭代次数,默认值为10 # 参数2:test_size=0.2表示有0.2的数据用于测试,也就是20%的数据用于测试,80%的数据用于训练 # 参数3:random_state表示随机数生成器的种子,如果希望第二次调用ShuffleSplit()方法 # 的结果和第一次调用的结果一致,那么就可以设置一个值,多少都可以,生产环境不要设值 cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0) # 定义从1到10为深度(max_depth)的参数值 max_depth = np.arange(1,11) # 通过不同的max_depth的参数值来计算训练集和测试集的分值 # 参数1:评估器,这里是决策树回归器 # 参数2:特征样本,房屋特征 # 参数3:目标标签,房屋价格 # 参数4:传入的深度参数名称 # 参数5:传入的深度参数范围值 # 参数6:交叉验证生成器,或可迭代对象 # 参数7:评分器,是一个可调用对象 train_scores, test_scores = \ validation_curve(DecisionTreeRegressor(), X, y, param_name="max_depth", param_range=max_depth, cv=cv, scoring='r2') # 计算训练集分值和测试集分值的均值 train_mean = np.mean(train_scores, axis=1) test_mean = np.mean(test_scores, axis=1) # 计算训练集分值和测试集分值的标准差 train_std = np.std(train_scores, axis=1) test_std = np.std(test_scores, axis=1) # 绘制验证分值的曲线图 # figsize表示要绘制的图形窗口大小,单位是英寸 plt.figure(figsize=(7, 5)) # 在绘制的图形窗口上添加一个标题 plt.title('Decision Tree Regressor Complexity Performance') # 绘制训练得分线,plot()方法 # 参数1:X轴方向的值 # 参数2:y轴方向的值 # 参数3:绘制出来的线的样式风格,比如这里的“o”表示一个圆点标记,而“-”表示实线 # 参数4:绘制的线的颜色 # 参数5:图例上的标题 plt.plot(max_depth, train_mean, 'o-', color='r', label='Training Score') # 绘制测试得分线 plt.plot(max_depth, test_mean, 'o-', color='g', label='Validation Score') # fill_between()方法表示为两条曲线描边,第一条是训练得分线,第二条是测试得分线 # 参数1:X轴方向的值 # 参数2:y轴方向的覆盖下限 # 参数3:y轴方向的覆盖上限 # 参数4:设置覆盖区域的透明度 # 参数5:设置覆盖区域的颜色 plt.fill_between(max_depth, train_mean - train_std, \ train_mean + train_std, alpha=0.15, color='r') plt.fill_between(max_depth, test_mean - test_std, \ test_mean + test_std, alpha=0.15, color='g') # 图上加标题注解 # 添加图例 plt.legend(loc='lower right') # 添加X轴的标题 plt.xlabel('Maximum Depth') # 添加y轴的标题 plt.ylabel('Score') # 设置y轴方向的最小值和最大值 plt.ylim([-0.05,1.05]) # 显示绘图 plt.show() ModelComplexityPerformanceMetrics(features, prices)
输出的图形如图1.7所示。
图1.7 决策树回归器的模型性能表现
从结果图上看,max_depth的参数值在1到10中时,max_depth=4是最佳位置,当max_depth>4时模型表现就有些过拟合了。
1.2.3 网格搜索模型
网格搜索最重要的功能就是能够自动调参,只要用户把参数传进去,它就能返回最优的结果模型和参数。它只适用于小数据集,同时还有一个缺点,那就是可能调参到局部最优,而不是全局最优。
# 从sklearn库导入网格搜索VC、数据清洗与分割、决策树和分值计算对象的函数 from sklearn.model_selection import GridSearchCV, ShuffleSplit from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import make_scorer # 定义网格搜索最佳模型函数 def gridSearchVC_fit_model(X, y): # 清洗和分割数据对象定义, # 参数1:n_splits表示重新清洗和分割数据的迭代次数,默认值为10 # 参数2:test_size=0.2表示有0.2的数据用于测试,也就是20%的数据用于测试,80%的数据用于训练 # 参数3:random_state表示随机数生成器的种子,如果希望第二次调用ShuffleSplit()方法 # 的结果和第一次调用的结果一致,那么就可以设置一个值,多少都可以,生产环境不要设值 cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0) # 创建决策树回归器对象 regressor = DecisionTreeRegressor(random_state=0) # 创建一个字典,表示max_depth的参数值是从1到10 # 注意:如果代码运行的环境是Python 2,去掉这个list()函数调用 params = { "max_depth" : list(range(1, 10)) } # 通过make_scorer()函数将上面定义的performance_metric()函数转换成计算分值函数 scoring_fnc = make_scorer(score_func=performance_metric) # 创建网格搜索对象 # 参数1:评估器,就是回归器,这里表示的是决策树回归器 # 参数2:网格搜索参数 # 参数3:计算分值函数 # 参数4:cv(Cross-Validation)交叉验证,传入交叉验证生成器,或者可迭代对象 grid = GridSearchCV(estimator=regressor, param_grid=params, scoring=scoring_fnc, cv=cv) # 根据数据计算/训练适合网格搜索对象的最佳模型 grid = grid.fit(X, y) # 返回计算得到的最佳模型 return grid.best_estimator_ # 网格搜索函数得到最佳模型 reg = gridSearchVC_fit_model(X_train, y_train) print("参数max_depth={}是最佳模型。".format(reg.get_params()['max_depth']))
输出如下。
参数max_depth=4是最佳模型。
1.2.4 波士顿房价预测
在预测波士顿房价前,我们先做一个模拟客户数据来预测,数据如表1.1所示。
表1.1 模拟房屋数据客户1
# 小测验 # 假设有以下3个客户的数据,分别是 client_data = [[3, 30, 10], # 客户1 [7, 20, 19], # 客户2 [9, 2, 9]] # 客户3 # 尝试预测 for i, price in enumerate(reg.predict(client_data)): print("预测客户{}的销售价格是${:,.2f}".format(i+1, price))
输出如下。
预测客户1的销售价格是$320,425.00 预测客户2的销售价格是$410,658.62 预测客户3的销售价格是$938,053.85
定义预测波士顿房价的函数,经过10次迭代,然后看下对比图。
# 预测房价函数 def PredictHousingPrice(X, y, fitter): # 迭代10次 epochs = 10 # 存储预测的价格 y_predict_test_price = None # 分割训练集和测试集数据,20%的数据用于测试,80%的数据用于训练 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0) # 迭代训练 for epoch_i in range(epochs): # 根据数据训练模型,并返回最佳模型 reg = fitter(X_train, y_train) # 预测测试集数据 predicted_price = reg.predict(X_test) # 将预测到的结果存起来 y_predict_test_price = predicted_price print("迭代第{}次。".format(epoch_i+1)) return y_test, y_predict_test_price y_true_price, y_predict_price = \ PredictHousingPrice(features, prices, gridSearchVC_fit_model)
输出如下。
迭代第1次。 迭代第2次。 迭代第3次。 …… 迭代第9次。 迭代第10次。
查看真实房价的前5行数据。我们先将y_true_price转换成Series对象,然后在调用reset_index()函数时就会把数据创建成一个新的Series,旧的索引就会被作为数据的一列,再通过drop()函数删除旧的索引那一列。
pd.Series(y_true_price).reset_index().drop('index', axis=1).head()
输出如表1.2所示。
表1.2 查看真实房价的前5行数据
我们再来看预测房价的前5行数据。
pd.Series(y_predict_price).head()
输出如表1.3所示。
表1.3 查看预测房价的前5行数据
构建真实房价和预测房价的对比图函数。
# 显示真实房价和预测房价对比图 def plotVersusFigure(y_true_price, y_predict_price): # 创建一个10×7的窗口 plt.figure(figsize=(10, 7)) # 绘制的图1是真实房价 X_show = np.rint(np.linspace(1, np.max(y_true_price), len(y_true_price)) ).astype(int) # 绘制图1线,plot()方法: # 参数1:X轴方向的值,真实房价从最低价到最高价 # 参数2:y轴方向的值,真实房价的值 # 参数3:绘制出来的线的样式风格,比如这里的“o”表示一个圆点标记,而“-”表示实线 # 参数4:绘制的线的颜色,这里是青色 plt.plot(X_show, y_true_price, 'o-', color='c') # 绘制的图2是预测房价,叠加在图1上 X_show_predicted = np.rint(np.linspace(1, np.max(y_predict_price), len(y_predict_price)) ).astype(int) # 绘制图2线,plot()方法: # 参数1:X轴方向的值,预测房价从最低价到最高价 # 参数2:y轴方向的值,预测房价的值 # 参数3:绘制出来的线的样式风格,比如这里的“o”表示一个圆点标记,而“-”表示实线 # 参数4:绘制的线的颜色,这里是洋红色 plt.plot(X_show_predicted, y_predict_price, 'o-', color='m') # 添加标题 plt.title('Housing Prices Prediction') # 添加图例 plt.legend(loc='lower right', labels=["True Prices", "Predicted Prices"]) # 添加X轴的标题 plt.xlabel("House's Price Tendency By Array") # 添加y轴的标题 plt.ylabel("House's Price") # 显示绘制 plt.show()
输出打印波士顿真实房价和预测房价的对比图。
# 波士顿房屋价格对比图 plotVersusFigure(y_true_price,y_predict_price)
输出如图1.8所示。
图1.8 波士顿真实房价与预测房价对比图
通过结果可知,预测房价与真实房价之间是有一定误差的。这需要我们不断地调整参数并重新训练、预测等。
1.2.5 北京房价预测
读取bj_housing.csv文件进行北京房价预测,其中一共有9999条数据。训练和预测使用的代码都在波士顿房价预测小节中详细介绍过,这里不再重复。所以本小节的代码就是读取数据,训练模型,预测和绘图显示。这里的北京房屋数据是真实的,但它的预测结果肯定受宏观环境等多种因素的影响,可能会造成预测结果不精准。读取数据代码如下。
df=pd.read_csv('bj_housing.csv') df.describe()
输出如图1.9所示。
图1.9 北京房价基本信息统计
数据的各列名称解释如下。
◇ Area:房屋面积,单位是平方米。
◇ Value:房屋售价,单位是万元。
◇ Room:房间数,单位是间。
◇ Living:厅数,单位是间。
◇ School:是否为学区房,0(表“否”)或1(表“是”)。
◇ Year:房屋建造年份。
◇ Floor:房屋所处楼层。
然后,将房屋特征数据集和真实价格数据集分开,代码如下。
bj_prices=df['Value'] bj_features=df.drop('Value',axis=1) bj_features.head()
房屋特征数据集前5行数据输出如图1.10所示。
图1.10 北京房屋特征数据集前5行数据
根据数据训练和预测模型。
y_true_bj_price,y_predict_bj_price=\ PredictHousingPrice(bj_features,bj_prices,gridSearchVC_fit_model)
输出如下。
迭代第1次。 迭代第2次。 迭代第3次。 …… 迭代第9次。 迭代第10次。
查看北京真实房价的前5行数据,先将y_true_bj_price转换成Series对象,然后在调用reset_index()时函数就会把数据创建成一个新的Series,旧的索引就会被作为该数据对象的一列,再通过drop()函数删除旧的索引那一列。
y_true_bj_price.reset_index().drop('index',axis=1).head()
输出如表1.4所示。
表1.4 查看真实房价的前5行数据
我们再来看预测房价的前5行数据。
pd.Series(y_predict_bj_price).head()
输出如表1.5所示。
表1.5 查看预测房价的前5行数据
构建真实房价和预测房价的对比图。
plotVersusFigure(y_true_bj_price,y_predict_bj_price)
绘制的结果如图1.11所示。
图1.11 北京真实房价与预测房价对比图
通过对比图我们可以看到,预测房价与真实房价之间还是有一定误差的,这需要我们不断地去调整参数重新训练模型和预测等。