Python-数据科学与机器学习实用手册-全-

Python 数据科学与机器学习实用手册(全)

原文:zh.annas-archive.org/md5/92E2CBA50423C2D275EEE8125598FF8B

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

成为科技行业的数据科学家是当今地球上最有价值的职业之一。我去研究了科技公司数据科学家职位的实际工作描述,并将这些要求归纳为您将在本课程中看到的主题。

《动手做数据科学和 Python 机器学习》真的非常全面。我们将从 Python 的速成课程开始,然后回顾一些基本的统计和概率知识,但接着我们将直接涉及超过 60 个数据挖掘和机器学习的主题。其中包括贝叶斯定理、聚类、决策树、回归分析、实验设计;我们将全面研究它们。其中一些主题真的非常有趣。

我们将开发一个实际的电影推荐系统,使用实际的用户电影评分数据。我们将创建一个真正适用于维基百科数据的搜索引擎。我们将构建一个可以正确分类垃圾邮件和非垃圾邮件的垃圾邮件分类器,并且我们还有一个关于将这项工作扩展到在大数据上运行的集群的整个部分,使用 Apache Spark。

如果您是一名软件开发人员或程序员,希望转向数据科学职业,这门课程将教会您最热门的技能,而不需要所有这些数学符号和伪装,这些都是与这些主题相关的。我们只会解释这些概念,并向您展示一些真正有效的 Python 代码,您可以深入研究并进行操作,以使这些概念深入人心,如果您在金融行业担任数据分析师,这门课程也可以教会您转向科技行业。您只需要一些编程或脚本编写的经验,就可以开始了。

这本书的一般格式是我将从每个概念开始,用一堆部分和图形示例来解释它。我会向您介绍一些数据科学家喜欢使用的符号和花哨的术语,这样您就可以用相同的语言交流,但这些概念本身通常非常简单。之后,我会让您实际运行一些真正有效的 Python 代码,让我们可以运行并进行一些操作,并且这将向您展示如何将这些想法应用到实际数据中。这些将被呈现为 IPython Notebook 文件,这是一种我可以在其中混合代码和解释代码周围的笔记的格式,解释概念中发生的事情。在阅读完本书后,您可以将这些笔记本文件带走,并在以后的职业生涯中使用它作为方便的快速参考,而在每个概念的结尾,我会鼓励您实际深入研究 Python 代码,进行一些修改,进行一些操作,并通过实际进行一些修改,看到它们产生的效果,从而更加熟悉。

这本书是为谁准备的

如果您是一名新兴的数据科学家或数据分析师,希望使用 Python 分析数据并获得可操作的见解,那么这本书适合您。有一些 Python 经验的程序员,希望进入数据科学这个利润丰厚的领域,也会发现这本书非常有用。

约定

在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是这些样式的一些示例以及它们的含义解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:"我们可以使用sklearn.metrics中的r2_score()函数来衡量这个。"

代码块设置如下:

import numpy as np 
import pandas as pd 
from sklearn import tree 

input_file = "c:/spark/DataScience/PastHires.csv" 
df = pd.read_csv(input_file, header = 0) 

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

import numpy as np
import pandas as pd
from sklearn import tree

input_file = "c:/spark/DataScience/PastHires.csv"
df = pd.read_csv(input_file, header = 0) 

任何命令行输入或输出都将按以下方式书写:

spark-submit SparkKMeans.py  

新术语和重要单词以粗体显示。例如,屏幕上显示的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“在 Windows 10 上,您需要打开“开始”菜单,然后转到“Windows 系统”|“控制面板”以打开“控制面板”。”

警告或重要提示会显示为这样。

提示和技巧会显示为这样。

第一章:入门

由于这本书将涉及与代码相关的内容和您需要获取的示例数据,让我先向您展示在哪里获取这些内容,然后我们就可以开始了。我们首先需要做一些设置。首先,让我们获取本书所需的代码和数据,这样您就可以跟着操作,并且有一些代码可以进行实际操作。最简单的方法是直接转到 入门

在本章中,我们将首先安装并准备好一个可用的 Python 环境:

  • 安装 Enthought Canopy

  • 安装 Python 库

  • 如何使用 IPython/Jupyter Notebook

  • 如何使用、阅读和运行本书的代码文件

  • 然后我们将进行一个快速课程,了解 Python 代码:

  • Python 基础知识 - 第一部分

  • 理解 Python 代码

  • 导入模块

  • 尝试使用列表

  • 元组

  • Python 基础知识 - 第二部分

  • 运行 Python 脚本

一旦我们设置好您的环境并在本章中让您熟悉 Python,您就会拥有一切进行 Python 数据科学的绝妙旅程所需的一切。

安装 Enthought Canopy

让我们立即开始,安装您在桌面上实际开发 Python 数据科学所需的内容。我将带您完成安装一个名为 Enthought Canopy 的软件包,它已经预先安装了开发环境和所有 Python 软件包。这将使生活变得非常容易,但如果您已经了解 Python,可能已经在您的 PC 上有现有的 Python 环境,如果您想继续使用它,也许您可以。

最重要的是,您的 Python 环境必须具有 Python 3.5 或更新版本,支持 Jupyter Notebook(因为这是我们在本课程中要使用的),并且您的环境中已安装了本书所需的关键软件包。我将详细解释如何通过几个简单的步骤实现完整安装 - 这将非常容易。

让我们首先概述这些关键软件包,其中大部分 Canopy 将自动为我们安装。Canopy 将为我们安装 Python 3.5,以及我们需要的一些其他软件包,包括:scikit_learnxlrdstatsmodels。我们需要手动使用 pip 命令来安装一个名为 pydot2plus 的软件包。就是这样 - 使用 Canopy 非常容易!

一旦完成以下安装步骤,我们将拥有一切需要的内容来真正开始运行,然后我们将打开一个小样本文件,进行一些真正的数据科学。现在让我们尽快为您设置好一切所需的内容:

  1. 您首先需要的是一个称为 IDE 的 Python 代码开发环境。我们将在本书中使用的是 Enthought Canopy。这是一个科学计算环境,将与本书很好地配合使用:

  1. 要安装 Canopy,只需转到 www.enthought.com,然后点击下载:Canopy:

  1. Enthought Canopy 是免费的,适用于 Canopy Express 版本 - 这是您在本书中需要的版本。然后您必须选择您的操作系统和架构。对我来说,这是 Windows 64 位,但您需要点击相应的下载按钮,选择适用于您操作系统的 Python 3.5 选项:

  1. 在这一步我们不需要提供任何个人信息。这是一个相当标准的 Windows 安装程序,所以只需让它下载:

  1. 下载完成后,我们继续打开 Canopy 安装程序,并运行它!您可能想在同意之前阅读许可协议,这取决于您,然后只需等待安装完成。

  2. 一旦您在安装过程的最后点击完成按钮,允许它自动启动 Canopy。您会看到 Canopy 自动设置 Python 环境,这很好,但这将需要一两分钟的时间。

  3. 安装程序设置完成您的 Python 环境后,您应该会看到下面的屏幕。它会显示欢迎来到 Canopy 和一堆友好的大按钮:

  1. 美妙的事情是,几乎您在本书中所需的一切都已经预先安装在 Enthought Canopy 中,这就是为什么我建议使用它!

  2. 我们只需要设置最后一件事,所以请点击 Canopy 欢迎屏幕上的编辑器按钮。然后您会看到编辑器屏幕出现,如果您在底部的窗口中点击,我希望您只是输入:

!pip install pydotplus 

  1. 当您在 Canopy 编辑器窗口底部输入上述行时,屏幕会显示如下;当然不要忘记按回车键:

  1. 按下回车键后,这将安装我们在本书后面需要的一个额外模块,当我们开始讨论决策树和渲染决策树时。

  2. 一旦安装完成pydotplus,它应该会回来并说它已成功安装,您现在已经拥有了开始的一切!此时安装已经完成-但让我们再走几步来确认我们的安装是否正常运行。

对安装进行测试

  1. 现在让我们对您的安装进行测试。首先要做的事情是完全关闭 Canopy 窗口!这是因为我们实际上不会在这个 Canopy 编辑器中编辑和使用我们的代码。相反,我们将使用一个称为 IPython 笔记本的东西,现在也被称为 Jupyter 笔记本。

  2. 让我向您展示一下它是如何工作的。如果您现在在操作系统中打开一个窗口,查看您下载的附带书籍文件,就像本书的前言中描述的那样。它应该看起来像这样,带有您为本书下载的一组.ipynb代码文件:

现在在列表中找到异常值文件,即Outliers.ipynb文件,双击它,应该会启动 Canopy,然后启动您的网络浏览器!这是因为 IPython/Jupyter 笔记本实际上存在于您的网络浏览器中。一开始可能会有一小段暂停,第一次可能会有点混乱,但您很快就会习惯的。

您很快就会看到 Canopy 出现,对我来说,我的默认网络浏览器 Chrome 会出现。您应该会看到以下 Jupyter 笔记本页面,因为我们双击了Outliers.ipynb文件:

如果您看到这个屏幕,这意味着您的安装工作得很好,您已经准备好继续阅读本书的其余部分了!

如果您偶尔遇到打开 IPNYB 文件的问题

偶尔,我注意到当您双击.ipynb文件时,有时会出现一些小问题。不要惊慌!有时,Canopy 可能会有点不稳定,您可能会看到一个寻找密码或令牌的屏幕,或者偶尔会看到一个完全无法连接的屏幕。

如果您遇到这些情况,不要惊慌,它们只是偶然的怪癖,有时事情就是不按正确的顺序启动,或者它们在您的 PC 上没有及时启动,没关系。

您只需返回并尝试第二次打开该文件。有时需要两三次尝试才能正确加载它,但如果您多试几次,最终它应该会弹出,并且您应该会看到一个 Jupyter 笔记本屏幕,就像我们之前看到的关于处理异常值的那个。

使用和理解 IPython(Jupyter)笔记本

恭喜您的安装!现在让我们探索使用 Jupyter 笔记本,也称为 IPython 笔记本。如今,更现代的名称是 Jupyter 笔记本,但很多人仍然称其为 IPython 笔记本,因此我认为这两个名称对于工作开发人员来说是可以互换的。我也发现 IPython 笔记本这个名称有助于我记住笔记本文件的后缀名是.ipynb,在本书中您将非常熟悉这个后缀名!

好的,现在让我们从头开始 - 首先探索 IPython/Jupyter 笔记本。如果您还没有这样做,请导航到我们为本书下载的DataScience文件夹。对我来说,那是E:DataScience,如果您在前面的安装部分中没有这样做,请现在双击并打开Outliers.ipynb文件。

现在,当我们双击此 IPython .ipynb文件时,首先会启动 Canopy,然后会启动一个 Web 浏览器。这是完整的Outliers笔记本网页在我的浏览器中的样子:

正如您在这里看到的,笔记本的结构使我可以在实际代码中穿插一些关于您在这里看到的内容的小注释和评论,您实际上可以在 Web 浏览器中运行此代码!因此,对我来说,这是一个非常方便的格式,可以为您提供一些参考,以便以后在生活中去回顾这些我们将要讨论的算法是如何工作的,并且实际上可以自己尝试和玩耍。

IPython/Jupyter 笔记本文件的工作方式是它们实际上是在您的浏览器中运行的,就像一个网页,但它们由您安装的 Python 引擎支持。因此,您应该看到与前一个屏幕截图中显示的类似的屏幕。

当您在浏览器中向下滚动笔记本时,您会注意到有代码块。它们很容易识别,因为它们包含我们的实际代码。请在异常值笔记本中找到此代码的代码框,它就在顶部附近:

%matplotlib inline 
import numpy as np 

incomes = np.random.normal(27000, 15000, 10000) 
incomes = np.append(incomes, [1000000000]) 

import matplotlib.pyplot as plt 
plt.hist(incomes, 50) 
plt.show() 

让我们在这里快速看一下这段代码。在这段代码中,我们设置了一些收入分布。我们模拟了人口中的收入分布,并且为了说明异常值对该分布的影响,我们模拟了唐纳德·特朗普加入并扰乱了收入分布的平均值。顺便说一句,我并不是在发表政治言论,这都是在特朗普成为政治人物之前完成的。所以,您知道,完全披露在这里。

我们可以通过单击来选择笔记本中的任何代码块。因此,如果您现在点击包含我们刚才查看的代码的代码块,然后点击顶部的运行按钮来运行它。这是屏幕顶部的区域,您将在其中找到运行按钮:

选择代码块并点击运行按钮,将导致重新生成此图:

同样,我们可以点击稍微向下的下一个代码块,您会看到其中有以下一行代码:

incomes.mean() 

如果您选择包含此行的代码块,并点击运行按钮运行代码,您将在其下看到输出,由于异常值的影响,输出将是一个非常大的值,类似于这样:

127148.50796177129

让我们继续并且玩得开心。在下面的下一个代码块中,您将看到以下代码,它尝试检测像唐纳德·特朗普这样的异常值,并将它们从数据集中删除:

def reject_outliers(data): 
    u = np.median(data) 
    s = np.std(data) 
    filtered = [e for e in data if (u - 2 * s < e < u + 2 * s)] 
    return filtered 

filtered = reject_outliers(incomes) 
plt.hist(filtered, 50) 
plt.show() 

因此,请在笔记本中选择相应的代码块,然后再次按运行按钮。当您这样做时,您将看到这张图:

现在我们看到了一个更好的直方图,代表了更典型的美国人-现在我们已经去掉了混乱的异常值。

所以,此时,您已经具备了开始本课程所需的一切。我们拥有您需要的所有数据,所有脚本,以及 Python 和 Python 笔记本的开发环境。所以,让我们开始吧。接下来,我们将进行一些关于 Python 本身的速成课程,即使您熟悉 Python,这也可能是一个不错的温习,所以您可能还是想观看一下。让我们深入学习 Python。

Python 基础-第一部分

如果您已经了解 Python,您可能可以跳过接下来的两个部分。但是,如果您需要温习,或者以前没有接触过 Python,您可能需要浏览一下。关于 Python 脚本语言有一些古怪的地方,您需要知道,所以让我们深入学习一下,通过编写一些实际代码来学习一些 Python。

就像我之前说的,在本书的要求中,您应该具备某种编程背景才能成功。您已经在某种语言中编写过代码,即使是脚本语言,JavaScript,我不在乎它是 C++,Java,还是其他什么,但如果您是 Python 的新手,我将在这里给您一个速成课程。我将直接开始并在本节中给出一些示例。

Python 有一些与您可能见过的其他语言有些不同的地方;所以我只是想通过查看一些真实的例子来介绍 Python 与其他脚本语言的不同之处。让我们直接开始,看一些 Python 代码:

如果您打开了在之前部分中下载的DataScience文件夹,您应该会找到一个Python101.ipynb文件;请双击打开。如果您已经正确安装了所有内容,它应该会在 Canopy 中立即打开,并且应该看起来有点像以下的截图:

新版本的 Canopy 将在您的网络浏览器中打开代码,而不是 Canopy 编辑器!这没问题!

Python 的一个很酷的地方是,有几种运行 Python 代码的方式。您可以将其作为脚本运行,就像您使用普通编程语言一样。您还可以在这个叫做IPython Notebook的东西中编写代码,这就是我们在这里使用的东西。因此,这是一种格式,您实际上可以在其中以类似网络浏览器的视图中编写一些小注释和 HTML 标记的笔记,还可以嵌入实际使用 Python 解释器运行的代码。

理解 Python 代码

我想给您展示一些 Python 代码的第一个例子就在这里。以下代码块代表了一些真正的 Python 代码,我们实际上可以在整个笔记本页面的视图中运行,但现在让我们放大一下,看看那段代码:

让我们看看发生了什么。我们有一个数字列表和 Python 中的一个列表,类似于其他语言中的数组。它由这些方括号指定:

我们有一个包含 1 到 6 的数字列表的数据结构,然后要遍历该列表中的每个数字,我们将说for number in listOfNumbers:,这是 Python 遍历一系列东西的语法,后面跟着一个冒号。

在 Python 中,制表符和空格是有实际意义的,所以您不能随意格式化。您必须注意它们。

我想要表达的观点是,在其他语言中,通常会有括号或大括号来表示我在for循环、if块或某种代码块中,但在 Python 中,这一切都是由空格来指定的。制表符实际上在告诉 Python 哪些代码块中有什么:

for number in listOfNumbers: 
    print number, 
    if (number % 2 == 0): 
        print ("is even")
    else: 
        print ("is odd") 

print ("Hooray! We're all done.")

你会注意到,在这个for块中,我们有一个制表符,对于listOfNumbers中的每个number,我们将执行所有这些代码,这些代码都是通过一个Tab进行缩进的。我们将打印出这个数字,逗号只是表示我们不会在后面换行。我们将在后面打印其他东西,如果(number % 2 = 0),我们将说它是even。否则,我们将说它是odd,当我们完成时,我们将打印出All done

你可以在代码下方看到输出。我之前已经运行了输出,因为我已经将它保存在我的笔记本中,但如果你想自己运行它,你只需点击该块并点击播放按钮,我们将实际执行它并再次运行。为了让自己确信它确实做了一些事情,让我们把print语句改成其他的,比如说,Hooray! We're all done. Let's party!如果我现在运行这个,你会看到,我的消息确实改变了:

所以,我想要表达的观点是空格很重要。你会使用缩进或制表符来指定一起运行的代码块,比如for循环或if then语句,所以记住这一点。还要注意冒号。你会注意到很多从句都是以冒号开始的。

导入模块

Python 本身,就像任何语言一样,其功能是相当有限的。使用 Python 进行机器学习、数据挖掘和数据科学的真正力量在于为此目的提供的所有外部库的强大功能。其中一个库叫做NumPy,或者叫做数值 Python,例如,我们可以import Numpy包,它包含在 Canopy 中,名称为np

这意味着我将把NumPy包称为np,我可以随意更改它的名称。我可以称它为FredTim,但最好还是使用有意义的名称;现在我把NumPy包称为np,我可以使用np来引用它了:

import numpy as np

在这个例子中,我将调用NumPy包提供的random函数,并调用其正态函数来生成一组随机数的正态分布,并将其打印出来。由于它是随机的,每次我应该得到不同的结果:

import numpy as np
A = np.random.normal(25.0, 5.0, 10)
print (A)

输出应该是这样的:

果然,我得到了不同的结果。这很酷。

数据结构

让我们继续讨论数据结构。如果你需要暂停一下,让事情沉淀一下,或者你想更多地玩弄一下这些,随时都可以这样做。学习这些东西的最好方法就是投入其中,实际进行实验,所以我绝对鼓励这样做,这也是为什么我给你们提供了可工作的 IPython/Jupyter 笔记本,这样你们就可以真正进入其中,改变代码,做不同的事情。

举个例子,这里我们有一个围绕25.0的分布,但让我们把它围绕55.0

import numpy as np
A = np.random.normal(55.0, 5.0, 10)
print (A)

嘿,我的所有数字都变了,它们现在更接近 55 了,怎么样?

好的,让我们来谈谈数据结构。就像我们在第一个例子中看到的那样,你可以有一个列表,语法看起来是这样的。

列表实验

x = [1, 2, 3, 4, 5, 6]
print (len(x))

你可以说,比如,调用一个名为x的列表,并将其赋值为数字16,这些方括号表示我们使用的是 Python 列表,它们是不可变的对象,我可以随意添加和重新排列。有一个用于确定列表长度的内置函数叫做len,如果我输入len(x),那么会返回数字6,因为我的列表中有 6 个数字。

只是为了确保,再次强调这实际上是在运行真正的代码,让我们在那里再添加一个数字,比如4545。如果你运行这个,你会得到7,因为现在列表中有 7 个数字:

x = [1, 2, 3, 4, 5, 6, 4545]
print (len(x))

前面代码示例的输出如下:

7

回到原来的例子。现在你也可以对列表进行切片。如果你想要取列表的一个子集,有一个非常简单的语法可以做到:

x[3:]

上面代码示例的输出如下:

[1, 2, 3]

冒号之前

例如,如果你想要取列表的前三个元素,即第 3 个元素之前的所有东西,我们可以说:3来获取前三个元素,123,如果你想想那里发生了什么,就是索引方面的事情,就像大多数语言一样,我们从 0 开始计数。所以第 0 个元素是1,第 1 个元素是2,第 2 个元素是3。因为我们说我们想要在第 3 个元素之前的所有东西,这就是我们得到的。

所以,你知道,在大多数语言中,你从 0 开始计数,而不是 1。

现在这可能会让事情变得混乱,但在这种情况下,它确实是直观的。你可以把冒号理解为我想要所有东西,我想要前三个元素,我可以再次将其更改为四,以再次说明我们实际上正在做一些真实的事情:

x[:4]

上面代码示例的输出如下:

[1, 2, 3, 4]

冒号之后

现在如果我把冒号放在3的另一侧,那就表示我想要3之后的所有东西,所以3和之后的。如果我说x[3:],那就是给我第三个元素,0,1,2,3,以及之后的所有东西。所以在这个例子中会返回 4,5 和 6,明白吗?

x[3:]

输出如下:

[4, 5, 6]

你可能想要保留这个 IPython/Jupyter Notebook 文件。这是一个很好的参考,因为有时候会让人困惑,不知道切片操作符是否包括该元素,或者是到或包括它,还是不包括。所以最好的方法就是在这里玩一下,提醒自己。

负数语法

你还可以使用这种负数语法:

x[-2:]

上面代码的输出如下:

[5, 6]

通过说x[-2:],这意味着我想要列表中的最后两个元素。这意味着从末尾向后退两个,这将给我56,因为这些是列表中的最后两个元素。

添加列表到列表

你还可以改变列表。比如说我想要把一个列表添加到另一个列表中。我可以使用extend函数来做到这一点,如下面的代码块所示:

x.extend([7,8])
x

上面代码的输出如下:

[1, 2, 3, 4, 5, 6, 7, 8]

我有一个列表123456。如果我想要扩展它,我可以说我有一个新列表在这里,[7, 8],那个方括号表示这本身就是一个新列表。这可能是一个隐式的列表,在那里,它可以被另一个变量引用。你可以看到,一旦我这样做了,我得到的新列表实际上是在原来的列表上附加了78的列表。所以我通过扩展另一个列表来得到一个新列表。

添加函数

如果你只想在列表中再添加一些东西,你可以使用append函数。所以我只想在末尾加上数字9,就这样:

x.append(9)
x

上面代码的输出如下:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

复杂的数据结构

你还可以使用列表创建复杂的数据结构。所以你不只是可以把数字放进去;你实际上可以把字符串放进去。你可以把数字放进去。你可以把其他列表放进去。都没关系。Python 是一种弱类型语言,所以你基本上可以把任何类型的数据放到任何地方,通常都是可以的:

y = [10, 11, 12]
listOfLists = [x, y]
listOfLists

在上面的例子中,我有一个包含101112的第二个列表,我称之为y。我将创建一个包含两个列表的新列表。这对你来说是不是很惊人?我们的listofLists列表将包含x列表和y列表,这是完全有效的。你可以看到这里有一个括号表示listofLists列表,而在其中,我们有另一组括号表示该列表中的每个单独的列表:

[[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ], [10, 11, 12]]

所以,有时这样的东西会派上用场。

取消引用单个元素

如果你想取消引用列表的单个元素,你可以像这样使用括号:

y[1]

上面代码的输出如下:

11

所以y[1]将返回元素1。记住y中有101112 - 观察上面的例子,我们从 0 开始计数,所以元素 1 实际上是列表中的第二个元素,或者在这种情况下是数字11,好吗?

排序函数

最后,让我们来看一个内置的排序函数,你可以使用它:

z = [3, 2, 1]
z.sort()
z

所以如果我从列表z开始,它是321,我可以在该列表上调用排序,然后z现在将按顺序排序。上面代码的输出如下:

[1, 2, 3]

反向排序

z.sort(reverse=True)
z

上面代码的输出如下:

[3, 2, 1]

如果你需要进行反向排序,你可以在sort函数中添加一个参数reverse=True,这将使它恢复到321

如果你需要让这个概念沉淀一下,可以随意回去再读一下。

元组

元组就像列表一样,只是它们是不可变的,所以你实际上不能扩展、追加或排序它们。它们就是它们,除了你不能改变它们,并且你用括号而不是方括号表示它们是不可变的元组,它们的行为就像列表一样。所以你可以看到它们在其他方面基本上是一样的:

#Tuples are just immutable lists. Use () instead of []
x = (1, 2, 3)
len(x)

上面代码的输出如下:

3

我们可以说x=(1,2,3)。我仍然可以在上面使用length - len来说这个元组中有三个元素,即使,如果你不熟悉术语元组,一个元组实际上可以包含任意多个元素。尽管它听起来像是基于数字三的拉丁语,但这并不意味着你在其中有三个东西。通常,它只有两个东西。它们可以有任意多个,真的。

取消引用一个元素

我们还可以对元组的元素进行取消引用,所以第 2 个元素再次是第三个元素,因为我们从 0 开始计数,这将在下面的截图中给我返回数字6

y = (4, 5, 6)
y[2]

上面代码的输出如下:

6

元组列表

我们也可以像对待列表一样,使用元组作为列表的元素。

listOfTuples = [x, y]
listOfTuples

上面代码的输出如下:

[(1, 2, 3), (4, 5, 6)]

我们可以创建一个包含两个元组的新列表。所以在上面的例子中,我们有我们的x元组(1,2,3)和我们的y元组(4,5,6);然后我们将这两个元组放入一个列表中,我们得到了这样的结构,其中我们有方括号表示包含两个由括号表示的元组的列表,元组在我们进行数据科学或任何数据管理或处理时通常用于将变量分配给输入数据。我想向你解释一下下面例子中发生了什么:

(age, income) = "32,120000".split(',')
print (age)
print (income)

上面代码的输出如下:

32
120000

假设我们有一行输入数据进来,它是一个逗号分隔的值文件,其中包含年龄,比如32,用逗号分隔的收入,比如120000,只是为了举个例子。当每行数据进来时,我可以调用split函数来将其分隔成由逗号分隔的一对值,并将split的结果元组一次性分配给两个变量-ageincome,通过定义一个年龄、收入的元组,并说我想将其设置为split函数的结果元组。

所以这基本上是你会看到的一种常见的简写,用于一次性为多个变量分配多个字段。如果我运行它,你会看到age变量实际上被分配为32income被分配为120,000,因为有这个小技巧。当你这样做的时候,你需要小心,因为如果你没有预期的字段数量或者结果元组中的预期元素数量,如果你尝试分配更多或更少的东西,你会得到一个异常。

字典

最后,我们将在 Python 中经常看到的最后一个数据结构是字典,你可以把它看作是其他语言中的映射或哈希表。这基本上是一种在 Python 中内置的一种方式,可以有一种类似于键/值数据存储的迷你数据库。所以假设我想建立一个小小的星际迷航飞船和他们的船长的字典:

我可以设置captains = {},花括号表示一个空字典。现在我可以使用这种语法来为字典中的条目赋值,所以我可以说captainsEnterpriseKirk,为Enterprise DPicard,为Deep Space NineSisko,为VoyagerJaneway。现在我基本上有了这个查找表,它将船名与船长关联起来,例如,我可以说print captains["Voyager"],我会得到Janeway

这基本上是一种非常有用的工具,可以用来做某种查找。假设你在数据集中有某种标识符,它映射到一些可读的名称。当你打印出来时,你可能会使用字典来进行实际的查找。

我们还可以看看如果尝试查找不存在的东西会发生什么。嗯,我们可以在字典上使用get函数来安全地返回一个条目。所以在这种情况下,Enterprise在我的字典中有一个条目,它只是给我Kirk,但如果我在字典上调用NX-01船,我从来没有定义过那个船的船长,所以在这个例子中它会返回一个None值,这比抛出异常要好,但你需要意识到这是可能的:

print (captains.get("NX-01"))

上面代码的输出如下:

None

船长是乔纳森·阿彻,但你知道,我现在有点太迷恋了。

遍历条目

for ship in captains:
     print (ship + ": " + captains[ship])

上面代码的输出如下:

让我们来看一个遍历字典条目的小例子。如果我想要遍历我字典中的每艘船,并打印出captains,我可以输入for ship in captains,这将遍历字典中的每个键。然后我可以打印出每艘船船长的查找值,这就是我得到的输出。

就是这样。这基本上是你在 Python 中会遇到的主要数据结构。还有其他一些,比如集合,但我们在这本书中不会真正使用它们,所以我认为这已经足够让你开始了。让我们在下一节中深入了解一些 Python 的细微差别。

Python 基础-第二部分

除了Python 基础-第一部分,现在让我们试着更详细地掌握更多 Python 概念。

Python 中的函数

让我们谈谈 Python 中的函数。与其他语言一样,你可以有函数让你重复一组操作,只是参数不同。在 Python 中,做到这一点的语法看起来像这样:

def SquareIt(x):
    return x * x
print (SquareIt(2))

上面代码的输出如下:

4

你使用def关键字声明一个函数。它只是说这是一个函数,我们将称这个函数为SquareIt,然后在括号内跟着参数列表。这个特定的函数只接受一个我们将称为x的参数。再次记住,在 Python 中空白很重要。这个函数不会有任何花括号或者其他东西来包围它。它完全由空白定义。所以我们有一个冒号,它表示这个函数声明行结束了,但是它是通过一个或多个制表符来告诉解释器我们实际上在SquareIt函数内部。

所以def SquareIt(x): tab 返回x * x,这将返回这个函数中x的平方。我们可以试一下。print squareIt(2)就是我们调用这个函数的方式。它看起来就像在任何其他语言中一样。这应该返回数字4;我们运行代码,事实上确实是这样的。太棒了!这很简单,这就是函数的全部。显然,如果我愿意,我可以有多个参数,甚至需要多少个参数都可以。

现在在 Python 中有一些奇怪的事情你可以做,这些事情有点酷。你可以像传递参数一样传递函数。让我们仔细看看这个例子:

#You can pass functions around as parameters
def DoSomething(f, x):
    return f(x)
print (DoSomething(SquareIt, 3))

前面代码的输出如下:

9

现在我有一个名为DoSomething的函数,def DoSomething,它将接受两个参数,一个我将称为f,另一个我将称为x,如果我愿意,我实际上可以为其中一个参数传递一个函数。所以,请思考一分钟。看看这个例子,更有意义。在这里,DoSomething(f,x):将返回fx;它基本上会调用 f 函数并将 x 作为参数传递进去,Python 中没有强类型,所以我们必须确保我们为第一个参数传递的是一个函数,这样才能正常工作。

例如,我们将打印DoSomething,并且对于第一个参数,我们将传入SquareIt,这实际上是另一个函数,以及数字3。这应该做的是使用SquareIt函数和3参数做一些事情,这将返回(SquareIt, 3),我上次检查的时候,3 的平方是 9,确实是这样的。

这可能对你来说是一个新概念,将函数作为参数传递,所以如果你需要停下来一分钟,等一下,让它沉淀下来,玩弄一下,请随意这样做。再次,我鼓励你停下来,按照自己的步调学习。

Lambda 函数 - 函数式编程

还有一件事,这是一种 Python 式的做法,你可能在其他语言中看不到,那就是 lambda 函数的概念,它有点叫做函数式编程。这个想法是你可以在一个函数中包含一个简单的函数。通过一个例子来解释会更有意义:

#Lambda functions let you inline simple functions
print (DoSomething(lambda x: x * x * x, 3))

上面代码的输出如下:

27

我们将打印DoSomething,记住我们的第一个参数是一个函数,所以我们可以使用lambda关键字内联声明这个函数,而不是传递一个命名函数。Lambda 基本上意味着我正在定义一个暂时存在的未命名函数。它是瞬时的,并且它接受一个参数x。在这里的语法中,lambda意味着我正在定义某种内联函数,后面跟着它的参数列表。它有一个参数x,然后是冒号,然后是这个函数实际上要做的事情。我将取x参数并将其自身乘以三次,基本上得到参数的立方。

在这个例子中,DoSomething将这个 lambda 函数作为第一个参数传递进去,它计算x的立方和3参数。那么在幕后这实际上是在做什么呢?这个lambda函数本身是一个函数,在前面的例子中被传递到DoSomething中的f,而这里的x将是3。这将返回xf,最终执行我们的 lambda 函数在值3上。所以这个3进入我们的x参数,我们的 lambda 函数将其转换为3乘以3乘以3,当然是27

当我们开始做 MapReduce 和 Spark 等工作时,这种情况经常出现。因此,如果以后要处理 Hadoop 等技术,这是一个非常重要的概念。再次,我鼓励您花点时间让它沉淀下来,理解发生了什么,如果需要的话。

理解布尔表达式

布尔表达式语法有点奇怪或不寻常,至少在 Python 中是这样:

print (1 == 3)

上面代码的输出如下:

False

像往常一样,我们有双等号符号,可以测试两个值之间的相等性。所以1等于3吗,不,因此False。值False是由 F 指定的特殊值。请记住,在测试时,当您在进行布尔运算时,相关的关键字是TrueFalse。这与我之前使用过的其他语言有点不同,所以请记住这一点。

print (True or False)

上面代码的输出如下:

True

嗯,TrueFalseTrue,因为其中一个是True,你运行它,它会返回True

if 语句

print (1 is 3)

上面代码的输出如下:

False

我们还可以使用is,它与等号的作用类似。这是一种更 Python 风格的相等表示,所以1 == 31 is 3是一样的,但这被认为是更 Pythonic 的方式。因此1 is 3返回False,因为1不是3

if-else 循环

if 1 is 3:
    print "How did that happen?"
elif 1 > 3:
    print ("Yikes")
else:
    print ("All is well with the world")

上面代码的输出如下:

All is well with the world

我们还可以在这里使用if-elseelse-if块。让我们在这里做一些更复杂的事情。如果1 是 3,我会打印怎么会发生这种事?但当然1不是3,所以我们将回到else-if块,否则,如果1不是3,我们将测试1>3。好吧,那也不对,但如果是的话,我们打印天哪,最后我们将进入这个万能的else子句,它将打印世界一切安好

实际上,1不是31也不大于3,确实,世界一切安好。所以,你知道,其他语言有非常相似的语法,但这些是 Python 的特点,以及如何做if-elseelse-if块。所以,随时保留这个笔记本。以后可能会成为一个很好的参考。

循环

我想在我们的 Python 基础知识中涵盖的最后一个概念是循环,我们已经看到了一些例子,但让我们再做一个:

for x in range(10):
 print (x),

上面代码的输出如下:

0 1 2 3 4 5 6 7 8 9

例如,我们可以使用这个范围运算符自动定义一个数字范围的列表。所以如果我们说for x in range(10)range 10将产生一个09的列表,通过在该列表中说for x,我们将遍历该列表中的每个条目并打印出来。再次强调,print语句后的逗号表示不要给我一个新行,继续进行。因此,这样的输出最终是该列表的所有元素打印在一起。

要做一些更复杂的事情,我们将做类似的事情,但这次我们将展示continuebreak的工作原理。与其他语言一样,您实际上可以选择跳过循环迭代的其余处理,或者提前停止循环的迭代:

for x in range(10):
    if (x is 1):
 continue
 if (x > 5):
    break
 print (x),

上面代码的输出如下:

0 2 3 4 5

在这个例子中,我们将遍历 0 到 9 的值,如果我们遇到数字 1,我们将在打印它之前继续。我们将跳过数字 1,基本上,如果数字大于5,我们将中断循环并完全停止处理。我们期望的输出是,我们将打印出数字05,除非是1,在这种情况下,我们将跳过数字1,确实,这就是它的作用。

while 循环

另一种语法是 while 循环。这是大多数语言中都能看到的一种标准循环语法:

x = 0
while (x < 10):
    print (x),
    x += 1

上述代码的输出如下:

0 1 2 3 4 5 6 7 8 9

我们还可以说,从x = 0开始,然后while (x < 10):,打印它,然后将x增加1。这将一遍又一遍地进行,递增 x 直到小于 10 为止,此时我们跳出while循环并完成。所以它和这里的第一个例子做的事情是一样的,只是以不同的风格。它使用while循环打印出数字09。只是一些例子,没有太复杂的东西。再次,如果你之前做过任何编程或脚本,这应该很简单。

现在为了让这个概念真正深入人心,我一直在整个章节中说,去尝试,动手去做,玩一下。所以我要让你这样做。

探索活动

这里有一个活动,对你来说有点挑战:

这是一个很好的代码块,你可以开始编写你自己的 Python 代码,运行它,并玩耍,所以请这样做。你的挑战是编写一些代码,创建一个整数列表,循环遍历该列表的每个元素,到目前为止都很容易,然后只打印出偶数。

现在这不应该太难。这本笔记本中有做所有这些事情的例子;你所要做的就是把它们放在一起并让它们运行起来。所以,重点不是给你一些难的东西。我只是想让你真的对编写自己的 Python 代码并实际运行它并看到它运行有信心,所以请这样做。我绝对鼓励你在这里进行互动。所以加油,祝你好运,欢迎来到 Python。

所以这就是你的 Python 速成课程,显然,只是一些非常基本的东西。随着我们在整本书中越来越多的例子,它会变得越来越有意义,因为你有更多的例子可以参考,但如果你现在感到有点害怕,也许你对编程或脚本有点太新了,那么在继续之前可能最好先进行一次 Python 复习,但如果你对到目前为止看到的东西感到相当满意,让我们继续前进,我们将继续前进。

运行 Python 脚本

在整本书中,我们将使用 IPython/Jupyter 笔记本格式(即.ipynb文件),这是一个很好的格式,因为它让我可以在里面放一些代码块,并加一些文字和解释它在做什么,你可以实时尝试一些东西。

当然,从这个角度来看,这很棒,但在现实世界中,你可能不会真的使用 IPython/Jupyter 笔记本来在生产中运行你的 Python 脚本,所以让我简要地介绍一下你可以运行 Python 代码的其他方式,以及其他交互式运行 Python 代码的方式。所以这是一个相当灵活的系统。让我们来看看。

不仅仅是 IPython/Jupyter 笔记本

我想确保你知道有多种运行 Python 代码的方式。现在,在整本书中,我们将使用 IPython/Jupyter 笔记本格式,但在现实世界中,你不会将你的代码作为笔记本来运行。你将把它作为一个独立的 Python 脚本来运行。所以我只是想确保你知道如何做到这一点并看看它是如何工作的。

所以让我们回到我们在书中运行的第一个例子,只是为了说明空格的重要性。我们可以从笔记本格式中选择并复制代码,然后粘贴到一个新文件中。

这可以通过点击最左边的“新建”按钮来完成。所以让我们创建一个新文件,粘贴进去,然后保存这个文件,命名为test.py,其中py是我们给 Python 脚本通常使用的扩展名。现在,我可以以几种不同的方式运行它。

在命令提示符中运行 Python 脚本

我实际上可以在命令提示符中运行脚本。如果我去工具,我可以选择 Canopy 命令提示符,这将打开一个命令窗口,其中已经设置好了运行 Python 所需的所有必要的环境变量。我只需输入python test.py并运行脚本,结果就出来了:

所以在现实世界中,你可能会做类似的事情。可能是在 Crontab 上或其他地方,谁知道呢?但在生产中运行一个真正的脚本就是这么简单。现在你可以关闭命令提示符。

使用 Canopy IDE

回到 IDE,我也可以在 IDE 中运行脚本。所以在 Canopy 中,我可以去运行菜单。我可以选择运行文件,或者点击小播放图标,这也会执行我的脚本,并在输出窗口底部看到结果,如下图所示:

这是另一种方法,最后,你也可以在底部的交互式提示符中运行脚本。我实际上可以逐个输入 Python 命令,并让它们在环境中执行并保留在那里:

例如,我可以说stuff,将其作为list调用,并有1234,现在我可以说len(stuff),这将给我4

我可以说,for x in stuff:print x,我们得到的输出是1 2 3 4

所以你可以看到,你可以在底部的交互式提示符中逐步制作脚本并逐个执行。在这个例子中,stuff是我们创建的一个变量,一个保留在内存中的列表,它有点像其他语言中的全局变量在这个环境中。

现在如果我想要重置这个环境,如果我想要摆脱stuff并重新开始,你可以这样做,你可以在这里点击运行菜单,然后选择重新启动内核,这将给你一个空白的状态:

所以现在我有一个新的 Python 环境,是一个干净的状态,这种情况下,我叫它什么来着?输入stuffstuff还不存在,因为我有一个新的环境,但我可以把它变成其他东西,比如[4, 5, 6];运行它,就是这样:

所以你看到了,有三种运行 Python 代码的方式:IPython/Jupyter Notebook,我们将在本书中使用它,因为它是一个很好的学习工具,你也可以作为独立的脚本文件运行脚本,也可以在交互式命令提示符中执行 Python 代码。

所以你看到了,你有三种不同的方式来运行 Python 代码和在生产中进行实验和运行。记住这一点。在本书的其余部分中,我们将使用笔记本,但当时机到来时,你还有其他选择。

总结

在本章中,我们开始了我们的旅程,建立了本书最重要的基石 - 安装 Enthought Canopy。然后我们继续安装其他库和不同类型的软件包。我们还借助各种 Python 代码掌握了一些 Python 的基础知识。我们涵盖了模块、列表以及元组等基本概念,最终更深入地了解了 Python 的基础知识,包括函数和循环。最后,我们开始运行一些简单的 Python 脚本。

在下一章中,我们将继续了解统计和概率的概念。

第二章:统计和概率复习,以及 Python 实践

在本章中,我们将介绍一些统计和概率的概念,这对于一些人来说可能是复习。如果您想成为一名数据科学家,这些概念很重要。我们将看到一些示例,以更好地理解这些概念。我们还将看看如何使用实际的 Python 代码来实现这些示例。

本章我们将涵盖以下主题:

  • 您可能会遇到的数据类型以及如何相应处理它们

  • 统计概念的均值、中位数、众数、标准差和方差

  • 概率密度函数和概率质量函数

  • 数据分布类型及如何绘制它们

  • 理解百分位数和矩

数据类型

好了,如果您想成为一名数据科学家,我们需要讨论您可能会遇到的数据类型,如何对其进行分类以及如何可能以不同方式对待它们。让我们深入了解您可能会遇到的不同类型的数据:

这似乎很基础,但我们必须从简单的东西开始,然后逐步深入研究更复杂的数据挖掘和机器学习内容。了解您正在处理的数据类型非常重要,因为不同的技术可能会根据您处理的数据类型有不同的细微差别。因此,可以说数据有几种不同的类型,我们将主要关注其中的三种。它们是:

  • 数值数据

  • 分类数据

  • 有序数据

同样,对于不同类型的数据,可能会使用不同的技术变体,因此在分析数据时,您始终需要牢记您正在处理的数据类型。

数值数据

让我们从数值数据开始。这可能是最常见的数据类型。基本上,它代表一些可以测量的可量化的东西。一些例子是人的身高、页面加载时间、股票价格等。变化的东西,您可以测量的东西,具有广泛可能性的东西。现在基本上有两种数值数据,所以可以说是一种变体的变体。

离散数据

有离散数据,基于整数,例如某种事件的计数。一些例子是客户一年内购买了多少次。这只能是离散值。他们买了一件东西,或者他们买了两件东西,或者他们买了三件东西。他们不可能买了 2.25 件或三个四分之三的东西。这是一个具有整数限制的离散值。

连续数据

另一种数值数据是连续数据,这是一种具有无限可能性范围的数据,可以进入分数。例如,回到人的身高,有无限可能的身高。您可能身高五英尺十点三七六二五英寸,或者做某事的时间,例如在网站上结账可能有任意巨大范围的可能性,可能是 10.7625 秒,或者一天内的降雨量。同样,这里有无限的精度。这就是连续数据的一个例子。

总之,数值数据是您可以用数字量化地测量的东西,它可以是离散的,例如基于事件计数的整数,也可以是连续的,其中您可以对该数据有无限范围的精度。

分类数据

我们将讨论的第二种数据类型是分类数据,这是没有固有数值含义的数据。

大多数时候,您实际上无法直接比较一个类别和另一个类别。例如性别、是/否问题、种族、居住州、产品类别、政党;您可以为这些类别分配数字,通常您会这样做,但这些数字没有固有含义。

所以,例如,我可以说德克萨斯的面积大于佛罗里达的面积,但我不能只是说德克萨斯大于佛罗里达,它们只是类别。它们没有真正的数值可量化的意义,只是我们根据类别对不同的事物进行分类的方式。

再次,我可能对每个州有某种数值的指定。我的意思是,我可以说佛罗里达是第 3 州,德克萨斯是第 4 州,但 3 和 4 之间没有真正的关系,对吧,这只是更紧凑地表示这些类别的一种简便方法。所以,分类数据没有任何固有的数值意义;它只是一种你选择根据类别来分割数据集的方式。

有序数据

你通常听到的最后一种数据类型是有序数据,它是数值和分类数据的一种混合。一个常见的例子是电影或音乐的星级评价,或其他什么的。

在这种情况下,我们有分类数据,可以是 1 到 5 颗星,其中 1 可能代表差,5 可能代表优秀,但它们确实有数学意义。我们知道 5 意味着比 1 好,所以这是一种数据,其中不同的类别之间有数值关系。所以,我可以说 1 颗星小于 5 颗星,我可以说 2 颗星小于 3 颗星,我可以说 4 颗星在质量上大于 2 颗星。现在你也可以把实际的星数看作是离散数值数据。所以,这些类别之间的界限确实很微妙,在很多情况下你实际上可以互换对待它们。

所以,这就是三种不同类型。有数值、分类和有序数据。让我们看看它是否已经深入人心。别担心,我不会让你交作业或者什么的。

快速测验:对于这些例子中的每一个,数据是数值的、分类的还是有序的?

  1. 让我们从你的油箱里有多少汽油开始。你觉得呢?嗯,正确的答案是数字。这是一个连续的数值,因为你的油箱里可能有无限的汽油可能性。我的意思是,是的,你可能有多少汽油可以装进去,但是你有多少汽油的可能值是没有尽头的。它可能是油箱的四分之三,可能是油箱的十六分之七,可能是油箱的1/π,谁知道呢?

  2. 如果你在 1 到 4 的范围内评估你的整体健康状况,其中这些选择对应于差、中等、良好和优秀的类别,你觉得呢?这是有序数据的一个很好的例子。这非常类似于我们的电影评分数据,再次取决于你如何对待它,你可能也可以把它当作离散数值数据,但从技术上讲,我们将把它称为有序数据。

  3. 你的同学的种族呢?这是一个很明显的分类数据的例子。你不能真正比较紫色的人和绿色的人,对吧,他们只是紫色和绿色,但它们是你可能想要研究和了解在某些其他维度上的差异的类别。

  4. 你的同学的年龄呢?这有点是个陷阱问题;如果我说它必须是整数年龄,比如 40、50 或 55 岁,那就是离散数值数据,但如果我有更多的精度,比如 40 年 3 个月 2.67 天,那就是连续数值数据,但无论如何,它都是数值数据类型。

  5. 最后,商店里花费的钱。同样,这可能是连续数值数据的一个例子。所以,这只是重要的原因是你可能会对不同类型的数据应用不同的技术。

也许有一些概念,我们对分类数据和数值数据采用不同类型的实现,例如。

这就是你需要了解的关于你通常会发现的不同类型的数据,以及我们在本书中将重点关注的内容。它们都是非常简单的概念:你有数值、分类和顺序数据,数值数据可以是连续的或离散的。根据你处理的数据类型,可能会有不同的技术应用到数据中,我们将在本书中看到这一点。让我们继续。

均值、中位数和众数

让我们来进行一次统计学 101 的复习。这就像小学的东西,但是再次经历一遍并看看这些不同的技术是如何使用的是很好的:均值、中位数和众数。我相信你以前听过这些术语,但看看它们是如何不同地使用是很好的,所以让我们深入研究一下。

这对大多数人来说应该是一个复习,一个快速的复习,现在我们开始真正地深入一些实际的统计数据。让我们看看一些实际的数据,然后找出如何测量这些数据。

均值

均值,你可能知道,只是平均值的另一个名称。要计算数据集的均值,你所要做的就是把所有值加起来,然后除以你拥有的值的数量。

样本总和/样本数量

让我们以同样的数据集来计算我社区每个房子的平均孩子数量。

假设我在我的社区挨家挨户地问每个人,他们家有多少孩子。 (顺便说一句,这是离散数值数据的一个很好的例子;还记得前一节吗?)假设我四处走动,发现第一家没有孩子,第二家有两个孩子,第三家有三个孩子,依此类推。我积累了这些离散数值数据的小数据集,为了计算均值,我所要做的就是把它们全部加起来,然后除以我去过的房子的数量。

我街上每个房子的孩子数量:

0, 2, 3, 2, 1, 0, 0, 2, 0

均值是(0+2+3+2+1+0+0+2+0)/9 = 1.11

结果是 0 加 2 加 3 加所有其他数字除以我看过的房子的总数,即 9,我样本中每个房子的平均孩子数量是 1.11。所以,这就是均值。

中位数

中位数有点不同。计算数据集的中位数的方法是通过对所有值进行排序(无论是升序还是降序),并取中间的那个值。

因此,例如,让我们使用我社区孩子的相同数据集

0, 2, 3, 2, 1, 0, 0, 2, 0

我会按数字顺序排列,然后取出数据中间的数字,结果是 1。

0, 0, 0, 0, 1, 2, 2, 2, 3

同样,我所要做的就是取数据,按数字顺序排列,然后取中间的点。

如果数据点的数量是偶数,那么中位数可能会落在两个数据点之间。不清楚哪一个实际上是中间的。在这种情况下,你所要做的就是取两个中间的数的平均值,并将该数字视为中位数。

异常值的因素

现在,在前面的例子中,每个家庭的孩子数量,中位数和均值非常接近,因为没有太多的异常值。我们有 0、1、2 或 3 个孩子,但我们没有一些有 100 个孩子的疯狂家庭。那会使均值受到很大的影响,但可能不会太大地改变中位数。这就是为什么中位数通常是一个非常有用的东西,经常被忽视。

中位数比均值更不容易受到异常值的影响。

有时人们倾向于用统计数据误导人。我会在整本书中尽可能地指出这一点。

例如,你可以谈论美国的平均家庭收入,去年我查到的实际数字大约是 72000 美元,但这并不能真正准确地反映出典型的美国人的收入。这是因为,如果你看中位数收入,它要低得多,只有 51939 美元。为什么呢?因为收入不平等。美国有一些非常富有的人,其他很多国家也是如此。美国甚至不是最糟糕的,但你知道那些亿万富翁,那些住在华尔街或硅谷或其他一些非常富有的地方的超级富人,他们会使平均值偏离。但他们数量很少,所以他们并不会对中位数产生太大影响。

这是一个很好的例子,中位数比平均数更好地反映了这个例子中典型的人或数据点的情况。每当有人谈论平均数时,你必须考虑数据分布是什么样子。是否有可能会使平均数偏离的异常值?如果答案可能是肯定的,你也应该要求中位数,因为通常情况下,中位数提供的洞察力比平均数更多。

众数

最后,我们将讨论众数。实际上在实践中这并不经常出现,但你不能谈论均值和中位数而不谈论众数。众数的意思就是数据集中最常见的值。

让我们回到我关于每个房子中孩子数量的例子。

0、2、3、2、1、0、0、2、0

每个值有多少个:

0: 4, 1: 1, 2: 3, 3: 1

众数是 0

如果我只看最频繁出现的数字,结果是 0,因此这组数据的众数是 0。在这个社区中,一个房子里孩子的最常见数量是没有孩子,这就是它的含义。

现在这实际上是一个很好的连续与离散数据的例子,因为这只适用于离散数据。如果我有一系列连续的数据,那么我就无法谈论最常出现的值,除非我以某种方式将其量化为离散值。所以我们已经遇到了一个数据类型很重要的例子。

众数通常只与离散数值数据相关,而不与连续数据相关。

很多现实世界的数据往往是连续的,所以也许这就是为什么我不太听到关于众数的事情,但我们在这里看到了它的完整性。

就是这样:均值、中位数和众数的概要。这可能是你能做的最基本的统计工作,但我希望你在选择中位数和均值的重要性上得到了一点复习。它们可以讲述非常不同的故事,但人们往往在头脑中将它们等同起来,所以确保你是一个负责任的数据科学家,并以传达你试图代表的含义的方式代表数据。如果你试图显示一个典型值,通常中位数比平均数更好,因为异常值,所以记住这一点。让我们继续。

在 Python 中使用均值、中位数和众数

让我们开始在 Python 中进行一些真正的编码,看看如何使用 Python 在 IPython Notebook 文件中计算均值、中位数和众数。

所以,如果你想跟着做的话,可以打开本节数据文件中的MeanMedianMode.ipynb文件,我绝对鼓励你这样做。如果你需要回到之前关于从哪里下载这些材料的部分,请去做,因为你将需要这些文件来完成本节。让我们开始吧!

使用 NumPy 包计算均值

我们要做的是创建一些虚假的收入数据,回到上一节的例子。在这个例子中,我们将创建一些虚假数据,其中典型的美国人在这个例子中每年赚大约$27,000,我们将说这是以正态分布和标准差为 15,000 分布的。所有的数字都是完全虚构的,如果你还不知道正态分布和标准差是什么意思,不用担心。我会在本章稍后介绍,但我只是想让你知道这个例子中这些不同参数代表什么。以后会有意义的。

在我们的 Python 笔记本中,记得将 NumPy 包导入 Python,这样计算平均值、中位数和众数就变得非常容易。我们将使用import numpy as np指令,这意味着我们可以使用np作为调用numpy的简写。

然后我们将使用np.random.normal函数创建一个名为incomes的数字列表。

import numpy as np 

incomes = np.random.normal(27000, 15000, 10000) 
np.mean(incomes) 

np.random.normal函数的三个参数表示我希望数据以27000为中心,标准差为15000,并且我希望 Python 在这个列表中生成10000个数据点。

一旦我这样做了,我就可以通过在incomes上调用np.mean来计算这些数据点的平均值,或者说是平均值。就是这么简单。

让我们继续运行。确保你选择了那个代码块,然后你可以点击播放按钮来执行它,由于这些收入数字有一个随机成分,每次我运行它,我都会得到一个略微不同的结果,但它应该总是接近27000

Out[1]: 27173.098561362742

好的,这就是在 Python 中计算平均值的全部内容,只需使用 NumPy(np.mean)就可以轻松搞定。你不必写一堆代码,或者实际上把所有东西加起来,计算出你有多少项,然后做除法。NumPy mean,为你做了所有这些。

使用 matplotlib 可视化数据

让我们可视化这些数据,以使它更加有意义。还有另一个叫做matplotlib的包,我们以后也会更多地谈论它,但它是一个让我在 IPython 笔记本中制作漂亮图形的包,所以这是一种简单的方式来可视化你的数据并了解发生了什么。

在这个例子中,我们使用matplotlib创建了一个包含50个不同桶的收入数据的直方图。所以基本上,我们将我们的连续数据离散化,然后我们可以在matplotlib.pyplot上调用 show 来实际显示这个直方图。参考以下代码:

%matplotlib inline 
import matplotlib.pyplot as plt 
plt.hist(incomes, 50) 
plt.show() 

继续选择代码块并点击播放。它将为我们创建一个新的图表。

如果你不熟悉直方图或者需要复习一下,解释这个的方法是,我们将数据离散化为每个桶,显示了该数据的频率。

例如,大约在 27,000 左右,我们看到每个给定值范围内大约有600个数据点。在 27,000 左右有很多人,但当你到达80,000这样的异常值时,就没有那么多了,显然有一些可怜的人甚至负债-40,000,但是他们很少,不太可能,因为我们定义了一个正态分布,这就是正态概率曲线的样子。我们以后会更详细地谈论这个,但我只是想让你知道这个想法,如果你还不知道的话。

使用 NumPy 包计算中位数

好的,计算中位数就像计算平均值一样简单。就像我们有 NumPy 的mean一样,我们也有一个 NumPy 的median函数。

我们可以在我们的数据列表incomes上使用median函数,这将给我们中位数。在这种情况下,中位数是$26,911,与均值$26988\相差不大。同样,初始数据是随机的,所以你的值会略有不同。

np.median(incomes) 

以下是前面代码的输出:

Out[4]: 26911.948365056276 

我们不希望看到很多异常值,因为这是一个很好的正态分布。当你没有很多奇怪的异常值时,中位数和均值是可以比较的。

分析异常值的影响

为了证明一点,让我们加入一个异常值。我们来加入唐纳德·特朗普;我认为他算是一个异常值。让我们继续添加他的收入。所以我将手动使用np.append将这个数据添加到数据中,假设添加 10 亿美元(这显然不是唐纳德·特朗普的实际收入)到收入数据中。

incomes = np.append(incomes, [1000000000]) 

我们将看到的是,这个异常值并没有真正改变中位数很多,你知道,它仍然会在大约相同的值$26,911 左右,因为我们实际上并没有改变中间点在哪里,只是在下面的例子中显示了一个值:

np.median(incomes) 

这将输出以下内容:

Out[5]: 26911.948365056276 

这给出了一个新的输出:

np.mean(incomes) 

以下是前面代码的输出:

Out[5]:127160.38252311043 

啊哈,就是这样!这是一个很好的例子,说明了中位数和均值,尽管人们在日常语言中倾向于将它们等同起来,但它们可能非常不同,并讲述了一个非常不同的故事。因此,这一个异常值导致了这个数据集中的平均收入超过了每年 12.7 万美元,但更准确的情况是这个数据集中典型人的年收入接近 2.7 万美元。我们只是因为一个大的异常值而使均值偏离了。

故事的寓意是:如果你怀疑可能存在异常值,那么对于谈论均值或平均数的人要持怀疑态度,而收入分布显然就是这种情况。

使用 SciPy 包计算众数

最后,让我们来看看众数。我们将生成一堆随机整数,精确地说是 500 个,范围在1890之间。我们将为人们创建一堆虚假的年龄。

ages = np.random.randint(18, high=90, size=500) 
ages 

你的输出将是随机的,但应该看起来像以下的截图:

现在,SciPy,有点像 NumPy,是一堆方便的统计函数,所以我们可以使用以下语法从 SciPy 导入stats。这与我们之前看到的有点不同。

from scipy import stats 
stats.mode(ages) 

这段代码的意思是,从scipy包中导入stats,我只是用stats来引用这个包,这意味着我不需要像之前使用 NumPy 那样使用别名,只是另一种做法。两种方法都可以。然后,我在ages上使用了stats.mode函数,这是我们的随机年龄列表。当我们执行上面的代码时,我们得到了以下输出:

Out[11]: ModeResult(mode=array([39]), count=array([12])) 

所以在这种情况下,实际的众数是39,在数组中出现了12次。

现在,如果我真的创建一个新的分布,我会期望得到一个完全不同的答案,因为这些数据确实是完全随机的。让我们再次执行上面的代码块来创建一个新的分布。

ages = np.random.randint(18, high=90, size=500) 
ages 

from scipy import stats 
stats.mode(ages) 

随机化方程的输出如下分布:

确保你选择了那个代码块,然后你可以点击播放按钮来执行它。

在这种情况下,众数最终是数字29,出现了14次。

Out[11]: ModeResult(mode=array([29]), count=array([14])) 

所以,这是一个非常简单的概念。你可以再做几次,只是为了好玩。这有点像转动轮盘。我们将再次创建一个新的分布。

就是这样,均值、中位数和众数就是这样。使用 SciPy 和 NumPy 包非常简单。

一些练习

我将在本节中给你一个小作业。如果你打开MeanMedianExercise.ipynb文件,里面有一些你可以玩的东西。我希望你能动手尝试一下。

在文件中,我们有一些随机的电子商务数据。这些数据代表的是每笔交易的总金额,就像我们之前的例子一样,这只是一组数据的正态分布。我们可以运行它,你的作业是使用 NumPy 包找出这些数据的平均值和中位数。这几乎是你能想象到的最简单的作业。你需要的所有技巧都在我们之前使用的MeanMedianMode.ipynb文件中。

这里的重点并不是真的要挑战你,而是让你真正写一些 Python 代码,并让自己相信你实际上可以得到一个结果并让事情发生。所以,继续玩吧。如果你想再玩一会儿,可以随意玩一下这里的数据分布,看看你对数字有什么影响。尝试添加一些异常值,就像我们在收入数据中所做的那样。这是学习这些东西的方法:掌握基础知识,高级知识就会随之而来。尽情享受吧。

一旦你准备好了,让我们继续前进到我们的下一个概念,标准差和方差。

标准差和方差

让我们谈谈标准差和方差。这些概念和术语你可能以前听过,但让我们更深入地了解一下它们的真正含义以及如何计算它们。这是数据分布的分散程度的一种度量,几分钟后你就会更清楚了。

标准差和方差是数据分布的两个基本量,你将在本书中一遍又一遍地看到它们。所以,如果你需要温习一下,让我们看看它们是什么。

方差

让我们来看一个直方图,因为方差和标准差都与数据的分散程度、数据集的分布形状有关。看一下下面的直方图:

假设我们有一些关于飞机在机场到达频率的数据,例如,这个直方图表明我们大约每分钟有 4 次到达,我们观察到的数据中大约有 12 天出现了这种情况。然而,我们也有一些异常值。有一天到达速度非常慢,每分钟只有一次到达,还有一天到达速度非常快,几乎每分钟有 12 次到达。因此,读取直方图的方法是查找给定值的桶,并告诉您该值在数据中出现的频率,直方图的形状可以告诉您很多关于给定数据集的概率分布的信息。

我们从这些数据中知道,我们的机场很可能每分钟有大约 4 次到达,但很不可能有 1 次或 12 次到达,我们还可以具体讨论中间所有数字的概率。因此,不仅每分钟有 12 次到达的可能性很小,每分钟有 9 次到达的可能性也很小,但一旦我们开始接近 8 左右,事情就开始有点起色了。从直方图中可以得到很多信息。

方差衡量数据的分散程度

测量方差

我们通常将方差称为 sigma 平方,你马上就会知道为什么,但现在,只需知道方差是平均值与平方差的差值。

  1. 要计算数据集的方差,首先要找出它的平均值。假设我有一些数据,可以代表任何东西。比如说某个小时排队的最大人数。在第一个小时,我观察到有 1 个人在排队,然后是 4 个,然后是 5 个,然后是 4 个,然后是 8 个。

  2. 计算方差的第一步就是找到这些数据的均值或平均值。我把它们全部加起来,将总和除以数据点的数量,结果是 4.4,这是排队人数的平均数(1+4+5+4+8)/5=4.4

  3. 现在下一步是找到每个数据点与均值的差异。我知道均值是 4.4。所以对于我的第一个数据点,我有 1,所以1-4.4=-3.4,下一个数据点是 4,所以 4-4.4=-0.44-4.4=-0.4,依此类推。所以我得到这些正负数,代表每个数据点与均值的方差(-3.4,-0.4,0.6,-0.4,3.6)

  4. 现在我需要一个单一的数字来代表整个数据集的方差。因此,我接下来要做的是找到这些差异的平方。我将逐个计算这些与均值的原始差异的平方。这是出于几个不同的原因:

    • 首先,我要确保负方差和正方差一样重要。否则,它们将互相抵消。那就不好了。
  • 其次,我还想给异常值更多的权重,因此这会放大与均值非常不同的事物的影响,同时确保负数和正数是可比较的(11.56,0.16,0.36,0.16,12.96)

让我们看看那里发生了什么,所以(-3.4)²是一个正数 11.56,(-0.4)²最终是一个更小的数字,即 0.16,因为它更接近均值 4.4。同样(0.6)²结果接近均值,只有 0.36。但是当我们到达正的异常值时,(3.6)²最终是 12.96。这给了我们:(11.56,0.16,0.36,0.16,12.96)

要找到实际的方差值,我们只需取所有这些平方差的平均值。因此,我们将所有这些平方差相加,将总和除以 5,也就是我们拥有的值的数量,最终得到方差为 5.04。

好的,这就是方差的全部内容。

标准差

通常,我们谈论标准差比方差更多,结果标准差只是方差的平方根。就是这么简单。

因此,如果我有这个方差5.04,标准差就是2.24。所以你现在明白为什么我们说方差=(σ)²。因为σ本身代表标准差。因此,如果我取(σ)²的平方根,我得到σ。在这个例子中,结果是 2.24。

使用标准差识别异常值

这是我们在前面的示例中查看的实际数据的直方图,用于计算方差。

现在我们看到数字4在我们的数据集中出现了两次,然后我们有一个1,一个5,和一个8

标准差通常用作一种思考如何识别数据集中的异常值的方法。如果我说如果我在均值 4.4 的标准差内,那在正态分布中被认为是一种典型值。然而,你可以看到在前面的图表中,数字18实际上位于该范围之外。因此,如果我取 4.4 加减 2.24,我们得到大约72,而18都落在标准差范围之外。因此我们可以数学上说,1 和 8 是异常值。我们不必猜测和凭眼测量。现在仍然需要判断一个数据点与均值相比是多少标准差的异常值。

通常可以通过一个数据点与均值相差多少标准差(有时也可以是多少西格玛)来谈论一个数据点有多少异常值。

这就是标准差在现实世界中使用的一些情况。

总体方差与样本方差

标准差和方差有一点微妙之处,就是当你谈论总体与样本方差时。如果你在处理完整的数据集,一组完整的观察数据,那么你就按照我告诉你的做。你只需取平均值,从均值开始所有平方差的平均值就是你的方差。

然而,如果你在对数据进行抽样,也就是说,如果你只是取数据的一个子集来简化计算,你就要做一些不同的事情。你不是除以样本数,而是除以样本数减 1。让我们看一个例子。

我们将使用刚刚研究的排队人员的样本数据。我们将平方差的总和除以 5,也就是我们有的数据点的数量,得到 5.04。

σ² = (11.56 + 0.16 + 0.36 + 0.16 + 12.96) / 5 = 5.04

如果我们看样本方差,用 S²表示,它是由平方差的总和除以 4 得到的,也就是(n - 1)。这给我们了样本方差,结果是 6.3。

S² = (11.56 + 0.16 + 0.36 + 0.16 + 12.96) / 4 = 6.3

所以,如果这是我们从一个更大的数据集中取出的样本,那就是你要做的。如果这是一个完整的数据集,你就除以实际的数量。好的,这就是我们计算总体和样本方差的方法,但背后的实际逻辑是什么呢?

数学解释

至于为什么总体和样本方差之间有差异,这涉及到概率的一些非常奇怪的东西,你可能不想太多地去思考,而且需要一些复杂的数学符号,我尽量避免在这本书中使用符号,因为我认为概念更重要,但这是非常基础的东西,你会一遍又一遍地看到它。

正如我们所见,总体方差通常被表示为 sigma squared (σ²),其中 sigma (σ)是标准差,我们可以说这是每个数据点 X 减去均值 mu 的平方的总和,这是每个样本平方的方差除以数据点的数量 N,我们可以用以下方程表示:

  • X 表示每个数据点

  • µ表示均值

  • N 表示数据点的数量

样本方差同样被表示为 S²,用以下方程表示:

  • X 表示每个数据点

  • M 表示均值

  • N-1 表示数据点的数量减 1

就是这样。

在直方图上分析标准差和方差

让我们在这里写一些代码,玩一些标准差和方差。所以如果你打开StdDevVariance.ipynb文件的 IPython 笔记本,并跟着我一起做。请这样做,因为最后有一个我想让你尝试的活动。我们要做的就像前面的例子一样,从以下代码开始:

%matplotlib inline 
import numpy as np 
import matplotlib.pyplot as plt 
incomes = np.random.normal(100.0, 20.0, 10000) 
plt.hist(incomes, 50) 
plt.show() 

我们使用matplotlib来绘制一些正态分布的随机数据的直方图,并将其命名为incomes。我们说它将以100为中心(希望这是小时工资之类的,而不是年薪,或者其他奇怪的单位),标准差为20,有10,000个数据点。

让我们继续执行上面的代码块,并按照下图所示绘制出来:

我们有 10,000 个以 100 为中心的数据点。通过正态分布和标准差为 20,这是数据的扩展度量,你可以看到最常见的情况是在 100 左右,随着我们离这个点越来越远,事情变得越来越不太可能。我们指定的标准差点 20 在 80 左右和 120 左右。你可以在直方图中看到,这是事情开始急剧下降的点,所以我们可以说在那个标准差边界之外的事情是不寻常的。

使用 Python 计算标准差和方差

现在,NumPy 也使计算标准差和方差变得非常容易。如果你想计算我们生成的数据集的实际标准差,你只需在数据集本身上调用std函数。因此,当 NumPy 创建列表时,它不仅仅是一个普通的 Python 列表,它实际上附加了一些额外的东西,所以你可以在上面调用函数,比如标准差的std。现在让我们来做一下:

incomes.std() 

这给我们一些类似以下输出(记住我们使用了随机数据,所以你的图形可能与我的不完全相同):

20.024538249134373 

当我们执行时,得到的数字非常接近 20,因为这是我们在创建随机数据时指定的。我们想要一个标准差为 20。果然,20.02,非常接近。

方差只是一个调用var的问题。

incomes.var() 

这给了我以下结果:

400.98213209104557 

结果非常接近 400,这是 20²。对,世界是有道理的!标准差只是方差的平方根,或者你可以说方差是标准差的平方。果然,这是成立的,所以世界是按照应有的方式运行的。

自己试试

我希望你能深入研究并实际尝试一下,让它变得真实,尝试使用不同的参数来生成正态数据。记住,这是对数据分布形状的度量,所以如果我改变中心点会发生什么?这重要吗?它实际上会影响形状吗?为什么不试一下,找出答案呢?

尝试改变我们指定的实际标准差,看看对图形形状有什么影响。也许尝试一个标准差为 30,然后你知道,你可以看看它实际上如何影响事物。让我们更夸张一点,比如 50。试试 50。你会看到图形开始变得有点胖。尝试不同的值,感受一下这些值是如何起作用的。这是真正获得标准差和方差直观感觉的唯一方法。尝试一些不同的例子,看看它的影响。

这就是实践中的标准差和方差。你已经亲自体验了一些,我希望你能稍微玩弄一下,以便更熟悉。这些是非常重要的概念,我们将在整本书中经常谈论标准差,毫无疑问,在你的数据科学职业生涯中也会经常谈论,所以确保你已经掌握了这些。让我们继续。

概率密度函数和概率质量函数

因此,我们已经在本书的一些例子中看到了正态分布函数。这是概率密度函数的一个例子,还有其他类型的概率密度函数。所以让我们深入了解一下,看看它实际上意味着什么,还有一些其他例子。

概率密度函数和概率质量函数

我们已经在本书中看到了一些正态分布函数的例子。这是概率密度函数的一个例子,还有其他类型的概率密度函数。让我们深入了解一下,看看这实际上意味着什么,还有一些其他例子。

概率密度函数

让我们谈谈概率密度函数,我们在书中已经使用过其中之一。我们只是没有这样称呼它。让我们正式化一些我们谈论过的东西。例如,我们已经多次看到正态分布,这是概率密度函数的一个例子。以下图是正态分布曲线的一个例子

在概念上,试图将这个图形看作给定值发生的概率是很容易的,但是当你谈论连续数据时,这有点误导。因为在连续数据分布中,实际可能的数据点有无限多个。可能是 0 或 0.001 或 0.00001,所以一个非常具体的值发生的实际概率是非常非常小的,无限小。概率密度函数实际上告诉了给定值范围发生的概率。所以这就是你必须考虑的方式。

例如,在上图中显示的正态分布中,均值(0)和均值的一个标准差()之间,有 34.1%的机会出现在这个范围内。你可以收紧或扩展这个范围,找出实际值,但这就是概率密度函数的思考方式。对于给定值范围,它给出了发生该范围的概率的方法。

  • 你可以看到在图中,当你接近均值(0)时,在一个标准差(-1σ)内,你很可能会落在这个范围内。我的意思是,如果你把 34.1 和 34.1 相加,等于 68.2%,你就得到了落在均值一个标准差范围内的概率。

  • 然而,当你处于两个到三个标准差之间(-3σ-2σ),我们只剩下略微超过 4%(确切地说是 4.2%)。

  • 当你超出三个标准差(-3σ)时,概率远远小于 1%。

因此,图表只是一种可视化和讨论给定数据点发生概率的方式。再次强调,概率分布函数给出了数据点落在给定值范围内的概率,正态函数只是概率密度函数的一个例子。我们稍后会看一些更多的例子。

概率质量函数

现在,当你处理离散数据时,关于有无限多个可能值的微妙之处就消失了,我们称之为另一种东西。这就是概率质量函数。如果你处理离散数据,你可以谈论概率质量函数。这里有一个图表来帮助可视化这一点:

例如,你可以在图中显示连续数据的正态概率密度函数,但如果我们将其量化为离散数据集,就像我们在直方图中所做的那样,我们可以说数字 3 出现了一定次数,你实际上可以说数字 3 有超过 30%的机会出现。因此,概率质量函数是我们可视化离散数据发生概率的方式,它看起来很像直方图,因为它基本上就是一个直方图。

术语差异:概率密度函数是描述连续数据发生范围的实心曲线。概率质量函数是数据集中给定离散值发生的概率。

数据分布类型

让我们看一些概率分布函数和数据分布的真实例子,更全面地理解数据分布以及如何在 Python 中可视化和使用它们。

继续打开书中的Distributions.ipynb,如果你愿意,你可以跟着我一起学习。

均匀分布

让我们从一个非常简单的例子开始:均匀分布。均匀分布意味着在给定范围内,一个值发生的概率是平坦的常数。

import numpy as np 
Import matplotlib.pyplot as plt 

values = np.random.uniform(-10.0, 10.0, 100000) 
plt.hist(values, 50) 
plt.show() 

所以我们可以使用 NumPy 的random.uniform函数创建一个均匀分布。前面的代码表示,我想要一个在-1010之间范围的均匀分布的随机值,并且我想要100000个。如果我然后创建这些值的直方图,你会看到它看起来像下面这样。

在这些数据中,任何给定值或值范围发生的概率几乎是相等的。因此,与正态分布不同,我们在均匀分布中看到的是在你定义的范围内任何给定值都有相等的概率。

那么这个的概率分布函数会是什么样子呢?嗯,我期望在-1010之外基本上看不到任何东西。但当我在-1010之间时,我会看到一条平直的线,因为任何一个这些值范围发生的概率是恒定的。因此在均匀分布中,你会看到概率分布函数上的一条平直线,因为每个值,每个值范围出现的概率都是相等的。

正态或高斯分布

现在我们已经在本书中看到了正态分布,也称为高斯分布函数。你实际上可以在 Python 中可视化这些。scipy.stats.norm包函数中有一个名为pdf(概率密度函数)的函数。

所以,让我们看下面的例子:

from scipy.stats import norm 
import matplotlib.pyplot as plt 

x = np.arange(-3, 3, 0.001) 
plt.plot(x, norm.pdf(x)) 

在上面的例子中,我们通过使用arange函数创建了一个在-3 和 3 之间以 0.001 为间隔的 x 值列表用于绘图。所以这些是图表上的 x 值,我们将使用这些值绘制x轴。y轴将是正态函数norm.pdf,即正态分布的概率密度函数,对这些 x 值。我们得到了下面的输出:

正态分布的概率密度函数看起来就像我们上一节中的样子,也就是说,对于我们提供的给定数字,0 代表均值,而数字-3-2-1123代表标准差。

现在,我们将使用正态分布生成随机数。我们已经做过几次了;把它当作一个复习。参考下面的代码块:

import numpy as np 
import matplotlib.pyplot as plt 

mu = 5.0 
sigma = 2.0 
values = np.random.normal(mu, sigma, 10000) 
plt.hist(values, 50) 
plt.show() 

在上面的代码中,我们使用了 NumPy 包的random.normal函数,第一个参数mu代表你想要将数据围绕其均值中心化的均值。sigma是数据的标准差,基本上是数据的扩散。然后,我们使用正态概率分布函数指定我们想要的数据点的数量,这里是10000。这是使用概率分布函数的一种方式,在这种情况下是正态分布函数,用来生成一组随机数据。然后我们可以绘制一个直方图,分成50个桶并显示出来。下面的输出就是我们得到的结果:

它看起来更像是一个正态分布,但由于有一个随机元素,它不会是一个完美的曲线。我们在谈论概率;有一些事情不太可能是我们期望的样子。

指数概率分布或幂律

你经常看到的另一个分布函数是指数概率分布函数,其中事物以指数方式下降。

当你谈论指数下降时,你期望看到一个曲线,在接近零时很可能发生某些事情,但随着你离开它越远,它会迅速下降。自然界中有很多事物都是以这种方式行为的。

在 Python 中,就像我们在scipy.stats中有一个norm.pdf函数一样,我们也有一个expon.pdf,或者指数概率分布函数来做这个。在 Python 中,我们可以使用与正态分布相同的语法来处理指数分布,如下面的代码块所示:

from scipy.stats import expon 
import matplotlib.pyplot as plt 

x = np.arange(0, 10, 0.001) 
plt.plot(x, expon.pdf(x)) 

所以在上面的代码中,我们只是使用 NumPy 的arange函数创建我们的 x 值,以便在010之间创建一堆值,步长为0.001。然后,我们将这些 x 值绘制在 y 轴上,y 轴定义为函数expon.pdf(x)。输出看起来像是指数下降。如下截图所示:

二项概率质量函数

我们也可以可视化概率质量函数。这被称为二项概率质量函数。我们将使用与之前相同的语法,如下面的代码所示:

from scipy.stats import expon 
import matplotlib.pyplot as plt 

x = np.arange(0, 10, 0.001) 
plt.plot(x, expon.pdf(x)) 

所以我们不再使用exponnorm,而是使用binom。提醒一下:概率质量函数处理离散数据。实际上,一直以来我们一直在处理离散数据,只是你要如何思考它。

回到我们的代码,我们正在创建一些在010之间的离散x值,间隔为0.01,并且我要绘制一个使用这些数据的二项概率质量函数。使用binom.pmf函数,我实际上可以使用两个形状参数np来指定数据的形状。在这种情况下,它们分别是100.5。输出如下图所示:

如果你想尝试不同的值来看看它的影响,这是一个直观了解这些形状参数如何影响概率质量函数的好方法。

泊松概率质量函数

最后,你可能听说的另一个分布函数是泊松概率质量函数,它有一个非常特定的应用。它看起来很像正态分布,但有点不同。

这里的想法是,如果你有关于在给定时间段内发生的事情的平均数量的一些信息,这个概率质量函数可以让你预测在未来的某一天获得其他值的几率。

举个例子,假设我有一个网站,平均每天有 500 位访客。我可以使用泊松概率质量函数来估计在特定一天看到其他数值的概率。例如,以我平均每天 500 位访客为例,看到在某一天有 550 位访客的几率是多少?这就是泊松概率质量函数可以给你的,看看下面的代码:

from scipy.stats import poisson 
import matplotlib.pyplot as plt 

mu = 500 
x = np.arange(400, 600, 0.5) 
plt.plot(x, poisson.pmf(x, mu)) 

在这个代码示例中,我说我的平均值是 500 mu。我将设置一些 x 值,范围在400600之间,间隔为0.5。我将使用poisson.pmf函数来绘制图表。我可以使用该图表来查找在正态分布情况下获得任何特定值的几率:

在特定一天看到 550 位访客的几率,结果是大约 0.002 或 0.2%的概率。非常有趣。

好的,这些是你在现实世界中可能遇到的一些常见数据分布。

记住,我们使用概率分布函数处理连续数据,但当我们处理离散数据时,我们使用概率质量函数。

所以这就是概率密度函数和概率质量函数。基本上,这是一种可视化和测量数据集中出现的一定范围数值的实际机会的方法。这是非常重要的信息,也是非常重要的理解的事情。我们将一遍又一遍地使用这个概念。好的,让我们继续。

百分位数和矩

接下来,我们将讨论百分位数和矩。你经常在新闻中听到百分位数。收入排在前 1%的人:这是百分位数的一个例子。我们将解释这一点,并举一些例子。然后,我们将讨论矩,这是一个非常复杂的数学概念,但事实证明,在概念上非常容易理解。让我们深入讨论百分位数和矩,这是统计学中的一些基本概念,但是,我们正在逐步解决困难的问题,所以请耐心等待我们复习一些内容。

百分位数

让我们看看百分位数的意思。基本上,如果你要对数据集中的所有数据进行排序,给定的百分位数是数据小于你所在位置的百分比。

一个常见的例子是收入分布。当我们谈论第 99 百分位数,或者百分之一的人,想象一下,你要把这个国家,这里是美国,所有人的收入按收入排序。第 99 百分位数将是收入的金额,99%的人收入低于这个金额。这是一个非常容易理解的方法。

在数据集中,百分位数是数值小于该点的值的x%

下图是收入分布的一个例子:

上图显示了一个收入分布数据的例子。例如,在第 99 百分位数,我们可以说 99%的数据点,代表美国人,年收入低于 50,6553 美元,而 1%的人年收入高于这个数。相反,如果你是百分之一的人,你的年收入超过 50,6553 美元。恭喜!但如果你是一个更典型的中位数人,第 50 百分位数定义了一半的人收入低于你,一半的人收入高于你,这就是中位数的定义。第 50 百分位数和中位数是一回事,在这个数据集中是 42,327 美元。所以,如果你在美国年收入 42,327 美元,你的收入正好是全国的中位数。

你可以从上面的图表中看到收入分布的问题。事物往往非常集中在图表的高端,这是目前在国家中一个非常大的政治问题。我们将看看发生了什么,但这超出了本书的范围。这就是百分位数的要点。

四分位数

百分位数也用于讨论分布中的四分位数。让我们看一个正态分布,以更好地理解这一点。

这是一个例子,说明正态分布中的百分位数:

看看上图中的正态分布,我们可以谈论四分位数。中间的四分位数 1(Q1)和四分位数 3(Q3)只是包含 50%数据的点,所以 25%在中位数的左侧,25%在中位数的右侧。

在这个例子中,中位数恰好接近平均值。例如,四分位距IQR),当我们谈论一个分布时,是分布中包含 50%数值的中间区域。

图像的最上部是我们所谓的箱线图的一个例子。暂时不要担心箱子边缘的东西。那有点混乱,我们稍后会讨论。即使它们被称为四分位数 1(Q1)和四分位数 3(Q1),它们并不真正代表 25%的数据,但暂时不要纠结在这一点上。重点是中间的四分位数代表数据分布的 25%。

在 Python 中计算百分位数

让我们看一些使用 Python 的百分位数的更多例子,并更深入地理解它。如果你愿意跟着做,可以打开Percentiles.ipynb文件,我鼓励你这样做,因为我希望你稍后能够玩一下这个。

让我们首先生成一些随机分布的正态数据,或者说是正态分布的随机数据,请参考以下代码块:

%matplotlib inline 
import numpy as np 
import matplotlib.pyplot as plt 

vals = np.random.normal(0, 0.5, 10000) 

plt.hist(vals, 50) 
plt.show() 

在这个例子中,我们要做的是生成一些以零为中心的数据,也就是均值为零,标准差为0.5,我将用这个分布生成10000个数据点。然后,我们将绘制一个直方图,看看我们得到了什么。

生成的直方图看起来非常像正态分布,但由于存在随机因素,我们在这个例子中有一个偏离值接近-2。在均值处有一点点的倾斜,一点点的随机变化使事情变得有趣。

NumPy 提供了一个非常方便的百分位数函数,可以为您计算这个分布的百分位数值。因此,我们使用np.random.normal创建了我们的vals数据列表,我可以调用np.percentile函数来计算第 50 个百分位数值,使用以下代码:

np.percentile(vals, 50) 

以下是前面代码的输出:

0.0053397035195310248

输出结果为 0.005。所以记住,第 50 个百分位数其实就是中位数的另一个名称,而在这个数据中,中位数非常接近零。你可以在图表中看到我们稍微向右倾斜,所以这并不太令人惊讶。

我想计算第 90 个百分位数,这将给我一个数,这个数小于它的值占总数的 90%。我们可以很容易地用以下代码来实现:

np.percentile(vals, 90) 

以下是该代码的输出:

Out[4]: 0.64099069837340827 

这些数据的第 90 个百分位数值是 0.64,所以大约在这里,基本上,在这个值以下的数据不到 90%。我可以相信这个结果。10%的数据大于 0.64,90%的数据小于 0.65。

让我们计算第 20 个百分位数值,这将给我一个数,这个数小于它的值占总数的 20%。同样,我们只需要对代码进行一个非常简单的修改:

np.percentile(vals, 20) 

这给出了以下输出:

Out[5]:-0.41810340026619164 

第 20 个百分位数值大约是-0.4,我相信这个结果。它表示 20%的数据位于-0.4 的左侧,相反,80%的数据大于-0.4。

如果你想了解数据集中的分界点在哪里,百分位数函数是一个简单的方法来计算它们。如果这是一个代表收入分布的数据集,我们可以调用np.percentile(vals, 99)来计算第 99 个百分位数。你可以找出人们一直在谈论的那些百分之一的人到底是谁,以及你是否是其中之一。

好了,现在让我们动手。我希望你能玩弄这些数据。这是一个 IPython Notebook,所以你可以随意修改它,尝试不同的标准差值,看看它对数据形状和百分位数的影响,例如。尝试使用更小的数据集大小,并在其中增加一点随机变化。只要熟悉一下,玩弄一下,你会发现你实际上可以做这些事情,并编写一些真正有效的代码。

接下来,让我们谈谈矩。矩是一个花哨的数学术语,但实际上你并不需要数学学位来理解它。直观地说,它比听起来要简单得多。

这是统计学和数据科学领域的一个例子,人们喜欢使用大而花哨的术语来使自己听起来很聪明,但实际上这些概念非常容易理解,这也是你将在本书中一再听到的主题。

基本上,时刻是衡量数据分布形状的方式,概率密度函数的方式,或者任何东西的方式。从数学上讲,我们有一些非常花哨的符号来定义它们:

如果你懂微积分,实际上这并不是一个很复杂的概念。我们正在计算每个值与某个值的差的 n 次方,其中 n 是时刻数,并在整个函数从负无穷到正无穷的范围内进行积分。但直观上,它比微积分容易得多。

时刻可以被定义为概率密度函数形状的定量度量。

准备好了吗?我们开始吧!

  1. 第一时刻实际上就是你所看到的数据的平均值。就是这样。第一时刻就是平均值,就是平均数。就是这么简单。

  2. 第二时刻是方差。就是这样。数据集的第二时刻就是方差值。这些东西似乎自然而然地从数学中产生出来有点吓人,但是想一想。方差实际上是基于与平均值的差的平方,所以找到一个数学方式来说方差与平均值相关并不是那么难以理解,对吧。就是这么简单。

  3. 现在当我们到达第三和第四时刻时,事情变得有点棘手,但它们仍然是容易理解的概念。第三时刻被称为偏度,它基本上是一个度量分布有多倾斜的度量。

  • 你可以在上面的这两个例子中看到,如果我左边有一个更长的尾部,那么这是一个负偏态,如果我右边有一个更长的尾部,那么这是一个正偏态。虚线显示了没有偏态的正态分布的形状。在左边的虚线上,我最终得到了一个负偏态,或者在另一边,这个例子中的正偏态。好的,这就是偏态。基本上就是拉长一侧的尾部,它是一个度量数据分布有多倾斜的度量。
  1. 第四时刻被称为峰度。哇,这是一个花哨的词!实际上,它就是尾部有多厚,峰有多尖。所以,它是数据分布形状的一种度量。这里有一个例子:

  • 你可以看到更高的峰值具有更高的峰度值。最顶部的曲线比最底部的曲线具有更高的峰度。这是一个非常微妙的差异,但仍然是一个差异。它基本上衡量了你的数据有多尖。

让我们回顾一下:第一时刻是平均值,第二时刻是方差,第三时刻是偏度,第四时刻是峰度。我们已经知道平均值和方差是什么。偏度是数据有多倾斜,一个尾部有多伸展。峰度是数据分布有多尖,有多挤在一起。

在 Python 中计算时刻

让我们在 Python 中玩耍并实际计算这些时刻,看看你如何做到这一点。要玩弄这个,请打开Moments.ipynb,你可以跟着我在这里一起进行。

让我们再次创建相同的随机数据的正态分布。再次,我们将使其以零为中心,标准差为 0.5,有 10,000 个数据点,并绘制出来:

import numpy as np 
import matplotlib.pyplot as plt 

vals = np.random.normal(0, 0.5, 10000) 

plt.hist(vals, 50) 
plt.show() 

所以,我们再次得到一个围绕零的正态分布的随机生成的数据集。

现在,我们找到了平均值和方差。我们以前做过这个;NumPy 只是给你一个meanvar函数来计算。所以,我们只需调用np.mean来找到第一时刻,这只是一个对平均值的花哨的说法,如下面的代码所示:

np.mean(vals)

这是我们示例中的输出:

Out [2]:-0.0012769999428169742

结果非常接近零,就像我们期望的那样,对于以零为中心的正态分布的数据。到目前为止,世界是有道理的。

现在我们找到了第二个矩,这只是方差的另一个名称。我们可以用以下代码来做到这一点,就像我们之前看到的那样:

np.var(vals)

提供以下输出:

Out[3]:0.25221246428323563

结果约为 0.25,这再次符合一个很好的检查。记住标准差是方差的平方根。如果你对 0.25 取平方根,结果是 0.5,这是我们在创建这个数据时指定的标准差,所以这也是正确的。

第三个矩是偏度,为了做到这一点,我们需要使用 SciPy 包而不是 NumPy。但这又是内置在任何科学计算包中的,比如 Enthought Canopy 或 Anaconda。一旦我们有了 SciPy,函数调用就像我们之前的两个一样简单:

import scipy.stats as sp
sp.skew(vals)

这显示了以下输出:

Out[4]: 0.020055795996111746

我们可以在vals列表上调用sp.skew,这将给我们一个偏度值。由于这是以零为中心的,它应该几乎没有偏度。结果是,随机变化确实有一点向左偏,实际上这与我们在图表中看到的形状是一致的。看起来我们确实把它拉向了负数。

第四个矩是峰度,描述了尾部的形状。同样,对于正态分布,这个值应该约为零。SciPy 为我们提供了另一个简单的函数调用

sp.kurtosis(vals)

以下是输出:

Out [5]:0.059954502386585506

事实上,结果确实是零。峰度以两种相关的方式显示我们的数据分布:尾部的形状,或者峰值有多尖。如果我把尾部压扁,峰值就会变得更尖,同样,如果我把分布压下去,你可以想象这样会把事情扩散开一点,使尾部变得更厚,峰值变得更低。这就是峰度的意思,在这个例子中,峰度接近零,因为它只是一个普通的正态分布。

如果你想玩一下,继续,再试着修改分布。使它以 0 以外的某个值为中心,看看是否真的会改变什么。应该吗?嗯,实际上不应该,因为这些都是关于分布形状的度量,它并不真的说出这个分布究竟在哪里。这是对形状的度量。这就是矩的全部意义。继续玩,尝试不同的中心值,尝试不同的标准差值,看看它对这些值有什么影响,它并没有改变。当然,你会期望像均值这样的东西会改变,因为你改变了均值,但方差、偏度,也许不会。玩一下,找出来。

这里有百分位数和矩。百分位数是一个相当简单的概念。矩听起来很难,但实际上很容易理解如何做,而且在 Python 中也很容易。现在你已经掌握了这个。是时候继续前进了。

总结

在本章中,我们看到了你可能会遇到的数据类型(数值、分类和有序数据),以及如何对它们进行分类,以及根据你处理的数据类型的不同对待它们。我们还介绍了统计概念的均值、中位数和众数,我们也看到了在中位数和均值之间进行选择的重要性,通常中位数比均值更好,因为存在离群值。

接下来,我们分析了如何在 IPython Notebook 文件中使用 Python 计算均值、中位数和众数。我们深入了解了标准差和方差的概念以及如何在 Python 中计算它们。我们看到它们是数据分布的扩展度量。我们还看到了一种可视化和测量数据集中给定范围的值发生的实际机会的方法,使用概率密度函数和概率质量函数。

我们总体上看了数据分布的类型(均匀分布、正态或高斯分布、指数概率分布、二项概率质量函数、泊松概率质量函数)以及如何使用 Python 对其进行可视化。我们分析了百分位数和矩的概念,并看到如何使用 Python 计算它们。

在下一章中,我们将更深入地研究使用matplotlib库,并深入探讨协方差和相关性等更高级的主题。

第三章:Matplotlib 和高级概率概念

在上一章中,我们已经介绍了一些统计学和概率的简单概念,现在我们将把注意力转向一些更高级的主题,这些主题是你需要熟悉的,以便充分利用本书的剩余部分。别担心,它们并不太复杂。首先,让我们来玩一玩,看看matplotlib库的一些惊人的绘图能力。

在本章中,我们将涵盖以下主题:

  • 使用matplotlib包绘制图表

  • 理解协方差和相关性以确定数据之间的关系

  • 理解条件概率及其示例

  • 理解贝叶斯定理及其重要性

Matplotlib 的速成课程

你的数据只有你能向他人呈现得好,所以让我们谈谈如何绘制和展示你的数据,以及如何向他人呈现你的图表并使其看起来漂亮。我们将更全面地介绍 Matplotlib,并对其进行全面测试。

我会向你展示一些技巧,让你的图表尽可能漂亮。让我们用图表玩一玩。将你的工作做成漂亮的图片总是很好的。这将为你提供更多的工具,用不同类型的图表来可视化不同类型的数据,并使其看起来漂亮。我们将使用不同的颜色、不同的线条样式、不同的坐标轴等等。重要的不仅是使用图表和数据可视化来尝试在数据中找到有趣的模式,而且还要有趣地向非技术人员展示你的发现。话不多说,让我们开始学习 Matplotlib 吧。

继续打开MatPlotLib.ipynb文件,你可以和我一起玩弄这些东西。我们将从绘制一个简单的折线图开始。

%matplotlib inline 

from scipy.stats import norm 
import matplotlib.pyplot as plt 
import numpy as np 

x = np.arange(-3, 3, 0.001) 

plt.plot(x, norm.pdf(x)) 
plt.show() 

所以在这个例子中,我导入matplotlib.pyplot作为plt,然后我们可以在笔记本中从现在开始将其称为plt。然后,我使用np.arange(-3, 3, 0.001)创建一个 x 轴,其中填充了在-33之间以 0.001 增量的值,并使用pyplotplot()函数来绘制x。y 函数将是norm.pdf(x)。所以我将根据x值创建一个正态分布的概率密度函数,并使用scipy.stats norm包来实现。

所以将其与上一章关于概率密度函数的内容联系起来,这里我们使用matplotlib绘制正态概率密度函数。我们只需调用pyplotplot()方法来设置我们的图表,然后使用plt.show()显示它。当我们运行前面的代码时,我们得到以下输出:

这就是我们得到的:一个漂亮的小图表,带有所有默认格式。

在一个图表上生成多个图表

假设我想一次绘制多个图表。在调用 show 之前,你实际上可以多次调用 plot 来添加多个函数到你的图表中。让我们看看下面的代码:

plt.plot(x, norm.pdf(x)) 
plt.plot(x, norm.pdf(x, 1.0, 0.5)) 
plt.show() 

在这个例子中,我调用了我的原始函数,只是一个正态分布,但我还要渲染另一个正态分布,均值约为1.0,标准差为0.5。然后,我会把这两个一起显示出来,这样你就可以看到它们彼此之间的比较。

你可以看到,默认情况下,matplotlib会自动为每个图形选择不同的颜色,这对你来说非常好和方便。

将图表保存为图像

如果我想把这个图表保存到文件中,也许我想把它包含在文档中,我可以像下面的代码一样做:

plt.plot(x, norm.pdf(x)) 
plt.plot(x, norm.pdf(x, 1.0, 0.5)) 
plt.savefig('C:\\Users\\Frank\\MyPlot.png', format='png') 

不仅仅调用plt.show(),我可以调用plt.savefig()并指定我想要保存这个文件的路径以及我想要的格式。

如果你在跟着做的话,你会想把它改成你的机器上实际存在的路径。你可能没有一个Users\Frank文件夹。还要记住,如果你在 Linux 或 macOS 上,你将使用正斜杠而不是反斜杠,并且你不会有一个驱动器号。对于所有这些 Python 笔记本,每当你看到这样的路径时,确保你将它改为在你的系统上有效的路径。我在 Windows 上,我有一个Users\Frank文件夹,所以我可以继续运行。如果我在Users\Frank下检查我的文件系统,我有一个MyPlot.png文件,我可以打开并查看,并且我可以在任何我想要的文档中使用它。

这很酷。还有一件要注意的事情是,根据你的设置,当你保存文件时可能会遇到权限问题。你只需要找到适合你的文件夹。在 Windows 上,你的Users\Name文件夹通常是一个安全的选择。好了,让我们继续。

调整坐标轴

假设我不喜欢上一个图表中的轴的默认选择。它自动调整到你可以找到的最紧凑的轴值集,这通常是一个好事,但有时你希望按绝对比例来做。看看下面的代码:

axes = plt.axes() 
axes.set_xlim([-5, 5]) 
axes.set_ylim([0, 1.0]) 
axes.set_xticks([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) 
axes.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 
plt.plot(x, norm.pdf(x)) 
plt.plot(x, norm.pdf(x, 1.0, 0.5)) 
plt.show() 

在这个例子中,首先我使用plt.axes获取坐标轴。一旦我有了这些坐标轴对象,我就可以调整它们。通过调用set_xlim,我可以将 x 范围设置为-5 到 5,通过set_ylim,我将 y 范围设置为 0 到 1。你可以在下面的输出中看到,我的 x 值范围从-55,y 值从 0 到 1。我还可以明确控制坐标轴上的刻度标记的位置。因此,在上面的代码中,我说我希望 x 刻度在-5-4-3等处,y 刻度从 0 到 1,间隔为 0.1,使用set_xticks()set_yticks()函数。现在我可以使用arange函数更紧凑地做到这一点,但关键是你可以明确控制这些刻度标记的位置,也可以跳过一些。你可以按照你想要的增量或分布来设置它们。除此之外,其他都是一样的。

一旦我调整了我的坐标轴,我只需调用plot()和我想要绘制的函数,并调用show()来显示它。确实,你可以看到结果。

添加网格

如果我想在图表中添加网格线呢?嗯,同样的道理。我只需要在从plt.axes()获取的坐标轴上调用grid()

axes = plt.axes() 
axes.set_xlim([-5, 5]) 
axes.set_ylim([0, 1.0]) 
axes.set_xticks([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) 
axes.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 
axes.grid() 
plt.plot(x, norm.pdf(x)) 
plt.plot(x, norm.pdf(x, 1.0, 0.5)) 
plt.show() 

通过执行上面的代码,我得到了漂亮的小网格线。这样可以更容易看到特定点在哪里,尽管会使事情有点凌乱。这是一个小小的风格选择。

更改线型和颜色

如果我想要玩线型和颜色的游戏呢?你也可以这样做。

axes = plt.axes() 
axes.set_xlim([-5, 5]) 
axes.set_ylim([0, 1.0]) 
axes.set_xticks([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) 
axes.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 
axes.grid() 
plt.plot(x, norm.pdf(x), 'b-') 
plt.plot(x, norm.pdf(x, 1.0, 0.5), 'r:') 
plt.show() 

所以你可以看到在上面的代码中,plot()函数的末尾实际上有一个额外的参数,我可以传递一个描述线条样式的小字符串。在第一个例子中,b-表示我想要一个蓝色的实线。b代表蓝色,短线表示实线。对于我的第二个plot()函数,我将以红色绘制,这就是r的含义,冒号表示我将以虚线绘制。

如果我运行它,你可以在上面的图表中看到它的效果,并且你可以改变不同类型的线型。

此外,你可以做一个双虚线(--)。

axes = plt.axes() 
axes.set_xlim([-5, 5]) 
axes.set_ylim([0, 1.0]) 
axes.set_xticks([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) 
axes.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 
axes.grid() 
plt.plot(x, norm.pdf(x), 'b-') 
plt.plot(x, norm.pdf(x, 1.0, 0.5), 'r--') 
plt.show() 

上面的代码给出了虚线红线作为线型,如下图所示:

我还可以做一个点划线组合(-.)。

axes = plt.axes() 
axes.set_xlim([-5, 5]) 
axes.set_ylim([0, 1.0]) 
axes.set_xticks([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) 
axes.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 
axes.grid() 
plt.plot(x, norm.pdf(x), 'b-') 
plt.plot(x, norm.pdf(x, 1.0, 0.5), 'r-.') 
plt.show() 

你会得到一个看起来像下面的图表图像的输出:

所以,这些就是不同的选择。我甚至可以让它变成绿色并带有垂直斜杠(g:)。

axes = plt.axes() 
axes.set_xlim([-5, 5]) 
axes.set_ylim([0, 1.0]) 
axes.set_xticks([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) 
axes.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 
axes.grid() 
plt.plot(x, norm.pdf(x), 'b-') 
plt.plot(x, norm.pdf(x, 1.0, 0.5), ' g:') 
plt.show() 

我将得到以下输出:

如果你愿意,可以尝试一些有趣的东西,尝试不同的值,你可以得到不同的线条样式。

给坐标轴加标签和添加图例

你经常会做的一件事是给你的坐标轴加上标签。你绝对不想孤立地呈现数据。你肯定想告诉人们它代表什么。为了做到这一点,你可以使用plt上的xlabel()ylabel()函数来实际在你的坐标轴上放置标签。我将 x 轴标记为 Greebles,y 轴标记为 Probability。你还可以添加一个图例插图。通常情况下,这将是相同的事情,但为了显示它是独立设置的,我也在以下代码中设置了一个图例:

axes = plt.axes() 
axes.set_xlim([-5, 5]) 
axes.set_ylim([0, 1.0]) 
axes.set_xticks([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) 
axes.set_yticks([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) 
axes.grid() 
plt.xlabel('Greebles') 
plt.ylabel('Probability') 
plt.plot(x, norm.pdf(x), 'b-') 
plt.plot(x, norm.pdf(x, 1.0, 0.5), 'r:') 
plt.legend(['Sneetches', 'Gacks'], loc=4) 
plt.show() 

在图例中,你基本上传入了一个你想要为每个图表命名的列表。所以,我的第一个图表将被称为 Sneetches,我的第二个图表将被称为 Gacks,而loc参数表示你想要的位置,其中4代表右下角。让我们运行一下代码,你应该会看到以下内容:

你可以看到我正在为 Sneetches 和 Gacks 绘制 Greebles 与 Probability 的图表。这是一个小的苏斯博士的参考。这就是你设置坐标轴标签和图例的方法。

一个有趣的例子

这里有一个小有趣的例子。如果你熟悉网络漫画 XKCD,Matplotlib 中有一个小彩蛋,你可以以 XKCD 风格绘制图表。以下代码显示了你可以这样做。

plt.xkcd() 

fig = plt.figure() 
ax = fig.add_subplot(1, 1, 1) 
ax.spines['right'].set_color('none') 
ax.spines['top'].set_color('none') 
plt.xticks([]) 
plt.yticks([]) 
ax.set_ylim([-30, 10]) 

data = np.ones(100) 
data[70:] -= np.arange(30) 

plt.annotate( 
    'THE DAY I REALIZED\nI COULD COOK BACON\nWHENEVER I WANTED', 
    xy=(70, 1), arrowprops=dict(arrowstyle='->'), xytext=(15, -10)) 

plt.plot(data) 

plt.xlabel('time') 
plt.ylabel('my overall health') 

在这个例子中,你调用了plt.xkcd(),这将 Matplotlib 置于 XKCD 模式。在这之后,事情将自动以一种漫画书字体和波浪线的风格呈现。这个简单的例子将展示一个有趣的小图表,其中我们绘制了你的健康与时间的关系,当你意识到你可以随时煮培根时,你的健康状况急剧下降。我们所做的就是使用xkcd()方法进入那种模式。你可以在下面看到结果:

这里有一点有趣的 Python,我们实际上是如何将这个图表放在一起的。我们首先制作了一个数据线,它只是在 100 个数据点上的值为 1。然后我们使用旧的 Python 列表切片运算符来取出值为 70 之后的所有内容,并从这个 30 个项目的子列表中减去 0 到 30 的范围。这样做的效果是,随着超过 70,线性地减去一个更大的值,导致该线在 70 之后向下倾斜到 0。

所以,这是 Python 列表切片的一个小例子,以及arange函数的一点创造性用法来修改你的数据。

生成饼图

现在,回到现实世界,我们可以通过在 Matplotlib 上调用rcdefaults()来移除 XKCD 模式,并在这里回到正常模式。

如果你想要一个饼图,你只需要调用plt.pie并给它一个包含你的值、颜色、标签以及是否要爆炸的数组,如果是的话,爆炸多少。以下是代码:

# Remove XKCD mode: 
plt.rcdefaults() 

values = [12, 55, 4, 32, 14] 
colors = ['r', 'g', 'b', 'c', 'm'] 
explode = [0, 0, 0.2, 0, 0] 
labels = ['India', 'United States', 'Russia', 'China', 'Europe'] 
plt.pie(values, colors= colors, labels=labels, explode = explode) 
plt.title('Student Locations') 
plt.show() 

在这段代码中,你可以看到我正在创建一个饼图,其中包含值125543214。我为每个值分配了明确的颜色,并为每个值分配了明确的标签。我将饼图中的俄罗斯部分扩大了 20%,并给这个图表加上了一个标题“学生位置”并显示出来。以下是你应该看到的输出:

就是这样。

生成条形图

如果我想生成一个条形图,也是非常简单的。这是一种类似于饼图的想法。让我们看看以下代码。

values = [12, 55, 4, 32, 14] 
colors = ['r', 'g', 'b', 'c', 'm'] 
plt.bar(range(0,5), values, color= colors) 
plt.show() 

我定义了一个值数组和一个颜色数组,然后绘制数据。上面的代码从 0 到 5 的范围绘制,使用values数组中的 y 值,并使用colors数组中列出的显式颜色列表。继续展示,你就会得到你的条形图:

生成散点图

散点图是我们在本书中经常看到的东西。所以,假设你有一些不同的属性,你想为同一组人或物体绘制图表。例如,也许我们正在为每个人绘制年龄与收入的散点图,其中每个点代表一个人,轴代表这些人的不同属性。

使用散点图的方法是调用plt.scatter(),使用包含你想要绘制的数据的两个轴,也就是包含你想要相互绘制的数据的两个属性。

假设我在XY中有一个随机分布,我把它们散点图上,然后展示出来:

from pylab import randn 

X = randn(500) 
Y = randn(500) 
plt.scatter(X,Y) 
plt.show() 

你会得到以下散点图作为输出:

这就是它的样子,非常酷。你可以看到中心的一种集中,因为在两个轴上都使用了正态分布,但由于它是随机的,所以这两者之间没有真正的相关性。

生成直方图

最后,我们会回顾一下直方图是如何工作的。我们在书中已经看到了很多次。让我们看看以下代码:

incomes = np.random.normal(27000, 15000, 10000) 
plt.hist(incomes, 50) 
plt.show() 

在这个例子中,我调用了一个以 27000 为中心,标准差为 15000 的正态分布,有 10000 个数据点。然后,我只是调用了pyplot的直方图函数,也就是hist(),并指定了输入数据和我们想要将事物分组到直方图中的桶的数量。然后我调用show(),剩下的就是魔术。

生成盒须图

最后,让我们看看盒须图。还记得在上一章中,当我们谈到百分位数时,我稍微提到了这一点。

同样,使用盒须图,盒子代表了 50%的数据所在的两个内四分位数。相反,另外 25%分布在盒子的两侧;盒须(在我们的例子中是虚线)代表了除异常值外的数据范围。

我们在盒须图中将异常值定义为超出 1.5 倍四分位距或盒子大小的任何值。因此,我们将盒子的大小乘以 1.5,然后在虚线盒须上到那一点,我们称这些部分为外四分位数。但是在外四分位数之外的任何值都被视为异常值,这就是超出外四分位数的线所代表的。这就是我们根据盒须图的定义来定义异常值的地方。

关于盒须图的一些要点:

  • 它们用于可视化数据的分布和偏斜

  • 盒子中间的线代表数据的中位数,盒子代表第 1 和第 3 四分位数的范围

  • 数据的一半存在于盒子中

  • “盒须”表示数据的范围,异常值除外,异常值绘制在盒须之外。

  • 异常值是四分位距的 1.5 倍或更多。

现在,为了给你一个例子,我们创建了一个虚假数据集。以下示例创建了在-40 和 60 之间均匀分布的随机数,再加上一些在100以上和-100以下的异常值:

uniformSkewed = np.random.rand(100) * 100 - 40 
high_outliers = np.random.rand(10) * 50 + 100 
low_outliers = np.random.rand(10) * -50 - 100 
data = np.concatenate((uniformSkewed, high_outliers, low_outliers)) 
plt.boxplot(data) 
plt.show() 

在代码中,我们有一个均匀随机分布的数据(uniformSkewed)。然后我们在高端添加了一些异常值(high_outliers),也添加了一些负异常值(low_outliers)。然后我们将这些列表连接在一起,并使用 NumPy 从这三个不同的集合创建了一个单一的数据集。然后我们拿到了这个组合数据集,其中包括均匀数据和一些异常值,我们使用plt.boxplot()进行了绘制,这就是如何得到箱线图的。调用show()进行可视化,就完成了。

你可以看到图表显示了代表所有数据内部 50%的箱子,然后我们有这些异常值线,我们可以看到每个在该范围内的个体异常值的小叉(在你的版本中可能是圆圈)。

自己试试

好了,这就是 Matplotlib 的速成课程。是时候动手操作了,在这里实际做一些练习。

作为挑战,我希望你创建一个散点图,代表你编造的年龄与看电视时间的随机数据,你可以随意制造任何你想要的东西。如果你脑海中有一个不同的虚构数据集,你想要玩玩,那就尽情玩吧。创建一个散点图,将两组随机数据相互绘制,并标记你的坐标轴。让它看起来漂亮一些,尝试一下,玩得开心一些。你需要的所有参考和示例都应该在这个 IPython 笔记本中。这是一种速查表,如果你愿意的话,可以用来生成不同类型的图表和不同风格的图表。希望它能够有所帮助。现在是时候回到统计学了。

协方差和相关性

接下来,我们将讨论协方差和相关性。假设我有某个东西的两个不同属性,我想看看它们是否实际上与彼此相关。这一部分将为你提供你需要的数学工具,以便这样做,我们将深入一些示例,并使用 Python 实际计算协方差和相关性。这些是衡量两个不同属性在一组数据中是否相关的方法,这可能是一个非常有用的发现。

定义概念

想象我们有一个散点图,每个数据点代表我们测量的一个人,我们在一个轴上绘制他们的年龄,另一个轴上绘制他们的收入。每一个点代表一个人,例如他们的 x 值代表他们的年龄,y 值代表他们的收入。我完全是在编造,这是虚假数据。

现在,如果我有一个散点图,看起来像前面图片中的左边那个,你会发现这些值倾向于分散在各个地方,这会告诉你基于这些数据,年龄和收入之间没有真正的相关性。对于任何给定的年龄,收入可能有很大的范围,它们倾向于聚集在中间,但我们并没有真正看到年龄和收入这两个不同属性之间非常明显的关系。相比之下,在右边的散点图中,你可以看到年龄和收入之间有一个非常明显的线性关系。

因此,协方差和相关性给了我们一种衡量这些关系有多紧密的方法。我期望左边散点图中的数据具有非常低的相关性或协方差,但右边散点图中的数据具有非常高的协方差和相关性。这就是协方差和相关性的概念。它衡量了我正在测量的这两个属性似乎彼此依赖的程度。

测量协方差

数学上测量协方差有点困难,但我会尝试解释一下。以下是步骤:

  • 将两个变量的数据集想象成高维向量

  • 将这些转换为与均值的方差向量。

  • 取两个向量的点积(它们之间的余弦值)

  • 除以样本大小

更重要的是你理解如何使用它以及它的含义。实际上,想象数据的属性是高维向量。我们要做的是对每个数据点的每个属性计算均值的方差。现在我有了这些高维向量,其中每个数据点,每个人,对应不同的维度。

在这个高维空间中,我有一个向量代表了所有属性(比如年龄)的方差与均值的差值。然后我有另一个向量代表了另一个属性(比如收入)的方差与均值的差值。然后我对这些测量每个属性的方差的向量进行点积。从数学上讲,这是一种衡量这些高维向量之间角度的方法。所以如果它们非常接近,那告诉我这些方差在不同属性上几乎是同步变化的。如果我将最终的点积除以样本大小,那就是协方差的量。

现在你永远不需要自己从头计算这个。我们将看到如何在 Python 中以简单的方式做到这一点,但从概念上讲,就是这样工作的。

现在协方差的问题在于它很难解释。如果我有一个接近零的协方差,那么我知道这告诉我这些变量之间几乎没有相关性,但是一个大的协方差意味着存在关系。但大到什么程度?根据我使用的单位不同,可能有非常不同的解释方式。这是相关性解决的问题。

相关性

相关性通过每个属性的标准差进行归一化(只需将协方差除以两个变量的标准差即可实现归一化)。通过这样做,我可以非常清楚地说,相关性为-1 意味着完全的反向相关,因此一个值增加,另一个值减少,反之亦然。相关性为 0 意味着这两组属性之间根本没有相关性。相关性为 1 意味着完美的相关性,这两个属性在查看不同数据点时以完全相同的方式移动。

记住,相关性并不意味着因果关系。仅仅因为你找到了一个非常高的相关性值,并不意味着其中一个属性导致了另一个属性。它只是意味着两者之间存在关系,而这种关系可能是由完全不同的东西引起的。真正确定因果关系的唯一方法是通过控制实验,我们稍后会更多地讨论。

在 Python 中计算协方差和相关性

好了,让我们用一些实际的 Python 代码来深入了解协方差和相关性。所以你可以从概念上将协方差看作是对每个属性的均值方差的多维向量,并计算它们之间的角度作为协方差的度量。做这件事的数学比听起来简单得多。我们谈论的是高维向量。听起来像是史蒂芬·霍金的东西,但从数学的角度来看,它非常直接。

计算相关性 - 最困难的方式

我将从最困难的方式开始。NumPy 确实有一个方法可以直接为你计算协方差,我们稍后会讨论,但现在我想展示你实际上可以从头原理开始做到这一点:

%matplotlib inline 

import numpy as np 
from pylab import * 

def de_mean(x): 
    xmean = mean(x) 
    return [xi - xmean for xi in x] 

def covariance(x, y): 
    n = len(x) 
    return dot(de_mean(x), de_mean(y)) / (n-1) 

协方差再次被定义为点积,这是两个向量之间的角度的度量,对于给定数据集的偏差向量和另一个给定数据集的偏差向量,我们将其除以 n-1,在这种情况下,因为我们实际上处理的是一个样本。

所以de_mean(),我们的偏差函数接收一组数据x,实际上是一个列表,并计算该数据集的平均值。return行包含一些 Python 的技巧。语法是说,我将创建一个新的列表,并遍历x中的每个元素,称之为xi,然后返回整个数据集中xi和平均值xmean之间的差异。这个函数返回一个新的数据列表,表示每个数据点相对于平均值的偏差。

我的covariance()函数将对进入的两组数据进行散点图绘制,除以数据点的数量减 1。还记得上一章关于样本与总体的事情吗?嗯,这在这里起作用。然后我们可以使用这些函数,看看会发生什么。

为了扩展这个例子,我将捏造一些数据,试图找到页面速度和人们花费的金额之间的关系。例如,在亚马逊,我们非常关心页面渲染速度和用户在体验之后实际花费的金额之间的关系。我们想知道网站速度和用户在网站上实际花费的金额之间是否存在实际关系。这是你可能会去解决这个问题的一种方式。让我们为页面速度和购买金额生成一些正态分布的随机数据,由于它是随机的,所以它们之间不会有真正的相关性。

pageSpeeds = np.random.normal(3.0, 1.0, 1000) 
purchaseAmount = np.random.normal(50.0, 10.0, 1000) 

scatter(pageSpeeds, purchaseAmount) 

covariance (pageSpeeds, purchaseAmount) 

所以,作为一个理智的检查,我们将从散点图开始:

你会看到它倾向于围绕中间聚集,因为每个属性上的正态分布,但两者之间没有真正的关系。对于任何给定的页面速度,花费的金额有很大的变化,对于任何给定的花费金额,页面速度也有很大的变化,所以除了通过随机性或正态分布的性质产生的相关性之外,没有真正的相关性。果然,如果我们计算这两组属性的协方差,我们最终得到一个非常小的值,-0.07。所以这是一个非常小的协方差值,接近于零。这意味着这两件事之间没有真正的关系。

现在让我们让生活变得更有趣一点。让我们实际上使购买金额成为页面速度的一个真实函数。

purchaseAmount = np.random.normal(50.0, 10.0, 1000) / pageSpeeds 

scatter(pageSpeeds, purchaseAmount) 

covariance (pageSpeeds, purchaseAmount) 

在这里,我们保持事情有点随机,但我们在这两组值之间建立了一个真实的关系。对于给定的用户,他们遇到的页面速度和他们花费的金额之间存在着真实的关系。如果我们绘制出来,我们可以看到以下输出:

你可以看到实际上有一个小曲线,事物倾向于紧密排列。在底部附近会有一些混乱,只是因为随机性的工作方式。如果我们计算协方差,我们最终得到一个更大的值,-8,这个数字的大小很重要。符号,正或负,只是意味着正相关或负相关,但 8 这个值表示比零大得多。所以有一些事情发生了,但是再次很难解释 8 实际上意味着什么。

这就是相关性的作用,我们通过以下代码将一切标准化:

def correlation(x, y): 
stddevx = x.std() 
stddevy = y.std() 
return covariance(x,y) / stddevx / stddevy  #In real life you'd check for divide by zero here 

correlation(pageSpeeds, purchaseAmount) 

再次,从第一原则出发,我们可以计算两组属性之间的相关性,计算每个属性的标准差,然后计算这两个属性之间的协方差,并除以每个数据集的标准差。这给我们提供了归一化到-1 到 1 的相关值。我们得到了一个值为-0.4,这告诉我们这两个属性之间存在一些负相关的关系:

这不是一个完美的线性关系,那将是-1,但其中有一些有趣的东西。

-1 的相关系数意味着完美的负相关,0 表示没有相关性,1 表示完美的正相关。

计算相关性-NumPy 的方式

现在,NumPy 实际上可以使用corrcoef()函数为您计算相关性。让我们看一下以下代码:

np.corrcoef(pageseeds, purchaseAmount) 

这一行代码给出了以下输出:

array([(1\.         ,-046728788], 
      [-0.46728788], 1\.       ]) 

所以,如果我们想简单地做到这一点,我们可以使用np.corrcoef(pageSpeeds, purchaseAmount),它会给你一个数组,其中包含了你传入的数据集的每一种可能的组合之间的相关性。输出的方式是:1 表示在比较pageSpeedspurchaseAmount自身时有完美的相关性,这是预期的。但当你开始比较pageSpeedspurchaseAmountpurchaseAmountpageSpeeds时,你得到了-0.4672 的值,这大致是我们用较困难的方法得到的结果。会有一些精度误差,但这并不重要。

现在我们可以通过制造一个完全线性的关系来强制产生完美的相关性,所以让我们看一个例子:

purchaseAmount = 100 - pageSpeeds * 3 

scatter(pageSpeeds, purchaseAmount) 

correlation (pageSpeeds, purchaseAmount) 

再次,我们期望相关性的结果为-1,表示完美的负相关,事实上,这就是我们得到的结果:

再次提醒:相关性并不意味着因果关系。只是因为人们可能会在页面速度更快时花费更多,也许这只是意味着他们能负担得起更好的互联网连接。也许这并不意味着页面渲染速度和人们花费的金额之间实际上存在因果关系,但它告诉你有一个值得进一步调查的有趣关系。你不能在没有进行实验的情况下说任何关于因果关系的事情,但相关性可以告诉你你可能想要进行的实验。

相关性活动

所以动手做,卷起袖子,我希望你使用numpy.cov()函数。这实际上是让 NumPy 为你计算协方差的一种方法。我们看到了如何使用corrcoef()函数计算相关性。所以回去重新运行这些例子,只使用numpy.cov()函数,看看你是否得到了相同的结果。它应该非常接近,所以不要用我从头开始编写的协方差函数的较困难的方法,只需使用 NumPy,看看你是否能得到相同的结果。再次强调,这个练习的目的是让你熟悉使用 NumPy 并将其应用到实际数据中。所以试试看,看看你能得到什么结果。

这就是协方差和相关性的理论和实践。这是一个非常有用的技术,所以一定要记住这一部分。让我们继续。

条件概率

接下来,我们将讨论条件概率。这是一个非常简单的概念。它试图找出在发生某事的情况下另一件事发生的概率。尽管听起来很简单,但实际上理解其中的一些细微差别可能会非常困难。所以多倒一杯咖啡,确保你的思维帽戴好了,如果你准备好接受一些更具挑战性的概念。让我们开始吧。

条件概率是衡量两件事相互发生关系的一种方法。假设我想找出在另一件事已经发生的情况下某个事件发生的概率。条件概率可以帮助你找出这个概率。

我试图通过条件概率找出的是两个事件是否相互依赖。也就是说,两者都发生的概率是多少?

在数学表示法中,我们表示这些事情的方式是 P(A,B) 表示 A 和 B 同时发生的概率,而与其他事情无关。也就是说,这两件事情发生的概率是多少,与其他一切无关。

而这种表示法 P(B|A),读作给定 A 的情况下 B 的概率。那么,在事件 A 已经发生的情况下,B 发生的概率是多少?这有点不同,但这些事情是相关的,就像这样:

给定 A 的情况下 B 的概率等于 A 和 B 同时发生的概率除以 A 单独发生的概率,这就揭示了 B 的概率依赖于 A 的概率。

这里举个例子会更容易理解,所以请耐心等待。

假设我给你们读者两个测试,60%的人都通过了两个测试。现在第一个测试比较容易,80%的人通过了。我可以利用这些信息来计算通过第一个测试的读者中有多少人也通过了第二个测试。这是一个真实的例子,说明了给定 A 的情况下 B 的概率和 A 和 B 的概率之间的差异。

我将把 A 表示为通过第一个测试的概率,B 表示通过第二个测试的概率。我要找的是在通过第一个测试的情况下通过第二个测试的概率,即 P(B|A)

因此,给定通过第一个测试的情况下通过第二个测试的概率等于通过两个测试的概率 P(A,B)(我知道 60%的人通过了两个测试,而不考虑彼此的影响),除以通过第一个测试的概率 P(A),即 80%。计算结果是通过了两个测试的概率为 60%,通过了第一个测试的概率为 80%,因此给定通过第一个测试的情况下通过第二个测试的概率为 75%。

好的,这个概念有点难以理解。我花了一点时间才真正理解了给定某事物的概率和两件事情发生的概率之间的差异。在继续之前,请确保你理解了这个例子以及它是如何运作的。

Python 中的条件概率练习

好的,让我们继续,用一些真实的 Python 代码来做另一个更复杂的例子。然后我们可以看看如何实际使用 Python 来实现这些想法。

让我们在这里把条件概率付诸实践,并使用一些想法来找出年龄和购买商品之间是否存在关系,使用一些虚构的数据。如果你愿意,可以打开ConditionalProbabilityExercise.ipynb并跟着我一起做。

我要做的是写一些 Python 代码,创建一些虚假数据:

from numpy import random 
random.seed(0) 

totals = {20:0, 30:0, 40:0, 50:0, 60:0, 70:0} 
purchases = {20:0, 30:0, 40:0, 50:0, 60:0, 70:0} 
totalPurchases = 0 
for _ in range(100000): 
    ageDecade = random.choice([20, 30, 40, 50, 60, 70]) 
    purchaseProbability = float(ageDecade) / 100.0 
    totals[ageDecade] += 1 
    if (random.random() < purchaseProbability): 
        totalPurchases += 1 
        purchases[ageDecade] += 1 

我要做的是取 10 万个虚拟人,随机分配到一个年龄段。他们可以是 20 多岁、30 多岁、40 多岁、50 多岁、60 多岁或 70 多岁。我还要给他们分配一些在某段时间内购买的东西的数量,并根据他们的年龄来加权购买某物的概率。

这段代码最终的作用是使用 NumPy 的random.choice()函数随机将每个人分配到一个年龄组。然后我将分配购买东西的概率,并且我已经加权,使得年轻人购买东西的可能性小于老年人。我将遍历 10 万人,并在遍历过程中将所有东西加起来,最终得到两个 Python 字典:一个给出了每个年龄组的总人数,另一个给出了每个年龄组内购买的总数。我还将跟踪总体购买的总数。让我们运行这段代码。

如果你想花点时间在脑海中思考并弄清楚代码是如何工作的,你可以使用 IPython Notebook。你以后也可以回头看。让我们看看我们最终得到了什么。

我们的totals字典告诉我们每个年龄段有多少人,这与我们预期的一样是相当均匀的。每个年龄组购买的数量实际上是随年龄增长而增加的,所以 20 岁的人只购买了大约 3000 件东西,70 岁的人购买了大约 11000 件东西,总体上整个人口购买了大约 45000 件东西。

让我们使用这些数据来玩玩条件概率的概念。首先让我们弄清楚在你 30 岁时购买东西的概率是多少。如果我们将购买表示为 E,将你 30 岁的事件表示为 F,那么表示为P(E|F)

现在我们有了这个复杂的方程,它给出了一种计算P(E|F)给定P(E,F)P(E)的方法,但我们不需要那个。你不能只是盲目地应用方程。你必须直观地思考你的数据。它告诉我们什么?我想要计算在你 30 岁时购买东西的概率。我有计算它所需的所有数据。

PEF = float(purchases[30]) / float(totals[30]) 

我知道 30 岁的人购买的东西在购买[30]桶中有多少,我也知道有多少 30 岁的人。所以我可以将这两个数字相除,得到 30 岁购买的比例与 30 岁人数的比率。然后我可以使用 print 命令输出这个比例:

print ("P(purchase | 30s): ", PEF) 

最终我得到了在你 30 岁时购买东西的概率大约为 30%。

P(purchase | 30s): 0.2992959865211 

请注意,如果你使用的是 Python 2,print 命令没有周围的括号,所以应该是:

print "p(purchase | 30s): ", PEF 

如果我想找到P(F),那就是总体 30 岁的概率,我可以将 30 岁的总人数除以我的数据集中的人数,即 10 万:

PF = float(totals[30]) / 100000.0 
print ("P(30's): ", PF) 

如果你使用的是 Python 2,再次删除 print 语句周围的括号。这应该得到以下输出:

P(30's): 0.16619 

我知道在你30 岁的概率大约是 16%。

我们现在要找出P(E),这只是代表不考虑年龄的总体购买概率:

PE = float(totalPurchases) / 100000.0 
print ("P(Purchase):", PE) 

P(Purchase): 0.45012 

在这个例子中,这大约是 45%。我只需将所有人购买的东西的总数除以总人数,就可以得到总体购买的概率。

好了,那么我有什么?我有在你 30 岁时购买东西的概率大约为 30%,然后我有总体购买东西的概率大约为 45%。

现在如果 E 和 F 是独立的,如果年龄不重要,那么我期望P(E|F)大约等于P(E)。我期望在你 30 岁时购买东西的概率大约等于总体购买东西的概率,但事实并非如此,对吧?因为它们不同,这告诉我它们实际上是有依赖关系的。所以这是一种使用条件概率来揭示数据中这些依赖关系的方法。

让我们在这里做一些更多的符号表示。如果你看到像P(E)P(F)这样的东西在一起,那意味着将这些概率相乘在一起。我可以简单地取购买的总体概率乘以在你30 岁的总体概率:

print ("P(30's)P(Purchase)", PE * PF) 

P(30's)P(Purchase) 0.07480544280000001 

这大约是 7.5%。

仅仅从概率的工作方式来看,我知道如果我想要得到两件事情同时发生的概率,那就等同于将它们各自的概率相乘。所以结果是P(E,F)发生,就等同于P(E)P(F)

print ("P(30's, Purchase)", float(purchases[30]) / 100000.0) 
P(30's, Purchase) 0.04974 

现在由于数据的随机分布,它并不完全相同。记住,我们在谈论概率,但它们大致相同,这是有道理的,大约 5%与 7%,足够接近。

现在这又不同于P(E|F),所以在你30 岁和购买某物的概率与在你30 岁的情况下购买某物的概率是不同的。

现在让我们做一个小小的检查。我们可以检查我们在之前的条件概率部分看到的方程式,即购买某物的概率,假设你是30 岁,等同于在你30 岁和购买某物的概率除以购买某物的概率。也就是说,我们检查P(E|F)=P(E,F)/P(F)

(float(purchases[30]) / 100000.0) / PF  

这给了我们:

Out []:0.29929598652145134 

果然,它起作用了。如果我取购买某物的概率,假设你是30 岁,除以总体概率,我们最终得到大约 30%,这几乎就是我们最初得出的P(E|F)。所以这个方程式是有效的,耶!

好了,有些东西确实很难理解。我知道有点令人困惑,但如果需要的话,再看一遍,研究一下,确保你理解这里发生了什么。我尽量在这里放了足够的例子来说明不同的思考方式。一旦你内化了它,我要挑战你实际上在这里做一些工作。

条件概率作业

我希望你修改以下 Python 代码,这些代码在前面的部分中使用过。

from numpy import random 
random.seed(0) 

totals = {20:0, 30:0, 40:0, 50:0, 60:0, 70:0} 
purchases = {20:0, 30:0, 40:0, 50:0, 60:0, 70:0} 
totalPurchases = 0 
for _ in range(100000): 
ageDecade = random.choice([20, 30, 40, 50, 60, 70]) 
purchaseProbability = 0.4 
totals[ageDecade] += 1 
if (random.random() < purchaseProbability): 
totalPurchases += 1 
purchases[ageDecade] += 1 

修改它以实际上不让购买和年龄之间存在依赖关系。同样让它成为均匀分布的机会。看看这对你的结果有什么影响。你最终得到了非常不同的在你30 岁购买东西的条件概率和总体购买东西的概率吗?这告诉了你关于你的数据和这两个不同属性之间关系的什么?继续尝试一下,确保你实际上可以从这些数据中得到一些结果并理解发生了什么,我马上就会运行我的解决方案来解决这个问题。

所以这就是条件概率,无论是在理论上还是在实践中。你可以看到它有很多微妙之处,还有很多令人困惑的符号表示。如果需要,回过头再看一遍这一节。我给了你一个作业,所以现在去做吧,看看你是否真的可以修改我的代码在那个 IPython 笔记本中为不同年龄组产生一个恒定的购买概率。然后回来,我们将看看我是如何解决这个问题以及我的结果是什么。

我的作业解决方案

你做完作业了吗?希望如此。让我们来看看我对在一个虚假数据集中如何使用条件概率来告诉我们年龄和购买概率之间是否存在关系的问题的解决方案。

提醒一下,我们试图做的是消除年龄和购买概率之间的依赖关系,并看看我们是否能够在我们的条件概率值中实际反映出来。这是我得到的:

from numpy import random 
random.seed(0) 

totals = {20:0, 30:0, 40:0, 50:0, 60:0, 70:0} 
purchases = {20:0, 30:0, 40:0, 50:0, 60:0, 70:0} 
totalPurchases = 0 
for _ in range(100000): 
    ageDecade = random.choice([20, 30, 40, 50, 60, 70]) 
    purchaseProbability = 0.4 
    totals[ageDecade] += 1 
    if (random.random() < purchaseProbability): 
        totalPurchases += 1 
        purchases[ageDecade] += 1 

我在这里所做的是,我拿了原始的代码片段,用于创建我们的年龄组字典以及每个年龄组购买了多少,针对 10 万个随机人。我没有让购买概率依赖于年龄,而是让它成为 40%的常数概率。现在我们只是随机地将人分配到一个年龄组,他们都有相同的购买某物的概率。让我们继续运行。

现在,这一次,如果我计算P(E|F),也就是,给定你处于30 岁的情况下购买某物的概率,我得到的结果大约是 40%。

PEF = float(purchases[30]) / float(totals[30]) 
print ("P(purchase | 30s): ", PEF) 

P(purchase | 30s):  0.398760454901 

如果我将其与总体购买概率进行比较,那也是大约 40%。

PE = float(totalPurchases) / 100000.0 
print ("P(Purchase):", PE) 

P(Purchase): 0.4003 

我可以看到,如果你处于30 岁,购买某物的概率与不考虑你的年龄而言购买某物的概率大致相同(也就是,P(E|F)P(E)非常接近)。这表明这两件事之间没有真正的关系,实际上,我知道从这些数据中并没有关系。

现在在实践中,你可能只是看到了随机的机会,所以你会想要观察不止一个年龄组。你会想要观察不止一个数据点,以查看是否真的存在关系,但这表明在我们修改的这个样本数据中,年龄和购买概率之间没有关系。

所以,这就是条件概率的作用。希望你的解决方案相当接近并且有类似的结果。如果不是,回去研究我的解决方案。这就在这本书的数据文件中,ConditionalProbabilitySolution.ipynb,如果你需要打开它并研究它并进行试验。显然,数据的随机性会使你的结果有些不同,并且会取决于你对总体购买概率的选择,但这就是这个想法。

有了这个背景,让我们继续讲贝叶斯定理。

贝叶斯定理

现在你了解了条件概率,你就能理解如何应用贝叶斯定理,这是基于条件概率的。这是一个非常重要的概念,特别是如果你要进入医学领域,但它也是广泛适用的,你马上就会明白为什么。

你会经常听到这个,但并不是很多人真正理解它的意义。有时它可以非常定量地告诉你,当人们用统计数据误导你时,所以让我们看看它是如何起作用的。

首先,让我们从高层次来谈谈贝叶斯定理。贝叶斯定理就是:给定 B 的情况下 A 的概率等于 A 的概率乘以给定 A 的情况下 B 的概率除以 B 的概率。所以你可以用任何你想要的东西来替换 A 和 B。

关键的见解是,依赖于 B 的某事的概率很大程度上取决于 B 和 A 的基本概率。人们经常忽视这一点。

一个常见的例子是药物测试。我们可能会说,如果你对某种药物测试呈阳性,那么你是实际使用该药物的概率是多少。贝叶斯定理之所以重要,是因为它指出这在很大程度上取决于 A 和 B 的概率。在整个人群中,对于药物测试呈阳性的概率,实际上很大程度上取决于整体使用该药物的概率和整体测试呈阳性的概率。药物测试的准确性很大程度上取决于整体人群中使用该药物的概率,而不仅仅是测试的准确性。

这也意味着在给定 A 的情况下 B 的概率并不等同于在给定 B 的情况下 A 的概率。也就是说,在测试呈阳性的情况下成为药物用户的概率可能与在成为药物用户的情况下测试呈阳性的概率非常不同。您可以看到这是一个非常现实的问题,在医学诊断测试或药物测试中会产生很多假阳性。您仍然可以说测试检测用户的概率可能非常高,但这并不一定意味着在测试呈阳性的情况下成为用户的概率很高。这是两回事,而贝叶斯定理允许您量化这种差异。

让我们举一个例子来更好地理解。

再次,药物测试可以是应用贝叶斯定理证明观点的常见例子。即使高度准确的药物测试也可能产生比真阳性更多的假阳性。因此,在我们的例子中,我们将提出一种药物测试,该测试可以在 99%的时间内准确识别药物用户,并且对非用户有 99%的准确的阴性结果,但是实际上只有 0.3%的总体人口实际使用该药物。因此,我们实际上成为药物用户的概率非常小。看起来非常高的 99%的准确性实际上还不够高,对吧?

我们可以通过以下方式计算出概率:

  • 事件 A = 使用该药物的用户

  • 事件 B = 测试呈阳性

事件 A 表示您使用某种药物,事件 B 表示您使用此药物测试呈阳性。

我们需要计算总体测试呈阳性的概率。我们可以通过计算用户测试呈阳性的概率和非用户测试呈阳性的概率的总和来计算出来。因此,在这个例子中,P(B)计算为 1.3%(0.990.003+0.010.997)。因此,我们有了 B 的概率,即在不了解您的其他情况下,总体上测试呈阳性的概率。

让我们来计算一下,在测试呈阳性的情况下实际成为药物用户的概率。

因此,在实际成为药物用户的情况下测试呈阳性的概率计算为总体上成为药物用户的概率(P(A)),即 3%(您知道 3%的人口是药物用户),乘以P(B|A),即在成为用户的情况下测试呈阳性的概率,除以总体上测试呈阳性的概率,即 1.3%。再次,这个测试听起来非常准确,准确率为 99%。我们有 0.3%的人口使用药物,乘以 99%的准确性,除以总体上测试呈阳性的概率,即 1.3%。因此,您在测试呈阳性的情况下实际成为该药物用户的概率只有 22.8%。因此,即使这种药物测试在 99%的时间内准确,它仍然在大多数情况下提供了错误的结果。

即使P(B|A)很高(99%),也不意味着P(A|B)很高。

人们经常忽视这一点,因此如果有一件事可以从贝叶斯定理中学到的,那就是始终要持怀疑态度。将贝叶斯定理应用于这些实际问题,您经常会发现,听起来高准确率实际上可能会产生非常误导性的结果,如果您处理的是某个问题的总体发生率很低的情况。我们在癌症筛查和其他类型的医学筛查中也看到了同样的情况。这是一个非常现实的问题;有很多人因为不理解贝叶斯定理而接受了非常真实且不必要的手术。如果您要从事医学行业的大数据工作,请,请,请记住这个定理。

这就是贝叶斯定理。永远记住,给定某事物的概率并不等同于反过来,它实际上很大程度上取决于你正在测量的这两件事物的基本概率。这是一件非常重要的事情要牢记,并且始终要以此为依据来查看你的结果。贝叶斯定理为你提供了量化这种影响的工具。我希望它能够证明有用。

总结

在本章中,我们讨论了如何绘制和图形化你的数据,以及如何使用 Python 中的matplotlib库使你的图形看起来漂亮。我们还介绍了协方差和相关性的概念。我们看了一些例子,并使用 Python 计算了协方差和相关性。我们分析了条件概率的概念,并看了一些例子以更好地理解它。最后,我们看到了贝叶斯定理及其重要性,特别是在医学领域。

在下一章中,我们将讨论预测模型。

第四章:预测模型

在本章中,我们将探讨预测建模是什么,以及它如何使用统计数据来预测现有数据的结果。我们将涵盖现实世界的例子,以更好地理解这些概念。我们将了解回归分析的含义,并详细分析其中的一些形式。我们还将看一个预测汽车价格的例子。

这些是本章中我们将涵盖的主题:

  • 线性回归及其在 Python 中的实现方式

  • 多项式回归,其应用和示例

  • 多元回归及其在 Python 中的实现方式

  • 我们将构建一个使用 Python 预测汽车价格的示例

  • 多层模型的概念和一些需要了解的内容

线性回归

让我们谈谈回归分析,这是数据科学和统计学中非常流行的话题。它的核心是试图将曲线或某种函数拟合到一组观察结果中,然后使用该函数来预测你尚未见过的新值。这就是线性回归的全部内容!

因此,线性回归是将一条直线拟合到一组观察结果中。例如,假设我测量了一群人,我测量的两个特征是他们的体重和身高:

我在x轴上显示了体重,y轴上显示了身高,我可以绘制所有这些数据点,就像人们的体重与身高一样,我可以说,“嗯,这看起来像是一个线性关系,不是吗?也许我可以拟合一条直线并用它来预测新值”,这就是线性回归的作用。在这个例子中,我得到了斜率为 0.6 和y截距为 130.2,这定义了一条直线(一条直线的方程是y=mx+b,其中 m 是斜率,b 是y截距)。给定一个斜率和一个y截距,最能适应我拥有的数据,我可以使用这条线来预测新值。

你可以看到我观察到的重量只涵盖了重 100 公斤的人。如果我有一个重 120 公斤的人怎么办?嗯,我可以使用这条线来计算基于先前数据的 120 公斤的人的身高。

我不知道为什么他们称之为回归。回归有点意味着你在做一些事情。我猜你可以把它看作是在根据你过去的观察结果创建一条线来预测新值,时间上倒退,但这似乎有点牵强。说实话,这只是一个令人困惑的术语,我们用非常花哨的术语来掩盖我们用非常简单的概念做的事情的一种方式。它只是将一条直线拟合到一组数据点。

普通最小二乘法技术

线性回归是如何工作的?在内部,它使用一种称为普通最小二乘法的技术;它也被称为 OLS。你可能也会看到这个术语被提及。它的工作方式是试图最小化每个点与直线之间的平方误差,其中误差只是每个点与你所拥有的直线之间的距离。

因此,我们总结了所有这些错误的平方和,这听起来很像我们计算方差时的情况,对吧,只是不是相对于均值,而是相对于我们定义的直线。我们可以测量数据点相对于该直线的方差,并通过最小化该方差,我们可以找到最适合的直线:

现在你永远不必自己费力去做这件事,但如果你因某种原因不得不这样做,或者如果你只是好奇发生了什么,我现在会为你描述整体算法,以及如果有一天你需要自己费力计算斜率和y截距,你将如何去做。这真的并不复杂。

还记得线的斜率截距方程吗?它是y=mx+c。斜率实际上就是两个变量之间的相关性乘以Y的标准差除以X的标准差。标准差在数学中自然地出现可能看起来有点奇怪,但是记住相关性也包含了标准差,所以不太奇怪你必须重新引入这个术语。

然后,截距可以计算为Y的平均值减去斜率乘以X的平均值。再次强调,尽管这并不是非常困难,Python 会为你完成所有计算,但重点是这些并不是难以运行的复杂事情。它们实际上可以非常高效地完成。

记住,最小二乘法最小化了每个点到线的平方误差的总和。另一种思考线性回归的方式是,你正在定义一条代表观察线的最大可能性的线;也就是说,y值在给定x值时的最大概率。

有时人们称线性回归为最大似然估计,这只是人们给一个非常简单的东西起了一个花哨的名字的又一个例子,所以如果你听到有人谈论最大似然估计,他们实际上是在谈论回归。他们只是试图显得很聪明。但现在你也知道了这个术语,所以你也可以显得很聪明。

梯度下降技术

进行线性回归有多种方法。我们已经谈到了普通最小二乘法是拟合一组数据的简单方法,但也有其他技术,梯度下降就是其中之一,它在三维数据中效果最好。因此,它试图为你跟随数据的轮廓。这非常高级,显然计算成本更高一些,但是 Python 确实让你很容易地尝试它,如果你想将其与普通最小二乘法进行比较。

使用梯度下降技术在处理三维数据时是有意义的。

通常情况下,最小二乘法是进行线性回归的一个完全合理的选择,它总是一个合法的事情,但是如果你遇到梯度下降,你会知道那只是进行线性回归的另一种方式,通常在更高维度的数据中看到。

确定系数或 R 平方

那么,我如何知道我的回归有多好?我的线对数据的拟合程度如何?这就是 R 平方的作用,R 平方也被称为确定系数。虽然有人可能会试图显得聪明一点,称其为确定系数,但通常被称为 R 平方。

它是你的模型捕捉到的 Y 的总变化的分数。你的线有多好地跟随了发生的变化?我们在你的线的两侧是否得到了相等数量的变化?这就是 R 平方在衡量的。

计算 R 平方

要实际计算该值,取 1 减去平方误差的总和除以平方变化的总和:

因此,计算起来并不是很困难,但是 Python 会为你提供函数,可以帮你计算,所以你实际上不需要自己进行数学计算。

解释 R 平方

对于 R 平方,你将得到一个从 0 到 1 的值。0 意味着你的拟合很糟糕。它没有捕捉到数据的任何变化。而 1 是完美的拟合,数据的所有变化都被这条线捕捉到,你在线的两侧看到的所有变化应该是相同的。所以 0 是糟糕的,1 是好的。这就是你真正需要知道的。介于两者之间的值就是介于两者之间的值。低 R 平方值意味着拟合很差,高 R 平方值意味着拟合很好。

正如你将在接下来的部分中看到的,有多种方法可以进行回归。线性回归是其中之一。这是一种非常简单的技术,但也有其他技术,你可以使用 R 平方作为一个定量的度量来衡量给定回归对一组数据点的拟合程度,然后使用它来选择最适合你的数据的模型。

使用 Python 计算线性回归和 R 平方

现在让我们来玩一下线性回归,实际计算一些线性回归和 R 平方。我们可以从这里创建一些 Python 代码,生成一些随机的数据,实际上是线性相关的。

在这个例子中,我将捏造一些关于页面渲染速度和人们购买金额的数据,就像之前的例子一样。我们将捏造网站加载所需时间和人们在该网站上花费的金额之间的线性关系:

%matplotlib inline
import numpy as np
from pylab import *
pageSpeeds = np.random.normal(3.0, 1.0, 1000)
purchaseAmount = 100 - (pageSpeeds + np.random.normal(0, 0.1,
1000)) * 3
scatter(pageSpeeds, purchaseAmount) 

我在这里所做的只是制作了一个随机的、以 3 秒为中心的页面速度的正态分布,标准差为 1 秒。我将购买金额设为它的线性函数。因此,我将它设为 100 减去页面速度加上一些围绕它的正态随机分布,乘以 3。如果我们散点图,我们可以看到数据最终看起来是这样的:

你可以通过肉眼观察到确实存在线性关系,这是因为我们在源数据中硬编码了一个真正的线性关系。

现在让我们看看是否可以通过最小二乘法找出最佳拟合线。我们讨论了如何进行最小二乘法和线性回归,但你不必自己进行任何数学计算,因为 SciPy 包有一个stats包,你可以导入:

from scipy import stats

slope, intercept, r_value, p_value, std_err =     
stats.linregress(pageSpeeds, purchaseAmount) 

你可以从scipy中导入stats,然后你可以在你的两个特征上调用stats.linregress()。因此,我们有一个页面速度(pageSpeeds)的列表和一个相应的购买金额(purchaseAmount)的列表。linregress()函数将给我们一堆东西,包括斜率、截距,这是我需要定义最佳拟合线的东西。它还给我们r_value,从中我们可以得到 R 平方来衡量拟合的质量,以及一些我们稍后会讨论的东西。现在,我们只需要斜率、截距和r_value,所以让我们继续运行这些。我们将从找到线性回归的最佳拟合开始:

r_value ** 2

你的输出应该是这样的:

现在我们得到的线的 R 平方值是 0.99,几乎是 1.0。这意味着我们有一个非常好的拟合,这并不太令人惊讶,因为我们确保这些数据之间存在真正的线性关系。即使在该线周围存在一些方差,我们的线也捕捉到了这些方差。我们在线的两侧大致有相同数量的方差,这是一件好事。这告诉我们我们确实有线性关系,我们的模型很适合我们的数据。

让我们画出那条线:

import matplotlib.pyplot as plt
def predict(x):
return slope * x + intercept
fitLine = predict(pageSpeeds)
plt.scatter(pageSpeeds, purchaseAmount)
plt.plot(pageSpeeds, fitLine, c='r')
plt.show()

以下是前面代码的输出:

这段代码将创建一个函数来绘制最佳拟合线与数据一起。这里有一些 Matplotlib 的魔法。我们将创建一个fitLine列表,并使用我们编写的predict()函数来获取pageSpeeds(我们的x轴),并从中创建 Y 函数。因此,我们不是使用花费金额的观察值,而是使用linregress()调用返回的斜率乘以x加上截距。基本上在这里,我们将做一个散点图,就像我们以前做的那样,来显示原始数据点,即观察值。

然后我们还将在同一个pyplot实例上调用plot,使用我们得到的线方程创建的fitLine,并将它们一起显示出来。当我们这样做时,图表看起来像下面这样:

你可以看到我们的直线实际上非常适合我们的数据!它正好位于中间,你只需要使用这个预测函数来预测新值。给定一个新的之前未见过的页面速度,我们可以使用斜率乘以页面速度加上截距来预测花费的金额。就是这么简单,我觉得很棒!

线性回归的活动

现在是时候动手了。尝试增加测试数据中的随机变化,并查看是否会产生影响。记住,R 平方是拟合程度的一个度量,我们捕捉了多少方差,所以方差的数量,嗯...你看看它是否真的有影响。

这就是线性回归,一个非常简单的概念。我们所做的就是将一条直线拟合到一组观察结果,然后我们可以使用这条直线来预测新值。就是这么简单。但是为什么要限制自己只使用一条直线呢?我们可以做其他更复杂的回归。我们接下来会探讨这些。

多项式回归

我们已经讨论了线性回归,其中我们将一条直线拟合到一组观察结果。多项式回归是我们接下来要讨论的话题,它使用更高阶的多项式来拟合你的数据。有时候你的数据可能并不适合一条直线。这就是多项式回归的用武之地。

多项式回归是回归的更一般情况。那么为什么要限制自己只使用一条直线呢?也许你的数据实际上并没有线性关系,或者可能有某种曲线关系,对吧?这种情况经常发生。

并非所有的关系都是线性的,但线性回归只是我们可以做的整个回归类别中的一个例子。如果你还记得我们最终得到的线性回归线的形式是y = mx + b,其中 m 和 b 是我们从普通最小二乘法线性回归分析中得到的值,或者你选择的任何方法。现在这只是一个一次多项式。阶数或度数就是你看到的 x 的幂。所以这是一个一次多项式。

现在如果我们想的话,我们也可以使用二次多项式,它看起来像y = ax² + bx + c。如果我们使用二次多项式进行回归,我们会得到 a、b 和 c 的值。或者我们可以使用三次多项式,它的形式是ax³ + bx² + cx + d。阶数越高,你可以表示的曲线就越复杂。所以,你将 x 的更多次幂混合在一起,你就可以得到更复杂的形状和关系。

但并不是阶数越高越好。通常你的数据中有一些自然关系并不是那么复杂,如果你发现自己在拟合数据时使用了非常大的阶数,你可能是在过度拟合!

注意过度拟合!

  • 不要使用比你需要的更多的度数

  • 首先可视化你的数据,看看可能存在多复杂的曲线

  • 可视化拟合并检查你的曲线是否在努力适应异常值

  • 高 R 平方仅意味着你的曲线很好地拟合了训练数据;它可能是一个好的预测器,也可能不是

如果你的数据有点乱七八糟,方差很大,你可以疯狂地创建一条上下波动的直线,试图尽可能地拟合数据,但实际上这并不代表数据的内在关系。它不能很好地预测新值。

所以,始终从可视化你的数据开始,考虑曲线实际上需要多复杂。现在你可以使用 R 平方来衡量你的拟合有多好,但要记住,这只是衡量这条曲线有多好地拟合了你的训练数据——也就是说,你用来实际进行预测的数据。它并不衡量你准确预测未来的能力。

稍后,我们将讨论一些防止过拟合的技术,称为训练/测试,但现在你只需要用眼睛来确保你没有过拟合,并且不要给函数添加比你需要的更多的度数。当我们探索一个例子时,这将更有意义,所以让我们接着做。

使用 NumPy 实现多项式回归

幸运的是,NumPy 有一个polyfit函数,可以让你轻松地玩弄这个并尝试不同的结果,所以让我们去看看。多项式回归的乐趣时刻到了。顺便说一下,我真的觉得这很有趣。实际上看到所有那些高中数学实际上应用到一些实际的场景中,这有点酷。打开PolynomialRegression.ipynb,让我们玩得开心一点。

让我们在页面速度和我们的购买金额虚假数据之间创建一个新的关系,这一次我们将创建一个不是线性的更复杂的关系。我们将把页面速度作为购买金额的除法函数的一部分:

%matplotlib inline
from pylab import *
np.random.seed(2)
pageSpeeds = np.random.normal(3.0, 1.0, 1000)
purchaseAmount = np.random.normal(50.0, 10.0, 1000) / pageSpeeds
scatter(pageSpeeds, purchaseAmount)

如果我们做一个散点图,我们得到以下结果:

顺便说一下,如果你想知道np.random.seed这一行是做什么的,它创建一个随机种子值,这意味着当我们进行后续的随机操作时,它们将是确定性的。通过这样做,我们可以确保每次运行这段代码时,我们都得到完全相同的结果。这将在以后变得重要,因为我将建议你回来实际尝试不同的拟合来比较你得到的拟合。所以,重要的是你从相同的初始点开始。

你可以看到这并不是一个线性关系。我们可以尝试对其进行拟合,对于大部分数据来说可能还可以,也许在图表右侧的下方,但在左侧就不太行了。我们实际上更多的是一个指数曲线。

现在碰巧 NumPy 有一个polyfit()函数,允许你对这些数据进行任意次数的多项式拟合。所以,例如,我们可以说我们的x轴是我们拥有的页面速度(pageSpeeds)的数组,我们的y轴是我们拥有的购买金额(purchaseAmount)的数组。然后我们只需要调用np.polyfit(x, y, 4),意思是我们想要一个四次多项式拟合这些数据。

x = np.array(pageSpeeds)
y = np.array(purchaseAmount)
p4 = np.poly1d(np.polyfit(x, y, 4))

让我们继续运行。它运行得相当快,然后我们可以绘制出来。所以,我们将在这里创建一个小图表,绘制我们原始点与预测点的散点图。

import matplotlib.pyplot as plt

xp = np.linspace(0, 7, 100)
plt.scatter(x, y)
plt.plot(xp, p4(xp), c='r')
plt.show()

输出看起来像下面的图表:

目前看起来是一个相当好的拟合。不过你要问自己的是,“我是不是过度拟合了?我的曲线看起来是不是真的在努力适应异常值?”我发现实际上并没有发生这种情况。我并没有看到太多疯狂的事情发生。

如果我有一个非常高阶的多项式,它可能会在顶部上升以捕捉那个异常值,然后向下下降以捕捉那里的异常值,并且在我们有很多密度的地方会变得更加稳定,也许它最终可能会到处尝试适应最后一组异常值。如果你看到这种无稽之谈,你就知道你的多项式阶数太多了,你应该把它降下来,因为虽然它适合你观察到的数据,但对于预测你没有看到的数据是没有用的。

想象一下,我有一条曲线,它向上飞起,然后又回到原点以适应异常值。我对中间的某些值的预测不会准确。曲线实际上应该在中间。在本书的后面,我们将讨论检测这种过拟合的主要方法,但现在,请只是观察它,并知道我们以后会更深入地讨论。

计算 r 平方误差

现在我们可以测量 r 平方误差。通过在sklearn.metrics中的r2_score()函数中取y和预测值(p4(x)),我们可以计算出来。

from sklearn.metrics import r2_score
r2 = r2_score(y, p4(x))

print r2

输出如下:

我们的代码将一组观察结果与一组预测进行比较,并为你计算 r 平方,只需一行代码!我们的 r 平方结果为 0.829,这还不错。记住,零是不好的,一是好的。0.82 接近一,不完美,直观上是有道理的。你可以看到我们的线在数据的中间部分非常好,但在极端左侧和极端右侧并不那么好。所以,0.82 听起来是合理的。

多项式回归的活动

我建议你深入研究这些东西。尝试不同阶数的多项式。回到我们运行polyfit()函数的地方,尝试除了 4 之外的不同值。你可以使用 1,那就会回到线性回归,或者你可以尝试一些非常高的值,比如 8,也许你会开始看到过拟合。看看它的影响。你会想要改变它。例如,让我们来看一个三次多项式。

x = np.array(pageSpeeds)
y = np.array(purchaseAmount)

p4 = np.poly1d(np.polyfit(x, y, 3))  

只需不断运行每一步,你就可以看到它的影响...

我们的三次多项式显然不如四次多项式拟合得好。如果你实际测量 r 平方误差,定量上会更糟,但如果我太高,你可能会开始看到过拟合。所以,只是玩一下,尝试不同的值,了解不同阶数的多项式对回归的影响。去动手尝试学习一些东西。

这就是多项式回归。再次强调,你需要确保你不会给问题增加比你需要的更多的度数。使用恰到好处的数量来找到看起来符合你的数据的直观拟合。太多可能导致过拟合,而太少可能导致拟合不足...所以你现在可以同时使用你的眼睛和 r 平方指标来找出你的数据的正确度数。让我们继续。

多元回归和预测汽车价格

那么,如果我们试图预测基于多于一个其他属性的值会发生什么?假设人的身高不仅取决于他们的体重,还取决于他们的遗传或其他一些可能影响它的因素。那么,多元分析就派上用场了。你实际上可以构建同时考虑多个因素的回归模型。用 Python 做起来实际上非常容易。

让我们谈谈多元回归,这有点复杂。多元回归的想法是:如果有多个因素影响你要预测的事物会怎么样?

在我们之前的例子中,我们看了线性回归。例如,我们讨论了基于体重预测人的身高。我们假设体重是影响身高的唯一因素,但也许还有其他因素。我们还研究了页面速度对购买金额的影响。也许影响购买金额的因素不仅仅是页面速度,我们想要找出这些不同因素如何结合在一起影响价值。这就是多元回归的作用。

我们现在要看的示例是这样的。假设您试图预测汽车的售价。它可能基于该汽车的许多不同特征,例如车身风格、品牌、里程数;谁知道,甚至还取决于轮胎的好坏。其中一些特征对于预测汽车价格更为重要,但您希望一次考虑所有这些特征。

因此,我们在这里前进的方式仍然是使用最小二乘法来拟合模型到您的一组观察结果。不同之处在于,我们将为您拥有的每个不同特征有一堆系数。

因此,例如,我们最终得到的价格模型可能是 alpha 的线性关系,一些常数,有点像您的 y 截距,再加上里程的一些系数,再加上年龄的一些系数,再加上它有多少个门的一些系数:

一旦您得到了那些最小二乘分析的系数,我们可以利用这些信息来弄清楚,每个特征对我的模型有多重要。因此,如果我得到了某些东西的系数非常小,比如车门数量,那就意味着车门数量并不重要,也许我应该完全将其从我的模型中移除,以使其更简单。

这是我在这本书中应该更经常说的一件事。在数据科学中,您总是希望做最简单有效的事情。不要过于复杂化事情,因为通常简单的模型效果最好。如果您能找到恰到好处的复杂度,但不要过多,那通常就是正确的模型。无论如何,这些系数给了您一种实际的方式,“嘿,有些因素比其他因素更重要。也许我可以丢弃其中一些因素。”

现在我们仍然可以使用 r-squared 来衡量多元回归的拟合质量。它的工作方式相同,尽管在进行多元回归时,您需要假设因素本身不相互依赖...而这并不总是正确的。因此,有时您需要将这个小小的警告放在脑后。例如,在这个模型中,我们将假设汽车的里程和年龄不相关;但实际上,它们可能非常紧密相关!这是这种技术的局限性,它可能根本没有捕捉到某种效应。

使用 Python 进行多元回归

幸运的是,Python 有一个名为statsmodel的包,可以很容易地进行多元回归。让我们深入了解一下它的工作原理。让我们使用 Python 进行一些多元回归。我们将使用一些关于凯利蓝皮书中汽车价值的真实数据。

import pandas as pd
df = pd.read_excel('http://cdn.sundog-soft.com/Udemy/DataScience/cars.xls')

我们要在这里介绍一个名为pandas的新包,它让我们非常容易地处理表格数据。它让我们能够轻松读取表格数据,并以不同的方式重新排列、修改、切片和切块它们。我们将在未来经常使用它。

我们将导入pandas作为pdpd有一个read_Excel()函数,我们可以使用它来从 Web 通过 HTTP 读取 Microsoft Excel 电子表格。因此,pandas 有非常棒的功能。

我已经提前为您在我的域上托管了该文件,如果我们运行它,它将加载到我们称之为dfDataFrame对象中。现在我可以在这个DataFrame上调用head(),只显示它的前几行:

df.head()

以下是前面代码的输出:

实际数据集要大得多。这只是前几个样本。因此,这是关于里程、制造商、型号、修剪、类型、车门、巡航、音响和皮革的真实数据。

好的,现在我们要使用pandas将其拆分为我们关心的特征。我们将创建一个模型,试图仅基于里程、型号和车门数量来预测价格,没有其他因素。

import statsmodels.api as sm

df['Model_ord'] = pd.Categorical(df.Model).codes
X = df[['Mileage', 'Model_ord', 'Doors']]
y = df[['Price']]

X1 = sm.add_constant(X)
est = sm.OLS(y, X1).fit()

est.summary() 

现在我遇到的问题是,模型是一个文本,比如 Buick 的世纪,正如您所记得的,当我进行这种分析时,一切都需要是数字。在代码中,我使用pandas中的Categorical()函数来将DataFrame中看到的模型名称转换为一组数字;也就是一组代码。我将说我的模型的 x 轴输入是里程(Mileage),转换为序数值的模型(Model_ord),和车门数量(Doors)。我试图在 y 轴上预测的是价格(Price)。

您可以看到这里的 R 平方值非常低。实际上,这不是一个很好的模型,但我们可以了解各种错误的一些见解,有趣的是,最低的标准误差与里程相关联。

现在我之前说过,系数是一种确定哪些项目重要的方法,但前提是您的输入数据已经标准化。也就是说,如果所有数据都在 0 到 1 的相同尺度上。如果不是,那么这些系数在一定程度上是在补偿它所看到的数据的尺度。如果您处理的不是标准化数据,就像在这种情况下一样,查看标准误差更有用。在这种情况下,我们可以看到里程实际上是这个特定模型的最大因素。我们早些时候能否已经想到这一点呢?嗯,我们只需稍微切片和切块就能发现车门数量实际上并不会对价格产生太大影响。让我们运行以下小行:

y.groupby(df.Doors).mean()

这里有一点pandas的语法。在 Python 中只需一行代码就能做到这一点,这很酷!这将打印出一个新的DataFrame,显示给定车门数量的平均价格:

接下来的两行代码只是创建了一个我称之为est的模型,它使用普通最小二乘法(OLS),并使用我给它的列MileageModel_ordDoors进行拟合。然后我可以使用 summary 调用来打印出我的模型是什么样子的:

我可以看到平均两门车的售价实际上比平均四门车的售价更高。如果有的话,车门数量和价格之间存在负相关,这有点令人惊讶。不过,这是一个小数据集,所以我们当然不能从中得出太多意义。

多元回归的活动

作为一个活动,请随意修改假输入数据。您可以下载数据并在电子表格中进行修改。从本地硬盘读取数据,而不是从 HTTP 读取,看看您可以有什么样的不同。也许您可以制作一个行为不同且拥有更好适合的模型的数据集。也许您可以更明智地选择特征来构建您的模型。所以,请随意尝试一下,然后我们继续。

这就是多元分析和其运行示例。和我们探索的多元分析概念一样重要的是我们在 Python 笔记本中所做的一些事情。所以,您可能需要回到那里,确切地研究发生了什么。

我们介绍了 pandas 和处理 pandas 和 DataFrame 对象的方法。pandas 是一个非常强大的工具。我们将在未来的章节中更多地使用它,但请确保您开始注意这些事情,因为这些将是您在 Python 技能中处理大量数据和组织数据的重要技术。

多级模型

现在谈论多层次模型是有意义的。这绝对是一个高级话题,我不会在这里详细讨论。我现在的目标是向你介绍多层次模型的概念,并让你了解一些挑战以及在组合它们时如何思考。就是这样。

多层次模型的概念是,一些影响发生在层次结构中的各个层次。例如,你的健康。你的健康可能取决于你的个体细胞的健康程度,而这些细胞可能取决于它们所在的器官的健康程度,而你的器官的健康可能取决于你整体的健康。你的健康可能部分取决于你家庭的健康和你家庭给予你的环境。而你家庭的健康反过来可能取决于你所在城市的一些因素,比如犯罪率、压力和污染程度。甚至超越这些,它可能取决于我们所生活的整个世界的因素。也许世界上医疗技术的状况是一个因素,对吧?

另一个例子:你的财富。你赚多少钱?这取决于你个人的努力,但也取决于你父母的努力,他们能够为你的教育投入多少钱以及你成长的环境,反过来,你的祖父母呢?他们能够创造什么样的环境,能够为你的父母提供什么样的教育,进而影响他们为你的教育和成长提供的资源。

这些都是多层次模型的例子,其中存在一个影响彼此的层次结构。现在多层次模型的挑战是要尝试弄清楚,“我该如何建模这些相互依赖关系?我该如何建模所有这些不同的影响以及它们如何相互影响?”

这里的挑战是识别每个层次中实际影响你试图预测的事物的因素。例如,如果我试图预测整体 SAT 成绩,我知道这部分取决于参加考试的个体孩子,但是孩子的哪些方面很重要呢?可能是基因,可能是他们的个体健康,他们的个体大脑大小。你可以想到任何可能影响个体的因素,可能会影响他们的 SAT 成绩。然后,如果你再往上看,看看他们的家庭环境,看看他们的家庭。家庭的哪些方面可能会影响他们的 SAT 成绩?他们能提供多少教育?父母是否能够辅导孩子学习 SAT 考试中的主题?这些都是第二层次的重要因素。那么他们的社区呢?社区的犯罪率可能很重要。他们为青少年提供的设施以及让他们远离街头的措施等等。

关键是你想要继续关注这些更高的层次,但在每个层次上识别影响你试图预测的事物的因素。我可以继续上升到学校老师的素质、学区的资金、州级的教育政策。你可以看到不同层次的不同因素都会影响你试图预测的事物,而其中一些因素可能存在于多个层次。例如,犯罪率存在于地方和州级。在进行多层次建模时,你需要弄清楚它们如何相互作用。

正如你可以想象的那样,这很快变得非常困难和复杂。这确实远远超出了本书的范围,也超出了任何数据科学入门书籍的范围。这是困难的东西。有整整厚厚的书籍讨论它,你可以写一本完整的书籍,这将是一个非常高级的话题。

那么为什么我要提到多层模型呢?因为我在一些工作描述中看到它被提到,在一些情况下,作为他们希望你了解的内容。我在实践中从未使用过它,但我认为在数据科学职业中重要的是,你至少要熟悉这个概念,知道它的含义以及创建多层模型所涉及的一些挑战。我希望我已经向你介绍了这些概念。有了这些,我们可以继续下一节了。

这就是多层模型的概念。这是一个非常高级的话题,但你至少需要了解这个概念,而这个概念本身是相当简单的。当你试图做出预测时,你只是在不同层次、不同层次之间寻找影响。所以也许有不同层次的影响相互影响,而这些不同层次可能有相互关联的因素。多层建模试图考虑所有这些不同的层次和因素以及它们如何相互作用。放心,这就是你现在需要知道的全部。

总结

在本章中,我们谈到了回归分析,即试图将曲线拟合到一组训练数据,然后使用它来预测新值。我们看到了它的不同形式。我们看了线性回归的概念及其在 Python 中的实现。

我们学习了多项式回归是什么,也就是使用更高次的多项式来为多维数据创建更好、更复杂的曲线。我们还看到了它在 Python 中的实现。

然后我们谈到了多元回归,这是一个稍微复杂一点的概念。我们看到了当有多个因素影响我们要预测的数据时,多元回归是如何使用的。我们看了一个有趣的例子,使用 Python 和一个非常强大的工具 pandas 来预测汽车的价格。

最后,我们看了多层模型的概念。我们了解了一些挑战,以及在将它们组合在一起时如何考虑它们。在下一章中,我们将学习一些使用 Python 的机器学习技术。

第五章:使用 Python 进行机器学习

在本章中,我们将介绍机器学习以及如何在 Python 中实际实现机器学习模型。

我们将研究监督学习和无监督学习的含义,以及它们之间的区别。我们将看到防止过拟合的技术,然后看一个有趣的示例,我们在其中实现了一个垃圾邮件分类器。我们将分析 K 均值聚类是什么,并使用 scikit-learn 对基于收入和年龄的人群进行聚类的工作示例!

我们还将介绍一种非常有趣的机器学习应用,称为决策树,并且我们将在 Python 中构建一个工作示例,用于预测公司的招聘决策。最后,我们将深入探讨集成学习和 SVM 的迷人概念,这些是我最喜欢的机器学习领域之一!

更具体地说,我们将涵盖以下主题:

  • 监督和无监督学习

  • 通过使用训练/测试来避免过拟合

  • 贝叶斯方法

  • 使用朴素贝叶斯实现电子邮件垃圾邮件分类器

  • K 均值聚类的概念

  • Python 中聚类的示例

  • 熵及其测量方法

  • 决策树的概念及其在 Python 中的示例

  • 什么是集成学习

  • 支持向量机SVM)及其在 scikit-learn 中的示例

机器学习和训练/测试

那么什么是机器学习?如果您在维基百科或其他地方查找,它会说这是一种可以从观测数据中学习并基于此进行预测的算法。听起来很花哨,对吧?就像人工智能一样,就像您的计算机内部有一个跳动的大脑。但实际上,这些技术通常非常简单。

我们已经看过回归,我们从中获取了一组观测数据,我们对其进行了拟合,并使用该线进行预测。所以按照我们的新定义,那就是机器学习!您的大脑也是这样工作的。

机器学习中的另一个基本概念是称为训练/测试的东西,它让我们非常聪明地评估我们制作的机器学习模型有多好。当我们现在看无监督和监督学习时,您将看到为什么训练/测试对机器学习如此重要。

无监督学习

现在让我们详细讨论两种不同类型的机器学习:监督学习和无监督学习。有时两者之间可能存在一种模糊的界限,但无监督学习的基本定义是,您不会给模型任何答案来学习。您只是向其提供一组数据,您的机器学习算法会尝试在没有额外信息的情况下理解它:

假设我给它一堆不同的对象,比如这些球和立方体以及一些骰子之类的东西。然后让我们假设有一些算法,它将根据某种相似性度量将这些对象聚类成相互相似的东西。

现在我没有提前告诉机器学习算法,某些对象属于哪些类别。我没有一个可以从中学习的作弊表,其中有一组现有对象和我对其正确分类的信息。机器学习算法必须自行推断这些类别。这是无监督学习的一个例子,我没有一组答案可以让它学习。我只是试图让算法根据所呈现给它的数据自行收集答案。

问题在于我们不一定知道算法会得出什么结果!如果我给它前面图像中显示的一堆物体,它会将物品分成圆形的,大的与小的,红色的与蓝色的吗,我不知道。这将取决于我为物品之间的相似性给出的度量标准。但有时您会发现令人惊讶的聚类,并且会出现您没有预料到的结果。

所以这确实是无监督学习的重点:如果你不知道你在寻找什么,它可以成为一个强大的工具,用来发现你甚至不知道存在的分类。我们称之为潜在变量。你最初甚至不知道的数据属性,可以通过无监督学习来挖掘出来。

让我们来看一个无监督学习的例子。假设我是在对人群进行聚类而不是对球和骰子进行聚类。我正在写一个约会网站,我想看看哪些类型的人倾向于聚集在一起。人们倾向于围绕一些属性进行聚类,这些属性决定了他们是否倾向于彼此喜欢和约会。现在你可能会发现出现的聚类并不符合你的先入为主的刻板印象。也许这与大学生与中年人、离婚者等无关,或者他们的宗教信仰。也许如果你看看实际从分析中出现的聚类,你会对你的用户学到一些新东西,并且真正发现有些东西比你的人群的任何现有特征更重要,以决定他们是否喜欢彼此。这就是监督学习提供有用结果的一个例子。

另一个例子可能是根据电影的属性对电影进行聚类。如果你对像 IMDb 这样的一组电影运行聚类,也许结果会让你惊讶。也许这不仅仅是关于电影的类型。也许还有其他属性,比如电影的年龄或播放时长或上映国家,更重要。你永远不知道。或者我们可以分析产品描述的文本,尝试找出对某个类别具有最重要意义的术语。同样,我们可能并不一定知道哪些术语或词语最能表明产品属于某个类别;但通过无监督学习,我们可以挖掘出这些潜在信息。

监督学习

相比之下,监督学习是一种模型可以从一组答案中学习的情况。我们给它一组训练数据,模型从中学习。然后它可以推断我们想要的特征和类别之间的关系,并将其应用于未见过的新值,并预测有关它们的信息。

回到我们之前的例子,我们试图根据这些汽车的属性来预测汽车价格。这是一个我们使用实际答案来训练模型的例子。所以我有一组已知的汽车及其实际售价。我在这组完整答案上训练模型,然后我可以创建一个能够预测我以前没有见过的新车价格的模型。这是一个监督学习的例子,你给它一组答案来学习。你已经给一组数据分配了类别或一些组织标准,然后你的算法使用这些标准来建立一个模型,从中可以预测新的数值。

评估监督学习

那么如何评估监督学习呢?监督学习的美妙之处在于我们可以使用一个称为训练/测试的技巧。这里的想法是将我希望我的模型学习的观察数据分成两组,一个训练集和一个测试集。所以当我基于我拥有的数据来训练/构建我的模型时,我只使用我称之为训练集的部分数据,然后我保留另一部分数据,我将用于测试目的。

我可以使用我的数据子集来构建我的模型作为训练数据,然后我可以评估从中得出的模型,并看看它是否能成功地预测我的测试数据的正确答案。

所以你看到我在这里做了什么?我有一组数据,我已经有了可以训练模型的答案,但我将保留一部分数据,实际上用它来测试使用训练集生成的模型!这让我有一个非常具体的方法来测试我的模型在未知数据上的表现,因为我实际上有一些数据被我留出来可以用来测试。

然后你可以定量地测量它的表现如何,使用 R 平方或其他指标,比如均方根误差。你可以用它来测试一个模型与另一个模型,看看对于给定问题哪个是最好的模型。你可以调整该模型的参数,并使用训练/测试来最大化该模型在测试数据上的准确性。这是防止过拟合的一个很好的方法。

有一些监督学习的注意事项。需要确保你的训练和测试数据集足够大,能够真正代表你的数据。你还需要确保你在训练和测试中捕捉到所有你关心的不同类别和异常值,以便对其成功进行良好的衡量和建立一个好的模型。

你必须确保你从这些数据集中随机选择,并且不是只将你的数据集分成两部分,说这里左边的是训练,右边的是测试。你想要随机抽样,因为你的数据中可能有一些你不知道的顺序模式。

现在,如果你的模型过拟合,并且只是竭尽全力接受训练数据中的异常值,那么当你将其放在未见过的测试数据中时,这将会显露出来。这是因为所有为了异常值而进行的旋转对于它以前没有见过的异常值是没有帮助的。

让我们清楚地说一下,训练/测试并不完美,有可能会得到误导性的结果。也许你的样本量太小,就像我们已经讨论过的那样,或者可能仅仅由于随机机会,你的训练数据和测试数据看起来非常相似,它们实际上确实有一组相似的异常值-你仍然可能会过拟合。正如你在下面的例子中所看到的,这确实可能发生:

K-fold 交叉验证

现在有一种解决这个问题的方法,叫做 k-fold 交叉验证,我们将在本书的后面看一个例子,但基本概念是你要多次进行训练/测试。所以你实际上不是将数据分成一个训练集和一个测试集,而是分成多个随机分配的段,k 个段。这就是 k 的含义。然后你保留其中一个段作为测试数据,然后开始在剩余的段上训练模型,并测量它们对测试数据集的表现。然后你取每个训练集模型结果的平均表现,并取它们的 R 平方平均分数。

这样,你实际上是在不同的数据片段上进行训练,将它们与相同的测试集进行比较,如果你的模型对训练数据的特定部分过拟合,那么它将被 k-fold 交叉验证中其他部分的平均值抵消。

以下是 K-fold 交叉验证的步骤:

  1. 将数据分成 K 个随机分配的段

  2. 将一个段保留为测试数据

  3. 在剩余的 K-1 个段上进行训练,并测量它们对测试集的表现

  4. 计算 K-1 个 R 平方分数的平均值

这在本书的后面会更有意义,现在我只是想让你知道这个工具实际上可以使训练/测试比它已经是更加健壮。所以让我们去实际玩一些数据,并使用训练/测试来评估它。

使用训练/测试来防止多项式回归的过拟合

让我们把训练/测试付诸实践。你可能记得回归可以被看作是一种监督式机器学习。让我们尝试一下多项式回归,我们之前介绍过,使用训练/测试来尝试找到适合给定数据集的正确次数的多项式。

就像我们之前的例子一样,我们将建立一个小的虚拟数据集,其中包括随机生成的页面速度和购买金额,我将在它们之间创建一个怪异的指数关系。

%matplotlib inline 
import numpy as np 
from pylab import * 

np.random.seed(2) 

pageSpeeds = np.random.normal(3.0, 1.0, 100) 
purchaseAmount = np.random.normal(50.0, 30.0, 100) / pageSpeeds 

scatter(pageSpeeds, purchaseAmount) 

让我们继续生成这些数据。我们将使用页面速度和购买金额的正态分布的随机数据,使用如下截图中所示的关系:

接下来,我们将拆分数据。我们将取 80%的数据,保留给训练数据。所以这些点中只有 80%会用于训练模型,然后我们将保留另外的 20%用于测试模型对未知数据的预测。

我们将使用 Python 的语法来拆分列表。前 80 个点将用于训练集,最后的 20 个点将用于测试集。你可能还记得我们在之前的 Python 基础章节中介绍过这个语法,我们将在购买金额中也做同样的事情:

trainX = pageSpeeds[:80] 
testX = pageSpeeds[80:] 

trainY = purchaseAmount[:80] 
testY = purchaseAmount[80:] 

在我们之前的章节中,我说过你不应该像这样简单地将数据集分成两部分,而是应该随机抽样进行训练和测试。但在这种情况下,它是有效的,因为我的原始数据本来就是随机生成的,所以没有任何规律可循。但在现实世界的数据中,你会希望在拆分之前对数据进行洗牌。

现在我们将看一下一个方便的方法,你可以用它来洗牌你的数据。另外,如果你使用 pandas 包,那里有一些方便的函数可以自动为你创建训练和测试数据集。但我们将在这里使用 Python 列表来做。所以让我们来可视化我们最终得到的训练数据集。我们将绘制我们的训练页面速度和购买金额的散点图。

scatter(trainX, trainY) 

这是你的输出现在应该看起来的样子:

基本上,从原始完整数据集中随机选择的 80 个点已经被绘制出来。它基本上具有相同的形状,这是一个好事。它代表了我们的数据。这很重要!

现在让我们绘制剩下的 20 个点,作为测试数据。

scatter(testX, testY) 

在这里,我们看到我们剩下的 20 个测试点也与我们原始数据的形状相同。所以我认为这也是一个代表性的测试集。当然,它比你在现实世界中想看到的要小一点。例如,如果你有 1000 个点而不是 100 个点可以选择,并且保留 200 个点而不是 20 个点,你可能会得到更好的结果。

现在我们将尝试对这些数据拟合一个 8 次多项式,我们只是随机选择了数字8,因为我知道这是一个非常高的阶数,可能会导致过拟合。

让我们继续使用np.poly1d(np.polyfit(x, y, 8)来拟合我们的 8 次多项式,其中x是仅包含训练数据的数组,y也是仅包含训练数据的数组。我们只使用了那 80 个保留用于训练的点来找到我们的模型。现在我们有了这个p4函数,可以用它来预测新的值:

x = np.array(trainX) 
y = np.array(trainY) 

p4 = np.poly1d(np.polyfit(x, y, 8)) 

现在我们将绘制这个多项式与训练数据的关系。我们可以散点绘制我们的原始训练数据,然后我们可以绘制我们的预测值:

import matplotlib.pyplot as plt 

xp = np.linspace(0, 7, 100) 
axes = plt.axes() 
axes.set_xlim([0,7]) 
axes.set_ylim([0, 200]) 
plt.scatter(x, y) 
plt.plot(xp, p4(xp), c='r') 
plt.show() 

你可以在下图中看到,它看起来非常匹配,但你知道显然它有一些过拟合:

右边的这种疯狂是什么?我非常确定,如果我们真的有真实数据,它不会像这个函数所暗示的那样疯狂高。所以这是一个很好的过拟合数据的例子。它非常适合你提供的数据,但是在图表右侧疯狂高的点之后,它会对预测新值做出糟糕的预测。所以让我们试着揭示这一点。让我们给它我们的测试数据集:

testx = np.array(testX) 
testy = np.array(testY) 

axes = plt.axes() 
axes.set_xlim([0,7]) 
axes.set_ylim([0, 200]) 
plt.scatter(testx, testy) 
plt.plot(xp, p4(xp), c='r') 
plt.show() 

实际上,如果我们将我们的测试数据绘制到同样的函数上,嗯,它看起来并不那么糟糕。

我们很幸运,我们的测试数据实际上并不在这里开始,但你可以看到这是一个合理的拟合,但远非完美。事实上,如果你实际测量 R 平方分数,它比你想象的要糟糕。我们可以使用sklearn.metrics中的r2_score()函数来测量。我们只需给它我们的原始数据和我们预测的值,它就会测量所有预测值的方差并为你平方:

from sklearn.metrics import r2_score  
r2 = r2_score(testy, p4(testx))  
print r2 

我们最终得到的 R 平方分数只有0.3。所以并不是很高!你可以看到它更适合训练数据:

from sklearn.metrics import r2_score  
r2 = r2_score(np.array(trainY), p4(np.array(trainX))) 
print r2 

R 平方值结果为0.6,这并不令人意外,因为它是在训练数据上训练的。测试数据是它的未知,它的测试,而它确实没有通过测试。30%,这是 F!

这是一个例子,我们使用训练/测试来评估监督学习算法,就像我之前说的那样,pandas 有一些方法可以使这变得更容易。我们稍后会看一下,我们还将在本书的后面看到更多关于训练/测试的例子,包括 k 折交叉验证。

活动

你可能能猜到你的作业是什么。所以我们知道 8 次多项式并不是很有用。你能做得更好吗?所以我希望你回到我们的例子中,使用不同的多项式阶数来拟合。将 8 更改为不同的值,看看你能否找出使用训练/测试作为度量标准的最佳多项式阶数。你的测试数据在哪里得到最好的 R 平方分数?哪个阶数更适合?去尝试一下。这应该是一个相当简单的练习,对你来说也是一个非常有启发性的练习。

所以这就是训练/测试的实际应用,这是一个非常重要的技术,你将一遍又一遍地使用它,以确保你的结果与你拥有的模型非常匹配,并且你的结果对未知值有很好的预测能力。这是在进行建模时防止过拟合的好方法。

贝叶斯方法-概念

你是否曾经想过你的电子邮件中的垃圾邮件分类器是如何工作的?它是如何知道一封电子邮件可能是垃圾邮件还是不是?嗯,一个流行的技术是一种称为朴素贝叶斯的技术,这是贝叶斯方法的一个例子。让我们更多地了解它是如何工作的。让我们讨论贝叶斯方法。

我们在本书的早些时候讨论了贝叶斯定理,讨论了像药物测试这样的事情在结果上可能非常误导。但你实际上可以将相同的贝叶斯定理应用到更大的问题,比如垃圾邮件分类器。所以让我们深入了解一下它可能是如何工作的,这就是所谓的贝叶斯方法。

所以对贝叶斯定理的一个复习-记住,给定 B 的 A 的概率等于 A 的整体概率乘以给定 A 的 B 的概率除以 B 的整体概率:

我们如何在机器学习中使用它?我实际上可以为此构建一个垃圾邮件分类器:一个可以分析一组已知的垃圾邮件和一组已知的非垃圾邮件,并训练一个模型来预测新邮件是否为垃圾邮件的算法。这是实际世界中实际使用的垃圾邮件分类器的真正技术。

作为一个例子,让我们来计算一下包含单词“free”的电子邮件被认为是垃圾邮件的概率。如果有人向你承诺免费的东西,那很可能是垃圾邮件!所以让我们来计算一下。在电子邮件中包含单词“free”时,电子邮件被认为是垃圾邮件的概率等于它是垃圾邮件的总体概率乘以包含单词“free”的概率,假设它是垃圾邮件,除以总体概率是免费的概率:

分子可以被认为是消息是“垃圾邮件”并包含单词“免费”的概率。但这与我们要寻找的有点不同,因为这是完整数据集中的几率,而不仅仅是包含单词“免费”的几率。分母只是包含单词“免费”的总体概率。有时,这可能不会立即从您拥有的数据中获得。如果没有,如果需要推导出来,可以将其扩展为以下表达式:

这给出了包含单词“free”的电子邮件中是垃圾邮件的百分比,这在您试图确定它是否是垃圾邮件时是一个有用的信息。

但是英语中的所有其他单词呢?所以我们的垃圾邮件分类器应该知道的不仅仅是单词“free”。理想情况下,它应该自动选择消息中的每个单词,并弄清楚每个单词对特定电子邮件被认为是垃圾邮件的可能性有多大的贡献。所以我们可以在训练时对我们遇到的每个单词进行训练,丢弃像“a”、“the”和“and”这样的东西以及无意义的单词。然后当我们浏览新电子邮件中的所有单词时,我们可以将每个单词的垃圾邮件概率相乘在一起,然后得到该电子邮件是垃圾邮件的总体概率。

现在它被称为朴素贝叶斯是有原因的。它是朴素的,因为我们假设单词之间没有关系。我们只是独立地查看消息中的每个单词,并基本上结合每个单词对其是否是垃圾邮件的概率。我们不考虑单词之间的关系。因此,更好的垃圾邮件分类器会这样做,但显然这要困难得多。

听起来好像是很多工作。但总体想法并不难,而且 Python 中的 scikit-learn 使得实际上很容易做到。它提供了一个名为 CountVectorizer 的功能,可以非常简单地将电子邮件拆分为其所有组成单词,并逐个处理这些单词。然后它有一个 MultinomialNB 函数,其中 NB 代表朴素贝叶斯,它将为我们完成所有朴素贝叶斯的繁重工作。

使用朴素贝叶斯实现垃圾邮件分类器

让我们使用朴素贝叶斯编写一个垃圾邮件分类器。你会惊讶地发现这是多么容易。实际上,大部分工作最终都是读取我们将要训练的所有输入数据,并实际解析这些数据。实际的垃圾邮件分类部分,机器学习部分,本身只是几行代码。所以通常情况下是这样的:当你在做数据科学时,读取和整理数据通常是大部分工作,所以要习惯这个想法!

import os 
import io 
import numpy 
from pandas import DataFrame 
from sklearn.feature_extraction.text import CountVectorizer 
from sklearn.naive_bayes import MultinomialNB 

def readFiles(path): 
    for root, dirnames, filenames in os.walk(path): 
        for filename in filenames: 
            path = os.path.join(root, filename) 

            inBody = False 
            lines = [] 
            f = io.open(path, 'r', encoding='latin1') 
            for line in f: 
                if inBody: 
                    lines.append(line) 
                elif line == '\n': 
                    inBody = True 
            f.close() 
            message = '\n'.join(lines) 
            yield path, message 

def dataFrameFromDirectory(path, classification): 
    rows = [] 
    index = [] 
    for filename, message in readFiles(path): 
        rows.append({'message': message, 'class': classification}) 
        index.append(filename) 

    return DataFrame(rows, index=index) 

data = DataFrame({'message': [], 'class': []}) 

data = data.append(dataFrameFromDirectory(
                   'e:/sundog-consult/Udemy/DataScience/emails/spam',
                   'spam')) 
data = data.append(dataFrameFromDirectory(
                   'e:/sundog-consult/Udemy/DataScience/emails/ham',
                   'ham')) 

所以我们需要做的第一件事是以某种方式读取所有这些电子邮件,我们将再次使用 pandas 使这变得更容易一些。再次,pandas 是处理表格数据的有用工具。我们在这里的示例中导入了我们将在其中使用的所有不同包,包括 os 库、io 库、numpy、pandas,以及 scikit-learn 中的 CountVectorizer 和 MultinomialNB。

现在让我们详细地看一下这段代码。我们现在可以跳过readFiles()dataFrameFromDirectory()函数的定义,然后继续到我们的代码实际上要做的第一件事,那就是创建一个 pandas DataFrame 对象。

我们将从一个最初包含消息的空列表和一个空类列表的字典中构建这个 DataFrame。这个语法是在说:“我想要一个 DataFrame,它有两列:一个包含消息,即每封电子邮件的实际文本;另一个包含每封电子邮件的类别,也就是它是垃圾邮件还是正常邮件”。所以它是在说我想要创建一个电子邮件的小数据库,这个数据库有两列:电子邮件的实际文本和它是否是垃圾邮件。

现在我们需要向数据库中添加一些内容,也就是说,向那个 DataFrame 中添加内容,使用 Python 语法。所以我们调用了append()dataFrameFromDirectory()两个方法,实际上将来自我的spam文件夹的所有垃圾邮件和来自ham文件夹的所有正常邮件都放入了 DataFrame 中。

如果你在这里跟着做,请确保修改传递给dataFrameFromDirectory()函数的路径,以匹配你在系统中安装书籍材料的位置!再次强调,如果你使用的是 Mac 或 Linux,请注意反斜杠和正斜杠等等。在这种情况下,这并不重要,但如果你不是在 Windows 上,你就不会有一个驱动器号。所以请确保这些路径实际上指向你的spamham文件夹,以便进行本示例。

接下来,dataFrameFromDirectory()是我写的一个函数,基本上它说我有一个目录的路径,并且我知道它给定了分类,垃圾邮件或正常邮件,然后它使用了我也写的readFiles()函数,它会遍历目录中的每一个文件。所以readFiles()使用os.walk()函数来查找目录中的所有文件。然后它会为该目录中的每个单独的文件构建完整的路径名,然后读取它。在读取时,它实际上会跳过每封电子邮件的标题,直接进入文本,它是通过查找第一个空行来实现的。

它知道第一个空行之后的所有内容实际上是消息正文,而在第一个空行之前的所有内容只是一堆我实际上不想让我的垃圾邮件分类器进行训练的头部信息。所以它会把每个文件的完整路径和消息正文都返回给我。这就是我们读取所有数据的方式,也是代码的大部分内容!

所以,最终我得到的是一个 DataFrame 对象,基本上是一个有两列的数据库,包含了消息正文,以及是否是垃圾邮件。我们可以继续运行,并且可以使用 DataFrame 的head命令来预览一下它的样子:

data.head() 

我们 DataFrame 中的前几个条目看起来是这样的:对于给定文件中的每个电子邮件的路径,我们有一个分类和消息正文:

好了,现在到了有趣的部分,我们将使用 scikit-learn 中的MultinomialNB()函数来对我们的数据执行朴素贝叶斯。

vectorizer = CountVectorizer() 
counts = vectorizer.fit_transform(data['message'].values) 

classifier = MultinomialNB() 
targets = data['class'].values 
classifier.fit(counts, targets) 

现在你的输出应该是这样的:

一旦我们构建了MultinomialNB分类器,它需要两个输入。它需要我们正在训练的实际数据(counts),以及每个数据的目标(targets)。所以counts基本上是每封电子邮件中所有单词的列表,以及该单词出现的次数。

所以CountVectorizer()的作用是:它从 DataFrame 中取出message列并获取其中的所有值。我将调用vectorizer.fit_transform,它基本上是将我数据中出现的所有单词进行标记或转换为数字,为其赋予数值。然后它会计算每个单词出现的次数。

这是一种更紧凑的方式来表示每个单词在电子邮件中出现的次数。我不是保留单词本身,而是将这些单词表示为稀疏矩阵中的不同值,这基本上是说我将每个单词视为一个数字,作为一个数值索引,进入一个数组。它所做的是,用简单的英语说,将每个消息拆分成其中包含的单词列表,并计算每个单词出现的次数。所以我们称之为counts。它基本上是每个单词在每个单独消息中出现的次数的信息。同时,targets是我遇到的每封电子邮件的实际分类数据。所以我可以使用我的 MultinomialNB()函数调用 classifier.fit()来实际使用朴素贝叶斯创建一个模型,该模型将根据我们提供的信息预测新的电子邮件是否是垃圾邮件。

让我们继续运行。它运行得相当快!我将在这里使用几个例子。让我们尝试一个只说“现在免费赚钱!”的消息正文,这显然是垃圾邮件,还有一个更无辜的消息,只是说“嗨鲍勃,明天打一场高尔夫怎么样?”所以我们要传递这些消息。

examples = ['Free Money now!!!', "Hi Bob, how about a game of golf tomorrow?"] 
example_counts = vectorizer.transform(examples) 
predictions = classifier.predict(example_counts) 
predictions 

我们要做的第一件事是将消息转换为我训练模型的相同格式。所以我使用了创建模型时创建的相同的向量化器,将每条消息转换为一个单词和它们的频率的列表,其中单词由数组中的位置表示。一旦我完成了这个转换,我实际上可以在我的分类器上使用 predict()函数,对已经转换成单词列表的示例数组进行预测,看看我们得到了什么:

array(['spam', 'ham'], dtype='|S4') 

当然,它有效!所以,给定这两个输入消息的数组,“现在免费赚钱!”和“嗨鲍勃”,它告诉我第一个结果是垃圾邮件,第二个结果是正常邮件,这正是我所期望的。这很酷。就是这样。

活动

我们这里有一个相当小的数据集,所以如果你愿意,你可以尝试通过一些不同的电子邮件,并查看是否会得到不同的结果。如果你真的想挑战自己,尝试将训练/测试应用到这个例子中。所以是否我的垃圾邮件分类器好不好的真正衡量标准不仅仅是它是否能直观地判断“现在免费赚钱!”是垃圾邮件。你想要定量地衡量它。

所以如果你想挑战一下,尝试将这些数据分成一个训练集和一个测试数据集。你实际上可以在网上查找如何使用 pandas 很容易地将数据分成训练集和测试集,或者你可以手动操作。无论哪种方式都可以。看看你是否能够将你的 MultinomialNB 分类器应用到一个测试数据集上,并衡量其性能。所以,如果你想要一点挑战,一点挑战,那就试试看吧。

这有多酷?我们只是用几行 Python 代码编写了自己的垃圾邮件分类器。使用 scikit-learn 和 Python 非常容易。这就是朴素贝叶斯的实际应用,现在你可以去分类一些垃圾邮件或正常邮件了。非常酷。接下来让我们谈谈聚类。

K-Means 聚类

接下来,我们将讨论 K 均值聚类,这是一种无监督学习技术,你有一堆东西想要分成各种不同的簇。也许是电影类型或人口统计学,谁知道呢?但这实际上是一个相当简单的想法,所以让我们看看它是如何工作的。

K-means 聚类是机器学习中非常常见的技术,您只需尝试获取一堆数据,并根据数据本身的属性找到有趣的集群。听起来很花哨,但实际上非常简单。在 k-means 聚类中,我们所做的就是尝试将我们的数据分成 K 组-这就是 K 的含义,它是您尝试将数据分成多少不同组的数量-它通过找到 K 个质心来实现这一点。

所以,基本上,给定数据点属于哪个组是由散点图中它最接近的质心点来定义的。您可以在以下图像中可视化这一点:

这是一个 K 为三的 k-means 聚类的示例,方块代表散点图中的数据点。圆圈代表 k-means 聚类算法得出的质心,并且每个点根据它最接近的质心被分配到一个集群中。所以,这就是全部内容,真的。这是无监督学习的一个例子。这不是一个情况,我们有一堆数据,我们已经知道给定一组训练数据的正确集群;相反,你只是给出了数据本身,它试图仅基于数据的属性自然地收敛到这些集群。这也是一个例子,您正在尝试找到甚至您自己都不知道存在的集群或分类。与大多数无监督学习技术一样,重点是找到潜在价值,直到算法向您展示它们之前,您并没有真正意识到它们的存在。

例如,百万富翁住在哪里?我不知道,也许有一些有趣的地理集群,富人倾向于居住在那里,k-means 聚类可以帮助您找出答案。也许我真的不知道今天的音乐流派是否有意义。现在成为另类是什么意思?不多,对吧?但是通过对歌曲属性进行 k-means 聚类,也许我可以找到相关的歌曲集群,并为这些集群代表的内容想出新的名称。或者我可以查看人口统计数据,也许现有的刻板印象已经不再有用。也许西班牙裔已经失去了意义,实际上有其他属性可以定义人群,例如,我可以通过聚类发现。听起来很花哨,不是吗?真的很复杂。具有 K 个集群的无监督机器学习,听起来很花哨,但与数据科学中的大多数技术一样,实际上是一个非常简单的想法。

以下是我们用简单英语的算法:

  1. 随机选择 K 个质心(k-means):我们从一组随机选择的质心开始。所以如果我们有三个 K,我们将在我们的组中寻找三个集群,并且我们将在我们的散点图中分配三个随机位置的质心。

  2. 将每个数据点分配给最接近的质心点:然后我们将每个数据点分配给它最接近的随机分配的质心点。

  3. 根据每个质心点的平均位置重新计算质心:然后重新计算我们得出的每个集群的质心。也就是说,对于我们最终得到的给定集群,我们将移动该质心以成为所有这些点的实际中心。

  4. 迭代直到点停止改变分配到质心:我们将一直重复这个过程,直到这些质心停止移动,我们达到了一些阈值值,表示我们已经收敛到了某些东西。

  5. 预测新点的集群:要预测我以前没有见过的新点的集群,我们只需通过我们的质心位置并找出它最接近的质心来预测其集群。

让我们看一个图形示例,以便更容易理解。我们将以下图像中的第一个图形称为 A,第二个称为 B,第三个称为 C,第四个称为 D。

图 A 中的灰色方块代表我们散点图中的数据点。坐标轴代表某些不同特征。也许是年龄和收入;这是我一直在使用的一个例子,但它可以是任何东西。灰色方块可能代表个体人员、个体歌曲或我想要找到它们之间关系的任何东西。

因此,我首先随机在我的散点图上选择了三个点。可以是任何地方。总得从某个地方开始,对吧?我选择的三个点(质心)在图 A 中被表示为圆圈。接下来,我要做的是对于每个质心,计算它最接近的灰色点是哪一个。通过这样做,图中蓝色阴影的点与蓝色质心相关联。绿色点最接近绿色质心,而这个单独的红点最接近我选择的那个红色随机点。

当然,你可以看到这并不真正反映实际聚类的情况。因此,我要做的是取出每个聚类中的点,并计算这些点的实际中心。例如,在绿色聚类中,所有数据的实际中心实际上要低一点。我们会将质心向下移动一点。红色聚类只有一个点,所以它的中心移动到那个单个点的位置。而蓝色点实际上离中心相当近,所以只是移动了一点。在下一次迭代中,我们得到了类似图 D 的结果。现在你可以看到我们红色聚类的范围有所增加,事物也有所移动,也就是说,它们被从绿色聚类中取走了。

如果我们再次这样做,你可能可以预测接下来会发生什么。绿色的质心会移动一点,蓝色的质心仍然会保持在原来的位置。但最终你会得到你可能期望看到的聚类。这就是 k-means 的工作原理。因此,它会不断迭代,试图找到正确的质心,直到事物开始移动并收敛于一个解决方案。

k-means 聚类的局限性

因此,k-means 聚类存在一些局限性。以下是其中一些:

  1. 选择 K:首先,我们需要选择正确的 K 值,这并不是一件简单的事情。选择 K 的主要方法是从较低的值开始,根据你想要的群组数量不断增加 K 的值,直到停止获得平方误差的大幅减少。如果你观察每个点到它们的质心的距离,你可以将其视为一个误差度量。当你停止减少这个误差度量时,你就知道你可能有太多的聚类。因此,在那一点上,通过添加额外的聚类,你实际上并没有获得更多的信息。

  2. 避免局部最小值:此外,还存在局部最小值的问题。你可能会因为初始质心的选择而非常不幸,它们最终可能只会收敛于局部现象,而不是更全局的聚类,因此通常情况下,你需要多次运行这个过程,然后将结果进行平均。我们称之为集成学习。我们稍后会更详细地讨论这个问题,但多次运行 k-means 并使用不同的随机初始值是一个很好的主意,看看你最终是否得到了相同的结果。

  3. 标记聚类:最后,k-means 聚类的主要问题是得到的聚类没有标签。它只会告诉你这组数据点在某种程度上是相关的,但你无法给它们贴上名字。它无法告诉你该聚类的实际含义。假设我有一堆电影,我正在观看,k-means 聚类告诉我一堆科幻电影在这里,但它不会为我称它们为“科幻”电影。我需要深入数据并弄清楚,这些东西真正有什么共同点?我如何用英语描述它们?这是困难的部分,k-means 不会帮助你。所以再次,scikit-learn 使这变得非常容易。

现在,让我们举个例子,让 k-means 聚类付诸实践。

基于收入和年龄对人进行聚类

让我们看看使用 scikit-learn 和 Python 进行 k-means 聚类有多容易。

我们要做的第一件事是创建一些我们想要尝试进行聚类的随机数据。为了简化,我们实际上会在我们的假测试数据中构建一些聚类。所以假设这些数据之间存在一些真实的基本关系,并且其中存在一些真实的自然聚类。

为了做到这一点,我们可以使用 Python 中的createClusteredData()函数:

from numpy import random, array 

#Create fake income/age clusters for N people in k clusters 
def createClusteredData(N, k): 
    random.seed(10) 
    pointsPerCluster = float(N)/k 
    X = [] 
    for i in range (k): 
        incomeCentroid = random.uniform(20000.0, 200000.0) 
        ageCentroid = random.uniform(20.0, 70.0) 
        for j in range(int(pointsPerCluster)): 
            X.append([random.normal(incomeCentroid, 10000.0), 
            random.normal(ageCentroid, 2.0)]) 
    X = array(X) 
    return X 

该函数从一致的随机种子开始,因此每次都会得到相同的结果。我们想要在 k 个聚类中创建 N 个人的聚类。所以我们将Nk传递给createClusteredData()

我们的代码首先计算出每个聚类的点数,并将其存储在pointsPerCluster中。然后,它构建了一个起始为空的列表X。对于每个聚类,我们将创建一些收入的随机中心(incomeCentroid),介于 20,000 到 200,000 美元之间,以及一些年龄的随机中心(ageCentroid),介于 20 到 70 岁之间。

我们在这里所做的是创建一个假的散点图,显示了N个人和k个聚类的收入与年龄。所以对于我们创建的每个随机中心,我将创建一组正态分布的随机数据,收入的标准差为 10,000,年龄的标准差为 2。这将给我们一堆年龄收入数据,它们被聚类到一些我们可以随机选择的预先存在的聚类中。好的,让我们继续运行。

现在,要实际进行 k-means,你会看到它有多容易。

from sklearn.cluster import KMeans 
import matplotlib.pyplot as plt 
from sklearn.preprocessing import scale 
from numpy import random, float 

data = createClusteredData(100, 5) 

model = KMeans(n_clusters=5) 

# Note I'm scaling the data to normalize it! Important for good results. 
model = model.fit(scale(data)) 

# We can look at the clusters each data point was assigned to 
print model.labels_  

# And we'll visualize it: 
plt.figure(figsize=(8, 6)) 
plt.scatter(data[:,0], data[:,1], c=model.labels_.astype(float)) 
plt.show() 

你所需要做的就是从 scikit-learn 的 cluster 包中导入KMeans。我们还要导入matplotlib,这样我们就可以可视化数据,还要导入scale,这样我们就可以看看它是如何工作的。

所以我们使用我们的createClusteredData()函数来说有 100 个随机人分布在 5 个聚类中。所以对于我创建的数据,有 5 个自然的聚类。然后我们创建一个模型,一个 k 为 5 的 KMeans 模型,所以我们选择 5 个聚类,因为我们知道这是正确的答案。但是在无监督学习中,你不一定知道k的真实值。你需要自己迭代和收敛到它。然后我们只需使用我们的 KMeans 模型使用我们的数据来调用model.fit

现在我之前提到的规模,那是对数据进行归一化。k-means 的一个重要问题是,如果你的数据都归一化,它的效果会更好。这意味着一切都在相同的尺度上。所以我在这里遇到的问题是,我的年龄范围是 20 到 70 岁,但我的收入范围高达 20 万美元。所以这些值并不真正可比。收入远远大于年龄值。Scale将把所有数据一起缩放到一个一致的尺度,这样我就可以将这些数据进行比较,这将有助于你的 k-means 结果。

所以,一旦我们在我们的模型上调用了fit,我们实际上可以查看我们得到的结果标签。然后我们可以使用一点matplotlib的魔法来可视化它。你可以在代码中看到我们有一个小技巧,我们将颜色分配给了我们最终得到的标签,转换为一些浮点数。这只是一个小技巧,你可以用来为给定的值分配任意颜色。所以让我们看看我们最终得到了什么:

这并没有花太长时间。你可以看到结果基本上是我分配给每个东西的聚类。我们知道我们的假数据已经被预先聚类,所以它似乎很容易地识别了第一和第二个聚类。然而,在那之后它有点困惑了,因为我们中间的聚类实际上有点混在一起。它们并不是真的那么明显,所以这对 k 均值来说是一个挑战。但无论如何,它确实对聚类提出了一些合理的猜测。这可能是四个聚类更自然地适应数据的一个例子。

活动

所以我想让你做的一个活动是尝试不同的 k 值,看看你最终得到了什么。仅仅凭眼前的图表,看起来四个可能效果很好。真的吗?如果我把 k 增加得太大会发生什么?我的结果会怎样?它会尝试将事物分成什么,这甚至有意义吗?所以,玩一下,尝试不同的k值。所以在n_clusters()函数中,将 5 改为其他值。再次运行一遍,看看你最终得到了什么。

这就是 k 均值聚类的全部内容。就是这么简单。你可以使用 scikit-learn 的KMeanscluster中的东西。唯一真正需要注意的是:确保你对数据进行缩放,归一化。你希望确保你用 k 均值进行处理的东西是可比较的,scale()函数会为你做到这一点。所以这些是 k 均值聚类的主要内容。非常简单的概念,使用 scikit-learn 更简单。

就是这样。这就是 k 均值聚类。所以如果你有一堆未分类的数据,而且你事先并没有正确的答案,这是一个很好的方法来自然地找到数据的有趣分组,也许这可以让你对数据有一些见解。这是一个很好的工具。我以前在现实世界中使用过它,而且真的并不难使用,所以记住它。

测量熵

很快我们就要进入机器学习中更酷的部分之一,至少我认为是,叫做决策树。但在我们谈论那之前,理解数据科学中熵的概念是必要的。

所以熵,就像在物理学和热力学中一样,是数据集的混乱程度的度量,数据集的相同或不同程度。所以想象一下,我们有一个不同分类的数据集,例如动物。比如说我有一堆我已经按物种分类的动物。现在,如果我的数据集中的所有动物都是鬣蜥,我就有很低的熵,因为它们都是一样的。但如果我的数据集中的每个动物都是不同的动物,我有鬣蜥和猪和树懒和谁知道还有什么,那么我就会有更高的熵,因为我的数据集中有更多的混乱。事物之间的不同性大于相同性。

熵只是一种量化我的数据中相同或不同的方式。所以,熵为 0 意味着数据中的所有类别都是相同的,而如果一切都不同,我就会有很高的熵,而介于两者之间的情况将是介于两者之间的数字。熵只是描述数据集中的事物是相同还是不同的方式。

现在从数学上讲,它比那复杂一点,所以当我实际计算熵的数值时,它是使用以下表达式计算的:

所以对于我数据中的每个不同类,我将有一个这样的 p 项,p[1],p[2],等等,直到 p[n],对于我可能有的 n 个不同类。p 只是表示数据中是那个类的比例。如果你实际上绘制出每个项的样子- pi* ln * pi,它看起来会有点像下面的图表:

你为每个单独的类加起来。例如,如果数据的比例,也就是说,对于给定的类是 0,那么对总熵的贡献就是 0。如果一切都是这个类,那么对总熵的贡献也是 0,因为在任何一种情况下,如果没有任何东西是这个类或者一切都是这个类,那实际上并没有对总熵做出任何贡献。

中间的事物会为类的熵做出贡献,那里有一些这种分类和其他东西的混合。当你把所有这些项加在一起时,你就得到了整个数据集的总熵。所以从数学上讲,就是这样运作的,但是,这个概念非常简单。它只是衡量你的数据集有多无序,你的数据中的事物有多相同或不同。

决策树-概念

信不信由你,给定一组训练数据,你实际上可以让 Python 为你生成一个流程图来做出决定。所以如果你有一些你想要在某些分类上进行预测的东西,你可以使用决策树来实际查看流程图中每个级别上可以决定的多个属性。你可以打印出一个实际的流程图供你使用,以便基于实际的机器学习做出决定。这有多酷?让我们看看它是如何运作的。

我个人认为决策树是机器学习中最有趣的应用之一。决策树基本上给出了如何做出某些决定的流程图。你有一些依赖变量,比如今天是否应该根据天气出去玩。当你有一个依赖于多个属性或多个变量的决定时,决策树可能是一个不错的选择。

天气的许多不同方面可能会影响我是否应该出去玩的决定。这可能与湿度、温度、是否晴天等有关。决策树可以查看天气的所有这些不同属性,或者其他任何东西,并决定什么是阈值?在每个属性上我需要做出什么决定,然后才能决定我是否应该出去玩?这就是决策树的全部内容。所以它是一种监督学习。

在这个例子中,它的工作方式如下。我会有一些关于历史天气的数据集,以及关于人们在特定一天是否出去玩的数据。我会向模型提供这些数据,比如每天是否晴天,湿度是多少,是否刮风,以及那天是否适合出去玩。在给定这些训练数据的情况下,决策树算法可以得出一棵树,给我们一个流程图,我们可以打印出来。它看起来就像下面的流程图。你可以浏览并根据当前属性来判断是否适合出去玩。你可以用它来预测新一组值的决定:

这有多酷?我们有一个算法,可以根据观测数据自动生成流程图。更酷的是,一旦你学会了它的工作原理,它的一切都是如此简单。

决策树示例

假设我想建立一个系统,根据其中的信息自动筛选简历。技术公司面临的一个大问题是,我们为我们的职位收到了大量的简历。我们必须决定我们实际上要邀请谁来面试,因为飞某人出来并且实际上花时间进行面试可能是很昂贵的。那么,如果有一种方法可以将实际上被雇佣的人的历史数据与他们简历中的信息相匹配,那会怎么样呢?

我们可以构建一个决策树,让我们可以浏览个人简历,并说,“好的,这个人实际上有很高的被雇佣可能性,或者没有”。我们可以根据历史数据训练一个决策树,并为未来的候选人走过这个流程。那不是一件很美好的事情吗?

所以让我们制作一些完全捏造的雇佣数据,我们将在这个例子中使用。

在前面的表中,我们有一些只用数字标识的候选人。我将挑选一些我认为可能有趣或有助于预测他们是否是一个好的雇佣者的属性。他们有多少年的工作经验?他们目前有工作吗?他们之前有多少雇主?他们的教育水平是多少?他们有什么学位?他们是否上过我们分类为顶尖学校?他们在大学期间是否做过实习?我们可以看一下这些历史数据,这里的因变量是“被雇佣”。这个人实际上是否根据这些信息得到了工作机会?

现在,显然这个模型中没有的很多信息可能非常重要,但是我们从这些数据中训练出来的决策树实际上可能有助于在初步筛选一些候选人时使用。我们最终得到的可能是一个看起来像下面这样的树:

  • 所以事实证明,在我完全捏造的数据中,任何在大学实习的人最终都得到了工作机会。所以我的第一个决策点是“这个人是否做过实习?”如果是,那就让他们进来吧。根据我的经验,实习实际上是一个相当好的人才预测指标。如果他们有主动性去实习,并且在实习中真正学到了东西,那是一个好迹象。

  • 他们目前有工作吗?嗯,如果他们目前有工作,那么在我的非常小的虚拟数据集中,结果表明他们值得雇佣,只是因为其他人也认为他们值得雇佣。显然,在现实世界中,这将是一个更微妙的决定。

  • 如果他们目前没有工作,他们之前的雇主少于一个吗?如果是,这个人以前从未工作过,他们也没有做过实习。可能不是一个好的雇佣决定。不要雇佣这个人。

  • 但是如果他们之前有过雇主,他们是否至少上过一所顶尖学校?如果没有,那就有点靠不住。如果是,那么是的,我们应该根据我们训练的数据来雇佣这个人。

浏览决策树

这就是你如何浏览决策树的结果。就像浏览流程图一样,算法可以为你产生这样的结果,这真是太棒了。算法本身实际上非常简单。让我解释一下算法是如何工作的。

在决策树流程图的每一步,我们找到可以将我们的数据分区的属性,以最小化下一步数据的熵。所以我们得到了一组分类:在这种情况下是雇佣或不雇佣,我们希望选择在下一步最小化熵的属性决策。

在每一步,我们希望所有剩下的选择都导致尽可能多的不录用或尽可能多的录用决定。我们希望使数据变得越来越统一,因此当我们沿着流程图向下工作时,最终我们最终得到一组候选人,要么全部录用,要么全部不录用,这样我们就可以在决策树上对是/否做出分类。因此,我们只需沿着树走,通过选择正确的属性来最小化每一步的熵,直到我们用完为止。

这种算法有一个花哨的名字。它被称为ID3迭代二分器 3)。这是一个贪婪算法。因此,当它沿着树走时,它只选择在那一点上最小化熵的属性。现在,这可能实际上不会导致最小化你必须做出的选择的最佳树,但它将会得到一个树,鉴于你给它的数据。

随机森林技术

现在决策树的一个问题是它们很容易过拟合,所以你可能会得到一个对训练数据非常有效的决策树,但对于那些它以前没有见过的新人的正确分类预测可能并不那么好。决策树的核心是为你提供的训练数据做出正确的决策,但也许你并没有真正考虑到正确的属性,也许你没有给它足够代表性的人员样本来学习。这可能会导致真正的问题。

为了解决这个问题,我们使用一种称为随机森林的技术,其思想是我们以不同的方式对我们进行训练的数据进行采样,用于多个不同的决策树。每棵决策树从我们的训练数据集中随机选择不同的样本,并从中构建一棵树。然后每棵树都可以对正确的结果进行投票。

现在我们使用相同模型对我们的数据进行随机重采样的技术被称为自举聚合,或者称为装袋。这是一种我们称之为集成学习的形式,我们很快会更详细地介绍。但基本思想是我们有多个树,如果你愿意的话,可以称之为树的森林,每个树都使用我们要训练的数据的随机子样本。然后每棵树都可以对最终结果进行投票,这将帮助我们对给定的训练数据进行过拟合。

随机森林可以做的另一件事是在它尝试最小化熵的同时,实际上限制它可以在每个阶段选择的属性数量。我们可以在每个级别随机选择它可以选择的属性。因此,这也使我们的树与树之间更加多样化,因此我们得到了更多可以相互竞争的算法的变化。它们可以使用略有不同的方法对最终结果进行投票,以达到相同的答案。

这就是随机森林的工作原理。基本上,它是一组决策树的森林,它们从不同的样本和不同的属性集中进行选择。

因此,有了这一切,让我们去做一些决策树。当我们完成后,我们也将使用随机森林,因为 scikit-learn 使得这变得非常容易,很快你就会看到。

决策树 - 使用 Python 预测招聘决策

事实证明,制作决策树很容易;事实上,只需几行 Python 代码就可以做到这一点。所以让我们试一试。

我已经在你的书材料中包含了一个PastHires.csv文件,里面只包含了一些虚构的数据,是我根据候选人的属性编造的,这些候选人要么得到了工作机会,要么没有。

import numpy as np 
import pandas as pd 
from sklearn import tree 

input_file = "c:/spark/DataScience/PastHires.csv" 
df = pd.read_csv(input_file, header = 0) 

请立即更改我在这里使用的路径,用于我的系统(c:/spark/DataScience/PastHires.csv),改为你安装本书材料的位置。我不确定你把它放在哪里,但几乎肯定不是那里。

我们将使用pandas读取我们的 CSV 文件,并将其创建为一个 DataFrame 对象。让我们继续运行我们的代码,并可以使用 DataFrame 的head()函数打印出前几行,确保它看起来是有意义的。

df.head() 

确实,我们在输出中有一些有效的数据:

因此,对于每个候选人 ID,我们有他们的过去工作经验年限,是否就业,以前的雇主数量,他们的最高教育水平,是否就读顶级学校,是否做过实习;最后,在 Hired 列中,答案-我们知道我们是否向这个人提供了工作机会。

通常情况下,大部分工作只是在处理数据,准备数据,然后才实际运行算法,这就是我们需要在这里做的。现在 scikit-learn 要求一切都是数字,所以我们不能有 Y 和 N 和 BS 和 MS 和 PhD。我们必须将所有这些东西转换为数字,以便决策树模型能够工作。在 pandas 中使用一些简写可以使这些事情变得容易。例如:

d = {'Y': 1, 'N': 0} 
df['Hired'] = df['Hired'].map(d) 
df['Employed?'] = df['Employed?'].map(d) 
df['Top-tier school'] = df['Top-tier school'].map(d) 
df['Interned'] = df['Interned'].map(d) 
d = {'BS': 0, 'MS': 1, 'PhD': 2} 
df['Level of Education'] = df['Level of Education'].map(d) 
df.head() 

基本上,我们在 Python 中创建一个字典,将字母 Y 映射为数字 1,将字母 N 映射为值 0。因此,我们想将所有的 Y 转换为 1,将所有的 N 转换为 0。因此 1 表示是,0 表示否。我们只需从 DataFrame 中取出 Hired 列,并在其上调用map(),使用一个字典。这将遍历整个 DataFrame 中的 Hired 列,并使用该字典查找来转换该列中的所有条目。它返回一个新的 DataFrame 列,我将其放回到 Hired 列中。这将用 1 和 0 映射的列替换 Hired 列。

我们对就业,顶级学校和实习做同样的处理,所以所有这些都使用是/否字典进行映射。因此,Y 和 N 变成了 1 和 0。对于教育水平,我们也使用同样的技巧,创建一个将 BS 分配为 0,MS 分配为 1,PhD 分配为 2 的字典,并使用它来重新映射这些学位名称为实际的数值。所以如果我继续运行并再次使用head(),你会看到它起作用了:

我的所有是 1,我的所有否是 0,我的教育水平现在由具有实际含义的数值表示。

接下来,我们需要准备一切以实际进入我们的决策树分类器,这并不难。为此,我们需要分离我们的特征信息,即我们试图预测的属性,以及我们的目标列,其中包含我们试图预测的东西。为了提取特征名称列的列表,我们只需创建一个列的列表,直到第 6 列。我们继续打印出来。

features = list(df.columns[:6]) 
features 

我们得到以下输出:

上面是包含我们特征信息的列名:工作经验年限,是否就业,以前的雇主,教育水平,顶级学校和实习。这些是我们想要预测雇佣的候选人的属性。

接下来,我们构建我们的y向量,它被分配为我们要预测的内容,也就是我们的 Hired 列:

y = df["Hired"] 
X = df[features] 
clf = tree.DecisionTreeClassifier() 
clf = clf.fit(X,y) 

这段代码提取整个 Hired 列并将其命名为y。然后它将所有特征数据的列放入一个名为X的对象中。这是所有数据和所有特征列的集合,Xy是我们的决策树分类器需要的两个东西。

要实际创建分类器本身,只需两行代码:我们调用tree.DecisionTreeClassifier()来创建我们的分类器,然后将其拟合到我们的特征数据(X)和答案(y) - 是否雇佣了人。所以,让我们继续运行。

显示图形数据有点棘手,我不想在这里分散我们太多的注意力,所以请只考虑以下样板代码。你不需要深入了解 Graph viz 在这里的工作方式 - 以及 dot 文件和所有这些东西:这对我们的旅程现在不重要。你需要实际显示决策树最终结果的代码只是:

from IPython.display import Image   
from sklearn.externals.six import StringIO   
import pydot  

dot_data = StringIO()   
tree.export_graphviz(clf, out_file=dot_data,   
                         feature_names=features)   
graph = pydot.graph_from_dot_data(dot_data.getvalue())   
Image(graph.create_png()) 

所以让我们继续运行这个。

你的输出现在应该是这样的:

我们有了!这有多酷!我们这里有一个实际的流程图。

现在,让我告诉你如何阅读它。在每个阶段,我们都有一个决定。记住,我们的大多数数据是是或否,将是 0 或 1。所以,第一个决定点是:就业?小于 0.5 吗?这意味着如果我们有一个就业价值为 0,那就是不,我们将向左走。如果就业是 1,也就是是,我们将向右走。

那么,他们以前受雇吗?如果没有,向左走,如果是,向右走。结果是,在我的样本数据中,每个目前受雇的人实际上都得到了一个工作机会,所以我可以非常快速地说,如果你目前受雇,是的,你值得被带进来,我们将继续到第二个层级。

那么,你如何解释这个呢?基尼分数基本上是在每一步使用的熵的度量。记住,当我们进行算法时,它试图最小化熵的量。样本是之前的决定没有分割的剩余样本数量。

所以说这个人以前是受雇的。阅读右叶节点的方法是值列,告诉你在这一点上,我们有 0 个候选人是不被雇佣的,有 5 个是被雇佣的。所以,解释第一个决定点的方法是,如果就业?是 1,我会向右走,这意味着他们目前是受雇的,这让我进入了一个每个人都得到了工作机会的世界。所以,这意味着我应该雇佣这个人。

现在假设这个人目前没有工作。我接下来要看的是,他们有实习吗。如果是,那么在我们的训练数据中,每个人都得到了一个工作机会。所以,在那一点上,我们可以说我们的熵现在是 0(gini=0.0000),因为每个人都一样,在那一点上他们都得到了一个工作机会。然而,你知道,如果我们继续下去(在这个人没有做实习的情况下),我们将到达一个熵为 0.32 的点。它越来越低,这是一件好事。

接下来我们要看的是他们有多少经验,他们有不到一年的经验吗?如果情况是他们确实有一些经验,并且他们已经走到了这一步,他们是一个相当好的不被雇佣的决定。我们最终到达了熵为零的点,但是,在我们的训练集中剩下的三个样本都是不被雇佣的。我们有 3 个不被雇佣和 0 个被雇佣。但是,如果他们经验较少,那么他们可能刚刚从大学毕业,他们仍然值得一看。

我们要看的最后一件事是他们是否上了一所顶尖学校,如果是的话,他们最终会成为一个好的预测被雇佣。如果不是,他们最终会成为一个不被雇佣。我们最终有一个候选人属于这个类别,是一个不被雇佣,还有 0 个被雇佣。而在候选人确实上了一所顶尖学校的情况下,我们有 0 个不被雇佣和 1 个被雇佣。

所以,你可以看到,我们一直走下去,直到我们达到熵为 0,如果可能的话,对于每种情况。

集成学习 - 使用随机森林

现在,假设我们想使用随机森林,你知道,我们担心我们可能过度拟合我们的训练数据。实际上,很容易创建一个多个决策树的随机森林分类器。

所以,为了做到这一点,我们可以使用之前创建的相同数据。你只需要你的*X**y*向量,也就是特征集和你试图预测的列:

from sklearn.ensemble import RandomForestClassifier 

clf = RandomForestClassifier(n_estimators=10) 
clf = clf.fit(X, y) 

#Predict employment of an employed 10-year veteran 
print clf.predict([[10, 1, 4, 0, 0, 0]]) 
#...and an unemployed 10-year veteran 
print clf.predict([[10, 0, 4, 0, 0, 0]]) 

我们制作了一个随机森林分类器,也可以从 scikit-learn 中获得,并传递给它我们想要在我们的森林中的树的数量。所以,在上面的代码中,我们的随机森林中有十棵树。然后我们将其适配到模型上。

你不必手动遍历树,而且当你处理随机森林时,你也不能真正这样做。所以,我们在模型上使用predict()函数,也就是我们制作的分类器上。我们传入一个给定候选人的所有不同特征的列表,我们想要预测他们的就业情况。

如果你记得,这些映射到这些列:工作经验,就业情况,以前的雇主,教育水平,顶级学校和实习;被解释为数值。我们预测一个有工作的 10 年经验的老手的就业情况。我们还预测一个失业的 10 年经验的老手的就业情况。果然,我们得到了一个结果:

在这种情况下,我们最终都做出了雇佣决定。但有趣的是,这其中有一个随机因素。你实际上并不会每次都得到相同的结果!往往情况下,失业者并不会得到工作机会,如果你继续运行这个过程,你会发现通常情况下都是这样。但是,bagging 的随机性,每棵树的自助聚合的随机性,意味着你不会每次都得到相同的结果。所以,也许 10 棵树还不够。总之,这是一个很好的教训!

活动

作为一个活动,如果你想回去玩一下,可以玩弄我的输入数据。随意编辑我们一直在探索的代码,并创建一个颠倒世界的替代宇宙;例如,我给工作机会的每个人现在都不再得到工作机会,反之亦然。看看这对你的决策树有什么影响。只是随意玩弄一下,看看你能做什么,并尝试解释结果。

所以,那就是决策树和随机森林,我认为这是机器学习中更有趣的部分之一。我总是觉得能够从空中生成一个流程图非常酷。所以,希望你会觉得这很有用。

集成学习

当我们谈论随机森林时,那是集成学习的一个例子,我们实际上将多个模型组合在一起,以得到比任何单个模型更好的结果。所以,让我们更深入地了解一下。让我们更多地谈谈集成学习。

所以,还记得随机森林吗?我们有一堆使用输入数据的不同子样本和不同属性集的决策树,当你试图在最后对某些东西进行分类时,它们都对最终结果进行投票。这就是集成学习的一个例子。另一个例子:当我们谈论 k 均值聚类时,我们有一个想法,也许使用不同的 k 均值模型和不同的初始随机质心,让它们都对最终结果进行投票。这也是集成学习的一个例子。

基本上,这个想法是你有不止一个模型,它们可能是相同类型的模型,也可能是不同类型的模型,但你在你的训练数据集上运行它们所有,并且它们都对你试图预测的最终结果进行投票。往往情况下,你会发现这个不同模型的集合产生比任何单个模型本身更好的结果。

几年前的一个很好的例子是 Netflix 奖。Netflix 举办了一项比赛,他们提供了我认为是一百万美元的奖金,给任何研究人员,如果他们能够超越现有的电影推荐算法。获胜的方法是集成方法,他们实际上同时运行多个推荐算法,并让它们都对最终结果进行投票。因此,集成学习可以是一种非常强大而简单的工具,用于提高机器学习中最终结果的质量。现在让我们尝试探索各种类型的集成学习:

  • 自举聚合或装袋:现在,随机森林使用一种称为装袋的技术,简称自举聚合。这意味着我们从训练数据中随机抽取子样本,并将它们馈送到同一模型的不同版本中,让它们都对最终结果进行投票。如果你还记得,随机森林采用许多不同的决策树,这些决策树使用训练数据的不同随机样本进行训练,然后它们最终汇聚在一起对最终结果进行投票。这就是装袋。

  • 提升:提升是一种替代模型,其思想是你从一个模型开始,但每个后续模型都增强了前一个模型误分类的属性。因此,你在一个模型上进行训练/测试,找出它基本上搞错了什么属性,然后在后续模型中增强这些属性 - 希望后续模型会更加关注它们,并且把它们搞对。这就是提升的一般思想。你运行一个模型,找出它的弱点,随着你的进行,增强对这些弱点的关注,并且不断构建更多的模型,这些模型根据前一个模型的弱点进行改进。

  • 模型桶:另一种技术,这就是 Netflix 奖的获奖者所做的,称为模型桶,你可能有完全不同的模型来尝试预测某些东西。也许我正在使用 k-means、决策树和回归。我可以同时在一组训练数据上运行这三个模型,并让它们都对在预测时的最终分类结果进行投票。也许这比单独使用其中任何一个模型要好。

  • 堆叠:堆叠有相同的思想。因此,你在数据上运行多个模型,以某种方式将结果组合在一起。桶模型和堆叠之间的微妙差异在于你选择获胜的模型。因此,你运行训练/测试,找出最适合你的数据的模型,并使用该模型。相比之下,堆叠将所有这些模型的结果组合在一起,得出最终结果。

现在,有一个关于集成学习的研究领域,试图找到最佳的集成学习方法,如果你想显得聪明,通常这涉及大量使用贝叶斯这个词。因此,有一些非常先进的集成学习方法,但它们都有弱点,我认为这又是一个教训,即我们应该始终使用对我们有效的最简单的技术。

现在,这些都是非常复杂的技术,在本书的范围内我无法深入讨论,但归根结底,很难超越我们已经讨论过的简单技术。以下列出了一些复杂的技术:

  • 贝叶斯最优分类器:理论上,有一种称为贝叶斯最优分类器,它将始终是最好的,但这是不切实际的,因为计算上是禁止的。

  • 贝叶斯参数平均化:许多人尝试对贝叶斯最优分类器进行变化,使其更实用,比如贝叶斯参数平均化变化。但它仍然容易过拟合,通常被随机森林背包法超越;你只需多次重新采样数据,运行不同模型,让它们投票决定最终结果。结果表明这样做同样有效,而且简单得多!

  • 贝叶斯模型组合:最后,有一种称为贝叶斯模型组合的东西,试图解决贝叶斯最优分类器和贝叶斯参数平均化的所有缺点。但是,归根结底,它并没有比只是交叉验证组合模型做得更好。

再次强调,这些都是非常复杂的技术,非常难以使用。在实践中,我们最好使用我们更详细讨论过的更简单的技术。但是,如果你想显得聪明并经常使用贝叶斯这个词,熟悉这些技术并知道它们是什么是很好的。

因此,这就是集成学习。再次强调的是,像自助聚合、背包法、提升法、堆叠法或模型桶之类的简单技术通常是正确的选择。还有一些更花哨的技术,但它们在很大程度上是理论性的。但是,至少现在你知道它们了。

尝试集成学习总是一个好主意。一次又一次地证明,它将产生比任何单一模型更好的结果,因此一定要考虑它!

支持向量机概述

最后,我们将讨论支持向量机(SVM),这是一种非常先进的方法,用于聚类或分类高维数据。

那么,如果你有多个要预测的特征呢?支持向量机可能是一个非常强大的工具,结果可能非常好!它在内部非常复杂,但重要的是要理解何时使用它,以及它在更高层次上是如何工作的。所以,现在让我们来讨论支持向量机。

支持向量机是一个花哨的名字,实际上是一个花哨的概念。但幸运的是,它非常容易使用。重要的是要知道它的作用和用途。因此,支持向量机对于分类高维数据效果很好,我指的是许多不同的特征。因此,使用 k 均值聚类之类的东西很容易对具有两个维度的数据进行聚类,也许一个维度是年龄,另一个维度是收入。但是,如果我有许多不同的特征要预测,那么支持向量机可能是一个不错的选择。

支持向量机找到高维支持向量,用于划分数据(数学上,这些支持向量定义超平面)。也就是说,数学上,支持向量机可以找到高维支持向量(这就是它得名的地方),这些支持向量定义了将数据分成不同簇的高维平面。

显然,所有这些都很快变得非常奇怪。幸运的是,scikit-learn 软件包将为您完成所有这些,而无需您实际参与其中。在内部,您需要理解它使用一种称为核技巧的东西来实际找到那些在较低维度中可能不明显的支持向量或超平面。您可以使用不同的核来以不同的方式执行此操作。主要问题是,如果您有具有许多不同特征的高维数据,支持向量机是一个不错的选择,您可以使用不同的核,其计算成本不同,并且可能更适合手头的问题。

重要的一点是 SVM 使用一些高级数学技巧来对数据进行聚类,并且可以处理具有许多特征的数据集。它也相当昂贵 - "核技巧"是唯一使其可能的东西。

我想指出 SVM 是一种监督学习技术。因此,我们实际上要在一组训练数据上对其进行训练,并且我们可以使用它来对未来未见数据或测试数据进行预测。这与 k 均值聚类有点不同,k 均值是完全无监督的;相比之下,支持向量机是基于实际训练数据进行训练的,其中你有一些数据集的正确分类答案可以学习。因此,如果你愿意的话,SVM 对于分类和聚类是有用的 - 但这是一种监督技术!

SVM 经常使用的一个例子是使用称为支持向量分类的东西。典型的例子使用了 Iris 数据集,这是 scikit-learn 附带的样本数据集之一。这个数据集是对不同的鸢尾花进行分类,不同的鸢尾花的不同观察和它们的种类。这个想法是使用关于每朵花瓣的长度和宽度以及每朵花萼的长度和宽度的信息来对这些进行分类。 (萼片显然是花瓣下面的一个小支撑结构。我直到现在也不知道。)你有四个属性的维度;你有花瓣的长度和宽度,以及萼片的长度和宽度。你可以使用这些信息来预测给定信息的鸢尾花的种类。

这是使用 SVC 进行的一个例子:基本上,我们将萼片宽度和萼片长度投影到二维,这样我们就可以实际可视化它:

使用不同的核心可能会得到不同的结果。具有线性核心的 SVC 将产生与前面图像中看到的非常相似的东西。你可以使用多项式核心或更复杂的核心,可能会在图像中显示为二维曲线。你可以通过这种方式进行一些相当花哨的分类。

这些都会增加计算成本,并且可以产生更复杂的关系。但同样,这是一种情况,过于复杂可能会产生误导性的结果,因此你需要小心,并在适当的时候使用训练/测试。由于我们正在进行监督学习,你实际上可以进行训练/测试,并找到适合的模型,或者使用集成方法。

你需要找到适合当前任务的正确核心。对于多项式 SVC 之类的东西,使用什么程度的多项式才是正确的?即使是线性 SVC 也会有与之相关的不同参数,你可能需要进行优化。这将在一个真实的例子中更有意义,所以让我们深入一些实际的 Python 代码,看看它是如何工作的!

使用 SVM 通过 scikit-learn 对人进行聚类

让我们在这里尝试一些支持向量机。幸运的是,使用起来比理解起来要容易得多。我们将回到我用于 k 均值聚类的相同示例,我将创建一些关于一百个随机人的年龄和收入的虚构集群数据。

如果你想回到 k 均值聚类部分,你可以了解更多关于生成虚假数据的代码背后的想法。如果你准备好了,请考虑以下代码:

import numpy as np 

#Create fake income/age clusters for N people in k clusters 
def createClusteredData(N, k): 
    pointsPerCluster = float(N)/k 
    X = [] 
    y = [] 
    for i in range (k): 
        incomeCentroid = np.random.uniform(20000.0, 200000.0) 
        ageCentroid = np.random.uniform(20.0, 70.0) 
        for j in range(int(pointsPerCluster)): 
            X.append([np.random.normal(incomeCentroid, 10000.0),  
            np.random.normal(ageCentroid, 2.0)]) 
            y.append(i) 
    X = np.array(X) 
    y = np.array(y) 
    return X, y 

请注意,因为我们在这里使用的是监督学习,我们不仅需要再次使用特征数据,还需要我们训练数据集的实际答案。

这里的createClusteredData()函数的作用是根据年龄和收入创建一堆围绕k点聚集的随机数据,并返回两个数组。第一个数组是我们称之为X的特征数组,然后我们有我们试图预测的东西的数组,我们称之为y。在 scikit-learn 中,当你创建一个可以进行预测的模型时,这些是它将接受的两个输入,特征向量的列表和你试图预测的东西,它可以从中学习。所以,我们将继续运行。

所以现在我们将使用createClusteredData()函数创建 100 个随机人,分为 5 个不同的集群。我们将创建一个散点图来说明这些人的情况,并看看他们最终落在哪里:

%matplotlib inline 
from pylab import * 

(X, y) = createClusteredData(100, 5) 

plt.figure(figsize=(8, 6)) 
plt.scatter(X[:,0], X[:,1], c=y.astype(np.float)) 
plt.show() 

下图显示了我们正在处理的数据。每次运行这个程序,你都会得到一组不同的集群。所以,你知道,我实际上没有使用随机种子...让生活变得有趣。

这里有几个新东西 - 我在plt.figure()上使用了figsize参数来实际上制作一个更大的图。所以,如果你需要在matplotlib中调整大小,就是这样做的。我使用了相同的技巧,将颜色作为我最终得到的分类号。所以我开始的集群号被绘制为这些数据点的颜色。你可以看到,这是一个相当具有挑战性的问题,这里肯定有一些集群的交错:

现在我们可以使用线性 SVC(SVC 是 SVM 的一种形式)来将其分成集群。我们将使用具有线性核的 SVM,并且 C 值为1.0。C 只是一个可以调整的错误惩罚项;默认为1。通常情况下,你不会想去改变它,但如果你正在使用集成学习或训练/测试对正确模型进行一些收敛,那就是你可以玩耍的东西之一。然后,我们将将该模型拟合到我们的特征数据和我们的训练数据集的实际分类。

from sklearn import svm, datasets 

C = 1.0 
svc = svm.SVC(kernel='linear', C=C).fit(X, y) 

所以,让我们继续运行。我不想过多地讨论我们实际上将如何可视化结果,只是相信plotPredictions()是一个可以绘制分类范围和 SVC 的函数。

它帮助我们可视化不同分类的位置。基本上,它在整个网格上创建一个网格,并且会将来自 SVC 模型的不同分类作为网格上的不同颜色进行绘制,然后我们将在其上绘制我们的原始数据:

def plotPredictions(clf): 
    xx, yy = np.meshgrid(np.arange(0, 250000, 10), 
                     np.arange(10, 70, 0.5)) 
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]) 

    plt.figure(figsize=(8, 6)) 
    Z = Z.reshape(xx.shape) 
    plt.contourf(xx, yy, Z, cmap=plt.cm.Paired, alpha=0.8) 
    plt.scatter(X[:,0], X[:,1], c=y.astype(np.float)) 
    plt.show() 

plotPredictions(svc) 

所以,让我们看看它是如何工作的。SVC 的计算成本很高,所以运行时间很长:

你可以在这里看到它尽了最大努力。考虑到它必须绘制直线和多边形形状,它做了不错的工作来适应我们的数据。所以,你知道,它错过了一些 - 但总体上,结果还是相当不错的。

SVC 实际上是一种非常强大的技术;它的真正优势在于更高维度的特征数据。继续玩耍吧。顺便说一句,如果你不仅想可视化结果,你可以像在 scikit-learn 中的几乎任何模型一样使用predict()函数在 SVC 模型上,传入你感兴趣的特征数组。如果我想预测一个年收入为 20 万美元,年龄为 40 岁的人的分类,我将使用以下代码:

svc.predict([[200000, 40]])

这将把这个人放在我们的情况下,集群编号 1 中:

如果我在这里有一个年收入为 50,000 美元,年龄为 65 岁的人,我将使用以下代码:

svc.predict([[50000, 65]])

这是你的输出现在应该看起来的样子:

这个人最终会进入集群编号 2,无论在这个例子中代表什么。所以,继续玩耍吧。

活动

现在,线性只是你可以使用的许多内核之一,就像我说的,你可以使用许多不同的内核。其中之一是多项式模型,所以你可能想试试。请继续查阅文档。查看文档对你来说是一个很好的练习。如果你要深入使用 scikit-learn,你有很多不同的功能和选项可供选择。所以,去在线查找 scikit-learn,找出 SVC 方法的其他内核是什么,然后尝试它们,看看你是否真的得到了更好的结果。

这不仅是一个关于玩 SVM 和不同类型的 SVC 的练习,还是一个让你熟悉如何自己学习更多关于 SVC 的内容的练习。而且,老实说,任何数据科学家或工程师的一个非常重要的特质将是在你不知道答案时自己去查找信息的能力。

所以,你知道,我没有懒惰,没有告诉你那些其他内核是什么,我希望你习惯于自己查找这些东西的想法,因为如果你总是不得不问别人这些事情,你在工作中会变得非常烦人,非常快。所以,去查一下,玩一下,看看你能得到什么。

所以,这就是 SVM/SVC,一种非常强大的技术,你可以用它来对数据进行分类,在监督学习中。现在你知道它是如何工作的,以及如何使用它,所以记住它吧!

总结

在本章中,我们看到了一些有趣的机器学习技术。我们涵盖了机器学习背后的一个基本概念,称为训练/测试。我们看到如何使用训练/测试来尝试找到合适的多项式度数来适应给定的数据集。然后我们分析了监督学习和无监督学习之间的区别。

我们看到了如何实现一个垃圾邮件分类器,并使其能够使用朴素贝叶斯技术确定一封电子邮件是否是垃圾邮件。我们讨论了 k 均值聚类,一种无监督学习技术,它有助于将数据分组成簇。我们还看了一个使用 scikit-learn 的例子,根据他们的收入和年龄对人进行了聚类。

然后我们继续讨论了熵的概念以及如何衡量它。我们深入讨论了决策树的概念,以及如何在给定一组训练数据的情况下,实际上可以让 Python 为您生成一个流程图来做出决策。我们还建立了一个系统,根据简历中的信息自动过滤简历,并预测一个人的招聘决定。

我们沿途学到了集成学习的概念,并最后谈到了支持向量机,这是一种非常先进的聚类或分类高维数据的方法。然后我们继续使用 SVM 来使用 scikit-learn 对人进行聚类。在下一章中,我们将讨论推荐系统。

第六章:推荐系统

让我们谈谈我个人的专业领域——推荐系统,即可以根据其他人的行为向人们推荐东西的系统。我们将看一些例子以及几种方法。具体来说,两种叫做基于用户和基于物品的协同过滤技术。所以,让我们深入了解一下。

我在amazon.comimdb.com大部分职业生涯都在那里度过,我在那里做的很多工作都是开发推荐系统;比如购买这个的人也购买了,或者为你推荐,以及为人们推荐电影的东西。所以,这是我个人非常了解的东西,我希望能与你们分享一些这方面的知识。我们将逐步讲解以下主题:

  • 什么是推荐系统?

  • 基于用户的协同过滤

  • 基于物品的协同过滤

  • 寻找电影的相似之处

  • 向人们推荐电影

  • 改进推荐系统的结果

什么是推荐系统?

嗯,就像我说的,亚马逊是一个很好的例子,我对此非常熟悉。所以,如果你去他们的推荐部分,就像下面的图片所示,你会看到它会根据你在网站上的过去行为推荐你可能感兴趣的购买物品。

推荐系统可能包括你评价过的东西,或者你购买过的东西,以及其他数据。我不能详细说明,因为他们会追捕我,你知道,对我做坏事。但是,这很酷。你也可以把亚马逊上的购买这个的人也购买了功能看作是一种推荐系统。

不同之处在于你在亚马逊推荐页面上看到的推荐是基于你的所有过去行为,而购买这个的人也购买了浏览这个的人也浏览了之类的东西,只是基于你现在正在看的东西,向你展示与之相似的东西,你可能也会感兴趣。而且,结果表明,你现在正在做的事情可能是你兴趣最强烈的信号。

另一个例子来自 Netflix,就像下面的图片所示(下面的图片是 Netflix 的截图):

他们有各种功能,试图根据你以前喜欢或观看的电影来推荐新电影或其他你还没有看过的电影,并且他们会按类型进行分类。他们有一种不同的方式,他们试图确定你最喜欢的电影类型,然后向你展示更多来自这些类型的结果。所以,这是推荐系统在行动中的另一个例子。

它的整个目的是帮助你发现以前可能不知道的东西,所以这很酷。你知道,它给了个别电影、书籍、音乐或其他东西一个被那些以前可能没有听说过的人发现的机会。所以,你知道,它不仅是很酷的技术,它也在某种程度上平衡了竞争,帮助新物品被大众发现。所以,它在当今社会扮演着非常重要的角色,至少我是这么认为的!有几种方法可以做到这一点,我们将在本章中看到主要的方法。

基于用户的协同过滤

首先,让我们谈谈基于你过去行为推荐东西的方法。一种技术叫做基于用户的协同过滤,它是这样工作的:

顺便说一句,协同过滤只是一个花哨的说法,意思是根据你的行为和其他人的行为的组合来推荐东西,好吗?所以,它是在研究你的行为并将其与其他人的行为进行比较,以得出可能对你感兴趣但你还没有听说过的东西。

  1. 这里的想法是我们建立一个矩阵,记录每个用户曾经购买、查看、评分或者其他你想要基于的兴趣信号的一切。所以基本上,我们的系统中有一个用户的行,该行包含了他们可能对某个产品感兴趣的所有事情。所以,想象一张表,我有用户的行,每一列是一个项目,好吗?这可能是一部电影,一个产品,一个网页,无论什么;你可以用这个做很多不同的事情。

  2. 然后我使用该矩阵来计算不同用户之间的相似性。所以,我基本上将这个矩阵的每一行都视为一个向量,我可以根据他们的行为计算用户之间的相似性。

  3. 大部分喜欢相同东西的两个用户会非常相似,然后我可以根据这些相似性分数进行排序。如果我可以找到所有与你相似的用户,基于他们的过去行为,我就可以找到与我最相似的用户,并推荐他们喜欢但我还没看过的东西。

让我们看一个真实的例子,这样可能会更有意义:

假设在前面的例子中,这位可爱的女士看了《星球大战》和《帝国反击战》,她都很喜欢。所以,我们有一个用户向量,这位女士给《星球大战》和《帝国反击战》都打了 5 星的评分。

假设 Edgy Mohawk 先生来了,他只看了《星球大战》。这是他唯一看过的东西,他不知道《帝国反击战》还没有看过,不知何故,他生活在一个奇怪的宇宙里,他不知道实际上有很多很多《星球大战》电影,事实上每年都在增加。

当然,我们可以说这个家伙实际上与另一个女士相似,因为他们都非常喜欢《星球大战》,所以他们的相似性分数可能相当高,我们可以说,好吧,这位女士喜欢的他还没看过什么?《帝国反击战》就是其中之一,所以我们可以根据他们对《星球大战》的喜爱找到这位女士也喜欢《帝国反击战》,然后将其作为对 Edgy Mohawk 先生的良好推荐。

然后我们可以向他推荐《帝国反击战》,他可能会喜欢,因为在我看来,这实际上是一部更好的电影!但我不打算在这里和你进行极客之争。

基于用户的协同过滤的限制

不幸的是,基于用户的协同过滤有一些限制。当我们考虑基于物品和人之间的关系来推荐东西时,我们的思维往往会转向人与人之间的关系。所以,我们想要找到与你相似的人,并推荐他们喜欢的东西。这似乎是直观的做法,但并不是最好的做法!以下是基于用户的协同过滤的一些限制:

  • 一个问题是人们喜新厌旧;他们的口味总是在变化。所以,也许在前面的例子中,这位可爱的女士经历了一段短暂的科幻动作电影阶段,然后她克服了这一阶段,也许后来她开始更喜欢戏剧或者浪漫电影或者爱情喜剧。所以,如果我的 Edgy Mohawk 先生基于她早期的科幻时期与她有很高的相似性,然后我们因此向他推荐了浪漫喜剧,那将是糟糕的。我的意思是,在我们计算相似性分数的方式上,对此有一些保护,但人们的口味随时间变化仍然会污染我们的数据。所以,比较人与人之间并不总是一件简单的事情,因为人们会改变。

  • 另一个问题是在你的系统中通常会有比物品更多的人,全球有 70 亿人口,而且还在增加,世界上可能并不会有 70 亿部电影,或者你的目录中可能不会有 70 亿个物品需要推荐。在你的系统中找到所有用户之间的相似性可能比找到物品之间的相似性更困难。因此,通过将系统重点放在用户上,你让计算问题变得更加困难,因为你有很多用户,至少希望如此,如果你在一家成功的公司工作的话。

  • 最后一个问题是人们会做坏事。确保你的产品、电影或其他任何东西被推荐给人们有着非常现实的经济激励,有些人会试图操纵系统,让他们的新电影、新产品或新书等被推荐。

在系统中制造假身份非常容易,只需创建一个新用户,让他们执行一系列喜欢很多流行物品的事件,然后也喜欢你的物品。这被称为炒作攻击,我们希望能够拥有一个能够处理这种情况的系统。

关于如何检测和避免基于用户的协同过滤中的炒作攻击有研究,但更好的方法是使用一种完全不容易被操纵系统的全新方法。

这就是基于用户的协同过滤。再次强调,这是一个简单的概念-你根据用户的行为相似性来推荐东西,推荐那些你还没有看过但与你喜欢的东西相似的东西。正如我们所讨论的,这也有其局限性。因此,让我们来谈谈用一种称为基于物品的协同过滤的技术来颠覆整个概念。

基于物品的协同过滤

现在让我们尝试用一种称为基于物品的协同过滤的技术来解决基于用户的协同过滤的一些缺点,我们将看到这种技术是如何更加强大的。实际上,这是亚马逊在幕后使用的技术之一,他们公开谈论过这一点,所以我可以告诉你这么多,但让我们看看为什么这是一个如此好的主意。基于用户的协同过滤是基于人与人之间的关系来进行推荐的,但如果我们将其转变为基于物品之间的关系呢?这就是基于物品的协同过滤。

理解基于物品的协同过滤

这将涉及到一些见解。首先,我们谈到人们喜新厌旧,他们的口味会随时间改变,因此基于他们的过去行为来比较一个人和另一个人变得非常复杂。人们有不同的阶段,他们有不同的兴趣,你可能不会将处于相同阶段的人进行比较。但是,物品永远是什么它是的。一部电影永远是一部电影,它永远不会改变。星球大战永远是星球大战,至少在乔治·卢卡斯稍微改动一下之前是这样的,但总的来说,物品不会像人一样改变。因此,我们知道这些关系更加持久,而且在计算物品之间的相似性时可以进行更直接的比较,因为它们随时间不会改变。

另一个优势是,通常你要推荐的东西比你要推荐给的人要少。所以,全世界有 70 亿人,你的网站上可能并不会有 70 亿个推荐的东西,所以通过评估物品之间的关系而不是用户之间的关系,你可以节省大量的计算资源,因为你的系统中物品的数量可能比用户的数量要少。这意味着你可以更频繁地运行推荐,使它们更加及时、更加更新、更好!你可以使用更复杂的算法,因为你需要计算的关系更少,这是一件好事!

操纵系统也更难。我们谈到了通过创建一些喜欢流行东西的假用户然后推广你想要推广的东西来操纵基于用户的协同过滤方法是多么容易。但是基于物品的协同过滤变得更加困难。你必须让系统相信物品之间存在关系,而且由于你可能没有能力根据许多其他用户创建假物品并与其他物品建立虚假关系,操纵基于物品的协同过滤系统就变得更加困难,这是一件好事。

当我谈到操纵系统时,另一件重要的事情是确保人们用自己的钱投票。避免刷单攻击或人们试图操纵你的推荐系统的一般技术是确保信号行为是基于人们实际花钱的。因此,当你基于人们实际购买的东西而不是他们浏览或点击的东西进行推荐时,你总是会得到更好、更可靠的结果,明白吗?

基于物品的协同过滤是如何工作的?

好了,让我们来谈谈基于物品的协同过滤是如何工作的。它与基于用户的协同过滤非常相似,但我们不是看用户,而是看物品。

所以,让我们回到电影推荐的例子。我们首先要做的是找到每一对被同一个人观看的电影。然后,我们测量所有观看过这部电影的人之间的相似性。通过这种方式,我们可以根据观看过这两部电影的人的评分来计算两部不同电影之间的相似性。

所以,假设我有一对电影,好吧?也许是《星球大战》和《帝国反击战》。我找到了所有观看过这两部电影的人的名单,然后我比较他们对这两部电影的评分,如果他们相似,那么我可以说这两部电影是相似的,因为观看过它们的人对它们的评分相似。这是这里的一般想法。这是一种方法,有多种方法可以做到!

然后我可以按电影对一切进行排序,然后按相似电影的相似度强度进行排序,这就是我得到的喜欢这个也喜欢那个给这个高评分的人也给这个高评分等等的结果。就像我说的,这只是一种方法。

这是基于物品的协同过滤的第一步-首先我根据观看每一对电影的人之间的关系来找到电影之间的关系。当我们通过以下示例时,这将更加清晰:

(图片)

例如,让我们假设在上图中的这位年轻女士观看了《星球大战》和《帝国反击战》,并且喜欢这两部电影,所以给了它们五星或者其他什么评分。现在,又来了一个叫 Edgy Mohawk Man 的人,他也观看了《星球大战》和《帝国反击战》,并且也喜欢这两部电影。所以,此时我们可以说这两部电影之间存在关系,基于这两位喜欢这两部电影的用户。

我们要做的是查看每一对电影。我们有一对《星球大战》和《帝国反击战》,然后我们查看所有观看这两部电影的用户,这两个人,如果他们都喜欢这两部电影,那么我们可以说它们彼此相似。或者,如果他们都不喜欢,我们也可以说它们彼此相似,对吧?所以,我们只是在查看这两个用户与这对电影的相似度得分。

然后来了一个留着小胡子的伐木工艺师,他看了《帝国反击战》,他生活在一个奇怪的世界,他看了《帝国反击战》,但不知道《星球大战》这部第一部电影的存在。

好吧,我们根据这两个人的行为计算了《帝国反击战》和《星球大战》之间的关系,所以我们知道这两部电影彼此相似。因此,鉴于小胡子先生喜欢《帝国反击战》,我们可以有信心地说他也会喜欢《星球大战》,然后我们可以将这部电影推荐给他作为他的首选电影推荐。就像下面的插图一样:

你可以看到最终结果非常相似,但我们已经颠覆了整个事情的本质。所以,我们不再把系统的重点放在人与人之间的关系上,而是放在物品之间的关系上,而这些关系仍然是基于所有观看它们的人的集体行为。但从根本上讲,我们正在研究物品之间的关系,而不是人与人之间的关系。明白了吗?

使用 Python 进行协同过滤

好了,让我们开始吧!我们有一些 Python 代码,将使用 Pandas 和我们可以使用的各种其他工具,用非常少的代码创建电影推荐。

我们要做的第一件事是向你展示基于物品的协同过滤的实践。所以,我们将建立“看过这个电影的人也看过”的关系,基本上就是“喜欢某些东西的人也喜欢这个东西”,所以我们将基于我们从 MovieLens 项目中获得的真实数据来构建这些电影之间的关系。所以,如果你去 MovieLens.org,那里实际上有一个开放的电影推荐系统,人们可以对电影进行评分,并获得新电影的推荐。

他们将所有的基础数据公开供研究人员使用。因此,我们将使用一些真实的电影评分数据-它有点过时,大约是 10 年前的,所以请记住这一点,但这是我们最终要使用的真实行为数据。我们将使用这些数据来计算电影之间的相似性。这些数据本身就很有用。你可以使用这些数据来说“喜欢这部电影的人也喜欢……”。所以,假设我正在查看一部电影的网页。系统可以说:“如果你喜欢这部电影,并且考虑到你正在查看它,你可能也会喜欢这些电影。”即使我们不知道你是谁,这就是一种推荐系统的形式。

现在,这是真实世界的数据,所以我们将遇到一些真实世界的问题。我们最初的结果看起来不太好,所以我们将花一点额外的时间来尝试弄清楚原因,这正是作为数据科学家所花费时间的很大一部分-纠正这些问题,然后重新运行,直到得到有意义的结果。

最后,我们将完全进行基于物品的协同过滤,根据个人的行为向他们推荐电影。所以,让我们开始吧!

寻找电影的相似性

让我们应用基于物品的协同过滤的概念。首先,找出电影之间的相似性-找出哪些电影与其他电影相似。特别是,我们将尝试找出哪些电影与星球大战相似,基于用户评分数据,我们将看看我们得到了什么。让我们深入研究一下!

好的,让我们继续计算基于物品的协同过滤的前半部分,即找到物品之间的相似性。下载并打开SimilarMovies.ipynb文件。

在这种情况下,我们将根据用户行为来查看电影之间的相似性。我们将使用 GroupLens 项目的一些真实电影评分数据。GroupLens.org 提供了真实的电影评分数据,由真正使用MovieLens.org网站给电影评分并获得推荐的人提供。

我们已经在课程资料中包含了您需要的 GroupLens 数据集的数据文件,我们需要做的第一件事是将其导入 Pandas DataFrame 中,我们将在这个示例中真正看到 Pandas 的全部功能。这很酷!

理解代码

我们要做的第一件事是导入 MovieLens 数据集中的u.data文件,这是一个包含数据集中每个评分的制表符分隔文件。

import pandas as pd 

r_cols = ['user_id', 'movie_id', 'rating'] 
ratings = pd.read_csv('e:/sundog-consult/packt/datascience/ml-100k/u.data',  
                      sep='\\t', names=r_cols, usecols=range(3)) 

请注意,您需要在这里添加路径,指向您在计算机上存储下载的 MovieLens 文件的位置。因此,即使我们在 Pandas 上调用read_csv,我们也可以指定一个不同于逗号的分隔符。在这种情况下,它是一个制表符。

我们基本上是在说,从u.data文件中取前三列,并将其导入一个新的 DataFrame,有三列:user_idmovie_idrating

我们最终得到的是一个 DataFrame,对于每个user_id,都有一行,用于标识某个人,然后,对于他们评价的每部电影,我们有movie_id,这是给定电影的一些数字缩写,因此星球大战可能是第 53 部电影之类的,以及他们的评分,1 到 5 星。因此,我们在这里有一个数据库,一个 DataFrame,包含每个用户和他们评价的每部电影,好吗?

现在,我们希望能够使用电影标题,这样我们可以更直观地解释这些结果,所以我们将使用它们的可读名称。

如果您使用的是真正庞大的数据集,您会将其保存到最后,因为您希望尽可能长时间地使用数字,它们更紧凑。不过,出于示例和教学的目的,我们将保留标题,这样您就可以看到发生了什么。

m_cols = ['movie_id', 'title'] 
movies = pd.read_csv('e:/sundog-consult/packt/datascience/ml-100k/u.item', 
                     sep='|', names=m_cols, usecols=range(2)) 

MovieLens 数据集中有一个名为u.item的单独数据文件,它是以管道分隔的,我们导入的前两列将是movie_id和该电影的title。因此,现在我们有两个 DataFrame:r_cols包含所有用户评分,m_cols包含每个movie_id的所有标题。然后,我们可以使用 Pandas 中神奇的merge函数将它们全部合并在一起。

ratings = pd.merge(movies, ratings) 

让我们添加一个ratings.head()命令,然后运行这些单元格。我们最终得到的是类似以下表格的东西。那很快!

我们最终得到了一个新的 DataFrame,其中包含每个用户评价的user_id和电影评分,并且我们既有movie_id又有title,可以阅读并了解它是什么。因此,读取这个数据的方式是user_id308Toy Story (1995)电影评了4星,user_id287Toy Story (1995)电影评了5星,依此类推。如果我们继续查看更多的这个 DataFrame,我们会看到不同的电影的不同评分。

现在 Pandas 的真正魔力显现出来了。因此,我们真正想要的是根据观看每对电影的所有用户之间的关系来查看电影之间的关系,因此最终我们需要一个包含每部电影、每个用户以及每个用户对每部电影的所有评分的矩阵。Pandas 中的pivot_table命令可以为我们做到这一点。它基本上可以根据给定的 DataFrame 构建一个新表,几乎任何你想要的方式。为此,我们可以使用以下代码:

movieRatings = ratings.pivot_table(index=['user_id'],
                                   columns=['title'],values='rating') 
movieRatings.head() 

所以,这段代码的意思是-取出我们的评分 DataFrame 并创建一个名为movieRatings的新 DataFrame,我们希望它的索引是用户 ID,所以我们将为每个user_id有一行,并且我们将每一列都是电影标题。因此,我们将为在该 DataFrame 中遇到的每个标题都有一列,并且如果存在的话,每个单元格将包含rating值。让我们继续运行它。

然后,我们得到了一个新的 DataFrame,看起来像下表:

这真是太神奇了,现在你会看到一些NaN值,代表不是一个数字,这就是 Pandas 表示缺失值的方式。因此,解释这个的方法是,例如,user_id编号1没有观看电影1-900(1994),但user_id编号1观看了《101 斑点狗》(1996)并给了它2星的评价。user_id编号1还观看了《愤怒的公牛》(1957)并给了它5星的评价,但没有观看电影《2 天在山谷(1996)》,例如,明白了吗?因此,我们最终得到的是一个稀疏矩阵,其中包含了每个用户和每部电影,以及每个用户对每部电影的评分值。

所以,现在你可以看到,我们可以非常容易地提取出用户观看的每部电影的向量,也可以提取出每个评价了给定电影的用户的向量,这正是我们想要的。所以,这对基于用户和基于物品的协同过滤都很有用,对吧?如果我想要找到用户之间的关系,我可以查看这些用户行之间的相关性,但如果我想要找到电影之间的相关性,对于基于物品的协同过滤,我可以根据用户行为查看列之间的相关性。这就是真正颠覆用户与基于物品相似性的实现的地方。

现在,我们要进行基于物品的协同过滤,所以我们要提取列,为此让我们运行以下代码:

starWarsRatings = movieRatings['Star Wars (1977)'] 
starWarsRatings.head() 

现在,借助这个,让我们继续提取所有评价了《星球大战(1977)》的用户:

我们可以看到大多数人实际上都观看并评价了《星球大战(1977)》,并且每个人都喜欢它,至少在我们从 DataFrame 的开头取出的这个小样本中是这样。因此,我们得到了一组用户 ID 及其对《星球大战(1977)》的评分。用户 ID3没有对《星球大战(1977)》进行评分,因此我们有一个NaN值,表示那里有一个缺失值,但没关系。我们希望确保保留这些缺失值,以便我们可以直接比较不同电影的列。那么我们该如何做呢?

corrwith 函数

好吧,Pandas 一直让我们很容易,它有一个corrwith函数,你可以在下面的代码中看到,我们可以使用它:

similarMovies = movieRatings.corrwith(starWarsRatings) 
similarMovies = similarMovies.dropna() 
df = pd.DataFrame(similarMovies) 
df.head(10) 

该代码将对给定的列与 DataFrame 中的每一列进行相关性计算,并计算相关性得分并将其返回给我们。所以,我们在这里做的是在整个movieRatings DataFrame 上使用corrwith,这是用户电影评分的整个矩阵,将其与starWarsRatings列进行相关性计算,然后使用dropna删除所有缺失的结果。这样我们就只剩下了有相关性的项目,有多于一个人观看的项目,然后我们基于这些结果创建一个新的 DataFrame,然后显示前 10 个结果。所以,再次回顾一下:

  1. 我们将建立《星球大战》与每部其他电影之间的相关性得分。

  2. 删除所有的NaN值,这样我们只有实际存在的电影相似性,有多于一个人对其进行了评分。

  3. 然后,我们将从结果中构建一个新的 DataFrame,并查看前 10 个结果。

在下面的截图中,我们看到了结果:

我们得到了《星球大战》与每部电影之间的相关性得分结果,例如,与电影《直到有了你(1997)》有惊人的高相关性得分,与电影《1-900(1994)》有负相关性,与《101 斑点狗(1996)》有非常弱的相关性。

现在,我们只需要按相似性得分排序,我们就可以得到《星球大战》的前十个电影相似性了,对吧?让我们继续做吧。

similarMovies.sort_values(ascending=False) 

只需在生成的 DataFrame 上调用sort_values,Pandas 使这变得非常容易,我们可以说ascending=False,实际上按相关性得分的倒序排序。所以,让我们这样做:

好吧,《星球大战(1977)》排名靠前,因为它与自身相似,但其他的是什么?这是怎么回事?我们可以在前面的输出中看到一些电影,比如:《全速前进(1996)》、《年度人物(1995)》、《亡命之徒(1943)》。这些都是,你知道的,相当晦涩的电影,其中大多数我甚至从未听说过,但它们与《星球大战》有完美的相关性。这有点奇怪!显然我们在这里做错了什么。可能是什么呢?

事实证明,有一个完全合理的解释,这是一个很好的教训,为什么你在完成任何数据科学任务时总是需要检查你的结果-质疑结果,因为通常会有一些你忽略的东西,可能需要清理数据,可能你做错了什么。但你也应该怀疑地看待你的结果,不要只是盲目接受,好吗?如果你这样做,你会惹麻烦的,因为如果我真的把这些作为喜欢《星球大战》的人的推荐,我会被解雇的。不要被解雇!注意你的结果!所以,让我们深入研究下一节中出现的问题。

改进电影相似性的结果

让我们弄清楚我们的电影相似性出了什么问题。我们经历了所有这些令人兴奋的工作,计算了基于用户评分向量的电影之间的相关性得分,但我们得到的结果有点糟糕。只是为了提醒你,我们使用了这种技术寻找与《星球大战》相似的电影,结果我们得到了一堆怪异的推荐,排在前面的电影与《星球大战》有完美的相关性。

大多数都是非常晦涩的电影。那么,你认为可能发生了什么?嗯,可能有一个讲得通的解释,假设我们有很多人观看了《星球大战》和其他一些晦涩的电影。我们最终会得到这两部电影之间的很好的相关性,因为它们都与《星球大战》联系在一起,但归根结底,我们真的想要基于观看某些晦涩电影的一两个人的行为来做推荐吗?

可能不是!我的意思是,是的,世界上的两个人,或者无论是什么,看了电影《全速前进》,并且都喜欢它,除了《星球大战》,也许这对他们来说是一个很好的推荐,但对世界其他人来说可能不是一个很好的推荐。我们需要对相似性有一定的信心水平,通过强制执行观看给定电影的人数的最低限制来实现。我们不能仅仅基于一两个人的行为来判断一部电影是否好看。

因此,让我们尝试将这一见解付诸行动,使用以下代码:

import numpy as np 
movieStats = ratings.groupby('title').agg({'rating': [np.size, np.mean]}) 
movieStats.head() 

我们要做的是尝试识别那些实际上没有被很多人评价的电影,然后我们将它们排除,看看我们会得到什么。因此,为了做到这一点,我们将取得我们原始的评分 DataFrame,并且我们将说groupby('title'),同样 Pandas 在其中有各种魔法。这将基本上构建一个新的 DataFrame,将给定标题的所有行聚合成一行。

我们可以说,我们想要特别聚合评分,并且我们想要显示每部电影的大小,即每部电影的评分人数,以及平均平均分数,即该电影的平均评分。因此,当我们这样做时,我们最终得到类似以下的东西:

例如,这告诉我们电影《101 斑点狗(1996 年)》有 109 人评价了这部电影,他们的平均评分是 2.9 颗星,所以实际上并不是很高的分数!因此,如果我们仅凭眼力观察这些数据,我们可以说好吧,我认为比较不知名的电影,比如《187(1997 年)》,有 41 个评分,但《101 斑点狗(1996 年)》,我听说过,你知道《愤怒的公牛(1957 年)》,我也听说过。似乎在大约 100 个评分处有一种自然的截止值,也许这是一个魔法值,事情开始变得有意义。

让我们继续摆脱少于 100 人评分的电影,是的,你知道我在这一点上有点凭直觉。正如我们稍后将讨论的,有更有原则的方法来做到这一点,你实际上可以进行实验,并在不同的阈值上进行训练/测试实验,找到实际表现最好的那个。但最初,让我们只是用常识来过滤掉少于 100 人评分的电影。同样,Pandas 使这变得非常容易。让我们通过以下示例来弄清楚:

popularMovies = movieStats['rating']['size'] >= 100 
movieStats[popularMovies].sort_values([('rating', 'mean')], ascending=False)[:15] 

我们可以说popularMovies,一个新的 DataFrame,将通过查看movieStats构建,我们只会取评分大小大于或等于 100 的行,然后我将按mean评分排序,只是为了好玩,看看最受欢迎的广泛观看的电影。

我们这里有一份由 100 多人评分的电影列表,按其平均评分分数排序,这本身就是一个推荐系统。这些都是受欢迎的高评分电影。《剃须刀奇遇记(1995 年)》,显然是一部非常好的电影,很多人看过并且非常喜欢。

因此,这是一个非常古老的数据集,来自 90 年代末,所以即使你可能不熟悉电影《剃须刀奇遇记(1995 年)》,回头去重新发现它可能是值得的;把它加入你的 Netflix!《辛德勒的名单(1993 年)》并不是一个大惊喜,在大多数顶级电影列表中都会出现。《错误的裤子(1993 年)》,另一个例子,是一部不知名的电影,显然非常好看,也很受欢迎。因此,通过这样做,已经有一些有趣的发现了。

现在情况看起来好多了,所以让我们继续制作我们的新 DataFrame,其中包含与《星球大战》相似的电影,我们只基于出现在这个新 DataFrame 中的电影。所以,我们将使用join操作,将我们原始的similarMovies DataFrame 与这个只有超过 100 个评分的电影的新 DataFrame 进行连接,好吗?

df = movieStats[popularMovies].join(pd.DataFrame(similarMovies, columns=['similarity'])) 
df.head() 

在这段代码中,我们基于similarMovies创建了一个新的 DataFrame,从中提取了similarity列,将其与我们的movieStats DataFrame(即我们的popularMovies DataFrame)进行了连接,并查看了合并的结果。然后,我们就有了输出!

现在,我们只限制了那些被 100 多人评价的电影,与《星球大战》的相似度得分。所以,现在我们需要做的就是使用以下代码对其进行排序:

df.sort_values(['similarity'], ascending=False)[:15] 

在这里,我们将对其进行逆向排序,并只查看前 15 个结果。如果你现在运行它,你应该会看到以下内容:

情况开始好转了!《星球大战》(1977)因为与自己相似,所以排在第一位,《帝国反击战》(1980)排在第二位,《绝地归来》(1983)排在第三位,《夺宝奇兵》(1981)排在第四位。你知道,它还不完美,但这些更有意义,对吧?所以,你会期望原始三部曲的三部《星球大战》电影相互之间相似,这些数据还是在下一部三部曲之前,而《夺宝奇兵》(1981)也是一部风格非常相似的电影,排在第四位。所以,我对这些结果开始感到有点满意。还有改进的空间,但嘿!我们得到了一些有意义的结果,哇呜!

现在,理想情况下,我们还应该过滤掉《星球大战》,你不想看到与你开始的电影本身的相似性,但我们以后再担心这个!所以,如果你想再玩一下,就像我说的,100 是最低评分的一个任意截止点。如果你确实想尝试不同的截止值,我鼓励你回去尝试一下。看看它对结果有什么影响。你知道,在前面的表中,我们真正喜欢的结果实际上有更多的共同评分超过 100。所以,我们最终得到了《奥斯汀·鲍尔的国际人质》(1997)的评分相当高,只有 130 个评分,所以也许 100 还不够高!《木偶奇遇记》(1940)以 101 分进入,与《星球大战》不太相似,所以,你可能需要考虑更高的阈值,看看它会有什么影响。

请记住,这是一个非常小的、用于实验目的的有限数据集,它基于非常旧的数据,所以你只会看到较旧的电影。因此,从直觉上解释这些结果可能会有点具有挑战性,但结果并不差。

现在让我们继续,实际上进行全面的基于物品的协同过滤,通过使用更完整的系统向人们推荐电影,我们将在下一步中进行。

向人们推荐电影

好的,让我们实际构建一个完整的推荐系统,它可以查看系统中每个人的所有行为信息,以及他们评价的电影,并利用这些信息为数据集中的任何用户实际生成最佳推荐电影。这有点令人惊讶,你会对它有多简单感到惊讶。让我们开始吧!

让我们开始使用ItemBasedCF.ipynb文件,首先导入我们拥有的 MovieLens 数据集。同样,我们现在只使用其中包含 10 万个评分的子集。但是,你可以从 GroupLens.org 获得更大的数据集-高达数百万个评分;如果你愿意的话。但是请记住,当你开始处理真正大的数据时,你将会推动单台机器和 Pandas 所能处理的极限。话不多说,这是第一段代码:

import pandas as pd 

r_cols = ['user_id', 'movie_id', 'rating'] 
ratings = pd.read_csv('e:/sundog-consult/packt/datascience/ml-100k/u.data',      
                      sep='\t', names=r_cols, usecols=range(3)) 

m_cols = ['movie_id', 'title'] 
movies = pd.read_csv('e:/sundog-consult/packt/datascience/ml-100k/u.item', 
                     sep='|', names=m_cols, usecols=range(2)) 

ratings = pd.merge(movies, ratings) 

ratings.head() 

就像之前一样,我们将导入包含每个用户的所有个人评分以及他们评分的电影的u.data文件,然后将其与电影标题联系起来,这样我们就不必只使用数字电影 ID。点击运行单元格按钮,我们得到以下 DataFrame。

例如,user_id编号308玩具总动员(1995)评了 4 星,user_id编号66玩具总动员(1995)评了 3 星。而且,这将包含每个用户对每部电影的每个评分。

然后,就像之前一样,我们使用 Pandas 中的pivot_table命令来基于信息构建一个新的 DataFrame:

userRatings = ratings.pivot_table(index=['user_id'],
                                  columns=['title'],values='rating') 
userRatings.head() 

在这里,每行是user_id,列由数据集中所有独特的电影标题组成,每个单元格包含一个评分:

我们得到的是前面输出中显示的非常有用的矩阵,其中每行都有用户,每列都有电影。而且我们在这个矩阵中基本上有每部电影的每个用户评分。例如,user_id编号1101 斑点狗(1996)评了 2 星。而且,所有这些NaN值代表缺失的数据。这只是表示,例如,user_id编号1没有对电影1-900(1994)进行评分。

这是一个非常有用的矩阵。如果我们正在进行基于用户的协同过滤,我们可以计算每个单独用户评分向量之间的相关性以找到相似的用户。由于我们正在进行基于物品的协同过滤,我们更感兴趣的是列之间的关系。因此,例如,计算任意两列之间的相关性分数,这将为给定电影对给出相关性分数。那么,我们该如何做呢?事实证明,Pandas 也使这变得非常容易。

它有一个内置的corr函数,实际上会计算整个矩阵中找到的每一对列的相关性分数-这几乎就像它们在为我们考虑。

corrMatrix = userRatings.corr() 
corrMatrix.head() 

让我们继续运行前面的代码。这是一个计算量相当大的事情,所以实际上需要一些时间才能得出结果。但是,我们得到了!

那么,在前面的输出中我们有什么?我们在这里有一个新的 DataFrame,其中每部电影都在行上,列中。因此,我们可以查看任意两部电影的交集,并根据我们最初拥有的userRatings数据找到它们之间的相关性分数。这有多酷呢?例如,电影101 斑点狗(1996)与自己完全相关,因为它具有相同的用户评分向量。但是,如果你看看101 斑点狗(1996)电影与十二怒汉(1957)电影的关系,它的相关性分数要低得多,因为这些电影相当不相似,这是有道理的,对吧?

现在我有了一个很棒的矩阵,可以给出任意两部电影之间的相似度分数。这有点令人惊讶,并且对我们即将要做的事情非常有用。就像之前一样,我们必须处理虚假的结果。所以,我不想看到基于少量行为信息的关系。

原来 Pandas 的corr函数实际上有一些参数可以给它。其中一个是你想要使用的实际相关性评分方法,所以我要说使用pearson相关性。

corrMatrix = userRatings.corr(method='pearson', min_periods=100) 
corrMatrix.head() 

你会注意到它还有一个min_periods参数,你可以给它,基本上是说我只想要你考虑至少,例如在这个例子中,有 100 人评分过两部电影的相关性评分。运行这个将消除那些只基于少数人的虚假关系。运行代码后得到的矩阵如下:

这与我们在项目相似性练习中所做的有点不同,那里我们只是扔掉了少于 100 人评分的任何电影。我们在这里所做的是,扔掉了少于 100 人评分两部电影的电影相似性,好吗?所以,你可以看到在前面的矩阵中我们有更多的NaN值。

实际上,甚至与自己相似的电影也被排除了,所以例如,电影1-900 (1994),据推测,被少于 100 人观看,所以它被完全抛弃了。然而,电影101 斑点狗 (1996)以相关性评分1幸存下来,而在这个数据集的这个小样本中,没有一部电影与另一部有 100 个共同观看的人不同。但是,有足够多的电影幸存下来以获得有意义的结果。

通过示例了解电影推荐

那么,我们用这些数据做什么呢?嗯,我们想要为人们推荐电影。我们这样做的方式是,我们查看给定人的所有评分,找到与他们评分相似的电影,这些电影就是向该人推荐的候选电影。

让我们从创建一个虚拟人来为其创建推荐开始。我实际上已经手动添加了一个虚拟用户,ID 号为0,到我们正在处理的 MovieLens 数据集中。你可以用以下代码看到该用户:

myRatings = userRatings.loc[0].dropna() 
myRatings 

这给出了以下输出:

这有点像我这样的人,我喜欢《星球大战》和《帝国反击战》,但讨厌《飘》。所以,这代表着一个真正喜欢《星球大战》的人,但不喜欢老式的浪漫戏剧,好吗?所以,我给《帝国反击战 (1980)》和《星球大战 (1977)》评了5星,给《飘 (1939)》评了1星。所以,我要为这个虚构的用户找到推荐。

那么,我怎么做呢?嗯,让我们从创建一个名为simCandidates的系列开始,我将浏览我评分的每一部电影。

simCandidates = pd.Series() 
for i in range(0, len(myRatings.index)): 
    print "Adding sims for " + myRatings.index[i] + "..." 
    # Retrieve similar movies to this one that I rated 
    sims = corrMatrix[myRatings.index[i]].dropna() 
    # Now scale its similarity by how well I rated this movie 
    sims = sims.map(lambda x: x * myRatings[i]) 
    # Add the score to the list of similarity candidates 
    simCandidates = simCandidates.append(sims) 

#Glance at our results so far: 
print "sorting..." 
simCandidates.sort_values(inplace = True, ascending = False) 
print simCandidates.head(10) 

对于i在范围0到我在myRatings中拥有的评分数量,我将把我评分的相似电影加起来。所以,我将拿那个corrMatrix DataFrame,那个神奇的包含所有电影相似性的,然后我将用myRatings创建一个相关性矩阵,删除任何缺失值,然后我将按我对那部电影的评分来缩放结果的相关性评分。

这里的想法是,我将浏览例如《帝国反击战》的所有相似之处,然后将其全部缩放 5 倍,因为我真的很喜欢《帝国反击战》。但是,当我浏览《飘》的相似之处时,我只会将其缩放 1 倍,因为我不喜欢《飘》。所以,这将使与我喜欢的电影相似的电影更有力量,而与我不喜欢的电影相似的电影则更弱一些,好吗?

所以,我只是浏览并建立了这个相似候选列表,如果你愿意的话,就是推荐候选,对结果进行排序并打印出来。让我们看看我们得到了什么:

嘿,这些看起来不错,对吧?显然,《帝国反击战》(1980)和《星球大战》(1977)排在前面,因为我明确喜欢这些电影,我已经看过并评分了。但是,排在榜单前列的还有《绝地归来》(1983),这是我们预料到的,《亚马逊探险记》(1981)也是。

让我们开始进一步完善这些结果。我们发现我们得到了重复的值。如果有一部电影与我评分的多部电影相似,它将在结果中出现多次,所以我们希望将它们合并在一起。如果我确实有相同的电影,也许应该将它们加在一起,形成一个更强大的推荐分数。例如,《绝地归来》实际上与《星球大战》和《帝国反击战》都很相似。我们该怎么做呢?

使用 groupby 命令来合并行

我们将继续探索。我们将再次使用groupby命令来将所有属于同一部电影的行分组在一起。接下来,我们将总结它们的相关分数并查看结果:

simCandidates = simCandidates.groupby(simCandidates.index).sum() 
simCandidates.sort_values(inplace = True, ascending = False) 
simCandidates.head(10) 

以下是结果:

嘿,这看起来真的很不错!

所以,《绝地归来》(1983)得分最高,得分为 7,紧随其后的是《亚马逊探险记》(1981),得分为 5,然后我们开始看到《印第安纳琼斯:最后的十字军东征》(1989)和一些其他电影,《桂河大桥》(1957),《回到未来》(1985),《刺激》(1973)。这些都是我真的会喜欢看的电影!你知道,我其实也喜欢老式的迪士尼电影,所以《灰姑娘》(1950)并不像看起来那么疯狂。

我们需要做的最后一件事是过滤掉我已经评分过的电影,因为推荐你已经看过的电影是没有意义的。

使用删除命令删除条目

所以,我可以使用以下代码快速删除任何出现在我的原始评分系列中的行:

filteredSims = simCandidates.drop(myRatings.index) 
filteredSims.head(10) 

运行这个命令让我看到最终的前 10 个结果:

就是这样!《绝地归来》(1983),《亚马逊探险记》(1981),《印第安纳琼斯:最后的十字军东征》(1989),这些都是我虚构用户的前几个推荐结果,而且都很合理。我看到了一些适合家庭观看的电影,你知道,《灰姑娘》(1950),《绿野仙踪》(1939),《小飞象》(1941),可能是因为《飘》的存在,即使它的权重被降低了,但它仍然在其中,仍然被计算在内。所以,这就是我们的结果。就是这样!挺酷的!

实际上我们已经为特定用户生成了推荐,我们可以为数据框中的任何用户这样做。所以,如果你愿意的话,可以尝试一下。我还想谈谈你如何更深入地参与其中,玩弄这些结果;试着改进它们。

这其实是一门艺术,你知道,你需要不断迭代,尝试不同的想法和不同的技术,直到你得到越来越好的结果,你可以一直这样做。我的意思是,我把整个职业都建立在这个基础上。所以,我不指望你像我一样花 10 年的时间来完善这个,但是有一些简单的事情你可以做,所以让我们谈谈这个。

改进推荐结果

作为一个练习,我想挑战你去让这些推荐变得更好。所以,让我们谈谈我有的一些想法,也许你也有一些自己的想法,可以尝试和实验一下;动手尝试,努力做出更好的电影推荐。

好吧,这些推荐结果仍然有很大的改进空间。我们在如何根据你对物品的评分来权衡不同的推荐结果,或者你想要为两部给定电影评分的人数选择最低阈值等方面做出了很多决定。所以,有很多事情你可以调整,很多不同的算法你可以尝试,你可以尝试通过系统来做出更好的电影推荐。所以,如果你感兴趣,我挑战你去做到这一点!

以下是一些关于如何实际上尝试改进本章结果的想法。首先,你可以直接去玩ItembasedCF.ipynb文件并对其进行调整。例如,我们发现相关性方法实际上有一些相关性计算的参数,我们在示例中使用了 Pearson,但还有其他方法可以查找和尝试,看看它对你的结果有什么影响。我们使用了最小周期值为 100,也许这个值太高了,也许太低了;我们只是随意选择的。如果你调整这个值会发生什么?例如,如果你将它降低,我预计你会看到一些你从未听说过的新电影,但可能仍然是对那个人的一个很好的推荐。或者,如果你将它提高,你会看到,你知道,只有大片。

有时候你必须考虑一下你想从推荐系统中得到什么结果。在向人们展示他们听说过的电影和他们没听说过的电影之间,是否有一个很好的平衡?对于这些人来说,发现新电影有多重要,与通过看到许多他们听说过的电影来对推荐系统产生信心有多重要?所以,这确实是一种艺术。

我们还可以改进一下,因为我们在结果中看到了很多与《飘》相似的电影,尽管我不喜欢《飘》。你知道,我们将这些结果的权重低于我喜欢的电影的相似性,但也许这些电影实际上应该受到惩罚。如果我那么讨厌《飘》,也许与《飘》相似的电影,比如《绿野仙踪》,实际上应该受到惩罚,你知道,它们的得分应该降低而不是提高。

这是另一个简单的修改,你可以尝试一下。我们的用户评分数据集中可能有一些异常值,如果我把那些评价了大量电影的人排除掉会怎么样?也许他们在影响一切。你实际上可以尝试识别这些用户并将他们排除在外,这是另一个想法。而且,如果你真的想要一个大项目,如果你真的想要深入研究这些东西,你实际上可以通过使用训练/测试的技术来评估这个推荐引擎的结果。所以,如果不是使用每部电影的相关性得分的任意推荐得分,而是将其缩小到每部电影的预测评分,会怎么样呢?

如果我的推荐系统的输出是一部电影和我对那部电影的预测评分,在一个训练/测试系统中,我实际上可以尝试弄清楚我有多好地预测了用户实际上观看并评价过的电影?好吗?所以,我可以留出一些评分数据,看看我的推荐系统能够多好地预测用户对这些电影的评分。这将是一种定量和有原则的方法来衡量这个推荐引擎的误差。但是,这里比科学更多一点艺术。即使 Netflix 奖实际上使用了那个误差度量,称为均方根误差,这是他们特别使用的,但这真的是一个好的推荐系统的衡量标准吗?

基本上,你正在衡量你的推荐系统预测一个人已经观看的电影的能力。但是推荐引擎的目的不是推荐一个人尚未观看但可能会喜欢的电影吗?这是两回事。所以不幸的是,很难衡量你真正想要衡量的东西。有时,你确实必须凭直觉行事。而且,衡量推荐引擎结果的正确方式是衡量你试图通过它来推广的结果。

也许我试图让人们观看更多电影,或者更高评价新电影,或者购买更多东西。在真实网站上运行实际的控制实验将是优化的正确方式,而不是使用训练/测试。所以,你知道,我在那里详细介绍了一点,但教训是,你不能总是以黑白思维来考虑这些事情。有时,你不能直接和定量地衡量事物,你必须运用一点常识,这就是一个例子。

无论如何,这些是一些关于如何回头改进我们编写的推荐引擎结果的想法。所以,请随意尝试一下,看看你是否可以按照自己的意愿改进它,并且玩得开心。这实际上是书中非常有趣的部分,所以我希望你会喜欢它!

总结

所以,去尝试一下吧!看看你是否可以改进我们的初始结果。有一些简单的想法可以尝试使这些推荐更好,还有一些更复杂的想法。现在,没有对错答案;我不会要求你交作业,也不会审查你的工作。你知道,你决定玩弄它并熟悉一下,进行实验,看看你得到什么结果。这就是整个目的-只是让你更熟悉使用 Python 进行这种工作,并更熟悉基于物品的协同过滤背后的概念。

在本章中,我们看了不同的推荐系统-我们排除了基于用户的协同过滤系统,直接进入了基于物品的系统。然后,我们使用了 pandas 的各种函数来生成和完善我们的结果,我希望你在这里看到了 pandas 的强大之处。

在下一章中,我们将深入研究更高级的数据挖掘和机器学习技术,包括 K 最近邻算法。我期待着向你解释这些内容,并看看它们如何有用。

第七章:更多数据挖掘和机器学习技术

在这一章中,我们将讨论更多的数据挖掘和机器学习技术。我们将讨论一个称为k 最近邻居KNN)的非常简单的技术。然后,我们将使用 KNN 来预测电影的评级。之后,我们将继续讨论降维和主成分分析。我们还将看一个 PCA 的例子,其中我们将 4D 数据降低到两个维度,同时仍保留其方差。

然后,我们将介绍数据仓库的概念,并了解新的 ELT 过程相对于 ETL 过程的优势。我们将学习强化学习的有趣概念,并了解智能吃豆人游戏的背后使用的技术。最后,我们将看到一些用于强化学习的花哨术语。

我们将涵盖以下主题:

  • K 最近邻居的概念

  • KNN 的实施以预测电影的评级

  • 降维和主成分分析

  • 鸢尾花数据集的 PCA 示例

  • 数据仓库和 ETL 与 ELT

  • 什么是强化学习

  • 智能吃豆人游戏背后的工作

  • 用于强化学习的花哨术语

K 最近邻居 - 概念

让我们谈谈雇主希望您了解的一些数据挖掘和机器学习技术。我们将从一个称为 KNN 的非常简单的技术开始。您会对一个好的监督式机器学习技术有多简单感到惊讶。让我们来看看!

KNN 听起来很花哨,但实际上是最简单的技术之一!假设您有一个散点图,并且可以计算该散点图上任意两点之间的距离。假设您已经对一堆数据进行了分类,可以从中训练系统。如果我有一个新的数据点,我只需根据该距离度量查看 KNN,并让它们全部对新点的分类进行投票。

让我们想象以下散点图正在绘制电影。方块代表科幻电影,三角形代表戏剧电影。我们将说这是根据评分与受欢迎程度绘制的,或者您可以想象其他任何东西:

在这里,我们有一种基于散点图上任意两点之间的评分和受欢迎程度计算的某种距离。假设有一个新点进来,一个我们不知道流派的新电影。我们可以将K设置为3,并取散点图上这一点的3个最近邻居;然后它们可以就新点/电影的分类进行投票。

您可以看到,如果我选择三个最近的邻居(K=3),我有 2 部戏剧电影和 1 部科幻电影。然后我会让它们全部投票,我们将根据这 3 个最近的邻居选择这个新点的戏剧分类。现在,如果我将这个圈扩大到包括 5 个最近的邻居,即K=5,我会得到一个不同的答案。在这种情况下,我挑选了 3 部科幻电影和 2 部戏剧电影。如果我让它们全部投票,我最终会得到一个新电影的科幻分类。

我们选择 K 可能非常重要。您要确保它足够小,以免走得太远并开始挑选无关的邻居,但它必须足够大,以包含足够的数据点以获得有意义的样本。因此,通常您将不得不使用训练/测试或类似的技术来实际确定给定数据集的K的正确值。但是,最终,您必须从直觉开始并从那里开始工作。

就是这么简单,就是这么简单。因此,这是一种非常简单的技术。您所做的就是在散点图上找到 k 个最近邻,让它们全部对分类进行投票。它确实符合监督学习,因为它使用一组已知点的训练数据,即已知的分类,来指导新点的分类。

但让我们对此做一些更复杂的事情,并且实际上根据它们的元数据玩弄电影。让我们看看是否可以实际上根据这些电影的内在值,例如其评分、类型信息,找出电影的最近邻:

理论上,我们可以使用 k 最近邻算法重新创建类似于观看此商品的客户还观看了(上图是亚马逊的截图)的东西。而且,我可以再进一步:一旦我根据 k 最近邻算法确定了与给定电影相似的电影,我可以让它们全部对预测的电影评分进行投票。

这就是我们下一个示例要做的。所以现在您已经了解了 KNN,k 最近邻的概念。让我们继续并将其应用于实际找到彼此相似的电影,并使用这些最近邻的电影来预测我们以前没有看过的电影的评分。

使用 KNN 来预测电影的评分

好了,我们将实际上采用 KNN 的简单思想,并将其应用于一个更复杂的问题,即仅根据其类型和评分信息预测电影的评分。因此,让我们深入研究并尝试仅基于 KNN 算法来预测电影评分,看看我们能得到什么。因此,如果您想跟着做,请打开KNN.ipynb,您可以和我一起玩。

我们要做的是定义基于电影元数据的距离度量。通过元数据,我指的是仅与电影相关的信息,即与电影相关联的信息。具体来说,我们将查看电影的类型分类。

我们的MovieLens数据集中的每部电影都有关于它所属类型的附加信息。一部电影可以属于多种类型,比如科幻、戏剧、喜剧或动画。我们还将查看电影的整体受欢迎程度,由评分人数给出,并且我们还知道每部电影的平均评分。我可以将所有这些信息结合在一起,基本上创建一个基于评分信息和类型信息的两部电影之间的距离度量。让我们看看我们得到了什么。

我们将再次使用 pandas 来简化生活,如果您跟着做,请确保将MovieLens数据集的路径更改为您安装它的位置,这几乎肯定不是这个 Python 笔记本中的位置。

请继续进行更改,如果您想跟着做。与以前一样,我们将只导入实际的评分数据文件u.data,使用 pandas 中的read_csv()函数。我们将告诉它实际上是一个制表符分隔符而不是逗号。我们将导入前 3 列,这些列代表user_idmovie_id和评分,对于数据集中每个电影的评分:

import pandas as pd 

r_cols = ['user_id', 'movie_id', 'rating'] 
ratings = pd.read_csv('C:\DataScience\ml-100k\u.data', sep='\t', names=r_cols, usecols=range(3)) 
ratings.head()ratings.head() 

如果我们继续运行并查看顶部,我们可以看到它正在工作,输出应该如下所示:

我们最终得到一个具有user_idmovie_idratingDataFrame。例如,user_id 0movie_id 50进行了评分,我相信这是《星球大战》,给了 5 颗星,依此类推。

我们接下来要做的是,为每部电影聚合评分信息。我们使用 pandas 中的groupby()函数,实际上按movie_id对所有内容进行分组。我们将合并每部电影的所有评分,并输出每部电影的评分数量和平均评分分数,即平均值:

movieProperties = ratings.groupby('movie_id').agg({'rating': 
 [np.size, np.mean]}) 
movieProperties.head() 

让我们继续做这个 - 很快就会回来,以下是输出的样子:

这给我们另一个DataFrame,告诉我们,例如,movie_id 1452个评分(这是它受欢迎程度的衡量,即有多少人实际观看并评分),以及平均评分为 3.8。因此,有452人观看了movie_id 1,他们给出了平均评分为 3.87,这相当不错。

现在,评分的原始数量对我们来说并不那么有用。我的意思是,我不知道452是否意味着它受欢迎与否。因此,为了使其标准化,我们将基本上根据每部电影的最大和最小评分数量来衡量。我们可以使用lambda函数来做到这一点。因此,我们可以以这种方式将函数应用于整个DataFrame

我们要做的是使用np.min()np.max()函数来找到整个数据集中发现的最大评分数量和最小评分数量。因此,我们将找到最受欢迎的电影和最不受欢迎的电影,并将一切标准化到这个范围内:

movieNumRatings = pd.DataFrame(movieProperties['rating']['size']) 
movieNormalizedNumRatings = movieNumRatings.apply(lambda x: (x - np.min(x)) / (np.max(x) - np.min(x))) 
movieNormalizedNumRatings.head() 

当我们运行它时,它给我们的是以下内容:

这基本上是每部电影的受欢迎程度的衡量,范围是 0 到 1。因此,这里的得分为 0 意味着没有人观看,这是最不受欢迎的电影,得分为1意味着每个人都观看了,这是最受欢迎的电影,或者更具体地说,是最多人观看的电影。因此,我们现在有了一个可以用于我们的距离度量的电影受欢迎程度的衡量。

接下来,让我们提取一些一般信息。原来有一个u.item文件,不仅包含电影名称,还包含每部电影所属的所有流派:

movieDict = {} 
with open(r'c:/DataScience/ml-100k/u.item') as f: 
    temp = '' 
    for line in f: 
        fields = line.rstrip('\n').split('|') 
        movieID = int(fields[0]) 
        name = fields[1] 
        genres = fields[5:25] 
        genres = map(int, genres) 
        movieDict[movieID] = (name, genres,      
        movieNormalizedNumRatings.loc[movieID].get('size'),movieProperties.loc[movieID].rating.get('mean')) 

上面的代码实际上会遍历u.item的每一行。我们正在以困难的方式做这个;我们没有使用任何 pandas 函数;这次我们将直接使用 Python。再次确保将路径更改为您安装此信息的位置。

接下来,我们打开我们的u.item文件,然后逐行遍历文件中的每一行。我们去掉末尾的换行符,并根据该文件中的管道分隔符进行拆分。然后,我们提取movieID,电影名称和所有单独的流派字段。因此,基本上在这个源数据中有 19 个不同字段中的一堆 0 和 1,其中每个字段代表一个给定的流派。最后,我们构建一个 Python 字典,将电影 ID 映射到它们的名称、流派,然后我们还将我们的评分信息折叠回去。因此,我们将得到名称、流派、受欢迎程度(在 0 到 1 的范围内)、以及平均评分。这段代码就是做这个的。让我们运行一下!并且,为了看看我们最终得到了什么,我们可以提取movie_id 1的值:

movieDict[1] 

以下是上述代码的输出:

我们字典中movie_id 1的第一个条目恰好是《玩具总动员》,这是一部你可能听说过的 1995 年的皮克斯老电影。接下来是所有流派的列表,其中 0 表示它不属于该流派,1 表示它属于该流派。在MovieLens数据集中有一个数据文件,可以告诉你这些流派字段实际对应的是什么。

对于我们的目的来说,这实际上并不重要,对吧?我们只是试图根据它们的流派来衡量电影之间的距离。数学上重要的是这个流派向量与另一部电影有多相似,好吗?实际的流派本身并不重要!我们只想看看两部电影在它们的流派分类上有多相同或不同。所以我们有那个流派列表,我们有我们计算的受欢迎程度分数,还有 Toy Story 的平均评分。好了,让我们继续想办法将所有这些信息结合到一个距离度量中,这样我们就可以找到 Toy Story 的 k 个最近邻居了。

我已经相当随意地计算了这个ComputeDistance()函数,它接受两个电影 ID 并计算两者之间的距离分数。首先,我们将基于两个流派向量之间的相似性,使用余弦相似度度量来计算。就像我说的,我们将只是拿出每部电影的流派列表,看看它们彼此有多相似。再次强调,0表示它不属于该流派,1表示它属于该流派。

然后,我们将比较受欢迎程度分数,只取原始差异,这两个受欢迎程度分数之间的绝对值差异,并将其用于距离度量。然后,我们将仅使用这些信息来定义两部电影之间的距离。所以,例如,如果我们计算电影 ID 2 和 4 之间的距离,这个函数将返回一些仅基于该电影的受欢迎程度和这些电影的流派的距离函数。

现在,想象一下一个散点图,就像我们在前面的章节中看到的那样,其中一个轴可能是基于余弦度量的流派相似性的度量,另一个轴可能是受欢迎程度,好吗?我们只是在这两个事物之间找到距离:

from scipy import spatial 

def ComputeDistance(a, b): 
    genresA = a[1] 
    genresB = b[1] 
    genreDistance = spatial.distance.cosine(genresA, genresB) 
    popularityA = a[2] 
    popularityB = b[2] 
    popularityDistance = abs(popularityA - popularityB) 
    return genreDistance + popularityDistance 

ComputeDistance(movieDict[2], movieDict[4]) 

在这个例子中,我们试图使用我们的距离度量来计算电影 2 和 4 之间的距离,我们得到了一个 0.8 的分数:

记住,远距离意味着不相似,对吧?我们想要最近的邻居,距离最小。所以,0.8 的分数在 0 到 1 的范围内是一个相当高的数字。这告诉我这些电影实际上并不相似。让我们快速进行一次理智检查,看看这些电影实际上是什么:

print movieDict[2] 
print movieDict[4] 

结果是电影《黄金眼》和《短小的》这两部电影,它们是非常不同的电影:

你知道,你有詹姆斯·邦德动作冒险片,还有一部喜剧电影 - 完全不相似!它们在受欢迎程度上实际上是可比较的,但是流派的差异让它们不同。好了!那么,让我们把它全部整合在一起吧!

接下来,我们将写一小段代码来实际获取一些给定的电影 ID 并找到 KNN。所以,我们所要做的就是计算 Toy Story 和我们电影字典中的所有其他电影之间的距离,并根据它们的距离分数对结果进行排序。以下的代码片段就是这样做的。如果你想花点时间来理解一下,它其实非常简单。

我们有一个小小的getNeighbors()函数,它将获取我们感兴趣的电影和我们想要找到的 K 个邻居。它将遍历我们拥有的每部电影;如果它实际上是一部不同于我们正在查看的电影,它将计算之前的距离分数,将其附加到我们的结果列表中,并对该结果进行排序。然后我们将挑选出前 K 个结果。

在这个例子中,我们将K设置为 10,找到 10 个最近的邻居。我们将使用getNeighbors()找到 10 个最近的邻居,然后遍历所有这 10 个最近的邻居,并计算每个邻居的平均评分。这个平均评分将告诉我们对于所讨论的电影的评分预测。

作为一个副作用,我们还根据我们的距离函数得到了 10 个最近的邻居,我们可以称之为相似的电影。所以,这个信息本身是有用的。回到那个“观看此影片的顾客还观看了”这个例子,如果你想做一个类似的功能,它只是基于这个距离度量而不是实际的行为数据,这可能是一个合理的起点,对吧?

import operator 

def getNeighbors(movieID, K): 
    distances = [] 
    for movie in movieDict: 
        if (movie != movieID): 
            dist = ComputeDistance(movieDict[movieID], 
 movieDict[movie]) 
            distances.append((movie, dist)) 
    distances.sort(key=operator.itemgetter(1)) 
    neighbors = [] 
    for x in range(K): 
        neighbors.append(distances[x][0]) 
    return neighbors 

K = 10 
avgRating = 0 
neighbors = getNeighbors(1, K) 
for neighbor in neighbors: 
    avgRating += movieDict[neighbor][3] 
    print movieDict[neighbor][0] + " " + 
 str(movieDict[neighbor][3]) 
    avgRating /= float(K) 

所以,让我们继续运行这个,看看我们得到了什么。以下是上述代码的输出:

结果并不那么不合理。所以,我们以《玩具总动员》这部电影为例,它是电影 ID 1,我们得到的前 10 个最近邻居,是一些相当不错的喜剧和儿童电影。所以,鉴于《玩具总动员》是一部受欢迎的喜剧和儿童电影,我们得到了一堆其他受欢迎的喜剧和儿童电影;所以,似乎是有效的!我们并没有使用一堆花哨的协同过滤算法,这些结果并不那么糟糕。

接下来,让我们使用 KNN 来预测评分,这里我们将评分视为这个例子中的分类:

avgRating 

以下是上述代码的输出:

我们最终得到了一个预测评分为 3.34,实际上这与该电影的实际评分 3.87 并没有太大的不同。所以不是很好,但也不算太糟糕!我的意思是,实际上它的效果出奇的好,考虑到这个算法是多么简单!

活动

在这个例子中,大部分复杂性都在确定我们的距离度量上,你知道我们故意在那里搞了点花样,只是为了让它变得有趣,但你可以做任何其他你想做的事情。所以,如果你想玩弄一下这个,我绝对鼓励你这样做。我们选择 K 为 10 完全是凭空想象的,我就是编造出来的。这对不同的 K 值有什么影响?使用更高的 K 值会得到更好的结果吗?还是使用更低的 K 值?这有关系吗?

如果你真的想做一个更复杂的练习,你可以尝试将其应用到训练/测试中,实际上找到最能预测基于 KNN 的给定电影评分的 K 值。而且,你可以使用不同的距离度量,我也是凭空想象的!所以,玩一下距离度量,也许你可以使用不同的信息来源,或者以不同的方式权衡事物。这可能是一件有趣的事情。也许,流行度并不像流派信息那样重要,或者反过来也一样。看看这对你的结果有什么影响。所以,继续玩弄这些算法,玩弄代码并运行它,看看你能得到什么!如果你真的找到了一种显著的改进方法,那就和你的同学分享吧。

这就是 KNN 的实际运用!所以,这是一个非常简单的概念,但实际上它可能非常强大。所以,你看:仅仅基于流派和流行度就能找到相似的电影,没有别的。结果出奇的好!而且,我们使用了 KNN 的概念来实际使用那些最近的邻居来预测新电影的评分,这也实际上效果不错。所以,这就是 KNN 的实际运用,非常简单的技术,但通常效果相当不错!

降维和主成分分析

好了,是时候进入更高维度的世界了!我们要谈论更高维度和降维。听起来有点可怕!这里涉及到一些花哨的数学,但从概念上来说,它并不像你想象的那么难以理解。所以,让我们接下来谈谈降维和主成分分析。听起来非常戏剧化!通常当人们谈论这个时,他们谈论的是一种叫做主成分分析或 PCA 的技术,以及一种叫做奇异值分解或 SVD 的特定技术。所以 PCA 和 SVD 是本节的主题。让我们深入研究一下!

降维

那么,维度诅咒是什么?嗯,很多问题可以被认为有许多不同的维度。所以,例如,当我们在做电影推荐时,我们有各种电影的属性,每个单独的电影可以被认为是数据空间中的一个维度。

如果你有很多电影,那就有很多维度,你真的无法理解超过 3 个维度,因为这是我们成长演变的范围。你可能有一些你关心的许多不同特征的数据。你知道,在接下来的一刻,我们将看一个我们想要分类的花的例子,而且这个分类是基于花的 4 个不同的测量。这 4 个不同的特征,这 4 个不同的测量可以代表 4 个维度,再次,这是非常难以可视化的。

因此,降维技术存在是为了找到一种将更高维度信息降低到更低维度信息的方法。这不仅可以使它更容易查看和分类事物,而且还可以用于压缩数据。因此,通过保留最大方差,同时减少维度的数量,我们更紧凑地表示数据集。降维的一个非常常见的应用不仅仅是用于可视化,还用于压缩和特征提取。我们稍后会再谈一些。

降维的一个非常简单的例子可以被认为是 k 均值聚类:

所以你知道,例如,我们可能从数据集中开始有许多点,代表数据集中许多不同的维度。但是,最终,我们可以将其归纳为 K 个不同的质心,以及你到这些质心的距离。这是将数据归纳为更低维度表示的一种方法。

主成分分析

通常,当人们谈论降维时,他们谈论的是一种称为主成分分析的技术。这是一种更加复杂的技术,它涉及到一些相当复杂的数学。但是,从高层次来看,你需要知道的是它将一个更高维度的数据空间,找到该数据空间和更高维度内的平面。

这些更高维度的平面被称为超平面,并且它们由称为特征向量的东西定义。你可以取尽可能多的平面,最终在那些超平面上投影数据,那些就成为你的低维数据空间中的新轴:

你知道,除非你熟悉更高维度的数学并且之前考虑过它,否则很难理解!但是,最终,这意味着我们选择更高维度空间中的平面,仍然保留我们数据中的最大方差,并将数据投影到这些更高维度的平面上,然后将其带入更低维度的空间,好吗?

你真的不需要理解所有的数学来使用它;重要的是,这是一种非常有原则的方法,可以将数据集降低到更低维度的空间,同时仍然保留其中的方差。我们谈到了图像压缩作为这一应用的一个例子。所以你知道,如果我想要在图像中减少维度,我可以使用主成分分析将其归纳到其本质。

面部识别是另一个例子。所以,如果我有一个面部数据集,也许每张脸代表 2D 图像的第三个维度,并且我想将其归纳,SVD 和主成分分析可以是识别真正重要的特征的一种方法。因此,它可能更多地关注眼睛和嘴巴,例如,那些在保留数据集内方差方面是必要的重要特征。因此,它可以产生一些非常有趣和非常有用的结果,这些结果只是自然地从数据中出现,这有点酷!

为了使其更真实,我们将使用一个更简单的例子,使用所谓的鸢尾花数据集。这是一个包含在 scikit-learn 中的数据集。它在示例中经常被使用,其背后的想法是:鸢尾花实际上有两种不同类型的花瓣。一种叫做花瓣,就是你熟悉的花瓣,还有一种叫做萼片,它是花朵下部的一组支持性较低的花瓣。

我们可以拿一堆不同种类的鸢尾花,测量花瓣的长度和宽度,以及萼片的长度和宽度。因此,花瓣的长度和宽度,以及萼片的长度和宽度,共有 4 个不同的测量值对应于我们数据集中的 4 个不同维度。我想用这些来分类鸢尾花可能属于哪个物种。现在,PCA 将让我们在 2 个维度上可视化这个数据,而仍然保留数据集中的方差。所以,让我们看看这个方法的效果如何,并实际编写一些 Python 代码来对鸢尾花数据集进行 PCA。

这些就是降维、主成分分析和奇异值分解的概念。所有这些都是很高级的词汇,是的,这确实是一件高级的事情。你知道,我们正在以一种保留它们的方差的方式将高维空间缩减到低维空间。幸运的是,scikit-learn 使这变得非常容易,只需要 3 行代码就可以应用 PCA。所以让我们开始吧!

鸢尾花数据集的 PCA 示例

让我们将主成分分析应用于鸢尾花数据集。这是一个 4D 数据集,我们将将其降低到 2 个维度。我们将看到,即使丢弃了一半的维度,我们仍然可以保留数据集中的大部分信息。这是相当酷的东西,而且也相当简单。让我们深入研究一下,进行一些主成分分析,并解决维度的诅咒。继续打开PCA.ipynb文件。

使用 scikit-learn 实际上非常容易!再次强调,PCA 是一种降维技术。所有这些关于高维度的讨论听起来非常科幻,但为了使其更具体和真实,一个常见的应用是图像压缩。你可以将一张黑白图片看作是 3 个维度,其中宽度是 x 轴,高度是 y 轴,每个单元格都有一个 0 到 1 的亮度值,即黑色或白色,或者介于两者之间的一些值。因此,这将是 3D 数据;你有 2 个空间维度,然后还有一个亮度和强度维度。

如果你将其精炼为仅有 2 个维度,那将是一个压缩图像,如果你以一种尽可能保留图像方差的技术来做到这一点,你仍然可以重构图像,理论上损失不会太大。所以,这就是降维,精炼为一个实际的例子。

现在,我们将使用鸢尾花数据集的另一个示例,scikit-learn 包含了这个数据集。它只是一个包含各种鸢尾花测量值和该数据集中每株鸢尾花物种分类的数据集。就像我之前说的,它还包括每株鸢尾花标本的花瓣和萼片的长度和宽度测量值。因此,在花瓣的长度和宽度以及萼片的长度和宽度之间,我们的数据集中有 4 个特征数据维度。

我们希望将其精炼为我们实际可以查看和理解的内容,因为你的大脑无法很好地处理 4 个维度,但你可以很容易地在纸上查看 2 个维度。让我们继续加载:

from sklearn.datasets import load_iris 
from sklearn.decomposition import PCA 
import pylab as pl 
from itertools import cycle 

iris = load_iris() 

numSamples, numFeatures = iris.data.shape 
print numSamples 
print numFeatures 
print list(iris.target_names) 

scikit-learn 中有一个方便的load_iris()函数,它可以直接加载数据,无需额外的工作;所以你可以专注于有趣的部分。让我们来看看这个数据集是什么样子的,前面代码的输出如下:

你可以看到我们正在提取数据集的形状,也就是我们有多少数据点,即150,以及数据集有多少特征,或者说有多少维度,即4。所以,我们的数据集中有150朵鸢尾花标本,有 4 个信息维度。再次强调,这是萼片的长度和宽度,以及花瓣的长度和宽度,总共有4个特征,我们可以将其视为4个维度。

我们还可以打印出这个数据集中目标名称的列表,即分类,我们可以看到每朵鸢尾花属于三种不同的物种之一:山鸢尾、变色鸢尾或者维吉尼亚鸢尾。这就是我们要处理的数据:150 朵鸢尾花标本,分为 3 种物种之一,并且每朵鸢尾花都有 4 个特征。

让我们看看 PCA 有多容易。尽管在底层它是一个非常复杂的技术,但实际操作只需要几行代码。我们将整个鸢尾花数据集分配给 X。然后我们将创建一个 PCA 模型,并保持n_components=2,因为我们想要 2 个维度,也就是说,我们要从 4 维降到 2 维。

我们将使用whiten=True,这意味着我们将对所有数据进行归一化,确保一切都很好地可比较。通常情况下,为了获得良好的结果,你会想要这样做。然后,我们将把 PCA 模型拟合到我们的鸢尾花数据集X上。然后我们可以使用该模型将数据集转换为 2 维。让我们来运行一下。这发生得非常快!

X = iris.data 
pca = PCA(n_components=2, whiten=True).fit(X) 
X_pca = pca.transform(X) 

请思考刚才发生了什么。我们实际上创建了一个 PCA 模型,将 4 个维度降低到2,它通过选择 2 个 4D 向量来实现这一点,以创建超平面,将 4D 数据投影到 2 维。你实际上可以通过打印 PCA 的实际成分来看到这些 4D 向量,即特征向量。所以,PCA代表主成分分析,这些主成分就是我们选择来定义平面的特征向量:

print pca.components_ 

前面代码的输出如下:

你实际上可以查看这些值,它们对你来说可能没有太多意义,因为你无法真正想象 4 个维度,但我们这样做是为了让你看到它实际上正在处理主成分。所以,让我们评估一下我们的结果:

print pca.explained_variance_ratio_ 
print sum(pca.explained_variance_ratio_) 

PCA 模型给我们返回了一个叫做explained_variance_ratio的东西。基本上,这告诉你在将原始的 4D 数据降低到 2 维时,有多少方差得以保留。所以,让我们来看看:

它实际上给出了一个包含 2 个项目的列表,用于我们保留的 2 个维度。这告诉我,在第一个维度中,我实际上可以保留数据中 92%的方差,而第二个维度只给了我额外的 5%方差。如果将它们加在一起,我将数据投影到的这 2 个维度中,仍然保留了源数据中超过 97%的方差。我们可以看到,其实并不需要 4 个维度来捕捉这个数据集中的所有信息,这是非常有趣的。这是相当酷的东西!

如果你仔细想想,你觉得可能是为什么呢?也许花的整体大小与其物种中心有一定的关系。也许是花瓣和萼片的长度与宽度之比。你知道,这些东西可能会随着给定物种或给定花的整体大小一起协调地移动。因此,也许这 4 个维度之间存在 PCA 自行提取的关系。这很酷,也很强大。让我们继续可视化这一点。

将这个数据降低到 2 个维度的整个目的是为了我们能够制作一个漂亮的 2D 散点图,至少这是我们在这个小例子中的目标。因此,我们将在这里做一些 Matplotlib 的魔术。这里有一些花里胡哨的东西,我至少应该提一下。所以,我们将创建一个颜色列表:红色、绿色和蓝色。我们将创建一个目标 ID 列表,使值 0、1 和 2 映射到我们拥有的不同的鸢尾花物种。

我们将把所有这些与每个物种的实际名称一起压缩。for 循环将遍历 3 种不同的鸢尾花物种,当它这样做时,我们将有该物种的索引,与之关联的颜色,以及该物种的实际可读名称。我们将一次处理一种物种,并在我们的散点图上用给定的颜色和标签绘制该物种的散点图。然后我们将添加我们的图例并显示结果:

colors = cycle('rgb') 
target_ids = range(len(iris.target_names)) 
pl.figure() 
for i, c, label in zip(target_ids, colors, iris.target_names): 
    pl.scatter(X_pca[iris.target == i, 0], X_pca[iris.target == i, 1], 
        c=c, label=label) 
pl.legend() 
pl.show() 

以下是我们得到的结果:

这是我们将 4D 鸢尾花数据投影到 2 个维度。非常有趣!你可以看到它们仍然相当好地聚集在一起。你知道,所有的维吉尼亚人坐在一起,所有的变色鸢尾坐在中间,而山鸢尾则远在左侧。真的很难想象这些实际值代表什么。但是,重要的是,我们将 4D 数据投影到 2D,并且以这样的方式保留了方差。我们仍然可以清楚地看到这 3 个物种之间的明显区分。在其中有一些交织,不是完美的,你知道。但总的来说,它非常有效。

活动

正如你从explained_variance_ratio中回忆起的那样,我们实际上在一个维度中捕获了大部分的方差。也许花的整体大小才是真正重要的分类因素;你可以用一个特征来指定这一点。所以,如果你感觉可以的话,继续修改结果。看看你是否可以用 2 个维度或者 1 个维度来完成!所以,把n_components改成1,看看你得到什么样的方差比率。

发生了什么?这有意义吗?玩弄一下,熟悉一下。这就是降维、主成分分析和奇异值分解的全部过程。非常、非常花哨的术语,你知道,在公平的情况下,在这些术语的背后是一些相当花哨的数学。但正如你所看到的,这是一种非常强大的技术,并且在 scikit-learn 中,应用起来并不难。因此,请将其放入你的工具箱中。

就是这样!一个关于花信息的 4D 数据集被简化为我们可以轻松可视化的 2 个维度,并且仍然可以清楚地看到我们感兴趣的分类之间的区分。因此,在这个例子中,PCA 的效果非常好。再次强调,这是一个用于压缩、特征提取或面部识别等方面的有用工具。因此,请将其放入你的工具箱中。

数据仓库概述

接下来,我们将稍微谈一下数据仓库。这是一个领域,最近被 Hadoop 的出现以及一些大数据技术和云计算所颠覆。所以,有很多大的关键词,但这些概念对你来说是重要的。

让我们深入探讨这些概念!让我们谈谈 ELT 和 ETL,以及数据仓库的一般情况。这更多是一个概念,而不是一个具体的实际技术,所以我们将从概念上来谈论它。但是,在工作面试中,这可能会出现。所以,让我们确保你理解这些概念。

我们将首先谈论一般的数据仓库。什么是数据仓库?嗯,它基本上是一个包含来自许多不同来源的信息的巨大数据库,并为你将它们联系在一起。例如,也许你在一家大型电子商务公司工作,他们可能有一个订单系统,将人们购买的商品的信息输入到你的数据仓库中。

你还可以从网络服务器日志中获取信息,将其注入到数据仓库中。这将使你能够将网站上的浏览信息与人们最终购买的商品联系起来。也许你还可以将来自客户服务系统的信息联系起来,并衡量浏览行为与客户最终的满意度之间是否存在关系。

数据仓库面临着从许多不同来源获取数据的挑战,将它们转换为某种模式,使我们能够同时查询这些不同的数据来源,并通过数据分析得出见解。因此,大型公司和组织通常会有这种情况。我们正在涉及大数据的概念。你可以有一个巨大的 Oracle 数据库,例如,其中包含所有这些东西,也许以某种方式进行了分区和复制,并且具有各种复杂性。你可以通过 SQL,结构化查询语言,或者通过图形工具,比如 Tableau,来查询它,这是目前非常流行的一种工具。这就是数据分析师的工作,他们使用诸如 Tableau 之类的工具查询大型数据集。

这就是数据分析师和数据科学家之间的区别。你可能实际上正在编写代码,对数据执行更高级的技术,涉及到人工智能,而不仅仅是使用工具从数据仓库中提取图表和关系。这是一个非常复杂的问题。在亚马逊,我们有一个专门负责数据仓库的部门,全职负责这些事情,他们从来没有足够的人手,我可以告诉你;这是一项重大工作!

你知道,做数据仓库有很多挑战。其中之一是数据规范化:因此,你必须弄清楚这些不同数据来源中的所有字段实际上是如何相互关联的?我如何确保一个数据源中的列可以与另一个数据源中的列进行比较,并具有相同的数据集、相同的规模和相同的术语?我如何处理缺失数据?我如何处理损坏的数据或来自异常值、机器人等的数据?这些都是非常大的挑战。维护这些数据源也是一个非常大的问题。

当你将所有这些信息导入数据仓库时,很多问题可能会出现,特别是当你需要进行非常大的转换,将从网络日志中保存的原始数据转换为实际的结构化数据库表,然后导入到你的数据仓库中。当你处理一个庞大的数据仓库时,扩展也可能会变得棘手。最终,你的数据会变得如此庞大,以至于这些转换本身开始成为一个问题。这开始涉及到 ELT 与 ETL 的整个话题。

ETL 与 ELT

让我们首先谈谈 ETL。它是什么意思?它代表提取、转换和加载-这是做数据仓库的传统方式。

基本上,首先从你想要的操作系统中提取你想要的数据。例如,我可能每天从我们的 Web 服务器中提取所有的 Web 日志。然后,我需要将所有这些信息转换为一个实际的结构化数据库表,可以将其导入到我的数据仓库中。

这个转换阶段可能会遍历每一行 Web 服务器日志,将其转换为一个实际的表,从每个 Web 日志行中提取会话 ID、他们查看的页面、时间、引荐者等信息,并将其组织成一个表格结构,然后将其加载到数据仓库本身,作为数据库中的一个实际表。因此,随着数据变得越来越大,这个转换步骤可能会成为一个真正的问题。想想在 Google、Amazon 或任何大型网站上处理所有 Web 日志并将其转换为数据库可以摄取的内容需要多少处理工作。这本身就成为一个可扩展性挑战,并且可能会通过整个数据仓库管道引入稳定性问题。

这就是 ELT 概念的出现,并且它有点颠覆了一切。它说,“如果我们不使用一个庞大的 Oracle 实例会怎样?如果我们使用一些允许我们在 Hadoop 集群上拥有更分布式数据库的新技术,让我们利用 Hive、Spark 或 MapReduce 这些分布式数据库的能力,在加载后实际进行转换。”

这里的想法是,我们将提取我们想要的信息,就像以前一样,比如从一组 Web 服务器日志中。但是,我们将直接将其加载到我们的数据存储库中,并且我们将使用存储库本身的功能来实际进行转换。因此,这里的想法是,与其进行离线过程来转换我的 Web 日志,例如,将其作为原始文本文件导入并逐行处理,使用类似 Hadoop 的东西的功能,实际上将其转换为更结构化的格式,然后可以跨整个数据仓库解决方案进行查询。

像 Hive 这样的东西让你在 Hadoop 集群上托管一个庞大的数据库。还有像 Spark SQL 这样的东西,让你也可以以非常类似 SQL 的数据仓库方式进行查询,实际上是在 Hadoop 集群上分布的数据仓库上进行查询。还有一些分布式 NoSQL 数据存储,可以使用 Spark 和 MapReduce 进行查询。这个想法是,你不是使用单一的数据库作为数据仓库,而是使用建立在 Hadoop 或某种集群之上的东西,实际上不仅可以扩展数据的处理和查询,还可以扩展数据的转换。

再次强调,首先提取原始数据,然后将其加载到数据仓库系统本身。然后使用数据仓库的能力(可能建立在 Hadoop 上)作为第三步进行转换。然后我可以一起查询这些东西。这是一个非常庞大的项目,非常庞大的主题。你知道,数据仓库本身就是一个完整的学科。我们将很快在这本书中更多地讨论 Spark,这是处理这个问题的一种方式——特别是有一个叫做 Spark SQL 的东西是相关的。

总体概念是,如果你从建立在 Oracle 或 MySQL 上的单一数据库转移到建立在 Hadoop 之上的这些更现代的分布式数据库之一,你可以在加载原始数据后进行转换阶段。这可能会更简单、更可扩展,并且利用今天可用的大型计算集群的能力。

这是 ETL 与 ELT 的对比,云计算中的传统方式与当今有意义的方式,当我们有大规模的计算资源可用于转换大型数据集时。这就是概念。

ETL 是一种老派的做法,你在导入和加载到一个巨大的数据仓库、单片数据库之前,离线转换一堆数据。但是在今天的技术中,使用基于云的数据库、Hadoop、Hive、Spark 和 MapReduce,你实际上可以更有效地做到这一点,并利用集群的力量在将原始数据加载到数据仓库后执行转换步骤。

这真的改变了这个领域,你知道这一点很重要。再次强调,关于这个主题还有很多要学习,所以我鼓励你在这个主题上进行更多的探索。但是,这就是基本概念,现在你知道人们谈论 ETL 与 ELT 时在谈论什么了。

强化学习

我们下一个话题是一个有趣的话题:强化学习。我们可以用 Pac-Man 的例子来理解这个概念。我们实际上可以创建一个能够自己玩得很好的智能 Pac-Man 代理。你会惊讶于构建这个智能 Pac-Man 背后的技术是多么简单。让我们来看看!

因此,强化学习的理念是,你有某种代理,比如 Pac-Man,在我们的例子中,这个空间将是 Pac-Man 所在的迷宫。随着它的前进,它学会了在不同条件下不同状态变化的价值。

例如,在前面的图像中,Pac-Man 的状态可能是由它南边有一个幽灵,西边有一堵墙,北边和东边是空的空间来定义的,这可能定义了 Pac-Man 的当前状态。它可以进行的状态变化是朝特定方向移动。然后我可以学习朝某个方向前进的价值。例如,如果我向北移动,实际上不会发生什么,这并没有真正的奖励。但是,如果我向南移动,我会被幽灵摧毁,这将是一个负值。

当我去探索整个空间时,我可以建立起一组所有可能的 Pac-Man 可能处于的状态,并与在每个状态中朝特定方向移动相关联的值,这就是强化学习。随着它探索整个空间,它会为给定状态细化这些奖励值,然后可以使用存储的奖励值来选择在当前条件下做出最佳决策。除了 Pac-Man,还有一个叫做 Cat & Mouse 的游戏,这是一个常用的例子,我们稍后会看一下。

这种技术的好处在于,一旦你探索了你的代理可能处于的所有可能状态,你可以在运行不同迭代时非常快速地获得非常好的性能。所以,你可以通过运行强化学习来制作一个智能 Pac-Man,让它探索在不同状态下可以做出不同决策的价值,然后存储这些信息,以便在未知条件下看到未来状态时快速做出正确的决策。

Q-learning

因此,强化学习的一个非常具体的实现被称为 Q-learning,这更加正式地阐述了我们刚刚谈到的内容:

  • 因此,你从代理的一组环境状态开始(我旁边有幽灵吗?我前面有能量丸吗?诸如此类的事情。),我们将称之为 s。

  • 我有一组可能在这些状态下采取的行动,我们将称之为 a。在 Pac-Man 的情况下,这些可能的行动是向上、向下、向左或向右移动。

  • 然后我们有每个状态/行动对的一个值,我们将其称为 Q;这就是为什么我们称之为 Q 学习。因此,对于每个状态,Pac-Man 周围给定的一组条件,给定的行动将有一个值Q。因此,向上移动可能会有一个给定的值 Q,向下移动可能会有一个负的Q值,如果这意味着遇到幽灵,例如。

因此,我们从 Pac-Man 可能处于的每个可能状态开始,都有一个Q值为 0。随着 Pac-Man 探索迷宫,当 Pac-Man 遇到不好的事情时,我们会减少 Pac-Man 当时所处状态的Q值。因此,如果 Pac-Man 最终被幽灵吃掉,我们会惩罚他在当前状态所做的任何事情。当 Pac-Man 遇到好事时,当他吃到一个能量丸或吃掉一个幽灵时,我们将增加他所处状态的那个动作的Q值。然后,我们可以使用这些Q值来指导 Pac-Man 的未来选择,并构建一个可以表现最佳的小智能体,制作一个完美的小 Pac-Man。从我们刚才看到的 Pac-Man 的相同图像开始,我们可以通过定义他的西边有一堵墙,北边和东边有空地,南边有一个幽灵来进一步定义 Pac-Man 的当前状态。

我们可以看看他可以采取的行动:他实际上根本不能向左移动,但他可以向上、向下或向右移动,我们可以为所有这些行动分配一个值。向上或向右移动,实际上什么都不会发生,没有能量丸或点可以消耗。但如果他向左走,那肯定是一个负值。我们可以说对于由当前条件给出的状态,Pac-Man 所处的状态,向下移动将是一个非常糟糕的选择;对于那个给定状态的那些行动选择,应该有一个负的Q值。根本不能向左移动。向上或向右或保持中立,Q值对于那个给定状态的那些行动选择将保持为 0。

现在,你也可以向前看一点,使智能体变得更加智能。因此,实际上我离得到能量丸还有两步。因此,如果 Pac-Man 要探索这个状态,如果我在下一个状态吃到那个能量丸,我实际上可以将其纳入到先前状态的Q值中。如果你只有某种折扣因子,基于你在时间上有多远,你有多少步远,你可以将所有这些因素结合在一起。这是实际上在系统中建立一点记忆的方法。你可以使用折扣因子来计算 Q(这里s是先前的状态,s'是当前的状态)来向前看超过一步:

Q(s,a) += 折扣 * (奖励(s,a) + max(Q(s')) - Q(s,a))

因此,当我消耗那个能量丸时所体验到的Q值实际上可能会提升我沿途遇到的先前Q值。这是使 Q 学习变得更好的一种方法。

探索问题

在强化学习中我们面临的一个问题是探索问题。在探索阶段,我如何确保有效地覆盖所有不同的状态和这些状态中的行动?

简单的方法

一种简单的方法是始终选择具有迄今为止计算出的最高Q值的给定状态的动作,如果有平局,就随机选择。因此,最初我的所有Q值可能都是 0,我会首先随机选择动作。

当我开始获得关于给定动作和给定状态的更好Q值的信息时,我将开始在前进时使用它们。但是,这最终会变得非常低效,如果我只将自己固定在始终选择到目前为止计算出的最佳Q值的这种刚性算法中,我实际上可能会错过很多路径。

更好的方法

因此,更好的方法是在探索时引入一些随机变化到我的行动中。因此,我们称之为一个 epsilon 项。假设我们有某个值,我掷骰子,得到一个随机数。如果它小于这个 epsilon 值,我实际上不遵循最高的Q值;我不做有意义的事情,我只是随机选择一条路径来尝试一下,看看会发生什么。这实际上让我在探索阶段更有效地探索更广泛的可能性、更广泛的行动,更广泛的状态。

因此,我们刚刚做的事情可以用非常花哨的数学术语来描述,但你知道概念上它相当简单。

花哨的词

我探索一些我可以在给定状态下采取的行动,我用它来指导与给定状态相关的给定行动的奖励,探索结束后,我可以使用那些Q值的信息,来智能地穿越一个全新的迷宫,例如。

这也可以称为马尔可夫决策过程。因此,很多数据科学只是给简单的概念赋予花哨、令人生畏的名字,强化学习中也有很多这样的情况。

马尔可夫决策过程

因此,如果你查阅马尔可夫决策过程的定义,它是“一个数学框架,用于建模决策,其中结果部分是随机的,部分受决策者控制”。

  • 决策: 在给定状态的一系列可能性中,我们采取什么行动?

  • 在结果部分是随机的情况下: 嗯,有点像我们的随机探索。

  • 部分受决策者控制: 决策者是我们计算出的Q值。

因此,MDPs,马尔可夫决策过程,是一种花哨的方式来描述我们刚刚为强化学习描述的探索算法。符号甚至相似,状态仍然被描述为 s,s'是我们遇到的下一个状态。我们有被定义为P[a]的状态转移函数,对于给定的 s 和 s'。我们有我们的Q值,它们基本上被表示为一个奖励函数,对于给定的 s 和 s'有一个R[a]值。因此,从一个状态转移到另一个状态有一个与之相关的奖励,从一个状态转移到另一个状态由一个状态转移函数定义:

  • 状态仍然被描述为ss''

  • 状态转移函数被描述为Pa(s,s')

  • 我们的Q值被描述为奖励函数Ra(s,s')

因此,再次描述我们刚刚做的事情,只是用数学符号和一个更花哨的词,马尔可夫决策过程。如果你想要听起来更聪明一点,你也可以用另一个名字来称呼马尔可夫决策过程:离散时间随机控制过程。听起来很聪明!但概念本身就是我们刚刚描述的东西。

动态规划

因此,更花哨的词:动态规划也可以用来描述我们刚刚做的事情。哇!听起来像是人工智能,计算机自我编程,《终结者 2》,天网之类的东西。但不,这只是我们刚刚做的事情。如果你查阅动态规划的定义,它是一种通过将复杂问题分解为一系列更简单的子问题来解决复杂问题的方法,每个子问题只解决一次,并理想地存储它们的解决方案,使用基于内存的数据结构。

下次出现相同的子问题时,不需要重新计算其解决方案,只需查找先前计算的解决方案,从而节省计算时间,但以(希望)在存储空间上进行适度的开销:

  • 解决复杂问题的方法: 就像创造一个智能的吃豆人,这是一个相当复杂的最终结果。

  • 通过将其分解为一系列更简单的子问题:例如,对于可能出现在 Pac-Man 中的给定状态,采取的最佳行动是什么。Pac-Man 可能会发现自己处于许多不同的状态,但每个状态都代表一个更简单的子问题,在这个子问题中,我可以做出有限的选择,并且有一个正确的答案来做出最佳的移动。

  • 存储它们的解决方案:这些解决方案是我与每个可能的动作在每个状态关联的Q值。

  • 理想情况下,使用基于内存的数据结构:当然,我需要以某种方式存储这些Q值并将它们与状态关联起来,对吧?

  • 下次出现相同的子问题:下次 Pac-Man 处于我已经有一组Q值的给定状态时。

  • 而不是重新计算其解决方案,只需查找先前计算的解决方案:我已经从探索阶段得到的Q值。

  • 从而节省计算时间,以牺牲存储空间的适度开支:这正是我们刚刚用强化学习做的。

我们有一个复杂的探索阶段,找到与每个动作对应的给定状态的最佳奖励。一旦我们有了这个表格,我们就可以非常快速地使用它来使我们的 Pac-Man 在一个全新的迷宫中以最佳方式移动。因此,强化学习也是一种动态规划的形式。哇!

简而言之,你可以通过让它半随机地探索不同的移动选择来制作一个智能的 Pac-Man 代理,给定不同的条件,其中这些选择是动作,这些条件是状态。我们在进行时跟踪与每个动作或状态相关联的奖励或惩罚,我们实际上可以打折,回溯多步,如果你想让它变得更好。

然后我们存储这些Q值,我们可以使用它来指导其未来的选择。因此,我们可以进入一个全新的迷宫,并且有一个非常聪明的 Pac-Man,可以有效地避开幽灵并吃掉它们。这是一个非常简单但非常强大的概念。你也可以说你理解了一堆花哨的术语,因为它都是同一个东西。Q 学习,强化学习,马尔可夫决策过程,动态规划:都与同一个概念相关联。

我不知道,我觉得你实际上可以通过这样一个简单的技术制作出一种人工智能的 Pac-Man,这真的很酷!如果你想更详细地了解它,以下是一些示例,你可以查看其中一个实际的源代码,并且可能进行调试,Python 马尔可夫决策过程工具包pymdptoolbox.readthedocs.org/en/latest/api/mdp.html

有一个 Python 马尔可夫决策过程工具包,它用我们谈到的所有术语包装起来。有一个你可以查看的示例,一个关于猫和老鼠游戏的工作示例,类似的。实际上,还有一个你可以在线查看的 Pac-Man 示例,它更直接地与我们谈论的内容相关。请随意探索这些链接,并了解更多。

这就是强化学习。更一般地说,这是一种构建代理程序的有用技术,该代理程序可以在可能具有一组与每个状态相关联的动作的不同状态中导航。因此,我们大多数时候在迷宫游戏的背景下讨论它。但是,你可以更广泛地思考,你知道每当你需要根据一组当前条件和一组可以采取的行动来预测某物的行为时。强化学习和 Q 学习可能是一种方法。所以,请记住这一点!

总结

在本章中,我们看到了一种最简单的机器学习技术,称为 k 最近邻算法。我们还看了一个 KNN 的例子,它预测了一部电影的评分。我们分析了降维和主成分分析的概念,并看到了一个 PCA 的例子,它将 4D 数据降低到两个维度,同时保留了其方差。

接下来,我们学习了数据仓库的概念,并看到了如何在今天使用 ELT 过程而不是 ETL 更有意义。我们深入了解了强化学习的概念,并看到了它在 Pac-Man 游戏背后的应用。最后,我们看到了一些用于强化学习的花哨词汇(Q 学习,马尔可夫决策过程和动态学习)。在下一章中,我们将看到如何处理真实世界的数据。

第八章:处理真实世界的数据

在本章中,我们将讨论处理真实世界数据的挑战,以及你可能遇到的一些怪癖。本章首先讨论了偏差-方差的权衡,这是一种更有原则的谈论你可能过拟合和欠拟合数据的不同方式的方式,以及它们如何相互关联。然后我们讨论了 k 折交叉验证技术,这是你用来对抗过拟合的重要工具,并看看如何使用 Python 实现它。

接下来,我们分析了在实际应用任何算法之前清理和归一化数据的重要性。我们看了一个示例来确定网站上最受欢迎的页面,这将展示清理数据的重要性。本章还涵盖了记住归一化数值数据的重要性。最后,我们看看如何检测异常值并处理它们。

具体来说,本章涵盖以下主题:

  • 分析偏差/方差的权衡

  • k 折交叉验证的概念及其实现

  • 清理和归一化数据的重要性

  • 确定网站的热门页面的示例

  • 归一化数值数据

  • 检测异常值并处理它们

偏差/方差的权衡

在处理真实世界数据时面临的一个基本挑战是过拟合与欠拟合你的回归数据,或者你的模型,或者你的预测。当我们谈论欠拟合和过拟合时,我们经常可以在偏差和方差的背景下谈论这一点,以及偏差-方差的权衡。所以,让我们谈谈这意味着什么。

从概念上讲,偏差和方差非常简单。偏差就是你离正确值有多远,也就是说,你的预测在整体上预测正确的值有多好。如果你取所有预测的平均值,它们是否更多或更少在正确的位置上?或者你的错误是一直偏向某个方向?如果是这样,那么你的预测就有某个方向的偏差。

方差只是衡量你的预测有多分散、多散乱的一个指标。所以,如果你的预测到处都是,那就是高方差。但是,如果它们非常集中在正确的值上,甚至在高偏差的情况下也是如此,那么你的方差就很小。

让我们看一些例子。假设以下飞镖板代表我们正在做的一堆预测,我们试图预测的真实值在靶心的中心:

  • 从左上角的飞镖板开始,你可以看到我们的点都散落在中心周围。所以总体上,你知道平均误差非常接近实际情况。我们的偏差实际上非常低,因为我们的预测都在同一个正确的点周围。然而,我们的方差非常高,因为这些点散布在各个地方。所以,这是一个低偏差和高方差的例子。

  • 如果我们转移到右上角的飞镖板,我们会看到我们的点都一直偏离了正确的位置,向西北方向。所以这是我们预测中高偏差的一个例子,它们一直偏离了一定的距离。我们的方差很低,因为它们都紧密地聚集在错误的位置周围,但至少它们是紧密在一起的,所以我们在预测中是一致的。这是低方差。但是,偏差很高。所以再次,这是高偏差,低方差。

  • 在左下角的飞镖板上,你可以看到我们的预测散布在错误的平均点周围。所以,我们有很高的偏差;一切都偏向了不应该去的地方。但我们的方差也很高。所以,这在这个例子中是最糟糕的情况;我们既有高偏差又有高方差。

  • 最后,在一个完美的世界中,你会有一个像右下方飞镖板那样的例子,那里我们有低偏差,一切都集中在应该的位置,以及低方差,事物都紧密地聚集在应该的位置。所以,在一个完美的世界中,这就是你最终得到的结果。

实际上,你经常需要在偏差和方差之间做出选择。这归结为过拟合与欠拟合数据。让我们看看以下例子:

这是一种对偏差和方差的不同思考方式。所以,在左边的图表中,我们有一条直线,你可以认为相对于这些观察结果,它具有非常低的方差。所以,这条线的方差不大,也就是说,它具有低方差。但是偏差,每个单独点的误差,实际上是很高的。

现在,对比一下右边图表中过拟合的数据,我们已经努力去拟合这些观察结果。这条线具有高方差,但低偏差,因为每个单独的点都非常接近它应该在的位置。所以,这就是我们用方差换取偏差的一个例子。

最终,你不是为了只减少偏差或只减少方差,你想要减少错误。这才是真正重要的,结果表明你可以将错误表达为偏差和方差的函数:

看这个,错误等于偏差的平方加上方差。所以,这两个因素都会对总体错误产生影响,实际上偏差的影响更大。但要记住,你真正想要最小化的是错误,而不是偏差或方差特别,一个过于复杂的模型最终可能会产生高方差和低偏差,而一个过于简单的模型会产生低方差和高偏差。然而,它们最终可能都会产生类似的错误项。当你试图拟合你的数据时,你只需要找到这两个因素的正确平衡点。我们将在接下来的部分讨论一些更有原则的方法来避免过拟合。但是,我只是想传达偏差和方差的概念,因为人们确实会谈论它,你会被期望知道它的含义。

现在让我们把它与本书中一些早期的概念联系起来。例如,在 K 最近邻中,如果我们增加 K 的值,我们开始扩大我们要平均的邻域到一个更大的区域。这会减少方差,因为我们在更大的空间上平滑了事物,但它可能会增加我们的偏差,因为我们可能会选择一个更大的人口,这个人口可能与我们起始的点越来越不相关。通过在更多的邻居上平滑 KNN,我们可以减少方差,因为我们在更多的值上平滑了事物。但是,我们可能会引入偏差,因为我们引入了越来越不相关于我们起始点的点。

决策树就是另一个例子。我们知道单个决策树容易过拟合,这可能意味着它具有高方差。但是,随机森林试图通过拥有多个随机变体的树并将它们的解决方案平均在一起来换取一些偏差减少的方差,就像我们通过增加 K 值来平均 KNN 的结果一样:我们可以通过使用多个决策树来平均决策树的结果,使用随机森林类似的想法。

这就是偏差-方差折衷。你知道你必须在整体准确度和散布程度或紧密聚集程度之间做出决定。这就是偏差-方差折衷,它们都对总体错误产生影响,而你真正关心的是最小化这个错误。所以,记住这些术语!

K 折交叉验证以避免过拟合

在本书的前面,我们谈到了训练和测试作为防止过拟合并实际测量模型在从未见过的数据上的表现的好方法。我们可以通过一种称为 k 折交叉验证的技术将其提升到一个新的水平。因此,让我们谈谈这个强大的工具,用于对抗过拟合;k 折交叉验证,并了解它的工作原理。

回想一下训练/测试,其思想是我们将构建机器学习模型的所有数据分成两部分:一个训练数据集和一个测试数据集。我们只使用训练数据集来训练模型,然后使用我们保留的测试数据集来评估其性能。这样可以防止我们对已有数据过拟合,因为我们正在测试模型对其从未见过的数据的表现。

然而,训练/测试仍然有其局限性:你仍然可能会对特定的训练/测试分割过拟合。也许你的训练数据集并不真正代表整个数据集,太多的东西最终进入了你的训练数据集,导致了偏差。这就是 k 折交叉验证发挥作用的地方,它将训练/测试提升到一个新的水平。

尽管听起来很复杂,但其实思想相当简单:

  1. 我们将数据分成 K 个桶,而不是两个桶,一个用于训练,一个用于测试。

  2. 我们保留其中一个桶用于测试目的,用于评估我们模型的结果。

  3. 我们对剩下的桶(K-1)进行模型训练,然后我们拿出我们的测试数据集,用它来评估我们的模型在所有这些不同的训练数据集中的表现如何。

  4. 我们将这些结果的误差指标(即 R 平方值)进行平均,得到 k 折交叉验证的最终误差指标。

就是这样。这是一种更健壮的训练/测试方法,这是一种方法。

现在,你可能会想,如果我对我保留的那个测试数据集过拟合了怎么办?我仍然对每一个训练数据集使用相同的测试数据集。如果那个测试数据集也不真正代表实际情况呢?

还有一些 k 折交叉验证的变体,也会对此进行随机化。因此,你也可以每次随机选择训练数据集,并将不同的数据随机分配到不同的桶中进行测量。但通常,当人们谈论 k 折交叉验证时,他们指的是这种特定的技术,其中你保留一个桶用于测试,其余桶用于训练,并在构建每个模型时使用测试数据集评估所有训练数据集。

使用 scikit-learn 进行 k 折交叉验证的示例

幸运的是,scikit-learn 使这变得非常容易,甚至比普通的训练/测试更容易!进行 k 折交叉验证非常简单,所以你可能会选择这样做。

现在,在实践中,这一切是如何运作的是,你会有一个你想要调整的模型,以及该模型的不同变体,你可能想要对其进行微调的不同参数,对吧?

比如,多项式拟合的多项式程度。因此,想法是尝试模型的不同值,不同的变体,使用 k 折交叉验证对它们进行测量,并找到最小化与测试数据集的误差的值。这就是你的最佳选择。在实践中,你想使用 k 折交叉验证来衡量模型对测试数据集的准确性,并不断完善模型,尝试其中的不同值,尝试模型的不同变体,甚至可能是完全不同的模型,直到找到最大程度减少误差的技术,使用 k 折交叉验证。

让我们来看一个例子,看看它是如何工作的。我们将再次将其应用于我们的鸢尾花数据集,重新审视 SVC,并且我们将使用 k-fold 交叉验证来尝试一下,看看它是多么简单。实际上,让我们将 k-fold 交叉验证和训练/测试应用到实践中,使用一些真正的 Python 代码。你会发现它实际上非常容易使用,这是一件好事,因为这是一种你应该使用来衡量监督学习模型准确性和有效性的技术。

请继续打开KFoldCrossValidation.ipynb,如果愿意的话可以跟着做。我们将再次看看鸢尾花数据集;还记得我们在谈论降维时介绍过它吗?

为了让你记起来,鸢尾花数据集包含了 150 个鸢尾花的测量数据,每朵花都有其花瓣和萼片的长度和宽度。我们还知道每朵花属于 3 种不同的鸢尾花中的哪一种。这里的挑战是创建一个能够成功预测鸢尾花种类的模型,仅仅基于其花瓣和萼片的长度和宽度。所以,让我们继续做这件事。

我们将使用 SVC 模型。如果你还记得,这只是一种对数据进行分类的相当强大的方法。如果需要,可以查看相关部分来复习一下:

import numpy as np 
from sklearn import cross_validation 
from sklearn import datasets 
from sklearn import svm 

iris = datasets.load_iris() 

# Split the iris data into train/test data sets with 
#40% reserved for testing 
X_train, X_test, y_train, y_test = cross_validation.train_test_split(iris.data, 
                                    iris.target, test_size=0.4, random_state=0) 

# Build an SVC model for predicting iris classifications 
#using training data 
clf = svm.SVC(kernel='linear', C=1).fit(X_train, y_train) 

# Now measure its performance with the test data 
clf.score(X_test, y_test) 

我们使用 scikit-learn 中的cross_validation库,首先进行传统的训练测试分割,只是一个单一的训练/测试分割,看看它的效果如何。

为此,我们有一个train_test_split()函数,使得这变得相当容易。这样的工作方式是,我们将一组特征数据输入到train_test_split()中。iris.data只包含每朵花的实际测量数据。iris.target基本上是我们要预测的东西。

在这种情况下,它包含了每朵花的所有种类。test_size表示我们想要训练与测试的百分比。因此,0.4 表示我们将随机提取 40%的数据进行测试,并使用 60%进行训练。这给我们带来的是 4 个数据集,基本上是一个用于训练的数据集和一个用于测试的数据集,分别用于特征数据和目标数据。因此,X_train最终包含了我们鸢尾花测量的 60%,而X_test包含了用于测试我们模型结果的测量的 40%。y_trainy_test包含了每个部分的实际种类。

然后我们继续构建一个 SVC 模型,用于预测鸢尾花的种类,只使用训练数据。我们使用线性核来拟合这个 SVC 模型,只使用训练的特征数据和训练的种类数据,也就是目标数据。我们将该模型称为clf。然后,我们在clf上调用score()函数,只是为了衡量它在我们的测试数据集上的表现。因此,我们将这个模型与我们为鸢尾花测量保留的测试数据集以及测试鸢尾花种类进行比分,看看它的表现如何:

结果表明它表现得非常好!超过 96%的时间,我们的模型能够基于那些鸢尾花的测量结果,准确预测出它们的种类,即使是它之前从未见过的鸢尾花。所以这很酷!

但是,这是一个相当小的数据集,大约有 150 朵花,如果我没记错的话。因此,我们只使用 150 朵花的 60%进行训练,只使用 150 朵花的 40%进行测试。这些数字仍然相当小,所以我们仍然可能会过度拟合我们所做的特定训练/测试分割。因此,让我们使用 k-fold 交叉验证来防止这种情况发生。事实证明,使用 k-fold 交叉验证,即使它是一种更强大的技术,实际上比训练/测试更容易使用。所以,这很酷!那么,让我们看看它是如何工作的:

# We give cross_val_score a model, the entire data set and its "real" values, and the number of folds: 
scores = cross_validation.cross_val_score(clf, iris.data, iris.target, cv=5) 

# Print the accuracy for each fold: 
print scores 

# And the mean accuracy of all 5 folds: 
print scores.mean() 

我们已经有了一个模型,即我们为这个预测定义的 SVC 模型,你所需要做的就是在cross_validation包上调用cross_val_score()。因此,您需要向这个函数传递一个给定类型的模型(clf),您拥有的所有测量数据集,也就是所有的特征数据(iris.data)和所有的目标数据(所有的物种),iris.target

我想要 cv=5,这意味着它实际上会使用 5 个不同的训练数据集,同时保留 1 用于测试。基本上,它会运行 5 次,这就是我们需要做的全部。这将自动评估我们的模型针对整个数据集,分成五种不同的方式,并将结果返回给我们。

如果我们打印出来,它会给我们返回一个实际错误指标的列表,即每个迭代的错误指标,也就是每个折叠的错误指标。我们可以将这些平均起来,得到基于 k 折交叉验证的总体错误指标:

当我们在 5 个折叠上进行时,我们会发现我们的结果甚至比我们想象的要好!98%的准确率。这非常棒!事实上,在几次运行中我们都获得了完美的准确率。这真是令人惊讶的事情。

现在让我们看看是否可以做得更好。我们之前使用了线性核,如果我们使用多项式核并变得更加花哨会怎样呢?那会是过拟合还是实际上更好地拟合了我们的数据?这取决于这些花瓣测量和实际物种之间是否实际上存在线性关系或多项式关系。所以,让我们试一试:

clf = svm.SVC(kernel='poly', C=1).fit(X_train, y_train)
scores = cross_validation.cross_val_score(clf, iris.data, iris.target, cv=5)
print scores
print scores.mean()

我们将再次运行所有这些,使用相同的技术。但这次,我们使用多项式核。我们将将其拟合到我们的训练数据集上,而在这种情况下,拟合到哪里并不重要,因为cross_val_score()会为您不断重新运行它:

事实证明,当我们使用多项式拟合时,最终得分甚至比我们原始运行的得分还要低。这告诉我们多项式核可能是过拟合的。当我们使用 k 折交叉验证时,它显示出的得分比线性核还要低。

这里的重要一点是,如果我们只使用了单一的训练/测试拆分,我们就不会意识到我们过拟合了。如果我们只是在这里进行了单一的训练/测试拆分,我们实际上会得到与线性核相同的结果。因此,我们可能会无意中过拟合我们的数据,并且甚至不知道我们没有使用 k 折交叉验证时。因此,这是 k 折交叉验证拯救的一个很好的例子,并警告您过拟合,而单一的训练/测试拆分可能无法发现。因此,请将其放入您的工具箱。

如果您想进一步尝试,可以尝试不同的次数。因此,您实际上可以指定不同的次数。多项式核的默认次数是 3 次,但您可以尝试不同的次数,可以尝试两次。

这样做会更好吗?如果你降到一次,基本上就会退化为线性核,对吧?所以,也许仍然存在多项式关系,也许只是二次多项式。试一试,看看你得到什么。这就是 k 折交叉验证。正如你所看到的,由于 scikit-learn 的便利性,它非常容易使用。这是衡量模型质量的重要方式。

数据清洗和归一化

现在,这是最简单的部分之一,但它可能是整本书中最重要的部分。我们将讨论清理输入数据,这将占用您大部分的时间。

您清理输入数据的程度以及了解原始输入数据将对您的结果质量产生巨大影响 - 甚至可能比您选择的模型或调整模型的效果更大。所以,请注意;这很重要!

清理原始输入数据通常是数据科学家工作中最重要且耗时的部分!

让我们谈谈数据科学的一个不便之真相,那就是你实际上大部分时间都在清理和准备数据,而相对较少的时间用于分析和尝试新的算法。这并不像人们经常说的那样光彩夺目。但是,这是一个非常重要的事情需要注意。

原始数据中可能会有很多不同的问题。送到你手上的原始数据会非常肮脏,以许多不同的方式被污染。如果你不处理它,它将会扭曲你的结果,并最终导致你的业务做出错误的决定。

如果最终发现你犯了一个错误,即摄入了大量错误数据却没有考虑清理它,然后基于这些结果告诉你的业务做一些后来被证明完全错误的事情,你将会陷入麻烦!所以,请注意!

有很多不同类型的问题和数据需要注意:

  • 异常值:也许你的数据中有一些行为看起来有点奇怪,当你深入挖掘时,发现这些数据根本不应该被看到。一个很好的例子是,如果你在查看网络日志数据时,发现一个会话 ID 一次又一次地重复出现,并且以一个人类无法做到的速度进行某些操作。你可能看到的是一个机器人,一个在某处运行的脚本实际上在抓取你的网站。甚至可能是某种恶意攻击。但无论如何,你不希望这些行为数据影响你的模型,这些模型旨在预测真正使用你的网站的人类的行为。因此,观察异常值是一种识别在构建模型时可能需要剔除的数据类型的方法。

  • 缺失数据:当数据不在那里时,你该怎么办?回到网络日志的例子,那一行可能有一个引荐者,也可能没有。如果没有怎么办?你是创建一个新的分类来表示缺失,还是完全丢弃那一行?你必须考虑在那里做什么才是正确的。

  • 恶意数据:可能有人试图操纵你的系统,可能有人试图欺骗系统,你不希望这些人得逞。比如说你正在制作一个推荐系统。可能有人试图捏造行为数据以推广他们的新项目,对吧?因此,你需要警惕这种情况,并确保你能识别出操纵攻击或其他类型的攻击,过滤掉它们的结果,不让它们得逞。

  • 错误数据:如果在某个系统中有软件错误,导致在某些情况下写入了错误的值,该怎么办?这种情况可能发生。不幸的是,你无法知道这一点。但是,如果你看到的数据看起来可疑,或者结果对你来说毫无意义,深入挖掘有时可以发现潜在的错误,导致错误数据首先被写入。也许在某个地方没有正确地组合事物。也许会话没有在整个会话期间保持。例如,人们可能在浏览网站时丢失他们的会话 ID,并获得新的会话 ID。

  • 无关数据:这里有一个非常简单的例子。也许你只对来自纽约市的人的数据感兴趣,或者出于某种原因。在这种情况下,来自世界其他地方的人的所有数据对于你想要找出的内容都是无关的。你首先要做的就是抛弃所有这些数据,并限制你的数据,将其减少到你真正关心的数据。

  • 不一致的数据:这是一个巨大的问题。例如,在地址中,人们可以用许多不同的方式写相同的地址:他们可能缩写街道,也可能不缩写街道,他们可能根本不在街道名称后面加上“街”。他们可能以不同的方式组合行,可能拼写不同的东西,可能在美国使用邮政编码或美国的邮政编码加 4 位,可能在上面有一个国家,也可能没有国家。你需要想办法弄清楚你看到的变化是什么,以及如何将它们全部规范化在一起。

  • 也许我在研究有关电影的数据。一部电影在不同国家可能有不同的名称,或者一本书在不同国家可能有不同的名称,但它们意思相同。因此,你需要注意这些地方,需要对数据进行规范化处理,同样的数据可能以许多不同的方式表示,你需要将它们组合在一起以获得正确的结果。

  • 格式化:这也可能是一个问题;事物可能格式不一致。以日期为例:在美国,我们总是按月、日、年(MM/DD/YY)的顺序,但在其他国家,他们可能按日、月、年(DD/MM/YY)的顺序,谁知道呢。你需要注意这些格式上的差异。也许电话号码的区号周围有括号,也许没有;也许数字的每个部分之间有破折号,也许没有;也许社会保障号码有破折号,也许没有。这些都是你需要注意的事情,你需要确保格式上的变化不会在处理过程中被视为不同的实体或不同的分类。

因此,有很多需要注意的事情,前面的列表只是需要注意的主要事项。记住:垃圾进,垃圾出。你的模型只有你给它的数据那么好,这是极其真实的!如果你给它大量干净的数据,甚至一个非常简单的模型也可以表现得非常好,而且实际上可能会胜过一个更复杂的模型在一个更脏的数据集上。

因此,确保你有足够的数据和高质量的数据通常是大部分工作。你会惊讶于现实世界中一些最成功的算法有多简单。它们之所以成功,仅仅是因为输入的数据质量和数量。你并不总是需要花哨的技术来获得好的结果。通常情况下,你的数据的质量和数量同其他任何因素一样重要。

始终质疑你的结果!你不希望在得到不喜欢的结果时才回头查看你的输入数据中的异常。这将在你的结果中引入一种无意识的偏见,你让你喜欢或期望的结果不经质疑地通过了,对吧?你需要一直质疑事物,以确保你一直留意这些事情,因为即使你找到了一个你喜欢的结果,如果结果是错误的,它仍然是错误的,它仍然会让你的公司朝错误的方向发展。这可能会在以后给你带来麻烦。

举个例子,我有一个名为 No-Hate News 的网站。这是一个非营利性网站,所以我并不是在告诉你它来赚钱。假设我只想找到我拥有的这个网站上最受欢迎的页面。这听起来是一个相当简单的问题,不是吗?我应该只需要浏览我的网络日志,计算每个页面的点击次数,并对它们进行排序,对吧?有多难呢?嗯,事实证明这真的很难!所以,让我们深入探讨这个例子,看看为什么它很困难,并看看一些必须进行的真实世界数据清理的例子。

清理网络日志数据

我们将展示清理数据的重要性。我有一些来自我拥有的小网站的网络日志数据。我们只是尝试找到该网站上最受欢迎的页面。听起来很简单,但正如您将看到的,实际上相当具有挑战性!所以,如果您想跟着做,TopPages.ipynb是我们在这里工作的笔记本。让我们开始吧!

我实际上有一个从我的实际网站中获取的访问日志。这是 Apache 的真实 HTTP 访问日志,包含在您的书籍材料中。所以,如果您想参与其中,请确保更新路径,将访问日志移动到您保存书籍材料的位置:

logPath = "E:\\sundog-consult\\Packt\\DataScience\\access_log.txt" 

在网络日志上应用正则表达式

所以,我去网上找了下面的一小段代码,它可以将 Apache 访问日志行解析成一堆字段:

format_pat= re.compile( 
    r"(?P<host>[\d\.]+)\s" 
    r"(?P<identity>\S*)\s" 
    r"(?P<user>\S*)\s" 
    r"\[(?P<time>.*?)\]\s" 
    r'"(?P<request>.*?)"\s' 
    r"(?P<status>\d+)\s" 
    r"(?P<bytes>\S*)\s" 
    r'"(?P<referer>.*?)"\s' 
    r'"(?P<user_agent>.*?)"\s*' 
) 

这段代码包含主机、用户、时间、实际页面请求、状态、引用者、user_agent(表示用于查看此页面的浏览器)。它构建了一个称为正则表达式的东西,我们使用re库来使用它。这基本上是一种在大字符串上进行模式匹配的非常强大的语言。因此,我们可以将这个正则表达式应用到我们访问日志的每一行上,并自动将访问日志行中的信息部分分组到这些不同的字段中。让我们继续运行这个。

在这里要做的明显的事情是,让我们编写一个小脚本,计算我们遇到的每个 URL 被请求的次数,并记录它被请求的次数。然后我们可以对列表进行排序,得到我们的热门页面,对吧?听起来足够简单!

因此,我们将构建一个名为URLCounts的小 Python 字典。我们将打开我们的日志文件,对于每一行,我们将应用我们的正则表达式。如果它实际上返回了成功匹配我们试图匹配的模式,我们会说,好的,这看起来像是我们访问日志中的一个不错的行。

让我们从中提取请求字段,也就是浏览器实际请求的实际 HTTP 请求的页面。我们将把它分成三个部分:它包括一个动作,比如 get 或 post;实际请求的 URL;以及使用的协议。在得到这些信息后,我们可以看看该 URL 是否已经存在于我的字典中。如果是,我将增加该 URL 已经被遇到的次数1;否则,我将为该 URL 引入一个新的字典条目,并将其初始化为值1。我对日志中的每一行都这样做,以数字逆序排序结果,并将其打印出来:

URLCounts = {}
with open(logPath, "r") as f:
    for line in (l.rstrip() for l in f):
        match= format_pat.match(line)
        if match:
            access = match.groupdict()
            request = access['request']
            (action, URL, protocol) = request.split()
            if URLCounts.has_key(URL):
                URLCounts[URL] = URLCounts[URL] + 1
            else:
                URLCounts[URL] = 1
results = sorted(URLCounts, key=lambda i: int(URLCounts[i]), reverse=True)

for result in results[:20]:
    print result + ": " + str(URLCounts[result])

所以,让我们继续运行:

哎呀!我们遇到了一个大错误。它告诉我们,我们需要多于1个值来解包。所以显然,我们得到了一些不包含动作、URL 和协议的请求字段,而包含其他内容。

让我们看看那里发生了什么!所以,如果我们打印出所有不包含三个项目的请求,我们就会看到实际显示的内容。所以,我们要做的是一个类似的小代码片段,但我们要在请求字段上实际执行拆分,并打印出我们没有得到预期的三个字段的情况。

URLCounts = {}

with open(logPath, "r") as f:
    for line in (l.rstrip() for l in f):
        match= format_pat.match(line)
        if match:
            access = match.groupdict()
            request = access['request']
            fields = request.split()
            if (len(fields) != 3):
                print fields

让我们看看实际上有什么:

所以,我们有一堆空字段。这是我们的第一个问题。但是,然后我们有第一个字段是完全垃圾。谁知道那是从哪里来的,但显然是错误的数据。好吧,让我们修改我们的脚本。

修改一 - 过滤请求字段

我们实际上会丢弃任何没有预期的 3 个字段的行。这似乎是一个合理的做法,因为事实上这确实包含了完全无用的数据,这样做并不会让我们错过任何东西。所以,我们将修改我们的脚本来做到这一点。我们在实际尝试处理之前引入了一个if (len(fields) == 3)行。我们将运行它:

URLCounts = {}

with open(logPath, "r") as f:
    for line in (l.rstrip() for l in f):
        match= format_pat.match(line)
        if match:
            access = match.groupdict()
            request = access['request']
            fields = request.split()
            if (len(fields) == 3):
                URL = fields[1]
                if URLCounts.has_key(URL):
                    URLCounts[URL] = URLCounts[URL] + 1
                else:
                    URLCounts[URL] = 1

results = sorted(URLCounts, key=lambda i: int(URLCounts[i]), reverse=True)

for result in results[:20]:
    print result + ": " + str(URLCounts[result])

嘿,我们得到了一个结果!

但这看起来并不像是我网站上的热门页面。记住,这是一个新闻网站。所以,我们得到了一堆 PHP 文件点击,那是 Perl 脚本。那是怎么回事?我们的最佳结果是这个xmlrpc.php脚本,然后是WP_login.php,然后是主页。所以,没有什么用。然后是robots.txt,然后是一堆 XML 文件。

你知道,当我后来调查这个问题时,结果发现我的网站实际上受到了恶意攻击;有人试图侵入。这个xmlrpc.php脚本是他们试图猜测我的密码的方式,他们试图使用登录脚本登录。幸运的是,在他们真正进入这个网站之前,我就把他们关掉了。

这是一个恶意数据被引入到我的数据流中,我必须过滤掉的例子。所以,通过观察,我们可以看到这次恶意攻击不仅查看了 PHP 文件,而且还试图执行一些东西。它不仅仅是一个 get 请求,它是对脚本的 post 请求,实际上试图在我的网站上执行代码。

修改二 - 过滤 post 请求

现在,我知道我关心的数据,你知道我试图弄清楚的事情的精神是,人们从我的网站获取网页。所以,我可以合理地做的一件事是,过滤掉这些日志中不是 get 请求的任何内容。所以,让我们接着做这个。我们将再次检查我们的请求字段中是否有三个字段,然后我们还将检查操作是否是 get。如果不是,我们将完全忽略该行:

URLCounts = {}

with open(logPath, "r") as f:
    for line in (l.rstrip() for l in f):
        match= format_pat.match(line)
        if match:
            access = match.groupdict()
            request = access['request']
            fields = request.split()
            if (len(fields) == 3):
                (action, URL, protocol) = fields
                if (action == 'GET'):
                    if URLCounts.has_key(URL):
                        URLCounts[URL] = URLCounts[URL] + 1
                    else:
                        URLCounts[URL] = 1

results = sorted(URLCounts, key=lambda i: int(URLCounts[i]), reverse=True)

for result in results[:20]:
    print result + ": " + str(URLCounts[result])

现在我们应该更接近我们想要的东西了,以下是前面代码的输出:

是的,这开始看起来更合理了。但是,它仍然没有真正通过合理性检查。这是一个新闻网站;人们去那里是为了阅读新闻。他们真的在看我那个只有几篇文章的小博客吗?我不这么认为!这似乎有点可疑。所以,让我们深入一点,看看到底是谁在看那些博客页面。如果你真的去查看那个文件并手动检查,你会发现很多这些博客请求实际上根本没有任何用户代理。它们只有一个用户代理是-,这是非常不寻常的:

如果一个真正的人类和一个真正的浏览器试图获取这个页面,它会显示类似 Mozilla、Internet Explorer、Chrome 或其他类似的东西。所以,看起来这些请求来自某种刮取器。同样,可能是一种恶意流量,没有标识出是谁。

修改三 - 检查用户代理

也许,我们应该也看看用户代理,看看这些是不是真正的人在发出请求。让我们继续打印出我们遇到的所有不同的用户代理。所以,按照实际总结我们看到的不同 URL 的代码精神,我们可以查看我们看到的所有不同用户代理,并按照日志中最流行的user_agent字符串对它们进行排序:

UserAgents = {}

with open(logPath, "r") as f:
    for line in (l.rstrip() for l in f):
        match= format_pat.match(line)
        if match:
            access = match.groupdict()
            agent = access['user_agent']
            if UserAgents.has_key(agent):
                UserAgents[agent] = UserAgents[agent] + 1
            else:
                UserAgents[agent] = 1

results = sorted(UserAgents, key=lambda i: int(UserAgents[i]), reverse=True)

for result in results:
    print result + ": " + str(UserAgents[result])

我们得到以下结果:

你可以看到大部分看起来都是合法的。所以,如果是一个刮取器,而在这种情况下实际上是一次恶意攻击,但他们实际上是在假装成一个合法的浏览器。但这个破折号user_agent也经常出现。所以,我不知道那是什么,但我知道那不是一个真正的浏览器。

我看到的另一件事是有很多来自蜘蛛、网络爬虫的流量。所以,有百度,这是中国的搜索引擎,有 Googlebot 在爬网页。我想我也在那里看到了 Yandex,一个俄罗斯的搜索引擎。所以,我们的数据被很多只是为了挖掘我们网站的搜索引擎目的而爬行的爬虫所污染。再次强调,这些流量不应计入我分析的预期目的,即查看实际人类在我的网站上查看的页面。这些都是自动脚本。

过滤蜘蛛/机器人的活动

好了,这变得有点棘手。仅仅根据用户字符串来识别蜘蛛或机器人没有真正好的方法。但我们至少可以试一试,过滤掉任何包含“bot”这个词的东西,或者来自我的缓存插件的可能提前请求页面的东西。我们还将去除我们的朋友单破折号。所以,我们将再次完善我们的脚本,除了其他一切,还要去除任何看起来可疑的 UserAgents:

URLCounts = {}

with open(logPath, "r") as f:
    for line in (l.rstrip() for l in f):
        match= format_pat.match(line)
        if match:
            access = match.groupdict()
            agent = access['user_agent']
            if (not('bot' in agent or 'spider' in agent or 
                    'Bot' in agent or 'Spider' in agent or
                    'W3 Total Cache' in agent or agent =='-')):
                request = access['request']
                fields = request.split()
                if (len(fields) == 3):
                    (action, URL, protocol) = fields
                    if (action == 'GET'):
                        if URLCounts.has_key(URL):
                            URLCounts[URL] = URLCounts[URL] + 1
                        else:
                            URLCounts[URL] = 1

results = sorted(URLCounts, key=lambda i: int(URLCounts[i]), reverse=True)

for result in results[:20]:
    print result + ": " + str(URLCounts[result])

URLCounts = {}

with open(logPath, "r") as f:
    for line in (l.rstrip() for l in f):
        match= format_pat.match(line)
        if match:
            access = match.groupdict()
            agent = access['user_agent']
            if (not('bot' in agent or 'spider' in agent or 
                    'Bot' in agent or 'Spider' in agent or
                    'W3 Total Cache' in agent or agent =='-')):
                request = access['request']
                fields = request.split()
                if (len(fields) == 3):
                    (action, URL, protocol) = fields
                    if (URL.endswith("/")):
                        if (action == 'GET'):
                            if URLCounts.has_key(URL):
                                URLCounts[URL] = URLCounts[URL] + 1
                            else:
                                URLCounts[URL] = 1

results = sorted(URLCounts, key=lambda i: int(URLCounts[i]), reverse=True)

for result in results[:20]:
    print result + ": " + str(URLCounts[result])

我们得到了什么?

好了,我们开始了!前两个条目看起来更合理了,主页最受欢迎,这是预料之中的。奥兰多头条也很受欢迎,因为我比其他人更多地使用这个网站,而且我住在奥兰多。但之后,我们得到了一堆根本不是网页的东西:一堆脚本,一堆 CSS 文件。这些都不是网页。

修改四 - 应用特定于网站的过滤器

我只需应用一些关于我的网站的知识,我碰巧知道我的网站上所有合法的页面都以它们的 URL 结尾斜杠。所以,让我们继续修改一下,去掉任何不以斜杠结尾的东西:

URLCounts = {}

with open (logPath, "r") as f:
    for line in (l.rstrip() for 1 in f):
        match= format_pat.match(line)
        if match:
            access = match.groupdict()
            agent = access['user_agent']
            if (not('bot' in agent or 'spider' in agent or
                    'Bot' in agent or 'Spider' in agent or
                    'W3 Total Cache' in agent or agent =='-')):
                request = access['request']
                fields = request.split()
                if (len(fields) == 3):
                    (action, URL, protocol) = fields
                    if (URL.endswith("/")):
                        if (action == 'GET'):
                            if URLCounts.has_key(URL):
                                URLCounts[URL] = URLCounts[URL] + 1
                            else:
                                URLCounts[URL] = 1

results = sorted(URLCounts, key=lambda i: int(URLCounts[i]), reverse=True)

for result in results[:20]:
    print result + ": " + str(URLCounts[result])

让我们运行一下!

最后,我们得到了一些看起来合理的结果!看起来,从我小小的 No-Hate News 网站上实际人类请求的顶级页面是主页,然后是orlando-headlines,然后是世界新闻,然后是漫画,然后是天气,然后是关于页面。所以,这开始看起来更合理了。

如果你再深入一点,你会发现这个分析还存在问题。例如,那些 feed 页面仍然来自只是想从我的网站获取 RSS 数据的机器人。所以,这是一个很好的寓言,说明一个看似简单的分析需要大量的预处理和清理源数据,才能得到任何有意义的结果。

再次确保你在清理数据时所做的事情是有原则的,而不是只是挑选与你先入为主观念不符的问题。所以,始终质疑你的结果,始终查看你的源数据,并寻找其中的奇怪之处。

网络日志数据的活动

好了,如果你想再深入研究一下,你可以解决那个 feed 问题。继续去除包括 feed 的东西,因为我们知道那不是一个真正的网页,只是为了熟悉代码。或者,更仔细地查看日志,了解那些 feed 页面实际来自哪里。

也许有一种更好、更健壮的方法来识别那些流量作为一个更大的类别。所以,随意尝试一下。但我希望你已经学到了教训:数据清理 - 非常重要,而且会花费你大量的时间!

所以,令人惊讶的是,要在一个简单的问题上获得一些合理的结果,比如“我的网站上哪些页面被浏览次数最多?”竟然是多么困难。你可以想象,如果为了解决这样一个简单的问题需要做这么多工作,那么想想脏数据可能会如何影响更复杂问题和复杂算法的结果。

非常重要的是要了解你的数据源,查看它,查看它的代表样本,确保你了解数据输入系统。始终质疑你的结果,并将其与原始数据源联系起来,看看可疑的结果是从哪里来的。

数值数据的标准化

这是一个非常快速的部分:我只是想提醒你关于标准化数据的重要性,确保你的各种输入特征数据在同一尺度上,并且是可比较的。有时很重要,有时不重要。但是,你必须意识到什么时候重要。只要记住这一点,因为有时如果你不这样做,它会影响你的结果的质量。

有时候模型将基于几个不同的数值属性。如果你记得多变量模型,我们可能有不同的汽车属性,它们可能不是直接可比较的测量。或者,例如,如果我们正在研究年龄和收入之间的关系,年龄可能从 0 到 100 不等,但以美元计的收入可能从 0 到数十亿不等,根据货币的不同,范围可能更大!有些模型可以接受这种情况。

如果你在做回归,通常这不是什么大问题。但是,其他模型在这些值被缩放到一个公共尺度之前表现得不那么好。如果你不小心,你可能会发现一些属性比其他属性更重要。也许收入最终会比年龄更重要,如果你试图将这两个值作为模型中可比较的值来处理的话。

这也可能导致属性的偏差,这也可能是一个问题。也许你的数据集中的一组数据是倾斜的,你知道,有时你需要对事物进行标准化,而不仅仅是将其标准化到 0 到最大值的范围。没有固定的规则来决定何时应该做这种标准化。我只能说的是,无论你使用什么技术,都要始终阅读文档。

例如,在 scikit-learn 中,他们的 PCA 实现有一个whiten选项,它会自动为你标准化你的数据。你应该使用它。它还有一些预处理模块可用,可以自动为你标准化和缩放事物。

还要注意文本数据实际上应该以数字或顺序方式表示。如果你有yesno的数据,你可能需要将其转换为10,并以一致的方式进行转换。所以再次,只需阅读文档。大多数技术在使用原始、未标准化的数据时都能很好地工作,但在第一次使用新技术之前,只需阅读文档,了解输入是否应该首先进行缩放、标准化或白化。如果是这样,scikit-learn 可能会让你很容易地做到,你只需要记得这样做!在完成后不要忘记重新缩放你的结果,如果你正在缩放输入数据的话。

如果你想要解释你得到的结果,有时你需要在完成后将它们重新缩放到原始范围。如果你在输入模型之前缩放事物,甚至可能使它们倾向于某个特定数量,确保在向某人呈现这些结果之前,你将它们重新缩放和去偏。否则它们就毫无意义了!还有一个小提醒,一个寓言,你应该始终检查是否应该在将数据传递到给定模型之前对其进行标准化或白化。

本节与运动无关;这只是我想让你记住的事情。我只是想强调一下。有些算法需要白化或标准化,有些则不需要。所以,请务必阅读文档!如果您确实需要对输入算法的数据进行标准化,它通常会告诉您如何做,而且会使这一过程变得非常容易。请注意这一点!

检测异常值

真实世界数据的一个常见问题是异常值。您总会有一些奇怪的用户,或者一些奇怪的代理,它们会污染您的数据,表现出与典型用户不同的异常和非典型行为。它们可能是合法的异常值;它们可能是由真实人员而不是某种恶意流量或虚假数据引起的。因此,有时候适当地将它们移除,有时候则不适当。确保您负责任地做出这个决定。因此,让我们深入一些处理异常值的示例。

例如,如果我正在进行协同过滤,并且试图进行电影推荐之类的事情,您可能会有一些超级用户,他们观看了每部电影,并对每部电影进行了评分。他们可能对每个人的推荐产生了不成比例的影响。

您真的不希望少数人在您的系统中拥有如此大的权力。因此,这可能是一个例子,您可以合理地过滤掉异常值,并通过他们实际放入系统的评分数量来识别它们。或者,异常值可能是那些没有足够评分的人。

我们可能正在查看网络日志数据,就像我们在之前的示例中看到的那样,当我们进行数据清理时,异常值可能会告诉您,从一开始您的数据就存在很大问题。这可能是恶意流量,可能是机器人,或者其他应该被丢弃的代理,它们并不代表您试图建模的实际人类。

如果有人真的想知道美国的平均收入(而不是中位数),您不应该仅仅因为您不喜欢他而丢弃唐纳德·特朗普。事实是,即使他的数十亿美元并没有改变中位数,但它们会推高平均数。因此,不要通过丢弃异常值来篡改您的数据。但如果它与您首先尝试建模的内容不一致,那么就丢弃异常值。

现在,我们如何识别异常值?嗯,还记得我们的老朋友标准差吗?我们在这本书的早期就讨论过这个问题。这是一个非常有用的工具,用于检测异常值。您可以以一种非常有原则的方式计算应该具有更或多或少正态分布的数据集的标准差。如果您看到一个数据点超出了一个或两个标准差,那么您就有一个异常值。

记住,我们之前也谈到了箱线图和须状图,它们也有一种内置的方法来检测和可视化异常值。这些图表将异常值定义为位于 1.5 倍四分位距之外的值。

您选择什么倍数?嗯,您必须运用常识,您知道,没有硬性规定什么是异常值。您必须查看您的数据,用眼睛观察,查看分布,查看直方图。看看是否有明显的异常值,并在丢弃它们之前了解它们是什么。

处理异常值

因此,让我们看一些示例代码,看看您如何在实践中处理异常值。让我们玩弄一些异常值。这是一个非常简单的部分。实际上是一点点复习。如果您想跟着做,我们在Outliers.ipynb中。所以,如果您愿意,请打开它:

import numpy as np

incomes = np.random.normal(27000, 15000, 10000)
incomes = np.append(incomes, [1000000000])

import matplotlib.pyplot as plt
plt.hist(incomes, 50)
plt.show()

我们在书的早期做过非常类似的事情,那里我们创建了美国收入分布的假直方图。我们要做的是从这里开始,用一个年收入平均为 27000 美元,标准偏差为 15000 美元的正态分布收入。我将创建 10000 个在该分布中有收入的假美国人。顺便说一句,这完全是虚构的数据,尽管它与现实并不那么遥远。

然后,我要插入一个异常值 - 叫它唐纳德·特朗普,他有十亿美元。我们将把这个家伙插入到我们数据集的末尾。所以,我们有一个围绕着 27000 美元的正态分布数据集,然后我们要在最后插入唐纳德·特朗普。

我们将继续将其绘制为直方图:

哇!这并不是很有帮助!我们把全国其他人的整个正态分布挤进了直方图的一个桶里。另一方面,我们把唐纳德·特朗普放在右边,以十亿美元搞乱了整个事情。

另一个问题是,如果我试图回答典型美国人赚多少钱这个问题。如果我用平均值来尝试弄清楚这个问题,那将不会是一个很好的、有用的数字:

incomes.mean ()

前面代码的输出如下:

126892.66469341301

唐纳德·特朗普独自把这个数字推高到了 126000 美元,而我知道,不包括唐纳德·特朗普的正态分布数据的真实均值只有 27000 美元。所以,在这种情况下,正确的做法是使用中位数而不是平均值。

但是,假设我们不得不出于某种原因使用平均值,正确的处理方式是排除像唐纳德·特朗普这样的异常值。所以,我们需要弄清楚如何识别这些人。嗯,你可以随意选择一个截断点,然后说,“我要抛弃所有亿万富翁”,但这不是一个很有原则的做法。10 亿是从哪里来的?

这只是我们如何计算数字的一些意外。所以,更好的做法是实际测量数据集的标准偏差,并将异常值定义为距离平均值的某个标准偏差的倍数。

接下来是我写的一个小函数,它就是reject_outliers()

def reject_outliers(data): 
    u = np.median(data) 
    s = np.std(data) 
    filtered = [e for e in data if (u - 2 * s < e < u + 2 * s)] 
    return filtered 

filtered = reject_outliers(incomes) 

plt.hist(filtered, 50) 
plt.show() 

它接收一个数据列表并找到中位数。它还找到该数据集的标准偏差。所以,我对此进行了过滤,只保留了在我的数据中距离中位数两个标准偏差之内的数据点。所以,我可以在我的收入数据上使用这个方便的reject_outliers()函数,自动剔除奇怪的异常值:

果然,它奏效了!现在我得到了一个更漂亮的图表,排除了唐纳德·特朗普,聚焦于中心的更典型的数据集。所以,非常酷!

所以,这是一个识别异常值并自动删除它们或以任何你认为合适的方式处理它们的例子。记住,一定要以原则的方式做这件事。不要只是因为它们不方便就抛弃异常值。要理解它们来自何处,以及它们实际上如何影响你试图在精神上衡量的事物。

顺便说一句,现在我们的平均值也更有意义了;现在我们已经摆脱了那个异常值,它更接近应该是的 27000。

异常值的活动

所以,如果你想玩玩这个,你知道,就像我通常要求你做的那样,试着用标准偏差的不同倍数,试着添加更多的异常值,试着添加不那么像唐纳德·特朗普那样的异常值。你知道,只是编造一些额外的假数据,然后玩弄一下,看看你是否能成功地识别出这些人。

就是这样!异常值;非常简单的概念。所以,这是一个通过查看标准偏差来识别异常值的示例,只需查看与平均值或中位数相差的标准偏差的数量。实际上,中位数可能是一个更好的选择,因为异常值可能会使平均值产生偏差,对吧?因此,使用标准偏差是一种比仅仅选择一些任意截断更有原则的识别异常值的方法。再次强调,您需要决定如何处理这些异常值。您实际上想要衡量什么?是否适合实际丢弃它们?所以,请记住这一点!

总结

在本章中,我们谈到了在偏差和方差之间取得平衡以及最小化误差的重要性。接下来,我们了解了 k 折交叉验证的概念以及如何在 Python 中实现它以防止过拟合。我们学到了在处理数据之前清洁数据和对数据进行归一化的重要性。然后我们看到了一个示例,用于确定网站的热门页面。在第九章中,《Apache Spark - 大数据上的机器学习》,我们将学习如何使用 Apache Spark 进行大数据上的机器学习。

第九章:Apache Spark - 大数据上的机器学习

到目前为止,在这本书中,我们已经讨论了许多通用的数据挖掘和机器学习技术,你可以在数据科学职业中使用,但它们都在你的桌面上运行。因此,你只能使用诸如 Python 和 scikit-learn 等技术来处理单台机器可以处理的数据量。

现在,每个人都在谈论大数据,很可能你正在为一家实际上有大数据需要处理的公司工作。大数据意味着你实际上无法控制所有数据,你无法在一个系统上处理所有数据。你需要使用整个云、一组计算资源的集群来计算它。这就是 Apache Spark 的用武之地。Apache Spark 是一个非常强大的工具,用于管理大数据,并在大规模数据集上进行机器学习。到本章结束时,你将对以下主题有深入的了解:

  • 安装和使用 Spark

  • 弹性分布式数据集RDDs

  • MLlib机器学习库

  • Spark 中的决策树

  • Spark 中的 K 均值聚类

安装 Spark

在这一部分,我将帮助你使用 Apache Spark,并向你展示一些实际使用 Apache Spark 解决与本书中过去在单台计算机上解决的相同问题的示例。我们需要做的第一件事是在你的计算机上设置 Spark。因此,我们将在接下来的几节中为你介绍如何做到这一点。这是相当简单的事情,但有一些需要特别注意的地方。所以,不要只是跳过这些部分;有一些东西你需要特别注意,才能成功地运行 Spark,尤其是在 Windows 系统上。让我们在你的系统上设置 Apache Spark,这样你就可以真正地投入其中并开始尝试一些东西。

我们现在将在你自己的桌面上运行这个。但是,我们在本章中要编写的相同程序可以在实际的 Hadoop 集群上运行。因此,你可以将我们正在编写并在 Spark 独立模式下在你的桌面上运行的这些脚本,实际上从实际的 Hadoop 集群的主节点上运行它们,然后让它扩展到整个 Hadoop 集群的强大处理大规模数据集的能力。即使我们要在你自己的计算机上本地运行这些东西,也要记住这些相同的概念也可以扩展到在集群上运行。

在 Windows 上安装 Spark

在 Windows 上安装 Spark 涉及几个步骤,我们将在这里为你逐步介绍。我假设你在 Windows 上,因为大多数人在家里使用这本书。我们稍后会谈一下如何处理其他操作系统。如果你已经熟悉在计算机上安装东西和处理环境变量,那么你可以使用以下简短的提示表并开始操作。如果你对 Windows 内部不太熟悉,我将在接下来的几节中逐步为你介绍。以下是那些 Windows 专家的快速步骤:

  1. 安装 JDK:你需要首先安装 JDK,即 Java 开发工具包。如果需要,你可以直接去 Sun 的网站下载并安装。我们需要 JDK,因为即使在这门课程中我们将使用 Python 进行开发,但在底层,它会被转换为 Scala 代码,而 Spark 就是用 Scala 原生开发的。而 Scala 又是在 Java 解释器之上运行的。因此,为了运行 Python 代码,你需要一个 Scala 系统,这将作为 Spark 的一部分默认安装。此外,我们需要 Java,或者更具体地说,需要 Java 的解释器来实际运行那些 Scala 代码。就像是一个技术层的蛋糕。

  2. 安装 Python:显然,你需要 Python,但如果你已经阅读到这本书的这一部分,你应该已经设置好了 Python 环境,希望是 Enthought Canopy。所以,我们可以跳过这一步。

  3. 安装 Hadoop 的预编译版本的 Spark:幸运的是,Apache 网站提供了预编译版本的 Spark,可以直接运行,已经为最新的 Hadoop 版本进行了预编译。您不需要构建任何东西,只需将其下载到计算机上并放在正确的位置,大部分情况下就可以使用了。

  4. 创建 conf/log4j.properties 文件:我们有一些配置要处理。我们想要做的一件事是调整警告级别,以便在运行作业时不会收到大量警告信息。我们将介绍如何做到这一点。基本上,您需要重命名一个属性文件,然后在其中调整错误设置。

  5. 添加 SPARK_HOME 环境变量:接下来,我们需要设置一些环境变量,以确保您可以从任何路径运行 Spark。我们将添加一个指向安装 Spark 的 SPARK_HOME 环境变量,然后将%SPARK_HOME%\bin添加到系统路径中,这样当您运行 Spark Submit、PySpark 或其他 Spark 命令时,Windows 就知道在哪里找到它。

  6. 设置 HADOOP_HOME 变量:在 Windows 上,我们还需要做一件事,那就是设置HADOOP_HOME变量,因为即使在独立系统上不使用 Hadoop,它也会期望找到 Hadoop 的一小部分。

  7. 安装 winutils.exe:最后,我们需要安装一个名为winutils.exe的文件。本书的资源中有winutils.exe的链接,您可以从那里获取。

如果您想更详细地了解步骤,可以参考接下来的部分。

在其他操作系统上安装 Spark

关于在其他操作系统上安装 Spark 的快速说明:基本上,这些步骤也适用于它们。主要区别在于如何在系统上设置环境变量,以便在您登录时自动应用。这将因操作系统而异。macOS 的做法与各种 Linux 的做法不同,因此您至少需要稍微熟悉使用 Unix 终端命令提示符,以及如何操纵您的环境来做到这一点。但是,大多数已经掌握这些基本原理的 macOS 或 Linux 用户都不需要winutils.exe。因此,这些是在不同操作系统上安装的主要区别。

安装 Java 开发工具包

要安装 Java 开发工具包,返回浏览器,打开一个新标签页,然后搜索jdk(Java 开发工具包的简称)。这将带您到 Oracle 网站,从那里您可以下载 Java。

在 Oracle 网站上,点击 JDK DOWNLOAD。现在,点击 Accept License Agreement,然后您可以选择适用于您操作系统的下载选项:

对我来说,这将是 Windows 64 位,等待 198MB 的好东西下载:

下载完成后,找到安装程序并运行它。请注意,我们不能在 Windows 安装程序中接受默认设置。因此,这是一个特定于 Windows 的解决方法,但在撰写本书时,当前版本的 Spark 是 2.1.1,结果表明 Spark 2.1.1 在 Windows 上与 Java 存在问题。问题在于,如果您将 Java 安装到带有空格的路径中,它将无法工作,因此我们需要确保 Java 安装到没有空格的路径中。这意味着即使您已经安装了 Java,也不能跳过此步骤,所以让我向您展示如何做到这一点。在安装程序上,点击下一步,您将看到如下屏幕,它默认要安装到C:\Program Files\Java\jdk路径,无论版本是什么:

Program Files路径中的空格会引起麻烦,因此让我们单击“更改...”按钮并安装到c:\jdk,一个简单的路径,易于记忆,并且其中没有空格:

现在,它还希望安装 Java 运行时环境,因此为了安全起见,我也将其安装到没有空格的路径。

在 JDK 安装的第二步,我们应该在屏幕上看到这个:

我也将更改目标文件夹,并为其创建一个名为C:\jre的新文件夹:

好了,安装成功。哇呼!

现在,您需要记住我们安装 JDK 的路径,我们的情况下是C:\jdk。我们还有一些步骤要走。接下来,我们需要安装 Spark 本身。

安装 Spark

让我们回到一个新的浏览器选项卡,转到spark.apache.org,并单击“下载 Spark”按钮:

现在,我们在本书中使用的是 Spark 2.1.1,但超过 2.0 的任何版本都应该可以正常工作。

确保您获得了预构建版本,并选择直接下载选项,因此所有这些默认设置都非常好。继续并单击第 4 条指示旁边的链接以下载该软件包。

现在,它下载了一个TGZTar in GZip)文件,您可能不熟悉。坦率地说,Windows 实际上对 Spark 来说有点事后诸葛亮,因为在 Windows 上,您将没有内置的实用程序来实际解压缩 TGZ 文件。这意味着您可能需要安装一个,如果您还没有的话。我使用的是 WinRAR,您可以从www.rarlab.com获取。如果需要,转到下载页面,并下载 WinRAR 32 位或 64 位的安装程序,具体取决于您的操作系统。像平常一样安装 WinRAR,这将允许您在 Windows 上实际解压缩 TGZ 文件:

所以,让我们继续解压缩 TGZ 文件。我将打开我的“下载”文件夹,找到我们下载的 Spark 存档,然后右键单击该存档,并将其提取到我选择的文件夹中-我现在只是将其放在我的“下载”文件夹中。同样,此时 WinRAR 正在为我执行此操作:

所以,我现在应该在我的“下载”文件夹中有一个与该软件包相关联的文件夹。让我们打开它,里面就是 Spark 本身。您应该看到类似下面显示的文件夹内容。因此,您需要将其安装在您可以记住的某个地方:

显然,您不希望将其留在“下载”文件夹中,所以让我们打开一个新的文件资源管理器窗口。我转到我的 C 驱动器并创建一个新文件夹,让我们称之为spark。所以,我的 Spark 安装将位于C:\spark中。再次,很容易记住。打开该文件夹。现在,我回到下载的spark文件夹,并使用Ctrl + A选择 Spark 分发中的所有内容,Ctrl + C将其复制,然后返回到C:\sparkCtrl + V将其粘贴进去:

非常重要的是要记住粘贴spark文件夹的内容,而不是spark文件夹本身。因此,我现在应该有一个包含 Spark 分发中所有文件和文件夹的C驱动器中的spark文件夹。

好吧,还有一些东西我们需要配置。所以,当我们在C:\spark中时,让我们打开conf文件夹,为了确保我们不会被日志消息淹没,我们将在这里更改日志级别设置。因此,右键单击log4j.properties.template文件,然后选择重命名:

删除文件名中的.template部分,使其成为一个真正的log4j.properties文件。Spark 将使用这个文件来配置它的日志记录:

现在,用某种文本编辑器打开这个文件。在 Windows 上,你可能需要右键单击,然后选择“打开方式”,然后选择“WordPad”。在文件中,找到log4j.rootCategory=INFO

让我们把这个改成log4j.rootCategory=ERROR,这样就可以消除运行时打印出的所有日志垃圾。保存文件,然后退出编辑器。

到目前为止,我们安装了 Python、Java 和 Spark。现在我们需要做的下一件事是安装一些东西,让你的电脑认为 Hadoop 是存在的,这一步在 Windows 上是必要的。所以,如果你在 Mac 或 Linux 上,可以跳过这一步。

我有一个小文件可以解决问题。让我们去media.sundog-soft.com/winutils.exe。下载winutils.exe将给你一个可执行文件的一小部分副本,可以用来欺骗 Spark,让它认为你实际上安装了 Hadoop:

现在,因为我们将在我们的桌面上本地运行我们的脚本,这并不是什么大不了的事,我们不需要真正安装 Hadoop。这只是绕过在 Windows 上运行 Spark 的另一个怪癖。所以,现在我们有了这个,让我们在“下载”文件夹中找到它,Ctrl + C复制它,然后让我们去我们的C驱动器,为它创建一个位置。

所以,在C驱动器的根目录中再次创建一个新文件夹,我们将称之为winutils

现在让我们打开这个winutils文件夹,并在其中创建一个bin文件夹:

现在在这个bin文件夹中,我希望你把我们下载的winutils.exe文件粘贴进去。所以你应该有C:\winutils\bin,然后winutils.exe

这个下一步只在一些系统上需要,但为了安全起见,在 Windows 上打开命令提示符。你可以通过转到开始菜单,然后转到 Windows 系统,然后点击命令提示符来做到这一点。在这里,我希望你输入cd c:\winutils\bin,这是我们放置winutils.exe文件的地方。现在如果你输入dir,你应该会看到那个文件。现在输入winutils.exe chmod 777 \tmp\hive。这只是确保你需要成功运行 Spark 的所有文件权限都已经放置好,没有任何错误。现在你可以关闭命令提示符了,因为你已经完成了这一步。哇,我们几乎完成了,信不信由你。

现在我们需要设置一些环境变量才能让事情正常运行。我将向你展示如何在 Windows 上做到这一点。在 Windows 10 上,你需要打开开始菜单,然后转到 Windows 系统 | 控制面板来打开控制面板:

在控制面板中,点击系统和安全:

然后,点击系统:

然后从左侧的列表中点击高级系统设置:

从这里,点击环境变量...:

我们将得到这些选项:

现在,这是一个非常特定于 Windows 的设置环境变量的方法。 在其他操作系统上,您将使用不同的进程,因此您需要查看如何在它们上安装 Spark。 在这里,我们将设置一些新的用户变量。 单击第一个 New...按钮以创建一个新的用户变量,并将其命名为SPARK_HOME,如下所示,全部大写。 这将指向我们安装 Spark 的位置,对我们来说是c:\spark,因此在变量值中键入它,然后单击确定:

我们还需要设置JAVA_HOME,因此再次单击新建...,并键入JAVA_HOME作为变量名。 我们需要将其指向我们安装 Java 的位置,对我们来说是c:\jdk

我们还需要设置HADOOP_HOME,这是我们安装winutils软件包的位置,因此我们将其指向c:\winutils

到目前为止,一切都很好。 我们需要做的最后一件事是修改我们的路径。 您应该在这里有一个 PATH 环境变量:

单击 PATH 环境变量,然后单击编辑...,并添加一个新路径。 这将是%SPARK_HOME%\bin,我将添加另一个,%JAVA_HOME%\bin

基本上,这使得 Spark 的所有二进制可执行文件都可以在 Windows 上运行。 单击此菜单上的确定以及前两个菜单上的确定。 我们最终设置好了一切。

Spark 介绍

让我们从高层次概述 Apache Spark 开始,看看它是什么,它适用于什么,以及它是如何工作的。

什么是 Spark?嗯,如果你去 Spark 的网站,他们会给你一个非常高层次的,模糊的答案,“一个用于大规模数据处理的快速通用引擎。” 它切片,切块,它可以洗你的衣服。 嗯,不是真的。 但它是一个用于编写可以处理大量数据的作业或脚本的框架,并且它管理将该处理分布到计算集群中。 基本上,Spark 通过让你将数据加载到称为弹性分布式数据存储的大型对象中来工作,RDDs。 它可以自动执行转换和创建基于这些 RDD 的操作,你可以将其视为大型数据框架。

它的美妙之处在于,Spark 将自动地并且最优地将处理分布在整个计算机集群中,如果您有一个可用的话。 您不再受限于在单台计算机或单台计算机的内存上可以做什么。 您实际上可以将其扩展到整个机器集群可用的所有处理能力和内存,而且在今天这个时代,计算是相当便宜的。 您实际上可以通过像亚马逊的弹性 MapReduce 服务这样的服务租用集群上的时间,并且只需花费几美元就可以在整个计算机集群上租用一些时间,并运行您无法在自己的桌面上运行的作业。

它是可扩展的

Spark 如何实现可扩展性? 好吧,让我们在这里更具体一点看看它是如何工作的。

它的工作原理是,您编写一个驱动程序,它只是一个看起来与任何其他 Python 脚本非常相似的小脚本,并且它使用 Spark 库来实际编写您的脚本。 在该库中,您定义了所谓的 Spark 上下文,这在您在 Spark 中开发时是您要使用的根对象。

从那里开始,Spark 框架会接管并为您分配任务。因此,如果您在自己的计算机上以独立模式运行,就像我们将在接下来的部分中进行的那样,所有任务都会留在您的计算机上。然而,如果您在集群管理器上运行,Spark 可以识别并自动利用它。Spark 实际上有自己内置的集群管理器,您甚至可以在没有安装 Hadoop 的情况下单独使用它,但如果您有可用的 Hadoop 集群,它也可以使用。

Hadoop 不仅仅是 MapReduce;实际上,Hadoop 有一个名为 YARN 的组件,它将 Hadoop 的整个集群管理部分分离出来。Spark 可以与 YARN 接口,实际上使用它来在 Hadoop 集群中有效地分配处理组件的资源。

在集群中,您可能有正在运行的个别执行器任务。这些可能在不同的计算机上运行,也可能在同一台计算机的不同核心上运行。它们各自有自己的缓存和自己的任务。驱动程序、Spark Context 和集群管理器共同协调所有这些工作,并将最终结果返回给您。

它的美妙之处在于,您只需要编写最初的小脚本,即驱动程序,它使用 Spark Context 在高层次上描述您想要对这些数据进行的处理。Spark 与您使用的集群管理器一起工作,找出如何分散和分发,因此您不必担心所有这些细节。当然,如果不起作用,显然,您可能需要进行一些故障排除,以找出您手头的任务是否有足够的资源可用,但理论上,这都只是魔术。

它很快

Spark 有什么了不起的?我的意思是,有类似的技术,比如 MapReduce 已经存在很长时间了。不过,Spark 很快,网站上声称 Spark 在内存中运行作业时比 MapReduce 快 100 倍,或者在磁盘上快 10 倍。当然,这里的关键词是“最多”,您的情况可能有所不同。我从来没有见过任何东西实际上比 MapReduce 快那么多。一些精心设计的 MapReduce 代码实际上仍然可以非常高效。但我会说,Spark 确实使许多常见操作更容易。MapReduce 迫使您真正将事情分解为映射器和减速器,而 Spark 则更高级一些。您不必总是那么费心地使用 Spark 做正确的事情。

这部分原因之一是 Spark 为何如此快的原因。它有一个 DAG 引擎,即有向无环图。哇,这是另一个花哨的词。这是什么意思?Spark 的工作方式是,您编写一个描述如何处理数据的脚本,您可能有一个 RDD,基本上就像一个数据框架。您可能对其进行某种转换或某种操作。但直到您对该数据执行某种操作之前,实际上什么都不会发生。在那一点上发生的是,Spark 会说“嗯,好吧。所以,这是您在这些数据上想要的最终结果。我为了达到这一点必须做的所有其他事情是什么,以及达到这一点的最佳策略是什么?”因此,在幕后,它将找出最佳的方式来分割处理,并分发信息以获得您所寻找的最终结果。因此,这里的关键是,Spark 等到您告诉它实际产生结果,只有在那一点上它才会去找出如何产生那个结果。因此,这是一个很酷的概念,这是它效率的关键。

它很年轻

Spark 是一种非常炙手可热的技术,而且相对年轻,所以它仍然在不断发展和迅速变化,但很多大公司都在使用它。例如,亚马逊声称他们在使用它,eBay,NASA 的喷气推进实验室,Groupon,TripAdvisor,雅虎,还有许多其他公司也在使用。我相信有很多公司在使用它,但他们不会承认,但如果你去 Spark Apache Wiki 页面spark.apache.org/powered-by.html

实际上有一个你可以查阅的已知大公司使用 Spark 解决实际数据问题的列表。如果你担心自己正在接触最前沿的技术,不用担心,有一些非常大的公司正在使用 Spark 来解决实际问题,你是和一些非常重要的人一起使用 Spark 来解决实际问题。在这一点上,它是相当稳定的东西。

这并不困难

这也不难。你可以选择用 Python、Java 或 Scala 编程,它们都是围绕我之前描述的相同概念构建的,即弹性分布式数据集,简称 RDD。我们将在本章的后续部分详细讨论这一点。

Spark 的组件

Spark 实际上有许多不同的组件构成。因此,有一个 Spark 核心,只需使用 Spark 核心功能就可以做出几乎任何你可以想象的事情,但还有其他一些构建在 Spark 之上的东西也很有用。

  • Spark Streaming:Spark Streaming 是一个库,它让你实际上可以实时处理数据。数据可以持续地流入服务器,比如来自网络日志,Spark Streaming 可以帮助你实时处理数据,一直进行下去。

  • Spark SQL:这让你实际上可以将数据视为 SQL 数据库,并在其上发出 SQL 查询,如果你已经熟悉 SQL,这是很酷的。

  • MLlib:这是我们在本节中要重点关注的内容。它实际上是一个机器学习库,让你可以执行常见的机器学习算法,底层使用 Spark 来实际分布式处理集群中的数据。你可以对比以前能处理的更大的数据集进行机器学习。

  • GraphX:这不是用来制作漂亮的图表和图形的。它是指网络理论意义上的图。想想一个社交网络;这就是图的一个例子。GraphX 只有一些函数,让你分析信息图的属性。

Python 与 Scala 在 Spark 中的比较

有时候我在教授 Apache Spark 时会遇到一些批评,因为我使用 Python,但我的做法是有道理的。的确,很多人在编写 Spark 代码时使用 Scala,因为 Spark 是本地开发的。因此,通过强制 Spark 将你的 Python 代码转换为 Scala,然后在最后一天转换为 Java 解释器命令,你会增加一些开销。

然而,Python 要容易得多,而且你不需要编译东西。管理依赖项也要容易得多。你可以真正把时间集中在算法和你正在做的事情上,而不是在实际构建、运行、编译和所有那些废话上。此外,显然,这本书到目前为止一直都在关注 Python,继续使用我们学到的东西并在这些讲座中坚持使用 Python 是有意义的。以下是两种语言的优缺点的快速总结:

Python Scala

|

  • 无需编译、管理依赖等

  • 编码开销更少

  • 你已经了解 Python

  • 让我们专注于概念而不是新语言

|

  • Scala 可能是 Spark 的更受欢迎的选择

  • Spark 是用 Scala 构建的,所以在 Scala 中编码对于 Spark 来说是“本地”的

  • 新功能、库往往是首先使用 Scala

|

然而,我要说的是,如果您在现实世界中进行一些 Spark 编程,很有可能人们正在使用 Scala。不过不要太担心,因为在 Spark 中,Python 和 Scala 代码最终看起来非常相似,因为它们都围绕着相同的 RDD 概念。语法略有不同,但并不是很大的不同。如果您能够弄清楚如何使用 Python 进行 Spark 编程,学习如何在 Scala 中使用它并不是一个很大的飞跃。这里有两种语言中相同代码的快速示例:

因此,这就是 Spark 本身的基本概念,为什么它如此重要,以及它如何在让您在非常大的数据集上运行机器学习算法或任何算法方面如此强大。现在让我们更详细地讨论一下它是如何做到这一点的,以及弹性分布式数据集的核心概念。

Spark 和弹性分布式数据集(RDD)

让我们深入了解一下 Spark 的工作原理。我们将谈论弹性分布式数据集,即 RDD。这是您在 Spark 编程中使用的核心,我们将提供一些代码片段来尝试使其变得真实。我们将在这里为您提供 Apache Spark 的速成课程。比我们接下来要涵盖的内容更加深入,但我只会为您提供实际理解这些示例所需的基础知识,并希望能够让您开始并指向正确的方向。

如前所述,Spark 最基本的部分称为弹性分布式数据集,即 RDD,这将是您实际用来加载、转换和获取您想要的数据的对象。这是一个非常重要的理解。RDD 中的最后一个字母代表数据集,最终它只是一堆包含几乎任何内容的信息行。但关键是 R 和第一个 D。

  • 弹性:它是弹性的,因为 Spark 确保如果您在集群上运行此任务并且其中一个集群出现故障,它可以自动从中恢复并重试。不过,请注意,这种弹性是有限的。如果您没有足够的资源可用于您要运行的作业,它仍然会失败,您将不得不为其添加更多资源。它只能从许多事情中恢复;它会尝试多少次重新尝试给定的任务是有限的。但它会尽最大努力确保在面对不稳定的集群或不稳定的网络时,仍然会继续尽最大努力运行到完成。

  • 分布式:显然,它是分布式的。使用 Spark 的整个目的是,您可以将其用于可以横向分布到整个计算机集群的 CPU 和内存功率的大数据问题。这可以水平分布,因此您可以将尽可能多的计算机投入到给定的问题中。问题越大,使用的计算机就越多;在这方面真的没有上限。

SparkContext 对象

您始终通过获取 SparkContext 对象来启动 Spark 脚本,这个对象体现了 Spark 的核心。它将为您提供要在其上处理的 RDD,因此它生成了您在处理中使用的对象。

你知道吗,当你实际编写 Spark 程序时,你并不会非常关注 SparkContext,但它实际上是在幕后为你运行它们的基础。如果你在 Spark shell 中交互式运行,它已经为你提供了一个sc对象,你可以用它来创建 RDD。然而,在独立脚本中,你将不得不显式创建 SparkContext,并且你将不得不注意你使用的参数,因为你实际上可以告诉 Spark 上下文你希望它如何分布。我应该利用我可用的每个核心吗?我应该在集群上运行还是只在我的本地计算机上独立运行?所以,这就是你设置 Spark 操作的基本设置的地方。

创建 RDD

让我们看一些实际创建 RDD 的小代码片段,我认为这一切都会开始变得更加清晰。

使用 Python 列表创建 RDD

以下是一个非常简单的例子:

nums = parallelize([1, 2, 3, 4]) 

如果我只想从一个普通的 Python 列表中创建 RDD,我可以在 Spark 中调用parallelize()函数。这将把一系列东西,比如这里的数字 1、2、3、4,转换为一个名为nums的 RDD 对象。

这是创建 RDD 的最简单情况,只是从一个硬编码的列表中创建。该列表可以来自任何地方;它也不必是硬编码的,但这有点违背了大数据的目的。我的意思是,如果我必须在创建 RDD 之前将整个数据集加载到内存中,那还有什么意义呢?

从文本文件加载 RDD

我还可以从文本文件中加载 RDD,它可以是任何地方。

sc.textFile("file:///c:/users/frank/gobs-o-text.txt")  

在这个例子中,我有一个巨大的文本文件,整个百科全书之类的东西。我正在从我的本地磁盘读取它,但如果我想要将这个文件托管在分布式的 AmazonS3 存储桶上,我也可以使用 s3n,或者如果我想引用存储在分布式 HDFS 集群上的数据,我可以使用 hdfs(如果您对 HDFS 不熟悉,它代表 Hadoop 分布式文件系统)。当你处理大数据并使用 Hadoop 集群时,通常你的数据会存储在那里。

这行代码实际上会将文本文件的每一行转换为 RDD 中的一行。所以,你可以把 RDD 看作是一行的数据库,在这个例子中,它将我的文本文件加载到一个 RDD 中,其中每一行,每一行,包含一行文本。然后我可以在那个 RDD 中进行进一步的处理,解析或分解数据中的分隔符。但这是我开始的地方。

还记得我们之前在书中讨论 ETL 和 ELT 吗?这是一个很好的例子,你可能实际上正在将原始数据加载到系统中,并在系统本身上进行转换,用于查询数据的系统。你可以拿未经任何处理的原始文本文件,并利用 Spark 的强大功能将其转换为更结构化的数据。

它还可以与 Hive 等东西通信,所以如果你的公司已经设置了现有的 Hive 数据库,你可以创建一个基于你的 Spark 上下文的 Hive 上下文。这是多么酷啊?看看这个例子代码:

hiveCtx = HiveContext(sc)  rows = hiveCtx.sql("SELECT name, age FROM users")  

你实际上可以创建一个 RDD,这里称为 rows,它是通过在你的 Hive 数据库上实际执行 SQL 查询来生成的。

创建 RDD 的更多方法

还有更多创建 RDD 的方法。您可以从 JDBC 连接创建它们。基本上,任何支持 JDBC 的数据库也可以与 Spark 通信,并从中创建 RDD。Cassandra、HBase、Elasticsearch,还有 JSON 格式、CSV 格式、序列文件对象文件以及一堆其他压缩文件(如 ORC)都可以用来创建 RDD。我不想深入讨论所有这些细节,如果需要,您可以找一本书查看,但重点是很容易从数据中创建 RDD,无论数据是在本地文件系统还是分布式数据存储中。

再次强调,RDD 只是一种加载和维护大量数据并一次跟踪所有数据的方法。但是,在脚本中,概念上,RDD 只是包含大量数据的对象。您不必考虑规模,因为 Spark 会为您处理。

RDD 操作

现在,一旦您拥有 RDD,您可以对其执行两种不同类型的操作,即转换和操作。

转换

让我们先谈谈转换。转换就是它听起来的样子。这是一种将 RDD 中的每一行根据您提供的函数转换为新值的方法。让我们看看其中一些函数:

  • map() 和 flatmap(): mapflatmap是您经常看到的函数。这两个函数都将接受您可以想象的任何函数,该函数将以 RDD 的一行作为输入,并输出一个转换后的行。例如,您可以从 CSV 文件中获取原始输入,您的map操作可能会将该输入根据逗号分隔符拆分为单独的字段,并返回一个包含以更结构化格式的数据的 Python 列表,以便您可以进行进一步的处理。您可以链接 map 操作,因此一个map的输出可能最终创建一个新的 RDD,然后您可以对其进行另一个转换,依此类推。再次强调,关键是,Spark 可以在集群上分发这些转换,因此它可能会在一台机器上转换 RDD 的一部分,然后在另一台机器上转换 RDD 的另一部分。

就像我说的,mapflatmap是您将看到的最常见的转换。唯一的区别是map只允许您为每一行输出一个值,而flatmap将允许您实际上为给定的行输出多个新行。因此,您实际上可以使用flatmap创建一个比您开始时更大或更小的 RDD。

  • filter(): 如果您只想创建一个布尔函数来判断“是否应该保留此行?是或否。”

  • distinct(): distinct是一个不太常用的转换,它将仅返回 RDD 中的不同值。

  • sample(): 此函数允许您从 RDD 中随机抽取样本

  • union(), intersection(), subtract() 和 Cartesian(): 您可以执行诸如并集、交集、差集,甚至生成 RDD 中存在的每个笛卡尔组合的操作。

使用 map()

以下是您如何在工作中使用 map 函数的一个小例子:

rdd = sc.parallelize([1, 2, 3, 4]) 
rdd.map(lambda x: x*x) 

假设我只是从列表 1、2、3、4 创建了一个 RDD。然后我可以使用一个 lambda 函数 x 调用rdd.map(),该函数接受每一行,也就是 RDD 的每个值,将其称为 x,然后将函数 x 乘以 x 应用于平方。如果我然后收集此 RDD 的输出,它将是 1、4、9 和 16,因为它将获取该 RDD 的每个单独条目并对其进行平方,然后将其放入新的 RDD 中。

如果您不记得 lambda 函数是什么,我们在本书的前面稍微谈到过,但是作为提醒,lambda 函数只是定义一个内联函数的简写。因此,rdd.map(lambda x: x*x)与一个单独的函数def squareIt(x): return x*x是完全相同的,并且说rdd.map(squareIt)

这只是一个非常简单的函数的简写,您希望将其作为转换传递。它消除了实际将其声明为自己的单独命名函数的需要。这就是函数式编程的整个理念。所以你现在可以说你理解函数式编程了!但实际上,这只是定义一个内联函数作为map()函数的参数之一,或者任何转换的简写符号。

行动

您还可以对 RDD 执行操作,当您真正想要获得结果时。以下是一些您可以执行的示例:

  • collect(): 您可以在 RDD 上调用 collect(),这将为您提供一个普通的 Python 对象,然后您可以遍历并打印结果,或将其保存到文件,或者您想做的任何其他事情。

  • count(): 您还可以调用count(),这将强制其实际上计算此时 RDD 中有多少条目。

  • countByValue(): 此函数将为您提供 RDD 中每个唯一值出现的次数的统计。

  • take(): 您还可以使用take()从 RDD 中进行抽样,它将从 RDD 中获取随机数量的条目。

  • top(): 如果您只想为了调试目的查看 RDD 中的前几个条目,top()将为您提供这些条目。

  • reduce(): 更强大的操作是reduce(),它实际上允许您将相同的公共键值的值组合在一起。您还可以在键-值数据的上下文中使用 RDD。reduce()函数允许您定义一种将给定键的所有值组合在一起的方式。它在精神上与 MapReduce 非常相似。reduce()基本上是 MapReduce 中reducer()的类似操作,而map()类似于mapper()。因此,通过使用这些函数,实际上很容易将 MapReduce 作业转换为 Spark。

还记得,在 Spark 中实际上什么都不会发生,直到您调用一个操作。一旦调用其中一个操作方法,Spark 就会出去并使用有向无环图进行其魔术,并实际计算获得所需答案的最佳方式。但请记住,直到发生那个操作,实际上什么都不会发生。因此,当您编写 Spark 脚本时,有时可能会遇到问题,因为您可能在其中有一个小的打印语句,并且您可能期望得到一个答案,但实际上直到执行操作时才会出现。

这就是 Spark 编程的基础。基本上,什么是 RDD 以及您可以对 RDD 执行哪些操作。一旦掌握了这些概念,您就可以编写一些 Spark 代码。现在让我们改变方向,谈谈 MLlib,以及 Spark 中一些特定的功能,让您可以使用 Spark 进行机器学习算法。

介绍 MLlib

幸运的是,在进行机器学习时,您不必在 Spark 中以困难的方式进行操作。它有一个名为 MLlib 的内置组件,它位于 Spark Core 之上,这使得使用大规模数据集执行复杂的机器学习算法变得非常容易,并将该处理分布到整个计算机集群中。非常令人兴奋的事情。让我们更多地了解它可以做什么。

一些 MLlib 功能

那么,MLlib 可以做些什么?其中之一是特征提取。

您可以在规模上执行词频和逆文档频率等操作,这对于创建搜索索引非常有用。我们稍后将实际上通过本章的一个示例来进行说明。关键是,它可以使用大规模数据集在整个集群中执行此操作,因此您可以使用它来为网络创建自己的搜索引擎。它还提供基本的统计函数,卡方检验,皮尔逊或斯皮尔曼相关性,以及一些更简单的东西,如最小值,最大值,平均值和方差。这些本身并不是非常令人兴奋,但令人兴奋的是,您实际上可以计算大规模数据集的方差或平均值,或者相关性得分,如果必要,它实际上会将该数据集分解成各种块,并在整个集群中运行。

因此,即使其中一些操作并不是非常有趣,有趣的是它可以操作的规模。它还支持诸如线性回归和逻辑回归之类的东西,因此如果您需要将函数拟合到大量数据集并用于预测,您也可以这样做。它还支持支持向量机。我们正在进入一些更高级的算法,一些更高级的东西,这也可以使用 Spark 的 MLlib 扩展到大规模数据集。MLlib 中内置了朴素贝叶斯分类器,因此,还记得我们在本书前面构建的垃圾邮件分类器吗?您实际上可以使用 Spark 为整个电子邮件系统执行此操作,并根据需要扩展。

决策树,机器学习中我最喜欢的东西之一,也受到 Spark 的支持,我们稍后在本章中将有一个示例。我们还将研究 K 均值聚类,您可以使用 Spark 和 MLlib 对大规模数据集进行聚类。甚至主成分分析和奇异值分解也可以使用 Spark 进行,我们也将有一个示例。最后,MLlib 中内置了一种名为交替最小二乘法的推荐算法。就我个人而言,我对它的效果有些参差不齐,您知道,对于我来说,它有点太神秘了,但我是一个推荐系统的挑剔者,所以请带着一颗谨慎的心来看待这一点!

特殊的 MLlib 数据类型

使用 MLlib 通常非常简单,只需要调用一些库函数。但是,它确实引入了一些新的数据类型,您需要了解一下,其中之一就是向量。

向量数据类型

还记得我们在本书前面做电影相似性和电影推荐时吗?向量的一个例子可能是给定用户评分的所有电影的列表。有两种类型的向量,稀疏和密集。让我们看看这两种的例子。世界上有很多很多电影,密集向量实际上会表示每部电影的数据,无论用户是否真的观看了它。所以,例如,假设我有一个用户观看了《玩具总动员》,显然我会存储他们对《玩具总动员》的评分,但如果他们没有观看电影《星球大战》,我实际上会存储没有《星球大战》的数字这一事实。因此,我们最终会占用所有这些缺失数据点的空间。稀疏向量只存储存在的数据,因此不会浪费任何内存空间在缺失数据上。因此,它是一种更紧凑的内部向量表示形式,但显然在处理时会引入一些复杂性。因此,如果您知道您的向量中将有很多缺失数据,这是一种节省内存的好方法。

带标签的点数据类型

还有一个LabeledPoint数据类型,它就像它听起来的那样,一个带有某种标签的点,以人类可读的方式传达这些数据的含义。

评级数据类型

最后,如果您在使用 MLlib 进行推荐,您将遇到Rating数据类型。这种数据类型可以接受代表 1-5 或 1-10 的评级,无论一个人可能有什么星级评价,并使用它来自动提供产品推荐。

因此,我认为您终于有了开始的一切,让我们深入实际查看一些真正的 MLlib 代码并运行它,然后它将变得更加清晰。

在 Spark 中使用 MLlib 的决策树

好了,让我们使用 Spark 和 MLlib 库实际构建一些决策树,这是非常酷的东西。无论你把这本书的课程材料放在哪里,我希望你现在就去那个文件夹。确保你完全关闭了 Canopy,或者你用于 Python 开发的任何环境,因为我想确保你是从这个目录开始的,好吗?然后找到SparkDecisionTree脚本,双击打开 Canopy:

现在,在这一点上,我们一直在使用 IPython 笔记本来编写我们的代码,但是你不能真正很好地使用它们与 Spark。对于 Spark 脚本,你需要实际将它们提交到 Spark 基础设施并以非常特殊的方式运行它们,我们很快就会看到它是如何工作的。

探索决策树代码

所以,现在我们只是看一个原始的 Python 脚本文件,没有 IPython 笔记本的通常修饰。让我们来看看脚本中发生了什么。

我们会慢慢来,因为这是你在本书中看到的第一个 Spark 脚本。

首先,我们将从pyspark.mllib中导入我们在 Spark 机器学习库中需要的部分。

from pyspark.mllib.regression import LabeledPoint 
from pyspark.mllib.tree import DecisionTree 

我们需要LabeledPoint类,这是DecisionTree类所需的数据类型,以及从mllib.tree导入的DecisionTree类本身。

接下来,你会看到几乎每个 Spark 脚本都会包含这一行,我们在其中导入SparkConfSparkContext

from pyspark import SparkConf, SparkContext 

这是创建SparkContext对象所需的,它是你在 Spark 中做任何事情的根本。

最后,我们将从numpy中导入数组库:

from numpy import array 

是的,你仍然可以在 Spark 脚本中使用NumPyscikit-learn或者任何你想要的东西。你只需要确保首先这些库在你打算在其上运行的每台机器上都已安装好。

如果你在集群上运行,你需要确保这些 Python 库已经以某种方式安装好了,并且你还需要明白,Spark 不会使 scikit-learn 的方法等变得可扩展。你仍然可以在给定 map 函数的上下文中调用这些函数,但它只会在那一个机器的一个进程中运行。不要过分依赖这些东西,但是对于像管理数组这样的简单事情,这是完全可以的。

创建 SparkContext

现在,我们将开始设置我们的SparkContext,并给它一个SparkConf,一个配置。

conf = SparkConf().setMaster("local").setAppName("SparkDecisionTree") 

这个配置对象表示,我将把主节点设置为"local",这意味着我只是在自己的本地桌面上运行,我实际上根本不是在集群上运行,我只会在一个进程中运行。我还会给它一个应用程序名称"SparkDecisionTree",你可以随意命名它,Fred、Bob、Tim,随你喜欢。这只是当你稍后在 Spark 控制台中查看时,这个作业将显示为什么。

然后,我们将使用该配置创建我们的SparkContext对象:

sc = SparkContext(conf = conf) 

这给了我们一个sc对象,我们可以用它来创建 RDDs。

接下来,我们有一堆函数:

# Some functions that convert our CSV input data into numerical 
# features for each job candidate 
def binary(YN): 
    if (YN == 'Y'): 
        return 1 
    else: 
        return 0 

def mapEducation(degree): 
    if (degree == 'BS'): 
        return 1 
    elif (degree =='MS'): 
        return 2 
    elif (degree == 'PhD'): 
        return 3 
    else: 
        return 0 

# Convert a list of raw fields from our CSV file to a 
# LabeledPoint that MLLib can use. All data must be numerical... 
def createLabeledPoints(fields): 
    yearsExperience = int(fields[0]) 
    employed = binary(fields[1]) 
    previousEmployers = int(fields[2]) 
    educationLevel = mapEducation(fields[3]) 
    topTier = binary(fields[4]) 
    interned = binary(fields[5]) 
    hired = binary(fields[6]) 

    return LabeledPoint(hired, array([yearsExperience, employed, 
        previousEmployers, educationLevel, topTier, interned])) 

现在先记住这些函数,稍后我们会回来再讨论它们。

导入和清理我们的数据

让我们来看一下这个脚本中实际执行的第一部分 Python 代码。

我们要做的第一件事是加载PastHires.csv文件,这是我们在本书早期做决策树练习时使用的同一个文件。

让我们暂停一下,回顾一下那个文件的内容。如果你记得的话,我们有一堆求职者的属性,还有一个字段,表示我们是否雇佣了这些人。我们要做的是建立一个决策树,来预测 - 根据这些属性,我们是否会雇佣这个人。

现在,让我们快速查看一下PastHires.csv,这将是一个 Excel 文件。

您可以看到 Excel 实际上将其导入为一个表,但如果您查看原始文本,您会发现它由逗号分隔的值组成。

第一行是每列的实际标题,所以上面的内容是先前经验年数,候选人当前是否在职,以及之前的雇主数量,教育水平,是否就读于顶尖学校,是否在学校期间有实习,最后,我们试图在最后一天预测的目标,即他们是否得到了工作机会。现在,我们需要将这些信息读入 RDD,以便我们可以对其进行处理。

让我们回到我们的脚本:

rawData = sc.textFile("e:/sundog-consult/udemy/datascience/PastHires.csv") 
header = rawData.first() 
rawData = rawData.filter(lambda x:x != header) 

我们需要做的第一件事是读取 CSV 数据,并且我们将丢弃第一行,因为那是我们的标题信息,记住。这里有一个小技巧。我们首先从文件中导入每一行到一个原始数据 RDD 中,我可以随意命名它,但我们称它为sc.textFile。SparkContext 有一个textFile函数,它将获取一个文本文件并创建一个新的 RDD,其中每个条目,RDD 的每一行,都包含一个输入行。

确保将文件的路径更改为您实际安装的位置,否则它将无法工作。

现在,我将使用first函数从 RDD 中提取第一行,也就是第一行列标题。现在,头部 RDD 将包含一个条目,即列标题的那一行。现在,看看上面的代码,我在包含 CSV 文件中的原始数据上使用filter,并定义了一个filter函数,只有当该行不等于初始标题行的内容时,才允许该行通过。我在这里所做的是,我从我的原始 CSV 文件中剥离出了第一行,只允许不等于第一行的行通过,并将其返回给rawData RDD 变量。所以,我从rawData中过滤掉了第一行,并创建了一个只包含数据本身的新rawData。到目前为止明白了吗?并不复杂。

现在,我们要使用map函数。接下来,我们需要开始对这些信息进行更多的结构化处理。现在,我的 RDD 的每一行都只是一行文本,它是逗号分隔的文本,但它仍然只是一行巨大的文本,我想将逗号分隔的值列表实际分割成单独的字段。最终,我希望每个 RDD 都从一行文本转换为一个 Python 列表,其中包含我拥有的每个信息列的实际单独字段。这就是这个 lambda 函数的作用:

csvData = rawData.map(lambda x: x.split(",")) 

它调用了内置的 Python 函数split,该函数将获取一行输入,并在逗号字符上进行拆分,并将其分成一个由逗号分隔的每个字段的列表。

这个map函数的输出,我传入了一个 lambda 函数,它只是根据逗号将每一行拆分成字段,得到了一个名为csvData的新 RDD。此时,csvData是一个 RDD,其中每一行都包含一个列表,其中每个元素都是源数据中的列。现在,我们接近了。

事实证明,为了在 MLlib 中使用决策树,需要满足一些条件。首先,输入必须是 LabeledPoint 数据类型,并且所有数据都必须是数字性质的。因此,我们需要将所有原始数据转换为实际可以被 MLlib 消耗的数据,这就是我们之前跳过的createLabeledPoints函数所做的事情。我们马上就会讲到,首先是对它的调用:

trainingData = csvData.map(createLabeledPoints) 

我们将在csvData上调用 map,并将其传递给createLabeledPoints函数,该函数将将每个输入行转换为最终我们想要的东西。所以,让我们看看createLabeledPoints做了什么:

def createLabeledPoints(fields): 
    yearsExperience = int(fields[0]) 
    employed = binary(fields[1]) 
    previousEmployers = int(fields[2]) 
    educationLevel = mapEducation(fields[3]) 
    topTier = binary(fields[4]) 
    interned = binary(fields[5]) 
    hired = binary(fields[6]) 

    return LabeledPoint(hired, array([yearsExperience, employed, 
        previousEmployers, educationLevel, topTier, interned])) 

它接受一个字段列表,再次提醒您一下它是什么样子,让我们再次打开那个.csv的 Excel 文件:

因此,此时每个 RDD 条目都有一个字段,它是一个 Python 列表,其中第一个元素是工作经验,第二个元素是就业情况,依此类推。问题在于我们希望将这些列表转换为 Labeled Points,并且我们希望将所有内容转换为数值数据。因此,所有这些 yes 和 no 答案都需要转换为 1 和 0。这些经验水平需要从学位名称转换为某些数值序数值。也许我们将值 0 分配给没有教育,1 表示学士学位,2 表示硕士学位,3 表示博士学位,例如。同样,所有这些 yes/no 值都需要转换为 0 和 1,因为归根结底,进入我们的决策树的一切都需要是数值的,这就是createLabeledPoints的作用。现在,让我们回到代码并运行它:

def createLabeledPoints(fields): 
    yearsExperience = int(fields[0]) 
    employed = binary(fields[1]) 
    previousEmployers = int(fields[2]) 
    educationLevel = mapEducation(fields[3]) 
    topTier = binary(fields[4]) 
    interned = binary(fields[5]) 
    hired = binary(fields[6]) 

    return LabeledPoint(hired, array([yearsExperience, employed, 
        previousEmployers, educationLevel, topTier, interned])) 

首先,它接受我们的StringFields列表,准备将其转换为LabeledPoints,其中标签是目标值-这个人是否被雇佣?0 或 1-后面是由我们关心的所有其他字段组成的数组。因此,这就是您创建DecisionTree MLlib类可以使用的LabeledPoint的方式。因此,您可以在上面的代码中看到,我们将工作经验从字符串转换为整数值,并且对于所有的 yes/no 字段,我们调用了我在代码顶部定义的binary函数,但我们还没有讨论过:

def binary(YN): 
    if (YN == 'Y'): 
        return 1 
    else: 
        return 0 

它只是将字符 yes 转换为 1,否则返回 0。所以,Y 将变为 1,N 将变为 0。同样,我有一个mapEducation函数:

def mapEducation(degree): 
    if (degree == 'BS'): 
        return 1 
    elif (degree =='MS'): 
        return 2 
    elif (degree == 'PhD'): 
        return 3 
    else: 
        return 0 

正如我们之前讨论的,这只是将不同类型的学位转换为与我们的 yes/no 字段完全相同的序数数值。

作为提醒,这是让我们通过这些函数的代码行:

trainingData = csvData.map(createLabeledPoints) 

在使用createLabeledPoints函数映射我们的 RDD 之后,我们现在有了一个trainingData RDD,这正是 MLlib 构建决策树所需要的。

创建测试候选人并构建我们的决策树

让我们创建一个小的测试候选人,这样我们就可以使用我们的模型来预测是否会雇佣某个新人。我们要做的是创建一个测试候选人,其中包含与 CSV 文件中每个字段相同的值的数组:

testCandidates = [ array([10, 1, 3, 1, 0, 0])] 

让我们快速将该代码与 Excel 文档进行比较,以便您可以看到数组映射:

同样,我们需要将它们映射回它们的原始列表示,以便 10、1、3、1、0、0 表示 10 年的工作经验,目前就业,三个以前的雇主,学士学位,没有上过一流学校,也没有做实习。如果我们愿意,我们实际上可以创建一个完整的 RDD 候选人,但现在我们只做一个。

接下来,我们将使用 parallelize 将该列表转换为 RDD:

testData = sc.parallelize(testCandidates) 

没有新东西。好了,现在让我们移动到下一个代码块:

model = DecisionTree.trainClassifier(trainingData, numClasses=2, 
                    categoricalFeaturesInfo={1:2, 3:4, 4:2, 5:2}, 
                    impurity='gini', maxDepth=5, maxBins=32) 

我们将调用DecisionTree.trainClassifier,这将实际构建我们的决策树本身。我们传入我们的trainingData,这只是一个充满LabeledPoint数组的 RDD,numClasses=2,因为我们基本上是在做一个是或否的预测,这个人会被雇佣吗?下一个参数叫做categoricalFeaturesInfo,这是一个 Python 字典,将字段映射到每个字段中的类别数。因此,如果某个字段有一个连续的范围可用,比如工作经验的年数,你就不需要在这里指定它,但对于那些具有分类特性的字段,比如他们拥有什么学位,例如,那会说字段 ID3,映射到所获得的学位,有四种不同的可能性:没有教育、学士、硕士和博士。对于所有的是/否字段,我们将它们映射到 2 种可能的类别,是/否或 0/1 是我们将它们转换成的。

继续通过我们的DecisionTree.trainClassifier调用,我们将使用'gini'不纯度度量作为我们测量熵的指标。我们有一个最大深度为 5,这只是我们将要走多远的一个上限,如果你愿意,它可以更大。最后,maxBins只是一种权衡计算开销的方式,如果可以的话,它只需要至少是每个特征中你拥有的最大类别数。记住,直到我们调用一个操作之前,什么都不会发生,因此我们将实际使用这个模型来为我们的测试候选人做出预测。

我们使用我们的DecisionTree模型,其中包含了在我们的测试训练数据上训练的决策树,并告诉它对我们的测试数据进行预测:

predictions = model.predict(testData) 
print ('Hire prediction:') 
results = predictions.collect() 
for result in results: 
     print (result) 

我们将得到一个预测列表,然后我们可以进行迭代。因此,predict返回一个普通的 Python 对象,是我可以collect的一个操作。让我稍微改一下:collect将返回我们预测的 Python 对象,然后我们可以迭代遍历列表中的每个项目并打印出预测的结果。

我们还可以通过使用toDebugString打印出决策树本身:

print('Learned classification tree model:') 
print(model.toDebugString()) 

这将实际打印出它内部创建的决策树的一个小表示,你可以在自己的头脑中跟踪。所以,这也很酷。

运行脚本

好了,随意花点时间,多看一下这个脚本,消化一下正在发生的事情,但是,如果你准备好了,让我们继续并实际运行这个程序。因此,你不能直接从 Canopy 运行它。我们将转到工具菜单,打开 Canopy 命令提示符,这只是打开一个 Windows 命令提示符,其中包含运行 Canopy 中 Python 脚本所需的所有必要环境变量。确保工作目录是你安装所有课程材料的目录。

我们需要做的就是调用spark-submit,这是一个脚本,可以让你从 Python 运行 Spark 脚本,然后是脚本的名称SparkDecisionTree.py。这就是我需要做的全部。

spark-submit SparkDecisionTree.py 

按回车键,然后它就会运行。再次强调,如果我在集群上进行操作,并且相应地创建了我的SparkConf,这实际上会分发到整个集群,但是现在,我们只是在我的电脑上运行它。完成后,你应该会看到下面的输出:

因此,在上面的图像中,你可以看到我们上面输入的测试人员的预测是这个人会被雇佣,我也打印出了决策树本身,所以这很酷。现在,让我们再次打开那个 Excel 文档,这样我们就可以将其与输出进行比较:

我们可以逐步进行并看看它的意思。所以,在我们的输出决策树中,实际上我们最终得到了一个深度为四的树,有九个不同的节点,再次提醒一下,这些不同的字段是如何相关的,阅读的方式是:如果(特征 1 为 0),这意味着如果受雇者为否,那么我们就会下降到特征 5。这个列表是从 0 开始的,所以在我们的 Excel 文档中,特征 5 是实习。我们可以像这样遍历整个树:这个人目前没有工作,没有做实习,没有工作经验,有学士学位,我们不会雇佣这个人。然后我们来到了 Else 子句。如果这个人有高级学位,我们会雇用他们,仅仅基于我们训练的数据。所以,你可以根据这些不同的特征 ID 回溯到你的原始数据源,记住,你总是从 0 开始计数,并据此进行解释。请注意,在这个可能的类别列表中,所有的分类特征都是用布尔值表示的,而连续数据则是用数字表示小于或大于的关系。

就是这样,使用 Spark 和 MLlib 构建的实际决策树确实有效且有意义。非常棒的东西。

Spark 中的 K-Means 聚类

好了,让我们看看在 MLlib 中使用 Spark 的另一个例子,这一次我们将看看 k-means 聚类,就像我们使用决策树一样,我们将采用与使用 scikit-learn 相同的例子,但这次我们将在 Spark 中进行,这样它就可以扩展到大规模数据集。所以,我已经确保关闭了其他所有东西,然后我将进入我的书籍材料,打开SparkKMeansPython 脚本,让我们来研究一下其中的内容。

好了,再次开始一些样板文件。

from pyspark.mllib.clustering import KMeans 
from numpy import array, random 
from math import sqrt 
from pyspark import SparkConf, SparkContext 
from sklearn.preprocessing import scale 

我们将从聚类MLlib包中导入KMeans包,我们将从numpy中导入数组和随机数,因为,再次强调,你可以自由使用任何你想要的东西,这是一个 Python 脚本,MLlib通常需要numpy数组作为输入。我们将导入sqrt函数和通常的样板文件,我们需要从pyspark中几乎每次都导入SparkConfSparkContext。我们还将从scikit-learn中导入缩放函数。再次强调,只要确保在你要运行这个作业的每台机器上都安装了scikit-learn,并且不要假设scikit-learn会因为在 Spark 上运行就会自动扩展。但是,因为我只是用它来进行缩放函数,所以没问题。好了,让我们开始设置吧。

我将首先创建一个全局变量:

 K=5 

在这个例子中,我将使用 K 为 5 来运行 k-means 聚类,意味着有五个不同的簇。然后我将设置一个本地的SparkConf,只在我的桌面上运行:

conf = SparkConf().setMaster("local").setAppName("SparkKMeans") 
sc = SparkContext(conf = conf) 

我将把我的应用程序的名称设置为SparkKMeans,并创建一个SparkContext对象,然后我可以使用它来创建在我的本地机器上运行的 RDD。我们暂时跳过createClusteredData函数,直接到第一行被运行的代码。

data = sc.parallelize(scale(createClusteredData(100, K)))  

  1. 我们要做的第一件事是通过并行化一些我创建的假数据来创建一个 RDD,这就是createClusteredData函数所做的。基本上,我告诉你创建 100 个围绕 K 个质心聚集的数据点,这与我们在本书早期玩 k-means 聚类时看到的代码几乎完全相同。如果你需要复习,可以回头看看那一章。基本上,我们要做的是创建一堆随机的质心,围绕它们通常分布一些年龄和收入数据。所以,我们正在尝试根据他们的年龄和收入对人进行聚类,并且我们正在制造一些数据点来做到这一点。这将返回我们的假数据的numpy数组。

  2. 一旦createClusteredData返回结果,我会在其上调用scale,这将确保我的年龄和收入在可比较的尺度上。现在,记住我们学过的关于数据归一化的部分吗?这是一个重要的例子,所以我们正在使用scale对数据进行归一化,以便我们从 k-means 中得到好的结果。

  3. 最后,我们使用parallelize将结果数组列表并行化为 RDD。现在我们的数据 RDD 包含了所有的假数据。我们所要做的,甚至比决策树还要简单,就是在我们的训练数据上调用KMeans.train

clusters = KMeans.train(data, K, maxIterations=10, 
        initializationMode="random") 

我们传入我们想要的簇的数量,我们的 K 值,一个参数,它对它要处理的量设置了一个上限;然后告诉它使用 k-means 的默认初始化模式,在我们开始迭代之前,我们只是随机选择我们的簇的初始质心,然后我们可以使用返回的模型。我们将称之为clusters

好了,现在我们可以玩玩那个簇。

让我们从打印出每一个点的簇分配开始。所以,我们将使用一个 lambda 函数来对我们的原始数据进行转换:

resultRDD = data.map(lambda point: clusters.predict(point)).cache() 

这个函数只是将每个点转换为从我们的模型预测的簇编号。同样,我们只是拿着我们的数据点的 RDD。我们调用clusters.predict来找出我们的 k-means 模型分配给它们的簇,然后我们将结果放入我们的resultRDD中。现在,我想在上面的代码中指出的一件事是这个缓存调用。

在做 Spark 时一个重要的事情是,每当你要在 RDD 上调用多个操作时,首先将其缓存起来是很重要的,因为当你在 RDD 上调用一个操作时,Spark 会去计算它的 DAG,以及如何最优地得到结果。

它将去执行一切以得到结果。所以,如果我在同一个 RDD 上调用两个不同的操作,它实际上会评估那个 RDD 两次,如果你想避免所有这些额外的工作,你可以缓存你的 RDD,以确保它不会被计算超过一次。

通过这样做,我们确保这两个后续操作做了正确的事情:

print ("Counts by value:") 
counts = resultRDD.countByValue() 
print (counts) 

print ("Cluster assignments:") 
results = resultRDD.collect() 
print (results) 

为了得到实际的结果,我们将使用countByValue,它将给我们一个包含每个簇中有多少点的 RDD。记住,resultRDD目前已经将每个单独的点映射到它最终所在的簇,所以现在我们可以使用countByValue来计算每个给定簇 ID 看到多少个值。然后我们可以轻松地打印出那个列表。我们也可以通过在其上调用collect来实际查看该 RDD 的原始结果,并打印出所有的结果。

在一组平方误差和(WSSSE)内

现在,我们如何衡量我们的簇有多好呢?嗯,其中一个度量标准就是被称为簇内平方和误差(WSSSE),哇,听起来很高级!这个术语如此之大,以至于我们需要一个缩写,WSSSE。它就是我们看每个点到它所在簇的质心的距离,每个簇的最终质心,取这个误差的平方并对整个数据集进行求和。它只是衡量每个点距离它所在簇的质心有多远。显然,如果我们的模型中有很多误差,那么它们很可能会远离可能适用的质心,因此我们需要更高的 K 值。我们可以继续计算这个值,并用以下代码打印出来:

def error(point): 
    center = clusters.centers[clusters.predict(point)] 
    return sqrt(sum([x**2 for x in (point - center)])) 

WSSSE = data.map(lambda point: error(point)).reduce(lambda x, y: x + y) 
print("Within Set Sum of Squared Error = " + str(WSSSE)) 

首先,我们定义了这个error函数,它计算每个点的平方误差。它只是取每个点到每个簇的质心的距离,并将它们相加。为了做到这一点,我们取我们的源数据,在其上调用一个 lambda 函数,实际上计算每个质心中心点的误差,然后我们可以在这里链接不同的操作。

首先,我们调用map来计算每个点的误差。然后为了得到代表整个数据集的最终总和,我们对该结果调用reduce。所以,我们使用data.map来计算每个点的误差,然后使用reduce将所有这些误差相加在一起。这就是这个小 lambda 函数的作用。基本上就是一种高级的说法,即“我希望你把这个 RDD 中的所有东西加起来得到一个最终结果”。reduce会一次取整个 RDD 的两个元素,并使用你提供的任何函数将它们组合在一起。我上面提供的函数是“取我要组合在一起的两行,然后把它们加起来”。

如果我们在 RDD 的每个条目中都这样做,最终我们会得到一个总和的总数。这可能看起来有点绕,但通过这种方式做,我们能够确保如果需要的话,我们实际上可以分发这个操作。我们实际上可能会在一台机器上计算数据的总和,而在另一台机器上计算不同部分的总和,然后将这两个总和组合在一起得到最终结果。这个reduce函数是在问,我如何将这个操作的任何两个中间结果组合在一起?

同样,如果你想让它深入你的脑海中,可以随意花点时间盯着它看一会儿。这里没有什么特别复杂的东西,但有一些重要的要点:

  • 我们介绍了缓存的使用,如果你想确保在一个你将要多次使用的 RDD 上不进行不必要的重新计算。

  • 我们介绍了reduce函数的使用。

  • 我们还有一些有趣的映射函数在这里,所以这个例子中有很多可以学习的地方。

最后,它只会执行 k 均值聚类,所以让我们继续运行它。

运行代码

转到工具菜单,Canopy 命令提示符,然后输入:

spark-submit SparkKMeans.py  

按回车,然后它就会运行。在这种情况下,你可能需要等待一段时间才能看到输出,但你应该会看到类似这样的东西:

它起作用了,太棒了!所以记住,我们要求的输出首先是每个簇中有多少点的计数。这告诉我们,簇 0 中有 21 个点,簇 1 中有 20 个点,依此类推。它最终分布得相当均匀,这是一个好迹象。

接下来,我们打印出了每个点的聚类分配,如果你还记得,生成这些数据的原始数据是顺序的,所以看到所有的 3 都在一起,所有的 1 都在一起,所有的 4 都在一起,看起来它开始对 0 和 2 有点困惑,但总的来说,它似乎已经很好地揭示了我们最初创建数据的聚类。

最后,我们计算了 WSSSE 指标,在这个例子中为 19.97。所以,如果你想玩一下,我鼓励你这样做。你可以看到当你增加或减少 K 的值时,错误指标会发生什么变化,并思考为什么会这样。你也可以尝试一下如果不对所有数据进行归一化会发生什么,这实际上是否会以一种有意义的方式影响你的结果?这实际上是否是一件重要的事情?你还可以尝试一下在模型本身上调整maxIterations参数,了解它对最终结果的实际影响以及它的重要性。所以,随意尝试并进行实验。这是使用 MLlib 和 Spark 进行可扩展的 k 均值聚类。非常酷。

TF-IDF

所以,我们 MLlib 的最后一个例子将使用一种称为词项频率逆文档频率(TF-IDF)的东西,这是许多搜索算法的基本构建块。像往常一样,听起来很复杂,但实际上并没有听起来那么糟糕。

所以,首先,让我们谈谈 TF-IDF 的概念,以及我们如何使用它来解决搜索问题。我们实际上要用 TF-IDF 来为维基百科创建一个基本的搜索引擎,使用 Apache Spark 中的 MLlib。多么棒啊?让我们开始吧。

TF-IDF 代表词项频率和逆文档频率,这基本上是两个密切相关的指标,用于进行搜索并确定给定单词与文档的相关性,给定更大的文档集。所以,例如,维基百科上的每篇文章可能都有与之关联的词项频率,互联网上的每个页面可能都有与之关联的词项频率,对于出现在该文档中的每个单词。听起来很花哨,但是,正如你将看到的那样,这是一个相当简单的概念。

  • 所有词项频率的意思就是给定单词在给定文档中出现的频率。所以,在一个网页内,在一个维基百科文章内,在一个任何地方,给定单词在该文档内有多常见?你知道,该单词在该文档中所有单词中出现率的比率是多少?就是这样。这就是词项频率的全部。

  • 文档频率,是相同的概念,但这次是该单词在整个文档语料库中的频率。所以,这个单词在我拥有的所有文档,所有网页,所有维基百科文章中出现的频率有多高。例如,像"a"或"the"这样的常见词汇会有很高的文档频率,我也期望它们在特定文档中也有很高的词项频率,但这并不一定意味着它们与给定文档相关。

你可以看出我们要做什么。所以,假设我们有一个给定单词的词项频率很高,文档频率很低。这两者的比率可以给我一个衡量该单词与文档相关性的指标。所以,如果我看到一个单词在给定文档中经常出现,但在整个文档空间中并不经常出现,那么我知道这个单词可能对这个特定文档传达了一些特殊的含义。它可能传达了这个文档实际上是关于什么。

所以,这就是 TF-IDF。它只是词频 x 逆文档频率的缩写,这只是一种说词频除以文档频率的花哨方式,这只是一种说这个词在这个文档中出现的频率与它在整个文档体中出现的频率相比有多频繁的花哨方式。就是这么简单。

实践中的 TF-IDF

在实践中,我们在使用这个方法时有一些小细节。例如,我们使用逆文档频率的实际对数值,而不是原始值,这是因为实际上单词频率往往呈指数分布。因此,通过取对数,我们最终得到了对单词的稍微更好的加权,考虑到它们的整体流行度。显然,这种方法也有一些局限性,其中之一是我们基本上假设一个文档只是一袋词,我们假设词之间没有关系。显然,这并不总是事实,实际上解析它们可能是工作的一大部分,因为你必须处理同义词和各种时态的词、缩写、大写、拼写错误等。这又回到了清理数据作为数据科学家工作的一个重要部分的想法,特别是当你处理自然语言处理的东西时。幸运的是,有一些库可以帮助你解决这个问题,但这确实是一个真正的问题,它会影响你的结果的质量。

我们在 TF-IDF 中使用的另一个实现技巧是,我们不是存储实际的字符串词及其词频和逆文档频率,为了节省空间并使事情更有效率,我们实际上将每个词映射到一个数值,我们称之为哈希值。这个想法是我们有一个函数,可以取任何词,查看它的字母,并以一种相当均匀分布的方式将其分配给一个数字范围内的一组数字。这样,我们可以用“10”来代表“represented”。现在,如果你的哈希值空间不够大,你可能会得到不同的词被同一个数字表示,这听起来比实际情况要糟糕。但是,你要确保你有一个相当大的哈希空间,这样才不太可能发生。这些被称为哈希冲突。它们可能会引起问题,但实际上,人们在英语中常用的词并不多。你可以用 10 万左右就可以了。

在规模上做到这一点是困难的。如果你想在整个维基百科上做到这一点,那么你将不得不在一个集群上运行这个。但是为了论证,我们现在只是在我们自己的桌面上运行这个,使用维基百科数据的一个小样本。

使用 TF-IDF

我们如何将这转化为一个实际的搜索问题?一旦我们有了 TF-IDF,我们就有了每个词对每个文档相关性的度量。我们该怎么处理呢?嗯,你可以做的一件事是为我们遇到的整个文档体中的每个词计算 TF-IDF,然后,假设我们想搜索一个给定的术语,一个给定的词。比如说我们想搜索“在我的维基百科文章集中,哪篇文章与葛底斯堡最相关?”我可以按照它们对葛底斯堡的 TF-IDF 得分对所有文档进行排序,然后只取前几个结果,这些就是我对葛底斯堡的搜索结果。就是这样。只需取你的搜索词,计算 TF-IDF,取前几个结果。就这样。

显然,在现实世界中,搜索的内容要比这多得多。谷歌有大批人在解决这个问题,实际上这个问题要复杂得多,但这实际上会给你一个能产生合理结果的工作搜索引擎算法。让我们继续深入了解它是如何工作的。

使用 Spark MLlib 搜索维基百科

我们将使用 Apache Spark 在 MLlib 中为维基百科的一部分构建一个实际工作的搜索算法,并且我们将在不到 50 行的代码中完成所有工作。这可能是我们在整本书中做的最酷的事情!

进入您的课程材料,打开TF-IDF.py脚本,这将打开 Canopy,并显示以下代码:

现在,暂停一下,让它沉淀下来,我们实际上正在创建一个工作的搜索算法,以及在不到 50 行的代码中使用它的一些示例,而且它是可扩展的。我可以在集群上运行这个。这有点令人惊讶。让我们逐步了解代码。

导入语句

我们将首先导入我们在 Python 中运行任何 Spark 脚本所需的SparkConfSparkContext库,然后使用以下命令导入HashingTFIDF

from pyspark import SparkConf, SparkContext 
from pyspark.mllib.feature import HashingTF 
from pyspark.mllib.feature import IDF 

所以,这就是计算我们文档中的词项频率(TF)和逆文档频率(IDF)的方法。

创建初始 RDD

我们将从创建本地SparkConfigurationSparkContext的样板 Spark 内容开始,然后我们可以从中创建我们的初始 RDD。

conf = SparkConf().setMaster("local").setAppName("SparkTFIDF") 
sc = SparkContext(conf = conf) 

接下来,我们将使用我们的SparkContextsubset-small.tsv创建一个 RDD。

rawData = sc.textFile("e:/sundog-consult/Udemy/DataScience/subset-small.tsv") 

这是一个包含制表符分隔值的文件,它代表了维基百科文章的一个小样本。同样,您需要根据前面的代码所示更改路径,以适应您在本书课程材料安装的位置。

这给我返回了一个 RDD,其中每个文档都在 RDD 的每一行中。tsv文件中的每一行都包含一个完整的维基百科文档,我知道每个文档都分成了包含有关每篇文章的各种元数据的表字段。

接下来我要做的是将它们分开:

fields = rawData.map(lambda x: x.split("\t")) 

我将根据它们的制表符分隔符将每个文档分割成一个 Python 列表,并创建一个新的fields RDD,该 RDD 不再包含原始输入数据,而是包含该输入数据中每个字段的 Python 列表。

最后,我将映射这些数据,接收每个字段列表,提取字段编号三x[3],我碰巧知道这是文章正文,实际的文章文本,然后我将根据空格拆分它:

documents = fields.map(lambda x: x[3].split(" ")) 

x[3]的作用是从每篇维基百科文章中提取文本内容,并将其拆分成一个单词列表。我的新documents RDD 中每个文档都有一个条目,该 RDD 中的每个条目都包含该文档中出现的单词列表。现在,我们实际上知道在评估结果时如何称呼这些文档。

我还将创建一个新的 RDD 来存储文档名称:

documentNames = fields.map(lambda x: x[1]) 

所有它做的就是使用这个map函数从相同的fields RDD 中提取文档名称,我碰巧知道它在字段编号一中。

所以,我现在有两个 RDD,documents,其中包含每个文档中出现的单词列表,以及documentNames,其中包含每个文档的名称。我也知道它们是按顺序排列的,所以我实际上可以稍后将它们组合在一起,以便查找给定文档的名称。

创建和转换 HashingTF 对象

现在,魔术发生了。我们要做的第一件事是创建一个HashingTF对象,并传入一个参数 100,000。这意味着我要将每个单词哈希成 100,000 个数字值中的一个:

hashingTF = HashingTF(100000)  

它不是将单词内部表示为字符串,这样效率很低,而是尝试尽可能均匀地将每个单词分配给唯一的哈希值。我给了它多达 100,000 个哈希值可供选择。基本上,这是将单词映射到数字。

接下来,我将在实际的文档 RDD 上调用hashingTFtransform

tf = hashingTF.transform(documents) 

这将把每个文档中的单词列表转换为哈希值列表,代表每个单词的数字列表。

此时,实际上是以稀疏向量的形式表示,以节省更多的空间。因此,我们不仅将所有单词转换为数字,还剥离了任何缺失的数据。如果一个单词在文档中不存在,您不需要显式存储该单词不存在的事实,这样可以节省更多的空间。

计算 TF-IDF 分数

要计算每个文档中每个单词的 TF-IDF 分数,我们首先缓存这个tf RDD。

tf.cache() 

我们这样做是因为我们将使用它不止一次。接下来,我们使用IDF(minDocFreq=2),这意味着我们将忽略任何出现次数不到两次的单词:

idf = IDF(minDocFreq=2).fit(tf) 

我们在tf上调用fit,然后在下一行上调用transform

tfidf = idf.transform(tf) 

我们最终得到的是每个文档中每个单词的 TF-IDF 分数的 RDD。

使用维基百科搜索引擎算法

让我们尝试并使用该算法。让我们尝试查找单词Gettysburg的最佳文章。如果您对美国历史不熟悉,那就是亚伯拉罕·林肯发表著名演讲的地方。因此,我们可以使用以下代码将单词 Gettysburg 转换为其哈希值:

gettysburgTF = hashingTF.transform(["Gettysburg"]) 
gettysburgHashValue = int(gettysburgTF.indices[0]) 

然后,我们将从该哈希值中提取 TF-IDF 分数到每个文档的新 RDD 中:

gettysburgRelevance = tfidf.map(lambda x: x[gettysburgHashValue])  

这样做的目的是从映射到每个文档的哈希值中提取 Gettysburg 的 TF-IDF 分数,并将其存储在gettysburgRelevance RDD 中。

然后,我们将其与documentNames结合起来,以便查看结果:

zippedResults = gettysburgRelevance.zip(documentNames)  

最后,我们可以打印出答案:

print ("Best document for Gettysburg is:") 
print (zippedResults.max()) 

运行算法

因此,让我们运行一下,看看会发生什么。通常情况下,要运行 Spark 脚本,我们不会只是点击播放图标。我们需要转到工具>Canopy 命令提示符。在打开的命令提示符中,我们将输入spark-submit TF-IDF.py,然后就可以运行了。

尽管这只是维基百科的一个小样本,但我们要求它处理相当多的数据,因此可能需要一些时间。让我们看看为 Gettysburg 找到的最佳文档匹配是什么,哪个文档具有最高的 TF-IDF 分数?

这是亚伯拉罕·林肯!这不是很棒吗?我们只需几行代码就制作了一个真正有效的搜索引擎。

这就是使用 Spark 在 MLlib 和 TF-IDF 中实际工作的搜索算法。美妙的是,如果我们有足够大的集群来运行它,我们实际上可以将其扩展到整个维基百科。

希望我们引起了您对 Spark 的兴趣,您可以看到它如何应用于以分布式方式解决相当复杂的机器学习问题。因此,这是一个非常重要的工具,我希望您在阅读本数据科学书籍时,至少要了解 Spark 如何应用于大数据问题的概念。因此,当您需要超越单台计算机的能力时,请记住,Spark 可以为您提供帮助。

使用 Spark 2.0 DataFrame API 进行 MLlib

本章最初是为 Spark 1 制作的,因此让我们谈谈 Spark 2 中的新功能以及 MLlib 现在存在的新功能。

因此,Spark 2 的主要特点是它越来越向 Dataframes 和 Datasets 迈进。有时 Datasets 和 Dataframes 有点交替使用。从技术上讲,Dataframe 是一组行对象的 Dataset,它们有点像 RDD,但唯一的区别在于,RDD 只包含非结构化数据,而 Dataset 具有定义的模式。

Dataset 提前知道每行中存在的信息列以及这些信息的类型。因为它提前知道该 Dataset 的实际结构,所以它可以更有效地优化事物。它还让我们将该 Dataset 的内容视为一个小型数据库,实际上,如果它在集群上,那就是一个非常大的数据库。这意味着我们可以对其执行 SQL 查询等操作。

这创建了一个更高级的 API,我们可以在 Spark 集群上查询和分析大型数据集。这是相当酷的东西。它更快,有更多的优化机会,并且有一个更高级的 API,通常更容易使用。

Spark 2.0 MLlib 的工作原理

在 Spark 2.0 中,MLlib 正在将数据框架作为其主要 API。这是未来的发展方向,所以让我们看看它是如何工作的。我已经打开了 Canopy 中的SparkLinearRegression.py文件,如下图所示,让我们来看一下:

正如你所看到的,首先,我们使用ml而不是MLlib,这是因为新的基于数据框架的 API 在其中。

实施线性回归

在这个例子中,我们要做的是实现线性回归,线性回归只是一种将一条线拟合到一组数据的方法。在这个练习中,我们将使用两个维度中的一堆虚构数据,并尝试用线性模型拟合一条线。

我们将数据分成两组,一组用于构建模型,一组用于评估模型,并比较这个线性模型在实际预测真实值时的表现。首先,在 Spark 2 中,如果要使用SparkSQL接口并使用数据集,你必须使用SparkSession对象而不是SparkContext。要设置一个,你可以这样做:

spark = SparkSession.builder.config("spark.sql.warehouse.dir", "file:///C:/temp").appName("LinearRegression").getOrCreate() 

请注意,中间部分只在 Windows 和 Spark 2.0 中才需要。说实话,这是为了解决一个小 bug。所以,如果你在 Windows 上,请确保你有一个C:/temp文件夹。如果你想运行这个程序,如果需要的话现在就创建它。如果你不在 Windows 上,你可以删除整个中间部分,留下:spark = SparkSession.builder.appName("LinearRegression").getOrCreate()

好的,所以你可以说spark,给它一个appNamegetOrCreate()

这很有趣,因为一旦你创建了一个 Spark 会话,如果它意外终止,你实际上可以在下次运行时从中恢复。所以,如果我们有一个检查点目录,它可以使用getOrCreate在上次中断的地方重新启动。

现在,我们将使用我提供的regression.txt文件:

inputLines = spark.sparkContext.textFile("regression.txt")  

这只是一个文本文件,其中有两列逗号分隔的值,它们只是两列,或多或少地,线性相关的数据。它可以代表任何你想要的东西。比如,我们可以想象它代表身高和体重。所以,第一列可能代表身高,第二列可能代表体重。

在机器学习的术语中,我们谈论标签和特征,其中标签通常是你要预测的东西,而特征是数据的一组已知属性,你用它来进行预测。

在这个例子中,也许身高是标签,体重是特征。也许我们试图根据你的体重来预测身高。它可以是任何东西,都无所谓。这一切都被归一化到-1 到 1 之间的数据。数据的规模没有真正的意义,你可以假装它代表任何你想要的东西。

要在 MLlib 中使用这个,我们需要将我们的数据转换成它期望的格式:

data = inputLines.map(lambda x: x.split(",")).map(lambda x: (float(x[0]), Vectors.dense(float(x[1]))))  

我们要做的第一件事是使用map函数将数据拆分成两个不同的值列表,然后将其映射到 MLlib 期望的格式。这将是一个浮点标签,然后是特征数据的密集向量。

在这种情况下,我们只有一个特征数据,即重量,所以我们有一个只包含一个元素的向量,但即使只有一个元素,MLlib 线性回归模型也需要一个密集向量。这就像旧 API 中的labeledPoint,但我们必须用更麻烦的方式来做。

接下来,我们需要为这些列实际分配名称。以下是执行此操作的语法:

colNames = ["label", "features"] 
df = data.toDF(colNames) 

我们将告诉 MLlib,结果 RDD 中的这两列实际上对应于标签和特征,然后我可以将该 RDD 转换为 DataFrame 对象。此时,我有一个实际的数据框,或者说,一个包含两列标签和特征的数据集,其中标签是浮点高度,特征列是浮点权重的密集向量。这是 MLlib 所需的格式,而 MLlib 对此可能会很挑剔,因此重要的是您注意这些格式。

现在,就像我说的,我们要把我们的数据分成两半。

trainTest = df.randomSplit([0.5, 0.5]) 
trainingDF = trainTest[0] 
testDF = trainTest[1] 

我们将在训练数据和测试数据之间进行 50/50 的拆分。这将返回两个数据框,一个用于创建模型,一个用于评估模型。

接下来,我将使用一些标准参数创建我的实际线性回归模型。

lir = LinearRegression(maxIter=10, regParam=0.3, elasticNetParam=0.8) 

我们将调用lir = LinearRegression,然后我将把该模型拟合到我留出用于训练的数据集上,即训练数据框:

model = lir.fit(trainingDF) 

这将使我得到一个模型,我可以用它来进行预测。

让我们继续做吧。

fullPredictions = model.transform(testDF).cache() 

我将调用model.transform(testDF),这将根据我的测试数据集中的权重预测身高。我实际上有已知的标签,即实际的正确身高,这将在该数据框中添加一个名为预测的新列,其中包含基于该线性模型的预测值。

我将缓存这些结果,现在我可以提取它们并将它们进行比较。因此,让我们提取预测列,就像在 SQL 中使用select一样,然后我将实际转换该数据框并从中提取 RDD,并使用它将其映射到这种情况下的一组浮点高度:

predictions = fullPredictions.select("prediction").rdd.map(lambda x: x[0]) 

这些是预测的身高。接下来,我们将从标签列中获取实际的身高:

labels = fullPredictions.select("label").rdd.map(lambda x: x[0]) 

最后,我们可以将它们重新组合在一起,然后将它们并排打印出来,看看效果如何:

predictionAndLabel = predictions.zip(labels).collect() 

for prediction in predictionAndLabel: 
    print(prediction) 

spark.stop() 

这种方法有点复杂;我之所以这样做是为了与之前的示例保持一致,但更简单的方法是实际上选择预测和标签,将它们合并成一个 RDD,将这两列一起映射出来,然后我就不必将它们合并在一起,但无论哪种方法都可以。您还会注意到,在最后,我们需要停止 Spark 会话。

让我们看看它是否有效。让我们转到工具,Canopy 命令提示符,然后输入spark-submit SparkLinearRegression.py,看看会发生什么。

实际上,使用数据集运行这些 API 需要更多的前期时间,但一旦开始,它们就非常快。好了,就是这样。

在这里,我们将实际值和预测值并排放在一起,您可以看到它们并不太糟糕。它们往往在同一范围内。就是这样,使用 Spark 2.0 进行线性回归模型,使用 MLlib 的基于新数据框的 API。今后,您将越来越多地使用这些 API 来进行 Spark 中的 MLlib,因此请尽量选择这些 API。好了,这就是 Spark 中的 MLlib,一种实际上可以在整个集群上分发大规模计算任务以处理大型数据集的机器学习方法。这是一个很好的技能。让我们继续。

总结

在本章中,我们从安装 Spark 开始,然后深入介绍了 Spark,同时了解了 Spark 与 RDD 的结合工作原理。我们还通过探索不同的操作方式,介绍了创建 RDD 的各种方法。然后我们介绍了 MLlib,并详细介绍了 Spark 中决策树和 K-Means 聚类的一些示例。然后我们通过使用 TF-IDF 仅需几行代码就创建了一个搜索引擎。最后,我们看了一下 Spark 2.0 的新功能。

在下一章中,我们将介绍 A/B 测试和实验设计。

第十章:测试和实验设计

在本章中,我们将了解 A/B 测试的概念。我们将深入研究 t 检验、t 统计量和 p 值,这些都是用于确定结果是否真实或是随机变化结果的有用工具。我们将深入一些真实的例子,并用一些 Python 代码进行实践,并计算 t 统计量和 p 值。

接下来,我们将探讨在达成结论之前应该运行实验多长时间。最后,我们将讨论可能影响实验结果并导致您得出错误结论的潜在问题。

我们将涵盖以下主题:

  • A/B 测试概念

  • T 检验和 p 值

  • 使用 Python 测量 t 统计量和 p 值

  • 确定实验运行时间

  • A/B 测试的陷阱

A/B 测试概念

如果您在一家网络公司担任数据科学家,您可能会被要求花一些时间分析 A/B 测试的结果。这些基本上是网站上的受控实验,用于衡量给定更改的影响。因此,让我们谈谈 A/B 测试是什么以及它们是如何工作的。

A/B 测试

如果您将成为一家大型科技网络公司的数据科学家,这是您肯定会参与的事情,因为人们需要进行实验,尝试网站上的不同事物,并衡量其结果,这实际上并不像大多数人认为的那样简单。

什么是 A/B 测试?嗯,这是一个通常在网站上进行的受控实验,也可以应用于其他情境,但通常我们谈论的是网站,并且我们将测试对网站的某些更改的性能,与之前的方式进行比较。

基本上,您有一组控制看到旧网站的人,还有一组测试看到网站更改的人,这个想法是测量这两组人之间的行为差异,并使用这些数据来实际决定这个更改是否有益。

例如,我拥有一家有网站的企业,我们向人们许可软件,现在我有一个友好的橙色按钮,人们在想购买许可证时点击它,如下图左侧所示。但是,如果我将该按钮的颜色更改为蓝色,如右侧所示,会发生什么?

因此,在这个例子中,如果我想找出蓝色是否更好。我怎么知道呢?

我的意思是,直觉上,也许那可能更能吸引人们的注意,或者直觉上,也许人们更习惯于看到橙色的购买按钮,并更有可能点击它,我可以两种方式来解释,对吧?因此,我的内在偏见或先入之见并不重要。重要的是人们如何对我网站上的这种更改做出反应,这就是 A/B 测试的作用。

A/B 测试将人们分为看到橙色按钮的人和看到蓝色按钮的人,然后我可以测量这两组人之间的行为以及它们可能有何不同,并根据这些数据做出关于按钮颜色的决定。

您可以使用 A/B 测试测试各种事物。这些包括:

  • 设计更改:这些可以是按钮颜色的更改、按钮的放置位置或页面的布局。

  • 用户界面流程:因此,也许您实际上正在更改购买流程的方式以及人们在网站上结账的方式,您实际上可以衡量其影响。

  • 算法变更:让我们考虑在第六章中讨论的电影推荐的例子,推荐系统。也许我想测试一个算法与另一个算法。我真正关心的不是依赖于错误指标和我的训练测试能力,而是关心如何在网站上推动购买或租赁或其他任何事情。

  • A/B 测试可以让我直接衡量这种算法对我真正关心的最终结果的影响,而不仅仅是我预测其他人已经看过的电影的能力。

  • 还有其他任何您能想到的事情,任何影响用户与您的网站互动的变化都值得测试。也许甚至是使网站更快,或者任何其他事情。

  • 定价变化:这个有点具有争议性。理论上,您可以使用 A/B 测试尝试不同的价格点,并查看它是否实际增加了销量以抵消价格差异,但是要谨慎使用这个方法。

  • 如果顾客得知其他人因为没有好的原因而得到了更优惠的价格,他们就不会对您感到满意。请记住,进行定价实验可能会产生负面反弹,您不希望陷入这种情况。

A/B 测试的转化测量

在设计网站实验时,您需要弄清楚的第一件事是,您试图优化什么?您真正想通过这个变化推动什么?这并不总是一个非常明显的事情。也许是人们的花费金额,收入的数量。我们已经讨论了使用花费金额的方差问题,但是如果您有足够的数据,很多时候您仍然可以收敛于这个指标。

然而,也许这并不是您真正想要优化的。也许您实际上是故意以亏损的价格销售某些商品,只是为了占领市场份额。您的定价策略比仅仅是顶线收入更加复杂。

也许您真正想要衡量的是利润,这可能是一个非常棘手的事情,因为许多因素会影响产品的盈利,而这些因素可能并不总是显而易见的。如果您有亏损产品,这个实验将忽略这些产品本应产生的效果。也许您只关心在网站上推动广告点击,或者订单数量以减少方差,也许人们对此无所谓。

最重要的是,您必须与正在进行测试的业务所有者交谈,并弄清楚他们试图优化什么。他们被衡量在什么上?他们的成功是如何衡量的?他们的关键绩效指标或者无论 NBAs 想称呼它什么?并确保我们正在衡量对他们来说最重要的事情。

您也可以同时测量多个指标,不必选择一个,实际上可以报告许多不同事物的影响:

  • 收入

  • 利润

  • 点击

  • 广告展示次数

如果所有这些事情都朝着正确的方向发展,那就是这种变化在多方面产生了积极影响的非常强有力的迹象。那么,为什么要限制自己只关注一个指标呢?只需确保您知道在实验成功的标准中哪个指标最重要。

如何归因转化

另一件需要注意的事情是将转化归因于下游的变化。如果您试图推动的行为不是用户立即在体验到您正在测试的事物后发生的,情况就会变得有些棘手。

假设我改变了 A 页面上按钮的颜色,用户然后转到 B 页面并做了其他事情,最终从 C 页面购买了东西。

那么,谁应该得到这次购买的功劳?是 A 页面,还是 B 页面,还是介于两者之间的某个页面?我是否应根据用户点击次数来折扣转化的功劳?我是否应该丢弃任何不是在看到变化后立即发生的转化行为?这些都是复杂的事情,通过调整您对转化和您正在测量的变化之间的不同距离的计算方式,很容易产生误导性的结果。

方差是您的敌人

另一件你需要真正内化的事情是,方差是你进行 A/B 测试时的敌人。

一个非常常见的错误是,那些不懂得如何运用数据科学的人会在网页上进行测试,比如蓝色按钮对比橙色按钮,然后运行一周,然后从每个组中得到平均花费金额。然后他们会说:“哦看!平均而言,点击蓝色按钮的人比点击橙色按钮的人多花了一美元;蓝色太棒了,我喜欢蓝色,我要在整个网站上都用蓝色了!”

但实际上,他们可能只是看到了购买的随机变化。他们没有足够大的样本,因为人们不倾向于购买很多。你的网站可能有很多浏览量,但与此相比,你可能没有很多购买量,而且这些购买金额可能有很大的差异,因为不同的产品成本不同。

因此,如果你不了解这些结果对方差的影响,你很容易做出错误的决定,最终会让你的公司损失金钱,而不是赚钱。我们将在本章后面讨论一些测量和考虑这一点的主要方法。

你需要确保你的业务所有者明白这是一个重要的影响,你需要在进行 A/B 测试或者在网站上进行的任何实验之后,做出商业决策之前,对其进行量化和理解。

有时候你需要选择一个方差较小的转化指标。可能是你网站上的数字意味着你必须运行多年的实验才能得到一个基于收入或花费金额的显著结果。

有时,如果你正在观察多个指标,比如订单金额或订单数量,它的方差较小,你可能会在订单数量上看到信号,而在收入上看不到信号,例如。最终,这取决于判断。如果你看到订单数量有显著增加,而收入增长不那么显著,那么你必须说:“嗯,我认为这里可能有一些真实和有益的事情发生。”

然而,统计和数据大小能告诉你的唯一的是,一个效应是真实的概率。最终,你必须决定它是否是真实的。所以,让我们更详细地讨论如何做到这一点。

这里的关键是,仅仅看平均值的差异是不够的。当你试图评估实验结果时,你需要考虑方差。

t 检验和 p 值

A/B 测试产生的变化是否真的是你所改变的结果,还是只是随机变化?嗯,我们有一些统计工具可以使用,叫做 t 检验或 t 统计量,以及 p 值。让我们更多地了解一下它们是什么,以及它们如何帮助你确定一个实验是否有效。

目标是弄清楚一个结果是否是真实的。这只是数据本身固有的随机变化的结果,还是我们看到了控制组和测试组之间的实际、统计显著的行为变化?t 检验和 p 值是计算这一点的一种方法。

记住,“统计显著性”并没有一个具体的含义。最终,这必须是一个判断。你必须选择一个概率值,你会接受一个结果是真实的或不真实的。但仍然会有可能是随机变化的结果,你必须确保你的利益相关者明白这一点。

t 统计量或 t 检验。

让我们从t-统计开始,也被称为 t-检验。它基本上是衡量这两组行为之间的差异的一种方式,即你的控制组和处理组之间的差异,以标准误差的单位表示。它基于标准误差,考虑了数据本身固有的方差,因此通过将一切都标准化为标准误差,我们得到了一些考虑到方差的这两组行为变化的度量。

解释 t-统计的方法是,高 t 值意味着这两组之间可能存在真正的差异,而低 t 值意味着差异不大。你必须决定你愿意接受的门槛是多少?t-统计的符号将告诉你这是一个正向还是负向的变化。

如果你将你的控制组与处理组进行比较,最终得到一个负的 t-统计,这意味着这是一个不好的改变。你最终希望 t-统计的绝对值很大。什么样的 t-统计值被认为是大的?这是有争议的。我们很快会看一些例子。

现在,这假设了你有一个正态分布的行为,当我们谈论人们在网站上的花费时,这通常是一个合理的假设。人们的花费往往有一个正态分布。

然而,还有更精细的 t-统计的版本,你可能想要针对其他特定情况进行研究。例如,当你谈论点击率时,有一种叫做费舍尔精确检验的东西,当你谈论每个用户的交易时,比如他们看了多少网页,有E-检验,还有卡方检验,通常与订单数量有关。有时你会想要查看给定实验的所有这些统计数据,并选择最适合你所尝试做的事情的那个。

p 值

现在,谈论 p 值比 t-统计要容易得多,因为你不必考虑,我们谈论多少个标准偏差?实际值是什么意思?p 值对人们来说更容易理解,这使得它成为一个更好的工具,用来向你业务中的利益相关者传达实验结果。

p 值基本上是这个实验满足零假设的概率,也就是说,控制组和处理组的行为之间没有真正的差异的概率。低 p 值意味着它没有影响的概率很低,有点双重否定的意思,所以这有点反直觉,但最终你只需要明白,低 p 值意味着你的改变有真正的影响的概率很高。

你想要看到的是高 t-统计和低 p-值,这将意味着显著的结果。现在,在你开始实验之前,你需要决定你的成功门槛是多少,并且这意味着与业务负责人一起决定门槛。

那么,你愿意接受什么样的 p 值作为成功的衡量标准?是 1%?是 5%?再次强调,这基本上是没有真正效应的可能性,只是随机方差的结果。这最终是一个判断。很多时候人们使用 1%,有时如果他们感觉有点冒险,他们会使用 5%,但总会有那种可能性,你的结果只是偶然的,是随机数据。

然而,你可以选择愿意接受的概率,认为这是一个真正的效应,值得投入生产。

当你的实验结束时,我们稍后会讨论何时宣布实验结束,你需要测量你的 p 值。如果它小于你决定的阈值,那么你可以拒绝零假设,并且可以说“嗯,有很高的可能性,这种变化产生了真正的正面或负面结果。”

如果结果是正面的,那么你可以将这种变化推广到整个网站,它不再是一个实验,而是你网站的一部分,希望随着时间的推移能给你带来更多的收入,如果结果是负面的,你希望在它给你造成更多损失之前摆脱它。

记住,当你的实验结果是负面的时候,运行 A/B 测试是有真正成本的。所以,你不想运行太长时间,因为有可能会亏钱。

这就是为什么你要每天监控实验结果,所以如果有早期迹象表明这种变化对网站造成了可怕的影响,也许有 bug 或者其他可怕的东西,你可以在必要时提前终止它,并限制损失。

让我们看一个实际的例子,看看如何使用 Python 测量 t 统计量和 p 值。

使用 Python 测量 t 统计量和 p 值

让我们制造一些实验数据,并使用 t 统计量和 p 值来确定给定实验结果是否是真实效果。我们将实际制造一些假的实验数据,并对它们进行 t 统计量和 p 值的计算,看看它是如何工作的,以及如何在 Python 中计算它。

在一些实验数据上运行 A/B 测试

假设我们在一个网站上运行 A/B 测试,我们已经随机将用户分为两组,A 组和 B 组。A 组将成为我们的测试对象,我们的处理组,而 B 组将成为我们的对照组,基本上是网站以前的样子。我们将使用以下代码设置这个:

import numpy as np 
from scipy import stats 

A = np.random.normal(25.0, 5.0, 10000) 
B = np.random.normal(26.0, 5.0, 10000) 

stats.ttest_ind(A, B) 

在这个代码示例中,我们的处理组(A)将具有随机分布的购买行为,他们平均每笔交易花费 25 美元,标准差为五,样本量为一万,而旧网站的平均每笔交易为 26 美元,标准差和样本量相同。我们基本上在看一个实验结果是负面的实验。要计算 t 统计量和 p 值,你只需要使用 scipy 中的 stats.ttest_ind 方法。你只需要将处理组和对照组传递给它,就会得到 t 统计量,如下图所示:

在这种情况下,我们有一个 t 统计量为 -14。负号表示这是一个负面的变化,这是一件坏事。而 p 值非常非常小。因此,这意味着这种变化是由随机机会产生的可能性极低。

记住,为了宣布显著性,我们需要看到一个高 t 值 t 统计量和一个低 p 值。

这正是我们在这里看到的,我们看到 -14,这是一个非常高的 t 统计量的绝对值,负号表示这是一件坏事,而极低的 P 值告诉我们,几乎没有可能这只是随机变化的结果。

如果你在现实世界中看到这些结果,你会尽快终止这个实验。

当两组之间没有真正的差异时

作为一个理智的检查,让我们改变一下,使得这两组之间没有真正的差异。所以,我要改变 B 组,在这种情况下是对照组,使其与处理组相同,其中均值为 25,标准差不变,样本量也不变,如下所示:

B = np.random.normal(25.0, 5.0, 10000) 

stats.ttest_ind(A, B) 

如果我们继续运行这个实验,你会看到我们的 t 检验结果现在低于一:

请记住,这是标准差的问题。因此,这意味着除非我们的 p 值更高,超过 30%,否则那里可能没有真正的变化。

现在,这些仍然是相对较高的数字。您可以看到随机变化可能是一种隐匿的东西。这就是为什么您需要提前决定 p 值的可接受限制。

您知道,您事后可能会看到这一点并说,“30%的几率,你知道,那还不错,我们可以接受”,但是,实际上,您希望看到的是低于 5%的 p 值,理想情况下是低于 1%,而 30%的值意味着实际上并不是一个强有力的结果。因此,不要在事后为其辩护,进入实验时要知道您的阈值是多少。

样本量是否有影响?

让我们对样本量进行一些更改。我们在相同条件下创建这些集合。让我们看看通过增加样本量是否实际上会在行为上产生差异。

样本量增加到六位数

所以,我们将从10000增加到100000个样本,如下所示:

A = np.random.normal(25.0, 5.0, 100000) 
B = np.random.normal(25.0, 5.0, 100000) 

stats.ttest_ind(A, B) 

在以下输出中,您可以看到实际上 p 值略低,t 检验略高,但仍不足以宣布真正的差异。它实际上是朝着你不希望的方向发展的?挺有趣的!

但这些仍然是高值。再次强调,这只是随机变异的影响,它可能比您意识到的要大。特别是在网站上,当您谈论订单金额时。

样本量增加到七位数

让我们将样本量实际增加到1000000,如下所示:

A = np.random.normal(25.0, 5.0, 1000000) 
B = np.random.normal(25.0, 5.0, 1000000) 

stats.ttest_ind(A, B) 

这是结果:

那会有什么影响呢?现在,我们的 t 统计量又低于 1,而我们的值约为 35%。

随着样本量的增加,我们会看到这种波动在某种程度上有所变化。这意味着从 10000 个样本增加到 100000 个样本再到 1000000 个样本,最终结果不会改变。进行这种实验是了解您可能需要运行实验的时间的一种好方法。需要多少样本才能得到显著结果?如果您事先了解数据的分布情况,您实际上可以运行这些模型。

A/A 测试

如果我们将集合与自身进行比较,这被称为 A/A 测试,如下面的代码示例所示:

stats.ttest_ind(A, A) 

我们可以在以下输出中看到,t 统计量为0,p 值为1.0,因为实际上这些集合之间根本没有任何差异。

现在,如果您使用真实的网站数据进行运行,您观察到相同的人群并且看到不同的值,这表明您运行测试的系统本身存在问题。归根结底,就像我说的,这都是一种判断。

继续尝试,看看不同标准差对初始数据集或均值差异以及不同样本量的影响。我只是希望您深入研究,尝试运行这些不同的数据集,看看它们对 t 统计量和 p 值的影响。希望这能让您更直观地理解如何解释这些结果。

再次强调的重要一点是,您要寻找一个较大的 t 统计量和一个较小的 p 值。P 值可能是您想要向业务传达的内容。记住,p 值越低越好,最好在单个数字以下,理想情况下在 1%以下,然后再宣布胜利。

我们将在本章的其余部分更多地讨论 A/B 测试。SciPy 使得计算给定数据集的 t 统计量和 p 值变得非常容易,因此你可以非常容易地比较控制组和处理组之间的行为,并测量这种效果是真实的概率还是仅仅是随机变化的结果。确保你关注这些指标,并且在进行比较时测量你关心的转化指标。

确定运行实验的时间长短

你运行实验多长时间?实际上要得到结果需要多长时间?在什么时候放弃?让我们更详细地讨论一下。

如果你公司的某人开发了一个新的实验,一个他们想要测试的新变化,那么他们对于看到它成功有着切身利益。他们投入了大量的工作和时间,他们希望它能够成功。也许你已经进行了几周的测试,但仍然没有在这个实验上取得显著的结果,无论是积极的还是消极的。你知道他们会希望继续无限期地运行它,希望最终能够显示出积极的结果。你需要决定你愿意运行这个实验多长时间。

我怎么知道何时结束 A/B 测试?我的意思是,预测在你能够取得显著结果之前需要多长时间并不总是直截了当的,但显然,如果你取得了显著结果,如果你的 p 值已经低于 1%或 5%或你选择的任何阈值,那么你就结束了。

在那一点上,你可以中止实验,要么更广泛地推出变化,要么移除它,因为它实际上产生了负面影响。你总是可以告诉人们重新尝试,利用他们从实验中学到的东西,也许做一些改变再试一次,减轻一点打击。

另一种可能发生的情况是根本没有收敛。如果你在 p 值上没有看到任何趋势,那可能是一个好迹象,表明你不会很快看到这种收敛。无论你运行多长时间,它都不会对行为产生足够的影响,甚至无法测量。

在这些情况下,你每天想做的是为给定实验绘制一个图表,显示 p 值、t 统计量,或者你用来衡量这个实验成功的任何东西,如果你看到一些有希望的东西,你会发现 p 值随着时间的推移而下降。因此,它获得的数据越多,你的结果就应该变得更加显著。

现在,如果你看到的是一条平直的线,或者一条到处都是的线,那就告诉你 p 值不会有任何变化,无论你运行这个实验多长时间,它都不会发生。你需要事先达成一致,即在你没有看到 p 值的任何趋势的情况下,你愿意运行这个实验多长时间?是两周?还是一个月?

另一件需要记住的事情是,同时在网站上运行多个实验可能会混淆你的结果。

实验所花费的时间是一种宝贵的资源,你无法在世界上创造更多的时间。在一年内,你只能运行尽可能多的实验。因此,如果你花费太多时间运行一个几乎没有机会收敛到结果的实验,那么你就错过了在这段时间内运行另一个潜在更有价值的实验的机会。

在实验链接上划清界限是很重要的,因为当你在网站上进行 A/B 测试时,时间是非常宝贵的,至少在你有更多的想法而没有时间的情况下,这种情况希望是存在的。确保你在进行给定实验测试的时间上设定了上限,如果你没有看到 p 值中令人鼓舞的趋势,那么就是时候停止实验了。

A/B 测试的陷阱

我想要强调的一个重要观点是,即使你使用 p 值以一种合理的方式来衡量 A/B 测试的结果,这也不是绝对的。有很多因素实际上可能会扭曲你实验的结果,并导致你做出错误的决定。让我们来看看 A/B 测试中的一些陷阱,以及如何注意避免它们。让我们谈谈 A/B 测试的一些陷阱。

说一个实验的 p 值为 1%,听起来很正式,意味着某个实验结果是由偶然结果或随机变化引起的可能性只有 1%,但这仍然不是衡量实验成功的全部和终极标准。有很多因素可能会扭曲或混淆你的结果,你需要意识到这一点。所以,即使你看到一个非常令人鼓舞的 p 值,你的实验仍然可能在欺骗你,你需要了解可能导致这种情况发生的因素,以免做出错误的决定。

记住,相关性不意味着因果关系。

即使进行了精心设计的实验,你只能说这种效果有一定的概率是由你所做的改变引起的。

最终,总会有可能没有真正的效果,或者你甚至可能在测量错误的效果。这可能仍然是随机事件,可能还有其他事情发生,你有责任确保业主明白这些实验结果需要被解释,它们只是决策的一部分。

它们不能成为他们决策的全部和终极标准,因为结果中存在误差,并且有可能扭曲这些结果。如果这种改变还有一些更大的商业目标,而不仅仅是驱动短期收入,那么这也需要考虑在内。

新奇效应

一个问题是新奇效应。A/B 测试的一个主要弱点是它们倾向于运行的短时间范围,这会导致一些问题。首先,改变可能会产生长期效果,而你无法测量到这些效果,但也有一定效果,因为网站上的某些东西变得与众不同。

例如,也许你的客户习惯于在网站上一直看到橙色按钮,如果出现蓝色按钮,它会因为与众不同而吸引他们的注意。然而,随着新客户的到来,他们从未见过你的网站,他们不会注意到这种不同,随着时间的推移,即使是你的老客户也会习惯新的蓝色按钮。很可能,如果你在一年后进行同样的测试,结果可能没有任何差异,或者可能会相反。

我很容易能想象到这样一种情况:你测试橙色按钮和蓝色按钮,前两周蓝色按钮获胜。人们购买更多,因为他们更喜欢它,因为它与众不同。但一年过去了,我可能可以再次进行实验,将蓝色按钮与橙色按钮对比,橙色按钮会再次获胜,仅仅因为橙色按钮与众不同,新颖,吸引人们的注意力。

因此,如果你做出了一些有争议的改变,最好的办法是稍后重新运行实验,看看是否能够复制其结果。这实际上是我知道的唯一解决新奇效应的方法;当它不再新奇时再次进行测量,当它不再只是因为不同而吸引人们注意的改变时。

我真的无法低估理解这一点的重要性。这可能会扭曲很多结果,使你倾向于将积极的变化归因于那些实际上并不值得的事情。在这种情况下,仅仅因为与众不同并不是一种美德。

季节性影响

如果你在圣诞节期间进行实验,人们的行为不会像在其他时间一样。他们在那个季节的花钱方式肯定不同,他们在家里花更多时间,可能有点放松,所以人们的心态不同。

这甚至可能与天气有关,夏天人们的行为会有所不同,因为天气炎热,他们感到有点懒散,更经常度假。也许如果你碰巧在高人口密集地区的一次可怕风暴期间进行实验,这也可能会扭曲你的结果。

再次,只需注意潜在的季节性影响,节假日是需要注意的重要因素,如果实验是在已知具有季节性的时间段运行的,那么始终要以一颗谨慎的心对待你的经验。

你可以通过定量方法来确定这一点,实际上观察你试图衡量的指标作为成功指标的行为,无论你称之为什么,你的转化指标,然后观察它在去年同一时间段的行为。你是否看到每年都有季节性波动?如果是这样,你就要尽量避免在这些高峰或低谷期间进行实验。

选择偏差

另一个可能会扭曲你的结果的潜在问题是选择偏差。非常重要的是,顾客被随机分配到你的对照组或处理组,你的 A 组或 B 组。

然而,有微妙的方式使得那种随机分配实际上可能并不那么随机。例如,假设你正在对顾客 ID 进行哈希处理,以将它们放入一个桶中。也许在哈希函数如何影响较低顾客 ID 和较高顾客 ID 的人之间存在一些微妙的偏差。这可能导致将所有长期忠诚的顾客放入对照组,将那些不太了解你的新顾客放入处理组。

那时你所测量的只是老客户和新客户之间的行为差异。审计你的系统非常重要,以确保在将人们分配到对照组或处理组时没有选择偏差。

你还需要确保分配是固定的。如果你在整个会话期间测量了一项变化的影响,你需要测量他们是否在 A 页面看到了变化,但是在 C 页面上他们实际上进行了转化,你必须确保他们在这些点击之间没有切换组。因此,你需要确保在给定的会话中,人们保持在同一组中,而如何定义一个会话也可能变得有点模糊。

这些都是使用像 Google 实验、Optimizely 或类似公司的成熟现成框架可以帮助解决的问题,这样你就不必在所有这些问题上重新发明轮子。如果你的公司有自己开发的内部解决方案,因为他们不愿意与外部公司分享数据,那么审计是否存在选择偏差是值得的。

审计选择偏差问题

审计选择偏差问题的一种方法是运行所谓的 A/A 测试,就像我们之前看到的那样。因此,如果你实际上进行了一个实验,处理组和对照组之间没有差异,你不应该在最终结果中看到差异。当你比较这两个事物时,行为不应该有任何改变。

A/A 测试可以是测试你的 A/B 框架本身的好方法,并确保没有固有的偏见或其他问题,例如会话泄漏等,这些都需要解决。

数据污染

另一个大问题是数据污染。我们详细讨论了清理输入数据的重要性,尤其是在 A/B 测试的背景下。如果你的网站上有一个机器人,一个恶意的网络爬虫一直在爬取你的网站,进行不自然的交易量,会发生什么?如果那个机器人最终被分配到处理组或对照组呢?

一个机器人可能会扭曲你实验的结果。研究进入你的实验的输入非常重要,寻找异常值,然后分析这些异常值,以及它们是否应该被排除。你是否真的让一些机器人泄漏到你的测量中,并且它们是否扭曲了你实验的结果?这是一个非常常见的问题,你需要意识到这一点。

有恶意的机器人存在,有人试图入侵你的网站,也有善意的爬虫只是为了搜索引擎或其他目的爬取你的网站。网站上存在各种奇怪的行为,你需要过滤掉这些行为,找到真正的客户,而不是这些自动脚本。这实际上可能是一个非常具有挑战性的问题。这也是使用像 Google Analytics 这样的现成框架的另一个原因,如果你可以的话。

归因错误

我们之前简要谈到了归因错误。如果你实际上使用了变化的下游行为,那就会涉及到一个灰色地带。

你需要了解如何根据距离的函数来计算这些转化,并与你的业务利益相关者事先达成一致,以确定你将如何衡量这些影响。你还需要意识到,如果你同时运行多个实验,它们是否会相互冲突?是否存在页面流,使得某人可能在同一会话中遇到两个不同的实验?

如果是这样,那将是一个问题,你必须根据自己的判断力来判断这些变化是否会以某种有意义的方式相互干扰,并以某种有意义的方式影响客户的行为。同样,你需要对这些结果持保留态度。有很多因素可能会使结果产生偏差,你需要意识到这一点。只要意识到这一点,并确保你的业务所有者也意识到 A/B 测试的局限性,一切都会没问题的。

此外,如果你没有足够长的时间来进行实验,你需要对这些结果持保留态度,并在以后的不同时间段进行重新测试。

总结

在本章中,我们讨论了什么是 A/B 测试以及围绕它们的挑战。我们举了一些例子,说明了如何使用 t 统计量和 p 值指标来测量方差的影响,并介绍了使用 Python 进行 t 检验的编码和测量。然后我们讨论了 A/B 测试的短期性质及其局限性,例如新奇效应或季节效应。

这也是我们在这本书中的时间。恭喜你走到这一步,这是一个严肃的成就,你应该为自己感到自豪。我们在这里涵盖了很多材料,我希望你至少理解了这些概念,并且对今天数据科学中使用的大多数技术有一些实际经验。这是一个非常广泛的领域,所以我们触及了一点点所有的东西。所以,再次恭喜。

如果你想在这个领域进一步发展你的职业,我真的鼓励你和你的老板谈谈。如果你在一家公司工作,这家公司有自己的一些有趣的数据集,看看你能否玩弄一下。显然,在你使用公司拥有的任何数据之前,你需要先和老板谈一下,因为可能会有一些围绕它的隐私限制。你要确保你没有侵犯公司客户的隐私,这可能意味着你只能在工作场所的受控环境中使用或查看这些数据。所以,在你这样做的时候要小心。

如果你能得到实际在工作中加班几天,并且玩弄一些这些数据集,看看你能做些什么,这不仅表明你有主动性让自己成为一个更好的员工,你可能会发现一些对你的公司有价值的东西,这可能会让你看起来更好,甚至可能导致内部调动,也许是进入一个与你想要发展职业方向更直接相关的领域。

所以,如果你想从我这里得到一些职业建议,我经常被问到的一个常见问题是,“嘿,我是一名工程师,我想更多地涉足数据科学,我该怎么做?”最好的方法就是去做,你知道,实际做一些副业项目,并展示你能做到,并从中展示一些有意义的结果。向你的老板展示,并看看它会带你去哪里。祝你好运。

posted @ 2024-05-21 12:53  绝不原创的飞龙  阅读(28)  评论(0编辑  收藏  举报