Python 【机器学习】 进阶 之 【实战案例】房价数据中位数分析 [ 项目介绍 ] [ 获取数据 ] [ 创建测试集 ]| 1/3(含分析过程)

Python 【机器学习】 进阶 之 【实战案例】房价数据中位数分析 [ 项目介绍 ] [ 获取数据 ] [ 创建测试集 ]| 1/3(含分析过程)

目录

Python 【机器学习】 进阶 之 【实战案例】房价数据中位数分析 [ 项目介绍 ] [ 获取数据 ] [ 创建测试集 ]| 1/3(含分析过程)

一、简单介绍

二、机器学习

1、为什么使用机器学习?

2、机器学习系统的类型,及其对应的学习算法

3、机器学习可利用的开源数据

三、房价数据中位数分析 项目介绍

1、划定问题

2、选择性能指标

3、核实假设

四、环境准备

五、获取数据(该案例房价的数据集)

1、下载数据

2、快速查看数据结构

3、创建测试集

附录:

一、一些知识点

1、流水线

二、源码工程

三、该案例的环境 package 信息如下


一、简单介绍

Python是一种跨平台的计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越多被用于独立的、大型项目的开发。Python是一种解释型脚本语言,可以应用于以下领域: Web 和 Internet开发、科学计算和统计、人工智能、教育、桌面界面开发、软件开发、后端开发、网络爬虫。

Python 机器学习是利用 Python 编程语言中的各种工具和库来实现机器学习算法和技术的过程。Python 是一种功能强大且易于学习和使用的编程语言,因此成为了机器学习领域的首选语言之一。Python 提供了丰富的机器学习库,如Scikit-learn、TensorFlow、Keras、PyTorch等,这些库包含了许多常用的机器学习算法和深度学习框架,使得开发者能够快速实现、测试和部署各种机器学习模型。

通过 Python 进行机器学习,开发者可以利用其丰富的工具和库来处理数据、构建模型、评估模型性能,并将模型部署到实际应用中。Python 的易用性和庞大的社区支持使得机器学习在各个领域都得到了广泛的应用和发展。

二、机器学习

机器学习(Machine Learning)是人工智能(AI)的一个分支领域,其核心思想是通过计算机系统的学习和自动化推理,使计算机能够从数据中获取知识和经验,并利用这些知识和经验进行模式识别、预测和决策。机器学习算法能够自动地从数据中学习并改进自己的性能,而无需明确地编程。这一过程涉及对大量输入数据的分析和解释,以识别数据中的模式和趋势,并生成可以应用于新数据的预测模型。

1、为什么使用机器学习?

使用机器学习的原因主要包括以下几点:

  1. 高效性和准确性:机器学习算法能够处理大规模数据集,并从中提取有价值的信息,其预测和决策的准确性往往高于传统方法。
  2. 自动化和智能化:机器学习能够自动学习和改进,减少了对人工干预的依赖,提高了工作效率和智能化水平。
  3. 广泛应用性:机器学习在各个领域中都有广泛的应用,如图像识别、语音识别、自然语言处理、推荐系统、金融预测等,为许多实际问题的解决提供了有效的方法和工具。
  4. 未来趋势:随着人工智能技术的不断发展,机器学习已成为未来的趋势,掌握机器学习技能将有助于提高职业竞争力和创造力。

2、机器学习系统的类型,及其对应的学习算法

机器学习系统可以根据不同的学习方式和目标进行分类,主要包括以下几种类型及其对应的学习算法:

  1. 监督学习(Supervised Learning)

    • 定义:使用带有标签的训练数据来训练模型,以预测新数据的标签或目标值。
    • 常见算法
      • 线性回归(Linear Regression):用于预测连续值。
      • 逻辑回归(Logistic Regression):用于分类问题,尤其是二分类问题。
      • 支持向量机(SVM, Support Vector Machines):用于分类和回归问题,通过寻找最优的超平面来分割数据。
      • 决策树(Decision Trees)随机森林(Random Forests):通过构建决策树或决策树集合来进行分类或回归。
      • 神经网络(Neural Networks):模仿人脑神经元的工作方式,通过多层节点之间的连接和权重调整来进行学习和预测。
  2. 无监督学习(Unsupervised Learning)

    • 定义:在没有标签的情况下,从数据中发现隐藏的结构和模式。
    • 常见算法
      • 聚类算法(Clustering Algorithms):如K均值(K-Means)、层次聚类分析(HCA)等,用于将数据分组为具有相似特征的簇。
      • 降维算法(Dimensionality Reduction Algorithms):如主成分分析(PCA)、t-分布邻域嵌入算法(t-SNE)等,用于减少数据的维度以便于分析和可视化。
      • 异常检测(Anomaly Detection):用于识别数据中的异常点或离群点。
  3. 强化学习(Reinforcement Learning)

    • 定义:通过与环境的交互学习,以最大化累积奖励为目标。
    • 特点:强化学习不需要明确的标签或监督信号,而是根据环境给出的奖励或惩罚来指导学习过程。
    • 应用场景:游戏AI、机器人控制、自动驾驶等领域。
  4. 半监督学习(Semi-Supervised Learning)

    • 定义:处理部分带标签的训练数据,通常是大量不带标签数据加上小部分带标签数据。
    • 特点:结合了监督学习和无监督学习的特点,旨在利用未标记数据来提高模型的泛化能力。
    • 常见算法:多数半监督学习算法是非监督和监督算法的结合,如自训练(Self-Training)、协同训练(Co-Training)等。

3、机器学习可利用的开源数据

开源数据集可以根据需要进行选择,涵盖多个领域。以下是一些可以查找的数据的地方,供参考:

(注意:代码执行的时候,可能需要科学上网)

三、房价数据中位数分析 项目介绍

欢迎来到机器学习房地产公司!你的第一个任务是利用加州普查数据,建立一个加州房价模型。这个数据包含每个街区组的人口、收入中位数、房价中位数等指标。

街区组是美国调查局发布样本数据的最小地理单位(一个街区通常有 600 到 3000 人)。我们将其简称为“街区”。

你的模型要利用这个数据进行学习,然后根据其它指标,预测任何街区的的房价中位数。

1、划定问题

问老板的第一个问题应该是商业目标是什么?建立模型可能不是最终目标。公司要如何使用、并从模型受益?这非常重要,因为它决定了如何划定问题,要选择什么算法,评估模型性能的指标是什么,要花多少精力进行微调。

过程

  • 1.1 老板告诉你你的模型的输出(一个区的房价中位数)会传给另一个机器学习系统

(见图 3 - 1 ),也有其它信号会传入后面的系统。这一整套系统可以确定某个区进行投资值不值。确定值不值得投资非常重要,它直接影响利润。

图 3-1 房地产投资的机器学习流水线

  • 1.2 下一个要问的问题是,现在的解决方案效果如何。

老板通常会给一个参考性能,以及如何解决问题。老板说,现在街区的房价是靠专家手工估计的,专家队伍收集最新的关于一个区的信息(不包括房价中位数),他们使用复杂的规则进行估计。这种方法费钱费时间,而且估计结果不理想,误差率大概有 15%。

  • 1.3 OK,有了这些信息,你就可以开始设计系统了。

首先,你需要划定问题:监督或非监督,还是强化学习?这是个分类任务、回归任务,还是其它的?要使用批量学习还是线上学习?继续阅读之前,请暂停一下,尝试自己回答下这些问题。

  • 1.4 你能回答出来吗?

一起看下答案:很明显,这是一个典型的监督学习任务,因为你要使用的是有标签的训练样本(每个实例都有预定的产出,即街区的房价中位数)。并且,这是一个典型的回归任务,因为你要预测一个值。讲的更细些,这是一个多变量回归问题,因为系统要使用多个变量进行预测(要使用街区的人口,收入中位数等等)。

2、选择性能指标

下一步是选择性能指标。回归问题的典型指标是均方根误差(RMSE)。均方根误差测量的是系统预测误差的标准差。例如,RMSE 等于 50000,意味着,68% 的系统预测值位于实际值的 50000 美元以内,95% 的预测值位于实际值的 100000 美元以内(一个特征通常都符合高斯分布,即满足 “68-95-99.7”规则:大约 68% 的值落在内,95% 的值落在内,99.7% 的值落在内,这里的σ等于 50000)。公式 3-1 展示了计算 RMSE 的方法。

RMSE\left ( X,h \right )=\sqrt{\frac{1}{m}\sum_{i=1}^{m}\left ( h\left ( x^{\left ( i \right )} \right ) -y^{\left ( i \right )}\right )^{2}}

公式 3-1 均方根误差(RMSE)

符号的含义

这个方程引入了一些常见的贯穿本书的机器学习符号:

  • m是测量 RMSE 的数据集中的实例数量。
    例如,如果用一个含有 2000 个街区的验证集求 RMSE,则m = 2000

虽然大多数时候 RMSE 是回归任务可靠的性能指标,在有些情况下,你可能需要另外的函数。例如,假设存在许多异常的街区。此时,你可能需要使用平均绝对误差(Mean Absolute Error,也称作平均绝对偏差),见公式 3-2:

MAE(X,h)=\frac{1}{m}\sum_{i=1}^{m}\left | h(x^{(i)})-y^{(i)} \right |

公式 3-2 平均绝对误差

MSE 和 MAE 都是测量预测值和目标值两个向量距离的方法。有多种测量距离的方法,或范数:

  • 计算对应欧几里得范数的平方和的根(RMSE):这个距离介绍过。它也称作ℓ2范数,标记为||·||₂(或只是||·||)。

  • 计算对应于ℓ1(标记为||·||₁)范数的绝对值和(MAE)。有时,也称其为曼哈顿范数,因为它测量了城市中的两点,沿着矩形的边行走的距离。

  • 更一般的,包含n个元素的向量vℓk范数(K 阶闵氏范数),定义成

        \left | v| \right |_{k}=(|v_{0}|^{k}+(|v_{1}|^{k}+...+|v_{n}|^{k})^{\frac{1}{k}}

        ℓ0(汉明范数)只显示了这个向量的基数(即,非零元素的个数),ℓ∞(切比雪夫范数)是向量中最大的绝对值。

  • 范数的指数越高,就越关注大的值而忽略小的值。这就是为什么 RMSE 比 MAE 对异常值更敏感。但是当异常值是指数分布的(类似正态曲线),RMSE 就会表现很好。

3、核实假设

最后,最好列出并核对迄今(你或其他人)作出的假设,这样可以尽早发现严重的问题。例如,你的系统输出的街区房价,会传入到下游的机器学习系统,我们假设这些价格确实会被当做街区房价使用。但是如果下游系统实际上将价格转化成了分类(例如,便宜、中等、昂贵),然后使用这些分类,而不是使用价格。这样的话,获得准确的价格就不那么重要了,你只需要得到合适的分类。问题相应地就变成了一个分类问题,而不是回归任务。你可不想在一个回归系统上工作了数月,最后才发现真相。

幸运的是,在与下游系统主管探讨之后,你很确信他们需要的就是实际的价格,而不是分类。很好!整装待发,可以开始写代码了。

四、环境准备

1、最好构建一个虚拟环境,避免与其他工程公用一环境,造成一些包的版本不同导致运行问题

2、环境中除了使用 Python 外,还用主要用到 jupyter matplotlib numpy pandas scipy scikit-learn

pip install jupyter matplotlib numpy pandas scipy scikit-learn

3、该案例的环境 package 版本信息见附录

五、获取数据(该案例房价的数据集)

1、下载数据

一般情况下,数据是存储于关系型数据库(或其它常见数据库)中的多个表、文档、文件。要访问数据,你首先要有密码和登录权限,并要了解数据模式。但是在这个项目中,这一切要简单些:只要下载一个压缩文件,housing.tgz,它包含一个CSV 文件housing.csv,含有所有数据。

你可以使用浏览器下载,运行tar xzf housing.tgz解压出csv文件,但是更好的办法是写一个小函数来做这件事。如果数据变动频繁,这么做是非常好的,因为可以让你写一个小脚本随时获取最新的数据(或者创建一个定时任务来做)。如果你想在多台机器上安装数据集,获取数据自动化也是非常好的。

下面是获取数据的函数:

import os  # 用于操作文件和目录
import tarfile  # 用于处理tar压缩文件
import requests  # 用于发送HTTP请求
from requests.adapters import HTTPAdapter  # 用于在requests中设置重试策略
from requests.packages.urllib3.util.retry import Retry  # 用于设置重试策略

# 下载文件的根URL
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
# 存储数据的路径
HOUSING_PATH = "datasets/housing"
# 完整的下载URL
HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + "/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")
    
    # 设置重试机制
    session = requests.Session()
    retry = Retry(
        total=5,  # 重试总次数
        backoff_factor=1,  # 重试间隔时间的倍数
        status_forcelist=[502, 503, 504]  # 需要重试的HTTP状态码
    )
    adapter = HTTPAdapter(max_retries=retry)  # 创建HTTP适配器
    session.mount('http://', adapter)  # 为HTTP协议挂载适配器
    session.mount('https://', adapter)  # 为HTTPS协议挂载适配器

    try:
        # 发送GET请求以下载文件
        response = session.get(housing_url, stream=True)
        response.raise_for_status()  # 如果响应状态码不是200,则抛出异常
        
        # 将下载的文件内容写入本地文件
        with open(tgz_path, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)
    except requests.exceptions.RequestException as e:
        print(f"下载文件时出错: {e}")
        return

    try:
        # 打开压缩文件
        housing_tgz = tarfile.open(tgz_path)
        # 解压缩文件到指定目录
        housing_tgz.extractall(path=housing_path)
        housing_tgz.close()  # 关闭文件
    except tarfile.TarError as e:
        print(f"解压文件时出错: {e}")
        return
# 调用函数以下载和解压文件
fetch_housing_data(HOUSING_URL,HOUSING_PATH)

现在,当你调用fetch_housing_data(),就会在工作空间创建一个datasets/housing目录,下载housing.tgz文件,解压出housing.csv

然后使用 Pandas 加载数据。还是用一个小函数来加载数据:

# 导入pandas库,并给它一个别名pd,这是pandas的常用别名。
import pandas as pd

# 定义一个函数load_housing_data,用于加载房地产数据。
# 函数接受一个可选参数housing_path,默认值为HOUSING_PATH。
def load_housing_data(housing_path=HOUSING_PATH):
    # 使用os模块的path.join方法来拼接文件路径。
    # 这确保了代码在不同操作系统中的兼容性。
    csv_path = os.path.join(housing_path, "housing.csv")
    
    # 使用pandas的read_csv函数读取csv文件,并返回DataFrame对象。
    return pd.read_csv(csv_path)

这个函数会返回一个包含所有数据的 Pandas DataFrame 对象。

2、快速查看数据结构

使用DataFramehead()方法查看该数据集的前 5 行(见图 3-2)。

# 调用load_housing_data函数加载数据,并将结果存储在变量housing中。
# 注意:此处需要事先定义HOUSING_PATH变量,指向包含housing.csv的目录。
housing = load_housing_data()

# 使用head()方法查看DataFrame的前五行数据,这有助于快速了解数据的结构。
housing.head()

运行结果:

图 3-2 数据集的前五行

每一行都表示一个街区。共有 10 个属性(截图中可以看到 6 个):经度、维度、房屋年龄中位数、总房间数、总卧室数、人口数、家庭数、收入中位数、房屋价值中位数、离大海距离。

info()方法可以快速查看数据的描述,特别是总行数、每个属性的类型和非空值的数量。

# 使用info()方法打印DataFrame 'housing' 的概览信息。
# 这个方法会显示每列的数据类型、非空值的数量以及内存使用情况。
# 这有助于快速检查数据集的基本信息和数据完整性。
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  float64
 1   latitude            20640 non-null  float64
 2   housing_median_age  20640 non-null  float64
 3   total_rooms         20640 non-null  float64
 4   total_bedrooms      20433 non-null  float64
 5   population          20640 non-null  float64
 6   households          20640 non-null  float64
 7   median_income       20640 non-null  float64
 8   median_house_value  20640 non-null  float64
 9   ocean_proximity     20640 non-null  object 
dtypes: float64(9), object(1)
memory usage: 1.6+ MB

数据集中共有 20640 个实例,按照机器学习的标准这个数据量很小,但是非常适合入门。我们注意到总卧室数只有 20433 个非空值,这意味着有 207 个街区缺少这个值。我们将在后面对它进行处理。

所有的属性都是数值的,除了离大海距离这项。它的类型是对象,因此可以包含任意 Python 对象,但是因为该项是从 CSV 文件加载的,所以必然是文本类型。在刚才查看数据前五项时,你可能注意到那一列的值是重复的,意味着它可能是一项表示类别的属性。可以使用value_counts()方法查看该项中都有哪些类别,每个类别中都包含有多少个街区:

# 使用value_counts()方法计算'ocean_proximity'列中各个唯一值的出现次数。
# 'ocean_proximity'列可能包含描述房产与海洋接近程度的分类标签。
# 这个方法会返回一个Series,其中索引是'ocean_proximity'列中的唯一值,
# 而数据是这些值在列中出现的次数。
# 这有助于了解数据集中房产与海洋接近程度的分布情况。
housing["ocean_proximity"].value_counts()

运行结果:

ocean_proximity
<1H OCEAN     9136
INLAND        6551
NEAR OCEAN    2658
NEAR BAY      2290
ISLAND           5
Name: count, dtype: int64

再来看其它字段。describe()方法展示了数值属性的概括(见图 3-3)。

# 使用describe()方法生成'housing' DataFrame的描述性统计信息。
# 这个方法会返回一个新的DataFrame,其中包含了数值型列的统计摘要。
# 描述性统计包括:
# - count: 非空值的数量
# - mean: 平均值
# - std: 标准差,衡量数据的离散程度
# - min: 最小值
# - 25%: 第一四分位数,即所有值中位置在25%的值
# - 50%: 中位数,即所有值中位置在50%的值
# - 75%: 第三四分位数,即所有值中位置在75%的值
# - max: 最大值
# 这有助于快速了解数据集的分布特征和中心趋势。
housing.describe()

运行结果:

图 3-3 每个数值属性的概括

countmeanminmax几行的意思很明显了。注意,空值被忽略了(所以,卧室总数是 20433 而不是 20640)。std是标准差(揭示数值的分散度)。25%、50%、75% 展示了对应的分位数:每个分位数指明小于这个值,且指定分组的百分比。例如,25% 的街区的房屋年龄中位数小于 18,而 50% 的小于 29,75% 的小于 37。这些值通常称为第 25 个百分位数(或第一个四分位数),中位数,第 75 个百分位数(第三个四分位数)。

另一种快速了解数据类型的方法是画出每个数值属性的柱状图。柱状图(的纵轴)展示了特定范围的实例的个数。你还可以一次给一个属性画图,或对完整数据集调用hist()方法,后者会画出每个数值属性的柱状图(见图 3-4)。例如,你可以看到略微超过 800 个街区的median_house_value值差不多等于 500000 美元。

# 设置Matplotlib的显示模式,使得图表可以在Jupyter Notebook中内联显示。
%matplotlib inline

# 导入matplotlib.pyplot模块,并给它一个别名plt,这是matplotlib.pyplot的常用别名。
import matplotlib.pyplot as plt

# 使用DataFrame的hist()方法绘制'housing'中所有数值型列的直方图。
# 'bins'参数设置为50,表示每个直方图的柱子数量。
# 'figsize'参数设置为(20,15),表示图表的宽度和高度(英寸)。
housing.hist(bins=50, figsize=(20,15))

# 假设save_fig是一个自定义函数,用于保存图表为图片文件。
# 这里调用save_fig函数,将当前图表保存为名为"attribute_histogram_plots"的图片。
# 请注意,save_fig函数需要事先定义。
plt.savefig("images/attribute_histogram_plots.png",bbox_inches='tight')

# 使用plt.show()显示图表。在Jupyter Notebook中,这通常不是必需的,因为图表已经内联显示。
# 但在其他环境中,调用plt.show()可以弹出一个窗口显示图表。
plt.show()

运行结果:

图 3-4 每个数值属性的柱状图

注:hist()方法依赖于 Matplotlib,后者依赖于用户指定的图形后端以打印到屏幕上。因此在画图之前,你要指定 Matplotlib 要使用的后端。最简单的方法是使用 Jupyter 的魔术命令%matplotlib inline。它会告诉 Jupyter 设定好 Matplotlib,以使用 Jupyter 自己的后端。绘图就会在笔记本中渲染了。注意在 Jupyter 中调用show()不是必要的,因为代码框执行后 Jupyter 会自动展示图像。

注意柱状图中的一些点:

  1. 首先,收入中位数貌似不是美元(USD)。与数据采集团队交流之后,你被告知数据是经过缩放调整的,过高收入中位数的会变为 15(实际为 15.0001),过低的会变为 5(实际为 0.4999)。在机器学习中对数据进行预处理很正常,这不一定是个问题,但你要明白数据是如何计算出来的。

  2. 房屋年龄中位数和房屋价值中位数也被设了上限。后者可能是个严重的问题,因为它是你的目标属性(你的标签)。你的机器学习算法可能学习到价格不会超出这个界限。你需要与下游团队核实,这是否会成为问题。如果他们告诉你他们需要明确的预测值,即使超过 500000 美元,你则有两个选项:

    1. 对于设了上限的标签,重新收集合适的标签;
    2. 将这些街区从训练集移除(也从测试集移除,因为若房价超出 500000 美元,你的系统就会被差评)。
  3. 这些属性值有不同的量度。我们会在本章后面讨论特征缩放。

  4. 最后,许多柱状图的尾巴很长:相较于左边,它们在中位数的右边延伸过远。对于某些机器学习算法,这会使检测规律变得更难些。我们会在后面尝试变换处理这些属性,使其变为正态分布。

希望你现在对要处理的数据有一定了解了。

3、创建测试集

在这个阶段就分割数据,听起来很奇怪。毕竟,你只是简单快速地查看了数据而已,你需要再仔细调查下数据以决定使用什么算法。这么想是对的,但是人类的大脑是一个神奇的发现规律的系统,这意味着大脑非常容易发生过拟合:如果你查看了测试集,就会不经意地按照测试集中的规律来选择某个特定的机器学习模型。再当你使用测试集来评估误差率时,就会导致评估过于乐观,而实际部署的系统表现就会差。这称为数据透视偏差。

理论上,创建测试集很简单:只要随机挑选一些实例,一般是数据集的 20%,放到一边:

# 导入numpy库,并给它一个别名np,这是numpy的常用别名。
import numpy as np

# 这是一个自定义函数,用于将数据集分割为训练集和测试集。
# 它模拟了sklearn库中的train_test_split()函数的功能,仅用于示例说明。
def split_train_test(data, test_ratio):
    # 使用numpy的random.permutation函数生成一个随机排列的索引数组。
    # 这个数组的长度与输入数据集的长度相同。
    shuffled_indices = np.random.permutation(len(data))
    
    # 计算测试集的大小,基于输入的测试集比例test_ratio和数据集的总长度。
    test_set_size = int(len(data) * test_ratio)
    
    # 根据计算出的测试集大小,从随机排列的索引中选取前test_set_size个索引作为测试集索引。
    test_indices = shuffled_indices[:test_set_size]
    
    # 剩余的索引将用作训练集的索引。
    train_indices = shuffled_indices[test_set_size:]
    
    # 使用pandas的iloc函数根据索引从原始数据集中选取行,分别创建训练集和测试集。
    # 返回两个数据集:一个是训练集,另一个是测试集。
    return data.iloc[train_indices], data.iloc[test_indices]

然后可以像下面这样使用这个函数:

# 调用自定义的split_train_test函数,传入整个数据集housing和测试集比例0.2,
# 即将数据集的20%作为测试集,剩余的80%作为训练集。
train_set, test_set = split_train_test(housing, 0.2)

# 打印训练集和测试集的大小,以确认数据集已经被正确分割。
# len函数返回DataFrame的行数,即数据点的数量。
# 这有助于验证我们的分割函数是否按预期工作。
print(len(train_set), "train +", len(test_set), "test")

运行结果:

16512 train + 4128 test

这个方法可行,但是并不完美:如果再次运行程序,就会产生一个不同的测试集!多次运行之后,你(或你的机器学习算法)就会得到整个数据集,这是需要避免的。

解决的办法之一是保存第一次运行得到的测试集,并在随后的过程加载。另一种方法是在调用np.random.permutation()之前,设置随机数生成器的种子(比如np.random.seed(42)),以产生总是相同的洗牌指数(shuffled indices)。

# 导入numpy库,并给它一个别名np,这是numpy的常用别名。
import numpy as np

# 使用numpy的random模块中的seed函数设置随机数生成器的种子。
# 这里的种子值是42,这是一个常用的种子值,因为42在流行文化中被认为是一个“幸运数字”。
# 通过设置种子,可以确保每次运行代码时生成的随机数序列是相同的。
# 这有助于结果的可重复性,特别是在需要多次运行实验或测试的情况下。
np.random.seed(42)

但是如果数据集更新,这两个方法都会失效。一个通常的解决办法是使用每个实例的 ID 来判定这个实例是否应该放入测试集(假设每个实例都有唯一并且不变的 ID)。例如,你可以计算出每个实例 ID 的哈希值,只保留其最后一个字节,如果该值小于等于 51(约为 256 的 20%),就将其放入测试集。这样可以保证在多次运行中,测试集保持不变,即使更新了数据集。新的测试集会包含新实例中的 20%,但不会有之前位于训练集的实例。下面是一种可用的方法:

from zlib import crc32
import numpy as np
import pandas as pd

# 导入zlib库中的crc32函数,用于生成数据的循环冗余校验码。
# CRC32是一种广泛使用的哈希函数,可以用于检测数据的完整性。

# 定义一个辅助函数test_set_check,用于确定一个给定的标识符是否应该属于测试集。
# 这个函数接收两个参数:identifier(数据点的标识符)和test_ratio(测试集的比例)。
# 它使用crc32函数生成标识符的哈希值,并将其与测试集比例对应的数值进行比较,
# 以确定数据点是否应该被分配到测试集中。
# 这里使用了位运算符& 0xffffffff来确保crc32的结果是一个32位的无符号整数。
# 如果哈希值小于测试集比例乘以2的32次方,那么这个数据点将被分配到测试集。
def test_set_check(identifier, test_ratio):
    return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32

# 定义一个函数split_train_test_by_id,用于根据数据点的标识符来分割数据集。
# 这个函数接收三个参数:data(原始数据集),test_ratio(测试集的比例)和id_column(包含标识符的列名)。
# 函数首先从数据集中提取id_column列的值。
# 然后,使用apply函数和lambda表达式调用test_set_check函数,为每个数据点生成一个布尔值,
# 表示该数据点是否属于测试集。
# 最后,使用loc函数和布尔索引来分别选取不属于测试集的数据点和属于测试集的数据点,
# 返回训练集和测试集的DataFrame。
def 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]

不过,房产数据集没有 ID 这一列。最简单的方法是使用行索引作为 ID:

# 使用pandas的reset_index方法重置housing DataFrame的索引,
# 并将原来的索引添加为一个新的列`index`。
# 这是因为split_train_test_by_id函数需要一个标识符列来决定哪些数据点属于测试集。
# 通过添加索引作为列,我们可以确保每个数据点都是唯一的,并且可以用作标识符。
housing_with_id = housing.reset_index()   # adds an `index` column

# 调用split_train_test_by_id函数,传入重置索引后的housing_with_id DataFrame,
# 测试集比例设置为0.2,意味着20%的数据将被分配到测试集,其余的80%将分配到训练集。
# 指定"index"作为标识符列,因为我们刚刚添加了索引作为列。
# 这个函数将基于索引列的值,使用crc32哈希函数和测试集比例来决定数据点的分配。
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")

如果使用行索引作为唯一识别码,你需要保证新数据都放到现有数据的尾部,且没有行被删除。如果做不到,则可以用最稳定的特征来创建唯一识别码。例如,一个区的维度和经度在几百万年之内是不变的,所以可以将两者结合成一个 ID:

# 为housing_with_id DataFrame创建一个新的列"id",用于作为数据点的唯一标识符。
# 这里使用了一个简单的方法来生成标识符:将经度值乘以1000后加上纬度值。
# 这种方法可以确保每个数据点的标识符都是唯一的,因为经度和纬度的组合是唯一的。
# 请注意,这种方法可能不适用于所有情况,特别是在经度和纬度的范围重叠时。
# 但它在这里用于示例,以展示如何创建一个基于地理位置的唯一标识符。
housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]

# 使用split_train_test_by_id函数,传入housing_with_id DataFrame,测试集比例为0.2,
# 并指定新创建的"id"列作为标识符列。
# 这个函数将基于"id"列的值,使用crc32哈希函数和测试集比例来随机分配数据点到训练集或测试集。
# 通过这种方式,我们确保了数据集的分割是随机且可重复的,有助于公平评估模型性能。
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")
# 使用pandas的head()方法查看DataFrame 'test_set' 的前五行数据。
# 这是一个快速检查数据集内容的方法,可以帮助我们了解测试集的基本情况,
# 比如数据的格式、列的名称以及前几个数据点的值。
# 这对于验证数据集是否正确分割以及初步了解数据特征非常有用。
test_set.head()

运行结果:

Scikit-Learn 提供了一些函数,可以用多种方式将数据集分割成多个子集。最简单的函数是train_test_split,它的作用和之前的函数split_train_test很像,并带有其它一些功能。首先,它有一个random_state参数,可以设定前面讲过的随机生成器种子;第二,你可以将种子传递给多个行数相同的数据集,可以在相同的索引上分割数据集(这个功能非常有用,比如你的标签值是放在另一个DataFrame里的):

# 从sklearn的model_selection模块导入train_test_split函数。
# 这个函数是Scikit-learn库提供的一个标准工具,用于将数据集分割为训练集和测试集。
from sklearn.model_selection import train_test_split

# 使用train_test_split函数将原始数据集housing分割为训练集和测试集。
# 'test_size=0.2'参数指定了测试集占原始数据集的比例,即20%。
# 'random_state=42'参数用于确保每次分割时都能产生相同的随机结果,
# 这有助于实验的可重复性,特别是在进行多次实验或分享实验结果时。
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
# 使用pandas的head()方法查看DataFrame 'test_set' 的前五行数据。
# 这是一个快速检查数据集内容的方法,可以帮助我们了解测试集的基本情况,
# 比如数据的格式、列的名称以及前几个数据点的值。
# 这对于验证数据集是否正确分割以及初步了解数据特征非常有用。
test_set.head()

运行结果:

目前为止,我们采用的都是纯随机的取样方法。当你的数据集很大时(尤其是和属性数相比),这通常可行;但如果数据集不大,就会有采样偏差的风险。当一个调查公司想要对 1000 个人进行调查,它们不是在电话亭里随机选 1000 个人出来。调查公司要保证这 1000 个人对人群整体有代表性。例如,美国人口的 51.3% 是女性,48.7% 是男性。所以在美国,严谨的调查需要保证样本也是这个比例:513 名女性,487 名男性。这称作分层采样(stratified sampling):将人群分成均匀的子分组,称为分层,从每个分层去取合适数量的实例,以保证测试集对总人数有代表性。如果调查公司采用纯随机采样,会有 12% 的概率导致采样偏差:女性人数少于 49%,或多于 54%。不管发生那种情况,调查结果都会严重偏差。

假设专家告诉你,收入中位数是预测房价中位数非常重要的属性。你可能想要保证测试集可以代表整体数据集中的多种收入分类。因为收入中位数是一个连续的数值属性,你首先需要创建一个收入类别属性。再仔细地看一下收入中位数的柱状图(图 3-5),处理后的图见图 3-6:

# 使用pandas的hist()方法绘制housing DataFrame中"median_income"列的直方图。
# 直方图是一种用于展示数值型数据分布的图形表示方法,它将数据分成多个“箱子”,
# 并计算每个箱子中数据点的数量,然后以柱状图的形式展示这些数量。
# 这种方法有助于我们快速了解数据的分布情况,比如是否存在偏态分布或异常值。
# 绘制直方图是数据分析中的一个基本步骤,可以帮助我们做出更好的数据预处理决策。
housing["median_income"].hist()

运行结果:

图 3-5 收入分类的柱状图

(对该图是的收入中位数处理过后的图过程如下)

大多数的收入中位数的值聚集在 2-5(万美元),但是一些收入中位数会超过 6。数据集中的每个分层都要有足够的实例位于你的数据中,这点很重要。否则,对分层重要性的评估就会有偏差。这意味着,你不能有过多的分层,且每个分层都要足够大。后面的代码通过将收入中位数除以 1.5(以限制收入分类的数量),创建了一个收入类别属性,用ceil对值舍入(以产生离散的分类),然后将所有大于 5 的分类归入到分类 5:

# 将"median_income"列的值除以1.5,并对结果向上取整,创建一个新的列"income_cat"。
# 这样做的目的是将收入分为更少的类别,以简化模型处理。
# 向上取整确保了即使收入值在1.5的倍数之间,也能被正确地分类到下一个整数类别。
housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)

# 使用where方法将"income_cat"列中大于或等于5的值设置为5。
# where方法的第一个参数是一个条件表达式,如果为True,则使用第二个参数的值;
# 如果为False,则使用第三个参数的值。在这里,我们使用5.0作为上限值。
# 这样做可以防止某些极端高收入值对模型造成过大的影响,有助于数据的正则化。
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)
# 使用pandas的cut函数将"median_income"列的值切分为不同的区间,并创建一个新的列"income_cat"。
# 这个操作被称为分箱(binning),是一种将连续数值数据离散化的方法。
# 这里定义了5个区间的边界:[0., 1.5, 3.0, 4.5, 6., np.inf],表示:
# - 收入在0以下(不包括0)的数据点将被分到第一个箱。
# - 收入在0到1.5之间的数据点将被分到第二个箱,以此类推。
# - 收入大于或等于6的数据点将被分到第五个箱。
# 同时,定义了与这些区间相对应的标签:[1, 2, 3, 4, 5]。
# 这有助于将连续的收入数据转换为离散的类别,可能用于简化模型或处理异常值。
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])

# 使用value_counts方法计算"income_cat"列中各个类别的出现次数。
# 这有助于我们了解不同收入类别的分布情况,对于数据探索和可视化非常有用。
# 例如,它可以帮助我们识别是否有某个收入类别的样本数量特别多或特别少,
# 这可能影响模型训练和预测的准确性。
housing["income_cat"].value_counts()

运行结果:

income_cat
3    7236
2    6581
4    3639
5    2362
1     822
Name: count, dtype: int64
# 使用pandas的hist()方法绘制housing DataFrame中新创建的"income_cat"列的直方图。
# 由于"income_cat"是一个分箱后的类别数据,直方图的每个柱子将代表一个特定的收入区间。
# 这个直方图有助于我们可视化不同收入类别的分布情况,比如哪些类别的样本数量更多,
# 以及是否存在某些类别的样本数量异常少,这可能需要进一步的数据探索或处理。
# 绘制直方图是数据分析中的一个重要步骤,它可以帮助我们更好地理解数据的特征和分布。
housing["income_cat"].hist()

运行结果:

图 3-5 收入分类的柱状图(处理后的图)

现在,就可以根据收入分类,进行分层采样。你可以使用 Scikit-Learn 的StratifiedShuffleSplit类:

# 从sklearn的model_selection模块导入StratifiedShuffleSplit类。
# StratifiedShuffleSplit用于分层抽样,确保训练集和测试集中各类别的比例
# 与原始数据集中的相应比例相同。这在类别不平衡的情况下特别有用。
from sklearn.model_selection import StratifiedShuffleSplit

# 初始化StratifiedShuffleSplit对象,设置n_splits=1表示只生成一个训练集和测试集的分割,
# test_size=0.2表示测试集占原始数据集的20%,random_state=42确保结果的可重复性。
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)

# 使用split对象的split方法对housing数据集进行分层抽样。
# 该方法接受两个参数:数据集housing和用于分层的列"income_cat"。
# 迭代生成的索引对(train_index, test_index),分别代表训练集和测试集的行索引。
for train_index, test_index in split.split(housing, housing["income_cat"]):
    # 使用.loc方法和生成的索引创建训练集和测试集的DataFrame。
    # strat_train_set包含训练数据,strat_test_set包含测试数据。
    # 这种分层抽样方法有助于保持数据的多样性,提高模型评估的准确性。
    strat_train_set = housing.loc[train_index]
    strat_test_set = housing.loc[test_index]

检查下结果是否符合预期。你可以在完整的房产数据集中查看收入分类比例:

# 使用pandas的value_counts方法计算strat_test_set中"income_cat"列各个类别的频率分布。
# 这个方法返回一个Series,其中索引是"income_cat"列中的唯一值,数据是这些值在列中出现的次数。
# 然后,我们将这个频率分布除以strat_test_set的总行数(即len(strat_test_set)),
# 以得到每个类别在测试集中的相对频率。

# 这种计算方法可以帮助我们了解在分层抽样后,不同收入类别在测试集中的分布情况。
# 通过分析这些相对频率,我们可以评估测试集是否在各个类别上保持了与原始数据集相似的比例,
# 这对于确保模型评估的公正性和准确性是非常重要的。

# 最终结果是一个表示每个收入类别相对频率的Series,可以用于进一步的数据分析或可视化。
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

比较

# 使用pandas的value_counts方法计算housing中"income_cat"列各个类别的出现次数,
# 并返回一个Series,其中索引是"income_cat"列中的唯一值,数据是这些值在列中出现的次数。
# 这个方法有助于了解不同收入类别在数据集中的分布情况。

# 然后,将得到的每个类别的出现次数除以housing的总行数(即len(housing)),
# 以计算每个收入类别的相对频率。相对频率是一个比例值,表示每个类别在数据集中所占的比例。

# 这种计算方法可以帮助我们快速了解数据集中不同收入类别的分布情况,
# 并为进一步的数据分析和模型训练提供有价值的信息。
# 例如,如果某个收入类别的频率特别高或特别低,可能需要在模型训练中特别考虑。

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

使用相似的代码,还可以测量测试集中收入分类的比例。图 3-7  对比了总数据集、分层采样的测试集、纯随机采样测试集的收入分类比例。可以看到,分层采样测试集的收入分类比例与总数据集几乎相同,而随机采样数据集偏差严重。

# 定义一个函数income_cat_proportions,用于计算数据集中"income_cat"列各个类别的相对频率。
# 这个函数接受一个DataFrame data作为参数,使用value_counts方法计算"income_cat"列中每个类别的出现次数,
# 然后除以data的总行数,以得到每个类别的相对频率。
def income_cat_proportions(data):
    return data["income_cat"].value_counts() / len(data)

# 使用sklearn的train_test_split函数将housing数据集分割为训练集和测试集。
# 测试集大小设置为20%(test_size=0.2),并设置随机状态为42以确保结果的可重复性。
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

# 创建一个DataFrame compare_props,用于存储不同数据集中"income_cat"类别的相对频率,
# 并进行比较。DataFrame包含三列:"Overall"表示原始数据集的类别频率,
# "Stratified"表示分层抽样测试集的类别频率,"Random"表示随机抽样测试集的类别频率。
compare_props = pd.DataFrame({
    "Overall": income_cat_proportions(housing),
    "Stratified": income_cat_proportions(strat_test_set),
    "Random": income_cat_proportions(test_set),
}).sort_index()

# 计算"Random"和"Stratified"列相对于"Overall"列的百分比误差。
# 百分比误差是通过计算两个频率的比值再减去100得到的。
# 这有助于评估分层抽样和随机抽样方法在保持原始数据集分布上的效果。
compare_props["Rand. %error"] = 100 * compare_props["Random"] / compare_props["Overall"] - 100
compare_props["Strat. %error"] = 100 * compare_props["Stratified"] / compare_props["Overall"] - 100

compare_props

运行结果:

图 3-7 分层采样和纯随机采样的样本偏差比较

现在,你需要删除income_cat属性,使数据回到初始状态:

# 遍历包含strat_train_set和strat_test_set的元组,这两个变量分别代表经过分层抽样得到的训练集和测试集。
for set_ in (strat_train_set, strat_test_set):
    # 对每个数据集set_,使用pandas的drop方法删除"income_cat"列。
    # "income_cat"是我们之前为了分层抽样而创建的辅助列,可能不再需要用于后续的模型训练。
    # axis=1参数指定操作是针对列而不是行,inplace=True参数表示直接在原始DataFrame上进行修改,而不是创建一个新的DataFrame。
    # 删除"income_cat"列可以简化数据集,避免在模型训练过程中不小心使用到这个辅助特征。
    set_.drop("income_cat", axis=1, inplace=True)

我们用了大量时间来生成测试集的原因是:测试集通常被忽略,但实际是机器学习非常重要的一部分。还有,生成测试集过程中的许多思路对于后面的交叉验证讨论是非常有帮助的。接下来进入下一阶段:数据探索。

附录:

一、一些知识点

1、流水线

一系列的数据处理组件被称为数据流水线。流水线在机器学习系统中很常见,因为有许多数据要处理和转换。

组件通常是异步运行的。每个组件吸纳进大量数据,进行处理,然后将数据传输到另一个数据容器中,而后流水线中的另一个组件收入这个数据,然后输出,这个过程依次进行下去。每个组件都是独立的:组件间的接口只是数据容器。这样可以让系统更便于理解(记住数据流的图),不同的项目组可以关注于不同的组件。进而,如果一个组件失效了,下游的组件使用失效组件最后生产的数据,通常可以正常运行(一段时间)。这样就使整个架构相当健壮。

另一方面,如果没有监控,失效的组件会在不被注意的情况下运行一段时间。数据会受到污染,整个系统的性能就会下降。

二、源码工程

https://github.com/XANkui/PythonMachineLearnIntermediateLevel

三、该案例的环境 package 信息如下

Package                   Version
------------------------- --------------
anyio                     4.4.0
argon2-cffi               23.1.0
argon2-cffi-bindings      21.2.0
arrow                     1.3.0
asttokens                 2.4.1
async-lru                 2.0.4
attrs                     23.2.0
Babel                     2.15.0
beautifulsoup4            4.12.3
bleach                    6.1.0
certifi                   2024.7.4
cffi                      1.16.0
charset-normalizer        3.3.2
colorama                  0.4.6
comm                      0.2.2
contourpy                 1.2.1
cycler                    0.12.1
debugpy                   1.8.2
decorator                 5.1.1
defusedxml                0.7.1
executing                 2.0.1
fastjsonschema            2.20.0
fonttools                 4.53.1
fqdn                      1.5.1
h11                       0.14.0
httpcore                  1.0.5
httpx                     0.27.0
idna                      3.7
ipykernel                 6.29.5
ipython                   8.26.0
ipywidgets                8.1.3
isoduration               20.11.0
jedi                      0.19.1
Jinja2                    3.1.4
joblib                    1.4.2
json5                     0.9.25
jsonpointer               3.0.0
jsonschema                4.23.0
jsonschema-specifications 2023.12.1
jupyter                   1.0.0
jupyter_client            8.6.2
jupyter-console           6.6.3
jupyter_core              5.7.2
jupyter-events            0.10.0
jupyter-lsp               2.2.5
jupyter_server            2.14.2
jupyter_server_terminals  0.5.3
jupyterlab                4.2.4
jupyterlab_pygments       0.3.0
jupyterlab_server         2.27.3
jupyterlab_widgets        3.0.11
kiwisolver                1.4.5
MarkupSafe                2.1.5
matplotlib                3.9.1
matplotlib-inline         0.1.7
mistune                   3.0.2
nbclient                  0.10.0
nbconvert                 7.16.4
nbformat                  5.10.4
nest-asyncio              1.6.0
notebook                  7.2.1
notebook_shim             0.2.4
numpy                     2.0.1
overrides                 7.7.0
packaging                 24.1
pandas                    2.2.2
pandocfilters             1.5.1
parso                     0.8.4
pillow                    10.4.0
pip                       24.1.2
platformdirs              4.2.2
prometheus_client         0.20.0
prompt_toolkit            3.0.47
psutil                    6.0.0
pure_eval                 0.2.3
pycparser                 2.22
Pygments                  2.18.0
pyparsing                 3.1.2
python-dateutil           2.9.0.post0
python-json-logger        2.0.7
pytz                      2024.1
pywin32                   306
pywinpty                  2.0.13
PyYAML                    6.0.1
pyzmq                     26.0.3
qtconsole                 5.5.2
QtPy                      2.4.1
referencing               0.35.1
requests                  2.32.3
rfc3339-validator         0.1.4
rfc3986-validator         0.1.1
rpds-py                   0.19.1
scikit-learn              1.5.1
scipy                     1.14.0
Send2Trash                1.8.3
setuptools                70.1.1
six                       1.16.0
sniffio                   1.3.1
soupsieve                 2.5
stack-data                0.6.3
terminado                 0.18.1
threadpoolctl             3.5.0
tinycss2                  1.3.0
tornado                   6.4.1
traitlets                 5.14.3
types-python-dateutil     2.9.0.20240316
typing_extensions         4.12.2
tzdata                    2024.1
uri-template              1.3.0
urllib3                   2.2.2
wcwidth                   0.2.13
webcolors                 24.6.0
webencodings              0.5.1
websocket-client          1.8.0
wheel                     0.43.0
widgetsnbextension        4.0.11

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仙魁XAN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值