无监督机器学习研讨会(三)

原文:annas-archive.org/md5/59ef285f4ffb779ed4c411e356902e16

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:8. 市场篮子分析

概述

在本章中,我们将探讨市场篮子分析,这是一种最初设计用于帮助零售商了解和改善其业务的算法。然而,正如我们将在本章中讨论的,它并不仅限于零售业。市场篮子分析揭示了顾客购买的商品之间的潜在关系。本章结束时,你应该对交易数据、定义两件商品关系的基本指标、Apriori 算法和关联规则有一个扎实的理解。

介绍

大多数数据科学从业者都认为,自然语言处理(包括主题建模)是数据科学的前沿领域,是一个活跃的研究方向。我们现在理解到,主题模型可以并且应该在任何潜在能够提供洞察或推动增长的文本数据中被应用,包括社交媒体分析、推荐引擎和新闻过滤等。上一章探讨了主题模型的基本特征和两种主要算法。在本章中,我们将完全改变方向。

本章带我们进入零售领域,探索一种用于分析交易数据的基础且可靠的算法。虽然这种算法可能不是最前沿的,也不在最流行的机器学习算法目录中,但它在零售领域中无处不在,并且具有不可否认的影响力。它所提供的洞察易于解读、可以立即付诸行动,并且对确定分析的下一步至关重要。如果你在零售领域工作或涉及交易数据的分析,深入学习市场篮子分析将大有裨益。市场篮子分析的重要性在于,它提供了关于为什么人们会将某些商品一起购买的洞察,以及这些商品组合是否能被利用来加速增长或增加盈利。

市场篮子分析

假设你为一家零售商工作,卖着数十种产品,你的老板来找你,问你以下问题:

  • 哪些产品最常一起购买?

  • 产品应该如何在商店中组织和定位?

  • 我们如何识别最适合通过优惠券打折的产品?

你可能会完全困惑地回应,因为这些问题非常多样,看起来似乎无法用单一的算法和数据集来回答。然而,所有这些问题以及更多问题的答案都是市场篮子分析。市场篮子分析的基本理念是识别和量化哪些商品或商品组合是经常一起购买的,从而深入了解顾客行为和产品关系。

在我们深入分析之前,值得定义一下市场篮子这一术语。市场篮子是一个经济系统中的固定产品集合。固定并不一定意味着传统意义上的永久性。它意味着,直到产品从目录中移除之前,它将始终可以购买。上述定义中提到的产品可以是任何商品、服务或某一群体的元素,包括一辆自行车、给房子粉刷,或者一个网站。最后,经济系统可以是一个公司、一组活动或一个国家。市场篮子的最简单例子是一个超市,它是由一系列食品和饮品组成的系统:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_02.jpg

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_01.jpg)

图 8.1: 一个示例市场篮子

即使不使用任何模型或分析,某些产品之间的关系也是显而易见的。我们来看看肉类和蔬菜之间的关系。通常,市场篮子分析模型会返回比肉类和蔬菜更为具体的关系,但为了讨论的方便,我们将其泛化为肉类和蔬菜。好吧,肉类和蔬菜之间有关系。那又怎么样呢?嗯,我们知道这些是常见的主食,通常是一起购买的。我们可以利用这一信息,将蔬菜和肉类放置在商店的两侧,你会发现这两个物品通常就是这样被摆放的,迫使顾客走遍整个商店,这样会增加他们购买更多商品的可能性,哪怕他们原本并没有打算购买那些商品。

零售公司面临的一个挑战是如何有效地打折商品。我们再考虑一个显而易见的关系:花生酱和果冻。在美国,花生酱果冻三明治非常受欢迎,尤其是在孩子们中间。当购物篮里有花生酱时,可以假设果冻也会有很高的可能性。由于我们知道花生酱和果冻通常是一起购买的,因此同时对两者打折并没有意义。如果我们希望顾客购买这两样商品,我们只需对其中一项商品打折,因为如果我们能让顾客购买打折的商品,他们很可能也会购买另一项商品,尽管它是全价的。就像前一章中的主题模型一样,市场篮子分析的核心就是识别频繁出现的组合。下图展示了这样的组合的一个例子:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_01.jpg

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_02.jpg)

图 8.2: 市场篮子分析的可视化

在市场篮子分析中,我们寻找的是频繁出现的产品组合,而在主题模型中,我们寻找的是频繁出现的词汇组合。因此,正如它适用于主题模型一样,聚类这个词也可以应用于市场篮子分析。主要的区别在于,市场篮子分析中的聚类是微型的——每个聚类中只有少数几种产品——并且当涉及到计算概率度量时,聚类中项目的顺序非常重要。我们将在本章稍后深入探讨这些度量及其计算方法。

从前两个例子中可以明显看出,在市场篮子分析中,零售商可以发现顾客购买产品之间的关系——无论是显而易见的还是出乎意料的。一旦这些关系被揭示出来,就可以用来指导和改善决策过程。市场篮子分析的一个重要特点是,尽管这一分析方法最初是与零售领域相关开发、讨论和应用的,但它也可以应用于许多不同类型的商业。

执行这种分析的唯一要求是数据必须是项集合的列表。在零售情况下,这将是一个交易列表,其中每个交易都是一组已购买的产品。另一个应用的例子是分析网站流量。在网站流量分析中,我们将产品视为网站,因此列表的每个元素就是一个人在指定时间段内访问的所有网站集合。不用说,市场篮子分析的应用远远超出了最初的零售应用。

使用案例

传统零售应用中的三个主要使用案例是:定价优化、优惠券和折扣推荐以及店铺布局。如前所述,通过使用模型揭示的产品关联,零售商可以有策略地将产品放置在商店中,促使顾客购买更多商品,从而花费更多的钱。如果两个或更多产品之间的关联足够强,意味着在数据集中该产品组合出现得很频繁,并且组合中的单独产品很少脱离该组合出现,那么这些产品就可以放置在商店的远离位置,而不会显著影响顾客购买两种产品的几率。通过迫使顾客横跨整个商店去购买这两种产品,零售商增加了顾客注意到并购买更多产品的机会。同样,零售商也可以通过将两种关联较弱或非日常产品放置在一起,增加顾客购买这两种产品的几率。显然,商店布局受到许多因素的影响,但市场篮子分析绝对是其中的一个因素。

https://example.org

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_03.jpg)

图 8.3:帮助优化高效和有利可图商店布局的产品关联

定价优化和优惠券及折扣推荐是同一枚硬币的两面。它们可以简单地解释为在哪里提高价格,在哪里降低价格。考虑到两个强关联的商品。这两件商品很可能会在同一交易中购买,所以增加交易的利润的一种方式就是提高其中一件商品的价格。如果这两件商品之间的关联足够强,那么价格的提升几乎没有客户不购买两者的风险。类似地,零售商可以通过折扣或优惠券鼓励客户购买一件与另一件商品弱相关的商品。

例如,零售商可以将单个客户的购买历史与基于所有交易的市场篮子分析结果进行比较,找出某些客户购买的商品与他们当前未购买的商品之间的弱关联。通过这种对比,零售商可以为这些客户提供尚未购买商品的折扣,而这些商品是模型建议与他们之前购买的商品相关的。如果你曾在交易结束时收到与收据一同打印的优惠券,那么很可能这些商品就是通过这种方式发现与刚刚完成的交易中涉及的商品相关。

市场篮子分析的一个非传统但可行的应用是增强在线广告和搜索引擎优化。假设我们能够访问个人访问过的网站列表。利用市场篮子分析,我们可以找到网站之间的关系,并利用这些关系来战略性地排列和分组搜索引擎查询结果中的网站。在很多方面,这与商店布局的应用场景相似。

对于市场篮子分析的基本概念和它的应用场景有了大致了解后,让我们深入研究这些模型所使用的数据。

重要的概率度量

市场篮子分析建立在多个概率度量的计算基础上。这里涉及的五个主要度量是支持度置信度提升度杠杆度定罪度。在深入了解交易数据和具体的市场篮子分析模型(包括Apriori 算法关联规则)之前,我们应该花些时间使用一个小型的虚构交易集来定义和探索这些度量。我们首先构建一些数据来使用。

练习 8.01:创建示例交易数据

由于这是本章的第一个练习,我们先设置环境。本章将使用与第七章主题建模中相同的环境要求。如果任何包无法加载,就像前一章那样,请通过命令行使用pip进行安装。我们将使用的一个库是mlxtend,这对您来说可能比较陌生。它是一个机器学习扩展库,包含有用的补充工具,包括集成、堆叠,当然还有市场篮子分析模型。这个练习没有实际输出,我们只是创建一个示例交易数据集,以供后续练习使用:

  1. 打开一个带有 Python 3 的 Jupyter 笔记本。

  2. 安装以下库:matplotlib.pyplot,用于绘制模型的结果;mlxtend.frequent_patterns,用于运行模型;mlxtend.preprocessing,用于编码和准备数据以供模型使用;numpy,用于处理数组;pandas,用于处理数据框。

    import matplotlib.pyplot as plt
    import mlxtend.frequent_patterns
    import mlxtend.preprocessing
    import numpy
    import pandas
    
  3. 创建 10 个包含杂货店商品的虚拟交易,然后打印出这些交易。数据将以列表的形式呈现,这种数据结构将在后续讨论格式化交易数据时与模型相关:

    example = [['milk', 'bread', 'apples', 'cereal', 'jelly', \
                'cookies', 'salad', 'tomatoes'], \
               ['beer', 'milk', 'chips', 'salsa', 'grapes', \
                'wine', 'potatoes', 'eggs', 'carrots'], \
               ['diapers', 'baby formula', 'milk', 'bread', \
                'chicken', 'asparagus', 'cookies'], \
               ['milk', 'cookies', 'chicken', 'asparagus', \
                'broccoli', 'cereal', 'orange juice'], \
               ['steak', 'asparagus', 'broccoli', 'chips', \
                'salsa', 'ketchup', 'potatoes', 'salad'], \
               ['beer', 'salsa', 'asparagus', 'wine', 'cheese', \
                'crackers', 'strawberries', 'cookies'],\
               ['chocolate cake', 'strawberries', 'wine', 'cheese', \
                'beer', 'milk', 'orange juice'],\
               ['chicken', 'peas', 'broccoli', 'milk', 'bread', \
                'eggs', 'potatoes', 'ketchup', 'crackers'],\
               ['eggs', 'bread', 'cheese', 'turkey', 'salad', \
                'tomatoes', 'wine', 'steak', 'carrots'],\
               ['bread', 'milk', 'tomatoes', 'cereal', 'chicken', \
                'turkey', 'chips', 'salsa', 'diapers']]
    print(example)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_04.jpg

图 8.4:打印交易

现在我们已经创建了数据集,接下来我们将探讨几种概率度量,量化项目对之间的关系。

注意

要访问此特定部分的源代码,请参考packt.live/3fhf9bS

您还可以在packt.live/303vIBJ上在线运行此示例。

您必须执行整个笔记本,以便获得预期的结果。

支持度

支持度只是给定项集在数据中出现的概率,可以通过计算项集出现的交易次数,并将该次数除以总交易次数来计算。

注意

项集可以是单个项或一组项。

支持度是一个重要的度量,尽管它非常简单,因为它是用来确定项集之间关联的可信度和强度的主要度量之一。例如,可能会有两个只在一起出现的项,表明它们之间的关联非常强,但在包含 100 个交易的数据集中,仅出现两次并不令人印象深刻。因为项集只出现在 2%的交易中,而 2%在出现次数上是较小的,所以该关联不能被认为是显著的,因此可能在决策中无法使用。

请注意,由于支持度是一个概率,因此它的取值范围为 [0,1]。如果商品集是两个商品 X 和 Y,并且 N 是总交易数,则该公式具有以下形式:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_05.jpg

图 8.5:支持度公式

在进行市场篮分析时,如果某个商品或商品组合的支持度低于预定义的阈值,那么购买该商品或商品组合的情况就足够少,无法采取行动。让我们暂时回到来自练习 8.01创建样本交易数据的虚构数据,并将一个商品集定义为牛奶和面包。我们可以轻松地查看这 10 笔交易,并计算包含此牛奶和面包商品集的交易次数 - 共有 4 次。考虑到总共有 10 笔交易,牛奶和面包的支持度为 4 除以 10,即 0.4. 支持度是否足够大取决于数据集本身,我们将在后面的章节中详细介绍。

置信度

置信度指标可以从条件概率的角度来理解,因为它基本上是指购买产品 A 后购买产品 B 的概率。置信度通常表示为 A https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_01.png B,并且表示包含 A 的交易中也包含 B 的比例。因此,置信度是通过将完整的交易集筛选到包含 A 的交易中,然后计算这些交易中包含 B 的比例来确定的。与支持度一样,置信度是一个概率,因此其范围是 [0,1]。使用与支持度部分相同的变量定义,以下是置信度的公式:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_06.jpg

图 8.6:置信度公式

为了演示置信度,我们将使用啤酒和葡萄酒这两种商品。具体来说,让我们计算啤酒 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_01.png 葡萄酒的置信度。首先,我们需要确定包含啤酒的交易。有三笔交易,它们是第 2、6 和 7 笔。现在,这些交易中有多少包含葡萄酒?答案是全部。因此,啤酒 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_01.png 葡萄酒的置信度为 1. 每次客户购买啤酒时,他们也购买了葡萄酒。这可能是显而易见的,但对于识别可操作的关联,较高的置信度值更为有利。

提升度和杠杆

我们将同时讨论接下来的两个指标:提升(lift)和杠杆(leverage),因为尽管它们的计算方式不同,但都旨在回答相同的问题。像置信度一样,提升杠杆的表示形式是 A https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_01.png B。我们要回答的问题是,一个项目,比如 A,能否用来推断另一个项目,比如 B?换句话说,如果某个人购买了产品 A,我们能否在某种程度上有信心地说他们是否会购买产品 B?这些问题通过比较 A 和 B 在标准情况下(即假设 A 和 B 不独立时)与假设这两个产品独立时的支持度来回答。提升计算这两种情况的比率,因此它的取值范围是 [0, 无穷大]。当提升值为 1 时,两个产品是独立的,因此在购买产品 A 时,无法得出关于产品 B 的任何结论:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_01.png

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_07.jpg)

图 8.7:提升公式

杠杆计算这两种情况的差异,因此它的取值范围是 [-1, 1]。杠杆值为零的解释方式与提升值为 1 时相同:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_01.png

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_08.jpg)

图 8.8:杠杆公式

这些指标的值衡量了项目之间关系的程度和方向(换句话说,正向或负向)。当提升值不为 1 时,说明项目之间存在某种依赖关系。当提升值大于 1 时,购买第一个产品时,第二个产品更可能被购买。同样,当提升值小于 1 时,购买第一个产品时,第二个产品的购买可能性较小。如果提升值为 0.1,我们可以说这两个项目之间的关系是强烈的负向关系。也就是说,可以说当购买了一个产品时,购买第二个产品的机会会减少。提升值为 1 表示产品之间是独立的。在杠杆的情况下,正值表示正相关,负值表示负相关。正相关和负相关由独立点分开,正如前面所说,提升的独立点是 1,而杠杆的独立点是 0,值离这些独立点越远,关联越强。

确信度

接下来讨论的最后一个度量是信念(conviction),它比其他度量更不直观。信念是指在 X 和 Y 相互独立的情况下,X 发生而 Y 不发生的预期频率与错误预测频率的比率。错误预测频率定义为 1 减去 X 的置信度 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_01.png Y。请记住,置信度可以定义为 P(Y|X),这意味着 1 – P(Y|X) = P(Not Y|X)。分子也可以被理解为 1 – P(Y|X) = P(Not Y|X)。这两者之间的唯一区别在于,分子假设 X 和 Y 之间是独立的,而分母则没有这个假设。理想情况下,值大于 1,因为这意味着如果 X 和 Y 之间的关联是随机的(换句话说,X 和 Y 是独立的),则 X 和 Y 之间的关系会更常出现错误。重申一下,这表明 X 和 Y 之间的关联是有意义的。值为 1 时表示独立,值小于 1 则表示随机的 X 和 Y 关系比已定义的 X 和 Y 关系正确的情况更常见。此时,关系可能是反向的(换句话说,Y https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png X)。信念的取值范围是 [0, 无穷大],其公式如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_09.jpg

图 8.9:信念公式

让我们再次回到啤酒和葡萄酒这两个产品,但在本次解释中,我们将考虑葡萄酒 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png 啤酒的反向关联。Support(Y),或者在这个例子中,Support(Beer) 是 3/10,Confidence X https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Y,或者在这个例子中,Confidence(Wine https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Beer) 是 3/4。因此,Conviction(Wine https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Beer) 是 (1-3/10) / (1-3/4) = (7/10) * (4/1)。我们可以得出结论,如果葡萄酒和啤酒是独立的,那么葡萄酒 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png 啤酒的关联会错误出现 2.8 次。因此,之前所述的葡萄酒和啤酒之间的关联是合法的。

练习 8.02:计算度量

在本次练习中,我们将使用 练习 8.01 中的虚拟数据,即 创建样本事务数据,来计算之前描述的五个度量,这些度量将再次用于 Apriori 算法和关联规则的讲解。将评估这些度量的关联是 Milk https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Bread:

注意

本章中的所有练习都需要在同一个 Jupyter notebook 中进行。

  1. 定义并打印所有五个度量的基础频率,包括 Frequency(Milk)、Frequency(Bread) 和 Frequency(Milk, Bread)。此外,定义 N 为数据集中事务的总数:

    # the number of transactions
    N = len(example)
    # the frequency of milk
    f_x = sum(['milk' in i for i in example])
    # the frequency of bread
    f_y = sum(['bread' in i for i in example]) 
    # the frequency of milk and bread
    f_x_y = sum([all(w in i for w in ['milk', 'bread']) \
                 for i in example])
    # print out the metrics computed above
    print("N = {}\n".format(N) + "Freq(x) = {}\n".format(f_x) \
          + "Freq(y) = {}\n".format(f_y) \
          + "Freq(x, y) = {}".format(f_x_y))
    

    输出结果如下:

    N = 10
    Freq(x) = 7
    Freq(y) = 5
    Freq(x, y) = 4
    
  2. 计算并打印支持度(Milk https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Bread):

    support = f_x_y / N
    print("Support = {}".format(round(support, 4)))
    

    xy的支持度为0.4。根据经验,如果我们处理的是完整的交易数据集,在许多情况下,这个支持度值会被认为是非常大的。

  3. 计算并打印置信度(Milk https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Bread):

    confidence = support / (f_x / N)
    print("Confidence = {}".format(round(confidence, 4)))
    

    xy的置信度为0.5714。这意味着,给定x已购买,Y 被购买的概率仅略高于 50%。

  4. 计算并打印提升度(Milk https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Bread):

    lift = confidence / (f_y / N)
    print("Lift = {}".format(round(lift, 4)))
    

    xy的提升度为1.1429

  5. 计算并打印杠杆率(Milk https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Bread):

    leverage = support - ((f_x / N) * (f_y / N))
    print("Leverage = {}".format(round(leverage, 4)))
    

    xy的杠杆率为0.05。提升度和杠杆率都可以用来表示xy的关联是正向的(换句话说,x意味着y),但关联较弱。提升度和杠杆率的值分别接近 1 和 0。

  6. 计算并打印确定度(Milk https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Bread):

    conviction = (1 - (f_y / N)) / (1 - confidence)
    print("Conviction = {}".format(round(conviction, 4)))
    

    1.1667的确定度值可以解释为:如果牛奶和面包是独立的,Milk https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png Bread 关联会错误地出现1.1667倍。

    在这个练习中,我们探索了一系列旨在量化两个项目之间关系的概率度量。五个度量分别是支持度、置信度、提升度、杠杆率和确定度。我们将在后续的 Apriori 算法和关联规则中再次使用这些度量,它们是这两者的基础。

    注意

    要访问本节的源代码,请参考packt.live/3fhf9bS

    你还可以在网上运行这个示例,网址是packt.live/303vIBJ

    你必须执行整个 Notebook 才能获得预期的结果。

在实际进行 Apriori 算法和关联规则学习之前,我们将探索交易数据,并加载一些零售数据,为建模做好准备。

交易数据的特点

在市场篮子分析中使用的数据是交易数据或任何类似交易数据的类型。最基本的形式下,交易数据包含某种交易标识符,比如发票或交易号,以及与该标识符关联的产品列表。恰好这两个基本元素就是执行市场篮子分析所需的全部。然而,交易数据很少——甚至可以说从未——以这种最基本的形式存在。交易数据通常包括定价信息、日期和时间、客户标识符等许多其他信息。以下是每个产品如何映射到多个发票的方式:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_10.jpg

图 8.10:每个可用的产品都将映射回多个发票号码

由于交易数据的复杂性,数据清洗至关重要。在市场篮子分析的背景下,数据清洗的目标是过滤掉所有不必要的信息,包括删除数据中不相关的变量,并筛选掉有问题的交易。完成这两步清洗的技术方法因具体的交易数据文件而异。为了避免过度陷入数据清洗,本书从此开始使用 UCI 机器学习库中的一个在线零售数据集的子集,活动则会使用整个数据集。这既限制了数据清洗的讨论,也为我们提供了一个机会,讨论数据集大小变化时结果的变化。这一点非常重要,因为如果你在零售商工作并进行市场篮子分析,了解并能够清晰表达一个事实是非常关键的:随着更多数据的接收,产品之间的关系可能会发生变化,而且很可能会发生变化。在讨论此数据集所需的特定清洗过程之前,我们先来加载在线零售数据集。

注意

在接下来的所有练习和活动中,输出结果可能与下面展示的有所不同。这可能是由于以下两种原因之一:数据加载的问题(换句话说,行数据被打乱)或是mlxtend没有设置种子选项来确保执行结果的一致性。

练习 8.03:加载数据

在本练习中,我们将加载并查看一个示例的在线零售数据集。该数据集最初来自 UCI 机器学习库,可以在packt.live/2XeT6ft找到。下载数据集后,请保存并记录路径。现在,让我们继续进行练习。此练习的输出是将用于未来建模练习的交易数据,以及一些探索性图形,帮助我们更好地理解我们所使用的数据。

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/online+retail#(UCI 机器学习库 [archive.ics.uci.edu/ml]。加利福尼亚州尔湾:加利福尼亚大学信息与计算机科学学院)。

引用:这是从 UCI 机器学习库获取的在线零售数据集的一个子集。Daqing Chen, Sai Liang Sain 和 Kun Guo, 在线零售行业的数据挖掘基于 RFM 模型的数据挖掘客户细分案例研究,《数据库营销与客户战略管理杂志》,第 19 卷,第 3 期,页码 197-208,2012 年。

它可以从packt.live/2XeT6ft下载。

执行以下步骤:

  1. 使用pandas中的read_excel函数加载数据。注意,我们可以通过在read_excel函数中添加header=0来定义第一行作为列名:

    online = pandas.read_excel(io="./Online Retail.xlsx", \
                               sheet_name="Online Retail", \
                               header=0)
    

    注意

    Online Retail.xlsx的路径应根据文件在你系统上的位置进行更改。

  2. 打印出 DataFrame 的前 10 行。注意,数据包含一些与市场篮分析无关的列:

    online.head(10)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_11.jpg

    图 8.11:原始在线零售数据

  3. 打印出 DataFrame 中每列的数据类型。这些信息在执行特定清理任务时非常有用。列需要具备正确的数据类型,才能确保过滤和计算按预期执行:

    online.dtypes
    

    输出如下:

    InvoiceNo              object
    StockCode              object
    Description            object
    Quantity                int64
    InvoiceDate    datetime64[ns]
    UnitPrice             float64
    CustomerID            float64
    Country                object
    dtype: object
    
  4. 获取 DataFrame 的维度,以及唯一发票号码和客户标识的数量:

    print("Data dimension (row count, col count): {dim}" \
          .format(dim=online.shape))
    print("Count of unique invoice numbers: {cnt}" \
          .format(cnt=online.InvoiceNo.nunique()))
    print("Count of unique customer ids: {cnt}" \
          .format(cnt=online.CustomerID.nunique()))
    

    输出如下:

    Data dimension (row count, col count): (541909, 8)
    Count of unique invoice numbers: 25900
    Count of unique customer ids: 4372
    

从前面的输出中,我们可以看到数据成功加载,并且获得了一些关键的信息,这些信息将在本章练习中进一步使用。

注意

若要访问此特定部分的源代码,请参见packt.live/3fhf9bS

你也可以在packt.live/303vIBJ上在线运行此示例。

你必须执行整个 Notebook,才能得到期望的结果。

数据清理与格式化

现在数据集已经加载完成,接下来让我们深入了解具体的数据清理过程。由于我们要将数据过滤到仅包含发票号码和商品,因此我们将数据清理重点放在这两列上。记住,市场篮分析旨在识别所有顾客在一段时间内购买的商品之间的关联。因此,数据清理的主要任务是去除包含非正数商品数量的交易。这种情况可能发生在交易作废时、商品退货时,或交易为行政操作时。这类交易将通过两种方式进行过滤。第一种方式是,取消的交易会在发票号码前加上“C”字母,因此我们会识别出这些特定的发票号码并将其从数据中删除。另一种方式是移除所有商品数量为零或负数的交易。执行完这两个步骤后,数据将被筛选至仅包含发票号码和商品描述两列,并且任何具有至少一个缺失值的两列数据行都将被删除。

数据清洗练习的下一阶段是将数据转换为适合建模的格式。在本练习和后续练习中,我们将使用完整数据的一个子集。该子集将通过取前 5,000 个唯一发票号来创建。一旦我们将数据减少到前 5,000 个唯一发票号,我们将数据结构更改为运行模型所需的结构。请注意,目前数据每行一个商品,因此多件商品的交易占用多行。所需的格式是一个列表的列表,就像本章早些时候提到的虚构数据一样。每个子列表代表一个唯一的发票号,因此在这种情况下,外部列表应包含 5,000 个子列表。子列表的元素是属于该发票号的所有商品。按照描述的清洗过程,让我们继续进行练习。

练习 8.04:数据清洗与格式化

在本练习中,我们将执行之前描述的清洗步骤。在处理过程中,通过打印出数据的当前状态并计算一些基本的汇总指标来监控数据的演变。请确保在加载数据的同一本笔记本中执行数据清洗:

  1. 创建一个指示列,规定发票号是否以 “C” 开头:

    # create new column called IsCPresent
    online['IsCPresent']  = (# looking for C in InvoiceNo column \
                             .astype(str)\
                             .apply(lambda x: 1 if x.find('C') \
                                    != -1 else 0))
    
  2. 筛选出所有包含零个或负数商品(换句话说,商品已退还)的交易,使用第一个步骤创建的列删除所有以"C"开头的发票号,将数据框子集化为InvoiceNoDescription,最后删除所有含有至少一个缺失值的行。将数据框重命名为online1

    online1 = (online\
               # filter out non-positive quantity values\
               .loc[online["Quantity"] > 0]\
               # remove InvoiceNos starting with C\
               .loc[online['IsCPresent'] != 1]\
               # column filtering\
               .loc[:, ["InvoiceNo", "Description"]]\
               # dropping all rows with at least one missing value\
               .dropna())
    
  3. 打印出过滤后的online1数据框的前 10 行:

    online1.head(10)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_12.jpg

    图 8.12:清洗后的在线零售数据集

  4. 打印出清洗后数据框的维度以及通过nunique函数计算的唯一发票号数量,该函数计算数据框列中唯一值的数量:

    print("Data dimension (row count, col count): {dim}"\
          .format(dim=online1.shape)\
    )
    print("Count of unique invoice numbers: {cnt}"\
          .format(cnt=online1.InvoiceNo.nunique())\
    )
    

    输出如下:

    Data dimension (row count, col count): (530693, 2)
    Count of unique invoice numbers: 20136
    

    请注意,我们已经删除了大约 10,000 行和 5,800 个发票号。

  5. 将发票号从数据框中提取为列表。删除重复元素,创建一个唯一发票号的列表。通过打印唯一发票号列表的长度来确认该过程是否成功。与第 4 步的输出进行比较:

    invoice_no_list = online1.InvoiceNo.tolist()
    invoice_no_list = list(set(invoice_no_list))
    print("Length of list of invoice numbers: {ln}" \
          .format(ln=len(invoice_no_list)))
    

    输出如下:

    Length of list of invoice numbers: 20136
    
  6. 第 5 步中获取列表,仅保留前 5,000 个元素。打印出新列表的长度,以确认它确实是预期的 5,000 长度:

    subset_invoice_no_list = invoice_no_list[0:5000]
    print("Length of subset list of invoice numbers: {ln}"\
          .format(ln=len(subset_invoice_no_list)))
    

    输出如下:

    Length of subset list of invoice numbers: 5000
    
  7. 通过只保留前一步骤中列出的发票号来过滤online1数据框:

    online1 = online1.loc[online1["InvoiceNo"]\
                         .isin(subset_invoice_no_list)]
    
  8. 打印出online1的前 10 行:

    online1.head(10)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_13.jpg

    图 8.13:清洗后的数据集,仅包含 5000 个唯一的发票编号

  9. 打印出数据框的维度以及唯一发票编号的数量,以确认过滤和清洗过程成功:

    print("Data dimension (row count, col count): {dim}"\
          .format(dim=online1.shape))
    print("Count of unique invoice numbers: {cnt}"\
          .format(cnt=online1.InvoiceNo.nunique()))
    

    输出如下:

    Data dimension (row count, col count): (133315, 2)
    Count of unique invoice numbers: 5000
    
  10. online1中的数据转换为前述的列表形式,称为invoice_item_list。执行此操作的过程是遍历唯一的发票编号,在每次迭代时,将物品描述提取为一个列表,并将该列表附加到更大的invoice_item_list列表中。打印出列表中的前四个元素:

    invoice_item_list = []
    for num in list(set(online1.InvoiceNo.tolist())):
        # filter dataset down to one invoice number
        tmp_df = online1.loc[online1['InvoiceNo'] == num]
        # extract item descriptions and convert to list
        tmp_items = tmp_df.Description.tolist()
        # append list invoice_item_list
        invoice_item_list.append(tmp_items)
    print(invoice_item_list[1:5])
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_14.jpg

图 8.14:列表中的四个元素

在前述的列表中,每个子列表包含属于单个发票的所有项目。

注意

这个步骤可能需要几分钟才能完成。

在这个练习中,数据框被过滤并子集化为仅包含所需的列和相关的行。接着我们将完整的数据集缩减为前 5000 个唯一的发票编号。完整的数据集将在接下来的活动中使用。最后一步将数据框转换为列表形式,这是数据需要为编码器准备的格式,接下来会讨论该编码器。

注意

要访问该部分的源代码,请参考packt.live/3fhf9bS

你也可以在线运行这个示例,网址为packt.live/303vIBJ

你必须执行整个笔记本才能获得期望的结果。

数据编码

虽然清洗数据至关重要,但数据准备过程最重要的部分是将数据塑造成正确的格式。在运行模型之前,目前以列表形式存在的数据需要进行编码,并重新转换为数据框。为此,我们将利用mlxtend库中的preprocessing模块中的TransactionEncoder。编码器的输出是一个多维数组,其中每行的长度等于交易数据集中唯一项目的总数,元素是布尔变量,表示该特定项目是否与该行表示的发票编号相关联。编码后的数据可以重新转化为数据框,其中行是发票编号,列是交易数据集中的唯一项目。

在接下来的练习中,数据编码将使用 mlxtend 完成,但实际上不使用任何包也可以轻松编码数据。第一步是将列表中的列表拆开,并返回一个包含原始列表中所有值的单一列表。接下来,过滤掉重复的产品,并且如果需要的话,可以按字母顺序对数据进行排序。在进行实际编码之前,我们通过让所有元素初始值为 false,行数等于数据集中的发票号数量,列名为去重后的产品名称列表,来初始化最终的 DataFrame。

在这个案例中,我们有 5,000 个交易和超过 3,100 个独特的产品。因此,DataFrame 中有超过 15,000,000 个元素。实际的编码是通过遍历每个交易和每个交易中的每个商品来完成的。如果第 i 个交易包含第 j 个产品,则将初始化数据集中的第 i 行和第 j 列的单元格值从 false 改为 true。这个双重循环效率不高,因为我们需要遍历 15,000,000 个单元格。虽然有一些方法可以提高性能,包括在 mlxtend 中实现的一些方法,但为了更好地理解这个过程,还是有助于通过双重循环的方法来进行操作。以下是一个示例函数,从零开始进行编码,除了 pandas 外不使用其他包:

def manual_encoding(ll):
    # unlist the list of lists input
    # result is one list with all the elements of the sublists
    list_dup_unsort_items = \
    [element for sub in ll for element in sub]
    # two cleaning steps:
    """
    1\. remove duplicate items, only want one of each item in list
    """
    #     2\. sort items in alphabetical order
    list_nondup_sort_items = \
    sorted(list(set(list_dup_unsort_items)))
    # initialize DataFrame with all elements having False value
    # name the columns the elements of list_dup_unsort_items
    manual_df = pandas.DataFrame(False, \
                                 index=range(len(ll)), \
                                 columns=list_dup_unsort_items)
    """
    change False to True if element is 
    in individual transaction list
    """
    """
    each row is represents the contains of an individual transaction
    """
    # (sublist from the original list of lists)
    for i in range(len(ll)):
        for j in ll[i]:
            manual_df.loc[i, j] = True
    # return the True/False DataFrame
    return manual_df

练习 8.05:数据编码

在本练习中,我们将继续数据准备过程,通过使用前一练习中生成的列表列表并按照特定方式对数据进行编码,以便运行模型:

  1. 初始化并拟合交易编码器。打印结果数据的一个示例:

    online_encoder = mlxtend.preprocessing.TransactionEncoder()
    online_encoder_array = online_encoder\
                           .fit_transform(invoice_item_list)
    print(online_encoder_array)
    

    输出如下:

    [[False False False ... False False False]
     [False False False ... False False False]
     [False False False ... False False False]
     ...
     [False False False ... False False False]
     [False False False ... False False False]
     [False False False ... False False False]]
    

    上述数组包含布尔变量,指示每个交易中是否包含某个产品。

  2. 将编码后的数组重新构建为名为 online_encoder_df 的 DataFrame。打印该 DataFrame 的预定义子集,其中包含 TrueFalse 值:

    online_encoder_df = pandas.DataFrame(online_encoder_array, \
                                         columns=online_encoder\
                                         .columns_)
    """
    this is a very big table, so for more 
    easy viewing only a subset is printed
    """
    online_encoder_df.loc[4970:4979, \
                          online_encoder_df.columns.tolist()[0:8]]
    

    输出将类似于以下内容:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_15.jpg

    图 8.15:重新构建为 DataFrame 的编码数据的一个小部分

  3. 打印编码后的 DataFrame 的维度。它应该有 5,000 行,因为用于生成它的数据之前已被过滤为 5,000 个唯一的发票号:

    print("Data dimension (row count, col count): {dim}"\
          .format(dim=online_encoder_df.shape))
    

    输出将类似于以下内容:

    Data dimension (row count, col count): (5000, 3135)
    

数据现在已经为建模做好准备,我们将在 练习 8.06 中执行 Apriori 算法

注意

要访问此特定部分的源代码,请参阅 packt.live/3fhf9bS

你也可以在 packt.live/303vIBJ 在线运行此示例。

你必须执行整个 Notebook 才能获得所需的结果。

活动 8.01:加载并准备完整的在线零售数据

在这项活动中,我们负责加载和准备大型交易数据集用于建模。最终输出将是一个适当编码的数据集,其中每个唯一交易在数据集中有一行,并且数据集中每个唯一项目有一列。如果一个项目出现在单个交易中,数据框的该元素将标记为true

此活动将主要重复前几个练习,但将使用完整的在线零售数据集文件。不需要执行新的下载,但需要先前下载文件的路径。请在单独的 Jupyter 笔记本中执行此活动。

下列步骤将帮助您完成这项活动:

  1. 加载在线零售数据集文件。

    注意

    此数据集来源于archive.ics.uci.edu/ml/datasets/online+retail#(UCI 机器学习库[archive.ics.uci.edu/ml]。加州尔湾:加州大学信息与计算机科学学院)。

    引用:Daqing Chen,Sai Liang Sain 和 Kun Guo,《面向在线零售行业的数据挖掘:基于数据挖掘的 RFM 模型客户分割的案例研究》,《数据库营销和客户战略管理杂志》,第 19 卷,第 3 期,第 197-208 页,2012 年。

    可以从packt.live/39Nx3iQ下载。

  2. 清理和准备建模数据,包括将清理后的数据转换为列表的列表。

  3. 编码数据并将其重新转换为数据框。

    注意

    此活动的解决方案可以在第 490 页找到。

输出将类似于以下内容:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_16.jpg

图 8.16:从完整在线零售数据集构建的已清理、编码和重新转换的数据框的子集

Apriori 算法

Apriori 算法是识别和量化交易数据中频繁项集的数据挖掘方法论,是关联规则学习的基础组件。将 Apriori 算法的结果扩展到关联规则学习将在下一节讨论。项集被认为频繁的阈值是模型的输入(即超参数),因此可调整。在这里频率被量化为支持度,因此输入到模型中的值是分析中所接受的最小支持度。然后,模型识别所有支持度大于或等于模型提供的最小支持度的项集。

注意

最小支持超参数不是可以通过网格搜索优化的值,因为 Apriori 算法没有评估度量标准。相反,最小支持参数是基于数据、用例和领域专业知识设置的。

Apriori 算法背后的主要思想是 Apriori 原理:任何频繁物品集的子集必须本身也是频繁的。

另一个值得提及的方面是推论:不频繁物品集的任何超集都不能是频繁的。

让我们举一些例子。如果物品集{锤子、锯子和钉子}是频繁的,那么根据 Apriori 原理,并且这应该是显而易见的,从中派生的任何更简单的物品集,例如{锤子、锯子},也是频繁的。相反,如果同样的物品集{锤子、锯子、钉子}是不频繁的,那么通过增加复杂性,如将木材加入物品集{锤子、锯子、钉子、木材},是不会使该物品集变得频繁的。

对于事务数据库中的每个物品集计算支持度值,并仅返回那些支持度大于或等于预设最小支持度阈值的物品集,似乎很简单,但由于所需的计算量,这并非易事。例如,假设有一个包含 10 个独特物品的物品集,这将导致 1,023 个单独的物品集需要计算支持度值。现在,试着推算我们的工作数据集,它包含 3,135 个独特物品。这将是一个庞大的物品集数量,我们需要为这些物品集计算支持度值。计算效率是一个重大问题:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_17.jpg

图 8.17:计算效率问题的表现

前面的图示展示了物品集的构建方式以及 Apriori 原理如何显著降低计算需求(所有灰色节点都是不频繁的)。

为了解决计算需求,Apriori 算法被定义为一个自底向上的模型,包含两个步骤。这些步骤涉及通过向已有的频繁物品集添加物品来生成候选物品集,并将这些候选物品集与数据集进行测试,以确定这些候选物品集是否也频繁。对于包含不频繁物品集的物品集,不计算支持度值。该过程会重复,直到没有更多的候选物品集为止:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_18.jpg

图 8.18:一般 Apriori 算法结构

假设最低支持度阈值为 0.4,前面的图示展示了 Apriori 算法的一般结构。

图 8.20包括建立物品集、计算支持度值、筛选掉不频繁的物品集、创建新物品集,并重复这一过程。

有一个明确的树状结构,作为识别候选物品集的路径。所使用的具体搜索技术是广度优先搜索,它是为遍历树状数据结构而构建的,这意味着搜索过程的每一步都集中在完全搜索树的一个层级之后再继续,而不是逐个分支地进行搜索。

算法的高层步骤旨在完成以下任务:

  1. 定义频繁项集(换句话说,只选择那些支持度大于预定义阈值的项)。通常,首先选择的是单个项集。

  2. 通过组合频繁项集来推导候选项集。每次只增加一个项的大小。也就是说,从包含一个项的项集开始,逐步扩展到包含两个项、三个项,以此类推。

  3. 计算每个候选项集的支持度。

  4. 创建一个新的频繁项集,由支持度超过指定阈值的候选项集组成。

重复步骤 1步骤 4,直到没有更多的频繁项集为止;也就是说,直到我们处理完所有的组合。

Apriori 算法的伪代码如下:

L1 = {frequent items}
k = 1
L = {}
while Lk.Length is not an empty set do
    Ck+1 = candidate item sets derived from Lk
    For each transaction t in the dataset do
        Increment the count of the candidates \
            in Ck+1 that appear in t
    Compute the support for the candidates in Ck+1 \
        using the appearance counts
    Lk+1 = the candidates in Ck+1 meeting \
          the minimum support requirement
    L.append(Lk)
    k = k + 1
End
Return L = all frequent item sets with corresponding support values

尽管遵循 Apriori 原则,该算法仍可能面临显著的计算挑战,这取决于事务数据集的大小。目前有几种公认的策略可以进一步减少计算需求。

计算修正

事务简化是一种减少计算负载的简单方法。注意,在生成每个候选项集之后,需要扫描整个事务数据集,以统计每个候选项集的出现次数。如果我们能缩小事务数据集的大小,数据集扫描的规模将大大减少。事务数据集的缩小是通过实现以下方式:任何在第i次迭代中不包含频繁项集的事务,在后续迭代中也不会包含任何频繁项集。因此,一旦每个事务不包含频繁项集,就可以从用于未来扫描的事务数据集中移除。

对事务数据集进行抽样,并在其上测试每个候选项集,是另一种减少计算需求的方法,这种方法可以避免扫描事务数据集来计算每个项集的支持度。在实现这种方法时,重要的是降低最小支持度要求,以确保不会遗漏任何应包含在最终数据中的项集。由于抽样的事务数据集会自然导致支持度值较小,因此将最小支持度保持在原始值可能会错误地删除那些应该是频繁项集的项集。

一种类似的方法是分区。在这种情况下,数据集被随机划分为多个单独的数据集,每个数据集上都会执行对每个候选项集的评估。如果一个项集在其中一个分区中是频繁的,那么它就被认为在完整事务数据集中是频繁的。每个分区依次扫描,直到确定一个项集的频率。如果频率在第一个分区中得以确定,那么我们就不需要在大多数分区上测试它,就可以为整个数据集确立该频率。像抽样一样,分区也是另一种避免在完整数据集上测试每个项集的方法,如果完整数据集非常大,测试会非常耗费计算资源。

无论是否采用这些技术,Apriori 算法的计算需求通常都会相当庞大。如现在所见,算法的核心——支持度的计算——并不像本书中讨论的其他模型那样复杂。

练习 8.06:执行 Apriori 算法

使用 mlxtend 执行 Apriori 算法变得非常简单。因此,本练习将重点关注如何处理输出数据集并解读结果。你会记得,清理并编码后的事务数据被定义为 online_encoder_df

注意

在与之前所有练习相同的笔记本中执行此练习,因为我们将继续使用该笔记本中已经建立的环境、数据和结果。(因此,你应该使用包含 5,000 张发票的简化数据集的笔记本,而不是在活动中使用的完整数据集。)

  1. 使用 mlxtend 运行 Apriori 算法,而不改变任何默认的参数值:

    mod = mlxtend.frequent_patterns.apriori(online_encoder_df)
    mod
    

    输出是一个空的 DataFrame。默认的最小支持度值设置为 0.5,因此由于返回的是空的 DataFrame,我们知道所有项集的支持度都低于 0.5。根据事务的数量和可用项的多样性,没有任何项集具有超过 0.5 的支持度值并不奇怪。

  2. 重新运行 Apriori 算法,但将最小支持度设置为 0.01:

    mod_minsupport = mlxtend.frequent_patterns\
                     .apriori(online_encoder_df, \
                     min_support=0.01)
    mod_minsupport.loc[0:6]
    

    这个最小支持度值相当于在分析 5,000 个事务时,需要一个项集出现 50 次才会被认为是频繁的。如前所述,最小支持度可以设置为 [0,1] 范围内的任何值。没有最佳的最小支持度值;该值的设置完全是主观的。许多企业有自己特定的显著性阈值,但没有行业标准或优化此值的方法。

    输出将类似于以下内容:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_19.jpg

    图 8.19:使用 mlxtend 运行的 Apriori 算法的基本输出

    请注意,输出中的项集以数字形式标识,这使得结果难以解读。

  3. 重新运行 Apriori 算法,使用与 步骤 2 中相同的最小支持度,但这次将 use_colnames 设置为 True。这将用实际的项名替换数值标识:

    mod_colnames_minsupport = mlxtend.frequent_patterns\
                              .apriori(online_encoder_df, \
                                       min_support=0.01, \
                                       use_colnames=True)
    mod_colnames_minsupport.loc[0:6]
    

    输出将类似于以下内容:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_20.jpg

    图 8.20:Apriori 算法输出,实际项名代替了数值标识

    这个 DataFrame 包含了所有支持度大于指定最小支持度值的项集。也就是说,这些项集出现的频率足够高,有可能是有意义的,因此是可操作的。

  4. 步骤 3 的输出中添加一个额外的列,包含项集的大小(换句话说,就是该项集中有多少项),这将有助于筛选和进一步分析:

    mod_colnames_minsupport['length'] = \
    (mod_colnames_minsupport['itemsets'].apply(lambda x: len(x)))
    mod_colnames_minsupport.loc[0:6]
    

    输出将类似于以下内容:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_21.jpg

    图 8.21:Apriori 算法输出加上一个额外的列,包含项集的长度

  5. 查找包含 10 COLOUR SPACEBOY PEN 的项集的支持度:

    mod_colnames_minsupport[mod_colnames_minsupport['itemsets'] \
                            == frozenset({'10 COLOUR SPACEBOY PEN'})]
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_22.jpg

    图 8.22:筛选至单个项集的输出 DataFrame

    这个单行 DataFrame 给出了包含一个项的特定项集的支持度值。支持度值表示该特定项集出现在 1.78% 的交易中。

  6. 返回所有支持度在 [0.02, 0.021] 范围内,且长度为 2 的项集:

    mod_colnames_minsupport[(mod_colnames_minsupport['length'] == 2) \
                             & (mod_colnames_minsupport\
                                ['support'] >= 0.02) \
                             & (mod_colnames_minsupport\
                                ['support'] < 0.021)]
    

    输出将类似于以下内容:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_23.jpg

    图 8.23:通过长度和支持度筛选后的 Apriori 算法输出 DataFrame

    这个 DataFrame 包含了所有支持度值在步骤开始时指定的范围内的项集(即一对一起购买的商品)。每个项集出现在 2.0% 到 2.1% 的交易中。

    注意,当按 support 进行筛选时,最好指定一个范围,而不是一个具体值,因为很可能会选择一个没有项集的值。前面的输出中有 32 个项集;这里只显示了一个子集。记住这些项集中的具体项,因为我们将在扩展到完整数据时使用相同的筛选条件,并且需要执行对比。

  7. 绘制支持度值图。注意,该图不会包含支持度小于 0.01 的数据,因为该值作为 步骤 2 中的最小支持度值:

    mod_colnames_minsupport.hist("support", grid=False, bins=30)
    plt.xlabel("Support of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Support")
    plt.show()
    

    输出将类似于以下图表:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_24.jpg

图 8.24:Apriori 算法返回的支持度值分布

最大支持值大约为 0.14,约为 700 次交易。看似较小的值可能并不小,因为涉及的产品数量较多。产品数量越大,支持值往往越低,因为商品组合的变动性增加。

注意

要访问此特定部分的源代码,请参考packt.live/3fhf9bS

您也可以在线运行此示例,网址为packt.live/303vIBJ

您必须执行整个笔记本才能获得期望的结果。

希望您能想到更多使用这些数据的方法。在下一节中,我们将通过使用 Apriori 算法结果生成关联规则,产生更多有用的信息。

活动 8.02:在完整的在线零售数据集上运行 Apriori 算法

假设您为一家在线零售商工作。您获得了上个月所有的交易数据,并被告知找出至少出现在 1%交易中的商品集。一旦识别出符合条件的商品集,接下来您需要找出支持度值的分布情况。支持度值的分布将告知所有相关方,哪些商品组合是以较高概率共同购买的,并给出支持度值的均值。让我们收集所有这些信息,供公司领导层和战略人员参考。

在此活动中,您将对完整的在线零售数据集运行 Apriori 算法。

注意

该数据集来源于archive.ics.uci.edu/ml/datasets/online+retail#(UCI 机器学习库 [archive.ics.uci.edu/ml])。加利福尼亚大学尔湾分校信息与计算机科学学院。

引用:Daqing Chen, Sai Liang Sain, 和 Kun Guo, 面向在线零售行业的数据挖掘:基于 RFM 模型的客户细分案例研究,《数据库营销与客户战略管理期刊》,第 19 卷,第 3 期,197-208 页,2012 年。

它可以从packt.live/39Nx3iQ下载。

确保在与前一个活动相同的笔记本中完成此活动(换句话说,即使用完整数据集的笔记本,而不是使用 5,000 个发票子集的笔记本,后者是用于练习的)。

这也为您提供了一个机会,将结果与仅使用 5,000 笔交易生成的结果进行比较。这是一个有趣的活动,因为它能提供一些关于随着更多数据收集,数据变化的方式,以及在使用分区技术时支持度值变化的见解。请注意,在练习中所做的工作并不是分区技术的完美代表,因为 5,000 笔交易是随机抽取的一个样本量。

注意

本章中的所有活动需要在同一个笔记本中进行。

以下步骤将帮助你完成这个活动:

  1. 在整个数据集上运行 Apriori 算法,并设置合理的参数。

  2. 将结果筛选到包含10 COLOUR SPACEBOY PEN的项集。将支持度值与练习 8.06执行 Apriori 算法中的支持度值进行比较。

  3. 添加一列包含项集长度的列。然后筛选出那些长度为二且支持度在[0.02, 0.021]范围内的项集。将此结果与练习 8.06执行 Apriori 算法中的结果进行比较。

  4. 绘制支持度值。

    注意

    此活动的解决方案可以在第 492 页找到。

此活动的输出结果将类似于以下内容:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_25.jpg

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_25.jpg)

图 8.25:支持度值的分布

关联规则

关联规则学习是一种机器学习模型,旨在发掘交易数据中的隐藏模式(换句话说,就是描述任何零售商顾客购物习惯的关系)。关联规则的定义在本章早些时候定义并解释的常见概率度量中有所暗示。

考虑一个虚拟的频繁项集{牛奶,面包}。可以从该项集形成两个关联规则:牛奶 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png 面包和面包 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_Formula_06.png 牛奶。为了简便起见,关联规则中的第一个项集称为前提,而第二个项集称为后果。一旦识别出关联规则,就可以计算所有之前讨论的度量,以评估关联规则的有效性,进而决定这些规则是否能在决策过程中加以利用。

关联规则的建立基于支持度和置信度。正如我们在上一节中讨论的,支持度用于识别哪些项集是频繁的,而置信度衡量特定规则的真实性频率。置信度通常被视为有趣性度量之一,因为它是决定是否形成关联规则的度量之一。因此,建立关联规则是一个两步过程。首先识别频繁的数据集,然后评估候选关联规则的置信度,如果该置信度值超过某个临界值,则结果为一个关联规则。

关联规则学习的一个主要问题是发现虚假关联,考虑到潜在规则的巨大数量,这些虚假关联很可能存在。所谓虚假关联是指在数据中出现惊人的频繁性,考虑到这种关联完全是偶然发生的。为了清楚地阐明这个观点,假设我们处于这样一种情况:我们有 100 条候选规则。如果我们以 0.05 的显著性水平运行独立性统计测试,我们仍然面临 5% 的机会,即当不存在关联时也发现关联的可能性。再进一步假设这 100 条候选规则都不是有效的关联。考虑到这 5% 的机会,我们仍然期望找到 5 条有效的关联规则。现在,将想象的候选规则列表扩展到数百万或数十亿,以便 5% 对应大量的关联。这个问题与几乎每个模型都面临的统计显著性和误差问题类似。值得注意的是,一些技术用于应对虚假关联问题,但它们既没有被一贯地纳入常用的关联规则库中,也不在本章的范围内。

现在让我们将我们对关联规则学习的工作知识应用到在线零售数据集中。

练习 8.07:推导关联规则

在这个练习中,我们将为在线零售数据集推导关联规则并探索相关指标。

注意

确保您在与之前练习相同的笔记本中完成此练习(换句话说,使用了 5,000 发票子集的笔记本,而不是来自活动的完整数据集)。

  1. 使用 mlxtend 库为在线零售数据集推导关联规则。将置信度作为趣味度量,将最小阈值设置为 0.6,并返回所有指标,而不仅仅是支持度:

    rules = mlxtend.frequent_patterns\
            .association_rules(mod_colnames_minsupport, \
            metric="confidence", \
            min_threshold=0.6,  \
            support_only=False)
    rules.loc[0:6]
    

    输出类似如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_26.jpg

    图 8.26:仅使用 5,000 笔交易生成的前七行关联规则

  2. 打印关联数量:

    print("Number of Associations: {}".format(rules.shape[0]))
    

    找到了 1,064 条关联规则。

    注意

    关联规则的数量可能有所不同。

  3. 尝试运行模型的另一个版本。选择任意最小阈值和任意的趣味度度量。探索返回的规则:

    rules2 = mlxtend.frequent_patterns\
             .association_rules(mod_colnames_minsupport, \
             metric="lift", \
             min_threshold=50,\
             support_only=False)
    rules2.loc[0:6]
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_27.jpg

    图 8.27:关联规则的前七行

  4. 打印关联数量:

    print("Number of Associations: {}".format(rules2.shape[0]))
    

    使用提升度度量和最小阈值 50 找到的关联规则数量为 176,这比 步骤 2 中的数量显著低。我们将在后续步骤中看到 50 是相当高的阈值,因此返回的关联规则较少并不令人意外。

  5. 绘制置信度与支持度的图表,并识别数据中的特定趋势:

    rules.plot.scatter("support", "confidence", \
                       alpha=0.5, marker="*")
    plt.xlabel("Support")
    plt.ylabel("Confidence")
    plt.title("Association Rules")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_28.jpg

    图 8.28:置信度与支持度的关系图

    注意,既具有极高置信度又具有极高支持度的关联规则并不存在。这应该是有道理的。如果一个项集有高支持度,那么这些项很可能会与许多其他项一起出现,这使得置信度很难达到很高。

  6. 看看置信度的分布:

    rules.hist("confidence", grid=False, bins=30)
    plt.xlabel("Confidence of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Confidence")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_29.jpg

    图 8.29:置信度值的分布

  7. 现在,看看提升度的分布:

    rules.hist("lift", grid=False, bins=30)
    plt.xlabel("Lift of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Lift")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_30.jpg

    图 8.30:提升度值的分布

    如前所述,这个图表显示50是一个较高的阈值,因为很少有数据点超过这个值。

  8. 现在,看看杠杆的分布:

    rules.hist("leverage", grid=False, bins=30)
    plt.xlabel("Leverage of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Leverage")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_31.jpg

    图 8.31:杠杆值的分布

  9. 现在,看看信念度的分布:

    plt.hist(rules[numpy.isfinite(rules['conviction'])]\
                                  .conviction.values, bins = 30)
    plt.xlabel("Coviction of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Conviction")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_32.jpg

图 8.32:信念度值的分布

有趣的是,四个分布的图表顶部出现了不同大小的尖峰,这意味着存在一些非常强的关联规则。置信度分布随着置信值增大而逐渐减少,但在最右端,接近最大值时,分布会稍微上升。提升度分布有最明显的尖峰。信念度分布图在 50 左右显示出一个小尖峰,也许更准确地说是一个小突起。最后,杠杆分布在较高的值处并没有显示出尖峰,但它具有一个较长的尾部,包含一些非常高的杠杆值。

花些时间探索模型发现的关联规则。这些产品配对对你来说合理吗?当你改变模型参数值时,关联规则的数量发生了什么变化?你是否意识到这些规则在试图改善任何零售业务时可能产生的影响?

在之前的练习中,我们构建并绘制了关联规则。关联规则可能很难解释,并且在很大程度上依赖于用于创建它们的阈值和度量标准。上一段中的问题旨在激发您思考算法如何工作以及如何使用规则。我们逐一讨论这些问题。显然有很多规则,所以关于这些规则是否合理的问题很难一概而论。对一些规则对的抽查似乎表明这些规则是合理的。例如,三组对包括儿童杯子和碗、茶杯和盘子、以及游乐屋厨房和客厅,这些都很有道理。当度量标准和参数发生变化时,结果也会发生变化。正如几乎所有建模练习中的情况一样,理想的做法是查看在不同情况下的结果,并利用所有发现做出最佳决策。

注意

要访问此特定部分的源代码,请参考packt.live/3fhf9bS

您还可以在packt.live/303vIBJ在线运行此示例。

您必须执行整个笔记本才能获得所需的结果。

活动 8.03:在完整的在线零售数据集上查找关联规则

让我们继续进行活动 8.02中设定的场景,在完整的在线零售数据集上运行 Apriori 算法。公司领导回来说,知道每个商品集在数据集中出现的频率非常好,但我们可以采取哪些商品集进行操作呢?哪些商品集可以用于改变商店布局或调整定价?为了找到这些答案,我们推导出完整的关联规则。

在此活动中,让我们从完整的在线零售交易数据集中推导关联规则。确保在使用完整数据集的笔记本中完成此活动(换句话说,要在包含完整零售数据集的笔记本中进行,而不是使用 5,000 个发票子集的练习笔记本)。

以下步骤将帮助我们执行此活动:

  1. 在完整的数据集上拟合关联规则模型。使用置信度度量标准和0.6的最低阈值。

  2. 统计关联规则的数量。这个数量与练习 8.07步骤 1中的数量是否不同?推导关联规则

  3. 绘制置信度与支持度的关系图。

  4. 查看置信度、提升度、杠杆度和置信度的分布。

预期的关联规则输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_33.jpg

图 8.33:预期的关联规则

注意

此活动的解决方案可在第 496 页找到。

在此活动结束时,您将获得提升度、杠杆度和置信度的图表。

总结

市场篮子分析用于分析和提取交易或类似交易的数据洞察,这些洞察可以帮助推动许多行业的发展,最著名的就是零售行业。这些决策可能包括如何布局零售空间、哪些产品进行折扣、以及如何定价产品。市场篮子分析的核心支柱之一是关联规则的建立。关联规则学习是一种机器学习方法,用于揭示个体购买产品之间的关联,这些关联足够强大,可以用来做商业决策。关联规则学习依赖于 Apriori 算法,以计算高效的方式找到频繁项集。这些模型不同于典型的机器学习模型,因为它们不进行预测,结果也无法用任何单一指标进行评估,参数值不是通过网格搜索选择的,而是根据特定问题的领域需求来选择的。尽管如此,所有机器学习模型核心的模式提取目标在这里依然是存在的。

本章结束时,你应该能够舒适地评估和解读概率性指标,能够使用mlxtend运行和调整 Apriori 算法及关联规则学习模型,并了解这些模型在商业中的应用。你应该知道,你所在社区的杂货店内物品的定位和定价,可能是基于你和许多其他顾客过去的购买行为来做出的选择!

在下一章,我们将探讨使用核密度估计的热点分析,核密度估计无疑是所有统计学和机器学习中最常用的算法之一。

第十章:9. 热点分析

概述

在本章中,我们将进行热点分析,并可视化热点分析的结果。我们将使用核密度估计,这是构建分布时最常用的算法,使用的是一组观察值。我们将构建核密度估计模型,并描述概率密度函数背后的基本原理。在本章结束时,你应该能够利用 Python 库构建多维密度估计模型,并处理地理空间数据。

引言

在前一章中,我们探讨了市场篮子分析。市场篮子分析,正如你希望记得的那样,是一种算法,旨在理解交易数据中所有项目和项目组之间的关系。这些关系随后被用来帮助零售商优化店铺布局、更加准确地订货库存,并在不减少每笔交易中的商品数量的情况下调整价格。现在,我们将转向探索热点建模。

让我们考虑一个假设场景:一种新的疾病开始在你所在国家的多个社区中蔓延,政府正在努力寻找应对这一公共卫生紧急情况的方法。应对这一健康危机的关键是流行病学知识,包括患者所在的位置以及疾病的传播情况。定位和量化问题区域(通常称为热点)能够帮助卫生专业人员、政策制定者和应急响应团队制定最有效和高效的抗疫策略。这个场景突出了热点建模的众多应用之一。

热点建模是一种用于识别一个群体在地理区域分布情况的方法;例如,如何将之前提到的疾病感染者的群体分布在全国各地。创建这种分布依赖于代表性样本数据的可用性。请注意,群体可以是任何在地理学上可定义的事物,包括但不限于犯罪、感染疾病的个体、具有某些人口特征的人群或飓风等:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_01.jpg

图 9.1:一个虚构的火灾位置数据示例,展示了一些潜在的热点区域

热点分析非常流行,这主要是因为它在可视化和解释结果方面非常容易。报纸、网站、博客和电视节目都利用热点分析来支持其中的论点、章节和话题。尽管它可能不像最流行的机器学习模型那样广为人知,但主要的热点分析算法,即核密度估计,无疑是最广泛使用的分析技术之一。人们甚至在日常生活中不自觉地进行核密度估计。核密度估计是一种热点分析技术,用于估计特定地理事件的真实人口分布。在深入了解算法本身之前,我们需要简要回顾一下空间统计学和概率密度函数。

空间统计学

空间统计学是统计学的一个分支,专注于分析具有空间属性的数据,包括地理或拓扑坐标。它与时间序列分析类似,目标是分析在某个维度上变化的数据。在时间序列分析中,数据变化的维度是时间,而在空间统计学中,数据则在空间维度上发生变化。空间统计学涵盖了多种技术,其中我们这里关注的技术是核密度估计。与大多数统计分析的目标相同,在空间统计学中,我们试图通过采集地理数据样本来生成洞察并做出预测。地震分析是空间统计分析常用的一个领域。通过收集地震位置数据,可以生成显示高低地震概率区域的地图,这可以帮助科学家确定未来地震可能发生的地点以及地震强度的预期。

概率密度函数

核密度估计采用了概率密度函数PDF)的思想,这是统计学中的基本概念之一。概率密度函数是描述连续随机变量行为的函数。也就是说,它表达了随机变量取某个范围内值的可能性或概率。以美国男性身高为例,利用美国男性身高的概率密度函数,我们可以计算出某位美国男性身高在 1.9 米到 1.95 米之间的概率。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_02.jpg

图 9.2:标准正态分布

统计学中最常见的密度函数可能是标准正态分布,它就是以零为中心,标准差为一的正态分布。

与密度函数不同,统计学家或数据科学家通常获得的是来自未知总体分布的随机收集的样本值。这时,核密度估计就派上了用场;它是一种利用样本数据估计随机变量未知概率密度函数的技术。下图表示了一个简单但更合理的分布示例,这是我们希望通过核密度估计来估计的分布。我们会选择一些观察值(样本数据点),并使用这些观察值来创建一个平滑的分布,模拟我们无法知晓的真实底层分布。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_03.jpg

图 9.3:三种正态分布的混合

在商业中使用热点分析

我们已经提到了一些热点建模可以有效影响行业的方法。在报告传染病时,卫生组织和媒体公司通常会使用热点分析来传达疾病的地理分布和根据地理位置感染该疾病的可能性。通过使用热点分析,这些信息可以可靠地计算和传播。热点分析非常适用于处理健康数据,因为可视化非常直观。这意味着数据被故意或无意误解的可能性相对较低。

热点分析也可以用来预测某些事件可能发生的地理位置。越来越多的研究领域正在利用热点分析的预测能力,其中包括自然灾害和极端天气事件的研究。以地震为例,由于重大地震之间的时间间隔可能很长,而且需要跟踪和测量地震的设备相对较新,因此地震预测一直以难度大著称。

在公共政策和资源部署方面,热点分析在处理人口统计学分析时非常有影响力。确定资源(包括金钱和人员)应该部署到哪里可能是具有挑战性的;然而,鉴于资源通常是人口特定的,热点分析是一种有用的技术,因为它可以用来确定特定人口特征的分布。我们所说的人口特征是指我们可以找到高中毕业生、来自特定全球地区的移民,或者年收入达到或超过$100,000 的人的地理分布。

热点建模的应用几乎是无穷无尽的。我们这里只讨论了其中的三种主要应用。

核密度估计

热点分析的主要方法之一是核密度估计。核密度估计通过样本数据和两个参数(即核函数带宽值)构建估计的密度。估计的密度像任何分布一样,本质上是随机变量行为的指导。这里的意思是,随机变量在任何特定值{x1, ……, xn}上出现的频率。在处理通常是地理数据的热点分析时,估计的密度回答了这个问题:给定事件的特定经纬度对出现的频率如何? 如果某个特定经纬度对{xlongitude, xlatitude}以及附近的其他经纬度对出现的频率很高,那么使用样本数据构建的估计密度将显示出上述经纬度对周围区域的出现概率较高。

核密度估计被称为一种平滑算法,因为它在样本数据上绘制了一条平滑曲线。如果数据是一个具有代表性的样本,这条曲线可以很好地估计真实的总体密度函数。换句话说,当核密度估计方法正确应用时,它旨在去除样本数据中固有的噪声,而这些噪声并不是总体特征。该模型的唯一假设是数据确实属于某种可解释且有意义的密度,从中可以获得见解并付诸实践。也就是说,存在一个真实的潜在分布。我们假设样本数据中包含数据点簇,这些簇与真实总体中的高概率区域对齐。创建真实总体密度的高质量估计的一个好处是,估计的密度可以用于从总体中采样更多的数据。

在这一简短的介绍之后,您可能会有以下两个问题:

  • 什么是带宽值?

  • 什么是核函数?

接下来我们将回答这两个问题。

带宽值

核密度估计中最关键的参数被称为带宽值,它对估计质量的影响不容小觑。带宽值的高阶定义是它决定了平滑的程度。如果带宽值较低,则估计的密度会呈现有限的平滑,这意味着密度将捕捉到样本数据中的所有噪声。如果带宽值较高,则估计的密度将非常平滑。过于平滑的密度会去除估计密度中的真实密度特征,而这些特征是合法的,而不是噪声。

用更统计学的术语来说,带宽参数控制着偏差-方差的权衡。也就是说,低带宽值会导致高方差,因为密度对样本数据的方差非常敏感。低带宽值会限制模型适应和解决样本数据中不存在于总体中的空隙的能力。使用低带宽值估计的密度往往会过度拟合数据(这也称为欠平滑的密度)。当使用高带宽值时,结果密度会发生欠拟合,估计密度的偏差较大(这也称为过度平滑的密度)。

注意

在所有随后的练习和活动中,输出可能会略有不同于下文所示的结果。原因如下:数据样本的差异可能会导致输出略有不同,而且sklearnseaborn库中有一些非确定性元素,可能导致结果在每次运行时有所不同。

练习 9.01:带宽值的影响

在本练习中,我们将使用九个不同的带宽值拟合九个不同的模型,来处理本练习中创建的样本数据。此处的目标是巩固我们对带宽参数影响的理解,并明确指出,如果要得到准确的估计密度,带宽值需要谨慎选择。请注意,寻找最佳带宽值将是下一节的主题。所有练习都将在使用 Python 3 的 Jupyter 笔记本中完成;请确保通过pip安装所有必要的包。安装mpl_toolkits中的basemap模块最简单的方法是使用Anaconda。有关下载和安装Anaconda的说明,请参见本书开头:

  1. 加载本章所需的所有库。basemap库用于创建涉及位置数据的图形。其他所有库都在本书的前面部分使用过。

    get_ipython().run_line_magic('matplotlib', 'inline')
    import matplotlib.pyplot as plt
    import mpl_toolkits.basemap
    import numpy
    import pandas
    import scipy.stats
    import seaborn
    import sklearn.model_selection
    import sklearn.neighbors
    seaborn.set()
    
  2. 通过混合三个正态分布来创建一些样本数据(vals)。除了样本数据外,还需要定义真实的密度曲线(true_density)和数据将被绘制的范围(x_vec):

    x_vec = numpy.linspace(-30, 30, 10000)[:, numpy.newaxis]
    numpy.random.seed(42)
    vals = numpy.concatenate(( \
           numpy.random.normal(loc=1, scale=2.5, size=500), \
           numpy.random.normal(loc=10, scale=4, size=500), \
           numpy.random.normal(loc=-12, scale=5, size=500) \
    ))[:, numpy.newaxis]
    true_density = ((1 / 3) * scipy.stats.norm(1, 2.5)\
                              .pdf(x_vec[:, 0]) \
                    + (1 / 3) * scipy.stats.norm(10, 4)\
                                .pdf(x_vec[:, 0]) \
                    + (1 / 3) * scipy.stats.norm(-12, 5)\
                                .pdf(x_vec[:, 0]))
    
  3. 定义一个元组列表,用于指导多图形的创建。每个元组包含特定子图的行和列索引,以及用于在该子图中创建估计密度的带宽值。请注意,为了本练习的方便,带宽值是随机选择的,但实际上选择最佳带宽值是有一定策略的。我们将在下一节深入探讨这一点。

    position_bandwidth_vec = [(0, 0, 0.1), (0, 1, 0.4), (0, 2, 0.7), \
                              (1, 0, 1.0), (1, 1, 1.3), (1, 2, 1.6), \
                              (2, 0, 1.9), (2, 1, 2.5), (2, 2, 5.0)]
    
  4. 创建九个图,每个图使用不同的带宽值。第一个图(索引为(0, 0))将使用最低的带宽值,最后一个图(索引为(2, 2))将使用最高的带宽值。这些值不是绝对的最小或最大带宽值,而只是前一步骤中定义的列表中的最小值和最大值:

    fig, ax = plt.subplots(3, 3, sharex=True, \
                           sharey=True, figsize=(12, 9))
    fig.suptitle('The Effect of the Bandwidth Value', fontsize=16)
    for r, c, b in position_bandwidth_vec:
        kde = sklearn.neighbors.KernelDensity(bandwidth=b).fit(vals)
        log_density = kde.score_samples(x_vec)
        ax[r, c].hist(vals, bins=50, density=True, alpha=0.5)
        ax[r, c].plot(x_vec[:, 0], numpy.exp(log_density), \
                      '-', linewidth=2)
        ax[r, c].set_title('Bandwidth = {}'.format(b))
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_04.jpg

图 9.4:一个 3 x 3 矩阵的子图

请注意,在第九个子图中(带宽为 5 的地方),估计的密度曲线明显不足以拟合数据。随着带宽值的增加,估计的密度变得更加平滑,直到它明显不足以拟合数据。从视觉效果来看,最优带宽值可能约为1.6

注意

要访问该特定部分的源代码,请参考 packt.live/2UOHbTZ

您还可以在网上运行此示例,网址为 packt.live/38DbmTo

您必须执行整个 Notebook 才能获得预期的结果。

下一步是设计一个算法来识别最优带宽值,以使估计的密度最合理,从而是最可靠且可操作的。

选择最优带宽

如前面练习中提到的,我们可以通过简单地通过视觉比较几种密度来接近选择最优带宽。然而,这既不是选择参数值的最有效方法,也不是最可靠的方法。

优化带宽值有两种标准方法,这两种方法都会出现在未来的练习和活动中。第一种方法是插件法(或公式化方法),它是确定性的,并且没有在样本数据上进行优化。插件法通常实现速度更快,编码更简单,解释也更容易。然而,这些方法有一个大缺点,那就是与在样本数据上进行优化的方法相比,它们的准确性往往较低。这些方法还存在分布假设。最流行的插件方法是 Silverman 法则和 Scott 法则。详细解释这些法则超出了本文的范围,并且对于完全理解核密度估计并非必需,且需要一些复杂的数学工作,因此我们将跳过进一步的探讨。不过,如果有兴趣,公开的许多优秀资源都详细解释了这些法则,并且有不同深度的说明。默认情况下,seaborn包(将在未来的练习中使用)使用 Scott 法则作为确定带宽值的方法。

寻找最优带宽值的第二种方法,也是更强健的方法,是通过搜索一个预定义的带宽值网格。网格搜索是一种经验性方法,在机器学习和预测建模中经常用来优化模型的超参数。该过程从定义带宽网格开始,带宽网格就是要评估的一系列带宽值。带宽网格是随机选择的。使用网格中的每个带宽值来创建估计密度;然后,使用伪对数似然值来评分估计密度。最优带宽值是具有最大伪对数似然值的那个带宽值。可以把伪对数似然值看作是获得数据点的概率,数据点出现在我们希望的地方,而没有数据点出现的地方的概率。理想情况下,这两个概率应该都很大。考虑一种情况,即获得数据点的概率很低,这意味着样本中的数据点可能是异常的,因为在真实分布下,我们不会期望在某个地方获得数据点,且其高概率值不成立。伪对数似然值是一种评估指标,其作用与分类问题中的准确度分数和回归问题中的均方根误差相同。

现在,让我们实现网格搜索方法来优化带宽值。

练习 9.02:使用网格搜索选择最优带宽

在本练习中,我们将为练习 9.01中的样本数据创建一个估计的密度,带宽值的影响,并通过网格搜索和交叉验证确定一个最优带宽值。为了执行网格搜索和交叉验证,我们将使用sklearn,这是我们在本书中一直使用的工具。

注意

本练习是练习 9.01带宽值的影响的延续,因为我们使用的是相同的样本数据,并继续探讨带宽值的影响。

  1. 定义带宽值网格和网格搜索交叉验证模型。理想情况下,应该使用留一交叉验证方法,但为了使模型在合理的时间内运行,我们将使用 10 折交叉验证。将模型拟合到样本数据,如下所示:

    # define a grid of 100 possible bandwidth values
    bandwidths = 10 ** numpy.linspace(-1, 1, 100)
    # define the grid search cross validation model
    grid = sklearn.model_selection.GridSearchCV\
           (estimator=sklearn.neighbors.KernelDensity(),\
            param_grid={"bandwidth": bandwidths},\
            cv=10)
    # run the model on the previously defined data
    grid.fit(vals)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_05.jpg

    图 9.5:交叉验证模型的输出

  2. 从模型中提取最优带宽值。best_params_函数从模型对象中提取网格中表现最好的参数。

    best_bandwidth = grid.best_params_["bandwidth"]
    print("Best Bandwidth Value: {}" \
          .format(best_bandwidth))
    

    最优带宽值应大约为1.6。我们可以将最优带宽值解释为产生最大伪对数似然值的带宽值。请注意,根据网格中包含的值,最优带宽值可能会发生变化。

  3. 绘制样本数据的直方图,并叠加真实密度和估计密度。在这种情况下,估计的密度将是最佳估计密度:

    fig, ax = plt.subplots(figsize=(14, 10))
    ax.hist(vals, bins=50, density=True, alpha=0.5, \
            label='Sampled Values')
    ax.fill(x_vec[:, 0], true_density,\
            fc='black', alpha=0.3, label='True Distribution')
    log_density = numpy.exp(grid.best_estimator_\
                            .score_samples(x_vec))
    ax.plot(x_vec[:, 0], log_density,\
            '-', linewidth=2, label='Kernel = Gaussian')
    ax.legend(loc='upper right')
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_06.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_06.jpg)

图 9.6:随机样本的直方图

在这张直方图中,真实密度和最佳估计密度重叠显示。估计密度没有明显的过拟合或欠拟合,且能够很好地捕捉到三个聚类。可以说,它可能更好地贴合真实密度,但这仅仅是由模型根据给定的数据集生成的估计密度。

注意

若要访问此特定部分的源代码,请参考 packt.live/2UOHbTZ

你还可以在线运行这个示例,地址是 packt.live/38DbmTo

你必须执行整个 Notebook 才能得到预期的结果。

现在让我们进入第二个问题:什么是核函数,它在其中扮演什么角色?

核函数

另一个需要设置的参数是核函数。核函数是一个非负函数,它控制密度的形状。像主题模型一样,我们在一个非负环境中工作,因为出现负的似然性或概率是没有意义的。核函数通过以系统的方式加权数据点来控制估计密度的形状。这种加权的系统方法相对简单;与许多其他数据点接近的数据点会被加权,而那些孤立或远离其他数据点的数据点会被减权。被加权的数据点在最终估计的密度中将对应于较高的似然点。

可以使用许多函数作为核函数,但六种常见的选择是高斯(Gaussian)、顶帽(Tophat)、埃潘尼切科夫(Epanechnikov)、指数(Exponential)、线性(Linear)和余弦(Cosine)。这些函数各自代表了不同的分布形状。请注意,在每个公式中,参数 h 代表带宽值:

图 9.7:高斯核函数的公式

图 9.8:顶帽核函数的公式

  • 埃潘尼切科夫:每个观察值具有丘状的权重。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_11.jpg

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_09.jpg)

图 9.9:埃潘尼切科夫核函数的公式

  • 指数:每个观察值具有三角形的权重,三角形的边是凹形的。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_10.jpg

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_10.jpg)

图 9.10:指数核函数的公式

  • 线性:每个观察值具有三角形的权重。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_09.jpg

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_11.jpg)

图 9.11:线性核的公式

  • 余弦核:每个观测值都有一个圆顶形的权重。这个圆顶形比埃潘尼科夫核在顶部更窄。

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_12.jpg

图 9.12:余弦核的公式

以下是六个核函数的分布形状:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_13.jpg

图 9.13:六个核函数的整体形状

核函数的选择并非完全无关紧要,但它肯定不像带宽值的选择那么重要。一个合理的做法是,对于所有密度估计问题,都使用高斯核,这也是我们在接下来的练习和活动中所做的。

练习 9.03:核函数的影响

我们将演示核函数的选择如何影响密度估计的质量。就像我们在探索带宽值效应时做的那样,我们将保持其他所有参数不变,使用在前两次练习中生成的相同数据,并使用之前指定的六个核函数运行六个不同的核密度估计模型。六个估计的密度之间应该能看到明显的差异,但这些差异应该比使用不同带宽值估计的密度之间的差异稍微小一些。请注意,本练习应在与之前练习相同的 Notebook 中执行。

  1. 定义一个元组列表,格式与之前定义的相同。每个元组包含子图的行和列索引,以及用于创建密度估计的核函数:

    position_kernel_vec = [(0, 0, 'gaussian'), (0, 1, 'tophat'), \
                           (1, 0, 'epanechnikov'), \
                           (1, 1, 'exponential'), \
                           (2, 0, 'linear'), (2, 1, 'cosine'),]
    
  2. 使用不同的核函数拟合并绘制六个核密度估计模型。为了真正理解核函数之间的差异,我们将带宽值设置为在练习 9.02中找到的最优带宽值,即使用网格搜索选择最优带宽,并且不调整它:

    fig, ax = plt.subplots(3, 2, sharex=True, \
                           sharey=True, figsize=(12, 9))
    fig.suptitle('The Effect of Different Kernels', fontsize=16)
    for r, c, k in position_kernel_vec:
        kde = sklearn.neighbors.KernelDensity(\
              kernel=k, bandwidth=best_bandwidth).fit(vals)
        log_density = kde.score_samples(x_vec)
        ax[r, c].hist(vals, bins=50, density=True, alpha=0.5)
        ax[r, c].plot(x_vec[:, 0], numpy.exp(log_density), \
                      '-', linewidth=2)
        ax[r, c].set_title('Kernel = {}'.format(k.capitalize()))
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_14.jpg

图 9.14:一个 3 x 2 的子图矩阵

在六个核函数中,高斯核生成了最合理的估计密度。除此之外,注意到不同核函数估计的密度之间的差异小于不同带宽值估计的密度之间的差异。这印证了之前的观点,即带宽值是更重要的参数,应在模型构建过程中作为重点。

注意

要访问此特定部分的源代码,请参阅packt.live/2UOHbTZ

您也可以在packt.live/38DbmTo在线运行此示例。

必须执行整个 Notebook 才能获得所需的结果。

在我们大致理解的基础上,让我们讨论核密度估计的推导过程。

核密度估计推导

我们跳过正式的数学推导,转而采用直观的流行推导方法。核密度估计将每个样本数据点转化为其自身的分布,其宽度由带宽值控制。然后,将这些个体分布相加,生成所需的密度估计。这一概念相对容易展示;然而,在接下来的练习中,我们在进行演示之前,先尝试从抽象的角度进行思考。对于包含许多样本数据点的地理区域,个体密度将会重叠,并通过相加这些密度,产生在估计密度中更高的可能性点。类似地,对于包含少量或没有样本数据点的地理区域,个体密度将不会重叠,因此在估计密度中会对应较低的可能性点。

练习 9.04:模拟核密度估计的推导

这里的目标是展示将个体分布相加,创建随机变量的整体估计密度的概念。我们将通过从一个样本数据点开始,逐步建立这一概念,并逐渐增加样本数据点的数量。此外,还会应用不同的带宽值,因此我们对带宽值对这些个体密度影响的理解将进一步巩固。请注意,本练习应与所有其他练习一起在同一个笔记本中完成。

  1. D 热点分析定义一个函数,用于评估正态分布。输入值包括表示随机变量X范围的网格、采样数据点m和带宽b

    def eval_gaussian(x, m, b):
        numerator = numpy.exp(-numpy.power(x - m, 2) \
                              / (2 * numpy.power(b, 2)))
        denominator = b * numpy.sqrt(2 * numpy.pi)
        return numerator / denominator
    
  2. 将一个样本数据点绘制为直方图,并显示其在不同带宽值下的个体密度:

    m = numpy.array([5.1])
    b_vec = [0.1, 0.35, 0.8]
    x_vec = numpy.linspace(1, 10, 100)[:, None]
    figOne, ax = plt.subplots(2, 3, sharex=True, \
                              sharey=True, figsize=(15, 10))
    for i, b in enumerate(b_vec):
        ax[0, i].hist(m[:], bins=1, fc='#AAAAFF', density=True)
        ax[0, i].set_title("Histogram: Normed")
        evaluation = eval_gaussian(x_vec, m=m[0], b=b)
        ax[1, i].fill(x_vec, evaluation, '-k', fc='#AAAAFF')
        ax[1, i].set_title("Gaussian Dist: b={}".format(b))
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_15.jpg

    图 9.15:显示一个数据点及其在不同带宽值下的个体密度

    在这里,我们可以看到已经建立的结论:较低的带宽值会产生非常狭窄的密度,往往会导致过拟合数据。

  3. 重复执行步骤 2中的工作,但这次将数据点规模扩展到 16 个:

    m = numpy.random.normal(4.7, 0.88, 16)
    n = len(m)
    b_vec = [0.1, 0.35, 1.1]
    x_vec = numpy.linspace(-1, 11, 100)[:, None]
    figMulti, ax = plt.subplots(2, 3, sharex=True, \
                                sharey=True, figsize=(15, 10))
    for i, b in enumerate(b_vec):
        ax[0, i].hist(m[:], bins=n, fc='#AAAAFF', density=True)
        ax[0, i].set_title("Histogram: Normed")
        sum_evaluation = numpy.zeros(len(x_vec))
        for j in range(n):
            evaluation = eval_gaussian(x_vec, m=m[j], b=b) / n
            sum_evaluation += evaluation[:, 0]
            ax[1, i].plot(x_vec, evaluation, \
                         '-k', linestyle="dashed")
        ax[1, i].fill(x_vec, sum_evaluation, '-k', fc='#AAAAFF')
        ax[1, i].set_title("Gaussian Dist: b={}".format(b))
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_16.jpg

图 9.16:绘制数据点

前面的图像展示了 16 个数据点,它们在不同带宽值下的个体密度,以及它们个体密度的总和。

再次,毫不意外,利用最小带宽值的图表展示了一个过度拟合的估计密度。也就是说,估计的密度捕捉了样本数据中的所有噪声。在这三种密度中,第二种密度(带宽值设置为0.35)是最合理的。

注意

若要访问该特定部分的源代码,请参考packt.live/2UOHbTZ

你也可以在packt.live/38DbmTo上在线运行此示例。

您必须执行整个 Notebook 才能获得所需的结果。

活动 9.01:在一维中估计密度

在本活动中,我们将生成一些伪造的样本数据,并使用核密度估计估算密度函数。带宽值将通过网格搜索交叉验证进行优化。目标是通过在简单的一维情况下运行模型来巩固我们对这种有用方法的理解。我们将再次利用 Jupyter 笔记本来完成工作。

假设我们将创建的样本数据描述的是美国某州的房屋价格。暂时忽略以下样本数据中的数值。问题是,房价的分布是什么样的?我们能否提取出某个特定价格区间内房屋的概率? 这些问题以及更多问题可以通过核密度估计来解答。

以下是完成活动的步骤:

  1. 打开一个新的笔记本并安装所有必要的库。

  2. 从标准正态分布中抽取 1,000 个数据点。在样本的最后 625 个值上加上 3.5(即索引在 375 到 1,000 之间)。使用numpy.random.RandomState设置随机状态为 100,以保证相同的样本值,然后使用rand.randn(1000)调用随机生成数据点。

  3. 将 1,000 个数据点样本数据绘制为直方图,并在其下方添加散点图。

  4. 定义一个带宽值网格。然后,定义并拟合一个网格搜索交叉验证算法。

  5. 提取最佳带宽值。

  6. 步骤 3重新绘制直方图并叠加估计的密度。

输出将如下所示:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_17.jpg

图 9.17:叠加最佳估计密度的随机样本直方图

本活动的解决方案可以在第 501 页找到。

热点分析

一开始,热点是数据点高度集中的区域,例如犯罪率异常高的特定社区,或受异常数量的龙卷风影响的国家大片区域。热点分析是通过抽样数据,在一个人群中寻找这些可能存在的热点。这个过程通常通过利用核密度估计来完成。

热点分析可以分为四个高层次的步骤:

  1. 收集数据:数据应包括物体或事件的位置。正如我们简要提到的,运行并实现可操作结果所需的数据量是相对灵活的。最佳状态是拥有一个能代表总体的样本数据集。

  2. 识别基础地图:下一步是识别哪种基础地图最适合项目的分析和展示需求。在这张基础地图上,模型的结果将被叠加,以便能更清晰地表达热点的位置,使用更易于理解的术语,如城市、邻里或区域。

  3. 执行模型:在这一步骤中,你需要选择并执行一种或多种空间模式提取方法,以识别热点。对我们来说,这个方法将是——毫不奇怪——核密度估计。

  4. 创建可视化:热点地图通过将模型结果叠加在基础地图上生成,以支持未解决的业务问题。

从可用性角度来看,热点分析的主要问题之一是,热点的统计显著性并不容易确定。大多数关于统计显著性的问题围绕热点的存在展开。也就是说,发生概率的波动是否真的构成了统计上显著的波动?需要注意的是,进行核密度估计并不需要统计显著性,我们在接下来的工作中将完全不涉及显著性。

虽然“热点”一词传统上用于描述位置数据点的聚集,但它并不限于位置数据。任何数据类型都可能有热点,不管它们是否被称为热点。在接下来的练习中,我们将对一些非位置数据进行建模,以寻找热点,这些热点将是特征空间中具有高或低发生概率的区域。

练习 9.05:加载数据并使用 Seaborn 建模

在本练习中,我们将使用 seaborn 库来拟合并可视化核密度估计模型。这将应用于位置数据和非位置数据。在进行建模之前,我们需要加载数据,该数据是来自 sklearn 的加州住房数据集,并以 CSV 格式提供。文件需要从 GitHub 仓库下载并保存在本地计算机上。该数据集取自 1990 年的美国人口普查,描述了当时加州的住房情况。每一行数据描述一个普查区块组。普查区块组的定义对于本练习并不重要,因此我们将在此跳过定义,专注于更多的实践编码和建模。需要强调的是,所有变量都已经按普查区块进行了汇总。例如,MedInc 是每个普查区块中家庭的中位数收入。有关此数据集的更多信息,请访问 scikit-learn.org/stable/datasets/index.html#california-housing-dataset

  1. 使用 california_housing.csv 加载加州住房数据集。打印数据框的前五行:

    df = pandas.read_csv('./california_housing.csv', header=0)
    df.head()
    

    注意

    文件的路径取决于文件在系统中的位置。

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_18.jpg

    图 9.18:来自 sklearn 的加利福尼亚州房屋数据集的前五行

  2. 根据HouseAge特征过滤数据框,该特征是每个普查区块的房屋中位数年龄。仅保留HouseAge小于或等于 15 的行,并将数据框命名为dfLess15。打印数据框的前五行,然后将数据框缩减为仅包含经度和纬度特征:

    dfLess15 = df[df['HouseAge'] <= 15.0]
    dfLess15 = dfLess15[['Latitude', 'Longitude']]
    dfLess15.head()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_19.jpg

    图 9.19:过滤后的数据集前五行

  3. 使用seaborn对基于经度和纬度数据点构建的核密度估计模型进行拟合和可视化。该模型有四个输入参数:需要估计密度的两个列的名称(即经度和纬度)、这些列所属的数据框,以及密度估计方法(即kde或核密度估计):

    seaborn.jointplot("Longitude", "Latitude", dfLess15, kind="kde")
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_20.jpg

    图 9.20:联合图

    注意

    图表可能有所不同,因为每次的估计都不完全相同。

    这个联合图包含了二维估计密度以及dfLess15数据集的边际密度。

    如果我们将这些结果叠加到加利福尼亚州的地图上,我们会看到热点区域集中在南加利福尼亚,包括洛杉矶和圣地亚哥,湾区,包括旧金山,以及在一定程度上被称为中央山谷的区域。这个seaborn图形的一个优点是,我们可以获得二维的估计密度图,并且分别展示了经度和纬度的边际密度。

  4. 基于HouseAge特征创建另一个过滤后的数据框;这次仅保留HouseAge大于 40 的行,并将数据框命名为dfMore40。此外,移除除经度和纬度之外的所有列。然后,打印数据框的前五行:

    dfMore40 = df[df['HouseAge'] > 40.0]
    dfMore40 = dfMore40[['Latitude', 'Longitude']]
    dfMore40.head()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_21.jpg

    图 9.21:过滤后的数据集顶部

  5. 重复步骤 3中的过程,但使用这个新的过滤后的数据框:

    seaborn.jointplot("Longitude", "Latitude", dfMore40, kind="kde")
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_22.jpg

    图 9.22:联合图

    这个联合图包含了二维估计密度以及dfMore40数据集的边际密度。

    这个估计密度要紧凑得多,因为数据几乎完全聚集在两个区域。这些区域分别是洛杉矶和湾区。与步骤 3中的图表对比,我们注意到房屋开发已经扩展到整个州。此外,新的住房开发在更多的普查区块中出现的频率也显著提高。

  6. 我们再创建另一个过滤后的数据框。这次仅保留HouseAge小于或等于五的行,并将数据框命名为dfLess5。绘制PopulationMedInc的散点图,如下所示:

    dfLess5 = df[df['HouseAge'] <= 5]
    x_vals = dfLess5.Population.values
    y_vals = dfLess5.MedInc.values
    fig = plt.figure(figsize=(10, 10))
    plt.scatter(x_vals, y_vals, c='black')
    plt.xlabel('Population', fontsize=18)
    plt.ylabel('Median Income', fontsize=16)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_23.jpg

    图 9.23:中位收入与人口的散点图

    这是HouseAge列中值小于或等于五的中位收入与人口的散点图。

  7. 使用另一个seaborn函数拟合核密度估计模型。使用 Scott 法则找到最佳带宽。重新绘制直方图,并叠加估算的密度,如下所示:

    fig = plt.figure(figsize=(10, 10))
    ax = seaborn.kdeplot(x_vals, \
                         y_vals,\
                         kernel='gau',\
                         cmap='Blues', \
                         shade=True, \
                         shade_lowest=False)
    plt.scatter(x_vals, y_vals, c='black', alpha=0.05)
    plt.xlabel('Population', fontsize=18)
    plt.ylabel('Median Income', fontsize=18)
    plt.title('Density Estimation With Scatterplot Overlay', size=18)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_24.jpg

图 9.24:与步骤 6 中创建的相同散点图,叠加了估算的密度

在这里,估算的密度显示,人口较少的普查区块相比于拥有较高中位收入的区块,更有可能拥有较低的中位收入。这个步骤的重点是展示如何在非位置数据上使用核密度估计。像这样的图通常被称为等高线图。

注意

要访问该部分的源代码,请参阅packt.live/2UOHbTZ

你还可以在packt.live/38DbmTo在线运行此示例。

你必须执行整个笔记本才能获得预期的结果。

在展示热点分析结果时,通常需要涉及某种类型的地图,因为热点分析通常是在位置数据上进行的。获取可以叠加估算密度的地图并不是一件容易的事。由于版权问题,我们将使用非常基础的地图,称为底图,在其上叠加我们的估算密度。将留给你自己扩展本章所学的知识,去处理更复杂和更详细的地图。地图环境也可能很复杂,下载和安装起来很耗时。

练习 9.06:与底图一起工作

本练习使用了mpl_toolkitsbasemap模块。basemap是一个地图绘制库,可以用来创建基础地图或地理区域的轮廓。这些地图可以叠加核密度估计的结果,从而清晰地展示热点位置。

首先,在 Jupyter 笔记本中运行import mpl_toolkits.basemap检查basemap是否已安装。如果没有错误加载,说明你已经准备好了,不需要采取任何进一步的操作。如果调用失败,请通过运行python3 -m pip install basemap使用pip安装basemap。重新启动任何已打开的笔记本后,你应该可以正常使用。请注意,pip安装仅在已安装 Anaconda 的情况下有效。

本练习的目标是重新建模并重新绘制练习 9.05中的位置数据,使用 Seaborn 加载数据与建模,使用sklearn的核密度估计功能和basemap的映射功能。从过滤后的数据框dfLess15中提取经度和纬度值,按步骤进行操作。请注意,本练习应与其他所有练习在同一个笔记本中完成。

  1. 构建位置网格,以便在其上铺设估计的密度。位置网格是练习 9.01中定义随机变量范围的一维向量的二维位置等效物:

    xgrid15 = numpy.sort(list(dfLess15['Longitude']))
    ygrid15 = numpy.sort(list(dfLess15['Latitude']))
    x15, y15 = numpy.meshgrid(xgrid15, ygrid15)
    print("X Grid Component:\n{}\n".format(x15))
    print("Y Grid Component:\n{}\n".format(y15))
    xy15 = numpy.vstack([y15.ravel(), x15.ravel()]).T 
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_25.jpg

    图 9.25:表示 dfLess15 数据集的网格的 x 和 y 分量

  2. 定义并拟合一个核密度估计模型。将带宽值设置为 0.05。然后为位置网格上的每个点创建可能性值:

    kde15 = sklearn.neighbors.KernelDensity(bandwidth=0.05, \
                                            metric='minkowski', \
                                            kernel='gaussian', \
                                            algorithm='ball_tree')
    kde15.fit(dfLess15.values)
    

    核密度估计模型的输出结果如下:

    KernelDensity(algorithm='ball_tree', atol=0, bandwidth=0.05, \
                  breadth_first=True, kernel='gaussian', \
                  leaf_size=40, metric='minkowski', \
                  metric_params=None, rtol=0)
    
  3. 将训练好的模型拟合到xy网格上,并打印出形状,如下所示:

    log_density = kde15.score_samples(xy15)
    density = numpy.exp(log_density)
    density = density.reshape(x15.shape)
    print("Shape of Density Values:\n{}\n".format(density.shape))
    

    输出结果如下:

    Shape of Density Values:
    (3287, 3287)
    

    请注意,如果你打印出可能性值的形状,它是 3,287 行 × 3,287 列,即 10,804,369 个可能性值。这与预设的经度和纬度网格xy15中的值数相同。

  4. 创建加利福尼亚州的轮廓,并叠加步骤 2中计算的估计密度:

    Exercise9.01-Exercise9.06.ipynb
    fig15 = plt.figure(figsize=(10, 10))
    fig15.suptitle(\
        """
        Density Estimation:
        Location of Housing Blocks
        Where the Median Home Age <= 15 Years
        """,\
        fontsize=16)
    The complete code for this step can be found at https://packt.live/38DbmTo.
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_26.jpg

    图 9.26:将 dfLess15 的估计密度叠加到加利福尼亚州的轮廓上

    0.05的值是故意设置为略微过拟合数据。你会注意到,与练习 9.05中的较大聚类(构成密度的部分)不同,使用 Seaborn 加载数据与建模中的估计密度由许多更小的聚类组成。这种略微过拟合的密度可能比之前版本的密度更有帮助,因为它能更清晰地显示高概率的普查区块的确切位置。之前密度中一个高概率区域是南加州,但南加州是一个面积庞大、人口众多且有许多市政区的地方。请记住,在使用这些结果做商业决策时,可能需要某些特定的精确度,且如果样本数据能支持该级别的精确度或细化结果,应该提供相关信息。

  5. 重复步骤 1,但使用dfMore40数据框:

    xgrid40 = numpy.sort(list(dfMore40['Longitude']))
    ygrid40 = numpy.sort(list(dfMore40['Latitude']))
    x40, y40 = numpy.meshgrid(xgrid40, ygrid40)
    print("X Grid Component:\n{}\n".format(x40))
    print("Y Grid Component:\n{}\n".format(y40))
    xy40 = numpy.vstack([y40.ravel(), x40.ravel()]).T 
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_27.jpg

    图 9.27:表示 dfMore40 数据集的网格的 x 和 y 分量

  6. 使用步骤 4中建立的网格重复步骤 2

    kde40 = sklearn.neighbors.KernelDensity(bandwidth=0.05, \
                                            metric='minkowski', \
                                            kernel='gaussian', \
                                            algorithm='ball_tree')
    kde40.fit(dfMore40.values)
    log_density = kde40.score_samples(xy40)
    density = numpy.exp(log_density)
    density = density.reshape(x40.shape)
    print("Shape of Density Values:\n{}\n".format(density.shape))
    
  7. 使用步骤 5中计算出的估算密度重复执行步骤 3

Exercise9.01-Exercise9.06.ipynb
fig40 = plt.figure(figsize=(10, 10))
fig40.suptitle(\
    """
    Density Estimation:
    Location of Housing Blocks
    Where the Median Home Age > 40 Years
    """, \
    fontsize=16)
The complete code for this step can be found at https://packt.live/38DbmTo.

输出结果如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_28.jpg

图 9.28:估算的 dfMore40 密度叠加在加利福尼亚州的轮廓上

注意

要访问该特定部分的源代码,请参考packt.live/2UOHbTZ

你也可以在packt.live/38DbmTo上在线运行这个示例。你必须执行整个 Notebook 才能获得预期的结果。

这个估算密度实际上是我们在练习 9.05加载数据与使用 Seaborn 建模中所做的密度的重新计算。虽然步骤 3中的密度能为那些对房地产或人口普查感兴趣的人提供更多细节,但这个密度与练习 9.05中对应的密度实际上看起来没有太大不同。热点主要集中在洛杉矶和旧金山,几乎没有其他地方的点。

活动 9.02:分析伦敦的犯罪数据

在这个活动中,我们将使用来自data.police.uk/data/的伦敦犯罪数据,通过核密度估计进行热点分析。由于处理地图数据的难度较大,我们将使用seaborn来可视化分析结果。然而,如果你觉得勇敢,并且已经能够运行练习 9.06与底图一起工作中的所有图形,那么鼓励你尝试使用地图。

对该犯罪数据进行热点分析的动机有两个方面。首先,我们被要求确定某些类型的犯罪发生的高概率位置,以便能够最大化警力资源的分配。接着,作为后续工作,我们需要确定某些类型的犯罪热点是否随着时间变化而发生变化。这两个问题都可以通过使用核密度估计来解答。

注意

该数据集来自data.police.uk/data/。它包含根据开放政府许可证 v3.0 许可的公共部门信息。

你还可以从 Packt 的 GitHub 下载,链接为packt.live/2JIWs2z

或者,若要直接从源网站下载数据,请访问前述的警察网站,勾选Metropolitan Police Service,然后设置日期范围为2018 年 7 月2018 年 12 月。接着,点击生成文件,然后点击立即下载,并将下载的文件命名为metro-jul18-dec18。确保你知道如何或能够找到下载目录的路径。

该数据集包含根据开放政府许可证 v3.0 许可的公共部门信息。

完成此活动的步骤如下:

  1. 加载犯罪数据。使用你保存下载目录的路径,创建一个包含年月标签的列表,使用read_csv命令逐个加载文件,然后将这些文件连接在一起。

  2. 打印完整(六个月)和合并数据集的诊断信息。

  3. 将数据框缩减为四个变量(经度纬度月份,和犯罪类型)。

  4. 使用seaborn中的jointplot函数,为 2018 年 7 月、9 月和 12 月的自行车盗窃案件拟合并可视化三个核密度估计模型。

  5. 重复步骤 4;这次,使用 2018 年 8 月、10 月和 11 月的盗窃案件数据。

  6. 重复步骤 5;这次,使用 2018 年 7 月、10 月和 12 月的入室盗窃案件数据。

步骤 6 最后部分的输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_29.jpg

图 9.29:2018 年 12 月入室盗窃案件的估计联合和边际密度

再次澄清,活动中得到的密度应该已经叠加在地图上,以便我们能清楚地看到这些密度覆盖了哪些区域。如果你有合适的地图平台,建议尝试自己将结果叠加到地图上。如果没有,你可以访问在线地图服务,使用经纬度对来了解具体位置。

注意

本活动的解决方案可以在第 505 页找到。

摘要

核密度估计是一种经典的统计技术,属于与直方图相同类别的技术。它允许用户从样本数据中外推,以对特定对象或事件的总体做出洞察和预测。这种外推以概率密度函数的形式呈现,这很好,因为结果可以以可能性或概率的形式解读。该模型的质量依赖于两个参数:带宽值和核函数。如前所述,成功使用核密度估计的最关键部分是设置最优带宽。最优带宽通常通过使用网格搜索交叉验证和伪对数似然作为评分标准来确定。核密度估计的优点在于其简洁性和广泛的适用性。

核密度估计模型在犯罪学、流行病学、气象学和房地产等领域中非常常见,仅举几例。无论你从事哪个行业,核密度估计都应该是适用的。

在监督学习和无监督学习之间,无监督学习无疑是最少使用和最不被重视的学习类别。但情况不应该是这样的。监督学习技术是有限的,并且大多数可用数据并不适合回归和分类。扩展你的技能集,学习无监督学习技术,意味着你将能够利用不同的数据集,以更具创意的方式解决业务问题,甚至增强你现有的监督学习模型。本文并不详尽介绍所有无监督学习算法,但它是一个很好的起点,可以激发你的兴趣,并推动你继续学习。

附录

1. 聚类简介

活动 1.01:实现 k-means 聚类

解决方案:

  1. 导入所需的库:

    from sklearn.datasets import make_blobs
    from sklearn.cluster import KMeans
    from sklearn.metrics import accuracy_score, silhouette_score
    import matplotlib.pyplot as plt
    import pandas as pd
    import numpy as np
    from scipy.spatial.distance import cdist
    import math
    np.random.seed(0)
    %matplotlib inline
    
  2. 使用 pandas 加载种子数据文件:

    seeds = pd.read_csv('Seed_Data.csv')
    
  3. 返回数据集的前五行,如下所示:

    seeds.head()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_01_25.jpg

    图 1.25:显示数据集的前五行

  4. 按如下方式分离 X 特征:

    X = seeds[['A','P','C','LK','WK','A_Coef','LKG']]
    y = seeds['target']
    
  5. 按如下方式检查特征:

    X.head()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_01_26.jpg

    图 1.26:打印特征

  6. 定义 k_means 函数,如下所示,并随机初始化 k-质心。使用 while 循环重复此过程,直到新旧 centroids 之间的差值为 0

    Activity 1.01.ipynb
    def k_means(X, K):
        # Keep track of history so you can see K-Means in action
        centroids_history = []
        labels_history = []
    
        # Randomly initialize Kcentroids
        rand_index = np.random.choice(X.shape[0], K)  
        centroids = X[rand_index]
        centroids_history.append(centroids)
    The complete code for this step can be found at https://packt.live/2JPZ4M8.
    
  7. 将 pandas DataFrame 转换为 NumPy 矩阵:

    X_mat = X.values
    
  8. 将我们的种子矩阵传递给之前创建的 k_means 函数:

    centroids, labels, centroids_history, labels_history = \
    k_means(X_mat, 3)
    
  9. 打印标签:

    print(labels)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_01_27.jpg

    图 1.27:打印标签

  10. 按如下方式绘制坐标:

    plt.scatter(X['A'], X['LK'])
    plt.title('Wheat Seeds - Area vs Length of Kernel')
    plt.show()
    plt.scatter(X['A'], X['LK'], c=labels, cmap='tab20b')
    plt.title('Wheat Seeds - Area vs Length of Kernel')
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_01_28.jpg

    图 1.28:绘制坐标

  11. 按如下方式计算轮廓系数:

    silhouette_score(X[['A','LK']], labels)
    

    输出如下:

    0.5875704550892767
    

通过完成此活动,您已经获得了在实际数据集上调优 k-means 聚类算法的实践经验。种子数据集被视为数据科学领域经典的“Hello World”类型问题,对于测试基础技术非常有帮助。

注意

要访问此特定部分的源代码,请参考 packt.live/2JPZ4M8

您还可以在 packt.live/2Ocncuh 上在线运行此示例。

2. 层次聚类

活动 2.01:将 k-means 与层次聚类进行比较

解决方案:

  1. 从 scikit-learn 导入必要的包(KMeansAgglomerativeClusteringsilhouette_score),如下所示:

    from sklearn.cluster import KMeans
    from sklearn.cluster import AgglomerativeClustering
    from sklearn.metrics import silhouette_score
    import pandas as pd
    import matplotlib.pyplot as plt
    
  2. 将酒类数据集读取到 Pandas DataFrame 中并打印一个小样本:

    wine_df = pd.read_csv("wine_data.csv")
    print(wine_df.head())
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_02_25.jpg

    图 2.25:酒类数据集的输出

  3. 可视化酒类数据集以理解数据结构:

    plt.scatter(wine_df.values[:,0], wine_df.values[:,1])
    plt.title("Wine Dataset")
    plt.xlabel("OD Reading")
    plt.ylabel("Proline")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_02_26.jpg

    图 2.26:原始酒类数据的绘图

  4. 在酒类数据集上使用 sklearn 实现 k-means 聚类,已知有三种酒类:

    km = KMeans(3)
    km_clusters = km.fit_predict(wine_df)
    
  5. 使用 sklearn 实现层次聚类,应用于酒类数据集:

    ac = AgglomerativeClustering(3, linkage='average')
    ac_clusters = ac.fit_predict(wine_df)
    
  6. 按如下方式绘制 k-means 的预测聚类:

    plt.scatter(wine_df.values[:,0], \
                wine_df.values[:,1], c=km_clusters)
    plt.title("Wine Clusters from K-Means Clustering")
    plt.xlabel("OD Reading")
    plt.ylabel("Proline")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_02_27.jpg

    图 2.27:k-means 聚类的聚类图

  7. 如下所示绘制层次聚类的预测聚类:

    plt.scatter(wine_df.values[:,0], \
                wine_df.values[:,1], c=ac_clusters)
    plt.title("Wine Clusters from Agglomerative Clustering")
    plt.xlabel("OD Reading")
    plt.ylabel("Proline")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_02_28.jpg

    图 2.28:聚合聚类的聚类图

    注意

    图 2.23图 2.24中,每种颜色代表一个单独的聚类。每次执行代码时,聚类的颜色都会发生变化。

  8. 比较每种聚类方法的轮廓分数:

    print("Silhouette Scores for Wine Dataset:\n")
    print("K-Means Clustering: ", silhouette_score\
         (wine_df, km_clusters))
    print("Agg Clustering: ", silhouette_score(wine_df, ac_clusters))
    

    输出将如下所示:

    Silhouette Scores for Wine Dataset:
    K-Means Clustering:  0.5809421087616886
    Agg Clustering:  0.5988495817462
    

从前面的轮廓指标可以看出,在按平均簇内距离分离聚类时,聚合聚类稍微优于 k-means 聚类。然而,这并不是每个聚合聚类版本的情况。相反,尝试不同的连接类型,并检查每种类型下的轮廓分数和聚类的变化。

注意

要访问该特定部分的源代码,请参考packt.live/2AFA60Z

你也可以在线运行此示例,访问packt.live/3fe2lTi

3. 邻域方法和 DBSCAN

活动 3.01:从零实现 DBSCAN

解决方案:

  1. 生成一个随机的聚类数据集,如下所示:

    from sklearn.cluster import DBSCAN
    from sklearn.datasets import make_blobs
    import matplotlib.pyplot as plt
    import numpy as np
    %matplotlib inline
    X_blob, y_blob = make_blobs(n_samples=500, \
                                centers=4, n_features=2, \
                                random_state=800)
    
  2. 可视化生成的数据:

    plt.scatter(X_blob[:,0], X_blob[:,1])
    plt.show()
    

    输出如下所示:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_03_15.jpg

    图 3.15:生成的数据的绘图

  3. 从头开始创建函数,允许你在数据集上调用 DBSCAN:

    Activity3.01.ipynb
    def scratch_DBSCAN(x, eps, min_pts): 
        """
        param x (list of vectors): your dataset to be clustered
        param eps (float): neighborhood radius threshold 
        param min_pts (int): minimum number of points threshold for 
        a neighborhood to be a cluster
        """
        # Build a label holder that is comprised of all 0s
        labels = [0]* x.shape[0]
        # Arbitrary starting "current cluster" ID
        C = 0
    The complete code for this step can be found at https://packt.live/3c1rONO.
    
  4. 使用你创建的 DBSCAN 实现来查找生成数据集中的聚类。根据需要调整超参数,并根据它们在第 5 步中的表现进行调优:

    labels = scratch_DBSCAN(X_blob, 0.6, 5)
    
  5. 可视化你实现的 DBSCAN 聚类性能:

    plt.scatter(X_blob[:,0], X_blob[:,1], c=labels)
    plt.title("DBSCAN from Scratch Performance")
    plt.show()
    

    输出如下所示:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_03_16.jpg

图 3.16:DBSCAN 实现的绘图

在前面的输出中,你可以看到我们生成的数据中有四个明确定义的聚类。未突出显示的点超出了邻域范围,因此被视为噪声。尽管这可能不是理想的,因为不是每个点都被考虑在内,但对于大多数商业案例来说,这种噪声是可以接受的。如果在你的场景中不能接受,你可以调整提供的超参数,使得距离容忍度更高。

正如你可能已经注意到的,定制实现的运行时间较长。这是因为我们为了清晰起见,探索了非向量化版本的算法。在大多数情况下,你应该使用 scikit-learn 提供的 DBSCAN 实现,因为它经过高度优化。

注意

要访问该特定部分的源代码,请参考packt.live/3c1rONO

你也可以在线运行此示例,访问packt.live/2ZVoFuO

活动 3.02:将 DBSCAN 与 k-means 和层次聚类进行比较

解决方案:

  1. 导入必要的包:

    from sklearn.cluster \
    import KMeans, AgglomerativeClustering, DBSCAN
    from sklearn.metrics import silhouette_score
    import pandas as pd
    import matplotlib.pyplot as plt
    %matplotlib inline
    
  2. 第二章层次聚类中加载葡萄酒数据集,并再次熟悉数据的外观:

    # Load Wine data set
    wine_df = pd.read_csv("wine_data.csv")
    # Show sample of data set
    print(wine_df.head())
    

    输出如下所示:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_03_17.jpg

    图 3.17:葡萄酒数据集的前五行

  3. 可视化数据:

    plt.scatter(wine_df.values[:,0], wine_df.values[:,1])
    plt.title("Wine Dataset")
    plt.xlabel("OD Reading")
    plt.ylabel("Proline")
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_03_18.jpg

    图 3.18:数据图示

  4. 使用 k-means、凝聚聚类和 DBSCAN 生成聚类:

    # Generate clusters from K-Means
    km = KMeans(3)
    km_clusters = km.fit_predict(wine_df)
    # Generate clusters using Agglomerative Hierarchical Clustering
    ac = AgglomerativeClustering(3, linkage='average')
    ac_clusters = ac.fit_predict(wine_df)
    
  5. 评估不同的 DSBSCAN 超参数选项及其对轮廓得分的影响:

    db_param_options = [[20,5],[25,5],[30,5],[25,7],[35,7],[40,5]]
    for ep,min_sample in db_param_options:
        # Generate clusters using DBSCAN
        db = DBSCAN(eps=ep, min_samples = min_sample)
        db_clusters = db.fit_predict(wine_df)
        print("Eps: ", ep, "Min Samples: ", min_sample)
        print("DBSCAN Clustering: ", \
              silhouette_score(wine_df, db_clusters))
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_03_19.jpg

    图 3.19:打印聚类的轮廓得分

  6. 根据最高的轮廓得分生成最终聚类(eps35min_samples3):

    # Generate clusters using DBSCAN
    db = DBSCAN(eps=40, min_samples = 5)
    db_clusters = db.fit_predict(wine_df)
    
  7. 可视化使用三种方法生成的聚类:

    plt.title("Wine Clusters from K-Means")
    plt.scatter(wine_df['OD_read'], wine_df['Proline'], \
                c=km_clusters,s=50, cmap='tab20b')
    plt.show()
    plt.title("Wine Clusters from Agglomerative Clustering")
    plt.scatter(wine_df['OD_read'], wine_df['Proline'], \
                c=ac_clusters,s=50, cmap='tab20b')
    plt.show()
    plt.title("Wine Clusters from DBSCAN")
    plt.scatter(wine_df['OD_read'], wine_df['Proline'], \
                c=db_clusters,s=50, cmap='tab20b')
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_03_20.jpg

    图 3.20:使用不同算法绘制聚类图

  8. 评估每种方法的轮廓得分:

    # Calculate Silhouette Scores
    print("Silhouette Scores for Wine Dataset:\n")
    print("K-Means Clustering: ", \
           silhouette_score(wine_df, km_clusters))
    print("Agg Clustering: ", \
          silhouette_score(wine_df, ac_clusters))
    print("DBSCAN Clustering: ", \
          silhouette_score(wine_df, db_clusters))
    

    输出结果如下:

    Silhouette Scores for Wine Dataset:
    K-Means Clustering:  0.5809421087616886
    Agg Clustering:  0.5988495817462
    DBSCAN Clustering:  0.5739675293567901
    

如你所见,DBSCAN 并不是自动适合你聚类需求的最佳选择。与其他算法的一个关键区别在于它将噪声视为潜在的聚类。在某些情况下,这很有帮助,因为它能够去除异常值;然而,也可能会出现它调整得不够好,导致过多的点被分类为噪声的情况。你可以通过在拟合聚类算法时调整超参数进一步提高轮廓得分——尝试几种不同的组合,看看它们如何影响得分。

注意

要获取此特定部分的源代码,请参考 packt.live/2BNSQvC

你也可以在网上运行这个示例,链接地址为 packt.live/3iElboS

4. 降维技术与 PCA

活动 4.01:手动 PCA 与 scikit-learn

解决方案:

  1. 导入 pandasnumpymatplotlib 绘图库以及 scikit-learn 的 PCA 模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    
  2. 加载数据集,并按照之前的练习仅选择花萼特征。显示数据的前五行:

    df = pd.read_csv('../Seed_Data.csv')
    df = df[['A', 'LK']]
    df.head()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_04_36.jpg

    图 4.36:数据的前五行

  3. 计算数据的协方差矩阵:

    cov = np.cov(df.values.T)
    cov
    

    输出结果如下:

    array([[8.46635078, 1.22470367],
           [1.22470367, 0.19630525]])
    
  4. 使用 scikit-learn API 转换数据,并仅使用第一个主成分。将转换后的数据存储在 sklearn_pca 变量中:

    model = PCA(n_components=1)
    sklearn_pca = model.fit_transform(df.values)
    
  5. 使用手动 PCA 转换数据,并仅使用第一个主成分。将转换后的数据存储在 manual_pca 变量中:

    eigenvectors, eigenvalues, _ = \
    np.linalg.svd(cov, full_matrices=False)
    P = eigenvectors[0]
    manual_pca = P.dot(df.values.T)
    
  6. 在同一图上绘制 sklearn_pcamanual_pca 值,以可视化差异:

    plt.figure(figsize=(10, 7)) 
    plt.plot(sklearn_pca, label='Scikit-learn PCA')
    plt.plot(manual_pca, label='Manual PCA', linestyle='--')
    plt.xlabel('Sample')
    plt.ylabel('Transformed Value')
    plt.legend()
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_04_37.jpg

    图 4.37:数据图示

  7. 注意,两个图几乎一模一样,只是一个是另一个的镜像,且两者之间有偏移。显示 sklearn_pcamanual_pca 模型的组件:

    model.components_
    

    输出结果如下:

    array([[0.98965371, 0.14347657]])
    
  8. 现在,打印 P

    P
    

    输出结果如下:

    array([-0.98965371, -0.14347657])
    

    注意符号上的差异;数值是相同的,但符号不同,产生镜像结果。这只是约定上的差异,没有实际意义。

  9. manual_pca模型乘以-1并重新绘制:

    manual_pca *= -1
    plt.figure(figsize=(10, 7))
    plt.plot(sklearn_pca, label='Scikit-learn PCA')
    plt.plot(manual_pca, label='Manual PCA', linestyle='--')
    plt.xlabel('Sample')
    plt.ylabel('Transformed Value')
    plt.legend()
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_04_38.jpg

    图 4.38:重新绘制的数据

  10. 现在,我们只需处理两者之间的偏移。scikit-learn API 会在转换之前先减去数据的均值。在进行手动 PCA 转换之前,从数据集中减去每列的均值:

    mean_vals = np.mean(df.values, axis=0)
    offset_vals = df.values - mean_vals
    manual_pca = P.dot(offset_vals.T)
    
  11. 将结果乘以-1

    manual_pca *= -1
    
  12. 重新绘制各个sklearn_pcamanual_pca值:

    plt.figure(figsize=(10, 7))
    plt.plot(sklearn_pca, label='Scikit-learn PCA')
    plt.plot(manual_pca, label='Manual PCA', linestyle='--')
    plt.xlabel('Sample')
    plt.ylabel('Transformed Value')
    plt.legend()
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_04_39.jpg

图 4.39:重新绘制数据

最终图将展示两种方法完成的降维实际上是相同的。差异体现在covariance矩阵符号上的不同,因为两种方法仅使用不同的特征作为基准进行比较。最后,两个数据集之间也存在偏移,这归因于在执行 scikit-learn PCA 转换之前已减去均值样本。

注意

要访问此特定部分的源代码,请参考packt.live/2O9MEk4

你也可以在线运行此示例,访问packt.live/3gBntTU

活动 4.02:使用扩展的种子数据集进行 PCA

解决方案:

  1. 导入pandasmatplotlib。为了启用 3D 绘图,你还需要导入Axes3D

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from mpl_toolkits.mplot3d import Axes3D #Required for 3D plotting
    
  2. 读取数据集并选择ALKC列:

    df = pd.read_csv('../Seed_Data.csv')[['A', 'LK', 'C']]
    df.head()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_04_40.jpg

    图 4.40:内核的面积、长度和紧凑性

  3. 在三维空间中绘制数据:

    fig = plt.figure(figsize=(10, 7))
    # Where Axes3D is required
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter(df['A'], df['LK'], df['C'])
    ax.set_xlabel('Area of Kernel')
    ax.set_ylabel('Length of Kernel')
    ax.set_zlabel('Compactness of Kernel')
    ax.set_title('Expanded Seeds Dataset')
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_04_41.jpg

    图 4.41:扩展的种子数据集绘图

  4. 创建一个不指定主成分数量的PCA模型:

    model = PCA()
    
  5. 将模型拟合到数据集:

    model.fit(df.values)
    

    输出如下:

    PCA(copy=True, iterated_power='auto', n_components=None, 
        random_state=None,
        svd_solver='auto', tol=0.0, whiten=False)
    
  6. 显示特征值或explained_variance_ratio_

    model.explained_variance_ratio_
    

    输出如下:

    array([9.97794495e-01, 2.19418709e-03, 1.13183333e-05])
    

    我们希望减少数据集的维度,但仍保留至少 90%的方差。为了保留 90%的方差,最少需要多少个主成分?

    至少需要第一主成分以保留 90%以上的方差。第一主成分提供了数据集 99.7%的方差。

  7. 创建一个新的PCA模型,这次指定所需的主成分数量,以保留至少 90%的方差:

    model = PCA(n_components=1)
    
  8. 使用新模型转换数据:

    data_transformed = model.fit_transform(df.values)
    
  9. 将转换后的数据恢复到原始数据空间:

    data_restored = model.inverse_transform(data_transformed)
    
  10. 在一个子图中绘制恢复后的三维数据,在第二个子图中绘制原始数据,以可视化去除部分方差的效果:

    fig = plt.figure(figsize=(10, 14))
    # Original Data
    ax = fig.add_subplot(211, projection='3d')
    ax.scatter(df['A'], df['LK'], df['C'], label='Original Data');
    ax.set_xlabel('Area of Kernel');
    ax.set_ylabel('Length of Kernel');
    ax.set_zlabel('Compactness of Kernel');
    ax.set_title('Expanded Seeds Dataset');
    # Transformed Data
    ax = fig.add_subplot(212, projection='3d')
    ax.scatter(data_restored[:,0], data_restored[:,1], \
               data_restored[:,2], label='Restored Data');
    ax.set_xlabel('Area of Kernel');
    ax.set_ylabel('Length of Kernel');
    ax.set_zlabel('Compactness of Kernel');
    ax.set_title('Restored Seeds Dataset');
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_04_42.jpg

图 4.42:扩展和恢复后的 Seeds 数据集的绘图

从前面的图表中可以看出,我们已经去除了数据中的大部分噪声,但保留了关于数据趋势的最重要信息。你可以看到,通常情况下,小麦粒的紧密度随着面积的增大而增加。

注意

在应用 PCA 时,必须考虑所建模数据的大小以及可用的系统内存。奇异值分解过程涉及将数据分离为特征值和特征向量,这可能会占用大量内存。如果数据集过大,可能会导致无法完成处理、性能显著下降或系统崩溃。

要访问此特定部分的源代码,请参考 packt.live/2ZVpaFc

你也可以在线运行这个示例,网址:packt.live/3gIrR3D

5. 自编码器

活动 5.01:MNIST 神经网络

解决方案:

在这个活动中,你将训练一个神经网络来识别 MNIST 数据集中的图像,并强化你在训练神经网络方面的技能:

  1. 导入 picklenumpymatplotlib 以及 Keras 中的 SequentialDense 类:

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Sequential
    from keras.layers import Dense
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 加载 mnist.pkl 文件,该文件包含来自 MNIST 数据集的前 10,000 张图像及其对应标签,这些数据可在随附的源代码中找到。MNIST 数据集是包含 0 到 9 手写数字的 28 x 28 灰度图像系列。提取图像和标签:

    with open('mnist.pkl', 'rb') as f:
        data = pickle.load(f)
    images = data['images']
    labels = data['labels']
    
  3. 绘制前 10 个样本及其相应的标签:

    plt.figure(figsize=(10, 7))
    for i in range(10):
        plt.subplot(2, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.title(labels[i])
        plt.axis('off')
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_40.jpg

    图 5.40:前 10 个样本

  4. 使用独热编码对标签进行编码:

    one_hot_labels = np.zeros((images.shape[0], 10))
    for idx, label in enumerate(labels):
        one_hot_labels[idx, label] = 1
    one_hot_labels
    

    输出结果如下:

    array([[0., 0., 0., ..., 0., 0., 0.],
           [1., 0., 0., ..., 0., 0., 0.],
           [0., 0., 0., ..., 0., 0., 0.],
           ...,
           [0., 0., 0., ..., 0., 0., 0.],
           [0., 0., 0., ..., 0., 0., 1.],
           [0., 0., 0., ..., 1., 0., 0.]])
    
  5. 准备好输入神经网络的图像。作为提示,这个过程分为两个独立的步骤:

    images = images.reshape((-1, 28 ** 2))
    images = images / 255.
    
  6. 在 Keras 中构建一个神经网络模型,接受已准备好的图像,具有 600 个单元的隐藏层并使用 ReLU 激活函数,输出层的单元数与类别数相同。输出层使用 softmax 激活函数:

    model = Sequential([Dense(600, input_shape=(784,), \
                        activation='relu'), \
                        Dense(10, activation='softmax'),])
    
  7. 使用多类交叉熵、随机梯度下降和准确率性能指标来编译模型:

    model.compile(loss='categorical_crossentropy', \
                  optimizer='sgd', metrics=['accuracy'])
    
  8. 训练模型。需要多少个 epoch 才能在训练数据上达到至少 95% 的分类准确率?我们来看看:

    model.fit(images, one_hot_labels, epochs=20)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_41.jpg

图 5.41:训练模型

需要 15 个 epoch 才能在训练集上达到至少 95% 的分类准确率。

在这个示例中,我们使用分类器训练的数据来衡量神经网络分类器的性能。通常情况下,不应该使用这种方法,因为它通常报告比你期望的模型准确度更高。在监督学习问题中,应使用若干 交叉验证 技术。由于这是一本关于无监督学习的课程,交叉验证超出了本书的范围。

注意

要访问此特定部分的源代码,请参考 packt.live/2VZpLnZ

你也可以在网上运行这个示例,访问 packt.live/2Z9ueGz

活动 5.02:简单的 MNIST 自动编码器

解决方案:

  1. 导入 picklenumpymatplotlib,以及 Keras 中的 ModelInputDense 类:

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers import Input, Dense
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 从提供的 MNIST 数据集样本中加载图像,样本随附有源代码 (mnist.pkl):

    with open('mnist.pkl', 'rb') as f:
        images = pickle.load(f)['images']
    
  3. 准备将图像输入到神经网络中。作为提示,这个过程有 两个 独立的步骤:

    images = images.reshape((-1, 28 ** 2))
    images = images / 255.
    
  4. 构建一个简单的自动编码器网络,将图像大小在编码阶段压缩至 10 x 10:

    input_stage = Input(shape=(784,))
    encoding_stage = Dense(100, activation='relu')(input_stage)
    decoding_stage = Dense(784, activation='sigmoid')(encoding_stage)
    autoencoder = Model(input_stage, decoding_stage)
    
  5. 使用二元交叉熵损失函数和 adadelta 梯度下降法编译自动编码器:

    autoencoder.compile(loss='binary_crossentropy', \
                        optimizer='adadelta')
    
  6. 拟合编码器模型:

    autoencoder.fit(images, images, epochs=100)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_42.jpg

    图 5.42:训练模型

  7. 计算并存储编码阶段前五个样本的输出:

    encoder_output = Model(input_stage, encoding_stage)\
                     .predict(images[:5])
    
  8. 将编码器的输出重塑为 10 x 10(10 x 10 = 100)像素,并乘以 255:

    encoder_output = encoder_output.reshape((-1, 10, 10)) * 255
    
  9. 计算并存储解码阶段前五个样本的输出:

    decoder_output = autoencoder.predict(images[:5])
    
  10. 将解码器的输出重塑为 28 x 28,并乘以 255:

    decoder_output = decoder_output.reshape((-1, 28, 28)) * 255
    
  11. 绘制原始图像、编码器输出和解码器:

    images = images.reshape((-1, 28, 28))
    plt.figure(figsize=(10, 7))
    for i in range(5):
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i], cmap='gray')
        plt.axis('off')
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_43.jpg

图 5.43:原始图像、编码器输出和解码器

到目前为止,我们已经展示了如何使用编码和解码阶段的简单单隐层将数据降维。我们还可以通过向编码和解码阶段添加额外的层,使该模型更加复杂。

注意

要访问此特定部分的源代码,请参考 packt.live/3f5ZSdH

你也可以在网上运行这个示例,访问 packt.live/2W0ZkhP

活动 5.03:MNIST 卷积自动编码器

解决方案:

  1. 导入 picklenumpymatplotlibkeras.models 中的 Model 类,导入 keras.layers 中的 InputConv2DMaxPooling2DUpSampling2D

    import pickle
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.models import Model
    from keras.layers \
    import Input, Conv2D, MaxPooling2D, UpSampling2D
    import tensorflow.python.util.deprecation as deprecation
    deprecation._PRINT_DEPRECATION_WARNINGS = False
    
  2. 加载数据:

    with open('mnist.pkl', 'rb') as f:
        images = pickle.load(f)['images']
    
  3. 将图像缩放至 0 到 1 之间:

    images = images / 255.
    
  4. 我们需要重塑图像,以添加一个深度通道,以便用于卷积阶段。将图像重塑为 28 x 28 x 1:

    images = images.reshape((-1, 28, 28, 1))
    
  5. 定义输入层。我们将使用与图像相同形状的输入:

    input_layer = Input(shape=(28, 28, 1,))
    
  6. 添加一个具有 16 层或滤波器的卷积阶段,使用 3 x 3 的权重矩阵,ReLU 激活函数,并使用相同的填充,这意味着输出与输入图像的长度相同:

    hidden_encoding = \
    Conv2D(16, # Number of layers or filters in the weight matrix \
           (3, 3), # Shape of the weight matrix \
           activation='relu', \
           padding='same', # How to apply the weights to the images \
           )(input_layer)
    
  7. 为编码器添加一个最大池化层,使用 2 x 2 的卷积核:

    encoded = MaxPooling2D((2, 2))(hidden_encoding)
    
  8. 添加解码卷积层:

    hidden_decoding = \
    Conv2D(16, # Number of layers or filters in the weight matrix \
           (3, 3), # Shape of the weight matrix \
           activation='relu', \
           padding='same', # How to apply the weights to the images \
           )(encoded)
    
  9. 添加上采样层:

    upsample_decoding = UpSampling2D((2, 2))(hidden_decoding)
    
  10. 添加最后一个卷积阶段,按照初始图像的深度使用一层:

    decoded = \
    Conv2D(1, # Number of layers or filters in the weight matrix \
           (3, 3), # Shape of the weight matrix \
           activation='sigmoid', \
           padding='same', # How to apply the weights to the images \
           )(upsample_decoding)
    
  11. 通过将网络的第一层和最后一层传递给 Model 类来构建模型:

    autoencoder = Model(input_layer, decoded)
    
  12. 显示模型结构:

    autoencoder.summary()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_44.jpg

    图 5.44: 模型结构

  13. 使用二元交叉熵损失函数和 adadelta 梯度下降法编译自编码器:

    autoencoder.compile(loss='binary_crossentropy', \
                        optimizer='adadelta')
    
  14. 现在,让我们拟合模型;再次,将图像作为训练数据和期望的输出。由于卷积网络的计算时间较长,因此训练 20 个 epoch:

    autoencoder.fit(images, images, epochs=20)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_45.jpg

    图 5.45: 训练模型

  15. 计算并存储前五个样本的编码阶段输出:

    encoder_output = Model(input_layer, encoded).predict(images[:5])
    
  16. 为了可视化,重塑编码器输出,每个图像的大小为 X*Y:

    encoder_output = encoder_output.reshape((-1, 14 * 14, 16))
    
  17. 获取解码器对前五个图像的输出:

    decoder_output = autoencoder.predict(images[:5])
    
  18. 将解码器输出重塑为 28 x 28 的大小:

    decoder_output = decoder_output.reshape((-1, 28, 28))
    
  19. 将原始图像重塑回 28 x 28 的大小:

    images = images.reshape((-1, 28, 28))
    
  20. 绘制原始图像、平均编码器输出和解码器:

    plt.figure(figsize=(10, 7))
    for i in range(5):
        # Plot the original digit images
        plt.subplot(3, 5, i + 1)
        plt.imshow(images[i], cmap='gray')
        plt.axis('off')
        # Plot the encoder output
        plt.subplot(3, 5, i + 6)
        plt.imshow(encoder_output[i], cmap='gray')
        plt.axis('off')
        # Plot the decoder output
        plt.subplot(3, 5, i + 11)
        plt.imshow(decoder_output[i], cmap='gray')
        plt.axis('off')
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_05_46.jpg

图 5.46: 原始图像、编码器输出和解码器

在本次活动结束时,您将开发一个包含卷积层的自编码器神经网络。注意解码器表示的改进。与全连接神经网络层相比,这种架构在性能上有显著优势,并且在处理基于图像的数据集和生成人工数据样本时极为有用。

注意

要访问此特定部分的源代码,请参见 packt.live/2CdpIxY

您还可以在在线运行此示例 packt.live/3iKz8l2

6. t-分布随机邻域嵌入

活动 6.01: 葡萄酒 t-SNE

解决方案:

  1. 导入 pandasnumpymatplotlib,以及来自 scikit-learn 的 t-SNEPCA 模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 使用随附源代码中的 wine.data 文件加载葡萄酒数据集,并显示数据的前五行:

    df = pd.read_csv('wine.data', header=None)
    df.head()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_25.jpg

    图 6.25: 葡萄酒数据集的前五行

  3. 第一列包含标签;提取该列并将其从数据集中移除:

    labels = df[0]
    del df[0]
    
  4. 执行 PCA,将数据集降到前六个主成分:

    model_pca = PCA(n_components=6)
    wine_pca = model_pca.fit_transform(df)
    
  5. 确定这六个成分描述的数据中的方差量:

    np.sum(model_pca.explained_variance_ratio_)
    

    输出如下:

    0.99999314824536
    
  6. 创建一个使用指定随机状态并设置verbose值为 1 的 t-SNE 模型:

    tsne_model = TSNE(random_state=0, verbose=1)
    tsne_model
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_26.jpg

    图 6.26:创建 t-SNE 模型

  7. 将 PCA 数据拟合到 t-SNE 模型:

    wine_tsne = tsne_model.fit_transform\
                (wine_pca.reshape((len(wine_pca), -1)))
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_27.jpg

    图 6.27:将 PCA 数据拟合到 t-SNE 模型

  8. 确认 t-SNE 拟合数据的形状是二维的:

    wine_tsne.shape
    

    输出如下:

    (178, 2)
    
  9. 创建二维数据的散点图:

    plt.figure(figsize=(10, 7))
    plt.scatter(wine_tsne[:,0], wine_tsne[:,1])
    plt.title('Low Dimensional Representation of Wine')
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_28.jpg

    图 6.28:二维数据的散点图

  10. 创建带有类别标签的二维数据二次散点图,以可视化可能存在的聚类:

    MARKER = ['o', 'v', '^',]
    plt.figure(figsize=(10, 7))
    plt.title('Low Dimensional Representation of Wine')
    for i in range(1, 4):
        selections = wine_tsne[labels == i]
        plt.scatter(selections[:,0], selections[:,1], \
                    marker=MARKER[i-1], label=f'Wine {i}', s=30)
        plt.legend()
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_15.jpg

图 6.29:二维数据的二次图

请注意,尽管不同种类的葡萄酒存在重叠,但也可以看到数据中存在一些聚类。第一种葡萄酒类别主要位于图表的左上角,第二种葡萄酒类别位于右下角,而第三种葡萄酒类别位于前两者之间。这种表示方式肯定不能用来高信心地分类单个葡萄酒样本,但它展示了一个总体趋势以及我们之前无法看到的高维数据中的一系列聚类。

本节中,我们介绍了生成 SNE 图的基础知识。将高维数据表示为低维空间的能力至关重要,尤其是为了更全面地理解手头的数据。有时,这些图表可能很难解读,因为其中的确切关系有时是矛盾的,有时会导致误导性的结构。

注意

要访问此特定部分的源代码,请参考packt.live/2ZSVKrf

你也可以在线运行这个例子,访问packt.live/2CgAWBE

活动 6.02:t-SNE 葡萄酒与困惑度

解决方案:

  1. 导入pandasnumpymatplotlib,以及从 scikit-learn 导入t-SNEPCA模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 加载葡萄酒数据集并检查前五行:

    df = pd.read_csv('wine.data', header=None)
    df.head()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_30.jpg

    图 6.30:葡萄酒数据集的前五行

  3. 第一列提供了标签;从 DataFrame 中提取它们并存储到单独的变量中。确保将该列从 DataFrame 中删除:

    labels = df[0]
    del df[0]
    
  4. 对数据集执行 PCA 并提取前六个成分:

    model_pca = PCA(n_components=6)
    wine_pca = model_pca.fit_transform(df)
    wine_pca = wine_pca.reshape((len(wine_pca), -1))
    
  5. 构建一个循环,遍历困惑度值(1、5、20、30、80、160、320)。对于每个循环,生成一个带有相应困惑度的 t-SNE 模型,并打印带标签的葡萄酒类别的散点图。注意不同困惑度值的影响:

    MARKER = ['o', 'v', '^',]
    for perp in [1, 5, 20, 30, 80, 160, 320]:
        tsne_model = TSNE(random_state=0, verbose=1, perplexity=perp)
        wine_tsne = tsne_model.fit_transform(wine_pca)
        plt.figure(figsize=(10, 7))
        plt.title(f'Low Dimensional Representation of Wine. \
                  Perplexity {perp}');
        for i in range(1, 4):
            selections = wine_tsne[labels == i]
            plt.scatter(selections[:,0], selections[:,1], \
                        marker=MARKER[i-1], label=f'Wine {i}', s=30)
            plt.legend()
    plt.show()
    

    困惑度值为 1 无法将数据分离成任何特定结构:

    ![图 6.31:困惑度为 1 的图]

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_31.jpg)

图 6.31:困惑度为 1 的图

增加困惑度到 5 会导致非常非线性的结构,难以分离,并且很难识别任何簇或模式:

![图 6.32:困惑度为 5 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_32.jpg)

图 6.32:困惑度为 5 的图

困惑度为 20 时,最终开始显示某种马蹄形结构。虽然在视觉上很明显,但实现起来仍然很棘手:

![图 6.33:困惑度为 20 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_33.jpg)

图 6.33:困惑度为 20 的图

困惑度值为 30 显示了相当不错的结果。投影结构之间存在一定的线性关系,且葡萄酒类型之间有一定的分隔:

![图 6.34:困惑度为 30 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_34.jpg)

图 6.34:困惑度为 30 的图

最后,活动中的最后两张图展示了随着困惑度(perplexity)增加,图表变得越来越复杂和非线性的程度:

![图 6.35:困惑度为 80 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_35.jpg)

图 6.35:困惑度为 80 的图

这是困惑度值为 160 时的图:

![图 6.36:困惑度为 160 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_36.jpg)

图 6.36:困惑度为 160 的图

最后,这是困惑度值为 320 时的图:

![图 6.37:困惑度为 320 的图]

](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_37.jpg)

图 6.37:困惑度为 320 的图

通过查看每个困惑度值的单独图表,困惑度对数据可视化的影响立刻显而易见。非常小或非常大的困惑度值会产生一系列不寻常的形状,这些形状无法表示任何持久的模式。最合适的值似乎是 30(图 6.35),它产生了最线性的图表。

在这个活动中,我们展示了在选择困惑度时需要小心,并且可能需要一些迭代才能确定正确的值。

注意

要访问此特定部分的源代码,请参阅 packt.live/3faqESn

你还可以在线运行此示例,网址为 packt.live/2AF12Oi

活动 6.03:t-SNE 葡萄酒和迭代

解决方案:

  1. 导入 pandasnumpymatplotlib,以及来自 scikit-learn 的 t-SNEPCA 模型:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.decomposition import PCA
    from sklearn.manifold import TSNE
    
  2. 加载葡萄酒数据集并检查前五行:

    df = pd.read_csv('wine.data', header=None)
    df.head()
    

    输出如下:

    ![图 6.38:葡萄酒数据集的前五行]

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_38.jpg)

    图 6.38:葡萄酒数据集的前五行

  3. 第一列提供标签;从 DataFrame 中提取这些标签并将它们存储在一个单独的变量中。确保从 DataFrame 中删除该列:

    labels = df[0]
    del df[0]
    
  4. 对数据集执行 PCA 并提取前六个组件:

    model_pca = PCA(n_components=6)
    wine_pca = model_pca.fit_transform(df)
    wine_pca = wine_pca.reshape((len(wine_pca), -1))
    
  5. 构建一个循环,遍历迭代值(2505001000)。对于每个循环,生成具有相应迭代次数和相同进展值但不同迭代次数的 t-SNE 模型:

    MARKER = ['o', 'v', '1', 'p' ,'*', '+', 'x', 'd', '4', '.']
    for iterations in [250, 500, 1000]:
        model_tsne = TSNE(random_state=0, verbose=1, \
                          n_iter=iterations, \
                          n_iter_without_progress=iterations)
        wine_tsne = model_tsne.fit_transform(wine_pca)
    
  6. 绘制标记的葡萄酒类别散点图。注意不同迭代值的影响:

        plt.figure(figsize=(10, 7))
        plt.title(f'Low Dimensional Representation of Wine \
    (iterations = {iterations})')
        for i in range(10):
            selections = wine_tsne[labels == i]
            plt.scatter(selections[:,0], selections[:,1], \
                        alpha=0.7, marker=MARKER[i], s=10);
            x, y = selections.mean(axis=0)
            plt.text(x, y, str(i), \
                     fontdict={'weight': 'bold', 'size': 30})
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_39.jpg

图 6.39:带有 250 次迭代的葡萄酒类别散点图

这是 500 次迭代的情况图:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_40.jpg

图 6.40:带有 500 次迭代的葡萄酒类别散点图

这是 1,000 次迭代的情况图:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_06_41.jpg

图 6.41:带有 1,000 次迭代的葡萄酒类别散点图

同样,我们可以看到随着迭代次数的增加,数据结构得到了改善。即使在这样一个相对简单的数据集中,250 次迭代也不足以将数据结构投射到低维空间中。

正如我们在本活动中观察到的,设置迭代参数是一个平衡点。在本例中,250 次迭代不足,至少需要 1,000 次迭代才能稳定数据。

注意

要访问此特定部分的源代码,请参阅packt.live/2ZOJuYv

您还可以在packt.live/2Z8wEoP上线上运行此示例。

7. 主题建模

活动 7.01:加载和清洗 Twitter 数据

解决方案:

  1. 导入必要的库:

    import warnings
    warnings.filterwarnings('ignore')
    import langdetect 
    import matplotlib.pyplot 
    import nltk
    nltk.download('wordnet')
    nltk.download('stopwords')
    import numpy 
    import pandas 
    import pyLDAvis 
    import pyLDAvis.sklearn 
    import regex 
    import sklearn 
    
  2. packt.live/2Xje5xF加载 LA Times 健康 Twitter 数据(latimeshealth.txt)。

    path = 'latimeshealth.txt' 
    df = pandas.read_csv(path, sep="|", header=None)
    df.columns = ["id", "datetime", "tweettext"]
    
  3. 运行快速的探索性分析来确定数据的大小和结构:

    def dataframe_quick_look(df, nrows):
        print("SHAPE:\n{shape}\n".format(shape=df.shape))
        print("COLUMN NAMES:\n{names}\n".format(names=df.columns))
        print("HEAD:\n{head}\n".format(head=df.head(nrows)))
    dataframe_quick_look(df, nrows=2)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_49.jpg

    图 7.49:数据形状、列名和数据头

  4. 提取推文文本并将其转换为列表对象:

    raw = df['tweettext'].tolist() 
    print("HEADLINES:\n{lines}\n".format(lines=raw[:5])) 
    print("LENGTH:\n{length}\n".format(length=len(raw))) 
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_50.jpg

    图 7.50:标题及其长度

  5. 编写一个函数,在空格上执行语言检测和分词,然后将屏幕名称和网址分别替换为SCREENNAMEURL。该函数还应删除标点符号、数字以及SCREENNAMEURL的替换内容。将所有内容转换为小写,除了SCREENNAMEURL之外。它应该移除所有停用词,执行词形归并,并仅保留五个或更多字母的单词。

    Activity7.01-Activity7.03.ipynb
    def do_language_identifying(txt): 
        try: 
            the_language = langdetect.detect(txt) 
        except: 
            the_language = 'none' 
        return the_language 
    def do_lemmatizing(wrd): 
        out = nltk.corpus.wordnet.morphy(wrd)
        return (wrd if out is None else out)
    The complete code for this step can be found at https://packt.live/3e3VifV.
    
  6. 将第 5 步定义的函数应用于每条推文:

    clean = list(map(do_tweet_cleaning, raw)) 
    
  7. 删除输出列表中等于 None 的元素:

    clean = list(filter(None.__ne__, clean)) 
    print("HEADLINES:\n{lines}\n".format(lines=clean[:5]))
    print("LENGTH:\n{length}\n".format(length=len(clean)))
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_51.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_51.jpg)

    图 7.51: 移除 None 后的标题和长度

  8. 将每条推文的元素重新转换为字符串。使用空格连接:

    clean_sentences = [" ".join(i) for i in clean]
    print(clean_sentences[0:10])
    

    输出列表的前 10 个元素应类似于以下内容:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_52.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_22.jpg)

图 7.52: 已清理用于建模的推文

保持笔记本打开,便于未来的活动。完成此活动后,你现在应该能够相当自如地处理文本数据并为主题建模做好准备。一个重要的提示是,要注意练习和活动之间在数据清理需求上的细微差别。建模不是一成不变的过程,如果你在开始建模工作前花足够的时间探索数据,这一点会非常明显。

注意

要访问此特定部分的源代码,请参考packt.live/3e3VifV

你也可以在packt.live/3fegXlU在线运行这个示例。你必须执行整个笔记本,才能得到期望的结果。

活动 7.02: LDA 和健康推文

解决方案:

  1. 指定 number_wordsnumber_docsnumber_features 变量:

    number_words = 10
    number_docs = 10
    number_features = 1000
    
  2. 创建一个词袋模型并将特征名称赋值给另一个变量,以便后续使用:

    vectorizer1 = sklearn.feature_extraction.text\
                  .CountVectorizer(analyzer="word", \
                                   max_df=0.95, \
                                   min_df=10, \
                                   max_features=number_features)
    clean_vec1 = vectorizer1.fit_transform(clean_sentences)
    print(clean_vec1[0]) 
    feature_names_vec1 = vectorizer1.get_feature_names()
    

    输出结果如下:

    (0, 320)    1 
    
  3. 确定最优主题数量:

    Activity7.01-Activity7.03.ipynb
    def perplexity_by_ntopic(data, ntopics): 
        output_dict = {"Number Of Topics": [], \
                       "Perplexity Score": []}
        for t in ntopics: 
            lda = sklearn.decomposition\
                  .LatentDirichletAllocation(n_components=t, \
                                             learning_method="online", \
                                             random_state=0)
            lda.fit(data)
            output_dict["Number Of Topics"].append(t) 
            output_dict["Perplexity Score"]\
            .append(lda.perplexity(data))
    The complete code for this step can be found at https://packt.live/3e3VifV.
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_53.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_53.jpg)

    图 7.53: 主题数量与困惑度分数的数据框

  4. 使用最优主题数量拟合 LDA 模型:

    lda = sklearn.decomposition.LatentDirichletAllocation\
          (n_components=optimal_num_topics, \
           learning_method="online", \
           random_state=0)
    lda.fit(clean_vec1) 
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_54.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_54.jpg)

    图 7.54: LDA 模型

  5. 创建并打印词汇-主题表:

    Activity7.01-Activity7.03.ipynb
    def get_topics(mod, vec, names, docs, ndocs, nwords):
        # word to topic matrix 
        W = mod.components_ 
        W_norm = W / W.sum(axis=1)[:, numpy.newaxis] 
        # topic to document matrix 
        H = mod.transform(vec) 
        W_dict = {} 
        H_dict = {} 
    The complete code for this step can be found at https://packt.live/3e3VifV.
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_53.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_55.jpg)

    图 7.55: 健康推文数据的词汇-主题表

    注意

    由于支持 LDA 和 NMF 的优化算法,结果可能与所示略有不同。许多函数没有设置种子值的功能。

  6. 打印文档-主题表:

    print(H_df)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_56.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_56.jpg)

    图 7.56: 文档主题表

  7. 创建一个双变量图可视化:

    lda_plot = pyLDAvis.sklearn.prepare(lda, clean_vec1, \
                                        vectorizer1, R=10)
    pyLDAvis.display(lda_plot)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_54.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_39.jpg)

    图 7.57: LDA 模型对健康推文的直方图和双变量图

  8. 保持笔记本打开,以便进行未来的建模。

在讨论下一个主题建模方法之前——非负矩阵分解,我们先通过另一个词袋建模方法。你还记得 CountVectorizer 算法是如何返回每个词在每个文档中出现次数的简单计数的吧?在这个新方法中,称为 TF-IDF(词频-逆文档频率),返回的是代表每个词在每个文档中重要性的权重,而不是原始的计数。

CountVectorizerTfidfVectorizer 两种方法同样有效。何时以及如何使用它们,取决于语料库、使用的主题建模方法以及文档中的噪声量。在接下来的练习中,我们将使用 TfidfVectorizer,并利用输出构建本章稍后出现的 NMF 模型。

注意

要访问该特定部分的源代码,请参考 packt.live/3e3VifV

您还可以在 packt.live/3fegXlU 在线运行此示例。您必须执行整个 Notebook 才能获得期望的结果。

活动 7.03:非负矩阵分解

解决方案:

  1. 创建适当的词袋模型,并将特征名称作为另一个变量输出:

    vectorizer2 = sklearn.feature_extraction.text.TfidfVectorizer\
                  (analyzer="word", \
                   max_df=0.5,\
                   min_df=20,\
                   max_features=number_features,\
                   smooth_idf=False)
    clean_vec2 = vectorizer2.fit_transform(clean_sentences)
    print(clean_vec2[0]) 
    feature_names_vec2 = vectorizer2.get_feature_names() 
    
  2. 使用 活动 7.02 中的主题数(n_components)值来定义并拟合 NMF 算法,LDA 和健康推文

    nmf = sklearn.decomposition.NMF(n_components=optimal_num_topics, \
                                    init="nndsvda", \
                                    solver="mu", \
                                    beta_loss="frobenius", \
                                    random_state=0, \
                                    alpha=0.1, \
                                    l1_ratio=0.5)
    nmf.fit(clean_vec2) 
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_58.jpg

    图 7.58:定义 NMF 模型

  3. 获取主题-文档表和词-主题表。花几分钟探索词汇分组,并尝试定义抽象的主题:

    W_df, H_df = get_topics(mod=nmf, vec=clean_vec2, \
                            names=feature_names_vec2, \
                            docs=raw, \
                            ndocs=number_docs, \
                            nwords=number_words)
    print(W_df)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_59.jpg

    print(H_df)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_07_60.jpg

    图 7.60:带有概率的主题-文档表

  4. 调整模型参数,并重新运行 步骤 3步骤 4

在这个活动中,我们使用 TF-IDF 词袋模型和非负矩阵分解进行了一个主题建模示例。这里真正重要的是理解这些算法在做什么——而不仅仅是如何拟合它们——并理解结果。处理文本数据通常是复杂的,必须认识到并非每个算法每次都会返回有意义的结果。有时,结果根本就没有用。这不是对算法或从业者的反映,而是从数据中提取洞察的挑战之一。

注意

要访问该特定部分的源代码,请参考 packt.live/3e3VifV

您还可以在 packt.live/3fegXlU 在线运行此示例。您必须执行整个 Notebook 才能获得期望的结果。

8. 市场篮分析

活动 8.01:加载和准备完整的在线零售数据

解决方案:

  1. 导入所需的库:

    import matplotlib.pyplot as plt
    import mlxtend.frequent_patterns
    import mlxtend.preprocessing
    import numpy
    import pandas
    
  2. 加载在线零售数据集文件:

    online = pandas.read_excel(io="./Online Retail.xlsx", \
                               sheet_name="Online Retail", \
                               header=0)
    
  3. 清理并准备数据用于建模,包括将清理后的数据转换为列表的列表:

    online['IsCPresent'] = (online['InvoiceNo'].astype(str)\
                            .apply(lambda x: 1 \
                                   if x.find('C') != -1 else 0))
    online1 = (online.loc[online["Quantity"] > 0]\
                     .loc[online['IsCPresent'] != 1]\
                     .loc[:, ["InvoiceNo", "Description"]].dropna())
    invoice_item_list = []
    for num in list(set(online1.InvoiceNo.tolist())):
        tmp_df = online1.loc[online1['InvoiceNo'] == num]
        tmp_items = tmp_df.Description.tolist()
        invoice_item_list.append(tmp_items)
    
  4. 对数据进行编码并将其重新构建为数据框。由于数据量相当大,因此为了确保一切顺利执行,请使用至少具有 8 GB 内存的计算机:

    online_encoder = mlxtend.preprocessing.TransactionEncoder()
    online_encoder_array = \
    online_encoder.fit_transform(invoice_item_list)
    online_encoder_df = pandas.DataFrame(\
                        online_encoder_array, \
                        columns=online_encoder.columns_)
    online_encoder_df.loc[20125:20135, \
                          online_encoder_df.columns.tolist()\
                          [100:110]]
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_16.jpg

图 8.34:从完整的在线零售数据集构建的清理、编码和重构后的数据框的一个子集

注:

要访问此特定部分的源代码,请参阅 packt.live/2Wf2Rcz

本节目前没有在线交互示例,需要在本地运行。

活动 8.02:在完整在线零售数据集上运行 Apriori 算法

解决方案:

  1. 在完整数据上运行 Apriori 算法,使用合理的参数设置:

    mod_colnames_minsupport = mlxtend.frequent_patterns\
                              .apriori(online_encoder_df, \
                                       min_support=0.01, \
                                       use_colnames=True)
    mod_colnames_minsupport.loc[0:6]
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_35.jpg

    图 8.35:使用完整在线零售数据集的 Apriori 算法结果

  2. 将结果过滤到包含 10 COLOUR SPACEBOY PEN 的项集。将支持度值与 练习 8.06 中的结果进行比较,执行 Apriori 算法

    mod_colnames_minsupport[mod_colnames_minsupport['itemsets'] \
    == frozenset({'10 COLOUR SPACEBOY PEN'})]
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_36.jpg

    图 8.36:包含 10 COLOUR SPACEBOY PEN 的项集结果

    支持度值确实发生了变化。当数据集扩展到包含所有交易时,该项集的支持度从 0.0178 降到 0.015793。也就是说,在用于练习的简化数据集里,该项集出现在 1.78% 的交易中,而在完整数据集中,它出现在大约 1.6% 的交易中。

  3. 添加另一列,包含项集的长度。然后,过滤掉那些长度为 2 且支持度在 [0.02, 0.021] 范围内的项集。与 练习 8.06 中的项集相同吗?执行 Apriori 算法第 6 步

    mod_colnames_minsupport['length'] = (mod_colnames_minsupport\
                                         ['itemsets']\
                                         .apply(lambda x: len(x)))
    mod_colnames_minsupport[(mod_colnames_minsupport['length'] == 2) \
                            & (mod_colnames_minsupport['support'] \
                               >= 0.02)\
                            &(mod_colnames_minsupport['support'] \
                               < 0.021)]
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_37.jpg

    图 8.37:基于长度和支持度过滤的结果

    结果确实发生了变化。在查看特定项集及其支持度值之前,我们发现这个过滤后的数据框比前一个练习中的数据框少了项集。当我们使用完整数据集时,符合过滤条件的项集更少;也就是说,只有 17 个项集包含 2 个项目,并且支持度值大于或等于 0.02,小于 0.021。在上一个练习中,有 32 个项集符合这些标准。

  4. 绘制 support 值:

    mod_colnames_minsupport.hist("support", grid=False, bins=30)
    plt.xlabel("Support of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Support")
    plt.show()
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_38.jpg

图 8.38:支持度值的分布

该图显示了完整交易数据集的支持度分布。如你所料,分布呈右偏;即,大多数项集的支持度较低,并且在分布的高端有一长尾。鉴于存在如此多的独特项集,单一项集出现在大多数交易中的比例并不高也不足为奇。有了这些信息,我们可以告诉管理层,即使是最显著的项集,也仅出现在约 10% 的交易中,而绝大多数项集出现在不到 2% 的交易中。这些结果可能无法支持店面布局的调整,但却可以为定价和折扣策略提供很好的参考。通过形式化一系列关联规则,我们可以获得更多关于如何制定这些策略的信息。

注意

要访问该特定部分的源代码,请参见 packt.live/2Wf2Rcz

目前本节没有在线互动示例,需在本地运行。

活动 8.03:在完整在线零售数据集上寻找关联规则

解决方案:

  1. 在完整数据集上拟合关联规则模型。使用置信度度量和最小阈值 0.6

    rules = mlxtend.frequent_patterns\
            .association_rules(mod_colnames_minsupport, \
                               metric="confidence", \
                               min_threshold=0.6, \
                               support_only=False)
    rules.loc[0:6]
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_39.jpg

    图 8.39:基于完整在线零售数据集的关联规则

  2. 计算关联规则的数量。这个数字与 练习 8.07推导关联规则步骤 1 中的结果有区别吗?

    print("Number of Associations: {}".format(rules.shape[0]))
    

    498 条关联规则。是的,数量有所不同。

  3. 绘制置信度与支持度的关系图:

    rules.plot.scatter("support", "confidence", \
                       alpha=0.5, marker="*")
    plt.xlabel("Support")
    plt.ylabel("Confidence")
    plt.title("Association Rules")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_40.jpg

    图 8.40:置信度与支持度的关系图

    该图揭示了该数据集中一些关联规则具有较高的支持度和置信度值。

  4. 查看提升值、杠杆值和信念值的分布:

    rules.hist("lift", grid=False, bins=30)
    plt.xlabel("Lift of item")
    plt.ylabel("Number of items")
    plt.title("Frequency distribution of Lift")
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_41.jpg

图 8.41:提升值的分布

绘制杠杆值,如下所示:

rules.hist("leverage", grid=False, bins=30)
plt.xlabel("Leverage of item")
plt.ylabel("Number of items")
plt.title("Frequency distribution of Leverage")
plt.show()

输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_42.jpg

图 8.42:杠杆值的分布

绘制信念值,如下所示:

plt.hist(rules[numpy.isfinite(rules['conviction'])]\
         .conviction.values, bins = 3)
plt.xlabel("Conviction of item")
plt.ylabel("Number of items")
plt.title("Frequency distribution of Conviction")
plt.show()

输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_08_43.jpg

图 8.43:信念值的分布

在推导出关联规则后,我们可以为管理层提供附加信息,其中最重要的是大约有七个项目集在支持度和置信度方面都有合理的较高值。查看置信度与支持度的散点图,看看这七个项目集与其他所有项目集是如何分开的。这七个项目集也有较高的提升值,如提升直方图所示。看来我们已经识别出了一些可操作的关联规则——这些规则可以用来推动商业决策。

注意

若要访问此特定部分的源代码,请参阅packt.live/2Wf2Rcz

本节目前没有在线交互式示例,需要在本地运行。

9. 热点分析

活动 9.01:一维密度估计

解决方案:

  1. 打开一个新的笔记本并安装所有必要的库。

    get_ipython().run_line_magic('matplotlib', 'inline')
    import matplotlib.pyplot as plt
    import numpy
    import pandas
    import seaborn
    import sklearn.model_selection
    import sklearn.neighbors
    seaborn.set()
    
  2. 从标准正态分布中抽取 1,000 个数据点。将 3.5 加到样本中最后 625 个值(即索引在 375 到 1,000 之间的值)。使用numpy.random.RandomState设置随机状态为 100,以保证采样值一致,然后使用rand.randn(1000)调用随机生成数据点:

    rand = numpy.random.RandomState(100)
    vals = rand.randn(1000)  # standard normal
    vals[375:] += 3.5
    
  3. 将 1,000 个数据点样本绘制成直方图,并在其下方添加一个散点图:

    fig, ax = plt.subplots(figsize=(14, 10))
    ax.hist(vals, bins=50, density=True, label='Sampled Values')
    ax.plot(vals, -0.005 - 0.01 * numpy.random.random(len(vals)), \
            '+k', label='Individual Points')
    ax.legend(loc='upper right')
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_30.jpg

    图 9.30:带有散点图的随机样本直方图

  4. 定义一组带宽值的网格。然后,定义并拟合一个网格搜索交叉验证算法:

    bandwidths = 10 ** numpy.linspace(-1, 1, 100)
    grid = sklearn.model_selection.GridSearchCV\
           (estimator=sklearn.neighbors.KernelDensity(kernel="gaussian"),
            param_grid={"bandwidth": bandwidths}, cv=10)
    grid.fit(vals[:, None])
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_31.jpg

    图 9.31:交叉验证模型的输出

  5. 提取最优带宽值:

    best_bandwidth = grid.best_params_["bandwidth"]
    print("Best Bandwidth Value: {}".format(best_bandwidth))
    

    最优带宽值大约为0.4

  6. 重新绘制第 3 步中的直方图,并叠加估计的密度:

    fig, ax = plt.subplots(figsize=(14, 10))
    ax.hist(vals, bins=50, density=True, alpha=0.75, \
            label='Sampled Values')
    x_vec = numpy.linspace(-4, 8, 10000)[:, numpy.newaxis]
    log_density = numpy.exp(grid.best_estimator_.score_samples(x_vec))
    ax.plot(x_vec[:, 0], log_density, \
            '-', linewidth=4, label='Kernel = Gaussian')
    ax.legend(loc='upper right')
    plt.show()
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_32.jpg

图 9.32:带有最优估计密度的随机样本直方图

注意

若要访问此特定部分的源代码,请参阅packt.live/2wmh5yj

您也可以在网上运行此示例,网址为packt.live/2W0EAGK

活动 9.02:伦敦犯罪分析

解决方案:

  1. 加载犯罪数据。使用保存下载目录的路径,创建一个年份-月份标签的列表,使用read_csv命令迭代加载各个文件,然后将这些文件合并在一起:

    # define the file base path
    base_path = "./metro-jul18-dec18/{yr_mon}/{yr_mon}\
    -metropolitan-street.csv"
    print(base_path)
    

    输出如下:

    ./metro-jul18-dec18/{yr_mon}/{yr_mon}-metropolitan-street.csv
    
  2. 定义年份和月份组合的列表如下:

    yearmon_list = ["2018-0" + str(i) if i <= 9 else "2018-" + str(i) \
                    for i in range(7, 13)]
    print(yearmon_list)
    

    输出如下:

    ['2018-07', '2018-08', '2018-09', \
     '2018-10', '2018-11', '2018-12']
    
  3. 加载数据并打印一些基本信息,如下所示:

    data_yearmon_list = []
    # read each year month file individually
    #print summary statistics
    for idx, i in enumerate(yearmon_list):
        df = pandas.read_csv(base_path.format(yr_mon=i), \
                             header=0)
        data_yearmon_list.append(df)
        if idx == 0:
            print("Month: {}".format(i))
            print("Dimensions: {}".format(df.shape))
            print("Head:\n{}\n".format(df.head(2)))
    # concatenate the list of year month data frames together
    london = pandas.concat(data_yearmon_list)
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_33.jpg

    图 9.33:单个犯罪文件的示例

    打印出的信息仅适用于加载的第一个文件,即 2018 年 7 月来自大都会警察局的犯罪数据。该文件包含近 10 万个条目。你会注意到,这个数据集中有大量有趣的信息,但我们将重点关注Longitude(经度)、Latitude(纬度)、Month(月份)和Crime type(犯罪类型)。

  4. 打印完整且合并的数据集的诊断信息:

    Activity9.01-Activity9.02.ipynb
    print("Dimensions - Full Data:\n{}\n".format(london.shape))
    print("Unique Months - Full Data:\n{}\n".format(london["Month"].unique()))
    print("Number of Unique Crime Types - Full Data:\n{}\n"\
          .format(london["Crime type"].nunique()))
    The complete code for this step can be found at https://packt.live/2wmh5yj.
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_34.jpg

    图 9.34:完整犯罪数据集的描述符

  5. 将数据框缩小为四个变量(LongitudeLatitudeMonthCrime type):

    london_subset = london[["Month", "Longitude", "Latitude", \
                            "Crime type"]]
    london_subset.head(5)
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_35.jpg

    图 9.35:以数据框格式展示的犯罪数据

  6. 使用seaborn中的jointplot函数,拟合并可视化 2018 年 7 月、9 月和 12 月的自行车盗窃案件的三种核密度估计模型:

    crime_bicycle_jul = london_subset\
                        [(london_subset["Crime type"] \
                          == "Bicycle theft") \
                         & (london_subset["Month"] == "2018-07")]
    seaborn.jointplot("Longitude", "Latitude", \
                      crime_bicycle_jul, kind="kde")
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_36.jpg

    crime_bicycle_sept = london_subset\
                         [(london_subset["Crime type"] 
                           == "Bicycle theft") 
                          & (london_subset["Month"] == "2018-09")]
    seaborn.jointplot("Longitude", "Latitude", \
                      crime_bicycle_sept, kind="kde")
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_37.jpg

    crime_bicycle_dec = london_subset\
                        [(london_subset["Crime type"] \
                          == "Bicycle theft") 
                         & (london_subset["Month"] == "2018-12")]
    seaborn.jointplot("Longitude", "Latitude", \
                      crime_bicycle_dec, kind="kde")
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_38.jpg

    图 9.38:2018 年 12 月自行车盗窃案件的联合与边际密度估计

    从一个月到另一个月,自行车盗窃的密度保持相对稳定。密度之间有些许差异,这是可以预期的,因为这些估计密度的基础数据是三个一个月的样本。根据这些结果,警方或犯罪学家应当对预测未来自行车盗窃事件最可能发生的地方有较高的信心。

  7. 重复步骤 4;这一次,使用 2018 年 8 月、10 月和 11 月的扒窃犯罪数据:

    crime_shoplift_aug = london_subset\
                         [(london_subset["Crime type"] \
                           == "Shoplifting") 
                          & (london_subset["Month"] == "2018-08")]
    seaborn.jointplot("Longitude", "Latitude", \
                      crime_shoplift_aug, kind="kde")
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_39.jpg

    crime_shoplift_oct = london_subset\
                         [(london_subset["Crime type"] \
                           == "Shoplifting") \
                          & (london_subset["Month"] == "2018-10")]
    seaborn.jointplot("Longitude", "Latitude", \
                      crime_shoplift_oct, kind="kde")
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_40.jpg

    crime_shoplift_nov = london_subset\
                         [(london_subset["Crime type"] \
                           == "Shoplifting") \
                          & (london_subset["Month"] == "2018-11")]
    seaborn.jointplot("Longitude", "Latitude", \
                      crime_shoplift_nov, kind="kde")
    

    输出结果如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_41.jpg

    图 9.41:2018 年 11 月扒窃案件的联合与边际密度估计

    与自行车盗窃的结果类似,商店扒窃的密度在几个月内相当稳定。2018 年 8 月的密度看起来与其他两个月不同;但是,如果您查看经度和纬度值,您会注意到密度非常相似——只是偏移和缩放了。其原因是可能存在一些离群值,迫使创建一个更大的绘图区域。

  8. 重复步骤 5;这次使用 2018 年 7 月、10 月和 12 月的入室盗窃犯罪数据:

    crime_burglary_jul = london_subset\
                        [(london_subset["Crime type"] == "Burglary") \
                         & (london_subset["Month"] == "2018-07")]
    seaborn.jointplot("Longitude", "Latitude", \
                      crime_burglary_jul, kind="kde")
    

    输出如下:

    https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_42.jpg

图 9.42:2018 年 7 月入室盗窃的联合和边际密度的估计

对于 2018 年 10 月,用于入室盗窃的核密度估计模型的拟合和可视化代码如下:

crime_burglary_oct = london_subset\
                     [(london_subset["Crime type"] == "Burglary")\
                      & (london_subset["Month"] == "2018-10")]
seaborn.jointplot("Longitude", "Latitude", \
                  crime_burglary_oct, kind="kde")

输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_43.jpg

图 9.43:2018 年 10 月入室盗窃的联合和边际密度的估计

对于 2018 年 12 月,用于入室盗窃的核密度估计模型的拟合和可视化代码如下:

crime_burglary_dec = london_subset\
                     [(london_subset["Crime type"] == "Burglary")\
                      & (london_subset["Month"] == "2018-12")]
seaborn.jointplot("Longitude", "Latitude", \
                  crime_burglary_dec, kind="kde")

输出如下:

https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/unspr-lrn-ws/img/B15923_09_44.jpg

图 9.44:2018 年 12 月入室盗窃的联合和边际密度的估计

再次可以看到,这些分布在几个月内非常相似。唯一的区别是密度似乎从 7 月到 12 月扩展或分散。总是由于样本数据中的噪声和固有信息不足导致估计密度的小变化。

注意

要访问此特定部分的源代码,请参阅packt.live/2wmh5yj

您还可以在packt.live/2W0EAGK上在线运行此示例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值