Python-数据科学项目第二版-一-

45 阅读1小时+

Python 数据科学项目第二版(一)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于本书

如果数据是新石油,那么机器学习就是钻井机。随着公司能够获取越来越多的原始数据,能够提供支持商业决策的先进预测模型的能力变得愈发宝贵。

在本书中,你将基于一个现实的 数据集进行从头到尾的项目,内容被分解为易于操作的小练习。这种方式采用了案例研究的方法,模拟了你在实际数据科学项目中可能遇到的工作环境。

你将学习如何使用包括 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)(来自 python.org 或通过 Anaconda 安装,见下文推荐)。在撰写时,用于第六章的 SHAP 库(梯度提升、XGBoost 和 SHAP 值)与 Python 3.9 不兼容。因此,如果你使用的是 Python 3.9 作为基础环境,建议按照下一节的说明设置 Python 3.8 环境。

  • 根据需要安装 Python 库(如 Jupyter、NumPy、Pandas、Matplotlib 等,建议通过 Anaconda 安装,见下文)

安装与设置

在开始本书之前,建议安装 Anaconda 包管理器,并使用它来协调 Python 及其包的安装。

代码包

请查找本书的代码包,托管在 GitHub 上:github.com/PacktPublis…

Anaconda 和环境设置

你可以访问以下链接来安装 Anaconda:www.anaconda.com/products/in…

推荐在 Anaconda 中创建一个环境,以进行本书中的练习和活动,这些活动已在此处指示的软件版本上经过测试。安装 Anaconda 后,如果你使用的是 macOS 或 Linux,请打开终端;如果使用的是 Windows,请打开命令提示符窗口,然后执行以下操作:

  1. 创建一个包含大多数所需包的环境。你可以根据需要命名它;在这里它被命名为 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
    
  2. 当提示时,键入'y'并按[Enter]键。

  3. 激活环境:

    conda activate dspwp2
    
  4. 安装剩余的包:

    conda install -c conda-forge xgboost=1.3.0 shap=0.37.0
    
  5. 当提示时,键入'y'并按[Enter]键。

  6. 你已经可以使用该环境了。完成后,要停用它:

    conda deactivate
    

我们还在 github.com/PacktPublis… 上提供了来自我们丰富书籍和视频目录的其他代码包。快去看看吧!

约定

文章中的代码、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 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/err… 并填写表格。

copyright@packt.com,并附上材料链接。

如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有兴趣写作或为书籍贡献内容,请访问 authors.packtpub.com。

请留下评论

请通过在亚马逊上留下详细且公正的评论告诉我们你的想法。我们欢迎所有反馈——它帮助我们继续制作优秀的产品,并帮助有志开发者提升技能。请花几分钟时间提供你的想法——这对我们意义重大。你可以通过点击以下链接留下评论: packt.link/r/180056448…

第一章: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()函数,尽管你不需要关心这个细节。以下截图显示了打印在控制台上的前五个正整数的列表,以及一些索引操作,并将列表的第一个项更改为不同数据类型的新值:

图 1.1:列表创建和索引

](github.com/OpenDocCN/f…)

图 1.1:列表创建和索引

关于图 1.1需要注意几点:对于切片索引和range()函数,区间的端点是开放的,而起始点是闭合的。换句话说,注意当我们指定range()的起始和结束时,端点 6 不包括在结果中,但起始点 1 被包括在内。同样,当用切片[:3]索引列表时,它包括所有索引小于 3 的元素,但不包括索引为 3 的元素。

我们之前提到过有序集合,但 Python 也包括无序集合。其中一个重要的集合类型叫做{},它包含键:值对,通过逗号分隔。以下截图展示了如何创建一个包含水果数量的字典——首先查看苹果的数量,然后添加一种新的水果类型及其数量:

图 1.2:一个字典示例

](tos-cn-i-73owjymdk6/e2215ef7b0504498942e5edc8373add4)

图 1.2:一个字典示例

Python 还有许多其他独特的特点,我们这里只是给你一个大致的概念,不会涉及太多细节。实际上,你可能会使用像pandasnumpy这样的包来处理大多数 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 循环、dictlist。这将确认您已经完成了前言中的安装步骤,并展示 Python 语法和数据结构可能与您熟悉的其他编程语言有所不同。执行以下步骤以完成练习:

注释

在执行本章的练习和活动之前,请确保您已按照前言中提到的设置 Python 环境的说明进行操作。本练习的代码文件可以在此找到:packt.link/N0RPT

  1. 打开终端(如果您使用的是 macOS 或 Linux)或在 Windows 中打开命令提示符窗口。如果您使用的是环境,请使用conda activate <name_of_your_environment>激活它。然后在命令行中键入conda list。您应该看到类似以下内容的输出:图 1.3:从 conda list 中选择包

    图 1.3:从 conda list 中选择包

    你可以看到环境中安装的所有包,包括我们将直接交互的包,以及它们的依赖项,这些依赖项是它们正常运行所必需的。包管理系统的一个主要优势是能够管理包之间的依赖关系。

    注释

    有关 Anaconda 和命令行交互的更多信息,请查看此“备忘单”:docs.conda.io/projects/conda/en/latest/_downloads/843d9e0198f2a193a3484886fa28163c/conda-cheatsheet.pdf

  2. 在终端中输入python,打开命令行 Python 解释器。你应该会得到类似以下的输出:图 1.4:命令行 Python

    图 1.4:命令行 Python

    你应该看到一些关于 Python 版本的信息,以及 Python 命令提示符(>>>)。当你在此提示符后输入时,你正在编写 Python 代码。

    注释

    尽管本书中我们将使用 Jupyter notebook,但本练习的目标之一是通过在命令提示符下编写和运行 Python 程序的基本步骤。

  3. 在命令提示符下编写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/

    现在,我们将回到字典的示例。这里的第一步是创建字典。

  4. 使用以下代码创建一个水果字典(applesorangesbananas):

    example_dict = {'apples':5, 'oranges':8, 'bananas':13}
    
  5. 使用list()函数将字典转换为列表,如下所示的代码片段:

    dict_to_list = list(example_dict)
    dict_to_list
    

    一旦运行前面的代码,你应该会得到以下输出:

    ['apples', 'oranges', 'bananas']
    

    请注意,当这完成后,我们检查内容时,列表中仅捕获了字典的键。如果我们想要获取值,必须使用.values()方法指定。此外,请注意,字典键的列表恰好与我们创建字典时书写的顺序相同。然而,这并不保证,因为字典是无序集合类型。

    使用列表时,你可以通过+运算符将其他列表添加到现有列表中。作为示例,在下一步中,我们将现有的水果列表与只包含一种水果的新列表合并,并覆盖包含原始列表的变量,像这样:list(example_dict.values()); 有兴趣的读者可以自行验证这一点。

  6. 使用+运算符将现有的水果列表与只包含一个水果(pears)的新列表合并:

    dict_to_list = dict_to_list + ['pears']
    dict_to_list
    

    你的输出将如下所示:

    ['apples', 'oranges', 'bananas', 'pears']
    

    sorted()函数可以用于此;它将返回输入的排序版本。在我们的例子中,这意味着水果种类列表将按字母顺序排序。

  7. 使用sorted()函数按字母顺序排序水果列表,如下所示:

    sorted(dict_to_list)
    

    一旦运行前面的代码,你应该会看到以下输出:

    ['apples', 'bananas', 'oranges', 'pears']
    

现在已经足够的 Python 知识了。我们将展示如何执行本书中的代码,所以在过程中你的 Python 知识应该会有所提升。在你打开 Python 解释器时,你可能希望运行图 1.11.2中展示的代码示例。当你使用完解释器后,可以输入quit()退出。

注意

随着你学习的深入,并且不可避免地想尝试新事物,请参考官方的 Python 文档:docs.python.org/3/

数据科学问题的不同类型

作为数据科学家,你的大部分时间可能都会花在数据清理上:弄清楚如何获取数据、获取数据、检查数据、确保数据的正确性和完整性,并将数据与其他类型的数据结合。pandas 是 Python 中广泛使用的数据分析工具,它能帮助你加速数据探索过程,正如我们在本章中所看到的。然而,本书的一个关键目标是帮助你踏上成为机器学习数据科学家的旅程,而这需要你掌握预测建模的艺术和科学。这意味着使用数学模型或理想化的数学公式来学习数据中的关系,希望当新的数据到来时,能够做出准确且有用的预测。

对于预测建模的使用场景,数据通常以表格结构组织,包含特征响应变量。例如,如果你想根据一些关于房子的特征来预测房价,如面积卧室数量,这些特征将被视为特征,而房价则是响应变量。响应变量有时也称为目标变量因变量,而特征有时也称为自变量

如果你有一个包含 1,000 个房屋的数据集,其中包括这些特征的值和房屋的价格,那么你可以说你拥有 1,000 个样本标注数据,其中标签是响应变量的已知值:不同房屋的价格。通常,表格数据结构被组织成不同的行表示不同的样本,而特征和响应占据不同的列,并且还有其他元数据,如样本 ID,如图 1.6所示:

图 1.6:标注数据(房价是已知的目标变量)

图 1.6:标注数据(房价是已知的目标变量)

回归问题

一旦你训练了一个模型,通过使用标注数据学习特征与响应之间的关系,你就可以利用它对那些你不知道价格的房屋进行预测,基于特征中包含的信息。在这种情况下,预测建模的目标是能够做出接近房屋真实价值的预测。由于我们预测的是一个连续尺度上的数值,这被称为回归问题

分类问题

另一方面,如果我们试图对房屋做出定性预测,回答像“这栋房屋在未来 5 年内会出售吗?”或“房主会违约吗?”这样的问题,那么我们将解决一个称为分类问题的问题。在这里,我们希望能够正确地回答是或否的问题。下图是一个示意图,展示了模型训练的工作原理,以及回归或分类模型的可能结果:

图 1.7:回归和分类的模型训练与预测示意图

图 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 笔记本示例

图 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:带有整数行索引在左侧、字符串列索引的 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 找到。

  1. 打开终端(macOS 或 Linux)或命令提示符窗口(Windows),然后输入 jupyter notebook(如果你使用的是 Anaconda 环境,请先激活环境)。

    您将在浏览器中看到 Jupyter 界面。如果浏览器没有自动打开,您可以将终端中的 URL 复制并粘贴到浏览器中。在此界面中,您可以从启动笔记本服务器时所在的目录开始浏览您的文件夹。

  2. 导航到您将存储本书材料的方便位置,然后从 New 菜单创建一个新的 Python 3 笔记本,如下所示:图 1.10:Jupyter 首页

    图 1.10:Jupyter 首页

  3. 在命令模式下(按 Esc 进入命令模式)通过输入 m 来将您的第一个单元格设置为 Markdown 单元格,然后在第一行的开头输入一个井号 #,后面加一个空格,以设置标题。为您的笔记本添加标题。接下来的几行,输入描述。

    下面是一个示例的截图,展示了其他类型的 Markdown 语法,如粗体、斜体,以及如何在 Markdown 单元格中书写代码风格的文本:

    图 1.11:未渲染的 Markdown 单元格

    图 1.11:未渲染的 Markdown 单元格

    请注意,良好的实践是为您的笔记本添加标题和简短的描述,以便读者了解其目的。

  4. Shift + Enter 来渲染 Markdown 单元格。

    这也应该会创建一个新的单元格,它将是一个代码单元格。您可以通过按 m 将其更改为 Markdown 单元格,通过按 y 恢复为代码单元格。您可以通过旁边的 In [ ]: 来判断它是代码单元格。

  5. 在新单元格中输入 import pandas as pd,如下所示的截图所示:图 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

  6. 使用 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 中查看文件时看到的一致?

  7. 使用 .shape 方法查看行列的数量,如以下代码片段所示:

    df.shape
    

    一旦运行该单元格,你将得到以下输出:

    Out[3]: (30000, 25)
    

    这应该与你在电子表格中的观察一致。如果不一致,你就需要查看 pd.read_excel 的各种选项,看看是否需要调整什么。

通过这个练习,我们成功地将数据集加载到 Jupyter 笔记本中。你还可以尝试对 DataFrame 使用 .info().head() 方法,分别查看所有列的信息,并显示 DataFrame 的前几行。现在你已经能够开始使用 pandas 处理数据了。

最后,虽然这可能已经很清楚了,但请注意,如果你在一个代码单元中定义了一个变量,它在笔记本中的其他代码单元也可以使用。这是因为,只要笔记本在运行,笔记本中的代码单元被认为共享作用域,如下面的截图所示:

图 1.13:单元格之间的变量作用域

图 1.13:单元格之间的变量作用域

每次启动 Jupyter 笔记本时,尽管代码和 Markdown 单元格会保存你之前的工作,但环境会重新初始化,你需要重新加载所有模块和数据才能继续工作。你也可以使用笔记本中的内核菜单手动关闭或重启笔记本。关于 Jupyter 笔记本的更多细节可以在这里找到:jupyter-notebook.readthedocs.io/en/stable/

在本书中,每个新的练习和活动都会在一个新的 Jupyter 笔记本中完成。然而,一些练习笔记本也包含在练习前的部分中展示的额外 Python 代码和输出。还有一些参考笔记本包含了每个章节的全部内容。例如,第一章《数据探索与清洗》的笔记本可以在这里找到:packt.link/zwofX

熟悉数据并进行数据清洗

现在让我们初步查看一下这些数据。在你的数据科学家工作中,你可能会遇到几种收到这样的数据集的情况。包括以下几种:

  1. 你创建了生成数据的 SQL 查询。

  2. 一位同事根据你的意见为你写了一个 SQL 查询。

  3. 一位了解数据的同事把它交给了你,但没有征求你的意见。

  4. 你得到一个对数据了解不多的数据集。

在第 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. 数据中有多少行(样本)?

  3. 有哪些特征?哪些是类别型的,哪些是数值型的?

    类别特征的值属于离散的类别,例如“是”、“否”或“也许”。

    数值特征通常是连续的数值尺度,比如美元金额。

  4. 这些特征中的数据看起来如何?

    为了查看这一点,你可以检查数值特征的值范围,或类别特征中不同类别的频率,例如。

  5. 有没有缺失的数据?

我们在上一节已经回答了问题 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

  1. 导入 pandas,加载数据,并运行以下命令检查列名,使用Shift + Enter

    import pandas as pd
    df = pd.read_excel('../Data/default_of_credit_card'\
                       '_clients__courseware_version_1_21_19.xls')
    df.columns
    

    DataFrame 的.columns方法用于检查所有列名。运行单元格后,你将得到以下输出:

    图 1.14:数据集的列

    图 1.14:数据集的列

    如可以观察到,所有列名都列出了。账户 ID 列被标记为 ID。其余列似乎是我们的特征,最后一列是响应变量。让我们快速回顾一下客户提供的数据集信息:

    LIMIT_BAL: 提供的信用额度(以新台币为单位),包括个人消费信用和家庭(附加)信用。

    SEX: 性别 (1 = 男性;2 = 女性)。

    注意

    出于伦理考虑,我们不会使用性别数据来决定信用评级。

    EDUCATION: 教育水平 (1 = 研究生; 2 = 大学; 3 = 高中; 4 = 其他)。

    MARRIAGE: 婚姻状况 (1 = 已婚;2 = 单身;3 = 其他)。

    AGE: 年龄(岁)。

    PAY_1PAY_6: 过去付款记录。记录从 4 月到 9 月的每月付款,这些数据存储在这些列中。

    PAY_1 代表 9 月的还款状态;PAY_2 是 8 月的还款状态;依此类推,直到 PAY_6,代表 4 月的还款状态。

    还款状态的测量尺度如下:-1 = 按时支付;1 = 延迟支付 1 个月;2 = 延迟支付 2 个月;依此类推,直到 8 = 延迟支付 8 个月;9 = 延迟支付 9 个月及以上。

    BILL_AMT1BILL_AMT6: 账单金额(新台币)。

    BILL_AMT1 代表 9 月的账单金额;BILL_AMT2 代表 8 月的账单金额;依此类推,直到 BILL_AMT6,代表 4 月的账单金额。

    PAY_AMT1PAY_AMT6: 之前的付款金额(新台币)。PAY_AMT1 代表 9 月的支付金额;PAY_AMT2 代表 8 月的支付金额;依此类推,直到 PAY_AMT6,代表 4 月的支付金额。

    接下来,我们在下一步中使用 .head() 方法查看数据的前几行。默认情况下,这将返回前 5 行数据。

  2. 在随后的单元格中运行以下命令:

    df.head()
    

    这是你应该看到的输出的一部分:

    图 1.15:DataFrame 的 .head() 方法

    图 1.15:DataFrame 的 .head() 方法

    ID 列似乎包含唯一标识符。现在,为了验证它们是否确实在整个数据集中是唯一的,我们可以使用 .nunique() 方法计算 ID 列(即 Series)的唯一值数量。我们首先使用方括号选择该列。

  3. 选择列(ID)并使用以下命令计算唯一值:

    df['ID'].nunique()
    

    以下是输出结果:

    29687
    

    从前面的输出可以看出,唯一条目的数量是 29,687

  4. 运行以下命令以获取数据集中的行数:

    df.shape 
    

    如下输出所示,数据集的总行数为 30,000

    (30000, 25)
    

    我们看到这里的唯一 ID 数少于行数。这意味着 ID 不是数据行的唯一标识符。所以我们知道 ID 有重复。那么重复的程度如何?某个 ID 是否重复多次?有多少个 ID 是重复的?

    我们可以在 ID Series 上使用.value_counts()方法来开始回答这些问题。这类似于一个id_counts变量。

  5. 将值计数存储在定义为id_counts的变量中,然后使用.head()方法显示存储的值,如下所示:

    id_counts = df['ID'].value_counts()
    id_counts.head()
    

    你将获得以下输出:

    图 1.16:获取账户 ID 的值计数

    图 1.16:获取账户 ID 的值计数

    请注意,.head()默认返回前五行。你可以通过在括号()中传入所需的数字来指定显示的条目数。

  6. 通过运行另一个值计数来显示重复条目的数量:

    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)

接下来,我们使用rgintegers方法生成 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:使用布尔掩码索引数组

图 1.18:使用布尔掩码索引数组

现在你已经掌握了布尔数组的基础知识,它在许多情况下都非常有用。特别是,你可以使用 DataFrame 的 .loc 方法,通过布尔掩码对行进行索引,通过标签对列进行索引,从而获取满足条件的不同列中的值。让我们继续用这些技能探索案例研究数据。

注意

包含前一节中展示的代码和相应输出的 Jupyter notebook 可以在此找到:packt.link/pT9gT

练习 1.04:继续验证数据完整性

在本次练习中,利用我们对布尔数组的了解,我们将检查一些我们发现的重复 ID。在 练习 03验证基本数据完整性 中,我们学到没有 ID 出现超过两次。我们可以利用这一点来定位重复的 ID 并进行检查。然后我们采取措施从数据集中删除质量可疑的行。按照以下步骤完成本次练习:

注意

本次练习的 Jupyter notebook 可以在这里找到:packt.link/snAP0

  1. 继续我们在 练习 1.03验证基本数据完整性 中的内容,我们需要获取 id_counts Series 中计数为 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 系列的索引中。我们可以访问该索引,以便使用逻辑掩码进行选择。

  2. 使用以下命令访问 id_count 的索引,并显示前五行作为上下文:

    id_counts.index[0:5]
    

    这样,你将获得以下输出:

    图 1.20:重复的 ID

    图 1.20:重复的 ID

  3. 使用以下命令选择并将重复的 ID 存储到名为 dupe_ids 的新变量中:

    dupe_ids = id_counts.index[dupe_mask]
    
  4. dupe_ids 转换为列表,然后使用以下命令获取该列表的长度:

    dupe_ids = list(dupe_ids)
    len(dupe_ids)
    

    你应该获得以下输出:

    313
    

    我们将 dupe_ids 变量更改为 list 类型,因为在未来的步骤中我们将需要它以这种形式。该列表的长度为 313,如前面的输出所示,这与我们通过值计数了解到的重复 ID 数量一致。

  5. 我们通过使用以下命令显示前五个条目来验证 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 的所有列。

  6. 在你的笔记本中运行以下命令,以执行我们在上一步中制定的计划:

    df.loc[df['ID'].isin(dupe_ids[0:3]),:]
    

    图 1.22:检查重复 ID 的数据

    图 1.22:检查重复 ID 的数据

    我们在这里观察到,每个重复的 ID 似乎都有一行看起来像有效数据的行,以及一行完全为零的行。花一点时间想一想,你会如何利用这些信息。

    经过一番反思,应该很明显,你应该删除所有值为零的行。也许这些行是由于 SQL 查询中的错误连接条件生成的数据?无论如何,一行全为零的数据肯定是无效的,因为一个人的年龄为零,信用额度为零等,显然没有意义。

    解决这个问题的一种方法是找到所有列为零的行,除了第一列(包含 ID)。这些行无论如何都是无效数据,可能如果我们删除这些行,就能解决重复 ID 的问题。我们可以通过创建一个与整个 DataFrame 大小相同的布尔矩阵,基于“是否等于零”这一条件,来找到 DataFrame 中等于零的条目。

  7. 使用==创建一个与整个 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 外,所有值都是零。

  8. 创建布尔系列feature_zero_mask,如以下代码所示:

    feature_zero_mask = df_zero_mask.iloc[:,1:].all(axis=1)
    
  9. 使用以下命令计算布尔系列的总和:

    sum(feature_zero_mask)
    

    你应该获得以下输出:

    315
    

    上面的输出告诉我们,315 行除了第一列外每列都是零。这比重复 ID 的数量(313)还多,因此如果我们删除所有“零行”,可能就能解决重复 ID 的问题。

  10. 使用以下代码清理 DataFrame,删除所有除了 ID 之外的值全为零的行:

    df_clean_1 = df.loc[~feature_zero_mask,:].copy()
    

    在前面的清理操作中,我们返回了一个新的 DataFrame,名为 df_clean_1。请注意,在这里我们在 .loc 索引操作后使用了 .copy() 方法来创建该输出的副本,而不是对原始 DataFrame 的视图。你可以把它当作创建一个新的 DataFrame,而不是引用原始的 DataFrame。在 .loc 方法中,我们使用了逻辑非运算符 ~ 来选择所有没有零值的特征和响应变量的行,使用 : 来选择所有列。这是我们希望保留的有效数据。做完这个后,我们现在希望知道剩余的行数是否等于唯一 ID 的数量。

  11. 通过运行以下代码,验证 df_clean_1 的行数和列数:

    df_clean_1.shape
    

    你将得到以下输出:

    (29685, 25)
    
  12. 通过运行以下代码获取唯一 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

  1. 加载上一个练习的结果,并通过使用 .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,这表示它们是 IDPAY_1。我们已经熟悉了 ID;它包含的是字符串,即账户 ID。那么 PAY_1 呢?根据数据字典,我们可以预期它包含的是整数,就像其他所有特征一样。我们来仔细看看这一列。

  2. 使用 .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)。我们来仔细看一下,使用该列的值计数。

  3. 使用 .value_counts() 方法获取 PAY_1 列的值计数:

    df_clean_1['PAY_1'].value_counts()
    

    你应该看到以下输出:

    图 1.25:PAY_1 列的值计数

    图 1.25:PAY_1 列的值计数

    上述输出揭示了两个未记录的值:0 和 -2,并且解释了为什么 pandas 将该列导入为 object 数据类型,而不是我们预期的 int64(整数数据类型):因为该列中存在 'Not available' 字符串,表示缺失数据。在本书后续的章节中,我们会回到这一点,讨论如何处理缺失数据。目前,我们将删除数据集中包含缺失值的行。

  4. 使用 != 操作符(在 Python 中表示“不等于”)创建一个逻辑掩码,查找所有 PAY_1 特征没有缺失数据的行:

    valid_pay_1_mask = df_clean_1['PAY_1'] != 'Not available'
    valid_pay_1_mask[0:5]
    

    通过运行上面的代码,你将得到以下输出:

    图 1.26:创建布尔掩码

    图 1.26:创建布尔掩码

  5. 通过计算掩码的和来检查有多少行没有缺失数据:

    sum(valid_pay_1_mask)
    

    你将获得以下输出:

    26664
    

    我们看到有 26,664 行 PAY_1 列没有 'Not available' 这个值。从值计数中我们可以看到,有 3,021 行有这个值。这样合理吗?从图 1.23中我们知道数据集中有 29,685 条记录(行),29,685 – 3,021 = 26,664,因此这一结果是正确的。

  6. 清理数据,删除缺失值的PAY_1行,如下所示:

    df_clean_2 = df_clean_1.loc[valid_pay_1_mask,:].copy()
    
  7. 使用以下命令获取清理后数据的形状:

    df_clean_2.shape
    

    你将得到以下输出:

    (26664, 25)
    

    删除这些行后,我们检查结果 DataFrame 是否具有预期的形状。你还可以自己检查值计数是否表明所需的值已被删除,方法是:df_clean_2['PAY_1'].value_counts()

    最后,为了使这一列的数据类型与其他列一致,我们将其从通用的object类型转换为int64类型,像所有其他特征一样,使用.astype方法。然后我们选择几列,包括PAY_1,检查数据类型,并确保转换成功。

  8. 运行以下命令,将PAY_1的数据类型从object转换为int64,并使用列表选择多个列,显示PAY_1PAY_2的列元数据:

    df_clean_2['PAY_1'] = df_clean_2['PAY_1'].astype('int64')
    df_clean_2[['PAY_1', 'PAY_2']].info()
    

这是你将得到的输出:

图 1.27:检查已清理列的数据类型

图 1.27:检查已清理列的数据类型

恭喜你,完成了第二次数据清理操作!但是,如果你还记得,在此过程中我们也注意到PAY_1中存在未记录的值-2 和 0。现在,假设我们再次与商业伙伴取得联系,并了解了以下信息:

  • -2 表示账户在该月初余额为零,并且从未使用过信用。

  • -1 表示账户的余额已全部还清。

  • 0 表示至少支付了最低还款额,但并未偿还全部余额(即,存在正余额并转入下个月)。

我们感谢我们的商业伙伴,因为这回答了我们目前的问题。保持良好的沟通和合作关系非常重要,正如你所看到的,这可能决定一个项目的成败。

在你的笔记本中,像这样保存这次练习的进度:

df_clean_2.to_csv('../../Data/df_clean_2.csv', index=False)

数据质量保证与探索

到目前为止,我们通过提出一些基本问题或查看.info()摘要,已经解决了两个数据质量问题。接下来我们来看看前几列的数据。在查看历史账单支付记录之前,我们首先有LIMIT_BAL账户的信用额度,还有SEXEDUCATIONMARRIAGEAGE这些人口统计特征。我们的商业伙伴已经联系了我们,告诉我们性别不应被用来预测信用状况,因为按照他们的标准,这是不道德的。因此我们会在今后的工作中考虑这一点。现在我们将继续检查其余列,并进行必要的更正。

为了进一步探索数据,我们将使用直方图。直方图是一种很好的方法,可以可视化那些连续值的数据,如货币金额和年龄。直方图将相似的值分组到不同的箱子中,并以条形图的方式显示这些箱子中的数据点数量。

为了绘制直方图,我们将开始熟悉 pandas 的图形功能。pandas 依赖于另一个名为matplotlib的库。使用这些工具,我们还将学习如何快速获取 pandas 中数据的统计摘要。

练习 1.06:探索信用额度和人口统计特征

在这个练习中,我们将开始探索数据中的信用额度和年龄特征。我们将可视化它们并获取统计摘要,以检查这些特征中的数据是否合理。然后,我们将查看教育和婚姻等分类特征,看看这些值是否合理,必要时进行修正。LIMIT_BALAGE是数值型特征,意味着它们是在一个连续的尺度上进行测量的。因此,我们将使用直方图来可视化它们。按照以下步骤完成练习:

注意

本练习的 Jupyter 笔记本可以在这里找到:packt.link/PRdtP

  1. 除了 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 = 每英寸点数),以便得到清晰的图像;除非你准备展示这些内容,否则不需要担心最后这部分,因为它可能会使图片在笔记本中变得非常大。

  2. 使用以下代码加载我们上一个练习的进度:

    df_clean_2 = pd.read_csv('../Data/df_clean_2.csv'),
    
  3. 运行df_clean_2[['LIMIT_BAL', 'AGE']].hist(),你应该能看到以下直方图:图 1.28:信用额度和年龄数据的直方图

    图 1.28:信用额度和年龄数据的直方图

    这是这些特征的一个不错的视觉快照。我们可以通过这种方式快速大致地查看所有数据。为了查看均值和中位数(即第 50 百分位数)等统计信息,还有另一个有用的 pandas 函数。

  4. 使用以下命令生成汇总统计的表格报告:

    df_clean_2[['LIMIT_BAL', 'AGE']].describe()
    

    你应该看到以下输出:

    图 1.29:信用额度和年龄数据的统计摘要

    图 1.29:信用额度和年龄数据的统计摘要

    基于直方图和通过.describe()计算的便捷统计数据,其中包括非空值的计数、均值和标准差、最小值、最大值以及四分位数,我们可以做出一些判断。

    LIMIT_BAL(信用额度)看起来是合理的。信用额度的最小值为 10,000。该数据集来自台湾,具体的货币单位(新台币)可能不太熟悉,但直观上,信用额度应该大于零。我们建议你查找与本地货币的兑换汇率并考虑这些信用额度。例如,1 美元大约等于 30 新台币。

    AGE特征看起来也分布得比较合理,且 21 岁以下的人群没有信用账户。

    对于分类特征,查看值计数是有用的,因为唯一值相对较少。

  5. 使用以下代码获取EDUCATION特征的值计数:

    df_clean_2['EDUCATION'].value_counts()
    

    你应该看到以下输出:

    图 1.30:EDUCATION 特征的值计数

    图 1.30:EDUCATION 特征的值计数

    在这里,我们看到未记录的教育水平 0、5 和 6,因为数据字典只描述了教育(1 = 研究生;2 = 大学;3 = 高中;4 = 其他)。我们的业务合作伙伴告诉我们他们不知道其他教育水平。由于它们不太常见,我们将它们归类为其他类别,这似乎是合适的。

  6. 运行此代码将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。

  7. 使用以下代码获取MARRIAGE特征的值计数:

    df_clean_2['MARRIAGE'].value_counts()
    

    你应该获得以下输出:

    图 1.32:原始 MARRIAGE 特征的值计数

    图 1.32:原始 MARRIAGE 特征的值计数

    这里的问题与EDUCATION特征遇到的问题类似;有一个值 0,在数据字典中没有记录:1 = 已婚;2 = 单身;3 = 其他。因此,我们将其归类为其他

  8. 使用以下代码将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:有序特征在线性模型中可能有效,也可能无效

图 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.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

  1. 为类别标签创建一个空列,命名为EDUCATION_CAT。使用以下命令,每一行将包含字符串'none'

    df_clean_2['EDUCATION_CAT'] = 'none'
    
  2. 检查 EDUCATIONEDUCATION_CAT 列的 DataFrame 的前几行:

    df_clean_2[['EDUCATION', 'EDUCATION_CAT']].head(10)
    

    输出应如下所示:

    图 1.36:选择列并查看前 10 行

    图 1.36:选择列并查看前 10 行

    我们需要用适当的字符串填充这个新列。pandas 提供了一个方便的功能,可以将一个 Series 的所有值映射到新的值。这个函数实际上叫 .map,并依赖于一个字典来建立旧值和新值之间的对应关系。我们的目标是将 EDUCATION 中的数字映射到它们所代表的字符串。例如,当 EDUCATION 列的值为 1 时,我们将把 '研究生' 字符串赋值给 EDUCATION_CAT 列,其他教育水平也是如此。

  3. 使用以下代码创建描述教育类别映射的字典:

    cat_mapping = {1: "graduate school",\
                   2: "university",\
                   3: "high school",\
                   4: "others"}
    
  4. 使用 .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,直接通过步骤 34创建新列,而不需要先将新列赋值为 'none'。然而,有时候创建一个初始化为单一值的新列是有用的,因此了解如何做到这一点是值得的。

    现在我们准备进行一热编码。我们可以通过将一个 DataFrame 的 Series 传递给 pandas 的 get_dummies() 函数来实现。该函数得名于一热编码列也被称为虚拟变量。结果将是一个新的 DataFrame,包含与类别变量的级别数相等的列。

  5. 运行此代码以创建 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 一致。

  6. 如下所示,将独热编码后的 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:付款状态特征的摘要统计

图 1.40:付款状态特征的摘要统计

在这里,我们观察到所有这些特征的值范围都是相同的:-2,-1,0,... 8。看起来,数据字典中描述的值为 9,即九个月及以上的付款延迟,从未出现过。

我们已经澄清了所有这些级别的含义,其中一些并不在原始数据字典中。现在让我们再次查看PAY_1value_counts(),现在按我们正在计数的值进行排序,这些值是该 Series 的index

df[pay_feats[0]].value_counts().sort_index()

这应该产生以下输出:

图 1.41:上个月付款状态的值计数

图 1.41:上个月付款状态的值计数

与正整数值相比,大多数值要么是-2,-1,要么是 0,这对应于上个月处于良好状态的帐户:未使用,全额支付,或至少支付了最低还款额。

请注意,由于此变量的其他值的定义(1 = 一个月的付款延迟;2 = 两个月的付款延迟,依此类推),此特征在分类和数值特征之间有点混合。为什么没有信用使用对应于-2 的值,而值为 2 表示 2 个月的延迟付款,依此类推?我们应该意识到,付款状态的数值编码-2,-1 和 0 构成了数据集创建者对如何对某些分类特征进行编码的决定,然后将其与一个真正数值的特征混合在一起:付款延迟的月数(值为 1 及以上)。稍后,我们将考虑这种做法对该特征的预测能力的潜在影响。

现在,我们将继续探索数据。这个数据集足够小,有 18 个这些财务特征和少数其他特征,我们可以负担得起逐个检查每个特征。如果数据集有数千个特征,我们可能会放弃这一点,而是探索df[pay_feats[0]].hist(),以产生这个:

图 1.42:使用默认参数绘制的 PAY_1 直方图

图 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 中的 xlabelylabel 函数为此图表添加轴标签。代码如下:

df[pay_feats[0]].hist(bins=pay_1_bins)
plt.xlabel('PAY_1')
plt.ylabel('Number of accounts')

输出应该如下所示:

图 1.43:改进后的 PAY_1 直方图

图 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:直方图子图的网格

图 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)]

](github.com/OpenDocCN/f…)

图 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_2PAY_3等的正确值。然而,出于实际原因,他们无法在我们需要的时候完成这一回溯分析并将结果纳入我们的项目中。

因此,我们的支付状态数据中只有最新月份是正确的。这意味着,在所有支付状态特征中,只有PAY_1能够代表未来数据,即将用于我们开发的模型进行预测的数据。这是一个关键点:预测模型依赖于获取与其构建时相同类型的数据来进行预测。这意味着我们可以将PAY_1作为模型中的特征,但不能使用PAY_2或来自前几个月的其他支付状态特征。

本章节展示了对数据质量进行彻底检查的重要性。只有通过仔细地梳理数据,我们才发现了这个问题。如果客户能提前告知我们,在数据集收集的那段时间里,他们在报告过程中遇到过问题,并且报告过程在那段时间内并不一致,那就好了。然而,最终建立一个可信的模型是我们的责任,因此我们需要通过这种详细的探索,确保我们相信数据是正确的。我们向客户解释,由于旧的特征不代表模型将在其上进行评分的未来数据(即预测未来几个月的数据),因此无法使用这些旧特征,并要求他们告知我们他们所知的任何进一步的数据问题。目前没有。

活动 1.01:探索数据集中的剩余财务特征

在本活动中,您将以类似于我们检查 PAY_1PAY_2PAY_3 等特征的方式检查剩余的财务特征。为了更好地可视化这些数据,我们将使用一个大家应该熟悉的数学函数:对数。您将使用 pandas 的 apply 方法,这个方法可以将任何函数应用到整个列或 DataFrame。在完成活动后,您应该得到以下一组非零支付对数变换的直方图:

图 1.46:预期的直方图集

](tos-cn-i-73owjymdk6/8dac5e72c2ee4ce1af8bf5ec742491a0)

图 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')
  1. 创建剩余财务特征的特征名称列表。

  2. 使用 .describe() 检查账单金额特征的统计摘要。反思一下你看到的内容。这合理吗?

  3. 使用 2x3 网格的直方图绘制账单金额特征。

    提示:您可以为此可视化使用 20 个区间。

  4. 获取支付金额特征的 .describe() 摘要。这合理吗?

  5. 绘制类似于账单金额特征的账单支付特征的直方图,但也使用 xrot 关键字参数对 x 轴标签进行旋转,以避免重叠。在任何绘图函数中,您都可以使用 xrot=<角度> 关键字参数,将 x 轴标签按给定角度(以度为单位)旋转。考虑一下结果。

  6. 使用布尔掩码查看有多少支付金额数据正好等于 0。考虑一下在前一步的直方图中,这是否合理?

  7. 使用您在前一步创建的掩码忽略 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中的groupbycount来检查每个类别的样本数量是值得的。如下屏幕截图所示:

图 2.1:响应变量的类别平衡

图 2.1:响应变量的类别平衡

由于目标变量为10,取这一列的均值表示违约账户的比例为 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 个值

图 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:用于进行预测的新特征

图 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 的 scatterplot 函数来创建散点图和线图,来准备好数据。在线性回归部分的练习中,我们将使用 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.4: 使用 NumPy 创建随机、均匀分布的数字

线性回归数据

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

图 2.5:带有高斯噪声的线性方程

图 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 创建散点图,其中 Xy 是前两个参数,s 参数指定点的大小。

这段代码可以用于绘图:

mpl.rcParams['figure.dpi'] = 400
plt.scatter(X,y,s=1)
plt.xlabel('X')
plt.ylabel('y')

执行这些单元格后,你应该在你的笔记本中看到类似这样的内容:

图 2.6:绘制带噪声的线性关系

图 2.6:绘制带噪声的线性关系

看起来像一些带噪声的线性数据,正如我们所希望的那样。现在让我们开始建模。

注意

如果你正在阅读本书的印刷版,可以通过访问以下链接下载并浏览本章某些图像的彩色版本:packt.link/0dbUp

练习 2.01:Scikit-Learn 中的线性回归

在本练习中,我们将使用刚刚生成的合成数据,并使用 scikit-learn 确定最佳拟合线,或线性回归。第一步是从 scikit-learn 导入线性回归模型类并创建一个对象。导入的过程类似于我们之前使用的 LogisticRegression 类。和任何模型类一样,你应该观察所有默认选项。请注意,对于线性回归,指定的选项并不多:你将在本练习中使用默认值。默认设置包括 fit_intercept=True,这意味着回归模型将包括截距项。这是完全合适的,因为我们已经在合成数据中添加了截距。请按照以下步骤完成练习,注意前面部分生成线性回归数据的代码必须先在同一个笔记本中运行(如 GitHub 上所见):

注意

本练习的 Jupyter 笔记本可以在这里找到:packt.link/IaoyM

  1. 执行这段代码以导入线性回归模型类并用所有默认选项实例化它:

    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_,它包含斜率。

  2. 运行这段代码以拟合模型并检查系数:

    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方法的输出放入一个变量中。

  3. 运行这段代码以进行预测:

    y_pred = lin_reg.predict(X.reshape(-1,1))
    

    我们可以将预测结果y_pred与特征X绘制成线图,叠加在特征和响应数据的散点图上,就像我们在图 2.6中所做的那样。在这里,我们添加了plt.plot,它默认会生成线图,用来绘制特征和模型预测的响应值。请注意,在调用plt.plot时,我们在Xy数据后跟上了'r'。这个关键字参数让线条变成红色,是图表格式化的一种简写语法。

  4. 这段代码可以用来绘制原始数据,以及在这些数据上拟合的模型预测结果:

    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:训练集和测试集的形状

图 2.8:训练集和测试集的形状

你应该自行确认训练集和测试集中的样本(行数)是否与 80/20 拆分一致。

在进行训练/测试拆分时,我们还设置了random_state参数,这是一个随机数种子。使用这个参数可以确保在每次运行这个笔记本时,训练/测试拆分是一致的。否则,随机拆分过程每次运行时都会选择不同的 20%数据进行测试。

train_test_split的第一个参数是特征,在本例中是EDUCATION,第二个参数是响应。该函数有四个输出:分别是训练集和测试集中的样本特征,以及与这些特征集对应的响应变量。此函数所做的只是从数据集中随机选择 20%的行索引,并将这些特征和响应子集作为测试数据,剩下的用于训练。现在我们已经有了训练数据和测试数据,确保数据的性质在这些集合中是一致的很重要。特别是,正类的比例是否相似?您可以通过以下输出进行观察:

图 2.9: 训练数据和测试数据中的类别分布

图 2.9: 训练数据和测试数据中的类别分布

训练集和测试集中的正类比例均约为 22%。这是好的,因为我们可以说训练集是测试集的代表。在这种情况下,由于我们拥有一个包含数万个样本的大型数据集,并且类别不太失衡,因此我们不必采取额外的预防措施来确保这种情况发生。

然而,您可以想象,如果数据集较小,且正类非常稀有,训练集和测试集之间的类别比例可能会明显不同,甚至更糟,测试集中可能根本没有正样本。为了防止这种情况,您可以使用train_test_splitstratify关键字参数。此过程也会随机地将数据划分为训练集和测试集,但可以确保类别比例相等或非常相似。

注意

超时测试

如果您的数据包含跨越较长时间段的特征和响应,最好尝试基于时间进行训练/测试集划分。例如,如果您有两年的数据,每个月都有特征和响应,您可能希望尝试依次用 12 个月的数据训练模型,然后用下一个月或下下个月的数据进行测试,具体取决于在模型使用时可操作的情况。您可以一直重复这个过程,直到数据用完,以获得几个不同的测试分数。这将为您提供有价值的模型性能洞察,因为它模拟了模型部署时实际面临的条件:一个在旧特征和响应上训练的模型将用于对新数据进行预测。在案例研究中,响应仅来自某一时刻(一个月内的信用违约),所以这里不适用此方法。

分类准确率

现在我们继续拟合一个示例模型,以说明二分类度量。我们将继续使用接近默认选项的逻辑回归,选择我们在第一章数据探索与清洗中演示的相同选项:

图 2.10: 加载模型类并创建模型对象

图 2.10:加载模型类并创建模型对象

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

图 2.11:训练模型并对测试集进行预测

](tos-cn-i-73owjymdk6/b5df09c55f1c419291a5b63e97234f21)

图 2.11:训练模型并对测试集进行预测

我们已经将测试集的模型预测标签存储在一个名为 y_pred 的变量中。现在我们该如何评估这些预测的质量呢?我们有真实标签,存储在 y_test 变量中。首先,我们将计算可能是所有二分类指标中最简单的一个:准确度。准确度被定义为正确分类样本所占的比例。

计算准确度的一种方法是创建一个逻辑掩码,当预测标签等于实际标签时,掩码为True,否则为False。我们可以计算这个掩码的平均值,将True视为 1,False视为 0,从而得到正确分类的比例:

图 2.12:使用逻辑掩码计算分类准确度

](tos-cn-i-73owjymdk6/33b3c4feba9c41d79cfdfd96a5d61a40)

图 2.12:使用逻辑掩码计算分类准确度

这表示模型在 78%的时间里是正确的。虽然这是一个非常直接的计算方法,但实际上使用 scikit-learn 更简便的方法来计算准确度。我们可以使用训练好的模型的 .score 方法,将测试数据的特征传递给它进行预测,同时传递测试标签。该方法会执行预测,然后进行我们之前所做的相同计算,所有这些都可以一步完成。或者,我们可以导入 scikit-learn 的 metrics 库,该库包含许多模型性能指标,比如 accuracy_score。为此,我们需要传递真实标签和预测标签:

图 2.13:使用 scikit-learn 计算分类准确度

](tos-cn-i-73owjymdk6/83d968d087ec46c781b1cd79e31ee377)

图 2.13:使用 scikit-learn 计算分类准确度

这些方法都会得到相同的结果,正如预期的那样。现在我们知道了模型的准确度,那么如何解释这个指标呢?表面上看,78%的准确度可能听起来不错。我们大部分的预测都正确。然而,二分类准确度的一个重要测试是将其与一个非常简单的假设模型进行比较:这个假设模型对每个样本的预测都是相同的——无论特征是什么,它总是预测多数类别。虽然在实际中这个模型没有什么用处,但它提供了一个重要的极端情况,供我们用来与已训练模型的准确度进行比较。这样的极端情况有时被称为“零模型”。

想想看,这样一个空模型的准确率会是多少。在我们的数据集中,我们知道大约 22%的样本是正类。因此,负类是多数类,占剩余的 78%的样本。因此,一个总是预测负类的空模型在这个数据集中将有 78%的正确率。当我们将训练好的模型与这个空模型进行比较时,就会发现,78%的准确率其实并没有太大意义。我们可以通过一个不关注任何特征的模型来获得相同的准确率。

尽管我们可以通过多数类空模型来解释准确率,但还有其他一些二分类指标可以更深入地了解模型对正类和负类样本的表现。

真阳性率、假阳性率与混淆矩阵

在二分类中,只有两个标签需要考虑:正类和负类。作为比全体样本准确率更具描述性的模型性能评估方式,我们还可以仅查看那些正类标签样本的预测准确度。我们成功预测为正类的比例称为真阳性率TPR)。如果我们设P为测试数据中正类样本的数量,TP真阳性的数量,定义为被模型正确预测为正类的正类样本数,那么 TPR 公式如下:

图 2.14:TPR 公式

图 2.14:TPR 公式

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

图 2.15:FNR 公式

图 2.15:FNR 公式

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

图 2.16:TPR 与 FNR 的关系

图 2.16:TPR 与 FNR 的关系

由于 TPR 和 FNR 的和为 1,因此只需要计算其中一个就足够了。

与 TPR 和 FNR 类似,还有真负率TNR)和假阳性率FPR)。如果N负类样本的数量,那么真负样本(TN)的总和是那些被正确预测为负类的数量,假阳性FP)样本的总和则是被错误预测为正类的样本数量:

图 2.17:TNR 公式

图 2.17:TNR 公式

图 2.18:FPR 公式

图 2.18:FPR 公式

图 2.19:TNR 与 FPR 之间的关系

图 2.19:TNR 与 FPR 之间的关系

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

图 2.20:二分类的混淆矩阵

图 2.20:二分类的混淆矩阵

由于我们希望做出正确的分类,我们希望混淆矩阵的对角线条目(即从左上角到右下角的对角线上的条目:TN 和 TP)相对较大,而非对角线条目较小,因为它们代表错误的分类。可以通过将对角线上的条目(即正确的预测)相加,然后除以所有预测的总数来计算准确度。

练习 2.02:在 Python 中计算真实和假正例与假负例率以及混淆矩阵

在本练习中,我们将使用之前创建的逻辑回归模型的测试数据和模型预测,只使用EDUCATION特征。我们将展示如何手动计算真实正例和假负例率,以及混淆矩阵所需的真实和假正例及假负例的数量。然后我们将展示使用 scikit-learn 快速计算混淆矩阵的方法。执行以下步骤来完成练习,注意在做此练习之前必须先运行上一部分的代码(如 GitHub 上所示):

注意

本练习的 Jupyter 笔记本可以在此找到:packt.link/S02kz

  1. 运行此代码计算正例样本的数量:

    P = sum(y_test)
    P
    

    输出应该如下所示:

    1155
    

    现在我们需要真实正例的数量。这些是实际标签为 1 且预测也为 1 的样本。我们可以通过逻辑掩码来识别这些样本,其中正例为(y_test==1),并且&是逻辑运算符,y_pred==1)。

  2. 使用以下代码计算真实正例的数量:

    TP = sum( (y_test==1) & (y_pred==1) )
    TP
    

    以下是输出:

    0
    

    真实正例率是指真实正例与所有正例的比例,这在这里当然是 0。

  3. 运行以下代码获取真实正例率(TPR):

    TPR = TP/P
    TPR
    

    你将获得以下输出:

    0.0
    

    类似地,我们可以识别假负例。

  4. 使用以下代码计算假负例的数量:

    FN = sum( (y_test==1) & (y_pred==0) )
    FN
    

    输出应如下所示:1155

    我们还希望得到假负率(FNR)。

  5. 使用以下代码计算假负率(FNR):

    FNR = FN/P
    FNR
    

    输出应如下所示:

    1.0
    

    从真实正例和假负例率中我们学到了什么?

    首先,我们可以确认它们的总和为 1。这个事实很容易看出来,因为 TPR = 0 且 FPR = 1。这告诉我们关于模型什么信息?在测试集上,至少对于正样本,模型实际上表现得像一个多数类零模型。每个正样本都被预测为负样本,因此没有一个被正确预测。

  6. 让我们找出测试数据的 TNR 和 FPR。由于这些计算与我们之前查看的非常相似,因此我们一次性展示它们,并介绍一个新的 Python 函数:图 2.21:计算真正负类率和假正类率并打印它们

    图 2.21:计算真正负类率和假正类率并打印它们

    除了像我们之前那样计算 TNR 和 FPR,我们还展示了 Python 中的 print 函数,并结合 .format 方法来处理字符串,这样可以在大括号 {} 标记的位置替换变量。还有多种选项可以格式化数字,例如保留特定的小数位数。

    注意

    如需更多细节,请参考 docs.python.org/3/tutorial/inputoutput.html

    那么,我们在这里学到了什么?事实上,我们的模型对所有样本(正样本和负样本)表现得像一个多数类零模型。显然,我们需要一个更好的模型。

    虽然我们在本次练习中手动计算了混淆矩阵的所有条目,但在 scikit-learn 中有一种快速的方法来做这件事。请注意,在 scikit-learn 中,真正类位于混淆矩阵的纵轴上,预测类位于横轴上,正如我们之前所展示的那样。

  7. 使用以下代码在 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。该笔记本包含训练模型的先决步骤,应该在这里显示的第一步之前执行。

  1. 使用以下代码获取测试样本的预测概率:

    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来计算每个样本的概率和。

  2. 使用此代码计算每个样本的预测概率和:

    prob_sum = np.sum(y_pred_proba,1)
    prob_sum
    

    输出如下所示:

    array([1., 1., 1., ..., 1., 1., 1.])
    

    看起来确实全是 1。我们应该检查结果是否与测试数据标签的数组形状相同。

  3. 使用此代码检查数组形状:

    prob_sum.shape
    

    这应该输出以下内容:

    (5333,)
    

    很好;这正是预期的形状。现在,检查每个值是否为 1。我们使用np.unique来显示这个数组中所有唯一的元素。这类似于 SQL 中的DISTINCT。如果所有概率和确实为 1,那么概率数组中应该只有一个唯一元素:1。

  4. 使用此代码显示所有唯一的数组元素:

    np.unique(prob_sum)
    

    这应该输出以下内容:

    array([1.])
    

    在确认我们对预测概率的信心后,我们注意到,由于类概率和为 1,因此只考虑第二列,即正类成员资格的预测概率就足够了。让我们将这些捕获到一个数组中。

  5. 运行此代码,将预测概率数组的第二列(正类成员资格的预测概率)放入一个数组中:

    pos_proba = y_pred_proba[:,1]
    pos_proba
    

    输出应该如下所示:

    图 2.24:正类成员资格的预测概率

    ](tos-cn-i-73owjymdk6/466b55aca0384f068b8ca30b0e633356)

    图 2.24:正类成员资格的预测概率

    这些预测概率看起来如何?一种了解它们的方法,也是评估模型输出的一个好诊断手段,就是绘制预测概率的直方图。直方图是一种自然的方式,我们可以使用 matplotlib 的hist()函数来实现。请注意,如果你只执行包含直方图函数的代码单元,你会在绘图之前得到 NumPy 直方图函数的输出。这个输出包括每个桶中的样本数和桶边界的位置。

  6. 执行此代码可以查看直方图输出和一个未格式化的图(此处未显示):

    plt.hist(pos_proba)
    

    输出结果如下:

    图 2.25: 直方图计算的细节

    图 2.25: 直方图计算的细节

    这些信息可能对你有用,并且也可以直接通过np.histogram()函数获得。然而,在这里我们主要关注图形,因此我们调整了字体大小并添加了一些坐标轴标签。

  7. 运行此代码以获得格式化的预测概率直方图:

    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来获取负样本。

  8. 使用以下代码隔离正负样本的预测概率:

    pos_sample_pos_proba = pos_proba[y_test==1]
    neg_sample_pos_proba = pos_proba[y_test==0]
    

    现在,我们希望将这些数据绘制为堆叠直方图。代码与我们之前创建的直方图类似,不同之处在于,我们将传递一个包含数组的列表,这些数组分别是我们刚刚创建的正负样本的预测概率数组,并且添加一个关键字,指示我们希望柱形图堆叠而非并排显示。同时,我们还将创建一个图例,以便颜色能够在图中清晰区分。

  9. 使用以下代码绘制堆叠直方图:

    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')

并且图表应如下所示:

图 2.28:我们的逻辑回归的 ROC 曲线,带有随机机会线供比较

用于比较的随机机会显示

](tos-cn-i-73owjymdk6/f25764a4f87b4d83b4e26c91e07752ba)

图 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:精度公式

图 2.29:精度公式

考虑这一点的解释,考虑到在预测概率的范围内变化阈值,就像我们为 ROC 曲线所做的那样。在高阈值下,预测为正样本的样本相对较少。随着阈值的降低,越来越多的样本将被预测为正样本。我们的期望是,在执行这一操作时,真正的正样本数量会比假正样本的数量增加得更快,正如我们在 ROC 曲线中所看到的那样。精确度看的是真正的正样本数量与真正和假正样本总和的比例。考虑这里的分母:真正和假正样本的总和是多少?

这个总和实际上是所有正预测的总数,因为所有正预测要么是正确的,要么是错误的。因此,精确度衡量的是正确的正预测与所有正预测的比例。因此,它也被称为metrics.precision_recall_curve。精确度和召回率通常一起绘制,以评估正预测的质量,考虑哪些部分是正确的,同时考虑模型能够识别正类的比例。我们将在接下来的活动中绘制精确度-召回曲线。

为什么精确度是一个有用的分类器性能度量?想象一下,对于每个正的模型预测,你将采取一些昂贵的措施,比如对通过自动化程序标记为不当的内容进行耗时的复审。假正样本会浪费人工审阅者宝贵的时间。在这种情况下,你会希望确保你在做出哪些内容需要详细复审的决定时是正确的。精确度可能是这个情况中一个很好的度量指标。

活动 2.01:使用新特征执行逻辑回归并创建精确度-召回曲线

在这个活动中,你将使用除EDUCATION之外的特征来训练一个逻辑回归模型。然后,你将通过图形化评估精确度和召回率之间的权衡,并计算精确度-召回曲线下的面积。你还将计算训练集和测试集上的 ROC AUC 并进行比较。

执行以下步骤以完成该活动:

注意

该活动的代码和结果输出已加载到一个 Jupyter notebook 中,可以在此处找到:packt.link/SvAOD

  1. 使用 scikit-learn 的train_test_split来生成一组新的训练数据和测试数据。这次,不使用EDUCATION,而使用LIMIT_BAL,即账户的信用额度,作为特征。

  2. 使用从划分中得到的训练数据来训练一个逻辑回归模型。

  3. 创建测试数据的预测概率数组。

  4. 使用测试数据的预测概率和真实标签来计算 ROC AUC。将其与使用EDUCATION特征的 ROC AUC 进行比较。

  5. 绘制 ROC 曲线。

  6. 使用 scikit-learn 的功能,计算测试数据的精确率-召回率曲线的数据。

  7. 使用 matplotlib 绘制精确率-召回率曲线。

  8. 使用 scikit-learn 计算精确率-召回率曲线下的面积。你应该得到大约 0.315 的值。

  9. 现在重新计算 ROC AUC,不过这次使用训练数据。与之前的计算在概念上和数量上有何不同?

    注意

    包含此活动 Python 代码解决方案的 Jupyter notebook 可以在这里找到:packt.link/SvAOD。此活动的详细逐步解决方案可以通过这个链接查看。

总结

在本章中,我们通过检查响应变量完成了案例研究数据的初步探索。一旦我们对数据集的完整性和正确性充满信心,就准备好探索特征与响应之间的关系,并构建模型。

本章的大部分内容都在技术和编码层面上熟悉了 scikit-learn 中的模型拟合,并学习了我们可以在案例研究的二分类问题中使用的指标。在尝试不同的特征集和模型时,你将需要某种方法来判断哪种方法比另一种更有效。因此,你需要使用我们在本章中学到的模型性能指标。

尽管准确率作为正确分类百分比是一个熟悉且直观的指标,但我们了解到它可能无法有效评估分类器的性能。我们学会了如何使用多数类零假设模型来判断一个准确率是否真正优秀,还是仅仅与对所有样本预测最常见类别的结果没有差别。当数据不平衡时,准确率通常不是评判分类器的最佳方式。

为了更细致地了解模型的表现,必须将正类和负类分开,并独立评估它们的准确性。从由真正和虚假正负分类汇总而成的混淆矩阵中,我们可以得出其他几个指标:真正率、虚假正率和虚假负率。结合真正和虚假正负以及预测概率和可变预测阈值的概念,我们可以通过 ROC 曲线、精确率-召回率曲线及其下的面积进一步描述分类器的有效性。

有了这些工具,你已经能够充分回答任何领域中二分类器性能的一般问题。在本书后面的内容中,我们将学习如何通过为真阳性、假阳性、真阴性和假阴性分配成本和收益,来评估模型性能的应用特定方法。在此之前,从下一章开始,我们将开始学习可能是最流行且最简单的分类模型的细节:逻辑回归