机器学习项目从零到一:加州房价预测模型(PART 1)
一、食用指南
本系列文章将实现一个端到端的机器学习项目案例。假设你是一个A公司最近新雇用的数据科学家,以下是你将会经历的主要步骤:
- 观察大局。
- 获得数据。
- 从数据探索和可视化中获得洞见。
- 机器学习算法的数据准备。
- 选择并训练模型。
- 微调模型。
- 展示解决方案。
- 启动、监控和维护系统。
项目案例纯属虚构,目的仅仅是为了说明机器学习项目的主要步骤。
二、什么是端到端?
端到端(End-to-End),指的是从原始数据输入到最终结果输出均由一个统一的模型自动完成,无需人工设计中间处理模块或特征工程环节。其核心特征和含义如下:
1、核心定义
-
输入与输出的完整性
- 输入端(起点):项目直接接收最原始的数据(如传感器信号、语音波形、图像像素、文本字符),无需预先提取特征或人工预处理。
- 输出端(终点):模型直接生成最终任务结果(如故障诊断结论、翻译文本、控制指令),而非中间步骤的输出(如特征向量或中间标签)。
-
统一模型替代多模块流水线
- 传统方法需将任务拆分为多个独立模块(如语音识别中的“特征提取→音素识别→文本生成”),而端到端用一个模型替代整个流程,内部自动学习所有中间表示。
- 例如:自动驾驶中,摄像头原始画面直接输出方向盘转向角度,省去目标检测、路径规划等独立模块。
2、与传统方法的对比
对比维度 | 端到端方法 | 传统分步方法 |
---|---|---|
流程结构 | 单一模型黑箱处理 | 多模块串联流水线 |
人工干预 | 无需特征工程或模块设计 | 需人工设计特征提取和中间算法 |
数据依赖 | 需大量标注数据训练复杂模型 | 可分段优化,降低单模块数据需求 |
可解释性 | 较差(模型自主学习隐含逻辑) | 较好(各模块可独立分析) |
典型应用 | 语音识别、机器翻译、自动驾驶规划 | 早期图像分类、结构化数据分析 |
3、关键优势与挑战
-
优势:
- 简化流程:减少人工设计环节,提升开发效率;
- 性能优化:模型可全局优化,避免分步误差累积;
- 泛化能力:自动学习数据内在规律,可能发现人类未察觉的模式。
-
挑战:
- 数据需求大:需海量标注数据支撑复杂模型训练;
- 计算资源高:模型参数量大,依赖GPU等高性能硬件;
- 调试困难:中间过程不可见,定位问题较复杂。
4、典型应用场景
- 语音识别:原始音频→转录文本(如DeepSpeech);
- 机器翻译:源语言文本→目标语言文本(如Transformer);
- 自动驾驶:传感器数据(摄像头/LiDAR)→车辆控制指令;
- 医疗诊断:医学影像→疾病分类报告。
5、本质理解
端到端的本质是用数据驱动代替人工先验知识,通过深度神经网络隐式学习从输入到输出的完整映射函数,实现“原始数据进,最终结果出”的自动化流程。这一范式依赖深度学习的发展,是当前AI工程化的主流方向。
三、使用真实数据
学习机器学习最好使用真实数据进行实验,而不仅仅是人工数据集。我们有成千上万覆盖了各个领域的开放数据集可以选择,以下是一些可以获得数据的地方:
-
流行的开放数据存储库:
- UC Irvine Machine Learning Repository(http://archive.ics.uci.edu/ml/)
- Kaggle datasets(https://www.kaggle.com/datasets)
- Amazon’s AWS datasets(http://aws.amazon.com/fr/datasets/)
-
元门户站点(它们会列出开放的数据存储库):
- Data Portals(http://dataportals.org/)
- OpenDataMonitor(http://opendatamonitor.eu/)
- Quandl(http://quandl.com/)
-
其他一些列出许多流行的开放数据存储库的页面:
- Wikipedia’s list of Machine Learning datasets(https://goo.gl/SJHN2k)
- Quora.com(http://goo.gl/zDR78y)
- The datasets subreddit(https://www.reddit.com/r/datasets)
本章我们从 StatLib 库中选择了加州住房价格的数据集。该数据集基于1990年加州人口普查的数据。虽然不算是最新的数据,但是有很多可以学习的特质,所以我们就假定这是最新的数据吧。出于演示目的,我们还特意添加了一个分类属性,并且移除了一些特征。
四、观察大局
框出问题并看整体。
我们要做的第一件事是使用加州人口普查的数据建立起加州的房价模型。数据中有许多指标,诸如每个街区的人口数量、收入中位数、房价中位数等。街区是美国人口普查局发布样本数据的最小地理单位(一个街区通常人口数为600到3000人)。这里,我们将其简称为“区域”。
我们的模型需要从这个数据中学习,从而能够根据所有其他指标,预测任意区域的房价中位数。
!如果你是一名习惯良好的数据科学家,要做的第一件事应该是拿出机器学习项目清单。你可以从这里开始:机器学习项目从零到一:全流程实践指南,它适合绝大多数机器学习项目,但还是要确保它满足你的需求。
为什么是中位数?
中位数反映数据的集中趋势
中位数是数据集中位于中间位置的值,能够有效反映数据的集中趋势。与平均数不同,中位数对极端值不敏感,适合用于描述偏态分布的数据。例如在收入分布中,少数极高收入者会拉高平均数,而中位数更能代表普通人群的收入水平。
中位数体现数据的稳健性
由于中位数仅依赖于数据的中间值,不受极端值影响,因此具有较高的稳健性。在数据存在异常值或分布不对称时,中位数比平均数更能准确反映数据的典型值。例如在房价分析中,少量豪宅会显著拉高平均价格,而中位数能更好反映市场主流价格。
中位数揭示数据分布形态
通过比较中位数和平均数,可以初步判断数据分布的偏态情况。当中位数小于平均数时,数据通常右偏(存在较大极端值);当中位数大于平均数时,数据通常左偏(存在较小极端值)。这种特性使得中位数成为分析数据分布形态的重要工具。
中位数适用于顺序数据和异常数据
中位数不仅适用于数值型数据,也可用于顺序数据(如满意度评级)。在数据存在缺失值或测量误差时,中位数作为位置统计量仍能提供可靠的中心趋势度量。这使得中位数在社会科学和医学研究中具有广泛的应用价值。
1、框架问题
问题1
此时,我们问老板的第一个问题应该是业务目标是什么,因为建立模型本身可能不是最终的目标。公司期望知道如何使用这个模型,如何从中获益?这才是重要的问题,因为这将决定我们怎么设定问题,选择什么算法,使用什么测量方式来评估模型的性能,以及应该花多少精力来进行调整。
老板回答说,这个模型的输出(对一个区域房价中位数的预测)将会跟其他许多信号一起被传输给另一个机器学习系统。而这个下游系统将被用来决策一个给定的区域是否值得投资。因为直接影响到收益,所以正确获得这个信息至关重要。
问题2
要向老板询问的第二个问题是当前的解决方案(如果有的话)。我们可以将其当作参考,也能从中获得解决问题的洞察。
老板回答说,现在是由专家团队在手动估算区域的住房价格:一个团队持续收集最新的区域信息,当他们不能得到房价中位数时,便使用复杂的规则来进行估算。
这个过程既昂贵又耗时,而且估算结果还不令人满意,在某些情况下,他们估计的房价和实际房价的偏差高达20%。这就是为什么该公司认为给定该区域的其他数据有助于训练模型来预测该区域的房价中位数。普查数据看起来像是一个可用于此目的的很好的数据集,因为它包括数千个区域的房价中位数和其他数据。
回答框架问题
有了这些信息,我们现在可以开始设计系统了,首先需要回答框架问题:是有监督学习、无监督学习还是强化学习?是分类任务、回归任务还是其他任务?应该使用批量学习还是在线学习技术?
显然,这是一个典型的有监督学习任务,因为已经给出了标记的训练示例(每个实例都有预期的产出,也就是该区域的房价中位数)。并且这也是一个典型的回归任务,因为我们要对某个值进行预测,更具体地说,这是一个多重回归问题,因为系统要使用多个特征进行预测(使用区域的人口、收入中位数等)。这也是一元回归问题,因为我们仅尝试预测每个区域的单个值;如果我们试图预测每个区域的多个值,那将是多元回归问题。最后,我们没有一个连续的数据流不断流进系统,所以不需要针对变化的数据做出特别调整,数据量也不是很大,不需要多个内存,所以简单的批量学习应该就能胜任。
!如果数据庞大,则可以跨多个服务器拆分批处理学习(使用MapReduce技术)或使用在线学习技术。
2、选择性能指标
下一步是选择性能指标。回归问题的典型性能指标是均方根误差(RMSE)。它给出了系统通常会在预测中产生多大误差,对于较大的误差,权重较高。
-
mmm 是要在其上测量RMSE的数据集中的实例数。例如在2000个区域的验证集上评估RMSE,则m=2000。
-
xix^ixi 是数据集中第 iii 个实例的所有特征值(不包括标签)的向量,而 yiy^iyi是其标签(该实例的期望输出值)。例如,如果数据集中的第一个区域位于经度-118.29°,纬度33.91°,居民1416人,收入中位数为38 372美元,房屋价值中位数为156 400美元(忽略其他特征),那么
x1=[−118.2933.91141638372]x^1 = \begin{bmatrix} -118.29 \\ 33.91 \\ 1416 \\ 38372 \end{bmatrix} x1=−118.2933.91141638372
y1=156400y^1=156400 y1=156400 -
XXX 是一个矩阵,其中包含数据集中所有实例的所有特征值(不包括标签)。每个实例只有一行,第 iii 行等于 xix^ixi 的转置,记为(xi)T(x^i)^T(xi)T。
-
hhh 是系统的预测函数,也称为假设。当给系统输入一个实例的特征向量 xix^ixi 时,它会为该实例输出一个预测值。
-
RMSE(X,h)RMSE(X,h)RMSE(X,h) 是使用假设 hhh 在一组示例中测量的成本函数。
3、检查假设
最后,列举和验证到目前为止(由你或者其他人)做出的假设,是一个非常好的习惯。这可以在初期检查出严重问题。例如,当机器学习系统输出区域价格给下游系统时,我们的假设是价格会被使用。但是,如果下游系统实际上是将价格转换成为类别(例如,廉价、中等或者昂贵),转而使用这些类别,而不是价格本身呢?在这种情形下,并不需要完全准确地预估价格,我们的系统只需要得出正确的类别就够了。如果是这种情况,那么这个问题应该被设定为分类任务而不是回归任务。你肯定不会愿意在回归系统上努力了几个月之后才发现这一点。
幸运的是,跟下游系统的团队聊过之后,证实需要的确实是价格而不是类别。很好!一切就绪,现在可以开始编程了!
五、获取数据
1、创建工作区
建议直接安装 anaconda
创建 Python 开发环境,点这里直达官方地址下载安装。
# 创建 python 3.8 环境
conda create -n mlenv python=3.8
# 激活环境
conda activate mlenv
接下来安装依赖:
pip install -U jupyterlab matplotlib numpy pandas scipy scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple
启动 JupyterLab
:
jupyter-lab
此时会自动打开浏览器页面,没有自动打开页面的话也可以直接复制控制台中的 URL
手动打开:
http://localhost:8888/lab?token=06353a34526c3105d0a7cf9b16c59530614e84596a3c9963
http://127.0.0.1:8888/lab?token=06353a34526c3105d0a7cf9b16c59530614e84596a3c9963
创建一个名为 Housing.ipynb
的 Notebook,开始编码。
2、下载数据
在典型环境中,数据存储在关系型数据库里(或其他一些常用数据存储),并分布在多个表/文档/文件中。访问前,我们需要先获得证书和访问权限,并熟悉数据库模式。不过在这个项目中,事情要简单得多:只需要下载一个压缩文件housing.tgz
即可,这个文件已经包含所有的数据——一个以逗号来分隔值的CSV
文档housing.csv
。
当然我们也可以选择使用浏览器下载压缩包,运行tar xzf housing.tgz
来解压缩并提取CSV
文件,但更好的选择是创建一个小函数来实现它。尤其是当数据会定期发生变化时,这个函数非常有用:编写一个小脚本,在需要获取最新数据时直接运行(或者也可以设置一个定期自动运行的计划任务)。如果需要在多台机器上安装数据集,这个自动获取数据的函数也非常好用。
获取数据的函数如下所示:
import os
import tarfile
import urllib.requestDOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
HOUSING_PATH = os.path.join("datasets", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.tgz"def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):if not os.path.isdir(housing_path):os.makedirs(housing_path)tgz_path = os.path.join(housing_path, "housing.tgz")urllib.request.urlretrieve(housing_url, tgz_path)housing_tgz = tarfile.open(tgz_path)housing_tgz.extractall(path=housing_path)housing_tgz.close()# 执行下载数据
fetch_housing_data()
调用 fetch_housing_data
时,就会自动在工作区中创建一个 datasets/housing
目录,然后下载 housing.tgz
文件,并将 housing.csv
解压到这个目录。
现在我们来使用pandas加载数据:
import pandas as pddef load_housing_data(housing_path=HOUSING_PATH):csv_path = os.path.join(housing_path, "housing.csv")return pd.read_csv(csv_path)# 执行加载数据
load_housing_data()
这个函数会返回一个包含所有数据的 pandas DataFrame
对象。可以看到数据集中一共有 20640
行数据,10
个属性:
- longitude 经度
- latitude 维度
- housing_median_age 住房年龄中位数
- total_rooms 房间总数
- total_bedrooms 卧室总数
- population 人口
- households 家庭
- median_income 收入中位数
- median_house_value 房屋价值中位数
- ocean_proximity 邻近海洋
3、快速查看数据结构
使用 pandas 库
我们来看看使用 DataFrames
的 head
方法之后的前5行是怎样的:
housing = load_housing_data()
housing.head()
通过 info
方法可以快速获取数据集的简单描述,特别是总行数、每个属性的类型和非空值的数量:
housing.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):# Column Non-Null Count Dtype
--- ------ -------------- ----- 0 longitude 20640 non-null float641 latitude 20640 non-null float642 housing_median_age 20640 non-null float643 total_rooms 20640 non-null float644 total_bedrooms 20433 non-null float645 population 20640 non-null float646 households 20640 non-null float647 median_income 20640 non-null float648 median_house_value 20640 non-null float649 ocean_proximity 20640 non-null object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB
数据集中包含20640
个实例,以机器学习的标准来看,这个数字非常小,但却是个完美的开始。注意,total_bedrooms
这个属性只有20433
个非空值,这意味着有207
个区域缺失这个特征。我们后面需要考虑到这一点。
所有属性的字段都是数字,除了ocean_proximity
,它的类型是 object
,因此它可以是任何类型的Python
对象,不过因为从CSV
文件中加载了该数据,所以它必然是文本属性。通过查看前5行注意到,该列中的值是重复的,这意味着它有可能是一个分类属性,我们可以使用value_counts
方法查看有多少种分类存在,每种类别下分别有多少个区域:
housing["ocean_proximity"].value_counts()
ocean_proximity
<1H OCEAN 9136
INLAND 6551
NEAR OCEAN 2658
NEAR BAY 2290
ISLAND 5
Name: count, dtype: int64
可以看到,ocean_proximity
一共有5种分类存在。
再来看看其他区域,通过 describe
方法可以显示数值属性的摘要:
housing.describe()
count
、mean
、min
以及max
行的意思很清楚。需要注意的是,这里的空值会被忽略(因此本例中,total_bedrooms
的count
是20433
而不是20640
)。std
行显示的是标准差(用来测量数值的离散程度)。
25%
、50%
和75%
行显示相应的百分位数:百分位数表示一组观测值中给定百分比的观测值都低于该值。例如,对于housing_median_age
的值,25%
的区域小于18
,50%
的区域小于29
,以及75%
的区域小于37
。这些通常分别称为第25百分位数(或者第一四分位数)、中位数以及第75百分位数(或者第三四分位数)。
使用直方图
另一种快速了解数据类型的方法是绘制每个数值属性的直方图,直方图用来显示给定值范围(横轴)的实例数量(纵轴)。
%matplotlib inline
hist
方法依赖于 Matplotlib
,而 Matplotlib
又依赖于用户指定的图形后端才能在屏幕上完成绘制。所以在绘制之前,需要先指定 Matplotlib
使用哪个后端。最简单的选择是使用 Jupyter
的神奇命令 %matplotlib inline
。它会设置 Matplotlib
从而使用 Jupyter
自己的后端,随后图形会在 notebook
上呈现。需要注意的是,因为 Jupyter
在执行每个单元格时会自动显示图形,所以在 Jupyter notebook
中调用 show
是可选的。
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()
从直方图中我们注意到以下几点:
-
首先,收入中位数这个属性看起来不像是用美元(USD)在衡量。经与收集数据的团队核实,我们得知数据已经按比例缩小,并框出中位数的上限为15(实际为15.0001),下限为0.5(实际为0.4999),数字后的单位为万美元,例如,15代表15万美元。在机器学习中,使用经过预处理的属性是很常见的事情,倒不一定是个问题,但是我们至少需要了解数据是如何计算的。
-
房龄中位数和房价中位数也被设定了上限。由于房价中位数正是我们的目标属性(标签),因此这可能是个大问题。我们的机器学习算法很可能会学习到价格永远不会超过这个限制,所以我们需要再次与客户团队(使用我们的系统输出的团队)进行核实,查看是否存在问题,如果他们需要精确的预测值,甚至可以超过50万美元,那么通常我们有两个选择:
- a. 对那些标签值被设置了上限的区域,重新收集标签值。
- b. 将这些区域的数据从训练集中移除(包括从测试集中移除,因为如果预测值超过50万美元,系统不应被评估为不良)。
-
这些属性值被缩放的程度各不相同。这点我们后续在探讨特征缩放时再做讨论。
-
许多直方图都表现出重尾:图形在中位数右侧的延伸比左侧要远得多。这可能会导致某些机器学习算法难以检测模式。稍后我们会尝试一些转化方法,将这些属性转化为更偏向钟形的分布。
4、创建测试集
在进一步查看数据之前,我们需要先创建一个测试集,然后即可将其放置一旁,不用过多理会。
在这个阶段主动搁置部分数据听起来可能有些奇怪。毕竟,我们才只简单浏览了一下数据而已,在决定用什么算法之前,当然还需要了解更多的知识,对吧?没错,但是大脑是个非常神奇的模式检测系统,也就是说,它很容易过拟合:如果是你本人来浏览测试集数据,很可能会跌入某个看似有趣的测试数据模式,进而选择某个特殊的机器学习模型。然后当你再使用测试集对泛化误差率进行估算时,估计结果将会过于乐观,该系统启动后的表现将不如预期那般优秀,这称为数据窥探偏误。
随机抽样
理论上,创建测试集非常简单,只需要随机选择一些实例,通常是数据集的20%(如果数据集很大,比例将更小),然后将它们放在一边:
import numpy as np# to make this notebook's output identical at every run
np.random.seed(38)# For illustration only. Sklearn has train_test_split()
def split_train_test(data, test_ratio):shuffled_indices = np.random.permutation(len(data))test_set_size = int(len(data) * test_ratio)test_indices = shuffled_indices[:test_set_size]train_indices = shuffled_indices[test_set_size:]return data.iloc[train_indices], data.iloc[test_indices]
# 调用
train_set, test_set = split_train_test(housing, 0.2)
虽然我们设置一个随机数生成器的种子从而让它始终生成相同的随机索引,但是它无法自适应数据集更新的情况,常见的解决方案是每个实例都使用一个标识符来决定是否进入测试集(假定每个实例都有一个唯一且不变的标识符),这样,即使在更新数据集之后也有一个稳定的训练测试分割。
实现方式如下:
from zlib import crc32def test_set_check(identifier, test_ratio):return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32def split_train_test_by_id(data, test_ratio, id_column):ids = data[id_column]in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio))return data.loc[~in_test_set], data.loc[in_test_set]
housing
数据集没有标识符列,这里使用行索引作为ID:
# 方式一:使用行索引作为ID
housing_with_id = housing.reset_index() # adds an `index` column
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")
如果使用行索引作为唯一标识符,我们需要确保在数据集的末尾添加新数据,并且不会删除任何行。如果不能保证这一点,那么我们可以使用某个最稳定的特征来创建唯一标识符。例如,一个区域的经纬度肯定几百万年都不会变,可以将它们组合成如下的ID:
# 方式二:使用经纬度组合作为ID
housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")
除了以上两种方式外,Scikit-Learn
提供了一些函数,可以通过多种方式将数据集分成多个子集。最简单的函数是train_test_split
,除了几个额外特征它与前面定义的函数 split_train_test
几乎相同。首先,它也有 random_state
参数,我们可以像之前提到过的那样设置随机生成器种子;其次,我们可以把行数相同的多个数据集一次性发送给它,它会根据相同的索引将其拆分(例如,当我们有一个单独的 DataFrame
用于标记时,这就非常有用):
# 方式三:使用 sklearn 函数
from sklearn.model_selection import train_test_splittrain_set, test_set = train_test_split(housing, test_size=0.2, random_state=38)
到目前为止,我们思考过了纯随机的抽样方法,如果数据集足够庞大(特别是相较于属性的数量而言),这种方式通常不错;如果数据集规模小,则有可能会导致明显的抽样偏差,因此我们寻求另一种抽样方式。
分层抽样
要预测房价中位数,收入中位数是一个非常重要的属性,于是我们希望确保在收入属性上,测试集能够代表整个数据集中各种不同类型的收入,由于收入中位数是一个连续的数值属性,所以我们需要先创建一个收入类别的属性。
从收入中位数的直方图中,我们可以观察到大多数收入中位数值聚集在1.5~6(万美元)左右,但也有一部分远远超过了6万美元。
在数据集中,每一层都要有足够数量的实例,这一点至关重要,不然数据不足的层,其重要程度很有可能会被错估。也就是说,我们不应该将层数分得太多,每一层应该要足够大才行。下面这段代码是用pd.cut
来创建5个收入类别属性的(用1~5来做标签),0~1.5
是类别1,1.5~3
是类别2,以此类推:
housing["income_cat"] = pd.cut(housing["median_income"],bins=[0., 1.5, 3.0, 4.5, 6., np.inf],labels=[1, 2, 3, 4, 5])
此时我们查看分类结果:
housing["income_cat"].value_counts()
income_cat
3 7236
2 6581
4 3639
5 2362
1 822
Name: count, dtype: int64
housing["income_cat"].hist()
现在,我们可以根据收入类别进行分层抽样了,使用 Scikit-Learn
的 StratifiedShuffleSplit
类:
from sklearn.model_selection import StratifiedShuffleSplitsplit = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=38)
for train_index, test_index in split.split(housing, housing["income_cat"]):strat_train_set = housing.loc[train_index]strat_test_set = housing.loc[test_index]
此时查看测试集中收入类别比例分布:
strat_test_set["income_cat"].value_counts() / len(strat_test_set)
income_cat
3 0.350533
2 0.318798
4 0.176357
5 0.114341
1 0.039971
Name: count, dtype: float64
查看整个数据集中收入类别比例分布:
housing["income_cat"].value_counts() / len(housing)
income_cat
3 0.350581
2 0.318847
4 0.176308
5 0.114438
1 0.039826
Name: count, dtype: float64
综合对比在三种不同的数据集(完整数据集、分层抽样的测试集、纯随机抽样的测试集)中收入类别比例分布:
# 综合对比
def income_cat_proportions(data):return data["income_cat"].value_counts() / len(data)train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)compare_props = pd.DataFrame({"Overall": income_cat_proportions(housing), # 完整数据集"Stratified": income_cat_proportions(strat_test_set), # 分层抽样的测试集"Random": income_cat_proportions(test_set), # 纯随机抽样的测试集
}).sort_index()
compare_props["Rand. %error"] = 100 * compare_props["Random"] / compare_props["Overall"] - 100
compare_props["Strat. %error"] = 100 * compare_props["Stratified"] / compare_props["Overall"] - 100
从结果上可见,分层抽样的测试集中的比例分布与完整数据集中的分布几乎一致,而纯随机抽样的测试集结果则是有偏差的。
现在我们可以删除 income_cat
属性,将数据恢复原样:
for set_ in (strat_train_set, strat_test_set):set_.drop("income_cat", axis=1, inplace=True)
我们花了相当长的时间在测试集的生成上,理由很充分:这是机器学习项目中经常被忽视但是却至关重要的一部分。并且,当讨论到交叉验证时,这里谈到的许多想法也对其大有裨益。
六、数据探索(更新中…)
我们将在下一篇文章中介绍如何从数据探索和可视化中获得洞见。