Python-数据挖掘学习指南-一-

41 阅读52分钟

Python 数据挖掘学习指南(一)

原文:annas-archive.org/md5/403522ad77dfa36ee05e0fc0022b1b5e

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《使用 Python 进行数据挖掘学习》的第二版是针对程序员编写的。它的目的是向广泛的程序员介绍数据挖掘,因为我认为这对于计算机科学领域的所有人来说都至关重要。数据挖掘正迅速成为下一代人工智能系统的基石。即使你发现自己没有构建这些系统,你也会使用它们,与它们交互,并受到它们的指导。理解其背后的过程很重要,这有助于你充分利用它们。

第二版是在第一版的基础上构建的。许多章节和练习是相似的,尽管引入了新的概念,练习的范围也扩大了。那些阅读过第一版的人应该能够快速通过本书,并在途中获取新的知识,并参与额外提出的活动。对于那些是本书的新读者,鼓励他们花时间,做练习,进行实验。如果你有任何疑问,请随时打破代码来理解它,并寻求帮助。

由于这是一本面向程序员的书籍,我们假设你有一些编程知识和 Python 本身的知识。因此,对于 Python 代码本身在做什么的解释很少,除非它是模糊的。

本书涵盖的内容

第一章,数据挖掘入门,介绍了我们将要使用的技术,以及实现两个基本算法以开始学习。

第二章,使用 scikit-learn 进行分类,涵盖了分类,这是数据挖掘的关键形式。你还将了解一些使你的数据挖掘实验更容易执行的结构。

第三章,使用决策树预测体育比赛赢家,介绍了两种新的算法,决策树和随机森林,并使用它通过创建有用的特征来预测体育比赛的赢家。

第四章,使用关联分析推荐电影,探讨了基于以往经验推荐产品的问题,并介绍了 Apriori 算法。

第五章,特征与 scikit-learn 转换器,介绍了可以创建的更多类型的特征,以及如何处理不同的数据集。

第六章,使用朴素贝叶斯进行社交媒体洞察,使用朴素贝叶斯算法自动解析社交媒体网站 Twitter 的基于文本的信息。

第七章,使用图挖掘进行推荐跟踪,应用聚类分析和网络分析来找到在社交媒体上值得关注的良好人选。

第八章,使用神经网络击败 CAPTCHAs,探讨了从图像中提取信息,然后训练神经网络以在那些图像中找到单词和字母。

第九章,作者归属分析,探讨了通过提取基于文本的特征和使用支持向量机来确定给定文档的作者。

第十章,聚类新闻文章,使用 k-means 聚类算法根据内容将新闻文章分组。

第十一章,使用深度神经网络进行图像目标检测,通过应用深度神经网络确定图像中展示的是哪种类型的对象。

第十二章,处理大数据,探讨了将算法应用于大数据的工作流程以及如何从中获得洞察。

附录,下一步,逐章介绍,提供有关如何进一步理解所介绍概念的提示。

您需要这本书

需要一台计算机或访问一台计算机来完成这本书,这应该不会让人感到惊讶。计算机应该是相当现代的,但不需要过于强大。任何从大约 2010 年开始的现代处理器和 4GB 的 RAM 就足够了,您可能还可以在较慢的系统上运行几乎所有的代码。

这里有一个例外,即在最后两章中。在这些章节中,我逐步介绍了使用亚马逊的云服务(AWS)来运行代码。这可能需要您支付一些费用,但优点是比在本地运行代码所需的系统设置要少。如果您不想为这些服务付费,所使用的工具都可以在本地计算机上设置,但您确实需要一个现代系统来运行它。至少需要 2012 年制造的处理器和超过 4GB 的 RAM。

我推荐使用 Ubuntu 操作系统,但代码在 Windows、Mac 或任何其他 Linux 变体上都应该运行良好。尽管如此,您可能需要查阅系统文档来安装一些东西。

在这本书中,我使用 pip 来安装代码,这是一个用于安装 Python 库的命令行工具。另一个选择是使用 Anaconda,您可以在以下网址找到它:continuum.io/downloads

我还使用 Python 3 测试了所有代码。大多数代码示例在 Python 2 上无需更改即可工作。如果您遇到任何问题,并且无法解决,请发送电子邮件,我们可以提供解决方案。

这本书的适用对象

这本书是为那些希望以应用为导向的方式开始数据挖掘的程序员而写的。

如果你之前没有编程经验,我强烈建议你在开始之前至少学习一些基础知识。本书不介绍编程,也不过多地解释如何实际(在代码中)输入指令的实现。话虽如此,一旦你通过了基础知识,你应该能够相当快地回到这本书——你不需要首先成为一个专家程序员!

我强烈建议你有一些 Python 编程经验。如果你没有,请随意开始,但你可能想先看看一些 Python 代码,可能专注于使用 IPython 笔记本的教程。在 IPython 笔记本中编写程序与其他方法(如在一个完整的 IDE 中编写 Java 程序)略有不同。

约定

在本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“接下来的几行代码读取链接并将其分配给dataset_filename函数。”

代码块设置如下:

import numpy as np 
dataset_filename = "affinity_dataset.txt" 
X = np.loadtxt(dataset_filename)

任何命令行输入或输出都如下所示:

 $ conda install scikit-learn

新术语重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“为了下载新模块,我们将转到文件 | 设置 | 项目名称 | 项目解释器。”

警告或重要注意事项以如下框的形式出现。

技巧和窍门如下所示。

读者反馈

我们欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中获得最大价值的标题。

要向我们发送一般反馈,只需发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍的标题。

如果你在某个领域有专业知识,并且你对撰写或参与一本书感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲所有者,我们有一些事情可以帮助你从你的购买中获得最大价值。

下载示例代码

你可以从你的账户www.packtpub.com下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

你可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”选项卡上。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载。

文件下载完成后,请确保使用最新版本的以下软件解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learning-Data-Mining-with-Python-Second-Edition。GitHub 仓库的好处是,任何与代码相关的问题,包括与软件版本更改相关的问题,都将被跟踪,那里的代码将包括来自世界各地读者的更改。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

为了避免缩进问题,请使用代码包在 IDE 中运行代码,而不是直接从 PDF 中复制

错误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站或添加到该标题的错误部分下的现有错误列表中。

要查看之前提交的错误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误部分下。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,您可以联系我们的questions@packtpub.com,我们将尽力解决问题。

第一章:数据挖掘入门

我们正在以人类历史上前所未有的规模收集有关我们世界的各种信息。随着这一趋势的发展,我们现在更加重视在日常生活中的使用这些信息。我们期望我们的计算机能够将网页翻译成其他语言,以高精度预测天气,推荐我们喜欢的书籍,以及诊断我们的健康问题。这些期望将在未来不断增长,无论是在应用范围还是效果上。数据挖掘是一种我们可以采用的方法,用于训练计算机通过数据做出决策,并构成了今天许多高科技系统的核心。

Python编程语言因其良好的原因而越来越受欢迎。它为程序员提供了灵活性,拥有许多模块来执行不同的任务,而且 Python 代码通常比其他任何语言都更易于阅读和简洁。有一个庞大且活跃的研究人员、实践者和初学者社区,他们使用 Python 进行数据挖掘。

在本章中,我们将使用 Python 介绍数据挖掘。我们将涵盖以下主题

  • 什么是数据挖掘,我们可以在哪里使用它?

  • 设置一个基于 Python 的环境以进行数据挖掘

  • (亲和力分析的)一个例子,根据购买习惯推荐产品

  • (一个经典的)分类问题的例子,根据植物测量值预测植物种类

介绍数据挖掘

数据挖掘为计算机提供了一种通过数据做出决策的方法。这个决策可以是预测明天的天气,阻止垃圾邮件进入您的收件箱,检测网站的语种,或者在交友网站上找到新的恋情。数据挖掘有许多不同的应用,而且新的应用正在不断被发现。

数据挖掘部分是算法设计、统计学、工程学、优化和计算机科学的结合。然而,结合这些领域的基础技能,我们还需要应用我们在应用数据挖掘的领域的领域知识(专业知识)。领域知识对于从良好结果到卓越结果至关重要。有效地应用数据挖掘通常需要将这些特定领域的知识与算法相结合。

大多数数据挖掘应用都采用相同的高级视图,其中模型从某些数据中学习,并将其应用于其他数据,尽管细节往往变化很大。

数据挖掘应用涉及创建数据集和调整算法,以下步骤将进行解释

  1. 我们通过创建数据集开始我们的数据挖掘过程,描述现实世界的一个方面。数据集包括以下两个方面:
  • 样本:这些是现实世界中的对象,例如一本书、照片、动物、人或任何其他对象。样本也被称为观察、记录或行,以及其他命名约定。

  • 特征:这些是我们数据集中样本的描述或测量。特征可以是长度、特定单词的频率、动物的腿数、创建日期等等。特征也被称为变量、列、属性或协变量,以及其他命名约定。

  1. 下一步是调整数据挖掘算法。每个数据挖掘算法都有参数,这些参数要么在算法内部,要么由用户提供。这种调整使算法能够学习如何对数据进行决策。

作为简单的例子,我们可能希望计算机能够将人们分类为。我们首先收集我们的数据集,其中包括不同人的身高以及他们是否被认为是矮或高:

人员身高矮或高?
1155cm
2165cm
3175cm
4185cm

如上所述,下一步涉及调整我们算法的参数。作为一个简单的算法;如果身高超过x,则该人被认为是高的。否则,他们被认为是矮的。然后我们的训练算法将查看数据并决定x的合适值。对于前面的数据,这个阈值的一个合理值是 170 厘米。算法认为身高超过 170 厘米的人是高的。其他人都被认为是矮的。这样,我们的算法就可以对新的数据进行分类,例如身高为 167 厘米的人,即使我们之前从未见过这样的人。

在前面的数据中,我们有一个明显的特征类型。我们想知道人们是矮还是高,所以我们收集了他们的身高。这个特征工程是数据挖掘中的一个关键问题。在后面的章节中,我们将讨论选择在数据集中收集的良好特征的方法。最终,这一步通常需要一些专业知识或至少一些试错。

在这本书中,我们将通过 Python 介绍数据挖掘。在某些情况下,我们选择代码和流程的清晰性,而不是执行每个任务的最优化方式。这种清晰性有时涉及到跳过一些可以提高算法速度或有效性的细节。

使用 Python 和 Jupyter Notebook

在本节中,我们将介绍安装 Python 以及我们将用于本书大部分内容的Jupyter Notebook。此外,我们还将安装NumPy模块,我们将使用它来进行第一组示例。

Jupyter Notebook 直到最近还被称为 IPython Notebook。你会在项目相关的网络搜索中注意到这个术语。Jupyter 是新的名称,代表着项目范围的扩大,而不仅仅是使用 Python。

安装 Python

Python 编程语言是一种出色、多功能且易于使用的语言。

对于这本书,我们将使用 Python 3.5,该版本可以从 Python 组织的网站www.python.org/downloads/获取。然而,我建议你使用 Anaconda 来安装 Python,你可以从官方网站www.continuum.io/downloads下载。

你将有两个主要版本可供选择,Python 3.5 和 Python 2.7。请记住下载并安装 Python 3.5,这是本书中测试过的版本。按照该网站上的安装说明进行安装。如果你有强烈理由学习 Python 2 版本,那么可以通过下载 Python 2.7 版本来实现。请注意,有些代码可能不会像书中那样工作,可能需要一些解决方案。

在这本书中,我假设你对编程和 Python 本身有一些了解。你不需要成为 Python 的专家就能完成这本书,尽管良好的知识水平会有所帮助。我不会在本书中解释一般的代码结构和语法,除非它与被认为是正常的 Python 编码实践不同。

如果你没有编程经验,我建议你从 Packt Publishing 出版的《Learning Python》这本书开始学习,或者在线可用的《Dive Into Python》这本书,可在www.diveintopython3.net找到。

Python 组织还维护了一份针对 Python 新手的两个在线教程列表:

  • 对于想通过 Python 语言学习编程的非程序员:

wiki.python.org/moin/BeginnersGuide/NonProgrammers

  • 对于已经知道如何编程但需要学习 Python 的程序员:

wiki.python.org/moin/BeginnersGuide/Programmers

Windows 用户需要设置一个环境变量才能从命令行使用 Python,而其他系统通常可以立即执行。我们将在以下步骤中设置它。

  1. 首先,找到你在电脑上安装 Python 3 的位置;默认位置是C:\Python35

  2. 接下来,将此命令输入到命令行(cmd 程序)中:设置环境为PYTHONPATH=%PYTHONPATH%;C:\Python35

如果你的 Python 安装在不同的文件夹中,请记住将C:\Python35进行更改。

一旦你的系统上运行了 Python,你应该能够打开命令提示符,并可以运行以下代码以确保它已正确安装。

    $ python
    Python 3.5.1 (default, Apr 11 2014, 13:05:11)
    [GCC 4.8.2] on Linux
    Type "help", "copyright", "credits" or "license" for more 
      information.
    >>> print("Hello, world!")
Hello, world!
    >>> exit()

注意,我们将使用美元符号($)来表示你需要在终端(在 Windows 上也称为 shell 或cmd)中输入的命令。你不需要输入这个字符(或重新输入屏幕上已经出现的内容)。只需输入剩余的行并按 Enter 键。

在您运行了上述 "Hello, world!" 示例之后,退出程序,然后继续安装一个更高级的环境来运行 Python 代码,即 Jupyter Notebook。

Python 3.5 将包含一个名为 pip 的程序,它是一个包管理器,可以帮助您在系统上安装新的库。您可以通过运行 $ pip freeze 命令来验证 pip 是否在您的系统上工作,该命令会告诉您您在系统上安装了哪些包。Anaconda 还安装了他们的包管理器 conda,您可以使用它。如果不确定,请先使用 conda,如果失败再使用 pip

安装 Jupyter Notebook

Jupyter 是一个 Python 开发平台,其中包含一些用于运行 Python 的工具和环境,它比标准解释器具有更多功能。它包含强大的 Jupyter Notebook,允许您在网页浏览器中编写程序。它还会格式化您的代码,显示输出,并允许您注释脚本。它是探索数据集的出色工具,我们将使用它作为本书代码的主要环境。

要在您的计算机上安装 Jupyter Notebook,您可以在命令行提示符中输入以下内容(不要在 Python 中输入):

    $ conda install jupyter notebook

您不需要管理员权限来安装它,因为 Anaconda 将包存储在用户的目录中。

安装了 Jupyter Notebook 后,您可以使用以下命令启动它:

    $ jupyter notebook

运行此命令将执行两个操作。首先,它将在您刚刚使用的命令提示符中创建一个 Jupyter Notebook 实例(后端)。其次,它将启动您的网页浏览器并连接到此实例,允许您创建一个新的笔记本。它看起来可能像以下截图(其中您需要将 /home/bob 替换为您的当前工作目录):

图片

要停止 Jupyter Notebook 的运行,请打开运行实例的命令提示符(你之前用来运行 jupyter notebook 命令的那个)。然后,按 Ctrl + C,你将收到提示 Shutdown this notebook server (y/[n])?。输入 y 并按 Enter,Jupyter Notebook 将会关闭。

安装 scikit-learn

scikit-learn 包是一个机器学习库,用 Python 编写(但也包含其他语言的代码)。它包含许多算法、数据集、实用工具和框架,用于执行机器学习。Scikit-learn 建立在科学 Python 堆栈之上,包括 NumPySciPy 等库,以提高速度。Scikit-learn 在许多情况下都快速且可扩展,适用于从初学者到高级研究用户的所有技能水平。我们将在第二章 使用 scikit-learn 估算器进行分类中详细介绍 scikit-learn。

要安装scikit-learn,您可以使用随 Python 3 一起提供的conda实用程序,如果您还没有安装,它还会安装NumPySciPy库。以管理员/根权限打开一个终端,并输入以下命令:

    $ conda install scikit-learn

主要的 Linux 发行版用户,如 Ubuntu 或 Red Hat,可能希望从他们的包管理器中安装官方包。

并非所有发行版都有 scikit-learn 的最新版本,所以在安装之前请检查版本。本书所需的最低版本是 0.14。我推荐使用 Anaconda 来为您管理这些,而不是使用系统包管理器进行安装。

想要通过编译源代码安装最新版本或查看更详细的安装说明的用户,可以访问scikit-learn.org/stable/install.html并参考安装 scikit-learn 的官方文档。

一个简单的亲和力分析示例

在本节中,我们将进入我们的第一个示例。数据挖掘的一个常见用途是通过询问购买产品的客户是否希望购买另一个类似的产品来提高销售额。您可以通过亲和力分析执行此分析,亲和力分析是研究事物共存时的情况,即相互关联。

为了重复在统计学课程中教授的臭名昭著的短语,相关性不等于因果关系。这个短语的意思是,亲和力分析的结果不能给出原因。在我们的下一个例子中,我们对产品购买进行亲和力分析。结果显示产品是共同购买的,但并不意味着购买一个产品会导致另一个产品的购买。这种区别很重要,尤其是在确定如何使用结果影响业务流程时,例如。

什么是亲和力分析?

亲和力分析是一种数据挖掘类型,它给出了样本(对象)之间的相似性。这可能是以下内容的相似性:

  • 网站上的用户,以提供多样化的服务或定向广告

  • 商品,以向这些用户销售,提供推荐电影或产品

  • 人类基因,以找到有相同祖先的人

我们可以通过几种方式来衡量亲和力。例如,我们可以记录两个产品一起购买的多频繁。我们还可以记录当一个人购买对象 1 和对象 2 时陈述的准确性。衡量亲和力的其他方法包括计算样本之间的相似性,这些内容我们将在后面的章节中介绍。

产品推荐

将传统业务(如商业)转移到线上时遇到的一个问题是,以前由人类完成的工作需要自动化,以便在线业务可以扩展并与其他现有自动化业务竞争。其中一个例子是向上销售,即向已经购买商品的客户销售额外商品。通过数据挖掘进行自动化的产品推荐是电子商务革命背后的推动力之一,每年将数十亿美元转化为收入。

在这个示例中,我们将关注一个基本的产品推荐服务。我们基于以下想法来设计它:当两个项目历史上一起购买时,它们在未来更有可能一起购买。这种思维方式是许多在线和线下产品推荐服务背后的理念。

对于这类产品推荐算法,一个非常简单的算法是简单地找到任何历史案例,其中用户购买了一个项目,然后推荐用户历史上购买的其他项目。在实践中,像这样的简单算法可以做得很好,至少比随机推荐项目要好。然而,它们可以显著改进,这就是数据挖掘的用武之地。

为了简化编码,我们将一次只考虑两个项目。例如,人们可能在超市同时购买面包和牛奶。在这个早期示例中,我们希望找到以下形式的简单规则:

如果一个人购买了产品 X,那么他们很可能会购买产品 Y

不会涉及多个项目的更复杂规则,例如人们购买香肠和汉堡更有可能购买番茄酱。

使用 NumPy 加载数据集

数据集可以从本书提供的代码包中下载,或从官方 GitHub 仓库下载:

github.com/dataPipelineAU/LearningDataMiningWithPython2

下载此文件并将其保存在你的电脑上,注意数据集的路径。将其放在你将运行代码的目录中是最容易的,但我们可以从电脑上的任何位置加载数据集。

对于这个示例,我建议你在电脑上创建一个新的文件夹来存储你的数据集和代码。从这里,打开你的 Jupyter Notebook,导航到这个文件夹,并创建一个新的笔记本。

我们将要用于这个示例的数据集是一个 NumPy 二维数组,这种格式是本书其余部分大多数示例的基础。这个数组看起来像一张表格,行代表不同的样本,列代表不同的特征。

单元代表特定样本特定特征的值。为了说明,我们可以用以下代码加载数据集:

import numpy as np 
dataset_filename = "affinity_dataset.txt" 
X = np.loadtxt(dataset_filename)

将前面的代码输入到你的(Jupyter)笔记本的第一个单元格中。然后你可以通过按 Shift + Enter 来运行代码(这也会为下一部分的代码添加一个新的单元格)。代码运行后,第一个单元格左侧的方括号将被分配一个递增的数字,让你知道这个单元格已经完成。第一个单元格应该看起来像以下这样:

图片

对于运行时间较长的代码,这里将放置一个星号来表示该代码正在运行或已安排运行。当代码运行完成时(包括如果代码因失败而完成),星号将被一个数字替换。

这个数据集有 100 个样本和五个特征,我们将在后面的代码中需要这些值。让我们使用以下代码提取这些值:

n_samples, n_features = X.shape

如果你选择将数据集存储在 Jupyter Notebooks 所在的目录之外,你需要将dataset_filename的值更改为新位置。

接下来,我们可以展示数据集的一些行,以了解数据。将以下代码行输入下一个单元格并运行它,以打印数据集的前五行:

print(X[:5])

结果将显示在列出的前五笔交易中购买了哪些商品:

[[ 0\.  1\.  0\.  0\.  0.] 
 [ 1\.  1\.  0\.  0\.  0.] 
 [ 0\.  0\.  1\.  0\.  1.] 
 [ 1\.  1\.  0\.  0\.  0.] 
 [ 0\.  0\.  1\.  1\.  1.]]

下载示例代码

你可以从你购买的所有 Packt Publishing 书籍的账户中下载示例代码文件www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。我还设置了一个 GitHub 仓库,其中包含代码的实时版本,以及新的修复、更新等。你可以在以下仓库中检索代码和数据集:github.com/dataPipelineAU/LearningDataMiningWithPython2

你可以通过一次查看每一行(水平线)来读取数据集。第一行(0, 1, 0, 0, 0)显示了第一笔交易中购买的商品。每一列(垂直行)代表每种商品。它们分别是面包、牛奶、奶酪、苹果和香蕉。因此,在第一笔交易中,这个人购买了奶酪、苹果和香蕉,但没有购买面包或牛奶。在新的单元格中添加以下行,以便我们将这些特征数字转换为实际单词:

features = ["bread", "milk", "cheese", "apples", "bananas"]

这些特征中的每一个都包含二进制值,仅表示是否购买了商品,而不表示购买的数量。1表示至少购买了这种类型的一种商品,而0表示完全没有购买这种商品。对于现实世界的数据集,使用精确的数字或更大的阈值是必要的。

实现规则的简单排序

我们希望找到类型为如果一个人购买产品 X,那么他们很可能会购买产品 Y的规则。我们可以通过简单地找到两个产品一起购买的所有场合来轻松地创建我们数据集中所有规则的一个列表。然而,然后我们需要一种方法来确定好的规则和不好的规则,以便我们可以选择特定的产品进行推荐。

我们可以用许多方式评估这类规则,我们将关注其中的两种:支持度置信度

支持度是规则在数据集中出现的次数,这通过简单地计算规则有效的样本数量来计算。有时可以通过将总数除以规则前提有效的总次数来归一化,但在这个实现中我们将简单地计算总数。

前提是规则被认为是活跃的要求。结论是规则的输出。对于例子如果一个人买苹果,他们也买香蕉,只有当前提发生时——一个人买了苹果——该规则才是有效的。然后,规则的结论声明这个人会买香蕉。

虽然支持度衡量规则存在的频率,但置信度衡量当它们可以使用时它们的准确性。你可以通过确定规则在前提适用时应用的百分比来计算这个值。我们首先计算规则在我们的数据中应用的次数,然后除以前提(即if语句)出现的样本数量。

作为例子,我们将计算规则如果一个人买苹果,他们也买香蕉的支持度和置信度。

如以下示例所示,我们可以通过检查sample[3]的值来判断某人在交易中是否购买了苹果,其中我们将一个样本分配到矩阵的某一行:

sample = X[2]

同样,我们可以通过查看sample[4]的值是否等于 1(等等)来检查交易中是否购买了香蕉。我们现在可以计算我们的规则在数据集中出现的次数,从而计算出置信度和支持度。

现在我们需要计算数据库中所有规则的这些统计数据。我们将为此创建两个字典,一个用于有效规则,另一个用于无效规则。这个字典的键将是一个元组(前提和结论)。我们将存储索引,而不是实际的特征名称。因此,我们会存储(3 和 4)来表示之前的规则如果一个人买了苹果,他们也会买香蕉。如果前提和结论都给出,则该规则被认为是有效的。而如果前提给出但结论没有给出,则该规则对该样本被认为是无效的。

以下步骤将帮助我们计算所有可能规则的置信度和支持度:

  1. 我们首先设置一些字典来存储结果。我们将使用defaultdict,它会在访问一个尚不存在的键时设置一个默认值。我们记录有效规则的数目、无效规则的数目以及每个前提的出现次数:
from collections import defaultdict 
valid_rules = defaultdict(int) 
invalid_rules = defaultdict(int) 
num_occurences = defaultdict(int)

  1. 接下来,我们在一个大的循环中计算这些值。我们遍历数据集中的每个样本,然后遍历每个特征作为前提。再次遍历每个特征作为可能的结论,映射前提到结论的关系。如果样本包含一个购买了前提和结论的人,我们在valid_rules中记录这个信息。如果他们没有购买结论产品,我们在invalid_rules中记录这个信息。

  2. 对于样本 X 中的每个样本:

for sample in X:
    for premise in range(n_features):
    if sample[premise] == 0: continue
# Record that the premise was bought in another transaction
    num_occurences[premise] += 1
    for conclusion in range(n_features):
    if premise == conclusion: 
# It makes little sense to
    measure if X -> X.
    continue
    if sample[conclusion] == 1:
# This person also bought the conclusion item
    valid_rules[(premise, conclusion)] += 1

如果前提对这个样本是有效的(它有一个值为1),那么我们记录这个信息并检查我们规则的每个结论。我们跳过任何与前提相同的结论——这会给我们规则,如:如果一个人买了苹果,那么他们也买了苹果,这显然对我们帮助不大。

我们现在已经完成了必要的统计计算,现在可以计算每个规则的支持度置信度。和之前一样,支持度只是我们的valid_rules值:

support = valid_rules

我们可以用相同的方式计算置信度,但我们必须遍历每个规则来计算这个值:

confidence = defaultdict(float)
for premise, conclusion in valid_rules.keys():
    rule = (premise, conclusion)
    confidence[rule] = valid_rules[rule] / num_occurences [premise]

我们现在有一个包含每个规则的支持度和置信度的字典。我们可以创建一个函数,以可读的格式打印出这些规则。规则的签名接受前提和结论索引、我们刚刚计算的支持度和置信度字典,以及一个告诉我们features含义的特征数组。然后我们打印出该规则的SupportConfidence

for premise, conclusion in confidence:
    premise_name = features[premise]
    conclusion_name = features[conclusion]
    print("Rule: If a person buys {0} they will also 
          buy{1}".format(premise_name, conclusion_name))
    print(" - Confidence: {0:.3f}".format
          (confidence[(premise,conclusion)]))
    print(" - Support: {0}".format(support
                                   [(premise, 
                                     conclusion)]))
    print("")

我们可以通过以下方式调用代码来测试它——请随意尝试不同的前提和结论:

for premise, conclusion in confidence:
    premise_name = features[premise]
    conclusion_name = features[conclusion]
    print("Rule: If a person buys {0} they will also 
          buy{1}".format(premise_name, conclusion_name))
    print(" - Confidence: {0:.3f}".format
          (confidence[(premise,conclusion)]))
    print(" - Support: {0}".format(support
                                   [(premise, 
                                     conclusion)]))
    print("")

排序以找到最佳规则

现在我们能够计算所有规则的支持度和置信度,我们希望能够找到最佳的规则。为此,我们进行排名并打印出具有最高值的规则。我们可以对支持和置信度值都这样做。

要找到支持度最高的规则,我们首先对支持度字典进行排序。字典默认不支持排序;items()函数给我们一个包含字典中数据的列表。我们可以使用itemgetter类作为我们的键来对这个列表进行排序,这允许我们排序如此类似的嵌套列表。使用itemgetter(1)允许我们根据值进行排序。将reverse=True设置为真,我们可以首先得到最高的值:

from operator import itemgetter 
sorted_support = sorted(support.items(), key=itemgetter(1), reverse=True)

然后,我们可以打印出前五条规则:

sorted_confidence = sorted(confidence.items(), key=itemgetter(1),
                           reverse=True)
for index in range(5):
    print("Rule #{0}".format(index + 1))
    premise, conclusion = sorted_confidence[index][0]
    print_rule(premise, conclusion, support, confidence, features)

结果看起来如下:

Rule #1 
Rule: If a person buys bananas they will also buy milk 
 - Support: 27 
 - Confidence: 0.474 
Rule #2 
Rule: If a person buys milk they will also buy bananas 
 - Support: 27 
 - Confidence: 0.519 
Rule #3 
Rule: If a person buys bananas they will also buy apples 
 - Support: 27 
 - Confidence: 0.474 
Rule #4 
Rule: If a person buys apples they will also buy bananas 
 - Support: 27 
 - Confidence: 0.628 
Rule #5 
Rule: If a person buys apples they will also buy cheese 
 - Support: 22 
 - Confidence: 0.512

同样,我们可以根据置信度打印出最佳规则。首先,计算排序后的置信度列表,然后使用之前相同的方法打印它们。

sorted_confidence = sorted(confidence.items(), key=itemgetter(1),
                           reverse=True)
for index in range(5):
    print("Rule #{0}".format(index + 1))
    premise, conclusion = sorted_confidence[index][0]
    print_rule(premise, conclusion, support, confidence, features)

两条规则在两个列表的顶部附近。第一条是如果一个人买了苹果,他们也会买奶酪,第二条是如果一个人买了奶酪,他们也会买香蕉。商店经理可以使用这样的规则来组织他们的商店。例如,如果本周苹果打折,就在附近放置奶酪的展示。同样,将香蕉和奶酪同时打折几乎没有意义,因为近 66%买奶酪的人可能会买香蕉——我们的促销不会大幅增加香蕉的销量。

Jupyter Notebook 将在笔记本中内联显示图表。然而,有时这并不是默认配置的。要配置 Jupyter Notebook 以内联显示图表,请使用以下代码行:%matplotlib inline

我们可以使用名为 matplotlib 的库来可视化结果。

我们将从展示规则置信度的简单折线图开始,按置信度顺序排列。matplotlib 使得这变得简单——我们只需传入数字,它就会绘制出一个简单但有效的图表:

from matplotlib import pyplot as plt 
plt.plot([confidence[rule[0]] for rule in sorted_confidence])

图片

使用之前的图表,我们可以看到前五条规则有相当高的置信度,但在此之后效果迅速下降。利用这些信息,我们可能会决定只使用前五条规则来驱动商业决策。最终,使用这种探索技术,结果取决于用户。

在这样的例子中,数据挖掘具有强大的探索能力。一个人可以使用数据挖掘技术来探索其数据集中的关系,以发现新的见解。在下一节中,我们将使用数据挖掘来实现不同的目的:预测和分类。

一个简单的分类示例

在亲和力分析示例中,我们寻找了数据集中不同变量之间的相关性。在分类中,我们有一个我们感兴趣的单一变量,我们称之为类别(也称为目标)。在先前的例子中,如果我们对人们如何购买更多苹果感兴趣,我们会探索与苹果相关的规则,并使用这些规则来指导我们的决策。

什么是分类?

分类是数据挖掘应用最广泛的一种,无论是在实际应用还是在研究中。与之前一样,我们有一组代表我们感兴趣分类的对象或事物的样本。我们还有一个新的数组,即类别值。这些类别值为我们提供了样本的分类。以下是一些例子:

  • 通过观察植物的测量值来确定其种类。这里的类别值将是:这是哪种物种?

  • 确定图像中是否包含狗。类别将是:这张图像中是否有狗?

  • 根据特定测试的结果来确定患者是否患有癌症。类别将是:这位患者是否有癌症?

虽然许多先前的例子是二元(是/否)问题,但它们不必是,就像本节中植物物种分类的例子一样。

分类应用的目的是在已知类别的样本集上训练一个模型,然后将该模型应用于具有未知类别的未见样本。例如,我们想在标记为垃圾邮件或非垃圾邮件的过去电子邮件上训练一个垃圾邮件分类器。然后我想使用这个分类器来确定我的下一封电子邮件是否是垃圾邮件,而无需我自己进行分类。

加载数据集和准备数据

我们将要用于此示例的数据集是著名的植物分类的鸢尾花数据库。在此数据集中,我们有 150 个植物样本,每个样本有四个测量值:萼片长度萼片宽度花瓣长度花瓣宽度(所有单位均为厘米)。这个经典数据集(首次使用于 1936 年!)是数据挖掘的经典数据集之一。有三个类别:鸢尾花塞托萨鸢尾花变色鸢尾花维吉尼卡。目标是通过对样本的测量来确定样本属于哪种植物类型。

scikit-learn 库内置了此数据集,使得数据集的加载变得简单:

from sklearn.datasets import load_iris 
dataset = load_iris() 
X = dataset.data 
y = dataset.target

您也可以使用 print(dataset.DESCR) 来查看数据集的概述,包括一些关于特征细节的信息。

本数据集中的特征是连续值,这意味着它们可以取任何范围的值。测量值是这种类型特征的很好例子,其中测量值可以是 1、1.2 或 1.25 等。连续特征的另一个方面是,彼此接近的特征值表示相似性。一个萼片长度为 1.2 厘米的植物就像一个萼片宽度为 1.25 厘米的植物。

相比之下,分类特征。这些特征虽然通常以数字表示,但不能以相同的方式进行比较。在鸢尾花数据集中,类别值是分类特征的例子。类别 0 代表鸢尾花塞托萨;类别 1 代表鸢尾花变色,类别 2 代表鸢尾花维吉尼卡。这里的编号并不意味着鸢尾花塞托萨比鸢尾花变色更相似,尽管类别值更相似。这里的数字代表类别。我们只能说类别是否相同或不同。

还有其他类型的特征,我们将在后面的章节中介绍。这些包括像素强度、词频和 n-gram 分析。

虽然此数据集中的特征是连续的,但我们将在此示例中使用的算法需要分类特征。将连续特征转换为分类特征的过程称为离散化。

一种简单的离散化算法是选择一个阈值,任何低于此阈值的值都被赋予值 0。同时,任何高于此阈值的值都被赋予值 1。对于我们的阈值,我们将计算该特征的平均值(平均值)。首先,我们计算每个特征的平均值:

attribute_means = X.mean(axis=0)

此代码的结果将是一个长度为 4 的数组,这是我们拥有的特征数量。第一个值是第一个特征的值的平均值,依此类推。接下来,我们使用这个结果将我们的数据集从具有连续特征的集合转换为具有离散分类特征的集合:

assert attribute_means.shape == (n_features,)
X_d = np.array(X >= attribute_means, dtype='int')

我们将使用这个新的 X_d 数据集(X 离散化)进行我们的 训练和测试,而不是原始数据集(X)。

实现 OneR 算法

OneR 是一个简单的算法,它通过找到特征值的最高频率类别来预测样本的类别。OneROne Rule 的缩写,表示我们只使用一个规则进行这种分类,通过选择表现最好的特征。虽然一些后续的算法要复杂得多,但这个简单的算法在现实世界的一些数据集中已被证明有良好的性能。

算法首先遍历每个特征的每个值。对于这个值,计算具有该特征值的每个类别的样本数量。记录特征值的最高频率类别和预测的错误。

例如,如果一个特征有两个值,01,我们首先检查所有具有值 0 的样本。对于这个值,我们可能在类别 A 中有 20 个,在类别 B 中有 60 个,以及在类别 C 中有进一步的 20 个。对于这个值最频繁的类别是 B,并且有 40 个实例具有不同的类别。对于这个特征值的预测是 B,错误率为 40,因为有 40 个样本与预测的类别不同。然后,我们对这个特征的值 1 执行相同的程序,然后对其他所有特征值组合执行。

一旦计算了这些组合,我们就通过累加该特征的值的错误来计算每个特征的错误。具有最低总错误的特征被选为 One Rule,然后用于分类其他实例。

在代码中,我们首先创建一个函数,用于计算特定特征值的类别预测和错误。我们有两个必要的导入,defaultdictitemgetter,我们在之前的代码中使用过:

from collections import defaultdict 
from operator import itemgetter

接下来,我们创建一个函数定义,该函数需要数据集、类别、我们感兴趣的特征的索引以及我们正在计算的值。它遍历每个样本,并计算每个特征值对应特定类别的次数。然后,我们选择当前特征/值对的最高频率类别:

def train_feature_value(X, y_true, feature, value):
# Create a simple dictionary to count how frequency they give certain
predictions
 class_counts = defaultdict(int)
# Iterate through each sample and count the frequency of each
class/value pair
 for sample, y in zip(X, y_true):
    if sample[feature] == value: 
        class_counts[y] += 1
# Now get the best one by sorting (highest first) and choosing the
first item
sorted_class_counts = sorted(class_counts.items(), key=itemgetter(1),
                             reverse=True)
most_frequent_class = sorted_class_counts[0][0]
 # The error is the number of samples that do not classify as the most
frequent class
 # *and* have the feature value.
    n_samples = X.shape[1]
    error = sum([class_count for class_value, class_count in
                 class_counts.items()
 if class_value != most_frequent_class])
    return most_frequent_class, error

作为最后一步,我们还计算了这个规则的错误。在 OneR 算法中,任何具有这个特征值的样本都会被预测为最频繁的类别。因此,我们通过累加其他类别的计数(不是最频繁的)来计算错误。这些代表导致错误或分类错误的训练样本。

使用这个函数,我们现在可以通过遍历该特征的值、汇总误差并记录每个值的预测类别来计算整个特征的误差。

该函数需要数据集、类别以及我们感兴趣的属性索引。然后它遍历不同的值,并找到用于此特定属性的、最准确的属性值,正如 OneR 规则:

def train(X, y_true, feature): 
    # Check that variable is a valid number 
    n_samples, n_features = X.shape 
    assert 0 <= feature < n_features 
    # Get all of the unique values that this variable has 
    values = set(X[:,feature]) 
    # Stores the predictors array that is returned 
    predictors = dict() 
    errors = [] 
    for current_value in values: 
        most_frequent_class, error = train_feature_value
        (X, y_true, feature, current_value) 
        predictors[current_value] = most_frequent_class 
        errors.append(error) 
    # Compute the total error of using this feature to classify on 
    total_error = sum(errors) 
    return predictors, total_error

让我们更详细地看看这个函数。

在一些初步测试之后,我们找到给定属性所具有的所有唯一值。下一行的索引查看给定属性的整个列,并将其作为数组返回。然后我们使用 set 函数来找到唯一的值:

    values = set(X[:,feature_index])

接下来,我们创建一个字典来存储预测值。这个字典将以属性值作为键,分类作为值。键为 1.5,值为 2 的条目意味着,当属性值设置为 1.5 时,将其分类为属于类别 2。我们还创建了一个列表来存储每个属性值的误差:

predictors = {} 
    errors = []

作为这个函数的主要部分,我们遍历这个特征的唯一值,并使用之前定义的 train_feature_value 函数来找到给定属性值的最大频率类别和误差。我们按照前面概述的方式存储结果:

最后,我们计算这个规则的总体误差,并返回预测值以及这个值:

total_error = sum(errors)
return predictors, total_error

测试算法

当我们评估前面章节中的亲和力分析算法时,我们的目标是探索当前数据集。与此分类不同,我们想要构建一个模型,通过将其与我们对该问题的了解进行比较,使我们能够对以前未见样本进行分类。

因此,我们将机器学习工作流程分为两个阶段:训练和测试。在训练阶段,我们取数据集的一部分来创建我们的模型。在测试阶段,我们应用这个模型并评估它在数据集上的有效性。由于我们的目标是创建一个可以分类以前未见样本的模型,我们不能使用测试数据来训练模型。如果我们这样做,我们就有可能发生过度拟合

过度拟合是创建一个模型,该模型在训练数据集上分类得很好,但在新样本上表现不佳的问题。解决方案很简单:永远不要使用训练数据来测试你的算法。这个简单规则有一些复杂的变体,我们将在后面的章节中介绍;但,现在,我们可以通过简单地分割我们的数据集为两个小数据集:一个用于训练,一个用于测试来评估我们的 OneR 实现。这个工作流程在本节中给出。

scikit-learn 库包含一个函数可以将数据分割成训练和测试组件:

from sklearn.cross_validation import train_test_split

此函数将数据集根据给定的比例(默认情况下使用数据集的 25% 用于测试)分成两个子数据集。它是随机进行的,这提高了算法在现实世界环境中按预期执行(我们期望数据来自随机分布)的置信度:

Xd_train, Xd_test, y_train, y_test = train_test_split(X_d, y, 
    random_state=14)

我们现在有两个较小的数据集:Xd_train 包含我们的训练数据,Xd_test 包含我们的测试数据。y_trainy_test 给出了这些数据集对应的类别值。

我们还指定了一个 random_state。设置随机状态将在每次输入相同值时给出相同的分割。它看起来是随机的,但使用的算法是确定性的,输出将是一致的。对于这本书,我建议将随机状态设置为与我相同的值,这样你将得到与我相同的结果,允许你验证你的结果。要获得每次运行都变化的真正随机结果,请将 random_state 设置为 None

接下来,我们计算数据集中所有特征的预测器。记住,只使用训练数据来完成这个过程。我们遍历数据集中的所有特征,并使用先前定义的函数来训练预测器和计算错误:

all_predictors = {} 
errors = {} 
for feature_index in range(Xd_train.shape[1]): 
    predictors, total_error = train(Xd_train,
                                    y_train,
                                    feature_index) 
    all_predictors[feature_index] = predictors 
    errors[feature_index] = total_error

接下来,我们通过找到具有最低错误的特征来找到用作我们的 One Rule 的最佳特征:

best_feature, best_error = sorted(errors.items(), key=itemgetter(1))[0]

然后,我们通过存储最佳特征的预测器来创建我们的 model

model = {'feature': best_feature,
         'predictor': all_predictors[best_feature]}

我们的模型是一个字典,它告诉我们应该使用哪个特征来进行我们的 One Rule 以及基于这些值的预测。有了这个模型,我们可以通过找到特定特征的值并使用适当的预测器来预测一个先前未见过的样本的类别。以下代码为给定样本执行此操作:

variable = model['feature'] 
predictor = model['predictor'] 
prediction = predictor[int(sample[variable])]

经常我们希望一次预测多个新样本,我们可以使用以下函数来完成。它只是简单地使用上面的代码,但遍历数据集中的所有样本,为每个样本获取预测:

def predict(X_test, model):
variable = model['feature']
predictor = model['predictor']
y_predicted = np.array([predictor
                        [int(sample[variable])] for sample
                        in X_test])
return y_predicted

对于我们的 testing 数据集,我们通过调用以下函数来获取预测:

y_predicted = predict(Xd_test, model)

然后,我们可以通过将其与已知类别进行比较来计算这个准确率:

accuracy = np.mean(y_predicted == y_test) * 100 
print("The test accuracy is {:.1f}%".format(accuracy))

此算法给出了 65.8% 的准确率,对于一个单一规则来说并不坏!

摘要

在本章中,我们介绍了使用 Python 进行数据挖掘。如果你能运行本节中的代码(注意,完整的代码包含在提供的代码包中),那么你的计算机已经为本书的大部分内容做好了设置。其他 Python 库将在后面的章节中介绍,以执行更专业的任务。

我们使用 Jupyter Notebook 运行我们的代码,这使得我们可以立即查看代码小段的结果。Jupyter Notebook 是一个有用的工具,将在整本书中使用。

我们介绍了一种简单的亲和分析,寻找一起购买的产品。这种类型的探索性分析可以深入了解业务流程、环境或场景。这些类型分析的信息可以帮助业务流程,找到下一个重大的医学突破,或者创造下一个人工智能。

此外,在本章中,我们使用OneR算法提供了一个简单的分类示例。这个简单的算法只是找到最佳特征,并预测在训练数据集中最频繁出现此值的类别。

为了扩展本章的成果,思考一下你将如何实现一个可以同时考虑多个特征/值对的OneR算法变体。尝试实现你的新算法并对其进行评估。记住,要在与训练数据不同的数据集上测试你的算法。否则,你可能会面临数据过拟合的风险。

在接下来的几章中,我们将扩展分类和亲和分析的概念。我们还将介绍 scikit-learn 包中的分类器,并使用它们来进行机器学习,而不是自己编写算法。

第二章:使用 scikit-learn 估计器进行分类

scikit-learn 库是一组数据挖掘算法的集合,用 Python 编写并使用。这个库允许用户轻松尝试不同的算法,以及利用标准工具进行有效的测试和参数搜索。scikit-learn 中包含许多算法和实用工具,包括现代机器学习中常用的许多算法。

在本章中,我们专注于设置一个良好的框架来运行数据挖掘过程。我们将在后续章节中使用这个框架,这些章节将专注于应用和那些情况下使用的技术。

本章介绍的关键概念如下:

  • 估计器: 这是为了执行分类、聚类和回归

  • 转换器: 这是为了执行预处理和数据修改

  • 管道: 这是为了将您的流程组合成一个可重复的格式

scikit-learn 估计器

估计器允许算法的标准化实现和测试,为分类器提供一个通用、轻量级的接口。通过使用此接口,我们可以将这些工具应用于任意分类器,而无需担心算法的工作方式。

估计器必须具有以下两个重要功能:

  • fit(): 此函数执行算法的训练 - 设置内部参数的值。fit()函数接受两个输入,即训练样本数据集和对应于这些样本的类别。

  • predict(): 这是测试样本的类别,我们将其作为唯一输入提供。此函数返回一个包含每个输入测试样本预测的NumPy数组。

大多数 scikit-learn 估计器使用NumPy数组或相关格式作为输入和输出。然而,这仅是一种惯例,并非必须使用该接口。

scikit-learn 中实现了许多估计器,在其他使用相同接口的开源项目中还有更多。我们将使用许多(SVM)、随机森林。我们将使用许多

在后续章节中介绍这些算法。在本章中,我们将使用最近邻算法。

算法。

对于本章,您需要安装一个名为matplotlib的新库。最简单的方法是使用pip3,就像在第一章“数据挖掘入门”中安装 scikit-learn 一样:

**pip3 install matplotlib**

如果您有matplotlib,请查找官方安装说明:

matplotlib 安装指南

最近邻

最近邻算法是我们新的样本。我们选取最相似的样本

并预测这些附近样本中大多数样本的相同类别。这种投票通常只是一个简单的计数,尽管也存在更复杂的方法,如加权投票。

以下图为例,我们希望根据三角形更接近哪个类别(在此处表示为相似对象更靠近)来预测三角形的类别。我们寻找最近的三个邻居,即画圈内的两个钻石和一个正方形。钻石比圆多,因此预测的三角形类别是钻石:

图片

最近邻算法几乎适用于任何数据集——然而,计算所有样本对之间的距离可能计算成本很高。例如,如果数据集中有十个样本,则需要计算 45 个独特的距离。然而,如果有 1000 个样本,则几乎有 500,000 个!存在各种方法来提高这种速度,例如使用树结构进行距离计算。其中一些算法可能相当复杂,但幸运的是,scikit-learn 已经实现了这些算法的版本,使我们能够在更大的数据集上进行分类。由于这些树结构是 scikit-learn 的默认设置,我们不需要进行任何配置即可使用它。

在基于类别的数据集、具有类别特征的情况下,最近邻算法表现不佳,应使用其他算法代替。最近邻算法的问题在于比较类别值差异的困难,这最好留给一个考虑每个特征重要性的算法。可以使用一些距离度量或预处理步骤(如我们在后续章节中使用的独热编码)来比较类别特征。选择正确的算法是数据挖掘中的难题之一,通常,测试一组算法并查看哪个在你的任务上表现最好是最简单的方法。

距离度量

数据挖掘中的一个基本概念是距离。如果我们有两个样本,我们需要回答诸如*这两个样本是否比另外两个样本更相似?*等问题。回答这些问题对于数据挖掘的结果非常重要。

最常用的距离是欧几里得距离,它是两个对象之间的实际世界距离。如果你要在图上绘制点并使用尺子测量距离,结果将是欧几里得距离。

更正式一点,点 a 和点 b 之间的欧几里得距离是每个特征平方距离之和的平方根。

欧几里得距离直观易懂,但如果某些特征值大于 0(称为稀疏矩阵),则准确性较差。

使用中的其他距离度量还有曼哈顿距离和余弦距离。

曼哈顿距离是每个特征绝对差异之和(不使用平方距离)。

直观地,我们可以将曼哈顿距离想象成车象棋子移动的步数。

(也称为城堡)在如果它被限制为每次移动一个方格的情况下。虽然当一些特征值大于其他特征时,曼哈顿距离确实会受到影响,但如果它被限制为每次移动一个方格,其影响不如欧几里得点那样显著。当一些特征值大于其他特征时,曼哈顿距离确实会受到影响,但其影响不如欧几里得距离那样剧烈。

余弦距离更适合某些特征值大于其他特征,并且数据集中有很多零的情况。

直观地,我们从原点到每个样本画一条线,并测量这些线之间的角度。我们可以在以下图中观察到算法之间的差异:

图片

在这个例子中,每个灰色圆圈与白色圆圈的距离完全相同。在(a)中,距离是欧几里得距离,因此,相似的距离适合围绕一个圆。这个距离可以用尺子来测量。在(b)中,距离是曼哈顿距离,也称为城市街区距离。我们通过跨越行和列来计算距离,就像国际象棋中的车(城堡)移动一样。最后,在(c)中,我们有余弦距离,它是通过计算从样本到向量的线之间的角度来测量的,并忽略线的实际长度。

所选的距离度量可以极大地影响最终性能。

例如,如果你有很多特征,随机样本之间的欧几里得距离会收敛(由于著名的维度诅咒)。在高维空间中,欧几里得距离很难比较样本,因为距离总是几乎相同!

在这种情况下,曼哈顿距离可能更稳定,但如果某些特征值非常大,这可能会掩盖其他特征中的许多相似性。例如,如果特征 A 的值在 1 到 2 之间,而另一个特征 B 的值在 1000 到 2000 之间,在这种情况下,特征 A 不太可能对结果有任何影响。这个问题可以通过归一化来解决,这使得曼哈顿(和欧几里得)距离在不同特征上更加可靠,我们将在本章后面看到。

最后,余弦距离是比较具有许多特征的项目的良好度量,但它丢弃了关于向量长度的某些信息,这在某些应用中是有用的。我们通常会在文本挖掘中使用余弦距离,因为文本挖掘固有的特征数量很大(见第六章,使用朴素贝叶斯进行社交媒体洞察)。

最终,需要一种理论方法来确定哪种距离方法需要,或者需要一种经验评估来查看哪种方法更有效。我更喜欢经验方法,但任何一种方法都可以产生良好的结果。

对于本章,我们将使用欧几里得距离,在后面的章节中使用其他度量标准。如果您想进行实验,请尝试将度量标准设置为曼哈顿距离,看看这对结果有何影响。

加载数据集

数据集 Ionosphere 与高频天线相关。天线的目的是确定电离层中是否存在结构以及上层大气中的区域。我们将具有结构的读取视为良好,而没有结构的读取则被视为不良。本应用的目的是构建一个数据挖掘分类器,以确定图像是良好还是不良。

(图片来源:www.flickr.com/photos/geck…

您可以下载此数据集用于不同的数据挖掘应用。访问 archive.ics.uci.edu/ml/datasets/Ionosphere 并点击数据文件夹。将 ionosphere.dataionosphere.names 文件下载到您的计算机上的一个文件夹中。对于本例,我将假设您已将数据集放在主文件夹中名为 Data 的目录下。您可以将数据放在另一个文件夹中,只需确保更新您的数据文件夹(此处以及所有其他章节)。

您的主文件夹位置取决于您的操作系统。对于 Windows,它通常位于 C:DocumentsSettingsusername。对于 Mac 或 Linux 计算机,它通常位于 /home/username。您可以通过在 Jupyter Notebook 中运行以下 Python 代码来获取您的家文件夹:

import os print(os.path.expanduser("~"))

对于数据集中的每一行,都有 35 个值。前 34 个是从 17 个天线(每个天线两个值)测量的。最后一个值是 'g' 或 'b';分别代表良好和不良。

启动 Jupyter Notebook 服务器并创建一个名为 Ionosphere Nearest Neighbors 的新笔记本。首先,我们加载所需的 NumPycsv 库,并设置我们代码中需要的数据文件名。

import numpy as np 
import csv 
data_filename = "data/ionosphere.data"

然后,我们创建 XyNumPy 数组来存储数据集。这些数组的大小来自数据集。如果您不知道未来数据集的大小,请不要担心——我们将在未来的章节中使用其他方法来加载数据集,您不需要事先知道这个大小:

X = np.zeros((351, 34), dtype='float') 
y = np.zeros((351,), dtype='bool')

该数据集是 逗号分隔值CSV)格式,这是数据集常用的格式。我们将使用 csv 模块来加载此文件。导入它并设置一个 csv 读取对象,然后遍历文件,为数据集中的每一行设置 X 中的适当行和 y 中的类别值:

with open(data_filename, 'r') as input_file: 
    reader = csv.reader(input_file) 
    for i, row in enumerate(reader): 
        # Get the data, converting each item to a float 
        data = [float(datum) for datum in row[:-1]] 
        # Set the appropriate row in our dataset 
        X[i] = data 
        # 1 if the class is 'g', 0 otherwise 
        y[i] = row[-1] == 'g'

我们现在有一个包含在 X 中的样本和特征数据集以及相应的 y 类别,正如我们在第一章入门数据挖掘中的分类示例中所做的那样。

首先,尝试将第一章中介绍的 OneR 算法应用于这个数据集。它不会很好用,因为这个数据集中的信息分布在某些特征的关联中。OneR 只对单个特征的值感兴趣,并且不能很好地捕捉更复杂数据集中的信息。其他算法,包括最近邻算法,合并多个特征的信息,使它们适用于更多场景。缺点是它们通常计算起来更昂贵。

向标准工作流程迈进

scikit-learn 的估计器有两个:fit()predict()。我们使用fit()方法来训练算法

在我们的测试集上使用predict()方法。我们使用测试集上的predict()方法来评估它。

  1. 首先,我们需要创建这些训练集和测试集。像以前一样,导入并运行train_test_split函数:
from sklearn.cross_validation import train_test_split 
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=14)

  1. 然后,我们导入nearest neighbor类并为其创建一个实例。现在我们将参数保留为默认值,将在本章后面测试其他值。默认情况下,算法将选择预测测试样本类别的五个最近邻:
from sklearn.neighbors import KNeighborsClassifier estimator = KNeighborsClassifier()

  1. 在创建我们的estimator之后,我们必须将其拟合到我们的训练数据集上。对于nearest neighbor类,这个训练步骤只是记录我们的数据集,使我们能够通过将新数据点与训练数据集进行比较来找到最近邻:
estimator.fit(X_train, y_train)

  1. 我们然后使用测试集训练算法,并使用测试集进行评估:
y_predicted = estimator.predict(X_test) 
accuracy = np.mean(y_test == y_predicted) * 100     
print("The accuracy is {0:.1f}%".format(accuracy))

此模型准确率达到 86.4%,对于一个默认算法来说非常出色,而且只需几行代码!大多数 scikit-learn 默认参数都是经过精心选择的,以便与各种数据集良好地工作。然而,你应该始终根据对应用实验的了解来选择参数。我们将在后面的章节中使用策略来进行这种参数搜索

运行算法

根据我们的测试数据集,之前的结果相当不错。然而,如果我们运气好,选择了一个容易的测试集会怎样?或者,如果它特别麻烦呢?我们可能会因为数据分割的不幸而丢弃一个好的模型。

交叉验证框架是一种解决选择单个测试集问题的方法,并且在数据挖掘中是一种标准的最佳实践方法论。这个过程通过进行许多实验,使用不同的训练和测试分割,但每个测试集中的每个样本只使用一次。程序如下:

  1. 将整个数据集分成几个称为折的部分。

  2. 对于数据集中的每个折,执行以下步骤:

    1. 将该折放在一边作为当前测试集

    2. 在剩余的折上训练算法

    3. 在当前测试集上评估

  3. 报告所有评估分数,包括平均分数。

在这个过程中,每个样本只用于测试集一次,这减少了(但并未消除)选择幸运测试集的可能性。

在整本书中,代码示例在每一章内相互构建。除非文本中另有说明,否则每个章节的代码应输入到同一个 Jupyter Notebook 中。

scikit-learn 库包含几种交叉验证方法。提供了一个执行先前过程的 helper 函数。我们现在可以在我们的 Jupyter Notebook 中导入它:

from sklearn.cross_validation import cross_val_score

通过 cross_val_score 使用一种称为 Stratified K-Fold 的特定方法来创建每个折叠中类比例大致相同的折叠,再次减少选择较差折叠的可能性。Stratified K-Fold 是一个很好的默认选项——我们现在不会去修改它。

接下来,我们使用这个新函数通过交叉验证评估我们的模型:

scores = cross_val_score(estimator, X, y, scoring='accuracy') 
average_accuracy = np.mean(scores) * 100 
print("The average accuracy is {0:.1f}%".format(average_accuracy))

我们的新代码返回了一个稍微谦虚的结果,82.3%,但考虑到我们还没有尝试设置更好的参数,这仍然相当不错。在下一节中,我们将看到如何改变参数以实现更好的结果。

在进行数据挖掘和尝试重复实验时,结果的变化是很自然的。这是由于折叠创建方式的不同以及某些分类算法中固有的随机性。我们可以故意选择通过设置随机状态(我们将在后面的章节中这样做)来精确复制一个实验。在实践中,多次重新运行实验以获得平均结果和所有实验结果(平均值和标准差)的分布(范围)是一个好主意。

设置参数

几乎所有用户可以设置的参数,让算法更多地关注特定的数据集,而不是只适用于一小部分特定的问题。设置这些参数可能相当困难,因为选择好的参数值通常高度依赖于数据集的特征。

最近邻算法有几个参数,但最重要的一个是在预测未见属性类别时使用的最近邻数量。在 -learn 中,这个参数称为 n_neighbors。在下面的图中,我们展示了当这个数字太低时,随机标记的样本可能会引起错误。相反,当它太高时,实际最近邻对结果的影响会降低:

图片

在图(a)的左侧,我们通常会期望将测试样本(三角形)分类为圆形。然而,如果n_neighbors为 1,这个区域中唯一的红色菱形(可能是噪声样本)会导致样本被预测为菱形。在图(b)的右侧,我们通常会期望将测试样本分类为菱形。但是,如果n_neighbors为 7,三个最近的邻居(它们都是菱形)被大量圆形样本所覆盖。最近邻是一个难以解决的问题,因为参数可以产生巨大的差异。幸运的是,大多数时候,具体的参数值不会对最终结果产生很大影响,标准值(通常是 5 或 10)通常足够接近

考虑到这一点,我们可以测试一系列的值,并调查这个参数对性能的影响。如果我们想测试n_neighbors参数的多个值,例如,从 1 到 20 的每个值,我们可以通过设置n_neighbors并观察结果来多次重新运行实验。下面的代码就是这样做的,将值存储在avg_scoresall_scores变量中。

avg_scores = [] 
all_scores = [] 
parameter_values = list(range(1, 21))  # Include 20 
for n_neighbors in parameter_values: 
    estimator = KNeighborsClassifier(n_neighbors=n_neighbors) 
    scores = cross_val_score(estimator, X, y, scoring='accuracy')     avg_scores.append(np.mean(scores))     
all_scores.append(scores)

我们可以绘制n_neighbors值与准确率之间的关系图。首先,我们告诉 Jupyter Notebook 我们希望在笔记本本身中显示inline图:

%matplotlib inline

然后,我们从matplotlib库中导入pyplot并绘制参数值和平均分数:

from matplotlib import pyplot as plt plt.plot(parameter_values,  avg_scores, '-o')

虽然有很大的变异性,但随着邻居数量的增加,图表显示了一个下降趋势。关于变异性,你可以预期在进行此类评估时会有大量的变异性。为了补偿,更新代码以运行 100 次测试,每次测试n_neighbors的每个值。

预处理

在对现实世界对象进行测量时,我们通常可以得到不同范围的特征。例如,如果我们测量动物的特性,我们可能会有几个特征,如下所示:

  • 腿的数量:对于大多数动物来说,这个范围在 0-8 之间,而有些动物更多!更多!更多!

  • 重量:这个范围只在几毫克到一只重达 190,000 千克的蓝鲸之间!

  • 心脏的数量:对于蚯蚓来说,这个范围在零到五之间。

对于基于数学的算法来比较这些特征,尺度、范围和单位之间的差异可能难以解释。如果我们使用上述特征在许多算法中,权重可能是最有影响力的特征,因为只有较大的数字,而与特征的真正有效性无关。

一种可能的策略标准化特征,使它们都具有相同的范围,或者将值转换为如这样的类别。突然之间,特征类型之间的巨大差异对算法的影响减小,可以导致准确率的大幅提高。

预处理还可以用来选择更有效的特征,创建新特征等。scikit-learn 中的预处理是通过Transformer对象完成的,这些对象接受一种形式的数据集,并在数据的一些转换后返回修改后的数据集。这些不必是数值型的,因为转换器也用于提取特征。然而,在本节中,我们将坚持使用预处理。

我们可以通过破坏Ionosphere数据集来展示这个问题的一个例子。虽然这只是一个例子,但许多现实世界的数据集都存在这种形式的问题。

  1. 首先,我们创建数组的副本,以确保我们不改变原始数据集:
X_broken = np.array(X)

  1. 接下来,我们通过将每个第二个特征除以10破坏数据集:
X_broken[:,::2] /= 10

理论上,这不应该对结果有太大影响。毕竟,这些特征的值仍然相对相同。主要问题是尺度发生了变化,奇数特征现在比偶数特征更大。我们可以通过计算准确率来看到这种影响:

estimator = KNeighborsClassifier() 
original_scores = cross_val_score(estimator, X, y,scoring='accuracy') 
print("The original average accuracy for is {0:.1f}%".format(np.mean(original_scores) * 100)) 
broken_scores = cross_val_score(estimator, X_broken, y,   scoring='accuracy') 
print("The 'broken' average accuracy for is   {0:.1f}%".format(np.mean(broken_scores) * 100))

这种测试方法给原始数据集评分为 82.3%,在破坏的数据集上降至 71.5%。我们可以通过将所有特征缩放到01的范围来解决这个问题。

标准预处理

我们将为这次实验执行的预处理称为基于特征的归一化,我们使用 scikit-learn 的MinMaxScaler类来完成。继续使用本章其余部分的 Jupyter Notebook,首先,我们导入这个类:

fromsklearn.preprocessing import MinMaxScaler

这个类将每个特征缩放到01的范围。这个预处理程序将最小值替换为0,最大值替换为1,其他值根据线性映射位于两者之间。

为了应用我们的预处理程序,我们在其上运行transform函数。转换器通常需要先进行训练,就像分类器一样。我们可以通过运行fit_transform函数来合并这些步骤:

X_transformed = MinMaxScaler().fit_transform(X)

在这里,X_transformed将与*X*具有相同的形状。然而,每一列的最大值将是1,最小值是0

以这种方式进行归一化有各种其他形式,这对于其他应用和特征类型是有效的:

  • 使用sklearn.preprocessing.Normalizer确保每个样本的值之和等于 1

  • 使用sklearn.preprocessing.StandardScaler将每个特征强制转换为具有零均值和方差为 1,这是归一化的常用起点

  • 使用sklearn.preprocessing.Binarizer将数值特征转换为二元特征,其中高于阈值的值为 1,低于阈值的值为 0

我们将在后续章节中使用这些预处理器的组合,以及其他类型的Transformers对象。

预处理是数据挖掘流程中的关键步骤,它可能意味着结果的好坏之分。

将所有这些放在一起

现在我们可以通过结合前几节中的代码,使用之前计算出的损坏数据集来创建一个工作流程:

X_transformed = MinMaxScaler().fit_transform(X_broken) 
estimator = KNeighborsClassifier() 
transformed_scores = cross_val_score(estimator, X_transformed, y,    scoring='accuracy') 
print("The average accuracy for is {0:.1f}%".format(np.mean(transformed_scores) * 100))

现在我们恢复了原来的准确率 82.3%,MinMaxScaler导致特征具有相同的尺度,这意味着没有特征仅仅因为值更大而压倒其他特征。虽然最近邻算法可能会被较大的特征所迷惑,但某些算法更好地处理尺度差异。相比之下,有些算法则差得多!

管道

随着实验的增多,操作的复杂性也在增加。我们可能需要分割我们的数据集,二值化特征,执行基于特征的缩放,执行基于样本的缩放,以及许多其他操作。

跟踪这些操作可能会变得相当混乱,并可能导致无法复制结果。问题包括忘记一个步骤,错误地应用转换,或者添加不必要的转换。

另一个问题是对代码的顺序。在前一节中,我们创建了我们的X_transformed数据集,然后为交叉验证创建了一个新的估计器。如果我们有多个步骤,我们就需要在代码中跟踪这些对数据集的更改。

管道(Pipelines)是一种解决这些问题的结构(以及我们将在下一章中看到的其他问题)。管道存储数据挖掘工作流程中的步骤。它们可以接收原始数据,执行所有必要的转换,然后创建预测。这使得我们可以在cross_val_score等函数中使用管道,这些函数期望一个估计器。首先,导入Pipeline对象:

fromsklearn.pipeline import Pipeline

管道接受一个步骤列表作为输入,表示数据挖掘应用的链。最后一个步骤需要是一个估计器,而所有之前的步骤都是转换器。输入数据集被每个转换器所改变,一个步骤的输出成为下一个步骤的输入。最后,我们通过最后一个步骤的估计器对样本进行分类。在我们的管道中,我们有两个步骤:

  1. 使用MinMaxScaler将特征值缩放到 0 到 1

  2. 使用KNeighborsClassifier作为分类算法

我们然后使用元组('name', step)`来表示每个步骤。然后我们可以创建我们的管道:

scaling_pipeline = Pipeline([('scale', MinMaxScaler()), 
                             ('predict', KNeighborsClassifier())])

关键在于元组的列表。第一个元组是我们的缩放步骤,第二个元组是预测步骤。我们给每个步骤起一个名字:第一个我们称之为scale,第二个我们称之为predict,但你可以选择自己的名字。元组的第二部分是实际的Transformerestimator对象。

现在运行这个管道非常简单,使用之前交叉验证的代码:

scores = cross_val_score(scaling_pipeline, X_broken, y, scoring='accuracy') 
print("The pipeline scored an average accuracy for is {0:.1f}%".format(np.mean(transformed_scores) * 100))

这给我们带来了与之前相同的分数(82.3%),这是预期的,因为我们正在运行完全相同的步骤,只是界面有所改进。

在后面的章节中,我们将使用更高级的测试方法,设置管道是确保代码复杂度不会无序增长的好方法。

摘要

在本章中,我们使用了 scikit-learn 的几种方法来构建一个标准的流程来运行和评估数据挖掘模型。我们介绍了最近邻算法,该算法在 scikit-learn 中作为估计器实现。使用这个类相当简单;首先,我们在训练数据上调用fit函数,然后使用predict函数来预测测试样本的类别。

然后,我们通过修复不良的特征缩放来查看预处理。这是通过Transformer对象和MinMaxScaler类来完成的。这些函数也有一个fit方法,然后是转换,它接受一种形式的数据作为输入,并返回一个转换后的数据集作为输出。

为了进一步研究这些转换,尝试用其他提到的转换器替换MinMaxScaler。哪个最有效,为什么会是这样?

scikit-learn 中还存在其他转换器,我们将在本书的后续部分使用,例如 PCA。也尝试一些这些转换器,参考 scikit-learn 的优秀文档scikit-learn.org/stable/modules/preprocessing.html

在下一章中,我们将使用这些概念在一个更大的例子中,使用现实世界的数据预测体育比赛的结果。

第三章:使用决策树预测体育比赛获胜者

在本章中,我们将探讨使用不同于我们之前所见类型的分类算法来预测体育比赛获胜者:决策树。这些算法相对于其他算法有许多优点。其中一个主要优点是它们可由人类阅读,这使得它们可以在人类驱动的决策中应用。通过这种方式,决策树可以用来学习一个程序,如果需要,可以将其交给人类执行。另一个优点是它们可以处理各种特征,包括分类特征,我们将在本章中看到。

本章将涵盖以下主题:

  • 使用 pandas 库加载数据和操作数据

  • 用于分类的决策树

  • 使用随机森林来改进决策树

  • 在数据挖掘中使用真实世界的数据集

  • 创建新特征并在稳健的框架中测试它们

加载数据集

在本章中,我们将探讨预测国家篮球协会NBA)比赛获胜者的问题。NBA 的比赛往往非常接近,有时在最后一刻才能分出胜负,这使得预测获胜者变得相当困难。许多运动都具备这种特征,即(通常)更好的队伍可能在某一天被另一支队伍击败。

对预测获胜者的各种研究表明,体育结果预测的准确性可能存在上限,这个上限取决于运动项目,通常在 70%到 80%之间。目前正在进行大量的体育预测研究,通常是通过数据挖掘或基于统计的方法进行的。

在本章中,我们将探讨一个入门级的篮球比赛预测算法,使用决策树来确定一支队伍是否会赢得某场比赛。不幸的是,它并不像体育博彩机构使用的模型那样盈利,这些模型通常更先进、更复杂,最终也更准确。

收集数据

我们将使用的是 2015-2016 赛季 NBA 的比赛历史数据。网站basketball-reference.com包含从 NBA 和其他联赛收集的大量资源和统计数据。要下载数据集,请执行以下步骤:

  1. 在您的网络浏览器中导航到www.basketball-reference.com/leagues/NBA_2016_games.html

  2. 点击“分享和更多”。

  3. 点击“获取表格为 CSV(适用于 Excel)”。

  4. 将包括标题在内的数据复制到名为basketball.csv的文本文件中。

  5. 对其他月份重复此过程,但不要复制标题。

这将为您提供包含本季 NBA 每场比赛结果的 CSV 文件。您的文件应包含 1316 场比赛和文件中的总行数 1317 行,包括标题行。

CSV 文件是文本文件,其中每行包含一个新行,每个值由逗号分隔(因此得名)。CSV 文件可以通过在文本编辑器中键入并保存为.csv扩展名来手动创建。它们可以在任何可以读取文本文件的程序中打开,也可以在 Excel 中以电子表格的形式打开。Excel(和其他电子表格程序)通常可以将电子表格转换为 CSV 格式。

我们将使用pandas库来加载文件,这是一个用于操作数据的极其有用的库。Python 还包含一个名为csv的内置库,它支持读取和写入 CSV 文件。然而,我们将使用 pandas,它提供了更强大的函数,我们将在本章后面创建新功能时使用。

对于本章,你需要安装 pandas。最简单的方法是使用 Anaconda 的conda安装程序,就像你在第一章“开始数据挖掘安装 scikit-learn”中所做的那样:

$ conda install pandas 如果你在安装 pandas 时遇到困难,请访问项目的网站pandas.pydata.org/getpandas.html,并阅读适用于您系统的安装说明。

使用 pandas 加载数据集

pandas库是一个用于加载数据、管理和操作数据的库。它在幕后处理数据结构,并支持数据分析函数,例如计算平均值和按值分组数据。

在进行多次数据挖掘实验时,你会发现你反复编写许多相同的函数,例如读取文件和提取特征。每次这种重新实现都会带来引入错误的风险。使用像pandas这样的高质量库可以显著减少执行这些函数所需的工作量,并使你更有信心使用经过良好测试的代码来支撑你的程序。

在整本书中,我们将大量使用 pandas,随着内容的展开介绍用例和所需的新函数。

我们可以使用read_csv函数来加载数据集:

import pandas as pd
data_filename = "basketball.csv"
dataset = pd.read_csv(data_filename)

这样做的结果是一个 pandas DataFrame,它有一些有用的函数,我们将在以后使用。查看生成的数据集,我们可以看到一些问题。输入以下内容并运行代码以查看数据集的前五行:

dataset.head(5)

这是输出:

图片

仅用参数读取数据就产生了一个相当可用的数据集,但它有一些问题,我们将在下一节中解决。

清理数据集

在查看输出后,我们可以看到许多问题:

  • 日期只是一个字符串,而不是日期对象

  • 从视觉检查结果来看,标题不完整或不正确

这些问题来自数据,我们可以通过改变数据本身来修复这些问题。然而,在这样做的时候,我们可能会忘记我们采取的步骤或错误地应用它们;也就是说,我们无法复制我们的结果。就像在前面一节中我们使用管道来跟踪我们对数据集所做的转换一样,我们将使用 pandas 对原始数据进行转换。

pandas.read_csv函数有参数可以修复这些问题,我们可以在加载文件时指定。我们还可以在加载文件后更改标题,如下面的代码所示:

dataset = pd.read_csv(data_filename, parse_dates=["Date"]) dataset.columns
        = ["Date", "Start (ET)", "Visitor Team", "VisitorPts", 
           "Home Team", "HomePts", "OT?", "Score Type", "Notes"]

结果显著提高,正如我们可以从打印出的结果数据框中看到:

dataset.head()

输出如下:

图片

即使在像这样精心编制的数据源中,你也需要做一些调整。不同的系统有不同的细微差别,导致数据文件之间并不完全兼容。在首次加载数据集时,始终检查加载的数据(即使它是已知的格式),并检查数据的数据类型。在 pandas 中,可以使用以下代码完成:

print(dataset.dtypes)

现在我们已经将数据集格式化为一致的形式,我们可以计算一个基线,这是一种在给定问题中获得良好准确率的好方法。任何合格的数据挖掘解决方案都应该击败这个基线数字。

对于产品推荐系统,一个好的基线是简单地推荐最受欢迎的产品

对于分类任务,可以是总是预测最频繁的任务,或者应用一个非常简单的分类算法,如OneR

对于我们的数据集,每场比赛有两支队伍:一支主队和一支客队。这个任务的明显基线是 50%,如果我们随机猜测获胜者,这是我们预期的准确率。换句话说,随机选择预测获胜的队伍(随着时间的推移)将导致大约 50%的准确率。然而,凭借一点领域知识,我们可以为这个任务使用更好的基线,我们将在下一节中看到。

提取新特征

现在,我们将通过组合和比较现有数据从该数据集中提取一些特征。首先,我们需要指定我们的类别值,这将给我们的分类算法提供一些比较的依据,以判断其预测是否正确。这可以通过多种方式编码;然而,对于这个应用,如果主队获胜,我们将指定类别为 1,如果客队获胜,则为 0。在篮球中,得分最高的队伍获胜。因此,尽管数据集没有直接指定谁获胜,我们仍然可以轻松地计算出结果。

我们可以通过以下方式指定数据集:

dataset["HomeWin"] = dataset["VisitorPts"] < dataset["HomePts"]

然后将这些值复制到一个 NumPy 数组中,以便稍后用于我们的 scikit-learn 分类器。目前 pandas 和 scikit-learn 之间没有干净的集成,但它们可以通过使用 NumPy 数组很好地一起工作。虽然我们将使用 pandas 提取特征,但我们需要提取值来与 scikit-learn 一起使用:

y_true = dataset["HomeWin"].values

前面的数组现在以 scikit-learn 可以读取的格式存储我们的类别值。

顺便说一句,体育预测更好的基线是预测每场比赛的主队。研究表明,主队在全球几乎所有体育项目中都有优势。这个优势有多大?让我们看看:

dataset["HomeWin"].mean()

最终得到的值,大约为 0.59,表明主队平均赢得 59%的比赛。这比随机机会的 50%要高,并且这是一条适用于大多数体育运动的简单规则。

我们还可以开始创建一些特征,用于我们的数据挖掘输入值(X数组)。虽然有时我们可以直接将原始数据扔进我们的分类器,但我们通常需要从我们的数据中推导出连续的数值或分类特征。

对于我们当前的数据库,我们实际上不能使用现有的特征(以它们当前的形式)来进行预测。在我们需要预测比赛结果之前,我们不知道比赛的分数,因此我们不能将它们用作特征。虽然这听起来可能很明显,但很容易忽略。

我们想要创建的前两个特征,以帮助我们预测哪支队伍会赢,是这两个队伍中的任何一个是否赢得了上一场比赛。这大致可以近似哪支队伍目前表现良好。

我们将通过按顺序遍历行并记录哪支队伍获胜来计算这个特征。当我们到达新行时,我们查看该队伍上次我们看到他们时是否获胜。

我们首先创建一个(默认)字典来存储球队的最后一次结果:

from collections import defaultdict 
won_last = defaultdict(int)

然后,我们在数据集上创建一个新的特征来存储我们新特征的成果:

dataset["HomeLastWin"] = 0
dataset["VisitorLastWin"] = 0

这个字典的键将是球队,值将是他们是否赢得了上一场比赛。然后我们可以遍历所有行,并更新当前行的球队最后结果:

for index, row in dataset.iterrows():
    home_team = row["Home Team"]
    visitor_team = row["Visitor Team"]
    row["HomeLastWin"] = won_last[home_team]
    dataset.set_value(index, "HomeLastWin", won_last[home_team])
    dataset.set_value(index, "VisitorLastWin", won_last[visitor_team])
    won_last[home_team] = int(row["HomeWin"])
    won_last[visitor_team] = 1 - int(row["HomeWin"])

注意,前面的代码依赖于我们的数据集是按时间顺序排列的。我们的数据集是有序的;然而,如果你使用的数据集不是按顺序排列的,你需要将dataset.iterrows()替换为dataset.sort("Date").iterrows()

循环中的最后两行根据哪支队伍赢得了当前比赛,将 1 或 0 更新到我们的字典中。这些信息被用于下一场每支队伍所打的比赛。

在前面的代码运行之后,我们将有两个新的特征:HomeLastWinVisitorLastWin。使用dataset.head(6)查看数据集,以了解一支主队和一支客队最近赢得比赛的例子。使用 pandas 的索引器查看数据集的其他部分:

dataset.ix[1000:1005]

目前,当它们首次出现时,这会给所有团队(包括上一年的冠军!)一个错误值。我们可以使用上一年的数据来改进这个功能,但在这个章节中我们不会这么做。

决策树

决策树是一类监督学习算法,类似于流程图,由一系列节点组成,其中样本的值用于在下一个节点上进行决策。

以下示例很好地说明了决策树是如何成为一类监督学习算法的:

图片

与大多数分类算法一样,使用它们有两个阶段:

  • 第一个阶段是训练阶段,在这个阶段,使用训练数据构建一个树。虽然上一章中的最近邻算法没有训练阶段,但决策树需要这个阶段。这样,最近邻算法是一个懒惰的学习者,只有在需要做出预测时才会进行任何工作。相比之下,决策树,像大多数分类方法一样,是积极的学习者,在训练阶段进行工作,因此在预测阶段需要做的工作更少。

  • 第二个阶段是预测阶段,在这个阶段,使用训练好的树来预测新样本的分类。使用之前的示例树,数据点["is raining", "very windy"]会被归类为恶劣天气

存在许多创建决策树的算法。其中许多算法是迭代的。它们从基本节点开始,决定第一个决策的最佳特征,然后转到每个节点并选择下一个最佳特征,依此类推。当决定进一步扩展树无法获得更多收益时,这个过程会在某个点上停止。

scikit-learn包实现了分类和回归树CART)算法,作为其默认的决策树类,它可以使用分类和连续特征。

决策树中的参数

对于决策树来说,最重要的参数之一是停止标准。当树构建接近完成时,最后的几个决策往往可能是相当随意的,并且只依赖于少量样本来做出决策。使用这样的特定节点可能导致树显著过度拟合训练数据。相反,可以使用停止标准来确保决策树不会达到这种精确度。

而不是使用停止标准,树可以完全创建,然后进行修剪。这个过程会移除对整体过程提供信息不多的节点。这被称为剪枝,结果是一个在新数据集上通常表现更好的模型,因为它没有过度拟合训练数据。

scikit-learn 中的决策树实现提供了一个方法,使用以下选项来停止树的构建:

  • **min_samples_split**:这指定了在决策树中创建新节点所需的样本数量

  • **min_samples_leaf**:这指定了节点必须产生的样本数量,以便它保持不变

第一个决定了是否创建决策节点,而第二个决定了是否保留决策节点。

决策树另一个参数是创建决策的标准。基尼不纯度信息增益是这个参数的两个流行选项:

  • 基尼不纯度:这是衡量决策节点错误预测样本类别的频率的度量

  • 信息增益:这使用基于信息论熵来指示决策节点通过决策获得的额外信息量

这些参数值大致做相同的事情——决定使用哪个规则和值来将节点分割成子节点。这个值本身只是确定分割时使用哪个指标,然而这可能会对最终模型产生重大影响。

使用决策树

我们可以导入DecisionTreeClassifier类,并使用 scikit-learn 创建决策树:

from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(random_state=14)

我们再次使用了 14 作为random_state,并在本书的大部分内容中都会这样做。使用相同的随机种子允许实验的可重复性。然而,在你的实验中,你应该混合随机状态以确保算法的性能不依赖于特定值。

我们现在需要从我们的 pandas 数据框中提取数据集,以便与我们的scikit-learn分类器一起使用。我们通过指定我们希望使用的列并使用数据框视图的值参数来完成此操作。以下代码使用主队和客队最后一场胜利的值创建了一个数据集:

X_previouswins = dataset[["HomeLastWin", "VisitorLastWin"]].values

决策树是估计量,如第二章中介绍的,*使用scikit-learn 估计量进行分类,因此有fitpredict方法。我们还可以使用cross_val_score方法来获取平均分数(如我们之前所做的那样):

from sklearn.cross_validation import cross_val_score
import numpy as np
scores = cross_val_score(clf, X_previouswins, y_true,
scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))

这个得分是 59.4%,我们比随机选择要好!然而,我们并没有打败仅仅选择主队的基线。事实上,我们几乎完全一样。我们应该能够做得更好。特征工程是数据挖掘中最困难的任务之一,选择好的特征是获得良好结果的关键——比选择正确的算法更重要!

体育结果预测

我们可以通过尝试其他特征来做得更好。我们有一种测试模型准确性的方法。cross_val_score方法允许我们尝试新的特征。

我们可以使用许多可能的特征,但我们将尝试以下问题:

  • 通常哪个队被认为是更好的?

  • 哪个队赢得了他们上次相遇?

我们还将尝试将原始队伍放入算法中,以检查算法是否可以学习一个模型,该模型检查不同队伍之间的比赛。

整合所有内容

对于第一个特征,我们将创建一个特征,告诉我们主队是否通常比客队更好。为此,我们将从上一个赛季的 NBA 中加载排名(在某些运动中也称为排行榜)。如果一个队在 2015 年的排名高于另一个队,则该队将被认为是更好的。

要获取排行榜数据,请执行以下步骤:

  1. 在您的网络浏览器中导航到 www.basketball-reference.com/leagues/NBA_2015_standings.html

  2. 选择扩展排名以获取整个联盟的单个列表。

  3. 点击导出链接。

  4. 将文本复制并保存在您数据文件夹中的名为 standings.csv 的文本/CSV 文件中。

回到您的 Jupyter Notebook 中,将以下行输入到新的单元格中。您需要确保文件已保存到由 data_folder 变量指向的位置。代码如下:

import os
standings_filename = os.path.join(data_folder, "standings.csv")
standings = pd.read_csv(standings_filename, skiprows=1)

您只需在新的单元格中输入“standings”并运行,就可以查看排行榜。

代码:

standings.head()

输出如下:

图片

接下来,我们使用与之前特征相似的图案创建一个新的特征。我们遍历行,查找主队和客队的排名。代码如下:

dataset["HomeTeamRanksHigher"] = 0
for index, row in dataset.iterrows():
    home_team = row["Home Team"]
    visitor_team = row["Visitor Team"]
    home_rank = standings[standings["Team"] == home_team]["Rk"].values[0]
    visitor_rank = standings[standings["Team"] == visitor_team]["Rk"].values[0]
    row["HomeTeamRanksHigher"] = int(home_rank > visitor_rank)
    dataset.set_value(index, "HomeTeamRanksHigher", int(home_rank < visitor_rank))

接下来,我们使用 cross_val_score 函数测试结果。首先,我们提取数据集:

X_homehigher = dataset[["HomeLastWin", "VisitorLastWin", "HomeTeamRanksHigher"]].values

然后,我们创建一个新的 DecisionTreeClassifier 并运行评估:

clf = DecisionTreeClassifier(random_state=14)
scores = cross_val_score(clf, X_homehigher, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))

现在这个得分是 60.9%,甚至比我们之前的结果更好,现在比每次只选择主队更好。我们能做得更好吗?

接下来,让我们测试两支球队中哪一支在最近一场比赛中赢得了对方。虽然排名可以给出一些关于谁赢的线索(排名更高的球队更有可能赢),但有时球队对其他球队的表现更好。这有很多原因——例如,一些球队可能有针对特定球队的策略或球员表现非常好。遵循我们之前的模式,我们创建一个字典来存储过去比赛的胜者,并在我们的数据框中创建一个新的特征。代码如下:

last_match_winner = defaultdict(int)
dataset["HomeTeamWonLast"] = 0

for index, row in dataset.iterrows():
    home_team = row["Home Team"]
    visitor_team = row["Visitor Team"]
    teams = tuple(sorted([home_team, visitor_team])) # Sort for a consistent ordering
    # Set in the row, who won the last encounter
    home_team_won_last = 1 if last_match_winner[teams] == row["Home Team"] else 0
    dataset.set_value(index, "HomeTeamWonLast", home_team_won_last)
    # Who won this one?
    winner = row["Home Team"] if row["HomeWin"] else row["Visitor Team"]
    last_match_winner[teams] = winner

这个特征与我们的上一个基于排名的特征非常相似。然而,不是查找排名,这个特征创建了一个名为 teams 的元组,并将之前的结果存储在字典中。当这两支球队再次比赛时,它将重新创建这个元组,并查找之前的结果。我们的代码没有区分主场比赛和客场比赛,这可能是一个有用的改进,值得考虑实施。

接下来,我们需要进行评估。这个过程与之前非常相似,只是我们添加了新的特征到提取的值中:

X_lastwinner = dataset[[ "HomeTeamWonLast", "HomeTeamRanksHigher", "HomeLastWin", "VisitorLastWin",]].values
clf = DecisionTreeClassifier(random_state=14, criterion="entropy")

scores = cross_val_score(clf, X_lastwinner, y_true, scoring='accuracy')

print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))

这个得分是 62.2%。我们的结果越来越好。

最后,我们将检查如果我们向决策树投入大量数据会发生什么,并看看它是否仍然可以学习到一个有效的模型。我们将把球队输入到树中,并检查决策树是否可以学习到包含这些信息。

虽然决策树能够从分类特征中学习,但scikit-learn中的实现要求这些特征被编码为数字和特征,而不是字符串值。我们可以使用LabelEncoder 转换器将基于字符串的球队名称转换为分配的整数值。代码如下:

from sklearn.preprocessing import LabelEncoder
encoding = LabelEncoder()
encoding.fit(dataset["Home Team"].values)
home_teams = encoding.transform(dataset["Home Team"].values)
visitor_teams = encoding.transform(dataset["Visitor Team"].values)
X_teams = np.vstack([home_teams, visitor_teams]).T

我们应该使用相同的转换器来编码主队和客队。这样,同一个球队作为主队和客队都会得到相同的整数值。虽然这对这个应用程序的性能不是至关重要,但它很重要,而且不做这件事可能会降低未来模型的表现。

这些整数可以输入到决策树中,但它们仍然会被DecisionTreeClassifier解释为连续特征。例如,球队可能被分配从 0 到 16 的整数。算法会将 1 号和 2 号球队视为相似,而 4 号和 10 号球队则非常不同——但这在所有方面都没有意义!所有的球队都是不同的——两个球队要么相同,要么不同!

为了解决这个问题的不一致性,我们使用OneHotEncoder 转换器将这些整数编码成一系列二进制特征。每个二进制特征将代表一个特征的单个值。例如,如果 NBA 球队芝加哥公牛被LabelEncoder分配为整数 7,那么OneHotEncoder返回的第七个特征将为 1,如果球队是芝加哥公牛,而对于所有其他特征/球队则为 0。这是对每个可能的值都这样做,结果得到一个更大的数据集。代码如下:

from sklearn.preprocessing import OneHotEncoder
onehot = OneHotEncoder()
X_teams = onehot.fit_transform(X_teams).todense()

接下来,我们像以前一样在新数据集上运行决策树:

clf = DecisionTreeClassifier(random_state=14)
scores = cross_val_score(clf, X_teams, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))

这得到了 62.8%的准确率。尽管提供的信息仅仅是参赛球队,但这个分数仍然更好。可能是因为更多的特征没有被决策树正确处理。因此,我们将尝试更改算法,看看是否有所帮助。数据挖掘可能是一个尝试新算法和特征的过程。

随机森林

单个决策树可以学习相当复杂的函数。然而,决策树容易过拟合——学习只适用于特定训练集的规则,并且对新数据泛化不好。

我们可以调整的一种方法是限制它学习的规则数量。例如,我们可以将树的深度限制为只有三层。这样的树将在全局层面上学习分割数据集的最佳规则,但不会学习将数据集分割成高度准确组的特定规则。这种权衡导致的结果是,树可能具有良好的泛化能力,但在训练数据集上的整体性能略差。

为了补偿这一点,我们可以创建许多这些有限的决策树,并要求每个树预测类值。我们可以进行多数投票,并使用那个答案作为我们的整体预测。随机森林就是从这个洞察力发展而来的算法。

上述程序有两个问题。第一个问题是构建决策树在很大程度上是确定的——使用相同的输入每次都会得到相同的结果。我们只有一个训练数据集,这意味着如果我们尝试构建多个树,我们的输入(以及因此的输出)将会相同。我们可以通过选择数据集的随机子样本来解决这个问题,从而有效地创建新的训练集。这个过程被称为袋装法,在数据挖掘的许多情况下可以非常有效。

第二个问题是我们可能会遇到的是,从类似数据创建许多决策树时,用于树中前几个决策节点的特征往往会相似。即使我们选择训练数据的随机子样本,仍然很可能构建的决策树在很大程度上是相同的。为了补偿这一点,我们还选择一个特征的随机子集来执行我们的数据拆分。

然后,我们使用随机选择的样本和(几乎)随机选择的特征来构建随机树。这是一个随机森林,也许不太直观,但这个算法对于许多数据集来说非常有效,几乎不需要调整模型的许多参数。

集成是如何工作的?

随机森林固有的随机性可能会让人感觉我们是在把算法的结果留给运气。然而,我们通过将平均化的好处应用于几乎随机构建的决策树,从而得到一个减少结果方差的算法。

方差是指训练数据集的变化对算法引入的错误。具有高方差(如决策树)的算法会受到训练数据集变化的很大影响。这导致模型存在过度拟合的问题。相比之下,偏差是指算法中的假设引入的错误,而不是与数据集有关的事情,也就是说,如果我们有一个假设所有特征都呈正态分布的算法,那么如果特征不是正态分布的,我们的算法可能会有很高的错误率。

通过分析数据以查看分类器的数据模型是否与实际数据相匹配,可以减少偏差带来的负面影响。

用一个极端的例子来说,一个总是预测为真的分类器,不管输入如何,都有很高的偏差。一个总是随机预测的分类器会有很高的方差。每个分类器都有很高的错误率,但性质不同。

通过平均大量决策树,这种方差大大降低。这至少在正常情况下会导致模型具有更高的整体准确性和更好的预测能力。权衡是时间增加和算法偏差的增加。

通常,集成方法基于预测误差是有效随机的,并且这些误差在各个分类器之间相当不同。通过在许多模型之间平均结果,这些随机误差被抵消,留下真正的预测。我们将在本书的其余部分看到更多集成方法的实际应用。

在随机森林中设置参数

scikit-learn 中的随机森林实现称为RandomForestClassifier,它有许多参数。由于随机森林使用许多DecisionTreeClassifier的实例,它们共享许多相同的参数,例如criterion(基尼不纯度或熵/信息增益)、max_featuresmin_samples_split

在集成过程中使用了一些新的参数:

  • n_estimators:这决定了应该构建多少个决策树。更高的值将运行时间更长,但(可能)会导致更高的准确性。

  • oob_score:如果为真,则使用不在为训练决策树选择的随机子样本中的样本进行方法测试。

  • n_jobs:这指定了在并行训练决策树时使用的核心数。

scikit-learn包使用名为Joblib的库来实现内置的并行化。此参数决定了要使用多少核心。默认情况下,只使用单个核心--如果你有更多核心,你可以增加这个值,或者将其设置为-1 以使用所有核心。

应用随机森林

scikit-learn 中的随机森林使用Estimator接口,允许我们使用几乎与之前完全相同的代码来进行交叉验证:

from sklearn.ensemble import RandomForestClassifier
clf = RandomForestClassifier(random_state=14)
scores = cross_val_score(clf, X_teams, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))

这通过仅仅交换分类器就带来了 65.3%的即时收益,提高了 2.5 个百分点。

随机森林,通过使用特征子集,应该能够比普通决策树更有效地学习,并且具有更多特征。我们可以通过向算法投入更多特征来测试这一点,看看效果如何:

X_all = np.hstack([X_lastwinner, X_teams])
clf = RandomForestClassifier(random_state=14)
scores = cross_val_score(clf, X_all, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))

结果是 63.3%,性能下降!一个原因是随机森林固有的随机性,它只选择了一些特征来使用,而不是其他特征。此外,X_teams中的特征比X_lastwinner中的特征要多得多,额外的特征导致使用了更不相关的信息。尽管如此,也不要对百分比的小幅变化过于兴奋,无论是上升还是下降。改变随机状态值对准确性的影响将大于我们刚刚观察到的这些特征集之间微小的差异。相反,你应该运行许多具有不同随机状态的测试,以获得准确度值的平均值和分布的良好感觉。

我们还可以尝试使用GridSearchCV类尝试一些其他参数,正如我们在第二章中介绍的,“使用scikit-learn 估计器进行分类”:

from sklearn.grid_search import GridSearchCV

parameter_space = {
 "max_features": [2, 10, 'auto'],
 "n_estimators": [100, 200],
 "criterion": ["gini", "entropy"],
 "min_samples_leaf": [2, 4, 6],
}

clf = RandomForestClassifier(random_state=14)
grid = GridSearchCV(clf, parameter_space)
grid.fit(X_all, y_true)
print("Accuracy: {0:.1f}%".format(grid.best_score_ * 100))

这使得准确率达到了 67.4%,非常好!

如果我们想查看使用的参数,我们可以打印出网格搜索中找到的最佳模型。代码如下:

print(grid.best_estimator_)

结果显示了最佳得分模型中使用的参数:

RandomForestClassifier(bootstrap=True, class_weight=None, criterion='entropy',
            max_depth=None, max_features=2, max_leaf_nodes=None,
            min_samples_leaf=2, min_samples_split=2,
            min_weight_fraction_leaf=0.0, n_estimators=100, n_jobs=1,
            oob_score=False, random_state=14, verbose=0, warm_start=False)

新特征工程

在之前的几个例子中,我们看到改变特征可以对算法的性能产生相当大的影响。通过我们的小量测试,我们仅从特征中就获得了超过 10%的方差。

您可以通过这样做来创建来自 pandas 中简单函数的特征:

dataset["New Feature"] = feature_creator()

feature_creator函数必须返回数据集中每个样本的特征值列表。一种常见的模式是使用数据集作为参数:

dataset["New Feature"] = feature_creator(dataset)

您可以通过将所有值设置为单个默认值(如下一行中的 0)来更直接地创建这些特征:

dataset["My New Feature"] = 0

然后,您可以遍历数据集,在遍历过程中计算特征。我们使用了

本章中我们使用这种格式创建了许多特征:

for index, row in dataset.iterrows():
    home_team = row["Home Team"]
    visitor_team = row["Visitor Team"]
    # Some calculation here to alter row
    dataset.set_value(index, "FeatureName", feature_value)

请记住,这种模式并不非常高效。如果您要这样做,请一次尝试所有特征。

一种常见的最佳实践是尽可能少地触摸每个样本,最好是只触摸一次。

您可以尝试实现的一些示例特征如下:

  • 每支球队上次比赛以来有多少天了?如果他们在短时间内打了很多比赛,球队可能会感到疲劳。

  • 在过去五场比赛中,每支球队赢了多少场比赛?这将为我们之前提取的HomeLastWinVisitorLastWin特征提供更稳定的形式(并且可以以非常相似的方式提取)。

  • 当球队访问某些其他球队时,他们的记录好吗?例如,一支球队可能在某个特定的体育场表现良好,即使他们是客队。

如果您在提取这些类型的特征时遇到麻烦,请查阅pandas 文档以获取帮助。或者,您也可以尝试在线论坛,如 Stack Overflow 寻求帮助。

更极端的例子可以使用球员数据来估计每支球队的实力,以预测谁会获胜。这些类型的复杂特征每天都在赌徒和体育博彩机构中使用,以预测体育比赛结果并试图从中获利。

摘要

在本章中,我们扩展了 scikit-learn 分类器的使用,以执行分类,并引入了pandas库来管理我们的数据。我们分析了 NBA 篮球比赛结果的真实世界数据,看到了即使是精心整理的数据也会引入的一些问题,并为我们的分析创建了新的特征。

我们看到了良好特征对性能的影响,并使用集成算法,随机森林,来进一步提高准确度。为了将这些概念进一步深化,尝试创建你自己的特征并对其进行测试。哪些特征表现更好?如果你在构思特征方面遇到困难,考虑一下可以包含哪些其他数据集。例如,如果关键球员受伤,这可能会影响特定比赛的结果,导致一支更强的队伍输掉比赛。

在下一章中,我们将扩展我们在第一章中进行的亲和力分析,以创建一个寻找相似书籍的程序。我们将看到如何使用排序算法,并使用近似方法来提高数据挖掘的可扩展性。

第四章:使用亲和力分析推荐电影

在本章中,我们将探讨亲和力分析,该分析用于确定对象何时频繁地一起出现。这通常也被称为市场篮子分析,因为这是一种常见的用例——确定在商店中频繁一起购买的商品。

在第三章*,使用决策树预测体育比赛胜者*中,我们将对象作为焦点,并使用特征来描述该对象。在本章中,数据具有不同的形式。我们有交易,其中感兴趣的物体(在本章中为电影)以某种方式被用于这些交易中。目标是发现对象何时同时出现。如果我们想找出两部电影是否被同一评论家推荐,我们可以使用亲和力分析。

本章的关键概念如下:

  • 亲和力分析用于产品推荐

  • 使用 Apriori 算法进行特征关联挖掘

  • 推荐系统和固有的挑战

  • 稀疏数据格式及其使用方法

亲和力分析

亲和力分析是确定对象以相似方式使用的任务。在前一章中,我们关注的是对象本身是否相似——在我们的案例中是游戏在本质上是否相似。亲和力分析的数据通常以交易的形式描述。直观地说,这来自商店的交易——通过确定对象何时一起购买,作为向用户推荐他们可能购买的产品的方式。

然而,亲和力分析可以应用于许多不使用这种意义上的交易的流程:

  • 欺诈检测

  • 客户细分

  • 软件优化

  • 产品推荐

亲和力分析通常比分类更具探索性。至少,我们通常只是简单地排名结果并选择前五项推荐(或某个其他数字),而不是期望算法给出一个特定的答案。

此外,我们通常没有我们期望的完整数据集来完成许多分类任务。例如,在电影推荐中,我们有不同人对不同电影的评论。然而,我们几乎不可能让每个评论家都评论我们数据集中的所有电影。这给亲和力分析留下了一个重要且困难的问题。如果一个评论家没有评论一部电影,这是否表明他们不感兴趣(因此不会推荐)或者只是他们还没有评论?

思考数据集中的差距可以导致这样的问题。反过来,这可能导致有助于提高你方法有效性的答案。作为一个初露头角的数据挖掘者,知道你的模型和方法需要改进的地方是创造出色结果的关键。

亲和力分析算法

在第一章*《数据挖掘入门》*中,我们介绍了一种基本的关联分析方法,它测试了所有可能的规则组合。我们计算了每个规则的置信度和支持度,这反过来又允许我们根据规则进行排序,以找到最佳规则。

然而,这种方法并不高效。我们在第一章*《数据挖掘入门》中的数据集只有五个销售项目。我们可以预期即使是小型商店也会有数百个销售项目,而许多在线商店会有数千(甚至数百万!)项目。使用我们之前在第一章《数据挖掘入门》中提到的简单规则创建方法,这些规则计算所需的时间会呈指数增长。随着我们添加更多项目,计算所有规则所需的时间增长得更快。具体来说,可能的总规则数是2n - 1*。对于五个项目的数据集,有 31 个可能的规则。对于十个项目,这个数字是 1023。对于仅仅 100 个项目,这个数字有 30 位。即使计算能力的急剧增加也无法跟上在线存储项目数量的增长。因此,我们需要更智能的算法,而不是更努力工作的计算机。

关联分析的经典算法被称为Apriori 算法。它解决了在数据库中创建频繁项集(称为频繁项集)的指数级问题。一旦发现这些频繁项集,创建关联规则就变得简单,我们将在本章后面看到这一点。

Apriori 背后的直觉既简单又巧妙。首先,我们确保规则在数据集中有足够的支持度。定义最小支持度是 Apriori 的关键参数。为了构建频繁项集,我们结合较小的频繁项集。对于项集(A,B)要有至少 30%的支持度,A 和 B 必须在数据库中至少出现 30 次。这一属性也适用于更大的集合。对于一个项集(A,B,C,D)要被认为是频繁的,集合(A,B,C)也必须是频繁的(同样,D 也必须是频繁的)。

这些频繁项集可以构建,而不频繁的可能项集(其中有很多)将永远不会被测试。这在新规则测试中节省了大量的时间,因为频繁项集的数量预计将远少于可能项集的总数。

其他关联分析的示例算法基于这个或类似的概念,包括EclatFP-growth算法。数据挖掘文献中有许多对这些算法的改进,进一步提高了方法的效率。在本章中,我们将重点关注基本的 Apriori 算法。

总体方法

为了进行关联规则挖掘以进行亲和力分析,我们首先使用 Apriori 算法生成频繁项集。接下来,我们通过测试那些频繁项集中前提和结论的组合来创建关联规则(例如,如果一个人推荐了电影 X,他们也会推荐电影 Y)。

  1. 在第一阶段,Apriori 算法需要一个值来表示项集需要达到的最小支持度,才能被认为是频繁的。任何支持度低于这个值的项集都不会被考虑。

将这个最小支持度设置得太低会导致 Apriori 测试更多的项集,从而减慢算法的速度。设置得太高会导致考虑的频繁项集更少。

  1. 在第二阶段,在频繁项集被发现之后,基于它们的置信度来测试关联规则。我们可以选择一个最小的置信度水平,返回的规则数量,或者简单地返回所有规则并让用户决定如何处理它们。

在本章中,我们只返回高于给定置信度水平的规则。因此,我们需要设置我们的最小置信度水平。设置得太低会导致具有高支持度但不太准确的规则。设置得更高将导致只返回更准确的规则,但总体上发现的规则更少。

处理电影推荐问题

产品推荐是一个庞大的产业。在线商店通过推荐其他可能购买的产品来向上销售给客户。做出更好的推荐可以带来更好的销售业绩。当在线购物每年向数百万客户销售时,通过向这些客户销售更多商品,就有大量的潜在利润可赚。

产品推荐,包括电影和书籍,已经研究了许多年;然而,当 Netflix 在 2007 年至 2009 年期间举办 Netflix Prize 时,该领域得到了显著的发展。这次比赛旨在确定是否有人能比 Netflix 目前所做的更好预测用户的电影评分。奖项授予了一个团队,他们的表现比当前解决方案高出 10%以上。虽然这种改进可能看起来并不大,但这样的改进将为 Netflix 在接下来的几年中带来数百万美元的收益,因为更好的电影推荐。

获取数据集

自从 Netflix Prize 启动以来,明尼苏达大学的 Grouplens 研究小组已经发布了几个常用于测试该领域算法的数据集。他们发布了多个电影评分数据集的不同版本,大小不同。有一个版本有 10 万条评论,一个版本有 100 万条评论,还有一个版本有 1000 万条评论。

数据集可以从grouplens.org/datasets/movielens/获取,本章我们将使用的是MovieLens 100K 数据集(包含 10 万条评论)。下载此数据集并将其解压到您的数据文件夹中。启动一个新的 Jupyter Notebook,并输入以下代码:

import os
import pandas as pd
data_folder = os.path.join(os.path.expanduser("~"), "Data", "ml-100k")
ratings_filename = os.path.join(data_folder, "u.data")

确保变量ratings_filename指向解压文件夹中的 u.data 文件。

使用 pandas 加载

MovieLens数据集状况良好;然而,与pandas.read_csv的默认选项相比,我们需要做一些更改。首先,数据是以制表符分隔的,而不是逗号。其次,没有标题行。这意味着文件中的第一行实际上是数据,我们需要手动设置列名。

在加载文件时,我们将分隔符参数设置为制表符,告诉 pandas 不要将第一行作为标题读取(使用header=None),并使用给定的值设置列名。让我们看看以下代码:

all_ratings = pd.read_csv(ratings_filename, delimiter="t", header=None, names
            = ["UserID", "MovieID", "Rating", "Datetime"])

虽然我们本章不会使用它,但您可以使用以下行正确解析日期时间戳。评论的日期对于推荐预测可能是一个重要特征,因为一起评分的电影通常比单独评分的电影有更相似的排名。考虑到这一点可以显著提高模型的效果。

all_ratings["Datetime"] = pd.to_datetime(all_ratings['Datetime'], unit='s')

您可以通过在新的单元格中运行以下代码来查看前几条记录:

all_ratings.head()

结果将类似于以下内容:

UserIDMovieIDRatingDatetime
019624231997-12-04 15:55:49
118630231998-04-04 19:22:22
22237711997-11-07 07:18:36
32445121997-11-27 05:02:03
416634611998-02-02 05:33:16

稀疏数据格式

此数据集是稀疏格式。每一行可以被视为之前章节中使用的大型特征矩阵中的一个单元格,其中行是用户,列是单独的电影。第一列将是每个用户对第一部电影的评论,第二列将是每个用户对第二部电影的评论,依此类推。

该数据集中大约有 1,000 个用户和 1,700 部电影,这意味着完整的矩阵会相当大(近 200 万条记录)。我们可能会遇到在内存中存储整个矩阵的问题,并且对其进行计算会相当麻烦。然而,这个矩阵具有大多数单元格为空的性质,也就是说,大多数用户对大多数电影没有评论。尽管如此,用户编号 213 对电影编号 675 的评论不存在,以及其他大多数用户和电影的组合也是如此。

这里给出的格式代表完整的矩阵,但以更紧凑的方式呈现。第一行表示用户编号 196 在 1997 年 12 月 4 日对电影编号 242 进行了评分,评分为 3(满分五分)。

任何不在数据库中的用户和电影的组合都被假定为不存在。这节省了大量的空间,与在内存中存储一串零相比。这种格式称为稀疏矩阵格式。一般来说,如果你预计你的数据集中有 60%或更多的数据为空或为零,稀疏格式将占用更少的空间来存储。

在稀疏矩阵上进行计算时,我们通常不会关注我们没有的数据——比较所有的零。我们通常关注我们有的数据,并比较这些数据。

理解 Apriori 算法及其实现

本章的目标是产生以下形式的规则:如果一个人推荐了这组电影,他们也会推荐这部电影。我们还将讨论扩展,其中推荐一组电影的人可能会推荐另一部特定的电影。

要做到这一点,我们首先需要确定一个人是否推荐了一部电影。我们可以通过创建一个新的特征“赞同”,如果该人对电影给出了好评,则为 True:

all_ratings["Favorable"] = all_ratings["Rating"] > 3

我们可以通过查看数据集来查看新功能:

all_ratings[10:15]

用户 ID电影 ID评分日期时间赞同
106225721997-11-12 22:07:14False
11286101451997-11-17 15:38:45True
1220022251997-10-05 09:05:40True
132104031998-03-27 21:59:54False
142242931998-02-21 23:40:57False

我们将采样我们的数据集以形成训练数据。这也帮助减少了要搜索的数据集的大小,使 Apriori 算法运行得更快。我们获取了前 200 个用户的所有评论:

ratings = all_ratings[all_ratings['UserID'].isin(range(200))]

接下来,我们可以创建一个只包含样本中好评的评论文本的数据集:

favorable_ratings_mask = ratings["Favorable"]
favorable_ratings = ratings[favorable_ratings_mask]

我们将在用户的好评中搜索我们的项集。因此,我们接下来需要的是每个用户给出的好评电影。我们可以通过按UserID对数据集进行分组并遍历每个组中的电影来计算这一点:

favorable_reviews_by_users = dict((k, frozenset(v.values)) for k, v in favorable_ratings.groupby("UserID")["MovieID"])

在前面的代码中,我们将值存储为frozenset,这样我们可以快速检查用户是否对电影进行了评分。

对于这种类型的操作,集合比列表快得多,我们将在后面的代码中使用它们。

最后,我们可以创建一个DataFrame,告诉我们每部电影被好评的频率:

num_favorable_by_movie = ratings[["MovieID", "Favorable"]].groupby("MovieID").sum()

通过运行以下代码,我们可以看到前五部电影:

num_favorable_by_movie.sort_values(by="Favorable", ascending=False).head()

让我们看看前五部电影列表。我们现在只有 ID,将在本章后面获取它们的标题。

电影 ID赞同
50100
10089
25883
18179
17474

探索 Apriori 算法的基本原理

Apriori 算法是我们亲和力分析方法的一部分,专门处理在数据中寻找频繁项集的问题。Apriori 的基本程序是从先前发现的频繁项集中构建新的候选项集。这些候选集被测试以查看它们是否频繁,然后算法按以下方式迭代:

  1. 通过将每个项目放置在其自己的项目集中来创建初始频繁项目集。在此步骤中仅使用至少具有最小支持度的项目。

  2. 从最近发现的频繁项目集中创建新的候选项目集,通过找到现有频繁项目集的超集。

  3. 所有候选项目集都会被测试以确定它们是否频繁。如果一个候选项目集不是频繁的,则将其丢弃。如果没有从这个步骤中产生新的频繁项目集,则转到最后一步。

  4. 存储新发现的频繁项目集并转到第二步。

  5. 返回所有发现的频繁项目集。

此过程在以下工作流程中概述:

实现 Apriori 算法

在 Apriori 的第一轮迭代中,新发现的项集长度将为 2,因为它们将是第一步中创建的初始项集的超集。在第二轮迭代(应用第四步并返回到第二步之后),新发现的项集长度将为 3。这使我们能够快速识别新发现的项集,正如在第二步中所需的那样。

我们可以在字典中存储发现频繁项目集,其中键是项目集的长度。这允许我们快速访问给定长度的项目集,以及通过以下代码帮助快速访问最近发现的频繁项目集:

frequent_itemsets = {}

我们还需要定义一个项目集被认为是频繁所需的最小支持度。此值基于数据集选择,但尝试不同的值以查看它如何影响结果。尽管如此,我建议每次只改变 10%,因为算法运行所需的时间将显著不同!让我们设置一个最小支持度值:

min_support = 50

要实现 Apriori 算法的第一步,我们为每部电影单独创建一个项目集,并测试该项目集是否频繁。我们使用frozenset**,**因为它们允许我们在稍后执行更快的基于集合的操作,并且它们还可以用作计数字典中的键(普通集合不能)。

让我们看看以下frozenset代码的示例:

frequent_itemsets[1] = dict((frozenset((movie_id,)), row["Favorable"])
 for movie_id, row in num_favorable_by_movie.iterrows()
 if row["Favorable"] > min_support)

为了提高效率,我们将第二步和第三步一起实现,通过创建一个函数来执行这些步骤,该函数接受新发现的频繁项目集,创建超集,然后测试它们是否频繁。首先,我们设置函数以执行这些步骤:

from collections import defaultdict

def find_frequent_itemsets(favorable_reviews_by_users, k_1_itemsets, min_support):
    counts = defaultdict(int)
    for user, reviews in favorable_reviews_by_users.items():
        for itemset in k_1_itemsets:
            if itemset.issubset(reviews):
                for other_reviewed_movie in reviews - itemset:
                    current_superset = itemset | frozenset((other_reviewed_movie,))
                    counts[current_superset] += 1
    return dict([(itemset, frequency) for itemset, frequency in counts.items() if frequency >= min_support])

为了遵循我们尽可能少读取数据的经验法则,我们每次调用此函数时只遍历数据集一次。虽然在这个实现中这不是很重要(与平均计算机相比,我们的数据集相对较小),单次遍历是对于更大应用的良好实践。

让我们详细看看这个函数的核心。我们遍历每个用户,以及之前发现的每个项集,然后检查它是否是当前存储在k_1_itemsets中的评论集的子集(注意,这里的 k_1 意味着k-1)。如果是,这意味着用户已经评论了项集中的每部电影。这是通过itemset.issubset(reviews)这一行完成的。

然后,我们可以遍历用户评论的每部单独的电影(那些尚未在项集中),通过将项集与新电影结合来创建超集,并在我们的计数字典中记录我们看到了这个超集。这些都是这个k值的候选频繁项集。

我们通过测试候选项集是否有足够的支持被认为是频繁的来结束我们的函数,并只返回那些支持超过我们的min_support值的项集。

这个函数构成了我们 Apriori 实现的核心,我们现在创建一个循环,遍历更大算法的步骤,随着k从 1 增加到最大值,存储新的项集。在这个循环中,k 代表即将发现的频繁项集的长度,允许我们通过在我们的频繁项集字典中使用键k-1来访问之前发现的最频繁的项集。我们通过它们的长度创建频繁项集并将它们存储在我们的字典中。让我们看看代码:

for k in range(2, 20):
    # Generate candidates of length k, using the frequent itemsets of length k-1
    # Only store the frequent itemsets
    cur_frequent_itemsets = find_frequent_itemsets(favorable_reviews_by_users,
                                                   frequent_itemsets[k-1], min_support)
    if len(cur_frequent_itemsets) == 0:
        print("Did not find any frequent itemsets of length {}".format(k))
        sys.stdout.flush()
        break
    else:
        print("I found {} frequent itemsets of length {}".format(len(cur_frequent_itemsets), k))
        sys.stdout.flush()
        frequent_itemsets[k] = cur_frequent_itemsets

如果我们找到了频繁项集,我们打印一条消息来让我们知道循环将再次运行。如果没有,我们停止迭代,因为没有频繁项集对于k+1,如果当前k值没有频繁项集,因此我们完成算法。

我们使用sys.stdout.flush()来确保打印输出在代码仍在运行时发生。有时,特别是在某些单元格的大循环中,打印输出可能直到代码完成才发生。以这种方式刷新输出确保打印输出在我们想要的时候发生,而不是当界面决定可以分配时间打印的时候。但是,不要过于频繁地刷新——刷新操作(以及正常的打印)都会带来计算成本,这会减慢程序的速度。

你现在可以运行上述代码。

上述代码返回了大约 2000 个不同长度的频繁项集。你会注意到,随着长度的增加,项集的数量先增加后减少。这是因为可能规则的数目在增加。过了一段时间,大量组合不再有必要的支持被认为是频繁的。这导致数量减少。这种减少是 Apriori 算法的优点。如果我们搜索所有可能的项集(而不仅仅是频繁项集的超集),我们将需要搜索成千上万的项集来查看它们是否频繁。

即使这种缩小没有发生,当发现所有电影的组合规则时,算法将达到绝对结束。因此,Apriori 算法将始终终止。

运行此代码可能需要几分钟,如果你有较旧的硬件,可能需要更长的时间。如果你发现运行任何代码示例有困难,可以考虑使用在线云服务提供商以获得额外的速度。有关使用云进行工作的详细信息,请参阅附录,下一步。

提取关联规则

Apriori 算法完成后,我们将有一个频繁项集的列表。这些不是精确的关联规则,但它们可以很容易地转换为这些规则。频繁项集是一组具有最小支持度的项,而关联规则有一个前提和结论。这两个的数据是相同的。

我们可以通过将项集中的一部电影作为结论,并将其他电影作为前提来从频繁项集中创建一个关联规则。这将形成以下形式的规则:如果一个评论家推荐了前提中的所有电影,他们也会推荐结论电影

对于每个项集,我们可以通过将每部电影设置为结论,将剩余的电影作为前提来生成多个关联规则。

在代码中,我们首先通过遍历每个长度的发现频繁项集,从每个频繁项集中生成所有规则的列表。然后,我们遍历项集中的每一部电影作为结论。

candidate_rules = []
for itemset_length, itemset_counts in frequent_itemsets.items():
    for itemset in itemset_counts.keys():
        for conclusion in itemset:
            premise = itemset - set((conclusion,))
            candidate_rules.append((premise, conclusion))

这将返回一个非常大的候选规则数。我们可以通过打印列表中的前几条规则来查看一些:

print(candidate_rules[:5])

生成的输出显示了获得的规则:

[(frozenset({79}), 258), (frozenset({258}), 79), (frozenset({50}), 64), (frozenset({64}), 50), (frozenset({127}), 181)]

在这些规则中,第一部分(frozenset)是前提中的电影列表,而它后面的数字是结论。在第一种情况下,如果一个评论家推荐了电影 79,他们也很可能推荐电影 258。

接下来,我们计算这些规则中每个规则的置信度。这与第一章*《数据挖掘入门》*中的操作非常相似,唯一的区别是那些必要的更改,以便使用新的数据格式进行计算。

计算置信度的过程首先是通过创建字典来存储我们看到前提导致结论(规则的正确示例)和它没有发生(规则的错误示例)的次数。然后,我们遍历所有评论和规则,确定规则的前提是否适用,如果适用,结论是否准确。

correct_counts = defaultdict(int)
incorrect_counts = defaultdict(int)
for user, reviews in favorable_reviews_by_users.items():
    for candidate_rule in candidate_rules:
        premise, conclusion = candidate_rule
        if premise.issubset(reviews):
            if conclusion in reviews:
                correct_counts[candidate_rule] += 1
            else:
                incorrect_counts[candidate_rule] += 1

然后,我们通过将正确计数除以规则被看到的总次数来计算每个规则的置信度:

rule_confidence = {candidate_rule:
                    (correct_counts[candidate_rule] / float(correct_counts[candidate_rule] +  
                      incorrect_counts[candidate_rule]))
                  for candidate_rule in candidate_rules}

现在,我们可以通过排序这个置信度字典并打印结果来打印前五条规则:

from operator import itemgetter
sorted_confidence = sorted(rule_confidence.items(), key=itemgetter(1), reverse=True)
for index in range(5):
    print("Rule #{0}".format(index + 1))
    premise, conclusion = sorted_confidence[index][0]
    print("Rule: If a person recommends {0} they will also recommend {1}".format(premise, conclusion))
    print(" - Confidence: {0:.3f}".format(rule_confidence[(premise, conclusion)]))
    print("")

生成的打印输出只显示电影 ID,没有电影名称的帮助并不太有用。数据集附带一个名为 u.items 的文件,该文件存储电影名称及其对应的 MovieID(以及其他信息,如类型)。

我们可以使用 pandas 从这个文件中加载标题。有关文件和类别的更多信息可在随数据集提供的 README 文件中找到。文件中的数据是 CSV 格式,但数据由|符号分隔;它没有标题

并且编码设置很重要。列名在 README 文件中找到。

movie_name_filename = os.path.join(data_folder, "u.item")
movie_name_data = pd.read_csv(movie_name_filename, delimiter="|", header=None,
                              encoding = "mac-roman")
movie_name_data.columns = ["MovieID", "Title", "Release Date", "Video Release", "IMDB", "<UNK>",
                           "Action", "Adventure", "Animation", "Children's", "Comedy", "Crime",
                           "Documentary", "Drama", "Fantasy", "Film-Noir", "Horror", "Musical",   
                           "Mystery", "Romance", "Sci-Fi", "Thriller", "War", "Western"]

获取电影标题是一个重要且常用的步骤,因此将其转换为函数是有意义的。我们将创建一个函数,该函数将从其 MovieID 返回电影标题,从而避免每次都查找的麻烦。让我们看看代码:

def get_movie_name(movie_id):
    title_object = movie_name_data[movie_name_data["MovieID"] == movie_id]["Title"]
    title = title_object.values[0]
    return title

在一个新的 Jupyter Notebook 单元中,我们调整了之前用于打印最佳规则的代码,以包括标题:

for index in range(5):
    print("Rule #{0}".format(index + 1))
    premise, conclusion = sorted_confidence[index][0]
    premise_names = ", ".join(get_movie_name(idx) for idx in premise)
    conclusion_name = get_movie_name(conclusion)
    print("Rule: If a person recommends {0} they will also recommend {1}".format(premise_names, conclusion_name))
    print(" - Confidence: {0:.3f}".format(rule_confidence[(premise, conclusion)]))
    print("")

结果更易于阅读(仍然有一些问题,但现在我们可以忽略它们):

Rule #1
Rule: If a person recommends Shawshank Redemption, The (1994), Silence of the Lambs, The (1991), Pulp Fiction (1994), Star Wars (1977), Twelve Monkeys (1995) they will also recommend Raiders of the Lost Ark (1981)
 - Confidence: 1.000

Rule #2
Rule: If a person recommends Silence of the Lambs, The (1991), Fargo (1996), Empire Strikes Back, The (1980), Fugitive, The (1993), Star Wars (1977), Pulp Fiction (1994) they will also recommend Twelve Monkeys (1995)
 - Confidence: 1.000

Rule #3
Rule: If a person recommends Silence of the Lambs, The (1991), Empire Strikes Back, The (1980), Return of the Jedi (1983), Raiders of the Lost Ark (1981), Twelve Monkeys (1995) they will also recommend Star Wars (1977)
 - Confidence: 1.000

Rule #4
Rule: If a person recommends Shawshank Redemption, The (1994), Silence of the Lambs, The (1991), Fargo (1996), Twelve Monkeys (1995), Empire Strikes Back, The (1980), Star Wars (1977) they will also recommend Raiders of the Lost Ark (1981)
 - Confidence: 1.000

Rule #5
Rule: If a person recommends Shawshank Redemption, The (1994), Toy Story (1995), Twelve Monkeys (1995), Empire Strikes Back, The (1980), Fugitive, The (1993), Star Wars (1977) they will also recommend Return of the Jedi (1983)
 - Confidence: 1.000

评估关联规则

在广义上,我们可以使用与分类相同的概念来评估关联规则。我们使用未用于训练的数据测试集,并根据它们在这个测试集中的性能来评估我们发现的规则。

要做到这一点,我们将计算测试集置信度,即每个规则在测试集中的置信度。在这种情况下,我们不会应用正式的评估指标;我们只是检查规则并寻找好的例子。

正式评估可能包括通过确定用户是否对给定电影给予好评的预测准确性来进行分类准确率。在这种情况下,如下所述,我们将非正式地查看规则以找到那些更可靠的规则:

  1. 首先,我们提取测试数据集,这是我们未在训练集中使用的所有记录。我们使用了前 200 个用户(按 ID 值)作为训练集,我们将使用其余所有用户作为测试数据集。与训练集一样,我们还将获取该数据集中每个用户的正面评论。让我们看看代码:
test_dataset = all_ratings[~all_ratings['UserID'].isin(range(200))]
test_favorable = test_dataset[test_dataset["Favorable"]]
test_favorable_by_users = dict((k, frozenset(v.values)) for k, v in 
                               test_favorable.groupby("UserID")["MovieID"])

  1. 然后,我们计算前提导致结论的正确实例数,就像我们之前做的那样。这里唯一的变化是使用测试数据而不是训练数据。让我们看看代码:
correct_counts = defaultdict(int)
incorrect_counts = defaultdict(int)
for user, reviews in test_favorable_by_users.items():
    for candidate_rule in candidate_rules:
        premise, conclusion = candidate_rule
        if premise.issubset(reviews):
            if conclusion in reviews:
                correct_counts[candidate_rule] += 1
            else:
                incorrect_counts[candidate_rule] += 1

  1. 接下来,我们计算每个规则的置信度,并按此排序。让我们看看代码:
test_confidence = {candidate_rule:
                             (correct_counts[candidate_rule] / float(correct_counts[candidate_rule] + incorrect_counts[candidate_rule]))
                             for candidate_rule in rule_confidence}
sorted_test_confidence = sorted(test_confidence.items(), key=itemgetter(1), reverse=True)

  1. 最后,我们以标题而不是电影 ID 的形式打印出最佳关联规则:
for index in range(10):
    print("Rule #{0}".format(index + 1))
    premise, conclusion = sorted_confidence[index][0]
    premise_names = ", ".join(get_movie_name(idx) for idx in premise)
    conclusion_name = get_movie_name(conclusion)
    print("Rule: If a person recommends {0} they will also recommend {1}".format(premise_names, conclusion_name))
    print(" - Train Confidence: {0:.3f}".format(rule_confidence.get((premise, conclusion), -1)))
    print(" - Test Confidence: {0:.3f}".format(test_confidence.get((premise, conclusion), -1)))
    print("")

现在,我们可以看到哪些规则在新的未见数据中最为适用:

Rule #1
Rule: If a person recommends Shawshank Redemption, The (1994), Silence of the Lambs, The (1991), Pulp Fiction (1994), Star Wars (1977), Twelve Monkeys (1995) they will also recommend Raiders of the Lost Ark (1981)
 - Train Confidence: 1.000
 - Test Confidence: 0.909

Rule #2
Rule: If a person recommends Silence of the Lambs, The (1991), Fargo (1996), Empire Strikes Back, The (1980), Fugitive, The (1993), Star Wars (1977), Pulp Fiction (1994) they will also recommend Twelve Monkeys (1995)
 - Train Confidence: 1.000
 - Test Confidence: 0.609

Rule #3
Rule: If a person recommends Silence of the Lambs, The (1991), Empire Strikes Back, The (1980), Return of the Jedi (1983), Raiders of the Lost Ark (1981), Twelve Monkeys (1995) they will also recommend Star Wars (1977)
 - Train Confidence: 1.000
 - Test Confidence: 0.946

Rule #4
Rule: If a person recommends Shawshank Redemption, The (1994), Silence of the Lambs, The (1991), Fargo (1996), Twelve Monkeys (1995), Empire Strikes Back, The (1980), Star Wars (1977) they will also recommend Raiders of the Lost Ark (1981)
 - Train Confidence: 1.000
 - Test Confidence: 0.971

Rule #5
Rule: If a person recommends Shawshank Redemption, The (1994), Toy Story (1995), Twelve Monkeys (1995), Empire Strikes Back, The (1980), Fugitive, The (1993), Star Wars (1977) they will also recommend Return of the Jedi (1983)
 - Train Confidence: 1.000
 - Test Confidence: 0.900

例如,第二个规则在训练数据中具有完美的置信度,但在测试数据中只有 60%的案例是准确的。前 10 条规则中的许多其他规则在测试数据中具有高置信度,这使得它们成为制定推荐的好规则。

你可能还会注意到,这些电影往往非常受欢迎且是优秀的电影。这为我们提供了一个基准算法,我们可以将其与之比较,即不是尝试进行个性化推荐,而是推荐最受欢迎的电影。尝试实现这个算法——Apriori 算法是否优于它,以及优于多少?另一个基准可能是简单地从同一类型中随机推荐电影。

如果你正在查看其余的规则,其中一些将具有-1 的测试置信度。置信值总是在 0 和 1 之间。这个值表示特定的规则根本未在测试数据集中找到。

摘要

在本章中,我们进行了亲和力分析,以便根据大量评论者推荐电影。我们分两个阶段进行。首先,我们使用 Apriori 算法在数据中找到频繁项集。然后,我们从这些项集中创建关联规则。

由于数据集的大小,使用 Apriori 算法是必要的。在第一章*,数据挖掘入门*中,我们使用了暴力方法,这种方法在计算那些用于更智能方法的规则所需的时间上呈指数增长。这是数据挖掘中的一种常见模式:对于小数据集,我们可以以暴力方式解决许多问题,但对于大数据集,则需要更智能的算法来应用这些概念。

我们在我们的数据的一个子集上进行了训练,以找到关联规则,然后在这些规则的其余数据——测试集上进行了测试。根据我们之前章节的讨论,我们可以将这个概念扩展到使用交叉验证来更好地评估规则。这将导致对每个规则质量的更稳健的评估。

为了进一步探讨本章的概念,研究哪些电影获得了很高的总体评分(即有很多推荐),但没有足够的规则来向新用户推荐它们。你将如何修改算法来推荐这些电影?

到目前为止,我们所有的数据集都是用特征来描述的。然而,并非所有数据集都是以这种方式预先定义的。在下一章中,我们将探讨 scikit-learn 的转换器(它们在第三章,使用决策树预测体育比赛赢家中介绍过)作为从数据中提取特征的方法。我们将讨论如何实现我们自己的转换器,扩展现有的转换器,以及我们可以使用它们实现的概念。

第五章:特征和 scikit-learn 转换器

我们迄今为止所使用的数据集都是以特征的形式描述的。在前一章中,我们使用了一个以事务为中心的数据集。然而,这最终只是以不同格式表示基于特征的数据的另一种方式。

还有许多其他类型的数据集,包括文本、图像、声音、电影,甚至是真实物体。大多数数据挖掘算法都依赖于具有数值或分类特征。这意味着在我们将这些类型输入数据挖掘算法之前,我们需要一种方法来表示它们。我们称这种表示为模型

在本章中,我们将讨论如何提取数值和分类特征,并在我们有这些特征时选择最佳特征。我们将讨论一些常见的特征提取模式和技巧。适当地选择你的模型对于数据挖掘练习的结果至关重要,比分类算法的选择更为重要。

本章介绍的关键概念包括:

  • 从数据集中提取特征

  • 为你的数据创建模型

  • 创建新特征

  • 选择好的特征

  • 为自定义数据集创建自己的转换器

特征提取

提取特征是数据挖掘中最关键的任务之一,它通常比数据挖掘算法的选择对最终结果的影响更大。不幸的是,没有一成不变的规则来选择能够导致高性能数据挖掘的特征。特征的选择决定了你用来表示数据的模型。

模型创建是数据挖掘科学变得更加像艺术的地方,这也是为什么执行数据挖掘的自动化方法(有几种此类方法)专注于算法选择而不是模型创建。创建好的模型依赖于直觉、领域专业知识、数据挖掘经验、试错,有时还需要一点运气。

在模型中呈现现实

基于我们在本书中迄今为止所做的工作,很容易忘记我们进行数据挖掘的原因是影响现实世界中的对象,而不仅仅是操作一个值矩阵。并非所有数据集都是以特征的形式呈现的。有时,一个数据集可能仅仅是由某个作者所写的所有书籍组成。有时,它可能是 1979 年发布的每部电影的影片。在其他时候,它可能是一个有趣的历史文物的图书馆收藏。

从这些数据集中,我们可能想要执行数据挖掘任务。对于书籍,我们可能想知道作者写了哪些不同的类别。在电影中,我们可能希望看到女性是如何被描绘的。在历史文物中,我们可能想知道它们是来自一个国家还是另一个国家。仅仅将这些原始数据集输入决策树并查看结果是不可能的。

为了让数据挖掘算法在这里帮助我们,我们需要将这些数据表示为特征。特征是创建模型的一种方式,而模型以数据挖掘算法能够理解的方式提供对现实的一种近似。因此,模型只是现实世界某个方面的简化版本。例如,象棋游戏就是历史战争的一种简化模型(以游戏形式)。

选择特征还有另一个优点:它们将现实世界的复杂性简化为更易于管理的模型。

想象一下,要向一个对物品没有任何背景知识的人准确、全面地描述一个现实世界对象需要多少信息。你需要描述其大小、重量、质地、成分、年龄、缺陷、用途、起源等等。

由于现实对象的复杂性超出了当前算法的处理能力,我们使用这些更简单的模型来代替。

这种简化也使我们在数据挖掘应用中的意图更加集中。在后面的章节中,我们将探讨聚类及其至关重要的应用。如果你输入随机特征,你将得到随机的结果。

然而,这种简化也有缺点,因为它减少了细节,或者可能移除了我们希望进行数据挖掘的某些事物的良好指标。

我们应该始终思考如何以模型的形式表示现实。而不仅仅是使用过去使用过的方法,你需要考虑数据挖掘活动的目标。你试图实现什么?在第三章《使用决策树预测体育比赛胜者》中,我们通过思考目标(预测胜者)并使用一些领域知识来提出新特征的想法来创建特征。

并非所有特征都需要是数值或分类的。已经开发出可以直接在文本、图和其他数据结构上工作的算法。不幸的是,这些算法超出了本书的范围。在本书中,以及在你的数据挖掘生涯中,我们主要使用数值或分类特征。

Adult数据集是使用特征来尝试对复杂现实进行建模的一个很好的例子。在这个数据集中,目标是估计某人每年是否赚超过$50,000。

要下载数据集,请导航到archive.ics.uci.edu/ml/datasets/Adult并点击数据文件夹链接。将adult.dataadult.names下载到你的数据文件夹中名为 Adult 的目录下。

这个数据集将一个复杂任务描述为特征。这些特征描述了个人、他们的环境、他们的背景以及他们的生活状况。

为本章打开一个新的 Jupyter Notebook,设置数据文件名,并使用 pandas 加载数据:

import os
import pandas as pd
data_folder = os.path.join(os.path.expanduser("~"), "Data", "Adult")
adult_filename = os.path.join(data_folder, "adult.data")

adult = pd.read_csv(adult_filename, header=None, names=["Age", "Work-Class", "fnlwgt", 
                     "Education", "Education-Num", "Marital-Status", "Occupation",
                     "Relationship", "Race", "Sex", "Capital-gain", "Capital-loss",
                     "Hours-per-week", "Native-Country", "Earnings-Raw"])

大部分代码与前面的章节相同。

不想输入那些标题名称?别忘了你可以从 Packt Publishing 下载代码,或者从本书作者的 GitHub 仓库下载:

github.com/dataPipelineAU/LearningDataMiningWithPython2

成年文件本身在文件末尾包含两个空白行。默认情况下,pandas 将倒数第二个换行符解释为一个空行(但有效)。为了删除它,我们删除任何包含无效数字的行(使用 inplace 只确保影响相同的 Dataframe,而不是创建一个新的 Dataframe):

adult.dropna(how='all', inplace=True)

查看数据集,我们可以从 adult.columns 中看到各种特征:

adult.columns

结果显示了存储在 pandas Index 对象内的每个特征名称:

Index(['Age', 'Work-Class', 'fnlwgt', 'Education', 
'Education-Num', 'Marital-Status', 'Occupation', 'Relationship', 
'Race', 'Sex', 'Capital-gain', 'Capital-loss', 'Hours-per-week', 
'Native-Country', 'Earnings-Raw'], dtype='object')

常见的特征模式

虽然有数百万种创建模型的方法,但不同学科中都有一些常见的模式。然而,选择合适的特征是棘手的,值得考虑一个特征可能如何与最终结果相关。正如一句著名的谚语所说,不要以貌取人——如果你对书中的信息感兴趣,考虑书的尺寸可能并不值得。

一些常用的特征专注于研究现实世界对象的物理属性,例如:

  • 物体的空间属性,如长度、宽度和高度

  • 物体的重量和/或密度

  • 物体或其组件的年龄

  • 物体的类型

  • 物体的质量

其他特征可能依赖于对象的使用或历史:

  • 物体的生产者、出版商或创作者

  • 制造年份

其他特征以对象的部分来描述数据集:

  • 给定子组件的频率,例如一本书中的单词

  • 子组件的数量和/或不同子组件的数量

  • 子组件的平均大小,例如平均句子长度

有序特征使我们能够对相似值进行排序、排序和分组。正如我们在前面的章节中看到的,特征可以是数值的或分类的。

数值特征通常被描述为有序的。例如,三个人,Alice、Bob 和 Charlie,可能有 1.5 米、1.6 米和 1.7 米的身高。我们会说 Alice 和 Bob 在身高上比 Alice 和 Charlie 更相似。

我们在上一个部分加载的 Adult 数据集包含连续的有序特征的例子。例如,每周工作小时数特征跟踪人们每周工作多少小时。某些操作适用于此类特征。包括计算平均值、标准差、最小值和最大值。pandas 中有一个函数可以提供此类类型的一些基本摘要统计信息:

adult["Hours-per-week"].describe()

结果告诉我们关于这个特征的一些信息:

count 32561.000000
mean 40.437456
std 12.347429
min 1.000000
25% 40.000000
50% 40.000000
75% 45.000000
max 99.000000
dtype: float64

这些操作中的一些对于其他特征来说没有意义。例如,计算这些人的教育状态总和是没有意义的。相比之下,计算每个在线商店顾客的订单总数是有意义的。

还有一些特征不是数值的,但仍然是序数的。成年数据集中的教育特征就是这样一个例子。例如,学士学位比完成高中教育有更高的教育地位,而完成高中教育比没有完成高中教育有更高的地位。对这些值计算平均值并不完全合理,但我们可以通过取中位数来创建一个近似值。数据集提供了一个有用的特征,Education-Num,它分配一个基本上等同于完成教育年数的数字。这使得我们可以快速计算中位数:

adult["Education-Num"].median()

结果是 10,即完成高中后的一年。如果我们没有这个,我们可以通过在教育值上创建一个排序来计算中位数。

特征也可以是分类的。例如,一个球可以是网球、板球、足球或其他任何类型的球。分类特征也被称为名义特征。对于名义特征,其值要么相同,要么不同。虽然我们可以根据大小或重量对球进行排序,但仅仅类别本身并不足以比较事物。网球不是板球,它也不是足球。我们可以争论网球在大小上可能更接近板球(比如说),但仅仅类别本身并不能区分这一点——它们要么相同,要么不同。

我们可以使用独热编码将分类特征转换为数值特征,正如我们在第三章中看到的,即使用决策树预测体育比赛胜者。对于上述球类的类别,我们可以创建三个新的二元特征:是否是网球、是否是板球和是否是足球。这个过程就是我们第三章中使用的独热编码,即使用决策树预测体育比赛胜者。对于一个网球,向量将是 [1, 0, 0]。板球的值是 [0, 1, 0],而足球的值是 [0, 0, 1]。这些是二元特征,但许多算法可以将它们用作连续特征。这样做的一个关键原因是可以轻松地进行直接的数值比较(例如计算样本之间的距离)。

成年数据集包含几个分类特征,其中工作类别就是一个例子。虽然我们可以争论某些值可能比其他值有更高的等级(例如,有工作的人可能比没有工作的人有更好的收入),但对于所有值来说这并不合理。例如,为国家政府工作的人并不比在私营部门工作的人更有可能或更不可能有更高的收入。

我们可以使用unique()函数在数据集中查看该特征的唯一值:

adult["Work-Class"].unique()

结果显示了该列的唯一值:

array([' State-gov', ' Self-emp-not-inc', ' Private', ' Federal-gov',
' Local-gov', ' ?', ' Self-emp-inc', ' Without-pay',
' Never-worked', nan], dtype=object)

在前面的数据中存在一些缺失值,但它们不会影响本例中的计算。您也可以使用adult.value_counts()函数查看每个值出现的频率。

在使用新的数据集时,另一个非常有用的步骤是可视化它。以下代码将创建一个群组图,展示教育和工作时间与最终分类(通过颜色标识)之间的关系:

%matplotlib inline
import seaborn as sns
from matplotlib import pyplot as plt
sns.swarmplot(x="Education-Num", y="Hours-per-week", hue="Earnings-Raw", data=adult[::50])

图片

在上面的代码中,我们通过使用adult[::50]数据集索引来采样数据集,以显示每 50 行,设置此为adult将导致显示所有样本,但这可能会使图表难以阅读。

同样,我们可以通过称为离散化的过程将数值特征转换为分类特征,正如我们在第一章“数据挖掘入门”中看到的。我们可以将身高超过 1.7 米的人称为高,身高低于 1.7 米的人称为矮。这给我们一个分类特征(尽管仍然是有序的)。我们在这里会丢失一些数据。例如,两个身高分别为 1.69 米和 1.71 米的人将属于两个不同的类别,并且被我们的算法认为差异很大。相比之下,一个身高 1.2 米的人将被认为与身高 1.69 米的人大致相同!这种细节的丢失是离散化的副作用,这是我们创建模型时需要处理的问题。

在成人数据集中,我们可以创建一个LongHours特征,它告诉我们一个人是否每周工作超过 40 小时。这把我们的连续特征(每周小时数)转换为一个分类特征,如果小时数超过 40 则为 True,否则为 False:

adult["LongHours"] = adult["Hours-per-week"] > 40

创建良好的特征

由于建模的简化,这是我们没有可以简单应用于任何数据集的数据挖掘方法的关键原因。一个优秀的数据挖掘从业者将需要或获得他们在应用数据挖掘领域的领域知识。他们将研究问题、可用数据,并提出一个代表他们试图实现的目标的模型。

例如,一个人的身高特征可能描述一个人的一个方面,比如他们打篮球的能力,但可能无法很好地描述他们的学术表现。如果我们试图预测一个人的成绩,我们可能不会麻烦去测量每个人的身高。

这就是数据挖掘比科学更具艺术性的地方。提取良好的特征是困难的,这也是一个重要且持续的研究课题。选择更好的分类算法可以提高数据挖掘应用的表现,但选择更好的特征通常是一个更好的选择。

在所有数据挖掘应用中,你应该在开始设计寻找目标的方法之前,首先概述你正在寻找的内容。这将决定你希望达到的特征类型,你可以使用的算法类型,以及最终结果中的期望。

特征选择

在初步建模之后,我们通常会有一大批特征可供选择,但我们希望只选择一小部分。有许多可能的原因:

  • 降低复杂性:当特征数量增加时,许多数据挖掘算法需要显著更多的时间和资源。减少特征数量是使算法运行更快或使用更少资源的好方法。

  • 降低噪声:添加额外的特征并不总是导致更好的性能。额外的特征可能会使算法混淆,在训练数据中找到没有实际意义的关联和模式。这在较小的和较大的数据集中都很常见。只选择合适的特征是减少没有实际意义的随机关联的好方法。

  • 创建可读的模型:虽然许多数据挖掘算法乐于为具有数千个特征的模型计算答案,但结果可能对人类来说难以解释。在这些情况下,使用较少的特征并创建一个人类可以理解的模式可能是有价值的。

一些分类算法可以处理前面描述的问题。确保数据正确,并确保特征能够有效地描述你正在建模的数据集,这仍然可以帮助算法。

我们可以进行一些基本的测试,例如确保特征至少是不同的。如果一个特征的所有值都相同,它就不能为我们提供额外的信息来执行我们的数据挖掘。

例如,scikit-learn中的VarianceThreshold转换器将删除任何在值中至少没有最小变异水平的特征。为了展示这是如何工作的,我们首先使用 NumPy 创建一个简单的矩阵:

import numpy as np
X = np.arange(30).reshape((10, 3))

结果是 0 到 29 的数字,分为三列和 10 行。这代表了一个包含 10 个样本和三个特征的合成数据集:

array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14],
[15, 16, 17],
[18, 19, 20],
[21, 22, 23],
[24, 25, 26],
[27, 28, 29]])

然后,我们将整个第二列/特征设置为值 1:

X[:,1] = 1

结果在第一行和第三行有很多变异,但在第二行没有变异:

array([[ 0, 1, 2],
[ 3, 1, 5],
[ 6, 1, 8],
[ 9, 1, 11],
[12, 1, 14],
[15, 1, 17],
[18, 1, 20],
[21, 1, 23],
[24, 1, 26],
[27, 1, 29]])

我们现在可以创建一个VarianceThreshold转换器并将其应用于我们的数据集:

from sklearn.feature_selection import VarianceThreshold
vt = VarianceThreshold()
Xt = vt.fit_transform(X)

现在,结果Xt没有第二列:

array([[ 0, 2],
[ 3, 5],
[ 6, 8],
[ 9, 11],
[12, 14],
[15, 17],
[18, 20],
[21, 23],
[24, 26],
[27, 29]])

我们可以通过打印vt.variances_属性来观察每列的变异:

print(vt.variances_)

结果显示,虽然第一列和第三列至少包含一些信息,但第二列没有变异:

array([ 74.25, 0\. , 74.25])

当第一次看到数据时,运行这样一个简单明了的测试总是好的。没有变异的特征不会为数据挖掘应用增加任何价值;然而,它们可能会减慢算法的性能并降低其有效性。

选择最佳单个特征

如果我们有多个特征,找到最佳子集的问题是一个困难任务。它与解决数据挖掘问题本身相关,需要多次解决。正如我们在第四章中看到的,使用亲和分析推荐电影,随着特征数量的增加,基于子集的任务呈指数增长。这种所需时间的指数增长也适用于找到最佳特征子集。

解决这个问题的基本方法不是寻找能够良好协同工作的子集,而是仅仅找到最佳的单个特征。这种单变量特征选择根据特征单独表现的好坏给出一个分数。这通常用于分类任务,我们通常测量变量和目标类别之间的某种关联。

scikit-learn 包提供了一系列用于执行单变量特征选择的转换器。它们包括 SelectKBest,它返回性能最好的 k 个特征,以及 SelectPercentile,它返回前 R% 的特征。在这两种情况下,都有多种计算特征质量的方法。

有许多不同的方法来计算单个特征与类别值的相关性有多有效。常用的方法之一是卡方 (χ2) 测试。其他方法包括互信息和熵。

我们可以通过使用 Adult 数据集来观察单变量测试的实际操作。首先,我们从 pandas DataFrame 中提取数据集和类值。我们得到特征的选择:

X = adult[["Age", "Education-Num", "Capital-gain", "Capital-loss", "Hours-per-week"]].values

我们还将通过测试 Earnings-Raw 值是否超过 $50,000 来创建一个目标类数组。如果是,则类为 True。否则,为 False。让我们看看代码:

y = (adult["Earnings-Raw"] == ' >50K').values

接下来,我们使用 chi2 函数和 SelectKBest 转换器创建我们的转换器:

from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
transformer = SelectKBest(score_func=chi2, k=3)

运行 fit_transform 将调用 fit 然后使用相同的数据集进行转换。

结果将创建一个新的数据集,只选择最好的三个特征。

让我们看看代码:

Xt_chi2 = transformer.fit_transform(X, y)

结果矩阵现在只包含三个特征。我们还可以获取分数

对于每一列,这使我们能够找出哪些特征被使用了。让我们看看

代码如下:

print(transformer.scores_)

打印的结果给出了这些分数:

[ 8.60061182e+03 2.40142178e+03 8.21924671e+07 1.37214589e+066.47640900e+03]

最大的值是第一列、第三列和第四列,分别对应年龄、资本收益和资本损失特征。基于单变量特征选择,这些是最好的选择特征。

如果您想了解更多关于 Adult 数据集中的特征的信息,请查看随数据集一起提供的 adult.names 文件以及它引用的学术论文。

我们还可以实现其他相关性,例如皮尔逊相关系数。这在 SciPy 中实现,SciPy 是一个用于科学计算的库(scikit-learn 使用它作为基础)。

如果 scikit-learn 在您的计算机上运行,SciPy 也在运行。为了使这个示例工作,您不需要安装任何其他东西。

首先,我们从 SciPy 中导入 pearsonr 函数:

from scipy.stats import pearsonr

上述函数几乎符合在 scikit-learn 的单变量转换器中使用的接口。该函数需要接受两个数组(在我们的例子中是 x 和 y)作为参数,并返回两个数组,每个特征的得分和相应的 p 值。我们之前使用的 chi2 函数只使用了所需的接口,这使得我们可以直接将其传递给 SelectKBest。

SciPy 中的 pearsonr 函数接受两个数组;然而,它接受的 X 数组只有一个维度。我们将编写一个包装函数,使我们能够使用这个函数处理像我们这样的多元数组。让我们看看代码:

def multivariate_pearsonr(X, y):
    scores, pvalues = [], []
    for column in range(X.shape[1]):
        # Compute the Pearson correlation for this column only
        cur_score, cur_p = pearsonr(X[:,column], y)
        # Record both the score and p-value.
        scores.append(abs(cur_score))
        pvalues.append(cur_p)
    return (np.array(scores), np.array(pvalues))

皮尔逊值可能在-1 和 1 之间。1 的值意味着两个变量之间有完美的相关性,而-1 的值意味着完美的负相关性,即一个变量的高值对应另一个变量的低值,反之亦然。这样的特征非常有用。因此,我们在得分数组中存储了绝对值,而不是原始的有符号值。

现在,我们可以像以前一样使用 transformer 类,通过皮尔逊相关系数来对特征进行排序:

transformer = SelectKBest(score_func=multivariate_pearsonr, k=3)
Xt_pearson = transformer.fit_transform(X, y)
print(transformer.scores_)

这会返回一组不同的特征!这样选择的特征是第一列、第二列和第五列:年龄、教育和每周工作小时数。这表明,并没有一个明确的答案来决定哪些是最好的特征——它取决于所使用的指标和所进行的过程。

我们可以通过运行它们通过分类器来查看哪个特征集更好。请注意,结果仅表明对于特定的分类器或特征组合,哪个子集更好——在数据挖掘中,很少有一种方法在所有情况下都严格优于另一种方法!让我们看看代码:

from sklearn.tree import DecisionTreeClassifier
from sklearn.cross_validation import cross_val_score
clf = DecisionTreeClassifier(random_state=14)
scores_chi2 = cross_val_score(clf, Xt_chi2, y, scoring='accuracy')
scores_pearson = cross_val_score(clf, Xt_pearson, y, scoring='accuracy')

print("Chi2 score: {:.3f}".format(scores_chi2.mean()))
print("Pearson score: {:.3f}".format(scores_pearson.mean()))

这里的 chi2 平均值为 0.83,而皮尔逊分数较低,为 0.77。对于这个组合,chi2 返回更好的结果!

记住这个特定数据挖掘活动的目标是值得的:预测财富。通过结合良好的特征和特征选择,我们只需使用一个人的三个特征就能达到 83%的准确率!

特征创建

有时候,仅仅从我们所拥有的特征中选择特征是不够的。我们可以从已有的特征以不同的方式创建特征。我们之前看到的独热编码方法就是这样一个例子。而不是有选项 A、B 和 C 的类别特征,我们会创建三个新的特征:它是 A 吗?它是 B 吗?它是 C 吗?

创建新特征可能看起来是不必要的,并且没有明显的益处——毕竟,信息已经在数据集中,我们只需要使用它。然而,一些算法在特征高度相关或存在冗余特征时可能会遇到困难。它们也可能在存在冗余特征时遇到困难。因此,有各种方法可以从我们已有的特征中创建新特征。

我们将加载一个新的数据集,因此现在是开始一个新的 Jupyter Notebook 的好时机。从 archive.ics.uci.edu/ml/datasets/Internet+Advertisements 下载 Advertisements 数据集并将其保存到您的数据文件夹中。

接下来,我们需要使用 pandas 加载数据集。首先,我们设置数据的文件名,就像往常一样:

import os
import numpy as np
import pandas as pd
data_folder = os.path.join(os.path.expanduser("~"), "Data")
data_filename = os.path.join(data_folder, "Ads", "ad.data")

这个数据集有几个问题,阻止我们轻松地加载数据。您可以通过尝试使用 pd.read_csv 加载数据集来查看这些问题。首先,前几个特征是数值型的,但 pandas 会将它们加载为字符串。为了解决这个问题,我们需要编写一个转换函数,该函数将尝试将字符串转换为数字。否则,我们将得到一个 Not a Number (NaN) - 一个无效值,它是一个特殊值,表示该值无法解释为数字。在其他编程语言中,它类似于 none 或 null。

这个数据集的另一个问题是某些值缺失。这些值在数据集中用字符串 ? 表示。幸运的是,问号不能转换为浮点数,因此我们可以使用相同的概念将它们转换为 NaN。在后续章节中,我们将探讨处理此类缺失值的其他方法。

我们将创建一个函数来完成这个转换。它尝试将数字转换为浮点数,如果失败,则返回 NumPy 的特殊 NaN 值,该值可以存储在浮点数的位置:

def convert_number(x):
    try:
        return float(x)
    except ValueError:
        return np.nan

现在,我们创建一个字典用于转换。我们希望将所有特征转换为浮点数:

converters = {}
for i in range(1558):
    converters[i] = convert_number

此外,我们希望将最后一列,即类别列(列索引 #1558),设置为二进制特征。在 Adult 数据集中,我们为此创建了一个新特征。在加载数据集时,我们将转换该特征:

converters[1558] = lambda x: 1 if x.strip() == "ad." else 0

现在,我们可以使用 read_csv 加载数据集。我们使用 converters 参数将自定义转换传递给 pandas:

ads = pd.read_csv(data_filename, header=None, converters=converters)

结果数据集相当大,有 1,559 个特征和超过 3,000 行。以下是一些特征值,前五个,通过在新的单元格中插入 ads.head() 打印出来:

这个数据集描述了网站上的图像,目标是确定给定的图像是否是广告。

这些数据集中的特征值没有很好地通过其标题来描述。伴随 ad.data 文件的两个文件提供了更多信息:ad.DOCUMENTATIONad.names。前三个特征是图像的高度、宽度和尺寸比。最后一个特征如果是广告则为 1,如果不是则为 0。

其他特征表示 URL、alt 文本或图像标题中是否存在某些单词。这些单词,如赞助商一词,用于确定图像是否可能是广告。许多特征在很大程度上是其他特征的组合,因此这个数据集有很多冗余信息。

在我们的数据集加载到pandas后,我们现在将提取用于分类算法的xy数据。x矩阵将是我们的 DataFrame 中的所有列,除了最后一列。相比之下,y数组将只有最后一列,特征1558.。在那之前,我们通过删除任何包含 NaN 值的行来简化我们的数据集(只是为了本章的目的)。让我们看看代码:

ads.dropna(inplace=True)
X = ads.drop(1558, axis=1).values
y = ads[1558]

由于此命令,删除了 1000 多行,这对于我们的练习来说是可以接受的。对于实际应用,如果你能帮助避免数据丢失,你不想丢弃数据——相反,你可以使用插值或值替换来填充 NaN 值。例如,你可以用该列的平均值替换任何缺失值。

主成分分析

在某些数据集中,特征之间高度相关。例如,在单档的卡丁车中,速度和燃油消耗会高度相关。虽然对于某些应用来说,找到这些相关性可能是有用的,但数据挖掘算法通常不需要冗余信息。

广告数据集具有高度相关的特征,因为许多关键词在 alt 文本和标题中重复。

主成分分析(PCA)算法旨在找到描述数据集所需信息更少的特征组合。它旨在发现主成分,即不相互关联且解释数据信息——特别是方差——的特征。这意味着我们通常可以在更少的特征中捕捉到数据集的大部分信息。

我们应用 PCA 就像应用任何其他转换器一样。它有一个关键参数,即要找到的组件数量。默认情况下,它将产生与原始数据集中特征数量一样多的特征。然而,这些主成分是按顺序排列的——第一个特征解释了数据集中最大的方差,第二个稍微少一些,以此类推。因此,只需找到前几个特征就足以解释数据集的大部分内容。让我们看看代码:

from sklearn.decomposition import PCA
pca = PCA(n_components=5)
Xd = pca.fit_transform(X)

结果矩阵 Xd 只有五个特征。然而,让我们看看每个特征解释的方差量:

np.set_printoptions(precision=3, suppress=True)
pca.explained_variance_ratio_

结果,array([ 0.854, 0.145, 0.001, 0\. , 0\. ]) 显示我们第一个特征解释了数据集中 85.4%的方差,第二个解释了 14.5%,以此类推。到第四个特征时,特征中包含的方差不到百分之一的十分之一。其他 1,553 个特征解释的方差更少(这是一个有序数组)。

使用 PCA 转换数据的缺点是这些特征通常是其他特征的复杂组合。例如,前面代码中的第一个特征以[-0.092, -0.995, -0.024],开始,即用-0.092 乘以原始数据集中的第一个特征,用-0.995 乘以第二个,用-0.024 乘以第三个。这种特征有 1,558 个这样的值,每个原始数据集都有一个(尽管许多是零)。这种特征对人类来说是不可区分的,并且在没有大量使用经验的情况下很难从中获取相关信息。

使用 PCA 可以导致模型不仅近似原始数据集,还可以提高分类任务中的性能:

clf = DecisionTreeClassifier(random_state=14)
scores_reduced = cross_val_score(clf, Xd, y, scoring='accuracy')

得到的分数是 0.9356,这比我们原始模型的分数略高。PCA 并不总是能带来这样的好处,但这种情况比不常见。

我们在这里使用 PCA 来减少数据集中的特征数量。一般来说,你不应该在数据挖掘实验中用它来减少过拟合。原因在于 PCA 没有考虑类别。一个更好的解决方案是使用正则化。有关介绍和代码,请参阅blog.datadive.net/selecting-good-features-part-ii-linear-models-and-regularization/

另一个优点是 PCA 允许你绘制那些你否则难以可视化的数据集。例如,我们可以绘制 PCA 返回的前两个特征。

首先,我们告诉我们的 Notebook 显示内联图表:

%matplotlib inline
from matplotlib import pyplot as plt

接下来,我们获取我们数据集中的所有不同类别(只有两个:是广告还是不是广告):

classes = set(y)

我们还为这些类别中的每一个分配了颜色:

colors = ['red', 'green']

我们使用 zip 同时遍历两个列表,然后提取该类别的所有样本,并用适合该类别的颜色绘制它们:

for cur_class, color in zip(classes, colors):
mask = (y == cur_class)
    plt.scatter(Xd[mask,0], Xd[mask,1], marker='o', color=color, label=int(cur_class))

最后,在循环外部,我们创建一个图例并显示图表,显示每个类别的样本出现在哪里:

plt.legend()
plt.show()

创建你自己的变换器

随着数据集的复杂性和类型的改变,你可能会发现找不到一个现成的特征提取变换器来满足你的需求。我们将在第七章中看到一个例子,使用图挖掘遵循推荐,在那里我们从图中创建新的特征。

变换器类似于一个转换函数。它接收一种形式的数据作为输入,并返回另一种形式的数据作为输出。变换器可以使用某些训练数据集进行训练,并且这些训练好的参数可以用来转换测试数据。

变换器 API 非常简单。它接收特定格式的数据作为输入,并返回另一种格式(可以是与输入相同的格式或不同的格式)的数据作为输出。对程序员的要求不多。

变换器 API

变换器有两个关键功能:

  • fit(): 这个函数接受一个训练数据集作为输入并设置内部参数。

  • transform(): 这个函数执行实际的转换。它可以接受训练数据集,或者格式相同的新的数据集。

fit()transform() 函数都应该接受相同的数据类型作为输入,但 transform() 可以返回不同类型的数据,而 fit() 总是返回 self。

我们将创建一个简单的转换器来展示 API 的实际应用。这个转换器将接受一个 NumPy 数组作为输入,并根据平均值对其进行离散化。任何高于平均值的(训练数据的平均值)将被赋予值 1,任何低于或等于平均值的将被赋予值 0。

我们使用 pandas 对 Adult 数据集进行了类似的转换:我们取每周工作小时数特征,如果每周工作小时数超过 40 小时,就创建一个 LongHours 特征。这个转换器有两个不同之处。首先,代码将符合 scikit-learn API,允许我们在管道中使用它。其次,代码将学习平均值,而不是将其作为固定值(如 LongHours 示例中的 40)。

实现转换器

首先,打开我们用于 Adult 数据集的 Jupyter Notebook。然后,点击 Cell 菜单项并选择 Run All。这将重新运行所有单元格,确保笔记本是最新的。

首先,我们导入 TransformerMixin,这为我们设置了 API。虽然 Python 没有严格的接口(与 Java 等语言相反),但使用这样的 mixin 允许 scikit-learn 确定该类实际上是一个转换器。我们还需要导入一个检查输入是否为有效类型的函数。我们很快就会使用它。

让我们看看代码:

from sklearn.base import TransformerMixin
from sklearn.utils import as_float_array

让我们看一下我们的类的整体结构,然后我们将回顾一些细节:

class MeanDiscrete(TransformerMixin):
    def fit(self, X, y=None):
        X = as_float_array(X)
        self.mean = X.mean(axis=0)
        return self

    def transform(self, X, y=None):
        X = as_float_array(X)
        assert X.shape[1] == self.mean.shape[0]
        return X > self.mean

我们将在 fit 方法中通过计算X.mean(axis=0)来学习每个特征的均值,然后将其存储为对象属性。之后,fit 函数返回 self,符合 API(scikit-learn 使用此功能允许链式调用函数)。

在拟合后,transform 函数接受一个具有相同数量特征的矩阵(通过assert语句确认),并简单地返回给定特征的哪些值高于平均值。

现在我们已经构建了类,我们可以创建这个类的实例,并使用它来转换我们的 X 数组:

mean_discrete = MeanDiscrete()
X_mean = mean_discrete.fit_transform(X)

尝试将这个转换器实现到一个工作流程中,既使用 Pipeline 也使用不使用 Pipeline。你会发现,通过符合转换器 API,它非常简单地在内置的 scikit-learn 转换器对象中替代使用。

单元测试

在创建自己的函数和类时,始终进行单元测试是一个好主意。单元测试旨在测试代码的单个单元。在这种情况下,我们想要测试我们的转换器是否按预期工作。

好的测试应该是可以独立验证的。确认测试合法性的一个好方法是使用另一种计算机语言或方法来执行计算。在这种情况下,我使用了 Excel 来创建一个数据集,然后计算每个单元格的平均值。然后这些值被转移到单元测试中。

单元测试通常也应该小巧且运行速度快。因此,所使用的数据应该尽可能小。我用于创建测试的数据集存储在之前的 Xt 变量中,我们将在测试中重新创建它。这两个特征的均值分别是 13.5 和 15.5。

为了创建我们的单元测试,我们从 NumPy 的测试中导入assert_array_equal函数,该函数检查两个数组是否相等:

from numpy.testing import assert_array_equal

接下来,我们创建我们的函数。重要的是测试的名称必须以 test_ 开头,

因为这个命名约定用于自动查找和运行测试的工具。我们还设置了我们的测试数据:

def test_meandiscrete():
    X_test = np.array([[ 0, 2],
                       [ 3, 5],
                       [ 6, 8],
                       [ 9, 11],
                       [12, 14],
                       [15, 17],
                       [18, 20],
                       [21, 23],
                       [24, 26],
                       [27, 29]])
    # Create an instance of our Transformer
    mean_discrete = MeanDiscrete()
    mean_discrete.fit(X_test)
    # Check that the computed mean is correct
    assert_array_equal(mean_discrete.mean, np.array([13.5, 15.5]))
    # Also test that transform works properly
    X_transformed = mean_discrete.transform(X_test)
    X_expected = np.array([[ 0, 0],
                           [ 0, 0], 
                           [ 0, 0],
                           [ 0, 0],
                           [ 0, 0],
                           [ 1, 1],
                           [ 1, 1],
                           [ 1, 1],
                           [ 1, 1],
                           [ 1, 1]])
    assert_array_equal(X_transformed, X_expected)

我们可以通过直接运行函数本身来执行测试:

test_meandiscrete()

如果没有错误,那么测试就顺利运行了!你可以通过故意更改一些测试以使值不正确,并确认测试失败来验证这一点。记住要改回来,以便测试通过!

如果我们有多个测试,使用像 py.test 或 nose 这样的测试框架来运行我们的测试将是有价值的。使用这样的框架超出了本书的范围,但它们可以管理运行测试、记录失败并向程序员提供反馈,以帮助改进代码。

将所有这些放在一起

现在我们已经测试了一个转换器,是时候将其投入使用了。利用我们迄今为止所学的内容,我们创建了一个 Pipeline,将第一步设置为 MeanDiscrete 转换器,第二步设置为决策树分类器。然后我们运行交叉验证并打印出结果。让我们看看代码:

from sklearn.pipeline import Pipeline
pipeline = Pipeline([('mean_discrete', MeanDiscrete()), ('classifier', DecisionTreeClassifier(random_state=14))])
scores_mean_discrete = cross_val_score(pipeline, X, y, scoring='accuracy')
print("Mean Discrete performance: {0:.3f}".format(scores_mean_discrete.mean()))

结果是 0.917,虽然没有之前好,但对于一个简单的二进制特征模型来说已经非常不错了。

摘要

在本章中,我们探讨了特征和转换器以及它们如何在数据挖掘流程中使用。我们讨论了什么是一个好的特征以及如何从标准集中算法性地选择好的特征。然而,创建好的特征更多的是艺术而非科学,通常需要领域知识和经验。

我们然后使用一个允许我们在 scikit-learn 的辅助函数中使用它的接口创建了自己的转换器。我们将在后面的章节中创建更多的转换器,以便我们可以使用现有函数进行有效的测试。

为了将本章学到的知识进一步深化,我建议您注册到在线数据挖掘竞赛网站 Kaggle.com 并尝试一些竞赛。他们推荐的起点是泰坦尼克号数据集,这可以让您练习本章中特征创建的方面。许多特征都不是数值型的,需要您在应用数据挖掘算法之前将它们转换为数值特征。

在下一章中,我们将在文本文档语料库上使用特征提取。文本有很多转换器和特征类型,每种都有其优缺点。