R-应用无监督学习-全-
R 应用无监督学习(全)
原文:
annas-archive.org/md5/4c0eaccb71c3cc25e04cea28e06cad11译者:飞龙
第一章:前言
关于
本节简要介绍了作者、本书涵盖的内容、您开始所需的技术技能以及完成所有包含的活动和练习所需的硬件和软件要求。
关于本书
从基础知识开始,使用 R 进行应用无监督学习解释了聚类方法、分布分析、数据编码器和 R 的所有特性,这些特性使您能更好地理解数据并回答您最紧迫的业务问题。
本书从无监督学习最重要的和最常用的方法——聚类——开始,并解释了三种主要的聚类算法:k-means、划分和层次聚类。在此之后,您将学习市场篮子分析、核密度估计、主成分分析和异常检测。您将使用 R 编写的代码来介绍这些方法,并进一步说明如何使用、编辑和改进 R 代码。为了帮助您获得实际的理解,本书还提供了将这些方法应用于实际业务问题的有用提示,包括市场细分和欺诈检测。通过完成有趣的活动,您将探索数据编码器和潜在变量模型。
在本书结束时,您将更好地理解不同的异常检测方法,例如异常值检测、马氏距离以及上下文和集体异常检测。
关于作者
阿洛克·马利克是一位印度数据科学家。他之前在金融、加密货币交易、物流和自然语言处理等领域创建和部署无监督学习解决方案。他拥有印度信息技术、设计和制造学院贾巴尔普尔的科技学士学位,在那里他学习电子和通信工程。
布拉德福德·塔克菲尔德为各种行业的企业设计和实施数据科学解决方案。他获得了数学学士学位和经济学博士学位。他为学术期刊和大众媒体撰写文章,主题包括线性代数、心理学和公共政策。
摘要
设计聪明的算法,从非结构化和未标记的数据中发现隐藏的模式和业务相关的见解。
关键特性
-
构建能够解决您企业问题的最先进算法
-
学习如何在您的数据中找到隐藏的模式
-
通过使用真实世界的数据集进行动手练习来实施关键概念
描述
从基础知识开始,使用 R 进行应用无监督学习解释了聚类方法、分布分析、数据编码器和 R 的所有特性,这些特性使您能更好地理解数据并回答您所有的业务问题。
学习目标
-
实现聚类方法,如 k-means、层次聚类和划分聚类
-
使用 R 编写代码来分析市场细分和消费者行为
-
估计不同结果的分布和概率
-
使用主成分分析实现降维
-
应用异常检测方法以识别欺诈
-
使用 R 设计算法并学习如何编辑或改进代码
目标受众
《使用 R 进行无监督学习应用》是为希望了解更好地理解数据的方法的商业专业人士和有兴趣进行无监督学习的开发者设计的。尽管这本书是为初学者准备的,但了解 R 的基本、入门级知识将有所帮助。这包括了解如何打开 R 控制台、如何读取数据以及如何创建循环。为了轻松理解本书的概念,您还应了解基本数学概念,包括指数、*方根、*均值和中位数。
方法
《使用 R 进行无监督学习应用》采用实践方法,通过 R 揭示您非结构化数据中的隐藏模式。它包含多个活动,使用现实生活中的商业场景让您练习并应用您的新技能,在高度相关的环境中。
硬件要求
为了获得最佳的学生体验,我们推荐以下硬件配置:
-
处理器:Intel Core i5 或同等性能
-
内存:4 GB RAM
-
存储:5 GB 可用空间
-
一个互联网连接
软件要求
我们还建议您提前安装以下软件:
-
操作系统:Windows 7 SP1 64 位,Windows 8.1 64 位或 Windows 10 64 位,Linux(Ubuntu、Debian、Red Hat 或 Suse),或最新版本的 OS X
-
R(3.0.0 或更高版本,可在
cran.r-project.org/免费获取)
规范
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:“我们导入一个factoextra库来可视化我们刚刚创建的聚类。”
代码块设置如下:
plot(iris_data$Sepal.Length,iris_data$Sepal.Width,col=iris_data$t_color)
points(k1[1],k1[2],pch=4)
points(k2[1],k2[2],pch=5)
points(k3[1],k3[2],pch=6)
新术语和重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,将以如下方式显示:“执行 k-medoids 聚类的算法有很多种。其中最简单、最有效的是围绕类中心的划分,或简称PAM。”
安装和设置
每一段伟大的旅程都始于一个谦卑的步伐。我们即将在数据整理领域的冒险也不例外。在我们能够用数据做些令人惊叹的事情之前,我们需要准备好最富有成效的环境。在这篇简短的笔记中,我们将了解如何做到这一点。
在 Windows 上安装 R
要在 Windows 上安装 R,请按照以下步骤操作:
-
打开最新版 R 的 Windows 页面:
cran.r-project.org/bin/windows/base/。 -
点击名为“下载 R X.X.X for Windows”的链接,其中每个 X 都是一个自然数,例如 R 3.5.3。这将启动文件下载。
-
您在步骤 1中下载的文件是一个
exe文件。双击此文件,将运行一个程序,在您的计算机上安装 R。 -
在步骤 3中运行的安装程序将提示您有关如何安装 R 的一些问题。为每个提示选择默认选项。
-
您可以通过在 Windows 计算机上步骤 4中安装位置的双击 R 的图标来打开 R。
在 macOS X 上安装 R
要在 macOS X 上安装 R,请执行以下操作:
-
打开以下链接,该链接专门提供适用于 macOS X 的最新版 R:
cran.r-project.org/bin/macosx/. -
在最新发布标题下,点击名为R-X.X.X.pkg的文件,其中每个 X 都代表一个自然数,例如,R-3.5.2.pkg。
-
打开您在步骤 2中下载的文件。这将是一个
.pkg文件。当您打开此文件时,OS X 将打开安装向导,引导您完成剩余过程。 -
当安装向导要求您做出关于安装的决定时,请选择默认选项。
-
安装向导完成工作后,您可以在安装位置的双击 R 的图标。这将打开一个 R 控制台。
在 Linux 上安装 R
-
确定您的 Linux 版本使用的是哪种包管理器。两个常见的包管理器示例是
yum和apt。 -
打开终端,使用您的包管理器输入以下两个命令:
sudo apt update sudo apt install r-base在这个例子中,我们使用了
apt作为包管理器,但如果您的 Linux 版本使用yum或其他包管理器,您应该将这些两行中的每个apt替换为yum或您的包管理器名称。 -
如果您遇到问题,可能是您的 Linux 版本没有访问正确的仓库。为了告诉 Linux 正确的仓库位置,您应该打开您版本 Linux 的
sources.list文件。对于 Ubuntu,此文件默认存储在/etc/apt/sources.list。 -
在
sources.list文件中,如果您使用的是 Debian 或 Ubuntu,则需要添加以下两条中的一条。对于 Ubuntu 版本,使用以下链接:http://cran.rstudio.com/bin/linux/ubuntu/。对于 Debian 版本,使用以下链接:debhttp://cran.rstudio.com/bin/linux/debian/。如果您使用的是 Red Hat,您可以在终端中运行以下命令来告诉 Linux 下载 R 所使用的仓库:sudo yum install epel-release。
第二章:第一章
聚类方法简介
学习目标
到本章结束时,你将能够:
-
描述聚类的用途
-
使用内置的 R 库执行 k-means 算法
-
使用内置的 R 库执行 k-medoids 算法
-
确定最佳聚类数量
在本章中,我们将探讨聚类的概念和一些基本的聚类算法。
简介
21 世纪是数字世纪,在这个世纪里,经济阶梯上的每个人都在以前所未有的速度使用数字设备并以数字格式产生数据。在过去 10 年中产生的 90%的数据是在过去两年中产生的。这是一个指数增长的趋势,数据量每两年增加 10 倍。预计这种趋势将在可预见的未来继续:

图 1.1:年度数字数据增长
但这些数据不仅仅存储在硬盘上;它们正在被用来让生活变得更好。例如,谷歌使用它拥有的数据为你提供更好的搜索结果,Netflix 使用它拥有的数据为你提供更好的电影推荐。事实上,他们制作热门剧集《纸牌屋》的决定是基于数据分析的。IBM 正在使用它拥有的医疗数据创建一个人工智能医生,并从 X 光图像中检测癌细胞。
为了使用计算机处理如此大量的数据并得出相关结果,使用特定类别的算法。这些算法统称为机器学习算法。机器学习根据所使用的数据类型分为两部分:一种称为监督学习,另一种称为无监督学习。
当我们获得标记数据时,进行监督学习。例如,假设我们从一家医院获得了 1,000 张标记为正常或骨折的 X 光片。我们可以使用这些数据来训练一个机器学习模型,以预测 X 光片是否显示骨折的骨头。
无监督学习是在我们只有原始数据并期望在没有标签的情况下得出见解时进行的。我们有能力理解数据并识别其中的模式,而无需明确告知要识别哪些模式。到本书结束时,你将了解所有主要类型的无监督学习算法。在本书中,我们将使用 R 编程语言进行演示,但算法对所有语言都是相同的。
在本章中,我们将研究最基本的无监督学习类型,聚类。首先,我们将研究聚类是什么,它的类型,以及如何使用任何类型的数据集创建聚类。然后我们将研究每种聚类类型的工作原理,查看它们的优缺点。最后,我们将学习何时使用哪种类型的聚类。
聚类简介
聚类是一组用于根据数据集中变量的预定义属性找到自然分组的方法或算法。Merriam-Webster 词典将聚类定义为“一起发生的一组相似事物。”无监督学习中的聚类在传统意义上正是这个意思。例如,你如何从远处识别一串葡萄?你不需要仔细看这串葡萄就能直观地感觉到葡萄是否相互连接。聚类就是这样。下面提供了一个聚类的例子:

图 1.2:数据集中两个聚类的表示
在前面的图表中,数据点有两个属性:胆固醇和血压。根据它们之间的欧几里得距离,数据点被分类为两个聚类,或两个串。一个聚类包含明显有心脏病高风险的人,另一个聚类包含心脏病风险低的人。也可以有超过两个聚类,如下面的例子所示:

图 1.3:数据集中三个聚类的表示
在前面的图表中,有三个聚类。一个额外的人群有高血压但胆固醇低。这个群体可能或可能没有心脏病风险。在接下来的章节中,将在实际数据集上展示聚类,其中 x 和 y 坐标表示实际数量。
聚类的应用
与所有无监督学习方法一样,聚类通常在我们没有标记数据——即具有预定义类别的数据——用于训练模型时使用。聚类使用各种属性,如欧几里得距离和曼哈顿****距离,来寻找数据中的模式并将它们根据其属性的相似性进行分类,而不需要任何用于训练的标签。因此,聚类在标记数据不可用或我们想要找到由标签未定义的模式的应用领域中有许多用例。
以下是一些聚类的应用:
-
探索性数据分析:当我们有未标记的数据时,我们通常会进行聚类以探索数据集的潜在结构和类别。例如,一家零售店可能想要根据购买历史来探索他们有多少不同的客户细分市场。
-
生成训练数据:有时,在用聚类方法处理未标记数据后,它可以被标记以进一步使用监督学习算法进行训练。例如,两个未标记的不同类别可能形成两个完全不同的聚类,我们可以使用它们的聚类来为更有效的实时分类的监督学习算法标记数据,这些算法比我们的无监督学习算法更有效。
-
推荐系统:借助聚类,我们可以找到相似物品的特性,并利用这些特性进行推荐。例如,一个电子商务网站,在找到同一集群的客户后,可以根据该集群中其他客户购买的物品向该集群中的客户推荐商品。
-
自然语言处理:聚类可以用于对相似词语、文本、文章或推文的分组,无需标签数据。例如,你可能希望自动将同一主题的文章分组。
-
异常检测:你可以使用聚类来找到异常值。我们将在第六章 异常检测 中学习这一点。异常检测也可以用于数据中存在不*衡类别的情况,例如在欺诈信用卡交易检测中。
爱丽丝数据集简介
在本章中,我们将使用爱丽丝花朵数据集进行练习,学习如何在不使用标签的情况下对三种爱丽丝花朵(Versicolor、Setosa 和 Virginica)进行分类。这个数据集是 R 内置的,非常适合学习聚类技术的实现。
注意,在我们的练习数据集中,我们为花朵有最终标签。我们将比较聚类结果与这些标签。我们选择这个数据集只是为了展示聚类结果是有意义的。在数据集如批发客户数据集(本书后面将介绍)的情况下,我们没有最终标签,聚类结果无法客观验证,因此可能会导致错误的结论。这就是在实际生活中没有数据集的最终标签时使用聚类的用例。一旦你完成了这两个练习和活动,这一点将更加清晰。
练习 1:探索爱丽丝数据集
在这个练习中,我们将学习如何在 R 中使用爱丽丝数据集。假设你已经在系统中安装了 R,让我们继续:
-
按如下方式将爱丽丝数据集加载到变量中:
iris_data<-iris -
现在,我们的爱丽丝数据已经存储在
iris_data变量中,我们可以使用 R 中的head函数查看其前几行:head(iris_data)输出如下:
![图 1.4:爱丽丝数据集的前六行
![img/C12628_01_04.jpg]()
图 1.4:爱丽丝数据集的前六行
我们可以看到我们的数据集有五个列。我们将主要使用两个列来简化二维图的可视化。
聚类类型
如前所述,聚类算法可以在数据中找到自然分组。我们可以以多种方式在数据中找到自然分组。以下是我们将在本章中研究的方法:
-
k-means 聚类
-
k-medoids 聚类
一旦基本聚类类型的概念清晰,我们将探讨其他类型的聚类,如下所示:
-
k-modes
-
基于密度的聚类
-
聚类层次聚类
-
分裂聚类
k-means 聚类简介
K-means 聚类是最基本的无监督学习算法之一。这个算法根据预定义的相似度或距离度量找到自然分组。距离度量可以是以下任何一种:
-
欧几里得距离
-
曼哈顿距离
-
余弦距离
-
汉明距离
要了解距离度量做什么,以一束笔为例。你有 12 支笔。其中 6 支是蓝色的,6 支是红色的。其中 6 支是圆珠笔,6 支是墨水笔。如果你要用墨水颜色作为相似度度量,那么 6 支蓝色笔和 6 支红色笔将位于不同的簇中。这里的 6 支蓝色笔可以是墨水笔或圆珠笔,没有限制。但如果你要用笔的类型作为相似度度量,那么 6 支墨水笔和 6 支圆珠笔将位于不同的簇中。现在,每个簇中的笔是否颜色相同并不重要。
欧几里得距离
欧几里得距离是任意两点之间的直线距离。在二维空间中计算这个距离可以被视为你在学校可能学过的勾股定理的扩展。但欧几里得距离可以在任何 n 维空间中计算,而不仅仅是二维空间。任意两点之间的欧几里得距离是它们坐标差的*方和的*方根。这里展示了欧几里得距离计算的例子:
![图 1.5:欧几里得距离计算的表示
![img/C12628_01_05.jpg]
图 1.5:欧几里得距离计算的表示
在 k-means 聚类中,使用欧几里得距离。使用欧几里得距离的一个缺点是,当数据的维度非常高时,它就失去了意义。这与一个被称为维度诅咒的现象有关。当数据集具有许多维度时,它们可能更难处理,因为所有点之间的距离都可能变得极高,而这些距离难以解释和可视化。
因此,当数据的维度非常高时,我们要么通过主成分分析来降低其维度,我们将在第四章,降维中学习到这一方法,要么使用余弦相似度。
曼哈顿距离
根据定义,曼哈顿距离是沿着与坐标轴成直角测量的两点之间的距离:
![图 1.6:曼哈顿距离的表示
![img/C12628_01_06.jpg]
图 1.6:曼哈顿距离的表示
对角线的长度是两点之间的欧几里得距离。曼哈顿距离简单地说就是两个坐标之间差的绝对值的总和。因此,欧几里得距离和曼哈顿距离的主要区别在于,在欧几里得距离中,我们*方坐标之间的距离,然后取和的根,但在曼哈顿距离中,我们直接取坐标之间差的绝对值的总和。
余弦距离
任意两点之间的余弦相似度定义为以原点为顶点的任意两点之间的角度的余弦值。它可以通过将任意两个向量的点积除以向量的模的乘积来计算:

图 1.7:余弦相似度和余弦距离的表示
余弦距离定义为(1-余弦相似度)。
余弦距离的范围是 0 到 2,而余弦相似度的范围在-1 到 1 之间。始终记住,余弦相似度是余弦距离的值的倒数。
汉明距离
汉明距离是一种特殊的距离,用于分类变量。给定两个维度相等的点,汉明距离定义为彼此不同的坐标的数量。例如,让我们取两个点(0,1,1)和(0,1,0)。这两个变量之间只有一个值不同,即最后一个值。因此,它们之间的汉明距离是 1:

图 1.8:汉明距离的表示
k-means 聚类算法
K-means 聚类用于在未标记的数据集的相似点数据集中找到簇。在本章中,我们将使用鸢尾花数据集。这个数据集包含了不同物种的花瓣长度和宽度的信息。借助无监督学习,我们将学习如何在不了解哪些属性属于哪个物种的情况下区分它们。以下是我们数据集的散点图:

图 1.9:鸢尾花数据集的散点图
这是鸢尾花数据集两个变量的散点图:花瓣长度和花瓣宽度。
如果我们要根据点之间的距离来识别前一个数据集中的簇,我们会选择看起来像挂在树上的葡萄串的簇。你可以看到有两个主要的大串(一个在左上角,另一个是剩余的点)。k-means 算法识别这些“葡萄串”。
下图显示了相同的散点图,但用不同颜色显示了三种不同的鸢尾花品种。这些品种来自原始数据集的“species”列,具体如下:鸢尾花 setosa(用绿色表示),鸢尾花 versicolor(用红色表示),和鸢尾花 virginica(用蓝色表示)。我们将通过形成自己的分类来尝试确定这些品种,使用聚类:

图 1.10:展示不同品种的鸢尾花数据集的散点图
这里是鸢尾花 setosa 的照片,在先前的散点图中用绿色表示:

图 1.11:鸢尾花
下面的照片是鸢尾花 versicolor,在先前的散点图中用红色表示:

图 1.12:鸢尾花 versicolor
这里是鸢尾花 virginica 的照片,在先前的散点图中用蓝色表示:

图 1.13:鸢尾花 virginica
实现 k-means 聚类的步骤
正如我们在图 1.9 中的散点图中看到的,每个数据点代表一朵花。我们将找到可以识别这些品种的簇。为了进行这种聚类,我们将使用 k-means 聚类,其中 k 是我们想要的簇数。以下执行 k-means 聚类的步骤,为了理解简单,我们将用两个簇来演示。我们将在稍后使用三个簇,以尝试匹配实际的物种分组:
-
在散点图上选择任意两个随机坐标,k1 和 k2,作为初始簇中心。
-
计算散点图中每个数据点与坐标 k1 和 k2 的距离。
-
根据数据点与 k1 或 k2 的接*程度,将每个数据点分配到簇中。
-
找到每个簇中所有点的*均坐标,并将 k1 和 k2 的值分别更新到这些坐标。
-
从步骤 2重新开始,直到 k1 和 k2 的坐标停止显著移动,或者经过一定预定的迭代次数后。
我们将使用图表和代码演示先前的算法。
练习 2:在鸢尾花数据集上实现 k-means 聚类
在这个练习中,我们将逐步实现 k-means 聚类:
-
在
iris_data变量中加载内置的鸢尾花数据集:iris_data<-iris -
设置不同物种在散点图上的颜色,以便表示。这将帮助我们看到三种不同的物种是如何在我们最初的两组分类之间分割的:
iris_data$t_color='red' iris_data$t_color[which(iris_data$Species=='setosa')]<-'green' iris_data$t_color[which(iris_data$Species=='virginica')]<-'blue' -
选择任意两个随机簇的中心开始:
k1<-c(7,3) k2<-c(5,3)注意
你可以尝试改变点,看看它如何影响最终的簇。
-
绘制散点图,并包含您在上一步骤中选择的中心。在第一行中,将鸢尾花花瓣的长度和宽度以及颜色传递给
plot函数,然后将中心和点的 x 和 y 坐标传递给points()函数。在这里,pch用于选择聚类中心的表示类型——在这种情况下,4 代表一个十字,5 代表一个菱形:plot(iris_data$Sepal.Length,iris_data$Sepal.Width,col=iris_data$t_color) points(k1[1],k1[2],pch=4) points(k2[1],k2[2],pch=5)输出如下:
![图 1.14:所选聚类中心点的散点图]()
图 1.14:所选聚类中心点的散点图
-
选择您想要的迭代次数。迭代次数应使得每次迭代后中心的变化不再显著。在我们的例子中,六次迭代就足够了:
number_of_steps<-6 -
初始化将跟踪循环中迭代次数的变量:
n<-1 -
开始
while循环以找到最终的聚类中心:while(n<number_of_steps) { -
计算每个点到当前聚类中心的距离,这是算法中的第二步。我们在这里使用
sqrt函数计算欧几里得距离:iris_data$distance_to_clust1 <- sqrt((iris_data$Sepal.Length-k1[1])²+(iris_data$Sepal.Width-k1[2])²) iris_data$distance_to_clust2 <- sqrt((iris_data$Sepal.Length-k2[1])²+(iris_data$Sepal.Width-k2[2])²) -
将每个点分配到其最*的中心所在的聚类,这是算法的第三步:
iris_data$clust_1 <- 1*(iris_data$distance_to_clust1<=iris_data$distance_to_clust2) iris_data$clust_2 <- 1*(iris_data$distance_to_clust1>iris_data$distance_to_clust2) -
通过计算每个聚类中点的*均
x和y坐标来计算新的聚类中心(算法中的第四步),使用 R 中的mean()函数:k1[1]<-mean(iris_data$Sepal.Length[which(iris_data$clust_1==1)]) k1[2]<-mean(iris_data$Sepal.Width[which(iris_data$clust_1==1)]) k2[1]<-mean(iris_data$Sepal.Length[which(iris_data$clust_2==1)]) k2[2]<-mean(iris_data$Sepal.Width[which(iris_data$clust_2==1)]) -
更新变量以记录迭代的次数,以便有效地执行算法的第五步:
n=n+1 } -
现在,我们将用新的颜色覆盖物种颜色以展示两个聚类。因此,我们的下一个散点图上只有两种颜色——一种颜色代表聚类 1,另一种颜色代表聚类 2:
iris_data$color='red' iris_data$color[which(iris_data$clust_2==1)]<-'blue' -
绘制包含聚类及其中心的新的散点图:
plot(iris_data$Sepal.Length,iris_data$Sepal.Width,col=iris_data$color) points(k1[1],k1[2],pch=4) points(k2[1],k2[2],pch=5)输出如下:

图 1.15:用不同颜色表示每个聚类的散点图
注意到 setosa(以前是绿色)已被分组在左侧聚类中,而大多数 virginica 花朵(以前是蓝色)已被分组在右侧聚类中。versicolor 花朵(以前是红色)被分在两个新的聚类之间。
您已成功实现 k-means 聚类算法,根据花瓣大小识别两组花朵。注意算法运行后中心位置的变化。
在以下活动中,我们将增加聚类的数量到三个,以查看我们是否可以将花朵正确地分组到三种不同的物种中。
活动 1:使用三个聚类的 k-means 聚类
编写一个 R 程序,使用三个聚类对鸢尾花数据集进行 k-means 聚类。在这个活动中,我们将执行以下步骤:
-
在图上选择任意三个随机坐标,k1、k2 和 k3,作为中心。
-
计算每个数据点到 k1、k2 和 k3 的距离。
-
通过找到最*的聚类中心来对每个点进行分类。
-
找到各自聚类中所有点的*均坐标,并将 k1、k2 和 k3 的值更新到这些值。
-
从 步骤 2 重新开始,直到 k1、k2 和 k3 的坐标停止显著移动,或者经过 10 次迭代过程后。
本活动的结果将是一个包含三个聚类的图表,如下所示:


图 1.16:给定聚类中心的预期散点图
您可以将您的图表与图 1.10 进行比较,以查看聚类与实际物种分类的匹配程度。
注意
本活动的解决方案可在第 198 页找到。
使用内置函数的 k-means 聚类介绍
在本节中,我们将使用 R 的某些内置库来执行 k-means 聚类,而不是编写冗长且容易出错的自定义代码。使用预构建库而不是编写自己的代码也有其他优点:
-
库函数在计算上效率很高,因为这些函数的开发投入了数千人的工时。
-
库函数几乎无错误,因为它们在几乎所有实际可用的场景中都被数千人测试过。
-
使用库可以节省时间,因为您不必花费时间编写自己的代码。
使用三个聚类的 k-means 聚类
在上一个活动中,我们通过编写自己的代码执行了具有三个聚类的 k-means 聚类。在本节中,我们将借助预构建的 R 库实现类似的结果。
首先,我们将在数据集中以三种类型的花的分布开始,如下所示:


图 1.17:用三种颜色表示三种鸢尾花物种的图
在前面的图中,setosa 用蓝色表示,virginica 用灰色表示,versicolor 用粉色表示。
使用这个数据集,我们将执行 k-means 聚类,看看内置算法是否能够自己找到模式来根据花瓣大小对这些三种鸢尾花物种进行分类。这次,我们只需要四行代码。
练习 3:使用 R 库进行 k-means 聚类
在这个练习中,我们将学习使用 R 的预构建库以更简单的方式执行 k-means 聚类。通过完成这个练习,您将能够将三种 Iris 物种划分为三个单独的聚类:
-
我们将鸢尾花数据集的前两列,即花瓣长度和花瓣宽度,放入
iris_data变量中:iris_data<-iris[,1:2] -
我们找到 k-means 聚类中心以及每个点所属的聚类,并将所有这些存储在
km.res变量中。在这里,在kmeans函数中,我们将数据集作为第一个参数输入,我们想要的聚类数量作为第二个参数:km.res<-kmeans(iris_data,3)注意
k-means 函数有许多输入变量,可以调整以获得不同的最终输出。你可以在
www.rdocumentation.org/packages/stats/versions/3.5.1/topics/kmeans的文档中了解更多信息。 -
按以下步骤安装
factoextra库:install.packages('factoextra') -
我们导入
factoextra库来可视化我们刚刚创建的簇。Factoextra是一个 R 包,用于绘制多元数据:library("factoextra") -
生成簇的图。在这里,我们需要将 k-means 的结果作为第一个参数输入。在
data中,我们需要输入聚类所用的数据。在pallete中,我们选择点的几何形状类型,在ggtheme中,我们选择输出图的样式:fviz_cluster(km.res, data = iris_data,palette = "jco",ggtheme = theme_minimal())输出将如下所示:

图 1.18:三种鸢尾花已经被聚成三个簇
在这里,如果你将图 1.18 与图 1.17 进行比较,你会看到我们几乎正确地分类了所有三种物种。我们生成的簇与图 1.18 中显示的物种不完全匹配,但考虑到仅使用花瓣长度和宽度进行分类的限制,我们已经非常接*了。
从这个例子中,你可以看到,如果我们不知道它们的物种,聚类将是一个非常有用的对鸢尾花进行分类的方法。你将遇到许多数据集的例子,在这些数据集中,你没有标记的类别,但能够使用聚类来形成自己的分组。
市场细分简介
市场细分是根据共同特征将客户划分为不同的细分市场。以下是一些客户细分的用途:
-
提高客户转化率和留存率
-
通过识别特定细分市场和其需求来开发新产品
-
通过特定细分市场改善品牌沟通
-
识别营销策略中的差距并制定新的营销策略以增加销售额
练习 4:探索批发客户数据集
在这个练习中,我们将查看批发客户数据集中的数据。
注意
对于所有需要导入外部 CSV 或图像文件的外部练习和活动,请转到RStudio-> 会话-> 设置工作目录-> 到源文件位置。你可以在控制台中看到路径已自动设置。
-
要下载 CSV 文件,请访问
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson01/Exercise04/wholesale_customers_data.csv。点击wholesale_customers_data.csv。注意
此数据集来自 UCI 机器学习仓库。您可以在以下网址找到数据集:
archive.ics.uci.edu/ml/machine-learning-databases/00292/。我们已经下载了文件并将其保存在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson01/Exercise04/wholesale_customers_data.csv. -
将其保存到您安装 R 的文件夹中。现在,要在 R 中加载它,请使用以下函数:
ws<-read.csv("wholesale_customers_data.csv") -
现在,我们可以通过使用以下 R 函数来查看这个数据集的不同列和行:
head(ws)输出如下:
![Figure 1.19:批发客户数据集的列]
![img/C12628_01_19.jpg]
图 1.19:批发客户数据集的列
这六行显示了按产品类别划分的年度货币消费的前六行。
活动二:使用 k-means 进行客户细分
对于这个活动,我们将使用来自 UCI 机器学习仓库的批发客户数据集。它可在以下网址找到:github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson01/Activity02/wholesale_customers_data.csv。我们将通过聚类来识别属于不同市场细分、喜欢购买不同类型商品的客户。尝试使用 k-means 聚类,k 的值为 2 到 6。
注意
此数据集来自 UCI 机器学习仓库。您可以在以下网址找到数据集:archive.ics.uci.edu/ml/machine-learning-databases/00292/。我们已经下载了文件并将其保存在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson01/Activity02/wholesale_customers_data.csv.
这些步骤将帮助您完成活动:
-
将从 UCI 机器学习仓库下载的数据读入一个变量。数据可在以下网址找到:
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson01/Activity02/wholesale_customers_data.csv. -
只选择两列,即杂货和冷冻食品,以便于可视化聚类。
-
如同第 4 步中练习 4,探索批发客户数据集的第 2 步,将聚类数量设置为 2 并生成聚类中心。
-
按照第 4 步中的练习 4,探索批发客户数据集绘制图表。
-
保存您生成的图表。
-
通过改变聚类数量的值,重复步骤 3、步骤 4和步骤 5,将聚类数量设置为 3、4、5 和 6。
-
决定哪个聚类数量的值最适合对数据进行分类。
输出将是一个包含六个聚类的图表,如下所示:

图 1.20:预期六个聚类的图表
注意
本活动的解决方案可以在第 201 页找到。
k-medoids 聚类简介
k-medoids 是另一种聚类算法,可用于在数据集中找到自然分组。k-medoids 聚类与 k-means 聚类非常相似,除了几个不同之处。k-medoids 聚类算法的优化函数与 k-means 略有不同。在本节中,我们将研究 k-medoids 聚类。
k-medoids 聚类算法
有许多不同类型的算法可以执行 k-medoids 聚类,其中最简单且效率最高的算法是基于聚类中心的划分,简称 PAM。在 PAM 中,我们执行以下步骤来找到聚类中心:
-
从散点图中选择 k 个数据点作为聚类中心的起始点。
-
计算它们与散点图中所有点的距离。
-
将每个点分类到其最*的中心所在的聚类中。
-
在每个聚类中选择一个新点,该点使该聚类中所有点与该点的距离之和最小。
-
重复步骤 2,直到中心不再改变。
您可以看到,PAM 算法与 k-means 聚类算法相同,除了步骤 1和步骤 4。对于大多数实际应用,k-medoids 聚类几乎给出了与 k-means 聚类相同的结果。但在某些特殊情况下,如果数据集中有异常值,k-medoids 聚类更受欢迎,因为它对异常值更稳健。关于何时使用哪种类型的聚类及其差异将在后面的章节中研究。
k-medoids 聚类代码
在本节中,我们将使用与上一节相同的鸢尾花数据集,并将其与上次的结果进行比较,以查看结果是否与上次的结果有明显的不同。我们将直接使用 R 的库来执行 PAM 聚类,而不是编写代码来执行 k-medoids 算法的每个步骤。
练习 5:实现 k-medoid 聚类
在这个练习中,我们将使用 R 的预建库来执行 k-medoids 聚类:
-
将鸢尾花数据集的前两列存储在
iris_data变量中:iris_data<-iris[,1:2] -
安装
cluster包:install.packages("cluster") -
导入
cluster包:library("cluster") -
将 PAM 聚类结果存储在
km.res变量中:km<-pam(iris_data,3) -
导入
factoextra库:library("factoextra") -
在图中绘制 PAM 聚类结果:
fviz_cluster(km, data = iris_data,palette = "jco",ggtheme = theme_minimal())输出如下所示:

图 1.21:k-medoids 聚类结果
k-medoids 聚类的结果与我们在上一节中执行的 k-means 聚类结果没有很大差异。
因此,我们可以看到,前面的 PAM 算法将我们的数据集划分为三个与 k-means 聚类得到的簇相似的簇。如果我们将这两种聚类的结果并排绘制,我们可以清楚地看到它们是多么相似:

图 1.22:k-medoids 聚类与 k-means 聚类的结果
在前面的图表中,观察 k-means 和 k-medoids 聚类的中心是如何如此接*的,但 k-medoids 聚类的中心直接重叠在数据中的点上,而 k-means 聚类的中心则不是。
k-means 聚类与 k-medoids 聚类
现在我们已经研究了 k-means 和 k-medoids 聚类,它们几乎相同,我们将研究它们之间的差异以及何时使用哪种类型的聚类:
-
计算复杂度:在这两种方法中,k-medoids 聚类的计算成本更高。当我们的数据集太大(>10,000 个点)且我们想要节省计算时间时,我们将更倾向于选择 k-means 聚类而不是 k-medoids 聚类。
注意
数据集的大小完全取决于可用的计算能力。随着时间的推移,计算成本越来越低,因此被认为是大数据集的标准将在未来发生变化。
-
异常值的存在:与 k-medoids 聚类相比,k-means 聚类对异常值更敏感。由于数据集中存在异常值,簇中心的定位可能会发生显著变化,因此当我们需要构建对异常值有弹性的簇时,我们使用 k-medoids 聚类。
-
簇中心:k-means 和 k-medoids 算法以不同的方式找到簇中心。k-medoids 簇的中心始终是数据集中的数据点。k-means 簇的中心不需要是数据集中的数据点。
活动三:使用 k-medoids 聚类进行客户细分
使用批发客户数据集进行 k-means 和 k-medoids 聚类,然后比较结果。将从 UCI 机器学习仓库下载的数据读入一个变量。数据可以在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson01/Data/wholesale_customers_data.csv找到。
注意
此数据集来自 UCI 机器学习仓库。您可以在archive.ics.uci.edu/ml/machine-learning-databases/00292/找到数据集。我们已经下载了文件并将其保存在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson01/Activity03/wholesale_customers_data.csv.
这些步骤将帮助您完成活动:
-
仅选择两列,杂货和冷冻,以便于进行簇的二维可视化。
-
使用 k-medoids 聚类来绘制显示四个簇的图表。
-
使用 k-means 聚类来绘制四簇图表。
-
比较两个图表,以评论两种方法的结果差异。
结果将是一个 k-means 簇的图表,如下所示:

图 1.23:簇的预期 k-means 图
注意
本活动的解决方案可以在第 206 页找到。
决定簇的最佳数量
到目前为止,我们一直在处理鸢尾花数据集,其中我们知道有多少种类的花,并且我们根据这个知识将我们的数据集划分为三个簇。但在无监督学习中,我们的主要任务是处理关于我们没有任何信息的数据,例如数据集中有多少自然簇或类别。此外,聚类也可以是一种探索性数据分析的形式,在这种情况下,你不会对数据有太多了解。有时,当数据有超过两个维度时,可视化变得困难,手动找出簇的数量也变得困难。那么,在这些情况下,我们如何找到簇的最佳数量呢?在本节中,我们将学习获取簇数量最佳值的技术。
聚类度量类型
在无监督学习中,确定簇的最佳数量有多种方法。以下是我们将在本章中研究的方法:
-
轮廓分数
-
肘部方法 / WSS
-
Gap 统计量
轮廓分数
轮廓分数或*均轮廓分数的计算用于量化聚类算法获得的簇的质量。让我们以簇 x 中的一个点 a 为例:
-
计算点 a 与簇 x 中所有点的*均距离(用 dxa 表示):
![图 1.24:计算点 a 与簇 x 中所有点的*均距离]()
图 1.24:计算点 a 与簇 x 中所有点的*均距离
-
计算点 a 与另一个簇中离 a 最*的簇中所有点的*均距离(dya)
![图 1.25:计算点 a 与簇 x 中所有*点的*均距离]()
图 1.25:计算点 a 与簇 x 中所有点的*均距离
-
通过将 步骤 1 的结果与 步骤 2 的结果之差除以 步骤 1 和 步骤 2 的最大值来计算该点的轮廓分数((dya-dxa)/max(dxa,dya))。
-
对簇中的所有点重复前三个步骤。
-
在得到聚类中每个点的轮廓得分后,所有这些得分的*均值是该聚类的轮廓得分:![图 1.26:计算轮廓得分
![图片]()
图 1.26:计算轮廓得分
-
对数据集中的所有聚类重复前面的步骤。
-
在得到数据集中所有聚类的轮廓得分后,所有这些得分的*均值是该数据集的轮廓得分:

图 1.27:计算*均轮廓得分
轮廓得分介于 1 和 -1 之间。如果一个聚类的轮廓得分低(介于 0 和 -1 之间),这意味着该聚类分布较广或该聚类中点的距离较高。如果一个聚类的轮廓得分高(接* 1),这意味着聚类定义良好,聚类中点的距离较低,而与其他聚类中点的距离较高。因此,理想的轮廓得分接* 1。
理解前面的算法对于形成对轮廓得分的理解很重要,但它对于学习如何实现它并不重要。因此,我们将学习如何使用一些预构建的库在 R 中进行轮廓分析。
练习 6:计算轮廓得分
在这个练习中,我们将学习如何计算具有固定聚类数量的数据集的轮廓得分:
-
将 Iris 数据集的前两列,即花瓣长度和花瓣宽度,放入
iris_data变量中:iris_data<-iris[,1:2] -
导入
cluster库以执行 k-means 聚类:library(cluster) -
将 k-means 聚类存储在
km.res变量中:km.res<-kmeans(iris_data,3) -
将所有数据点的成对距离矩阵存储在
pair_dis变量中:pair_dis<-daisy(iris_data) -
计算数据集中每个点的轮廓得分:
sc<-silhouette(km.res$cluster, pair_dis) -
绘制轮廓得分图:
plot(sc,col=1:8,border=NA)输出如下:

图 1.28:每个聚类中每个点的轮廓得分用一个单独的条形表示
前面的图显示了数据集的*均轮廓得分为 0.45。它还显示了按聚类和按点计算的*均轮廓得分。
在前面的练习中,我们计算了三个聚类的轮廓得分。但为了决定有多少个聚类,我们必须计算数据集中多个聚类的轮廓得分。在下一个练习中,我们将学习如何使用 R 中的 factoextra 库来完成这项工作。
练习 7:确定最佳聚类数量
在这个练习中,我们将通过在一行代码中使用 R 库来计算 k 的各种值,以确定最佳聚类数量:
-
将 Iris 数据集的前两列,即花瓣长度和花瓣宽度,放入
iris_data变量中:iris_data<-iris[,1:2] -
导入
factoextra库:library("factoextra") -
绘制轮廓分数与簇数量(最多 20 个)的图表:
fviz_nbclust(iris_data, kmeans, method = "silhouette",k.max=20)注意
在第二个参数中,你可以将 k-means 改为 k-medoids 或任何其他类型的聚类。
k.max变量是要计算的簇的最大数量。在函数的方法参数中,你可以输入要包含的三种聚类度量类型。所有这三种度量在本章中都有讨论。输出如下:
![图 1.29:簇数量与*均轮廓分数的关系]

图 1.29:簇数量与*均轮廓分数的关系
从前面的图表中,你选择一个具有最高分数的 k 值;即 2。根据轮廓分数,2 是最佳簇数量。
WSS/肘部方法
为了在数据集中识别簇,我们尝试最小化簇内点之间的距离,而内部*方和(WSS)方法正是衡量这一点。WSS 分数是簇内所有点距离*方的总和。在此方法中,我们执行以下步骤:
-
使用不同的 k 值计算簇。
-
对于每个 k 值,使用以下公式计算 WSS:
![图 1.30:计算 WSS 的公式,其中 p 是数据的总维度数]()
图 1.30:计算 WSS 的公式,其中 p 是数据的总维度数
此公式在此处展示:
![图 1.31:相对于两个点的距离,但 WSS 衡量的是相对于每个簇内所有点的所有距离的总和]()
图 1.31:WSS 分数的说明
注意
图 1.31 说明了相对于两个点的 WSS,但现实中 WSS 衡量的是相对于每个簇内所有点的所有距离的总和。
-
绘制簇数量 k 与 WSS 分数的关系图。
-
确定 WSS 分数不再显著下降的 k 值,并选择这个 k 作为理想的簇数量。这个点也被称为图表的“肘部”,因此得名“肘部方法”。
在接下来的练习中,我们将学习如何借助factoextra库来确定理想簇的数量。
练习 8:使用 WSS 确定簇的数量
在这个练习中,我们将看到如何使用 WSS 来确定簇的数量。执行以下步骤。
-
将 Iris 数据集的前两列,即花瓣长度和花瓣宽度,放入
iris_data变量中:iris_data<-iris[,1:2] -
导入
factoextra库:library("factoextra") -
绘制 WSS 与簇数量(最多 20 个)的图表:
fviz_nbclust(iris_data, kmeans, method = "wss", k.max=20)输出如下:
![图 1.32:WSS 与簇数量的关系]

图 1.32:WSS 与簇数量的关系
在前面的图表中,我们可以选择图表的肘部作为 k=3,因为当 k=3 后,WSS 的值开始下降得更慢。选择图表的肘部始终是一个主观的选择,有时你可能选择 k=4 或 k=2 而不是 k=3,但在这个图表中,很明显 k>5 的值不合适,因为它们不是图表的肘部,图表的斜率在这里急剧变化。
间隙统计
间隙统计是寻找数据集中最佳聚类数量的最有效方法之一。它适用于任何类型的聚类方法。间隙统计是通过比较在观测数据集上生成的聚类与在参考数据集上生成的聚类(其中没有明显的聚类)的 WSS 值来计算的。参考数据集是在我们想要计算间隙统计的观测数据集的最小值和最大值之间均匀分布的数据点。
因此,简而言之,间隙统计测量观测和随机数据集的 WSS 值,并找出观测数据集与随机数据集的偏差。为了找到理想的聚类数量,我们选择一个使间隙统计值最大的 k 值。这些偏差如何测量的数学细节超出了本书的范围。在下一项练习中,我们将学习如何使用 factoviz 库计算间隙统计。
这里是一个参考数据集:

图 1.33:参考数据集
以下为观测数据集:

图 1.34:观测数据集
练习 9:使用间隙统计计算理想的聚类数量
在这个练习中,我们将使用间隙统计计算理想的聚类数量:
-
将 Iris 数据集的前两列,即花瓣长度和花瓣宽度,放入
iris_data变量中,如下所示:iris_data<-iris[,1:2] -
按以下方式导入
factoextra库:library("factoextra") -
绘制间隙统计与聚类数量(最多 20)的图表:
fviz_nbclust(iris_data, kmeans, method = "gap_stat",k.max=20)

图 1.35:间隙统计与聚类数量
如前图所示,间隙统计的最高值对应于 k=3。因此,iris 数据集中的理想聚类数量是三个。三个也是数据集中的物种数量,这表明间隙统计使我们得出了正确的结论。
活动 4:寻找理想的市场细分数量
使用前述三种方法在批发客户数据集中找到最佳聚类数量:
注意
此数据集来自 UCI 机器学习仓库。您可以在archive.ics.uci.edu/ml/machine-learning-databases/00292/找到数据集。我们已经下载了文件并将其保存在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson01/Activity04/wholesale_customers_data.csv.
-
在一个变量中加载批发客户数据集的第 5 到 6 列。
-
使用轮廓分数计算 k-means 聚类的最佳聚类数量。
-
使用 WSS 分数计算 k-means 聚类的最佳聚类数量。
-
使用 Gap 统计量计算 k-means 聚类的最佳聚类数量。
结果将是三个图表,分别表示最佳聚类数量与轮廓分数、WSS 分数和 Gap 统计量。
注意
本活动的解决方案可以在第 208 页找到。
正如我们所看到的,每种方法都会给出最佳聚类数量的不同值。有时,结果可能没有意义,就像你在 Gap 统计量的例子中看到的那样,它给出的最佳聚类数量为一,这意味着不应该在这个数据集上进行聚类,所有数据点都应该在一个单独的聚类中。
给定聚类中的所有点将具有相似的性质。对这些性质的解读留给领域专家。在无监督学习中,几乎永远没有一个正确的答案来确定正确的聚类数量。
摘要
恭喜!你已经完成了这本书的第一章。如果你理解了我们至今所学的所有内容,你现在对无监督学习的了解比大多数声称了解数据科学的人都要多。k-means 聚类算法对无监督学习来说如此基础,以至于许多人把 k-means 聚类和无监督学习等同起来。
在本章中,你不仅学习了 k-means 聚类及其应用,还学习了 k-medoids 聚类,以及各种聚类指标及其应用。因此,现在你对 k-means 和 k-medoid 聚类算法有了顶级理解。
在下一章,我们将探讨一些不太为人所知的聚类算法及其应用。
第三章:第二章
高级聚类方法
学习目标
到本章结束时,您将能够:
-
执行 k-modes 聚类
-
实现 DBSCAN 聚类
-
执行层次聚类并在树状图中记录聚类
-
执行分裂和聚合聚类
在本章中,我们将探讨一些高级聚类方法以及如何在树状图中记录聚类。
简介
到目前为止,我们已经学习了无监督学习的一些最基本算法:k-means 聚类和 k-medoids 聚类。这些算法不仅对实际应用很重要,而且对于理解聚类本身也很重要。
在本章中,我们将研究一些其他高级聚类算法。我们不称它们为高级是因为它们难以理解,而是因为在使用它们之前,数据科学家应该了解为什么他们选择这些算法而不是我们在上一章中研究的一般聚类算法。k-means 是一种通用聚类算法,对于大多数情况都足够用,但在某些特殊情况下,根据数据类型,高级聚类算法可以产生更好的结果。
k-modes 聚类的简介
我们迄今为止研究过的所有聚类类型都是基于距离度量的。但如果我们得到一个数据集,其中无法以传统方式测量变量之间的距离,例如分类变量的情况,怎么办?在这种情况下,我们使用 k-modes 聚类。
k-modes 聚类是 k-means 聚类的扩展,处理的是众数而不是均值。k-modes 聚类的主要应用之一是分析调查结果等分类数据。
k-Modes 聚类的步骤
在统计学中,众数定义为最频繁出现的值。因此,对于 k-modes 聚类,我们将计算分类值的众数来选择中心。因此,执行 k-modes 聚类的步骤如下:
-
选择任意 k 个随机点作为聚类中心。
-
计算每个点到中心的汉明距离(在第一章,聚类方法简介中讨论)。
-
根据汉明距离将每个点分配到最*的中心所在的聚类。
-
通过找到该聚类中所有数据点的众数来在每个聚类中选择新的聚类中心。
-
从 步骤 2 重复此操作,直到聚类中心不再变化。
您可能已经注意到,这些步骤与 k-means 聚类的步骤非常相似。这里只是改变了距离度量的类型。所以,如果您理解了 k-means 聚类,那么理解 k-modes 聚类也会很容易。
练习 10:实现 k-modes 聚类
注意
对于所有需要导入外部 CSV 或图像的练习和活动,请转到 RStudio-> 会话-> 设置工作目录-> 到源文件位置。您可以在控制台中看到路径已自动设置。
此练习的数据可以从这里下载:github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson02/Exercise10/breast_cancer.csv。这是一个包含九个变量的分类数据集,其中一些是分类的,一些是名义的,描述了不同的乳腺癌病例。在将数据保存到名为breast_cancer.csv的文件中后,我们将执行以下操作:
注意
此数据集来自 UCI 机器学习仓库。您可以在archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer/breast-cancer.data找到数据集。这个乳腺癌领域的数据来自南斯拉夫卢布尔雅那大学医学院,肿瘤研究所。感谢 M. Zwitter 和 M. Soklic 提供数据。我们已经下载了文件,并将其保存在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson02/Exercise10/breast_cancer.csv。
-
读取数据集并将其存储到一个变量中:
bc_data<-read.csv('breast_cancer.csv',header = FALSE) -
将从第二列到最后一列的所有列存储到一个新变量中:
k_bc_data<-bc_data[,2:10] -
查看变量
k_bc_data的前六行:head(k_bc_data)输出包含六个行,其中包含描述患者、症状和治疗的各个属性值:
V2 V3 V4 V5 V6 V7 V8 V9 V10 1 30-39 premeno 30-34 0-2 no 3 left left_low no 2 40-49 premeno 20-24 0-2 no 2 right right_up no 3 40-49 premeno 20-24 0-2 no 2 left left_low no 4 60-69 ge40 15-19 0-2 no 2 right left_up no 5 40-49 premeno 0-4 0-2 no 2 right right_low no 6 60-69 ge40 15-19 0-2 no 2 left left_low no -
导入
klaR库,它包含kmodes函数。klaR是一个 R 库,用于分类和可视化:install.packages("klaR") library(klaR) -
预测并存储最终的集群中心到一个变量中。在这一步,我们输入数据集和集群数量(即
k和找到集群数量的最大迭代次数):k.centers<-kmodes(k_bc_data,2,iter.max = 100) -
查看集群中心:
k.centers输出如下:

图 2.1:集群中心的截图
聚类算法将所有乳腺癌病例分为两个集群,每个集群包含彼此相似的病例。在输出中,有两个主要部分:集群模式和聚类向量。集群模式部分告诉我们集群 1 和集群 2 的中心或坐标。下面,聚类向量包含索引序列中每个数据点的集群编号。
注意
由于中心的随机起始位置,每次运行算法都可能得到不同的结果。
由于这是一个多维分类数据集,除了将数据打印到 R 控制台之外,没有简单的方法来可视化结果。
活动 5:在蘑菇数据集上实现 k-modes 聚类
注意
此数据集来自 UCI 机器学习仓库。您可以在 archive.ics.uci.edu/ml/datasets/Mushroom 找到数据集。我们已经下载了文件,清理了数据,并将其保存在 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson02/Activity05。
在本活动中,我们将对蘑菇数据集执行 k-modes 聚类。此数据集列出了 23 种不同蘑菇的属性。每种蘑菇被分类为可食用(e)或有毒(p)。我们将看到无监督学习如何通过将数据分组到两个簇中来对有毒和可食用蘑菇进行分类。以下步骤将帮助您完成活动:
-
从
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson02/Activity05下载mushrooms.csv。 -
将
mushrooms.csv文件读入一个变量中。 -
导入
klaR库。 -
根据 k-modes 聚类计算簇。
-
通过形成数据标签与分配的簇之间的矩阵来检查聚类的结果。
注意
本活动的解决方案可以在第 212 页找到。
输出将是一个真实标签和簇标签的表格,如下所示:
1 2
e 80 4128
p 3052 864
密度聚类(DBSCAN)简介
基于密度的聚类或 DBSCAN 是聚类中最直观的形式之一。使用这种类型的聚类很容易找到数据中的自然簇和异常值。此外,它不需要你定义簇的数量。例如,考虑以下图示:

图 2.2:一个示例散点图
此数据集中有四个自然簇和一些异常值。因此,DBSCAN 将分离簇和异常值,如图所示,而无需您指定数据集中要识别的簇的数量:

图 2.3:由 DBSCAN 分类出的簇和异常值
因此,DBSCAN 可以在散点图中找到由低密度区域分隔的高密度区域。
DBSCAN 步骤
如前所述,DBSCAN 不需要您选择簇的数量,但您必须选择其他两个参数来执行 DBSCAN。第一个参数通常用 ε(epsilon)表示,表示同一簇中两点之间的最大距离。另一个参数是簇中的最小点数,通常用 minPts 表示。现在我们将逐步查看 DBSCAN 聚类算法:
-
在数据集中选择任何点,
R。 -
在距离点
R的 epsilon 范围内找到所有点。 -
如果从点
R出发,距离 epsilon 内的点的总数大于minPts,那么它是一个簇,R是核心点。 -
如果从点 p 出发,距离 epsilon 内的点的总数小于
minPts,那么距离 epsilon 内的所有点将被归类为噪声。然后,我们选择一个新的点,R,它既没有被归类为噪声,也没有被归类为簇的一部分,从步骤 2 重新开始这个过程。 -
对簇中的其他点重复此过程,以找到距离 epsilon 内不在簇中的点。这些新点也将被归类到同一个簇中。
-
当对簇中的所有点执行了这些步骤后,通过选择一个新的随机点,
R,它既没有被归类到簇中,也没有被归类为噪声,重复相同的过程。
因此,为了说明前面的算法,让我们以 epsilon 为x和簇中的最小点数为 4 为例。看看下面的图:

图 2.4:只有 2 个点位于点 R1 的 x 距离内
只有三个点位于点R1的x距离范围内,而我们对于x半径内最小点数的阈值是四个。因此,这四个点将被归类为异常值或噪声。但如果再有一个点,R5,位于R1和R4之间,所有这四个点将属于一个簇,如下面的图所示:

图 2.5:这四个点都属于一个簇
在前面的图中,点R1和R5是核心点,因为它们各自有四个点在x距离内。而点R4、R2和R3不是核心点,如下面的图所示:

图 2.6:核心点与非核心点
任何位于这些圆圈之外的点将被归类为噪声点,如下面的图中的R6所示:

图 2.7:噪声点 R6
练习 11:实现 DBSCAN
在这个练习中,我们将查看使用 Iris 数据集实现 DBSCAN 的过程。为了执行 DBSCAN,我们将执行以下步骤:
-
将 Iris 数据集的前两列存储在
iris_data中:iris_data<-iris[,1:2] -
导入
dbscan库,它包含各种 DBSCAN 算法的实现:install.packages("dbscan") library(dbscan) -
计算并存储簇在
clus变量中。在这个步骤中,你还需要选择 epsilon 和minPts的值:clus<-dbscan(iris_data,eps=.3,minPts = 5)注意
你可以尝试调整 epsilon 的值。为了得到期望的输出,我们将其设置为 0.3。
-
导入
factoextra库以可视化簇:install.packages("factoextra") #use it only the first time if library is not installed already library(factoextra) -
绘制簇中心。您需要将存储 DBSCAN 结果的变量作为
plot函数的第一个参数输入。作为第二个参数,您需要输入一个数据集。在我们的例子中,它是iris_data。函数中的geom变量用于定义图形的几何形状。我们只会使用point。现在,ggtheme用于选择图形的主题。palette用于选择点的几何形状。将ellipse值设置为 false,这样函数就不会绘制簇的轮廓。fviz_cluster(clus,data=iris_data,geom = "point",palette="set2",ggtheme=theme_minimal(),ellipse=FALSE)这是输出结果:

图 2.8:不同颜色的 DBSCAN 簇
在图 2.8 中,有三个簇,分别是橙色、蓝色和绿色。黑色点是噪声或异常值。这里的橙色点属于一个物种,而绿色点属于两个不同的物种。
注意
您可以快速比较这张散点图与第一章,聚类方法简介中的图 1.18。
DBSCAN 与我们迄今为止研究过的所有聚类方法都不同,并且在科学文献中是最常见和最常引用的聚类方法之一。DBSCAN 相对于其他聚类方法有几个优点:
-
您最初不需要选择簇的数量。
-
它每次运行时不会产生不同的结果,这与 k-means 或其他“k 型”聚类方法不同,其中起始点可能会影响最终结果。因此,结果是可重复的。
-
它可以在数据中发现任何形状的簇。
但 DBSCAN 也有一些缺点:
-
确定正确的参数集,即 epsilon 和
minPts,对于数据的适当聚类来说很难确定。 -
DBSCAN 无法根据密度区分簇。如果一个数据集的密度变化很大,它可能表现不佳。
DBSCAN 的应用
作为最常引用的聚类方法之一,DBSCAN 有许多实际应用。DBSCAN 聚类的一些实际应用包括以下内容:
-
DBSCAN 可以在城市规划中以多种方式使用。例如,给定关于犯罪事件位置的数据,DBSCAN 可以用来识别城市中的犯罪多发区。这些数据可以用来规划警察力量的部署,甚至调查潜在的帮派活动。
-
DBSCAN 可以用来在板球和篮球等游戏中制定策略。给定板球场地上的投球数据,您可以识别击球手和投球手的优缺点。或者如果我们有关于击球手击球的数据,从 DBSCAN 中获得的数据可以用来相应地调整防守。
-
在自然灾害或病毒疾病的传播期间,可以使用 DBSCAN 识别受影响严重的地区。
活动 6:实现 DBSCAN 并可视化结果
在这个活动中,我们将执行 DBSCAN 并与 k-means 聚类进行比较。为此,我们将使用包含表示不同形状的模拟数据的multishapes数据集。
这些步骤将帮助您完成活动:
-
在
factoextra库中生成并存储multishapes数据集。 -
绘制
multishapes数据集的前两列。 -
在数据集上执行 k-means 聚类并可视化。
-
在数据集上执行 DBSCAN 并可视化数据,以比较与 k-means 聚类的结果。
注意
这个活动的解决方案可以在第 213 页找到。
DBSCAN 聚类的绘图将如下所示:

图 2.9:多形状数据集上 DBCAN 的预期绘图
在这里,所有黑色的点都是异常值,并且没有被归类到任何聚类中,而 DBSCAN 中形成的聚类无法用其他任何类型的聚类方法获得。这些聚类具有多种形状和大小,而 k-means 中所有聚类都是*似球形的。
层次聚类简介
我们将要研究的最后一种聚类类型是层次聚类。层次被定义为“一个将人或事物放置在一系列不同重要性或地位的层级系统。”层次聚类是按顺序合并聚类。这个合并聚类的序列被称为层次。我们可以通过一个名为树状图的层次聚类算法的输出更清楚地看到这一点。
层次聚类有两种类型:
-
聚类
-
分裂
由于两种类型的层次聚类都很相似,因此一起研究它们是有意义的。
聚类是层次聚类的自下而上的方法。在这个方法中,每个数据点最初被假定为单个聚类。从那里开始,我们根据相似度或距离度量开始合并最相似的聚类,直到所有数据点合并成一个单一的聚类。
在分裂聚类中,我们做的是完全相反的事情。它是层次聚类的自上而下方法。在这个方法中,所有数据点最初被假定为在一个单一的聚类中。从那里开始,我们开始将聚类分割成多个聚类,直到每个数据点都是一个单独的聚类。两种聚类类型之间的差异和相似性将在后续章节中变得清晰。但首先,我们应该尝试理解为什么我们需要这种其他类型的聚类以及它所服务的特殊目的,这是其他类型的聚类所不具备的。层次聚类主要用于以下原因:
-
就像 DBSCAN 一样,我们最初不需要选择聚类的数量。
-
层次聚类的最终输出,树状图,可以帮助我们以可视化的方式理解聚类结果,这意味着我们不需要重新运行算法来查看结果中不同数量的簇。
-
与 k-means 不同,层次聚类可以使用任何类型的距离度量。
-
它可以找到复杂形状的簇,与像 k-means 这样的其他聚类算法不同,后者只能找到*似球形的簇。
所有这些先前因素的结合使层次聚类成为无监督学习中的一个重要聚类方法。
相似性度量的类型
如前所述,层次聚类是一种自下而上的层次聚类方法。我们根据相似性度量逐个合并最相似的簇。这个相似性度量可以从几种不同类型中选择:
- 单链:在单链相似度中,我们测量两个簇中两个最相似点之间的距离或相似度:

图 2.10:单链度量的演示
- 完全连接:在这种度量中,我们测量簇中两个最远点之间的距离或相似度:

图 2.11:完全连接度量的演示
- 组*均:在这个度量中,我们测量一个簇中所有成员与第二个簇中任何成员之间的*均距离:

图 2.12:组*均度量的演示
注意
在这些相似性度量中,不测量同一簇成员之间的距离。
- 质心相似度:在这种相似度中,两个簇之间的相似度定义为两个簇质心之间的相似度:

图 2.13:质心相似度的演示
执行层次聚类的方法步骤
通过了解这些相似性度量,我们现在可以理解执行层次聚类算法的方法:
-
将每个点初始化为一个单独的簇。
-
计算每对簇之间的相似性度量。相似性度量可以是之前提到的四种度量中的任何一种。
-
根据第 2 步中选择的相似性度量,合并两个最相似的簇。
-
从第 2 步开始重复这个过程,直到只剩下一个簇为止。
整个过程将产生一个称为树状图的图。这个图记录了每个步骤形成的簇。一个简单的、元素非常少的树状图看起来如下:

图 2.14:一个示例树状图
在前面的树状图中,假设点 A 和点 B 是我们在使用的相似性度量中所有点上最接*的两个点。它们的接*度被用来确定连接线的长度,在点 A 和点 B 的情况下,这个长度是 L1。因此,点 A 和点 B 首先被聚类。之后,在 L2,点 D 和点 E 被聚类,然后,在 L3,点 A、B 和 C 被聚类。此时,我们有两个集群在 L4 处连接形成一个集群。
现在,为了从这个树状图中得到集群,我们进行水*切割。例如,如果我们将在 L4 和 L3 之间进行切割,我们将得到两个集群:
-
集群 1 – A、B 和 C
-
集群 2 – D 和 E
集群将看起来如下:

图 2.15:树状图中表示的集群
同样地,如果我们将在树状图中的 L3 和 L2 之间进行水*切割,我们将得到三个集群:
-
集群 1 – A 和 B
-
集群 2 – C
-
集群 3 – D 和 E
集群将看起来如下:

图 2.16:树状图中集群的表示
因此,为了得到不同数量的集群,我们不需要重新运行整个过程。使用这种方法,我们可以得到数据中可能出现的任何数量的集群,而无需再次执行整个过程。
练习 12:使用不同相似性度量的聚合聚类
在这个练习中,我们将使用不同的相似性度量进行聚合层次聚类,并比较结果:
-
让我们将
iris_flowers数据集的最后三列,即花瓣长度、花瓣宽度和物种,输入到iris_data变量中,如下所示:iris_data<-iris[,3:5] install.packages('cluster') library(cluster) -
在这一步,我们使用
hclust函数来获取层次聚类。在hclust函数中,我们需要输入所有点之间的成对距离,这可以通过dist()函数实现。第二个参数method用于定义层次聚类的相似性度量:h.clus<-hclust(dist(iris_data),method="complete") -
现在,让我们将层次聚类的结果绘制成树状图,如下所示:
plot(h.clus)输出如下:
![图 2.17:由完整相似性度量得到的树状图]()
图 2.17:由完整相似性度量得到的树状图
-
要从前面的树状图中选择多个集群,我们可以使用一个名为
cutree的 R 函数。我们将hclust的结果以及集群数量输入到这个函数中:clusterCut <- cutree(h.clus, 3) table(clusterCut, iris_data$Species)输出表格如下:
![img/C12628_02_18.jpg]
图 2.18:显示集群分布的表格
使用这种聚类方法,setosa 和 virginica 物种被准确分类。
-
使用
single作为相似性度量,如下所示:h.clus<-hclust(dist(iris_data),method = "single") plot(h.clus)输出如下:
![图 2.19:由单相似性度量得到的树状图]()
图 2.19:基于单相似度指标生成的树状图
注意这个树状图与使用
完全相似度指标创建的树状图的不同之处。 -
将此数据集划分为三个簇:
clusterCut <- cutree(h.clus, 3) table(clusterCut, iris_data$Species)输出如下:
![图 2.20:显示簇分布的表格]()
图 2.20:显示簇分布的表格
在这里,我们的聚类方法成功地将一个类别从其他两个类别中分离出来。
-
现在让我们使用
*均相似度指标执行层次聚类:h.clus<-hclust(dist(iris_data),method = "average") plot(h.clus)输出如下:
![图 2.21:基于*均相似度指标生成的树状图]()
图 2.21:基于*均相似度指标生成的树状图
-
让我们将前面的树状图再次划分为三个簇,并查看结果:
clusterCut <- cutree(h.clus, 3) table(clusterCut, iris_data$Species)输出如下:
![图 2.22:显示簇分布的表格]()
图 2.22:显示簇分布的表格
在这里,使用
*均相似度指标,我们得到了几乎完全正确的分类结果。 -
现在,让我们尝试使用最后一个相似度指标,
质心,创建一个树状图:h.clus<-hclust(dist(iris_data),method = "centroid") plot(h.clus)输出如下:
![图 2.23:基于质心相似度指标生成的树状图]()
图 2.23:基于质心相似度指标生成的树状图
-
现在,让我们将前面的树状图划分为三个簇,并查看结果:
clusterCut <- cutree(h.clus, 3) table(clusterCut, iris_data$Species)输出如下:

图 2.24:显示簇分布的表格
虽然基于*均和质心相似度指标的簇的树状图看起来不同,但当我们把树状图切割成三个簇时,每个簇中的元素数量是相同的。
划分聚类
划分聚类与聚合聚类相反。在聚合聚类中,我们以每个点作为其自己的簇开始,而在划分聚类中,我们以整个数据集作为一个簇开始,然后开始将其划分为更多的簇:

图 2.25:聚合聚类和划分聚类的表示
因此,划分聚类过程可以总结如下一个图示:

图 2.26:划分聚类过程的表示
执行划分聚类的步骤
从步骤 1 到步骤 6,划分聚类过程不断将点划分为更小的簇,直到每个点都是一个单独的簇。图 2.26 显示了划分聚类过程的前 6 步,但要正确运行完整过程,我们需要执行与数据集中点数相同数量的步骤。
因此,对于划分聚类,我们将使用 DIANA 算法——DIANA 代表划分分析。为此,我们需要执行以下步骤:
-
从数据集中所有点都在一个单独的簇中开始。
-
根据您喜欢的任何距离度量,从数据集中所有可能的簇中选择两个最不相似的簇。
-
重复步骤 2,直到数据集中的所有点都各自聚类。
我们将使用 R 中的 cluster 库来执行 DIANA 聚类。
练习 13:执行 DIANA 聚类
在这个练习中,我们将执行 DIANA 聚类:
-
将花瓣长度、花瓣宽度和物种名称放入
iris_data变量中:iris_data<-iris[,3:5] -
导入
cluster库:install.packages("cluster") library("cluster") -
将
iris_data数据集和用于度量差异度的度量传递给diana()函数:h.clus<-diana(iris_data, metric="euclidean") -
使用
pltree()函数绘制树状图。要绘制树状图,需要将diana()函数的结果和图表标题传递给pltree()函数:pltree(h.clus, cex = 0.6, main = "Dendrogram of divisive clustering")划分聚类的树状图如下所示:
![图 2.27:划分聚类的树状图]()
图 2.27:划分聚类的树状图
-
如果我们将前面的树状图划分为三个簇,我们会看到这种聚类方法也能够独立地识别不同的花卉种类:
clusterCut <- cutree(h.clus, 3) table(clusterCut, iris_data$Species)输出如下:
clusterCut setosa versicolor virginica 1 50 1 0 2 0 49 0 3 0 0 50
在前面的输出中,只有一朵花被错误地分类到另一个类别。这是我们在本书中遇到的所有聚类算法在不知道任何关于花卉信息的情况下对花卉物种进行分类的最佳性能。
活动 7:对种子数据集执行层次聚类分析
在这个活动中,我们将对种子数据集执行层次聚类分析。我们将看到当对三种种子进行分类时,聚类的结果是什么。
注意
此数据集来自 UCI 机器学习仓库。您可以在 archive.ics.uci.edu/ml/machine-learning-databases/00236/ 找到数据集。我们已经下载了文件,清理了它,并将其保存在 github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson02/Activity07/seeds_data.txt。
这些步骤将帮助您完成活动:
-
对数据集执行层次聚类并绘制树状图。
-
在
k=3处进行切割,并通过创建一个包含原始标签的表格来检查聚类的结果。 -
对数据集进行划分聚类并绘制树状图。
-
在
k=3处进行切割,并通过形成一个带有原始标签的表格来检查聚类的结果。注意
本活动的解决方案可以在第 215 页找到。
本活动的输出将是一个表格,显示聚类结果在分类三种类型种子方面的表现。它将如下所示:

图 2.28:预期表格,用于分类三种类型的种子
摘要
恭喜您完成了关于聚类技术的第二章!至此,我们已经涵盖了所有主要的聚类技术,包括 k-modes、DBSCAN 以及两种类型的层次聚类,我们还探讨了它们之间的联系。我们可以将这些技术应用到我们可能遇到的任何类型的数据集中。这些新方法有时在第一章中使用的数据集上也产生了更好的结果。在下一章中,我们将研究概率分布及其在探索性数据分析中的应用。
第四章:第三章
概率分布
学习目标
到本章结束时,你将能够:
-
在 R 中生成不同的分布
-
在 R 中估计新数据集的概率分布函数
-
比较同一分布的两个不同样本或不同分布的接*程度
在本章中,我们将学习如何使用概率分布作为无监督学习的一种形式。
简介
在本章中,我们将研究无监督学习的另一个方面,称为概率分布。概率分布是许多数学教科书和课程中涵盖的经典统计学的一部分。随着大数据的出现,我们已经开始在探索性数据分析和其他建模应用中使用概率分布。因此,在本章中,我们将研究如何在无监督学习中使用概率分布。
概率分布的基本术语
统计学中有两种方法家族:参数方法和非参数方法。非参数方法旨在处理可能具有任何形状的数据。相比之下,参数方法对数据可能采取的特定形状做出假设。这些假设通常编码为参数。以下是你应该注意的两个主要参数:
-
均值:这是分布中所有值的*均值。
-
标准差:这是衡量分布均值周围值分布的度量。
统计学中的大多数参数方法以某种方式依赖于这两个参数。在本章中我们将研究的参数分布是这些:
-
均匀分布
-
正态分布
-
对数正态分布。
-
二项分布
-
泊松分布
-
帕累托分布
均匀分布
在均匀分布中,一个区间内的所有值,比如说 [a,b],都是等可能的。数学上,它被定义为如下:

图 3.1:均匀分布的数学公式
它可以如下绘制在图上:

图 3.2:均匀分布图
均匀分布是参数分布中最简单的。现实世界中许多过程遵循均匀抽样:
-
如果在一个非常大的区域内下雨,雨滴落下的分布可以假设在较小区域内是均匀的。
-
社会保障号码的最后一位应该对任何人群子集,例如加利福尼亚州所有初创公司首席执行官,都遵循均匀分布。
-
均匀随机抽样对于生成实验数据和进行控制试验非常重要。
练习 14:在 R 中生成和绘制均匀样本
在这个练习中,我们将生成均匀样本并绘制它们。为此,执行以下步骤:
-
使用内置的
runifR 函数生成均匀样本。首先,输入你想要生成的样本数量。这里我们生成 1,000 个样本。然后,输入min值和max值:uni<-runif(1000, min=0, max=100) -
在将生成的随机数存储在
uni变量之后,我们将绘制它们的值与它们的索引的关系:plot(uni)输出如下:
![图 3.3:均匀分布]()
图 3.3:均匀分布
如您所见,点几乎均匀地散布在各个地方。我们也可以绘制这个直方图,以获得分布的更清晰图像。
-
我们将使用 R 的
hist()函数绘制生成的样本的直方图:hist(uni)输出如下:

图 3.4:分布的直方图
如您所见,它并不完全*坦,正如我们之前所设想的那样。它或多或少是均匀的,但并不完全均匀,因为它是由随机生成的。每次我们生成一个新的样本,它都会类似于这个直方图,而且很可能会不完全*坦,因为所有随机抽样方法都会伴随噪声。
正态分布
正态分布是一种受两个参数控制的参数分布:均值和均值的方差。它关于均值对称,大多数值都接*均值。其曲线也被称为钟形曲线:

图 3.5:正态分布数据的典型*似表示
正态分布由以下方程定义:

图 3.6:正态分布的方程
这里,
是均值,
是标准差。
正态分布是现实世界中非常常见的一种分布。以下是一些正态分布的例子:
-
篮球运动员的身高大约呈正态分布。
-
一个班级学生的分数可能有一个非常接*正态分布的分布。
-
尼罗河的年流量是正态分布的。
现在,我们将使用 R 生成并绘制一个正态分布。
练习 15:在 R 中生成和绘制正态分布
在这个练习中,我们将生成一个正态分布来模拟 1,000 名学生的(满分 100 分)考试成绩,并将它们绘制出来。为此,请执行以下步骤:
-
通过在 R 的
rnorm函数中输入样本数量、均值和标准差来生成正态分布:nor<-rnorm(1000,mean=50, sd= 15) -
将生成的数字与它们的索引进行对比:
plot(nor)输出如下:
![图 3.7:正态分布]()
图 3.7:正态分布
如您所见,大多数值都围绕着 50 的*均值,当我们远离 50 时,点的数量开始减少。这个分布将在下一步通过直方图来阐明。
-
使用
hist()函数绘制正态分布的直方图:hist(nor)输出如下:

图 3.8:正态分布直方图
您可以看到这个形状非常类似于正态分布的钟形曲线。
偏度和峰度
正如我们所见,您在实践中所看到的许多分布都被假定为正态分布。但并非每个分布都是正态分布。为了衡量一个分布偏离标准正态分布的程度,我们使用两个参数:
-
偏度
-
峰度
分布的偏度是衡量其相对于标准正态分布的不对称程度的指标。在具有高偏度的数据集中,均值和众数将彼此不同。偏度有两种类型:正偏度和负偏度:

图 3.9:负偏度和正偏度
负偏度是指*均值左侧存在长尾值,而正偏度是指*均值右侧存在长尾值。偏度也可以用以下公式表示:

图 3.10:偏度的数学公式
这里,
是
的期望值或*均值,其中
和
分别是分布的均值和标准差。
峰度是衡量分布尾部的胖瘦程度与正态分布相比的指标。与偏度不同,峰度不会在分布中引入不对称性。这里提供了一个说明:

图 3.11:峰度演示
峰度也可以用以下公式表示:

图 3.12:峰度的数学公式
这里,
是
的期望值或*均值,其中
和
分别是分布的均值和标准差。标准正态分布的偏度为 0,峰度测量值为 3。因为正态分布非常常见,所以我们有时只测量超峰度,如下所示:
Kexcess = K - 3
因此,正态分布的超峰度为 0。
对数正态分布
对数正态分布是值的对数呈正态分布的分布。如果我们在对数尺度上展示对数正态分布,它与正态分布完全相同,但如果我们在标准分布尺度上展示它,它将具有非常高的正偏斜:

图 3.13:对数正态分布
为了说明对数正态分布是按对数尺度分布的正态分布,我们将进行一个练习。
对数正态分布在金融风险管理领域用于模拟股价。由于假设增长因子是正态分布的,股价可以按对数正态分布建模。这种分布也用于与期权定价相关的计算,包括风险价值(VaR)。
练习 16:从正态分布生成对数正态分布
在这个练习中,我们将从正态分布生成对数正态分布。为此,执行以下步骤:
-
生成正态分布并将值存储在一个变量中:
nor<-rnorm(1000,mean=5, sd= 1) -
绘制具有 100 个不同区间的正态分布直方图:
hist(nor,breaks = 100)输出如下:
![图 3.14:均值为 5,标准差为 1 的正态分布]()
图 3.14:均值为 5,标准差为 1 的正态分布
-
创建一个将存储 1,000 个值的对数正态分布向量:
lnor <- vector("list", 1000) -
将指数值输入到
lnor向量中。指数函数是对数函数的逆函数:for (x in 1:1000){ lnor[x]=exp(nor[x]) } -
绘制
lnor的直方图:hist(as.integer(lnor), breaks = 200)输出如下:

图 3.15:对数正态分布
注意前一个图看起来像对数正态分布图,并且这个图是从正态分布生成的。如果我们对前一个图中的值取对数后绘制新的图形,那么它将再次是正态分布。
二项分布
二项分布是一种离散分布,与连续的正态分布或均匀分布不同。二项分布用于模拟每个事件有两种可能性的多个事件同时发生的概率。一个二项分布可以应用的例子是在同时掷三个硬币时,找出所有三个硬币都出现正面的概率。
二项分布的均值和方差分别是 np 和 np(1-p),其中 p 是成功的概率,n 是试验次数。当 p=0.5 时,二项分布是对称的。当 p 小于 0.5 时,它更偏向右侧,当 p 大于 0.5 时,它更偏向左侧。
二项分布的公式如下:
P(x) = n!/((n-x)!x!)*(p^x)*((1-p)^x)
在这里,n 是总试验次数,x 是焦点试验次数,p 是成功的概率。
练习 17:生成二项分布
在这个练习中,我们将生成一个二项分布来模拟抛掷硬币 50 次时出现正面的次数。为此,执行以下步骤:
-
要生成二项分布,我们首先需要一个由 50 个数字组成的序列作为索引,这将作为我们想要模拟的成功次数。这将是前一小节公式中的 x:
s <- seq(0,50,by = 1) -
现在我们将把
s作为参数传递给 R 中的dbinom()函数,该函数将计算s变量中每个值的概率并将它们存储在新的probs变量中。首先,在函数中,我们输入将编码成功次数范围的序列。然后,我们输入序列的长度,然后输入成功的概率:probs <- dbinom(s,50,0.5) -
在最后一步,我们将
s和probs一起绘制:plot(s,probs)输出如下:

图 3.16:二项分布
在这里,x 轴显示我们感兴趣的正面数量,y 轴显示在 50 次试验中恰好得到该数量正面的概率。当我们抛掷硬币 50 次时,最可能的结果是我们将得到 25 个正面和 25 个反面,但得到所有 50 个正面或反面的概率非常低。这在前面的图表中解释得很好。
泊松分布
泊松分布是另一种离散分布,用于在特定时间段内根据该事件在该时间段内*均发生次数来模拟事件的发生。
它由以下方程式表示:

图 3.17:泊松分布公式
在这里,lambda 是给定时间段的*均发生次数,e是欧拉常数,x是你想要找到概率的事件数量。根据到目前为止观察到的每分钟到达事件的新人数,泊松分布可以用来计算在下一分钟到达该事件的人数概率。
泊松分布图看起来是这样的:

图 3.18:泊松分布图
在这里,我们可以看到x的不同值的概率与 lambda 有关。
帕累托分布
帕累托分布是一种基于指数的概率分布。这种分布是为了模拟在许多现实世界情况中观察到的 80:20 规则而发明的。遵循 80:20 规则的一些有趣的情况如下列所示:
-
大约 80%的世界财富掌握在 20%的人手中。
-
在商业管理中,发现对于大多数公司来说,80%的收入是由 20%的客户产生的。
-
据说 20%的司机造成了 80%的所有事故。
有许多其他现实世界的观察可以通过帕累托分布来建模。帕累托分布的数学公式如下:

图 3.19:帕累托分布的数学公式
核密度估计简介
到目前为止,我们本章已经研究了参数分布,但在现实生活中,所有分布要么是参数分布的*似,要么根本不类似于任何参数分布。在这种情况下,我们使用一种称为核密度估计(KDE)的技术来估计它们的概率分布。
核密度估计(KDE)使用核来估计具有给定有限点的分布或随机变量的概率密度函数。在继续本章内容后,这对你来说会更加清晰。
核密度估计算法
尽管名字听起来很复杂,但核密度估计(KDE)是一个非常简单的两步过程:
-
选择核
-
将核放在数据点上并取核的总和
核是一个非负对称函数,用于建模分布。例如,在核密度估计(KDE)中,正态分布函数是最常用的核函数。核函数可以是不同类型的。它们与我们本章 earlier 研究的分布有很大关系。这里总结了其中一些类型:
- 在均匀核中,一个范围内的所有值都被赋予相同的权重。这表示如下:

图 3.20:均匀核函数的表示
- 在三角形核中,随着值向范围中间移动,权重线性增加。这表示如下:

图 3.21:三角形核函数的表示
- 在高斯核中,权重按正态分布。这表示如下:

图 3.22:高斯核函数的表示
除了核之外,在第一步中,我们还需要选择另一个参数,称为核的带宽。带宽是影响核*滑度的参数。选择正确的带宽非常重要,甚至比选择正确的核更重要。这里我们将看一个例子。
练习 18:可视化和理解核密度估计
假设我们有一个包含五个不同点(1,2,3,4 和 5)的分布。让我们使用这个例子来可视化和理解核密度估计(KDE):
-
将五个点的向量存储在一个变量中:
x<- c(1,2,3,4,5) -
绘制点:
y<-c(0,0,0,0,0) plot(x,y)输出如下:
![图 3.23:五个点的图]()
图 3.23:五个点的图
-
如果您还没有安装
kdensity包,请安装它,并导入它:install.packages("kdensity") library('kdensity') -
使用
kdensity()函数计算核密度。输入分布x和带宽参数为.35。默认情况下核是高斯核:dist <- kdensity(x, bw=.35) -
按以下方式绘制核密度估计图(KDE):
plot(dist)输出如下:
![图 3.24:高斯核的绘制图]()
图 3.24:高斯核的绘制图
这是核密度估计图的最终输出。在下一步中,假设每个点(1, 2, 3, 4, 和 5)都有一个以高斯核为中心的点,并将它们相加得到这个图。以下图将使它更清晰:
注意
此图用于说明目的,而不是在 R 中生成。
![图 3.25:每个点上的高斯核绘制图]()
图 3.25:每个点上的高斯核绘制图
如前图所示,每个点上都绘制了一个高斯核,然后所有核相加得到最终的曲线。
现在,如果我们把带宽改为 0.5 而不是 0.35 会怎样呢?
-
在
kdensity()函数中将带宽改为 0.5,并再次绘制kdensity图:dist <- kdensity(x, bw=.5) plot(dist)输出如下:

图 3.26:带宽为 0.5 的高斯核绘制图
你可以看到核现在要*滑得多。以下使用了以下核:

图 3.27:每个点上的高斯核绘制图
注意
此图用于说明目的,而不是在 R 中生成。
这次,核的宽度要宽得多。
如果我们被给予足够多的点来进行估计,核的选择不会像带宽参数的选择那样显著改变最终核密度估计图的形状。因此,选择理想的带宽参数是一个重要的步骤。有许多技术被用来选择理想的带宽参数。研究这些技术超出了本书的范围,但 R 库可以自动选择理想的参数。我们将在下一个练习中学习这一点。
练习 19:研究改变核对分布的影响
在这个练习中,我们将生成两个具有不同标准差和均值的正态分布,并将它们合并以生成它们的合并核密度估计图:
-
生成两个不同的正态分布并将它们存储在两个变量中:
y1 <- rnorm(100,mean = 0, sd = 1) y2<-rnorm(100, mean = 3, sd=.2) -
将生成的分布合并并绘制:
y3<-c(y1,y2) plot(y3)输出如下:
![图 3.28:合并分布的绘制图]()
图 3.28:合并分布的绘制图
你可以看到有两个不同的分布,它们有不同的均值和分散度(标准差)。
-
绘制
y3的直方图以供参考:hist(y3)输出如下:
![图 3.29:结果分布的直方图]()
图 3.29:结果分布的直方图
-
使用
gaussian核生成并绘制y3的核密度估计图:dist<-kdensity(y3,kernel = "gaussian") plot(dist)输出如下:
![图 3.30:高斯核密度图]()
图 3.30:高斯核密度图
在前面的图中,我们使用了高斯核,带宽是由函数自动选择的。在这个分布中,我们有 200 个点,这应该足以生成一个稳健的 KDE 图,这样改变核类型不会在最终的 KDE 图中产生显著差异。在下一步,让我们尝试改变核并查看最终的图。
-
使用
triangular核生成并绘制 KDE:dist<-kdensity(y3,kernel = "triangular") plot(dist)输出如下:

图 3.31:具有三角形核的 KDE
使用不同核的两种图看起来几乎相同。因此,带宽的选择比核的选择更重要。在这个练习中,带宽是由 R 的kdensity库自动选择的。
活动 8:寻找与 Iris 数据集变量分布最接*的标准分布
在这个活动中,我们将找到与 Iris 数据集的 setosa 物种变量分布最接*的标准分布。以下步骤将帮助你完成活动:
-
加载 Iris 数据集。
-
选择仅对应 setosa 物种的行。
-
绘制由
kdensity函数生成的花瓣长度和花瓣宽度的分布图。注意
这个活动的解决方案可以在第 218 页找到。
这个活动的最终结果将是花瓣宽度 KDE 图,如下所示:

图 3.32:预期花瓣宽度 KDE 图
Kolmogorov-Smirnov 测试简介
现在我们已经学会了如何生成不接*标准分布的数据集的概率密度函数,我们将学习如何执行一些测试来区分这些非标准分布。
有时候,我们被给出多个观测数据样本,我们想知道这些样本是否属于同一分布。在标准分布的情况下,我们有多个测试,例如 Student's t-test 和 z-test,来确定这一点。对于非标准分布,或者当我们不知道分布类型时,我们使用 Kolmogorov-Smirnov 测试。为了理解 Kolmogorov-Smirnov 测试,你首先需要理解一些术语:
-
累积分布函数(CDF):这是一个函数,其值给出了随机变量小于或等于函数参数的概率。
-
零假设:在假设检验中,零假设意味着观测样本之间没有显著差异。在假设检验中,我们的目标是证伪零假设。
Kolmogorov-Smirnov 测试算法
在 Kolmogorov-Smirnov 测试中,我们执行以下步骤:
-
为两个函数生成 CDF。
-
指定一个分布作为父分布。
-
在同一张图上绘制两个函数的 CDF。
-
找到两个 CDF 中点之间的最大垂直差异。
-
从上一步测量的距离计算测试统计量。
-
在 Kolmogorov-Smirnov 表中找到临界值。
在 R 中,这些步骤是自动化的,因此我们不需要逐个执行它们。
练习 20:对两个样本执行 Kolmogorov-Smirnov 测试
要对两个样本执行 Kolmogorov-Smirnov 测试,请执行以下步骤:
-
生成两个独立的分布进行比较:
x_norm<-rnorm(100, mean = 100, sd=5) y_unif<-runif(100,min=75,max=125) -
按如下方式绘制
x_norm的 CDF:plot(ecdf(x_norm))输出如下:
![图 3.33:C12628_03_33.jpg]()
![图 3.33:C12628_03_33.jpg]()
要绘制
ecdf(y_unif),请执行以下操作:plot(ecdf(y_unif),add=TRUE)输出如下:
![图 3.34:C12628_03_34.jpg]()
![图 3.34:C12628_03_34.jpg]()
如您所见,函数的累积分布函数(CDF)看起来完全不同,因此 Kolmogorov-Smirnov 测试将返回非常小的 p 值。
-
在 R 中使用
ks.test()函数运行 Kolmogorov-Smirnov 测试:ks.test(x_norm,y_unif)输出如下:
Two-sample Kolmogorov-Smirnov test data: x_norm and y_unif D = 0.29, p-value = 0.0004453 alternative hypothesis: two-sided注意
这个练习依赖于随机生成数据。因此,当您运行此代码时,一些数字可能会有所不同。在假设检验中,有两个假设:零假设和检验假设。假设检验的目的是确定我们是否有足够的证据来拒绝零假设。在这种情况下,零假设是两个样本是由同一分布生成的,检验假设是两个样本不是由同一分布生成的。p 值表示在零假设为真的情况下,观察到与观察到的差异一样极端或更极端的概率。当 p 值非常接*零时,我们将其视为零假设为假的证据,反之亦然。
如您所见,
ks.test()返回两个值,D和 p 值。D值是两个分布的 CDF 中两点之间的绝对最大距离。它越接*零,两个样本属于同一分布的可能性就越大。p 值的解释与任何其他情况相同。在我们这个例子中,
D的值为0.29,p 值非常低,接*零。因此,我们拒绝两个样本属于同一分布的零假设。接下来,我们将生成一个新的正态分布,并观察它对 p 值和D的影响。 -
生成一个与
xnorm具有相同mean和sd的新正态分布:x_norm2<-rnorm(100,mean=100,sd=5) -
绘制
x_norm和x_norm2的合并 CDF:plot(ecdf(x_norm)) plot(ecdf(x_norm2),add=TRUE)输出如下:
![图 3.35:C12628_03_35.jpg]()
图 3.35:合并 CDF 的绘图
-
在
x_norm和x_norm2上运行ks.test():ks.test(x_norm,x_norm2)输出如下:
Two-sample Kolmogorov-Smirnov test data: x_norm and x_norm2 D = 0.15, p-value = 0.2106 alternative hypothesis: two-sided如您所见,这次 p 值要高得多,而
D要低得多。因此,根据 p 值,我们不太有理由拒绝两个样本属于同一分布的零假设。
活动九:使用正态分布计算累积分布函数(CDF)和执行 Kolmogorov-Smirnov 测试
在随机生成的分布的帮助下,计算样本的花瓣长度和宽度最接*的标准分布:
-
将 Iris 数据集加载到一个变量中。
-
仅保留具有 setosa 物种行的记录。
-
计算花瓣长度的*均值和标准差。
-
使用花瓣长度列的*均值和标准差生成一个新的正态分布。
-
绘制两个函数的累积分布函数(CDF)。
-
生成 Kolmogorov-Smirnov 测试的结果并检查分布是否为正态分布。
-
对花瓣宽度列重复步骤 3、4、5 和 6。
注意
本活动的解决方案可以在第 219 页找到。
本活动的最终结果如下:
Two-sample Kolmogorov-Smirnov test
data: xnorm and df$Sepal.Width
D = 0.12, p-value = 0.7232
alternative hypothesis: two-sided
摘要
恭喜您完成了本书的第三章!在本章中,我们学习了标准概率分布的类型,以及如何在 R 中生成它们。我们还学习了如何使用核密度估计(KDE)找到未知分布的 PDF 和 CDF。在最后一节中,我们学习了如何在 R 中比较两个样本并确定它们是否属于同一分布。在接下来的章节中,我们将学习其他类型的无监督学习技术,这些技术不仅有助于探索性数据分析,还能为我们提供对数据的其他有用见解。
第五章:第四章
降维
学习目标
到本章结束时,你将能够:
-
应用不同的降维技术
-
使用 Apriori 算法执行市场篮子分析
-
对数据集执行主成分分析
在本章中,我们将探讨不同的降维技术。
简介
本章介绍了无监督学习技术,这些技术实现了所谓的降维。首先,我们将讨论什么是维度,为什么我们想要避免拥有太多的维度,以及降维的基本思想。然后,本章将详细介绍两种降维技术:市场篮子分析和主成分分析(PCA)。市场篮子分析是一种在数据集中生成关联规则的技术。本章将包含一个详细的 R 代码示例,展示如何实现这一目标。PCA 是一种非常常见的降维技术,源自理论线性代数。本章还将详细展示如何使用 R 实现 PCA。
降维的概念
数据集的维度不过是描述其中观测所需的不同数字的集合。例如,考虑以 Pac-Man 命名的游戏中的 Pac-Man 位置。Pac-Man 是一款在 20 世纪美国流行的游戏。这是一款极其简单的游戏:Pac-Man 是一个屏幕上的小圆形生物,喜欢吃小点和水果。他生活在一个迷宫中,只能用两组方向移动:上/下和左/右。有一些怪物试图追赶 Pac-Man 并杀死他。你可以在下面的插图看到 Pac-Man 游戏的样子,以及他必须在其中移动的世界:

图 4.1:Pac-Man 风格游戏的插图
如你所见,Pac-Man 的位置可以用两个数字完全描述:他距离屏幕左侧有多远,以及他距离屏幕顶部的距离有多远。如果我们知道这两个数值测量值,那么屏幕上就只有一个唯一的位置他可能在那里。所以,如果我们想要收集关于 Pac-Man 随时间位置的数据,我们就能收集一个包含这两个数字的二维数据集,这些数字被反复测量。我们会完全确信,每个由两个数字组成的观测值,完全描述了在观测时刻关于 Pac-Man 位置所能知道的一切。
不只是位置数据或几何数据可以被描述为二维。任何包含两种不同测量的数据集都可以被描述为二维。例如,如果我们测量了个人身高和体重,我们可以创建一个包含他们的身高和体重测量的二维数据集。如果我们记录了身高、体重和鞋码,那么我们就会有一个三维数据集。数据集中可以包含的维度数量没有限制。
降维是找到一个低维数据集来*似高维数据集的过程。考虑一个与 Pac-Man 相关的例子。想象我们有一个描述 Pac-Man 位置的三个维度的数据集。假设这个数据集的维度是(1)Pac-Man 距离屏幕左侧有多远,(2)Pac-Man 距离屏幕顶部有多远,以及(3)Pac-Man 距离追逐他的蓝色怪物有多远。这是一个三维数据集;然而,我们只需要前两个维度的信息就可以完全了解 Pac-Man 的位置。我们进行有效降维的最简单方法就是丢弃第三个维度,因为它不会比只有前两个维度帮助我们更好地定位 Pac-Man。因此,由数据集的前两个维度组成的二维数据集将是我们最初开始的三维数据集的良好*似。
在大多数实际场景中,降维并不像丢弃维度那样简单。通常,我们将尝试使用所有维度的数据来创建一个全新的数据集,其维度与原始数据集的维度具有不同的含义。本章剩余的练习将说明这个过程。
在接下来的练习中,我们将查看一个包含多个维度的数据集。我们将创建图表来说明降维以及它如何帮助我们。
练习 21:检查包含不同葡萄酒化学属性的数据集
前提条件:
注意
此数据集来自 UCI 机器学习仓库。您可以在archive.ics.uci.edu/ml/datasets/Wine找到数据集。我们已经下载了文件,并将其保存于github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson04/Exercise21/wine.csv。
下载此数据,并将其存储在名为wine.csv的文件中。
注意
对于所有需要导入外部 csv 或图像的练习和活动,请转到 R Studio-> 会话-> 设置工作目录-> 到源文件位置。你可以在控制台中看到路径已被自动设置。
这份数据包含了关于 178 个不同葡萄酒样本 13 个不同化学测量值的信息。总共有 13 维数据集。如果我们考虑只包含 13 个属性中的 2 个属性的数据子集,我们将得到一个 2 维数据集,这与我们的假设 Pac-Man 数据相似。对于 2 维数据,我们总是可以在 2 维散点图上绘制它。
在这个数据集中,第一列记录了葡萄酒的类别,换句话说,就是它的类型。其他每一列都记录了与葡萄酒化学成分相关的测量值。机器学习的一个美妙之处在于,即使我们对葡萄酒的化学知识一无所知,我们也可以使用纯粹的数据分析工具来发现模式并得出可能连化学专家都未曾注意到的结论。
完成步骤如下:
-
打开 R 控制台,确保你已将数据文件(
wine.csv)保存在 R 可以访问的位置。你可以使用setwd()命令来确保你的文件是可访问的。例如,如果你的wine.csv文件位于C:/Users/me/datasets文件夹中,那么你可以在 R 控制台中运行setwd('C:/Users/me/datasets')命令。然后,你将能够在 R 中打开葡萄酒数据文件,如下所示:wine<-read.csv('wine.csv') -
考虑以下由
flavanoids和total phenols属性创建的二维数据散点图:plot(wine$flavanoid,wine$phenol)输出如下:
![图 4.2:黄酮和酚类二维数据的散点图]()
图 4.2:黄酮和酚类二维数据的散点图
-
在绘制数据后,我们观察到黄酮和酚类测量值之间似乎存在强烈的关联。我们可以在图上画一条线来表示这种相关性。现在,你不必担心我们如何在下面的命令中找到标记为
a和b的系数:plot(wine$flavanoid,wine$phenol) abline(a=1.1954,b=.54171,col='red',lwd=5)

图 4.3:表示黄酮和酚类之间相关性的散点图,其中有一条线表示这种相关性
如你所见,红线非常接*我们数据的几何形状。数据中的大多数点都非常接*红线。如果我们想要简洁地描述这些点,我们可以说它们最接*红线的哪个点。这不会是数据的完美描述,因为即使它们的黄酮和酚类水*不同,一些点也会映射到红线上相同的点。然而,仅使用红线来描述这些数据是对实际数据的合理*似。
如果我们用每个观察结果最接*的红线上的点来描述它,那么我们所完成的就是降维。我们从一个需要两个测量值来描述每个观察结果的数据库开始,并找到了只用一个点来描述每个观察结果的方法。这是所有降维策略的基本思想,本章包含了一些实现它的实用策略。
降维的重要性
为什么降维是我们感兴趣做的事情?以下是一些原因:
-
一个原因可能是为了压缩数据。如果一个数据集特别大,而且如果你的笔记本电脑上运行的 R 实例在对其进行简单计算时花费时间过长,那么降低数据的维度可能是有用的,这样它就可以更容易地适应你的计算机内存。
-
降维更有趣的原因是,它为我们提供了对数据潜在结构和不同属性之间相互关系的洞察。在前面的练习中,即使我们没有在化学方面的先进培训,我们也可以使用我们从简单的降维练习中学到的知识来更好地理解葡萄酒化学。
注意
如果我们阅读更多关于酚类和黄烷醇(例如,在这个网站上:
www.researchgate.net/post/What_is_the_relation_between_total_Phenol_total_Flavonoids),我们可以了解到酚类和黄烷醇都具备抗氧化活性。因此,图表上的红线可能代表了特定葡萄酒的抗氧化活性水*,而黄酮醇和酚类的测量只是捕捉了这一事物的噪声测量。因此,降维使我们能够对葡萄酒的化学成分提出假设,即使没有高级领域的知识。
市场篮子分析
市场篮子分析是一种方法,它允许我们将高维数据降低到简单且易于管理的程度,同时不会在过程中丢失太多信息。在市场篮子分析中,我们的目标是生成控制数据的规则。
市场篮子分析也称为亲和分析。它是以一家杂货店试图对其顾客的交易进行分析的例子命名的——分析每个顾客放入其篮子的产品。任何给定时间,大型杂货店可能有大约 5,000 种商品出售。他们每天可能有数千名顾客。对于每位顾客,杂货店可以记录这些顾客的交易记录。一种方法就是使用二进制编码,如下面的例子所示:
客户 1 在第一天交易:
花生酱:否
果冻:是
面包:否
牛奶:否
…
客户 2 在第一天交易:
花生酱:是
果冻:是
面包:否
牛奶:否
…
这些交易可以存储在一个有 5,000 列的表中——每列代表商店中出售的每个项目——以及每行代表每条记录的交易。而不是为每个项目存储“是”和“否”的值,它们可以在一个看起来像以下表格的表中存储 1s 和 0s,其中 1 表示“是”,0 表示“否”:

图 4.4:展示客户交易的表格
上述表格只显示了四列和五行,但在实践中,表格会大得多。
市场篮子分析的最简单用例是回答一个简单的问题:通常一起购买哪些商品?杂货店老板可能纯粹出于好奇对此感兴趣。但事实上,有一些令人信服的商业理由使他们想要了解客户最常见的篮子。
到目前为止,这个问题似乎相当简单。我们的二进制数据是最简单的,只由 0s 和 1s 组成。我们的问题仅仅是找到哪些商品倾向于一起购买。复杂性不在于这些简单想法,而在于它们的实际实施。
考虑寻找倾向于一起购买的商品的暴力方法。如果我们考虑每个可能的项目篮子,即前述数据中 0s 和 1s 的每个可能组合,我们发现存在 2⁵⁰⁰⁰ 个可能的篮子。这比已知宇宙中的粒子数还要多,在合理的时间内检查每个可能的篮子或存储关于每个可能的篮子的发现都是计算上不可行的。
如果我们不能检查每个可能的篮子,我们如何才能找到任何信心地认为我们进行了全面检查的与任何篮子一起购买的篮子?答案是应用算法解决方案。Apriori算法是在时间和空间限制下进行彻底市场篮子分析的最流行方法。它是由 Agrawal 和 Srikant 发明的,他们在 1994 年发表了关于它的论文。它按顺序通过不断增加的市场篮子大小进行。
Apriori 算法由几个步骤组成。在前几个步骤中,我们将遍历我们的数据集以找到最常见的篮子。在我们的第一次遍历中,我们将找到包含恰好一个项目的最常见的篮子。在我们的第二次遍历中,我们将找到包含恰好两个项目的最常见的篮子。我们将继续进行这些遍历,直到我们找到我们感兴趣的每个尺寸的最常见篮子。在杂货店的例子中,可能最常见的两个项目篮子是“花生酱,果酱”,而最常见的三个项目篮子是“花生酱,果酱,面包”。
在找到最常见的篮子后,我们将为这些篮子生成关联规则。这些规则将表达最常见的篮子中项目之间的关系。例如,一个杂货店的关联规则可能如下所示:“如果花生酱和果酱都在篮子里,那么面包很可能也在篮子里。”这类规则使我们能够找到不同单个项目之间的关联,这可能对我们有用。例如,在知道花生酱和果酱经常与面包一起出现后,杂货店老板可能会对重新排列这些商品的陈列感兴趣,以便它们在商店中更靠*,让购物者更容易且不费力地将它们放入篮子中。
注意
规则“如果花生酱和果酱[在篮子里]存在,那么面包很可能[在那个篮子里]存在”是一个简单的关联规则。关联规则有时会画一个箭头从 X 指向 Y,表示 X“意味着”Y,尽管关联规则不一定具有因果关系。
许多美国人从小吃着花生酱和果酱三明治长大,因此对他们来说,花生酱、果酱和面包可能很可能会一起购买。市场篮子分析可能会生成一些看似明显的关联规则,例如这些。然而,在实践中,市场篮子分析可能会生成一些令人惊讶和意外的关联规则。这是另一个例子,即使不是购物或零售方面的专家,我们也可以使用机器学习来发现即使是专家也会感到惊讶的模式和见解。
在下一个练习中,我们将应用市场篮子分析到人口普查调查数据中。数据集的数据如下所示:

图 4.5:数据集截图
注意
此数据集来自 UCI 机器学习仓库。您可以在archive.ics.uci.edu/ml/datasets/Adult找到数据集。我们已经下载了文件并将其保存在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson04/Exercise22-Exercise25/。
这份数据与我们之前描述的假设购物篮数据不同,因为它的列不是 0-1 的二进制编码,而是可以接受多个值。由于 Apriori 算法是为 0-1 数据设计的,我们将对数据进行重新编码。在这里,重新编码意味着我们将创建新的变量,这些变量比原始变量更简单、更容易处理,但仍然传达相同的信息。我们将在这里执行的重编码将使数据由 0-1 编码组成。我们在这里所做的事情的另一个术语是创建虚拟变量。虚拟变量是一个只取 0 和 1 值的变量。对于数据集中的每一列,我们可以参考archive.ics.uci.edu/ml/datasets/Adult上的数据,以找到有关该列的信息,然后使用这些信息进行我们的重新编码。我们可以对所有的变量执行类似的转换。
对于像就业状态这样的分类变量,我们为每个可能的响应创建新的 0-1 变量。对于像年龄这样的有序变量,我们创建两个新的变量,表示值是高还是低。
我们将得出关于哪些调查答案倾向于以相同方式回答的结论。除了购物数据之外,市场篮子分析可以用于各种数据集。无论使用什么数据集,市场篮子分析都会生成关联规则,并告诉我们哪些数据属性倾向于具有相同的值。
练习 22:为 Apriori 算法准备数据
注意
练习 22-25 应一起执行。
在这个练习中,我们将使用在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson04/Exercise22-Exercise25/census.csv上免费提供的数据库。这是调查数据。要使用此数据,您应首先将其下载到您的计算机上 - 保存为名为census.csv的文件。您不需要加载任何特殊包来运行此数据或完成任何先决条件:
-
使用 R 中的
setwd()函数读取数据。在设置工作目录后,你可以按以下方式将其读入 R:filepath='census.csv' mkt<-read.csv(filepath,stringsAsFactors=FALSE,header=FALSE,sep=',') -
检查数据:
head(mkt)![图 4.6:数据截图]()
图 4.6:数据截图
你会注意到,R 已经自动为数据分配了列名,因为原始数据文件没有包含列名。默认情况下,R 从
V开始分配编号列名,因为每一列都可以被视为一个向量。 -
创建虚拟变量。
我们可以从数据网站上看到第一个变量,R 将其称为
V1,是年龄的测量值。对于这个变量,我们根据其值是否高于或低于中位数年龄值将其重新编码为 0-1 二进制变量。我们可以用"median(mkt$V1)"来计算中位数年龄值:mkt$old<-1*(mkt$V1>median(mkt$V1)) mkt$young<-1*(mkt$V1<=median(mkt$V1)) -
同样,我们可以在网站上看到第二列,R 将其标记为
V2,指的是就业状况。对于就业,我们可以创建几个新变量,每个就业类别一个:mkt$government_employee<-1*(mkt$V2 %in% c(" State-gov"," Local-gov"," Federal-gov")) mkt$self_employed<-1*(mkt$V2 %in% c(" Self-emp-not-inc"," Self-emp-inc")) mkt$never_worked<-1*(mkt$V2 %in% c(" Never-worked")) mkt$private_employment<-1*(mkt$V2 %in% c(" Private")) mkt$other_employment<-1*(mkt$V2 %in% c(" ?"," Without-pay" )) -
在这里,我们为受访者的教育水*编码 0-1 变量:
mkt$high_school_incomplete<-1*(mkt$V4 %in% c(" 1st-4th"," Preschool"," 5th-6th"," 7th-8th"," 9th"," 10th"," 11th"," 12th")) mkt$high_school_complete<-1*(mkt$V4 %in% c(" HS-grad"," Some-college"," Assoc-acdm"," Assoc-voc")) mkt$bachelors<-1*(mkt$V4 %in% c(" Bachelors")) mkt$post_bachelors<-1*(mkt$V4 %in% c(" Masters"," Prof-school"," Doctorate" ))我们使用
V4列来编码教育水*,因为标记为V3的列对我们来说没有用。我们不会使用V5列,因为它包含的是以不同方式表达相同数据。 -
在这里,我们为一个人的婚姻状况编码 0-1 变量:
mkt$married<-1*(mkt$V6 %in% c(" Married-civ-spouse"," Married-AF-spouse"," Married-spouse-absent")) mkt$never_married<-1*(mkt$V6 %in% c(" Never-married")) mkt$divorced_separated<-1*(mkt$V6 %in% c(" Divorced"," Separated")) mkt$widowed<-1*(mkt$V6 %in% c( " Widowed")) -
在这里,我们为受访者的职业编码 0-1 变量:
mkt$clerical<-1*(mkt$V7 %in% c(" Adm-clerical")) mkt$managerial<-1*(mkt$V7 %in% c(" Exec-managerial")) mkt$moving<-1*(mkt$V7 %in% c(" Transport-moving")) mkt$farming_fishing<-1*(mkt$V7 %in% c(" Farming-fishing")) mkt$craft_repair<-1*(mkt$V7 %in% c(" Craft-repair" )) mkt$sales<-1*(mkt$V7 %in% c(" Sales")) mkt$tech_support<-1*(mkt$V7 %in% c(" Tech-support")) mkt$service<-1*(mkt$V7 %in% c(" Protective-serv"," Priv-house-serv", " Other-service")) mkt$armed_forces<-1*(mkt$V7 %in% c(" Armed-Forces")) mkt$other_occupation<-1*(mkt$V7 %in% c(" Handlers-cleaners"," ?"," Machine-op-inspct"," Prof-specialty"))我们不会使用
V8列,因为它是为了普查目的而记录的,对我们分析没有用。 -
在这里,我们为受访者的自报性别编码 0-1 变量:
mkt$male<-1*(mkt$V9 %in% c(" Male")) mkt$female<-1*(mkt$V9 %in% c(" Female"))V10和V11列不太具有信息量,所以我们不会在分析中使用它们。 -
在这里,我们为每个受访者的自报工作时间编码 0-1 变量:
mkt$high_hours<-1*(mkt$V12 > median(mkt$V12)) mkt$low_hours<-1*(mkt$V12 <= median(mkt$V12)) -
在这里,我们为受访者报告的国籍是否为美国编码 0-1 变量:
mkt$usa<-1*(mkt$V13==" United-States") mkt$not_usa<-1*(mkt$V13!=" United-States") -
在这里,我们为受访者报告的收入是否高于或低于$50,000 编码 0-1 变量:
mkt$low_income<-1*(mkt$V14==" <=50K") mkt$high_income<-1*(mkt$V14==" >50K") -
现在,我们已经添加了 33 个新的变量,它们是 0-1 编码。由于我们只会在 0-1 编码上执行市场篮子分析,我们可以删除最初用来创建只包含虚拟变量的数据集的 14 个初始变量,如下所示:
mktdummies<-mkt[,15:ncol(mkt)] mktdummies -
我们可以通过运行以下代码来查看我们每个变量的*均值:
print(colMeans(mktdummies,na.rm=TRUE))虚拟变量的*均值等于它等于 1 的时间百分比。所以,当我们看到
已婚变量的*均值是 0.473 时,我们知道大约 47.3%的受访者已婚。完成这个练习后,你的数据将会有 33 列,每一列都是一个只取 0 和 1 值的
虚拟变量实例。如果你在控制台中运行print(head(mktdummies))来打印前 6 行,那么你可以看到生成的数据集如下所示:![图 4.7:虚拟变量结果数据集的一部分]()
图 4.7:虚拟变量结果数据集的一部分
现在我们已经完成了练习,我们有一个只包含 0-1 变量的虚拟变量数据集,这些变量提供了关于数据集中每个原始变量的真/假信息。
最后,我们准备实际执行 Apriori 算法。在接下来的练习中,我们将开始“遍历”我们的数据。在每次遍历中,我们将找到具有特定大小的最常见的篮子。
在我们开始遍历数据之前,我们需要指定一个称为支持率的东西。支持率是 Apriori 算法参数之一的名字。在这里,支持率指的是包含特定项目组合的篮子百分比。如果我们发现市场数据中 40%的受访者既是高收入又是女性,那么我们将说在我们的数据中,高收入、女性的“篮子”有 40%的支持率。
我们需要决定我们感兴趣的最低支持率。如果我们设定的最低支持率阈值过高,我们将找不到任何满足阈值的篮子。如果我们设定的最低支持率阈值过低,我们将找到太多的篮子,这将很难查看所有篮子以找到有趣的一个。此外,因为我们希望找到实际有用的规则,所以我们希望找到相对常见的篮子,因为更常见的篮子更有可能对我们有实际用途。
练习 23:通过数据遍历以找到最常见的篮子
现在数据已经准备好进行市场篮子分析的主要步骤。在继续之前,我们必须决定我们将在算法中使用哪些参数:
-
我们将要处理的第一参数是支持率,如前所述。在这种情况下,我们可以从将最低支持率阈值设定为 10%开始。
support_thresh<-0.1 -
首先,我们将找到所有符合我们支持阈值的单项篮子,如下所示:
firstpass<-unname(which(colMeans(mktdummies,na.rm=TRUE)>support_thresh))这显示了至少有 10%的受访者以相同方式回答的所有调查项目。
-
为了对数据进行第二次遍历,我们将定义所有可能的两项篮子候选者,这些篮子可能支持率超过 10%,如下所示:
secondcand<-t(combn(firstpass,2)) secondpass<-NULL注意
如果少于 10%的篮子包含某个特定项目,那么超过 10%的篮子同时包含该项目和另一个项目的可能性是不存在的。因此,支持率超过 10%的两项篮子候选者将是那些在第一次数据遍历中幸存的项目组合。
我们已经定义了
secondcand,这是我们第二次遍历的候选者集合,以及secondpass,我们将用它来存储第二次遍历的结果。secondpass变量初始值为NULL,因为我们还没有开始第二次遍历。如果我们查看
secondcand,我们可以看到它由一对数字组成。每个数字都指代mktdummies数据中的一个列。例如,secondcand的第四行指代一个潜在篮子,其中包含那些表示他们年龄大于中位数且是私企雇员的受访者。在第二次数据遍历中,我们将检查secondcand中的每个两项候选者,如果其支持率超过 10%,它将成功通过第二次数据遍历。 -
为了检查我们的候选者
secondcand中第四行的支持率,我们可以进行以下计算:k<-4 support<-mean(mktdummies[,secondcand[k,1]]*mktdummies[,secondcand[k,2]],na.rm=TRUE) print(support)输出如下:
0.05515801 -
我们需要为每个候选篮子进行相同的计算,我们可以通过将这个计算放入循环中来实现。这个循环将把达到支持阈值的最终两个项篮子保存在
secondpass变量中:k<-1 while(k<=nrow(secondcand)){ support<-mean(mktdummies[,secondcand[k,1]]*mktdummies[,secondcand[k,2]],na.rm=TRUE) if(support>support_thresh){ secondpass<-rbind(secondpass,secondcand[k,]) } k<-k+1 } -
这个练习的重要结果变量是名为
secondpass的变量。这个变量包含所有达到我们指定的支持阈值(10%)的两个项篮子。通过在控制台中运行以下命令,查看这个变量的前六行:print(head(secondpass))输出如下:
[,1] [,2] [1,] 1 6 [2,] 1 9 [3,] 1 12 [4,] 1 14 [5,] 1 25 [6,] 1 26在这里,每一行包含两个数字,每个数字都指代原始数据集中的列号。例如,第一行表示
mktdummies数据集的第一列和第六列共同构成一个支持度超过 10%的两个项篮子。由于我们数据集的第一列被称为old,而数据集的第六列被称为private_employment,因此我们得出结论,既是老年人又是私营部门雇员的调查受访者占所有调查受访者的 10%以上。
在此之后,我们已经完成了第二次数据遍历。通过完成第二次遍历,我们现在有一个包含所有最常见的两个项篮子的列表。
Apriori 算法的要点在于,我们可以利用两项篮子和单项篮子来缩小我们关注的三个项候选篮子,这使得我们的搜索速度大大加快。
要全面了解 Apriori 算法的工作原理,我们应该至少再遍历一次数据,这将在下面的练习中介绍。
练习 24:多次遍历数据
在下面的练习中,我们将多次遍历数据。回想一下,每次我们遍历数据时,我们都在寻找符合我们支持阈值的篮子。在每次遍历中,我们寻求比之前遍历中更多的篮子。因此,在第一次遍历中,我们寻找符合我们支持阈值的单项篮子。在第二次遍历中,我们寻找符合我们支持阈值的两项篮子。在下面的练习中,我们将说明如何进行多次数据遍历,包括第三次遍历,我们将寻找符合我们支持阈值的三个项篮子,以及第四次遍历,我们将寻找符合我们支持阈值的四个项篮子。
如果我们对许多项目所遵循的复杂规则感兴趣,能够多次遍历数据对我们来说将非常重要:
-
在第三次遍历数据时,我们将寻找至少有 10%支持度的三个项篮子。第三次遍历数据将从
product变量等于 1 开始。这个product变量将给我们数据的不同列的乘积,而product变量的*均值将给我们不同篮子的支持度,如下所示:product<-1 n<-1 -
这个
product变量将与在第二次遍历中幸存下来的两项篮子相关的观测值相乘:thirdpass<-NULL k<-1 while(k<=nrow(secondpass)){ j<-1 while(j<=length(firstpass)){ n<-1 product<-1 while(n<=ncol(secondpass)){ product<-product*mktdummies[,secondpass[k,n]] n<-n+1 } -
最后,每个
product变量将乘以第一轮中幸存下来的单个项目篮子的观测值:if(!(firstpass[j] %in% secondpass[k,])){ product<-product*mktdummies[,firstpass[j]] -
我们取产品的*均值以找到我们指定的篮子的支持度:
support<-mean(product,na.rm=TRUE) -
如果结果三项篮子的支持度高于我们指定的支持度阈值,则将其保存到我们的最终
thirdpass变量中:if(support>support_thresh){ thirdpass<-rbind(thirdpass,c(secondpass[k,],firstpass[j])) } } j<-j+1 } k<-k+1 }注意
步骤 2-5 应一起执行。
现在我们有一个包含数据中所有大小为三的常见篮子的列表。
-
经过几轮数据遍历后,我们可以开始看到 Apriori 算法所采取步骤的一般形式。一般来说,为了找到在
n轮中幸存下来的篮子,我们需要取在n-1轮中幸存下来的篮子,向其中添加一个在第一轮中幸存下来的项目,并查看结果组合的支持度是否大于我们选择的阈值:fourthpass<-NULL k<-1 while(k<=nrow(thirdpass)){ j<-1 while(j<=length(firstpass)){ n<-1 product<-1 while(n<=ncol(thirdpass)){ product<-product*mktdummies[,thirdpass[k,n]] n<-n+1 } if(!(firstpass[j] %in% thirdpass[k,])){ product<-product*mktdummies[,firstpass[j]] support<-mean(product,na.rm=TRUE) if(support>support_thresh){ fourthpass<-rbind(fourthpass,c(thirdpass[k,],firstpass[j])) } } j<-j+1 } k<-k+1 }我们可以无限期地继续这样做,创建任何大小且符合我们支持度阈值的篮子。在这里,我们的目的是在数据中遍历四次后停止,并检查我们第三遍的结果。
-
本练习的最终重要结果是
thirdpass和fourthpass变量。这些变量包含关于符合我们支持度阈值的三项和四项篮子的信息。您可以像解释secondpass的每一行一样解释这些变量的每一行。每一行代表一个符合我们支持度阈值的篮子,每一行中的每个数字都指的是我们的数据集中的一列编号。您可以通过执行以下操作来验证
thirdpass的前六行:print(head(thirdpass))输出如下:
[,1] [,2] [,3] [1,] 1 6 9 [2,] 1 6 12 [3,] 1 6 26 [4,] 1 6 29 [5,] 1 6 30 [6,] 1 6 32我们可以将第二行解释为表示包含项目 1、项目 6 和项目 12 的篮子达到了我们的支持度阈值。
-
您可以通过以下方式验证
fourthpass的前六行:print(head(fourthpass))输出如下:
[,1] [,2] [,3] [,4] [1,] 1 6 9 26 [2,] 1 6 9 29 [3,] 1 6 9 30 [4,] 1 6 9 32 [5,] 1 6 12 26 [6,] 1 6 12 29我们可以将第五行解释为告诉我们包含项目 1、项目 6、项目 12 和项目 26 的篮子达到了我们的支持度阈值。
在之前的练习中,我们已经找到了我们感兴趣的篮子。在这个练习中,我们将获得市场篮子分析的最后产品。我们感兴趣的最后产品将是“旧”、“私人雇佣”和“低小时数”的统一。我们还感兴趣于生成一个关联这三个项目的规则。这样一个规则可能就是“年龄超过中位数调查受访者且为私人雇佣的人,很可能工作时间少于中位数受访者”。因此,市场篮子分析比其他仅发现数据中组的分布分析和聚类方法更进一步。市场篮子分析不仅找到组,而且将它们按照有意义的规则进行分组。
为了生成这些规则,我们需要指定更多的参数,类似于我们之前指定的支持度阈值。
这些参数中的一个被称为置信度。置信度仅仅是一个条件概率。假设一个人既是女性又是低收入,她离婚的可能性有多大?我们迄今为止确定的是支持度,这可能告诉我们由女性、低收入和离婚这三个项目组成的篮子占所有调查者的 10%以上。置信度告诉我们更多——它告诉我们“离婚”是否只是一个常见的篮子项目,或者是否在“女性”和“低收入”存在的情况下特别常见。
我们最后必须指定的参数被称为提升度。提升度是规则预测的项目总体普遍性的置信度。在这种情况下,假设如果一个人是女性且低收入,她有 90%的可能性也是离婚的。那么 90%是这个规则的置信度,这似乎相当高。然而,如果 89%的人无论如何都是离婚的,那么这个置信度就不会显得那么令人印象深刻。如果是这样,那么知道篮子中存在“女性”和“低收入”只会略微提高我们的预测能力,大约 1%。在这种情况下,提升度的值将是 90%/89%,或大约 1.011。这只是一个假设——我们得检查实际数据来看到提升度的实际值是多少。
一起,置信度和提升度提供了帮助我们决定一个关联规则是否有用的测量指标。在一个复杂的情况,比如我们在这里看到的许多问题调查中,我们指定置信度和提升度的最小阈值,以过滤掉不够有用的关联规则,这样我们就可以用少量非常有用的规则完成 Apriori 算法。
练习 25:作为 Apriori 算法最后一步生成关联规则
在这个练习中,我们将完成 Apriori 算法的最后一步。到目前为止,任何经过我们数据处理而幸存下来的篮子都可以被认为是候选规则。在市场篮子分析的最终步骤中,我们将根据我们的最终标准——置信度和提升度进一步减少候选规则。
-
检查以下经过多次数据处理的篮子:
head(thirdpass)输出如下:
[,1] [,2] [,3] [1,] 1 6 9 [2,] 1 6 12 [3,] 1 6 26 [4,] 1 6 29 [5,] 1 6 30 [6,] 1 6 32你可以这样看到经过第三次处理幸存下来的三项篮子的数量:
nrow(thirdpass)输出如下:
[1] 549我们可以看到有 549 个三项篮子,即 549 个至少在我们的数据中有 10%支持度的候选规则。这些篮子不是市场篮子分析的最终产品——我们正在寻找的最终产品是关联规则。
-
我们三项篮子的置信度公式如下:由所有三个项目组成的篮子的支持度,除以只包含前两个项目的篮子的支持度。我们可以这样计算我们的
thirdpass三项篮子的第五行的置信度:k<-5 confidence<-mean(mktdummies[,thirdpass[k,1]]*mktdummies[,thirdpass[k,2]]*mktdummies[,thirdpass[k,3]],na.rm=TRUE)/mean(mktdummies[,thirdpass[k,1]]*mktdummies[,thirdpass[k,2]],na.rm=TRUE)注意
这只是包含三个项目的完整购物篮的支持度,除以不包含第三个项目的两个项目的支持度。
-
提升度是置信度除以规则预测的项目总体流行度。对于我们的第三遍候选人的第五行,提升度可以很容易地按以下方式计算:
k<-5 lift<-confidence/mean(mktdummies[,thirdpass[k,3]],na.rm=TRUE) -
为了将候选规则缩小到一组可接受的关联规则,我们将指定最小置信度和提升度阈值,就像我们对支持度所做的那样。在这里,我们指定了提升度阈值为 1.8 和置信度阈值为 0.8:
注意
lift_thresh<-1.8 conf_thresh<-.8 -
我们可以通过以下循环为我们的每个候选规则计算
提升度和置信度:thirdpass_conf<-NULL k<-1 while(k<=nrow(thirdpass)){ support<-mean(mktdummies[,thirdpass[k,1]]*mktdummies[,thirdpass[k,2]]*mktdummies[,thirdpass[k,3]],na.rm=TRUE) confidence<-mean(mktdummies[,thirdpass[k,1]]*mktdummies[, thirdpass[k,2]]*mktdummies[,thirdpass[k,3]],na.rm=TRUE)/ mean(mktdummies[,thirdpass[k,1]]*mktdummies[,thirdpass[k,2]],na.rm=TRUE) lift<-confidence/mean(mktdummies[,thirdpass[k,3]],na.rm=TRUE) thirdpass_conf<-rbind(thirdpass_conf,unname(c(thirdpass[k,],support,confidence,lift))) k<-k+1 }这生成了一个名为
thirdpass_conf的新变量,它是一个包含每个候选规则的支持度、置信度和提升度列的 DataFrame。在这里,conf被用作置信度的简称,这是我们添加到thirdpass数据中的。 -
最后,我们可以消除所有不符合指定置信度和提升度阈值的候选规则,如下所示:
thirdpass_high<-thirdpass_conf[which(thirdpass_conf[,5]>conf_thresh & thirdpass_conf[,6]>lift_thresh),] -
现在我们有了
thirdpass_high,这是我们数据中具有高置信度和高提升度的关联三项目规则的集合。我们可以通过以下方式将其中一些打印到控制台来浏览它们:head(thirdpass_high)

图 4.8:thirdpass_high 的输出
总的来说,我们在市场篮子分析中遵循的步骤可以总结如下:

注意
记住,这些是指我们在练习 22,为 Apriori 算法准备数据中创建的虚拟变量,其中我们创建了一个名为old的虚拟变量,对于年龄较高的个体,其值为 1,否则为 0。我们还创建了一个表示高收入的虚拟变量,其中 1 表示年收入超过 50,000 美元,否则为 0。
thirdpass_high的第一行规则的解释是:年龄超过中位数且收入较高的人,很可能(具有高置信度和高提升度)已婚。这在直觉上是有道理的:婚姻和高收入都需要很多年才能实现,所以没有很多年轻、已婚、高收入的人是有道理的。我们发现这个规则的置信度约为 87%,提升度约为 1.84。
在这种情况下,进行调查的公司可以使用这些数据来创建广告活动——要么创建针对已婚老年人的住房广告活动,因为这是一个经过证明的高收入人口群体,要么针对年轻的已婚人士的住房广告活动,因为这可能是一个未得到充分服务的群体,将构成商业机会。我们发现的每个七项规则都可以提供对人口模式和商业机会的见解,以及这些规则告诉我们什么以及它们提供的确定性量化测量。
在我们的市场篮子分析过程中,我们可以做出一些不同的选择,这些选择可能会改变我们的结果。如果我们改变指定的阈值,我们可能会得到更多的规则,或者更有用的规则。例如,如果我们将支持阈值设置为 9%而不是 10%,则过滤出的规则会更少,我们可能最终得到一条规则,例如“住在公寓里的年轻学生很可能是亚裔美国人”,这是一条只占调查受访者约 9%的群体的规则。
我们只关注了包含三项的篮子和与这些篮子元素相关的规则。通过允许更多或更少的物品进入我们用于搜索规则的篮子,我们可以找到更有趣的规则,这些规则可能导致坚实的商业洞察。所有这些都是在相对较短的时间内用相对较少的代码行完成的。这表明市场篮子分析在解决数据问题和商业问题方面的有用性和潜力。
市场篮子分析将一个高维问题(在大数据集中寻找模式的问题)转化为一个低维解决方案(六个简单、高置信度的规则),而无需太多的努力、计算能力或时间。
主成分分析
我们将要介绍的下一类降维方法是主成分分析(PCA)。这是一种在广泛领域的学者中非常常见的技巧。
线性代数复习
本节不会对线性代数进行全面回顾,而只是提醒一些主要观点。
备注
joshua.smcvt.edu/linearalgebra/#current_version 覆盖了一些基础知识,包括矩阵、协方差矩阵、特征向量和特征值。如果您已经熟悉这些术语,可以自由跳过线性代数复习。
矩阵
线性代数主要关注矩阵的分析。矩阵可以被视为一个矩形格式的数字集合。我们可以在 R 中创建一个矩阵,如下所示:
matrix1<-matrix(c(1,2,3,4,5,6),nrow=2)
在这里,我们创建了一个两行三列的矩阵,总共有六个条目。我们根据矩阵中条目出现的行和列来描述条目。在我们刚刚创建的"matrix1"中,数字 3 位于"1-2"位置,因为它位于第一行第二列。我们可以在 R 中通过调用matrix1[1,2]来访问该特定位置。
方差
通常,一个变量的方差让我们了解该变量分布的广泛程度。
协方差
协方差是测量两个不同变量一起的方差。它衡量它们的分散程度是否匹配。换句话说,它衡量如果一个变量高,另一个变量也高的程度,以及每个变量预期会多高。
练习 26:检查葡萄酒数据集中的方差和协方差
执行练习 21中要遵循的所有步骤,即检查包含不同葡萄酒化学属性的数据集。然后计算同一数据集的方差和协方差:
-
酒精测量值都在 11.03 和 14.83 之间,你可以通过运行以下代码来看到:
range(wine$alcohol)输出如下:
[1] 11.03 14.83 -
我们可以使用 R 的
var命令来计算方差。对于葡萄酒的酒精测量,我们发现var(wine$alcohol)约为 0.66。相比之下,我们发现通过执行以下代码,我们数据集中的镁测量值分布更广:range(wine$magnesium)输出如下:
[1] 70 162 -
这表明变量范围从 70 到 162。由于它分布更广,我们应该预期方差更高,我们确实通过执行以下代码找到了这一点:
var(wine$magnesium)输出如下:
[1] 203.9893 -
要计算协方差,执行以下代码:
cov(wine$alcohol,wine$magnesium)输出如下:
[1] 3.139878 -
在步骤 4中,我们发现酒精和镁变量的协方差约为 3.14。请注意,协方差是对称的,所以 X 与 Y 的协方差与 Y 与 X 的协方差相同。你可以通过尝试以下代码来检查这一点:
cov(wine$magnesium,wine$alcohol)输出如下:
[1] 3.139878你会注意到它产生了相同的值。
-
一个变量的方差就是该变量与其自身的协方差。你可以通过运行以下代码来看到这一点:
var(wine$magnesium)输出如下:
[1] 203.9893通过执行以下代码,你会得到相同的输出:
cov(wine$magnesium,wine$magnesium)输出如下:
[1] 203.9893
协方差矩阵是一个方阵,其中每个条目都是一个方差或协方差。要构建协方差矩阵,首先我们必须给我们的数据集中的每个变量编号。在葡萄酒数据集中,我们可以根据列列表中的顺序给每个变量一个编号。因此,酒精将是变量 1,苹果酸将是变量 2,依此类推。
注意
记住,你可以在数据源网站archive.ics.uci.edu/ml/datasets/wine看到变量的列表。
在对变量排序后,我们可以创建协方差矩阵。在这个矩阵中,我们说的是,“i-j 条目是变量 i 和变量 j 的协方差。”因此,第一行第二列的项目是 1-2 条目,它将等于第一个变量(酒精)与第二个变量(苹果酸)的协方差。由于协方差是一个对称操作,2-1 条目将与 1-2 条目相同。这意味着矩阵本身将是对称的——每个条目都与主对角线另一侧镜像位置的条目相同。
协方差矩阵主对角线上的条目将是方差而不是协方差。例如,矩阵的 3-3 位置的条目将是变量 3 与变量 3 的协方差——这是变量与其自身的协方差,这也是说它是变量方差的一种方式。
特征向量和特征值
当我们有一个如协方差矩阵这样的方阵时,我们可以计算一些特殊的向量,称为特征向量。每个特征向量都有一个与之相关的值,称为特征值。关于特征向量和特征值的讨论可以轻易填满一本书。对我们来说,关于特征向量最重要的知道是它们表达了数据中最大方差的方向。关于特征值最重要的知道是它们表明哪些特征向量是最重要的。
PCA 的概念
PCA 是一种基于前面复习中描述的线性代数主题的强大降维技术。
为了完成 PCA,我们将取我们数据的协方差矩阵,然后找到它的特征向量。协方差矩阵的特征向量被称为主成分。主成分使我们能够用不同的术语和不同的维度重新表达数据。
我们将使用本章开头探索的与葡萄酒相关的数据集。回想一下,葡萄酒数据集有 13 个维度,这些维度测量了特定葡萄酒的特定化学属性。该数据集中的一项观测值由 13 个数字组成——每个维度一个。
主成分分析(PCA)使数据能够以不同的术语重新表达。葡萄酒数据集的协方差矩阵将包含 13 个特征向量。我们可以将这些特征向量解释为 13 个新的维度——我们将在接下来的练习中看到如何做到这一点。本质上,我们将能够用我们通过 PCA 发现的新维度完全描述每个观测值。
更重要的是,PCA 使我们能够进行降维。我们不必用特征向量定义的 13 个新维度重新表示数据,而只需选择这 13 个新维度中最重要的 12 个,并用这 12 个维度来表示数据,而不是原来的 13 个维度。PCA 使得选择最重要的维度变得容易,因为每个特征向量的重要性是通过其对应的特征值来衡量的。以下练习将更详细地说明如何做到这一点。
在 PCA 过程中,我们将创建一种新的图表类型,称为散点图。散点图是一个简单的线段图,显示了矩阵的特征值,按从高到低的顺序排列,以指示它们相关特征向量的相对重要性。
散点图显示了矩阵的特征值,按从大到小的顺序绘制。我们将使用散点图来决定哪些特征向量(即哪些维度)是最重要的。
PCA 可能听起来很难,它基于一些可能对你来说是新术语和想法,但实际上在 R 中实现相对简单。
练习 27:执行主成分分析(PCA)
如果我们有一个协方差矩阵,我们就准备好执行 PCA 了。在这种情况下,我们将使用本章前面探索过的葡萄酒数据集。我们的目标是进行降维——用比原始数据集更少的维度来表示葡萄酒数据集。这个练习建立在练习 26,“在葡萄酒数据集上检查方差和协方差”的基础上:
-
首先,加载本章前面使用的相同
wine数据集。作为第一步,我们将从葡萄酒数据集中删除class列。我们这样做是因为class不是葡萄酒的化学属性,而是一个标签,我们感兴趣的是研究葡萄酒的化学属性。我们可以按照以下方式删除此列:wine_attributes<-wine[,2:14] -
我们可以按照以下方式获取这个较小矩阵的协方差矩阵:
wine_cov<-cov(wine_attributes) -
接下来,我们将使用 R 中的一个函数
eigen。这个函数计算称为特征向量的特殊向量,以及称为特征值的特殊值。我们可以将其应用于我们的协方差矩阵,如下所示:wine_eigen<-eigen(wine_cov) -
现在,我们可以查看我们找到的特征向量:
print(wine_eigen$vectors)输出如下:
![图 4.10:葡萄酒的特征向量]()
图 4.10:葡萄酒的特征向量
-
R 已经将特征向量编译成一个与我们的原始协方差矩阵大小相同的方阵。这个新矩阵的每一列都是协方差矩阵的一个特征向量。如果我们查看我们找到的特征值,我们可以看到每个特征向量的相对重要性。执行以下命令来查看特征值:
print(wine_eigen$values)输出如下:
![图 4.11:葡萄酒的特征值]()
图 4.11:葡萄酒的特征值
-
我们实际上已经完成了我们的 PCA。协方差矩阵的特征向量被称为数据的特征值。让我们看看第一个:
print(wine_eigen$vectors[,1])输出如下:
![图 4.12:这个第一个特征向量表示了原始维度的线性组合。]()
图 4.12:这个第一个特征向量表示了原始维度的线性组合。
我们可以这样理解我们的第一个主成分:
主成分 1 = -0.0016592647 * 酒精 + 0.0006810156 * 苹果酸 -0.0001949057 * 灰分 + 0.0046713006 * 碱度 -0.0178680075 * 镁 - 0.0009898297 * 酚 -0.0015672883 * 黄烷醇 +0.0001230867 * 非酚 -0.0006006078 * 白藜芦醇 -0.0023271432 * 颜色 -0.0001713800 * 色调 -0.0007049316 * OD280 -0.9998229365 * 脯氨酸
因此,特征向量的每个元素都是这个方程中的一个系数,用于生成一个新的主成分。主成分是原始维度的线性组合。我们可以将每个主成分用作新的维度。所以,我们不必通过说“它有 14.23 的酒精测量值,1.71 的苹果酸测量值……”等等来描述一个观察结果,我们可以通过说类似“它有 5.62 的主成分 1 测量值,9.19 的主成分 2 测量值……”等等来描述它。
这个练习最重要的结果是wine_eigen$vectors和wine_eigen$values对象。
任何降维技术都意味着我们必须在数据集中丢失一些编码的信息。这是不可避免的:一个数字永远不能完全表达出 13 个数字所表达的一切。PCA 的好处是它保证了这是降维最有效的方法——通过用主成分来表示数据,我们丢失了尽可能少的信息。
在接下来的练习中,我们将讨论如何转换数据以实现降维。
练习 28:使用 PCA 进行降维
这个练习是前一个练习的延续——它将使用相同的数据和相同的矩阵以及我们之前计算的特征向量:
-
记住,我们协方差矩阵的每个特征向量都告诉我们一个可以用来总结数据的 13 个葡萄酒属性的线性组合。在这种情况下,第一个特征向量告诉我们我们可以这样转换数据:
neigen<-1 transformed<-t(t(as.matrix(wine_eigen$vectors[,1:neigen])) %*% t(as.matrix(wine_attributes)))在这里,我们指定了若干个特征向量(1),并且将我们的原始数据集乘以这个数量的特征向量,创建了一个用这个特征向量或我们的第一个主成分表示的转换后的数据集。
-
我们可以这样查看我们转换后的数据集的一部分:
print(head(transformed))这将给出以下输出:
![图 4.13:转换后的数据集]()
图 4.13:转换后的数据集
在这里,我们有一个一维数据集,它只使用一个数字来描述每个观测值。因此,我们说第一个观测的葡萄酒在主成分 1 上的得分为-1067.0557。我们已经完成了降维。
-
我们可以通过以下乘法进行数据集的部分恢复:
restored<- t(as.matrix(wine_eigen$vectors[,1:neigen]) %*% t(as.matrix(transformed)))这应该可以恢复我们的原始数据集。
注意
由于降维总是丢失一些数据中编码的原始信息,因此它不会是一个完美的恢复。
-
我们可以通过以下方式测试我们的变换是否导致了数据的准确重建:
print(mean(abs(wine_attributes[,13]-restored[,13])))输出如下:
[1] 1.466919在这种情况下,错误相当小,这表明我们在恢复数据方面相当成功。
-
我们可以使用任意数量的维度进行降维。通常,我们可以通过生成以下所示的碎石图来确定在变换中应使用多少维度:
plot(wine_eigen$values,type='o')在这种情况下,我们的碎石图如下所示:

图 4.14:显示协方差矩阵特征值的碎石图
为了决定使用多少维度进行降维,我们可以查看这个碎石图,并选择一个与相对较高的特征值数量相对应的维度数。
我们可以看到,第一个特征值远远是最高的,因此第一个特征向量也是最重要的一个,它告诉我们第一个主成分是最重要的维度。在这种情况下,将数据集简化为一维数据集是非常合适的。
你刚刚对一个协方差矩阵进行了 PCA。
活动 10:对新的数据集执行 PCA 和市场篮子分析
在接下来的活动中,你将加载一个新的数据集,然后你将对它执行 PCA 和市场篮子分析。该活动将涵盖这两个程序的所有主要步骤,包括所需的数据准备。我们将使用的数据集来自对马萨诸塞州波士顿周边地区的社区所进行的研究,它包含了许多社区的属性,包括税率、房产价值和当地人口的人口统计信息。
对于这个活动,使用"Boston"数据集,可以在 R 中运行以下代码:
library(MASS)
data(Boston)
这些步骤将帮助你完成活动:
-
通过以下方式将所有变量转换为虚拟变量:对于每个变量,创建一个新的变量,如果它等于或高于该变量的中位数,则等于 1,如果它低于该变量的中位数,则等于 0。创建另一个新的变量,它是这个变量的补数:在之前创建的虚拟变量中,每个 0 都是 1,每个 1 都是 0。将所有虚拟变量保存到一个名为
Bostondummy的新数据集中。 -
找到原始数据的所有特征向量和特征值
-
创建该数据的特征值散点图。如何解释这个散点图?
-
尝试仅使用少数几个主成分来*似此数据。你的*似与原始数据有多接*?
-
使用你在步骤 1中创建的虚拟变量,通过找到值在超过 10%的行中为 1 的所有变量来进行市场篮子分析的第一遍。
-
通过找到在数据中有超过 10%支持的所有变量的组合来进行市场篮子分析的第二次遍历。
-
完成市场篮子分析,直到三项篮子。
预期输出:此活动的最重要的输出是数据集的主成分,以及从市场篮子分析中获得的三项规则。主成分在活动的第二步解决方案中获得,当我们创建Boston_eigen时,我们可以运行print(Boston_eigen$vectors)命令来查看主成分,如下所示:

图 4.15:原始数据的主成分
市场篮子分析的三项规则在活动的步骤 14的解决方案中获得,当我们运行控制台中的print(head(thirdpass_conf))时,我们可以看到最终结果:

图 4.16:市场篮子分析的三项规则
注意
此活动的解决方案可以在第 222 页找到。
摘要
在本章中,我们讨论了数据维度的概念。我们探讨了为什么降低数据的维度可能是有用的,并强调了降维过程可以揭示关于数据潜在结构的重要真相。我们介绍了两种重要的降维方法。我们讨论的第一种方法是市场篮子分析。这种方法对于从复杂数据中生成关联规则很有用,并且可以用于其命名的用例(分析购物篮)或广泛的其它应用(例如分析调查响应的聚类)。我们还讨论了 PCA,这是一种用其维度的线性组合来描述数据的方法。PCA 使用一些线性代数工具很容易执行,并提供了一种简单的方法来*似甚至非常复杂的数据。
在下一章中,我们将探讨不同的数据比较方法。
第六章:第五章
数据比较方法
学习目标
到本章结束时,你将能够:
-
创建数据哈希
-
创建图像签名
-
比较图像数据集
-
执行因子分析以隔离潜在变量
-
使用因子分析比较调查和其他数据集
在本章中,我们将探讨不同的数据比较方法。
简介
无监督学习关注于分析数据的结构以得出有用的结论。在本章中,我们将探讨使我们能够利用数据结构来比较数据集的方法。我们将重点研究的方法包括哈希函数、分析签名和潜在变量模型。
哈希函数
假设你想把一个 R 脚本发送给你的朋友。然而,你和你的朋友在文件上遇到了技术问题——也许你们的电脑被恶意软件感染了,或者也许有黑客正在篡改你的文件。所以,你需要一种方法来确保你的脚本在发送给朋友时是完整的,没有被损坏或更改。检查文件是否完整的一种方法就是使用哈希函数。
哈希函数可以为数据创建类似指纹的东西。我们所说的指纹是指一种小而易于检查的东西,使我们能够验证数据是否具有我们认为是其身份的东西。因此,在你创建想要发送的脚本之后,你将对脚本应用哈希函数并获取其指纹。然后,你的朋友可以在收到文件后使用相同的哈希函数对文件进行处理,并确保指纹匹配。如果发送的文件指纹与接收到的文件指纹匹配,那么这两个文件应该是相同的,这意味着文件是完整发送的。以下练习展示了如何创建和使用一个简单的哈希函数。
练习 29:创建和使用哈希函数
在这个练习中,我们将创建和使用一个哈希函数:
-
指定需要使用哈希函数的数据。我们一直在探讨你想发送的 R 脚本场景。以下是一个简单的 R 脚本示例:
string_to_hash<-"print('Take the cake')"在这里,我们有一个打印字符串
Take the cake的脚本。我们将其保存为名为string_to_hash的变量。 -
指定可能的哈希值总数。我们的目标是为我们脚本创建一个指纹。我们需要指定我们将允许存在的指纹总数。我们希望指定一个足够低以便于操作,但又足够高以至于不同脚本偶然具有相同指纹的可能性不大的数字。在这里,我们将使用 10,000:
total_possible_hashes<-10000 -
将脚本(目前是一个字符串)转换为数值。哈希函数通常是算术性的,因此我们需要用数字而不是字符串来工作。幸运的是,R 有一个内置函数可以为我们完成这项工作:
numeric<-utf8ToInt(string_to_hash)这已经将我们的脚本中的每个字符转换为整数,基于每个字符在 UTF-8 编码方案中的编码。我们可以通过打印到控制台来查看结果:
print(numeric)输出如下:
[1] 112 114 105 110 116 40 39 84 97 107 101 32 116 104 101 32 99 97 107[20] 101 39 41 -
应用我们的散列函数。我们将使用以下函数来生成我们的最终散列,或者说,脚本的指纹:
hash<-sum((numeric+123)²) %% total_possible_hashes在 R 中运行这一行后,我们发现
hash的最终值是 2702。由于我们使用了模运算符(在 R 中为%%),hash的值将始终在 0 到 10,000 之间。
我们用来将数值向量转换为最终散列值的简单函数并不是唯一的散列函数。专家们设计了许多具有不同复杂程度的此类函数。好的散列函数应该具有许多属性,其中之一是最重要的属性是抗碰撞性,这意味着很难找到两个产生相同散列值的数据集。
练习 30:验证我们的散列函数
在这个练习中,我们将验证我们的散列函数是否使我们能够有效地比较不同的数据,通过检查不同的信息产生不同的散列值。这个练习的结果将是不同信息的散列函数,我们将比较以验证不同的信息产生不同的散列值:
-
创建一个执行散列的函数。我们将把前面练习中引入的代码组合到一个函数中:
get_hash<-function(string_to_hash, total_possible_hashes){ numeric<-utf8ToInt(string_to_hash) hash<-sum((numeric+123)²) %% total_possible_hashes return(hash) }这个函数接收我们想要散列的字符串和可能的散列总数,然后应用我们在上一个练习中使用的相同的散列计算来计算散列值,并返回该散列值。
-
比较不同输入的散列值。我们可以如下比较来自不同输入的散列值:
script_1<-"print('Take the cake')" script_2<-"print('Make the cake')" script_3<-"print('Take the rake')" script_4<-"print('Take the towel')"在这里,我们有四个不同的字符串,表达不同的信息。我们可以看到它们的散列值如下:
print(get_hash(script_1,10000)) print(get_hash(script_2,10000)) print(get_hash(script_3,10000)) print(get_hash(script_4,10000))第一个脚本返回散列值 2702,正如我们在前面的练习中找到的那样。第二个脚本,尽管它只与第一个脚本有一个字符不同,返回的散列值是 9853,第三个脚本,也只与第一个脚本有一个字符不同,返回的散列值是 9587。最后一个脚本返回的散列值是 5920。尽管这四个脚本有相当大的相似性,但它们有不同的指纹,我们可以使用这些指纹来比较和区分它们。
这些散列对于您和您的消息接收者来说很有用,可以用来验证您的脚本在发送过程中未被篡改。当您发送脚本时,您可以告诉您的朋友确保脚本的散列值是 2702。如果您的朋友收到的脚本散列值不是 2702,那么您的朋友可以得出结论,脚本在发送和接收过程中被篡改了。如果您的朋友能够可靠地检测文件是否损坏,那么您可以避免传播恶意软件或与您的朋友因误解而争吵。
在线分发的软件有时会附带一个用户可以用来检查文件损坏的哈希值。为此,专业人士使用比前面练习中简单函数更高级的哈希函数。专业人士使用的其中一个哈希函数称为 MD5,在 R 中使用digest包可以非常容易地应用它:
install.packages('digest')
library(digest)
print(digest(string_to_hash,algo='md5'))
输出如下:
[1] "a3d9d1d7037a02d01526bfe25d1b7126"
在这里,我们可以看到我们简单 R 脚本的 MD5 哈希值。您可以自由尝试其他数据的 MD5 哈希值,以比较结果。
分析签名
在第四章,降维中,我们讨论了降维——这些方法使我们能够以给我们数据洞察力的方式简洁地表达数据。之前讨论的哈希函数是另一种实现降维的方法。哈希函数在许多用途中都很有用,包括我们讨论的文件验证用例。在那个场景中,我们感兴趣的是确定两个脚本是否完全相同。即使数据有细微的差异,例如将“take”一词改为“make”,也可能完全破坏预期的信息,因此需要精确性。
在其他情况下,我们可能希望在不需要比较的两个数据集具有完全相同性的情况下,进行有意义的比较。考虑检测版权侵犯的情况。假设一个网站托管了来自其用户的图像。它想要确保用户没有提交受版权保护的图像。因此,每次它收到上传时,它都希望检查该上传是否与大型版权图像数据库中的任何图像相同。
仅检查图像是否完全相同是不够的,因为一些不择手段的上传者可能会进行细微的修改并仍然尝试上传。例如,他们可能会改变一个像素的颜色,或者非常轻微地裁剪图像,或者将其压缩到比原始图像大或小。即使有这些细微的修改,图像仍然会违反版权法。一个检查完全相同性的哈希函数将无法识别这些版权侵犯。因此,图像托管网站将想要检查代表两张图片的数据中是否存在任何实质上相似的底层结构。
我们提到,哈希函数创建了一种类似于数据指纹的东西。指纹应该在每次观察时都完全相同,即使两个指纹在大多数方面相似,除非它们完全匹配,否则我们不会认为它们彼此匹配。
在这种情况下,我们需要的更像是一个签名,而不是指纹。每次您签名时,您的签名应该看起来或多或少相同。但是,即使是同一个人在尝试匹配之前的签名时,每个签名之间也会有细微的差异。为了验证签名是否匹配,我们需要检查实质性的相似性,而不是完美的身份。我们在这里提供的代码将展示如何将任何大小图像编码为一个小巧且健壮的编码,这允许在数据集之间进行快速且准确的*似比较。这种编码图像数据的方法可以被称为创建分析签名。
我们创建图像签名的步骤如下。首先,我们将图像分成一个 10x10 的网格。然后,我们将测量网格每个部分的亮度。之后,我们将比较每个部分的亮度与其相邻部分的亮度。最终的签名将包含一个向量,该向量包含每个网格部分与其每个相邻部分的比较。这种方法是由 Wong、Bern 和 Goldberg 发明的,并在一篇名为《任何类型图像的图像签名》的论文中发表。
在我们创建分析签名之前,我们需要进行一些数据准备,如下一练习所述。
练习 31:为创建图像分析签名进行数据准备
在这个练习中,我们将为阿拉莫的照片创建分析签名进行数据准备。
注意
对于所有需要导入外部 CSV 文件或图像的练习和活动,请转到RStudio-> 会话-> 设置工作目录-> 到源文件位置。您可以在控制台中看到路径已自动设置。
-
首先,我们需要配置 R 以能够读取并处理我们的图像数据。我们需要安装
imager包。您可以在 R 控制台中执行install.packages('imager')来安装此包,然后您可以通过在控制台中运行library('imager')来加载它。 -
接下来,我们需要读取数据。在我们的例子中,我们将使用这张阿拉莫的照片:
![]()
图 5.1:阿拉莫图像
首先,从
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/Lesson05/Exercise31/alamo.jpg 下载到您的计算机上,并将其保存为alamo.jpg。确保它保存在 R 的工作目录中。如果它不在 R 的工作目录中,那么请使用setwd()函数更改 R 的工作目录。然后,您可以将此图像加载到名为im(代表图像)的变量中,如下所示:filepath<-'alamo.jpg' im <- imager::load.image(file =filepath)我们将要探索的其余代码将使用这个名为
im的图像。在这里,我们已经将阿拉莫的照片加载到im中。然而,你可以通过将图像保存到你的工作目录并在filepath变量中指定其路径来运行其余的代码。 -
我们正在开发的签名是为了用于灰度图像。因此,我们将使用
imager包中的函数将此图像转换为灰度:im<-imager::rm.alpha(im) im<-imager::grayscale(im) im<-imager::imsplit(im,axis = "x", nb = 10)这段代码的第二行是将图像转换为灰度。最后一行将图像分割成 10 等份。
-
以下代码创建了一个空矩阵,我们将用有关我们 10x10 网格每个部分的详细信息来填充它:
matrix <- matrix(nrow = 10, ncol = 10)接下来,我们将运行以下循环。这个循环的第一行使用了
imsplit命令。这个命令之前也被用来将 x 轴分成 10 等份。这次,对于 x 轴的每个 10 等份,我们将在 y 轴上进行分割,也将它分成 10 等份:for (i in 1:10) { is <- imager::imsplit(im = im[[i]], axis = "y", nb = 10) for (j in 1:10) { matrix[j,i] <- mean(is[[j]]) } }在沿 y 轴分割后,矩阵通过
mean(is[[j]])更新。这是所选部分的*均亮度的度量。这段代码的结果是一个 10x10 的矩阵,其中
i-j元素包含原始照片i-j部分的*均亮度。如果你打印这个矩阵,你可以看到照片每个部分的亮度数字:
print(matrix)输出应该看起来像以下这样:

图 5.2:输出矩阵的截图
你可以将这些亮度数字与原始照片的外观进行比较。
我们可以在这里停止,因为我们已经生成了一个复杂数据集的压缩编码。然而,我们可以采取一些进一步的步骤来使这个编码更有用。
我们可以做的事情之一是创建一个brightnesscomparison函数。这个函数的目的是比较图像中两个不同部分的相对亮度。最终,我们将比较我们分析的每张图像的所有不同部分。我们的最终指纹将包含许多这样的亮度比较。这个练习的目的是创建一个亮度比较函数,这将最终使我们能够创建最终的指纹。
请注意,这个练习是在上一个练习的基础上构建的,这意味着你应该在运行这个练习的代码之前运行上一个练习中的所有代码。
练习 32:创建亮度比较函数
-
在这个函数中,我们传递两个参数,
x和y:每个参数代表图片中特定部分的亮度。如果x和y相当相似(小于 10%的差异),那么我们可以说它们基本上是相同的,我们返回 0,表示亮度差异大约为 0。如果x比y大 10%以上,我们返回 1,表示x比y亮,如果x比y小 10%以上,我们返回-1,表示x比y暗。 -
创建亮度比较函数。
brightnesscomparison函数的代码如下:brightnesscomparison<-function(x,y){ compared<-0 if(abs(x/y-1)>0.1){ if(x>y){ compared<-1 } if(x<y){ compared<-(-1) } } return(compared) } -
我们可以使用这个函数来比较我们为图片形成的 10x10 网格的两个部分。例如,为了找到与直接左侧部分的亮度比较,我们可以执行以下代码:
i<-5 j<-5 left<-brightnesscomparison(matrix[i,j-1],matrix[i,j])在这里,我们查看矩阵的第 5 行和第 5 列。我们将这个部分与它左侧直接的部分进行比较——第 5 行和第 4 列,我们通过指定
j-1来访问这个部分。 -
使用亮度比较函数来比较图像部分与其上面的邻居。我们可以进行类似的操作来比较这个部分与其上面的部分:
i<-5 j<-5 top<-brightnesscomparison(matrix[i-1,j],matrix[i,j])
在这里,top 是第五部分与它上面紧邻的节点的亮度比较,我们通过指定 i-1 来访问这个节点。
这个练习的重要输出是 top 和 left 的值,它们都是图像部分与其他相邻部分的比较。在这种情况下,left 等于零,意味着我们选择的图像部分的亮度与左侧的图像部分大致相同。同样,top 等于 1,意味着我们选择的节点的直接上方部分比我们选择的节点亮度更高。
在下一个练习中,我们将创建一个 neighborcomparison 函数。这个函数将比较我们 10x10 网格中的每个节点的亮度,与它的邻居进行比较。这些邻居包括我们刚才比较过的左侧邻居,以及上面的邻居。总的来说,我们图片的每个部分(顶部、底部、左侧、右侧、左上、右上、左下和右下)都有八个邻居。我们想要这个邻居比较函数的原因是,它将使我们很容易得到最终的解析特征。
请注意,这个练习建立在之前的练习之上,你应该在运行这段代码之前运行所有之前的代码。
练习 33:创建一个函数来比较图像部分与所有相邻部分
在这个练习中,我们将创建一个 neighborcomparison 函数来比较图像部分与其他所有相邻部分。为此,执行以下步骤:
-
创建一个函数,比较图像部分与其左侧的邻居。我们在之前的练习中做过这个操作。对于任何图像部分,我们可以比较其亮度与其左侧邻居的亮度,如下所示:
i<-5 j<-5 left<-brightnesscomparison(matrix[i,j-1],matrix[i,j]) -
创建一个函数,比较图像部分与其上面的邻居。我们在之前的练习中做过这个操作。对于任何图像部分,我们可以比较其亮度与其上方邻居的亮度,如下所示:
i<-5 j<-5 top<-brightnesscomparison(matrix[i-1,j],matrix[i,j]) -
如果你查看 Step 1和 Step 2,你可以开始注意到这些邻居比较中的模式。要比较图像部分与其左侧的部分,我们需要访问矩阵的
j-1索引部分。要比较图像部分与其右侧的部分,我们需要访问矩阵的j+1索引部分。要比较图像部分与其上方的部分,我们需要访问矩阵的i-1索引部分。要比较图像部分与其下方的部分,我们需要访问矩阵的i+1索引部分。因此,我们将有每个图像部分与其上方、下方、左侧和右侧的每个邻居的比较。以下代码显示了我们将进行的比较,除了在 Step 1和 Step 2中进行的顶部和左侧比较:
i<-5 j<-5 top_left<-brightnesscomparison(matrix[i-1,j-1], matrix[i,j]) bottom_left<-brightnesscomparison(matrix[i+1,j-1],matrix[i,j]) top_right<-brightnesscomparison(matrix[i-1,j+1],matrix[i,j]) right<-brightnesscomparison(matrix[i,j+1],matrix[i,j]) bottom_right<-brightnesscomparison(matrix[i+1,j+1],matrix[i,j]) bottom<-brightnesscomparison(matrix[i+1,j],matrix[i,j]) -
初始化一个向量,该向量将包含部分与其每个邻居的最终比较:
comparison<-NULL我们将使用这个
comparison向量来存储我们最终生成的所有邻居比较。它将包含图像部分与其每个邻居的比较。 -
在步骤 1-4 中,我们展示了邻居比较函数的各个部分。在这个步骤中,我们将它们组合起来。你可以看到的邻居比较函数接受一个图像矩阵作为参数,并且还有
i和j值,指定我们正在关注的图像矩阵的部分。该函数使用我们为top和left比较编写的代码,并为其他邻居添加了其他比较,例如top_left,它比较图像亮度级别与上方左侧部分的图像亮度。总的来说,每个图像部分应该有八个邻居:左上、上、右上、左、右、左下、下和右下。在这个步骤中,我们将进行这八个比较并将它们存储在comparison向量中。最后,有一个return语句,它返回所有比较。这里是我们可以使用来获取所有邻居比较的函数:
neighborcomparison<-function(mat,i,j){ comparison<-NULL top_left<-0 if(i>1 & j>1){ top_left<-brightnesscomparison(mat[i-1,j-1],mat[i,j]) } left<-0 if(j>1){ left<-brightnesscomparison(mat[i,j-1],mat[i,j]) } bottom_left<-0 if(j>1 & i<nrow(mat)){ bottom_left<-brightnesscomparison(mat[i+1,j-1],mat[i,j]) } top_right<-0 if(i>1 & j<nrow(mat)){ top_right<-brightnesscomparison(mat[i-1,j+1],mat[i,j]) } right<-0 if(j<ncol(mat)){ right<-brightnesscomparison(mat[i,j+1],mat[i,j]) } bottom_right<-0 if(i<nrow(mat) & j<ncol(mat)){ bottom_right<-brightnesscomparison(mat[i+1,j+1],mat[i,j]) } top<-0 if(i>1){ top<-brightnesscomparison(mat[i-1,j],mat[i,j]) } bottom<-0 if(i<nrow(mat)){ bottom<-brightnesscomparison(mat[i+1,j],mat[i,j]) } comparison<-c(top_left,left,bottom_left,bottom,bottom_right,right,top_right,top) return(comparison) }
此函数返回一个包含八个元素的向量:每个元素对应于我们网格中特定部分的邻居。你可能已经注意到,10x10 网格的一些部分似乎没有八个邻居。例如,10x10 网格的 1-1 元素下面有一个邻居,但没有上面的邻居。对于没有特定邻居的网格位置,我们说它们对该邻居的亮度比较为 0。这降低了创建和解释亮度比较的复杂性水*。
最终输出是一个名为comparison的向量,它包含图像部分与其八个邻居之间的亮度级别比较。
在下一个练习中,我们将完成创建我们的分析签名。每个图像的分析签名将包括每个图像部分与其八个邻居的比较。我们将使用两个嵌套的 for 循环来遍历我们 10x10 网格的每个部分。本练习的预期输出将是一个生成图像分析签名的函数。
练习 34:创建一个生成图像分析签名的函数
在这个练习中,我们将创建一个函数,用于为图像生成一个分析签名。为此,请执行以下步骤:
-
我们首先创建一个名为
signature的变量,并用NULL值初始化它:signature<-NULL当我们完成时,这个
signature变量将存储完整的签名。 -
现在,我们可以遍历我们的网格。对于网格的每个部分,我们向签名中添加八个新元素。我们添加的元素是我们之前介绍的
neighborcomparison函数的输出:for (i in 1:nrow(matrix)){ for (j in 1:ncol(matrix)){ signature<-c(signature,neighborcomparison(matrix,i,j)) } }我们可以通过在控制台中运行
print(signature)来查看我们的指纹是什么样的。它是一个包含 800 个值的向量,所有这些值都等于 0(表示相似的亮度或没有邻居)、1(表示某个区域比其邻居更亮)或 -1(表示某个区域比其邻居更暗)。 -
将 Step 1 和 Step 2 结合在一个函数中,该函数可以生成任何图像矩阵的签名:
get_signature<-function(matrix){ signature<-NULL for (i in 1:nrow(matrix)){ for (j in 1:ncol(matrix)){ signature<-c(signature,neighborcomparison(matrix,i,j)) } } return(signature) }此代码定义了一个名为
get_signature的函数,它使用来自 Step 1 和 Step 2 的代码来获取该签名。我们可以使用之前创建的图像矩阵来调用此函数。 -
由于我们稍后还将创建更多签名,我们将把这个签名保存到一个变量中,该变量指明了它代表的是什么。在这种情况下,我们将称之为
building_signature,因为它是一个建筑图像的签名。我们可以这样做:building_signature<-get_signature(matrix) building_signature输出如下:

图:5.3:building_signature 矩阵
存储在 building_signature 中的向量是这个练习的最终输出,也是我们在本章中一直试图开发的图像签名。
这个签名旨在类似于人类的亲笔签名:小巧且表面上与其他签名相似,但足够独特,使我们能够将其与数百万其他现有签名区分开来。
我们可以通过读取一个完全不同的图像并比较生成的签名来检查我们找到的签名解决方案的鲁棒性。这就是以下活动的场景。
活动 11:为人物照片创建图像签名
让我们尝试为这张图像创建一个图像指纹,这是一张伟大的豪尔赫·路易斯·博尔赫斯的照片。
要完成这个活动,您可以遵循本章中我们已经遵循的所有步骤。以下步骤将为您概述这个过程。请记住,在我们之前进行的图像签名练习中,我们使用了一个 10x10 的亮度测量矩阵。然而,10x10 的矩阵可能不适合某些情况,例如,如果我们处理的图像特别小,或者我们有数据存储限制,或者我们期望使用不同的矩阵大小可以获得更高的精度。因此,在以下活动中,我们也将使用 9x9 矩阵计算一个签名:
注意
这可以在任何给定的矩阵大小上执行。它可能是一个 5x5 的矩阵用于数据存储,或者是一个 20x20 的矩阵用于精确的签名。

图 5.4:豪尔赫·路易斯·博尔赫斯图像
这些步骤将帮助我们完成活动:
-
将图像加载到您的 R 工作目录中。将其保存到名为
im的变量中。 -
将您的图像转换为灰度并分成 100 个部分。
-
创建一个亮度值矩阵。
-
使用我们之前创建的
get_signature函数创建一个签名。将 Borges 图像的签名保存为borges_signature。注意
这个活动的解决方案可以在第 227 页找到。
这个活动的最终输出是 borges_signature 变量,它是 Borges 照片的分析签名。此外,我们还创建了 borges_signature_ninebynine 变量,它也是一个分析签名,但基于 9x9 而不是 10x10 的矩阵。我们可以在分析中使用它们中的任何一个,但我们将使用 borges_signature 变量。如果您已经完成了迄今为止的所有练习和活动,那么您应该有两个分析签名:一个名为 building_signature,另一个名为 borges_signature。
签名比较
接下来,我们可以比较这两个签名,看看它们是否将我们的不同图像映射到不同的签名值。
您可以使用以下一行 R 代码比较签名:
comparison<-mean(abs(borges_signature-building_signature))
这种比较计算了两个签名中每个元素之间的差的绝对值,然后计算这些值的*均值。如果两个签名完全相同,那么这个差值将为 0。comparison 的值越大,两个图像的差异就越大。
在这种情况下,comparison 的值为 0.644,这表明*均而言,相应的签名条目之间大约相差 0.644。对于值仅在 1 和 -1 之间变化的数据库来说,这种差异是显著的。因此,我们看到我们的签名创建方法为非常不同的图像创建了非常不同的签名,正如我们所期望的那样。
现在,我们可以计算一个与我们的原始图像非常相似但又不完全相同的图像的签名:

图 5.5:标记的阿拉莫图像
为了获得这张图像,我从一个原始的阿拉莫图像开始,并在四个地方添加了单词水印,模拟了有人可能对图像进行的修改。由于图像现在与原始图像不同,简单的版权检测软件可能会被这个水印欺骗。我们的分析签名方法不应该如此天真。我们将在以下活动中完成这项任务。
活动十二:为水印图像创建图像签名
在这个活动中,我们将为水印图像创建一个图像签名:
-
将图像加载到您的 R 工作目录中。将其保存到名为
im的变量中。 -
将您的图像转换为灰度并分成 100 个部分。
-
创建一个亮度值矩阵。
-
使用我们之前创建的
get_signature函数创建一个签名。将阿拉莫图像的签名保存为watermarked_signature。 -
将水印图像的签名与原始图像的签名进行比较,以确定签名方法是否能够区分图像。
输出将如下所示:

图 5.6:预期水印图像的签名
注意
本活动的解决方案可以在第 230 页找到。
为了检测版权侵权,我们可以计算数据库中每个受版权保护图像的签名。然后,对于每个新上传的图像,我们将新上传图像的签名与数据库中的签名进行比较。如果任何受版权保护的图像的签名与新上传的图像的签名相同或非常接*,我们将它们标记为潜在的匹配项,需要进一步调查。比较签名可能比比较原始图像快得多,并且它具有对水印等小变化具有鲁棒性的优点。
我们刚才执行签名方法是一种将数据编码以实现不同数据集之间比较的方法。有许多其他编码方法,包括一些使用神经网络的方法。每种编码方法都将具有其独特的特征,但它们都将共享一些共同特征:它们都将尝试返回压缩数据,以便于在不同数据集之间进行简单和准确的比较。
将其他无监督学习方法应用于分析签名
到目前为止,我们只使用了哈希和分析签名来比较两张图像或四个简短字符串。然而,可以应用于哈希或分析签名的无监督学习方法没有限制。为一系列图像创建分析签名可能是深入分析的第一步,而不是最后一步。在为一系列图像创建分析签名之后,我们可以尝试以下无监督学习方法:
-
聚类:我们可以将第一、二章节中讨论的任何聚类方法应用于由分析特征组成的数据库。这可能使我们能够找到所有倾向于彼此相似的一组图像,可能是因为它们是同一类型物体的照片。有关聚类方法的更多信息,请参阅第一、二章节。
-
异常检测:我们可以将第六章中描述的异常检测方法应用于由分析特征组成的数据库。这将使我们能够找到与数据集中其他图像非常不同的图像。
潜在变量模型 – 因子分析
本节将介绍潜在变量模型。潜在变量模型试图用少量隐藏或潜在的变量来表示数据。通过找到与数据集相对应的潜在变量,我们可以更好地理解数据,甚至可能理解数据来源或生成方式。
考虑到学生在各种不同课程中获得的分数,从数学到音乐、外语到化学。心理学家或教育工作者可能对使用这些数据更好地理解人类智力感兴趣。研究人员可能想要在数据中测试几种不同的智力理论,例如:
-
理论 1:有两种不同类型的智力,拥有一种类型的人将在一组课程中表现出色,而拥有另一种类型的人将在其他课程中表现出色。
-
理论 2:只有一种类型的智力,拥有它的人将在所有类型的课程中表现出色,而没有它的人则不会。
-
理论 3:一个人可能在一个或几个他们所学的课程标准下非常聪明,但在其他标准下并不聪明,每个人都会有一套不同的课程,他们在这些课程中表现出色。
这些理论中的每一个都表达了一个潜在变量的概念。数据只包含学生成绩,这可能是智力的表现,但不是智力本身的直接衡量标准。这些理论表达了不同类型的智力如何以潜在的方式影响成绩。即使我们不知道哪个理论是正确的,我们也可以使用无监督学习工具来理解数据的结构以及哪个理论最适合数据。
任何记得自己曾是学生的人可能都厌倦了别人评价他们的智力。因此,我们将使用一个稍微不同的例子。我们不会评价智力,而是评价个性。我们不会寻找左右脑,而是寻找人们个性的不同特征。但我们将采取相同的方法——使用潜在变量来识别复杂数据是否可以由少数几个因素解释。
为了为我们的分析做准备,我们需要安装和加载正确的包。下一个练习将涵盖如何加载因子分析所需的包。
练习 35:准备因子分析
在这个练习中,我们将为因子分析准备数据:
-
安装必要的包:
install.packages('psych') install.packages('GPArotation') install.packages('qgraph') -
然后,你可以运行以下行将它们加载到你的 R 工作空间中:
library(psych) library(GPArotation) library(qgraph) -
接下来,加载数据。我们将使用的数据是 500 份对名为“修订版 NEO 人格问卷”的人格测试的记录。该数据集包含在名为
qgraph的 R 包中。首先,将数据读入你的 R 工作空间。以下代码将允许你将其作为名为big5的变量访问:data(big5) -
你可以通过在 R 控制台中运行以下行来查看数据的顶部部分:
print(head(big5))输出如下:
![图 5.7:数据顶部部分]
![图 5.7:数据顶部部分]()
图 5.7:数据顶部部分
-
你可以通过以下方式查看你的数据行和列的数量:
print(nrow(big5))输出如下:
500要检查列,执行以下操作:
print(ncol(big5))输出如下:
240这份数据包含 500 行和 240 列。每一行是与一个调查受访者相关的完整记录。每一列记录了一个问题的答案。因此,有 500 名调查受访者每人回答了 240 个问题。每个答案都是数值型的,你可以看到以下回答的范围:
print(range(big5))输出如下:
[1] 1 5答案的范围是 1 到 5。
本练习的预期输出是一个已加载
big5数据的 R 工作空间。你可以确信数据已加载,因为当你控制台中运行print(range(big5))时,你会得到1 5作为输出。
在这个调查中,问题是通过向调查受访者展示一个问题并询问他们同意该答案的程度来提出的,其中 5 代表强烈同意,1 代表强烈不同意。这些陈述是关于一个人特定特征的说法,例如:
“我喜欢结识新朋友。”
“我有时会犯错误。”
“我喜欢修理东西。”
你可以想象,其中一些问题可能会测量相同的潜在“潜在”人格特质。例如,我们经常描述人们为好奇,从他们喜欢学习和尝试新事物的意义上来说。那些强烈同意“我喜欢结识新朋友”这一说法的人,如果这两个说法都在测量潜在的好奇心,那么他们很可能也会同意“我喜欢修理东西”这一说法。或者,可能是有些人具有对人们感兴趣的人格特质,而其他人则具有对事物感兴趣的人格特质。如果是这样,那些同意“我喜欢结识新朋友”的人就不太可能也同意“我喜欢修理东西”。在这里,因子分析的目的在于发现哪些问题对应于一个潜在的想法,并了解这些潜在想法是什么。
你可以看到,大多数列都以字母表中的一个字母以及一个数字开头。目前,你可以忽略这些标签,只需记住每个问题都有细微的差别,但其中许多是相似的。
我们现在准备好开始我们的潜在变量模型。在这种情况下,我们将执行因子分析。因子分析是一种强大且非常常见的方法,可以执行我们之前讨论过的潜在变量分析。因子分析假设存在某些潜在因子控制数据集,然后向我们展示这些因子是什么以及它们如何与我们的数据相关。
要开始我们的因子分析,我们需要指定我们要找多少个因子。有几种方法可以决定我们应该寻找多少个因子。目前,我们将从查看五个因子开始,因为这项数据被称为大五因子,然后我们稍后会检查是否其他数量的因子更好。
我们需要为我们新的数据创建一个相关矩阵。相关矩阵是一个矩阵,其中i-j项是存储在列i中的变量和存储在列j中的变量之间的相关系数。这与我们在第四章,降维中讨论的协方差矩阵类似。你可以按照以下方式创建这个数据的相关矩阵:
big_cor <- cor(big5)
现在,因子分析本身相当简单。我们可以通过使用fa命令来完成它,这是psych R 包的一部分:
solution <- fa(r = big_cor, nfactors = 5, rotate = "oblimin", fm = "pa")
你会注意到这个命令中有几个参数。首先,我们指定r=big_cor。在这种情况下,R 的psych包的作者决定使用小写r来指代因子分析中使用的协方差矩阵。下一个参数是nfactors=5。这指定了我们在因子分析中要寻找的因子数量。这次我们选择了五个因子,但稍后我们会看看使用更多或更少的因子。
最后两个参数对我们这里的目的是不太重要的。第一个参数说rotate="oblimin"。因子分析在向我们展示结果之前,在幕后对我们的数据进行旋转,这就是所谓的旋转。有许多技术可以用来完成这种幕后旋转,而oblimin是fa函数的作者选择作为默认的旋转方法。你可以自由地尝试其他旋转方法,但它们通常会产生实质上相似的结果。最后一个参数,就像旋转参数一样,指定了一个在幕后使用的方法。在这里,fm代表因子分解方法,pa代表主成分。你也可以尝试其他因子分解方法,但再次强调,它们应该会产生实质上相似的结果。
注意
你可以使用 R 中的?命令查找与任何其他命令相关的文档。在这种情况下,如果你对我们刚刚使用的fa命令感到好奇,你可以运行?fa来从你的 R 控制台加载文档。
现在,我们可以查看我们的因子分析输出:
print(solution)
输出如下:

图 5.8:输出的一部分
当我们这样做时,大部分输出是一个包含 240 行,标签为Standardized loadings的 DataFrame。这个数据框的每一行对应于我们原始数据框中的一列,例如N1。请记住,这些是我们数据来源的个性测试问题。这个数据框的前五行标签为PA1至PA5。这些对应于我们要找的五个因素。特定个性问题和特定因素的数量称为负载。例如,我们在数据框中有 0.54 的条目,对应于个性问题N1和因素PA1。我们说问题N1在因素PA1上的负载为 0.54。
我们可以将负载解释为对总分有贡献的因素。为了得到最终分数,你可以将每个特定负载乘以调查受访者对每个问题的回答,并将结果相加。用方程式表示,我们可以写成以下形式:
受访者 1 的因素 1 得分 =
(问题 1 上的因素 1 负载)*(受访者 1 对问题 1 的回答)+
(问题 2 上的因素 1 负载)*(受访者 1 对问题 2 的回答)+
...
(问题 240 上的因素 1 负载)*(受访者 1 对问题 240 的回答)
因此,如果一个特定问题的因素 1 负载很高,那么受访者对该问题的回答将对总因素 1 得分有较大的贡献。如果负载较低,那么该问题的回答对因素 1 得分的贡献就不那么大,换句话说,它对因素 1 的影响就不那么重要。这意味着每个负载都是衡量每个特定问题在因素测量中的重要程度的度量。
每个因素都有 240 个总负载——对应于个性调查中的每个问题。如果你查看负载矩阵,你可以看到许多问题在一个因素上有很大的负载,而在所有其他因素上的负载都很小(接*零)。研究人员经常试图根据每个因素的最高负载问题来假设每个因素的解释。
在我们的案例中,我们可以看到第一个因子,称为PA1,对第一个问题(标记为N1)有较高的负荷(0.54)。它还对第六个问题(N6)有相对较高的负荷(0.45),以及第十一题(N11)有较高的负荷(0.62)。在这里很容易看出一个模式——标记为N的问题往往对这个第一个因子有较高的负荷。结果证明,原始测试上的这些N问题都是为了衡量心理学家所说的“神经质”。神经质是一种基本的人格特质,使人们在面对困难时产生强烈的负面反应。调查中的N问题各不相同,但每个问题都是旨在衡量这种人格特质。我们在因子分析中发现,这个第一个因子往往对神经质问题有较高的负荷——因此我们可以称这个因子为“神经质因子”。
我们可以在其他负荷中看到类似的模式:
-
因素 2 似乎对“A”问题有较高的负荷,这些问题是用来衡量宜人性的。
-
因素 3 似乎对“E”问题有较高的负荷,这些问题是用来衡量外向性的。
-
因素 4 似乎对“C”问题有较高的负荷,这些问题是用来衡量尽责性的。
-
因素 5 似乎对“O”问题有较高的负荷,这些问题是用来衡量开放性的。
这些标签对应于“大五”人格理论,该理论认为这五种人格特质是人格最重要的最基本方面。在这种情况下,我们的五因素分析产生了与数据集中预先存在的标签相匹配的因子负荷模式。然而,我们不需要标记的问题来从因子分析中学习。如果我们获得了未标记的问题数据,我们仍然可以运行因子分析,并仍然可以在特定问题的特定因子负荷中找到模式。在找到这些模式后,我们必须仔细查看在相同因子上有高负荷的问题,并试图找出这些问题的共同之处。
因子分析背后的线性代数
因子分析是一种强大且灵活的方法,可以用多种方式使用。在以下练习中,我们将更改我们使用的因子分析命令的一些细节,以便更好地了解因子分析是如何工作的。
请注意:以下练习基于之前的因子分析代码。您必须运行之前突出显示的代码,特别是big_cor <- cor(big5),才能成功运行以下练习中的代码。
练习 36:使用因子分析的进一步探索
在这个练习中,我们将详细探讨因子分析:
-
我们可以改变
fa函数中的几个参数以获得不同的结果。记住上次我们运行以下代码来创建我们的解决方案:solution <- fa(r = big_cor, nfactors = 5, rotate = "oblimin", fm = "pa")这次,我们可以更改
fa函数的一些参数,如下所示:solution <- fa(r = big_cor, nfactors = 3, rotate = "varimax", fm = "minres")在这种情况下,我们将旋转方法更改为
varimax,将因素化方法更改为minres。这些都是对函数背后使用的方法的改变。对我们来说最重要的是,我们将因素数量(nfactors)从 5 更改为 3。检查此模型中的因素载荷:print(solution)输出如下:
![Figure 5.9: Section of the output]()
图 5.9:输出的一部分
你也可以尝试在这些载荷中寻找模式。如果你发现有三个、四个或其他一些数量的载荷存在一个显著的载荷模式,你甚至可能拥有一个新的个性心理学理论。
-
确定在因子分析中要使用的因素数量。在这个时候,一个自然的问题是要问:我们应该如何选择我们寻找的因素数量?最简单的答案是,我们可以使用 R 的
psych包中的另一个命令,称为fa.parallel。我们可以用以下方式运行此命令:parallel <- fa.parallel(big5, fm = 'minres', fa = 'fa')再次,我们对函数背后的行为做出了选择。你可以尝试不同的
fm和fa参数的选择,但你应该每次都看到实质上相似的结果。fa.parallel命令的一个输出是以下斯克里普图:


图 5.10:*行分析斯克里普图
我们在第四章,降维中讨论了斯克里普图。标记为主成分特征值的 y 轴显示了每个因素在解释我们模型方差中的重要性。在大多数因子分析中,前几个因素具有相对较高的特征值,然后特征值急剧下降,并出现一个长长的*台期。这种急剧下降和长长的*台期共同形成了一个类似肘部的图像。因子分析中的常见做法是选择一个接*这种模式形成的肘部的因素数量。心理学家们共同选择了五个作为通常用于个性调查表的因素数量。你可以检查这个斯克里普图,看看你是否认为这是正确的数量,并且可以自由尝试使用不同数量的因素进行因子分析。
我们刚刚进行的练习的最终重要结果是斯克里普图。
你可能已经注意到,因子分析似乎与主成分分析有一些共同之处。特别是,两者都依赖于绘制特征值作为衡量不同向量重要性的方法之一的斯克里普图。
本书中对因子分析和主成分分析之间的全面比较超出了本书的范围。只需说明的是,它们具有相似的线性代数基础:两者都是为了创建协方差矩阵的*似,但每种方法实现这一点的途径略有不同。我们在因子分析中找到的因子与我们在主成分分析中找到的主成分类似。我们鼓励您尝试在相同的数据集上使用因子分析和主成分分析,并比较结果。结果通常非常相似,但并非完全相同。在大多数实际应用中,可以使用任何一种方法——您使用的具体方法将取决于您自己的偏好以及您对哪种方法最适合当前问题的判断。
活动 13:执行因子分析
在这个活动中,我们将使用因子分析对新的数据集进行分析。您可以在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson05/Activity13找到数据集。
注意
此数据集来自 UCI 机器学习仓库。您可以在archive.ics.uci.edu/ml/machine-learning-databases/00484/tripadvisor_review.csv找到数据集。我们已经下载了文件并将其保存到github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson05/Activity13/factor.csv。
此数据集由 Renjith、Sreekumar 和 Jathavedan 编制,可在 UCI 机器学习仓库中找到。此数据集包含有关个人对旅游目的地所写评论的信息。每一行对应一个特定的唯一用户,总共有 980 个用户。列对应旅游景点的类别。例如,第二列记录每个用户的艺术画廊*均评分,第三列记录每个用户的舞厅*均评分。共有 10 个旅游景点类别。您可以在archive.ics.uci.edu/ml/datasets/Travel+Reviews找到关于每列记录的文档。
通过因子分析,我们将寻求确定不同类别用户评分之间的关系。例如,果汁吧(第 4 列)和餐厅(第 5 列)的用户评分可能相似,因为它们都由相同的潜在因素决定——用户对食物的兴趣。因子分析将尝试找到这些控制用户评分的潜在因素。我们可以遵循以下步骤进行因子分析:
-
下载数据并将其读入 R。
-
加载
psych包。 -
选择记录用户评分的列子集。
-
为数据创建一个相关矩阵。
-
确定应使用的因子数量。
-
使用
fa命令进行因子分析。 -
检查并解释因子分析的结果。
输出应类似于以下内容:
![图 5.11:因子分析预期结果
![img/C12628_05_12.jpg]
图 5.11:因子分析预期结果
注意
本活动的解决方案可以在第 231 页找到。
摘要
在本章中,我们讨论了与数据比较相关的话题。我们首先讨论了数据指纹的概念。为了说明数据指纹,我们引入了一个可以将任何字符串转换为固定范围内数字的哈希函数。这种哈希函数很有用,因为它使我们能够确保数据具有正确的身份——就像现实生活中的指纹一样。在介绍哈希函数之后,我们讨论了数据签名的重要性。数据签名或分析签名很有用,因为它使我们能够看到两个数据集是否是*似匹配的——指纹需要精确匹配。我们通过图像数据说明了分析签名的作用。我们通过介绍潜在变量模型来结束本章。我们讨论的潜在变量模型是因子分析。我们讨论了因子分析的一个应用,该应用使用心理调查数据来确定人们性格之间的差异。在下一章中,我们将讨论异常检测,这将使我们能够找到与数据集其他部分不匹配的观测值。
第七章:第六章
异常检测
学习目标
到本章结束时,你将能够:
-
使用参数和非参数方法在单变量和多变量数据中寻找异常值
-
使用数据转换来识别单变量和多变量数据中的异常值
-
使用马氏距离工作
-
通过结合季节性模型来提高异常检测性能
在本章中,我们将探讨不同的异常检测技术。
简介
数据分析通常隐含地假设所有观测值都是有效、准确和可靠的。但这种情况并不总是合理的假设。考虑信用卡公司的例子,他们收集的数据包括个人信用卡的消费记录。如果他们假设所有消费都是有效的,那么就会给盗贼和欺诈者可乘之机。相反,他们检查他们的交易数据集,寻找异常情况——即偏离一般观察模式的交易。由于欺诈交易没有标记,他们必须使用无监督学习来寻找这些异常情况,以防止犯罪活动。
在许多其他情况下,异常检测都是有用的。例如,制造商可能使用异常检测方法来寻找产品中的缺陷。医学研究人员可能在正常的心跳模式中寻找异常,以诊断疾病。IT 安全专业人员试图在服务器或计算机上找到异常活动,以识别恶意软件。在每种情况下,无监督学习方法都可以帮助区分有效观测值和异常观测值。
本章将介绍几种异常检测技术。我们将首先使用参数和非参数方法来寻找单变量和多变量数据中的异常值。然后,我们将讨论使用数据转换来识别单变量和多变量数据中的异常值。接下来,我们将探讨马氏距离,这是一种用于异常检测的多变量工具。我们将通过讨论使用回归模型来提高异常检测性能,通过结合季节性模型和检测上下文和集体异常来完成本章。
单变量异常值检测
在单变量情况下,异常检测是最简单的,也就是说,每个观测值只是一个数字。在这种情况下,我们可能首先通过检查观测值是否缺失、NULL、NA,或者记录为无穷大或其他与观测值类型不匹配的内容来进行常识性检查。完成此检查后,我们可以应用真正的无监督学习。
对于单变量数据,异常检测包括寻找异常值。R 的内置boxplot函数使得对异常值进行初步探索性检查变得非常容易,如下面的练习所示。
练习 37:使用 R 的 boxplot 函数进行异常值探索性视觉检查
对于单变量数据,异常检测包括寻找异常值。R 的内置箱线图函数使得在本练习中演示的初始探索性异常值检查变得非常简单。我们将使用一个名为mtcars的数据集,它是 R 内置的。
在这个练习中,我们将创建一个可以在图 6.3 中看到的箱线图。箱线图是一种重要的单变量图。在这里,围绕 3 的粗水*线表示数据中的中位数。围绕这个中位线的箱子在第一四分位数(25 百分位数)处有下限,在第三四分位数(75 百分位数)处有上限。垂直虚线延伸到所有非异常数据的上下端。这些虚线被称为图的触须,因此这种类型的图有时被称为“箱线和触须”图。最后,在图顶部附*表示为圆形点的两个观察值(至少根据 R 的结果)是异常值。
百分位数也可以称为分位数,在本例中我们将使用“分位数”来指代箱线图。分位数是数据中的一个点,它大于所有数据点中的一些固定比例。例如,0.1 分位数是数据中的一个观察值,它大于所有观察值的 10%,而小于其余部分。0.25 分位数也称为 25 百分位数或第一四分位数,它大于数据的 25%,而 75 百分位数或 0.75 分位数大于数据的 75%。当我们取一个观察值并找出它大于多少比例的观察值时,这被称为找到它的分位数。当我们取一个如 0.25 的分位数并试图找到一个与该分位数相对应的观察值时,我们可以称之为取逆分位数。
要使用 R 的boxplot函数进行异常值探索性视觉检查,请执行以下步骤:
-
要加载数据,打开 R 控制台并输入以下命令:
data(mtcars) -
执行以下命令以查看数据集的前六行:
head(mtcars)输出如下:
![图 6.1:mtcars 数据集的前六行]()
图 6.1:mtcars 数据集的前六行
-
您可以在 R 控制台中执行以下操作来查找有关此数据集的详细信息:
?mtcars文档如下:
![图 6.2:输出部分]()
图 6.2:输出部分
-
按以下方式创建汽车重量的箱线图:
boxplot(mtcars$wt)输出将如下所示:
![图 6.3:表示汽车重量的箱线图]()
图 6.3:表示汽车重量的箱线图
-
看起来有两个观察值被 R 归类为异常值,它们的值似乎高于 5。请注意,这些重量是以千磅为单位的,所以实际上这些重量高于 5,000 磅。我们可以通过在我们的数据上运行一个简单的过滤器来确定这些观察值:
highest<-mtcars[which(mtcars$wt>5),] print(highest)输出如下所示:

图 6.4:重量超过 5,000 磅的汽车
我们可以看到有三个车型的重量超过 5,000 磅。由于我们只观察到两个异常值,我们得出结论,重量第三高的凯迪拉克 Fleetwood 不是异常值。这留下了其他两个:林肯大陆和克莱斯勒帝国,作为显然具有异常重量的车型。与其他车型相比,这些车型构成了异常。研究人员的一个潜在下一步是调查为什么这些车型似乎具有异常高的汽车重量。
在第三章,概率分布中,我们讨论了数据集倾向于遵循的不同分布。许多数据集都有所谓的长尾或胖尾,这意味着与*均值相差很大的观测值所占的比例不均衡——不一定是因为它们是异常的异常值,只是因为它们分布的性质。如果我们碰巧正在处理一个遵循胖尾分布的数据集,我们定义异常值的标准应该改变。
在以下练习中,我们将转换一个遵循胖尾分布的数据集,并观察哪些观测值被报告为异常值。
练习 38:将胖尾数据集转换为改进异常值分类
在以下练习中,我们将转换一个遵循胖尾分布的数据集,并观察哪些观测值被报告为异常值。我们将使用预加载到 R 中的rivers数据集:
-
按照以下方式加载数据集:
data(rivers) -
执行以下命令以查看数据集的前六个观测值:
head(rivers)输出如下:
[1] 735 320 325 392 524 450 -
你可以看到
rivers数据集是一个向量。你可以通过输入以下内容来了解更多关于它的信息:?rivers文档说明如下:
![图 6.5:河流数据集的信息]()
图 6.5:河流数据集的信息
-
观察异常值的分布。首先,尝试通过运行以下命令来绘制河流数据的箱线图:
boxplot(rivers)箱线图看起来是这样的:
![图 6.6:河流数据集的箱线图]()
图 6.6:河流数据集的箱线图
你可以看到这个箱线图与之前我们查看的
mtcars箱线图看起来不同。特别是,箱线和须占据了较小的绘图部分,而大量的绘图由 R 分类为异常值的观测值组成。然而,异常值在任何数据集中都不应该非常多——根据定义,它们应该是稀少的。当我们观察到这样的箱线图,其中包含许多占据绘图大部分的异常值时,我们可以合理地得出结论,我们的分布是胖尾分布。 -
为了得到更好的异常值分类,对数据进行转换可能有所帮助。许多与自然和自然现象相关的数据集已知遵循对数正态分布。如果我们对每个观测值取对数,得到的分布将是正态分布(因此不是厚尾分布)。要转换数据,可以使用以下命令:
log_rivers<-log(rivers) -
观察转换数据的箱线图。最后,我们可以执行以下命令来查看转换数据的箱线图:
boxplot(log_rivers)箱线图将如下所示:

图 6.7:转换数据集的箱线图
如预期,箱线和须线占据了更大的绘图比例,被分类为异常值的观测值更少。在这种情况下,我们可以使用 log_rivers 图而不是 rivers 图来分类异常值。
之前的练习展示了数据准备在异常检测中的重要性。我们也可以尝试在原始数据集上进行异常检测。但有时,就像我们之前看到的 rivers 示例一样,通过进行一些简单的数据准备,我们可以得到不同且更好的结果。需要注意的是,数据有多种可能的转换方式。我们已经使用了对数转换,但还有许多其他可能有效的方法。另一件需要注意的事情是,数据准备既有利也有弊:一些数据准备方法,如*均和数据*滑,可能会导致我们丢弃有价值的信息,从而使我们的异常检测效果降低。
到目前为止,我们一直依赖于 R 的内置异常检测方法,并且我们已经通过简单的箱线图视觉检查来确定哪些观测值是异常值。在下一个练习中,我们将自己确定哪些观测值是异常值,而不使用 R 的内置异常分类。我们将找到数据的分位数——具体来说是 0.25 和 0.75 分位数。
练习 39:不使用 R 的内置 boxplot 函数找出异常值
在这个练习中,我们将自己确定哪些观测值是异常值,而不使用 R 的内置异常值分类。我们将找到数据的分位数——具体来说是 .25 和 .75 分位数。我们将再次使用 rivers 数据,这是我们之前练习中使用的数据:
-
通过执行以下命令加载数据:
data(rivers) -
四分位距是第一四分位数(25 百分位数)和第三四分位数(75 百分位数)之间的差值。因此,我们可以通过运行以下命令将四分位距存储在名为
interquartile_range的变量中:interquartile_range<-unname(quantile(rivers,.75)-quantile(rivers,.25))在这种情况下,我们使用
unname确保将interquartile_range只是一个数字,而不是 DataFrame、列表或其他数据类型。这将使后续的工作更容易且更可靠。 -
使用以下命令检查四分位距:
print(interquartile_range)输出如下:
[1] 370 -
R 中 boxplot 函数的标准方法是使用 1.5 倍的四分位距作为非异常观察值可以分散的范围的上限。然后非异常值的上限是第三四分位数加 1.5 倍的四分位距,非异常值的下限是第一四分位数减去 1.5 倍的四分位距。我们可以这样计算:
upper_limit<-unname(quantile(rivers,.75)+1.5*interquartile_range) lower_limit<-unname(quantile(rivers,.25)-1.5*interquartile_range) -
我们识别出的异常值是那些高于我们的
upper_limit或低于我们的lower_limit的观察值。我们可以这样确定:rivers[which(rivers>upper_limit | rivers<lower_limit)]这将输出一个我们将其分类为异常值的观察值列表:
[1] 1459 1450 1243 2348 3710 2315 2533 1306 1270 1885 1770注意
这个练习使用了 R 中 boxplot 函数所使用的方法。但在无监督学习中,关于方法细节的灵活性总是存在的。
-
另一种寻找异常值的方法是使用前一个练习中的方法,但使用不同的上限和下限值。我们可以通过改变乘以
四分位距的系数来实现这一点,如下所示:upper_limit<-unname(quantile(rivers,.75)+3*interquartile_range) lower_limit<-unname(quantile(rivers,.25)-3*interquartile_range)我们已经将系数从 1.5 改为 3。这种改变使得我们的方法不太可能将任何特定观察值分类为异常值,因为它增加了上限并降低了下限。
通常,你可以尝试以你认为合理且能导致良好结果的方式创造性地改变无监督学习方法。
前一个练习中的方法是所谓的非参数方法。在统计学中,有些方法被称为参数方法,而有些被称为非参数方法。参数方法对数据的潜在分布做出假设,例如,假设数据遵循正态分布。非参数方法旨在摆脱这些约束性假设。前一个练习中的方法仅依赖于分位数,因此它不对数据的分布或与之相关的参数(如均值和方差)做出任何假设。正因为如此,我们称之为非参数方法。相比之下,参数异常检测方法是一种对数据的分布或其参数(如均值和方差)做出假设的方法。请注意,非参数方法和参数方法总是在寻找相同的异常:不存在参数异常或非参数异常,只有参数方法和非参数方法。我们将在下一个练习中讨论参数异常检测方法。
我们将计算一个称为z 分数的东西。z 分数是衡量观察值与*均值之间距离的标准测量。每个 z 分数都是以标准差为单位测量的。因此,z 分数为 0 表示观察值与*均值相差 0 个标准差;换句话说,它等于*均值。z 分数为 1 表示观察值比*均值高 1 个标准差。z 分数为-2.5 表示观察值比*均值低 2.5 个标准差。在某些情况下,将距离*均值两个标准差以上的观察值视为异常或异常是惯例。
练习 40:使用参数方法检测异常值
在这个练习中,我们将通过寻找 z 分数大于 2 或小于-2 的观察值来调查异常。我们将使用前一个练习中使用的河流数据集:
-
按如下方式加载数据并确定标准差:
data(rivers) standard_deviation<-sd(rivers) -
通过计算每个观察值与*均值相差多少个标准差来确定每个观察值的 z 分数:
z_scores<-(rivers-mean(rivers))/ standard_deviation -
通过选择 z 分数大于 2 或小于-2 的观察值来确定哪些观察值是异常值:
outliers<-rivers[which(z_scores>2 | z_scores<(-2))] outliers输出如下:
[1] 2348 3710 2315 2533 1885 1770在这种情况下,我们可以看到有六条河流被归类为异常值——它们的 z 分数都高于 2。在这里,“异常值”这个词可能对这些异常来说过于强烈,因为 2 的 z 分数并不特别巨大。没有严格的规则来定义异常值,是否使用这个术语将取决于具体情况和背景。
参数异常检测,如本练习所示,是一种常见做法。然而,其适用性取决于其背后的参数假设是否有效。在这种情况下,我们计算了标准差,并寻找了距离*均值两个标准差以上的异常值。如果数据来自高斯(正态)分布,这种做法是合理的。在这种情况下,一些证据表明河流数据并不来自高斯分布。对于对基础数据分布有疑问的情况,可以使用非参数方法或转换方法(如前几项练习中使用的)可能更合适。
多变量异常检测
到目前为止的所有方法都集中在单变量异常值检测。然而,在实践中,这样的数据实际上是罕见的。大多数数据集包含关于数据多个属性的调查。在这些情况下,不清楚如何计算一个点是否是异常值。我们可以对每个单独的维度计算 z 分数,但对于那些在一个维度上 z 分数高而在另一个维度上低,或者在一个维度上相对较大而在另一个维度上*均的观察值,我们应该怎么办?没有简单的答案。在这些情况下,我们可以使用称为马氏 距离的东西来计算 z 分数的多维类似物。马氏距离是 z 分数的多维类似物。这意味着它测量两点之间的距离,但使用特殊的单位,就像 z 分数一样,依赖于数据的方差。在完成以下练习后,我们将更好地理解马氏距离的含义。
练习 41:计算马氏距离
在这个练习中,我们将学习一种新的测量方法,它最终将帮助我们找到在考虑身高和体重时,与一般人群相比哪些个体是异常值。这个练习的预期输出将是一个单一数据点的马氏距离,测量它距离数据中心的距离。
注意
对于所有需要导入外部 CSV 文件或图像的练习和活动,请转到RStudio-> 会话-> 设置工作目录-> 到源文件位置。您可以在控制台中看到路径已自动设置。
在后续的练习中,我们将计算数据集中所有点的马氏距离。然后,我们可以使用这些测量结果来分类哪些个体在考虑身高和体重时与一般人群相比是异常值:
注意
此数据集来自在线统计计算资源。您可以在wiki.stat.ucla.edu/socr/index.php/SOCR_Data_Dinov_020108_HeightsWeights找到数据集。我们已经下载了文件并将其保存在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson06/Exercise41-42/heightsweights.csv。我们使用了数据的前 200 行。
-
首先,在 R 中加载此数据。首先,从我们的 GitHub 存储库
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson06/Exercise41-42/heightsweights.csv下载它。然后,将其保存在 R 的工作目录中。然后,按照以下方式将其加载到 R 中:filename<-'heightsweights.csv' raw<-read.csv(filename, stringsAsFactors=FALSE) -
我们已经给数据集命名为
raw。你可以确保列有正确的名称如下:names(raw)<-c('index','height','weight') -
绘制数据并观察模式:
plot(raw$height,raw$weight)图形如下所示:
![图 6.8:身高和体重的图]()
图 6.8:身高和体重的图
我们可以看到,在这个样本中,身高和体重都有很大的变化。请注意,我们的单变量方法在这个数据中不会直接起作用。我们可以计算身高的标准差或四分位数范围,并找到身高的异常值。但身高的异常值不一定是体重的异常值——它们可能是正常体重,或者正好符合体重的预期。同样,体重的异常值可能有完全*均或预期的身高。我们并不立即清楚如何计算“完全异常值”,或者说在某些整体意义上是异常的观测值,考虑到身高和体重两个方面。
-
接下来,我们将计算多维*均值的等价物,称为
centroid:centroid<-c(mean(raw$height),mean(raw$weight)) -
计算任何给定点和质心之间的距离。作为一个例子,我们将选择数据集中的第一个观测值:
example_distance<-raw[1,c('height','weight')]-centroid -
计算我们数据中身高和体重协方差矩阵的逆。首先,我们计算身高和体重数据的协方差矩阵:
cov_mat<-cov(raw[,c('height','weight')]) -
使用 R 中的
solve函数计算其逆,该函数用于计算矩阵逆:inv_cov_mat<-solve(cov_mat) -
计算我们的点和数据集质心之间的马氏距离:
mahalanobis_dist<-t(matrix(as.numeric(example_distance)))%*% matrix(inv_cov_mat,nrow=2) %*% matrix(as.numeric(example_distance))在这个情况下,我们使用
%*%因为它表示矩阵乘法,这正是我们想要执行的,我们同样需要将每个参数转换为一个数值矩阵。这个练习的输出是一个马氏距离,它是多维 z 分数的推广:也就是说,它是衡量每个点距离均值多少个标准差的广义度量。在这种情况下,我们找到的马氏距离是 1.71672。马氏距离类似于任何类型的距离测量——0 是最低可能的测量值,数值越高,距离越远。只有质心会有测量的马氏距离为 0。马氏距离的优势在于它们是以一种考虑了每个变量的方差的方式进行标准化的,这使得它们在异常值检测中非常有效。在本章的后面部分,我们将看到如何使用这种类型的测量来找到这个多维数据集中的异常值。
在簇中检测异常
在前两章中,我们讨论了不同的聚类方法。我们现在讨论的方法,马氏距离,如果想象我们正在查看的数据是某个特定聚类的数据,就可以在聚类应用中非常有用。例如,在划分聚类中,具有最高马氏距离的点可以被选为从聚类中移除的点。此外,马氏距离的范围可以用来表示任何给定聚类的分散程度。
多变量异常值检测的其他方法
对于多变量异常值检测,还有其他方法,包括一些被称为非参数的方法。与非参数方法一样,一些先前的练习可能依赖于分位数,换句话说,从大到小排列每个观测值的排名,以分类异常值。一些非参数方法使用此类排名的总和来理解数据的分布。然而,这些方法并不常见,并且在一般情况下并不比马氏距离更有效,因此我们建议在多变量异常值检测中依赖马氏距离。
练习 42:基于马氏距离比较分类异常值
在这个练习中,我们将使用马氏距离的比较来分类异常值。这个练习是之前练习的延续,它将依赖于相同的数据库和一些相同的变量。在那个练习中,我们找到了一个数据点的马氏距离;现在我们将为所有数据点找到它。在执行以下练习之前,您应该运行之前练习中的所有代码,并确保您熟悉那里提出的思想:
注意
此数据集来自 UCI 机器学习仓库。您可以在wiki.stat.ucla.edu/socr/index.php/SOCR_Data_Dinov_020108_HeightsWeights找到数据集。我们已经下载了文件,并将其保存在github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson06/Exercise41-42/heightsweights.csv。
-
创建一个
NULL变量,用于存储我们计算出的每个距离:all_distances<-NULL -
遍历每个观测值,并计算它与数据质心的马氏距离。此循环中的代码来自之前的练习,我们在那里学习了如何计算马氏距离:
k<-1 while(k<=nrow(raw)){ the_distance<-raw[k,c('height','weight')]-centroid mahalanobis_dist<-t(matrix(as.numeric(the_distance)))%*% matrix(inv_cov_mat,nrow=2) %*% matrix(as.numeric(the_distance)) all_distances<-c(all_distances,mahalanobis_dist) k<-k+1 }运行此循环后,我们为数据中的每个点测量了马氏距离。
-
绘制所有具有特别高 Mahalanobis 距离的观测值。在这种情况下,我们将特别高定义为 Mahalanobis 距离的最高 10%。这意味着所有高于 Mahalanobis 距离的 0.9 分位数(我们将在以下代码中选择)的 Mahalanobis 距离:
plot(raw$height,raw$weight) points(raw$height[which(all_distances>quantile(all_distances,.9))],raw$weight[which(all_distances>quantile(all_distances,.9))],col='red',pch=19)输出如下:

图 6.9:具有高 Mahalanobis 距离的观测值
此图显示了 Mahalanobis 距离最高的 10% 的每个点的实心红色点。我们可以看到,其中一些在身高和体重方面似乎都是异常值,并且可以通过执行我们的单变量异常检测方法来观察到。然而,图表上许多红色点既不是身高的单变量异常,也不是体重的单变量异常。它们只是相对于整个点云的异常,而 Mahalanobis 距离使我们能够量化并检测这一点。
在季节性数据中检测异常
到目前为止,我们只讨论了异常检测作为一种检测异常的方法。然而,异常检测不仅仅包括异常检测。一些异常不容易被检测为原始异常。接下来,我们将查看季节性、趋势性数据。在这类数据中,我们希望找到在季节性趋势或周期背景下发生的异常。
我们将使用 R 中的 expsmooth 包的数据。
我们将使用的数据记录了 1985 年至 2005 年间澳大利亚每月游客数量,以千人计。
在以下练习中,我们将处理具有时间趋势的数据。通过时间趋势,我们指的是随着时间的推移,观测值倾向于增加或减少(在这种情况下,它们倾向于增加)。为了检测异常,我们想要做的是去趋势。去趋势数据意味着尽可能去除其时间趋势,以便我们可以找到每个观测值与预期值的偏差程度。
练习 43:执行季节性建模
在这个练习中,我们将尝试对数据进行建模,以确定我们应该将哪些视为数据的预期值,哪些视为与预期值的偏差。预期的输出是一组误差值,我们将在未来的练习中使用这些误差值来分类异常——误差最大的观测值将被分类为数据集的异常:
-
首先,通过执行以下命令将此数据加载到 R 中:
install.packages("expsmooth") library(expsmooth) data(visitors) plot(visitors)输出如下:
![图 6.10:1985 年至 2005 年间澳大利亚每月游客数量的图表]()
图 6.10:1985 年至 2005 年间澳大利亚每月游客数量的图表
-
按以下方式检查数据的第一个六个观测值:
head(visitors)输出如下:
May Jun Jul Aug Sep Oct 1985 75.7 75.4 83.1 82.9 77.3 105.7按以下方式检查最后六个观测值:
tail(visitors)输出如下:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec 2004 479.9 593.1 2005 462.4 501.6 504.7 409.5 -
由于日期可能难以处理,我们可以分配一个数值变量来跟踪日期的顺序。我们将把这个变量称为
period并定义如下:period<-1:length(visitors) -
将
visitors数据与刚刚创建的period变量结合,将它们都放入一个名为raw的 DataFrame 中:raw<-data.frame(cbind(visitors,period)) -
这一步在技术上属于监督学习,而不是无监督学习。在这种情况下,我们将使用监督学习,但仅作为进行无监督学习过程中的一个中间步骤。为了找到数据中的时间趋势,我们可以运行一个线性回归,将销售额与数值时间周期相关联,如下所示:
timetrend<-lm(visitors~period+I(log(period)),data=raw) -
接下来,我们可以获得这个时间趋势的拟合值,并将它们作为
rawDataFrame 的一部分存储:raw$timetrend<-predict(timetrend,raw) -
去趋势的过程意味着我们从第 6 步中找到的预测趋势中减去。我们这样做的原因是我们想找到数据中的异常值。如果我们保留数据中的趋势,看起来像异常的东西实际上可能是趋势的预期结果。通过从数据中去除趋势,我们可以确保观察到的异常不是趋势的结果。我们可以按照以下方式完成去趋势:
raw$withouttimetrend<-raw$visitors-raw$timetrend -
我们可以绘制去趋势数据的一个简单图表如下:
plot(raw$withouttimetrend,type='o')绘图如下:
![图 6.11:去趋势数据的绘图]()
图 6.11:去趋势数据的绘图
在这个图表中,你应该注意到数据中没有明显的从左到右的趋势,这表明我们已经成功地去除了趋势。
我们的数据记录了澳大利亚每月的访客数量。澳大利亚的季节温度和天气有变化,因此合理地假设游客访问量可能会根据特定月份的天气是否宜人而增加或减少。甚至可能存在与季节和全年天气变化相关的商务或外交访问澳大利亚的变化。因此,合理地假设澳大利亚的访问量存在年度模式。我们的下一步将是将这些年度模式从我们的数据中去除。
-
首先,我们创建一个矩阵,其中每一列包含关于一年中不同月份的去趋势访客数据:
seasonsmatrix = t(matrix(data = raw$withouttimetrend, nrow = 12)) -
计算每一列的*均值,以得到该特定月份去趋势访客的*均值:
seasons = colMeans(seasonsmatrix, na.rm = T) -
这给我们一个包含 12 个值的向量——每年一个月。由于我们有 20 年的数据,我们将这个向量重复 20 次:
raw$seasons<-c(rep(seasons,20)) -
最后,我们可以获得去趋势、去周期的数据,我们将它命名为
error,因为在移除了数据中的时间趋势和年度周期后,随机误差是唯一剩下的东西:raw$error<-raw$visitors-raw$timetrend-raw$seasons -
我们可以绘制这个图表来查看其外观:
plot(raw$error,type='o')绘图如下:
![图 6.12:去趋势数据的绘图]()
图 6.12:去趋势数据的绘图
-
将季节性建模的所有元素一起绘制。最后,我们可以展示我们从季节性数据中分离出的所有元素:时间趋势、年度周期和随机误差:
par(mfrow=c(3,1)) plot(raw$timetrend,type='o') plot(raw$seasons,type='o') plot(raw$error,type='o')输出如下:

图 6.13:季节性建模元素的绘图
图 6.13 中一起显示的三个图显示了原始数据的分解。第一个图显示了时间趋势,换句话说,就是数据中观察到的整体模式,即每个月的访问量往往比上个月多。当然,这是对数据的过度简化,因为也存在季节性模式:某些月份的访问量往往比其他月份多或少,无论整体趋势如何。第二个图显示了这些季节性模式,这些模式每年都以相同的方式重复。最后,第三个图显示了误差,这是数据中未被整体时间趋势或每年内的季节性模式捕获的所有变化。如果你将这三个图中呈现的所有数据相加,你将恢复原始数据集。但在这些三个图中,我们可以看到这些元素被分解,这种分解使我们更好地理解了时间趋势、季节性和误差如何相互作用,从而构成观察到的数据。
这个练习使我们能够创建一个名为 error 的变量,并将其添加到原始数据框中。现在我们已经创建了误差向量,我们可以使用标准的异常检测来在数据框中找到异常。
练习 44:使用参数方法在季节性数据中寻找异常
在这个练习中,我们将通过寻找与预期值的最大偏差来进行异常检测:
-
计算前一个练习中计算出的误差数据的标准差:
stdev<-sd(raw$error) -
找出哪些数据点距离*均值超过两个标准差:
high_outliers<-which(raw$error>(mean(raw$error)+2*sd(raw$error))) low_outliers<-which(raw$error<(mean(raw$error)-2*sd(raw$error))) -
检查我们已分类为异常的观察结果:
raw[high_outliers,]输出如下:
visitors period timetrend withouttimetrend seasons error 1 75.7 1 67.18931 8.510688 -55.94655 64.45724 130 392.7 130 305.93840 86.761602 24.35847 62.40313 142 408.0 142 323.44067 84.559332 24.35847 60.20086 147 397.4 147 330.70509 66.694909 11.55558 55.13933 188 559.9 188 389.78579 170.114205 91.11673 78.99748低异常被分类如下:
raw[low_outliers,]输出如下:
visitors period timetrend withouttimetrend seasons error 80 266.8 80 231.4934 35.30663 91.11673 -55.81010 216 321.5 216 429.7569 -108.25691 -20.46137 -87.79553 217 260.9 217 431.1801 -170.28007 -55.94655 -114.33352 218 308.3 218 432.6029 -124.30295 -42.40371 -81.8992 -
我们可以如下绘制这些点:
plot(raw$period,raw$visitors,type='o') points(raw$period[high_outliers],raw$visitors[high_outliers],pch=19,col='red') points(raw$period[low_outliers],raw$visitors[low_outliers],pch=19,col='blue')图形如下所示:

图 6.14:被分类为异常的数据的图形
该图显示了所有我们的数据和我们已经分类为异常的点,高异常用红色表示,低异常用蓝色表示。你应该注意,并非所有这些点都立即明显是异常值。我们只是在之前进行的季节性建模练习中,确定了异常偏离的预期值后,才将它们识别为异常。
接下来,我们将介绍两种更多类型的异常:上下文异常和集体异常。为了介绍这些概念,我们将生成一个包含上下文和集体异常的人工数据集。
上下文和集体异常
在图 6.15 中大约 x=1 的位置,你可以看到一个与邻居相当远的单独点。这是一个上下文异常的例子,我们将首先讨论这意味着什么以及如何检测这些类型的异常。在大约 x=3.6 的位置,你可以看到一个 y 值*坦的区域,每个值都等于 0。在这个数据中,零值并不异常,但这么多零值放在一起就是异常。因此,这些数据被称作集体异常。
我们将首先考虑上下文异常。上下文异常是指仅因为其邻居而被认为是异常的观测值。在我们刚刚生成的数据集中,有一个点在 x=1 处,y=0。然而,x=0.99 和 x=1.01 处的 y 值接* 0.84,在这个上下文中与 0 相差甚远。上下文异常可以通过找到与邻居距离异常的观测值来检测,正如我们将在以下练习中看到的那样。
练习 45:检测上下文异常
以下练习展示了如何检测我们刚刚介绍的数据集中的上下文异常。由于上下文异常是与邻居非常不同的观测值,我们需要对每个观测值与其邻居进行显式比较。为了做到这一点,我们计算第一差分。第一差分简单地是观测值的值减去其前一个观测值的值。
本练习的预期结果将是数据集中那些具有上下文异常的观测值:
-
我们将生成一个包含上下文和集体异常的人工数据集。该数据集可以通过在 R 控制台中运行以下代码生成:
x<-1:round(2*pi*100+100)/100 y<-rep(0,round(2*pi*100)+100) y[1:314]<-sin(x[1:314]) y[415:728]<-sin(x[315:628]) y[100]<-0 -
你可以通过以下方式绘制
x和y来查看这些数据的外观:plot(x,y,type='o')输出如下:
![图 6.15:生成数据集的绘图]()
图 6.15:生成数据集的绘图
此图显示了一个正弦曲线:一种从 0 开始,向上和向下轻轻倾斜,最终再次回到 0 的简单曲线。这些数据是人工生成的;然而,我们可以想象它代表了温度的观测值:在某些月份较低,在某些月份上升较高,在某些月份较高,在其他月份下降。温度和天气数据通常遵循可以用正弦或余弦等三角曲线建模的规律。我们已经修改了我们的正弦曲线,使其包含一些异常数据。
-
同时为每个观测值找到第一差分如下:
difference_y<-y[2:length(y)]-y[1:(length(y)-1)] -
创建第一差分数据的箱线图:
boxplot(difference_y)结果箱线图如下所示:
![图 6.16:第一差分数据的箱线图]()
图 6.16:第一差分数据的箱线图
此箱线图显示,几乎所有第一差分都非常接*零,而两个异常差分则远离其他数据。我们可以从第一差分箱线图中看到,单个高异常值大于 0.5。
你可能会注意到图 6.16 中有两个明显的异常值,但在图 6.15 中只有一个明显的异常值。这是因为图 6.16 显示的是一阶差分数据,数据中的单个异常值导致了两个大的第一阶差异:第 99 个和第 100 个值之间的差异,以及第 100 个和第 101 个值之间的差异。原始数据中的一个异常观察值导致了一阶差分数据中的两个异常观察值。
-
使用 R 的有用
which函数确定哪个观察值对应于这个异常值:which(difference_y>0.5)which返回值为 100,表示它是第 100 个观察值,是一个上下文异常。如果我们检查,y[100]等于 0,而其邻居不是,所以我们已经成功找到了上下文异常。
接下来,我们将讨论集体异常。在我们的 x-y 图中,我们指出了在 x=3.64 处所有观察值都等于 0 的 100 个观察点。在这个数据集中,0 值本身并不是异常,但是有 100 个观察值都等于 0 则是异常的。这里 100 个零值一起被称为集体异常。集体异常比上下文异常更难检测,但我们将尝试在下面的练习中检测它们。
练习 46:检测集体异常
在接下来的练习中,我们将检测我们之前创建的数据集中的集体异常。这个练习的预期结果是构成集体异常的观察值列表:
-
为了检测这种异常,我们需要寻找包含没有变化或只有微小变化的观察值组或邻域。以下循环实现了这一点。它创建了一个向量,该向量由两个差异的最大值组成:一个观察值与其 50 个周期前的观察值之间的差异,以及一个观察值与其 50 个周期后的观察值之间的差异。如果这些差异中的最大值是零或非常小,那么我们就检测到了一个极端*坦的邻域,这是这种集体异常的迹象。以下是我们将使用的循环:
changes<-NULL ks<-NULL k<-51 while(k<(length(y)-50)){ changes<-c(changes,max(abs(y[k-50]),abs(y[k+50]))) ks<-c(ks,k) k<-k+1 }这个循环创建了一个名为
changes的向量。这个向量的每个元素都测量了一个观察值与其邻居之间观察到的最大差异,距离为 50 个观察点。changes向量中特别小的值将表明我们可能有一个由*坦邻域组成的集体异常。 -
现在我们有一个测量邻域变化的向量,我们可以创建一个简单的箱线图,就像我们在之前的练习中所做的那样:
boxplot(changes)输出如下:
![图 6.17:邻域变化的箱线图]()
图 6.17:邻域变化的箱线图
我们可以看到有很多被分类为异常值的观察值,这表明存在非常低的邻域变化。
-
我们可以按照以下方式找到导致这种集体异常的观察值:
print(ks[which(changes==min(changes))])输出为 364。
-
我们可以通过检查该索引处的 y 值来验证这是对应于集体异常的索引:
print(y[ks[which(changes==min(changes))]])因此,y 的值为 0,这是集体异常处的 y 值,这为我们找到了正确的点提供了证据。
核密度
为了结束本章,我们将讨论如何使用核密度估计在一系列血液样本上执行异常值检测。核密度估计提供了一种自然的方式来测试特定的血液检测结果是否异常,即使没有特定血液测试或医学的一般专业知识。
假设你在一个诊所工作,你的老板要求你对患者进行一种新的血液测试。你的老板想知道是否有任何患者的测试结果异常。然而,你对这种新的血液测试不熟悉,也不知道正常和异常结果应该是什么样子。你所拥有的只是老板向你保证来自正常患者的先前血液测试记录。假设这些测试的结果如下:
normal_results<-c(100,95,106,92,109,190,210,201,198)
现在假设你的老板要求你找出以下新血液测试结果中的异常值(如果有的话):
new_results<-c(98,35,270,140,200)
在核密度估计中,我们使用一组核来建模数据集。对于我们的目的,核将只是具有不同均值和方差的正态分布。你可以在第三章,概率分布中了解更多关于正态分布的信息。我们将假设我们的数据具有由正态分布之和捕获的密度。然后,任何看起来与我们指定的正态分布之和不一致的数据都可以被分类为异常。
当我们计算核密度时,我们必须指定一个叫做带宽的东西。在这里,带宽将是我们用来建模数据的正态分布的方差的度量。如果我们指定一个高的带宽,我们假设数据广泛分散,如果我们指定一个低的带宽,我们假设数据主要包含在一个相对较窄的范围内。随着你完成以下练习,这应该会变得更加清晰。
练习 47:使用核密度估计寻找异常值
在这个练习中,我们将介绍如何使用核密度估计寻找异常值。这个练习的预期输出是按照核密度估计方法构成异常的一组观测值列表:
-
指定我们的数据和参数。对于我们的数据,我们将使用之前指定的
正常结果和新结果:normal_results<-c(100,95,106,92,109,190,210,201,198) new_results<-c(98,35,270,140,200) -
核密度估计依赖于一个称为
带宽的参数。因此,我们将从 25 开始。带宽的选择将取决于您的个人偏好以及原始数据。如果您不确定,可以选择一个与您数据的标准差大致相同的带宽。在这种情况下,我们将选择 25,这低于我们数据的标准差。您可以如下设置带宽为 25:bandwidth<-25注意
如果您喜欢,可以自由地尝试其他带宽值。
-
使用 R 的
density函数来获得核密度估计:our_estimate<-density(normal_results, bw=bandwidth) -
绘制密度估计:
plot(our_estimate)生成的图表如下所示:
![图 6.18:密度估计图]()
图 6.18:密度估计图
-
图 6.18 中的图形形状代表我们原始数据的分布。您可以在第三章,概率分布中了解更多关于概率分布的信息。这个分布被称为
双峰分布,这意味着数据似乎主要围绕两个点聚集:大多数观察值接* 100 或 200。如果观察值在图 6.18 所示的分布中的对应点显示它特别不可能,我们将将其解释为异常。 -
我们可以为我们的每个新结果获得密度估计。对于
new_results中的每个观察,我们将根据图 6.18 中展示的核来计算密度估计。我们将按照以下方式将这些密度估计存储在新变量中:new_density_1<-density(normal_results,bw=25,n=1,from=new_results[1],to=new_results[1])$y new_density_2<-density(normal_results,bw=25,n=1,from=new_results[2],to=new_results[2])$y new_density_3<-density(normal_results,bw=25,n=1,from=new_results[3],to=new_results[3])$y new_density_4<-density(normal_results,bw=25,n=1,from=new_results[4],to=new_results[4])$y new_density_5<-density(normal_results,bw=25,n=1,from=new_results[5],to=new_results[5])$y输出将是图表上从步骤 3对应于
new_results向量中指定的每个 x 值的的高度。我们可以通过打印来观察这些值。按照以下方式打印new_density_1:print(new_density_1)输出如下:
[1] 0.00854745按照以下方式打印
new_density_2:print(new_density_2)输出如下:
[1] 0.0003474778按照以下方式打印
new_density_3:print(new_density_3)输出如下:
[1] 0.0001787185按照以下方式打印
new_density_4:print(new_density_4)输出如下:
[1] 0.003143966按照以下方式打印
new_density_5:print(new_density_5)输出如下:
[1] 0.006817359 -
我们可以将这些点绘制在我们的密度图上如下:
plot(our_estimate) points(new_results[1],new_density_1,col='red',pch=19) points(new_results[2],new_density_2,col='red',pch=19) points(new_results[3],new_density_3,col='red',pch=19) points(new_results[4],new_density_4,col='red',pch=19) points(new_results[5],new_density_5,col='red',pch=19) -
执行这些命令的结果如下所示:![图 6.19:密度图上的点映射
![img/C12628_06_19.jpg]()
图 6.19:密度图上的点映射
这显示了之前我们检查过的相同密度图,增加了五个点——每个新结果对应一个,我们正在检查这些新结果以寻找异常值。每个点都显示了每个特定观察的相对可能性:具有更高估计密度值的点更有可能被观察到,而具有较低估计密度值的点不太可能被观察到,因此更有可能是异常值。关于哪些观察是异常值,哪些不是,没有严格的规则,但一般来说,估计密度值最接*零的观察最可能是异常值。
-
解释结果以分类异常。
每个密度值看起来都相当接*零。然而,有些比其他更接*零。在这种情况下,密度估计值越接*零,我们越有信心认为观察结果是异常的。在这种情况下,我们将选择一个阈值值,并说那些核密度估计值低于阈值的血液检测结果是异常的,而那些核密度估计值高于阈值的检测结果不是异常的。看起来 0.001 是一个合理的阈值,因为它将高密度值与最低密度值分开——密度值低于 0.001 的观察结果在步骤 6所示的图表中看起来非常不可能。因此,我们将血液检测结果 35 和 270 归类为异常结果,而其他所有结果都视为合理,因为我们看到在步骤 4 中,35 和 270 对应的是低于 0.001 的密度估计值。
因此,我们练习的最终结论是血液检测结果 35 和 270 是异常的,而其他所有血液检测结果都是合理的。
继续学习异常检测
如果你继续学习异常检测,你会发现有大量的不同异常检测技术。然而,所有这些技术都遵循我们在季节性建模示例中看到的基本模式。具体来说,高级异常检测通常包括以下内容:
-
指定预期的模型
-
计算基于模型预期的值与观察到的值之间的差异——这被称为误差
-
使用单变量异常值检测对误差向量进行检测以确定异常
最大的困难在于第一步:指定一个有用的预期模型。在这种情况下,我们指定了一个季节性模型。在其他情况下,将需要指定考虑多维图像数据、音频记录、复杂的经济指标或其他复杂属性的模型。为这些情况设置模型的方法将需要研究数据来源的特定领域。然而,在每种情况下,异常检测都将遵循前面列出的三个步骤的要点模式。
活动十四:使用参数方法和非参数方法寻找单变量异常
活动的目的是使用参数方法来寻找单变量异常。为此活动,我们将使用 R 中内置的数据集,称为islands。如果你在 R 中执行?islands,你可以找到这个数据集的文档。在这个文档中,你可以注意到这个数据集包含了地球上面积超过 10,000 *方英里的陆地面积。
这可能是一个对研究地球陆地形成过程的地质学家感兴趣的数据集。根据科学家的说法,岛屿可以通过多种方式形成:有时是通过火山活动,有时是通过珊瑚生长,有时是通过其他方式。地质学家可能对寻找异常大或小的岛屿感兴趣——这些岛屿可能是进行进一步研究以尝试理解岛屿自然形成过程的最佳地点。在这个活动中,我们将寻找islands数据集中的异常值。
这些步骤将帮助您完成活动:
-
在 R 的内置数据集中加载名为
islands的数据,并创建该数据的箱线图。您注意到数据分布和异常值有什么特点? -
对
islands数据进行对数变换,并创建变换后数据的箱线图。这如何改变了被分类为异常值的数据点? -
使用非参数方法(该方法将异常值定义为位于第一四分位数和第三四分位数以上或以下 1.5 倍四分位距的点)手动计算
islands数据集中的异常值。对islands数据的对数变换也进行同样的操作。 -
使用参数方法通过计算数据的均值和标准差来对
islands数据集中的异常值进行分类,将异常值分类为距离均值超过两个标准差的观测值。对islands数据的对数变换也进行同样的操作。 -
比较这些异常值检测方法的结果。
注意
本活动的解决方案可在第 234 页找到。
活动十五:使用马氏距离寻找异常值
在接下来的活动中,我们将检查与汽车速度和停车距离相关的数据。这些数据可能对试图研究哪些汽车表现最佳的汽车工程师有所帮助。那些相对于其速度具有特别低停车距离的汽车可以用作高性能汽车的例子,而那些相对于其速度具有异常高停车距离的汽车可能是进一步研究以寻找改进领域的候选者。在这个活动中,我们将基于速度和停车距离来寻找异常值。因为我们正在处理多变量数据,所以使用马氏距离来寻找异常值是有意义的。
这些步骤将帮助您完成这项活动:
-
从 R 的内置数据集中加载
cars数据集。这个数据集包含一些非常古老的汽车的速度以及在该速度下所需的停车距离。绘制数据图表。 -
计算这些数据的质心,并计算每个点与质心之间的马氏距离。找出异常值(与质心的马氏距离最高的点)。绘制一个显示所有观测值和异常值的图表。
图表将如下所示:

图 6.20:标记了异常值的图表
注意
本活动的解决方案在第 237 页。
摘要
在本章中,我们讨论了异常检测。我们首先介绍了单变量异常检测,包括非参数和参数方法。我们讨论了进行数据转换以获得更好的异常分类。然后,我们讨论了使用马氏距离的多变量异常检测。我们完成了更多高级练习,以分类与季节性变化数据相关的异常。我们讨论了集体和上下文异常,并在本章结束时讨论了如何在异常检测中使用核密度估计。
第八章:附录
关于
本节包含的内容旨在帮助学生完成书中的活动。它包括学生为实现活动目标需要执行的详细步骤。
第一章:聚类方法简介
活动一:具有三个聚类的 k-means 聚类
解答:
-
在
iris_data变量中加载 Iris 数据集:iris_data<-iris -
创建一个
t_color列并将其默认值设为red。将两种物种的值改为green和blue,这样第三个就保持为red:iris_data$t_color='red' iris_data$t_color[which(iris_data$Species=='setosa')]<-'green' iris_data$t_color[which(iris_data$Species=='virginica')]<-'blue'注意
在这里,我们只更改那些物种为
setosa或virginica的值的color列) -
选择任意三个随机的聚类中心:
k1<-c(7,3) k2<-c(5,3) k3<-c(6,2.5) -
通过在
plot()函数中输入花萼长度和花萼宽度以及颜色来绘制x,y图:plot(iris_data$Sepal.Length,iris_data$Sepal.Width,col=iris_data$t_color) points(k1[1],k1[2],pch=4) points(k2[1],k2[2],pch=5) points(k3[1],k3[2],pch=6)这里是输出:
![图 1.36:给定聚类中心的散点图]()
图 1.36:给定聚类中心的散点图
-
选择迭代次数:
number_of_steps<-10 -
选择
n的初始值:n<-1 -
开始
while循环以找到聚类中心:while(n<number_of_steps){ -
计算每个点到当前聚类中心的距离。这里我们使用
sqrt函数计算欧几里得距离:iris_data$distance_to_clust1 <- sqrt((iris_data$Sepal.Length-k1[1])²+(iris_data$Sepal.Width-k1[2])²) iris_data$distance_to_clust2 <- sqrt((iris_data$Sepal.Length-k2[1])²+(iris_data$Sepal.Width-k2[2])²) iris_data$distance_to_clust3 <- sqrt((iris_data$Sepal.Length-k3[1])²+(iris_data$Sepal.Width-k3[2])²) -
将每个点分配到最*的聚类中心:
iris_data$clust_1 <- 1*(iris_data$distance_to_clust1<=iris_data$distance_to_clust2 & iris_data$distance_to_clust1<=iris_data$distance_to_clust3) iris_data$clust_2 <- 1*(iris_data$distance_to_clust1>iris_data$distance_to_clust2 & iris_data$distance_to_clust3>iris_data$distance_to_clust2) iris_data$clust_3 <- 1*(iris_data$distance_to_clust3<iris_data$distance_to_clust1 & iris_data$distance_to_clust3<iris_data$distance_to_clust2) -
通过使用 R 中的
mean()函数计算每个中心的*均x和y坐标来计算新的聚类中心:k1[1]<-mean(iris_data$Sepal.Length[which(iris_data$clust_1==1)]) k1[2]<-mean(iris_data$Sepal.Width[which(iris_data$clust_1==1)]) k2[1]<-mean(iris_data$Sepal.Length[which(iris_data$clust_2==1)]) k2[2]<-mean(iris_data$Sepal.Width[which(iris_data$clust_2==1)]) k3[1]<-mean(iris_data$Sepal.Length[which(iris_data$clust_3==1)]) k3[2]<-mean(iris_data$Sepal.Width[which(iris_data$clust_3==1)]) n=n+1 } -
为每个中心选择颜色以绘制散点图:
iris_data$color='red' iris_data$color[which(iris_data$clust_2==1)]<-'blue' iris_data$color[which(iris_data$clust_3==1)]<-'green' -
绘制最终的图表:
plot(iris_data$Sepal.Length,iris_data$Sepal.Width,col=iris_data$color) points(k1[1],k1[2],pch=4) points(k2[1],k2[2],pch=5) points(k3[1],k3[2],pch=6)输出如下:

图 1.37:用不同颜色表示不同物种的散点图
活动二:使用 k-means 进行客户细分
解答:
-
将数据读入
ws变量:ws<-read.csv('wholesale_customers_data.csv') -
只存储
ws变量中的第 5 和第 6 列,丢弃其他列:ws<-ws[5:6] -
导入
factoextra库:library(factoextra) -
计算两个中心的聚类中心:
clus<-kmeans(ws,2) -
绘制两个聚类的图表:
fviz_cluster(clus,data=ws)输出如下:
![图 1.38:两个聚类的图表]()
图 1.38:两个聚类的图表
注意到异常值也成为了两个聚类的一部分。
-
计算三个聚类的聚类中心:
clus<-kmeans(ws,3) -
绘制三个聚类的图表:
fviz_cluster(clus,data=ws)输出如下:
![图 1.39:三个聚类的图表]()
图 1.39:三个聚类的图表
注意现在一些异常值已经成为一个单独聚类的部分。
-
计算四个中心的聚类中心:
clus<-kmeans(ws,4) -
绘制四个聚类的图表:
fviz_cluster(clus,data=ws)输出如下:
![图 1.40:四个聚类的图表]()
图 1.40:四个聚类的图表
注意到异常值已经开始分离成两个不同的聚类。
-
计算五个聚类的聚类中心:
clus<-kmeans(ws,5) -
绘制五个聚类的图表:
fviz_cluster(clus,data=ws)输出如下:
![图 1.41:五个聚类的图表]()
图 1.41:五个聚类的图表
注意到异常值如何明显地形成了两个分开的红色和蓝色聚类,而其余数据被分类到三个不同的聚类中。
-
计算六个聚类的聚类中心:
clus<-kmeans(ws,6) -
绘制六个聚类的图表:
fviz_cluster(clus,data=ws)输出如下:

图 1.42:六个聚类的图表
活动 3:使用 k-medoids 聚类进行客户细分
解决方案:
-
将 CSV 文件读入
ws变量:ws<-read.csv('wholesale_customers_data.csv') -
只在
ws变量中存储第 5 和第 6 列:ws<-ws[5:6] -
导入
factoextra库进行可视化:library(factoextra) -
导入
cluster库进行 PAM 聚类:library(cluster) -
通过在
pam函数中输入数据和聚类数量来计算聚类:clus<-pam(ws,4) -
绘制聚类可视化图:
fviz_cluster(clus,data=ws)输出如下:
![图 1.43:聚类 K-medoid 图]()
图 1.43:聚类 K-medoid 图
-
再次,使用 k-means 聚类计算聚类并绘制输出,以与
pam聚类的输出进行比较:clus<-kmeans(ws,4) fviz_cluster(clus,data=ws)输出如下:

图 1.44:聚类 K-means 图
活动 4:寻找理想的市场细分数量
解决方案:
-
将下载的数据集读入
ws变量:ws<-read.csv('wholesale_customers_data.csv') -
通过丢弃其他列,只在变量中存储第 5 和第 6 列:
ws<-ws[5:6] -
使用轮廓分数计算最佳聚类数量:
fviz_nbclust(ws, kmeans, method = "silhouette",k.max=20)这里是输出:
![图 1.45:表示轮廓分数最佳聚类数量的图表]()
图 1.45:表示轮廓分数最佳聚类数量的图表
根据轮廓分数,最佳聚类数量是两个。
-
使用 WSS 分数计算最佳聚类数量:
fviz_nbclust(ws, kmeans, method = "wss", k.max=20)这里是输出:
![图 1.46:WSS 分数下的最佳聚类数量]()
图 1.46:WSS 分数下的最佳聚类数量
根据 WSS 肘部方法,最佳聚类数量大约是六个。
-
使用 Gap 统计量计算最佳聚类数量:
fviz_nbclust(ws, kmeans, method = "gap_stat",k.max=20)这里是输出:

图 1.47:Gap 统计量下的最佳聚类数量
根据 Gap 统计量,最佳聚类数量是一个。
第二章:高级聚类方法
活动 5:在蘑菇数据集上实现 k-modes 聚类
解决方案:
-
从 https://github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/blob/master/Lesson02/Activity05/mushrooms.csv 下载
mushrooms.csv。 -
下载后,在 R 中加载
mushrooms.csv文件:ms<-read.csv('mushrooms.csv') -
检查数据集的维度:
dim(ms)输出如下:
[1] 8124 23 -
检查所有列的分布:
summary.data.frame(ms)输出如下:
![图 2.29:所有列分布摘要的屏幕截图]()
图 2.29:所有列分布摘要的屏幕截图
每一列包含所有唯一的标签及其计数。
-
将数据集的所有列(除了最终标签)存储在一个新变量
ms_k中:ms_k<-ms[,2:23] -
导入具有
kmodes函数的klaR库:install.packages('klaR') library(klaR) -
计算
kmodes聚类并将它们存储在kmodes_ms变量中。将不带true标签的数据集作为第一个参数输入,将聚类数量作为第二个参数输入:kmodes_ms<-kmodes(ms_k,2) -
通过创建一个包含
true标签和cluster标签的表格来检查结果:result = table(ms$class, kmodes_ms$cluster) result输出如下:
1 2 e 80 4128 p 3052 864
如你所见,大多数可食用蘑菇都在第 2 个聚类中,而大多数有毒蘑菇都在第 1 个聚类中。因此,使用 k-modes 聚类已经合理地完成了识别每种蘑菇是否可食用或有毒的工作。
活动 6:实现 DBSCAN 并可视化结果
解决方案:
-
导入
dbscan和factoextra库:library(dbscan) library(factoextra) -
导入
multishapes数据集:data(multishapes) -
将
multishapes数据集的列放入ms变量中:ms<-multishapes[,1:2] -
按以下方式绘制数据集:
plot(ms)输出如下:
![图 2.30:多形状数据集的折线图]()
图 2.30:多形状数据集的折线图
-
对数据集执行 k-means 聚类并绘制结果:
km.res<-kmeans(ms,4) fviz_cluster(km.res, ms,ellipse = FALSE)输出如下:
![]()
图 2.31:多形状数据集上的 k-means 折线图
-
对
ms变量执行 DBSCAN 并绘制结果:db.res<-dbscan(ms,eps = .15) fviz_cluster(db.res, ms,ellipse = FALSE,geom = 'point')输出如下:

图 2.32:多形状数据集上的 DBCAN 折线图
在这里,你可以看到所有黑色点都是异常值,并且不在任何聚类中,而 DBSCAN 形成的聚类在其他任何聚类方法中都是不可能的。这些聚类具有所有类型的形状和大小,而 k-means 中的所有聚类都是球形的。
活动 7:对种子数据集进行层次聚类分析
解决方案:
-
将下载的文件读入
sd变量:sd<-read.delim('seeds_dataset.txt')注意
根据系统上文件的位置修改路径。
-
首先,将数据集的所有列(除了最终标签)放入
sd_c变量中:sd_c<-sd[,1:7] -
导入
cluster库:library(cluster) -
计算层次聚类并绘制树状图:
h.res<-hclust(dist(sd_c),"ave") plot(h.res)输出如下:
![图 2.33:聚类树状图]()
图 2.33:聚类树状图
-
在
k=3处切割树并绘制一个表格,以查看聚类结果在分类三种种子类型方面的表现:memb <- cutree(h.res, k = 3) results<-table(sd$X1,memb) results输出如下:
![图 2.34:分类三种种子类型的表格]()
图 2.34:分类三种种子类型的表格
-
对
sd_c数据集执行划分聚类并绘制树状图:d.res<-diana(sd_c,metric ="euclidean",) plot(d.res)输出如下:
![图 2.35:划分聚类的树状图]()
图 2.35:分裂聚类的树状图
-
在
k=3处切割树,并绘制一个表格以查看聚类结果在分类三种种子类型时的表现:memb <- cutree(h.res, k = 3) results<-table(sd$X1,memb) results输出如下:

图 2.36:分类三种种子类型的表格
您可以看到两种聚类方法都产生了相同的结果。这些结果还表明,分裂聚类是层次聚类的逆过程。
第三章:概率分布
活动 8:寻找与鸢尾花数据集中变量分布最接*的标准分布
解答:
-
将
df变量加载到 Iris 数据集中:df<-iris -
仅选择对应 setosa 物种的行:
df=df[df$Species=='setosa',] -
导入
kdensity库:library(kdensity) -
使用
kdensity函数计算并绘制花瓣长度的核密度估计图:dist <- kdensity(df$Sepal.Length) plot(dist)输出如下:
![图 3.36:花瓣长度的核密度估计图]()
图 3.36:花瓣长度的核密度估计图
这个分布与我们之前章节中研究的正态分布最接*。在这里,*均值和中位数都在 5 左右。
-
使用
kdensity函数从df中计算并绘制花瓣宽度的核密度估计图:dist <- kdensity(df$Sepal.Width) plot(dist)输出如下:

图 3.37:花瓣宽度的核密度估计图
这个分布也最接*正态分布。我们可以通过柯尔莫哥洛夫-斯米尔诺夫检验来形式化这种相似性。
活动 9:使用正态分布计算累积分布函数和执行柯尔莫哥洛夫-西蒙诺夫检验
解答:
-
将 Iris 数据集加载到
df变量中:df<-iris -
仅保留 setosa 物种的行:
df=df[df$Species=='setosa',] -
计算
df中花瓣长度列的*均值和标准差:sdev<-sd(df$Sepal.Length) mn<-mean(df$Sepal.Length) -
使用花瓣长度列的标准差和*均值生成一个新的分布:
xnorm<-rnorm(100,mean=mn,sd=sdev) -
绘制
xnorm和花瓣长度列的累积分布函数:plot(ecdf(xnorm),col='blue') plot(ecdf(df$Sepal.Length),add=TRUE,pch = 4,col='red')输出如下:
![图 3.38:xnorm 和花瓣长度的累积分布函数]()
图 3.38:xnorm 和花瓣长度的累积分布函数
在分布中,样本看起来非常接*。让我们看看,在下一个测试中,花瓣长度样本是否属于正态分布。
-
对两个样本进行柯尔莫哥洛夫-斯米尔诺夫检验,如下所示:
ks.test(xnorm,df$Sepal.Length)输出如下:
Two-sample Kolmogorov-Smirnov test data: xnorm and df$Sepal.Length D = 0.14, p-value = 0.5307 alternative hypothesis: two-sided这里,
p-value非常高,而D值很低,因此我们可以假设花瓣长度的分布接*正态分布。 -
对
df中花瓣宽度列重复相同的步骤:sdev<-sd(df$Sepal.Width) mn<-mean(df$Sepal.Width) xnorm<-rnorm(100,mean=mn,sd=sdev) plot(ecdf(xnorm),col='blue') plot(ecdf(df$Sepal.Width),add=TRUE,pch = 4,col='red')输出如下:
![图 3.39:xnorm 和花瓣宽度的累积分布函数]()
图 3.39:xnorm 和花瓣宽度的累积分布函数
-
按如下进行柯尔莫哥洛夫-斯米尔诺夫检验:
ks.test(xnorm,df$Sepal.Length)输出如下:
Two-sample Kolmogorov-Smirnov test data: xnorm and df$Sepal.Width D = 0.12, p-value = 0.7232 alternative hypothesis: two-sided
这里,花瓣宽度的样本分布也接*正态分布。
第四章:降维
活动 10:在新数据集上执行 PCA 和市场篮子分析
解答:
-
在开始我们的主要分析之前,我们将移除一个对我们无关的变量:
Boston<-Boston[,-12] -
我们将创建虚拟变量。最终我们将得到一个原始数据集和一个虚拟变量数据集。我们这样做如下:
Boston_original<-Boston接下来,我们将为原始数据集中的每个测量值创建虚拟变量。您可以在 MASS 包的文档中找到每个变量的含义,该文档可在
cran.r-project.org/web/packages/MASS/MASS.pdf找到。 -
为是否一个城镇的人均犯罪率高低创建虚拟变量:
Boston$highcrim<-1*(Boston$indus>median(Boston$crim)) Boston$lowcrim<-1*(Boston$indus<=median(Boston$crim))为是否一个城镇划定的地块面积超过 25,000 *方英尺的比例高或低创建虚拟变量:
Boston$highzn<-1*(Boston$zn>median(Boston$zn)) Boston$lowzn<-1*(Boston$zn<=median(Boston$zn))为是否一个城镇非零售商业用地比例高或低创建虚拟变量:
Boston$highindus<-1*(Boston$indus>median(Boston$indus)) Boston$lowindus<-1*(Boston$indus<=median(Boston$indus))为是否一个城镇毗邻查尔斯河创建虚拟变量:
Boston$highchas<-(Boston$chas) Boston$lowchas<-(1-Boston$chas)为是否一个城镇氮氧化物浓度高或低创建虚拟变量:
Boston$highnox<-1*(Boston$nox>median(Boston$nox)) Boston$lownox<-1*(Boston$nox<=median(Boston$nox))为是否一个城镇每户*均房间数量高或低创建虚拟变量:
Boston$highrm<-1*(Boston$rm>median(Boston$rm)) Boston$lowrm<-1*(Boston$rm<=median(Boston$rm))为是否一个城镇拥有 1940 年之前建造的业主自住单元比例高或低创建虚拟变量:
Boston$highage<-1*(Boston$age>median(Boston$age)) Boston$lowage<-1*(Boston$age<=median(Boston$age))为是否一个城镇距离波士顿五个就业中心*均距离高或低创建虚拟变量:
Boston$highdis<-1*(Boston$dis>median(Boston$dis)) Boston$lowdis<-1*(Boston$dis<=median(Boston$dis))为是否一个城镇对放射状高速公路的可达性指数高或低创建虚拟变量:
Boston$highrad<-1*(Boston$rad>median(Boston$rad)) Boston$lowrad<-1*(Boston$rad<=median(Boston$rad))为是否一个城镇的全值财产税税率高或低创建虚拟变量:
Boston$hightax<-1*(Boston$tax>median(Boston$tax)) Boston$lowtax<-1*(Boston$tax<=median(Boston$tax))为是否一个城镇的学生-教师比例高或低创建虚拟变量:
Boston$highptratio<-1*(Boston$ptratio>median(Boston$ptratio)) Boston$lowptratio<-1*(Boston$ptratio<=median(Boston$ptratio))为是否一个城镇低阶层人口比例高或低创建虚拟变量:
Boston$highlstat<-1*(Boston$lstat>median(Boston$lstat)) Boston$lowlstat<-1*(Boston$lstat<=median(Boston$lstat))为是否一个城镇的中位家庭价值高或低创建虚拟变量:
Boston$highmedv<-1*(Boston$medv>median(Boston$medv)) Boston$lowmedv<-1*(Boston$medv<=median(Boston$medv)) -
创建一个完全由我们刚刚创建的虚拟变量组成的数据集:
Bostondummy<-Boston[,14:ncol(Boston)] -
最后,我们将我们的
Boston_2数据集恢复到添加所有虚拟变量之前的原始形式:Boston<-Boston_original -
按如下方式计算数据集协方差矩阵的特征值和特征向量:
Boston_cov<-cov(Boston) Boston_eigen<-eigen(Boston_cov) print(Boston_eigen$vectors)输出如下:
![图 4.17:协方差矩阵的特征向量]()
图 4.17:协方差矩阵的特征向量
-
按如下方式打印特征值:
print(Boston_eigen$values)输出如下:
![图 4.18:协方差矩阵的特征值]()
图 4.18:协方差矩阵的特征值
-
对于第三部分,我们将基于特征值创建一个简单的斯克奇图:
plot(Boston_eigen$values,type='o')输出如下:
![图 4.19:特征值的绘图]()
图 4.19:特征值的绘图
-
接下来,我们选择我们将使用的特征向量的数量(我选择了 10),并将数据集转换为 10 维,如下所示:
neigen<-10 transformed<-t(t(as.matrix(Boston_eigen$vectors[,1:neigen])) %*% t(as.matrix(Boston))) -
然后,我们将尽可能恢复数据集:
restored<- t(as.matrix(Boston_eigen$vectors[,1:neigen]) %*% t(as.matrix(transformed))) -
最后,我们可以检查我们的恢复与原始数据集的接*程度,如下所示:
print(head(restored-Boston)) -
在这里,我们需要指定一个
support阈值(例如,20%),并完成对数据的第一次遍历:support_thresh<-0.2 firstpass<-unname(which(colMeans(Bostondummy,na.rm=TRUE)>support_thresh)) -
在这里,我们完成对数据的第二次遍历:
secondcand<-t(combn(firstpass,2)) secondpass<-NULL k<-1 while(k<=nrow(secondcand)){ support<-mean(Bostondummy[,secondcand[k,1]]*Bostondummy[,secondcand[k,2]],na.rm=TRUE) if(support>support_thresh){ secondpass<-rbind(secondpass,secondcand[k,]) } k<-k+1 } -
在这里,我们完成第三次遍历,然后根据
confidence和lift阈值进行过滤:thirdpass<-NULL k<-1 while(k<=nrow(secondpass)){ j<-1 while(j<=length(firstpass)){ n<-1 product<-1 while(n<=ncol(secondpass)){ product<-product*Bostondummy[,secondpass[k,n]] n<-n+1 } if(!(firstpass[j] %in% secondpass[k,])){ product<-product*Bostondummy[,firstpass[j]] support<-mean(product,na.rm=TRUE) if(support>support_thresh){ thirdpass<-rbind(thirdpass,c(secondpass[k,],firstpass[j])) } } j<-j+1 } k<-k+1 } thirdpass_conf<-NULL k<-1 while(k<=nrow(thirdpass)){ support<-mean(Bostondummy[,thirdpass[k,1]]*Bostondummy[,thirdpass[k,2]]*Bostondummy[,thirdpass[k,3]],na.rm=TRUE) confidence<-mean(Bostondummy[,thirdpass[k,1]]*Bostondummy[,thirdpass[k,2]]*Bostondummy[,thirdpass[k,3]],na.rm=TRUE)/mean(Bostondummy[,thirdpass[k,1]]*Bostondummy[,thirdpass[k,2]],na.rm=TRUE) lift<-confidence/mean(Bostondummy[,thirdpass[k,3]],na.rm=TRUE) thirdpass_conf<-rbind(thirdpass_conf,unname(c(thirdpass[k,],support,confidence,lift))) k<-k+1 } -
我们最终的输出是那些通过了
support、confidence和lift阈值的三个项目篮子的列表:print(head(thirdpass_conf))输出结果如下:

图 4.20:三项篮子的输出
第五章:数据比较方法
活动 11:为人物照片创建图像签名
解决方案:
-
将 Borges 照片下载到你的电脑上,并保存为
borges.jpg。确保它保存在 R 的工作目录中。如果它不在 R 的工作目录中,那么使用setwd()函数更改 R 的工作目录。然后,你可以将这张图片加载到一个名为im(代表图像)的变量中,如下所示:install.packages('imager') library('imager') filepath<-'borges.jpg' im <- imager::load.image(file =filepath)我们将要探索的其余代码将使用这张图片,称为
im。在这里,我们已经将阿拉莫的图片加载到im中。然而,你可以通过将图片保存到你的工作目录并在filepath变量中指定其路径来运行其余的代码。 -
我们正在开发的签名旨在用于灰度图像。因此,我们将使用
imager包中的函数将此图像转换为灰度:im<-imager::rm.alpha(im) im<-imager::grayscale(im) im<-imager::imsplit(im,axis = "x", nb = 10)代码的第二行是转换为灰度。最后一行执行了将图像分割成 10 等份的操作。
-
以下代码创建了一个空矩阵,我们将用关于我们 10x10 网格每个部分的详细信息来填充它:
matrix <- matrix(nrow = 10, ncol = 10)接下来,我们将运行以下循环。这个循环的第一行使用了
imsplit命令。这个命令之前也被用来将 x 轴分成 10 等份。这次,对于 x 轴的每个 10 等份,我们将沿着 y 轴进行分割,也将它分成 10 等份:for (i in 1:10) { is <- imager::imsplit(im = im[[i]], axis = "y", nb = 10) for (j in 1:10) { matrix[j,i] <- mean(is[[j]]) } }到目前为止的输出是
matrix变量。我们将在步骤 4中使用它。 -
通过运行以下代码获取 Borges 照片的签名:
borges_signature<-get_signature(matrix) borges_signature输出结果如下:
![图 5.12:borges_signature 矩阵]()
图 5.12:borges_signature 矩阵
-
接下来,我们将开始使用 9x9 矩阵而不是 10x10 矩阵来计算一个签名。我们开始使用之前使用过的相同过程。以下代码行加载我们的 Borges 图像,就像我们之前做的那样。代码的最后一行将图像分割成相等的部分,但这次我们设置
nb=9,以便将图像分割成 9 等份:filepath<-'borges.jpg' im <- imager::load.image(file =filepath) im<-imager::rm.alpha(im) im<-imager::grayscale(im) im<-imager::imsplit(im,axis = "x", nb = 9) -
以下代码创建了一个空矩阵,我们将用关于我们 9x9 网格每个部分的详细信息来填充它:
matrix <- matrix(nrow = 9, ncol = 9)注意我们使用
nrow=9和ncol=9,这样我们就有了一个 9x9 的矩阵来填充我们的亮度测量值。 -
接下来,我们将运行以下循环。此循环的第一行使用
imsplit命令。此命令之前也用于将 x 轴分割成 9 个相等的部分。这次,对于每个 9 个 x 轴分割,我们将沿着 y 轴进行分割,也将它分割成 9 个相等的部分:for (i in 1:9) { is <- imager::imsplit(im = im[[i]], axis = "y", nb = 9) for (j in 1:9) { matrix[j,i] <- mean(is[[j]]) } }到目前为止的输出是
matrix变量。我们将重复 步骤 4。 -
通过运行以下代码获取博尔赫斯照片的 9x9 签名:
borges_signature_ninebynine<-get_signature(matrix) borges_signature_ninebynine输出如下:

图 5.13:borges_signature_ninebynine 矩阵
活动 12:为水印图像创建图像签名
解决方案:
-
将带水印的图片下载到您的计算机上,并将其保存为
alamo_marked.jpg。请确保它保存在 R 的工作目录中。如果它不在 R 的工作目录中,请使用setwd()函数更改 R 的工作目录。然后,您可以将此图像加载到名为im(代表图像)的变量中,如下所示:install.packages('imager') library('imager') filepath<-'alamo_marked.jpg' im <- imager::load.image(file =filepath)我们将要探索的其余代码将使用名为
im的此图像。在这里,我们已经将阿拉莫的水印图片加载到im中。然而,您只需将图片保存到您的工作目录中,并在filepath变量中指定其路径,就可以在任意图片上运行其余的代码。 -
我们正在开发的签名旨在用于灰度图像。因此,我们将使用
imager包中的函数将此图像转换为灰度:im<-imager::rm.alpha(im) im<-imager::grayscale(im) im<-imager::imsplit(im,axis = "x", nb = 10)此代码的第二行是转换为灰度。最后一行执行将图像分割成 10 个相等部分的操作。
-
以下代码创建了一个空矩阵,我们将用有关我们 10x10 网格每个部分的信息来填充它:
matrix <- matrix(nrow = 10, ncol = 10)接下来,我们将运行以下循环。此循环的第一行使用
imsplit命令。此命令之前也用于将 x 轴分割成 10 个相等的部分。这次,对于每个 10 个 x 轴分割,我们将沿着 y 轴进行分割,也将它分割成 10 个相等的部分:for (i in 1:10) { is <- imager::imsplit(im = im[[i]], axis = "y", nb = 10) for (j in 1:10) { matrix[j,i] <- mean(is[[j]]) } }到目前为止的输出是
matrix变量。我们将在 步骤 4 中使用此变量。 -
我们可以通过运行以下代码获取水印照片的签名:
watermarked_signature<-get_signature(matrix) watermarked_signature输出如下:
![图 5.14:水印图像的签名]()
图 5.14:水印图像的签名
此活动的最终输出是
watermarked_signature变量,它是水印阿拉莫照片的分析签名。如果您已经完成了迄今为止的所有练习和活动,那么您应该有三个分析签名:一个名为building_signature,一个名为borges_signature,还有一个名为watermarked_signature。 -
完成此活动后,我们将此签名存储在名为
watermarked_signature的变量中。现在,我们可以将其与我们的原始阿拉莫签名进行比较,如下所示:comparison<-mean(abs(watermarked_signature-building_signature)) comparison在这种情况下,我们得到的结果是 0.015,这表明原始图像签名与这个新图像的签名非常接*。
我们所看到的是,我们的分析签名方法对相似图像返回相似的签名,对不同的图像返回不同的签名。这正是我们希望签名所做的,因此我们可以判断这种方法是成功的。
活动 13:执行因子分析
解决方案:
-
数据文件可以从
github.com/TrainingByPackt/Applied-Unsupervised-Learning-with-R/tree/master/Lesson05/Data/factor.csv下载。将其保存到您的计算机上,并确保它在 R 的工作目录中。如果您将其保存为factor.csv,则可以通过执行以下命令在 R 中加载它:factor<-read.csv('factor.csv') -
按如下方式加载
psych包:library(psych) -
我们将对用户评分进行因子分析,这些评分记录在数据文件的 2 至 11 列中。我们可以如下选择这些列:
ratings<-factor[,2:11] -
按如下方式创建评分数据的相关矩阵:
ratings_cor<-cor(ratings) -
通过创建灰度图来确定我们应该使用多少个因素。灰度图是以下命令的输出之一:
parallel <- fa.parallel(ratings_cor, fm = 'minres', fa = 'fa') -
灰度图如下所示:
![图 5.15:*行分析灰度图]()
图 5.15:*行分析灰度图
灰度图显示了其中一个特征值远高于其他特征的特征。虽然我们在分析中可以自由选择任意数量的特征,但其中一个特征值远大于其他特征,这为我们使用一个特征进行分析提供了充分的理由。
-
我们可以如下进行因子分析,指定
nfactors参数中的因子数量:factor_analysis<-fa(ratings_cor, nfactors=1)这将我们的因子分析结果存储在一个名为
factor_analysis的变量中: -
我们可以如下检查我们的因子分析结果:
print(factor_analysis)输出如下所示:

图 5.16:因子分析结果
在MR1下的数字显示了我们对每个类别的单特征因子载荷。由于我们只有一个解释性因素,所有在这个因素上有正载荷的类别之间都是正相关。我们可以将这个因素解释为普遍的积极性,因为它会表明,如果人们对一个类别评价很高,他们也会对其他类别评价很高;如果他们对一个类别评价很低,他们很可能也会对其他类别评价很低。
唯一的例外是“类别 10”,它记录了用户对宗教机构的*均评分。在这种情况下,因子载荷大且为负。这表明,那些对大多数其他类别评分很高的人往往对宗教机构评分较低,反之亦然。因此,我们可以将我们发现的积极因子解释为对休闲活动的积极态度,而不是对宗教机构的积极态度,因为宗教机构可能不是休闲场所,而是崇拜场所。似乎在这个数据集中,对休闲活动持积极态度的人对崇拜持消极态度,反之亦然。对于接* 0 的因子载荷,我们也可以得出关于休闲活动积极性的规则不太适用的结论。你可以看到,因子分析使我们能够找到我们数据中观测值之间的关系,这是我们之前未曾怀疑的。
第六章:异常检测
活动 14:使用参数方法和非参数方法寻找单变量异常值
解答:
-
按以下方式加载数据:
data(islands) -
按以下方式绘制箱线图:
boxplot(islands)![图 6.21:岛屿数据集的箱线图]()
图 6.21:岛屿数据集的箱线图
你应该注意到数据具有极端的厚尾分布,这意味着中位数和四分位距在图中所占比例相对较小,与 R 软件分类为异常值的许多观测值相比。
-
按以下方式创建一个新的对数转换后的数据集:
log_islands<-log(islands) -
按以下方式创建对数转换数据的箱线图:
boxplot(log_islands)![图 6.22:对数转换后的数据集箱线图]()
图 6.22:对数转换后的数据集箱线图
你应该注意到对数转换后只有五个异常值。
-
计算四分位距:
interquartile_range<-quantile(islands,.75)-quantile(islands,.25) -
将四分位距的 1.5 倍加到第三四分位数上,以得到非异常数据的上限:
upper_limit<-quantile(islands,.75)+1.5*interquartile_range -
将异常值定义为任何高于此上限的观测值:
outliers<-islands[which(islands>upper_limit)] -
计算对数转换数据的四分位距:
interquartile_range_log<-quantile(log_islands,.75)-quantile(log_islands,.25) -
将四分位距的 1.5 倍加到第三四分位数上,以得到非异常数据的上限:
upper_limit_log<-quantile(log_islands,.75)+1.5*interquartile_range_log -
将异常值定义为任何高于此上限的观测值:
outliers_log<-islands[which(log_islands>upper_limit_log)] -
按以下方式打印未转换的异常值:
print(outliers)对于未转换的异常值,我们得到以下结果:
![图 6.23:未转换的异常值]()
图 6.23:未转换的异常值
按以下方式打印对数转换后的异常值:
print(outliers_log)对于对数转换后的异常值,我们得到以下结果:
![图 6.24:对数转换后的异常值]()
图 6.24:对数转换后的异常值
-
计算数据的均值和标准差:
island_mean<-mean(islands) island_sd<-sd(islands) -
选择距离均值超过两个标准差的观测值:
outliers<-islands[which(islands>(island_mean+2*island_sd))] outliers我们得到以下异常值:
![图 6.25:异常值的截图]()
图 6.25:异常值的截图
-
首先,我们计算对数转换数据的均值和标准差:
island_mean_log<-mean(log_islands) island_sd_log<-sd(log_islands) -
选择距离*均值超过两个标准差的观测值:
outliers_log<-log_islands[which(log_islands>(island_mean_log+2*island_sd_log))] -
我们如下打印对数变换后的异常值:
print(outliers_log)输出如下:

图 6.26:对数变换后的异常值
活动 15:使用马氏距离查找异常值
解答:
-
您可以按照以下步骤加载数据并绘制图表:
data(cars) plot(cars)输出图表如下:
![图 6.27:汽车数据集的绘图]()
图 6.27:汽车数据集的绘图
-
计算质心:
centroid<-c(mean(cars$speed),mean(cars$dist)) -
计算协方差矩阵:
cov_mat<-cov(cars) -
计算协方差矩阵的逆:
inv_cov_mat<-solve(cov_mat) -
创建一个
NULL变量,它将保存我们计算出的每个距离:all_distances<-NULL -
我们可以遍历每个观测值,并计算它们与数据集质心的马氏距离:
k<-1 while(k<=nrow(cars)){ the_distance<-cars[k,]-centroid mahalanobis_dist<-t(matrix(as.numeric(the_distance)))%*% matrix(inv_cov_mat,nrow=2) %*% matrix(as.numeric(the_distance)) all_distances<-c(all_distances,mahalanobis_dist) k<-k+1 } -
绘制具有特别高马氏距离的所有观测值,以查看我们的异常值:
plot(cars) points(cars$speed[which(all_distances>quantile(all_distances,.9))], cars$dist[which(all_distances>quantile(all_distances,.9))],col='red',pch=19)我们可以如下查看输出图表,异常点用红色表示:

图 6.28:标记异常值的绘图















































































浙公网安备 33010602011771号