0%

Boston housing Predictor

机器学习实验项目_波士顿房价预测

0. 项目背景

这是一个几年之前的机器学习练习项目。数据来源是UCI的机器学习知识库数据集。该数据从1978年开始,共506个数据点,涵盖了马萨诸塞州波士顿郊区的房屋信息数据。

在此项目中,我会使用这些数据训练并测试一个可用的预测模型,并对模型的性能和预测能力进行测试。训练好的模型可以对房屋的价值进行预测,对某些工作中,应用这个模型可以帮助他们了解房屋的潜在价值,是非常实用的。

从学习的角度来看,做这个项目则是可以初步了解机器学习的实践方法,了解如何处理数据,并熟悉 python 和 sklearn在项目中的使用。

提示:Code 和 Markdown 区域可通过 Shift + Enter 快捷键运行。此外,Markdown可以通过双击进入编辑模式。


1. 导入数据

本项目的数据集来自UCI机器学习知识库(数据集已下线)
可以看到,波士顿房屋这些数据于1978年开始统计,共506个数据点,涵盖了麻省波士顿不同郊区房屋14种特征的信息。
在此,首先我将针对本项目对原始数据集做以下处理:观察数据 -> 移除异常值 -> 思考哪些是和目标相关的特征。
具体处理如下:

  • 有16个'MEDV' 值为50.0的数据点被移除。 因为这些数据点包含遗失或者不明确的数值。
  • 有1个数据点的 'RM' 值为8.78. 这是一个异常值,已经被移除。
  • 对于本项目,房屋的'RM''LSTAT''PTRATIO'以及'MEDV'特征是必要的,其余不相关特征已经被移除。
  • 'MEDV'特征的值已经过必要的数学转换,可以反映35年来市场的通货膨胀效应。

运行下面区域的代码后,我就可以载入波士顿房屋数据集,以及一些此项目所需的Python库。
我会从 csv 文件中读取到需要的数值,如果成功返回数据集的大小,则表示数据集已载入成功。

1
2
3
4
5
6
7
8
9
10
11
12
# import 
import numpy as np
import pandas as pd
import visuals as vs # import supplement

# check Python version
from sys import version_info
if version_info.major != 2 and version_info.minor != 7:
raise Exception('Please use Python 2.7')

# show result in notebook
%matplotlib inline
1
2
3
4
5
6
7
# 载入波士顿房屋的数据集
data = pd.read_csv('housing.csv')
prices = data['MEDV']
features = data.drop('MEDV', axis = 1)

# 完成
print "Boston housing dataset has {} data points with {} variables each.".format(*data.shape)
Boston housing dataset has 489 data points with 4 variables each.

2. 分析数据

我需要继续观察目前得到的数据,以便在后续阶段能够理解对数据进行分析后得到的结果或对结果进行分析。

由于我的最终目标是将实现一个表现良好的房屋价值预估的模型,所以我依然需要把完整的数据集分成featuretarget

  • 特征'RM''LSTAT',和 'PTRATIO'
  • 目标变量'MEDV',是我希望预测的变量。

我用 featuresprices两个变量进行存储。

2.1 基础统计运算

我导入了numpy 执行计算。这些统计数据对于分析模型的预测结果非常重要的。

  • 计算prices中的'MEDV'的最小值、最大值、均值、中值和标准差;
  • 将运算结果储存在相应的变量中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

#目标:计算价值的最小值
minimum_price = np.min(prices)

#目标:计算价值的最大值
maximum_price = np.max(prices)

#目标:计算价值的平均值
mean_price = np.mean(prices)

#目标:计算价值的中值
median_price = np.median(prices)

#目标:计算价值的标准差
std_price = np.std(prices)

#目标:输出计算的结果
print "Statistics for Boston housing dataset:\n"
print "Minimum price: ${:,.2f}".format(minimum_price)
print "Maximum price: ${:,.2f}".format(maximum_price)
print "Mean price: ${:,.2f}".format(mean_price)
print "Median price ${:,.2f}".format(median_price)
print "Standard deviation of prices: ${:,.2f}".format(std_price)
Statistics for Boston housing dataset:

Minimum price: $105,000.00
Maximum price: $1,024,800.00
Mean price: $454,342.94
Median price $438,900.00
Standard deviation of prices: $165,171.13

2.2 特征观察

如前文所述,本项目中我们关注的是其中三个值:'RM''LSTAT''PTRATIO',对每一个数据点:

  • 'RM' 是该地区中每个房屋的平均房间数量;
  • 'LSTAT' 是指该地区有多少百分比的业主属于是低收入阶层(有工作但收入微薄);
  • 'PTRATIO' 是该地区的中学和小学里,学生和老师的数目比(学生/老师)。

凭直觉,上述三个特征中对每一个来说,你认为增大该特征的数值,'MEDV'的值会是增大还是减小呢?

  • RM 变大,MEDV 越大,因为房间多整体空间更大
  • LSTAT 越大,MEDV 越小,因为代表着当地经济价值比较低
  • PTRATIO 越大,MEDV 越低,因为该地区学生多教师少,教育价值降低了

2.2 数据分割与重排

接下来,我需要把波士顿房屋数据集分成训练和测试两个子集。
通常在这个过程中,数据也会被重排列,以消除数据集中由于顺序而产生的偏差。

我使用 sklearn.model_selection 中的 train_test_split, 将featuresprices的数据都分成用于训练的数据子集和用于测试的数据子集。

  • 分割比例为:80%的数据用于训练,20%用于测试;
  • 选定一个数值以设定 train_test_split 中的 random_state ,这会确保结果的一致性;
1
2
3
# 提示: 导入train_test_split
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(features, prices, test_size=0.2, random_state=22)

2.3 训练及测试

将数据集按一定比例分为训练用的数据集和测试用的数据集对学习算法有什么好处?

如果用模型已经见过的数据,例如部分训练集数据进行测试,又有什么坏处? 如果没有数据来对模型进行测试,会出现什么问题?

  • 将数据集按照一定比例分割,我们可以合理评估学习算法在面对未知数据时的表现效果
  • 用来评价模型的数据如果不是独立于样本的,则可能评价结果是不准确的情况。我们也无法得出,这个算法是否可以应用在更广泛的情况

3. 模型衡量标准

在项目的第三步中,你需要了解必要的工具和技巧来让你的模型进行预测。用这些工具和技巧对每一个模型的表现做精确的衡量可以极大地增强你预测的信心。

3.1 定义衡量标准

如果不能对模型的训练和测试的表现进行量化地评估,就很难衡量模型的好坏。一般情况下,我们可以定义一些衡量标准,这些标准可以通过对某些误差或者拟合程度的计算来得到。在这个项目中,你将通过运算决定系数 R2 来量化模型的表现。模型的决定系数是回归分析中十分常用的统计信息,经常被当作衡量模型预测能力好坏的标准。

R2的数值范围从0至1,表示目标变量的预测值和实际值之间的相关程度平方的百分比。一个模型的R2 值为0还不如直接用平均值来预测效果好;而一个R2 值为1的模型则可以对目标变量进行完美的预测。从0至1之间的数值,则表示该模型中目标变量中有百分之多少能够用特征来解释。模型也可能出现负值的R2,这种情况下模型所做预测有时会比直接计算目标变量的平均值差很多。

在下方代码的 performance_metric 函数中,我实现了:

  • 使用 sklearn.metrics 中的 r2_score 来计算 y_truey_predict的R2值,作为对其表现的评判。
  • 将他们的表现评分储存到score变量中。

或(:这个并没有实现:)

  • (可选) 不使用任何外部库,参考决定系数的定义进行计算,这也可以帮助你更好的理解决定系数在什么情况下等于0或等于1。
1
2
3
4
5
6
7
8
9
# 导入r2_score
from sklearn.metrics import r2_score

def performance_metric(y_true, y_predict):
"""计算并返回预测值相比于预测值的分数"""

score = r2_score(y_true, y_predict)

return score
1
2
3
4
5
6
7
8
# 不导入任何计算决定系数的库

def performance_metric2(y_true, y_predict):
"""计算并返回预测值相比于预测值的分数"""

score = None

return score

3.2 拟合程度

假设一个数据集有五个数据且一个模型做出下列目标变量的预测:

真实数值 预测数值
3.0 2.5
-0.5 0.0
2.0 2.1
7.0 7.8
4.2 5.3

怎么样可以判断,这个模型已成功地描述了目标变量的变化?

通过运行下方的代码,我可以使用performance_metric函数来计算模型的决定系数。

1
2
3
# 计算这个模型的预测结果的决定系数
score = performance_metric([3, -0.5, 2, 7, 4.2], [2.5, 0.0, 2.1, 7.8, 5.3])
print "Model has a coefficient of determination, R^2, of {:.3f}.".format(score)
Model has a coefficient of determination, R^2, of 0.923.
  • 这个模型以及较为成功地描述了目标变量的变化。
  • 因为可以看到 R^2 计算结果为 0.923 意味着数据超过90的部分能够使用模型进行特征的描述和预测

4. 分析模型的表现

在项目的第四步,可以来看一下不同参数下,模型在训练集和验证集上的表现。
我只是用了一个特定的算法(带剪枝的决策树),在参数选择上,这个算法只包括了选择参数 'max_depth'
我会用全部训练集训练,但选择不同'max_depth' 参数,观察这个参数的变化如何影响模型的表现。
最后,画出模型的表现来对于分析过程十分有益,这可以让我直观地看到模型的表现。

4.1 学习曲线

  • 下方区域内的代码可以输出四幅图像,他们分别表现了一个决策树模型在不同最大深度下的表现。
  • 每一条曲线都直观得显示了随着训练数据量的增加,模型学习曲线的在训练集评分和验证集评分的变化。
  • 评分使用决定系数R2。曲线的阴影区域代表的是该曲线的不确定性(用标准差衡量)。
1
2
# 根据不同的训练集大小,和最大深度,生成学习曲线
vs.ModelLearning(X_train, y_train)

png

4.2 对学习曲线的观察

所以从上面的图形中,我应该怎么决定合适的最大深度呢?

其次,随着寻来拿数据量的增加,训练集曲线的评分正在发生变化,验证集曲线也有变化。我也需要考虑数据继续增加的情况下,如何保持或者有效提升模型的表现呢?但是学习曲线看上去最终都很稳定,是不是会最终收敛到一个特定的值。

  • 我想选择 max = 3,也就是将深度设定为3.
  • 从曲线的观察上来看,随着训练数据量的增加,训练集曲线评分降低了一点,但趋于稳定。
  • 随着训练数据量的增加,验证集曲线迅速上升后也趋于稳定
  • 如果有更多训练数据,且新训练数据分布和现有相同,模型的表现不会有什么明显提升,因为现在已经能够捕捉数据的全部特征了。

4.3 复杂度曲线

下列代码内的区域会输出一幅图像,用于展示了一个已经经过训练和验证的决策树模型在不同最大深度条件下的表现。

这个图形将包含两条曲线,一个是训练集的变化,一个是验证集的变化。跟学习曲线相似,阴影区域代表该曲线的不确定性,模型训练和测试部分的评分都用的 performance_metric 函数。

1
2
# 根据不同的最大深度参数,生成复杂度曲线
vs.ModelComplexity(X_train, y_train)

png

4.4 偏差(bias)与方差(variance)之间的权衡取舍

当模型以最大深度 1训练时,模型的预测是出现很大的偏差还是出现了很大的方差?当模型以最大深度10训练时,情形又如何呢?图形中的哪些特征能够支持你的结论?

我要如何判断模型是否出现了偏差很大或者方差很大的问题?

  • 模型深度为 1 时,偏差大。
  • 因为可以看到模型预测在训练集上的得分低,说明模型预测不准确。
  • 而当模型深度为 10 时,方差大。
  • 可以看到模型预测在训练集上的得分高,接近1,说明预测准确。但模型在训练集上表现较好,但测试集上表现差,说明面对新数据时表现不好。

4.5 最优模型的猜测

我应该选择哪一个最大深度,才能对未来(未知)的数据进行预测。

  • 按照图 5 来看可以选择深度为 6 的模型。
  • 个人认为,深度为 4 时,方差最小,但偏差仅仅超过0.8。
  • 随着深度增加,偏差仍在迅速缩小,而在 6 之后,偏差开始趋于稳定,处在0.9左右。
  • 此时方差看上去增大了0.1左右,可以接受。

5. 选择最优参数

5.1 网格搜索(Grid Search)

什么是网格搜索法?如何用它来优化模型?

  • 网格搜索,是一种系统地遍历多种参数组合,通过交叉验证确定最佳效果参数的方法
  • 使用网格搜索优化模型,可以对不同参数组合进行评估,得到最合适的参数组合,优化模型

5.2 交叉验证

  • 什么是K折交叉验证法(k-fold cross-validation)?
  • GridSearchCV是如何结合交叉验证来完成对最佳参数组合的选择的?
  • GridSearchCV中的'cv_results_'属性能告诉我们什么?
  • 网格搜索时如果不使用交叉验证会有什么问题?交叉验证又是如何解决这个问题的?

在下面 fit_model函数最后加入 print pd.DataFrame(grid.cv_results_) 可以查看更多信息。

  1. 什么是K折交叉验证法(k-fold cross-validation)?

    • K折交叉验证,是将训练数据(验证集)平分到 k 个容器。在每次实验时,选择其中 1 个容器中的数据作为验证集,其他(k-1)个容器作为训练集。
    • 继续训练模型,并进行验证。
    • 交叉验证会在过程中进行 k 次实验,然后取得 k 次实验测试结果的平均值。
  2. 网格搜索如何结合交叉验证完成最佳参数选择?

    • 这种方法中我们对每 1 个参数组合进行 1 次 K 折较差验证,得到平均分数。
    • 一般选择均分最高的参数组合作为最优参数,但如果品分标准是loss,会选择评分最低的参数组合。
  3. cv_results_属性代表什么?

    • cv_results_可以用来来获得最优参数组合
    • 它返回了一个字典,其中记录了每一组网格参数每一次实验时的结果,如时间、评估值、其他统计信息
  4. 网格搜索时如果不使用交叉验证会有什么问题?交叉验证又是如何解决这个问题的?

    • 网格搜索不使用交叉验证时所花费的训练时间更短,但模型参数并不是最优;
    • 交叉验证进行更多次的试验,使用了全部训练集,能够对每一个参数组合得出更准确的评分。

5.3 决策树算法

下面,我会使用决策树算法训练一个模型。

为了得出的是一个最优模型,我需要使用网格搜索法训练模型,以找到最佳的 'max_depth' 参数。我可以把'max_depth' 参数理解为决策树算法在做出预测前,可以允许的对数据提出问题的数量。而决策树也是监督学习算法中的一种。

在下方 fit_model 函数中,我进行了如下的定义:

  1. 定义 'cross_validator' 变量: 使用 sklearn.model_selection 中的 KFold 创建一个交叉验证生成器对象;
  2. 定义 'regressor' 变量: 使用 sklearn.tree 中的 DecisionTreeRegressor 创建一个决策树的回归函数;
  3. 定义 'params' 变量: 为 'max_depth' 参数创造一个字典,它的值是从1至10的数组;
  4. 定义 'scoring_fnc' 变量: 使用 sklearn.metrics 中的 make_scorer 创建一个评分函数;
    ‘performance_metric’ 作为参数传至这个函数中;
  5. 定义 'grid' 变量: 使用 sklearn.model_selection 中的 GridSearchCV 创建一个网格搜索对象;将变量'regressor', 'params', 'scoring_fnc''cross_validator' 作为参数传至这个对象构造函数中;

关于python函数的默认参数定义和传递,可以参考的资料是,这个MIT课程的视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#提示: 导入 'KFold' 'DecisionTreeRegressor' 'make_scorer' 'GridSearchCV' 
from sklearn.model_selection import KFold,GridSearchCV
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import make_scorer

def fit_model(X, y):
""" 基于输入数据 [X,y],利于网格搜索找到最优的决策树模型"""

cross_validator = KFold(n_splits=10)

regressor = DecisionTreeRegressor()

params = {'max_depth':range(1,11)}

scoring_fnc = make_scorer(performance_metric)

grid = GridSearchCV(regressor, params, scoring=scoring_fnc,cv=cross_validator)

# 基于输入数据 [X,y],进行网格搜索
grid = grid.fit(X, y)

# 返回网格搜索后的最优模型
return grid.best_estimator_

5.4 训练最优模型(*没有实现)

另外一种方法是在下方 fit_model 函数中:

  • 遍历参数‘max_depth’的可选值 1~10,构造对应模型
  • 计算当前模型的交叉验证分数
  • 返回最优交叉验证分数对应的模型

但这一个方法还没有完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Not finished!

'''
不允许使用 DecisionTreeRegressor 以外的任何 sklearn 库

提示: 你可能需要实现下面的 cross_val_score 函数

def cross_val_score(estimator, X, y, scoring = performance_metric, cv=3):
""" 返回每组交叉验证的模型分数的数组 """
scores = [0,0,0]
return scores
'''

def fit_model2(X, y):
""" 基于输入数据 [X,y],利于网格搜索找到最优的决策树模型"""

#最优交叉验证分数对应的最优模型
best_estimator = None

return best_estimator

5.5 最优模型

最优模型的最大深度(maximum depth)是多少?

通过下面的代码,我能够将决策树回归函数代入训练数据的集合,以得到最优化的模型。并得到最大深度。

1
2
3
4
5
# 基于训练数据,获得最优模型
optimal_reg = fit_model(X_train, y_train)

# 输出最优模型的 'max_depth' 参数
print "Parameter 'max_depth' is {} for the optimal model.".format(optimal_reg.get_params()['max_depth'])
Parameter 'max_depth' is 4 for the optimal model.

可以看到,此时的最大深度为4,和我开始目测的不同。

6. 做出预测

当我们用数据训练出一个模型,它现在就可用于对新的数据进行预测。在决策树回归函数中,模型已经学会对新输入的数据提问,并返回对目标变量的预测值。你可以用这个预测来获取数据未知目标变量的信息,这些数据必须是不包含在训练数据之内的。

6.1 预测销售价格

接下来由于模型已经得到了,我需要开始应用这个模型处理一些问题。

假设我是一个在波士顿地区的房屋经纪人,我可以使用此模型以帮助你的客户评估他们想出售的房屋。

假设你已经从你的三个客户收集到以下的资讯:

特征 客戶 1 客戶 2 客戶 3
房屋内房间总数 5 间房间 4 间房间 8 间房间
社区贫困指数(%被认为是贫困阶层) 17% 32% 3%
邻近学校的学生-老师比例 15:1 22:1 12:1

那么接下来,我需要给每位客户的房屋销售的价格进行建议?并通过房屋特征的数值提供对于价格的合理判断。

运行下列的代码区域,我就可以使用优化的模型来为每位客户的房屋价值做出预测。并结合我在第一步中数据分析的阶段,计算出来的部分统计信息来辅助证明我的模型是否正确。

1
2
3
4
5
6
7
8
9
# 生成三个客户的数据
client_data = [[5, 17, 15], # 客户 1
[4, 32, 22], # 客户 2
[8, 3, 12]] # 客户 3

# 进行预测
predicted_price = optimal_reg.predict(client_data)
for i, price in enumerate(predicted_price):
print "Predicted selling price for Client {}'s home: ${:,.2f}".format(i+1, price)
Predicted selling price for Client 1's home: $409,100.00
Predicted selling price for Client 2's home: $285,600.00
Predicted selling price for Client 3's home: $957,218.18
  • 预测价值为409100、285600、957218 美元。

  • 从房屋特征来看,房屋质量的高中低档水平应该是 客户 3 (高档)> 客户 1 (中档)> 客户 2(低档) 。

  • 从统计数据来看,客户 1 的房屋价值处在 Min 与 Max 之间,接近均价和中位数。客户 2 的房屋价值较低,只稍微比 Min 高一点。客户 3 的房屋价值较高,接近 Max 价格。符合房屋特征的判断,因此预测还是是比较合理的。

6.2 使用模型进行预测

我对三个客户的房子的售价进行了预测。接下来,我会使用我的最优模型在整个测试数据上进行预测, 并计算相对于目标变量的决定系数 R2的值**。

1
2
3
4
5
6
7
8
9

# 提示:你可能需要用到 X_test, y_test, optimal_reg, performance_metric
# 提示:你可能需要参考问题10的代码进行预测
# 提示:你可能需要参考问题3的代码来计算R^2的值

from sklearn.metrics import r2_score
r2 = r2_score(y_test, optimal_reg.predict(X_test))

print "Optimal model has R^2 score {:,.2f} on test data".format(r2)
Optimal model has R^2 score 0.78 on test data

6.3 分析决定系数

刚刚计算了最优模型在测试集上的决定系数,但我应该如何评价这个结果?

结果为 0.78 结果不算太好,但也可以进行基本的房屋价值预测了。

6.4 模型的健壮性

一个最优的模型不一定是一个健壮模型。有的时候模型会过于复杂或者过于简单,以致于难以泛化新增添的数据;有的时候模型采用的学习算法并不适用于特定的数据结构;有的时候样本本身可能有太多噪点或样本过少,使得模型无法准确地预测目标变量。这些情况下我们会说模型是欠拟合的。

模型是否足够健壮来保证预测的一致性?

下面,我会采用不同的训练和测试集执行 fit_model 函数10次。

对一个特定的客户来说,预测是如何随训练数据的变化而变化的。

1
2
# 注释掉 fit_model 函数里的所有 print 语句
vs.PredictTrials(features, prices, fit_model, client_data)
Trial 1: $391,183.33
Trial 2: $411,417.39
Trial 3: $415,800.00
Trial 4: $420,622.22
Trial 5: $413,334.78
Trial 6: $411,931.58
Trial 7: $399,663.16
Trial 8: $407,232.00
Trial 9: $402,531.82
Trial 10: $413,700.00

Range in prices: $29,438.89
  • 可以从上述的结果中看到,模型还是没有太不健壮,保持了一定的稳定性。模型随着数据集的变化,最大变化差值为 29438.89 ,约为 7.5% 左右的幅度。

6.5 结果分析

从结果来看,我建构的模型能否在现实世界中使用?

  • 1978年所采集的数据,在已考虑通货膨胀的前提下,在今天是否仍然适用?
  • 数据中呈现的特征是否足够描述一个房屋?
  • 在波士顿这样的大都市采集的数据,能否应用在其它乡镇地区
  • 你觉得仅仅凭房屋所在社区的环境来判断房屋价值合理吗

不同时代经济发展状况不同,人们的经济观念也不同,房屋价值的模型会随之改变。因此会在今天并不适用。
数据中呈现的数据还是比较简化的,没有包括房屋的采光、房体结构、社区成熟度、交通便利性等因素。
波士顿城市数据,应该不能应用在乡镇地区。一级城市、二级城市、偏远乡村的整体房屋价值均值相差肯定很大,会让数据集呈现异常,影响模型对特征的判断。
不合理,社区环境还不够全面