Python-数据科学项目第二版-全-
Python 数据科学项目第二版(全)
原文:
annas-archive.org/md5/c89da1393a37db56d0a53ed5ccaa3f00译者:飞龙
前言
关于本书
如果数据是新石油,那么机器学习就是钻井机。随着公司能够获取越来越多的原始数据,能够提供支持商业决策的先进预测模型的能力变得愈发宝贵。
在本书中,你将基于一个现实的 数据集进行从头到尾的项目,内容被分解为易于操作的小练习。这种方式采用了案例研究的方法,模拟了你在实际数据科学项目中可能遇到的工作环境。
你将学习如何使用包括 pandas、Matplotlib 和 scikit-learn 在内的关键 Python 包,并掌握数据探索和数据处理的过程,随后再进行算法的拟合、评估和调优,如正则化逻辑回归和随机森林。
本书现已出版第二版,将带领你走过从探索数据到交付机器学习模型的全过程。该版本已更新至 2021 年,新增了关于 XGBoost、SHAP 值、算法公平性以及在现实世界中部署模型的伦理问题的内容。
在完成这本数据科学书籍后,你将具备构建自己的机器学习模型并从真实数据中获得洞见的技能、理解力和信心。
关于作者
Stephen Klosterman 是一位机器学习数据科学家,拥有数学、环境科学和生态学的背景。他的教育背景包括哈佛大学的生物学博士学位,并曾在数据科学课程中担任助教。他的专业经验涵盖了环境、医疗和金融领域。在工作中,他喜欢研究和开发能够创造价值并且易于理解的机器学习解决方案。在业余时间,他喜欢跑步、骑行、划桨板和音乐。
目标
-
使用 pandas Python 包加载、探索和处理数据
-
使用 Matplotlib 创建有效的数据可视化
-
使用 scikit-learn 和 XGBoost 实现预测性机器学习模型
-
使用 lasso 回归和岭回归减少模型的过拟合
-
构建决策树的集成模型,使用随机森林和梯度提升
-
评估模型性能并解释模型预测
-
通过明确的商业建议提供有价值的洞见
目标读者
Python 数据科学项目(第二版)适合任何想要入门数据科学和机器学习的人。如果你希望通过数据分析和预测建模来生成商业洞察,推动你的职业发展,那么本书是一个完美的起点。为了快速掌握所涉及的概念,建议你具有 Python 或其他类似语言(如 R、Matlab、C 等)的编程基础。此外,了解基本统计学知识,包括概率与线性回归等课程内容,或者在阅读本书时自行学习这些知识将会对你有所帮助。
方法
Python 数据科学项目采用案例学习方法,通过真实世界数据集的背景来教授概念。清晰的解释将加深你的理解,而富有趣味的练习和具有挑战性的活动将通过实践巩固你的知识。
关于各章节
第一章,数据探索与清洗,将帮助你开始使用 Python 和 Jupyter 笔记本。随后,本章将探索案例数据集,深入进行探索性数据分析、质量保证以及使用 pandas 进行数据清洗。
第二章,Scikit-Learn 简介与模型评估,将向你介绍二分类模型的评估指标。你将学习如何使用 scikit-learn 构建和评估二分类模型。
第三章,逻辑回归与特征探索的细节,深入探讨逻辑回归和特征探索。你将学习如何生成多特征与响应变量的相关性图,并将逻辑回归视为线性模型进行解读。
第四章,偏差-方差权衡,通过研究如何扩展逻辑回归模型来解决过拟合问题,探索了机器学习中过拟合、欠拟合和偏差-方差权衡的基础概念。
第五章,决策树与随机森林,将向你介绍基于树的机器学习模型。你将学习如何为机器学习任务训练决策树、可视化训练后的决策树,并训练随机森林并可视化结果。
第六章,梯度提升、XGBoost 与 SHAP 值,向你介绍了两个关键概念:梯度提升和Shapley 加性解释(SHAP)。你将学习如何训练 XGBoost 模型,并了解如何使用 SHAP 值为任何数据集的模型预测提供个性化的解释。
第七章,测试集分析、财务洞察与客户交付,介绍了几种分析模型测试集的技术,以推导出未来模型性能的可能洞察。本章还描述了交付和部署模型时需要考虑的关键因素,例如交付格式和如何监控模型的使用情况。
硬件要求
为了获得最佳的学习体验,我们推荐以下硬件配置:
-
处理器:Intel Core i5 或同等处理器
-
内存:4 GB RAM
-
存储:35 GB 可用空间
软件要求
你还需要预先安装以下软件:
-
操作系统:Windows 7 SP1 64 位、Windows 8.1 64 位或 Windows 10 64 位,Ubuntu Linux,或最新版本的 OS X
-
浏览器:Google Chrome/Mozilla Firefox 最新版本
-
Notepad++/Sublime Text 作为 IDE(这是可选的,因为你可以在浏览器中使用 Jupyter Notebook 完成所有练习)
-
安装 Python 3.8+(本书使用 Python 3.8.2)(来自 https://python.org 或通过 Anaconda 安装,见下文推荐)。在撰写时,用于第六章的 SHAP 库(梯度提升、XGBoost 和 SHAP 值)与 Python 3.9 不兼容。因此,如果你使用的是 Python 3.9 作为基础环境,建议按照下一节的说明设置 Python 3.8 环境。
-
根据需要安装 Python 库(如 Jupyter、NumPy、Pandas、Matplotlib 等,建议通过 Anaconda 安装,见下文)
安装与设置
在开始本书之前,建议安装 Anaconda 包管理器,并使用它来协调 Python 及其包的安装。
代码包
请查找本书的代码包,托管在 GitHub 上:https://github.com/PacktPublishing/Data-Science-Projects-with-Python-Second-Ed。
Anaconda 和环境设置
你可以访问以下链接来安装 Anaconda:https://www.anaconda.com/products/individual。滚动到页面底部,下载与你系统相关的安装程序。
推荐在 Anaconda 中创建一个环境,以进行本书中的练习和活动,这些活动已在此处指示的软件版本上经过测试。安装 Anaconda 后,如果你使用的是 macOS 或 Linux,请打开终端;如果使用的是 Windows,请打开命令提示符窗口,然后执行以下操作:
-
创建一个包含大多数所需包的环境。你可以根据需要命名它;在这里它被命名为
dspwp2。请将以下语句复制粘贴或直接在终端中输入:conda create -n dspwp2 python=3.8.2 jupyter=1.0.0 pandas=1.2.1 scikit-learn=0.23.2 numpy=1.19.2 matplotlib=3.3.2 seaborn=0.11.1 python-graphviz=0.15 xlrd=2.0.1 -
当提示时,键入
'y'并按[Enter]键。 -
激活环境:
conda activate dspwp2 -
安装剩余的包:
conda install -c conda-forge xgboost=1.3.0 shap=0.37.0 -
当提示时,键入
'y'并按[Enter]键。 -
你已经可以使用该环境了。完成后,要停用它:
conda deactivate
我们还在 https://github.com/PacktPublishing/ 上提供了来自我们丰富书籍和视频目录的其他代码包。快去看看吧!
约定
文章中的代码、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账户名显示如下:“通过在命令行输入conda list,你可以看到在你的环境中安装的所有包。”
一块代码的设置如下:
import numpy as np #numerical computation
import pandas as pd #data wrangling
import matplotlib.pyplot as plt #plotting package
#Next line helps with rendering plots
%matplotlib inline
import matplotlib as mpl #add'l plotting functionality
mpl.rcParams['figure.dpi'] = 400 #high res figures
import graphviz #to visualize decision trees
新术语和重要单词以粗体显示。屏幕上看到的词汇,例如菜单或对话框中的词汇,会像这样出现在文本中:“从New菜单创建一个新的 Python 3 笔记本,如下所示。”
代码展示
跨越多行的代码使用反斜杠( \ )进行分割。当代码执行时,Python 会忽略反斜杠,并将下一行的代码视为当前行的直接延续。
例如:
my_new_lr = LogisticRegression(penalty='l2', dual=False,\
tol=0.0001, C=1.0,\
fit_intercept=True,\
intercept_scaling=1,\
class_weight=None,\
random_state=None,\
solver='lbfgs',\
max_iter=100,\
multi_class='auto',\
verbose=0, warm_start=False,\
n_jobs=None, l1_ratio=None)
注释被添加到代码中以帮助解释特定的逻辑。单行注释使用 # 符号表示,如下所示:
import pandas as pd
import matplotlib.pyplot as plt #import plotting package
#render plotting automatically
%matplotlib inline
与我们联系
我们始终欢迎读者的反馈。
customercare@packtpub.com。
勘误:虽然我们已经尽力确保内容的准确性,但错误仍然可能发生。如果你在本书中发现错误,我们将非常感激你能报告给我们。请访问 www.packtpub.com/support/errata 并填写表格。
copyright@packt.com,并附上材料链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有兴趣写作或为书籍贡献内容,请访问 authors.packtpub.com。
请留下评论
请通过在亚马逊上留下详细且公正的评论告诉我们你的想法。我们欢迎所有反馈——它帮助我们继续制作优秀的产品,并帮助有志开发者提升技能。请花几分钟时间提供你的想法——这对我们意义重大。你可以通过点击以下链接留下评论: https://packt.link/r/1800564481。
第一章:1. 数据探索与清理
概述
在本章中,你将迈出使用 Python 和 Jupyter 笔记本的第一步,这些是数据科学家常用的工具。接下来,你将首次查看本书核心案例研究项目的数据集。你将开始培养对数据在建模前需要进行的质量保证检查的直觉。到本章结束时,你将能够使用 pandas,这是 Python 中处理表格数据的顶级包,进行探索性数据分析、质量保证和数据清理。
介绍
大多数企业拥有大量关于其运营和客户的数据。通过描述性图表、图形和表格来报告这些数据,是了解企业当前状况的好方法。然而,为了为未来的商业战略和运营提供量化指导,还需要进一步深入。这正是机器学习和预测建模技术派上用场的地方。本书将展示如何通过预测模型,从描述性分析转变为为未来运营提供具体指导的方法。
为了实现这个目标,我们将通过 Python 和许多它的包,介绍一些最广泛使用的机器学习工具。你还将获得执行成功项目所需的实用技能:在检查数据时保持好奇心,以及与客户的沟通。花时间仔细查看数据集,并批判性地检查它是否准确地满足预期目的,是值得的。你将在这里学习评估数据质量的几种技术。
在本章中,熟悉了基本的数据探索工具之后,我们将讨论几种典型的工作场景,说明你可能如何接收数据。然后,我们将开始对案例研究数据集进行全面的探索,帮助你学习如何发现潜在问题,以便当你准备进行建模时,能够有信心地进行操作。
Python 和 Anaconda 包管理系统
本书中,我们将使用 Python 编程语言。Python 是数据科学的顶级语言,也是增长最快的编程语言之一。Python 受欢迎的一个常见原因是它易于学习。如果你有 Python 经验,那就太好了;不过,如果你有其他语言的经验,比如 C、Matlab 或 R,你应该也不会遇到太多困难。你应该熟悉计算机编程的一般结构,以便最大限度地利用本书。此类结构的示例包括for循环和if语句,它们指导程序的控制流。不论你曾使用什么语言,你很可能都对这些结构有所了解,而它们也同样出现在 Python 中。
Python 的一个关键特点是它与其他一些语言不同,它是零索引的;换句话说,一个有序集合的第一个元素的索引是0。Python 还支持负索引,其中索引-1表示有序集合中的最后一个元素,负索引从集合的末尾开始倒数。切片操作符:可以用来从有序集合中选择一个范围内的多个元素,既可以从开始位置选择,也可以选择到集合的末尾。
索引和切片操作符
这里,我们展示了索引和切片操作符是如何工作的。为了进行索引操作,我们将创建一个range() Python 函数。range()函数在技术上创建了一个list()函数,尽管你不需要关心这个细节。以下截图显示了打印在控制台上的前五个正整数的列表,以及一些索引操作,并将列表的第一个项更改为不同数据类型的新值:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_01_01.jpg)
图 1.1:列表创建和索引
关于图 1.1需要注意几点:对于切片索引和range()函数,区间的端点是开放的,而起始点是闭合的。换句话说,注意当我们指定range()的起始和结束时,端点 6 不包括在结果中,但起始点 1 被包括在内。同样,当用切片[:3]索引列表时,它包括所有索引小于 3 的元素,但不包括索引为 3 的元素。
我们之前提到过有序集合,但 Python 也包括无序集合。其中一个重要的集合类型叫做{},它包含键:值对,通过逗号分隔。以下截图展示了如何创建一个包含水果数量的字典——首先查看苹果的数量,然后添加一种新的水果类型及其数量:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_01_02.jpg)
图 1.2:一个字典示例
Python 还有许多其他独特的特点,我们这里只是给你一个大致的概念,不会涉及太多细节。实际上,你可能会使用像pandas和numpy这样的包来处理大多数 Python 中的数据。NumPy 提供了对数组和矩阵的快速数值计算,而 pandas 则提供了丰富的数据处理和探索功能,特别是对被称为DataFrames的数据表的操作。然而,熟悉一些 Python 的基础知识是很有帮助的,因为它是所有这些内容的基础。例如,索引在 NumPy 和 pandas 中的工作方式与在 Python 中相同。
Python 的一个优势是它是开源的,并且拥有一个活跃的开发者社区,创造了许多令人惊叹的工具。我们将在本书中使用其中的几个工具。使用不同贡献者提供的开源包的一个潜在陷阱是各个包之间的依赖关系。例如,如果您想安装 pandas,它可能依赖于某个版本的 NumPy,而您可能已经安装了该版本,也可能没有。包管理系统在这方面让生活变得更加轻松。当您通过包管理系统安装新包时,它会确保所有依赖关系都已满足。如果没有,它会提示您升级或根据需要安装新包。
对于本书,我们将使用Anaconda包管理系统,您应该已经安装了它。虽然我们这里只使用 Python,但也可以在 Anaconda 中运行 R。
注释:环境
推荐为本书创建一个新的 Python 3.x 环境。环境就像是 Python 的独立安装版本,其中已安装的包集可能不同,Python 的版本也可能不同。环境对于开发需要在不同版本的 Python 中部署的项目非常有用,这些项目可能依赖于不同的包版本。有关这方面的一般信息,请参阅 docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html。请在开始接下来的练习之前,查看前言中关于为本书设置 Anaconda 环境的具体说明。
练习 1.01:检查 Anaconda 并熟悉 Python
在本练习中,您将检查 Anaconda 安装中的包,并练习一些基本的 Python 控制流和数据结构,包括for 循环、dict 和 list。这将确认您已经完成了前言中的安装步骤,并展示 Python 语法和数据结构可能与您熟悉的其他编程语言有所不同。执行以下步骤以完成练习:
注释
在执行本章的练习和活动之前,请确保您已按照前言中提到的设置 Python 环境的说明进行操作。本练习的代码文件可以在此找到:packt.link/N0RPT。
-
打开终端(如果您使用的是 macOS 或 Linux)或在 Windows 中打开命令提示符窗口。如果您使用的是环境,请使用
conda activate <name_of_your_environment>激活它。然后在命令行中键入condalist。您应该看到类似以下内容的输出:![图 1.3:从 conda list 中选择包]()
图 1.3:从 conda list 中选择包
你可以看到环境中安装的所有包,包括我们将直接交互的包,以及它们的依赖项,这些依赖项是它们正常运行所必需的。包管理系统的一个主要优势是能够管理包之间的依赖关系。
注释
有关 Anaconda 和命令行交互的更多信息,请查看此“备忘单”:
docs.conda.io/projects/conda/en/latest/_downloads/843d9e0198f2a193a3484886fa28163c/conda-cheatsheet.pdf。 -
在终端中输入
python,打开命令行 Python 解释器。你应该会得到类似以下的输出:![图 1.4:命令行 Python]()
图 1.4:命令行 Python
你应该看到一些关于 Python 版本的信息,以及 Python 命令提示符(
>>>)。当你在此提示符后输入时,你正在编写 Python 代码。注释
尽管本书中我们将使用 Jupyter notebook,但本练习的目标之一是通过在命令提示符下编写和运行 Python 程序的基本步骤。
-
在命令提示符下编写
for循环,使用以下代码打印从 0 到 4 的值(请注意,在命令行 Python 解释器中编写代码时,第二行和第三行开头的三个点会自动出现;如果你在 Jupyter notebook 中编写代码,这些点将不会出现):for counter in range(5): ... print(counter) ...当你在看到
...提示符时按下Enter,你应该得到以下输出:![图 1.5:命令行中 for 循环的输出]()
图 1.5:命令行中 for 循环的输出
请注意,在 Python 中,
for循环的开始后面紧跟一个冒号,for循环打印由range()迭代器返回的值,这些值通过使用counter变量与in关键字反复访问。注释
有关更多关于 Python 代码规范的详细信息,请参考以下链接:
www.python.org/dev/peps/pep-0008/。现在,我们将回到字典的示例。这里的第一步是创建字典。
-
使用以下代码创建一个水果字典(
apples、oranges和bananas):example_dict = {'apples':5, 'oranges':8, 'bananas':13} -
使用
list()函数将字典转换为列表,如下所示的代码片段:dict_to_list = list(example_dict) dict_to_list一旦运行前面的代码,你应该会得到以下输出:
['apples', 'oranges', 'bananas']请注意,当这完成后,我们检查内容时,列表中仅捕获了字典的键。如果我们想要获取值,必须使用
.values()方法指定。此外,请注意,字典键的列表恰好与我们创建字典时书写的顺序相同。然而,这并不保证,因为字典是无序集合类型。使用列表时,你可以通过
+运算符将其他列表添加到现有列表中。作为示例,在下一步中,我们将现有的水果列表与只包含一种水果的新列表合并,并覆盖包含原始列表的变量,像这样:list(example_dict.values());有兴趣的读者可以自行验证这一点。 -
使用
+运算符将现有的水果列表与只包含一个水果(pears)的新列表合并:dict_to_list = dict_to_list + ['pears'] dict_to_list你的输出将如下所示:
['apples', 'oranges', 'bananas', 'pears']sorted()函数可以用于此;它将返回输入的排序版本。在我们的例子中,这意味着水果种类列表将按字母顺序排序。 -
使用
sorted()函数按字母顺序排序水果列表,如下所示:sorted(dict_to_list)一旦运行前面的代码,你应该会看到以下输出:
['apples', 'bananas', 'oranges', 'pears']
现在已经足够的 Python 知识了。我们将展示如何执行本书中的代码,所以在过程中你的 Python 知识应该会有所提升。在你打开 Python 解释器时,你可能希望运行图 1.1和1.2中展示的代码示例。当你使用完解释器后,可以输入quit()退出。
注意
随着你学习的深入,并且不可避免地想尝试新事物,请参考官方的 Python 文档:docs.python.org/3/。
数据科学问题的不同类型
作为数据科学家,你的大部分时间可能都会花在数据清理上:弄清楚如何获取数据、获取数据、检查数据、确保数据的正确性和完整性,并将数据与其他类型的数据结合。pandas 是 Python 中广泛使用的数据分析工具,它能帮助你加速数据探索过程,正如我们在本章中所看到的。然而,本书的一个关键目标是帮助你踏上成为机器学习数据科学家的旅程,而这需要你掌握预测建模的艺术和科学。这意味着使用数学模型或理想化的数学公式来学习数据中的关系,希望当新的数据到来时,能够做出准确且有用的预测。
对于预测建模的使用场景,数据通常以表格结构组织,包含特征和响应变量。例如,如果你想根据一些关于房子的特征来预测房价,如面积和卧室数量,这些特征将被视为特征,而房价则是响应变量。响应变量有时也称为目标变量或因变量,而特征有时也称为自变量。
如果你有一个包含 1,000 个房屋的数据集,其中包括这些特征的值和房屋的价格,那么你可以说你拥有 1,000 个样本的标注数据,其中标签是响应变量的已知值:不同房屋的价格。通常,表格数据结构被组织成不同的行表示不同的样本,而特征和响应占据不同的列,并且还有其他元数据,如样本 ID,如图 1.6所示:

图 1.6:标注数据(房价是已知的目标变量)
回归问题
一旦你训练了一个模型,通过使用标注数据学习特征与响应之间的关系,你就可以利用它对那些你不知道价格的房屋进行预测,基于特征中包含的信息。在这种情况下,预测建模的目标是能够做出接近房屋真实价值的预测。由于我们预测的是一个连续尺度上的数值,这被称为回归问题。
分类问题
另一方面,如果我们试图对房屋做出定性预测,回答像“这栋房屋在未来 5 年内会出售吗?”或“房主会违约吗?”这样的是或否问题,那么我们将解决一个称为分类问题的问题。在这里,我们希望能够正确地回答是或否的问题。下图是一个示意图,展示了模型训练的工作原理,以及回归或分类模型的可能结果:

图 1.7:回归和分类的模型训练与预测示意图
分类和回归任务被称为监督学习,这是一类依赖于标注数据的问题。可以将这些问题视为需要目标变量的已知值进行“监督”。相对而言,还有无监督学习,它涉及到更开放性的问题,试图在一个没有标签的数据集中找到某种结构。从更广泛的角度来看,任何应用数学问题,包括像优化、统计推断和时间序列建模这样的领域,都可能被视为数据科学家的适当职责。
使用 Jupyter 和 pandas 加载案例研究数据
现在是时候首次查看我们在案例研究中将使用的数据了。我们在本节中不会做任何其他事情,只是确保我们能正确地将数据加载到Jupyter notebook中。数据的检查和对你将要解决的问题的理解将在之后进行。
数据文件是一个名为default_of_credit_card_clients__courseware_version_1_21_19.xls的 Excel 电子表格。我们建议你先在 Excel 或你选择的电子表格程序中打开该电子表格。注意行数和列数。查看一些示例值。这将帮助你了解是否已经正确加载该文件到 Jupyter 笔记本中。
注意
数据集可以从以下链接获取:packt.link/wensZ。这是原始数据集的修改版本,原数据集来自 UCI 机器学习库[archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院。
什么是 Jupyter 笔记本?
Jupyter 笔记本是互动式编码环境,允许插入文本和图形。它们是数据科学家用于交流和保存结果的绝佳工具,因为方法(代码)和信息(文本和图形)是集成在一起的。你可以将这个环境看作一个可以编写和执行代码的网页。实际上,Jupyter 笔记本可以呈现为网页,就像在 GitHub 上那样。这里有一个示例笔记本:packt.link/pREet。查看它,了解你可以做什么。以下是该笔记本的摘录,展示了代码、图形和散文,这在这种情况下被称为Markdown:

图 1.8:展示代码、图形和 Markdown 文本的 Jupyter 笔记本示例
学习 Jupyter 笔记本的首要任务之一是如何浏览和进行编辑。你可以选择两种模式。如果你选择一个单元格并按下Enter,你会进入编辑模式,在该模式下你可以编辑该单元格中的文本。如果你按下Esc,则进入命令模式,可以在笔记本中进行导航。
注意
如果你正在阅读本书的印刷版,可以通过访问以下链接下载并浏览本章中某些图像的彩色版本:packt.link/T5EIH。
当你处于命令模式时,有许多有用的快捷键可以使用。上箭头和下箭头可以帮助你选择不同的单元格并滚动浏览笔记本。如果在命令模式下按下y键,选中的单元格会变为代码单元格,其中的文本会被解释为代码。按下m键会将其变为Markdown 单元格,在其中你可以编写格式化文本。按下Shift + Enter会执行该单元格,呈现 Markdown 或执行代码,具体取决于情况。在接下来的练习中,你将通过 Jupyter 笔记本进行一些实践。
在我们第一个 Jupyter notebook 中的第一个任务是加载案例研究数据。为此,我们将使用一个名为 pandas 的工具。毫不夸张地说,pandas 可能是 Python 中最优秀的数据处理工具。
DataFrame 是 pandas 中的一个基础类。我们稍后会讨论类是什么,但你可以将其看作数据结构的模板,其中数据结构类似于我们之前讨论的列表或字典。然而,DataFrame 的功能比这两者都要强大得多。DataFrame 在许多方面类似于电子表格。它有行,这些行通过行索引进行标记;它还有列,通常会有类似列头的标签,可以被看作列索引。Index 实际上是 pandas 中用来存储 DataFrame 索引的数据类型,而列则有自己的数据类型,称为 Series。
使用 DataFrame,你可以做很多与 Excel 表格相同的操作,比如创建数据透视表和筛选行。pandas 还包含类似 SQL 的功能。例如,你可以将不同的 DataFrame 合并在一起。DataFrame 的另一个优点是,一旦你的数据被包含在其中,你就可以随时使用 pandas 提供的强大功能进行数据分析。下图是一个 pandas DataFrame 的示例:

图 1.9:带有整数行索引在左侧、字符串列索引的 pandas DataFrame 示例
图 1.9 中的示例实际上就是案例研究的数据。作为使用 Jupyter 和 pandas 的第一步,我们现在将展示如何创建一个 Jupyter notebook 并使用 pandas 加载数据。在 pandas 中,你可以使用几个方便的函数来探索数据,包括 .head() 查看 DataFrame 的前几行,.info() 查看所有列的数据类型,.columns 返回列名的字符串列表,等等,我们将在接下来的练习中学习这些函数。
练习 1.02:在 Jupyter Notebook 中加载案例研究数据
现在你已经了解了 Jupyter notebooks——我们将编写代码的环境,和 pandas——数据处理包,让我们来创建第一个 Jupyter notebook。在这个 notebook 中,我们将使用 pandas 加载案例研究数据,并对其进行简单的检查。请按照以下步骤完成练习:
注意
本练习的 Jupyter notebook 可以在 packt.link/GHPSn 找到。
-
打开终端(macOS 或 Linux)或命令提示符窗口(Windows),然后输入
jupyter notebook(如果你使用的是 Anaconda 环境,请先激活环境)。您将在浏览器中看到 Jupyter 界面。如果浏览器没有自动打开,您可以将终端中的 URL 复制并粘贴到浏览器中。在此界面中,您可以从启动笔记本服务器时所在的目录开始浏览您的文件夹。
-
导航到您将存储本书材料的方便位置,然后从 New 菜单创建一个新的 Python 3 笔记本,如下所示:
![图 1.10:Jupyter 首页]()
图 1.10:Jupyter 首页
-
在命令模式下(按 Esc 进入命令模式)通过输入 m 来将您的第一个单元格设置为 Markdown 单元格,然后在第一行的开头输入一个井号
#,后面加一个空格,以设置标题。为您的笔记本添加标题。接下来的几行,输入描述。下面是一个示例的截图,展示了其他类型的 Markdown 语法,如粗体、斜体,以及如何在 Markdown 单元格中书写代码风格的文本:
![图 1.11:未渲染的 Markdown 单元格]()
图 1.11:未渲染的 Markdown 单元格
请注意,良好的实践是为您的笔记本添加标题和简短的描述,以便读者了解其目的。
-
按 Shift + Enter 来渲染 Markdown 单元格。
这也应该会创建一个新的单元格,它将是一个代码单元格。您可以通过按 m 将其更改为 Markdown 单元格,通过按 y 恢复为代码单元格。您可以通过旁边的
In [ ]:来判断它是代码单元格。 -
在新单元格中输入
importpandasaspd,如下所示的截图所示:![图 1.12:渲染后的 Markdown 单元格和代码单元格]()
图 1.12:渲染后的 Markdown 单元格和代码单元格
执行此单元格后,
pandas模块将被加载到您的计算环境中。通常,我们会使用as来导入模块,并为其创建一个简短的别名,比如pd。现在,我们将使用 pandas 加载数据文件。该文件是 Microsoft Excel 格式,因此我们可以使用pd.read_excel。注意
若要了解
pd.read_excel的所有可能选项,请参考以下文档:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_excel.html。 -
使用
pd.read_excel()方法将数据集(Excel 格式)作为 DataFrame 导入,如下所示的代码片段:df = pd.read_excel('../../Data/default_of_credit_card_clients'\ '__courseware_version_1_21_19.xls')请注意,您需要指定 Excel 文件所在的位置。如果文件与您的笔记本在同一目录下,您可以只输入文件名。
pd.read_excel方法会将 Excel 文件加载到一个DataFrame中,我们将其命名为df。默认情况下,电子表格的第一张表单会被加载,而在此情况下,只有这一张表单。现在我们可以使用 pandas 的强大功能了。让我们在接下来的几个步骤中进行一些快速检查。首先,行数和列数是否与我们在 Excel 中查看文件时看到的一致?
-
使用
.shape方法查看行列的数量,如以下代码片段所示:df.shape一旦运行该单元格,你将得到以下输出:
Out[3]: (30000, 25)这应该与你在电子表格中的观察一致。如果不一致,你就需要查看
pd.read_excel的各种选项,看看是否需要调整什么。
通过这个练习,我们成功地将数据集加载到 Jupyter 笔记本中。你还可以尝试对 DataFrame 使用 .info() 和 .head() 方法,分别查看所有列的信息,并显示 DataFrame 的前几行。现在你已经能够开始使用 pandas 处理数据了。
最后,虽然这可能已经很清楚了,但请注意,如果你在一个代码单元中定义了一个变量,它在笔记本中的其他代码单元也可以使用。这是因为,只要笔记本在运行,笔记本中的代码单元被认为共享作用域,如下面的截图所示:

图 1.13:单元格之间的变量作用域
每次启动 Jupyter 笔记本时,尽管代码和 Markdown 单元格会保存你之前的工作,但环境会重新初始化,你需要重新加载所有模块和数据才能继续工作。你也可以使用笔记本中的内核菜单手动关闭或重启笔记本。关于 Jupyter 笔记本的更多细节可以在这里找到:jupyter-notebook.readthedocs.io/en/stable/。
注
在本书中,每个新的练习和活动都会在一个新的 Jupyter 笔记本中完成。然而,一些练习笔记本也包含在练习前的部分中展示的额外 Python 代码和输出。还有一些参考笔记本包含了每个章节的全部内容。例如,第一章《数据探索与清洗》的笔记本可以在这里找到:packt.link/zwofX。
熟悉数据并进行数据清洗
现在让我们初步查看一下这些数据。在你的数据科学家工作中,你可能会遇到几种收到这样的数据集的情况。包括以下几种:
-
你创建了生成数据的 SQL 查询。
-
一位同事根据你的意见为你写了一个 SQL 查询。
-
一位了解数据的同事把它交给了你,但没有征求你的意见。
-
你得到一个对数据了解不多的数据集。
在第 1 和第 2 种情况下,你的输入参与了数据的生成/提取。在这些场景中,你可能理解了商业问题,然后在数据工程师的帮助下找到所需的数据,或者自己做研究并设计了生成数据的 SQL 查询。通常,尤其是随着你在数据科学角色上经验的积累,第一步会是与商业合作伙伴会面,理解并完善商业问题的数学定义。然后,你将在定义数据集内容中发挥关键作用。
即使你对数据有相对较高的熟悉度,进行数据探索并查看不同变量的汇总统计仍然是一个重要的第一步。这个步骤将帮助你选择好的特征,或者给你一些如何构建新特征的思路。然而,在第三和第四种情况中,如果你的输入没有涉及或者你对数据了解较少,数据探索就显得更加重要。
数据科学过程中的另一个重要初步步骤是检查数据字典。数据字典是一个文档,解释了数据拥有者认为数据中应该包含的内容,比如列标签的定义。数据科学家的职责是仔细审查数据,确保这些定义与数据实际内容一致。在第 1 和第 2 种情况下,你可能需要自己创建数据字典,这应该视为重要的项目文档。在第 3 和第 4 种情况下,你应该尽可能寻找数据字典。
本书中我们将使用的案例研究数据类似于此处的第 3 种情况。
商业问题
我们的客户是一家信用卡公司。他们为我们提供了一个数据集,包含过去 6 个月内约 30,000 名账户持有人的一些人口统计信息和近期财务数据。该数据集是在信用账户级别的;换句话说,每一行代表一个账户(你应始终明确数据集中的每一行的定义)。每一行会标注账户所有者是否在 6 个月历史数据期之后的下一个月违约,或者换句话说,未能按时支付最低款项。
目标
你的目标是根据人口统计信息和历史数据,开发一个预测模型,预测账户下个月是否会违约。在本书后续部分,我们将讨论该模型的实际应用。
数据已经准备好,并且提供了数据字典。本书附带的数据集default_of_credit_card_clients__courseware_version_1_21_19.xls是 UCI 机器学习库中该数据集的修改版:archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients。请查看该网页,其中包含数据字典。
数据探索步骤
现在我们已经了解了业务问题,并对数据中应该包含的内容有了大致了解,我们可以将这些印象与实际数据进行对比。你在数据探索中的任务,不仅是通过直接查看数据以及使用数值和图形摘要来了解数据,还要批判性地思考这些数据是否合理,并与所提供的信息相匹配。这些都是数据探索中的有用步骤:
-
数据中有多少列?
这些可能是特征、响应或元数据。
-
数据中有多少行(样本)?
-
有哪些特征?哪些是类别型的,哪些是数值型的?
类别特征的值属于离散的类别,例如“是”、“否”或“也许”。
数值特征通常是连续的数值尺度,比如美元金额。
-
这些特征中的数据看起来如何?
为了查看这一点,你可以检查数值特征的值范围,或类别特征中不同类别的频率,例如。
-
有没有缺失的数据?
我们在上一节已经回答了问题 1 和 2;数据中有 30,000 行和 25 列。当我们在接下来的练习中开始探索其余问题时,pandas 将是我们的首选工具。我们从验证基本数据完整性开始,进入下一个练习。
注意
请注意,与网站描述的数据字典相比,我们数据中的X6-X11被称为PAY_1-PAY_6。类似地,X12-X17对应的是BILL_AMT1-BILL_AMT6,而X18-X23则是PAY_AMT1-PAY_AMT6。
练习 1.03:验证基本数据完整性
在这个练习中,我们将进行基本的检查,验证我们的数据集是否包含我们所期望的内容,并检查样本数量是否正确。
数据应该包含 30,000 个信用账户的观察数据。虽然有 30,000 行数据,但我们还应该检查是否有 30,000 个唯一账户 ID。如果生成数据的 SQL 查询是在一个不熟悉的架构下运行的,那么本应唯一的值可能实际上并不唯一。
为了检查这一点,我们可以检查唯一账户 ID 的数量是否与行数相同。按照以下步骤完成练习:
注意
这个练习的 Jupyter 笔记本可以在这里找到:packt.link/EapDM。
-
导入 pandas,加载数据,并运行以下命令检查列名,使用Shift + Enter:
import pandas as pd df = pd.read_excel('../Data/default_of_credit_card'\ '_clients__courseware_version_1_21_19.xls') df.columnsDataFrame 的
.columns方法用于检查所有列名。运行单元格后,你将得到以下输出:![图 1.14:数据集的列]()
图 1.14:数据集的列
如可以观察到,所有列名都列出了。账户 ID 列被标记为
ID。其余列似乎是我们的特征,最后一列是响应变量。让我们快速回顾一下客户提供的数据集信息:LIMIT_BAL: 提供的信用额度(以新台币为单位),包括个人消费信用和家庭(附加)信用。SEX: 性别 (1 = 男性;2 = 女性)。注意
出于伦理考虑,我们不会使用性别数据来决定信用评级。
EDUCATION: 教育水平 (1 = 研究生; 2 = 大学; 3 = 高中; 4 = 其他)。MARRIAGE: 婚姻状况 (1 = 已婚;2 = 单身;3 = 其他)。AGE: 年龄(岁)。PAY_1–PAY_6: 过去付款记录。记录从 4 月到 9 月的每月付款,这些数据存储在这些列中。PAY_1代表 9 月的还款状态;PAY_2是 8 月的还款状态;依此类推,直到PAY_6,代表 4 月的还款状态。还款状态的测量尺度如下:-1 = 按时支付;1 = 延迟支付 1 个月;2 = 延迟支付 2 个月;依此类推,直到 8 = 延迟支付 8 个月;9 = 延迟支付 9 个月及以上。
BILL_AMT1–BILL_AMT6: 账单金额(新台币)。BILL_AMT1代表 9 月的账单金额;BILL_AMT2代表 8 月的账单金额;依此类推,直到BILL_AMT6,代表 4 月的账单金额。PAY_AMT1–PAY_AMT6: 之前的付款金额(新台币)。PAY_AMT1代表 9 月的支付金额;PAY_AMT2代表 8 月的支付金额;依此类推,直到PAY_AMT6,代表 4 月的支付金额。接下来,我们在下一步中使用
.head()方法查看数据的前几行。默认情况下,这将返回前 5 行数据。 -
在随后的单元格中运行以下命令:
df.head()这是你应该看到的输出的一部分:
![图 1.15:DataFrame 的 .head() 方法]()
图 1.15:DataFrame 的 .head() 方法
ID 列似乎包含唯一标识符。现在,为了验证它们是否确实在整个数据集中是唯一的,我们可以使用
.nunique()方法计算ID列(即 Series)的唯一值数量。我们首先使用方括号选择该列。 -
选择列(
ID)并使用以下命令计算唯一值:df['ID'].nunique()以下是输出结果:
29687从前面的输出可以看出,唯一条目的数量是
29,687。 -
运行以下命令以获取数据集中的行数:
df.shape如下输出所示,数据集的总行数为
30,000:(30000, 25)我们看到这里的唯一 ID 数少于行数。这意味着 ID 不是数据行的唯一标识符。所以我们知道 ID 有重复。那么重复的程度如何?某个 ID 是否重复多次?有多少个 ID 是重复的?
我们可以在 ID Series 上使用
.value_counts()方法来开始回答这些问题。这类似于一个id_counts变量。 -
将值计数存储在定义为
id_counts的变量中,然后使用.head()方法显示存储的值,如下所示:id_counts = df['ID'].value_counts() id_counts.head()你将获得以下输出:
![图 1.16:获取账户 ID 的值计数]()
图 1.16:获取账户 ID 的值计数
请注意,
.head()默认返回前五行。你可以通过在括号()中传入所需的数字来指定显示的条目数。 -
通过运行另一个值计数来显示重复条目的数量:
id_counts.value_counts()你将获得以下输出:
![图 1.17:获取账户 ID 的值计数]()
图 1.17:获取账户 ID 的值计数
在这里,我们可以看到大多数 ID 都恰好出现一次,正如预期的那样。然而,313 个 ID 出现了两次。所以,没有任何 ID 出现超过两次。有了这些信息,我们可以开始仔细查看这个数据质量问题,并着手修复它。我们将创建布尔掩码来实现这一点。
布尔掩码
为了帮助清理案例研究数据,我们引入了==的概念,用来查找数组中包含某个特定值的位置。其他比较方式,如“大于” (>)、 “小于” (<)、 “大于或等于” (>=)、 “小于或等于” (<=),也可以类似使用。此类比较的输出是一个True/False值的数组或 Series,如果条件成立,则为True,否则为False。为了说明其工作原理,我们将使用np。我们还将从 NumPy 中的 random 模块导入默认的随机数生成器:
import numpy as np
from numpy.random import default_rng
现在我们使用所谓的12345:
rg = default_rng(12345)
接下来,我们使用rg的integers方法生成 100 个随机整数,传入合适的参数。我们生成的整数范围为 1 到 4 之间。请注意,high参数默认指定的是开区间,即范围的上限不包含在内:
random_integers = rg.integers(low=1,high=5,size=100)
让我们看一下该数组的前五个元素,使用random_integers[:5]。输出应该如下所示:
array ([3, 1, 4, 2, 1])
假设我们想知道random_integers中所有等于 3 的元素的位置。我们可以创建一个布尔掩码来实现:
is_equal_to_3 = random_integers == 3
通过检查前五个元素,我们知道第一个元素等于 3,但其余的都不等于。所以在我们的布尔掩码中,我们期望第一个位置为True,接下来的四个位置为False。这是对的吗?
is_equal_to_3[:5]
上述代码应当给出以下输出:
array([ True, False, False, False, False])
这是我们所期待的。这显示了布尔掩码的创建。但我们还可以用它们做什么呢?假设我们想知道有多少个元素等于 3。为了知道这一点,你可以对布尔掩码进行求和,它将 True 解释为 1,将 False 解释为 0:
sum(is_equal_to_3)
这将给我们以下输出:
31
这很有道理,因为在一个随机、每个值等可能的 4 个值中,我们会预期每个值大约有 25% 的概率出现。除了看到数组中有多少个值符合布尔条件外,我们还可以使用布尔掩码选择数组中符合该条件的元素。布尔掩码可以直接用于索引数组,正如下面所示:
random_integers[is_equal_to_3]
这将输出符合我们指定的布尔条件的 random_integers 数组元素。在这个例子中,31 个等于 3 的元素:

图 1.18:使用布尔掩码索引数组
现在你已经掌握了布尔数组的基础知识,它在许多情况下都非常有用。特别是,你可以使用 DataFrame 的 .loc 方法,通过布尔掩码对行进行索引,通过标签对列进行索引,从而获取满足条件的不同列中的值。让我们继续用这些技能探索案例研究数据。
注意
包含前一节中展示的代码和相应输出的 Jupyter notebook 可以在此找到:packt.link/pT9gT。
练习 1.04:继续验证数据完整性
在本次练习中,利用我们对布尔数组的了解,我们将检查一些我们发现的重复 ID。在 练习 03,验证基本数据完整性 中,我们学到没有 ID 出现超过两次。我们可以利用这一点来定位重复的 ID 并进行检查。然后我们采取措施从数据集中删除质量可疑的行。按照以下步骤完成本次练习:
注意
本次练习的 Jupyter notebook 可以在这里找到:packt.link/snAP0。
-
继续我们在 练习 1.03,验证基本数据完整性 中的内容,我们需要获取
id_countsSeries 中计数为2的位置,以定位重复项。首先,我们加载数据并获取 ID 的值计数,以便回到 练习 03,验证基本数据完整性 中的位置,然后我们创建一个布尔掩码,定位重复的 ID,变量名为dupe_mask,并显示前五个元素。使用以下命令:import pandas as pd df = pd.read_excel('../../Data/default_of_credit_card_clients'\ '__courseware_version_1_21_19.xls') id_counts = df['ID'].value_counts() id_counts.head() dupe_mask = id_counts == 2 dupe_mask[0:5]你将得到以下输出(请注意,ID 的排序在你的输出中可能不同,因为
value_counts是按频率排序的,而不是 ID 的索引):![图 1.19:使用布尔掩码定位重复的 ID]()
图 1.19:使用布尔掩码定位重复的 ID
请注意,在上面的输出中,我们仅使用
dupe_mask显示前五个条目,以说明此数组的内容。你可以编辑方括号 ([]) 中的整数索引来更改显示的条目数。下一步是使用此逻辑掩码选择重复的 ID。这些 ID 本身包含在
id_count系列的索引中。我们可以访问该索引,以便使用逻辑掩码进行选择。 -
使用以下命令访问
id_count的索引,并显示前五行作为上下文:id_counts.index[0:5]这样,你将获得以下输出:
![图 1.20:重复的 ID]()
图 1.20:重复的 ID
-
使用以下命令选择并将重复的 ID 存储到名为
dupe_ids的新变量中:dupe_ids = id_counts.index[dupe_mask] -
将
dupe_ids转换为列表,然后使用以下命令获取该列表的长度:dupe_ids = list(dupe_ids) len(dupe_ids)你应该获得以下输出:
313我们将
dupe_ids变量更改为list类型,因为在未来的步骤中我们将需要它以这种形式。该列表的长度为313,如前面的输出所示,这与我们通过值计数了解到的重复 ID 数量一致。 -
我们通过使用以下命令显示前五个条目来验证
dupe_ids中的数据:dupe_ids[0:5]我们获得了以下输出:
![图 1.21:制作重复 ID 列表]()
图 1.21:制作重复 ID 列表
我们可以从前面的输出中观察到,列表包含所需的重复 ID 条目。现在我们可以检查这些重复 ID 的数据,特别是我们想查看这些特征的值,看看是否有任何区别。我们将使用 DataFrame
df的.isin和.loc方法来实现这一目的。使用我们重复列表中的前三个 ID,
dupe_ids[0:3],我们将首先查找包含这些 ID 的行。如果我们将这个 ID 列表传递给 ID 系列的.isin方法,它将创建另一个逻辑掩码,我们可以用来在较大的 DataFrame 中显示包含这些 ID 的行。.isin方法嵌套在.loc语句中,后者用于索引 DataFrame,以选择所有包含True的行的位置。.loc索引语句的第二个参数是:, 这意味着选择所有列。通过执行以下步骤,我们实际上是在过滤 DataFrame,以查看前三个重复 ID 的所有列。 -
在你的笔记本中运行以下命令,以执行我们在上一步中制定的计划:
df.loc[df['ID'].isin(dupe_ids[0:3]),:]![图 1.22:检查重复 ID 的数据]()
图 1.22:检查重复 ID 的数据
我们在这里观察到,每个重复的 ID 似乎都有一行看起来像有效数据的行,以及一行完全为零的行。花一点时间想一想,你会如何利用这些信息。
经过一番反思,应该很明显,你应该删除所有值为零的行。也许这些行是由于 SQL 查询中的错误连接条件生成的数据?无论如何,一行全为零的数据肯定是无效的,因为一个人的年龄为零,信用额度为零等,显然没有意义。
解决这个问题的一种方法是找到所有列为零的行,除了第一列(包含 ID)。这些行无论如何都是无效数据,可能如果我们删除这些行,就能解决重复 ID 的问题。我们可以通过创建一个与整个 DataFrame 大小相同的布尔矩阵,基于“是否等于零”这一条件,来找到 DataFrame 中等于零的条目。
-
使用
==创建一个与整个 DataFrame 大小相同的布尔矩阵,如下所示:df_zero_mask = df == 0在接下来的步骤中,我们将使用
df_zero_mask,它是另一个包含布尔值的 DataFrame。目标是创建一个布尔系列feature_zero_mask,标识出每一行,其中从第二列开始的所有元素(特征和响应,而不是 ID)都为 0。为此,我们首先需要使用整数索引(.iloc)方法对df_zero_mask进行索引。在此方法中,我们传递(:)来检查所有行,并传递(1:)来检查从第二列开始的所有列(索引1)。最后,我们将在列轴(axis=1)上应用all()方法,只有当该行的每一列都是True时,它才会返回True。这个过程需要思考,但编写代码其实很简单,正如接下来的步骤所示。目标是得到一个与 DataFrame 长度相同的系列,告诉我们哪些行除了 ID 外,所有值都是零。 -
创建布尔系列
feature_zero_mask,如以下代码所示:feature_zero_mask = df_zero_mask.iloc[:,1:].all(axis=1) -
使用以下命令计算布尔系列的总和:
sum(feature_zero_mask)你应该获得以下输出:
315上面的输出告诉我们,315 行除了第一列外每列都是零。这比重复 ID 的数量(313)还多,因此如果我们删除所有“零行”,可能就能解决重复 ID 的问题。
-
使用以下代码清理 DataFrame,删除所有除了 ID 之外的值全为零的行:
df_clean_1 = df.loc[~feature_zero_mask,:].copy()在前面的清理操作中,我们返回了一个新的 DataFrame,名为
df_clean_1。请注意,在这里我们在.loc索引操作后使用了.copy()方法来创建该输出的副本,而不是对原始 DataFrame 的视图。你可以把它当作创建一个新的 DataFrame,而不是引用原始的 DataFrame。在.loc方法中,我们使用了逻辑非运算符~来选择所有没有零值的特征和响应变量的行,使用:来选择所有列。这是我们希望保留的有效数据。做完这个后,我们现在希望知道剩余的行数是否等于唯一 ID 的数量。 -
通过运行以下代码,验证
df_clean_1的行数和列数:df_clean_1.shape你将得到以下输出:
(29685, 25) -
通过运行以下代码获取唯一 ID 的数量:
df_clean_1['ID'].nunique()这是输出:
29685从之前的输出中,我们可以看到我们成功地消除了重复项,因为唯一 ID 的数量等于行数。现在,深呼吸一下,拍拍自己背。这是对一些用于索引和表征数据的 pandas 技巧的快速介绍。现在,我们已经筛选出了重复的 ID,接下来可以开始查看实际的数据:特征,最终是响应变量。
完成这个练习后,按以下步骤将进度保存为 CSV(逗号分隔值)文件。请注意,在保存时我们不包括 DataFrame 的索引,因为这不是必需的,而且当我们稍后加载时可能会创建额外的列:
df_clean_1.to_csv('../../Data/df_clean_1.csv', index=False)
练习 1.05:探索和清理数据
到目前为止,我们已经识别出一个与元数据相关的数据质量问题:我们曾被告知数据集中的每个样本都对应一个唯一的账户 ID,但发现事实并非如此。我们能够利用逻辑索引和 pandas 来纠正这个问题。这是一个基本的数据质量问题,仅涉及基于元数据的样本存在情况。除此之外,我们对账户 ID 的元数据列并不感兴趣:这些列不会帮助我们开发信用违约的预测模型。
现在,我们准备开始检查特征和响应变量的值,这些数据将用于开发我们的预测模型。按照以下步骤完成这个练习:
注意
这个练习的 Jupyter notebook 可以在这里找到:packt.link/q0huQ。
-
加载上一个练习的结果,并通过使用
.info()方法获取数据中各列的数据类型,如下所示:import pandas as pd df_clean_1 = pd.read_csv('../../Data/df_clean_1.csv') df_clean_1.info()你应该看到以下输出:
![图 1.23:获取列元数据]()
图 1.23:获取列元数据
我们可以从图 1.23中看到,数据中有 25 列。每行旁边都有 29,685 个
int64,这表示它们是ID和PAY_1。我们已经熟悉了ID;它包含的是字符串,即账户 ID。那么PAY_1呢?根据数据字典,我们可以预期它包含的是整数,就像其他所有特征一样。我们来仔细看看这一列。 -
使用
.head(n)pandas 方法查看PAY_1列的前n行:df_clean_1['PAY_1'].head(5)你应该得到以下输出:
![图 1.24:检查几列的内容]()
图 1.24:检查几列的内容
输出结果左侧的整数是 DataFrame 索引,简单来说就是从 0 开始的连续整数。右侧显示的是
PAY_1列的数据。它本应是最近一个月账单的还款状态,使用的值有 -1、1、2、3 等等。然而,我们可以看到这里存在值 0,这在数据字典中没有说明。根据数据字典,“还款状态的测量尺度为:-1 = 按时还款;1 = 逾期一个月;2 = 逾期两个月;...;8 = 逾期八个月;9 = 逾期九个月及以上” (archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients)。我们来仔细看一下,使用该列的值计数。 -
使用
.value_counts()方法获取PAY_1列的值计数:df_clean_1['PAY_1'].value_counts()你应该看到以下输出:
![图 1.25: 列的值计数]()
图 1.25:
PAY_1列的值计数上述输出揭示了两个未记录的值:0 和 -2,并且解释了为什么 pandas 将该列导入为
object数据类型,而不是我们预期的int64(整数数据类型):因为该列中存在'Not available'字符串,表示缺失数据。在本书后续的章节中,我们会回到这一点,讨论如何处理缺失数据。目前,我们将删除数据集中包含缺失值的行。 -
使用
!=操作符(在 Python 中表示“不等于”)创建一个逻辑掩码,查找所有PAY_1特征没有缺失数据的行:valid_pay_1_mask = df_clean_1['PAY_1'] != 'Not available' valid_pay_1_mask[0:5]通过运行上面的代码,你将得到以下输出:
![图 1.26:创建布尔掩码]()
图 1.26:创建布尔掩码
-
通过计算掩码的和来检查有多少行没有缺失数据:
sum(valid_pay_1_mask)你将获得以下输出:
26664我们看到有 26,664 行
PAY_1列没有'Not available'这个值。从值计数中我们可以看到,有 3,021 行有这个值。这样合理吗?从图 1.23中我们知道数据集中有 29,685 条记录(行),29,685 – 3,021 = 26,664,因此这一结果是正确的。 -
清理数据,删除缺失值的
PAY_1行,如下所示:df_clean_2 = df_clean_1.loc[valid_pay_1_mask,:].copy() -
使用以下命令获取清理后数据的形状:
df_clean_2.shape你将得到以下输出:
(26664, 25)删除这些行后,我们检查结果 DataFrame 是否具有预期的形状。你还可以自己检查值计数是否表明所需的值已被删除,方法是:
df_clean_2['PAY_1'].value_counts()。最后,为了使这一列的数据类型与其他列一致,我们将其从通用的
object类型转换为int64类型,像所有其他特征一样,使用.astype方法。然后我们选择几列,包括PAY_1,检查数据类型,并确保转换成功。 -
运行以下命令,将
PAY_1的数据类型从object转换为int64,并使用列表选择多个列,显示PAY_1和PAY_2的列元数据:df_clean_2['PAY_1'] = df_clean_2['PAY_1'].astype('int64') df_clean_2[['PAY_1', 'PAY_2']].info()
这是你将得到的输出:

图 1.27:检查已清理列的数据类型
恭喜你,完成了第二次数据清理操作!但是,如果你还记得,在此过程中我们也注意到PAY_1中存在未记录的值-2 和 0。现在,假设我们再次与商业伙伴取得联系,并了解了以下信息:
-
-2 表示账户在该月初余额为零,并且从未使用过信用。
-
-1 表示账户的余额已全部还清。
-
0 表示至少支付了最低还款额,但并未偿还全部余额(即,存在正余额并转入下个月)。
我们感谢我们的商业伙伴,因为这回答了我们目前的问题。保持良好的沟通和合作关系非常重要,正如你所看到的,这可能决定一个项目的成败。
在你的笔记本中,像这样保存这次练习的进度:
df_clean_2.to_csv('../../Data/df_clean_2.csv', index=False)
数据质量保证与探索
到目前为止,我们通过提出一些基本问题或查看.info()摘要,已经解决了两个数据质量问题。接下来我们来看看前几列的数据。在查看历史账单支付记录之前,我们首先有LIMIT_BAL账户的信用额度,还有SEX、EDUCATION、MARRIAGE和AGE这些人口统计特征。我们的商业伙伴已经联系了我们,告诉我们性别不应被用来预测信用状况,因为按照他们的标准,这是不道德的。因此我们会在今后的工作中考虑这一点。现在我们将继续检查其余列,并进行必要的更正。
为了进一步探索数据,我们将使用直方图。直方图是一种很好的方法,可以可视化那些连续值的数据,如货币金额和年龄。直方图将相似的值分组到不同的箱子中,并以条形图的方式显示这些箱子中的数据点数量。
为了绘制直方图,我们将开始熟悉 pandas 的图形功能。pandas 依赖于另一个名为matplotlib的库。使用这些工具,我们还将学习如何快速获取 pandas 中数据的统计摘要。
练习 1.06:探索信用额度和人口统计特征
在这个练习中,我们将开始探索数据中的信用额度和年龄特征。我们将可视化它们并获取统计摘要,以检查这些特征中的数据是否合理。然后,我们将查看教育和婚姻等分类特征,看看这些值是否合理,必要时进行修正。LIMIT_BAL和AGE是数值型特征,意味着它们是在一个连续的尺度上进行测量的。因此,我们将使用直方图来可视化它们。按照以下步骤完成练习:
注意
本练习的 Jupyter 笔记本可以在这里找到:packt.link/PRdtP。
-
除了 pandas,还需要导入
matplotlib并使用以下代码片段设置一些绘图选项。注意 Python 中的注释用法,注释以#开头。任何出现在#后面的内容都会被 Python 解释器忽略:import pandas as pd import matplotlib.pyplot as plt #import plotting package #render plotting automatically %matplotlib inline import matplotlib as mpl #additional plotting functionality mpl.rcParams['figure.dpi'] = 400 #high resolution figures这段代码导入了
matplotlib并使用.rcParams设置了分辨率(dpi= 每英寸点数),以便得到清晰的图像;除非你准备展示这些内容,否则不需要担心最后这部分,因为它可能会使图片在笔记本中变得非常大。 -
使用以下代码加载我们上一个练习的进度:
df_clean_2 = pd.read_csv('../Data/df_clean_2.csv'), -
运行
df_clean_2[['LIMIT_BAL', 'AGE']].hist(),你应该能看到以下直方图:![图 1.28:信用额度和年龄数据的直方图]()
图 1.28:信用额度和年龄数据的直方图
这是这些特征的一个不错的视觉快照。我们可以通过这种方式快速大致地查看所有数据。为了查看均值和中位数(即第 50 百分位数)等统计信息,还有另一个有用的 pandas 函数。
-
使用以下命令生成汇总统计的表格报告:
df_clean_2[['LIMIT_BAL', 'AGE']].describe()你应该看到以下输出:
![图 1.29:信用额度和年龄数据的统计摘要]()
图 1.29:信用额度和年龄数据的统计摘要
基于直方图和通过
.describe()计算的便捷统计数据,其中包括非空值的计数、均值和标准差、最小值、最大值以及四分位数,我们可以做出一些判断。LIMIT_BAL(信用额度)看起来是合理的。信用额度的最小值为 10,000。该数据集来自台湾,具体的货币单位(新台币)可能不太熟悉,但直观上,信用额度应该大于零。我们建议你查找与本地货币的兑换汇率并考虑这些信用额度。例如,1 美元大约等于 30 新台币。AGE特征看起来也分布得比较合理,且 21 岁以下的人群没有信用账户。对于分类特征,查看值计数是有用的,因为唯一值相对较少。
-
使用以下代码获取
EDUCATION特征的值计数:df_clean_2['EDUCATION'].value_counts()你应该看到以下输出:
![图 1.30:EDUCATION 特征的值计数]()
图 1.30:EDUCATION 特征的值计数
在这里,我们看到未记录的教育水平 0、5 和 6,因为数据字典只描述了
教育(1 = 研究生;2 = 大学;3 = 高中;4 = 其他)。我们的业务合作伙伴告诉我们他们不知道其他教育水平。由于它们不太常见,我们将它们归类为其他类别,这似乎是合适的。 -
运行此代码将
EDUCATION特征中未记录的级别合并到其他级别中,然后检查结果:df_clean_2['EDUCATION'].replace(to_replace=[0, 5, 6],\ value=4, inplace=True) df_clean_2['EDUCATION'].value_counts()pandas 的
.replace方法使得执行上述替换操作非常快速。运行代码后,你应该会看到以下输出:![图 1.31:清理 EDUCATION 特征]()
图 1.31:清理 EDUCATION 特征
请注意,这里我们使用了
inplace=True参数。这意味着,操作将直接修改现有的 DataFrame,而不是返回一个新的 DataFrame。 -
使用以下代码获取
MARRIAGE特征的值计数:df_clean_2['MARRIAGE'].value_counts()你应该获得以下输出:
![图 1.32:原始 MARRIAGE 特征的值计数]()
图 1.32:原始 MARRIAGE 特征的值计数
这里的问题与
EDUCATION特征遇到的问题类似;有一个值 0,在数据字典中没有记录:1 = 已婚;2 = 单身;3 = 其他。因此,我们将其归类为其他。 -
使用以下代码将
MARRIAGE特征中的 0 值改为 3,并检查结果:df_clean_2['MARRIAGE'].replace(to_replace=0, value=3, \ inplace=True) df_clean_2['MARRIAGE'].value_counts()输出应该如下所示:
![图 1.33:清理后的 MARRIAGE 特征的值计数]()
图 1.33:清理后的 MARRIAGE 特征的值计数
我们现在已经完成了大量数据的探索和清理。接下来,我们将在 DataFrame 中对其后的财务历史特征进行更高级的可视化和探索。首先,我们将考虑EDUCATION特征的含义,这是数据集中的一个分类特征。
按照以下方式保存此练习的进度:
df_clean_2.to_csv('../../Data/df_clean_2_01.csv', index=False)
深度分析:分类特征
机器学习算法只处理数字。如果你的数据包含文本特征,例如,这些特征需要以某种方式转化为数字。我们上面了解到,我们的案例研究的数据实际上完全是数字化的。然而,值得思考的是它是如何变成这样的。特别是,考虑一下EDUCATION特征。
这是一个例子,说明什么是研究生院、大学、高中和其他。这些被称为分类特征的等级;这里有四个等级。正是通过已经为我们选择的映射,数据才在我们的数据集中以 1、2、3 和 4 的数字形式存在。这个将类别映射到数字的特定分配创建了所谓的有序特征,因为这些等级按顺序映射到数字。作为数据科学家,至少你需要意识到这样的映射,除非你自己选择这些映射。
这种映射有什么影响?
教育水平按等级排列是有一定道理的,1 对应我们数据集中最高的教育水平,2 对应次高水平,3 对应再高水平,4 可能包括最低水平。然而,当你将这种编码作为机器学习模型中的数值特征时,它会像处理任何其他数值特征一样被对待。对于某些模型,这种效果可能并不希望出现。
如果一个模型试图找到特征与响应之间的直线关系,会怎样呢?
这个问题可能看起来有些随意,尽管在书的后面你会了解区分线性模型和非线性模型的重要性。在本节中,我们将简要介绍一些模型确实会寻找特征与响应变量之间的线性关系。是否能够在教育特征的情况下起作用,取决于不同教育水平与我们试图预测的结果之间的实际关系。
在这里,我们考察了两个假设的合成数据案例,每个案例都包含 10 个等级的有序分类变量。这些等级衡量的是访问网站的客户自我报告的满意度。每个等级的客户在网站上停留的平均分钟数绘制在 y 轴上。我们还在每种情况下绘制了最佳拟合线,以说明线性模型如何处理这些数据,如下图所示:

图 1.34:有序特征在线性模型中可能有效,也可能无效
我们可以看到,如果一个算法假设特征与响应变量之间存在线性(直线)关系,这可能根据真实关系的不同效果好坏不一。注意,在这个合成示例中,我们正在建模一个回归问题:响应变量采用连续的数字范围。虽然我们的案例研究涉及分类问题,但一些分类算法,如逻辑回归,也假设特征的线性效应。我们将在稍后更详细地讨论这个问题,当我们进入为案例研究建模的数据时。
大致而言,对于二分类问题,即响应变量只有两个结果,我们假设其编码为 0 和 1,您可以通过每个类别特征在每个水平内响应变量的平均值来查看类别特征的不同水平。这些平均值表示每个水平的正类“比率”(即响应变量=1 的样本)。这可以让您了解顺序编码是否适合与线性模型配合使用。假设您在 Jupyter 笔记本中导入了与前面章节相同的包,您可以通过groupby/agg聚合过程以及 pandas 中的条形图快速查看这一点。
这将根据EDUCATION特征中的值对数据进行分组,然后在每个组内通过default payment next month响应变量的平均值进行聚合:
df_clean_2 = pd.read_csv('../../Data/df_clean_2_01.csv')
df_clean_2.groupby('EDUCATION').agg({'default payment next '\
'month':'mean'})\
.plot.bar(legend=False)
plt.ylabel('Default rate')
plt.xlabel('Education level: ordinal encoding')
运行代码后,您应该会得到以下输出:

图 1.35:不同教育水平的违约率
与图 1.34 中的示例 2类似,这里的数据似乎不太适合用直线拟合来描述。如果某个特征具有类似的非线性效应,可能更适合使用更复杂的算法,例如决策树或随机森林。或者,如果需要更简单且更具可解释性的线性模型(如逻辑回归),我们可以避免使用顺序编码,而采用不同的类别变量编码方式。一种常见的方式叫做独热编码(OHE)。
OHE 是一种将类别特征(可能由原始数据中的文本标签组成)转换为数值特征的方法,以便在数学模型中使用。
让我们在一个练习中学习这个。如果你在想为什么逻辑回归更具可解释性,而随机森林更复杂,我们将在后续章节详细学习这些概念。
练习 1.07:为类别特征实现 OHE
在本次练习中,我们将“逆向工程”数据集中的EDUCATION特征,以获取表示不同教育水平的文本标签,然后展示如何使用 pandas 创建 OHE。作为初步步骤,请设置环境并加载之前练习的进度:
import pandas as pd
import matplotlib as mpl #additional plotting functionality
mpl.rcParams['figure.dpi'] = 400 #high resolution figures
df_clean_2 = pd.read_csv('../../Data/df_clean_2_01.csv')
首先,让我们考虑EDUCATION特征在编码为顺序之前的样子。从数据字典中我们知道,1 = 研究生,2 = 大学,3 = 高中,4 = 其他。我们希望重新创建一个包含这些字符串的列,而不是数字。执行以下步骤完成练习:
注意
本次练习的 Jupyter 笔记本可以在这里找到:packt.link/akAYJ。
-
为类别标签创建一个空列,命名为
EDUCATION_CAT。使用以下命令,每一行将包含字符串'none':df_clean_2['EDUCATION_CAT'] = 'none' -
检查
EDUCATION和EDUCATION_CAT列的 DataFrame 的前几行:df_clean_2[['EDUCATION', 'EDUCATION_CAT']].head(10)输出应如下所示:
![图 1.36:选择列并查看前 10 行]()
图 1.36:选择列并查看前 10 行
我们需要用适当的字符串填充这个新列。pandas 提供了一个方便的功能,可以将一个 Series 的所有值映射到新的值。这个函数实际上叫
.map,并依赖于一个字典来建立旧值和新值之间的对应关系。我们的目标是将EDUCATION中的数字映射到它们所代表的字符串。例如,当EDUCATION列的值为 1 时,我们将把'研究生'字符串赋值给EDUCATION_CAT列,其他教育水平也是如此。 -
使用以下代码创建描述教育类别映射的字典:
cat_mapping = {1: "graduate school",\ 2: "university",\ 3: "high school",\ 4: "others"} -
使用
.map将映射应用到原始的EDUCATION列,并将结果赋值给新的EDUCATION_CAT列:df_clean_2['EDUCATION_CAT'] = df_clean_2['EDUCATION']\ .map(cat_mapping) df_clean_2[['EDUCATION', 'EDUCATION_CAT']].head(10)运行这些代码后,你应该看到以下输出:
![图 1.37:检查对应于序数编码的字符串值 EDUCATION 的编码]()
图 1.37:检查对应于 EDUCATION 的序数编码的字符串值
很好!请注意,我们本可以跳过步骤 1,直接通过步骤 3和4创建新列,而不需要先将新列赋值为
'none'。然而,有时候创建一个初始化为单一值的新列是有用的,因此了解如何做到这一点是值得的。现在我们准备进行一热编码。我们可以通过将一个
DataFrame的 Series 传递给 pandas 的get_dummies()函数来实现。该函数得名于一热编码列也被称为虚拟变量。结果将是一个新的 DataFrame,包含与类别变量的级别数相等的列。 -
运行此代码以创建
EDUCATION_CAT列的一热编码 DataFrame。查看前 10 行:edu_ohe = pd.get_dummies(df_clean_2['EDUCATION_CAT']) edu_ohe.head(10)这应该产生以下输出:
![图 1.38:一热编码的 DataFrame]()
图 1.38:一热编码的 DataFrame
你现在可以理解为什么这叫做“独热编码”:在所有这些列中,任何一行都会在恰好一列中为 1,其余列为 0。对于给定的一行,含有 1 的列应该与原始分类变量的水平相匹配。为了验证这一点,我们需要将这个新的 DataFrame 与原始 DataFrame 进行合并,并并排查看结果。我们将使用 pandas 的
concat函数,传入我们希望合并的 DataFrame 列表,并使用axis=1参数表示水平合并;也就是说,沿着列轴合并。这基本上意味着我们将这两个 DataFrame “并排”组合在一起,我们知道我们可以这样做,因为我们刚刚从原始 DataFrame 创建了这个新 DataFrame:我们知道它将有相同数量的行,且行的顺序与原始 DataFrame 一致。 -
如下所示,将独热编码后的 DataFrame 合并到原始 DataFrame 中:
df_with_ohe = pd.concat([df_clean_2, edu_ohe], axis=1) df_with_ohe[['EDUCATION_CAT', 'graduate school',\ 'high school', 'university', 'others']].head(10)你应该会看到以下输出:
![图 1.39:检查独热编码列]()
图 1.39:检查独热编码列
好的,看起来这个方法如预期一样有效。OHE 是另一种编码分类特征的方法,它避免了顺序编码中隐含的数值结构。然而,请注意这里发生了什么:我们将单一列 EDUCATION 拓展成了与特征水平数量相同的多列。在这种情况下,由于只有四个水平,因此问题不大。但如果你的分类变量有非常多的水平,你可能需要考虑使用其他策略,比如将某些水平合并为一个类别。
现在是时候保存我们创建的 DataFrame 了,它包含了我们清洗数据并添加 OHE 列的成果。
将最新的 DataFrame 写入文件,如下所示:df_with_ohe.to_csv('../../Data/Chapter_1_cleaned_data.csv', index=False)。
探索数据集中的财务历史特征
我们已经准备好探索案例研究数据集中的其余特征。首先设置环境并加载上一个练习中的数据。可以使用以下代码片段来实现:
import pandas as pd
import matplotlib.pyplot as plt #import plotting package
#render plotting automatically
%matplotlib inline
import matplotlib as mpl #additional plotting functionality
mpl.rcParams['figure.dpi'] = 400 #high resolution figures
import numpy as np
df = pd.read_csv('../../Data/Chapter_1_cleaned_data.csv')
注意
你的 CSV 文件的路径可能会有所不同,具体取决于你保存的路径。
需要检查的其余特征是财务历史特征。它们自然分为三组:过去 6 个月的月度付款状态,以及同一时期的账单和已付款金额。首先,让我们来看一下付款状态。将这些特征拆分成一个列表,以便我们可以一起研究它们,比较方便。你可以使用以下代码来实现:
pay_feats = ['PAY_1', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', \
'PAY_6']
我们可以使用 .describe 方法对这六个 Series 进行汇总统计分析:
df[pay_feats].describe()
这将产生以下输出:

图 1.40:付款状态特征的摘要统计
在这里,我们观察到所有这些特征的值范围都是相同的:-2,-1,0,... 8。看起来,数据字典中描述的值为 9,即九个月及以上的付款延迟,从未出现过。
我们已经澄清了所有这些级别的含义,其中一些并不在原始数据字典中。现在让我们再次查看PAY_1的value_counts(),现在按我们正在计数的值进行排序,这些值是该 Series 的index:
df[pay_feats[0]].value_counts().sort_index()
这应该产生以下输出:

图 1.41:上个月付款状态的值计数
与正整数值相比,大多数值要么是-2,-1,要么是 0,这对应于上个月处于良好状态的帐户:未使用,全额支付,或至少支付了最低还款额。
请注意,由于此变量的其他值的定义(1 = 一个月的付款延迟;2 = 两个月的付款延迟,依此类推),此特征在分类和数值特征之间有点混合。为什么没有信用使用对应于-2 的值,而值为 2 表示 2 个月的延迟付款,依此类推?我们应该意识到,付款状态的数值编码-2,-1 和 0 构成了数据集创建者对如何对某些分类特征进行编码的决定,然后将其与一个真正数值的特征混合在一起:付款延迟的月数(值为 1 及以上)。稍后,我们将考虑这种做法对该特征的预测能力的潜在影响。
现在,我们将继续探索数据。这个数据集足够小,有 18 个这些财务特征和少数其他特征,我们可以负担得起逐个检查每个特征。如果数据集有数千个特征,我们可能会放弃这一点,而是探索df[pay_feats[0]].hist(),以产生这个:

图 1.42:使用默认参数绘制的 PAY_1 直方图
现在我们将深入研究如何生成这个图形,并考虑它是否如此信息丰富。关于 pandas 的图形功能的一个关键点是.hist()方法是**kwds,文档指出这些是matplotlib关键字参数。
注意
欲了解更多信息,请参考以下链接:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.hist.html。
查阅matplotlib文档中的matplotlib.pyplot.hist可以看到更多可以与 pandas 的.hist()方法一起使用的附加参数,比如绘制的直方图类型(更多详情请参见matplotlib.org/api/_as_gen/matplotlib.pyplot.hist.html)。通常,为了获得更详细的绘图功能,了解matplotlib是非常重要的,在某些场景下,你可能希望直接使用matplotlib而不是 pandas,以便更好地控制图形外观。
你应该意识到,pandas 使用了matplotlib,而matplotlib又使用了 NumPy。在使用matplotlib绘制直方图时,实际上生成直方图数值的计算是由 NumPy 的.histogram函数来执行的。这是代码复用的一个关键示例,或称“不要重复造轮子”。如果像绘制直方图这样的标准功能已经在 Python 中有了很好的实现,那么就没有理由重新创建它。而且,如果绘制直方图所需的数学计算已经实现,我们也应该利用它。这展示了 Python 生态系统的相互关联性。
现在我们将讨论计算和绘制直方图时出现的几个关键问题。
箱体数量
直方图通过将值分组到所谓的PAY_1特征中,那里有 11 个唯一的值。在这种情况下,最好手动将直方图的箱体数设置为唯一值的数量。
在当前的示例中,由于PAY_1的高箱体值非常少,图表可能看起来没有太大变化。但通常来说,绘制直方图时要牢记这一点。
箱体边缘
箱体边缘的位置决定了值在直方图中的分组方式。你可以选择不向绘图函数指定箱体数量,而是为bins关键字参数提供一个数字列表或数组。该输入将被解释为 x 轴上的箱体边缘位置。理解matplotlib如何使用这些边缘位置将值分组到箱体中是很重要的。除了最后一个箱体外,所有箱体都会将值从左边缘开始,包括左边缘,但不包括右边缘,换句话说,左边缘是闭合的,右边缘是开放的。然而,最后一个箱体则包括了两个边缘,它的左右边缘都是闭合的。当你将相对较少的唯一值分配到箱体边缘时,这一点特别重要。
为了更好地控制图形外观,通常最好指定箱体的边缘位置。我们将创建一个包含 12 个数字的数组,这将生成 11 个箱体,每个箱体都围绕PAY_1的一个唯一值进行中心对齐:
pay_1_bins = np.array(range(-2,10)) - 0.5
pay_1_bins
输出显示了箱体的边缘位置:
array([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5,\
3.5,4.5, 5.5, 6.5, 7.5,8.5])
最后一个风格点是,始终标注你的图表,使其具有可解释性。我们还没有手动标注,因为在某些情况下,pandas 会自动完成,其他情况下我们只是让图表保持无标签。从现在开始,我们将遵循最佳实践并标注所有图表。我们使用 matplotlib 中的 xlabel 和 ylabel 函数为此图表添加轴标签。代码如下:
df[pay_feats[0]].hist(bins=pay_1_bins)
plt.xlabel('PAY_1')
plt.ylabel('Number of accounts')
输出应该如下所示:

图 1.43:改进后的 PAY_1 直方图
图 1.43 展示了改进后的直方图,因为条形图已对齐实际数据值,每个唯一值对应一个条形图。虽然仅使用默认参数调用绘图函数很有吸引力,并且通常足够,但作为数据科学家的职责之一是创建准确且具有代表性的数据可视化图表。为此,有时你需要深入了解绘图代码的细节,就像我们在这里做的那样。
我们从这次数据可视化中学到了什么?
由于我们已经查看了值的计数,这进一步确认了大多数账户处于良好状态(值为 -2、-1 和 0)。对于那些没有处于良好状态的账户,"延迟月份" 较小的情况更为常见。这是有道理的;很可能大多数人会在不久后支付完余额。否则,他们的账户可能会被关闭或转交给收款公司。检查特征的分布并确保其合理性是与客户确认的好方法,因为数据的质量直接影响到你所进行的预测建模。
既然我们已经为直方图建立了一些良好的绘图风格,让我们使用 pandas 一起绘制多个直方图,并可视化最近 6 个月的还款状态特征。我们可以将包含列名的列表 pay_feats 传递给 .hist() 方法,指定我们已确定的箱子边界,并表示我们希望绘制 2 行 3 列的子图。首先,我们将字体大小设置得足够小,以适应这些子图之间的间距。以下是相关代码:
mpl.rcParams['font.size'] = 4
df[pay_feats].hist(bins=pay_1_bins, layout=(2,3))
绘图标题已经根据列名自动生成。y 轴表示计数。生成的可视化结果如下:

图 1.44:直方图子图的网格
我们已经看过了第一个图表,这很有意义。那么其余的呢?请记住这些特征的正整数值定义及其含义。例如,PAY_2 是 8 月的还款状态,PAY_3 是 7 月的还款状态,其他则追溯得更久。值为 1 表示延迟支付 1 个月,值为 2 表示延迟支付 2 个月,依此类推。
你有没有注意到似乎有什么不对劲?看看 7 月(PAY_3)和 8 月(PAY_2)之间的值。7 月,支付延迟 1 个月的账户非常少;在直方图中几乎看不见这一条。然而,到了 8 月,突然出现了数千个支付延迟 2 个月的账户。这不合逻辑:在一个月内,2 个月延迟的账户数量应该小于或等于前一个月支付延迟 1 个月的账户数量。
让我们仔细看看 8 月有 2 个月延迟的账户,查看它们在 7 月的支付状态。我们可以使用以下代码,通过布尔掩码和.loc来实现,代码示例如下:
df.loc[df['PAY_2']==2, ['PAY_2', 'PAY_3']].head()
输出结果应该如下所示:
![图 1.45:8 月有 2 个月支付延迟的账户在 7 月的支付状态(PAY_3)
延迟支付状态(PAY_2)]
](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_01_45.jpg)
图 1.45:8 月有 2 个月支付延迟的账户在 7 月的支付状态(PAY_3)(PAY_2)
从图 1.45可以看出,8 月有 2 个月延迟的账户在 7 月的支付状态值是无意义的。实现 2 个月延迟的唯一途径应该是从前一个月的 1 个月延迟开始,但这些账户都没有显示这一点。
当你在数据中看到类似的情况时,你需要检查用来创建数据集的查询逻辑,或者联系提供数据集的人。在仔细检查这些结果之后,比如使用.value_counts()直接查看数字,我们联系了客户以询问这个问题。
客户告诉我们,他们在获取最新月份数据时遇到了问题,这导致了支付延迟 1 个月的账户报告错误。在 9 月,他们大部分修复了这些问题(尽管没有完全修复,这也是我们发现PAY_1特征中存在缺失值的原因)。因此,在我们的数据集中,除了 9 月(PAY_1特征)之外,所有月份中的 1 的值都被低估了。从理论上讲,客户可以创建查询来回溯他们的数据库,并确定PAY_2、PAY_3等的正确值。然而,出于实际原因,他们无法在我们需要的时候完成这一回溯分析并将结果纳入我们的项目中。
因此,我们的支付状态数据中只有最新月份是正确的。这意味着,在所有支付状态特征中,只有PAY_1能够代表未来数据,即将用于我们开发的模型进行预测的数据。这是一个关键点:预测模型依赖于获取与其构建时相同类型的数据来进行预测。这意味着我们可以将PAY_1作为模型中的特征,但不能使用PAY_2或来自前几个月的其他支付状态特征。
本章节展示了对数据质量进行彻底检查的重要性。只有通过仔细地梳理数据,我们才发现了这个问题。如果客户能提前告知我们,在数据集收集的那段时间里,他们在报告过程中遇到过问题,并且报告过程在那段时间内并不一致,那就好了。然而,最终建立一个可信的模型是我们的责任,因此我们需要通过这种详细的探索,确保我们相信数据是正确的。我们向客户解释,由于旧的特征不代表模型将在其上进行评分的未来数据(即预测未来几个月的数据),因此无法使用这些旧特征,并要求他们告知我们他们所知的任何进一步的数据问题。目前没有。
活动 1.01:探索数据集中的剩余财务特征
在本活动中,您将以类似于我们检查 PAY_1、PAY_2、PAY_3 等特征的方式检查剩余的财务特征。为了更好地可视化这些数据,我们将使用一个大家应该熟悉的数学函数:对数。您将使用 pandas 的 apply 方法,这个方法可以将任何函数应用到整个列或 DataFrame。在完成活动后,您应该得到以下一组非零支付对数变换的直方图:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_01_46.jpg)
图 1.46:预期的直方图集
执行以下步骤以完成活动:
在开始之前,设置您的环境并按以下方式加载清理过的数据集:
import pandas as pd
import matplotlib.pyplot as plt #import plotting package
#render plotting automatically
%matplotlib inline
import matplotlib as mpl #additional plotting functionality
mpl.rcParams['figure.dpi'] = 400 #high resolution figures
mpl.rcParams['font.size'] = 4 #font size for figures
from scipy import stats
import numpy as np
df = pd.read_csv('../../Data/Chapter_1_cleaned_data.csv')
-
创建剩余财务特征的特征名称列表。
-
使用
.describe()检查账单金额特征的统计摘要。反思一下你看到的内容。这合理吗? -
使用 2x3 网格的直方图绘制账单金额特征。
提示:您可以为此可视化使用 20 个区间。
-
获取支付金额特征的
.describe()摘要。这合理吗? -
绘制类似于账单金额特征的账单支付特征的直方图,但也使用
xrot关键字参数对 x 轴标签进行旋转,以避免重叠。在任何绘图函数中,您都可以使用xrot=<角度>关键字参数,将 x 轴标签按给定角度(以度为单位)旋转。考虑一下结果。 -
使用布尔掩码查看有多少支付金额数据正好等于 0。考虑一下在前一步的直方图中,这是否合理?
-
使用您在前一步创建的掩码忽略 0 的支付,使用 pandas 的
.apply()和 NumPy 的np.log10()对非零支付的对数变换进行直方图绘制。考虑一下结果。提示:您可以使用
.apply()将任何函数,包括log10,应用到 DataFrame 或列的所有元素,语法如下:.apply(<函数名称>)。注意
包含本次活动的 Python 代码及对应输出的 Jupyter notebook 可以在这里找到:
packt.link/FQQOB。本次活动的详细逐步解决方案可以通过此链接找到。
总结
在本章的介绍部分,我们广泛使用了 pandas 来加载和探索案例研究数据。我们学习了如何通过结合统计摘要和可视化来检查基本的一致性和正确性。我们回答了诸如“唯一的账户 ID 真的唯一吗?”,“是否有缺失数据已被填充?”以及“特征的值是否符合其定义?”等问题。
你可能注意到,我们几乎将本章的所有时间都花在了识别和修正数据集的问题上。这通常是数据科学项目中最耗时的阶段。虽然这未必是最激动人心的部分,但它为你提供了构建激动人心的模型和洞察所需的原材料。这些将成为本书其余部分的大部分内容。
软件工具和数学概念的掌握使你能够在技术层面执行数据科学项目。然而,管理与客户的关系同样重要,因为客户依赖你的服务从数据中提取洞察。你必须尽可能多地利用业务伙伴对数据的理解。除非你已经是该领域的专家,否则他们对数据可能比你更熟悉。然而,即便如此,你的第一步应该是对所使用的数据进行彻底且批判性的审查。
在我们的数据探索过程中,我们发现了一个可能会破坏我们项目的问题:我们收到的数据在内部并不一致。大多数支付状态特征的月份存在数据报告问题,包括不合逻辑的值,而且这些数据并不是最新一个月的数据,也不是未来模型可能使用的数据。我们只有通过仔细查看所有特征才发现了这个问题。虽然这并非总是可能的,特别是当特征非常多时,但你应该始终抽时间检查尽可能多的特征。如果无法检查每个特征,那么在特征有类别时,比如财务或人口统计特征,检查每类中的几个特征会很有用。
在与客户讨论此类数据问题时,请确保保持尊重和专业。客户在向你提供数据时,可能只是忘记了这个问题。或者,他们可能知道这个问题,但出于某种原因认为它不会影响你的分析。无论如何,你通过提醒客户并解释为何使用有缺陷的数据建立模型会成为问题,实际上是在为他们提供一项重要的服务。尽量具体说明,展示你用来发现问题的图表和表格类型。
在下一章,我们将检查我们的案例研究问题中的响应变量,这将完成初步的数据探索。然后我们将开始接触机器学习模型,学习如何判断一个模型是否有用。当我们开始使用案例研究数据建立模型时,这些技能将变得非常重要。
第二章:2. Scikit-Learn 简介与模型评估
概述
在探索了案例研究数据的响应变量后,本章通过简单的逻辑回归和线性回归使用案例,介绍了 scikit-learn 在训练模型和进行预测方面的核心功能。我们将展示二分类模型的评估指标,包括真阳性率和假阳性率、混淆矩阵、受试者工作特征(ROC)曲线以及精准率-召回率曲线,既通过从头开始实现,也通过便捷的 scikit-learn 功能来演示。到本章结束时,你将能够使用 scikit-learn 构建和评估二分类模型。
介绍
在上一章中,你已经熟悉了基本的 Python,并学习了用于数据探索的 pandas 工具。通过使用 Python 和 pandas,你执行了如加载数据集、验证数据完整性以及对数据中的特征(即自变量)进行探索性分析等操作。
在本章中,我们将通过检查响应变量来完成数据的探索。在我们得出数据质量高且合理的结论后,就可以开始开发机器学习模型了。我们将以 scikit-learn 作为起步,scikit-learn 是 Python 语言中最流行的机器学习库之一。在下一章学习数学模型的具体细节之前,本章将让我们熟悉在 scikit-learn 中使用这些模型的语法。
我们还将学习一些常用技术,用来回答“这个模型好不好?”这个问题。模型评估有许多不同的方式。对于商业应用来说,进行财务分析以确定模型可能带来的价值,是了解工作潜在影响的重要方式。通常,最好在项目一开始就界定商业机会。然而,由于本书的重点是机器学习和预测建模,我们将在最后一章展示财务分析。
有几个重要的模型评估标准被视为数据科学和机器学习中的基本知识。我们将在这里介绍一些最广泛使用的分类模型性能指标。
探索响应变量并完成初步探索
我们现在已经检查过所有的特征,看看是否有缺失数据,并且对它们进行了一般性检查。特征很重要,因为它们构成了我们机器学习算法的输入。在模型的另一端是输出,即对响应变量的预测。对于我们的问题来说,这是一个二元标志,指示信用账户下个月是否会违约。
案例研究项目的关键任务是为该目标提出预测模型。由于响应变量是一个是/否标志,因此此问题被称为“下个月是否违约”的问题('default payment next month' = 1),属于正类,而未违约的属于负类。
关于二元分类问题响应的主要信息是:正类的比例是多少?这是一个简单的检查。
在执行此检查之前,我们使用以下代码加载所需的软件包:
import numpy as np #numerical computation
import pandas as pd #data wrangling
import matplotlib.pyplot as plt #plotting package
#Next line helps with rendering plots
%matplotlib inline
import matplotlib as mpl #add'l plotting functionality
mpl.rcParams['figure.dpi'] = 400 #high res figures
现在,我们像这样加载案例研究数据的清理版本:
df = pd.read_csv('../../Data/Chapter_1_cleaned_data.csv')
注意
清理后的数据集应该已经保存在第一章,数据探索和清理中的工作结果中。如果您将其保存在不同的位置,则前面代码片段中的清理数据的路径可能会有所不同。
现在,要找出正类的比例,我们只需得到整个数据集上响应变量的平均值。这被解释为违约率。此外,使用pandas中的groupby和count来检查每个类别的样本数量是值得的。如下屏幕截图所示:

图 2.1:响应变量的类别平衡
由于目标变量为1或0,取这一列的均值表示违约账户的比例为 22%。正类(违约 = 1)的样本比例,也称为此类别的类别分数,是一个重要的统计量。在二元分类中,数据集通常描述为平衡或不平衡:正类和负类的比例是否相等?大多数机器学习分类模型都设计用于处理平衡数据:类别之间的 50/50 分布。
然而,在实际应用中,真实数据很少是平衡的。因此,有几种方法专门用于处理不平衡数据。这些包括以下方法:
-
过采样多数类别:随机丢弃多数类别的样本,直到类别分布相等,或至少更少不平衡。
-
过采样少数类别:随机添加少数类别的重复样本,以达到相同的目标。
-
加权样本:此方法作为训练步骤的一部分执行,因此少数类在训练模型中具有与多数类相同的“重视”。其效果类似于过采样。
-
更复杂的方法,如合成少数类过采样技术(SMOTE)。
虽然我们的数据在严格意义上不是平衡的,但我们也注意到,22%的正类比例并不是特别不平衡。在一些领域,比如欺诈检测,通常处理的正类比例更小,通常在 1%或更少。这是因为“坏演员”的比例相对于交易总量非常小;与此同时,如果可能的话,能够识别它们是很重要的。对于这类问题,使用方法来处理类别不平衡更可能带来显著更好的结果。
现在我们已经探索了响应变量,初步的数据探索也已经完成。然而,数据探索应该被视为一个持续的任务,应该在任何项目中时刻考虑。当你创建模型并生成新结果时,始终思考这些结果对数据意味着什么,这通常需要快速回到探索阶段进行迭代。一个特别有用的探索方法,通常也是在构建模型之前进行的,是检查特征与响应变量之间的关系。我们在第一章《数据探索与清洗》中已经展示了这一点,当时我们按照EDUCATION特征进行分组,并检查了响应变量的均值。我们以后还会做更多这样的工作。不过,这更涉及到构建模型,而不是检查数据的内在质量。
我们刚刚完成的对所有数据的初步浏览是项目开始时需要打下的重要基础。在此过程中,你应该问自己以下问题:
-
数据是否完整?
是否存在缺失值或其他异常情况?
-
数据是否一致?
数据分布是否随时间变化,如果是,是否可以预期?
-
数据是否合理?
特征的值是否符合数据字典中的定义?
后两个问题有助于你判断数据是否正确。如果这些问题的答案是“否”,那么在继续项目之前应该解决这些问题。
此外,如果你想到任何可能有帮助的额外数据,并且可以获取,现在是项目生命周期中一个很好的时机将其加入到数据集中。例如,如果你有与账户相关的地址数据,可以将邮政编码级别的人口统计数据加入到数据集中。我们在案例研究数据中没有这些数据,因此决定在现有数据的基础上继续进行该项目。
Scikit-Learn 简介
虽然 pandas 可以节省你大量加载、检查和清理数据的时间,但使你能够进行预测建模的机器学习算法位于其他包中。Scikit-learn 是一个基础的 Python 机器学习包,包含许多有用的算法,并且也影响了其他 Python 机器学习库的设计和语法。因此,我们将重点学习 scikit-learn,以培养预测建模的实践技能。虽然没有任何一个包能够提供所有功能,但就适配分类、回归和无监督学习的经典方法而言,scikit-learn 已经做得相当接近了。然而,它对一些较新的进展(如深度学习)并没有太多功能。
这里有几个其他相关的包,你应该了解:
SciPy:
-
到目前为止,我们使用的大多数包,如 NumPy 和 pandas,实际上都是 SciPy 生态系统的一部分。
-
SciPy 提供轻量级函数,支持经典方法,如线性回归和线性规划。
StatsModels:
-
更偏向于统计学,可能对于熟悉 R 的用户更为舒适
-
可以获取回归系数的 p 值和置信区间
-
时间序列模型的能力,如 ARIMA
XGBoost 和 LightGBM:
- 提供一套先进的集成模型,这些模型通常比随机森林表现更好。我们将在第六章,梯度提升、SHAP 值和处理缺失数据中学习 XGBoost。
TensorFlow, Keras, 和 PyTorch:
- 深度学习能力
还有许多其他的 Python 包可能会派上用场,但这些给你提供了一个大致的了解。
Scikit-learn 提供了丰富的不同模型用于各种任务,但方便的是,它们的使用语法是一致的。在这一节中,我们将使用 逻辑回归 模型来说明模型语法。尽管名字中带有“回归”,逻辑回归实际上是一个分类模型。这是最简单的分类模型之一,因此也是最重要的模型之一。在下一章中,我们将详细讲解逻辑回归的数学原理。在此之前,你可以简单地将其视为一个可以从标记数据中学习的黑箱,然后做出预测。
从第一章开始,你应该熟悉在标记数据上训练算法的概念,这样你就可以使用训练好的模型对新数据进行预测。Scikit-learn 将这些核心功能封装在 .fit 方法中用于训练模型,.predict 方法中用于进行预测。由于语法的一致性,你可以在任何 scikit-learn 模型上调用 .fit 和 .predict,从线性回归到分类树。
第一步是选择一个模型,在这个例子中是逻辑回归模型,并从.fit和数据中实例化它,例如从模型拟合过程学到的信息。当你从 scikit-learn 实例化一个模型类时,你是拿到 scikit-learn 为你提供的模型蓝图,并将其创建为一个有用的对象。你可以在你的数据上训练这个对象,然后将其保存到磁盘以供以后使用。以下代码片段可以用来执行这个任务。第一步是导入类:
from sklearn.linear_model import LogisticRegression
将类实例化为对象的代码如下:
my_lr = LogisticRegression()
该对象现在是我们工作区中的一个变量。我们可以使用以下代码进行检查:
my_lr
这应该会输出以下内容:
LogisticRegression()
请注意,创建模型对象的过程本质上并不需要了解逻辑回归是什么或它如何工作。尽管我们在创建逻辑回归模型对象时没有选择任何特定选项,但我们现在实际上使用了很多默认选项来构建和训练模型。实际上,这些是我们在不知道的情况下做出的关于模型实现细节的选择。像 scikit-learn 这样易于使用的包的危险在于,它可能会让你忽视这些选择。然而,每当你使用一个为你准备好的机器学习模型时,就像 scikit-learn 模型一样,你的首要任务是理解所有可用的选项。在这种情况下,最佳实践是,在创建对象时明确提供每个关键字参数给模型。即使你只是选择所有默认选项,这也有助于提高你对所做选择的意识。
我们稍后将回顾这些选择的解释,但现在这里是使用所有默认选项实例化逻辑回归模型的代码:
my_new_lr = LogisticRegression(penalty='l2', dual=False,\
tol=0.0001, C=1.0,\
fit_intercept=True,\
intercept_scaling=1,\
class_weight=None,\
random_state=None,\
solver='lbfgs',\
max_iter=100,\
multi_class='auto',\
verbose=0, warm_start=False,\
n_jobs=None, l1_ratio=None)
尽管我们在my_new_lr中创建的对象与my_lr完全相同,但像这样显式地指定,尤其在你刚开始学习并了解不同模型时是非常有帮助的。一旦你更加熟悉,你可能希望只使用默认选项进行实例化,并在必要时稍后进行更改。在这里,我们展示了如何做到这一点。以下代码设置了两个选项并显示了模型对象的当前状态:
my_new_lr.C = 0.1
my_new_lr.solver = 'liblinear'
my_new_lr
这应该会产生以下内容:
Out[11]:LogisticRegression(C=0.1, solver='liblinear')
请注意,仅显示了我们已从默认值更新的选项。在这里,我们将一个叫做C的参数从默认值1更新为0.1,并且我们还指定了一个求解器。现在,了解超参数是你在将模型拟合到数据之前提供的选项就足够了。这些选项指定了模型将如何训练。稍后,我们将详细解释所有选项是什么以及如何有效选择它们的值。
为了说明核心功能,我们将用这个几乎默认的逻辑回归算法来拟合一些数据。监督学习算法依赖于带标签的数据。这意味着我们需要特征,通常包含在一个名为X的变量中,以及对应的响应,包含在一个名为y的变量中。我们将从数据集中借用前 10 个样本的一个特征和响应来说明:
X = df['EDUCATION'][0:10].values.reshape(-1,1)
X
这应该显示前 10 个样本的EDUCATION特征值:

图 2.2:特征的前 10 个值
可以通过以下方式获得响应变量的前 10 个对应值:
y = df['default payment next month'][0:10].values
y
这是输出结果:
Out[13]: array([1, 1, 0, 0, 0, 0, 0, 0, 0, 0])
在这里,我们选择了 DataFrame 中的几个 Series(即列):我们讨论过的EDUCATION特征和响应变量。然后我们选择了每个特征的前 10 个元素,并最终使用.values方法返回了 NumPy 数组。还注意到,我们使用了.reshape方法来调整特征的形状。Scikit-learn 期望特征数组的第一个维度(即行数)等于样本数,因此我们需要对X进行这种形状调整,但y不需要。.reshape的第一个位置参数中的–1表示根据输入数据的数量,在该维度上灵活调整输出数组的形状。由于这个例子中我们只有一个特征,所以我们指定了第二个参数,即列数为1,并让–1参数指示数组应根据需要填充第一维,容纳数据,在这个例子中是 10 个元素。请注意,虽然我们提取了数据并转换为 NumPy 数组来展示这种方法,但也可以直接将 pandas Series 作为输入传递给 scikit-learn。
现在让我们使用这些数据来拟合我们的逻辑回归。这只需要一行代码:
my_new_lr.fit(X, y)
这是输出结果:
Out[14]:LogisticRegression(C=0.1, solver='liblinear')
仅此而已。一旦数据准备好并且模型被指定,拟合模型几乎就像是顺便做的事。当然,我们现在忽略了所有重要的选项以及它们的含义。但从技术角度来看,拟合一个模型在代码层面是非常简单的。你可以看到,这个单元的输出只是打印出了我们已经看到的相同选项。虽然拟合过程没有返回任何内容,除了这个输出,但一个非常重要的变化已经发生。my_new_lr模型对象现在是一个已训练的模型。我们可以说,这个变化发生在my_new_lr,它已经被修改。这类似于修改 DataFrame 的原地操作。现在我们可以使用训练好的模型,利用新的样本特征来进行预测,而这些样本是模型之前从未“见过”的。让我们试试EDUCATION特征的接下来的 10 行。
我们可以使用一个新变量new_X来选择和查看这些特征:
new_X = df['EDUCATION'][10:20].values.reshape(-1,1)
new_X

图 2.3: 新特征用于预测
预测是这样进行的:
my_new_lr.predict(new_X)
这里是输出结果:
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
我们还可以查看与这些预测对应的真实值,因为这些数据是有标签的:
df['default payment next month'][10:20].values
这里是输出结果:
Out[17]:array([0, 0, 0, 1, 0, 0, 1, 0, 0, 0])
在这里,我们展示了几件事。在获取到新的特征值后,我们调用了经过训练的模型的 .predict 方法。请注意,这个方法的唯一参数是一组特征,也就是我们称之为 new_X 的“X”。
我们的小模型表现得如何?我们可能天真地认为,因为模型预测了所有的 0,而且真实标签中 80% 是 0,所以我们有 80% 的预测是正确的,看起来似乎不错。另一方面,我们完全未能成功预测任何 1。所以,如果 1 的预测很重要,我们实际上表现得并不好。虽然这只是一个让你熟悉 scikit-learn 工作方式的例子,但值得考虑一下,对于这个问题,什么是“好的”预测。我们很快会详细讨论如何评估模型的预测能力。现在,先为自己鼓掌,因为你已经在实际数据中动手,完成了第一个机器学习模型的拟合。
生成合成数据
在接下来的练习中,你将独立完成模型拟合过程。我们将通过使用线性回归来引导这个过程,线性回归是最著名的数学模型之一,应该是基本统计学中比较熟悉的内容,也叫作最佳拟合线。如果你不知道它是什么,可以查阅基础统计学资料,尽管这里的目的是展示 scikit-learn 中模型拟合的机制,而不是深入理解模型。我们将在本书后面讨论其他数学模型的应用,如逻辑回归。在此之前,你将通过使用 random 库来生成随机数,以及 matplotlib 的 scatter 和 plot 函数来创建散点图和线图,来准备好数据。在线性回归部分的练习中,我们将使用 scikit-learn。
为了开始,我们使用 NumPy 创建一个由 1,000 个随机实数(也就是说,不仅仅是整数,还有小数)组成的单维特征数组 X,这些数字的范围在 0 到 10 之间。我们再次使用 default_rng(随机数生成器)的方法 .uniform,从均匀分布中抽取:在 low(包含)和 high(不包含)之间,选择任意数字的概率是相等的,并且返回一个由你指定的 size 大小组成的数组。我们创建一个包含 1,000 个元素的一维数组(即向量),然后检查前 10 个数字。所有这些都可以通过以下代码完成:
from numpy.random import default_rng
rg = default_rng(12345)
X = rg.uniform(low=0.0, high=10.0, size=(1000,))
X[0:10]
输出应该如下所示:

图 2.4: 使用 NumPy 创建随机、均匀分布的数字
线性回归数据
现在我们需要一个响应变量。对于这个例子,我们将生成符合线性回归假设的数据:数据将展示出与特征之间的线性趋势,但同时具有正态分布的误差:

图 2.5:带有高斯噪声的线性方程
这里,a 是斜率,b 是截距,而高斯噪声的均值是 µ,标准差是 σ。为了实现这一点,我们需要创建一个对应的响应向量 y,它通过斜率乘以特征数组 X,再加上一些高斯噪声(同样使用 NumPy)和一个截距来计算。噪声将是一个包含 1,000 个数据点的数组,它与特征数组 X 的形状相同(size),噪声的均值(loc)为 0,标准差(scale)为 1. 这样就会为我们的线性数据增加一点“散布”:
slope = 0.25
intercept = -1.25
y = slope * X + rg.normal(loc=0.0, scale=1.0, size=(1000,))\
+ intercept
现在我们想要可视化这些数据。我们将使用 matplotlib 将 y 与特征 X 绘制为散点图。首先,我们使用 .rcParams 设置图像的分辨率(dpi = 每英寸点数),以获得清晰的图像。然后,我们使用 plt.scatter 创建散点图,其中 X 和 y 是前两个参数,s 参数指定点的大小。
这段代码可以用于绘图:
mpl.rcParams['figure.dpi'] = 400
plt.scatter(X,y,s=1)
plt.xlabel('X')
plt.ylabel('y')
执行这些单元格后,你应该在你的笔记本中看到类似这样的内容:

图 2.6:绘制带噪声的线性关系
看起来像一些带噪声的线性数据,正如我们所希望的那样。现在让我们开始建模。
注意
如果你正在阅读本书的印刷版,可以通过访问以下链接下载并浏览本章某些图像的彩色版本:packt.link/0dbUp。
练习 2.01:Scikit-Learn 中的线性回归
在本练习中,我们将使用刚刚生成的合成数据,并使用 scikit-learn 确定最佳拟合线,或线性回归。第一步是从 scikit-learn 导入线性回归模型类并创建一个对象。导入的过程类似于我们之前使用的 LogisticRegression 类。和任何模型类一样,你应该观察所有默认选项。请注意,对于线性回归,指定的选项并不多:你将在本练习中使用默认值。默认设置包括 fit_intercept=True,这意味着回归模型将包括截距项。这是完全合适的,因为我们已经在合成数据中添加了截距。请按照以下步骤完成练习,注意前面部分生成线性回归数据的代码必须先在同一个笔记本中运行(如 GitHub 上所见):
注意
本练习的 Jupyter 笔记本可以在这里找到:packt.link/IaoyM。
-
执行这段代码以导入线性回归模型类并用所有默认选项实例化它:
from sklearn.linear_model import LinearRegression lin_reg = LinearRegression(fit_intercept=True, normalize=False,\ copy_X=True, n_jobs=None) lin_reg你应该会看到以下输出:
Out[11]:LinearRegression()由于我们使用了所有默认选项,因此没有显示任何选项。现在我们可以使用我们的合成数据来拟合模型,记得像之前一样重新调整特征数组的形状(将样本放置在第一维)。在拟合线性回归模型后,我们查看
lin_reg.intercept_,它包含拟合模型的截距,以及lin_reg.coef_,它包含斜率。 -
运行这段代码以拟合模型并检查系数:
lin_reg.fit(X.reshape(-1,1), y) print(lin_reg.intercept_) print(lin_reg.coef_)你应该会看到截距和斜率的输出:
-1.2522197212675905 [0.25711689]我们再次看到,一旦数据准备好并且模型选项确定,实际上在 scikit-learn 中拟合模型是一个非常简单的过程。这是因为所有关于确定模型参数的算法工作都被抽象化,用户无需关心。稍后我们将讨论这个过程,特别是在我们用来处理案例研究数据的逻辑回归模型。
X。我们将其输出捕获为一个变量y_pred。这与图 2.7中的示例非常相似,只是这里我们是在用于拟合模型的相同数据上进行预测(之前我们是在不同的数据上进行预测),并且我们将.predict方法的输出放入一个变量中。 -
运行这段代码以进行预测:
y_pred = lin_reg.predict(X.reshape(-1,1))我们可以将预测结果
y_pred与特征X绘制成线图,叠加在特征和响应数据的散点图上,就像我们在图 2.6中所做的那样。在这里,我们添加了plt.plot,它默认会生成线图,用来绘制特征和模型预测的响应值。请注意,在调用plt.plot时,我们在X和y数据后跟上了'r'。这个关键字参数让线条变成红色,是图表格式化的一种简写语法。 -
这段代码可以用来绘制原始数据,以及在这些数据上拟合的模型预测结果:
plt.scatter(X,y,s=1) plt.plot(X,y_pred,'r') plt.xlabel('X') plt.ylabel('y')执行这个单元格后,你应该会看到类似的输出:
![图 2.7:绘制数据和回归线]()
图 2.7:绘制数据和回归线
绘图看起来像是最优拟合线,正如预期的那样。
在这个练习中,与我们之前在使用逻辑回归时调用 .predict 不同,我们在同一数据 X 上进行了预测,而这些数据也用于训练模型。这是一个重要的区别。虽然在这里,我们看到模型如何“拟合”它所训练的相同数据,但之前我们检查了模型在新数据上的预测。在机器学习中,我们通常关心的是预测能力:我们希望模型能帮助我们了解未来情景的可能结果。然而,事实证明,无论是模型在用于拟合的训练数据上的预测,还是在未用于拟合的测试数据上的预测,对于理解模型的工作原理都非常重要。我们将在稍后的第四章中正式定义这些概念,即讨论偏差-方差权衡时。
二分类模型性能指标
在我们开始认真构建预测模型之前,我们希望了解如何在创建模型后判断它在某种意义上是否“好”。正如你可能想象的那样,这个问题已引起了研究人员和实践者的广泛关注。因此,有许多不同的模型性能指标可供选择。
注意
为了了解选项的范围,可以查看 scikit-learn 模型评估页面:scikit-learn.org/stable/modules/model_evaluation.html#model-evaluation。
在选择模型性能指标来评估模型的预测质量时,重要的是要牢记两点。
该指标是否适用于问题
指标通常只为特定类型的问题定义,比如分类或回归。对于二分类问题,有几个指标用来衡量模型回答“是”或“否”问题的正确性。这里的一个附加细节是,模型对于每个类别(正类和负类)的正确率如何。我们将在这里详细讨论这些指标。另一方面,回归指标旨在衡量预测值与目标数量的接近程度。如果我们试图预测房价,我们的预测与实际价格有多接近?我们是系统性地高估还是低估?我们是否在预测更贵的房子时出错,但预测便宜的房子正确?有许多不同的方式来观察回归指标。
这个指标能回答业务问题吗?
无论你正在处理哪类问题,都有许多选择可以用来衡量指标。哪一个是正确的呢?即便如此,你如何判断模型在该指标下是否“足够好”呢?在某种程度上,这是一个主观性问题。然而,当我们考虑模型的目标时,我们可以变得更加客观。在商业环境中,典型的目标是增加利润或减少损失。最终,你需要统一你的商业问题(通常以某种方式与金钱相关)和你用来评估模型的指标。
例如,在我们的信用违约问题中,未能正确识别将会违约的账户是否会产生特别高的成本?这是否比误分类一些不会违约的账户更为重要?
在本书的后续章节中,我们将结合正确和错误分类的相对成本与收益概念,并进行财务分析。首先,我们将介绍一些常用的指标,用于评估二元分类模型的预测质量,这也是我们案例研究中需要构建的模型类型。
数据拆分:训练集和测试集
在本章的 scikit-learn 介绍中,我们引入了使用训练好的模型对模型从未“见过”的新数据进行预测的概念。事实证明,这是预测建模中的一个基础概念。在我们创建具有预测能力的模型的过程中,我们需要某种衡量标准,来评估模型对未用于拟合模型的数据的预测能力。这是因为在拟合模型时,模型会“专门化”于学习特征和响应之间的关系,且仅限于用于拟合的特定标注数据集。虽然这很不错,但最终我们希望能够使用该模型对新的、未见过的数据做出准确的预测,而我们对于这些数据的标签值并不了解。
例如,在我们的案例研究中,一旦我们将训练好的模型交付给客户,他们就会生成一个新的特征数据集,特征与我们现在使用的相似,只不过数据范围不再是从四月到九月,而是从五月到十月。然后,我们的客户将使用这个模型来预测账户是否会在十一月违约。
为了了解我们预计模型在预测哪些账户将在 11 月违约时的表现(这在 12 月之前无法得知),我们可以将当前数据集中的一部分数据保留作为测试数据,并从模型训练过程中分离出来。这些数据也可以称为外样本数据,因为它们由未参与模型训练的样本组成。用于训练模型的样本称为训练数据。将一部分数据保留下来作为测试数据的做法,让我们能够了解模型在实际应用时的表现,即在对未用于训练的数据进行预测时的表现。在本章中,我们将创建一个示例的训练/测试拆分,以说明不同的二元分类指标。
我们将使用 scikit-learn 中方便的train_test_split功能来拆分数据,使得 80%的数据用于训练,剩余的 20%用于测试。这些百分比是常见的拆分方式;通常来说,你需要足够的训练数据,以便算法能够从代表性数据样本中充分“学习”。然而,这些百分比并不是固定不变的。如果你拥有大量样本,可能不需要占用那么大比例的训练数据,因为即便使用较小的比例,也能获得一个较大且具有代表性的训练集。我们鼓励你尝试不同的大小并观察其效果。此外,要注意,每个问题对于有效训练模型所需的数据量都是不同的。没有固定的规则来决定训练集和测试集的大小。
对于我们的 80/20 数据拆分,我们可以使用以下代码片段:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split\
(df['EDUCATION']\
.values.reshape(-1,1),\
df['default payment\
' next month']\
.values, test_size=0.2,\
random_state=24)
注意我们将test_size设置为0.2,即 20%。训练数据的大小将自动设置为剩余的 80%。让我们检查一下训练数据和测试数据的形状,看看它们是否符合预期,如下所示的输出所示:

图 2.8:训练集和测试集的形状
你应该自行确认训练集和测试集中的样本(行数)是否与 80/20 拆分一致。
在进行训练/测试拆分时,我们还设置了random_state参数,这是一个随机数种子。使用这个参数可以确保在每次运行这个笔记本时,训练/测试拆分是一致的。否则,随机拆分过程每次运行时都会选择不同的 20%数据进行测试。
train_test_split的第一个参数是特征,在本例中是EDUCATION,第二个参数是响应。该函数有四个输出:分别是训练集和测试集中的样本特征,以及与这些特征集对应的响应变量。此函数所做的只是从数据集中随机选择 20%的行索引,并将这些特征和响应子集作为测试数据,剩下的用于训练。现在我们已经有了训练数据和测试数据,确保数据的性质在这些集合中是一致的很重要。特别是,正类的比例是否相似?您可以通过以下输出进行观察:

图 2.9: 训练数据和测试数据中的类别分布
训练集和测试集中的正类比例均约为 22%。这是好的,因为我们可以说训练集是测试集的代表。在这种情况下,由于我们拥有一个包含数万个样本的大型数据集,并且类别不太失衡,因此我们不必采取额外的预防措施来确保这种情况发生。
然而,您可以想象,如果数据集较小,且正类非常稀有,训练集和测试集之间的类别比例可能会明显不同,甚至更糟,测试集中可能根本没有正样本。为了防止这种情况,您可以使用train_test_split的stratify关键字参数。此过程也会随机地将数据划分为训练集和测试集,但可以确保类别比例相等或非常相似。
注意
超时测试
如果您的数据包含跨越较长时间段的特征和响应,最好尝试基于时间进行训练/测试集划分。例如,如果您有两年的数据,每个月都有特征和响应,您可能希望尝试依次用 12 个月的数据训练模型,然后用下一个月或下下个月的数据进行测试,具体取决于在模型使用时可操作的情况。您可以一直重复这个过程,直到数据用完,以获得几个不同的测试分数。这将为您提供有价值的模型性能洞察,因为它模拟了模型部署时实际面临的条件:一个在旧特征和响应上训练的模型将用于对新数据进行预测。在案例研究中,响应仅来自某一时刻(一个月内的信用违约),所以这里不适用此方法。
分类准确率
现在我们继续拟合一个示例模型,以说明二分类度量。我们将继续使用接近默认选项的逻辑回归,选择我们在第一章,数据探索与清洗中演示的相同选项:

图 2.10:加载模型类并创建模型对象
现在我们继续训练模型,正如你想象的那样,使用我们训练集中的标签数据。我们接着使用训练好的模型对从保留的测试集中的样本特征进行预测:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_02_11.jpg)
图 2.11:训练模型并对测试集进行预测
我们已经将测试集的模型预测标签存储在一个名为 y_pred 的变量中。现在我们该如何评估这些预测的质量呢?我们有真实标签,存储在 y_test 变量中。首先,我们将计算可能是所有二分类指标中最简单的一个:准确度。准确度被定义为正确分类样本所占的比例。
计算准确度的一种方法是创建一个逻辑掩码,当预测标签等于实际标签时,掩码为True,否则为False。我们可以计算这个掩码的平均值,将True视为 1,False视为 0,从而得到正确分类的比例:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_02_12.jpg)
图 2.12:使用逻辑掩码计算分类准确度
这表示模型在 78%的时间里是正确的。虽然这是一个非常直接的计算方法,但实际上使用 scikit-learn 更简便的方法来计算准确度。我们可以使用训练好的模型的 .score 方法,将测试数据的特征传递给它进行预测,同时传递测试标签。该方法会执行预测,然后进行我们之前所做的相同计算,所有这些都可以一步完成。或者,我们可以导入 scikit-learn 的 metrics 库,该库包含许多模型性能指标,比如 accuracy_score。为此,我们需要传递真实标签和预测标签:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_02_13.jpg)
图 2.13:使用 scikit-learn 计算分类准确度
这些方法都会得到相同的结果,正如预期的那样。现在我们知道了模型的准确度,那么如何解释这个指标呢?表面上看,78%的准确度可能听起来不错。我们大部分的预测都正确。然而,二分类准确度的一个重要测试是将其与一个非常简单的假设模型进行比较:这个假设模型对每个样本的预测都是相同的——无论特征是什么,它总是预测多数类别。虽然在实际中这个模型没有什么用处,但它提供了一个重要的极端情况,供我们用来与已训练模型的准确度进行比较。这样的极端情况有时被称为“零模型”。
想想看,这样一个空模型的准确率会是多少。在我们的数据集中,我们知道大约 22%的样本是正类。因此,负类是多数类,占剩余的 78%的样本。因此,一个总是预测负类的空模型在这个数据集中将有 78%的正确率。当我们将训练好的模型与这个空模型进行比较时,就会发现,78%的准确率其实并没有太大意义。我们可以通过一个不关注任何特征的模型来获得相同的准确率。
尽管我们可以通过多数类空模型来解释准确率,但还有其他一些二分类指标可以更深入地了解模型对正类和负类样本的表现。
真阳性率、假阳性率与混淆矩阵
在二分类中,只有两个标签需要考虑:正类和负类。作为比全体样本准确率更具描述性的模型性能评估方式,我们还可以仅查看那些正类标签样本的预测准确度。我们成功预测为正类的比例称为真阳性率(TPR)。如果我们设P为测试数据中正类样本的数量,TP为真阳性的数量,定义为被模型正确预测为正类的正类样本数,那么 TPR 公式如下:

图 2.14:TPR 公式
真阳性率的反面是假阴性率(FNR)。这表示我们错误地预测为负类的正类测试样本的比例。这样的错误被称为假阴性(FN),假阴性率(FNR)的计算公式如下:

图 2.15:FNR 公式
由于所有的正类样本要么被正确预测,要么被错误预测,因此真阳性数与假阴性数的总和等于正类样本的总数。从数学上讲,P = TP + FN,因此,结合 TPR 和 FNR 的定义,我们可以得到以下公式:

图 2.16:TPR 与 FNR 的关系
由于 TPR 和 FNR 的和为 1,因此只需要计算其中一个就足够了。
与 TPR 和 FNR 类似,还有真负率(TNR)和假阳性率(FPR)。如果N是负类样本的数量,那么真负样本(TN)的总和是那些被正确预测为负类的数量,假阳性(FP)样本的总和则是被错误预测为正类的样本数量:

图 2.17:TNR 公式

图 2.18:FPR 公式

图 2.19:TNR 与 FPR 之间的关系
真实正例、假正例和假负例可以方便地在一个表格中总结,这个表格叫做混淆矩阵。二分类问题的混淆矩阵是一个 2 x 2 的矩阵,其中真实类别位于一个轴上,预测类别位于另一个轴上。混淆矩阵快速总结了真实和假正例及假负例的数量:

图 2.20:二分类的混淆矩阵
由于我们希望做出正确的分类,我们希望混淆矩阵的对角线条目(即从左上角到右下角的对角线上的条目:TN 和 TP)相对较大,而非对角线条目较小,因为它们代表错误的分类。可以通过将对角线上的条目(即正确的预测)相加,然后除以所有预测的总数来计算准确度。
练习 2.02:在 Python 中计算真实和假正例与假负例率以及混淆矩阵
在本练习中,我们将使用之前创建的逻辑回归模型的测试数据和模型预测,只使用EDUCATION特征。我们将展示如何手动计算真实正例和假负例率,以及混淆矩阵所需的真实和假正例及假负例的数量。然后我们将展示使用 scikit-learn 快速计算混淆矩阵的方法。执行以下步骤来完成练习,注意在做此练习之前必须先运行上一部分的代码(如 GitHub 上所示):
注意
本练习的 Jupyter 笔记本可以在此找到:packt.link/S02kz。
-
运行此代码计算正例样本的数量:
P = sum(y_test) P输出应该如下所示:
1155现在我们需要真实正例的数量。这些是实际标签为 1 且预测也为 1 的样本。我们可以通过逻辑掩码来识别这些样本,其中正例为(
y_test==1),并且&是逻辑运算符,y_pred==1)。 -
使用以下代码计算真实正例的数量:
TP = sum( (y_test==1) & (y_pred==1) ) TP以下是输出:
0真实正例率是指真实正例与所有正例的比例,这在这里当然是 0。
-
运行以下代码获取真实正例率(TPR):
TPR = TP/P TPR你将获得以下输出:
0.0类似地,我们可以识别假负例。
-
使用以下代码计算假负例的数量:
FN = sum( (y_test==1) & (y_pred==0) ) FN输出应如下所示:1155
我们还希望得到假负率(FNR)。
-
使用以下代码计算假负率(FNR):
FNR = FN/P FNR输出应如下所示:
1.0从真实正例和假负例率中我们学到了什么?
首先,我们可以确认它们的总和为 1。这个事实很容易看出来,因为 TPR = 0 且 FPR = 1。这告诉我们关于模型什么信息?在测试集上,至少对于正样本,模型实际上表现得像一个多数类零模型。每个正样本都被预测为负样本,因此没有一个被正确预测。
-
让我们找出测试数据的 TNR 和 FPR。由于这些计算与我们之前查看的非常相似,因此我们一次性展示它们,并介绍一个新的 Python 函数:
![图 2.21:计算真正负类率和假正类率并打印它们]()
图 2.21:计算真正负类率和假正类率并打印它们
除了像我们之前那样计算 TNR 和 FPR,我们还展示了 Python 中的
print函数,并结合.format方法来处理字符串,这样可以在大括号{}标记的位置替换变量。还有多种选项可以格式化数字,例如保留特定的小数位数。注意
如需更多细节,请参考
docs.python.org/3/tutorial/inputoutput.html。那么,我们在这里学到了什么?事实上,我们的模型对所有样本(正样本和负样本)表现得像一个多数类零模型。显然,我们需要一个更好的模型。
虽然我们在本次练习中手动计算了混淆矩阵的所有条目,但在 scikit-learn 中有一种快速的方法来做这件事。请注意,在 scikit-learn 中,真正类位于混淆矩阵的纵轴上,预测类位于横轴上,正如我们之前所展示的那样。
-
使用以下代码在 scikit-learn 中创建混淆矩阵:
metrics.confusion_matrix(y_test, y_pred)你将获得以下输出:
![图 2.22:我们示例模型的混淆矩阵]()
图 2.22:我们示例模型的混淆矩阵
计算 TPR、FNR、TNR 和 FPR 所需的所有信息都包含在混淆矩阵中。我们还注意到,可以从混淆矩阵中派生出许多其他分类指标。实际上,其中一些实际上是我们已经讨论过的指标的同义词。例如,TPR 也叫做 召回率 和 敏感性。与召回率一起,二分类中常用的另一个指标是 精确度:它是正确的正类预测的比例(与正确预测的正样本比例相对)。在本章的活动中,我们将进一步了解精确度。
注意
多类分类
我们的案例研究涉及一个二分类问题,只有两种可能的结果:账户是否违约。另一种重要的机器学习分类问题是多分类问题。在多分类问题中,存在若干个相互排斥的结果。一个经典的例子是手写数字的图像识别;一个手写数字应该只能是 0、1、2、… 9 之一。尽管多分类问题超出了本书的范围,但我们现在学习的二分类指标可以扩展到多分类设置中。
发现预测概率:逻辑回归如何做出预测?
现在我们已经熟悉了准确率、真阳性和假阳性、真阴性和假阴性,以及混淆矩阵,我们可以探索使用逻辑回归学习更多高级的二分类指标的方法。到目前为止,我们仅将逻辑回归视为一个可以从标注的训练数据中学习,然后对新特征做出二分类预测的“黑箱”。虽然我们稍后会详细学习逻辑回归的工作原理,但我们现在可以开始窥探这个黑箱的内部。
理解逻辑回归的工作方式的一件事是,原始预测——换句话说,从定义逻辑回归的数学方程得出的直接输出——并不是二进制标签。它们实际上是一个从 0 到 1 的概率(尽管从技术上讲,这个方程永远不会允许概率等于 0 或 1,稍后我们将看到)。这些概率只有通过使用阈值才能转化为二分类预测。阈值是用来决定预测为正类的概率值,低于该值则预测为负类。scikit-learn 中的默认阈值是 0.5。这意味着,任何预测概率至少为 0.5 的样本都会被识别为正类,而预测概率小于 0.5 的样本则被判定为负类。然而,我们可以自由选择任何我们想要的阈值。事实上,选择阈值是逻辑回归以及其他机器学习分类算法中估计类别成员概率的关键灵活性之一。
练习 2.03:从训练好的逻辑回归模型中获取预测概率
在接下来的练习中,我们将熟悉逻辑回归的预测概率,以及如何从 scikit-learn 模型中获取它们。
我们可以通过进一步检查在本章中早些时候训练的逻辑回归模型对象上可用的方法,来开始发现预测概率。回想一下,在我们训练模型后,可以通过将新样本的特征值传递给训练好的模型的.predict方法,来进行二分类预测。这些是基于 0.5 的阈值假设做出的预测。
然而,我们可以直接访问这些样本的预测概率,使用.predict_proba方法。执行以下步骤来完成练习,请记住,如果您开始一个新的笔记本,您需要重新创建在本章中之前训练的相同模型:
注意
这个练习的 Jupyter 笔记本可以在这里找到:packt.link/yDyQn。该笔记本包含训练模型的先决步骤,应该在这里显示的第一步之前执行。
-
使用以下代码获取测试样本的预测概率:
y_pred_proba = example_lr.predict_proba(X_test) y_pred_proba输出应该如下所示:
![图 2.23:测试数据的预测概率]()
图 2.23:测试数据的预测概率
我们可以在存储的
y_pred_proba的输出中看到,那里有两列。这是因为我们的分类问题中有两个类别:负类和正类。假设负类标签编码为 0,正类标签编码为 1(如数据中所示),scikit-learn 会将负类成员资格的概率报告为第一列,正类成员资格的概率报告为第二列。由于这两个类别是互斥的,并且是唯一的选项,因此每个样本的两类预测概率和应为 1。让我们确认这一点。
首先,我们可以在第一维度(列)上使用
np.sum来计算每个样本的概率和。 -
使用此代码计算每个样本的预测概率和:
prob_sum = np.sum(y_pred_proba,1) prob_sum输出如下所示:
array([1., 1., 1., ..., 1., 1., 1.])看起来确实全是 1。我们应该检查结果是否与测试数据标签的数组形状相同。
-
使用此代码检查数组形状:
prob_sum.shape这应该输出以下内容:
(5333,)很好;这正是预期的形状。现在,检查每个值是否为 1。我们使用
np.unique来显示这个数组中所有唯一的元素。这类似于 SQL 中的DISTINCT。如果所有概率和确实为 1,那么概率数组中应该只有一个唯一元素:1。 -
使用此代码显示所有唯一的数组元素:
np.unique(prob_sum)这应该输出以下内容:
array([1.])在确认我们对预测概率的信心后,我们注意到,由于类概率和为 1,因此只考虑第二列,即正类成员资格的预测概率就足够了。让我们将这些捕获到一个数组中。
-
运行此代码,将预测概率数组的第二列(正类成员资格的预测概率)放入一个数组中:
pos_proba = y_pred_proba[:,1] pos_proba输出应该如下所示:
![图 2.24:正类成员资格的预测概率]()
](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_02_24.jpg)
图 2.24:正类成员资格的预测概率
这些预测概率看起来如何?一种了解它们的方法,也是评估模型输出的一个好诊断手段,就是绘制预测概率的直方图。直方图是一种自然的方式,我们可以使用 matplotlib 的
hist()函数来实现。请注意,如果你只执行包含直方图函数的代码单元,你会在绘图之前得到 NumPy 直方图函数的输出。这个输出包括每个桶中的样本数和桶边界的位置。 -
执行此代码可以查看直方图输出和一个未格式化的图(此处未显示):
plt.hist(pos_proba)输出结果如下:
![图 2.25: 直方图计算的细节]()
图 2.25: 直方图计算的细节
这些信息可能对你有用,并且也可以直接通过
np.histogram()函数获得。然而,在这里我们主要关注图形,因此我们调整了字体大小并添加了一些坐标轴标签。 -
运行此代码以获得格式化的预测概率直方图:
mpl.rcParams['font.size'] = 12 plt.hist(pos_proba) plt.xlabel('Predicted probability of positive class '\ 'for test data') plt.ylabel('Number of samples')绘图应如下所示:
![图 2.26: 预测概率的直方图]()
图 2.26: 预测概率的直方图
注意,在预测概率的直方图中,实际上只有四个桶中有样本,而且它们之间的间隔相当大。这是因为在我们的示例模型中,
EDUCATION特征只有四个独特的值。此外,注意到所有预测概率都低于 0.5。正是因为使用了 0.5 的阈值,每个样本都被预测为负类。如果我们将阈值设定为低于 0.5,我们可能会得到不同的结果。例如,如果我们将阈值设置为 0.25,那么图 2.26最右边最小的那一栏中的所有样本都会被分类为正类,因为这些样本的预测概率都高于 0.25。如果我们能够看到这些样本中实际上有多少是正类标签,那么这对我们来说是有价值的信息。这样,我们就能知道将阈值调低到 0.25 是否能通过将最右边一栏的样本分类为正类来改善分类器的性能。
实际上,我们可以通过以下方式轻松可视化这一点,使用
y_test == 1来获取正样本,然后使用y_test == 0来获取负样本。 -
使用以下代码隔离正负样本的预测概率:
pos_sample_pos_proba = pos_proba[y_test==1] neg_sample_pos_proba = pos_proba[y_test==0]现在,我们希望将这些数据绘制为堆叠直方图。代码与我们之前创建的直方图类似,不同之处在于,我们将传递一个包含数组的列表,这些数组分别是我们刚刚创建的正负样本的预测概率数组,并且添加一个关键字,指示我们希望柱形图堆叠而非并排显示。同时,我们还将创建一个图例,以便颜色能够在图中清晰区分。
-
使用以下代码绘制堆叠直方图:
plt.hist([pos_sample_pos_proba, neg_sample_pos_proba],\ histtype='barstacked') plt.legend(['Positive samples', 'Negative samples']) plt.xlabel('Predicted probability of positive class') plt.ylabel('Number of samples')绘图应如下所示:
![图 2.27: 按类别堆叠的预测概率直方图]()
图 2.27:按类别堆叠的预测概率直方图
该图展示了每个预测概率的样本的真实标签。现在我们可以考虑将阈值降低到 0.25 时的效果。花一点时间思考一下这意味着什么,记住任何预测概率达到或超过阈值的样本都将被分类为正样本。
由于图 2.28右侧的小区间几乎全是负样本,如果我们将阈值降低到 0.25,我们将错误地将这些样本分类为正样本,并增加我们的 FPR。与此同时,我们仍然未能正确分类很多(如果有的话)正样本,因此我们的 TPR 几乎不会增加。进行这样的改变似乎会降低模型的准确性。
接收者操作特征(ROC)曲线
为分类器决定一个阈值是一个找到“最佳点”的问题,在这个点上我们成功地回收了足够的真正正样本,同时没有引入太多的假正样本。随着阈值越来越低,正负样本都会增加。一个好的分类器能够捕捉到更多的真正正样本,而不会付出大量假正样本的代价。降低阈值进一步的效果是什么呢?基于前面练习中的预测概率,事实证明,在机器学习中有一种经典的可视化方法和一个相应的度量,可以帮助回答这种问题。
接收者操作特征(ROC)曲线是将从 1 逐渐降低到 0 的阈值所产生的 TPR(y 轴)和 FPR(x 轴)的配对图。你可以想象,当阈值为 1 时,没有正预测,因为逻辑回归仅预测 0 到 1 之间的概率(不包括端点)。由于没有正预测,TPR 和 FPR 都为 0,因此 ROC 曲线从(0, 0)开始。随着阈值降低,TPR 将开始增加,如果是一个好的分类器,TPR 增加的速度应该比 FPR 更快。最终,当阈值降到 0 时,每个样本都被预测为正样本,包括所有实际上是正样本的样本,但也包括所有实际上是负样本的样本。这意味着 TPR 为 1,但 FPR 也是 1。在这两个极端之间,是你可能希望设置阈值的合理选项,具体取决于针对特定问题正负样本的相对成本和收益。通过这种方式,你可以全面了解分类器在不同阈值下的性能,从而决定使用哪个阈值。
我们可以编写代码,通过使用预测概率并将阈值从 1 到 0 变化来确定 ROC 曲线的 TPR 和 FPR。相反,我们将使用 scikit-learn 的便捷功能,它将使用真实标签和预测概率作为输入,返回 TPR、FPR 数组以及导致它们的阈值。然后我们将绘制 TPR 与 FPR 的关系图来展示 ROC 曲线。运行此代码,使用 scikit-learn 生成 TPR 和 FPR 数组,用于生成 ROC 曲线,必要时导入 metrics 模块:
from sklearn import metrics
fpr, tpr, thresholds = metrics.roc_curve(y_test, pos_proba)
现在我们需要生成一个图表。我们将使用 plt.plot,它会使用第一个参数作为 x 值(FPR),第二个参数作为 y 值(TPR),并使用缩写 '*-' 来表示带有星号符号的线性图,其中数据点所在的位置。我们还添加了一条从(0,0)到(1,1)的直线图,它将显示为红色('r')并为虚线('--')。我们还给图表添加了图例(我们稍后会解释),以及坐标轴标签和标题。此代码将生成 ROC 图:
plt.plot(fpr, tpr, '*-')
plt.plot([0, 1], [0, 1], 'r--')
plt.legend(['Logistic regression', 'Random chance'])
plt.xlabel('FPR')
plt.ylabel('TPR')
plt.title('ROC curve')
并且图表应如下所示:

用于比较的随机机会显示
](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_02_28.jpg)
图 2.28:我们的逻辑回归的 ROC 曲线,带有随机机会线供比较
我们从 ROC 曲线中学到了什么?我们可以看到它从(0,0)开始,阈值足够高,以至于没有任何正分类。然后,正如我们之前所想,当将阈值降低到约 0.25 时,首先发生的事情是我们观察到假阳性率(FPR)增加,但真正的阳性率(TPR)几乎没有增加。继续降低阈值以使堆叠直方图图中图 2.28的其他条形图被视为正分类的效果,可以通过线条上的后续点来看。我们可以通过检查阈值数组(它不是图的一部分)来查看导致这些比率的阈值。使用以下代码查看用于计算 ROC 曲线的阈值:
thresholds
输出应该如下所示:
array([1.2549944 , 0.2549944 , 0.24007604, 0.22576598, 0.21207085])
请注意,第一个阈值实际上是大于 1 的;从实际角度来看,它只需要足够高,以至于没有正分类。
现在考虑什么样的 ROC 曲线是“好的”。随着我们降低阈值,我们希望看到 TPR 增加,这意味着我们的分类器能够很好地正确识别正样本。同时,理想情况下,FPR 应该不会增加太多。有效分类器的 ROC 曲线会紧贴图的左上角:高 TPR,低 FPR。你可以想象,完美的分类器将得到 1 的 TPR(恢复所有正样本)和 0 的 FPR,并且呈现为一种从(0,0)开始,直达(0,1),再到(1,1)的方形曲线。虽然在实践中这种表现极不可能出现,但它给我们提供了一个极限情况。
进一步考虑这样一个分类器的 曲线下的面积(AUC),如果你曾学习过微积分,可以回想一下积分的概念。完美分类器的 AUC 为 1,因为曲线的形状将在单位区间 [0, 1] 上形成一个正方形。
另一方面,我们图中标记为“随机机会”的线是我们绘制的 ROC 曲线,它理论上是通过使用一个公正的硬币来作为分类器时产生的:它产生真阳性和假阳性的概率相同,因此,降低阈值会等比例地引入更多的每种情况,TPR 和 FPR 以相同的速度增加。这条 ROC 曲线下的 AUC 将是完美分类器 AUC 的一半,正如你从图形中看到的那样,值为 0.5。
因此,一般来说,ROC AUC 的值会在 0.5 和 1 之间(虽然技术上也有可能小于 0.5)。接近 0.5 的值表明模型的分类效果几乎与随机猜测(硬币投掷)相当,而接近 1 的值则表示更好的性能。ROC AUC 是衡量分类器质量的关键指标,并广泛应用于机器学习中。ROC AUC 也可以称为 C 统计量(一致性统计量)。
作为一个重要的指标,scikit-learn 提供了一种方便的方式来计算 ROC AUC。让我们看看逻辑回归分类器的 ROC AUC,方法是传递与 roc_curve 函数相同的信息。使用以下代码计算 ROC 曲线下的面积:
metrics.roc_auc_score(y_test, pos_proba)
观察输出:
0.5434650477972642
逻辑回归的 ROC AUC 值接近 0.5,这意味着它不是一个非常有效的分类器。考虑到我们目前没有花费任何精力去确定候选特征中哪些实际上是有用的,这一点并不令人惊讶。我们只是习惯于模型拟合语法,并学习如何使用仅包含 EDUCATION 特征的简单模型来计算模型质量指标。稍后,通过考虑其他特征,希望能够获得更高的 ROC AUC。
注意
ROC 曲线:它是如何得到这个名字的?
在第二次世界大战期间,雷达接收员根据他们判断雷达屏幕上出现的目标是否为敌机来评估他们的能力。这些决策涉及与我们在二元分类中关注的真阳性、假阳性和真阴性相同的概念。ROC 曲线就是为了衡量雷达接收设备操作员的有效性而设计的。
精度
在开始活动之前,我们将简要考虑之前介绍过的分类指标:精度。像 ROC 曲线一样,这个诊断在不同的阈值范围内都很有用。精度定义如下:

图 2.29:精度公式
考虑这一点的解释,考虑到在预测概率的范围内变化阈值,就像我们为 ROC 曲线所做的那样。在高阈值下,预测为正样本的样本相对较少。随着阈值的降低,越来越多的样本将被预测为正样本。我们的期望是,在执行这一操作时,真正的正样本数量会比假正样本的数量增加得更快,正如我们在 ROC 曲线中所看到的那样。精确度看的是真正的正样本数量与真正和假正样本总和的比例。考虑这里的分母:真正和假正样本的总和是多少?
这个总和实际上是所有正预测的总数,因为所有正预测要么是正确的,要么是错误的。因此,精确度衡量的是正确的正预测与所有正预测的比例。因此,它也被称为metrics.precision_recall_curve。精确度和召回率通常一起绘制,以评估正预测的质量,考虑哪些部分是正确的,同时考虑模型能够识别正类的比例。我们将在接下来的活动中绘制精确度-召回曲线。
为什么精确度是一个有用的分类器性能度量?想象一下,对于每个正的模型预测,你将采取一些昂贵的措施,比如对通过自动化程序标记为不当的内容进行耗时的复审。假正样本会浪费人工审阅者宝贵的时间。在这种情况下,你会希望确保你在做出哪些内容需要详细复审的决定时是正确的。精确度可能是这个情况中一个很好的度量指标。
活动 2.01:使用新特征执行逻辑回归并创建精确度-召回曲线
在这个活动中,你将使用除EDUCATION之外的特征来训练一个逻辑回归模型。然后,你将通过图形化评估精确度和召回率之间的权衡,并计算精确度-召回曲线下的面积。你还将计算训练集和测试集上的 ROC AUC 并进行比较。
执行以下步骤以完成该活动:
注意
该活动的代码和结果输出已加载到一个 Jupyter notebook 中,可以在此处找到:packt.link/SvAOD。
-
使用 scikit-learn 的
train_test_split来生成一组新的训练数据和测试数据。这次,不使用EDUCATION,而使用LIMIT_BAL,即账户的信用额度,作为特征。 -
使用从划分中得到的训练数据来训练一个逻辑回归模型。
-
创建测试数据的预测概率数组。
-
使用测试数据的预测概率和真实标签来计算 ROC AUC。将其与使用
EDUCATION特征的 ROC AUC 进行比较。 -
绘制 ROC 曲线。
-
使用 scikit-learn 的功能,计算测试数据的精确率-召回率曲线的数据。
-
使用 matplotlib 绘制精确率-召回率曲线。
-
使用 scikit-learn 计算精确率-召回率曲线下的面积。你应该得到大约 0.315 的值。
-
现在重新计算 ROC AUC,不过这次使用训练数据。与之前的计算在概念上和数量上有何不同?
注意
包含此活动 Python 代码解决方案的 Jupyter notebook 可以在这里找到:
packt.link/SvAOD。此活动的详细逐步解决方案可以通过这个链接查看。
总结
在本章中,我们通过检查响应变量完成了案例研究数据的初步探索。一旦我们对数据集的完整性和正确性充满信心,就准备好探索特征与响应之间的关系,并构建模型。
本章的大部分内容都在技术和编码层面上熟悉了 scikit-learn 中的模型拟合,并学习了我们可以在案例研究的二分类问题中使用的指标。在尝试不同的特征集和模型时,你将需要某种方法来判断哪种方法比另一种更有效。因此,你需要使用我们在本章中学到的模型性能指标。
尽管准确率作为正确分类百分比是一个熟悉且直观的指标,但我们了解到它可能无法有效评估分类器的性能。我们学会了如何使用多数类零假设模型来判断一个准确率是否真正优秀,还是仅仅与对所有样本预测最常见类别的结果没有差别。当数据不平衡时,准确率通常不是评判分类器的最佳方式。
为了更细致地了解模型的表现,必须将正类和负类分开,并独立评估它们的准确性。从由真正和虚假正负分类汇总而成的混淆矩阵中,我们可以得出其他几个指标:真正率、虚假正率和虚假负率。结合真正和虚假正负以及预测概率和可变预测阈值的概念,我们可以通过 ROC 曲线、精确率-召回率曲线及其下的面积进一步描述分类器的有效性。
有了这些工具,你已经能够充分回答任何领域中二分类器性能的一般问题。在本书后面的内容中,我们将学习如何通过为真阳性、假阳性、真阴性和假阴性分配成本和收益,来评估模型性能的应用特定方法。在此之前,从下一章开始,我们将开始学习可能是最流行且最简单的分类模型的细节:逻辑回归。
第三章:3. 逻辑回归及特征探索的详细内容
概述
本章将教你如何快速高效地评估特征,以便了解哪些特征可能对机器学习模型最为重要。一旦我们掌握了这一点,我们将深入探讨逻辑回归的内部工作原理,让你能够继续在这一基础技术上迈向精通之路。阅读完本章后,你将能够制作多特征与响应变量的相关性图,并将逻辑回归解读为线性模型。
引言
在上一章中,我们使用 scikit-learn 开发了几个示例机器学习模型,以便熟悉它的工作原理。然而,我们使用的特征EDUCATION和LIMIT_BAL并不是以系统化的方式选择的。
在本章中,我们将开始开发评估特征在建模中有效性的方法。这将使你能够快速浏览所有候选特征,从而大致了解哪些特征可能是最重要的。对于最有潜力的特征,我们将看到如何创建视觉总结,以便作为有用的沟通工具。
接下来,我们将详细研究逻辑回归。我们将了解为什么逻辑回归被认为是一个线性模型,即使其公式中涉及了一些非线性函数。我们将学习什么是决策边界,并看到由于其线性特性,逻辑回归的决策边界可能会使得准确分类响应变量变得困难。在这个过程中,我们将通过使用列表推导和编写函数,更加熟悉 Python。
检查特征与响应变量之间的关系
为了准确预测响应变量,好的特征是必要的。我们需要那些与响应变量有明确联系的特征。到目前为止,我们已经通过计算特征和响应变量的groupby/mean值,或在模型中使用单独的特征并检查性能,来检查几个特征与响应变量之间的关系。然而,我们还没有系统地探讨所有特征与响应变量的关系。现在我们将进行这项工作,并开始利用我们在探索特征和确保数据质量时所付出的努力。
一种快速了解所有特征与响应变量之间关系,以及特征间相互关系的流行方法是使用相关性图。我们将首先为案例研究数据创建一个相关性图,然后讨论如何解读它,并提供一些数学细节。
为了创建相关性图,我们需要的输入包括我们计划探索的所有特征以及响应变量。由于我们将使用 DataFrame 中的大部分列名,获取适当列名列表的快速方法是在 Python 中从所有列名开始,然后移除那些我们不需要的列名。作为初步步骤,我们为本章启动一个新的笔记本,并加载第一章、数据探索与清理中的包和清理后的数据,代码如下:
import numpy as np #numerical computation
import pandas as pd #data wrangling
import matplotlib.pyplot as plt #plotting package
#Next line helps with rendering plots
%matplotlib inline
import matplotlib as mpl #add'l plotting functionality
import seaborn as sns #a fancy plotting package
mpl.rcParams['figure.dpi'] = 400 #high res figures
df = pd.read_csv('../../Data/Chapter_1_cleaned_data.csv')
注
您的清理数据文件的路径可能不同,取决于您在第一章、数据探索与清理中保存的位置。本节中提供的代码和输出也可以在参考笔记本中找到:packt.link/pMvWa。
注意,这个笔记本的开始与上一章的笔记本非常相似,唯一不同的是我们还导入了Seaborn包,它基于Matplotlib提供了许多方便的绘图功能。现在,让我们列出 DataFrame 中的所有列,并查看前五行和后五行:

图 3.1:获取列名列表
回想一下,由于伦理问题,我们不能使用gender变量,并且我们了解到PAY_2、PAY_3、…、PAY_6是错误的,应当忽略。此外,我们不会检查从EDUCATION变量创建的独热编码,因为这些列中的信息已经包含在原始特征中,至少以某种形式存在。我们将直接使用EDUCATION特征。最后,使用ID作为特征没有意义,因为它仅仅是一个唯一的账户标识符,与响应变量无关。我们需要列出那些既不是特征也不是响应变量的列名,并将它们从我们的分析中排除:
items_to_remove = ['ID', 'SEX',\
'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6',\
'EDUCATION_CAT',\
'graduate school', 'high school', 'none',\
'others', 'university']
为了拥有一个仅包含我们将使用的特征和响应的列名列表,我们需要从当前的features_response列表中删除items_to_remove中的列名。在 Python 中,有几种方法可以做到这一点。我们将利用这个机会学习一种特定的构建列表的方式,叫做列表推导式。当人们谈论某些构造是Pythonic或符合 Python 语言习惯时,列表推导式通常是其中之一。
什么是列表推导式?从概念上讲,它与for循环基本相同。然而,列表推导式使得可以将原本可能需要多行代码的for循环,用一行代码来实现。由于 Python 内置的优化,列表推导式通常比for循环稍微更快。虽然在这里这可能不会节省我们太多时间,但这是一个很好机会来熟悉它们。以下是一个列表推导式的例子:

图 3.2:列表推导示例
就是这么简单。
我们还可以使用其他子句来使列表推导更加灵活。例如,我们可以用它们重新赋值 features_response 变量,创建一个包含所有不在我们希望删除的字符串列表中的内容的列表:

图 3.3:使用列表推导修剪列名
在列表推导中使用 if 和 not in 是相当直观的。结构如列表推导的易读性是 Python 流行的原因之一。
注意
Python 文档(docs.python.org/3/tutorial/datastructures.html)将列表推导定义为如下内容:
“列表推导由包含一个表达式的括号组成,后面跟着一个 for 子句,然后是零个或多个 for 或 if 子句。”
因此,列表推导可以让你通过更少的代码来完成任务,而且通常非常易读和易理解。
Pearson 相关性
现在我们准备创建我们的相关性图。相关性图的基础是对这些列调用 .corr() 方法。在我们计算这个时,注意到在 pandas 中可用的相关性类型是线性相关性,也称为Pearson 相关性。Pearson 相关性用于衡量两个变量之间线性关系的强度和方向(即,正向或负向):

图 3.4:相关性矩阵的前五行和列
在创建相关性矩阵后,注意到行和列名是相同的。然后,对于所有特征对之间的每一个可能的比较,以及所有特征和响应之间的比较,虽然在这里的前五行和列中我们还看不到响应,但每个比较都有一个数值。这个数值被称为这两列之间的相关性。所有的相关性都在 -1 和 1 之间;一列与自身的相关性为 1(即相关性矩阵的对角线),并且存在重复:每个比较都会出现两次,因为原始 DataFrame 中的每个列名都会同时作为行和列出现在相关性矩阵中。在进一步讨论相关性之前,我们将使用 Seaborn 绘制一个漂亮的图表。以下是绘图代码,后面是输出(如果你是在黑白模式下阅读,请参阅 GitHub 上的笔记本中的彩色图形;在这里这是必要的 - packt.link/pMvWa):
sns.heatmap(corr,
xticklabels=corr.columns.values,
yticklabels=corr.columns.values,
center=0)
你应该看到以下输出:

图 3.5:Seaborn 中相关性图的热力图
Seaborn 的 heatmap 功能能够清晰地可视化相关矩阵,参照图 3.5右侧的颜色标尺,这个功能叫做 sns.heatmap。除了矩阵外,我们还为 x 和 y 轴提供了刻度标签,这些刻度标签分别代表特征和响应名称,并且指出颜色条的中心应该是 0,这样正相关和负相关就能分别以红色和蓝色区分开来。
注意
如果你正在阅读这本书的印刷版,你可以通过访问以下链接下载并浏览本章部分图像的彩色版本:packt.link/veMmT。
这个图表告诉我们什么?从整体来看,如果两个特征或一个特征与响应之间的相关性非常强,那么你可以说它们之间存在很强的关联。与响应变量高度相关的特征将是预测中很好的特征。这个强相关性可以是正相关也可以是负相关;我们稍后会解释两者的区别。
为了查看与响应变量的相关性,我们可以查看底部行,或者等价地,最后一列。在这里我们看到,PAY_1 特征可能是与响应变量最强相关的特征。我们还可以看到许多特征彼此之间高度相关,特别是 BILL_AMT 特征。我们将在下一章讨论彼此相关的特征的重要性;对于某些模型(例如逻辑回归)来说,这一点非常重要,因为这些模型假设特征之间存在相关性。目前,我们可以观察到,PAY_1 很可能是我们模型中最好的、最具预测力的特征。另一个看起来可能重要的特征是 LIMIT_BAL,它与响应变量呈负相关。根据你的观察能力,只有这两个特征在图 3.5的底部行中看起来有颜色(即不同于黑色,表示 0 相关性)。
线性相关性数学
从数学角度来说,什么是线性相关性?如果你学过基础统计学,你可能已经对线性相关性有所了解。线性相关性与线性回归非常相似。对于两列数据,X 和 Y,线性相关性 ρ(希腊字母“rho”)定义如下:

图 3.6:线性相关性方程
这个方程描述了 [-1, 1]。因为皮尔逊相关性已经调整了数据的均值和标准差,所以数据的实际值并不像 X 和 Y 之间的关系那么重要。较强的线性相关性越接近 1 或 -1。如果 X 和 Y 之间没有线性关系,相关性将接近 0。
值得注意的是,尽管数据科学从业者在此背景下经常使用皮尔逊相关性,但对于二元响应变量,它并不严格适用,就像我们在案例研究问题中所遇到的那样。从技术上讲,皮尔逊相关性在其他一些限制条件下,仅适用于连续数据,例如我们在第二章中使用的数据——Scikit-Learn 和模型评估入门。然而,皮尔逊相关性仍然可以快速提供特征潜在有用性的初步了解。它也方便地可以在如 pandas 等软件库中找到。
在数据科学领域,通常会发现某些广泛使用的技术可能被应用于违反其正式统计假设的数据。了解分析方法背后的正式假设是很重要的。事实上,这些假设的知识可能会在数据科学职位的面试中进行考察。然而,在实际操作中,只要某项技术能帮助我们理解问题并找到有效的解决方案,它仍然可以是一个有价值的工具。
话虽如此,线性相关性并不是所有特征预测能力的有效衡量标准。特别是,它只关注线性关系。稍微转移我们的焦点,假设我们正在处理一个回归问题,看看以下例子,并讨论你预期的线性相关性是什么。注意,数据的 x 轴和 y 轴上的值没有标签;这是因为数据的位置(均值)和标准差(尺度)并不影响皮尔逊相关性,只有变量之间的关系会影响相关性,这可以通过将它们一起绘制出来来辨别:

图 3.7:示例变量之间关系的散点图
对于示例 A和B,根据前面给出的公式,这些数据集的实际皮尔逊相关性分别为 0.96 和 -0.97。从图表上看,很明显,接近 1 或 -1 的相关性为我们提供了关于这些变量之间关系的有用见解。对于示例 C,相关性为 0.06。接近 0 的相关性看起来有效地表明这里没有关联:Y 的值似乎与 X 的值没有太大关系。然而,在示例 D中,变量之间显然存在某种关系。但线性相关性实际上比前一个例子低,为 0.02。在这里,X 和 Y 在较小的尺度上“共同变化”,但在计算线性相关性时,这种关系会被所有样本的平均值所抵消。
注意
生成本节及前一节中所示图表的代码可以在此找到:packt.link/XrUJU。
最终,任何你选择的汇总统计量(如相关系数)都只是汇总。它可能会隐藏重要的细节。因此,通常最好通过可视化来检查特征和响应之间的关系。这可能会占用页面上的大量空间,因此我们不会在案例研究中对所有特征进行演示。然而,pandas 和 Seaborn 都提供了创建散点图矩阵的功能。散点图矩阵类似于相关图,但它实际上显示了所有数据,以所有特征和响应变量的散点图网格的形式。这使你能够以简洁的格式直接检查数据。由于这可能包含大量数据和图表,你可能需要对数据进行降采样,并查看较少的特征,以便函数能够高效运行。
F 检验
虽然皮尔逊相关系数在理论上对于连续响应变量是有效的,但案例研究数据中的二元响应变量可以视为分类数据,只有两个类别:0 和 1。在我们可以进行的不同类型的检验中,用于查看特征是否与分类响应相关的是 f_classif 和 f_regression。
我们将使用候选特征对案例研究数据进行 ANOVA F 检验。你将看到输出包括 F 统计量以及p 值。我们该如何解释这些输出?我们将重点关注 p 值,原因将在练习中解释清楚。p 值是一个在多种统计测量中都很有用的概念。例如,虽然我们没有检查它们,但之前为相关矩阵计算的每个皮尔逊相关系数都有一个相应的 p 值。对于线性回归系数、逻辑回归系数及其他测量值,也有类似的 p 值概念。
在 F 检验的背景下,p 值回答了这个问题:“对于正类样本,特征的平均值与负类样本的平均值相同的可能性有多大?”如果数据表明某个特征在正负类样本之间的平均值非常不同,那么以下情况将成立:
-
这些平均值相同的可能性将非常低(低 p 值)。
-
这个特征可能是我们模型中的一个好特征,因为它有助于我们区分正负类样本。
在接下来的练习中,请牢记以下几点。
练习 3.01:F 检验和单变量特征选择
在本次练习中,我们将使用 F 检验来检查特征和响应变量之间的关系。我们将使用这种方法进行所谓的单变量特征选择:逐一检验特征与响应变量的关系,看看哪些特征具有预测能力。请按以下步骤完成练习:
注:
这个练习的 Jupyter 笔记本可以在这里找到:packt.link/ZDPYf。该笔记本还包含加载清洗过的数据和导入必要库的先决步骤。在执行本练习的第一步之前,应该先执行这些步骤。
-
进行 ANOVA F 检验的第一步是将特征和响应分离为 NumPy 数组,利用我们创建的列表以及 pandas 中的整数索引:
X = df[features_response].iloc[:,:-1].values y = df[features_response].iloc[:,-1].values print(X.shape, y.shape)输出应该显示特征和响应的形状:
(26664, 17) (26664, )有 17 个特征,并且特征和响应数组的样本数与预期相同。
-
导入
f_classif函数并传入特征和响应:from sklearn.feature_selection import f_classif [f_stat, f_p_value] = f_classif(X, y)f_classif有两个输出:F 统计量和p 值,用于比较每个特征与响应变量之间的关系。我们可以创建一个新的 DataFrame,包含特征名称以及这些输出,以便于检查。我们展示了按 p 值升序排序的 DataFrame。 -
使用以下代码创建一个包含特征名称、F 统计量和 p 值的 DataFrame,并按 p 值排序显示:
f_test_df = pd.DataFrame({'Feature':features_response[:-1], 'F statistic':f_stat, 'p value':f_p_value}) f_test_df.sort_values('p value')输出应如下所示:
![图 3.8:ANOVA F 检验结果]()
图 3.8:ANOVA F 检验结果
注意,每当 p 值降低时,F 统计量会增加,因此这些列中的信息在特征排名方面是相同的。
从 F 统计量和 p 值的 DataFrame 中得出的结论与我们在相关性图中观察到的相似:
PAY_1和LIMIT_BAL似乎是最有用的特征。它们的 p 值最小,表示这些特征的平均值是SelectPercentile类。还要注意,选择前*k*个特征(其中k是您指定的任何数字)有类似的类,称为SelectKBest。在这里,我们演示如何选择前 20%的特征。 -
要根据 F 检验选择前 20%的特征,首先导入
SelectPercentile类:from sklearn.feature_selection import SelectPercentile -
实例化该类的一个对象,表示我们希望使用与本练习中已经考虑过的相同特征选择标准——ANOVA F 检验,并且我们希望选择前 20%的特征:
selector = SelectPercentile(f_classif, percentile=20) -
使用
.fit方法对我们的特征和响应数据进行拟合,类似于模型拟合的方式:selector.fit(X, y)输出应该如下所示:
SelectPercentile(percentile=20)有几种方法可以直接访问所选特征,你可以在 scikit-learn 文档中了解(即
.transform方法,或与.fit_transform在同一步骤中使用)。然而,这些方法会返回 NumPy 数组,它们不会告诉你已选择的特征名称,只会给出特征的值。为此,你可以使用特征选择器对象的.get_support方法,它将返回所选特征数组的列索引。 -
将所选特征的索引捕获到一个名为
best_feature_ix的数组中:best_feature_ix = selector.get_support() best_feature_ix输出应如下所示,表示一个逻辑索引,可与特征名称数组及其对应的值一起使用,前提是它们与传递给
SelectPercentile的特征数组的顺序一致:array([ True, False, False, False, True, False, False, False, False, False, False, True, True, False, False, False, False]) -
可以通过索引
features_response列表中的:-1,获取除最后一个元素(name响应变量)外的所有特征名称:features = features_response[:-1] -
使用我们在第 7 步中创建的索引数组,通过列表推导式和
features列表,查找所选特征名称,如下所示:best_features = [features[counter] for counter in range(len(features)) if best_feature_ix[counter]] best_features输出应如下所示:
['LIMIT_BAL', 'PAY_1', 'PAY_AMT1', 'PAY_AMT2']在这段代码中,列表推导式通过
features数组中的元素数量(len(features))和counter循环递增,使用best_feature_ix布尔数组表示已选择的特征,在if语句中测试每个特征是否被选择,如果是,则捕获该特征的名称。所选特征与我们的 F 检验结果数据框的前四行一致,因此特征选择按预期工作。尽管从严格意义上讲不必两种方式都做,因为它们都会导致相同的结果,但检查你的工作是一个好习惯,尤其是在你学习新概念时。你应该意识到,使用像
SelectPercentile这样的便捷方法时,你无法看到 F 统计量或 p 值。然而,在某些情况下,使用这些方法可能更方便,因为 p 值的排名作用可能不是特别重要。
F 检验的细节:与两类 t 检验的等价性及注意事项
当我们使用 F 检验来查看仅两个组之间的均值差异时,正如我们在案例研究的二分类问题中所做的那样,实际上我们执行的检验会简化为t 检验。F 检验可以扩展到三个或更多组,因此对于多分类问题非常有用。t 检验仅比较两个样本组之间的均值,以查看这些均值之间的差异是否具有统计显著性。
虽然 F 检验在这里满足了我们的单变量特征选择目的,但仍有一些注意事项需要牢记。回到正式统计假设的概念,对于 F 检验,这些假设包括数据为 y,从矩阵 X 中提取了多个潜在特征,我们进行了统计学中所称的多重比较。简而言之,这意味着通过反复比较多个特征与同一响应的关系,我们找到“好特征”的机会会因为纯粹的随机机会而增加。然而,这些特征可能无法推广到新的数据。有针对多重比较的统计修正,即调整 p 值以考虑这一点。
即使我们没有遵循与这些方法相关的所有统计规则,我们仍然可以从中获得有用的结果。当 p 值是最终关注的量时,多重比较的修正会更为重要,例如在进行统计推断时。在这里,p 值只是对特征列表进行排序的一种手段。如果对 p 值进行了多重比较修正,排序的顺序不会发生变化。
除了了解哪些特征可能对建模有用外,深入理解重要特征也是很有必要的。因此,我们将在下一个练习中对这些特征进行详细的图形化探索。稍后我们还会查看其他特征选择方法,这些方法不作我们在此介绍的假设,并且与我们将要构建的预测模型更加直接集成。
假设和下一步
根据我们的单变量特征探索,和响应变量关联最强的特征是 PAY_1。这是否有意义?PAY_1的解释是什么?PAY_1是账户在最近一个月的付款状态。正如我们在最初的数据探索中所学到的,有些值表示账户状态良好:-2 表示未使用账户,-1 表示余额已全额支付,0 表示至少已支付最低金额。另一方面,正整数值表示延迟付款的月份数。上个月延迟付款的账户可以视为违约账户。实际上,这意味着该特征捕捉到了响应变量的历史值。像这样的特征非常重要,因为几乎任何机器学习问题中最好的预测因子之一就是关于你要预测的同一事物的历史数据(即响应变量)。这应该是合理的:曾经违约过的人可能是再次违约的最高风险群体。
LIMIT_BAL,账户的信用额度如何?考虑到信用额度的分配方式,我们的客户很可能在决定他们的信用额度时评估了借款人的风险。风险更高的客户应该被给予较低的限额,这样债权人的风险就较小了。因此,我们可能期望看到LIMIT_BAL较低值的账户的违约概率较高。
我们从我们的单变量特征选择练习中学到了什么?我们对我们模型中最重要的特征有了一个大致的了解。并且从相关矩阵中,我们对它们与响应变量的关系有了一些想法。然而,知道我们所使用的测试的限制是个好主意,最好将这些特征可视化,以更仔细地观察特征和响应变量之间的关系。我们还开始对这些特征发展了假设:我们为什么认为它们很重要?现在,通过可视化特征和响应变量之间的关系,我们可以确定我们的想法是否与数据中所见的相符。
这些假设和可视化通常是向客户展示结果的重要部分,客户可能对模型的工作方式感兴趣,而不仅仅是它能工作这个事实。
练习 3.02:可视化特征与响应变量之间的关系
在这个练习中,您将进一步了解本书中早期使用的 Matplotlib 绘图函数。您将学习如何自定义图形以更好地回答数据中的特定问题。随着您进行这些分析,您将创建关于PAY_1和LIMIT_BAL特征如何与响应变量相关的深刻可视化,这可能会支持您对这些特征形成的假设。这将通过更加熟悉 Matplotlib 应用程序编程接口(API)来完成,换句话说,您用来与 Matplotlib 交互的语法。执行以下步骤完成本练习:
注意
在开始本练习的第一步之前,请确保已导入必要的库并加载了正确的数据框架。您可以参考以下笔记本获取先决步骤以及此练习的代码:packt.link/DOrZ9。
-
使用 pandas 的
.mean()计算整个数据集中响应变量的违约率基线:overall_default_rate = df['default payment next month'].mean() overall_default_rate这个练习的输出应该如下所示:
0.2217971797179718如何有效地可视化
PAY_1特征不同值的违约率?回想一下我们之前的观察,这个特征有点像混合型的类别和数值型特征。由于独特值的数量相对较少,我们选择以典型的类别特征方式来绘制它。在 第一章,《数据探索与清洗》中,我们在数据探索中使用了
value_counts来查看这个特征的分布,之后我们学习了如何通过groupby和mean来查看EDUCATION特征的情况。groupby和mean也是一个很好的方式来可视化不同支付状态下的违约率。 -
使用这段代码来创建一个
groupby/mean聚合:group_by_pay_mean_y = df.groupby('PAY_1').agg({'下个月违约支付': np.mean})
group_by_pay_mean_y输出应该如下所示:
![图 3.9:按 PAY_1 特征分组的响应变量的均值]()
图 3.9:按 PAY_1 特征分组的响应变量的均值
看着这些值,你可能已经能够辨别出趋势了。我们直接开始绘制它们。我们将一步步进行,并介绍一些新的概念。你应该把从 步骤 3 到 步骤 6 的所有代码放在一个代码单元格中。
在 Matplotlib 中,每个图表都存在于一个坐标轴(axes)和一个
figure窗口中。通过创建axes和figure对象,你可以直接访问并修改它们的属性,包括坐标轴标签和坐标轴上的其他注释。 -
使用以下代码在一个名为
axes的变量中创建一个axes对象:axes = plt.axes() -
将整体违约率绘制为一条红色的水平线。
Matplotlib 使这变得简单;你只需要通过
axhline函数来指示该直线的 y 截距。注意,现在我们不是从plt调用这个函数,而是作为axes对象的方法来调用:axes.axhline(overall_default_rate, color='red')现在,我们希望在这条线的基础上,绘制每个
PAY_1值组内的违约率。 -
使用我们创建的分组数据的 DataFrame 的
plot方法。指定在线条图中包括一个'x'标记,不要有legend实例(我们稍后会创建它),并且该图的 父轴 应该是我们当前使用的轴(否则,pandas 会擦除已经存在的内容并创建新的轴):group_by_pay_mean_y.plot(marker='x', legend=False, ax=axes)这就是我们要绘制的所有数据。
-
设置 y 轴标签,并创建一个
legend实例(有许多控制图例外观的选项,但一种简单的方法是提供一个字符串列表,表示按添加到轴上的顺序排列的图形元素的标签):axes.set_ylabel('Proportion of credit defaults') axes.legend(['Entire dataset', 'Groups of PAY_1']) -
执行从 步骤 3 到 步骤 6 的所有代码,结果应该是以下图表:
![图 3.10:数据集中的信用违约率]()
图 3.10:数据集中的信用违约率
我们对支付状态的可视化揭示了一个明确的,并且可能是预期的故事:那些曾经违约的人实际上更可能再次违约。保持良好状态的账户的违约率远低于整体违约率,后者我们之前知道大约是 22%。然而,根据这一点,超过 30% 上个月处于违约状态的账户下个月仍然会处于违约状态。这是一个很好的可视化,值得与我们的业务合作伙伴分享,因为它展示了我们模型中可能是最重要的特征之一的影响。
现在,我们将注意力转向排名第二与目标变量关联最强的特征:
LIMIT_BAL。这是一个具有许多唯一值的数值特征。对于分类问题,查看此类特征的一个好方法是将多个直方图绘制在同一坐标轴上,为不同类别使用不同的颜色。作为区分类别的一种方式,我们可以使用逻辑数组从 DataFrame 中索引它们。 -
使用此代码为正类和负类样本创建逻辑掩码:
pos_mask = y == 1 neg_mask = y == 0为了创建我们的双重直方图,我们将再创建一个
axes对象,然后调用.hist方法分别为正类和负类直方图绘制两次。我们提供一些额外的关键字参数:第一个直方图将有黑色边缘和白色条形,而第二个将使用alpha来创建透明度,这样我们就可以看到两个直方图在重叠的地方。得到直方图后,我们旋转 x 轴刻度标签,使它们更易读,并创建一些其他自解释的注释。 -
使用以下代码来创建具有上述属性的双重直方图:
axes = plt.axes() axes.hist(df.loc[neg_mask, 'LIMIT_BAL'],\ edgecolor='black', color='white') axes.hist(df.loc[pos_mask, 'LIMIT_BAL'],\ alpha=0.5, edgecolor=None, color='black') axes.tick_params(axis='x', labelrotation=45) axes.set_xlabel('Credit limit (NT$)') axes.set_ylabel('Number of accounts') axes.legend(['Not defaulted', 'Defaulted']) axes.set_title('Credit limits by response variable')图表应该如下所示:
![图 3.11:信用额度的双重直方图]()
df['LIMIT_BAL'].max() -
使用以下代码创建并显示直方图的箱子边缘:
bin_edges = list(range(0,850000,50000)) print(bin_edges)输出应该如下所示:
[0, 50000, 100000, 150000, 200000, 250000, 300000, 350000, 40000, 450000, 500000, 550000, 600000, 650000, 700000, 750000, 800000]归一化直方图的绘图代码与之前类似,但有几个关键的变化:使用
bins关键字来定义箱子边缘的位置,density=True用于归一化直方图,并对绘图注释进行了一些更改。最复杂的部分是我们需要调整np.round,因为浮点数运算可能会有轻微的误差。 -
运行此代码以生成归一化的直方图:
mpl.rcParams['figure.dpi'] = 400 axes = plt.axes() axes.hist( df.loc[neg_mask, 'LIMIT_BAL'], bins=bin_edges, density=True, edgecolor='black', color='white') axes.hist( df.loc[pos_mask, 'LIMIT_BAL'], bins=bin_edges, density=True, alpha=0.5, edgecolor=None, color='black') axes.tick_params(axis='x', labelrotation=45) axes.set_xlabel('Credit limit (NT$)') axes.set_ylabel('Proportion of accounts') y_ticks = axes.get_yticks() axes.set_yticklabels(np.round(y_ticks*50000,2)) axes.legend(['Not defaulted', 'Defaulted']) axes.set_title('Normalized distributions of '\ 'credit limits by response variable')图表应该如下所示:
![图 3.12:归一化的双重直方图]()
图 3.12:归一化的双重直方图
你可以看到,Matplotlib 中的图表是高度可定制的。为了查看你可以从 Matplotlib 坐标轴中获取和设置的所有不同内容,可以查看这里:matplotlib.org/stable/api/axes_api.html。
我们从这个图表中能学到什么?看起来,违约的账户往往具有较高比例的低信用额度。信用额度低于新台币 150,000 元的账户更有可能违约,而对于信用额度高于这个数额的账户则相反。我们应该问自己,这是否有意义?我们的假设是,客户会给风险较高的账户设置较低的信用额度。这种直觉与我们在此观察到的低信用额度账户中违约者的较高比例相符。
根据模型构建的进展,如果我们在本次练习中检查的特征如我们预期的那样对预测建模非常重要,那么将这些图表展示给客户作为我们工作成果的一部分是很好的。这样可以让客户了解模型如何工作,以及对他们数据的洞察。
本节的一个重要学习点是,制作有效的视觉展示需要大量时间。最好在项目工作流程中预留一些时间用于此项工作。令人信服的视觉效果是值得付出努力的,因为它们应该能够迅速且有效地将重要的发现传达给客户。与其在制作材料时加入大量文字,视觉效果通常是更好的选择。定量概念的视觉传达是数据科学的核心技能。
单变量特征选择:它能做什么,不能做什么
在这一章中,我们学习了逐一查看特征以判断它们是否具有预测能力的技巧。这是一个良好的第一步,如果你已经有了对结果变量具有较强预测能力的特征,你可能不需要花太多时间考虑其他特征,便可以进行建模。然而,单变量特征选择也有其缺点,特别是它没有考虑特征之间的相互作用。例如,如果信用违约率特别高的是那些既有某种教育水平又有一定范围的信用额度的人群怎么办?
此外,我们在这里使用的方法仅能捕捉特征的线性效应。如果某个特征在经历某种变换(如多项式或对数变换,或者分箱(离散化))后能更好地预测,单变量特征选择的线性方法可能就不太有效。相互作用和变换是特征工程的例子,或者说,在这些情况下通过现有特征创建新特征。线性特征选择方法的不足可以通过非线性建模技术来弥补,包括决策树及其相关方法,我们将在后续进行讨论。但从简单关系入手,寻找那些可以通过线性方法实现的单变量特征选择,依然具有价值,而且这种方法非常迅速。
使用 Python 函数语法理解逻辑回归和 Sigmoid 函数
在这一部分,我们将全面揭开逻辑回归的“黑箱”:我们将全面理解它是如何工作的。我们将从介绍一个新的编程概念:函数开始。同时,我们将学习一个数学函数——sigmoid 函数,它在逻辑回归中起着关键作用。
从最基本的角度来看,计算机编程中的函数是一段接受输入并产生输出的代码。你在本书中已经使用了函数:这些函数是由别人编写的。每次你使用类似于这样的语法:output = do_something_to(input)时,你实际上就是在使用一个函数。例如,NumPy 有一个函数可以用来计算输入的均值:
np.mean([1, 2, 3, 4, 5])
3.0
函数抽象了正在执行的操作,以便在我们的示例中,每次需要计算均值时,你不需要看到所有执行此操作的代码行。对于许多常见的数学函数,像 NumPy 这样的包中已经有了预定义的版本。你无需“发明轮子”。流行包中的实现之所以流行,可能是因为人们花了时间思考如何以最有效的方式创建它们。因此,使用它们是明智的。然而,由于我们使用的所有包都是开源的,如果你有兴趣查看我们使用的库中函数的实现,你可以查看它们的代码。
现在,为了说明,我们通过编写自己的算术平均数函数来学习 Python 函数语法。Python 中的函数语法类似于for或if块,其中函数体是缩进的,函数声明后面跟着一个冒号。下面是一个计算均值的函数代码:
def my_mean(input_argument):
output = sum(input_argument)/len(input_argument)
return(output)
在你执行了包含此定义的代码单元后,函数在笔记本中的其他代码单元中可以使用。举个例子:
my_mean([1, 2, 3, 4, 5])
3.0
定义函数的第一部分,如这里所示,是以def开始一行代码,后面跟一个空格,然后是你想为函数命名的名称。接下来是括号,括号内指定函数的参数名称。参数是输入变量的名称,这些名称是函数体内部的:作为参数定义的变量在函数被调用(使用)时可用,但在函数外部不可用。可以有多个参数;它们之间用逗号分隔。括号后跟冒号。
函数体是缩进的,可以包含对输入进行操作的任何代码。操作完成后,最后一行应以return开头,并包含输出变量,如果有多个输出变量,它们之间用逗号分隔。在这段非常简单的函数介绍中,我们省略了许多细节,但这些是你开始使用函数时需要掌握的基本部分。
函数的威力在于它的使用。注意,我们定义了函数后,在一个单独的代码块中,我们可以通过给定的名称 调用 它,它会对我们 传递 的输入进行操作。就像我们把所有代码复制粘贴到这个新位置一样,但看起来比实际复制粘贴要整洁得多。如果你需要多次使用相同的代码,函数可以大大减少代码的整体长度。
作为一个简短的补充说明,你可以选择明确指定输入参数的名称,这样在有多个输入时会更清晰:
my_mean(input_argument=[1, 2, 3])
2.0
现在我们已经熟悉了 Python 函数的基础知识,接下来我们将讨论一个对逻辑回归非常重要的数学函数,叫做 Sigmoid 函数。这个函数也可以称为 逻辑函数。Sigmoid 的定义如下:

图 3.13:Sigmoid 函数
我们将分解这个函数的不同部分。正如你所看到的,sigmoid 函数涉及到 exp,它会自动将 e 提供给输入指数。如果你查看文档,你会看到这个过程叫做取“指数”,这听起来有点模糊。但通常理解的是,这里指数的底数是 e。一般来说,如果你想在 Python 中计算指数,比如 23(“2 的三次方”),语法是两个星号:2**3,例如结果为 8。
考虑如何将输入传递给 np.exp 函数。由于 NumPy 的实现是 向量化 的,这个函数可以接受单个数字,也可以接受数组或矩阵作为输入。为了说明单个参数,我们计算了 1 的指数,这显示了 e 的近似值,以及 e0,它当然等于 1,就像任何底数的零次方一样:
np.exp(1)
2.718281828459045
np.exp(0)
1.0
为了说明 np.exp 的向量化实现,我们使用 NumPy 的 linspace 函数创建一个数字数组。这个函数输入一个范围的起始和结束点(包括这两个点),以及该范围内你希望包含的值的数量,以创建一个等间距的数组。这个函数的作用与 Python 的 range 类似,但还可以生成小数值:

图 3.14:使用 np.linspace 创建数组
由于 np.exp 是向量化的,它会一次性高效地计算整个数组的指数。这里是计算我们 X_exp 数组的指数并检查前五个值的代码和输出:

图 3.15:NumPy 的 exp 函数
练习 3.03:绘制 Sigmoid 函数
在本练习中,我们将使用先前创建的 X_exp 和 Y_exp,绘制指数函数在区间 [-4, 4] 上的图形。你需要先运行 图 3.14 和 图 3.15 中的所有代码,以便为本练习提供这些变量。接下来,我们将定义一个 sigmoid 函数,绘制它的图形,并考虑它与指数函数的关系。执行以下步骤以完成此练习:
注意
在开始本练习的第一步之前,请确保你已导入必要的库。导入库的代码以及练习其余步骤的代码可以在这里找到:packt.link/Uq012。
-
使用此代码绘制指数函数:
plt.plot(X_exp, Y_exp) plt.title('Plot of $e^X$')图形应该是这样的:
![图 3.14:使用 np.linspace 创建一个数组]()
图 3.16:绘制指数函数
注意,在图表标题中,我们利用了一种叫做
^的语法。还要注意在 图 3.16 中,许多紧密间隔的点创造出平滑曲线的效果,但实际上它是由离散点通过线段连接而成的图形。
我们可以从指数函数中观察到什么?
它永远不会是负数:当 X 趋近于负无穷时,Y 趋近于 0。
随着 X 的增加,Y 起初增长缓慢,但很快就“爆炸”了。这就是人们所说的“指数增长”来表示快速增长的含义。
你如何从指数函数的角度理解 sigmoid?
首先,sigmoid 涉及 e-X,而不是 eX。e-X 的图形只是 eX 关于 y 轴的反射。这可以很容易地绘制出来,并在图表标题中使用大括号标注多字符的上标。
-
运行此代码以查看 e-X 的图形:
Y_exp = np.exp(-X_exp) plt.plot(X_exp, Y_exp) plt.title('Plot of $e^{-X}$')输出应该是这样的:
![图 3.17:exp(-X) 的图]()
图 3.17:exp(-X) 的图
现在,在 sigmoid 函数中,e-X 位于分母,且加上了 1。分子是 1。那么,当 X 趋近于负无穷时,sigmoid 会发生什么呢?我们知道 e-X 会“爆炸”,变得非常大。总的来说,分母变得非常大,分数接近于 0。那么,当 X 增加到正无穷时,会怎样呢?我们可以看到 e-X 会变得非常接近于 0。所以,在这种情况下,sigmoid 函数大约等于 1/1 = 1。这应该给你一个直觉,sigmoid 函数始终保持在 0 和 1 之间。现在让我们在 Python 中实现一个 sigmoid 函数,并用它绘制一个图形,看看现实与这个直觉如何匹配。
-
定义一个 sigmoid 函数,如下所示:
def sigmoid(X): Y = 1 / (1 + np.exp(-X)) return Y -
扩大 x 值的范围以绘制 sigmoid 图。使用以下代码:
X_sig = np.linspace(-7,7,141) Y_sig = sigmoid(X_sig) plt.plot(X_sig,Y_sig) plt.yticks(np.linspace(0,1,11)) plt.grid() plt.title('The sigmoid function')图形应该是这样的:
![图 3.18:一个 sigmoid 函数图]()
图 3.18:一个 sigmoid 函数图
这个图与我们预期的结果一致。此外,我们可以看到sigmoid(0) = 0.5。那么,sigmoid 函数有什么特别之处?这个函数的输出严格地限制在 0 和 1 之间。对于一个应该预测概率的函数来说,这个特性非常好,因为概率值也必须在 0 和 1 之间。技术上,概率值可以恰好等于 0 和 1,而 sigmoid 函数永远不会是。但 sigmoid 函数的值可以接近 0 或 1,这在实际中并不是一个限制。
回顾一下,我们将逻辑回归描述为生成预测的类别概率,而不是直接预测类别成员资格。这使得逻辑回归的实现更加灵活,可以选择阈值概率。sigmoid 函数是这些预测概率的来源。稍后,我们将看到不同特征是如何用于计算预测概率的。
函数的作用域
当你开始使用函数时,你应该对sigmoid函数的概念有所了解,我们在函数内部创建了一个变量Y。在函数内部创建的变量与在函数外部创建的变量不同。它们在函数被调用时会在函数内部有效地创建和销毁。这些变量在使用sigmoid函数后被称为Y变量:

图 3.19:Y 变量不在笔记本的作用域内
Y变量不在笔记本的全局作用域中。然而,在函数外部创建的全局变量可以在函数的局部作用域内使用,即使它们没有作为参数传递给函数。在这里,我们展示了如何在函数外部创建一个全局变量,然后在函数内部访问它。这个函数实际上没有接受任何参数,但如你所见,它可以使用全局变量的值来生成输出:

图 3.20:全局变量在函数的局部作用域中可用
注意
更多关于作用域的细节
变量的作用域可能会让人感到困惑,但当你开始更高级地使用函数时,了解作用域是非常有益的。虽然本书中并不要求了解这些内容,但你可能希望在这里了解更多关于 Python 中变量作用域的深度知识:nbviewer.jupyter.org/github/rasbt/python_reference/blob/master/tutorials/scope_resolution_legb_rule.ipynb。
sigmoid 曲线在科学应用中的应用
除了在逻辑回归中是基础,S 型曲线还广泛应用于各种领域。在生物学中,它们可以用来描述生物体的生长过程,首先是缓慢开始,然后进入快速阶段,最后以平滑的方式逐渐趋于稳定,直到最终大小达到。S 型曲线也可以用来描述人口增长,其轨迹类似,快速增长后会减慢,直到环境的承载能力达到。
为什么逻辑回归被认为是线性模型?
我们之前提到,逻辑回归被认为是第一章《数据探索与清洗》中EDUCATION特征的groupby/mean,以及本章中PAY_1特征的groupby/mean,用来查看这些特征值之间的违约率是否表现出线性趋势。虽然这是一种快速近似判断这些特征是否“线性”的好方法,但在这里我们将形式化为什么逻辑回归是线性模型的概念。
如果计算预测所使用的特征变换是特征的线性组合,则该模型被认为是线性的。线性组合的可能性是每个特征可以乘以一个数值常数,这些项可以相加,并且可以添加一个额外的常数。例如,在一个简单的包含两个特征的模型中,X1 和 X2,线性组合的形式如下:

图 3.21:X1 和 X2 的线性组合
常数𝜃i 可以是任何数值,正数、负数或零,i = 0, 1, 和 2(尽管如果某个系数为 0,这将从线性组合中移除一个特征)。一个常见的线性变换的例子是单变量的直线方程 y = mx + b,如在第二章《Scikit-Learn 入门与模型评估》中讨论的那样。在这种情况下,𝜃o = b,𝜃1 = m。𝜃o 被称为线性组合的截距,这在代数中应该是熟悉的概念。
在线性变换中“禁止”哪些操作?除了刚才描述的内容之外,任何其他数学表达式,如下所示:
-
将特征与自身相乘;例如,X12 或 X13。这被称为多项式项。
-
将特征相乘;例如,X1X2。这被称为交互作用。
-
对特征应用非线性变换;例如,对数和平方根。
-
其他复杂的数学函数。
-
"如果...那么..."类型的语句。例如,"如果 X1 > a,那么 y = b"。
然而,虽然这些变换不是线性组合的基本形式,但它们可以通过特征工程添加到线性模型中,例如定义一个新特征,X3 = X12。
之前我们学过,逻辑回归的预测结果是概率形式,它们是通过 sigmoid 函数得出的。再看这里,我们可以清楚地看到这个函数是非线性的:

图 3.22:非线性 sigmoid 函数
那么,为什么逻辑回归被认为是线性模型呢?事实证明,答案在于 sigmoid 方程的另一种表述形式,即logit函数。我们可以通过求解 sigmoid 函数的X来推导出logit函数;换句话说,就是找到 sigmoid 函数的逆函数。首先,我们将 sigmoid 设为p,我们将其解释为观察到正类的概率,然后按如下方式求解X:

图 3.23:求解 X
在这里,我们利用了幂和对数的一些法则来求解X。你可能还会看到logit以以下形式表示:

图 3.24:logit 函数
在这个表达式中,logit函数也被称为对数几率,因为它是几率比的自然对数,p/q。几率比在赌博世界中可能比较常见,例如“a队战胜b队的几率是 2 比 1”。
一般来说,我们在这些操作中所称的大写X可以代表所有特征的线性组合。例如,在我们这个简单的包含两个特征的案例中,X = 𝜃o + 𝜃1X1 + 𝜃2X2。逻辑回归被认为是线性模型,因为在考虑响应变量为对数几率时,包含在X中的特征实际上仅受限于线性组合。这是与 sigmoid 方程相比的一种问题表述方式。
将各部分组合在一起,特征X1、X2、…、Xj 在 sigmoid 方程版逻辑回归中是这样的:

图 3.25:sigmoid 版逻辑回归
但在对数几率版本中它们看起来是这样的,这也是为什么逻辑回归被称为线性模型:

图 3.26:对数几率版逻辑回归
因此,从这种角度看待逻辑回归时,理想情况下,逻辑回归模型的特征应该是在响应变量的对数几率上是线性的。我们将在接下来的练习中看到这一点是什么意思。
逻辑回归是统计模型的一个更广泛类别,称为广义线性模型(GLMs)。广义线性模型与普通线性回归的基本概念有关,普通线性回归可能只有一个特征(即 最佳拟合线,y = mx + b,对于单个特征 x)或多个特征,称为多元线性回归。广义线性模型与线性回归之间的数学联系是连接函数。逻辑回归的连接函数是我们刚刚学习的对数几率函数。
练习 3.04:检查特征在逻辑回归中的适用性
在 练习 3.02,可视化特征与响应变量之间的关系 中,我们绘制了可能是模型中最重要特征之一的 PAY_1 特征的 groupby/mean。通过按 PAY_1 的值对样本进行分组,并查看响应变量的平均值,我们实际上是在查看每个组内的违约概率,p。
在本练习中,我们将评估PAY_1在逻辑回归中的适用性。我们通过检查这些组内的违约对数赔率来判断响应变量是否在对数赔率中是线性的,正如逻辑回归所正式假设的那样。完成以下步骤以完成本练习:
注意
在开始本练习的第 1 步之前,确保已经导入了必要的库。你可以参考以下笔记本中的先决步骤:packt.link/gtpF9。
-
确认你仍然可以访问 练习 3.02,可视化特征与响应变量之间的关系 中的变量,在笔记本中查看
PAY_1不同值下响应变量的平均值的 DataFrame,使用以下代码:group_by_pay_mean_y输出应该如下所示:
![图 3.27:按 PAY_1 值分组的违约率作为违约概率]()
图 3.27:按 PAY_1 值分组的违约率作为违约概率
-
从这些组中提取响应变量的平均值,并将其放入一个变量
p中,表示违约的概率:p = group_by_pay_mean_y['default payment next month'].values -
创建一个不违约的概率
q。由于这是一个二元问题,且所有结果的概率和始终为 1,因此很容易计算出q。同时打印出p和q的值以确认:q = 1-p print(p) print(q)输出应该如下所示:
![图 3.28:从 p 计算 q]()
图 3.28:从 p 计算 q
-
使用 NumPy 的自然对数函数从
p和q计算赔率比和对数赔率:odds_ratio = p/q log_odds = np.log(odds_ratio) log_odds输出应该如下所示:
![图 3.29:赔率比和对数赔率]()
图 3.29:赔率比和对数赔率
-
为了绘制对数几率与特征值的关系,我们可以从包含
groupby/mean的 DataFrame 的索引中获取特征值。你可以这样显示索引:group_by_pay_mean_y.index这应该会产生以下输出:
Int64Index([-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8], dtype='int64', name='PAY_1') -
创建一个类似我们已经做过的图表,展示对数几率与特征值之间的关系。以下是代码:
plt.plot(group_by_pay_mean_y.index, log_odds, '-x') plt.ylabel('Log odds of default') plt.xlabel('Values of PAY_1')图表应该如下所示:
![图 3.30:PAY_1 值的违约对数几率]()
图 3.30:PAY_1 值的违约对数几率
我们可以从这个图中看到,响应变量的对数几率与PAY_1特征之间的关系,与我们在练习 3.02中绘制的违约率与该特征之间的关系几乎没有什么不同,可视化特征与响应变量之间的关系。因此,如果“违约率”是一个更易于向业务伙伴传达的概念,那么它可能更合适。然而,在理解逻辑回归的工作原理方面,这个图正好展示了假定为线性的内容。
直线拟合是否是此数据的良好模型?
看起来在这个图中,画一条“最佳拟合线”应该是从左下角到右上角的。同时,这些数据似乎并不会导致一个真正的线性过程。我们可以通过一种方式来看待这些数据,即-2、-1 和 0 的值似乎落在了一个与其他值不同的对数几率范围内。PAY_1 = 1处于一个中间值,而其余的值则较大。也许基于此变量的工程特征,或者以不同方式编码-2、-1 和 0 所代表的类别,会对建模更有效。随着我们继续使用逻辑回归建模这些数据,并在本书后面介绍其他方法时,记住这一点。
从逻辑回归系数到使用 Sigmoid 的预测
在下一个练习之前,我们先来看一下逻辑回归的系数是如何用来计算预测概率的,最终为响应变量的类别做出预测的。
回忆一下,逻辑回归根据 Sigmoid 方程预测类成员的概率。在具有截距的两个特征的情况下,方程如下:

图 3.31:Sigmoid 函数预测两个特征的类成员概率
当你使用训练数据调用 scikit-learn 中逻辑回归模型对象的.fit方法时,𝜃0、𝜃1 和𝜃2 参数(截距和系数)是从这些有标签的训练数据中估计出来的。实际上,scikit-learn 会计算出如何选择𝜃0、𝜃1 和𝜃2 的值,以便尽可能准确地分类尽可能多的训练数据点。我们将在下一章中深入了解这个过程是如何运作的。
当你调用 .predict 时,scikit-learn 会根据拟合的参数值和 sigmoid 方程计算预测的概率。给定的样本将在 p ≥ 0.5 时被分类为正类,否则为负类。
我们知道,sigmoid 方程的图像如下所示,我们可以通过将 X = 𝜃0 + 𝜃1X1 + 𝜃2X2 代入 图 3.31 中的方程来进行连接:

图 3.32:预测与真实类别一起绘制
请注意,如果 X = 𝜃o + 𝜃1X1 + 𝜃2X2 ≥ 0 时,在 x 轴上,则预测的概率为 p ≥ 0.5,该样本将被分类为正类。否则,p < 0.5,样本将被分类为负类。我们可以利用这个观察结果,计算出一个线性条件来进行正向预测,基于 X1 和 X2 特征,使用系数和截距。解这个不等式,得到类似于 y = mx + b 形式的线性不等式:X2 ≥ -(**𝜃1/**𝜃2)X1 - (**𝜃o/**𝜃2)。
这将帮助在以下练习中看到逻辑回归的线性决策边界在 X1-X2 特征空间 中的表现。
我们现在已经从理论和数学的角度了解了为什么逻辑回归被认为是一个线性模型。我们还检查了单个特征,并考虑了线性假设是否合理。理解线性假设也很重要,这涉及到我们可以期望逻辑回归具有多大的灵活性和强大性。我们将在接下来的练习中探讨这一点。
练习 3.05:逻辑回归的线性决策边界
在这个练习中,我们展示了二分类问题的决策边界的概念。我们使用合成数据创建一个清晰的示例,展示逻辑回归的决策边界与训练样本的对比。我们首先随机生成两个特征,X1 和 X2。由于有两个特征,我们可以说这个问题的数据是二维的,这使得可视化变得容易。我们在这里展示的概念可以推广到更多特征的情况,比如你在工作中可能会遇到的真实世界数据集;然而,决策边界在高维空间中更难以可视化。
执行以下步骤以完成练习:
注意
在开始此练习的第 1 步之前,请确保你已导入必要的库。你可以参考以下笔记本,获取前置步骤:packt.link/35ge1。
-
使用以下代码生成特征:
from numpy.random import default_rng rg = default_rng(4) X_1_pos = rg.uniform(low=1, high=7, size=(20,1)) print(X_1_pos[0:3]) X_1_neg = rg.uniform(low=3, high=10, size=(20,1)) print(X_1_neg[0:3]) X_2_pos = rg.uniform(low=1, high=7, size=(20,1)) print(X_2_pos[0:3]) X_2_neg = rg.uniform(low=3, high=10, size=(20,1)) print(X_2_neg[0:3])你不需要过于担心我们选择这些值的原因;我们稍后绘制的图形应该能够让你理解。不过要注意,我们同时分配了真实类别,通过定义哪些点(
X1, X2)将属于正类和负类,来进行这一操作。这样,我们就得到了每个类别各 20 个样本,总共 40 个样本,并且每个样本有两个特征。我们展示了正类和负类每个特征的前三个值。输出应为以下内容:
![图 3.33:生成二分类问题的合成数据]()
图 3.33:生成二分类问题的合成数据
-
绘制这些数据,将正样本用红色方块表示,负样本用蓝色 x 形表示。绘图代码如下:
plt.scatter(X_1_pos, X_2_pos, color='red', marker='s') plt.scatter(X_1_neg, X_2_neg, color='blue', marker='x') plt.xlabel(‚$X_1$') plt.ylabel(‚$X_2$') plt.legend(['Positive class', 'Negative class'])结果应该如下所示:
![图 3.34:生成二分类问题的合成数据]()
图 3.34:生成二分类问题的合成数据
为了将我们的合成特征与 scikit-learn 一起使用,我们需要将它们组装成一个矩阵。我们使用 NumPy 的
block函数来创建一个 40x2 的矩阵。因为总共有 40 个样本,所以会有 40 行,且每个样本有两个特征,因此有 2 列。我们会将正类样本的特征放在前 20 行,负类样本的特征放在后 20 行。 -
创建一个 40x2 的矩阵,然后显示其形状和前三行:
X = np.block([[X_1_pos, X_2_pos], [X_1_neg, X_2_neg]]) print(X.shape) print(X[0:3])输出应该如下所示:
(40, 2) [[6.65833663 5.15531227] [4.06796532 5.6237829 ] [6.85746223 2.14473103]]我们还需要一个响应变量来与这些特征配合使用。我们已经知道它们是如何定义的,但我们还需要一个
y数组来告诉 scikit-learn。 -
创建一个垂直堆叠(
vstack)的 20 个 1 和 20 个 0,以匹配我们特征的排列方式,并重新调整形状以符合 scikit-learn 的要求。代码如下:y = np.vstack((np.ones((20,1)), np.zeros((20,1)))).reshape(40,) print(y[0:5]) print(y[-5:])你将获得以下输出:
[1\. 1\. 1\. 1\. 1.] [0\. 0\. 0\. 0\. 0.]目前,我们准备使用 scikit-learn 来拟合一个逻辑回归模型。我们将使用所有数据作为训练数据,并观察线性模型如何拟合这些数据。接下来的几个步骤应该与你在前几章学习的如何实例化模型类并拟合模型的内容相似。
-
首先,使用以下代码导入模型类:
from sklearn.linear_model import LogisticRegression -
现在实例化模型,指定使用
liblinear求解器,并使用以下代码显示模型对象:example_lr = LogisticRegression(solver='liblinear') example_lr输出应该如下所示:
LogisticRegression(solver='liblinear')我们将在 第四章《偏差-方差权衡》中讨论 scikit-learn 中不同求解器的使用方法,但现在我们将使用这个求解器。
-
现在在合成数据上训练模型:
example_lr.fit(X, y)对我们用于模型训练的相同样本应用
.predict方法。然后,为了将这些预测添加到图中,我们将根据预测值是 1 还是 0,创建两个索引列表以与数组一起使用。看看你是否能理解我们是如何使用列表推导式,包括if语句,来完成这一步的。 -
使用此代码获取预测值,并将其分离为正类和负类的索引。显示正类预测的索引作为检查:
y_pred = example_lr.predict(X) positive_indices = [counter for counter in range(len(y_pred)) if y_pred[counter]==1] negative_indices = [counter for counter in range(len(y_pred)) if y_pred[counter]==0] positive_indices输出应该如下所示:
[2, 3, 4, 5, 6, 7, 9, 11, 13, 15, 16, 17, 18, 19, 26, 34, 36]从正预测的索引,我们可以已经看出,并非训练数据中的每个样本都被正确分类:正样本是前 20 个样本,但这里有超出这个范围的索引。你可能已经猜到,线性决策边界无法完美地对这些数据进行分类,因为观察它后会得出这个结论。现在,让我们将这些预测显示在图上,形式为每个数据点周围的方框和圆圈,分别按照正负预测进行着色:红色表示正类,蓝色表示负类。
你可以比较内层符号的颜色和形状(数据的真实标签)与外层符号(预测标签),以查看哪些点被正确分类,哪些点被错误分类。
-
这是绘图代码:
plt.scatter(X_1_pos, X_2_pos, color='red', marker='s') plt.scatter(X_1_neg, X_2_neg, color='blue', marker='x') plt.scatter(X[positive_indices,0], X[positive_indices,1], s=150, marker='s', edgecolors='red', facecolors='none') plt.scatter(X[negative_indices,0], X[negative_indices,1], s=150, marker='o', edgecolors='blue', facecolors='none') plt.xlabel('$X_1$') plt.ylabel('$X_2$') plt.legend(['Positive class', 'Negative class',\ 'Positive predictions', 'Negative predictions'])绘图应该如下所示:
![图 3.35:预测值和真实类别一起绘制]()
图 3.35:预测值和真实类别一起绘制
从图中可以看出,分类器在靠近线性决策边界位置的数据点上表现不佳;其中一些可能会出现在边界的另一侧。我们怎么做才能找出并可视化决策边界的实际位置呢?从前一节中,我们知道可以通过不等式 X2 ≥ -(**𝜃1/**𝜃2)X1 - (**𝜃0/**𝜃2) 来获取逻辑回归的决策边界,在二维特征空间中。既然我们已经拟合了模型,就可以检索 𝜃1 和 𝜃2 的系数,以及 𝜃0 的截距,将这些值代入方程并绘制图表。
-
使用此代码从拟合的模型中获取系数并打印它们:
theta_1 = example_lr.coef_[0][0] theta_2 = example_lr.coef_[0][1] print(theta_1, theta_2)输出应该如下所示:
-0.16472042583006558 -0.25675185949979507 -
使用此代码获取截距:
theta_0 = example_lr.intercept_现在使用系数和截距来定义线性决策边界。这个边界捕捉了不等式的分界线,X2 ≥ -(**𝜃1/**𝜃2)X1 - (**𝜃0/**𝜃2):
X_1_decision_boundary = np.array([0, 10]) X_2_decision_boundary = -(theta_1/theta_2)*X_1_decision_boundary\ - (theta_0/theta_2)总结最后几步,在使用
.coef_和.intercept_方法来获取 𝜃1 和 𝜃2 的模型系数以及 𝜃0 的截距之后,我们使用这些值根据我们描述的决策边界方程创建了一条由两点定义的线。 -
使用以下代码绘制决策边界,并做一些调整以分配正确的标签用于图例,同时将图例移动到图外的某个位置(
loc),避免图表拥挤:pos_true = plt.scatter(X_1_pos, X_2_pos, color='red', marker='s', label='Positive class') neg_true = plt.scatter(X_1_neg, X_2_neg, color='blue', marker='x', label='Negative class') pos_pred = plt.scatter(X[positive_indices,0], X[positive_indices,1], s=150, marker='s', edgecolors='red', facecolors='none', label='Positive predictions') neg_pred = plt.scatter(X[negative_indices,0], X[negative_indices,1], s=150, marker='o', edgecolors='blue', facecolors='none', label='Negative predictions') dec = plt.plot(X_1_decision_boundary, X_2_decision_boundary, 'k-', label='Decision boundary') plt.xlabel('$X_1$') plt.ylabel('$X_2$') plt.legend(loc=[0.25, 1.05])你将获得以下图形:
![图 3.36:真实类别、预测类别和决策边界 逻辑回归的]()
图 3.36:逻辑回归的真实类别、预测类别和决策边界
决策边界的位置与你原本预想的有何不同?
你能看出线性决策边界永远无法完美分类这些数据吗?
为了绕过这个问题,我们可以从现有特征中创建工程特征,例如多项式或交互特征,以便在逻辑回归中允许更复杂的非线性决策边界。或者,我们可以使用非线性模型,如随机森林,后者也能实现这一点,稍后我们会看到。
最后需要注意的是,由于只有两个特征,这个例子可以很容易地在二维中进行可视化。通常,决策边界可以通过超平面来描述,它是直线在多维空间中的推广。然而,线性决策边界的限制性质仍然是超平面的一个因素。
活动 3.01:拟合逻辑回归模型并直接使用系数
在这个活动中,我们将训练一个逻辑回归模型,使用在单变量特征探索中发现的两个最重要的特征,并学习如何使用拟合模型中的系数手动实现逻辑回归。这将展示如何在没有 scikit-learn 环境的计算环境中使用逻辑回归,但可以计算 sigmoid 函数所需的数学函数。成功完成该活动后,你应该会发现,使用 scikit-learn 预测和手动预测计算的 ROC AUC 值应该是相同的:大约为 0.63。
执行以下步骤完成活动:
-
创建一个训练/测试划分(80/20),以
PAY_1和LIMIT_BAL作为特征。 -
导入
LogisticRegression,使用默认选项,但将求解器设置为'liblinear'。 -
在训练数据上训练,并使用测试数据获取预测类别及类别概率。
-
从训练好的模型中提取系数和截距,并手动计算预测概率。你需要向特征中添加一列 1,以便与截距相乘。
-
使用
0.5的阈值,手动计算预测类别。与 scikit-learn 输出的类别预测进行比较。 -
使用 scikit-learn 的预测概率和手动预测的概率计算 ROC AUC,并进行比较。
注意
包含此活动代码的 Jupyter 笔记本可以在这里找到:
packt.link/4FHec。该笔记本仅包含 Python 代码及相应输出。完整的逐步解决方案可以通过此链接找到。
总结
在本章中,我们学习了如何逐一探索特征,使用包括皮尔逊相关系数和 ANOVA F 检验在内的单变量特征选择方法。虽然以这种方式查看特征并不总是能揭示完整的故事,因为可能会忽略特征间的重要交互作用,但这通常是一个有用的步骤。理解最具预测性的特征与响应变量之间的关系,并围绕它们创建有效的可视化,是向客户传达你的发现的好方法。我们使用了定制的图形,比如使用Matplotlib创建的重叠直方图,来可视化最重要的特征。
然后我们开始深入描述逻辑回归是如何工作的,探讨了如sigmoid函数、对数几率和线性决策边界等话题。虽然逻辑回归是最简单的分类模型之一,且通常不如其他方法强大,但它是应用最广泛的模型之一,并且是深度神经网络等更复杂分类模型的基础。因此,详细理解逻辑回归将帮助你在探索机器学习的更高级话题时提供帮助。而在某些情况下,简单的逻辑回归可能是唯一需要的模型。综合考虑所有因素,满足要求的最简单模型可能就是最好的模型。
如果你掌握了本章和下一章的内容,你将为在工作中使用逻辑回归做好充分准备。在下一章,我们将在这里学到的基础上进一步探讨,了解如何估计逻辑回归的系数,以及如何在特征数量较多的情况下有效使用逻辑回归,并用于特征选择。
第四章:4. 偏差-方差权衡
概述
本章将介绍逻辑回归的剩余部分,包括调用.fit训练模型时发生的事情,以及在使用此建模技术时应该注意的统计假设。你将学习如何在逻辑回归中使用 L1 和 L2 正则化来防止过拟合,并了解如何使用交叉验证实践来决定正则化的强度。阅读本章后,你将能够在工作中使用逻辑回归,并在模型拟合过程中使用正则化,以利用偏差-方差权衡并提高模型在未见数据上的表现。
引言
本章将介绍上一章中剩余的逻辑回归细节。除了能够使用 scikit-learn 拟合逻辑回归模型外,你还将深入了解梯度下降过程,这与 scikit-learn 中用于完成模型拟合的“幕后”过程类似。最后,我们将通过熟悉这种方法的正式统计假设,完成对逻辑回归模型的讨论。
我们通过探讨如何扩展逻辑回归模型以解决过拟合问题,开始探索机器学习中基础概念——过拟合、欠拟合和偏差-方差权衡。在回顾用于缓解过拟合的正则化方法的数学细节后,你将学到一种调优正则化超参数的实用方法:交叉验证。通过正则化方法和一些简单的特征工程,你将理解如何改进过拟合和欠拟合的模型。
虽然本章我们主要关注逻辑回归,但过拟合、欠拟合、正则化以及偏差-方差权衡的概念几乎适用于机器学习中所有监督学习建模技术。
估计逻辑回归的系数和截距
在上一章,我们学习了逻辑回归模型的系数(每个系数对应一个特定的特征)以及截距,这些值是在调用 scikit-learn 中逻辑回归模型的.fit方法时,使用训练数据来确定的。这些数值被称为模型的参数,而找到最佳参数值的过程称为参数估计。一旦参数确定,逻辑回归模型就基本完成了:只需要这些数值,我们就可以在任何可以执行常见数学函数的环境中使用逻辑回归模型。
显然,参数估计过程是非常重要的,因为正是通过这个过程,我们能够从数据中构建预测模型。那么,参数估计是如何工作的呢?要理解这一点,第一步是熟悉代价函数的概念。代价函数是一种衡量模型预测与数据完美描述之间距离的方式。模型预测与实际数据之间的差异越大,代价函数返回的“代价”就越大。
对于回归问题,这是一个直观的概念:预测值与真实值之间的差异可以用作代价,通过某种变换(例如取绝对值或平方)将代价值转换为正数,再对所有训练样本进行平均。
对于分类问题,特别是在拟合逻辑回归模型时,一个典型的代价函数是对数损失函数,也叫交叉熵损失。这是 scikit-learn 在拟合逻辑回归时使用的代价函数,经过修改:

图 4.1:对数损失函数
这里有n个训练样本,yi 是第 i 个样本的真实标签(0 或 1),pi 是第 i 个样本标签为 1 的预测概率,log 是自然对数。对所有训练样本求和的符号(即大写的希腊字母 sigma)和除以 n,用于对所有训练样本的代价函数进行平均。考虑到这一点,看看下面的自然对数函数图像,并思考这个代价函数的解释:

图 4.2:区间 (0, 1) 上的自然对数
为了理解对数损失代价函数是如何工作的,考虑一个样本,其中真实标签为 1,即 y = 1,因此代价函数的第二部分,(1 - yi)log(1 - pi),将完全等于 0,不会影响结果。此时,代价函数的值为 -yilog(pi) = -log(pi),因为 yi = 1。因此,该样本的代价就是预测概率的自然对数的负值。现在,由于该样本的真实标签为 1,考虑代价函数应该如何表现。我们期望,对于接近 1 的预测概率,代价函数会很小,表示预测值与真实值接近时的误差很小。对于接近 0 的预测,代价会更大,因为代价函数应当随着预测错误的增大而增大。
从图 4.2中的自然对数图中,我们可以看到,对于更接近 0 的p值,自然对数的值越来越负。这意味着成本函数将变得越来越大,因此,分类一个具有非常低概率的正样本的成本相对较高,这正是我们所期望的。相反,如果预测的概率更接近 1,则图形表明成本将接近 0——再次,这与一个“更正确”预测的期望一致。因此,成本函数在正样本的情况下表现如预期。对于标签为 0 的样本,也可以做类似的观察。
现在我们已经了解了对数损失成本函数在逻辑回归中的工作原理。但这与系数和截距的确定有什么关系呢?我们将在下一节学习。
注意
生成本节中展示的图表的代码可以在这里找到:packt.link/NeF8P。
梯度下降寻找最优参数值
使用对数损失成本找到逻辑回归模型的参数值(系数和截距)的问题,归结为 scikit-learn 中逻辑回归模型的.fit方法的问题。找到具有最低成本的参数集有不同的解决技术,您可以在实例化模型类时使用solver关键字选择您想要使用的技术。所有这些方法都略有不同,但它们都基于梯度下降的概念。
梯度下降过程从solver关键字开始。然而,对于像深度神经网络这样的更高级机器学习算法,选择参数的初始猜测需要更多的关注。
为了说明问题,我们考虑一个只需要估计一个参数的情况。我们将观察一个假设的成本函数(y = f(x) = x2 – 2x)的值,并设计一个梯度下降过程来找到使成本y最小的参数值x。在这里,我们选择一些x值,创建一个返回成本函数值的函数,并观察在这个参数范围内成本函数的值。
执行此操作的代码如下:
X_poly = np.linspace(-3,5,81)
print(X_poly[:5], '...', X_poly[-5:])
这是打印语句的输出:
[-3\. -2.9 -2.8 -2.7 -2.6] ... [4.6 4.7 4.8 4.9 5\. ]
剩余的代码片段如下:
def cost_function(X):
return X * (X-2)
y_poly = cost_function(X_poly)
plt.plot(X_poly, y_poly)
plt.xlabel('Parameter value')
plt.ylabel('Cost function')
plt.title('Error surface')
结果图应如下所示:

图 4.3:成本函数图
注意
在之前的代码片段中,我们假设您已经导入了必要的库。您可以参考以下笔记本,获取包括前述代码片段导入语句的完整代码:packt.link/A4VyF。
查看 误差面(在 图 4.3 中),这是代价函数在一系列参数值上的图像,显而易见,哪个参数值将导致代价函数的最低值:x = 1。实际上,利用一些微积分,你可以通过将导数设置为零并求解 x,轻松确认 x = 1 是最小值。然而,通常来说,并不是所有问题都能如此简单地解决。在需要使用梯度下降的情况下,我们并不总是知道整个误差面的形状。相反,在我们选择了参数的初始猜测值之后,我们只能知道在该点周围区域内误差面的方向。
梯度下降是一种迭代算法;从初始猜测值开始,我们尝试找到一个新的猜测值,使代价函数降低,并继续进行,直到找到一个好的解决方案。我们试图在误差面上“下坡”,但我们只能根据当前猜测值附近的误差面形状知道该朝哪个方向移动以及在该方向上走多远。从数学角度看,我们只知道当前猜测值的参数处的 导数(在多维情况下称为 梯度)。如果你没有学习过微积分,可以把梯度理解为告诉你哪个方向是下坡,以及从你站立的地方山坡有多陡。我们利用这些信息在减少误差的方向上“迈出一步”。我们决定走多大的步伐取决于 学习率。由于梯度朝着误差减少的方向减小,我们希望朝梯度的负方向迈步。
这些概念可以通过以下方程进行形式化。为了从当前猜测值 xold 获得新猜测值 xnew,其中 f'(xold) 是当前猜测值处代价函数的导数(即梯度):

图 4.4:从当前猜测值获取新猜测值的方程
在下图中,我们可以看到从 x = 4.5 开始进行梯度下降过程的结果,学习率为 0.75,然后通过优化 x 使代价函数达到最小值:

图 4.5:梯度下降路径
梯度下降也适用于更高维的空间;换句话说,适用于多个参数。然而,你只能在单一图表中可视化最多二维的误差面(即在三维图中同时展示两个参数)。
在描述了梯度下降的工作原理后,让我们进行一个练习,实现梯度下降算法,并扩展本节的例子。
注意
用于生成本节所呈现图表的代码可以在这里找到:packt.link/NeF8P。如果你正在阅读本书的印刷版,你可以通过访问以下链接下载并浏览本章一些图像的彩色版本:packt.link/FAXBM
练习 4.01:使用梯度下降最小化代价函数
在本练习中,我们的任务是找到一组最佳参数,以最小化以下假设的代价函数:y = f(x) = x2 – 2x。为此,我们将采用前面部分描述的梯度下降方法。执行以下步骤以完成练习:
注意
在开始本练习之前,请确保你已执行了导入必要库和加载清理后的数据框架的先决步骤。有关这些步骤以及本练习的代码,你可以在packt.link/NeF8P找到。
-
创建一个返回代价函数值的函数,并查看在一系列参数下代价函数的值。你可以使用以下代码来做到这一点(注意,这部分代码重复了前面的部分):
X_poly = np.linspace(-3,5,81) print(X_poly[:5], '...', X_poly[-5:]) def cost_function(X): return X * (X-2) y_poly = cost_function(X_poly) plt.plot(X_poly, y_poly) plt.xlabel('Parameter value') plt.ylabel('Cost function') plt.title('Error surface')你将获得以下的代价函数图:
![图 4.6:代价函数图]()
图 4.6:代价函数图
-
创建一个函数来求梯度值。这是代价函数的解析导数。使用此函数来计算在 x = 4.5 时的梯度,然后将其与学习率结合,找到梯度下降过程的下一步:
def gradient(X): return (2*X) - 2 x_start = 4.5 learning_rate = 0.75 x_next = x_start - gradient(x_start)*learning_rate x_next -0.75这是 x = 4.5 后的下一个梯度下降步骤。
-
使用以下代码绘制梯度下降路径,从起点到下一个点:
plt.plot(X_poly, y_poly) plt.plot([x_start, x_next], [cost_function(x_start), cost_function(x_next)], '-o') plt.xlabel('Parameter value') plt.ylabel('Cost function') plt.legend(['Error surface', 'Gradient descent path'])你将获得以下输出:
![图 4.7:第一次梯度下降路径步骤]()
图 4.7:第一次梯度下降路径步骤
在这里,看起来我们似乎朝着正确的方向迈出了第一步。然而,很明显我们已经越过了我们想要到达的位置。可能是我们的学习率过大,因此我们采取了过大的步伐。虽然调节学习率是加速收敛到最优解的好方法,但在这个例子中,我们可以继续演示过程的其余部分。这里看起来我们可能还需要再迈几步。实际上,梯度下降会一直进行,直到步伐变得非常小,或者代价函数的变化变得非常小(你可以通过使用
tol参数在 scikit-learn 的逻辑回归中指定多小),这表示我们已经接近一个好的解——也就是max_iter。 -
通过使用以下代码片段执行 14 次迭代,以便向代价函数的局部最小值收敛(请注意,
iterations = 15,但在调用range()时不包括终点):iterations = 15 x_path = np.empty(iterations,) x_path[0] = x_start for iteration_count in range(1,iterations): derivative = gradient(x_path[iteration_count-1]) x_path[iteration_count] = x_path[iteration_count-1] \ - (derivative*learning_rate) x_path你将获得以下输出:
array([ 4.5 , -0.75 , 1.875 , 0.5625 , 1.21875 , 0.890625 , 1.0546875 , 0.97265625, 1.01367188, 0.99316406, 1.00341797, 0.99829102, 1.00085449, 0.99957275, 1.00021362])这个
for循环将连续的估计值存储在x_path数组中,使用当前估计值计算导数并找到下一个估计值。从梯度下降过程的结果值来看,我们似乎已经非常接近(1.00021362)最优解 1。 -
使用以下代码绘制梯度下降路径:
plt.plot(X_poly, y_poly) plt.plot(x_path, cost_function(x_path), '-o') plt.xlabel('Parameter value') plt.ylabel('Cost function') plt.legend(['Error surface', 'Gradient descent path'])你将获得以下输出:
![图 4.8:梯度下降路径]()
图 4.8:梯度下降路径
我们鼓励你重复之前的过程,尝试不同的学习率,看看它们如何影响梯度下降路径。选择合适的学习率,可以非常快速地收敛到一个高度准确的解。虽然在不同的机器学习应用中,学习率的选择很重要,但对于逻辑回归来说,这个问题通常比较容易解决,在 scikit-learn 中你不需要特别选择学习率。
当你尝试不同的学习率时,是否注意到当学习率大于 1 时发生了什么?在这种情况下,我们朝着减少误差的方向迈出的步伐过大,实际上会导致更高的误差。这个问题可能会自我加剧,甚至导致梯度下降过程远离最小误差区域。另一方面,如果步长太小,找到理想的解可能需要非常长的时间。
逻辑回归的假设
由于它是一个经典的统计模型,类似于我们已经考察过的 F 检验和皮尔逊相关性,逻辑回归对数据有一些假设。虽然不必严格遵循每一个假设,但了解它们是很有帮助的。这样,如果逻辑回归模型表现不佳,你可以尝试调查并找出原因,利用你对逻辑回归所期望的理想情况的理解。你可能会在不同的资源中看到略有不同的假设列表,然而这里列出的假设是被广泛接受的。
特征在对数几率中是线性的
我们在上一章第三章中学习了这个假设,逻辑回归与特征探索的详细信息。逻辑回归是一个线性模型,所以只要特征能够有效描述对数几率中的线性趋势,它就能很好地工作。特别地,逻辑回归无法捕捉特征之间的交互作用、多项式特征或特征的离散化。你可以将这些指定为“新特征”——即使它们可能是由现有特征衍生出来的。
记住上一章提到的,从单变量特征探索中,PAY_1特征在对数几率中并不是线性的。
特征之间没有多重共线性
多重共线性意味着特征之间存在相关性。这个假设最严重的违反情况是特征之间完全相关,例如一个特征与另一个特征完全相同,或者一个特征等于另一个特征乘以常数。我们可以使用我们已经熟悉的相关性图来调查特征的相关性,这个图也在单变量特征选择中出现过。以下是上一章的相关性图:

图 4.9: 特征与响应的相关性图
我们可以从相关性图中看到完美相关的样子:由于每个特征和响应变量与其自身的相关性为 1,我们可以看到 1 的相关性是浅色的奶油色。从颜色条中,我们可以知道没有-1 的相关性。
注意
包含本节中代码和相应图表的 Jupyter 笔记本可以在此找到:packt.link/UOEMp。
在我们的案例研究数据中,最明显的相关预测变量是BILL_AMT特征。直观来看,账单在同一个账户的每个月可能会相似。例如,可能有一个账户通常保持零余额,或者有一个账户存在大量余额,且需要较长时间才能还清。BILL_AMT特征之间是否存在完全相关?从图 4.9来看,似乎没有。所以,虽然这些特征可能没有提供太多独立的信息,但我们目前不会出于担心多重共线性的原因而删除它们。
观察值的独立性
这是经典统计模型中的一个常见假设,包括线性回归。在这里,假设观察值(或样本)是独立的。这个假设在案例研究数据中是否合理?我们需要与客户确认,数据集中的同一个人是否可以拥有多个信用账户,并根据这种情况的普遍性来决定如何处理。假设我们已经被告知,在我们的数据中,每个信用账户都属于唯一的人,因此我们可以假设在这一点上观察值是独立的。
在不同的数据领域中,观察值独立性的一些常见违反情况如下:
-
空间自相关的观察值;例如,在自然现象中,如土壤类型,其中地理上彼此接近的观察值可能相似。
-
时间自相关的观察值,通常出现在时间序列数据中。在时间序列数据中,通常假设当前时刻的观察值与最近的时刻(们)相关。
然而,这些问题与我们的案例研究数据无关。
无异常值
异常值是指特征(或响应)的值与大多数数据的差异非常大,或者在其他方面有所不同。对于特征值的异常值,更恰当的术语是高杠杆点,因为“异常值”通常用于描述响应变量。然而,在我们的二分类问题中,不可能有响应变量的异常值,因为它只能取值 0 或 1。在实际应用中,您可能会看到这两个术语都用于描述特征。
为了理解为什么这些类型的点通常会对线性模型产生不利影响,请看这个包含 100 个点的合成线性数据以及由线性回归得到的最佳拟合线:

图 4.10:“表现良好”的线性数据和回归拟合
在这里,模型直观上看似与数据拟合得很好。然而,如果加入一个异常值特征值会怎样呢?为了说明这一点,我们添加了一个点,其 x 值与大多数观测值非常不同,而 y 值与其他观测值处于相似范围。然后,我们展示了结果回归线:

图 4.11:显示当包含异常值时会发生什么的图表
由于存在一个高杠杆点,所有数据的回归模型拟合不再很好地代表大部分数据。这展示了单个数据点对线性模型的潜在影响,特别是当该点似乎与其余数据的趋势不一致时。
处理异常值有很多方法。但一个更根本的问题是:“这样的数据现实吗?”如果数据看起来不太对,可以询问客户这些异常值是否可信。如果不可信,应该将它们排除。但如果它们代表有效的数据,则应使用非线性模型或其他方法。
在我们的案例研究数据中,在特征探索过程中绘制的直方图中并没有观察到异常值。因此,我们没有这个顾虑。
你应该包含多少个特征?
这不完全是一个假设,更像是构建模型的指导原则。没有明确的定律说明在逻辑回归模型中应该包含多少个特征。然而,一个常见的经验法则是“10 的法则”,即每出现 10 次最稀有的结果类别,就可以在模型中添加 1 个特征。例如,在一个包含 100 个样本的二分类逻辑回归问题中,如果类别平衡是 20%的正样本和 80%的负样本,那么正样本总数只有 20 个,因此模型中应该仅使用 2 个特征。此外,还建议采用“20 的法则”,它对包含的特征数量设定了更严格的限制(在我们的例子中为 1 个特征)。
另一个需要考虑的点是,对于二进制特征(例如由独热编码产生的特征),即该特征有多少样本会有正值。如果该特征非常不平衡,换句话说,包含 1 或 0 的样本非常少,那么将其纳入模型可能没有意义。
对于案例研究数据,我们很幸运拥有相对较多的样本和较为平衡的特征,因此这些问题并不显著。
注意
本节中呈现的绘图代码可以在此处找到:packt.link/SnX3y。
正则化的动机:偏差-方差权衡
我们可以通过使用一种强大的概念——收缩或正则化,来扩展我们所学的基本逻辑回归模型。实际上,到目前为止,您在 scikit-learn 中拟合的每一个逻辑回归模型都使用了一定量的正则化。这是因为正则化是逻辑回归模型对象中的默认选项。不过,直到现在,我们一直忽视了它。
当你对这些概念有更深入的了解时,你还会熟悉一些机器学习中的基础概念:过拟合、欠拟合和偏差-方差权衡。如果一个模型在训练数据上的表现(例如,ROC AUC)远远好于在保留的测试集上的表现,那么这个模型被认为是对训练数据进行了过拟合。换句话说,在训练集上的良好表现并不能推广到未见过的测试集。我们在第二章,Scikit-Learn 简介与模型评估中开始讨论这些概念,当时我们区分了模型训练分数和测试分数。
当一个模型对训练数据发生过拟合时,它被认为具有较高的方差。换句话说,训练数据中存在的任何变异性,模型都学得非常好——实际上,学得太好了。这将在较高的训练得分中得到体现。然而,当这样的模型用于对新的、未见过的数据进行预测时,其表现较差。以下情况下,过拟合的可能性更大:
-
可用的特征数量相较于样本数量非常庞大。尤其是,可能存在如此多的特征,以至于直接检查所有特征变得繁琐,就像我们在案例研究数据中能够做到的那样。
-
使用了更复杂的模型,即比逻辑回归更复杂的模型。这些包括梯度提升集成模型或神经网络等。
在这种情况下,模型有机会在模型拟合过程中开发出关于特征与响应变量之间关系的更复杂的假设,从而使过拟合的可能性增加。
相反,如果一个模型无法很好地拟合训练数据,这就是所谓的欠拟合,模型被认为具有较高的偏差。
我们可以通过在一些假设数据上拟合多项式模型,来检查欠拟合、过拟合和理想模型之间的区别:

图 4.12:包含欠拟合、过拟合和理想模型的二次数据
在图 4.12中,我们可以看到,包含过少特征(在这种情况下,是仅有两个特征的y线性模型,一个斜率和一个截距)显然不是对数据的良好表示。这被称为欠拟合模型。然而,如果我们包含过多特征,即许多高次多项式项,比如x²、x³、x⁴、…… x¹⁰,虽然可以几乎完美地拟合训练数据,但这不一定是好事。当我们观察过拟合模型在训练数据点之间的结果时,尤其是在可能需要进行新预测的地方,我们可以看到模型不稳定,并且可能无法为未出现在训练集中的数据提供可靠的预测。我们仅凭对特征与响应变量之间关系的直观理解,就能看出这一点,这种理解来自于对数据的可视化。
注意
生成本节中展示的图表的代码可以在此找到:packt.link/SnX3y。
本例中的合成数据是通过二次(即二次方)多项式生成的。知道这一点后,我们可以通过将二次多项式拟合到训练数据上,轻松找到理想模型,如图 4.12所示。
然而,通常情况下,我们无法提前知道理想模型的公式。因此,我们需要通过比较训练和测试得分,来评估模型是否存在过拟合或欠拟合的情况。
在某些情况下,引入一些偏差到模型训练过程中是可取的,特别是当这样做可以减少过拟合,并提高模型在新数据(即未见过的数据)上的表现时。通过这种方式,可能可以利用偏差-方差权衡来改善模型。我们可以使用正则化方法来实现这一点。此外,我们也可以将这些方法用于变量选择,作为建模过程的一部分。使用预测模型来选择变量,是我们之前探讨的单变量特征选择方法的替代方案。在接下来的练习中,我们将开始实验这些概念。
练习 4.02:生成和建模合成分类数据
在本练习中,我们将通过使用合成数据集来观察过拟合现象。假设你现在面临一个二分类数据集,包含许多候选特征(200 个),而你没有时间逐一检查它们。可能其中一些特征是高度相关的,或者以其他方式相互关联。然而,特征的数量如此之多,可能会使得有效地探索每个特征变得困难。此外,数据集的样本数量相对较少:只有 1,000 个样本。我们将通过使用 scikit-learn 提供的一个功能来生成这个具有挑战性的数据集,该功能允许你创建合成数据集,用于进行此类概念性探索。请按照以下步骤完成练习:
注意
在开始本练习之前,请确保你已经执行了导入必要库的前提步骤。这些步骤及本练习的代码可以在 packt.link/mIMsT 找到。
-
使用以下代码导入
make_classification、train_test_split、LogisticRegression和roc_auc_score类:from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score请注意,我们从 scikit-learn 导入了几个熟悉的类,另外还导入了一个我们之前没有见过的新类:
make_classification。这个类的功能正如其名所示——它用于生成分类问题的数据。通过使用各种关键字参数,你可以指定要包含多少样本和特征,以及响应变量将有多少个类别。还有一系列其他选项,可以有效控制问题的“难易程度”。注意
更多信息,请参考
scikit-learn.org/stable/modules/generated/sklearn.datasets.make_classification.html。简单来说,我们在这里选择了使问题相对容易解决的选项,但也加入了一些复杂因素。换句话说,我们期望模型表现良好,但我们需要付出一点努力才能实现这一点。 -
生成一个包含两个变量的数据集,
x_synthetic和y_synthetic。x_synthetic包含 200 个候选特征,y_synthetic包含响应变量,每个包含 1,000 个样本。使用以下代码:X_synthetic, y_synthetic = make_classification( n_samples=1000, n_features=200, n_informative=3, n_redundant=10, n_repeated=0, n_classes=2, n_clusters_per_class=2, weights=None, flip_y=0.01, class_sep=0.8, hypercube=True, shift=0.0, scale=1.0, shuffle=True, random_state=24) -
使用以下代码检查数据集的形状以及响应变量的类别比例:
print(X_synthetic.shape, y_synthetic.shape) print(np.mean(y_synthetic))你将获得以下输出:
(1000, 200) (1000,) 0.501检查输出形状后,注意到我们生成了一个几乎完美平衡的数据集:类别平衡接近 50/50。还需要注意的是,我们已生成所有特征,使它们具有相同的
shift和scale——即均值为 0,标准差为 1。确保特征在相同的尺度上,或者说具有大致相同的取值范围,是使用正则化方法的关键点——稍后我们将看到为什么。如果原始数据集中的特征尺度差异较大,建议对其进行归一化,以确保它们处于相同的尺度上。Scikit-learn 提供了简便的方法来实现这一点,我们将在本章末的活动中学习。 -
使用以下代码将前几个特征绘制为直方图,以显示它们的取值范围相同:
for plot_index in range(4): plt.subplot(2, 2, plot_index+1) plt.hist(X_synthetic[:, plot_index]) plt.title('Histogram for feature {}'.format(plot_index+1)) plt.tight_layout()您将得到以下输出:
![图 4.13:200 个合成特征中的前 4 个特征的直方图]()
图 4.13:200 个合成特征中的前 4 个特征的直方图
由于我们生成了这个数据集,因此无需直接检查所有 200 个特征来确保它们在相同的尺度上。那么,这个数据集可能存在哪些问题呢?由于响应变量的类别比例已经平衡,因此我们无需进行欠采样、过采样或使用其他对不平衡数据有帮助的方法。那么特征之间以及特征与响应变量之间的关系呢?这些关系有很多,直接调查它们是一个挑战。根据我们的经验法则(即每 10 个稀有类别样本对应 1 个特征),200 个特征过多。我们在最稀有类别中有 500 个观察值,所以根据这个规则,我们不应该有超过 50 个特征。特征数量过多可能会导致模型训练过程过拟合。接下来,我们将开始学习如何在 scikit-learn 的逻辑回归中使用选项来防止这种情况发生。
-
使用 80/20 的比例将数据拆分为训练集和测试集,然后使用以下代码实例化一个逻辑回归模型对象:
X_syn_train, X_syn_test, y_syn_train, y_syn_test = \ train_test_split(X_synthetic, y_synthetic,\ test_size=0.2, random_state=24) lr_syn = LogisticRegression(solver='liblinear', penalty='l1', C=1000, random_state=1) lr_syn.fit(X_syn_train, y_syn_train)请注意,我们在逻辑回归模型中指定了一些新的选项,这是我们之前未关注的。首先,我们将
penalty参数设置为l1。这意味着我们将使用C参数,值为 1,000。根据 scikit-learn 文档(scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html),C是“正则化强度的倒数”。这意味着较大的C值对应较少的正则化。通过选择相对较大的数值,如 1,000,我们使用的是相对较少的正则化。C的默认值是 1。所以,我们这里实际上并没有使用太多正则化,而只是熟悉如何使用这些选项。最后,我们使用liblinear求解器,这是我们以前使用过的。尽管我们这里使用的是经过缩放的数据(所有特征的均值为 0,标准差为 1),但值得注意的是,在我们可用的各种求解器选项中,
liblinear是“对未缩放数据具有鲁棒性的”。另外要注意的是,liblinear是唯一支持 L1 惩罚的两个求解器选项之一,另一个选项是saga。注意
您可以在
scikit-learn.org/stable/modules/linear_model.html#logistic-regression上了解更多关于可用求解器的信息。 -
使用以下代码在训练数据上拟合逻辑回归模型:
lr_syn.fit(X_syn_train, y_syn_train)这是输出结果:
LogisticRegression(C=1000, penalty='l1', random_state=1, \ solver='liblinear') -
使用以下代码计算训练得分,首先获取预测概率,然后得到 ROC AUC:
y_syn_train_predict_proba = lr_syn.predict_proba(X_syn_train) roc_auc_score(y_syn_train, y_syn_train_predict_proba[:,1])输出应如下所示:
0.9420000000000001 -
使用与计算训练得分相似的方法计算测试得分:
y_syn_test_predict_proba = lr_syn.predict_proba(X_syn_test) roc_auc_score(y_syn_test, y_syn_test_predict_proba[:,1])输出应如下所示:
0.8075807580758075从这些结果来看,很明显,逻辑回归模型已经过拟合数据。也就是说,训练数据上的 ROC AUC 得分远高于测试数据上的得分。
Lasso (L1) 和 Ridge (L2) 正则化
在将正则化应用于逻辑回归模型之前,我们先花点时间理解什么是正则化以及它是如何工作的。在 scikit-learn 中,正则化逻辑回归模型的两种方式分别叫做penalty = 'l1'或'l2'。它们被称为“惩罚”,因为正则化的作用是增加惩罚或成本,以防止逻辑回归模型中系数的值过大。
正如我们已经学到的,逻辑回归模型中的系数描述了响应变量的对数几率与每个特征之间的关系。因此,如果某个系数值特别大,那么该特征的微小变化将在预测中产生较大的影响。当模型正在拟合并学习特征与响应变量之间的关系时,模型可能开始学习数据中的噪声。我们之前在图 4.12中看到过这一点:如果在拟合模型时可用的特征很多,并且没有对它们系数值施加限制,那么模型拟合过程可能会试图发现特征与响应变量之间的关系,这些关系无法推广到新数据。这样,模型就会变得更适应现实世界中不完美数据中的不可预测的随机噪声。不幸的是,这只会提高模型对训练数据的预测能力,而这并不是我们的最终目标。因此,我们应该努力从模型中剔除这些虚假的关系。
Lasso 和岭回归正则化使用不同的数学公式来实现这一目标。这些方法通过对模型拟合时使用的成本函数进行修改来工作,我们之前介绍过这个函数是对数损失函数。Lasso 正则化使用的是所谓的1-范数(因此也叫 L1):

图 4.14:带 Lasso 惩罚的对数损失方程
1-范数,即图 4.14中方程的第一项,实际上是* m 个不同特征系数绝对值的和。使用绝对值是因为无论系数是正向还是负向过大,都可能导致过拟合。那么,这个成本函数与我们之前看到的对数损失函数相比,有什么不同呢?嗯,现在有一个C*因子,它乘以了对数损失函数前面分数的部分。
这是“正则化强度的倒数”,正如 scikit-learn 文档中所描述的(scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html)。由于这个因子位于计算预测误差的成本函数项前面,而不是正则化项前面,因此增大它会让预测误差在成本函数中变得更加重要,而正则化则变得不那么重要。简而言之,在 scikit-learn 实现中,C 值越大,正则化越少。
L2 或岭回归正则化类似于 L1 正则化,不同之处在于,岭回归使用的是系数的平方和,而不是绝对值之和,这个平方和被称为2-范数:

图 4.15:带脊回归惩罚的对数损失方程
请注意,如果你查看 scikit-learn 文档中的逻辑回归成本函数,具体形式与这里使用的不同,但总体思路是相似的。此外,在你熟悉了套索(lasso)和脊回归(ridge)惩罚的概念之后,你应该知道还有一种叫做 弹性网(elastic-net) 的额外正则化方法,它是套索和脊回归的结合。
为什么正则化有两种不同的公式?
可能其中一个方法会提供更好的样本外表现,因此你可能希望同时测试这两种方法。这些方法之间还有另一个关键差异:L1 惩罚除了执行正则化外,还进行特征选择。它通过在正则化过程中将某些系数值设置为零,从而有效地从模型中去除这些特征。L2 正则化则是将系数值变小,但不会完全消除它们。并非所有的求解器选项都支持 L1 和 L2 正则化,因此你需要为你想使用的正则化技术选择合适的求解器。
注意
为什么 L1 正则化会去除特征,而 L2 不会的数学原理超出了本书的范围。然而,关于这个话题的更深入解释以及进一步的阅读,我们推荐一本非常易读(且免费的)资源——Gareth James 等人编著的《统计学习导论》。特别是,参见修订版第七印刷的 第 222 页,上面有一幅有助于理解 L1 和 L2 正则化差异的图示。
截距和正则化
我们并没有过多讨论截距,除了提到我们已经通过线性模型估计了它们,以及与每个特征相关的系数。那么,应该使用截距吗?答案可能是肯定的,直到你对线性模型有了更深入的理解,并确信在特定情况下不需要使用它。然而,确实存在这样的情况,例如在一个特征和响应变量都已归一化为零均值的线性回归模型中。
截距与任何特定特征无关。因此,对其进行正则化没有太大意义,因为它不应该有助于过拟合。请注意,在 L1 的正则化惩罚项中,求和从 j = 1 开始,同样在 L2 中,我们跳过了 σ0,这就是截距项。
这是理想的情况:不对截距进行正则化。然而,scikit-learn 中的一些求解器,如 liblinear,实际上会对截距进行正则化。你可以通过提供一个 intercept_scaling 选项来对抗这一效应。我们在这里没有展示这一点,因为虽然从理论上讲,这样做是不正确的,但在实践中,正则化截距通常对模型的预测质量影响不大。
缩放与正则化
如前一个练习所述,最佳实践是LIMIT_BAL在我们的数据集中远大于其他特征,比如PAY_1,实际上,可能希望为PAY_1的系数赋予较大的值,而为LIMIT_BAL的系数赋予较小的值,从而使它们在特征和系数的线性组合中对模型预测的影响处于相同的尺度。通过在使用正则化之前对所有特征进行标准化,可以避免因尺度差异而引发的此类复杂问题。
事实上,缩放数据可能也是必要的,这取决于你使用的求解器。scikit-learn 中可用的不同梯度下降变体可能无法有效处理未缩放的数据。
选择合适求解器的重要性
如我们所了解的,scikit-learn 中可用的不同逻辑回归求解器在以下方面有不同的表现:
-
它们是否支持 L1 和 L2 正则化
-
它们如何在正则化过程中处理截距
-
它们如何处理未缩放的数据
注意
还有其他的区别。一个有用的表格比较了这些和其他特性,可以参考
scikit-learn.org/stable/modules/linear_model.html#logistic-regression。你可以使用这个表格来决定哪个求解器最适合你的问题。
总结这一部分内容,我们学习了 lasso 和 ridge 正则化的数学基础。这些方法通过将系数值收缩到接近 0 来工作,在 lasso 的情况下,还会将某些系数精确地设为 0,从而执行特征选择。你可以想象,在我们图 4.12中的过拟合例子中,如果复杂的过拟合模型将一些系数收缩到接近 0,它将更像理想模型,而理想模型的系数较少。
这里是一个正则化回归模型的图示,使用与过拟合模型相同的高阶多项式特征,但加上了脊岭惩罚:

图 4.16:一个过拟合模型和使用相同特征的正则化模型
正则化后的模型看起来类似于理想模型,展示了正则化纠正过拟合的能力。然而,需要注意的是,正则化模型不应推荐用于外推。在此,我们可以看到正则化模型在图 4.16的右侧开始增加。这个增加应该被视为可疑,因为训练数据中没有任何迹象表明这是可以预期的。这是不推荐对超出训练数据范围的模型预测进行外推的一般观点的一个例子。然而,从图 4.16可以清楚地看到,即使我们没有关于生成这些合成数据的模型的知识(因为在现实世界的预测建模工作中,我们通常没有数据生成过程的知识),我们仍然可以使用正则化来减少在有大量候选特征时的过拟合影响。
模型与特征选择
L1 正则化是一种使用模型(如逻辑回归)进行特征选择的方法。其他方法包括从候选特征池中进行前向或后向逐步选择。这些方法背后的高层次思想如下:在前向选择的情况下,特征一个一个地添加到模型中,并观察外样本性能的变化。在每次迭代时,都会考虑将所有候选池中的特征添加到模型中,并选择能够最大化外样本性能提升的特征。当添加更多特征不再改善模型性能时,就不需要再从候选特征中添加更多特征。在后向选择的情况下,首先从模型中开始使用所有特征,并确定应该删除哪个特征:删除后对外样本性能影响最小的特征。你可以继续按这种方式删除特征,直到性能开始显著下降。
注意
本节中展示的生成图表的代码可以在此找到:packt.link/aUBMb。
交叉验证:选择正则化参数
到目前为止,你可能会怀疑我们是否可以使用正则化来减少在尝试对练习 4.02中的合成数据建模时观察到的过拟合现象,生成与建模合成分类数据。问题是,我们该如何选择正则化参数C呢?C是一个模型的超参数示例。超参数与在训练模型时估计的参数不同,例如逻辑回归的系数和截距。超参数不像参数那样通过自动化程序估计,而是由用户直接输入作为关键字参数,通常在实例化模型类时进行输入。那么,我们如何知道应该选择什么值呢?
超参数比参数更难估算。这是因为数据科学家需要决定最佳值,而不是让优化算法来寻找它。然而,程序化选择超参数值是可能的,这可以被视为一种优化过程。从实际角度看,在正则化参数C的情况下,最常见的做法是,使用特定的C值在一组数据上拟合模型,确定模型的训练性能,然后在另一组数据上评估out-of-sample性能。
我们已经熟悉使用模型训练集和测试集的概念。然而,这里有一个关键的区别;例如,如果我们多次使用测试集,以查看不同C值的效果,会发生什么?
你可能会想到,在第一次使用未见过的测试集来评估特定值的out-of-sample性能后,它就不再是“未见过”的测试集了。虽然在估算模型参数(即系数和截距)时仅使用了训练数据,但现在测试数据被用来估算超参数C。实际上,测试数据已经变成了额外的训练数据,因为它用于寻找超参数的最佳值。
因此,通常将数据分为三部分:训练集、测试集和验证集。验证集有多个用途:
估算超参数
验证集可以反复使用,以评估不同超参数值的out-of-sample性能,从而选择超参数。
不同模型的比较
除了为模型找到超参数值外,验证集还可以用来估算不同模型的out-of-sample性能;例如,如果我们想将逻辑回归与随机森林进行比较。
注意
数据管理最佳实践
作为数据科学家,如何划分数据以进行不同的预测建模任务是你的责任。在理想情况下,你应该保留一部分数据用于流程的最后阶段,即在你已经选择了模型超参数并确定了最佳模型之后。这未见过的测试集被保留到最后一步,可以用来评估你模型构建工作的最终结果,查看最终模型如何泛化到新的未见数据。在保留测试集时,最好确保特征和响应的特性与其余数据相似。换句话说,类别比例应该相同,特征的分布应该相似。这样,测试数据就能代表你用来构建模型的数据。
虽然模型验证是一个好习惯,但它引发了一个问题:我们为训练集、验证集和测试集选择的特定拆分,是否对我们跟踪的结果有任何影响。例如,也许特征和响应变量之间的关系在我们保留的未见测试集或验证集与训练集之间略有不同。要消除所有此类变异几乎是不可能的,但我们可以使用交叉验证的方法,以避免对某一特定数据拆分过度依赖。
Scikit-learn 提供了便捷的函数来促进交叉验证分析。这些函数与我们已经使用的 train_test_split 起到类似的作用,尽管默认行为有些不同。现在让我们来熟悉它们。首先,导入这两个类:
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import KFold
类似于 train_test_split,我们需要指定数据集用于训练和测试的比例。然而,在交叉验证中(特别是我们刚刚导入的类中实现的k 折交叉验证),我们不直接指定比例,而是简单地指明我们希望有多少个折叠——即“k 折”。这里的想法是,数据将被分成k个相等的部分。例如,如果我们指定 4 个折叠,那么每个折叠将包含 25%的数据。这些折叠将在四个单独的模型训练实例中作为测试数据,而每个折叠的其余 75%将用于训练模型。在此过程中,每个数据点总共会作为训练数据使用 k - 1 次,而仅作为测试数据使用一次。
在实例化该类时,我们指定了折数、是否在拆分数据前进行洗牌,以及是否设置随机种子,以确保在不同运行中得到可重复的结果:
n_folds = 4
k_folds = KFold(n_splits=n_folds, shuffle=False)
这里,我们实例化了一个具有四个折叠且没有洗牌的对象。我们使用返回的对象(我们称之为 k_folds)的方法是将我们希望用于交叉验证的特征数据和响应数据传递给该对象的 .split 方法。这会输出 X_syn_train 和 y_syn_train,我们可以像这样遍历这些拆分:
for train_index, test_index in k_folds_iterator.split(X_syn_train,
y_syn_train):
迭代器将返回 X_syn_train 和 y_syn_train 的行索引,我们可以用这些索引来获取数据。在这个 for 循环内部,我们可以编写代码,使用这些索引反复选择数据进行模型训练和测试,使用不同的数据子集。通过这种方式,我们可以获得一个稳健的模型外表现指标,当使用某一特定超参数值时,然后重复整个过程,使用另一个超参数值。因此,交叉验证循环可能会嵌套在一个外部的超参数值循环中。我们将在下面的练习中演示这一点。
不过,首先,这些拆分看起来是什么样子的?如果我们只是将train_index和test_index的索引用不同的颜色绘制出来,我们将得到如下图所示的效果:

图 4.17:没有打乱的四折 k 折训练/测试拆分
在这里,我们可以看到,按照我们为KFold类指定的选项,程序简单地将数据的前 25%(按行的顺序)作为第一个测试折,然后将下一个 25%的数据作为第二个折,依此类推。但如果我们想要分层抽样折呢?换句话说,如果我们希望确保每个折中的响应变量的类别比例相等呢?虽然train_test_split允许通过关键字参数实现这个选项,但有一个独立的StratifiedKFold类,它为交叉验证实现了这个功能。我们可以通过以下方式来说明分层拆分的效果:
k_folds = StratifiedKFold(n_splits=n_folds, shuffle=False)

图 4.18:分层 k 折训练/测试拆分
在图 4.18中,我们可以看到不同折之间已经进行了一定程度的“打乱”。程序根据需要在折之间移动样本,以确保每个折中的类别比例相等。
那么,如果我们想要将数据打乱,以便从每个测试折中选择整个索引范围的样本,该怎么办呢?首先,为什么我们想这么做?嗯,对于我们为这个问题创建的合成数据,我们可以确定数据是没有特定顺序的。然而,在许多实际情况下,我们收到的数据可能以某种方式进行了排序。
例如,数据的行可能是按账户创建日期排序的,或按其他逻辑排序的。因此,在拆分数据之前先打乱数据可能是个好主意。这样,任何可能被用于排序的特征,应该在每个折中都能保持一致。否则,不同折中的数据可能会有不同的特征,可能导致特征与响应之间的关系不同。
这可能导致模型在不同折之间的表现不均衡。为了在数据集的所有行索引中“打乱”折,只需要将shuffle参数设置为True:
k_folds = StratifiedKFold(n_splits=n_folds, shuffle=True,
random_state=1)

图 4.19:带有打乱的分层 k 折训练/测试拆分
通过打乱,测试折会随机且均匀地分布在输入数据的索引上。
K 折交叉验证是数据科学中广泛使用的一种方法。然而,选择使用多少折数取决于手头的特定数据集。使用较小的折数意味着每个折中的训练数据量相对较小。因此,这增加了模型过拟合的机会,因为模型通常在更多数据的训练下效果更好。建议尝试几种不同的折数,看看 k 折测试分数的均值和变异性如何变化。常见的折数范围通常是从 4 或 5 到 10。
在数据集非常小的情况下,可能需要在交叉验证折中尽可能多地使用数据进行训练。在这种情况下,可以使用一种叫做留一法交叉验证(LOOCV)的方法。在 LOOCV 中,每个折的测试集由一个单一的样本组成。换句话说,折数将与训练数据中的样本数量相同。在每次迭代中,模型会在除一个样本外的所有样本上进行训练,并对该样本进行预测。然后,可以根据这些预测来构建准确度或其他性能指标。
与创建测试集相关的其他问题,如为那些需要使用过去的观测值来预测未来事件的问题选择超时测试集,也同样适用于交叉验证。
在练习 4.02,生成和建模合成分类数据中,我们看到对训练数据拟合逻辑回归导致了过拟合。事实上,测试分数(ROC AUC = 0.81)明显低于训练分数(ROC AUC = 0.94)。我们实际上使用了非常少或没有正则化,因为我们将正则化参数C设置为一个相对较大的值(1,000)。现在我们将看到当我们在一个较宽的范围内调整C时会发生什么。
注意
本节中呈现的生成图形的代码可以在这里找到:packt.link/37Zks。
练习 4.03:减少合成数据分类问题中的过拟合
本练习是练习 4.02,生成和建模合成分类数据的延续。在这里,我们将使用交叉验证程序来找到超参数C的一个合适值。我们将通过仅使用训练数据来完成此任务,将测试数据保留到模型构建完成后再使用。请做好准备——这将是一个较长的练习——但它将展示一个通用过程,您可以将其应用于许多不同类型的机器学习模型,因此,花费时间完成它是非常值得的。按照以下步骤完成此练习:
注意
在开始此练习之前,您需要执行一些先决步骤,这些步骤可以在以下笔记本中找到,并附有此练习的代码:packt.link/JqbsW。
-
调整正则化参数 C 的值,使其范围从 C = 1000 到 C = 0.001。你可以使用以下代码片段来实现这一点。
首先,定义指数,它们将是 10 的幂次方,如下所示:
C_val_exponents = np.linspace(3,-3,13) C_val_exponents以下是前面代码的输出:
array([ 3\. , 2.5, 2\. , 1.5, 1\. , 0.5, 0\. , -0.5, -1\. , -1.5, -2\. , -2.5, -3\. ])现在,按 10 的幂次方调整 C 值,如下所示:
C_vals = np.float(10)**C_val_exponents C_vals以下是前面代码的输出:
array([1.00000000e+03, 3.16227766e+02, 1.00000000e+02, 3.16227766e+01, 1.00000000e+01, 3.16227766e+00, 1.00000000e+00, 3.16227766e-01, 1.00000000e-01, 3.16227766e-02, 1.00000000e-02, 3.16227766e-03, 1.00000000e-03])通常,最好通过 10 的幂次方来调整正则化参数,或者使用类似的策略,因为训练模型可能需要大量时间,特别是在使用 k 折交叉验证时。这能让你更好地了解不同的C值如何影响偏差-方差权衡,而无需训练大量模型。除了 10 的整数次方,我们还包括 log10 坐标轴上大约位于中间的点。如果在这些相对间隔较大的值之间似乎有一些有趣的行为,你可以在可能值的较小范围内添加更多细化的 C 值。
-
导入
roc_curve类:from sklearn.metrics import roc_curve我们将继续使用 ROC AUC 分数来评估、训练和测试性能。现在,我们有几个不同的 C 值要尝试,并且有多个折(在这个例子中是四个)进行交叉验证,我们将需要存储每个折和每个 C 值对应的训练和测试分数。
-
定义一个函数,该函数接受
k_folds交叉验证分割器、C 值数组(C_vals)、模型对象(model)、特征和响应变量(X和Y)作为输入,以通过 k 折交叉验证探索不同的正则化量。使用以下代码:def cross_val_C_search(k_folds, C_vals, model, X, Y):注意
我们在此步骤中开始的函数将返回 ROC AUC 和 ROC 曲线数据。返回块将在后续步骤中编写。现在,你可以按照原样编写上述代码,因为我们将在练习过程中定义
k_folds、C_vals、model、X和Y。 -
在这个函数块内,创建一个 NumPy 数组来保存模型性能数据,数组的维度为
n_folds×len(C_vals):n_folds = k_folds.n_splits cv_train_roc_auc = np.empty((n_folds, len(C_vals))) cv_test_roc_auc = np.empty((n_folds, len(C_vals)))接下来,我们将把与每个测试 ROC AUC 分数相关联的真正阳性率、假阳性率和阈值存储在一个列表的列表中。
注意
这是存储所有模型性能信息的一种方便方式,因为 Python 中的列表可以包含任何类型的数据,包括另一个列表。在这里,列表的列表中的每个内层列表项将是一个元组,包含每个折叠的 TPR、FPR 和阈值数组,对于每个C值。元组是 Python 中的有序集合数据类型,类似于列表,但与列表不同的是它们是不可变的:一旦元组创建,元组中的项不能更改。当一个函数返回多个值时,像 scikit-learn 的 roc_curve 函数,这些值可以输出到一个单一的变量中,这个变量将是一个包含这些值的元组。这种存储结果的方式,在我们稍后访问这些数组以进行检查时,应该更为明显。
-
使用
[[]]和*len(C_vals)创建一个空列表,如下所示:cv_test_roc = [[]]*len(C_vals)使用
*len(C_vals)表示每个C值应该有一个包含指标(TPR、FPR、阈值)元组的列表。我们已经在前一节中学习了如何在交叉验证中遍历不同的折叠。接下来我们需要做的是编写一个外部循环,其中嵌套交叉验证循环。
-
为每个C值创建一个外部循环来训练和测试每个 k 折:
for c_val_counter in range(len(C_vals)): #Set the C value for the model object model.C = C_vals[c_val_counter] #Count folds for each value of C fold_counter = 0我们可以重用已经有的相同模型对象,并在每次循环中设置一个新的C值。在C值的循环中,我们运行交叉验证循环。我们从为每个拆分生成训练和测试数据的行索引开始。
-
获取每个折叠的训练和测试索引:
for train_index, test_index in k_folds.split(X, Y): -
使用以下代码索引特征和响应变量,以获取该折叠的训练和测试数据:
X_cv_train, X_cv_test = X[train_index], X[test_index] y_cv_train, y_cv_test = Y[train_index], Y[test_index]然后使用当前折叠的训练数据来训练模型。
-
在训练数据上拟合模型,如下所示:
model.fit(X_cv_train, y_cv_train)这将有效地“重置”模型,从之前的系数和截距中恢复,反映出在这组新数据上的训练。
然后获得训练和测试的 ROC AUC 分数,以及与测试数据相关的 TPR、FPR 和阈值数组。
-
获取训练 ROC AUC 分数:
y_cv_train_predict_proba = model.predict_proba(X_cv_train) cv_train_roc_auc[fold_counter, c_val_counter] = \ roc_auc_score(y_cv_train, y_cv_train_predict_proba[:,1]) -
获取测试 ROC AUC 分数:
y_cv_test_predict_proba = model.predict_proba(X_cv_test) cv_test_roc_auc[fold_counter, c_val_counter] = \ roc_auc_score(y_cv_test, y_cv_test_predict_proba[:,1]) -
使用以下代码获取每个折叠的测试 ROC 曲线:
this_fold_roc = roc_curve(y_cv_test, y_cv_test_predict_proba[:,1]) cv_test_roc[c_val_counter].append(this_fold_roc)我们将使用一个折叠计数器来跟踪递增的折叠,在交叉验证循环之外,打印状态更新到标准输出。每当执行长时间的计算过程时,定期打印作业的状态是个好主意,这样你可以监控进展并确认一切正常工作。这个交叉验证过程在你的笔记本电脑上可能只需要几秒钟,但对于较长的任务,这样做尤其令人放心。
-
使用以下代码递增折叠计数器:
fold_counter += 1 -
编写以下代码以显示每个C值的执行进度:
print('Done with C = {}'.format(lr_syn.C)) -
编写代码以返回 ROC AUC 和 ROC 曲线数据并完成函数:
return cv_train_roc_auc, cv_test_roc_auc, cv_test_roc请注意,我们将继续使用之前展示的四折拆分,但鼓励你尝试使用不同数量的折来比较效果。
我们在前面的步骤中已经覆盖了很多内容。你可能想花几分钟时间和你的同学一起复习一下,以确保你理解每个部分。运行这个函数相对简单。这就是设计良好的函数的魅力——所有复杂的部分都被抽象化了,允许你专注于如何使用它。
-
运行我们设计的函数来检查交叉验证的性能,使用我们之前定义的C值,并使用我们在上一个练习中使用的模型和数据。使用以下代码:
cv_train_roc_auc, cv_test_roc_auc, cv_test_roc = \ cross_val_C_search(k_folds, C_vals, lr_syn, X_syn_train, y_syn_train)当你运行此代码时,你应该会看到以下输出,随着每个C值的交叉验证完成,输出会出现在代码单元格下方:
Done with C = 1000.0 Done with C = 316.22776601683796 Done with C = 100.0 Done with C = 31.622776601683793 Done with C = 10.0 Done with C = 3.1622776601683795 Done with C = 1.0 Done with C = 0.31622776601683794 Done with C = 0.1 Done with C = 0.03162277660168379 Done with C = 0.01 Done with C = 0.0031622776601683794 Done with C = 0.001那么,交叉验证的结果是什么样的呢?有几种方法可以查看这个结果。单独查看每一折的性能是很有用的,这样你可以看到结果的变化程度。
这告诉你数据的不同子集作为测试集的表现,从而大致了解你可以从未见过的测试集期望的表现范围。我们在这里感兴趣的是,是否能够通过正则化来缓解我们所看到的过拟合问题。我们知道使用C = 1,000导致了过拟合——我们通过比较训练和测试分数得知这一点。但对于我们尝试的其他C值呢?一个很好的可视化方法是将训练和测试分数绘制在y 轴上,将C值绘制在x 轴上。
-
使用以下代码,循环遍历每一折,以单独查看它们的结果:
for this_fold in range(k_folds.n_splits): plt.plot(C_val_exponents, cv_train_roc_auc[this_fold], '-o',\ color=cmap(this_fold),\ label='Training fold {}'.format(this_fold+1)) plt.plot(C_val_exponents, cv_test_roc_auc[this_fold], '-x',\ color=cmap(this_fold),\ label='Testing fold {}'.format(this_fold+1)) plt.ylabel('ROC AUC') plt.xlabel('log$_{10}$(C)') plt.legend(loc = [1.1, 0.2]) plt.title('Cross validation scores for each fold')你将获得以下输出:
![图 4.20:每一折和 C 值的训练和测试得分]()
图 4.20:每一折和 C 值的训练和测试得分
我们可以看到,对于交叉验证的每一折,随着C值的减小,训练性能也在下降。然而,与此同时,测试性能却在增加。对于某些折和C的值,测试的 ROC AUC 分数实际上超过了训练数据的分数,而对于其他情况,这两个指标则趋向于接近。在所有情况下,我们可以说,10^-1.5 和 10^-2 的C值在测试性能上表现相似,明显高于C = 10³的测试性能。因此,似乎正则化成功解决了我们的过拟合问题。
那么C的较低值呢?对于低于 10-2 的值,ROC AUC 指标突然下降到 0.5。正如您所知,这个值意味着分类模型基本上是无用的,性能不比抛硬币好。当探索正则化如何影响系数值时,鼓励您稍后检查这一点;然而,当应用了如此多的 L1 正则化以至于所有模型系数都收缩到 0 时,就会发生这种情况。显然,这样的模型对我们没有用,因为它们不包含关于特征和响应变量之间关系的任何信息。
查看每个 k 折分割的训练和测试性能有助于了解当模型在新的未见数据上得分时可能预期的模型性能的变化。但为了总结 k 折过程的结果,一个常见的方法是对每个正在考虑的超参数值的性能指标进行折叠平均。我们将在下一步中执行此操作。
-
使用以下代码绘制每个C值的训练和测试 ROC AUC 分数的平均值:
plt.plot(C_val_exponents, np.mean(cv_train_roc_auc, axis=0), \ '-o', label='Average training score') plt.plot(C_val_exponents, np.mean(cv_test_roc_auc, axis=0), \ '-x', label='Average testing score') plt.ylabel('ROC AUC') plt.xlabel('log$_{10}$(C)') plt.legend() plt.title('Cross validation scores averaged over all folds')![图 4.21:跨交叉验证折叠的平均训练和测试分数]()
图 4.21:跨交叉验证折叠的平均训练和测试分数
从这个图中可以看出,C = 10-1.5 和10-2 是最佳的C值。这里几乎没有过拟合,因为平均训练和测试分数几乎相同。您可以搜索更精细的C值网格(即C = 10-1.1、10-1.2 等),以更精确地定位C值。然而,从我们的图表中,我们可以看到C = 10-1.5 或C = 10-2 可能是很好的解决方案。我们将继续使用C = 10*-1.5。
检查 ROC AUC 的摘要指标是了解模型性能的快速方法。然而,对于任何真实的业务应用程序,您通常需要选择一个特定的阈值,该阈值与特定的真正和假正率相对应。这些将需要使用分类器来做出所需的“是”或“否”决定,在我们的案例研究中,这是关于账户是否会违约的预测。因此,查看交叉验证的不同折叠中的 ROC 曲线是有用的。为了方便起见,前面的函数已经被设计为返回每个测试折叠和C值的真正和假正率以及阈值,在
cv_test_roc列表的列表中。首先,我们需要找到对应于我们选择的C值10-1.5 的外部列表的索引。要实现这一点,我们可以简单地查看我们的C值列表并手动计数,但最好通过编程方式找到布尔数组的非零元素的索引来进行操作,如下一步所示。
-
使用布尔数组找到C = 10-1.5 的索引,并使用以下代码将其转换为整数数据类型:
best_C_val_bool = C_val_exponents == -1.5 best_C_val_bool.astype(int)以下是前面代码的输出:
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]) -
使用
nonzero函数将布尔数组的整数版本转换为单个整数索引,代码如下:best_C_val_ix = np.nonzero(best_C_val_bool.astype(int)) best_C_val_ix[0][0]以下是前面代码的输出:
9我们现在已经成功找到了我们希望使用的C值。
-
访问真正阳性和假阳性率,以绘制每个折叠的 ROC 曲线:
for this_fold in range(k_folds_n_splits): fpr = cv_test_roc[best_C_val_ix[0][0]][this_fold][0] tpr = cv_test_roc[best_C_val_ix[0][0]][this_fold][1] plt.plot(fpr, tpr, label='Fold {}'.format(this_fold+1)) plt.xlabel('False positive rate') plt.ylabel('True positive rate') plt.title('ROC curves for each fold at C = $10^{-1.5}$') plt.legend()你将得到以下输出:
![图 4.22:每个折叠的 ROC 曲线]()
图 4.22:每个折叠的 ROC 曲线
看起来 ROC 曲线存在相当大的变异性。例如,如果出于某种原因,我们想将假阳性率限制在 40%,那么从图中可以看出,我们可能能够实现的真正阳性率大约在 60%到 80%之间。你可以通过检查我们绘制的数组来找到精确值。这给你一个关于在新数据上部署模型时,性能波动的预期情况。通常,训练数据越多,交叉验证的折叠之间的变异性就越小,因此这也可能是一个收集更多数据的好主意,尤其是当训练折叠之间的变异性似乎不可接受地高时。你可能还希望尝试使用不同数量的折叠进行此过程,以查看结果变异性对折叠之间的影响。
虽然通常我们会尝试在我们的合成数据问题上使用其他模型,例如随机森林或支持向量机,但如果我们假设在交叉验证中,逻辑回归证明是最好的模型,我们将决定将其作为最终选择。当最终模型被选定后,可以使用所有训练数据来拟合该模型,使用在交叉验证中选择的超参数。最好在模型拟合时使用尽可能多的数据,因为通常情况下,模型在训练时使用更多的数据效果更好。
-
在我们的合成问题中,使用所有训练数据训练逻辑回归,并比较训练和测试分数,使用以下步骤所示的保留测试集。
注意
这是模型选择过程中的最后一步。只有在你选择好模型和超参数之后,才能使用未见过的测试集,否则它就不再是“未见过”的。
-
设置C值,并使用以下代码在所有训练数据上训练模型:
lr_syn.C = 10**(-1.5) lr_syn.fit(X_syn_train, y_syn_train)以下是前面代码的输出:
LogisticRegression(C=0.03162277660168379, penalty='l1', \ random_state=1, solver='liblinear')) -
使用以下代码获取训练数据的预测概率和 ROC AUC 分数:
y_syn_train_predict_proba = lr_syn.predict_proba(X_syn_train) roc_auc_score(y_syn_train, y_syn_train_predict_proba[:,1])以下是前面代码的输出:
0.8802812499999999 -
使用以下代码获取测试数据的预测概率和 ROC AUC 分数:
y_syn_test_predict_proba = lr_syn.predict_proba(X_syn_test) roc_auc_score(y_syn_test, y_syn_test_predict_proba[:,1])以下是前面代码的输出:
0.8847884788478848在这里,我们可以看到,通过使用正则化,模型的训练分数和测试分数相似,表明过拟合问题已大大减轻。训练分数较低,因为我们在模型中引入了偏差,牺牲了方差。然而,这没关系,因为最重要的测试分数较高。样本外测试分数才是预测能力的关键。建议您通过打印我们之前绘制的数组中的值,检查这些训练分数和测试分数是否与交叉验证过程中的结果相似;您应该发现它们是相似的。
注意
在一个实际项目中,在将这个模型交付给客户用于生产使用之前,您可能希望在所有提供的数据上训练模型,包括未见过的测试集。这遵循了一个想法,即模型看到的数据越多,实际表现可能越好。然而,一些从业者更喜欢只使用经过测试的模型,这意味着您只会交付在训练数据上训练的模型,而不包括测试集。
我们知道,L1 正则化通过减少逻辑回归系数的大小(即绝对值)来工作。它还可以将一些系数设置为零,从而执行特征选择。在下一步,我们将确定有多少个系数被设置为零。
-
使用以下代码访问训练模型的系数,并确定有多少个系数不等于零(
!= 0):sum((lr_syn.coef_ != 0)[0])输出应如下所示:
2这段代码对一个布尔数组求和,表示非零系数的位置,因此显示模型中有多少个系数没有被 L1 正则化设置为零。在 200 个特征中,只有 2 个被选择了!
-
使用以下代码检查截距的值:
lr_syn.intercept_输出应如下所示:
array([0.])这表明截距被正则化为 0。
在这个练习中,我们完成了几个目标。我们使用了 k 折交叉验证过程来调整正则化超参数。我们看到了正则化在减少过拟合方面的强大作用,并且在逻辑回归中的 L1 正则化情况下,还能进行特征选择。
许多机器学习算法提供某种类型的特征选择功能。许多算法还需要调整超参数。这里的函数通过循环超参数并执行交叉验证,提供了一个强大的概念,可以推广到其他模型。Scikit-learn 提供了简化这个过程的功能;特别是,sklearn.model_selection.GridSearchCV过程,它对超参数进行网格搜索并应用交叉验证。当需要调整多个超参数时,网格搜索非常有帮助,因为它可以查看您指定的不同超参数范围的所有组合。随机网格搜索可以通过随机选择较少的组合来加速这一过程,尤其是在全面的网格搜索过于耗时的情况下。一旦您熟悉了这里展示的概念,建议您通过使用像这些方便的函数来简化工作流程。
Scikit-Learn 中逻辑回归的选项
我们已经使用并讨论了在实例化或调整LogisticRegression模型类的超参数时,您可能提供的大部分选项。在这里,我们列出了所有选项,并提供了一些关于它们使用的通用建议:

图 4.23:Scikit-learn 中逻辑回归模型的完整选项列表
如果您对使用哪个选项进行逻辑回归感到疑惑,我们建议您参考 scikit-learn 文档以获取进一步的指导(scikit-learn.org/stable/modules/linear_model.html#logistic-regression)。一些选项,例如正则化参数C,或正则化惩罚的选择,需要通过交叉验证过程来探索。在这里,正如许多数据科学决策一样,没有一种通用的方法适用于所有数据集。查看使用哪些选项最适合给定数据集的最佳方法是尝试其中的几个,并查看哪一个在样本外表现最好。交叉验证为您提供了一种稳健的方式来做到这一点。
在 Scikit-Learn 中的缩放数据、管道和交互特征
缩放数据
与我们之前处理的合成数据相比,案例研究数据相对较大。如果我们想使用 L1 正则化,那么根据官方文档(scikit-learn.org/stable/modules/linear_model.html#logistic-regression),我们应该使用 saga 解算器。然而,这个解算器对未缩放的数据集不具备鲁棒性。因此,我们需要确保对数据进行缩放。每当进行正则化时,这也是一个好主意,这样所有特征就处于相同的尺度,并且在正则化过程中会受到同等的惩罚。确保所有特征具有相同尺度的一个简单方法是将它们都通过一个变换过程,即减去最小值并除以最小值到最大值的范围。这将把每个特征转换为使其最小值为 0,最大值为 1。为了实例化一个执行这一过程的 MinMaxScaler 缩放器,我们可以使用以下代码:
from sklearn.preprocessing import MinMaxScaler
min_max_sc = MinMaxScaler()
管道
以前,我们在交叉验证循环中使用了逻辑回归模型。然而,现在我们对数据进行了缩放,新的考虑因素是什么?缩放实际上是通过训练数据的最小值和最大值来“学习”的。之后,逻辑回归模型将基于由模型训练数据的极值缩放过的数据进行训练。然而,我们无法知道新数据(未见数据)的最小值和最大值。因此,按照使交叉验证成为评估未见数据模型性能的有效指标的理念,我们需要在每个交叉验证折叠中使用训练数据的最小值和最大值,以便在该折叠中对测试数据进行缩放,然后再对测试数据进行预测。Scikit-learn 提供了便捷的功能来结合多个训练和测试步骤,以应对这种情况:Pipeline。我们的管道将包括两个步骤:缩放器和逻辑回归模型。这两个步骤可以都在训练数据上进行拟合,然后用于对测试数据进行预测。拟合管道的过程在代码中作为一个单一步骤执行,因此从这个角度看,管道的所有部分都是一次性拟合的。以下是如何实例化一个Pipeline:
from sklearn.pipeline import Pipeline
scale_lr_pipeline = Pipeline(steps=[('scaler', min_max_sc), \
('model', lr)])
交互特征
考虑到案例研究数据,你认为一个包含所有可能特征的逻辑回归模型会过拟合还是欠拟合?你可以从经验法则的角度来考虑,例如“10 法则”,以及我们拥有的特征数(17 个)与样本数(26,664 个)之间的关系。或者,你也可以回顾我们迄今为止在这个数据上所做的所有工作。例如,我们已经有机会对所有特征进行可视化,并确保它们是合理的。由于特征相对较少,并且由于我们通过数据探索工作对它们的质量有较高的信心,我们的情况与本章中使用合成数据的练习不同,后者有大量特征,但我们对其了解较少。因此,可能目前我们的案例研究数据过拟合问题不太明显,正则化的好处可能也不会显著。
实际上,使用仅有的 17 个特征,我们可能会出现欠拟合。应对这种情况的一种策略是进行特征工程。我们讨论过的一些简单特征工程技术包括交互特征和多项式特征。考虑到某些数据的编码方式,多项式特征可能没有意义;例如,-12 = 1*,这对于PAY_1可能并不合理。然而,我们可能希望尝试创建交互特征,以捕捉特征之间的关系。PolynomialFeatures可以用来仅创建交互特征,而不包括多项式特征。示例代码如下:
make_interactions = PolynomialFeatures(degree=2, \
interaction_only=True, \
include_bias=False)
这里,degree表示多项式特征的阶数,interaction_only是布尔值(将其设置为True表示仅创建交互特征),include_bias也是布尔值,它会向模型添加截距项(默认值为False,这里是正确的,因为逻辑回归模型会自动添加截距)。
活动 4.01:使用案例研究数据进行交叉验证和特征工程
在本活动中,我们将应用本章中学到的交叉验证和正则化知识到案例研究数据中。我们将进行基础的特征工程。为了为案例研究数据的正则化逻辑回归模型估计参数,由于该数据集比我们之前使用的合成数据集大,因此我们将使用saga求解器。为了使用此求解器,并出于正则化的目的,我们需要使用 scikit-learn 中的Pipeline类。完成活动后,你应当能够得到使用交互特征的改进版交叉验证测试表现,具体如下图所示:

图 4.24:改进的模型测试表现
执行以下步骤以完成活动:
-
从案例研究数据的数据框中选择特征。
你可以使用我们在本章中已经创建的特征名称列表,但一定要确保不包括响应变量,因为它是一个非常好的(但完全不适当的)特征!
-
使用随机种子 24 进行训练/测试集划分。
我们将继续使用这个并将此测试数据保留为未见过的测试集。通过指定随机种子,我们可以轻松创建包含其他建模方法的独立笔记本,并使用相同的训练数据。
-
实例化
MinMaxScaler来缩放数据。 -
使用
saga求解器、L1 惩罚并将max_iter设置为1000来实例化一个逻辑回归模型,因为我们希望求解器有足够的迭代次数来找到一个良好的解。 -
导入
Pipeline类,并使用'scaler'和'model'作为步骤名称,分别创建一个包含缩放器和逻辑回归模型的流水线。 -
使用
get_params和set_params方法查看每个流水线阶段的参数,并进行更改。 -
创建一个较小范围的C值以进行交叉验证测试,因为这些模型在使用比我们之前练习更多数据时,训练和测试将花费更长时间;我们推荐的C值为 C = [102, 10, 1, 10-1, 10-2, 10-3*]。
-
创建一个新的
cross_val_C_search函数版本,名为cross_val_C_search_pipe。这个函数将不再使用model参数,而是接受一个pipeline参数。函数内部的更改将是通过在流水线中使用set_params(model__C = <value you want to test>)来设置C值,替换fit和predict_proba方法中的模型为流水线,并通过pipeline.get_params()['model__C']访问C值,以打印状态更新。 -
像之前的练习一样运行这个函数,但使用新的C值范围、你创建的流水线,以及来自案例研究数据训练集的特征和响应变量。
你可能会看到关于求解器不收敛的警告,可能出现在此处或后续步骤中;你可以尝试使用
tol或max_iter选项来实现收敛,尽管使用max_iter = 1000获得的结果可能已经足够。 -
绘制每个C值在各折交叉验证中的平均训练和测试 ROC AUC。
-
为案例研究数据创建交互特征,并确认新特征的数量是合理的。
-
重复交叉验证过程,并观察在使用交互特征时模型的表现。
注意,由于特征数量较多,这将需要更多时间,但可能不超过 10 分钟。那么,交互特征是否改善了平均交叉验证测试性能?正则化有用吗?
注意
包含此活动的 Python 代码的 Jupyter notebook 可以在
packt.link/ohGgX找到。此活动的详细逐步解决方案可以通过此链接查看。
总结
在本章中,我们介绍了逻辑回归的最终细节,并继续学习如何使用scikit-learn拟合逻辑回归模型。通过了解代价函数的概念,我们对模型拟合过程有了更多的了解,代价函数通过梯度下降过程来最小化,从而在模型拟合过程中估计参数。
我们还通过引入欠拟合和过拟合的概念,了解到正则化的必要性。为了减少过拟合,我们了解了如何调整代价函数,通过 L1 或 L2 惩罚对逻辑回归模型的系数进行正则化。我们使用交叉验证来选择正则化的程度,通过调整正则化超参数来进行选择。为了减少欠拟合,我们还学习了如何通过交互特征进行一些简单的特征工程,来处理案例研究数据。
我们现在已经熟悉了一些机器学习中最重要的概念。到目前为止,我们仅使用了一个非常基础的分类模型:逻辑回归。然而,随着你逐步扩展所掌握的模型工具箱,你会发现过拟合和欠拟合的概念、偏差-方差权衡以及超参数调优将一次又一次地出现。这些概念,以及我们在本章中编写的交叉验证函数的便捷scikit-learn实现,将帮助我们在探索更先进的预测方法时提供支持。
在下一章,我们将学习决策树,这是一种完全不同于逻辑回归的预测模型类型,以及基于决策树的随机森林。然而,我们将使用在本章中学到的相同概念——交叉验证和超参数搜索——来调优这些模型。
第五章:5. 决策树与随机森林
概述
在本章中,我们将重点介绍近年来在数据科学中风靡一时的另一类机器学习模型:基于树的模型。在本章中,在单独学习决策树后,你将学习由多棵树组成的模型(即随机森林),它们如何改善单棵树所产生的过拟合问题。读完本章后,你将能够为机器学习训练决策树、可视化训练好的决策树,并训练随机森林并可视化结果。
引言
在过去的两章中,我们已经深入理解了逻辑回归的工作原理,并且已经积累了大量使用 Python 中的 scikit-learn 包来创建逻辑回归模型的经验。
在本章中,我们将介绍一种强大的预测模型,这种模型与逻辑回归模型采用完全不同的方法:决策树。决策树及其基础上的模型是目前可用于一般机器学习应用的最具表现力的模型之一。使用树状过程进行决策的概念简单明了,因此,决策树模型易于理解。然而,决策树的一个常见批评是它们容易对训练数据过拟合。为了解决这个问题,研究人员开发了集成方法,如随机森林,通过将多棵决策树结合在一起,协同工作,做出比任何单棵树更好的预测。
我们将看到,决策树和随机森林可以提升案例研究数据的预测建模质量,超越我们目前使用逻辑回归所取得的成果。
决策树
决策树及其基础上的机器学习模型,特别是随机森林和梯度提升树,与广义线性模型(GLM),如逻辑回归,是根本不同的模型类型。GLM 源自经典统计学理论,这些理论有着悠久的历史。线性回归背后的数学最初由勒让德和高斯在 19 世纪初提出。因此,正态分布也被称为高斯分布。
相比之下,虽然使用树状过程进行决策的想法相对简单,决策树作为数学模型的流行是在最近才兴起的。我们目前用于制定决策树的数学方法是在 1980 年代发布的。之所以出现这种较新的发展,是因为用于生长决策树的方法依赖于计算能力——即快速处理大量数字的能力。如今我们理所当然地拥有这种能力,但在数学历史上,直到近代才广泛可用。
那么,决策树是什么意思呢?我们可以通过一个实际的例子来说明基本概念。假设你正在考虑是否在某一天外出。你做决定时唯一依赖的信息是天气,特别是阳光是否明媚以及气温有多暖和。如果是晴天,你对凉爽气温的耐受性会提高,只要气温至少为 10°C,你就会外出。
然而,如果是阴天,你需要稍微温暖一些的气温,并且只有当气温达到 15°C 或更高时,你才会外出。你的决策过程可以通过以下树状图表示:

图 5.1:根据天气决定是否外出的决策树
正如你所看到的,决策树具有直观的结构,并模拟了人类可能做出逻辑决策的方式。因此,它们是一个高度可解释的数学模型类型,这在某些受众中可能是一个特别理想的特性。例如,数据科学项目的客户可能特别关注如何清晰地理解一个模型是如何工作的。只要其性能足够,决策树是满足这一要求的好方法。
决策树术语及其与机器学习的关系
看图中的图 5.1,我们可以开始熟悉一些决策树的术语。因为在第一层基于云层条件,第二层基于气温做出决策,所以我们说这棵决策树的深度为二。这里,第二层的两个节点都是基于气温做出的决策,但在同一层次内,决策的种类可能不同;例如,如果不是晴天,我们也可以根据是否下雨来做决定。
在机器学习的背景下,用于在节点处做决策(换句话说,分裂节点)的量是特征。在图 5.1中的示例中,特征包括是否晴天的二元分类特征和温度的连续特征。虽然我们在树的给定分支中只展示了每个特征被使用一次,但同一个特征也可以在一个分支中被多次使用。例如,我们可能选择在阳光明媚的日子里,温度至少为 10 °C 时外出,但如果温度超过 40 °C,就不出去了——那太热了!在这种情况下,图 5.1中的节点 4 将根据“温度是否大于 40 °C?”这一条件进行分裂,如果答案是“是”,结果是“待在室内”,如果答案是“否”,则结果是“外出”,这意味着温度在 10 °C 到 40 °C 之间。因此,决策树能够捕捉特征的非线性效应,而不是假设温度越高,我们越可能外出的一种线性关系,无论温度有多高。
考虑树通常是如何表示的,例如在图 5.1中。分支基于二元决策向下生长,这些二元决策可以将节点分裂成两个子节点。这些二元决策可以被视为“如果,那么”的规则。换句话说,如果某个条件满足,就做这个,否则做别的事情。我们示例树中的决策类似于机器学习中的响应变量的概念。如果我们为信用违约的案例研究问题做一个决策树,决策将会是预测二元响应值,即“此账户违约”或“此账户不违约”。回答二元是/否问题的树被称为分类树。然而,决策树非常多功能,也可以用于多类分类和回归问题。
树的最底层节点被称为叶子,或叶节点。在我们的示例中,叶子是最终的决策,即是否外出或待在室内。我们的树上有四个叶子,尽管你可以想象,如果树的深度只有一层,其中的决策仅基于云层情况,那么将会有两个叶子;在图 5.1中,节点 2 和节点 3 将是叶节点,分别以“外出”和“待在室内”作为决策。
在我们的示例中,每个层次上的每个节点都被分裂了。在严格意义上,这并非必要,因为你可能会选择在任何阳光明媚的日子外出,无论温度如何。在这种情况下,节点 2 将不会被分裂,因此该分支会在第一层次以“是”的决策结束。然而,在阴天的情况下,你的决策可能会涉及温度,这意味着该分支可以扩展到更深的层次。如果每个节点在最终层次之前都被分裂,考虑一下随着层数增加,叶子数量增长的速度。
例如,如果我们将图 5.1中的决策树再向下生长一个额外的层级,或许增加一个风速特征,以便考虑四种云层条件和温度的风寒效应会发生什么情况。现在作为叶子的四个节点,编号从四到七的节点图 5.1,将会基于每种情况的风速被拆分成两个更多的叶节点。然后,叶节点将变为4 × 2 = 8个。一般来说,应该清楚的是,在一个有 n 层的树中,若每个最终层之前的节点都被拆分,那么将会有2n个叶节点。考虑到这一点是很重要的,因为最大深度是你可以为决策树分类器设置的超参数之一。接下来我们将在以下练习中探讨这一点。
练习 5.01:在 Scikit-Learn 中使用决策树
在本练习中,我们将使用案例研究数据来生长一棵决策树,其中我们指定最大深度。我们还将使用一些便捷的功能来可视化决策树,使用的是 graphviz 包。请按以下步骤完成练习:
注意
本练习的 Jupyter notebook 可在 packt.link/IUt7d 找到。在开始练习之前,请确保你已按照前言中的说明设置好环境并导入必要的库。
-
加载我们一直在使用的几个包,并额外加载一个包
graphviz,以便我们可以可视化决策树:import numpy as np #numerical computation import pandas as pd #data wrangling import matplotlib.pyplot as plt #plotting package #Next line helps with rendering plots %matplotlib inline import matplotlib as mpl #add'l plotting functionality mpl.rcParams['figure.dpi'] = 400 #high res figures import graphviz #to visualize decision trees -
加载清理后的案例研究数据:
df = pd.read_csv('../Data/Chapter_1_cleaned_data.csv')注意
清理后的数据的位置可能因你保存数据的位置而有所不同。
-
获取数据框的列名列表:
features_response = df.columns.tolist() -
创建一个列出要移除的不是特征或响应变量的列的列表:
items_to_remove = ['ID', 'SEX', 'PAY_2', 'PAY_3',\ 'PAY_4', 'PAY_5', 'PAY_6',\ 'EDUCATION_CAT', 'graduate school',\ 'high school', 'none',\ 'others', 'university'] -
使用列表推导式从我们的特征列表和响应变量中移除这些列名:
features_response = [item for item in features_response if item not in items_to_remove] features_response这应该输出特征列表和响应变量:
['LIMIT_BAL', 'EDUCATION', 'MARRIAGE', 'AGE', 'PAY_1', 'BILL_AMT1', 'BILL_AMT2', 'BILL_AMT3', 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6', 'PAY_AMT1', 'PAY_AMT2', 'PAY_AMT3', 'PAY_AMT4', 'PAY_AMT5', 'PAY_AMT6', 'default payment next month']现在特征列表已准备好。接下来,我们将从 scikit-learn 导入一些库。我们需要进行训练/测试集拆分,这是我们已经熟悉的操作。我们还需要导入决策树功能。
-
运行以下代码从 scikit-learn 进行导入:
from sklearn.model_selection import train_test_split from sklearn import treescikit-learn 的
tree库包含与决策树相关的类。 -
使用我们在全书中使用的相同随机种子,将数据拆分为训练集和测试集:
X_train, X_test, y_train, y_test = \ train_test_split(df[features_response[:-1]].values, df['default payment next month'].values, test_size=0.2, random_state=24)在这里,我们使用列表中的所有元素(除了最后一个)来获取特征名称,而不是响应变量:
features_response[:-1]。我们用它从 DataFrame 中选择列,然后使用.values方法检索它们的值。我们对响应变量也做类似的操作,但直接指定列名。在进行训练/测试数据划分时,我们使用与之前相同的随机种子以及相同的划分比例。这样,我们可以直接将本章所做的工作与以前的结果进行比较。此外,我们继续保留与模型开发过程中的“未见测试集”相同的数据集。现在我们准备实例化决策树类。
-
通过将
max_depth参数设置为2来实例化决策树类:dt = tree.DecisionTreeClassifier(max_depth=2)我们使用了
DecisionTreeClassifier类,因为这是一个分类问题。由于我们指定了max_depth=2,当我们使用案例研究数据生长决策树时,树的最大深度将为2。现在让我们训练这个模型。 -
使用以下代码拟合决策树模型并生长树:
dt.fit(X_train, y_train)这应显示以下输出:
DecisionTreeClassifier(max_depth=2)现在我们已经拟合了这个决策树模型,我们可以使用
graphviz包来显示树的图形表示。 -
使用以下代码将训练好的模型导出为
graphviz包可以读取的格式:dot_data = tree.export_graphviz(dt, out_file=None, filled=True, rounded=True, feature_names=\ features_response[:-1], proportion=True, class_names=[ 'Not defaulted', 'Defaulted'])在这里,我们为
.export_graphviz方法提供了多个选项。首先,我们需要指定要绘制的训练模型,即dt。接下来,我们说不需要输出文件:out_file=None。相反,我们提供了dot_data变量来保存此方法的输出。其余选项设置如下:filled=True:每个节点将填充颜色。rounded=True:节点将呈现圆角边缘,而不是矩形。feature_names=features_response[:-1]:我们列表中的特征名称将被使用,而不是像X[0]这样的通用名称。proportion=True:每个节点中训练样本的比例将显示(我们稍后会进一步讨论)。class_names=['Not defaulted', 'Defaulted']:每个节点将显示预测类的名称。这个方法的输出是什么?
如果检查
dot_data的内容,你会发现它是一个长文本字符串。graphviz包可以解析这个文本字符串并创建可视化效果。 -
使用
graphviz包的.Source方法从dot_data创建图像并显示它:graph = graphviz.Source(dot_data) graph输出应如下所示:
![图 5.2:来自 graphviz 的决策树图]()
图 5.2:来自 graphviz 的决策树图
决策树的图形表示应直接呈现在你的 Jupyter 笔记本中,如图 5.2所示。
注意
或者,你可以通过为
out_file关键字参数提供文件路径,将.export_graphviz的输出保存到磁盘。例如,要将这个输出文件转换为图像文件,如.png文件,以便在演示中使用,你可以在命令行运行以下代码,并根据需要替换文件名:$ dot -Tpng <exported_file_name> -o <image_file_name_you_want>.png。关于
.export_graphviz选项的更多细节,你应参考 scikit-learn 文档(scikit-learn.org/stable/modules/generated/sklearn.tree.export_graphviz.html)。图 5.2中的可视化包含了许多有关决策树训练和如何使用它进行预测的信息。我们稍后会更详细地讨论训练过程,但简而言之,训练决策树的过程是从树顶部初始节点的所有训练样本开始,然后根据第一个节点中的
PAY_1 <= 1.5将这些样本分成两组。所有
PAY_1特征值小于或等于1.5的样本将在此布尔条件下表示为True。如图 5.2所示,这些样本会根据旁边写着True的箭头被排序到树的左侧。正如你在图表中看到的,每个被拆分的节点包含拆分标准的第一行文本。下一行与
gini有关,我们稍后会讨论。下一行包含每个节点中样本比例的信息。在顶部节点,我们从所有样本(
samples = 100.0%)开始。第一次拆分后,89.5%的样本被排序到左侧节点,剩余的 10.5%进入右侧节点。这些信息直接显示在可视化中,反映了如何使用训练数据来创建树。让我们通过检查训练数据来确认这一点。 -
要确认训练样本中
PAY_1特征小于或等于1.5的比例,首先识别该特征在features_response[:-1]特征名称列表中的索引:features_response[:-1].index('PAY_1')这段代码应输出如下内容:
4 -
现在,观察训练数据的形状:
X_train.shape这应为你提供以下输出:
(21331, 17)要确认决策树第一次拆分后的样本比例,我们需要知道满足
PAY_1特征布尔条件的样本比例,这些样本被用于进行此拆分。为此,我们可以使用训练数据中PAY_1特征的索引,这对应于特征名称列表中的索引,并使用训练数据中的样本数量,这个数量是我们从.shape观察到的行数。 -
使用此代码确认决策树第一次拆分后的样本比例:
(X_train[:,4] <= 1.5).sum()/X_train.shape[0]输出应如下所示:
0.8946134733486475通过对训练数据中与
PAY_1特征对应的列应用逻辑条件,然后计算满足该条件的样本数量,再除以样本总数,我们将其转换为比例。我们可以看到,从训练数据直接计算出的比例与图 5.2中第一次分裂后的左节点显示的比例相等。在第一次分裂之后,第一层中每个节点包含的样本会再次被分裂。随着进一步的分裂,树枝后续层级中任何给定节点的训练数据所占比例会越来越小,这一点可以在图 5.2中看到。
现在我们要解释节点中其余文本行的含义,这些节点出现在图 5.2中。以
value开头的行给出了每个节点中样本的响应变量类别比例。例如,在顶部节点中,我们看到value = [0.777, 0.223]。这些只是整体训练集的类别比例,你可以在下一步中验证这些比例。 -
使用以下代码计算训练集中的类别比例:
y_train.mean()输出应如下所示:
0.223102526838873这等同于在顶部节点中
value后面那对数字的第二个数;第一个数字就是减去该数字后的结果,换句话说,就是负类训练样本的比例。在每个后续节点中,都会显示该节点中样本的类别比例。类别比例也决定了节点的颜色:负类比例高于正类的节点为橙色,较深的橙色表示比例越高,而正类比例较高的节点则采用类似的蓝色配色方案。最后,以
class开头的行表示如果某个节点是叶节点,决策树如何根据给定的节点进行预测。分类的决策树通过确定样本根据特征值会被划分到哪个叶节点,然后预测该叶节点中大多数训练样本的类别来进行预测。这一策略意味着,树结构和叶节点中的类别比例是做出预测所需的信息。例如,如果我们没有进行任何分裂,且只能在不知道其他信息的情况下,仅凭整体训练数据的类别比例做出预测,那么我们将选择多数类别。由于大多数人不会违约,顶部节点的类别为
未违约。然而,深层节点中的类别比例不同,导致不同的预测。scikit-learn 是如何决定树的结构的呢?我们将在接下来的部分讨论训练过程。
max_depth 的重要性
回想一下,在本练习中我们指定的唯一超参数是max_depth,也就是在模型训练过程中决策树可以生长的最大深度。事实证明,这是最重要的超参数之一。如果没有对深度进行限制,树将继续生长,直到其他由其他超参数指定的限制起作用。这可能导致非常深的树,并且节点数量非常多。例如,考虑一棵深度为 20 的树,它可能有多少叶节点呢?这将是220个叶节点,超过 100 万个!我们甚至有足够的训练样本来将所有这些节点填充吗?在这种情况下,我们没有。显然,使用这些训练数据生长这样的树是不可能的,因为在最终层之前的每个节点都会被分裂。然而,如果我们移除max_depth限制并重新运行本练习的模型训练,观察效果:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16392_05_03.jpg)
图 5.3:没有最大深度限制的决策树的一部分
这里展示了一个使用默认选项生成的决策树的一部分,默认选项包括max_depth=None,意味着树的深度没有限制。整个树大约是这里展示部分的两倍宽。树的节点非常多,以至于它们只作为非常小的橙色或蓝色斑点出现;每个节点的具体解释并不重要,因为我们只是想说明树的规模可能会非常大。可以清楚地看出,如果没有超参数来控制树的生长过程,可能会生成极其庞大且复杂的树。
训练决策树:节点不纯度
到此为止,你应该已经了解了决策树是如何利用特征进行预测的,以及叶节点中训练样本的类别分布。现在,我们将学习决策树是如何训练的。训练过程涉及选择特征来对节点进行分裂,并决定分裂的阈值,例如在前面练习中的树的第一次分裂是PAY_1 <= 1.5。从计算角度来看,这意味着每个节点中的样本必须根据每个特征的值进行排序,以考虑分裂,并且在排序后的特征值之间的每对连续值都会被考虑作为潜在的分裂点。所有特征都可以被考虑,或者如我们稍后将要学习的那样,仅考虑一部分特征。
在训练过程中,如何决定分裂?
由于预测方法是选择叶节点的多数类,因此我们希望找到主要来自某一类的叶节点;选择多数类将是更准确的预测,节点越接近只包含某一类,其预测越准确。在理想情况下,训练数据可以被划分,使得每个叶节点完全包含正类或完全包含负类样本。然后,我们就可以高信心地认为,一旦新样本被分配到其中一个节点,它将是正类或负类。然而,在实践中,这种情况很少发生,几乎不会发生。然而,这说明了训练决策树的目标——也就是做出分裂,使得分裂后的两个节点具有更高的 纯度,换句话说,更接近只包含正类或负类样本。
在实践中,决策树实际上是使用纯度的逆,即 节点不纯度 进行训练的。这是衡量节点中训练样本距离完全属于某一类的程度的一个指标,类似于代价函数的概念,表示给定解决方案与理论上完美解决方案的差距。节点不纯度的最直观概念是 误分类率。采用广泛使用的符号(例如, scikit-learn.org/stable/modules/tree.html)表示每个节点中属于某一类的样本比例,我们可以定义 pmk 为第 m 个节点中属于第 k 类的样本比例。在二分类问题中,只有两类:k = 0 和 k = 1。对于给定的节点 m,误分类率就是该节点中较少见类别的比例,因为当该节点中的多数类作为预测类别时,所有这些样本都会被误分类。
让我们将误分类率可视化,作为开始思考决策树训练方式的一种方法。在程序中,我们使用 NumPy 的 linspace 函数考虑负类 k = 0 在节点 m 中可能的类比例 pm0,范围从 0.01 到 0.99:
pm0 = np.linspace(0.01,0.99,99)
pm1 = 1 - pm0
然后,这个节点的正类比例是 1 减去 pm0:

图 5.4:计算节点 m0 的正类比例的公式
现在,这个节点的误分类率将是 pm0 和 pm1 之间较小的类比例。我们可以使用 NumPy 的 minimum 函数来找到两个形状相同的数组中对应元素的较小值:
misclassification_rate = np.minimum(pm0, pm1)
误分类率与负类可能的类比例绘制出来是什么样的?
我们可以使用以下代码绘制这个图:
mpl.rcParams['figure.dpi'] = 400
plt.plot(pm0, misclassification_rate,
label='Misclassification rate')
plt.xlabel('$p_{m0}$')
plt.legend()
你应该得到这个图:

图 5.5:节点的误分类率
现在,很明显,负类的类分数pm0 越接近 0 或 1,误分类率就越低。那么在构建决策树时,如何利用这些信息呢?考虑一下可能遵循的过程。
每次在构建决策树时进行节点划分时,都会创建两个新节点。由于这两个新节点的预测值只是多数类,因此一个重要的目标是减少误分类率。因此,我们需要找到一个特征和该特征的一个值作为切分点,使得在所有类别上取平均后,两个新节点的误分类率尽可能低。这与实际训练决策树时使用的过程非常接近。
继续讨论最小化误分类率的思路,决策树训练算法通过考虑所有特征进行节点划分,尽管如果你将max_features超参数设置为少于特征总数的值,算法可能只会考虑一个随机选择的特征子集。稍后我们将讨论为什么要这么做。在任何情况下,算法会考虑每个候选特征的所有可能阈值,并选择那个能使得不纯度最低的阈值,不纯度的计算方式是通过加权每个节点的样本数量,计算两个新节点的平均不纯度。节点划分过程如图 5.6所示。该过程会一直重复,直到树的停止准则(如max_depth)达到:

图 5.6:如何选择特征和阈值来划分节点
虽然误分类率是一个直观的衡量不纯度的指标,但实际上还有更好的指标可以用来在模型训练过程中找到最佳分裂。scikit-learn 提供了两种可供选择的计算不纯度的方法,你可以通过criterion关键字参数来指定,分别是基尼不纯度和交叉熵。在这里,我们将从数学上描述这些方法,并展示它们与误分类率的比较。
基尼不纯度通过以下公式计算节点m的不纯度:

图 5.7:计算基尼不纯度的公式
在这里,求和是对所有类别进行的。在二分类问题中,只有两个类别,我们可以像下面这样编写程序:
gini = (pm0*(1-pm0)) + (pm1*(1-pm1))
交叉熵通过以下公式计算:

图 5.8:计算交叉熵的公式
使用这段代码,我们可以计算交叉熵:
cross_ent = -1*((pm0*np.log(pm0)) + (pm1*np.log(pm1)))
为了将 Gini 不纯度和交叉熵添加到我们的误分类率图中并查看它们的比较,我们只需要在绘制误分类率后添加以下代码行:
mpl.rcParams['figure.dpi'] = 400
plt.plot(pm0, misclassification_rate,\
label='Misclassification rate')
plt.plot(pm0, gini, label='Gini impurity')
plt.plot(pm0, cross_ent, label='Cross entropy')
plt.xlabel('$p_{m0}$')
plt.legend()
最终的图形应如下所示:

图 5.9:误分类率、Gini 不纯度和交叉熵
注意
如果你正在阅读本书的纸质版,你可以通过访问以下链接下载并浏览本章中某些图像的彩色版本:
与误分类率类似,Gini 不纯度和交叉熵在类别比例为 0.5 时最大,随着节点变得更加纯净——换句话说,当它们包含的类别比例更高时——它们会逐渐降低。然而,Gini 不纯度在某些类别比例区域比误分类率变化得更陡峭,这使得它能够更有效地找到最佳分裂点。交叉熵看起来变化得更加陡峭。那么,哪一个更适合你的工作呢?这是一个在所有数据集上都没有明确答案的问题。你应该在交叉验证超参数的过程中同时考虑这两种不纯度度量,以确定最合适的一个。需要注意的是,在 scikit-learn 中,Gini 不纯度可以通过 criterion 参数使用 'gini' 字符串来指定,而交叉熵则简单地称为 'entropy'。
用于第一次分裂的特征:与单变量特征选择和交互的关系
我们可以根据图 5.2所示的小树开始了解不同特征对决策树模型的重要性。注意,PAY_1是第一次分裂时选择的特征。这意味着它是在减少包含所有训练样本的节点不纯度方面表现最好的特征。回想我们在第三章“逻辑回归和特征探索的细节”中的单变量特征选择经验,其中PAY_1是通过 F 检验选出的最佳特征。因此,考虑到我们之前的分析,PAY_1出现在决策树的第一次分裂中是合理的。
在树的第二层,PAY_1上有另一个分裂,同时也有在BILL_AMT_1上的分裂。BILL_AMT_1在单变量特征选择中并没有列为重要特征。然而,可能是BILL_AMT_1与PAY_1之间存在一个重要的交互作用,而这种交互作用在单变量方法中无法发现。特别是,从决策树选择的分裂来看,似乎那些PAY_1值为 2 或更大的账户,并且BILL_AMT_1大于 568 的账户,尤其容易违约。PAY_1和BILL_AMT_1的这种组合效应是一种交互作用,这也可能是我们通过在前一章的活动中包含交互项来改善逻辑回归性能的原因。
训练决策树:一种贪婪算法
没有保证通过前述过程训练得到的决策树是找到最低不纯度叶节点的最佳决策树。这是因为训练决策树所使用的算法是一种所谓的贪婪算法。在这种情况下,这意味着在每次分裂节点的机会中,算法都会寻找当前时刻最佳的分裂,而不会考虑后续分裂机会受到影响的事实。
例如,考虑以下假设场景:案例研究的训练数据的最佳初始分裂涉及PAY_1,正如我们在图 5.2中所看到的。但是,如果我们改为在BILL_AMT_1上进行分裂,然后在下一级做PAY_1的后续分裂呢?即使初始在BILL_AMT_1上的分裂不是最优的,最终结果可能会更好,如果树是这样生长的。如果存在这样的解决方案,算法是无法找到的,因为它只考虑每个节点的最佳分裂,而不考虑未来可能的分裂。
我们仍然使用贪心的树生长算法的原因是,考虑所有可能的分裂方式需要的时间相当长,这样才能找到真正最优的树。尽管决策树训练过程中有这一缺陷,但仍然有方法可以减少贪心算法可能带来的不利影响。你可以将splitter关键字参数设置为random,以便在每个节点选择一个随机特征进行分裂,而不是寻找最优的分裂。默认值是best,它会搜索所有特征以找到最佳分裂。另一个我们已经讨论过的选项是,通过max_features关键字限制在每次分裂时将搜索的特征数量。最后,你还可以使用决策树的集成方法,如随机森林,我们稍后会介绍。请注意,所有这些选项除了可能避免贪心算法的副作用外,也是解决决策树常被批评的过拟合问题的选项。
训练决策树:不同的停止标准与其他选项
我们已经回顾了使用max_depth参数来限制树的生长深度。然而,scikit-learn 中还有几个其他可选项。这些选项主要与叶子节点中样本的数量相关,或者进一步分裂节点时如何减少不纯度。如前所述,树的生长深度可能会受到数据集大小的限制。如果分裂过程不再找到具有显著更高纯度的节点,那么继续加深树的深度可能没有意义。
我们在这里总结了你可以提供给DecisionTreeClassifier类的所有关键字参数,适用于 scikit-learn:

图 5.10:scikit-learn 中决策树分类器的完整选项列表
使用决策树:优势与预测概率
尽管决策树在概念上很简单,但它们具有多个实际优势。
无需缩放特征
考虑我们为什么需要对特征进行缩放以应用逻辑回归。一个原因是,对于一些基于梯度下降的解决算法,特征必须在相同的尺度上,以便快速找到成本函数的最小值。另一个原因是,当我们使用 L1 或 L2 正则化来惩罚系数时,所有特征必须在相同的尺度上,这样才能均等地对它们进行惩罚。而对于决策树,节点分裂算法会单独考虑每个特征,因此特征是否在相同尺度上并不重要。
非线性关系与交互作用
因为决策树中的每个后续分裂都是在先前分裂产生的训练样本子集上进行的,所以决策树能够描述单个特征的复杂非线性关系,以及特征之间的交互作用。回想我们之前在首次分裂所使用的特征:与单变量特征选择和交互作用的关联部分中的讨论。另外,作为一个假设的例子,考虑以下分类的合成数据集:

图 5.11:一个示例分类数据集,类别以红色和蓝色显示(如果是黑白阅读,请参阅 GitHub 仓库获取该图的彩色版本;蓝色点位于内圆圈内)
我们从第三章,逻辑回归和特征探索的详细信息中了解到,逻辑回归具有线性决策边界。那么,你认为逻辑回归如何处理像图 5.11中展示的数据集呢?你会在哪里画一条线来分隔蓝色和红色类别?应该很明显,在没有额外工程特征的情况下,逻辑回归不太可能是这个数据的好分类器。现在想一想决策树的“如果,那么”的规则,它可以与图 5.11中X和Y轴上表示的特征一起使用。你认为决策树对这组数据有效吗?
在这里,我们在背景中绘制了这两个模型的类别成员预测概率,使用红色和蓝色表示:

图 5.12:决策树和逻辑回归预测
在图 5.12中,两个模型的预测概率已经上色,深红色表示红色类别的较高预测概率,深蓝色表示蓝色类别的较高预测概率。我们可以看到,决策树能够将蓝色类别从红色点的中间圈分隔出来。这是因为,通过在节点分裂过程中使用X和Y坐标的阈值,决策树可以在数学上模拟蓝色和红色类别的位置依赖于X和Y坐标(交互作用),并且每个类别的可能性不是X或Y的线性增减函数(非线性)。因此,决策树方法能够正确分类大多数数据。
注意
生成图 5.11和图 5.12的代码可以在参考笔记本中找到:packt.link/9W4WN。
然而,逻辑回归具有线性决策边界,这将是背景中最浅蓝色和红色区域之间的直线。逻辑回归的决策边界穿过数据的中间,并未提供一个有效的分类器。这展示了决策树“开箱即用”的强大功能,而无需工程化非线性或交互特征。
预测概率
我们知道逻辑回归输出的是概率。然而,决策树是根据叶节点中的多数类来做出预测的。那么,像图 5.12中所示的预测概率从哪里来呢?实际上,决策树在 scikit-learn 中确实提供了.predict_proba方法来计算预测概率。该概率基于用于给定预测的叶节点中多数类的比例。例如,如果一个叶节点中 75%的样本属于正类,那么该节点的预测将是正类,预测的概率将是 0.75。来自决策树的预测概率在统计上不如广义线性模型的预测概率严谨,但它们仍然被广泛用于通过变化分类阈值来衡量模型性能的方法,如 ROC 曲线或精确度-召回曲线。
注意
在这里,我们专注于分类决策树,因为案例研究的性质。然而,决策树也可以用于回归,这使它成为一种多功能的方法。决策树的生长过程对于回归和分类是类似的,唯一的区别是,回归树不是寻求减少节点的不纯度,而是寻求最小化其他指标,如均方误差(MSE)或平均绝对误差(MAE),其中节点的预测可能是该节点中样本的平均值或中位数。
更便捷的交叉验证方法
在第四章,偏差-方差权衡中,我们通过编写自己的函数来进行交叉验证,深入理解了交叉验证的概念,使用KFold类来生成训练和测试索引。这对于全面理解这个过程如何工作非常有帮助。然而,scikit-learn 提供了一个便捷的类,可以为我们做更多繁重的工作:GridSearchCV。GridSearchCV可以作为输入,接受我们想要寻找最优超参数的模型,如决策树或逻辑回归,以及我们希望进行交叉验证的超参数“网格”。例如,在逻辑回归中,我们可能希望获得不同正则化参数C值下,所有折叠的平均交叉验证得分。在决策树中,我们可能希望探索不同的树深度。
你也可以一次性搜索多个参数,例如,如果我们想尝试不同的树深度和不同数量的max_features来考虑每个节点的分裂。
GridSearchCV执行的是所谓的“穷举式网格搜索”,遍历我们提供的所有可能的参数组合。这意味着,如果我们为每个超参数提供五个不同的值,那么交叉验证过程将运行 5 x 5 = 25 次。如果你在搜索许多超参数的多个值时,交叉验证的运行次数会迅速增加。在这种情况下,你可能会想使用RandomizedSearchCV,它会从你提供的网格中搜索超参数组合的一个随机子集。
GridSearchCV通过简化交叉验证过程,可以加速你的工作。你应该已经了解了前一章中交叉验证的概念,因此我们直接列出GridSearchCV可用的所有选项。
在接下来的练习中,我们将通过实际操作使用GridSearchCV,结合案例研究数据,来搜索决策树分类器的超参数。以下是GridSearchCV的选项:

图 5.13:GridSearchCV 的选项
在接下来的练习中,我们将利用均值的标准误差来创建误差条。我们将对模型性能指标在测试折中的平均值进行计算,误差条将帮助我们可视化模型性能在各个折中的变化程度。
平均值的标准误差也被称为样本均值的抽样分布的标准差。这个名字很长,但概念并不复杂。其背后的思想是,我们希望为模型性能度量创建误差条的总体,代表了从一个理论上较大的类似样本群体中抽取样本的一种可能方式,例如如果有更多数据可用并用它进行更多的测试折叠。如果我们能从较大的总体中进行反复抽样,每次抽样事件会导致略微不同的均值(样本均值)。通过反复抽样事件构建这些均值的分布(样本均值的抽样分布)可以让我们知道这个抽样分布的方差,这将作为样本均值的不确定性度量。事实证明,这个方差(我们称之为
,其中
表示这是样本均值的方差)取决于我们样本中的观察次数(n):它与样本大小成反比,但也与更大、未观察的总体方差
成正比。如果您在处理样本均值的标准差,简单地对两边取平方根:
。虽然我们不知道
的真实值,因为我们无法观察到理论总体,但我们可以通过观察到的测试折叠的总体方差来估计它。
这是统计学中的一个关键概念,称为中心极限定理。
练习 5.02:为决策树寻找最优超参数
在本练习中,我们将使用GridSearchCV来调优决策树模型的超参数。您将学习一种使用 scikit-learn 搜索不同超参数的便捷方法。请执行以下步骤来完成练习:
注意
在开始本练习之前,您需要导入必要的包并加载已清理的数据框。您可以参考以下 Jupyter notebook 来了解前提步骤:packt.link/SKuoB。
-
使用以下代码导入
GridSearchCV类:from sklearn.model_selection import GridSearchCV下一步是定义我们希望使用交叉验证进行搜索的超参数。我们将通过
max_depth参数找到树的最佳最大深度。深度较大的树会有更多的节点分裂,这些分裂使用特征将训练集划分为越来越小的子空间。虽然我们无法预先知道最佳的最大深度,但在考虑用于网格搜索的参数范围时,考虑一些极限情况是有帮助的。我们知道,1 是最小深度,由只有一个分割的树构成。至于最大深度,你可以考虑你的训练数据中有多少个样本,或者更适当地,在这种情况下,考虑交叉验证每次分割时,训练折叠中有多少个样本。我们将像上一章一样执行 4 折交叉验证。那么,每个训练折叠中将有多少样本,这与树的深度有什么关系?
-
使用以下代码查找训练数据中的样本数量:
X_train.shape输出应如下所示:
(21331, 17)在 21,331 个训练样本和 4 折交叉验证的情况下,每个训练折叠中将有三分之四的样本,即大约 16,000 个样本。
max_depth超参数。我们将探索从 1 到 12 的深度范围。 -
定义一个字典,键为超参数名称,值为我们想要在交叉验证中搜索的该超参数的值列表:
params = {'max_depth':[1, 2, 4, 6, 8, 10, 12]}在这种情况下,我们只搜索一个超参数。然而,你可以定义一个字典,包含多个键值对,来同时搜索多个超参数。
-
如果你在一个笔记本中运行本章的所有练习,可以重用之前的决策树对象
dt。如果没有,你需要为超参数搜索创建一个决策树对象:dt = tree.DecisionTreeClassifier()现在我们想要实例化
GridSearchCV类。 -
使用这些选项实例化
GridSearchCV类:cv = GridSearchCV(dt, param_grid=params, scoring='roc_auc', n_jobs=None, refit=True, cv=4, verbose=1, pre_dispatch=None, error_score=np.nan, return_train_score=True)请注意,我们使用的是 ROC AUC 指标(
scoring='roc_auc'),进行 4 折交叉验证(cv=4),并计算训练分数(return_train_score=True)来评估偏差-方差权衡。一旦交叉验证对象被定义,我们可以像使用模型对象一样,简单地对其使用
.fit方法。这基本上封装了我们在上一章中编写的交叉验证循环的所有功能。 -
使用以下代码执行 4 折交叉验证,搜索最优的最大深度:
cv.fit(X_train, y_train)输出应如下所示:
![图 5.14:交叉验证拟合输出]()
图 5.14:交叉验证拟合输出
我们指定的所有选项都会作为输出打印出来。此外,还有一些关于执行了多少次交叉验证拟合的输出信息。我们有 4 个折叠和 7 个超参数,意味着执行了 4 x 7 = 28 次拟合。还显示了这所花费的时间。你可以通过
verbose关键字参数控制从该过程中获得的输出量;较大的数字意味着更多的输出。现在是时候查看交叉验证过程的结果了。在已拟合的
GridSearchCV对象上,有一个方法是.cv_results_。这是一个字典,字典的键是结果的名称,值是结果本身。例如,mean_test_score键包含了每个超参数的平均测试得分。你可以通过在代码单元中运行cv.cv_results_来直接查看这个输出。然而,这样查看输出不太方便。具有这种结构的字典可以直接用于创建 pandas DataFrame,这样查看结果会稍微容易一些。 -
运行以下代码来创建并查看一个 pandas DataFrame,该 DataFrame 显示了交叉验证的结果:
cv_results_df = pd.DataFrame(cv.cv_results_) cv_results_df输出应如下所示:
![图 5.15:交叉验证结果 DataFrame 的前几列]()
](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16392_05_15.jpg)
图 5.15:交叉验证结果 DataFrame 的前几列
DataFrame 中每一行代表网格中每一组超参数的组合。由于我们这里只搜索一个超参数,因此每一行代表我们搜索的七个值之一。你可以看到每一行的输出信息,包括每一折训练(拟合)和测试(评分)所用时间的均值和标准差,单位为秒。搜索的超参数值也会显示出来。在图 5.16中,我们可以看到第一折(索引 0)的测试数据的 ROC AUC 分数。那么结果 DataFrame 中其余的列包含了什么内容呢?
-
使用以下代码查看结果 DataFrame 中剩余列的名称:
cv_results_df.columns输出应如下所示:
Index(['mean_fit_time', 'std_fit_time',\ 'mean_score_time', 'std_score_time',\ 'param_max_depth', 'params',\ 'split0_test_score', 'split1_test_score',\ 'split2_test_score', 'split3_test_score',\ 'mean_test_score', 'std_test_score',\ 'rank_test_score', 'split0_train_score',\ 'split1_train_score', 'split2_train_score',\ 'split3_train_score', 'mean_train_score',\ 'std_train_score'], dtype='object')交叉验证结果 DataFrame 中的列包括每一折的测试得分、它们的平均值和标准差,以及训练得分的相同信息。
一般来说,“最佳”超参数组合是具有最高平均测试得分的组合。这是对使用这些超参数拟合的模型,在新数据上评分时可能表现如何的估计。我们绘制一张图,展示平均测试得分如何随
max_depth超参数变化。我们还将在同一张图上展示平均训练得分,以查看随着我们允许在模型拟合过程中生长更深、更复杂的树,偏差和方差是如何变化的。我们将 4 折的训练和测试得分的标准误差作为误差条,使用 Matplotlib 的
errorbar函数。这将显示得分在各折之间的变异情况。 -
执行以下代码,创建一个关于每个
max_depth值的训练和测试得分的误差条图,这些值是在交叉验证中进行检查的:ax = plt.axes() ax.errorbar(cv_results_df['param_max_depth'], cv_results_df['mean_train_score'], yerr=cv_results_df['std_train_score']/np.sqrt(4), label='Mean $\pm$ 1 SE training scores') ax.errorbar(cv_results_df['param_max_depth'], cv_results_df['mean_test_score'], yerr=cv_results_df['std_test_score']/np.sqrt(4), label='Mean $\pm$ 1 SE testing scores') ax.legend() plt.xlabel('max_depth') plt.ylabel('ROC AUC')该图应如下所示:
![图 5.16:跨四折的训练和测试得分的误差条图]()
](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16392_05_15.jpg)
图 5.16:跨四折的训练和测试得分的误差条图
请注意,标准误差是通过标准差除以折叠数量的平方根来计算的。训练和测试评分的标准误差以垂直线的形式显示在每个尝试过的max_depth值处;平均分数上下的距离表示 1 个标准误差。在制作误差条图时,最好确保误差测量的单位与y轴的单位相同。在本例中,它们是相同的,因为标准误差的单位与底层数据相同,而不是像方差那样的平方单位。
错误条表示评分在不同折叠之间的变动情况。如果各个折叠之间的变异性很大,这表明数据在不同折叠之间的性质存在差异,从而影响了我们模型的拟合能力。这可能是个问题,因为这意味着我们可能没有足够的数据来训练一个能在新数据上稳定表现的模型。然而,在我们这里,折叠之间的变异性并不大,因此这不是问题。
那么,不同max_depth值下训练和测试评分的总体趋势如何呢?我们可以看到,随着树的深度越来越大,模型对训练数据的拟合越来越好。如前所述,如果我们把树长得足够深,使得每个叶节点只有一个训练样本,我们将会创建一个非常针对训练数据的模型。事实上,它将完美地拟合训练数据。我们可以说,这样的模型具有极高的方差。
但是,训练集上的表现不一定会转化到测试集上。在图 5.16中,我们可以明显看到,增加max_depth只会在某个点之前提高测试评分,而超过该点后,树的深度增加反而导致测试表现下降。这是另一个例子,说明我们如何利用偏差-方差权衡来创建更好的预测模型——类似于我们使用正则化的逻辑回归。较浅的树具有更高的偏差,因为它们无法很好地拟合训练数据。但这是可以接受的,因为如果我们接受一定的偏差,我们将在测试数据上获得更好的表现,而测试数据才是我们最终关心的指标。
在这种情况下,我们选择max_depth = 6。你也可以通过尝试 2 到 12 之间的每个整数来进行更彻底的搜索,而不是像我们这里那样每次跳 2 个值。一般来说,最好尽可能深入地探索参数空间,直到你有的计算时间为止。在本例中,这将导致相同的结果。
模型比较
到目前为止,我们已经对案例研究数据进行了几种不同机器学习模型的 4 折交叉验证。那么,我们的表现如何呢?到目前为止,我们的最佳表现是什么?在上一章中,我们使用逻辑回归得到了平均测试 ROC AUC 为 0.718,通过在逻辑回归中工程化交互特征得到了 0.740。而在这里,使用决策树,我们可以达到 0.745。所以,我们在模型性能上有所提升。现在,让我们探索另一种基于决策树的模型,看看是否能够进一步提高性能。
随机森林:决策树的集成
正如我们在前面的练习中看到的,决策树容易出现过拟合问题。这是它们使用的主要批评之一,尽管它们具有高度的可解释性。然而,我们通过限制树的最大深度,能够在一定程度上限制这种过拟合。
基于决策树的概念,机器学习研究人员利用多棵树作为更复杂过程的基础,最终形成了一些最强大且最广泛使用的预测模型。在本章中,我们将重点介绍决策树的随机森林。随机森林是所谓的集成模型的例子,因为它们是通过组合其他更简单的模型形成的。通过结合多个模型的预测,可以改善任何给定模型的缺陷。这有时被称为将多个弱学习者结合成一个强学习者。
一旦你理解了决策树,随机森林背后的概念就非常简单。那是因为随机森林只是许多决策树的集成;这种集成中的所有模型都具有相同的数学形式。那么,一个随机森林中会包括多少个决策树模型呢?这是构建随机森林模型时需要指定的超参数之一,n_estimators。一般来说,树木越多越好。随着树木数量的增加,整个集成的方差将减少。这应该导致随机森林模型对新数据的泛化能力更强,反映在测试分数的提高上。然而,在某个点之后,增加树木的数量将不再显著提高模型的性能。
那么,随机森林是如何减少影响决策树的高方差(过拟合)问题的呢?这个问题的答案在于森林中不同树的不同之处。树之间的差异主要有两种方式,其中一种我们已经熟悉:
-
每次划分时考虑的特征数量
-
用于生长不同树的训练样本
每次划分时考虑的特征数量
我们已经熟悉了DecisionTreeClassifier类中的这个选项:max_features。在之前使用该类时,我们将max_features保持在默认值None,这意味着每次分割时会考虑所有特征。通过使用所有特征来拟合训练数据,可能会导致过拟合。通过限制每次分割时考虑的特征数量,随机森林中的某些决策树可能会找到更好的分割点。这是因为,尽管它们仍在贪心地寻找最佳分割,但它们是在有限的特征选择下进行的。这可能会使得某些分割在树的后续部分变得可能,而如果在每次分割时都搜索所有特征,可能就无法找到这些分割点。
在 scikit-learn 中的RandomForestClassifier类中有一个max_features选项,就像在DecisionTreeClassifier类中一样,这两个选项是类似的。然而,对于随机森林,默认设置是'auto',这意味着算法每次分割时只会搜索可能特征数量的平方根的随机选择。例如,从总共有 9 个可能特征中,随机选择√9 = 3 个特征。由于森林中的每棵树在生长过程中可能会选择不同的特征随机分割,因此森林中的树木不会完全相同。
用于生成不同树木的样本
随机森林中的树木彼此之间的另一种区别是它们通常使用不同的训练样本进行生长。为此,需要使用一种叫做自助抽样(bootstrapping)的统计方法,这意味着从原始数据中生成新的合成数据集。合成数据集通过从原始数据集中随机选择样本来创建,并允许重复选择。这里的“重复选择”意味着,如果我们选择了某个样本,我们将继续考虑该样本用于选择,也就是说,它在被采样后会被“替换”到原始数据集中。合成数据集中的样本数量与原始数据集中的样本数量相同,但由于替换机制,一些样本可能会重复,而另一些样本则可能完全不在其中。
使用随机抽样创建合成数据集并分别对其进行训练的过程称为袋装法(bagging),即自助聚合(bootstrapped aggregation)的简称。事实上,袋装法可以与任何机器学习模型一起使用,而不仅仅是决策树,scikit-learn 提供了该功能,用于分类问题(BaggingClassifier)和回归问题(BaggingRegressor)。对于随机森林来说,袋装法默认开启,且bootstrap选项被设置为True。但是,如果你希望森林中的所有树都使用全部训练数据进行生长,可以将该选项设置为False。
现在你应该对随机森林有了一个较好的理解。正如你所见,如果你已经熟悉决策树,那么理解随机森林并不涉及太多额外的知识。这个事实的体现是,scikit-learn中的RandomForestClassifier类的超参数大多数与DecisionTreeClassifier类的超参数相同。
除了我们之前讨论过的n_estimators和bootstrap,还有两个新选项,它们超出了决策树的可用选项:
-
oob_score,一个bool值:此选项控制是否计算 OOB 分数,True表示计算,False(默认值)表示不计算。 -
warm_start,一个bool值:默认值为False——如果将其设置为True,则重新使用相同的随机森林模型对象会在已生成的森林中添加额外的树。 -
max_samples,一个int或float值:控制在使用自助法程序训练每棵树时使用多少样本。默认值是使用与原始数据集相同的样本数。
其他类型的集成模型
随着我们现在所知道的,随机森林是袋装集成的一种示例。另一种集成方法是提升集成。提升的一般思路是使用一系列相同类型的新模型,并且将它们训练在前一个模型的错误上。通过这种方式,后续模型学习到早期模型未能解决的问题,并对这些错误进行修正。提升在决策树中获得了成功的应用,并且在scikit-learn和另一个流行的 Python 库 XGBoost 中都可以使用。我们将在下一章讨论提升。
堆叠集成方法是一种较为高级的集成方法,在这种方法中,集成中的不同模型(估计器)不需要像在袋装法和提升法中那样是相同类型的。例如,你可以通过随机森林和逻辑回归来构建一个堆叠集成。集成中不同成员的预测会通过另一个模型(堆叠器)结合起来进行最终预测,该模型将堆叠模型的预测作为特征来考虑。
随机森林:预测与可解释性
由于随机森林只是决策树的集合,因此,必须以某种方式将所有这些树的预测结合起来,以得出随机森林的预测。
在模型训练后,分类树将接受一个输入样本并生成一个预测类别,例如,在我们的案例研究问题中,预测信用账户是否会违约。将这些树的预测组合成森林的最终预测的一种直观方法是采用多数投票。也就是说,无论所有树的最常见预测是什么,它就成为该样本的森林预测。这是最初描述随机森林的文献中采用的方法(scikit-learn.org/stable/modules/ensemble.html#forest)。然而,scikit-learn 使用的是一种稍有不同的方法:将每个类别的预测概率相加,然后选择具有最高概率和的类别。这比仅仅选择预测类别捕获了更多来自每棵树的信息。
随机森林的可解释性
决策树的主要优势之一是它能够直观地看到每个单独预测的生成过程。你可以通过一系列“如果, 那么”的规则追溯任何样本的决策路径,准确知道它是如何得出该预测的。相反,假设你有一个包含 1,000 棵树的随机森林,这意味着有 1,000 组这样的规则,它们比单一规则更难以向人类传达!
尽管如此,仍然有多种方法可以用来理解随机森林是如何做出预测的。一种简单的方法是观察特征重要性,这种方法可以在 scikit-learn 中使用。随机森林的特征重要性是衡量在生成森林中的树木时,每个特征的有用程度。这种有用性是通过结合每个特征在训练样本中被用于分裂的比例,以及由此导致的节点杂质降低来度量的。
由于特征重要性计算,可以通过该计算按特征在随机森林模型中的影响力对特征进行排序,因此,随机森林还可以用于特征选择。
练习 5.03:拟合随机森林
在这个练习中,我们将通过使用随机森林模型并在案例研究中的训练数据上进行交叉验证,来扩展我们在决策树方面的工作。我们将观察增加森林中树木数量的效果,并检查可以使用随机森林模型计算的特征重要性。请执行以下步骤来完成此练习:
注意
本练习的 Jupyter 笔记本可以在packt.link/VSz2T找到。此笔记本包含了导入必要库和加载清洗后的数据框的前提步骤。请在开始本练习之前执行这些步骤。
-
按如下方式导入随机森林分类器模型类:
from sklearn.ensemble import RandomForestClassifier -
使用这些选项实例化类:
rf = RandomForestClassifier(n_estimators=10,\ criterion='gini',\ max_depth=3,\ min_samples_split=2,\ min_samples_leaf=1,\ min_weight_fraction_leaf=0.0,\ max_features='auto',\ max_leaf_nodes=None,\ min_impurity_decrease=0.0,\ min_impurity_split=None,\ bootstrap=True,\ oob_score=False,\ n_jobs=None, random_state=4,\ verbose=0,\ warm_start=False,\ class_weight=None)对于这个练习,我们主要使用默认选项。但请注意,我们将设置
max_depth = 3。在这里,我们只探索使用不同数量的树的效果,因此选择相对较浅的树以便更短的运行时间。要找到最佳模型性能,通常会尝试更多的树和更深的树。我们还为了保证运行结果的一致性设置了
random_state。 -
创建参数网格以便在范围从 10 到 100 的树中搜索:
rf_params_ex = {'n_estimators':list(range(10,110,10))}我们使用 Python 的
range()函数创建所需整数值的迭代器,然后使用list()将其转换为列表。 -
使用先前步骤中的参数网格实例化随机森林模型的网格搜索交叉验证对象。否则,您可以使用与决策树交叉验证相同的选项:
cv_rf_ex = GridSearchCV(rf, param_grid=rf_params_ex, scoring='roc_auc', n_jobs=None, refit=True, cv=4, verbose=1, pre_dispatch=None, error_score=np.nan, return_train_score=True) -
将交叉验证对象拟合如下:
cv_rf_ex.fit(X_train, y_train)拟合过程应该输出以下内容:
![图 5.17:随机森林交叉验证的输出 在不同数量的树上]()
图 5.17:随机森林在不同数量的树上的交叉验证输出
你可能已经注意到,尽管我们只在 10 个超参数值上进行交叉验证,与前一练习中决策树所检验的 7 个值相比,这次的交叉验证花费的时间明显更长。考虑一下我们在这种情况下生成了多少棵树。对于最后一个超参数
n_estimators = 100,我们在所有交叉验证的拆分中总共生成了 400 棵树。我们刚才尝试的各种树的模型拟合时间是多长?通过使用更多的树,我们在交叉验证测试性能方面获得了多大的提升?这些都是通过绘图来检查的好方法。首先,我们将交叉验证结果提取到一个 pandas DataFrame 中,就像以前做过的那样。
-
将交叉验证结果放入 pandas DataFrame 中:
cv_rf_ex_results_df = pd.DataFrame(cv_rf_ex.cv_results_)您可以在附带的 Jupyter 笔记本中查看整个 DataFrame。这里,我们直接转向创建感兴趣的量的图形。我们将制作一条线图,其中包含每个超参数的平均拟合时间(包含在
mean_fit_time列中的符号),以及一个测试分数的误差条形图,这些我们已经为决策树做过。两个图都将根据* x *轴上的树数量进行绘制。 -
创建两个子图,分别显示平均训练时间和平均测试分数及标准误差:
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(6, 3)) axs[0].plot(cv_rf_ex_results_df['param_n_estimators'], cv_rf_ex_results_df['mean_fit_time'], '-o') axs[0].set_xlabel('Number of trees') axs[0].set_ylabel('Mean fit time (seconds)') axs[1].errorbar(cv_rf_ex_results_df['param_n_estimators'], cv_rf_ex_results_df['mean_test_score'], yerr=cv_rf_ex_results_df['std_test_score']/np.sqrt(4)) axs[1].set_xlabel('Number of trees') axs[1].set_ylabel('Mean testing ROC AUC $\pm$ 1 SE ') plt.tight_layout()在这里,我们使用
plt.subplots一次创建了一个 figure 内的两个轴,配置为一行两列。然后,我们通过对返回的axs轴数组进行索引来访问轴对象,以便创建绘图。输出应该类似于此图:
![图 5.18:不同数量的树的平均拟合时间和测试分数 森林中的树的数量]()
图 5.18:不同数量树木的森林的平均拟合时间和测试分数
注
由于平台差异或设置了不同的随机种子,你的结果可能会有所不同。
关于这些可视化,还有几个要注意的点。首先,我们可以看到,通过使用随机森林,我们在交叉验证测试折叠上的模型表现超过了我们之前的任何尝试。虽然我们还没有调整随机森林的超参数以获得最佳的模型性能,但这是一个有前景的结果,表明随机森林将是我们建模工作中一个有价值的补充。
然而,随着这些更高模型测试分数的提升,请注意,折叠之间的变异性也比我们在决策树中看到的更大;这种变异性在模型测试分数的标准误差中表现得更加明显。虽然这表明使用该模型时,模型性能的范围可能更广,但我们建议直接在 Jupyter notebook 中的 pandas DataFrame 中查看各个折叠的模型测试分数。你会看到,即使是个别折叠的最低分数,仍然高于决策树的平均测试分数,这表明使用随机森林会更好。
那么,我们用这个可视化来探讨的其他问题如何呢?我们感兴趣的是查看拟合不同数量树木的随机森林模型需要多长时间,以及使用更多树木时模型性能的提升。图 5.18 左侧的小图显示,随着树木数量的增加,训练时间呈现出相当线性的增长。这可能是可以预期的;通过增加更多树木,我们实际上是在增加训练过程中需要进行的计算量。
但是,考虑到模型性能的提升,这额外的计算时间是否值得呢?图 5.18 右侧的小图显示,超过大约 20 棵树后,添加更多树木并不一定能可靠地提高测试性能。虽然拥有 50 棵树的模型得分最高,但添加更多树木实际上使测试分数略有下降,这表明 50 棵树的 ROC AUC 增益可能只是由于随机性,因为理论上增加更多树木应该提高模型性能。基于这个推理,如果我们将
max_depth = 3作为限制,我们可能会选择 20 棵或许 50 棵树的森林继续进行。但在本章最后的活动中,我们将更全面地探索这个参数空间。最后,请注意,我们没有在这里显示训练集的 ROC AUC 指标。如果你绘制这些或在结果数据框中查看,你会发现训练分数高于测试分数,这表明某些过拟合现象正在发生。尽管如此,仍然可以确定这个随机森林模型的交叉验证测试分数比我们观察到的任何其他模型的分数要高。基于这个结果,我们很可能会选择此时的随机森林模型。
为了更深入地了解我们可以通过拟合的交叉验证对象访问哪些内容,让我们看看最佳超参数和特征重要性。
-
使用以下代码查看交叉验证中最好的超参数:
cv_rf_ex.best_params_这应该是输出结果:
{'n_estimators': 50}这里,best 仅仅指的是那些导致最高平均模型测试分数的超参数。
-
运行此代码以创建特征名称和重要性的数据框,然后显示按重要性排序的横向条形图:
feat_imp_df = pd.DataFrame({ 'Importance':cv_rf_ex.best_estimator_.feature_importances_ }, index=features_response[:-1]) feat_imp_df.sort_values('Importance', ascending=True).plot.barh()图表应该是这样的:
![图 5.19:来自随机森林的特征重要性]()
图 5.19:来自随机森林的特征重要性
在这段代码中,我们创建了一个包含特征重要性的字典,并将它与特征名称作为索引一起使用,以创建一个数据框。特征重要性来自拟合后的交叉验证对象的best_estimator_方法,所以它指的是具有最高平均测试分数的模型(换句话说,就是具有 50 棵树的模型)。这是一种访问随机森林模型对象的方法,该对象已经在所有训练数据上进行了训练,并使用了交叉验证网格搜索找到的最佳超参数。feature_importances_是可以在已拟合的随机森林模型上使用的方法。
在访问了所有这些属性后,我们将它们绘制在一个横向条形图上,这是一种方便查看特征重要性的方法。请注意,来自随机森林的前五个最重要特征与第三章中通过 ANOVA F 检验选择的前五个特征相同,尽管它们的顺序有所不同。这是不同方法之间的一种良好确认。
棋盘图
在继续进行活动之前,我们展示了一种在 Matplotlib 中进行可视化的技术。绘制一个二维网格,并在其上用彩色方块或其他形状表示数据,当你想展示数据的三个维度时,这种方法很有用。这里,颜色表示第三维度。例如,您可能想要在两个超参数的网格上可视化模型的测试分数,就像我们将在活动 5.01中做的那样,使用随机森林的交叉验证网格搜索。
过程的第一步是创建 x 和 y 坐标的网格。可以使用 NumPy 的 meshgrid 函数来完成这项工作。该函数接受一维的 x 和 y 坐标数组,并创建所有可能的坐标对的网格。网格中的点将是棋盘图中每个方块的角落。下面是一个 4 x 4 彩色块网格的代码示例。由于我们指定了角落,所以我们需要一个 5 x 5 的点网格。我们还展示了 x 和 y 坐标的数组:
xx_example, yy_example = np.meshgrid(range(5), range(5))
print(xx_example)
print(yy_example)
输出如下:
[[0 1 2 3 4]
[0 1 2 3 4]
[0 1 2 3 4]
[0 1 2 3 4]
[0 1 2 3 4]]
[[0 0 0 0 0]
[1 1 1 1 1]
[2 2 2 2 2]
[3 3 3 3 3]
[4 4 4 4 4]]
用于在此网格上绘制的数据应该具有 4 x 4 的形状。我们创建了一个包含从 1 到 16 的整数的一维数组,并将其重塑为一个二维的 4 x 4 网格:
z_example = np.arange(1,17).reshape(4,4)
z_example
这将输出以下内容:
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
我们可以使用以下代码在 xx_example, yy_example 网格上绘制 z_example 数据。请注意,我们使用 pcolormesh 来生成图表,并采用 jet 色图,这会给出彩虹色的颜色刻度。我们还添加了一个 colorbar,它需要传入 pcolor_ex 对象(由 pcolormesh 返回)作为参数,这样颜色刻度的解释就会清晰:
ax = plt.axes()
pcolor_ex = ax.pcolormesh(xx_example, yy_example, z_example,
cmap=plt.cm.jet)
plt.colorbar(pcolor_ex, label='Color scale')
ax.set_xlabel('X coordinate')
ax.set_ylabel('Y coordinate')

图 5.20:连续整数的 pcolormesh 图
活动 5.01:使用随机森林进行交叉验证网格搜索
在这个活动中,你将对随机森林模型在案例研究数据上进行网格搜索,搜索的超参数包括森林中的树木数量(n_estimators)和树的最大深度(max_depth)。然后,你将创建一个可视化,展示你搜索过的超参数网格的平均测试得分。请按照以下步骤完成此活动:
-
创建一个字典,表示将要搜索的
max_depth和n_estimators超参数的网格。包括深度 3、6、9 和 12,以及树木数量 10、50、100 和 200。将其他超参数保持为默认值。 -
使用本章前面提到的相同选项实例化一个
GridSearchCV对象,但使用步骤 1 中创建的超参数字典。设置verbose=2,以查看每次拟合过程的输出。你可以重用我们之前使用的随机森林模型对象rf,或者创建一个新的。 -
在训练数据上拟合
GridSearchCV对象。 -
将网格搜索的结果放入 pandas DataFrame 中。
-
创建一个
pcolormesh可视化,显示每个超参数组合的平均测试得分。你应该得到类似于以下的可视化效果:![图 5.21:随机森林交叉验证结果 在具有两个超参数的网格上]()
图 5.21:随机森林在具有两个超参数的网格上的交叉验证结果
-
确定使用哪组超参数。
注意
包含此活动的 Python 代码的 Jupyter 笔记本可以在
packt.link/D0OBc找到。此活动的详细分步解决方案可以通过这个链接找到。
总结
在本章中,我们学习了如何使用决策树和由多个决策树组成的集成模型——随机森林。通过使用这些简单构思的模型,我们能够比使用逻辑回归做出更好的预测,这从交叉验证的 ROC AUC 分数中可以看出。这种情况在许多实际问题中都适用。决策树对于很多可能影响逻辑回归模型性能的潜在问题具有较强的鲁棒性,例如特征与响应变量之间的非线性关系,以及特征之间复杂交互的存在。
尽管单棵决策树容易出现过拟合问题,但随机森林集成方法已被证明能够减少这一高方差问题。随机森林通过训练多棵树来构建。通过仅在部分可用训练集上训练每棵树(自助聚合或袋 ging),并且在每个节点分裂时只考虑减少的特征数量,树的集成可以减少方差,同时增加单棵树的偏差。
现在我们已经尝试了几种不同的机器学习方法来建模案例数据,发现有些方法效果更好;例如,经过调优的超参数的随机森林提供了最高的平均交叉验证 ROC AUC 分数 0.776,正如我们在活动 5中看到的,使用随机森林进行交叉验证网格搜索。
在下一章中,我们将学习另一种集成方法——梯度提升,它通常与决策树结合使用。梯度提升在所有机器学习模型中为二分类问题提供了一些最佳性能。我们还将学习一种强大的方法,通过SHapely 加法解释(SHAP)值来解释和解读梯度提升树集成的预测。
第六章:6. 梯度提升、XGBoost 与 SHAP 值
概述
阅读完本章后,您将能够描述梯度提升的概念,这是 XGBoost 包的基本理念。然后,您将通过合成数据训练 XGBoost 模型,并在此过程中学习早期停止以及几个 XGBoost 超参数。除了使用我们之前讨论的相似方法来生成树(通过设置max_depth),您还将发现 XGBoost 提供的另一种生成树的新方式:基于损失的树生成。在学习了 XGBoost 之后,您将接触到一种新的、强大的模型预测解释方法,称为SHAP(SHapley Additive exPlanations)。您将看到如何使用 SHAP 值为任何数据集的模型预测提供个性化的解释,而不仅仅是训练数据,同时也理解 SHAP 值的加法属性。
介绍
正如我们在上一章中看到的,基于决策树和集成模型提供了强大的方法来创建机器学习模型。尽管随机森林已经存在了几十年,但最近关于另一种树集成方法——梯度提升树的研究,已经产生了最先进的模型,这些模型在预测建模领域,尤其是在使用结构化表格数据(如案例研究数据)方面,占据了主导地位。如今,机器学习数据科学家使用的两个主要包是 XGBoost 和 LightGBM,用于创建最准确的预测模型。在本章中,我们将使用合成数据集熟悉 XGBoost,然后在活动中将其应用到案例研究数据中。
注意
也许使用 XGBoost 的最佳动机之一,来自于描述这个机器学习系统的论文,尤其是在 Kaggle 这个流行的在线机器学习竞赛论坛的背景下:
“在 2015 年 Kaggle 博客上发布的 29 个挑战获胜解决方案中,有 17 个解决方案使用了 XGBoost。在这些解决方案中,8 个仅使用 XGBoost 来训练模型,而大多数其他解决方案则将 XGBoost 与神经网络结合使用在集成中。相比之下,第二受欢迎的方法——深度神经网络,在 11 个解决方案中得到了使用。”(陈和郭斯特林,2016,dl.acm.org/doi/abs/10.1145/2939672.2939785)
正如我们将看到的,XGBoost 将我们迄今为止讨论的几种不同思想结合起来,包括决策树、集成建模以及梯度下降。
除了性能更强的模型,近年来的机器学习研究还提供了更详细的模型预测解释方式。我们不再依赖仅仅表示模型训练集整体的解释方法,比如逻辑回归系数或随机森林的特征重要性,而是通过一个名为 SHAP 的新包,能够为任何我们希望的 dataset(如验证集或测试集)逐个解释模型预测。这对于帮助我们数据科学家以及我们的业务合作伙伴深入理解模型的工作原理非常有帮助,即使是对于新数据也是如此。
梯度提升和 XGBoost
什么是提升(Boosting)?
提升(Boosting)是一种创建多个机器学习模型或估计器集成的方法,类似于随机森林模型背后的袋装(bagging)概念。像袋装方法一样,提升可以与任何类型的机器学习模型一起使用,但它通常用于构建决策树的集成模型。与袋装方法的一个主要区别是,在提升过程中,每个新加入集成的估计器都依赖于之前加入的所有估计器。由于提升过程是按顺序进行的,并且集成成员的预测结果会加总以计算整体集成预测结果,因此它也被称为逐步加法建模(stagewise additive modeling)。袋装和提升的区别可以如图 6.1所示:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_06_01.jpg)
图 6.1:袋装与提升的对比
虽然袋装方法使用不同的训练数据随机样本训练多个估计器,但提升则通过利用上一轮估计器错误分类的样本信息来训练新估计器。通过将新的估计器集中在这些错误分类的样本上,目标是使得整体集成在整个训练数据集上的表现更好。AdaBoost,作为XGBoost的前身,通过为错误分类的样本赋予更多的权重,达到了这一目标,从而训练新的估计器加入集成。
梯度提升与 XGBoost
XGBoost 是一种建模过程和 Python 包,是当今最受欢迎的机器学习方法之一,因其在许多领域(从商业到自然科学)的卓越表现而广受欢迎。XGBoost 还证明是机器学习竞赛中最成功的工具之一。我们不会讨论 XGBoost 实现的所有细节,而是了解它如何工作的高层次概念,并查看一些最重要的超参数。有关详细信息,感兴趣的读者应参考 Tianqi Chen 和 Carlos Guestrin 的论文《XGBoost: A Scalable Tree Boosting System》(dl.acm.org/doi/abs/10.1145/2939672.2939785)。
XGBoost 实现的梯度提升过程是一个逐步加法模型,类似于 AdaBoost。然而,XGBoost 并不是直接在模型训练过程中对分类错误的样本赋予更多权重,而是使用一种类似于梯度下降的过程。回想一下第四章,偏差方差权衡,梯度下降优化利用了损失函数(成本函数的另一种称呼)导数的信息,在训练逻辑回归模型时更新估计的系数。损失函数的导数包含了关于每次迭代中如何调整系数估计的方向和幅度的信息,从而减少预测中的误差。
XGBoost 将梯度下降的思想应用于逐步加法建模,利用损失函数的梯度(导数的另一种说法)信息训练新的决策树,加入到集成模型中。实际上,XGBoost 在梯度下降的基础上更进一步,正如第四章,偏差方差权衡中所描述的,它同时利用了损失函数的一阶和二阶导数。使用误差梯度训练决策树的方法是第五章,决策树与随机森林中引入的节点不纯度思想的替代方案。从概念上讲,XGBoost 训练新树的目标是将集成预测朝着减少误差的方向移动。朝这个方向迈多大的步伐由 learning_rate 超参数控制,类似于第 4.01 章 使用梯度下降最小化成本函数中的 learning_rate,该内容也出现在第四章,偏差方差权衡中。
到此为止,我们应该已经掌握了足够的知识,了解 XGBoost 如何工作,可以开始动手实践了。为了说明 XGBoost,我们将使用 scikit-learn 的 make_classification 函数创建一个用于二分类的合成数据集。该数据集包含 5,000 个样本和 40 个特征。这里的其余选项控制了分类任务的难度,你应当查阅 scikit-learn 文档以更好地理解它们。特别值得注意的是,我们将使用多个簇(n_clusters_per_class),意味着在多维特征空间中会有几个属于某一类别的点的区域,类似于上一章图 5.3中展示的簇。基于树的模型应该能够识别这些簇。此外,我们还指定在 40 个特征中只有 3 个是有信息的(n_informative),另外有 2 个冗余特征(n_redundant),它们包含与有信息特征相同的信息。因此,总的来说,在 40 个特征中,只有 5 个特征在做出预测时是有用的,而且这些信息都编码在其中的 3 个特征中。
如果你想在自己的电脑上跟随本章的示例进行操作,请参考 Jupyter 笔记本:packt.link/L5oS7:
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=5000, n_features=40,\
n_informative=3, n_redundant=2,\
n_repeated=0, n_classes=2,\
n_clusters_per_class=3,\
weights=None, flip_y=0.05,\
class_sep=0.1, hypercube=True,\
shift=0.0,scale=1.0, shuffle=True,\
random_state=2)
请注意,响应变量y的类别比例大约为 50%:
y.mean()
这应该输出以下内容:
0.4986
本章中,我们将一次性将此合成数据集划分为训练集和验证集,而不是使用交叉验证。然而,我们在这里介绍的概念可以扩展到交叉验证场景。我们将把合成数据集划分为 80%的训练集和 20%的验证集。在现实世界的数据问题中,我们还希望保留一个测试集,供以后评估最终模型时使用,但在这里我们不做此操作:
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = \
train_test_split(X, y, test_size=0.2, random_state=24)
现在我们已经为建模准备好了数据,接下来需要实例化一个XGBClassifier类的对象。请注意,我们现在将使用 XGBoost 包,而不是 scikit-learn,来开发预测模型。然而,XGBoost 的 API(应用程序接口)设计得与 scikit-learn 类似,因此使用这个类应该是直观的。XGBClassifier类可以用来创建一个模型对象,并提供fit、predict方法以及其他常见功能,我们还可以在实例化类时指定模型的超参数。我们在此仅指定几个已经讨论过的超参数:n_estimators是用于模型的提升轮数(换句话说,是阶段性加法建模过程中的阶段数),objective是用来计算梯度的损失函数,learning_rate控制每个新估计器对集成模型的贡献,或者本质上控制减少预测误差的步长。剩余的超参数与在模型训练过程中希望看到的输出量(verbosity)以及即将被弃用的label_encoder选项有关,XGBoost 的开发者建议将其设置为False:
xgb_model_1 = xgb.XGBClassifier(n_estimators=1000,\
verbosity=1,\
use_label_encoder=False,\
objective='binary:logistic',\
learning_rate=0.3)
我们所指定的超参数值表示:
-
我们将使用 1,000 个估计器,或提升轮次。稍后我们会详细讨论需要多少轮次;默认值是 100。
-
我们熟悉二元逻辑回归的目标函数(也称为代价函数),这在第四章《偏差-方差权衡》中有所介绍。XGBoost 还提供了各种目标函数,适用于分类和回归等多种任务。
-
学习率设置为
0.3,这是默认值。可以通过超参数搜索程序探索不同的值,我们将在后续演示。注意
推荐使用 Anaconda 环境安装 XGBoost 和 SHAP,具体方法可参考前言。如果安装与此处所示版本不同,您的结果可能与此处展示的不同。
现在我们有了模型对象和一些训练数据,可以开始拟合模型了。这与在 scikit-learn 中的做法类似:
%%time
xgb_model_1.fit(X_train, y_train,\
eval_metric="auc",\
verbose=True)
在这里,我们正在使用 Jupyter notebook 中的 %%time “单元魔法”来跟踪拟合过程所需的时间。我们需要提供训练数据的特征 X_train 和响应变量 y_train。我们还需要提供 eval_metric 并设置详细程度,我们稍后将解释。执行此单元格后,应显示类似以下的输出:
CPU times: user 52.5 s, sys: 986 ms, total: 53.4 s
Wall time: 17.5 s
Out[7]:
XGBClassifier(base_score=0.5, booster='gbtree',\
colsample_bylevel=1, colsample_bynode=1,\
colsample_bytree=1, gamma=0, gpu_id=-1,\
importance_type='gain',interaction_constraints='',\
learning_rate=0.3, max_delta_step=0, max_depth=6,\
min_child_weight=1, missing=nan,\
monotone_constraints='()', n_estimators=1000,\
n_jobs=4, num_parallel_tree=1, random_state=0,\
reg_alpha=0, reg_lambda=1, scale_pos_weight=1,\
subsample=1, tree_method='exact',\
use_label_encoder=False, validate_parameters=1,\
verbosity=1)
输出告诉我们,这个单元格执行了 17.5 秒,称为“墙时”,即你墙上的时钟上显示的经过时间。CPU 时间比这个更长,因为 XGBoost 高效地同时使用多个处理器。XGBoost 还打印出所有超参数,包括我们设置的和那些保留默认值的超参数。
现在,为了检查这个拟合模型的性能,我们将评估验证集上的 ROC 曲线下的面积。首先,我们需要获取预测的概率:
val_set_pred_proba = xgb_model_1.predict_proba(X_val)[:,1]
from sklearn.metrics import roc_auc_score
roc_auc_score(y_val, val_set_pred_proba)
此单元格的输出应如下所示:
0.7773798710782294
这表示 ROC AUC 约为 0.78。这将是我们模型性能的基准,使用 XGBoost 的几乎默认选项。
XGBoost 超参数
早期停止
在使用 XGBoost 训练决策树集成时,有许多选项可以用于减少过拟合并利用偏差-方差权衡。早期停止是其中一种简单的方式,可以帮助自动回答“需要多少次提升轮次?”的问题。值得注意的是,早期停止依赖于拥有一个独立的验证集数据,而不仅仅是训练集。然而,这个验证集实际上会在模型训练过程中使用,因此它不算作“未见过”的数据,这与我们在第四章《偏差-方差权衡》中使用验证集来选择模型超参数的方式类似。
当 XGBoost 训练连续的决策树以减少训练集上的误差时,可能会出现添加越来越多的树到集成中,能够提供越来越好的拟合训练数据,但开始导致在保留数据上的性能下降。为避免这种情况,我们可以使用一个验证集,也叫做评估集或 XGBoost 称之为 eval_set。评估集将作为特征及其对应响应变量的元组列表提供。该列表中最后的元组将用于早期停止。我们希望这个验证集,因为训练数据将用于拟合模型,无法提供对样本外泛化的估计:
eval_set = [(X_train, y_train), (X_val, y_val)]
现在我们可以再次拟合模型,但这次我们提供eval_set关键字参数,并传入我们刚刚创建的评估集。此时,eval_metric中的auc变得非常重要。这意味着在每轮提升之后,在训练另一个决策树之前,ROC 曲线下面积将在所有通过eval_set提供的数据集上进行评估。由于我们将verbosity=True,因此我们将在单元格下方看到输出,其中包含训练集和验证集的 ROC AUC。这提供了一个实时视图,展示随着更多提升轮次的训练,模型在训练和验证数据上的性能变化。
由于在预测建模中,我们主要关心模型在新数据和未见过的数据上的表现,当我们发现进一步的提升轮次不再改善模型在样本外数据上的表现时,我们希望停止训练额外的提升轮次。early_stopping_rounds=30参数表示一旦完成 30 轮提升,且在验证集上的 ROC AUC 没有任何进一步的提升,XGBoost 就会停止模型训练。一旦模型训练完成,最终拟合的模型将只包含获得最高验证集性能所需的集成成员。这意味着最后的 30 个集成成员将被丢弃,因为它们没有提供验证集性能的提升。现在我们来拟合这个模型,并观察进展:
%%time
xgb_model_1.fit(X_train, y_train, eval_set=eval_set,\
eval_metric='auc',\
verbose=True, early_stopping_rounds=30)
输出应该类似于这样:
[0] validation_0-auc:0.80412 validation_1-auc:0.75223
[1] validation_0-auc:0.84422 validation_1-auc:0.79207
[2] validation_0-auc:0.85920 validation_1-auc:0.79278
[3] validation_0-auc:0.86616 validation_1-auc:0.79517
[4] validation_0-auc:0.88261 validation_1-auc:0.79659
[5] validation_0-auc:0.88605 validation_1-auc:0.80061
[6] validation_0-auc:0.89226 validation_1-auc:0.80224
[7] validation_0-auc:0.89826 validation_1-auc:0.80305
[8] validation_0-auc:0.90559 validation_1-auc:0.80095
[9] validation_0-auc:0.91954 validation_1-auc:0.79685
[10] validation_0-auc:0.92113 validation_1-auc:0.79608
…
[33] validation_0-auc:0.99169 validation_1-auc:0.78323
[34] validation_0-auc:0.99278 validation_1-auc:0.78261
[35] validation_0-auc:0.99329 validation_1-auc:0.78139
[36] validation_0-auc:0.99344 validation_1-auc:0.77994
CPU times: user 2.65 s, sys: 136 ms, total: 2.78 s
Wall time: 2.36 s
…
请注意,这比之前的拟合花费了更少的时间。这是因为通过提前停止,我们只训练了 37 轮提升(请注意提升轮次是零索引的)。这意味着提升过程只需 8 轮就能达到最佳验证得分,而不是我们之前尝试的 1,000 轮!你可以通过模型对象的booster属性访问达到最佳验证集得分所需的提升轮数以及该得分。这个属性提供了比我们一直在使用的 scikit-learn API 更底层的模型接口:
xgb_model_1.get_booster().attributes()
输出应该如下所示,确认迭代次数和最佳验证得分:
{'best_iteration': '7', 'best_score': '0.80305'}
从训练过程,我们还可以看到每一轮的 ROC AUC,包括训练数据的validation_0-auc和验证数据的validation_1-auc,这些都能提供有关随着提升过程进行,模型是否过拟合的洞见。在这里,我们可以看到验证得分在第 8 轮之前一直在上升,但之后开始下降,表明进一步提升可能会导致模型过拟合。然而,训练得分则持续上升,直到过程被终止,这展示了 XGBoost 在拟合训练数据方面的强大能力。
我们还可以进一步确认拟合后的模型对象仅表示七轮提升,并通过手动计算 ROC AUC(如前所述)来检查验证集的表现:
val_set_pred_proba_2 = xgb_model_1.predict_proba(X_val)[:,1]
roc_auc_score(y_val, val_set_pred_proba_2)
这应该输出如下内容:
0.8030501882609966
这与经过七轮提升后获得的最高验证分数一致。因此,通过对模型训练过程进行一次简单的调整——使用验证集和提前停止,我们成功地将模型在验证集上的表现从大约 0.78 提升到 0.80,取得了显著的提高。这展示了提前停止在提升中的重要性。
这里自然会有一个问题:“我们怎么知道 30 轮提前停止就足够了?”你可以像调整任何超参数一样尝试不同的轮数,对于不同的数据集,可能需要不同的值。你可以观察验证分数在每轮提升中的变化来大致判断。有时,验证分数在每轮之间可能会出现跳跃性波动,因此最好有足够的轮数,以确保找到最大值,并且越过任何暂时的下降。
调整学习率
学习率在 XGBoost 文档中也被称为eta,以及步长缩减。这个超参数控制每个新估算器对集成预测的贡献大小。如果你增加学习率,你可能会更快地达到最佳模型,即在验证集上表现最好的模型。然而,设置学习率过高的风险在于,这可能会导致提升步骤过大。在这种情况下,梯度提升过程可能无法收敛到最优模型,这与第四章 偏差方差权衡中的练习 4.01,使用梯度下降最小化成本函数中讨论的梯度下降中的大学习率问题类似。接下来,让我们探讨学习率如何影响我们合成数据上的模型表现。
学习率是一个介于零和 1 之间的数字(包含端点,尽管零学习率没有实际意义)。我们创建一个包含 25 个在 0.01 和 1 之间均匀分布的数字的数组,用来测试不同的学习率:
learning_rates = np.linspace(start=0.01, stop=1, num=25)
现在我们设置一个for循环,为每个学习率训练一个模型,并将验证分数保存在数组中。我们还会追踪达到最佳迭代所需的提升轮数。接下来的几个代码块应该作为一个单元格在 Jupyter notebook 中一起运行。我们首先通过测量所需的时间,创建空列表来存储结果,并开启for循环:
%%time
val_aucs = []
best_iters = []
for learning_rate in learning_rates:
在每次循环迭代中,learning_rate变量将保存learning_rates数组的连续元素。一旦进入循环,第一步是用新的学习率更新模型对象的超参数。这是通过set_params方法完成的,我们用双星号**和一个字典来传递超参数名称和值。Python 中的**函数调用语法允许我们传递任意数量的关键字参数,也称为kwargs,以字典的形式传递。在这种情况下,我们只改变一个关键字参数,所以字典中只有一个项:
xgb_model_1.set_params(**{'learning_rate':learning_rate})
现在我们已经在模型对象上设置了新的学习率,我们按照之前的方法使用早期停止来训练模型:
xgb_model_1.fit(X_train, y_train, eval_set=eval_set,\
eval_metric='auc',\
verbose=False, early_stopping_rounds=30)
经过拟合后,我们获得了验证集的预测概率,并使用这些概率来计算验证集的 ROC AUC。然后,我们使用append方法将其添加到结果列表中:
val_set_pred_proba_2 = xgb_model_1.predict_proba(X_val)[:,1]
val_aucs.append(roc_auc_score(y_val, val_set_pred_proba_2))
最后,我们还捕捉了每个学习率所需的轮次数量:
best_iters.append(
int(xgb_model_1.get_booster().\
attributes()['best_iteration']))
前面提到的五个代码片段应该一起在一个单元格中运行。输出结果应该类似于以下内容:
CPU times: user 1min 23s, sys: 526 ms, total: 1min 24s
Wall time: 22.2 s
现在我们已经得到了这个超参数搜索的结果,可以可视化验证集的性能和迭代次数。由于这两个指标的尺度不同,我们希望创建一个双* y *轴图。pandas 使这变得简单,因此首先我们将所有数据放入一个数据框:
learning_rate_df = \
pd.DataFrame({'Learning rate':learning_rates,\
'Validation AUC':val_aucs,\
'Best iteration':best_iters})
现在我们可以像这样可视化不同学习率下的性能和迭代次数,注意:
-
我们设置了索引(
set_index),使得学习率绘制在* x 轴上,其他列绘制在 y *轴上。 -
secondary_y关键字参数指示要绘制在右侧* y *轴上的列。 -
style参数允许我们为每一列指定不同的线条样式。-o是带点的实线,而--o是带点的虚线:mpl.rcParams['figure.dpi'] = 400 learning_rate_df.set_index('Learning rate')\ .plot(secondary_y='Best iteration', style=['-o', '--o'])
结果图应该如下所示:

图 6.2:XGBoost 模型在验证集上的表现,以及不同学习率下直到最佳迭代的提升轮次
总的来说,似乎较小的学习率能够使这个合成数据集上的模型表现更好。通过使用小于默认值 0.3 的学习率,我们能得到的最佳表现如下所示:
max(val_aucs)
输出如下:
0.8115309360232714
通过调整学习率,我们将验证 AUC 从大约 0.80 提高到 0.81,表明使用适当的学习率是有益的。
通常,较小的学习率通常会导致更好的模型性能,尽管它们需要更多的提升轮次,因为每轮的贡献较小。这将导致模型训练所需的时间增加。在图 6.2的轮次与最佳迭代的关系图中,我们可以看到这一点。在这种情况下,看起来可以通过少于 50 轮来获得良好的性能,并且对于这组数据,模型训练时间不会很长。对于较大的数据集,训练时间可能会更长。根据你的计算时间,如果减少学习率并训练更多轮次,可能是提高模型性能的有效方法。
在探索较小学习率时,务必将n_estimators超参数设置得足够大,以便训练过程能够找到最佳模型,理想情况下与早停策略结合使用。
XGBoost 中的其他重要超参数
我们已经看到,XGBoost 中的过拟合可以通过使用不同的学习率以及早停策略来进行补偿。还有哪些其他可能相关的超参数?XGBoost 有很多超参数,我们不会在这里列出所有内容。建议你查阅文档(xgboost.readthedocs.io/en/latest/parameter.html)获取完整列表。
在接下来的练习中,我们将对六个超参数的范围进行网格搜索,其中包括学习率。我们还将包含max_depth,该参数在第五章《决策树与随机森林》中应该已经熟悉,它控制集成中树的生长深度。除了这些,我们还会考虑以下内容:
-
gamma通过仅在损失函数值减少超过一定量时才允许节点分裂,从而限制集成中树的复杂性。 -
min_child_weight也通过仅在节点的“样本权重”达到一定量时才分裂节点,从而控制树的复杂性。如果所有样本的权重相等(如我们练习中一样),这就相当于节点中的最小训练样本数。这类似于 scikit-learn 中决策树的min_weight_fraction_leaf和min_samples_leaf。 -
colsample_bytree是一个随机选择的特征子集,用于在集成中生长每棵树。这类似于 scikit-learn 中的max_features参数(该参数在节点级别进行选择,而不是在树级别进行选择)。XGBoost 还提供了colsample_bylevel和colsample_bynode,分别在每棵树的每一层和每个节点进行特征采样。 -
subsample控制从训练数据中随机选择的样本比例,在为集成模型生长新树之前进行选择。这类似于 scikit-learn 中随机森林的bootstrap选项。subsample和colsample参数都限制了模型训练期间可用的信息,增加了各个集成成员的偏差,但希望也能减少整体集成的方差,从而改善样本外模型的表现。
正如你所看到的,XGBoost 中的梯度提升树实现了许多来自决策树和随机森林的概念。现在,让我们探索这些超参数如何影响模型性能。
练习 6.01:用于调优 XGBoost 超参数的随机网格搜索
在这个练习中,我们将使用随机网格搜索来探索六个超参数的空间。当你有许多超参数值需要搜索时,随机网格搜索是一个不错的选择。在这里,我们将研究六个超参数。例如,如果每个超参数有五个值需要测试,我们就需要进行56 = 15,625*次搜索。即使每次模型拟合只需要一秒钟,我们也仍然需要几个小时来穷举所有可能的组合。通过只搜索这些组合的随机样本,随机网格搜索也能取得令人满意的结果。这里,我们将展示如何使用 scikit-learn 和 XGBoost 来实现这一点。
随机网格搜索的第一步是为每个超参数指定你希望从中采样的值的范围。你可以通过提供值列表或分布对象来实现这一点。对于离散超参数,如max_depth,它只有少数几个可能的值,因此可以将其指定为列表。而对于连续超参数,如subsample,它可以在区间(0, 1]上任意变化,因此我们不需要指定一个值列表。相反,我们可以要求网格搜索在这个区间内以均匀的方式随机采样值。我们将使用均匀分布来采样我们考虑的几个超参数:
注意
本练习的 Jupyter 笔记本可以在packt.link/TOXso找到。
-
从
scipy导入uniform分布类,并使用字典指定所有需要搜索的超参数的范围。uniform可以接受两个参数,loc和scale,分别指定采样区间的下界和区间的宽度:from scipy.stats import uniform param_grid = {'max_depth':[2,3,4,5,6,7], 'gamma':uniform(loc=0.0, scale=3), 'min_child_weight':list(range(1,151)), 'colsample_bytree':uniform(loc=0.1, scale=0.9), 'subsample':uniform(loc=0.5, scale=0.5), 'learning_rate':uniform(loc=0.01, scale=0.5)}在这里,我们根据实验和经验选择了参数范围。例如,对于
subsample,XGBoost 文档建议选择至少 0.5 的值,因此我们表示为uniform(loc=0.5, scale=0.5),这意味着从区间[0.5, 1]中采样。 -
现在我们已经指明了要从哪些分布中采样,我们需要进行采样。scikit-learn 提供了
ParameterSampler类,它将随机采样提供的param_grid参数,并返回请求的样本数量(n_iter)。我们还设置了RandomState,以便在不同的 notebook 运行中得到可重复的结果:from sklearn.model_selection import ParameterSampler rng = np.random.RandomState(0) n_iter=1000 param_list = list(ParameterSampler(param_grid, n_iter=n_iter, random_state=rng))我们已经将结果以字典列表的形式返回,字典包含特定的参数值,对应于 6 维超参数空间中的位置。
请注意,在本次练习中,我们正在遍历 1,000 个超参数组合,这可能需要超过 5 分钟。您可能希望减少这个数字,以便获得更快的结果。
-
检查
param_list的第一个项目:param_list[0]这应该返回一个包含六个参数值的组合,来自所指示的分布:
{'colsample_bytree': 0.5939321535345923, 'gamma': 2.1455680991172583, 'learning_rate': 0.31138168803582195, 'max_depth': 5, 'min_child_weight': 104, 'subsample': 0.7118273996694524} -
观察如何使用
**语法,通过字典同时设置多个 XGBoost 超参数。首先,为本次练习创建一个新的 XGBoost 分类器对象。xgb_model_2 = xgb.XGBClassifier( n_estimators=1000, verbosity=1, use_label_encoder=False, objective='binary:logistic') xgb_model_2.set_params(**param_list[0])输出应该显示所设置的超参数:
XGBClassifier(base_score=0.5, booster='gbtree',\ colsample_bylevel=1, colsample_bynode=1,\ colsample_bytree=0.5939321535345923,\ gamma=2.1455680991172583, gpu_id=-1,\ importance_type='gain',interaction_constraints='',\ learning_rate=0.31138168803582195,\ max_delta_step=0, max_depth=5,\ min_child_weight=104, missing=nan,\ monotone_constraints='()', n_estimators=1000,\ n_jobs=4, num_parallel_tree=1,\ random_state=0, reg_alpha=0, reg_lambda=1,\ scale_pos_weight=1, subsample=0.7118273996694524,\ tree_method='exact', use_label_encoder=False,\ validate_parameters=1, verbosity=1)我们将在循环中使用此过程,以查看所有超参数值。
-
接下来的几个步骤将包含在
for循环的一个单元格中。首先,测量执行此操作所需的时间,创建一个空列表以保存验证 AUC 值,然后启动计数器:%%time val_aucs = [] counter = 1 -
打开
for循环,设置超参数,并拟合 XGBoost 模型,类似于调整学习率的前述示例:for params in param_list: #Set hyperparameters and fit model xgb_model_2.set_params(**params) xgb_model_2.fit(X_train, y_train, eval_set=eval_set,\ eval_metric='auc',\ verbose=False, early_stopping_rounds=30) -
在
for循环内,获取预测概率和验证集 AUC 值:#Get predicted probabilities and save validation ROC AUC val_set_pred_proba = xgb_model_2.predict_proba(X_val)[:,1] val_aucs.append(roc_auc_score(y_val, val_set_pred_proba)) -
由于这个过程需要几分钟时间,最好将进度打印到 Jupyter notebook 输出中。我们使用 Python 余数语法
%,每 50 次迭代打印一次信息,换句话说,当counter除以 50 的余数为零时。最后,我们增加计数器:#Print progress if counter % 50 == 0: print('Done with {counter} of {n_iter}'.format( counter=counter, n_iter=n_iter)) counter += 1 -
将步骤 5-8 组合在一个单元格中并运行 for 循环,应该得到如下输出:
Done with 50 of 1000 Done with 100 of 1000 … Done with 950 of 1000 Done with 1000 of 1000 CPU times: user 24min 20s, sys: 18.9 s, total: 24min 39s Wall time: 6min 27s -
现在我们已经得到所有超参数探索的结果,我们需要对其进行检查。我们可以很容易地将所有超参数组合放入数据框中,因为它们是作为字典列表组织的。做这件事并查看前几行:
xgb_param_search_df = pd.DataFrame(param_list) xgb_param_search_df.head()输出应该像这样:
![图 6.3:来自随机网格搜索的超参数组合]()
图 6.3:来自随机网格搜索的超参数组合
-
我们还可以将验证集的 ROC AUC 值添加到数据框中,查看最大值:
xgb_param_search_df['Validation ROC AUC'] = val_aucs max_auc = xgb_param_search_df['Validation ROC AUC'].max() max_auc输出应该如下:
0.8151220995602575在超参数空间搜索的结果是验证集 AUC 大约为 0.815。虽然这个值比我们通过提前停止和学习率搜索得到的 0.812 稍大(图 6.3),但差距不大。这意味着,对于这组数据,默认的超参数(除了学习率)已经足够实现相当好的性能。虽然我们没有通过超参数搜索大幅提升性能,但观察超参数变化如何影响模型表现依然具有启发性。在接下来的步骤中,我们将逐个检查 AUC 相对于每个参数的边际分布。这意味着我们将查看当一个超参数逐渐变化时,AUC 如何变化,并牢记在网格搜索结果中其他超参数也在变化。
-
使用以下代码设置一个六个子图的网格,用于绘制每个超参数与性能之间的关系,同时调整图像分辨率并启动一个计数器,我们将用它来遍历子图:
mpl.rcParams['figure.dpi'] = 400 fig, axs = plt.subplots(3,2,figsize=(8,6)) counter = 0 -
打开一个
for循环来遍历超参数名称,它们是数据框的列,不包括最后一列。通过将subplot返回的 3 x 2 数组展平,并使用counter索引来访问轴对象。对于每个超参数,使用数据框的plot.scatter方法在适当的坐标轴上绘制散点图。x 轴将显示超参数,y 轴显示验证 AUC,其他选项帮助我们获得具有白色内部的黑色圆形标记:for col in xgb_param_search_df.columns[:-1]: this_ax = axs.flatten()[counter] xgb_param_search_df.plot.scatter(x=col,\ y='Validation ROC AUC',\ ax=this_ax, marker='o',\ color='w',\ edgecolor='k',\ linewidth=0.5) -
数据框的
plot方法会自动创建 x 和 y 轴标签。然而,由于 y 轴标签对于所有这些图来说是相同的,我们只需要在第一个图中包含它。因此,我们将其他所有图的标签设置为空字符串'',并递增计数器:if counter > 0: this_ax.set_ylabel('') counter += 1由于我们将绘制边际分布图,在观察验证集 AUC 随着给定超参数变化时,其他所有超参数也会发生变化。这意味着关系可能会有噪声。为了了解整体趋势,我们还将创建折线图,显示每个超参数的分位数中验证 AUC 的平均值。分位数将数据按值是否落入最低 10%、接下来的 10% 等,直到最高 10% 来组织成不同的箱体。pandas 提供了一个名为
qcut的函数,可以将一个 Series 切分成分位数(分位数是均匀大小的箱体中的一个,例如在 10 个箱体的情况下是一个十分位数),返回另一个 Series,包含这些分位数以及分位数的边界值,您可以把它们看作是直方图的边缘。 -
使用 pandas 的
qcut为每个超参数(除了max_depth)生成一个分位数序列(10 个分位数),返回区间边界(对于 10 个分位数会有 11 个边界),如果唯一值不足以分成 10 个分位数,则删除不需要的区间边界(duplicates='drop')。创建一个列表,包含每对区间边界之间的中点,用于绘图:if col != 'max_depth': out, bins = pd.qcut(xgb_param_search_df[col], q=10,\ retbins=True, duplicates='drop') half_points = [(bins[ix] + bins[ix+1])/2 for ix in range(len(bins)-1)] -
对于
max_depth,由于只有六个唯一值,我们可以像处理分位数一样直接使用这些值:else: out = xgb_param_search_df[col] half_points = np.sort(xgb_param_search_df[col].unique()) -
通过复制超参数搜索数据框创建一个临时数据框,创建一个包含分位数序列的新列,并利用此列查找每个超参数分位数内的验证 AUC 平均值:
tmp_df = xgb_param_search_df.copy() tmp_df['param_decile'] = out mean_df = tmp_df.groupby('param_decile').agg( {'Validation ROC AUC':'mean'}) -
我们可以通过在每个散点图的同一坐标轴上,绘制表示每个分位数平均值的虚线图来可视化结果。关闭
for循环并使用plt.tight_layout()清理子图格式:this_ax.plot(half_points,\ mean_df.values,\ color='k',\ linestyle='--') plt.tight_layout()运行
for循环后,生成的图像应如下所示:![图 6.4:验证集 AUC 与每个超参数的关系图,并显示每个超参数分位数内的平均值]()
图 6.4:验证集 AUC 与每个超参数的关系图,并显示每个超参数分位数内的平均值
尽管我们注意到,本次练习中的超参数搜索并未显著提高验证 AUC,相较于本章之前的尝试,但图 6.4中的图表仍能展示 XGBoost 超参数如何影响该特定数据集的模型表现。XGBoost 通过在生成树时限制可用数据来对抗过拟合,一种方式是随机选择每棵树可用的特征的一部分(
colsample_bytree),或者随机选择训练样本的一部分(subsample)。然而,在该合成数据中,似乎当每棵树使用 100% 的特征和样本时,模型表现最佳;低于此比例时,模型表现逐渐下降。另一种控制过拟合的方法是通过控制树的复杂度来限制集成中的树,方法包括限制max_depth、叶子节点中的最小训练样本数(min_child_weight)或分裂节点所需的最小损失函数减少值(gamma)。在我们这里的例子中,max_depth和gamma似乎对模型表现没有太大影响,而限制叶子节点中的样本数量似乎会带来负面效果。看起来在这个案例中,梯度提升过程本身足够稳健,能够在没有额外技巧的情况下实现良好的模型表现,以减少过拟合。然而,正如我们上面所观察到的,较小的
learning_rate是有益的。 -
我们可以显示最佳的超参数组合及其对应的验证集 AUC,如下所示:
max_ix = xgb_param_search_df['Validation ROC AUC'] == max_auc xgb_param_search_df[max_ix]这应该返回类似于以下的数据框行:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_06_05.jpg)
图 6.5:最佳超参数组合与验证集 AUC
验证集 AUC 与我们在上一步(步骤 10)通过仅调整学习率所达到的结果相似。
另一种生长树的方式:XGBoost 的 grow_policy
除了通过max_depth超参数限制树的最大深度外,还有一种控制树生长的范式:寻找一个节点,在这个节点上分裂将导致损失函数的最大减少,并进行分裂,而不考虑这会使树变得多深。这可能导致树有一个或两个非常深的分支,而其他分支可能不会生长得很远。XGBoost 提供了一个名为grow_policy的超参数,设置为lossguide时会产生这种树的生长方式,而depthwise选项是默认设置,会将树生长到指定的max_depth,正如我们在第五章《决策树与随机森林》中所做的,以及在本章至今为止的操作一样。lossguide生长策略是 XGBoost 中的一个较新选项,它模拟了 LightGBM(另一个流行的梯度提升包)的行为。
要使用lossguide策略,需要设置我们尚未讨论的另一个超参数tree_method,它必须设置为hist或gpu-hist。不详细讲解,hist方法将使用更快的方式来搜索分裂。它不会在节点中对训练样本的每一对排序特征值进行逐一比较,而是构建一个直方图,仅考虑直方图边缘的分裂。例如,如果一个节点中有 100 个样本,它们的特征值可能被分为 10 组,这意味着只考虑 9 个可能的分裂,而不是 99 个。
我们可以按照如下方式实例化一个使用lossguide生长策略的 XGBoost 模型,使用学习率0.1,这是根据我们在前一个练习中进行的超参数探索的直觉得出的:
xgb_model_3 = xgb.XGBClassifier(
n_estimators=1000,
max_depth=0,
learning_rate=0.1,
verbosity=1,
objective='binary:logistic',
use_label_encoder=False,
n_jobs=-1,
tree_method='hist',
grow_policy='lossguide')
请注意,我们已经设置了max_depth=0,因为该超参数与lossguide策略无关。相反,我们将设置一个名为max_leaves的超参数,它简单地控制将要生长的树的最大叶子数。我们将进行一个超参数搜索,范围从 5 到 100 个叶子:
max_leaves_values = list(range(5,105,5))
print(max_leaves_values[:5])
print(max_leaves_values[-5:])
这将输出如下内容:
[5, 10, 15, 20, 25]
[80, 85, 90, 95, 100]
现在我们准备好在这一系列超参数值范围内反复进行模型拟合和验证,类似于我们之前做过的操作:
%%time
val_aucs = []
for max_leaves in max_leaves_values:
#Set parameter and fit model
xgb_model_3.set_params(**{'max_leaves':max_leaves})
xgb_model_3.fit(X_train, y_train, eval_set=eval_set,\
eval_metric='auc', verbose=False,\
early_stopping_rounds=30)
#Get validation score
val_set_pred_proba = xgb_model_3.predict_proba(X_val)[:,1]
val_aucs.append(roc_auc_score(y_val, val_set_pred_proba))
输出将包括所有这些拟合的壁钟时间,在测试中大约是 24 秒。现在,让我们把结果放入数据框中:
max_leaves_df = \
pd.DataFrame({'Max leaves':max_leaves_values,
'Validation AUC':val_aucs})
我们可以可视化验证集 AUC 随最大叶子数的变化,类似于我们对学习率的可视化:
mpl.rcParams['figure.dpi'] = 400
max_leaves_df.set_index('Max leaves').plot()
这将产生如下图所示的图形:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_06_06.jpg)
图 6.6:验证集 AUC 与 max_leaves 超参数的关系
较小的max_leaves值会限制生成的树的复杂度,这将理想地增加偏差,但也会减少方差,从而提高样本外的表现。当树的叶子数限制在 15 或 20 时,我们可以看到验证集 AUC 有所提高。那么,最大验证集 AUC 是多少呢?
max_auc = max_leaves_df['Validation AUC'].max()
max_auc
这应该输出以下内容:
0.8151200989120475
让我们确认最大验证 AUC 出现在max_leaves=20时,如图 6.6所示:
max_ix = max_leaves_df['Validation AUC'] == max_auc
max_leaves_df[max_ix]
这应该返回数据框的一行:

图 6.7:最优 max_leaves
通过使用lossguide增长策略,我们可以实现至少与我们迄今为止尝试过的任何方法一样好的性能。lossguide策略的一个关键优势是,对于较大的数据集,它能够提供比depthwise策略更快的训练速度,特别是在max_leaves较小的情况下。尽管这里的数据集足够小,这一速度差异并没有实际重要性,但在其他应用中,这种速度可能是理想的。
使用 SHAP 值解释模型预测
随着像 XGBoost 这样的前沿建模技术的发展,解释模型预测的实践在近年来有了显著的进展。到目前为止,我们已经了解到,逻辑回归的系数,或者随机森林中的特征重要性,可以为模型预测的原因提供洞察。2017 年,Scott Lundberg 和 Su-In Lee 在论文《统一模型预测解释方法》中描述了一种更强大的模型预测解释技术(arxiv.org/abs/1705.07874)。该技术被称为SHAP(Shapley 加法解释),它基于数学家 Lloyd Shapley 的早期工作。Shapley 发展了博弈论中的一个领域,用以理解玩家联盟如何为博弈的总体结果做出贡献。近期的机器学习研究在模型解释方面借鉴了这一概念,考虑了预测模型中的特征组或联盟如何贡献于模型的最终预测输出。通过考虑不同特征组的贡献,SHAP 方法能够孤立出单一特征的影响。
注意
在撰写本文时,第六章《梯度提升、XGBoost 和 SHAP 值》中使用的 SHAP 库与 Python 3.9 不兼容。因此,如果您使用的是 Python 3.9 作为基础环境,我们建议您按照前言中所述,设置 Python 3.8 环境。
使用 SHAP 值解释模型预测的一些显著特点包括:
-
SHAP 值可以用来对模型预测做个性化解释;换句话说,可以通过 SHAP 理解单个样本的预测结果,分析每个特征的贡献。这与我们之前见过的随机森林特征重要性解释方法不同,后者仅考虑特征在模型训练集中的平均重要性。
-
SHAP 值是相对于背景数据集计算的。默认情况下,这是训练数据集,当然也可以提供其他数据集。
-
SHAP 值是可加的,这意味着对于单个样本的预测,SHAP 值可以加起来恢复预测值,例如预测的概率。
SHAP 方法有不同的实现,适用于各种类型的模型,这里我们将专注于树模型的 SHAP(Lundberg 等,2019 年,arxiv.org/abs/1802.03888),以便了解我们在验证集上的 XGBoost 模型预测。首先,我们将使用最优的max_leaves(即 20)重新拟合上一节中的xgb_model_3:
%%time
xgb_model_3.set_params(**{'max_leaves':20})
xgb_model_3.fit(X_train, y_train,\
eval_set=eval_set,\
eval_metric='auc',
verbose=False,\
early_stopping_rounds=30)
现在我们准备开始计算验证数据集的 SHAP 值。这里有 40 个特征和 1000 个样本:
X_val.shape
这将输出以下内容:
(1000, 40)
为了自动标注我们可以使用shap包绘制的图,我们将把验证集特征放入一个带列名的数据框中。我们将使用列表推导来生成通用的特征名称,例如“Feature 0, Feature 1, …”,并按如下方式创建数据框:
feature_names = ['Feature {number}'.format(number=number)
for number in range(X_val.shape[1])]
X_val_df = pd.DataFrame(data=X_val, columns=feature_names)
X_val_df.head()
dataframe的头部应如下所示:

图 6.8:验证特征的数据框
使用训练好的模型xgb_model_3和验证特征的数据框,我们准备创建一个explainer接口。SHAP 包有多种类型的解释器,我们将使用专门针对树模型的解释器:
explainer = shap.explainers.Tree(xgb_model_3, data=X_val_df)
这创建了一个使用模型验证数据作为背景数据集的解释器。现在我们已经准备好使用该解释器来获取 SHAP 值。SHAP 包使这变得非常简单。我们需要做的就是传入我们想要解释的数据集:
shap_values = explainer(X_val_df)
就是这样!那么,创建的这个变量shap_values是什么呢?如果你直接检查shap_values变量的内容,你会看到它包含三个属性。第一个是values,它包含 SHAP 值。让我们查看它的形状:
shap_values.values.shape
这将返回以下结果:
(1000, 40)
因为 SHAP 提供了个性化的解释,每个验证集中的 1,000 个样本都有一行数据。总共有 40 列,因为我们有 40 个特征,SHAP 值告诉我们每个特征对每个样本预测的贡献。shap_values 还包含一个 base_values 属性,即在考虑任何特征贡献之前的初始预测值,也定义为整个数据集的平均预测值。每个样本(1,000 个)都有一个这样的值。最后,还有一个 data 属性,包含特征值。所有这些信息可以通过不同的方式结合起来解释模型预测。
幸运的是,shap 包不仅提供了快速便捷的计算 SHAP 值的方法,还提供了一整套丰富的可视化技术。其中最受欢迎的一种是 SHAP 汇总图,它可视化每个特征对每个样本的贡献。我们来创建这个图表,然后理解其中展示的内容。请注意,大多数有趣的 SHAP 可视化使用了颜色,因此如果你正在阅读的是黑白版本,请参考 GitHub 仓库中的彩色图形:
mpl.rcParams['figure.dpi'] = 75
shap.summary_plot(shap_values.values, X_val_df)
这应该会生成以下内容:

图 6.9:合成数据验证集的 SHAP 汇总图
注意
如果你正在阅读本书的印刷版,你可以通过访问以下链接下载并浏览本章中部分图像的彩色版本:packt.link/ZFiYH
图 6.9 包含了大量信息,帮助我们解释模型。汇总图可能包含多达 40,000 个绘制点,每个特征对应一个点,每个样本(1,000 个验证样本)对应一个点(尽管默认情况下只显示前 20 个特征)。我们先从理解 x 轴开始。SHAP 值表示每个特征值对样本预测的加性贡献。这里显示的 SHAP 值是相对于预期值的,预期值即前面提到的 base_values。因此,如果某个特征对某个样本的预测影响较小,它将不会使预测偏离预期值太远,SHAP 值接近于零。然而,如果某个特征的影响较大,对于我们的二分类问题而言,这意味着预测的概率将被推向 0 或 1,SHAP 值会远离 0。负的 SHAP 值表示某个特征使得预测值更接近 0,正的 SHAP 值则表示更接近 1。
注意,图 6.9中显示的 SHAP 值不能直接解释为预测概率。默认情况下,XGBoost 二分类模型的 SHAP 值,使用 binary:logistic 目标函数计算并绘制,使用的是概率的对数赔率表示法,这在第三章中的逻辑回归细节和特征探索部分的为什么逻辑回归被认为是线性模型?小节中介绍过。这意味着 SHAP 值可以进行加减,换句话说,我们可以对其进行线性变换。
那么,图 6.9中点的颜色如何解释呢?这些颜色代表了每个样本的特征值,红色表示特征值较高,蓝色表示特征值较低。因此,例如,在图的第四行,我们可以看到特征 29 的高特征值(红点)对应的是最低的 SHAP 值。
点的垂直排列,换句话说,每个特征的点带宽度,表示在该位置上 x 轴上有多少个点。如果样本多,点带的宽度就会更大。
图中特征的垂直排列是根据特征重要性进行的。最重要的特征,也就是那些对模型预测具有最大平均影响(均值绝对 SHAP 值)的特征,排在列表的顶部。
虽然图 6.9中的总结图是查看所有最重要特征及其 SHAP 值的好方法,但它可能无法揭示一些有趣的关系。例如,最重要的特征——特征 3,似乎在特征值范围的中间部分有一大簇紫色点,这些点的 SHAP 值为正,而该特征的负 SHAP 值可能来自于特征值过高或过低。
这是什么情况呢?通常,当从 SHAP 总结图中看不出特征的影响时,我们使用的基于树的模型正在捕捉特征之间的交互效应。为了进一步了解单个特征及其与其他特征的交互作用,我们可以使用 SHAP 散点图。首先,我们绘制特征 3 的 SHAP 值散点图。注意,我们可以像索引数据框一样索引 shap_values 对象:
shap.plots.scatter(shap_values[:,'Feature 3'])
这将生成如下图所示:

](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/ds-proj-py-2e/img/B16925_06_10.jpg)
图 6.10:特征 3 的 SHAP 值散点图
从图 6.10中,我们几乎可以得出与图 6.9总结图相同的信息:特征值在范围中间的部分对应较高的 SHAP 值,而极值部分的 SHAP 值较低。然而,scatter方法还允许我们通过另一个特征值来为散点图上的点上色,这样我们就可以看到特征之间是否存在交互作用。我们将使用第二重要特征——特征 5,为点上色:
shap.plots.scatter(shap_values[:,'Feature 3'],
color=shap_values[:,'Feature 5'])
生成的图应该像这样:

图 6.11:特征 3 的 SHAP 值散点图,按特征 5 的特征值着色。箭头 A 和 B 指示这些特征之间有趣的交互效应
图 6.11 显示了特征 3 和特征 5 之间的有趣交互。当样本位于特征 3 的特征值范围的中间时,也就是说,位于 图 6.11 中山丘形状的顶部时,从底部到顶部的点的颜色似乎变得越来越红(箭头 A)。这意味着,对于特征 3 范围中间的特征值,随着特征 5 的值增加,特征 3 的 SHAP 值也会增加。我们还可以看到,随着特征 3 的特征值沿 x 轴从中间向上升高,这种关系发生反转,特征 5 的较高特征值开始对应于特征 3 较低的 SHAP 值(箭头 B)。因此,与特征 5 的交互似乎对特征 3 的 SHAP 值有显著影响。
图 6.11 展示了复杂的关系,说明当存在交互效应时,增加特征值可能导致 SHAP 值增加或减少。图 6.11 中模式的具体原因与我们建模的合成数据集的创建有关,在此数据集中,我们在特征空间中指定了多个簇。如 第五章 中讨论的,决策树与随机森林,在 使用决策树:优势与预测概率 部分中提到,基于树的模型,如 XGBoost,能够有效地在多维特征空间中建模属于某一类别的点簇。SHAP 解释有助于我们理解模型如何进行这些表示。
在这里,我们使用了合成数据,特征没有现实世界的解释,因此我们无法为观察到的交互分配任何含义。然而,对于现实世界的数据,结合 SHAP 值和交互作用的详细探索可以提供有关模型如何表示客户或用户属性之间复杂关系的见解。例如,SHAP 值也很有用,因为它们可以提供相对于任何背景数据集的解释。尽管逻辑回归系数和随机森林的特征重要性完全由模型训练数据决定,SHAP 值可以为任何背景数据集计算;到目前为止,在本章中,我们一直使用的是验证数据。这为当预测模型部署到生产环境中时,提供了一个了解新预测是如何做出的机会。如果新预测的 SHAP 值与模型训练和测试数据的 SHAP 值非常不同,这可能表明传入数据的性质发生了变化,可能是时候考虑开发一个新模型了。在最后一章中,我们将讨论这些在实际应用中使用模型的实际问题。
练习 6.02:绘制 SHAP 交互作用、特征重要性,并从 SHAP 值中重构预测概率
在本练习中,你将更熟悉使用 SHAP 值来提供模型工作原理的可见性。首先,我们将再次查看特征 3 和特征 5 之间的交互作用,然后使用 SHAP 值计算特征重要性,类似于我们在第五章 决策树与随机森林中使用随机森林模型所做的那样。最后,我们将看到如何从 SHAP 值中获取模型输出,利用它们的加法性质:
注意
本练习的 Jupyter notebook 可以在packt.link/JcMoA找到。
-
由于本节已经完成了初步步骤,我们可以再次查看特征 3 和特征 5 之间的交互作用,它们是合成数据集中的两个最重要的特征。使用以下代码制作图 6.11的另一个版本,不过这次我们关注的是特征 5 的 SHAP 值,并按照特征 3 的值进行着色:
shap.plots.scatter(shap_values[:,'Feature 5'], color=shap_values[:,'Feature 3'])结果图应如下所示:
![图 6.12:特征 5 的 SHAP 值散点图,按特征 3 的特征值着色]()
图 6.12:特征 5 的 SHAP 值散点图,按特征 3 的特征值着色
与图 6.11不同,这里我们看到的是特征 5 的 SHAP 值。一般来说,从散点图来看,我们可以看到,随着特征 5 的特征值增加,SHAP 值也趋向增加。然而,确实有一些反例与这一普遍趋势相悖,并且与特征 3 有一个有趣的交互作用:对于特征 5 的给定值,可以将其视为图像中的一个垂直切片,点的颜色可以从下到上变得更红,表示负特征值,或者对于正特征值,点的颜色变得不那么红。这意味着对于特征 5 的给定值,它的 SHAP 值依赖于特征 3 的值。这进一步说明了特征 3 和 5 之间有趣的交互作用。在实际项目中,您选择展示哪个图表取决于您希望通过数据讲述什么样的故事,涉及特征 3 和 5 可能代表的实际世界中的量。
-
使用以下代码创建特征重要性条形图:
mpl.rcParams['figure.dpi'] = 75 shap.summary_plot(shap_values.values, X_val, plot_type='bar')![图 6.13: 使用 SHAP 值的特征重要性条形图]()
图 6.13: 使用 SHAP 值的特征重要性条形图
特征重要性条形图提供了类似于第五章《决策树与随机森林》中的练习 5.03,拟合随机森林所获得的信息的可视化呈现:这是每个特征的一个数字,表示它对数据集的总体重要性。
这些结果合理吗?回想一下,我们是通过创建包含三个有信息的特征和两个冗余特征的合成数据来实现的。在图 6.13中,似乎有四个特征的显著性比其他所有特征重要,因此,可能其中一个冗余特征的创建方式导致 XGBoost 经常选择它来进行节点分裂,而另一个冗余特征则使用较少。
与我们在第五章《决策树与随机森林》中找到的特征重要性相比,这里的特征重要性有些不同。我们从 scikit-learn 中获得的随机森林模型的特征重要性是通过特征导致的节点杂质下降以及特征划分的训练样本的比例来计算的。相比之下,使用 SHAP 值计算的特征重要性是这样得到的:首先,取所有 SHAP 值(
shap_values.values)的绝对值,然后对每个特征取所有样本的平均值,正如 x 轴标签所示。感兴趣的读者可以通过直接从shap_values计算这些指标来确认这一点。现在我们已经熟悉了 SHAP 值的多种用法,让我们看看它们的加法性质是如何允许重建预测概率的。
-
SHAP 值是相对于模型的期望值或基准值计算的。这可以解释为背景数据集中所有样本的平均预测值。然而,正如前面提到的,为了支持可加性,预测将以对数几率单位表示,而不是概率。可以通过以下方式从解释器对象中访问模型的期望值:
explainer.expected_value输出应如下所示:
-0.30949621941894295这条信息本身并不特别有用。然而,它为我们提供了一个基准,我们可以用它来重构预测概率。
-
回顾一下,SHAP 值矩阵的形状是样本数和特征数。在我们对验证数据进行的练习中,形状将是 1,000 x 40。为了将每个样本的所有 SHAP 值相加,我们需要对列轴(
axis=1)进行求和。这将把所有特征的贡献加在一起,实际上提供了与期望值的偏移量。如果我们将期望值加到其中,就得到了以下的预测值:shap_sum = shap_values.values.sum(axis=1) + explainer.expected_value shap_sum.shape这应该返回以下结果:
(1000,)这意味着我们现在为每个样本有了一个单一的数值。然而,这些预测是在对数几率空间中。为了将它们转换为概率空间,我们需要应用在第三章,逻辑回归细节与特征探索中介绍的逻辑函数。
-
如此应用逻辑转换到对数几率预测:
shap_sum_prob = 1 / (1 + np.exp(-1 * shap_sum))现在,我们希望将通过 SHAP 值获得的预测概率与直接模型输出进行比较以确认。
-
获取模型验证集的预测概率,并使用以下代码检查其形状:
y_pred_proba = xgb_model_3.predict_proba(X_val)[:,1] y_pred_proba.shape输出应如下所示:
(1000,)这与我们从 SHAP 导出的预测结果形状相同,如预期的那样。
-
将模型输出和 SHAP 值的总和放在数据框中进行并排比较,并随机检查五行数据:
df_check = pd.DataFrame( {'SHAP sum':shap_sum_prob, 'Predicted probability':y_pred_proba}) df_check.sample(5, random_state=1)输出应该确认这两种方法的结果是相同的:
![图 6.14: SHAP 导出的预测概率比较 和那些直接从 XGBoost 获得的结果。]()
图 6.14:SHAP 导出的预测概率与直接从 XGBoost 获得的预测概率比较
随机检查表明这五个样本具有相同的值。虽然由于机器算术的四舍五入误差,这些值可能不完全相等,但你可以使用 NumPy 的
allclose函数,确保它们在用户配置的四舍五入误差范围内相同。 -
确保 SHAP 导出的概率和模型输出的概率非常接近,如下所示:
np.allclose(df_check['SHAP sum'],\ df_check['Predicted probability'])输出应如下所示:
True这表明这两列中的所有元素在四舍五入误差范围内是相等的。
allclose在出现四舍五入误差时非常有用,而精确相等(可以通过np.array_equal进行测试)通常不成立。
到目前为止,你应该对 SHAP 值在帮助理解机器学习模型方面的强大功能有了一些印象。SHAP 值的样本特定、个性化特性开启了非常详细的分析可能性,这可以帮助回答来自业务利益相关者的各种潜在问题,比如“模型会如何对像这样的人的数据做出预测?”或“为什么模型会对这个特定人做出这样的预测?”现在,我们已经熟悉了 XGBoost 和 SHAP 值,这两种先进的机器学习技术,接下来我们将回到案例研究数据上应用它们。
缺失数据
关于同时使用 XGBoost 和 SHAP 的最后一点说明,这两个包的一个宝贵特性是它们能够处理缺失值。回想一下在第一章,数据探索与清洗中,我们发现案例研究数据中一些样本在PAY_1特征上存在缺失值。到目前为止,我们的方法是,在构建模型时,简单地将这些样本从数据集中移除。这是因为,如果不以某种方式专门处理缺失值,scikit-learn 实现的机器学习模型将无法处理这些数据。忽略缺失值是一种方法,尽管这可能不令人满意,因为它涉及丢弃数据。如果缺失的数据仅占很小的比例,这可能没问题;然而,一般来说,知道如何处理缺失值是很重要的。
有几种方法可以填充缺失值,例如使用该特征非缺失值的均值或众数,或者随机选择一个非缺失值。你还可以构建一个模型,将该特征作为响应变量,所有其他特征作为该新模型的特征,然后预测缺失的特征值。这些方法在本书的第一版中有所探讨(packt.link/oLb6C)。然而,由于 XGBoost 通常在使用我们这里的表格数据进行二分类任务时,表现至少与其他机器学习模型一样好,并且能够处理缺失值,因此我们将不再深入探讨填充缺失值的问题,而是让 XGBoost 来为我们完成这项工作。
XGBoost 如何处理缺失数据?在每次有机会分裂节点时,XGBoost 仅考虑非缺失的特征值。如果一个特征包含缺失值且被选择用于分裂,缺失该特征值的样本会被发送到一个子节点,在最小化损失函数的基础上选择最优路径。
将 Python 变量保存到文件
在本章的活动中,为了进行文件读写,我们将使用新的 Python 语句(with)和pickle包。with语句使得处理文件更加方便,因为它们不仅打开文件,还会在使用完成后自动关闭文件,而无需用户单独进行这些操作。你可以使用如下代码片段将变量保存到文件中:
with open('filename.pkl', 'wb') as f:
pickle.dump([var_1, var_2], f)
其中filename.pkl是你选择的文件路径,'wb'表示文件以二进制格式打开以供写入,pickle.dump将变量var_1和var_2保存到该文件中。要打开此文件并加载这些变量,可能需要在另一个 Jupyter Notebook 中,代码类似,但现在需要以二进制格式('rb')打开文件:
with open('filename.pkl', 'rb') as f:
var_1, var_2 = pickle.load(f)
活动 6.01:使用 XGBoost 建模案例研究数据并使用 SHAP 解释模型
在本活动中,我们将利用本章所学的内容,使用一个合成数据集,并将其应用于案例研究数据。我们将观察 XGBoost 模型在验证集上的表现,并使用 SHAP 值解释模型预测。我们已通过替换先前忽略的、PAY_1特征缺失值的样本来准备数据集,同时保持没有缺失值的样本的训练/测试划分。你可以在本活动的笔记本附录中查看数据是如何准备的。
注意
包含解决方案以及附录的 Jupyter 笔记本可以在这里找到:packt.link/YFb4r。
-
加载已为此练习准备好的案例研究数据。文件路径为
../../Data/Activity_6_01_data.pkl,变量包括:features_response, X_train_all, y_train_all, X_test_all, y_test_all。 -
定义一个验证集,用于训练 XGBoost 并进行早期停止。
-
实例化一个 XGBoost 模型。使用
lossguide增长策略,以便检查验证集在多个max_leaves值下的表现。 -
创建一个
max_leaves值的列表,范围从 5 到 200,步长为 5。 -
创建用于早期停止的评估集。
-
遍历超参数值并创建一个验证 ROC AUC 的列表,使用与练习 6.01:随机网格搜索调优 XGBoost 超参数相同的技术。
-
创建一个超参数搜索结果的数据框,并绘制验证 AUC 与
max_leaves的关系图。 -
观察对应于验证集上最高 ROC AUC 的
max_leaves数量。 -
使用最佳超参数重新拟合 XGBoost 模型。这样我们就可以检查验证集的 SHAP 值,制作该数据的数据框。
-
使用验证数据作为背景数据集,为我们的新模型创建 SHAP 解释器,获取 SHAP 值,并绘制总结图。
-
绘制
LIMIT_BALSHAP 值的散点图,按与最强交互特征相关的颜色进行着色。 -
将训练好的模型以及训练数据和测试数据保存到一个文件中。
注意
该活动的解决方案可以通过此链接找到。
总结
在本章中,我们学习了一些构建机器学习模型的前沿技术,尤其是针对表格数据。虽然其他类型的数据,如图像或文本数据,需要使用不同类型的模型(如神经网络)进行探索,但许多标准的商业应用仍然依赖于表格数据。XGBoost 和 SHAP 是一些最先进且流行的工具,你可以用它们来构建和理解这类数据的模型。在通过这些工具与合成数据进行实践并积累经验后,在接下来的活动中,我们将回到案例研究的数据集,看看如何使用 XGBoost 来对其建模,包括包含缺失特征值的样本,并利用 SHAP 值来理解模型。
第七章:7. 测试集分析、财务洞察和交付给客户
概述
本章介绍了几种分析模型测试集的技术,用于推导未来可能的模型表现。这些技术包括我们已经计算过的相同模型性能指标,例如 ROC AUC,以及一些新的可视化方法,如按预测概率分组的违约风险变化和预测概率的校准。阅读本章后,您将能够弥合机器学习的理论指标与商业世界财务指标之间的差距。您将能够识别关键洞察,并估算模型的财务影响,向客户提供如何实现这一影响的指导。最后,我们讨论了交付和部署模型时需要考虑的关键因素,如交付格式和监控模型使用情况的方法。
引言
在上一章中,我们使用了 XGBoost 进一步提高了模型的性能,超越了我们之前的所有努力,并学会了如何通过 SHAP 值解释模型预测。现在,我们将认为模型构建已完成,并解决交付给客户之前需要关注的剩余问题。本章的关键内容是对测试集的分析,包括财务分析,以及交付模型给希望在现实世界中使用它的客户时需要考虑的事项。
我们通过查看测试集来了解模型未来的表现。通过计算我们已知的指标,如 ROC AUC,但这次是针对测试集的,我们可以增强对模型在新数据上有用性的信心。我们还将学习一些直观的方法来可视化模型将客户分为不同违约风险等级的能力,比如十分位图。
您的客户可能会欣赏您在创建更准确的模型或更高 ROC AUC 模型方面所做的努力。然而,他们一定会更重视了解模型能够帮助他们赚取或节省多少钱,并且可能会乐于接受关于如何最大化模型潜力的具体指导。对测试集的财务分析可以模拟基于模型的不同策略场景,帮助客户选择适合他们的策略。
在完成财务分析后,我们将总结如何交付模型给客户使用,并讨论如何随着时间推移监控其表现。
模型结果回顾
为了开发满足客户商业需求的二分类模型,我们已经尝试了几种建模技术,取得了不同程度的成功。最终,我们希望选择性能最好的模型进行进一步分析,并呈现给客户。然而,向客户传达我们探索的其他选项也是很重要的,这样可以展示我们进行了一项彻底的研究项目。
在这里,我们回顾了针对案例研究问题所尝试的不同模型、需要调整的超参数以及交叉验证结果,或者在 XGBoost 的情况下使用的验证集。我们仅包括使用所有可能特征所做的工作,而不包括早期只使用一两个特征的探索性模型:

图 7.1:使用案例研究数据的建模活动总结
在向客户展示结果时,你应该准备好为各个技术背景层次的业务伙伴解释这些结果,包括那些技术背景很少的人。例如,业务伙伴可能不理解 ROC AUC 指标的推导过程;然而,这是一个重要的概念,因为它是我们用来评估模型的主要性能指标。你可能需要解释它是一个介于 0.5 和 1 之间变化的指标,并给出这些界限的直观解释:0.5 就像是抛硬币一样,而 1 是完美的,实际上几乎是无法达到的。
我们的结果介于两者之间,最佳模型接近 0.78。虽然一个给定模型的 ROC AUC 可能单独来看没有太大意义,图 7.1 显示我们尝试了几种方法,并且在最初的尝试基础上取得了性能的提升。最终,对于像案例研究这样的商业应用,像 ROC AUC 这样的抽象模型性能指标如果能配合财务分析,效果会更好。我们将在本章后面深入探讨这一点。
注:关于解释 ROC AUC
ROC AUC 分数的一个有趣解释是,对于两个样本,其中一个是正面结果,另一个是负面结果,正面样本的预测概率会比负面样本高。换句话说,对于评估数据集中所有可能的正负样本对,正面样本的模型预测高于负面样本的比例等于 ROC AUC。
从图 7.1中,我们可以看到,对于案例研究,通过工程化新特征来增强简单的逻辑回归模型或创建决策树集成模型来构建更复杂模型的努力,取得了更好的模型性能。特别是随机森林和 XGBoost 模型表现相似,尽管这些验证得分在技术上并不直接可比,因为在随机森林的情况下我们排除了缺失值,并且使用了 4 倍交叉验证,而在 XGBoost 中,缺失值被包含在内,且只有一个验证集用于提前停止。然而,图 7.1 表明,XGBoost 或随机森林可能是最好的选择。我们将在这里继续使用 XGBoost 模型。
现在我们已经决定了要交付的模型,考虑一下在模型开发过程中我们可能尝试过的其他方法也是好的。这些概念本书中不会展开,但你可能希望自己进行实验。
特征工程
提高模型性能的另一种方式,我们简要提到过的是LIMIT_BAL。与此特征最强交互的特征是两个月前的账单金额。尽管 XGBoost 可以找到类似的交互并在一定程度上对其进行建模,我们还可以构造一个新特征:过去每月账单金额与信用额度的比率,假设账单金额是账户的余额。这样计算得到的信用利用率可能是一个更强的特征,并且在这种方式下计算可能会比将信用额度和每月账单金额分别提供给模型的效果更好。
特征工程可能表现为通过操作现有特征来创建新特征,如之前的示例,或者它可能涉及引入完全新的数据源并用它们来创建特征。
新特征的灵感可能来自领域知识:与业务伙伴讨论他们认为可能是良好特征的内容会非常有帮助,尤其是在你对所从事的应用领域了解不如他们时。检查现有特征的交互作用也是假设新特征的一种方式,就像我们在活动 6.01中看到的与信用利用率相关的交互作用那样,使用 XGBoost 建模案例数据并用 SHAP 解释模型。
集成多个模型
在选择最终交付的案例研究项目模型时,交付随机森林或 XGBoost 中的任何一个都可能是可以接受的。机器学习中另一种常用的方法是集成多个模型。这意味着将不同模型的预测结果结合起来,类似于随机森林和 XGBoost 将多个决策树结合的方式。但在这种情况下,如何结合模型预测由数据科学家决定。创建模型集成的一种简单方式是取它们预测结果的平均值。
集成通常是在存在多个模型时进行的,可能是不同种类的模型或使用不同特征训练的模型,这些模型都具有良好的表现。在我们的案例中,可能使用随机森林和 XGBoost 的平均预测会比单独使用任何一个模型的表现更好。为了探索这一点,我们可以在验证集上比较表现,例如,用于 XGBoost 早停的验证集。
不同的建模技术
根据你为项目分配的时间和在不同建模技术方面的专业知识,你可能希望尝试尽可能多的方法。更高级的方法,例如用于分类的神经网络,可能在这个问题上提供更好的表现。我们鼓励你继续学习并掌握如何使用这些模型。然而,对于像我们这个案例研究中使用的表格数据,XGBoost 是一个非常好的默认选择,并且很可能提供优秀的表现,甚至可能是所有方法中最好的表现。
平衡类别
请注意,我们并没有处理响应变量中的类别不平衡。我们鼓励你尝试使用 scikit-learn 中的 class_weight='balanced' 选项或在 XGBoost 中使用 scale_pos_weight 超参数来拟合模型,以观察其效果。
尽管这些是进一步模型开发的有趣方向,但就本书而言,我们在此时已经完成了模型构建。我们将继续向前,检查 XGBoost 模型在测试集上的表现。
测试集上的模型表现
我们已经通过验证集对 XGBoost 模型的外部样本表现有了一些了解。然而,验证集在模型拟合过程中通过提前停止被使用。因此,我们能够做出的最严格的预期未来表现估计应该使用完全没有用于模型拟合的数据。这也是我们从模型构建过程中预留一个测试数据集的原因。
你可能会注意到,我们已经在一定程度上检查了测试集,例如,在第一章中评估数据质量和进行数据清理时。预测建模的金标准是在项目开始时预留出一个测试集,并且在模型完成之前不要对其进行任何检查。这是确保测试集中的任何知识没有在模型开发过程中“泄漏”到训练集中的最简单方法。当发生这种情况时,就有可能测试集不再能真实地代表未来的未知数据。然而,有时将所有数据一起探索和清理是很方便的,就像我们做的那样。如果测试数据与其余数据有相同的质量问题,那么就不会发生泄漏。最重要的是确保在决定使用哪些特征、拟合不同的模型并比较它们的表现时,不要查看测试集。
我们通过加载来自 活动 6.01、使用 XGBoost 构建案例研究数据模型并使用 SHAP 解释模型 中训练好的模型,连同训练数据、测试数据和特征名称,使用 Python 的 pickle 开始对测试集进行检查:
with open('../../Data/xgb_model_w_data.pkl', 'rb') as f:
features_response, X_train_all, y_train_all, X_test_all,\
y_test_all, xgb_model_4 = pickle.load(f)
在将这些变量加载到笔记本后,我们可以对测试集进行预测并进行分析。首先,获取测试集的预测概率:
test_set_pred_proba = xgb_model_4.predict_proba(X_test_all)[:,1]
现在,从 scikit-learn 导入 ROC AUC 计算函数,使用它来计算测试集的这一指标并显示出来:
from sklearn.metrics import roc_auc_score
test_auc = roc_auc_score(y_test_all, test_set_pred_proba)
test_auc
结果应如下所示:
0.7735528979671706
测试集上的 ROC AUC 为 0.774,略低于我们在验证集上看到的 XGBoost 模型的 0.779;然而,它们差异不大。由于模型拟合过程是针对验证集的性能进行了优化,因此在新数据上看到稍微低一点的表现并不完全令人意外。总体来说,测试性能符合预期,我们可以认为该模型在 ROC AUC 指标上已经成功测试。
虽然我们这里不会做这个,但在交付训练好的模型之前,最后一步可能是使用所有可用的数据,包括未见过的测试集,来拟合模型。这可以通过将训练数据和测试数据的特征(X_train_all,X_test_all)以及标签(y_train_all,y_test_all)进行拼接,使用它们来拟合一个新的模型,可能通过定义一个新的验证集来进行早停,或者使用当前的测试集来进行早停。这个方法的动机是机器学习模型通常在更多数据上训练时表现更好。缺点是,由于在这种情况下没有未见过的测试集,最终的模型可能被认为是未经测试的。
数据科学家对于采用哪种方法有不同的看法:是仅使用未见过的测试集进行模型评估,还是在完成所有前期步骤后,利用尽可能多的数据(包括测试集)来训练最终模型。一个考虑因素是模型是否会从更多数据中受益。这可以通过构建学习曲线来确定。虽然我们在这里不做说明,但学习曲线的概念是训练一个模型,使用不断增加的数据量,并计算在同一验证集上的验证分数。例如,如果你有 10,000 个训练样本,你可以留出 500 个作为验证集,然后先用前 1,000 个样本训练模型,再用前 2,000 个样本,依此类推,直到用完所有 9,500 个不在验证集中的样本。如果在更多数据上训练时,验证分数持续提高,甚至在使用所有可用数据时仍然有效,这表明使用更多数据训练模型会带来好处。然而,如果模型性能在某个点开始趋于平稳,并且似乎额外的数据并不能提升模型表现,那么你可能不需要这么做。学习曲线可以为如何使用测试集以及项目中是否需要更多数据提供指导。
就本案例研究而言,我们假设重新使用测试集来重新拟合模型不会带来任何益处。所以,现在我们主要关注的是向客户展示模型,帮助他们设计使用模型以实现业务目标的策略,并提供如何监控模型表现随时间变化的指导。
预测概率分布和十分位图
ROC AUC 指标很有帮助,因为它提供了一个总结模型在数据集上表现的单一数值。然而,观察模型在不同子集上的表现也是很有洞察力的。将数据集划分为不同子集的一种方式是使用模型预测结果。使用测试集,我们可以通过直方图可视化预测的概率:
mpl.rcParams['figure.dpi'] = 400
plt.hist(test_set_pred_proba, bins=50)
plt.xlabel('Predicted probability')
plt.ylabel('Number of samples')
这段代码应当生成如下图:

图 7.2:测试集的预测概率分布
测试集预测概率的直方图显示大多数预测集中在 [0, 0.2] 范围内。换句话说,根据模型,大多数借款人违约的概率在 0 到 20% 之间。然而,似乎有一小部分借款人存在更高的风险,集中在 0.7 附近。
检查模型在不同预测违约风险区域表现的直观方式是创建一个十分位图表,该图表根据预测概率的十分位将借款人分组。在每个十分位中,我们可以计算真实的违约率。我们预计会看到从最低预测十分位到最高预测十分位违约率的稳步上升。
我们可以像在 练习 6.01 中那样,使用 pandas 的 qcut 计算十分位数,随机网格搜索调优 XGBoost 超参数:
deciles, decile_bin_edges = pd.qcut(x=test_set_pred_proba,\
q=10,\
retbins=True)
在这里,我们正在拆分测试集的预测概率,这些预测概率通过 x 关键字参数提供。我们希望将它们分成 10 个大小相等的区间,预测概率最低的 10% 在第一个区间,以此类推,因此我们设置 q=10 分位数。然而,你可以将其分成任何数量的区间,比如 20(百分位)或 5(五分位)。由于我们设置了 retbins=True,区间的边界会保存在 decile_bin_edges 变量中,而十分位标签的序列则保存在 deciles 中。我们可以查看创建 10 个区间所需的 11 个区间边界:
decile_bin_edges
这应当产生如下图所示:
array([0.02213463, 0.06000734, 0.08155108, 0.10424594, 0.12708404,
0.15019046, 0.18111563, 0.23032923, 0.32210371, 0.52585585,
0.89491451])
为了使用 decile 序列,我们可以将其与测试集的真实标签和预测概率合并成一个 DataFrame:
test_set_df = pd.DataFrame({'Predicted probability':test_set_pred_proba,\
'Prediction decile':deciles,\
'Outcome':y_test_all})
test_set_df.head()
DataFrame 的前几行应如下所示:

图 7.3:包含预测概率和十分位数的 DataFrame
在 DataFrame 中,我们可以看到每个样本都被标注上了一个十分位区间,这个区间通过包含预测概率的区间边界来指示。结果显示了真实标签。我们希望在十分位图表中展示的是真实的违约率。为此,我们可以使用 pandas 的 groupby 功能。首先,我们通过对 decile 列进行分组来创建一个 groupby 对象:
test_set_gr = test_set_df.groupby('Prediction decile')
groupby对象可以通过其他列进行聚合。特别是在这里,我们关注的是每个十分位箱中的违约率,它是outcome变量的平均值。我们还计算了每个箱中数据的计数。由于分位数(如十分位)将总体分成相等大小的箱,因此我们预期计数应该相同或相似:
gr_df = test_set_gr.agg({'Outcome':['count', 'mean']})
查看我们的分组 DataFrame,gr_df:

图 7.4:测试集上预测概率的十个分位中的违约率
在图 7.4中,我们可以看到所有箱中的计数几乎相等。我们还可以看出,真实的违约率随着十分位的增加而增加,这是我们所期望的,因为我们知道我们的模型表现良好。在可视化数据之前,值得注意的是,这个 DataFrame 有一种特殊的列索引,叫做Outcome,以及第二级索引,标签分别为count和mean。访问具有多重索引的 DataFrame 的数据比我们之前使用的 DataFrame 要稍微复杂一些。我们可以通过以下方式显示列索引:
gr_df.columns
这将生成以下结果:
MultiIndex([('Outcome', 'count'),
('Outcome', 'mean')],
)
在这里我们可以看到,要访问多重索引中的某一列,我们需要使用元组来指定索引的每一层级,例如,gr_df[('Outcome','count')]。虽然在这里 MultiIndex 并不是必需的,因为我们只对一列(Outcome)进行了聚合,但在对多个列进行聚合时,它会变得非常有用。
现在我们想要创建一个可视化图表,展示模型的预测如何将借款人准确地分组,并且这些组的违约风险不断上升。我们将展示每个箱中的计数,以及每个箱中的违约风险。由于这些列的量级不同,计数在数百之间,风险在 0 到 1 之间,因此我们应该使用双y轴图。为了对图表外观有更多的控制,我们将使用 Matplotlib 函数来创建这个图,而不是通过 pandas 来做。首先,我们绘制每个箱中样本数量的图,并用与图表相同的颜色标注y轴刻度,确保清晰。请参阅 GitHub 上的笔记本,如果你是以黑白阅读的,因为颜色对该图很重要。这段代码应该与接下来的代码段在同一个单元格中运行。这里我们创建了一组坐标轴,然后添加了图表并进行了格式化和标注:
ax_1 = plt.axes()
color_1 = 'tab:blue'
gr_df[('Outcome', 'count')].plot.bar(ax=ax_1, color=color_1)
ax_1.set_ylabel('Count of observations', color=color_1)
ax_1.tick_params(axis='y', labelcolor=color_1)
ax_1.tick_params(axis='x', labelrotation = 45)
请注意,我们正在为样本大小创建一个bar图。我们希望在此基础上添加一条线形图,显示每个箱中的违约率,且该线图使用右侧的y轴,但与现有图表共享相同的x轴。Matplotlib 为此目的提供了一个叫做twinx的方法,该方法可以在axes对象上调用,返回一个共享相同x轴的新坐标轴对象。我们采取类似的步骤,然后绘制违约率并进行标注:
ax_2 = ax_1.twinx()
color_2 = 'tab:red'
gr_df[('Outcome', 'mean')].plot(ax=ax_2, color=color_2)
ax_2.set_ylabel('Default rate', color=color_2)
ax_2.tick_params(axis='y', labelcolor=color_2)
在运行前两个代码片段后,应该会出现以下图形:

图 7.5:根据模型预测十分位数的违约率
图 7.5 包含与 图 7.4 中显示的数据框相同的信息,但呈现方式更为美观。很明显,违约风险随着每个十分位数的增加而增加,风险最大的 10%借款人的违约率接近 70%,而风险最小的借款人违约率低于 10%。当一个模型能够有效地区分出违约风险持续增加的借款人群体时,称该模型倾斜了所研究的总体。还要注意,违约率在最低的 5 到 7 个十分位数之间相对平坦,这可能是因为这些观察值大多集中在预测风险范围[0, 0.2]中,如图 7.2中的直方图所示。
将测试集分成等人口十分位数是评估模型性能的一种方式,特别是在违约风险倾斜方面。然而,客户可能希望查看按不同群体划分的违约率,例如按等间隔箱(例如,将所有预测范围为 0, 0.2)、[0.2, 0.4)等的观察值放在一起,而不管每个箱中的样本大小),或以其他方式。你将在接下来的练习中探索如何在 pandas 中轻松实现这一点。
在接下来的练习中,我们将使用一些统计学概念来帮助创建误差条,包括我们之前学到的均值的标准误差和二项分布的正态近似。
我们从第五章,决策树和随机森林中知道,我们可以估计样本均值的方差为 ![1,其中 n 是样本大小,
是一个理论上更大总体的未观察到的方差。虽然我们不知道
,但可以通过我们观察到的样本方差来估计。对于二元变量,样本方差可以计算为 p(1-p),其中 p 是成功的比例,或者说是案例研究中的违约比例。根据上面的样本均值方差公式,我们可以代入观察到的方差,然后取平方根得到均值的标准误差:
。在某些情况下,这个公式也被称为二项分布的正态近似。我们将在下面使用它来为不同模型预测区间的违约率的等间隔图表创建误差条。有关这些概念的更多细节,建议查阅统计学教材。
练习 7.01:等间隔图表
在这个练习中,你将制作一个类似于图 7.5所示的图表;然而,与你将测试集拆分为预测概率的等人口十分位不同,你将使用预测概率的等间隔区间。如果业务伙伴希望使用某些得分范围来思考潜在的基于模型的策略,指定这些区间可能会有帮助。你可以使用 pandas 的 cut 来创建等间隔区间,或者使用一个区间边界数组创建自定义区间,这类似于你使用 qcut 来创建分位标签的方式:
注意
你可以在 packt.link/4Ev3n 找到这个练习的 Jupyter notebook。
-
使用以下代码创建等间隔标签系列,针对 5 个区间:
equal_intervals, equal_interval_bin_edges = \ pd.cut(x=test_set_pred_proba,\ bins=5,\ retbins=True)请注意,这与调用
qcut类似,不同的是在这里使用cut时,我们可以通过向bins参数传递一个整数来指定我们想要的等间隔区间数。你也可以为此参数提供一个数组来指定自定义的区间边界。 -
使用以下代码检查等间隔区间边界:
equal_interval_bin_edges结果应如下所示:
array([0.02126185, 0.1966906 , 0.37124658, 0.54580256, 0.72035853, 0.89491451])你可以通过从第一个元素到倒数第二个元素的子数组与从第二个元素开始到最后一个元素的子数组相减,来确认这些区间边界之间是等间隔的。
-
使用以下代码检查区间边界之间的间隔:
equal_interval_bin_edges[1:] - equal_interval_bin_edges[:-1]结果应如下所示:
array([0.17542876, 0.17455598, 0.17455598, 0.17455598, 0.17455598])你可以看到区间边界之间的距离大致相等。第一个区间边界比最小的预测概率稍小,你可以自行确认这一点。
为了创建一个类似于图 7.5的图表,首先我们需要将区间标签与响应变量放入一个 DataFrame,就像我们之前使用十分位标签时做的那样。我们还将预测概率放入 DataFrame 以供参考。
-
创建一个包含预测概率、区间标签和测试集响应变量的 DataFrame,如下所示:
test_set_bins_df =\ pd.DataFrame({'Predicted probability':test_set_pred_proba,\ 'Prediction bin':equal_intervals,\ 'Outcome':y_test_all}) test_set_bins_df.head()结果应如下所示:
![图 7.6:包含等间隔区间的 DataFrame]()
图 7.6:包含等间隔区间的 DataFrame
我们可以使用这个 DataFrame 按照区间标签进行分组,然后获取我们感兴趣的指标:表示默认率和每个区间内样本数量的聚合数据。
-
使用以下代码按区间标签分组并计算各区间内的默认率和样本数:
test_set_equal_gr = test_set_bins_df.groupby('Prediction bin') gr_eq_df = test_set_equal_gr.agg({'Outcome':['count', 'mean']}) gr_eq_df结果的 DataFrame 应该如下所示:
![图 7.7:五个等间隔区间的分组数据]()
图 7.7:五个等间隔区间的分组数据
请注意,与分位数不同,这里每个区间内的样本数是不同的。默认率在各区间之间呈现一致的增长趋势。让我们绘制这个 DataFrame,创建一个类似于图 7.5的可视化。
在创建此可视化之前,为了考虑到由于这些范围内的样本量减少,高预测概率的违约率估计可能不够稳健,我们将计算违约率的标准误差。
-
使用以下代码计算箱体内违约率的标准误差:
p = gr_eq_df[('Outcome', 'mean')].values n = gr_eq_df[('Outcome', 'count')].values std_err = np.sqrt(p * (1-p) / n) std_err结果应该如下所示:
array([0.00506582, 0.01258848, 0.02528987, 0.02762643, 0.02683029])注意,对于那些高分范围且样本较少的箱体,标准误差较大。将这些标准误差与违约率一起可视化会非常有帮助。
-
使用以下代码创建一个违约率与样本大小的等间隔图。该代码与 图 7.5 所需的代码非常相似,不同之处在于这里我们使用
yerr关键字和上一阶段的结果,在违约率图上加入了误差条:ax_1 = plt.axes() color_1 = 'tab:blue' gr_eq_df[('Outcome', 'count')].plot.bar(ax=ax_1, color=color_1) ax_1.set_ylabel('Count of observations', color=color_1) ax_1.tick_params(axis='y', labelcolor=color_1) ax_1.tick_params(axis='x', labelrotation = 45) ax_2 = ax_1.twinx() color_2 = 'tab:red' gr_eq_df[('Outcome', 'mean')].plot(ax=ax_2, color=color_2, yerr=std_err) ax_2.set_ylabel('Default rate', color=color_2) ax_2.tick_params(axis='y', labelcolor=color_2)结果应该如下所示:
![图 7.8:等间隔箱体中的违约率与样本数量的图]()
图 7.8:等间隔箱体中的违约率与样本数量的图
我们可以在 图 7.8 中看到,不同箱体中的样本数量差异较大,与分位数方法形成对比。尽管高分数箱体中的样本相对较少,导致较大的标准误差,但违约率图上的误差条仍然较小,相比于从低分到高分箱体的总体趋势,我们可以对这一趋势有信心。
预测概率的校准
图 7.8 的一个有趣特点是,违约率的折线图从一个箱体到下一个箱体大致增加相同的量。与 图 7.5 中的十分位图对比,违约率最初增加缓慢,然后迅速增加。还要注意,违约率大致是每个箱体内预测概率边缘的中点。这意味着违约率与每个箱体的平均模型预测相似。换句话说,我们的模型不仅能有效地按照从低到高的违约风险对借款人进行排序(通过 ROC AUC 量化),而且还似乎能够准确预测违约概率。
衡量预测概率与实际概率的匹配程度是校准****概率的目标。概率校准的标准度量遵循上述讨论的概念,称为期望校准误差(ECE),定义为:

图 7.9:期望校准误差
其中,索引 i 从 1 到箱体数目(N)范围,Fi 是所有样本中落入箱体 i 的比例,oi 是箱体 i 中为正样本(即在案例研究中为违约者)的比例,ei 是箱体 i 内预测概率的平均值。
我们可以使用一个与图 7.4中非常相似的 DataFrame 来计算测试集中分位区间内预测概率的 ECE,这个 DataFrame 用于创建分位图。我们唯一需要添加的是每个区间内的平均预测概率。按如下方式创建这样的 DataFrame:
cal_df = test_set_gr.agg({'Outcome':['count', 'mean'],\
'Predicted probability':'mean'})
cal_df
输出的 DataFrame 应如下所示:

图 7.10:计算 ECE 指标的 DataFrame
为了方便起见,我们定义一个变量F,表示每个区间中样本的比例。这是从上面的 DataFrame 中每个区间的计数除以样本总数,后者是从测试集响应变量的形状中获得的:
F = cal_df[('Outcome', 'count')].values/y_test_all.shape[0]
F
输出应为:
array([0.10003368, 0.10003368, 0.10003368, 0.09986527, 0.10003368,
0.10003368, 0.09986527, 0.10003368, 0.10003368, 0.10003368])
因此,每个区间大约包含 10%的样本。这是预期的,当然,因为区间是使用分位数方法创建的。然而,对于其他分箱方法,区间中的样本数量可能不相等。现在让我们在代码中实现 ECE 的公式来计算这个指标:
ECE = np.sum(
F
* np.abs(
cal_df[('Outcome', 'mean')]
- cal_df[('Predicted probability', 'mean')]))
ECE
输出应为:
0.008144502190176022
这个数字代表我们最终模型在测试集上的 ECE。单独来看,这个数字并没有太多意义。然而,像这样的指标可以在时间上进行监控,在模型投入生产并在实际应用中使用之后。如果 ECE 开始增加,这表示模型的校准性变差,可能需要重新训练,或者对输出应用校准过程。
检查我们预测概率的校准的一个更直观方法是绘制 ECE 所需的各个成分,特别是响应变量的真实违约率,与每个区间内模型预测的平均值进行对比。在此基础上,我们加上一条 1-1 线,表示完美校准,作为参考点:
ax = plt.axes()
ax.plot([0, 0.8], [0, 0.8], 'k--', linewidth=1,
label='Perfect calibration')
ax.plot(cal_df[('Outcome', 'mean')],\
cal_df[('Predicted probability', 'mean')],\
marker='x',\
label='Model calibration on test set')
ax.set_xlabel('True default rate in bin')
ax.set_ylabel('Average model prediction in bin')
ax.legend()
结果图应如下所示:

图 7.11:预测概率的校准图
图 7.11显示了模型预测的概率与真实的违约率非常接近,因此模型似乎被良好校准。为了获得更多见解,你可以尝试自己向这个图添加误差条作为练习。还要注意,scikit-learn 提供了一个函数来计算创建图 7.11所需的信息:sklearn.calibration.calibration_curve。然而,这个函数并不会返回每个区间的样本大小。
对于概率校准,另一个需要注意的点是,一些处理类别不平衡的方法,如过采样或欠采样,会改变训练数据集中类别的比例,这会影响预测的概率,并可能使其不那么准确。不过,这一点可能不那么重要,特别是与模型根据借款人违约风险进行排名的能力(通过 ROC AUC 衡量)相比,这取决于客户的需求。
财务分析
我们目前计算的模型性能指标基于一些抽象的度量,可以应用于分析任何分类模型:模型的准确度、模型在不同阈值下识别真阳性与假阳性能力的准确性(ROC AUC)、正向预测的正确性(精确度),或诸如斜坡风险等直观的度量。这些指标对于理解模型的基本工作原理非常重要,并且在机器学习领域广泛使用,因此理解它们非常关键。然而,在将模型应用于商业案例时,我们并不能总是直接使用这些性能指标来制定如何使用模型来指导商业决策或评估模型可能创造的价值的策略。为了更进一步地将预测概率和阈值的数学世界与成本和收益的商业世界联系起来,通常需要进行某种财务分析。
为了帮助客户进行此分析,数据科学家需要了解根据模型预测可能采取的决策和行动。这应该是与客户的对话内容,最好在项目生命周期的早期进行。我们将其留到本书的最后,以便建立对预测建模是什么以及如何工作的基础理解。然而,在项目开始时了解模型使用的商业背景,可以让你为模型性能设定目标,并在整个项目过程中追踪这些目标,就像我们追踪不同模型的 ROC AUC 一样。将模型性能指标转化为财务术语是本节的主题。
对于像案例研究中的二分类模型,数据科学家需要了解以下几个问题的答案,以帮助客户弄清楚如何使用该模型:
-
客户希望通过模型帮助做出哪些决策?
-
如何利用二分类模型的预测概率来帮助做出这些决策?
-
它们是是/否决策吗?如果是,那么选择一个单一的预测概率阈值就足够了。
-
是否会根据模型结果做出超过两级的活动决策?如果是,那么选择两个或更多的阈值,例如将预测结果分为低、中、高风险,可能是解决方案。例如,预测概率低于 0.5 可以视为低风险,0.5 到 0.75 之间为中风险,0.75 以上为高风险。
-
根据模型指导,采取不同行动方案的成本是什么?
-
从模型指导下采取成功行动可能获得的潜在好处是什么?
与客户的财务对话
我们向案例研究客户询问了上述要点,得知以下信息:对于那些高风险违约的信贷账户,客户正在设计一个新项目,为账户持有人提供个性化的咨询,鼓励他们按时支付账单,或在无法按时支付的情况下提供替代付款选项。信贷咨询由经过培训的客户服务代表在呼叫中心进行。每次咨询的费用为新台币 7,500 元,每次咨询的预期成功率为 70%,意味着平均来说,70% 接到提供咨询电话的客户会按时支付账单,或者采取对债权人可接受的替代安排。成功咨询的潜在收益在于,如果账户本应违约但在咨询后未违约,则账户的每月账单金额将视为节省。目前,违约账户的每月账单会被报告为损失。
在与客户进行前述对话后,我们已经获得了进行财务分析所需的材料。客户希望我们帮助他们决定应该联系哪些成员并提供信贷咨询。如果我们能帮助他们缩小需要联系进行咨询的人员名单,就能通过避免不必要和昂贵的联系方式为他们节省费用。客户在信贷咨询上的有限资源将更合适地用于高风险违约账户。这应该能够通过预防违约带来更大的节省。此外,客户告诉我们,如果我们能给他们一个关于值得提供多少次咨询的概念,我们的分析可以帮助他们申请咨询项目的预算。
在进行财务分析时,我们看到模型将帮助客户做出的决策是逐个账户进行的“是/否”决策:是否为给定账户的持有人提供咨询。因此,我们的分析应该专注于找到一个合适的预测概率阈值,通过该阈值我们可以将账户分为两组:高风险账户将接受咨询,低风险账户则不接受。
练习 7.02: 成本与节省的描述
模型输出与客户将做出的业务决策之间的联系,归结为为预测概率选择一个阈值。因此,在本次练习中,我们将描述咨询程序的预期成本(以提供单独咨询会议的成本表示)以及预期节省(以预防违约的节省表示),这些将在一系列阈值下进行计算。每个阈值下会有不同的成本和节省,因为每个阈值预计会导致不同数量的正预测结果,并且在这些结果中会有不同数量的真正正例。第一步是创建一个潜在阈值的数组。我们将使用从 0 到 1,步长为 0.01 的值。执行以下步骤以完成本次练习:
注意
本次练习的 Jupyter notebook 可以在这里找到:packt.link/yiMEr。基于本章前面的结果,已在 notebook 中添加了准备数据的额外步骤。请确保在进行本次练习前,先执行 notebook 中展示的前提步骤。
-
使用以下代码创建一个阈值范围,以计算咨询的预期成本和收益:
thresholds = np.linspace(0, 1, 101)这会在 0 到 1 之间(包括 0 和 1)创建 101 个等间距的点。
现在,我们需要了解预防违约的潜在节省。为了精确计算这一点,我们需要知道下个月的月度账单。然而,客户已经告知我们,在他们需要创建待联系账户持有者名单时,这些数据将无法提供。因此,为了估算潜在节省,我们将使用最近的月度账单。
我们将使用测试数据来创建此分析,因为它提供了模型交付给客户后使用的模拟:在未用于模型训练的新账户上。
-
确认测试数据特征数组中对应最近一个月账单的索引:
features_response[5]输出应为:
'BILL_AMT1'索引 5 对应的是最近几个月的账单,我们稍后会用到。
-
将咨询成本存储在一个变量中,以便用于分析:
cost_per_counseling = 7500我们还知道,客户告知我们,咨询程序的效果并不是 100%的。我们应该在分析中考虑这一点。
-
存储客户提供的有效性率,以便用于分析:
effectiveness = 0.70现在,我们将计算每个阈值的成本和节省。我们会逐步讲解每个计算过程,但目前我们需要创建空数组来存储每个阈值的结果。
-
创建空数组来存储分析结果。我们将在后续步骤中解释每个数组的作用:
n_pos_pred = np.empty_like(thresholds) total_cost = np.empty_like(thresholds) n_true_pos = np.empty_like(thresholds) total_savings = np.empty_like(thresholds)这些创建了与我们分析中的阈值数量相同的空数组。我们将遍历每个阈值值来填充这些数组。
-
创建一个
counter变量并打开一个for循环来遍历各个阈值:counter = 0 for threshold in thresholds:对于每个阈值,根据预测概率高于该阈值的数量,会有不同数量的正向预测。这些预测对应的是被认为会违约的账户。每个被预测为违约的账户都会接到一次咨询电话,这会产生相应的费用。因此,这是成本计算的第一部分。
-
确定在该阈值下哪些账户获得了正向预测:
pos_pred = test_set_pred_proba > thresholdpos_pred是一个布尔数组。pos_pred的总和表示在该阈值下预测的违约数量。 -
计算给定阈值下的正向预测数量:
n_pos_pred[counter] = sum(pos_pred) -
计算给定阈值下的咨询总成本:
total_cost[counter] \ = n_pos_pred[counter] * cost_per_counseling现在,我们已经了解了咨询项目在每个阈值下的可能成本,我们需要查看预期的节省。在提供咨询服务给正确的账户持有人时,会获得节省:即那些本来会违约的账户。从分类问题的角度来看,这些是正向预测,其响应变量的真实值也是正向的——换句话说,是真正的正向预测。
-
基于正向预测数组和响应变量,确定哪些账户是真正的正向预测:
true_pos = pos_pred & y_test_all.astype(bool) -
通过对真正的正向预测数组求和来计算真正的正向预测数量:
n_true_pos[counter] = sum(true_pos)我们从成功为原本会违约的账户持有人提供咨询所能获得的节省,取决于每个预防违约的节省金额以及咨询的有效性率。我们无法预防每一个违约。
-
使用真正的正向预测数量、预防违约的节省(通过上个月的账单估算)和咨询的有效性率来计算每个阈值下的预期节省:
total_savings[counter] = np.sum( true_pos.astype(int) * X_test_all[:,5] * effectiveness ) -
增加计数器:
counter += 1步骤 5 到 13 应该作为
for循环在 Jupyter Notebook 的一个单元格中运行。之后,可以通过将节省减去成本来计算每个阈值的净节省。 -
通过将节省和成本数组相减,计算所有阈值的净节省:
net_savings = total_savings - total_cost现在,我们可以可视化通过为合适的账户持有人提供咨询服务,我们可能帮助客户节省多少钱。让我们来可视化一下。
-
按照如下方式绘制净节省与阈值的关系:
mpl.rcParams['figure.dpi'] = 400 plt.plot(thresholds, net_savings) plt.xlabel('Threshold') plt.ylabel('Net savings (NT$)') plt.xticks(np.linspace(0,1,11)) plt.grid(True)结果图应如下所示:
![图 7.12:净节省与阈值的关系图]()
图 7.12:净节省与阈值的关系图
图表显示,阈值的选择非常重要。虽然在许多不同的阈值下都能实现净节省,但看起来通过将阈值设置在大约 0.25 到 0.5 之间的某个范围内,可以获得最大的净节省。
让我们确认产生最大节省的最佳阈值,并查看节省的具体数额。
-
使用 NumPy 的
argmax查找净节省数组中最大元素的索引:max_savings_ix = np.argmax(net_savings) -
显示产生最大净节省的阈值:
thresholds[max_savings_ix]输出应如下所示:
0.36 -
显示最大的净节省:
net_savings[max_savings_ix]输出应如下所示:
13415710.0
我们发现,最大净节省发生在 0.36 的阈值下。在这个阈值下,所实现的净节省金额超过 1300 万新台币,针对这个测试数据集中的账户。这些节省需要根据客户所服务的账户数量进行规模化估算,以推算出总的可能节省金额,前提是我们所使用的数据能够代表所有这些账户。
然而需要注意的是,节省金额在约 0.5 的阈值之前大致相同,如图 7.12所示。
随着阈值的增加,我们在“提高标准”,即要求客户的风险更高,才能与他们联系并提供咨询服务。从 0.36 提高到 0.5 意味着我们只会联系那些风险较高、概率大于 0.5 的客户。这意味着联系的客户较少,从而减少了项目的前期成本。图 7.12 表明,即使我们联系较少的客户,可能仍然能够创造大致相同的净节省。虽然净效应相同,但咨询的初始支出较小。这对客户来说可能是更可取的。我们将在接下来的活动中进一步探讨这个概念。
活动 7.01:得出财务见解
财务分析的原始数据已完成。然而,在本活动中,你的目标是从这些结果中生成一些额外的见解,为客户提供更多的背景信息,帮助他们理解我们构建的预测模型如何为他们创造价值。特别地,我们已经查看了模型构建中保留的测试集结果。客户可能拥有比他们提供给我们的更多的账户,这些账户能代表他们的业务。你应该向他们报告可以轻松扩展到他们业务规模的结果,具体来说,是以账户数量为基础。
我们还可以帮助客户了解这个项目的成本;虽然净节省是需要考虑的一个重要数字,但客户必须在实现这些节省之前为咨询项目提供资金。最后,我们将把财务分析与标准的机器学习模型性能指标关联起来。
一旦完成活动,你应该能够向客户传达咨询项目的初步成本,并获得诸如下图所示的精确率和召回率的图表:

图 7.13:预期的精确率-召回率曲线
这个曲线在解释模型在不同阈值下创造的价值时非常有用。
执行以下步骤以完成活动:
注意
包含此活动代码的 Jupyter 笔记本可以在此处找到:packt.link/2kTVB。根据本章先前的结果,已向笔记本添加了为此活动准备数据的额外步骤。请按照笔记本中呈现的先决步骤执行这些步骤。
-
使用测试集,计算如果没有辅导计划,所有违约的成本。
-
计算辅导计划可以减少违约成本的百分比。
-
计算在最优阈值下每个帐户的净储蓄,考虑可能能够辅导的所有帐户,换句话说,相对于整个测试集。
-
绘制每个阈值下每个帐户的净储蓄与每个帐户的辅导成本。
-
绘制在每个阈值下预测为正的帐户的比例(这称为“标志率”)。
-
绘制测试数据的精确率-召回率曲线。
-
在y-轴上分别绘制精确率和召回率,对应x-轴上的阈值。
注意
通过此链接可找到此活动的解决方案。
关于向客户交付预测模型的最终思考
我们现在已经完成了建模活动,并创建了财务分析,以指示客户如何使用模型。虽然我们已完成了数据科学家的基本智力贡献,但需要与客户达成一致意见,以确定所有这些贡献的交付形式。
关键贡献是训练模型中体现的预测能力。假设客户可以使用我们用 XGBoost 创建的训练模型对象,此模型可以像我们所做的那样保存到磁盘并发送给客户。然后,客户可以在其工作流程中使用它。这种模型交付路径可能需要数据科学家与客户组织的工程师合作,以在客户基础设施中部署模型。
或者,可能需要将模型表达为数学方程(例如,使用逻辑回归系数)或一组 if-then 语句(如决策树或随机森林),客户可以使用 SQL 实现预测能力。虽然由于可能存在许多树和许多级别,将随机森林表达为 SQL 代码是很繁琐的,但有软件包可以根据训练好的 scikit-learn 模型为您创建此表示(例如,pypi.org/project/SKompiler/)。
注意:云平台用于模型开发和部署
在本书中,我们使用 scikit-learn 和 XGBoost 包在本地计算机上构建预测模型。最近,云平台如亚马逊网络服务(AWS)通过 Amazon SageMaker 等服务提供了机器学习能力。SageMaker 包含了一个 XGBoost 的版本,您可以使用类似于我们在这里所做的语法来训练模型。在本书中展示的方法和亚马逊 SageMaker 的模型训练实现之间可能存在细微差异,建议您在每一步检查您的工作,以确保结果符合预期。例如,在 SageMaker 中使用早停止来拟合 XGBoost 模型可能需要额外的步骤,以确保训练的模型使用最佳迭代进行预测,而不是在训练停止时的最后迭代。
云平台如 AWS 很有吸引力,因为它们可能极大地简化将训练好的机器学习模型集成到客户的技术堆栈中的过程,而这在许多情况下可能已经建立在云平台上。
在使用模型进行预测之前,客户需要确保数据的准备方式与我们之前构建模型时相同。例如,删除所有特征值为0的样本以及清理EDUCATION和MARRIAGE特征的操作必须与我们在本章前面演示的方式相同。另外,还有其他可能的交付模型预测的方式,比如客户将特征提供给数据科学家,然后接收预测结果。
讨论交付内容的另一个重要考虑因素是:预测结果应该以什么格式交付? 二元分类模型的典型交付格式,例如我们为案例研究创建的模型,是按照预测违约概率对账户进行排名。预测的概率应该与账户 ID 以及客户希望的其他列一起提供。这样,当呼叫中心逐个联系账户持有人以提供咨询时,他们可以首先联系那些违约风险最高的人,然后根据时间和资源的允许继续联系优先级较低的账户持有人。客户应该被告知使用哪个预测概率阈值,以获得最高的净节省。这个阈值将代表按照违约概率排名的账户持有人联系的终止点。
模型监控
根据客户与数据科学家的合作时间,持续监控模型性能随时间的变化始终是有益的,因为它在使用过程中是否保持稳定或逐渐下降?在评估此案例时,需要牢记,如果账户持有人正在接受辅导,他们的违约概率可能会低于预测的概率,因为新辅导计划的预期效果。因此,为了测试辅导计划的有效性,最好将一部分随机选择的账户持有人保留下来,这部分人将不会接受任何辅导,无论他们的违约风险如何。这部分人被称为对照组,它应该相对于接受辅导的账户群体较小,但足够大以便得出统计学上显著的推论。
尽管本书的范围并不包括如何设计和使用对照组的细节,但在这里可以简要说明,模型的预测能力可以通过对照组进行评估,因为他们没有接受任何辅导,类似于模型训练时使用的账户群体。对照组的另一个好处是,可以将违约率及因违约而产生的财务损失与那些接受了模型引导辅导计划的账户进行比较。如果该计划按预期运行,接受辅导的账户应具有较低的违约率和较小的违约财务损失。对照组可以提供证据,证明该计划实际上是有效的。
注意:选择性治疗的高级建模技术——提升建模
当一家企业考虑是否有选择地为其客户提供昂贵的治疗方案(如案例研究中的辅导计划)时,应考虑使用一种被称为提升建模的技术。提升建模旨在基于个体来确定治疗的有效性。我们假设电话辅导对客户的有效性平均为 70%。然而,效果可能因客户而异;一些客户更容易接受,另一些则不太容易。有关提升建模的更多信息,请参见www.steveklosterman.com/uplift-modeling/。
一种相对简单的监控模型实施方法是查看模型预测的分布是否随着时间变化,与用于模型训练的人群相比有所变化。我们在图 7.2中绘制了测试集的预测概率直方图。如果预测概率的直方图形状发生了显著变化,这可能表明特征发生了变化,或者特征与响应之间的关系发生了变化,模型可能需要重新训练或重建。为了量化分布的变化,感兴趣的读者可以查阅统计资源,学习卡方拟合优度检验或 Kolmogorov-Smirnov 检验。如果模型预测的分布发生变化,或者根据所选择的阈值,预测违约账户的比例发生明显变化,也可能会显现出来。
本章及全书中呈现的所有其他模型评估指标也可以是监控模型在生产环境中表现的好方法:十分位数图和等间距图、校准、ROC AUC 等。
预测建模中的伦理问题
随着机器学习的应用扩展到大多数现代企业,模型是否做出公平预测的问题受到了更多关注。公平性可以通过模型是否同样擅长为不同受保护类群体的成员做出预测来评估,例如,不同性别群体。
在本书中,我们采用了将性别从模型特征中移除的方法。然而,其他特征可能有效地充当性别的代理,因此即使性别没有作为特征使用,模型也可能对不同性别群体产生偏见的结果。筛查这种偏见可能性的一个简单方法是检查模型中使用的特征是否与受保护类群体有特别高的关联性,例如,可以通过使用 t 检验来进行检查。如果是这样,最好将这些特征从模型中移除。
如何判断模型是否公平,以及如果不公平,应该如何处理,这个问题是当前研究的热点。我们鼓励你熟悉像 AI Fairness 360(aif360.mybluemix.net/)这样的努力,这些努力正在提供工具,以提高机器学习中的公平性。在开始与公平性相关的工作之前,理解客户对于公平的定义至关重要,因为不同国家的法律和客户组织的具体政策可能会导致公平的定义因地区而异。
总结
在本章中,你学习了几种分析技术,以提供对模型性能的洞察,例如按模型预测区间划分的违约率的十分位和等距图表,以及如何调查模型校准的质量。通过使用模型测试集得出这些洞察并计算诸如 ROC AUC 等指标是很好的,因为这旨在代表模型在真实世界中新数据上的表现。
我们还看到如何进行模型性能的财务分析。虽然我们将此部分留到了书的最后,但对与模型决策相关的成本和节省的理解应该从典型项目开始时就有所了解。这些使数据科学家能够朝着通过增加利润或节省成本的具体目标努力。对于二分类模型,这一过程的关键步骤是选择一个预测概率阈值,以此来宣布一个正向预测,从而最大化由模型指导决策所带来的利润或节省。
最后,我们考虑了与交付和监控模型相关的任务,包括建立对照组以监控模型性能并测试任何由模型输出指导的程序有效性的想法。对照组的结构和模型监控策略会因项目而异,因此你需要在每个新案例中确定合适的行动方案。为了进一步了解在现实世界中使用模型,你应继续研究如实验设计、可以用于训练和部署模型的云平台(例如 AWS)以及预测建模中的公平性问题等主题。
你现在已经完成了项目,并准备向客户交付你的研究成果。除了将训练好的模型保存到磁盘或你可能提供给客户的其他数据产品或服务外,你还可能希望创建一个演示文稿,通常是一个幻灯片展示,详细说明你的进展。此类演示文稿的内容通常包括问题陈述、数据探索和清理结果、你构建的不同模型的性能比较、模型解释(如 SHAP 值),以及展示你的工作价值的财务分析。在制作你的工作演示文稿时,通常更好的是通过图片讲述故事,而不是大量的文字。在整本书中,我们展示了许多可用于此目的的可视化技术,你应继续探索描绘数据和建模结果的方式。
始终确保询问客户他们可能希望在演示文稿中看到的具体内容,并确保回答他们所有的问题。当客户看到你能够以易于理解的方式为他们创造价值时,你就成功了。
附录
1. 数据探索与清理
活动 1.01:探索数据集中剩余的财务特征
解决方案:
在开始之前,设置好你的环境并按如下方式加载已清理的数据集:
import pandas as pd
import matplotlib.pyplot as plt #import plotting package
#render plotting automatically
%matplotlib inline
import matplotlib as mpl #additional plotting functionality
mpl.rcParams['figure.dpi'] = 400 #high resolution figures
mpl.rcParams['font.size'] = 4 #font size for figures
from scipy import stats
import numpy as np
df = pd.read_csv('../../Data/Chapter_1_cleaned_data.csv')
-
为剩余的财务特征创建特征名称列表。
这些可以分为两组,因此我们将像之前一样列出特征名称,以便一起分析。你可以使用以下代码来实现:
bill_feats = ['BILL_AMT1', 'BILL_AMT2', 'BILL_AMT3', \ 'BILL_AMT4', 'BILL_AMT5', 'BILL_AMT6'] pay_amt_feats = ['PAY_AMT1', 'PAY_AMT2', 'PAY_AMT3', \ 'PAY_AMT4', 'PAY_AMT5', 'PAY_AMT6'] -
使用
.describe()方法查看账单金额特征的统计摘要。反思你所看到的内容。这合理吗?使用以下代码查看摘要:
df[bill_feats].describe()输出应该如下所示:
![图 1.47:过去 6 个月账单金额的统计描述]()
图 1.47:过去 6 个月账单金额的统计描述
我们看到平均每月账单大约是 40,000 到 50,000 新台币。建议你检查一下本地货币的汇率。例如,1 美元约等于 30 新台币。做一下换算,问问自己,这个月度支付是否合理?我们也应该向客户确认这一点,但看起来是合理的。
我们还注意到有些账单金额为负。这似乎是合理的,因为可能是前一个月的账单超额支付了,或许是预期当前账单中会有某项购买。类似的情况会导致该账户余额为负,意味着该账户持有人有了一个信用额度。
-
使用以下代码,按 2x3 的网格方式可视化账单金额特征的直方图:
df[bill_feats].hist(bins=20, layout=(2,3))图表应该是这样的:
![图 1.48:账单金额的直方图]()
图 1.48:账单金额的直方图
图 1.48中的直方图从多个方面来看是有意义的。大多数账户的账单金额较小。随着账单金额的增加,账户的数量逐渐减少。看起来账单金额的分布在每个月之间大致相似,因此我们没有像处理支付状态特征时那样发现数据不一致问题。该特征似乎通过了我们的数据质量检查。现在,我们将继续分析最后一组特征。
-
使用
.describe()方法,通过以下代码获取支付金额特征的摘要:df[pay_amt_feats].describe()输出应如下所示:
![图 1.49:过去 6 个月账单支付金额的统计描述]()
图 1.49:过去 6 个月账单支付金额的统计描述
平均支付金额大约比我们在前面的活动中总结的平均账单金额低一个数量级(10 的幂)。这意味着“平均情况”是一个每月未还清全部余额的账户。从我们对
PAY_1特征的探索来看,这很有意义,因为该特征中最常见的值是 0(账户至少支付了最低付款额,但没有支付全部余额)。没有负支付,这也似乎是合理的。 -
绘制与账单金额特征类似的支付金额特征的直方图,但还要使用
xrot关键字参数对 x 轴 标签进行旋转,以避免重叠。使用xrot=<角度>关键字参数按给定的角度(以度为单位)旋转 x 轴 标签,使用以下代码:df[pay_amt_feats].hist(layout=(2,3), xrot=30)在我们的案例中,我们发现 30 度的旋转效果很好。绘图应如下所示:
![图 1.50:原始支付金额数据的直方图]()
py图 1.50:原始支付金额数据的直方图
这张图的快速浏览表明,这不是一个非常有用的图形;大多数直方图只有一个区间的高度较为显著。这不是可视化这些数据的有效方式。看起来,月度支付金额主要集中在包含 0 的区间中。那么,实际上有多少项是 0 呢?
-
使用布尔掩码来查看支付金额数据中有多少项恰好等于 0,使用以下代码:使用以下代码执行此操作:
pay_zero_mask = df[pay_amt_feats] == 0 pay_zero_mask.sum() ```py 输出应如下所示:  图 1.51:支付金额等于 0 的账单计数 `pay_zero_mask` 是一个包含 `True` 和 `False` 值的 DataFrame,表示支付金额是否等于 0。第二行对该 DataFrame 进行列求和,将 `True` 视为 1,将 `False` 视为 0,因此列的和表示每个特征中支付金额为 0 的账户数。 我们可以看到,约 20-25% 的账户在任何给定的月份里账单支付额为 0。然而,大多数账单支付额大于 0。那么,为什么我们在直方图中看不到它们呢?这是由于账单支付额的**范围**相对于大多数账单支付额的值。 在统计摘要中,我们可以看到一个月中的最大账单支付额通常比平均账单支付额大两个数量级(100 倍)。看起来这些非常大的账单支付可能只有少数几个。然而,由于直方图的创建方式,使用相同大小的区间,几乎所有数据都被聚集在最小的区间中,较大的区间几乎不可见,因为它们的账户数太少。我们需要一种有效的策略来可视化这些数据。 -
忽略前一步创建的掩码中的 0 支付值,使用 pandas 的
.apply()方法和 NumPy 的np.log10()方法绘制非零支付的对数转换直方图。你可以使用.apply()将任何函数(包括log10)应用到 DataFrame 的所有元素。使用以下代码来完成此操作:df[pay_amt_feats][~pay_zero_mask].apply(np.log10)\ .hist(layout=(2,3)) ```py 这是 pandas 的一个相对高级的使用方法,所以如果你自己没弄明白也不必担心。然而,开始理解如何用相对少量的代码在 pandas 中做很多事情是很有帮助的。 输出应如下所示: 
图 1.52:非零账单支付金额的 10 为基对数
虽然我们本可以尝试创建不同宽度的区间来更好地可视化支付金额,但另一种常用且便捷的方法是对数变换,或称为对数变换。我们使用了 10 为基的对数变换。大致来说,这种变换告诉我们一个数值中有多少个零。换句话说,一个余额至少为 100 万美元但不到 1000 万美元的账户,其对数变换结果会是 6 到 7 之间,因为 106 = 1,000,000(而log10(1,000,000) = 6),而 107 = 10,000,000。
为了将此变换应用到我们的数据,首先需要屏蔽掉零支付值,因为log10(0)是未定义的(另一种常见方法是对所有值加上一个非常小的数字,例如 0.01,这样就没有零值)。我们使用了 Python 逻辑运算符not(~)和我们已经创建的零值掩码。然后,我们使用了 pandas 的.apply()方法,它可以将我们喜欢的任何函数应用到我们选择的数据上。在这种情况下,我们希望应用的是一个基于 10 的对数,使用np.log10来计算。最后,我们对这些值绘制了直方图。
结果是更加有效的数据可视化:这些值在直方图的区间中分布得更具信息量。我们可以看到,最常出现的账单支付金额位于千元范围内(log10(1,000) = 3),这与我们在统计摘要中观察到的平均账单支付金额一致。也有一些非常小的账单支付金额,以及少数较大的支付金额。总体来看,账单支付金额的分布从每月来看似乎非常一致,因此我们没有发现该数据中存在任何潜在问题。
2. Scikit-Learn 简介与模型评估
活动 2.01:使用新特征执行逻辑回归并创建精准率-召回率曲线
解决方案:
-
使用 scikit-learn 的
train_test_split生成新的训练和测试数据集。这次,使用LIMIT_BAL,即账户的信用额度,作为特征,而不是EDUCATION。执行以下代码来实现:
X_train_2, X_test_2, y_train_2, y_test_2 = train_test_split\ (df['LIMIT_BAL']\ .values\ .reshape(-1,1),\ df['default'\ 'payment next'\ 'month'].values,\ test_size=0.2,\ random_state=24)) ```py 请注意,这里我们创建了新的训练和测试数据集,并且变量名称也发生了变化。 -
使用你拆分后的训练数据训练一个逻辑回归模型。
以下代码实现了这个功能:
example_lr.fit(X_train_2, y_train_2) ```py 如果你在一个单一的笔记本中运行整个章节,可以重新使用之前使用的模型对象`example_lr`。你可以**重新训练**这个对象,以学习这个新特征与响应之间的关系。如果你愿意,也可以尝试不同的训练/测试拆分,而不必创建新的模型对象。在这些场景中,现有的模型对象已经被**原地更新**。 -
创建测试数据的预测概率数组。
这里是此步骤的代码:
y_test_2_pred_proba = example_lr.predict_proba(X_test_2) ```py -
使用预测的概率和测试数据的真实标签计算 ROC AUC。将其与使用
EDUCATION特征的 ROC AUC 进行比较。运行以下代码进行这一步操作:
metrics.roc_auc_score(y_test_2, y_test_2_pred_proba[:,1]) ```py 输出结果如下:0.6201990844642832
请注意,我们对预测的概率数组进行了索引,以便从第二列获取正类的预测概率。与`EDUCATION`的逻辑回归 ROC AUC 相比,结果如何?AUC 更高。这可能是因为现在我们使用的是与账户财务状况(信用额度)相关的特征,来预测与账户财务状况相关的另一项内容(是否违约),而不是使用与财务关系较弱的特征。 -
绘制 ROC 曲线。
这里是实现此功能的代码;它与我们在前一个练习中使用的代码类似:
fpr_2, tpr_2, thresholds_2 = metrics.roc_curve\ (y_test_2, \ y_test_2_pred_proba[:,1]) plt.plot(fpr_2, tpr_2, '*-') plt.plot([0, 1], [0, 1], 'r--') plt.legend(['Logistic regression', 'Random chance']) plt.xlabel('FPR') plt.ylabel('TPR') plt.title('ROC curve for logistic regression with '\ 'LIMIT_BAL feature') ```py 图形应如下所示:  图 2.30:LIMIT_BAL 逻辑回归的 ROC 曲线 这看起来有点像我们希望看到的 ROC 曲线:它比仅使用`EDUCATION`特征的模型更远离随机机会线。还注意到,真实和假阳性率的变化在阈值范围内更加平滑,反映了`LIMIT_BAL`特征具有更多不同的值。 -
使用 scikit-learn 的功能计算测试数据上精确度-召回率曲线的数据。
精确度通常与召回率一起考虑。我们可以使用
sklearn.metrics中的precision_recall_curve来自动调整阈值,并计算每个阈值下的精确度和召回率对。以下是提取这些值的代码,类似于roc_curve:precision, recall, thresh_3 = metrics.precision_recall_curve\ (y_test_2,\ y_test_2_pred_proba[:,1]) ```py -
使用 matplotlib 绘制精确度-召回率曲线:我们可以通过以下代码实现这一点。
注意,我们将召回率放在
x轴,将精确度放在y轴,并将坐标轴的限制设置为[0, 1]范围:plt.plot(recall, precision, '-x') plt.xlabel('Recall') plt.ylabel('Precision') plt.title('Precision and recall for the logistic'\ 'regression 'with LIMIT_BAL') plt.xlim([0, 1]) plt.ylim([0, 1]) ```py  图 2.31:精确度-召回率曲线图 -
使用 scikit-learn 计算精确度-召回率曲线下的面积。
以下是实现此操作的代码:
metrics.auc(recall, precision) ```py 你将获得以下输出:0.31566964427378624
我们看到,精确率-召回率曲线表明,该模型的精确率通常相对较低;在几乎所有阈值范围内,精确率(即正确的正类分类所占比例)都不到一半。我们可以通过计算精确率-召回率曲线下面积来比较这个分类器与我们可能考虑的其他模型或特征集。 Scikit-learn 提供了一个计算任何`x-y`数据 AUC 的功能,使用的是梯形规则,你可能还记得这个方法来自微积分:`metrics.auc`。我们使用这个功能来获取精确率-召回率曲线下面积。 -
现在重新计算 ROC AUC,不过这次要计算训练数据的 ROC AUC。这在概念上和定量上与之前的计算有何不同?
首先,我们需要使用训练数据而不是测试数据来计算预测概率。然后,我们可以使用训练数据标签来计算 ROC AUC。以下是代码:
y_train_2_pred_proba = example_lr.predict_proba(X_train_2) metrics.roc_auc_score(y_train_2, y_train_2_pred_proba[:,1]) ```py 你应该得到以下输出:0.6182918113358344
定量来看,我们可以看到这个 AUC 与我们之前计算的测试数据 ROC AUC 差别不大。两者都大约是 0.62。概念上,这有什么不同?当我们在训练数据上计算这个指标时,我们衡量的是模型在预测“教会”模型如何进行预测的相同数据上的能力。我们看到的是模型如何拟合数据。另一方面,测试数据的指标表示模型在未见过的外部样本数据上的表现。如果这些得分差异很大,通常表现为训练得分高于测试得分,那就意味着虽然模型很好地拟合了数据,但训练好的模型无法很好地泛化到新的、未见过的数据。
在这种情况下,训练得分和测试得分相似,这意味着模型在训练数据和未见过的数据(外部样本数据)上的表现差不多。我们将在第四章,偏差-方差权衡中学习更多关于通过比较训练和测试得分可以获得的见解。
3. 逻辑回归和特征探索的详细信息
活动 3.01:拟合逻辑回归模型并直接使用系数
解答:
前几个步骤与我们在之前的活动中做的类似:
-
创建一个训练/测试数据集(80/20),以
PAY_1和LIMIT_BAL作为特征:from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( df[['PAY_1', 'LIMIT_BAL']].values, df['default payment next month'].values, test_size=0.2, random_state=24) ```py -
导入
LogisticRegression,使用默认选项,但将求解器设置为'liblinear':from sklearn.linear_model import LogisticRegression lr_model = LogisticRegression(solver='liblinear') ```py -
在训练数据上进行训练,并使用测试数据获取预测类别以及类别概率:
lr_model.fit(X_train, y_train) y_pred = lr_model.predict(X_test) y_pred_proba = lr_model.predict_proba(X_test) ```py -
从训练好的模型中提取系数和截距,并手动计算预测概率。你需要在特征中添加一列值为 1 的列,以便与截距相乘。
首先,让我们创建特征数组,添加一列 1 值,使用水平堆叠:
ones_and_features = np.hstack\ ([np.ones((X_test.shape[0],1)), X_test]) ```py 现在我们需要截距和系数,我们将从 scikit-learn 输出中重新调整形状并连接它们:intercept_and_coefs = np.concatenate
([lr_model.intercept_.reshape(1,1),
lr_model.coef_], axis=1)为了反复将截距和系数乘以 `ones_and_features` 的所有行,并求每行的和(也就是求线性组合),你可以使用乘法和加法将这些全部写出来。不过,使用点积会更快:X_lin_comb = np.dot(intercept_and_coefs,
np.transpose(ones_and_features))现在 `X_lin_comb` 包含了我们需要传递给我们定义的 sigmoid 函数的参数,以计算预测概率:y_pred_proba_manual = sigmoid(X_lin_comb)
-
使用
0.5的阈值,手动计算预测的类别。与 scikit-learn 输出的类别预测进行比较。手动预测的概率
y_pred_proba_manual应该与y_pred_proba相同,我们马上检查这一点。首先,使用阈值手动预测类别:y_pred_manual = y_pred_proba_manual >= 0.5 ```py 这个数组的形状将与 `y_pred` 不同,但它应该包含相同的值。我们可以像这样检查两个数组的所有元素是否相等:np.array_equal(y_pred.reshape(1,-1), y_pred_manual)
如果数组相等,这应该返回一个逻辑值`True`。 -
使用 scikit-learn 的预测概率和手动预测的概率计算 ROC AUC,并进行比较。
首先,导入以下内容:
from sklearn.metrics import roc_auc_score ```py 然后,在两个版本上计算此指标,确保访问正确的列,或根据需要调整形状: 
图 3.37:从预测概率计算 ROC AUC
实际上,AUC 是相同的。我们在这里做了什么?我们已经确认,从这个拟合的 scikit-learn 模型中,我们实际上只需要三个数字:截距和两个系数。一旦我们得到了这些,就可以使用几行代码,借助数学函数,来创建模型预测,这与直接从 scikit-learn 生成的预测是等效的。
这有助于确认你对知识的理解,但除此之外,你为什么要这么做呢?我们将在最后一章讨论模型部署。不过,根据你的具体情况,你可能会遇到一种情形,在其中你没有 Python 环境来为模型输入新的特征进行预测。例如,你可能需要完全在 SQL 中进行预测。虽然这在一般情况下是一个限制,但使用逻辑回归时,你可以利用 SQL 中可用的数学函数重新创建逻辑回归预测,只需要将截距和系数粘贴到 SQL 代码的某个地方即可。点积可能不可用,但你可以使用乘法和加法来实现相同的目的。
那么,结果如何呢?我们看到的是,通过稍微提高模型的表现,我们可以超过之前的尝试:在上一章的活动中,仅使用 LIMIT_BAL 作为特征时,ROC AUC 稍低为 0.62,而此处为 0.63。下一章我们将学习使用逻辑回归的高级技术,进一步提升性能。
4. 偏差-方差权衡
活动 4.01:使用案例研究数据进行交叉验证和特征工程
解答:
-
从案例研究数据的 DataFrame 中选择特征。
你可以使用我们在本章中已经创建的特征名称列表,但请确保不要包括响应变量,它本可以是一个非常好的(但完全不合适的)特征:
features = features_response[:-1] X = df[features].values ```py -
使用随机种子 24 进行训练/测试集拆分:
X_train, X_test, y_train, y_test = \ train_test_split(X, df['default payment next month'].values, test_size=0.2, random_state=24) ```py 我们将在后续中使用这个数据,并将其作为未见测试集进行保留。通过指定随机种子,我们可以轻松创建包含其他建模方法的独立笔记本,使用相同的训练数据。 -
实例化
MinMaxScaler来缩放数据,如下所示的代码:from sklearn.preprocessing import MinMaxScaler min_max_sc = MinMaxScaler() ```py -
实例化一个逻辑回归模型,使用
saga求解器,L1 惩罚,并将max_iter设置为1000,因为我们希望允许求解器有足够的迭代次数来找到一个好的解:lr = LogisticRegression(solver='saga', penalty='l1', max_iter=1000) ```py -
导入
Pipeline类并创建一个包含缩放器和逻辑回归模型的管道,分别使用'scaler'和'model'作为步骤的名称:from sklearn.pipeline import Pipeline scale_lr_pipeline = Pipeline( steps=[('scaler', min_max_sc), ('model', lr)]) ```py -
使用
get_params和set_params方法查看每个阶段的参数,并更改它们(在你的笔记本中分别执行以下每行代码并观察输出):scale_lr_pipeline.get_params() scale_lr_pipeline.get_params()['model__C'] scale_lr_pipeline.set_params(model__C = 2) ```py -
创建一个较小范围的 C 值进行交叉验证测试,因为这些模型相比我们之前的练习,在训练和测试时需要更多的数据,因此训练时间会更长;我们推荐的 C = [102, 10, 1, 10-1, 10-2, 10-3]:
C_val_exponents = np.linspace(2,-3,6) C_vals = np.float(10)**C_val_exponents ```py -
创建
cross_val_C_search函数的新版本,命名为cross_val_C_search_pipe。这个函数将接受一个pipeline参数,而不是model参数。函数内的更改是使用set_params(model__C = <你想测试的值>)设置 C 值,替换模型为管道,并在fit和predict_proba方法中使用管道,且通过pipeline.get_params()['model__C']获取 C 值以打印状态更新。更改如下:
def cross_val_C_search_pipe(k_folds, C_vals, pipeline, X, Y): ##[…] pipeline.set_params(model__C = C_vals[c_val_counter]) ##[…] pipeline.fit(X_cv_train, y_cv_train) ##[…] y_cv_train_predict_proba = pipeline.predict_proba(X_cv_train) ##[…] y_cv_test_predict_proba = pipeline.predict_proba(X_cv_test) ##[…] print('Done with C = {}'.format(pipeline.get_params()\ ['model__C'])) ```py 注意 有关完整的代码,请参阅 [`packt.link/AsQmK`](https://packt.link/AsQmK)。 -
按照上一个练习的方式运行这个函数,但使用新的C值范围、你创建的管道以及从案例研究数据训练集分割得到的特征和响应变量。你可能会在此或后续步骤中看到关于求解器未收敛的警告;你可以尝试调整
tol或max_iter选项以尝试实现收敛,尽管使用max_iter = 1000时得到的结果应该是足够的。以下是执行此操作的代码:cv_train_roc_auc, cv_test_roc_auc, cv_test_roc = \ cross_val_C_search_pipe(k_folds, C_vals, scale_lr_pipeline, X_train, y_train) ```py 你将获得以下输出:Done with C = 100.0
Done with C = 10.0
Done with C = 1.0
Done with C = 0.1
Done with C = 0.01
Done with C = 0.001 -
使用以下代码绘制每个C值下,跨折叠的平均训练和测试 ROC AUC:
plt.plot(C_val_exponents, np.mean(cv_train_roc_auc, axis=0), '-o', label='Average training score') plt.plot(C_val_exponents, np.mean(cv_test_roc_auc, axis=0), '-x', label='Average testing score') plt.ylabel('ROC AUC') plt.xlabel('log$_{10}$(C)') plt.legend() plt.title('Cross-validation on Case Study problem') ```py 你将获得以下输出:  图 4.25:交叉验证测试性能 你应该注意到,正则化在这里并没有带来太多好处,正如预期的那样:对于较低的*C*值,即较强的正则化,模型的测试(以及训练)性能下降。虽然通过使用所有可用特征,我们能够提高模型的性能,但似乎并没有出现过拟合。相反,训练和测试分数差不多。与其说是过拟合,不如说我们可能存在欠拟合的情况。让我们尝试构建一些交互特征,看看它们是否能提升性能。 -
使用以下代码为案例研究数据创建交互特征,并确认新特征的数量是合理的:
from sklearn.preprocessing import PolynomialFeatures make_interactions = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False) X_interact = make_interactions.fit_transform(X) X_train, X_test, y_train, y_test = train_test_split( X_interact, df['default payment next month'].values, test_size=0.2, random_state=24) print(X_train.shape) print(X_test.shape) ```py 你将获得以下输出:(21331, 153)
(5333, 153)从中你应该看到新特征的数量是 153,计算方式是*17 + "17 选 2" = 17 + 136 = 153*。*“17 选 2”*部分来自于从 17 个原始特征中选择所有可能的两个特征的组合进行交互。 -
重复交叉验证过程,并观察使用交互特征时模型的表现;也就是说,重复步骤 9和步骤 10。请注意,由于特征数量增加,这将需要更多时间,但应该不会超过 10 分钟。
你将获得以下输出:
![图 4.26:通过添加交互特征改善的交叉验证测试性能]()
图 4.26:通过添加交互特征改善的交叉验证测试性能
那么,交互特征是否改善了平均交叉验证测试性能?正则化是否有用?
构造交互特征使得最佳模型测试分数在各折叠之间平均约为ROC AUC = 0.74,而没有交互特征时大约是 0.72。这些分数出现在C = 100时,即几乎没有正则化。在包含交互特征的模型的训练与测试分数对比图上,你可以看到训练分数稍高于测试分数,因此可以说存在一定程度的过拟合。然而,在这里我们无法通过正则化来提高测试分数,因此这可能不是过拟合的一个问题实例。在大多数情况下,任何能够产生最高测试分数的策略就是最佳策略。
总结来说,添加交互特征提高了交叉验证的表现,而正则化在目前使用逻辑回归模型的案例中似乎并不有用。我们将在稍后进行全量训练数据拟合的步骤,当我们尝试其他模型并在交叉验证中找到最佳模型时再进行。
5. 决策树与随机森林
活动 5.01:使用随机森林进行交叉验证网格搜索
解决方案:
-
创建一个字典,表示将要搜索的
max_depth和n_estimators超参数的网格。包括深度为 3、6、9 和 12,以及树的数量为 10、50、100 和 200。保持其他超参数为默认值。使用以下代码创建字典:rf_params = {'max_depth':[3, 6, 9, 12], 'n_estimators':[10, 50, 100, 200]} ```py 注意 还有许多其他可能的超参数需要搜索。特别是,scikit-learn 文档中关于随机森林指出:“使用这些方法时,主要调整的参数是`n_estimators`和`max_features`”,并且“经验上良好的默认值是……分类任务时,`max_features=sqrt(n_features)`。” 来源:https://scikit-learn.org/stable/modules/ensemble.html#parameters 出于本书的目的,我们将使用`max_features='auto'`(等同于`sqrt(n_features)`)并将探索限制在`max_depth`和`n_estimators`上,以缩短运行时间。在实际情况中,你应根据可承受的计算时间探索其他超参数。记住,为了在特别大的参数空间中搜索,你可以使用`RandomizedSearchCV`,以避免为网格中每个超参数组合计算所有指标。 -
使用我们在本章之前使用的相同选项实例化一个
GridSearchCV对象,但使用步骤 1 中创建的超参数字典。设置verbose=2以查看每次拟合的输出。你可以重用我们一直在使用的相同随机森林模型对象rf,或者创建一个新的。创建一个新的随机森林对象,并使用以下代码实例化GridSearchCV类:rf = RandomForestClassifier(n_estimators=10,\ criterion='gini',\ max_depth=3,\ min_samples_split=2,\ min_samples_leaf=1,\ min_weight_fraction_leaf=0.0,\ max_features='auto',\ max_leaf_nodes=None,\ min_impurity_decrease=0.0,\ min_impurity_split=None,\ bootstrap=True,\ oob_score=False,\ n_jobs=None, random_state=4,\ verbose=0,\ warm_start=False,\ class_weight=None) cv_rf = GridSearchCV(rf, param_grid=rf_params,\ scoring='roc_auc',\ n_jobs=-1,\ refit=True,\ cv=4,\ verbose=2,\ error_score=np.nan,\ return_train_score=True) ```py -
在训练数据上拟合
GridSearchCV对象。使用以下代码执行网格搜索:cv_rf.fit(X_train, y_train) ```py 由于我们选择了`verbose=2`选项,你将看到笔记本中相对较多的输出。每个超参数组合都会有输出,并且对于每个折叠,都有拟合和测试的输出。以下是输出的前几行:  图 5.22:交叉验证的冗长输出 虽然在较短的交叉验证过程中不必查看所有这些输出,但对于较长的交叉验证,看到这些输出可以让你确认交叉验证正在进行,并且能给你一些关于不同超参数组合下拟合所需时间的概念。如果某些操作花费的时间过长,你可能需要通过点击笔记本顶部的停止按钮(方形)中断内核,并选择一些运行时间更短的超参数,或者使用一个更有限的超参数集。 当这一切完成时,你应该会看到以下输出:  图 5.23:交叉验证完成后的输出 这个交叉验证任务大约运行了 2 分钟。随着任务规模的增大,你可能希望通过`n_jobs`参数探索并行处理,以查看是否能够加速搜索。使用`n_jobs=-1`进行并行处理时,运行时间应该比串行处理更短。然而,在并行处理中,你将无法看到每个单独模型拟合操作的输出,如*图 5.23*所示。 -
将网格搜索结果放入 pandas DataFrame 中。使用以下代码将结果放入 DataFrame:
cv_rf_results_df = pd.DataFrame(cv_rf.cv_results_) ```py -
创建一个
pcolormesh可视化图,显示每个超参数组合的平均测试分数。以下是创建交叉验证结果网格图的代码。它与我们之前创建的示例图类似,但带有特定于此处执行的交叉验证的注释:ax_rf = plt.axes() pcolor_graph = ax_rf.pcolormesh\ (xx_rf, yy_rf,\ cv_rf_results_df['mean_test_score']\ .values.reshape((4,4)), cmap=cm_rf) plt.colorbar(pcolor_graph, label='Average testing ROC AUC') ax_rf.set_aspect('equal') ax_rf.set_xticks([0.5, 1.5, 2.5, 3.5]) ax_rf.set_yticks([0.5, 1.5, 2.5, 3.5]) ax_rf.set_xticklabels\ ([str(tick_label) for tick_label in rf_params['n_estimators']]) ax_rf.set_yticklabels\ ([str(tick_label) for tick_label in rf_params['max_depth']]) ax_rf.set_xlabel('Number of trees') ax_rf.set_ylabel('Maximum depth') ```py 我们与之前示例的主要区别在于,我们不是绘制从 1 到 16 的整数,而是绘制我们通过`cv_rf_results_df['mean_test_score'].values.reshape((4,4))`检索并重塑的平均测试分数。这里的另一个新内容是,我们使用列表推导式基于网格中超参数的数值,创建字符串的列表作为刻度标签。我们从定义的字典中访问这些数值,然后在列表推导式中将它们逐一转换为`str`(字符串)数据类型,例如,`ax_rf.set_xticklabels([str(tick_label) for tick_label in rf_params['n_estimators']])`。我们已经使用`set_xticks`将刻度位置设置为我们希望显示刻度的位置。此外,我们使用`ax_rf.set_aspect('equal')`创建了一个正方形图形。图形应该如下所示:  图 5.24:在两个超参数的网格上进行随机森林交叉验证的结果 -
确定使用哪一组超参数。
从我们的网格搜索中可以得出什么结论?显然,使用深度大于 3 的树有一定的优势。在我们尝试的参数组合中,
max_depth=9且树木数量为 200 的组合提供了最佳的平均测试得分,你可以在数据框中查找并确认其 ROC AUC = 0.776。这是我们迄今为止所有努力中找到的最佳模型。
在实际场景中,我们可能会进行更彻底的搜索。接下来的好步骤是尝试更多的树木数量,并且不再花费时间在
n_estimators< 200 上,因为我们知道至少需要 200 棵树才能获得最佳性能。你还可以更精细地搜索max_depth的空间,而不是像我们这里一样按 3 递增,并尝试其他超参数,比如max_features。不过为了我们的目的,我们将假设已经找到了最佳超参数并继续前进。
6. 梯度提升、XGBoost 和 SHAP 值
活动 6.01:使用 XGBoost 建模案例研究数据并通过 SHAP 解释模型
解决方案:
在本次活动中,我们将结合本章学习的内容,使用合成数据集并将其应用到案例研究数据中。我们将观察 XGBoost 模型在验证集上的表现,并使用 SHAP 值解释模型预测。我们已经通过替换之前忽略的PAY_1特征缺失值的样本来准备该数据集,同时保留了没有缺失值的样本的训练/测试分割。你可以在本活动的笔记本附录中查看数据是如何准备的。
-
加载为本次练习准备的案例研究数据。文件路径为
../../Data/Activity_6_01_data.pkl,变量包括:features_response, X_train_all, y_train_all, X_test_all, y_test_all:with open('../../Data/Activity_6_01_data.pkl', 'rb') as f: features_response, X_train_all, y_train_all, X_test_all,\ y_test_all = pickle.load(f) ```py -
定义一个验证集,用于训练 XGBoost 并进行提前停止:
from sklearn.model_selection import train_test_split X_train_2, X_val_2, y_train_2, y_val_2 = \ train_test_split(X_train_all, y_train_all,\ test_size=0.2, random_state=24) ```py -
实例化一个 XGBoost 模型。我们将使用
lossguide增长策略,并检查不同max_leaves值下的验证集表现:xgb_model_4 = xgb.XGBClassifier( n_estimators=1000, max_depth=0, learning_rate=0.1, verbosity=1, objective='binary:logistic', use_label_encoder=False, n_jobs=-1, tree_method='hist', grow_policy='lossguide') ```py -
搜索
max_leaves的值,从 5 到 200,步长为 5:max_leaves_values = list(range(5,205,5)) ```py -
创建用于提前停止的评估集:
eval_set_2 = [(X_train_2, y_train_2), (X_val_2, y_val_2)] ```py -
遍历超参数值,并创建一个验证集的 ROC AUC 列表,采用与练习 6.01:随机化网格搜索调优 XGBoost 超参数相同的方法:
%%time val_aucs = [] for max_leaves in max_leaves_values: #Set parameter and fit model xgb_model_4.set_params(**{'max_leaves':max_leaves}) xgb_model_4.fit(X_train_2, y_train_2,\ eval_set=eval_set_2,\ eval_metric='auc',\ verbose=False,\ early_stopping_rounds=30) #Get validation score val_set_pred_proba = xgb_model_4.predict_proba(X_val_2)[:,1] val_aucs.append(roc_auc_score(y_val_2, val_set_pred_proba)) ```py -
创建一个数据框,记录超参数搜索结果,并绘制验证 AUC 与
max_leaves的关系:max_leaves_df_2 = \ pd.DataFrame({'Max leaves':max_leaves_values,\ 'Validation AUC':val_aucs}) mpl.rcParams['figure.dpi'] = 400 max_leaves_df_2.set_index('Max leaves').plot() ```py 图表应该看起来像这样:  图 6.15:案例研究数据中验证 AUC 与`max_leaves`的关系 尽管关系有些噪声,我们可以看到,通常较低的`max_leaves`值会导致较高的验证集 ROC AUC。这是因为通过允许较少的叶子来限制树的复杂度,从而减少了过拟合,增加了验证集得分。 -
观察
max_leaves对应于验证集上最高 ROC AUC 的数量:max_auc_2 = max_leaves_df_2['Validation AUC'].max() max_auc_2 max_ix_2 = max_leaves_df_2['Validation AUC'] == max_auc_2 max_leaves_df_2[max_ix_2] ```py 结果应如下所示:  图 6.16:案例研究数据的最佳`max_leaves`和验证集 AUC 我们希望结合之前在建模案例数据方面的努力来解释这些结果。这不是一个完美的对比,因为这里我们在训练和验证数据中有缺失值,而之前我们忽略了这些缺失值,并且这里只有一个验证集,而不像之前使用的 k 折交叉验证(尽管有兴趣的读者可以尝试在 XGBoost 中使用 k 折交叉验证进行多个训练/验证分割,并加上早停)。 然而,即使考虑到这些限制,下面的验证结果应提供一个类似于我们之前进行的 k 折交叉验证的样本外表现度量。我们注意到,这里的验证 ROC AUC 值为 0.779,比之前在*活动 5.01*中使用随机森林获得的 0.776 略高,*随机森林的交叉验证网格搜索*,出自*第五章,决策树与随机森林*。这些验证分数相当接近,实际上使用这两种模型都应该是可以的。接下来我们将继续使用 XGBoost 模型。 -
使用最佳超参数重新拟合 XGBoost 模型:
xgb_model_4.set_params(**{'max_leaves':40}) xgb_model_4.fit(X_train_2, y_train_2, eval_set=eval_set_2, eval_metric='auc', verbose=False, early_stopping_rounds=30) ```py -
为了能够检查验证集的 SHAP 值,我们需要将此数据做成一个数据框:
X_val_2_df = pd.DataFrame(data=X_val_2, columns=features_response[:-1]) ```py -
创建一个 SHAP 解释器,使用验证数据作为背景数据集,获取 SHAP 值,并制作一个摘要图:
explainer_2 = shap.explainers.Tree(xgb_model_4, data=X_val_2_df) shap_values_2 = explainer_2(X_val_2_df) mpl.rcParams['figure.dpi'] = 75 shap.summary_plot(shap_values_2.values, X_val_2_df) ```py 图应如下所示:  图 6.17:案例研究数据在验证集上的 XGBoost 模型 SHAP 值 从*图 6.17*中,我们可以看到,XGBoost 模型中最重要的特征与我们在*第五章,决策树与随机森林*中探索的随机森林模型中的特征略有不同(参见*图 5.15*)。`PAY_1`不再是最重要的特征,尽管它仍然在第三位,仍然很重要。现在,`LIMIT_BAL`(借款人的信用额度)是最重要的特征。这个结果是合理的,因为贷方可能根据借款人的风险来设定信用额度,因此它应该是一个很好的违约风险预测因子。 让我们探索`LIMIT_BAL`是否与其他特征有任何有趣的 SHAP 交互。我们可以通过不为颜色参数指定特征,允许`shap`包自动选择与其他特征交互最强的特征,从而绘制散点图。 -
绘制
LIMIT_BAL的 SHAP 值散点图,按最强交互特征进行着色:shap.plots.scatter(shap_values_2[:,'LIMIT_BAL'], color=shap_values_2) ```py 该图应如下所示:  图 6.18:`LIMIT_BAL`和与之有最强交互特征的 SHAP 值散点图 `BILL_AMT2`,即两个月前的账单金额,和`LIMIT_BAL`的交互最强。我们可以看到,对于大多数`LIMIT_BAL`的值,如果账单特别高,这会导致更高的 SHAP 值,意味着违约风险增加。通过观察*图 6.18*中最红的点分布,可以发现这些点集中在点带的顶部。这符合直觉:即使借款人获得了较大的信用额度,如果账单金额过大,也可能意味着违约风险增加。 最后,我们将保存模型以及训练和测试数据以供分析,并交付给我们的商业伙伴。我们通过使用 Python 的`pickle`功能来完成此操作。 -
将训练好的模型以及训练和测试数据保存到文件中:
with open('../Data/xgb_model_w_data.pkl', 'wb') as f: pickle.dump([X_train_all, y_train_all,\ X_test_all, y_test_all,\ xgb_model_4], f) ```py
7. 测试集分析、财务洞察和交付给客户
活动 7.01:推导财务洞察
解决方案:
-
使用测试集,计算如果没有辅导程序的所有违约成本。
使用此代码进行计算:
cost_of_defaults = np.sum(y_test_all * X_test_all[:,5]) cost_of_defaults ```py 输出应为:60587763.0
-
计算辅导项目可以降低违约成本的百分比。
违约成本的潜在降低是辅导项目可能带来的最大净节省量,除以在没有辅导项目情况下所有违约的成本:
net_savings[max_savings_ix]/cost_of_defaults ```py 输出应为:0.2214260658542551
结果表明,使用辅导程序可以将违约成本降低 22%,这一结果是通过预测建模得出的。 -
计算在最优阈值下每个账户的净节省(考虑所有可能进行辅导的账户,也就是相对于整个测试集)。
使用此代码进行计算:
net_savings[max_savings_ix]/len(y_test_all) ```py 输出应如下所示:2259.2977433479286
这样的结果帮助客户根据辅导项目的潜在节省量,推算其可以为服务的所有账户节省的金额。 -
绘制每个阈值下,每个账户的净节省与每个账户辅导成本的关系图。
使用此代码创建图表:
plt.plot(total_cost/len(y_test_all), net_savings/len(y_test_all)) plt.xlabel\ ('Upfront investment: cost of counselings per account (NT$)') plt.ylabel('Net savings per account (NT$)') ```py 生成的图应如下所示:  图 7.14:实现一定节省金额所需的咨询项目初始成本 这表明客户需要在某个月份为咨询项目预算多少资金,以实现一定的节省金额。看起来最大利益可以通过为每个账户预算大约 NT$1300 来实现(你可以使用 `np.argmax` 找到对应于最大净节省的精确预算金额)。然而,在 NT$1000 到 2000 之间,前期投资的净节省相对平稳,超出此范围则较低。客户实际上可能无法为该项目预算这么多资金,但这个图表为他们提供了证据,如果需要的话可以为更大预算进行辩护。 这个结果与我们在上一个练习中的图形相对应。虽然我们显示了最优阈值是 0.36,但对客户而言,使用更高的阈值(高至 0.5)可能是可以接受的,这样就会做出更少的正预测,向更少的账户持有者提供咨询,并且前期项目成本较小。*图 7.14* 显示了这一点在成本和每个账户的净节省方面的体现。 -
在每个阈值下,绘制预测为正的账户比例(这称为“标记率”)。
使用此代码绘制标记率与阈值的关系:
plt.plot(thresholds, n_pos_pred/len(y_test_all)) plt.ylabel('Flag rate') plt.xlabel('Threshold') ```py 图表应如下所示:  图 7.15:信用咨询项目的标记率与阈值关系 这个图表显示了在每个阈值下,将被预测为违约并因此会被推荐接触的账户比例。看起来在最优阈值 0.36 时,只有大约 20% 的账户会被标记为需要咨询。这表明,使用模型优先考虑需要咨询的账户可以帮助集中资源,减少资源浪费。更高的阈值,可能会导致几乎最优的节省,直到约 0.5 的阈值,如 *图 7.12* 所示(*第七章*,*测试集分析、财务洞察及交付给客户*)会导致更低的标记率。 -
使用以下代码为测试数据绘制精度-召回率曲线:
plt.plot(n_true_pos/sum(y_test_all),\ np.divide(n_true_pos, n_pos_pred)) plt.xlabel('Recall') plt.ylabel('Precision') ```py 该图应如下所示:  图 7.16:精度-召回率曲线 *图 7.16* 显示,为了开始获得高于 0 的真实正率(即召回率),我们需要接受约 0.8 或更低的精准度。 精度和召回率与项目的成本和节省直接相关:我们预测越精准,由于模型预测错误而浪费在咨询上的钱就越少。而且,召回率越高,我们就能通过成功识别即将违约的账户创造更多节省。将此步骤中的代码与上一个练习中用于计算成本和节省的代码进行比较,即可看到这一点。 为了查看精度和召回率与定义正负预测的阈值之间的关系,分别绘制它们可能会很有启发性。 -
将精度和召回率分别绘制在y轴上,阈值绘制在x轴上。
使用这个代码来生成图表:
plt.plot(thresholds, np.divide(n_true_pos, n_pos_pred), label='Precision') plt.plot(thresholds, n_true_pos/sum(y_test_all), label='Recall') plt.xlabel('Threshold') plt.legend()图表应如下所示:
![图 7.17:精度和召回率分别绘制与阈值的关系]()
图 7.17:精度和召回率分别绘制与阈值的关系
这个图表揭示了为何最佳阈值为 0.36 的原因。虽然最佳阈值还取决于成本和节省的财务分析,但我们可以看到,精度最初增加的陡峭部分,代表了正向预测的准确性,因此也反映了模型引导的咨询有多具成本效益,发生在大约 0.36 的阈值之前。

嘿!
我是 Stephen Klosterman,本书的作者。我非常希望你喜欢阅读我的书,并且觉得它对你有所帮助。
如果你能在亚马逊上留下关于《使用 Python 进行数据科学项目》第二版的评论,分享你的想法,将对我(和其他潜在读者!)非常有帮助。
请访问链接packt.link/r/1800564481。
或者
扫描二维码留下你的评论。

你的评论将帮助我了解这本书中哪些地方做得好,哪些地方可以改进,以便为未来的版本做出改进,因此我非常感激。
最好的祝愿,
Stephen Klosterman




















































































浙公网安备 33010602011771号