原文:
annas-archive.org/md5/20b62d9571c391689a2d53277f7e2459
译者:飞龙
前言
第一章:本书概览
想了解机器学习技术和数据分析如何在全球范围内引领企业发展吗?从生物信息学分析到气候变化预测,机器学习在我们的社会中扮演着越来越重要的角色。
尽管现实世界中的应用可能看起来复杂,但本书通过逐步互动的方式简化了初学者的监督学习。通过使用实时数据集,您将学习如何使用 Python 进行监督学习,以构建高效的预测模型。
从监督学习的基础知识开始,您将很快理解如何自动化手动任务,并通过 Jupyter 和像 pandas 这样的 Python 库评估数据。接下来,您将使用数据探索和可视化技术开发强大的监督学习模型,然后了解如何区分变量,并使用散点图、热图和箱线图表示它们之间的关系。在使用回归和分类模型处理实时数据集以预测未来结果后,您将掌握高级集成技术,如提升方法和随机森林。最后,您将了解监督学习中模型评估的重要性,并学习评估回归和分类任务的度量标准。
在本书结束时,您将掌握自己进行实际监督学习 Python 项目所需的技能。
读者对象
如果您是初学者或刚刚入门的数据科学家,正在学习如何实现机器学习算法来构建预测模型,那么本书适合您。为了加速学习过程,建议具备扎实的 Python 编程基础,因为您将编辑类或函数,而不是从零开始创建。
章节概览
第一章,基础知识,将介绍监督学习、Jupyter 笔记本以及一些最常见的 pandas 数据方法。
第二章,探索性数据分析与可视化,教授您如何对新数据集进行探索和分析。
第三章,线性回归,教授您如何解决回归问题和进行分析,介绍线性回归、多个线性回归和梯度下降法。
第四章,自回归,教授您如何实现自回归方法来预测依赖于过去值的未来值。
第五章,分类技术,介绍分类问题,包括线性回归和逻辑回归、k 近邻算法和决策树的分类方法。
第六章,集成建模,教授您如何检视不同的集成建模方法,包括它们的优点和局限性。
第七章,模型评估,展示了如何通过使用超参数和模型评估指标来提高模型的性能。
约定
文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户名以如下方式显示:“使用 pandas 的 read_csv 函数加载包含 synth_temp.csv 数据集的 CSV 文件,然后显示前五行数据。”
屏幕上显示的单词,例如在菜单或对话框中,也会以这种形式出现在文本中:“通过点击 Jupyter notebook 首页的 titanic.csv 文件来打开它。”
一段代码的设置如下:
print(data[pd.isnull(data.damage_millions_dollars)].shape[0])
print(data[pd.isnull(data.damage_millions_dollars) &
(data.damage_description != ‘NA’)].shape[0])
新术语和重要单词以如下方式显示:“监督式学习意味着数据的标签在训练过程中已经提供,从而让模型能够基于这些标签进行学习。”
代码展示
跨越多行的代码使用反斜杠( \ )进行拆分。当代码执行时,Python 会忽略反斜杠,将下一行的代码视为当前行的延续。
例如:
history = model.fit(X, y, epochs=100, batch_size=5, verbose=1, \
validation_split=0.2, shuffle=False)
注释被添加到代码中,以帮助解释特定的逻辑。单行注释使用#符号,如下所示:
打印数据集的大小
print("数据集中的示例数量 = ", X.shape[0])
print("每个示例的特征数 = ", X.shape[1])
多行注释用三个引号括起来,如下所示:
“”"
为随机数生成器定义一个种子,以确保结果可复现
结果将是可重复的
“”"
seed = 1
np.random.seed(seed)
random.set_seed(seed)
设置你的环境
在详细探讨本书内容之前,我们需要设置一些特定的软件和工具。在接下来的部分,我们将看到如何操作。
安装与设置
本书中的所有代码都是在 Jupyter Notebooks 和 Python 3.7 上执行的。安装 Anaconda 后,Jupyter Notebooks 和 Python 3.7 可供使用。以下部分列出了在 Windows、macOS 和 Linux 系统上安装 Anaconda 的说明。
在 Windows 上安装 Anaconda
以下是完成安装所需遵循的步骤:
访问 https://www.anaconda.com/products/individual 并点击下载按钮。
在 Anaconda 安装程序/Windows 部分,选择 Python 3.7 版本的安装程序。
确保安装与你的计算机架构(32 位或 64 位)相符的版本。你可以在操作系统的“系统属性”窗口中找到此信息。
下载完成后,双击文件,按照屏幕上的指示完成安装。
这些安装将在你系统的 ‘C’ 盘执行。不过,你可以选择更改安装目标。
在 macOS 上安装 Anaconda
访问 www.anaconda.com/products/individual
并点击下载按钮。
在 Anaconda 安装程序/MacOS 部分,选择 (Python 3.7) 64 位图形安装程序。
下载完安装程序后,双击文件,并按照屏幕上的指示完成安装。
在 Linux 上安装 Anaconda
访问 www.anaconda.com/products/individual
并点击下载按钮。
在 Anaconda 安装程序/Linux 部分,选择 (Python 3.7) 64 位 (x86) 安装程序。
下载完安装程序后,在终端运行以下命令:bash ~/Downloads/Anaconda-2020.02-Linux-x86_64.sh
按照终端中出现的指示完成安装。
你可以通过访问此网站了解有关各种系统安装的更多详情:docs.anaconda.com/anaconda/install/
。
安装库
pip 在 Anaconda 中预装。安装完 Anaconda 后,可以使用 pip 安装所有必需的库,例如:pip install numpy。或者,你也可以使用 pip install –r requirements.txt 来安装所有必需的库。你可以在 packt.live/3hSJgYy
找到 requirements.txt 文件。
练习和活动将在 Jupyter Notebooks 中执行。Jupyter 是一个 Python 库,可以像其他 Python 库一样通过 pip install jupyter 安装,但幸运的是,它在 Anaconda 中已预装。要打开笔记本,只需在终端或命令提示符中运行命令 jupyter notebook。
访问代码文件
你可以在 packt.live/2TlcKDf
找到本书的完整代码文件。你也可以通过使用互动实验环境 packt.live/37QVpsD
直接在浏览器中运行许多活动和练习。
我们已经尽力支持所有活动和练习的互动版本,但我们仍然推荐进行本地安装,以防这些互动支持不可用。
如果你在安装过程中遇到任何问题或有任何疑问,请通过邮件联系我们:workshops@packt.com。
第二章:1. 基础知识
概述
本章将向你介绍监督学习,使用 Anaconda 管理编码环境,以及使用 Jupyter notebooks 创建、管理和运行代码。它还涵盖了一些在监督学习中最常用的 Python 包:pandas、NumPy、Matplotlib 和 seaborn。本章结束时,你将能够安装并加载 Python 库到你的开发环境中,用于分析和机器学习问题。你还将能够使用 pandas 加载外部数据源,并使用多种方法搜索、过滤和计算数据的描述性统计信息。本章将使你能够评估数据源中诸如缺失数据、类别不平衡和样本量不足等问题的潜在影响。
介绍
机器学习和人工智能的研究和应用最近引起了技术和商业界的广泛关注。先进的数据分析和机器学习技术在推动许多领域取得巨大进展方面表现出巨大的潜力,例如个性化医疗、自驾汽车,以及解决世界上一些最大的挑战,例如应对气候变化(参见《使用机器学习应对气候变化》:https://arxiv.org/pdf/1906.05433.pdf)。
本书旨在帮助你利用当今数据科学和机器学习领域中的独特发展机遇。全球范围内,私营企业和政府都在认识到数据驱动的产品和服务的价值和效率。与此同时,硬件成本的降低和开源软件解决方案显著降低了学习和应用机器学习技术的门槛。
在这里,我们将重点介绍监督式机器学习(简称监督学习)。我们稍后将解释不同类型的机器学习,但我们先从一些简单的信息开始。监督学习的经典示例是开发一个算法来区分猫和狗的图片。监督的部分源于两个方面;首先,我们有一组图片,其中正确答案是已知的。我们称这种数据为标签数据。其次,我们进行一个过程,不断测试我们的算法是否能够根据图片预测“猫”或“狗”,并在预测错误时对算法进行修正。从高层次上讲,这个过程类似于教孩子。然而,训练一个算法通常需要比教一个孩子识别猫和狗更多的数据!幸运的是,我们有越来越多的数据源可以利用。请注意,在开发我们的算法时,使用了“学习”和“训练”这两个词。这些词看起来似乎是赋予机器和计算机程序人的特质,但它们已经深深根植于机器学习(和人工智能)文献中,所以我们就用它们,并理解它们。在这里,“训练”指的是将标签数据提供给算法,并对算法进行调整,以便根据数据最好地预测标签。监督的意思是,数据的标签在训练过程中已提供,使得模型能够从这些标签中学习。
现在让我们了解监督学习与其他形式机器学习的区别。
何时使用监督学习
通常,如果你试图自动化或复制一个现有的过程,那么问题就是一个监督学习问题。举个例子,假设你是一本评论并排名各个时期发型的杂志的出版商。你的读者经常向你发送远多于你能够手动处理的发型图片进行评审。为了节省时间,你希望自动化排序你收到的发型图片,按照时间顺序从 1960 年代和 1980 年代的发型开始,正如你在下图中所看到的:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-S9FJQL0H.jpg
图 1.1:来自不同时期的发型图片
为了创建你的发型排序算法,你首先需要收集大量发型图片,并手动为每一张图片标注对应的时间时期。这样的数据集(称为标签数据集)是输入数据(发型图片),其所需的输出信息(时间时期)是已知并记录下来的。这类问题是经典的监督学习问题;我们正在尝试开发一个算法,它可以接收一组输入,并学会返回我们告知它正确的答案。
Python 包和模块
Python 是最常用的机器学习编程语言之一,并且是本书中使用的语言。
虽然 Python 中包含的标准功能确实功能丰富,但 Python 的真正强大之处在于额外的库(也称为包),这些库由于开源许可,可以通过几个简单的命令轻松下载和安装。在本书中,我们一般假设您的系统已经使用 Anaconda 配置,Anaconda 是一个用于 Python 的开源环境管理器。根据您的系统,您可以使用 Anaconda 配置多个虚拟环境,每个环境都配置有特定的包,甚至不同版本的 Python。使用 Anaconda 可以解决许多准备进行机器学习的要求,因为许多最常用的包都已经预构建在 Anaconda 中。有关 Anaconda 安装说明,请参阅前言。
在本书中,我们将使用以下附加的 Python 包:
NumPy(发音为 Num Pie,可在 https://www.numpy.org/ 获取):NumPy(即数字 Python 的简称)是 Python 科学计算的核心组成部分之一。NumPy 提供了多个数据类型,这些数据类型衍生出了许多其他数据结构,包括线性代数、向量和矩阵,以及关键的随机数功能。
SciPy(发音为 Sigh Pie,可在 https://www.scipy.org 获取):SciPy 与 NumPy 一起,是核心的科学计算包。SciPy 提供了许多统计工具、信号处理工具和其他功能,如傅里叶变换。
pandas(可在 https://pandas.pydata.org/ 获取):pandas 是一个高性能的库,用于加载、清洗、分析和操作数据结构。
Matplotlib(可在 https://matplotlib.org/ 获取):Matplotlib 是 Python 中用于创建数据集图表和绘图的基础库,也是其他 Python 绘图库的基础包。Matplotlib 的 API 设计与 Matlab 的绘图库对齐,以便于轻松过渡到 Python。
Seaborn(可在 https://seaborn.pydata.org/ 获取):Seaborn 是一个基于 Matplotlib 构建的绘图库,提供了吸引人的颜色和线条样式,以及一些常见的绘图模板。
Scikit-learn(可在 https://scikit-learn.org/stable/ 获取):Scikit-learn 是一个 Python 机器学习库,提供了许多数据挖掘、建模和分析技术,且有一个简单的 API。Scikit-learn 内置了许多机器学习算法,包括分类、回归和聚类技术。
这些包构成了一个多功能的机器学习开发环境的基础,每个包都提供了一个关键功能集。如前所述,使用 Anaconda 时,您已经安装了所有必需的包,并且可以随时使用。如果需要安装 Anaconda 安装包中未包含的包,可以通过在 Jupyter 笔记本单元格中输入并执行以下代码来安装:
!conda install
例如,如果我们想安装 Seaborn,可以运行以下命令:
!conda install seaborn
要在笔记本中使用这些包,我们只需要导入它:
import matplotlib
在 Pandas 中加载数据
pandas 能够读取和写入多种不同的文件格式和数据结构,包括 CSV、JSON、HDF5 文件,以及 SQL 和 Python Pickle 格式。pandas 的输入/输出文档可以在 https://pandas.pydata.org/pandas-docs/stable/user_guide/io.html 找到。我们将继续通过加载 CSV 文件来研究 pandas 的功能。
注意
本章使用的数据集可以通过以下链接在我们的 GitHub 仓库中找到:https://packt.live/2vjyPK9。下载整个仓库到您的系统后,您可以在 Datasets 文件夹中找到数据集。此外,这个数据集是 Titanic: Machine Learning from Disaster 数据集,最初可以在 https://www.kaggle.com/c/Titanic/data 找到。
该数据集包含了著名的泰坦尼克号上的乘客名单,以及他们的年龄、幸存状态和兄弟姐妹/父母的数量。在开始将数据加载到 Python 之前,关键是花时间查看数据集提供的信息,以便我们能够深入理解它所包含的内容。下载数据集并将其放置在您的工作目录中。
从数据描述中,我们可以看到我们有以下字段:
survival: 这告诉我们一个人是否幸存(0 = 否,1 = 是)。
pclass: 这是社会经济地位的代理,其中头等舱代表上层,二等舱代表中层,三等舱代表下层。
sex: 这告诉我们一个人是男性还是女性。
age: 如果年龄小于 1,则这是一个分数值;例如,0.25 表示 3 个月。如果年龄是估算的,它将以 xx.5 的形式表示。
sibsp: 兄弟姐妹定义为兄弟、姐妹、继兄或继姐,配偶定义为丈夫或妻子。
parch: 父母是母亲或父亲,而子女则是女儿、儿子、继女或继子。仅与保姆一起旅行的孩子没有与父母一起旅行。因此,该字段赋值为 0。
ticket: 这是乘客的票号。
fare: 这是乘客的票价。
cabin: 这告诉我们乘客的舱号。
embark: 登船地点是乘客登船的地点。
请注意,数据集提供的信息没有说明数据是如何收集的。survival、pclass 和 embarked 字段被称为分类变量,因为它们被分配到一组固定的标签或类别中,以指示其他信息。例如,在 embarked 中,C 标签表示乘客在瑟堡登船,而 survival 中的 1 表示他们在沉船事故中幸存。
练习 1.01:加载并总结 Titanic 数据集
在本练习中,我们将把 Titanic 数据集读入 Python 并进行一些基本的总结操作:
打开一个新的 Jupyter notebook。
使用简写语法导入 pandas 和 numpy 包:
import pandas as pd
import numpy as np
通过点击 Jupyter notebook 首页上的 titanic.csv 文件来打开并读取文件,如下图所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-UD4G36A4.jpg
图 1.2:打开 CSV 文件
该文件是一个 CSV 文件,可以将其视为一个表格,每一行代表表格中的一行,每个逗号分隔表格中的列。幸运的是,我们无需以原始文本形式处理这些表格,可以使用 pandas 加载它们:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-4IABVFBO.jpg
图 1.3:CSV 文件内容
注释
花点时间查阅 pandas 文档中关于 read_csv 函数的说明,网址:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html。注意加载 CSV 数据到 pandas DataFrame 中的不同选项。
在可执行的 Jupyter notebook 单元格中,执行以下代码来从文件加载数据:
df = pd.read_csv(r’…\Datasets\titanic.csv’)
pandas 的 DataFrame 类提供了一整套可以在其内容上执行的属性和方法,涵盖从排序、过滤、分组方法到描述性统计,以及绘图和转换等功能。
注释
打开 pandas DataFrame 对象的文档,网址:https://pandas.pydata.org/pandas-docs/stable/reference/frame.html。
使用 DataFrame 的 head() 方法读取前十行数据:
注释
以下代码片段中的 # 符号表示代码注释。注释是添加到代码中以帮助解释特定逻辑的部分。
df.head(10) # 检查前 10 个样本
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-A5PCLLFO.jpg
图 1.4:读取前 10 行
注释
要访问此特定部分的源代码,请参考 https://packt.live/2Ynb7sf。
你也可以在网上运行这个示例,网址:https://packt.live/2BvTRrG。你必须执行整个笔记本以获得预期结果。
在此示例中,我们展示了 DataFrame 中信息的可视化表示。我们可以看到数据以表格化的方式组织,几乎像电子表格一样。不同类型的数据被组织成列,而每个样本被组织成行。每一行都分配了一个索引值,并以粗体数字 0 到 9 显示在 DataFrame 的左侧。每一列都分配了一个标签或名称,如粗体所示,在 DataFrame 的顶部。
DataFrame 作为一种电子表格的类比是合理的。如本章所示,我们可以像在电子表格程序中那样对数据进行排序、过滤和计算。虽然本章未涉及,但值得注意的是,DataFrame 还包含了类似电子表格的透视表功能(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.pivot_table.html)。
练习 1.02:索引和选择数据
现在我们已经加载了一些数据,让我们使用 DataFrame 的选择和索引方法来访问一些感兴趣的数据。这个练习是练习 1.01,“加载并总结泰坦尼克数据集”的延续:
通过使用列的标签,以类似于常规字典的方式选择单个列,如下所示:
df[‘Age’]
输出结果如下:
0 22.0
1 38.0
2 26.0
3 35.0
4 35.0
…
1304 NaN
1305 39.0
1306 38.5
1307 NaN
1308 NaN
名称:年龄,长度:1309,数据类型:float64
如果列名中没有空格,我们还可以使用点操作符。如果列名中有空格,则需要使用括号表示法:
df.Age
输出结果如下:
0 22.0
1 38.0
2 26.0
3 35.0
4 35.0
…
1304 NaN
1305 39.0
1306 38.5
1307 NaN
1308 NaN
名称:年龄,长度:1309,数据类型:float64
使用括号表示法一次选择多个列,如下所示:
df[[‘Name’, ‘Parch’, ‘Sex’]]
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-T3OOZHPG.jpg
图 1.5:选择多个列
注意
由于展示需要,输出已被截断。
使用 iloc 选择第一行:
df.iloc[0]
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-HCT3JIYP.jpg
图 1.6:选择第一行
使用 iloc 选择前三行:
df.iloc[[0,1,2]]
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-I49IQNZH.jpg
图 1.7:选择前三行
接下来,获取所有可用列的列表:
columns = df.columns # 提取列名列表
print(columns)
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-KWXX3C2L.jpg
图 1.8:获取所有列
使用此列名列表和标准的 Python 切片语法,获取第 2、3、4 列及其对应的值:
df[columns[1:4]] # 第 2、3、4 列
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-WE9Q3XRE.jpg
图 1.9:获取第二列、第三列和第四列
使用 len 运算符获取 DataFrame 中的行数:
len(df)
输出结果如下所示:
1309
使用以行中心的方法获取第 2 行的 Fare 列值:
df.iloc[2][‘Fare’] # 以行为中心
输出结果如下所示:
7.925
使用点操作符来访问列,如下所示:
df.iloc[2].Fare # 以行为中心
输出结果如下所示:
7.925
使用以列为中心的方法,如下所示:
df[‘Fare’][2] # 以列为中心
输出结果如下所示:
7.925
使用以列为中心的方法并使用点操作符,如下所示:
df.Fare[2] # 以列为中心
输出结果如下所示:
7.925
注意
要访问这一特定部分的源代码,请参考 packt.live/2YmA7jb
。
你也可以在 packt.live/3dmk0qf
上在线运行此示例。你必须执行整个笔记本才能得到预期的结果。
在这个练习中,我们学习了如何使用 pandas 的 read_csv() 函数将数据加载到 Python 中的 Jupyter 笔记本中。然后,我们探讨了 pandas 通过以 DataFrame 形式呈现数据,如何便捷地选择 DataFrame 中的特定项并查看其内容。在理解了这些基础知识后,我们将进一步探索更高级的索引和数据选择方法。
练习 1.03:高级索引和选择
在掌握了基本的索引和选择方法之后,我们可以将注意力转向更高级的索引和选择方法。在这个练习中,我们将探索几种重要的方法来执行高级索引和数据选择。这个练习是练习 1.01 “加载并总结 Titanic 数据集” 的延续:
创建一个包含 21 岁以下乘客姓名和年龄的列表,如下所示:
child_passengers = df[df.Age < 21][[‘Name’, ‘Age’]]
child_passengers.head()
输出结果如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-W5SL6I98.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-W5SL6I98.jpg)
图 1.10:21 岁以下乘客的姓名和年龄列表
计算有多少名儿童乘客,如下所示:
print(len(child_passengers))
输出结果如下所示:
249
计算年龄在 21 岁到 30 岁之间的乘客数量。此步骤不使用 Python 的逻辑与运算符,而是使用和符号(&)。具体操作如下:
注意
这里展示的代码片段使用了反斜杠(\)将逻辑分割成多行。当代码执行时,Python 会忽略反斜杠,并将下一行代码视为当前行的直接延续。
young_adult_passengers = df.loc[(df.Age > 21) \
& (df.Age < 30)]
len(young_adult_passengers)
输出结果如下所示:
279
查找第一类或第三类票的乘客。我们不会使用 Python 的逻辑或运算符,而是使用管道符号(|)。具体操作如下:
df.loc[(df.Pclass == 3) | (df.Pclass ==1)]
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-8DU9WLXA.jpg
图 1.11:既是头等舱票持有者又是三等舱票持有者的乘客数量
查找不是头等或者三等舱票持有者的乘客。不要简单地选择二等舱票持有者,而是使用 ~ 符号作为非逻辑运算符。操作如下:
df.loc[~((df.Pclass == 3) | (df.Pclass == 1))]
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-H15XD8JZ.jpg
图 1.12:不是头等舱或三等舱票持有者的乘客数量
我们不再需要未命名的 0 列,因此使用 del 运算符删除它:
del df[‘Unnamed: 0’]
df.head()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-DSH2JKHK.jpg
图 1.13:del 运算符
注意
要访问此特定部分的源代码,请参考 https://packt.live/3empSRO。
您也可以在 https://packt.live/3fEsPgK 上在线运行此示例。为了获得期望的结果,您必须执行整个笔记本。
在这个练习中,我们学习了如何使用条件运算符从 DataFrame 中选择数据,并返回我们想要的子集。我们还看到如何删除我们不需要的列(在这种情况下,未命名列只包含对分析无关的行号)。现在,让我们深入了解一些 pandas 的强大功能。
Pandas 方法
现在我们对一些 pandas 基础知识和一些更高级的索引和选择工具感到自信,让我们看一些其他 DataFrame 方法。要获取 DataFrame 中可用的所有方法的完整列表,可以参考类文档。
注意
pandas 的文档可以在 https://pandas.pydata.org/pandas-docs/stable/reference/frame.html 找到。
现在您应该知道 DataFrame 中有多少方法可用。这些方法太多,无法在本章节详细介绍,因此我们将选择一些能为您提供优秀启动的方法。
我们已经看到了一个方法的用法,head(),它提供了 DataFrame 的前五行。如果需要,我们可以通过提供行数来选择更多或更少的行,如下所示:
df.head(n=20) # 20 行
df.head(n=32) # 32 行
或者,您可以使用 tail() 函数查看 DataFrame 结尾的指定行数。
另一个有用的方法是 describe,这是一种快速获取 DataFrame 中数据描述统计信息的方法。我们可以看到样本大小(计数)、均值、最小值、最大值、标准差以及第 25、50 和 75 百分位数对所有数值数据列返回(请注意,文本列已被省略):
df.describe()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZLC1M3ID.jpg
图 1.14: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()
输出将如下所示:
Cabin 295
Embarked 1307
Fare 1308
Pclass 1309
Ticket 1309
Age 1046
Name 1309
Parch 1309
Sex 1309
SibSp 1309
Survived 891
dtype: int64
但我们也有一些包含文本数据的列,例如 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/spr-lrn-ws/img/image-1ZRAJN7I.jpg
图 1.15:带文本列的 describe 方法
这样更好了——现在我们有了更多信息。查看 Cabin 列,我们可以看到有 295 个条目,其中有 186 个唯一值。最常见的值是 C32、C25 和 C27,它们出现了 6 次(来自 freq 值)。类似地,查看 Embarked 列,我们看到有 1,307 个条目,3 个唯一值,最常出现的值是 S,共有 914 个条目。
注意我们在 describe 输出表中出现了 NaN 值。NaN,即非数字(Not a Number),在 DataFrame 中非常重要,因为它表示缺失或不可用的数据。pandas 库能够读取包含缺失或不完整信息的数据源,既是一个福音,也是一个诅咒。许多其他库在遇到缺失信息时会直接无法导入或读取数据文件,而 pandas 能够读取数据也意味着必须妥善处理这些缺失数据。
当查看 describe 方法的输出时,你会注意到 Jupyter 笔记本将其呈现为与我们通过 read_csv 读取的原始 DataFrame 相同的方式。这样做是有充分理由的,因为 describe 方法返回的结果本身就是一个 pandas DataFrame,因此它具备与从 CSV 文件读取的数据相同的方法和特征。可以使用 Python 内置的 type 函数轻松验证这一点,如以下代码所示:
type(df.describe(include=‘all’))
输出将如下所示:
pandas.core.frame.DataFrame
现在我们已经有了数据集的摘要,让我们深入研究一下,以便更好地理解可用数据。
注意
对可用数据的全面理解对于任何监督学习问题都至关重要。数据的来源和类型、收集的方式,以及可能由于收集过程中的错误所导致的问题,都对最终模型的性能产生影响。
希望到现在为止,你已经习惯使用 pandas 提供数据的高级概览。接下来,我们将花一些时间更深入地研究数据。
练习 1.04:使用聚合方法
我们已经看到如何索引或选择 DataFrame 中的行或列,并使用高级索引技术根据特定标准过滤可用数据。另一个方便的选择方法是 groupby 方法,它提供了一种快速选择一组数据的方法,并通过 DataFrameGroupBy 对象提供额外的功能。本练习是练习 1.01《加载并总结泰坦尼克号数据集》的延续:
使用 groupby 方法对 Embarked 列下的数据进行分组,以找出 Embarked 有多少种不同的值:
embarked_grouped = df.groupby(‘Embarked’)
print(f’共有{len(embarked_grouped)}个 Embarked 组’)
输出将如下所示:
共有 3 个 Embarked 组
显示 embarked_grouped.groups 的输出,以查看 groupby 方法实际执行了什么操作:
embarked_grouped.groups
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-PM7LCIIW.jpg
图 1.16: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/spr-lrn-ws/img/image-R9TRRNJC.jpg
图 1.17:检查第 1 行
由于这些组是字典类型,我们可以遍历它们并对每个单独的组执行计算。计算每个组的平均年龄,如下所示:
对于 name, group 在 embarked_grouped 中:
print(name, group.Age.mean())
输出将如下所示:
C 32.33216981132075
Q 28.63
S 29.245204603580564
另一种选择是使用 aggregate 方法,简称 agg,并提供一个函数来应用于各列。使用 agg 方法来确定每个组的均值:
embarked_grouped.agg(np.mean)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-SIFV6CSB.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-0S7SC7MC.jpg)
图 1.18:使用 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/spr-lrn-ws/img/image-SIFV6CSB.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-0S7SC7MC.jpg)
图 1.19:使用.agg 方法与函数
注意
要访问此特定部分的源代码,请参考 https://packt.live/2NlEkgM。
你也可以在 https://packt.live/2AZnq51 上在线运行此示例。你必须执行整个 Notebook 才能获得预期的结果。
在这个练习中,我们展示了如何在 DataFrame 中对数据进行分组,从而允许使用.agg()应用额外的函数,比如计算组的均值。这类操作在分析和准备数据时非常常见。
分位数
前一个练习展示了如何找到均值。在统计数据分析中,我们也经常感兴趣的是数据集中某个值以下或以上,某个比例的点会落在其中。这些点被称为分位数。例如,如果我们有一个从 1 到 10,001 的数字序列,25%的分位数值为 2,501。也就是说,在值 2,501 处,25%的数据位于该截止值以下。分位数在数据可视化中经常使用,因为它们能传达数据分布的感觉。特别是,Matplotlib 中的标准箱线图会绘制一个由四个分位数中的第一和第三个分位数界定的箱体。
例如,让我们建立以下 DataFrame 的 25%分位数:
import pandas as pd
df = pd.DataFrame({“A”:[1, 6, 9, 9]})
#计算 DataFrame 的 25%分位数
df.quantile(0.25, axis = 0)
输出将如下所示:
A 4.75
Name: 0.25, dtype: float64
从前面的输出中可以看出,4.75 是 DataFrame 的 25%分位数值。
注意
有关分位数方法的更多信息,请参考 https://pandas.pydata.org/pandas-docs/stable/reference/frame.html。
本书稍后我们将继续使用分位数的概念,深入探讨数据。
Lambda 函数
实现 agg 的一种常见且有用的方法是通过使用 Lambda 函数。
Lambda 或匿名函数(在其他语言中也称为内联函数)是小型的单表达式函数,可以声明并使用,而无需通过 def 关键字进行正式的函数定义。Lambda 函数本质上是为了方便而提供的,通常不用于长时间的复杂任务。Lambda 函数的主要优点是它们可以在不适合或不方便定义函数的地方使用,例如在其他表达式或函数调用中。Lambda 函数的标准语法如下(总是以 lambda 关键字开头):
lambda <输入值>: <计算返回的值>
现在我们来做一个练习,创建一些有趣的 Lambda 函数。
练习 1.05:创建 Lambda 函数
在本练习中,我们将创建一个 Lambda 函数,返回列中的第一个值,并与 agg 一起使用。本练习是练习 1.01“加载和汇总泰坦尼克数据集”的延续:
将 first_val 函数写为 Lambda 函数,并传递给 agg:
embarked_grouped = df.groupby(‘Embarked’)
embarked_grouped.agg(lambda x: x.values[0])
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-888JAY5N.jpg
图 1.20:使用 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/spr-lrn-ws/img/image-JXL36A0N.jpg
图 1.21:使用 agg 方法和多个 Lambda 函数
将 numpy.sum 应用于 Fare 列,并将 Lambda 函数应用于 Age 列,通过传递一个包含列名(作为字典的键)和相应函数(作为字典的值)的字典给 agg,从而使得可以对 DataFrame 中的不同列应用不同的函数:
embarked_grouped.agg({‘Fare’: np.sum, \
‘Age’: lambda x: x.values[0]})
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZL21TYBZ.jpg
图 1.22:使用 agg 方法和包含不同列的字典
最后,使用多个列执行 groupby 方法。提供包含列列表(性别和登船港口)以进行分组,如下所示:
age_embarked_grouped = df.groupby([‘Sex’, ‘Embarked’])
age_embarked_grouped.groups
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-H8XDKHM7.jpg
图 1.23:使用 groupby 方法进行多列分组
与仅通过 Embarked 列进行分组计算时类似,我们可以看到这里返回了一个字典,其中键是 Sex 和 Embarked 列的组合,以元组形式返回。字典中的第一个键值对是元组 (‘Male’, ‘S’),值对应于具有该特定组合的行索引。每个 Sex 和 Embarked 列中唯一值的组合都会有一个键值对。
注意
要查看此特定部分的源代码,请参考 https://packt.live/2B1jAZl。
你也可以在 https://packt.live/3emqwPe 上在线运行这个示例。你必须执行整个 Notebook 才能获得期望的结果。
这标志着我们对数据检查和处理的简要探索的结束。接下来,我们将进入数据科学中最重要的话题之一——数据质量。
数据质量考虑
在任何机器学习问题中,无论是监督学习还是无监督学习,所使用数据的质量对最终模型的表现至关重要,因此在规划任何机器学习项目时,数据质量应处于首要位置。作为一个简单的经验法则,如果你拥有干净的数据、足够的数量,并且输入数据类型与期望输出之间有良好的相关性,那么选用哪种类型的监督学习模型及其具体细节对获得良好的结果变得不那么重要。
然而,实际上这种情况很少发生。通常,关于数据量、数据质量或信噪比、输入与输出之间的相关性,或三者的某种组合,都会存在一些问题。因此,我们将在本章的最后一部分考虑可能出现的一些数据质量问题以及解决这些问题的一些机制。我们之前提到,在任何机器学习问题中,深入了解数据集对于构建高性能模型至关重要。
在研究数据质量并尝试解决数据中存在的一些问题时,尤其如此。如果没有对数据集的全面理解,数据清理过程中可能会引入额外的噪声或其他未预见的问题,从而导致性能进一步下降。
注意
Titanic 数据集的详细描述和其中包含的数据类型可以在 pandas 中的加载数据部分找到。如果你需要快速回顾,可以回去复习那部分内容。
管理缺失数据
正如我们之前讨论的,pandas 读取带有缺失值的数据的能力既是一种福音也是一种诅咒,可以说是在继续开发我们的监督学习模型之前需要解决的最常见问题。最简单但不一定最有效的方法是简单地删除或忽略那些缺失数据的条目。我们可以在 pandas 中轻松地使用 DataFrame 上的 dropna 方法来实现这一点:
完整数据 = df.dropna()
仅仅删除具有缺失数据的行有一个非常重要的后果,那就是我们可能会丢失很多重要信息。这在泰坦尼克号数据集中非常明显,因为很多行包含缺失数据。如果我们简单地忽略这些行,我们将从 1,309 个样本开始,最终只剩下 183 个条目。使用略多于 10% 的数据开发一个合理的监督学习模型将非常困难。以下代码显示了使用 dropna() 方法处理缺失条目的示例:
len(df)
前面的输入产生以下输出:
1309
dropna() 方法的实现如下:
len(df.dropna())
前面的输入产生以下输出:
183
因此,除了早期的探索阶段外,简单丢弃所有具有无效信息的行通常是不可接受的。我们可以确定哪些行实际上缺少信息,以及缺失信息是某些列特有的问题还是整个数据集中所有列都存在的问题。我们也可以使用 aggregate 来帮助我们:
df.aggregate(lambda x: x.isna().sum())
输出如下:
船舱 1014
登船港口 2
票价 1
客舱等级 0
票号 0
年龄 263
姓名 0
父母/子女 0
性别 0
兄弟姐妹/配偶 0
生还 418
类型:int64
现在这很有用!我们可以看到绝大多数缺失信息在船舱列中,一些在年龄中,还有一些在生还中。这是数据清洗过程中我们可能需要做出明智判断的首次之一。
对于船舱列我们想要做什么?这里缺失的信息太多了,事实上,可能无法以任何合理的方式使用它。我们可以尝试通过查看姓名、年龄和父母/兄弟姐妹的数量来恢复信息,看看是否可以将一些家庭匹配在一起提供信息,但在这个过程中会有很多不确定性。我们也可以简化列,使用船上舱位的级别而不是确切的舱位号,这样可能会更好地与姓名、年龄和社会地位相关联。这很不幸,因为船舱和生还之间可能存在很好的相关性,也许那些在船的下层甲板上的乘客可能更难撤离。我们可以仅检查具有有效船舱值的行,看看舱位信息是否具有任何预测能力;但是,目前,我们将简单地将船舱视为一个合理的输入(或特征)而忽略。
我们可以看到,Embarked 和 Fare 列之间只有三条缺失样本。如果我们决定模型需要这两列,那么直接删除这些行是一个合理的选择。我们可以使用索引技巧来实现这一点,其中 ~ 代表取反操作,或者反转结果(也就是说,当 df.Embarked 和 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 下降,平均年龄也逐渐降低,以下代码可以看到这一点:
df_valid.loc[df.Pclass == 1, ‘Age’].mean()
上述输入会产生以下输出:
37.956806510096975
第二等级的平均年龄如下:
df_valid.loc[df.Pclass == 2, ‘Age’].mean()
上述输入会产生以下输出:
29.52440879717283
第三等级的平均年龄如下:
df_valid.loc[df.Pclass == 3, ‘Age’].mean()
上述输入会产生以下输出:
26.23396338788047
如果我们同时考虑人的性别和票务等级(社会地位)呢?这里的平均年龄也会有所不同吗?让我们来探讨一下:
for name, grp in df_valid.groupby([‘Pclass’, ‘Sex’]):
print(‘%i’ % name[0], name[1], ‘%0.2f’ % grp[‘Age’].mean())
输出将如下所示:
1 女性 36.84
1 男性 41.03
2 女性 27.50
2 男性 30.82
3 女性 22.19
3 男性 25.86
我们可以看到,所有票务等级中的男性通常年龄较大。性别和票务等级的组合提供的信息比仅仅用平均年龄填充缺失值要丰富得多。为此,我们将使用 transform 方法,它可以对 Series 或 DataFrame 的内容应用一个函数,并返回另一个具有变换值的 Series 或 DataFrame。当与 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)或兄弟姐妹数量,甚至是票务等级、性别和父母/子女数量来对数据进行分组。关于此过程中做出的决策,最重要的是最终的预测准确性。我们可能需要尝试不同的选项,重新运行模型,并考虑对最终预测准确性的影响。因此,选择能为模型提供最大预测能力的特征或组件。你会发现,在这个过程中,你会尝试不同的特征,运行模型,查看最终结果,然后重复这个过程,直到你对性能感到满意。
这个监督学习问题的最终目标是根据我们所掌握的信息预测泰坦尼克号乘客的生存情况。所以,这意味着“是否生存”这一列提供了我们训练的标签。如果我们缺少 418 个标签该怎么办?如果这是一个我们可以控制数据收集并访问数据来源的项目,我们显然可以通过重新收集数据或请求明确标签来进行修正。但在泰坦尼克号数据集中,我们没有这种能力,因此必须做出另一个有根据的判断。一个方法是从训练数据中删除这些行,之后用在(更小的)训练集上训练的模型来预测其他人的结果(这实际上是 Kaggle 泰坦尼克号比赛中的任务)。在某些商业问题中,我们可能没有简单忽略这些行的选项;我们可能在尝试预测一个非常关键过程的未来结果,而这些数据就是我们所拥有的。我们可以尝试一些无监督学习技术,看看是否能发现一些生存信息的模式以供使用。然而,通过无监督技术来估计真实标签,可能会给数据集引入显著的噪声,从而降低我们准确预测生存情况的能力。
类别不平衡
缺失数据并不是数据集中可能存在的唯一问题。类别不平衡——即一个类别或多个类别的样本数量大大超过其他类别——可能是一个显著问题,特别是在分类问题中(我们将在第五章“分类技术”中深入讨论分类问题),即我们试图预测一个样本属于哪个类别。通过查看我们的“Survived”列,可以看到数据集中死亡的人数(Survived 等于 0)远多于生还的人数(Survived 等于 1),如下代码所示:
len(df.loc[df.Survived ==1])
输出如下:
342
死亡人数为:
len(df.loc[df.Survived ==0])
输出如下:
549
如果我们不考虑这个类别不平衡问题,我们模型的预测能力可能会大大降低,因为在训练过程中,模型只需要猜测这个人没有生还,就可以正确预测 61%的时间(549 / (549 + 342))。如果实际生还率是 50%,那么在应用于未见数据时,我们的模型会过于频繁地预测“没有生还”。
管理类别不平衡有几种可选方法,其中一种方法,类似于缺失数据场景,是随机删除过度代表类的样本,直到达到平衡为止。再次强调,这种方法并不理想,甚至可能不适当,因为它涉及忽略可用的数据。一个更具建设性的例子可能是通过随机复制数据集中不足代表类的样本来进行过采样,以增加样本数量。虽然删除数据可能导致由于丢失有用信息而产生准确性问题,但对不足代表类进行过采样可能会导致无法预测未见数据的标签,这也被称为过拟合(我们将在第六章“集成建模”中讨论)。
向过采样数据的输入特征中添加一些随机噪声可能有助于防止一定程度的过拟合,但这在很大程度上取决于数据集本身。与缺失数据一样,检查任何类别不平衡修正对整体模型性能的影响非常重要。使用 append 方法将更多数据复制到 DataFrame 中是相对简单的,append 方法的工作方式与列表非常相似。如果我们想把第一行复制到 DataFrame 的末尾,可以这样做:
df_oversample = df.append(df.iloc[0])
样本量过小
机器学习领域可以视为更大统计学领域的一个分支。因此,置信度和样本量的原理也可以用于理解小数据集的问题。回想一下,如果我们从一个高方差的数据源中获取测量数据,那么这些测量的 不确定性程度也会很高,并且为了在均值的值上获得特定的置信度,我们需要更多的样本。样本原理可以应用于机器学习数据集。那些特征方差较大的数据集,通常需要更多的样本才能获得合理的性能,因为更高的置信度也是必需的。
有一些技术可以用来弥补样本量减少的问题,比如迁移学习。然而,这些技术超出了本书的范围。然而,最终来说,使用小数据集所能做的事有限,显著的性能提升可能只有在增加样本量后才会发生。
活动 1.01:实现 Pandas 函数
在这个活动中,我们将测试我们在本章中学到的各种 Pandas 函数。我们将使用相同的 Titanic 数据集进行练习。
执行的步骤如下:
打开一个新的 Jupyter 笔记本。
使用 Pandas 加载 Titanic 数据集,并对数据集使用 head
函数以显示数据集的顶部行。描述所有列的总结数据。
我们不需要 Unnamed: 0
列。在练习 1.03: 高级索引和选择中,我们演示了如何使用 del
命令删除该列。还有其他方法可以删除此列吗?不使用 del
删除此列。
计算 DataFrame 列的均值、标准差、最小值和最大值,而不使用 describe
。注意,可以使用 df.min()
和 df.max()
函数找到最小值和最大值。
使用 quantile
方法获取 33%、66% 和 99%的分位数值。
使用 groupby
方法找出每个舱位的乘客数量。
使用选择/索引方法计算每个舱位的乘客数量。可以使用 unique()
方法找出每个舱位的唯一值。
确认第 6 步和第 7 步的答案是否匹配。
确定第三舱中最年长的乘客是谁。
对于许多机器学习问题,常常需要将数值数据缩放到 0 和 1 之间。使用 agg
方法与 Lambda 函数将 Fare 和 Age 列的数据缩放到 0 和 1 之间。
数据集中有一位个体没有列出票价值,可以通过以下方式确认:
df_nan_fare = df.loc[(df.Fare.isna())]
df_nan_fare
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-WVCTUMST.jpg
图 1.24:没有列出票价的个体
使用 groupby 方法,将主数据框中该行的 NaN 值替换为与该行对应的相同类别和登船地点的平均票价值。
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-H95O9GVO.jpg
图 1.25:没有列出票价详情的个人输出
注意
本活动的解决方案可以通过此链接找到。
通过本活动,我们回顾了所有基本的数据加载、检查和操作方法,以及一些基本的总结统计方法。
总结
本章介绍了监督学习的概念,并提供了一些应用案例,包括自动化手动任务,如识别 1960 年代和 1980 年代的发型。在此介绍中,我们遇到了标记数据集的概念,以及将一个信息集(输入数据或特征)映射到相应标签的过程。我们通过实践方法,使用 Jupyter 笔记本和功能强大的 pandas 库,讲解了数据加载和清理的过程。请注意,本章仅涵盖了 pandas 功能的一小部分,实际上,整个书籍都可以专门讲解这个库。建议您熟悉阅读 pandas 文档,并通过实践继续提升您的 pandas 技能。本章的最后部分讨论了在开发高性能监督学习模型时需要考虑的一些数据质量问题,包括缺失数据、类别不平衡和样本量过小等问题。我们讨论了管理这些问题的多种选择,并强调了将这些缓解措施与模型性能进行对比检查的重要性。在下一章中,我们将扩展本章所涉及的数据清理过程,并研究数据探索和可视化过程。数据探索是任何机器学习解决方案中的关键部分,因为如果没有对数据集的全面了解,几乎不可能对所提供的信息进行建模。
第三章:2. 探索性数据分析与可视化
概述
本章带领我们了解如何对一个新的数据集进行探索和分析。到本章结束时,你将能够解释数据探索的重要性,并能够传达数据集的汇总统计信息。你将能够可视化数据中缺失值的模式,并能够适当地替换空值。你将学会识别连续特征、分类特征,并可视化各个变量的值分布。你还将能够使用相关性和可视化来描述和分析不同类型变量之间的关系。
介绍
假设我们有一个问题陈述,涉及预测某次地震是否引发了海啸。我们如何决定使用什么模型?我们对现有的数据了解多少?什么都不知道!但如果我们不了解数据,最终可能会建立一个不太可解释或不可靠的模型。在数据科学中,彻底理解我们所处理的数据非常重要,以便生成高度信息化的特征,并因此构建准确而强大的模型。为了获得这种理解,我们对数据进行探索性分析,看看数据能告诉我们关于特征和目标变量(你试图通过其他变量预测的值)之间关系的信息。了解数据甚至有助于我们解释所构建的模型,并找出改进其准确性的方法。我们采取的做法是让数据揭示其结构或模型,这有助于我们获得一些新的、往往是意想不到的见解。
我们将首先简要介绍探索性数据分析,然后逐步解释汇总统计和中心值。本章还将教你如何查找和可视化缺失值,并描述处理缺失值问题的各种填充策略。接下来的部分将专注于可视化。具体来说,本章教你如何创建各种图表,如散点图、直方图、饼图、热图、配对图等。让我们从探索性数据分析开始。
探索性数据分析(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 列。
在我们开始之前,首先导入所需的库,这些库将用于我们大多数的数据操作和可视化。
在 Jupyter Notebook 中,导入以下库:
import json
import pandas as pd
import numpy as np
import missingno as msno
from sklearn.impute import SimpleImputer
import matplotlib.pyplot as plt
import seaborn as sns
我们还可以读取包含每列数据类型的元数据,这些数据以 JSON 文件的形式存储。使用以下命令来读取。此命令以可读格式打开文件,并使用 json 库将文件读取为字典:
with open(‘…\dtypes.json’, ‘r’) as jsonfile:
dtyp = json.load(jsonfile)
注意
前述命令的输出可以在这里找到:https://packt.live/3a4Zjhm
概要统计和中心值
为了了解我们的数据真实情况,我们使用一种称为数据概况的技术。数据概况被定义为检查现有信息来源(例如,数据库或文件)中的数据,并收集该数据的统计信息或信息性摘要的过程。其目的是确保你充分理解数据,并能够及早识别数据可能在项目中带来的挑战,这通常通过总结数据集并评估其结构、内容和质量来实现。
数据概况包括收集描述性统计信息和数据类型。常见的数据概况命令包括你之前见过的命令,例如 data.describe()、data.head() 和 data.tail()。你还可以使用 data.info(),它会告诉你每列中有多少非空值,以及这些值的数据类型(非数字类型表示为对象类型)。
练习 2.01:总结我们的数据集统计信息
在本练习中,我们将使用之前学到的概要统计函数,初步了解我们的数据集:
注意
数据集可以在我们的 GitHub 仓库中找到,链接如下:https://packt.live/2TjU9aj
将地震数据读取到 pandas DataFrame 中,并使用我们在前一节中使用 json 库读取的 dtyp 字典,指定 CSV 中每列的数据类型。首先加载所需的库和我们已经准备好的包含数据类型的 JSON 文件。你可以在读取数据之前检查数据类型:
import json
import pandas as pd
import numpy as np
import missingno as msno
from sklearn.impute import SimpleImputer
import matplotlib.pyplot as plt
import seaborn as sns
with open(‘…/dtypes.json’, ‘r’) as jsonfile:
dtyp = json.load(jsonfile)
dtyp
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-J7XDCDBE.jpg
图 2.1:检查数据类型
使用 data.info() 函数获取数据集的概览:
data = pd.read_csv(‘…/Datasets/earthquake_data.csv’, dtype = dtyp)
data.info()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-R6JIB68X.jpg
图 2.2:数据集概览
打印数据集的前五行和最后五行。前五行打印如下:
data.head()
data.tail()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-2B3IJSFX.jpg
图 2.3:前五行和最后五行
我们可以从这些输出中看到,数据集有 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/spr-lrn-ws/img/image-1BKB9Y9V.jpg
图 2.4:汇总统计信息
注意
要访问该特定部分的源代码,请参考 packt.live/2Yl5qer
。
你也可以在线运行这个示例,访问 packt.live/2V3I76D
。你必须执行整个 Notebook 才能得到期望的结果。
请注意,这里 describe() 函数只显示数值型列的统计信息。这是因为我们无法对非数值型列计算统计数据(尽管我们可以像稍后所见那样可视化它们的值)。
缺失值
当数据点的某个特征没有记录值(即为空值)时,我们称数据为缺失数据。在实际数据集中出现缺失值是不可避免的;没有数据集是完美的。然而,理解数据缺失的原因以及是否有某些因素影响了数据丢失非常重要。理解并识别这一点可以帮助我们以合适的方式处理其余数据。例如,如果数据是随机缺失的,那么剩余的数据很可能仍然能够代表整个数据集。但如果缺失的数据并非随机缺失,而我们假设它是随机的,这可能会导致我们的分析和后续建模出现偏差。
让我们来看一下缺失数据的常见原因(或机制):
完全随机缺失(MCAR):如果数据集中缺失的值与任何其他记录的变量或外部参数之间没有任何相关性,那么这些缺失值被称为 MCAR。这意味着其余数据仍然能够代表整个群体,尽管这种情况很少发生,且假设缺失数据是完全随机的通常是不现实的。
例如,在一项研究中,如果要确定 K12 学生肥胖的原因,MCAR 的情况是父母忘记带孩子去诊所参加研究。
随机缺失(MAR):如果数据缺失的原因与已记录的数据相关,而与未记录的数据无关,那么这些数据就被称为 MAR。由于无法通过统计方法验证数据是否为 MAR,我们只能依赖是否存在合理的可能性来判断。
以 K12 研究为例,缺失数据的原因是父母搬到了其他城市,导致孩子不得不退出研究;缺失与研究本身无关。
非随机缺失(MNAR):既不是 MAR 也不是 MCAR 的数据被称为 MNAR。这种情况通常是不可忽略的非响应情况,也就是说,缺失的变量值与其缺失的原因有关。
继续使用案例研究的例子,如果父母因为研究的性质感到不悦,不希望孩子被欺负,因此将孩子从研究中撤出,那么数据就是 MNAR(非随机缺失)。
查找缺失值
所以,现在我们知道了为什么了解数据缺失背后的原因如此重要,接下来让我们讨论如何在数据集中找到这些缺失值。对于一个 pandas DataFrame,最常见的做法是使用 .isnull()
方法,这个方法会在 DataFrame 上创建一个缺失值的掩码(即一个布尔值的 DataFrame),用来指示缺失值的位置——任何位置上为 True 的值表示该位置是缺失值,而 False 则表示该位置有有效值。
注意
.isnull()
方法与 .isna()
方法可以互换使用,在 pandas DataFrame 中,两者做的事情完全相同——之所以有两个方法做同一件事,是因为 pandas DataFrame 最初是基于 R DataFrame 开发的,因此复用了很多 R DataFrame 的语法和思想。
缺失数据是否随机可能一开始并不明显。通过两种常见的可视化技术,我们可以发现数据集中特征的缺失值性质:
缺失矩阵:这是一种数据密集型的显示方式,可以让我们快速可视化数据完成情况的模式。它可以帮助我们快速查看一个特征(以及多个特征)中的缺失值是如何分布的,缺失值的数量是多少,以及它们与其他特征的关联频率。
空值相关性热图:该热图直观地描述了每对特征之间的空值关系(或数据完整性关系);即它衡量了一个变量的存在或缺失如何影响另一个变量的存在。
类似于常规的相关性分析,空值相关性值的范围从 -1 到 1,前者表示一个变量出现时,另一个变量肯定不出现,后者则表示两个变量同时存在。值为 0 表示一个变量的空值对另一个变量的空值没有影响。
练习 2.02:可视化缺失值
让我们首先通过查看每个特征的缺失值数量和百分比,来分析缺失值的性质,然后使用 Python 中的 missingno 库绘制空值矩阵和相关性热图。我们将继续使用前面练习中的相同数据集。
请注意,本练习是练习 2.01:总结我们的数据集统计信息的延续。
以下步骤将帮助你完成这个练习,以可视化数据集中的缺失值:
计算每列缺失值的数量和百分比,并按降序排列。我们将使用 .isnull()
函数对 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/spr-lrn-ws/img/image-Z70NA5T0.jpg
图 2.5:每列缺失值的数量和百分比
在这里,我们可以看到 state、total_damage_millions_dollars 和 damage_millions_dollars 列的缺失值超过 90%,意味着数据集中这些列的可用数据点不到 10%。另一方面,year、flag_tsunami、country 和 region_code 列没有缺失值。
绘制空值矩阵。首先,我们使用 .any()
函数从前一步骤的 DataFrame mask 中找出包含空值的列。然后,我们使用 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/spr-lrn-ws/img/image-G34KOPOB.jpg
图 2.6:缺失值矩阵
在这里,黑色线条表示非空值,而白色线条表示该列中存在缺失值。总体来看,location_name 列似乎完全填充(我们从前面的步骤中得知,这一列实际上只有一个缺失值),而 latitude 和 longitude 列似乎大部分完整,但有些地方较为稀疏。
右侧的火花图总结了数据完整性的整体形态,并指出了数据集中最大和最小缺失值所在的行。请注意,这仅适用于 500 个数据点的样本。
绘制缺失值相关性热图。我们将使用 missingno 库绘制缺失值相关性热图,仅针对那些存在缺失值的列:
msno.heatmap(data[nullable_columns], figsize=(18,18))
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-UT14Y4SX.jpg
图 2.7:缺失值相关性热图
在这里,我们还可以看到一些标有<1 的框:这仅意味着这些情况下的相关性值都接近 1.0,但仍然不是完全一致。我们可以看到在 injuries 和 total_injuries 之间有<1 的值,这意味着每个类别中的缺失值是相关的。我们需要深入挖掘,了解这些缺失值是否因为基于相同或类似的信息而相关,或者是出于其他原因。
注意
要访问此特定部分的源代码,请参考 https://packt.live/2YSXq3k。
您也可以在 https://packt.live/2Yn3Us7 上在线运行此示例。您必须执行整个 Notebook 才能获得所需的结果。
缺失值的填充策略
处理列中缺失值有多种方法。最简单的方法是直接删除包含缺失值的行;然而,这可能会导致丧失其他列中的有价值信息。另一种选择是对数据进行填充,即用从已知数据部分推断出的有效值替换缺失值。常见的填充方法如下:
创建一个与其他值不同的新值,以替换列中的缺失值,从而区分这些行。然后,使用非线性机器学习算法(如集成模型或支持向量机),将这些值分离出来。
使用列中的适当中心值(均值、中位数或众数)来替换缺失值。
使用模型(例如 K 近邻或高斯混合模型)来学习最佳值,以替换缺失值。
Python 有一些函数可以用来将列中的空值替换为静态值。实现这一点的一种方式是使用 pandas 本身的.fillna(0)函数:这里没有歧义——替换空数据点的静态值就是传递给函数的参数(括号中的值)。
然而,如果列中空值的数量较大,并且无法立即确定可以用来替换每个空值的合适中心值,那么我们可以从建模的角度出发,选择删除含有空值的行,或者直接删除整个列,因为它可能不会增加任何重要的价值。这可以通过在 DataFrame 上使用.dropna()函数来完成。可以传递给该函数的参数如下:
axis: 这定义了是删除行还是列,具体取决于为参数分配 0 或 1 的值。
how: 可以为此参数分配“all”或“any”值,表示行/列是否应包含所有空值才能删除该列,或者是否应在至少有一个空值时删除该列。
thresh: 这定义了行/列应具有的最小空值数量,才会被删除。
此外,如果无法确定一个适当的替代值来填充分类特征的空值,那么删除该列的一个可能替代方案是为该特征创建一个新的类别,用来表示空值。
注意
如果可以通过直观理解或领域知识立即得出一个适合的值来替换列中的空值,那么我们可以当场进行替换。请记住,任何此类数据更改应在代码中进行,而绝不是直接在原始数据上操作。这样做的一个原因是它使得将来可以轻松地更新策略;另一个原因是它使得其他人如果以后审查这项工作时,可以清楚地看到在哪里进行了更改。直接更改原始数据可能会导致数据版本控制问题,并使得其他人无法重现你的工作。在很多情况下,推断会在后续的探索阶段变得更加明显。在这种情况下,我们可以在找到合适的方法时,随时替换空值。
练习 2.03:使用 Pandas 进行填充
让我们来看一下缺失值,并在具有至少一个空值的基于时间(连续)的特征中将它们替换为零(如月份、日期、小时、分钟和秒)。我们这么做是因为对于那些没有记录值的情况,可以安全地假设事件发生在时间段的开始。这项操作是练习 2.02:可视化缺失值的延续:
创建一个包含我们想要填充的列名的列表:
time_features = [‘month’, ‘day’, ‘hour’, ‘minute’, ‘second’]
使用 .fillna() 填充空值。我们将使用 pandas 自带的 .fillna() 函数,并传递 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/spr-lrn-ws/img/image-Z1ZGRYK5.jpg
图 2.8:空值计数
如今,我们可以看到数据框中所有特征的值都不再是空值。
注意
要访问该特定部分的源代码,请参阅 https://packt.live/2V9nMx3。
你也可以在线运行这个示例,网址为 https://packt.live/2BqoZZM。你必须执行整个 Notebook 才能得到预期的结果。
练习 2.04:使用 Scikit-Learn 执行填充
在本练习中,你将使用 scikit-learn 的 SimpleImputer 类来替换描述相关的类别特征中的空值。在练习 2.02:可视化缺失值中,我们看到几乎所有这些特征都包含超过 50% 的缺失值。用中央值替换这些缺失值可能会导致我们构建的模型产生偏差,从而认为它们无关紧要。我们不妨将这些空值替换为一个单独的类别,赋值为 NA。本练习是练习 2.02:可视化缺失值的延续:
创建一个包含我们希望填充其值的列名的列表:
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:指定用来替换所有缺失值的值。如果保持默认值,当填充数值数据时,填充值将为 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/spr-lrn-ws/img/image-PSVVIOTZ.jpg
图 2.9:空值计数
注
要访问此特定部分的源代码,请参考 https://packt.live/3ervLgk。
你也可以在线运行这个示例,访问 https://packt.live/3doEX3G。你必须执行整个 Notebook 才能得到预期的结果。
在前两个练习中,我们研究了使用 pandas 和 scikit-learn 方法进行缺失值插补的两种方法。这些方法是我们在对底层数据几乎没有或没有任何信息时可以使用的非常基础的方法。接下来,我们将看看我们可以使用的更高级的技术来填补缺失数据。
练习 2.05:使用推断值进行插补
让我们用来自类别 damage_description 特征的信息替换连续的 damage_millions_dollars 特征中的空值。虽然我们可能不知道具体的损失金额,但类别特征能提供关于地震损失金额区间的信息。这个练习是练习 2.04(使用 scikit-learn 进行插补)的延续:
查找多少行具有空的 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])
输出结果如下:
5594
3849
正如我们所见,3849 个空值(在 5594 个空值中)可以通过另一个变量轻松替代。例如,我们知道所有列名以 _description 结尾的变量是描述字段,包含一些原始数值列可能缺失的数据估算值。对于死亡、伤害和总伤害,相关的类别值表示如下:
0 = 无
1 = 少量(~1 到 50 死亡)
2 = 一些(~51 到 100 死亡)
3 = 多(~101 到 1000 死亡)
4 = 非常多(~1001 或更多死亡)
关于 damage_millions_dollars,相应的类别值表示如下:
0 = 无
1 = 有限(大致对应不到 100 万美元)
2 = 中等(~1 到 500 万美元)
3 = 严重(~>5 到 2400 万美元)
4 = 极端(~2500 万美元或更多)
查找每个类别的平均 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/spr-lrn-ws/img/image-QMB0CEWJ.jpg
图 2.10:每个类别的平均 damage_millions_dollars 值
请注意,前面定义的前三个值具有直观意义:0.42 位于 0 和 1 之间,3.1 位于 1 和 5 之间,13.8 位于 5 和 24 之间。最后一个类别定义为 2500 万或更多;事实证明,这些极端情况的均值非常高(3575!)。
将均值存储为字典。在这一步,我们将包含均值的 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/spr-lrn-ws/img/image-M7BYA4VU.jpg
图 2.11:均值字典
创建一系列替代值。对于 damage_description 列中的每个值,我们使用 map 函数将类别值映射到均值。使用 .map() 函数将列中的键映射到 replacement_values 字典中对应元素的值:
imputed_values = data.damage_description.map(replacement_values)
替换列中的空值。我们通过使用 np.where 作为三元运算符来完成这一步:第一个参数是掩码,第二个参数是当掩码为正时取值的系列,第三个参数是当掩码为负时取值的系列。
这确保了 np.where 返回的数组只会用 imputed_values 系列中的值替换 damage_millions_dollars 中的空值:
data[‘damage_millions_dollars’] = \
np.where(data.damage_millions_dollars.isnull(), \
data.damage_description.map(replacement_values), \
data.damage_millions_dollars)
使用 .info() 函数查看插补列的空值计数:
data[[‘damage_millions_dollars’]].info()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-H5UW1GNA.jpg
图 2.12:空值计数
我们可以看到,在替换后,damage_millions_dollars 列中没有空值。
注意
要访问此特定部分的源代码,请参阅 https://packt.live/3fMRqQo。
您还可以在 https://packt.live/2YkBgYC 上在线运行此示例。您必须执行整个 Notebook,才能获得预期结果。
在本节中,我们已研究了多种替换缺失值的方法。某些情况下,我们将缺失值替换为零;另一些情况下,我们通过进一步了解数据集的信息来推测,可以用描述性字段的信息和我们已有值的均值来替换缺失值。在处理真实数据时,这类决策和步骤非常常见。我们还注意到,偶尔在数据足够充分且缺失值实例较少的情况下,我们可以直接删除这些实例。在接下来的活动中,我们将使用不同的数据集,以帮助你练习并巩固这些方法。
活动 2.01:汇总统计与缺失值
在本活动中,我们将回顾本章到目前为止我们所研究的一些汇总统计和缺失值探索内容。我们将使用一个新的数据集“房价:高级回归技术”,该数据集可在 Kaggle 上获得。
注意
原始数据集可通过以下链接获取:https://www.kaggle.com/c/house-prices-advanced-regression-techniques/data,或者访问我们的 GitHub 仓库:https://packt.live/2TjU9aj。
虽然用于练习的地震数据集是为了处理分类问题(当目标变量只有离散值时),但我们在活动中将使用的数据集是为了解决回归问题(当目标变量为连续值范围时)。我们将使用 pandas 函数生成汇总统计,并通过空值矩阵和空值相关性热图来可视化缺失值。
执行的步骤如下:
读取数据(house_prices.csv)。
使用 pandas 的 .info() 和 .describe() 方法查看数据集的汇总统计。
info() 方法的输出如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-Q9SXD9EK.jpg
图 2.13:info() 方法的输出(简略版)
info() 方法的输出如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-99VP2HBT.jpg
图 2.14:describe() 方法的输出(简略版)
注意
为了展示方便,info() 和 describe() 方法的输出已被截断。你可以在这里找到完整的输出:https://packt.live/2TjZSgi
查找 DataFrame 中每一列的缺失值总数和缺失值百分比,并按缺失百分比降序显示至少有一个空值的列。
绘制空值矩阵和空值相关性热图。
空值矩阵如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-TTRK4BON.jpg
图 2.15:空值矩阵
空值相关性热图如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-QT4NJDMU.jpg
图 2.16:空值相关性热图
删除缺失值超过 80% 的列。
将 FireplaceQu 列中的空值替换为 NA 值。
注意
此活动的解决方案可通过此链接找到。
现在,您应该能够使用我们学到的方法来调查任何类型表格数据中的缺失值。
值的分布
在本节中,我们将研究各个变量的行为——它们取什么样的值,这些值的分布情况如何,以及如何通过视觉手段表示这些分布。
目标变量
目标变量可以是连续值(在回归问题中)或离散值(如分类问题中)。本章我们研究的问题涉及预测地震是否引发海啸,即 flag_tsunami 变量,该变量只有两个离散值,使其成为分类问题。
一种可视化方法是使用柱状图来显示有多少地震引发了海啸,有多少没有引发。在柱状图中,每个柱子代表变量的一个单一离散值,柱子的高度等于具有相应离散值的数据点的计数。这使我们能够很好地比较每个类别的绝对计数。
Exercise 2.06: 绘制柱状图
让我们看看我们的数据集中有多少地震引发了海啸。我们将使用 value_counts() 方法对列进行操作,并直接在返回的 pandas 系列上使用 .plot(kind=‘bar’) 函数。这个练习是 Exercise 2.05: 使用推断值进行插补的延续:
使用 plt.figure() 开始绘图:
plt.figure(figsize=(8,6))
接下来,键入我们的主要绘图命令:
data.flag_tsunami.value_counts().plot(kind=‘bar’, \
color = (‘grey’, \
‘black’))
设置显示参数并显示图表:
plt.ylabel(‘数据点数目’)
plt.xlabel(‘flag_tsunami’)
plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-2DJC2J7X.jpg
图 2.17: 显示引发海啸的地震数量的柱状图
从这个条形图中,我们可以看到大多数地震并没有引发海啸,实际上不到三分之一的地震确实引发了海啸。这显示数据集略微不平衡。
注:
要访问此特定部分的源代码,请参考 https://packt.live/2Yn4UfR。
您还可以在线运行此示例,网址为 https://packt.live/37QvoJI。您必须执行整个笔记本才能获得所需的结果。
让我们更仔细地看看这些 Matplotlib 命令的作用:
plt.figure(figsize=(8,6)): 此命令定义了我们的图表大小,提供了宽度和高度数值。这是在任何绘图命令之前始终首先执行的命令。
plt.xlabel() 和 plt.ylabel():这些命令接受字符串作为输入,并允许我们指定绘图的 X 和 Y 轴标签应该是什么。
plt.show():这是绘制可视化时所写的最终命令,用于在 Jupyter 笔记本中内联显示图表。
类别数据
类别变量是那些取离散值,表示不同类别或观察级别的变量,这些值可以是字符串对象或整数值。例如,我们的目标变量 flag_tsunami 是一个类别变量,有两个类别,分别是 Tsu 和 No。
类别变量可以分为两种类型:
名义变量:没有优先顺序的类别标签的变量称为名义变量。我们数据集中的一个名义变量例子是 location_name。这个变量的值不能说是有顺序的,即一个位置不大于另一个位置。同样,更多这样的变量例子包括颜色、鞋类类型、种族类型等。
有序变量:具有某种顺序关系的变量称为有序变量。我们数据集中的一个例子是 damage_description,因为每个值表示一个逐渐增加的损害程度。另一个例子可以是星期几,它的值从星期一到星期天,具有一定的顺序关系,我们知道星期四在星期三之后但在星期五之前。
虽然有序变量可以用对象数据类型表示,但它们通常也用数值数据类型表示,这常常使得它们与连续变量之间的区别变得困难。
处理数据集中的类别变量时面临的一个主要挑战是高基数,即类别或不同值的数量非常大,每个值出现的次数相对较少。例如,location_name 有大量唯一的值,每个值在数据集中出现的次数很少。
此外,非数值的类别变量总是需要进行某种形式的预处理,以将其转换为数值格式,以便可以供机器学习模型进行训练。如何将类别变量转换为数值而不丢失上下文信息是一大挑战,尽管这些信息对于人类来说容易理解(由于领域知识或常识),但对于计算机来说却很难自动理解。例如,像国家或位置名称这样的地理特征本身无法表明不同值之间的地理接近性,但这可能是一个重要特征——如果东南亚地区发生的地震比欧洲发生的地震引发更多的海啸呢?仅通过将该特征编码为数值,无法捕捉到这种信息。
练习 2.07:识别类别变量的数据类型
让我们确定 Earthquake 数据集中哪些变量是分类变量,哪些是连续变量。正如我们现在所知道的,分类变量也可以具有数值,因此仅有数值数据类型并不能保证变量是连续的。本练习是练习 2.05:使用推测值进行插补的延续:
查找所有数值型和对象型的列。我们在 DataFrame 上使用 .select_dtypes() 方法,创建一个子集 DataFrame,包含数值型(np.number)和分类型(np.object)列,然后打印每个列的列名。对于数值列,使用以下命令:
numeric_variables = data.select_dtypes(include=[np.number])
numeric_variables.columns
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-76AZUMB9.jpg
图 2.18:所有数值型列
对于分类列,使用以下命令:
object_variables = data.select_dtypes(include=[np.object])
object_variables.columns
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-81ULCRD2.jpg
图 2.19:所有对象类型的列
在这里,显然,属于对象类型的列是分类变量。为了区分数值列中的分类变量和连续变量,让我们看看每个特征的唯一值数量。
查找数值特征的唯一值个数。我们在 DataFrame 上使用 select_dtypes 方法,查找每列中的唯一值数量,并按升序排列结果序列。对于数值列,使用以下命令:
numeric_variables.nunique().sort_values()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-AUNV0ACJ.jpg
图 2.20:数值特征的唯一值数量
对于分类列,使用以下命令:
object_variables.nunique().sort_values()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-YC1T58GJ.jpg
图 2.21:分类列的唯一值数量
注意
要访问此特定部分的源代码,请参考 https://packt.live/2YlSmFt。
您也可以在 https://packt.live/31hnuIr 在线运行此示例。您必须执行整个 Notebook 才能得到预期的结果。
对于数值变量,我们可以看到前九个变量具有显著较少的唯一值,远少于其余行,因此这些变量很可能是分类变量。然而,我们必须记住,某些变量可能只是具有较低范围的四舍五入值的连续变量。另外,月份和日期在这里不应视为分类变量。
练习 2.08:计算分类值的计数
对于具有分类值的列,查看特征的唯一值(类别)及其频率是非常有用的,即每个不同值在数据集中出现的频率。让我们找出 injuries_description 分类变量中每个 0 到 4 标签和 NaN 值的出现次数。此练习是练习 2.07:识别分类变量的数据类型的延续:
使用 value_counts() 函数对 injuries_description 列进行操作,找出每个类别的频率。使用 value_counts 会以降序的形式返回每个值的频率,结果为 pandas 系列:
counts = data.injuries_description.value_counts(dropna=False)
计数
输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-UW9LQJF8.jpg
图 2.22:每个类别的频率
按顺序变量的升序排序值。如果我们希望按值本身的频率排序,我们可以重置索引以得到一个 DataFrame,并按索引排序(即按顺序变量):
counts.reset_index().sort_values(by=‘index’)
输出如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-TGI2TTM4.jpg
图 2.23:排序后的值
注意
要访问此特定部分的源代码,请参考 https://packt.live/2Yn5URj。
你也可以在 https://packt.live/314dYIr 上在线运行此示例。你必须执行整个 Notebook 才能得到所需结果。
练习 2.09:绘制饼图
由于我们样本数据中的目标变量是分类变量,在练习 2.06:绘制柱状图中,我们展示了可视化分类值分布的一种方法(使用柱状图)。另一个可以清楚显示每个类别如何作为整体数据集的一部分的方法是饼图。我们将绘制一个饼图来可视化 damage_description 变量的离散值分布。此练习是练习 2.08:计算类别值计数的延续:
将数据格式化为需要绘制的形式。在这里,我们对列运行 value_counts() 并按索引排序系列:
counts = data.damage_description.value_counts()
counts = counts.sort_index()
绘制饼图。plt.pie() 类别绘制饼图并使用计数数据。我们将使用在练习 2.06:绘制柱状图中描述的相同三个步骤进行绘制:
fig, ax = plt.subplots(figsize=(10,10))
slices = ax.pie(counts, \
labels=counts.index, \
colors = [‘white’], \
wedgeprops = {‘edgecolor’: ‘black’})
patches = slices[0]
hatches = [‘/’, ‘\’, ‘|’, ‘-’, ‘+’, ‘x’, ‘o’, ‘O’, ‘.’, ‘*’]
对每个补丁执行循环,范围为补丁数量:
patches[patch].set_hatch(hatches[patch])
plt.title('饼图显示\ndamage_description 的计数 '\
‘类别’)
plt.show()
输出如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-5PCI6LVT.jpg
图 2.24:饼图显示损害描述类别的计数
注意
要访问此特定部分的源代码,请参考 https://packt.live/37Ovj9s。
你也可以在 https://packt.live/37OvotM 在线运行这个示例。你必须执行整个 Notebook 才能获得预期的结果。
图 2.24 展示了五个损害描述类别中每个类别的相对项目数量。请注意,最好做额外的工作将无意义的标签更改为类别标签——回忆一下从 EDA 讨论中提到的:
0 = 无
1 = 有限(大致对应于低于 100 万美元)
2 = 中等(约 100 万到 500 万美元)
3 = 严重(约 500 万到 2400 万美元)
4 = 极端(约 2500 万美元或更多)
此外,虽然饼图能给我们一个快速的视觉印象,告诉我们哪些是最大和最小的类别,但它并没有提供实际的数量信息,因此添加这些标签将提升图表的价值。你可以使用本书中仓库里的代码来更新图表。
连续数据
连续变量可以取任何数量的值,通常是整数(例如,死亡人数)或浮动数据类型(例如,山的高度)。了解特征值的基本统计信息是非常有用的:从 describe()函数的输出中得到的最小值、最大值和百分位数值可以为我们提供一个合理的估计。
然而,对于连续变量,查看其在所处范围内的分布情况也是非常有用的。由于我们不能简单地找到单个值的计数,因此我们将值按升序排列,分组为均匀大小的区间,并计算每个区间的计数。这为我们提供了底层的频率分布,并绘制这个分布会得到一个直方图,这让我们能够检查数据的形状、中心值和变异性。
直方图为我们提供了一个便捷的视图,让我们可以一眼看出数据的表现,包括底层分布(例如,正态分布或指数分布)、异常值、偏斜程度等。
注意
很容易将条形图和直方图混淆。主要的区别在于,直方图用于绘制已分箱的连续数据,用以可视化频率分布,而条形图可以用于多种其他用途,包括表示分类变量,正如我们所做的那样。此外,在直方图中,箱数是可以变化的,因此箱内值的范围由箱数决定,直方图中条形的高度也是如此。在条形图中,条形的宽度通常不具有传达意义,而高度通常是类别的属性,比如计数。
最常见的频率分布之一是高斯(或正态)分布。这是一个对称分布,具有钟形曲线,表示数据集中中间范围附近的值出现的频率最高,随着远离中间,出现的频率对称性地减少。你几乎肯定见过高斯分布的例子,因为许多自然和人为过程生成的值几乎呈现高斯分布。因此,数据与高斯分布的比较是非常常见的。
它是一个概率分布,曲线下方的面积等于 1,如图 2.25 所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-5FZD4VW6.jpg
图 2.25:高斯(正态)分布
像正态分布这样的对称分布可以通过两个参数完全描述——均值(µ)和标准差(σ)。例如,在图 2.25 中,均值为 7.5。然而,许多实际数据并不遵循正态分布,可能是非对称的。数据的非对称性通常被称为偏斜。
偏度
如果一个分布不对称,则称为偏斜,偏度衡量的是变量相对于其均值的非对称性。其值可以为正、负(或未定义)。在前一种情况下,尾部位于分布的右侧,而后一种情况则表示尾部位于左侧。
但是,必须注意的是,粗短的尾部与细长的尾部对偏度值的影响是相同的。
峰度
峰度是衡量变量分布尾部程度的指标,用于衡量一个尾部相对于另一个尾部的异常值情况。高峰度值表示尾部较胖,并且存在异常值。与偏度概念类似,峰度也描述了分布的形状。
练习 2.10:绘制直方图
让我们使用 Seaborn 库绘制 eq_primary 特征的直方图。这个练习是练习 2.09,绘制饼图的延续:
使用 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/spr-lrn-ws/img/image-DME453BH.jpg
图 2.26:示例主特征的直方图
图表为我们提供了一个标准化(或归一化)的直方图,这意味着直方图柱形下方的面积等于 1。此外,直方图上方的线是核密度估计,它能给我们一个变量概率分布的大致形态。
注意
若要访问此特定部分的源代码,请参考 https://packt.live/2BwZrdj。
你也可以在网上运行此示例,网址为 https://packt.live/3fMSxj2。你必须执行整个 Notebook 才能获得期望的结果。
从图表中,我们可以看到 eq_primary
的值大多介于 5 和 8 之间,这意味着大多数地震的震级处于中等至高值范围,几乎没有地震的震级是低值或极高值。
练习 2.11:计算偏度和峰度
让我们使用 pandas 中的核心函数来计算数据集中所有特征的偏度和峰度值。此练习是练习 2.10: 绘制直方图的延续:
使用 .skew() DataFrame 方法计算所有特征的偏度,然后按升序排列这些值:
data.skew().sort_values()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-V0JJI54Q.jpg
图 2.27: 数据集中所有特征的偏度值
使用 .kurt() DataFrame 方法计算所有特征的峰度:
data.kurt()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-WUUXAE1Y.jpg
图 2.28: 数据集中所有特征的峰度值
这里我们可以看到,某些变量的峰度值与 0 的偏差较大。这意味着这些列具有长尾分布。但是,这些变量尾部的值(表示死亡人数、受伤人数和损失金额),在我们的案例中,可能是我们需要特别关注的异常值。较大的值实际上可能表示有额外的力量加剧了地震带来的破坏,即海啸。
注意
若要访问此特定部分的源代码,请参考 https://packt.live/2Yklmh0。
你也可以在网上运行此示例,网址为 https://packt.live/37PcMdj。你必须执行整个 Notebook 才能获得期望的结果。
活动 2.02:可视化表示值的分布
在此活动中,我们将通过创建不同的图表,如直方图和饼图,来实现上一节中学到的内容。此外,我们将计算数据集特征的偏度和峰度。这里我们将使用在活动 2.01: 总结统计与缺失值中使用的相同数据集,即“房价:高级回归技巧”数据集。我们将使用不同类型的图表来可视化该数据集的值分布。本活动是活动 2.01: 总结统计与缺失值的延续:
执行的步骤如下:
使用 Matplotlib 绘制目标变量 SalePrice 的直方图。
输出结果如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-5YLC5971.jpg
图 2.29:目标变量的直方图
找出每个列中具有对象类型的唯一值的数量。
创建一个数据框,表示 HouseStyle 列中每个类别值的出现次数。
绘制表示这些计数的饼图。
输出结果如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-IYKYADF1.jpg
图 2.30:表示计数的饼图
找出每个列中具有数字类型的唯一值的数量。
使用 seaborn 绘制 LotArea 变量的直方图。
输出结果如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-JBM6D62T.jpg
图 2.31:LotArea 变量的直方图
计算每列值的偏斜度和峰度值。
偏斜度值的输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-8QWHUC43.jpg
图 2.32:每列的偏斜值
对峰度值的输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-XPECYU25.jpg
图 2.33:每列的峰度值
注意
本活动的解决方案可以通过此链接找到。
我们已经看到了如何更详细地了解数据的性质,特别是通过开始理解数据的分布,使用直方图或密度图、数据的相对计数使用饼图,以及检查变量的偏斜度和峰度,作为发现潜在问题数据、异常值等的第一步。
到现在为止,你应该已经能够熟练处理各种数据统计量,例如摘要统计、计数和数值分布。使用直方图和密度图等工具,你可以探索数据集的形状,并通过计算如偏斜度和峰度等统计量来增强对数据集的理解。你应该逐渐培养一些直觉,识别出需要进一步调查的标志,如较大的偏斜度或峰度值。
数据中的关系
寻找数据中变量之间关系的重要性有两个原因:
确定哪些特征可能重要是至关重要的,因为找到与目标变量有强关系的特征有助于特征选择过程。
寻找不同特征之间的关系也很有用,因为数据集中的变量通常不是完全独立的,这会以多种方式影响我们的建模。
现在,有多种方式可以可视化这些关系,这主要取决于我们试图找到关系的变量类型,以及我们在方程或比较中考虑的变量数量。
两个连续变量之间的关系
建立两个连续变量之间的关系基本上是观察其中一个变量在另一个变量增加时如何变化。最常见的可视化方法是使用散点图,我们将每个变量沿单一轴(当有两个变量时是二维平面中的 X 和 Y 轴)绘制,并在 X-Y 平面中使用标记绘制每个数据点。这种可视化方式可以很好地帮助我们了解这两个变量之间是否存在某种关系。
然而,如果我们想量化两个变量之间的关系,最常见的方法是找出它们之间的相关性。如果目标变量是连续的,并且与另一个变量有很高的相关性,这表明该特征将在模型中占据重要地位。
皮尔逊相关系数
皮尔逊相关系数是常用的相关系数,用于展示一对变量之间的线性关系。该公式返回一个介于 -1 和 +1 之间的值,其中:
+1 表示强正相关
-1 表示强负相关
0 表示完全没有关系
找出特征对之间的相关性也非常有用。在某些模型中,高度相关的特征可能会引发问题,包括系数在数据或模型参数发生微小变化时剧烈波动。在极端情况下,完全相关的特征(例如 X2 = 2.5 * X1)会导致某些模型(包括线性回归)返回未定义的系数(如 Inf)。
注意
在拟合线性模型时,特征之间的高度相关性可能会导致模型不可预测且变化较大。这是因为线性模型中每个特征的系数可以解释为在保持其他特征不变的情况下,目标变量的单位变化。然而,当一组特征不是独立的(即它们是相关的)时,我们无法确定每个特征对目标变量的独立变化影响,导致系数大幅波动。
要查找 DataFrame 中每个数值特征与其他特征之间的配对相关性,我们可以使用 DataFrame 上的 .corr()
函数。
练习 2.12:绘制散点图
我们来绘制一个散点图,X 轴为主要地震震级,Y 轴为相应的受伤人数。此练习是练习 2.11(计算偏度和峰度)的延续:
筛选出非空值。由于我们知道两列中都有空值,首先我们筛选出只包含非空行的数据:
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(‘主要地震震级’)
plt.ylabel(‘受伤人数’)
plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-OA11WG07.jpg
Figure 2.34: 散点图
从图中我们可以推断,尽管看不出受伤人数与地震震级之间有趋势,但随着震级增加,受伤人数较多的地震数量有所增加。然而,对于大多数地震,似乎没有明显的关系。
注意
要访问此特定部分的源代码,请参阅 https://packt.live/314eupR。
您也可以在 https://packt.live/2YWtbsm 上在线运行此示例。为了获得期望的结果,您必须执行整个笔记本。
练习 2.13: 绘制相关性热图
我们将使用 seaborn 的 sns.heatmap() 函数在数据集中的特征间相关性值上绘制一个相关性热图。这是练习 2.12 绘制散点图的延续。
传递给 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/spr-lrn-ws/img/image-KJ0HRKHR.jpg
Figure 2.35: 相关性热图
我们可以从图右侧的色条中看到,最小值约为 -0.2,是最浅的色调,这是对相关性值的误表示,实际上相关性值的范围是从 -1 到 1。
在更定制的热图中绘制特征子集。我们将使用 vmin 和 vmax 参数指定上下限,并在特定特征子集上重新绘制带有注释的热图,指定成对相关性值。我们还将更改颜色方案,以便更好地解释—中性的白色表示无相关性,越来越深的蓝色和红色表示更高的正相关和负相关值:
特征子集 = [‘震源深度’, ‘主要地震震级’, ‘震级 (MW)’, \
‘震级 (MS)’, ‘震级 (MB)’, ‘强度’, \
‘纬度’, ‘经度’, ‘受伤人数’, \
‘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/spr-lrn-ws/img/image-8A67TY8S.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-Y1Y2KFEB.jpg)
图 2.36:自定义相关性热图
注意
要访问此特定部分的源代码,请参考 https://packt.live/2Z1lPUB。
你也可以在 https://packt.live/2YntBc8 上在线运行这个示例。你必须执行整个笔记本才能得到预期的结果。
现在,虽然我们可以计算相关性值,但这仅仅给出了线性关系的一个指示。为了更好地判断是否存在可能的依赖关系,我们可以绘制特征对之间的散点图,这在变量之间的关系未知时尤其有用,且可视化数据点如何散布或分布可以让我们初步判断这两个变量是否可能有关联(以及如何关联)。
使用成对图
成对图对于一次性可视化多个特征对之间的关系非常有用,可以使用 Seaborn 的 .pairplot() 函数绘制。在接下来的练习中,我们将创建一个成对图,并可视化数据集中特征之间的关系。
练习 2.14:实现一个成对图
在本练习中,我们将查看数据集中具有最高成对相关性的特征之间的成对图。此练习是练习 2.13《绘制相关性热图》的延续:
定义一个列表,其中包含要创建成对图的特征子集:
feature_subset = [‘focal_depth’, ‘eq_primary’, ‘eq_mag_mw’, \
‘eq_mag_ms’, ‘eq_mag_mb’, ‘intensity’,]
使用 seaborn 创建成对图。传递给绘图函数的参数是 kind=‘scatter’,这表示我们希望网格中每一对变量之间的单独图像显示为散点图,diag_kind=‘kde’,这表示我们希望对角线(即特征对相同的地方)上的图形是一个核密度估计。
还需要注意的是,这里对角线对称的图将本质上是相同的,只是坐标轴被反转了:
sns.pairplot(data[feature_subset].dropna(), kind =‘scatter’, \
diag_kind=‘kde’)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-Y1Y2KFEB.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-8A67TY8S.jpg)
图 2.37:具有最高成对相关性的特征之间的成对图
我们已经成功地通过成对图可视化了数据集中具有高相关性的特征之间的关系。
注意
要访问此特定部分的源代码,请参考 https://packt.live/2Ni11T0。
你也可以在 https://packt.live/3eol7aj 上在线运行这个示例。你必须执行整个笔记本才能得到预期的结果。
连续变量与分类变量之间的关系
查看一个分类变量和一个连续变量之间关系的常见方法是使用条形图或箱线图:
条形图有助于比较离散参数集的变量值,是最常见的图表类型之一。每个条形代表一个分类值,条形的高度通常代表该类别下连续变量的聚合值(例如平均值、总和或该类别下连续变量值的计数)。
箱线图是用来表示每个分类变量的离散值对应的连续变量分布的矩形。它不仅能有效地可视化离群值,还能帮助我们比较分类变量不同类别之间的连续变量分布。矩形的上下边缘分别代表第一四分位数和第三四分位数,中间的线代表中位数,矩形上下方的点(或异常值)表示离群值。
练习 2.15:绘制条形图
让我们使用条形图可视化由不同强度级别的地震产生的海啸总数。这个练习是练习 2.14 的延续,内容为:实现一个配对图(Pairplot):
对 flag_tsunami 变量进行预处理。在使用 flag_tsunami 变量之前,我们需要对其进行预处理,将 No 值转换为零,Tsu 值转换为一。这样就可以得到二进制目标变量。为此,我们使用 .loc 操作符设置列中的值,: 表示对所有行设置值,第二个参数指定要设置值的列名:
data.loc[:,‘flag_tsunami’] = data.flag_tsunami\
.apply(lambda t: int(str(t) == ‘Tsu’))
从我们要绘制的数据中删除所有具有空强度值的行:
subset = data[~pd.isnull(data.intensity)][[‘intensity’,\
‘flag_tsunami’]]
查找每个强度级别的海啸总数并显示 DataFrame。为了以条形图可视化这些数据,我们需要按强度级别对行进行分组,然后对 flag_tsunami 值求和,以得到每个强度级别的海啸总数:
data_to_plot = subset.groupby(‘intensity’).sum()
data_to_plot
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-TZAVF3XT.jpg
图 2.38:每个强度级别的海啸总数
使用 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(‘地震强度’)
plt.ylabel(‘海啸数量’)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-R4RR092D.jpg
图 2.39: 条形图
从这个图中,我们可以看到,随着地震强度的增加,海啸数量也随之增加,但当强度超过 9 时,海啸数量似乎突然下降。
思考一下为什么会发生这种情况。也许只是因为强度这么高的地震较少,因此海啸也就更少。或者,这可能是一个完全独立的因素;也许高强度地震历史上发生在陆地上,无法触发海啸。请探索数据来找出原因。
注意
要访问此特定部分的源代码,请访问 https://packt.live/3enFjsZ。
你也可以在线运行此示例,网址:https://packt.live/2V5apxV。你必须执行整个笔记本才能得到预期的结果。
练习 2.16: 可视化箱型图
在本练习中,我们将绘制一个箱型图,表示发生至少 100 次地震的国家中 eq_primary 的变化。此练习是练习 2.15“绘制条形图”的延续:
查找发生超过 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/spr-lrn-ws/img/image-85T9MVBS.jpg
图 2.40: 超过 100 次地震的国家
子集筛选数据框,只保留那些国家属于前述集合的行。要筛选行,我们使用 .isin() 方法在 pandas 系列上选择那些包含传递为参数的类数组对象中的值的行:
subset = data[data.country.isin(top_countries.index)]
创建并显示箱型图。绘制数据的主要命令是 sns.boxplot(x=…, y=…, data=…, order=)。x 和 y 参数是数据框中要绘制在每个轴上的列名——前者被认为是分类变量,后者是连续变量。data 参数接受数据框,并且 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/spr-lrn-ws/img/image-Y7EBC9P1.jpg
图 2.41: 箱型图
注意
要访问此特定部分的源代码,请访问 https://packt.live/2zQHPZw。
你也可以在线运行此示例,网址:https://packt.live/3hPAzhN。你必须执行整个笔记本才能得到预期的结果。
两个分类变量之间的关系
当我们仅查看一对分类变量以寻找它们之间的关系时,最直观的方法是基于第一个类别划分数据,然后根据第二个分类变量进一步细分数据,查看结果计数以了解数据点的分布。虽然这可能看起来有些困惑,但一种常见的可视化方法是使用堆叠条形图。与常规条形图一样,每个条形图表示一个分类值。但每个条形图会再次被细分为颜色编码的类别,这些类别表示主类别中有多少数据点属于每个子类别(即第二个类别)。通常,类别数量较多的变量被认为是主类别。
练习 2.17:绘制堆叠条形图
在这个练习中,我们将绘制一个堆叠条形图,表示每个强度级别发生的海啸数量。这个练习是练习 2.16(可视化箱形图)的延续:
查找每个分组值的强度和 flag_tsunami 的数据点数量:
grouped_data = data.groupby([‘intensity’, \
‘flag_tsunami’]).size()
grouped_data
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-G6BXFRIT.jpg
图 2.42:每个分组值的强度和 flag_tsunami 的数据点
使用.unstack()
方法在结果 DataFrame 上获取 level-1 索引(flag_tsunami)作为列:
data_to_plot = grouped_data.unstack()
data_to_plot
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-43W4BPAB.jpg
图 2.43:level-1 索引
创建堆叠条形图。我们首先使用 sns.set()函数来指示我们想使用 seaborn 作为可视化库。然后,我们可以轻松地使用 pandas 中的.native .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/spr-lrn-ws/img/image-JSR92G61.jpg
图 2.44:堆叠条形图
注意
要访问此特定部分的源代码,请参阅 https://packt.live/37SnqA8。
你也可以在线运行此示例,网址为 https://packt.live/3dllvVx。你必须执行整个 Notebook 才能获得所需的结果。
该图现在让我们能够可视化和解释每个强度级别导致海啸的地震所占的比例。在练习 2.15:绘制条形图中,我们看到对于强度大于 9 的地震,海啸的数量减少了。从这张图中,我们现在可以确认,这主要是因为地震本身的数量在超过 10 级之后减少了;甚至在 11 级时,海啸的比例有所增加。
活动 2.03:数据中的关系
在这个活动中,我们将复习上一节关于数据关系的知识。我们将使用在活动 2.01: 总结统计和缺失值中使用的相同数据集,即《房价:高级回归技术》。我们将使用不同的图表来突出数据集中的变量关系。这个活动是活动 2.01: 总结统计和缺失值的延续:
执行的步骤如下:
绘制数据集的相关性热图。
输出应类似于以下内容:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-RV8XI78X.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-KZM5Q1AT.jpg)
图 2.45:房屋数据集的相关性热图
使用以下特征子集绘制一个更紧凑的热图,并附加相关值的注释:
feature_subset = [‘GarageArea’,‘GarageCars’,‘GarageCond’, \
‘GarageFinish’,‘GarageQual’,‘GarageType’, \
‘GarageYrBlt’,‘GrLivArea’,‘LotArea’, \
‘MasVnrArea’,‘SalePrice’]
输出应类似于以下内容:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-SYRP2X30.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-R3PJ6CKC.jpg)
图 2.46:房屋数据集选择变量的相关性热图
显示相同特征子集的配对图,直方图在对角线上,散点图在其他位置。
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-R3PJ6CKC.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-SYRP2X30.jpg)
图 2.47:相同特征子集的配对图
创建一个箱型图,以显示不同 GarageCars 类别中 SalePrice 的变化:
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-KZM5Q1AT.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-RV8XI78X.jpg)
图 2.48:展示不同 GarageCars 类别中 SalePrice 变化的箱型图
使用 seaborn 绘制折线图,展示较旧和较新建的房屋的 SalePrice 变化:
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-0972CWFE.jpg
针对较旧和较新建的房屋
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-0972CWFE.jpg)
图 2.49:展示较旧和较新建的房屋 SalePrice 变化的折线图
注意
该活动的解决方案可以通过此链接找到。
你已经学会了如何使用 seaborn 包中的更高级方法来可视化大量变量,使用相关性热图、配对图和箱型图等图表。通过箱型图,你学会了如何可视化一个变量在另一个分类变量中的范围。箱型图还直接可视化了四分位数和异常值,使其成为你 EDA 工具包中的强大工具。你还创建了一些初步的折线图和散点图,这对于可视化随时间或其他变量变化的连续数据非常有帮助。
总结
在本章中,我们首先讨论了为什么数据探索是建模过程中的重要部分,以及它如何帮助我们不仅为建模过程进行数据预处理,还能帮助我们工程化有意义的特征并提高模型的准确性。本章不仅侧重于对数据集及其特征的基本概述,还通过创建结合多个特征的可视化图表来获得深入见解。我们介绍了如何使用 pandas 的核心功能找到数据集的摘要统计信息。我们讨论了如何查找缺失值,并讲解了它们的重要性,同时学习了如何使用 Missingno 库来分析缺失值,以及如何使用 pandas 和 scikit-learn 库来填补缺失值。接着,我们学习了如何研究数据集中变量的单变量分布,并使用条形图、饼图和直方图等方法将其可视化,适用于分类变量和连续变量。最后,我们学习了如何探索变量之间的关系,并了解了如何使用散点图、热力图、箱线图和堆叠条形图等方式来展示它们。
在接下来的章节中,我们将开始探索有监督的机器学习算法。现在,我们已经对如何探索现有数据集有了一定的了解,可以进入建模阶段。下一章将介绍回归,这是一类主要用于构建连续目标变量模型的算法。
第四章:3. 线性回归
概述
本章涵盖回归问题及其分析,介绍了线性回归、多个线性回归和梯度下降法。到本章结束时,你将能够区分回归问题和分类问题。你将能够在回归问题中实现梯度下降法,并将其应用于其他模型架构。你还将能够使用线性回归为 x-y 平面中的数据构建线性模型,评估线性模型的性能,并利用评估结果选择最佳模型。此外,你还将能够执行特征工程,创建虚拟变量,以构建复杂的线性模型。
介绍
在第一章《基础知识》和第二章《探索性数据分析与可视化》中,我们介绍了 Python 中有监督机器学习的概念,以及加载、清理、探索和可视化原始数据源所需的基本技术。我们讨论了在进行进一步分析之前,全面理解数据的重要性,以及初步数据准备过程有时可能占据整个项目大部分时间的问题。特别地,我们考虑了所有变量之间的相关性、寻找并处理缺失值,以及通过直方图、条形图和密度图理解数据的形态。在本章中,我们将深入探讨模型构建过程,并使用线性回归构建我们的第一个有监督机器学习解决方案。
回归与分类问题
我们在第一章《基础》中讨论了两种不同的方法:监督学习和无监督学习。监督学习问题旨在将输入信息映射到已知的输出值或标签,但还有两个子类别需要考虑。监督学习问题可以进一步分为回归问题或分类问题。本章的回归问题旨在预测或建模连续值,例如,从历史数据中预测明天的温度(以摄氏度为单位),或基于产品的销售历史预测未来的产品销售额。相对而言,分类问题则不是返回一个连续值,而是预测属于一个或多个指定类别或类的成员身份。第一章《基础》中提到的监督学习问题示例,就是想要判断发型是属于 1960 年代还是 1980 年代,这是一个良好的监督分类问题示例。在那里,我们试图预测发型是属于两个不同类别中的一个,其中类别 1 为 1960 年代,类别 2 为 1980 年代。其他分类问题包括预测泰坦尼克号乘客是否生还,或经典的 MNIST 问题(http://yann.lecun.com/exdb/mnist/)。(MNIST 是一个包含 70,000 张标注过的手写数字图像的数据库,数字从 0 到 9。MNIST 分类任务是从这 70,000 张输入图像中选取一张,预测或分类该图像中写的是哪一个数字(0 到 9)。该模型必须预测该图像属于 10 个不同类别中的哪一个。)
机器学习工作流
在我们开始讨论回归问题之前,我们首先需要了解创建任何机器学习模型(无论是监督回归还是其他类型模型)所涉及的六个主要阶段。这些阶段如下:
商业理解
数据理解
数据准备
建模
评估
部署
该工作流由一个知名的开放行业标准 CRISP-DM(跨行业数据挖掘标准流程)描述,可以如下查看:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-K3TDNA00.jpg
图 3.1:CRISP-DM 工作流
建议确保完全理解这个流程以及本节所描述的内容,因为每个阶段对于实现良好的模型性能和满足业务需求至关重要。在这里,我们回顾每个阶段的关键要素。
商业理解
任何数据分析和建模项目的第一阶段并不是直接跳入数据或建模,而是理解我们为什么要分析这些数据,以及我们的模型和结论对业务的影响是什么。作为数据处理人员,你可能没有这阶段所需的全部领域知识;解决方案是花时间与业务中的利益相关者进行互动,他们了解痛点和业务目标。不要低估这一阶段的重要性。从流程图中还可以看到,业务理解阶段和数据理解阶段之间有反馈流动,评估阶段也会反馈到业务理解阶段。换句话说,这些都是持续进行的阶段,你应当尽力不断发现更多关于你所研究问题的业务方面的信息。在这一阶段的初步工作中,你还应该制定一个初步的整体项目计划。
数据理解
在大多数实际项目中,可能会有多个潜在的数据来源,并且这些数据来源可能随着时间的推移而变化。本阶段的目的是获取数据,并对其有足够的了解,以便选择解决问题所需的数据。这可能导致确定需要更多数据。基本步骤是先确定初始可用数据,并制作数据清单。接着,审查数据,这可能包括将数据读取到 Python 中并评估数据质量;常见的问题如缺失值、异常值等可以在此阶段发现,并与业务团队讨论,确定最佳的处理方案。尽管流行文献中广泛描述了填补缺失值的方法,但你不应立即跳到使用工具“修复”数据中的问题——此阶段的目标是理解这些问题,并与业务利益相关者讨论合适的解决方案。请注意,可能比填补缺失值或进行填补过程更合适的做法是直接舍弃缺失值的数据实例。
除了数据清单外,本阶段的一个关键输出是描述数据、所发现内容和预期行动的报告。为了得到这个输出,需要进行一些 EDA(探索性数据分析),正如在第二章《探索性数据分析与可视化》中所描述的那样。
数据准备
在数据准备阶段,我们使用从前一阶段确定为合适的数据,并对其进行清洗和转换,使其能够用于建模。这是第一章《基础》中的一个重要部分,因此在本节中不会进一步分析。然而,重要的是要充分理解数据规范、收集和清理/整理过程的关键性。如果输入数据不理想,我们无法期望产生高性能的系统。关于数据质量的一个常用词句是“垃圾进,垃圾出”。如果你使用低质量的数据,就会得到低质量的结果。在我们的发型示例中,我们希望样本大小至少为数百个,理想情况下应为数千个,并且这些样本已正确标注为 1960 年代或 1980 年代的样本。我们不希望使用错误标注或甚至不属于这两个年代的样本。
请注意,在数据准备过程中,完全有可能发现数据的额外方面,并且在此过程中可能需要进行额外的可视化,以便得到用于建模的数据集。
建模
建模阶段包括两个子阶段:模型架构规范和模型训练。
模型架构规范:在更复杂的项目中,这些步骤可能是迭代相关的。在许多情况下,可能有多种模型类型(如线性回归、人工神经网络、梯度提升等)适用于当前问题。因此,有时调查不止一个模型架构是有益的,并且为了做到这一点,必须训练并比较这些模型的预测能力。
训练:建模的第二个子阶段是训练,在这个阶段,我们使用现有的数据和已知的结果,通过一个过程“学习”候选模型的参数。在这里,我们必须建立训练过程的设计和执行;这些细节将根据选择的模型架构和输入数据的规模而有所不同。例如,对于非常大的数据集,我们可能需要将数据流式传输或流动通过训练过程,因为数据太大,无法完全存入计算机内存,而对于较小的数据集,我们可以一次性使用所有数据。
评估
工作流程的下一阶段是对模型进行评估,得出最终的性能指标。这是我们判断模型是否值得发布、是否比以前的版本更好,或者是否已经在不同的编程语言或开发环境之间有效迁移的机制。我们将在第七章《模型评估》中更详细地讨论一些这些指标,因此在此阶段不会详细展开。只需要记住,无论使用何种方法,都需要能够一致地报告并独立地衡量模型相对于指标的表现,且需要使用适当的样本来自数据中进行评估。
部署
在一个完整的数据分析工作流程中,大多数模型一旦开发完成,就需要进行部署以供使用。部署在某些应用中至关重要,例如,在电子商务网站上,模型可能作为推荐系统的基础,每次更新时,模型必须重新部署到 Web 应用程序中。部署的形式多种多样,从简单地共享 Jupyter notebook,到在代码提交时自动更新网站代码,再到主仓库。尽管部署非常重要,但它超出了本书的范围,我们不会在接下来的内容中深入讨论。
在进入回归建模之前,让我们做一些最终的数据准备练习。为此,我们创建了一个合成的数据集,记录了从 1841 年到 2010 年的空气温度数据,该数据集可在本书附带的代码包中或在 GitHub 上找到,网址为 https://packt.live/2Pu850C。该数据集包含的数值旨在展示本章的主题,不能与科学研究中收集的数据混淆。
练习 3.01:使用移动平均法绘制数据
正如我们在第一章《基础知识》中讨论的,以及在前面的章节中提到的,充分理解所使用的数据集对于构建高性能的模型至关重要。所以,考虑到这一点,让我们通过这个练习加载、绘制并查询数据源:
导入 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(‘…/Datasets/synth_temp.csv’)
df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-EASY3Z4X.jpg
图 3.2:前五行
对于我们的目的,我们不希望使用所有这些数据,但让我们看一下每年有多少个数据点。创建一个打印语句,输出 1841 年、1902 年和 2010 年的点数,并制作一个简单的图表,显示每年的数据点数:
快速查看每年数据点的数量
print('There are ’ + str(len(df.loc[df[‘Year’] == 1841])) \
-
’ points in 1841\n’ + 'and ’ \
-
str(len(df.loc[df[‘Year’] == 2010])) \
-
’ 2010 年的数据点\n’ + '以及 ’ \
-
str(len(df.loc[df[‘Year’] == 1902])) \
-
’ 1902 年的数据点’]
看到每年有不同数量的数据点,做个快速图表看看
fig, ax = plt.subplots()
ax.plot(df[‘Year’].unique(), [len(df.loc[df[‘Year’] == i]) \
for i in df[‘Year’].unique()])
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-UHLA56I2.jpg
图 3.3:每年数据点数量不同
我们看到每年的数据点数量不同。还需要注意的是,我们没有关于每年各个数据点测量时间的确切信息。如果这很重要,我们可能需要询问相关业务负责人是否能获得这些信息。
让我们切片 DataFrame,去除所有 1901 年及之前的数据,因为我们可以看到这些年份的数据较少:
从 1902 年开始切片
df = df.loc[df.Year > 1901]
df.head()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-0RI5ICIW.jpg
图 3.4:1902 年及以后数据的子集
快速绘制图表以可视化数据:
快速绘制图表以了解目前为止的情况
fig, ax = plt.subplots()
ax.scatter(df.Year, df.RgnAvTemp)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-MN04Y5R9.jpg
图 3.5:过滤日期后的原始数据基本可视化
我们可以看到每年有相当大的差异。将数据按年分组,使用 DataFrame 的 agg 方法来创建年度平均值。这绕过了我们每年都有多个未知日期的数据点的问题,但仍然使用了所有数据:
按年汇总
df_group_year = (df.groupby(‘Year’).agg(‘mean’)\
.rename(columns = {‘RgnAvTemp’ : ‘AvgTemp’}))
print(df_group_year.head())
print(df_group_year.tail())
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-22CX18VC.jpg
图 3.6:每年平均数据
和以前一样,进行快速可视化,方法如下:
可视化按年份平均的结果
fig, ax = plt.subplots()
ax.scatter(df_group_year.index, df_group_year[‘AvgTemp’])
plt.show()
数据现在将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-0TF4WTD6.jpg
图 3.7:每年平均数据
由于数据仍然有噪声,移动平均滤波器可以提供一个有用的整体趋势指示器。移动平均滤波器简单地计算最近 N 个值的平均值,并将此平均值分配给第 N 个样本。使用 10 年的窗口计算温度测量的移动平均值:
window = 10
smoothed_df = \
pd.DataFrame(df_group_year.AvgTemp.rolling(window).mean())
smoothed_df.colums = ‘AvgTemp’
print(smoothed_df.head(14))
print(smoothed_df.tail())
我们将得到以下输出:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-VGGD9MD5.jpg
图 3.8:10 年移动平均温度
请注意,前 9 个样本为 NaN,这是因为移动平均滤波器窗口的大小。窗口大小是 10,因此需要 9 个样本(10-1)来生成第一个平均值,因此前 9 个样本为 NaN。rolling() 方法有额外的选项,可以将值延伸到左侧或右侧,或允许基于更少的点计算早期值。在这种情况下,我们将只过滤掉它们:
过滤掉 NaN 值
smoothed_df = smoothed_df[smoothed_df[‘AvgTemp’].notnull()]
快速绘制图表以了解目前的进展
fig, ax = plt.subplots()
ax.scatter(smoothed_df.index, smoothed_df[‘AvgTemp’])
plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-Q3RS2M7V.jpg
图 3.9:预处理温度数据的可视化
最后,按年份绘制测量数据以及移动平均信号:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
raw_plot_data = df[df.Year > 1901]
ax.scatter(raw_plot_data.Year, \
raw_plot_data.RgnAvTemp, \
label = ‘原始数据’, c = ‘blue’, s = 1.5)
年度平均
annual_plot_data = df_group_year\
.filter(items = smoothed_df.index, axis = 0)
ax.scatter(annual_plot_data.index, \
annual_plot_data.AvgTemp, \
label = ‘年度平均’, c = ‘k’)
移动平均
ax.plot(smoothed_df.index, smoothed_df.AvgTemp, \
c = ‘r’, linestyle = ‘–’, \
label = f’{window} 年移动平均’)
ax.set_title(‘平均空气温度测量’, fontsize = 16)
使刻度包括第一个和最后一个年份
tick_years = [1902] + list(range(1910, 2011, 10))
ax.set_xlabel(‘年份’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.set_xticks(tick_years)
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-7Y65GJJM.jpg
图 3.10:年度平均温度与 10 年移动平均叠加图
我们可以通过调整 y 轴的尺度,专注于我们最感兴趣的部分——年度平均值,从而改进图表。这是大多数可视化中的一个重要方面,即尺度应优化以向读者传递最有用的信息:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
raw_plot_data = df[df.Year > 1901]
ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \
label = ‘原始数据’, c = ‘blue’, s = 1.5)
年度平均
annual_plot_data = df_group_year\
.filter(items = smoothed_df.index, axis = 0)
ax.scatter(annual_plot_data.index, annual_plot_data.AvgTemp, \
label = ‘年度平均’, c = ‘k’)
移动平均
ax.plot(smoothed_df.index, smoothed_df.AvgTemp, c = ‘r’, \
linestyle = ‘–’, \
label = f’{window} 年移动平均’)
ax.set_title(‘平均空气温度测量’, fontsize = 16)
使刻度包括第一个和最后一个年份
tick_years = [1902] + list(range(1910, 2011, 10))
ax.set_xlabel(‘年份’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.set_ylim(17, 20)
ax.set_xticks(tick_years)
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
最终图表应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-7Q9XR9QF.jpg
图 3.11:原始数据、年均值和平滑数据的最终图表
看图 3.11,我们可以立即得出几个有趣的观察结果。首先,温度从 1902 年到大约 1950 年保持相对一致,之后温度趋势上升直到数据结束。其次,测量值中即使在每年平均后仍然存在波动或噪声。第三,1960 年左右似乎有一个变化,这可能代表测量方法的变化或其他因素;我们可能需要与业务团队进一步沟通以更全面地了解这一点。
最后,请注意,在存在趋势的时期,移动平均值通常位于原始数据的右侧。这是滚动()方法中默认参数的直接结果;每个移动平均值是当前点和其左侧 9 个点的平均值。
注意
要访问本节的源代码,请参阅 https://packt.live/316S0o6。
你也可以在 https://packt.live/2CmpJPZ 在线运行这个示例。你必须执行整个 Notebook 才能得到期望的结果。
活动 3.01:使用移动平均绘制数据
在本次活动中,我们获取了来自德克萨斯州奥斯丁的天气信息数据集(austin_weather.csv),该数据集可以在随附的源代码中找到,我们将查看日均温度的变化情况。我们将为此数据集绘制一个移动平均滤波器。
注意
原始数据集可以在这里找到:https://www.kaggle.com/grubenm/austin-weather
需要执行的步骤如下:
导入 pandas 和 matplotlib.pyplot。
将数据集从 CSV 文件加载到 pandas DataFrame 中。
我们只需要日期和 TempAvgF 列;请从数据集中删除所有其他列。
最初,我们只关注第一年的数据,因此需要提取该信息。
在 DataFrame 中创建一个列来存储年份值,并从 Date 列中的字符串中提取年份值,将这些值分配到 Year 列中。
重复此过程以提取月份值,并将这些值作为整数存储在 Month 列中。
再次重复此过程,将日值存储为 Day 列中的整数。
将第一年的数据复制到 DataFrame 中。
计算 20 天的移动平均滤波器。
绘制原始数据和移动平均信号,x 轴表示年份中的天数。
输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-EZE2LD9E.jpg
图 3.12:温度数据叠加在 20 天移动平均线上
注意
本活动的解答可以通过这个链接找到。
你已经学习了如何从 CSV 文件加载数据,如何删除不需要的列,如何从包含日期的文本字段中提取信息,如何使用移动平均法平滑数据,以及如何可视化结果。
线性回归
我们将通过选择线性模型开始对回归模型的研究。线性模型是一个非常好的初步选择,因为它们具有直观的性质,而且在预测能力上也非常强大,前提是数据集包含一定程度的线性或多项式关系,输入特征和输出值之间有某种联系。线性模型的直观性质通常源于能够将数据绘制在图表上,并观察数据中呈现出的趋势模式,例如,输出(数据的 y 轴值)与输入(x 轴值)呈正相关或负相关。线性回归模型的基本组件通常也在高中数学课程中学习过。你可能还记得,直线的方程定义如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-4J4XXS00.jpg
图 3.13:直线方程
在这里,x 是输入值,y 是相应的输出或预测值。模型的参数包括直线的斜率(y 值的变化除以 x 的变化,也叫梯度),在方程中用β1 表示,以及 y 截距值β1,表示直线与 y 轴交点的位置。通过这样的模型,我们可以提供β1 和β0 参数的值来构建一个线性模型。
例如,y = 1 + 2 * x 的斜率为 2,这意味着 y 值的变化速度是 x 值的两倍;该直线在 y 轴的截距为 1,以下图所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-MGXYYY7W.jpg
图 3.14:直线参数和线性模型
所以,我们了解了定义直线所需的参数,但这并没有做什么特别有趣的事情。我们只是规定了模型的参数来构建一条直线。我们想要做的是,使用一个数据集来构建一个最能描述该数据集的模型。根据上一节的内容,我们希望选择线性模型作为模型架构,然后训练模型来找到最佳的β0 和β1 值。如前所述,这个数据集需要具有某种程度的线性关系,才能使线性模型成为一个好的选择。
最小二乘法
机器学习中使用的许多技术其实早在机器学习作为描述出现之前就已经存在。有些技术体现了统计学的元素,而其他技术则已经在科学中被用来“拟合”数据很长时间。最小二乘法用于找出最能代表一组数据的直线方程,这就是其中之一,最早创建于 19 世纪初。这种方法可以用来说明监督学习回归模型的许多关键概念,因此我们将在这里从它开始。
最小二乘法侧重于最小化预测的 y 值与实际 y 值之间的误差平方。最小化误差的思想是机器学习中的基本概念,也是几乎所有学习算法的基础。
尽管使用最小二乘法的简单线性回归可以写成简单的代数表达式,但大多数包(如 scikit-learn)在“幕后”会有更通用的优化方法。
Scikit-Learn 模型 API
Scikit-learn API 使用类似的代码模式,无论构建的是何种类型的模型。通用流程是:
导入你想要使用的模型类型的类。
在这里,我们将使用 from sklearn.linear_model import LinearRegression
。
实例化模型类的一个实例。此处将设置超参数。对于简单的线性回归,我们可以使用默认值。
使用拟合方法并应用我们想要建模的 x 和 y 数据。
检查结果,获取指标,然后进行可视化。
让我们使用这个工作流程,在下一个练习中创建一个线性回归模型。
练习 3.02:使用最小二乘法拟合线性模型
在本练习中,我们将使用最小二乘法构建第一个线性回归模型,来可视化每年时间范围内的气温,并使用评估指标评估模型的表现:
注意
我们将使用与练习 3.01 中相同的 synth_temp.csv 数据集:使用移动平均法绘制数据。
从 scikit-learn 的 linear_model
模块导入 LinearRegression
类,并导入我们需要的其他包:
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
加载数据。对于本练习,我们将使用之前使用的相同的合成温度数据:
加载数据
df = pd.read_csv(‘…/Datasets/synth_temp.csv’)
重复之前的数据预处理过程:
切片从 1902 年开始
df = df.loc[df.Year > 1901]
按年份进行汇总
df_group_year = df.groupby([‘Year’]).agg({‘RgnAvTemp’ : ‘mean’})
df_group_year.head(12)
添加年列,以便我们可以在模型中使用它。
df_group_year[‘Year’] = df_group_year.index
df_group_year = \
df_group_year.rename(columns = {‘RgnAvTemp’ : ‘AvTemp’})
df_group_year.head()
数据应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-RJBJLLG8.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-RJBJLLG8.jpg)
图 3.15:预处理后的数据
实例化 LinearRegression 类。然后,我们可以使用我们的数据来拟合模型。最初,我们只会将温度数据拟合到年份。在以下代码中,请注意该方法要求 x 数据为 2D 数组,并且我们仅传递了年份。我们还需要使用 reshape 方法,并且在 (-1, 1) 参数中,-1 表示“该值从数组的长度和剩余维度中推断出来”:
构建模型并检查结果
linear_model = LinearRegression(fit_intercept = True)
linear_model.fit(df_group_year[‘Year’].values.reshape((-1, 1)), \
df_group_year.AvTemp)
print('模型斜率 = ', linear_model.coef_[0])
print('模型截距 = ', linear_model.intercept_)
r2 = linear_model.score(df_group_year[‘Year’]\
.values.reshape((-1, 1)), \
df_group_year.AvTemp)
print('r 平方 = ', r2)
注意
有关 scikit-learn 的更多阅读,请参考以下链接: scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-CEGQBU5B.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-CEGQBU5B.jpg)
图 3.16:使用拟合方法的结果
注意使用 score 方法,这是模型对象的方法,用于获得 r2 值。这个度量叫做决定系数,是线性回归中广泛使用的度量。r2 越接近 1,说明我们的模型对数据的预测越精确。有多个公式可以用来计算 r2。这里有一个例子:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZH6EW0LQ.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZH6EW0LQ.jpg)
图 3.17:r2 计算
从图 3.17 中,你可以通过注意到分子是预测误差的总和,而分母是数据与均值的变动之和,来对 r2 有一些理解。因此,随着预测误差的减小,r2 会增大。这里需要强调的是,r2 只是“拟合优度”的一个度量——在本例中,指的是简单的直线如何拟合给定的数据。在更复杂的现实世界监督学习问题中,我们会使用更稳健的方法来优化模型并选择最佳/最终模型。特别是,一般情况下,我们会在未用于训练的数据上评估模型,因为在训练数据上评估会给出过于乐观的性能度量。第七章《模型评估》将讨论这一点。
为了可视化结果,我们需要将一些数据传递给模型的 predict 方法。一个简单的方法是直接重用我们用来拟合模型的数据:
生成可视化预测
pred_X = df_group_year.loc[:, ‘Year’]
pred_Y = linear_model.predict(df_group_year[‘Year’]\
.values.reshape((-1, 1)))
现在,我们已经拥有可视化结果所需的一切:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
raw_plot_data = df[df.Year > 1901]
ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \
label = ‘原始数据’, c = ‘red’, s = 1.5)
年度平均值
ax.scatter(df_group_year.Year, df_group_year.AvTemp, \
label = ‘年平均’, c = ‘k’, s = 10)
线性拟合
ax.plot(pred_X, pred_Y, c = “blue”, linestyle = ‘-.’, \
linewidth = 4, label = ‘线性拟合’)
ax.set_title(‘平均气温测量’, fontsize = 16)
使刻度包含第一个和最后一个年份
tick_years = [1902] + list(range(1910, 2011, 10))
ax.set_xlabel(‘年份’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.set_ylim(15, 21)
ax.set_xticks(tick_years)
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-AEK3FF8A.jpg
图 3.18:线性回归 - 第一个简单的线性模型
从图 3.18 中可以看出,直线并不是数据的一个很好的模型。我们将在一个活动之后回到这个问题。
注意
要访问此特定部分的源代码,请参阅 https://packt.live/2NwANg1。
你也可以在 https://packt.live/2Z1qQfT 上在线运行这个示例。你必须执行整个 Notebook 才能得到预期的结果。
我们已经看到了如何加载一些数据,如何从 scikit-learn 导入 LinearRegression 类,并使用 fit、score 和 predict 方法构建模型,查看性能指标,并可视化结果。在此过程中,我们介绍了最小二乘法,给出了一些数学背景,并展示了部分计算过程。
我们看到,对于我们的合成温度数据,线性模型并不完全适合这些数据。没关系。在大多数情况下,早期生成一个基准模型是一个好习惯,这个模型可以作为更复杂模型性能的比较基准。因此,我们可以将这里开发的线性模型视为一个简单的基准模型。
在继续之前,需要注意的是,当报告机器学习模型的性能时,训练模型所使用的数据不能用于评估模型性能,因为这会给出模型性能的过于乐观的视角。我们将在第七章《模型评估》中讨论验证的概念,包括评估和报告模型性能。然而,本章中我们将使用训练数据来检查模型性能;只要记住,在完成第七章《模型评估》后,你会更清楚如何做。
活动 3.02:使用最小二乘法进行线性回归
对于这个活动,我们将使用在前一个活动中使用的德克萨斯州奥斯汀的天气数据集。我们将使用最小二乘法为该数据集绘制线性回归模型。
要执行的步骤如下:
导入必要的包、类等。如果需要,请参阅练习 3.02:使用最小二乘法拟合线性模型。
从 csv 文件加载数据(austin_weather.csv)。
检查数据(使用 head() 和 tail() 方法)。
df.head()的输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3A1PPP86.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-UPOYLJPO.jpg)
图 3.19:df.head()的输出
df.tail()的输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3A1PPP86.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-LPNVRK9C.jpg)
图 3.20:df.tail()的输出
删除除 Date 和 TempAvgF 列以外的所有列。
创建新的 Year、Month 和 Day 列,并通过解析 Date 列来填充它们。
创建一个新的列用于移动平均,并用 TempAvgF 列的 20 天移动平均值填充它。
切割出一整年的数据用于模型训练。确保该年份的数据没有因移动平均而缺失。此外,创建一个 Day_of_Year 列(应从 1 开始)。
创建一个散点图,显示原始数据(原始 TempAvgF 列),并在其上叠加 20 天移动平均线。
绘图将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3A1PPP86.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-5VDXTR6A.jpg)
图 3.21:原始数据与叠加的 20 天移动平均
使用默认参数创建线性回归模型,即计算模型的 y 截距,并且不对数据进行归一化。
现在拟合模型,其中输入数据是年份的天数(1 到 365),输出是平均温度。打印模型的参数和 r² 值。
结果应如下所示:
模型斜率: [0.04304568]
模型截距: 62.23496914044859
模型 r² 值: 0.09549593659736466
使用相同的 x 数据从模型中生成预测。
创建一个新的散点图,像之前一样,添加模型预测的叠加图层。
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-LPNVRK9C.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3A1PPP86.jpg)
图 3.22:原始数据、20 天移动平均和线性拟合
注意
该活动的解决方案可以通过此链接找到。
基于之前的练习,你已经了解了如何加载并使用 scikit-learn 中的 LinearRegression 类,以及 fit、score 和 predict 方法。不出所料,产生直线的简单线性模型并不是该数据的最佳模型。在后续练习中,我们将探讨可能的改进方法。
你已经学会了如何加载数据、将其结构化以适应 scikit-learn API,并使用 LinearRegression 类将一条简单的直线拟合到数据上。显然,这对该数据来说是一个不理想的模型,因此我们将探索改进模型的方法,从下一个话题“具有分类变量的线性回归”开始。
具有分类变量的线性回归
模型架构选择阶段有一个方面与数据准备阶段有些重叠:特征工程。广义上讲,特征工程涉及创建附加的特征(在我们这里是列),并将它们用于模型中以提高模型性能。特征可以通过转换现有特征(例如取对数或平方根)来工程化,或者以某种方式生成并添加到数据集中。举个后者的例子,我们可以从数据集中的日期信息中提取出月份、日期、星期几等。虽然像月份这样的新特征可以是一个数值,但在大多数监督学习的情况下,简单地使用这种特征的数值并不是最佳实践。一个简单的思路是:如果我们将 1 月到 12 月编码为 1 到 12,那么模型可能会给 12 月更多的权重,因为 12 月比 1 月大 12 倍。此外,当日期从 12 月切换回 1 月时,值会发生人为的阶跃变化。因此,这样的特征被认为是名义类别的。名义类别变量是具有多个可能值的特征,但这些值的顺序不包含任何信息,甚至可能会误导。还有一些类别变量确实有隐含的顺序,它们被称为有序类别变量。例如,“小”、“中”、“大”、“特大”和“巨大”等。
为了处理大多数机器学习模型中的任何类型的类别数据,我们仍然需要将其转换为数字。这种转换的通用方法叫做编码。一种非常强大但易于理解的编码方法是使用独热编码将类别特征转换为数值。
使用独热编码时,类别特征的每个可能值都会变成一列。在对应于给定值的列中,如果该数据实例在该值下具有该特征,则输入 1,否则输入 0。一个例子会让这一点更加清晰,如下图所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-QM4YDGVR.jpg
图 3.23:名义类别列的独热编码
因此,通过创建这些列并在适当的位置插入 1,我们让模型“知道”名义类别变量的存在,但不会对任何特定值赋予额外的权重。在图 3.23 中的例子中,如果我们试图模拟狗的预期寿命,在使用独热编码之前,我们只有饮食和体重作为预测因素。应用独热编码后,我们预计能够得到一个更好的模型,因为我们的直觉是:在其他因素相等的情况下,一些犬种比其他犬种活得更久。在接下来的练习中,我们将看到如何使用编码来利用线性模型的强大能力来模拟复杂的行为。
注意
还有许多其他可能的编码分类变量的方法;例如,参见《神经网络分类器的分类变量编码技术比较研究》:https://pdfs.semanticscholar.org/0c43/fb9cfea23e15166c58e24106ce3605b20229.pdf
在某些情况下,最佳方法可能取决于所使用的模型类型。例如,线性回归要求特征之间没有线性依赖(我们将在本章后面进一步讨论)。独热编码实际上会引入这个问题,因为第 n 类别实际上可以通过其他 n-1 类别来确定——直观地,在图 3.23 中,如果比格犬、拳师犬、吉娃娃、柯利犬和德国牧羊犬都是 0,那么迷你杜宾犬就是 1(假设一个实例不可能有多个有效类别)。因此,在进行线性回归时,我们使用稍有不同的编码方法,即虚拟变量。虚拟变量和独热编码的唯一区别是我们去掉了 n 个列中的一个,从而消除了依赖关系。
练习 3.03:引入虚拟变量
在本练习中,我们将向线性回归模型中引入虚拟变量,以提高其性能。
我们将使用与之前练习相同的 synth_temp 数据集:
导入所需的包和类:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
加载数据:
加载数据
df = pd.read_csv(‘…/Datasets/synth_temp.csv’)
从 1902 年起切片数据框,然后计算每年的平均值:
从 1902 年起切片
print(df.head())
df = df.loc[df.Year > 1901]
print(df.head())
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-2MOLJ3LE.jpg
图 3.24:切片 1902 后的输出
按年份汇总
df_group_year = df.groupby([‘Year’, ‘Region’])\
.agg({‘RgnAvTemp’:‘mean’})
“”"
注意,.droplevel() 方法会移除多重索引
通过 .agg() 方法添加()以简化操作
后续分析中
“”"
print(df_group_year.head(12))
print(df_group_year.tail(12))
数据应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-SO0LPKV6.jpg
图 3.25:按地区划分的年均温度
使用索引(即年份)级别 0 和地区列(即索引级别 1)来添加 Year 列和 Region 列:
添加地区列,以便我们可以用它来创建虚拟变量
df_group_year[‘Region’] = df_group_year.index.get_level_values(1)
添加 Year 列,以便我们可以在模型中使用它
df_group_year[‘Year’] = df_group_year.index.get_level_values(0)
重置长轴上的索引
df_group_year = df_group_year.droplevel(0, axis = 0)
df_group_year = df_group_year.reset_index(drop = True)
也许温度水平或变化因地区而异。让我们看一下每个地区的整体平均温度:
按地区检查数据
region_temps = df_group_year.groupby(‘Region’).agg({‘RgnAvTemp’:‘mean’})
colors = [‘red’, ‘green’, ‘blue’, ‘black’, ‘lightcoral’, \
‘palegreen’,‘skyblue’, ‘lightslategray’, ‘magenta’, \
‘chartreuse’, ‘lightblue’, ‘olive’]
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1])
ax.bar(region_temps.index, region_temps.RgnAvTemp, \
color = colors, alpha = 0.5)
ax.set_title(‘平均空气温度测量值’, fontsize = 16)
ax.set_xlabel(‘区域’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.tick_params(labelsize = 12)
plt.show()
结果应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZOU2B3CJ.jpg
图 3.26: 各区域的整体平均温度
我们看到,平均而言,区域之间的温差可达 5 度。因此,考虑区域可能对模型有益。为此,我们将从 Region 列创建虚拟变量。
Pandas 有一个名为 get_dummies() 的 DataFrame 方法,我们可以用它来满足我们的需求。首先,我们创建一个包含新列的新 DataFrame。请注意,它们已经填充了零和一。然后,我们将虚拟变量列与数据合并,并删除 Region 列,因为它现在是冗余的:
将分类变量 ‘region’ 转换为虚拟变量
dummy_cols = pd.get_dummies(df_group_year.Region, \
drop_first = True)
df_group_year = pd.concat([df_group_year, dummy_cols], axis = 1)
print(df_group_year.head())
print(df_group_year.tail())
结果应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-MJQ1MBKO.jpg
图 3.27: 添加区域的虚拟变量
请注意,在 get_dummies 方法中,我们设置了 drop_first = True 参数以删除其中一列,正如前面讨论的那样。
我们现在创建一个线性模型,和之前一样,使用 Year 列和所有虚拟列:
linear_model = LinearRegression(fit_intercept = True)
linear_model.fit(df_group_year.loc[:, ‘Year’:‘L’], \
df_group_year.RgnAvTemp)
r2 = linear_model.score(df_group_year.loc[:, ‘Year’:‘L’], \
df_group_year.RgnAvTemp)
print('r 平方 ', r2)
输出结果如下:
r 平方 0.7778768442731825
r2 值比之前高得多,看起来很有前景。从包含虚拟变量的 DataFrame 生成预测结果,然后将所有内容可视化到图表中:
构建用于预测的模型数据
pred_X = df_group_year.drop([‘RgnAvTemp’, ‘Region’], axis = 1)
pred_Y = linear_model.predict(pred_X.values)
preds = pd.concat([df_group_year.RgnAvTemp, \
df_group_year.Region, \
pred_X, pd.Series(pred_Y)], axis = 1)
preds.rename(columns = {0 : ‘pred_temp’}, inplace = True)
print(preds.head())
数据应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-FALLIFO0.jpg
图 3.28: 新模型的预测结果
为了绘图,我们通过从预测结果中抽样来减少杂乱:
定义原始数据和预测值的样本
设置随机种子,以确保结果可重复
np.random.seed(42)
plot_data = preds.sample(n = 100)
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1])
原始数据
raw_plot_data = plot_data
ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \
label = ‘Raw Data’, c = ‘red’, s = 1.5)
年度平均值
annual_plot_data = df_group_year.groupby(‘Year’).agg(‘mean’)
ax.scatter(annual_plot_data.index, annual_plot_data.RgnAvTemp, \
label = ‘Annual average’, c = ‘k’, s = 10)
让我们也可视化线性拟合结果:
fit_data = plot_data
for i in range(len(plot_data.Region.unique())):
region = plot_data.Region.unique()[i]
plot_region = fit_data.loc[fit_data.Region == region, :]
ax.scatter(plot_region.Year, plot_region.pred_temp, \
edgecolor = colors[i], facecolor = “none”, \
s = 80, label = region)
绘制连接原始数据和预测值的虚线
for i in fit_data.index:
ax.plot([fit_data.Year[i], fit_data.Year[i]], \
[fit_data.pred_temp[i], fit_data.RgnAvTemp[i]], \
‘-’, linewidth = 0.1, c = “red”)
ax.set_title(‘Mean Air Temperature Measurements’, fontsize = 16)
使刻度包括第一年和最后一年
tick_years = [1902] + list(range(1910, 2011, 10))
ax.set_xlabel(‘Year’, fontsize = 14)
ax.set_ylabel(‘Temperature ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.set_ylim(15, 21)
ax.set_xticks(tick_years)
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
注意
为确保无错误执行,您应在编写步骤 9 和 10 的代码后再运行该单元。
绘制结果应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-R8RM4U48.jpg
图 3.29:新模型的预测
我们可以看到,模型正在为不同区域预测不同的水平,尽管仍未完全跟随趋势,但相比之前,它已能解释更多的变异性。
现在让我们通过绘制一个区域来结束,以便了解模型的效果:
我们先绘制一个区域
region_B = preds.loc[preds.B == 1, :]
np.random.seed(42)
plot_data = region_B.sample(n = 50)
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1])
原始数据
ax.scatter(plot_data.Year, plot_data.RgnAvTemp, \
label = ‘Raw Data’, c = ‘red’, s = 1.5)
ax.scatter(plot_data.Year, plot_data.pred_temp, \
label = “Predictions”, facecolor = “none”, \
edgecolor = “blue”, s = 80)
绘制连接原始数据和预测值的虚线:
for i in plot_data.index:
ax.plot([plot_data.Year[i], plot_data.Year[i]], \
[plot_data.pred_temp[i], plot_data.RgnAvTemp[i]], \
‘-’, linewidth = 0.1, c = “red”)
使刻度包括第一年和最后一年
tick_years = [1902] + list(range(1910, 2011, 10))
ax.set_xlabel(‘Year’, fontsize = 14)
ax.set_ylabel(‘Temperature ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.set_ylim(16, 21)
ax.set_xticks(tick_years)
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
注意
为确保无错误执行,您应在编写步骤 11 和 12 的代码后再运行该单元。
结果应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-58ZV8PPQ.jpg
图 3.30:B 区域的预测
注意
若要访问此特定部分的源代码,请参见 https://packt.live/2YogxDt。
你也可以通过 https://packt.live/311LDCx 在线运行这个示例。你必须执行整个 Notebook 才能获得所需结果。
我们现在有了一个改进的模型,它能够跟随数据的大部分变化。然而,我们可以看到,仍然没有捕捉到大约在 1960 年左右出现的趋势变化。为了解决这个问题,我们将探索使用线性回归通过使用 x 数据的幂(即多项式模型)来拟合模型。首先,你将通过使用奥斯汀温度数据集来练习使用虚拟变量。
活动 3.03:虚拟变量
对于这项活动,我们将使用在前一项活动中使用的奥斯汀(德州)天气数据集。在本次活动中,我们将使用虚拟变量来增强该数据集的线性回归模型。
需要执行的步骤如下:
从 scikit-learn 加载 LinearRegression 类,以及 fit、score 和 predict 方法,还需要导入 pandas 和 matplotlib.pyplot。
加载 austin_weather.csv 数据集,删除除 Date 和 TempAvgF 列以外的所有列,并从数据中创建 Year、Month 和 Day 列。
创建一个 20 天的移动平均列并填充数据,然后切片出完整的第一年数据(从第 1 天到第 365 天—即 2015 年)。切片后,重置数据框的索引(使用 Pandas 核心方法 reset_index)。现在,创建一个 Day_of_Year 列并填充数据(请记住,第一天应为 1,而不是 0)。
绘制原始数据和移动平均线相对于 Day_of_Year 的图表。
图表应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-MF5YIEM2.jpg
图 3.31:奥斯汀气温和移动平均线
现在,研究将月份添加到模型中是否能够改进模型。为此,使用 pandas 的 get_dummies 方法对数据框的 Month 列创建一个 dummy_vars 数据框,并将虚拟列重命名为 Jan 至 Dec。现在,将 dummy_vars 合并到新的数据框 df_one_year 中。
显示数据框并确认虚拟列已存在。
使用最小二乘法线性回归模型,并将模型拟合到 Day_of_Year 值和虚拟变量上,以预测 TempAvgF。
获取模型参数和 r2 值。r2 值应该比前一项活动中的值大得多。
使用 Day_of_Year 值和虚拟变量,预测 df_one_year 数据中的温度。
绘制原始数据、20 天移动平均线和新的预测值。
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-E96FFRA8.jpg
图 3.32:使用月份虚拟变量的线性回归结果
注意
本次活动的解决方案可以通过此链接找到。
你已经学习了如何使用 scikit-learn API 中的 LinearRegression 类来拟合包含虚拟变量的数据。使用 get_dummies pandas 方法生成了额外的变量列来编码月份,以改进模型。新模型的一个有用特性是,它考虑了温度的季节性变化,以及任何整体趋势。然而,它是相当分段的,这可能不完全符合预测的业务需求。
到目前为止,你应该已经对基本的线性回归以及使用 scikit-learn 接口和 get_dummies pandas 方法感到熟悉。我们还介绍了一些可视化的要点,并在使用虚拟变量的背景下引入了特征工程的概念。接下来我们将介绍多项式回归,它将特征工程带入一个不同的方向,同时仍然利用线性回归的强大功能。
线性回归的多项式模型
线性回归模型并不限于直线线性模型。我们可以使用完全相同的技术拟合一些更复杂的模型。在合成的温度数据中,我们可以看到趋势的上升曲线。因此,除了任何整体(随时间变化的线性)趋势外,可能还存在与时间的正幂相关的趋势。如果我们使用自变量的整数幂来构建模型,这就叫做多项式回归。对于幂次为 2 的情况,方程式如下所示。注意,我们将多项式的阶数称为最高幂次,因此这是一个阶数为 2 的多项式:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-O76ZRUSK.jpg
图 3.33:二阶多项式的方程
添加这个平方项将趋势线从直线转变为具有曲率的线。一般来说,多项式模型在拟合给定数据时可能非常强大,但它们可能无法很好地在数据范围之外进行外推。这将是过拟合的一个例子,尤其是在多项式阶数增加时,这一点尤为明显。
因此,通常情况下,除非有明确的业务需求或已知的潜在模型表明需要采取不同方法,否则你应该有限地使用多项式回归,并保持阶数较低:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-9Y4YPB9U.jpg
图 3.34:二阶多项式的 y 与 x 的关系图
注意,这里我们正在查看一个简单的模型,其中只有一个特征变量 x。在更复杂的情况下,我们可能有多个特征。随着特征数量的增加,方程中的项数会迅速增加。为了构建多项式,像 scikit-learn 这样的回归包提供了自动生成多项式特征的方法(例如,sklearn.preprocessing.PolynomialFeatures)。在这里,我们将手动构建一个简单的多项式模型,以说明这一方法。
练习 3.04:线性回归的多项式模型
为了使用线性回归拟合多项式模型,我们需要创建提升到所需幂次的特征。回顾一下之前关于特征工程的讨论;我们将创建新的特征,即将原始自变量提升到一个幂次:
从 synth_temp.csv 数据开始,加载包和类,然后像以前一样预处理数据:
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
加载数据
df = pd.read_csv(‘…/Datasets/synth_temp.csv’)
从 1902 年及以后开始切片
df = df.loc[df.Year > 1901]
按年汇总
df_group_year = df.groupby([‘Year’]).agg({‘RgnAvTemp’ : ‘mean’})
现在,我们使用索引添加 Year 列,然后通过将 Year 列的值平方来计算 Year2 列:
添加 Year 列,以便我们可以在模型中使用它
df_group_year[‘Year’] = df_group_year.index
df_group_year = df_group_year\
.rename(columns = {‘RgnAvTemp’ : ‘AvTemp’})
添加一个 Year**2 列,构建二次多项式模型
df_group_year[‘Year2’] = df_group_year[‘Year’]**2
print(df_group_year.head())
print(df_group_year.tail())
结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-GOFHTDW8.jpg
图 3.35:带有年份和年份平方的年度温度数据
将数据拟合到模型中。这次,我们需要提供两组值作为模型的输入,Year 和 Year2,相当于将 x 和 x2 传递给多项式方程。由于我们提供了两列数据,因此不需要重塑输入数据,它将默认作为一个 N x 2 的数组提供。目标 y 值保持不变:
构建模型并检查结果
linear_model = LinearRegression(fit_intercept = True)
linear_model.fit(df_group_year.loc[:, [‘Year’, ‘Year2’]], \
df_group_year.AvTemp)
print('模型系数 = ', linear_model.coef_)
print('模型截距 = ', linear_model.intercept_)
r2 = linear_model.score(df_group_year.loc[:, [‘Year’, ‘Year2’]], \
df_group_year.AvTemp)
print('r 平方 = ', r2)
输出结果如下:
模型系数 = [-1.02981369e+00 2.69257683e-04]
模型截距 = 1002.0087338444181
r 平方 = 0.9313996496373635
该模型在虚拟变量方法上有所改进,但让我们通过可视化结果来看看它是否更合理。首先,生成预测结果。这里,我们额外采取一步,将预测延伸到未来 10 年,看看这些预测是否合理。在大多数监督学习问题中,最终目标是预测以前未知的数据值。由于我们的模型仅使用 Year 和 Year2 变量,我们可以生成一个年份值的列表,然后像之前一样平方它们,预测未来 10 年的温度:
生成可视化的预测
pred_X = df_group_year.loc[:, [‘Year’, ‘Year2’]]
pred_Y = linear_model.predict(pred_X)
生成未来 10 年的预测
pred_X_future = pd.DataFrame(list(range(2011, 2021)))\
.rename(columns = {0 : ‘Year’})
pred_X_future[‘Year2’] = pred_X_future[‘Year’]**2
pred_Y_future = linear_model.predict(pred_X_future)
现在,创建一个可视化:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1]);
原始数据
raw_plot_data = df
ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \
label = ‘原始数据’, c = ‘red’, s = 1.5)
年度平均
ax.scatter(df_group_year.Year, df_group_year.AvTemp, \
label = ‘年度平均’, c = ‘k’, s = 10)
线性拟合
ax.plot(pred_X.Year, pred_Y, c = “blue”, linestyle = ‘-.’, \
linewidth = 4, label = ‘线性拟合’)
可视化未来预测:
ax.plot(pred_X_future.Year, pred_Y_future, c = “purple”, \
linestyle = ‘–’, linewidth = 4, \
label = ‘未来预测’)
ax.set_title(‘平均气温测量’, fontsize = 16)
使得刻度包含首尾年份
tick_years = [1902] + list(range(1910, 2021, 10))
ax.set_xlabel(‘年份’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.set_ylim(15, 21)
ax.set_xticks(tick_years)
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-627EP11X.jpg
图 3.36:使用二次多项式模型的线性回归
注意
要访问此特定部分的源代码,请参考 https://packt.live/3fSusaR.
你也可以在线运行这个例子,访问 https://packt.live/2BulmCd。你必须执行整个笔记本才能获得期望的结果。
参考图 3.36,我们可以看到使用多项式模型的性能优势,趋势线几乎跟随 10 年移动平均。这是一个相对较好的拟合,因为每年平均的原始数据有一定噪声。在这种情况下,不应期望模型能够完美地拟合数据。如果我们的模型能够完美地拟合观察到的样本,那么就有很强的过拟合数据的风险,导致对未知样本的预测能力差。例如,假设我们对这些数据进行了 10 阶多项式拟合。结果模型会在数据点上上下波动,最后一个数据点可能会非常陡峭地上升或下降,这将导致糟糕的预测。在这种情况下,使用二阶多项式拟合,我们可以看到未来的趋势似乎是合理的,尽管这仍然需要验证。
活动 3.04:使用线性回归进行特征工程
我们尝试了标准线性模型以及包含虚拟变量的模型。在这项工作中,我们将创建一些周期特征,试图更好地拟合数据。周期特征源自在独立变量某个范围内重复的函数。在图 3.37 中,我们可以看到,年初的数据接近相同的值,而在中间,温度先升高然后降低。这在直觉上是合理的,因为我们知道在温带气候中,存在一年一度的温度周期。因此,如果我们在 1 年的时间尺度上包含周期性特征,可能会改进模型。我们可以构造正弦和余弦函数,其行为符合所需。
当拟合一个含有工程周期特征的模型时,我们面临一个额外的挑战,即确定如何将特征的周期循环与实际数据对齐。在这种情况下,您可以将其视为时间偏移量,这是我们事先不知道的。您也可以将偏移量视为超参数——拟合模型并不会给出这个值,因此我们必须以其他方式找到最佳值。在下图中,所需的偏移量为Δt:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ARY05UEQ.jpg
图 3.37:展示随时间周期性行为的一些数据及适合数据的候选函数
幸运的是,如果我们使用正弦和余弦函数作为我们的特征,有一种方法可以解决这个问题。这是一个数学事实,即任何给定的正弦函数在单一周期内(如图 3.37 中的原始数据)可以表示为相同周期的正弦和余弦函数的线性组合。在下一张图中,我们展示了一个周期为 365 天的正弦和余弦函数以及图 3.37 中的原始数据。我们还展示了与原始数据非常匹配的正弦和余弦函数的线性组合。因此,要将正弦(或余弦)函数拟合到我们的数据中,我们只需设计两个特征,一个是时间的正弦值,另一个是时间的余弦值。线性回归然后将找到最佳系数,就像处理任何其他特征一样。请注意,这意味着我们知道周期是多少:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZZ6QZXDQ.jpg
图 3.38:正弦和余弦函数的线性组合将匹配一个经未知时间偏移量移动的正弦函数
我们最后需要知道的是如何构建正弦和余弦函数。为此,我们可以使用 NumPy 方法 sin 和 cos。我们知道我们需要 1 年的周期,而我们的数据是按天计算的。编写一个具有 365 天周期的正弦函数的正确方法如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3RARL1CB.jpg
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-4MALZ9L4.jpg
图 3.40:一个周期为 365 天的 Python 系列
现在,让我们继续进行使用周期函数来拟合奥斯汀温度数据的活动。要执行的步骤如下:
加载软件包和类(numpy、pandas、LinearRegression 和 matplotlib)。
执行与之前相同的预处理,直到创建 Day_of_Year 列的步骤。
添加一个 Day_of_Year 的正弦列和一个余弦列。
对平均温度与 Day_of_Year 以及正弦和余弦特征进行线性回归。
打印模型的参数和 r2 分数。
使用新特征生成预测。
可视化原始数据和新模型。
输出如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-4MALZ9L4.jpg
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-3RARL1CB.jpg
图 3.41:在线性回归模型中使用正弦和余弦特征的预期结果
到目前为止,您已经了解到我们可以通过将函数应用于现有特征来工程化特征,例如多项式函数或周期函数。您已经看到如何使用正弦和余弦函数构建周期函数以适应任意正弦或余弦函数。在这种情况下,我们假设了 365 天的周期,这是基于地球温带地区的年度气候周期而合理的。在实际业务案例中,我们可能不确定周期,或者可能同时存在多个周期(例如销售中的每周、每月和每季度周期)。此外,使用多项式和正弦/余弦等函数很容易导致过拟合,从而导致非常差的外推。
注意
您可以通过此链接找到此活动的解决方案。
在图 3.41 中,我们看到新模型在一年内平稳变化,并在年末返回接近年初的值。由于我们已经知道一年中存在总体趋势,这个模型因包含 Day_of_Year 特征,显示出年末的温度比年初略高。在特征工程方面,我们可以考虑使用日期(将其转换为整数)并在超过 1 年的数据上拟合模型,以捕捉长期趋势。
通用模型训练
构建线性回归模型的最小二乘法是一种有用且准确的训练方法,假设数据集的维度较低,并且系统内存足够大以管理数据集。
近年来,大型数据集变得更加容易获取,许多大学、政府,甚至一些公司都将大型数据集免费发布到网上;因此,在使用最小二乘法回归建模时,可能会相对容易超过系统内存。在这种情况下,我们需要采用不同的训练方法,比如梯度下降,这种方法不容易受到高维度的影响,可以处理大规模数据集,并且避免使用内存密集型的矩阵运算。
在我们更详细地探讨梯度下降之前,我们将以更一般的形式回顾一下模型训练的过程,因为大多数训练方法,包括梯度下降,都是遵循这个通用过程的。以下是模型训练过程中的参数更新循环概述:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-64VSZ15N.jpg
图 3.42:通用模型参数更新循环
训练过程包括将模型及其参数(包括超参数)反复暴露于一组样本训练数据,并将模型预测出的值传递给指定的代价或误差函数。代价函数与某些超参数一起,决定了如何计算“更新参数”模块中的更新,如图 3.42 所示。
代价函数用于确定模型与目标值之间的接近程度,并作为训练过程中进展的衡量标准。然而,代价函数也与一些超参数一起用于确定参数更新。例如,在我们的线性回归案例中,代价函数是均方误差。
最小二乘法,我们展示为构建线性回归模型的一种方法,最小化均方误差(MSE),因此称为最小二乘法。因此,我们可以将训练过程的图示更新为以下内容:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-8U511IE7.jpg
图 3.43:通用训练过程
梯度下降
梯度下降的过程可以总结为一种根据系统中的误差,通过代价函数更新模型参数的方式。可以选择多种代价函数,具体取决于拟合的模型类型或解决的问题。我们将选择简单但有效的均方误差代价函数。
回想一下,直线方程可以写作如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-X27Z7BC9.jpg
图 3.44:直线方程
下图展示了代价函数 J 在 β0 和 β1 范围值下的图像。最优的参数集是代价函数最小的参数,这个点被称为代价函数的全局最小值。我们可以将其类比为在徒步旅行中寻找山谷中最低点的过程。直观地说,不管我们站在何处,如果不在底部,那么我们正站在斜坡上,要到达底部,就需要朝下坡走。因此,斜坡就是梯度,而找到最小值的过程就是梯度下降:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-SO29HL5F.jpg
图 3.45:简单的两参数线性回归中梯度下降的可视化表现
如前图所示,在每个训练周期中,参数 β0 和 β1 会更新,以朝着最陡峭的斜坡(梯度)方向移动,最终找到 J(β) 的最小值。
让我们更详细地了解梯度下降算法:
梯度下降从对所有 β 值进行初始随机猜测开始。注意,在某些模型中,这一步骤,称为初始化,可能会通过选择某个特定的分布来约束初始值的采样。初始化的选择可以被视为一个超参数,并且可能会影响最终结果,尤其是在像人工神经网络这样的复杂模型中。大多数 Python 实现的方法都有很好的默认值,使用默认值是很常见的做法。
使用 β 的随机值为训练集中的每个样本做出预测,然后计算代价函数 J(β)。
然后,β 值会被更新,进行一个与误差成比例的小调整,以尽量减少误差。通常,尝试直接从当前 β 值跳到能够最小化 J(β) 的值并不是最好的方法,因为如图 3.45 所示,损失面可能并不平滑。大多数实际的损失面,即使在三维空间中,也有多个峰值和谷值。对于更复杂的代价函数面,存在多个最小值,称为局部最小值,所有最小值中的最低点就是全局最小值。非凸代价函数可能会在找到全局最小值时带来挑战,研究界在高效寻找非凸表面全局最小值方面投入了大量努力。超参数学习率,用 γ 表示,用于在每次训练时调整步长。
该过程在以下图表中进行了可视化,简化为二维:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-LQZKETFG.jpg
图 3.46:梯度下降过程
在这个简化的情况下,使用较低的学习率导致路径 A,并且我们陷入局部最小值。路径 C 是由于非常高的学习率而不收敛。路径 B 使用中间的学习率值,并收敛到全局最小值。
作为设置学习率的一般提示,开始较大,例如约为 0.1,如果找不到解决方案,即错误为 NaN 或波动剧烈,则将学习率降低为 10 的因子。一旦找到允许误差随着 epoch 相对平稳下降的学习率,可以测试其他超参数,包括学习率,以达到最佳结果。
尽管这个过程听起来复杂,但实际上并不像看起来那么可怕。梯度下降可以通过一次性猜测参数值、计算猜测误差、对参数进行微小调整并持续重复此过程直到误差在最小值处收敛来概括。为了加强我们的理解,让我们看一个更具体的例子。我们将使用梯度下降来训练我们在练习 3.02 中构建的原始线性回归模型:使用最小二乘法拟合线性模型,将最小二乘法替换为梯度下降。
练习 3.05:使用梯度下降进行线性回归
在这个练习中,我们将手动实现梯度下降算法。我们将使用如前所述的 synth_temp.csv 数据:
与之前一样导入包和类:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
在我们开始梯度下降过程之前,我们需要实现一些关键函数。
编写一个函数来定义我们的线性模型。这里使用线性代数乘法将参数(β)和输入值 x 之间的乘积形式,这是使用简化形式的线性模型的优势所在。
模型函数
def h_x(Beta, X):
计算 X 和 Betas 的矩阵点积
返回 np.dot(Beta, X).flatten()
我们还需要编写一个评估成本函数 J(β) 的函数:
成本函数
def J_beta(pred, true):
均方误差
return np.mean((pred - true) ** 2)
最后,我们需要实现更新参数的函数:
更新函数
def update(pred, true, X, gamma):
返回 gamma * np.sum((true - pred) * X, axis = 1)
接下来,加载数据,从 1902 年起切片,计算年均值,并添加年份列:
载入数据
df = pd.read_csv(‘…/Datasets/synth_temp.csv’)
切片 1902 年及以后
df = df.loc[df.Year > 1901]
按年份卷起
df_group_year = df.groupby([‘Year’]).agg({‘RgnAvTemp’ : ‘mean’})
添加年份列以便在模型中使用
df_group_year[‘Year’] = df_group_year.index
df_group_year = \
df_group_year.rename(columns = {‘RgnAvTemp’ : ‘AvTemp’})
现在,我们将构建训练数据。首先,我们需要在使用梯度下降之前,将数据缩放到 0 到 1 之间。某些机器学习算法可以处理原始数据(如常规线性回归),但在使用梯度下降时,如果变量的尺度差异很大,那么某些参数的梯度值将远大于其他参数的梯度值。如果数据没有缩放,原始数据可能会扭曲成本函数表面上的下降,从而偏移结果。直观地讲,在我们的例子中,Year 数据的量级是千,而 AvTemp 数据的量级是十。因此,Year 变量会在参数的影响力上占主导地位。
机器学习中使用了多种缩放方法。例如,将数据归一化到特定范围(如(0, 1)或(-1, 1)),以及标准化(将数据缩放到均值为 0,标准差为 1)。在这里,我们将 x 和 y 数据归一化到范围(0, 1):
对数据进行缩放并添加 X0 序列
X_min = df_group_year.Year.min()
X_range = df_group_year.Year.max() - df_group_year.Year.min()
Y_min = df_group_year.AvTemp.min()
Y_range = df_group_year.AvTemp.max() - df_group_year.AvTemp.min()
scale_X = (df_group_year.Year - X_min) / X_range
train_X = pd.DataFrame({‘X0’ : np.ones(df_group_year.shape[0]), \
‘X1’ : scale_X}).transpose()
train_Y = (df_group_year.AvTemp - Y_min) / Y_range
print(train_X.iloc[:, :5])
print(train_Y[:5])
输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-EROBZLC5.jpg
图 3.47:用于梯度下降的归一化数据
请注意,train_Y 的值是真实值,也称为真实标签(ground truth)。
如我们所学,我们需要初始化参数值。请注意,我们使用带有常数值的 NumPy random.seed()
方法。设置 random.seed
将在每次运行笔记本时重现相同的结果。在模型开发过程中以及探索超参数时,这非常有用,因为你可以看到所做更改的影响,而不是随机初始化的影响。reshape()
方法用于将数据转化为正确的矩阵形式:
初始化 Beta 和学习率 gamma
np.random.seed(42)
Beta = np.random.randn(2).reshape((1, 2)) * 0.1
print(‘初始 Beta\n’, Beta)
值应如下所示:
初始 Beta [[ 0.04967142 -0.01382643]]
我们还需要设置一些超参数,包括学习率 gamma 和训练周期的最大次数(epochs):
gamma = 0.0005
max_epochs = 100
做出初步预测并使用定义的 h_x 和 J_beta 函数计算该预测的误差或成本:
y_pred = h_x(Beta, train_X)
print('初始成本 J(Beta) = ’ + str(J_beta(y_pred, train_Y)))
输出将如下所示:
初始成本 J(Beta) = 0.18849128813354338
我们现在准备使用循环来迭代训练过程。在这里,我们存储了 epoch 和 cost 值,以便稍后进行可视化。同时,我们每 10 个 epoch 输出一次成本函数和 epoch 的值:
epochs = []
costs = []
for epoch in range(max_epochs):
Beta += update(y_pred, train_Y, train_X, gamma)
y_pred = h_x(Beta, train_X)
cost = J_beta(y_pred, train_Y)
if epoch % 10 == 0:
print('新成本 J(Beta) = ’ + str(round(cost, 3)) \
- ’ 在第 ’ + str(epoch) + ’ 轮’)
epochs.append(epoch)
costs.append(cost)
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-W414240J.jpg
图 3.48:每 10 个 epoch 的训练结果
在图 3.48 中观察到,成本函数在前 20 个周期内快速下降,然后改进速度放缓。这是梯度下降训练中的典型模式,尤其是当学习率处于合理值时。
可视化训练历史:
绘制训练历史
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1])
ax.plot(epochs, costs)
ax.tick_params(labelsize = 14)
ax.set_ylabel(‘成本函数 J(’ + r’ θ \theta θ’ + ‘)’, \
fontsize = 18)
ax.set_xlabel(‘Epoch’, fontsize = 18)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-YH54C9TH.jpg
图 3.49:成本函数与 epoch 的关系图(前 100 个 epoch)
在图 3.49 中,我们可以看到,当我们停止更新时,成本函数仍在下降。因此,我们可以通过增大 max_epochs 超参数重新运行训练,看看结果是否有所改善。
使用我们之前导入的 sklearn.metrics 中的 r2_score 函数,计算使用梯度下降训练的模型的 R 平方值:
计算 r 平方值
r2 = r2_score(train_Y, y_pred)
print('r squared = ', r2)
输出应与以下内容类似:
r squared = 0.5488427996385263
注意,您可以调整学习率和最大 epoch 参数,观察它们对训练历史和 r2 值的影响。
现在,我们使用训练数据生成预测值,以便可视化结果模型。在这个单元格中,我们首先使用缩放后的训练数据进行预测,因为模型系数是基于缩放输入的,然后使用我们在缩放过程中保存的(Y_min 和 Y_range)值将结果还原为“实际”值。请注意,我们使用我们的模型函数 h_x 来生成预测值。另外,为了方便起见,我们将 pred_X 替换为原始年份值,用于可视化:
生成预测以便可视化
pred_X = train_X
进行预测
pred_Y = h_x(Beta, pred_X)
将预测值还原为实际值
pred_Y = (pred_Y * Y_range) + Y_min
替换 X 为原始值
pred_X = df_group_year[‘Year’]
使用缩放数据进行回归的一个影响是,β 值是相对于缩放后的数据,而不是原始数据。在许多情况下,我们希望得到未缩放的参数值。特别是在线性回归中,未缩放的系数可以解释为自变量每单位变化时因变量的变化。例如,在我们的案例中,β1 是直线的“斜率”,表示每增加 1 年,平均年气温的变化。我们现在可以计算未缩放模型的参数:
将系数缩放回实际值
Beta0 = (Y_min + Y_range * Beta[0, 0] \
- Y_range * Beta[0, 1] * X_min / X_range)
Beta1 = Y_range * Beta[0, 1] / X_range
可视化结果:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1])
原始数据
raw_plot_data = df
ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \
label = ‘原始数据’, c = ‘red’, s = 1.5)
年度平均
ax.scatter(df_group_year.Year, df_group_year.AvTemp, \
label = ‘年度平均’, c = ‘k’, s = 10)
线性拟合
ax.plot(pred_X, pred_Y, c = “blue”, linestyle = ‘-.’, \
linewidth = 4, label = ‘线性拟合’)
将模型绘制到图表上:
ax.text(1902, 20, 'Temp = ’ + str(round(Beta0, 2)) \
+’ + ’ + str(round(Beta1, 4)) + ’ * Year’, \
fontsize = 16, backgroundcolor = ‘white’)
ax.set_title(‘平均气温测量’, fontsize = 16)
使刻度包含第一个和最后一个年份
tick_years = [1902] + list(range(1910, 2011, 10))
ax.set_xlabel(‘年份’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.set_ylim(15, 21)
ax.set_xticks(tick_years)
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
注意
为确保无错误执行,你应该在编写完步骤 15 和 16 的代码后再运行该单元。
输出结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-PLI7QRI3.jpg
图 3.50:使用梯度下降法测量的平均气温
注意
要访问该特定部分的源代码,请参考 https://packt.live/3diOR76。
你还可以在 https://packt.live/2YWvviZ 在线运行这个例子。你必须执行整个笔记本,才能得到期望的结果。
你刚刚用梯度下降法训练了你的第一个模型。这是一个重要的步骤,因为这个简单的工具可以用来构建更复杂的模型,如逻辑回归和神经网络模型。然而,我们首先必须注意一个重要的观察:梯度下降模型产生的 r-squared 值不如最小二乘法模型高,而且线的方程也不同。
话虽如此,梯度下降过程还有更多的选项可以调整,包括不同类型的梯度下降算法、更高级的学习率使用方法以及在训练过程中如何提供数据。这些修改超出了本书的范围,因为一本书可以专门讨论梯度下降过程及其性能优化方法。通过足够的实验,我们可以将两个结果匹配到任意精度,但在本案例中,这样做并不是高效的时间利用。
在本练习中,我们直接实现了梯度下降;然而,我们通常不会使用这种实现,而是利用现有的高效优化包。scikit-learn 的梯度下降方法包含了许多优化,并且只需几行代码即可使用。以下内容来自 scikit-learn 文档(参考 https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDRegressor.html):“SGD 代表随机梯度下降:每次估计一个样本的损失梯度,模型会在过程中按递减强度(即学习率)更新。”
到目前为止,在我们的示例中,我们直接使用了所有数据进行线性回归方法,或者通过梯度下降方法在每次更新时使用所有数据。然而,使用梯度下降时,我们可以完全控制何时更新我们对参数的估计。
练习 3.06:优化梯度下降
在本练习中,我们将使用 scikit-learn 模块中的 SGDRegressor,它利用随机梯度下降来训练模型。
在本练习中,我们从 synth_temp.csv 数据开始,和之前一样:
如之前一样导入所需的包和类,并添加 SGDRegressor:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
来自 sklearn.metrics 导入 r2_score
来自 sklearn.linear_model 导入 SGDRegressor
加载数据并进行与之前相同的预处理和标准化:
加载数据
df = pd.read_csv(‘…/Datasets/synth_temp.csv’)
从 1902 年开始切片
df = df.loc[df.Year > 1901]
按年份汇总
df_group_year = df.groupby([‘Year’]).agg({‘RgnAvTemp’ : ‘mean’})
添加 Year 列,以便在模型中使用该列
df_group_year[‘Year’] = df_group_year.index
df_group_year = df_group_year\
.rename(columns = {‘RgnAvTemp’ : ‘AvTemp’})
标准化数据
X_min = df_group_year.Year.min()
X_range = df_group_year.Year.max() - df_group_year.Year.min()
Y_min = df_group_year.AvTemp.min()
Y_range = df_group_year.AvTemp.max() - df_group_year.AvTemp.min()
scale_X = (df_group_year.Year - X_min) / X_range
train_X = scale_X.ravel()
train_Y = ((df_group_year.AvTemp - Y_min) / Y_range).ravel()
我们通过调用 SGDRegressor 来实例化模型,并传递超参数。在这里,我们设置了 NumPy random.seed 方法,且由于我们没有为 SGDRegressor 提供种子或方法,它将使用 NumPy 随机生成器:
创建模型对象
np.random.seed(42)
model = SGDRegressor(loss = ‘squared_loss’, max_iter = 100, \
learning_rate = ‘constant’, eta0 = 0.0005, \
tol = 0.00009, penalty = ‘none’)
我们通过调用模型对象的 fit 方法来拟合模型:
拟合模型
model.fit(train_X.reshape((-1, 1)), train_Y)
输出应如下所示,回显调用中使用的参数:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-PDO68833.jpg
图 3.51:调用模型对象的 fit 方法后的输出
我们现在想要从模型中提取系数,并像之前的练习那样将其重新缩放,以便可以直接比较结果:
Beta0 = (Y_min + Y_range * model.intercept_[0] \
- Y_range * model.coef_[0] * X_min / X_range)
Beta1 = Y_range * model.coef_[0] / X_range
print(Beta0)
print(Beta1)
输出应如下所示:
-0.5798539884018439
0.009587734834970016
如前所述,我们现在生成预测结果,然后使用 r2_score 函数计算 r2。请注意,由于我们使用的是 scikit-learn 方法,我们通过模型对象的 predict 方法来返回预测结果:
生成预测结果
pred_X = df_group_year[‘Year’]
pred_Y = model.predict(train_X.reshape((-1, 1)))
计算 r squared 值
r2 = r2_score(train_Y, pred_Y)
print('r squared = ', r2)
结果将类似于以下内容:
r squared = 0.5436475116024911
最后,我们将预测结果重新缩放回实际温度以进行可视化:
将预测结果缩放回实际值
pred_Y = (pred_Y * Y_range) + Y_min
现在,展示结果:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_axes([1, 1, 1, 1])
原始数据
raw_plot_data = df
ax.scatter(raw_plot_data.Year, raw_plot_data.RgnAvTemp, \
label = ‘原始数据’, c = ‘red’, s = 1.5)
年度平均值
ax.scatter(df_group_year.Year, df_group_year.AvTemp, \
label = ‘年度平均’, c = ‘k’, s = 10)
线性拟合
ax.plot(pred_X, pred_Y, c = “blue”, linestyle = ‘-.’, \
linewidth = 4, label = ‘线性拟合’)
将模型绘制在图上
ax.text(1902, 20, ‘温度 = ’ + str(round(Beta0, 2)) +’ + ’ \
- str(round(Beta1, 4)) + ’ * 年份’, fontsize = 16)
ax.set_title(‘平均空气温度测量’, fontsize = 16)
使刻度包括第一个和最后一个年份
tick_years = [1902] + list(range(1910, 2011, 10))
ax.set_xlabel(‘年份’, fontsize = 14)
ax.set_ylabel(‘温度 ( ∘ ^\circ ∘C)’, fontsize = 14)
ax.set_ylim(15, 21)
ax.set_xticks(tick_years)
ax.tick_params(labelsize = 12)
ax.legend(fontsize = 12)
plt.show()
输出将如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-37FGFWB9.jpg
图 3.52:使用 scikit-learn 接口进行线性拟合的梯度下降结果
注意
要访问此特定部分的源代码,请参考 https://packt.live/2zWIadm。
您也可以在 https://packt.live/3eqhroj 上在线运行此示例。您必须执行整个 Notebook 才能得到期望的结果。
将此图与使用梯度下降手动实现构建的图进行比较。注意相似之处:这让我们确信两种梯度下降实现都是正确的。然而,现在我们可以利用 scikit-learn 实现 SGD 的全部功能。对于更复杂的问题,这可能至关重要。例如,您可能已经注意到 SGDRegressor 支持正则化方法,但我们没有使用它们。一些正则化方法向成本函数方程添加调整,以对相对较大的参数值(使参数变小的因子)施加惩罚。有其他方法专门适用于某些模型(例如,人工神经网络有几种额外的正则化方法可供使用)。正则化的一个重要用途是减少过拟合,尽管到目前为止我们使用的简单模型中不存在过拟合问题。有关正则化的进一步讨论可在第六章“集成建模”中找到。
活动 3.05:梯度下降
在此活动中,我们将实现与“活动 3.02: 使用最小二乘法进行线性回归”相同的模型,但我们将使用梯度下降过程。
要执行的步骤如下:
导入模块和类;在本例中:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
from sklearn.linear_model import SGDRegressor
加载数据(austin_weather.csv),并预处理以创建 Day_of_Year 列并切片一整年(2015 年)。
创建经过缩放的 X 和 Y 数据,每种情况都在 0 和 1 之间进行缩放。
使用 SGDRegressor 实例化模型。记得设置 NumPy 的 random.seed() 方法。
拟合模型。
提取经过重新缩放的模型系数 Theta0 和 Theta1,并将它们打印出来。
使用缩放数据生成预测,使用 r2_score 方法获取拟合的 r2 值,然后打印出 r2。
重新缩放预测值以用于绘图。
创建可视化图表,显示原始数据、20 天移动平均线以及新的线性拟合线。在图表上包含模型方程式。
输出应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-MRZHKBBI.jpg
图 3.53:优化的梯度下降预测趋势线
注意
可通过此链接找到此活动的解决方案。
到目前为止,您应该已经熟悉使用 scikit-learn 的 SGDRegressor 接口以及理解梯度下降的基本过程。您还应该对何时使用 SGD 而不是标准线性回归有一些想法。
我们现在已经覆盖了数据平滑和简单线性回归,手动实现了梯度下降算法,并使用 scikit-learn 的 SGDRegressor 接口将梯度下降应用于线性回归。你已经看到如何使用虚拟变量、和多项式特征以及正弦/余弦特征进行一些特征工程。你可能会注意到,大部分代码是用来准备数据和可视化结果的。对于机器学习来说,这并不奇怪——理解和处理数据通常是最重要的任务,并且对成功至关重要。接下来我们将讨论回归的另一个应用——多元线性回归。
多元线性回归
我们已经涵盖了常规线性回归,以及带有多项式和其他项的线性回归,并考虑了用最小二乘法和梯度下降法训练它们。本章的这一部分考虑了一种额外的线性回归类型:多元线性回归,其中使用多个变量(或特征)来构建模型。事实上,我们已经在不明确提及的情况下使用了多元线性回归——当我们添加虚拟变量时,或者再添加正弦和余弦项时,我们实际上是在拟合多个 x 变量以预测单一的 y 变量。
让我们考虑一个简单的例子,说明多元线性回归如何自然成为建模的解决方案。假设你看到以下图表,它显示了一个假设的技术工人在长期职业生涯中的年度总收入。你可以看到,随着时间推移,他们的收入在增加,但数据中有一些异常的跳跃和斜率变化:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-ZO5PBBF9.jpg
图 3.54:假设工人职业生涯中的收入
你可能猜测这位工人时常换工作,导致了收入的波动。然而,假设根据我们得到的数据,薪酬是每年每周平均工作小时数与小时工资的乘积。直观地讲,每年的总收入应当是总工作小时数与小时工资的乘积。我们可以构建一个多元线性模型,使用年份、工资和每周工作小时数来预测总收入,而不是简单的收入与年份的线性模型。在这个假设的情况下,使用多元线性回归与简单线性回归相比,结果如下图所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-FVHX4WL6.jpg
图 3.55:简单线性回归与多元线性回归在假设数据集上的比较
红色圆圈似乎比简单的蓝色线条更能满足模型需求。数据中仍然存在一些未被解释的特征——也许在某些年份有奖金或退休基金配对,或者与数据相关的其他因素我们尚不清楚。尽管如此,在这种情况下,使用多个 x 变量是合理的。
在继续之前,让我们先讨论一些细节。我们将使用 pandas 中的 corr()
方法,它接受一个 DataFrame 并计算所有变量之间的成对相关性。在接下来的练习中,我们将研究两个关键问题。首先,当执行多元回归时,如果某些变量之间高度相关,可能会导致模型出现问题。这个问题被称为多重共线性,它可能导致系数估计对数据或模型的微小变化不稳定。在极端情况下,如果一个变量实际上是其他变量的线性组合,模型可能变得奇异;在某些方法中,线性相关的变量的系数可能会返回为 Inf,或者出现其他错误。其次,我们还希望了解这些变量是否会对预测结果产生影响。让我们通过解决一个练习来更好地理解这一点。
练习 3.07: 多元线性回归
对于这个练习,我们将使用一个 UCI 数据集,它包含一个组合循环电厂的功率输出和几个可能的解释变量:
注意
原始数据和描述文件可从 https://archive.ics.uci.edu/ml/datasets/Combined+Cycle+Power+Plant 获得
或者,您也可以在我们的存储库中找到数据: https://packt.live/2Pu850C
加载模块和类;注意,我们在这里添加了 seaborn,以便在一些可视化中使用:
导入 pandas 为 pd
导入 numpy 为 np
导入 matplotlib.pyplot 为 plt
导入 seaborn 为 sns
来自 sklearn.linear_model 的导入 LinearRegression
加载并检查数据:
注意
以下代码片段中的三引号(“”")用于表示多行代码注释的开始和结束。注释用于帮助解释代码中的特定逻辑。
“”"
加载并检查数据
来自描述文件
(https://archive.ics.uci.edu/ml/machine-learning-databases/00294/)
变量包括:
注意:描述文件中某些变量名称不正确
环境温度 (AT)
环境压力 (AP)
相对湿度 (RH)
排气真空 (V)
因变量是
每小时净电能输出 (PE)
“”"
power_data = pd.read_csv\
(‘…/Datasets/combined_cycle_power_plant.csv’)
print(power_data.shape)
print(power_data.head())
missings = power_data.isnull().sum()
print(missings)
结果应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-B6OMOHPM.jpg
图 3.56: 组合动力循环数据集没有缺失值
由于我们之前没有使用过这个数据,让我们进行一些快速的 EDA(探索性数据分析)。首先,我们将查看所有变量之间的相关性:
“”"
快速 EDA
相关性分析
“”"
corr = power_data.corr()
用于 seaborn 中热图的掩码
mask = np.ones((power_data.shape[1], power_data.shape[1]))
mask = [[1 if j< i else 0 \
for j in range(corr.shape[0])] \
for i in range(corr.shape[1])]
fig, ax = plt.subplots(figsize = (10, 7))
“”"
绘制相关矩阵的热图
隐藏上三角形(重复项)
“”"
sns.heatmap(corr, cmap = ‘jet_r’, square = True, linewidths = 0.5, \
center = 0, annot = True, mask = mask, \
annot_kws = {“size” : 12}, \
xticklabels = power_data.columns, \
yticklabels = power_data.columns)
plt.show()
该图表应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-J58CFLEM.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-JMMJK0S2.jpg)
图 3.57:变量相关性图表
在前面的数据中,x 变量之间最强的相关性是 V 和 AT 之间的 0.84。因此,这里应该没有多重共线性问题。
我们希望能够显示出 x 变量会影响我们试图预测的目标变量 PE。在图表的最后一行,我们可以看到 PE 与所有其他变量之间有显著的相关性,这是它们在模型中都将是有价值的一个良好指示。如果特征非常多,我们可能会考虑删除一些在减少模型噪音方面不太重要的变量。然而,这些相关系数只是成对的,即使我们看到低相关性,也需要更多的工作来证明删除某个变量的合理性。
关于可视化,我们使用了 seaborn 的热图功能来生成图表。由于相关值是对称的,比如 AT 和 V 之间的相关性与 V 和 AT 之间的相关性是相同的。因此,网格的右上三角形会与左下三角形镜像,所以热图方法提供了一种方式来隐藏我们想要忽略的方块。我们通过 mask 变量实现这一点,mask 是一个与相关矩阵形状相同的矩阵,并隐藏任何在 mask 中对应为 False 值的方块。我们使用了嵌套的列表推导式来将 False 值(整数 0)放入 mask 中。
最后,请注意对角线上的值都是 1;根据定义,一个变量与自身的相关性是完美的。你可能会看到这些方块用于绘制变量分布或其他有价值的信息,同时,右上(三角形)或左下(三角形)部分的方块可以用于额外的信息,例如下一步中的图表。
使用 seaborn 的 pairplot 来可视化所有变量之间的成对关系。这些信息可以补充相关图,并且如前所述,有时会将其与相关图合并成一个网格:
(2) 查看成对的变量关系
plot_grid = sns.pairplot(power_data)
结果应该如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-JMMJK0S2.jpg
](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-J58CFLEM.jpg)
图 3.58:Seaborn 数据的 pairplot 图
默认情况下,这个图表展示了每个变量与其他变量之间的散点图,并且每个变量沿对角线的分布。请注意,右上三角形是左下三角形的镜像,且每个图表的坐标轴是翻转的。
沿着对角线,我们可以看到所有变量的分布。我们可以看到 RH 变量向左偏斜;我们可以考虑对该列应用变换,例如 numpy.log(power_data[‘RH’])或 numpy.sqrt(power_data[‘RH’])。但在本次练习中,我们将保持原样。
从这个图表中我们还可以观察到底部行的散点图;注意,AT 与 PE 的负相关性最大,AT 与 PE 的关系呈现出明显的负趋势,这在直观上是有意义的。向右移动时,相关性变得较弱,这与散点图一致。在第三张图中,PE 与 AP 的关系,我们可以看到一些积极相关的迹象。
现在,我们将数据结构化以用于线性回归模型,拟合模型,并获得预测值和 r2 值。这与我们在之前的练习中做的方式相同:
结构化数据
X_train = power_data.drop(‘PE’, axis = 1)
Y_train = power_data[‘PE’]
拟合模型
model = LinearRegression()
model.fit(X_train, Y_train)
获取预测值
Y_pred = model.predict(X_train)
r2 = model.score(X_train, Y_train)
print('模型系数 ’ + str(model.coef_))
print('r2 值 ’ + str(round(r2, 3)))
输出结果应如下所示:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-RNX0761D.jpg
图 3.59:多重线性回归拟合结果
相对较高的 r2 值是模型在预测 PE 时可能有效的良好信号。
对于多重线性回归,我们无法像绘制预测变量与 x 变量的图那样轻松地可视化结果。然而,有一个非常强大的可视化方法几乎可以在任何情况下使用,那就是将预测值与真实值绘制在一起。为了便于解释,最好使这个图对称(x 和 y 的轴范围应相同)——完美的预测值应沿对角线分布。我们添加一条对角线以帮助视觉解释:
fig, ax = plt.subplots(figsize=(10, 10))
设置一些限制
PE_range = max(power_data.PE) - min(power_data.PE)
plot_range = [min(power_data.PE) - 0.05 * PE_range, \
max(power_data.PE) + 0.05 * PE_range]
ax.scatter(Y_train, Y_pred)
ax.set_xlim(plot_range)
ax.set_ylim(plot_range)
ax.set_xlabel(‘实际 PE 值’, fontsize = 14)
ax.set_ylabel(‘预测 PE 值’, fontsize = 14)
ax.plot(plot_range, plot_range, c = “black”)
plt.show()
结果如下:
https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/spr-lrn-ws/img/image-M6OZ7IW5.jpg
图 3.60:多重线性回归的预测值与实际 PE 值对比
图 3.60 显示,大多数预测结果都位于对角线上。也有一些值的预测显著高于大多数,我们可能需要对这些特定点进行进一步调查。此外,在最高值处,预测通常偏低,这可能表明模型中缺少一些特征工程或其他数据。回想一下,我们没有对任何变量进行变换;在这里尝试进行变换可能会有所帮助,看是否能改进结果。我们在这里不会进一步探讨这个问题。
注意
要访问此特定部分的源代码,请参考 packt.live/2CwfIzZ
。
你也可以在线运行此示例,网址是 packt.live/37UzZuK
。你必须执行整个 Notebook 才能获得预期的结果。
我们已经看到,使用多元线性回归是工具集中的一个强大补充,并且是我们已掌握的方法的一个非常简单的扩展。事实上,多元线性回归在回归问题中,往往能够与更复杂的模型表现得一样好甚至更好。尽管我们在这里没有讨论,但使用多元线性回归与人工神经网络相比的一个好处是,多元线性回归模型的系数可以解释为每个 x 变量对预测变量的影响估计;在某些情况下,这种解释非常有价值。
总结
在本章中,我们迈出了构建机器学习模型并使用带标签的数据集进行预测的第一步。我们通过查看多种构建线性模型的方式开始了分析,首先使用精确的最小二乘法,这是在使用现有计算机内存处理小量数据时非常有效的方法。线性模型的性能可以通过使用虚拟变量来提高,这些虚拟变量是由分类变量创建的,能够为模型添加额外的特征和背景。随后,我们使用带有多项式模型的线性回归分析来进一步提高性能,为数据集拟合更自然的曲线,并探讨了通过增加正弦和余弦序列作为预测变量的其他非线性特征工程。
作为显式线性回归的一个概括,我们实现了梯度下降算法。正如我们所指出的,梯度下降虽然不像最小二乘法那样精确(对于给定的迭代次数或轮次),但能够处理任意大的数据集和更多的变量。此外,使用广义梯度下降引入了许多其他参数,即所谓的超参数,我们作为数据科学家可以对其进行优化,从而提高模型性能。我们将进一步的模型优化研究推迟到第七章《模型评估》。
现在我们已经对线性回归有了充分的理解,在下一章中我们将深入研究自回归模型。