数据科学研讨会-全-

数据科学研讨会(全)

原文:annas-archive.org/md5/b58febc9386aa555b22554a367c4e4b7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于本书

数据无处不在,洞察也随之而来。在如此庞大的数据生成过程中,存在巨大的空间来提取有意义的信息,这些信息将提升业务的生产力和盈利能力。通过学习如何将原始数据转化为改变游戏规则的洞察,你将为自己打开新的职业道路和机会。

《数据科学工作坊(第二版)》首先介绍了不同类型的项目,并展示了如何将机器学习算法融入其中。你将学习选择相关的评估指标,甚至评估模型的性能。为了调节算法的超参数并提高其准确性,你将实际操作网格搜索和随机搜索等方法。

最后,你将学习降维技术,轻松处理大量变量,然后探索如何使用模型集成技术并创建新特征以增强模型性能。

本书结束时,你将掌握信心十足地开始数据科学项目的技能。

读者群体

本书是一本非常实用的数据科学书籍,适合有志成为数据分析师、数据科学家、数据库工程师和业务分析师的人。它面向那些希望通过快速学习数据科学技巧来启动数据科学职业生涯的人,而无需深入了解机器学习算法背后的数学原理。对 Python 编程语言的基础知识将帮助你轻松理解本书中的概念。

关于章节

第一章Python 中的数据科学简介,将向你介绍数据科学领域,并带你了解 Python 的核心概念及其在数据科学世界中的应用。

第二章回归分析,将使你熟悉线性回归分析及其在数据科学中的实际应用。

第三章二分类,将教你一种监督学习技术——分类,用以生成商业成果。

第四章使用随机森林进行多类分类,将向你展示如何使用随机森林算法训练一个多类分类器。

第五章进行第一次聚类分析,将向你介绍无监督学习任务,在这些任务中,算法必须从数据中自动学习模式,因为事先没有定义目标变量。

第六章如何评估性能,将教你如何评估一个模型并在决定将其投入生产之前评估其性能。

第七章机器学习模型的泛化,将教你如何通过拆分数据或使用交叉验证,使你的数据更好地用于训练更好的模型。

第八章超参数调优,将指导你通过系统评估具有不同超参数的估计器,进一步提升预测性能。

第九章解释机器学习模型,将向你展示如何解释机器学习模型的结果,并深入了解它所发现的模式。

第十章数据集分析,将向你介绍进行探索性数据分析和可视化数据的技巧,以识别质量问题、潜在的数据转换和有趣的模式。

第十一章数据准备,将介绍你可以用来处理数据问题的主要技巧,以确保你的数据质量足够高,适合进行成功建模。

第十二章特征工程,将教你在现有数据集上创建新变量的一些关键技巧。

第十三章不平衡数据集,将帮助你识别数据集可能不平衡的使用场景,并制定处理不平衡数据集的策略。

第十四章降维,将展示如何分析高维数据集,并处理这些数据集带来的挑战。

第十五章集成学习,将教你如何将不同的集成学习技术应用于你的数据集。

还有三章附加章节,第十六章机器学习管道第十七章自动特征工程,和第十八章使用 Flask 的模型即服务,你可以在packt.live/2ZagB9y找到这些章节。

约定

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号名以如下方式显示:

"sklearn中有一个名为train_test_split的类,它提供了拆分数据的功能。"

你在屏幕上看到的词语,例如菜单或对话框中的内容,也会以相同的格式显示。

代码块的设置如下:

import pandas as pd
from sklearn.model_selection import train_test_split

新术语和重要单词以如下方式显示:

“字典包含多个元素,类似于列表,但每个元素都是以键值对的形式组织的。”

代码展示

跨越多行的代码使用反斜杠(\)拆分。当代码执行时,Python 会忽略反斜杠,并将下一行的代码视为当前行的直接延续。

例如:

history = model.fit(X, y, epochs=100, batch_size=5, verbose=1, \
                    validation_split=0.2, shuffle=False)

注释被添加到代码中以帮助解释特定的逻辑。单行注释使用#符号表示,如下所示:

# Print the sizes of the datasets
print("Number of Examples in the Dataset = ", X.shape[0])
print("Number of Features for each example = ", X.shape[1])

多行注释用三个引号括起来,如下所示:

"""
Define a seed for the random number generator to ensure the 
result will be reproducible
"""
seed = 1
np.random.seed(seed)
random.set_seed(seed)

设置你的环境

在详细探讨本书内容之前,我们需要设置特定的软件和工具。在接下来的部分中,我们将看到如何做到这一点。

如何设置 Google Colab

有许多集成开发环境IDE)可供 Python 使用。用于数据科学项目的最流行的 IDE 是来自 Anaconda 的 Jupyter Notebook,但这不是我们为本书推荐的工具。当你开始进入数据科学领域时,我们认为与其让你从零开始设置 Python 环境,不如建议你使用即插即用的解决方案,这样你就可以专注于学习本书中介绍的概念。我们希望尽量减少困难,确保你进入数据科学的第一步尽可能简单明了。

幸运的是,确实有这样的工具,它叫做Google Colab。这是 Google 提供的一个免费的工具,运行在云端,因此你不需要购买新的笔记本电脑或升级其规格。使用 Colab 的另一个好处是,本书中使用的大多数 Python 包已经安装好,你可以直接使用。你所需要的只是一个 Google 账户。如果你没有账户,可以在这里创建一个:packt.live/37mea5X

然后,你需要订阅 Colab 服务:

  1. 首先,登录到 Google Drive:packt.live/2TM1v8w

  2. 然后,访问以下网址:packt.live/2NKaAuP

    你应该看到以下屏幕:

    图 0.1:Google Colab 介绍页面

    图 0.1:Google Colab 介绍页面

  3. 然后,你可以点击NEW PYTHON 3 NOTEBOOK,你应该会看到一个新的 Colab 笔记本!图 0.2:新的 Colab 笔记本

图 0.2:新的 Colab 笔记本

你刚刚将 Google Colab 添加到你的 Google 账户,现在你可以开始编写和执行自己的 Python 代码了。

如何使用 Google Colab

现在你已经将 Google Colab 添加到你的账户中,让我们来看看如何使用它。Google Colab 与 Jupyter Notebook 非常相似。它实际上基于 Jupyter,但运行在 Google 的服务器上,并与他们的服务(如 Google Drive)有额外的集成。

要打开新的 Colab 笔记本,你需要登录到你的 Google Drive 账户,然后点击+ 新建图标:

图 0.3:打开新笔记本的选项

图 0.3:打开新笔记本的选项

在显示的菜单中,选择更多,然后选择Google Colaboratory

图 0.4:从 Google Drive 打开 Colab 笔记本的选项

图 0.4:从 Google Drive 打开 Colab 笔记本的选项

会创建一个新的 Colab 笔记本。

图 0.5:新的 Colab 笔记本

图 0.5:新的 Colab 笔记本

Colab 笔记本是一个交互式集成开发环境(IDE),你可以在其中运行 Python 代码或使用单元格添加文本。单元格是一个容器,你将在其中添加代码行或与你的项目相关的任何文本信息。在每个单元格中,你可以添加任意数量的代码行或文本。单元格可以显示运行代码后的输出结果,因此它是一种非常强大的测试和检查工作成果的方式。最佳实践是不要将大量代码塞进一个单元格。尽量将代码分割成多个单元格,这样你就可以独立运行它们,并一步一步地检查你的代码是否正常工作。

现在让我们看看如何在单元格中编写一些 Python 代码并运行它。一个单元格由四个主要部分组成:

  1. 你将编写代码的文本框

  2. 用于运行代码的 Run 按钮

  3. 提供额外功能的选项菜单

  4. 输出显示!图 0.6: Colab 笔记本单元格的部分内容

图 0.6: Colab 笔记本单元格的部分内容

在前面的例子中,我们仅编写了一行简单的代码,将 2 加到 3。现在,我们需要点击 Run 按钮或使用快捷键 Ctrl + Enter 来运行代码。然后,结果将显示在单元格下方。如果你的代码出现错误(当代码出错时),错误信息将显示在单元格下方:

图 0.7: Google Colab 的错误信息

图 0.7: Google Colab 的错误信息

如你所见,我们尝试将一个整数与字符串相加,这是不可能的,因为它们的数据类型不兼容,这正是错误信息所告诉我们的。

要添加一个新的单元格,你只需要点击顶部选项栏上的 + Code+ Text

图 0.8: 新建单元格按钮

图 0.8: 新建单元格按钮

如果你添加一个新的 Text 单元格,你将能够使用一些特定的选项来编辑文本,例如粗体、斜体、超链接等:

图 0.9: 单元格上的不同选项

图 0.9: 单元格上的不同选项

这种类型的单元格实际上是支持 Markdown 的。因此,你可以轻松创建标题、副标题、项目符号等。这里有一个链接可以了解更多关于 Markdown 选项的内容: packt.live/2NVgVDT

使用单元格选项菜单,你可以删除单元格或将其在笔记本中上下移动:

图 0.10: 单元格选项

图 0.10: 单元格选项

如果你需要安装 Google Colab 中没有的特定 Python 包,你只需要运行一个带有以下语法的单元格:

!pip install <package_name>

注意

'!' 是一个魔术命令,用于运行 shell 命令。

图 0.11: 使用 "!" 命令

图 0.11: 使用 "!" 命令

你刚刚学习了 Google Colab 提供的主要功能,用于运行 Python 代码。虽然还有许多其他功能,但你现在已经掌握了足够的知识,可以继续学习本书的内容。

访问代码文件

你可以在packt.live/2ucwsId找到本书的完整代码文件。你还可以通过使用packt.live/3gfsH76的互动实验环境,直接在你的网页浏览器中运行许多活动和练习。

我们尽力为所有活动和练习提供互动版本,但对于不支持的情况,我们建议在 Google Colab 上执行代码。

本书中使用的高质量彩色图像可以在packt.live/30O91Bd找到。

如果你在安装过程中遇到任何问题或有任何疑问,请通过电子邮件联系我们:workshops@packt.com

第一章:1. Python 中的数据科学介绍

概述

本章将向您介绍数据科学领域,并带您了解 Python 的核心概念及其在数据科学中的应用。

到本章结束时,您将能够解释什么是数据科学,并区分监督学习和无监督学习。您还将能够解释什么是机器学习,并区分回归、分类和聚类问题。您将学会创建和操作不同类型的 Python 变量,包括核心变量、列表和字典。您将能够构建一个for循环,使用 f-strings 打印结果,定义函数,导入 Python 包,并使用pandas加载不同格式的数据。您还将第一次体验使用 scikit-learn 训练模型。

介绍

欢迎来到数据科学的迷人世界!我们相信您一定很兴奋开始您的旅程,学习有趣和激动人心的技术和算法。这正是本书的目的所在。

但在深入之前,让我们定义一下什么是数据科学:它是多个学科的结合,包括商业、统计学和编程,旨在通过运行类似科学研究的控制实验,从数据中提取有意义的见解。

任何数据科学项目的目标都是从数据中提取有价值的知识,以便做出更好的决策。数据科学家有责任定义项目要达成的目标。这需要业务知识和专业技能。在本书中,您将接触到一些来自真实数据集的数据科学任务示例。

统计学是一个用于分析数据并发现模式的数学领域。许多最新和最先进的技术仍然依赖于核心统计方法。本书将向您展示理解我们将要讲解的概念所需的基本技术。

随着数据生成量的指数级增长,处理数据所需的计算能力也在不断增加。这就是为什么编程是数据科学家必备技能的原因。你可能会问,为什么我们在本次工作坊中选择了 Python。那是因为 Python 是数据科学中最流行的编程语言之一。由于其简单易读的语法,学习 Python 编程非常容易。它还拥有大量免费开放的包,例如 pandas、scikit-learn、TensorFlow 和 PyTorch。Python 的社区正以惊人的速度扩展,添加了越来越多的新功能,并提高了性能和可靠性。难怪像 Facebook、Airbnb 和 Google 这样的公司将其作为主要技术栈之一。对于本书而言,不需要事先了解 Python。如果你有一些 Python 或其他编程语言的经验,那会是一个优势,但所有的概念都会完全解释清楚,所以即使你是编程新手也不用担心。

数据科学的应用

如前言所述,数据科学是一种多学科的方法,用于分析和识别复杂的模式,并从数据中提取有价值的洞察。运行一个数据科学项目通常涉及多个步骤,包括以下内容:

  1. 定义要解决的业务问题

  2. 收集或提取现有数据

  3. 分析、可视化和准备数据

  4. 训练模型以识别数据中的模式并做出预测

  5. 评估模型的表现并进行改进

  6. 沟通和展示发现的结果及获得的洞察

  7. 部署和维护模型

正如其名字所暗示的,数据科学项目需要数据,但实际上,首先定义清晰的业务问题才是最重要的。如果问题没有正确定义,项目可能会导致错误的结果,因为你可能使用了错误的信息,数据没有正确准备,或者让模型学习了错误的模式。因此,正确地与相关方定义数据科学项目的范围和目标是至关重要的。

数据科学在现实世界或商业环境中有很多应用。例如,医疗提供者可能会训练一个模型,根据医学测量数据预测医疗结果或其严重程度,或者一所高中可能想根据学生的历史成绩和过往行为预测哪些学生在一年内有辍学的风险。公司可能希望了解基于客户的历史购买记录,某个客户购买某个产品的可能性。他们也可能需要更好地了解哪些客户更有可能停止使用现有服务并流失。这些都是数据科学可以用来实现明确目标的例子,例如早期发现患有心脏病的患者数量,或者减少六个月后取消订阅的客户数量。这听起来很令人兴奋吧?很快,你也将参与这样的有趣项目。

什么是机器学习?

当我们提到数据科学时,通常会想到机器学习,有些人可能不了解它们之间的区别。机器学习是构建能够自我学习模式的算法的领域,而无需明确编程。所以,机器学习是一系列可以在数据科学项目建模阶段使用的技术。

机器学习由三种不同类型的学习组成:

  • 有监督学习

  • 无监督学习

  • 强化学习

有监督学习

有监督学习指的是一种任务类型,其中算法根据先前的知识学习模式。这意味着这种学习方式需要提前标注预测的结果(也叫响应变量、因变量或目标变量)。例如,如果你想训练一个预测客户是否会取消订阅的模型,你需要一个数据集,其中包含一个列(或变量),该列已经包含了过去或现有客户的流失结果(取消或未取消)。这个结果必须在模型训练之前由某人进行标注。如果这个数据集包含 5,000 条观测数据,那么所有数据都需要标注好结果。模型的目标是学习这个结果列与其他特征(也叫独立变量或预测变量)之间的关系。以下是此类数据集的示例:

图 1.1:客户流失数据集示例

图 1.1:客户流失数据集示例

Cancel 列是响应变量。这是你关注的列,你希望模型能够准确预测新输入数据(在这个例子中是新客户)的结果。所有其他列是预测变量。

在经过训练后,模型可能会发现以下模式:当客户订阅 12 个月后,并且其月均消费超过$50时,取消订阅的可能性较大。因此,如果一位新客户已经订阅了 15 个月,且每月消费为$85,模型会预测该客户未来会取消合同。

当响应变量包含有限数量的可能值(或类别)时,这是一个分类问题(你将在第三章,二分类第四章,随机森林多类分类中了解更多)。模型将学习如何根据独立变量的值预测正确的类别。我们刚才提到的客户流失例子就是一个分类问题,因为响应变量只能取两个不同的值:yesno

另一方面,如果响应变量可以有无限多的可能值,那么这就是回归问题。

回归问题的一个例子是预测某些制造工厂每天生产的手机数量。这个值可能在 0 到无限大的范围内(或一个足够大的范围,以包含大量可能的值),如图 1.2所示。

图 1.2:手机生产数据集示例

图 1.2:手机生产数据集示例

在上面的图中,你可以看到日产量的值可以从15000到超过50000。这是一个回归问题,我们将在第二章,回归中详细讲解。

无监督学习

无监督学习是一种不需要任何响应变量的算法。在这种情况下,模型会自行从数据中学习模式。你可能会问,如果没有预先指定目标,它能找到什么样的模式呢?

这种类型的算法通常可以检测变量或记录之间的相似性,因此它会尝试将彼此非常接近的对象进行分组。这类算法可以用于聚类(分组记录)或降维(减少变量数量)。聚类在客户细分中非常流行,算法会根据数据将具有相似行为的客户分组。第五章执行您的第一次聚类分析,将带你走过一个聚类分析的例子。

强化学习

强化学习是另一种算法,它根据所收到的反馈学习如何在特定环境中采取行动。你可能看过一些视频,算法通过自身训练来玩 Atari 游戏。强化学习技术正被用于教导智能体根据游戏中的奖励或惩罚来决定如何在游戏中行动。

举例来说,在游戏 Pong 中,智能体会在多轮训练后学会不让球掉落,因为每次球掉落时它会受到很高的惩罚。

注意

强化学习算法不在本书的讨论范围内,将不会在本书中涉及。

Python 概述

如前所述,Python 是数据科学中最流行的编程语言之一。但在深入探讨 Python 在数据科学中的应用之前,让我们快速了解一下 Python 的一些核心概念。

变量类型

在 Python 中,你可以处理和操作不同类型的变量。每种变量类型都有其独特性和优势。我们不会逐一介绍所有的变量类型,而是重点讲解本书中你将需要使用的主要变量类型。对于以下每个代码示例,你可以在 Google Colab 中运行代码来查看给定的输出结果。

数字变量

最基本的变量类型是数字型。这种变量可以包含整数或小数(或浮动点)数字,并且可以对其执行一些数学运算。

假设我们使用一个名为var1的整数变量,它的值是8,另一个名为var2的变量,其值为160.88,我们可以用+运算符将它们相加,如下所示:

var1 = 8
var2 = 160.88
var1 + var2

你应该得到以下输出:

图 1.3:两个变量相加的输出结果

图 1.3:两个变量相加的输出结果

在 Python 中,你还可以对数字变量执行其他数学运算,例如乘法(使用*运算符)和除法(使用/运算符)。

文本变量

另一个有趣的变量类型是string,它包含文本信息。你可以使用单引号或双引号创建一个包含特定文本的变量,如下例所示:

var3 = 'Hello, '
var4 = 'World'

为了显示一个变量的内容,你可以调用print()函数:

print(var3)
print(var4)

你应该得到以下输出:

图 1.4:打印两个文本变量

图 1.4:打印两个文本变量

Python 还提供了一种名为 f-strings 的接口,用于打印带有已定义变量值的文本。当你想要打印结果并附加额外文本使其更易读,或是解释结果时,这非常方便。f-strings 在打印日志时也非常常见。你需要在引号(或双引号)前添加f,以指定文本为 f-strings。然后,你可以将现有的变量放在引号内,并显示该变量的值。变量需要用大括号{}括起来。

举例来说,如果我们想要在var3var4的值前面打印Text:,我们将写出以下代码:

print(f"Text: {var3} {var4}!")

你应该得到以下输出:

图 1.5:使用 f-strings 打印

图 1.5:使用 f-strings 打印

你还可以对字符串变量执行一些与文本相关的转换操作,例如将字符大写或替换字符。例如,你可以使用 + 运算符将两个变量连接起来:

var3 + var4

你应该得到以下输出:

图 1.6:两个文本变量的连接

图 1.6:两个文本变量的连接

Python 列表

另一个非常有用的变量类型是列表。它是一个可以更改的项集合(你可以添加、更新或删除项)。声明一个列表时,你需要使用方括号 [],像这样:

var5 = ['I', 'love', 'data', 'science']
print(var5)

你应该得到以下输出:

图 1.7:仅包含字符串项的列表

图 1.7:仅包含字符串项的列表

列表可以包含不同类型的项,因此你可以在其中混合数字和文本变量:

var6 = ['Packt', 15019, 2020, 'Data Science']
print(var6)

你应该得到以下输出:

图 1.8:包含数字和字符串项的列表

图 1.8:包含数字和字符串项的列表

列表中的项可以通过其索引(在列表中的位置)来访问。要访问列表中的第一个项(索引 0)和第三个项(索引 2),你可以这样做:

print(var6[0])
print(var6[2])

注意

在 Python 中,所有索引从 0 开始。

你应该得到以下输出:

图 1.9:var6 列表中的第一个和第三个项

图 1.9:var6 列表中的第一个和第三个项

Python 提供了一个 API,可以使用 : 运算符访问一系列项。你只需要指定运算符左侧的起始索引和右侧的结束索引。结束索引总是被排除在范围之外。所以,如果你想获得前三个项(索引 0 到 2),你应该这样做:

print(var6[0:3])

你应该得到以下输出:

图 1.10:var6 中的前三个项

图 1.10:var6 中的前三个项

你还可以使用 for 循环遍历列表中的每个项。如果你想打印 var6 列表中的每个项,你应该这样做:

for item in var6:
    print(item)

你应该得到以下输出:

图 1.11:for 循环的输出

图 1.11:for 循环的输出

你可以使用 .append() 方法将项添加到列表的末尾:

var6.append('Python')
print(var6)

你应该得到以下输出:

图 1.12:插入 'Python' 项后的 var6 输出

图 1.12:插入 'Python' 项后的 var6 输出

要从列表中删除一项,你可以使用 .remove() 方法:

var6.remove(15019)
print(var6)

你应该得到以下输出:

图 1.13:删除 '15019' 项后的 var6 输出

图 1.13:删除 '15019' 项后的 var6 输出

Python 字典

另一个数据科学家非常常用的 Python 变量是字典类型。例如,它可以用来将 JSON 数据加载到 Python 中,然后将其转换为 DataFrame(你将在后续章节中学习更多关于 JSON 格式和 DataFrame 的内容)。字典包含多个元素,类似{},并通过:分隔键和值,如下所示:

var7 = {'Topic': 'Data Science', 'Language': 'Python'}
print(var7)

你应该得到以下输出:

图 1.14:var7 的输出

图 1.14:var7 的输出

要访问特定的值,你需要提供相应的键名。例如,如果你想获取值Python,你可以这样做:

var7['Language']

你应该得到以下输出:

图 1.15:'Language'键的值

图 1.15:'Language'键的值

注意

字典中的每个键值对必须是唯一的。

Python 提供了一种方法来访问字典中的所有键名,.keys(),其用法如下面的代码片段所示:

var7.keys()

你应该得到以下输出:

图 1.16:键名列表

图 1.16:键名列表

还有一种叫做.values()的方法,可以用于访问字典的所有值:

var7.values()

你应该得到以下输出:

图 1.17:值的列表

图 1.17:值的列表

你可以使用for循环和.items()方法遍历字典中的所有项,如下所示的代码片段所示:

for key, value in var7.items():
    print(key)
    print(value)

你应该得到以下输出:

图 1.18:遍历字典项后的输出

图 1.18:遍历字典项后的输出

你可以通过提供键名来向字典中添加一个新元素,方法如下:

var7['Publisher'] = 'Packt'
print(var7)

你应该得到以下输出:

图 1.19:在向字典中添加项目后的输出

图 1.19:在向字典中添加项目后的输出

你可以使用del命令从字典中删除一个项目:

del var7['Publisher']
print(var7)

你应该得到以下输出:

图 1.20:在移除一个项目后的字典输出

图 1.20:在移除一个项目后的字典输出

练习 1.01创建一个包含机器学习算法的字典中,我们将使用刚才学习的这些概念。

注意

如果你有兴趣深入探索 Python,可以访问我们的网站(packt.live/2FcXpOp)购买 Python 工作坊课程。

练习 1.01:创建一个包含机器学习算法的字典

在本练习中,我们将使用 Python 创建一个字典,其中包含本书中将涵盖的不同机器学习算法的集合。

以下步骤将帮助你完成练习:

注意

本书中的每个练习和活动都将在 Google Colab 上执行。

  1. 在新的 Colab 笔记本中打开。

  2. 创建一个名为algorithm的列表,包含以下元素:Linear RegressionLogistic RegressionRandomForesta3c

    algorithm = ['Linear Regression', 'Logistic Regression', \
                 'RandomForest', 'a3c']
    \ ) to split the logic across multiple lines. When the code is executed, Python will ignore the backslash, and treat the code on the next line as a direct continuation of the current line.
    
  3. 现在,创建一个名为learning的列表,包含以下元素:SupervisedSupervisedSupervisedReinforcement

    learning = ['Supervised', 'Supervised', 'Supervised', \
                'Reinforcement']
    
  4. 创建一个名为algorithm_type的列表,包含以下元素:RegressionClassificationRegression or ClassificationGame AI

    algorithm_type = ['Regression', 'Classification', \
                      'Regression or Classification', 'Game AI']
    
  5. 使用.append()方法将k-means项添加到algorithm列表中:

    algorithm.append('k-means')
    
  6. 使用print()函数显示algorithm的内容:

    print(algorithm)
    

    你应该得到以下输出:

    图 1.21:'algorithm' 的输出

    图 1.21:'algorithm' 的输出

    从前面的输出中,我们可以看到我们将k-means项添加到列表中。

  7. 现在,使用.append()方法将Unsupervised项添加到learning列表中:

    learning.append('Unsupervised')
    
  8. 使用print()函数显示learning的内容:

    print(learning)
    

    你应该得到以下输出:

    图 1.22:'learning' 的输出

    图 1.22:'learning' 的输出

    从前面的输出中,我们可以看到我们将Unsupervised项添加到了列表中。

  9. 使用.append()方法将Clustering项添加到algorithm_type列表中:

    algorithm_type.append('Clustering')
    
  10. 使用print()函数显示algorithm_type的内容:

    print(algorithm_type)
    

    你应该得到以下输出:

    图 1.23:'algorithm_type' 的输出

    图 1.23:'algorithm_type' 的输出

    从前面的输出中,我们可以看到我们将Clustering项添加到了列表中。

  11. 使用大括号{}创建一个空字典machine_learning

    machine_learning = {}
    
  12. machine_learning中创建一个新项,键为algorithm,值为algorithm列表中的所有项:

    machine_learning['algorithm'] = algorithm
    
  13. 使用print()函数显示machine_learning的内容。

    print(machine_learning)
    

    你应该得到以下输出:

    图 1.24:'machine_learning' 的输出

    图 1.24:'machine_learning' 的输出

    从前面的输出中,我们注意到已经从algorithm列表创建了一个字典。

  14. machine_learning中创建一个新项,键为learning,值为learning列表中的所有项:

    machine_learning['learning'] = learning
    
  15. 现在,在machine_learning中创建一个新项,键为algorithm_type,值为algorithm_type列表中的所有项:

    machine_learning['algorithm_type'] = algorithm_type
    
  16. 使用print()函数显示machine_learning的内容。

    print(machine_learning)
    

    你应该得到以下输出:

    图 1.25:'machine_learning' 的输出

    图 1.25:'machine_learning' 的输出

  17. 使用.remove()方法从algorithm键中移除a3c项:

    machine_learning['algorithm'].remove('a3c')
    
  18. 使用print()函数显示来自machine_learning字典中的algorithm项:

    print(machine_learning['algorithm'])
    

    你应该得到以下输出:

    图 1.26:来自'machine_learning'的'algorithm'输出

    图 1.26:来自'machine_learning'的'algorithm'输出

  19. 使用 .remove() 方法从 learning 键中移除 Reinforcement 项:

    machine_learning['learning'].remove('Reinforcement')
    
  20. 使用 .remove() 方法从 algorithm_type 键中移除 Game AI 项:

    machine_learning['algorithm_type'].remove('Game AI')
    
  21. 使用 print() 函数显示 machine_learning 的内容:

    print(machine_learning)
    

    你应该得到以下输出:

    图 1.27:'machine_learning' 的输出

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_27.jpg)

图 1.27:'machine_learning' 的输出

你已经成功创建了一个包含本书中将遇到的机器学习算法的字典。你学习了如何创建和操作 Python 列表和字典。

注意

要访问此特定部分的源代码,请参阅 packt.live/315EmRP

你也可以在线运行这个示例,网址是 packt.live/3ay1tYg

在下一节中,你将学习更多关于数据科学中常用的两个 Python 包:

  • pandas

  • scikit-learn

Python 数据科学

Python 为数据科学提供了大量的包。包是由作者(们)公开共享的一组预构建的函数和类。这些包扩展了 Python 的核心功能。Python 包索引(packt.live/37iTRXc)列出了 Python 中所有可用的包。

在本节中,我们将向您展示两种最流行的 Python 包:pandasscikit-learn

pandas 包

pandas 包提供了大量用于操作数据结构的 API。pandas 包中定义的两种主要数据结构是 DataFrameSeries

DataFrame 和 Series

DataFrame 是一种表格数据结构,表现为二维表格。它由行、列、索引和单元格组成。它非常类似于 Excel 表格或数据库中的表格:

图 1.28:DataFrame 的组成部分

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_28.jpg)

图 1.28:DataFrame 的组成部分

图 1.28 中,有三列:algorithmlearningtype。每列(也称为变量)包含特定类型的信息。例如,algorithm 变量列出了不同机器学习算法的名称。

一行存储与一条记录(也称为观察)相关的信息。例如,第 2 行(索引号 2)表示 RandomForest 记录,并且所有与该记录相关的属性都存储在不同的列中。

最后,一个单元格是给定行和列的值。例如,Clustering 是第 2 行和 type 列交叉处的单元格值。你可以将其看作指定行和列的交集。

所以,DataFrame 是一种结构化的数据表示形式,按行和列组织数据。一行代表一个观察,每一列包含该观察的属性值。这是数据科学中最常用的数据结构。

在 pandas 中,DataFrame 由 DataFrame 类表示。一个 pandas DataFrame 是由 pandas Series 组成的,而 pandas Series 是一维数组。一个 pandas Series 本质上是 DataFrame 中的一列。

数据通常分为两类:结构化数据和非结构化数据。把结构化数据看作数据库表格或 Excel 表格,其中每一列和每一行都有预定义的结构。例如,在列出公司所有员工的表格或电子表格中,每一条记录都会遵循相同的模式,比如第一列是出生日期,第二列和第三列是名字和姓氏,依此类推。

另一方面,非结构化数据并没有预定义的静态模式进行组织。文本和图像是非结构化数据的好例子。如果你读一本书并查看每个句子,你无法确定句子的第二个词总是动词或人名,它可以是任何东西,这取决于作者如何传达他们想要分享的信息。每个句子都有自己的结构,并且与上一个句子不同。同样,对于一组图像,你无法说像素 20 到 30 总是代表一个人的眼睛或一辆车的车轮:每张图像都不同。

数据可以来自不同的数据源:例如,可能是平面文件、数据存储或应用程序接口(API)馈送。在本书中,我们将处理如 CSV、Excel 电子表格或 JSON 这样的平面文件。所有这些文件类型都有各自的格式和结构来存储信息。

我们先来看一下 CSV 文件。

CSV 文件

CSV 文件使用逗号字符—,—来分隔列,并通过换行符来表示新的一行。前面提到的 DataFrame 示例在 CSV 文件中会像这样:

algorithm,learning,type
Linear Regression,Supervised,Regression
Logistic Regression,Supervised,Classification
RandomForest,Supervised,Regression or Classification
k-means,Unsupervised,Clustering

在 Python 中,使用某个包之前,你需要先导入该包。为此,你需要使用 import 命令。你还可以使用 as 关键字为每个导入的包创建别名。通常会将 pandas 包导入并使用别名 pd

import pandas as pd

pandas 提供了一个 .read_csv() 方法,可以轻松地将 CSV 文件直接加载到 DataFrame 中。你只需要提供 CSV 文件的路径或 URL,如下所示。

注意

请注意下面字符串中的斜杠。记住,反斜杠(\)用于将代码分割到多行,而正斜杠(/)是 URL 的一部分。

pd.read_csv('https://raw.githubusercontent.com/PacktWorkshops'\
            '/The-Data-Science-Workshop/master/Chapter01/'\
            'Dataset/csv_example.csv')

你应该得到以下输出:

图 1.29:加载 CSV 文件后的 DataFrame

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_29.jpg)

图 1.29:加载 CSV 文件后的 DataFrame

注意

在本书中,我们将加载存储在 Packt GitHub 仓库中的数据集:packt.live/2ucwsId

GitHub 将存储的数据包装成其特定格式。要加载数据集的原始版本,你需要通过点击Raw按钮并复制浏览器中提供的 URL 来加载其原始版本。

看看图 1.30

图 1.30:获取 GitHub 上原始数据集的 URL

图 1.30:获取 GitHub 上原始数据集的 URL

Excel 电子表格

Excel 是微软的一个工具,在业界非常流行。它有自己的内部结构,用于记录附加信息,比如每个单元格的数据类型,甚至是 Excel 公式。在pandas中,有一个特定的方法用于加载 Excel 电子表格,叫做.read_excel()

pd.read_excel('https://github.com/PacktWorkshops'\
              '/The-Data-Science-Workshop/blob/master'\
              '/Chapter01/Dataset/excel_example.xlsx?raw=true')

你应该得到以下输出:

图 1.31:加载 Excel 电子表格后的数据框

图 1.31:加载 Excel 电子表格后的数据框

JSON

JSON 是一种非常流行的文件格式,主要用于从 Web API 传输数据。其结构与 Python 字典中的键值对非常相似。我们之前使用的示例数据框在 JSON 格式中将是这样的:

{
  "algorithm":{
     "0":"Linear Regression",
     "1":"Logistic Regression",
     "2":"RandomForest",
     "3":"k-means"
  },
  "learning":{
     "0":"Supervised",
     "1":"Supervised",
     "2":"Supervised",
     "3":"Unsupervised"
  },
  "type":{
     "0":"Regression",
     "1":"Classification",
     "2":"Regression or Classification",
     "3":"Clustering"
  }
}

正如你可能猜到的,pandas也有一个方法用于读取 JSON 数据,叫做.read_json()

pd.read_json('https://raw.githubusercontent.com/PacktWorkshops'\
             '/The-Data-Science-Workshop/master/Chapter01'\
             '/Dataset/json_example.json')

你应该得到以下输出:

图 1.32:加载 JSON 数据后的数据框

图 1.32:加载 JSON 数据后的数据框

pandas还提供了更多方法来加载其他类型的文件。完整的列表可以在以下文档中找到:packt.live/2FiYB2O

pandas 不仅限于将数据加载到数据框中;它还提供了许多其他 API 来创建、分析或转换数据框。接下来的章节将介绍一些最有用的方法。

练习 1.02:将不同格式的数据加载到 pandas 数据框中

在这个练习中,我们将练习加载不同的数据格式,比如 CSV、TSV 和 XLSX 到 pandas 数据框中。我们将使用的数据集是“首次购房者补助金的前 10 个邮政编码”数据集(这是澳大利亚政府为帮助首次购房者提供的补助金)。该数据集列出了获得最多首次购房者补助金的 10 个邮政编码(也称为邮政区号)。

在这个数据集中,你将找到每个邮政编码的首次购房者补助金申请数量和相应的郊区。

注意

该数据集可以在我们的 GitHub 仓库中找到,链接:packt.live/2FgAT7d

另外,它可以在此公开访问:packt.live/2ZJBYhi

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本。

  2. 导入 pandas 包,如下所示的代码片段:

    import pandas as pd
    
  3. 创建一个新的变量,命名为csv_url,其中包含原始 CSV 文件的 URL:

    csv_url = 'https://raw.githubusercontent.com/PacktWorkshops'\
              '/The-Data-Science-Workshop/master/Chapter01'\
              '/Dataset/overall_topten_2012-2013.csv'
    
  4. 使用 pandas 的 .read_csv() 方法将 CSV 文件加载到数据框中。该 CSV 文件的第一行包含文件名,如果你直接打开文件,可以看到这一点。你需要通过使用 skiprows=1 参数来排除这一行。将结果保存到名为 csv_df 的变量中,并打印出来:

    csv_df = pd.read_csv(csv_url, skiprows=1)
    csv_df
    

    你应该看到以下输出:

    图 1.33:加载 CSV 文件后的数据框

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_33.jpg)

    图 1.33:加载 CSV 文件后的数据框

  5. 创建一个名为 tsv_url 的新变量,包含原始 TSV 文件的 URL:

    tsv_url = 'https://raw.githubusercontent.com/PacktWorkshops'\
              '/The-Data-Science-Workshop/master/Chapter01'\
              '/Dataset/overall_topten_2012-2013.tsv'
    

    注意

    TSV 文件类似于 CSV 文件,但它使用制表符(\t)作为分隔符,而不是使用逗号字符(,)。

  6. 使用 pandas 的 .read_csv() 方法将 TSV 文件加载到数据框中,并指定 skiprows=1sep='\t' 参数。将结果保存到名为 tsv_df 的变量中,并打印出来:

    tsv_df = pd.read_csv(tsv_url, skiprows=1, sep='\t')
    tsv_df
    

    你应该看到以下输出:

    图 1.34:加载 TSV 文件后的数据框

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_34.jpg)

    图 1.34:加载 TSV 文件后的数据框

  7. 创建一个名为 xlsx_url 的新变量,包含原始 Excel 表格的 URL:

    xlsx_url = 'https://github.com/PacktWorkshops'\
               '/The-Data-Science-Workshop/blob/master/'\
               'Chapter01/Dataset'\
               '/overall_topten_2012-2013.xlsx?raw=true'
    
  8. 使用 pandas 的 .read_excel() 方法将 Excel 表格加载到数据框中。将结果保存到名为 xlsx_df 的变量中,并打印出来:

    xlsx_df = pd.read_excel(xlsx_url)
    xlsx_df
    

    你应该看到以下输出:

    图 1.35:加载 Excel 表格后的数据框显示

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_35.jpg)

    图 1.35:加载 Excel 表格后的数据框显示

    默认情况下,.read_excel() 会加载 Excel 表格的第一个工作表。在这个示例中,我们需要的数据实际上存储在第二个工作表中。

  9. 使用 pandas 的 .read_excel() 方法将 Excel 表格加载到数据框中,并指定 skiprows=1sheet_name=1 参数。(请注意,sheet_name 参数是零索引的,因此 sheet_name=0 返回第一个工作表,而 sheet_name=1 返回第二个工作表。)将结果保存到名为 xlsx_df1 的变量中,并打印出来:

    xlsx_df1 = pd.read_excel(xlsx_url, skiprows=1, sheet_name=1)
    xlsx_df1
    

    你应该看到以下输出:

    图 1.36:加载 Excel 表格第二个工作表后的数据框

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_36.jpg)

图 1.36:加载 Excel 表格第二个工作表后的数据框

注意

要访问此特定部分的源代码,请参考 packt.live/2Yajzuq

你也可以在线运行此示例,访问 packt.live/2Q4dThe

在本练习中,我们学习了如何从不同的文件格式加载首次购房者补贴数据集的前十个邮政编码。

在接下来的部分中,我们将介绍 scikit-learn。

Scikit-Learn

Scikit-learn(也称为 sklearn)是数据科学家们使用的另一个非常流行的包。sklearn 的主要目的是提供用于处理数据和训练机器学习算法的 API。但在继续之前,我们需要了解什么是模型。

什么是模型?

一个机器学习模型从数据中学习模式,并创建一个数学函数来生成预测。一个监督学习算法将试图找到响应变量和给定特征之间的关系。

请看下面的示例。

数学函数可以表示为一个应用于一些输入变量 X(由多个特征组成)的函数ƒ(),并计算出一个输出(或预测)ŷ:

图 1.37:函数 f(X)

图 1.37:函数 f(X)

函数ƒ()可以非常复杂,具有不同数量的参数。如果我们以线性回归为例(将在第二章 回归中详细介绍),模型参数可以表示为 W=( w1, w2, ... , wn)。因此,我们之前看到的函数将变成如下:

图 1.38:线性回归的函数

图 1.38:线性回归的函数

一个机器学习算法将接收一些输入X及相关输出y的示例,它的目标是找到(w1,w2,...,wn)的值,以最小化其预测ŷ与真实输出y之间的差异。

前面的公式可能有些令人畏惧,但其实这非常简单。假设我们有一个仅包含一个目标变量 y 和一个特征 X 的数据集,比如下面这个数据集:

图 1.39:具有一个目标变量和一个特征的数据集示例

图 1.39:具有一个目标变量和一个特征的数据集示例

如果我们在这个数据集上拟合线性回归,算法将尝试为以下方程找到一个解:

图 1.40:线性回归拟合数据集的函数 f(x)

图 1.40:线性回归拟合数据集的函数 f(x)

所以,它只需要找到w0w1参数的值,使得模型尽可能地拟合数据。在这个例子中,算法可能得出wo = 0w1 = 10。因此,模型学习到的函数将如下所示:

图 1.41:使用估算值的函数 f(x)

图 1.41:使用估算值的函数 f(x)

我们可以在与数据相同的图表上可视化这一点:

图 1.42:在示例数据集上拟合的线性模型

图 1.42:在示例数据集上拟合的线性模型

我们可以看到,拟合的模型(橙色线条)与原始数据非常接近。因此,如果我们预测一个新的数据点的结果,它将非常接近真实值。例如,如果我们取一个接近 5 的点(假设它的值是x = 5.1y = 48),模型将预测如下:

图 1.43:模型预测

图 1.43:模型预测

这个值实际上非常接近真实值 48(红色圆圈)。因此,我们的模型预测是非常准确的。

就是这样。很简单,对吧?一般来说,一个数据集会有多个特征,但逻辑是一样的:训练好的模型将尝试为每个变量找到最佳参数,以使预测结果尽可能接近真实值。

我们刚刚看了一个线性模型的示例,但实际上还有其他类型的机器学习算法,比如基于树的算法或神经网络,它们可以从数据中发现更复杂的模式。

模型超参数

除了由算法自动学习的模型参数(现在你明白我们为什么称之为机器学习了),还有另一种参数叫做超参数。超参数不能被模型学习,它们是由数据科学家设置的,用于定义算法学习过程中的一些特定条件。这些超参数对于每种算法族都是不同的,它们可以例如帮助加速学习过程或限制过拟合的风险。在本书中,你将学习如何调整一些机器学习超参数。

sklearn API

如前所述,scikit-learn(或sklearn)包实现了大量的机器学习算法,如逻辑回归、k 最近邻、k 均值和随机森林。

注意

不必担心这些术语——你不需要立即了解这些算法的具体内容。在本章中,你将看到一个简单的随机森林示例,但所有这些算法将在本书后续章节中详细讲解。

sklearn按算法家族进行分组。例如,RandomForestGradientBoosting属于ensemble模块。为了使用某个算法,你需要先像这样导入它:

from sklearn.ensemble import RandomForestClassifier

sklearn之所以如此流行的另一个原因是,所有算法都遵循相同的 API 结构。因此,一旦你学会了如何训练一个算法,训练另一个算法就变得极其容易,几乎不需要改动代码。使用sklearn,训练机器学习模型有四个主要步骤:

  1. 使用指定的超参数实例化模型:这将配置你想要训练的机器学习模型。

  2. 使用训练数据训练模型:在这一步骤中,模型将学习最佳参数,以便使预测结果尽可能接近目标的实际值。

  3. 从输入数据预测结果:利用学习到的参数,模型将为新数据预测结果。

  4. 评估模型预测的表现:用于检查模型是否学习到正确的模式以获得准确的预测结果。

    注意

    在实际项目中,根据具体情况可能会有更多的步骤,但为了简便起见,我们暂时只讨论这四个步骤。你将在接下来的章节中学习其余的步骤。

如前所述,每个算法都会有自己特定的超参数,可以调整。要实例化模型,你只需要从之前导入的类创建一个新变量,并指定超参数的值。如果你不指定超参数的值,模型将使用sklearn指定的默认值。

建议至少设置random_state超参数,以便每次运行相同代码时获得可重复的结果:

rf_model = RandomForestClassifier(random_state=1)

第二步是使用一些数据来训练模型。在这个例子中,我们将使用一个简单的数据集,该数据集基于 13 个特征将 178 个意大利葡萄酒实例分类为 3 个类别。这个数据集是sklearn在其 API 中提供的几个示例之一。我们需要先加载数据:

from sklearn.datasets import load_wine
features, target = load_wine(return_X_y=True)

然后使用.fit()方法训练模型,你需要提供特征和目标变量作为输入:

rf_model.fit(features, target)

你应该得到以下输出:

图 1.44: 训练好的随机森林模型的日志

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_44.jpg)

图 1.44: 训练好的随机森林模型的日志

在前面的输出中,我们可以看到一个具有默认超参数的随机森林模型。你将在第四章使用随机森林进行多类别分类中了解到其中的一些内容。

训练完成后,我们可以使用.predict()方法来预测一个或多个观察结果的目标值。在这里,我们将使用与训练步骤相同的数据:

preds = rf_model.predict(features)
preds

你应该得到以下输出:

图 1.45: 训练好的随机森林模型的预测结果

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_45.jpg)

图 1.45: 训练好的随机森林模型的预测结果

从前面的输出中可以看到,数据集中 178 种不同的葡萄酒已经被分类为三种不同的葡萄酒类别。第一批葡萄酒被分类为类别 0,第二批是类别 1,最后一批是类别 2。此时,我们不知道类别 0、1 或 2 代表什么(在每个类别的“葡萄酒类型”上下文中),但找出这一点将构成更大数据科学项目的一部分。

最后,我们希望通过将模型的预测与目标变量的实际值进行比较,来评估模型的性能。评估模型性能可以使用很多不同的指标,你将在本书的后面了解更多内容。不过,目前我们将使用一个叫做准确率的指标。该指标计算正确预测的比例与观察总数的比值:

from sklearn.metrics import accuracy_score
accuracy_score(target, preds)

你应该得到以下输出

图 1.46: 训练好的随机森林模型的准确性

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_01_46.jpg)

图 1.46: 训练好的随机森林模型的准确性

在这个示例中,随机森林模型学会了正确预测该数据集中的所有观察结果;它达到了1的准确率(即 100%的预测与实际真实值匹配)。

就这么简单!这可能看起来太完美了。接下来的章节中,你将学习如何检查训练的模型是否能准确预测未见过或未来的数据点,或者它们是否仅仅学习了特定输入数据的模式(这也叫过拟合)。

练习 1.03:使用 sklearn 从数据集中预测乳腺癌

在这个练习中,我们将使用sklearn中的RandomForest构建一个机器学习分类器,预测患者的乳腺癌是恶性(有害)还是良性(无害)。

我们将使用的数据集是乳腺癌威斯康星(诊断)数据集,可以直接从sklearn包中获取,网址:packt.live/2FcOTim

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本。

  2. sklearn.datasets导入load_breast_cancer函数:

    from sklearn.datasets import load_breast_cancer
    
  3. 使用load_breast_cancer函数并设置return_X_y=True参数来加载数据集,仅返回特征和响应变量:

    features, target = load_breast_cancer(return_X_y=True)
    
  4. 打印features变量:

    print(features)
    

    你应该得到以下输出:

    图 1.47:特征变量的输出

    图 1.47:特征变量的输出

    上面的输出显示了特征的值。(你可以从之前提供的链接中了解更多关于特征的信息。)

  5. 打印target变量:

    print(target)
    

    你应该得到以下输出:

    图 1.48:目标变量的输出

    图 1.48:目标变量的输出

    上面的输出显示了目标变量的值。数据集中每个实例有两个类别,这两个类别是01,表示癌症是恶性还是良性。

  6. sklearn.ensemble导入RandomForestClassifier类:

    from sklearn.ensemble import RandomForestClassifier
    
  7. 创建一个名为seed的新变量,赋值为888(任意选择):

    seed = 888
    
  8. 使用random_state=seed参数实例化RandomForestClassifier,并将其保存到名为rf_model的变量中:

    rf_model = RandomForestClassifier(random_state=seed)
    
  9. 使用.fit()方法训练模型,并将featurestarget作为参数:

    rf_model.fit(features, target)
    

    你应该得到以下输出:

    图 1.49:随机森林分类器的日志

    图 1.49:随机森林分类器的日志

  10. 使用经过训练的模型,通过.predict()方法并将features作为参数进行预测,并将结果保存到名为preds的变量中:

    preds = rf_model.predict(features)
    
  11. 打印preds变量:

    print(preds)
    

    你应该得到以下输出:

    图 1.50:随机森林模型的预测结果

    图 1.50:随机森林模型的预测结果

    上面的输出显示了训练集的预测结果。你可以将其与实际的目标变量值(如图 1.48所示)进行比较。

  12. sklearn.metrics导入accuracy_score方法:

    from sklearn.metrics import accuracy_score
    
  13. 使用targetpreds作为参数计算accuracy_score()

    accuracy_score(target, preds)
    

    你应该得到以下输出:

    图 1.51:模型的准确度

图 1.51:模型的准确率

注意

若要访问此特定部分的源代码,请参考packt.live/3aBso5i

你也可以在线运行此示例,网址为packt.live/316OiKA

你刚刚使用sklearn API 训练了一个随机森林模型,并成功地将乳腺癌观察数据分类,取得了准确率为1的成绩。

活动 1.01:训练垃圾邮件检测算法

你正在为一家电子邮件服务提供商工作,任务是训练一个算法,该算法可以从给定的数据集中识别一封邮件是否为垃圾邮件,并检查其性能。

在这个数据集中,作者们已经根据相关关键词的某些统计数据创建了 57 个不同的特征,以便分类判断一封邮件是否是垃圾邮件。

注意

数据集最初由 Mark Hopkins、Erik Reeber、George Forman 和 Jaap Suermondt 共享:packt.live/35fdUUA

你可以从 Packt GitHub 下载,网址为packt.live/2MPmnrl

以下步骤将帮助你完成此活动:

  1. 导入所需的库。

  2. 使用.pd.read_csv()加载数据集。

  3. 使用.pop()pandas提取响应变量。此方法将从 DataFrame 中提取作为参数提供的列。然后你可以为其分配一个变量名,例如target = df.pop('class')

  4. 实例化RandomForestClassifier

  5. 训练一个随机森林模型来预测结果,使用.fit()

  6. 使用.predict()从输入数据中预测结果。

  7. 使用accuracy_score计算准确率。

    输出将类似于以下内容:

    图 1.52:垃圾邮件检测器的准确率

图 1.52:垃圾邮件检测器的准确率

注意

此活动的解决方案可以在以下地址找到:packt.live/2GbJloz

概述

本章为你提供了数据科学的总体概述。我们还学习了不同类型的机器学习算法,包括有监督学习和无监督学习,以及回归和分类。我们简要介绍了 Python,并讲解了如何操作本书中将使用的主要数据结构(列表和字典)。

接着,我们带你了解了 DataFrame 是什么,以及如何通过使用著名的 pandas 包从不同文件格式加载数据来创建 DataFrame。最后,我们学习了如何使用 sklearn 包来训练机器学习模型并进行预测。

这只是快速了解数据科学迷人世界的一瞥。在本书中,你将学习更多内容,并发现处理数据科学项目从头到尾的新技术。

下一章将向你展示如何在一个实际数据集上执行回归任务。

第二章:2. 回归

概述

本章是对线性回归分析的介绍,以及它在数据科学中的实际应用。您将学习如何使用 Python 这一多功能编程语言进行回归分析并检查结果。还将介绍使用对数函数转换变量之间固有的非线性关系,以便能够应用线性回归分析方法。

在本章结束时,您将能够识别和导入回归分析所需的 Python 模块;使用 pandas 模块加载数据集并为回归分析做准备;创建双变量数据的散点图并拟合回归线;使用 Python statsmodels 模块中可用的方法将回归模型拟合到数据集;解释简单和多元线性回归分析的结果;评估线性回归模型的拟合优度;并将线性回归分析作为解决实际问题的工具。

介绍

上一章介绍了 Python 编程的基础知识和数据科学领域的概述。数据科学是一个相对年轻的跨学科研究领域。它借鉴了传统的统计学、计算机科学和广泛的人工智能(AI)领域的概念和方法,尤其是人工智能的子领域——机器学习:

图 2.1:数据科学模型

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_02_01.jpg)

图 2.1:数据科学模型

如您在图 2.1中所见,数据科学旨在利用结构化非结构化数据,开发能够有效使用的模型,进行预测,并为决策提供见解。

对结构化数据的宽泛描述是任何能够方便地安排成由行和列组成的表格的数据。这种数据通常存储在数据库管理系统中。

然而,非结构化数据不能方便地以表格形式存储——这样的数据集的一个例子是文本文件。为了实现数据科学的目标,需要一种灵活的编程语言,能够有效地将交互性与计算能力和速度相结合。这正是 Python 编程语言满足数据科学需求的地方,正如第一章《Python 中的数据科学入门》中提到的,我们将在本书中使用 Python。

需要开发模型以进行预测和获得决策见解的需求横跨许多行业。因此,数据科学正在许多行业中得到应用,包括医疗保健、制造业、一般过程行业、银行和金融部门、市场营销和电子商务、政府以及教育。

在本章中,我们将特别讨论回归分析,它是数据科学中常用的关键方法之一,用于建模变量之间的关系,其中目标变量(即你要寻找的值)是一个实数。

假设一个房地产公司希望了解并且如果可能的话,建模城市中房产价格与房产关键属性之间的关系。这是一个数据科学问题,可以通过回归来解决。

这是因为感兴趣的目标变量,即房产价格,是一个实数。可以用来预测房产价值的关键属性示例如下:

  • 房产的年龄

  • 房产的卧室数量

  • 房产是否有游泳池

  • 房产所占土地的面积

  • 房产距离铁路站和学校等设施的距离

回归分析可以用于研究这种情况,你需要创建一个函数,将房产的关键属性映射到目标变量,在这个例子中,目标变量是房产的价格。

回归分析是属于机器学习技术中的一个类别,叫做监督机器学习。之所以叫做监督,是因为学习模型的机器学习算法会提供一个类似于问题答案的数据集供其学习。这里的问题是关键属性,而答案则是用于研究的每个房产的价格,如下图所示:

图 2.2:监督学习技术的示例

图 2.2:监督学习技术的示例

一旦算法学习了模型,我们就可以向模型提供一个问题(即一组房产属性,我们希望找出其价格),让它告诉我们该房产的价格(即答案)是多少。

本章介绍了线性回归及其如何应用于解决数据科学中像前面描述的实际问题。Python 提供了丰富的模块(库),可以用来进行各种类型的严谨回归分析。在本章中,我们将使用以下 Python 模块,当然还有其他模块:pandasstatsmodelsseabornmatplotlibscikit-learn

简单线性回归

图 2.3中,你可以看到波士顿市的人均犯罪率和业主自住房屋的中位数价值,波士顿是马萨诸塞州最大的城市。我们希望通过回归分析来了解影响该市犯罪率的因素。

这样的分析对政策制定者和社会整体非常有用,因为它有助于决策,推动减少犯罪率的措施,并有望在整个社区消除犯罪。这可以使社区更加安全,提高社会的生活质量。

这是一个数据科学问题,属于有监督机器学习类型。存在一个因变量犯罪率(我们将其表示为Y),我们试图理解其变化,并将其与一个自变量业主自住房屋中位数价值(我们将其表示为X)联系起来。

换句话说,我们试图根据不同的邻里情况,理解犯罪率的变化。

回归分析是寻找一个函数,在给定假设条件下,最好地描述因变量(在本例中为Y)与自变量(在本例中为X)之间的关系。

当自变量只有一个,且假设因变量与自变量之间的关系为直线时,如图 2.3所示,这种回归分析称为简单线性回归。直线关系被称为回归线或最佳拟合线:

图 2.3:犯罪率与业主自住房屋中位数价值的散点图业主自住房屋

图 2.3:犯罪率与业主自住房屋中位数价值的散点图

图 2.3中,回归线以实心黑色线条表示。忽略回归线与图中数据拟合的较差质量,我们可以看到随着业主自住房屋中位数价值的增加,每人犯罪率有所下降。

从数据科学的角度来看,这一观察可能引发许多问题。例如,是什么原因导致每人犯罪率随着业主自住房屋中位数价值的增加而下降?较富裕的郊区和城镇是否比经济条件较差的郊区和城镇获得更多的警力资源?不幸的是,这些问题无法通过像图 2.3中的简单图表来解答。但观察到的趋势可能作为讨论的起点,用于审视警力和社区安全资源的分配情况。

回到回归线拟合数据的效果问题,显然回归线的近三分之一处没有任何数据点分布在其周围。许多数据点仅仅聚集在横轴上,接近零(0)犯罪率的位置。这并不是你所期待的一个良好的回归线应该有的效果。一个良好的回归线应该能够很好地拟合数据,且应当位于一片数据点云之中。

看起来每人犯罪率与业主自住房屋中位数价值之间的关系,并不像你最初想的那样呈线性关系。

在本章中,我们将学习如何使用对数函数(一种用于转换数值的数学函数)将人均犯罪率与自有住房中位数之间的关系线性化,从而改善回归线与散点图数据点的拟合度。

到目前为止,我们忽略了一个非常重要的问题。也就是,如何为给定的数据集确定回归线?

确定回归线的常用方法叫做最小二乘法,接下来将介绍该方法。

最小二乘法

简单线性回归线通常呈现出图 2.4所示的形式,其中 β0 和 β1 是未知常数,分别表示回归线的截距和斜率。

截距是当自变量(X)为零时因变量(Y)的值。斜率是自变量(X)变化一个单位时因变量(Y)变化的速率。未知常数称为 )在图 2.4中。该模型被称为概率模型,因为它没有模拟因变量(Y)中的所有变异性:

图 2.4:简单线性回归方程

图 2.4:简单线性回归方程

计算实际因变量值与预测因变量值之间的差异,得到的误差通常称为残差(ϵi)。

对样本中的每个数据点重复此计算,每个数据点的残差(ϵi)可以被平方,以消除代数符号,并加在一起以得到误差平方和(ESS)。

最小二乘法旨在最小化误差平方和(ESS)。

多元线性回归

在前面讨论的简单线性回归中,我们只有一个自变量。如果我们在分析中包含多个自变量,就得到一个多元线性回归模型。多元线性回归的表示方式类似于简单线性回归。

假设我们要拟合一个包含三个自变量 X1、X2 和 X3 的线性回归模型。多元线性回归方程的公式将如下所示,如图 2.5所示:

图 2.5:多元线性回归方程

图 2.5:多元线性回归方程

每个自变量将有自己的系数或参数(即 β1、β2 或 β3)。β 系数告诉我们,如果其他自变量保持不变,某个自变量的变化如何影响因变量。

回归系数估计(β0、β1、β2 和 β3)

图 2.5中的回归系数是使用与介绍简单线性回归时讨论的最小二乘法相同的方法估计的。为了满足最小二乘法的要求,所选的系数必须最小化平方残差的和。

在本章稍后,我们将利用 Python 编程语言实际计算这些系数估计值。

变量的对数变换

如前所述,依赖变量与自变量之间的关系有时并不是线性的。这限制了线性回归的使用。为了解决这个问题,根据关系的性质,可以使用对数函数来变换感兴趣的变量。这样变换后的变量通常与其他未变换的变量呈线性关系,从而使得可以使用线性回归来拟合数据。本书后续的练习中将通过实践展示这一点。

相关矩阵

图 2.3中,我们看到如何通过直线图分析两个变量之间的线性关系。另一种可视化变量之间线性关系的方法是使用相关矩阵。相关矩阵是一种数字交叉表,显示了变量对之间的相关性,也就是两个变量之间的关联程度(可以理解为一个变量的变化如何导致另一个变量的变化)。分析表格中的原始数据并不容易。因此,相关矩阵可以转换为“热图”的形式,便于通过不同的颜色观察变量之间的相关性。练习 2.01中展示了这一点,加载并准备数据以供分析

使用 Python 进行回归分析

在讨论了回归分析的基础知识后,现在是时候动手实践,使用 Python 进行实际的回归分析了。

为了开始我们的分析,我们需要在 Python 中启动一个会话,并加载所需的相关模块和数据集。

本章中我们进行的所有回归分析都将基于波士顿住房数据集。该数据集适合教学,且适用于线性回归分析。它呈现了一个足够的挑战性,需要使用对数函数来变换变量,以便更好地拟合数据。该数据集包含了波士顿地区一组房产的信息,可以用来确定特定房产的不同属性如何影响其价值。

波士顿住房数据集 CSV 文件的列标题可以解释如下:

  • CRIM – 每个城镇的人均犯罪率。

  • ZN – 划定为超过 25,000 平方英尺的住宅用地比例。

  • INDUS – 每个城镇的非零售商业用地比例。

  • CHAS – 查尔斯河虚拟变量(= 1 如果区域边界接壤河流;否则为 0)

  • NOX – 一氧化氮浓度(单位:每千万分之一)

  • RM – 每个住宅的平均房间数

  • AGE – 1940 年之前建造的自有住房单元比例

  • DIS – 到五个波士顿就业中心的加权距离

  • RAD – 通向辐射公路的可达性指数

  • TAX – 每 $10,000 的全额财产税税率

  • PTRATIO – 按城镇划分的师生比

  • LSTAT – 低收入人群所占百分比

  • MEDV – 自有住房的中位数价格(单位:千美元)

我们使用的数据集是原始数据集的轻微修改版本,来自于 packt.live/39IN8Y6

练习 2.01:加载并准备数据进行分析

在本练习中,我们将学习如何将 Python 模块和我们需要的分析数据集加载到 Python 会话中,并准备数据进行分析。

注意

我们将在本章中使用波士顿房价数据集,您可以在我们的 GitHub 仓库找到它,链接:packt.live/2QCCbQB

以下步骤将帮助您完成此练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 通过将以下代码片段输入到一个 Colab 笔记本单元中,加载所需的 Python 模块。按 matplotlib 显示代码在笔记本环境中的图形输出。接下来的代码行使用 import 关键字将各种 Python 模块加载到我们的编程环境中。这些模块包括 patsy,它是一个 Python 模块。为了方便引用,某些模块被赋予别名,例如 seaborn 模块的别名是 sns。因此,当我们在后续代码中引用 seaborn 时,使用别名 snspatsy 模块没有使用别名,因此在代码中需要时,我们使用 patsy 的全名。

    plot_corrtrain_test_split 函数分别从 statsmodels.graphics.correlationsklearn.model_selection 模块导入。最后的语句用于设置 matplotlib 生成的图表的美学外观,使其与 seaborn 模块显示的类型一致。

  3. 接下来,加载 Boston.CSV 文件,并通过运行以下代码将变量名 rawBostonData 分配给它:

    rawBostonData = pd.read_csv\
                    ('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-'\
                     'Workshop/master/Chapter02/'\
                     'Dataset/Boston.csv')
    
  4. 检查 DataFrame 中的前五条记录:

    rawBostonData.head()
    

    您应该得到以下输出:

    图 2.6:数据集的前五行

    图 2.6:数据集的前五行

  5. 检查 DataFrame 中是否存在缺失值(null 值),然后删除这些值以获得干净的数据集。使用 pandas 方法 dropna() 来查找并移除这些缺失值:

    rawBostonData = rawBostonData.dropna()
    
  6. 检查 DataFrame 中是否存在重复记录,然后删除它们以获得干净的数据集。使用 pandas 中的 drop_duplicates() 方法:

    rawBostonData = rawBostonData.drop_duplicates()
    
  7. 列出 DataFrame 的列名,以便您检查数据集中的字段,并在必要时修改为有意义的名称:

    list(rawBostonData.columns)
    

    您应该得到以下输出:

    图 2.7:列出所有列名

    图 2.7:列出所有列名

  8. 重命名数据框的列,使其具有意义。请注意精确匹配列名,甚至在名称字符串中留空格也会导致错误。例如,字符串ZN前后有空格,它与ZN不同。重命名后,打印新的数据框头部,如下所示:

    renamedBostonData = rawBostonData.rename\
                        (columns = {\
                         'CRIM':'crimeRatePerCapita',\
                         ' ZN ':'landOver25K_sqft',\
                         'INDUS ':'non-retailLandProptn',\
                         'CHAS':'riverDummy',\
                         'NOX':'nitrixOxide_pp10m',\
                         'RM':'AvgNo.RoomsPerDwelling',\
                         'AGE':'ProptnOwnerOccupied',\
                         'DIS':'weightedDist',\
                         'RAD':'radialHighwaysAccess',\
                         'TAX':'propTaxRate_per10K',\
                         'PTRATIO':'pupilTeacherRatio',\
                         'LSTAT':'pctLowerStatus',\
                         'MEDV':'medianValue_Ks'})
    renamedBostonData.head()
    

    你应该得到以下输出:

    图 2.8:数据框被重命名

    图 2.8:数据框被重命名

    注意

    上述输出已被截断。请前往 GitHub 仓库查看完整的输出。

  9. 使用.info()函数检查数据框中各列的数据类型:

    renamedBostonData.info()
    

    你应该得到以下输出:

    图 2.9:数据集中的不同数据类型

    图 2.9:数据集中的不同数据类型

    输出显示数据集共有506行(Int64Index: 506 entries)。共有13列(Data columns)。13列中没有任何一列有缺失值(所有506行都是非空)。10 列具有浮动(小数)类型数据,3 列具有整数类型数据。

  10. 现在,计算数据框中数值列的基本统计数据:

    renamedBostonData.describe(include=[np.number]).T
    

    我们使用了 pandas 函数describe,该函数用于计算数据框中数值字段的简单统计数据(包括任何具有numpy数值数据类型的字段)。统计数据包括最小值、最大值、每列的行数、每列的平均值(均值)、第 25 百分位数、第 50 百分位数和第 75 百分位数。我们通过转置(使用.T函数)describe函数的输出,以获得更好的布局。

    你应该得到以下输出:

    图 2.10:数值列的基本统计数据

    图 2.10:数值列的基本统计数据

  11. 将数据框划分为训练集和测试集,如下代码片段所示:

    X = renamedBostonData.drop('crimeRatePerCapita', axis = 1)
    y = renamedBostonData[['crimeRatePerCapita']]
    seed = 10 
    test_data_size = 0.3 
    X_train, X_test, \
    y_train, y_test = train_test_split(X, y, \
                                       test_size = test_data_size, \
                                       random_state = seed)
    train_data = pd.concat([X_train, y_train], axis = 1)
    test_data = pd.concat([X_test, y_test], axis = 1)
    

    我们选择了 30%的测试数据大小,即0.3。使用train_test_split函数实现这一点。我们设置了随机数生成器的种子,以便每次运行此代码时都能获得可重复的划分。这里使用了一个任意值10。在构建模型时,将用于开发模型的数据集至少划分为两部分是一种良好的做法。一部分用于开发模型,称为训练集(X_trainy_train合并)。

    注意

    将数据分割为训练和测试子集允许您使用部分数据来训练模型(即,它允许您构建一个学习变量之间关系的模型),其余数据用于测试模型(即,查看新数据给出时您的新模型能够做出多好的预测)。您将在本书中始终使用训练-测试拆分,并且这个概念将在第七章,机器学习模型的泛化中详细解释。

  12. 计算并绘制 train_data 集的相关矩阵:

    corrMatrix = train_data.corr(method = 'pearson')
    xnames=list(train_data.columns)
    ynames=list(train_data.columns)
    plot_corr(corrMatrix, xnames=xnames, ynames=ynames,\
              title=None, normcolor=False, cmap='RdYlBu_r')
    

    在前述代码片段中,第 4 行 使用反斜杠字符\ 的目的是在 Python 中强制将代码延续到新的行。如果您在笔记本中将整行代码输入到单行中,则不需要\字符。

    您应该得到以下输出:

    图 2.11:预期热力图输出

图 2.11:预期热力图输出

在上述热力图中,我们可以看到具有橙色或红色方块的变量之间存在强正相关(一个变量的增加导致另一个变量的增加)。具有蓝色方块的变量之间存在强负相关(一个变量的增加导致另一个变量的减少)。具有浅色方块的变量之间几乎没有相关性。例如,nitrixOxide_pp10mnon-retailLandProptn 之间似乎存在相对较强的相关性,但 riverDummy 和任何其他变量之间的相关性较低。

注意

要访问本节的源代码,请参阅 packt.live/320HLAH

您也可以在 packt.live/34baJAA 上在线运行此示例。

我们可以利用相关矩阵的发现作为进一步回归分析的起点。热力图为我们提供了数据关系的良好概述,并能显示我们在调查中应关注的变量。

相关系数

在前一练习中,我们已经看到如何使用相关矩阵热力图来可视化变量对之间的关系。我们也可以用数值形式看到这些相同的关系,使用原始的相关系数数字。这些数值介于-1 和 1 之间,表示两个变量之间的密切程度。

Pandas 提供了一个 corr 函数,当在 DataFrame 上调用时提供所有数值数据类型的相关性矩阵(表)。在我们的情况下,在 Colab 笔记本中运行代码 train_data.corr(method='pearson') 提供了 图 2.12 的结果。

注意

Pearson 是衡量变量之间关系的标准相关系数。

值得注意的是,图 2.12 沿左对角线是对称的。左对角线的值是特征与自身之间的相关系数(因此它们的值都是 1),所以这些值与我们的分析无关。图 2.12 中的数据即为在 练习 2.01加载和准备数据进行分析步骤 12 输出中的绘图展示。

您应获得以下输出:

图 2.12:训练数据集的相关矩阵

图 2.12:训练数据集的相关矩阵

注意

上述输出已被截断。

数据科学家使用相关系数作为统计量,以衡量两个数值变量 X 和 Y 之间的线性关系。双变量数据样本的相关系数通常用 r 表示。在统计学中,衡量两个数值变量之间相关性的常见方法是使用 Pearson 相关系数。因此,本章中任何提及相关系数的地方,均指代 Pearson 相关系数。

为了实际计算本课程中数据集变量的相关系数统计量,我们使用 Python 函数。对本讨论来说,重要的是我们计算的相关系数所取值的含义。相关系数 (r) 的值范围在 +1 和 -1 之间。

当 r 等于 +1 时,X 和 Y 之间的关系是,X 和 Y 会完美地朝同一方向同时增加或减少。当 r 等于 -1 时,X 和 Y 之间的关系是,X 的增加与 Y 的减少完全相对应,反之亦然。当 r 等于零(0)时,X 和 Y 之间没有线性关系。

X 和 Y 之间没有线性关系并不意味着 X 和 Y 没有关系;相反,这意味着如果存在任何关系,它不能用直线来描述。在实践中,相关系数值在 0.6 或更高(或 -0.6 或更低)时,通常是 X 和 Y 之间潜在的线性关系的标志。

练习 2.01 的最后一列输出,加载和准备数据进行分析步骤 12,提供了犯罪率与其他特征之间的 r 值,并通过颜色深浅展示。通过颜色条可以明显看出,radialHighwaysAccesspropTaxRate_per10KnitrixOxide_pp10mpctLowerStatus 与犯罪率之间具有最强的相关性。这表明犯罪率与这些自变量之间可能存在一种线性关系,值得进一步研究。

练习 2.02:使用 Python 进行线性关系的图形化探究

配有回归线的散点图是数据科学家可以用来快速可视化因变量与自变量之间可能相关性的方式。

这次练习的目标是使用该技术来调查波士顿市区内每个镇的犯罪率与自有住房的中位数价值之间可能存在的线性关系。

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本文件,并执行到包括步骤 11在内的 练习 2.01加载和准备数据以进行分析。如下代码块所示,从导入语句开始:

    %matplotlib inline
    import matplotlib as mpl
    import seaborn as sns
    import matplotlib.pyplot as plt
    import statsmodels.formula.api as smf
    import statsmodels.graphics.api as smg
    import pandas as pd
    import numpy as np
    import patsy
    from statsmodels.graphics.correlation import plot_corr
    from sklearn.model_selection import train_test_split
    plt.style.use('seaborn')
    

    加载和预处理数据:

    rawBostonData = pd.read_csv\
                    ('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-'\
                     'Workshop/master/Chapter02/'
                     'Dataset/Boston.csv')
    rawBostonData = rawBostonData.dropna()
    rawBostonData = rawBostonData.drop_duplicates()
    renamedBostonData = rawBostonData.rename\
                        (columns = {\
                         'CRIM':'crimeRatePerCapita',\
                         ' ZN ':'landOver25K_sqft',\
                         'INDUS ':'non-retailLandProptn',\
                         'CHAS':'riverDummy',\
                         'NOX':'nitrixOxide_pp10m',\
                         'RM':'AvgNo.RoomsPerDwelling',\
                         'AGE':'ProptnOwnerOccupied',\
                         'DIS':'weightedDist',\
                         'RAD':'radialHighwaysAccess',\
                         'TAX':'propTaxRate_per10K',\
                         'PTRATIO':'pupilTeacherRatio',\
                         'LSTAT':'pctLowerStatus',\
                         'MEDV':'medianValue_Ks'})
    

    设置测试数据和训练数据:

    X = renamedBostonData.drop('crimeRatePerCapita', axis = 1)
    y = renamedBostonData[['crimeRatePerCapita']]
    seed = 10 
    test_data_size = 0.3 
    X_train, X_test, y_train, y_test = train_test_split\
                                       (X, y, \
                                        test_size = test_data_size,\
                                        random_state = seed)
    train_data = pd.concat([X_train, y_train], axis = 1)
    test_data = pd.concat([X_test, y_test], axis = 1)
    
  2. 现在使用 matplotlib 中的 subplots 函数来定义画布(在以下代码中赋值给变量 fig)和图形对象(在以下代码中赋值给变量 ax)。你可以通过设置 figsize(宽度 = 10,高度 = 6)参数来设置图形的大小:

    fig, ax = plt.subplots(figsize=(10, 6))
    

    现在不要执行代码。

  3. 使用 seaborn 函数 regplot 创建散点图。现在不要执行该代码单元,我们将在下一步中添加更多代码来对图形进行样式设置:

    sns.regplot(x='medianValue_Ks', y='crimeRatePerCapita', \
                ci=None, data=train_data, ax=ax, color='k', \
                scatter_kws={"s": 20,"color": "royalblue", \
                "alpha":1})
    

    该函数接受独立变量(x)、依赖变量(y)、回归参数的置信区间(ci,其值在 0 到 100 之间)、包含 xy 的 DataFrame(data)、matplotlib 图形对象(ax)以及其他控制图形点的美学参数。(在此案例中,置信区间设置为 None——我们将在本章稍后讨论更多关于置信区间的内容。)

  4. 在与步骤 3 相同的单元格中,设置 xy 标签,fontsizename 标签,xy 的范围,以及 matplotlib 图形对象(ax)的 tick 参数。同时,将画布的布局设置为 tight

    ax.set_ylabel('Crime rate per Capita', fontsize=15, \
                   fontname='DejaVu Sans')
    ax.set_xlabel("Median value of owner-occupied homes "\
                  "in $1000's", fontsize=15, \
                  fontname='DejaVu Sans')
    ax.set_xlim(left=None, right=None)
    ax.set_ylim(bottom=None, top=30)
    ax.tick_params(axis='both', which='major', labelsize=12)
    fig.tight_layout()
    

    现在执行该单元格,你应该得到以下输出:

图 2.13:使用 Python 的回归线散点图

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_02_13.jpg)

图 2.13:使用 Python 的回归线散点图

如果正确执行了练习,输出结果必须与图 2.3中的图形相同。在图 2.3中,展示了这个输出,并用来引入线性回归,但并没有展示它是如何创建的。这次练习教会了我们如何使用 Python 创建散点图并拟合回归线。

注意

要访问此特定部分的源代码,请参考packt.live/34cITnk

你还可以在packt.live/2CA4NFG上在线运行此示例。

练习 2.03:使用 Python 检查可能的对数-线性关系

在本次练习中,我们将使用对数函数来转换变量,并研究这是否有助于使回归线更好地拟合数据。我们还将查看如何通过在图表上包含回归系数的 95% 置信区间来使用置信区间。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本文件,并执行直到步骤 11的所有步骤,来自练习 2.01加载和准备数据进行分析

  2. 使用matplotlib中的subplots函数在 Python 中定义一个画布和图形对象:

    fig, ax = plt.subplots(figsize=(10, 6))
    

    现在不要执行此代码。

  3. 在同一代码单元格中,使用numpy中的对数函数(np.log)转换因变量(y)。这本质上创建了一个新变量log(y)

    y = np.log(train_data['crimeRatePerCapita'])
    

    现在不要执行此代码。

  4. 使用 seaborn 的regplot函数创建散点图。将regplot函数的置信区间参数(ci)设置为95%。这将计算回归系数的95%置信区间,并将其作为回归线上的阴影区域绘制在图中。

    sns.regplot(x='medianValue_Ks', y=y, ci=95, \
                data=train_data, ax=ax, color='k', \
                scatter_kws={"s": 20,"color": "royalblue", \
                "alpha":1})
    
  5. 在同一单元格中,设置xy标签、fontsizename标签、xy的坐标范围,以及matplotlib图形对象(ax)的tick参数。同时,将画布的布局设置为tight

    ax.set_ylabel('log of Crime rate per Capita', \
                  fontsize=15, fontname='DejaVu Sans')
    ax.set_xlabel("Median value of owner-occupied homes "\
                  "in $1000's", fontsize=15, \
                  fontname='DejaVu Sans')
    ax.set_xlim(left=None, right=None)
    ax.set_ylim(bottom=None, top=None)
    ax.tick_params(axis='both', which='major', labelsize=12)
    fig.tight_layout()
    

    现在执行此单元格。输出如下:

    图 2.14:改进的线性回归线的期望散点图

图 2.14:改进的线性回归线的期望散点图

通过完成这个练习,我们成功地改善了我们的散点图。此活动中创建的回归线比练习 2.02中的回归线更好地拟合了数据,使用 Python 进行线性关系的图形分析。通过比较这两张图,可以看到在对数图中,回归线更清楚地与数据点的分布匹配。我们解决了回归线下三分之一部分没有点聚集的问题。这是通过使用对数函数转换因变量实现的。经过转换的因变量(人均犯罪率的对数)与自有住房的中位数相比,具有比未转换变量更好的线性关系。

注意

要访问此特定部分的源代码,请参考packt.live/3ay4aZS

您还可以在packt.live/324nN7R上在线运行此示例。

Statsmodels 公式 API

图 2.3中,实线表示人均犯罪率与自有住房的中位数之间的关系。那么,我们如何获得描述这条线的方程呢?换句话说,我们如何找到直线关系的截距和斜率?

Python 提供了丰富的应用程序编程接口 (API),使这一过程变得容易。Statsmodels 公式 API 使数据科学家能够使用公式语言定义回归模型,这些模型可以在统计学文献中找到,并且在许多专门的统计计算软件包中都有应用。

练习 2.04:使用 Statsmodels 公式 API 拟合简单线性回归模型

在本练习中,我们检查一个简单的线性回归模型,其中犯罪率人均是因变量,而业主自住住房的中位数是自变量。我们使用 statsmodels 公式 API 来创建一个线性回归模型,以便在 Python 中进行分析。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本文件并导入所需的包。

    import pandas as pd
    import statsmodels.formula.api as smf
    from sklearn.model_selection import train_test_split
    
  2. 执行 第 2 步第 11 步,内容来自 练习 2.01加载并准备分析数据

  3. 定义一个线性回归模型,并将其赋值给名为 linearModel 的变量:

    linearModel = smf.ols\
                  (formula='crimeRatePerCapita ~ medianValue_Ks',\
                   data=train_data)
    

    如你所见,我们调用了 statsmodels API 的ols函数,并通过定义一个 patsy 公式字符串来设置其公式参数,该字符串使用波浪号(~)符号将因变量与自变量关联。通过将 ols 函数的数据参数指向包含变量的 DataFrame(train_data),告诉函数在哪里查找这些在字符串中命名的变量。

  4. 调用模型实例的 .fit 方法,并将该方法的结果赋值给名为 linearModelResult 的变量,如下所示的代码片段:

    linearModelResult = linearModel.fit()
    
  5. 通过运行以下代码,打印存储在 linearModelResult 变量中的结果摘要:

    print(linearModelResult.summary())
    

    你应该得到以下输出:

    图 2.15:简单线性回归分析结果摘要

图 2.15:简单线性回归分析结果摘要

如果练习正确执行,则已经使用 statsmodels 公式 API 创建了一个模型。调用了模型对象的 fit 方法(.fit())来将线性回归模型拟合到数据中。这里的拟合意味着使用普通最小二乘法来估计回归系数(参数)。

注意

要访问此特定部分的源代码,请参考 packt.live/3iJXXge

你还可以在线运行这个例子,访问 packt.live/2YbZ54v

分析模型摘要

.fit 方法提供了许多函数来探索其输出。这些函数包括 conf_int()pvaluestvaluessummary() 参数。通过这些函数,可以从结果中获取模型参数、置信区间以及分析的 p 值和 t 值。(p 值和 t 值的概念将在本章稍后解释。)

语法非常简单,方法是紧随包含结果的变量名后使用点符号表示相关函数名称——例如,linearModelResult.conf_int() 将输出置信区间值。最方便的函数是 summary(),它会展示分析的所有相关结果表格。

图 2.15 中,展示了在 练习 2.04,使用 Statsmodels 公式 API 拟合简单线性回归模型 中使用的 summary 函数的输出。summary 函数的输出被用双重虚线分为三个主要部分。

第九章解释机器学习模型 中,将详细讨论这三部分的结果。然而,在此有几点需要特别说明。

图 2.15第一部分 左上角,我们可以看到模型中的因变量(Dep. Variable)以及 crimeRatePerCapita,这是 练习 2.04,使用 Statsmodels 公式 API 拟合简单线性回归模型 的值。模型的 R-squared 统计量为 0.144,该值出现在 第一部分 中。R-squared 值由 Python 计算得出,表示为一个小数(0.144),但通常以百分比形式报告,因此我们模型的值为 14.4%。R-squared 统计量提供了一个衡量模型能解释因变量(crimeRatePerCapita)变异性的指标。它可以解读为模型与数据集拟合程度的度量。在 图 2.15第二部分 中,报告了模型中的截距和自变量。模型中的自变量是房主自住住房的中位数价值(medianValue_Ks)。

在同一 第二部分 中,紧挨着截距和自变量,有一列报告了模型系数(coef)。截距和自变量的系数在 Python 打印的摘要报告中列于 coef 栏下。截距的值为 11.2094,而自变量的系数为负 0.3502-0.3502)。如果我们选择将模型中的因变量(crimeRatePerCapita)表示为 y,自变量(房主自住住房的中位数价值)表示为 x,我们就有了写出定义我们模型的方程所需的所有元素。

因此,y ≈ 11.2094 - 0.3502 x,是我们模型的方程。在 第九章解释机器学习模型 中,将全面讨论该模型的意义及其应用方式。

模型公式语言

Python 是一种非常强大的编程语言,深受许多开发者喜爱。自从 statsmodels 发布了 0.5.0 版本后,Python 现在为统计分析和建模提供了一个非常有竞争力的选项,堪比 R 和 SAS。

这包括通常所说的 R 风格公式语言,通过这种语言可以轻松定义统计模型。Statsmodels 通过内部使用 Patsy Python 库实现 R 风格的公式语言,将公式和数据转换为用于模型拟合的矩阵。

图 2.16 总结了用于构建 Patsy 公式字符串的操作符及其含义:

图 2.16:Patsy 公式语法及示例总结

图 2.16:Patsy 公式语法和示例摘要

截距处理

在 patsy 公式字符串中,string 1用于定义模型的截距。因为截距在大多数情况下是必需的,所以string 1会自动包含在每个公式字符串定义中。您无需在字符串中显式包含它来指定截距,它是隐式存在的。然而,如果您想从模型中删除截距,可以从公式字符串中减去一个(-1),这将定义一个通过原点的模型。为了与其他统计软件兼容,Patsy还允许使用字符串零(0)和负一(-1)来排除模型中的截距。这意味着,如果在公式字符串的右侧包含 0 或-1,您的模型将没有截距。

活动 2.01:使用 Statsmodels 公式 API 拟合对数线性模型

您已经了解了如何使用 statsmodels API 来拟合线性回归模型。在本活动中,您需要拟合一个对数线性模型。您的模型应该表示对数转换后的因变量(每人犯罪率的对数)与业主自住房屋的中位数值之间的关系。

完成此活动的步骤如下:

  1. 定义一个线性回归模型并将其赋值给一个变量。记得在公式字符串中使用log函数转换因变量。

  2. 调用对数线性模型实例的fit方法,并将该方法的结果赋值给一个变量。

  3. 打印结果摘要并分析输出。

您的输出应如下图所示:

图 2.17:按每人犯罪率对房屋中位数值的对数线性回归业主自住房屋

图 2.17:按业主自住房屋中位数值对每人犯罪率的对数线性回归

注意

本活动的解决方案可以在这里找到:packt.live/2GbJloz

多元回归分析

到目前为止,在这些练习和活动中,我们只使用了一个自变量进行回归分析。在实践中,正如我们在波士顿住房数据集中看到的那样,分析中感兴趣的过程和现象很少仅受单一特征的影响。为了能够更准确地建模变异性,因此,有必要研究所有可能对解释因变量的变异性具有显著贡献的自变量。因此,多元回归分析是实现这一目标的方法。

练习 2.05:使用 Statsmodels 公式 API 拟合多元线性回归模型

在本练习中,我们将使用加法运算符(+)在patsy公式字符串中定义一个包含多个自变量的线性回归模型。

要完成此活动,请在您的 Colab 笔记本中运行以下步骤中的代码:

  1. 打开一个新的 Colab 笔记本文件并导入所需的包。

    import statsmodels.formula.api as smf
    import pandas as pd
    from sklearn.model_selection import train_test_split
    
  2. 执行步骤 2步骤 11,来自练习 2.01加载和准备数据进行分析

  3. 使用 Patsy 公式语言的加号操作符(+)定义一个线性模型,将crimeRatePerCapitapctLowerStatusradialHighwaysAccessmedianValue_KsnitrixOxide_pp10m进行回归,并将其分配给名为multiLinearModel的变量。如果代码行不够长,使用 Python 的行续符号(\)继续编写代码:

    multiLinearModel = smf.ols\
                       (formula = 'crimeRatePerCapita \
                                   ~ pctLowerStatus \
                                   + radialHighwaysAccess \
                                   + medianValue_Ks \
                                   + nitrixOxide_pp10m', \
                                   data=train_data)
    
  4. 调用模型实例的fit方法,并将该方法的结果分配给一个变量:

    multiLinearModResult = multiLinearModel.fit()
    
  5. 打印出步骤 3中创建的变量中存储的结果摘要:

print(multiLinearModResult.summary())

输出结果如下:

图 2.18:多元线性回归结果摘要

图 2.18:多元线性回归结果摘要

注意

要访问此特定部分的源代码,请参见packt.live/34cJgOK

你也可以在packt.live/3h1CKOt在线运行这个示例。

如果正确完成了练习,图 2.18将是分析的结果。在活动 2.01中,使用了 R 平方统计量来评估模型的拟合优度。当涉及多个自变量时,使用调整后的 R 平方统计量来评估所创建模型的拟合优度。

调整后的 R 平方统计量考虑了模型中额外自变量的存在,并对模型拟合优度的膨胀进行了修正,这种膨胀只是因为模型中使用了更多的自变量。

我们从这个练习中学到的经验是,图 2.18第一部分的调整后 R 平方值有所提高。当仅使用一个自变量来创建模型以解释练习 2.04crimeRatePerCapita的变异性时,计算出的 R 平方值仅为14.4百分比。而在本练习中,我们使用了四个自变量。创建的模型将调整后的 R 平方统计量提高到了39.1百分比,增加了24.7百分比。

我们了解到,自变量与因变量之间的相关性可以帮助解释模型中自变量的变异性。但显然,因变量中的相当一部分变异性,约60.9百分比,仍然没有被我们的模型解释。

如果我们希望模型能够很好地解释crimeRatePerCapita中的变异性,仍然有改进的空间。在图 2.18第二部分中,我们列出了模型中的截距和所有自变量,并附上了它们的系数。如果我们将pctLowerStatus表示为 x1,radialHighwaysAccess表示为 x2,medianValue_Ks表示为 x3,nitrixOxide_pp10m表示为 x4,那么创建的模型的数学表达式可以写为 y ≈ 0.8912 + 0.1028x1 + 0.4948x2 - 0.1103x3 - 2.1039x4。

上述表达式定义了在本练习中创建的模型,它可以与图 2.5中早先提供的多元线性回归的表达式进行比较。

回归分析的假设

由于线性回归分析的参数化特性,该方法对所分析的数据做出了一些假设。当这些假设不成立时,回归分析的结果可能会误导我们,至少是这样。因此,检查分析工作以确保回归假设没有被违反是必要的。

让我们回顾一下线性回归分析的主要假设,这些假设必须得到满足,才能开发出一个良好的模型:

  1. 因变量与自变量之间的关系必须是线性且可加的。

    这意味着关系必须是直线型的,如果涉及多个自变量,则是多元线性回归,这些自变量的加权和必须能够解释因变量的变异性。

  2. 残差项(ϵi)必须服从正态分布。这样可以确保估计的标准误差计算正确。这个估计标准误差统计量用于计算 t 值,而 t 值又用于做出统计显著性决策。因此,如果估计标准误差是错误的,那么 t 值也会错误,随后根据 p 值做出的统计显著性决策也会错误。使用估计标准误差计算的 t 值还用于构建总体回归参数的置信区间。如果标准误差错误,那么置信区间也会错误。

  3. 残差项(ϵi)必须具有恒定的方差(同方差性)。当情况不是这样时,我们就面临异方差性问题。这个问题指的是残差项的方差。我们假设它是恒定的。我们假设回归分析中的每个数据点对我们希望建模的变异性贡献相等。如果某些数据点比其他数据点贡献更多的解释,我们的回归线将被拉向信息更多的点。这些数据点不会均匀地分布在回归线周围。在这种情况下,回归线的误差(方差)将不是恒定的。

  4. 残差项(ϵi)必须没有相关性。当残差项之间存在相关性时,我们就遇到了所谓的自相关问题。知道一个残差项的值,不能帮助我们预测下一个残差项的值。自相关的残差项不太可能符合正态分布。

  5. 自变量之间不得存在相关性。当自变量之间相互相关时,我们就遇到了一个问题,称为多重共线性。这将导致我们开发出一个模型,其中的系数值依赖于其他自变量的存在。换句话说,如果某个特定的自变量从模型中移除,模型可能会发生剧烈变化。这样的模型将是不准确的。

活动 2.02:拟合多元对数线性回归模型

你之前开发的对数线性回归模型能够解释转化后的每人犯罪率变量约 24%的变化性(见图 2.17中的数值)。现在你被要求开发一个对数线性多元回归模型,该模型可能会解释转化后的因变量 80%或更多的变化性。你应该使用波士顿住房数据集中相关系数为 0.4 或更高的自变量。

我们也鼓励你在模型中包括这些变量的二阶交互项。你应该生成图表和数据,证明你的模型满足线性回归的假设。

步骤如下:

  1. 定义一个线性回归模型并将其赋值给一个变量。记得在公式字符串中使用log函数来转换因变量,并且在分析中包括多个自变量。

  2. 调用模型实例的fit方法,并将该方法的结果赋值给一个新变量。

  3. 打印结果的摘要并分析你的模型。

    你的输出应该如图所示:

    图 2.19:预期的 OLS 结果

图 2.19:预期的 OLS 结果

注意

此活动的解决方案可以在这里找到:packt.live/2GbJloz

解释回归分析的结果

回归分析的一个主要目标是找到一个能够解释因变量变化性的模型。因此,非常重要的是要有一个量度,能够衡量回归模型解释变化性的程度。这个统计量被称为 R 平方(R2)。有时,它也被称为决定系数。为了理解它到底衡量了什么,我们需要查看一些其他的定义。

其中第一个是被称为总平方和TSS)的量。TSS 为我们提供了因变量相对于其均值的总方差的度量。

下一个量称为回归平方和RSS)。它衡量了我们模型解释的因变量的变异程度。如果你想象我们创建了一个完美的模型,预测没有任何误差,那么 TSS 将等于 RSS。我们假设的完美模型将解释因变量相对于均值的所有变异。在实践中,这种情况很少发生。相反,我们创建的模型并不完美,因此 RSS 小于 TSS。RSS 与 TSS 的差额是回归模型无法解释的因变量变异量。这个量被称为误差平方和ESS),本质上是我们模型的残差项之和。

R 平方是 RSS 与 TSS 的比率。因此,它给出了我们的回归模型相对于因变量均值总变异量能够解释的变异百分比。当 RSS 变小,R2 也会变小,反之亦然。在简单线性回归中,当自变量只有一个时,R2 足以决定模型与数据的整体拟合程度。

然而,当涉及到多元线性回归时,存在一个问题。R2 已知对模型中加入额外自变量非常敏感,即使该自变量与因变量只有微弱的相关性。它的加入会增加 R2。仅仅依赖 R2 来在为同一个因变量定义的模型之间做决策,最终会导致追求一个包含许多自变量的复杂模型。这种复杂性在实践中没有帮助。事实上,它可能会导致一个叫做过拟合的建模问题。

为了克服这个问题,使用了调整后的 R2(在 statsmodels 的输出中表示为Adj. R-Squared),用来在为同一个因变量定义的模型之间进行选择。只有当自变量的加入有意义地帮助解释因变量的变异时,调整后的 R2 才会增加。

活动 2.02中,拟合多个对数线性回归模型,我们的模型解释了转化后因变量 88%的变异,这非常好。我们从简单的模型开始,利用不同的技术来改进模型的拟合度。本章中所有的练习和活动都强调回归分析工作流是迭代的。你从绘图开始,获得一个可视化图像,然后根据这个图像继续改进最终开发的模型,使用不同的技术。一旦开发出一个好的模型,下一步就是在统计上验证该模型,然后才能用它进行预测或获取决策支持。接下来,我们来讨论在统计上验证模型意味着什么。

回归分析中的制衡

在前面的讨论中,我们使用了 R 平方值和调整后的 R 平方值统计量来评估模型的拟合优度。尽管 R 平方值统计量提供了模型与因变量之间关系强度的估计,但它并没有提供该关系的正式统计假设检验。

那么,什么是针对模型中因变量与某些自变量之间关系的正式统计假设检验呢?

我们必须记住,要说一个自变量与回归模型中的因变量有关系,该自变量的回归系数(β)必须是非零(不等于 0)。如果我们用波士顿住房数据集进行回归分析,找到一个自变量(比如拥有者自住房屋的中位数)在模型中有一个非零的回归系数(β),那是很好且正确的。

问题是,如果我们使用不同位置或时间的波士顿住房数据集的不同样本重复此分析,我们是否(或其他人)会发现拥有者自住房屋的中位数具有非零的回归系数(β)?在我们的分析中发现的拥有者自住房屋中位数的非零回归系数,是特定于我们的样本数据集,还是对于任何其他波士顿住房数据样本来说都是零?我们是否偶然发现了拥有者自住房屋中位数的非零回归系数?这些问题是假设检验要解决的。我们无法百分之百确定一个自变量的非零回归系数(β)是否是偶然的,但假设检验提供了一个框架,让我们能够计算出一个置信度水平,以便我们可以说,在我们的分析中发现的非零回归系数(β)不是偶然的。这就是它的工作原理。

我们首先确定一个风险水平(α值或α风险或第一类错误),即回归系数(β)可能是偶然发现的风险。我们接受在这个风险水平下,错误地认为回归系数(β)是非零的,而实际上它是零。

在大多数实际分析中,α值设定为 0.05,即百分比上的 5%。当我们从 1 中减去α风险(1-α)时,我们得到了一个度量,表示我们对在分析中发现的非零回归系数(β)不是由偶然造成的置信度。因此,我们的置信水平在 5% α值下是 95%。

接下来,我们计算一个概率值(通常称为 p 值),它为我们提供了模型中感兴趣的回归系数(β)相关的α风险的度量。我们将 p 值与我们选择的α风险进行比较,如果 p 值小于我们设定的α风险,我们就拒绝认为非零回归系数(β)是偶然发现的观点。这是因为我们所设定的接受限度内,错误地认为回归系数(β)非零的风险是可以接受的。

另一种说法是,非零系数(β)不是偶然发现的,我们可以说该系数(β)具有统计显著性,或者我们拒绝零假设(零假设为研究的变量之间没有关系)。我们将这些统计显著性的概念应用于我们的模型中,分为两个阶段:

  1. 在第一阶段,我们整体验证模型的统计显著性。

  2. 在第二阶段,我们单独验证模型中的每个自变量的统计显著性。

F 检验

F 检验用于验证模型与其因变量之间关系的整体统计显著性。如果 F 检验的 p 值小于所选的α水平(在我们的例子中是 0.05),我们拒绝零假设,并得出结论,模型在整体上具有统计显著性。

当我们拟合回归模型时,会生成一个 F 值。这个值可以用来判断测试是否具有统计学意义。通常,R²的增加会导致 F 值的增加。这意味着,F 值越大,模型的整体统计显著性越有可能。

一个好的 F 值应该大于1图 2.19中的模型 F 统计量值为 261.5,超过了 1,并且其 p 值(Prob (F-statistic))接近零。我们所选择的 5%错误容忍限度小于犯错的风险(即,当我们不应该拒绝零假设时错误地拒绝了它,这被称为假设检验中的 I 型错误)。因为 p 值小于 0.05,我们拒绝了图 2.19模型的零假设。因此,我们可以说该模型在所选的 95%置信水平下具有统计显著性。

t 检验

一旦确定模型在整体上具有统计显著性,我们可以继续检验模型中各个自变量的显著性。在图 2.19中,提供了自变量的 p 值(在第二部分中用 p>|t| 表示)。这些 p 值是通过摘要结果中给出的 t 值计算得出的。这个过程与之前讨论的全局显著性检验没有什么不同。我们将 p 值与 0.05 的 α 水平进行比较。如果某个自变量的 p 值小于 0.05,则该自变量在解释因变量的变异性方面在统计上是显著的。如果 p 值大于或等于 0.05,则该自变量(或项)在我们的模型中并不具有统计显著性。这意味着该项在我们的模型中不能为解释因变量的变异性提供统计学上的贡献。仔细观察图 2.19,可以看到一些项的 p 值大于 0.05。这些项在统计学上对解释我们转化后的因变量的变异性没有显著贡献。为了改进这个模型,需要删除这些项,并尝试一个新的模型。到此为止,我们已经清楚地认识到,建立回归模型的过程是一个真正的迭代过程。

概述

本章介绍了使用 Python 进行线性回归分析的主题。我们了解到回归分析通常是一个监督式机器学习或数据科学问题。我们学习了线性回归分析的基本原理,包括最小二乘法背后的思想。我们还学习了如何使用 pandas Python 模块加载和准备数据进行探索和分析。

我们探索了如何创建双变量数据的散点图,并学习了如何通过这些图形拟合最佳拟合线。在这个过程中,我们发现了 Python 中 statsmodels 模块的强大功能。我们探索了如何使用它来定义简单线性回归模型,并求解相关参数。我们还学习了如何将这种方法扩展到自变量超过一个的情况——多元线性回归。我们研究了通过某些方法将因变量与自变量之间的非线性关系转化为线性问题,从而使用线性回归处理非线性问题,这种转化带来了新的可能性。我们深入探讨了 statsmodels 公式语言,学习了如何使用它来定义各种线性模型并求解它们的相应模型参数。

我们继续学习模型拟合优度背后的思想。我们讨论了 R 平方统计量,作为回归模型拟合优度的度量。接着,我们讨论了统计显著性的基本概念。我们学习了如何使用 F 统计量对回归模型进行全局验证,而这项工作由 Python 为我们完成。我们还检查了如何使用 t 检验及其关联的 p 值来检验单个模型系数的统计显著性。我们回顾了线性回归分析的假设以及它们如何影响回归分析工作的有效性。

我们现在将从回归分析转向第三章二分类,以及第四章使用随机森林的多分类,它们分别讨论二分类和多标签分类。这些章节将介绍处理监督学习问题所需的技术,其中因变量是分类数据类型。

回归分析将在本书后续部分深入讨论模型性能改进和解释时再次出现。在第八章超参数调优中,我们将看到如何使用 k-最近邻算法作为进行回归分析的另一种方法。我们还将介绍岭回归,这是一种线性回归方法,适用于参数数量较多的情况。

第三章:3. 二元分类

概述

在本章中,我们将使用一个现实世界的数据集,并使用一种名为分类的监督学习技术来生成商业结果。

到本章结束时,你将能够从商业角度制定数据科学问题陈述;根据影响使用案例的各种商业驱动因素建立假设,并通过探索性数据分析验证这些假设;通过特征工程基于探索性分析中的直觉派生特征;使用逻辑回归函数建立二元分类模型,并分析分类指标,制定模型改进的行动计划。

介绍

在前几章中,介绍了机器学习的两个广泛类别;监督学习和无监督学习。监督学习可以进一步细分为两种类型的问题:回归和分类。在上一章中,我们讨论了回归问题。在本章中,我们将窥探分类问题的世界。

看一下以下的图 3.1

图 3.1:机器学习算法概述

图 3.1:机器学习算法概述

分类问题是你在现实世界中最常遇到的用例。与回归问题不同,回归问题预测的是一个实际数值,而分类问题则处理将一个示例与一个类别关联。分类用例将呈现以下几种形式:

  • 预测客户是否会购买推荐的产品

  • 确定信用交易是否为欺诈性交易

  • 确定患者是否患有某种疾病

  • 分析动物的图像并预测图像是狗、猫还是熊猫

  • 分析文本评论并捕捉潜在的情绪,如幸福、愤怒、悲伤或讽刺

如果观察前述示例,会发现前三个和最后两个之间有一个微妙的区别。前三个围绕二元决策:

  • 客户可以选择购买产品或不购买。

  • 信用卡交易可能是欺诈性的或合法的。

  • 患者可以被诊断为疾病的阳性或阴性。

与前述三种类型对齐的用例,其中做出二元决策的问题称为二元分类问题。与前三者不同,后两者将一个示例与多个类别或类别关联。这类问题被称为多类别分类问题。本章将讨论二元分类问题。多类别分类将在第四章中介绍,随机森林的多类别分类

理解商业背景

使用一个与你相关的示例来理解概念是最好的方法。为了理解商业背景,我们可以考虑以下示例。

你作为一名数据科学家,所在银行的营销负责人找到你,希望解决一个问题。最近,营销团队完成了一项营销活动,他们已经收集了关于现有客户的大量信息。他们需要你帮助确定哪些客户可能会购买定期存款计划。根据你对客户群体的评估,营销团队将制定目标营销策略。营销团队提供了过去营销活动及其结果的历史数据,即目标客户是否真的购买了定期存款。凭借历史数据,你已经开始了识别具有最高购买倾向(倾向)的客户的任务。

业务发现

如前所述,像前述这样从事数据科学问题的第一个过程是业务发现过程。这包括理解影响业务问题的各种驱动因素。了解业务驱动因素很重要,因为这将有助于制定关于业务问题的假设,在探索性数据分析(EDA)期间可以进行验证。验证假设将有助于制定特征工程的直觉,这对我们构建的模型的准确性至关重要。

让我们从我们用例的背景详细了解这个过程。问题陈述是识别那些有购买定期存款倾向的客户。如你所知,定期存款是一种银行工具,您的资金将被锁定一段时间,提供比储蓄账户或带息支票账户更高的利率。从投资倾向的角度来看,定期存款通常受到风险厌恶型客户的欢迎。有了业务背景,让我们看看一些影响购买定期存款倾向的业务因素的问题:

  • 年龄会是一个因素吗?老年人显示更高的购买倾向吗?

  • 就业状态与购买定期存款的倾向之间是否存在任何关系?

  • 顾客的资产组合(即房屋、贷款或更高的银行存款)会影响购买的倾向吗?

  • 人口统计信息,如婚姻状况和教育水平,会影响购买定期存款的倾向吗?如果会,人口统计数据与购买倾向之间如何相关?

在进行探索性分析时,制定有关业务背景的问题至关重要。在我们进行探索性分析时,这将有助于我们找出可以采取的各种路径。让我们先来探索与前述业务问题相关的数据。

练习 3.01:从数据集中加载和探索数据

在本次练习中,我们将在 Colab 笔记本中加载数据集,并进行一些基本的探索,如使用.shape()函数打印数据集的维度,并使用.describe()函数生成数据集的汇总统计信息。

注意

本次练习使用的数据集是银行数据集,感谢 S. Moro、P. Cortez 和 P. Rita 提供:《一种数据驱动方法预测银行电话营销的成功》。

数据集来源于 UCI 机器学习库:packt.live/2MItXEl,并且可以从我们的 GitHub 上下载:packt.live/2Wav1nJ

以下步骤将帮助你完成本次练习:

  1. 打开一个新的 Colab 笔记本。

  2. 现在,在你的 Colab 笔记本中,import pandaspd

    import pandas as pd
    
  3. 将数据集的链接分配给一个名为file_url的变量

    file_url = 'https://raw.githubusercontent.com/PacktWorkshops'\
               '/The-Data-Science-Workshop/master/Chapter03'\
               '/bank-full.csv'
    
  4. 现在,使用pd.read_csv()函数读取文件,来自 pandas 的 DataFrame:

    # Loading the data using pandas
    bankData = pd.read_csv(file_url, sep=";")
    bankData.head()
    pd.read_csv() function's arguments are the filename as a string and the limit separator of a CSV, which is ";". After reading the file, the DataFrame is printed using the .head() function. Note that the # symbol in the code above denotes a comment. Comments are added into code to help explain specific bits of logic. 
    

    你应该获得以下输出:

    图 3.2:将数据加载到 Colab 笔记本中

    图 3.2:将数据加载到 Colab 笔记本中

    这里,我们加载了CSV文件并将其存储为 pandas DataFrame,以便进一步分析。

  5. 接下来,打印数据集的形状,如以下代码片段所示:

    # Printing the shape of the data 
    print(bankData.shape function is used to find the overall shape of the dataset.You should get the following output:
    
    

    (45211, 17)

    
    
  6. 现在,使用 pandas 中的.describe()函数,以表格输出的方式查找数值原始数据的摘要,如以下代码片段所示:

    # Summarizing the statistics of the numerical raw data
    bankData.describe()
    

    你应该获得以下输出:

    图 3.3:将数据加载到 Colab 笔记本中

图 3.3:将数据加载到 Colab 笔记本中

从数据的形状来看,数据集有45211个样本,包含17个变量。变量集包括分类变量和数值变量。前面的汇总统计值仅针对数值数据得出。

注意

要访问这一特定部分的源代码,请参考packt.live/31UQhAU

你也可以在packt.live/2YdiSAF在线运行这个示例。

你已经完成了开始我们旅程之前所需的第一项任务。在本次练习中,你学习了如何加载数据并从数据集中得出基本统计数据,如汇总统计值。在随后的数据集中,我们将深入分析加载的数据集。

使用探索性数据分析测试业务假设

在上一节中,你从领域的角度接近了问题的陈述,识别了一些业务驱动因素。一旦识别出业务驱动因素,下一步是提出一些关于这些业务驱动因素与所设定的业务结果之间关系的假设。这些假设需要使用你所拥有的数据进行验证。探索性数据分析EDA)在数据科学生命周期中发挥了重要作用。

让我们回到我们试图分析的问题。从上一节中,我们识别了一些商业驱动因素,如年龄、人口统计学、就业状况和资产组合,这些因素可能会影响购买定期存款的倾向。现在让我们根据这些商业驱动因素提出假设,并通过 EDA 进行验证。

探索性数据分析的可视化

可视化是探索性数据分析(EDA)中的关键。有效的可视化有助于从数据中提取商业直觉。在本节中,我们将介绍一些用于 EDA 的可视化技术:

图 3.4:折线图示例

图 3.5:直方图示例

图 3.6:密度图示例

  • YesNo,在同一条条形图上。这可以通过堆叠条形图实现,而其他图表则无法做到这一点。

    让我们创建一些虚拟数据并生成堆叠条形图,以检查不同领域中的职位比例。

    # Importing library files
    import matplotlib.pyplot as plt
    import numpy as np
    

    接下来,创建一些样本数据,详细列出职位列表:

    # Create a simple list of categories
    jobList = ['admin','scientist','doctor','management']
    

    每个职位将有两个类别需要绘制,yesNo,它们之间存在某种比例。具体细节如下:

    # Getting two categories ( 'yes','No') for each of jobs
    jobYes = [20,60,70,40]
    jobNo = [80,40,30,60]
    

    在接下来的步骤中,将计算职位列表的长度来绘制xlabels,然后使用np.arange()函数进行排列:

    # Get the length of x axis labels and arranging its indexes
    xlabels = len(jobList)
    ind = np.arange(xlabels)
    

    接下来,定义每个条形的宽度并进行绘图。在图表p2中,我们定义堆叠时,yes位于底部,No位于顶部:

    # Get width of each bar
    width = 0.35
    # Getting the plots
    p1 = plt.bar(ind, jobYes, width)
    p2 = plt.bar(ind, jobNo, width, bottom=jobYes)
    

    定义Y轴的标签和图表标题:

    # Getting the labels for the plots
    plt.ylabel('Proportion of Jobs')
    plt.title('Job')
    

    接下来定义X轴和Y轴的索引。对于X轴,列出各个职位,而对于Y轴,索引是从0100的比例,增量为10(0, 10, 20, 30,依此类推):

    # Defining the x label indexes and y label indexes
    plt.xticks(ind, jobList)
    plt.yticks(np.arange(0, 100, 10))
    

    最后一步是定义图例并将坐标轴标签旋转至90度。最终显示图表:

    # Defining the legends
    plt.legend((p1[0], p2[0]), ('Yes', 'No'))
    # To rotate the axis labels 
    plt.xticks(rotation=90)
    plt.show()
    

以下是基于前面示例的堆叠条形图的样子:

图 3.7:堆叠条形图示例

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_03_08.jpg)

图 3.7:堆叠条形图示例

让我们在接下来的练习和活动中使用这些图表。

练习 3.02:年龄与定期贷款购买倾向的商业假设验证

这个练习的目标是定义一个假设,检查个人购买定期存款计划的倾向与他们的年龄之间的关系。我们将在这个练习中使用折线图。

以下步骤将帮助你完成这个练习:

  1. 从定义假设开始。

    验证过程的第一步是定义一个关于关系的假设。假设可以基于你的经验、领域知识、一些已发布的知识或你的商业直觉。

    首先,我们定义关于年龄与购买定期存款倾向的假设:

    老年客户比年轻客户更倾向于购买定期存款。这是我们的假设。

    现在我们已经定义了假设,是时候用数据来验证它的真实性了。通过从数据中提取横截面并进行可视化,是从数据中获得商业直觉的最佳方式之一。

  2. 导入pandasaltair包:

    import pandas as pd
    import altair as alt
    
  3. 接下来,你需要加载数据集,就像在练习 3.01中加载数据集一样,加载和探索数据集中的数据

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter03/bank-full.csv'
    bankData = pd.read_csv(file_url, sep=";")
    

    注意

    步骤 2-3将在本章的后续练习中重复进行。

    我们将验证购买定期存款的情况如何按年龄分布。

  4. 接下来,我们将统计每个年龄组的记录数量。我们将使用pandas中的.groupby().agg().reset_index()方法的组合。

    filter_mask = bankData['y'] == 'yes'
    bankSub1 = bankData[filter_mask]\
               .groupby('age')['y'].agg(agegrp='count')\
               .reset_index()
    

    我们首先取出在练习 3.01中加载的pandas DataFramebankData,然后使用掩码bankData['y'] == 'yes'筛选出所有定期存款为“是”的案例。这些案例通过groupby()方法进行分组,然后通过agg()方法根据年龄进行聚合。最后,我们需要使用.reset_index()获取一个结构良好的DataFrame,并将其存储在一个新的DataFrame中,称为bankSub1

  5. 现在,使用altair.Chart().mark_line().encode()方法绘制折线图,我们将定义xy变量,如下所示的代码片段:

    # Visualising the relationship using altair
    alt.Chart(bankSub1).mark_line().encode(x='age', y='agegrp')
    

    你应该得到以下输出:

    图 3.8:年龄与购买倾向之间的关系

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_03_08.jpg)

    图 3.8:年龄与购买倾向之间的关系

    从图表中我们可以看到,定期存款购买的数量在年龄范围为 25 至 40 岁之间的客户中最高,且购买倾向随着年龄的增长逐渐减弱。

    这个关系和我们在假设中的预期相反,对吧?但是,等一下,难道我们没有忽略一个重要的点吗?我们是根据每个年龄段客户的绝对数量来处理数据的。如果 25 到 40 岁年龄段的银行客户比例较高,那么我们很可能会得到类似我们当前得到的图表。我们真正应该绘制的是每个年龄段内,购买定期存款的客户比例。

    让我们看看如何通过获取客户比例来表示数据。就像您在之前的步骤中所做的那样,我们将根据年龄对客户购买倾向进行汇总,然后将每个购买倾向类别除以该年龄组内的总客户数,从而得到比例。

  6. 使用groupby()方法按年龄对数据进行分组,并使用agg()方法找到每个年龄组内客户的总数:

    # Getting another perspective
    ageTot = bankData.groupby('age')['y']\
             .agg(ageTot='count').reset_index()
    ageTot.head()
    

    输出如下:

    图 3.9:各年龄段客户

    图 3.9:各年龄段客户

  7. 现在,按年龄和购买倾向对数据进行分组,并找到每个购买倾向类别(yesno)下的总计数:

    # Getting all the details in one place
    ageProp = bankData.groupby(['age','y'])['y']\
              .agg(ageCat='count').reset_index()
    ageProp.head()
    

    输出如下:

    图 3.10:各年龄段购买倾向

    图 3.10:各年龄段购买倾向

  8. 使用pd.merge()函数根据age变量合并这两个 DataFrame,然后将每个年龄组内的购买倾向类别除以该年龄组内的总客户数,从而得到客户比例,如以下代码片段所示:

    # Merging both the data frames
    ageComb = pd.merge(ageProp, ageTot,left_on = ['age'], \
                       right_on = ['age'])
    ageComb['catProp'] = (ageComb.ageCat/ageComb.ageTot)*100
    ageComb.head()
    

    输出如下:

    图 3.11:按年龄组合并的 DataFrame 及客户比例

    图 3.11:按年龄组合并的 DataFrame 及客户比例

  9. 现在,显示比例图表,分别将yesno类别作为单独的图表进行绘制。这可以通过altair中的facet()方法实现:

    # Visualising the relationship using altair
    alt.Chart(ageComb).mark_line()\
       .encode(x='age', y='catProp').facet(column='y')
    

    这个函数会根据变量中类别的数量生成相应数量的图表。在这里,我们将'y'变量(即表示yesno类别的变量名称)传递给facet()函数,得到两个不同的图表:一个表示yes,另一个表示no

    您应该得到以下输出:

    图 3.12:可视化标准化关系

图 3.12:可视化标准化关系

在本次练习结束时,您应该能够得到两个有意义的图表,展示人们购买定期存款计划的倾向。该练习的最终输出展示了两个图表,左侧图表显示的是没有购买定期存款的人群比例,右侧图表显示的是购买定期存款的客户。

我们可以看到,在第一个图表中,年龄从22岁到60岁之间的个体并不倾向于购买定期存款。然而,在第二个图表中,我们看到相反的情况,60岁及以上的群体更倾向于购买定期存款计划。

注意

若要访问此部分的源代码,请参考packt.live/3iOw7Q4

本节目前没有在线互动示例,但可以像往常一样在 Google Colab 上运行。

在接下来的部分,我们将开始基于我们的直觉分析我们的图表。

探索性分析中的直觉

到目前为止,我们从这个练习中能提取出哪些直觉?我们通过考虑用户的比例和不考虑比例的两种不同情形看到了两种对比的图表。正如你所看到的,考虑用户比例是获取正确视角的正确方法,这是我们假设所要求的视角。我们从图表中可以看出,从22岁到大约60岁之间的年龄段,定期存款的购买倾向较低。

60岁之后,我们看到定期存款需求呈上升趋势。另一个有趣的事实是,20岁以下的群体有较高的定期存款购买比例。

练习 3.02中,关于年龄与定期贷款倾向的商业假设检验,我们发现了如何发展我们的假设,然后使用 EDA 验证这个假设。在接下来的活动中,我们将深入探讨旅程中的另一个重要步骤——特征工程。

活动 3.01:商业假设检验,找出就业状态与定期存款倾向的关系

你正在为一家银行担任数据科学家。你收到了来自银行管理层的历史数据,并被要求尝试建立就业状态与定期存款购买倾向之间的假设。

练习 3.02中,关于年龄与定期贷款倾向的商业假设检验,我们解决了一个问题,旨在寻找年龄与定期存款购买倾向之间的关系。在这个活动中,我们将采用类似的路线,并验证就业状态与定期存款购买倾向之间的关系。

步骤如下:

  1. 构建关于就业状态和定期存款倾向之间的假设。假设如下:高薪员工比其他类别的员工更倾向于选择定期存款

  2. 打开一个类似于练习 3.02中使用的 Colab 笔记本文件,并安装和导入必要的库,如pandasaltair

  3. 从银行数据框bankData中,使用.groupby().agg().reset_index()方法查找就业状态的分布。

    使用.groupby()方法按就业状态对数据进行分组,并使用.agg()方法查找每种就业状态的倾向总数。

  4. 现在,使用pd.merge()函数合并两个 DataFrame,然后通过计算每种就业状态的倾向比例来找到倾向计数。在创建新变量以找到倾向比例时。

  5. 使用matplotlib绘制数据并总结图表中的直觉。此活动使用堆叠条形图。

    注意

    用于此活动的bank-full.csv数据集可以在packt.live/2Wav1nJ找到。

预期输出:关于购买倾向与就业状态的最终图表将类似于以下图表:

图 3.13:按职业可购买倾向的可视化

图 3.13:按职业可购买倾向的可视化

注意

这个活动的解决方案可以通过以下地址找到:packt.live/2GbJloz

既然我们已经了解了 EDA,让我们深入探讨特征工程。

特征工程

在上一节中,我们遍历了 EDA 的过程。作为早期过程的一部分,我们通过切片、切分数据和可视化来测试我们的业务假设。你可能会想知道我们会在哪里使用从所有分析中得出的直觉。这个问题的答案将在本节中解决。

特征工程是将原始变量转换为新变量的过程,本章稍后将详细讨论。特征工程是影响我们构建的模型准确性的重要步骤之一。

特征工程大致分为两类:

  1. 在这里,我们根据业务角度的直觉来转换原始变量。这些直觉是在探索性分析过程中建立的。

  2. 原始变量的转换是从统计学和数据规范化的角度进行的。

接下来我们将探讨每种特征工程类型。

注意

特征工程将在第十二章特征工程中详细讲解。在本节中,您将看到学习分类的目的。

基于业务驱动的特征工程

基于业务驱动的特征工程是根据在探索性分析过程中得出的业务直觉,转换原始变量的过程。它涉及根据影响业务问题的业务因素或驱动因素,转化数据并创建新变量。

在之前的探索性分析练习中,我们探讨了单一变量与因变量之间的关系。在本练习中,我们将结合多个变量并推导出新特征。我们将探索资产组合与购买定期存款的倾向之间的关系。资产组合是客户在银行的所有资产和负债的组合。我们将结合银行余额、房产所有权和贷款等资产与负债,得到一个名为资产的指数。

这些特征工程步骤将分为两个练习。在练习 3.03特征工程 – 单独特征的探索中,我们将探索平衡、住房和贷款等单个变量,以了解它们与定期存款倾向的关系。

练习 3.04特征工程 – 从现有特征创建新特征中,我们将转化单个变量并将它们组合成一个新特征。

练习 3.03:特征工程 – 单独特征的探索

在本练习中,我们将探索两个变量之间的关系,即一个人是否拥有房屋以及是否有贷款,这些与这些人购买定期存款的倾向有关。

以下步骤将帮助你完成本练习:

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包。

    import pandas as pd
    
  3. 将数据集的链接赋值给名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter03/bank-full.csv'
    
  4. 使用.read_csv()函数读取银行数据集:

    # Reading the banking data
    bankData = pd.read_csv(file_url, sep=";")
    
  5. 接下来,我们将找到住房与定期存款倾向之间的关系,如以下代码片段所示:

    # Relationship between housing and propensity for term deposits
    bankData.groupby(['housing', 'y'])['y']\
            .agg(houseTot='count').reset_index()
    

    你应该得到以下输出:

    图 3.14:住房状况与购买定期存款的倾向

    图 3.14:住房状况与购买定期存款的倾向

    代码的第一部分是根据客户是否拥有房屋将其分组。使用.agg()方法计算每个类别下的客户数量。从这些值中我们可以看到,购买定期存款的倾向在没有房屋的客户中明显高于有房屋的客户:(3354 / (3354 + 16727) = 17% 与 1935 / (1935 + 23195) = 8%)

  6. 探索'loan'变量,以找出它与定期存款倾向之间的关系,如以下代码片段所示:

    """
    Relationship between having a loan and propensity for term 
    deposits
    """
    bankData.groupby(['loan', 'y'])['y']\
            .agg(loanTot='count').reset_index()
    # symbol. 
    

    你应该得到以下输出:

    图 3.15:贷款与定期存款倾向

    图 3.15:贷款与定期存款倾向

    对于贷款组合,购买定期存款的倾向在没有贷款的客户中较高:(4805 / (4805 + 33162) = 12% 与 484 / (484 + 6760) = 6%)

    住房和贷款是分类数据,找到它们之间的关系是直接的。然而,银行余额数据是数值型的,要分析它,我们需要采用不同的策略。一个常见的策略是将连续的数值数据转换为有序数据,并观察各类别之间的倾向性如何变化。

  7. 为了将数值值转换为有序值,我们首先找到分位数值并将它们作为阈值。分位数是使用以下代码片段获得的:

    #Taking the quantiles for 25%, 50% and 75% of the balance data
    import numpy as np
    np.quantile(bankData['balance'],[0.25,0.5,0.75])
    

    您应该得到以下输出:

    图 3.16:银行余额数据的分位数

    Step 4, we calculated the 25th, 50th, and 75th percentiles, which resulted in 72, 448, and 1428.
    
  8. 现在,将银行余额的数值转换为分类值,如以下代码片段所示:

    bankData['balanceClass'] = 'Quant1'
    bankData.loc[(bankData['balance'] > 72) \
                  & (bankData['balance'] < 448), \
                  'balanceClass'] = 'Quant2'
    bankData.loc[(bankData['balance'] > 448) \
                  & (bankData['balance'] < 1428), \
                  'balanceClass'] = 'Quant3'
    bankData.loc[bankData['balance'] > 1428, \
                 'balanceClass'] = 'Quant4'
    bankData.head()
    

    您应该得到以下输出:

    图 3.17:来自银行余额数据的新特征

    图 3.17:来自银行余额数据的新特征

    我们通过查看在步骤 4中采取的分位数阈值,将数值数据分类到相应的分位数类别。例如,所有低于第 25 分位数值 72 的值被分类为Quant1,介于 72 和 448 之间的值被分类为Quant2,依此类推。为了存储分位数类别,我们在银行数据集中创建了一个名为balanceClass的新特征,并将其默认值设置为Quan1。之后,根据每个值的阈值,数据点被分类到相应的分位数类别。

  9. 接下来,我们需要根据客户所属的每个分位数来寻找定期存款购买的倾向性。这个任务类似于我们在练习 3.02中做的内容,即年龄与定期贷款倾向性之间的商业假设测试

    # Calculating the customers under each quantile 
    balanceTot = bankData.groupby(['balanceClass'])['y']\
                         .agg(balanceTot='count').reset_index()
    balanceTot
    

    您应该得到以下输出:

    图 3.18:基于分位数的分类

    图 3.18:基于分位数的分类

  10. 计算按分位数和倾向性分类的客户总数,如以下代码片段所示:

    """
    Calculating the total customers categorised as per quantile 
    and propensity classification
    """
    balanceProp = bankData.groupby(['balanceClass', 'y'])['y']\
                          .agg(balanceCat='count').reset_index()
    balanceProp
    

    您应该得到以下输出:

    图 3.19:按分位数分类的客户总数    以及倾向性分类

    图 3.19:按分位数和倾向性分类的客户总数

  11. 现在,merge两个数据框:

    # Merging both the data frames
    balanceComb = pd.merge(balanceProp, balanceTot, \
                           on = ['balanceClass'])
    balanceComb['catProp'] = (balanceComb.balanceCat \
                              / balanceComb.balanceTot)*100
    balanceComb
    

    您应该得到以下输出:

    图 3.20:倾向性与余额类别

图 3.20:倾向性与余额类别

从数据的分布情况来看,我们可以看到,当从第 1 分位数移动到第 4 分位数时,购买定期存款的客户比例持续增加。例如,在所有属于Quant 1的客户中,7.25%购买了定期存款(我们从catProp中得到这个百分比)。这个比例在Quant 2中增加到 10.87%,然后在Quant 3Quant4中分别增加到 12.52%和 16.15%。从这个趋势可以得出结论,余额较高的个人更倾向于购买定期存款。

在这个练习中,我们探讨了每个变量与定期存款购买倾向之间的关系。我们可以观察到的总体趋势是,手头现金更多(没有贷款且余额较高)的人,购买定期存款的倾向较高。

注意

要访问此特定部分的源代码,请参考packt.live/3g7rK0w

你还可以在packt.live/2PZbcNV上在线运行这个示例。

在下一个练习中,我们将使用这些直觉推导出一个新的特征。

练习 3.04:特征工程——从现有特征中创建新特征

在这个练习中,我们将结合在练习 3.03中分析的各个变量,特征工程——单独特征的探索,推导出一个新的特征,叫做资产指数。创建资产指数的一种方法是根据客户的资产或负债来分配权重。

例如,较高的银行余额或房产拥有权将对总体资产指数产生积极影响,因此会被分配更高的权重。相反,贷款的存在将是负债,因此需要分配较低的权重。如果客户有房子,我们赋予权重为 5,若没有则为 1。类似地,如果客户有贷款,我们赋予权重为 1,如果没有贷款,则为 5:

  1. 打开一个新的 Colab 笔记本。

  2. 导入 pandas 和 numpy 包:

    import pandas as pd
    import numpy as np
    
  3. 将数据集的链接赋值给一个名为'file_url'的变量。

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter03/bank-full.csv'
    
  4. 使用.read_csv()函数读取银行数据集:

    # Reading the banking data
    bankData = pd.read_csv(file_url,sep=";")
    
  5. 我们将遵循的第一步是标准化数值变量。可以通过以下代码片段来实现:

    # Normalizing data
    from sklearn import preprocessing
    x = bankData[['balance']].values.astype(float)
    
  6. 由于银行余额数据集包含数值数据,我们需要先对数据进行标准化。标准化的目的是将我们用于创建新特征的所有变量转换到一个共同的尺度。我们可以在这里使用的有效标准化方法叫做MinMaxScaler(),它将所有数值数据转换到 0 到 1 的范围内。MinMaxScaler函数可以在sklearnpreprocessing方法中找到:

    minmaxScaler = preprocessing.MinMaxScaler()
    
  7. 使用minmaxScaler对余额数据进行标准化转换:

    bankData['balanceTran'] = minmaxScaler.fit_transform(x)
    

    在这一步中,我们创建了一个名为'balanceTran'的新特征,用来存储标准化的银行余额值。

  8. 使用.head()函数打印数据的前几行:

    bankData.head()
    

    你应该得到以下输出:

    图 3.21:标准化银行余额数据

    图 3.21:标准化银行余额数据

  9. 创建标准化变量后,添加一个小值0.001,以消除变量中的 0 值。这个在以下代码片段中提到:

    # Adding a small numerical constant to eliminate 0 values
    bankData['balanceTran'] = bankData['balanceTran'] + 0.00001
    

    添加这个小值的目的是因为,在后续步骤中,我们将把三个转化后的变量相乘以形成一个综合指数。为了避免变量值在乘法运算中变为 0,我们加上了这个小值。

  10. 现在,按照开始此练习时讨论的加权方法,为贷款和住房引入转换变量的两列附加列:

    # Let us transform values for loan data
    bankData['loanTran'] = 1
    # Giving a weight of 5 if there is no loan
    bankData.loc[bankData['loan'] == 'no', 'loanTran'] = 5
    bankData.head()
    

    你应该得到以下输出:

    图 3.22:包含转换变量的附加列

    图 3.22:包含转换变量的附加列

    我们根据加权方法对贷款数据进行了转换。当客户有贷款时,赋予权重1,没有贷款时,赋予权重515是我们赋予的直观权重。我们所赋予的权重可以根据你所提供的业务背景有所不同。

  11. 现在,转换Housing 数据中的数值,如下所示:

    # Let us transform values for Housing data
    bankData['houseTran'] = 5
    
  12. 如果客户有房产,则赋予权重1并打印结果,如以下代码片段所示:

    bankData.loc[bankData['housing'] == 'no', 'houseTran'] = 1
    print(bankData.head())
    

    你应该得到以下输出:

    图 3.23:转换贷款和住房数据

    图 3.23:转换贷款和住房数据

    一旦所有转换后的变量都创建完成,我们可以将所有转换后的变量相乘,生成一个新的指数,称为assetIndex。这是一个综合指数,表示所有三个变量的综合效果。

  13. 现在,创建一个新的变量,这是所有转换后变量的乘积:

    """ 
    Let us now create the new variable which is a product of all 
    these
    """
    bankData['assetIndex'] = bankData['balanceTran'] \
                             * bankData['loanTran'] \
                             * bankData['houseTran']
    bankData.head()
    

    你应该得到以下输出:

    图 3.24:创建综合指数

    图 3.24:创建综合指数

  14. 探索相对于综合指数的倾向性。

    我们观察资产指数与定期存款购买倾向之间的关系。我们采用类似的策略,将资产指数的数值转换为序数值,通过分位数将数值映射到定期存款购买的倾向性上,如练习 3.03中所述,特征工程 – 探索单个特征

    # Finding the quantile
    np.quantile(bankData['assetIndex'],[0.25,0.5,0.75])
    

    你应该得到以下输出:

    图 3.25:将数值转换为序数值

    图 3.25:将数值转换为序数值

  15. 接下来,根据以下代码片段从assetindex数据创建分位数:

    bankData['assetClass'] = 'Quant1'
    bankData.loc[(bankData['assetIndex'] > 0.38) \
                  & (bankData['assetIndex'] < 0.57), \
                  'assetClass'] = 'Quant2'
    bankData.loc[(bankData['assetIndex'] > 0.57) \
                  & (bankData['assetIndex'] < 1.9), \
                  'assetClass'] = 'Quant3'
    bankData.loc[bankData['assetIndex'] > 1.9, \
                 'assetClass'] = 'Quant4'
    bankData.head()
    bankData.assetClass[bankData['assetIndex'] > 1.9] = 'Quant4'
    bankData.head()
    

    你应该得到以下输出:

    图 3.26:资产指数的分位数

    图 3.26:资产指数的分位数

  16. 计算每个资产类别的总额及类别计数,如以下代码片段所示:

    # Calculating total of each asset class
    assetTot = bankData.groupby('assetClass')['y']\
                       .agg(assetTot='count').reset_index()
    # Calculating the category wise counts
    assetProp = bankData.groupby(['assetClass', 'y'])['y']\
                        .agg(assetCat='count').reset_index()
    
  17. 接下来,合并两个数据框:

    # Merging both the data frames
    assetComb = pd.merge(assetProp, assetTot, on = ['assetClass'])
    assetComb['catProp'] = (assetComb.assetCat \
                            / assetComb.assetTot)*100
    assetComb
    

    你应该得到以下输出:

    图 3.27:综合指数关系映射

图 3.27:综合指数关系映射

从我们创建的新特征中可以看到,18.88%(我们从catProp中得到这个百分比)在Quant2中的客户购买了定期存款,而Quant1为 10.57%,Quant3为 8.78%,Quant4为 9.28%。由于Quant2中购买定期存款的客户比例最高,我们可以得出结论,Quant2中的客户购买定期存款的倾向高于其他所有客户。

注意

要访问此特定部分的源代码,请参见packt.live/316hUrO

你还可以在packt.live/3kVc7Ny上运行这个示例。

与我们刚完成的练习类似,你应该根据业务直觉考虑从现有变量中创建的新变量。基于业务直觉创建新特征是基于业务的特征工程的核心。在下一节中,我们将介绍另一种特征工程类型——基于数据的特征工程。

基于数据的特征工程

上一节讨论了基于业务的特征工程。除了从业务角度推导出的特征外,从数据结构的角度进行特征工程转换也是至关重要的。我们将探讨识别数据结构的不同方法,并快速了解一些数据转换技术。

快速查看数据类型和描述性总结

查看数据类型,如类别型或数值型,然后推导出汇总统计数据,是在进行一些后续特征工程步骤之前快速浏览数据的好方法。我们来看看来自我们数据集的一个示例:

# Looking at Data types
print(bankData.dtypes)
# Looking at descriptive statistics
print(bankData.describe())

你应该得到以下输出:

图 3.28:显示数据集中不同数据类型的输出

图 3.28:显示数据集中不同数据类型的输出

在前面的输出中,你可以看到数据集中不同类型的信息及其对应的数据类型。例如,age是整数,day也是整数。

以下输出是描述性总结统计数据,显示了各特征的一些基本度量,如均值标准差计数和相应的分位数值

图 3.29:数据类型和描述性总结

图 3.29:数据类型和描述性总结

描述性总结的目的是快速了解数据的分布情况和一些基本统计信息,如均值和标准差。了解汇总统计数据对思考每个变量需要进行什么样的转换至关重要。

例如,在前面的练习中,我们根据分位数值将数值数据转换为分类变量。转换变量的直觉来自于我们从数据集中可以推导出的快速汇总统计。

在接下来的章节中,我们将查看相关性矩阵和可视化。

相关性矩阵与可视化

如你所知,相关性是一个度量,用于表示两个变量是如何一起波动的。任何接近 1 或为 1 的相关性值,表示这两个变量之间有很强的相关性。高度相关的变量有时可能会影响模型的准确性,在许多情况下,我们会决定消除这些变量或将它们合并成复合变量或交互变量。

让我们在接下来的练习中看看如何生成并可视化数据的相关性。

练习 3.05:利用银行数据生成相关性图并找到数据中的相关性

在这个练习中,我们将创建一个相关性图并分析银行数据集的结果。

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本,安装 pandas 包并加载银行数据:

    import pandas as pd
    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter03/bank-full.csv'
    bankData = pd.read_csv(file_url, sep=";")
    
  2. 现在,按照此处的说明,从 pandas 导入 set_option 库:

    from pandas import set_option
    

    set_option 函数用于定义许多操作的显示选项。

  3. 接下来,创建一个变量来存储诸如 'age','balance','day','duration','campaign','pdays','previous' 等数值变量,如以下代码片段所示。相关性图只能通过数值数据提取。这就是为什么必须单独提取数值数据:

    bankNumeric = bankData[['age','balance','day','duration',\
                            'campaign','pdays','previous']]
    
  4. 现在,使用 .corr() 函数找到数据集的相关性矩阵:

    set_option('display.width',150)
    set_option('precision',3)
    bankCorr = bankNumeric.corr(method = 'pearson')
    bankCorr
    

    你应该得到以下输出:

    图 3.30:相关性矩阵

    图 3.30:相关性矩阵

    我们使用的相关性方法是皮尔逊相关系数。从相关性矩阵中可以看到,主对角线上的元素的相关性为 1\。这是因为对角线元素表示的是变量与自身的相关性,显然永远为 1\。这就是皮尔逊相关系数。

  5. 现在,绘制数据:

    from matplotlib import pyplot
    corFig = pyplot.figure()
    figAxis = corFig.add_subplot(111)
    corAx = figAxis.matshow(bankCorr,vmin=-1,vmax=1)
    corFig.colorbar(corAx)
    pyplot.show()
    

    你应该得到以下输出:

    图 3.31:相关性图

图 3.31:相关性图

在此代码块中,我们使用了许多绘图参数。pyplot.figure() 是要实例化的绘图类。.add_subplot() 是绘图的网格参数。例如,111 表示第一个子图的 1 x 1 网格。.matshow() 函数用于显示图形,vminvmax 参数用于规范化图中的数据。

让我们看一下相关矩阵的图,以便更快速地识别相关变量。几个明显的候选变量包括'balance''balanceTran'之间的高相关性,以及'asset index'与我们在之前练习中创建的许多转换变量之间的高相关性。除此之外,没有太多变量高度相关。

注意

要访问这一特定部分的源代码,请参考packt.live/3kXr9SK

你也可以在packt.live/3gbfbkR上在线运行这个示例。

在本次练习中,我们开发了一个相关性图,帮助我们可视化变量之间的相关性。

数据的偏度

另一个特征工程的领域是偏度。偏斜数据是指数据在某个方向上发生了偏移。偏度可能会导致机器学习模型的性能下降。许多机器学习模型假设数据是正态分布的,或者数据结构遵循高斯结构。任何偏离高斯结构的情况(即著名的钟形曲线)都可能影响模型的表现。一个非常有效的特征工程方法是通过观察数据的偏度,并通过对数据进行归一化来纠正偏度。我们可以通过绘制直方图和密度图来可视化偏度。我们将研究这些技术中的每一个。

让我们看一下下面的示例。在这里,我们使用.skew()函数来查找数据的偏度。例如,为了查找我们bank-full.csv数据集中数据的偏度,我们可以执行以下操作:

# Skewness of numeric attributes
bankNumeric.skew()

注意

这段代码涉及到bankNumeric数据,因此你应该确保在与前面练习相同的笔记本中进行操作。

你应该获得如下输出:

图 3.32:偏度的程度

图 3.32:偏度的程度

上述矩阵为偏度指标。任何接近 0 的值表示偏度较低。正值表示右偏,负值表示左偏。显示出较高右偏和左偏的变量是进一步通过归一化进行特征工程的候选变量。现在让我们通过绘制直方图和密度图来可视化偏度。

直方图

直方图是绘制数据分布并识别数据偏度(如果有的话)的有效方法。这里列出了bankData两个列的直方图输出。直方图是使用matplotlib中的pyplot包通过.hist()函数绘制的。我们希望包含的子图数量由.subplots()函数控制。子图中的(1,2)表示一行两列。标题通过set_title()函数设置:

# Histograms
from matplotlib import pyplot as plt
fig, axs = plt.subplots(1,2)
axs[0].hist(bankNumeric['age'])
axs[0].set_title('Distribution of age')
axs[1].hist(bankNumeric['balance'])
axs[1].set_title('Distribution of Balance')
# Ensure plots do not overlap
plt.tight_layout()

你应该获得如下输出:

图 3.33:显示生成直方图的代码

图 3.33:显示生成直方图的代码

从直方图中我们可以看到,age变量的分布接近钟形曲线,偏态较低。相比之下,资产指数显示出较高的右偏,意味着它是归一化的更有可能的候选者。

密度图

密度图有助于可视化数据的分布。可以使用kind = 'density'参数来创建密度图:

from matplotlib import pyplot as plt
# Density plots
bankNumeric['age'].plot(kind = 'density', subplots = False, \
                        layout = (1,1))
plt.title('Age Distribution')
plt.xlabel('Age')
plt.ylabel('Normalised age distribution')
pyplot.show()

你应该看到以下输出:

图 3.34:展示生成密度图的代码

图 3.34:展示生成密度图的代码

密度图有助于更平滑地展示数据的分布。从年龄的密度图中,我们可以看到它的分布类似于钟形曲线。

其他特征工程方法

到目前为止,我们一直在查看各种描述性统计和可视化方法,这些都是对数据结构应用许多特征工程技术的前奏。在练习 3.02关于年龄与定期贷款倾向的商业假设测试中,我们使用了最小-最大缩放器对数据进行归一化,探讨了其中一种特征工程技术。

现在我们将介绍另外两种类似的数据转换技术,即标准化缩放器和归一化器。标准化缩放器将数据标准化为均值为 0,标准差为 1。均值是数据的平均值,标准差是衡量数据分布程度的指标。通过将数据标准化为相同的均值和标准差,可以实现不同数据分布之间的比较。

归一化器函数对数据的长度进行归一化。这意味着每行中的每个值都会被该行向量的归一化值除以,从而实现对该行的归一化。归一化器函数应用于行,而标准化缩放器应用于列。归一化器和标准化缩放器是应用于数据的关键特征工程步骤,在下游建模步骤之前需要对数据进行处理。让我们来看看这两种技术:

# Standardize data (0 mean, 1 stdev)
from sklearn.preprocessing import StandardScaler
from numpy import set_printoptions
scaling = StandardScaler().fit(bankNumeric)
rescaledNum = scaling.transform(bankNumeric)
set_printoptions(precision = 3)
print(rescaledNum)

你应该看到以下输出:

图 3.35:标准化数据后的输出

图 3.35:标准化数据后的输出

以下代码展示了使用归一化器数据传输技术:

# Normalizing Data (Length of 1)
from sklearn.preprocessing import Normalizer
normaliser = Normalizer().fit(bankNumeric)
normalisedNum = normaliser.transform(bankNumeric)
set_printoptions(precision = 3)
print(normalisedNum)

你应该看到以下输出:

图 3.36:归一化器的输出

图 3.36:归一化器的输出

标准化缩放器的输出在列之间进行了归一化。输出将有 11 列,对应于 11 个数值列(年龄、余额、天数、持续时间等)。如果观察输出,我们可以看到每一列的值都已经标准化,使得均值为 0,标准差为 1。通过这种方式转换数据,我们可以轻松地进行列间比较。

例如,在age变量中,我们的数据范围从 18 到 95. 相比之下,余额数据的范围从-8,019 到 102,127. 我们可以看到这两个变量的数据范围不同,无法直接比较。标准化函数将这些尺度差异很大的数据点转换为共同的尺度,从而能够比较数据的分布。归一化则将每一行重新调整,使其成为长度为 1 的向量。

我们必须思考的一个大问题是:为什么我们必须对数据进行标准化或归一化?许多机器学习算法在特征具有相似的尺度或呈正态分布时会更快收敛。标准化在假设输入变量具有高斯结构的算法中更为有用。线性回归、逻辑回归和线性判别分析等算法都属于这一类。归一化技术更适合处理稀疏数据集(包含大量零值的数据集),特别是在使用诸如 k 最近邻或神经网络的算法时。

特征工程总结

在本节中,我们从商业角度和数据结构角度研究了特征工程的过程。特征工程是数据科学项目生命周期中非常重要的一步,有助于确定我们构建模型的真实性。如在练习 3.02中所见,年龄与定期贷款倾向的商业假设检验,我们将对领域的理解和直觉转化为智能特征。让我们总结一下我们遵循的过程:

  1. 我们通过 EDA 从商业角度获得了直觉。

  2. 基于商业直觉,我们设计了一个新特性,它是三个其他变量的组合。

  3. 我们验证了新特征的组成变量的影响,并设计了应用权重的方法。

  4. 将有序数据转换为相应的权重。

  5. 通过使用适当的归一化方法对数值数据进行转换。

  6. 将所有三个变量组合成一个新特征。

  7. 观察了综合指数与购买定期存款倾向之间的关系,并从中得出了我们的直觉。

  8. 探索了从数据中可视化和提取汇总统计信息的技术。

  9. 确定了将数据转化为特征工程数据结构的技术。

现在我们已经完成了特征工程步骤,接下来的问题是我们接下来怎么做,我们创建的新特征的相关性是什么?正如您将在后续章节中看到的,我们创建的新特征将用于建模过程中。前述练习是我们可以遵循的创建新特征的示例。将会有多条类似的线索,这些应该被视为基于更多的领域知识和理解。我们构建的模型的真实性将依赖于我们可以通过将业务知识转化为数据来构建的所有这些智能特征。

构建二元分类模型使用逻辑回归函数

数据科学的核心是将业务问题映射到其数据元素,然后转换这些数据元素以获得我们期望的业务结果。在前面的章节中,我们讨论了如何对数据元素进行必要的转换。数据元素的正确转换可以通过下游建模过程大大影响正确业务结果的生成。

让我们从我们使用案例的角度来看业务结果生成过程。在我们的使用案例中,期望的业务结果是识别那些有可能购买定期存款的客户。要正确识别哪些客户有可能购买定期存款,我们首先需要学习当客户拥有时在识别过程中有帮助的特征或特性。通过机器学习来学习这些特性就是我们所达到的目标。

到现在为止,您可能已经意识到机器学习的目标是估计输出变量和输入变量之间的映射函数(f)。在数学形式上,这可以写成如下形式:

图 3.37:数学形式中的映射函数

图 3.37:数学形式中的映射函数

让我们从我们使用案例的角度来看这个方程。

Y 是因变量,即我们预测客户是否有购买定期存款的可能性。

X 是独立变量,即诸如年龄、教育和婚姻状况等属性,是数据集的一部分。

f() 是一个连接数据各种属性到客户是否购买定期存款的概率的函数。这个函数是在机器学习过程中学习的。这个函数是应用于每个属性的不同系数或参数的组合,以获得定期存款购买的概率。让我们用银行数据使用案例的简单例子来揭开这个概念。

为了简化起见,我们假设只有两个属性,年龄和银行余额。基于这些,我们需要预测客户是否可能购买定期存款。假设年龄为 40 岁,余额为 $1,000。根据所有这些属性值,假设映射方程如下:

图 3.38:更新后的映射方程

图 3.38:更新后的映射方程

使用前面的方程,我们得到:

Y = 0.1 + 0.4 * 40 + 0.002 * 1000

Y = 18.1

现在,你可能会问,我们得到的是一个实数,这如何代表一个客户是否会购买定期存款的决策呢?这就是决策边界概念的所在。我们还假设,在分析数据时,我们已经确定,如果 Y 的值超过 15(在这个例子中假设的值),那么客户可能会购买定期存款,否则他们不会购买定期存款。这意味着,根据这个例子,客户很可能会购买定期存款。

现在我们来看一下这个例子中的动态,并尝试解读其中的概念。像 0.1、0.4 和 0.002 这样应用于每个属性的值是系数。这些系数与连接系数和变量的方程一起,构成了我们从数据中学习的函数。机器学习的本质就是从提供的数据中学习这些内容。所有这些系数和函数也可以被称为另一个常见的名称——模型。模型是数据生成过程的近似。在机器学习过程中,我们试图尽可能接近生成我们所分析的数据的真实模型。为了学习或估计数据生成模型,我们使用不同的机器学习算法。

机器学习模型可以大致分为两种类型,参数模型和非参数模型。参数模型是我们假设我们试图学习的函数的形式,然后从训练数据中学习系数。通过假设函数的形式,我们简化了学习过程。

为了更好地理解这个概念,让我们以线性模型为例。对于线性模型,映射函数的形式如下:

图 3.39:线性模型映射函数

图 3.39:线性模型映射函数

C**0M**1M**2 是影响直线截距和斜率的系数。X**1X**2 是输入变量。我们在这里做的是假设数据生成模型是线性模型,然后利用数据估计系数,从而生成预测。通过假设数据生成模型,我们简化了整个学习过程。然而,这些简单的过程也有其陷阱。只有当潜在函数是线性或类似线性时,我们才能获得好的结果。如果关于模型形式的假设是错误的,我们注定会得到不好的结果。

一些参数模型的例子包括:

  • 线性回归与逻辑回归

  • 朴素贝叶斯

  • 线性支持向量机

  • 感知机

不对函数做强假设的机器学习模型被称为非参数模型。在没有假设形式的情况下,非参数模型可以自由地从数据中学习任何函数形式。非参数模型通常需要大量的训练数据来估计潜在函数。一些非参数模型的例子包括:

  • 决策树

  • K-近邻

  • 神经网络

  • 支持向量机与高斯核

逻辑回归揭秘

逻辑回归是一个类似于前一章中讨论的线性回归的线性模型。逻辑回归的核心是 Sigmoid 函数,它将任何实数值压缩到 0 到 1 之间,这使得该函数非常适合用来预测概率。逻辑回归函数的数学表达式可以写成如下形式:

图 3.40: 逻辑回归函数

图 3.40: 逻辑回归函数

这里,Y 是顾客是否可能购买定期存款的概率。

C0 + M1 * X1 + M2 * X2 这一表达式与我们在前一章中看到的线性回归函数非常相似。正如你现在所学的,线性回归函数给出的是实数值输出。为了将实数值输出转换为概率,我们使用逻辑函数,它的形式如下:

图 3.41: 将实数值输出转换为概率的表达式

图 3.41: 将实数值输出转换为概率的表达式

这里,e 是自然对数。我们不会深入探讨背后的数学原理;然而,应该意识到,使用逻辑函数,我们可以将实数值输出转换为概率函数。

现在让我们来看一下我们试图解决的业务问题中的逻辑回归函数。在这个业务问题中,我们正在尝试预测顾客是否会购买定期存款的概率。为了做到这一点,让我们回到我们从问题陈述中推导出的例子:

图 3.42:更新后的逻辑回归函数业务问题陈述

图 3.42:更新后的逻辑回归函数,结合了业务问题陈述

添加以下数值后,我们得到Y = 0.1 + 0.4 * 40 + 0.002 * 100

为了获得概率,我们必须使用逻辑函数来转化这个问题陈述,如下所示:

图 3.43:转化后的问题陈述,用于计算概率使用逻辑函数

图 3.43:使用逻辑函数计算概率的转化问题陈述

应用此方法后,我们得到一个Y = 1的值,这表示客户购买定期存款的概率为 100%。正如前面例子所讨论的那样,模型的系数如 0.1、0.4 和 0.002 是我们通过逻辑回归算法在训练过程中学习到的。

评估模型性能的度量指标

作为数据科学家,你总是需要对你构建的模型做出决策。这些评估是基于对预测结果的各种度量来完成的。在这一部分,我们介绍一些用于评估模型性能的重要度量指标。

注意

模型性能将在第六章中更详细地讲解,如何评估性能。这一部分为你介绍了如何处理分类模型。

混淆矩阵

正如你将会学到的,我们根据模型在测试集上的表现来评估它。测试集有其标签,我们称之为真实标签,使用模型后,我们还会为测试集生成预测结果。模型性能的评估就是对比真实标签和预测结果。让我们通过一个虚拟的测试集来看这个过程:

图 3.44:生成混淆矩阵

图 3.44:生成混淆矩阵

上面的表格展示了一个包含七个示例的虚拟数据集。第二列是实际标签(真实标签),第三列包含我们的预测结果。从数据中可以看出,四个示例被正确分类,三个示例被错误分类。

混淆矩阵生成了预测与实际标签之间的对比,如下表所示:

图 3.45:混淆矩阵

图 3.45:混淆矩阵

从表格中可以看出,有五个示例的标签(真实标签)为Yes,剩下的两个示例标签为No

混淆矩阵的第一行是对标签Yes的评估。True positive表示那些真实标签和预测标签均为Yes的示例(示例 1、3 和 5)。False negative表示那些真实标签为Yes,但被错误预测为No的示例(示例 2 和 7)。

同样,混淆矩阵的第二行评估标签No的表现。假正例是指那些真实标签为No,但被错误分类为Yes的示例(示例 6)。真负例是指那些真实标签和预测值都为No的示例(示例 4)。

混淆矩阵的生成用于计算许多矩阵,如准确性和分类报告,后续将详细解释。它基于像准确性或其他分类报告中显示的详细指标(如精确度或召回率)等度量标准,用于测试模型。我们通常选择这些指标值最高的模型。

准确性

准确性是评估的第一级,我们将依赖它来快速检查模型的表现。参考前面的表格,准确性可以表示如下:

图 3.46:表示准确性的函数

图 3.46:表示准确性的函数

准确性是指所有预测中正确预测的比例。

分类报告

分类报告输出三个关键指标:精确度召回率F1 分数

精确度是指真正例数与真正例数和假正例数之和的比例:

图 3.47:精确度比例

图 3.47:精确度比例

精确度是衡量在所有预测为正的实例中,实际为正的实例所占的比例。

召回率是指真正例数与真正例数和假负例数之和的比例:

图 3.48:召回率

图 3.48:召回率

召回率表现了模型识别所有真正例的能力。

F1 分数是精确度和召回率的加权得分。F1 分数为 1 表示最佳表现,0 表示最差表现。

在下一节中,我们将深入了解数据预处理,这是处理数据并得出结论时的重要过程。

数据预处理

数据预处理在数据科学项目的生命周期中起着重要作用。这些过程通常是数据科学生命周期中最耗时的部分。谨慎实施预处理步骤至关重要,将对数据科学项目的结果产生重大影响。

各种预处理步骤包括以下内容:

  • 数据加载:这涉及将数据从不同的来源加载到笔记本中。

  • 数据清洗:数据清洗过程包括移除异常数据,例如特殊字符、重复数据,并识别数据集中缺失的部分。数据清洗是数据科学过程中最耗时的步骤之一。

  • 数据插补:数据插补是用新的数据点填补缺失数据。

  • 转换数据类型:数据集将包含不同类型的数据,如数值数据、类别数据和字符数据。运行模型时需要对数据类型进行转换。

    注意

    数据处理将在本书接下来的章节中详细讨论。

我们将在后续部分以及练习 3.06中实现其中一些预处理步骤,《用于预测银行定期存款购买倾向的逻辑回归模型》

练习 3.06:用于预测银行定期存款购买倾向的逻辑回归模型

在本次练习中,我们将构建一个逻辑回归模型,用于预测定期存款购买的倾向。这个练习分为三个部分。第一部分是数据预处理,第二部分处理训练过程,最后一部分将用于预测、指标分析以及制定进一步改进模型的策略。

你从数据预处理开始。

在这一部分中,我们将首先加载数据,将有序数据转换为虚拟变量数据,然后将数据拆分为训练集和测试集,以便进入随后的训练阶段:

  1. 打开一个 Colab 笔记本,挂载驱动器,安装必要的包,并加载数据,方法与之前的练习相同:

    import pandas as pd
    import altair as alt
    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter03/bank-full.csv'
    bankData = pd.read_csv(file_url, sep=";")
    
  2. 现在,加载库函数和数据:

    from sklearn.linear_model import LogisticRegression
    from sklearn.model_selection import train_test_split
    
  3. 现在,找出数据类型:

    bankData.dtypes
    

    你应该得到以下输出:

    图 3.49:数据类型

    图 3.49:数据类型

  4. 将有序数据转换为虚拟变量数据。

    正如你在数据集中看到的,我们有两种类型的数据:数值数据和有序数据。机器学习算法需要数据的数值表示,因此我们必须通过创建虚拟变量将有序数据转换为数值形式。虚拟变量的值为 1 或 0,取决于该类别是否存在。我们用于将有序数据转换为数值形式的函数是pd.get_dummies()。这个函数将数据结构转换为长格式或横向格式。所以,如果一个变量有三个类别,就会创建三个新的虚拟变量,分别对应每个类别。

    每个变量的值将是 1 或 0,具体取决于该类别是否在变量中作为示例出现。我们来看一下执行此操作的代码:

    """
    Converting all the categorical variables to dummy variables
    """
    bankCat = pd.get_dummies\
              (bankData[['job','marital',\
                         'education','default','housing',\
                         'loan','contact','month','poutcome']])
    bankCat.shape
    

    你应该得到以下输出:

    (45211, 44)
    

    现在,我们有了一个新的数据子集,对应于已经转换为数值形式的类别数据。此外,原始数据集中还有一些数值变量,它们不需要任何转换。转换后的类别数据和原始的数值数据需要合并,以获取所有的原始特征。为了合并这两者,让我们首先从原始数据框中提取数值数据。

  5. 现在,分离数值变量:

    bankNum = bankData[['age','balance','day','duration',\
                        'campaign','pdays','previous']]
    bankNum.shape
    

    你应该得到以下输出:

    (45211, 7)
    
  6. 现在,准备XY变量,并打印Y的形状。X变量是转换后的类别变量和分离出的数值数据的连接:

    # Preparing the X variables
    X = pd.concat([bankCat, bankNum], axis=1)
    print(X.shape)
    # Preparing the Y variable
    Y = bankData['y']
    print(Y.shape)
    X.head()
    

    下方显示的输出已被截断:

    图 3.50 合并类别和数值数据框

    图 3.50 合并类别和数值数据框

    一旦 DataFrame 创建完成,我们可以将数据分割为训练集和测试集。我们指定 DataFrame 应该以什么样的比例分割成训练集和测试集。

  7. 将数据分割为训练集和测试集:

    # Splitting the data into train and test sets
    X_train, X_test, y_train, y_test = train_test_split\
                                       (X, Y, test_size=0.3, \
                                        random_state=123)
    

    现在,数据已经为建模任务做好了准备。接下来,我们开始建模。

    在这一部分,我们将使用之前创建的训练集来训练模型。首先,我们调用logistic regression函数,然后用训练集数据拟合模型。

  8. 定义LogisticRegression函数:

    bankModel = LogisticRegression()
    bankModel.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 3.51:拟合模型的参数

    图 3.51:拟合模型的参数

  9. 现在,模型已经创建,使用它对测试集进行预测,然后获取预测的准确度:

    pred = bankModel.predict(X_test)
    print('Accuracy of Logistic regression model' \
          'prediction on test set: {:.2f}'\
          .format(bankModel.score(X_test, y_test)))
    

    你应该得到以下输出:

    图 3.52:使用模型进行预测

    图 3.52:使用模型进行预测

  10. 从初步来看,90%的准确度给人一种模型已经很好地逼近数据生成过程的印象。真的是这样吗?让我们通过生成模型的指标,仔细看看预测的细节。我们将使用两个生成指标的函数,混淆矩阵和分类报告:

    # Confusion Matrix for the model
    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到以下格式的输出;然而,由于建模任务的随机性,数值可能有所不同:

    图 3.53:生成混淆矩阵

    图 3.53:生成混淆矩阵

    注意

    你得到的最终结果将与这里看到的不同,因为这取决于你使用的系统。这是因为建模部分具有随机性,结果总会有所不同。

  11. 接下来,我们生成一个classification_report

    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到类似的输出;然而,由于建模过程中的变异性,数值会有所不同:

    图 3.54:混淆矩阵和分类报告

图 3.54:混淆矩阵和分类报告

注意

要访问此特定部分的源代码,请参考packt.live/2CGFYYU

你也可以在packt.live/3aDq8KX上在线运行这个例子。

从指标中可以看出,在总共 11,998 个no的样本中,有 11,754 个被正确分类为no,剩下的 244 个被错误分类为yes。这给出的召回率是11,754/11,998,接近 98%。从精度的角度来看,在总共 12,996 个被预测为no的样本中,只有 11,754 个是真正的no,这使得我们的精度为 11,754/12,996,或者 90%。

然而,yes的指标给出了不同的结果。在总共 1,566 个yes的样本中,只有 324 个被正确识别为yes。这给出的召回率是324/1,566 = 21%。精度是324 / (324 + 244) = 57%

从整体准确度的角度来看,可以通过以下公式计算:正确分类的样本 / 总样本 = (11754 + 324) / 13564 = 89%

当你只看准确度时,指标可能看起来不错。然而,仔细看细节,我们可以看到分类器实际上在分类yes样本时表现不佳。该分类器已被训练为主要预测no值,而从业务角度来看,这是没有用的。从业务角度来看,我们主要希望预测到yes的样本,以便能够将这些样本作为目标进行精准营销,尝试销售定期存款。然而,凭借当前的结果,我们似乎未能很好地帮助业务通过定期存款销售增加收入。

在本练习中,我们进行了数据预处理,然后进行了训练过程,最后,我们得到了有用的预测结果、指标分析,并提出了进一步改进模型的策略。

我们现在构建的模型是第一个模型,或者说是基准模型。下一步是通过不同的策略尝试改进基准模型。一个这样的策略是进行特征工程,构建包含新特征的新模型。让我们在下一活动中实现这一点。

活动 3.02:模型迭代 2——使用特征工程变量的逻辑回归模型

作为银行的数据科学家,你创建了一个基准模型来预测哪些客户可能会购买定期存款。然而,管理层希望改善基准模型中的结果。在练习 3.04中,特征工程——从现有特征创建新特征,你与营销和运营团队讨论了业务场景,并通过特征工程创建了一个新变量assetIndex,该变量由三个原始变量组成。现在,你正在对特征工程后的变量拟合另一个逻辑回归模型,并努力改进结果。

在此活动中,你将进行一些变量的特征工程,以验证它们对预测结果的影响。

步骤如下:

  1. 打开用于练习 3.04特征工程——从现有特征创建新特征的 Colab 笔记本,并执行该练习中的所有步骤。

  2. 使用pd.get_dummies()函数为分类变量创建虚拟变量。排除诸如贷款和住房等原始变量,它们用于创建新的变量assetIndex

  3. 选择数值变量,包括创建的新特征变量assetIndex

  4. 通过使用MinMaxScaler()函数对一些数值变量进行标准化处理。

  5. 使用pd.concat()函数连接数值变量和分类变量,然后创建XY变量。

  6. 使用train_test_split()函数拆分数据集,然后使用LogisticRegression()模型在新特征上拟合一个新模型。

  7. 在生成混淆矩阵和分类报告后分析结果。

    你应该获得以下输出:

    图 3.55:包含分类报告的预期输出

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_03_55.jpg)

图 3.55:包含分类报告的预期输出

分类报告将类似于这里展示的报告。然而,由于建模过程中的变动,数值可能会有所不同。

注意

本活动的解决方案可以在以下地址找到:packt.live/2GbJloz

现在让我们讨论接下来需要采取的步骤,以改进我们从两次迭代中得到的度量。

下一步

我们可以问的下一个明显问题是,在本章中我们已经实现的所有过程之后,我们应该走向哪里?让我们讨论可以采取的策略以进一步改进:

  • no类别。当类别不平衡时,分类器很可能会对多数类过拟合。这在我们的示例中也得到了验证。这也是为什么我们不应该仅仅通过查看准确率来得出分类器性能结论的原因。

    类别不平衡在许多应用场景中都很常见,如欺诈检测、医疗诊断和客户流失等。针对类别不平衡问题有不同的解决策略。我们将在第十三章不平衡数据集中处理类别不平衡的场景。

  • 特征工程:数据科学是一门迭代的科学。获得理想的结果将取决于我们进行的各种实验。改进初始模型的一个重要方向是通过特征工程对原始变量进行修改。我们进行了特征工程,并使用特征工程变量建立了模型。在创建新特征的过程中,我们跟随了一条与资产组合相关的新特征创建路径。同样,从业务角度出发,可能会有其他路径可以追踪,这些路径有可能会生成与我们创建的特征类似的更多特征。识别这些路径将取决于通过我们制定的假设以及我们进行的探索性分析来验证这些业务假设,从而扩展我们应用的业务知识。一种有效提升模型准确性的方法是识别更多的业务路径,然后通过创新的特征工程来构建模型。

  • 模型选择策略:当我们讨论参数模型和非参数模型时,我们提到如果真实的数据生成过程与我们假设的模型不一致,那么结果将会很差。在我们的案例中,我们假设了线性关系,因此采用了线性模型。如果真实的数据生成过程不是线性的呢?或者,是否存在其他更适合此用例的参数模型或非参数模型?这些都是在分析结果并尝试改进模型时需要考虑的问题。我们必须采取一种叫做模型抽查的策略,即使用不同的模型来处理用例,并在采用模型之前检查初始指标。在后续的章节中,我们将讨论其他建模技术,建议尝试使用其他类型的模型来抽查哪个建模技术更适合此用例。

总结

在本章中,我们从解决一个实际案例的角度学习了使用逻辑回归进行二分类。让我们总结一下本章的学习内容。我们介绍了分类问题,特别是二分类问题。我们还从通过业务发现过程预测定期存款倾向的角度来审视分类问题。在业务发现过程中,我们识别了影响业务结果的不同业务驱动因素。

从探索性分析中得出的直觉被用来从原始变量中创建新特征。我们建立了一个基准逻辑回归模型,并分析了指标,以确定未来的行动方向,然后通过引入特征工程变量建立第二个模型,对基准模型进行了迭代。

在掌握了解决二分类问题的技巧后,是时候迈出下一步了。在下一章中,你将处理多分类问题,并会介绍不同的解决这些问题的技术。

第四章:4. 使用随机森林进行多类分类

概述

本章将展示如何使用随机森林算法训练一个多类分类器。你还将看到如何评估多类模型的性能。

本章结束时,你将能够实现一个随机森林分类器,并调节超参数以提高模型性能。

引言

在上一章中,你学习了如何使用著名的逻辑回归算法构建一个二分类器。二分类器只能为其响应变量取两个不同的值,如 0 和 1 或是与否。多类分类任务则是二分类的扩展,其响应变量可以有超过两个不同的值。

在数据科学行业中,你经常会遇到多类分类问题。例如,如果你为 Netflix 或任何其他流媒体平台工作,你需要构建一个模型,能够根据关键属性如类型、时长或演员来预测电影的用户评分。可能的评分值列表包括:讨厌不喜欢中立喜欢非常喜欢。该模型的目标是从这五个可能值中预测正确的评分。

多类分类并不总是意味着响应变量是文本。在某些数据集中,目标变量可能已被编码成数字形式。以之前讨论的示例为例,评分可能会从 1 到 5 编码:1 表示讨厌,2 表示不喜欢,3 表示中立,以此类推。因此,在断定这是回归问题之前,首先理解该响应变量的含义非常重要。

在接下来的章节中,我们将学习如何训练第一个随机森林分类器。

训练一个随机森林分类器

在本章中,我们将使用随机森林算法进行多类分类。市场上有其他算法,但随机森林可能是最受欢迎的算法之一,尤其适用于此类项目。

随机森林方法最早由 Tin Kam Ho 于 1995 年提出,但它是在 2001 年由 Leo Breiman 首次发展的。

所以,随机森林并不是一种最近的算法。它已经使用了近二十年。但由于其优异的性能和简洁性,它的受欢迎程度并没有消退。

在本章的示例中,我们将使用一个名为“基于多传感器数据的活动识别系统”的数据集。该数据集最初由F. Palumbo, C. Gallicchio, R. Pucci, 和 A. Micheli, 《基于水库计算的多传感器数据融合的人类活动识别》,《环境智能与智能环境杂志》,2016 年,第 8 卷第 2 期,第 87-107 页分享。

注意

完整的数据集可以在这里找到:packt.live/3a5FI1s

让我们看看如何在这个数据集上训练一个随机森林分类器。首先,我们需要使用pandas从 GitHub 仓库加载数据,然后使用head()方法打印出数据集的前五行。

注意

本章中所有练习外的示例代码都与这个活动识别数据集相关。建议将所有这些示例中的代码输入并运行在一个单独的 Google Colab 笔记本中,并与练习笔记本分开。

import pandas as pd
file_url = 'https://raw.githubusercontent.com/PacktWorkshops'\
           '/The-Data-Science-Workshop/master/Chapter04/'\
           'Dataset/activity.csv'
df = pd.read_csv(file_url)
df.head()

输出将如下所示:

图 4.1:数据集的前五行

图 4.1:数据集的前五行

每一行代表一个人执行的活动,活动的名称存储在Activity列中。这个变量中有七种不同的活动:bending1bending2cyclinglyingsittingstandingWalking。其他六列是从传感器数据中获取的不同测量值。

在这个示例中,您将通过随机森林模型准确地预测目标变量('Activity'),其特征是六个其他列。例如,对于前面的示例中的第一行,模型将接收以下特征作为输入,并预测'bending1'类别:

图 4.2:数据集第一行的特征

图 4.2:数据集第一行的特征

但在此之前,我们需要做一些数据准备工作。sklearn包(我们将用它来训练随机森林模型)要求目标变量和特征变量分开。因此,我们需要使用.pop()方法从pandas中提取响应变量。.pop()方法提取指定的列并将其从数据框中删除:

target = df.pop('Activity')

现在,响应变量包含在名为target的变量中,所有特征存储在名为df的数据框中。

现在我们将数据集分割为训练集和测试集。模型使用训练集来学习预测响应变量的相关参数。测试集用于检查模型是否能够准确预测未见过的数据。当模型只学习了与训练集相关的模式,并且对测试集做出了不正确的预测时,我们称模型发生了过拟合。在这种情况下,模型在训练集上的表现会明显高于测试集。理想情况下,我们希望训练集和测试集的表现水平非常相似。这个话题将在第七章《机器学习模型的泛化》中深入讨论。

sklearn包提供了一个名为train_test_split()的函数,用于随机地将数据集分割成两个不同的子集。我们需要为此函数指定以下参数:特征变量和目标变量、测试集的比例(test_size)以及random_state,以便在需要重新运行代码时获得可重复的结果:

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split\
                                   (df, target, test_size=0.33, \
                                    random_state=42)

train_test_split()函数有四个不同的输出:训练集的特征、训练集的目标变量、测试集的特征和测试集的目标变量。

现在我们已经准备好了训练集和测试集,可以开始建模了。让我们首先从sklearn.ensemble导入RandomForestClassifier类:

from sklearn.ensemble import RandomForestClassifier

现在我们可以使用一些超参数实例化随机森林分类器。请记住,在第一章,Python 中的数据科学入门中提到,超参数是模型无法学习的参数,而是由数据科学家设置的,用来调整模型的学习过程。这个主题将在第八章,超参数调优中进行更深入的讲解。现在,我们只需指定random_state值。在接下来的章节中,我们将介绍一些关键的超参数:

rf_model = RandomForestClassifier(random_state=1, \
                                  n_estimators=10)

下一步是使用训练数据对模型进行训练(也叫做拟合)。在此过程中,模型将尝试学习响应变量与自变量之间的关系,并保存学到的参数。我们需要将特征和目标变量作为参数指定:

rf_model.fit(X_train, y_train)

输出结果如下:

图 4.3:训练后的随机森林日志

图 4.3:训练后的随机森林日志

现在模型已经完成训练,我们可以使用它学到的参数对我们将提供的输入数据进行预测。在以下示例中,我们使用的是来自训练集的特征:

preds = rf_model.predict(X_train)

现在我们可以打印这些预测结果:

preds

输出结果如下:

图 4.4:随机森林算法在训练集上的预测结果

图 4.4:随机森林算法在训练集上的预测结果

该输出显示了模型分别预测了前 3 个观测值为lyingbending1cycling,以及最后 3 个观测值为cyclingbending1standing。默认情况下,Python 会截断长列表的输出,这就是为什么这里只显示了六个值。

这些基本上就是训练随机森林分类器所需的关键步骤。这相当简单,对吧?训练机器学习模型非常容易,但获取有意义且准确的结果才是挑战所在。在下一节中,我们将学习如何评估已训练模型的性能。

评估模型的性能

既然我们已经知道如何训练随机森林分类器,接下来就该检查我们是否做得好。我们想要得到一个可以做出极其准确预测的模型,因此我们需要使用某种度量来评估其性能。

对于分类问题,可以使用多种度量来评估模型的预测能力,例如 F1 分数、精确度、召回率或 ROC AUC。每种度量有其特定的应用场景,具体使用哪一种取决于项目和数据集。

在本章中,我们将使用一个叫做准确度评分的度量标准。它计算正确预测的数量与模型所做预测总数之间的比例:

图 4.5:准确度评分公式

图 4.5:准确度评分公式

例如,如果你的模型在 1000 个案例中做出了 950 个正确预测,那么准确度评分就是 950/1000 = 0.95。 这意味着你的模型在该数据集上的准确度为 95%。sklearn 包提供了一个函数来自动计算这个评分,称为 accuracy_score()。我们需要先导入它:

from sklearn.metrics import accuracy_score

然后,我们只需要提供一些观测值的预测列表和对应的目标变量的真实值。使用之前的例子,我们将使用 y_trainpreds 变量,分别包含训练集的响应变量(也称为目标)和 Random Forest 模型所做的相应预测。我们将重用前一节的预测——preds

accuracy_score(y_train, preds)

输出结果如下:

图 4.6:训练集上的准确度评分

图 4.6:训练集上的准确度评分

我们在训练数据上取得了 0.988 的准确度评分。这意味着我们准确地预测了超过98%的情况。不幸的是,这并不意味着你可以在新的、未见过的数据上达到如此高的评分。你的模型可能只是学习到了与该训练集相关的模式,在这种情况下,模型会发生过拟合。

如果我们以学生学习某个学科一个学期为类比,他们可能能背诵课本上的所有习题,但当给出一个类似但未见过的习题时,他们却无法解答。理想情况下,学生应该理解该学科的基本概念,并能够将这些知识应用到任何类似的习题中。这与我们的模型完全相同:我们希望它学习到有助于在未见数据上做出准确预测的通用模式。

那么,我们如何评估模型在未见数据上的表现呢?有没有一种方法可以进行这种评估?这些问题的答案是肯定的。

请记住,在上一节中,我们将数据集分为训练集和测试集。我们使用训练集来拟合模型并评估其在该数据集上的预测能力。但它根本没有见过测试集中的观测数据,所以我们可以用它来评估我们的模型是否能够对未见数据进行泛化。让我们计算测试集的准确度评分:

test_preds = rf_model.predict(X_test)
accuracy_score(y_test, test_preds)

输出结果如下:

图 4.7:测试集上的准确度评分

图 4.7:测试集上的准确度评分

好的。现在准确率已经大幅下降到0.77。训练集和测试集之间的差距相当大。这告诉我们我们的模型实际上是过拟合了,只学到了与训练集相关的模式。在理想情况下,模型在这两个集上的表现应该相等或非常接近。

在接下来的章节中,我们将调整一些随机森林的超参数,以减少过拟合。

练习 4.01:构建分类动物类型的模型并评估其表现

在这个练习中,我们将训练一个随机森林分类器,根据动物的属性预测其类型,并检查其准确度评分:

注:

我们将使用的数据集是由 Richard S. Forsyth 共享的动物园数据集:packt.live/36DpRVK。该数据集的 CSV 版本可以在这里找到:packt.live/37RWGhF

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包:

    import pandas as pd
    
  3. 创建一个名为file_url的变量,包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter04/Dataset'\
               '/openml_phpZNNasq.csv'
    
  4. 使用pandas.read_csv()方法将数据集加载到数据框中:

    df = pd.read_csv(file_url)
    
  5. 打印数据框的前五行:

    df.head()
    

    你应该得到以下输出:

    图 4.8:数据框的前五行

    图 4.8:数据框的前五行

    我们将使用type列作为我们的目标变量。我们需要从数据框中移除animal列,只使用其余的列作为特征。

  6. 使用pandas.drop()方法删除'animal'列,并指定columns='animal'inplace=True参数(直接更新原始数据框):

    df.drop(columns='animal', inplace=True)
    
  7. 使用pandas.pop()方法提取'type'列:

    y = df.pop('type')
    
  8. 打印更新后的数据框的前五行:

    df.head()
    

    你应该得到以下输出:

    图 4.9:数据框的前五行

    图 4.9:数据框的前五行

  9. sklearn.model_selection导入train_test_split函数:

    from sklearn.model_selection import train_test_split
    
  10. 使用dfytest_size=0.4random_state=188参数将数据集分成训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split\
                                       (df, y, test_size=0.4, \
                                        random_state=188)
    
  11. sklearn.ensemble导入RandomForestClassifier

    from sklearn.ensemble import RandomForestClassifier
    
  12. 使用random_state等于42实例化RandomForestClassifier对象。将n_estimators值设置为初始默认值10。我们稍后将讨论更改此值如何影响结果。

    rf_model = RandomForestClassifier(random_state=42, \
                                      n_estimators=10)
    
  13. 使用训练集拟合RandomForestClassifier

    rf_model.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 4.10:RandomForestClassifier 的日志

    图 4.10:RandomForestClassifier 的日志

  14. 使用.predict()方法预测训练集的结果,将结果保存在一个名为train_preds的变量中,并打印其值:

    train_preds = rf_model.predict(X_train)
    train_preds
    

    你应该得到以下输出:

    图 4.11:训练集上的预测结果

    图 4.11:训练集上的预测结果

  15. sklearn.metrics导入accuracy_score函数:

    from sklearn.metrics import accuracy_score
    
  16. 计算训练集上的准确率,将结果保存到名为train_acc的变量中,并打印其值:

    train_acc = accuracy_score(y_train, train_preds)
    print(train_acc)
    

    您应该得到以下输出:

    图 4.12: 训练集上的准确率

    图 4.12: 训练集上的准确率

    我们的模型在训练集上的准确率为1,这意味着它在所有观察值上都完美地预测了目标变量。现在让我们查看在测试集上的表现。

  17. 使用.predict()方法预测测试集的结果,并将结果保存到一个名为test_preds的变量中:

    test_preds = rf_model.predict(X_test)
    
  18. 计算测试集上的准确率,将结果保存到名为test_acc的变量中,并打印其值:

    test_acc = accuracy_score(y_test, test_preds)
    print(test_acc)
    

    您应该得到以下输出:

    图 4.13: 测试集上的准确率

图 4.13: 测试集上的准确率

在这个练习中,我们训练了一个随机森林模型来预测动物的种类,基于它们的关键属性。我们的模型在训练集上的准确率达到了完美的1,但在测试集上的准确率只有0.88。这意味着我们的模型发生了过拟合,缺乏足够的泛化能力。理想的情况是,模型在训练集和测试集上的准确率应非常相似且较高。

注意

要查看这一特定部分的源代码,请参考 packt.live/2Q4jpQK

你也可以在线运行这个示例,网址是 packt.live/3h6JieL

树的数量估计器

现在我们已经知道如何拟合一个随机森林分类器并评估其性能,是时候深入探讨细节了。在接下来的章节中,我们将学习如何调整一些对该算法非常重要的超参数。如第一章:Python 中的数据科学简介所述,超参数是机器学习算法无法自动学习的参数。它们的值必须由数据科学家设置。这些超参数对模型的性能、其对未见数据的泛化能力以及从数据中学习模式所需的时间有着巨大影响。

在本节中,您将首先关注一个名为n_estimators的超参数。这个超参数负责定义RandomForest算法将训练的树的数量。

在查看如何调整这个超参数之前,我们需要了解什么是树以及它为什么对RandomForest算法如此重要。

一棵树是一个逻辑图,它在每个节点处映射一个决策及其结果。简单来说,它是一系列是/否(或真/假)的问题,指向不同的结果。

叶子是一个特殊类型的节点,模型将在此处进行预测。叶子之后不会再进行分裂。一个树的单节点分裂可能如下所示:

图 4.14: 单一树节点的示例

图 4.14: 单一树节点的示例

一棵树的节点由一个问题和两个结果组成,取决于问题定义的条件是否满足。在前面的例子中,问题是 avg_rss12 > 41? 如果答案是“是”,结果就是 bending_1 叶子,如果答案是否定的,那么结果就是 sitting 叶子。

一棵树就是由一系列节点和叶子组成:

图 4.15:树的示例

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_04_15.jpg)

图 4.15:树的示例

在前面的例子中,这棵树由三个具有不同问题的节点组成。现在,为了预测一个观察值为 sitting,它需要满足以下条件:avg_rss13 <= 41var_rss > 0.7,和 avg_rss13 <= 16.25

RandomForest 算法将根据它看到的训练数据构建这种类型的树。我们不会详细讨论它如何定义每个节点的分割,但基本上,它会遍历数据集的每一列,看看哪个分割值能最好地帮助将数据分为两个相似类别的组。以前面的例子为例,带有 avg_rss13 > 41 条件的第一个节点有助于将左侧的数据分组,其中大多数属于 bending_1 类别。RandomForest 算法通常会构建多个这种类型的树,这也是它被称为森林的原因。

如你现在可能已经猜到,n_estimators 超参数用于指定 RandomForest 算法将构建的树的数量。例如(如前面的练习中所示),假设我们要求它构建 10 棵树。对于给定的观察,它将让每棵树进行预测。然后,它会对这些预测结果求平均,并将结果作为该输入的最终预测。例如,如果在 10 棵树中,有 8 棵预测结果为 sitting,那么 RandomForest 算法将使用这个结果作为最终预测。

注意

如果你没有传入特定的 n_estimators 超参数,它将使用默认值。默认值取决于你使用的 scikit-learn 版本。在早期版本中,默认值为 10。在 0.22 版本及以后,默认值为 100。你可以通过执行以下代码来查看你使用的版本:

import sklearn

sklearn.__version__

更多信息,请参见:scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html

通常来说,树的数量越多,性能越好。让我们看看 n_estimators = 2 时在活动识别数据集上的表现:

rf_model2 = RandomForestClassifier(random_state=1, \
                                   n_estimators=2)
rf_model2.fit(X_train, y_train)
preds2 = rf_model2.predict(X_train)
test_preds2 = rf_model2.predict(X_test)
print(accuracy_score(y_train, preds2))
print(accuracy_score(y_test, test_preds2))

输出将如下所示:

图 4.16: 时 RandomForest 的准确率

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_04_16.jpg)

图 4.16:n_estimators = 2 时 RandomForest 的准确率

如预期的那样,准确度明显低于前面 n_estimators = 10 的例子。现在让我们试试 50 棵树:

rf_model3 = RandomForestClassifier(random_state=1, \
                                   n_estimators=50)
rf_model3.fit(X_train, y_train)
preds3 = rf_model3.predict(X_train)
test_preds3 = rf_model3.predict(X_test)
print(accuracy_score(y_train, preds3))
print(accuracy_score(y_test, test_preds3))

输出将如下所示:

图 4.17:n_estimators=50 的随机森林准确度

图 4.17:n_estimators=50 的随机森林准确度

使用n_estimators=50时,我们在训练集和测试集的准确度上分别提高了1%2%,这非常好。但增加树木数量的主要缺点是需要更多的计算能力。因此,训练模型的时间会更长。在实际项目中,你需要找到性能与训练时长之间的最佳平衡。

练习 4.02:调整 n_estimators 以减少过拟合

在本练习中,我们将训练一个随机森林分类器,基于动物的属性预测其类型,并尝试n_estimators超参数的两个不同值:

我们将使用与之前练习相同的动物园数据集。

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包,train_test_splitRandomForestClassifier,和accuracy_scoresklearn中:

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.metrics import accuracy_score
    
  3. 创建一个名为file_url的变量,包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter04/Dataset'\
               '/openml_phpZNNasq.csv'
    
  4. 使用.read_csv()方法从pandas加载数据集到 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用.drop()删除animal列,然后使用.pop()type目标变量提取到一个新变量y中:

    df.drop(columns='animal', inplace=True)
    y = df.pop('type')
    
  6. 使用train_test_split()将数据分为训练集和测试集,并设置test_size=0.4random_state=188参数:

    X_train, X_test, y_train, y_test = train_test_split\
                                       (df, y, test_size=0.4, \
                                        random_state=188)
    
  7. 使用random_state=42n_estimators=1实例化RandomForestClassifier,然后用训练集拟合模型:

    rf_model = RandomForestClassifier(random_state=42, \
                                      n_estimators=1)
    rf_model.fit(X_train, y_train)
    

    你应该会得到以下输出:

    图 4.18:随机森林分类器的日志

    图 4.18:随机森林分类器的日志

  8. 使用.predict()对训练集和测试集进行预测,并将结果保存到两个新变量train_predstest_preds中:

    train_preds = rf_model.predict(X_train)
    test_preds = rf_model.predict(X_test)
    
  9. 计算训练集和测试集的准确度分数,并将结果保存到两个新变量train_acctest_acc中:

    train_acc = accuracy_score(y_train, train_preds)
    test_acc = accuracy_score(y_test, test_preds)
    
  10. 打印准确度分数:train_acctest_acc

    print(train_acc)
    print(test_acc)
    

    你应该会得到以下输出:

    图 4.19:训练集和测试集的准确度分数

    图 4.19:训练集和测试集的准确度分数

    训练集和测试集的准确度分数都有所下降。但现在,与练习 4.01《构建动物分类模型并评估其性能》中的结果相比,二者之间的差异较小。

  11. 使用random_state=42n_estimators=30实例化另一个RandomForestClassifier,然后用训练集拟合模型:

    rf_model2 = RandomForestClassifier(random_state=42, \
                                       n_estimators=30)
    rf_model2.fit(X_train, y_train)
    

    你应该会得到以下输出:

    图 4.20:n_estimators=30 的随机森林日志

    图 4.20:n_estimators=30 的随机森林日志

  12. 使用.predict()对训练集和测试集进行预测,并将结果保存到两个新变量train_preds2test_preds2中:

    train_preds2 = rf_model2.predict(X_train)
    test_preds2 = rf_model2.predict(X_test)
    
  13. 计算训练集和测试集的准确率,并将结果保存到名为train_acc2test_acc2的两个新变量中:

    train_acc2 = accuracy_score(y_train, train_preds2)
    test_acc2 = accuracy_score(y_test, test_preds2)
    
  14. 打印准确率:train_acctest_acc

    print(train_acc2)
    print(test_acc2)
    

    你应该会得到以下输出:

    ![图 4.21:训练集和测试集的准确率]

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_04_21.jpg)

图 4.21:训练集和测试集的准确率

这个输出显示了我们的模型比前一步的结果有了更少的过拟合,并且在训练集上的性能仍然非常高。

在上一个练习中,我们在训练集上获得了准确率1,在测试集上获得了0.88。在这个练习中,我们训练了两个额外的随机森林模型,分别设置了n_estimators = 130。树木数量最少的模型准确率最低:0.92(训练)和0.8(测试)。另一方面,将树木数量增加到30时,我们达到了更高的准确率:10.9。我们的模型现在过拟合稍微少了些。它并不完美,但这是一个不错的开始。

注意

要访问该部分的源代码,请参考packt.live/322x8gz

你也可以在线运行这个示例,访问packt.live/313gUV8

最大深度

在上一节中,我们学习了随机森林如何构建多棵树来进行预测。增加树木数量确实能提高模型的性能,但通常对于减少过拟合的风险帮助不大。我们在上一个示例中的模型在训练集(已经看到的数据)上的表现仍然远远优于在测试集(未见过的数据)上的表现。

因此,我们还不能完全确定模型在生产环境中的表现如何。有多种超参数可以帮助减少随机森林的过拟合风险,其中之一就是max_depth

这个超参数定义了随机森林构建的树的深度。基本上,它告诉随机森林模型,在做出预测之前,最多可以创建多少个节点(问题)。但你可能会问,这样如何帮助减少过拟合呢?嗯,假设你构建了一棵树,并将max_depth超参数设置为50。这意味着在做出预测之前,你可以提出 49 个不同的问题(值c包括最终的叶子节点)。所以,逻辑是:IF X1 > value1 AND X2 > value2 AND X1 <= value3 AND … AND X3 > value49 THEN predict class A

如你所想,这是一条非常特定的规则。最终,它可能仅适用于训练集中的少数几个观测值,这种情况发生得非常少。因此,你的模型会出现过拟合。默认情况下,max_depth参数的值是None,这意味着树的深度没有设置限制。

你真正想要的是找到一些足够通用的规则,能够应用于更大范围的观察数据。这就是为什么建议不要在随机森林中创建过深的树。我们在活动识别数据集上尝试几个不同的max_depth超参数值:31050

rf_model4 = RandomForestClassifier(random_state=1, \
                                   n_estimators=50, max_depth=3)
rf_model4.fit(X_train, y_train)
preds4 = rf_model4.predict(X_train)
test_preds4 = rf_model4.predict(X_test)
print(accuracy_score(y_train, preds4))
print(accuracy_score(y_test, test_preds4))

你应该得到以下输出:

图 4.22:训练集和测试集的准确率,

图 4.22:训练集和测试集的准确率,max_depth = 3

对于max_depth = 3,训练集和测试集的结果非常相似,但整体性能急剧下降至0.61。我们的模型不再过拟合,但现在出现了欠拟合;也就是说,它没有很好地预测目标变量(仅在61%的情况下预测正确)。让我们将max_depth增加到10

rf_model5 = RandomForestClassifier(random_state=1, \
                                   n_estimators=50, \
                                   max_depth=10)
rf_model5.fit(X_train, y_train)
preds5 = rf_model5.predict(X_train)
test_preds5 = rf_model5.predict(X_test)
print(accuracy_score(y_train, preds5))
print(accuracy_score(y_test, test_preds5))

图 4.23:训练集和测试集的准确率,

图 4.23:训练集和测试集的准确率,max_depth = 10

训练集的准确率有所提升,并且与测试集相对接近。我们开始获得一些良好的结果,但模型仍然略微过拟合。接下来,我们将看到max_depth = 50的结果:

rf_model6 = RandomForestClassifier(random_state=1, \
                                   n_estimators=50, \
                                   max_depth=50)
rf_model6.fit(X_train, y_train)
preds6 = rf_model6.predict(X_train)
test_preds6 = rf_model6.predict(X_test)
print(accuracy_score(y_train, preds6))
print(accuracy_score(y_test, test_preds6))

输出将如下所示:

图 4.24:训练集和测试集的准确率,

图 4.24:训练集和测试集的准确率,max_depth = 50

训练集的准确率跃升至0.99,但测试集的表现提升不大。因此,模型在max_depth = 50时出现了过拟合。看起来在这个数据集上,要获得较好的预测效果并避免过拟合的最佳位置是在max_depth10时。

练习 4.03:调整max_depth以减少过拟合

在本练习中,我们将继续调整我们的随机森林分类器,该分类器通过尝试两种不同的max_depth超参数值来预测动物类型:

我们将使用与上一练习中相同的动物园数据集。

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包、train_test_splitRandomForestClassifieraccuracy_score,这些都来自sklearn

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.metrics import accuracy_score
    
  3. 创建一个名为file_url的变量,包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com'\
               'PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter04/Dataset'\
               '/openml_phpZNNasq.csv'
    
  4. 使用pandas.read_csv()方法将数据集加载到一个 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用.drop()删除animal列,然后使用.pop()type目标变量提取到一个名为y的新变量中:

    df.drop(columns='animal', inplace=True)
    y = df.pop('type')
    
  6. 使用train_test_split()将数据分为训练集和测试集,参数为test_size=0.4random_state=188

    X_train, X_test, y_train, y_test = train_test_split\
                                       (df, y, test_size=0.4, \
                                        random_state=188)
    
  7. 使用random_state=42n_estimators=30max_depth=5实例化RandomForestClassifier,然后使用训练集拟合模型:

    rf_model = RandomForestClassifier(random_state=42, \
                                      n_estimators=30, \
                                      max_depth=5)
    rf_model.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 4.25:随机森林日志

    图 4.25:随机森林日志

  8. 使用.predict()对训练集和测试集进行预测,并将结果保存在两个新变量中,分别命名为train_predstest_preds

    train_preds = rf_model.predict(X_train)
    test_preds = rf_model.predict(X_test)
    
  9. 计算训练集和测试集的准确度分数,并将结果保存在两个新变量中,分别命名为train_acctest_acc

    train_acc = accuracy_score(y_train, train_preds)
    test_acc = accuracy_score(y_test, test_preds)
    
  10. 打印准确度分数:train_acctest_acc

    print(train_acc)
    print(test_acc)
    

    你应该得到以下输出:

    图 4.26:训练集和测试集的准确度分数

    图 4.26:训练集和测试集的准确度分数

    我们得到的准确度分数与在之前的练习中获得的最佳结果完全相同。对于max_depth超参数的这个值并没有影响模型的表现。

  11. 实例化另一个RandomForestClassifier,并设置random_state=42n_estimators=30max_depth=2,然后使用训练集拟合模型:

    rf_model2 = RandomForestClassifier(random_state=42, \
                                       n_estimators=30, \
                                       max_depth=2)
    rf_model2.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 4.27:max_depth = 2 时,RandomForestClassifier 的日志

    图 4.27:max_depth = 2 时,RandomForestClassifier 的日志

  12. 使用.predict()对训练集和测试集进行预测,并将结果保存在两个新变量中,分别命名为train_preds2test_preds2

    train_preds2 = rf_model2.predict(X_train)
    test_preds2 = rf_model2.predict(X_test)
    
  13. 计算训练集和测试集的准确度分数,并将结果保存在两个新变量中,分别命名为train_acc2test_acc2

    train_acc2 = accuracy_score(y_train, train_preds2)
    test_acc2 = accuracy_score(y_test, test_preds2)
    
  14. 打印准确度分数:train_acctest_acc

    print(train_acc2)
    print(test_acc2)
    

    你应该得到以下输出:

    图 4.28:训练集和测试集的准确度分数

图 4.28:训练集和测试集的准确度分数

你在这个练习中学会了如何调整max_depth超参数。将其值减少到2使得训练集的准确度分数降至 0.9,但也有助于减少训练集和测试集的过拟合(0.83),因此我们将保持这个值作为最优值,并继续下一步。

注意

要访问这一特定部分的源代码,请参考packt.live/31YXkIY

你也可以在线运行这个示例,链接为packt.live/2CCkxYX

叶节点的最小样本数

之前,我们学习了如何减少或增加随机森林中的树的深度,并观察了它如何影响性能以及是否会发生过拟合。现在,我们将介绍另一个重要的超参数:min_samples_leaf

这个超参数顾名思义,与树的叶节点有关。我们之前看到,RandomForest算法会构建节点,清晰地将观测值分成两个不同的组。如果我们看看图 4.15中的树示例,顶节点将数据分成两组:左侧组主要包含bending_1类别的观测值,而右侧组则可能来自任何类别。这看起来像是一个合理的划分,但我们能确定它不会增加过拟合的风险吗?例如,如果这个划分导致只有一个观测值落在左侧呢?这个规则会非常具体(仅适用于一个单一的情况),我们不能说它对未见数据具有足够的泛化性。它可能是训练集中的一个极端情况,未来永远不会再发生。

如果我们能让模型知道不要创建那些发生频率很低的特定规则就好了。幸运的是,RandomForest有一个这样的超参数,没错,它就是min_samples_leaf。这个超参数指定了在树中要考虑的叶节点下必须有至少多少个观测值(或样本)。例如,如果我们将min_samples_leaf设置为3,那么RandomForest只会考虑那些左叶节点和右叶节点上至少有三个观测值的划分。如果这个条件没有满足,模型就不会考虑这个划分,并将其从树中排除。sklearn中这个超参数的默认值是1。让我们尝试为活动识别数据集找到min_samples_leaf的最佳值:

rf_model7 = RandomForestClassifier(random_state=1, \
                                   n_estimators=50, \
                                   max_depth=10, \
                                   min_samples_leaf=3)
rf_model7.fit(X_train, y_train)
preds7 = rf_model7.predict(X_train)
test_preds7 = rf_model7.predict(X_test)
print(accuracy_score(y_train, preds7))
print(accuracy_score(y_test, test_preds7))

输出将如下所示:

图 4.29: 时训练集和测试集的准确度得分

图 4.29:min_samples_leaf=3 时训练集和测试集的准确度得分

min_samples_leaf=3时,训练集和测试集的准确度与我们在前一部分找到的最佳模型相比变化不大。我们来试试将其增大到10

rf_model8 = RandomForestClassifier(random_state=1, \
                                   n_estimators=50, \
                                   max_depth=10, \
                                   min_samples_leaf=10)
rf_model8.fit(X_train, y_train)
preds8 = rf_model8.predict(X_train)
test_preds8 = rf_model8.predict(X_test)
print(accuracy_score(y_train, preds8))
print(accuracy_score(y_test, test_preds8))

输出将如下所示:

图 4.30: 时训练集和测试集的准确度得分

图 4.30:min_samples_leaf=10 时训练集和测试集的准确度得分

现在训练集的准确度稍微下降了,但测试集的准确度增加了,而且它们之间的差距变小了。因此,我们的模型过拟合的情况减少了。让我们再试试这个超参数的另一个值——25

rf_model9 = RandomForestClassifier(random_state=1, \
                                   n_estimators=50, \
                                   max_depth=10, \
                                   min_samples_leaf=25)
rf_model9.fit(X_train, y_train)
preds9 = rf_model9.predict(X_train)
test_preds9 = rf_model9.predict(X_test)
print(accuracy_score(y_train, preds9))
print(accuracy_score(y_test, test_preds9))

输出将如下所示:

图 4.31: 时训练集和测试集的准确度得分

图 4.31:min_samples_leaf=25 时训练集和测试集的准确度得分

训练集和测试集的准确度都下降了,但它们现在非常接近。所以,我们将25作为这个数据集的最佳值,因为性能仍然可以接受,而且我们没有过度拟合。

在选择该超参数的最佳值时,你需要小心:一个过低的值会增加模型过拟合的可能性,但另一方面,设置一个非常高的值会导致欠拟合(模型无法准确预测正确的结果)。

例如,如果你有一个1000行的数据集,如果将min_samples_leaf设置为400,那么模型将无法找到适合预测5个不同类别的良好切分。在这种情况下,模型只能创建一个单一的切分,并且只能预测两个类别,而不是5个类别。最佳实践是先从较低的值开始,然后逐步增加,直到达到令人满意的性能。

练习 4.04:调整 min_samples_leaf

在本练习中,我们将继续调整我们的随机森林分类器,通过尝试min_samples_leaf超参数的两个不同值来预测动物类型:

我们将使用与前一个练习中相同的动物园数据集。

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包、train_test_splitRandomForestClassifieraccuracy_scoresklearn

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.metrics import accuracy_score
    
  3. 创建一个名为file_url的变量,包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter04/Dataset/openml_phpZNNasq.csv'
    
  4. 使用pandas.read_csv()方法将数据集加载到 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用.drop()移除animal列,然后使用.pop()type目标变量提取到一个新变量y中:

    df.drop(columns='animal', inplace=True)
    y = df.pop('type')
    
  6. 使用train_test_split()将数据分割为训练集和测试集,参数设置为test_size=0.4random_state=188

    X_train, X_test, \
    y_train, y_test = train_test_split(df, y, test_size=0.4, \
                                       random_state=188)
    
  7. 使用random_state=42n_estimators=30max_depth=2min_samples_leaf=3实例化RandomForestClassifier,然后用训练集拟合模型:

    rf_model = RandomForestClassifier(random_state=42, \
                                      n_estimators=30, \
                                      max_depth=2, \
                                      min_samples_leaf=3)
    rf_model.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 4.32:随机森林日志

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_04_32.jpg)

    图 4.32:随机森林日志

  8. 使用.predict()对训练集和测试集进行预测,并将结果保存到两个新变量train_predstest_preds中:

    train_preds = rf_model.predict(X_train)
    test_preds = rf_model.predict(X_test)
    
  9. 计算训练集和测试集的准确度评分,并将结果保存在两个新变量train_acctest_acc中:

    train_acc = accuracy_score(y_train, train_preds)
    test_acc = accuracy_score(y_test, test_preds)
    
  10. 打印准确度评分——train_acctest_acc

    print(train_acc)
    print(test_acc)
    

    你应该得到以下输出:

    图 4.33:训练集和测试集的准确度评分

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_04_33.jpg)

    图 4.33:训练集和测试集的准确度评分

    与我们在前一个练习中得到的最佳结果相比,训练集和测试集的准确度评分都有所下降。现在,训练集和测试集的准确度评分差距要小得多,说明我们的模型过拟合的情况较少。

  11. 使用random_state=42n_estimators=30max_depth=2min_samples_leaf=7实例化另一个RandomForestClassifier,然后用训练集拟合模型:

    rf_model2 = RandomForestClassifier(random_state=42, \
                                       n_estimators=30, \
                                       max_depth=2, \
                                       min_samples_leaf=7)
    rf_model2.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 4.34:max_depth=2 的随机森林日志

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_04_34.jpg)

    图 4.34:max_depth=2 的随机森林日志

  12. 使用.predict()对训练集和测试集进行预测,并将结果保存到两个新变量中,分别命名为train_preds2test_preds2

    train_preds2 = rf_model2.predict(X_train)
    test_preds2 = rf_model2.predict(X_test)
    
  13. 计算训练集和测试集的准确率得分,并将结果保存在两个新变量中,分别命名为train_acc2test_acc2

    train_acc2 = accuracy_score(y_train, train_preds2)
    test_acc2 = accuracy_score(y_test, test_preds2)
    
  14. 打印准确率得分:train_acctest_acc

    print(train_acc2)
    print(test_acc2)
    

    你应该得到以下输出:

    图 4.35:训练集和测试集的准确率得分

图 4.35:训练集和测试集的准确率得分

min_samples_leaf的值增加到7后,模型不再出现过拟合现象。我们得到了非常相似的训练集和测试集准确率得分,大约为0.8。我们将选择这个值作为该数据集的min_samples_leaf的最优值。

注意

要访问这一部分的源代码,请参考packt.live/3kUYVZa

你还可以在网上运行这个示例:packt.live/348bv0W

最大特征数

我们已经接近本章的结尾。你已经学习了如何调整RandomForest中最重要的几个超参数。在这一部分,我们将向你介绍另一个非常重要的参数:max_features

之前,我们了解了RandomForest如何构建多棵树并取平均值来进行预测。这就是为什么它被称为森林,但我们还没有真正讨论“随机”部分。通过本章的内容,你可能会问自己:构建多棵树如何帮助得到更好的预测结果?而且如果输入数据相同,所有的树难道不会看起来一样吗?

在回答这些问题之前,我们可以通过类比法来理解。某些国家,审判的最终决定是由法官或陪审团做出的。法官是精通法律的人,能决定一个人是否违反了法律。另一方面,陪审团由不同背景的人组成,他们彼此不认识,也不认识案件的当事人,而且对法律体系的了解有限。在这种情况下,我们让一些非法律专家来决定案件的结果,乍一听起来似乎很冒险。一个人做出错误判断的风险很高,但事实上,10 或 20 个人都做出错误判断的风险相对较低。

但有一个条件需要满足才能使这种方法有效:随机性。如果陪审团中的所有人都来自相同的背景、在相同的行业工作或生活在相同的地区,他们可能会有相同的思维方式并做出相似的决定。例如,如果一群人在一个只喝热巧克力作为早餐的社区长大,某天你问他们早上喝咖啡是否合适,他们都会说不行。

另一方面,假设你有另一组来自不同背景、拥有不同习惯的人:有些人喝咖啡,有些人喝茶,还有一些人喝橙汁,等等。如果你问他们相同的问题,你最终会发现大多数人都会回答“是”。因为我们随机挑选了这些人,他们作为一个群体的偏差较小,因此降低了他们做出错误决策的风险。

RandomForest 实际上应用了相同的逻辑:通过随机抽样数据,独立地构建多个树。一个树可能看到60%的训练数据,另一个树看到70%,依此类推。通过这种方式,树之间有很大的可能性是完全不同的,并且不会共享相同的偏差。这就是 RandomForest 的秘密:构建多个随机树可以提高准确性。

但这并不是 RandomForest 创建随机性的唯一方式。它还通过随机抽样列来实现这一点。每棵树只会看到特征的子集,而不是所有的特征。这正是max_features超参数的作用:它设置了每棵树可以看到的最大特征数。

sklearn中,你可以通过以下方式指定该超参数的值:

  • 最大特征数,作为一个整数。

  • 一个比率,作为允许特征的百分比。

  • sqrt函数(sklearn中的默认值,表示平方根),它将使用特征数量的平方根作为最大值。如果数据集有25个特征,则它的平方根为5,这将是max_features的值。

  • log2函数,它将使用特征数量的以 2 为底的对数作为最大值。如果数据集有 8 个特征,则其log23,这将是max_features的值。

  • None值表示 RandomForest 将使用所有可用特征。

让我们在活动数据集上尝试三个不同的值。首先,我们将最大特征数指定为 2:

rf_model10 = RandomForestClassifier(random_state=1, \
                                    n_estimators=50, \
                                    max_depth=10, \
                                    min_samples_leaf=25, \
                                    max_features=2)
rf_model10.fit(X_train, y_train)
preds10 = rf_model10.predict(X_train)
test_preds10 = rf_model10.predict(X_test)
print(accuracy_score(y_train, preds10))
print(accuracy_score(y_test, test_preds10))

输出结果如下:

图 4.36:max_features=2 时训练集和测试集的准确性得分

图 4.36:max_features=2 时训练集和测试集的准确性得分

我们得到了类似于之前训练的最佳模型的结果。这并不令人惊讶,因为当时我们使用的是max_features的默认值,即sqrt2的平方根等于1.45,与2相当接近。这次,我们尝试使用比率0.7

rf_model11 = RandomForestClassifier(random_state=1, \
                                    n_estimators=50, \
                                    max_depth=10, \
                                    min_samples_leaf=25, \
                                    max_features=0.7)
rf_model11.fit(X_train, y_train)
preds11 = rf_model11.predict(X_train)
test_preds11 = rf_model11.predict(X_test)
print(accuracy_score(y_train, preds11))
print(accuracy_score(y_test, test_preds11))

输出结果如下:

图 4.37:max_features=0.7 时训练集和测试集的准确性得分

图 4.37:max_features=0.7 时训练集和测试集的准确性得分

使用这个比例,训练集和测试集的准确率都提高了,并且它们之间的差距更小。我们的模型现在过拟合较少,预测能力稍有提升。让我们尝试使用 log2 选项:

rf_model12 = RandomForestClassifier(random_state=1, \
                                    n_estimators=50, \
                                    max_depth=10, \
                                    min_samples_leaf=25, \
                                    max_features='log2')
rf_model12.fit(X_train, y_train)
preds12 = rf_model12.predict(X_train)
test_preds12 = rf_model12.predict(X_test)
print(accuracy_score(y_train, preds12))
print(accuracy_score(y_test, test_preds12))

输出结果如下:

图 4.38:max_features='log2' 时训练集和测试集的准确率

图 4.38:max_features='log2' 时训练集和测试集的准确率

我们得到了与默认值(sqrt)和 2 相似的结果。同样,这是正常的,因为 6log2 等于 2.58。所以,我们为这个数据集找到的 max_features 超参数的最佳值是 0.7

练习 4.05:调整 max_features

在本练习中,我们将继续调整我们的随机森林分类器,通过尝试 max_features 超参数的两个不同值来预测动物类型:

我们将使用与前一个练习相同的动物园数据集。

  1. 打开一个新的 Colab 笔记本。

  2. 导入 pandas 包,train_test_splitRandomForestClassifieraccuracy_scoresklearn

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.metrics import accuracy_score
    
  3. 创建一个名为 file_url 的变量,其中包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter04/Dataset/openml_phpZNNasq.csv'
    
  4. 使用 .read_csv() 方法从 pandas 将数据集加载到 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用 .drop() 删除 animal 列,然后使用 .pop() 提取 type 目标变量,并将其存储到一个名为 y 的新变量中:

    df.drop(columns='animal', inplace=True)
    y = df.pop('type')
    
  6. 使用 train_test_split() 将数据集拆分为训练集和测试集,参数为 test_size=0.4random_state=188

    X_train, X_test, \
    y_train, y_test = train_test_split(df, y, test_size=0.4, \
                                       random_state=188)
    
  7. 实例化 RandomForestClassifier,参数为 random_state=42n_estimators=30max_depth=2min_samples_leaf=7max_features=10,然后用训练集拟合模型:

    rf_model = RandomForestClassifier(random_state=42, \
                                      n_estimators=30, \
                                      max_depth=2, \
                                      min_samples_leaf=7, \
                                      max_features=10)
    rf_model.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 4.39:随机森林的日志

    图 4.39:随机森林的日志

  8. 使用 .predict() 对训练集和测试集进行预测,并将结果保存到两个名为 train_predstest_preds 的新变量中:

    train_preds = rf_model.predict(X_train)
    test_preds = rf_model.predict(X_test)
    
  9. 计算训练集和测试集的准确率,并将结果保存在两个名为 train_acctest_acc 的新变量中:

    train_acc = accuracy_score(y_train, train_preds)
    test_acc = accuracy_score(y_test, test_preds)
    
  10. 打印准确率分数:train_acctest_acc

    print(train_acc)
    print(test_acc)
    

    你应该得到以下输出:

    图 4.40:训练集和测试集的准确率

    图 4.40:训练集和测试集的准确率

  11. 实例化另一个 RandomForestClassifier,参数为 random_state=42n_estimators=30max_depth=2min_samples_leaf=7max_features=0.2,然后用训练集来拟合模型:

    rf_model2 = RandomForestClassifier(random_state=42, \
                                       n_estimators=30, \
                                       max_depth=2, \
                                       min_samples_leaf=7, \
                                       max_features=0.2)
    rf_model2.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 4.41:max_features = 0.2 时随机森林的日志

    图 4.41:max_features = 0.2 时随机森林的日志

  12. 使用 .predict() 对训练集和测试集进行预测,并将结果保存到两个名为 train_preds2test_preds2 的新变量中:

    train_preds2 = rf_model2.predict(X_train)
    test_preds2 = rf_model2.predict(X_test)
    
  13. 计算训练集和测试集的准确率,并将结果保存在两个新变量中,分别命名为train_acc2test_acc2

    train_acc2 = accuracy_score(y_train, train_preds2)
    test_acc2 = accuracy_score(y_test, test_preds2)
    
  14. 打印准确率评分:train_acctest_acc

    print(train_acc2)
    print(test_acc2)
    

    你应该得到如下输出:

    图 4.42:训练集和测试集的准确率评分

图 4.42:训练集和测试集的准确率评分

在本次练习中,我们尝试的max_features超参数的值100.2确实提高了训练集的准确率,但对测试集没有影响。使用这些值,模型开始出现过拟合。对于这个数据集,max_features的最佳值是默认值(sqrt)。最终,我们成功构建了一个准确率为 0.8 且没有过拟合的模型。考虑到数据集并不大,我们只有6个特征和41759个观察值,这已经是一个相当不错的结果。

注意

要访问此特定部分的源代码,请参考packt.live/3g8nTk7

你也可以在此在线运行此示例:packt.live/324quGv

活动 4.01:在 ISOLET 数据集上训练一个随机森林分类器

你正在为一家科技公司工作,该公司计划推出一款新的语音助手产品。你被分配到建立一个分类模型,识别用户根据信号频率拼写的字母。每个声音可以被捕获并表示为由多个频率组成的信号。

注意

本活动使用 ISOLET 数据集,来自 UCI 机器学习库,链接如下:packt.live/2QFOawy

该数据集的 CSV 版本可以在此处找到:packt.live/36DWHpi

以下步骤将帮助你完成此活动:

  1. 使用pandas中的.read_csv()下载并加载数据集。

  2. 使用pandas中的.pop()提取响应变量。

  3. 使用sklearn.model_selection中的train_test_split()将数据集拆分为训练集和测试集。

  4. 创建一个函数,使用sklearn.ensemble中的.fit()实例化并拟合RandomForestClassifier

  5. 创建一个函数,使用.predict()为训练集和测试集预测结果。

  6. 创建一个函数,使用sklearn.metrics中的accuracy_score()打印训练集和测试集的准确率。

  7. 在一系列不同的超参数值下训练并获取准确率。以下是你可以尝试的一些选项:

    • n_estimators = 2050

    • max_depth = 510

    • min_samples_leaf = 1050

    • max_features = 0.50.3

  8. 选择最佳超参数值。

这些是我们训练的最佳模型的准确率评分:

图 4.43:随机森林分类器的准确率评分

图 4.43:随机森林分类器的准确率评分

注意

活动的解决方案可以在这里找到:packt.live/2GbJloz

总结

我们终于结束了关于随机森林的多类分类章节。我们学到了,多类分类是二分类的扩展:它不仅仅是预测两个类别,目标变量可以有更多的值。我们看到,只需几行代码就能训练一个随机森林模型,并通过计算训练集和测试集的准确度来评估其性能。最后,我们了解了如何调整一些最重要的超参数:n_estimatorsmax_depthmin_samples_leafmax_features。我们还看到,它们的值对模型的预测能力以及对未见数据的泛化能力有着显著的影响。

在实际项目中,选择一个有效的测试集是极其重要的。这是你在将模型投入生产前的最终验证,因此你希望它能够真实反映你认为未来模型可能接收到的数据类型。例如,如果你的数据集有一个日期字段,你可以将过去几周或几个月的数据作为测试集,而在此日期之前的数据作为训练集。如果你没有正确选择测试集,你可能最终得到一个看似没有过拟合的优秀模型,但一旦投入生产,它就会生成错误的结果。问题并不在于模型本身,而在于测试集选择不当。

在某些项目中,你可能会看到数据集被拆分为三个不同的集合:训练集、验证集和测试集。验证集可以用来调整超参数,一旦你足够自信,就可以在测试集上测试你的模型。如前所述,我们不希望模型看到过多的测试集数据,但超参数调整需要你多次运行模型,直到找到最优值。这也是为什么大多数数据科学家为此目的创建了验证集,并且仅在少数情况下使用测试集。这将在第七章,机器学习模型的泛化中更深入地解释。

在下一章中,你将接触到无监督学习,并学习如何使用 k-means 算法构建聚类模型。

第五章:5. 执行您的第一次聚类分析

概述

本章将介绍无监督学习任务,在这种任务中,算法必须自动从数据中学习模式,因为没有事先定义目标变量。我们将特别关注 k-means 算法,并学习如何标准化和处理数据,以便用于聚类分析。

本章结束时,您将能够加载和可视化数据与聚类散点图;为聚类分析准备数据;使用 k-means 执行质心聚类;解释聚类结果并确定给定数据集的最优聚类数。

引言

之前的章节向您介绍了非常流行且极其强大的机器学习算法。它们有一个共同点,那就是它们都属于同一类算法:有监督学习。这类算法试图基于指定的结果列(目标变量),例如销售、员工流失率或客户类别,来学习模式。

但是,如果您的数据集中没有这样的变量,或者您不想指定目标变量呢?您还能在数据上运行一些机器学习算法并发现有趣的模式吗?答案是肯定的,借助属于无监督学习类别的聚类算法,您仍然可以做到这一点。

聚类算法在数据科学行业中非常受欢迎,用于将相似的数据点分组并检测异常值。例如,银行可以使用聚类算法进行欺诈检测,通过识别数据中的异常群体。电子商务公司也可以使用聚类算法来识别具有相似浏览行为的用户群体,如下图所示:

图 5.1:具有相似浏览行为的客户数据示例未执行聚类分析

图 5.1:具有相似浏览行为的客户数据示例,未执行聚类分析

对该数据进行聚类分析将揭示自然的模式,通过将相似的数据点分组,从而可能得到以下结果:

图 5.2:对具有相似浏览行为的客户数据执行的聚类分析具有相似浏览行为

图 5.2:对具有相似浏览行为的客户数据执行的聚类分析

该数据现在被分为三个客户群体,分别根据他们的重复访问和在网站上花费的时间进行划分,之后可以为这些群体制定不同的营销计划,以最大化销售。

在本章中,您将学习如何使用一种非常著名的聚类算法——k-means 算法,来执行这种分析。

使用 k-means 进行聚类

k-means 是数据科学家中最流行的聚类算法之一(如果不是最流行的话),因其简单且高效。它的起源可以追溯到 1956 年,当时著名数学家 Hugo Steinhaus 为其奠定了基础,但十年后,另一位研究人员 James MacQueen 给这种方法命名为 k-means。

k-means 的目标是将相似的数据点(或观察值)分在一起,从而形成一个聚类。可以把它看作是将彼此接近的元素分到一起(我们将在本章稍后定义如何衡量“接近”)。例如,如果你在手动分析一个移动应用的用户行为,你可能会把那些频繁登录的客户,或者那些进行较大应用内购买的用户分到一起。这就是像 k-means 这样的聚类算法能够自动从数据中找到的分组。

本章我们将使用由澳大利亚税务局ATO)公开分享的开源数据集。该数据集包含了澳大利亚 2014-15 财政年度内每个邮政编码(也称为邮政区号,是用于按地区排序邮件的识别码)的统计数据。

澳大利亚税务局(ATO)数据集可以在 Packt 的 GitHub 仓库中找到:packt.live/340xO5t

数据集的来源可以在这里找到:packt.live/361i1p3

我们将对这个数据集进行聚类分析,分析两个特定的变量(或列):平均净税额平均总扣除额。我们的目标是找到具有相似税收和扣除模式的邮政编码(或称邮政区号)的分组(或聚类)。以下是这两个变量的散点图:

图 5.3:ATO 数据集的散点图

图 5.3:ATO 数据集的散点图

作为数据科学家的一个重要任务,你需要分析从这个数据集中获得的图表并得出结论。假设你需要手动分析数据集中可能的观察值分组。一个潜在的结果可能如下:

  • 所有位于左下角的数据点可以归为一组(平均净税额从 0 到 40,000)。

  • 第二组可能是所有位于中心区域的数据点(平均净税额从 40,000 到 60,000,平均总扣除额低于 10,000)。

  • 最后,所有剩余的数据点可以归为一组。

但是,既然我们可以使用算法来自动进行这些分组,为什么还需要你手动去猜测呢?这就是我们将在接下来的练习中实践的内容,我们将在此数据集上进行聚类分析。

练习 5.01:在 ATO 数据集上进行第一次聚类分析

在这个练习中,我们将对 ATO 数据集进行 k-means 聚类,并观察数据集划分出的不同簇,之后我们将通过分析输出结果得出结论:

  1. 打开一个新的 Colab 笔记本。

  2. 接下来,加载所需的 Python 包:pandasKMeans,来自sklearn.cluster

    我们将使用 Python 中的import功能:

    import pandas as pd
    from sklearn.cluster import KMeans
    

    注意

    我们将在本章后续详细解释KMeans(来自sklearn.cluster),你在这里的代码中已经使用了它。

  3. 接下来,创建一个变量来保存文件链接。我们将这个变量称为file_url

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter05/DataSet/taxstats2015.csv'
    

    在下一步中,我们将使用pandas包将数据加载到 DataFrame 中(可以把它看作一个表格,类似于 Excel 中的电子表格,具有行索引和列名)。

    我们的输入文件是CSV格式,pandas有一个方法可以直接读取这种格式,即.read_csv()

  4. 使用usecols参数仅选择我们需要的列,而不是加载整个数据集。我们只需要提供一个我们感兴趣的列名列表,这些列名在以下代码片段中提到:

    df = pd.read_csv(file_url, \
                     usecols=['Postcode', \
                              'Average net tax', \
                              'Average total deductions'])
    

    现在我们已经将数据加载到pandas DataFrame 中。

  5. 接下来,使用.head()方法显示 DataFrame 的前 5 行:

    df.head()
    

    你应该得到以下输出:

    图 5.4:ATO 数据框的前五行

    图 5.4:ATO 数据框的前五行

  6. 现在,使用.tail()输出最后 5 行:

    df.tail()
    

    你应该得到以下输出:

    图 5.5:ATO 数据框的最后五行

    图 5.5:ATO 数据框的最后五行

    现在我们已经有了数据,直接进入我们想要做的事情:寻找聚类。

    正如你在前几章中看到的,sklearn提供了相同的 API 用于训练不同的机器学习算法,比如:

    • 使用指定的超参数实例化一个算法(这里是 KMeans(超参数))。

    • 使用.fit()方法用训练数据拟合模型。

    • 使用.predict()方法预测给定输入数据的结果。

      注意

      在这里,我们将使用 k-means 超参数的所有默认值,除了random_state。指定一个固定的随机状态(也叫种子)将帮助我们每次重新运行代码时获得可重复的结果。

  7. 使用随机状态42实例化 k-means,并将其保存到名为kmeans的变量中:

    kmeans = KMeans(random_state=42)
    
  8. 现在将 k-means 与我们的训练数据进行拟合。为此,我们需要只获取用于拟合模型的变量(或列)。在我们的例子中,变量是'Average net tax''Average total deductions',它们保存在一个名为X的新变量中:

    X = df[['Average net tax', 'Average total deductions']]
    
  9. 现在,用这些训练数据拟合kmeans

    kmeans.fit(X)
    

    你应该得到以下输出:

    图 5.6:拟合的 k-means 及其超参数的总结

    图 5.6:拟合的 k-means 及其超参数的总结

    我们仅用几行代码就运行了第一个聚类算法。

  10. 通过使用.predict()方法查看每个数据点属于哪个聚类:

    y_preds = kmeans.predict(X)
    y_preds
    

    你应该得到如下输出:

    图 5.7:k-means 预测输出

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_08.jpg)

    图 5.7:k-means 预测输出

    注意

    尽管我们设置了一个random_state值,但你仍然可能得到与上述不同的聚类编号。这取决于你所使用的 scikit-learn 版本。上述输出是使用版本 0.22.2 生成的。你可以通过执行以下代码来查看你使用的版本:

    import sklearn

    sklearn.__version__

  11. 现在,将这些预测结果添加到原始 DataFrame 中,看看前五个邮政编码:

    df['cluster'] = y_preds
    df.head()
    

    注意

    来自 sklearn predict()方法的预测与输入数据的顺序完全相同。因此,第一个预测将对应于 DataFrame 中的第一行。

    你应该得到如下输出:

    图 5.8:分配给前五个邮政编码的聚类编号

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_08.jpg)

图 5.8:分配给前五个邮政编码的聚类编号

我们的 k-means 模型已经将前两行分配到了同一个聚类中,6。我们可以看到,这两个观测值的平均净税值大约为 28,000。最后三条数据点被分配到不同的聚类中(分别是572),我们可以看到它们的平均总扣除和平均净税值有很大差异。似乎较低的值被分到聚类5,而较高的值被分类到聚类7。我们开始逐步理解 k-means 是如何决定将这个数据集的观测值分组的。

注意

要访问该特定部分的源代码,请参见packt.live/2DUCWAZ

你也可以在packt.live/2EgduFS在线运行这个示例。

这是一个很好的开始。你已经学会了如何用几行代码训练(或拟合)一个 k-means 模型。接下来,我们可以开始深入探讨 k-means 背后的魔力。

解释 k-means 结果

在训练我们的 k-means 算法后,我们可能会对其结果进行更详细的分析。记住,聚类分析的目标是将具有相似模式的观测值分组在一起。但是,如何判断算法找到的分组是否有意义呢?我们将在本节中通过使用刚刚生成的数据集结果来探讨这个问题。

调查此问题的一种方法是逐行分析数据集,并查看每个观测值的分配聚类。这可能会相当繁琐,特别是当数据集非常大时,因此最好有一个聚类结果的总结。

如果你熟悉 Excel 电子表格,你可能会想到使用透视表来计算每个簇的变量平均值。在 SQL 中,你可能会使用GROUP BY语句。如果你不熟悉这两者,你可能会想到将每个簇分组,然后计算它们的平均值。好消息是,这可以通过 Python 中的pandas包轻松实现。让我们通过一个示例来看如何完成这项任务。

为了创建类似于 Excel 的透视表,我们将使用pandaspivot_table()方法。我们需要为该方法指定以下参数:

  • values:此参数对应于你想要计算摘要(或聚合)的数值列,如获取平均值或计数。在 Excel 透视表中,它也被称为values。在我们的数据集中,我们将使用Average net taxAverage total deductions变量。

  • index:此参数用于指定你想要查看汇总的列。在我们的例子中,它是cluster列。在 Excel 透视表中,这对应于Rows字段。

  • aggfunc:在这里,你将指定用于总结数据的聚合函数,如获取平均值或计数。在 Excel 中,这是values字段中的Summarize by选项。下面展示了如何使用aggfunc方法的示例。

    注意

    在与你进行先前练习的同一本笔记本中运行下面的代码。

import numpy as np
df.pivot_table(values=['Average net tax', \
                       'Average total deductions'], \
               index='cluster', aggfunc=np.mean)

注意

我们将使用numpy实现的mean()函数,因为它对 pandas DataFrame 进行了更多优化。

图 5.9:透视表函数的输出

图 5.9:透视表函数的输出

在这个总结中,我们可以看到算法已经将数据分为八个簇(簇 0 到簇 7)。簇 0 的平均净税和总扣除金额在所有簇中最低,而簇 4 的值最高。通过这个透视表,我们可以使用总结值对簇进行相互比较。

使用聚合视图来查看簇之间的差异是一种很好的方式,但这并不是唯一的方式。另一种可能性是将簇可视化成图表。这正是我们接下来要做的。

你可能听说过不同的可视化包,例如matplotlibseabornbokeh,但在本章中,我们将使用altair包,因为它非常简单易用(它的 API 与sklearn非常相似)。首先让我们导入它:

import altair as alt

然后,我们将实例化一个Chart()对象,并将其与我们的 DataFrame 一起保存到一个名为chart的变量中:

chart = alt.Chart(df)

现在,我们将通过.mark_circle()方法指定我们想要的图表类型,即散点图,并将其保存到一个名为scatter_plot的新变量中:

scatter_plot = chart.mark_circle()

最后,我们需要通过指定将作为图表中x轴和y轴的列名来配置我们的散点图。我们还告诉散点图根据每个点的聚类值使用color选项来为每个点着色:

scatter_plot.encode(x='Average net tax', \
                    y='Average total deductions', \
                    color='cluster:N')

注意

你可能已经注意到,在cluster列名的末尾添加了:N。这个额外的参数在altair中用于指定该列的值类型。:N意味着该列中的信息是类别型的。altair会根据列的类型自动定义要使用的颜色方案。

你应该得到以下输出:

图 5.10:聚类的散点图

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_10.jpg)

图 5.10:聚类的散点图

我们现在可以轻松地看到这个图中的聚类及其相互之间的差异。我们可以清楚地看到,k-means 主要根据 x 轴变量(即平均净税)将数据点分配到每个聚类中。聚类的边界是垂直的直线。例如,分隔黄色和橙色聚类的边界大约在 18,000 左右。低于这个限制的观测值被分配到红色聚类(2),高于这个限制的则被分配到紫色聚类(6)。

注意

你可以在以下链接查看本工作坊的高质量彩色图片:packt.live/30O91Bd

如果你使用过TableauPower BI等可视化工具,你可能会感到有些沮丧,因为这个图表是静态的,你无法悬停在每个数据点上获取更多信息,无法找到例如橙色聚类和粉色聚类之间的分界线。但是,使用 altair,这可以轻松实现,这也是我们选择使用它的原因之一。我们可以通过最小的代码更改来为图表添加一些交互功能。

假设我们想要添加一个工具提示,显示我们关心的两个列的值:邮政编码和分配的聚类。使用altair,我们只需要在encode()方法中添加一个名为tooltip的参数,并列出相应的列名,然后在后面调用interactive()方法,代码片段如下所示:

scatter_plot.encode(x='Average net tax', \
                    y='Average total deductions', \
                    color='cluster:N', \
                    tooltip=['Postcode', \
                             'cluster', 'Average net tax', \
                             'Average total deductions'])\
                    .interactive()

你应该得到以下输出:

图 5.11:带工具提示的交互式散点图

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_11.jpg)

图 5.11:带工具提示的交互式散点图

现在,我们可以轻松地悬停并检查接近聚类边界的数据点,发现区分紫色聚类(6)和红色聚类(2)的阈值接近于 32,000 的平均净税

练习 5.02:按商业收入和支出对澳大利亚邮政编码进行聚类

在本练习中,我们将学习如何使用 k-means 进行聚类分析,并根据按商业收入和支出排序的邮政编码值可视化其结果。以下步骤将帮助你完成本练习:

  1. 打开一个新的 Colab 笔记本来进行这个练习。

  2. 现在import所需的包(pandassklearnaltairnumpy):

    import pandas as pd
    from sklearn.cluster import KMeans
    import altair as alt
    import numpy as np
    
  3. 将 ATO 数据集的链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter05/DataSet/taxstats2015.csv'
    
  4. 使用pandas包中的read_csv方法,只加载以下列,并使用use_cols参数:'Postcode''Average total business income''Average total business expenses'

    df = pd.read_csv(file_url, \
                     usecols=['Postcode', \
                              'Average total business income', \
                              'Average total business expenses'])
    
  5. 使用 pandas 的.tail()方法显示 ATO 数据集的最后 10 行:

    df.tail(10)
    

    你应该得到以下输出:

    图 5.12:ATO 数据集的最后 10 行

    图 5.12:ATO 数据集的最后 10 行

  6. 使用以下 pandas 列子集语法提取'Average total business income''Average total business expenses'列:dataframe_name[<list_of_columns>]。然后,将其保存到一个名为X的新变量中:

    X = df[['Average total business income', \
            'Average total business expenses']]
    
  7. 现在,使用一个random_state超参数值为8,用这个新变量来拟合kmeans

    kmeans = KMeans(random_state=8)
    kmeans.fit(X)
    

    你应该得到以下输出:

    图 5.13:拟合的 kmeans 及其超参数摘要

    图 5.13:拟合的 kmeans 及其超参数摘要

  8. 使用sklearn包中的predict方法,从输入变量(X)预测聚类分配结果,将结果保存到一个名为y_preds的新变量中,并显示最后10个预测值:

    y_preds = kmeans.predict(X)
    y_preds[-10:]
    

    你应该得到以下输出:

    图 5.14:分配给最后 10 个观测值的聚类结果

    图 5.14:分配给最后 10 个观测值的聚类结果

  9. 将预测的聚类结果保存回数据框,通过创建一个名为'cluster'的新列,并使用pandas包的.tail()方法打印数据框的最后10行:

    df['cluster'] = y_preds
    df.tail(10)
    

    你应该得到以下输出:

    图 5.15:添加聚类列后的 ATO 数据集最后 10 行

    图 5.15:添加聚类列后的 ATO 数据集最后 10 行

  10. 使用pandas包中的pivot_table方法,根据以下参数生成每个聚类值的两个列的平均值透视表:

    将要聚合的列名'Average total business income''Average total business expenses'提供给参数values

    将要分组的列名'cluster'提供给参数index

    使用 NumPy(np)的.mean方法作为aggfunc参数的聚合函数:

    df.pivot_table(values=['Average total business income', \
                           'Average total business expenses'], \
                   index='cluster', aggfunc=np.mean)
    

    你应该得到以下输出:

    图 5.16:函数的输出

    图 5.16:pivot_table函数的输出

  11. 现在我们来绘制聚类结果,使用交互式散点图。首先,使用altair包中的Chart()mark_circle()来实例化散点图:

    scatter_plot = alt.Chart(df).mark_circle()
    
  12. 使用altairencodeinteractive方法,指定散点图的显示和交互选项,使用以下参数:

    'Average total business income'列的名称提供给x参数(x 轴)。

    'Average total business expenses'列的名称提供给y参数(y 轴)。

    cluster:N列的名称提供给color参数(为每个组提供不同的颜色)。

    将这些列名——'Postcode''cluster''Average total business income''Average total business expenses'——提供给'tooltip'参数(这是提示框显示的信息):

    scatter_plot.encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color='cluster:N', tooltip = ['Postcode', \
                                                      'cluster', \
                        'Average total business income', \
                        'Average total business expenses'])\
                        .interactive()
    

    您应该得到以下输出:

    图 5.17:聚类的交互式散点图

图 5.17:聚类的交互式散点图

注意

要访问此特定部分的源代码,请参考packt.live/3hacbXA

本节目前没有在线交互式示例,但可以像往常一样在 Google Colab 上运行。

我们可以看到,k-means 根据两个变量('Average total business income''Average total business expense')的值将观测值分为八个不同的簇。例如,所有低值数据点都被分配到簇 0,而具有极高值的数据点则属于簇 4。因此,k-means 将具有相似行为的数据点进行了分组。

你刚刚成功地完成了聚类分析并可视化了其结果。你学习了如何加载真实世界的数据集,拟合 k-means 并显示散点图。这是一个很好的开始,我们将在本章接下来的部分中深入探讨如何提高模型的性能。

选择聚类数目

在前面的章节中,我们看到将 k-means 算法应用于给定数据集是多么简单。在我们的 ATO 数据集中,我们发现了 8 个主要由Average net tax变量的值定义的不同簇。

但你可能会问自己:“为什么是 8 个簇?为什么不是 3 个或 15 个簇?”这些确实是很好的问题。简短的回答是我们使用了 k-means 的默认超参数n_cluster值,定义了要找到的簇数为 8。

正如你从第二章《回归》和第四章《使用随机森林进行多类分类》中回忆的那样,超参数的值不是由算法学习的,而是在训练前由你随意设置的。对于 k-means,n_cluster是你需要调整的最重要的超参数之一。选择一个较小的值会导致 k-means 将许多数据点分组在一起,即使它们彼此非常不同。另一方面,选择一个较大的值可能会迫使算法将相似的观测值分割成多个簇,即使它们非常相似。

从 ATO 数据集的散点图来看,八个集群似乎有点多。在图上,一些集群看起来彼此非常接近,并且具有相似的值。凭直觉,仅通过查看图形,你本可以说数据中有两个到四个不同的集群。如你所见,这一点很有启发性,如果有一个函数能够帮助我们定义数据集的正确集群数,那该有多好。确实存在这样一个方法,叫做肘部法则。

该方法评估集群的紧凑性,目标是最小化一个称为惯性的值。更多的细节和解释将在本章后面提供。现在,可以将惯性视为一个值,表示对于一组数据点,它们之间有多远或有多近。

让我们将此方法应用于我们的 ATO 数据集。首先,我们将定义一个我们希望评估的聚类数量范围(1 到 10 之间),并将其保存在一个名为clusters的数据框中。我们还将创建一个空列表inertia,在其中存储计算得到的值。

注意

打开你用于练习 5.01的笔记本,在 ATO 数据集上执行你的第一次聚类分析,执行你已输入的代码,然后在笔记本末尾继续以下代码。

clusters = pd.DataFrame()
clusters['cluster_range'] = range(1, 10)
inertia = []

接下来,我们将创建一个for循环,遍历范围,使用指定数量的clusters拟合 k-means 模型,提取inertia值,并将其存储在我们的列表中,如以下代码片段所示:

for k in clusters['cluster_range']:
    kmeans = KMeans(n_clusters=k, random_state=8).fit(X)
    inertia.append(kmeans.inertia_)

现在我们可以使用clusters数据框中的inertia值:

clusters['inertia'] = inertia
clusters

你应该得到以下输出:

图 5.18:包含我们集群惯性值的数据框

图 5.18:包含我们集群惯性值的数据框

然后,我们需要使用altair绘制一张折线图,使用mark_line()方法。我们将指定'cluster_range'列作为 x 轴,'inertia'作为 y 轴,如以下代码片段所示:

alt.Chart(clusters).mark_line()\
                   .encode(x='cluster_range', y='inertia')

你应该得到以下输出:

图 5.19:绘制肘部法则

图 5.19:绘制肘部法则

注意

你不需要将每个altair对象保存在一个单独的变量中;你可以将方法一个接一个地附加,用""。

现在我们已经绘制了惯性值与簇数量的关系图,我们需要找到最优的簇数量。我们需要做的是找到图中的拐点,即惯性值开始变得较慢下降的地方(也就是曲线斜率几乎达到 45 度的地方)。找到正确的拐点可能有些困难。如果你把这条折线图想象成一只手臂,我们要做的就是找到肘部的中心(现在你知道这个方法的名字来源于哪里了)。因此,看看我们的例子,我们可以说最优的簇数量是三个。如果我们继续增加簇的数量,惯性值就不会大幅度下降,也不会带来任何价值。这就是为什么我们要找到肘部的中间点作为拐点的原因。

现在,让我们使用这个超参数重新训练Kmeans并绘制簇,如下代码片段所示:

kmeans = KMeans(random_state=42, n_clusters=3)
kmeans.fit(X)
df['cluster2'] = kmeans.predict(X)
scatter_plot.encode(x='Average net tax', \
                    y='Average total deductions', \
                    color='cluster2:N', \
                    tooltip=['Postcode', 'cluster', \
                             'Average net tax', \
                             'Average total deductions'])\
                    .interactive()

你应该得到以下输出:

图 5.20:三个簇的散点图

图 5.20:三个簇的散点图

这与我们最初的结果非常不同。观察这三个簇,我们可以看到:

  • 第一个簇(红色)表示那些在平均净税和总扣除方面都较低的邮政编码。

  • 第二个簇(蓝色)表示中等的平均净税和低的平均总扣除。

  • 第三个簇(橙色)将所有平均净税值超过 35,000 的邮政编码归为一类。

    注意

    值得注意的是,第三个簇的数据点分布较广,这可能表明该组中有一些离群值。

这个示例向我们展示了在训练 k-means 算法之前,定义正确的簇数量有多么重要,特别是当我们希望从数据中获得有意义的分组时。我们使用了一种叫做肘部法则的方法来找到这个最优数量。

练习 5.03:寻找最优簇的数量

在这个练习中,我们将应用肘部法则,使用与练习 5.02中相同的数据集,即通过商业收入和支出对澳大利亚邮政编码进行聚类,以找到最优的簇数量,然后再拟合 k-means 模型:

  1. 打开一个新的 Colab 笔记本进行此练习。

  2. 现在import所需的包(pandassklearnaltair):

    import pandas as pd
    from sklearn.cluster import KMeans
    import altair as alt
    

    接下来,我们将加载数据集,并选择与练习 5.02中相同的列,即通过商业收入和支出对澳大利亚邮政编码进行聚类,并打印前五行。

  3. 将 ATO 数据集的链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter05/DataSet/taxstats2015.csv'
    
  4. 使用 pandas 包中的.read_csv()方法,加载数据集并仅使用use_cols参数选择以下列:'Postcode''Average total business income''Average total business expenses'

    df = pd.read_csv(file_url, \
                     usecols=['Postcode', \
                              'Average total business income', \
                              'Average total business expenses'])
    
  5. 使用 pandas 包中的.head()方法显示数据框的前五行:

    df.head()
    

    你应该得到以下输出:

    图 5.21:ATO 数据框的前五行

    图 5.21:ATO 数据框的前五行

  6. 'Average total business income''Average total business expenses' 列分配给一个新变量,命名为 X

    X = df[['Average total business income', \
            'Average total business expenses']]
    
  7. 创建一个空的 pandas DataFrame,命名为 clusters,并创建一个空的列表,命名为 inertia

    clusters = pd.DataFrame()
    inertia = []
    

    现在,使用 range 函数生成一个包含集群数量范围的列表,从 115,并将其分配到来自 'clusters' DataFrame 的名为 'cluster_range' 的新列:

    clusters['cluster_range'] = range(1, 15)
    
  8. 创建一个 for 循环,遍历每个集群编号,并相应地拟合一个 k-means 模型,然后使用 'inertia_' 参数将 inertia 值附加到 'inertia' 列表中:

    for k in clusters['cluster_range']:
        kmeans = KMeans(n_clusters=k).fit(X)
        inertia.append(kmeans.inertia_)
    
  9. inertia 列分配到名为 'inertia' 的新列中,该列来自 clusters DataFrame,并显示其内容:

    clusters['inertia'] = inertia
    clusters
    

    你应该得到以下输出:

    图 5.22:绘制肘部法则

    图 5.22:绘制肘部法则

  10. 现在,使用 altair 包中的 mark_line()encode() 绘制肘部图,'cluster_range' 为 x 轴,'inertia' 为 y 轴:

    alt.Chart(clusters).mark_line()\
       .encode(alt.X('cluster_range'), alt.Y('inertia'))
    

    你应该得到以下输出:

    图 5.23:绘制肘部法则

    图 5.23:绘制肘部法则

  11. 查看肘部图,确定最优集群数量,并将此值分配给名为 optim_cluster 的变量:

    optim_cluster = 4
    
  12. 使用 sklearn 中的 fit 方法,训练一个具有此集群数量和 random_state 值为 42 的 k-means 模型:

    kmeans = KMeans(random_state=42, n_clusters=optim_cluster)
    kmeans.fit(X)
    
  13. 现在,使用 sklearn 中的 predict 方法,为 X 变量中包含的每个数据点获取预测的分配集群,并将结果保存到名为 'cluster2' 的新列中,该列来自 df DataFrame:

    df['cluster2'] = kmeans.predict(X)
    
  14. 使用 pandas 包中的 head 方法显示 df DataFrame 的前五行:

    df.head()
    

    你应该得到以下输出:

    图 5.24:前五行与集群预测

    图 5.24:前五行与集群预测

  15. 现在使用 altair 包中的 mark_circle()encode() 方法绘制散点图。同时,为了增加交互性,使用 tooltip 参数和 altair 包中的 interactive() 方法,如以下代码片段所示:

    alt.Chart(df).mark_circle()\
                 .encode\
                  (x='Average total business income', \
                   y='Average total business expenses', \
                   color='cluster2:N', \
                   tooltip=['Postcode', 'cluster2', \
                            'Average total business income',\
                            'Average total business expenses'])\
                 .interactive()
    

    你应该得到以下输出:

    图 5.25:四个集群的散点图

图 5.25:四个集群的散点图

注意

要访问此特定部分的源代码,请参考 packt.live/3iK1rPQ

本部分目前没有在线交互示例,但可以像往常一样在 Google Colab 上运行。

你刚刚学习了如何在拟合 k-means 模型之前找到最优集群数量。数据点在我们的输出中被分成了四个不同的集群:

  • 集群 0(蓝色)包含所有平均总营业收入低于 100,000 且平均总营业支出低于 80,000 的观察值。

  • 簇 1(橙色)包含那些总商业收入值低于 180,000,且总商业支出值低于 160,000 的数据点。

  • 簇 3(青色)适用于那些总商业收入值低于 370,000,且总商业支出值低于 330,000 的数据点。

  • 簇 2(红色)适用于那些具有极端值的数据点——其总商业收入值高于 370,000,且总商业支出值高于 330,000。

练习 5.02 中的结果,根据商业收入和支出对澳大利亚邮政编码进行聚类,有八个不同的簇,其中一些簇之间非常相似。在这里,你看到拥有最优簇数能更好地区分各个组,这也是为什么它是 k-means 中最重要的超参数之一。接下来的部分,我们将研究初始化 k-means 时的另外两个重要超参数。

初始化簇

从本章开始,我们每次拟合聚类算法时都提到了 k-means。但你可能已经注意到,在每个模型总结中都有一个叫做 init 的超参数,其默认值为 k-means++。事实上,我们一直在使用 k-means++。

k-means 和 k-means++ 之间的区别在于它们在训练开始时如何初始化簇。k-means 随机选择每个簇的中心(称为质心),然后将每个数据点分配到最近的簇。如果簇初始化选择不当,可能会导致训练过程结束时分组不理想。例如,在下图中,我们可以清晰地看到数据的三个自然分组,但算法并未成功地正确识别它们:

图 5.26:非最优簇的示例

图 5.26:非最优簇的示例

k-means++ 是一种尝试在初始化时找到更好簇的算法。它的基本思路是随机选择第一个簇,然后通过从剩余的数据点中使用概率分布选择距离更远的其他簇。尽管 k-means++ 相较于原始的 k-means 通常能得到更好的结果,但在某些情况下,它仍然可能导致不理想的聚类。

数据科学家可以使用的另一个超参数来降低错误聚类的风险是 n_init。这对应于 k-means 运行的次数,每次使用不同的初始化,最终模型是最佳运行结果。因此,如果这个超参数设置得很高,你将有更大的机会找到最优簇,但缺点是训练时间会更长。所以,你必须仔细选择这个值,特别是在数据集较大的情况下。

让我们通过以下示例在 ATO 数据集上尝试一下。

注意

打开你为练习 5.01对 ATO 数据集进行第一次聚类分析,以及之前的示例所用的笔记本。执行你已输入的代码,然后继续执行笔记本末尾的以下代码。

首先,让我们仅使用随机初始化运行一次迭代:

kmeans = KMeans(random_state=14, n_clusters=3, \
                init='random', n_init=1)
kmeans.fit(X)

如常,我们希望通过散点图来可视化我们的聚类,正如以下代码片段所定义的那样:

df['cluster3'] = kmeans.predict(X)
alt.Chart(df).mark_circle()\
             .encode(x='Average net tax', \
                     y='Average total deductions', \
                     color='cluster3:N', \
                     tooltip=['Postcode', 'cluster', \
                              'Average net tax', \
                              'Average total deductions']) \
             .interactive()

你应该会得到以下输出:

![图 5.27:使用 n_init 为 1 和 init 为 random 的聚类结果]

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_27.jpg)

图 5.27:使用 n_init 为 1 和 init 为 random 的聚类结果

总体来看,结果与我们之前的运行非常接近。值得注意的是,聚类之间的边界略有不同。

现在,让我们尝试使用五次迭代(通过n_init超参数)和 k-means++初始化(通过init超参数):

kmeans = KMeans(random_state=14, n_clusters=3, \
                init='k-means++', n_init=5)
kmeans.fit(X)
df['cluster4'] = kmeans.predict(X)
alt.Chart(df).mark_circle()\
             .encode(x='Average net tax', \
                     y='Average total deductions', \
                     color='cluster4:N', \
                     tooltip=['Postcode', 'cluster', \
                              'Average net tax', \
                              'Average total deductions'])\
                    .interactive()

你应该会得到以下输出:

![图 5.28:使用 n_init 为 5 和 init 为 k-means++的聚类结果]

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_28.jpg)

图 5.28:使用 n_init 为 5 和 init 为 k-means++的聚类结果

在这里,结果与使用 10 次迭代的原始运行非常接近。这意味着我们不必运行这么多迭代就能使 k-means 收敛,本可以通过较少的迭代节省一些时间。

练习 5.04:使用不同的初始化参数来获得合适的结果

在本练习中,我们将使用与练习 5.02按商业收入和支出聚类澳大利亚邮政编码中相同的数据,并尝试不同的initn_init超参数值,观察它们如何影响最终的聚类结果:

  1. 打开一个新的 Colab 笔记本。

  2. 导入所需的包,包括pandassklearnaltair

    import pandas as pd
    from sklearn.cluster import KMeans
    import altair as alt
    
  3. 将 ATO 数据集的链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter05/DataSet/taxstats2015.csv'
    
  4. 加载数据集,并选择与练习 5.02按商业收入和支出聚类澳大利亚邮政编码,以及练习 5.03寻找最佳聚类数中相同的列,使用pandas包中的read_csv()方法:

    df = pd.read_csv(file_url, \
                     usecols=['Postcode', \
                              'Average total business income', \
                              'Average total business expenses'])
    
  5. 'Average total business income''Average total business expenses'列赋值给一个新变量X

    X = df[['Average total business income', \
            'Average total business expenses']]
    
  6. 使用n_init等于1和随机init来拟合一个 k-means 模型:

    kmeans = KMeans(random_state=1, n_clusters=4, \
                    init='random', n_init=1)
    kmeans.fit(X)
    
  7. 使用sklearn包中的predict方法,从输入变量(X)中预测聚类分配,并将结果保存到名为'cluster3'的新列中:

    df['cluster3'] = kmeans.predict(X)
    
  8. 使用交互式散点图绘制聚类。首先,使用altair包中的Chart()mark_circle()来实例化散点图,如以下代码片段所示:

    scatter_plot = alt.Chart(df).mark_circle()
    
  9. 使用altair中的encodeinteractive方法,指定散点图的显示方式及其交互选项,使用以下参数:

    'Average total business income'列的名称传递给x参数(x 轴)。

    'Average total business expenses' 列的名称提供给 y 参数(y 轴)。

    'cluster3:N' 列的名称提供给 color 参数(用于定义每个组的不同颜色)。

    将以下列名 'Postcode''cluster3''Average total business income''Average total business expenses' 提供给 tooltip 参数:

    scatter_plot.encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color='cluster3:N', \
                        tooltip=['Postcode', 'cluster3', \
                                 'Average total business income', \
                                 'Average total business expenses'])\
                       .interactive()
    

    你应该得到以下输出:

    图 5.29:使用  为 1 和  为随机的聚类结果

    图 5.29:使用 n_init 为 1 和 init 为随机的聚类结果

  10. 重复 步骤 5步骤 8,但使用不同的 k-means 超参数 n_init=10 和随机 init,如以下代码片段所示:

    kmeans = KMeans(random_state=1, n_clusters=4, \
                    init='random', n_init=10)
    kmeans.fit(X)
    df['cluster4'] = kmeans.predict(X)
    scatter_plot = alt.Chart(df).mark_circle()
    scatter_plot.encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color='cluster4:N',
                        tooltip=['Postcode', 'cluster4', \
                                 'Average total business income', \
                                 'Average total business expenses'])\
                       .interactive()
    

    你应该得到以下输出:

    图 5.30:使用  为 10 和  为随机的聚类结果

    图 5.30:使用 n_init 为 10 和 init 为随机的聚类结果

  11. 再次重复 步骤 5步骤 8,但使用不同的 k-means 超参数 — n_init=100 和随机 init

    kmeans = KMeans(random_state=1, n_clusters=4, \
                    init='random', n_init=100)
    kmeans.fit(X)
    df['cluster5'] = kmeans.predict(X)
    scatter_plot = alt.Chart(df).mark_circle()
    scatter_plot.encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color='cluster5:N', \
                        tooltip=['Postcode', 'cluster5', \
                        'Average total business income', \
                        'Average total business expenses'])\
                .interactive()
    

    你应该得到以下输出:

图 5.31:使用  为 10 和  为随机的聚类结果

图 5.31:使用 n_init 为 10 和 init 为随机的聚类结果

注意

要访问此特定部分的源代码,请参考 packt.live/315yGqP

本节目前没有在线交互示例,但可以像往常一样在 Google Colab 上运行。

你刚刚学会了如何调整负责初始化 k-means 聚类的两个主要超参数。你在这个练习中看到,增加 n_init 的迭代次数对这个数据集的聚类结果影响不大。

在这种情况下,最好使用较低的超参数值,因为这将加速训练时间。但对于不同的数据集,你可能会遇到一个案例,其中结果会根据 n_init 值的不同而发生巨大变化。在这种情况下,你需要找到一个既不太小也不太大的 n_init 值。你想找到一个最佳值,使得结果与之前使用不同值获得的结果相比变化不大。

计算到质心的距离

我们在前面的章节中讨论了数据点之间的相似性,但实际上并没有定义这意味着什么。你可能已经猜到,这与观察值彼此之间的接近程度或远离程度有关。你猜对了。这与两点之间某种距离的度量有关。k-means 使用的距离度量称为 平方欧几里得距离,其公式为:

图 5.32:平方欧几里得距离公式

图 5.32:平方欧几里得距离公式

如果你没有统计学背景,这个公式可能看起来有点吓人,但实际上它非常简单。它是数据坐标之间平方差的总和。这里,xy是两个数据点,索引i表示坐标的数量。如果数据是二维的,i等于 2。同样,如果是三维数据,i将是 3。

我们将此公式应用于 ATO 数据集。

首先,我们将抓取所需的值——也就是来自前两个观测值的坐标——并打印出来:

注意

打开你在练习 5.01中使用的笔记本,在 ATO 数据集上执行第一次聚类分析,以及之前的示例。执行你已经输入的代码,然后在笔记本的末尾继续输入以下代码。

x = X.iloc[0,].values
y = X.iloc[1,].values
print(x)
print(y)

你应该得到以下输出:

图 5.33:从 ATO 数据集中提取前两个观测值

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_33.jpg)

图 5.33:从 ATO 数据集中提取前两个观测值

注意

在 pandas 中,iloc方法用于通过索引子集化 DataFrame 的行或列。例如,如果我们想获取第 888 行和第 6 列,我们可以使用以下语法:dataframe.iloc[888, 6]

x的坐标是(27555, 2071)y的坐标是(28142, 3804)。这里,公式告诉我们计算两个数据点在每个坐标轴上的平方差并将它们相加:

squared_euclidean = (x[0] - y[0])**2 + (x[1] - y[1])**2
print(squared_euclidean)

你应该得到以下输出:

3347858

k-means 使用此度量计算每个数据点与其分配的聚类中心(也称为质心)之间的距离。以下是该算法的基本逻辑:

  1. 随机选择聚类的中心(质心)。

  2. 使用平方欧几里得距离将每个数据点分配给最近的质心。

  3. 更新每个质心的坐标为分配给它的数据点的新计算中心。

  4. 重复步骤 2步骤 3,直到聚类收敛(即直到聚类分配不再变化)或直到达到最大迭代次数为止。

就这样。k-means 算法就是这么简单。我们可以在拟合 k-means 模型后,通过cluster_centers_提取质心。

让我们看看如何在一个示例中绘制质心。

首先,我们拟合一个 k-means 模型,如以下代码片段所示:

kmeans = KMeans(random_state=42, n_clusters=3, \
                init='k-means++', n_init=5)
kmeans.fit(X)
df['cluster6'] = kmeans.predict(X)

现在将centroids提取到一个 DataFrame 并打印出来:

centroids = kmeans.cluster_centers_
centroids = pd.DataFrame(centroids, \
                         columns=['Average net tax', \
                                  'Average total deductions'])
print(centroids)

你应该得到以下输出:

图 5.34:三个质心的坐标

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_34.jpg)

图 5.34:三个质心的坐标

我们将绘制常规的散点图,但将其分配给一个名为chart1的变量:

chart1 = alt.Chart(df).mark_circle()\
            .encode(x='Average net tax', \
                    y='Average total deductions', \
                    color='cluster6:N', \
                    tooltip=['Postcode', 'cluster6', \
                             'Average net tax', \
                             'Average total deductions'])\
                   .interactive()
chart1

你应该得到以下输出:

图 5.35:聚类的散点图

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_35.jpg)

图 5.35:聚类的散点图

现在,为质心创建第二个散点图,命名为chart2

chart2 = alt.Chart(centroids).mark_circle(size=100)\
            .encode(x='Average net tax', \
                    y='Average total deductions', \
                    color=alt.value('black'), \
                    tooltip=['Average net tax', \
                             'Average total deductions'])\
                   .interactive()
chart2

你应该得到以下输出:

图 5.36:质心的散点图

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_36.jpg)

图 5.36:质心的散点图

现在我们将这两个图表组合在一起,使用altair非常简单:

chart1 + chart2

你应该得到如下输出:

图 5.37:簇及其质心的散点图

图 5.37:簇及其质心的散点图

现在我们可以轻松地看到观测值最接近哪些质心。

练习 5.05:寻找我们数据集中的最近质心

在本练习中,我们将编写 k-means 的第一轮代码,以将数据点分配给它们最近的簇质心。以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本。

  2. 现在import所需的包,它们是pandassklearnaltair

    import pandas as pd
    from sklearn.cluster import KMeans
    import altair as alt
    
  3. 加载数据集,并选择与练习 5.02按商业收入和支出对澳大利亚邮政编码进行聚类中相同的列,使用pandas包中的read_csv()方法:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter05/DataSet/taxstats2015.csv'
    df = pd.read_csv(file_url, \
                     usecols=['Postcode', \
                              'Average total business income', \
                              'Average total business expenses'])
    
  4. 'Average total business income''Average total business expenses'列赋值给一个名为X的新变量:

    X = df[['Average total business income', \
            'Average total business expenses']]
    
  5. 现在,计算'Average total business income''Average total business income'变量的最小值和最大值,使用min()max()方法,如下代码片段所示:

    business_income_min = df['Average total business income'].min()
    business_income_max = df['Average total business income'].max()
    business_expenses_min = df['Average total business expenses']\
                            .min()
    business_expenses_max = df['Average total business expenses']\
                            .max()
    
  6. 打印这四个变量的值,它们是这两个变量的最小值和最大值:

    print(business_income_min)
    print(business_income_max)
    print(business_expenses_min)
    print(business_expenses_max)
    

    你应该得到如下输出:

    0
    876324
    0
    884659
    
  7. 现在导入random包并使用seed()方法将种子设置为42,如下代码片段所示:

    import random
    random.seed(42)
    
  8. 创建一个空的 pandas 数据框,并将其赋值给名为centroids的变量:

    centroids = pd.DataFrame()
    
  9. 使用random包中的sample()方法生成四个随机值,可能的取值范围在 'Average total business expenses' 列的最小值和最大值之间,使用range(),并将结果存储在名为 'Average total business income' 的新列中,该列位于centroids数据框中:

    centroids\
    ['Average total business income'] = random.sample\
                                        (range\
                                        (business_income_min, \
                                         business_income_max), 4)
    
  10. 重复相同的过程,为'Average total business expenses'生成4个随机值:

    centroids\
    ['Average total business expenses'] = random.sample\
                                          (range\
                                          (business_expenses_min,\
                                           business_expenses_max), 4)
    
  11. 使用 pandas 包中的.index属性从centroids数据框中创建一个名为'cluster'的新列,并打印该数据框:

    centroids['cluster'] = centroids.index
    centroids
    

    你应该得到如下输出:

    图 5.38:四个随机质心的坐标

    图 5.38:四个随机质心的坐标

  12. 使用altair包创建一个散点图,显示df数据框中的数据,并将其保存在名为'chart1'的变量中:

    chart1 = alt.Chart(df.head()).mark_circle()\
                .encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color=alt.value('orange'), \
                        tooltip=['Postcode', \
                                 'Average total business income', \
                                 'Average total business expenses'])\
                       .interactive()
    
  13. 现在,使用altair包创建第二个散点图,显示质心,并将其保存在名为'chart2'的变量中:

    chart2 = alt.Chart(centroids).mark_circle(size=100)\
                .encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color=alt.value('black'), \
                        tooltip=['cluster', \
                                 'Average total business income',\
                                 'Average total business expenses'])\
                       .interactive()
    
  14. 使用 altair 语法将两个图表显示在一起:<chart> + <chart>

    chart1 + chart2
    

    你应该得到如下输出:

    图 5.39:随机质心和前五个观测值的散点图

    图 5.39:随机质心和前五个观测值的散点图

  15. 定义一个函数,用于计算 squared_euclidean 距离并返回其值。此函数将接受一个数据点的 xy 坐标以及一个质心:

    def squared_euclidean(data_x, data_y, \
                          centroid_x, centroid_y, ):
        return (data_x - centroid_x)**2 + (data_y - centroid_y)**2
    
  16. 使用 pandas 包的 .at 方法,提取第一行的 xy 坐标,并将其保存在两个变量 data_xdata_y 中:

    data_x = df.at[0, 'Average total business income']
    data_y = df.at[0, 'Average total business expenses']
    
  17. 使用 for 循环或列表推导,计算第一条观测值(使用其 data_xdata_y 坐标)与 centroids4 个不同质心的 squared_euclidean 距离,将结果保存在名为 distance 的变量中并显示出来:

    distances = [squared_euclidean\
                 (data_x, data_y, centroids.at\
                  [i, 'Average total business income'], \
                  centroids.at[i, \
                  'Average total business expenses']) \
                  for i in range(4)]
    distances
    

    你应该得到以下输出:

    [215601466600, 10063365460, 34245932020, 326873037866]
    
  18. 使用包含 squared_euclidean 距离的列表的 index 方法,找到距离最短的簇,如以下代码片段所示:

    cluster_index = distances.index(min(distances))
    
  19. 使用 pandas 包的 .at 方法,在 df 数据框中为第一条观测值将 cluster 索引保存在名为 'cluster' 的列中:

    df.at[0, 'cluster'] = cluster_index
    
  20. 使用 pandas 包的 head() 方法显示 df 的前五行:

    df.head()
    

    你应该得到以下输出:

    图 5.40:ATO 数据框前五行的分配    第一行的簇编号

    图 5.40:ATO 数据框前五行及其分配的第一行簇编号

  21. 重复步骤 15步骤 19,对接下来的 4 行计算它们与质心的距离,并找出距离值最小的簇:

    distances = [squared_euclidean\
                 (df.at[1, 'Average total business income'], \
                  df.at[1, 'Average total business expenses'], \
                  centroids.at[i, 'Average total business income'],\
                  centroids.at[i, \
                               'Average total business expenses'])\
                 for i in range(4)]
    df.at[1, 'cluster'] = distances.index(min(distances))
    distances = [squared_euclidean\
                 (df.at[2, 'Average total business income'], \
                  df.at[2, 'Average total business expenses'], \
                  centroids.at[i, 'Average total business income'],\
                  centroids.at[i, \
                               'Average total business expenses'])\
                 for i in range(4)]
    df.at[2, 'cluster'] = distances.index(min(distances))
    distances = [squared_euclidean\
                 (df.at[3, 'Average total business income'], \
                  df.at[3, 'Average total business expenses'], \
                  centroids.at[i, 'Average total business income'],\
                  centroids.at[i, \
                               'Average total business expenses'])\
                 for i in range(4)]
    df.at[3, 'cluster'] = distances.index(min(distances))
    distances = [squared_euclidean\
                 (df.at[4, 'Average total business income'], \
                  df.at[4, 'Average total business expenses'], \
                  centroids.at[i, \
                  'Average total business income'], \
                  centroids.at[i, \
                  'Average total business expenses']) \
                 for i in range(4)]
    df.at[4, 'cluster'] = distances.index(min(distances))
    df.head()
    

    你应该得到以下输出:

    图 5.41:ATO 数据框的前五行及其分配的簇

    图 5.41:ATO 数据框的前五行及其分配的簇

  22. 最后,使用 altair 包绘制质心和数据集的前 5 行,如步骤 12步骤 13所示:

    chart1 = alt.Chart(df.head()).mark_circle()\
                .encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color='cluster:N', \
                        tooltip=['Postcode', 'cluster', \
                                 'Average total business income', \
                                 'Average total business expenses'])\
                       .interactive()
    chart2 = alt.Chart(centroids).mark_circle(size=100)\
                .encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color=alt.value('black'), \
                        tooltip=['cluster', \
                                 'Average total business income',\
                                 'Average total business expenses'])\
                       .interactive()
    chart1 + chart2
    

    你应该得到以下输出:

图 5.42:随机质心和前五条观测值的散点图

图 5.42:随机质心和前五条观测值的散点图

注意

要访问此特定部分的源代码,请参考 packt.live/3iPQo7L

本节目前没有在线互动示例,但可以在 Google Colab 上正常运行。

在这个最终结果中,我们可以看到四个簇在图中的位置,以及五个数据点被分配到哪个簇:

  • 左下角的两个数据点已分配给簇 2,该簇的质心坐标为 26,000(平均总商业收入)和 234,000(平均总商业支出)。对于这两个点,它是最接近的质心。

  • 中间的两个观测值非常接近坐标为 116,000(平均总商业收入)和 256,000(平均总商业支出)的质心,该质心对应簇 1。

  • 上方的观察数据被分配到了聚类 0,该聚类的质心坐标为 670,000(平均总营业收入)和 288,000(平均总营业支出)。

你刚刚重新实现了 k-means 算法的大部分内容。你了解了如何随机初始化质心(聚类中心)、计算某些数据点的平方欧几里得距离、找到它们最近的质心并将它们分配到相应的聚类中。这并不容易,但你做到了。

标准化数据

你已经学到了很多关于 k-means 算法的内容,我们也接近本章的结尾了。在这一节,我们将不讨论另一个超参数(你已经学习过主要的超参数),而是一个非常重要的话题:数据处理

拟合 k-means 算法非常简单。最棘手的部分是确保得到的聚类对你的项目有意义,我们已经看到如何调整一些超参数来确保这一点。但处理输入数据和你迄今为止学到的所有步骤一样重要。如果你的数据集没有很好地准备,即使你找到了最佳的超参数,你依然会得到不好的结果。

让我们再看看我们的 ATO 数据集。在上一节中,计算到质心的距离,我们发现了三个不同的聚类,它们主要是由'平均净税额'变量定义的。就好像 k-means 完全没有考虑到第二个变量'平均总扣除额'。实际上,这是因为这两个变量具有非常不同的值范围,以及平方欧几里得距离的计算方式。

平方欧几里得距离对高值变量的加权影响较大。让我们通过一个例子来说明这一点,假设有两个数据点 A 和 B,分别具有坐标(1, 50000)和(100, 100000)。A 和 B 之间的平方欧几里得距离将是(100000 - 50000)² + (100 - 1)²。我们可以清楚地看到,结果将主要受到 100,000 与 50,000 差异的驱动:50,000²。而 100 与 1 的差异(99²)在最终结果中的影响微乎其微。

但是,如果你看一下 100,000 与 50,000 的比值,它是一个 2 的倍数(100,000 / 50,000 = 2),而 100 与 1 的比值是 100 的倍数(100 / 1 = 100)。对于数值较大的变量“主导”聚类结果是否有意义呢?这真的取决于你的项目,这种情况可能是故意的。但如果你希望不同轴之间的结果公平,最好在拟合 k-means 模型之前将它们标准化到一个相似的数值范围。这就是为什么在运行 k-means 算法之前,你总是需要考虑标准化数据的原因。

有多种方式可以对数据进行标准化,我们将重点介绍两种最常用的方法:sklearn库中实现了这两种方法。

最小-最大标准化的公式非常简单:在每个轴上,你需要将每个数据点的最小值减去,并将结果除以最大值和最小值之间的差异。标度化后的数据将在 0 到 1 之间:

图 5.43:最小-最大标准化公式

图 5.43:最小-最大标准化公式

让我们看看在下面的示例中使用sklearn进行最小-最大标准化。

注意

打开你用于练习 5.01在 ATO 数据集上执行你的第一个聚类分析和之前的例子的笔记本。执行你已经输入的代码,然后继续在笔记本的末尾使用以下代码。

首先,我们导入相关的类并实例化一个对象:

from sklearn.preprocessing import MinMaxScaler
min_max_scaler = MinMaxScaler()

然后,我们将其适配到我们的数据集中:

min_max_scaler.fit(X)

你应该获得以下输出:

图 5.44:最小-最大标准化摘要

图 5.44:最小-最大标准化摘要

最后,调用transform()方法来标准化数据:

X_min_max = min_max_scaler.transform(X)
X_min_max

你应该获得以下输出:

图 5.45:最小-最大标准化数据

图 5.45:最小-最大标准化数据

现在我们打印最小和最大值的最小-最大标准化数据的每个轴:

X_min_max[:,0].min(), X_min_max[:,0].max(), \
X_min_max[:,1].min(), X_min_max[:,1].max()

你应该获得以下输出:

图 5.46:最小和最大值的最小-最大标准化数据

图 5.46:最小和最大值的最小-最大标准化数据

我们可以看到现在两个轴的值都在 0 到 1 之间。

z-score是通过将数据点从总体平均值中移除并将结果除以每个轴的标准差来计算的。标准化数据的分布将具有平均值为 0 和标准差为 1:

图 5.47:Z-score 公式

图 5.47:Z-score 公式

要在sklearn中应用它,首先我们必须导入相关的StandardScaler类并实例化一个对象:

from sklearn.preprocessing import StandardScaler
standard_scaler = StandardScaler()

这次,我们不再调用fit()然后调用transform(),而是使用fit_transform()方法:

X_scaled = standard_scaler.fit_transform(X)
X_scaled

你应该获得以下输出:

图 5.48:Z-score 标准化数据

图 5.48:Z-score 标准化数据

现在我们将查看每个轴的最小和最大值:

X_scaled[:,0].min(), X_scaled[:,0].max(), \
X_scaled[:,1].min(), X_scaled[:,1].max()

你应该获得以下输出:

图 5.49:Z-score 标准化数据的最小和最大值

图 5.49:Z-score 标准化数据的最小和最大值

现在,两个轴的值域大大降低,我们可以看到它们的最大值约为 9 和 18,这表明数据中存在一些极端异常值。

现在,使用以下代码片段拟合 k-means 模型并在 Z-score 标准化数据上绘制散点图:

kmeans = KMeans(random_state=42, n_clusters=3, \
                init='k-means++', n_init=5)
kmeans.fit(X_scaled)
df['cluster7'] = kmeans.predict(X_scaled)
alt.Chart(df).mark_circle()\
             .encode(x='Average net tax', \
                     y='Average total deductions', \
                     color='cluster7:N', \
                     tooltip=['Postcode', 'cluster7', \
                              'Average net tax', \
                              'Average total deductions'])\
                    .interactive()

你应该获得以下输出:

图 5.50:标准化数据的散点图

图 5.50:标准化数据的散点图

k-means 结果与标准化数据有很大的不同。现在我们可以看到有两个主要的簇(蓝色和红色),它们的边界不再是直的垂直线,而是对角线。所以,k-means 现在实际上考虑了两个轴。橙色簇包含的数据点比之前的迭代要少得多,似乎它将所有高值的极端异常值都归为一组。如果你的项目是关于检测异常的,你可以在这里找到一种方法,轻松将异常值与“正常”观测值分离。

练习 5.06: 标准化我们的数据集中的数据

在这个最终练习中,我们将使用最小-最大缩放和 z-score 标准化来标准化数据,并为每种方法拟合一个 k-means 模型,查看它们对 k-means 的影响:

  1. 打开一个新的 Colab 笔记本。

  2. 现在导入所需的pandassklearnaltair包:

    import pandas as pd
    from sklearn.cluster import KMeans
    import altair as alt 
    
  3. 加载数据集并选择与练习 5.02中相同的列,通过商业收入和支出对澳大利亚邮政编码进行聚类,使用pandas包中的read_csv()方法:

    file_url = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter05/DataSet/taxstats2015.csv'
    df = pd.read_csv(file_url, \
                     usecols=['Postcode', \
                              'Average total business income', \
                              'Average total business expenses'])
    
  4. 'Average total business income''Average total business expenses'列分配给一个名为X的新变量:

    X = df[['Average total business income', \
            'Average total business expenses']]
    
  5. sklearn导入MinMaxScalerStandardScaler类:

    from sklearn.preprocessing import MinMaxScaler
    from sklearn.preprocessing import StandardScaler
    
  6. 实例化并用数据拟合MinMaxScaler

    min_max_scaler = MinMaxScaler()
    min_max_scaler.fit(X)
    

    你应该得到以下输出:

    图 5.51: 最小-最大缩放器总结

    图 5.51: 最小-最大缩放器总结

  7. 执行最小-最大缩放变换并将数据保存到一个新变量X_min_max中:

    X_min_max = min_max_scaler.transform(X)
    X_min_max
    

    你应该得到以下输出:

    图 5.52: 最小-最大缩放后的数据

    图 5.52: 最小-最大缩放后的数据

  8. 在缩放后的数据上拟合一个 k-means 模型,使用以下超参数:random_state=1, n_clusters=4, init='k-means++', n_init=5,如下所示的代码片段:

    kmeans = KMeans(random_state=1, n_clusters=4, \
                    init='k-means++', n_init=5)
    kmeans.fit(X_min_max)
    
  9. df数据框中为X的每个值分配 k-means 预测到一个新列,名为'cluster8'

    df['cluster8'] = kmeans.predict(X_min_max)
    
  10. 使用altair包将 k-means 结果绘制成散点图:

    scatter_plot = alt.Chart(df).mark_circle()
    scatter_plot.encode(x='Average total business income', \
                        y='Average total business expenses',\
                        color='cluster8:N',\
                        tooltip=['Postcode', 'cluster8', \
                                 'Average total business income',\
                                 'Average total business expenses'])\
                       .interactive()
    

    你应该得到以下输出:

    图 5.53: 使用最小-最大缩放后的数据的 k-means 结果散点图

    图 5.53: 使用最小-最大缩放后的数据的 k-means 结果散点图

  11. 重新训练 k-means 模型,但使用 z-score 标准化后的数据,并保持相同的超参数值,random_state=1, n_clusters=4, init='k-means++', n_init=5

    standard_scaler = StandardScaler()
    X_scaled = standard_scaler.fit_transform(X)
    kmeans = KMeans(random_state=1, n_clusters=4, \
                    init='k-means++', n_init=5)
    kmeans.fit(X_scaled)
    
  12. df数据框中为X_scaled的每个值分配 k-means 预测到一个新列,名为'cluster9'

    df['cluster9'] = kmeans.predict(X_scaled)
    
  13. 使用altair包绘制 k-means 结果的散点图:

    scatter_plot = alt.Chart(df).mark_circle()
    scatter_plot.encode(x='Average total business income', \
                        y='Average total business expenses', \
                        color='cluster9:N', \
                        tooltip=['Postcode', 'cluster9', \
                                 'Average total business income',\
                                 'Average total business expenses'])\
                       .interactive()
    

    你应该得到以下输出:

    图 5.54: 使用 z-score 标准化数据的 k-means 结果散点图

图 5.54: 使用 z-score 标准化数据的 k-means 结果散点图

注意

若要访问此特定部分的源代码,请参考 packt.live/2Q2BSND

本节目前没有在线互动示例,但可以像往常一样在 Google Colab 上运行。

k-means 聚类结果在 min-max 标准化和 z-score 标准化之间非常相似,它们分别是步骤 1013的输出。与练习 5.04中使用不同初始化参数以实现合适结果的对比,我们可以看到,标准化后,第 1 类和第 2 类之间的边界略微下降。结果如此接近的原因在于,这两个变量(平均总营业收入和平均总营业支出)的数值范围几乎相同:介于 0 和 900,000 之间。因此,k-means 并没有给予这两个变量其中之一更多的权重。

你已完成本章的最终练习。你已经学会了如何在拟合 k-means 模型之前使用两种非常流行的方法来预处理数据:min-max 缩放和 z-score 标准化。

活动 5.01:使用 k-means 在银行中执行客户细分分析

你正在为一家国际银行工作。信用部门正在审查其产品,并希望更好地了解当前的客户。你被分配了进行客户细分分析的任务。你将使用 k-means 聚类分析来识别相似客户的群体。

以下步骤将帮助你完成此活动:

  1. 下载数据集并将其加载到 Python 中。

  2. 使用read_csv()方法读取 CSV 文件。

    注意

    该数据集采用.dat文件格式。你仍然可以使用read_csv()加载文件,但需要指定以下参数:header=None, sep= '\s\s+' 和 prefix='X'

  3. 你将使用第四列和第十列(X3X9)。提取这些数据。

  4. 通过实例化StandardScaler对象来执行数据标准化。

  5. 分析并确定最优的聚类数。

  6. 使用你定义的聚类数来拟合 k-means 算法。

  7. 创建聚类的散点图。

    注意

    这是来自 UCI 机器学习库的德国信用数据集。可以从 Packt 库下载,网址为packt.live/31L2c2b。尽管该数据集中的所有列都是整数,但大多数实际上是分类变量。这些列中的数据并不是连续的。只有两个变量是真正的数值型数据。你将使用这些变量进行聚类分析。

你应该得到类似于以下的输出:

![图 5.55:四个聚类的散点图]

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_05_55.jpg)

图 5.55:四个聚类的散点图

注意

该活动的解决方案可以在以下地址找到:packt.live/2GbJloz

总结

现在,你已经准备好在自己的数据集上使用 k-means 算法进行聚类分析。这种类型的分析在行业中非常流行,用于细分客户档案以及检测可疑交易或异常。

我们学习了许多不同的概念,例如质心和平方欧几里得距离。我们介绍了主要的 k-means 超参数:init(初始化方法)、n_init(初始化运行次数)、n_clusters(簇的数量)和random_state(指定的种子)。我们还讨论了选择最佳簇数、正确初始化质心和标准化数据的重要性。你已经学会了如何使用以下 Python 包:pandasaltairsklearnKMeans

在本章中,我们只讨论了 k-means,但它并不是唯一的聚类算法。还有许多其他算法使用不同的方法,例如层次聚类、主成分分析和高斯混合模型等。如果你对这一领域感兴趣,现在你已经掌握了所有基本知识,可以自主探索这些其他算法。

接下来,你将看到我们如何评估这些模型的表现,以及可以使用哪些工具使它们更好。

第六章:6. 如何评估模型性能

概述

本章将介绍模型评估,您将评估或评估在决定将模型投入生产之前训练的每个模型的性能。通过本章学习,您将能够创建评估数据集。您将具备使用平均绝对误差MAE)和均方误差MSE)评估线性回归模型性能的能力。您还将能够使用准确率、精确率、召回率和 F1 得分来评估逻辑回归模型的性能。

引言

在评估模型性能时,您会查看一些衡量指标或数值,这些指标能告诉您模型在特定条件下的表现情况,从而帮助您做出是否将已训练的模型投入实际应用的明智决定。本章中您将遇到的一些衡量指标包括 MAE、精确率、召回率和 R2 得分。

您在第二章:回归中学会了如何训练回归模型,在第三章:二分类中学会了如何训练分类模型。请考虑预测客户是否可能购买定期存款的任务,这是您在第三章:二分类中涉及的内容。您已经学会了如何训练模型来执行这类分类任务。现在,您关心的是这个模型的实用性。您可以先训练一个模型,然后评估该模型的预测正确率。接着,您可能会训练更多的模型,并评估它们是否比您之前训练的模型表现更好。

您已经在练习 3.06中看到了使用train_test_split拆分数据的例子,《银行定期存款购买倾向的逻辑回归模型》。在第七章:机器学习模型的泛化中,您将进一步了解拆分数据的必要性和应用,但目前,您应当注意到,拆分数据为两组是非常重要的:一组用于训练模型,另一组用于验证模型。正是这个验证步骤帮助您决定是否将模型投入生产。

拆分数据

您将在第七章:机器学习模型的泛化中进一步了解如何拆分数据,我们将讨论以下内容:

  • 使用train_test_split进行简单的数据拆分

  • 使用交叉验证进行多次数据拆分

目前,您将学习如何使用sklearn中的一个名为train_test_split的函数来拆分数据。

非常重要的一点是,您不能使用所有数据来训练模型。您必须将一部分数据保留用于验证,这部分数据之前不能用于训练。当您训练一个模型时,它会尝试生成一个适合您数据的方程式。训练时间越长,方程式会变得越复杂,以便尽可能多地通过数据点。

当你对数据进行洗牌并将一部分数据留作验证集时,它能确保模型学会避免过拟合你尝试生成的假设。

练习 6.01:导入和拆分数据

在这个练习中,你将从一个代码库中导入数据,并将其拆分成训练集和评估集以训练一个模型。拆分数据是必须的,这样你以后可以评估模型的效果。这个练习将帮助你熟悉拆分数据的过程;这是你将频繁进行的操作。

注意

你将在本章中使用的汽车数据集可以在我们的 GitHub 仓库中找到:packt.live/30I594E

数据来源于 UCI 机器学习库。

这个数据集是关于汽车的。提供了一个文本文件,包含以下信息:

  • buying – 购买该车辆的成本

  • maint – 车辆的维护成本

  • doors – 车辆的门数

  • persons – 车辆能够运输的乘客数量

  • lug_boot – 车辆的行李厢容量

  • safety – 车辆的安全评级

  • car – 这是模型尝试预测的类别

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本。

  2. 导入所需的库:

    import pandas as pd
    from sklearn.model_selection import train_test_split
    

    你在第一行导入了一个叫做pandas的库。这个库对于将文件读取到一个叫做DataFrame的数据结构中非常有用,你在前几章中已经使用过这种结构。这个结构就像一个电子表格或表格,有行和列,我们可以对其进行操作。由于你可能需要多次引用该库,我们给它创建了一个别名pd

    在第二行,你从名为model_selection的模块中导入了一个名为train_test_split的函数,该模块位于sklearn中。你将使用这个函数来拆分通过pandas读取的数据。

  3. 创建一个 Python 列表:

    # data doesn't have headers, so let's create headers
    _headers = ['buying', 'maint', 'doors', 'persons', \
                'lug_boot', 'safety', 'car']
    

    你正在读取的数据存储为 CSV 文件。

    浏览器会将文件下载到你的计算机上。你可以使用文本编辑器打开该文件。如果你这么做,你会看到类似于以下内容:

    图 6.1:没有标题的汽车数据集

    图 6.1:没有标题的汽车数据集

    注意

    另外,你也可以在浏览器中输入数据集的 URL 来查看数据集。

    CSV文件通常会在数据的第一行写出每一列的名称。例如,查看这个数据集的 CSV 文件,它是你在第三章 二元分类中使用过的:

    图 6.2:没有标题的 CSV 文件

    图 6.2:没有标题的 CSV 文件

    但在这种情况下,列名缺失。不过,这不是问题。此步骤中的代码创建了一个名为_headers的 Python 列表,包含了每一列的名称。你将在下一步读取数据时提供这个列表。

  4. 读取数据:

    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter06/Dataset/car.data', \
                     names=_headers, index_col=None)
    

    在此步骤中,代码使用名为read_csv的函数读取文件。第一个参数,'https://raw.githubusercontent.com/PacktWorkshops/The-Data-Science-Workshop/master/Chapter06/Dataset/car.data',是必需的,表示文件的位置。在我们的例子中,文件位于互联网上。它也可以选择性地下载,然后我们可以指定本地文件的位置。

    第二个参数(names=_headers)要求函数在读取数据后将行头添加到数据中。第三个参数(index_col=None)要求函数为表格生成一个新的索引,因为数据本身没有包含索引。该函数将生成一个 DataFrame,我们将其赋值给名为df的变量。

  5. 打印出前五条记录:

    df.head()
    

    这一步中的代码用于打印出 DataFrame 的前五行。该操作的输出显示在以下截图中:

    图 6.3:DataFrame 的前五行

    图 6.3:DataFrame 的前五行

  6. 创建训练集和评估集的 DataFrame:

    training, evaluation = train_test_split(df, test_size=0.3, \
                                            random_state=0)
    

    上述代码将把包含你数据的 DataFrame 分割成两个新的 DataFrame。第一个叫training,用于训练模型。第二个叫evaluation,将在下一步中进一步拆分。我们之前提到过,你必须将数据集分为训练集和评估集,前者用于训练模型,后者用于评估模型。

    在这一点上,train_test_split函数接受两个参数。第一个参数是我们想要分割的数据,第二个是我们希望分割的比例。我们所做的是指定评估数据占数据的 30%。

    注意

    第三个参数 random_state 设置为 0,以确保结果的可复现性。

  7. 创建验证集和测试集:

    validation, test = train_test_split(evaluation, test_size=0.5, \
                                        random_state=0)
    

    这段代码与第 6 步中的代码类似。在此步骤中,代码将我们的评估数据分成两部分,因为我们指定了0.5,意味着50%

    注意

    要访问此特定部分的源代码,请参考packt.live/3haKvl3

    你也可以在线运行这个例子,访问packt.live/3g8zI9R

    在前面的章节中,我们已经学习了如何将数据分割为训练集和测试集,但在这里,我们将进一步将数据分为三部分:一部分用于训练模型,一部分在训练过程中用于评估模型,还有一部分在模型投入生产之前用于最终评估模型。

    现在你已经将数据分割成不同的集合,可以开始训练和评估模型了。

回归模型的模型性能评估

当你创建一个回归模型时,你创建的模型会预测一个连续的数值变量,正如你在第二章,回归分析中学到的那样。当你设置评估数据集时,你可以使用它来比较模型的质量。

为了评估你的模型质量,你需要将预测结果与所谓的真实值进行比较,真实值是你试图预测的实际观测值。请看一下图 6.4,其中第一列是实际值(称为实际值),第二列是预测值:

图 6.4:实际值与预测值的比较

图 6.4:实际值与预测值的比较

输出中的第0行将我们评估数据集中的实际值与模型预测值进行比较。我们评估数据集中的实际值是4.891,模型预测的值是4.132270

1行将实际值4.194与模型预测的值4.364320进行比较。

实际上,评估数据集会包含大量记录,因此你不会通过视觉进行此比较。相反,你会使用一些方程式。

你可以通过计算损失来进行此比较。损失是前面截图中实际值与预测值之间的差异。在数据挖掘中,这称为距离度量。计算距离度量的方法有很多种,产生了不同的损失函数。以下是其中两种:

  • 曼哈顿距离

  • 欧几里得距离

回归有多种损失函数,但在本书中,我们将研究两种常用的回归损失函数,它们是:

  • 平均绝对误差(MAE)——这是基于曼哈顿距离的

  • 均方误差(MSE)——这是基于欧几里得距离的

这些函数的目标是通过给出一个数值,衡量你的模型的有效性,这个数值显示了真实值和模型预测值之间的偏差程度。

你的任务是训练新的模型,确保它们的误差持续降低。在此之前,我们先快速介绍一些数据结构。

数据结构——向量与矩阵

在本节中,我们将介绍不同的数据结构,如下所示。

标量

标量变量是简单的数字,例如 23。当你单独使用数字时,它们就是标量。你将它们赋值给变量,如以下表达式所示:

temperature = 23

如果你需要存储 5 天的温度数据,你需要将这些值存储在 5 个不同的变量中,例如以下代码片段所示:

temp_1 = 23
temp_2 = 24
temp_3 = 23
temp_4 = 22
temp_5 = 22

在数据科学中,你将经常处理大量的数据点,例如一年内每小时的温度测量。存储大量值的更高效方式叫做向量。我们将在下一个主题中介绍向量。

向量

向量是标量的集合。考虑一下前面代码片段中的五个温度值。向量是一种数据类型,它允许你将所有这些温度值收集到一个变量中,该变量支持算术运算。向量看起来类似于 Python 列表,可以从 Python 列表创建。考虑以下用于创建 Python 列表的代码片段:

temps_list = [23, 24, 23, 22, 22]

你可以通过首先导入 numpy,然后使用以下代码片段,从列表创建一个向量,方法是使用 numpy.array() 方法:

import numpy as np
temps_ndarray = np.array(temps_list)

你可以继续使用以下代码片段验证数据类型:

print(type(temps_ndarray))

该代码片段将导致编译器输出以下内容:

图 6.5:temps_ndarray 向量数据类型

图 6.5:temps_ndarray 向量数据类型

你可以使用以下代码片段检查向量的内容:

print(temps_ndarray)

这将生成以下输出:

图 6.6:temps_ndarray 向量

图 6.6:temps_ndarray 向量

请注意,输出中包含单个方括号 [],数字之间用空格分隔。这与 Python 列表的输出不同,后者可以通过以下代码片段获得:

print(temps_list)

该代码片段产生以下输出:

图 6.7:temps_list 中元素的列表

图 6.7:temps_list 中元素的列表

注意,输出中包含单个方括号 [],数字之间用逗号分隔。

向量有形状和维度。你可以通过以下代码片段来确定这两者:

print(temps_ndarray.shape)

输出是一个名为 tuple 的 Python 数据结构,看起来像这样:

图 6.8:temps_ndarray 向量的形状

图 6.8:temps_ndarray 向量的形状

请注意,输出由括号 () 组成,包含一个数字和逗号。数字后跟逗号表示该对象只有一个维度。该数字的值是元素的数量。输出的意思是“一个包含五个元素的向量”。这非常重要,因为它与矩阵有很大不同,矩阵我们接下来将讨论。

矩阵

矩阵也是由标量组成的,但与标量不同,矩阵既有行又有列

有时你需要在向量和矩阵之间进行转换。让我们回顾一下 temps_ndarray。你可能记得它有五个元素,因为它的形状是 (5,)。要将其转换为一个具有五行一列的矩阵,你可以使用以下代码片段:

temps_matrix = temps_ndarray.reshape(-1, 1)

该代码片段使用了 .reshape() 方法。第一个参数 -1 指示解释器保持第一维不变。第二个参数 1 指示解释器添加一个新维度,这个新维度就是列。要查看新的形状,可以使用以下代码片段:

print(temps_matrix.shape)

你将得到以下输出:

图 6.9:矩阵的形状

图 6.9:矩阵的形状

注意,元组现在有两个数字,51。第一个数字5表示行数,第二个数字1表示列数。你可以使用以下代码片段打印出矩阵的值:

print(temps_matrix)

代码的输出如下:

图 6.10: 矩阵的元素

图 6.10: 矩阵的元素

注意,输出与向量的输出不同。首先,我们有一个外部的方括号。然后,每一行都有其元素被方括号括起来。每一行只包含一个数字,因为矩阵只有一列。

你可以将矩阵重塑为15列,并使用以下代码片段打印出其值:

print(temps_matrix.reshape(1,5))

输出将如下所示:

图 6.11: 重塑矩阵

图 6.11: 重塑矩阵

注意,现在所有的数字都在一行中,因为该矩阵只有一行五列。外部的方括号表示矩阵,而内部的方括号表示行。

最后,你可以通过以下代码片段删除列来将矩阵转换回向量:

vector = temps_matrix.reshape(-1)

你可以打印出向量的值,以确认你得到以下结果:

图 6.12: 向量的值

图 6.12: 向量的值

注意,现在你只有一组方括号。你仍然有相同数量的元素。

现在让我们来看一个重要的度量指标——R2 分数。

R2 分数

R2 分数(发音为“r 平方”)有时也被称为“得分”,用于衡量模型的决定系数。可以把它看作是模型进行准确可靠预测的能力。这个度量值可以通过模型的score()方法来获取,并且对每个模型都可用。

你的目标是训练多个模型,直到获得更高的 R2 分数。R2 分数的范围在01之间。你的目标是尽量让模型的分数接近1

练习 6.02:计算线性回归模型的 R2 分数

如前所述,R2 分数是评估模型性能的一个重要因素。因此,在本练习中,我们将创建一个线性回归模型,并计算其 R2 分数。

注意

本章中你将使用的鱼类毒性数据集可以在我们的 GitHub 仓库中找到:packt.live/2sNChvv

本数据集来自 UCI 机器学习库:packt.live/2TSyJTB

以下属性对我们的任务有帮助:

  • CIC0: 信息指数

  • SM1_Dz(Z): 基于 2D 矩阵的描述符

  • GATS1i: 2D 自相关

  • NdsCH: Pimephales promelas

  • NdssC: 原子类型计数

  • MLOGP: 分子属性

  • 定量响应,LC50 [-LOG(mol/L)]:该属性表示在 96 小时测试期间,导致 50%的测试鱼死亡的浓度。

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本,编写并执行你的代码。

  2. 接下来,导入以下代码片段中提到的库:

    # import libraries
    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LinearRegression
    

    在这一步,你导入 pandas,用于读取数据。你还导入了 train_test_split(),用于将数据分割为训练集和验证集,并导入 LinearRegression,用于训练模型。

  3. 现在,从数据集读取数据:

    # column headers
    _headers = ['CIC0', 'SM1', 'GATS1i', 'NdsCH', 'Ndssc', \
                'MLOGP', 'response']
    # read in data
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter06/Dataset/'\
                     'qsar_fish_toxicity.csv', \
                     names=_headers, sep=';')
    

    在这一步,你创建一个 Python 列表,用于存储数据中列的名称。这样做是因为包含数据的 CSV 文件没有第一行列头。你接着使用 read_csv() 方法将文件读取并存储在名为 df 的变量中。通过将包含列头的列表传入 names 参数来指定列名。该 CSV 使用分号作为列分隔符,因此需要通过 sep 参数指定。你可以使用 df.head() 来查看数据框的内容:

    图 6.13:数据框的前五行

    图 6.13:数据框的前五行

  4. 将数据分成特征和标签,并划分为训练集和评估集:

    # Let's split our data
    features = df.drop('response', axis=1).values
    labels = df[['response']].values
    X_train, X_eval, y_train, y_eval = train_test_split\
                                       (features, labels, \
                                        test_size=0.2, \
                                        random_state=0)
    X_val, X_test, y_val, y_test = train_test_split(X_eval, y_eval,\
                                                    random_state=0)
    

    在这一步,你创建两个 numpy 数组,分别命名为 featureslabels。然后,你将它们分割两次。第一次分割产生一个 training 集和一个 evaluation 集。第二次分割创建一个 validation 集和一个 test 集。

  5. 创建一个线性回归模型:

    model = LinearRegression()
    

    在这一步,你创建一个 LinearRegression 实例,并将其存储在名为 model 的变量中。你将使用它来训练训练数据集。

  6. 训练模型:

    model.fit(X_train, y_train)
    

    在这一步,你使用 fit() 方法和在步骤 4中创建的训练数据集来训练模型。第一个参数是 features NumPy 数组,第二个参数是 labels

    你应该得到类似以下的输出:

    图 6.14:训练模型

    图 6.14:训练模型

  7. 进行预测,如以下代码片段所示:

    y_pred = model.predict(X_val)
    

    在这一步,你使用验证数据集进行预测,并将其存储在 y_pred 中。

  8. 计算 R2 分数:

    r2 = model.score(X_val, y_val)
    print('R² score: {}'.format(r2))
    

    在这一步,你计算 r2,即模型的 R2 分数。R2 分数是通过模型的 score() 方法计算的。下一行会让解释器输出 R2 分数。

    输出结果类似于以下内容:

    图 6.15:R2 分数

    图 6.15:R2 分数

    注:

    MAE 和 R2 分数可能会根据数据集的分布有所不同。

  9. 你可以看到我们获得的 R2 分数是0.56238,它距离 1 还很远。在下一步,我们将进行比较。

  10. 将预测值与实际的真实值进行比较:

    _ys = pd.DataFrame(dict(actuals=y_val.reshape(-1), \
                            predicted=y_pred.reshape(-1)))
    _ys.head()
    

    在这一步,你粗略地查看了预测值与真实值的对比。在第 8 步中,你会注意到你为模型计算的 R2 得分远非完美(完美得分为 1)。在这一步中,在第一行,你通过使用 pandas 中的DataFrame方法创建了一个 DataFrame。你提供了一个字典作为参数。该字典有两个键:actualspredictedactuals包含了y_vals,即验证数据集中的实际标签。predicted包含了y_pred,即预测值。y_valsy_pred都是二维矩阵,所以你通过使用.reshape(-1)将它们重塑为 1D 向量,这样可以去掉第二个维度。

    第二行会让解释器显示前五条记录。

    输出结果看起来类似于以下内容:

    图 6.16:模型的实际值与预测值

图 6.16:模型的实际值与预测值

注意

要访问此特定部分的源代码,请参考packt.live/31aw6QE

你也可以在packt.live/3aASLbE上在线运行此示例。

在本练习中,我们计算了 R2 得分,它是一个评估指标,可以用于比较不同的模型。

在下一个主题中,我们将讨论平均绝对误差,它是另一种评估指标。

平均绝对误差

平均绝对误差MAE)是回归模型的评估指标,用来衡量预测值与真实值之间的绝对距离。绝对距离是不考虑符号的距离,无论是正数还是负数。例如,如果真实值为 6,预测值为 5,则距离为 1;但是如果预测值为 7,则距离为-1。无论符号如何,绝对距离都是 1。这就是所谓的大小。MAE 通过将所有大小求和并除以观察次数来计算。

练习 6.03:计算模型的 MAE

本练习的目标是使用与练习 6.02相同的数据集,找到一个模型的得分和损失,计算线性回归模型的 R2 得分

在本练习中,我们将计算模型的 MAE。

以下步骤将帮助你完成本练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 导入必要的库:

    # Import libraries
    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LinearRegression
    from sklearn.metrics import mean_absolute_error
    

    在这一步,你从sklearn.metrics导入了一个名为mean_absolute_error的函数。

  3. 导入数据:

    # column headers
    _headers = ['CIC0', 'SM1', 'GATS1i', 'NdsCH', 'Ndssc', \
                'MLOGP', 'response']
    # read in data
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter06/Dataset/'\
                     'qsar_fish_toxicity.csv', \
                     names=_headers, sep=';')
    

    在前面的代码中,你读取了数据。这些数据托管在网上,包含了一些关于鱼类毒性的资料。数据以 CSV 格式存储,但没有包含任何表头。此外,该文件中的列并非通过逗号分隔,而是通过分号分隔。名为_headers的 Python 列表包含了列的名称。

    在下一行,你使用名为read_csv的函数,该函数包含在pandas库中,用于加载数据。第一个参数指定文件位置。第二个参数指定一个包含数据列名的 Python 列表。第三个参数指定用于分隔数据中列的字符。

  4. 将数据拆分为featureslabels,并划分为训练集和评估集:

    # Let's split our data
    features = df.drop('response', axis=1).values
    labels = df[['response']].values
    X_train, X_eval, y_train, y_eval = train_test_split\
                                       (features, labels, \
                                        test_size=0.2, \
                                        random_state=0)
    X_val, X_test, y_val, y_test = train_test_split(X_eval, y_eval,\
                                                    random_state=0)
    

    在这一步,你将数据划分为训练集、验证集和测试集。在第一行,你创建了一个numpy数组,分两步进行。第一步,drop方法传入一个列名,表示要从 DataFrame 中删除的列。第二步,你使用values将 DataFrame 转换为一个二维的numpy数组,这是一种具有行列结构的表格。这个数组被存储在名为features的变量中。

    在第二行,你将列转换为包含要预测标签的numpy数组。你通过从 DataFrame 中提取该列,然后使用values将其转换为numpy数组。

    在第三行,你使用train_test_split以 80:20 的比例拆分featureslabels。训练数据包含在X_train中的特征和y_train中的标签中。评估数据集包含在X_evaly_eval中。

    在第四行,你使用train_test_split将评估数据集拆分为验证集和测试集。由于未指定test_size,因此默认值为25%。验证数据存储在X_valy_val中,而测试数据存储在X_testy_test中。

  5. 创建一个简单的线性回归模型并进行训练:

    # create a simple Linear Regression model
    model = LinearRegression()
    # train the model
    model.fit(X_train, y_train)
    

    在这一步,你使用训练数据来训练模型。在第一行,你创建了一个LinearRegression实例,命名为model。在第二行,你使用X_trainy_train训练模型。X_train包含features,而y_train包含labels

  6. 现在预测我们验证数据集的值:

    # let's use our model to predict on our validation dataset
    y_pred = model.predict(X_val)
    

    此时,你的模型已经准备好使用了。你可以使用predict方法对数据进行预测。在此案例中,你将X_val作为参数传递给该函数。回顾一下,X_val是你的验证数据集。结果会被赋值给一个名为y_pred的变量,并将在下一步中用于计算模型的 MAE。

  7. 计算 MAE:

    # Let's compute our MEAN ABSOLUTE ERROR
    mae = mean_absolute_error(y_val, y_pred)
    print('MAE: {}'.format(mae))
    

    在这一步,你使用mean_absolute_error函数计算模型的 MAE,传入y_valy_predy_val是提供给训练数据的标签,而y_pred是模型的预测结果。前面的代码应该给出约为 0.72434 的 MAE 值:

    图 6.17 MAE 分数

    图 6.17 MAE 分数

    y_valy_pred 都是包含相同数量元素的 numpy 数组。mean_absolute_error 函数将 y_predy_val 中减去,得到一个新数组。结果数组中的元素应用了绝对值函数,确保所有负号被去除。然后计算元素的平均值。

  8. 计算模型的 R2 分数:

    # Let's get the R2 score
    r2 = model.score(X_val, y_val)
    print('R² score: {}'.format(r2))
    

    你应该得到类似于以下的输出:

    图 6.18:模型的 R2 分数

图 6.18:模型的 R2 分数

注意

MAE 和 R2 分数可能会根据数据集的分布而有所不同。

更高的 R2 分数意味着更好的模型,使用一个计算决定系数的方程。

注意

要访问本节的源代码,请参考 packt.live/349mG9P

你也可以在网上运行这个示例,访问 packt.live/3aA1rza

在这个练习中,我们计算了 MAE,这是评估模型时一个重要的参数。

现在你将训练第二个模型,并将其 R2 分数和 MAE 与第一个模型进行比较,以评估哪个模型表现更好。

练习 6.04:计算第二个模型的平均绝对误差(MAE)

在这个练习中,我们将生成新特征,并计算新模型的分数和损失。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 导入所需的库:

    # Import libraries
    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LinearRegression
    from sklearn.metrics import mean_absolute_error
    # pipeline
    from sklearn.pipeline import Pipeline
    # preprocessing
    from sklearn.preprocessing import MinMaxScaler
    from sklearn.preprocessing import StandardScaler
    from sklearn.preprocessing import PolynomialFeatures
    

    在第一步中,你将导入一些库,如 train_test_splitLinearRegressionmean_absolute_error。我们使用管道(pipeline)来快速转换特征,并通过 MinMaxScalerPolynomialFeatures 来生成新特征。MinMaxScaler 通过将所有值调整到 0 到 1 之间的范围来减少数据的方差。它通过减去数据的均值并除以范围(即最大值减去最小值)来实现这一点。PolynomialFeatures 通过将列中的值提升到某个幂次来生成新特征,并在数据框中创建新列以容纳它们。

  3. 从数据集中读取数据:

    # column headers
    _headers = ['CIC0', 'SM1', 'GATS1i', 'NdsCH', 'Ndssc', \
                'MLOGP', 'response']
    # read in data
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter06/Dataset/'\
                     'qsar_fish_toxicity.csv', \
                     names=_headers, sep=';')
    

    在这一步,你将读取数据。虽然数据存储在一个 CSV 文件中,但它没有列出列名的第一行。名为 _headers 的 Python 列表将保存你要提供给 pandas 方法 read_csv 的列名。

    在下一行,你调用 read_csvpandas 方法,并提供要读取的文件的位置和名称,以及列头名称和文件分隔符。文件中的列使用分号分隔。

  4. 将数据拆分为训练集和评估集:

    # Let's split our data
    features = df.drop('response', axis=1).values
    labels = df[['response']].values
    X_train, X_eval, y_train, y_eval = train_test_split\
                                       (features, labels, \
                                        test_size=0.2, \
                                        random_state=0)
    X_val, X_test, y_val, y_test = train_test_split(X_eval, y_eval,\
                                                    random_state=0)
    

    在此步骤中,你首先将名为 df 的 DataFrame 拆分为两个部分。第一个 DataFrame 被命名为 features,包含你将用来进行预测的所有自变量。第二个 DataFrame 被命名为 labels,其中包含你试图预测的值。

    在第三行,你使用 train_test_splitfeatureslabels 拆分为四个数据集。X_trainy_train 包含 80% 的数据,用于训练模型。X_evaly_eval 包含剩余的 20% 数据。

    在第四行,你将 X_evaly_eval 拆分为两个附加数据集。X_valy_val 包含 75% 的数据,因为你没有指定比例或大小。X_testy_test 包含剩余的 25% 数据。

  5. 创建一个管道:

    # create a pipeline and engineer quadratic features
    steps = [('scaler', MinMaxScaler()),\
             ('poly', PolynomialFeatures(2)),\
             ('model', LinearRegression())]
    

    在此步骤中,你首先创建一个名为 steps 的 Python 列表。列表包含三个元组,每个元组代表模型的一次变换。第一个元组代表缩放操作,元组中的第一个项目是步骤的名称,叫做 scaler。这个步骤使用 MinMaxScaler 来转换数据。第二个元组叫做 poly,它通过交叉数据的列来创建额外的特征,直到你指定的度数为止。在这个例子中,你指定了 2,因此它会将这些列交叉到 2 次方。接下来是你的 LinearRegression 模型。

  6. 创建一个管道:

    # create a simple Linear Regression model with a pipeline
    model = Pipeline(steps)
    

    在此步骤中,你创建了一个 Pipeline 的实例,并将其存储在名为 model 的变量中。Pipeline 执行一系列变换,这些变换在你之前定义的步骤中有所说明。这个操作之所以有效,是因为转换器(MinMaxScalerPolynomialFeatures)实现了两个名为 fit()fit_transform() 的方法。你可能还记得在之前的例子中,模型是使用 LinearRegression 实现的 fit() 方法来训练的。

  7. 训练模型:

    # train the model
    model.fit(X_train, y_train)
    

    在接下来的行中,你调用 fit 方法并提供 X_trainy_train 作为参数。因为模型是一个管道,三个操作将依次进行。首先,X_train 会被缩放。接下来,将生成额外的特征。最后,使用 LinearRegression 模型进行训练。此步骤的输出类似于以下内容:

    图 6.19:训练模型

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_06_19.jpg)

    图 6.19:训练模型

  8. 使用验证数据集进行预测:

    # let's use our model to predict on our validation dataset
    y_pred = model.predict(X_val)
    
  9. 计算模型的 MAE:

    # Let's compute our MEAN ABSOLUTE ERROR
    mae = mean_absolute_error(y_val, y_pred)
    print('MAE: {}'.format(mae))
    

    在第一行,你使用 mean_absolute_error 来计算平均绝对误差。你传入 y_valy_pred,结果存储在 mae 变量中。接下来的行中,你打印出 mae

    图 6.20:MAE 分数

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_06_20.jpg)

    图 6.20:MAE 分数

    你在此步骤中计算的损失被称为验证损失,因为你使用了验证数据集。这与使用训练数据集计算的训练损失不同。理解这一点很重要,因为你在阅读其他文档或书籍时,可能会看到这两者的概念。

  10. 计算 R2 分数:

    # Let's get the R2 score
    r2 = model.score(X_val, y_val)
    print('R² score: {}'.format(r2))
    

    在最后两行中,你计算了 R2 分数,并且显示了它,如下面的截图所示:

    图 6.21:R2 分数

图 6.21:R2 分数

在此时,你应该能看到第一个模型和第二个模型之间的 R2 分数和 MAE 的差异(第一个模型的 MAER2 分别为 0.7816290.498688)。

注意

若要访问此特定部分的源代码,请参考 packt.live/2EjCaNn

你也可以在网上运行这个示例,访问 packt.live/2Yb5vRd

在这个练习中,你构建了新的特征,使得你拥有一个假设更高多项式度的模型。这个模型在某个阶段应当比简单的模型表现得更好。构建并训练完新模型后,你计算了 R2 分数和 MAE,这可以用来将这个模型与之前训练的模型进行比较。我们可以得出结论,认为这个模型更好,因为它具有更高的 R2 分数和更低的 MAE。

其他评估指标

虽然我们使用了 mean_absolute_error,但也有其他回归模型评估函数。请记住,这些都属于成本(或损失)函数。包括 max_errormean_squared_errormean_squared_log_errormedian_absolute_error。如果你和数据科学家一起工作,他们通常负责告诉你使用哪种评估指标。如果没有,那么你可以选择任何你喜欢的指标。

MAE 是通过从每个预测中减去真实值,取绝对值,求和所有绝对值,然后除以观测值的数量来计算的。这种距离度量在数据挖掘中被称为曼哈顿距离。

均方误差 (MSE) 是通过计算地面真值和预测值之间差异的平方,求和后,再除以观测值的数量来计算的。MSE 较大时,有时会使用它的平方根,即 均方根误差 (RMSE)。

均方对数误差 (MSLE) 在方程中引入了对数,通过在对地面真值和预测值分别加 1 后再取对数,计算差异的平方,然后对它们求和,并除以观测值的数量。MSLE 的特点是,对于高于地面真值的预测,其代价较低,而对于低于地面真值的预测则代价较高。

最后,median_absolute_error 计算绝对误差的中位数,绝对误差是地面真值和预测值之间的差异。

现在,让我们开始评估分类模型的性能。

评估分类模型的性能

分类模型用于预测一组特征将属于哪个类别。你在第三章二分类》中学会了创建二分类模型,在第四章,《使用随机森林进行多分类》中学会了创建多分类模型。

当你考虑一个分类模型时,你可能会问自己模型的准确性有多高。那么,如何评估准确性呢?

在开始评估之前,你需要先创建一个分类模型。

练习 6.05:创建一个用于计算评估指标的分类模型

在这个练习中,你将创建一个分类模型,之后你将利用该模型进行评估。

你将使用 UCI 机器学习库中的汽车数据集。你将利用该数据集,将汽车分类为可接受或不可接受,依据以下分类特征:

  • buying:汽车的购买价格

  • maint:汽车的维护成本

  • doors:车门的数量

  • persons:车辆的载客量

  • lug_boot:行李厢的大小

  • safety:汽车的安全性估计

    注意

    你可以在这里找到数据集:packt.live/30I594E

以下步骤将帮助你完成任务:

  1. 打开一个新的 Colab 笔记本。

  2. 导入你需要的库:

    # import libraries
    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LogisticRegression
    

    在这一步,你导入了pandas并将其别名设置为pdpandas用于将数据读取到数据框中。你还导入了train_test_split,它用于将数据拆分为训练集和评估集。最后,你还导入了LogisticRegression类。

  3. 导入你的数据:

    # data doesn't have headers, so let's create headers
    _headers = ['buying', 'maint', 'doors', 'persons', \
                'lug_boot', 'safety', 'car']
    # read in cars dataset
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter06/Dataset/car.data', \
                     names=_headers, index_col=None)
    df.head()
    

    在这一步,你创建了一个名为_headers的 Python 列表,用来保存你将要导入的文件中的列名,因为该文件没有标题。然后,你使用pd.read_csv读取文件并指定文件位置以及包含文件标题的列表,最后通过df.head()显示前五行数据。

    你应该得到类似于以下的输出:

    图 6.22:检查数据框

    图 6.22:检查数据框

  4. 按照以下代码片段对分类变量进行编码:

    # encode categorical variables
    _df = pd.get_dummies(df, columns=['buying', 'maint', 'doors',\
                                      'persons', 'lug_boot', \
                                      'safety'])
    _df.head()
    

    在这一步,你使用一种称为独热编码的技术,将分类列转换为数值列。你在第 13 步练习 3.04》的特征工程——从现有特征创建新特征中看到了这个示例。你需要这样做,因为模型的输入必须是数值型的。你可以通过pandas库中的get_dummies方法,从分类变量中得到数值型变量。你将数据框作为输入,指定需要编码的列,然后将结果赋值给一个新的数据框_df,最后使用head()查看结果。

    输出应类似于以下截图:

    图 6.23:编码分类变量

    图 6.23:编码分类变量

    注意

    输出已被截断以便展示。请在packt.live/3aBNlg7查看完整的输出。

  5. 将数据分为训练集和验证集:

    # split data into training and evaluation datasets
    features = _df.drop('car', axis=1).values
    labels = _df['car'].values
    X_train, X_eval, y_train, y_eval = train_test_split\
                                       (features, labels, \
                                        test_size=0.3, \
                                        random_state=0)
    X_val, X_test, y_val, y_test = train_test_split(X_eval, y_eval,\
                                                    test_size=0.5, \
                                                    random_state=0)
    

    在这一步中,首先将特征列和标签提取到两个 NumPy 数组中,分别命名为featureslabels。然后将 70%的数据提取到X_trainy_train中,剩余的 30%数据存入X_evaly_eval中。接着,将X_evaly_eval再分为两个相等的部分,分别赋值给X_valy_val用于验证,以及X_testy_test用于之后的测试。

  6. 训练逻辑回归模型:

    # train a Logistic Regression model
    model = LogisticRegression()
    model.fit(X_train, y_train)
    

    在这一步,你创建一个LogisticRegression实例,并通过将X_trainy_train传递给fit方法来训练模型。

    你应该获得一个类似于以下的输出:

    图 6.24:训练逻辑回归模型

    图 6.24:训练逻辑回归模型

  7. 进行预测:

    # make predictions for the validation set
    y_pred = model.predict(X_val)
    

    在这一步,你对验证数据集X_val进行预测,并将结果存储在y_pred中。通过查看前 10 个预测结果(执行y_pred[0:9]),你应该能获得类似于以下的输出:

    图 6.25:验证集的预测结果

图 6.25:验证集的预测结果

这个模型之所以有效,是因为你能够用它来进行预测。这些预测将每辆车分类为可接受(acc)或不可接受(unacc),根据汽车的特征。在此时,你已经准备好对模型进行各种评估。

注意

要访问此部分的源代码,请参考packt.live/3aBNlg7

你还可以在线运行这个示例,网址是packt.live/34eg7CH

因此,我们已经成功创建了一个分类模型来进行预测,并将在未来的练习中评估该模型的表现。

在本次练习中,我们只训练了一次逻辑回归模型,以免重复执行,因为这个过程步骤较多。在接下来的部分,你将查看混淆矩阵。

混淆矩阵

你在第三章,二分类中遇到过混淆矩阵。你可能记得,混淆矩阵将模型预测的类别数量与验证数据集中这些类别的实际出现情况进行比较。输出结果是一个方阵,行数和列数等于你要预测的类别数。列表示实际值,而行表示预测值。你可以通过使用sklearn.metrics中的confusion_matrix来得到混淆矩阵。

练习 6.06:为分类模型生成混淆矩阵

本练习的目标是为你在练习 6.05中训练的分类模型创建一个混淆矩阵,创建一个用于计算评估指标的分类模型

注意

你应在与练习 6.05, 创建一个用于计算评估指标的分类模型相同的笔记本中继续此练习。如果你希望使用新的笔记本,请确保复制并运行练习 6.05, 创建一个用于计算评估指标的分类模型中的所有代码,然后开始执行本练习的代码。

以下步骤将帮助你完成任务:

  1. 打开一个新的 Colab 笔记本文件。

  2. 导入confusion_matrix

    from sklearn.metrics import confusion_matrix
    

    在这一步,你需要从sklearn.metrics导入confusion_matrix。此函数将帮助你生成混淆矩阵。

  3. 生成混淆矩阵:

    confusion_matrix(y_val, y_pred)
    

    在这一步,你通过提供y_val(实际类别)和y_pred(预测类别)来生成混淆矩阵。

    输出应类似于以下内容:

    图 6.26: 混淆矩阵

图 6.26: 混淆矩阵

注意

要访问此特定部分的源代码,请参考packt.live/3hbreQz

你还可以在packt.live/2EebSMD上在线运行此示例。

我们可以看到数据有四个类别。第一列显示所有应该属于第一类的数据。第一行显示正确放入第一类的预测数量。在本示例中,这个数字是41。第二行显示被预测为第二类,但应该属于第一类的数据数量。在本示例中,这个数字是7。第三行显示被预测为第三类,但应该属于第一类的数据数量。这个数字是7。最后,第四行显示被错误分类为第四类,实际上应该属于第一类的数据数量。在本例中,数字是1

关于混淆矩阵的更多内容

混淆矩阵帮助你分析如果将模型投入生产,你需要做出的决策的影响。假设我们预测某种疾病的存在与否,基于输入数据进行判断。这是一个二分类问题,其中 1 表示疾病存在,0 表示疾病不存在。该模型的混淆矩阵将有两列和两行。

第一列会显示属于真阴性类别的项目。第二行会显示被错误分类为假阳性的项目。

第二列会显示属于假阴性类别的项目。最后,第二行显示正确分类到类别 1 的项目,这些称为真正例

假阳性是指样本被错误预测为感染者,而实际上它们是健康的。其影响是这些病例会被误诊为患有某种疾病。

假阴性是指那些被错误预测为健康的样本,实际上它们患有该病。其影响是这些病例不会得到治疗,尽管它们实际上患有该病。

你需要问的关于这个模型的问题取决于疾病的性质,并需要关于该疾病的领域专业知识。例如,如果该疾病具有传染性,那么未治疗的病例将被释放到普通人群中,可能会感染其他人。与将这些病例隔离并观察其症状相比,这将产生什么影响?

另一方面,如果该疾病不具传染性,问题就变成了治疗那些不患病的人与不治疗患病人群的影响。

应该清楚的是,这些问题并没有明确的答案。该模型需要进行调整,以提供对用户可接受的性能。

精确度

精确度在第三章,二分类中已经介绍;然而,在本章中我们将更详细地讨论它。精确度是将正确分类为阳性的样本数量(称为真正阳性,缩写为TP)除以该预测中的总样本数(即该行中的所有条目总数,包括正确分类(TP)和错误分类(FP)来自混淆矩阵)。假设有 10 个条目被分类为阳性。如果其中 7 个条目实际为阳性,那么 TP 就是 7,FP 就是 3。因此,精确度为 0.7。公式如下所示:

图 6.27:精确度公式

图 6.27:精确度公式

在上述公式中:

  • tp 是真正阳性——正确分类为该类别的预测数量。

  • fp 是假阳性——被错误分类为该类别的预测数量。

  • 用于计算精确度的 sklearn.metrics 函数是 precision_score。不妨试试看。

练习 6.07:计算分类模型的精确度

在本练习中,你将计算在练习 6.05,创建分类模型以计算评估指标中训练的分类模型的精确度。

你应该在与练习 6.05,创建分类模型以计算评估指标相同的笔记本中继续此练习。如果你想使用新的笔记本,请确保复制并运行练习 6.05,创建分类模型以计算评估指标中的完整代码,然后开始执行本练习中的代码。

以下步骤将帮助您完成任务:

  1. 导入所需的库:

    from sklearn.metrics import precision_score
    

    在这一步,您需要从sklearn.metrics导入precision_score

  2. 接下来,计算精度得分,如下所示的代码片段所示:

    precision_score(y_val, y_pred, average='macro')
    

    在这一步,您使用precision_score计算精度得分。

    输出是一个介于 0 和 1 之间的浮动数字。它可能看起来像这样:

    图 6.28:精度得分

图 6.28:精度得分

注意

精度得分可能会因数据而异。

在这个练习中,您会看到分类模型的精度得分是0.924592%可能是一个不错的分数,在一些领域是可以接受的,但在某些领域则较低。因此,还有改进的空间。

注意

要访问此特定部分的源代码,请参考packt.live/3kROW6R

您还可以在线运行此示例,访问packt.live/3aCS8ye

将精度得分视为问这个模型在某个类别上做出正确预测的频率吗?该值需要比我们刚刚达到的得分更接近 1。

召回率

召回率是正确预测的总数除以该类别的预测总数(包括正确和错误的预测)。可以将其视为真正例除以列中的所有条目之和。公式如下:

图 6.29:召回率公式

图 6.29:召回率公式

这个函数是recall_score,它可以从sklearn.metrics获得。

练习 6.08:计算分类模型的召回率

这个练习的目标是计算您在练习 6.05中训练的分类模型的召回率,创建一个计算评估指标的分类模型

注意

您应该在与练习 6.05:创建一个计算评估指标的分类模型中使用的相同笔记本中继续进行这个练习。如果您想使用新的笔记本,请确保复制并运行练习 6.05中的所有代码,然后开始执行本练习的代码。

以下步骤将帮助您完成任务:

  1. 打开一个新的 Colab 笔记本文件。

  2. 现在,导入所需的库:

    from sklearn.metrics import recall_score
    

    在这一步,您从sklearn.metrics导入recall_score。这是您将在第二步中使用的函数。

  3. 计算召回率:

    recall_score(y_val, y_pred, average='macro')
    

    在这一步,您使用recall_score计算召回率。您需要将y_valy_pred作为参数传递给该函数。recall_score的文档解释了您可以提供给average的值。如果您的模型进行二元预测,并且标签是01,您可以将average设置为binary。其他选项包括micromacroweightedsamples。您应该阅读文档以了解它们的作用。

    您应该得到如下所示的输出:

    图 6.30:召回率得分

图 6.30:召回率得分

注意

召回率得分会有所变化,具体取决于数据。

如你所见,我们在练习中计算了召回率得分,结果为0.622。这意味着在所有预测的类别中,62%是正确预测的。单独看这个值可能不太有意义,直到将其与其他模型的召回率得分进行比较。

注意

要访问该部分的源代码,请参阅packt.live/31axPp6

你也可以在packt.live/2YdNv8O在线运行这个示例。

现在我们来计算 F1 分数,这也对评估模型表现有很大帮助,进而帮助你在选择模型时做出更好的决策。

F1 分数

F1 分数是另一个重要的参数,帮助我们评估模型的表现。它通过以下公式同时考虑精度和召回率的贡献:

图 6.31:F1 分数

图 6.31:F1 分数

F1 分数的范围是从 0 到 1,1 是最好的得分。你可以使用来自sklearn.metricsf1_score来计算 F1 分数。

练习 6.09:计算分类模型的 F1 分数

在此练习中,你将计算在练习 6.05中训练的分类模型的 F1 分数,创建分类模型以计算评估指标

注意

你应在与练习 6.05,创建分类模型以计算评估指标相同的笔记本中继续此练习。如果你希望使用新的笔记本,确保复制并运行练习 6.05中的全部代码,创建分类模型以计算评估指标,然后再开始本练习的代码执行。

以下步骤将帮助你完成任务:

  1. 打开一个新的 Colab 笔记本文件。

  2. 导入必要的模块:

    from sklearn.metrics import f1_score
    

    在此步骤中,你将从sklearn.metrics导入f1_score方法。这个分数将帮助你计算评估指标。

  3. 计算 F1 分数:

    f1_score(y_val, y_pred, average='macro')
    

    在此步骤中,你通过传入y_valy_pred来计算 F1 分数。同时,由于这不是二分类问题,你还需指定average='macro'

    你应该得到类似如下的输出:

    图 6.32:F1 分数

图 6.32:F1 分数

注意

要访问该部分的源代码,请参阅packt.live/3iWCqkq

你也可以在packt.live/2Q84epY在线运行这个示例。

在本练习结束时,你将看到我们获得的F1分数为0.6746。仍有很大的改进空间,你可以通过构建新特征和训练新模型来尝试获得更好的 F1 分数。

准确率

准确率是应用于分类模型的评估指标。它通过计算正确预测的标签数量来得出,即预测标签与真实标签完全一致。accuracy_score()函数存在于sklearn.metrics中,用于提供此值。

练习 6.10:计算分类模型的准确率

本练习的目标是计算在练习 6.04中训练的模型的准确率分数,计算第二个模型的平均绝对误差

注意

你应该在与练习 6.05, 创建一个分类模型以计算评估指标相同的笔记本中继续本练习。如果你希望使用一个新的笔记本,请确保复制并运行练习 6.05中的完整代码,然后开始执行本练习的代码。

以下步骤将帮助你完成任务:

  1. 练习 6.05创建一个分类模型以计算评估指标中的代码结束处开始,继续在你的笔记本中操作。

  2. 导入accuracy_score()

    from sklearn.metrics import accuracy_score
    

    在此步骤中,你导入了accuracy_score(),该函数将用于计算模型的准确率。

  3. 计算准确率:

    _accuracy = accuracy_score(y_val, y_pred)
    print(_accuracy)
    

    在此步骤中,通过将y_valy_pred作为参数传递给accuracy_score()来计算模型的准确率。解释器将结果赋值给名为c的变量。print()方法会导致解释器呈现_accuracy的值。

    结果类似于以下内容:

    图 6.33 准确率分数

图 6.33 准确率分数

注意

要访问该特定部分的源代码,请参考packt.live/2Q6K5Ao

你也可以在线运行这个示例,网址是packt.live/2Ye55JT

因此,我们成功地计算出了模型的准确率为0.876。本练习的目标是向你展示如何计算模型的准确率,并将此准确率与未来你将训练的另一个模型的准确率进行比较。

对数损失

对数损失(或 log 损失)是分类模型的损失函数,也称为分类交叉熵。它旨在惩罚不正确的预测。sklearn 文档将其定义为“给定模型预测值下,真实值的负对数似然”。

练习 6.11:计算分类模型的对数损失

本练习的目标是预测在练习 6.05中训练的模型的对数损失,创建一个分类模型以计算评估指标

注意

你应该在与练习 6.05, 创建一个分类模型以计算评估指标相同的笔记本中继续本练习。如果你希望使用一个新的笔记本,请确保复制并运行练习 6.05中的完整代码,然后开始执行本练习的代码。

以下步骤将帮助您完成任务:

  1. 打开您的 Colab 笔记本,并继续从练习 6.05创建分类模型以计算评估指标,的停止处继续。

  2. 导入所需的库:

    from sklearn.metrics import log_loss
    

    在此步骤中,您导入了log_loss()来自sklearn.metrics

  3. 计算对数损失:

    _loss = log_loss(y_val, model.predict_proba(X_val))
    print(_loss)
    

在此步骤中,您计算对数损失并将其存储在一个名为_loss的变量中。您需要观察一个非常重要的事项:之前,您使用了y_val(真实值)和y_pred(预测值)。

在此步骤中,您不使用预测值,而是使用预测的概率。您可以看到在代码中,您指定了model.predict_proba()。您指定了验证数据集,并且它返回预测的概率。

print()函数使解释器呈现日志损失。

这应该如下所示:

图 6.34:对数损失输出

图 6.34:对数损失输出

注意

损失值对于不同的数据可能会有所变化。

要访问此特定部分的源代码,请参考packt.live/2Q5plZR

您还可以在网上运行这个例子:packt.live/34eMIsm

因此,我们已经成功地计算了分类模型的log_loss

接收者操作特征曲线(ROC 曲线)

回想一下我们之前讨论的真正阳性率(True Positive Rate),它也被称为灵敏度。还记得我们尝试做的事情是,通过一个逻辑回归模型找到一个阈值,超过这个阈值时,我们预测输入属于某一类别,低于这个阈值时,我们预测输入不属于该类别。

接收者操作特征(ROC)曲线是一个图表,显示了随着阈值的变化,模型的真正阳性率和假阳性率是如何变化的。

让我们做一个练习来加深对 ROC 曲线的理解。

练习 6.12:计算并绘制二分类问题的 ROC 曲线

本练习的目标是绘制二分类问题的 ROC 曲线。该问题的数据用于预测一位母亲是否需要剖腹产分娩。

注意

本章中使用的数据集可以在我们的 GitHub 仓库中找到:packt.live/36dyEg5

来自 UCI 机器学习库的数据集摘要如下:“此数据集包含 80 名孕妇的剖腹产结果信息,其中包含在医学领域中最重要的分娩问题特征。” 关注的属性包括年龄、分娩次数、分娩时间、血压和心脏状态。

以下步骤将帮助您完成此任务:

  1. 打开一个 Colab 笔记本文件。

  2. 导入所需的库:

    # import libraries
    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import roc_curve
    from sklearn.metrics import auc
    

    在这一步,你导入pandas,用于读取数据。同时,你还导入train_test_split,用于创建训练集和验证集数据集,并导入LogisticRegression,用于创建模型。

  3. 读取数据:

    # data doesn't have headers, so let's create headers
    _headers = ['Age', 'Delivery_Nbr', 'Delivery_Time', \
                'Blood_Pressure', 'Heart_Problem', 'Caesarian']
    # read in cars dataset
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter06/Dataset/caesarian.csv.arff',\
                     names=_headers, index_col=None, skiprows=15)
    df.head()
    # target column is 'Caesarian'
    

    在这一步,你读取了数据集。数据集有一个有趣的格式。底部部分包含 CSV 格式的数据,而上面部分包含一些文件描述符。如果你从packt.live/38qJe4A下载并使用记事本打开文件,你将看到如下内容:

    图 6.35:读取数据集

    图 6.35:读取数据集

    你需要做一些操作来处理这个文件。跳过 15 行,指定列头,并读取文件时不带索引。

    代码展示了如何通过创建一个 Python 列表来存储列头,并使用read_csv()方法读取文件。你传入的参数包括文件的位置、作为 Python 列表的列头、索引列的名称(在这个例子中为 None),以及要跳过的行数。

    head()方法将打印出前五行,应该类似于以下内容:

    图 6.36:数据框的前五行

    图 6.36:数据框的前五行

  4. 切分数据:

    # target column is 'Caesarian'
    features = df.drop(['Caesarian'], axis=1).values
    labels = df[['Caesarian']].values
    # split 80% for training and 20% into an evaluation set
    X_train, X_eval, y_train, y_eval = train_test_split\
                                       (features, labels, \
                                        test_size=0.2, \
                                        random_state=0)
    """
    further split the evaluation set into validation and test sets 
    of 10% each
    """
    X_val, X_test, y_val, y_test = train_test_split(X_eval, y_eval,\
                                                    test_size=0.5, \
                                                    random_state=0)
    

    在这一步,你首先创建两个numpy数组,分别命名为featureslabels。然后,你将这两个数组分割成trainingevaluation数据集。接着,你进一步将evaluation数据集分割成validationtest数据集。

  5. 现在,训练并拟合一个逻辑回归模型:

    model = LogisticRegression()
    model.fit(X_train, y_train)
    

    在这一步,你首先创建一个逻辑回归模型的实例。然后,你开始在训练数据集上训练或拟合该模型。

    输出应类似于以下内容:

    图 6.37:训练逻辑回归模型

    图 6.37:训练逻辑回归模型

  6. 按照以下代码片段预测概率:

    y_proba = model.predict_proba(X_val)
    

    在这一步,模型为验证数据集中的每个条目预测概率,并将结果存储在y_proba中。

  7. 计算真正阳性率、假阳性率和阈值:

    _false_positive, _true_positive, _thresholds = roc_curve\
                                                   (y_val, \
                                                    y_proba[:, 0])
    

    在这一步,你调用roc_curve()并指定真实值和预测概率的第一列。结果是一个包含假阳性率、真正阳性率和阈值的元组。

  8. 探索假阳性率:

    print(_false_positive)
    

    在这一步,你指示解释器打印出假阳性率。输出应类似于以下内容:

    图 6.38:假阳性率

    图 6.38:假阳性率

    注意

    假阳性率可能会有所不同,取决于数据。

  9. 探索真正阳性率:

    print(_true_positive)
    

    在这一步,你指示解释器打印出真正阳性率。输出应该类似于以下内容:

    图 6.39:真正阳性率

    图 6.39:真正阳性率

  10. 探索阈值:

    print(_thresholds)
    

    在这一步骤中,你指示解释器显示阈值。输出应类似于以下内容:

    图 6.40:阈值

    图 6.40:阈值

  11. 现在,绘制 ROC 曲线:

    # Plot the RoC
    import matplotlib.pyplot as plt
    %matplotlib inline
    plt.plot(_false_positive, _true_positive, lw=2, \
             label='Receiver Operating Characteristic')
    plt.xlim(0.0, 1.2)
    plt.ylim(0.0, 1.2)
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('Receiver Operating Characteristic')
    plt.show()
    

    在这一步骤中,你导入matplotlib.pyplot作为绘图库,并将其命名为plt。然后,你通过指定假阳性率和真阳性率来绘制折线图。其余的代码为图表添加标题和水平、垂直坐标轴标签。

    输出应类似于以下内容:

    图 6.41:ROC 曲线

图 6.41:ROC 曲线

注意

要访问此特定部分的源代码,请参考packt.live/322jiLa

你还可以在线运行此示例,网址为packt.live/324ii9s

在本练习中,你学习了如何绘制模型的真阳性率和假阳性率如何随着预测阈值的变化而变化。回想一下,模型所做的事情是输出一个介于 0 和 1 之间的值。这个值被称为 logit。作为数据科学家,你的任务是决定一个阈值,例如 0.5。如果 logit 值高于该阈值,则预测输入属于某个类别(如果是正负预测,则为正类)。如果 logit 值低于该阈值,则预测输入属于负类。

例如,如果阈值是 0.5,那么一个 logit 值为 0.33 将被预测为负类,而 logit 值为 0.80 将被预测为正类。

然而,如果阈值是 0.95,那么一个 logit 值为 0.33 将被预测为负类,而 logit 值为 0.80 仍然会被预测为负类。

现在,回想一下,你希望模型做的是尽可能正确地分类尽可能多的数据点。分类是由你选择的阈值控制的。来自模型的 logit(预测概率)始终是相同的,但分配给预测的类别将取决于阈值。

随着阈值的变化,预测结果会发生变化,真阳性和真阴性的数量也会随之变化。

ROC 曲线显示了随着阈值从 0 到 1 变化,真阳性和真阴性的百分比是如何变化的。

阈值越高,模型在将预测分类为正类之前需要越有信心。回想一下,logit 是输入属于某个类别的概率,是一个从 0 到 1 的置信度得分。

ROC 曲线下面积

接收者操作特征曲线下的面积ROC AUC)是衡量模型将随机选择的正例排在随机选择的负例之前的可能性的指标。换句话说,AUC 值越高,模型在将负类预测为负类、将正类预测为正类方面的能力越强。该值范围从 0 到 1。如果 AUC 为 0.6,表示模型有 60% 的概率根据输入正确区分负类和正类。该指标常用于模型比较。

练习 6.13:计算剖腹产数据集的 ROC AUC

本练习的目标是计算你在练习 6.12中训练的二分类模型的 ROC AUC,即计算并绘制二分类问题的 ROC 曲线

注意

你应继续在与练习 6.12,计算并绘制二分类问题的 ROC 曲线相同的笔记本中进行此练习。如果你希望使用新的笔记本,请确保先复制并运行练习 6.12的完整代码,然后开始执行本练习的代码。

以下步骤将帮助你完成任务:

  1. 打开一个 Colab 笔记本,找到练习 6.12的代码,即计算并绘制二分类问题的 ROC 曲线,并继续编写你的代码。

  2. 预测概率:

    y_proba = model.predict_proba(X_val)
    

    在这一步,你将计算验证数据集中的类别概率,并将结果存储在 y_proba 中。

  3. 计算 ROC AUC:

    from sklearn.metrics import roc_auc_score
    _auc = roc_auc_score(y_val, y_proba[:, 0])
    print(_auc)
    

    在这一步,你将计算 ROC AUC 并将结果存储在 _auc 中。然后继续打印出这个值。结果应与以下类似:

    图 6.42:计算 ROC AUC

图 6.42:计算 ROC AUC

注意

AUC 的值可能因数据而异。

要访问此特定部分的源代码,请参考 packt.live/32jCrIT

你还可以在packt.live/319zoDy 上在线运行此示例。

在本练习中,你学习了如何计算 ROC AUC,这是衡量模型将随机选择的正例排在随机选择的负例之前的可能性的指标。在本示例中,AUC 为 0.1944,模型仍有改进的空间。

当你完成选择模型后,可能会对将其保存以备将来使用感兴趣。接下来的主题将探讨如何保存和加载模型。

保存和加载模型

你最终需要将你训练的一些模型转移到另一台计算机上,以便它们可以投入生产。虽然有多种工具可以实现这一点,但我们将讨论的工具叫做joblib

joblib 支持保存和加载模型,并且它将模型保存为其他机器学习架构(如 ONNX)所支持的格式。

joblib位于sklearn.externals模块中。

练习 6.14:保存和加载模型

在本练习中,你将训练一个简单的模型并用它进行预测。然后,你将保存模型并重新加载。你将使用加载后的模型进行第二次预测,并将第一次模型的预测结果与第二次模型的预测结果进行比较。你将在本练习中使用汽车数据集。

以下步骤将引导你朝着目标迈进:

  1. 打开一个 Colab 笔记本。

  2. 导入所需的库:

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LinearRegression
    
  3. 读取数据:

    _headers = ['CIC0', 'SM1', 'GATS1i', 'NdsCH', 'Ndssc', \
                'MLOGP', 'response']
    # read in data
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter06/Dataset/'\
                     'qsar_fish_toxicity.csv', \
                     names=_headers, sep=';')
    
  4. 检查数据:

    df.head()
    

    输出结果应类似于以下内容:

    图 6.43:检查数据框的前五行

    图 6.43:检查数据框的前五行

  5. 将数据分为特征标签,并分为训练集和验证集:

    features = df.drop('response', axis=1).values
    labels = df[['response']].values
    X_train, X_eval, y_train, y_eval = train_test_split\
                                       (features, labels, \
                                        test_size=0.2, \
                                        random_state=0)
    X_val, X_test, y_val, y_test = train_test_split(X_eval, y_eval,\
                                                    random_state=0)
    
  6. 创建一个线性回归模型:

    model = LinearRegression()
    print(model)
    

    输出结果如下:

    图 6.44:训练线性回归模型

    图 6.44:训练线性回归模型

  7. 将训练数据拟合到模型:

    model.fit(X_train, y_train)
    
  8. 使用模型进行预测:

    y_pred = model.predict(X_val)
    
  9. 导入joblib

    from sklearn.externals import joblib
    
  10. 保存模型:

    joblib.dump(model, './model.joblib')
    

    输出结果应类似于以下内容:

    图 6.45:保存模型

    图 6.45:保存模型

  11. 将其加载为一个新模型:

    m2 = joblib.load('./model.joblib')
    
  12. 使用新模型进行预测:

    m2_preds = m2.predict(X_val)
    
  13. 比较预测结果:

    ys = pd.DataFrame(dict(predicted=y_pred.reshape(-1), \
                           m2=m2_preds.reshape(-1)))
    ys.head()
    

    输出结果应类似于以下内容:

    图 6.46:比较预测结果

图 6.46:比较预测结果

注意

要访问此部分的源代码,请参阅packt.live/322VxTb

你也可以在线运行此示例,访问packt.live/3aAUz4q

你可以看到,保存前的模型预测与保存后重新加载的模型预测完全相同。可以安全地得出结论,保存和加载模型不会影响其质量。

在本练习中,你学习了如何保存和加载模型。你还检查并确认了即使保存并加载模型,模型的预测结果保持不变。

活动 6.01:训练三种不同的模型并使用评估指标选择表现最佳的模型

你在银行担任数据科学家。银行希望实现一个模型,用于预测客户购买定期存款的可能性。银行为你提供了一份数据集,该数据集与第三章二分类中的数据集相同。你之前已经学习了如何为二分类训练一个逻辑回归模型。你还听说过其他非参数建模技术,并希望尝试决策树和随机森林,看看它们与逻辑回归模型的表现如何。

在此活动中,你将训练一个逻辑回归模型并计算分类报告。接着,你将训练一个决策树分类器并计算分类报告。你将通过分类报告比较这些模型。最后,你将训练一个随机森林分类器并生成分类报告。然后,你将通过比较逻辑回归模型和随机森林模型的分类报告来决定将哪个模型投入生产。

完成此任务的步骤是:

  1. 打开一个 Colab 笔记本。

  2. 加载必要的库。

  3. 读取数据。

  4. 探索数据。

  5. 使用pandas.get_dummies()转换分类变量。

  6. 准备Xy变量。

  7. 将数据拆分为训练集和评估集。

  8. 创建一个LogisticRegression实例。

  9. 将训练数据拟合到LogisticRegression模型中。

  10. 使用评估集进行预测。

  11. 使用LogisticRegression模型的预测结果来计算分类报告。

  12. 创建一个DecisionTreeClassifier实例:

    dt_model = DecisionTreeClassifier(max_depth= 6)
    
  13. 将训练数据拟合到DecisionTreeClassifier模型中:

    dt_model.fit(train_X, train_y)
    
  14. 使用DecisionTreeClassifier模型对评估数据集进行预测:

    dt_preds = dt_model.predict(val_X)
    
  15. 使用DecisionTreeClassifier模型的预测结果来计算分类报告:

    dt_report = classification_report(val_y, dt_preds)
    print(dt_report)
    

    注意

    我们将在第七章,机器学习模型的泛化中详细研究决策树。

  16. 比较线性回归模型的分类报告与决策树分类器的分类报告,以确定哪个模型更好。

  17. 创建一个RandomForestClassifier实例。

  18. 将训练数据拟合到RandomForestClassifier模型。

  19. 使用RandomForestClassifier模型对评估数据集进行预测。

  20. 使用随机森林分类器的预测结果来计算分类报告。

  21. 比较线性回归模型的分类报告与随机森林分类器的分类报告,以决定保留哪个模型或进行改进。

  22. 比较所有三个模型的 R²分数。输出应类似于以下内容:图 6.47:比较 R²分数

图 6.47:比较 R²分数

注意

此活动的解决方案可以在以下地址找到:packt.live/2GbJloz

摘要

在本章中,我们观察到一些分类模型的评估指标需要使用二分类模型。我们看到,当处理超过两个类别时,我们需要使用一对多(one-versus-all)方法。一对多方法为每个类别构建一个模型,并尝试预测输入属于某个特定类别的概率。我们看到,一旦完成这一过程,就可以预测输入属于概率最高的那个类别。我们还将评估数据集拆分成了两部分,这是因为X_testy_test仅在模型性能的最终评估时使用。在将模型投入生产之前,你可以利用它们来查看模型在生产环境中的表现。

你已经学会了如何通过观察损失值的变化来评估回归模型的质量。你看到了一些使用 MAE 的例子,并且也了解了 MSE 的存在。在活动中,你还学到了如何评估分类模型的质量。在下一章中,你将学习如何使用交叉验证训练多个模型,并实现正则化技术。

7

第七章:7. 机器学习模型的泛化

概述

本章将教你如何利用已有数据来训练更好的模型,方法是当数据足够时进行数据拆分,或在数据不足时使用交叉验证。通过本章的学习,你将掌握如何将数据划分为训练集、验证集和测试集。你将能够确定数据拆分的比例,并在拆分时考虑某些特征。你还将能够实施交叉验证,利用有限的数据进行测试,并使用正则化减少模型的过拟合。

介绍

在上一章中,你学习了使用各种评估指标(如 R2 分数、MAE 和准确度)来评估模型的表现。这些指标帮助你决定哪些模型保留,哪些模型舍弃。在本章中,你将学习一些训练更好模型的技巧。

泛化处理的是如何让模型在训练过程中未遇到的数据点上也能表现得足够好(即,未在训练中接触过的数据)。我们将讨论两个具体的方面:

  • 如何尽可能多地利用数据来训练模型

  • 如何减少模型中的过拟合

过拟合

当一个模型在训练数据上过拟合时,它生成的假设能够解释每一个例子。换句话说,它能正确预测每个例子的结果。这个场景的问题在于,模型方程变得极其复杂,这样的模型已被观察到无法正确预测新的观察数据。

过拟合发生在模型被过度设计时。出现过拟合的两种方式是:

  • 模型训练使用了过多的特征。

  • 模型训练时间过长。

我们将在接下来的章节中讨论这两个要点。

在过多特征上训练

当模型在过多的特征上进行训练时,假设变得极其复杂。假设你有一列特征,需要生成一个假设。在这种情况下,假设将是一个简单的线性方程,如下所示:

图 7.1:线性假设的方程

图 7.1:线性假设的方程

现在,假设你有两列特征,并且通过相乘来交叉这两列特征。假设变成了如下形式:

图 7.2:曲线假设的方程

图 7.2:曲线假设的方程

虽然第一个方程生成的是一条直线,第二个方程生成的是一条曲线,因为它现在是一个二次方程。但同样的两列特征可能变得更加复杂,取决于你如何设计这些特征。考虑以下方程:

图 7.3:假设的立方方程

图 7.3:假设的立方方程

相同的一组特征现在已经引发了一个三次方程。这个方程具有大量的权重,例如:

  • 简单的线性方程有一个权重和一个偏置。

  • 二次方程有三个权重和一个偏置。

  • 三次方程有五个权重和一个偏置。

由于特征过多导致的过拟合问题的一个解决方案是消除某些特征。这种技术称为套索回归(lasso regression)。

由于特征过多导致的过拟合问题的第二个解决方案是为模型提供更多的数据。这可能并不总是可行的选项,但在可能的情况下,最好这么做。

训练过长

该模型通过初始化权重向量,使得所有值都为零,开始训练。在训练过程中,权重根据梯度更新规则进行更新。这会系统地给每个权重加或减去一个小值。随着训练的进行,权重的大小不断增加。如果模型训练时间过长,这些模型权重会变得过大。

由于权重大导致的过拟合问题的解决方法是将权重的大小减小到尽可能接近零。这种技术称为岭回归(ridge regression)。

欠拟合

考虑另一种情况,其中数据有 10 个特征,但你只使用了 1 个特征。你的模型假设仍然是以下形式:

图 7.4:直线假设的方程

图 7.4:直线假设的方程

然而,这只是直线方程,但你的模型可能忽略了很多信息。模型过于简化,称为欠拟合数据。

欠拟合的解决方法是为模型提供更多特征,或者相反,提供更少的数据进行训练;但更多的特征是更好的方法。

数据

在机器学习的世界中,你所拥有的数据并不会全部用于训练你的模型。相反,你需要将数据分成三个集合,如下所述:

  • 一个训练数据集,用于训练你的模型并衡量训练损失。

  • 一个评估或验证数据集,用来衡量模型的验证损失,以查看验证损失是否随着训练损失的减少而继续减小。

  • 一个测试数据集,用于最终测试模型在投入生产之前的表现。

数据集划分的比例

评估数据集是从你所有的训练数据中分出来的,并且永远不会用于训练。关于评估数据集的比例有不同的观点,但通常范围从最高 30%到最低 10%。这个评估数据集通常会进一步划分为在训练过程中使用的验证数据集和最后用于“健全性检查”的测试数据集。如果你使用 10%作为评估数据集,可能会将 5%用于验证,剩下的 5%用于测试。如果使用 30%,可能会将 20%用于验证,10%用于测试。

总结来说,你可以将数据划分为 70%用于训练,20%用于验证,10%用于测试,或者你也可以将数据划分为 80%用于训练,15%用于验证,5%用于测试。最后,你还可以将数据划分为 90%用于训练,5%用于验证,5%用于测试。

选择使用什么比例取决于你拥有的数据量。例如,如果你有 100,000 条记录,20%的验证集将给你 20,000 条记录。然而,如果你有 100,000,000 条记录,那么 5%的比例将给你 500 万条验证记录,这样的数据量就足够了。

创建数据集划分

在最基础的层面上,划分数据涉及到随机抽样。假设你有 10 个物品在一个碗里。为了得到 30%的物品,你将随机拿出 3 个物品。

同样,因为你正在编写代码,你可以这样做:

  1. 创建一个 Python 列表。

  2. 将 10 个数字放入列表中。

  3. 从 0 到 9 生成 3 个不重复的随机整数。

  4. 选择索引与先前生成的随机数字相对应的项。!图 7.5:数据划分的可视化

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_05.jpg)

图 7.5:数据划分的可视化

这是你只会对特定数据集做一次的操作。你可能会为此编写一个函数。如果这是你需要重复做的事,并且你还需要处理一些高级功能,你可能会想为它编写一个类。

sklearn有一个叫做train_test_split的类,它提供了数据划分的功能。它可以通过sklearn.model_selection.train_test_split来使用。这个函数允许你将一个数据框(DataFrame)划分为两部分。

看一下下面关于导入和划分数据的练习。

练习 7.01:导入并划分数据

本练习的目标是从一个数据仓库导入数据,并将其划分为训练集和评估集。

我们将使用来自 UCI 机器学习库的 Cars 数据集。

注意

你可以在这里找到数据集:packt.live/2RE5rWi

数据集也可以在我们的 GitHub 上找到,链接如下:packt.live/36cvyc4

你将在本章的练习中使用这个数据集。

这个数据集关于拥有特定属性的汽车的成本。网站摘要中提到:“从简单的层次决策模型派生,数据库可能对测试构造性归纳和结构发现方法有用。”以下是该数据集的一些关键属性:

CAR car acceptability
. PRICE overall price
. . buying buying price
. . maint price of the maintenance
. TECH technical characteristics
. . COMFORT comfort
. . . doors number of doors
. . . persons capacity in terms of persons to carry
. . . lug_boot the size of luggage boot
. . safety estimated safety of the car

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 导入必要的库:

    # import libraries
    import pandas as pd
    from sklearn.model_selection import train_test_split
    

    在这一步,你已经导入了pandas并将其别名设置为pd。正如你所知道的,pandas 是读取文件所必需的。你还从sklearn.model_selection导入了train_test_split,用于将数据拆分为两部分。

  3. 在将文件读入你的笔记本之前,使用编辑器打开并检查文件(car.data)。你应该看到类似以下的输出:图 7.6:汽车数据

    图 7.6:汽车数据

    从之前的截图中,你会注意到该文件没有包含第一行作为列头。

  4. 创建一个 Python 列表来保存数据的列名:

    # data doesn't have headers, so let's create headers
    _headers = ['buying', 'maint', 'doors', 'persons', \
                'lug_boot', 'safety', 'car']
    
  5. 现在,按照下面的代码片段导入数据:

    # read in cars dataset
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter07/Dataset/car.data', \
                     names=_headers, index_col=None)
    

    然后,你通过 pd.read_csv 将数据导入到一个名为 df 的变量中。你指定了数据文件的位置以及列头的列表。你还指定了数据没有列索引。

  6. 显示前五条记录:

    df.info()
    

    为了获取有关数据列和记录数的信息,你可以使用 info() 方法。你应该得到类似于以下的输出:

    图 7.7:DataFrame 的前五条记录

    图 7.7:DataFrame 的前五条记录

    RangeIndex 的值显示记录数为 1728

  7. 现在,你需要将 df 中的数据拆分为训练数据集和评估数据集:

    #split the data into 80% for training and 20% for evaluation
    training_df, eval_df = train_test_split(df, train_size=0.8, \
                                            random_state=0)
    

    在这一步,你使用 train_test_split 创建两个新的 DataFrame,分别命名为 training_dfeval_df

    你为 train_size 指定了一个值 0.8,这样 80% 的数据就会被分配给 training_df

    random_state 确保实验的可重现性。如果没有 random_state,每次数据都会用不同的随机数进行拆分,结果会不同。使用 random_state 后,数据每次都会以相同的方式拆分。我们将在下一章深入研究 random_state

  8. 检查 training_df 的信息:

    training_df.info()
    

    在这一步,你使用 .info() 来获取 training_df 的详细信息。这将打印出列名以及记录的数量。

    你应该得到类似于以下的输出:

    图 7.8:有关  的信息

    图 7.8:有关 training_df 的信息

    你应该观察到列名与 df 中的列名一致,但你应该会有 80% 的记录数,相当于从 1728 条记录中抽取了 1382 条。

  9. 检查 eval_df 的信息:

    eval_df.info()
    

    在这一步中,你将打印出关于eval_df的信息。这将给你列名和记录数量。输出应该类似于以下内容:

    图 7.9:关于 eval_df 的信息

图 7.9:关于 eval_df 的信息

注意

要访问此特定部分的源代码,请参考 packt.live/3294avL

你还可以在线运行此示例,网址:packt.live/2E8FHhT

现在你知道如何拆分数据了。每当你拆分数据时,记录将完全相同。你可以多次重复这个操作,并注意eval_df索引中的条目范围。

这意味着你无法重复你的实验。如果你运行相同的代码,每次都会得到不同的结果。而且,如果你与同事共享代码,他们也会得到不同的结果。这是因为编译器使用了随机数。

这些随机数实际上并不是真正的随机数,而是使用了一个叫做伪随机数生成器的东西。生成器有一组预定的随机数,它会使用这些数字,因此,你可以指定一个随机状态,使其使用特定的一组随机数。

随机状态

重现相同结果的关键是随机状态。你只需指定一个数字,每当使用该数字时,就会产生相同的结果。这是因为计算机没有真正的随机数生成器,而是使用伪随机数生成器。这意味着,如果你设置一个随机状态,你就可以生成相同的随机数序列。

请看下面的图示作为示例。列是你的随机状态。如果你选择 0 作为随机状态,将会生成以下数字:41、52、38、56…

然而,如果你选择 1 作为随机状态,将会生成一组不同的数字,依此类推。

图 7.10:使用随机状态生成的数字

图 7.10:使用随机状态生成的数字

在前一个练习中,你将随机状态设置为 0,以便实验可重复。

练习 7.02:拆分数据时设置随机状态

该练习的目标是为你在 练习 7.01 中导入并拆分的数据提供一种可重复的拆分方式。

注意

我们将重构前一个练习的代码。因此,如果你正在使用一个新的 Colab 笔记本,请确保复制前一个练习中的代码。或者,你可以复制 练习 7.01 中使用的笔记本,并按照以下步骤使用修改后的代码。

以下步骤将帮助你完成该练习:

  1. 从前一个 练习 7.01 笔记本继续。

  2. 设置随机状态为1并拆分数据:

    """
    split the data into 80% for training and 20% for evaluation 
    using a random state
    """
    training_df, eval_df = train_test_split(df, train_size=0.8, \
                                            random_state=1)
    

    在这一步中,你将train_test_split函数的random_state值设置为 1。

  3. 现在,查看training_df中的前五条记录:

    #view the head of training_eval
    training_df.head()
    

    在此步骤中,您将打印出training_df中的前五条记录。

    输出应类似于以下内容:

    图 7.11:训练评估集的前五行

    图 7.11:训练评估集的前五行

  4. 查看eval_df中的前五条记录:

    #view the top of eval_df
    eval_df.head()
    

    在此步骤中,您将打印出eval_df中的前五条记录。

    输出应类似于以下内容:

    图 7.12:eval_df 的前五行

图 7.12:eval_df 的前五行

注意

要访问此特定部分的源代码,请参考packt.live/2Q6Jb7e

您还可以在packt.live/2EjFvMp在线运行此示例。

本练习的目标是获得可重复的拆分。如果您运行代码,您将在training_dfeval_df中获得相同的记录。您可以在每个系统上多次运行此代码,并验证两个数据集中的记录是否相同。

每次更改random_state时,您将得到一组不同的训练和验证数据。

但是,如何找到最佳的数据集拆分方式来训练您的模型呢?当您没有大量数据时,推荐的方法是使用所有数据。

但是,如果您使用了所有数据,如何保留验证数据呢?

解决方案是将数据分成多个部分。这种方法称为交叉验证,我们将在下一节中详细讨论。

交叉验证

考虑一个例子,将数据分成五个部分,每部分占 20%。然后,您可以利用四个部分进行训练,剩下的一个部分用于评估。由于您有五个部分,您可以重复五次每次使用一个部分进行验证,剩余的数据用于训练。

图 7.13:交叉验证

图 7.13:交叉验证

交叉验证是一种数据拆分方法,您将数据分成多个部分,然后使用其中一些部分进行训练,其余部分用于验证。接着,您可以使用所有数据的组合来训练多个模型。

这种方法称为 n 折交叉验证或 k 折交叉验证。

注意

有关 k 折交叉验证的更多信息,请参阅packt.live/36eXyfi

KFold

sklearn.model_selection中的KFold类返回一个生成器,该生成器提供一个包含两个索引的元组,一个用于训练,另一个用于测试或验证。生成器函数允许您声明一个像迭代器一样工作的函数,从而可以在循环中使用它。

练习 7.03:创建一个五折交叉验证数据集

本练习的目标是从练习 7.01中导入并拆分的数据中创建一个五折交叉验证数据集。

注意

如果你正在使用一个新的 Colab 笔记本,确保从练习 7.01中复制代码,导入并分割数据。或者,你可以复制练习 7.01中使用的笔记本,并按照以下步骤建议的方式使用代码。

以下步骤将帮助你完成练习:

  1. 练习 7.01的笔记本文件继续。

  2. 导入所有必要的库:

    from sklearn.model_selection import KFold
    

    在这一步,你从sklearn.model_selection导入KFold

  3. 现在创建类的一个实例:

    _kf = KFold(n_splits=5)
    

    在这一步,你创建一个KFold的实例,并将其赋值给一个名为_kf的变量。你为n_splits参数指定值5,这样它就会将数据集分割成五个部分。

  4. 现在按以下代码片段分割数据:

    indices = _kf.split(df)
    

    在这一步,你调用split方法,即在_kf上调用.split()。结果存储在一个名为indices的变量中。

  5. 查明indices的数据类型:

    print(type(indices))
    

    在这一步,你检查调用以分割输出的返回结果。

    输出应该是一个generator,如下所示:

    图 7.14:索引的数据类型

    图 7.14:索引的数据类型

  6. 获取第一组索引:

    #first set
    train_indices, val_indices = next(indices)
    

    在这一步,你使用 Python 的next()函数来操作生成器函数。使用next()是获取生成器返回结果的方式。你要求了五个分割,因此可以在这个生成器上调用next()五次。第六次调用next()将导致 Python 运行时引发异常。

    next()的调用返回一个元组。在这种情况下,它是一个包含索引对的元组。第一个包含你的训练索引,第二个包含你的验证索引。你将它们分别赋值给train_indicesval_indices

  7. 按照以下代码片段创建一个训练数据集:

    train_df = df.drop(val_indices)
    train_df.info()
    

    在这一步,你通过从包含所有数据的df DataFrame 中删除验证索引,创建一个新的 DataFrame,称为train_df。这是一种类似于集合论中操作的减法操作。df集合是trainval的并集。一旦你知道了val是什么,你可以通过从df中减去val来反向推导出train。如果你把df看作一个叫做A的集合,把val看作一个叫做B的集合,把train看作一个叫做C的集合,那么以下公式成立:

    图 7.15:Dataframe A

    info() method on the new DataFrame.
    

    该调用的结果应类似于以下截图:

    图 7.17:新 DataFrame 的信息

    图 7.17:新 DataFrame 的信息

  8. 创建验证数据集:

    val_df = df.drop(train_indices)
    val_df.info()
    

    在这一步,你通过从df DataFrame 中删除训练索引来创建val_df验证数据集。同样,你可以通过调用info()方法查看这个新 DataFrame 的详细信息。

    输出应类似于以下内容:

    图 7.18:验证数据集的信息

图 7.18:验证数据集的信息

注意

要访问此特定部分的源代码,请参考packt.live/3kRRaDf

你也可以在线运行这个示例,网址是packt.live/3kTNPnf

你可以将前面的所有步骤编程为一个循环,这样你就不需要手动调用next()五次。这正是我们将在下一个练习中做的。

练习 7.04:使用循环进行调用来创建五折交叉验证数据集

本练习的目标是从你在练习 7.01中导入的数据显示,使用循环调用生成器函数来创建一个五折交叉验证数据集。

注意

如果你正在使用新的 Colab 笔记本,请确保复制练习 7.01,导入和分割数据中的代码。或者,你可以复制在练习 7.01中使用的笔记本,并按照以下步骤使用建议的代码。该练习的笔记本链接如下:packt.live/3g83AmU

以下步骤将帮助你完成此练习:

  1. 打开一个新的 Colab 笔记本,并重复在练习 7.01中用于导入数据的步骤,导入和分割数据

  2. 定义你希望的分割数量:

    from sklearn.model_selection import KFold
    #define number of splits
    n_splits = 5
    

    在此步骤中,你将分割的数量设置为5,并将其存储在一个名为n_splits的变量中。

  3. 创建一个Kfold的实例:

    #create an instance of KFold
    _kf = KFold(n_splits=n_splits)
    

    在此步骤中,你创建了一个Kfold的实例,并将该实例赋值给一个名为_kf的变量。

  4. 生成分割索引:

    #create splits as _indices
    _indices = _kf.split(df)
    

    在此步骤中,你在之前定义的KFold实例_kf上调用split()方法,并将df作为参数传入,这样分割操作将在名为df的数据框上进行。生成的生成器将被存储为_indices

  5. 创建两个 Python 列表:

    _t, _v = [], []
    

    在此步骤中,你创建了两个 Python 列表。第一个名为_t,包含训练数据框;第二个名为_v,包含验证数据框。

  6. 遍历生成器并创建名为train_idxval_idx_train_df_val_df的数据框:

    #iterate over _indices
    for i in range(n_splits):
        train_idx, val_idx = next(_indices)
        _train_df = df.drop(val_idx)
        _t.append(_train_df)
        _val_df = df.drop(train_idx)
        _v.append(_val_df)
    

    在此步骤中,你使用range创建一个循环来确定迭代次数。你通过将n_splits作为参数传递给range()来指定迭代次数。在每次迭代中,你在_indices生成器上执行next()并将结果存储在train_idxval_idx中。然后,你通过从df中删除验证索引val_idx来创建_train_df。你还通过从df中删除训练索引来创建_val_df

  7. 遍历训练列表:

    for d in _t:
        print(d.info())
    

    在此步骤中,你验证编译器是否已创建数据框。你可以通过遍历列表并使用.info()方法打印每个元素的详细信息来完成此操作。输出类似于以下屏幕截图,因输出过大而不完全显示。列表中的每个元素都是一个包含 1,382 条记录的数据框:

    图 7.19:遍历训练列表

    图 7.19:遍历训练列表

    注意

    上述输出是实际输出的简化版本。

  8. 遍历验证列表:

    for d in _v:
        print(d.info())
    

    在这一步,你遍历验证列表,并使用 .info() 打印出每个元素的详细信息。输出类似于以下截图,因大小问题未完全显示。每个元素是一个包含 346 条条目的 DataFrame:

    图 7.20:遍历验证列表

图 7.20:遍历验证列表

注意

上述输出是实际输出的简化版本。

若要访问此特定部分的源代码,请参考 packt.live/3g83AmU

你也可以在在线运行这个例子:packt.live/3iXwEPR

在本练习中,你已经学会如何使用循环进行 k 折交叉验证,以提取训练和验证数据集。你可以利用这些数据集来训练和评估多个模型。

创建交叉验证数据集的核心思想是,你可以训练并评估多个模型。如果你不必在循环中训练这些模型会怎样?

好消息是,你可以避免在循环中训练多个模型,因为如果你那样做,你将需要数组来跟踪许多指标。

cross_val_score

cross_val_score() 函数在 sklearn.model_selection 中可用。到目前为止,你已经学会如何在循环中创建交叉验证数据集。如果你使用了这种方法,你需要在循环中跟踪所有训练和评估的模型。

cross_val_score 处理以下事项:

  • 创建交叉验证数据集

  • 通过将模型拟合到训练数据来训练模型

  • 在验证数据上评估模型

  • 返回每个训练模型的 R2 分数列表

要使上述操作生效,你需要提供以下输入:

  • 一个估计器实例(例如,LinearRegression

  • 原始数据集

  • 要创建的拆分数(这也是将训练和评估的模型数量)

练习 7.05:获取五折交叉验证的得分

本练习的目标是从你在 练习 7.01导入并拆分数据 中导入的数据创建一个五折交叉验证数据集。然后,你将使用 cross_val_score 获取在这些数据集上训练的模型的得分。

注意

如果你使用的是新的 Colab 笔记本,确保从 练习 7.01导入并拆分数据 中复制代码。或者,你可以复制 练习 7.01 中使用的笔记本,并按照以下步骤中的建议使用修改后的代码。该练习的笔记本链接可以在这里找到:packt.live/2DWTkAY

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本,并重复 步骤 1-6,这些步骤用于导入 练习 7.01 中的数据,导入并拆分数据

  2. 对数据集中的分类变量进行编码:

    # encode categorical variables
    _df = pd.get_dummies(df, columns=['buying', 'maint', 'doors', \
                                      'persons', 'lug_boot', \
                                      'safety'])
    _df.head()
    

    在这一步中,你使用 pd.get_dummies() 将分类变量转换为编码。你将结果存储在一个新的 DataFrame 变量 _df 中。然后,你继续查看前五条记录。

    结果应类似于以下内容:

    图 7.21:编码分类变量

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_21.jpg)

    图 7.21:编码分类变量

  3. 将数据拆分为特征和标签:

    # separate features and labels DataFrames
    features = _df.drop(['car'], axis=1).values
    labels = _df[['car']].values
    

    在这一步中,你通过从 _df 中删除 car 来创建一个 features DataFrame。你还通过选择新的 DataFrame 中仅包含 car 的部分来创建 labels。在这里,特征和标签在“汽车”数据集中是相似的。

  4. 创建 LogisticRegression 类的一个实例,供后续使用:

    from sklearn.linear_model import LogisticRegression
    # create an instance of LogisticRegression
    _lr = LogisticRegression()
    

    在这一步中,你从 sklearn.linear_model 中导入 LogisticRegression。我们使用 LogisticRegression 是因为它可以让我们创建一个分类模型,正如你在 第三章,二分类 中学到的那样。然后你继续创建一个实例,并将其存储为 _lr

  5. 导入 cross_val_score 函数:

    from sklearn.model_selection import cross_val_score
    

    在这一步,你导入 cross_val_score,你将利用它来计算模型的得分。

  6. 计算交叉验证得分:

    _scores = cross_val_score(_lr, features, labels, cv=5)
    

    在这一步,你计算交叉验证得分,并将结果存储在一个 Python 列表中,命名为 _scores。你可以使用 cross_val_score 完成这个操作。该函数需要以下四个参数:所使用的模型(在我们的例子中,它叫做 _lr);数据集的特征;数据集的标签;以及创建交叉验证拆分的数量(在我们的例子中是五)。

  7. 现在,按照以下代码片段显示得分:

    print(_scores)
    

    在这一步,你使用 print() 来显示得分。

    输出结果应类似于以下内容:

    图 7.22:打印交叉验证得分

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_22.jpg)

图 7.22:打印交叉验证得分

注意

你可能会得到略微不同的输出,但最佳得分应该属于第二个拆分。

要访问该特定部分的源代码,请参考 packt.live/2DWTkAY

你也可以在线运行这个例子,地址是 packt.live/34d5aS8

在前面的输出中,你可以看到存储在 variable _scores 中的 Python 列表包含五个结果。每个结果都是 LogisticRegression 模型的 R2 得分。正如练习开始时所提到的,数据将被拆分为五个集合,每个五集合的组合都将用于训练和评估模型,之后计算 R2 得分。

从前面的例子中,你应该能观察到,相同的模型在五个不同的数据集上训练时产生了不同的得分。这表明数据的重要性以及数据如何分割的重要性。

通过完成本练习,我们发现最佳分数是0.832,它属于第二个分割。这是我们在此得出的结论。

你已经看到,交叉验证会生成不同的模型。

但是,如何获取最佳模型来使用呢?有些模型或估计器具有内建的交叉验证功能。我们来解释一下这些模型。

理解实现 CV 的估计器

使用交叉验证的目标是通过你拥有的数据找到表现最好的模型。其过程如下:

  1. 使用类似Kfold()的方式分割数据。

  2. 在不同的分割数之间进行迭代并创建估计器。

  3. 训练并评估每个估计器。

  4. 选择具有最佳指标的估计器。你已经看过多种方法来实现这一点。

交叉验证是一种流行的技术,因此也有许多为交叉验证设计的估计器。例如,LogisticRegressionCV是一个实现了交叉验证的类,嵌套在LogisticRegression中。当你使用LogisticRegressionCV时,它会返回一个LogisticRegression的实例。返回的实例是表现最好的实例。

当你创建LogisticRegressionCV的实例时,你需要指定你希望使用的cv分割数。例如,如果你将cv设置为3LogisticRegressionCV将训练三个LogisticRegression实例,然后评估它们并返回表现最好的实例。

你不需要使用LogisticRegressionCV。你可以继续使用LogisticRegressionKfold和迭代方法。LogisticRegressionCV只是作为一种便利存在。

以类似的方式,LinearRegressionCV作为一种便利的方式实现了使用LinearRegression进行交叉验证。

所以,为了明确一下,你不必使用像LogisticRegressionCV这样的便利方法。此外,它们并不是其主要实现的替代品,比如LogisticRegression。相反,当你需要实施交叉验证时,你可以使用这些便利方法,这样就可以省略前面的四个步骤。

LogisticRegressionCV

LogisticRegressionCV是一个实现交叉验证的类。这个类会训练多个LogisticRegression模型,并返回表现最好的一个。

练习 7.06:使用交叉验证训练逻辑回归模型

本练习的目标是使用交叉验证训练一个逻辑回归模型,并获得最佳的 R2 结果。我们将使用你之前处理过的 Cars 数据集。

以下步骤将帮助你完成本练习:

  1. 打开一个新的 Colab 笔记本。

  2. 导入必要的库:

    # import libraries
    import pandas as pd
    from sklearn.model_selection import train_test_split
    

    在这一步,你导入pandas并将其别名设为pd。你将使用 pandas 来读取你将要处理的文件。

  3. 为数据创建标题:

    # data doesn't have headers, so let's create headers
    _headers = ['buying', 'maint', 'doors', 'persons', \
                'lug_boot', 'safety', 'car']
    

    在这一步,你首先创建一个 Python 列表,用来保存你将要处理的文件的headers列。你将这个列表存储为_headers

  4. 读取数据:

    # read in cars dataset
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter07/Dataset/car.data', \
                     names=_headers, index_col=None)
    

    然后,你继续读取文件并将其存储为df。这就是一个数据框。

  5. 打印前五条记录:

    df.info()
    

    最后,你使用.info()查看数据框的总结信息。

    输出结果类似于以下内容:

    图 7.23:数据框的前五条记录

    图 7.23:数据框的前五条记录

  6. 如下代码片段所示,编码类别变量:

    # encode categorical variables
    _df = pd.get_dummies(df, columns=['buying', 'maint', 'doors', \
                                      'persons', 'lug_boot', \
                                      'safety'])
    _df.head()
    

    在此步骤中,你使用 pandas 的get_dummies()方法将类别变量转换为编码。你将原始数据框作为参数传入,并指定需要编码的列。

    最后,你查看前五行数据。输出结果类似于以下内容:

    图 7.24:编码类别变量

    图 7.24:编码类别变量

  7. 将数据框分割成特征和标签:

    # separate features and labels DataFrames
    features = _df.drop(['car'], axis=1).values
    labels = _df[['car']].values
    

    在此步骤中,你创建了两个 NumPy 数组。第一个数组叫做features,包含自变量。第二个数组叫做labels,包含模型学习预测的值,也叫做targets

  8. 导入带有交叉验证的逻辑回归:

    from sklearn.linear_model import LogisticRegressionCV
    

    在此步骤中,你导入LogisticRegressionCV类。

  9. 如下代码片段所示,实例化LogisticRegressionCV

    model = LogisticRegressionCV(max_iter=2000, multi_class='auto',\
                                 cv=5)
    

    在此步骤中,你创建一个LogisticRegressionCV实例。你指定以下参数:

    max_iter:你将其设置为2000,使得训练器继续训练2000次迭代,以找到更好的权重。

    multi_class:你将其设置为auto,以便模型自动检测数据中是否有多个类别。

    cv:你将其设置为5,即你希望用于训练的交叉验证集数量。

  10. 现在拟合模型:

    model.fit(features, labels.ravel())
    

    在此步骤中,你训练模型。你传入featureslabels。由于labels是二维数组,你可以使用ravel()将其转换为一维数组或向量。

    解释器生成的输出结果类似于以下内容:

    图 7.25:拟合模型

    图 7.25:拟合模型

    在前面的输出中,你会看到模型拟合了训练数据。输出结果显示了用于训练的参数,避免了意外情况的发生。例如,注意到max_iter的值为2000,这就是你设置的值。其他未设置的参数则使用默认值,具体可以查阅文档了解更多信息。

  11. 评估训练的 R2 值:

    print(model.score(features, labels.ravel()))
    

    在此步骤中,我们利用训练数据集计算 R2 得分。虽然我们没有专门留出验证数据集,但需要注意的是,模型仅使用了 80%的训练数据,因此在进行此评估时,仍然有新数据可以使用。

    输出结果类似于以下内容:

    图 7.26:计算 R2 得分

图 7.26:计算 R2 得分

注意

要访问此特定章节的源代码,请参考 packt.live/34eD1du

您还可以在线运行此示例,网址为 packt.live/2Yey40k

在前面的输出中,您会看到最终模型的R2得分为0.95,这是一个不错的得分。

此时,您应该看到一个比以前遇到的更好的R2得分。

如果您正在使用没有内置交叉验证的其他类型的模型怎么办?您可以利用交叉验证来训练模型并找到最佳模型吗?让我们来看看。

使用 GridSearchCV 进行超参数调优

GridSearchCV将接受一个模型和参数,并为每个参数的排列训练一个模型。在训练结束时,它将提供对参数和模型评分的访问权限。这被称为超参数调优,您将在第八章,超参数调优中深入了解这个过程。

通常的做法是利用一个小的训练集来通过超参数调优找到最佳参数,然后使用所有数据训练最终模型。

在下一步练习之前,让我们简要回顾一下决策树,它是一种模型或估算器。

决策树

决策树通过为数据中的特征生成一个分割超平面或阈值来工作。它通过考虑每个特征,并找到该特征的值分布与您试图预测的标签之间的相关性来实现这一点。

考虑以下关于气球的数据。您需要预测的标签是inflated。这个数据集用于预测根据特征气球是否充气。特征包括:

  • color

  • size

  • act

  • age

以下表格显示了特征的分布:

图 7.27:气球特征的表格数据

图 7.27:气球特征的表格数据

现在,考虑以下图表,这些图表根据特征与标签的分布进行可视化:

  • 如果考虑Color特征,值为PURPLEYELLOW,但观察值的数量相同,因此无法根据颜色推断气球是否充气,正如下图所示:图 7.28:颜色特征的条形图

图 7.28:颜色特征的条形图

  • Size特征有两个值:LARGESMALL。这些值分布均匀,因此无法根据颜色推断气球是否充气,正如下图所示:图 7.29:尺寸特征的条形图

图 7.29:尺寸特征的条形图

  • Act特征有两个值:DIPSTRETCH。从图表中可以看出,大部分STRETCH的值对应的是气球被充气。如果你需要做出猜测,可以很容易地说,如果ActSTRETCH,那么气球就是充气的。请看下图:图 7.30: 特征的条形图

图 7.30: Act特征的条形图

  • 最后,Age特征也有两个值:ADULTCHILD。从图表中也可以看出,ADULT值占据了大部分充气气球的比例:图 7.31: 年龄特征的条形图

图 7.31: 年龄特征的条形图

对于决策树有用的两个特征是ActAge。决策树可以从判断Act是否为STRETCH开始。如果是,则预测结果为真。这个树形结构大致如下图所示:

图 7.32: 深度为 1 的决策树

图 7.32: 深度为 1 的决策树

左侧表示条件为假时的情况,右侧表示条件为真时的情况。这个树的深度为 1。F 表示预测为假,T 表示预测为真。

为了获得更好的结果,决策树可以引入第二层。第二层将使用Age特征,并判断其值是否为ADULT。其结构大致如下图所示:

图 7.33: 深度为 2 的决策树

图 7.33: 深度为 2 的决策树

这个树的深度为 2。在第一层,它会判断Act是否为STRETCH,如果是,则预测为真。如果Act不是STRETCH,它会检查Age是否为ADULT。如果是,则预测为真,否则预测为假。

决策树可以有任意多个层级,但在某个点之后会开始过拟合。像数据科学中的所有问题一样,最佳深度取决于数据本身,是一个超参数,这意味着你需要尝试不同的值来找到最佳的深度。

在接下来的练习中,我们将使用网格搜索和交叉验证来为决策树估算器找到最佳参数。

练习 7.07: 使用网格搜索和交叉验证找到模型的最佳参数

本次练习的目标是使用网格搜索找到DecisionTree分类器的最佳参数。我们将使用你之前使用过的汽车数据集。

以下步骤将帮助你完成练习:

  1. 打开一个 Colab 笔记本文件。

  2. 导入pandas

    import pandas as pd
    

    在这一步中,你导入pandas并将其别名为pdPandas用于读取你之后将使用的数据。

  3. 创建headers

    _headers = ['buying', 'maint', 'doors', 'persons', \
                'lug_boot', 'safety', 'car']
    
  4. 读取headers

    # read in cars dataset
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter07/Dataset/car.data', \
                     names=_headers, index_col=None)
    
  5. 检查前五条记录:

    df.info()
    

    输出结果类似于下图:

    图 7.34: 数据框的前五条记录

    图 7.34: 数据框的前五条记录

  6. 对类别变量进行编码:

    _df = pd.get_dummies(df, columns=['buying', 'maint', 'doors',\
                                      'persons', 'lug_boot', \
                                      'safety'])
    _df.head()
    

    在此步骤中,您使用.get_dummies()将分类变量转换为编码。.head()方法指示 Python 解释器输出前五列。

    输出类似于以下内容:

    图 7.35:编码分类变量

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_35.jpg)

    图 7.35:编码分类变量

  7. 分离featureslabels

    features = _df.drop(['car'], axis=1).values
    labels = _df[['car']].values
    

    在此步骤中,您创建了两个numpy数组,featureslabels,第一个包含独立变量或预测变量,第二个包含依赖变量或目标变量。

  8. 导入更多库——numpyDecisionTreeClassifierGridSearchCV

    import numpy as np
    from sklearn.tree import DecisionTreeClassifier
    from sklearn.model_selection import GridSearchCV
    

    在此步骤中,您导入numpy。NumPy 是一个数值计算库,您将其别名为np。您还导入DecisionTreeClassifier,用于创建决策树。最后,您导入GridSearchCV,它将使用交叉验证训练多个模型。

  9. 实例化决策树:

    clf = DecisionTreeClassifier()
    

    在此步骤中,您创建了一个DecisionTreeClassifier的实例,命名为clf。此实例将被网格搜索重复使用。

  10. 创建参数——max_depth

    params = {'max_depth': np.arange(1, 8)}
    

    在此步骤中,您创建了一个参数字典。这个字典有两个部分:

    字典的键是传递给模型的参数。在此案例中,max_depthDecisionTreeClassifier所接受的一个参数。

    该值是一个 Python 列表,网格搜索将对其进行迭代并传递给模型。在此案例中,我们创建了一个从 1 到 7(包含)的数组。

  11. 按照以下代码片段实例化网格搜索:

    clf_cv = GridSearchCV(clf, param_grid=params, cv=5)
    

    在此步骤中,您创建了GridSearchCV的一个实例。第一个参数是要训练的模型。第二个参数是要搜索的参数。第三个参数是要创建的交叉验证拆分的数量。

  12. 现在训练模型:

    clf_cv.fit(features, labels)
    

    在此步骤中,您使用特征和标签训练模型。根据模型类型,这可能需要一些时间。因为我们使用的是决策树,它训练得很快。

    输出类似于以下内容:

    图 7.36:训练模型

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_36.jpg)

    图 7.36:训练模型

    通过阅读输出,您可以学到很多信息,比如创建的交叉验证数据集数量(称为cv,等于5),使用的估算器(DecisionTreeClassifier),以及参数搜索空间(称为param_grid)。

  13. 打印最佳参数:

    print("Tuned Decision Tree Parameters: {}"\
          .format(clf_cv.best_params_))
    

    在此步骤中,您打印出最佳参数是什么。在此案例中,我们要寻找的是最佳的max_depth。输出看起来类似于以下内容:

    图 7.37:打印最佳参数

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_37.jpg)

    图 7.37:打印最佳参数

    在前面的输出中,您会看到最佳表现的模型是max_depth2的模型。

    访问best_params_可以让您使用更大的训练数据集,通过最佳已知参数重新训练另一个模型。

  14. 打印最佳R2值:

    print("Best score is {}".format(clf_cv.best_score_))
    

    在此步骤中,您打印出最佳表现模型的R2得分。

    输出结果类似于以下内容:

    Best score is 0.7777777777777778
    

    在前面的输出中,你可以看到表现最好的模型的R2得分为0.778

  15. 访问最佳模型:

    model = clf_cv.best_estimator_
    model
    

    在这一步,你通过best_estimator_访问最佳模型(或估算器)。这将让你分析模型,或者可选地使用它进行预测并找到其他指标。指示 Python 解释器打印最佳估算器将输出类似于以下内容:

    图 7.38:访问模型

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_39.jpg)

图 7.38:访问模型

在前面的输出中,你可以看到最好的模型是DecisionTreeClassifier,其max_depth2

注意

要访问这一特定部分的源代码,请参考packt.live/2E6TdCD

你也可以在线运行此示例,网址为packt.live/3aCg30V

网格搜索是超参数调优中最早教授的技术之一。然而,随着搜索空间的增大,计算成本迅速增加。随着你增加参数选项,搜索空间也会增大,因为会考虑每一种可能的参数组合。

考虑一下模型(或估算器)需要多个参数的情况。搜索空间将是参数数量的倍数。例如,如果我们要训练一个随机森林分类器,我们需要指定森林中的树木数量以及最大深度。如果我们指定了最大深度为 1、2 和 3,并且森林中有 1,000、2,000 和 3,000 棵树,那么我们就需要训练 9 个不同的估算器。如果我们再增加更多参数(或超参数),我们的搜索空间将呈几何增长。

使用 RandomizedSearchCV 进行超参数调优

网格搜索会遍历整个搜索空间,并为每一种参数组合训练一个模型或估算器。而随机搜索只会遍历部分组合。这是一种更优化的资源使用方式,仍然能提供超参数调优和交叉验证的好处。在第八章,超参数调优中你将深入了解这一点。

请查看以下练习。

练习 7.08:使用随机搜索进行超参数调优

这个练习的目标是使用随机搜索和交叉验证进行超参数调优。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 导入pandas

    import pandas as pd
    

    在这一步中,你导入pandas库。你将在下一步中使用它。

  3. 创建headers

    _headers = ['buying', 'maint', 'doors', 'persons', \
                'lug_boot', 'safety', 'car']
    
  4. 读取数据:

    # read in cars dataset
    df = pd.read_csv('https://raw.githubusercontent.com/'\
                     'PacktWorkshops/The-Data-Science-Workshop/'\
                     'master/Chapter07/Dataset/car.data', \
                     names=_headers, index_col=None)
    
  5. 查看前五行:

    df.info()
    

    你需要提供一个包含列名的 Python 列表,因为数据本身不包含列头。你还需要查看你创建的 DataFrame。

    输出结果类似于以下内容:

    图 7.39:DataFrame 的前五行

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_39.jpg)

    图 7.39:DataFrame 的前五行

  6. 按照以下代码片段对类别变量进行编码:

    _df = pd.get_dummies(df, columns=['buying', 'maint', 'doors',\
                                      'persons', 'lug_boot', \
                                      'safety'])
    _df.head()
    

    在这一步中,您通过使用独热编码找到文本数据的数值表示。该操作结果会生成一个新的 DataFrame。您将看到,生成的数据结构看起来类似于以下内容:

    图 7.40:编码分类变量

    图 7.40:编码分类变量

  7. 将数据分为独立变量和依赖变量,即featureslabels

    features = _df.drop(['car'], axis=1).values
    labels = _df[['car']].values
    

    在这一步中,您将 DataFrame 分为两个numpy数组,分别称为featureslabelsFeatures包含独立变量,而labels包含目标或依赖变量。

  8. 导入额外的库——numpyRandomForestClassifierRandomizedSearchCV

    import numpy as np
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import RandomizedSearchCV
    

    在这一步中,您导入numpy进行数值计算,导入RandomForestClassifier以创建一个估计器集成,并导入RandomizedSearchCV以执行带有交叉验证的随机搜索。

  9. 创建RandomForestClassifier的实例:

    clf = RandomForestClassifier()
    

    在这一步中,您实例化RandomForestClassifier。随机森林分类器是一种投票分类器。它利用多棵决策树,这些树在不同的数据子集上进行训练。各个树的结果通过投票机制贡献到随机森林的输出中。

  10. 指定参数:

    params = {'n_estimators':[500, 1000, 2000], \
              'max_depth': np.arange(1, 8)}
    

    RandomForestClassifier接受许多参数,但我们指定了两个:森林中树的数量,称为n_estimators,以及每棵树的节点深度,称为max_depth

  11. 实例化一个随机搜索:

    clf_cv = RandomizedSearchCV(clf, param_distributions=params, \
                                cv=5)
    

    在这一步中,您在实例化clf类时指定三个参数:要使用的估计器或模型(即随机森林分类器)、param_distributions(参数搜索空间)和cv(创建交叉验证数据集的数量)。

  12. 执行搜索:

    clf_cv.fit(features, labels.ravel())
    

    在这一步中,您通过调用fit()来执行搜索。此操作使用交叉验证数据集和不同超参数的组合训练不同的模型。该操作的输出类似于以下内容:

    图 7.41:搜索操作的输出

    图 7.41:搜索操作的输出

    在前面的输出中,您可以看到随机搜索将使用五折交叉验证(cv=5)进行。要使用的估计器是RandomForestClassifier

  13. 打印最佳参数组合:

    print("Tuned Random Forest Parameters: {}"\
          .format(clf_cv.best_params_))
    

    在这一步中,您打印出最佳的超参数。

    输出结果类似于以下内容:

    图 7.42:打印最佳参数组合

    图 7.42:打印最佳参数组合

    在前面的输出中,您可以看到最佳估计器是一个包含 1,000 棵树的随机森林分类器(n_estimators=1000)和max_depth=5。您可以通过执行print("Best score is {}".format(clf_cv.best_score_))来打印最佳分数。对于这个练习,这个值大约是0.76

  14. 检查最佳模型:

    model = clf_cv.best_estimator_
    model
    

    在这一步中,你需要找到表现最佳的估计器(或模型)并打印出其详细信息。输出结果类似于以下内容:

    图 7.43:检查模型

图 7.43:检查模型

在前面的输出中,你可以看到最佳估计器是RandomForestClassifier,其n_estimators=1000max_depth=5

注意

要访问此特定部分的源代码,请参考packt.live/3aDFijn

你也可以在packt.live/3kWMQ5r上在线运行此示例。

在本练习中,你学会了如何利用交叉验证和随机搜索来找到最佳模型,这个过程结合了超参数。这一过程叫做超参数调优,在其中你可以找到最佳的超参数组合来训练模型,并将其投入生产使用。

使用套索回归进行模型正则化

如本章开头所述,模型可能会对训练数据发生过拟合。造成过拟合的原因之一是特征数量过多且系数较大(也叫权重)。解决此类过拟合问题的关键是减小系数的大小。

你可能记得,权重是在模型训练过程中进行优化的。优化权重的一种方法叫做梯度下降。梯度更新规则利用可微分的损失函数。可微分损失函数的例子包括:

  • 平均绝对误差(MAE)

  • 均方误差(MSE)

对于套索回归,损失函数中引入了惩罚项。该实现的技术细节由类隐藏。这个惩罚项也叫做正则化参数。

请考虑以下练习,在其中你过度工程化一个模型以引入过拟合,然后使用套索回归来获得更好的结果。

练习 7.09:使用套索回归修复模型过拟合

本练习的目标是教你如何识别模型何时开始过拟合,并使用套索回归修复模型中的过拟合问题。

注意

你将使用的数据集来自 UCI 机器学习库中的联合循环电厂数据集。该数据集包含从联合循环电厂收集的 9568 个数据点。特征包括温度、气压、湿度和排气真空。这些数据用于预测电厂的每小时净电能输出。请参阅以下链接:packt.live/2v9ohwK

属性信息表明“特征由每小时平均的环境变量组成:

  • 温度(T)范围为 1.81°C 至 37.11°C,

  • 环境气压(AP)范围为 992.89-1033.30 毫巴,

  • 相对湿度(RH)范围为 25.56%至 100.16%

  • 排气真空(V)范围为 25.36-81.56 cm Hg

  • 每小时净电能输出(EP)为 420.26-495.76 MW

平均值来自位于工厂各地的传感器,这些传感器每秒记录一次环境变量。这些变量未经过归一化处理。

以下步骤将帮助你完成练习:

  1. 打开一个 Colab 笔记本。

  2. 导入所需的库:

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LinearRegression, Lasso
    from sklearn.metrics import mean_squared_error
    from sklearn.pipeline import Pipeline
    from sklearn.preprocessing import MinMaxScaler, \
    PolynomialFeatures
    
  3. 读取数据:

    _df = pd.read_csv('https://raw.githubusercontent.com/'\
                      'PacktWorkshops/The-Data-Science-Workshop/'\
                      'master/Chapter07/Dataset/ccpp.csv')
    
  4. 检查数据框:

    _df.info()
    

    .info() 方法打印数据框的概述,包括列的名称和记录的数量。输出可能类似于以下内容:

    图 7.44:检查数据框

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_44.jpg)

    图 7.44:检查数据框

    从前面的图中可以看出,数据框有 5 列和 9,568 条记录。你可以看到所有列都包含数字数据,并且列的名称如下:ATVAPRHPE

  5. 将特征提取到名为 X 的列中:

    X = _df.drop(['PE'], axis=1).values
    
  6. 将标签提取到名为 y 的列中:

    y = _df['PE'].values
    
  7. 将数据分为训练集和评估集:

    train_X, eval_X, train_y, eval_y = train_test_split\
                                       (X, y, train_size=0.8, \
                                        random_state=0)
    
  8. 创建一个 LinearRegression 模型的实例:

    lr_model_1 = LinearRegression()
    
  9. 在训练数据上拟合模型:

    lr_model_1.fit(train_X, train_y)
    

    这个步骤的输出应类似于以下内容:

    图 7.45:在训练数据上拟合模型

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_45.jpg)

    图 7.45:在训练数据上拟合模型

  10. 使用模型在评估数据集上进行预测:

    lr_model_1_preds = lr_model_1.predict(eval_X)
    
  11. 打印模型的 R2 分数:

    print('lr_model_1 R2 Score: {}'\
          .format(lr_model_1.score(eval_X, eval_y)))
    

    这个步骤的输出应类似于以下内容:

    图 7.46:打印 R2 分数

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_46.jpg)

    图 7.46:打印 R2 分数

    你会注意到,这个模型的 R2 分数为 0.926。你将用这个分数来与下一个训练的模型进行比较。记住,这也是一个评估指标。

  12. 打印该模型的均方误差(MSE):

    print('lr_model_1 MSE: {}'\
          .format(mean_squared_error(eval_y, lr_model_1_preds)))
    

    这个步骤的输出应类似于以下内容:

    图 7.47:打印 MSE

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_47.jpg)

    图 7.47:打印 MSE

    你会注意到,MSE 为 21.675。这是一个评估指标,你将用它来比较这个模型与后续模型的表现。

    第一个模型是基于四个特征进行训练的。你现在将基于四个立方特征训练一个新模型。

  13. 创建一个元组列表,作为管道:

    steps = [('scaler', MinMaxScaler()),\
             ('poly', PolynomialFeatures(degree=3)),\
             ('lr', LinearRegression())]
    

    在此步骤中,你将创建一个包含三个元组的列表。第一个元组表示使用 MinMaxScaler 进行的缩放操作。第二个元组表示特征工程步骤,使用 PolynomialFeatures。第三个元组表示 LinearRegression 模型。

    元组的第一个元素表示步骤的名称,第二个元素表示执行转换或估算的类。

  14. 创建管道的实例:

    lr_model_2 = Pipeline(steps)
    
  15. 训练管道的实例:

    lr_model_2.fit(train_X, train_y)
    

    该管道实现了 .fit() 方法,这个方法在所有的转换器和估算器实例中都有实现。.fit() 方法会触发对转换器调用 .fit_transform(),并会触发对估算器调用 .fit()。这个步骤的输出应类似于以下内容:

    图 7.48:训练管道的实例

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_07_48.jpg)

    图 7.48:训练管道实例

    从输出中可以看到,管道已经被训练完成。你可以看到,步骤由MinMaxScalerPolynomialFeatures组成,最终步骤由LinearRegression组成。

  16. 打印出模型的R2评分:

    print('lr_model_2 R2 Score: {}'\
          .format(lr_model_2.score(eval_X, eval_y)))
    

    输出类似于以下内容:

    图 7.49:模型的 R2 评分

    图 7.49:模型的 R2 评分

    从前面的输出中可以看到,R2评分为0.944,比第一个模型的R2评分0.932要好。你可以开始观察到,指标表明这个模型比第一个模型要好。

  17. 使用模型对评估数据进行预测:

    lr_model_2_preds = lr_model_2.predict(eval_X)
    
  18. 打印第二个模型的 MSE:

    print('lr_model_2 MSE: {}'\
          .format(mean_squared_error(eval_y, lr_model_2_preds)))
    

    输出类似于以下内容:

    图 7.50:第二个模型的 MSE

    图 7.50:第二个模型的 MSE

    从输出中可以看到,第二个模型的 MSE 是16.27。这低于第一个模型的 MSE,后者为19.73。你可以得出结论,第二个模型比第一个模型要好。

  19. 检查模型系数(也叫权重):

    print(lr_model_2[-1].coef_)
    

    在这一步,你会注意到lr_model_2是一个管道。这个管道中的最后一个对象是模型,所以你通过设置列表元素的索引为-1来使用列表访问它。

    一旦你得到了模型,即管道中的最后一个元素,你可以使用.coef_来获取模型系数。输出类似于以下内容:

    图 7.51:打印模型系数

    图 7.51:打印模型系数

    从前面的输出中你会注意到,大部分值在十位数,有些值在百位数,而有一个值的幅度非常小。

  20. 检查这个模型中的系数数量:

    print(len(lr_model_2[-1].coef_))
    

    这一步的输出类似于以下内容:

    35
    

    从前面的截图中可以看到,第二个模型有35个系数。

  21. 创建一个包含PolynomialFeatures阶数为10steps列表:

    steps = [('scaler', MinMaxScaler()),\
             ('poly', PolynomialFeatures(degree=10)),\
             ('lr', LinearRegression())]
    
  22. 从前面的步骤创建第三个模型:

    lr_model_3 = Pipeline(steps)
    
  23. 在训练数据上拟合第三个模型:

    lr_model_3.fit(train_X, train_y)
    

    这一步的输出类似于以下内容:

    图 7.52:将第三个模型拟合到数据上

    图 7.52:将第三个模型拟合到数据上

    从输出中可以看到,管道使用了PolynomialFeatures,其阶数为10。你这么做是希望获得一个更好的模型。

  24. 打印出这个模型的R2评分:

    print('lr_model_3 R2 Score: {}'\
          .format(lr_model_3.score(eval_X, eval_y)))
    

    这个模型的输出类似于以下内容:

    图 7.53:模型的 R2 评分

    图 7.53:模型的 R2 评分

    从前面的图中可以看到,R2 评分现在是0.56。之前的模型的R2评分是0.944。这个模型的 R2 评分明显低于前一个模型lr_model_2的评分。这发生在模型出现过拟合时。

  25. 使用 lr_model_3 对评估数据进行预测:

    lr_model_3_preds = lr_model_3.predict(eval_X)
    
  26. 打印 lr_model_3 的 MSE:

    print('lr_model_3 MSE: {}'\
          .format(mean_squared_error(eval_y, lr_model_3_preds)))
    

    该步骤的输出可能类似于以下内容:

    图 7.54:模型的 MSE

    图 7.54:模型的 MSE

    从前面的图中可以看出,MSE 也明显变差了。MSE 为 126.25,而前一个模型的 MSE 为 16.27

  27. 打印此模型中的系数数量(也称为权重):

    print(len(lr_model_3[-1].coef_))
    

    输出可能类似于以下内容:

    图 7.55:打印系数数量

    图 7.55:打印系数数量

    你可以看到该模型有 1,001 个系数。

  28. 检查前 35 个系数,以了解单个系数的幅度:

    print(lr_model_3[-1].coef_[:35])
    

    输出可能类似于以下内容:

    图 7.56:检查前 35 个系数

    图 7.56:检查前 35 个系数

    从输出中可以看到,系数的幅度明显大于 lr_model_2 的系数。

    在接下来的步骤中,你将使用相同的特征训练一个 lasso 回归模型,以减少过拟合。

  29. 创建你将稍后创建的管道的步骤列表:

    steps = [('scaler', MinMaxScaler()),\
             ('poly', PolynomialFeatures(degree=10)),\
             ('lr', Lasso(alpha=0.01))]
    

    你创建了将要创建的管道的步骤列表。请注意,此列表中的第三步是 lasso 实例。调用 Lasso() 中的 alpha 参数是正则化参数。你可以尝试不同的 0 到 1 之间的值,看看它如何影响你训练的模型的性能。

  30. 创建一个管道实例:

    lasso_model = Pipeline(steps)
    
  31. 在训练数据上拟合管道:

    lasso_model.fit(train_X, train_y)
    

    此操作的输出可能类似于以下内容:

    图 7.57:在训练数据上拟合管道

    图 7.57:在训练数据上拟合管道

    从输出中可以看到,管道在最后一步训练了一个 lasso 模型。正则化参数为 0.01,模型最多训练了 1,000 次迭代。

  32. 打印 lasso_modelR2 分数:

    print('lasso_model R2 Score: {}'\
          .format(lasso_model.score(eval_X, eval_y)))
    

    该步骤的输出可能类似于以下内容:

    图 7.58:R2 分数

    图 7.58:R2 分数

    可以看到,R2 分数已回升至 0.94,比 lr_model_30.56 分数要好得多。这已经看起来像是一个更好的模型。

  33. 使用 lasso_model 对评估数据进行预测:

    lasso_preds = lasso_model.predict(eval_X)
    
  34. 打印 lasso_model 的 MSE:

    print('lasso_model MSE: {}'\
          .format(mean_squared_error(eval_y, lasso_preds)))
    

    输出可能类似于以下内容:

    图 7.59:lasso 模型的 MSE

    图 7.59:lasso 模型的 MSE

    从输出中可以看到,MSE 为 17.01,远低于 lr_model_3 的 MSE 值 126.25。你可以放心地得出结论,这是一个更好的模型。

  35. 打印 lasso_model 中的系数数量:

    print(len(lasso_model[-1].coef_))
    

    输出可能类似于以下内容:

    1001
    

    你可以看到该模型有 1,001 个系数,这与 lr_model_3 的系数数量相同。

  36. 打印前 35 个系数的值:

    print(lasso_model[-1].coef_[:35])
    

    输出可能类似于以下内容:

    图 7.60:打印 35 个系数的值

图 7.60:打印 35 个系数的值

从前面的输出中可以看到,有些系数被设置为 0。这意味着在输入数据中相应的列被忽略。你还可以看到,剩余的系数幅度小于 100,这表明模型不再过拟合。

注意

若要访问此特定部分的源代码,请参阅 packt.live/319S6en

你还可以在网上运行这个例子,网址是 packt.live/319AAXD

本练习教你如何通过使用 LassoRegression 训练新模型来解决过拟合问题。

在接下来的部分中,你将学习如何使用岭回归解决模型中的过拟合问题。

岭回归

你刚刚学习了套索回归(lasso regression),它引入了惩罚项并试图从数据中消除某些特征。岭回归(ridge regression)采用了另一种方法,它通过引入惩罚项来惩罚大权重。因此,优化过程尝试减少系数的幅度,而不是完全消除它们。

练习 7.10:使用岭回归修复模型过拟合

本练习的目标是教你如何识别模型何时开始过拟合,并使用岭回归修正模型的过拟合问题。

注意

你将使用与 练习 7.09 相同的数据集,使用 Lasso 回归修复模型过拟合

以下步骤将帮助你完成练习:

  1. 打开一个 Colab 笔记本。

  2. 导入所需的库:

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LinearRegression, Ridge
    from sklearn.metrics import mean_squared_error
    from sklearn.pipeline import Pipeline
    from sklearn.preprocessing import MinMaxScaler, \
    PolynomialFeatures
    
  3. 读取数据:

    _df = pd.read_csv('https://raw.githubusercontent.com/'\
                      'PacktWorkshops/The-Data-Science-Workshop/'\
                      'master/Chapter07/Dataset/ccpp.csv')
    
  4. 检查 DataFrame:

    _df.info()
    

    .info() 方法打印出 DataFrame 的摘要,包括列名和记录数。输出可能类似于以下内容:

    图 7.61:检查 DataFrame

    图 7.61:检查 DataFrame

    从前面的图中可以看到,DataFrame 有 5 列和 9,568 条记录。你可以看到所有列都包含数字数据,列名分别为:ATVAPRHPE

  5. 将特征提取到名为 X 的列中:

    X = _df.drop(['PE'], axis=1).values
    
  6. 将标签提取到名为 y 的列中:

    y = _df['PE'].values
    
  7. 将数据分为训练集和评估集:

    train_X, eval_X, train_y, eval_y = train_test_split\
                                       (X, y, train_size=0.8, \
                                        random_state=0)
    
  8. 创建一个 LinearRegression 模型的实例:

    lr_model_1 = LinearRegression()
    
  9. 在训练数据上拟合模型:

    lr_model_1.fit(train_X, train_y)
    

    这一步的输出应该类似于以下内容:

    图 7.62:在数据上拟合模型

    图 7.62:在数据上拟合模型

  10. 使用模型对评估数据集进行预测:

    lr_model_1_preds = lr_model_1.predict(eval_X)
    
  11. 打印出模型的 R2 分数:

    print('lr_model_1 R2 Score: {}'\
          .format(lr_model_1.score(eval_X, eval_y)))
    

    这一步的输出应该类似于以下内容:

    图 7.63:R2 分数

    图 7.63:R2 分数

    你会注意到,这个模型的 R2 分数是 0.933。你将利用这个数据与下一个训练的模型进行比较。请记住,这是一个评估指标。

  12. 输出该模型的 MSE:

    print('lr_model_1 MSE: {}'\
          .format(mean_squared_error(eval_y, lr_model_1_preds)))
    

    此步骤的输出应类似于以下内容:

    图 7.64:模型的均方误差(MSE)

    图 7.64:模型的均方误差(MSE)

    你会注意到,模型的 MSE 为 19.734。这是一个评估指标,你将用它来比较该模型与后续模型的表现。

    第一个模型是通过四个特征进行训练的。现在,你将基于四个立方特征训练一个新模型。

  13. 创建一个元组列表作为管道:

    steps = [('scaler', MinMaxScaler()),\
             ('poly', PolynomialFeatures(degree=3)),\
             ('lr', LinearRegression())]
    

    在此步骤中,你将创建一个包含三个元组的列表。第一个元组表示使用 MinMaxScaler 的缩放操作。第二个元组表示特征工程步骤,使用 PolynomialFeatures。第三个元组表示 LinearRegression 模型。

    元组的第一个元素表示步骤的名称,第二个元素表示执行变换或估算的类。

  14. 创建一个管道实例:

    lr_model_2 = Pipeline(steps)
    
  15. 训练管道实例:

    lr_model_2.fit(train_X, train_y)
    

    该管道实现了一个 .fit() 方法,该方法在所有变换器和估算器实例中也有实现。.fit() 方法使得在变换器上调用 .fit_transform(),并使得在估算器上调用 .fit()。此步骤的输出类似于以下内容:

    图 7.65:训练管道实例

    图 7.65:训练管道实例

    你可以从输出中看到,管道已被训练。你可以看到步骤由 MinMaxScalerPolynomialFeatures 组成,最后一步是 LinearRegression

  16. 输出模型的 R2 分数:

    print('lr_model_2 R2 Score: {}'\
          .format(lr_model_2.score(eval_X, eval_y)))
    

    输出类似于以下内容:

    图 7.66:R2 分数

    图 7.66:R2 分数

    从前面的输出中,你可以看到 R2 分数为 0.944,这比第一个模型的 R2 分数 0.933 更好。你可以开始观察到,指标显示该模型比第一个模型更好。

  17. 使用该模型对评估数据进行预测:

    lr_model_2_preds = lr_model_2.predict(eval_X)
    
  18. 输出第二个模型的 MSE:

    print('lr_model_2 MSE: {}'\
          .format(mean_squared_error(eval_y, lr_model_2_preds)))
    

    输出类似于以下内容:

    图 7.67:模型的均方误差(MSE)

    图 7.67:模型的均方误差(MSE)

    你可以从输出中看到,第二个模型的 MSE 为 16.272。这低于第一个模型的 MSE 19.734。你可以得出结论,第二个模型比第一个模型更好。

  19. 检查模型的系数(也称为权重):

    print(lr_model_2[-1].coef_)
    

    在此步骤中,你会注意到 lr_model_2 是一个管道。该管道中的最终对象是模型,因此你可以使用列表索引方式,通过将索引设置为 -1 来访问它。

    一旦你拥有模型,它就是管道中的最后一个元素,你可以使用 .coef_ 来获取模型的系数。输出类似于以下内容:

    图 7.68:打印模型系数

    图 7.68:打印模型系数

    你会注意到,前面的输出中大部分值在十几的数量级,有些值在几百左右,且有一个值的幅度非常小。

  20. 检查该模型的系数数量:

    print(len(lr_model_2[-1].coef_))
    

    该步骤的输出类似于以下内容:

    图 7.69:检查系数的数量

    图 7.69:检查系数的数量

    从前面的结果可以看到,第二个模型有 35 个系数。

  21. 创建一个包含 10 次方的 PolynomialFeaturessteps 列表:

    steps = [('scaler', MinMaxScaler()),\
             ('poly', PolynomialFeatures(degree=10)),\
             ('lr', LinearRegression())]
    
  22. 根据前面的步骤创建一个第三个模型:

    lr_model_3 = Pipeline(steps)
    
  23. 在训练数据上拟合第三个模型:

    lr_model_3.fit(train_X, train_y)
    

    该步骤的输出类似于以下内容:

    图 7.70:在训练数据上拟合 lr_model_3

    图 7.70:在训练数据上拟合 lr_model_3

    从输出中可以看到,管道使用了 10 次方的 PolynomialFeatures。你这样做是希望能够得到一个更好的模型。

  24. 打印出该模型的 R2 分数:

    print('lr_model_3 R2 Score: {}'\
          .format(lr_model_3.score(eval_X, eval_y)))
    

    该模型的输出类似于以下内容:

    图 7.71:R2 分数

    图 7.71:R2 分数

    从前面的图可以看到,R2 分数现在是 0.568,而前一个模型的 R2 分数为 0.944。这个模型的 R2 分数比前一个模型 lr_model_2 的分数差。这种情况发生在模型发生过拟合时。

  25. 使用 lr_model_3 对评估数据进行预测:

    lr_model_3_preds = lr_model_3.predict(eval_X)
    
  26. 打印出 lr_model_3 的均方误差:

    print('lr_model_3 MSE: {}'\
          .format(mean_squared_error(eval_y, lr_model_3_preds)))
    

    该步骤的输出可能类似于以下内容:

    图 7.72:lr_model_3 的均方误差

    图 7.72:lr_model_3 的均方误差

    从前面的图中可以看到,均方误差(MSE)也变得更差。MSE 为 126.254,而前一个模型的 MSE 为 16.271

  27. 打印出该模型的系数(也叫权重)数量:

    print(len(lr_model_3[-1].coef_))
    

    输出可能类似于以下内容:

    1001
    

    你可以看到,模型有 1,001 个系数。

  28. 检查前 35 个系数,以了解单个系数的幅度:

    print(lr_model_3[-1].coef_[:35])
    

    输出可能类似于以下内容:

    图 7.73:检查 35 个系数

    图 7.73:检查 35 个系数

    从输出中可以看到,该模型的系数的幅度显著大于 lr_model_2 的系数。

    在接下来的步骤中,你将使用相同特征集训练一个岭回归模型,以减少过拟合。

  29. 创建一个步骤列表,用于你稍后创建的管道:

    steps = [('scaler', MinMaxScaler()),\
             ('poly', PolynomialFeatures(degree=10)),\
             ('lr', Ridge(alpha=0.9))]
    

    你为将要创建的管道列出了步骤清单。请注意,列表中的第三步是 Ridge 的实例。调用 Ridge() 时,名为 alpha 的参数是正则化参数。你可以尝试从 0 到 1 的任意值,看看它如何影响你训练的模型的性能。

  30. 创建管道的实例:

    ridge_model = Pipeline(steps)
    
  31. 在训练数据上拟合管道:

    ridge_model.fit(train_X, train_y)
    

    此操作的输出可能类似于以下内容:

    图 7.74:在训练数据上拟合管道

    图 7.74:在训练数据上拟合管道

    从输出中可以看到,管道在最后一步训练了一个岭回归模型。正则化参数为 0

  32. 打印 ridge_model 的 R2 分数:

    print('ridge_model R2 Score: {}'\
          .format(ridge_model.score(eval_X, eval_y)))
    

    此步骤的输出可能类似于以下内容:

    图 7.75:R2 分数

    图 7.75:R2 分数

    你可以看到,R2 分数已回升至 0.945,远远高于 lr_model_30.568。这已经看起来是一个更好的模型。

  33. 使用 ridge_model 对评估数据进行预测:

    ridge_model_preds = ridge_model.predict(eval_X)
    
  34. 打印 ridge_model 的均方误差(MSE):

    print('ridge_model MSE: {}'\
          .format(mean_squared_error(eval_y, ridge_model_preds)))
    

    输出可能类似于以下内容:

    图 7.76: 的均方误差(MSE)

    图 7.76:ridge_model 的均方误差(MSE)

    从输出中可以看到,ridge_model 的均方误差(MSE)为 16.030,低于 lr_model_3126.254。可以放心地得出结论,这是一个更好的模型。

  35. 打印 ridge_model 中的系数数量:

    print(len(ridge_model[-1].coef_))
    

    输出可能类似于以下内容:

    图 7.77:岭回归模型中的系数数量

    图 7.77:岭回归模型中的系数数量

    你可以看到,这个模型有 1001 个系数,和 lr_model_3 的系数数量相同。

  36. 打印前 35 个系数的值:

    print(ridge_model[-1].coef_[:35])
    

    输出可能类似于以下内容:

    图 7.78:前 35 个系数的值

图 7.78:前 35 个系数的值

从前面的输出可以看到,系数值不再具有较大的幅度。很多系数的幅度小于 10,且没有看到有超过 100 的系数。这表明模型不再出现过拟合。

注意

要访问此特定章节的源代码,请参考 packt.live/3248PPx

你还可以在在线运行此示例,访问 packt.live/2E4LWDu

本练习教你如何通过使用 RidgeRegression 来训练新模型,以解决过拟合问题。

活动 7.01:寻找预测超导体临界温度的最佳模型

你作为一名数据科学家在一家电缆制造公司工作。管理层决定开始向全球客户发货低电阻电缆。为了确保将正确的电缆发送到正确的国家,他们希望根据某些观测数据预测不同电缆的临界温度。

在本次活动中,你将训练一个线性回归模型,并计算 R2 分数和 MSE。接着,你将使用三次方的多项式特征工程化新特征。你将比较这个新模型的 R2 分数和 MSE 与第一个模型的结果,以确定是否存在过拟合。然后,你将使用正则化技术训练一个可以泛化到未见过的数据的模型。

注意

你可以在这里找到本次活动所需的数据集:packt.live/2tJFVqu

原始数据集可以在这里找到:packt.live/3ay3aoe

引用:

Hamidieh, Kam, 《一种数据驱动的统计模型用于预测超导体的临界温度》,《计算材料科学》,第 154 卷,2018 年 11 月,第 346-354 页。

完成此任务的步骤如下:

  1. 打开一个 Colab 笔记本。

  2. 加载必要的库。

  3. superconduct文件夹中读取数据。

  4. 准备Xy变量。

  5. 将数据分割为训练集和评估集。

  6. 创建一个基准线性回归模型。

  7. 输出模型的 R2 分数和均方误差(MSE)。

  8. 创建一个管道,用于工程化多项式特征并训练一个线性回归模型。

  9. 输出 R2 分数和 MSE。

  10. 确定该新模型出现了过拟合。

  11. 创建一个管道,用于工程化多项式特征并训练一个岭回归或套索回归模型。

  12. 输出 R2 分数和 MSE。

    输出将如下所示:

    图 7.79:岭回归模型的 R2 分数和 MSE

    图 7.79:岭回归模型的 R2 分数和 MSE

  13. 确定该模型不再出现过拟合。这就是要投入生产的模型。

    岭回归模型的系数如下图所示:

    图 7.80:岭回归模型的系数

图 7.80:岭回归模型的系数

注意

本次活动的解决方案可以在以下地址找到:packt.live/2GbJloz

总结

在本章中,我们研究了在评估模型时保留一些可用数据的重要性。我们还学习了如何通过交叉验证技术利用所有可用数据,从一组正在训练的模型中找到表现最佳的模型。我们还使用了评估指标来判断模型何时开始过拟合,并使用岭回归和套索回归修正了过拟合的模型。

在下一章中,我们将深入讨论超参数调优。你将学习寻找最佳超参数以训练模型的各种技术。

8

第八章:8. 超参数调节

概览

在本章中,每种超参数调节策略将首先分解为其关键步骤,然后再展示任何高级的 scikit-learn 实现。这是为了确保在跳到更自动化的方法之前,你能完全理解每种策略背后的概念。

在本章结束时,你将能够通过系统地评估具有不同超参数的估算器,进一步提升预测性能。你将成功地部署手动搜索、网格搜索和随机搜索策略,找到最优超参数。你将能够对k 最近邻k-NN)、支持向量机SVMs)、岭回归和随机森林分类器进行参数化,以优化模型性能。

引言

在前几章中,我们讨论了几种方法来得到一个表现良好的模型。这些方法包括通过预处理、特征工程和缩放来转换数据,或从大量可用的 scikit-learn 估算器中选择一个合适的估算器(算法)类型。

根据最终选择的估算器,可能有一些设置可以调整,以提高整体预测性能。这些设置被称为超参数,推导出最佳超参数的过程称为调参或优化。正确调节超参数可以带来性能的显著提升,甚至达到两位数的百分比,因此在任何建模任务中都非常值得进行调参。

本章将讨论超参数调节的概念,并介绍一些简单的策略,帮助你为估算器找到最佳超参数。

在前几章中,我们看到了一些使用各种估算器的练习,但我们并没有进行任何超参数调节。阅读完本章后,我们建议你重新审视这些练习,应用所学的技巧,看看能否提升结果。

什么是超参数?

超参数可以被看作是每个估算器的控制旋钮和开关,改变它们会影响估算器如何工作,从而解释数据中的关系。

请查看图 8.1

图 8.1:超参数如何工作

图 8.1:超参数如何工作

如果你从左到右阅读前面的图示,你会看到在调节过程中,我们改变超参数的值,这会导致估算器的变化。进而,这会引起模型性能的变化。我们的目标是找到能带来最佳模型性能的超参数配置。这将是最优的超参数配置。

估算器可以具有不同数量和类型的超参数,这意味着有时你可能会面临需要选择大量可能的超参数配置的情况。

例如,scikit-learn 实现的 SVM 分类器(sklearn.svm.SVC),你将在本章稍后看到,是一个具有多种可能超参数配置的估算器。我们将仅测试其中的一小部分配置,即使用线性核或二次、三次或四次的多项式核。

这些超参数中,有些是连续型的,而有些是离散型的,连续型超参数的存在意味着理论上可能的超参数化组合是无限的。当然,当涉及到生成具有良好预测性能的模型时,一些超参数化组合要比其他的好得多,作为数据科学家,你的工作就是找到这些更优的超参数配置。

在接下来的章节中,我们将更详细地探讨如何设置这些超参数。但首先,需要澄清一些术语。

超参数与统计模型参数的区别

在你阅读数据科学的材料时,特别是在统计学领域,你会遇到“模型参数”、“参数估计”和“(非)参数化模型”等术语。这些术语与模型数学公式中的参数有关。最简单的例子是没有截距项的单变量线性模型,它的形式如下:

图 8.2:单变量线性模型的方程

图 8.2:单变量线性模型的方程

在这里,𝛽 是统计模型参数,如果选择这种形式,数据科学家的任务就是使用数据来估计它的值。这个过程可以通过普通最小二乘法OLS)回归建模实现,也可以通过一种叫做中位数回归的方法实现。

超参数的不同之处在于它们是外部于数学模型的。例如,在这个案例中,超参数是估计 𝛽 的方法(如最小二乘法(OLS)或中位数回归)。在某些情况下,超参数可能会完全改变算法(即,生成一个完全不同的数学模型)。你将在本章中看到这种情况的例子。

在接下来的章节中,你将学习如何设置超参数。

设置超参数

第七章机器学习模型的泛化中,你接触到了用于分类的 k-NN 模型,并且你看到了随着 k(最近邻的数量)的变化,模型性能在预测类别标签时发生了变化。在这里,k 是一个超参数,手动尝试不同的 k 值就是超参数调整的一个简单形式。

每次初始化一个 scikit-learn 估计器时,它将根据你为其参数设置的值来采用超参数化。如果你没有指定任何值,那么估计器将采用默认的超参数化。如果你想查看估计器的超参数设置以及可以调整的超参数,只需打印estimator.get_params()方法的输出。

例如,假设我们初始化一个 k-NN 估计器但未指定任何参数(空括号)。要查看默认的超参数设置,我们可以运行:

from sklearn import neighbors
# initialize with default hyperparameters
knn = neighbors.KNeighborsClassifier()
# examine the defaults
print(knn.get_params())

你应该看到以下输出:

{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 5, 
 'p': 2, 'weights': 'uniform'}

现在,所有超参数的字典已打印到屏幕上,显示它们的默认设置。注意,k,即我们最近邻居的数量,设置为 5

若要了解这些参数的含义,如何更改它们以及它们可能带来的效果,你可以运行以下命令并查看相关估计器的帮助文件。

对于我们的 k-NN 估计器:

?knn

输出将如下所示:

图 8.3:k-NN 估计器的帮助文件

图 8.3:k-NN 估计器的帮助文件

如果你仔细查看帮助文件,你会看到在 String form 标题下列出了估计器的默认超参数化,并且在 Parameters 标题下有对每个超参数含义的解释。

回到我们的示例,如果我们想将超参数从 k = 5 更改为 k = 15,只需重新初始化估计器并将 n_neighbors 参数设置为 15,这将覆盖默认设置:

"""
initialize with k = 15 and all other hyperparameters as default
"""
knn = neighbors.KNeighborsClassifier(n_neighbors=15)
# examine
print(knn.get_params())

你应该看到以下输出:

{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 15, 
 'p': 2, 'weights': 'uniform'}

你可能已经注意到,k 并不是 k-NN 分类器唯一的超参数。设置多个超参数就像指定相关参数一样简单。例如,我们可以将邻居数从 5 增加到 15,并强制算法在训练时考虑邻域中点的距离,而非简单的多数投票。更多信息,请参阅帮助文件中 weights 参数的说明(?knn):

"""
initialize with k = 15, weights = distance and all other 
hyperparameters as default 
"""
knn = neighbors.KNeighborsClassifier(n_neighbors=15, \
                                     weights='distance')
# examine
print(knn.get_params())

输出将如下所示:

{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 15, 
 'p': 2, 'weights': 'distance'}

在输出中,你可以看到 n_neighbors(即 k)现在被设置为 15,而 weights 设置为 distance,而非 uniform

注意

本节代码可以在 packt.live/2tN5CH1 找到。

关于默认值的说明

通常,机器学习库的开发人员已尽力为估计器设置合理的默认超参数。但需要注意的是,对于某些数据集,通过调优可能会获得显著的性能提升。

寻找最佳超参数设置

最佳的超参数设置取决于你最初构建机器学习模型的整体目标。在大多数情况下,这就是找到对未见数据具有最高预测性能的模型,通常通过其正确标注数据点(分类)或预测数字(回归)的能力来衡量。

未见数据的预测可以通过保留测试集或交叉验证来模拟,本章使用的是前者方法。每种情况的性能评估方式不同,例如,回归使用均方误差MSE),分类则使用准确度。我们力求降低 MSE 或提高预测的准确度。

让我们在以下练习中实现手动超参数调优。

练习 8.01:手动调优 k-NN 分类器的超参数

在本练习中,我们将手动调优一个 k-NN 分类器,该分类器在第七章:机器学习模型的泛化中介绍,我们的目标是基于来自受影响乳腺样本的细胞测量值预测恶性或良性乳腺癌的发生。

注意

本练习中使用的数据集可以在我们的 GitHub 仓库找到,网址为packt.live/36dsxIF

这些是数据集的重要属性:

  • ID 号码

  • 诊断(M = 恶性,B = 良性)

  • 3-32)

为每个细胞核计算 10 个实数值特征,计算方法如下:

  • 半径(从中心到周长上点的平均距离)

  • 纹理(灰度值的标准差)

  • 周长

  • 面积

  • 平滑度(局部半径长度的变化)

  • 紧凑性(周长² / 面积 - 1.0)

  • 凹度(轮廓凹部分的严重程度)

  • 凹点(轮廓中凹部分的数量)

  • 对称性

  • 分形维度(指的是组织结构的复杂性;“海岸线近似” - 1)

    注意

    数据集的属性详情可以在packt.live/30HzGQ6找到。

以下步骤将帮助你完成本练习:

  1. 在 Google Colab 中创建一个新的笔记本。

  2. 接下来,从 scikit-learn 导入neighborsdatasetsmodel_selection

    from sklearn import neighbors, datasets, model_selection
    
  3. 加载数据。我们将这个对象命名为cancer,并隔离目标y和特征X

    # dataset
    cancer = datasets.load_breast_cancer()
    # target
    y = cancer.target
    # features
    X = cancer.data
    
  4. 初始化一个 k-NN 分类器,使用其默认的超参数设置:

    # no arguments specified
    knn = neighbors.KNeighborsClassifier()
    
  5. 将这个分类器输入到 10 折交叉验证(cv)中,计算每一折的精确度分数。假设最大化精确度(在所有正分类中真正例的比例)是本练习的主要目标:

    # 10 folds, scored on precision
    cv = model_selection.cross_val_score(knn, X, y, cv=10,\
                                         scoring='precision')
    
  6. 打印cv显示每一折计算得到的精确度分数:

    # precision scores
    print(cv)
    

    你将看到以下输出:

    [0.91666667 0.85       0.91666667 0.94736842 0.94594595 
     0.94444444 0.97222222 0.92105263 0.96969697 0.97142857]
    
  7. 计算并打印所有折的平均精确度分数。这将给我们一个模型整体表现的概念,具体如下面的代码片段所示:

    # average over all folds
    print(round(cv.mean(), 2))
    

    你应该会得到以下输出:

    0.94
    

    你应该会看到平均分数接近 94%。能否进一步提高?

  8. 再次运行所有内容,这次将超参数k设置为15。你会看到结果实际上稍微更差(低了 1%):

    # k = 15
    knn = neighbors.KNeighborsClassifier(n_neighbors=15)
    cv = model_selection.cross_val_score(knn, X, y, cv=10, \
                                         scoring='precision')
    print(round(cv.mean(), 2))
    

    输出结果如下:

    0.93
    
  9. 再尝试一次,k = 731。在这种情况下,默认值 5 似乎是最好的选择。为了避免重复,你可以定义并调用一个 Python 函数,如下所示:

    def evaluate_knn(k):
        knn = neighbors.KNeighborsClassifier(n_neighbors=k)
        cv = model_selection.cross_val_score(knn, X, y, cv=10, \
                                             scoring='precision')
        print(round(cv.mean(), 2))
    evaluate_knn(k=7)
    evaluate_knn(k=3)
    evaluate_knn(k=1)
    

    输出结果如下:

    0.93
    0.93
    0.92
    

    没有什么能超过 94%。

  10. 我们来改变第二个超参数。将k = 5,如果我们把 k-NN 的加权系统从uniform改为基于distance,会发生什么呢?重新运行所有代码,这次使用以下超参数设置:

    # k =5, weights evaluated using distance
    knn = neighbors.KNeighborsClassifier(n_neighbors=5, \
                                         weights='distance')
    cv = model_selection.cross_val_score(knn, X, y, cv=10, \
                                         scoring='precision')
    print(round(cv.mean(), 2))
    

    性能有提升吗?

    对于默认的超参数设置,你不应看到进一步的提升,因为输出结果是:

    0.93
    

因此我们得出结论,在这种情况下,默认的超参数设置是最优的。

注意

要访问此部分的源代码,请参阅 packt.live/322lWk4

你也可以在线运行这个示例,网址为 packt.live/3gbOyfU

手动搜索的优缺点

在所有的超参数调优策略中,手动过程给了你最多的控制权。通过这个过程,你可以感受到不同超参数设置下,估计器的表现如何,这意味着你可以根据自己的期望进行调整,而无需不必要地尝试大量的可能性。然而,这个策略只有在你想尝试的可能性较少时才可行。当可能性超过大约五个时,这个策略就变得过于繁琐,无法实际应用。

在接下来的几节中,我们将介绍两种策略,以更好地处理这种情况。

使用网格搜索进行调优

在机器学习的背景下,网格搜索指的是一种策略,即系统地测试从预定义的超参数可能性集合中,每一个超参数设置。你决定用于评估性能的标准,搜索完成后,你可以手动检查结果并选择最佳的超参数设置,或者让计算机为你自动选择。

总体目标是尝试找到一个最佳的超参数设置,从而在预测未见数据时提高性能。

在我们开始介绍 scikit-learn 中的网格搜索实现之前,先用简单的 Python for循环演示该策略。

网格搜索策略的简单演示

在接下来的网格搜索策略演示中,我们将使用我们在练习 8.01中看到的乳腺癌预测数据集,手动调节 k-NN 分类器的超参数,当时我们手动调整了 k-NN 分类器的超参数,以优化癌症预测的精确度。

这一次,我们不再手动拟合具有不同k值的模型,而是通过 Python 字典定义我们希望尝试的k值,即k = 1, 3, 5, 7。这个字典将作为我们进行网格搜索的基础,帮助我们找到最佳超参数设置。

注意

本部分代码可以在packt.live/2U1Y0Li找到。

代码如下:

from sklearn import neighbors, datasets, model_selection
# load data
cancer = datasets.load_breast_cancer()
# target
y = cancer.target
# features
X = cancer.data
# hyperparameter grid
grid = {'k': [1, 3, 5, 7]}

在代码片段中,我们使用了一个字典{}并将k值设置在 Python 字典中。

在代码片段的下一部分,为了进行搜索,我们通过遍历网格来拟合每个k值的模型,并每次通过 10 折交叉验证评估模型。

在每次迭代结束时,我们通过print方法提取、格式化并报告交叉验证后的平均精度得分:

# for every value of k in the grid
for k in grid['k']:
    # initialize the knn estimator
    knn = neighbors.KNeighborsClassifier(n_neighbors=k)
    # conduct a 10-fold cross-validation
    cv = model_selection.cross_val_score(knn, X, y, cv=10, \
                                         scoring='precision')
    # calculate the average precision value over all folds
    cv_mean = round(cv.mean(), 3)
    # report the result
    print('With k = {}, mean precision = {}'.format(k, cv_mean))

输出结果如下:

图 8.4:所有折叠的平均精度

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_08_04.jpg)

图 8.4:所有折叠的平均精度

从输出中我们可以看到,k = 5是找到的最佳超参数化,平均精度大约为 94%。将k增加到7并没有显著改善性能。需要注意的是,我们这里只更改了k这个参数,每次初始化 k-NN 估算器时,其他超参数都保持默认值。

为了清晰地表达这一点,我们可以运行相同的循环,这次仅打印出将要尝试的超参数设置:

# for every value of k in the grid 
for k in grid['k']:
    # initialize the knn estimator
    knn = neighbors.KNeighborsClassifier(n_neighbors=k)
    # print the hyperparameterization
    print(knn.get_params())

输出结果如下:

{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 1, 
 'p': 2, 'weights': 'uniform'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski',
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 3, 
 'p': 2, 'weights': 'uniform'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 5, 
 'p': 2, 'weights': 'uniform'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 7, 
 'p': 2, 'weights': 'uniform'}

从输出中你可以看到,我们唯一改变的参数是k,每次迭代中的其他设置都保持不变。

对于单一超参数的网格搜索,简单的单循环结构是可以的,但如果我们想尝试第二个超参数呢?记住,对于 k-NN,我们还有可以取值为uniformdistance的权重,选择不同的权重会影响 k-NN 如何学习如何分类数据点。

为了继续,我们只需要创建一个字典,包含k的值以及我们希望尝试的权重函数,作为单独的键/值对:

# hyperparameter grid
grid = {'k': [1, 3, 5, 7],\
        'weight_function': ['uniform', 'distance']}
# for every value of k in the grid
for k in grid['k']:
    # and every possible weight_function in the grid 
    for weight_function in grid['weight_function']:
        # initialize the knn estimator
        knn = neighbors.KNeighborsClassifier\
              (n_neighbors=k, \
               weights=weight_function)
        # conduct a 10-fold cross-validation
        cv = model_selection.cross_val_score(knn, X, y, cv=10, \
                                             scoring='precision')
        # calculate the average precision value over all folds
        cv_mean = round(cv.mean(), 3)
        # report the result
        print('With k = {} and weight function = {}, '\
              'mean precision = {}'\
              .format(k, weight_function, cv_mean))

输出结果如下:

图 8.5:不同值的所有折叠的平均精度值

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_08_05.jpg)

图 8.5:不同k值的所有折叠的平均精度值

你可以看到,当k = 5时,权重函数不基于距离,所有其他超参数保持默认值,平均精度最高。如前所述,如果你想查看 k-NN 的完整超参数化集,只需在估算器初始化后,在for循环中添加print(knn.get_params())

# for every value of k in the grid
for k in grid['k']:
    # and every possible weight_function in the grid 
    for weight_function in grid['weight_function']:
        # initialize the knn estimator
        knn = neighbors.KNeighborsClassifier\
              (n_neighbors=k, \
               weights=weight_function)
        # print the hyperparameterizations
        print(knn.get_params())

输出结果如下:

{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 1, 
 'p': 2, 'weights': 'uniform'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 1, 
 'p': 2, 'weights': 'distance'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 3, 
 'p': 2, 'weights': 'uniform'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 3, 
 'p': 2, 'weights': 'distance'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 5, 
 'p': 2, 'weights': 'uniform'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 5, 
 'p': 2, 'weights': 'distance'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 7, 
 'p': 2, 'weights': 'uniform'}
{'algorithm': 'auto', 'leaf_size': 30, 'metric': 'minkowski', 
 'metric_params': None, 'n_jobs': None, 'n_neighbors': 7, 
 'p': 2, 'weights': 'distance'}

这个实现虽然非常适合展示网格搜索过程的工作原理,但在尝试评估具有34甚至10种不同类型超参数(每种都有大量可能设置)的估算器时,可能并不实用。

以这种方式继续进行意味着需要编写和跟踪多个 for 循环,这可能会很繁琐。幸运的是,scikit-learnmodel_selection 模块提供了一种叫做 GridSearchCV 的方法,它更加用户友好。我们将在接下来的主题中讨论这个方法。

GridSearchCV

GridsearchCV 是一种调优方法,通过评估网格中提到的参数组合来构建模型。在下图中,我们将看到 GridSearchCV 与手动搜索的不同,并通过表格形式详细了解网格搜索。

使用 GridSearchCV 进行调优

我们可以通过利用 model_selection.GridSearchCV 更轻松地进行网格搜索。

为了做对比,我们将使用之前的同一个乳腺癌数据集和 k-NN 分类器:

from sklearn import model_selection, datasets, neighbors
# load the data
cancer = datasets.load_breast_cancer()
# target
y = cancer.target
# features
X = cancer.data

加载数据后,我们需要做的下一件事是初始化我们希望在不同超参数化下评估的估计器类:

# initialize the estimator
knn = neighbors.KNeighborsClassifier()

然后我们定义网格:

# grid contains k and the weight function
grid = {'n_neighbors': [1, 3, 5, 7],\
        'weights': ['uniform', 'distance']}

为了设置搜索,我们将刚初始化的估计器和超参数网格传递给 model_selection.GridSearchCV()。我们还必须指定一个评分指标,这个方法将用于评估在搜索过程中尝试的各种超参数化的表现。

最后一步是使用交叉验证通过 cv 参数设置将要使用的分割数。我们将其设置为 10,从而进行 10 折交叉验证:

"""
 set up the grid search with scoring on precision and 
number of folds = 10
"""
gscv = model_selection.GridSearchCV(estimator=knn, \
                                    param_grid=grid, \
                                    scoring='precision', cv=10)

最后一步是通过 fit() 方法将数据传递给这个对象。一旦完成,网格搜索过程将启动:

# start the search
gscv.fit(X, y)

默认情况下,关于搜索的信息将打印到屏幕上,允许你看到将要评估的 k-NN 估计器的确切估计器参数化:

图 8.6: k-NN 估计器的估计器参数化

图 8.6: k-NN 估计器的估计器参数化

一旦搜索完成,我们可以通过访问和打印 cv_results_ 属性来查看结果。cv_results_ 是一个字典,包含关于在每个超参数化下模型表现的有用信息,例如评分指标的平均测试集值(mean_test_score,值越低越好),尝试的所有超参数化的完整列表(params),以及模型与 mean_test_score 相关的排名(rank_test_score)。

找到的最佳模型的排名为 1,第二好的模型排名为 2,以此类推,正如你在 图 8.8 中看到的那样。模型拟合时间通过 mean_fit_time 报告。

虽然对于较小的数据集通常不是一个考虑因素,但在某些情况下,这个值可能很重要,因为您可能会发现通过某些超参数化在模型性能上的边际增加与模型拟合时间的显著增加相关联,这取决于您可用的计算资源,可能会导致该超参数化变得不可行,因为它将花费太长时间来拟合:

# view the results
print(gscv.cv_results_)

输出将如下所示:

图 8.7:GridsearchCV 结果

图 8.7:GridsearchCV 结果

模型排名可见以下图片:

图 8.8:模型排名

图 8.8:模型排名

注意

出于展示目的,输出已经被截断。您可以在这里查看完整的输出:packt.live/2uD12uP

在输出中,值得注意的是,此字典可以轻松转换为 pandas DataFrame,这样使得信息更加清晰易读,并允许我们选择性地显示我们感兴趣的指标。

例如,假设我们只对前五个表现最佳模型的每个超参数化(params)和平均交叉验证测试分数(mean_test_score)感兴趣:

import pandas as pd
# convert the results dictionary to a dataframe
results = pd.DataFrame(gscv.cv_results_)
"""
select just the hyperparameterizations tried, 
the mean test scores, order by score and show the top 5 models
"""
print(results.loc[:,['params','mean_test_score']]\
      .sort_values('mean_test_score', ascending=False).head(5))

运行此代码将产生以下输出:

图 8.9:前 5 个模型的 mean_test_score

图 8.9:前 5 个模型的 mean_test_score

我们还可以使用 pandas 生成以下结果的可视化:

# visualise the result
results.loc[:,['params','mean_test_score']]\
       .plot.barh(x = 'params')

输出将如下所示:

图 8.10:使用 pandas 可视化输出

图 8.10:使用 pandas 可视化输出

当您查看前述图表时,您会发现找到的最佳超参数设置是 n_neighbors = 5weights = 'uniform',因为这会产生最高的平均测试分数(精度)。

注意

此部分的代码可以在 packt.live/2uD12uP 找到。

支持向量机(Support Vector Machine,SVM)分类器

SVM 分类器基本上是一种监督学习模型。它是一种常用的估算器类别,可用于二元和多类分类。它在数据有限的情况下表现良好,因此是一个可靠的模型。与高度迭代或集成方法(如人工神经网络或随机森林)相比,训练速度相对较快,这使其成为在计算机处理能力有限的情况下的良好选择。

它通过利用一种称为核函数的特殊数学公式进行预测。这个函数可以采用多种形式,其中一些函数(如具有其自身可调参数的多项式核函数)。

SVM 在图像分类的背景下表现良好,您将在以下练习中看到。

注意

有关支持向量机的更多信息,请参见packt.live/37iDytw,并参考packt.live/38xaPkC

练习 8.02: 支持向量机的网格搜索超参数调优

在本练习中,我们将使用一种叫做 SVM 分类器的估计器,并通过网格搜索策略来调整其超参数。

我们在这里关注的监督学习目标是基于图像对手写数字(0-9)进行分类。我们将使用的数据集包含 1,797 个标记的手写数字图像。

注意

本练习所用的数据集可以在我们的 GitHub 库中找到:packt.live/2vdbHg9

数据集的详细信息可以在原始数据集的网址找到:packt.live/36cX35b

  1. 在 Google Colab 中创建一个新的笔记本。

  2. 从 scikit-learn 中导入datasetssvmmodel_selection

    from sklearn import datasets, svm, model_selection
    
  3. 加载数据。我们将这个对象命名为 images,然后将目标y和特征X分离。在训练步骤中,SVM 分类器将学习y如何与X相关联,因此,当给定新的X值时,它能够预测新的y值:

    # load data
    digits = datasets.load_digits()
    # target
    y = digits.target
    # features
    X = digits.data
    
  4. 将估计器初始化为多类 SVM 分类器,并将gamma参数设置为scale

    # support vector machine classifier
    clr = svm.SVC(gamma='scale')
    

    注意

    关于gamma参数的更多信息,请访问packt.live/2Ga2l79

  5. 定义我们的网格,涵盖四种不同的分类器超参数配置,包括线性核和多项式核(分别为234度)。我们希望查看哪种超参数配置能够提供更准确的预测:

    # hyperparameter grid. contains linear and polynomial kernels
    grid = [{'kernel': ['linear']},\
            {'kernel': ['poly'], 'degree': [2, 3, 4]}]
    
  6. 设置网格搜索 k 折交叉验证,使用10折和准确度评分方法。确保它的输入包含我们的gridestimator对象:

    """
    setting up the grid search to score on accuracy and 
    evaluate over 10 folds
    """
    cv_spec = model_selection.GridSearchCV\
              (estimator=clr, param_grid=grid, \
               scoring='accuracy', cv=10)
    
  7. 通过将数据提供给.fit()方法来开始搜索。过程中,将会打印出超参数配置尝试情况以及选择的评分方法:

    # start the grid search
    cv_spec.fit(X, y)
    

    你应该看到以下输出:

    图 8.11: 使用.fit()方法进行网格搜索

    图 8.11: 使用.fit()方法进行网格搜索

  8. 要查看所有结果,只需将cv_spec.cv_results_打印到屏幕上。你会看到结果结构为字典,这样你就可以通过键访问所需的信息:

    # what is the available information
    print(cv_spec.cv_results_.keys())
    

    你将看到以下信息:

    图 8.12: 结果作为字典

    图 8.12: 结果作为字典

  9. 对于这个练习,我们主要关心每种不同超参数配置在测试集上的表现。你可以通过cv_spec.cv_results_['mean_test_score']查看第一个超参数配置,通过cv_spec.cv_results_['params']查看第二个超参数配置。

    我们将结果字典转换为pandas DataFrame,并找到最佳的超参数配置:

    import pandas as pd
    # convert the dictionary of results to a pandas dataframe
    results = pd.DataFrame(cv_spec.cv_results_)
    # show hyperparameterizations
    print(results.loc[:,['params','mean_test_score']]\
          .sort_values('mean_test_score', ascending=False))
    

    你将看到以下结果:

    图 8.13:参数化结果

    图 8.13:参数化结果

    注意

    你可能会得到略有不同的结果。不过,你获得的值应与前面的输出大致一致。

  10. 最好的做法是可视化你所得到的任何结果。pandas 使这一过程变得简单。运行以下代码来生成可视化图表:

    # visualize the result
    (results.loc[:,['params','mean_test_score']]\
            .sort_values('mean_test_score', ascending=True)\
            .plot.barh(x='params', xlim=(0.8)))
    

    输出结果如下:

    图 8.14:使用 pandas 可视化结果

图 8.14:使用 pandas 可视化结果

注意

要访问此特定部分的源代码,请参考packt.live/36At2MO

你也可以在packt.live/2YdQsGq上在线运行此示例。

我们可以看到,具有三次多项式核函数的 SVM 分类器在所有评估的超参数组合中具有最高的准确率。你可以随意向网格中添加更多超参数组合,看看是否能提高分数。

网格搜索的优缺点

与手动搜索相比,网格搜索的主要优点在于它是一个自动化过程,用户只需设置一次并可忘记它。此外,你还可以精确控制所评估的超参数组合,当你对哪些超参数组合可能在你的场景中有效有先验知识时,这种精确性是一个很好的优势。由于网格的定义是明确的,你也很容易理解搜索过程中将会发生什么。

网格搜索策略的主要缺点是计算开销非常大;也就是说,当需要尝试的超参数组合数量大幅增加时,处理时间可能非常慢。此外,在定义网格时,你可能会无意中遗漏一个实际上是最优的超参数组合。如果它没有出现在网格中,它将永远不会被尝试。

为了克服这些缺点,我们将在下一部分探讨随机搜索。

随机搜索

与网格搜索通过预定义的超参数集合逐一进行搜索不同,在随机搜索中,我们通过假设每个超参数是一个随机变量,从可能性的分布中进行采样。在深入了解这一过程之前,简要回顾一下随机变量及其分布的含义是有帮助的。

随机变量及其分布

随机变量是非恒定的(其值可能变化),其变异性可以通过分布来描述。随机变量的分布有很多种类型,但每种都属于两大类之一:离散型和连续型。我们使用离散型分布来描述值只能取整数的随机变量,例如计数。

一个例子是主题公园一天的游客数量,或者高尔夫球手打入一杆所需的尝试次数。

我们使用连续分布来描述那些值沿着由无限小增量组成的连续体的随机变量。示例包括人类的身高或体重,或者外部空气温度。分布通常具有控制其形状的参数。

离散分布可以通过所谓的概率质量函数来数学描述,该函数定义了随机变量取某一特定值的确切概率。该函数左侧的常见符号为P(X=x),其英文含义是随机变量X等于某一特定值x的概率为P。请记住,概率的范围介于0(不可能)和1(必然)之间。

根据定义,所有可能的x值对应的每个P(X=x)的总和将等于 1,或者换句话说,X取任何值的概率为 1。这种分布的一个简单例子是离散均匀分布,其中随机变量X只能取有限范围内的一个值,而且它取任何特定值的概率对于所有值都是相同的,因此称之为均匀分布。

例如,如果有 10 个可能的值,X取任何特定值的概率恰好为 1/10。如果有 6 个可能的值,就像标准的六面骰子一样,概率将是 1/6,依此类推。离散均匀分布的概率质量函数为:

图 8.15: 离散均匀分布的概率质量函数

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_08_15.jpg)

图 8.15: 离散均匀分布的概率质量函数

以下代码将帮助我们查看该分布的形式,其中X有 10 个可能值。

首先,我们创建一个X可能取值的所有值的列表:

# list of all xs
X = list(range(1, 11))
print(X)

输出结果如下:

 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

然后我们计算X取任意x值的概率(P(X=x)):

# pmf, 1/n * n = 1
p_X_x = [1/len(X)] * len(X)
# sums to 1
print(p_X_x)

如前所述,概率的总和将等于 1,而这适用于任何分布。现在我们拥有了所有需要的内容来可视化该分布:

import matplotlib.pyplot as plt
plt.bar(X, p_X_x)
plt.xlabel('X')
plt.ylabel('P(X=x)')

输出结果如下:

图 8.16: 可视化条形图

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_08_16.jpg)

图 8.16: 可视化条形图

在视觉输出中,我们看到X在 1 到 10 之间的特定整数的概率为 1/10。

注意

你常见的其他离散分布包括二项分布、负二项分布、几何分布和泊松分布,所有这些我们都鼓励你进一步研究。可以将这些术语输入搜索引擎以获取更多信息。

连续随机变量的分布稍微复杂一点,因为我们无法直接计算 P(X=x),因为 X 位于一个连续体上。然而,我们可以使用积分来近似某一范围内的概率,但这超出了本书的讨论范围。X 与概率的关系通过概率密度函数 P(X) 来描述。或许最著名的连续分布就是正态分布,它在视觉上呈现为钟形曲线。

正态分布有两个描述其形状的参数:均值(𝜇)和方差(𝜎2)。正态分布的概率密度函数为:

图 8.17:正态分布的概率密度函数

图 8.17:正态分布的概率密度函数

以下代码展示了两个正态分布,它们的均值(𝜇 = 0)相同,但方差参数(𝜎``2 = 1𝜎``2 = 2.25)不同。首先,我们使用 NumPy 的 .linspace 方法生成从 -1010 的 100 个均匀分布的值:

import numpy as np
# range of xs
x = np.linspace(-10, 10, 100)

然后,我们为这两个正态分布生成近似的 X 概率。

使用 scipy.stats 是处理分布的好方法,它的 pdf 方法使我们可以轻松地可视化概率密度函数的形状:

import scipy.stats as stats
# first normal distribution with mean = 0, variance = 1
p_X_1 = stats.norm.pdf(x=x, loc=0.0, scale=1.0**2)
# second normal distribution with mean = 0, variance = 2.25
p_X_2 = stats.norm.pdf(x=x, loc=0.0, scale=1.5**2)

注意

在这个例子中,loc 对应于 𝜇,而 scale 对应于标准差,它是 𝜎``2 的平方根,因此我们要对输入进行平方处理。

然后我们可视化结果。注意,𝜎``2 控制着分布的宽度,也就控制了随机变量的变化范围:

plt.plot(x,p_X_1, color='blue')
plt.plot(x, p_X_2, color='orange')
plt.xlabel('X')
plt.ylabel('P(X)')

输出结果如下:

图 8.18:可视化正态分布

图 8.18:可视化正态分布

你常见的其他离散分布包括伽玛分布、指数分布和贝塔分布,我们鼓励你进行进一步研究。

注意

本节的代码可以在packt.live/38Mfyzm找到。

随机搜索过程的简单演示

再次强调,在我们开始讲解 scikit-learn 实现的随机搜索参数调优之前,我们将使用简单的 Python 工具演示这个过程。到目前为止,我们一直使用分类问题来展示调优概念,但接下来我们将探讨回归问题。我们能否找到一个模型,基于患者的 BMI 和年龄等特征预测糖尿病的发展?

注意

原始数据集可以在packt.live/2O4XN6v找到。

本节的代码可以在packt.live/3aOudvK找到。

我们首先加载数据:

from sklearn import datasets, linear_model, model_selection
# load the data
diabetes = datasets.load_diabetes()
# target
y = diabetes.target
# features
X = diabetes.data

为了更好地理解数据,我们可以检查第一个患者的疾病进展:

# the first patient has index 0
print(y[0])

输出结果如下:

 151.0

现在让我们来分析它们的特征:

# let's look at the first patients data
print(dict(zip(diabetes.feature_names, X[0])))

我们应该看到以下结果:

图 8.19:患者特征字典

图 8.19:患者特征字典

对于这种情况,我们将尝试一种叫做岭回归的技术,它将拟合一个线性模型到数据上。岭回归是一种特殊的方法,允许我们直接使用正则化来帮助减轻过拟合问题。岭回归有一个关键超参数𝛼,它控制模型拟合中的正则化程度。如果𝛼设置为 1,将不使用正则化,这实际上是岭回归模型拟合与 OLS 线性回归模型拟合相等的特殊情况。增大𝛼的值,会增加正则化的程度。

注意

我们在第七章中讨论了岭回归,机器学习模型的泛化

关于岭回归和正则化的更多信息,请参见packt.live/2NR3GUq

在随机搜索超参数调整的背景下,我们假设𝛼是一个随机变量,我们需要指定一个可能的分布。

在这个例子中,我们假设𝛼遵循伽马分布。这个分布有两个参数,k 和𝜃,分别控制分布的形状和尺度。

对于岭回归,我们认为最优的𝛼值接近 1,当𝛼远离 1 时,其可能性逐渐降低。反映这一思想的伽马分布的参数化方式是 k 和𝜃都等于 1。为了可视化这种分布的形式,我们可以运行以下代码:

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
# values of alpha
x = np.linspace(1, 20, 100)
# probabilities
p_X = stats.gamma.pdf(x=x, a=1, loc=1, scale=2)
plt.plot(x,p_X)
plt.xlabel('alpha')
plt.ylabel('P(alpha)')

输出将如下所示:

图 8.20:概率的可视化

图 8.20:概率的可视化

在图中,你可以看到,对于较小的𝛼值,概率急剧下降,而对于较大的𝛼值,下降速度较慢。

随机搜索过程的下一步是从所选分布中采样 n 个值。在这个例子中,我们将绘制 100 个𝛼值。请记住,抽取某个特定𝛼值的概率与该值在此分布中定义的概率有关:

# n sample values
n_iter = 100
# sample from the gamma distribution
samples = stats.gamma.rvs(a=1, loc=1, scale=2, \
                          size=n_iter, random_state=100)

注意

我们设置一个随机状态以确保结果的可重复性。

绘制样本的直方图,如下图所示,可以揭示出一个大致符合我们所采样的分布的形状。请注意,随着样本数量的增加,直方图将更符合该分布:

# visualize the sample distribution
plt.hist(samples)
plt.xlabel('alpha')
plt.ylabel('sample count')

输出将如下所示:

图 8.21:样本分布的可视化

图 8.21:样本分布的可视化

然后,将为每个采样的𝛼值拟合一个模型并评估其性能。正如我们在本章中其他超参数调整方法中看到的,性能将通过 k 折交叉验证(k = 10)来评估,但因为我们处理的是回归问题,所以评估指标将是测试集的负均方误差(MSE)。

使用此度量意味着较大的值更好。我们将结果存储在一个字典中,每个 𝛼 值作为键,对应的交叉验证负 MSE 作为值:

# we will store the results inside a dictionary
result = {}
# for each sample
for sample in samples:
    """
    initialize a ridge regression estimator with alpha set 
    to the sample value
    """
    reg = linear_model.Ridge(alpha=sample)
    """
    conduct a 10-fold cross validation scoring on 
    negative mean squared error
    """
    cv = model_selection.cross_val_score\
         (reg, X, y, cv=10, \
          scoring='neg_mean_squared_error')
    # retain the result in the dictionary
    result[sample] = [cv.mean()]

我们将不再检查原始的结果字典,而是将其转换为 pandas DataFrame,转置并为列命名。按负均方误差降序排序显示,对于此问题,正则化的最佳水平实际上是在 𝛼 约为 1 时,即我们没有找到证据表明该问题需要正则化,OLS 线性模型足以解决该问题:

import pandas as pd
"""
convert the result dictionary to a pandas dataframe, 
transpose and reset the index
"""
df_result = pd.DataFrame(result).T.reset_index()
# give the columns sensible names
df_result.columns = ['alpha', 'mean_neg_mean_squared_error']
print(df_result.sort_values('mean_neg_mean_squared_error', \
                            ascending=False).head())

输出将如下所示:

图 8.22:随机搜索过程的输出

图 8.22:随机搜索过程的输出

注意

结果会有所不同,取决于所使用的数据。

只要可能,进行结果可视化总是有益的。将 𝛼 与负均方误差绘制成散点图清晰地表明,远离 𝛼 = 1 并不会改善预测性能:

plt.scatter(df_result.alpha, \
            df_result.mean_neg_mean_squared_error)
plt.xlabel('alpha')
plt.ylabel('-MSE')

输出将如下所示:

图 8.23:绘制散点图

图 8.23:绘制散点图

我们发现最优的 𝛼 值为 1(其默认值),这在超参数调优中是一个特殊情况,因为最优的超参数配置恰好是默认值。

使用 RandomizedSearchCV 进行调优

实际上,我们可以使用 scikit-learn 的 model_selection 模块中的 RandomizedSearchCV 方法进行搜索。你只需要传入估计器、你希望调优的超参数及其分布、你希望从每个分布中抽取的样本数量,以及你希望评估模型性能的度量标准。这些对应于 param_distributionsn_iterscoring 参数。为了演示,我们使用 RandomizedSearchCV 进行之前完成的搜索。首先,我们加载数据并初始化我们的岭回归估计器:

from sklearn import datasets, model_selection, linear_model
# load the data
diabetes = datasets.load_diabetes()
# target
y = diabetes.target
# features
X = diabetes.data
# initialise the ridge regression
reg = linear_model.Ridge()

然后,我们指定希望调优的超参数是 alpha,并且我们希望 𝛼 按 gamma 分布,k = 1𝜃 = 1

from scipy import stats
# alpha ~ gamma(1,1)
param_dist = {'alpha': stats.gamma(a=1, loc=1, scale=2)}

接下来,我们设置并运行随机搜索过程,它将从我们的 gamma(1,1) 分布中抽取 100 个值,拟合岭回归并使用交叉验证评估其性能,评分标准是负均方误差:

"""
set up the random search to sample 100 values and 
score on negative mean squared error
"""
rscv = model_selection.RandomizedSearchCV\
       (estimator=reg, param_distributions=param_dist, \
        n_iter=100, scoring='neg_mean_squared_error')
# start the search
rscv.fit(X,y)

完成搜索后,我们可以提取结果并生成一个 pandas DataFrame,正如我们之前所做的那样。按 rank_test_score 排序并查看前五行与我们的结论一致,即 alpha 应设置为 1,并且该问题似乎不需要正则化:

import pandas as pd
# convert the results dictionary to a pandas data frame
results = pd.DataFrame(rscv.cv_results_)
# show the top 5 hyperparamaterizations
print(results.loc[:,['params','rank_test_score']]\
      .sort_values('rank_test_score').head(5))

输出将如下所示:

图 8.24:使用 RandomizedSearchCV 调优的输出

图 8.24:使用 RandomizedSearchCV 调优的输出

注意

之前的结果可能会有所不同,取决于数据。

练习 8.03:随机搜索超参数调优,应用于随机森林分类器

在这个练习中,我们将重新审视手写数字分类问题,这次使用随机森林分类器,并通过随机搜索策略调优超参数。随机森林是一种常用的方法,适用于单类和多类分类问题。它通过生成 n 个简单的树模型来学习,每棵树逐步将数据集分割成能够最佳分离不同类别数据点的区域。

最终生成的模型可以看作是每个树模型的平均值。通过这种方式,随机森林是一种 集成 方法。我们将在这个练习中调优的参数是 criterionmax_features

criterion 是指从类纯度角度评估每次分裂的方式(分裂越纯净越好),而 max_features 是随机森林在寻找最佳分裂时可以使用的最大特征数量。

以下步骤将帮助你完成这个练习。

  1. 在 Google Colab 中创建一个新的笔记本。

  2. 导入数据并提取特征 X 和目标 y

    from sklearn import datasets
    # import data
    digits = datasets.load_digits()
    # target
    y = digits.target
    # features
    X = digits.data
    
  3. 初始化随机森林分类器估算器。我们将 n_estimators 超参数设置为 100,这意味着最终模型的预测基本上是 100 个简单树模型的平均值。请注意使用随机状态来确保结果的可重复性:

    from sklearn import ensemble
    # an ensemble of 100 estimators
    rfc = ensemble.RandomForestClassifier(n_estimators=100, \
                                          random_state=100)
    
  4. 我们将调优的参数之一是 max_features。让我们找出它可以取的最大值:

    # how many features do we have in our dataset?
    n_features = X.shape[1]
    print(n_features)
    

    你应该看到我们有 64 个特征:

    64
    

    现在我们知道了 max_features 的最大值,我们可以自由地定义超参数输入到随机搜索过程中。此时,我们没有理由认为 max_features 的某个特定值更加优化。

  5. 设置一个覆盖 164 范围的离散均匀分布。记住概率质量函数 P(X=x) = 1/n,因此在我们的情况下 P(X=x) = 1/64。由于 criterion 只有两个离散选项,因此它也将作为一个离散均匀分布进行采样,P(X=x) = ½

    from scipy import stats
    """
    we would like to smaple from criterion and 
    max_features as discrete uniform distributions
    """
    param_dist = {'criterion': ['gini', 'entropy'],\
                  'max_features': stats.randint(low=1, \
                                                high=n_features)}
    
  6. 现在我们已经拥有了设置随机搜索过程所需的一切。如前所述,我们将使用准确率作为模型评估的指标。请注意使用随机状态:

    from sklearn import model_selection
    """
    setting up the random search sampling 50 times and 
    conducting 5-fold cross-validation
    """
    rscv = model_selection.RandomizedSearchCV\
           (estimator=rfc, param_distributions=param_dist, \
            n_iter=50, cv=5, scoring='accuracy' , random_state=100)
    
  7. 让我们通过 fit 方法启动过程。请注意,由于随机森林的内部迭代过程,拟合随机森林和交叉验证都是计算量大的过程。生成结果可能需要一些时间:

    # start the process
    rscv.fit(X,y)
    

    你应该看到如下内容:

    图 8.25:RandomizedSearchCV 结果

    图 8.25:RandomizedSearchCV 结果

  8. 接下来,您需要检查结果。从results属性创建一个pandas DataFrame,按rank_test_score排序,并查看排名前五的模型超参数配置。请注意,由于随机搜索是随机抽取超参数配置,因此可能会出现重复项。我们从 DataFrame 中移除重复的条目:

    import pandas as pd
    # convert the dictionary of results to a pandas dataframe
    results = pd.DataFrame(rscv.cv_results_)
    # removing duplication
    distinct_results = results.loc[:,['params',\
                                      'mean_test_score']]
    # convert the params dictionaries to string data types
    distinct_results.loc[:,'params'] = distinct_results.loc\
                                       [:,'params'].astype('str')
    # remove duplicates
    distinct_results.drop_duplicates(inplace=True)
    # look at the top 5 best hyperparamaterizations
    distinct_results.sort_values('mean_test_score', \
                                 ascending=False).head(5)
    

    您应该会得到以下输出:

    图 8.26:前五名超参数配置

    图 8.26:前五名超参数配置

    注意

    您可能会得到略有不同的结果。不过,您获得的值应该与前述输出中的值大体一致。

  9. 最后一步是可视化结果。包括所有参数配置会导致图表杂乱,因此我们将筛选出那些平均测试得分 > 0.93 的参数配置:

    # top performing models
    distinct_results[distinct_results.mean_test_score > 0.93]\
                     .sort_values('mean_test_score')\
                     .plot.barh(x='params', xlim=(0.9))
    

    输出将如下所示:

    图 8.27:可视化表现最佳模型的测试得分

图 8.27:可视化表现最佳模型的测试得分

注意

要访问此特定部分的源代码,请参考packt.live/2uDVct8

您还可以在网上运行此示例,访问packt.live/3gbQMvw

我们发现最好的超参数配置是使用gini准则的随机森林分类器,最大特征数设置为 4。

随机搜索的优缺点

由于随机搜索从可能的超参数配置范围中抽取有限的样本(在model_selection.RandomizedSearchCV中为n_iter),因此可以将超参数搜索的范围扩展到超出网格搜索实际可行的范围。这是因为网格搜索必须尝试该范围内的所有可能性,设置一个较大的值范围可能会导致处理速度过慢。搜索更广泛的范围使您有机会发现真正的最优解。

与手动搜索和网格搜索策略相比,您确实牺牲了一定的控制力以获得这一好处。另一个需要考虑的问题是,设置随机搜索比其他选项稍微复杂一些,因为您必须指定分布。总是有出错的可能性。也就是说,如果您不确定使用什么分布,可以选择离散或连续均匀分布,针对各个变量类型,这将为所有选项分配相等的选择概率。

活动 8.01:蘑菇有毒吗?

想象一下,您是一名数据科学家,在当地大学的生物学系工作。您的同事是一位专门研究真菌的生物学家(真菌学家),她请求您帮助她开发一个机器学习模型,能够根据蘑菇外观的相关特征判断某个蘑菇物种是否有毒。

本活动的目标是使用网格搜索和随机搜索策略来找到一个最优模型。

注意

本次练习所用的数据集可以在我们的 GitHub 仓库找到:packt.live/38zdhaB

数据集的属性详情可以在原始数据集网站找到:packt.live/36j0jfA

  1. 使用pandas.read_csv()方法将数据加载到 Python 中,命名为mushrooms对象。

    提示:数据集是 CSV 格式且没有表头。在pandas.read_csv()中设置header=None

  2. 从数据集中分离出目标变量y和特征X

    提示:目标变量可以在第一列中找到(mushrooms.iloc[:,0]),特征则位于其余列(mushrooms.iloc[:,1:])。

  3. 重新编码目标变量y,使有毒蘑菇表示为1,可食用蘑菇表示为0

  4. 将特征集X的列转换为具有二进制表示的numpy数组。这称为独热编码(one-hot encoding)。

    提示:使用preprocessing.OneHotEncoder()X转换。

  5. 进行网格搜索和随机搜索,以找到随机森林分类器的最佳超参数配置。使用准确率作为模型评估方法。确保在初始化分类器时,以及进行随机搜索时,random_state = 100

    对于网格搜索,使用以下内容:

    {'criterion': ['gini', 'entropy'],\
     'max_features': [2, 4, 6, 8, 10, 12, 14]}
    

    对于随机搜索,使用以下内容:

    {'criterion': ['gini', 'entropy'],\
     'max_features': stats.randint(low=1, high=max_features)}
    
  6. 绘制随机搜索找到的前 10 个模型的平均测试分数与超参数化的关系图。

    你应该看到一个类似下面的图表:

图 8.28:平均测试分数图

图 8.28:平均测试分数图

注意

活动的解决方案可以在这里找到:packt.live/2GbJloz

摘要

本章中,我们介绍了基于搜索估计器超参数化以提高性能的三种超参数调优策略。

手动搜索是三种方法中最具操作性的,但可以让你对整个过程有独特的理解。它适用于估算器较简单(超参数较少)的情况。

网格搜索是一种自动化方法,是三种方法中最系统化的,但当可能的超参数范围增大时,运行时可能会非常消耗计算资源。

随机搜索虽然设置最为复杂,但它基于从超参数分布中进行采样,这使得你能够扩大搜索范围,从而增加发现优秀解的机会,而这些可能在网格搜索或手动搜索中被错过。

在下一章,我们将学习如何可视化结果、总结模型并阐述特征的重要性和权重。

第九章:9. 解释机器学习模型

概述

本章将向你展示如何解释机器学习模型的结果,并深入了解它所发现的模式。通过本章学习后,你将能够分析线性模型的权重和RandomForest的变量重要性。你还将能够通过置换法实现变量重要性分析,分析特征的重要性。你将使用部分依赖图(partial dependence plot)来分析单一变量,并使用 lime 包进行局部解释。

介绍

在上一章中,你已经学习了如何找到一些最流行机器学习算法的最佳超参数,以获得更好的预测性能(即,更准确的预测)。

机器学习算法通常被称为“黑箱”,我们只能看到输入和输出,而算法内部的实现非常不透明,因此人们不知道其内部发生了什么。

随着时间的推移,我们可以感受到对机器学习模型更高透明度的需求日益增加。在过去几年中,我们看到一些算法被指控歧视特定群体。例如,几年前,一个名为 ProPublica 的非盈利新闻机构揭示了由 Northpointe 公司开发的 COMPAS 算法中的偏见。该算法的目的是评估罪犯再次犯罪的可能性。研究表明,该算法根据人口统计特征而非其他特征,为某些群体预测了较高的风险水平。这个例子突显了正确、清晰地解释模型结果及其逻辑的重要性。

幸运的是,一些机器学习算法提供了方法,帮助我们理解它们为特定任务和数据集学习到的参数。也有一些模型无关的函数,可以帮助我们更好地理解模型做出的预测。因此,解释模型的方法有很多种,既有针对特定模型的,也有通用的。

这些技术的适用范围也有所不同。在文献中,我们通常分为全局解释和局部解释。全局解释意味着我们关注整个数据集中所有观测值的变量,目的是了解哪些特征对目标变量有最大的整体影响。例如,如果你正在预测电信公司客户流失率,你可能会发现模型最重要的特征是客户使用情况和每月平均支付金额。而局部解释则只关注单一观测值,并分析不同变量的影响。我们将查看一个具体的案例,了解模型做出最终预测的原因。例如,你会查看一个被预测会流失的客户,发现他每年 9 月都会购买新的 iPhone 型号。

在本章中,我们将讨论一些技术,帮助你如何解读模型或其结果。

线性模型系数

第二章,回归第三章,二分类中,你看到了线性回归模型是如何学习以下形式的函数参数的:

图 9.1: 线性回归模型的函数参数

图 9.1: 线性回归模型的函数参数

目标是找到最优参数(w1, w2 …, wn),使得预测值 ŷ̂ 非常接近实际目标值 y。因此,一旦你训练了模型,并且在没有过拟合的情况下得到了良好的预测性能,你可以利用这些参数(或系数)来理解哪些变量对预测产生了较大影响。如果某个系数接近 0,这意味着相关特征对结果的影响较小。另一方面,如果系数较高(无论是正的还是负的),这意味着该特征对预测结果有很大的影响。

我们以以下函数为例:100 + 0.2 * x1 + 200 * x2 - 180 * x3。x1 的系数只有0.2,与其他系数相比非常低,对最终结果的影响较小。x2 的系数为正,因此它会对预测产生正面影响。它与 x3 的系数相反,因为 x3 的系数为负。

但是,为了能够比较“苹果与苹果”,你需要重新缩放特征,使它们具有相同的尺度,这样你就可以比较它们的系数。如果不这样做,可能 x1 的范围是从 100 万到 500 万,而 x2 和 x3 的范围是188。在这种情况下,尽管 x1 的系数较小,但 x1 值的微小变化对预测有很大影响。另一方面,如果三个系数都在 -1 到 1 之间,那么我们可以说,预测目标变量的主要驱动因素是特征 x2 和 x3。

sklearn中,获取线性模型的系数非常简单,你只需要调用 coef_ 属性。让我们用 sklearn 中的糖尿病数据集做一个实际的例子:

from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression
data = load_diabetes()
# fit a linear regression model to the data
lr_model = LinearRegression()
lr_model.fit(data.data, data.target)
lr_model.coef_

输出将如下所示:

图 9.2: 线性回归参数的系数

图 9.2: 线性回归参数的系数

让我们创建一个包含这些值和列名的 DataFrame:

import pandas as pd
coeff_df = pd.DataFrame()
coeff_df['feature'] = data.feature_names
coeff_df['coefficient'] = lr_model.coef_
coeff_df.head()

输出将如下所示:

图 9.3: 线性回归模型的系数

图 9.3: 线性回归模型的系数

对于特征系数而言,较大的正数或负数意味着它对结果有强烈的影响。另一方面,如果系数接近 0,则意味着该变量对预测的影响较小。

从这张表格中,我们可以看到列 s1 的系数非常低(一个大负数),所以它对最终预测有负面影响。如果 s1 增加 1 单位,预测值将减少 -792.184162。另一方面,bmi 在预测中有一个较大的正数(519.839787),因此糖尿病的风险与该特征高度相关:体重指数(BMI)的增加意味着糖尿病风险显著增加。

练习 9.01:提取线性回归系数

在本次练习中,我们将训练一个线性回归模型来预测客户流失率,并提取其系数。

注意

我们将使用的数据集由多伦多大学的 Carl Rasmussen 分享:packt.live/37hInDr

这个数据集是通过模拟合成生成的,用于预测因为长时间排队而离开银行的客户比例。

该数据集的 CSV 版本可以在此找到:packt.live/3kZrggU

这个数据集的变量数据字典可以在此找到:

packt.live/3aBGhQD

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本。

  2. 导入以下包:pandastrain_test_split 来自 sklearn.model_selectionStandardScaler 来自 sklearn.preprocessingLinearRegression 来自 sklearn.linear_modelmean_squared_error 来自 sklearn.metricsaltair

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import StandardScaler
    from sklearn.linear_model import LinearRegression
    from sklearn.metrics import mean_squared_error
    import altair as alt
    
  3. 创建一个名为 file_url 的变量,包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter09/Dataset/phpYYZ4Qc.csv'
    
  4. 使用 .read_csv() 将数据集加载到名为 df 的 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 打印 DataFrame 的前五行:

    df.head()
    

    你应该得到以下输出:

    图 9.4:加载的 DataFrame 前五行

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_04.jpg)

    图 9.4:加载的 DataFrame 前五行

    注意

    为了展示的方便,输出已被截断。请参考 packt.live/3kZrggU 获取完整的输出。

  6. 使用 .pop() 提取 rej 列,并将其保存为名为 y 的变量:

    y = df.pop('rej')
    
  7. 使用 .describe() 打印 DataFrame 的摘要。

    df.describe()
    

    你应该得到以下输出:

    图 9.5:DataFrame 的统计测量

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_05.jpg)

    图 9.5:DataFrame 的统计测量

    注意

    上述图表是输出的简化版本。

    从这个输出中,我们可以看到数据没有经过标准化。变量的尺度不同。

  8. 使用 train_test_split() 将 DataFrame 分割成训练集和测试集,test_size=0.3random_state = 1

    X_train, X_test, y_train, y_test = train_test_split\
                                       (df, y, test_size=0.3, \
                                        random_state=1)
    
  9. 实例化 StandardScaler

    scaler = StandardScaler()
    
  10. 在训练集上训练 StandardScaler 并使用 .fit_transform() 标准化数据:

    X_train = scaler.fit_transform(X_train)
    
  11. 使用 .transform() 标准化测试集:

    X_test = scaler.transform(X_test)
    
  12. 实例化 LinearRegression 并将其保存为名为 lr_model 的变量:

    lr_model = LinearRegression()
    
  13. 在训练集上使用 .fit() 训练模型:

    lr_model.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 9.6:线性回归的日志

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_06.jpg)

    图 9.6:线性回归日志

  14. 使用.predict()预测训练集和测试集的结果:

    preds_train = lr_model.predict(X_train)
    preds_test = lr_model.predict(X_test)
    
  15. 计算训练集的均方误差并打印其值:

    train_mse = mean_squared_error(y_train, preds_train)
    train_mse
    

    你应该得到以下输出:

    图 9.7:训练集的 MSE 得分

    图 9.7:训练集的 MSE 得分

    我们在训练集上获得了相当低的 MSE 得分。

  16. 计算测试集的均方误差并打印其值:

    test_mse = mean_squared_error(y_test, preds_test)
    test_mse
    

    你应该得到以下输出:

    图 9.8:测试集的 MSE 得分

    图 9.8:测试集的 MSE 得分

    我们在测试集上的 MSE 得分也很低,并且与训练集的得分非常相似。所以,我们的模型并没有出现过拟合。

    你可能会得到与此处显示的稍有不同的输出。不过,你得到的值应该与本练习中得到的值大致一致。

  17. 使用.coef_打印线性回归模型的系数:

    lr_model.coef_
    

    你应该得到以下输出:

    图 9.9:线性回归模型的系数

    图 9.9:线性回归模型的系数

  18. 创建一个名为coef_df的空 DataFrame:

    coef_df = pd.DataFrame()
    
  19. 为此 DataFrame 创建一个名为feature的新列,列值为df的列名,使用.columns

    coef_df['feature'] = df.columns
    
  20. 使用.coef_为此 DataFrame 创建一个名为coefficient的新列,列值为线性回归模型的系数:

    coef_df['coefficient'] = lr_model.coef_
    
  21. 打印coef_df的前五行:

    coef_df.head()
    

    你应该得到以下输出:

    图 9.10:的前五行

    图 9.10:coef_df的前五行

    从这个输出中,我们可以看到变量a1sxa1sy的值最低(最负值),因此它们对预测的贡献大于此处显示的其他三个变量。

  22. 使用 Altair 绘制一个条形图,使用coef_dfcoefficient作为x轴,feature作为y轴:

    alt.Chart(coef_df).mark_bar().encode(x='coefficient',\
                                         y="feature")
    

    你应该得到以下输出:

    图 9.11:显示线性回归模型系数的图

图 9.11:显示线性回归模型系数的图

从这个输出中,我们可以看到对预测影响最大的变量是:

  • a2pop,表示区域 2 的人口

  • a1pop,表示区域 1 的人口

  • a3pop,表示区域 3 的人口

  • mxql,表示队列的最大长度

  • b1eff,表示银行 1 的效率水平

  • temp,表示温度

前三个变量对结果产生了正向影响(增加目标变量值)。这意味着,在这三个区域中,任何一个区域的人口增长都会增加客户流失的可能性。另一方面,后三个特征对目标变量产生了负向影响(降低目标变量值):如果最大队列长度、银行 1 的效率水平或温度增加,客户流失的风险会降低。

要访问这一特定章节的源代码,请参考packt.live/3kZrggU

本节目前没有在线互动示例,但可以像往常一样在 Google Colab 上运行。

在这个练习中,你学会了如何提取线性回归模型学到的系数,并识别出哪些变量对预测做出了最大贡献。

随机森林变量重要性

第四章基于随机森林的多类别分类,介绍了一种非常强大的树形算法:RandomForest。它是行业中最流行的算法之一,不仅因为它在预测方面取得了非常好的结果,还因为它提供了多个工具来解释其结果,例如变量重要性。

记住在第四章基于随机森林的多类别分类中,我们学到RandomForest构建多个独立的树,然后将它们的结果平均以做出最终预测。我们还学到,它会在每棵树中创建节点,找到能够清晰地将观测值分成两组的最佳切分点。RandomForest使用不同的度量来找到最佳切分点。在sklearn中,你可以使用基尼指数或熵度量进行分类任务,使用均方误差(MSE)或平均绝对误差(MAE)进行回归。我们不深入讲解这些度量,它们都计算给定切分的杂质程度。杂质度量观察的是节点内的观测值彼此之间有多大的不同。例如,如果一个组内的某个特征的值完全相同,那么它的纯度很高;反之,如果组内的值差异较大,则杂质较高。

每个构建的子树都会降低这个杂质评分。因此,我们可以使用这个杂质评分来评估每个变量对最终预测的重要性。这种技术不仅仅适用于RandomForest,还可以应用于任何基于树的算法,如决策树或梯度提升树。

在训练完RandomForest后,你可以通过feature_importances_属性评估其变量重要性(或特征重要性)。

让我们看看如何从sklearn的乳腺癌数据集中提取这些信息:

from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
data = load_breast_cancer()
X, y = data.data, data.target
rf_model = RandomForestClassifier(random_state=168)
rf_model.fit(X, y)
rf_model.feature_importances_

输出将如下图所示:

图 9.12:随机森林模型的特征重要性

图 9.12:随机森林模型的特征重要性

注意

由于随机化,你可能会得到略有不同的结果。

从这个输出中评估哪个重要性值对应于哪个变量可能有点困难。让我们创建一个 DataFrame,里面包含这些值并显示列名:

import pandas as pd
varimp_df = pd.DataFrame()
varimp_df['feature'] = data.feature_names
varimp_df['importance'] = rf_model.feature_importances_
varimp_df.head()

输出将如下所示:

图 9.13:乳腺癌数据集前五个特征的随机森林变量重要性乳腺癌数据集的结果

图 9.13:乳腺癌数据集前五个特征的随机森林变量重要性

从输出结果中,我们可以看到mean radiusmean perimeter的得分最高,这意味着它们在预测目标变量时最为重要。mean smoothness列的值非常低,因此它似乎对模型的输出预测影响不大。

注意

变量重要性的值范围对于不同数据集是不同的;它不是一个标准化的度量。

让我们使用altair将这些变量重要性值绘制成图:

import altair as alt
alt.Chart(varimp_df).mark_bar().encode(x='importance',\
                                       y="feature")

输出结果如下:

图 9.14: 显示随机森林变量重要性的图

图 9.14: 显示随机森林变量重要性的图

从这个图表中,我们可以看到,对于这个随机森林模型来说,最重要的特征是worst perimeterworst areaworst concave points。所以现在我们知道,这些特征在预测肿瘤是良性还是恶性方面最为重要。

练习 9.02: 提取随机森林特征重要性

在本次练习中,我们将提取一个训练好的随机森林分类器模型的特征重要性,该模型用于预测客户流失率。

我们将使用与上一节相同的数据集。

以下步骤将帮助你完成本次练习:

  1. 打开一个新的 Colab 笔记本。

  2. 导入以下包:pandastrain_test_split来自sklearn.model_selection,以及RandomForestRegressor来自sklearn.ensemble

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestRegressor
    from sklearn.metrics import mean_squared_error
    import altair as alt
    
  3. 创建一个名为file_url的变量,其中包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter09/Dataset/phpYYZ4Qc.csv'
    
  4. 使用.read_csv()将数据集加载到名为df的 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用.pop()提取rej列,并将其保存为变量y

    y = df.pop('rej')
    
  6. 使用train_test_split()将数据集拆分为训练集和测试集,设置test_size=0.3random_state=1

    X_train, X_test, y_train, y_test = train_test_split\
                                       (df, y, test_size=0.3, \
                                        random_state=1)
    
  7. 使用random_state=1n_estimators=50max_depth=6min_samples_leaf=60实例化RandomForestRegressor

    rf_model = RandomForestRegressor(random_state=1, \
                                     n_estimators=50, max_depth=6,\
                                     min_samples_leaf=60)
    
  8. 使用.fit()在训练集上训练模型:

    rf_model.fit(X_train, y_train)
    

    你应该得到以下输出:

    图 9.15: 随机森林模型的日志

    图 9.15: 随机森林模型的日志

  9. 使用.predict()预测训练集和测试集的结果:

    preds_train = rf_model.predict(X_train)
    preds_test = rf_model.predict(X_test)
    
  10. 计算训练集的均方误差并打印其值:

    train_mse = mean_squared_error(y_train, preds_train)
    train_mse
    

    你应该得到以下输出:

    图 9.16: 训练集的均方误差得分

    图 9.16: 训练集的均方误差得分

    我们在训练集上取得了一个相当低的均方误差得分。

  11. 计算测试集上的均方误差并打印其值:

    test_mse = mean_squared_error(y_test, preds_test)
    test_mse
    

    你应该得到以下输出:

    图 9.17: 测试集的均方误差得分

    图 9.17: 测试集的均方误差得分

    我们在测试集上也得到了一个很低的均方误差得分,这个得分与训练集的得分非常相似。所以我们的模型并没有发生过拟合。

  12. 使用.feature_importances_打印变量重要性:

    rf_model.feature_importances_
    

    你应该得到以下输出:

    图 9.18: 测试集的均方误差得分

    图 9.18: 测试集的均方误差得分

  13. 创建一个名为 varimp_df 的空 DataFrame:

    varimp_df = pd.DataFrame()
    
  14. 为此 DataFrame 创建一个名为 feature 的新列,并使用 .columns 获取 df 的列名:

    varimp_df['feature'] = df.columns
    varimp_df['importance'] = rf_model.feature_importances_
    
  15. 打印 varimp_df 的前五行:

    varimp_df.head()
    

    你应该得到以下输出:

    图 9.19:前五个变量的重要性

    图 9.19:前五个变量的重要性

    从此输出中,我们可以看到变量 a1cya1sy 具有最高值,因此它们对预测目标变量比这里显示的其他三个变量更为重要。

  16. 使用 Altair 绘制条形图,coef_dfimportancex 轴,featurey 轴:

    alt.Chart(varimp_df).mark_bar().encode(x='importance',\
                                           y="feature")
    

    你应该得到以下输出:

    图 9.20:显示前五个变量重要性的图表

图 9.20:显示前五个变量重要性的图表

从此输出中,我们可以看到对于这个 Random Forest 模型,最能影响预测的变量按重要性递减的顺序是 a2popa1popa3popb1efftemp

注意

要访问此特定章节的源代码,请参考 packt.live/327Pi0i

本节目前没有在线交互示例,但可以像往常一样在 Google Colab 上运行。

在本练习中,你学习了如何提取 Random Forest 模型所学到的特征重要性,并确定哪些变量对其预测最为重要。

通过置换评估变量的重要性

在上一节中,我们看到如何提取 Random Forest 的特征重要性。实际上,还有另一种技术也称为特征重要性置换,但它的基本逻辑不同,适用于任何算法,而不仅限于树模型。

该技术可称为通过置换评估变量的重要性。假设我们训练了一个模型来预测一个有五个类别的目标变量,并且获得了 0.95 的准确率。评估某个特征的重要性的一种方法是移除该特征并重新训练模型,查看新的准确率。如果准确率显著下降,那么我们可以推断该变量对预测有重要影响。另一方面,如果准确率略微下降或保持不变,那么我们可以认为该变量并不重要,对最终预测的影响不大。因此,我们可以通过模型性能的变化来评估变量的重要性。

这种方法的缺点是需要为每个变量重新训练一个新模型。如果你花了几个小时训练了原始模型,而你有 100 个不同的特征,那么计算每个变量的重要性将花费相当长的时间。如果我们不必重新训练不同的模型,那将是非常棒的。因此,另一种解决方案是生成噪声或给定列的新值,从这些修改过的数据中预测最终结果并比较准确度分数。例如,如果你有一个值在 0 到 100 之间的列,你可以取原始数据并随机生成该列的新值(保持其他变量不变),然后预测它们的类别。

这个选项也有一个问题。随机生成的值可能与原始数据非常不同。回到我们之前看到的同一个例子,如果一个列的原始值范围在 0 到 100 之间,而我们生成的值可能是负数或者非常高的值,这就不太能代表原始数据的真实分布。因此,我们在生成新值之前,需要理解每个变量的分布情况。

与生成随机值不同,我们可以简单地交换(或置换)不同行之间某一列的值,并使用这些修改后的案例进行预测。然后,我们可以计算相关的准确度分数,并将其与原始分数进行比较,以评估该变量的重要性。例如,我们在原始数据集中有以下几行:

图 9.21:数据集的示例

图 9.21:数据集的示例

我们可以交换 X1 列的值并得到一个新的数据集:

图 9.22:数据集中交换列的示例

图 9.22:数据集中交换列的示例

mlxtend包提供了一个函数来执行变量置换并计算变量重要性值:feature_importance_permutation。让我们看看如何使用它与sklearn中的乳腺癌数据集。

首先,让我们加载数据并训练一个随机森林模型:

from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier

data = load_breast_cancer()
X, y = data.data, data.target
rf_model = RandomForestClassifier(random_state=168)
rf_model.fit(X, y)

然后,我们将调用mlxtend.evaluate中的feature_importance_permutation函数。该函数接受以下参数:

  • predict_method:用于模型预测的函数。在这里,我们将提供训练好的rf_model模型的predict方法。

  • X:数据集中的特征。它需要是 NumPy 数组形式。

  • y:数据集中的目标变量。它需要是Numpy数组形式。

  • metric:用于比较模型性能的度量标准。对于分类任务,我们将使用准确率。

  • num_roundmlxtend将对数据执行置换操作并评估性能变化的轮次。

  • seed:用于获取可重复结果的种子。

考虑以下代码片段:

from mlxtend.evaluate import feature_importance_permutation
imp_vals, _ = feature_importance_permutation\
              (predict_method=rf_model.predict, X=X, y=y, \
               metric='r2', num_rounds=1, seed=2)
imp_vals

输出应如下所示:

图 9.23:通过置换获得的变量重要性

图 9.23:通过置换计算的变量重要性

让我们创建一个包含这些值和特征名称的 DataFrame,并使用altair在图表上绘制它们:

import pandas as pd
varimp_df = pd.DataFrame()
varimp_df['feature'] = data.feature_names
varimp_df['importance'] = imp_vals
varimp_df.head()
import altair as alt
alt.Chart(varimp_df).mark_bar().encode(x='importance',\
                                       y="feature")

输出应如下所示:

图 9.24:通过置换显示的变量重要性图

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_24.jpg)

图 9.24:通过置换显示的变量重要性图

这些结果与我们在前一节中从RandomForest得到的结果不同。在这里,最重要的是最差的凹点,其次是最差的面积,最差的周长的值高于平均半径。因此,我们得到了相同的最重要变量列表,但顺序不同。这验证了这三个特征确实是预测肿瘤是否恶性的最重要特征。RandomForest的变量重要性和置换方法有不同的逻辑,因此,当你运行前一节中给出的代码时,可能会得到不同的输出。

练习 9.03:通过置换提取特征重要性

在本练习中,我们将计算并提取通过置换训练的随机森林分类器模型的特征重要性,以预测客户流失率。

我们将使用与前一节相同的数据集。

以下步骤将帮助你完成练习:

  1. 打开一个新的 Colab 笔记本。

  2. 导入以下包:pandastrain_test_split来自sklearn.model_selectionRandomForestRegressor来自sklearn.ensemblefeature_importance_permutation来自mlxtend.evaluate,以及altair

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestRegressor
    from mlxtend.evaluate import feature_importance_permutation
    import altair as alt
    
  3. 创建一个名为file_url的变量,包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter09/Dataset/phpYYZ4Qc.csv'
    
  4. 使用.read_csv()将数据集加载到一个名为df的 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用.pop()提取rej列,并将其保存到一个名为y的变量中:

    y = df.pop('rej')
    
  6. 使用train_test_split()将 DataFrame 拆分为训练集和测试集,test_size=0.3,并设置random_state=1

    X_train, X_test, y_train, y_test = train_test_split\
                                       (df, y, test_size=0.3, \
                                        random_state=1)
    
  7. 实例化RandomForestRegressor,设置random_state=1n_estimators=50max_depth=6min_samples_leaf=60

    rf_model = RandomForestRegressor(random_state=1, \
                                     n_estimators=50, max_depth=6, \
                                     min_samples_leaf=60)
    
  8. 使用.fit()在训练集上训练模型:

    rf_model.fit(X_train, y_train)
    

    你应得到以下输出:

    图 9.25:随机森林日志

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_25.jpg)

    图 9.25:随机森林日志

  9. 使用mlxtend中的feature_importance_permutation通过置换提取特征重要性,采用随机森林模型、测试集、r2作为使用的指标,num_rounds=1,并设置seed=2。将结果保存到一个名为imp_vals的变量中,并打印其值:

    imp_vals, _ = feature_importance_permutation\
                  (predict_method=rf_model.predict, \
                   X=X_test.values, y=y_test.values, \
                   metric='r2', num_rounds=1, seed=2)
    imp_vals
    

    你应得到以下输出:

    图 9.26:通过置换计算的变量重要性

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_26.jpg)

    图 9.26:通过置换计算的变量重要性

    原始结果相当难以解读。让我们通过在图表上置换模型来绘制变量重要性。

  10. 创建一个名为varimp_df的 DataFrame,包含两列:feature,其中包含df的列名,使用.columns,以及importance,其中包含imp_vals的值:

    varimp_df = pd.DataFrame({'feature': df.columns, \
                              'importance': imp_vals})
    
  11. 使用 Altair 绘制条形图,coef_dfimportance 作为 x 轴,feature 作为 y 轴:

    alt.Chart(varimp_df).mark_bar().encode(x='importance',\
                                           y="feature")
    

    你应该得到以下输出:

    图 9.27:通过置换显示变量重要性的图表

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_27.jpg)

图 9.27:通过置换显示变量重要性的图表

从这个输出结果中,我们可以看到,对这个随机森林模型预测影响最大的变量是:a2popa1popa3popb1efftemp,按重要性递减排序。这与练习 9.02中提到的提取随机森林特征重要性结果非常相似。

注意

要访问此特定部分的源代码,请参考 packt.live/2YdstY9

本节目前没有在线交互示例,但可以像往常一样在 Google Colab 上运行。

你成功地通过置换这个模型提取了特征重要性,并识别出哪些变量对预测最为重要。

部分依赖图

另一种与模型无关的工具是部分依赖图。它是一个用于分析特征对目标变量影响的可视化工具。为了实现这一点,我们可以将我们感兴趣的特征值绘制在 x 轴上,将目标变量绘制在 y 轴上,然后将数据集中的所有观测结果显示在这个图表上。让我们尝试一下在 sklearn 的乳腺癌数据集上进行分析:

from sklearn.datasets import load_breast_cancer
import pandas as pd
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target

现在我们已经加载数据并将其转换为 DataFrame,让我们看看最差凹点列:

import altair as alt
alt.Chart(df).mark_circle(size=60)\
             .encode(x='worst concave points', y='target')

生成的图表如下所示:

图 9.28:最差凹点和目标变量的散点图

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_28.jpg)

图 9.28:最差凹点和目标变量的散点图

注意

上述代码和图形只是示例。我们鼓励你通过改变代码中 xy 的值来分析不同的特征。例如,你可以通过将 x='worst concavity'y='worst perimeter' 设置在上述代码中,可能分析最差凹度与最差周长之间的关系。

从这个图中,我们可以看到:

  • 目标变量为 1 的大多数情况,其最差凹点的值低于 0.16。

  • 目标值为 0 的情况,其最差凹点的值超过 0.08。

通过这个图,我们不太确定在最差凹点的值介于 0.08 和 0.16 之间时,会得到哪种结果(0 或 1)。该范围内观测结果不确定的原因可能有多种,比如在该范围内的记录较少,或者其他变量可能会影响这些情况的结果。这就是部分依赖图可以帮助的地方。

逻辑与通过置换计算变量重要性非常相似,不同的是,我们不是随机替换列中的值,而是测试该列中所有可能的值对所有观测值的预测结果。如果我们以图 9.21 的例子为例,原本的三条观测数据,使用这种方法会生成六条新记录,保持 X2X3 列不变,而替换 X1 的值:

图 9.29:从部分依赖图生成的记录示例

图 9.29:从部分依赖图生成的记录示例

通过这组新数据,我们可以看到,例如,值 12 是否真的对预测目标变量为 1 有强烈影响。原始记录中,X1 列的值为 42 和 1 时,预测结果为 0,只有值 12 生成了预测为 1。但如果我们取相同的观测数据,X1 等于 42 和 1,然后将该值替换为 12,我们就可以看到新的预测是否会将目标变量预测为 1。这正是部分依赖图背后的逻辑,它会评估列的所有排列组合,并绘制预测值的平均值。

sklearn 提供了一个名为 plot_partial_dependence() 的函数,用于显示给定特征的部分依赖图。我们来看看如何在乳腺癌数据集上使用它。首先,我们需要获取我们感兴趣列的索引。我们将使用 pandas.get_loc() 方法来获取 最差凹点 列的索引:

import altair as alt
from sklearn.inspection import plot_partial_dependence
feature_index = df.columns.get_loc("worst concave points")

现在我们可以调用 plot_partial_dependence() 函数。我们需要提供以下参数:训练好的模型、训练集以及要分析的特征索引:

plot_partial_dependence(rf_model, df, \
                        features=[feature_index])

图 9.30:最差凹点列的部分依赖图

图 9.30:最差凹点列的部分依赖图

这个部分依赖图展示了,平均而言,所有“最差凹点”列值小于 0.17 的观测值很可能会预测目标为 1(概率超过 0.5),而所有大于 0.17 的记录则预测为 0(概率低于 0.5)。

练习 9.04:绘制部分依赖图

在这个练习中,我们将为两个变量a1poptemp绘制部分依赖图,这些变量来自一个训练好的随机森林分类器模型,用于预测客户流失率。

我们将使用与上一个练习相同的数据集。

  1. 打开一个新的 Colab 笔记本。

  2. 导入以下包:pandastrain_test_split(来自 sklearn.model_selection)、RandomForestRegressor(来自 sklearn.ensemble)、plot_partial_dependence(来自 sklearn.inspection)和 altair

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestRegressor
    from sklearn.inspection import plot_partial_dependence
    import altair as alt
    
  3. 创建一个名为 file_url 的变量,包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter09/Dataset/phpYYZ4Qc.csv'
    
  4. 使用 .read_csv() 将数据集加载到名为 df 的 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用 .pop() 提取 rej 列,并将其保存到一个名为 y 的变量中:

    y = df.pop('rej')
    
  6. 使用train_test_split()将数据框分割为训练集和测试集,test_size=0.3random_state=1

    X_train, X_test, y_train, y_test = train_test_split\
                                       (df, y, test_size=0.3, \
                                        random_state=1)
    
  7. 使用random_state=1n_estimators=50max_depth=6min_samples_leaf=60实例化RandomForestRegressor

    rf_model = RandomForestRegressor(random_state=1, \
                                     n_estimators=50, max_depth=6,\
                                     min_samples_leaf=60)
    
  8. 使用.fit()在训练集上训练模型:

    rf_model.fit(X_train, y_train)
    

    你应该得到如下输出:

    图 9.31:随机森林日志

    图 9.31:随机森林日志

  9. 使用来自sklearnplot_partial_dependence(),结合随机森林模型、测试集和a1pop列的索引,绘制部分依赖图:

    plot_partial_dependence(rf_model, X_test, \
                            features=[df.columns.get_loc('a1pop')])
    

    你应该得到如下输出:

    图 9.32:a1pop 的部分依赖图

    图 9.32:a1pop 的部分依赖图

    这个部分依赖图显示,平均而言,当a1pop变量的值低于 2 时,它对目标变量的影响不大,但从这个值开始,目标变量会随着a1pop每增加一个单位而线性增加 0.04。这意味着,如果区域 1 的人口规模低于 2,流失风险几乎为零。但超过这个临界值后,区域 1 每增加一个人口单位,流失的机会将增加4%

  10. 使用来自sklearnplot_partial_dependence(),结合随机森林模型、测试集和temp列的索引,绘制部分依赖图:

    plot_partial_dependence(rf_model, X_test, \
                            features=[df.columns.get_loc('temp')])
    

    你应该得到如下输出:

    图 9.33:temp 的部分依赖图

图 9.33:temp 的部分依赖图

这个部分依赖图显示,平均而言,temp变量对目标变量有负线性影响:当temp增加 1 时,目标变量将减少 0.12。这意味着,如果温度升高 1 度,离开队列的机会将减少 12%。

注意

要访问该特定部分的源代码,请参考packt.live/2DWnSmn

你也可以在packt.live/2DWnUL1上在线运行这个示例。

你已经学会了如何绘制并分析a1poptemp特征的部分依赖图。在本次练习中,我们看到a1pop对目标变量有正向线性影响,而temp则有负向线性影响。

使用 LIME 进行局部解释

训练完我们的模型后,我们通常会用它来预测未见数据的结果。我们之前看到的全局解释方法,如模型系数、变量重要性和部分依赖图,提供了关于特征的整体信息。有时候,我们希望了解在特定情况下是什么因素影响了模型的预测结果。例如,如果你的模型是用来评估为新客户提供信用的风险,你可能想了解为什么它拒绝了某个特定客户的申请。这就是局部解释的作用:分析单个观测值并理解模型决策背后的逻辑。在本节中,我们将向你介绍一种名为局部可解释模型无关解释LIME)的技术。

如果我们使用线性模型,那么理解每个变量对预测结果的贡献是非常简单的。我们只需要查看模型的系数。例如,模型将学习如下的函数:y = 100 + 0.2 * x1 + 200 * x2 - 180 * x3。假设我们收到一个观测值:x1=0,x2=2,x3=1,我们就可以知道:

  • x1 的贡献是 0.2 * 0 = 0

  • x2 的贡献是 200 * 2 = +400

  • x3 的贡献是-180 * 1 = -180

所以,最终的预测结果(100 + 0 + 400 - 180 = 320)主要是由 x2 驱动的。

但是对于一个非线性模型来说,要获得如此清晰的视图是相当困难的。LIME 是一种在这种情况下提高可见性的方式。LIME 的基本逻辑是用线性模型来逼近原始的非线性模型。然后,它使用该线性模型的系数来解释每个变量的贡献,正如我们在前面的例子中所看到的那样。但 LIME 并不是尝试为整个数据集逼近整个模型,而是尝试在你感兴趣的观测值周围进行局部逼近。LIME 使用训练好的模型来预测靠近你观测值的新数据点,然后在这些预测数据上拟合一个线性回归模型。

让我们看看如何在乳腺癌数据集上使用它。首先,我们将加载数据并训练一个随机森林模型:

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split\
                                   (X, y, test_size=0.3, \
                                    random_state=1)
rf_model = RandomForestClassifier(random_state=168)
rf_model.fit(X_train, y_train)

lime包在 Google Colab 上无法直接访问,因此我们需要使用以下命令手动安装它:

!pip install lime

输出结果如下:

图 9.34:LIME 包的安装日志

图 9.34:LIME 包的安装日志

安装完成后,我们将通过提供训练数据、特征名称、要预测的类别名称和任务类型(在本例中是分类)来实例化LimeTabularExplainer类:

from lime.lime_tabular import LimeTabularExplainer
lime_explainer = LimeTabularExplainer\
                 (X_train, feature_names=data.feature_names,\
                 class_names=data.target_names,\
                 mode='classification')

然后,我们将调用.explain_instance()方法,传入我们感兴趣的观测值(这里是测试集中的第二个观测值)和用于预测结果概率的函数(这里是.predict_proba())。最后,我们将调用.show_in_notebook()方法来显示lime的结果:

exp = lime_explainer.explain_instance\
      (X_test[1], rf_model.predict_proba, num_features=10)
exp.show_in_notebook()

输出如下:

图 9.35:LIME 输出

图 9.35:LIME 输出

注意

你的输出可能会略有不同。这是由于 LIME 的随机采样过程所致。

前面的输出包含了大量信息。我们来逐步分析。左侧显示了目标变量两个类别的预测概率。对于此观察,模型认为预测值为恶性的概率为 0.85:

图 9.36:来自 LIME 的预测概率

图 9.36:来自 LIME 的预测概率

右侧显示了该观察的每个特征的值。每个特征都用颜色编码,以突出其对目标变量可能类别的贡献。列表按特征重要性递减排序。在这个例子中,mean perimetermean areaarea error 特征对模型的贡献使得预测类别 1 的概率增加。所有其他特征则促使模型预测结果为 0:

图 9.37:感兴趣观察的特征值

图 9.37:感兴趣观察的特征值

最后,中央部分显示了每个变量对最终预测的贡献。在这个例子中,worst concave pointsworst compactness 变量分别导致了预测结果 0 的概率分别增加了 0.10 和 0.05。另一方面,mean perimetermean area 两个变量分别对预测类别 1 的概率增加了 0.03:

图 9.38:各特征对最终预测的贡献

图 9.38:各特征对最终预测的贡献

就这么简单。使用 LIME,我们可以轻松查看每个变量如何影响预测模型不同结果的概率。如你所见,LIME 包不仅计算了局部近似,还提供了其结果的可视化表示。比查看原始输出更容易解释。它也非常有助于展示你的发现并说明不同特征如何影响单个观察的预测。

练习 9.05:使用 LIME 进行局部解释

在本练习中,我们将分析一个训练好的随机森林分类模型的预测结果,该模型用于使用 LIME 预测客户流失率。

我们将使用与前一个练习相同的数据集。

  1. 打开一个新的 Colab 笔记本。

  2. 导入以下包:pandastrain_test_split 来自 sklearn.model_selectionRandomForestRegressor 来自 sklearn.ensemble

    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.ensemble import RandomForestRegressor
    
  3. 创建一个名为 file_url 的变量,包含数据集的 URL:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter09/Dataset/phpYYZ4Qc.csv'
    
  4. 使用 .read_csv() 将数据集加载到一个名为 df 的 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用 .pop() 提取 rej 列并将其保存到一个名为 y 的变量中:

    y = df.pop('rej')
    
  6. 使用train_test_split()将 DataFrame 分割为训练集和测试集,test_size=0.3random_state=1

    X_train, X_test, y_train, y_test = train_test_split\
                                       (df, y, test_size=0.3, \
                                        random_state=1)
    
  7. 使用random_state=1n_estimators=50max_depth=6min_samples_leaf=60实例化RandomForestRegressor

    rf_model = RandomForestRegressor(random_state=1, \
                                     n_estimators=50, max_depth=6,\
                                     min_samples_leaf=60)
    
  8. 使用.fit()在训练集上训练模型:

    rf_model.fit(X_train, y_train)
    

    您应该获得以下输出:

    图 9.39:随机森林日志

    图 9.39:随机森林日志

  9. 使用!pip安装命令安装 lime 包:

    !pip install lime
    
  10. lime.lime_tabular导入LimeTabularExplainer

    from lime.lime_tabular import LimeTabularExplainer
    
  11. 使用训练集和mode='regression'实例化LimeTabularExplainer

    lime_explainer = LimeTabularExplainer\
                     (X_train.values, \
                      feature_names=X_train.columns, \
                      mode='regression')
    
  12. 使用.explain_instance().show_in_notebook()显示测试集第一行的 LIME 分析:

    exp = lime_explainer.explain_instance\
          (X_test.values[0], rf_model.predict)
    exp.show_in_notebook()
    

    您应该获得以下输出:

    图 9.40:测试集第一行的 LIME 输出

    图 9.40:测试集第一行的 LIME 输出

    此输出显示,针对该观察值,预测的结果是客户流失的概率为 0.02,这主要受a1popa3popa2popb2eff特征的影响。例如,a1pop低于 0.87 使目标变量的值减少了 0.01。

  13. 使用.explain_instance().show_in_notebook()显示测试集第三行的 LIME 分析:

    exp = lime_explainer.explain_instance\
          (X_test.values[2], rf_model.predict)
    exp.show_in_notebook()
    

    您应该获得以下输出:

    图 9.41:测试集第三行的 LIME 输出

图 9.41:测试集第三行的 LIME 输出

此输出显示,针对该观察值,预测的结果是客户流失的概率为 0.09,这主要受a2popa3popa1popb1eff特征的影响。例如,b1eff低于 0.87 使目标变量的值增加了 0.01。b1eff特征代表银行 1 的效率水平,因此 LIME 的结果告诉我们,如果该效率水平低于 0.87,客户流失的概率就会增加。

注意

要访问此特定部分的源代码,请参考packt.live/2Q5i3Fp

您还可以通过packt.live/327Nl3Z在线运行此示例。

您已完成本章的最后一个练习。您了解了如何使用 LIME 解释单个观察值的预测结果。我们了解到,a1popa2popa3pop特征对训练集的第一行和第三行观察值有很强的负面影响。

活动 9.01:训练并分析网络入侵检测模型

您为一家网络安全公司工作,您的任务是构建一个能够识别网络入侵的模型,然后分析其特征重要性,绘制部分依赖图,并使用 LIME 对单个观察值进行局部解释。

提供的数据集包含 7 周网络流量的数据。

注意

本活动中使用的数据集来自 KDD Cup 1999:

packt.live/2tFKUIV

该数据集的 CSV 版本可以在这里找到:

packt.live/2RyVsBm

以下步骤将帮助你完成此活动:

  1. 使用pandas中的.read_csv()下载并加载数据集。

  2. 使用pandas中的.pop()提取响应变量。

  3. 使用sklearn.model_selection中的train_test_split()将数据集划分为训练集和测试集。

  4. 创建一个函数,使用sklearn.ensemble中的.fit()实例化并拟合RandomForestClassifier

  5. 创建一个函数,使用.predict()来预测训练集和测试集的结果。

  6. 创建一个函数,使用sklearn.metrics中的accuracy_score()来打印训练集和测试集的准确率。

  7. 使用feature_importance_permutation()通过置换计算特征重要性,并使用altair在条形图上展示。

  8. 使用plot_partial_dependence绘制src_bytes变量的部分依赖图。

  9. 使用!pip install安装lime

  10. 对行99893进行 LIME 分析,使用explain_instance()

    输出应如下所示:

    图 9.42:LIME 分析的输出

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_09_42.jpg)

图 9.42:LIME 分析的输出

你已经成功训练了一个随机森林模型来预测网络连接类型。你还分析了哪些特征对这个随机森林模型最为重要,发现它主要依赖于src_bytes特征。我们还分析了该特征的部分依赖图,以了解其对normal类别的影响。最后,我们使用 LIME 分析了一个单独的观测值,找出了导致预测结果的变量。

总结

在本章中,我们学习了一些机器学习模型解释技术。我们看到有些技术是特定于使用的模型的:线性模型的系数和树模型的变量重要性。也有一些方法是模型无关的,例如通过置换计算变量重要性。

所有这些技术都是全局解释器,它们查看整个数据集并分析每个变量对预测的整体贡献。我们可以利用这些信息来识别哪些变量对预测影响最大,并将其筛选出来。与其保留数据集中所有的特征,不如只保留那些影响力较大的特征。这可以显著减少训练模型或计算预测时的计算时间。

我们还通过 LIME 进行了本地解释器场景的分析,该方法分析了单个观察值。它帮助我们更好地理解模型在预测给定案例的最终结果时所做的决策。这是一个非常强大的工具,可以评估模型是否对某些可能包含敏感信息(如个人详细信息或人口统计数据)的变量存在偏向。我们还可以使用它比较两个不同的观察值,并理解模型产生不同结果的原因。

在下一章中,我们将重点分析一个数据集,并学习探索性数据分析和数据可视化技术,以便深入了解其中包含的信息。

第十章:10. 分析数据集

概览

本章结束时,您将能够解释进行探索性数据分析的关键步骤;识别数据集中包含的数据类型;总结数据集并对每个变量进行详细分析;可视化每一列的数据分布;找到变量之间的关系,并分析每个变量的缺失值和异常值。

本章将向您介绍如何进行探索性数据分析并可视化数据,以便识别质量问题、潜在的数据转换以及有趣的模式。

介绍

上一章主要讨论了如何改进我们的机器学习模型,并解释其结果和参数,以便为业务提供有意义的见解。本章开启了本书的第三部分:提升您的数据集。在接下来的三章中,我们将退后一步,专注于任何机器学习模型的关键输入:数据集。我们将学习如何探索一个新的数据集,准备它进入建模阶段,并创建新的变量(也叫做特征工程)。这些都是非常激动人心且重要的主题,所以让我们开始吧。

当我们提到数据科学时,大多数人想到的是构建精密的机器学习算法来预测未来的结果。他们通常不会想到数据科学项目中涉及的所有其他关键任务。实际上,建模步骤只涵盖了这样一个项目的一个小部分。你可能已经听说过一个经验法则,即数据科学家将 20%的时间花在建模上,剩下的 80%时间用于理解和准备数据。这个说法其实很接近现实。

在业界用于开展数据科学项目的一个非常流行的方法论是CRISP-DM

注意

我们不会对这个方法论进行过多的详细介绍,因为它超出了本书的范围。但如果你有兴趣了解更多,可以在这里找到关于 CRISP-DM 的描述:packt.live/2QMRepG

该方法论将数据科学项目分解为六个不同的阶段:

  1. 业务理解

  2. 数据理解

  3. 数据准备

  4. 建模

  5. 评估

  6. 部署

如你所见,建模只占六个阶段中的一个阶段,而且发生在项目的后期。在本章中,我们将主要聚焦于 CRISP-DM 的第二步:数据理解阶段。

你可能会想,为什么理解数据如此重要,为什么我们不应该把更多的时间花在建模上。实际上,一些研究者已经证明,在高质量数据上训练非常简单的模型,效果超过了在差的数据上训练复杂的模型。

如果你的数据不正确,即使是最先进的模型也无法找到相关的模式并预测正确的结果。这就是垃圾进,垃圾出,意味着错误的输入会导致错误的输出。因此,我们需要充分了解数据集的局限性和问题,并在将其应用于模型之前加以修正。

理解输入数据如此重要的第二个原因是,它还将帮助我们定义正确的方法,并相应地筛选出相关的算法。例如,如果你发现在数据集中某个特定类别的样本比其他类别少,那么你可能需要使用能够处理不平衡数据的算法,或者预先使用一些重采样技术来使类别分布更均匀。

本章中,你将学习一些关键概念和技术,以深入理解你的数据。

探索你的数据

如果你按照 CRISP-DM 方法论进行项目管理,第一步将是与相关方讨论项目,并清楚地定义他们的需求和期望。只有当这一点明确后,你才能开始查看数据,并判断是否能够实现这些目标。

在接收到数据集后,你可能想确认数据集是否包含你项目所需的信息。例如,如果你正在进行监督学习项目,你将检查数据集中是否包含你需要的目标变量,以及该字段是否存在缺失值或错误值。你还可以检查数据集包含多少观察值(行)和变量(列)。这些是你在处理新数据集时可能会首先遇到的问题。本节将向你介绍一些可以用来回答这些问题的技术。

本节剩余部分,我们将使用一个包含在线零售店交易的数据库。

注意

该数据集存储在我们的 GitHub 库中:packt.live/36s4XIN

数据集来自packt.live/2Qu5XqC,由 Daqing Chen、Sai Liang Sain 和 Kun Guo 提供,标题为《在线零售行业的数据挖掘》,来源于 UCI 机器学习库。

我们的数据集是一个 Excel 电子表格。幸运的是,pandas包提供了一个方法,可以用来加载这种类型的文件:read_excel()

让我们使用.read_excel()方法读取数据,并将其存储在一个pandas DataFrame 中,如下代码片段所示:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter10/dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

在将数据加载到 DataFrame 后,我们想知道这个数据集的大小,即它的行数和列数。为了获取这些信息,我们只需要调用pandas.shape属性:

df.shape

你应该得到以下输出:

(541909, 8)

这个属性返回一个元组,包含行数作为第一个元素,列数作为第二个元素。加载的数据集包含541909行和8列。

由于这个属性返回的是一个元组,我们可以通过提供相关索引独立访问其每个元素。让我们提取行数(索引0):

df.shape[0]

你应该得到如下输出:

541909

类似地,我们可以通过第二个索引获取列数:

df.shape[1]

你应该得到如下输出:

8

通常,数据集的第一行是表头,包含每一列的名称。默认情况下,read_excel()方法假设文件的第一行是表头。如果header存储在其他行,可以通过read_excel()方法的header参数指定其他的表头行索引,例如pd.read_excel(header=1)用于指定表头在第二行。

一旦加载到pandas数据框中,你可以通过直接调用它来打印出其内容:

df

你应该得到如下输出:

图 10.1: 加载的在线零售数据框的前几行

图 10.1: 加载的在线零售数据框的前几行

要访问这个数据框的列名,我们可以调用.columns属性:

df.columns

你应该得到如下输出:

图 10.2: 在线零售数据框的列名列表

图 10.2: 在线零售数据框的列名列表

该数据集的列包括InvoiceNoStockCodeDescriptionQuantityInvoiceDateUnitPriceCustomerIDCountry。我们可以推测该数据集的每一行代表了一个特定客户在特定日期购买某件商品的销售记录,包括数量和价格。

看着这些列名,我们或许能猜到这些列中包含的是什么类型的信息,但为了确保准确无误,我们可以使用dtypes属性,代码如下所示:

df.dtypes

你应该得到如下输出:

图 10.3: 数据框每列的数据类型描述

图 10.3: 数据框每列的数据类型描述

从这个输出中,我们可以看到InvoiceDate列是日期类型(datetime64[ns]),Quantity是整数类型(int64),UnitPriceCustomerID是小数类型(float64)。其余的列是文本类型(object)。

pandas包提供了一个方法,可以显示我们迄今为止看到的所有信息,这个方法是info()方法:

df.info()

你应该得到如下输出:

图 10.4: 方法的输出

图 10.4: info()方法的输出

仅通过几行代码,我们就能了解有关该数据集的一些高级信息,例如其大小、列名及其类型。

在下一部分,我们将分析数据集的内容。

分析你的数据集

之前,我们了解了数据集的整体结构以及其中包含的信息类型。现在,是时候深入探讨每一列的具体值了。

首先,我们需要导入 pandas 包:

import pandas as pd

然后,我们将数据加载到一个 pandas DataFrame 中:

file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter10/dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

pandas 包提供了多个方法,方便你查看数据集的快照。最常用的有 head()tail()sample()

head() 方法将显示数据集的前几行。默认情况下,pandas 将显示前五行:

df.head()

你应该会看到以下输出:

图 10.5:使用  方法显示前五行

图 10.5:使用 head() 方法显示前五行

head() 方法的输出显示,InvoiceNoStockCodeCustomerID 列是每个采购发票、销售商品和客户的唯一标识符字段。Description 字段是描述所售商品的文本。QuantityUnitPrice 分别表示售出商品的数量和单价。Country 是一个文本字段,用于指定客户或商品的位置,或指定客户在哪个国家版本的在线商店下的订单。在实际项目中,你可以联系提供此数据集的团队,确认 Country 列的含义,或者任何其他你可能需要的列信息。

使用 pandas 时,可以通过在 head() 方法中提供整数参数来指定显示的前几行数量。我们通过显示前 10 行来尝试一下:

df.head(10)

你应该会看到以下输出:

图 10.6:使用  方法显示前 10 行

图 10.6:使用 head() 方法显示前 10 行

从这个输出来看,我们可以假设数据是按 InvoiceDate 列升序排序,并按 CustomerIDInvoiceNo 分组的。在 Country 列中,我们只看到一个值:United Kingdom。让我们通过查看数据集的最后几行来确认这是否属实。这可以通过调用 tail() 方法实现。与 head() 方法类似,默认情况下,该方法将仅显示五行,但你可以通过参数指定要显示的行数。这里,我们将显示最后八行:

df.tail(8)

你应该会看到以下输出:

图 10.7:使用  方法显示最后八行

图 10.7:使用 tail() 方法显示最后八行

看起来我们之前的假设是正确的,数据确实是按 InvoiceDate 列升序排序的。我们也可以确认,Country 列中实际上有不止一个值。

我们还可以使用 sample() 方法,利用 n 参数随机选择指定数量的行。你也可以指定 random_state 参数:

df.sample(n=5, random_state=1)

你应该会看到以下输出:

图 10.8:使用 sample()方法显示五个随机采样的行

图 10.8:使用 sample()方法显示五个随机采样的行

在这个输出中,我们可以看到Country列中有一个额外的值:Germany。我们还可以注意到几个有趣的点:

  • InvoiceNo 也可以包含字母(第94,801行以C开头,这可能有特殊意义)。

  • Quantity 可以有负值:-2(第94801行)。

  • CustomerID包含缺失值:NaN(第210111行)。

练习 10.01:使用描述性统计分析 Ames Housing 数据集

在本次练习中,我们将探索Ames Housing dataset,通过分析其结构并查看其中的一些行来充分理解它。

我们将在这个练习中使用的数据显示集是 Ames Housing 数据集,您可以在我们的 GitHub 仓库中找到它:packt.live/35kRKAo

注意

该数据集由 Dean De Cock 编制。

该数据集包含 2016 年到 2010 年间爱荷华州 Ames 市的住宅房屋销售数据。

更多关于每个变量的信息可以在packt.live/2sT88L4找到。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包:

    import pandas as pd
    
  3. 将 AMES 数据集的链接分配给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter10/dataset/ames_iowa_housing.csv'
    
  4. 使用pandas包中的.read_csv()方法,将数据集加载到一个名为df的新变量中:

    df = pd.read_csv(file_url)
    
  5. 使用pandas包中的shape属性打印 DataFrame 的行数和列数:

    df.shape
    

    你应该得到以下输出:

    (1460, 81)
    

    我们可以看到这个数据集包含了1460行和81个不同的列。

  6. 使用pandas库中的columns属性打印此 DataFrame 包含的变量名:

    df.columns
    

    你应该得到以下输出:

    图 10.9:房屋数据集的列列表

    图 10.9:房屋数据集的列列表

    我们可以通过查看变量名称来推测一些变量所包含的信息类型,例如LotArea(房产大小)、YearBuilt(建造年份)和SalePrice(房产销售价格)。

  7. 使用pandas包中的dtypes属性打印此 DataFrame 包含的每个变量的类型:

    df.dtypes
    

    你应该得到以下输出:

    图 10.10:房屋数据集的列及其类型

    图 10.10:房屋数据集的列及其类型

    我们可以看到,变量要么是数值型的,要么是文本类型的。这个数据集中没有日期列。

  8. 使用pandas库中的head()方法显示 DataFrame 的前几行:

    df.head()
    

    你应该得到以下输出:

    图 10.11:房屋数据集的前五行

    图 10.11:房屋数据集的前五行

  9. 使用pandas中的tail()方法显示 DataFrame 的最后五行:

    df.tail()
    

    你应该得到以下输出:

    图 10.12:房屋数据集的最后五行

    图 10.12:房屋数据集的最后五行

    看起来 Alley(巷道)列有很多缺失值,这些缺失值由 NaN 表示(表示“不是一个数字”)。Street(街道)和 Utilities(公用设施)列似乎只有一个值。

  10. 现在,使用 pandas 中的 sample() 方法显示 5 行随机抽样数据,并为其传入 'random_state' 参数值 8

    df.sample(n=5, random_state=8)
    

    你应该得到以下输出:

    图 10.13:房屋数据集的五个随机抽样行

图 10.13:房屋数据集的五个随机抽样行

通过这些随机样本,我们还可以看到 LotFrontage(地块前沿)列也有一些缺失值。我们还看到这个数据集中包含了数值和文本数据(对象类型)。我们将在 练习 10.02 中分析这些分类变量, 《从 Ames 房屋数据集中分析分类变量》,以及 练习 10.03 中分析数值变量, 《从 Ames 房屋数据集中分析数值变量》

注意

要访问该特定部分的源代码,请参考 packt.live/2E8HMdH

你也可以在线运行这个示例,网址:packt.live/3g62zLT

通过这几行代码,我们已经学到了很多关于这个数据集的内容,比如行数和列数、每个变量的数据类型以及它们的信息。我们还发现了一些缺失值的问题。

分析分类变量的内容

现在我们已经对 在线零售数据集 中包含的信息有了初步了解,我们想深入探讨每一列数据:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob'\
           '/master/Chapter10/dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

例如,我们希望通过调用 nunique() 方法来了解每个变量中包含多少不同的值。这对于一个具有有限值数量的分类变量,像 Country(国家)这种情况特别有用:

df['Country'].nunique()

你应该得到以下输出:

38

我们可以看到,这个数据集中有 38 个不同的国家。如果我们能获得这一列中所有值的列表,那就太好了。幸运的是,pandas 包提供了一个方法来获取这些结果:unique()

df['Country'].unique()

你应该得到以下输出:

图 10.14:'Country'(国家)列的唯一值列表

图 10.14:'Country'(国家)列的唯一值列表

我们可以看到,来自不同大陆的多个国家,但大多数国家来自欧洲。我们还看到有一个值叫做 Unspecified(未指定)和另一个值 European Community(欧洲共同体),这可能是指所有欧元区的国家,这些国家没有单独列出。

pandas中的另一个非常有用的方法是value_counts()。这个方法列出了给定列中的所有值,还包括它们的出现次数。通过提供dropna=Falsenormalize=True参数,该方法将包括缺失值并分别计算出现次数的比率:

df['Country'].value_counts(dropna=False, normalize=True)

你应该得到以下输出:

图 10.15:唯一值及其出现百分比的截断列表对于列

图 10.15:'Country'列唯一值及其出现百分比的截断列表

从输出中,我们可以看到United Kingdom的值在这一列中完全占据主导地位,它代表了超过 91%的行,而AustriaDenmark等其他值则相当稀有,代表了该数据集中不到 1%的行。

练习 10.02:分析 Ames Housing 数据集中的分类变量

在本次练习中,我们将通过分析数据集的分类变量继续进行数据集探索。为此,我们将实现我们自己的describe函数。

我们将在本次练习中使用的数据集是 Ames Housing 数据集,可以在我们的 GitHub 仓库找到:packt.live/35kRKAo。让我们开始吧:

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包:

    import pandas as pd
    
  3. 将以下链接分配给名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter10/dataset/ames_iowa_housing.csv'
    
  4. 使用pandas包中的.read_csv()方法,将数据集加载到一个名为df的新变量中:

    df = pd.read_csv(file_url)
    
  5. 创建一个名为obj_df的新 DataFrame,只包含使用pandas包中的select_dtypes方法选出的数值类型的列。然后,将object值传递给include参数:

    obj_df = df.select_dtypes(include='object')
    
  6. 使用pandas中的columns属性,提取该 DataFrame obj_df的列列表,并将其分配给一个名为obj_cols的新变量,然后打印出其内容:

    obj_cols = obj_df.columns
    obj_cols
    

    你应该得到以下输出:

    图 10.16:分类变量列表

    图 10.16:分类变量列表

  7. 创建一个名为describe_object的函数,该函数接受一个pandas DataFrame 和一个列名作为输入参数。然后,在函数内部,打印出给定列的名称、使用nunique()方法计算的唯一值数量,以及使用value_counts()方法列出的值及其出现次数,代码示例如下:

    def describe_object(df, col_name):
        print(f"\nCOLUMN: {col_name}")
        print(f"{df[col_name].nunique()} different values")
        print(f"List of values:")
        print(df[col_name].value_counts\
                           (dropna=False, normalize=True))
    
  8. 通过提供df DataFrame 和'MSZoning'列来测试这个函数:

    describe_object(df, 'MSZoning')
    

    你应该得到以下输出:

    图 10.17:展示为 MSZoning 列创建的函数

    图 10.17:展示为 MSZoning 列创建的函数

    对于MSZoning列,RL值几乎代表了79%的值,而C(全部)仅出现在不到1%的行中。

  9. 创建一个for循环,调用为obj_cols列表中的每个元素创建的函数:

    for col_name in obj_cols:
        describe_object(df, col_name)
    

    你应该得到以下输出:

    图 10.18: 展示在 obj_cols 中创建的函数的第一个列

图 10.18: 展示在 obj_cols 中创建的函数的第一个列

前一步的输出在图 10.18中被截断了。我们可以确认Street列几乎是常数,因为 99.6%的行包含相同的值:Pave。对于Alley列,几乎 94%的行包含缺失值。

注意

若要访问此部分的源代码,请参见packt.live/3kWoRn2

你也可以在线运行这个示例,链接:packt.live/3gbPnp0

我们刚刚分析了数据集中所有的类别变量。我们展示了如何查看任何特征中所有值的分布。我们还发现其中一些特征由单一值主导,而其他特征则主要包含缺失值。

总结数值变量

现在,让我们来看看一个数值列,深入了解它的内容。我们将使用一些统计度量来总结一个变量。所有这些度量被称为描述性统计。在本章中,我们将介绍一些最常见的度量方法。

使用pandas包,许多这些度量已经作为方法实现。例如,如果我们想知道'数量'列中包含的最大值是什么,我们可以使用.max()方法:

df['Quantity'].max()

你应该得到以下输出:

80995

我们可以看到,在这个数据集中,某个商品的最大销售数量是80995,这对于零售业务来说似乎极高。在实际项目中,这种意外的值需要与数据所有者或关键利益相关者讨论并确认,看看这是有效值还是错误值。现在,让我们使用.min()方法查看'数量'的最小值:

df['Quantity'].min()

你应该得到以下输出:

-80995

该变量的最小值极低。我们可以认为退货商品的负值是可能的,但这里的最小值(-80995)非常低。这同样需要与你组织中的相关人员确认。

现在,我们将看看此列的集中趋势。集中趋势是一个统计学术语,指的是数据将围绕其聚集的中心点。最著名的集中趋势度量是平均数(或均值)。平均数是通过将列中的所有值相加并除以值的数量来计算的。

如果我们将数量列绘制在带有平均值的图表上,它将如下所示:

图 10.19: '数量'列的平均值

图 10.19: '数量'列的平均值

我们可以看到数量列的平均值非常接近 0,且大多数数据位于-50+50之间。

我们可以通过使用pandas中的mean()方法来获得某个特征的平均值:

df['Quantity'].mean()

你应该得到以下输出:

9.55224954743324

在这个数据集中,销售的平均数量约为9.55。平均值对异常值非常敏感,正如我们之前看到的,Quantity列的最小值和最大值非常极端(-80995 到 +80995)。

我们也可以使用中位数作为另一种集中趋势的度量。中位数的计算方法是将列分成两个长度相等的组,然后通过分隔这两组来获得中间点的值,如以下示例所示:

图 10.20:示例中位数

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_10_21.jpg)

图 10.20:示例中位数

pandas中,你可以调用median()方法来获得这个值:

df['Quantity'].median()

你应该得到以下输出:

3.0

这个列的中位数是 3,和我们之前找到的平均数(9.55)差别很大。这告诉我们这个数据集中有一些异常值,等我们进一步调查后,需要决定如何处理它们(这将在第十一章数据准备中讲解)。

我们还可以评估这一列的分布情况(数据点与中心点的差异)。一个常见的分布度量是标准差。这个度量越小,数据越接近其平均值。相反,如果标准差很高,意味着有一些观测值远离平均值。我们将使用pandas中的std()方法来计算这个度量:

df['Quantity'].std()

你应该得到以下输出:

218.08115784986612

如预期的那样,这一列的标准差相当高,因此数据点分布较广,离平均值9.55相差较大。

pandas包中,有一个方法可以通过一行代码显示大部分描述性统计数据:describe()

df.describe()

你应该得到以下输出:

图 10.21:方法的输出

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_10_21.jpg)

图 10.21:describe()方法的输出

我们得到了与之前相同的Quantity列值。这个方法已经计算了三个数值列(QuantityUnitPriceCustomerID)的描述性统计信息。

虽然CustomerID列仅包含数值数据,但我们知道这些值用于标识每个客户,并没有数学意义。例如,表格中将客户 ID 12680 到 17850 相加或计算这些标识符的平均值是没有意义的。这个列实际上不是数值型,而是类别型。

describe()方法不知道这些信息,只注意到有数字,因此假设这是一个数值变量。这正是为什么你应该完全理解你的数据集,并在将数据输入算法之前识别需要修复的问题的完美例子。在这种情况下,我们将需要将该列的类型更改为分类类型。在第十一章数据准备中,我们将看到如何处理此类问题,但现在,我们将看看一些图形工具和技术,这些工具将帮助我们更好地理解数据。

练习 10.03:分析 Ames Housing 数据集中的数值变量

在本练习中,我们将通过分析此数据集的数值变量继续我们的数据集探索。为此,我们将实现我们自己的describe函数。

我们将在本练习中使用的数据集是 Ames Housing 数据集,可以在我们的 GitHub 仓库找到:packt.live/35kRKAo。让我们开始吧:

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包:

    import pandas as pd
    
  3. 将 AMES 数据集的链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter10/dataset/ames_iowa_housing.csv'
    
  4. 使用pandas包中的.read_csv()方法,将数据集加载到一个名为df的新变量中:

    df = pd.read_csv(file_url)
    
  5. 创建一个新的 DataFrame,名为num_df,仅包含数值型的列,使用pandas包中的select_dtypes方法,并将'number'值传递给include参数:

    num_df = df.select_dtypes(include='number')
    
  6. 使用pandas中的columns属性,提取此 DataFrame (num_df) 的列列表,将其赋值给一个名为num_cols的新变量,并打印其内容:

    num_cols = num_df.columns
    num_cols
    

    你应该得到以下输出:

    图 10.22:数值列列表

    图 10.22:数值列列表

  7. 创建一个名为describe_numeric的函数,该函数接受一个pandas DataFrame 和一个列名作为输入参数。然后,在函数内部,打印给定列的名称、使用min()的最小值、使用max()的最大值、使用mean()的平均值、使用std()的标准差和median()的中位数:

    def describe_numeric(df, col_name):
        print(f"\nCOLUMN: {col_name}")
        print(f"Minimum: {df[col_name].min()}")
        print(f"Maximum: {df[col_name].max()}")
        print(f"Average: {df[col_name].mean()}")
        print(f"Standard Deviation: {df[col_name].std()}")
        print(f"Median: {df[col_name].median()}")
    
  8. 现在,通过提供df DataFrame 和SalePrice列来测试此函数:

    describe_numeric(df, 'SalePrice')
    

    你应该得到以下输出:

    图 10.23:为'SalePrice'列创建的函数显示

    图 10.23:为'SalePrice'列创建的函数显示

    销售价格范围从34,900755,000,平均值为180,921。中位数略低于平均值,这告诉我们存在一些高销售价格的异常值。

  9. 创建一个for循环,调用为num_cols列表中的每个元素创建的函数:

    for col_name in num_cols:
        describe_numeric(df, col_name)
    

    你应该得到以下输出:

    图 10.24:显示为前几列创建的函数    包含在'num_cols'中

图 10.24:显示为'num_cols'中包含的前几列创建的函数

图 10.25 显示了截断的输出。Id 列的范围从 11460,这正是该数据集行数的准确值。这意味着该列肯定是已售财产的唯一标识符。看起来 MSSubClass 的值都是四舍五入的,这可能表明该列中的信息已被分成了 10 为一组的类别变量。

注意

要访问这一特定部分的源代码,请参考:packt.live/2Q2TJEc

你也可以在线运行此示例,网址:packt.live/2Yez18U

我们看到如何通过几行代码探索一个新收到的数据集。这帮助我们理解其结构、每个变量中包含的信息类型,并帮助我们识别一些潜在的数据质量问题,如缺失值或不正确的值。

可视化你的数据

在上一节中,我们看到了如何探索一个新的数据集并计算一些简单的描述性统计数据。这些度量帮助我们将数据集总结成可解释的度量,例如平均值或最大值。现在是时候更深入地分析每一列数据,利用数据可视化获得更细致的视图。

在数据科学项目中,数据可视化可以用于数据分析或传达获得的见解。以一种利益相关者可以轻松理解和解释的方式展示结果,绝对是任何优秀数据科学家的必备技能。

然而,在本章中,我们将重点介绍使用数据可视化来分析数据。与阅读书面信息相比,大多数人倾向于更容易通过图表来解释信息。例如,在查看以下描述性统计数据和同一变量的散点图时,哪一种你认为更容易理解?我们来看看:

图 10.25:示例可视化数据分析

图 10.25:示例可视化数据分析

尽管通过描述性统计显示的信息更加详细,但通过查看图表,你已经能够看到数据被拉伸并且主要集中在 0 值附近。你可能用了不到 1 到 2 秒的时间就得出了这个结论:数据点聚集在 0 值附近,且随着远离该值而减少。如果你是在解读描述性统计数据,得出这个结论可能会花费更多时间。这就是为什么数据可视化是分析数据的一个非常强大的工具。

使用 Altair API

我们将使用一个名为altair的包(如果你还记得,我们在第五章《执行你的第一次聚类分析》中已经简要使用过它)。市面上有很多用于数据可视化的 Python 包,比如matplotlibseabornBokeh,与它们相比,altair相对较新,但由于其简单的 API 语法,用户社区正在快速增长。

让我们看看如何逐步在在线零售数据集上显示条形图。

首先,导入pandasaltair包:

import pandas as pd
import altair as alt

然后,将数据加载到pandas的 DataFrame 中:

file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter10/dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

我们将随机抽取 5,000 行数据使用sample()方法(altair需要额外的步骤来显示更大的数据集):

sample_df = df.sample(n=5000, random_state=8)

现在,从altair实例化一个Chart对象,并将pandas的 DataFrame 作为输入参数:

base = alt.Chart(sample_df)

接下来,我们调用mark_circle()方法来指定我们想绘制的图表类型:散点图:

chart = base.mark_circle()

最后,我们使用encode()方法指定将在x轴和y轴上显示的列名称:

chart.encode(x='Quantity', y='UnitPrice')

我们仅用七行代码就绘制了一个散点图:

图 10.26:散点图输出

图 10.26:散点图输出

Altair 提供了将其方法组合成一行代码的选项,像这样:

alt.Chart(sample_df).mark_circle()\
   .encode(x='Quantity', y='UnitPrice')

你应该得到以下输出:

图 10.27:结合 altair 方法的散点图输出

图 10.27:结合 altair 方法的散点图输出

我们可以看到,输出和之前完全相同。这个图表显示了两个变量都有很多离群值(极端值):UnitPrice的大多数值都在 100 以下,但也有超过 300 的值,Quantity的范围从-200 到 800,而大部分观测值都在-50 到 150 之间。我们还可以注意到一个模式,单位价格高的物品其数量较低(单位价格超过 50 的物品数量接近 0),反之亦然(数量超过 100 的物品单位价格接近 0)。

现在,假设我们想在同一个图表中展示Country列的信息。实现这一点的一个简单方法是使用encode()方法中的color参数。这样就可以根据Country列的值为所有数据点上色:

alt.Chart(sample_df).mark_circle()\
   .encode(x='Quantity', y='UnitPrice', color='Country')

你应该得到以下输出:

图 10.28:基于“Country”列颜色的散点图

图 10.28:基于“Country”列颜色的散点图

我们将Country列的信息加入到图表中,但正如我们所见,值太多了,难以区分不同的国家:有很多蓝色的点,但很难判断它们代表的是哪个国家。

使用altair,我们可以轻松地在图表上添加一些交互,以便为每个观测点显示更多的信息;我们只需使用encode()方法中的tooltip参数,并指定要显示的列列表,然后调用interactive()方法使整个图表变得可交互(如在第五章执行第一次聚类分析中所见):

alt.Chart(sample_df).mark_circle()\
   .encode(x='Quantity', y='UnitPrice', color='Country', \
           tooltip=['InvoiceNo','StockCode','Description',\
                    'InvoiceDate','CustomerID']).interactive()

你应该得到以下输出:

图 10.29:带有工具提示的交互式散点图

图 10.29:带有工具提示的交互式散点图

现在,如果我们将鼠标悬停在UnitPrice值最高的观测点(即接近 600 的那个),我们可以看到工具提示显示的信息:这个观测点没有StockCode的值,它的DescriptionManual。因此,看起来这不是网站上发生的正常交易。这可能是一个手动输入到系统中的特殊订单。这个问题你需要与相关方讨论并确认。

数值型变量的直方图

现在我们已经熟悉了altair API,让我们来看看一些特定类型的图表,这些图表将帮助我们分析并理解每个变量。首先,我们关注在线零售数据集中的数值型变量,如UnitPriceQuantity

对于这种类型的变量,通常使用直方图来展示给定变量的分布情况。直方图的 x 轴显示此列中的可能值,y 轴绘制落在每个值下的观测数量。由于数值型变量的可能值非常多(可能是无限多个潜在值),因此最好将这些值分组为若干个区间(也叫做箱体)。例如,我们可以将价格分为 10 步的区间(即每组 10 个项目),比如 0 到 10,11 到 20,21 到 30,依此类推。

让我们通过实际的例子来看这个问题。我们将使用mark_bar()encode()方法,结合以下参数来绘制'UnitPrice'的直方图:

  • alt.X("UnitPrice:Q", bin=True):这是另一个altair API 语法,允许你调整 x 轴的一些参数。在这里,我们告诉altair使用'UnitPrice'列作为轴。':Q'表示这一列是定量数据(即数值型),而bin=True强制将可能的值分组为箱体。

  • y='count()':用于计算观测数量并将其绘制在 y 轴上,如下所示:

alt.Chart(sample_df).mark_bar()\
   .encode(alt.X("UnitPrice:Q", bin=True), \
           y='count()')

你应该得到以下输出:

图 10.30:带有默认箱体步长的 UnitPrice 直方图

图 10.30:带有默认箱体步长的 UnitPrice 直方图

默认情况下,altair将观测值按 100 步的区间进行分组:从 0 到 100,然后是 100 到 200,以此类推。选择的步长并不理想,因为几乎所有的观测值都落在第一个区间(0 到 100)内,我们看不到其他的区间。使用altair时,我们可以指定bin参数的值,我们将尝试使用步长为 5,即alt.Bin(step=5)

alt.Chart(sample_df).mark_bar()\
   .encode(alt.X("UnitPrice:Q", bin=alt.Bin(step=5)), \
           y='count()')

你应该得到如下输出:

图 10.31:单价的直方图,步长为 5

图 10.31:单价的直方图,步长为 5

这样好多了。通过这个步长,我们可以看到大多数观测值的单价低于 5(几乎 4,200 个观测值)。我们还可以看到有 500 多个数据点的单价低于 10。随着单价的增加,记录的数量逐渐减少。

现在,让我们绘制Quantity列的直方图,步长为 10:

alt.Chart(sample_df).mark_bar()\
   .encode(alt.X("Quantity:Q", bin=alt.Bin(step=10)), \
           y='count()')

你应该得到如下输出:

图 10.32:数量的直方图,步长为 10

图 10.32:数量的直方图,步长为 10

在这个直方图中,大多数记录的数量在 0 到 30 之间(前几个最高的区间)。还有一个区间约有 50 个观测值,它们的数量在-10 到 0 之间。如前所述,这些可能是客户退货的商品。

类别变量的条形图

现在,我们将来看一下类别变量。对于这类变量,不需要将值分组为区间,因为它们的潜在值数量有限。我们仍然可以通过简单的条形图绘制这些列的分布。在altair中,这非常简单——它与绘制直方图类似,但没有bin参数。我们将尝试在Country列上进行这个操作,查看每个值的记录数量:

alt.Chart(sample_df).mark_bar()\
   .encode(x='Country',y='count()')

你应该得到如下输出:

图 10.33:国家列的出现次数的条形图

图 10.33:国家列的出现次数的条形图

我们可以确认United Kingdom是这个数据集中最具代表性的国家(远远超过其他国家),其次是GermanyFranceEIRE。我们显然有不平衡的数据,这可能会影响预测模型的性能。在第十三章不平衡数据集中,我们将探讨如何处理这种情况。

现在,让我们分析日期时间列,即InvoiceDatealtair包提供了一些功能,我们可以利用这些功能按时间段(如天、星期几、月等)对日期时间信息进行分组。例如,如果我们希望查看一个变量的按月分布,可以使用yearmonth函数对日期时间进行分组。我们还需要通过在列名后添加:O来指定该变量的类型是顺序型(值之间有顺序):

alt.Chart(sample_df).mark_bar()\
   .encode(alt.X('yearmonth(InvoiceDate):O'),\
           y='count()')

你应该得到如下输出:

图 10.34:按月份分布的发票日期

图 10.34:按月分布的发票日期

该图表告诉我们,2011 年 11 月的销售量出现了巨大的峰值。该月售出了 800 件商品,而平均销售量大约为 300 件。那时是否有促销或广告活动?这些都是你可能需要向利益相关者提问的问题,以便他们能确认这一销售激增的原因。

箱形图

现在,我们将看看另一种特殊类型的图表——箱形图。这种图表用于显示基于四分位数的变量分布。四分位数是将数据集分成四个部分的值。每个部分包含 25%的观测值。例如,在以下样本数据中,四分位数如下:

图 10.35:给定数据的四分位数示例

图 10.35:给定数据的四分位数示例

因此,第一个四分位数(通常称为 Q1)是 4;第二个四分位数(Q2),也就是中位数,是 5;第三个四分位数(Q3)是 8。

箱形图将显示这些四分位数,还会显示一些额外的信息,如下所示:

  • 四分位差(IQR),即 Q3 - Q1

  • 最低值,等于 Q1 - (1.5 * IQR)

  • 最高值,等于 Q3 + (1.5 * IQR)

  • 异常值,也就是任何超出最低点和最高点的点:图 10.36:箱形图示例

图 10.36:箱形图示例

使用箱形图,我们很容易看到中心点(中位数),50%的数据落在(IQR)下方,以及异常值。

使用箱形图的另一个好处是,可以绘制分类变量与数值变量的分布,并进行比较。让我们通过mark_boxplot()方法尝试一下CountryQuantity列:

alt.Chart(sample_df).mark_boxplot()\
   .encode(x='Country:O', y='Quantity:Q')

你应该得到以下输出:

图 10.37:'Country'和'Quantity'列的箱形图

图 10.37:'Country'和'Quantity'列的箱形图

此图显示了Quantity变量在不同国家之间的分布情况。我们可以看到United Kingdom有很多异常值,尤其是在负值上。Eire是另一个有负值异常值的国家。除JapanNetherlandsSweden外,大多数国家的销售量都非常低。

在本节中,我们展示了如何使用altair包生成图表,这些图表帮助我们深入了解数据集,并识别出一些潜在问题。

练习 10.04:使用 Altair 可视化 Ames Housing 数据集

在本次练习中,我们将学习如何使用数据可视化功能,如直方图、散点图或箱形图,来更好地理解数据集以及变量之间的关系。

注意

你将使用与之前练习中相同的 Ames housing 数据集。

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandasaltair包:

    import pandas as pd
    import altair as alt
    
  3. 将 AMES 数据集的链接赋值给名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter10/dataset/ames_iowa_housing.csv'
    
  4. 使用pandas包中的read_csv方法,将数据集加载到名为'df'的新变量中:

    df = pd.read_csv(file_url)
    

    使用altair包中的mark_bar()encode()方法绘制SalePrice变量的直方图。使用alt.Xalt.Bin API 来指定箱子步长的数量,即50000

    alt.Chart(df).mark_bar()\
       .encode(alt.X("SalePrice:Q", bin=alt.Bin(step=50000)),\
               y='count()')
    

    你应该得到如下输出:

    图 10.38:的直方图

    图 10.38:SalePrice的直方图

    该图表显示,大多数房产的售价集中在100,000 – 150,000之间。也有少数异常值,其售价超过500,000

  5. 现在,让我们绘制LotArea的直方图,但这次使用步长为10000的箱子:

    alt.Chart(df).mark_bar()\
       .encode(alt.X("LotArea:Q", bin=alt.Bin(step=10000)),\
               y='count()')
    

    你应该得到如下输出:

    图 10.39:的直方图

    图 10.39:LotArea的直方图

    LotArea的分布与SalePrice完全不同。大多数观测值介于020,000之间,其余观测值则占数据集的一小部分。我们还可以注意到有一些极端的异常值,超过150,000

  6. 现在,绘制一个散点图,将LotArea作为X轴,SalePrice作为Y轴,以了解这两个变量之间的关系:

    alt.Chart(df).mark_circle()\
       .encode(x='LotArea:Q', y='SalePrice:Q')
    

    你应该得到如下输出:

    图 10.40:和的散点图

    图 10.40:SalePriceLotArea的散点图

    显然,房产大小和售价之间存在一定的相关性。如果我们仅观察LotArea小于 50,000 的房产,可以看到它们之间呈线性关系:如果我们从(0,0)坐标画一条直线到(20000,800000)坐标,可以说SalePrice随着LotArea每增加 1,000 而增加 40,000。这个直线(或回归线)的公式为SalePrice = 40000 * LotArea / 1000。我们还可以看到,对于某些房产,尽管它们的面积相当大,但价格并未遵循这种模式。例如,面积为 160,000 的房产以不到 300,000 的价格售出。

  7. 现在,让我们绘制OverallCond的直方图,但这次使用默认的箱子步长,即(bin=True):

    alt.Chart(df).mark_bar()\
       .encode(alt.X("OverallCond", bin=True), \
               y='count()')
    

    你应该得到如下输出:

    图 10.41:的直方图

    图 10.41:OverallCond的直方图

    我们可以看到该列中的值是离散的:它们只能取有限的值(介于19之间的任何整数)。这个变量不是数值型的,而是有序的:顺序很重要,但你不能对其执行一些数学运算,比如将值2加到值8上。这个列是对房产整体质量的一个任意映射。在下一章,我们将探讨如何改变这种列的数据类型。

  8. 使用mark_boxplot()方法,构建一个箱线图,将OverallCond:O':O'表示该列为有序列)作为 x 轴,将SalePrice作为 y 轴,如下代码片段所示:

    alt.Chart(df).mark_boxplot()\
       .encode(x='OverallCond:O', y='SalePrice:Q')
    

    你应该得到以下输出:

    图 10.42:OverallCond 的箱线图

    图 10.42:OverallCond 的箱线图

    看起来OverallCond变量是按升序排列的:条件值高时销售价格较高。然而,我们会发现,SalePrice在条件值为 5 时较高,这似乎代表中等状态。可能有其他因素影响这一类别的销售价格,例如,买家之间的竞争较为激烈。

  9. 现在,让我们绘制一个柱状图,以YrSold作为 x 轴,count()作为 y 轴。不要忘记使用':O'来指定YrSold是有序变量而非数值变量:

    alt.Chart(df).mark_bar()\
       .encode(alt.X('YrSold:O'), y='count()')
    

    你应该得到以下输出:

    图 10.43:YrSold 的柱状图

    图 10.43:YrSold 的柱状图

    我们可以看到,除 2010 年外,几乎每年售出的房产数量相同。从 2006 年到 2009 年,平均每年售出 310 套房产,而 2010 年仅售出 170 套。

  10. 绘制一个类似于步骤 8中的箱线图,但将YrSold作为其 x 轴:

    alt.Chart(df).mark_boxplot()\
       .encode(x='YrSold:O', y='SalePrice:Q')
    

    你应该得到以下输出:

    图 10.44:YrSold 与 SalePrice 的箱线图

    图 10.44:YrSold 与 SalePrice 的箱线图

    总体来看,销售价格的中位数在各年份之间相当稳定,2010 年略有下降。

  11. 让我们通过绘制类似于步骤 9中的柱状图,来分析SalePriceNeighborhood之间的关系:

    alt.Chart(df).mark_bar()\
       .encode(x='Neighborhood',y='count()')
    

    你应该得到以下输出:

    图 10.45:邻里的柱状图

    图 10.45:邻里的柱状图

    已售房产的数量根据位置不同有所差异。'NAmes'邻里售出的房产数量最多,超过 220 套。相反,像'Blueste''NPkVill'这样的邻里仅售出了几套房产。

  12. 让我们通过绘制一个类似于步骤 10中的箱线图,来分析SalePriceNeighborhood之间的关系:

    alt.Chart(df).mark_boxplot()\
       .encode(x='Neighborhood:O', y='SalePrice:Q')
    

    你应该得到以下输出:

    图 10.46:邻里与 SalePrice 的箱线图

图 10.46:邻里与 SalePrice 的箱线图

已售房产的地理位置对销售价格有显著影响。noRidgeNridgHtStoneBr等邻里地区的房价普遍较高。值得注意的是,NoRidge中存在一些极端的异常值,某些房产的售价远高于该邻里其他房产的价格。

注意

若要访问此特定部分的源代码,请参考packt.live/2CDWfO4

此部分目前没有在线互动示例,但可以像往常一样在 Google Colab 上运行。

通过数据可视化进行此分析,我们完成了此练习。通过数据可视化,我们可以获得关于数据集的一些宝贵见解。例如,使用散点图,我们确定了SalePriceLotArea之间的线性关系,即随着物业面积的增大,价格倾向于增加。直方图帮助我们了解数值变量的分布,条形图则为分类变量提供了类似的视图。例如,我们看到某些社区出售的房产比其他社区更多。最后,通过箱线图,我们能够分析和比较不同变量值对SalePrice的影响。我们看到,房产的良好状态将会使销售价格更高。数据可视化是数据科学家的重要工具,让他们能够探索和分析数据集。

活动 10.01:使用视觉数据分析技术分析流失数据

你正在为一家主要的电信公司工作。市场部门注意到最近客户流失率出现了上升(停止使用或取消公司服务的客户)。

要求您分析客户配置文件并预测未来的客户流失。在构建预测模型之前,您的第一个任务是分析市场部门与您共享的数据并评估其整体质量。

注意

可以在我们的 GitHub 仓库中找到此活动中要使用的数据集:packt.live/2s1yquq

我们将使用的数据集最初由 Eduardo Arino De La Rubia 在 Data.World 上共享:packt.live/2s1ynie

以下步骤将帮助您完成此活动:

  1. 使用 .read_csv() 在 Python 中下载并加载数据集。

  2. 通过使用.shape.dtypes.head().tail().sample() 探索数据集的结构和内容。

  3. 使用 .describe() 计算并解释描述性统计信息。

  4. 使用条形图、直方图或箱线图分析每个变量。

  5. 识别需要向市场部门澄清的问题和潜在的数据质量问题。

期望的输出

这是预期的条形图输出:

图 10.47:预期的条形图输出

图 10.47:预期的条形图输出

这是预期的直方图输出:

图 10.48:预期的直方图输出

图 10.48:预期的直方图输出

这是预期的箱线图输出:

图 10.49:预期的箱线图输出

图 10.49:预期的箱线图输出

注意

此活动的解决方案可在此处找到:packt.live/2GbJloz

你刚刚完成了本章的活动。你已经分析了与客户流失相关的数据集。你通过描述性统计和数据可视化学到了很多关于这个数据集的内容。在几行代码中,你理解了 DataFrame 的结构(行数和列数)以及每个变量中包含的信息类型。通过绘制某些列的分布,我们发现对于白天、晚上或国际电话存在特定的收费。我们还看到流失变量是不平衡的:大约只有 10%的客户会流失。最后,我们发现其中一个变量numbervmailmessages,对于流失客户和非流失客户的分布有很大不同。这可能是一个强有力的客户流失预测因子。

总结

你刚刚学到了很多关于如何分析数据集的知识。这是任何数据科学项目中非常关键的一步。深入了解数据集将帮助你更好地评估实现业务要求的可行性。

获取正确格式、质量合格的数据,对于任何机器学习算法来说,都是获得良好预测性能的关键。这就是为什么在进入下一阶段之前,花时间分析数据是如此重要。这一任务在 CRISP-DM 方法论中被称为数据理解阶段,也可以称为探索性数据分析EDA)。

你学会了如何使用描述性统计来总结数据集的关键属性,比如数值列的平均值、标准差或范围(最小值和最大值)、类别变量的唯一值以及它的最频繁值。你还学习了如何使用数据可视化来获得每个变量的有价值见解。现在,你知道如何使用散点图、条形图、直方图和箱线图来理解列的分布。

在分析数据时,我们遇到了需要在正常项目中与业务讨论的额外问题。我们还发现了一些潜在的数据质量问题,比如缺失值、离群值或需要修正的不正确值。这将是我们在下章中讨论的主题:数据准备。敬请期待。

第十一章:11. 数据准备

概述

本章结束时,你将能够根据特定条件筛选 DataFrame;删除重复或无关的记录或列;将变量转换为不同的数据类型;替换列中的值,并处理缺失值和异常值。

本章将介绍你可以用来处理数据问题的主要技术,以便在建模之前实现数据集的高质量。

介绍

在上一章中,你看到了了解数据的重要性,并学到了不同的技术和工具来实现这一目标。在对给定的数据集进行探索性数据分析EDA)时,你可能会发现一些潜在的问题,在建模阶段之前需要解决。正是本章将讨论的内容,你将学习如何处理一些最常见的数据质量问题,并适当准备数据集。

本章将向你介绍在数据科学家职业生涯中经常遇到的一些问题(如重复行、错误的数据类型、错误的值和缺失值),并介绍你可以使用的技术来轻松解决这些问题。但要小心——你遇到的一些问题不一定需要修复。你发现的一些可疑或意外值可能在业务角度上是合理的。这些可能是非常罕见但完全真实的值。因此,在修改数据集之前,与你的相关方或数据工程团队确认非常重要。确保你在准备数据集时为业务做出正确的决策是你的责任。

比如,在第十章数据集分析中,你查看了在线零售数据集,该数据集中Quantity列有一些负值。这里我们期望的是只有正值。但在直接解决这个问题(比如删除记录或将其转换为正值)之前,最好先与相关方沟通,确认这些值对于业务来说是否不重要。他们可能会告诉你,这些值非常重要,因为它们代表了退货商品,并且给公司带来了巨大的成本,所以他们希望分析这些情况,以减少这些数字。如果你直接进入数据清洗阶段,你可能会错过这一关键信息,进而得出错误的结论。

处理行重复问题

大多数时候,你收到或能访问的数据集可能并非 100%清洁。它们通常存在一些需要修复的问题。其中一个问题可能是行重复。行重复意味着数据集中有几条记录包含完全相同的信息。使用pandas包,你可以非常轻松地找到这些情况。

让我们使用在第十章中看到的例子,分析数据集

首先导入数据集到 DataFrame:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter10/dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

pandasduplicated()方法检查行是否为重复项,如果是重复项则返回True,否则返回False

df.duplicated()

你应该得到如下输出:

图 11.1:duplicated()方法的输出

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_01.jpg)

图 11.1:duplicated()方法的输出

注意

本章中的输出已被截断,以便有效地使用页面空间。

在 Python 中,TrueFalse这两个二进制值分别对应数值 1 和 0。为了找出有多少行被标记为重复项,你可以在duplicated()的输出上使用sum()方法。这将把所有的 1(即True值)加起来,从而给出重复项的数量:

df.duplicated().sum()

你应该得到如下输出:

5268

由于duplicated()方法的输出是一个pandas系列,每一行对应一个二进制值,你也可以利用它来对子集化 DataFrame 的行。pandas包提供了不同的 API 来对子集化 DataFrame,具体如下:

  • df[<行>或<列>]

  • df.loc[<行>, <列>]

  • df.iloc[<行>, <列>]

第一个 API 通过InvoiceNoStockCodeInvoiceDateCustomerID对 DataFrame 进行子集化,你需要使用如下代码:

df[['InvoiceNo', 'StockCode', 'InvoiceDate', 'CustomerID']]

你应该得到如下输出:

图 11.2:子集化列

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_03.jpg)

图 11.2:子集化列

如果你只想过滤掉被认为是重复的行,可以在duplicated()方法的输出上使用相同的 API 调用。它将只保留值为True的行:

df[df.duplicated()]

你应该得到如下输出:

图 11.3:子集化重复行

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_03.jpg)

图 11.3:子集化重复行

如果你想同时对子集化行和列,必须使用其他两个可用的 API 之一:.loc.iloc。这两个 API 功能相同,但.loc使用标签或名称,而.iloc仅接受索引作为输入。你将使用.loc API 来子集化重复行,并只保留选定的四列,正如前面的示例所示:

df.loc[df.duplicated(), ['InvoiceNo', 'StockCode', \
                         'InvoiceDate', 'CustomerID']]

你应该得到如下输出:

图 11.4:使用.loc 子集化重复行和选定的列

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_04.jpg)

图 11.4:使用.loc 子集化重复行和选定的列

上述输出显示,前几个重复项的行号分别为517527537等。默认情况下,pandas不会将重复项的第一次出现标记为重复:所有的重复项除了第一次出现外,其余都会标记为True。你可以通过指定keep参数来改变这个行为。如果你想保留最后一个重复项,你需要指定keep='last'

df.loc[df.duplicated(keep='last'), ['InvoiceNo', 'StockCode', \
                                    'InvoiceDate', 'CustomerID']]

你应该得到如下输出:

图 11.5:子集化最后的重复行

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_05.jpg)

图 11.5:子集化最后的重复行

从前面的输出可以看到,行 485 的值与行 539 相同。正如预期的那样,行 539 不再被标记为重复行。如果你想标记所有重复记录为重复,你需要使用 keep=False

df.loc[df.duplicated(keep=False), ['InvoiceNo', 'StockCode',\
                                   'InvoiceDate', 'CustomerID']]

你应该得到以下输出:

图 11.6:获取所有重复行的子集

图 11.6:获取所有重复行的子集

这次,行 485539 被标记为重复行。现在你已经知道如何识别重复观察值,你可以决定是否要将它们从数据集中删除。如前所述,修改数据时必须小心。你可能需要向业务方确认,他们是否同意你删除这些数据行。你需要解释为什么要删除这些行。在在线零售数据集中,以行 485539 为例,这两条记录是完全相同的。从业务角度来看,这意味着一个特定的客户(CustomerID 17908)在同一个发票(InvoiceNo 536409)上,在完全相同的日期和时间(InvoiceDate 2010-12-01 11:45:00),购买了相同的商品(StockCode 22111`)。这非常可疑。在与业务方沟通时,他们可能会告诉你,在那个时候发布了新软件,并且有一个 BUG 导致所有购买的商品都被拆分成单独的行项目。

在这种情况下,你知道不应该删除这些行。另一方面,他们可能会告诉你重复不应该发生,可能是由于人工错误,在数据录入或数据提取步骤中发生了问题。假设确实是这种情况,那么现在可以安全地删除这些行。

为此,你可以使用 pandasdrop_duplicates() 方法。它有与 duplicated() 相同的 keep 参数,用于指定你希望保留哪个重复记录,或者是否希望删除所有重复记录。在这种情况下,我们希望保留至少一行重复数据。这里,我们希望保留第一次出现的重复行:

df.drop_duplicates(keep='first')

你应该得到以下输出:

图 11.7:使用  删除重复行

图 11.7:使用 keep='first' 删除重复行

该方法的输出是一个新的 DataFrame,其中包含唯一的记录,只有第一次出现的重复项被保留。如果你希望替换现有的 DataFrame,而不是获得一个新的 DataFrame,则需要使用 inplace=True 参数。

drop_duplicates()duplicated() 方法还有一个非常有用的参数:subset。这个参数允许你在查找重复项时指定要考虑的列列表。默认情况下,DataFrame 的所有列都会用来查找重复行。我们来看一下仅查看 InvoiceNoStockCodeinvoiceDateCustomerID 列时,有多少重复行:

df.duplicated(subset=['InvoiceNo', 'StockCode', 'InvoiceDate',\
                      'CustomerID'], keep='first').sum()

你应该得到以下输出:

10677

仅查看这四列,而不是所有列,我们可以看到重复行的数量已从5268增加到10677。这意味着有些行在这四列的值完全相同,但在其他列的值不同,可能是不同的记录。在这种情况下,最好使用所有列来识别重复记录。

练习 11.01:处理乳腺癌数据集中的重复数据

在本练习中,你将学习如何识别重复记录,以及如何处理这些问题,使得数据集仅包含唯一记录。让我们开始吧:

注意

我们在本练习中使用的数据集是乳腺癌检测数据集,分享者是来自威斯康星大学医院的 Dr. William H. Wolberg,并由 UCI 机器学习库托管。该数据集的属性信息可以在此找到:packt.live/39LaIDx

这个数据集也可以在本书的 GitHub 仓库中找到:packt.live/2QqbHBC

  1. 打开一个新的Colab笔记本。

  2. 导入pandas包:

    import pandas as pd
    
  3. Breast Cancer数据集的链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter11/dataset/'\
               'breast-cancer-wisconsin.data'
    
  4. 使用pandas包中的read_csv()方法,加载数据集到一个名为df的新变量,并使用header=None参数。我们这么做是因为该文件没有列名:

    df = pd.read_csv(file_url, header=None)
    
  5. 创建一个名为col_names的变量,包含列名:Sample code number, Clump Thickness, Uniformity of Cell Size, Uniformity of Cell Shape, Marginal Adhesion, Single Epithelial Cell Size, Bare Nuclei, Bland Chromatin, Normal Nucleoli, MitosesClass

    col_names = ['Sample code number','Clump Thickness',\
                 'Uniformity of Cell Size',\
                 'Uniformity of Cell Shape',\
                 'Marginal Adhesion','Single Epithelial Cell Size',\
                 'Bare Nuclei','Bland Chromatin',\
                 'Normal Nucleoli','Mitoses','Class'] 
    
  6. 使用columns属性赋值 DataFrame 的列名:

    df.columns = col_names
    
  7. 使用.shape属性显示 DataFrame 的形状:

    df.shape
    

    你应该得到以下输出:

    (699, 11)
    

    该 DataFrame 包含699行和11列。

  8. 使用head()方法显示 DataFrame 的前五行:

    df.head()
    

    你应该得到以下输出:

    图 11.8:乳腺癌数据集的前五行

    图 11.8:乳腺癌数据集的前五行

    所有变量都是数值型的。样本代码号列是测量样本的标识符。

  9. 使用duplicated()sum()方法查找重复行的数量:

    df.duplicated().sum()
    

    你应该得到以下输出:

    8
    

    通过查看该数据集的 11 列,我们可以看到有8行重复数据。

  10. 使用loc()duplicated()方法显示重复行:

    df.loc[df.duplicated()]
    

    你应该得到以下输出:

    图 11.9:重复记录

    图 11.9:重复记录

    以下行是重复的:208253254258272338561684

  11. 如同在步骤 9中那样显示重复行,但这次使用keep='last'参数:

    df.loc[df.duplicated(keep='last')]
    

    你应该得到以下输出:

    图 11.10:使用 keep='last' 的重复记录

    图 11.10:使用 keep='last' 的重复记录

    通过使用keep='last'参数,以下行被认为是重复的:4262168207267314560,和683。通过将这个输出与前一步的输出进行比较,我们可以看到第 253 行和第 42 行是相同的。

  12. 使用drop_duplicates()方法以及keep='first'参数删除重复行,并将其保存到一个名为df_unique的新 DataFrame 中:

    df_unique = df.drop_duplicates(keep='first')
    
  13. 使用.shape属性显示df_unique的形状:

    df_unique.shape
    

    你应该会得到以下输出:

    (691, 11)
    

    现在我们已经删除了八条重复记录,剩下的只有691行数据。现在,数据集只包含唯一的观察值。

    注意

    若要访问此部分的源代码,请参考packt.live/2EiArYI

    你还可以在packt.live/349tLat在线运行这个例子。

在本练习中,你学习了如何识别并删除来自真实世界数据集的重复记录。

转换数据类型

你在项目中可能会遇到的另一个问题是某些列的数据类型被错误推断。正如我们在第十章《分析数据集》中所看到的,pandas包提供了一个非常简单的方法,通过.dtypes属性显示每一列的数据类型。你可能会想,pandas是什么时候识别每一列的数据类型的?当你使用read_csv()read_excel()等方法将数据集加载到pandas DataFrame 时,类型就被检测出来了。

完成这些操作后,pandas将尽力根据每一列中包含的值自动找出最佳数据类型。让我们看看这个方法在Online Retail数据集上的效果。

首先,你必须导入pandas

import pandas as pd

然后,你需要将数据集的 URL 赋给一个新变量:

file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter10/dataset/'\
           'Online%20Retail.xlsx?raw=true'

让我们使用read_excel()将数据集加载到pandas DataFrame 中:

df = pd.read_excel(file_url)

最后,让我们打印每一列的数据类型:

df.dtypes

你应该会得到以下输出:

图 11.11:在线零售数据集每列的数据类型

图 11.11:在线零售数据集每列的数据类型

上面的输出显示了每一列被分配的数据类型。QuantityUnitPriceCustomerID被识别为数值变量(int64float64),InvoiceDate是一个datetime变量,其他所有列都被认为是文本(object)。这并不算太糟糕。pandas在识别非文本列方面做得很不错。

但是如果你想改变某些列的数据类型呢?你有两种方法可以实现这一点。

第一种方法是重新加载数据集,但这一次,你需要使用dtype参数指定感兴趣列的数据类型。该参数接受一个字典,其中列名作为键,正确的数据类型作为值,例如{'col1': np.float64, 'col2': np.int32},作为输入。让我们尝试在CustomerID上使用这个方法。我们知道它不是一个数值变量,因为它包含唯一的标识符(代码)。在这里,我们将把它的数据类型更改为object

df = pd.read_excel(file_url, dtype={'CustomerID': 'category'})
df.dtypes

你应该得到如下输出:

图 11.12:转换 CustomerID 后每列的数据类型

图 11.12:转换 CustomerID 后每列的数据类型

如你所见,CustomerID的数据类型已成功更改为category类型。

现在,让我们看看第二种将单列转换为不同类型的方法。在pandas中,你可以使用astype()方法,并指定要转换成的新数据类型,作为其pandas序列,更准确地说,你需要将其重新赋值给数据框的同一列。例如,如果你想将InvoiceNo列更改为分类变量,你可以这样做:

df['InvoiceNo'] = df['InvoiceNo'].astype('category')
df.dtypes

你应该得到如下输出:

图 11.13:转换 InvoiceNo 后每列的数据类型

图 11.13:转换 InvoiceNo 后每列的数据类型

如你所见,InvoiceNo的数据类型已更改为分类变量。objectcategory的区别在于,后者具有有限的可能值(也叫离散变量)。一旦这些被更改为分类变量,pandas将自动列出所有值。你可以通过.cat.categories属性访问它们:

df['InvoiceNo'].cat.categories

你应该得到如下输出:

图 11.14:InvoiceNo 分类变量的类别(可能值)列表

图 11.14:InvoiceNo 分类变量的类别(可能值)列表

pandas已识别出该列中有 25,900 个不同的值,并列出了所有这些值。根据变量分配的数据类型,pandas提供了不同的属性和方法,这些对于数据转换或特征工程非常有用(将在第十二章特征工程中讨论)。

最后需要说明的是,你可能会想知道何时使用第一种更改某些列类型的方法(在加载数据集时)。为了找出每个变量的当前类型,你必须先加载数据,那么为什么还需要使用新的数据类型重新加载数据呢?在第一次加载后,使用astype()方法来更改类型会更容易。使用它的原因有几个。一个可能的原因是你已经在其他工具中探索了数据集,比如 Excel,并且已经知道正确的数据类型是什么。

第二个原因可能是你的数据集很大,无法一次性加载。如你所见,默认情况下,pandas 使用 64 位编码来处理数值变量,这会占用大量内存,可能有些过于浪费。

例如,Quantity 列的数据类型是 int64,这意味着可能的值范围是 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。然而,在第十章数据集分析中分析该列分布时,你了解到该列的值范围仅为 -80,995 到 80,995。你不需要使用如此多的空间。通过将该变量的数据类型从 int64 降级为 int32(范围为 -2,147,483,648 到 2,147,483,647),你可能能够重新加载整个数据集。

练习 11.02:为 Ames Housing 数据集转换数据类型

在本练习中,你将通过将变量转换为正确的数据类型来准备数据集。

你将使用 Ames Housing 数据集来完成此任务,我们也在第十章数据集分析中使用了该数据集。有关此数据集的更多信息,请参阅以下说明。让我们开始吧:

注意

本练习中使用的数据集是 Ames Housing 数据集,由 Dean De Cock 编制:packt.live/2QTbTbq

为了方便,你可以在本书的 GitHub 仓库中找到该数据集:packt.live/2ZUk4bz

  1. 打开一个新的 Colab 笔记本。

  2. 导入 pandas 包:

    import pandas as pd
    
  3. 将 Ames 数据集的链接赋值给一个名为 file_url 的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter10/dataset/ames_iowa_housing.csv'
    
  4. 使用 pandas 包的 read_csv 方法,将数据集加载到一个名为 df 的新变量中:

    df = pd.read_csv(file_url)
    
  5. 使用 dtypes 属性打印每一列的数据类型:

    df.dtypes
    

    你应该得到以下输出:

    图 11.15:列及其分配的数据类型列表

    图 11.15:列及其分配的数据类型列表

    注意

    上面的输出已被截断。

    第十章数据集分析中,你知道 IdMSSubClassOverallQualOverallCond 列被错误地归类为数值变量。它们有有限数量的唯一值,不能进行任何数学运算。例如,将 Id 列中的两个不同值相加、相减、相乘或相除是没有意义的。因此,你需要将它们转换为类别变量。

  6. 使用 astype() 方法,将 'Id' 列转换为类别变量,如下所示:

    df['Id'] = df['Id'].astype('category')
    
  7. 'MSSubClass''OverallQual''OverallCond' 列转换为类别变量,就像我们在上一步中做的那样:

    df['MSSubClass'] = df['MSSubClass'].astype('category')
    df['OverallQual'] = df['OverallQual'].astype('category')
    df['OverallCond'] = df['OverallCond'].astype('category')
    
  8. 创建一个 for 循环,遍历四个类别列('Id', 'MSSubClass', 'OverallQual','OverallCond')并使用 .cat.categories 属性打印它们的名称和类别:

    for col_name in ['Id', 'MSSubClass', 'OverallQual', \
                     'OverallCond']:
        print(col_name)
        print(df[col_name].cat.categories)
    

    你应该得到以下输出:

    图 11.16:四个新转换变量的类别列表

    图 11.16:四个新转换变量的类别列表

    现在,这四列已经被转换为类别变量。从步骤 5的输出中,我们可以看到有很多object类型的变量。让我们看看它们,看看是否也需要转换。

  9. 创建一个新的 DataFrame,命名为obj_df,该 DataFrame 仅包含object类型的变量,使用select_dtypes方法并加上include='object'参数:

    obj_df = df.select_dtypes(include='object')
    
  10. 创建一个名为obj_cols的新变量,包含obj_df DataFrame 中列名的列表,使用.columns属性并显示其内容:

    obj_cols = obj_df.columns
    obj_cols
    

    你应该得到以下输出:

    图 11.17:“object”类型变量列表

    图 11.17:“object”类型变量列表

  11. 像我们在步骤 8中做的那样,创建一个for循环,遍历obj_cols中包含的列名,并使用unique()方法打印它们的名称和唯一值:

    for col_name in obj_cols:
        print(col_name)
        print(df[col_name].unique())
    

    你应该得到以下输出:

    图 11.18:每个“object”类型变量的唯一值列表

    图 11.18:每个“object”类型变量的唯一值列表

    如你所见,所有这些列都包含有限数量的唯一值,且这些值由文本组成,这表明它们是类别变量。

  12. 现在,创建一个for循环,遍历obj_cols中包含的列名,并使用astype()方法将每个列转换为类别变量:

    for col_name in obj_cols:
        df[col_name] = df[col_name].astype('category')
    
  13. 打印每一列的数据类型,使用dtypes属性:

    df.dtypes
    

    你应该得到以下输出:

    图 11.19:变量及其新数据类型的列表

图 11.19:变量及其新数据类型的列表

注意

上面的输出已被截断。

要访问此部分的源代码,请参考packt.live/2FvR8R6

你也可以在packt.live/3aAmKka上运行此示例。

你已经成功地将具有错误数据类型(数值型或对象型)的列转换为类别变量。你的数据集现在距离准备好进行建模更进一步了。

在下一部分,我们将讨论如何处理错误值。

处理错误值

你可能会遇到的另一个问题是数据集中某些观察值的错误值。有时,这是由于语法错误引起的;例如,某个国家的名称可能全为小写、全为大写、标题化(只有第一个字母大写),或者甚至缩写。例如,法国可能会有不同的值,如‘France’,‘FRANCE’,‘france’,‘FR’等。如果你将‘France’定义为标准格式,那么所有其他变体都被认为是数据集中的错误值,并需要修正。

如果这种问题在建模阶段之前没有得到处理,可能会导致错误的结果。模型会认为这些不同的变体是完全不同的值,并可能不太关注这些值,因为它们有独立的频率。例如,假设'France'代表 2%的值,'FRANCE'代表 2%,'FR'代表 1%。你知道这些值对应的是同一个国家,应该代表 5%的值,但模型会把它们当作不同的国家处理。

让我们通过使用Online Retail数据集来学习如何在实际生活中检测此类问题。

首先,你需要将数据加载到一个pandas DataFrame 中:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter10/dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

在这个数据集中,有两个变量似乎彼此相关:StockCodeDescription。第一个包含所售物品的标识代码,另一个包含它们的描述。然而,如果你查看一些例子,比如StockCode 23131Description列有不同的值:

df.loc[df['StockCode'] == 23131, 'Description'].unique()

你应该得到以下输出:

图 11.20:描述列和库存代码 23131 的唯一值列表

图 11.20:描述列和库存代码 23131 的唯一值列表

上述输出中有多个问题。一个问题是单词Mistletoe拼写错误,变成了Miseltoe。其他错误是意外值和缺失值,这些将在下一节中讨论。看来Description列被用来记录诸如had been put aside的评论。

让我们专注于拼写错误问题。我们需要做的是修改错误的拼写并用正确的值替换它。首先,让我们创建一个名为StockCodeDescription的新列,它是Description列的完全副本:

df['StockCodeDescription'] = df['Description']

你将使用这个新列来修正拼写错误问题。为此,使用你在本章前面学到的子集技巧。你需要使用.loc并筛选你想要的行和列,即所有StockCode == 21131Description == MISELTOE HEART WREATH CREAM的行,以及Description列:

df.loc[(df['StockCode'] == 23131) \
        & (df['StockCodeDescription'] \
           == 'MISELTOE HEART WREATH CREAM'), \
        'StockCodeDescription'] = 'MISTLETOE HEART WREATH CREAM'

如果你重新打印这个问题的值,你会看到拼写错误的值已经被修正,不再出现:

df.loc[df['StockCode'] == 23131, 'StockCodeDescription'].unique()

你应该得到以下输出:

图 11.21:描述列和库存代码 23131 的唯一值列表修正了第一个拼写错误问题后

图 11.21:修正第一个拼写错误问题后的描述列和库存代码 23131 的唯一值列表

如你所见,这个产品仍然有五个不同的值,但其中一个值,即MISTLETOE,被拼写错误:MISELTOE

这一次,我们不再查找完全匹配(一个单词必须与另一个单词完全相同),而是查找部分匹配(一个单词的部分内容出现在另一个单词中)。在我们的例子中,我们将不再查找MISELTOE的拼写,而只查找MISELpandas包提供了一种名为.str.contains()的方法,我们可以使用它来查找与给定表达式部分匹配的观察值。

让我们使用这个方法来查看整个数据集中是否有相同的拼写错误问题(MISEL)。由于此方法无法处理缺失值,你需要添加一个额外的参数。此外,你还需要筛选出Description列中没有缺失值的行。这可以通过向.str.contains()方法提供na=False参数来实现:

df.loc[df['StockCodeDescription']\
  .str.contains('MISEL', na=False),]

你应该得到以下输出:

图 11.22:显示所有包含拼写错误'MISELTOE'的行

图 11.22:显示所有包含拼写错误'MISELTOE'的行

这个拼写错误问题(MISELTOE)不仅与StockCode 23131相关,还涉及其他项目。你需要使用str.replace()方法修复所有这些问题。该方法接受两个参数:需要替换的字符字符串和替换字符串:

df['StockCodeDescription'] = df['StockCodeDescription']\
                             .str.replace\
                             ('MISELTOE', 'MISTLETOE')

现在,如果你打印出所有包含拼写错误MISEL的行,你将看到这些行已经不存在了:

df.loc[df['StockCodeDescription']\
  .str.contains('MISEL', na=False),]

你应该得到以下输出:

图 11.23:清理后显示所有包含拼写错误 MISELTOE 的行

图 11.23:清理后显示所有包含拼写错误 MISELTOE 的行

你刚刚看到如何轻松清理数据中包含错误值的观察值,方法是使用pandas包提供的.str.contains.str.replace()方法。这些方法只能用于包含字符串的变量,但相同的逻辑也可以应用于数值变量,并且可以用来处理极端值或异常值。你可以使用==、>、<、>=或<=运算符来筛选你想要的行,然后将观察值替换为正确的值。

练习 11.03:修复 State 列中的错误值

在这个练习中,你将通过列出所有美国的财务官员来清理数据集中State变量的值。我们之所以这样做,是因为数据集中包含一些错误的值。让我们开始吧:

注意:

原始数据集由 Forest Gregg 和 Derek Eder 共享,可以在packt.live/2rTJVns找到。

我们在这里使用的修改版数据集可以在本书的 GitHub 仓库中找到:packt.live/2MZJsrk

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包:

    import pandas as pd
    
  3. 将数据集链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter11/dataset/officers.csv'
    
  4. 使用pandas包中的read_csv()方法,将数据集加载到一个名为df的新变量中:

    df = pd.read_csv(file_url)
    
  5. 使用 .head() 方法打印出数据框的前五行:

    df.head()
    

    你应该得到以下输出:

    图 11.24:财务官员数据集的前五行

    图 11.24:财务官员数据集的前五行

  6. 打印出 State 变量的所有唯一值:

    df['State'].unique()
    

    你应该得到以下输出:

    图 11.25:State 列中唯一值的列表

    图 11.25:State 列中唯一值的列表

    所有州的值都已编码为两位大写字符格式。如你所见,有些值是错误的,如 iliL(看起来像是 Illinois 的拼写错误),还有一些意外的值,如 8II60。在接下来的几步中,你将解决这些问题。

  7. 使用 pandas.str.contains() 方法和子集 API 打印出 State 列中包含 il 值的行,即 DataFrame [条件]。你还需要在 str.contains() 中将 na 参数设置为 False,以排除缺失值的观测:

    df[df['State'].str.contains('il', na=False)]
    

    你应该得到以下输出:

    图 11.26:具有 il 值的观测

    图 11.26:具有 il 值的观测

    如你所见,所有具有 il 值的城市都来自伊利诺伊州。因此,正确的 State 值应为 IL。你可能会认为以下值也指向伊利诺伊州:IliLIl。我们接下来将查看它们。

  8. 现在,创建一个 for 循环,迭代 State 列中的以下值:IliLIl。然后,使用 pandas 的子集方法打印出 City 和 State 变量的值,即 .loc():DataFrame.loc[行条件,列条件]。对每个观测值执行此操作:

    for state in ['Il', 'iL', 'Il']:
        print(df.loc[df['State'] == state, ['City', 'State']])
    

    你应该得到以下输出:

    图 11.27:具有 il 值的观测

    图 11.27:具有 il 值的观测

    注意

    前面的输出已被截断。

    如你所见,这些城市都属于伊利诺伊州。我们将用正确的值替换它们。

  9. 创建一个条件掩码(il_mask),使用 isin() 方法和这些值的列表作为参数,筛选出包含四个错误值(ilIliLIl)的所有行。然后,将结果保存在一个名为 il_mask 的变量中:

    il_mask = df['State'].isin(['il', 'Il', 'iL', 'Il'])
    
  10. 使用 .sum() 方法打印出与我们在 il_mask 中设置的条件匹配的行数。这将对所有值为 True(匹配条件)的行进行求和:

    il_mask.sum()
    

    你应该得到以下输出:

    672
    
  11. 使用 pandas.loc() 方法,筛选出符合 il_mask 条件的行,并将 State 列的值替换为 IL

    df.loc[il_mask, 'State'] = 'IL'
    
  12. 再次打印出 State 变量的所有唯一值:

    df['State'].unique()
    

    你应该得到以下输出:

    图 11.28:'State' 列唯一值的列表

    df.loc[df['State'] == 'II',]
    

    你应该得到以下输出:

    图 11.29:在 State 列中筛选出值为 IL 的行

    图 11.29:在 State 列中筛选出值为 IL 的行

    State列中仅有两个地方使用了II值,且这两个地方的城市都是 Bloomington,位于伊利诺伊州。此处,正确的State值应该是IL

  13. 现在,创建一个for循环,迭代三个不正确的值(I8I60),并使用我们在步骤 12中使用的相同逻辑输出子集行。只显示CityState列:

    for val in ['I', '8I', '60']:
        print(df.loc[df['State'] == val, ['City', 'State']])
    

    你应该得到以下输出:

    图 11.30:包含不正确值(I、8I 和 60)的观测

    图 11.30:包含不正确值(I、8I 和 60)的观测

    所有包含不正确值的观测都是位于伊利诺伊州的城市。让我们现在修复它们。

  14. 创建一个for循环,迭代四个不正确的值(III8I60),并重新使用步骤 12中的子集逻辑将State中的值替换为IL

    for val in ['II', 'I', '8I', '60']:
        df.loc[df['State'] == val, 'State'] = 'IL'
    
  15. 输出所有State变量的唯一值:

    df['State'].unique()
    

    你应该得到以下输出:

    图 11.31:State 列的唯一值列表

    图 11.31:State 列的唯一值列表

    你已经修复了伊利诺伊州的状态问题。然而,在这一列中还有两个不正确的值:Inng

  16. 重复步骤 13,但改为迭代Inng值:

    for val in ['In', 'ng']:
        print(df.loc[df['State'] == val, ['City', 'State']])
    

    你应该得到以下输出:

    图 11.32:包含不正确值(In,ng)

    图 11.32:包含不正确值(In,ng)的观测

    State中具有ng值的行缺少值。我们将在下一部分中讨论这个话题。StateIn的观测是印第安纳州的一个城市,所以正确的值应该是IN。让我们修正它。

  17. 使用.loc().str.contains()方法筛选出State中包含In值的行,并将州值替换为IN。别忘了为.str.contains()指定na=False参数:

    df.loc[df['State']\
      .str.contains('In', na=False), 'State'] = 'IN'
    

    输出所有State变量的唯一值:

    df['State'].unique()
    

    你应该得到以下输出:

    图 11.33:State 列的唯一值列表

图 11.33:State 列的唯一值列表

注意

要访问此特定部分的源代码,请参考packt.live/319pfGX

你也可以在线运行这个例子,网址为packt.live/2E8ICHn

你刚刚使用pandas包提供的方法修复了State变量中的所有不正确值。在下一部分,我们将讨论如何处理缺失值。

处理缺失值

到目前为止,你已经看到了数据集中的各种问题。现在是时候讨论另一个经常发生的问题:缺失值。如你所料,这类问题意味着某些变量的某些值缺失。

pandas包提供了一个方法,我们可以用它来识别 DataFrame 中的缺失值:.isna()。我们来看一下它在Online Retail数据集上的应用。首先,你需要导入pandas并将数据加载到 DataFrame 中:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter10/dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

.isna()方法返回一个pandas系列,表示 DataFrame 中每个单元格的二进制值,指出它是否缺少值(True)或者不缺少值(False):

df.isna()

你应该得到以下输出:

图 11.34:方法的输出

图 11.34:.isna()方法的输出

正如我们之前看到的,我们可以将一个二元变量的输出传递给.sum()方法,它会将所有的True值加在一起(具有缺失值的单元格),并为每一列提供一个总结:

df.isna().sum()

你应该得到以下输出:

图 11.35:每个变量的缺失值总结

图 11.35:每个变量的缺失值总结

正如你所看到的,Description列中有1454个缺失值,而CustomerID列中有135080个缺失值。让我们来看一下Description中缺失值的观测。你可以使用.isna()方法的输出来提取缺失值的行:

df[df['Description'].isna()]

你应该得到以下输出:

图 11.36:提取具有缺失值的描述行

图 11.36:提取具有缺失值的描述行

从上面的输出中,你可以看到所有缺失值的行,unit price(单价)都是0.0,并且缺失了CustomerID列。在实际项目中,你需要与业务方讨论这些情况,检查这些交易是否真实。如果业务方确认这些观测数据无关,那么你需要从数据集中删除它们。

pandas包提供了一个方法,我们可以用它轻松地删除缺失值:.dropna()。这个方法会返回一个没有缺失值的 DataFrame。默认情况下,它会查看所有列。你可以使用subset参数指定一个列列表,让它去查找:

df.dropna(subset=['Description'])

这个方法会返回一个新的 DataFrame,其中指定的列没有缺失值。如果你希望直接替换原始数据集,可以使用inplace=True参数:

df.dropna(subset=['Description'], inplace=True)

现在,来看一下每个变量的缺失值总结:

df.isna().sum()

你应该得到以下输出:

图 11.37:每个变量的缺失值总结

图 11.37:每个变量的缺失值总结

正如你所看到的,Description列中不再有缺失值。让我们看一下CustomerID列:

df[df['CustomerID'].isna()]

你应该得到以下输出:

图 11.38:缺失值的 CustomerID 行

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_41.jpg)

图 11.38:缺失值的 CustomerID 行

这次,所有交易看起来都正常,只是CustomerID列缺失值;其他所有变量都已填充了看起来合理的值。无法推断CustomerID列的缺失值。这些行占数据集的约 25%,因此不能将它们删除。

然而,大多数算法要求每个观察值都有一个值,因此你需要为这些情况提供一个值。我们将使用pandas.fillna()方法来做到这一点。将填充的值设置为Missing,并将inplace=True作为参数:

df['CustomerID'].fillna('Missing', inplace=True)
df[1443:1448]

你应该得到以下输出:

图 11.39:缺失值的 CustomerID 行示例

已被替换为 Missing

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_39.jpg)

图 11.39:缺失值的 CustomerID 已被替换为 Missing 的行示例

让我们检查数据集中是否有缺失值:

df.isna().sum()

你应该得到以下输出:

图 11.40:各变量缺失值的总结

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_40.jpg)

图 11.40:各变量缺失值的总结

你已成功修复了数据集中所有缺失的值。这些方法同样适用于处理缺失的数值变量。我们将在接下来的练习中讨论这一点。当你想要使用.fillna()填充缺失值时,只需提供一个数值。

练习 11.04:修复马匹肠绞痛数据集中的缺失值

在本练习中,你将清理马匹肠绞痛数据集中所有数值变量的缺失值。

肠绞痛是马匹可能遭受的一种疼痛情况,这个数据集包含了与该疾病的特定病例相关的各种信息。如果你想了解更多关于数据集属性的信息,可以使用“注”部分提供的链接。让我们开始吧:

该数据集来自 UCI 机器学习库。关于属性的信息可以在packt.live/2MZwSrW找到。

为了方便你,我们将在本练习中使用的数据集文件已经上传到本书的 GitHub 仓库:packt.live/35qESZq

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandas包:

    import pandas as pd
    
  3. 将数据集的链接赋值给名为file_url的变量:

    file_url = 'http://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter11/dataset/horse-colic.data'
    
  4. 使用pandas包中的.read_csv()方法,将数据集加载到一个名为df的新变量中,并指定header=Nonesep='\s+'prefix='X'参数:

    df = pd.read_csv(file_url, header=None, \
                     sep='\s+', prefix='X')
    
  5. 使用.head()方法打印数据框的前五行:

    df.head()
    

    你应该得到以下输出:

    图 11.41:马匹肠绞痛数据集的前五行

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_11_41.jpg)

    图 11.41:马匹肠绞痛数据集的前五行

    如你所见,作者使用了?字符表示缺失值,但pandas包认为这只是一个正常值。你需要将它们转换为缺失值。

  6. 使用.read_csv()方法将数据集重新加载到pandas数据框中,但这次需要添加na_values='?'参数,指定将此值视为缺失值:

    df = pd.read_csv(file_url, header=None, sep='\s+', \
                     prefix='X', na_values='?')
    
  7. 使用.head()方法打印数据框的前五行:

    df.head()
    

    你应该得到以下输出:

    图 11.42:马疝气数据集的前五行

    图 11.42:马疝气数据集的前五行

    现在,你可以看到pandas已将所有?值转换为缺失值。

  8. 使用dtypes属性打印每列的数据类型:

    df.dtypes
    

    你应该得到以下输出:

    图 11.43:每列的数据类型

    图 11.43:每列的数据类型

  9. 通过结合.isna().sum()方法,打印每列的缺失值数量:

    df.isna().sum()
    

    你应该得到以下输出:

    图 11.44:每列的缺失值数量

    图 11.44:每列的缺失值数量

  10. 创建一个名为x0_mask的条件掩码,这样你就可以使用.isna()方法找到X0列中的缺失值:

    x0_mask = df['X0'].isna()
    
  11. 使用.sum()方法对x0_mask显示此列的缺失值数量:

    x0_mask.sum()
    

    你应该得到以下输出:

    1
    

    这里,你得到了与步骤 9X0相同的缺失值数量。

  12. 使用.median()方法提取X0的均值,并将其存储在名为x0_median的新变量中。打印其值:

    x0_median = df['X0'].median()
    print(x0_median)
    

    你应该得到以下输出:

    1.0
    

    这个列的中位数值是1。你将用这个值替换X0列中的所有缺失值。

  13. 使用.fillna()方法和inplace=True参数,用中位数值替换X0变量中的所有缺失值:

    df['X0'].fillna(x0_median, inplace=True)
    
  14. 通过结合.isna().sum()方法,打印X0的缺失值数量:

    df['X0'].isna().sum()
    

    你应该得到以下输出:

    0
    

    变量中不再有缺失值。

  15. 创建一个for循环,遍历数据框中的所有列。在循环中,计算每列的中位数,并将其保存到一个名为col_median的变量中。然后,使用.fillna()方法以及inplace=True参数,用这个中位数值填充缺失值,并打印列名及其中位数值:

    for col_name in df.columns:
        col_median = df[col_name].median()
        df[col_name].fillna(col_median, inplace=True)
        print(col_name)
        print(col_median)
    

    你应该得到以下输出:

    图 11.45:每列的中位数值

    图 11.45:每列的中位数值

  16. 通过结合.isna().sum()方法,打印每列的缺失值数量:

    df.isna().sum()
    

    你应该得到以下输出:

    图 11.46:每列的缺失值数量

图 11.46:每列的缺失值数量

注意

要访问此部分的源代码,请参考 packt.live/34c1zUd

你也可以在 packt.live/324mHt0 上在线运行这个示例。

你已经成功地使用 pandas 包提供的方法 .isna().fillna() 修复了所有数值变量的缺失值。

活动 11.01:准备快速约会数据集

作为一名企业家,你计划推出一款新的约会应用程序。你的应用与其他竞争对手的主要区别将是你高效的用户匹配算法。在构建这个模型之前,你与一家快速约会公司合作,从真实活动中收集数据。你刚刚收到了合作伙伴公司提供的数据集,但你意识到数据并不像你预期的那样干净;存在缺失和错误的值。你的任务是修复该数据集中主要的数据质量问题。

以下步骤将帮助你完成此活动:

  1. 使用 .read_csv() 下载并加载数据集到 Python 中。

  2. 使用 .shape 打印出 DataFrame 的维度。

  3. 使用 .duplicated().sum() 检查所有列的重复行。

  4. 使用 .duplicated().sum() 检查标识符列(iididpartnerpid)的重复行。

  5. 检查以下数值变量是否有意外值:'imprace', 'imprelig', 'sports', 'tvsports', 'exercise', 'dining', 'museums', 'art', 'hiking', 'gaming', 'clubbing', 'reading', 'tv', 'theater', 'movies', 'concerts', 'music', 'shopping''yoga'

  6. 替换已识别的错误值。

  7. 使用 .dtypes 检查不同列的数据类型。

  8. 使用 .astype() 将不包含数值的列的数据类型更改为分类数据类型。

  9. 使用 .isna().sum() 检查每个数值变量的缺失值。

  10. 使用 .fillna().mean().median() 替换每个数值变量的缺失值,填充为相应的均值或中位数值。

    注意

    本活动的数据集可以在本书的 GitHub 仓库中找到:packt.live/36u0jtR

    原始数据集由哥伦比亚商学院的 Ray Fisman 和 Sheena Iyengar 分享:packt.live/2Fp5rUg

    作者提供了一份非常有用的文档,描述了数据集及其特征:packt.live/2Qrp7gD

你应该得到以下输出。该图表示具有意外值的 imprace 行数以及意外值的列表:

Figure 11.47: Number of rows with unexpected values for 'imprace'以及一组意外的值列表。

图 11.47:具有意外值的 imprace 行数和意外值的列表。

下图展示了具有意外值的行数以及每列意外值的列表:

图 11.48:具有意外值的行数和每列的意外值列表

图 11.48:具有意外值的行数以及每列意外值的列表

下图展示了游戏的唯一值列表:

图 11.49:游戏的唯一值列表

图 11.49:游戏的唯一值列表

下图展示了每列的数据类型:

图 11.50:每列的数据类型

图 11.50:每列的数据类型

下图显示了每列的更新数据类型:

图 11.51:每列的数据类型

图 11.51:每列的数据类型

下图显示了数值变量的缺失值数量:

图 11.52:数值变量的缺失值数量

图 11.52:数值变量的缺失值数量

下图显示了 int_corr 的唯一值列表:

图 11.53:'int_corr' 的唯一值列表

图 11.53:'int_corr' 的唯一值列表

下图显示了数值变量的唯一值列表:

图 11.54:数值变量的唯一值列表

图 11.54:数值变量的唯一值列表

下图显示了数值变量的缺失值数量:

图 11.55:数值变量的缺失值数量

图 11.55:数值变量的缺失值数量

注意

此活动的解决方案可以在以下地址找到:packt.live/2GbJloz

总结

在本章中,你了解了准备给定数据集并修复其主要质量问题的重要性。这一点至关重要,因为数据集越干净,任何机器学习模型就越容易学到相关的模式。更重要的是,大多数算法无法处理缺失值等问题,因此必须在建模阶段之前解决这些问题。在本章中,你涵盖了数据科学项目中最常遇到的问题:重复行、错误的数据类型、意外值和缺失值。

本章的目标是向您介绍一些概念,这些概念将帮助您发现一些问题并轻松修复它们,从而拥有基本工具箱,可以处理其他情况。最后需要注意的是,在本章的整个过程中,我们强调了与您所在的业务或数据工程团队讨论发现的问题的重要性。例如,如果您在数据集中检测到意外的值,您可能希望在删除或替换它们之前确认它们在业务上是否具有特殊意义。

在修复问题时,您还需要非常小心:您不希望过度修改数据集,以免产生额外的意外模式。这正是为什么建议您用平均值中位数替换数值变量中的任何缺失值。否则,您将会大幅改变其分布。例如,如果某个变量的值在 0 到 10 之间,将所有缺失值替换为-999 将极大地改变它们的平均值和标准 偏差

在下一章中,我们将讨论特征工程这一有趣的主题。

第十二章:12. 特征工程

概述

本章结束时,你将能够将多个数据集合并在一起;对分类变量和数值变量进行分箱处理;对数据进行聚合操作;以及使用pandas操作日期。

本章将向你介绍一些在现有数据集上创建新变量的关键技术。

介绍

在前几章中,我们学习了如何分析和准备数据集,以提高其质量水平。在本章中,我们将向你介绍另一个有趣的话题:创建新特征,也叫做特征工程。你已经在第三章《二分类》中看到了一些这些概念,但我们将在本章中对其进行更深入的探讨。

特征工程的目标是为你所进行的分析或将要训练的机器学习算法提供更多的信息。增加更多的信息将帮助你获得更好、更准确的结果。

新特征可以来自内部数据源,如数据库中的另一个表,或者来自不同的系统。例如,你可能想要将公司使用的 CRM 工具中的数据与营销工具中的数据连接起来。添加的特征也可以来自外部数据源,如开源数据或来自合作伙伴或供应商的共享数据。例如,你可能想要将销售量与天气 API 或政府普查数据联系起来。但它也可以通过从现有特征中创建新变量而来自原始数据集。

让我们暂停片刻,理解为什么特征工程对训练机器学习算法如此重要。我们都知道,这些算法近年来在从数据中发现极其复杂的模式方面取得了令人难以置信的成果。但它们的主要局限性在于,它们只能分析和发现输入数据中存在的有意义的模式。如果数据不正确、不完整,或缺少重要特征,算法将无法正确执行。

另一方面,我们人类往往能轻松地理解更广泛的背景并看到更宏观的全貌。例如,如果你被要求分析客户流失,在查看现有数据之前,你就已经可以预期数据中会包含一些描述客户属性的特征,比如人口统计信息、订阅的服务或产品以及订阅日期等。一旦我们收集到数据,我们可以突出那些我们认为重要但数据集中缺失的特征。这就是为什么数据科学家需要凭借他们的专业知识和经验,思考哪些附加信息能够帮助算法理解并从丰富的数据中发现更有意义的模式。事不宜迟,让我们深入探讨。

合并数据集

大多数组织将其数据存储在数据存储中,例如数据库、数据仓库或数据湖。信息的流动可以来自不同的系统或工具。大多数时候,数据存储在由多个表组成的关系型数据库中,而不是单一的表,且这些表之间有明确的关系。

例如,一个在线商店可能有多个表来记录平台上所有的购买。一个表可能包含有关现有客户的信息,另一个表可能列出所有现有和过去的目录产品,而第三个表可能包含所有发生的交易,等等。

如果你在为像亚马逊这样的电子商务平台开发产品推荐系统,你可能只得到了交易表的数据。在这种情况下,你可能需要获取每个产品和客户的一些属性,并要求提取这些额外的表格,然后将三个表格合并在一起,最后再构建推荐系统。

让我们看看如何通过一个实际的例子来合并多个数据源:我们在上一章使用的在线零售数据集。我们将添加关于交易是否发生在英国的公共假期的信息。这些附加数据可能帮助模型理解销售和一些公共假期之间是否有相关性,例如圣诞节或女王生日,这在像澳大利亚这样的国家是一个假期。

注意

英国的公共假期列表将从此网站中提取:packt.live/2twsFVR

首先,我们需要将在线零售数据集导入到pandas DataFrame 中:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter12/Dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)
df.head()

你应该得到以下输出。

图 12.01:在线零售数据集的前五行

图 12.1:在线零售数据集的前五行

接下来,我们将把所有英国的公共假期加载到另一个pandas DataFrame 中。从第十章数据集分析中我们知道,这个数据集的记录仅限于 2010 年和 2011 年。因此,我们将提取这两年的公共假期,但需要分两步进行,因为date.nager提供的 API 仅按单一年份进行分割。

首先让我们关注 2010 年:

uk_holidays_2010 = pd.read_csv\
                   ('https://date.nager.at/PublicHoliday/'\
                    'Country/GB/2010/CSV')

我们可以打印它的形状,看看它有多少行和列:

uk_holidays_2010.shape

你应该得到以下输出。

(13, 8)

我们可以看到,那一年有13个公共假期,并且有8个不同的列。

让我们打印这个 DataFrame 的前五行:

uk_holidays_2010.head()

你应该得到以下输出:

图 12.02:英国 2010 年公共假期 DataFrame 的前五行

图 12.2:英国 2010 年公共假期 DataFrame 的前五行

现在我们有了 2010 年的公共假期列表,让我们提取 2011 年的公共假期:

uk_holidays_2011 = pd.read_csv\
                   ('https://date.nager.at/PublicHoliday/'\
                    'Country/GB/2011/CSV')
uk_holidays_2011.shape

你应该得到以下输出。

(15, 8)

2011 年共有15个公共假期。现在我们需要将这两个数据框的记录合并。我们将使用pandas.append()方法,并将结果分配到一个新的数据框中:

uk_holidays = uk_holidays_2010.append(uk_holidays_2011)

让我们检查在合并这两个数据框后,是否得到了正确的行数:

uk_holidays.shape

你应该得到如下输出:

(28, 8)

我们得到了28条记录,正好对应 2010 年和 2011 年的公共假期总数。

为了将两个数据框合并,我们需要确保它们之间至少有一列公共列,也就是说,两个数据框应至少有一列包含相同类型的信息。在我们的例子中,我们将通过Date列和在线零售数据框中的InvoiceDate列进行合并。我们可以看到这两列的数据格式不同:一个是日期格式(yyyy-mm-dd),另一个是日期时间格式(yyyy-mm-dd hh:mm:ss)。

所以,我们需要将InvoiceDate列转换为日期格式(yyyy-mm-dd)。一种方法是将此列转换为文本,然后使用.str.slice()方法提取每个单元格的前 10 个字符。

例如,日期2010-12-01 08:26:00将首先被转换为字符串,然后我们将只保留前 10 个字符,即2010-12-01。我们将这些结果保存到一个新列InvoiceDay中:

df['InvoiceDay'] = df['InvoiceDate'].astype(str)\
                                    .str.slice(stop=10)
df.head()

输出如下:

图 12.03:创建后的前五行

图 12.3:创建InvoiceDay后的前五行

现在,来自在线零售数据框的InvoiceDay列和来自英国公共假期数据框的Date列包含相似的信息,因此我们可以使用pandas.merge()方法将这两个数据框合并在一起。

有多种方法可以将两张表连接在一起:

  • 左连接

  • 右连接

  • 内连接

  • 外连接

左连接

左连接将保留第一个数据框(即在线零售数据集,位于左侧)中的所有行,并将其与第二个数据框(即英国公共假期数据集,位于右侧)中的匹配行连接,如图 12.04所示:

图 12.04:左连接的文氏图

图 12.4:左连接的文氏图

要执行左连接,我们需要在.merge()方法中指定以下参数:

  • how = 'left'表示进行左连接

  • left_on = InvoiceDay 用于指定从左侧(这里是在线零售数据框中的InvoiceDay列)合并的列

  • right_on = Date 用于指定从右侧(这里是英国公共假期数据框中的Date列)合并的列

这些参数如下面的代码片段所示,已被组合在一起:

df_left = pd.merge(df, uk_holidays, left_on='InvoiceDay', \
                   right_on='Date', how='left')
df_left.shape

你应该得到如下输出:

(541909, 17)

我们得到了与原始在线零售数据框完全相同的行数,这是左连接所期望的结果。让我们看看前五行:

df_left.head()

你应该得到以下输出:

图 12.05:左连接合并后的前五行数据

图 12.5:左连接合并后的前五行数据

我们可以看到,来自公共假期数据框的八列已经与原始数据框合并。如果第二个数据框(在这种情况下是公共假期数据框)没有匹配的行,pandas 将用缺失值(NaTNaN)填充所有单元格,如图 12.05所示。

右连接

右连接与左连接类似,不同之处在于它会保留第二个数据框(右侧)的所有行,并尝试将其与第一个数据框(左侧)进行匹配,如图 12.06所示:

图 12.06:右连接的韦恩图

图 12.6:右连接的韦恩图

我们只需要指定以下参数:

  • how = 'right'.merge()方法中来执行这种类型的连接。

  • 我们将使用与前一个示例相同的列进行合并,即InvoiceDay(在线零售数据框)和Date(英国公共假期数据框)。

这些参数被组合在一起,如下所示的代码片段:

df_right = df.merge(uk_holidays, left_on='InvoiceDay', \
                    right_on='Date', how='right')
df_right.shape

你应该得到以下输出:

(9602, 17)

我们可以看到,右连接的结果行数较少,但它并没有得到与公共假期数据框相同的行数。这是因为在线零售数据框中有多行与公共假期数据框中的单一日期匹配。

例如,查看合并后的数据框的前几行,我们可以看到 2011 年 1 月 4 日有多次购买,因此所有这些购买都已与相应的公共假期匹配。看看以下代码片段:

df_right.head()

你应该得到以下输出:

图 12.07:右连接合并后的前五行数据

图 12.7:右连接合并后的前五行数据

还有两种其他类型的合并:内连接和外连接。

内连接将只保留两个表之间匹配的行:

图 12.08:内连接的韦恩图

图 12.8:内连接的韦恩图

你只需要在.merge()方法中指定how = 'inner'参数。

这些参数被组合在一起,如下所示的代码片段:

df_inner = df.merge(uk_holidays, left_on='InvoiceDay', \
                    right_on='Date', how='inner')
df_inner.shape

你应该得到以下输出:

(9579, 17)

我们可以看到,在英国有 9,579 条观察记录发生在公共假期期间。

外连接将保留两个表中的所有行(匹配的和不匹配的),如图 12.09所示:

图 12.09:外连接的韦恩图

图 12.9:外连接的韦恩图

如你所猜测的那样,你只需要在.merge()方法中指定how == 'outer'参数:

df_outer = df.merge(uk_holidays, left_on='InvoiceDay', \
                    right_on='Date', how='outer')
df_outer.shape

你应该得到以下输出:

(541932, 17)

在合并两张表之前,了解你的重点非常重要。如果你的目标是通过添加另一个数据集的列来扩展原始数据集的特征数量,那么你可能会使用左连接或右连接。但请注意,由于两个表之间可能存在多重匹配,你可能会得到更多的观测值。另一方面,如果你对两张表之间的匹配或不匹配的观测值感兴趣,那么你将使用内连接或外连接。

练习 12.01:将 ATO 数据集与 Postcode 数据集合并

在这个练习中,我们将把 ATO 数据集(28 列)与 Postcode 数据集(150 列)合并,以得到一个列数更多的丰富数据集。

注意

澳大利亚税务局(ATO)数据集可以在 Packt GitHub 库中找到:packt.live/39B146q

Postcode 数据集可以在这里找到:packt.live/2sHAPLc

数据集的来源如下:

澳大利亚税务局ATO):packt.live/361i1p3

Postcode 数据集:packt.live/2umIn6u

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本。

  2. 现在,开始导入pandas包:

    import pandas as pd
    
  3. 将 ATO 数据集的链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter12/Dataset/taxstats2015.csv'
    
  4. 使用pandas包中的.read_csv()方法,将数据集加载到一个名为df的新 DataFrame 中:

    df = pd.read_csv(file_url)
    
  5. 使用.shape属性显示此 DataFrame 的维度:

    df.shape
    

    你应该得到以下输出:

    (2473, 28)
    

    ATO 数据集包含2471行和28列。

  6. 使用.head()方法显示 ATO DataFrame 的前五行:

    df.head()
    

    你应该得到以下输出:

    图 12.10:ATO 数据集的前五行

    图 12.10:ATO 数据集的前五行

    两个 DataFrame 都有一个名为Postcode的列,包含邮政编码,因此我们将使用它来合并它们。

    注意

    Postcode 是澳大利亚用于表示邮政编码的名称,是邮政区域的标识符。

    我们希望了解每个邮政编码的更多信息。让我们确保它们在该数据集中都是唯一的。

  7. 使用.nunique()方法显示Postcode变量的唯一值数量:

    df['Postcode'].nunique()
    

    你应该得到以下输出:

    2473
    

    该列中有2473个唯一值,且 DataFrame 有2473行,因此我们可以确认Postcode变量只包含唯一值。

  8. 现在,将第二个 Postcode 数据集的链接赋值给一个名为postcode_df的变量:

    postcode_url = 'https://github.com/PacktWorkshops/'\
                   'The-Data-Science-Workshop/blob/'\
                   'master/Chapter12/Dataset/'\
                   'taxstats2016individual06taxablestatusstate'\
                   'territorypostcodetaxableincome%20(2).xlsx?'\
                   'raw=true'
    
  9. 使用.read_excel()方法将第二个 Postcode 数据集加载到一个名为postcode_df的新 DataFrame 中。

    我们只加载 Individuals Table 6B 表单,因为数据就在这里,所以我们需要将此名称提供给 sheet_name 参数。此外,此电子表格中包含变量名称的标题行位于第三行,因此我们需要将其指定给 header 参数。

    postcode_df = pd.read_excel(postcode_url, \
                                sheet_name='Individuals Table 6B', \
                                header=2)
    
  10. 使用 .shape 属性打印 postcode_df 的维度:

    postcode_df.shape
    

    你应该得到以下输出:

    (2567, 150)
    

    该 DataFrame 包含 2567 行和 150 列。通过与 ATO 数据集的合并,我们将获得每个邮政编码的附加信息。

  11. 使用 .head() 方法打印 postcode_df 的前五行:

    postcode_df.head()
    

    你应该得到以下输出:

    图 12.11:邮政编码数据集的前五行

    图 12.11:邮政编码数据集的前五行

    我们可以看到第二列包含邮政编码值,这就是我们用来与 ATO 数据集合并的列。让我们检查一下它们是否唯一。

  12. 使用 .nunique() 方法打印该列中唯一值的数量,如下代码片段所示:

    postcode_df['Postcode'].nunique()
    

    你应该得到以下输出:

    2567
    

    2567 个唯一值,这恰好对应于此 DataFrame 的行数,因此我们可以完全确定这一列包含唯一值。这也意味着,在合并两个表后,将会是一对一的匹配。我们不会遇到某个数据集中的多行与另一个数据集的单行匹配的情况。例如,ATO 数据集中的邮政编码 2029 将会在第二个邮政编码 DataFrame 中恰好匹配一行。

  13. 使用 .merge() 方法对两个 DataFrame 执行左连接,并将结果保存到一个名为 merged_df 的新 DataFrame 中。指定 how='left'on='Postcode' 参数:

    merged_df = pd.merge(df, postcode_df, \
                         how='left', on='Postcode')
    
  14. 使用 .shape 属性打印新合并的 DataFrame 的维度:

    merged_df.shape
    

    你应该得到以下输出:

    (2473, 177)
    

    合并后我们得到了正好 2473 行,这是我们期望的结果,因为我们使用了左连接,并且两个原始 DataFrame 中的 Postcode 列存在一对一的匹配。此外,我们现在有了 177 列,这也是本次练习的目标。但在得出结论之前,我们想看看两个数据集之间是否有不匹配的邮政编码。为此,我们将查看来自右侧 DataFrame(邮政编码数据集)的一列,看看是否有任何缺失值。

  15. 通过结合 .isna().sum() 方法打印 'State/Territory1' 列中缺失值的总数:

    merged_df['State/ Territory1'].isna().sum()
    

    你应该得到以下输出:

    4
    

    ATO 数据集中有四个邮政编码未与邮政编码代码匹配。

    让我们看看它们是什么。

  16. 使用 .iloc() 方法打印缺失的邮政编码,如下代码片段所示:

    merged_df.loc[merged_df['State/ Territory1'].isna(), \
                  'Postcode']
    

    你应该得到以下输出:

    图 12.12:不匹配的邮政编码列表

图 12.12:不匹配的邮政编码列表

Postcode 数据集中的缺失邮政编码有3010446260686758。在实际项目中,你需要联系你的利益相关者或数据团队,看看是否能获取这些数据。

我们已经成功地合并了两个感兴趣的数据集,并将特征数量从28扩展到了177。现在,我们有了一个更丰富的数据集,可以对其进行更详细的分析。

注意

要访问这一特定部分的源代码,请参考packt.live/324bV67

你也可以在网上运行这个例子,链接:packt.live/2CDYv80

在下一个主题中,你将学习分箱变量。

分箱变量

如前所述,特征工程不仅仅是获取数据集中没有的信息。很多时候,你需要从现有特征中创建新的特征。一个例子就是将现有列的值合并成一个新的值列表。

例如,你的数据集中某些分类列可能包含非常多的唯一值,比如每个变量超过 1,000 个值。这实际上是非常庞大的信息量,需要额外的计算能力才能让算法处理并从中学习模式。如果你使用的是云计算服务,这可能会对项目成本产生重大影响,或者会延迟项目的交付时间。

一种可能的解决方案是不使用这些列并将其删除,但在这种情况下,你可能会失去一些对业务非常重要和关键的信息。另一种解决方案是通过减少唯一值的数量,创建这些列的一个更简化版本,比如将数量减少到 100 个。这样可以大大加快算法的训练过程,同时不会丢失太多信息。这种转化方法称为分箱(binning),传统上它是指数值变量,但同样的逻辑也可以应用于分类变量。

让我们看看如何在“在线零售”数据集上实现这一点。首先,我们需要加载数据:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter12/Dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

第十章数据集分析中,我们了解到Country列包含38个不同的唯一值:

df['Country'].unique()

你应该得到以下输出:

![图 12.13:Country 列的唯一值列表]

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_12_13.jpg)

图 12.13:Country 列的唯一值列表

我们将把一些国家分组到不同的地区,比如亚洲、中东和美洲。我们会保持欧洲国家不变。

首先,让我们通过复制Country列来创建一个新的列,命名为Country_bin

df['Country_bin'] = df['Country']

然后,我们将创建一个名为asian_countries的列表,包含Country列中唯一值列表中的亚洲国家名称:

asian_countries = ['Japan', 'Hong Kong', 'Singapore']

最后,使用pandas.loc().isin()方法,我们将把Country_bin的值更改为Asia,适用于asian_countries列表中所有的国家:

df.loc[df['Country'].isin(asian_countries), \
       'Country_bin'] = 'Asia'

现在,如果我们打印这个新列的唯一值列表,我们会看到三个亚洲国家(JapanHong KongSingapore)已经被替换为Asia

df['Country_bin'].unique()

你应该会得到以下输出:

图 12.14:列的唯一值列表对亚洲国家进行分箱后

图 12.14:对亚洲国家进行分箱后的Country_bin列唯一值列表

让我们对中东国家执行相同的操作:

m_east_countries = ['Israel', 'Bahrain', 'Lebanon', \
                    'United Arab Emirates', 'Saudi Arabia']
df.loc[df['Country'].isin(m_east_countries), \
       'Country_bin'] = 'Middle East'
df['Country_bin'].unique()

你应该会得到以下输出:

图 12.15:对列进行分箱后的唯一值列表对中东国家进行分箱

图 12.15:对中东国家进行分箱后的Country_bin列唯一值列表

最后,让我们将所有来自北美和南美的国家归为一组:

american_countries = ['Canada', 'Brazil', 'USA']
df.loc[df['Country'].isin(american_countries), \
       'Country_bin'] = 'America'
df['Country_bin'].unique()

你应该会得到以下输出:

图 12.16:对列进行分箱后的唯一值列表来自北美和南美的国家

图 12.16:对北美和南美国家进行分箱后的Country_bin列唯一值列表

df['Country_bin'].nunique()

你应该会得到以下输出:

30

30Country_bin列的唯一值数量。所以我们将该列的唯一值从38减少到30

我们刚刚看到如何将分类值分组在一起,但同样的过程也可以应用于数值值。例如,将人的年龄分为不同的区间,如 20 岁(20 到 29 岁)、30 岁(30 到 39 岁)等,这是很常见的做法。

看一下练习 12.02从 AMES Housing 数据集中对 YearBuilt 变量进行分箱

练习 12.02:从 AMES Housing 数据集中对 YearBuilt 变量进行分箱

在这个练习中,我们将通过对现有的数值列进行分箱,来创建一个新特征,从而将唯一值的数量从112减少到15

注意

我们将在此练习中使用的数据集是 Ames Housing 数据集,可以在我们的 GitHub 仓库中找到:packt.live/35r2ahN

这个数据集是由 Dean De Cock 编制的:packt.live/2uojqHR

该数据集包含 2010 年至 2016 年期间,爱荷华州阿姆斯市的住宅房屋销售清单。

有关每个变量的更多信息可以在这里找到:packt.live/2sT88L4

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandasaltair包:

    import pandas as pd
    import altair as alt
    
  3. 将数据集链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter12/Dataset/ames_iowa_housing.csv'
    
  4. 使用pandas包的.read_csv()方法,将数据集加载到一个新的 DataFrame 中,命名为df

    df = pd.read_csv(file_url)
    
  5. 使用.head()方法显示前五行:

    df.head()
    

    你应该会得到以下输出:

    图 12.17:AMES 房屋数据框的前五行

    图 12.17:AMES housing DataFrame 的前五行

  6. 使用 .nunique() 显示列中的独特值数量:

    df['YearBuilt'].nunique()
    

    你应该得到以下输出:

    112
    

    YearBuilt 列中有 112 个不同或独特的值:

  7. 使用 altair 打印一个散点图,来可视化每年建筑的记录数量。在 .encode() 方法中指定 YearBuilt:O 作为 x 轴,count() 作为 y 轴:

    alt.Chart(df).mark_circle().encode(alt.X('YearBuilt:O'),\
                                       y='count()')
    

    你应该得到以下输出:

    图 12.18:AMES housing DataFrame 的前五行

    图 12.18:AMES housing DataFrame 的前五行

    注意

    由于 GitHub 的限制,输出未显示。如果你在 Colab 文件中运行,图形将会显示。

    在某些年份中,出售的房产不多。因此,你可以按十年(10 年一组)进行分组。

  8. 创建一个名为 year_built 的列表,包含 YearBuilt 列中的所有独特值:

    year_built = df['YearBuilt'].unique()
    
  9. 创建另一个列表,用于计算 year_built 中每年对应的十年。使用列表推导遍历每一年并应用以下公式:year - (year % 10)

    例如,应用此公式于 2015 年,将得到 2015 - (2015 % 10),即 2015 - 5 等于 2010。

    decade_list = [year - (year % 10) for year in year_built]
    
  10. 创建一个包含 decade_list 中所有独特值的排序列表,并将结果保存在一个名为 decade_built 的新变量中。为此,将 decade_list 转换为集合(这将排除所有重复项),然后使用 sorted() 函数,如以下代码片段所示:

    decade_built = sorted(set(decade_list))
    
  11. 打印 decade_built 的值:

    decade_built
    

    你应该得到以下输出:

    图 12.19:十年列表

    图 12.19:十年列表

    现在我们有了将 YearBuilt 列进行分组的十年列表。

  12. df DataFrame 中创建一个名为 DecadeBuilt 的新列,将 YearBuilt 中的每个值按十年分组。你将使用 pandas.cut() 方法,并指定 bins=decade_built 参数:

    df['DecadeBuilt'] = pd.cut(df['YearBuilt'], \
                               bins=decade_built)
    
  13. 打印 DataFrame 的前五行,仅显示 'YearBuilt''DecadeBuilt' 列:

    df[['YearBuilt', 'DecadeBuilt']].head()
    

    你应该得到以下输出:

    图 12.20:分组后的前五行

图 12.20:分组后的前五行

我们可以看到每年已正确地分配到了相关的十年。

注意

要访问此部分的源代码,请参考 packt.live/3kXnC7c

本节目前没有在线互动示例,但可以在 Google Colab 上照常运行。

我们已经通过将 YearBuilt 列的值按十年分组,成功创建了一个新特征。我们将独特值的数量从 112 减少到 15

操作日期

在你将要处理的大多数数据集中,会有一列或多列包含日期信息。通常,你不会将此类信息直接作为输入提供给机器学习算法。原因是你不希望它学习到过于特定的模式,例如顾客 A 在 2012 年 8 月 3 日上午 08:11 购买了产品 X。如果这样,模型将会过拟合,无法对未来数据进行泛化。

你真正希望的是模型学习到一些模式,例如,带小孩的顾客倾向于在 12 月购买独角兽玩具。与其提供原始日期,你希望提取一些周期性的特征,如年份的月份、星期几等。我们将在本节中看到,使用 pandas 包提取这些信息是多么简单。

注意

这个经验法则有一个例外。如果你正在进行时间序列分析,这种算法需要一个日期列作为输入特征,但这超出了本书的范围。

第十章分析数据集 中,你已了解了 pandas 中数据类型的概念。那时,我们主要关注的是数值变量和类别变量,但还有一个重要的数据类型:datetime。让我们再次查看在线零售数据集中每列的类型:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter12/Dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)
df.dtypes

你应该得到以下输出:

图 12.21:在线零售数据集中变量的数据类型

图 12.21:在线零售数据集中变量的数据类型

我们可以看到,pandas 自动检测到 InvoiceDatedatetime 类型。但对于一些其他数据集,它可能无法正确识别日期。在这种情况下,你需要使用 .to_datetime() 方法手动转换它们:

df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])

一旦列被转换为 datetime 类型,pandas 提供了许多属性和方法来提取与时间相关的信息。例如,如果你想获取某个日期的年份,可以使用 .dt.year 属性:

df['InvoiceDate'].dt.year

你应该得到以下输出:

图 12.22:为每行提取的  列的年份

图 12.22:为每行提取的 InvoiceDate 列的年份

如你所料,提取日期的月和日的属性分别是 .dt.month.dt.day。你可以使用 .dt.dayofweek 属性从日期中获取星期几:

df['InvoiceDate'].dt.dayofweek

你应该得到以下输出。

图 12.23:为每行提取的  列的星期几

图 12.23:为每行提取的 InvoiceDate 列的星期几

注意

你可以在这里找到所有可用属性的完整列表:packt.live/2ZUe02R

对于 datetime 列,你还可以进行一些数学运算。例如,我们可以通过使用 pandas 的时间序列偏移对象 pd.tseries.offsets.Day(3),为每个日期添加 3 天:

df['InvoiceDate'] + pd.tseries.offsets.Day(3)

您应该得到以下输出:

图 12.24:InvoiceDate 列偏移了三天

图 12.24:InvoiceDate 列偏移了三天

您还可以使用 pd.tseries.offsets.BusinessDay() 按工作日偏移天数。例如,如果我们想获得前一个工作日,可以这样做:

df['InvoiceDate'] + pd.tseries.offsets.BusinessDay(-1)

您应该得到以下输出:

图 12.25:InvoiceDate 列偏移了 -1 个工作日

图 12.25:InvoiceDate 列偏移了 -1 个工作日

另一个有趣的日期操作是使用 pd.Timedelta() 应用特定的时间频率。例如,如果您想从某个日期获取该月的第一天,可以这样做:

df['InvoiceDate'] + pd.Timedelta(1, unit='MS')

您应该得到以下输出:

图 12.26:InvoiceDate 列转换为月初

图 12.26:InvoiceDate 列转换为月初

如您在本节中所见,pandas 包提供了许多不同的 API 来操作日期。您已经学会了如何使用其中一些最常用的 API。现在,您可以自行探索其他的 API。

练习 12.03:金融服务消费者投诉的日期操作

在本次练习中,我们将学习如何使用 pandas 从两个现有的日期列中提取与时间相关的信息,并创建六个新列:

注意

我们将在本次练习中使用的数据集是金融服务客户投诉数据集,您可以在我们的 GitHub 仓库中找到它:packt.live/2ZYm9Dp

原始数据集可以在这里找到:packt.live/35mFhMw

  1. 打开一个新的 Colab 笔记本。

  2. 导入 pandas 包:

    import pandas as pd
    
  3. 将数据集的链接赋值给一个名为 file_url 的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter12/Dataset/Consumer_Complaints.csv'
    
  4. 使用 pandas 包中的 .read_csv() 方法将数据集加载到一个名为 df 的新数据框中:

    df = pd.read_csv(file_url)
    
  5. 使用 .head() 方法显示前五行:

    df.head()
    

    您应该得到以下输出:

    图 12.27:客户投诉数据框的前五行

    图 12.27:客户投诉数据框的前五行

  6. 使用 .dtypes 属性打印每一列的数据类型:

    df.dtypes
    

    您应该得到以下输出:

    图 12.28:客户投诉数据框的各列数据类型

    图 12.28:客户投诉数据框的各列数据类型

    Date receivedDate sent to company 列尚未被识别为日期时间格式,因此我们需要手动将它们转换为日期时间。

  7. 使用 pd.to_datetime() 方法将 Date receivedDate sent to company 列转换为日期时间格式:

    df['Date received'] = pd.to_datetime(df['Date received'])
    df['Date sent to company'] = pd.to_datetime\
                                 (df['Date sent to company'])
    
  8. 使用 .dtypes 属性打印每一列的数据类型:

    df.dtypes
    

    您应该得到以下输出:

    图 12.29:客户投诉数据框在转换后的数据类型

    图 12.29:客户投诉数据框在转换后的数据类型

    现在这两列的数据类型已经正确。接下来,让我们从这两列日期中创建一些新特征。

  9. 创建一个名为YearReceived的新列,其中将使用.dt.year属性提取Date Received列中的年份:

    df['YearReceived'] = df['Date received'].dt.year
    
  10. 创建一个名为MonthReceived的新列,其中将使用.dt.month属性提取每个日期的月份:

    df['MonthReceived'] = df['Date received'].dt.month
    
  11. 创建一个名为DayReceived的新列,其中将使用.dt.day属性提取每个日期的天:

    df['DomReceived'] = df['Date received'].dt.day
    
  12. 创建一个名为DowReceived的新列,其中将使用.dt.dayofweek属性提取每个日期的星期几:

    df['DowReceived'] = df['Date received'].dt.dayofweek
    
  13. 使用.head()方法显示前五行:

    df.head()
    

    你应该得到以下输出:

    图 12.30:客户投诉数据框的前五行    创建了四个新特征后

    图 12.30:在创建四个新特征后,客户投诉数据框的前五行

    我们可以看到,我们成功地创建了四个新特征:YearReceivedMonthReceivedDayReceivedDowReceived。现在让我们创建另一个特征,指示日期是否在周末。

  14. 创建一个名为IsWeekendReceived的新列,其中将包含二进制值,指示DowReceived列的值是否大于或等于50代表星期一,56分别代表星期六和星期日):

    df['IsWeekendReceived'] = df['DowReceived'] >= 5
    
  15. 使用.head()方法显示前5行:

    df.head()
    

    你应该得到以下输出:

    图 12.31:客户投诉数据框的前五行    创建周末特征后

    图 12.31:在创建周末特征后,客户投诉数据框的前五行

    我们已经创建了一个新特征,表明每个投诉是否是在周末收到的。接下来,我们将创建一个新列,表示Date sent to companyDate received之间的天数差。

  16. 创建一个名为RoutingDays的新列,其中将包含Date sent to companyDate received之间的差值:

    df['RoutingDays'] = df['Date sent to company'] \
                        - df['Date received']
    
  17. 使用.dtype属性打印新列'RoutingDays'的数据类型:

    df['RoutingDays'].dtype
    

    你应该得到以下输出:

    图 12.32:列的数据类型

    图 12.32:RoutingDays列的数据类型

    从两个日期时间列相减的结果是一个新的日期时间列(dtype('<M8[ns]'),这是一种特定的日期时间类型,属于numpy包)。我们需要将该数据类型转换为int,以便计算这两天之间的天数。

  18. 使用.dt.days属性转换RoutingDays列:

    df['RoutingDays'] = df['RoutingDays'].dt.days
    
  19. 使用.head()方法显示前五行:

    df.head()
    

    你应该得到以下输出:

    图 12.33:在创建后,客户投诉数据框的前五行

图 12.33:在创建RoutingDays后,客户投诉数据框的前五行

在这个练习中,你将实践不同的技术,从真实世界的数据集中基于日期时间列进行特征工程,生成新的变量。通过 Date sent to companyDate received 两列,你成功创建了六个新特征,这些特征将提供额外的有价值信息。

注意

若要访问此特定部分的源代码,请参考 packt.live/3kWvhmf

你也可以在网上运行这个示例:packt.live/316YT8z

例如,我们能够发现一些模式,比如投诉数量通常在十一月或星期五较高。我们还发现,当投诉在周末收到时,处理投诉需要更多时间,这可能是由于那时工作人员有限。

执行数据聚合

好的,我们已经接近本章的结尾了。但在结束之前,还有一个要探索的技术:数据聚合。其背后的想法是,通过另一列中的特定分组来总结某个数值列。我们已经看到如何使用 .pivot_table() 方法在 第五章《进行第一次聚类分析》中,对 ATO 数据集中的两个数值变量(平均净税和平均总扣除)进行聚合。但那时,我们进行数据聚合并非为了创建新特征,而是为了理解这些聚类之间的差异。

你可能会好奇,在什么情况下你会想使用数据聚合进行特征工程。如果你已经有一个数值列,其中包含每个记录的值,为什么还需要对其进行汇总,并将这些信息添加回 DataFrame 中呢?这似乎是在添加相同的信息,只是没有那么详细。但其实,使用这种技术有多个合理的原因。

一个潜在的原因可能是你希望通过这种聚合来规范化另一个数值列。例如,如果你正在处理一个零售商的数据集,包含全球每个门店的所有销售数据,不同国家的销售量可能会有很大差异,因为它们的人口不同。在这种情况下,与其使用每个门店的原始销售数字,你可以计算一个比例(或百分比),即某个门店的销售额除以该国家的总销售额。通过这个新的比例特征,那些看起来因为销售额较低而表现不佳的门店,实际上可能在该国的平均水平上表现更好。

pandas 中,进行数据聚合非常简单。我们只需要依次结合以下方法:.groupby().agg()

我们需要将需要分组的列名列表提供给 .groupby() 方法。如果你熟悉 Excel 透视表,这相当于 Rows 字段。

.agg() 方法期望一个字典,其中列名作为键,聚合函数作为值,例如 {'column_name': 'aggregation_function'}。在 Excel 透视表中,聚合后的列称为 values

让我们看看如何在在线零售数据集上实现这个操作。首先,我们需要导入数据:

import pandas as pd
file_url = 'https://github.com/PacktWorkshops/'\
           'The-Data-Science-Workshop/blob/'\
           'master/Chapter12/Dataset/'\
           'Online%20Retail.xlsx?raw=true'
df = pd.read_excel(file_url)

让我们计算每个国家的总销售数量。我们将 Country 列作为分组列:

df.groupby('Country').agg({'Quantity': 'sum'})

你应该得到以下输出:

图 12.34:每个国家的数量总和(截断)

图 12.34:每个国家的数量总和(截断)

这个结果给出了每个国家销售的物品总量。我们可以看到,澳大利亚的销售量几乎是比利时的四倍。这种层级的信息可能太过粗略,我们可能需要更多的细节。让我们进行相同的聚合,但这次我们将根据两列进行分组:CountryStockCode。我们只需要将这些列的名称作为列表传递给 .groupby() 方法:

df.groupby(['Country', 'StockCode']).agg({'Quantity': 'sum'})

你应该得到以下输出:

图 12.35:每个国家和 StockCode 的数量总和

图 12.35:每个国家和 StockCode 的数量总和

我们可以看到每个国家的销售数量。可以注意到,澳大利亚对产品 206752067620677 销售数量相同(每个 216 件)。这可能表示这些产品总是一起销售。

我们可以添加更多的信息层级,获取每个国家、每个产品和每个日期的销售数量。为此,我们首先需要创建一个新特征,从 InvoiceDate 中提取日期组件(我们在上一节中刚刚学到如何做这件事):

df['Invoice_Date'] = df['InvoiceDate'].dt.date

然后,我们可以在 .groupby() 方法中添加这个新列:

df.groupby(['Country', 'StockCode', \
            'Invoice_Date']).agg({'Quantity': 'sum'})

你应该得到以下输出:

图 12.36:每个国家、StockCode 和 Invoice_Date 的数量总和

图 12.36:每个国家、StockCode 和 Invoice_Date 的数量总和

我们已经生成了一个新的数据框,其中包含每个国家、物品 ID 和日期的总销售数量。我们可以看到 StockCode 150362011-05-17Australia 非常畅销,共售出了 600 件。而另一方面,在 2011-03-24StockCode 20665Australia 只售出了 6 件。

现在,我们可以将这些额外的信息合并回原始数据框中。但是在此之前,需要执行一个额外的数据转换步骤:重置列索引。pandas 包默认在数据聚合后创建一个多级索引。你可以把它想象成列名被存储在多行中,而不是仅存储在一行中。要将其更改回单级索引,你需要调用 .reset_index() 方法:

df_agg = df.groupby(['Country', 'StockCode', 'Invoice_Date'])\
           .agg({'Quantity': 'sum'}).reset_index()
df_agg.head()

你应该得到以下输出:

图 12.37:包含数据汇总信息的 DataFrame

图 12.37:包含数据汇总信息的 DataFrame

现在我们可以使用本章前面提到的.merge()方法,将这个新的 DataFrame 合并到原始的 DataFrame 中:

df_merged = pd.merge(df, df_agg, how='left', \
                     on = ['Country', 'StockCode', \
                           'Invoice_Date'])
df_merged

你应该得到以下输出:

图 12.38:合并后的 DataFrame(已截断)

图 12.38:合并后的 DataFrame(已截断)

我们可以看到,原来的Quantity列被替换成了Quantity_xQuantity_y

原因是,在合并后,出现了两个具有完全相同名称的不同列(Quantity),因此默认情况下,pandas 添加了后缀以区分它们。

我们可以通过合并前替换其中一列的名称,或者合并后替换两列的名称来解决这个问题。要替换列名,我们可以使用pandas.rename()方法,提供一个字典,其中旧名称为键,新名称为值,例如{'old_name': 'new_name'}

合并后,我们将列名替换为QuantityDailyQuantity

df_merged.rename(columns={"Quantity_x": "Quantity", \
                          "Quantity_y": "DailyQuantity"}, \
                 inplace=True)
df_merged

你应该得到以下输出:

图 12.39:重命名后的 DataFrame(已截断)

图 12.39:重命名后的 DataFrame(已截断)

现在我们可以创建一个新特征,用于计算每个国家中售出物品的数量与该国家每日总售出数量之间的比例:

df_merged['QuantityRatio'] = df_merged['Quantity'] \
                             / df_merged['DailyQuantity']
df_merged

你应该得到以下输出:

图 12.40:包含新特征的最终 DataFrame

图 12.40:包含新QuantityRatio特征的最终 DataFrame

在这一部分中,我们学习了如何通过数据汇总,帮助我们通过计算每个感兴趣的分组的比率或百分比来创建新特征。看第一行和第二行,我们可以看到在StockCode交易84123A71053中,卖出了6个商品。但是如果我们看一下新创建的DailyQuantity列,我们会发现StockCode 84123A更受欢迎:在那一天(2010-12-01),该店售出了45484123A,但仅售出了3371053QuantityRatio显示,第三笔交易卖出了StockCode 84406B8个商品,这笔单笔交易占当天该商品销售量的 20%。通过进行数据汇总,我们为每一条记录获得了额外的信息,并且将数据集中的原始信息放到了一个新的视角中。

练习 12.04:使用数据汇总进行特征工程,基于 AMES 房价数据集

在本次练习中,我们将通过数据汇总创建新的特征。首先,我们将计算每个街区的最大SalePriceLotArea,并按YrSold分组。然后,我们将这些信息添加回数据集,最后,我们将计算每个房产销售量与这两个最大值的比率:

注意

本次练习中我们将使用的数据库是 Ames Housing 数据库,可以在我们的 GitHub 仓库中找到:packt.live/35r2ahN

  1. 打开一个新的 Colab 笔记本。

  2. 导入pandasaltair包:

    import pandas as pd
    
  3. 将数据集的链接赋值给一个名为file_url的变量:

    file_url = 'https://raw.githubusercontent.com/'\
               'PacktWorkshops/The-Data-Science-Workshop/'\
               'master/Chapter12/Dataset/ames_iowa_housing.csv'
    
  4. 使用pandas包的.read_csv()方法,将数据集加载到一个名为df的新数据框中:

    df = pd.read_csv(file_url)
    
  5. 使用.groupby.agg()方法进行数据聚合,找出每个NeighborhoodYrSold的最大SalePrice,并将结果保存到一个名为df_agg的新数据框中:

    df_agg = df.groupby(['Neighborhood', 'YrSold'])\
               .agg({'SalePrice': 'max'}).reset_index()
    
  6. df_agg的列名重命名为NeighborhoodYrSoldSalePriceMax

    df_agg.columns = ['Neighborhood', 'YrSold', 'SalePriceMax']
    
  7. 打印出df_agg的前五行:

    df_agg.head()
    

    你应该得到以下输出:

    图 12.41:聚合后的数据框前五行

    图 12.41:聚合后的数据框前五行

  8. 使用merge()方法,通过对NeighborhoodYrSold列进行左连接(how='left'),将原始数据框dfdf_agg合并,并将结果保存到一个名为df_new的新数据框中:

    df_new = pd.merge(df, df_agg, how='left', \
                      on=['Neighborhood', 'YrSold'])
    
  9. 打印出df_new的前五行:

    df_new.head()
    

    你应该得到以下输出:

    图 12.42:的前五行

    图 12.42:df_new的前五行

    请注意,我们显示的是输出的最后八列。

  10. 创建一个名为SalePriceRatio的新列,将SalePrice除以SalePriceMax

    df_new['SalePriceRatio'] = df_new['SalePrice'] \
                               / df_new['SalePriceMax']
    
  11. 打印出df_new的前五行:

    df_new.head()
    

    你应该得到以下输出:

    图 12.43:特征工程后的前五行

    图 12.43:特征工程后df_new的前五行

    请注意,我们显示的是输出的最后八列。

  12. 使用.groupby.agg()方法进行数据聚合,找出每个NeighborhoodYrSold的最大LotArea,并将结果保存到一个名为df_agg2的新数据框中:

    df_agg2 = df.groupby(['Neighborhood', 'YrSold'])\
                .agg({'LotArea': 'max'}).reset_index()
    
  13. df_agg2的列名重命名为NeighborhoodYrSoldLotAreaMax,并打印出前五列:

    df_agg2.columns = ['Neighborhood', 'YrSold', 'LotAreaMax']
    df_agg2.head()
    

    你应该得到以下输出:

    图 12.44:聚合后的数据框前五行

    图 12.44:聚合后的数据框前五行

  14. 使用merge()方法,通过对NeighborhoodYrSold列进行左连接(how='left'),将原始数据框dfdf_agg2合并,并将结果保存到一个名为df_final的新数据框中:

    df_final = pd.merge(df_new, df_agg2, how='left', \
                        on=['Neighborhood', 'YrSold'])
    
  15. 创建一个名为LotAreaRatio的新列,将LotArea除以LotAreaMax

    df_final['LotAreaRatio'] = df_final['LotArea'] \
                               / df_final['LotAreaMax']
    
  16. 打印出df_final的前五行,包含以下列:IdNeighborhoodYrSoldSalePriceSalePriceMaxSalePriceRatioLotAreaLotAreaMaxLotAreaRatio

    df_final[['Id', 'Neighborhood', 'YrSold', 'SalePrice', \
              'SalePriceMax', 'SalePriceRatio', 'LotArea', \
              'LotAreaMax', 'LotAreaRatio']].head()
    

    你应该得到以下输出:

    图 12.45:最终数据框的前五行

图 12.45:最终数据框的前五行

注意

要访问该特定部分的源代码,请参考 packt.live/2Q8a5eU

你还可以在在线环境中运行此示例,网址为 packt.live/2Q8dhXI

就这样。我们刚刚创建了两个新特征,分别表示一个房产的 SalePriceLotArea 与同一年同一社区出售的最大房产的比率。现在我们可以轻松且公正地比较这些房产。例如,从上一步的输出中我们可以看到,第五个房产的大小(Id 5LotArea 14260)几乎与同一地区、同一年售出的最大房产(LotArea 14303)一样接近(LotAreaRatio 0.996994)。但它的售价(SalePrice 250000)显著低于(SalePriceRatio0.714286)最高售价(SalePrice 350000)。这表明房产的其他特征对售价产生了影响。

活动 12.01:财务数据集的特征工程

你在捷克共和国的一家大银行工作,负责分析现有客户的交易数据。数据团队已经从他们的数据库中提取了所有认为对你分析数据集有用的表格。你需要将这些表格中的数据合并成一个单一的 DataFrame,并创建新特征,以获得一个丰富的数据集,从中你可以进行深入的客户银行交易分析。

你将只使用以下四个表格:

  • account: 给定分行的客户银行账户特征

  • client: 与银行客户相关的个人信息

  • disp: 将账户与客户关联的表格

  • trans: 记录每个账户的所有历史交易

    注意

    如果你想了解这些表格的更多信息,可以查看该数据集的数据字典:packt.live/2QSev9F

以下步骤将帮助你完成此任务:

  1. 下载并将此数据集中的不同表格加载到 Python 中。

  2. 使用 .shape.head() 方法分析每个表格。

  3. 根据步骤 2的分析,找到用于合并的表格之间的共同/相似列。

  4. 应该有四个公共表格。使用 pd.merge() 将四个表格合并在一起。

  5. 使用 .rename() 在合并后重命名列名。

  6. 合并后,使用 .duplicated().sum() 检查是否存在重复项。

  7. 使用 .to_datetime() 转换日期列的数据类型。

  8. birth_number 中创建两个单独的特征,以获取每个客户的出生日期和性别。

    注意

    本列中与生日和性别相关的数据编码规则如下:男性的号码为 YYMMDD 格式,女性的号码为 YYMM+50DD 格式,其中 YYMMDD 表示出生日期。

  9. 使用 .isna() 修复数据质量问题。

  10. 创建一个新特征,使用日期操作计算客户开户时的年龄:

    注意

    数据集最初由 Berka, Petr 在 Discovery Challenge PKDD'99 中共享:packt.live/2ZVaG7J.

    你将在这个活动中使用的数据集可以在我们的 GitHub 仓库找到:

    packt.live/2QpUOXC.

    packt.live/36sN2BR.

    packt.live/2MZLzLB.

    packt.live/2rW9hkE.

    CSV 版本可以在这里找到:packt.live/2N150nn.

预期输出:

Figure 12.46: 期望输出与合并行

图 12.46:期望输出与合并行

注意

这个活动的解决方案可以在以下地址找到:packt.live/2GbJloz.

概要

我们首先学习了如何分析数据集并通过数据汇总和数据可视化对其数据进行深入理解。这对于发现数据集的限制和识别数据质量问题非常有用。我们看到如何使用pandas的 API 来处理和修复一些最常见的问题(重复行、类型转换、值替换和缺失值)。

最后,我们介绍了几种特征工程技术。不可能涵盖所有用于创建特征的现有技术。本章的目标是介绍可以显著提高分析质量和模型性能的关键步骤。但是请记住,在数据转换过于激进之前,务必定期与业务或数据工程团队联系以确认。准备数据集并不总是意味着拥有最干净的数据集,而是获得最接近业务感兴趣的真实信息的数据集。否则,您可能会发现不正确或无意义的模式。正如我们所说,伴随着巨大的权力而来的是巨大的责任

下一章将开启本书呈现数据科学应用案例的新篇章。第十三章不平衡数据集,将通过一个不平衡数据集的示例来引导你如何处理这种情况。

第十三章:13. 不平衡数据集

概述

到本章结束时,您将能够识别数据集可能不平衡的用例;制定处理不平衡数据集的策略;在平衡数据集后构建分类模型,例如逻辑回归模型;并分析分类指标以验证所采用的策略是否产生了期望的结果。

在本章中,您将处理非常常见于现实场景中的不平衡数据集。您将使用SMOTEMSMOTE和随机欠采样等技术来解决不平衡数据集问题。

介绍

在前一章节第十二章特征工程中,我们处理与日期相关的数据点,我们正在处理与特征相关的场景。在本章中,我们将处理整体数据集中例子比例构成挑战的情况。

让我们重新审视我们在第三章二元分类中处理的数据集,即针对'否'存款的例子远远超过了'是'的例子,比例为 88%对 12%。我们还确定,导致该数据集上逻辑回归模型表现不佳的原因之一是示例的倾斜比例。像我们在第三章二元分类中分析过的那种数据集,称为不平衡数据集,在真实世界的用例中非常普遍。

我们遇到不平衡数据集的一些用例包括以下情况:

  • 信用卡或保险理赔的欺诈检测

  • 在医疗诊断中,我们必须检测罕见疾病的存在

  • 网络入侵检测

在所有这些用例中,我们可以看到我们真正想要检测的将是少数情况。例如,在医疗诊断罕见疾病的领域,存在罕见疾病的例子甚至可能少于总例子的 1%。不平衡数据集用例的一个固有特征是,如果没有使用正确的度量标准,分类器的质量并不明显。这使得不平衡数据集的问题确实具有挑战性。

在本章中,我们将讨论识别不平衡数据集的策略以及缓解不平衡数据集影响的方法。

理解业务背景

作为数据科学家为一家银行工作,最近业务负责人对您在第三章二元分类中构建的存款倾向模型的结果提出了警告。观察到,大部分被识别为潜在目标市场的客户,最终拒绝了提供的服务。这已经对销售团队的交叉销售和增值销售的业绩指标造成了重大影响。业务团队急需您的帮助来解决这个问题,以达到本季度的销售目标。不过,别担心,这是我们将在本章后期解决的问题。

首先,我们从分析问题开始。

练习 13.01:对数据集上的逻辑回归模型进行基准测试

在本练习中,我们将分析预测客户是否会购买定期存款的问题。为此,你将拟合一个逻辑回归模型,就像在第三章二元分类中做的那样,并且你将仔细观察各项指标:

注意

你将在本练习中使用的数据集可以在我们的 GitHub 仓库找到:packt.live/2twFgIM

  1. 在 Google Colab 中打开一个新的笔记本。

  2. 接下来,import pandas 并从 GitHub 仓库加载数据:

    import pandas as pd
    filename = 'https://raw.githubusercontent.com/PacktWorkshops'\
               '/The-Data-Science-Workshop/master/'\
               'Chapter13/Dataset/bank-full.csv'
    
  3. 现在,使用 pandas 加载数据

    #Loading the data using pandas
    bankData = pd.read_csv(filename,sep=";")
    bankData.head()
    

    你的输出将如下所示:

    图 13.1:bankData 的前五行

    图 13.1:bankData 的前五行

    现在,为了进一步分解数据集,让我们执行一些特征工程步骤。

  4. 通过缩放对数值特征(年龄、余额和时长)进行归一化,这在第三章二元分类中已经讲解过。请输入以下代码:

    from sklearn.preprocessing import RobustScaler
    rob_scaler = RobustScaler()
    

    在上面的代码片段中,我们使用了一个名为 RobustScaler() 的缩放函数来缩放数值数据。RobustScaler() 是一个类似于第三章二元分类中的 MinMaxScaler 的缩放函数。

  5. 在缩放数值数据后,我们将每一列转换为缩放版,如下所示的代码片段:

    # Converting each of the columns to scaled version
    bankData['ageScaled'] = rob_scaler.fit_transform\
                            (bankData['age'].values.reshape(-1,1))
    bankData['balScaled'] = rob_scaler.fit_transform\
                            (bankData['balance']\
                             .values.reshape(-1,1))
    bankData['durScaled'] = rob_scaler.fit_transform\
                            (bankData['duration']\
                             .values.reshape(-1,1))
    
  6. 现在,使用 .drop() 函数删除原始特征,然后引入缩放特征:

    # Dropping the original columns
    bankData.drop(['age','balance','duration'], \
                  axis=1, inplace=True)
    
  7. 使用 .head() 函数显示前五列:

    bankData.head()
    

    输出如下所示:

    图 13.2:带有缩放特征的 bankData

    图 13.2:带有缩放特征的 bankData

    数据集中的分类特征必须通过将其转换为虚拟变量来转换为数值,这在第三章二元分类中已经介绍过。

  8. 使用 .get_dummies() 函数将所有分类变量转换为虚拟变量:

    bankCat = pd.get_dummies(bankData[['job','marital','education',\
                                       'default','housing','loan',\
                                       'contact','month',\
                                       'poutcome']])
    
  9. 分离数值数据并观察其形状:

    bankNum = bankData[['ageScaled','balScaled','day',\
                        'durScaled','campaign','pdays','previous']]
    bankNum.shape
    

    输出将如下所示:

    (45211, 7)
    

    在转换分类变量后,必须将其与数据框的缩放数值结合,以获得特征工程化的数据集。

  10. 从合并的数据集中创建独立变量 X 和依赖变量 Y 以供建模,如下代码片段所示:

    # Merging with the original data frame
    # Preparing the X variables
    X = pd.concat([bankCat, bankNum], axis=1)
    print(X.shape)
    # Preparing the Y variable
    Y = bankData['y']
    print(Y.shape)
    X.head()
    

    输出如下所示:

    图 13.3:独立变量和合并数据(截断)

    图 13.3:独立变量和合并数据(截断)

    我们现在已经准备好进行建模任务。让我们首先导入所需的包。

  11. 现在,从 sklearnimport 所需的 train_test_split()LogisticRegression 函数:

    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LogisticRegression
    
  12. 在拆分函数中将数据分为训练集和测试集,设置 test_size = 0.3。我们还设置了 random_state 来确保代码的可重复性:

    X_train, X_test, y_train, y_test = train_test_split\
                                       (X, Y, test_size=0.3, \
                                        random_state=123)
    
  13. 现在,使用 .fit 在训练数据上拟合模型:

    # Defining the LogisticRegression function
    bankModel = LogisticRegression()
    bankModel.fit(X_train, y_train)
    

    你的输出应如下所示:

    图 13.4:拟合模型

    图 13.4:拟合模型

    现在模型已经拟合,接下来我们对测试集进行预测并生成指标。

  14. 接下来,找到测试集上的预测结果并打印准确度分数:

    pred = bankModel.predict(X_test)
    print('Accuracy of Logistic regression model prediction on '\
          'test set: {:.2f}'\
          .format(bankModel.score(X_test, y_test)))
    

    你应该得到如下输出:

    Accuracy of Logistic regression model prediction on test set: 0.90
    
  15. 现在,使用 confusion_matrix()classification_report() 两个函数生成进一步分析的指标,详细内容将在结果分析部分中讨论:

    # Confusion Matrix for the model
    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到如下输出:

    图 13.5:显示准确度结果及混淆矩阵的指标

图 13.5:显示准确度结果及混淆矩阵的指标

注意

你将得到类似以下的指标。然而,由于建模过程中的变异性,数值会有所不同。

在这次练习中,我们发现了一份报告,可能导致了预期购买定期存款计划的客户数量出现问题。从指标中,我们可以看到 No 的值相对比 Yes 的值要高。

注意

要访问此特定部分的源代码,请参阅 packt.live/3hapnvB

你也可以在线运行这个例子,访问 packt.live/3hh6Xta

为了更好地理解结果偏差的原因,我们将在接下来的部分详细分析这些指标。

结果分析

为了分析在上一部分中获得的结果,让我们将混淆矩阵扩展成以下形式:

图 13.6:结果指标的混淆矩阵

图 13.6:结果指标的混淆矩阵

我们从前一个练习中获得的输出中输入了值 117072911060506。然后我们将这些值按图示方式排列。我们将表示选择定期存款(No)的倾向作为正类,其他作为负类。因此,从混淆矩阵中,我们可以计算出准确度度量,这些内容在第三章二元分类中有介绍。模型的准确度由以下公式给出:

图 13.7:模型的准确度

图 13.7:模型的准确度

在我们的例子中,它将是(11707 + 506)/(11707 + 1060 + 291 + 506),即 90%。

从准确度的角度来看,模型似乎做得不错。然而,现实可能大不相同。为了找出实际情况,我们来看看精确度和召回率值,这些值可以从我们获得的分类报告中得到。任何类别的精确度公式在第三章二元分类中有介绍。

任何类别的精确度值由以下公式给出:

图 13.8:模型的精确度

图 13.8:模型的精确度

在我们的例子中,对于正类,精确度是 TP/(TP + FP),即 11707 / (11707 + 1060),大约为 92%。

对于负类,精度可以写作 TN / (TN + FN),即 506 / (506 + 291),大约为 63%。

类似地,任何类别的召回率都可以表示如下:

图 13.9:召回模型

图 13.9:召回模型

正类的召回率,TP / (TP + FN) = 11707 / (11707 + 291),大约为 98%。

负类的召回率,TN / (TN + FP) = 506 / (506 + 1060),大约为 32%。

召回率表示分类器正确识别各类别的能力。从这些指标中我们可以看到,我们构建的模型在识别正类方面表现良好,但在正确识别负类方面表现很差。

你认为分类器为什么会偏向于某一类?这个问题的答案可以通过查看训练集中各类别的平衡情况来揭示。

以下代码将生成训练数据中各类别的百分比:

print('Percentage of negative class :',\
      (y_train[y_train=='yes'].value_counts()\
       /len(y_train) ) * 100)
print('Percentage of positive class :',\
      (y_train[y_train=='no'].value_counts()\
       /len(y_train) ) * 100)

你应该得到以下输出:

Percentage of negative class: yes    11.764148
Name: y, dtype: float64
Percentage of positive class: no    88.235852
Name: y, dtype: float64

从中我们可以看到,大多数训练集(88%)由正类组成。这种不平衡是我们选择的逻辑回归分类器表现不佳的主要原因之一。

现在,让我们来看一下不平衡数据集的挑战。

不平衡数据集的挑战

从分类器的例子中可以看出,不平衡数据集的最大挑战之一是对多数类的偏向,在上一个例子中这个偏向为 88%。这将导致次优的结果。然而,更具挑战性的是,如果不使用正确的指标,结果会具有欺骗性。

假设我们有一个数据集,其中负类约占 99%,正类约占 1%(例如,用于检测一种罕见疾病的情况)。

看一下下面的代码片段:

Data set Size: 10,000 examples
Negative class : 9910
Positive Class : 90

假设我们有一个差的分类器,它只能预测负类;我们将得到如下的混淆矩阵:

图 13.10:差的分类器的混淆矩阵

图 13.10:差的分类器的混淆矩阵

从混淆矩阵中,我们来计算准确性指标。看一下下面的代码片段:

# Classifier biased to only negative class
Accuracy = (TP + TN ) / ( TP + FP + FN + TN)
 = (0 + 9900) / ( 0 + 0 + 90 + 9900) = 9900/10000
 = 99%

使用这样的分类器,如果我们使用像准确率这样的指标,我们仍然会得到大约 99% 的结果,在正常情况下这看起来很出色。然而,在这种情况下,分类器的表现很差。想一想使用这样的分类器和像准确率这样的指标的现实影响。对于罕见疾病的患者,错误地将其分类为没有疾病可能是致命的。

因此,识别不平衡数据集的情况非常重要,同时选择正确的度量标准来分析这些数据集同样重要。在本例中,正确的度量标准应该是查看两个类别的召回值:

Recall Positive class  = TP / ( TP + FN ) = 0 / ( 0 + 90)
 = 0
Recall Negative Class = TN / ( TN + FP) = 9900 / ( 9900 + 0)
= 100%

从召回值来看,我们本可以识别出分类器对多数类的偏倚,这促使我们查看缓解此类偏倚的策略,这是我们接下来要关注的主题。

应对不平衡数据集的策略

现在我们已经识别出不平衡数据集的挑战,接下来让我们看一下应对不平衡数据集的策略:

图 13.11:应对不平衡数据集的策略

图 13.11:应对不平衡数据集的策略

收集更多数据

在遇到不平衡数据集时,你需要问的第一个问题是,是否有可能获取更多的数据。这看起来可能有些天真,但收集更多的数据,尤其是来自少数类的数据,然后再平衡数据集,应该是解决类别不平衡的首要策略。

重采样数据

在许多情况下,收集更多的数据,尤其是来自少数类的数据,可能是一个挑战,因为少数类的数据点通常非常稀少。在这种情况下,我们需要采用不同的策略来应对我们的约束条件,并努力平衡数据集。一种有效的策略是对数据集进行重采样,使数据集更加平衡。重采样意味着从现有数据集中提取样本,以创建一个新的数据集,从而使新数据集更加平衡。

让我们详细了解一下这个思想:

图 13.12:对多数类进行随机欠采样

图 13.12:对多数类进行随机欠采样

图 13.8所示,重采样的思想是从多数类中随机选取样本,以使最终的数据集更加平衡。在该图中,我们可以看到少数类的样本数量与原始数据集相同,而多数类则被欠采样,从而使最终的数据集更加平衡。这种类型的重采样被称为随机欠采样,因为我们是在对多数类进行欠采样。在接下来的练习中,我们将执行随机欠采样。

练习 13.02:在我们的银行数据集上实现随机欠采样和分类,以找到最佳结果

在本次练习中,你将对多数类(倾向性为 'No')进行欠采样,然后使数据集平衡。在新的平衡数据集上,你将拟合一个逻辑回归模型,并分析结果:

注意

你将在本次练习中使用的数据集可以在我们的 GitHub 仓库中找到:packt.live/2twFgIM

  1. 为本次练习打开一个新的 Colab 笔记本。

  2. 执行 练习 13.01 的前 12 步,在数据集上对逻辑回归模型进行基准测试,以便将数据集拆分为训练集和测试集。

  3. 现在,先将 Xy 变量合并成训练集,之后再进行重采样:

    """
    Let us first join the train_x and train_y for ease of operation
    """
    trainData = pd.concat([X_train,y_train],axis=1)
    

    在此步骤中,我们将 X_trainy_train 数据集合并为一个数据集。这是为了使后续的重采样过程更简单。合并这两个数据集时,我们使用 pandas 中的 .concat() 函数。在代码中,我们使用 axis = 1 表示沿列方向水平合并。

  4. 现在,使用 .head() 函数显示新数据:

    trainData.head()
    

    你应该得到以下输出:

    图 13.13:使用 .head() 显示数据集的前五行

    图 13.13:使用 .head() 显示数据集的前五行

    上述输出显示了数据集中的一些列。

    现在,让我们开始将少数类别和多数类别分开到不同的数据集中。

    接下来,我们将分离少数类别和多数类别。这是必要的,因为我们必须分别从多数类别中采样,以构建平衡的数据集。要分离少数类别,我们必须识别数据集中标记为“yes”的索引。可以使用 .index() 函数来识别这些索引。

    一旦识别出这些索引,它们将被从主数据集中分离出来,并使用 .loc() 函数存储在一个新的变量中,表示少数类别。少数类别数据集的形状也会被打印出来。对于多数类别,执行类似的过程,完成这两个步骤后,我们将得到两个数据集:一个是少数类别数据集,一个是多数类别数据集。

  5. 接下来,找到样本数据集中倾向为 yes 的索引:

    ind = trainData[trainData['y']=='yes'].index
    print(len(ind))
    

    你应该得到以下输出:

    3723
    
  6. 按照以下代码片段分离少数类别:

    minData = trainData.loc[ind]
    print(minData.shape)
    

    你应该得到以下输出:

    (3723, 52)
    
  7. 现在,找到多数类别的索引:

    ind1 = trainData[trainData['y']=='no'].index
    print(len(ind1))
    

    你应该得到以下输出:

    27924
    
  8. 按照以下代码片段分离多数类别:

    majData = trainData.loc[ind1]
    print(majData.shape)
    majData.head()
    

    你应该得到以下输出:

    图 13.14:分离大多数类别后的输出

    图 13.14:分离多数类别后的输出

    一旦大多数类别被分离出来,我们就可以开始从大多数类别中进行采样。一旦采样完成,大多数类别数据集的形状及其头部将被打印出来。

    随机采样的数量应与少数类别的长度相等,以使数据集保持平衡。

  9. 使用 .sample() 函数提取样本:

    majSample = majData.sample(n=len(ind),random_state = 123)
    

    采样的例子数量等于少数类别的例子数量。这是通过参数 (n=len(ind)) 实现的。

  10. 现在,采样完成后,大多数类别数据集的形状及其头部将被打印出来:

    print(majSample.shape)
    majSample.head()
    

    你应该得到以下输出:

    图 13.15:显示多数类别数据集的形状

    图 13.15:显示多数类数据集形状的输出

    现在,我们继续准备新的训练数据

  11. 准备好各个数据集后,我们可以使用pd.concat()函数将它们连接在一起:

    """
    Concatenating both data sets and then shuffling the data set
    """
    balData = pd.concat([minData,majSample],axis = 0)
    

    注意

    在这种情况下,我们是在垂直方向上进行连接,因此使用axis = 0

  12. 现在,使用shuffle()函数打乱数据集,使少数类和多数类均匀分布:

    # Shuffling the data set
    from sklearn.utils import shuffle
    balData = shuffle(balData)
    balData.head()
    

    您应该得到以下输出:

    图 13.16:打乱数据集后的输出

    图 13.16:打乱数据集后的输出

  13. 现在,将打乱的数据集分为自变量X_trainNew和因变量y_trainNew。可以使用.iloc()函数,在pandas中通过索引特征051来分离因变量。因变量通过子集化列名'y'来分离:

    # Making the new X_train and y_train
    X_trainNew = balData.iloc[:,0:51]
    print(X_trainNew.head())
    y_trainNew = balData['y']
    print(y_trainNew.head())
    

    您应该得到以下输出:

    图 13.17:将数据集打乱为自变量

    图 13.17:将数据集打乱为自变量

    现在,将模型拟合到新数据上,并生成混淆矩阵和分类报告供我们分析。

  14. 首先,定义LogisticRegression函数,代码如下:

    from sklearn.linear_model import LogisticRegression
    bankModel1 = LogisticRegression()
    bankModel1.fit(X_trainNew, y_trainNew)
    

    您应该得到以下输出:

    图 13.18:拟合模型

    图 13.18:拟合模型

  15. 接下来,使用以下代码片段对测试数据进行预测:

    pred = bankModel1.predict(X_test)
    print('Accuracy of Logistic regression model prediction on '\
          'test set for balanced data set: {:.2f}'\
          .format(bankModel1.score(X_test, y_test)))
    

    您应该得到以下输出:

    Accuracy of Logistic regression model prediction on test set for balanced data set:0.83
    

    '{:.2f}'.format用于打印字符串值以及准确率分数,这是通过bankModel1.score(X_test, y_test)输出的。在这里,2f表示带有两位小数的数值分数。

  16. 现在,生成模型的混淆矩阵并打印结果:

    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    您应该得到以下输出:

    图 13.19:模型的混淆矩阵

图 13.19:模型的混淆矩阵

注意

输出中的值可能会有所不同,因为建模过程会受到变化的影响。

要访问该特定部分的源代码,请参阅packt.live/348njjY

您也可以在线运行此示例,网址为packt.live/318R81I

分析

让我们分析结果并与我们在本章开始时构建的基准逻辑回归模型进行比较。在基准模型中,我们面临着模型偏向于多数类,且yes类的召回率非常低的问题。

通过平衡数据集,我们发现少数类的召回率大幅提高,从0.32增加到大约0.82。这意味着通过平衡数据集,分类器在识别负例方面的能力得到了提升。

然而,我们可以看到整体准确率出现了下降。从大约 90%的高准确率下降到约 85%。准确率下降的一个主要原因是虚假正例的数量增加了,即那些错误地预测为YesNo案例。

从业务角度分析结果,这比我们在基准模型中得到的结果要好得多。在基准模型中,1,566 个Yes案例中,只有 506 个被正确识别。然而,在平衡数据集后,我们能够从 1,566 个数据中识别出 1,277 个可能购买定期存款的客户,这可能带来更高的转化率。然而,另一方面,销售团队还需要花费大量时间处理那些不太可能购买定期存款的客户。从混淆矩阵中,我们可以看到虚假负例的数量从基准模型中的 291 个上升到了 1,795 个。理想情况下,我们希望第二和第三象限的数量下降,以便有利于另外两个象限。

生成合成样本

在前一部分中,我们讨论了欠采样方法,通过缩小多数类样本来平衡数据集。然而,使用欠采样时,我们减少了数据集的大小。在许多情况下,缩小数据集可能会对分类器的预测能力产生不利影响。应对数据集缩小的有效方法是过采样少数类。过采样是通过生成类似于少数类的合成数据点来实现,从而平衡数据集。

生成此类合成点的两种非常流行的方法是:

  • 合成少数类过采样技术 (SMOTE)

  • 修改后的 SMOTE (MSMOTE)

SMOTE算法生成合成数据的方式是通过观察少数类的邻域,并在邻域内生成新的数据点:

图 13.20:包含两类数据集

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_13_20.jpg)

图 13.20:包含两类数据集

让我们通过图示来解释生成合成数据集的概念。假设图 13.15表示一个包含两类的 dataset:灰色圆圈代表少数类,黑色圆圈代表多数类。

在创建合成点时,会创建一条连接所有少数类样本的假想线,并在这条线上生成新的数据点,如图 13.16所示,从而平衡数据集:

图 13.21:连接邻域中的样本

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_13_21.jpg)

图 13.21:连接邻域中的样本

然而,MSMOTE是对SMOTE算法的改进,并且在生成合成点的方式上有所不同。MSMOTE将少数类分为三组:安全样本边界样本潜在噪声样本。根据每个少数类所属的组,采用不同的策略来生成邻域点。

我们将在接下来的部分中看到SMOTEMSMOTE的实现。

实现 SMOTE 和 MSMOTE

SMOTEMSMOTE可以通过一个名为smote-variants的包在 Python 中实现。该库可以通过在 Colab 笔记本中使用pip install进行安装,如下所示:

!pip install smote-variants

注意

更多关于该包及其不同版本的详细信息,请访问 packt.live/2QsNhat

现在让我们实现这两种方法并分析结果。

练习 13.03:在我们的银行数据集上实现 SMOTE,以找到最佳结果

在本练习中,我们将使用SMOTE生成少数类的合成样本,并使数据集达到平衡。然后,在新的平衡数据集上,我们将拟合一个逻辑回归模型并分析结果:

  1. 实现练习 13.01在数据集上基准化逻辑回归模型的所有步骤,直到拆分训练集和测试集(步骤 12)。

  2. 现在,在我们进行过采样之前,打印两个类的计数:

    # Shape before oversampling
    print("Before OverSampling count of yes: {}"\
          .format(sum(y_train=='yes')))
    print("Before OverSampling count of no: {} \n"\
          .format(sum(y_train=='no')))
    

    你应该会看到以下输出:

    Before OverSampling count of yes: 3694
    Before OverSampling count of no: 27953
    

    注意

    这个输出中提到的计数可能会有所不同,因为采样过程具有变异性。

    接下来,我们将使用SMOTE对训练集进行过采样。

  3. 首先导入svnumpy

    !pip install smote-variants
    import smote_variants as sv
    import numpy as np
    

    用于对训练集进行过采样的库文件包括我们之前安装的smote_variants库,它被导入为sv。另一个需要的库是numpy,因为训练集必须提供一个numpy数组给smote_variants库。

  4. 现在,使用sv.SMOTE()函数将SMOTE库实例化为一个名为oversampler的变量:

    # Instantiating the SMOTE class
    oversampler= sv.SMOTE()
    

    这是实例化smote_variants库中任何SMOTE变体的常见方式。

  5. 现在,使用oversampler.sample()函数对过程进行采样:

    # Creating new training set
    X_train_os, y_train_os = oversampler.sample\
                             (np.array(X_train), np.array(y_train))
    

    注意

    在应用.sample()函数之前,Xy变量都已转换为numpy数组。

  6. 现在,打印新Xy变量的形状,以及各类的counts。你会注意到,整体数据集的大小从先前约 31,647(3694 + 27953)增加到了 55,906。大小的增加可以归因于少数类的过采样,从 3,694 增加到 27,953:

    # Shape after oversampling
    print('After OverSampling, the shape of train_X: {}'\
          .format(X_train_os.shape))
    print('After OverSampling, the shape of train_y: {} \n'\
          .format(y_train_os.shape))
    print("After OverSampling, counts of label 'Yes': {}"\
          .format(sum(y_train_os=='yes')))
    print("After OverSampling, counts of label 'no': {}"\
          .format(sum(y_train_os=='no')))
    

    你应该会看到以下输出:

    After OverSampling, the shape of train_X: (55906, 51)
    After OverSampling, the shape of train_y: (55906,) 
    After OverSampling, counts of label 'Yes': 27953
    After OverSampling, counts of label 'no': 27953
    

    注意

    这个输出中提到的计数可能会有所不同,因为采样过程具有变异性。

    现在,我们使用SMOTE生成了合成点并平衡了数据集,让我们在新的样本上拟合逻辑回归模型,并使用混淆矩阵和分类报告分析结果。

  7. 定义LogisticRegression函数:

    # Training the model with Logistic regression model
    from sklearn.linear_model import LogisticRegression
    bankModel2 = LogisticRegression()
    bankModel2.fit(X_train_os, y_train_os)
    
  8. 现在,在测试集上使用.predict进行预测,如以下代码片段所示:

    pred = bankModel2.predict(X_test)
    
  9. 接下来,print准确度值:

    print('Accuracy of Logistic regression model prediction on '\
          'test set for Smote balanced data set: {:.2f}'\
          .format(bankModel2.score(X_test, y_test)))
    

    你的输出应如下所示:

    Accuracy of Logistic regression model prediction on test set for Smote balanced data set: 0.83
    
  10. 然后,为模型生成ConfusionMatrix

    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    矩阵如下所示:

    [[10042  1956]
     [  306  1260]]
    
  11. 为模型生成Classification_report

    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到以下输出:

    图 13.22:模型的分类报告

    ](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-ws/img/B15019_13_22.jpg)

图 13.22:模型的分类报告

从生成的指标中,我们可以看到结果与欠采样结果非常相似,唯一不同的是,'Yes'类的召回值从0.82下降到了大约0.80。生成的结果会因使用案例的不同而有所变化。SMOTE及其变种已被证明在数据平衡方面具有强大的效果,因此在遇到高度不平衡数据的使用案例时,成为最受欢迎的方法。

注意

输出值可能有所变化,因为建模过程本身会有波动。

要访问该特定部分的源代码,请参考packt.live/2Ycxu34

你也可以在线运行此示例,访问packt.live/2FDvTgo

在下一个练习中,我们将实现MSMOTE

练习 13.04:在我们的银行数据集上实现 MSMOTE 以找到最佳结果

在这个练习中,我们将使用MSMOTE生成少数类的合成样本,然后使数据集平衡。接着,在新的平衡数据集上,我们将拟合一个逻辑回归模型并分析结果。这个练习将与之前的练习非常相似。

  1. 实现练习 13.01的所有步骤,基准化逻辑回归模型在数据集上的表现,直到训练集和测试集的拆分(步骤 12)。

  2. 现在,打印我们进行过采样前两类的计数:

    # Shape before oversampling
    print("Before OverSampling count of yes: {}"\
          .format(sum(y_train=='yes')))
    print("Before OverSampling count of no: {} \n"\
          .format(sum(y_train=='no')))
    

    你应该得到以下输出:

    Before OverSampling count of yes: 3723
    Before OverSampling count of no: 27924
    

    注意

    输出中提到的计数值可能会有所不同,因为采样过程具有变异性。

    接下来,我们将使用MSMOTE对训练集进行过采样。

  3. 开始时导入svnumpy

    !pip install smote-variants
    import smote_variants as sv
    import numpy as np
    

    用于对训练集进行过采样的库文件包括我们之前安装的smote_variants库,已作为sv导入。另一个必需的库是numpy,因为训练集需要提供numpy数组供smote_variants库使用。

  4. 现在,通过sv.MSMOTE()函数将MSMOTE库实例化为一个名为oversampler的变量:

    # Instantiating the MSMOTE class
    oversampler= sv.MSMOTE()
    
  5. 现在,使用oversampler.sample()函数对过程进行采样:

    # Creating new training set
    X_train_os, y_train_os = oversampler.sample\
                             (np.array(X_train), np.array(y_train))
    # Shape after oversampling
    print('After OverSampling, the shape of train_X: {}'\
          .format(X_train_os.shape))
    print('After OverSampling, the shape of train_y: {} \n'\
          .format(y_train_os.shape))
    print("After OverSampling, counts of label 'Yes': {}"\
          .format(sum(y_train_os=='yes')))
    print("After OverSampling, counts of label 'no': {}"\
          .format(sum(y_train_os=='no')))
    

    你应该得到以下输出:

    After OverSampling, the shape of train_X: (55848, 51)
    After OverSampling, the shape of train_y: (55848,) 
    After OverSampling, counts of label 'Yes': 27924
    After OverSampling, counts of label 'no': 27924
    

    现在,我们已经使用MSMOTE生成了合成样本并平衡了数据集,接下来让我们在新样本上拟合逻辑回归模型,并使用混淆矩阵和分类报告分析结果。

  6. 定义LogisticRegression函数:

    # Training the model with Logistic regression model
    from sklearn.linear_model import LogisticRegression
    # Defining the LogisticRegression function
    bankModel3 = LogisticRegression()
    bankModel3.fit(X_train_os, y_train_os)
    
  7. 现在,像下面的代码片段一样,在测试集上使用.predict进行预测:

    pred = bankModel3.predict(X_test)
    
  8. 接下来,print精度值:

    print('Accuracy of Logistic regression model prediction on '\
          'test set for MSMOTE balanced data set: {:.2f}'\
          .format(bankModel3.score(X_test, y_test)))
    

    你应该得到以下输出:

    Accuracy of Logistic regression model prediction on test set for MSMOTE balanced data set: 0.84
    
  9. 为模型生成ConfusionMatrix

    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    矩阵应如下所示:

    [[10167  1831]
     [  314  1252]]
    
  10. 为模型生成Classification_report

    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到以下输出:

    图 13.23:模型的分类报告

图 13.23:模型的分类报告

注意

由于建模过程存在变动,输出的值可能会有所不同。

要访问此特定部分的源代码,请参阅 packt.live/34bCWHd

你也可以在线运行此示例,网址是 packt.live/2Edccvh

MSMOTE的实现中可以看出,与练习 13.03中实现的SMOTE方法相比,指标有所下降,练习 13.03在我们的银行数据集上实现 SMOTE 以找到最佳结果。因此,我们可以得出结论,MSMOTE可能不是该用例的最佳方法。

在电信数据集上应用平衡技术

现在我们已经看到了不同的平衡技术,接下来让我们将这些技术应用到一个与电信客户流失相关的新数据集上。该数据集可以通过以下链接获取:packt.live/37IvqSX

该数据集包含与移动连接使用水平相关的各种变量,如总通话时长、通话费用、在某些时间段内拨打的电话、国际电话的详细信息以及拨打客户服务电话的详细信息。

问题陈述是预测客户是否会流失。这个数据集是高度不平衡的,其中客户流失的案例占少数。你将在接下来的活动中使用这个数据集。

活动 13.01:通过在电信流失数据集上拟合分类器来寻找最佳平衡技术

你在一家电信公司担任数据科学家。你遇到了一个高度不平衡的数据集,你希望在拟合分类器进行流失分析之前,先纠正类不平衡。你知道不同的纠正数据集不平衡的方法,并且你希望比较它们,以找出最适合的最佳方法,之后再拟合模型。

在这个活动中,你需要实现到目前为止遇到的所有三种方法,并比较结果。

注意

你将使用你在第十章分析数据集中使用的电信流失数据集。

使用MinMaxscaler函数来缩放数据集,而不是你一直使用的鲁棒缩放器函数。通过拟合逻辑回归模型比较两种方法的结果。

步骤如下:

  1. 实现所有初始步骤,包括安装 smote-variants 并使用 pandas 加载数据。

  2. 使用我们在第三章,二元分类中学习的MinMaxScaler()函数规范化数值原始数据。

  3. 使用pd.get_dummies()函数为分类变量创建虚拟数据。

  4. 从原始数据框中分离数值数据。

  5. 使用pd.concat()函数连接数值数据和虚拟分类数据。

  6. 使用train_test_split()函数将早期的数据集拆分为训练集和测试集。

    由于数据集不平衡,你需要执行以下步骤中提到的各种技术。

  7. 对于欠采样方法,使用.index()函数找到少数类的索引并分离少数类。然后,采样多数类并使用.sample()函数将多数数据集调整为与少数类相同的大小。将少数类和欠采样后的多数类合并,形成一个新的数据集。打乱数据集并分离XY变量。

  8. 在欠采样的数据集上拟合一个逻辑回归模型,并命名为churnModel1

  9. 对于SMOTE方法,使用sv.SMOTE()函数创建过采样器,并创建新的XY训练集。

  10. 使用SMOTE拟合逻辑回归模型,并命名为churnModel2

  11. 导入smote-variant库并使用sv.MSMOTE()函数实例化MSMOTE算法。

  12. 使用过采样器创建过采样数据。请注意,Xy变量在过采样之前必须转换为numpy数组。

  13. 使用MSMOTE数据集拟合逻辑回归模型,并将模型命名为churnModel3

  14. 为每个模型生成三个独立的预测。

  15. 为每个预测生成单独的准确性度量、分类报告和混淆矩阵。

  16. 分析结果并选择最佳方法。

预期输出

你可以期待的最终指标将与这里看到的类似。

欠采样输出

图 13.24:欠采样输出报告

图 13.24:欠采样输出报告

SMOTE 输出

图 13.25:SMOTE 输出报告

图 13.25:SMOTE 输出报告

MSMOTE 输出

图 13.26:MSMOTE 输出报告

图 13.26:MSMOTE 输出报告

注意

由于建模是随机性的,你将获得不同的输出。

该活动的解决方案可以在此处找到:packt.live/2GbJloz

总结

在本章中,我们学习了不平衡数据集和解决不平衡数据集的策略。我们介绍了会遇到不平衡数据集的应用场景。我们探讨了不平衡数据集所带来的挑战,并介绍了在不平衡数据集的情况下应使用的评估指标。我们制定了处理不平衡数据集的策略,并实现了不同的策略,如随机欠采样和过采样,用于平衡数据集。然后,在平衡数据集后,我们训练了不同的模型并分析了结果。

平衡数据集是一种提高分类器性能的非常有效的方法。然而,应该注意的是,平衡数据集可能会导致大类的整体准确度下降。应该根据问题陈述以及在这些问题陈述下进行严格实验后,来确定在不同情况下采取哪些策略。

学习了处理不平衡数据集的方法后,我们将介绍另一种在许多现代数据集中常见的重要技术——降维。降维的不同技术将在第十四章降维中讨论。

第十四章:14. 降维

概述

本章介绍了数据科学中的降维。你将使用互联网广告数据集来分析和评估不同的降维技术。在本章结束时,你将能够分析高维数据集并应对这些数据集带来的挑战。同时,你将应用不同的降维技术处理大型数据集,并基于这些数据集拟合模型并分析其结果。到本章结束时,你将能够处理现实世界中的巨型数据集。

介绍

在上一章关于数据集平衡的内容中,我们处理了银行营销数据集,该数据集有 18 个变量。我们能够非常轻松地加载该数据集,拟合模型并获得结果。但你是否考虑过,当你需要处理的变量数量非常大时,会是什么情况?例如,当变量数量达到 1800 万,而不是上一章中的 18 个时,你该如何加载如此庞大的数据集并进行分析?如何处理需要如此大量计算资源的建模问题?

这是一些现代数据集中在以下领域的现实:

  • 医疗保健领域,其中遗传数据集可能有数百万个特征

  • 高分辨率成像数据集

  • 与广告、排名和爬取相关的网页数据

在处理如此庞大的数据集时,可能会出现许多挑战:

  • 存储和计算挑战:高维的大型数据集需要大量的存储空间和昂贵的计算资源来进行分析。

  • 探索挑战:在尝试探索数据并提取洞察时,高维数据可能非常繁琐。

  • 算法挑战:许多算法在高维设置下扩展性较差。

那么,当我们必须处理高维数据时,解决方案是什么?这就是降维技术发挥作用的地方,本章将详细探讨这些技术。

降维的目的是减少数据集的维度,以克服高维数据所带来的挑战。在本章中,我们将探讨一些常见的降维技术:

  • 反向特征消除或递归特征消除

  • 正向特征选择

  • 主成分分析PCA

  • 独立成分分析ICA

  • 因子分析

让我们首先检查一下我们的商业背景,然后将这些技术应用到问题陈述中。

商业背景

你公司市场部的负责人向你提出了一个她一直在处理的问题。许多客户投诉你公司网站在浏览过程中弹出广告太多,影响了他们的浏览体验。你公司希望在网页服务器上建立一个引擎,识别潜在的广告,并在广告弹出之前将其删除。

为了帮助您实现这一目标,我们提供了一个包含一组可能广告的数据集,这些广告出现在各种网页上。数据集的特征代表了可能广告的图像几何形状,以及 URL 中的短语、图像 URL、锚文本和锚文本附近的词汇。这个数据集已经被标注,每个可能的广告都被标注为是否为广告。使用此数据集,您需要构建一个模型来预测某个内容是否为广告。您可能认为这是一个相对简单的问题,可以使用任何二分类算法来解决。然而,数据集存在一个挑战:特征数量非常庞大。您已经开始着手解决这个高维数据集的挑战。

此数据集已上传到 GitHub 仓库,用于完成后续所有练习。数据集的属性可以通过以下链接访问:packt.live/36rqiCg

练习 14.01:加载和清理数据集

在本练习中,我们将下载数据集,加载到 Colab 笔记本中,并进行一些基本的探索,例如使用 .shape().describe() 函数打印数据集的维度,同时还要清理数据集。

注意

internet_ads 数据集已上传到我们的 GitHub 仓库,并可通过 packt.live/2sPaVF6 访问。

以下步骤将帮助您完成此练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 现在,将 import pandas 导入到您的 Colab 笔记本中:

    import pandas as pd
    
  3. 接下来,设置已上传 ad.Data 文件所在驱动器的路径,如以下代码片段所示:

    # Defining file name of the GitHub repository
    filename = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter14/Dataset/ad.data'
    
  4. 使用 pandas 数据框中的 pd.read_csv() 函数读取文件:

    adData = pd.read_csv(filename,sep=",",header = None,\
                         error_bad_lines=False)
    adData.head()
    

    pd.read_csv() 函数的参数是文件名字符串和 CSV 文件的分隔符,通常是 ","。请注意,由于数据集没有标题行,我们特别使用 header = None 命令来指定这一点。最后一个参数 error_bad_lines=False 是跳过文件格式中的任何错误,然后加载数据。

    读取文件后,使用 .head() 函数打印数据框。

    您应该得到以下输出:

    图 14.1:将数据加载到 Colab 笔记本中

    图 14.1:将数据加载到 Colab 笔记本中

  5. 现在,打印数据集的形状,如以下代码片段所示:

    # Printing the shape of the data
    print(adData.shape)
    

    您应该得到以下输出:

    (3279, 1559)
    

    从数据的形状来看,我们可以看到特征数量非常大,共有 1559 个特征。

  6. 使用 pandas 的 .describe() 函数查找原始数据的数值特征摘要,如以下代码片段所示:

    # Summarizing the statistics of the numerical raw data
    adData.describe()
    

    您应该得到以下输出:

    图 14.2:将数据加载到 Colab 笔记本中

    图 14.2:将数据加载到 Colab 笔记本中

    如我们从数据的形状中看到的,数据集有3279个示例,1559个变量。变量集包含分类变量和数值变量。汇总统计仅针对数值数据得出。

  7. 从我们的数据集中分离出因变量和自变量,如以下代码片段所示:

    # Separate the dependent and independent variables
    # Preparing the X variables
    X = adData.loc[:,0:1557]
    print(X.shape)
    # Preparing the Y variable
    Y = adData[1558]
    print(Y.shape)
    

    你应该得到以下输出:

    (3279, 1558)
    (3279, )
    

    如前所见,数据集中有1559个特征。前1558个特征是自变量。它们通过.loc()函数从初始的adData数据框中分离出来,并给出相应特征的索引(01557)。这些自变量被加载到一个新的变量X中。因变量,即数据集的标签,被加载到变量Y中。因变量和自变量的形状也被打印出来。

  8. 打印自变量的前15个示例:

    # Printing the head of the independent variables
    X.head(15)
    

    你可以通过在head()函数中定义数字来打印任意数量的行。在这里,我们打印了数据的前15行。

    输出如下:

    图 14.3:自变量的前 15 个示例

    图 14.3:自变量的前 15 个示例

    从输出结果中,我们可以看到数据集存在许多缺失值,这些缺失值用?表示。为了进一步分析,我们必须去除这些特殊字符,并将这些单元格替换为假定值。替换特殊字符的一种常见方法是用相应特征的均值进行填充。我们就采用这种策略。然而,在此之前,让我们先查看这个数据集的变量类型,以便采用合适的替换策略。

  9. 打印数据集的变量类型:

    # Printing the data types
    print(X.dtypes)
    

    我们应该得到以下输出:

    图 14.4:我们数据集中的变量类型

    图 14.4:我们数据集中的变量类型

    从输出结果中,我们可以看到前四列是对象类型,表示字符串数据,其他列则是整数数据。在替换数据中的特殊字符时,我们需要注意数据的类型。

  10. 将前四列中的特殊字符替换为NaN值。

    将前四列(对象类型)的特殊字符替换为NaN值。NaN是“不是数字”(not a number)的缩写。用NaN值替换特殊字符使得后续填充数据变得更加容易。

    这是通过以下代码片段实现的:

    """
    Replacing special characters in first 3 columns 
    which are of type object
    """
    for i in range(0,3):
        X[i] = X[i].str.replace("?", 'nan')\
                   .values.astype(float)
    print(X.head(15))
    

    为了替换前面三列,我们使用for()循环和range()函数来遍历这些列。由于前面三列的数据类型是objectstring类型,我们使用.str.replace()函数,即“字符串替换”。在用nan替换数据中的特殊字符?后,我们通过.values.astype(float)函数将数据类型转换为float,这是进一步处理所必需的。通过打印前 15 个示例,我们可以看到所有特殊字符都已被nanNaN值替换。

    你应该获得以下输出:

    图 14.5:替换特殊字符为 NaN 之后

    图 14.5:替换特殊字符为 NaN 之后

  11. 现在,替换整数特征中的特殊字符。

    步骤 9所示,让我们使用以下代码片段,替换int64数据类型特征中的特殊字符:

    """
    Replacing special characters in the remaining 
    columns which are of type integer
    """
    for i in range(3,1557):
        X[i] = X[i].replace("?", 'NaN').values.astype(float)
    

    注意

    对于整数特征,我们在.replace()函数前没有.str,因为这些特征是整数值,而不是字符串值。

  12. 现在,为NaN值填充每列的均值。

    现在我们已经将数据中的特殊字符替换为NaN值,我们可以使用 pandas 中的fillna()函数将NaN值替换为该列的均值。可以使用以下代码片段来执行此操作:

    import numpy as np
    # Impute the 'NaN'  with mean of the values
    for i in range(0,1557):
        X[i] = X[i].fillna(X[i].mean())
    print(X.head(15))
    

    在前面的代码片段中,.mean()函数计算每列的均值,然后用该列的均值替换nan值。

    你应该获得以下输出:

    图 14.6:NaN 列的均值

    图 14.6:NaN 列的均值

  13. 使用minmaxScaler()函数对数据集进行缩放。

    第三章二分类中一样,缩放数据在建模步骤中是有用的。让我们按照第三章二分类中学到的方法,使用minmaxScaler()函数对数据集进行缩放。

    如下代码片段所示:

    # Scaling the data sets
    # Import library function
    from sklearn import preprocessing
    # Creating the scaling function
    minmaxScaler = preprocessing.MinMaxScaler()
    # Transforming with the scaler function
    X_tran = pd.DataFrame(minmaxScaler.fit_transform(X))
    # Printing the output
    X_tran.head() 
    

    你应该获得以下输出。这里,我们展示了前 24 列:

    图 14.7:使用 MinMaxScaler()函数缩放数据集

图 14.7:使用 MinMaxScaler()函数缩放数据集

注意

要访问此特定部分的源代码,请参阅packt.live/2Yi7Nym

你也可以在packt.live/2Q6l9ZZ在线运行此示例。

你已经完成了第一个练习。在本练习中,我们加载了数据集,提取了汇总统计信息,清洗了数据,并且对数据进行了缩放。你可以看到,在最终输出中,所有原始值都已转换为缩放值。

在下一节中,让我们尝试通过添加更多的特征来增强这个数据集,使其成为一个庞大的数据集,然后在这个数据集上拟合一个简单的逻辑回归模型,作为基准模型。

让我们首先看一下如何通过示例增强数据。

创建高维数据集

在之前的部分,我们处理了一个约有1,558个特征的数据集。为了演示高维数据集的挑战,我们将从已有的互联网数据集中创建一个极高维度的数据集。

我们将通过多次复制现有特征的数量来实现这一点,使数据集变得非常大。为了复制数据集,我们将使用一个名为np.tile()的函数,它可以在我们指定的轴上多次复制数据框。我们还将使用time()函数来计算活动所需的时间。

让我们通过一个简单的例子来看一下这两个函数的作用。

你首先需要导入必要的库函数:

import pandas as pd
import numpy as np

然后,为了创建一个虚拟数据框,我们将使用一个包含两行三列的小数据集作为示例。我们使用pd.np.array()函数来创建数据框:

# Creating a simple data frame
df = pd.np.array([[1, 2, 3], [4, 5, 6]])
print(df.shape)
df

你应该获得以下输出:

图 14.8:示例虚拟数据框的数组

图 14.8:示例虚拟数据框的数组

接下来,你将复制虚拟数据框,列的复制是通过以下代码片段中的pd.np.tile()函数来完成的:

# Replicating the data frame and noting the time
import time
# Starting a timing function
t0=time.time()
Newdf = pd.DataFrame(pd.np.tile(df, (1, 5)))
print(Newdf.shape)
print(Newdf)
# Finding the end time
print("Total time:", round(time.time()-t0, 3), "s")

你应该获得以下输出:

图 14.9:数据框的复制

图 14.9:数据框的复制

如片段所示,pd.np.tile()函数接受两组参数。第一组是我们要复制的数据框df,下一组参数(1,5)定义了我们要复制的轴。在这个示例中,我们定义行保持不变,因为参数是1,而列将被复制5次,因为参数是5。从shape()函数中我们可以看到,原始数据框形状为(2,3),经过转换后变为形状为(2,15)的数据框。

总时间的计算是通过time库来完成的。为了开始计时,我们调用time.time()函数。在这个例子中,我们将初始时间存储在一个名为t0的变量中,然后从结束时间中减去它,得到该过程所需的总时间。因此,我们已经扩展并向现有的互联网广告数据集添加了更多的数据框。

活动 14.01:在高维数据集上拟合逻辑回归模型

你希望在数据集较大时测试模型的性能。为此,你将人工增大互联网广告数据集,使得数据集的维度比原始数据集大 300 倍。你将拟合一个逻辑回归模型,并观察结果。

提示:在本次活动中,我们将使用类似于练习 14.01加载和清理数据集的笔记本,并且我们还将像第三章二元分类中一样拟合一个逻辑回归模型。

注意

我们将在本次活动中使用相同的广告数据集。

internet_ads 数据集已上传到我们的 GitHub 仓库,并可以通过 packt.live/2sPaVF6 访问。

完成此活动的步骤如下:

  1. 打开一个新的 Colab 笔记本。

  2. 实现 练习 14.01加载和清理数据集,直到数据标准化的所有步骤。推导变换后的自变量 X_tran

  3. 使用 pd.np.tile() 函数将列复制 300 次来创建一个高维数据集。打印新数据集的形状并观察新数据集中的特征数量。

  4. 将数据集拆分为训练集和测试集。

  5. 在新数据集上拟合一个逻辑回归模型,并注意拟合模型所需的时间。注意你 Colab 笔记本中 RAM 指标的颜色变化。

    预期输出

    在对新数据集拟合逻辑回归模型后,你应该得到类似以下的输出:

    Total training time: 23.86 s
    

    图 14.10:Google Colab RAM 使用情况

    图 14.10:Google Colab RAM 使用情况

  6. 在测试集上进行预测,并打印分类报告和混淆矩阵。

    你应该得到以下输出:

    图 14.11:混淆矩阵和分类报告结果

图 14.11:混淆矩阵和分类报告结果

注意

该活动的解决方案可以在此处找到:packt.live/2GbJloz

在这个活动中,你将通过复制现有数据库的列来创建一个高维数据集,并识别出该高维数据集的资源利用率非常高。由于维度较大,资源利用率指示器的颜色变为橙色。你还注意到,在该数据集上的建模时间较长,为 23.86 秒。你还会在测试集上进行预测,并使用逻辑回归模型得到大约 97% 的准确率。

首先,你需要了解为什么 Colab 上的 RAM 使用情况指示器变成了橙色。由于我们通过复制创建了一个庞大的数据集,Colab 必须使用访问 RAM,这导致颜色变为橙色。

但是,出于好奇,如果你将复制次数从 300 增加到 500,你认为这会对 RAM 使用情况产生什么影响呢?让我们来看一下以下示例。

注意

你不需要在你的 Colab 笔记本上执行此操作。

我们首先定义 GitHub 仓库中“ads”数据集的路径:

# Defining the file name from GitHub
filename = 'https://raw.githubusercontent.com'\
           '/PacktWorkshops/The-Data-Science-Workshop'\
           '/master/Chapter14/Dataset/ad.data'

接下来,我们使用 pandas 加载数据:

# import pandas as pd
# Loading the data using pandas
adData = pd.read_csv(filename,sep=",",header = None,\
                     error_bad_lines=False)

创建一个具有 500 缩放因子的高维数据集:

# Creating a high dimension dataset
X_hd = pd.DataFrame(pd.np.tile(adData, (1, 500)))

你将看到以下输出:

图 14.12:Colab 崩溃

图 14.12:Colab 崩溃

从输出中,您可以看到会话崩溃,因为 Colab 提供的所有 RAM 已被用尽。会话将重启,您将失去所有变量。因此,始终保持对所提供资源的敏感性是很重要的,尤其是在处理数据集时。作为数据科学家,如果您觉得数据集庞大,特征众多,但处理这些数据集的资源有限,您需要联系组织,获取所需资源或制定相应的策略来处理这些高维数据集。

处理高维数据集的策略

活动 14.01在高维数据集上拟合逻辑回归模型中,我们见识了高维数据集带来的挑战。我们看到,当复制因子为 300 时,资源受到了挑战。您还看到了,当复制因子增加到 500 时,笔记本崩溃了。当复制因子为 500 时,特征数量大约为 750,000。在我们的案例中,在特征数量达到 100 万之前,资源就会失败扩展。现代数据集中有时会有数亿个特征,或在许多情况下达到数十亿个特征。想象一下,要从这些数据集中提取任何可操作的见解需要多少资源和时间。

幸运的是,我们有许多强大的方法来处理高维数据集。这些技术中的许多都非常有效,并且帮助解决了由庞大数据集带来的挑战。

让我们来看看处理高维数据集的一些技巧。在图 14.14中,您可以看到本章中我们将要介绍的处理此类数据集的策略:

图 14.13:处理高维数据集的策略

图 14.13:处理高维数据集的策略

后向特征消除(递归特征消除)

后向特征消除算法的机制是递归地消除特征,并在消除所有特征后,基于剩余的特征构建模型。

让我们一步步地看看这个算法的工作原理:

  1. 最初,在给定的迭代中,选定的分类算法会首先在所有 n 个特征上进行训练。例如,假设我们有一个原始数据集,包含 1,558 个特征。在第一次迭代中,算法会使用所有的 1,558 个特征开始。

  2. 在下一步中,我们一次去除一个特征,并使用剩下的 n-1 个特征来训练模型。这个过程重复 n 次。例如,首先我们去除特征 1,然后使用剩余的 1,557 个变量拟合模型。在下一次迭代中,我们保留特征 1,而去除特征 2,然后再拟合模型。这个过程会重复 n 次(即 1,558 次)。

  3. 对于每个拟合的模型,都会计算模型的性能(使用准确率等衡量指标)。

  4. 替换后导致性能变化最小的特征将被永久移除,并且步骤 2会在n-1个特征下重复。

  5. 然后,该过程会在n-2个特征下重复进行,以此类推。

  6. 该算法不断地消除特征,直到达到我们所需的特征数量阈值。

让我们来看一下在下一个练习中,后向特征消除算法如何在扩展的广告数据集上应用。

练习 14.02:使用后向特征消除进行降维

在这个练习中,我们将在消除特征后,使用后向消除技巧拟合一个逻辑回归模型,以找到该模型的准确度。我们将使用之前相同的广告数据集,并且在这个练习中我们将对其进行增强,添加更多特征。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 实现所有初始步骤,类似于练习 14.01加载和清理数据集,直到使用minmaxscaler()函数对数据集进行缩放:

    filename = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter14/Dataset/ad.data'
    import pandas as pd
    adData = pd.read_csv(filename,sep=",",header = None,\
                         error_bad_lines=False)
    X = adData.loc[:,0:1557]
    Y = adData[1558]
    import numpy as np
    for i in range(0,3):
        X[i] = X[i].str.replace("?", 'NaN').values.astype(float)
    for i in range(3,1557):
        X[i] = X[i].replace("?", 'NaN').values.astype(float)
    for i in range(0,1557):
        X[i] = X[i].fillna(X[i].mean())
    from sklearn import preprocessing
    minmaxScaler = preprocessing.MinMaxScaler()
    X_tran = pd.DataFrame(minmaxScaler.fit_transform(X))
    
  3. 接下来,创建一个高维数据集。我们将通过一个2的因子人工扩展数据集。后向特征消除是一个计算密集型过程,使用更高的维度会导致更长的处理时间。这就是为什么扩展因子保持为2的原因。该过程通过以下代码片段实现:

    # Creating a high dimension data set
    X_hd = pd.DataFrame(pd.np.tile(X_tran, (1, 2)))
    print(X_hd.shape)
    

    你应该得到以下输出:

    (3279, 3116)
    
  4. 定义后向消除模型。后向消除通过向RFE()函数提供两个参数来工作,这个函数是我们想要尝试的模型(在我们这个例子中是逻辑回归)以及我们希望数据集缩减到的特征数量。具体实现如下:

    from sklearn.linear_model import LogisticRegression
    from sklearn.feature_selection import RFE
    # Defining the Classification function
    backModel = LogisticRegression()
    """
    Reducing dimensionality to 250 features for the 
    backward elimination model
    """
    rfe = RFE(backModel, 250)
    

    在这个实现中,我们给定的特征数量250是通过反复试验确定的。该过程是先假设一个任意数量的特征,然后根据最终的度量结果,确定最适合模型的特征数量。在这个实现中,我们对250的初步假设意味着我们希望后向消除模型开始消除特征,直到我们得到最佳的250个特征。

  5. 适配后向消除法以识别最佳的250个特征。

    我们现在准备将后向消除法应用于高维数据集。我们还将记录后向消除法所需的时间。这是通过以下代码片段实现的:

    # Fitting the rfe for selecting the top 250 features
    import time
    t0 = time.time()
    rfe = rfe.fit(X_hd, Y)
    t1 = time.time()
    print("Backward Elimination time:", \
          round(t1-t0, 3), "s")
    

    使用.fit()函数来适配后向消除法。我们给定独立和依赖的训练集。

    注意

    后向消除法是一个计算密集型过程,因此该过程将花费大量时间来执行。特征数量越大,所需时间就越长。

    后向消除的时间会在通知的最后显示:

    图 14.14:后向消除过程所花费的时间

    图 14.14:向后淘汰过程所需时间

    你可以看到,向后淘汰法找到最佳250个特征的过程花费了230.35秒。

  6. 显示通过向后淘汰法识别的特征。我们可以使用get_support()函数来显示通过向后淘汰法识别的250个特征。实现代码如下:

    # Getting the indexes of the features used
    rfe.get_support(indices = True)
    

    你应该得到以下输出:

    图 14.15:展示已识别特征

    图 14.15:展示已识别特征

    这些是通过向后淘汰法从整个数据集中最终选择出的250个最佳特征。

  7. 现在,将数据集划分为训练集和测试集以进行建模:

    from sklearn.model_selection import train_test_split
    # Splitting the data into train and test sets
    X_train, X_test, y_train, y_test = train_test_split\
                                       (X_hd, Y, test_size=0.3,\
                                        random_state=123)
    print('Training set shape',X_train.shape)
    print('Test set shape',X_test.shape)
    

    你应该得到以下输出:

    Training set shape (2295, 3116)
    Test set shape (984, 3116)
    

    从输出中,你可以看到训练集和测试集的形状。

  8. 转换训练集和测试集。在步骤 5中,我们通过向后淘汰法识别了前250个特征。现在,我们需要将训练集和测试集减少到这250个特征。这可以通过.transform()函数来完成。以下代码片段实现了这一点:

    # Transforming both train and test sets
    X_train_tran = rfe.transform(X_train)
    X_test_tran = rfe.transform(X_test)
    print("Training set shape",X_train_tran.shape)
    print("Test set shape",X_test_tran.shape)
    

    你应该得到以下输出:

    Training set shape (2295, 250)
    Test set shape (984, 250)
    

    我们可以看到,训练集和测试集都已被缩减为250个最佳特征。

  9. 在训练集上拟合逻辑回归模型,并记录时间:

    # Fitting the logistic regression model
    import time
    # Defining the LogisticRegression function
    RfeModel = LogisticRegression()
    # Starting a timing function
    t0=time.time()
    # Fitting the model
    RfeModel.fit(X_train_tran, y_train)
    # Finding the end time
    print("Total training time:", \
          round(time.time()-t0, 3), "s")
    

    你应该得到以下输出:

    Total training time: 0.016 s
    

    如预期所示,在减少特征集后拟合模型所需的总时间比活动 14.01中对更大数据集进行拟合所需的时间要低,后者需要23.86秒。这是一个很大的改进。

  10. 现在,对测试集进行预测并打印准确度指标,如以下代码片段所示:

    # Predicting on the test set and getting the accuracy
    pred = RfeModel.predict(X_test_tran)
    print('Accuracy of Logistic regression model after '\
          'backward elimination: {:.2f}'\
          .format(RfeModel.score(X_test_tran, y_test)))
    

    你应该得到以下输出:

    图 14.16:逻辑回归模型的准确度

    图 14.16:逻辑回归模型的准确度

    你可以看到,与我们在活动 14.01中获得的高维数据集模型的准确率(0.97)相比,这个模型的准确度有所提高。这一提高可能归因于从完整特征集识别出的非相关特征,这可能提升了模型的表现。

  11. 打印混淆矩阵:

    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到以下输出:

    图 14.17:混淆矩阵

    图 14.17:混淆矩阵

  12. 打印分类报告:

    from sklearn.metrics import classification_report
    # Getting the Classification_report
    print(classification_report(y_test, pred))
    

    你应该得到以下输出:

    图 14.18:分类矩阵

图 14.18:分类矩阵

注意

若要访问此特定部分的源代码,请参考packt.live/31ca5k6

你也可以在packt.live/329yJkF上在线运行此示例。

从后向消除过程可以看出,我们能够通过 250 个特征将准确率提高到 98%,相比于 活动 14.01 中的模型,在高维数据集上拟合逻辑回归模型,当时使用了一个人为增强的数据集,准确率为 97%

然而,应该注意的是,降维技术不应被视为提高任何模型性能的方法。降维技术应该从帮助我们在具有大量特征的数据集上拟合模型的角度来看待。当维度增加时,拟合模型变得不可行。如果将 活动 14.01 中使用的缩放因子从 300 增加到 500,就可以观察到这种现象。在这种情况下,使用当前资源无法完成模型拟合。

降维在这种情况下的作用是通过减少特征数量,从而使得模型能够在减少维度的情况下进行拟合,并且不会导致性能的大幅下降,有时甚至会带来结果的改善。然而,也应该注意到,像后向消除这样的算法计算量大。你应该已经注意到,当缩放因子仅为 2 时,识别前 250 个特征所需的时间。如果缩放因子更高,识别前 250 个特征所需要的时间和资源将大大增加。

在了解了后向消除方法后,我们来看看下一种技术,即前向特征选择。

前向特征选择

前向特征选择的工作原理与后向消除相反。在这个过程中,我们从一个初始特征开始,特征逐个加入,直到性能不再提升为止。详细过程如下:

  1. 从一个特征开始建立模型。

  2. 迭代模型构建过程 n 次,每次选择一个特征。选择能带来性能最大提升的特征。

  3. 一旦选择了第一个特征,就可以开始选择第二个特征。选择第二个特征的过程与 步骤 2 完全相同。将剩余的 n-1 个特征与第一个特征一起迭代,并观察模型的性能。选择能带来最大性能提升的特征作为第二个特征。

  4. 特征的迭代将继续,直到提取出我们设定的阈值数量的特征。

  5. 最终选择的特征集合将是那些能提供最大模型性能的特征。

现在让我们在下一个练习中实现这个算法。

练习 14.03:使用前向特征选择进行降维

在这个练习中,我们将通过前向特征选择来选择最佳特征,并观察模型的性能,从而拟合一个逻辑回归模型。我们将使用与之前相同的广告数据集,并且在这个练习中通过增加额外的特征来增强数据集。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本。

  2. 实现所有与 练习 14.01加载和清理数据集 类似的初步步骤,直到使用 MinMaxScaler() 进行数据集缩放:

    filename = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter14/Dataset/ad.data'
    import pandas as pd
    adData = pd.read_csv(filename,sep=",",header = None,\
                         error_bad_lines=False)
    X = adData.loc[:,0:1557]
    Y = adData[1558]
    import numpy as np
    for i in range(0,3):
        X[i] = X[i].str.replace("?", 'NaN')\
                   .values.astype(float)
    for i in range(3,1557):
        X[i] = X[i].replace("?", 'NaN').values.astype(float)
    for i in range(0,1557):
        X[i] = X[i].fillna(X[i].mean())
    from sklearn import preprocessing
    minmaxScaler = preprocessing.MinMaxScaler()
    X_tran = pd.DataFrame(minmaxScaler.fit_transform(X))
    
  3. 创建一个高维数据集。现在,将数据集人工扩展到 50 的倍数。将数据集扩展到更高倍数会导致笔记本因内存不足而崩溃。可以通过以下代码片段来实现:

    # Creating a high dimension dataset
    X_hd = pd.DataFrame(pd.np.tile(X_tran, (1, 50)))
    print(X_hd.shape)
    

    你应该得到如下输出:

    (3279, 77900)
    
  4. 将高维数据集拆分为训练集和测试集:

    from sklearn.model_selection import train_test_split
    # Splitting the data into train and test sets
    X_train, X_test, y_train, y_test = train_test_split\
                                       (X_hd, Y, test_size=0.3, \
                                        random_state=123)
    
  5. 现在我们定义阈值特征。一旦训练集和测试集创建完成,下一步是导入特征选择函数 SelectKBest。我们给这个函数的参数是我们想要的特征数量。特征是通过实验选择的,作为第一步,我们假设一个阈值。在这个例子中,我们假设阈值为 250。可以通过以下代码片段来实现:

    from sklearn.feature_selection import SelectKBest
    # feature extraction
    feats = SelectKBest(k=250)
    
  6. 迭代并获取最佳的阈值特征集。根据我们定义的阈值特征集,我们必须拟合训练集并获取最佳的阈值特征集。训练集拟合使用 .fit() 函数进行。我们还需要记录找到最佳特征集所花费的时间。可以通过以下代码片段来执行:

    # Fitting the features for training set
    import time
    t0 = time.time()
    fit = feats.fit(X_train, y_train)
    t1 = time.time()
    print("Forward selection fitting time:", \
          round(t1-t0, 3), "s")
    

    你应该得到如下类似的输出:

    Forward selection fitting time: 2.682 s
    

    我们可以看到,前向选择方法大约花费了 2.68 秒,这比反向选择方法的时间要短得多。

  7. 创建新的训练集和测试集。一旦我们确定了最佳特征集,我们需要修改训练集和测试集,使它们只包含这些选定的特征。可以通过 .transform() 函数来完成:

    # Creating new training set and test sets 
    features_train = fit.transform(X_train)
    features_test = fit.transform(X_test)
    
  8. 让我们在转换前和转换后验证训练集和测试集的形状:

    """
    Printing the shape of training and test sets 
    before transformation
    """
    print('Train shape before transformation',\
          X_train.shape)
    print('Test shape before transformation',\
          X_test.shape)
    """
    Printing the shape of training and test sets 
    after transformation
    """
    print('Train shape after transformation',\
          features_train.shape)
    print('Test shape after transformation',\
          features_test.shape)
    

    你应该得到如下输出:

    图 14.19:训练集和测试集的数据形状

    图 14.19:训练集和测试集的数据形状

    你可以看到,训练集和测试集的特征数都减少到每个 250 个特征。

  9. 现在我们在转换后的数据集上拟合一个逻辑回归模型,并记录拟合模型所需的时间:

    # Fitting a Logistic Regression Model
    from sklearn.linear_model import LogisticRegression
    import time
    t0 = time.time()
    forwardModel = LogisticRegression()
    forwardModel.fit(features_train, y_train)
    t1 = time.time()
    
  10. 打印总时间:

    print("Total training time:", round(t1-t0, 3), "s")
    

    你应该得到如下输出:

    Total training time: 0.035 s
    

    你可以看到,训练时间比在 活动 14.01在高维数据集上拟合逻辑回归模型 中拟合的模型要少,后者花费了 23.86 秒。这较短的时间归因于前向选择模型中的特征数量。

  11. 现在,对测试集进行预测并打印准确率指标:

    # Predicting with the forward model
    pred = forwardModel.predict(features_test)
    print('Accuracy of Logistic regression'\
          ' model prediction on test set: {:.2f}'
          .format(forwardModel.score(features_test, y_test)))
    

    你应该得到以下输出:

    Accuracy of Logistic regression model prediction on test set: 0.94
    
  12. 打印混淆矩阵:

    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到类似以下输出:

    图 14.20:结果混淆矩阵

    图 14.20:结果混淆矩阵

  13. 打印分类报告:

    from sklearn.metrics import classification_report
    # Getting the Classification_report
    print(classification_report(y_test, pred))
    

    你应该得到类似以下输出:

    图 14.21:结果分类报告

图 14.21:结果分类报告

注意

要访问此特定部分的源代码,请参考packt.live/2YhQE7X

本节目前没有在线交互示例,但可以像往常一样在 Google Colab 上运行。

正如我们从前向选择过程可以看到的,我们能够在使用250个特征时获得94%的准确率。这一得分低于使用后向消除法(98%)和在Activity 14.01中构建的基准模型(97%)所取得的分数,在高维数据集上拟合逻辑回归模型。然而,寻找最佳特征所花费的时间(2.68 秒)远低于后向消除法(230.35 秒)。

在接下来的部分中,我们将介绍主成分分析(PCA)。

主成分分析(PCA)

PCA 是一种非常有效的降维技术,它在不损害数据内容的情况下实现降维。PCA 的基本思想是首先识别数据集内不同变量之间的相关性。一旦识别出相关性,算法会决定以一种方式去除变量,以保持数据的变异性。换句话说,PCA 旨在找到不相关的数据源。

在原始变量上实施 PCA 会将其转化为一组全新的变量,称为主成分。每个主成分代表数据在与其他主成分正交的轴向上的变异性。这意味着第一个轴会沿着数据变异性最大的位置进行拟合。接下来,第二个轴的选择方式是它与第一个选定的轴正交(垂直),并覆盖下一个最高的变异性。

让我们通过一个示例来看一下 PCA 的概念。

我们将创建一个包含 2 个变量和每个变量 100 个随机数据点的样本数据集。使用rand()函数生成随机数据点。以下代码实现了这一过程:

import numpy as np
# Setting the seed for reproducibility
seed = np.random.RandomState(123)
# Generating an array of random numbers
X = seed.rand(100,2)
# Printing the shape of the dataset
X.shape

结果输出为:(100, 2)

注意

使用RandomState(123)函数定义一个随机状态。这是为了确保任何复现此示例的人都能得到相同的输出。

让我们使用matplotlib可视化这些数据:

import matplotlib.pyplot as plt
%matplotlib inline
plt.scatter(X[:, 0], X[:, 1])
plt.axis('equal')

你应该得到以下输出:

(-0.04635361265714105,
 1.0325632864350174,
 -0.003996887112708292,
 1.0429468329457663)

图 14.22:数据可视化

图 14.22:数据可视化

在图中,我们可以看到数据均匀地分布开来。

现在让我们为这个数据集找到主成分。我们将把这个二维数据集降维为一维数据集。换句话说,我们将把原始数据集压缩为它的一个主成分。

代码实现如下:

from sklearn.decomposition import PCA
# Defining one component
pca = PCA(n_components=1)
# Fitting the PCA function
pca.fit(X)
# Getting the new dataset
X_pca = pca.transform(X)
# Printing the shapes
print("Original data set:   ", X.shape)
print("Data set after transformation:", X_pca.shape)

你应该得到以下输出:

original shape: (100, 2)
transformed shape: (100, 1)

如代码中所示,我们首先通过'n_components' = 1参数定义主成分的数量。之后,PCA 算法会对输入数据集进行拟合。拟合后,初始数据集被转换为一个只有一个变量的新数据集,这个变量就是它的主成分。

该算法通过使用数据变异性最大化的轴将原始数据集转换为其第一个主成分。

为了可视化这个概念,假设我们将X_pca数据集从其原始形式进行逆转化,然后将这个数据与原始数据一起可视化。为了逆转化,我们使用.inverse_transform()函数:

# Reversing the transformation and plotting 
X_reverse = pca.inverse_transform(X_pca)
# Plotting the original data
plt.scatter(X[:, 0], X[:, 1], alpha=0.1)
# Plotting the reversed data
plt.scatter(X_reverse[:, 0], X_reverse[:, 1], alpha=0.9)
plt.axis('equal');

你应该得到以下输出:

图 14.23:带有逆转化的图示

图 14.23:带有逆转化的图示

正如我们在图中所看到的,橙色的数据点代表了具有最大变异性的轴。所有数据点都被投影到该轴上,从而生成第一个主成分。

当数据在不同的主成分中转换时,生成的数据点会与转换前的原始数据点有很大不同。每个主成分都会位于与其他主成分正交(垂直)的轴上。如果为上述示例生成第二个主成分,那么第二个主成分将在图中蓝色箭头所示的轴上。我们为模型构建选择主成分的方式是选择那些解释了某个方差阈值的主成分数量。

例如,如果原本有 1,000 个特征,我们将其减少到 100 个主成分,然后发现这 100 个主成分中,前 75 个主成分解释了数据 90%的变异性,那么我们就选择这 75 个主成分来构建模型。这个过程叫做根据解释的方差百分比选择主成分。

现在让我们看看如何在我们的用例中使用 PCA 作为降维工具。

练习 14.04:使用 PCA 进行降维

在这个练习中,我们将通过选择解释最大数据变异性的主成分来拟合一个逻辑回归模型。我们还将观察特征选择和模型构建过程的表现。我们将使用之前的相同广告数据集,并在此练习中增加一些额外的特征。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 实现练习 14.01中的初始步骤,加载和清理数据集,直到使用minmaxscaler()函数进行数据集缩放:

    filename = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter14/Dataset/ad.data'
    import pandas as pd
    adData = pd.read_csv(filename,sep=",",header = None,\
                         error_bad_lines=False)
    X = adData.loc[:,0:1557]
    Y = adData[1558]
    import numpy as np
    for i in range(0,3):
        X[i] = X[i].str.replace("?", 'NaN').values.astype(float)
    for i in range(3,1557):
        X[i] = X[i].replace("?", 'NaN').values.astype(float)
    for i in range(0,1557):
        X[i] = X[i].fillna(X[i].mean())
    from sklearn import preprocessing
    minmaxScaler = preprocessing.MinMaxScaler()
    X_tran = pd.DataFrame(minmaxScaler.fit_transform(X))
    
  3. 创建一个高维数据集。现在,我们将人工增加数据集的大小,倍增到 50 倍。将数据集增大到更高的倍数将导致笔记本崩溃,因为内存不足。这是通过以下代码片段实现的:

    # Creating a high dimension data set
    X_hd = pd.DataFrame(pd.np.tile(X_tran, (1, 50)))
    print(X_hd.shape)
    

    你应该得到以下输出:

    (3279, 77900)
    
  4. 让我们将高维数据集拆分为训练集和测试集:

    from sklearn.model_selection import train_test_split
    # Splitting the data into train and test sets
    X_train, X_test, y_train, y_test = train_test_split\
                                       (X_hd, Y, test_size=0.3, \
                                        random_state=123)
    
  5. 现在,我们将在训练集上拟合 PCA 函数。这是通过.fit()函数完成的,代码如下所示。我们还将记录在数据集上拟合 PCA 模型所需的时间:

    from sklearn.decomposition import PCA
    import time
    t0 = time.time()
    pca = PCA().fit(X_train)
    t1 = time.time()
    print("PCA fitting time:", round(t1-t0, 3), "s")
    

    你应该得到以下输出:

    PCS fitting time: 179.545 s
    

    我们可以看到,在数据集上拟合 PCA 函数所花的时间少于后向淘汰模型(230.35 秒),但高于前向选择方法(2.682 秒)。

  6. 现在,我们将通过绘制所有主成分所解释的累计方差来确定主成分的数量。方差的解释由pca.explained_variance_ratio_方法确定。这个结果通过以下代码片段在matplotlib中绘制:

    %matplotlib inline
    import numpy as np
    import matplotlib.pyplot as plt
    plt.plot(np.cumsum(pca.explained_variance_ratio_))
    plt.xlabel('Number of Principal Components')
    plt.ylabel('Cumulative explained variance');
    

    在代码中,np.cumsum()函数用于获取每个主成分的累计方差。

    你将得到以下图形作为输出:

    图 14.24:方差图

    图 14.24:方差图

    从图中,我们可以看到前250个主成分解释了超过90%的方差。根据这个图,我们可以决定希望保留多少个主成分,取决于它所解释的变异性。我们选择250个主成分来拟合我们的模型。

  7. 现在我们已经确认250个主成分解释了大量的变异性,让我们用250个主成分重新拟合训练集。这在以下代码片段中描述:

    # Defining PCA with 250 components
    pca = PCA(n_components=250)
    # Fitting PCA on the training set
    pca.fit(X_train)
    
  8. 现在,我们使用 200 个主成分来转换训练集和测试集:

    # Transforming training set and test set
    X_pca = pca.transform(X_train)
    X_test_pca = pca.transform(X_test)
    
  9. 在转换之前和转换之后,让我们验证训练集和测试集的形状:

    """
    Printing the shape of train and test sets before 
    and after transformation
    """
    print("original shape of Training set:   ", \
          X_train.shape)
    print("original shape of Test set:   ", \
          X_test.shape)
    print("Transformed shape of training set:", \
          X_pca.shape)
    print("Transformed shape of test set:", \
          X_test_pca.shape)
    

    你应该得到以下输出:

    图 14.25:转换后的训练集和测试集以及原始训练集和测试集

    图 14.25:转换后的训练集和测试集以及原始训练集和测试集

    你可以看到,训练集和测试集都被缩减为各自250个特征。

  10. 现在,我们将在转换后的数据集上拟合逻辑回归模型,并记录拟合模型所需的时间:

    # Fitting a Logistic Regression Model
    from sklearn.linear_model import LogisticRegression
    import time
    pcaModel = LogisticRegression()
    t0 = time.time()
    pcaModel.fit(X_pca, y_train)
    t1 = time.time()
    
  11. 打印总时间:

    print("Total training time:", round(t1-t0, 3), "s")
    

    你应该得到以下输出:

    Total training time: 0.293 s
    

    你可以看到,训练时间远低于在活动 14.01中拟合的模型,在高维数据集上拟合逻辑回归模型,其时间为 23.86 秒。这个更短的时间归因于在 PCA 中选择了更少的特征,即250个特征。

  12. 现在,在测试集上进行预测,并打印出准确性指标:

    # Predicting with the pca model
    pred = pcaModel.predict(X_test_pca)
    print('Accuracy of Logistic regression model '\
          'prediction on test set: {:.2f}'\
          .format(pcaModel.score(X_test_pca, y_test)))
    

    你应该得到以下输出:

    图 14.26:逻辑回归模型的准确性

    图 14.26:逻辑回归模型的准确性

    你可以看到,所有特征的准确性(97%)和前向选择模型(94%)的准确性均优于基准模型。

  13. 打印混淆矩阵:

    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到以下输出:

    图 14.27:结果混淆矩阵

    图 14.27:结果混淆矩阵

  14. 打印分类报告:

    from sklearn.metrics import classification_report
    # Getting the Classification_report
    print(classification_report(y_test, pred))
    

    你应该得到以下输出:

    图 14.28:结果分类矩阵

图 14.28:结果分类矩阵

注意

要访问本节的源代码,请参考packt.live/3iXNVbq

本节目前没有在线交互示例,但可以像往常一样在 Google Colab 上运行。

从结果可以明显看出,我们得到了 98% 的分数,优于基准模型。其表现较高的一个原因可能是通过 PCA 方法创建了无关的主成分,从而提升了性能。

在下一节中,我们将讨论独立成分分析(ICA)。

独立成分分析(ICA)

ICA 是一种降维技术,其概念上与 PCA 路径相似。ICA 和 PCA 都试图通过线性组合原始数据来推导新的数据源。

然而,它们之间的区别在于它们用于寻找新数据源的方法。PCA 尝试寻找无关的数据源,而 ICA 则尝试寻找独立的数据源。

ICA 在降维方面有与 PCA 非常相似的实现。

让我们看看 ICA 在我们用例中的实现。

练习 14.05:使用独立成分分析进行降维

在本次练习中,我们将使用 ICA 技术拟合一个逻辑回归模型,并观察模型的表现。我们将使用之前相同的广告数据集,并在本次练习中增加额外的特征。

以下步骤将帮助你完成本次练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 实现 练习 14.01加载和清理数据集 中的所有步骤,直到使用 MinMaxScaler() 对数据集进行缩放:

    filename = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter14/Dataset/ad.data'
    import pandas as pd
    adData = pd.read_csv(filename,sep=",",header = None,\
                         error_bad_lines=False)
    X = adData.loc[:,0:1557]
    Y = adData[1558]
    import numpy as np
    for i in range(0,3):
        X[i] = X[i].str.replace("?", 'NaN')\
                   .values.astype(float)
    for i in range(3,1557):
        X[i] = X[i].replace("?", 'NaN')\
                   .values.astype(float)  
    for i in range(0,1557):
        X[i] = X[i].fillna(X[i].mean())
    from sklearn import preprocessing
    minmaxScaler = preprocessing.MinMaxScaler()
    X_tran = pd.DataFrame(minmaxScaler.fit_transform(X))
    
  3. 现在让我们将数据集人工扩展至 50 倍。将数据集扩展到超过 50 倍的因子将导致笔记本因内存不足而崩溃。以下代码片段实现了这一点:

    # Creating a high dimension data set
    X_hd = pd.DataFrame(pd.np.tile(X_tran, (1, 50)))
    print(X_hd.shape)
    

    你应该得到以下输出:

    (3279, 77900)
    
  4. 让我们将高维数据集分割成训练集和测试集:

    from sklearn.model_selection import train_test_split
    # Splitting the data into train and test sets
    X_train, X_test, y_train, y_test = train_test_split\
                                       (X_hd, Y, test_size=0.3,\
                                        random_state=123)
    
  5. 让我们加载 ICA 函数 FastICA,然后定义我们所需的成分数。我们将使用与 PCA 相同数量的成分:

    # Defining the ICA with number of components
    from sklearn.decomposition import FastICA 
    ICA = FastICA(n_components=250, random_state=123)
    
  6. 一旦 ICA 方法被定义,我们将在训练集上拟合该方法,并将训练集转换为一个具有所需数量组件的新训练集。我们还会记录拟合和转换所需的时间:

    """
    Fitting the ICA method and transforming the 
    training set import time
    """
    t0 = time.time()
    X_ica=ICA.fit_transform(X_train)
    t1 = time.time()
    print("ICA fitting time:", round(t1-t0, 3), "s")
    

    在代码中,.fit()函数用于在训练集上拟合,transform()方法用于获取具有所需特征数量的新训练集。

    你应该得到以下输出:

    ICA fitting time: 203.02 s
    

    我们可以看到,实现 ICA 花费的时间远超过 PCA(179.54 秒)。

  7. 我们现在使用250个成分对测试集进行转换:

    # Transforming the test set 
    X_test_ica=ICA.transform(X_test)
    
  8. 让我们在转换前后验证训练集和测试集的形状:

    """
    Printing the shape of train and test sets 
    before and after transformation
    """
    print("original shape of Training set:   ", \
          X_train.shape)
    print("original shape of Test set:   ", \
          X_test.shape)
    print("Transformed shape of training set:", \
          X_ica.shape)
    print("Transformed shape of test set:", \
          X_test_ica.shape)
    

    你应该得到以下输出:

    图 14.29:原始数据集和转换后数据集的形状

    图 14.29:原始数据集和转换后数据集的形状

    你可以看到训练集和测试集都被减少到每个250个特征。

  9. 现在让我们在转换后的数据集上拟合逻辑回归模型,并记录所花费的时间:

    # Fitting a Logistic Regression Model
    from sklearn.linear_model import LogisticRegression
    import time
    icaModel = LogisticRegression()
    t0 = time.time()
    icaModel.fit(X_ica, y_train)
    t1 = time.time()
    
  10. 打印总时间:

    print("Total training time:", round(t1-t0, 3), "s")
    

    你应该得到以下输出:

    Total training time: 0.054 s
    
  11. 现在让我们对测试集进行预测,并打印准确性指标:

    # Predicting with the ica model
    pred = icaModel.predict(X_test_ica)
    print('Accuracy of Logistic regression model '\
          'prediction on test set: {:.2f}'\
          .format(icaModel.score(X_test_ica, y_test)))
    

    你应该得到以下输出:

    Accuracy of Logistic regression model prediction on test set: 0.87
    

    我们可以看到,ICA 模型的结果比其他模型差。

  12. 打印混淆矩阵:

    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到以下输出:

    图 14.30:结果混淆矩阵

    图 14.30:结果混淆矩阵

    我们可以看到,ICA 模型在广告分类方面表现不佳。所有的示例都被错误地分类为非广告。我们可以得出结论,ICA 不适用于此数据集。

  13. 打印分类报告:

    from sklearn.metrics import classification_report
    # Getting the Classification_report
    print(classification_report(y_test, pred))
    

    你应该得到以下输出:

    图 14.31:结果分类报告

图 14.31:结果分类报告

注意

要访问此特定部分的源代码,请参考packt.live/325H88Q

本节目前没有在线交互示例,但可以在 Google Colab 上照常运行。

如我们所见,将数据转换为前 250 个独立成分并未捕获数据中所有必要的变异性。这导致了该方法分类结果的下降。我们可以得出结论,ICA 不适用于此数据集。

还观察到,找到最佳独立特征所需的时间比 PCA 要长。然而,应该注意的是,不同的方法根据输入数据会有不同的结果。尽管 ICA 不适合此数据集,但它仍然是一个强大的降维方法,应该是数据科学家工具库中的一部分。

通过这个练习,你可能会提出几个问题:

  • 你认为我们如何利用 ICA 改善分类结果?

  • 增加成分数量会导致准确性指标的边际提高。

  • 由于提高结果的策略是否会产生其他副作用?

增加组件数量也会导致逻辑回归模型的训练时间延长。

因子分析

因子分析是一种通过将高度相关的变量进行分组来实现降维的技术。让我们来看一个来自广告预测的示例。

在我们的数据集中,可能有许多特征描述了网页上广告图像的几何形状(图像的大小和形状)。这些特征可能是相关的,因为它们指向图像的特定特征。

同样,可能有许多特征描述了 URL 中出现的锚文本或短语,这些特征高度相关。因子分析从数据中查看这些相关的组,并将它们分组为潜在因子。因此,如果有 10 个原始特征描述图像的几何形状,因子分析将它们分组为一个特征,来描述图像的几何形状。这些组中的每一个都被称为因子。由于许多相关特征被组合成一个组,最终的特征数量将比数据集的原始维度要小得多。

现在让我们看看如何将因子分析作为一种降维技术来实现。

练习 14.06:使用因子分析进行降维

在这个练习中,我们将在将原始维度降至一些关键因子后,拟合一个逻辑回归模型,然后观察模型的表现。

以下步骤将帮助你完成这个练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 实现与练习 14.01加载和清理数据集相同的初始步骤,直到使用 minmaxscaler() 函数对数据集进行缩放:

    filename = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter14/Dataset/ad.data'
    import pandas as pd
    adData = pd.read_csv(filename,sep=",",header = None,\
                         error_bad_lines=False)
    X = adData.loc[:,0:1557]
    Y = adData[1558]
    import numpy as np
    for i in range(0,3):
        X[i] = X[i].str.replace("?", 'NaN')\
                   .values.astype(float)
    for i in range(3,1557):
        X[i] = X[i].replace("?", 'NaN')\
                   .values.astype(float)  
    for i in range(0,1557):
        X[i] = X[i].fillna(X[i].mean())
    from sklearn import preprocessing
    minmaxScaler = preprocessing.MinMaxScaler()
    X_tran = pd.DataFrame(minmaxScaler.fit_transform(X))
    
  3. 现在让我们将数据集人工增大到 50 倍。将数据集增大到超过 50 的因子数会导致笔记本崩溃,因为内存不足。实现代码片段如下:

    # Creating a high dimension data set
    X_hd = pd.DataFrame(pd.np.tile(X_tran, (1, 50)))
    print(X_hd.shape)
    

    你应该得到以下输出:

    (3279, 77900)
    
  4. 让我们将高维数据集拆分为训练集和测试集:

    from sklearn.model_selection import train_test_split
    # Splitting the data into train and test sets
    X_train, X_test, y_train, y_test = train_test_split\
                                       (X_hd, Y, test_size=0.3,\
                                        random_state=123)
    
  5. 因子分析中的一个重要步骤是定义数据集中的因子数量。这个步骤是通过实验来实现的。在我们的案例中,我们将任意假设有 20 个因子。实现方式如下:

    # Defining the number of factors
    from sklearn.decomposition import FactorAnalysis
    fa = FactorAnalysis(n_components = 20,\
                        random_state=123)
    

    因子的数量是通过 n_components 参数来定义的。我们还为可重复性定义了随机状态。

  6. 一旦定义了因子方法,我们将把方法应用于训练集,并将训练集转化为一个新的训练集,得到所需数量的因子。我们还将记录拟合所需因子数量的时间:

    """
    Fitting the Factor analysis method and 
    transforming the training set
    """
    import time
    t0 = time.time()
    X_fac=fa.fit_transform(X_train)
    t1 = time.time()
    print("Factor analysis fitting time:", \
          round(t1-t0, 3), "s")
    

    在代码中,.fit() 函数用于拟合训练集,transform() 方法用于获取一个新的训练集,该训练集具有所需数量的因子。

    你应该得到以下输出:

    Factor analysis fitting time: 130.688 s
    

    因子分析也是一种计算密集型方法。这也是只选择 20 个因子的原因。我们可以看到,它已经花费了 130.688 秒来处理 20 个因子。

  7. 现在我们使用相同数量的因子来转换测试集:

    # Transforming the test set 
    X_test_fac=fa.transform(X_test)
    
  8. 在转换前后,我们来验证训练集和测试集的形状:

    """
    Printing the shape of train and test sets 
    before and after transformation
    """
    print("original shape of Training set:   ", \
          X_train.shape)
    print("original shape of Test set:   ", \
          X_test.shape)
    print("Transformed shape of training set:", \
          X_fac.shape)
    print("Transformed shape of test set:", \
          X_test_fac.shape)
    

    你应该得到以下输出:

    图 14.32:原始和转换后的数据集值

    图 14.32:原始和转换后的数据集值

    你可以看到训练集和测试集都已经缩减为每个20个因子。

  9. 现在我们来对转换后的数据集拟合逻辑回归模型,并记录拟合模型所需的时间:

    # Fitting a Logistic Regression Model
    from sklearn.linear_model import LogisticRegression
    import time
    facModel = LogisticRegression()
    t0 = time.time()
    facModel.fit(X_fac, y_train)
    t1 = time.time()
    
  10. 打印总时间:

    print("Total training time:", round(t1-t0, 3), "s")
    

    你应该得到以下输出:

    Total training time: 0.028 s
    

    我们可以看到,拟合逻辑回归模型所需的时间与其他方法相当。

  11. 现在我们来对测试集进行预测并打印准确率指标:

    # Predicting with the factor analysis model
    pred = facModel.predict(X_test_fac)
    print('Accuracy of Logistic regression '\
          'model prediction on test set: {:.2f}'
          .format(facModel.score(X_test_fac, y_test)))
    

    你应该得到以下输出:

    Accuracy of Logistic regression model prediction on test set: 0.92
    

    我们可以看到,因子模型的结果优于 ICA 模型,但比其他模型的结果差。

  12. 打印混淆矩阵:

    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到以下输出:

    图 14.33:结果混淆矩阵

    图 14.33:结果混淆矩阵

    我们可以看到,因子模型在分类广告方面的表现优于 ICA 模型。然而,仍然有大量的假阳性。

  13. 打印分类报告:

    from sklearn.metrics import classification_report
    # Getting the Classification_report
    print(classification_report(y_test, pred))
    

    你应该得到以下输出:

    图 14.34:结果分类报告

图 14.34:结果分类报告

注意

要访问此特定章节的源代码,请参考 packt.live/32b9SNk

本节目前没有在线交互示例,但可以像往常一样在 Google Colab 上运行。

正如我们在结果中看到的,通过将变量减少到仅 20 个因子,我们能够获得不错的分类结果。尽管结果有所退化,但我们仍然有一个可管理的特征数量,这将在任何算法中很好地扩展。准确度度量与特征管理能力之间的平衡需要通过更多实验来探索。

你认为我们如何改进因子分析的分类结果?

好吧,增加组件的数量会导致准确率指标的提高。

比较不同的降维技术

现在我们已经了解了不同的降维技术,让我们将所有这些技术应用到一个新的数据集上,这个数据集将从现有的广告数据集中创建。

我们将从已知分布中随机抽取一些数据点,然后将这些随机样本添加到现有数据集中,以创建一个新数据集。让我们进行一个实验,看看如何从现有数据集中创建一个新数据集。

我们导入必要的库:

import pandas as pd
import numpy as np

接下来,我们创建一个虚拟数据框。

我们将使用一个包含两行三列的小数据集作为本示例。我们使用pd.np.array()函数创建数据框:

# Creating a simple data frame
df = pd.np.array([[1, 2, 3], [4, 5, 6]])
print(df.shape)
df

你应该得到以下输出:

图 14.35:示例数据框

图 14.35:示例数据框

接下来,我们将采样一些数据点,数据框的形状与我们创建的数据框相同。

从均值为0,标准差为0.1的正态分布中采样一些数据点。我们在第三章,二分类中简要提到了正态分布。正态分布有两个参数,第一个是均值,它是分布中所有数据的平均值;第二个是标准差,它是衡量数据点分布程度的指标。

通过假设均值和标准差,我们将能够使用np.random.normal() Python 函数从正态分布中绘制样本。我们需要为此函数提供的参数是均值、标准差和新数据集的形状。

让我们看看如何在代码中实现这一点:

# Defining the mean and standard deviation
mu, sigma = 0, 0.1 
# Generating random sample
noise = np.random.normal(mu, sigma, [2,3]) 
noise.shape

你应该得到以下输出:

(2, 3)

正如我们所看到的,我们提供了均值(mu)、标准差(sigma)以及数据框形状[2,3],以生成新的随机样本。

打印采样的数据框:

# Sampled data frame
noise

你将得到类似以下的输出:

array([[-0.07175021, -0.21135372,  0.10258917],
       [ 0.03737542,  0.00045449, -0.04866098]])

下一步是将原始数据框和采样数据框添加在一起,得到新的数据集:

# Creating a new data set by adding sampled data frame
df_new = df + noise
df_new

你应该得到类似以下的输出:

array([[0.92824979, 1.78864628, 3.10258917],
       [4.03737542, 5.00045449, 5.95133902]])

在了解如何创建新数据集后,让我们在接下来的活动中应用这些知识。

活动 14.02:在增强广告数据集上比较降维技术

你已经学会了不同的降维技术。现在你想确定哪种技术最适合你将要创建的数据集。

提示:在本活动中,我们将使用你迄今为止在所有练习中使用的不同技术。你还将像前一部分那样创建一个新的数据集。

完成此活动的步骤如下:

  1. 打开一个新的 Colab 笔记本。

  2. 对原始广告数据进行归一化,并推导出转化后的自变量X_tran

  3. 使用pd.np.tile()函数通过复制列两次来创建高维数据集。

  4. 从均值=0,标准差=0.1 的正态分布中创建随机样本。根据第 3 步创建的数据集形状,制作新的数据集。

  5. 添加高维数据集和随机样本,得到新的数据集。

  6. 将数据集分为训练集和测试集。

  7. 使用以下步骤实现向后消除法:

    使用RFE()函数实现向后消除法步骤。

    使用逻辑回归作为模型,选择最佳的300个特征。

    在训练集上拟合RFE()函数,并测量拟合 RFE 模型所需的时间。

    使用 RFE 模型变换训练集和测试集。

    在变换后的训练集上拟合逻辑回归模型。

    在测试集上进行预测,并打印准确率、混淆矩阵和分类报告。

  8. 使用以下步骤实现前向选择技术:

    使用SelectKBest()函数定义特征数量。选择最佳的300个特征。

    使用.fit()函数在训练集上拟合前向选择,并记录拟合所需的时间。

    使用.transform()函数变换训练集和测试集。

    在变换后的训练集上拟合逻辑回归模型。

    在变换后的测试集上进行预测,并打印准确率、混淆矩阵和分类报告。

  9. 实现 PCA:

    使用PCA()函数定义主成分。使用 300 个组件。

    在训练集上拟合PCA()。注意时间。

    使用.transform()函数变换训练集和测试集,获取这些数据集的相应组件数量。

    在变换后的训练集上拟合逻辑回归模型。

    在变换后的测试集上进行预测,并打印准确率、混淆矩阵和分类报告。

  10. 实现 ICA:

    使用FastICA()函数并使用300个组件定义独立成分。

    在训练集上拟合独立成分并变换训练集。记录实现的时间。

    使用.transform()函数将测试集变换,获取这些数据集的相应组件数量。

    在变换后的训练集上拟合逻辑回归模型。

    在变换后的测试集上进行预测,并打印准确率、混淆矩阵和分类报告。

  11. 实现因子分析:

    使用FactorAnalysis()函数定义因子数量,并使用30个因子。

    在训练集上拟合因子并变换训练集。记录实现的时间。

    使用.transform()函数将测试集变换,获取这些数据集的相应组件数量。

    在变换后的训练集上拟合逻辑回归模型。

    在变换后的测试集上进行预测,并打印准确率、混淆矩阵和分类报告。

  12. 比较所有方法的输出结果。

预期输出

以下是结果的示例汇总表:

图 14.36:所有降维技术的汇总输出

图 14.36:所有降维技术的汇总输出

注意事项

本活动的解决方案可以在这里找到:packt.live/2GbJloz

在本活动中,我们使用从互联网广告数据集创建的新数据集实现了五种不同的降维方法。

从表格化的结果中,我们可以看到三种方法(反向淘汰、前向选择和 PCA)具有相同的准确度分数。因此,选择最佳方法的标准应该是基于降维所需的时间。根据这一标准,前向选择方法是最好的,其次是 PCA。

对于第三名,我们应该在准确性和降维所花时间之间找到平衡。我们可以看到因子分析和反向淘汰的准确度非常接近,分别为 96%和 97%。然而,反向淘汰所需的时间比因子分析长得多。因此,我们应该将因子分析作为第三名,即使它的准确度略低于反向淘汰。最后的位置应归于 ICA,因为它的准确度远低于其他所有方法。

总结

在这一章中,我们学习了各种降维技术。让我们总结一下本章所学内容。

本章开始时,我们介绍了现代数据集在可扩展性方面的一些挑战。为了更好地理解这些挑战,我们下载了互联网广告数据集,并进行了一项活动,在其中我们亲眼见证了大数据集所带来的可扩展性问题。在这个活动中,我们人工创建了一个大数据集,并对其进行了逻辑回归建模。

在后续部分,我们介绍了五种不同的降维方法。

反向特征淘汰是基于逐步淘汰特征,直到没有明显的准确度下降为止。这个方法计算量较大,但我们得到了比基准模型更好的结果。

前向特征选择与反向淘汰方法相反,它一次选择一个特征,以获得我们预定的最佳特征集。这个方法也计算量较大。我们还发现这个方法的准确度稍微低一些。

主成分分析PCA)旨在寻找彼此正交的成分,并最好地解释数据的变异性。我们使用 PCA 得到了比基准模型更好的结果。

独立成分分析ICA)与 PCA 相似;然而,它在选择成分的方式上有所不同。ICA 从数据集中寻找独立的成分。我们看到,ICA 在我们的实验中取得了最差的结果之一。

因子分析的核心是寻找最佳描述数据的相关特征组或因子。我们通过因子分析取得的结果远优于 ICA。

本章的目标是为你提供一套技术,帮助应对模型可扩展性面临挑战的场景。获得良好结果的关键是理解在不同场景中应使用哪种方法。这可以通过大量的实践和对许多大规模数据集的实验来实现。

在本章中,我们学习了一套用于管理可扩展性的工具,接下来我们将进入下一章,讨论提升性能的问题。在下一章中,你将接触到一种叫做集成学习的技术,这种技术将帮助提升你的机器学习模型的性能。

第十五章:15. 集成学习

概述

到本章结束时,你将能够描述集成学习,并将不同的集成学习技术应用到你的数据集上。你还将能够将数据集拟合到模型上,并分析集成学习后的结果。

在本章中,我们将使用信用卡申请数据集,尝试预测信用卡申请是否会被批准。

介绍

在上一章中,我们学习了多种技术,如向后淘汰技术、因子分析等,帮助我们处理高维数据集。

在本章中,我们将通过另一组技术进一步丰富我们的技能库,称为 集成学习,我们将在其中涉及不同的集成学习技术,如下所示:

  • 平均

  • 加权平均

  • 最大投票法

  • 装袋法

  • 提升法

  • 融合

集成学习

集成学习,顾名思义,是一种将多个机器学习模型结合起来生成更优模型的方法,从而降低变异性/方差和偏差,并提高性能。

在探讨什么是集成学习之前,让我们通过经典的偏差-方差象限来理解偏差和方差的概念,如下图所示:

图 15.1:偏差-方差象限

图 15.1:偏差-方差象限

方差

方差是衡量数据分散程度的指标。在机器学习中,高方差的模型意味着在相同的测试集上生成的预测结果,在使用不同的训练集来拟合模型时,会有显著的差异。高方差的根本原因可能是模型过于关注训练数据的特定细节,而没有对输入和输出之间的关系进行有效的概括。理想情况下,我们希望每个机器学习模型的方差都很低。

偏差

偏差是实际值和我们预测的平均值之间的差异。低偏差表示预测值与实际值非常接近。高偏差则意味着模型将输入与输出之间的关系过于简化,导致测试集上的错误率较高,这同样是一个不理想的结果。

图 15.1 帮助我们可视化偏差和方差之间的权衡。左上角展示的是偏差高、方差低的情景。右上象限显示的是偏差和方差都高的情景。从图中可以看出,当偏差高时,它距离真实值较远,在此情况下,真实值即为 靶心。方差的存在表现为箭头是分散的还是聚集在某一处。

集成模型结合了许多较弱的模型,这些模型在方差和偏差上有所不同,从而创建一个更好的模型,超越单个较弱的模型。集成模型体现了“群体的智慧”这一格言。在本章中,我们将学习不同的集成技术,分为两类:简单技术和高级技术:

图 15.2:不同的集成学习方法

图 15.2:不同的集成学习方法

商业背景

你在银行的信用卡部门工作。公司运营部门的负责人请求你帮助确定一个客户是否值得信贷。你已经获得了信用卡操作数据。

这个数据集包含了大约 15 个变量的信用卡申请数据。变量包括与信用卡操作相关的连续数据和分类数据。数据集的标签是一个标志,指示申请是否已被批准。

你希望为数据集拟合一些基准模型,并尝试一些集成学习方法,以解决问题并制定一个工具,预测某个客户的信用申请是否应被批准。

练习 15.01:加载、探索和清理数据

在本次练习中,我们将下载信用卡数据集,将其加载到 Colab 笔记本中,并进行一些基本的探索。此外,我们还将清理数据集,以移除不需要的字符。

注意

我们将在本次练习中使用的数据集来自 UCI 机器学习库:packt.live/39NCgZ2。你可以从我们的 GitHub 下载该数据集:packt.live/3018OdD

以下步骤将帮助你完成本次练习:

  1. 打开一个新的 Colab 笔记本文件。

  2. 现在,将 pandas 导入到你的 Colab 笔记本中:

    import pandas as pd
    
  3. 接下来,设置 crx.data 上传至 GitHub 仓库的路径,如以下代码片段所示:

    #Load data from the GitHub repository
    filename = 'https://raw.githubusercontent.com'\
               '/PacktWorkshops/The-Data-Science-Workshop'\
               '/master/Chapter15/Dataset/crx.data'
    
  4. 使用 pandas 数据框架的 pd.read_csv() 函数读取文件:

    credData = pd.read_csv(filename,sep= ",",\
                           header = None,\
                           na_values =  "?")
    credData.head()
    

    pd.read_csv() 函数的参数是文件名(以字符串形式)和 CSV 文件的分隔符,默认是,

    注意

    数据集没有标题行;我们通过使用 header = None 命令来特别指定这一点。

    我们通过使用 na_values = "?" 参数,将数据集中的缺失值(表示为 ?)替换为 na 值。这一替换便于后续处理。

    读取文件后,使用 .head() 函数打印数据框架。你应该会得到以下输出:

    图 15.3:将数据加载到 Colab 笔记本

    图 15.3:将数据加载到 Colab 笔记本

  5. 将类别更改为 10

    如果你注意到数据集中的话,第 15 列表示的类别是特殊字符:+代表已批准,-代表未批准。你需要将其更改为数值1(表示已批准)和0(表示未批准),如下代码片段所示:

    # Changing the Classes to 1 & 0
    credData.loc[credData[15] == '+' , 15] = 1
    credData.loc[credData[15] == '-' , 15] = 0
    credData.head()
    

    你应该得到以下输出:

    图 15.4:替换特殊字符后的数据框

    .loc() was used to locate the fifteenth column and replace the + and - values with 1 and 0, respectively.
    
  6. 查找数据集中的null值数量。

    现在,我们将使用.isnull()函数查找每个特征中的null值数量。.sum()函数将汇总数据集中每列的所有空值。

    这在以下代码片段中表示:

    # Finding number of null values in the data set
    credData.isnull().sum()
    

    你应该得到以下输出:

    图 15.5:总结数据集中的空值

    图 15.5:总结数据集中的空值

    从前面的输出可以看到,有许多列包含null值。

  7. 现在,打印每列的形状和数据类型:

    # Printing Shape and data types
    print('Shape of raw data set',credData.shape)
    print('Data types of data set',credData.dtypes)
    

    你应该得到以下输出:

    图 15.6:每列的形状和数据类型

    图 15.6:每列的形状和数据类型

  8. 移除包含na值的行。

    为了清理数据集,我们使用.dropna()函数和以下代码片段来移除所有包含na值的行:

    # Dropping all the rows with na values
    newcred = credData.dropna(axis = 0)
    newcred.shape
    

    你应该得到以下输出:

    (653, 16)
    

    如你所见,大约有 37 行包含na值的行被移除。在代码片段中,我们定义了axis = 0,表示删除na值时应该沿着行进行。

  9. 验证是否存在null值:

    # Verifying no null values exist
    newcred.isnull().sum()
    

    你应该得到以下输出:

    图 15.7:验证没有空值存在

    图 15.7:验证没有空值存在

  10. 接下来,将类别变量转换为虚拟值。

    从数据类型可以看出,很多变量是类别类型的,需要使用pd.get_dummies()函数将它们转换为虚拟变量。这可以通过以下代码片段完成:

    """
    Separating the categorical variables to 
    make dummy variables
    """
    credCat = pd.get_dummies(newcred[[0,3,4,5,6,8,9,11,12]])
    
  11. 分离数值变量。

    我们还将从原始数据集中分离出数值变量,并将它们与虚拟变量进行连接。此步骤如下所示:

    # Separating the numerical variables
    credNum = newcred[[1,2,7,10,13,14]]
    

    注意

    你可以通过运行命令credCatcredNum来查看这些新的 DataFrame。

  12. 创建Xy变量。虚拟变量和数值变量将被连接起来形成X变量。y变量将通过提取数据集中的标签单独创建。让我们通过以下代码片段看看这些步骤的实际操作:

    """
    Making the X variable which is a concatenation 
    of categorical and numerical data
    """
    X = pd.concat([credCat,credNum],axis = 1)
    print(X.shape)
    # Separating the label as y variable
    y = pd.Series(newcred[15], dtype="int")
    print(y.shape)
    

    你应该得到以下输出:

    (653, 46)
    (653,)
    
  13. 使用MinMaxScaler()函数对数据集进行归一化:

    # Normalizing the data sets
    # Import library function
    from sklearn import preprocessing
    # Creating the scaling function
    minmaxScaler = preprocessing.MinMaxScaler()
    # Transforming with the scaler function
    X_tran = pd.DataFrame(minmaxScaler.fit_transform(X))
    
  14. 将数据集分成训练集和测试集。

    作为数据准备的最后一步,我们将使用train_test_split()函数将数据集划分为训练集和测试集:

    from sklearn.model_selection import train_test_split
    # Splitting the data into train and test sets
    X_train, X_test, y_train, y_test = train_test_split\
                                       (X_tran, y, test_size=0.3,\
                                        random_state=123)
    

    注意

    要访问此特定部分的源代码,请参考packt.live/31hM3od

    你也可以在线运行这个示例:packt.live/3glWdZf

我们现在有了准备好的数据集可以进行进一步的操作。像往常一样,让我们从在清洗后的数据集上使用逻辑回归拟合基准模型开始。这将在下一活动中实现。

活动 15.01:在信用卡数据上拟合逻辑回归模型

你刚刚清洗了收到的数据集,目的是预测客户的信用度。在应用集成学习方法之前,你希望先在数据集上拟合一个基准模型。

执行以下步骤来完成本活动:

  1. 打开一个新的 Colab 笔记本。

  2. 完成所有来自练习 15.01加载、探索和清理数据的适当步骤,直到你将数据集分割为训练集和测试集。

  3. 在训练集上拟合逻辑回归模型。

  4. 获取测试集上的预测结果。

  5. 打印基准模型的混淆矩阵和分类报告。

    在对数据集进行逻辑回归模型拟合后,你应该得到类似以下的输出:

    图 15.8:拟合逻辑回归模型后的预期输出

图 15.8:拟合逻辑回归模型后的预期输出

注意

请注意,由于预测过程中的变动,你的输出结果可能不会完全相同。

本活动的解决方案可以在这里找到:packt.live/2GbJloz

在本活动中,我们创建了一个基准模型,以便与后续的模型进行比较。

从输出中可以看出,我们通过基准模型达到了0.89的准确率。

现在我们已经拟合了基准模型,接下来将在下一部分探索不同的集成学习方法,从简单方法开始。

集成学习的简单方法

正如本章早些时候定义的,集成学习的核心就是将各个模型的优点结合起来,获得一个更强大的模型。在本部分中,我们将探索一些简单的技术,比如:

  • 平均法

  • 加权平均

  • 最大投票

让我们依次看看每一种方法。

平均法

平均法是一种简单的集成学习方式;然而,它也非常有用。这种技术的基本思想是取多个独立模型的预测结果,然后将这些预测结果平均,从而生成最终的预测。假设通过平均不同个体学习者的预测结果,我们可以消除各个学习者的错误,从而生成一个优于基模型的模型。使得平均法有效的一个前提是基模型的预测结果必须是互不相关的。这意味着各个独立模型不应该犯相同类型的错误。模型的多样性是确保错误互不相关的关键因素。

在实现平均技术时,预测结果中有一些细微差别需要注意。在预测时,到目前为止,我们一直在使用 predict() 函数。正如你现在可能已经知道的那样,predict() 函数输出的是概率最高的类别。例如,在我们的基准模型中,当我们对测试集进行预测时,得到的输出是每个测试样本的 '1' 或 '0'。还有另一种预测方式,即生成每个类别的概率。如果我们为我们的基准模型输出每个类别的概率,我们会得到每个样本对应每个类别的两个输出。这在以下表格中的示例预测中得到了展示:

图 15.9:一个例子预测

图 15.9:一个例子预测

从前面的例子中,我们可以看到每个测试样本有两个输出,分别对应每个类别的概率。因此,对于第一个样本,预测结果是类别 '1',因为类别 10.902)的概率高于类别 00.098)。对于样本 23,各自的预测结果是类别 0 和类别 1

当我们通过平均法生成集成时,我们生成的是每个类别的概率,而不是类别预测结果。每个类别的概率是通过一个名为 predict_proba() 的单独函数来预测的。我们将在练习 15.02中看到平均法的实现,使用平均技术的集成模型

练习 15.02:使用平均技术的集成模型

在本练习中,我们将使用平均技术实现一个集成模型。我们将用于本练习的基本模型是逻辑回归模型,我们将其作为基准模型,KNN 和随机森林模型,它们分别在第四章使用随机森林进行多类分类第八章超参数调优中介绍:

  1. 打开一个新的 Colab 笔记本。

  2. 执行练习 15.01中的所有适当步骤,加载、探索和清理数据,将数据集分成训练集和测试集。

  3. 让我们定义三个基本模型。导入我们将作为基本模型使用的分类器:

    from sklearn.linear_model import LogisticRegression
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.ensemble import RandomForestClassifier
    model1 = LogisticRegression(random_state=123)
    model2 = KNeighborsClassifier(n_neighbors=5)
    model3 = RandomForestClassifier(n_estimators=500)
    
  4. 在训练集上拟合所有三个模型:

    # Fitting all three models on the training data
    model1.fit(X_train,y_train)
    model2.fit(X_train,y_train)
    model3.fit(X_train,y_train)
    
  5. 我们现在将使用 predict_proba() 函数预测每个模型的概率:

    """
    Predicting probabilities of each model 
    on the test set
    """
    pred1=model1.predict_proba(X_test)
    pred2=model2.predict_proba(X_test)
    pred3=model3.predict_proba(X_test)
    
  6. 对所有三个模型生成的预测结果进行平均:

    """
    Calculating the ensemble prediction by 
    averaging three base model predictions
    """
    ensemblepred=(pred1+pred2+pred3)/3
    
  7. 显示集成预测数组的前四行:

    # Displaying first 4 rows of the ensemble predictions
    ensemblepred[0:4,:]
    

    你应该得到类似以下的输出:

    图 15.10:集成预测的前四行

    图 15.10:集成预测的前四行

    从前面的输出可以看到,对于每个样本,我们有两个概率值,分别对应每个类别。

  8. 打印每个类别的预测顺序。如步骤 6所示,预测输出有两列,分别对应每个类别。为了找到类别预测的顺序,我们使用了一种叫做.classes_的方法。以下代码片段实现了这一功能:

    # Printing the order of classes for each model
    print(model1.classes_)
    print(model2.classes_)
    print(model3.classes_)
    

    你应该得到以下输出:

    图 15.11:类别顺序

    图 15.11:类别顺序

  9. 现在,我们需要从输出的概率中获取每个例子的最终预测。最终预测将是概率最高的类别。为了得到概率最高的类别,我们使用numpy函数,.argmax()。执行方式如下:

    import numpy as np
    pred = np.argmax(ensemblepred,axis = 1)
    pred
    

    从前面的代码中,axis = 1意味着我们需要在列中找到最大值的索引。

    你应该得到以下输出:

    图 15.12:数组输出

    图 15.12:数组输出

  10. 生成预测的混淆矩阵:

    # Generating confusion matrix
    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到类似以下的输出:

    图 15.13:混淆矩阵

    图 15.13:混淆矩阵

  11. 生成分类报告:

    # Generating classification report
    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到类似以下的输出:

    图 15.14:分类报告

图 15.14:分类报告

注意

要访问该特定部分的源代码,请参见packt.live/31aLa0w

你也可以在线运行这个示例,访问packt.live/31hMpex

在本练习中,我们实现了集成学习的平均技术。从分类报告中可以看出,我们通过平均方法将准确率从基准模型的0.89提升至0.91。然而,需要注意的是,集成方法并不能保证在所有情况下都能提高结果。有时,可能不会观察到性能的提升。

让我们还看看信用卡申请的业务背景下的结果。91%的准确率意味着在测试集中的196个案例中,我们能够正确预测客户是否具有信用的比例为91%。同时,查看类别0的召回率也很有趣,它是91%。在这种情况下,我们正确识别出了91%的不合格客户。另一个值得关注的地方是,有9%的不合格客户被错误地识别为信用合格,这实际上对信用卡部门构成了风险。理想情况下,如果我们想减少信用卡部门的风险,我们需要提高不合格客户的召回率。

另一方面,还存在一个问题,即通过正确预测信用良好客户(1)来最大化业务机会。我们已经看到,在总共 107 位信用良好客户中,有 91% 被正确预测,这是一个积极的结果。剩余的 9% 信用良好客户则将成为一个失去的商业机会。

调优模型是一个平衡的过程,数据科学家必须对此有所认识。数据科学家必须根据业务的偏好行事。如果业务风险规避,并且愿意放弃业务机会,那么模型将需要调整以增加非良好客户的召回率。另一方面,如果业务积极并且希望增长,那么首选路线将是提高信用良好客户的召回率。总之,调优模型是基于业务现实的平衡行为。

加权平均

加权平均是我们之前看到的平均方法的扩展。这两种方法的主要区别在于生成组合预测的方式。在加权平均方法中,我们为每个模型的预测分配权重,然后生成组合预测。这些权重最初是根据我们对哪个模型在集成中最有影响力的判断而任意分配的。这些权重在经过大量实验后需要进化。首先,我们假设一些权重,然后使用不同的权重迭代每个模型,以验证是否在性能上有所改进。让我们在接下来的练习中实现加权平均方法。

练习 15.03:使用加权平均技术的集成模型

在这个练习中,我们将使用加权平均技术实现集成模型。我们将使用与 练习 15.02 中相同的基本模型,逻辑回归、KNN 和随机森林:

  1. 打开一个新的 Colab 笔记本。

  2. 执行所有从 练习 15.02使用平均技术的集成模型 开始到预测三个模型概率的步骤。

  3. 对预测值进行加权平均。在加权平均方法中,权重是根据我们对每个预测的判断任意分配的。具体操作如下:

    """
    Calculating the ensemble prediction by applying 
    weights for each prediction
    """
    ensemblepred=(pred1 *0.60 + pred2 * 0.20 + pred3 * 0.20)
    

    请注意,这些权重的分配方式是使所有权重的总和为 10.6 + 0.2 + 0.2 = 1)。

  4. 显示集成预测数组的前四行:

    # Displaying first 4 rows of the ensemble predictions
    ensemblepred[0:4,:]
    

    您应该会得到类似以下的输出:

    图 15.15:集成预测数组输出

    图 15.15:集成预测数组输出

    正如您从输出中看到的那样,每个示例对应每个类别有两个概率。

  5. 打印预测输出中每个类的顺序:

    # Printing the order of classes for each model
    print(model1.classes_)
    print(model2.classes_)
    print(model3.classes_)
    

    您应该会得到以下输出:

    图 15.16:从预测输出中得到的类别顺序

    图 15.16:从预测输出中得到的类别顺序

  6. 从概率中计算最终的预测值。

    我们现在需要使用 np.argmax() 函数,从输出的概率中获取每个示例的最终预测:

    import numpy as np
    pred = np.argmax(ensemblepred,axis = 1)
    pred
    

    你应该得到类似以下的输出:

    图 15.17:最终预测的数组

    图 15.17:最终预测的数组

  7. 生成预测的混淆矩阵:

    # Generating confusion matrix
    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到类似以下的输出:

    图 15.18:混淆矩阵

    图 15.18:混淆矩阵

  8. 生成分类报告:

    # Generating classification report
    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到类似以下的输出:

    图 15.19:分类报告

图 15.19:分类报告

注意

要访问此特定部分的源代码,请参见 packt.live/2CINUc5

你也可以在网上运行此示例,访问 packt.live/2YfrTZK

迭代 2:使用不同的权重

从第一次迭代中,我们看到我们得到了 89% 的准确率。这个指标反映了我们在第一次迭代中应用的权重。现在让我们尝试改变权重,看看它对指标有什么影响。尝试不同权重的过程是基于我们对数据集和数据分布的判断。假设我们认为数据分布更线性,因此决定增加线性回归模型的权重,并减少其他两个模型的权重。现在让我们在第二次迭代中尝试新的权重组合:

  1. 取预测的加权平均值。

    在这个迭代中,我们将逻辑回归预测的权重从 0.6 增加到 0.7,并将另外两个权重从 0.2 减少到 0.15

    """
    Calculating the ensemble prediction by applying 
    weights for each prediction
    """
    ensemblepred=(pred1 *0.70+pred2 * 0.15+pred3 * 0.15)
    
  2. 从概率中计算最终的预测值。

    我们现在需要使用 np.argmax() 函数,从输出的概率中获取每个示例的最终预测:

    # Generating predictions from probabilities
    import numpy as np
    pred = np.argmax(ensemblepred,axis = 1)
    
  3. 生成预测的混淆矩阵:

    # Generating confusion matrix
    from sklearn.metrics import confusion_matrix
    confusionMatrix = confusion_matrix(y_test, pred)
    print(confusionMatrix)
    

    你应该得到类似以下的输出:

    图 15.20:混淆矩阵

    图 15.20:混淆矩阵

  4. 生成分类报告:

    # Generating classification report
    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到类似以下的输出:

    图 15.21:分类报告

图 15.21:分类报告

在这个练习中,我们实现了集成学习的加权平均技术。我们使用权重进行了两次迭代。我们看到,在第二次迭代中,当我们将逻辑回归预测的权重从0.6增加到0.7时,准确率实际上从0.89提高到了0.90。这验证了我们关于逻辑回归模型在集成中的重要性的假设。为了检查是否还有进一步提升的空间,我们应该像在迭代2中那样再次调整权重,然后根据度量标准进行验证。我们应继续进行这些迭代,直到度量标准不再有任何改进为止。

与平均方法的度量标准进行比较时,我们可以看到,准确率从0.91降至0.90。然而,类别1的召回值从0.91升至0.92,而类别0的对应值从0.91降至0.88。这可能是因为我们应用的权重导致了结果的轻微下降,相比于我们从平均方法中得到的结果。

从商业角度来看,我们可以看到,随着类别1的召回值的提高,信用卡部门获得了更多有信用的客户。然而,这也伴随着风险的增加,更多的不合格客户被标记为有信用客户,12%100% - 88%)被标记为信用客户。

最大投票法

最大投票法基于多数规则原理。在这种方法中,多数人的意见占主导地位。在这种技术中,个别模型,或者在集成学习术语中,个别学习者,是在训练集上进行训练的,然后在测试集上生成预测结果。每个个别学习者的预测被视为一票。在测试集中,获得最多票数的类别就是最终的获胜者。我们用一个简单的示例来演示这一点。

假设我们有三个独立的学习者,他们在训练集上进行了学习。每个学习者都在测试集上生成了预测结果,这些结果汇总在下表中。预测结果要么是类别'1',要么是类别'0':

图 15.22: 学习者的预测

图 15.22: 学习者的预测

在前面的示例中,我们可以看到,对于Example 1Example 3,多数投票支持类别'1',而对于其他两个示例,大多数投票支持类别'0'。最终的预测是根据哪个类别获得多数票来确定的。这种投票方法,即输出一个类别,被称为“硬”投票。

在使用scikit-learn库实现最大投票法时,我们使用一个名为VotingClassifier()的特殊函数。我们将单个学习器作为输入提供给VotingClassifier,以创建集成模型。然后,这个集成模型用于拟合训练集,并最终用于在测试集上进行预测。我们将在练习 15.04中探索最大投票法的动态,使用最大投票的集成模型

练习 15.04:使用最大投票的集成模型

在本次练习中,我们将使用最大投票法实现集成模型。我们将选择的单个学习器与之前练习中选择的相似,即逻辑回归、KNN 和随机森林:

  1. 打开一个新的 Colab 笔记本。

  2. 执行练习 15.01中所有步骤,加载、探索和清理数据,直到将数据集拆分为训练集和测试集。

  3. 现在我们将导入选定的分类器,作为单独的学习器:

    """
    Defining the voting classifier and three 
    individual learners
    """
    from sklearn.ensemble import VotingClassifier
    from sklearn.linear_model import LogisticRegression
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.ensemble import RandomForestClassifier
    # Defining the models
    model1 = LogisticRegression(random_state=123)
    model2 = KNeighborsClassifier(n_neighbors=5)
    model3 = RandomForestClassifier(n_estimators=500)
    
  4. 在定义了单个学习器后,我们现在可以使用VotingClassifier()函数构建集成模型。以下是实现的代码片段:

    # Defining the ensemble model using VotingClassifier
    model = VotingClassifier(estimators=[('lr', model1),\
                            ('knn', model2),('rf', model3)],\
                             voting= 'hard')
    

    如代码片段所示,单个学习器作为输入通过estimators参数传递。估计器会接受每个定义的学习器以及用来表示模型的字符串值。例如,lr表示逻辑回归。此外,注意投票是“硬投票”(hard voting),这意味着输出将是类别标签,而不是概率。

  5. 在集成模型上拟合训练集:

    # Fitting the model on the training set
    model.fit(X_train,y_train)
    
  6. 训练后打印准确率得分:

    """
    Predicting accuracy on the test set using 
    .score() function
    """
    model.score(X_test,y_test)
    

    你应该得到类似如下的输出:

    0.9081632653061225
    
  7. 从集成模型生成测试集上的预测结果:

    # Generating the predictions on the test set
    preds = model.predict(X_test)
    
  8. 生成预测结果的混淆矩阵:

    # Printing the confusion matrix
    from sklearn.metrics import confusion_matrix
    # Confusion matrix for the test set
    print(confusion_matrix(y_test, preds))
    

    你应该得到类似如下的输出:

    图 15.23:混淆矩阵

    图 15.23:混淆矩阵

  9. 生成分类报告:

    # Printing the classification report
    from sklearn.metrics import classification_report
    print(classification_report(y_test, preds))
    

    你应该得到类似如下的输出:

    图 15.24:分类报告

图 15.24:分类报告

注意

要访问本节的源代码,请参考packt.live/3aESLr6

你也可以在packt.live/31cL7RJ上运行此示例。

在本次练习中,我们实现了最大投票法用于集成学习。正如分类报告所示,结果与我们从平均法(0.91)得到的结果相似。从商业角度来看,我们可以看到这个结果更加平衡。值得客户的召回值很高,达到了92%;然而,这也带来了更多的风险,因为更多的不值得客户被包含在内。在这种情况下,不值得客户的比例约为10%100% - 90%)。

集成学习的高级技术

在学习了集成学习的一些简单技术之后,让我们来探索一些高级技术。在这些高级技术中,我们将处理三种不同的集成学习方法:

  • 自助法

  • 提升(Boosting)

  • 堆叠/混合

在我们处理每种技术之前,有一些关于这些高级集成学习技术的基本动态需要解读。如本章开头所述,集成学习的本质是将多个单独的模型结合起来,形成一个更优的模型。在这些高级技术中,生成优越模型的方式有一些微妙的差异。在这些技术中,单个模型或学习者会生成预测结果,这些预测结果将用于形成最终的预测结果。生成第一组预测结果的单个模型或学习者被称为基础学习器或基础估计器,而将基础学习器的预测结果结合起来的模型称为学习器或估计器。元学习器从基础学习器学习的方式在每种高级技术中都有所不同。让我们详细了解每种高级技术。

自助法

自助法(Bagging)是Bootstrap Aggregating的缩写。在解释自助法如何工作之前,我们先来描述什么是自助法。自助法的词源来源于短语Pulling oneself up by one's bootstrap。该短语的精髓是充分利用现有的资源。在统计学中,自助法指的是通过替换从现有数据集中抽取样本。我们通过一个简单的例子来了解这个概念。

假设我们有一个包含从 1 到 10 的 10 个数字的数据集。现在我们需要从现有数据集中创建 4 个每个包含 10 个数据的新数据集。我们该如何做呢?这就是自助法派上用场的地方。在这种方法中,我们从现有数据集中逐个抽取样本,并在每次抽取样本后将已抽取的数字放回数据集,然后继续抽取,直到获得我们需要的数据点数量。

由于我们在选择每个数字后都会将其替换,因此可能会出现某个数据点在样本中出现多次的情况。下面的图示解释了这一点:

图 15.25:自助法

图 15.25:自助法

现在我们已经理解了自助法(bootstrapping),让我们将这个概念应用到机器学习的背景中。在本章的前面,我们讨论了集成学习有助于减少预测的方差。减少方差的一种方式是通过对多个学习者的预测结果进行平均。在自助法中,使用自助法创建数据的多个子集。在这些数据的每个子集上,都会训练一个基础学习器并生成预测结果。所有基础学习器的预测结果将被平均,以得到元学习器或最终的预测结果。

在实现集成学习时,我们使用一个名为BaggingClassifier()的函数,该函数可以在Scikit learn库中找到。创建集成模型时提供的一些重要参数包括以下内容:

  • base_estimator:该参数用于定义要使用的基本估计器。

  • n_estimator:该参数定义将在集成中使用的基本估计器的数量。

  • max_samples:该参数定义了用于拟合基本估计器的自助样本的最大大小。它表示为一个比例(例如 0.8,0.7 等)。

  • max_features:在拟合多个个体学习器时,发现随机选择用于每个数据集的特征会带来更好的性能。max_features参数表示要使用的特征数量。例如,如果数据集中有 10 个特征,并且将max_features参数定义为 0.8,那么将使用 8 个(0.8 x 10)特征来拟合使用基本学习器的模型。

让我们在练习 15.05中探索使用集成学习进行 bagging,使用 Bagging 进行集成学习

练习 15.05:使用 Bagging 进行集成学习

在本练习中,我们将使用 bagging 实现一个集成模型。我们将选择的个体学习器是随机森林:

  1. 打开一个新的 Colab 笔记本。

  2. 执行练习 15.01中所有步骤,加载、探索和清理数据,直到将数据集拆分为训练集和测试集。

  3. 定义基本学习器,即随机森林分类器:

    # Defining the base learner
    from sklearn.ensemble import RandomForestClassifier
    bl1 = RandomForestClassifier(random_state=123)
    
  4. 在定义了个体学习器之后,我们可以使用BaggingClassifier()函数来构建集成模型。以下代码片段实现了这一点:

    # Creating the bagging meta learner
    from sklearn.ensemble import BaggingClassifier
    baggingLearner = \
    BaggingClassifier(base_estimator=bl1, n_estimators=10, \
                      max_samples=0.8, max_features=0.7)
    

    我们提供的参数是任意的。最优值需要通过实验来确定。

  5. 在集成模型上拟合训练集:

    # Fitting the model using the meta learner
    model = baggingLearner.fit(X_train, y_train)
    
  6. 在测试集上生成集成模型的预测:

    # Predicting on the test set using the model
    pred = model.predict(X_test)
    
  7. 生成预测的混淆矩阵:

    # Printing the confusion matrix
    from sklearn.metrics import confusion_matrix
    print(confusion_matrix(y_test, pred))
    

    你应该得到类似以下的输出:

    图 15.26:混淆矩阵

    图 15.26:混淆矩阵

  8. 生成分类报告:

    # Printing the classification report
    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到类似以下的输出:

    图 15.27:分类报告

图 15.27:分类报告

注意

要访问此特定部分的源代码,请参考packt.live/3iYcHZ3

你也可以在packt.live/3hfRCbZ上在线运行此示例。

在本次练习中,我们使用了袋装法实现集成学习。从分类报告中可以看出,我们的结果(0.90)略低于平均和最大投票方法的结果(0.91)。然而,从业务角度来看,我们可以看到两个类别的召回值存在较大波动。我们发现,信用良好的客户的召回率约为 87%。这意味着大约有 13% 的机会丧失。我们还看到,识别不可信客户的风险也在减少。识别不可信客户的召回率约为 93%,这意味着只有大约 7% 的不可信客户被错误分类为信用良好的客户。因此,对于更注重风险的企业,建议使用该模型。

提升

我们在上一节中讨论的袋装技术可以被称为并行学习技术。这意味着每个基学习器都是独立训练的,并将它们的预测结果进行汇总。与袋装方法不同,提升法是按顺序进行的。它的原理是通过纠正每个基学习器的预测误差来工作。基学习器是按顺序一个接一个地训练的。每个基学习器试图纠正前一个学习器产生的错误,这个过程会一直进行,直到生成一个优越的元学习器。提升技术的步骤如下:

  1. 基学习器在数据集的一个子集上进行训练。

  2. 模型拟合完成后,对整个数据集进行预测。

  3. 通过将预测结果与实际标签进行比较,识别预测中的错误。

  4. 那些产生错误预测的示例会被赋予更大的权重。

  5. 另一个基学习器会在数据集上进行训练,其中前一步中错误预测示例的权重会被调整。

  6. 该基学习器试图纠正之前模型的错误,并给出其预测结果。

  7. 步骤 4步骤 5步骤 6 会重复进行,直到生成一个强大的元学习器。

在实现提升技术时,我们可以使用 AdaBoostClassifier() 方法,该方法在 scikit-learn 中非常常用。与袋装估计器类似,AdaBoostClassifier() 方法中的一些重要参数包括 base_estimatorn_estimators。接下来我们将在 练习 15.06 中实现提升算法,使用提升的集成学习

练习 15.06:使用提升的集成学习

在本次练习中,我们将使用提升方法实现一个集成模型。我们选择的单个学习器将是逻辑回归模型。实现此算法的步骤与袋装算法非常相似:

  1. 打开一个新的 Colab 笔记本文件。

  2. 执行 练习 15.01 中的所有步骤,加载、探索和清理数据,直到将数据集划分为训练集和测试集。

  3. 定义基学习器,该学习器将是一个逻辑回归分类器:

    # Defining the base learner
    from sklearn.linear_model import LogisticRegression
    bl1 = LogisticRegression(random_state=123)
    
  4. 在定义了个体学习器后,我们现在可以使用 AdaBoostClassifier() 函数构建集成模型。以下是实现该功能的代码片段:

    # Define the boosting meta learner
    from sklearn.ensemble import AdaBoostClassifier
    boosting = AdaBoostClassifier(base_estimator=bl1, \
                                  n_estimators=200)
    

    我们给定的参数值是任意的。最优值需要通过实验来识别。

  5. 在集成模型上拟合训练集:

    # Fitting the model on the training set
    model = boosting.fit(X_train, y_train)
    
  6. 从集成模型中生成测试集的预测结果:

    # Getting the predictions from the boosting model
    pred = model.predict(X_test)
    
  7. 为预测结果生成混淆矩阵:

    # Printing the confusion matrix
    from sklearn.metrics import confusion_matrix
    print(confusion_matrix(y_test, pred))
    

    你应该得到类似于以下的输出:

    Figure 15.28: 混淆矩阵

    Figure 15.28: 混淆矩阵

  8. 生成分类报告:

    # Printing the classification report
    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到类似于以下的输出:

    Figure 15.29: 分类报告

Figure 15.29: 分类报告

注意

要访问此特定部分的源代码,请参考packt.live/3iUas96

你也可以在线运行此示例,网址为packt.live/2EkkW2J

在这个练习中,我们使用提升法实现了集成学习模型。从分类报告中可以看出,我们得到了与先前练习中实现的袋装法非常相似的结果(0.90)。从业务角度来看,与袋装法(召回率为 0.93 和 0.87)得到的结果相比,这里结果更为平衡。在这里,我们可以看到不值得信任的客户(90%)和值得信任的客户(91%)的召回值非常接近,这表明结果非常平衡。

堆叠

堆叠(Stacking)从原则上来说,与袋装法(bagging)和提升法(boosting)类似,都是通过结合基础学习器来形成元学习器。然而,在堆叠中,从基础学习器获取元学习器的方法有很大不同。在堆叠中,元学习器是通过基础学习器的预测结果进行拟合的。堆叠算法可以解释如下:

  1. 训练集被分割成多个部分,比如五个部分。

  2. 一个基础学习器(比如,KNN)被拟合到训练集的四个部分,然后对第五个部分进行预测。这个过程会一直继续,直到基础学习器在训练集的五个部分上都进行了预测。所有这些预测结果将被汇总,以获得完整训练集的预测。

  3. 然后使用相同的基础学习器对测试集进行预测。

  4. 步骤 2步骤 3 随后将使用不同的基础学习器(比如,随机森林)重复进行。

  5. 接下来进入一个新的模型,它作为元学习器(比如,逻辑回归)。

  6. 元学习器在基础学习器在训练集上生成的预测结果上进行拟合。

  7. 一旦元学习器在训练集上进行拟合,便可以使用相同的模型在基础学习器在测试集上生成的预测结果上进行预测。

    所有这些过程通过图示方式解释如下:

    Figure 15.30: 堆叠过程

Figure 15.30: 堆叠过程

堆叠的实现是通过一个名为StackingClassifier()的函数完成的。这个函数来自一个叫做mlxtend的包。此函数的各种参数是我们指定为基础学习器的模型以及作为元学习器的模型。堆叠技术的实现将在练习 15.07使用堆叠法的集成学习中进行。

练习 15.07:使用堆叠法的集成学习

在这个练习中,我们将使用堆叠法实现一个集成模型。我们将使用的基础学习器是 KNN 和随机森林。我们的元学习器将是逻辑回归:

  1. 打开一个新的 Colab 笔记本。

  2. 执行练习 15.01中的所有步骤,即加载、探索和清理数据,直到将数据集分割为训练集和测试集。

  3. 导入基础学习器和元学习器。在此实现中,我们将使用两个基础学习器(KNN 和随机森林)。元学习器将使用逻辑回归:

    # Importing the meta learner and base learners
    from sklearn.linear_model import LogisticRegression
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.ensemble import RandomForestClassifier
    bl1 = KNeighborsClassifier(n_neighbors=5)
    bl2 = RandomForestClassifier(random_state=123)
    ml = LogisticRegression(random_state=123)
    
  4. 一旦定义了基础学习器和元学习器,我们将继续创建堆叠分类器:

    # Creating the stacking classifier
    from mlxtend.classifier import StackingClassifier
    stackclf = StackingClassifier(classifiers=[bl1, bl2],\
                                  meta_classifier=ml)
    

    我们所给出的参数是两个基础学习器和一个元学习器。

  5. 将训练集拟合到集成模型:

    # Fitting the model on the training set
    model = stackclf.fit(X_train, y_train)
    
  6. 在测试集上生成集成模型的预测:

    # Generating predictions on test set
    pred = model.predict(X_test)
    
  7. 生成分类报告:

    # Printing the classification report
    from sklearn.metrics import classification_report
    print(classification_report(y_test, pred))
    

    你应该得到类似以下的输出:

    图 15.31:分类报告

    图 15.31:分类报告

  8. 为预测生成混淆矩阵:

    # Printing the confusion matrix
    from sklearn.metrics import confusion_matrix
    print(confusion_matrix(y_test, pred))
    

    你应该得到类似以下的输出:

    图 15.32:混淆矩阵

图 15.32:混淆矩阵

注意

要访问此特定章节的源代码,请参考packt.live/31dZRjy

你也可以在线运行此示例,网址是packt.live/2Fwsdgf

在这个练习中,我们通过堆叠实现了集成学习模型。从分类报告中可以看出,我们使用堆叠分类器并未带来相对于基准模型的任何改进。导致这一结果的原因可能有很多,从我们选择的基础学习器和元学习器,到数据集的大小。需要注意的是,并不是所有的集成学习方法都会是理想的“灵丹妙药”。许多高级方法可能无法提高性能,这个练习就证明了这一点。然而,提升性能的最佳方法往往需要大量的实验,这才是能够取得成效的关键。毕竟,在机器学习中,没有免费的午餐。

从商业角度来看,结果在收入方面具有重大影响,因为大量信用良好的客户(16%100% - 84%))被标记为不值得信任。而召回的风险方面表现相当好,较高的比例(92%)的不合格客户被识别出来。

到目前为止,我们已经看到三种集成学习的高级技术。现在,让我们将这些技术应用于信用卡数据集,然后选择最佳方法。我们将在活动 15.02中进行此操作,使用平均技术的集成模型

活动 15.02:高级集成技术的比较

场景:你已经在信用卡数据集上尝试过基准模型,并获得了一些基准指标。通过学习一些高级集成技术,你希望确定要为信用卡审批数据集使用哪种技术。

在这个活动中,你将使用所有三种高级技术,并在选择最终技术之前比较它们的结果。

步骤如下:

  1. 打开一个新的 Colab 笔记本。

  2. 实现练习 15.01中所有步骤,加载、探索和清洗数据,直到将数据集划分为训练集和测试集。

  3. 实现使用逻辑回归模型作为基学习器的袋装技术。在袋装分类器中,定义n_estimators = 15max_samples = 0.7max_features = 0.8。将模型拟合到训练集,生成预测结果,并打印混淆矩阵和分类报告。

  4. 使用随机森林作为基学习器实现提升。在AdaBoostClassifier中,定义n_estimators = 300。将模型拟合到训练集,生成预测结果,并打印混淆矩阵和分类报告。

  5. 实现堆叠技术。将 KNN 和逻辑回归模型作为基学习器,随机森林作为元学习器。将模型拟合到训练集,生成预测结果,并打印混淆矩阵和分类报告。

  6. 比较所有三种技术的结果,并选择最佳技术。

  7. 输出:对于这三种方法,你应该得到类似于以下的输出。请注意,由于预测过程中的变异性,你的输出值可能与此不同。

    袋装的输出结果如下:

    图 15.33:袋装的输出

图 15.33:袋装的输出

提升的输出结果如下:

图 15.34:提升的输出

图 15.34:提升的输出

堆叠的输出结果如下:

图 15.35:堆叠的输出

图 15.35:堆叠的输出

注意

此活动的解决方案可以在这里找到:packt.live/2GbJloz

在这个活动中,我们在信用卡数据集上实现了所有三种高级集成技术。根据指标,我们发现提升法(0.91)的结果优于袋装法(0.90)和堆叠法(0.87)。因此,我们选择提升算法来提升模型的性能。从商业角度来看,提升算法产生了更平衡的结果,其中信用良好与不良客户的召回值(91%)相似。

总结

在本章中,我们学习了各种集成学习技术。让我们总结一下本章所学的内容。

在本章开始时,我们介绍了方差和偏差的概念,并了解到集成学习是一种旨在将个体模型结合起来,创建更优模型的技术,从而减少方差和偏差,提升性能。为了进一步探索不同的集成学习技术,我们下载了信用卡审批数据集,并使用逻辑回归拟合了一个基准模型。

在随后的部分中,我们学习了六种不同的集成学习技术;其中三种是简单的技术,另外三种是高级技术。平均法通过结合基础学习器的预测并对预测概率进行平均,创建了一个集成模型。我们使用这种方法得到了比基准模型更好的结果。加权平均法与平均法类似,不同之处在于预测组合的方式。在这种方法中,给每个学习器的预测结果分配任意权重,从而得到最终的预测结果。最大投票法是一种基于大多数基础学习器投票结果来得出最终预测的技术。

使用装袋法(bagging)进行集成学习利用了自助法(bootstrapping)技术。基础学习器在自助法数据集上进行训练,结果通过聚合得到元学习器。提升法(boosting)是一种顺序学习方法,其工作原理是对基础学习器进行错误修正。我们发现,提升法技术为我们的应用场景提供了一些优越的结果。我们使用堆叠技术通过学习基础学习器的预测结果来生成最终预测。

本章的目标是为你提供一套旨在提升机器学习模型性能的技能。然而,需要注意的是,机器学习技术中并没有灵丹妙药,并非所有技术在所有场景下都有效。帮助你成为一名优秀数据科学家的“秘诀”是不断实验不同的技术、使用案例和数据集。

学习了一套旨在提升性能的工具后,你现在已经准备好将这些工具应用到一系列现实世界的项目中。本章标志着本书的结束,也是你反思自己在各个章节中所学技能的好机会。然而,你仍然可以在packt.live/2ZagB9y获取三章附加内容。

目录

  1. 数据科学工作坊

  2. 第二版

  3. 前言

    1. 关于本书

      1. 受众

      2. 章节介绍

      3. 约定

      4. 代码展示

      5. 设置您的环境

        1. 如何设置 Google Colab

        2. 如何使用 Google Colab

      6. 访问代码文件

    1. Python 数据科学简介

    2. 介绍

    3. 数据科学的应用

      1. 什么是机器学习?

        1. 监督学习

        2. 无监督学习

        3. 强化学习

    4. Python 概述

      1. 变量类型

        1. 数值变量

        2. 文本变量

        3. Python 列表

        4. Python 字典

      2. 练习 1.01: 创建包含机器学习算法的字典

    5. Python 数据科学

      1. pandas 包

        1. 数据框和系列

        2. CSV 文件

        3. Excel 电子表格

        4. JSON

      2. 练习 1.02: 将不同格式的数据加载到 pandas 数据框中

    6. Scikit-Learn

      1. 什么是模型?

        1. 模型超参数

        2. sklearn API

      2. 练习 1.03: 使用 sklearn 从数据集中预测乳腺癌

      3. 活动 1.01: 训练垃圾邮件检测算法

    7. 总结

    1. 回归

    2. 介绍

    3. 简单线性回归

      1. 最小二乘法
    4. 多元线性回归

      1. 估计回归系数(β0,β1,β2 和 β3)

      2. 变量的对数变换

      3. 相关矩阵

    5. 使用 Python 进行回归分析

      1. 练习 2.01:加载并准备数据进行分析

      2. 相关系数

      3. 练习 2.02:使用 Python 进行线性关系的图形化调查

      4. 练习 2.03:使用 Python 检查可能的对数-线性关系

      5. Statsmodels 公式 API

      6. 练习 2.04:使用 Statsmodels 公式 API 拟合简单线性回归模型

      7. 分析模型摘要

      8. 模型公式语言

      9. 截距处理

      10. 活动 2.01:使用 Statsmodels 公式 API 拟合对数-线性模型

    6. 多元回归分析

      1. 练习 2.05:使用 Statsmodels 公式 API 拟合多元线性回归模型
    7. 回归分析的假设

      1. 活动 2.02:拟合多元对数-线性回归模型
    8. 解释回归分析结果

      1. 回归分析的检查和平衡

      2. F 检验

      3. t 检验

    9. 总结

  4. 3. 二元分类

    1. 介绍

    2. 理解商业背景

      1. 商业发现

      2. 练习 3.01:加载并探索数据集中的数据

      3. 使用探索性数据分析测试商业假设

      4. 探索性数据分析的可视化

      5. 练习 3.02:年龄与定期贷款倾向的商业假设检验

      6. 来自探索性分析的直觉

      7. 活动 3.01:业务假设检验,以确定就业状态与定期存款倾向的关系

    3. 特征工程

      1. 业务驱动的特征工程

      2. 练习 3.03:特征工程 – 探索单一特征

      3. 练习 3.04:特征工程 – 从现有特征中创建新特征

    4. 数据驱动的特征工程

      1. 快速了解数据类型及描述性摘要
    5. 相关性矩阵与可视化

      1. 练习 3.05:在银行数据中寻找数据的相关性并生成相关性图

      2. 数据的偏度

      3. 直方图

      4. 密度图

      5. 其他特征工程方法

      6. 总结特征工程

      7. 使用逻辑回归函数构建二元分类模型

      8. 揭开逻辑回归的神秘面纱

      9. 评估模型性能的指标

      10. 混淆矩阵

      11. 准确率

      12. 分类报告

      13. 数据预处理

      14. 练习 3.06:用于预测银行定期存款购买倾向的逻辑回归模型

      15. 活动 3.02:模型迭代 2 – 使用特征工程变量的逻辑回归模型

      16. 下一步

    6. 总结

    1. 多类分类与随机森林

    2. 介绍

    3. 训练随机森林分类器

    4. 评估模型的性能

      1. 练习 4.01:构建分类动物类型的模型并评估其性能

      2. 树木数量估算器

      3. 练习 4.02:调整 n_estimators 以减少过拟合

    5. 最大深度

      1. 练习 4.03:调整 max_depth 以减少过拟合
    6. 叶子节点的最小样本数

      1. 练习 4.04:调整 min_samples_leaf
    7. 最大特征数

      1. 练习 4.05:调整 max_features

      2. 活动 4.01:在 ISOLET 数据集上训练随机森林分类器

    8. 总结

    1. 执行首次聚类分析

    2. 介绍

    3. 使用 k-means 聚类

      1. 练习 5.01:对 ATO 数据集进行首次聚类分析
    4. 解释 k-means 结果

      1. 练习 5.02:通过商业收入和支出对澳大利亚邮政编码进行聚类
    5. 选择聚类数量

      1. 练习 5.03:寻找最佳聚类数量
    6. 初始化聚类

      1. 练习 5.04:使用不同的初始化参数以获得合适的结果
    7. 计算与质心的距离

      1. 练习 5.05:在我们的数据集中寻找最接近的质心
    8. 标准化数据

      1. 练习 5.06:标准化我们的数据集中的数据

      2. 活动 5.01:使用 k-means 在银行中进行客户分群分析

    9. 总结

    1. 如何评估性能

    2. 介绍

    3. 拆分数据

      1. 练习 6.01:导入和拆分数据
    4. 评估回归模型的性能

      1. 数据结构 – 向量与矩阵

        1. 标量

        2. 向量

        3. 矩阵

      2. R2 得分

      3. 练习 6.02:计算线性回归模型的 R2 得分

      4. 平均绝对误差

      5. 练习 6.03:计算模型的 MAE

      6. 练习 6.04:计算第二个模型的平均绝对误差

        1. 其他评估指标
    5. 评估分类模型的性能

      1. 练习 6.05:创建分类模型以计算评估指标
    6. 混淆矩阵

      1. 练习 6.06:为分类模型生成混淆矩阵

      2. 关于混淆矩阵的更多内容

      3. 精度

      4. 练习 6.07:计算分类模型的精度

      5. 召回率

      6. 练习 6.08:计算分类模型的召回率

      7. F1 得分

      8. 练习 6.09:计算分类模型的 F1 得分

      9. 准确率

      10. 练习 6.10:计算分类模型的准确率

      11. 对数损失

      12. 练习 6.11:计算分类模型的对数损失

    7. 接收者操作特征曲线(ROC Curve)

      1. 练习 6.12:计算并绘制二分类问题的 ROC 曲线
    8. ROC 曲线下的面积

      1. 练习 6.13:计算剖腹产数据集的 ROC AUC
    9. 保存和加载模型

      1. 练习 6.14:保存和加载模型

      2. 活动 6.01:训练三种不同的模型,并使用评估指标选择表现最佳的模型

    10. 总结

    1. 机器学习模型的泛化

    2. 简介

    3. 过拟合

      1. 在特征过多的情况下训练模型

      2. 训练时间过长

    4. 欠拟合

    5. 数据

      1. 数据集拆分比例

      2. 创建数据集拆分

      3. 练习 7.01:导入和拆分数据

    6. 随机状态

      1. 练习 7.02:拆分数据时设置随机状态
    7. 交叉验证

      1. K 折交叉验证

      2. 练习 7.03:创建一个五折交叉验证数据集

      3. 练习 7.04:使用循环调用创建五折交叉验证数据集

    8. cross_val_score

      1. 练习 7.05:从五折交叉验证中获取评分

      2. 理解实现交叉验证的估计器

    9. LogisticRegressionCV

      1. 练习 7.06:使用交叉验证训练 Logistic 回归模型
    10. 使用 GridSearchCV 进行超参数调优

      1. 决策树

      2. 练习 7.07:使用 GridSearch 和交叉验证找到模型的最佳参数

    11. 使用 RandomizedSearchCV 进行超参数调优

      1. 练习 7.08:使用随机搜索进行超参数调优
    12. Lasso 回归的模型正则化

      1. 练习 7.09:使用 Lasso 回归修正模型过拟合
    13. Ridge 回归

      1. 练习 7.10:使用 Ridge 回归修正模型过拟合

      2. 活动 7.01:找到一个最优模型来预测超导体的临界温度

    14. 总结

    1. 超参数调优

    2. 简介

    3. 什么是超参数?

      1. 超参数与统计模型参数的区别

      2. 设置超参数

      3. 关于默认设置的说明

    4. 寻找最佳超参数化

      1. 练习 8.01:手动调优 k-NN 分类器的超参数

      2. 手动搜索的优缺点

    5. 使用网格搜索调优

      1. 网格搜索策略的简单演示
    6. GridSearchCV

      1. 使用 GridSearchCV 进行调优

        1. 支持向量机(SVM)分类器
      2. 练习 8.02:使用网格搜索调优 SVM 的超参数

      3. 网格搜索的优缺点

    7. 随机搜索

      1. 随机变量及其分布

      2. 随机搜索过程的简单演示

      3. 使用 RandomizedSearchCV 调优

      4. 练习 8.03:随机搜索超参数调优,适用于随机森林分类器

      5. 随机搜索的优缺点

      6. 活动 8.01:蘑菇是否有毒?

    8. 总结

    1. 解释机器学习模型

    2. 简介

    3. 线性模型系数

      1. 练习 9.01:提取线性回归系数
    4. 随机森林特征重要性

      1. 练习 9.02:提取随机森林特征重要性
    5. 通过置换提取特征重要性

      1. 练习 9.03:通过置换提取特征重要性
    6. 部分依赖图

      1. 练习 9.04:绘制部分依赖图
    7. 使用 LIME 进行局部解释

      1. 练习 9.05:使用 LIME 进行局部解释

      2. 活动 9.01:训练和分析网络入侵检测模型

    8. 总结

  5. 10. 分析数据集

    1. 介绍

    2. 探索你的数据

    3. 分析你的数据集

      1. 练习 10.01:使用描述性统计分析 Ames 房屋数据集
    4. 分析分类变量的内容

      1. 练习 10.02:分析 Ames 房屋数据集中的分类变量
    5. 总结数值变量

      1. 练习 10.03:分析 Ames 房屋数据集中的数值变量
    6. 可视化你的数据

      1. 使用 Altair API

      2. 数值变量的直方图

      3. 分类变量的条形图

    7. 箱型图

      1. 练习 10.04:使用 Altair 可视化 Ames 房屋数据集

      2. 活动 10.01:使用可视化数据分析技术分析流失数据

    8. 总结

  6. 11. 数据准备

    1. 介绍

    2. 处理行重复

      1. 练习 11.01:处理乳腺癌数据集中的重复项
    3. 转换数据类型

      1. 练习 11.02:转换 Ames 房屋数据集的数据类型
    4. 处理错误值

      1. 练习 11.03:修复州列中的错误值
    5. 处理缺失值

      1. 练习 11.04:修复马疝数据集中的缺失值

      2. 活动 11.01:准备速配数据集

    6. 总结

  7. 12. 特征工程

    1. 介绍

      1. 合并数据集

        1. 左连接

        2. 右连接

      2. 练习 12.01:将 ATO 数据集与邮政编码数据合并

      3. 对变量进行分箱

      4. 练习 12.02:对 AMES 住房数据集中的 YearBuilt 变量进行分箱

      5. 日期操作

      6. 练习 12.03:对金融服务消费者投诉数据进行日期操作

      7. 执行数据聚合

      8. 练习 12.04:通过数据聚合在 AMES 住房数据集上进行特征工程

      9. 活动 12.01:在金融数据集上进行特征工程

      10. 总结

    1. 不平衡数据集

    2. 简介

    3. 理解业务背景

      1. 练习 13.01:在数据集上对逻辑回归模型进行基准测试

      2. 结果分析

    4. 不平衡数据集的挑战

    5. 应对不平衡数据集的策略

      1. 收集更多数据

      2. 重采样数据

      3. 练习 13.02:在我们的银行数据集上实现随机欠采样和分类,以找到最佳结果

      4. 分析

    6. 生成合成样本

      1. SMOTE 和 MSMOTE 的实现

      2. 练习 13.03:在我们的银行数据集上实现 SMOTE 以找到最佳结果

      3. 练习 13.04:在我们的银行数据集上实现 MSMOTE 以找到最佳结果

      4. 在电信数据集上应用平衡技术

      5. 活动 13.01:通过在电信流失数据集上拟合分类器来找到最佳平衡技术

    7. 总结

    1. 降维

    2. 简介

      1. 业务背景

      2. 练习 14.01:加载和清洗数据集

    3. 创建高维数据集

      1. 活动 14.01:在高维数据集上拟合逻辑回归模型
    4. 处理高维数据集的策略

      1. 向后特征消除(递归特征消除)

      2. 练习 14.02:使用向后特征选择的降维

      3. 向前特征选择

      4. 练习 14.03:使用向前特征选择的降维

      5. 主成分分析(PCA)

      6. 练习 14.04:使用 PCA 的降维

      7. 独立成分分析(ICA)

      8. 练习 14.05:使用独立成分分析的降维

      9. 因子分析

      10. 练习 14.06:使用因子分析的降维

    5. 比较不同的降维技术

      1. 活动 14.02:在增强广告数据集上比较降维技术
    6. 总结

  8. 15. 集成学习

    1. 介绍

    2. 集成学习

      1. 方差

      2. 偏差

      3. 业务背景

      4. 练习 15.01:加载、探索和清理数据

      5. 活动 15.01:在信用卡数据上拟合逻辑回归模型

    3. 集成学习的简单方法

      1. 平均法

      2. 练习 15.02:使用平均法的集成模型

      3. 加权平均法

      4. 练习 15.03:使用加权平均法的集成模型

        1. 使用不同权重的第二次迭代

        2. 最大投票法

      5. 练习 15.04:使用最大投票的集成模型

    4. 集成学习的高级技术

      1. 袋装法

      2. 练习 15.05:使用袋装法的集成学习

      3. 提升方法

      4. 练习 15.06:使用提升法进行集成学习

      5. 堆叠法

      6. 练习 15.07:使用堆叠法进行集成学习

      7. 活动 15.02:高级集成技术比较

    5. 总结

地标

  1. 封面

  2. 目录

posted @ 2025-10-27 09:01  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报