原文:
annas-archive.org/md5/d906d6d9346f8d8b7965d192afaf9a47
译者:飞龙
前言
关于
本节简要介绍了作者、本书内容、你需要掌握的技术技能,以及完成所有活动和练习所需的硬件和软件要求。
关于本书
机器学习——机器基于输入数据给出正确答案的能力——彻底改变了我们做生意的方式。《应用 Python 监督学习》提供了如何将机器学习技术应用于你的数据科学项目的丰富理解。你将探索 Jupyter 笔记本,一种在学术界和商业圈广泛使用的技术,支持运行内联代码。
在有趣的示例帮助下,你将获得使用 Python 机器学习工具包的经验——从执行基本的数据清理和处理,到使用多种回归和分类算法。掌握基础后,你将学习如何使用决策树、集成建模、验证和误差指标等高级技术来构建和训练自己的模型。你还将学习如何使用强大的 Python 库,如 Matplotlib 和 Seaborn,进行数据可视化。
本书还涵盖了集成建模和随机森林分类器,以及结合多个模型结果的其他方法,最后深入探讨了交叉验证,用于测试你的算法并检查模型在未见数据上的表现。
到本书结束时,你不仅将能够使用机器学习算法,还将能够创建你自己的算法!
关于作者
本杰明·约翰斯顿是全球领先的以数据驱动的医疗科技公司之一的高级数据科学家,参与从问题定义、解决方案研发到最终部署的整个产品开发过程中创新数字解决方案的开发。他目前正在攻读机器学习博士学位,专注于图像处理和深度卷积神经网络。他在医疗设备设计与开发方面拥有超过 10 年的经验,曾在多个技术岗位工作,并获得澳大利亚悉尼大学的工程学与医学科学的双学士学位,并且均为一等荣誉学位。
伊希塔·马图尔在数据科学领域有 2.5 年的工作经验,曾在面向产品的初创公司工作,解决来自各个领域的业务问题,并将这些问题转化为可以通过数据和机器学习解决的技术问题。她目前在 GO-JEK 的工作涉及机器学习项目的端到端开发,作为产品团队的一员,定义、原型设计并在产品中实现数据科学模型。她在英国爱丁堡大学完成了高性能计算与数据科学的硕士学位,并在德里圣斯蒂芬学院获得了物理学的荣誉学士学位。
目标
-
理解监督学习的概念及其应用
-
使用机器学习 Python 库实现常见的监督学习算法
-
使用 k 折交叉验证技术验证模型
-
使用决策树构建模型,轻松获得结果
-
使用集成模型技术提高模型的性能
-
使用多种度量标准比较机器学习模型
受众
使用 Python 的应用监督学习 适合那些想要通过 Python 深入理解机器学习的人。如果你有任何函数式或面向对象编程语言的经验,并对 Python 的库和表达式(如数组和字典)有基本了解,将对你有帮助。
方法
使用 Python 的应用监督学习 采用实践性的方法,帮助理解如何使用 Python 进行监督学习。它包含多个活动,使用实际的商业场景,帮助你在高度相关的环境中练习和应用新技能。
硬件要求
为了获得最佳的学习体验,我们推荐以下硬件配置:
-
处理器:双核或更高
-
内存:4 GB RAM
-
硬盘:10 GB 可用空间
-
网络连接
软件要求
你还需要预先安装以下软件:
-
以下操作系统中的任何一个:
Windows 7 SP1 32/64 位,Windows 8.1 32/64 位,或 Windows 10 32/64 位
Ubuntu 14.04 或更高版本
macOS Sierra 或更高版本
-
浏览器:Google Chrome 或 Mozilla Firefox
-
Anaconda
约定
书中出现的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名均按如下方式展示:“可以通过 Python 内建的 type
函数轻松验证这一点。”
一段代码如下所示:
description_features = [
'injuries_description', 'damage_description',
'total_injuries_description', 'total_damage_description'
]
新术语和重要单词以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,文本中会这样出现:“点击 Untitled 文本,弹出框将允许你重命名该笔记本。”
安装与设置
安装 Anaconda 后,可以使用 Jupyter notebooks。可以通过docs.anaconda.com/anaconda/install/windows/
上的步骤,在 Windows 系统上安装 Anaconda。
对于其他系统,请从docs.anaconda.com/anaconda/install/
导航到相应的安装指南。
安装代码包
将书中的代码包复制到 C:/Code
文件夹中。
附加资源
本书的代码包也托管在 GitHub 上,地址为:github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python
。
我们还提供其他代码包,来自我们丰富的书籍和视频目录,地址为 https://github.com/PacktPublishing/。快来看看吧!
第一章:Python 机器学习工具包
学习目标
在本章结束时,你将能够:
-
解释监督学习,并描述常见的机器学习问题示例
-
安装并加载 Python 库到开发环境中,用于分析和机器学习问题
-
访问并解读 Python 库子集的文档,包括强大的 pandas 库
-
创建一个 IPython Jupyter 笔记本,并使用可执行代码单元和 markdown 单元来创建动态报告
-
使用 pandas 加载外部数据源,并利用各种方法对数据进行搜索、过滤和计算描述性统计
-
清洗质量较差的数据源,并评估数据源中各种问题可能产生的影响
本章介绍了监督学习、Jupyter 笔记本以及一些常见的 pandas 数据方法。
介绍
机器学习和人工智能的研究与应用,近年来成为了科技和商业界广泛关注和研究的热点。先进的数据分析和机器学习技术在推动许多领域(如个性化医疗和自动驾驶汽车)发展方面展现出了巨大的潜力,同时也在解决全球一些重大挑战(如应对气候变化)方面发挥着重要作用。本书旨在帮助你抓住当前数据科学和机器学习领域的独特机遇。在全球范围内,私营企业和政府意识到数据驱动的产品和服务的价值和效率。同时,硬件成本的降低和开源软件解决方案的普及大大降低了学习和应用机器学习技术的门槛。
本书将帮助你发展所需的技能,使你能够使用 Python 编程语言中的监督学习技术来识别、准备和构建预测模型。六个章节分别涵盖了监督学习的各个方面。本章介绍了 Python 机器学习工具包的一个子集,以及在加载和使用数据源时需要考虑的一些事项。这个数据探索过程将在第二章《探索性数据分析与可视化》中进一步探讨,介绍探索性数据分析和可视化。第三章《回归分析》和第四章《分类》将探讨机器学习问题的两个子集——回归分析和分类分析,并通过实例展示这些技术。最后,第五章《集成建模》介绍了集成网络,该技术通过多个不同模型的预测来提高整体性能,而第六章《模型评估》则涵盖了验证和评估指标这两个极为重要的概念。这些指标提供了一种估计模型真实表现的方法。
有监督机器学习
机器学习算法通常被认为仅仅是数学过程(或算法)本身,例如神经网络、深度神经网络或随机森林算法。然而,这只是整体系统的一个组成部分;首先,我们必须定义可以通过这些技术充分解决的问题。接着,我们必须指定并获取一个干净的数据集,该数据集由可以从第一个数值空间映射到第二个数值空间的信息组成。一旦数据集被设计并获得,机器学习模型就可以被指定和设计;例如,使用tanh激活函数的 100 个隐藏节点的单层神经网络。
在数据集和模型被明确定义后,可以指定确定模型精确值的方法。这是一个重复的优化过程,它通过评估模型的输出与现有数据的匹配度,通常被称为训练。一旦训练完成,且你拥有已定义的模型,接下来最好通过一些参考数据对其进行评估,以提供整体性能的基准。
考虑到完整机器学习算法的这个一般描述,问题定义和数据收集阶段通常是最关键的。你要解决的问题是什么?你希望达到什么样的结果?你打算如何实现这一目标?你如何回答这些问题将决定并定义后续的许多决策或模型设计选择。在回答这些问题时,我们将选择哪种机器学习算法类别:有监督还是无监督方法。
那么,什么是有监督和无监督机器学习问题或方法呢?有监督学习技术的核心是通过为训练过程提供输入信息和期望输出,映射某些信息集到另一个信息集,并检查其提供正确结果的能力。例如,假设你是一本杂志的出版商,负责评审并排名来自不同时间段的发型。你的读者常常会给你发送大量的发型图片,比你手动处理的要多。为了节省时间,你希望自动对收到的发型图片按时间段进行分类,从 1960 年代和 1980 年代的发型开始:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_01.jpg
图 1.1:来自不同时间段的发型图片
要创建你的发型分类算法,你需要先收集大量发型图片,并手动为每一张图片标注对应的时间段。这样的数据集(称为标注数据集)是输入数据(发型图片),并且所需的输出信息(时间段)是已知且记录下来的。这种问题是经典的监督学习问题;我们试图开发一个算法,输入一组数据,然后让它学会返回我们已经告诉它正确的答案。
何时使用监督学习
通常情况下,如果你试图自动化或复制一个现有的过程,那么这个问题就是一个监督学习问题。监督学习技术既非常有用又非常强大,或许你已经接触过它们,甚至在不知情的情况下帮助创建了它们的标注数据集。举个例子,几年前,Facebook 引入了在平台上传任何图片时标记朋友的功能。要标记一个朋友,你只需在朋友的面部上画一个框,然后添加朋友的名字以通知他们图片的存在。快进到今天,Facebook 会自动识别图片中的朋友并为你标记他们。这又是一个监督学习的例子。如果你曾经使用过早期的标记系统,并手动在图片中标记你的朋友,实际上你是在帮助创建 Facebook 的标注数据集。用户上传一个人的面部图片(输入数据),并用该人物的名字标记照片,这样就为数据集创建了标签。随着用户持续使用这个标记服务,一个足够大的标注数据集就被创建出来,解决了监督学习问题。现在,Facebook 自动完成朋友标记,使用监督学习算法替代了手动输入的过程:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_02.jpg
图 1.2:在 Facebook 上标记朋友
一个特别及时且直接的监督学习例子是自动驾驶汽车的训练。在这个例子中,算法使用由 GPS 系统确定的目标路线,以及车载仪器,如速度测量、刹车位置和/或 光学雷达(LIDAR)进行道路障碍物检测,作为系统的标注输出。在训练过程中,算法采集人类驾驶员提供的控制输入,如速度、转向角度和刹车位置,并将其与系统的输出进行映射,从而提供标注数据集。这些数据可以用来训练自动驾驶汽车的驾驶/导航系统,或用于模拟练习。
基于图像的监督问题虽然很流行,但并不是监督学习问题的唯一例子。监督学习也常用于自动分析文本,以判断消息的意见或语气是积极的、消极的还是中立的。这种分析被称为情感分析,通常涉及创建并使用一个标记数据集,其中一系列的单词或语句被手动标识为积极、消极或中立。例如,考虑以下句子:我喜欢这部电影 和 我讨厌这部电影。第一个句子显然是积极的,而第二个是消极的。我们可以将句子中的单词分解为积极、消极或中立(都积极、都消极);见下表:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_03.jpg
图 1.3:单词的分解
使用情感分析,可能会创建一个监督学习算法,比如使用电影数据库网站 IMDb 分析发布的关于电影的评论,以确定观众是对电影进行积极评价还是消极评价。监督学习方法还可以应用于其他领域,如分析客户投诉、自动化故障排除电话/聊天会话,甚至在医学领域,如分析痣的图像来检测异常(www.nature.com/articles/nature21056
)。
这应该能帮助你更好地理解监督学习的概念,以及一些可以使用这些技术解决的问题示例。监督学习涉及训练一个算法,将输入信息映射到相应的已知输出;而无监督学习方法则不同,它们不使用已知的输出,可能是因为这些输出不可用或甚至未知。无监督学习方法并不依赖于一组手动标注的标签,而是通过在训练过程中设计的特定约束或规则来对提供的数据进行建模。
聚类分析是一种常见的无监督学习形式,其中数据集会根据所使用的聚类过程被划分为指定数量的不同组。以 k 近邻聚类为例,数据集中的每个样本会根据与该样本最接近的 k 个点的多数投票结果进行标记或分类。由于没有手动识别的标签,无监督算法的性能会根据使用的数据以及模型选择的参数大不相同。例如,我们是否应该在 k 个最接近的点中使用 5 个最接近的点,还是 10 个最接近的点进行多数投票?由于训练过程中缺乏已知和目标输出,无监督方法通常用于探索性分析或在那些真实目标比较模糊且通过学习方法的约束来更好定义的场景中。
本书中我们不会深入讨论无监督学习,但总结两种方法之间的主要区别是有帮助的。监督学习方法需要真实标签或输入数据的答案,而无监督学习方法不使用这些标签,最终结果由训练过程中应用的约束决定。
为什么选择 Python?
那么,为什么我们选择 Python 编程语言来进行监督学习的研究呢?虽然有多种替代语言可供选择,包括 C++、R 和 Julia,甚至 Rust 社区也在为其新兴语言开发机器学习库,但 Python 仍是机器学习的首选语言,原因有很多:
-
在工业界和学术研究中,对具备 Python 技能的开发人员有着极大的需求。
-
Python 目前是最受欢迎的编程语言之一,甚至在IEEE Spectrum杂志对十大编程语言的调查中位居第一(
spectrum.ieee.org/at-work/innovation/the-2018-top-programming-languages
)。 -
Python 是一个开源项目,Python 编程语言的全部源代码在 GNU GPL Version 2 许可证下免费提供。这种许可机制使得 Python 能够在许多其他项目中使用、修改甚至扩展,包括 Linux 操作系统、支持 NASA 的项目(
www.python.org/about/success/usa/
),以及众多提供额外功能、选择和灵活性的库和项目。在我们看来,这种灵活性是 Python 如此受欢迎的关键因素之一。 -
Python 提供了一套通用的功能,可用于运行 Web 服务器、嵌入式设备上的微服务,或利用图形处理单元的强大功能对大型数据集进行精确计算。
-
使用 Python 和一些特定的库(或在 Python 中称为“包”),可以开发出整个机器学习产品——从探索性数据分析、模型定义与优化,到 API 构建和部署。这些步骤都可以在 Python 中完成,构建一个端到端的解决方案。这是 Python 相对于一些竞争者,特别是在数据科学和机器学习领域的一个显著优势。尽管 R 和 Julia 在数值计算和统计计算方面具有优势,但用这些语言开发的模型通常需要在部署到生产环境之前,转译成其他语言。
我们希望通过本书,你能了解 Python 编程语言的灵活性和强大功能,并开始开发 Python 中端到端监督学习解决方案的道路。那么,让我们开始吧。
Jupyter 笔记本
数据科学开发环境的一个独特之处是使用 IPython Jupyter 笔记本 (jupyter.org
),这与其他 Python 项目有所不同。Jupyter 笔记本提供了一种创建和共享交互式文档的方式,文档中可以包含实时执行的代码片段、图表以及通过 Latex (www.latex-project.org
) 排版系统渲染的数学公式。本章节将介绍 Jupyter 笔记本及其一些关键特性,确保你的开发环境正确设置。
在本书中,我们将经常参考每个介绍的工具/包的文档。有效阅读和理解每个工具的文档非常重要。我们将使用的许多包包含了如此多的功能和实现细节,以至于很难记住它们的所有内容。以下文档可能对接下来的 Jupyter 笔记本部分有所帮助:
-
Anaconda 文档可以在
docs.anaconda.com
找到。 -
Anaconda 用户指南可以在
docs.anaconda.com/anaconda/user-guide
找到。 -
Jupyter 笔记本文档可以在
jupyter-notebook.readthedocs.io/en/stable/
找到。
练习 1:启动 Jupyter 笔记本
在本练习中,我们将启动 Jupyter 笔记本。请确保你已按 前言 中的说明正确安装了 Python 3.7 版本的 Anaconda:
-
通过 Anaconda 启动 Jupyter 笔记本有两种方式。第一种方法是通过 Windows 开始菜单中的
Anaconda
文件夹打开 Jupyter。点击http://localhost:8888
,它将在默认的文件夹路径下启动。 -
第二种方法是通过 Anaconda 提示符启动 Jupyter。要启动 Anaconda 提示符,只需点击 Windows 开始菜单中的 Anaconda Prompt 菜单项,你应该会看到一个类似以下屏幕截图的弹出窗口:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_04.jpg
图 1.4:Anaconda 提示符
-
进入 Anaconda 提示符后,使用
cd
(更改目录)命令切换到所需的目录。例如,要切换到Packt
用户的Desktop
目录,可以执行以下操作:C:\Users\Packt> cd C:\Users\Packt\Desktop
-
进入所需的目录后,使用以下命令启动 Jupyter 笔记本:
C:\Users\Packt> jupyter notebook
笔记本将会在你之前指定的工作目录中启动。这样,你可以在你选择的目录中浏览并保存你的笔记本,而不是默认的目录,默认目录因系统而异,但通常是你的主目录或
我的电脑
目录。无论是通过何种方式启动 Jupyter,都会在你的默认浏览器中打开一个类似下面的窗口。如果目录中有现有文件,你也应该在这里看到它们:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_05.jpg
图 1.5:Jupyter 笔记本启动窗口
练习 2:Hello World
Hello World 练习是一个必经之路,所以你当然不能错过这个体验!现在,让我们在这个练习中在 Jupyter 笔记本中打印出Hello World
:
-
首先通过点击新建按钮并选择Python 3来创建一个新的 Jupyter 笔记本。Jupyter 允许你在同一个界面中运行不同版本的 Python 以及其他语言,如 R 和 Julia。我们也可以在这里创建新的文件夹或文本文件。但现在,我们将从一个 Python 3 笔记本开始:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_06.jpg
图 1.6:创建一个新笔记本
这将会在一个新的浏览器窗口中启动一个新的 Jupyter 笔记本。我们将首先花一些时间查看笔记本中可用的各种工具:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_07.jpg
图 1.7:新笔记本
每个 Jupyter 笔记本有三个主要部分,如下截图所示:标题栏(1)、工具栏(2)和文档正文(3)。我们按顺序来看一下这些组件:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_08.jpg
图 1.8:笔记本的组件
-
标题栏仅显示当前 Jupyter 笔记本的名称,并允许对笔记本进行重命名。点击
Hello World
,然后点击重命名:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_09.jpg图 1.9:重命名笔记本
-
大多数情况下,工具栏包含所有你预期的常见功能。你可以在文件菜单中打开、保存和复制笔记本,或者创建新的 Jupyter 笔记本。在编辑菜单中,你可以查找替换、复制和剪切内容,在视图菜单中,你可以调整文档的显示方式。在我们讨论文档正文的同时,我们还会更详细地描述一些其他功能,例如插入、单元格和内核菜单中的功能。工具栏有一个部分需要进一步检查,即位于 Python 3 右侧的圆形轮廓区域。
将鼠标悬停在圆圈上,你将看到内核空闲的弹出窗口。这个圆圈是一个指示器,用来表示 Python 内核当前是否正在处理;当正在处理时,这个圆圈指示器将被填充。如果你怀疑某些操作正在运行或没有运行,你可以轻松查看这个图标获取更多信息。当 Python 内核没有运行时,你将看到这个:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_10.jpg
图 1.10:内核空闲
当 Python 内核正在运行时,你会看到这个:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_11.jpg
图 1.11:内核忙碌
-
这将带我们进入文档的主体部分,实际的 notebook 内容将在这里输入。Jupyter notebooks 与标准的 Python 脚本或模块不同,它们被分成多个可执行的单元格。虽然 Python 脚本或模块在执行时会运行整个脚本,但 Jupyter notebooks 可以按顺序运行所有单元格,或者如果手动执行,还可以单独运行它们并以不同的顺序执行。
双击第一个单元格并输入以下内容:
>>> print('Hello World!')
-
点击运行(或使用Ctrl + Enter 快捷键):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_12.jpg
图 1.12:运行单元格
恭喜!你刚刚在 Jupyter notebook 中完成了Hello World。
练习 3:Jupyter Notebook 中的执行顺序
在上一个练习中,注意到print
语句是在单元格下方执行的。现在让我们再深入一点。如前所述,Jupyter notebooks 由多个可单独执行的单元格组成;最好将它们视为你输入到 Python 解释器中的代码块,代码在你按下Ctrl + Enter 键之前不会执行。尽管代码在不同的时间运行,但所有的变量和对象都保持在 Python 内核的会话中。让我们进一步探讨这一点:
-
启动一个新的 Jupyter notebook,然后在三个单独的单元格中输入以下截图中显示的代码:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_13.jpg
图 1.13:在多个单元格中输入代码
-
点击
hello_world
变量在第二个单元格中被声明(并执行),并保持在内存中,因此会在第三个单元格中打印出来。如前所述,你还可以不按顺序运行这些单元格。 -
点击第二个单元格,里面包含了
hello_world
的声明,修改值,添加几个感叹号,然后重新运行该单元格:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_14.jpg图 1.14:更改第二个单元格的内容
注意第二个单元格现在是最新执行的单元格(
print
语句后它没有更新)。要更新print
语句,你需要执行其下方的单元格。警告:小心执行顺序。如果不小心,你很容易覆盖值或在变量首次使用之前在下方单元格中声明变量,因为在 notebooks 中,你不需要一次性运行整个脚本。因此,建议定期点击 Kernel | Restart & Run All。这将清除内存中的所有变量,并按顺序从上到下运行所有单元格。你还可以在 Cell 菜单中选择运行特定单元格下方或上方的所有单元格:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_15.jpg
图 1.15:重启内核
注意
写作和组织你的 notebook 单元格时,应该像是要按顺序从上到下依次运行它们一样。仅在调试/早期调查时使用手动单元格执行。
-
你还可以使用
hello_world
变量左侧的上下箭头将单元格移动到其声明之前:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_16.jpg图 1.16:移动单元格
-
点击 Restart & Run All 单元格:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_17.jpg
图 1.17:变量未定义错误
注意错误报告中显示变量未定义。这是因为它在声明之前被使用了。还要注意,错误后的单元格没有被执行,In [ ] 显示为空。
练习 4:Jupyter Notebooks 的优势
Jupyter notebooks 还有许多其他有用的功能。在本练习中,我们将探讨其中的一些功能:
-
Jupyter notebooks 可以通过在 Anaconda 提示符中包含感叹号前缀 (
!
) 来直接执行命令。输入以下截图所示的代码并运行该单元格:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_18.jpg图 1.18:运行 Anaconda 命令
-
Jupyter notebooks 最棒的功能之一是能够创建包含可执行代码的实时报告。这不仅节省了防止分开创建报告和代码的时间,还能帮助传达正在完成的分析的准确性质。通过使用 Markdown 和 HTML,我们可以嵌入标题、章节、图片,甚至是用于动态内容的 JavaScript。
要在我们的 notebook 中使用 Markdown,首先需要更改单元格类型。首先,点击你想要更改为 Markdown 的单元格,然后点击 Code 下拉菜单,选择 Markdown:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_19.jpg
图 1.19:运行 Anaconda 命令
请注意,In [ ] 已经消失,单元格边框的颜色不再是蓝色。
-
现在您可以通过双击单元格并点击运行来输入有效的 Markdown 语法和 HTML,以渲染 Markdown。输入以下截图中显示的语法并运行单元格以查看输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_20.jpg
图 1.20:Markdown 语法
输出如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_21.jpg
图 1.21:Markdown 输出
注意
若要快速参考 Markdown,请查看本章的代码文件中的Markdown Syntax.ipynb
Jupyter 笔记本。
Python 包和模块
尽管 Python 包含的标准功能确实非常丰富,但 Python 真正的力量在于额外的库(在 Python 中也称为包),由于开源许可证,可以通过几个简单的命令轻松下载和安装。在 Anaconda 安装中,这更加简单,因为许多最常见的包都预先在 Anaconda 中构建。您可以通过在笔记本单元格中运行以下命令来获取 Anaconda 环境中预安装包的完整列表:
!conda list
在本书中,我们将使用以下附加 Python 包:
-
NumPy(发音为Num Pie,可访问
www.numpy.org/
):NumPy(即数值 Python)是 Python 科学计算的核心组件之一。NumPy 提供了基础数据类型,包括线性代数、向量和矩阵,以及关键的随机数功能。 -
SciPy(发音为Sigh Pie,可访问
www.scipy.org
):SciPy 与 NumPy 一起,是核心科学计算包。SciPy 提供了许多统计工具、信号处理工具以及傅立叶变换等其他功能。 -
pandas(可访问
pandas.pydata.org/
):pandas 是一个高性能库,用于加载、清理、分析和操作数据结构。 -
Matplotlib(可访问
matplotlib.org/
):Matplotlib 是创建数据集的图形和图表的基础 Python 库,也是其他 Python 绘图库的基础包。Matplotlib API 与 Matlab 绘图库设计保持一致,以便轻松过渡到 Python。 -
Seaborn(可访问
seaborn.pydata.org/
):Seaborn 是建立在 Matplotlib 之上的绘图库,提供吸引人的颜色和线条样式,以及许多常见的绘图模板。 -
Scikit-learn(可在
scikit-learn.org/stable/
获取):Scikit-learn 是一个 Python 机器学习库,提供了一系列简单 API 的数据挖掘、建模和分析技术。Scikit-learn 包含了许多开箱即用的机器学习算法,包括分类、回归和聚类技术。
这些包构成了一个多功能的机器学习开发环境,每个包都提供了一套关键功能。如前所述,通过使用 Anaconda,您将已经安装并准备好所有必需的包。如果您需要一个在 Anaconda 安装中未包含的包,可以通过在 Jupyter 笔记本单元中输入并执行以下命令来安装:
!conda install <package name>
作为示例,如果我们想要安装 Seaborn,只需运行以下命令:
!conda install seaborn
要在笔记本中使用这些包,我们只需要导入它:
import matplotlib
pandas
如前所述,pandas 是一个用于加载、清洗和分析各种数据结构的库。正是由于 pandas 的灵活性,以及内置功能的丰富性,使其成为一个强大、流行且实用的 Python 包。它也是一个非常适合入门的包,因为显然,如果我们不先将数据加载到系统中,就无法对其进行分析。由于 pandas 提供了如此多的功能,使用该包的一个非常重要的技能就是能够阅读和理解文档。即使是多年使用 Python 编程和 pandas 的经验,我们仍然经常参考文档。API 中的功能如此广泛,以至于无法记住所有特性和实现细节。
注意
pandas 文档可以在 pandas.pydata.org/pandas-docs/stable/index.html
找到。
在 pandas 中加载数据
pandas 具有读取和写入多种文件格式和数据结构的能力,包括 CSV、JSON 和 HDF5 文件,以及 SQL 和 Python Pickle 格式。pandas 的输入/输出文档可以在pandas.pydata.org/pandas-docs/stable/user_guide/io.html
找到。我们将继续通过加载 CSV 文件来探讨 pandas 的功能。本章使用的数据集是TITANIC: 机器学习灾难数据集,可以从www.kaggle.com/c/Titanic/data
或github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python
下载,该数据集包含了泰坦尼克号上乘客的名单以及他们的年龄、生存状态和兄弟姐妹/父母人数。在我们开始加载数据到 Python 之前,至关重要的是我们花些时间查看数据集提供的信息,以便全面了解其内容。请下载数据集并将其放置在你正在使用的目录中。
查看数据的描述,我们可以看到我们有以下字段可用:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_22.jpg
图 1.22: 泰坦尼克数据集中的字段
我们还提供了一些额外的上下文信息:
-
pclass
:这是社会经济地位的代理变量,其中头等舱为上层,中等舱为中层,三等舱为下层。 -
age
:如果年龄小于 1,这是一个分数值;例如,0.25表示 3 个月。如果年龄是估算的,通常以xx.5的形式表示。 -
sibsp
:兄弟姐妹定义为兄弟、姐妹、继兄或继姐,配偶定义为丈夫或妻子。 -
parch
:父母是指母亲或父亲,孩子是指女儿、儿子、继女或继子。只有与保姆一起旅行的儿童才不与父母一起旅行。因此,这一字段为0。 -
embarked
:登船地点是乘客登船的地点。
请注意,数据集提供的信息没有说明数据是如何收集的。survival
、pclass
和embarked
字段被称为类别变量,因为它们被分配到固定数量的标签或类别中,用于表示其他信息。例如,在embarked
字段中,C
标签表示乘客在谢尔堡登船,而survival
字段中的值1
表示他们在沉船事故中幸存。
练习 5: 加载和总结泰坦尼克数据集
在本练习中,我们将把泰坦尼克数据集读入 Python,并对其执行一些基本的总结操作:
-
使用简写符号导入 pandas 包,如下图所示:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_23.jpg
图 1.23:导入 pandas 包
-
在 Jupyter notebook 首页中点击
titanic.csv
文件以打开它:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_24.jpg图 1.24:打开 CSV 文件
该文件是 CSV 文件,可以视为一个表格,其中每一行是表格中的一行,每个逗号分隔表格中的列。幸运的是,我们不需要以原始文本形式处理这些表格,可以使用 pandas 将其加载:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_25.jpg
图 1.25:CSV 文件的内容
注意
花点时间查看 pandas 文档中的
read_csv
函数,地址:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html
。请注意,加载 CSV 数据到 pandas DataFrame 中有多种不同的选项。 -
在可执行的 Jupyter notebook 单元格中,执行以下代码来从文件加载数据:
df = pd.read_csv('Titanic.csv')
pandas DataFrame 类提供了一整套属性和方法,可以在其自身内容上执行,范围涵盖排序、过滤、分组方法到描述性统计分析、绘图和转换等功能。
注意
打开并阅读关于 pandas DataFrame 对象的文档,地址:
pandas.pydata.org/pandas-docs/stable/reference/frame.html
。 -
使用 DataFrame 的
head()
方法读取前五行数据:df.head()
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_26.jpg
图 1.26:读取前五行
在这个示例中,我们可以看到 DataFrame 中信息的可视化表示。我们可以看到数据是按表格形式组织的,几乎像一个电子表格。不同类型的数据按列组织,每个样本按行组织。每一行都有一个索引值,并且在 DataFrame 的左侧以粗体数字 0 到 4 显示。每一列都有一个标签或名称,如同在 DataFrame 顶部以粗体显示的那样。
将 DataFrame 看作一种电子表格是一个合理的类比;正如我们将在本章中看到的那样,我们可以像在电子表格程序中一样对数据进行排序、过滤和计算。虽然本章没有涉及,但有趣的是,DataFrame 还包含数据透视表功能,就像电子表格一样(pandas.pydata.org/pandas-docs/stable/reference/api/pandas.pivot_table.html
)。
练习 6:索引和选择数据
现在我们已经加载了一些数据,让我们使用 DataFrame 的选择和索引方法来访问一些感兴趣的数据:
-
以类似普通字典的方式选择单独的列,方法是使用列的标签,如下所示:
df['Age']
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_27.jpg
图 1.27:选择 Age 列
如果列名中没有空格,我们也可以使用点操作符。如果列名中有空格,则需要使用括号表示法:
df.Age
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_28.jpg
图 1.28:使用点操作符选择 Age 列
-
使用括号表示法一次选择多个列,如下所示:
df[['Name', 'Parch', 'Sex']]
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_29.jpg
图 1.29:选择多个列
-
使用
iloc
选择第一行:df.iloc[0]
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_30.jpg
图 1.30:选择第一行
-
使用
iloc
选择前三行:df.iloc[[0,1,2]]
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_31.jpg
图 1.31:选择前三行
-
我们还可以获取所有可用列的列表。按以下方式操作:
columns = df.columns # Extract the list of columns print(columns)
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_32.jpg
图 1.32:获取所有列
-
使用这个列列表和标准的 Python 切片语法来获取第 2、3 和 4 列及其对应的值:
df[columns[1:4]] # Columns 2, 3, 4
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_33.jpg
图 1.33:获取第二、第三和第四列
-
使用
len
操作符获取 DataFrame 中的行数:len(df)
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_34.jpg
图 1.34:获取行数
-
如果我们想要获取
Fare
列在第2
行的值,该怎么做呢?有几种不同的方法。首先,我们将尝试行中心方法。按以下方式操作:df.iloc[2]['Fare'] # Row centric
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_35.jpg
图 1.35:使用正常的行中心方法获取特定值
-
尝试使用点操作符来选择列。按以下方式操作:
df.iloc[2].Fare # Row centric
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_36.jpg
图 1.36:使用行中心点操作符获取特定值
-
尝试使用列中心方法。按以下方式操作:
df['Fare'][2] # Column centric
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_37.jpg
图 1.37:使用正常的列中心方法获取特定值
-
尝试使用列中心方法与点操作符。按以下方式操作:
df.Fare[2] # Column centric
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_38.jpg
图 1.38:使用列中心点操作符获取特定值
练习 7:高级索引和选择
在掌握了索引和选择的基础知识后,我们可以将注意力转向更高级的索引和选择。在这个练习中,我们将介绍一些执行高级索引和选择数据的重要方法:
-
为年龄小于 21 岁的乘客创建一个包含姓名和年龄的列表,如下所示:
child_passengers = df[df.Age < 21][['Name', 'Age']] child_passengers.head()
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_39.jpg
图 1.39:列出所有 21 岁以下乘客的姓名和年龄
-
计算儿童乘客的数量,如下所示:
print(len(child_passengers))
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_40.jpg
图 1.40:儿童乘客计数
-
计算年龄在 21 到 30 岁之间的乘客数量。不要使用 Python 的
and
逻辑运算符,而是使用与符号(&
)。操作如下:young_adult_passengers = df.loc[ (df.Age > 21) & (df.Age < 30) ] len(young_adult_passengers)
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_41.jpg
图 1.41:计算年龄在 21 到 30 岁之间的乘客
-
计算那些持有一等舱或三等舱票的乘客。再次提醒,我们不会使用 Python 的
or
逻辑运算符,而是使用管道符号(|
)。操作如下:df.loc[ (df.Pclass == 3) | (df.Pclass ==1) ]
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_42.jpg
图 1.42:计算那些持有一等舱或三等舱票的乘客
-
计算那些既没有持有一等舱也没有持有三等舱票的乘客人数。不要简单地选择二等舱票的持有者,而是使用
~
符号作为not
逻辑运算符。操作如下:df.loc[ ~((df.Pclass == 3) | (df.Pclass ==1)) ]
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_43.jpg
图 1.43:计算那些既没有持有一等舱也没有持有三等舱票的乘客
-
我们不再需要
Unnamed: 0
列,因此可以使用del
运算符将其删除:del df['Unnamed: 0'] df.head()
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_44.jpg
图 1.44:del 运算符
pandas 方法
现在我们已经掌握了一些 pandas 基础知识以及一些更高级的索引和选择工具,让我们来看一下其他 DataFrame 方法。有关所有可用方法的完整列表,我们可以参考类文档。
注
pandas 文档可以在pandas.pydata.org/pandas-docs/stable/reference/frame.html
找到。
现在你应该知道 DataFrame 中有哪些方法可用。本章内容太多,无法详细介绍所有方法,因此我们将选择一些方法,帮助你在监督学习中起步。
我们已经看过使用head()
方法,它可以提供 DataFrame 的前五行。如果我们希望选择更多或更少的行,可以通过提供行数作为参数来实现,如下所示:
df.head(n=20) # 20 lines
df.head(n=32) # 32 lines
另一个有用的方法是describe
,它是快速获取 DataFrame 中数据描述性统计信息的方式。我们接下来可以看到,对于 DataFrame 中的所有数值数据列(注意文本列已被省略),返回了样本量(count)、均值、最小值、最大值、标准差以及 25%、50%和 75%的分位数:
df.describe()
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_45.jpg
图 1.45:describe 方法
请注意,只有数值型数据列包含在汇总中。这个简单的命令为我们提供了很多有用的信息;查看count
的值(它统计有效样本的数量),我们可以看到Age
类别中有 1,046 个有效样本,Fare
中有 1,308 个,Survived
中只有 891 个。我们可以看到最年轻的人是 0.17 岁,平均年龄是 29.898 岁,最年长的是 80 岁。最低票价是£0,平均票价为£33.30,最贵票价为£512.33。如果我们查看Survived
列,我们有 891 个有效样本,均值为 0.38,这意味着大约 38%的人存活。
我们还可以通过调用 DataFrame 的各自方法,单独获取每一列的这些值,如下所示:
df.count()
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_46.jpg
图 1.46:count 方法
但我们有些列包含文本数据,如Embarked
、Ticket
、Name
和Sex
。那这些呢?我们怎么获取这些列的描述性信息呢?我们仍然可以使用describe
,只需要为其提供更多信息。默认情况下,describe
只会包含数值列,并计算第 25、第 50 和第 75 百分位数。但我们可以通过传递include = 'all'
参数来配置它,包含文本列,如下所示:
df.describe(include='all')
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_47.jpg
图 1.47:带有文本列的 describe 方法
现在好多了——我们有更多的信息了。查看Cabin
列,我们可以看到有 295 条记录,其中 186 个唯一值。最常见的值是C32
、C25
和C27
,它们出现了 6 次(根据freq
值)。类似地,如果我们查看Embarked
列,我们可以看到有 1,307 条记录,3 个唯一值,最常出现的值是S
,出现了 914 次。
请注意我们在describe
输出表格中出现的NaN
值。NaN
,即非数字,在 DataFrame 中非常重要,因为它们表示缺失或不可用的数据。pandas 库能够读取包含缺失或不完整信息的数据源既是一个优势也是一个弊端。许多其他库会在缺失信息的情况下直接无法导入或读取数据文件,而它能够读取数据也意味着必须适当地处理这些缺失的数据。
查看describe
方法的输出时,你应该注意到 Jupyter Notebook 将其呈现的方式与我们通过read_csv
读取的原始 DataFrame 相同。这样做是有充分理由的,因为describe
方法返回的结果本身就是一个 pandas DataFrame,因此它具有与从 CSV 文件中读取的数据相同的方法和特性。你可以通过 Python 内建的type
函数轻松验证这一点:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_48.jpg
图 1.48:检查类型
现在我们已经有了数据集的概览,接下来让我们深入一些,详细了解可用数据。
注意
对可用数据的全面理解在任何监督学习问题中都是至关重要的。数据的来源和类型、收集数据的方式以及收集过程中可能出现的任何错误都会影响最终模型的表现。
希望到目前为止,您已经能够熟练使用 pandas 提供数据的高层次概览。接下来我们将花些时间更深入地分析这些数据。
练习 8:拆分、应用和合并数据源
我们已经看到如何从 DataFrame 中索引或选择行或列,并使用高级索引技术根据特定标准过滤可用数据。另一个有用的方法是 groupby
方法,它提供了一种快速选择一组数据的方法,并通过 DataFrameGroupBy
对象提供了额外的功能:
-
使用
groupby
方法按Embarked
列对数据进行分组。Embarked
列有多少个不同的值?让我们来看一下:embarked_grouped = df.groupby('Embarked') print(f'There are {len(embarked_grouped)} Embarked groups')
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_49.jpg
图 1.49:按 Embarked 列对数据进行分组
-
groupby
方法到底做了什么?我们来看看。显示embarked_grouped.groups
的输出:embarked_grouped.groups
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_50.jpg
图 1.50:embarked_grouped.groups 的输出
在这里我们可以看到,三个组是
C
、Q
和S
,而embarked_grouped.groups
实际上是一个字典,字典的键是这些组,值是属于该组的行或索引。 -
使用
iloc
方法检查第1
行,并确认它属于 Embarked 组C
:df.iloc[1]
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_51.jpg
图 1.51:检查第 1 行
-
由于这些组是字典,我们可以遍历它们,并对各个组执行计算。计算每个组的平均年龄,如下所示:
for name, group in embarked_grouped: print(name, group.Age.mean())
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_52.jpg
图 1.52:使用迭代计算每个组的平均年龄
-
另一种选择是使用
aggregate
方法,简称agg
,并提供要应用到列上的函数。使用agg
方法来计算每个组的均值:embarked_grouped.agg(np.mean)
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_53.jpg
图 1.53:使用 agg 方法
那么,
agg
到底是如何工作的,我们可以传递什么类型的函数给它呢?在回答这些问题之前,我们需要首先考虑 DataFrame 中每列的数据类型,因为每列都会传递给此函数,以生成我们在此看到的结果。每个 DataFrame 由一组 pandas 系列数据列组成,这在许多方面类似于列表。因此,任何可以接受列表或类似可迭代对象并计算出一个单一值的函数,都可以与agg
一起使用。 -
例如,定义一个简单的函数,返回列中的第一个值,然后将该函数传递给
agg
:def first_val(x): return x.values[0] embarked_grouped.agg(first_val)
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_54.jpg
图 1.54:使用 agg 方法与函数
Lambda 函数
使用 Lambda 函数实现agg
是一种常见且实用的方法。
def
关键字。Lambda 函数本质上是为了方便而提供的,并不打算长期使用。Lambda 函数的标准语法如下(始终以lambda
关键字开头):
lambda <input values>: <computation for values to be returned>
练习 9:Lambda 函数
在本练习中,我们将创建一个 Lambda 函数,该函数返回列中的第一个值,并与agg
一起使用:
-
将
first_val
函数写为一个 Lambda 函数,并传递给agg
:embarked_grouped.agg(lambda x: x.values[0])
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_55.jpg
图 1.55:使用 agg 方法与 Lambda 函数
显然,我们得到了相同的结果,但请注意,Lambda 函数的使用更加便捷,尤其是考虑到它本意仅用于短时间的操作。
-
我们还可以通过列表将多个函数传递给
agg
,以便对整个数据集应用这些函数。传递 Lambda 函数以及 NumPy 的均值和标准差函数,如下所示:embarked_grouped.agg([lambda x: x.values[0], np.mean, np.std])
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_56.jpg
图 1.56:使用 agg 方法与多个 Lambda 函数
-
如果我们想对 DataFrame 中的不同列应用不同的函数怎么办?可以通过向
agg
传递一个字典,字典的键是需要应用函数的列,而值是相应的函数,从而将numpy.sum
应用到Fare
列,将 Lambda 函数应用到Age
列:embarked_grouped.agg({ 'Fare': np.sum, 'Age': lambda x: x.values[0] })
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_57.jpg
图 1.57:使用 agg 方法与包含不同列的字典
-
最后,您还可以使用多个列来执行
groupby
方法。向方法提供一个包含列(Sex
和Embarked
)的列表进行groupby
,如下所示:age_embarked_grouped = df.groupby(['Sex', 'Embarked']) age_embarked_grouped.groups
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_58.jpg
图 1.58:使用 groupby 方法与多个列
类似于我们仅通过 Embarked
列进行分组时的情况,我们可以看到这里返回的是一个字典,其中键是Sex
和Embarked
列的组合,作为元组返回。字典中的第一个键值对是一个元组 ('Male', 'S')
,值对应的是具有该特定组合的行的索引。对于Sex
和Embarked
列中每个唯一值的组合,都将有一个键值对。
数据质量考虑因素
在任何机器学习问题中,无论是监督学习还是无监督学习,数据的质量对最终模型的表现至关重要,应该在规划任何机器学习项目时放在首位。简单的经验法则是,如果你拥有干净的数据,数据量足够,而且输入数据类型与期望输出之间有良好的相关性,那么关于所选监督学习模型的类型和细节就变得不那么重要,仍然可以获得良好的结果。
然而,实际上这种情况很少发生。通常会涉及到一些关于数据量、数据质量或信噪比、输入和输出之间的相关性,或者这些因素的某种组合。因此,我们将利用本章的最后部分来讨论可能出现的一些数据质量问题以及一些解决这些问题的机制。之前我们提到过,在任何机器学习问题中,彻底理解数据集是至关重要的,尤其是在构建高性能模型时。当涉及到数据质量并试图解决数据中的一些问题时,这一点尤其重要。如果没有对数据集的全面了解,在数据清理过程中可能会引入额外的噪声或其他意外问题,从而导致性能进一步下降。
注意
关于 Titanic 数据集的详细描述以及其中包含的数据类型,详见 在 pandas 中加载数据 部分。如果你需要快速回顾这些细节,请现在回去查看。
处理缺失数据
正如我们之前讨论的,pandas 处理缺失数据的能力既是一个福音也是一个诅咒,这无疑是我们在继续开发监督学习模型之前需要管理的最常见问题。最简单的做法(但不一定是最有效的)是直接删除或忽略缺失数据的条目。我们可以通过 pandas 中 dropna
方法轻松实现这一点:
complete_data = df.dropna()
简单地丢弃缺失数据的行有一个非常重大的后果,那就是我们可能会丢失很多重要的信息。这在泰坦尼克数据集中非常明显,因为很多行都包含缺失数据。如果我们简单地忽略这些行,我们将从 1,309 个样本开始,最终只剩下 183 个样本。在仅使用不到 10%的数据的情况下,开发一个合理的监督学习模型将变得非常困难:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_59.jpg
图 1.59:行总数和含有 NaN 值的行总数
所以,除了早期的探索性阶段,简单地丢弃所有包含无效信息的行通常是不被接受的。不过,我们可以对此稍微做得更复杂一些。到底是哪些行缺失了信息?缺失信息的问题是某些特定列独有的,还是贯穿整个数据集的所有列?我们也可以使用aggregate
来帮助我们:
df.aggregate(lambda x: x.isna().sum())
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_60.jpg
图 1.60:使用 agg 与 Lambda 函数识别含有 NaN 值的行
现在,这很有用!我们可以看到,大多数缺失的信息都在Cabin
列中,一些在Age
列中,还有一点在Survived
列中。这是数据清洗过程中第一次我们可能需要做出有根据的判断。
我们该如何处理Cabin
列呢?这里缺失的信息太多,实际上可能无法以任何合理的方式使用它。我们可以尝试通过查看姓名、年龄和父母/兄弟姐妹的数量来恢复信息,看看能否将一些家庭联系起来提供信息,但这个过程会充满不确定性。我们也可以通过使用船舱的等级而不是具体的船舱号来简化该列,这可能与姓名、年龄和社会地位更好地相关。这是令人遗憾的,因为Cabin
与Survived
之间可能有很好的相关性,或许船舱较低层的乘客可能更难撤离。我们也可以仅查看包含有效Cabin
值的行,看看Cabin
条目是否具有任何预测能力;但现在,我们会暂时忽略Cabin
作为一个合理的输入(或特征)。
我们可以看到,Embarked
和Fare
列之间只有三个缺失的样本。如果我们决定需要Embarked
和Fare
列来进行建模,那么仅仅丢弃这些行是一个合理的做法。我们可以使用索引技巧来完成这项操作,其中~
表示not
操作,或者翻转结果(即,df.Embarked
不是NaN
并且df.Fare
不是NaN
):
df_valid = df.loc[(~df.Embarked.isna()) & (~df.Fare.isna())]
缺失的年龄值稍微有些复杂,因为有太多行缺少年龄值,不能仅仅把它们丢弃。但我们在这里有更多的选择,因为我们可以对一些合理的值有更多的信心来填充。最简单的选项是直接用数据集的平均年龄填补缺失的年龄值:
df_valid[['Age']] = df_valid[['Age']].fillna(df_valid.Age.mean())
这样做没问题,但可能有更好的方法来填充数据,而不是直接给所有 263 个人相同的值。记住,我们的目标是清理数据,以最大化输入特征的预测能力和生存率。虽然这种做法简单,但似乎不太合理。如果我们考虑每个类别(Pclass
)成员的平均年龄呢?这可能会给出一个更好的估算,因为从类别 1 到类别 3,平均年龄逐渐减少:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_61.jpg
图 1.61: 各个类别成员的平均年龄
如果我们考虑性别以及票种类(社会地位)呢?平均年龄在这里也有差异吗?让我们来看看:
for name, grp in df_valid.groupby(['Pclass', 'Sex']):
print('%i' % name[0], name[1], '%0.2f' % grp['Age'].mean())
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_62.jpg
图 1.62: 各性别和类别成员的平均年龄
我们可以看到,所有票种类的男性通常年龄较大。性别和票种类的组合提供了比简单地用平均年龄填补所有缺失值更高的分辨率。为了实现这一点,我们将使用transform
方法,它将一个函数应用于序列或数据框的内容,并返回一个包含转换后值的序列或数据框。将它与groupby
方法结合使用时尤其强大:
mean_ages = df_valid.groupby(['Pclass', 'Sex'])['Age'].\
transform(lambda x: x.fillna(x.mean()))
df_valid.loc[:, 'Age'] = mean_ages
这两行代码中有很多内容,所以让我们将其分解为几个部分。我们先看第一行:
mean_ages = df_valid.groupby(['Pclass', 'Sex'])['Age'].\
transform(lambda x: x.fillna(x.mean()))
我们已经熟悉df_valid.groupby(['Pclass', 'Sex'])['Age']
,它根据票种类和性别对数据进行分组,并仅返回Age
列。lambda x: x.fillna(x.mean())
Lambda 函数接受输入的 pandas 序列,并用该序列的均值填充NaN
值。
第二行将mean_ages
中的填充值赋给Age
列。请注意使用了loc[:, 'Age']
索引方法,这表示Age
列中的所有行都将被赋予mean_ages
中包含的值:
df_valid.loc[:, 'Age'] = mean_ages
我们已经描述了几种填补Age
列缺失值的方法,但这并不是一个详尽的讨论。我们还有很多其他方法可以用来填补缺失数据:我们可以为分组数据应用均值一个标准差范围内的随机值,也可以根据性别、父母/子女数量(Parch
)或兄弟姐妹数量,或者根据舱位、性别和父母/子女数量对数据进行分组。这个过程中最重要的决策是最终预测准确度的结果。在这个过程中,我们可能需要尝试不同的选项,重新运行模型并考虑其对最终预测准确度的影响。这是特征工程过程中一个重要的方面,即选择能为模型提供最大预测能力的特征或组件;在这个过程中,你会尝试不同的特征,运行模型,查看最终结果并重复,直到你对模型的表现感到满意。
这个监督学习问题的最终目标是根据我们可用的信息预测泰坦尼克号乘客的生还情况。因此,这意味着Survived
列提供了我们训练的标签。如果我们缺失了 418 个标签,我们该怎么办?如果这是一个我们可以控制数据收集并访问其来源的项目,我们显然可以通过重新收集数据或要求澄清标签来纠正这一点。在泰坦尼克号数据集中,我们无法做到这一点,因此必须做出另一个有根据的判断。我们可以尝试一些无监督学习技术,看看是否能发现一些可以用于生还信息的模式。然而,我们可能别无选择,只能忽略这些行。我们的任务是预测一个人是否生还,而不是他们是否可能生还。通过估算真实标签,我们可能会给数据集引入显著的噪音,降低我们准确预测生还情况的能力。
类别不平衡
缺失数据并不是数据集可能存在的唯一问题。类别不平衡——即某一类别或多个类别的样本数远多于其他类别——可能是一个显著的问题,特别是在分类问题中(我们将在第四章,分类中深入讨论分类问题),在这些问题中,我们试图预测一个样本属于哪个类别(或哪些类别)。查看我们的Survived
列,我们可以看到数据集中死亡人数(Survived
为0
)远多于生还人数(Survived
为1
):
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_63.jpg
图 1.63: 死亡与生还人数对比
如果我们不考虑类别不平衡,模型的预测能力可能会大大降低,因为在训练过程中,模型只需要猜测“该人未能幸存”就可以正确预测 61%(549 / (549 + 342))的时间。如果现实中的实际生存率是 50%,那么当模型应用于未见数据时,可能会过度预测未幸存的情况。
有几种方法可以用来管理类别不平衡,其中一种方法与缺失数据情境类似,就是随机从过多样本的类别中删除样本,直到达到平衡。再说一次,这个选项并不理想,甚至可能不合适,因为它涉及到忽略可用数据。一个更具建设性的例子是通过从数据集中随机复制样本,来对不足样本的类别进行过采样,从而增加样本数量。虽然删除数据可能会导致由于丢失有用信息而出现准确性问题,但对不足样本类别进行过采样可能会导致无法预测未见数据的标签,这也叫做过拟合(我们将在第五章,集成建模中讨论)。
向过采样数据的输入特征添加一些随机噪声可能有助于防止一定程度的过拟合,但这高度依赖于数据集本身。与缺失数据一样,检查任何类别不平衡修正对整体模型性能的影响非常重要。使用append
方法将更多数据复制到 DataFrame 中相对简单,它的工作方式与列表非常相似。如果我们想把第一行复制到 DataFrame 的末尾,我们可以这样做:
df_oversample = df.append(df.iloc[0])
样本量不足
机器学习领域可以看作是更大统计学领域的一个分支。因此,置信度和样本量的原理同样可以应用于理解小数据集的问题。回想一下,如果我们从一个具有高方差的数据源中采样,那么测量值的不确定性程度也会很高,并且为了对均值值达到指定的置信度,需要更多的样本。这些样本原理也可以应用到机器学习数据集中。那些具有最具预测力的特征方差的数据集通常需要更多的样本来获得合理的性能,因为也需要更多的置信度。
有几种技术可以用来弥补样本量不足的问题,例如迁移学习。然而,这些技术超出了本书的范围。不过,最终来说,使用小数据集能做的事情有限,显著的性能提升可能只有在样本量增加后才会出现。
活动 1:pandas 函数
在这个活动中,我们将测试自己在本章中学习到的各种 pandas 函数。我们将使用相同的 Titanic 数据集进行测试。
要执行的步骤如下:
-
打开一个新的 Jupyter 笔记本。
-
使用 pandas 加载泰坦尼克号数据集,并描述所有列的摘要数据。
-
我们不需要
Unnamed: 0
列。在练习 7:高级索引和选择中,我们展示了如何使用del
命令删除该列。我们还可以通过其他方式删除此列?不使用del
命令删除此列。 -
计算数据框列的均值、标准差、最小值和最大值,而不使用
describe
方法。 -
那么 33%、66%和 99%的四分位数呢?我们如何使用各自的方法得到这些值?使用
quantile
方法来完成此操作(pandas.pydata.org/pandas-docs/stable/reference/frame.html
)。 -
每个类别的乘客有多少?使用
groupby
方法找到答案。 -
每个类别的乘客有多少?通过选择/索引方法统计每个类别的成员数量,找到答案。
确认步骤 6和步骤 7的答案是否匹配。
-
确定第三类中最年长的乘客是谁。
-
对于许多机器学习问题,常常需要将数值缩放到 0 和 1 之间。使用带有 Lambda 函数的
agg
方法将Fare
和Age
列缩放到 0 和 1 之间。 -
数据集中有一名个体没有列出
Fare
值,可以通过以下方法找出。df_nan_fare = df.loc[(df.Fare.isna())] df_nan_fare
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_01_64.jpg
图 1.64:没有列出Fare
值的个体
使用groupby
方法,将主数据框中这一行的NaN
值替换为与相同类别和Embarked
位置对应的Fare
平均值。
注意
该活动的解决方案可以在第 300 页找到。
总结
本章介绍了监督学习机器学习的概念,并列举了一些应用案例,包括自动化手动任务,如识别 1960 年代和 1980 年代的发型。在此介绍中,我们接触到了标签数据集的概念,以及将一个信息集(输入数据或特征)映射到相应标签的过程。
我们采用了实际操作的方法,通过 Jupyter 笔记本和强大的 pandas 库加载和清洗数据。需要注意的是,本章仅涵盖了 pandas 功能的一小部分,实际上整个书籍都可以专门讨论该库。建议你熟悉 pandas 文档,并通过实践不断提升你的 pandas 技能。
本章的最后部分讨论了数据质量问题,这些问题在开发高效的监督学习模型时需要考虑,包括缺失数据、类别不平衡和样本量过小。我们讨论了多种处理这些问题的方法,并强调了根据模型表现检查这些缓解措施的重要性。
在下一章,我们将扩展我们所涵盖的数据清理过程,并将探讨数据探索和可视化过程。数据探索是任何机器学习解决方案中的关键环节,因为没有对数据集的全面了解,几乎不可能对提供的信息进行建模。
第二章:探索性数据分析与可视化
学习目标
到本章结束时,你将能够:
-
解释数据探索的重要性并传达数据集的总结性统计信息
-
可视化数据中缺失值的模式,并能够适当替换空值
-
识别连续特征和类别特征
-
可视化单个变量值的分布
-
使用相关性和可视化分析描述不同类型变量之间的关系
本章将带领我们进行对新数据集的探索与分析。
介绍
假设我们有一个问题陈述,涉及预测某次地震是否引发了海啸。我们如何决定使用什么模型?我们知道我们拥有的数据是什么吗?什么都不知道!但如果我们不了解数据,最终可能会构建一个既难以解释又不可靠的模型。
在数据科学中,深入理解我们处理的数据非常重要,这样可以生成高度信息量的特征,从而构建准确且强大的模型。
为了获得这种理解,我们对数据进行探索性分析,看看数据能告诉我们特征与目标变量之间的关系。了解数据甚至有助于我们解释构建的模型,并找出改善其准确性的方法。
我们实现这一目标的方法是让数据揭示其结构或模型,这有助于我们获得一些新的、常常是意想不到的数据洞察。让我们深入了解这种方法。
探索性数据分析(EDA)
探索性数据分析(EDA)被定义为一种分析数据集的方法,旨在总结其主要特征,通常采用可视化方法。
EDA 的目的:
-
发现数据集中的模式
-
发现异常
-
对数据的行为形成假设
-
验证假设
从基础的总结性统计数据到复杂的可视化图形,都帮助我们对数据本身形成直观的理解,这在形成新的假设和揭示哪些参数影响目标变量时非常重要。通常,通过发现目标变量如何随单一特征的变化而变化,可以帮助我们判断某个特征的重要性,而多个特征组合的变化则帮助我们构思新的有用特征。
大多数探索和可视化的目的是理解特征与目标变量之间的关系。因为我们想要找出我们拥有的数据和我们想要预测的值之间存在哪些(或不存在的)关系。
一些非常基本的领域知识通常是必要的,以便能够理解问题陈述本身以及数据所传达的信息。在本章中,我们将探索通过分析我们拥有的特征,了解更多关于数据的方式。
EDA 可以告诉我们:
-
不干净、缺失值或存在异常值的特征
-
具有信息价值并且是目标的良好指示器的特征
-
特征与目标之间的关系类型
-
数据可能需要的其他特征,而这些特征我们现在还没有
-
你可能需要单独处理的边缘情况
-
你可能需要在数据集上应用的过滤器
-
错误或虚假的数据点的存在
现在我们已经了解了为什么 EDA 很重要以及它能告诉我们什么,接下来让我们讨论一下 EDA 具体包括什么内容。EDA 可以包括从查看基本的摘要统计到可视化多个变量之间复杂趋势的任何操作。然而,即使是简单的统计数据和图表也可以是强大的工具,因为它们可能揭示出数据中的重要事实,这些事实可能会改变我们建模的视角。当我们看到表示数据的图表时,我们能够轻松地检测到趋势和模式,相比于仅仅处理原始数据和数字。这些可视化进一步让我们能够提出类似“如何?”和“为什么?”的问题,并对数据集形成假设,这些假设可以通过进一步的可视化来验证。这是一个持续的过程,能够帮助我们更深入地理解数据。本章将向你介绍一些可以用来探索任何数据集的基本工具,同时牢记最终的问题陈述。
我们将从一些基本的摘要统计量开始,介绍如何解读它们,接着是关于查找、分析和处理缺失值的部分。然后我们将研究单变量关系,即单个变量的分布和行为。最后,我们会探讨变量之间关系的探索部分。本章将向你介绍可以用来获得数据集及其特征基本概览的图表类型,以及如何通过创建结合多个特征的可视化来获得见解,我们还将通过一些示例展示它们如何使用。
我们将用于探索性分析和可视化的数据集来自 NOAA 的重大地震数据库,该数据集作为公共数据集可在 Google BigQuery 上获取(表 ID:'bigquery-public-data.noaa_significant_earthquakes.earthquakes'
)。我们将使用其中部分列,相关元数据可以在console.cloud.google.com/bigquery?project=packt-data&folder&organizationId&p=bigquery-public-data&d=noaa_significant_earthquakes&t=earthquakes&page=table
查看,并将其加载到 pandas DataFrame 中以进行探索。我们主要使用 Matplotlib 来进行大多数可视化,同时也会使用 Seaborn 和 Missingno 进行部分可视化。然而需要注意的是,Seaborn 只是对 Matplotlib 功能的封装,因此任何使用 Seaborn 绘制的图表都可以使用 Matplotlib 绘制。我们会通过混合使用两个库的可视化方式,来保持事情的趣味性。
探索和分析将以一个示例问题为基础进行:给定我们拥有的数据,我们希望预测地震是否引发了海啸。这将是一个分类问题(更多内容请见第四章,分类),其中目标变量是flag_tsunami
列。
练习 10:导入数据探索所需的库
在我们开始之前,首先导入我们将用于大多数数据操作和可视化的必要库:
-
在 Jupyter Notebook 中,导入以下库:
import json import pandas as pd import numpy as np import missingno as msno from sklearn.impute import SimpleImputer %matplotlib inline import matplotlib.pyplot as plt import seaborn as sns
%matplotlib inline
命令允许 Jupyter 在 Notebook 中直接显示图表。 -
我们还可以读取包含每列数据类型的元数据,这些数据类型以 JSON 文件的形式存储。可以使用以下命令完成此操作。该命令将以可读格式打开文件,并使用
json
库将文件读入字典:with open('dtypes.json', 'r') as jsonfile: dtyp = json.load(jsonfile)
现在,让我们开始吧。
汇总统计与中心值
为了了解我们的数据到底是什么样子,我们使用一种叫做数据分析的技术。数据分析被定义为检查现有信息源(例如数据库或文件)中的数据,并收集关于该数据的统计信息或信息摘要的过程。目标是确保你充分理解你的数据,并能够尽早识别数据可能带来的挑战,这通过总结数据集并评估其结构、内容和质量来实现。
数据分析包括收集描述性统计和数据类型。以下是一些常用的命令,可以用来获取数据集的总结信息:
-
data.info()
:此命令告诉我们每列中有多少非空值,并显示每列值的数据类型(非数值类型以object
类型表示)。 -
data.describe()
:该命令为 DataFrame 中所有数值列提供基本的总结统计数据,例如非空值的计数、最小值和最大值、均值和标准差,以及所有数值特征的四分位数百分位数。如果有任何字符串类型的特征,则不包括这些特征的总结。 -
data.head()
和data.tail()
:这两个命令分别显示 DataFrame 的前五行和后五行数据。虽然前面的命令可以给我们一个数据集的总体概览,但更深入地了解实际数据本身是一个好主意,可以使用这些命令来完成。
标准差
标准差表示 x 的值分布的广泛程度。
对于一组数值 xi,标准差由以下公式给出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_01.jpg
图 2.1:标准差公式
这里,𝝈 是标准差,N 是数据点的数量,𝝁 是均值。
假设我们有一组 10 个值,x = [0,1,1,2,3,4,2,2,0,1]。均值 𝝁 将是这些值的总和除以 10。也就是说,𝝁 = 1.6:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_02.jpg
图 2.2:x 的均方值
然后,标准差 = sqrt(14.4/10) = 1.2。
百分位数
对于一组值,n**th 百分位数是大于该值的数值所占比例为 n% 的数据点。例如,第 50th 百分位数是数据集中大于该值和小于该值的元素数量相同的值。此外,数据集的第 50 百分位数也称为其中位数,第 25 百分位数和第 75 百分位数也称为下四分位数和上四分位数。
假设我们有与之前相同的 10 个值,x = [0,1,1,2,3,4,2,2,0,1]。我们先对这个值列表进行排序。排序后,得到 x = [0,0,1,1,1,2,2,2,3,4]。为了找到第 25 百分位数,我们首先计算该值所在的索引:i = (p/100) * n),其中 p = 25,n = 10。然后,i = 2.5。
由于 i 不是整数,我们将其四舍五入为 3,并取排序列表中的第三个元素作为第 25 百分位数。给定列表中的第 25 百分位数是 1,即我们排序后的列表中的第三个元素。
练习 11:我们数据集的总结统计
在本练习中,我们将使用之前了解过的总结统计函数,获取我们数据集的基本情况:
-
将地震数据读入一个
data
pandas DataFrame,并使用在上一练习中通过json
库读取的dtyp
字典来指定 CSV 中每一列的数据类型:data = pd.read_csv('earthquake_data.csv', dtype=dtyp)
-
使用
data.info()
函数来获取数据集的概览:data.info()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_03.jpg
图 2.3:数据集概览
-
打印数据集的前五行和最后五行。前五行打印如下:
data.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_04.jpg
图 2.4:前五行
最后五行打印如下:
data.tail()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_05.jpg
图 2.5:最后五行
我们可以在这些输出中看到,共有 28 列,但并不是所有列都显示出来。只显示了前 10 列和最后 10 列,省略号表示中间还有其他未显示的列。
-
使用
data.describe()
查找数据集的摘要统计信息。运行data.describe().T
:data.describe().T
在这里,
.T
表示我们正在对应用的 DataFrame 进行转置操作,即将列转换为行,反之亦然。将其应用于describe()
函数,可以使我们更容易地查看输出,因为转置后的 DataFrame 中每一行现在都对应于单个特征的统计数据。我们应该得到如下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_06.jpg
图 2.6:摘要统计
请注意,这里的 describe()
函数仅显示具有数值的列的统计信息。这是因为我们无法为具有非数值的列计算统计信息。
缺失值
当某个数据点的特征没有记录值(即缺失值)时,我们称数据缺失。在真实的数据集中出现缺失值是不可避免的;没有数据集是完美的。然而,理解为什么数据会缺失,以及是否有某种因素影响了数据丢失是很重要的。理解和认识到这一点可以帮助我们以合适的方式处理剩余数据。例如,如果数据缺失是随机的,那么剩余数据很可能仍能代表总体。然而,如果缺失的数据不是随机的,我们却假设它是随机的,可能会偏向我们的分析和后续建模。
让我们来看一下缺失数据的常见原因(或机制):
-
完全随机缺失(MCAR):如果数据集中的缺失值与任何其他记录的变量或外部参数之间没有任何相关性,则该值被认为是 MCAR。这意味着剩余的数据仍然能够代表总体,尽管这种情况很少发生,并且将缺失数据视为完全随机通常是一个不现实的假设。
例如,在一项研究中,研究 K12 儿童肥胖的原因,MCAR 是父母忘记带孩子去诊所参加研究的情况。
-
随机缺失 (MAR):如果数据缺失的情况与已记录的数据相关,而不是与未记录的数据相关,那么数据被认为是 MAR。由于无法通过统计方法验证数据是否为 MAR,我们只能依赖于其是否为合理的可能性。
在 K12 研究中,缺失数据是由于父母搬到其他城市,导致孩子不得不退出研究;缺失性与研究本身无关。
-
缺失非随机 (MNAR):既不是 MAR 也不是 MCAR 的数据被称为 MNAR。这是一个不可忽略的非响应情况,也就是说,缺失的变量值与其缺失的原因相关。
继续以案例研究为例,如果父母对研究的性质感到反感,不希望孩子受到欺负,因此他们将孩子从研究中撤出,那么数据将是 MNAR。
寻找缺失值
既然我们已经了解了熟悉数据缺失原因的重要性,接下来我们来讨论如何在数据集中找到这些缺失值。对于 pandas DataFrame,通常使用 .isnull()
方法创建空值掩码(即一个布尔值的 DataFrame),用以指示空值的位置——在任意位置的 True
值表示空值,而 False
值表示该位置存在有效值。
注意
.isnull()
方法可以与 .isna()
方法互换使用。对于 pandas DataFrame,这两个方法的功能完全相同——之所以有两个方法实现相同的功能,是因为 pandas DataFrame 最初是基于 R DataFrame 的,因此复用了很多 R 中的语法和思想。
数据缺失是否随机可能不会立刻显现:通过两种常见的可视化技术,可以发现数据集中各特征之间缺失值的性质:
-
空值矩阵:这是一种数据密集的展示方式,能帮助我们快速可视化数据补全中的模式。它让我们一眼看到每个特征(以及跨特征)的空值分布、数量以及它们与其他特征出现的频率。
-
空值相关热图:该热图形象地描述了每对特征之间的空值关系(或数据完整性关系),即它衡量了一个变量的存在或缺失对另一个变量存在的影响强度。
与常规相关性类似,空值相关性值的范围从 -1 到 1:前者表示一个变量出现时另一个变量肯定不出现,后者则表示两个变量同时存在。值为 0 表示一个变量的空值对另一个变量为空没有影响。
练习 12:可视化缺失值
我们来分析缺失值的性质,首先查看每个特征的缺失值数量和百分比,然后使用 Python 中的 missingno
库绘制空值矩阵和相关性热图:
-
计算每列缺失值的数量和百分比,并按降序排列。我们将使用
.isnull()
函数在 DataFrame 上获取掩码。每列的空值数量可以通过对掩码 DataFrame 使用.sum()
函数来获得。同样,空值的比例可以通过对掩码 DataFrame 使用.mean()
函数得到,并乘以 100 转换为百分比。然后,我们使用
pd.concat()
函数将空值的总数和百分比合并成一个单一的 DataFrame,并按空值的百分比对行进行排序,最后打印出 DataFrame:mask = data.isnull() total = mask.sum() percent = 100*mask.mean() missing_data = pd.concat([total, percent], axis=1,join='outer', keys=['count_missing', 'perc_missing']) missing_data.sort_values(by='perc_missing', ascending=False, inplace=True) missing_data
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_07.jpg
图 2.7:每列缺失值的数量和百分比
在这里,我们可以看到
state
、total_damage_millions_dollars
和damage_millions_dollars
列的缺失值超过 90%,这意味着数据集中这些列中不到 10% 的数据点是可用的。另一方面,year
、flag_tsunami
、country
和region_code
列没有缺失值。 -
绘制空值矩阵。首先,我们使用
.any()
函数在上一步的掩码 DataFrame 上查找包含任何空值的列列表。然后,我们使用missingno
库绘制空值矩阵,针对数据集中随机抽取的 500 个数据点,仅绘制那些包含缺失值的列:nullable_columns = data.columns[mask.any()].tolist() msno.matrix(data[nullable_columns].sample(500)) plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_08.jpg
图 2.8:空值矩阵
在这里,黑色线条代表非空值,而白色线条则表示该列中存在空值。通过一眼看去,
location_name
似乎完全填充(我们从之前的步骤知道,这一列实际上只有一个缺失值),而latitude
和longitude
列大多完整,但有一些空白。右侧的火花线总结了数据完整性的总体形状,并指出数据集中空值最多和最少的行。
-
绘制空值相关性热图。我们将使用
missingno
库绘制数据集的空值相关性热图,仅针对那些包含空值的列:msno.heatmap(data[nullable_columns], figsize=(18,18)) plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_09.jpg
图 2.9:空值相关性热图
在这里,我们还可以看到一些标记为injuries
和total_injuries
的框,这告诉我们有一些记录分别包含这两个值中的一个,但不同时包含这两个值。这类情况需要特别关注——如果这些变量之间的相关性较高,那么拥有两个值并没有意义,两个值中的一个可以被删除。
缺失值的插补策略
处理列中缺失值的方法有多种。最简单的方法是直接删除缺失值所在的行;然而,这样做可能会导致丢失其他列中的有价值信息。另一个选项是插补数据,即用从已知部分数据推断出的有效值替代缺失值。常见的插补方法列举如下:
-
创建一个新的值,该值不同于其他值,用来替代列中的缺失值,以便完全区分这些行。然后,使用非线性机器学习算法(如集成模型或支持向量机)来区分这些值。
-
使用列中的适当中心值(均值、中位数或众数)来替换缺失值。
-
使用模型(例如 K 近邻或高斯混合模型)来学习替换缺失值的最佳值。
Python 有一些函数对于用静态值替换列中的空值非常有用。实现这一功能的一种方法是使用内建的 pandas .fillna(0)
函数:在插补中没有歧义——用来替代列中空数据点的静态值即为传递给函数的参数(括号中的值)。
然而,如果列中空值的数量较多,并且无法立即明确可以用来替换每个空值的适当中心值,那么我们可以选择删除包含空值的行,或从建模的角度完全删除该列,因为它可能不会带来任何显著的价值。这可以通过在 DataFrame 上使用.dropna()
函数来完成。可以传递给该函数的参数包括:
-
axis
:此参数定义了是删除行还是删除列,通过将参数分别赋值为 0 或 1 来确定。 -
how
:可以将all
或any
的值赋给此参数,以指示行/列是否应包含所有空值以删除该列,或者是否至少有一个空值时删除该列。 -
thresh
:此参数定义了行/列必须具有的最小空值数量,才会被删除。
此外,如果无法为分类特征的空值确定合适的替代值,可以考虑在特征中创建一个新的类别来表示空值,而不是删除该列。
注意
如果从直观理解或领域知识上立即能够看出如何替换列的空值,那么我们可以当场替换这些值。然而,在许多情况下,这些推断会在探索过程的后期变得更加明显。在这些情况下,我们可以根据找到的合适方法,随时替换空值。
练习 13:使用 pandas 填充
让我们查看缺失值,并将它们替换为零,针对那些具有至少一个空值的基于时间的(连续)特征(如月份、日期、小时、分钟和秒)。我们这样做是因为对于没有记录值的情况,可以安全地假设事件发生在时间段的开始。
-
创建一个列表,包含我们想要填充值的列名:
time_features = ['month', 'day', 'hour', 'minute', 'second']
-
使用
.fillna()
填充空值。我们将使用 pandas 的内建.fillna()
函数,将这些列中的缺失值替换为0
,并将0
作为参数传递给函数:data[time_features] = data[time_features].fillna(0)
-
使用
.info()
函数查看填充列的空值计数:data[time_features].info()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_10.jpg
图 2.10:空值计数
如今,我们可以看到,数据框中所有特征的值都已经是非空值。
练习 14:使用 scikit-learn 填充
让我们使用 scikit-learn 的 SimpleImputer
类,替换与描述相关的分类特征中的空值。在练习 12:可视化缺失值中,我们看到几乎所有这些特征中超过 50% 的值都是空值。将这些空值替换为中心值可能会对我们试图构建的模型产生偏差,使它们变得不相关。因此,我们将空值替换为一个单独的类别,值为 NA
:
-
创建一个列表,包含我们想要填充值的列名:
description_features = [ 'injuries_description', 'damage_description', 'total_injuries_description', 'total_damage_description' ]
-
创建一个
SimpleImputer
类的对象。在这里,我们首先创建一个imp
对象,并使用表示如何填充数据的参数初始化它。我们将传递给对象初始化的参数包括:missing_values
:这是缺失值的占位符,即所有出现在missing_values
参数中的值将被填充。strategy
:这是填充策略,可以是mean
、median
、most_frequent
(即众数)或constant
。前三者只能用于数值数据,并会通过指定的中心值替换每列中的缺失值,而最后一个则会根据fill_value
参数,用常数替换缺失值。fill_value
:指定用来替换所有missing_values
的值。如果保持默认设置,当填充数值数据时,填充值将为0
,而字符串或对象数据类型将使用missing_value
字符串。imp = SimpleImputer(missing_values=np.nan, strategy='constant', fill_value='NA')
-
执行填充。我们将使用
imp.fit_transform()
实际执行填充。它将带有空值的 DataFrame 作为输入,并返回填充后的 DataFrame:data[description_features] = imp.fit_transform(data[description_features])
-
使用
.info()
函数查看填充列的空值计数:data[description_features].info()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_11.jpg
图 2.11:空值计数
练习 15:使用推断值进行填充
让我们用来自类别 damage_description
特征的信息替换连续的 damage_millions_dollars
特征中的空值。尽管我们可能不知道确切的损失金额,但类别特征为我们提供了因地震造成的损失金额的范围信息:
-
找出有多少行的
damage_millions_dollars
值为空,以及其中有多少行的damage_description
值不为空:print(data[pd.isnull(data.damage_millions_dollars)].shape[0]) print(data[pd.isnull(data.damage_millions_dollars) & (data.damage_description != 'NA')].shape[0])
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_12.jpg
图 2.12:包含空值的行数
如我们所见,5,594 个空值中有 3,849 个可以通过另一个变量轻松替换。
-
找出每个类别的平均
damage_millions_dollars
值。由于damage_description
中的每个类别代表一系列值,我们从已存在的非空值中找到每个类别的平均damage_millions_dollars
值。这些为该类别提供了合理的最可能值估算:category_means = data[['damage_description', 'damage_millions_dollars']].groupby('damage_description').mean() category_means
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_13.jpg
图 2.13:每个类别的平均 damage_millions_dollars 值
-
将平均值存储为字典。在此步骤中,我们将包含平均值的 DataFrame 转换为字典(Python
dict
对象),这样访问它们就更加方便。此外,由于新创建的
NA
类别(在前一个练习中的填充值)的值为NaN
,并且0
类别的值缺失(数据集中没有damage_description
等于0
的行),我们也明确地将这些值添加到字典中:replacement_values = category_means.damage_millions_dollars.to_dict() replacement_values['NA'] = -1 replacement_values['0'] = 0 replacement_values
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_14.jpg
图 2.14:平均值字典
-
创建一系列替代值。对于
damage_description
列中的每个值,我们使用map
函数将类别值映射到平均值。.map()
函数用于将列中的键映射到replacement_values
字典中每个元素的对应值:imputed_values = data.damage_description.map(replacement_values)
-
替换列中的空值。我们通过使用
np.where
作为三元运算符来实现:第一个参数是掩码,第二个是如果掩码为正时从中获取值的系列,第三个是如果掩码为负时从中获取值的系列。这确保了
np.where
返回的数组仅将damage_millions_dollars
中的空值替换为imputed_values
序列中的值:data['damage_millions_dollars'] = np.where(condition=data.damage_millions_dollars.isnull(), x=imputed_values, y=data.damage_millions_dollars)
-
使用
.info()
函数查看填充列的空值计数:data[['damage_millions_dollars']].info()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_15.jpg
图 2.15:空值计数
我们可以看到,替换后 damage_millions_dollars
列中没有空值。
活动 2:汇总统计与缺失值
在本活动中,我们将回顾本章中到目前为止看到的一些汇总统计和缺失值分析。我们将使用一个新的数据集,来自 Kaggle 的 House Prices: Advanced Regression Techniques 比赛(可以在 www.kaggle.com/c/house-prices-advanced-regression-techniques/data
或 GitHub 上的 github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python
获取)。虽然在练习中使用的地震数据集是针对解决分类问题的(当目标变量只有离散值时),我们将在活动中使用的数据集将用于解决回归问题(当目标变量包含连续值的范围时)。我们将使用 pandas 函数生成汇总统计,并通过空值矩阵和空值相关热图可视化缺失值。
执行的步骤如下:
-
读取数据。
-
使用 pandas 的
.info()
和.describe()
方法查看数据集的汇总统计信息。 -
查找每列的缺失值总数和缺失值百分比,并按缺失百分比的降序显示至少有一个空值的列。
-
绘制空值矩阵和空值相关热图。
-
删除缺失值超过 80% 的列。
-
将
FireplaceQu
列中的空值替换为NA
值。注意
本活动的解决方案可以在第 307 页找到。
值的分布
在本节中,我们将查看各个变量的行为——它们取什么值,这些值的分布如何,以及如何以可视化方式表示这些分布。
目标变量
目标变量可以是连续值(回归问题的情况)或离散值(分类问题的情况)。本章讨论的问题是预测地震是否引发海啸,也就是 flag_tsunami
变量,它只有两个离散值——因此是一个分类问题。
可视化有多少地震引发了海啸,以及有多少没有引发海啸的一种方法是柱状图,其中每个柱子代表一个离散值,柱子的高度等于具有相应离散值的数据点的计数。这为我们提供了每个类别的绝对计数的良好比较。
练习 16:绘制柱状图
让我们看看数据集中有多少地震引发了海啸。我们将通过对列使用value_counts()
方法,并直接对返回的 pandas 系列使用.plot(kind='bar')
函数来完成。按照以下步骤操作:
-
使用
plt.figure()
初始化绘图:plt.figure(figsize=(8,6))
-
接下来,输入我们的主要绘图命令:
data.flag_tsunami.value_counts().plot(kind='bar')
-
设置显示参数并显示图表:
plt.ylabel('Number of data points') plt.xlabel('flag_tsunami') plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_16.jpg
图 2.16:柱状图显示有多少地震引发了海啸
从这个柱状图中,我们可以看到大多数地震没有引发海啸,而且不到三分之一的地震引发了海啸。这显示出数据集略微失衡。
让我们更仔细地看一下这些 Matplotlib 命令的作用:
-
plt.figure(figsize=(8,6))
:此命令定义了我们的图表大小,通过提供宽度和高度的值。这是所有绘图命令之前的第一条命令。 -
plt.xlabel()
和plt.ylabel()
:这些命令接受字符串作为输入,允许我们指定图表中X轴和Y轴的标签。 -
plt.show()
:这是绘图时写入的最后一条命令,它将图表以内联方式显示在 Jupyter Notebook 中。
类别数据
类别变量是那些具有离散值,表示不同类别或观察水平的变量,可以是字符串对象或整数值。例如,我们的目标变量flag_tsunami
是一个类别变量,具有两个类别:Tsu
和No
。
类别变量可以分为两种类型:
-
location_name
。这个变量的值不能说是有序的,也就是说,一个地点并不大于另一个地点。类似的变量示例还包括颜色、鞋类类型、种族类型等。 -
damage_description
,因为每个值表示逐渐增加的损害值。另一个例子可以是星期几,其值从星期一到星期天,具有一定的顺序关系,我们知道星期四在星期三之后,但在星期五之前。尽管有序变量可以通过对象数据类型表示,但它们通常也表示为数值数据类型,这通常使得它们与连续变量之间的区分变得困难。
处理数据集中的类别变量时面临的主要挑战之一是高基数,即大量的类别或不同的值,其中每个值在数据集中的出现次数相对较少。例如,location_name
具有大量的唯一值,每个值在数据集中的出现频率较低。
此外,非数值型的类别变量总是需要某种形式的预处理,将其转换为数值格式,以便机器学习模型能够读取并进行训练。在没有丢失上下文信息的情况下,如何将类别变量编码为数值型是一个挑战。尽管这些信息对于人类来说(由于领域知识或常识)非常容易理解,但计算机却很难自动理解。例如,像国家或地点名称这样的地理特征本身并不能表明不同值之间的地理接近性,但这可能是一个重要特征——如果发生在东南亚地区的地震比欧洲地区的地震触发更多海啸呢?仅仅通过数值编码特征,无法捕获这些信息。
练习 17:类别变量的数据类型
让我们找出地震数据集中哪些变量是类别型的,哪些是连续型的。正如我们现在所知道的,类别变量也可以具有数值值,因此,拥有数值数据类型并不意味着变量就是连续型的:
-
找出所有数值型和对象型的列。我们在 DataFrame 上使用
.select_dtypes()
方法,创建一个包含数值型(np.number
)和类别型(np.object
)列的子集 DataFrame,然后打印每个列的列名。对于数值列,请使用以下方法:numeric_variables = data.select_dtypes(include=[np.number]) numeric_variables.columns
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_17.jpg
图 2.17:所有数值型的列
对于类别列,请使用以下方法:
object_variables = data.select_dtypes(include=[np.object]) Object_variables.columns
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_18.jpg
图 2.18:所有对象类型的列
这里显然可以看出,对象类型的列是类别变量。为了区分数值列中的类别变量和连续变量,让我们查看这些特征的唯一值数量。
-
找出数值特征的唯一值数量。我们在 DataFrame 上使用
select_dtypes
方法,找到每一列的唯一值数量,并将结果序列按升序排序。对于数值列,请使用以下方法:numeric_variables.nunique().sort_values()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_19.jpg
图 2.19:数值特征的唯一值数量
对于类别列,请使用以下方法:
object_variables.nunique().sort_values()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_20.jpg
图 2.20:分类列的唯一值数量
对于数值型变量,我们可以看到前九个变量的唯一值显著少于其余行,这些变量很可能是分类变量。然而,我们必须记住,其中一些可能只是具有较小范围的四舍五入值的连续变量。另外,month
和 day
在这里不会被视为分类变量。
练习 18:计算类别值计数
对于具有分类值的列,查看该特征的唯一值(类别)以及这些类别的频率将非常有用,也就是说,每个不同的值在数据集中出现的次数。我们来找出 injuries_description
分类变量中每个 0
到 4
标签和 NaN
值的出现次数:
-
对
injuries_description
列使用value_counts()
函数来找出每个类别的频率。使用value_counts
会以降序形式返回每个值的频率,并以 pandas 系列的形式显示:counts = data.injuries_description.value_counts(dropna=False) counts
输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_21.jpg
图 2.21:每个类别的频率
-
按照顺序变量的升序排序这些值。如果我们希望按值本身的顺序显示频率,我们可以重置索引,从而得到一个 DataFrame,并按索引(即顺序变量)排序值:
counts.reset_index().sort_values(by='index')
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_22.jpg
图 2.22:排序后的值
练习 19:绘制饼图
由于我们示例数据中的目标变量是分类的,练习 16:绘制条形图 中的示例展示了可视化分类值分布的一种方式(使用条形图)。另一种可以帮助我们轻松查看每个类别在整个数据集中所占比例的图表是饼图。让我们绘制一个饼图来可视化 damage_description
变量的离散值分布:
-
将数据格式化成需要绘制的形式。在这里,我们对该列使用
value_counts()
并按索引排序系列:counts = data.damage_description.value_counts() counts = counts.sort_index()
-
绘制饼图。
plt.pie()
分类函数使用计数数据绘制饼图。我们将按照 练习 16:绘制条形图 中描述的相同三步进行绘图:plt.figure(figsize=(10,10)) plt.pie(counts, labels=counts.index) plt.title('Pie chart showing counts for\ndamage_description categories') plt.show()
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_23.jpg
图 2.23:显示 damage_description
类别计数的饼图
连续数据
连续变量可以取任意数量的值,通常是整数(例如,死亡人数)或浮动数据类型(例如,山脉的高度)。了解特征中值的基本统计信息是非常有用的:describe()
函数的输出显示的最小值、最大值和百分位数给我们提供了一个合理的估算。
然而,对于连续变量来说,了解其在操作范围内的分布情况也非常有用。由于我们不能简单地计算各个值的计数,我们会将值按升序排列,将其分组为等间隔的区间,然后计算每个区间的计数。这为我们提供了底层的频率分布,绘制该分布便能得到直方图,从而让我们查看数据的形态、中心值以及变异性。
直方图为我们提供了一个简洁的视角,帮助我们了解正在查看的数据。它们让我们一目了然地看到数据的行为,揭示了底层分布(例如正态分布或指数分布)、异常值、偏度等信息。
注意
很容易将条形图与直方图混淆。主要的区别在于,直方图用于绘制已被分组的连续数据以可视化频率分布,而条形图可以用于多种其他用途,包括表示我们之前处理的类别变量。此外,条形图中的条形高度表示该箱子的频率,但直方图中的宽度也会影响频率的表示,这在条形图中并不适用。
最常见的频率分布之一是高斯(或正态)分布。这是一种对称分布,具有钟形曲线,表示接近中间值的范围在数据集中出现频率最高,随着远离中间部分,频率对称性地减少。
它是一个概率分布,曲线下的面积等于一。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_24.jpg
图 2.24:正态分布
偏度
如果一个分布不是对称的,我们称其为偏斜的,偏度衡量的是变量相对于其均值的非对称性。偏度的值可以是正值、负值(或未定义)。在前一种情况下,尾巴位于分布的右侧,而后一种情况则表示尾巴位于左侧。
然而,必须注意的是,厚而短的尾部对偏度的影响与长而细的尾部相同。
峰度
峰度是衡量变量分布的尾部形态的一个指标,用来衡量一侧尾部是否存在异常值。较高的峰度值表示尾部较厚,且存在异常值。与偏度的概念类似,峰度也描述了分布的形态。
练习 20:绘制直方图
让我们使用 Seaborn 库绘制eq_primary
特征的直方图:
-
使用
plt.figure()
来启动绘图:plt.figure(figsize=(10,7))
-
sns.distplot()
是我们用来绘制直方图的主要命令。第一个参数是用来绘制直方图的单维数据,bins 参数定义了箱子的数量和大小。使用方式如下:sns.distplot(data.eq_primary.dropna(), bins=np.linspace(0,10,21))
-
使用
plt.show()
显示图表:plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_25.jpg
图 2.25:eq_primary 特征的直方图
该图给出了一个标准化(或归一化)直方图,这意味着直方图下方的区域总和为 1。此外,直方图上的曲线是核密度估计,它给我们提供了变量的概率分布的形态。
从图中我们可以看到,eq_primary
的值大多位于 5 到 8 之间,这意味着大多数地震的震级是中等到高值,几乎没有地震震级非常低或非常高。
练习 21:偏度与峰度
让我们使用 pandas 核心函数计算数据集中所有特征的偏度和峰度值:
-
使用
.skew()
数据框方法计算所有特征的偏度,然后按升序排序值:data.skew().sort_values()
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_26.jpg
图 2.26:数据集中所有特征的偏度值
-
使用
.kurt()
数据框方法计算所有特征的峰度:data.kurt()
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_27.jpg
图 2.27:数据集中所有特征的峰度值
在这里,我们可以看到某些变量的峰度值显著偏离 0。这意味着这些列具有长尾。但是,这些变量尾部的值(即表示死亡、受伤人数以及损失金额的数值)在我们的案例中,可能是离群值,我们可能需要特别关注它们。较大的值可能实际上表示额外的因素,增加了由地震引起的破坏,即海啸。
活动 3:可视化表示值的分布
在这个活动中,我们将复习上一节关于不同类型数据的内容。我们将使用与活动 2:摘要统计和缺失值中相同的数据集,即 房价:高级回归技术(可以在www.kaggle.com/c/house-prices-advanced-regression-techniques/data
或在 GitHub 上找到 github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python
)。我们将使用不同类型的图表来可视化表示该数据集的值分布。
执行的步骤如下:
-
使用 Matplotlib 绘制目标变量
SalePrice
的直方图。 -
找出每个对象类型列中唯一值的数量。
-
创建一个数据框,表示
HouseStyle
列中每个类别值的出现次数。 -
绘制一个饼图,表示这些计数。
-
查找每个具有数字类型的列中唯一值的数量。
-
使用 Seaborn 绘制
LotArea
变量的直方图。 -
计算每列值的偏度和峰度值。
注意
此活动的解决方案可以在第 312 页找到。
数据中的关系
找到数据中变量之间关系的重要性有两个原因:
-
找出哪些特征可能是重要的可以被认为是至关重要的,因为找到与目标变量有强烈关系的特征将有助于特征选择过程。
-
找到不同特征之间的关系是有用的,因为数据集中的变量通常不可能完全独立于其他所有变量,而这可能会以多种方式影响我们的建模。
现在,我们有许多方法可以可视化这些关系,具体方法取决于我们试图找到关系的变量类型,以及我们考虑作为方程或比较的一部分的变量数量。
两个连续变量之间的关系
找到两个连续变量之间的关系,基本上是看一个变量的值增加时另一个变量如何变化。最常见的可视化方法是使用散点图,其中我们将每个变量沿一个轴(当我们有两个变量时,在二维平面中的X和Y轴)绘制,并使用标记在X-Y平面中绘制每个数据点。这种可视化能够很好地展示这两个变量之间是否存在某种关系。
然而,如果我们想量化两个变量之间的关系,最常用的方法是找到它们之间的相关性。如果目标变量是连续的,并且与另一个变量高度相关,这表明该特征在模型中是一个重要部分。
皮尔逊相关系数
皮尔逊相关系数是一种常用的相关系数,用来显示一对变量之间的线性关系。公式返回一个介于-1 和+1 之间的值,其中:
-
+1 表示强正相关
-
-1 表示强负相关
-
0 表示没有关系
同样,找到特征对之间的相关性也很有用。尽管高度相关的特征的存在不会使模型变差,但它们也不一定会使任何模型变得更好。为了简化起见,最好从一组高度相关的特征中只保留一个。
注意
在拟合线性模型时,特征之间高度相关可能会导致模型不可预测且变化幅度较大。这是因为线性模型中每个特征的系数可以解释为在保持其他特征不变的情况下,目标变量的单位变化。然而,当一组特征不独立(即存在相关性)时,我们无法确定每个特征对目标变量的独立变化所造成的影响,导致系数变化幅度较大。
要找到 DataFrame 中每个数值特征与其他特征的成对相关性,可以在 DataFrame 上使用.corr()
函数。
练习 22:绘制散点图
让我们绘制主震震中震级(X轴)与对应的受伤人数(Y轴)之间的散点图:
-
过滤掉空值。由于我们知道两列中都有空值,首先让我们过滤数据,只保留非空行:
data_to_plot = data[~pd.isnull(data.injuries) & ~pd.isnull(data.eq_primary)]
-
创建并显示散点图。我们将使用 Matplotlib 的
plt.scatter(x=..., y=...)
作为绘制数据的主要命令。x
和y
参数指定哪个特征应沿哪个轴绘制。它们接受单一维度的数据结构,如列表、元组或 pandas 系列。我们还可以向scatter
函数传递更多参数,例如指定绘制单个数据点时使用的图标。例如,要使用红色的交叉符号作为图标,我们需要传递以下参数:marker='x', c='r'
:plt.figure(figsize=(12,9)) plt.scatter(x=data_to_plot.eq_primary, y=data_to_plot.injuries) plt.xlabel('Primary earthquake magnitude') plt.ylabel('No. of injuries') plt.show()
输出结果将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_28.jpg
图 2.28:散点图
从图表中我们可以推断出,虽然受伤人数和震中震级之间似乎没有明显的趋势,但随着震级的增加,受伤人数较多的地震数量也在增加。然而,对于大多数地震而言,似乎没有明显的关系。
练习 23:相关性热力图
让我们使用 Seaborn 的sns.heatmap()
函数绘制数据集中所有数值变量之间的相关性热力图,该函数基于数据集中的特征间相关性值。
传递给sns.heatmap()
函数的可选参数是square
和cmap
,它们分别表示绘制的图表中每个像素应为正方形,并指定要使用的颜色方案:
-
绘制一个包含所有特征的基本热力图:
plt.figure(figsize = (12,10)) sns.heatmap(data.corr(), square=True, cmap="YlGnBu") plt.show()
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_29.jpg
图 2.29:相关性热力图
从图表右侧的颜色条中我们可以看到,最小值大约为
-0.2
,对应的颜色是最浅的,这在某种程度上误导了相关性值的表示,实际相关性值范围应该是从-1 到 1。 -
在一个更自定义的热图中绘制特征的子集。我们将使用
vmin
和vmax
参数指定上下限,并使用带有注释的热图重新绘制,注释显示特征对之间的相关系数值。我们还将更改颜色方案,使其更易于解读——中性色白色表示无相关性,而越来越深的蓝色和红色分别表示更高的正相关性和负相关性:feature_subset = [ 'focal_depth', 'eq_primary', 'eq_mag_mw', 'eq_mag_ms', 'eq_mag_mb', 'intensity', 'latitude', 'longitude', 'injuries', 'damage_millions_dollars', 'total_injuries', 'total_damage_millions_dollars'] plt.figure(figsize = (12,10)) sns.heatmap(data[feature_subset].corr(), square=True, annot=True, cmap="RdBu", vmin=-1, vmax=1) plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_30.jpg
图 2.30:自定义的相关性热图
现在,虽然我们可以计算相关系数的值,但这仅仅提供了线性关系的指示。为了更好地判断是否存在可能的依赖关系,我们可以绘制特征对之间的散点图,这在变量之间的关系不明确时特别有用,因为通过可视化数据点的分布,我们可以大致判断这两个变量是否(以及如何)相关。
练习 24:Pairplot
pairplot 对于同时可视化多个特征对之间的关系非常有用,可以使用 Seaborn 的 .pairplot()
函数绘制。在这个练习中,我们将查看数据集中具有最高成对相关性的特征之间的 pairplot:
-
定义一个包含要创建 pairplot 的特征子集的列表:
feature_subset = [ 'focal_depth', 'eq_primary', 'eq_mag_mw', 'eq_mag_ms', 'eq_mag_mb', 'intensity', 'latitude', 'longitude', 'injuries', 'damage_millions_dollars', 'total_injuries', 'total_damage_millions_dollars']
-
使用 Seaborn 创建 pairplot。传递给绘图函数的参数是:
kind='scatter'
,表示我们希望网格中每一对变量之间的单独图形以散点图的形式展示;diag_kind='kde'
,表示我们希望对角线上的图形(即两变量相同的位置)为核密度估计图。此外,还应该注意,对角线对称的图形本质上是相同的,只不过坐标轴被反转了:
plt.figure(figsize = (12,10)) sns.heatmap(data[feature_subset].corr(), square=True, annot=True, cmap="RdBu", vmin=-1, vmax=1) plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_31.jpg
图 2.31:具有最高成对相关性的特征之间的 pairplot
连续型变量与类别型变量之间的关系
当一个变量是类别型变量,另一个是连续型变量时,查看它们之间关系的一种常见方法是使用条形图或箱线图。
-
条形图有助于比较一个变量在一组离散参数中的值,是最常见的图形类型之一。每根条形代表一个类别值,条形的高度通常表示该类别下连续变量的聚合值(如平均值、总和或该类别中连续变量的值的计数)。
-
箱型图是一个矩形,用来表示每个离散类别变量的连续变量的分布。它不仅能够有效地展示异常值,还可以让我们比较不同类别变量下连续变量的分布。矩形的下边缘和上边缘分别代表第一和第三四分位数,矩形中间的线代表中位数,而矩形上下的点(或离群点)代表异常值。
练习 25:柱状图
让我们使用柱状图来可视化每个强度级别的地震所造成的海啸总数:
-
预处理
flag_tsunami
变量。在使用flag_tsunami
变量之前,我们需要预处理它,将No
值转换为零,将Tsu
值转换为一。这将为我们提供二元目标变量。为此,我们使用.loc
操作符设置列中的值,:
表示需要为所有行设置值,第二个参数指定要设置值的列名称:data.loc[:,'flag_tsunami'] = data.flag_tsunami.apply(lambda t: int(str(t) == 'Tsu'))
-
删除所有
intensity
值为 null 的行,去掉我们要绘制的数据:subset = data[~pd.isnull(data.intensity)][['intensity','flag_tsunami']]
-
查找每个
intensity
级别的海啸总数并显示数据框。为了将数据转换为可视化柱状图的格式,我们需要按每个强度级别对行进行分组,然后对flag_tsunami
值进行求和,以获得每个强度级别的海啸总数:data_to_plot = subset.groupby('intensity').sum() data_to_plot
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_32.jpg
图 2.32:每个强度级别的海啸总数
-
使用 Matplotlib 的
plt.bar(x=..., height=...)
方法绘制柱状图,该方法需要两个参数,一个指定绘制柱形的位置(x
值),另一个指定每个柱形的高度。这两者都是一维数据结构,必须具有相同的长度:plt.figure(figsize=(12,9)) plt.bar(x=data_to_plot.index, height=data_to_plot.flag_tsunami) plt.xlabel('Earthquake intensity') plt.ylabel('No. of tsunamis') plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_33.jpg
图 2.33:柱状图
从这个图中,我们可以看到,随着地震强度的增加,造成的海啸数量也增加,但在强度超过 9 之后,海啸的数量似乎突然下降。
想想为什么会发生这种情况。或许只是因为高强度的地震较少,因此海啸也较少。或者可能是完全独立的因素;也许高强度的地震历史上发生在陆地上,无法引发海啸。探索数据以找出原因。
练习 26:箱型图
在本练习中,我们将绘制一个箱型图,表示具有至少 100 次地震的国家中eq_primary
的变化:
-
查找发生超过 100 次地震的国家。我们将查找数据集中所有国家的计数值。然后,我们将创建一个仅包含计数大于 100 的国家的系列:
country_counts = data.country.value_counts() top_countries = country_counts[country_counts > 100] top_countries
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_34.jpg
图 2.34:地震超过 100 次的国家
-
对 DataFrame 进行子集化,筛选出前面集合中包含的国家的行。为了筛选这些行,我们使用
.isin()
方法在 pandas 系列上选择包含传递为参数的类似数组对象中的值的行:subset = data[data.country.isin(top_countries.index)]
-
创建并显示箱形图。绘制数据的主要命令是
sns.boxplot(x=..., y=..., data=..., order=)
。x
和y
参数是 DataFrame 中要绘制在每个坐标轴上的列名——前者假定是类别变量,后者是连续变量。data
参数指定要从中获取数据的 DataFrame,order
参数接受一个类别名称列表,指示在 X 轴上显示类别的顺序:plt.figure(figsize=(15, 15)) sns.boxplot(x='country', y="eq_primary", data=subset, order=top_countries.index) plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_35.jpg
图 2.35:箱形图
两个类别变量之间的关系
当我们只关注一对类别变量以查找它们之间的关系时,最直观的方式是基于第一个类别将数据进行划分,然后再根据第二个类别变量进一步细分,查看结果的计数以找到数据点的分布。虽然这可能看起来有些混乱,但一种常见的可视化方式是使用堆叠条形图。与常规的条形图一样,每个条形图表示一个类别值。但每个条形图将再次被细分为颜色编码的子类别,这可以指示在主类别中有多少数据点落入每个子类别(即第二个类别)。类别数较多的变量通常被认为是主类别。
练习 27:堆叠条形图
在本练习中,我们将绘制一个堆叠条形图,表示每个强度级别发生的海啸数量:
-
查找落入每个
intensity
和flag_tsunami
分组值中的数据点数量:grouped_data = data.groupby(['intensity', 'flag_tsunami']).size() grouped_data
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_36.jpg
图 2.36:每个分组的强度和 flag_tsunami 中落入的数据点
-
对结果 DataFrame 使用
.unstack()
方法,将一级索引(flag_tsunami
)作为列:data_to_plot = grouped_data.unstack() data_to_plot
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_37.jpg
图 2.37:一级索引
-
创建堆叠条形图。我们首先使用
sns.set()
函数来指示我们希望使用 Seaborn 作为可视化库。然后,我们可以轻松地使用 pandas 中的原生.plot()
函数,通过传递kind='bar'
和stacked=True
参数来绘制堆叠条形图:sns.set() data_to_plot.plot(kind='bar', stacked=True, figsize=(12,8)) plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_02_38.jpg
图 2.38:堆叠条形图
现在,该图表让我们能够可视化和解释每个强度级别导致海啸的地震比例。在练习 25: 条形图中,我们看到大于 9 级的地震所导致的海啸数量有所下降。从这个图中,我们可以确认,主要是因为超过 10 级的地震数量减少了;而 11 级的地震所导致的海啸比例甚至有所增加。
活动 4: 数据内的关系
在本次活动中,我们将回顾上一节中关于数据关系的学习内容。我们将使用与活动 2: 汇总统计和缺失值相同的数据集,即房价:高级回归技术(可在www.kaggle.com/c/house-prices-advanced-regression-techniques/data
或 GitHub 上的github.com/TrainingByPackt/Applied-Supervised-Learning-with-Python
找到)。我们将使用不同的图表来突出显示数据集中的值之间的关系。
要执行的步骤如下:
-
绘制数据集的相关性热图。
-
绘制一个更紧凑的热图,使用以下特征子集并带有相关性值的注释:
feature_subset = [ 'GarageArea', 'GarageCars','GarageCond','GarageFinish','GarageQual','GarageType', 'GarageYrBlt','GrLivArea','LotArea','MasVnrArea','SalePrice' ]
-
显示相同特征子集的配对图,KDE 图位于对角线位置,散点图位于其他地方。
-
创建一个箱型图,显示
SalePrice
在每个GarageCars
类别中的变化。 -
使用 Seaborn 绘制折线图,显示旧公寓和新建公寓的
SalePrice
变化。注意
本次活动的解决方案可以在第 319 页找到。
概述
在本章中,我们首先讨论了为什么数据探索是建模过程中的重要部分,它不仅有助于对数据集进行预处理,还能帮助我们设计具有信息量的特征并提高模型的准确性。本章不仅侧重于对数据集及其特征的基本概览,还通过创建结合多个特征的可视化来获得洞见。
我们研究了如何使用 pandas 的核心功能找到数据集的汇总统计数据。我们还研究了如何发现缺失值,并讨论了它们的重要性,同时学习如何使用 Missingno 库分析这些缺失值,以及使用 pandas 和 scikit-learn 库填补缺失值。
接下来,我们研究了如何研究数据集中变量的单变量分布,并通过条形图、饼图和直方图等可视化方式展示这些分布。最后,我们学习了如何探索变量之间的关系,并了解到它们可以通过散点图、热图、箱型图、堆积条形图等形式进行表示。
在接下来的章节中,我们将开始探索监督式机器学习算法。现在我们已经了解了如何探索我们拥有的数据集,我们可以进入建模阶段。下一章将介绍回归:一种主要用于构建连续目标变量模型的算法类别。
第三章:回归分析
学习目标
在本章结束时,您将能够:
-
描述回归模型,并解释回归与分类问题的区别
-
解释梯度下降的概念,它在线性回归问题中的应用,以及如何应用到其他模型架构中
-
使用线性回归为x-y平面上的数据构建线性模型
-
评估线性模型的性能,并利用评估结果选择最佳模型
-
使用特征工程为构建更复杂的线性模型创建虚拟变量
-
构建时间序列回归模型,使用自回归方法
本章涵盖了回归问题和分析,向我们介绍了线性回归以及多元线性回归、梯度下降和自回归。
介绍
在前两章中,我们介绍了 Python 中监督机器学习的概念,以及加载、清理、探索和可视化原始数据源所需的基本技术。我们讨论了指定输入和所需输出之间相关性的关键性,以及初始数据准备过程有时可能占据整个项目所花费时间的大部分。
在本章中,我们将深入探讨模型构建过程,并使用线性回归构建我们的第一个监督机器学习解决方案。所以,让我们开始吧。
回归与分类问题
我们在第一章、Python 机器学习工具包中讨论了两种不同的方法:监督学习和无监督学习。监督学习问题旨在将输入信息映射到已知的输出值或标签,但在此基础上,还有两个进一步的子类别需要考虑。监督学习和无监督学习问题都可以进一步细分为回归问题或分类问题。本章的主题是回归问题,它们旨在预测或建模连续值,例如预测明天的气温(摄氏度)或确定图像中人脸的位置。相反,分类问题则不同,它们预测的是某个输入属于预定类别中的某一个,而不是返回一个连续值。第一章、Python 机器学习工具包中的示例监督学习问题,即我们想要预测一个假发是来自 1960 年代还是 1980 年代,就是一个很好的监督分类问题的例子。在这个例子中,我们试图预测一个假发是否来自两个不同的类别:类别 1 为 1960 年代,类别 2 为 1980 年代。其他分类问题包括预测泰坦尼克号的乘客是否幸存或死亡,或者经典的 MNIST 问题(yann.lecun.com/exdb/mnist/
)。MNIST 是一个包含 70,000 个标注过的手写数字图像的数据库,数字范围为 0 到 9。MNIST 分类任务的目标是,从 70,000 张输入图像中挑选一张,预测或分类图像中的数字 0-9。因此,模型必须预测该图像属于 10 个类别中的哪一个。
数据、模型、训练和评估
在我们深入探讨回归问题之前,我们将首先审视创建任何机器学习模型所涉及的四个主要阶段,无论是监督回归还是其他类型的模型。这些阶段如下:
-
数据准备
-
模型架构的规范
-
训练过程的设计与执行
-
训练模型的评估
建议确保你完全理解这个流程以及本节中描述的内容,因为每个阶段对实现高效或合理的系统性能都至关重要。我们将在第一章、Python 机器学习工具包的背景下,考虑这些阶段如何应用于假发分类问题。
数据准备
管道的第一阶段是数据准备,这是第一章、《Python 机器学习工具包》的一个重要组成部分,因此在本节中不再进一步分析。然而,重要的是要理解数据规范、收集和清理/整理过程的关键性。如果输入数据是次优的,我们不能期望能够产生一个高性能的系统。关于数据质量,有一句常见的格言是垃圾进,垃圾出。如果你输入的是垃圾数据,那么你得到的结果也将是垃圾。在我们的假发示例中,我们希望样本量至少在几百个,理想情况下是几千个,并且这些样本已正确标记为 1960 年代或 1980 年代的样本。我们不希望样本被错误标记,或者根本不属于这两个时代。
模型架构
第二阶段是模型架构规范,在本章中将进行更详细的描述。该阶段定义了将要使用的模型类型,以及组成模型本身的参数的类型和值。模型本质上是一个数学方程,用于定义输入数据与期望结果之间的关系。与任何方程一样,模型由变量和常量组成,并通过一组过程进行组合,例如加法、减法或卷积。模型参数的性质和值会根据选择的模型类型以及模型能够描述所观察关系的复杂性水平而有所不同。较简单的模型将包含较少的参数,并对其值有更大的约束,而更复杂的模型则可能包含更多的参数,并且这些参数可能会发生变化。在本章中,我们将使用一个线性模型,与一些其他模型(如卷积神经网络模型,它可能包含超过一百万个需要优化的参数)相比,线性模型是较简单的。这种简单性不应被误认为是缺乏能力,或者无法描述数据中的关系,而仅仅是意味着可调参数较少(即调整这些参数的值以优化性能)。
模型训练
系统管道的第三个阶段是训练过程的设计与执行,即通过这种机制来确定模型参数的值。在监督学习问题中,我们可以将训练过程类比为一个学生在课堂中的学习过程。在典型的课堂环境中,老师已经知道给定问题的答案,并尝试向学生展示如何根据一组输入来解决问题。在这种情况下,学生就是模型,参数就像学生大脑中的知识,是学生正确解答问题的手段。
训练过程是教师用来训练学生正确回答问题的方法;该方法可以根据学生的学习能力和理解能力进行调整和变化。一旦模型架构被定义(即班级中的学生),训练过程就会提供所需的指导和约束,以接近最优解。就像一些学生在不同的学习环境中表现更好一样,模型也如此。因此,还存在一组额外的参数,称为超参数,它们虽然不在模型内部用于根据某些输入数据集进行预测,但它们被定义、使用并调优,旨在通过指定的成本(或误差)函数(例如,均方根误差)优化模型的性能。我们将在本章中更详细地讨论超参数,但目前,最简单的理解方式是将超参数视为决定模型实际参数的环境。
模型评估
管道的最终阶段是模型评估,它产生最终的性能指标。这是我们知道模型是否值得发布、是否优于先前版本,或是否在不同的编程语言或开发环境中有效迁移的机制。我们将在第六章,模型评估中更详细地介绍这些指标,因此在此阶段不会详细讨论。只需记住,无论选择哪种验证技术,它都需要能够持续报告并独立地衡量模型在数据集上的性能表现。再次以我们的假发数据集为例,评估阶段将查看模型在给定假发图像和已知年代标签的情况下,达成多少个正确预测。
线性回归
我们将从选择线性模型开始研究回归问题。线性模型因其直观性而成为一个很好的首选,同时也具有强大的预测能力,前提是数据集在输入特征和数值之间存在某种线性或多项式关系。线性模型的直观性通常来源于能够将数据绘制成图表,并观察数据中的趋势模式,例如输出(数据的y轴值)随着输入(x轴值)的变化呈正向或负向趋势。虽然通常不会以这种方式呈现,线性回归模型的基本组成部分也常常是在高中数学课程中学习的。你可能还记得,直线或线性模型的方程式定义如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_01.jpg
图 3.1:直线方程
这里,x 是输入值,y 是对应的输出或预测值。模型的参数是由 m 定义的线的梯度或斜率(y 值的变化除以 x 值的变化),以及 y 截距值 b,它表示线与 y 轴的交点。通过这样的模型,我们可以提供 m 和 b 参数的值来构建线性模型。例如,y = 2x + 1,其斜率为 2,表示 y 值的变化速度是 x 的两倍;这条线与 y 截距相交于 1:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_02.jpg
图 3.2:直线的参数
所以,我们了解了定义直线所需的参数,但这实际上并没有做什么特别有趣的事情。我们只是规定了模型的参数来构建一条线。我们真正想做的是,拿一个数据集并构建一个最好描述数据集的模型。正如之前提到的,这个数据集需要在输入特征和输出值之间有某种线性关系的近似。为此,我们创建了一个合成数据集,记录了从 1841 年到 2010 年的空气温度数据,这些数据可以在本书附带的代码包中找到,或者在 GitHub 上查看 github.com/TrainingByPackt/Supervised-Learning-with-Python
。这个数据集包含的值旨在演示本章的主题,不应与来自科学研究的实际数据混淆。
练习 28:使用移动平均绘制数据
正如我们在第一章中讨论的,Python 机器学习工具包,如果要构建一个高性能的模型,彻底理解所使用的数据集是至关重要的。所以,考虑到这一点,让我们利用这个练习加载、绘制并查询数据源:
-
导入
numpy
、pandas
和matplotlib
包,并使用替代名称:Import numpy as np import pandas as pd import matplotlib.pyplot as plt
-
使用 pandas 的
read_csv
函数加载包含synth_temp.csv
数据集的 CSV 文件,然后显示前五行数据:df = pd.read_csv('synth_temp.csv') df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_03.jpg
图 3.3:前五行数据
-
由于我们只对 1901 年至 2010 年的数据感兴趣,因此需要删除 1901 年之前的所有行:
df = df.loc[df.Year > 1901] df.head()
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_04.jpg
图 3.4:删除所有 1901 年之前的行后的前五行数据
-
原始数据集包含每年多个温度测量值,较晚年份的测量值更多(2010 年有 12 个),较早年份的测量值较少(1841 年有 6 个);然而,我们感兴趣的是每年的平均温度列表。按年份对数据进行分组,并使用 DataFrame 的
agg
方法来计算每年的平均值:df_group_year = df.groupby('Year').agg(np.mean) df_group_year.head()
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_05.jpg
图 3.5:每年平均数据
-
鉴于数据相当嘈杂,移动平均滤波器将提供总体趋势的有用指标。移动平均滤波器仅计算最后 N 个值的平均值,并将此平均值分配给 (N+1) 个样本。使用 10 年的窗口计算温度测量的移动平均信号的值:
window = 10 rolling = df_group_year.AverageTemperature.rolling(window).mean() rolling.head(n=20)
我们将获得以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_06.jpg
图 3.6:移动平均信号的值
注意,前 9 个样本是
NaN
,这是由于移动平均滤波器窗口的大小。窗口大小为 10,因此需要 9(10-1)个样本来生成第一个平均值,因此前 9 个样本为NaN
。 -
最后,绘制每年的测量值以及移动平均信号:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_07.jpg
图 3.7:年均空气温度
图 3.7 是此练习的预期输出,是每年平均陆地温度测量的图表,并显示了 10 年移动平均趋势。通过查看此图,我们可以立即得出几个有趣的观察结果。第一个观察结果是,从 1901 年到大约 1960 年间温度保持相对稳定,之后呈增加趋势,直到数据截至于 2010 年。其次,测量中存在相当多的散点或噪声。
活动 5:绘制带有移动平均线的数据
对于此活动,我们获取了奥斯汀,德克萨斯州的天气信息数据集(austin_weather.csv
),该数据集可在附带的源代码中找到,并将查看平均日温度的变化。我们将为此数据集绘制移动平均滤波器。
在开始之前,我们需要导入一些库,可以按如下方式完成:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
需要执行的步骤如下:
-
从 CSV 文件中将数据集加载到 pandas DataFrame 中。
-
我们只需要
Date
和TempAvgF
列;将数据集中的所有其他列删除。 -
最初,我们只对第一年的数据感兴趣,因此需要仅提取该信息。在 DataFrame 中为年份值创建一列,并从
Date
列中的字符串中提取年份值作为整数,并将这些值分配给Year
列。注意,温度是每天记录的。
-
重复此过程以提取月份值,并将这些值存储为
Month
列中的整数。 -
将第一年的数据复制到一个 DataFrame 中。
-
计算 20 天的移动平均滤波器。
-
绘制原始数据和移动平均信号,其中 x 轴是年中的日期编号。
注意
此活动的解决方案可在第 325 页找到。
最小二乘法
机器学习和人工智能领域本质上是统计学的一个专业分支,因此,时不时地反思这些起源是很重要的,这有助于我们深入理解模型如何作为预测工具被应用。通过回顾机器学习如何从统计学中发展而来,并对比今天更现代的方法,亦是很有趣的。线性回归模型就是一个很好的例子,它既可以用来展示一些经典的求解方法,如最小二乘法,也可以展示一些更现代的方法,如梯度下降法,这些我们将在本章中讨论。线性模型还具有额外的优势,它包含了高中数学中常见的概念,例如直线方程,为描述拟合数据的方法提供了一个有用的平台。
求解线性模型的传统方法是通过诸如 scikit-learn、SciPy、Minitab 和 Excel 等工具包执行的最小二乘法,这也是我们将要讨论的第一个方法。参考我们的标准直线方程(图 3.1),m 是直线的斜率或梯度,c 是 y 轴偏移。通过首先确定平均的 x 和 y 值(分别表示为 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_01.png 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_02.png),在最小二乘法中这些值可以直接计算出来。计算出平均值后,我们可以通过将 x 值的差与平均值的差以及 y 值的差与平均值的差相乘,再除以 x 与平均值的差的平方,来计算梯度 m。然后,可以通过使用新计算的 m 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_01.png 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_02.png 来解出偏移 b。数学上可以表示如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_08.jpg
图 3.8:最小二乘法
我们可以从更实际的角度来理解这一点,回顾一下梯度实际上就是纵向(或 y)值的变化除以横向(或 x)值的变化。在年均气温随时间变化的背景下,我们可以看到我们在计算的是每个温度值与平均值的差的总和,乘以每个时间值与平均值的差。通过将结果除以时间差的平方和,得出的趋势梯度即为完成,从而为温度随时间的模型提供部分数据。
现在,我们不需要手动计算这些值,虽然这并不困难。但像 SciPy 和 scikit-learn 这样的专业库可以帮助我们完成这项工作,并且关注一些细节,例如计算效率。对于本节内容,我们将使用 scikit-learn 作为我们的首选库,因为它提供了很好的 scikit-learn 接口入门。
需要注意的一个实现细节是,scikit-learn 的线性回归模型实际上是 SciPy 普通最小二乘法函数的封装,并提供了一些附加的便捷方法:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_09.jpg
图 3.9:scikit-learn 的线性回归实现
scikit-learn 模型 API
scikit-learn API 使用一个相对简单的代码模式,无论构建的是何种类型的模型。简单来说,模型必须首先定义所有与训练或拟合过程相关的超参数。在定义模型时,会返回一个模型对象,然后在模型构建的第二阶段——训练或拟合时使用该对象。调用模型对象的fit
方法并提供适当的训练数据,即可使用定义的超参数训练模型。我们现在将使用这个模式来构建我们的第一个线性回归模型。
练习 29:使用最小二乘法拟合线性模型
在这个练习中,我们将使用最小二乘法构建我们的第一个线性回归模型。
-
我们将在这个练习中使用 scikit-learn 的
LinearRegression
模型,因此从 scikit-learn 的linear_regression
模块中导入该类:from sklearn.linear_model import LinearRegression
-
使用默认值构建线性回归模型;即,计算 y 截距的值并且不对输入数据进行归一化:
model = LinearRegression() model
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_10.jpg
图 3.10:线性回归模型
-
现在我们准备好将模型拟合或训练到数据上了。我们将提供年份作为输入,年均温度作为输出。请注意,scikit-learn 模型的
fit
方法期望提供二维数组作为X
和Y
的值。因此,年份或索引值需要调整为适合该方法的形状。使用.values
方法获取索引值,并将其调整为((-1, 1))
的形状,这样就变成了一个 N x 1 数组。NumPy 形状定义中的值-1
表示该值由数组的当前形状和目标形状推断得出:model.fit(df_group_year.index.values.reshape((-1, 1)), gf_group_year.AverageTemperature)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_11.jpg
图 3.11:fit 方法的输出
-
通过打印
model.coef_
(即 m 的值)和model.intercept_
(即 y 截距的值),获取模型的参数:print(f'm = {model.coef_[0]}') print(f'c = {model.intercept_}') print('\nModel Definition') print(f'y = {model.coef_[0]:0.4}x + {model.intercept_:0.4f}')
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_12.jpg
图 3.12:模型系数和模型截距的输出
-
现在我们已经生成了模型,可以预测一些值来构建趋势线。那么,让我们使用第一个、最后一个和平均年份的值作为输入来预测当地温度。用这些值构建一个 NumPy 数组并将其命名为
trend_x
。完成后,将trend_x
的值传递给模型的predict
方法来获取预测值:trend_x = np.array([ df_group_year.index.values.min(), df_group_year.index.values.mean(), df_group_year.index.values.max() ]) trend_y = model.predict(trend_x.reshape((-1, 1))) trend_y
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_13.jpg
图 3.13:数组显示最小值、均值和最大值
-
现在绘制由模型生成的趋势线,并将模型参数叠加到之前的图表上,包含原始数据:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(trend_x, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_14.jpg
图 3.14:线性回归 – 第一个简单的线性模型
现在我们有了模型,需要评估其性能,以了解它与数据的拟合程度,并与其他可能生成的模型进行比较。我们将在第六章《模型评估》中详细讨论这个主题,我们将探讨验证和交叉验证的方法,但目前我们将计算模型与数据集之间的R-squared值。R-squared,通常在统计建模中报告,是预测值与实际值之间的平方和与实际值与其均值之间的平方和之比。完美的拟合将有一个 R² 值为 1,而随着性能的下降,分数会降低至 0。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_15.jpg
图 3.15:R-squared 分数
我们可以使用score
方法计算 R² 值:
# Note the year values need to be provided as an N x 1 array
r2 = model.score(df_group_year.index.values.reshape((-1, 1)), df_group_year.AverageTemperature)
print(f'r2 score = {r2:0.4f}')
我们将得到如下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_16.jpg
图 3.16:模型与数据集的 R-squared 分数
因此,观察图 3.14中的趋势线,我们可以看到线性模型表现良好。在 1960 年后的移动平均线性区域,它的表现明显更好,但对于 1970 年之前的数据仍有改进的空间。我们能做些什么来处理这个问题吗?似乎两个单独的线性模型可能比一个模型表现得更好。1960 年前的数据可以作为一个模型,1960 年后的数据作为另一个模型?我们可以这样做,直接将数据分开,创建两个独立的模型,分别评估它们,然后将它们以分段的方式合并起来。但我们也可以通过使用虚拟变量,在现有模型中加入类似的特征。
注意
在继续之前,需要注意的是,在报告机器学习模型的表现时,不得使用用于训练模型的数据来评估模型表现,因为这会给出模型表现的乐观看法。我们将在第六章《模型评估》中讨论验证的概念,包括评估和报告模型表现。然而,为了本章的目的,我们将使用训练数据来检查模型的表现;只需记住,一旦你完成了第六章《模型评估》,你将能更好地理解这一点。
活动 6:使用最小二乘法进行线性回归
对于本活动,我们将使用前一个活动中使用的德克萨斯州奥斯丁的天气数据集。我们将使用最小二乘法为该数据集绘制线性回归模型。
在我们开始之前,我们需要导入一些库并加载来自前一个活动的数据,方法如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
df_first_year = df[:365]
rolling = pd.read_csv('activity2_rolling.csv')
window = 20
需要执行的步骤如下:
-
可视化测量值。
-
可视化滚动平均值。
-
使用默认参数创建线性回归模型,也就是说,为模型计算 y 截距,并且不对数据进行归一化处理。
-
现在拟合模型,其中输入数据为年份中的天数(1 到 365),输出为平均温度。为了便于后续计算,插入一列(
DayOfYear
),对应于该测量的年份中的天数。 -
使用
DayOfYear
值作为输入,df_first_year.TempAvgF
作为输出,来拟合模型。 -
打印模型的参数。
-
让我们检查模型提供的趋势线。只需使用线性方程中的第一个、中间和最后的值(年份中的天数)来绘制即可。
-
绘制带有趋势线的值。
-
评估模型的表现。
-
让我们检查模型拟合数据的情况。计算 r2 得分来了解。
注意
本活动的解答可以在第 329 页找到。
带虚拟变量的线性回归
虚拟变量是我们可以通过现有数据集提供的信息引入模型中的分类变量。这些变量的设计和选择被视为特征工程的一部分,具体结果会根据变量的选择而有所不同。我们之前观察到,移动平均值从大约 1960 年开始持续上升,初始的平稳期大约在 1945 年结束。我们将引入两个虚拟变量,Gt_1960
和Gt_1945
;这两个变量将表示测量时间是否大于 1960 年和 1945 年。虚拟变量通常被赋值为 0 或 1,以表示每行数据是否具有指定类别。在我们的例子中,由于Year
的值较大,我们需要增加虚拟变量的正值,因为在Year
的值达到千位数时,1 的值几乎没有影响。在接下来的练习中,我们将展示回归模型可以由离散值和连续值组成,并且根据适当选择虚拟变量,模型的性能可以得到改善。
练习 30: 引入虚拟变量
在本练习中,我们将向线性回归模型中引入两个虚拟变量:
-
为了方便起见,将
df_group_year
数据框的索引值分配给Year
列:df_group_year['Year'] = df_group_year.index
-
创建一个虚拟变量,并为其添加一个名为
Gt_1960
的列,其中,如果年份小于 1960,则值为0
,如果大于 1960,则值为10
:df_group_year['Gt_1960'] = [0 if year < 1960 else 10 for year in df_group_year.Year] # Dummy Variable - greater than 1960 df_group_year.head(n=2)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_17.jpg
图 3.17: 添加的列 Gt_1960
-
创建一个虚拟变量,并为其添加一个名为
Gt_1945
的列,其中,如果年份小于 1945,则值为0
,如果大于 1945,则值为10
:df_group_year['Gt_1945'] = [0 if year < 1945 else 10 for year in df_group_year.Year]# Dummy Variable - greater than 1945 df_group_year.head(n=2)
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_18.jpg
图 3.18: 添加的列 Gt_1945
-
调用
tail()
方法查看df_group_year
数据框的最后两行,以确认 1960 年后和 1945 年后的标签是否已正确分配:df_group_year.tail(n=2)
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_19.jpg
图 3.19: 最后两行值
-
通过将
Year
、Gt_1960
和Gt_1945
列作为输入,AverageTemperature
列作为输出,来拟合带有附加虚拟变量的线性模型:# Note the year values need to be provided as an N x 1 array model.fit(df_group_year[['Year', 'Gt_1960', 'Gt_1945']], df_group_year.AverageTemperature)
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_20.jpg
图 3.20: 基于数据拟合的线性模型
-
检查新模型的 R-squared 得分,并与训练数据进行比较,看看是否有所改进:
# Note the year values need to be provided as an N x 1 array r2 = model.score(df_group_year[['Year', 'Gt_1960', 'Gt_1945']], df_group_year.AverageTemperature) print(f'r2 score = {r2:0.4f}')
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_21.jpg
图 3.21: 模型的 R-squared 得分
-
我们已经取得了进展!考虑到第一个模型的性能为 0.8618,这在准确度上是一个合理的步骤。我们将绘制另一条趋势线,但由于虚拟变量增加了额外的复杂性,我们需要更多的值。使用
linspace
创建 20 个在 1902 到 2013 年之间均匀分布的值:# Use linspace to get a range of values, in 20 year increments x = np.linspace(df_group_year['Year'].min(), df_group_year['Year'].max(), 20) x
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_22.jpg
图 3.22:使用 linspace 创建的 20 年数组
-
创建一个形状为20 x 3的零数组,并将第一列的值填充为
x
,第二列填充为大于 1960 的虚拟变量值,第三列填充为大于 1945 的虚拟变量值:trend_x = np.zeros((20, 3)) trend_x[:,0] = x # Assign to the first column trend_x[:,1] = [10 if _x > 1960 else 0 for _x in x] # Assign to the second column trend_x[:,2] = [10 if _x > 1945 else 0 for _x in x] # Assign to the third column trend_x
输出结果将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_23.jpg
图 3.23:寻找 trend_x
-
现在通过对
trend_x
进行预测,获取趋势线的 y 值:trend_y = model.predict(trend_x) trend_y
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_24.jpg
图 3.24:寻找 trend_y
-
绘制趋势线:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(trend_x[:,0], trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_25.jpg
图 3.25:使用虚拟变量的预测
引入虚拟变量对模型进行了相当大的改进,但从趋势线来看,这似乎不是一个自然现象(如温度)应遵循的合理路径,可能存在过拟合的问题。我们将在第五章,集成建模中详细讲解过拟合;不过,我们可以先使用线性回归来拟合一个更平滑的预测曲线模型,比如抛物线。
活动 7:虚拟变量
对于本活动,我们将使用在之前活动中使用的德克萨斯州奥斯汀的天气数据集。在本活动中,我们将使用虚拟变量来增强该数据集的线性回归模型。
在我们开始之前,我们需要导入一些库,并从之前的活动中加载数据,具体操作如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
df_first_year = pd.read_csv('activity_first_year.csv')
rolling = pd.read_csv('activity2_rolling.csv')
window = 20
# Trendline values
trend_x = np.array([
1,
182.5,
365
])
需要执行的步骤如下:
-
绘制原始数据(
df
)和移动平均(rolling
)。 -
从上一步的结果来看,似乎在第 250 天左右有一个拐点。创建一个虚拟变量,将此特征引入线性模型中。
-
检查首尾样本,以确认虚拟变量是否正确。
-
使用最小二乘法线性回归模型,将模型拟合到
DayOfYear
的值和虚拟变量,预测TempAvgF
。 -
计算 R2 分数。
-
使用
DayOfYear
的值,利用该模型构造趋势线进行预测。 -
将趋势线与数据和移动平均进行对比绘制。
注意
本活动的解答请见第 334 页。
线性回归的抛物线模型
线性回归模型不仅仅局限于直线模型。我们可以使用完全相同的技术拟合一些更复杂的模型。我们提到过数据似乎具有一些抛物线特征,所以我们来尝试拟合一个抛物线模型。提醒一下,抛物线的方程是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_26.jpg
图 3.26:抛物线的方程
添加这个平方项将把模型从一条直线转换为具有抛物线(或弧线)轨迹的模型。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_27.jpg
图 3.27:抛物线曲线
练习 31:使用线性回归拟合抛物线模型
为了使用线性回归拟合一个抛物线模型,我们只需要稍微调整一下输入。在这个练习中,我们将看到如何做到这一点:
-
我们需要做的第一件事是为年份值提供平方项。为了方便,创建一个索引的副本并将其存储在
Year
列中。现在对Year
列进行平方,提供抛物线特征,并将结果分配给Year2
列:df_group_year['Year'] = df_group_year.index df_group_year['Year2'] = df_group_year.index ** 2 df_group_year.head()
我们将得到这个:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_28.jpg
图 3.28:前五行数据
-
将数据拟合到模型中。这一次,我们需要提供两组值作为模型的输入,
Year
和Year2
,这相当于将 x 和 x**2 传递给抛物线方程。由于我们提供了两列数据,因此不需要重新调整输入数据,它将默认作为 N x 2 数组提供。目标 y 值保持不变:# Note the year values need to be provided as an N x 1 array model.fit(df_group_year[['Year2', 'Year']], df_group_year.AverageTemperature)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_29.jpg
图 3.29:模型拟合
-
打印模型的参数,通过查看系数和截距;现在将有两个系数需要打印:
print(f'a = {model.coef_[0]}') print(f'm = {model.coef_[1]}') print(f'c = {model.intercept_}') print('\nModel Definition') print(f'y = {model.coef_[0]:0.4}x² + {model.coef_[1]:0.4}x + {model.intercept_:0.4f}')
输出将会是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_30.jpg
图 3.30:模型系数和截距
-
使用
score
方法评估模型的表现。性能有所提高吗?# Note the year values need to be provided as an N x 1 array r2 = model.score(df_group_year[['Year2', 'Year']], df_group_year.AverageTemperature) print(f'r2 score = {r2:0.4f}')
我们将获得以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_31.jpg
图 3.31:R 平方得分
-
是的,模型在虚拟变量方法上略有改进,但让我们看看趋势线,看看它是否更合理地拟合。像之前那样绘制趋势线。为了有效地绘制趋势线的抛物线弧线,我们需要更多的预测值。使用
linspace
创建 1902 和 2013 之间 20 个线性间隔的值:# Use linspace to get a range of values, in 20 yr increments x = np.linspace(df_group_year['Year'].min(), df_group_year['Year'].max(), 20) x
我们将得到这个:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_32.jpg
图 3.32:使用 linspace 查找 20 个增量
-
现在我们训练的模型需要两列年份数据作为输入:第一列包含平方的年份值,第二列仅包含年份值本身。为了向模型提供数据,创建一个 20 行 2 列的零数组(
trend_x
)。将x
的平方值赋值给trend_x
的第一列,直接将x
赋值给trend_x
的第二列:trend_x = np.zeros((20, 2)) trend_x[:,0] = x ** 2 # Assign to the first column trend_x[:,1] = x # Assign to the second column trend_x
输出结果为:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_33.jpg
图 3.33:x 变量的趋势
-
现在通过对
trend_x
进行预测,获取趋势线的y值:trend_y = model.predict(trend_x) trend_y
我们将得到如下结果:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_34.jpg
图 3.34:y 变量的趋势
-
按照直线模型绘制趋势线。记住,
trend_y
的x轴值是年份,也就是trend_x
的第二列,而不是年份的平方:fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(trend_x[:,1], trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_35.jpg
图 3.35:带有抛物线模型的线性回归
参考图 3.35,我们可以看到使用抛物线模型的性能优势,趋势线几乎与 10 年移动平均线相吻合。考虑到年度平均原始数据中的噪声量,这个拟合效果相当不错。在这种情况下,不应该期望模型能够完美地拟合数据。如果我们的模型能够完美拟合观察到的示例,那么就可能存在严重的过拟合问题,导致模型在面对未见数据时的预测能力较差。
活动 8:其他线性回归模型类型
我们尝试了标准的线性模型以及虚拟变量。在这个活动中,我们将尝试几个不同的函数,看看如何更好地拟合数据。对于每个不同的函数,确保打印函数参数、R2 值,并将趋势线与原始数据及移动平均数据进行比较。
尝试几个不同的函数,实验数据,看看你的预测能力能达到什么程度。在这个活动中,我们将使用正弦函数。
在开始之前,我们需要导入一些库并加载来自先前活动的数据,操作步骤如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
df_first_year = pd.read_csv('activity_first_year.csv')
rolling = pd.read_csv('activity2_rolling.csv')
window = 20
# Trendline values
trend_x = np.array([
1,
182.5,
365
])
执行的步骤如下:
通用模型训练
最小二乘法构建线性回归模型是一种有用且准确的训练方法,前提是数据集的维度较低,并且系统内存足够大,能够管理数据集以及在 scikit-learn 实现中进行矩阵除法操作。近年来,大型数据集变得更加容易获取,许多大学、政府甚至一些公司都在线免费发布了大型数据集;因此,在使用最小二乘法回归建模时,可能相对容易超出系统内存。在这种情况下,我们需要采用不同的训练方法,比如梯度下降,它不那么容易受到高维度的影响,能够训练大规模数据集,并避免使用内存密集型的矩阵运算。在我们进一步探讨梯度下降之前,我们将重新审视训练模型的一般过程,因为大多数训练方法,包括梯度下降,都遵循这一通用过程(图 3.36)。训练过程涉及将模型及其参数反复暴露给一组示例训练数据,并将模型输出的预测值传递给指定的成本或误差函数。
成本函数用于确定模型与目标值的接近程度,并衡量训练过程中进展的程度。过程的最后一步是定义训练超参数,如本章开头所讨论的那样,这些超参数是调节模型更新过程的手段:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_36.jpg
图 3.36:通用训练过程
梯度下降
梯度下降过程可以总结为根据成本函数定义的误差,逐步更新模型参数,并响应系统中的错误。有多种成本函数可以选择,具体取决于拟合的模型类型或解决的问题。我们将选择简单但有效的均方误差成本函数,但首先,我们将以与机器学习文献中普遍使用的符号一致的方式重写我们的模型方程。以直线方程为我们的模型:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_37.jpg
图 3.37:直线方程
它可以重写为:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_38.jpg
图 3.38:简化线性模型
其中 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_03.png 是模型做出的预测,按照惯例,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_04.png 用来表示截距项。使用新的模型符号,我们可以定义均方误差函数如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_39.jpg
图 3.39:均方误差
其中 y**t 是对应的真实值,N 是训练样本的数量。
定义了这两个函数后,我们现在可以更详细地看一下梯度下降算法:
-
梯度下降法从对所有https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_05.png的初始随机猜测开始。
-
对训练集中的每个样本,使用随机值为https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_05.png进行预测。
-
然后计算这些参数https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_06.png的误差。
-
然后修改https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_05.png的值,做出与误差成比例的小调整,试图最小化误差。更正式地说,更新过程使用https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_07.png的当前值,并减去https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_06.png中与https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_07.png相关的部分,该部分等于https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_07.png乘以小调整量https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_08.png,也称为学习率。
不深入探讨数学细节,更新参数或权重(https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_09.png)的方程可以写成如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_40.jpg
图 3.40:梯度下降更新步骤
让我们来讨论这个方程:
-
:=
操作符表示计算机编程概念中的变量重新赋值或更新。 -
这个训练过程将持续到收敛;也就是说,直到权重的变化非常小,以至于参数几乎没有变化,或者直到我们介入并停止该过程,就像在交叉验证中的情况一样。
-
为学习率分配的值对训练过程至关重要,因为它定义了权重变化的大小,从而决定了沿误差曲线下降的步伐大小。如果值过小,训练过程可能会耗费过长时间,或可能会陷入误差曲线的局部最小值,无法找到最优的全局值。相反,如果步伐过大,训练过程可能会变得不稳定,因为它们会越过局部和全局最小值。
该过程在下图中进行了可视化:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_41.jpg
图 3.41:梯度下降过程
注意
关于设置学习率的一般建议是,从较大的值开始,比如 0.1,如果找不到解决方案,即误差为NaN
(不是一个数字),则将其减少一个数量级。继续这样做,直到训练过程持续进行并且误差不断减少。一旦你对模型满意并且几乎完成时,将学习率稍微减小,并让训练继续更长时间。
虽然这个过程听起来可能很复杂,但它远没有看起来那么可怕。梯度下降可以总结为:第一次猜测权重的值,计算猜测的误差,对权重进行微小调整,并不断重复这一过程,直到误差收敛到最小值。为了加深我们的理解,让我们看一个更具体的例子。我们将使用梯度下降法来训练我们在练习 29:使用最小二乘法拟合线性模型中构建的原始线性回归模型,使用梯度下降法替代最小二乘法。
练习 32:使用梯度下降法的线性回归
在开始梯度下降过程之前,我们需要花一点时间来设置模型。在我们的 Jupyter 笔记本中,执行以下步骤:
-
编写一个函数来定义我们的线性模型。这时,使用线性模型的简化形式(图 3.38)的优势就显现出来了。我们可以利用线性代数乘法,将权重(theta)与输入值,x,进行相乘,这等价于 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_10.png:
def h_x(weights, x): return np.dot(weights, x).flatten()
-
为了使用这种线性代数乘法技巧,我们必须通过插入一行全为 1 的值来修改输入数据,以表示偏置项。创建一个具有两列形状的全 1 数组(一个用于权重的梯度项,另一个用于偏置项)。将标准化后的
Year
值插入到新创建数组的第一行。要在梯度下降过程中使用输入数据,我们还必须将所有值标准化到 0 到 1 之间。这是过程中的一个关键步骤,因为如果一个变量的值是 1,000 级别,而第二个变量是 10 级别,那么第一个变量在训练过程中将比第二个变量影响大 100 倍,这可能会导致模型无法训练。通过确保所有变量都在 0 到 1 之间缩放,它们将在训练过程中有相同的影响。通过将
Year
的值除以最大值来对输入数据进行缩放:x = np.ones((2, len(df_group_year))) x[0,:] = df_group_year.Year x[1,:] = 1 x /= x.max() x[:,:5]
你将得到如下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_42.jpg
图 3.42:修改后的数据
-
正如我们所学,我们需要对权重的值进行初始猜测。我们需要定义两个权重值,一个用于梯度,另一个用于y截距。为了确保每次初始化时使用相同的第一个随机数,需要对 NumPy 的随机数生成器进行初始化。初始化随机数生成器确保每次运行脚本时,生成的随机数集是相同的。这保证了多次运行中模型的一致性,并提供了检查模型表现是否受变化影响的机会:
np.random.seed(255) # Ensure the same starting random values
-
使用均值为 0、标准差为 0.1 的正态分布随机数初始化权重。我们希望初始化的权重是随机的,但仍然接近零,以便给它们找到良好解的机会。为了执行
h_x
中的矩阵乘法操作,将随机数重塑为一行两列(一个用于梯度,一个用于y截距):Theta = np.random.randn(2).reshape((1, 2)) * 0.1 Theta
我们将得到如下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_43.jpg
图 3.43:Theta 值
-
定义真实值为平均年气温:
y_true = df_group_year.AverageTemperature.values
-
将代价函数(均方误差)定义为 Python 函数:
def J_theta(pred, true): return np.mean((pred - true) ** 2) # mean squared error
-
定义如前所述的学习率。这是一个非常重要的参数,必须适当设置。如前所述,设置得太小,模型可能需要很长时间才能找到最小值;设置得太大,可能根本找不到最小值。将学习率定义为
1e-6
:gamma = 1e-6
-
定义一个函数来实现梯度下降的一步(图 3.40)。该函数将接收预测值和真实值,以及x和gamma的值,并返回需要添加到权重(theta)的值:
def update(pred, true, x, gamma): return gamma * np.sum((true - pred) * x, axis=1)
-
定义我们希望训练过程运行的最大 epoch 数(或迭代次数)。每个 epoch 根据给定的x预测y值(标准化的年均气温),并根据预测误差更新权重:
max_epochs = 100000
-
做一个初步预测,并使用定义的
h_x
和J_theta
函数计算该预测的误差或代价:y_pred = h_x(Theta, x) print(f'Initial cost J(Theta) = {J_theta(y_pred, y_true): 0.3f}')
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_44.jpg
图 3.44:J theta 的初始代价
-
手动完成第一次更新步骤。使用新预测的值来调用
update
函数,再次调用h_x
获取预测值,并得到新的误差:Theta += update(y_pred, y_true, x, gamma) y_pred = h_x(Theta, x) print(f'Initial cost J(Theta) = {J_theta(y_pred, y_true): 0.3f}')
我们将得到如下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_45.jpg
图 3.45:更新后的 J theta 代价
-
注意到误差的微小减少;因此,需要很多 epoch 的训练。将
predict
和update
函数调用放入for
循环中,循环次数为max_epochs
,并在每第十个 epoch 打印相应的误差:error_hist = [] epoch_hist = [] for epoch in range(max_epochs): Theta += update(y_pred, y_true, x, gamma) y_pred = h_x(Theta, x) if (epoch % 10) == 0: _err = J_theta(y_pred, y_true) error_hist.append(_err) epoch_hist.append(epoch) print(f'epoch:{epoch:4d} J(Theta) = {_err: 9.3f}')
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_46.jpg
图 3.46:十个 epoch
-
通过绘制
epoch_hist
与error_hist
来可视化训练历史:plt.figure(figsize=(10, 7)) plt.plot(epoch_hist, error_hist); plt.title('Training History'); plt.xlabel('epoch'); plt.ylabel('Error');
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_47.jpg
图 3.47:训练历史曲线:一个非常重要的工具
注意到误差在 30,000 个 epoch 时达到了一个渐近值,因此
max_epochs
可以减少。 -
使用
sklearn.metrics
中的r2_score
函数来计算通过梯度下降训练的模型的 R 平方值:from sklearn.metrics import r2_score r2_score(y_true, y_pred)
我们将得到如下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_48.jpg
图 3.48:R 平方值
-
为了绘制新模型的趋势线,再次创建 1901 到 2013 年之间线性间隔的 20 个年份值:
# Use linspace to get a range of values, in 20 yr increments x = np.linspace(df_group_year['Year'].min(), df_group_year['Year'].max(), 20) x
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_49.jpg
图 3.49:使用 linspace 的值
-
为了将这些数据与我们的模型一起使用,我们必须首先将最大值标准化到 0 到 1 之间,并插入一行 1。执行此步骤的方式与为训练准备数据时的 步骤 2 类似。
trend_x = np.ones((2, len(x))) trend_x[0,:] = x trend_x[1,:] = 1 trend_x /= trend_x.max() trend_x
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_50.jpg
图 3.50:x 的趋势
-
使用训练过程中保存的权重,调用
h_x
模型函数,得到趋势线的预测 y 值:trend_y = h_x(Theta, trend_x) trend_y
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_51.jpg
图 3.51:y 的趋势
-
用数据绘制趋势线:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(x, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_52.jpg
图 3.52:使用梯度下降的平均气温测量值
恭喜!你刚刚用梯度下降训练了第一个模型。这是一个重要的步骤,因为这个简单的工具可以用来构建更复杂的模型,如逻辑回归和神经网络模型。然而,我们首先需要注意一个重要的观察结果:梯度下降模型产生的 r 方值不如最小二乘法模型高。
在梯度下降的第一步,我们猜测一些合理的权重值,然后对权重进行小幅调整,试图减少误差,并在误差停止减少时停止训练。梯度下降的优势体现在两个特定的应用中:
-
解决更复杂的模型,这些模型的数学最优解尚未找到或无法找到
-
提供一种训练方法,适用于数据集或参数非常庞大,以至于物理硬件的限制(例如可用内存)阻止了使用其他方法,如最小二乘法
所以,如果数据集不是特别庞大并且可以通过优化解决,我们应该毫不犹豫地使用更精确的方法。话虽如此,还有很多方法可以修改梯度下降过程,包括不同类型的梯度下降算法、更高级的学习率使用方式以及训练过程中数据的供给方式。这些修改超出了本书的范围,因为关于梯度下降过程和改进性能方法的整本书都有写作空间。
练习 33:优化梯度下降
在前一个练习中,我们直接实现了梯度下降;然而,我们通常不会使用这种实现。scikit-learn 的梯度下降方法包含了许多优化,并且只需要几行代码即可使用:
-
导入
SGDRegressor
类,并使用与前一个练习中相同的参数构建模型:from sklearn.linear_model import SGDRegressor model = SGDRegressor( max_iter=100000, learning_rate='constant', eta0=1e-6, random_state=255, tol=1e-6, penalty='none', )
-
使用年份值,除以最大年份值,作为输入,并与
AverageTemperature
值作为真实值进行拟合:x = df_group_year.Year / df_group_year.Year.max() y_true = df_group_year.AverageTemperature.values.ravel() model.fit(x.values.reshape((-1, 1)), y_true)
-
使用训练好的模型预测值,并确定 r-squared 值:
y_pred = model.predict(x.values.reshape((-1, 1))) r2_score(y_true, y_pred)
-
绘制由模型确定的趋势线,除了原始数据和移动平均值之外:
fig = plt.figure(figsize=(10, 7)) ax = fig.add_axes([1, 1, 1, 1]); # Temp measurements ax.scatter(df_group_year.index, df_group_year.AverageTemperature, label='Raw Data', c='k'); ax.plot(df_group_year.index, rolling, c='k', linestyle='--', label=f'{window} year moving average'); ax.plot(x, trend_y, c='k', label='Model: Predicted trendline') ax.set_title('Mean Air Temperature Measurements') ax.set_xlabel('Year') ax.set_ylabel('Temperature (degC)') ax.set_xticks(range(df_group_year.index.min(), df_group_year.index.max(), 10)) ax.legend();
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_53.jpg
图 3.53:优化后的梯度下降预测趋势线
将此图与使用梯度下降手动实现构建的图形进行比较。注意它们的相似性:这使我们有信心,梯度下降的两种实现都是正确的。
活动 9:梯度下降
在本活动中,我们将实现与 活动 6,使用最小二乘法进行线性回归 相同的模型;但是,我们将使用梯度下降过程。
在我们开始之前,我们需要导入一些库并加载来自先前活动的数据,可以按照以下方式进行:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import SGDRegressor
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
df_first_year = pd.read_csv('activity_first_year.csv')
rolling = pd.read_csv('activity2_rolling.csv')
window = 20
# Trendline values
trend_x = np.array([
1,
182.5,
365
])
执行的步骤如下:
-
创建一个通用的梯度下降模型,并将年份值标准化到 0 到 1 之间。
-
拟合模型。
-
打印模型的详细信息。
-
准备 x
(trend_x)
趋势线的值,通过除以最大值。使用梯度下降模型预测y_trend_values
。 -
绘制数据、移动平均值和趋势线。
注意
该活动的解决方案可以在第 341 页找到。
多元线性回归
我们已经涵盖了常规线性回归,以及带有多项式项的线性回归,并考虑了使用最小二乘法和梯度下降法训练它们。本章的这一部分将考虑另一种类型的线性回归:多元线性回归,其中使用多个变量(或特征)来构建模型。为了研究多元线性回归,我们将使用波士顿住房数据集的修改版,该数据集可从 archive.ics.uci.edu/ml/index.php
获取。修改后的数据集可以在随附的源代码中找到,或者在 GitHub 上找到:github.com/TrainingByPackt/Supervised-Learning-with-Python
,并且已被重新格式化以简化使用。该数据集包含波士顿地区不同属性的列表,包括按城镇计算的人均犯罪率、低社会经济状态人口的百分比、每个住宅的平均房间数,以及该地区业主自住房屋的中位数价值。
练习 34:多元线性回归
我们将使用波士顿住房数据集来构建一个多元线性模型,该模型预测在给定低社会经济状态人口百分比和每个住宅的平均房间数的情况下,业主自住房屋的中位数价值:
-
导入所需的依赖项:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression
-
读取住房数据库:
df = pd.read_csv('housing_data.csv') df.head()
head()
函数将返回以下输出:https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_54.jpg
图 3.54:前五行
-
绘制两个列:平均房间数量(
RM
)和低社会经济状态人口的百分比(PTRATIO
):fig = plt.figure(figsize=(10, 7)) fig.suptitle('Parameters vs Median Value') ax1 = fig.add_subplot(121) ax1.scatter(df.LSTAT, df.MEDV, marker='*', c='k'); ax1.set_xlabel('% lower status of the population') ax1.set_ylabel('Median Value in $1000s') ax2 = fig.add_subplot(122, sharey=ax1) ax2.scatter(df.RM, df.MEDV, marker='*', c='k'); ax2.get_yaxis().set_visible(False) ax2.set_xlabel('average number of rooms per dwelling');
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_55.jpg
图 3.55:参数与中位数值的关系
-
构建一个线性回归模型,用于预测低社会经济状态的百分比(
LSTAT
)与中位数房产价值(MEDV
)之间的关系,并计算模型的性能,使用 R 平方值来衡量:model = LinearRegression() model.fit(df.LSTAT.values.reshape((-1, 1)), df.MEDV.values.reshape((-1, 1))) model.score(df.LSTAT.values.reshape((-1, 1)), df.MEDV.values.reshape((-1, 1)))
我们将获得以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_56.jpg
图 3.56:使用 LSTAT 的模型评分
-
计算使用平均房间数量训练的线性模型的预测性能,以预测房产价值:
model.fit(df.RM.values.reshape((-1, 1)), df.MEDV.values.reshape((-1, 1))) model.score(df.RM.values.reshape((-1, 1)), df.MEDV.values.reshape((-1, 1)))
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_57.jpg
图 3.57:使用 RM 的模型评分
-
创建一个多元线性回归模型,使用
LSTAT
和RM
值作为输入,预测中位数房产价值:model.fit(df[['LSTAT', 'RM']], df.MEDV.values.reshape((-1, 1))) model.score(df[['LSTAT', 'RM']], df.MEDV.values.reshape((-1, 1)))
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_58.jpg
图 3.58:使用 LSTAT 和 RM 的模型评分
自回归模型
自回归模型是经典统计建模技术的一部分,通常用于时间序列数据(即任何随时间变化的数据集),并在本章中涉及的线性回归技术基础上进行扩展。自回归模型在经济学和金融行业中广泛应用,因为它们在拥有大量测量数据的时间序列数据集中尤其有效。为了反映这一点,我们将数据集更改为 1986 年到 2018 年的标准普尔每日收盘价格,该数据可在随附的源代码中找到。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_59.jpg
图 3.59:标准普尔 500 指数每日收盘价
自回归模型背后的主要原理是,给定足够的先前观察数据,就可以对未来做出合理的预测;换句话说,我们实际上是在使用数据集将其自身作为回归来构建模型,因此称为 自回归。这种关系可以用线性方程在数学上建模:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_60.jpg
图 3.60:一阶自回归模型
其中 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_11.png 是时间 t 的预测值,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_12.png 是模型的第一个权重,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_13.png 是第二个权重,且 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_14.png 是数据集中前一个值,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_15.png 是误差项。
图中的方程 图 3.60 表示使用数据集中的前一个值进行预测的模型。这是一个一阶自回归模型,可以扩展为包括更多的前置样本。
图 3.61 中的方程提供了一个二阶模型的示例,包括之前的两个值。
类似地,k**阶自回归模型包含与 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_Formula_03_16.png 相应的参数值,增加了关于模型前期观测的更多上下文。同样,参考 图 3.61 中的方程和 k**阶自回归模型,可以观察到自回归模型的递归特性。每个预测都会使用前一个值(或多个值)进行求和,因此,如果我们取之前预测的值,它们本身也会使用前一个值的预测,因此产生了递归。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_61.jpg
图 3.61:二阶和 k 阶自回归模型
练习 35:创建自回归模型
我们将使用标准普尔 500 指数模型来创建一个自回归模型:
-
加载标准普尔 500 指数数据集,从日期列中提取表示年份的两位数字,并创建一个新的列
Year
,将年份转换为四位数格式(例如,02-Jan-86 会变为 1986,31-Dec-04 会变为 2004):df = pd.read_csv('spx.csv') yr = [] for x in df.date: x = int(x[-2:]) if x < 10: x = f'200{x}' elif x < 20: x = f'20{x}' else: x = f'19{x}' yr.append(x) df['Year'] = yr df.head()
我们将获得以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_62.jpg
图 3.62:前五行
-
绘制原始数据集,x 轴按五年为单位:
plt.figure(figsize=(10, 7)) plt.plot(df.close.values); yrs = [yr for yr in df.Year.unique() if (int(yr[-2:]) % 5 == 0)] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('S&P 500 Daily Closing Price'); plt.xlabel('Year'); plt.ylabel('Price ($)');
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_63.jpg
图 3.63:历年收盘价图
-
在我们构建自回归模型之前,必须首先检查该模型是否能够作为一个回归模型使用。为此,我们可以再次使用 pandas 库检查数据集与其平移了定义数量样本的副本之间的相关性,这种方法称为
shift
方法,引入一个3
的样本滞后并查看前 10 个收盘价的结果:df.close[:10].values df.close[:10].shift(3).values
我们将得到这个输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_64.jpg
图 3.64:滞后三期的值
注意数组中引入了三个 NaN 值,并且最后三个值已从数组中删除。这是平移的效果,实质上是将数据集根据滞后期定义的时间段向前滑动。
-
将数据集按 100 的滞后平移并绘制结果:
plt.figure(figsize=(15, 7)) plt.plot(df.close.values, label='Original Dataset', c='k', linestyle='-'); plt.plot(df.close.shift(100), c='k', linestyle=':', label='Lag 100'); yrs = [yr for yr in df.Year.unique() if (int(yr[-2:]) % 5 == 0)] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('S&P 500 Daily Closing Price'); plt.xlabel('Year'); plt.ylabel('Price ($)'); plt.legend();
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_65.jpg
图 3.65:全年收盘价图
-
现在我们已经理解了时间平移,我们将确认数据是否能够与其自身相关。为此,使用 pandas 的
autocorrelation_plot
方法来检查数据中的随机性:plt.figure(figsize=(10, 7)) pd.plotting.autocorrelation_plot(df.close);
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_66.jpg
图 3.66:自相关与滞后的关系
确定是否可以进行自动回归的所有信息都定义在这个图中。我们可以在x轴上看到,Lag的值从 0 到 8,000 个样本变化,而Autocorrelation的值大约在-0.4 到 1 之间。还有五条其他的附加线比较重要;不过,在这个y轴的刻度下,很难看到它们。
-
将y轴的限制设置为-0.1 到 0.1 之间:
plt.figure(figsize=(10, 7)) ax = pd.plotting.autocorrelation_plot(df.close); ax.set_ylim([-0.1, 0.1]);
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_67.jpg
图 3.67:自相关与滞后图
在增强视图中,我们可以看到有两条灰色虚线,表示 99%置信带,表明该序列是非随机的。实线灰色线表示 95%置信带。一旦自相关图在这些带内接近零,带有指定滞后的时间序列就变得足够随机,自动回归模型将不再适用。
-
为了进一步巩固我们的理解,绘制收盘价与滞后 100 个样本的收盘价的图。根据我们的自相关图,这两组数据之间存在高度的相关性。那是什么样子的呢?
plt.figure(figsize=(10,7)) ax = pd.plotting.lag_plot(df.close, lag=100);
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_68.jpg
图 3.68:自相关图
-
创建收盘价与滞后 4,000 个样本的收盘价图。同样,根据自相关图,在滞后 4,000 时,自相关值大约为 0,表示两者之间没有真正的相关性,它们大多是随机的:
plt.figure(figsize=(10,7)) ax = pd.plotting.lag_plot(df.close, lag=4000);
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_69.jpg
图 3.69:收盘价与滞后 4,000 个样本的收盘价图
-
现在我们准备创建模型了。然而,为了做到这一点,我们还需要一个 Python 包——
statsmodel
包(www.statsmodels.org
),它类似于 scikit-learn,但专门用于创建模型并执行使用更经典统计技术的测试。安装statsmodel
包。你可以使用conda install
或pip
来安装。对于 Anaconda 安装,推荐使用conda install
方法:#!pip install statsmodels !conda install -c conda-forge statsmodels
-
从
statsmodel
导入自回归类(AR
),并使用收盘价数据构建模型:from statsmodels.tsa.ar_model import AR model = AR(df.close)
-
使用
fit
方法拟合模型,并打印出选择使用的滞后值以及模型的系数:model_fit = model.fit() print('Lag: %s' % model_fit.k_ar) print('Coefficients: %s' % model_fit.params)
输出将是:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_70.jpg
图 3.70:滞后系数
请注意,每个权重都有 36 个系数,还有一个常数;为了简便起见,这里仅显示了一部分。所有系数可以在随附源代码中的
Ex7-AutoRegressors.ipynb
Jupyter 笔记本中找到。 -
使用该模型从第 36 个样本(滞后)开始创建一组预测,直到数据集结束后的 500 个样本为止:
predictions = model_fit.predict(start=36, end=len(df) + 500) predictions[:10].values
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_71.jpg
图 3.71:预测值
-
将预测值覆盖在原始数据集上进行绘制:
plt.figure(figsize=(10, 7)) plt.plot(predictions, c='g', linestyle=':', label='Predictions'); plt.plot(df.close.values, label='Original Dataset'); yrs = [yr for yr in df.Year.unique() if (int(yr[-2:]) % 5 == 0)] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('S&P 500 Daily Closing Price'); plt.xlabel('Year'); plt.ylabel('Price ($)'); plt.legend();
这将产生以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_72.jpg
图 3.72:全年价格变化图
请注意,预测值很好地跟踪了数据集,并且在数据集结束后,预测值相对线性。由于模型是基于之前的样本构建的,因此一旦数据集结束,它变得不那么确定,尤其是数据中没有重复模式时,这一点尤为明显。
-
拟合看起来非常接近——预测值与原始数据集之间的差异是什么样的?增强模型以观察差异:
plt.figure(figsize=(10, 7)) plt.plot(predictions, c='g', linestyle=':', label='Predictions'); plt.plot(df.close.values, label='Original Dataset'); yrs = [yr for yr in df.Year.unique() if (int(yr[-2:]) % 5 == 0)] plt.xticks(np.arange(0, len(df), len(df) // len(yrs)), yrs); plt.title('S&P 500 Daily Closing Price'); plt.xlabel('Year'); plt.ylabel('Price ($)'); plt.xlim([2000, 2500]) plt.ylim([420, 500]) plt.legend();
这将提供以下图形:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/app-spr-lrn-py/img/C12622_03_73.jpg
图 3.73:原始数据集值的预测
通过使用自回归模型的这个练习,我们可以看到,在数据集缺失数据时,或当我们尝试在测量间隔之间进行预测时,使用这些模型具有显著的预测能力。对于 S&P 500 数据集显示的自回归模型,它能够有效地提供观测样本范围内的预测。然而,超出该范围,当预测未来值且没有测量数据时,预测能力可能会有所限制。
活动 10:自回归模型
在本活动中,我们将使用自回归模型来建模奥斯汀的天气数据集,并预测未来的值:
在开始之前,我们需要导入一些库,并从之前的活动中加载数据,操作如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.ar_model import AR
# Loading the data from activity 5
df = pd.read_csv('activity2_measurements.csv')
需要执行的步骤如下:
-
绘制完整的平均温度值(
df.TempAvgF
)图,年份作为X轴。 -
创建一个 20 天的滞后,并将滞后的数据绘制在原始数据集上。
-
构建自相关图,查看平均温度是否可以与自回归模型一起使用。
-
选择一个可接受的滞后和一个不可接受的滞后,并使用这些值构建滞后图。
-
创建一个自回归模型,注意所选的滞后,计算 r2 值,并将自回归模型与原始图形一起绘制。该模型将预测超出可用数据的 1,000 个样本。
-
将模型拟合到数据中。
-
为数据集结束后的 1,000 天创建一组预测。
-
绘制预测值以及原始数据集。
-
通过显示第 100 个到第 200 个样本,增强视图以查找差异。
注意
本活动的解决方案可以在第 344 页找到。
总结
在本章中,我们迈出了构建机器学习模型和使用带标签数据进行预测的第一大步。我们首先分析了多种构建线性模型的方式,从精确的最小二乘法开始,当对少量数据建模时,这种方法非常有效,可以使用可用的计算机内存进行处理。我们通过使用从分类变量中创建的虚拟变量来改进我们的基础线性模型,为模型增加了额外的特征和上下文。接着,我们使用了带抛物线模型的线性回归分析来进一步改进性能,使得模型更贴近数据集的自然曲线。我们还实施了梯度下降算法,虽然我们注意到,对于我们的有限数据集来说,它并不像最小二乘法那样精确,但在系统资源无法处理数据集时表现最为强大。
最后,我们研究了自回归模型的应用,这些模型基于先前数据的经验预测未来值。通过使用自回归模型,我们能够准确地对 1986 年至 2018 年间标准普尔 500 指数的收盘价进行建模。
现在我们已经有了监督回归问题的经验,我们将在下一章节关注分类问题。