数据科学算法的一星期指南-全-

数据科学算法的一星期指南(全)

原文:annas-archive.org/md5/2f0b17cc4af6c12e87ee4558171f9d26

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

数据科学是一个交叉学科,涵盖了机器学习、统计学和数据挖掘,旨在通过算法和统计分析从现有数据中获得新知识。本书将教您数据科学中分析数据的七种最重要方法。每一章首先以简单的概念解释其算法或分析,并通过一个简单的例子进行支持。随后,更多的例子和练习将帮助您构建并扩展对特定分析类型的知识。

本书适合谁阅读

本书适合那些熟悉 Python,并且有一定统计学背景的有志数据科学专业人士。对于那些目前正在实现一两个数据科学算法并希望进一步扩展技能的开发者,本书将非常有用。

如何最大化利用本书

为了最大化地利用本书,首先,您需要保持积极的态度来思考问题——许多新内容会在每章末尾的 Problems 部分的练习中呈现出来。然后,您还需要能够在您选择的操作系统上运行 Python 程序。作者在 Linux 操作系统上使用命令行运行了这些程序。

下载示例代码文件

您可以从您的 www.packt.com 账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packt.com/support 并注册,将文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册 www.packt.com

  2. 选择 SUPPORT 标签。

  3. 点击“Code Downloads & Errata”。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

文件下载后,请确保使用最新版本的工具解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Data-Science-Algorithms-in-a-Week-Second-Edition。如果代码有更新,将会在现有的 GitHub 仓库中更新。

我们还有其他代码包,来自我们丰富的书籍和视频目录,您可以在 github.com/PacktPublishing/ 查看。不要错过!

下载彩色图片

我们还提供了一个包含本书中使用的截图/图表彩色图片的 PDF 文件。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789806076_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:指示文本中的代码字词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。例如:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。"

代码块设置如下:

def dic_key_count(dic, key):
if key is None:
return 0
if dic.get(key, None) is None:
return 0
else:
return int(dic[key])

当我们希望引起您对代码块的特定部分的注意时,相关的行或条目会用粗体标记:

def construct_general_tree(verbose, heading, complete_data,
enquired_column, m):
available_columns = []
for col in range(0, len(heading)):
if col != enquired_column:

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

$ python naive_bayes.py chess.csv

粗体:表示新术语,重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词在文本中出现如此。例如:"从管理面板中选择系统信息。"

警告或重要提示如下所示。

提示和技巧如下所示。

联系我们

我们随时欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在消息主题中提及书名,并发送邮件至customercare@packtpub.com与我们联系。

勘误表:尽管我们已尽一切努力确保内容的准确性,但错误确实会发生。如果您在本书中发现了错误,请向我们报告。请访问www.packt.com/submit-errata,选择您的书,单击勘误提交表单链接并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法副本,请提供地址或网站名称给我们。请通过copyright@packt.com与我们联系,并附上链接到该资料的链接。

如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有意撰写或为书籍做贡献,请访问authors.packtpub.com

评论

请留下您的评论。一旦您阅读并使用了这本书,为什么不在购买它的网站上留下评论呢?潜在的读者可以看到并使用您的公正意见来做购买决定,我们在 Packt 可以了解您对我们产品的看法,而我们的作者也可以看到您对他们的书的反馈。谢谢!

要获取有关 Packt 的更多信息,请访问packt.com

第一章:使用 K-最近邻算法进行分类

最近邻算法根据邻居对数据实例进行分类。通过k-最近邻算法确定的数据实例的类别是k个最近邻居中出现次数最多的类别。

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

  • 如何通过 Mary 及其温度偏好示例实现 k-NN 算法的基本原理

  • 如何选择正确的k值,以便算法能够正确执行,并且在使用意大利地图示例时具有最高的准确度

  • 如何调整值的比例并为 k-NN 算法做准备,以房屋偏好为例

  • 如何选择一个合适的度量标准来测量数据点之间的距离

  • 如何在高维空间中消除不相关的维度,以确保算法在文本分类示例中能够准确执行

Mary 及其温度偏好

例如,如果我们知道我们的朋友 Mary 在 10°C 时感觉冷,在 25°C 时感觉温暖,那么在一个温度为 22°C 的房间里,最近邻算法会猜测她会感觉温暖,因为 22 比 10 更接近 25。

假设我们想知道 Mary 在何时感到温暖,何时感到寒冷,如前面的例子所示,但此外,风速数据在询问 Mary 是否感到温暖或寒冷时也可用:

温度(°C) 风速(km/h) Mary 的感知
10 0
25 0 温暖
15 5
20 3 温暖
18 7
20 10
22 5 温暖
24 6 温暖

我们可以通过图表来表示数据,如下所示:

现在,假设我们想通过使用1-NN 算法来了解当温度为 16°C、风速为 3 km/h 时,Mary 的感受:

为了简化起见,我们将使用曼哈顿距离度量法来测量网格上相邻点之间的距离。邻居N[1]=(x[1],y[1])到邻居N[2]=(x[2],y[2])的曼哈顿距离d[Man]定义为d[Man]=|x[1]**- x[2]|+|y[1]**- y[2]|

让我们给网格标上邻居的距离,看看哪个已知类别的邻居距离我们希望分类的点最近:

我们可以看到,距离该点最近的已知类别的邻居是温度为 15°C(蓝色)且风速为 5 km/h 的邻居。它与该点的距离为三单位。它的类别是蓝色(冷)。最近的红色(温暖)邻居与该点的距离为四单位。由于我们使用的是 1-最近邻算法,我们只需要查看最近的邻居,因此该点的类别应为蓝色(冷)。

通过对每个数据点应用这个过程,我们可以完成图表,如下所示:

请注意,有时数据点可能距离两个已知类别的距离相同:例如,20°C 和 6 km/h。在这种情况下,我们可以偏好一个类别,或者忽略这些边界情况。实际结果取决于算法的具体实现。

k 近邻算法的实现

现在,我们将在 Python 中实现 k-NN 算法来查找玛丽的温度偏好。在本节结束时,我们还将实现上一节中生成的数据的可视化,即玛丽和她的温度偏好。完整的可编译代码和输入文件可以在本书附带的源代码中找到。最重要的部分已提取并在此呈现:

# source_code/1/mary_and_temperature_preferences/knn_to_data.py # Applies the knn algorithm to the input data.
# The input text file is assumed to be of the format with one line per
# every data entry consisting of the temperature in degrees Celsius,
# wind speed and then the classification cold/warm.

import sys
sys.path.append('..')
sys.path.append('../../common')
import knn # noqa
import common # noqa

# Program start
# E.g. "mary_and_temperature_preferences.data"
input_file = sys.argv[1]
# E.g. "mary_and_temperature_preferences_completed.data"
output_file = sys.argv[2]
k = int(sys.argv[3])
x_from = int(sys.argv[4])
x_to = int(sys.argv[5])
y_from = int(sys.argv[6])
y_to = int(sys.argv[7])

data = common.load_3row_data_to_dic(input_file)
new_data = knn.knn_to_2d_data(data, x_from, x_to, y_from, y_to, k)
common.save_3row_data_from_dic(output_file, new_data)
# source_code/common/common.py # ***Library with common routines and functions*** def dic_inc(dic, key):
    if key is None:
        pass
    if dic.get(key, None) is None:
        dic[key] = 1
    else:
        dic[key] = dic[key] + 1
# source_code/1/knn.py
# ***Library implementing knn algorithm***

def info_reset(info):
    info['nbhd_count'] = 0
    info['class_count'] = {}

# Find the class of a neighbor with the coordinates x,y.
# If the class is known count that neighbor.
def info_add(info, data, x, y):
    group = data.get((x, y), None)
    common.dic_inc(info['class_count'], group)
    info['nbhd_count'] += int(group is not None)

# Apply knn algorithm to the 2d data using the k-nearest neighbors with
# the Manhattan distance.
# The dictionary data comes in the form with keys being 2d coordinates
# and the values being the class.
# x,y are integer coordinates for the 2d data with the range
# [x_from,x_to] x [y_from,y_to].
def knn_to_2d_data(data, x_from, x_to, y_from, y_to, k):
    new_data = {}
    info = {}
    # Go through every point in an integer coordinate system.
    for y in range(y_from, y_to + 1):
        for x in range(x_from, x_to + 1):
            info_reset(info)
            # Count the number of neighbors for each class group for
            # every distance dist starting at 0 until at least k
            # neighbors with known classes are found.
            for dist in range(0, x_to - x_from + y_to - y_from):
                # Count all neighbors that are distanced dist from
                # the point [x,y].
                if dist == 0:
                    info_add(info, data, x, y)
                else:
                    for i in range(0, dist + 1):
                        info_add(info, data, x - i, y + dist - i)
                        info_add(info, data, x + dist - i, y - i)
                    for i in range(1, dist):
                        info_add(info, data, x + i, y + dist - i)
                        info_add(info, data, x - dist + i, y - i)
                # There could be more than k-closest neighbors if the
                # distance of more of them is the same from the point
                # [x,y]. But immediately when we have at least k of
                # them, we break from the loop.
                if info['nbhd_count'] >= k:
                    break
            class_max_count = None
            # Choose the class with the highest count of the neighbors
            # from among the k-closest neighbors.
            for group, count in info['class_count'].items():
                if group is not None and (class_max_count is None or
                   count > info['class_count'][class_max_count]):
                    class_max_count = group
            new_data[x, y] = class_max_count
    return new_data

输入

前面的程序将使用以下文件作为输入数据的来源。该文件包含了有关玛丽温度偏好的已知数据表:

# source_code/1/mary_and_temperature_preferences/
marry_and_temperature_preferences.data
10 0 cold
25 0 warm
15 5 cold
20 3 warm
18 7 cold
20 10 cold
22 5 warm
24 6 warm

输出

我们通过使用 k-NN 算法(k=1邻居)在mary_and_temperature_preferences.data输入文件上运行前面的实现。该算法将所有具有整数坐标的点分类到大小为(30-5=25) by (10-0=10)的矩形区域内,因此,矩形的大小为(25+1) * (10+1) = 286个整数点(加一是为了计算边界上的点)。使用wc命令,我们发现输出文件确实包含 286 行——每个点对应一个数据项。通过使用head命令,我们显示输出文件中的前 10 行:

$ python knn_to_data.py mary_and_temperature_preferences.data mary_and_temperature_preferences_completed.data 1 5 30 0 10

$ wc -l mary_and_temperature_preferences_completed.data 
286 mary_and_temperature_preferences_completed.data

$ head -10 mary_and_temperature_preferences_completed.data 
7 3 cold
6 9 cold
12 1 cold
16 6 cold
16 9 cold
14 4 cold
13 4 cold
19 4 warm
18 4 cold
15 1 cold

可视化

在本章前面描述的可视化中,使用了matplotlib库。数据文件被加载后,显示为散点图:

# source_code/common/common.py
# returns a dictionary of 3 lists: 1st with x coordinates,
# 2nd with y coordinates, 3rd with colors with numeric values
def get_x_y_colors(data):
    dic = {}
    dic['x'] = [0] * len(data)
    dic['y'] = [0] * len(data)
    dic['colors'] = [0] * len(data)
    for i in range(0, len(data)):
        dic['x'][i] = data[i][0]
        dic['y'][i] = data[i][1]
        dic['colors'][i] = data[i][2]
    return dic
# source_code/1/mary_and_temperature_preferences/
mary_and_temperature_preferences_draw_graph.py import sys
sys.path.append('../../common')  # noqa
import common
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib
matplotlib.style.use('ggplot')

data_file_name = 'mary_and_temperature_preferences_completed.data'
temp_from = 5
temp_to = 30
wind_from = 0
wind_to = 10

data = np.loadtxt(open(data_file_name, 'r'),
                  dtype={'names': ('temperature', 'wind', 'perception'),
                         'formats': ('i4', 'i4', 'S4')})

# Convert the classes to the colors to be displayed in a diagram.
for i in range(0, len(data)):
    if data[i][2] == 'cold':
        data[i][2] = 'blue'
    elif data[i][2] == 'warm':
        data[i][2] = 'red'
    else:
        data[i][2] = 'gray'
# Convert the array into the format ready for drawing functions.
data_processed = common.get_x_y_colors(data)

# Draw the graph.
plt.title('Mary and temperature preferences')
plt.xlabel('temperature in C')
plt.ylabel('wind speed in kmph')
plt.axis([temp_from, temp_to, wind_from, wind_to])
# Add legends to the graph.
blue_patch = mpatches.Patch(color='blue', label='cold')
red_patch = mpatches.Patch(color='red', label='warm')
plt.legend(handles=[blue_patch, red_patch])
plt.scatter(data_processed['x'], data_processed['y'],
            c=data_processed['colors'], s=[1400] * len(data))
plt.show()

意大利地图示例 - 选择 k 值

在我们的数据中,我们获得了一些来自意大利及其周围地区地图的点(大约 1%)。蓝色点代表水域,绿色点代表陆地;白色点是未知的。从我们所拥有的部分信息中,我们希望预测白色区域是水域还是陆地。

仅绘制地图数据的 1%会使其几乎不可见。如果我们从意大利及其周围地区的地图中获取大约 33 倍的数据,并将其绘制在图中,结果将如下所示:

分析

对于这个问题,我们将使用 k-NN 算法——这里的k表示我们将查看k个最接近的邻居。给定一个白色点,如果它的k个最接近的邻居大多数位于水域区域,它将被分类为水域;如果它的k个最接近的邻居大多数位于陆地区域,它将被分类为陆地。我们将使用欧几里得度量来计算距离:给定两个点,X=[x[0],x[1]]Y=[y[0],y[1]],它们的欧几里得距离定义为d[Euclidean] = sqrt((x[0]-y[0])²+(x[1]-y[1])²)

欧几里得距离是最常用的度量。给定纸面上的两点,它们的欧几里得距离就是通过尺子量测得的两点之间的长度,如下图所示:

为了将 k-NN 算法应用到不完整的地图中,我们必须选择k值。由于一个点的最终分类是其k个最接近邻居的多数类别,因此k应为奇数。让我们将此算法应用于k=1,3,5,7,9的值。

将此算法应用到不完整地图上的每一个白点,将得到以下完整地图:

k=1

k=3

k=5

k=7

k=9

正如你所注意到的,k的最高值会导致一个边界更平滑的完整地图。这里展示了意大利的实际完整地图:

我们可以使用这个实际的完整地图来计算不同k值下错误分类点的百分比,从而确定 k-NN 算法在不同k值下的准确性:

k 错误分类点的百分比
1 2.97
3 3.24
5 3.29
7 3.40
9 3.57

因此,对于这种特定类型的分类问题,k-NN 算法在k=1时达到了最高准确性(最小误差率)。

然而,在现实生活中,问题在于我们通常没有完整的数据或解决方案。在这种情况下,我们需要选择一个适合部分可用数据的k值。有关这一点,请参阅本章末尾的问题 4

房屋拥有情况 – 数据重缩放

对于每个人,我们都知道他们的年龄、年收入,以及是否拥有房屋:

年龄 年收入(美元) 房屋拥有情况
23 50,000 非拥有者
37 34,000 非拥有者
48 40,000 拥有者
52 30,000 非拥有者
28 95,000 拥有者
25 78,000 非拥有者
35 130,000 拥有者
32 105,000 拥有者
20 100,000 非拥有者
40 60,000 拥有者
50 80,000 彼得

房屋拥有情况与年收入

目标是预测 50 岁、年收入 80,000 美元的彼得是否拥有房屋,并可能成为我们保险公司的潜在客户。

分析

在这种情况下,我们可以尝试应用 1-NN 算法。然而,我们需要小心如何衡量数据点之间的距离,因为收入范围远大于年龄范围。收入为 11.5 万美元和 11.6 万美元之间相差 1,000 美元。这两个数据点在收入上差距很大。但相对而言,这些数据点之间的差异实际上并没有那么大。由于我们认为年龄和年收入这两个度量大致同等重要,我们将根据以下公式将它们都缩放到 0 到 1 之间:

在我们特定的案例中,这简化为以下内容:

缩放后,我们得到以下数据:

年龄 缩放后的年龄 年收入(美元) 缩放后的年收入 房屋所有权状态
23 0.09375 50,000 0.2 非拥有者
37 0.53125 34,000 0.04 非拥有者
48 0.875 40,000 0.1 拥有者
52 1 30,000 0 非拥有者
28 0.25 95,000 0.65 拥有者
25 0.15625 78,000 0.48 非拥有者
35 0.46875 130,000 1 拥有者
32 0.375 105,000 0.75 拥有者
20 0 100,000 0.7 非拥有者
40 0.625 60,000 0.3 拥有者
50 0.9375 80,000 0.5 ?

现在,如果我们使用欧几里得度量应用 1-NN 算法,我们会发现 Peter 很可能拥有一套房子。请注意,如果不进行重新缩放,算法将得出不同的结果。有关更多信息,请参见练习 1.5

文本分类 – 使用非欧几里得距离

我们给出以下与关键词算法计算机相关的词频数据,适用于信息学和数学学科分类中的文档:

每千字算法词汇 每千字计算机词汇 学科分类
153 150 信息学
105 97 信息学
75 125 信息学
81 84 信息学
73 77 信息学
90 63 信息学
20 0 数学
33 0 数学
105 10 数学
2 0 数学
84 2 数学
12 0 数学
41 42 ?

那些高频出现算法计算机词汇的文档属于信息学类。数学类的文档偶尔也包含大量的算法词汇,例如涉及欧几里得算法的数论领域的文档。但由于数学类在算法领域的应用较少,计算机一词在这些文档中的出现频率较低。

我们想要分类一份文档,该文档中每千字出现 41 次算法,每千字出现 42 次计算机

分析

例如,使用 1-NN 算法和曼哈顿距离或欧几里得距离,结果会将该文档归类为 mathematics 类别。然而,直观上我们应该使用不同的度量方法来衡量文档之间的距离,因为该文档中的 computer 词汇出现频率远高于其他已知的 mathematics 类文档。

另一个可选的度量方法是,测量文档实例之间词汇的比例或角度。你可以使用角度的余弦值,cos(θ),然后使用著名的点积公式来计算 cos(θ)

设我们使用 a=(a[x],a[y]), b=(b[x],b[y]),使用以下公式:

这将得出以下结果:

使用余弦距离度量方法,你可以将该文档归类为 informatics 类:

文本分类 – 高维空间中的 k-NN

假设我们有一些文档,并希望基于它们的词频来分类其他文档。例如,出现在《金色圣经》项目古腾堡电子书中的 120 个最常出现的词汇如下:

任务是设计一种度量方法,给定每个文档的词频,能够准确判断这些文档在语义上的相似度。因此,这样的度量方法可以被 k-NN 算法用来基于现有文档对新文档中的未知实例进行分类。

分析

假设我们考虑,例如,语料库中最常出现的 N 个词。然后,我们统计每个 N 个词在给定文档中的出现频率,并将它们放入一个 N 维向量中,代表该文档。接着,我们定义两个文档之间的距离为这两个文档的词频向量之间的距离(例如,欧几里得距离)。

这种方法的问题在于,只有某些词汇才代表书籍的实际内容,其他词汇之所以出现在文本中,是因为语法规则或它们的一般基础意义。例如,在《圣经》中,120 个最常出现的词语中,每个词的意义不同。下表中,我们标出了那些在《圣经》中既频繁出现又具有重要意义的词语:

|

  1. lord - 使用频率 1.00%

  2. god - 0.56%

|

  1. Israel - 0.32%

  2. king - 0.32%

|

  1. David - 0.13%

  2. Jesus - 0.12%

|

这些词汇在数学文本中出现的可能性较低,但在宗教或基督教相关的文本中出现的可能性较高。

然而,如果我们只看《圣经》中六个最常见的词,它们在检测文本意义方面其实没有什么用:

|

  1. the - 8.07%

  2. and - 6.51%

|

  1. of - 4.37%

  2. to - 1.72%

|

  1. that - 1.63%

  2. in - 1.60%

|

与数学、文学及其他学科相关的文本,在这些词汇的频率上会有类似的表现。差异主要可能来源于写作风格。

因此,为了确定两个文档之间的相似性距离,我们只需要查看重要词汇的频率统计。一些词汇不太重要——这些维度最好减少,因为它们的包含可能最终导致结果的误解。因此,我们需要做的就是选择那些对分类文档至关重要的词汇(维度)。为此,请参考问题 6

摘要

在本章中,我们了解到k-最近邻算法是一种分类算法,它将给定数据点的k-最近邻中多数类别分配给该数据点。两个点之间的距离是通过度量来衡量的。我们介绍了几种距离的例子,包括欧几里得距离、曼哈顿距离、切线距离和余弦距离。我们还讨论了如何通过不同参数的实验和交叉验证,帮助确定应使用哪个参数k以及哪个度量。

我们还学到了数据点的维度和位置是由其特征决定的。大量的维度可能导致 k-NN 算法的准确性较低。减少重要性较小的特征维度可以提高准确性。同样,为了进一步提高准确性,每个维度的距离应该根据该维度特征的重要性进行缩放。

在下一章,我们将讨论朴素贝叶斯算法,该算法基于贝叶斯定理使用概率方法对元素进行分类。

问题

在本节中,我们将讨论以下问题:

  • 玛丽和她的温度偏好问题

  • 意大利地图 – 选择k的值

  • 房屋所有权

为了尽可能最佳地学习本章的内容,请先独立分析这些问题,然后再查看本章末尾的分析部分。

玛丽和她的温度偏好问题

问题 1:假设你知道你的朋友玛丽在-50°C 时感到寒冷,而在 20°C 时感到温暖。那么 1-NN 算法会怎么判断玛丽的感受呢?在 22°C、15°C 和-10°C 的温度下,她会感到温暖还是寒冷?你认为算法预测玛丽对温度的感知是正确的吗?如果不是,请给出你的理由,并指出为什么算法没有给出合适的结果,以及需要改进哪些方面,以便算法能够做出更好的分类。

问题 2:你认为 1-NN 算法会比使用k-NN 算法(其中k>1)产生更好的结果吗?

问题 3:我们收集了更多数据,发现玛丽在 17°C 时感觉温暖,在 18°C 时却感觉寒冷。根据我们自己的常识,温度越高,玛丽应该越觉得温暖。你能解释数据中不一致的可能原因吗?我们如何改进数据分析?我们是否也应该收集一些非温度数据?假设我们只有一条温度数据,你认为仅用这些数据,1-NN 算法是否仍然能够得出更好的结果?我们应该如何选择k值来让k-NN 算法表现得更好?

意大利地图 – 选择 k 值

问题 4:我们得到意大利部分地图用于意大利地图问题。然而,假设完整数据不可用。因此,我们无法对不同k值的所有预测点计算误差率。你应如何选择k值,来使用k-NN 算法,完成意大利地图并最大化其准确性?

房屋所有权

问题 5:使用与房屋所有权问题相关部分的数据,通过欧几里得度量找出离彼得最近的邻居:

a) 不进行数据重新缩放

b) 使用缩放后的数据

最近的邻居是否在:

a) 与该邻居相同?

b) 哪个邻居拥有房屋?

问题 6:假设你想通过某种度量和 1-NN 算法,在古腾堡语料库(www.gutenberg.org)中找到与某本选定书籍(例如《圣经》)相似的书籍或文档。你会如何设计一种度量来衡量这两本书的相似度?

分析

问题 1:8°C 比-50°C 更接近 20°C。因此,算法会将玛丽在-8°C 时归类为感觉温暖。但如果我们运用常识和知识,这种判断不太可能成立。在更复杂的例子中,由于我们缺乏专业知识,可能会被分析结果误导,得出错误的结论。但请记住,数据科学不仅仅依赖数据分析,还需要实际的专业知识。为了得出合理的结论,我们应该对问题和数据有深刻的理解。

算法进一步指出,在 22°C 时,玛丽应该感觉温暖,这是毫无疑问的,因为 22°C 高于 20°C,人类在较高的温度下会感觉更温暖;这再次是我们常识的一个简单应用。对于 15°C,算法会认为玛丽感觉温暖,但如果我们使用常识,可能无法对这个结论完全确信。

为了能够使用我们的算法得到更好的结果,我们应该收集更多数据。例如,如果我们发现玛丽在 14°C 时感到寒冷,那么我们就有一个与 15°C 非常接近的数据实例,因此,我们可以更有信心地推测玛丽在 15°C 时也会觉得寒冷。

问题 2:我们处理的数据只是单维的,并且也分为两部分,冷和暖,具有以下特性:温度越高,人感觉越暖。即使我们知道 Mary 在温度-40、-39、…、39 和 40 时的感觉,我们的数据实例仍然非常有限——大约每摄氏度只有一个实例。因此,最好只查看一个最接近的邻居。

问题 3:数据中的差异可能是由于进行的测试不准确所致。这可以通过进行更多实验来缓解。

除了不准确性,还有其他因素可能会影响 Mary 的感觉:例如风速、湿度、阳光、Mary 的穿着(她是穿着外套和牛仔裤,还是仅仅穿着短裤和无袖上衣,甚至是泳衣),以及她是湿的还是干的。我们可以将这些附加维度(风速和她的穿着)添加到数据点的向量中。这将为算法提供更多且更高质量的数据,因此可以期望得到更好的结果。

如果我们只有温度数据,但数据量更多(例如,每摄氏度有 10 个分类实例),那么我们可以增加k值,查看更多的邻居以更准确地确定温度。但这纯粹依赖于数据的可用性。我们可以调整算法,使其基于某个距离d内的所有邻居来进行分类,而不是仅仅基于k个最接近的邻居进行分类。这将使算法在数据量较大且距离较近时以及数据实例与我们想要分类的实例距离较近时都能有效工作。

问题 4:为此,你可以使用交叉验证(请参考附录 A – 统计学中的交叉验证部分)来确定具有最高准确性的k值。例如,你可以将来自意大利部分地图的可用数据分为学习数据和测试数据,80%的分类像素将交给 k-NN 算法来完成地图。然后,剩余的 20%分类像素将用于根据 k-NN 算法计算正确分类的像素百分比。

问题 5

a) 在没有数据重缩放的情况下,Peter 最近的邻居年收入为 78,000 美元,年龄为 25 岁。这个邻居没有房子。

b) 在数据重缩放之后,Peter 最近的邻居年收入为 60,000 美元,年龄为 40 岁。这个邻居拥有房子。

问题 6:为了设计一个准确衡量两个文档相似度距离的度量,我们需要选择能够构成文档频率向量维度的重要单词。那些不能确定文档语义意义的单词,通常在所有文档中具有大致相同的词频。因此,我们可以创建一个包含文档相对词频的列表。例如,我们可以使用以下定义:

然后,文档可以通过一个N维向量来表示,该向量由相对频率最高的N个单词的词频组成。这样的向量通常会包含比由频率最高的N个单词组成的向量更重要的单词。

第二章:朴素贝叶斯

朴素贝叶斯分类算法根据贝叶斯定理将最可能的类别分配给一组元素。

假设 AB 是概率事件。P(A)Atrue 的概率。P(A|B) 是在 Btrue 的情况下,Atrue 的条件概率。如果是这种情况,那么贝叶斯定理声明如下:

P(A) 是在没有 P(B)P(B|A) 概率知识的情况下,Atrue 的先验概率。P(A|B) 是考虑到 Btrue 额外知识的条件概率。

在本章中,您将学习以下主题:

  • 如何在简单的医学检测示例中以基础方式应用贝叶斯定理来计算医学测试的正确概率

  • 如何通过证明贝叶斯定理的陈述及其扩展来掌握贝叶斯定理

  • 如何在国际象棋的示例中将贝叶斯定理应用于独立和依赖变量

  • 如何在医学测试和国际象棋的离散随机变量示例中应用贝叶斯定理,以及如何在性别分类的连续随机变量示例中使用连续随机变量的概率分布应用贝叶斯定理

  • 如何在 Python 中实现算法,利用贝叶斯定理计算后验概率

到本章结束时,您将能够通过解决问题验证您对朴素贝叶斯的理解。您还将能够辨别在什么情况下贝叶斯定理是合适的分析方法,以及在何时不适用。

医学测试 – 贝叶斯定理的基本应用

一位病人接受了一项特殊的癌症检测,准确率为test_accuracy=99.9%——如果结果为阳性,那么 99.9%的接受检测的患者会患有该特定类型的癌症。反之,99.9%接受检测的患者如果结果为阴性,则不会患有该特定类型的癌症。

假设病人接受了检测且结果为阳性。那么该病人患某种特定类型癌症的概率是多少?

分析

我们将使用贝叶斯定理来确定病人患癌症的概率:

为了确定病人患癌症的先验概率,我们必须了解癌症在人群中的发生频率。假设我们发现每 10 万人中就有 1 人患有这种癌症。因此,P(cancer)=1/100,000。因此,P(test_positive|cancer) = test_accuracy=99.9%=0.999,这是由检测的准确性给出的。

P(test_positive) 需要按以下方式计算:

因此,我们可以计算以下内容:

所以,即使测试结果是阳性,且测试的准确率为 99.9%,患者患所测试癌症的概率也只有大约 1%。与测试的高准确率相比,测试后患癌症的概率相对较低,但远高于在测试前已知的 1/100,000(0.001%)的概率,这是基于该癌症在整个群体中的发生率。

贝叶斯定理及其扩展

在本节中,我们将陈述并证明贝叶斯定理及其扩展。

贝叶斯定理

贝叶斯定理指出如下:

证明

我们可以通过使用初等集合论在事件AB的概率空间上证明这个定理。换句话说,在这里,概率事件将被定义为概率空间中可能结果的集合:

图 2.1:两个事件的概率空间

如你在上面的图示中所见,我们可以陈述如下关系:

将这些关系重新排列,我们得到如下:

这实际上就是贝叶斯定理:

这完成了证明。

扩展贝叶斯定理

我们可以通过考虑更多的概率事件来扩展贝叶斯定理。假设事件B1,…,Bn在给定A的情况下是条件独立的。让~A表示A的补集。那么,我们有如下结果:

证明

由于事件B1,…,Bn在给定A(以及给定~A)的情况下是条件独立的,我们得到如下结论:

应用贝叶斯定理的简易形式和这一事实,我们得到如下结果:

这完成了证明。

下棋 – 独立事件

假设我们给定了以下数据表。这告诉我们,基于一系列与天气相关的条件,我们的朋友是否愿意在公园外和我们下棋:

温度 风力 阳光 是否下棋
寒冷 强风 阴天
温暖 强风 阴天
温暖 晴天
晴天
微风 阴天
温暖 微风 晴天
寒冷 微风 阴天
寒冷 晴天
强风 阴天
温暖 阴天
温暖 强风 晴天 ?

我们希望通过使用贝叶斯定理来判断,如果温度是温暖的,风速较强,且阳光明媚,我们的朋友是否愿意在公园与我们下棋。

分析

在这种情况下,我们可能希望将TemperatureWindSunshine作为独立随机变量。扩展后的贝叶斯定理公式如下:

让我们计算表格中所有已知值的列数,以确定各个概率。

P(Play=Yes)=6/10=3/5,因为有 10 列完整数据,其中6列的Play属性值为Yes

P(Temperature=Warm|Play=Yes)=3/6=1/2,因为有6列的Play属性值为Yes,其中有3列的Temperature属性值为Warm。同样地,我们得到以下内容:

  • * *

因此:

因此,我们得到以下内容:

这意味着,在给定的天气条件下,我们的朋友很可能会高兴地与我们在公园下棋,概率大约为67%。由于这是多数,我们可以将数据向量(Temperature=Warm, Wind=Strong, Sunshine=Sunny)归类为Play=Yes类别。

实现一个朴素贝叶斯分类器

在本节中,我们将实现一个程序,通过使用贝叶斯定理来计算一个数据项属于某一类别的概率:

# source_code/2/naive_bayes.py 
# A program that reads the CSV file with the data and returns
# the Bayesian probability for the unknown value denoted by ? to
# belong to a certain class.
# An input CSV file should be of the following format:
# 1\. items in a row should be separated by a comma ','
# 2\. the first row should be a heading - should contain a name for each
# column of the data.
# 3\. the remaining rows should contain the data itself - rows with
# complete and rows with the incomplete data.
# A row with complete data is the row that has a non-empty and
# non-question mark value for each column. A row with incomplete data is
# the row that has the last column with the value of a question mark ?.
# Please, run this file on the example chess.csv to understand this help
# better:
# $ python naive_bayes.py chess.csv

import imp
import sys
sys.path.append('../common')
import common  # noqa

# Calculates the Bayesian probability for the rows of incomplete data and
# returns them completed by the Bayesian probabilities. complete_data
# are the rows with the data that is complete and are used to calculate
# the conditional probabilities to complete the incomplete data.
def bayes_probability(heading, complete_data, incomplete_data,
                      enquired_column):
    conditional_counts = {}
    enquired_column_classes = {}
    for data_item in complete_data:
        common.dic_inc(enquired_column_classes,
                       data_item[enquired_column])
        for i in range(0, len(heading)):
            if i != enquired_column:
                common.dic_inc(
                    conditional_counts, (
                        heading[i], data_item[i],
                        data_item[enquired_column]))

    completed_items = []
    for incomplete_item in incomplete_data:
        partial_probs = {}
        complete_probs = {}
        probs_sum = 0
        for enquired_group in enquired_column_classes.items():
            # For each class in the of the enquired variable A calculate
            # the probability P(A)*P(B1|A)*P(B2|A)*...*P(Bn|A) where
            # B1,...,Bn are the remaining variables.
            probability = float(common.dic_key_count(
                enquired_column_classes,
                enquired_group[0])) / len(complete_data)
            for i in range(0, len(heading)):
                if i != enquired_column:
                    probability = probability * (float(
                        common.dic_key_count(
                            conditional_counts, (
                                heading[i], incomplete_item[i],
                                enquired_group[0]))) / (
                        common.dic_key_count(enquired_column_classes,
                                             enquired_group[0])))
            partial_probs[enquired_group[0]] = probability
            probs_sum += probability

        for enquired_group in enquired_column_classes.items():
            complete_probs[enquired_group[0]
                           ] = partial_probs[enquired_group[0]
                                             ] / probs_sum
        incomplete_item[enquired_column] = complete_probs
        completed_items.append(incomplete_item)
    return completed_items

# Program start
if len(sys.argv) < 2:
    sys.exit('Please, input as an argument the name of the CSV file.')

(heading, complete_data, incomplete_data,
 enquired_column) = common.csv_file_to_ordered_data(sys.argv[1])

# Calculate the Bayesian probability for the incomplete data
# and output it.
completed_data = bayes_probability(
    heading, complete_data, incomplete_data, enquired_column)
print completed_data

我们使用公共库中的字典部分:

# source_code/common/common.py 
# Increments integer values in a dictionary.
def dic_inc(dic, key):
    if key is None:
        pass
    if dic.get(key, None) is None:
        dic[key] = 1
    else:
        dic[key] = dic[key] + 1

def dic_key_count(dic, key):
    if key is None:
        return 0
    if dic.get(key, None) is None:
        return 0
    else:
        return int(dic[key])

输入

我们将与下棋相关的表格数据(例如,关于下棋的数据)保存在以下 CSV 文件中:

source_code/2/naive_bayes/chess.csv
Temperature,Wind,Sunshine,Play
Cold,Strong,Cloudy,No
Warm,Strong,Cloudy,No
Warm,None,Sunny,Yes
Hot,None,Sunny,No
Hot,Breeze,Cloudy,Yes
Warm,Breeze,Sunny,Yes
Cold,Breeze,Cloudy,No
Cold,None,Sunny,Yes
Hot,Strong,Cloudy,Yes
Warm,None,Cloudy,Yes
Warm,Strong,Sunny,? 

输出

我们提供chess.csvfile作为输入,使用 Python 程序计算数据项(Temperature=Warm, Wind=Strong, Sunshine=Sunny)属于文件中现有类别的概率,分别为Play=YesPlay=No。正如我们之前发现的,该数据项属于Play=Yes类别,因为其概率较高。因此,我们将数据项归类到该类别中:

$ python naive_bayes.py chess.csv
[
    ['Warm', 'Strong', 'Sunny', {
        'Yes': 0.66666666666666_66,
        'No': 0.33333333333333337
    }]
]

玩棋——依赖事件

假设我们想要了解我们的朋友是否愿意在英国剑桥的公园与我们下棋。但这次,我们提供了不同的输入数据:

Temperature Wind Season Play
强风 冬季
温暖 强风 秋季
温暖 夏季
春季
微风 秋季
温暖 微风 春季
微风 冬季
春季
强风 夏季
温暖 秋季
温暖 强风 春季 ?

所以,现在我们想知道,当数据中温度温暖强风季节春季时,我们朋友是否会在英国剑桥的公园玩耍,答案会如何变化。

分析

我们可能会倾向于使用贝叶斯概率来计算我们朋友是否会在公园与我们下棋。然而,我们应该小心,问一下这些事件的概率是否相互独立。

在之前的例子中,我们使用了贝叶斯概率,给定的概率变量是温度阳光。这些变量是合理独立的。常识告诉我们,特定的温度阳光与特定的风速之间并没有强相关性。的确,阳光明媚的天气会带来更高的气温,但即使温度很低,阳光明媚的天气也很常见。因此,我们认为即使是阳光温度作为随机变量也是合理独立的,并应用了贝叶斯定理。

然而,在这个例子中,温度季节是紧密相关的,特别是在像英国这样的位置——我们所说的公园所在地。与靠近赤道的国家不同,英国的气温全年变化很大。冬天很冷,夏天很热。春天和秋天的气温介于其中。

因此,我们不能在这里应用贝叶斯定理,因为这些随机变量是相关的。然而,我们仍然可以通过对部分数据使用贝叶斯定理来进行一些分析。通过消除足够的相关变量,剩余的变量可能会变得独立。由于温度季节更为具体,且这两个变量是相关的,因此我们只保留温度变量。剩下的两个变量,温度,是独立的。

因此,我们得到以下数据:

温度 下棋
强风
温暖 强风
温暖
微风
温暖 微风
微风
强风
温暖
温暖 强风 ?

我们可以保留重复的行,因为它们提供了该特定数据行发生的更多证据。

输入

保存该表格,我们得到以下 CSV 文件:

# source_code/2/chess_reduced.csv
Temperature,Wind,Play
Cold,Strong,No
Warm,Strong,No
Warm,None,Yes
Hot,None,No
Hot,Breeze,Yes
Warm,Breeze,Yes
Cold,Breeze,No
Cold,None,Yes
Hot,Strong,Yes
Warm,None,Yes
Warm,Strong,?

输出

我们将保存的 CSV 文件输入到naive_bayes.py程序中,得到了以下结果:

python naive_bayes.py chess_reduced.csv
[['Warm', 'Strong', {'Yes': 0.49999999999999994, 'No': 0.5}]]

第一个类别 Yes 有 50%的概率为真。由于使用 Python 的非精确浮点数算术导致数值上的差异。第二个类别 No 也有相同的 50%概率为真。因此,根据我们目前的数据,我们无法对向量的类别(温暖)做出合理的结论。然而,您可能已经注意到,这个向量已经在具有结果类别 No 的表格中出现。因此,我们的猜测是,这个向量应该刚好存在于一个类别 No 中。但是,要有更高的统计置信度,我们需要更多的数据或涉及更多的独立变量。

性别分类 – 连续随机变量的贝叶斯方法

到目前为止,我们已经得到了属于有限数量类别中的一个概率事件,例如,一个温度被分类为冷、温暖或热。但是如果我们获得的是以°C 为单位的温度,我们如何计算后验概率呢?

在这个例子中,我们得到了五名男性和五名女性的身高数据,如下表所示:

身高(厘米) 性别
180 男性
174 男性
184 男性
168 男性
178 男性
170 女性
164 女性
155 女性
162 女性
166 女性
172 ?

假设下一个人的身高为 172 厘米。那么这个人更可能是什么性别,以及概率是多少?

分析

解决这个问题的一种方法是为数值赋予类别;例如,身高在 170 厘米和 179 厘米之间的人将属于同一类。使用这种方法,我们可能会得到一些非常宽的类,例如,具有较高厘米范围的类,或者具有更精确但成员较少的类,因此我们不能充分利用贝叶斯分类器的全部潜力。同样地,使用这种方法,我们不会考虑身高区间(170, 180)和(180, 190)之间的类比考虑(170, 180)和(190, 200)之间的类更接近。

让我们在这里回顾一下贝叶斯公式:

在这里表达出最终形式的公式消除了归一化 P(身高|男性)P(身高) 以获得基于身高判断一个人是男性的正确概率的需要。

假设人们的身高服从正态分布,我们可以使用正态概率分布来计算 P(男性|身高)。我们假设 P(男性)=0.5,即被测量的人是男性或女性的可能性相等。正态概率分布由总体的均值μ和方差 σ² 决定:

下表显示了按性别分组的平均身高和方差:

性别 平均身高 身高方差
男性 176.8 37.2
女性 163.4 30.8

因此,我们可以计算以下内容:

请注意,这些不是概率值,而是概率密度函数的值。然而,从这些值中,我们可以观察到,身高为 172 厘米的人更有可能是男性而不是女性,原因如下:

更准确地说:

因此,身高为172厘米的人是男性的概率为68.9%

总结

在这一章中,我们学习了朴素贝叶斯算法及其在不同场景下的应用。

贝叶斯定理如下所述:

这里,P(A|B)是条件概率,即在B为真的情况下,A为真的概率。它用于更新在新的观测值下,A为真时的概率值。这个定理可以扩展到含有多个随机变量的命题:

随机变量B1,...,Bn在给定A的条件下需要相互独立。这些随机变量可以是离散的或连续的,并且遵循概率分布,例如正态(高斯)分布。

我们还研究了离散随机变量。我们了解到,最佳做法是通过收集足够的数据,确保在给定任何条件(具有值A)的情况下,为离散随机变量的每个值收集一个数据项。

随着独立随机变量的增多,我们可以更准确地确定后验概率。然而,更大的危险在于,这些变量中的某些可能是相关的,导致最终结果不准确。当变量相关时,我们可以去除一些相关变量,只考虑相互独立的变量,或考虑采用其他算法来解决数据科学问题。

在下一章中,我们将学习如何使用决策树算法对元素进行分类。

问题

问题 1:一名患者接受病毒V的检测,测试的准确率为 98%。该病毒V在患者所在地区每 100 人中有 4 人感染:

a) 如果患者检测结果为阳性,患者感染病毒V的概率是多少?

b) 如果测试结果为阴性,患者仍然感染病毒的概率是多少?

问题 2:除了评估患者是否感染病毒V(见问题 1)外,医生通常还会检查其他症状。根据一位医生的说法,大约 85%的患者在出现发热、恶心、腹部不适和乏力等症状时,感染了病毒V

a) 如果病人出现了之前提到的症状,并且病毒检测结果为阳性,病人感染病毒V的概率是多少?

b) 如果病人出现了之前提到的症状,但测试结果为阴性,病人感染病毒的概率有多大?

问题 3:在某个岛屿上,每两个海啸中就有一个是由地震引发的。过去 100 年内发生了 4 次海啸和 6 次地震。一个地震站记录到岛屿附近海洋发生了地震。这个地震会导致海啸的概率是多少?

问题 4:病人接受四项独立测试,以确定是否患有某种疾病:

Test1 阳性 Test2 阳性 Test3 阳性 Test4 阳性 疾病
?

我们接诊了一位新病人,第二项和第三项测试为阳性,而第一项和第四项测试为阴性。该病人患有该疾病的概率是多少?

问题 5:我们给出以下包含在邮件中的词汇及这些邮件是否为垃圾邮件的数据:

Money Free Rich Naughty Secret Spam
?

a) 创建一个朴素贝叶斯算法来判断一封邮件是否为垃圾邮件。当一封邮件包含MoneyRichSecret这几个词,但不包含FreeNaughty时,算法的结果是什么?

b) 你同意算法的结果吗?在这里使用的朴素贝叶斯算法,作为分类邮件的好方法吗?请证明你的回答。

问题 6:这是一个性别分类问题。假设我们有关于十个人的以下数据:

身高(厘米) 体重(千克) 发长 性别
180 75
174 71
184 83
168 63
178 70
170 59
164 53
155 46
162 52
166 55
172 60 ?

第 11 个人,身高 172 cm,体重 60 kg,长发的情况下,他是男性的概率是多少?

分析

问题 1:在患者接受测试之前,他们患有病毒的概率是 4%,P(virus)=4%=0.04。测试的准确性为 test_accuracy=98%=0.98。我们应用医学测试示例中的公式:

因此,我们有以下公式:

这意味着,如果测试结果为阳性,患者患有病毒 V 的概率约为 67%:

如果测试结果为阴性,患者仍然可能携带病毒 V,概率为 0.08%。

问题 2:在这里,我们可以假设给定患者已感染病毒 V 的情况下,症状和阳性测试结果是条件独立的事件。我们有以下变量:

由于我们有两个独立的随机变量,因此我们将使用扩展的贝叶斯定理:

a)

所以,患者如果有病毒 V 的症状,并且测试结果为阳性,则该患者有大约 92% 的概率是感染了病毒。

请注意,在前一个问题中,我们得知在测试结果为阳性的情况下,患者患有病毒的概率仅约为 67%。但是,在加入另一个独立随机变量后,即使症状评估仅对 85% 的患者可靠,概率也提高到了 92%。这意味着通常,加入尽可能多的独立随机变量来计算后验概率,以提高准确性和信心,是一个非常好的选择。

b) 在这里,患者有病毒 V 的症状,但测试结果为阴性。因此,我们有以下内容:

因此,某患者在测试中结果为阴性,但有病毒症状V,其感染病毒的概率为 0.48%。

问题 3:我们应用了贝叶斯定理的基本形式:

记录到的地震后,发生海啸的概率为 33%。

请注意,这里我们将P(海啸)设置为过去 100 年某一天发生海啸的概率。我们也使用了一天作为单位来计算P(地震)的概率。如果我们将单位改为小时、周、月等,P(海啸)P(地震)的结果仍然会保持不变。计算中的关键在于比例P(海啸)😛(地震)=4:6=2/3:1,也就是说,海啸发生的概率大约是地震的 66%。

问题 4:我们将数据输入程序,计算从观测数据得出的后验概率,得到以下结果:

[['否', '是', '是', '否', {'是': 0.0, '否': 1.0}]]

根据这个计算,患者应当没有感染病毒。然而,'否'的概率似乎相当高。为了获得更精确的健康概率估计,获取更多的数据可能是个好主意。

问题 5

a) 算法的结果如下:

[['是', '否', '是', '否', '是', {'是': 0.8459918784779665, '否': 0.15400812152203341}]]

因此,根据朴素贝叶斯算法,当应用于表格中的数据时,电子邮件是垃圾邮件的概率大约为 85%。

b) 这种方法可能不如预期,因为垃圾邮件中某些词语的出现并不是独立的。例如,包含“money”(钱)一词的垃圾邮件,通常会试图说服受害者相信他们可以通过垃圾邮件发送者获得金钱,因此其他词语,如Rich(富有)、Secret(秘密)或Free(免费)等,更有可能出现在这类邮件中。最近邻算法在垃圾邮件分类中可能表现得更好。你可以通过交叉验证来验证实际方法。

问题 6:对于这个问题,我们将使用扩展的贝叶斯定理,适用于连续和离散随机变量:

让我们通过以下表格总结给定的信息:

性别 平均身高 身高方差
男性 176.8 37.2
女性 163.4 30.8
性别 平均体重 体重方差
男性 72.4 53.8
女性 53 22.5

从这些数据中,让我们确定确定最终为男性的概率所需的其他值:

因此,身高 172 厘米、体重 60 公斤且有长发的人是男性的概率为 20.3%。因此,他们更可能是女性。

第三章:决策树

决策树是将数据按树状结构排列的方式,在每个节点处,根据该节点的属性值将数据分成不同的分支。

为了构建决策树,我们将使用标准的 ID3 学习算法,该算法选择一个属性,能以最好的方式对数据样本进行分类,从而最大化信息增益——这是一个基于信息熵的度量方法。

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

  • 决策树是什么,如何通过游泳偏好的示例在决策树中表示数据

  • 信息熵和信息增益的概念,首先在理论上进行说明,然后在信息论部分通过游泳偏好的示例进行实际应用

  • 如何使用 ID3 算法从训练数据构建决策树,并在 Python 中实现

  • 如何通过游泳偏好的示例,使用构建的决策树对新的数据项进行分类

  • 如何使用决策树对第二章中的国际象棋问题进行替代分析,采用朴素贝叶斯方法,并比较两种算法结果的差异

  • 你将在问题部分验证自己的理解,并了解何时使用决策树作为分析方法

  • 如何在决策树构建过程中处理数据不一致性,使用购物问题示例进行说明

游泳偏好——通过决策树表示数据

我们可能有一些偏好,决定是否游泳。这些偏好可以记录在表格中,如下所示:

泳衣 水温 游泳偏好
温暖
温暖
温暖

这张表中的数据可以通过以下决策树呈现:

图 3.1:游泳偏好示例的决策树

在根节点,我们问一个问题——你有泳衣吗?问题的回答将可用数据分成三个组,每个组有两行。如果属性是泳衣 = 无,那么这两行的游泳偏好都是。因此,不需要再问水温的问题,因为所有具有泳衣 = 无属性的样本都会被分类为泳衣 = 小属性也一样。对于泳衣 = 好,剩下的两行可以分为两类——

如果没有更多的信息,我们将无法正确分类每一行。幸运的是,对于每一行,还有一个问题可以问,这个问题能够正确地将其分类。对于水=冷的行,游泳偏好是;对于水=温暖的行,游泳偏好是

总结一下,从根节点开始,我们在每个节点上提问,并根据答案向下移动树直到到达叶节点,在那里我们可以找到与这些答案对应的数据项的类别。

这就是我们如何使用现成的决策树来分类数据样本。但同样重要的是,知道如何从数据中构建决策树。

哪个属性在什么节点上有一个问题?这如何影响决策树的构建?如果我们改变属性的顺序,最终得到的决策树能否比另一棵树更好地进行分类?

信息论

信息论研究信息的量化、存储和传递。我们引入了信息熵和信息增益的概念,它们用于使用 ID3 算法构建决策树。

信息熵

任何给定数据的信息熵是表示该数据项所需的最小信息量的度量。信息熵的单位是熟悉的——比特、字节、千字节等等。信息熵越低,数据就越规律,数据中出现的模式也就越多,从而表示该数据所需的信息量越小。这就是为什么计算机上的压缩工具可以将大型文本文件压缩到更小的大小,因为单词和短语不断重复,形成了模式。

抛硬币

假设我们抛一枚不偏的硬币。我们想知道结果是正面还是反面。我们需要多少信息来表示这个结果?无论是正面还是反面,两个单词都由四个字符组成,如果我们用一个字节(8 位)来表示一个字符,按照 ASCII 表的标准,那么我们需要四个字节,或者 32 位,来表示这个结果。

但信息熵是表示结果所需的最小数据量。我们知道结果只有两种可能——正面或反面。如果我们同意用 0 表示正面,用 1 表示反面,那么 1 比特足以有效地传达结果。在这里,数据是硬币抛掷结果可能性的空间。它是集合{head, tail},可以表示为集合{0, 1}。实际的结果是该集合中的一个数据项。结果表明,这个集合的熵是 1。因为正面和反面的概率都是 50%。

现在假设硬币是偏的,每次抛出时正面朝上的概率是 25%,反面朝上的概率是 75%。这时,概率空间{0,1}的熵是多少?我们当然可以用 1 比特的信息表示结果。但是我们能做得更好吗?1 比特当然是不可分割的,但也许我们可以将信息的概念推广到非离散的量。

在前面的例子中,除非我们查看硬币,否则我们对前一次抛硬币的结果一无所知。但在偏向硬币的例子中,我们知道结果更可能是反面。如果我们在文件中记录了n次抛硬币的结果,将正面表示为 0,反面表示为 1,那么其中大约 75%的比特将为 1,25%的比特将为 0。这样的文件大小将为n比特。但由于它更有规律(1 的模式占主导地位),一个好的压缩工具应该能够将其压缩到小于n比特。

为了学习压缩背后的理论以及表示数据项所需的信息量,让我们来看一下信息熵的精确定义。

信息熵的定义

假设我们给定了一个概率空间S,其中包含元素1, 2, ..., n。从概率空间中选择元素i的概率为p[i]。该概率空间的信息熵定义如下:

在前面的公式中,log[2]是二进制对数。

因此,公正抛硬币的概率空间的信息熵如下:

当硬币偏向时,正面出现的概率为 25%,反面为 75%,此时该空间的信息熵如下:

这个值小于 1。因此,例如,如果我们有一个大文件,其中约 25%的比特是 0,75%的比特是 1,那么一个好的压缩工具应该能够将其压缩到大约 81.12%的大小。

信息增益

信息增益是通过某一过程获得的信息熵量。例如,如果我们想知道三次公正的抛硬币结果,那么信息熵为 3。但如果我们能查看第三次抛掷的结果,那么剩下两次抛掷的结果的信息熵为 2。因此,通过查看第三次抛掷,我们获得了一位信息,所以信息增益为 1。

我们也可以通过将整个集合S划分为按相似模式分组的集合,来获得信息熵。如果我们按属性A的值对元素进行分组,那么我们将信息增益定义如下:

这里,S[v] 是集合S中所有属性A值为v的元素集合。

游泳偏好 – 信息增益计算

让我们通过将游泳衣作为属性,计算游泳偏好示例中六行数据的信息增益。因为我们关心的是给定数据行是否会被分类为,即是否应该去游泳的问题,因此我们将使用游泳偏好来计算熵和信息增益。我们用游泳衣属性来划分集合S

S[none]={(none,cold,no),(none,warm,no)}

S[small]={(small,cold,no),(small,warm,no)}

S[good]={(good,cold,no),(good,warm,yes)}

S 的信息熵为:

E(S)=

划分后的信息熵为:

E(S[none])=-(2/2)log2=-log2=0*,原因类似。

E(S[good])=

因此,信息增益为:

IG(S,泳装)=E(S)-[(2/6)E(S[none])+(2/6)E(S[small])+(2/6)E(S[good])]*

=0.65002242164-(1/3)=0.3166890883

如果我们选择 水温 属性来划分集合 S,那么信息增益 IG(S,水温) 会是多少?水温将集合 S 划分为以下集合:

S[cold]={(none,cold,no),(small,cold,no),(good,cold,no)}

S[warm]={(none,warm,no),(small,warm,no),(good,warm,yes)}

它们的熵如下:

E(S[cold])=0,因为所有实例都被分类为否。

E(S[warm])=-(2/3)log2-(1/3)log2~0.91829583405

因此,使用 水温 属性划分集合 S 得到的信息增益如下:

IG(S,水温)=E(S)-[(1/2)E(S[cold])+(1/2)E(S[warm])]

= 0.65002242164-0.50.91829583405=0.19087450461*

这小于 IG(S,泳装)。因此,我们可以通过根据 泳装 属性划分集合 S(其实例的分类)来获得更多信息,而不是使用 水温 属性。这一发现将成为 ID3 算法在下一节构建决策树的基础。

ID3 算法 – 决策树构建

ID3 算法基于信息增益从数据中构建决策树。一开始,我们从集合 S 开始。集合 S 中的数据项有各种属性,根据这些属性,我们可以对集合 S 进行划分。如果某个属性 A 有值 {v[1], ..., v[n]},那么我们将集合 S 划分为 S[1], ..., S[n],其中集合 S[i] 是集合 S 的一个子集,包含属性 A 对应值为 v[i] 的元素。

如果集合 S 中的每个元素都有属性 A[1], ..., A[m],那么我们可以根据任意可能的属性划分集合 S。ID3 算法根据产生最大信息增益的属性来划分集合 S。现在假设它有属性 A[1],那么对于集合 S,我们有划分 S[1], ..., S[n],其中 A[1] 的可能值为 {v[1], ..., v[n]}

由于我们还没有构建树,因此首先放置一个根节点。对于 S 的每一个划分,从根节点放置一条新分支。每一条分支代表选定属性的一个值。一条分支包含该属性具有相同值的数据样本。对于每一条新分支,我们可以定义一个新节点,该节点将包含其祖先分支的数据样本。

一旦我们定义了一个新的节点,我们就选择剩余属性中信息增益最高的属性,以便进一步划分该节点上的数据,然后定义新的分支和节点。这个过程可以重复进行,直到节点上的所有属性用完,或者更早地,当节点上的所有数据具有相同的分类(我们感兴趣的类)时。在游泳偏好示例的情况下,游泳偏好只有两个可能的类别——no类和yes类。最后的节点称为叶节点,并决定数据项的分类。

游泳偏好 – 通过 ID3 算法构建的决策树

在这里,我们一步一步地描述 ID3 算法如何从给定的游泳偏好示例数据中构建决策树。初始集合由六个数据样本组成:

S={(none,cold,no),(small,cold,no),(good,cold,no),(none,warm,no),(small,warm,no),(good,warm,yes)}

在前面的章节中,我们计算了两个属性的增益,以及唯一的非分类属性游泳衣水温的增益,计算如下:

IG(S,swimming suit)=0.3166890883
IG(S,water temperature)=0.19087450461

因此,我们会选择游泳衣属性,因为它具有较高的信息增益。此时还没有画出树,所以我们从根节点开始。由于游泳衣属性有三个可能的值——{none, small, good},我们为每个值绘制三个可能的分支。每个分支将有一个来自划分集S:S[none]S[small],和S[good]的划分。我们在分支的末端添加节点。S[none]数据样本具有相同的游泳偏好类别=no,因此我们不需要用进一步的属性来分支该节点并划分集合。因此,包含数据S[none]的节点已经是叶节点。对于包含数据S[small]的节点也一样。

但是,包含数据的节点,S[good],有两种可能的游泳偏好类别。因此,我们会进一步分支该节点。剩下的唯一非分类属性是水温。因此,使用数据S[good]计算该属性的信息增益就没有必要了。从节点S[good]开始,我们会有两个分支,每个分支都来自集合S[good]的一个划分。一个分支将包含数据样本S[good,][cold]={(good,cold,no)};另一个分支将包含划分S[good,][warm]={(good,warm,yes)}。这两个分支将以一个节点结束。每个节点将是叶节点,因为每个节点的数据样本在分类游泳偏好属性上具有相同的值。

结果决策树有四个叶节点,并且是图 3.1 – 游泳偏好示例的决策树中的树。

实现

我们实现了一个 ID3 算法,它从 CSV 文件中给定的数据构建决策树。所有的源代码都位于章节目录中。源代码中最重要的部分如下:

# source_code/3/construct_decision_tree.py
# Constructs a decision tree from data specified in a CSV file.
# Format of a CSV file:
# Each data item is written on one line, with its variables separated
# by a comma. The last variable is used as a decision variable to
# branch a node and construct the decision tree.

import math
# anytree module is used to visualize the decision tree constructed by
# this ID3 algorithm.
from anytree import Node, RenderTree
import sys
sys.path.append('../common')
import common
import decision_tree

# Program start csv_file_name = sys.argv[1]
verbose = int(sys.argv[2])  # verbosity level, 0 - only decision tree

# Define the enquired column to be the last one.
# I.e. a column defining the decision variable.
(heading, complete_data, incomplete_data,
 enquired_column) = common.csv_file_to_ordered_data(csv_file_name)

tree = decision_tree.constuct_decision_tree(
    verbose, heading, complete_data, enquired_column)
decision_tree.display_tree(tree)
# source_code/common/decision_tree.py
# ***Decision Tree library ***
# Used to construct a decision tree and a random forest.
import math
import random
import common
from anytree import Node, RenderTree
from common import printfv

# Node for the construction of a decision tree.
class TreeNode: 
    def __init__(self, var=None, val=None):
        self.children = []
        self.var = var
        self.val = val

    def add_child(self, child):
        self.children.append(child)

    def get_children(self):
        return self.children

    def get_var(self):
        return self.var

    def get_val(self):
        return self.val

    def is_root(self):
        return self.var is None and self.val is None

    def is_leaf(self):
        return len(self.children) == 0

    def name(self):
        if self.is_root():
            return "[root]"
        return "[" + self.var + "=" + self.val + "]"

# Constructs a decision tree where heading is the heading of the table
# with the data, i.e. the names of the attributes.
# complete_data are data samples with a known value for every attribute.
# enquired_column is the index of the column (starting from zero) which
# holds the classifying attribute.
def construct_decision_tree(verbose, heading, complete_data, enquired_column):
    return construct_general_tree(verbose, heading, complete_data,
                                  enquired_column, len(heading))

# m is the number of the classifying variables that should be at most
# considered at each node. *m needed only for a random forest.*
def construct_general_tree(verbose, heading, complete_data,
                           enquired_column, m):
    available_columns = []
    for col in range(0, len(heading)):
        if col != enquired_column:
            available_columns.append(col)
    tree = TreeNode()
    printfv(2, verbose, "We start the construction with the root node" +
                        " to create the first node of the tree.\n")
    add_children_to_node(verbose, tree, heading, complete_data,
                         available_columns, enquired_column, m)
    return tree

# Splits the data samples into the groups with each having a different
# value for the attribute at the column col.
def split_data_by_col(data, col):
    data_groups = {}
    for data_item in data:
        if data_groups.get(data_item[col]) is None:
            data_groups[data_item[col]] = []
        data_groups[data_item[col]].append(data_item)
    return data_groups

# Adds a leaf node to node.
def add_leaf(verbose, node, heading, complete_data, enquired_column):
    leaf_node = TreeNode(heading[enquired_column],
                         complete_data[0][enquired_column])
    printfv(2, verbose,
            "We add the leaf node " + leaf_node.name() + ".\n")
    node.add_child(leaf_node)

# Adds all the descendants to the node.
def add_children_to_node(verbose, node, heading, complete_data,
                         available_columns, enquired_column, m):
    if len(available_columns) == 0:
        printfv(2, verbose, "We do not have any available variables " +
                "on which we could split the node further, therefore " +
                "we add a leaf node to the current branch of the tree. ")
        add_leaf(verbose, node, heading, complete_data, enquired_column)
        return -1

    printfv(2, verbose, "We would like to add children to the node " +
            node.name() + ".\n")

    selected_col = select_col(
        verbose, heading, complete_data, available_columns,
        enquired_column, m)
    for i in range(0, len(available_columns)):
        if available_columns[i] == selected_col:
            available_columns.pop(i)
            break

    data_groups = split_data_by_col(complete_data, selected_col)
    if (len(data_groups.items()) == 1):
        printfv(2, verbose, "For the chosen variable " +
                heading[selected_col] +
                " all the remaining features have the same value " +
                complete_data[0][selected_col] + ". " +
                "Thus we close the branch with a leaf node. ")
        add_leaf(verbose, node, heading, complete_data, enquired_column)
        return -1

    if verbose >= 2:
        printfv(2, verbose, "Using the variable " +
                heading[selected_col] +
                " we partition the data in the current node, where" +
                " each partition of the data will be for one of the " +
                "new branches from the current node " + node.name() +
                ". " + "We have the following partitions:\n")
        for child_group, child_data in data_groups.items():
            printfv(2, verbose, "Partition for " +
                    str(heading[selected_col]) + "=" +
                    str(child_data[0][selected_col]) + ": " +
                    str(child_data) + "\n")
        printfv(
            2, verbose, "Now, given the partitions, let us form the " +
                        "branches and the child nodes.\n")
    for child_group, child_data in data_groups.items():
        child = TreeNode(heading[selected_col], child_group)
        printfv(2, verbose, "\nWe add a child node " + child.name() +
                " to the node " + node.name() + ". " +
                "This branch classifies %d feature(s): " +
                str(child_data) + "\n", len(child_data))
        add_children_to_node(verbose, child, heading, child_data, list(
            available_columns), enquired_column, m)
        node.add_child(child)
    printfv(2, verbose,
            "\nNow, we have added all the children nodes for the " +
            "node " + node.name() + ".\n")

# Selects an available column/attribute with the highest
# information gain.
def select_col(verbose, heading, complete_data, available_columns,
               enquired_column, m):
    # Consider only a subset of the available columns of size m.
    printfv(2, verbose,
            "The available variables that we have still left are " +
            str(numbers_to_strings(available_columns, heading)) + ". ")
    if len(available_columns) < m:
        printfv(
            2, verbose, "As there are fewer of them than the " +
                        "parameter m=%d, we consider all of them. ", m)
        sample_columns = available_columns
    else:
        sample_columns = random.sample(available_columns, m)
        printfv(2, verbose,
                "We choose a subset of them of size m to be " +
                str(numbers_to_strings(available_columns, heading)) +
                ".")

    selected_col = -1
    selected_col_information_gain = -1
    for col in sample_columns:
        current_information_gain = col_information_gain(
            complete_data, col, enquired_column)
        # print len(complete_data),col,current_information_gain
        if current_information_gain > selected_col_information_gain:
            selected_col = col
            selected_col_information_gain = current_information_gain
    printfv(2, verbose,
            "Out of these variables, the variable with " +
            "the highest information gain is the variable " +
            heading[selected_col] +
            ". Thus we will branch the node further on this " +
            "variable. " +
            "We also remove this variable from the list of the " +
            "available variables for the children of the current node. ")
    return selected_col

# Calculates the information gain when partitioning complete_data
# according to the attribute at the column col and classifying by the
# attribute at enquired_column.
def col_information_gain(complete_data, col, enquired_column):
    data_groups = split_data_by_col(complete_data, col)
    information_gain = entropy(complete_data, enquired_column)
    for _, data_group in data_groups.items():
        information_gain -= (float(len(data_group)) / len(complete_data)
                             ) * entropy(data_group, enquired_column)
    return information_gain

# Calculates the entropy of the data classified by the attribute
# at the enquired_column.
def entropy(data, enquired_column):
    value_counts = {}
    for data_item in data:
        if value_counts.get(data_item[enquired_column]) is None:
            value_counts[data_item[enquired_column]] = 0
        value_counts[data_item[enquired_column]] += 1
    entropy = 0
    for _, count in value_counts.items():
        probability = float(count) / len(data)
        entropy -= probability * math.log(probability, 2)
    return entropy

程序输入

我们将游泳偏好示例的数据输入程序以构建决策树:

# source_code/3/swim.csv
swimming_suit,water_temperature,swim
None,Cold,No
None,Warm,No
Small,Cold,No
Small,Warm,No
Good,Cold,No
Good,Warm,Yes 

程序输出

我们从 swim.csv 数据文件构建了一个决策树,冗余度设置为 0。建议读者将冗余度设置为 2,以便查看决策树构建过程的详细解释:

$ python construct_decision_tree.py swim.csv 0 Root
├── [swimming_suit=Small]
│ ├── [water_temperature=Cold]
│ │ └── [swim=No]
│ └── [water_temperature=Warm]
│   └── [swim=No]
├── [swimming_suit=None]
│ ├── [water_temperature=Cold]
│ │ └── [swim=No]
│ └── [water_temperature=Warm]
│   └── [swim=No]
└── [swimming_suit=Good]
    ├── [water_temperature=Cold]
    │   └── [swim=No]
    └── [water_temperature=Warm]
        └── [swim=Yes]

使用决策树进行分类

一旦我们从具有属性 A[1], ..., A[m] 和类别 {c[1], ..., c[k]} 的数据中构建了决策树,我们就可以使用该决策树将一个具有属性 A[1], ..., A[m] 的新数据项分类到类别 {c[1], ..., c[k]} 中。

给定一个我们希望分类的新数据项,我们可以将每个节点,包括根节点,视为对数据样本的一个问题:该数据样本在选定属性 A[i] 上的值是什么? 然后,根据答案,我们选择决策树的一个分支并继续前进到下一个节点。然后,关于数据样本会再问一个问题,继续这样下去,直到数据样本到达叶节点。叶节点与某个类别 {c[1], ..., c[k]} 相关联;例如,c[i]。然后,决策树算法将该数据样本分类到该类别 c[i] 中。

使用游泳偏好决策树对数据样本进行分类

让我们使用 ID3 算法为游泳偏好示例构建决策树。现在我们已经构建了决策树,我们希望使用它将一个数据样本,(好,冷,?) 分类到 {否, 是} 这两个类别中的一个。

从树的根开始一个数据样本。第一个从根分支出的属性是 泳衣,所以我们询问样本的 泳衣 属性值 (好,冷,?)。我们得知该属性值为 泳衣=好;因此,沿着带有该值的数据样本的最右分支向下移动。我们到达了一个带有 水温 属性的节点,并问:对于该数据样本,水温属性的值是什么(好,冷,?)?我们得知对于该数据样本,水温为 ;因此,我们向下移动到左侧分支进入叶节点。这个叶节点与 游泳偏好=否 类别相关联。因此,决策树会将数据样本 (好,冷,?) 分类为该游泳偏好类别;换句话说,完成它(通过用确切的数据替换问号),变为数据样本 (好,冷,否)。

因此,决策树表示,如果你有一件好的泳衣,但水温很冷,那么根据表中收集的数据,你仍然不想游泳。

下棋——使用决策树分析

我们再来一个来自第二章的例子,朴素贝叶斯

温度 风力 阳光 游玩
强风 多云
强风 多云
温暖 无风 晴天
炎热 无风 晴天
炎热 微风 多云
温暖 微风 晴天
微风 多云
无风 晴天
强风 阴天
温暖 无风 阴天
温暖 强风 晴天 ?

我们想知道我们的朋友是否愿意在公园里和我们下棋。这一次,我们希望使用决策树来找到答案。

分析

我们有初始的数据集S,如下所示:

S={(Cold,Strong,Cloudy,No),(Warm,Strong,Cloudy,No),(Warm,None,Sunny,Yes), (Hot,None,Sunny,No),(Hot,Breeze,Cloudy,Yes),(Warm,Breeze,Sunny,Yes),(Cold,Breeze,Cloudy,No),(Cold,None,Sunny,Yes),(Hot,Strong,Cloudy,Yes),(Warm,None,Cloudy,Yes)}

首先,我们确定三个非分类属性的信息增益temperature(温度)、wind(风速)、sunshine(阳光)。temperature的可能值为Cold(冷)、Warm(温暖)和Hot(热)。因此,我们将集合S划分为三个子集:

Scold={(Cold,Strong,Cloudy,No),(Cold,Breeze,Cloudy,No),(Cold,None,Sunny,Yes)}
Swarm={(Warm,Strong,Cloudy,No),(Warm,None,Sunny,Yes),(Warm,Breeze,Sunny,Yes),(Warm,None,Cloudy,Yes)}
Shot={(Hot,None,Sunny,No),(Hot,Breeze,Cloudy,Yes),(Hot,Strong,Cloudy,Yes)}

我们首先计算这些集合的信息熵:

wind(风速)属性的可能值为None(无风)、Breeze(微风)和Strong(强风)。因此,我们将集合S划分为三个子集:

这些集合的信息熵如下:

最后,第三个属性Sunshine(阳光)有两个可能值:Cloudy(阴天)和Sunny(晴天)。因此,它将集合S划分为两个子集:

这些集合的信息熵如下:

IG(S,wind)IG(S,temperature)大于IG(S,sunshine)。这两个值相等,因此我们可以选择任意一个属性来形成三个分支;例如,选择第一个属性Temperature(温度)。在这种情况下,每个分支会包含数据样本S[cold]S[warm]S[hot]。在这些分支上,我们可以进一步应用算法来形成剩余的决策树。相反,我们可以使用程序来完成这个过程:

输入

source_code/3/chess.csv
Temperature,Wind,Sunshine,Play
Cold,Strong,Cloudy,No
Warm,Strong,Cloudy,No
Warm,None,Sunny,Yes
Hot,None,Sunny,No
Hot,Breeze,Cloudy,Yes
Warm,Breeze,Sunny,Yes
Cold,Breeze,Cloudy,No
Cold,None,Sunny,Yes
Hot,Strong,Cloudy,Yes
Warm,None,Cloudy,Yes

输出

$ python construct_decision_tree.py chess.csv 0
Root
├── [Temperature=Cold]
│ ├── [Wind=Breeze]
│ │ └── [Play=No]
│ ├── [Wind=Strong]
│ │ └── [Play=No]
│ └── [Wind=None]
│   └── [Play=Yes]
├── [Temperature=Warm]
│ ├── [Wind=Breeze]
│ │ └── [Play=Yes]
│ ├── [Wind=None]
│ │ ├── [Sunshine=Sunny]
│ │ │ └── [Play=Yes]
│ │ └── [Sunshine=Cloudy]
│ │   └── [Play=Yes]
│ └── [Wind=Strong]
│   └── [Play=No]
└── [Temperature=Hot]
    ├── [Wind=Strong]
    │ └── [Play=Yes]
    ├── [Wind=None]
    │ └── [Play=No]
    └── [Wind=Breeze]
        └── [Play=Yes]

分类

现在我们已经构建了决策树,我们想用它来将一个数据样本(warm,strong,sunny,?)分类到集合{no,yes}中的一个类别。

我们从根节点开始。该实例的temperature属性值是什么?是温暖,因此我们进入中间分支。该实例的wind属性值是什么?是,因此该实例将归类为,因为我们已经到达了叶节点。

因此,根据决策树分类算法,我们的朋友不会在公园与我们下棋。请注意,朴素贝叶斯算法得出的结论恰好相反。要选择最佳的方法,需要对问题有一定的理解。在其他时候,准确性更高的方法是考虑多个算法或多个分类器结果的方法,正如在第四章中所示的随机森林算法。

去购物 – 处理数据不一致性

我们有以下关于我们朋友简的购物偏好的数据:

温度 购物
温暖
温暖
温暖
?

我们希望通过决策树来找出,如果外部温度寒冷且没有雨,简是否会去购物。

分析

在这里,我们需要小心,因为某些数据实例在相同属性下有相同的值,但类别不同;即(cold,none,yes)(cold,none,no)。我们编写的程序会形成以下决策树:

    Root
    ├── [Temperature=Cold]
    │    ├──[Rain=None]
    │    │    └──[Shopping=Yes]
    │    └──[Rain=Strong]
    │         └──[Shopping=Yes]
    └── [Temperature=Warm]
         ├──[Rain=None]
         │    └──[Shopping=No]
         └── [Rain=Strong]
              └── [Shopping=No]

但在叶节点[Rain=None]且父节点为[Temperature=Cold]时,有两个数据样本分别属于两个类别。因此,我们无法准确地分类一个实例(cold,none,?)。为了使决策树算法更有效,我们需要在叶节点提供权重最大、即占多数的类别。一个更好的方法是为数据样本收集更多属性值,这样我们就可以更准确地做出决策。

因此,鉴于现有数据,我们无法确定简是否会去购物。

概要

在这一章中,我们了解了决策树 ID3 算法如何从输入数据中首先构建决策树,然后使用构建好的树对新数据实例进行分类。决策树是通过选择信息增益最高的属性来进行分支构建的。我们研究了信息增益如何衡量在信息熵增益方面能够学到的知识量。

我们还了解到,决策树算法可能与其他算法,如朴素贝叶斯算法,产生不同的结果。

在下一章中,我们将学习如何将多种算法或分类器组合成一个决策森林(称为随机森林),以获得更准确的结果。

问题

问题 1:以下多重集合的信息熵是多少?

a) {1,2},b) {1,2,3},c) {1,2,3,4},d) {1,1,2,2},e) {1,1,2,3}

问题 2:偏置硬币的概率空间的信息熵是多少?该硬币显示正面概率为 10%,反面为 90%?

问题 3:让我们来看一下第二章中的另一个例子,朴素贝叶斯,关于下棋的例子:

a) 表中每个非分类属性的信息增益是多少?

b) 根据给定的表格,构建出的决策树是什么?

c) 如何根据构建的决策树对数据样本(温暖, 强风, 春季, ?)进行分类?

温度 风速 季节 感受
寒冷 强风 冬季
温暖 强风 秋季
温暖 夏季
春季
微风 秋季
温暖 微风 春季
寒冷 微风 冬季
寒冷 春季
强风 夏季
温暖 秋季
温暖 强风 春季 ?

问题 4玛丽与温度偏好:让我们来看一下第一章中的例子,使用 K 近邻进行分类,关于玛丽的温度偏好:

温度(°C) 风速(km/h) 玛丽的感知
10 0 寒冷
25 0 温暖
15 5 寒冷
20 3 温暖
18 7 寒冷
20 10 寒冷
22 5 温暖
24 6 温暖

我们想使用决策树来判断我们的朋友玛丽在温度为 16°C,风速为 3 km/h 的房间中会感觉温暖还是寒冷。

你能解释一下如何在这里使用决策树算法吗?并说明使用它对于这个例子的益处?

分析

问题 1:以下是多重集的熵值:

a)

b)

c)

d)

e)

需要注意的是,具有多个类别的多重集的信息熵大于 1,因此我们需要多个比特来表示结果。但对于每个具有多个类别元素的多重集,这种情况都成立吗?

问题 2

问题 3:a) 三个属性的信息增益如下:

b) 因此,我们会选择季节属性作为从根节点分支的依据,因为它具有最高的信息增益。或者,我们可以将所有输入数据放入程序中,构建决策树,如下所示:

    Root
    ├── [Season=Autumn]
    │    ├──[Wind=Breeze]
    │    │    └──[Play=Yes]
    │    ├──[Wind=Strong]
    │    │    └──[Play=No]
    │    └──[Wind=None]
    │         └──[Play=Yes]
    ├── [Season=Summer]
    │    ├──[Temperature=Hot]
    │    │    └──[Play=Yes]
    │    └──[Temperature=Warm]
    │         └──[Play=Yes]
    ├── [Season=Winter]
    │    └──[Play=No]
    └── [Season=Spring]
         ├── [Temperature=Hot]
         │    └──[Play=No]
         ├── [Temperature=Warm]
         │    └──[Play=Yes]
         └── [Temperature=Cold]
              └── [Play=Yes]

c) 根据构建的决策树,我们将数据样本(warm,strong,spring,?)分类为Play=Yes,方法是从根节点向下走到最底层分支,然后通过走中间分支到达叶子节点。

问题 4:在这里,决策树算法在没有对数据进行任何处理的情况下可能表现不佳。如果我们考虑每一种温度类别,那么 25°C 仍然不会出现在决策树中,因为它不在输入数据中,因此我们无法分类玛丽在 16°C 和 3 km/h 的风速下的感受。

我们可以选择将温度和风速划分为区间,以减少类别数,从而使得最终的决策树能够对输入实例进行分类。但正是这种划分,即 25°C 和 3 km/h 应该归类到哪些区间,才是分析这种问题的基本过程。因此,未经任何重大修改的决策树无法很好地分析这个问题。

第四章:随机森林

随机森林是由一组随机决策树组成的(类似于第三章中描述的决策树),每棵树都是在数据的随机子集上生成的。随机森林通过大多数随机决策树投票所选的类别来分类特征。与决策树相比,随机森林通常能够提供更准确的分类,因为它们的偏差和方差较低。

本章将涵盖以下主题:

  • 作为随机森林构建的一部分,树袋法(或自助聚合)技术,但也可以扩展到数据科学中的其他算法和方法,以减少偏差和方差,从而提高准确性

  • 如何构建一个随机森林并使用通过游泳偏好示例构建的随机森林对数据项进行分类

  • 如何在 Python 中实现一个构建随机森林的算法

  • 使用下棋的例子,分析 Naive Bayes 算法、决策树和随机森林的区别

  • 随机森林算法如何克服决策树算法的缺点,并通过购物的例子展示其优势

  • 如何通过购物的例子展示随机森林如何表达其对特征分类的置信度

  • 如何通过减少分类器的方差来获得更准确的结果,在问题部分进行讨论

随机森林算法简介

通常,为了构建一个随机森林,首先我们需要选择它包含的树的数量。随机森林通常不会过拟合(除非数据非常嘈杂),因此选择更多的决策树不会降低预测的准确性。随机森林通常不会过拟合(除非数据非常嘈杂),因此增加决策树的数量不会显著降低预测准确性。重要的是要选择足够数量的决策树,以便在随机选择用于构建决策树的数据时,更多的数据用于分类。另一方面,树木越多,所需的计算能力越大。此外,增加决策树的数量并不会显著提高分类准确度。

在实践中,你可以在特定数量的决策树上运行算法,增加它们的数量,并比较较小和较大森林的分类结果。如果结果非常相似,那么就没有必要增加树木的数量。

为了简化演示,本书中我们将使用少量的决策树来构建随机森林。

随机森林构建概述

我们将描述如何以随机的方式构建每棵树。我们通过随机选择N个训练特征来构建决策树。这个从数据中随机选择并进行替换的过程,称为自助聚合(bootstrap aggregating),或树袋法(tree bagging)。自助聚合的目的是减少分类结果中的方差和偏差。

假设一个特征有M个变量,这些变量用于使用决策树对特征进行分类。当我们在节点上做出分支决策时,在 ID3 算法中,我们选择产生最大信息增益的变量。在这里,在随机决策树中,每个节点我们最多只考虑m个变量。我们不考虑那些已经被随机选择且没有替换的M个变量中的变量。然后,在这m个变量中,我们选择产生最大信息增益的变量。

随机决策树的其余构建过程与第三章中的决策树构建过程相同,参见决策树部分。

游泳偏好——涉及随机森林的分析

我们将使用来自第三章的关于游泳偏好的示例。我们有相同的数据表,具体如下:

泳衣 水温 游泳偏好
温暖
温暖
温暖

我们希望从这些数据构建一个随机森林,并使用它来对一个项目(好,冷,?)进行分类。

分析

我们给定了M=3个变量,根据这些变量可以对特征进行分类。在随机森林算法中,我们通常不会在每个节点使用所有三个变量来形成树枝。我们只会使用从M中随机选择的一个子集(m)的变量。所以我们选择m使得m小于或等于Mm的值越大,每个构建的树中的分类器就越强。然而,正如前面提到的,更多的数据会导致更多的偏差。但是,因为我们使用多棵树(每棵树的m较小),即使每棵构建的树是一个弱分类器,它们的联合分类准确度仍然较强。为了减少随机森林中的偏差,我们可能希望选择一个略小于Mm参数。

因此,我们选择节点中考虑的最大变量数量为:m=min(M,math.ceil(2math.sqrt(M)))=min(M,math.ceil(2math.sqrt(3)))=3

我们提供以下特征:

[['None', 'Cold', 'No'], ['None', 'Warm', 'No'], ['Small', 'Cold', 'No'], ['Small', 'Warm', 'No'], ['Good', 'Cold', 'No'], ['Good', 'Warm', 'Yes']]

在构建随机森林中的随机决策树时,我们只会随机选择这些特征的一个子集,并且进行替换。

随机森林构建

我们将构建一个由两棵随机决策树组成的随机森林。

随机决策树构建编号 0

我们得到六个特征作为输入数据。在这些特征中,我们随机选择六个特征并带有替换地用于构建这个随机决策树:

[['None', 'Warm', 'No'], ['None', 'Warm', 'No'], ['Small', 'Cold', 'No'], ['Good', 'Cold', 'No'], ['Good', 'Cold', 'No'], ['Good', 'Cold', 'No']]

我们从根节点开始构建,创建树的第一个节点。我们想要向[root]节点添加子节点。

我们有以下可用变量:['swimming_suit', 'water_temperature']。由于这些变量少于m=3参数的数量,我们将考虑这两个变量。在这些变量中,信息增益最高的是游泳衣。

因此,我们将根据该变量继续分支节点。我们还将从当前节点的子节点可用变量列表中移除此变量。使用swimming_suit变量,我们在当前节点上划分数据,如下所示:

  • swimming_suit=Small的划分:[['Small', 'Cold', 'No']]

  • swimming_suit=None的划分:[['None', 'Warm', 'No'], ['None', 'Warm', 'No']]

  • swimming_suit=Good的划分:[['Good', 'Cold', 'No'], ['Good', 'Cold', 'No'], ['Good', 'Cold', 'No']]

使用前述的划分,我们创建了分支和子节点。

现在我们向[root]节点添加一个子节点[swimming_suit=Small]。该分支对一个特征进行分类:[['Small', 'Cold', 'No']]

我们希望向[swimming_suit=Small]节点添加子节点。

我们有以下可用变量:['water_temperature']。由于这里只有一个变量,且少于m=3参数,因此我们将考虑这个变量。信息增益最高的是water_temperature变量。因此,我们将根据该变量继续分支节点。我们还将从当前节点的子节点可用变量列表中移除此变量。对于选定的变量water_temperature,所有剩余特征的值都是Cold。因此,我们在此分支的末尾加上一个叶节点,添加[swim=No]

现在我们向[root]节点添加一个子节点[swimming_suit=None]。该分支对两个特征进行分类:[['None', 'Warm', 'No'], ['None', 'Warm', 'No']]

我们希望向[swimming_suit=None]节点添加子节点。

我们有以下可用变量:['water_temperature']。由于这里只有一个变量,且少于m=3参数,因此我们将考虑这个变量。信息增益最高的是water_temperature变量。因此,我们将根据该变量继续分支节点。我们还将从当前节点的子节点可用变量列表中移除此变量。对于选定的变量water_temperature,所有剩余特征的值都是Warm。因此,我们在此分支的末尾加上一个叶节点,添加[swim=No]

现在我们向[root]节点添加一个子节点[swimming_suit=Good]。该分支对三个特征进行分类:[['Good', 'Cold', 'No'], ['Good', 'Cold', 'No'], ['Good', 'Cold', 'No']]

我们希望向[swimming_suit=Good]节点添加子节点。

我们可以使用以下变量:['water_temperature']。由于这里只有一个变量,它小于m=3参数,因此我们会考虑这个变量。信息增益最高的变量是water_temperature变量。因此,我们将继续在这个变量上分支。我们还会将该变量从当前节点子节点的可用变量列表中移除。对于选择的变量water_temperature,所有剩余特征的值相同:Cold。所以,我们以叶节点结束这个分支,添加[swim=No]

现在,我们已经将所有子节点添加到[root]节点。

随机决策树 1 的构建

我们提供了六个特征作为输入数据。我们从中随机选择六个特征,并允许重复,以构建这个随机决策树:

[['Good', 'Warm', 'Yes'], ['None', 'Warm', 'No'], ['Good', 'Cold', 'No'], ['None', 'Cold', 'No'], ['None', 'Warm', 'No'], ['Small', 'Warm', 'No']]

随机决策树 1 的其余构建过程与之前的随机决策树 0 类似。唯一的区别是,树是使用不同的随机生成子集(如之前所见)构建的。

我们从根节点开始构建,以创建树的第一个节点。我们希望向[root]节点添加子节点。

我们可以使用以下变量:['swimming_suit', 'water_temperature']。由于这里只有一个变量,它小于m=3参数,因此我们会考虑这个变量。在这些变量中,信息增益最高的是swimming_suit变量。

因此,我们将在这个变量上继续分支。我们还会将该变量从当前节点子节点的可用变量列表中移除。使用swimming_suit变量,我们将当前节点的数据划分如下:

  • swimming_suit=Small的划分:[['Small', 'Warm', 'No']]

  • swimming_suit=None的划分:[['None', 'Warm', 'No'], ['None', 'Cold', 'No'], ['None', 'Warm', 'No']]

  • swimming_suit=Good的划分:[['Good', 'Warm', 'Yes'], ['Good', 'Cold', 'No']]

现在,给定划分后,让我们创建分支和子节点。我们向[root]节点添加一个子节点[swimming_suit=Small]。这个分支分类了一个特征:[['Small', 'Warm', 'No']]

我们希望向[swimming_suit=Small]节点添加子节点。

我们可以使用以下变量:['water_temperature']。由于这里只有一个变量,它小于m=3参数,因此我们会考虑这个变量。信息增益最高的变量是water_temperature变量。因此,我们将继续在这个变量上分支。我们还会将该变量从当前节点子节点的可用变量列表中移除。对于选择的变量water_temperature,所有剩余特征的值相同:Warm。所以,我们以叶节点结束这个分支,添加[swim=No]

我们将一个子节点[swimming_suit=None]添加到[root]节点。此分支分类了三个特征:[['None', 'Warm', 'No']][['None', 'Cold', 'No']][['None', 'Warm', 'No']]

我们想要为[swimming_suit=None]节点添加子节点。

我们有以下可用的变量:['water_temperature']。由于这里只有一个变量,这个数量小于m=3参数,我们将考虑这个变量。信息增益最高的是water_temperature变量。因此,我们将基于这个变量进一步拆分节点。我们还将从当前节点的子节点的可用变量列表中移除此变量。使用water_temperature变量,我们将数据在当前节点中进行如下分区:

  • water_temperature=Cold的分区:[['None', 'Cold', 'No']]

  • water_temperature=Warm的分区:[['None', 'Warm', 'No']];现在,根据这些分区,让我们创建分支和子节点。

我们将一个子节点[water_temperature=Cold]添加到[swimming_suit=None]节点。此分支分类了一个特征:[['None', 'Cold', 'No']]

我们没有任何可用的变量可以进一步拆分该节点;因此,我们在当前树分支上添加一个叶节点。我们添加了[swim=No]叶节点。我们将一个子节点[water_temperature=Warm]添加到[swimming_suit=None]节点。此分支分类了两个特征:[['None', 'Warm', 'No']][['None', 'Warm', 'No']]

我们没有任何可用的变量可以进一步拆分该节点;因此,我们在当前树分支上添加一个叶节点。我们添加了[swim=No]叶节点。

现在,我们已经将所有子节点添加到[swimming_suit=None]节点。

我们将一个子节点[swimming_suit=Good]添加到[root]节点。此分支分类了两个特征:[['Good', 'Warm', 'Yes']][['Good', 'Cold', 'No']]

我们想要为[swimming_suit=Good]节点添加子节点。

我们有以下可用的变量:['water_temperature']。由于这里只有一个变量,这个数量小于m=3参数,我们将考虑这个变量。信息增益最高的是water_temperature变量。因此,我们将基于这个变量进一步拆分节点。我们还将从当前节点的子节点的可用变量列表中移除此变量。使用water_temperature变量,我们将数据在当前节点中进行如下分区:

  • water_temperature=Cold的分区:[['Good', 'Cold', 'No']]

  • water_temperature=Warm的分区:[['Good', 'Warm', 'Yes']]

现在,根据这些分区,让我们创建分支和子节点。

我们将一个子节点[water_temperature=Cold]添加到[swimming_suit=Good]节点。此分支分类了一个特征:[['Good', 'Cold', 'No']]

我们没有任何可用的变量来进一步划分节点;因此,我们向当前树的分支添加了一个叶节点。我们添加了[swim=No]叶节点。

我们在[swimming_suit=Good]节点下添加了一个子节点[water_temperature=Warm]。该分支分类一个特征:[['Good', 'Warm', 'Yes']]

我们没有任何可用的变量来进一步划分节点;因此,我们向当前树的分支添加了一个叶节点。我们添加了[swim=Yes]叶节点。

现在,我们已经添加了[swimming_suit=Good]节点的所有子节点。

我们也已经添加了[root]节点的所有子节点。

构建的随机森林

我们已经完成了随机森林的构建,随机森林由两个随机决策树组成,如下方代码块所示:

Tree 0:
    Root
    ├── [swimming_suit=Small]
    │ └── [swim=No]
    ├── [swimming_suit=None]
    │ └── [swim=No]
    └── [swimming_suit=Good]
      └── [swim=No]
Tree 1:
    Root
    ├── [swimming_suit=Small]
    │ └── [swim=No]
    ├── [swimming_suit=None]
    │ ├── [water_temperature=Cold]
    │ │ └── [swim=No]
    │ └──[water_temperature=Warm]
    │   └── [swim=No]
    └── [swimming_suit=Good]
      ├──  [water_temperature=Cold] 
      │ └── [swim=No]
      └──  [water_temperature=Warm]
        └── [swim=Yes]
The total number of trees in the random forest=2.
The maximum number of the variables considered at the node is m=3.

使用随机森林进行分类

因为我们只使用了原始数据的一个子集来构建随机决策树,所以可能没有足够的特征来构建能够分类每个特征的完整树。在这种情况下,树不会为应该分类的特征返回任何类别。因此,我们只会考虑能够分类特定类别特征的树。

我们希望分类的特征是['Good', 'Cold', '?']。随机决策树通过相同的方法对特征进行分类,就像在第三章《决策树》中那样。树 0 为No类别投票。树 1 为No类别投票。获得最多投票的类别是No。因此,构建的随机森林将特征['Good', 'Cold', '?']分类为No类别。

随机森林算法的实现

我们使用上一章节中的修改版决策树算法实现了一个随机森林算法。我们还在程序中添加了一个选项,可以设置详细模式,描述算法在特定输入上的整个工作过程——如何用随机决策树构建一个随机森林,以及如何使用这个构建好的随机森林来分类其他特征。

鼓励你参考上一章节中的decision_tree.construct_general_tree函数:

# source_code/4/random_forest.py import mathimport randomimport sys
sys.path.append('../common')
import common # noqa
import decision_tree # noqa
from common import printfv # noqa

#Random forest construction
def sample_with_replacement(population, size):
    sample = []
    for i in range(0, size):
        sample.append(population[random.randint(0, len(population) - 1)])
    return sample

def construct_random_forest(verbose, heading, complete_data,
 enquired_column, m, tree_count):
    printfv(2, verbose, "*** Random Forest construction ***\n")
    printfv(2, verbose, "We construct a random forest that will " +
            "consist of %d random decision trees.\n", tree_count)
    random_forest = []
    for i in range(0, tree_count):
        printfv(2, verbose, "\nConstruction of a random " +
                "decision tree number %d:\n", i)
        random_forest.append(construct_random_decision_tree(
            verbose, heading, complete_data, enquired_column, m))
    printfv(2, verbose, "\nTherefore we have completed the " +
            "construction of the random forest consisting of %d " +
            "random decision trees.\n", tree_count)
    return random_forest

def construct_random_decision_tree(verbose, heading, complete_data,
 enquired_column, m):
    sample = sample_with_replacement(complete_data, len(complete_data))
    printfv(2, verbose, "We are given %d features as the input data. " +
            "Out of these, we choose randomly %d features with the " +
            "replacement that we will use for the construction of " +
            "this particular random decision tree:\n" +
            str(sample) + "\n", len(complete_data),
            len(complete_data))
# The function construct_general_tree from the module decision_tree
# is written in the implementation section in the previous chapter
# on decision trees.
    return decision_tree.construct_general_tree(verbose, heading,
                                                sample,
                                                enquired_column, m)

# M is the given number of the decision variables, i.e. properties
# of one feature.
def choose_m(verbose, M):
    m = int(min(M, math.ceil(2 * math.sqrt(M))))
    printfv(2, verbose, "We are given M=" + str(M) +
            " variables according to which a feature can be " +
            "classified. ")
    printfv(3, verbose, "In random forest algorithm we usually do " +
            "not use all " + str(M) + " variables to form tree " +
            "branches at each node. ")
    printfv(3, verbose, "We use only m variables out of M. ")
    printfv(3, verbose, "So we choose m such that m is less than or " +
            "equal to M. ")
    printfv(3, verbose, "The greater m is, a stronger classifier an " +
            "individual tree constructed is. However, it is more " +
            "susceptible to a bias as more of the data is considered. " +
            "Since we in the end use multiple trees, even if each may " +
            "be a weak classifier, their combined classification " +
            "accuracy is strong. Therefore as we want to reduce a " +
            "bias in a random forest, we may want to consider to " +
            "choose a parameter m to be slightly less than M.\n")
    printfv(2, verbose, "Thus we choose the maximum number of the " +
            "variables considered at the node to be " +
            "m=min(M,math.ceil(2*math.sqrt(M)))" +
            "=min(M,math.ceil(2*math.sqrt(%d)))=%d.\n", M, m)
    return m

#Classification
def display_classification(verbose, random_forest, heading,
 enquired_column, incomplete_data):
    if len(incomplete_data) == 0:
        printfv(0, verbose, "No data to classify.\n")
    else:
        for incomplete_feature in incomplete_data:
            printfv(0, verbose, "\nFeature: " +
                    str(incomplete_feature) + "\n")
            display_classification_for_feature(
                verbose, random_forest, heading,
                enquired_column, incomplete_feature)

def display_classification_for_feature(verbose, random_forest, heading,
 enquired_column, feature):
    classification = {}
    for i in range(0, len(random_forest)):
        group = decision_tree.classify_by_tree(
            random_forest[i], heading, enquired_column, feature)
        common.dic_inc(classification, group)
        printfv(0, verbose, "Tree " + str(i) +
                " votes for the class: " + str(group) + "\n")
    printfv(0, verbose, "The class with the maximum number of votes " +
            "is '" + str(common.dic_key_max_count(classification)) +
            "'. Thus the constructed random forest classifies the " +
            "feature " + str(feature) + " into the class '" +
            str(common.dic_key_max_count(classification)) + "'.\n")

# Program start
csv_file_name = sys.argv[1]
tree_count = int(sys.argv[2])
verbose = int(sys.argv[3])

(heading, complete_data, incomplete_data,
 enquired_column) = common.csv_file_to_ordered_data(csv_file_name)
m = choose_m(verbose, len(heading))
random_forest = construct_random_forest(
    verbose, heading, complete_data, enquired_column, m, tree_count)
display_forest(verbose, random_forest)
display_classification(verbose, random_forest, heading,
                       enquired_column, incomplete_data)

输入

作为已实现算法的输入文件,我们提供了来自游泳偏好示例的数据:

# source_code/4/swim.csv
swimming_suit,water_temperature,swim
None,Cold,No
None,Warm,No
Small,Cold,No
Small,Warm,No
Good,Cold,No
Good,Warm,Yes
Good,Cold,?

输出

我们在命令行中输入以下命令来获取输出:

$ python random_forest.py swim.csv 2 3 > swim.out

2表示我们希望构建两棵决策树,3表示程序的详细级别,包含了构建随机森林、分类特征以及随机森林图的详细解释。最后一部分,> swim.out,表示输出被写入到swim.out文件中。此文件可以在章节目录source_code/4中找到。程序的输出之前曾用于撰写游泳偏好问题的分析。

下棋示例

我们将再次使用第二章的示例,朴素贝叶斯,以及第三章,决策树,如下所示:

温度 阳光 是否玩耍
寒冷 强风 多云
温暖 强风 多云
温暖 无风 阳光
无风 阳光
微风 多云
温暖 微风 阳光
寒冷 微风 多云
寒冷 无风 阳光
强风 多云
温暖 无风 多云
温暖 强风 阳光 ?

然而,我们希望使用由四棵随机决策树组成的随机森林来进行分类结果的预测。

分析

我们提供了M=4个变量用于特征分类。因此,我们选择在节点中考虑的变量的最大数量为:

我们提供了以下特征:

[['Cold', 'Strong', 'Cloudy', 'No'], ['Warm', 'Strong', 'Cloudy', 'No'], ['Warm', 'None', 'Sunny',
'Yes'], ['Hot', 'None', 'Sunny', 'No'], ['Hot', 'Breeze', 'Cloudy', 'Yes'], ['Warm', 'Breeze',
'Sunny', 'Yes'], ['Cold', 'Breeze', 'Cloudy', 'No'], ['Cold', 'None', 'Sunny', 'Yes'], ['Hot', 'Strong', 'Cloudy', 'Yes'], ['Warm', 'None', 'Cloudy', 'Yes']]

在构建随机森林中的随机决策树时,我们将仅随机选择这些特征中的一部分,并对其进行替换。

随机森林构建

我们将构建一个随机森林,由四棵随机决策树组成。

随机决策树构建 0

我们提供了 10 个特征作为输入数据。在这些特征中,我们随机选择所有特征及其替换项来构建这棵随机决策树:

[['Warm', 'Strong', 'Cloudy', 'No'], ['Cold', 'Breeze', 'Cloudy', 'No'], ['Cold', 'None', 'Sunny', 'Yes'], ['Cold', 'Breeze', 'Cloudy', 'No'], ['Hot', 'Breeze', 'Cloudy', 'Yes'], ['Warm', 'Strong', 'Cloudy', 'No'], ['Hot', 'Breeze', 'Cloudy', 'Yes'], ['Hot', 'Breeze', 'Cloudy', 'Yes'], ['Cold', 'Breeze', 'Cloudy', 'No'], ['Warm', 'Breeze', 'Sunny', 'Yes']]

我们从根节点开始构建,以创建树的第一个节点。我们希望为[root]节点添加子节点。

我们有以下可用的变量:['Temperature', 'Wind', 'Sunshine']。由于这些变量的数量少于m=4的参数,因此我们考虑所有的变量。在这些变量中,信息增益最高的是Temperature变量。因此,我们将基于此变量进一步分支当前节点。同时,我们会将该变量从当前节点的子节点可用变量列表中移除。利用Temperature变量,我们将当前节点的数据划分如下:

  • Temperature=Cold的划分:[['Cold', 'Breeze', 'Cloudy', 'No'], ['Cold', 'None', 'Sunny', 'Yes'], ['Cold', 'Breeze', 'Cloudy', 'No'], ['Cold', 'Breeze', 'Cloudy', 'No']]

  • Temperature=Warm的划分:[['Warm', 'Strong', 'Cloudy', 'No'], ['Warm', 'Strong', 'Cloudy', 'No'], ['Warm', 'Breeze', 'Sunny', 'Yes']]

  • Temperature=Hot的划分:[['Hot', 'Breeze', 'Cloudy', 'Yes'], ['Hot', 'Breeze', 'Cloudy', 'Yes'], ['Hot', 'Breeze', 'Cloudy', 'Yes']]

现在,给定这些划分,让我们创建分支和子节点。

我们为[root]节点添加一个子节点[Temperature=Cold]。此分支对四个特征进行分类:[['Cold', 'Breeze', 'Cloudy', 'No']['Cold', 'None', 'Sunny', 'Yes']['Cold', 'Breeze', 'Cloudy', 'No']['Cold', 'Breeze', 'Cloudy', 'No']]

我们希望为[Temperature=Cold]节点添加子节点。

我们有以下可用变量:['Wind', 'Sunshine']。由于这些变量的数量少于m=3参数,我们考虑了这两个变量。信息增益最高的是Wind变量。因此,我们将在此变量上进一步分支。我们还会将该变量从当前节点子节点的可用变量列表中移除。使用Wind变量,我们将当前节点的数据划分如下:

  • Wind=None的划分为:[['Cold', 'None', 'Sunny', 'Yes']]

  • Wind=Breeze的划分为:[['Cold', 'Breeze', 'Cloudy', 'No'], ['Cold', 'Breeze', 'Cloudy', 'No'], ['Cold', 'Breeze', 'Cloudy', 'No']]

现在,根据这些划分,让我们创建分支和子节点。

我们在[Temperature=Cold]节点下添加了一个子节点[Wind=None]。这个分支对单一特征进行分类:[['Cold', 'None', 'Sunny', 'Yes']]

我们想要在[Wind=None]节点下添加子节点。

我们有以下变量:available['Sunshine']。由于这些变量的数量少于m=3参数,我们考虑了这两个变量。信息增益最高的是Sunshine变量。因此,我们将在此变量上进一步分支。我们还会将该变量从当前节点子节点的可用变量列表中移除。对于所选变量Sunshine,所有剩余特征的值相同:Sunny。因此,我们将分支终止,并添加叶子节点[Play=Yes]

我们在[Temperature=Cold]节点下添加了一个子节点[Wind=Breeze]。这个分支对三个特征进行分类:[['Cold', 'Breeze', 'Cloudy', 'No'], ['Cold', 'Breeze', 'Cloudy', 'No'], ['Cold', 'Breeze', 'Cloudy', 'No']]

我们想要在[Wind=Breeze]节点下添加子节点。

我们有以下可用变量:['Sunshine']。由于这些变量的数量少于m=3参数,我们考虑了这两个变量。信息增益最高的是Sunshine变量。因此,我们将在此变量上进一步分支。我们还会将该变量从当前节点子节点的可用变量列表中移除。对于所选变量Sunshine,所有剩余特征的值相同:Cloudy。因此,我们将分支终止,并添加叶子节点[Play=No]

现在,我们已经为[Temperature=Cold]节点添加了所有子节点。

我们在[root]节点下添加了一个子节点[Temperature=Warm]。这个分支对三个特征进行分类:[['Warm', 'Strong', 'Cloudy', 'No'], ['Warm', 'Strong', 'Cloudy', 'No'], ['Warm', 'Breeze', 'Sunny', 'Yes']]

我们想要在[Temperature=Warm]节点下添加子节点。

我们仍然剩下的可用变量是['Wind', 'Sunshine']。由于这些变量的数量少于m=3的参数,我们考虑这两个变量。信息增益最高的是Wind变量。因此,我们将在这个变量上进一步分支节点。我们还会从当前节点的可用变量列表中移除这个变量。使用Wind变量,我们将当前节点中的数据进行划分,每个数据分区将对应当前节点的一个新分支,[Temperature=Warm]。我们得到以下分区:

  • Wind=Breeze的分区为:[['Warm', 'Breeze', 'Sunny', 'Yes']]

  • Wind=Strong的分区为:[['Warm', 'Strong', 'Cloudy', 'No'], ['Warm', 'Strong', 'Cloudy', 'No']]

现在,根据这些分区,让我们形成分支和子节点。

我们在[Temperature=Warm]节点上添加一个子节点,[Wind=Breeze]。这个分支对一个特征进行分类:[['Warm', 'Breeze', 'Sunny', 'Yes']]

我们希望为[Wind=Breeze]节点添加子节点。

我们剩下的可用变量是['Sunshine']。由于这些变量的数量少于m=3的参数,我们考虑这两个变量。信息增益最高的是Sunshine变量。因此,我们将在这个变量上进一步分支节点。我们还会从当前节点的可用变量列表中移除这个变量。对于选择的变量Sunshine,所有剩余特征的值相同:Sunny。所以,我们以一个叶子节点结束这个分支,添加[Play=Yes]

我们在[Temperature=Warm]节点上添加一个子节点,[Wind=Strong]。这个分支对两个特征进行分类:[['Warm', 'Strong', 'Cloudy', 'No'],['Warm', 'Strong', 'Cloudy', 'No']]

我们希望为[Wind=Strong]节点添加子节点。

我们剩下的可用变量是['Sunshine']。由于这些变量的数量少于m=3的参数,我们考虑这两个变量。信息增益最高的是Sunshine变量。因此,我们将在这个变量上进一步分支节点。我们还会从当前节点的可用变量列表中移除这个变量。对于选择的变量Sunshine,所有剩余特征的值相同:Cloudy。所以,我们以一个叶子节点结束这个分支,添加[Play=No]

现在,我们已经为[Temperature=Warm]节点添加了所有子节点。

我们在[root]节点上添加一个子节点,[Temperature=Hot]。这个分支对三个特征进行分类:[['Hot', 'Breeze', 'Cloudy', 'Yes'], ['Hot', 'Breeze', 'Cloudy', 'Yes'],['Hot', 'Breeze', 'Cloudy', 'Yes']]

我们希望为[Temperature=Hot]节点添加子节点。

我们可以使用以下变量:['风', '阳光']。由于这些变量少于m=3参数,所以我们考虑这两个变量。信息增益最高的是变量。因此,我们将在该变量上进一步分支节点。我们还将从当前节点的可用变量列表中移除此变量。对于选择的变量,所有剩余的特征值相同:微风。因此,我们以一个叶子节点结束该分支,添加[Play=Yes]

现在,我们已经将所有子节点添加到[root]节点。

构建随机决策树 1、2、3

我们以类似的方式构建接下来的三棵树。需要注意的是,由于构建是随机的,执行另一个正确构建的读者可能会得到不同的结果。然而,如果在随机森林中有足够数量的随机决策树,那么分类结果应该在所有随机构建中非常相似。

完整的构建可以在source_code/4/chess.out文件中的程序输出中找到。

构建的随机森林

Tree 0:
Root
├── [Temperature=Cold]
│ ├── [Wind=None]
│ │ └── [Play=Yes]
│ └── [Wind=Breeze]
│   └── [Play=No]
├── [Temperature=Warm]
│ ├── [Wind=Breeze]
│ │ └── [Play=Yes]
│ └── [Wind=Strong]
│   └── [Play=No]
└── [Temperature=Hot]
  └── [Play=Yes]

Tree 1:
Root
├── [Wind=Breeze]
│ └── [Play=No]
├── [Wind=None]
│ ├── [Temperature=Cold]
│ │ └── [Play=Yes]
│ ├── [Temperature=Warm]
│ │ ├── [Sunshine=Sunny]
│ │ │ └── [Play=Yes]
│ │ └── [Sunshine=Cloudy]
│ │   └── [Play=Yes]
│ └── [Temperature=Hot]
│   └── [Play=No]
└── [Wind=Strong]
    ├── [Temperature=Cold]
    │ └── [Play=No]
    └── [Temperature=Warm]
        └── [Play=No]

Tree 2: 
    Root
    ├── [Wind=Strong]
    │ └── [Play=No]
    ├── [Wind=None]
    │ ├── [Temperature=Cold]
    │ │ └── [Play=Yes]
    │ └── [Temperature=Warm]
    │   └── [Play=Yes]
    └── [Wind=Breeze]
      ├── [Temperature=Hot]
      │ └── [Play=Yes]
      └── [Temperature=Warm]
        └── [Play=Yes]

Tree 3:
    Root
    ├── [Temperature=Cold]
    │ └── [Play=No]
    ├── [Temperature=Warm]
    │ ├──[Wind=Strong]
    │ │ └──[Play=No]
    │ ├── [Wind=None]
    │ │ └── [Play=Yes]
    │ └──[Wind=Breeze]
    │   └── [Play=Yes]
    └── [Temperature=Hot]
      ├── [Wind=Strong]
      │ └── [Play=Yes]
      └── [Wind=Breeze]
        └── [Play=Yes]
The total number of trees in the random forest=4.
The maximum number of the variables considered at the node is m=4.

分类

给定构建的随机森林,我们将特征['Warm', 'Strong', 'Sunny', '?']分类:

  • 树 0 投票给类别No

  • 树 1 投票给类别No

  • 树 2 投票给类别No

  • 树 3 投票给类别No

拥有最多投票数的类别是No。因此,构建的随机森林将特征['Warm', 'Strong', 'Sunny', '?']分类为No类别。

输入

为了执行前面的分析,我们使用了本章早些时候实现的程序。首先,我们将表格中的数据插入到以下 CSV 文件中:

# source_code/4/chess.csv Temperature,Wind,Sunshine,Play  
Cold,Strong,Cloudy,No  
Warm,Strong,Cloudy,No  
Warm,None,Sunny,Yes  
Hot,None,Sunny,No  
Hot,Breeze,Cloudy,Yes  
Warm,Breeze,Sunny,Yes  
Cold,Breeze,Cloudy,No  
Cold,None,Sunny,Yes  
Hot,Strong,Cloudy,Yes  
Warm,None,Cloudy,Yes  
Warm,Strong,Sunny,? 

输出

我们通过执行以下命令行来生成输出:

$ python random_forest.py chess.csv 4 2 > chess.out

这里的数字4表示我们希望构建四棵决策树,而2是程序的冗长程度,包含了树是如何构建的解释。最后一部分,> chess.out,意味着输出会写入到chess.out文件中。该文件可以在章节目录source_code/4中找到。我们不会将所有的输出都放在这里,因为它非常大且重复。相反,其中一些内容已经包含在前面的分析中,以及在随机森林的构建中。

去购物——通过随机性克服数据不一致性并测量置信度

我们从上一章的问题开始。我们有关于我们朋友简(Jane)购物偏好的以下数据:

温度 购物
温暖
温暖
温暖
?

在上一章中,决策树无法对特征 (Cold, None) 进行分类。因此,这次我们希望使用随机森林算法来判断,如果温度寒冷且没有降雨,简会不会去购物。

分析

为了使用随机森林算法进行分析,我们使用已实现的程序。

输入

我们将表格中的数据插入以下 CSV 文件:

# source_code/4/shopping.csv Temperature,Rain,Shopping  
Cold,None,Yes  
Warm,None,No  
Cold,Strong,Yes  
Cold,None,No  
Warm,Strong,No  
Warm,None,Yes 
Cold,None,? 

输出

我们希望使用比前面的示例和解释中更多的树来获得更精确的结果。我们希望构建一个包含 20 棵树的随机森林,并且输出的详细程度为低级别——级别 0。因此,我们在终端中执行以下操作:

$ python random_forest.py shopping.csv 20 0
***Classification***
Feature: ['Cold', 'None', '?'] 
Tree 0 votes for the class: Yes 
Tree 1 votes for the class: No 
Tree 2 votes for the class: No 
Tree 3 votes for the class: No 
Tree 4 votes for the class: No 
Tree 5 votes for the class: Yes 
Tree 6 votes for the class: Yes 
Tree 7 votes for the class: Yes 
Tree 8 votes for the class: No 
Tree 9 votes for the class: Yes 
Tree 10 votes for the class: Yes 
Tree 11 votes for the class: Yes 
Tree 12 votes for the class: Yes 
Tree 13 votes for the class: Yes 
Tree 14 votes for the class: Yes 
Tree 15 votes for the class: Yes 
Tree 16 votes for the class: Yes 
Tree 17 votes for the class: No 
Tree 18 votes for the class: No 
Tree 19 votes for the class: No
The class with the maximum number of votes is 'Yes'. Thus the constructed random forest classifies the feature ['Cold', 'None', '?'] into the class 'Yes'.

然而,我们应该注意到,只有 20 棵树中的 12 棵投了“是”的票。因此,虽然我们有了明确的答案,但它可能不那么确定,类似于我们使用普通决策树时得到的结果。然而,与决策树中的数据不一致导致无法得出答案不同的是,在这里我们得到了一个答案。

此外,通过衡量每个类别的投票权重,我们可以衡量答案正确的置信度。在此案例中,特征['Cold', 'None', '?']属于*Yes*类别,置信度为 12/20,或者 60%。为了更精确地确定分类的确定性,需要使用一个更大的随机决策树集成。

总结

在本章中,我们了解到随机森林是一组决策树,其中每棵树都是从初始数据中随机选择的样本构建的。这个过程叫做自助聚合。它的目的是减少随机森林在分类时的方差和偏差。在构建决策树的过程中,通过仅考虑每个分支的随机变量子集,进一步减少偏差。

我们还了解到,一旦构建了随机森林,随机森林的分类结果就是所有树中的多数投票。多数票的水平也决定了答案正确的置信度。

由于随机森林由决策树组成,因此它适用于所有决策树表现良好的问题。因为随机森林可以减少决策树分类器中的偏差和方差,所以它们的性能优于决策树算法。

在下一章中,我们将学习将数据聚类到相似簇中的技术。我们还将利用该技术将数据分类到已创建的簇中。

问题

问题 1:让我们以第二章《朴素贝叶斯》中的下棋例子为例,如何根据随机森林算法对 (Warm, Strong, Spring, ?) 数据样本进行分类?

温度 风力 季节 是否玩
寒冷 强风 冬季
温暖 强风 秋季
温暖 夏季
Hot None Spring No
Hot Breeze Autumn Yes
Warm Breeze Spring Yes
Cold Breeze Winter No
Cold None Spring Yes
Hot Strong Summer Yes
Warm None Autumn Yes
Warm Strong Spring ?

问题 2:只使用一棵树和一个随机森林是一个好主意吗?请说明你的理由。

问题 3:交叉验证能否改善随机森林分类的结果?请说明你的理由。

分析

问题 1:我们运行程序来构建随机森林并对特征(Warm, Strong, Spring)进行分类。

输入

source_code/4/chess_with_seasons.csv Temperature,Wind,Season,Play  
Cold,Strong,Winter,No  
Warm,Strong,Autumn,No  
Warm,None,Summer,Yes  
Hot,None,Spring,No  
Hot,Breeze,Autumn,Yes  
Warm,Breeze,Spring,Yes  
Cold,Breeze,Winter,No  
Cold,None,Spring,Yes  
Hot,Strong,Summer,Yes  
Warm,None,Autumn,Yes  
Warm,Strong,Spring,? 

输出

我们在随机森林中构建了四棵树,具体如下:

$ python random_forest.py chess_with_seasons.csv 4 2 > chess_with_seasons.out

整个构建过程和分析结果存储在source_code/4/chess_with_seasons.out文件中。由于涉及随机性,你的构建结果可能有所不同。从输出中,我们提取出随机森林图,该图由随机决策树组成,依据的是我们运行过程中生成的随机数。

再次执行前面的命令很可能会导致不同的输出和不同的随机森林图。然而,由于随机决策树的多样性及其投票权的结合,分类结果很可能相似。单个随机决策树的分类可能会受到显著的方差影响。然而,多数投票将所有树的分类结果结合起来,从而减少了方差。为了验证你的理解,你可以将你的分类结果与以下随机森林图中的分类进行比较。

随机森林图和分类:

我们来看看随机森林图的输出和特征的分类:

Tree 0:
    Root
    ├── [Wind=None]
    │ ├── [Temperature=Cold]
    │ │ └── [Play=Yes]
    │ └── [Temperature=Warm]
    │   ├── [Season=Autumn]
    │   │ └── [Play=Yes]
    │   └── [Season=Summer]
    │     └── [Play=Yes]
    └── [Wind=Strong]
      ├── [Temperature=Cold]
      │ └── [Play=No]
      └── [Temperature=Warm]
        └── [Play=No]
Tree 1:
    Root
    ├── [Season=Autumn]
    │ ├──[Wind=Strong]
    │ │ └──[Play=No]
    │ ├── [Wind=None]
    │ │ └── [Play=Yes]
    │ └──[Wind=Breeze]
    │   └── [Play=Yes]
    ├── [Season=Summer]
    │   └── [Play=Yes]
    ├── [Season=Winter]
    │   └── [Play=No]
    └── [Season=Spring]
      ├── [Temperature=Cold]
      │ └── [Play=Yes]
      └── [Temperature=Warm]
        └── [Play=Yes]

Tree 2:
    Root
    ├── [Season=Autumn]
    │ ├── [Temperature=Hot]
    │ │ └── [Play=Yes]
    │ └── [Temperature=Warm]
    │   └── [Play=No]
    ├── [Season=Spring]
    │ ├── [Temperature=Cold]
    │ │ └── [Play=Yes]
    │ └── [Temperature=Warm]
    │   └── [Play=Yes]
    ├── [Season=Winter]
    │ └── [Play=No]
    └── [Season=Summer]
      ├── [Temperature=Hot]
      │ └── [Play=Yes]
      └── [Temperature=Warm]
        └── [Play=Yes]
Tree 3:
    Root
    ├── [Season=Autumn]
    │ ├──[Wind=Breeze]
    │ │ └── [Play=Yes]
    │ ├── [Wind=None]
    │ │ └── [Play=Yes]
    │ └──[Wind=Strong]
    │   └── [Play=No]
    ├── [Season=Spring]
    │ ├── [Temperature=Cold]
    │ │ └── [Play=Yes]
    │ └── [Temperature=Warm]
    │   └── [Play=Yes]
    ├── [Season=Winter]
    │ └── [Play=No]
    └── [Season=Summer]
      └── [Play=Yes]
The total number of trees in the random forest=4.
The maximum number of the variables considered at the node is m=4.

Classication
Feature: ['Warm', 'Strong', 'Spring', '?']
Tree 0 votes for the class: No
Tree 1 votes for the class: Yes
Tree 2 votes for the class: Yes
Tree 3 votes for the class: Yes
The class with the maximum number of votes is 'Yes'. Thus the constructed random forest classifies the feature ['Warm', 'Strong', 'Spring', '?'] into the class 'Yes'.

问题 2:当我们在随机森林中构建一棵树时,我们只使用数据的随机子集,并进行有放回抽样。这样做是为了消除分类器对某些特征的偏向。然而,如果只使用一棵树,这棵树可能恰好包含有偏向的特征,并且可能缺少一些重要特征,无法提供准确的分类。因此,使用仅有一棵决策树的随机森林分类器可能会导致非常差的分类结果。因此,我们应该在随机森林中构建更多的决策树,以便从减少偏差和方差的分类中获益。

问题 3:在交叉验证过程中,我们将数据分为训练数据和测试数据。训练数据用于训练分类器,测试数据用于评估哪些参数或方法最适合改进分类。交叉验证的另一个优点是减少了偏差,因为我们只使用部分数据,从而减少了过拟合特定数据集的可能性。

然而,在决策森林中,我们以一种替代的方式解决交叉验证所解决的问题。每个随机决策树仅在数据的子集上构建——从而减少了过拟合的可能性。最终,分类是这些树的结果的组合。最终的最佳决策,并不是通过在测试数据集上调优参数来做出的,而是通过对所有树进行多数投票,从而减少偏差。

因此,决策森林算法的交叉验证并不会太有用,因为它已经内置于算法中了。

第五章:将数据聚类为 K 个簇

聚类是一种将数据划分为簇的技术,相同特征的项会被归为同一个簇。

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

  • 如何使用k-均值聚类算法,通过涉及家庭收入的例子来演示

  • 如何通过将特征与已知类别一起聚类,使用性别分类的例子来对特征进行分类

  • 如何在实现 k 均值聚类算法部分中使用 Python 实现k-均值聚类算法

  • 房屋拥有情况的例子,以及如何为你的分析选择合适的簇数

  • 如何通过使用聚类算法,利用房屋拥有情况的例子,合理地缩放给定的数值数据,以提高分类的准确性

  • 理解不同簇数如何改变簇与簇之间分界线的含义,通过文档聚类的例子来说明

家庭收入 – 将数据聚类为 k 个簇

例如,我们来看一下年收入分别为 40,000 美元、55,000 美元、70,000 美元、100,000 美元、115,000 美元、130,000 美元和 135,000 美元的家庭。然后,如果我们将这些家庭根据收入作为相似度标准分为两个簇,第一个簇会包括年收入为 40k、55k 和 70k 的家庭,而第二个簇会包括年收入为 100k、115k、130k 和 135k 的家庭。

这是因为 40k 和 135k 相距最远,所以我们希望分成两个簇,它们必须分别属于不同的簇。55k 比 135k 更接近 40k,因此 40k 和 55k 会在同一个簇。类似地,130k 和 135k 会在同一个簇。70k 比 130k 和 135k 更接近 40k 和 55k,因此 70k 应该和 40k、55k 在同一个簇。115k 比 40k、55k 和 70k 所在的第一个簇更接近 130k 和 135k,因此它会在第二个簇。最后,100k 更接近包含 115k、130k 和 135k 的第二个簇,因此它会被归类到那里。因此,第一个簇将包括年收入为 40k、55k 和 70k 的家庭。第二个簇将包括年收入为 100k、115k、130k 和 135k 的家庭。

聚类具有相似属性的组的特征,并将簇分配给某个特征是一种分类形式。数据科学家需要解释聚类结果以及它所引发的分类。在这里,包含年收入为 40k、55k 和 70k 家庭的簇代表了低收入家庭类。第二个簇,包括年收入为 100k、115k、130k 和 135k 的家庭,代表了高收入家庭类。

我们根据直觉和常识,非正式地将家庭收入划分为两个簇。也有一些聚类算法根据精确的规则对数据进行聚类。这些算法包括模糊 C 均值聚类算法、层次聚类算法、高斯(EM)聚类算法、质量阈值聚类算法和k均值聚类算法,本章将重点介绍后者。

K 均值聚类算法

k均值聚类算法将给定的点分类到k个组中,使得同一组内的成员之间的距离最小。

k均值聚类算法确定初始的k质心(每个簇的中心点)——每个簇一个质心。然后,每个特征会被分类到与该特征距离最近的簇中。分类所有特征之后,我们就形成了初始的k个簇。

对于每个簇,我们重新计算质心,作为该簇内所有点的平均值。在移动了质心之后,我们再次重新计算类别。类别中的特征可能会发生变化。在这种情况下,我们需要重新计算质心。如果质心不再移动,则k均值聚类算法终止。

选择初始的 k 个质心

我们可以将初始的k个质心选择为数据中任意的k个特征。但是,理想情况下,我们希望从一开始就选择属于不同簇的点。因此,我们可能希望通过某种方式最大化它们之间的相互距离。简化过程后,我们可以将第一个质心选为数据中任意一个点。第二个质心可以选为距离第一个质心最远的点。第三个质心可以选为同时距离第一个和第二个质心最远的点,依此类推。

计算给定簇的质心

一个簇的质心只是该簇内所有点的平均值。如果一个簇包含一维点,其坐标为x[1]、x[2]、…、x[n],那么该簇的质心为。如果一个簇包含二维点,其坐标为(x[1],y[1])、(x[2],y[2])、…、(x[n],y[n]),那么该簇的质心在x坐标的值为(1/n)(x[1]+x[2]+...+x[n]),在y*坐标的值为

这个计算方法可以轻松扩展到更高的维度。如果在x坐标的高维特征值为x[1]、x[2]、…、x[n],那么该簇的质心在x坐标的值为

在家庭收入的例子中使用 k 均值聚类算法

我们将应用k聚类算法来分析家庭收入的例子。一开始,我们有收入分别为$40,000、$55,000、$70,000、$100,000、$115,000、$130,000 和$135,000 的家庭。

第一个被选择的质心可以是任何特征,例如,$70,000. 第二个质心应该是与第一个最远的特征,即 135 k,因为 135 k 减去 70 k 是 65 k,这是任何其他特征与 70 k 之间的最大差异。因此,70 k 是第一个簇的质心,而 135 k 是第二个簇的质心。

现在,通过差异,40 k、55 k、70 k 和 100 k 更接近 70 k 而不是 135 k,因此它们将位于第一个簇中,而 115 k、130 k 和 135 k 更接近 135 k 而不是 70 k,因此它们将位于第二个簇中。

在根据初始质心对特征进行分类之后,我们重新计算质心。第一个簇的质心如下:

第二个簇的质心如下:

使用新的质心,我们重新分类特征如下:

  • 包含质心 66.25 k 的第一个簇将包含特征 40 k、55 k 和 70 k

  • 包含质心 126.66 k 的第二个簇将包含特征 100 k、115 k、130 k 和 135 k

我们注意到,100 k 特征从第一个簇移动到第二个簇,因为它现在更接近第二个簇的质心(距离 |100 k-126.66 k|=26.66 k),而不是第一个簇的质心(距离 |100 k-66.25 k|=33.75 k)。由于簇中的特征已经改变,我们必须重新计算质心。

第一个簇的质心如下:

第二个簇的质心如下:

使用这些质心,我们将特征重新分类到簇中。第一个质心,55 k,将包含特征 40 k、55 k 和 70 k。第二个质心,120 k,将包含特征 100 k、115 k、130 k 和 135 k。因此,在更新质心时,簇没有改变。因此,它们的质心将保持不变。

因此,算法以两个簇终止:第一个簇包含特征 40 k、55 k 和 70 k,第二个簇包含特征 100 k、115 k、130 k 和 135 k。

性别分类 – 聚类分类

下面的数据来自性别分类示例,问题 6,第二章,朴素贝叶斯

身高(厘米) 体重(公斤) 头发长度 性别
180 75
174 71
184 83
168 63
178 70
170 59
164 53
155 46
162 52
166 55
172 60 ?

为了简化问题,我们将移除名为 Hair length 的列。我们还将移除名为 Gender 的列,因为我们希望根据身高和体重对表中的人进行聚类。我们希望通过聚类判断表中第十一位人的性别更可能是男性还是女性:

Height in cm Weight in kg
180 75
174 71
184 83
168 63
178 70
170 59
164 53
155 46
162 52
166 55
172 60

分析

我们可以对初始数据进行缩放,但为了简化问题,我们将在算法中使用未经缩放的数据。我们将把已有数据聚类为两个簇,因为性别只有两种可能——男性或女性。然后,我们将根据聚类结果来分类一个身高为 172 厘米、体重为 60 公斤的人,如果且仅当该聚类中男性更多时,才将其归类为男性。聚类算法是一种非常高效的技术。因此,以这种方式进行分类非常快速,尤其是在需要分类的特征非常多的情况下。

所以,让我们对已有数据应用 k-均值聚类算法。首先,选择初始质心。例如,将第一个质心设为一个身高 180 厘米、体重 75 公斤的人,用向量表示为 (180,75)。与 (180,75) 最远的点是 (155,46)。所以它将作为第二个质心。

通过计算欧几里得距离,离第一个质心 (180,75) 最近的点是 (180,75), (174,71), (184,83), (168,63), (178,70), (170,59)(172,60)。所以这些点将属于第一个簇。离第二个质心 (155,46) 最近的点是 (155,46), (164,53), (162,52)(166,55)。所以这些点将属于第二个簇。涉及这两个聚类的当前情况如下面的图示所示:

图 5.1:根据身高和体重对人的聚类

让我们重新计算聚类的质心。具有特征 (180,75), (174,71), (184,83), (168,63), (178,70), (170,59)(172,60) 的蓝色聚类将有质心 ((180+174+184+168+178+170+172)/7,(75+71+83+63+70+59+60)/7)~(175.14,68.71)

红色聚类包含特征 (155,46), (164,53), (162,52)(166,55),其质心为 ((155+164+162+166)/4,(46+53+52+55)/4)=(161.75, 51.5)

使用新的质心重新分类点时,点的类别没有变化。蓝色聚类将包含点 (180,75), (174,71), (184,83), (168,63), (178,70), (170,59)(172,60)。红色聚类将包含点 (155,46), (164,53), (162,52)(166,55)。因此,聚类算法在以下图示中结束,得到了聚类:

图 5.2:根据身高和体重对人的聚类

现在我们希望将实例 (172,60) 分类为男性还是女性。实例 (172,60) 位于蓝色簇中,因此它与蓝色簇中的特征相似。剩余的特征是在蓝色簇中更可能是男性还是女性呢?六个特征中有五个是男性,只有一个是女性。由于蓝色簇中的大多数特征是男性,而且 (172,60) 也位于蓝色簇中,因此我们将身高 172 cm、体重 60 kg 的人分类为男性。

k-means 聚类算法的实现

我们现在将实现 k-means 聚类算法。它以 CSV 文件作为输入,每一行代表一个数据项。每个数据项被转换为一个点。算法将这些点分为指定数量的簇。最终,聚类结果将在图表上可视化,使用 matplotlib 库:

# source_code/5/k-means_clustering.py
import math
import imp
import sys
import matplotlib.pyplot as plt
import matplotlib
import sys
sys.path.append('../common')
import common # noqa
matplotlib.style.use('ggplot')

# Returns k initial centroids for the given points.
def choose_init_centroids(points, k):
    centroids = []
    centroids.append(points[0])
    while len(centroids) < k:
        # Find the centroid that with the greatest possible distance
        # to the closest already chosen centroid.
        candidate = points[0]
        candidate_dist = min_dist(points[0], centroids)
        for point in points:
            dist = min_dist(point, centroids)
            if dist > candidate_dist:
                candidate = point
                candidate_dist = dist
        centroids.append(candidate)
    return centroids

# Returns the distance of a point from the closest point in points.
def min_dist(point, points):
    min_dist = euclidean_dist(point, points[0])
    for point2 in points:
        dist = euclidean_dist(point, point2)
        if dist < min_dist:
            min_dist = dist
    return min_dist

# Returns an Euclidean distance of two 2-dimensional points.
def euclidean_dist((x1, y1), (x2, y2)):
    return math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2))

# PointGroup is a tuple that contains in the first coordinate a 2d point
# and in the second coordinate a group which a point is classified to.
def choose_centroids(point_groups, k):
    centroid_xs = [0] * k
    centroid_ys = [0] * k
    group_counts = [0] * k
    for ((x, y), group) in point_groups:
        centroid_xs[group] += x
        centroid_ys[group] += y
        group_counts[group] += 1
    centroids = []
    for group in range(0, k):
        centroids.append((
            float(centroid_xs[group]) / group_counts[group],
            float(centroid_ys[group]) / group_counts[group]))
    return centroids

# Returns the number of the centroid which is closest to the point.
# This number of the centroid is the number of the group where
# the point belongs to.
def closest_group(point, centroids):
    selected_group = 0
    selected_dist = euclidean_dist(point, centroids[0])
    for i in range(1, len(centroids)):
        dist = euclidean_dist(point, centroids[i])
        if dist < selected_dist:
            selected_group = i
            selected_dist = dist
    return selected_group

# Reassigns the groups to the points according to which centroid
# a point is closest to.
def assign_groups(point_groups, centroids):
    new_point_groups = []
    for (point, group) in point_groups:
        new_point_groups.append(
            (point, closest_group(point, centroids)))
    return new_point_groups

# Returns a list of pointgroups given a list of points.
def points_to_point_groups(points):
    point_groups = []
    for point in points:
        point_groups.append((point, 0))
    return point_groups

# Clusters points into the k groups adding every stage
# of the algorithm to the history which is returned.
def cluster_with_history(points, k):
    history = []
    centroids = choose_init_centroids(points, k)
    point_groups = points_to_point_groups(points)
    while True:
        point_groups = assign_groups(point_groups, centroids)
        history.append((point_groups, centroids))
        new_centroids = choose_centroids(point_groups, k)
        done = True
        for i in range(0, len(centroids)):
            if centroids[i] != new_centroids[i]:
                done = False
                break
        if done:
            return history
        centroids = new_centroids

# Program start
csv_file = sys.argv[1]
k = int(sys.argv[2])
everything = False
# The third argument sys.argv[3] represents the number of the step of the
# algorithm starting from 0 to be shown or "last" for displaying the last
# step and the number of the steps.
if sys.argv[3] == "last":
    everything = True
else:
    step = int(sys.argv[3])

data = common.csv_file_to_list(csv_file)
points = data_to_points(data)  # Represent every data item by a point.
history = cluster_with_history(points, k)
if everything:
    print "The total number of steps:", len(history)
    print "The history of the algorithm:"
    (point_groups, centroids) = history[len(history) - 1]
    # Print all the history.
    print_cluster_history(history)
    # But display the situation graphically at the last step only.
    draw(point_groups, centroids)
else:
    (point_groups, centroids) = history[step]
    print "Data for the step number", step, ":"
    print point_groups, centroids
    draw(point_groups, centroids)

性别分类输入数据

我们将性别分类示例中的数据保存到 CSV 文件中,如下所示:

# source_code/5/persons_by_height_and_weight.csv 180,75
174,71
184,83
168,63
178,70
170,59
164,53
155,46
162,52
166,55
172,60

性别分类数据的程序输出

我们运行程序,实施 k-means 聚类算法在性别分类示例的数据上。数值参数 2 表示我们希望将数据聚类成两个簇,如下方代码块所示:

$ python k-means_clustering.py persons_by_height_weight.csv 2 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((180.0, 75.0), 0), ((174.0, 71.0), 0), ((184.0, 83.0), 0), ((168.0, 63.0), 0), ((178.0, 70.0), 0), ((170.0, 59.0), 0), ((164.0, 53.0), 1), ((155.0, 46.0), 1), ((162.0, 52.0), 1), ((166.0, 55.0), 1), ((172.0, 60.0), 0)]
centroids = [(180.0, 75.0), (155.0, 46.0)]
Step number 1: point_groups = [((180.0, 75.0), 0), ((174.0, 71.0), 0), ((184.0, 83.0), 0), ((168.0, 63.0), 0), ((178.0, 70.0), 0), ((170.0, 59.0), 0), ((164.0, 53.0), 1), ((155.0, 46.0), 1), ((162.0, 52.0), 1), ((166.0, 55.0), 1), ((172.0, 60.0), 0)]
centroids = [(175.14285714285714, 68.71428571428571), (161.75, 51.5)]

程序还输出了一个图表,见于图 5.2last 参数表示我们希望程序进行聚类直到最后一步。如果我们只想显示第一步(步骤 0),可以将 last 改为 0,如下面的代码所示:

$ python k-means_clustering.py persons_by_height_weight.csv 2 0

在程序执行后,我们将得到一个关于聚类及其质心的图像,如图 5.1所示。

房屋所有权 – 选择聚类数量

让我们以第一章中的房屋所有权示例为例:

年龄 年收入(美元) 房屋所有权状态
23 50,000 非拥有者
37 34,000 非拥有者
48 40,000 拥有者
52 30,000 非拥有者
28 95,000 拥有者
25 78,000 非拥有者
35 13,0000 拥有者
32 10,5000 拥有者
20 10,0000 非拥有者
40 60,000 拥有者
50 80,000 Peter

我们希望通过聚类预测 Peter 是否是房屋拥有者。

分析

就像第一章一样,我们需要对数据进行缩放,因为 收入 轴的数值明显大于 年龄 轴,从而减少了 年龄 轴的影响,而实际上在这种问题中,年龄 具有很好的预测能力。因为预期年长的人有更多的时间定居、存钱并购买房产,而相较于年轻人,年长的人更可能是房屋拥有者。

我们应用了第一章中的相同重缩放方法,基于 K 最近邻的分类,并得到了以下表格:

年龄 缩放后的年龄 年收入(美元) 缩放后的年收入 房屋所有权状态
23 0.09375 50000 0.2 非房主
37 0.53125 34000 0.04 非房主
48 0.875 40000 0.1 房主
52 1 30000 0 非房主
28 0.25 95000 0.65 房主
25 0.15625 78000 0.48 非房主
35 0.46875 130000 1 房主
32 0.375 105000 0.75 房主
20 0 100000 0.7 非房主
40 0.625 60000 0.3 房主
50 0.9375 80000 0.5 ?

根据该表,我们生成算法的输入文件并执行它,将特征聚类为两个簇。

输入

# source_code/5/house_ownership2.csv 0.09375,0.2
0.53125,0.04
0.875,0.1
1,0
0.25,0.65
0.15625,0.48
0.46875,1
0.375,0.75
0,0.7
0.625,0.3
0.9375,0.5

两个簇的输出

$ python k-means_clustering.py house_ownership2.csv 2 last The total number of steps: 3
The history of the algorithm:
Step number 0: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 0), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 0), ((0.15625, 0.48), 0), ((0.46875, 1.0), 0), ((0.375, 0.75), 0), ((0.0, 0.7), 0), ((0.625, 0.3), 1), ((0.9375, 0.5), 1)]
centroids = [(0.09375, 0.2), (1.0, 0.0)]
Step number 1: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 1), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 0), ((0.15625, 0.48), 0), ((0.46875, 1.0), 0), ((0.375, 0.75), 0), ((0.0, 0.7), 0), ((0.625, 0.3), 1), ((0.9375, 0.5), 1)]
centroids = [(0.26785714285714285, 0.5457142857142857), (0.859375, 0.225)]
Step number 2: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 1), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 0), ((0.15625, 0.48), 0), ((0.46875, 1.0), 0), ((0.375, 0.75), 0), ((0.0, 0.7), 0), ((0.625, 0.3), 1), ((0.9375, 0.5), 1)]
centroids = [(0.22395833333333334, 0.63), (0.79375, 0.188)]

蓝色簇包含已缩放的特征–(0.09375,0.2)(0.25,0.65)(0.15625,0.48)(0.46875,1)(0.375,0.75),和未缩放的特征–(23,50000)(28,95000)(25,78000)(35,130000)(32,105000),和(20,100000)。红色簇包含已缩放的特征–(0.53125,0.04)(0.875,0.1)(1,0)(0.625,0.3),和(0.9375,0.5),以及未缩放的特征–(37,34000)(48,40000)(52,30000)(40,60000),和(50,80000)

所以,彼得属于红色簇。那么,在不计彼得的情况下,红色簇中房主的比例是多少?红色簇中有 2/4,或者说 1/2 的人是房主。因此,彼得所在的红色簇似乎在判断彼得是否为房主上没有太高的预测能力。我们可以尝试将数据聚类成更多簇,希望能获得一个更纯粹的簇,这样对预测彼得是否为房主会更可靠。因此,让我们尝试将数据聚类成三个簇。

三个簇的输出

$ python k-means_clustering.py house_ownership2.csv 3 last The total number of steps: 3
The history of the algorithm:
Step number 0: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 0), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 2), ((0.15625, 0.48), 0), ((0.46875, 1.0), 2), ((0.375, 0.75), 2), ((0.0, 0.7), 0), ((0.625, 0.3), 1), ((0.9375, 0.5), 1)]
centroids = [(0.09375, 0.2), (1.0, 0.0), (0.46875, 1.0)]
Step number 1: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 1), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 2), ((0.15625, 0.48), 0), ((0.46875, 1.0), 2), ((0.375, 0.75), 2), ((0.0, 0.7), 2), ((0.625, 0.3), 1), ((0.9375, 0.5), 1)]
centroids = [(0.1953125, 0.355), (0.859375, 0.225), (0.3645833333333333, 0.7999999999999999)]
Step number 2: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 1), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 2), ((0.15625, 0.48), 0), ((0.46875, 1.0), 2), ((0.375, 0.75), 2), ((0.0, 0.7), 2), ((0.625, 0.3), 1), ((0.9375, 0.5), 1)]
centroids = [(0.125, 0.33999999999999997), (0.79375, 0.188), (0.2734375, 0.7749999999999999)]

红色簇保持不变。所以我们将数据聚类成四个簇。

四个簇的输出

$ python k-means_clustering.py house_ownership2.csv 4 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 0), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 3), ((0.15625, 0.48), 3), ((0.46875, 1.0), 2), ((0.375, 0.75), 2), ((0.0, 0.7), 3), ((0.625, 0.3), 1), ((0.9375, 0.5), 1)]
centroids = [(0.09375, 0.2), (1.0, 0.0), (0.46875, 1.0), (0.0, 0.7)]
Step number 1: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 0), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 3), ((0.15625, 0.48), 3), ((0.46875, 1.0), 2), ((0.375, 0.75), 2), ((0.0, 0.7), 3), ((0.625, 0.3), 1), ((0.9375, 0.5), 1)]
centroids = [(0.3125, 0.12000000000000001), (0.859375, 0.225), (0.421875, 0.875), (0.13541666666666666, 0.61)]

现在,彼得所在的红色簇发生了变化。那么,红色簇中房主的比例是多少呢?如果不计算彼得,红色簇中有 2/3 的人是房主。当我们聚成两个簇或三个簇时,这个比例只有 1/2,这并没有告诉我们彼得是否是房主。现在,红色簇中房主的比例多数(不算彼得),所以我们更相信彼得也是房主。然而,2/3 仍然是一个相对较低的置信度,无法将彼得确定为房主。让我们将数据聚类成五个簇,看看会发生什么。

五个簇的输出

$ python k-means_clustering.py house_ownership2.csv 5 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 0), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 3), ((0.15625, 0.48), 3), ((0.46875, 1.0), 2), ((0.375, 0.75), 2), ((0.0, 0.7), 3), ((0.625, 0.3), 4), ((0.9375, 0.5), 4)]
centroids = [(0.09375, 0.2), (1.0, 0.0), (0.46875, 1.0), (0.0, 0.7), (0.9375, 0.5)]
Step number 1: point_groups = [((0.09375, 0.2), 0), ((0.53125, 0.04), 0), ((0.875, 0.1), 1), ((1.0, 0.0), 1), ((0.25, 0.65), 3), ((0.15625, 0.48), 3), ((0.46875, 1.0), 2), ((0.375, 0.75), 2), ((0.0, 0.7), 3), ((0.625, 0.3), 4), ((0.9375, 0.5), 4)]
centroids = [(0.3125, 0.12000000000000001), (0.9375, 0.05), (0.421875, 0.875), (0.13541666666666666, 0.61), (0.78125, 0.4)]

现在,红色聚类只包含彼得和一名非所有者。这种聚类表明,彼得更可能是一个非所有者。然而,根据之前的聚类,彼得更可能是房主。因此,是否拥有房产的问题可能并不那么清晰。收集更多数据将有助于改善我们的分析,并应在做出最终分类之前进行。

通过我们的分析,我们注意到,聚类的不同数量可能会导致分类结果不同,因为单个聚类中的成员性质可能会变化。收集更多数据后,我们应进行交叉验证,以确定能够以最高准确率对数据进行分类的聚类数量。

文档聚类——理解语义上下文中的 k 个聚类数量

我们提供了关于来自古腾堡计划的 17 本书中 moneygod(s) 词汇频率的信息如下:

书籍编号 书名 货币占比 神明占比
1 吠檀多经注释拉马努贾翻译,乔治·希博特译 0 0.07
2 克里希纳·德瓦伊帕亚纳·维亚萨史诗 ——阿底帕尔瓦卷,由吉萨里·莫汉·甘古利翻译 0 0.17
3 克里希纳·德瓦伊帕亚纳·维亚萨史诗 第二部分,由克里希纳·德瓦伊帕亚纳·维亚萨编著 0.01 0.10
4 克里希纳·德瓦伊帕亚纳·维亚萨史诗 第 3 卷 第一部分,由克里希纳·德瓦伊帕亚纳·维亚萨编著 0 0.32
5 克里希纳·德瓦伊帕亚纳·维亚萨史诗 第 4 卷,由吉萨里·莫汉·甘古利翻译 0 0.06
6 克里希纳·德瓦伊帕亚纳·维亚萨史诗 第 3 卷 第二部分,由吉萨里·莫汉·甘古利翻译 0 0.27
7 吠檀多经注释 由桑卡拉查里亚编著 0 0.06
8 钦定版圣经 0.02 0.59
9 失乐园,由约翰·弥尔顿编著 0.02 0.45
10 基督的模仿,由托马斯·阿·肯皮斯编著 0.01 0.69
11 可兰经,由罗德威尔翻译 0.01 1.72
12 汤姆·索亚历险记 完整版,马克·吐温(塞缪尔·克莱门斯)编著 0.05 0.01
13 哈克贝里·费恩历险记 完整版,由马克·吐温(塞缪尔·克莱门斯)编著 0.08 0
14 远大前程,由查尔斯·狄更斯编著 0.04 0.01
15 道林·格雷的画像,由奥斯卡·王尔德编著 0.03 0.03
16 福尔摩斯的冒险,由阿瑟·柯南·道尔编著 0.04 0.03
17 变形记,由弗朗茨·卡夫卡,戴维·威利翻译 0.06 0.03

我们希望根据所选的单词频率,将该数据集按照其语义上下文聚类成不同的组。

分析

首先,我们将进行重缩放,因为单词 money 的最高频率为 0.08%,而单词 “god(s)” 的最高频率为 1.72%。因此,我们将 money 的频率除以 0.08,god(s) 的频率除以 1.72,计算结果如下:

书籍编号 货币重缩放 神明重缩放
1 0 0.0406976744
2 0 0.0988372093
3 0.125 0.0581395349
4 0 0.1860465116
5 0 0.0348837209
6 0 0.1569767442
7 0 0.0348837209
8 0.25 0.3430232558
9 0.25 0.261627907
10 0.125 0.4011627907
11 0.125 1
12 0.625 0.0058139535
13 1 0
14 0.5 0.0058139535
15 0.375 0.0174418605
16 0.5 0.0174418605
17 0.75 0.0174418605

现在我们已经对数据进行了重缩放,接下来我们通过尝试将数据划分为不同数量的聚类,应用 k-均值聚类算法。

输入

source_code/5/document_clustering/word_frequencies_money_god_scaled.csv 0,0.0406976744
0,0.0988372093
0.125,0.0581395349
0,0.1860465116
0,0.0348837209
0,0.1569767442
0,0.0348837209
0.25,0.3430232558
0.25,0.261627907
0.125,0.4011627907
0.125,1
0.625,0.0058139535
1,0
0.5,0.0058139535
0.375,0.0174418605
0.5,0.0174418605
0.75,0.0174418605

两个聚类的输出

$ python k-means_clustering.py document_clustering/word_frequencies_money_god_scaled.csv 2 last The total number of steps: 3
The history of the algorithm:
Step number 0: point_groups = [((0.0, 0.0406976744), 0), ((0.0, 0.0988372093), 0), ((0.125, 0.0581395349), 0), ((0.0, 0.1860465116), 0), ((0.0, 0.0348837209), 0), ((0.0, 0.1569767442), 0), ((0.0, 0.0348837209), 0), ((0.25, 0.3430232558), 0), ((0.25, 0.261627907), 0), ((0.125, 0.4011627907), 0), ((0.125, 1.0), 0), ((0.625, 0.0058139535), 1), ((1.0, 0.0), 1), ((0.5, 0.0058139535), 1), ((0.375, 0.0174418605), 0), ((0.5, 0.0174418605), 1), ((0.75, 0.0174418605), 1)]
centroids = [(0.0, 0.0406976744), (1.0, 0.0)]
Step number 1: point_groups = [((0.0, 0.0406976744), 0), ((0.0, 0.0988372093), 0), ((0.125, 0.0581395349), 0), ((0.0, 0.1860465116), 0), ((0.0, 0.0348837209), 0), ((0.0, 0.1569767442), 0), ((0.0, 0.0348837209), 0), ((0.25, 0.3430232558), 0), ((0.25, 0.261627907), 0), ((0.125, 0.4011627907), 0), ((0.125, 1.0), 0), ((0.625, 0.0058139535), 1), ((1.0, 0.0), 1), ((0.5, 0.0058139535), 1), ((0.375, 0.0174418605), 1), ((0.5, 0.0174418605), 1), ((0.75, 0.0174418605), 1)]
centroids = [(0.10416666666666667, 0.21947674418333332), (0.675, 0.0093023256)]
Step number 2: point_groups = [((0.0, 0.0406976744), 0), ((0.0, 0.0988372093), 0), ((0.125, 0.0581395349), 0), ((0.0, 0.1860465116), 0), ((0.0, 0.0348837209), 0), ((0.0, 0.1569767442), 0), ((0.0, 0.0348837209), 0), ((0.25, 0.3430232558), 0), ((0.25, 0.261627907), 0), ((0.125, 0.4011627907), 0), ((0.125, 1.0), 0), ((0.625, 0.0058139535), 1), ((1.0, 0.0), 1), ((0.5, 0.0058139535), 1), ((0.375, 0.0174418605), 1), ((0.5, 0.0174418605), 1), ((0.75, 0.0174418605), 1)]
centroids = [(0.07954545454545454, 0.2378435517909091), (0.625, 0.01065891475)]

我们可以观察到,将数据聚类成两个类别时,书籍被划分为宗教书籍(蓝色聚类)和非宗教书籍(红色聚类)。现在我们尝试将书籍分成三个聚类,看看算法是如何划分数据的。

三个聚类的输出

$ python k-means_clustering.py document_clustering/word_frequencies_money_god_scaled.csv 3 last The total number of steps: 3
The history of the algorithm:
Step number 0: point_groups = [((0.0, 0.0406976744), 0), ((0.0, 0.0988372093), 0), ((0.125, 0.0581395349), 0), ((0.0, 0.1860465116), 0), ((0.0, 0.0348837209), 0), ((0.0, 0.1569767442), 0), ((0.0, 0.0348837209), 0), ((0.25, 0.3430232558), 0), ((0.25, 0.261627907), 0), ((0.125, 0.4011627907), 0), ((0.125, 1.0), 2), ((0.625, 0.0058139535), 1), ((1.0, 0.0), 1), ((0.5, 0.0058139535), 1), ((0.375, 0.0174418605), 0), ((0.5, 0.0174418605), 1), ((0.75, 0.0174418605), 1)]
centroids = [(0.0, 0.0406976744), (1.0, 0.0), (0.125, 1.0)]
Step number 1: point_groups = [((0.0, 0.0406976744), 0), ((0.0, 0.0988372093), 0), ((0.125, 0.0581395349), 0), ((0.0, 0.1860465116), 0), ((0.0, 0.0348837209), 0), ((0.0, 0.1569767442), 0), ((0.0, 0.0348837209), 0), ((0.25, 0.3430232558), 0), ((0.25, 0.261627907), 0), ((0.125, 0.4011627907), 0), ((0.125, 1.0), 2), ((0.625, 0.0058139535), 1), ((1.0, 0.0), 1), ((0.5, 0.0058139535), 1), ((0.375, 0.0174418605), 1), ((0.5, 0.0174418605), 1), ((0.75, 0.0174418605), 1)]
centroids = [(0.10227272727272728, 0.14852008456363636), (0.675, 0.0093023256), (0.125, 1.0)]
Step number 2: point_groups = [((0.0, 0.0406976744), 0), ((0.0, 0.0988372093), 0), ((0.125, 0.0581395349), 0), ((0.0, 0.1860465116), 0), ((0.0, 0.0348837209), 0), ((0.0, 0.1569767442), 0), ((0.0, 0.0348837209), 0), ((0.25, 0.3430232558), 0), ((0.25, 0.261627907), 0), ((0.125, 0.4011627907), 0), ((0.125, 1.0), 2), ((0.625, 0.0058139535), 1), ((1.0, 0.0), 1), ((0.5, 0.0058139535), 1), ((0.375, 0.0174418605), 1), ((0.5, 0.0174418605), 1), ((0.75, 0.0174418605), 1)]
centroids = [(0.075, 0.16162790697), (0.625, 0.01065891475), (0.125, 1.0)]

这一次,算法将《可兰经》从宗教书籍中分离出来,单独放入一个绿色聚类。这是因为事实上,god(上帝)一词是《可兰经》中第五大高频词。这里的聚类恰好是根据书籍的写作风格将其划分的。四个聚类的结果将一本高频出现 money(金钱)一词的书从红色的非宗教书籍聚类中分离出来,单独成类。现在我们来看一下五个聚类的情况。

五个聚类的输出

$ python k-means_clustering.py word_frequencies_money_god_scaled.csv 5 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((0.0, 0.0406976744), 0), ((0.0, 0.0988372093), 0), ((0.125, 0.0581395349), 0), ((0.0, 0.1860465116), 0), ((0.0, 0.0348837209), 0), ((0.0, 0.1569767442), 0), ((0.0, 0.0348837209), 0), ((0.25, 0.3430232558), 4), ((0.25, 0.261627907), 4), ((0.125, 0.4011627907), 4), ((0.125, 1.0), 2), ((0.625, 0.0058139535), 3), ((1.0, 0.0), 1), ((0.5, 0.0058139535), 3), ((0.375, 0.0174418605), 3), ((0.5, 0.0174418605), 3), ((0.75, 0.0174418605), 3)]
centroids = [(0.0, 0.0406976744), (1.0, 0.0), (0.125, 1.0), (0.5, 0.0174418605), (0.25, 0.3430232558)]
Step number 1: point_groups = [((0.0, 0.0406976744), 0), ((0.0, 0.0988372093), 0), ((0.125, 0.0581395349), 0), ((0.0, 0.1860465116), 0), ((0.0, 0.0348837209), 0), ((0.0, 0.1569767442), 0), ((0.0, 0.0348837209), 0), ((0.25, 0.3430232558), 4), ((0.25, 0.261627907), 4), ((0.125, 0.4011627907), 4), ((0.125, 1.0), 2), ((0.625, 0.0058139535), 3), ((1.0, 0.0), 1), ((0.5, 0.0058139535), 3), ((0.375, 0.0174418605), 3), ((0.5, 0.0174418605), 3), ((0.75, 0.0174418605), 3)]
centroids = [(0.017857142857142856, 0.08720930231428571), (1.0, 0.0), (0.125, 1.0), (0.55, 0.0127906977), (0.20833333333333334, 0.3352713178333333)]

这种聚类进一步将蓝色宗教书籍聚类划分为印度教书籍的蓝色聚类和基督教书籍的灰色聚类。

我们可以通过这种方式使用聚类将具有相似属性的项分组,然后基于给定示例快速找到相似的项。聚类的粒度由参数 k 决定,它决定了我们可以期望组内项目之间的相似度。参数越高,聚类中的项目相似度越大,但聚类中的项目数量会减少。

总结

在本章中,我们学习了数据聚类的高效性,并且了解了如何通过将特征分类为属于该特征所在聚类的类别,从而加速新特征的分类。通过交叉验证,可以确定一个合适的聚类数量,选择最能提供准确分类结果的聚类数。

聚类根据数据的相似性对其进行排序。聚类的数量越多,聚类中各特征的相似性越大,但每个聚类中的特征数越少。

我们还了解到,k-均值算法是一种聚类算法,它试图以最小化簇内特征之间的互相距离为目标将特征聚集在一起。为了实现这一点,算法计算每个簇的质心,并且一个特征会归属于其质心最近的簇。算法在簇或其质心不再变化时结束计算。

在下一章中,我们将使用数学回归分析因变量之间的关系。与分类和聚类算法不同,回归分析将用于估算一个变量的最可能值,例如体重、距离或温度。

问题

问题 1:计算以下簇的质心:

a) 2, 3, 4

b) 100 美元,400 美元,1,000 美元

c) (10,20),(40,60),(0,40)

d) (200 美元,40 公里),(300 美元,60 公里),(500 美元,100 公里),(250 美元,200 公里)

e) (1,2,4), (0,0,3), (10,20,5), (4,8,2), (5,0,1)

问题 2:使用 k-均值聚类算法将以下数据集聚成两个、三个和四个簇:

a) 0,2,5,4,8,10,12,11

b) (2,2),(2,5),(10,4),(3,5),(7,3),(5,9),(2,8),(4,10),(7,4),(4,4),(5,8),(9,3)

问题 3:我们给出了一对夫妇的年龄以及他们的子女数量:

夫妻编号 妻子年龄 丈夫年龄 子女数量
1 48 49 5
2 40 43 2
3 24 28 1
4 49 42 3
5 32 34 0
6 24 27 0
7 29 32 2
8 35 35 2
9 33 36 1
10 42 47 3
11 22 27 2
12 41 45 4
13 39 43 4
14 36 38 2
15 30 32 1
16 36 38 0
17 36 39 3
18 37 38 ?

我们想通过聚类来猜测一对夫妻的子女数量,其中丈夫的年龄为 37 岁,妻子的年龄为 38 岁。

分析

问题 1: a)

b)

c)

d)

e)

问题 2: a) 我们添加第二个坐标,并将其设置为所有特征的 0。这样,特征之间的距离保持不变,我们可以使用本章早些时候实现的聚类算法。

输入

# source_code/5/problem5_2.csv 0,0
2,0
5,0
4,0
8,0
10,0
12,0
11,0

对于两个簇

$ python k-means_clustering.py problem5_2.csv 2 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((0.0, 0.0), 0), ((2.0, 0.0), 0), ((5.0, 0.0), 0), ((4.0, 0.0), 0), ((8.0, 0.0), 1), ((10.0, 0.0), 1), ((12.0, 0.0), 1), ((11.0, 0.0), 1)]
centroids = [(0.0, 0.0), (12.0, 0.0)]
Step number 1: point_groups = [((0.0, 0.0), 0), ((2.0, 0.0), 0), ((5.0, 0.0), 0), ((4.0, 0.0), 0), ((8.0, 0.0), 1), ((10.0, 0.0), 1), ((12.0, 0.0), 1), ((11.0, 0.0), 1)]
centroids = [(2.75, 0.0), (10.25, 0.0)]

对于三个簇

$ python k-means_clustering.py problem5_2.csv 3 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((0.0, 0.0), 0), ((2.0, 0.0), 0), ((5.0, 0.0), 2), ((4.0, 0.0), 2), ((8.0, 0.0), 2), ((10.0, 0.0), 1), ((12.0, 0.0), 1), ((11.0, 0.0), 1)]
centroids = [(0.0, 0.0), (12.0, 0.0), (5.0, 0.0)]
Step number 1: point_groups = [((0.0, 0.0), 0), ((2.0, 0.0), 0), ((5.0, 0.0), 2), ((4.0, 0.0), 2), ((8.0, 0.0), 2), ((10.0, 0.0), 1), ((12.0, 0.0), 1), ((11.0, 0.0), 1)]
centroids = [(1.0, 0.0), (11.0, 0.0), (5.666666666666667, 0.0)]

对于四个簇

$ python k-means_clustering.py problem5_2.csv 4 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((0.0, 0.0), 0), ((2.0, 0.0), 0), ((5.0, 0.0), 2), ((4.0, 0.0), 2), ((8.0, 0.0), 3), ((10.0, 0.0), 1), ((12.0, 0.0), 1), ((11.0, 0.0), 1)]
centroids = [(0.0, 0.0), (12.0, 0.0), (5.0, 0.0), (8.0, 0.0)]
Step number 1: point_groups = [((0.0, 0.0), 0), ((2.0, 0.0), 0), ((5.0, 0.0), 2), ((4.0, 0.0), 2), ((8.0, 0.0), 3), ((10.0, 0.0), 1), ((12.0, 0.0), 1), ((11.0, 0.0), 1)]
centroids = [(1.0, 0.0), (11.0, 0.0), (4.5, 0.0), (8.0, 0.0)]

b) 我们再次使用实现的算法。

输入

# source_code/5/problem5_2b.csv 2,2
2,5
10,4
3,5
7,3
5,9
2,8
4,10
7,4
4,4
5,8
9,3

两个簇的输出

$ python k-means_clustering.py problem5_2b.csv 2 last The total number of steps: 3
The history of the algorithm:
Step number 0: point_groups = [((2.0, 2.0), 0), ((2.0, 5.0), 0), ((10.0, 4.0), 1), ((3.0, 5.0), 0), ((7.0, 3.0), 1), ((5.0, 9.0), 1), ((2.0, 8.0), 0), ((4.0, 10.0), 0), ((7.0, 4.0), 1), ((4.0, 4.0), 0), ((5.0, 8.0), 1), ((9.0, 3.0), 1)]
centroids = [(2.0, 2.0), (10.0, 4.0)]
Step number 1: point_groups = [((2.0, 2.0), 0), ((2.0, 5.0), 0), ((10.0, 4.0), 1), ((3.0, 5.0), 0), ((7.0, 3.0), 1), ((5.0, 9.0), 0), ((2.0, 8.0), 0), ((4.0, 10.0), 0), ((7.0, 4.0), 1), ((4.0, 4.0), 0), ((5.0, 8.0), 0), ((9.0, 3.0), 1)]
centroids = [(2.8333333333333335, 5.666666666666667), (7.166666666666667, 5.166666666666667)]
Step number 2: point_groups = [((2.0, 2.0), 0), ((2.0, 5.0), 0), ((10.0, 4.0), 1), ((3.0, 5.0), 0), ((7.0, 3.0), 1), ((5.0, 9.0), 0), ((2.0, 8.0), 0), ((4.0, 10.0), 0), ((7.0, 4.0), 1), ((4.0, 4.0), 0), ((5.0, 8.0), 0), ((9.0, 3.0), 1)]
centroids = [(3.375, 6.375), (8.25, 3.5)]

三个簇的输出

$ python k-means_clustering.py problem5_2b.csv 3 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((2.0, 2.0), 0), ((2.0, 5.0), 0), ((10.0, 4.0), 1), ((3.0, 5.0), 0), ((7.0, 3.0), 1), ((5.0, 9.0), 2), ((2.0, 8.0), 2), ((4.0, 10.0), 2), ((7.0, 4.0), 1), ((4.0, 4.0), 0), ((5.0, 8.0), 2), ((9.0, 3.0), 1)]
centroids = [(2.0, 2.0), (10.0, 4.0), (4.0, 10.0)]
Step number 1: point_groups = [((2.0, 2.0), 0), ((2.0, 5.0), 0), ((10.0, 4.0), 1), ((3.0, 5.0), 0), ((7.0, 3.0), 1), ((5.0, 9.0), 2), ((2.0, 8.0), 2), ((4.0, 10.0), 2), ((7.0, 4.0), 1), ((4.0, 4.0), 0), ((5.0, 8.0), 2), ((9.0, 3.0), 1)]
centroids = [(2.75, 4.0), (8.25, 3.5), (4.0, 8.75)]

四个簇的输出

$ python k-means_clustering.py problem5_2b.csv 4 last The total number of steps: 2
The history of the algorithm:
Step number 0: point_groups = [((2.0, 2.0), 0), ((2.0, 5.0), 3), ((10.0, 4.0), 1), ((3.0, 5.0), 3), ((7.0, 3.0), 1), ((5.0, 9.0), 2), ((2.0, 8.0), 2), ((4.0, 10.0), 2), ((7.0, 4.0), 1), ((4.0, 4.0), 3), ((5.0, 8.0), 2), ((9.0, 3.0), 1)]
centroids = [(2.0, 2.0), (10.0, 4.0), (4.0, 10.0), (3.0, 5.0)]
Step number 1: point_groups = [((2.0, 2.0), 0), ((2.0, 5.0), 3), ((10.0, 4.0), 1), ((3.0, 5.0), 3), ((7.0, 3.0), 1), ((5.0, 9.0), 2), ((2.0, 8.0), 2), ((4.0, 10.0), 2), ((7.0, 4.0), 1), ((4.0, 4.0), 3), ((5.0, 8.0), 2), ((9.0, 3.0), 1)]
centroids = [(2.0, 2.0), (8.25, 3.5), (4.0, 8.75), (3.0, 4.666666666666667)]

问题 3:我们给出了 17 对夫妇的年龄和他们的孩子数量,并希望找出第 18 对夫妇的孩子数量。我们将使用前 14 对夫妇的数据作为训练数据,接下来的 3 对夫妇用于交叉验证,以确定我们将用来估算第 18 对夫妇孩子数量的聚类数k

聚类后,我们会说,一对夫妇很可能会拥有该聚类中的平均孩子数量。通过交叉验证,我们将选择能够最小化实际孩子数量与预测孩子数量之间差异的聚类数。我们将通过聚类内所有项的差异的平方和的平方根来累计捕捉这种差异。这将最小化随机变量与第 18 对夫妇预测的孩子数量之间的方差。

我们将进行两个、三个、四个和五个聚类的聚类分析。

输入

# source_code/5/couples_children.csv 48,49
40,43
24,28
49,42
32,34
24,27
29,32
35,35
33,36
42,47
22,27
41,45
39,43
36,38
30,32
36,38
36,39
37,38

两个聚类的输出

列出的每对夫妇对应一个聚类的形式是(couple_number,(wife_age,husband_age))

Cluster 0: [(1, (48.0, 49.0)), (2, (40.0, 43.0)), (4, (49.0, 42.0)), (10, (42.0, 47.0)), (12, (41.0, 45.0)), (13, (39.0, 43.0)), (14, (36.0, 38.0)), (16, (36.0, 38.0)), (17, (36.0, 39.0)), (18, (37.0, 38.0))]
Cluster 1: [(3, (24.0, 28.0)), (5, (32.0, 34.0)), (6, (24.0, 27.0)), (7, (29.0, 32.0)), (8, (35.0, 35.0)), (9, (33.0, 36.0)), (11, (22.0, 27.0)), (15, (30.0, 32.0))]

我们希望确定第 15 对夫妇(30,32)的预计孩子数量,即妻子 30 岁,丈夫 32 岁。(30,32)属于聚类 1。聚类 1 中的夫妇如下:(24.0, 28.0)(32.0, 34.0)(24.0, 27.0)(29.0, 32.0)(35.0, 35.0)(33.0, 36.0)(22.0, 27.0),以及(30.0, 32.0)。其中,包括前 14 对夫妇的数据,剩余的夫妇是:(24.0, 28.0)(32.0, 34.0)(24.0, 27.0)(29.0, 32.0)(35.0, 35.0)(33.0, 36.0),以及(22.0, 27.0)。这些夫妇的孩子数量平均值是est15=8/7~1.14。这是基于前 14 对夫妇数据估算的第 15 对夫妇的孩子数量。

第 16 对夫妇的估计孩子数量是est16=23/7~3.29。第 17 对夫妇的估计孩子数量也是est17=23/7~3.29,因为第 16 对和第 17 对夫妇属于同一聚类。

现在我们将计算E2误差(对于两个聚类来说是两个)在估计孩子数量(例如,第 15 对夫妇的孩子数量用est15表示)与实际孩子数量(例如,第 15 对夫妇的孩子数量用act15表示)之间的差异,如下所示:

现在我们已经计算了E2误差,我们将根据其他聚类来计算估算误差。我们将选择包含最少误差的聚类数来估算第 18 对夫妇的孩子数量。

三个聚类的输出

Cluster 0: [(1, (48.0, 49.0)), (2, (40.0, 43.0)), (4, (49.0, 42.0)), (10, (42.0, 47.0)), (12, (41.0, 45.0)), (13, (39.0, 43.0))]
Cluster 1: [(3, (24.0, 28.0)), (6, (24.0, 27.0)), (7, (29.0, 32.0)), (11, (22.0, 27.0)), (15, (30.0, 32.0))]
Cluster 2: [(5, (32.0, 34.0)), (8, (35.0, 35.0)), (9, (33.0, 36.0)), (14, (36.0, 38.0)), (16, (36.0, 38.0)), (17, (36.0, 39.0)), (18, (37.0, 38.0))]

现在,第 15 对夫妇在聚类 1,第 16 对夫妇在聚类 2,第 17 对夫妇在聚类 2。因此,每对夫妇的估计孩子数量是5/4=1.25

估算的 E3 错误如下:

四个聚类的输出

Cluster 0: [(1, (48.0, 49.0)), (4, (49.0, 42.0)), (10, (42.0, 47.0)), (12, (41.0, 45.0))]
Cluster 1: [(3, (24.0, 28.0)), (6, (24.0, 27.0)), (11, (22.0, 27.0))]
Cluster 2: [(2, (40.0, 43.0)), (13, (39.0, 43.0)), (14, (36.0, 38.0)), (16, (36.0, 38.0)), (17, (36.0, 39.0)), (18, (37.0, 38.0))]
Cluster 3: [(5, (32.0, 34.0)), (7, (29.0, 32.0)), (8, (35.0, 35.0)), (9, (33.0, 36.0)), (15, (30.0, 32.0))]

第 15^(th) 对夫妻位于聚类 3,第 16^(th) 对夫妻位于聚类 2,第 17^(th) 对夫妻也位于聚类 2。因此,第 15^(th) 对夫妻的估计子女数为 5/4=1.25。第 16^(th) 和第 17^(th) 对夫妻的估计子女数为 8/3=2.67。

估算的 E4 错误如下:

五个聚类的输出

Cluster 0: [(1, (48.0, 49.0)), (4, (49.0, 42.0))]
Cluster 1: [(3, (24.0, 28.0)), (6, (24.0, 27.0)), (11, (22.0, 27.0))]
Cluster 2: [(8, (35.0, 35.0)), (9, (33.0, 36.0)), (14, (36.0, 38.0)), (16, (36.0, 38.0)), (17, (36.0, 39.0)), (18, (37.0, 38.0))]
Cluster 3: [(5, (32.0, 34.0)), (7, (29.0, 32.0)), (15, (30.0, 32.0))]
Cluster 4: [(2, (40.0, 43.0)), (10, (42.0, 47.0)), (12, (41.0, 45.0)), (13, (39.0, 43.0))]

第 15^(th) 对夫妻位于聚类 3,第 16^(th) 对夫妻位于聚类 2,第 17^(th) 对夫妻也位于聚类 2。因此,第 15^(th) 对夫妻的估计子女数为 1。第 16^(th) 和第 17^(th) 对夫妻的估计子女数为 5/3=1.67。

估算的 E5 错误如下:

使用交叉验证来确定结果

我们使用了 14 对夫妻作为估算的训练数据,并使用另外三对夫妻进行交叉验证,以找出在 2、3、4 和 5 个聚类值之间的最佳 k 聚类参数。我们可以尝试聚类为更多的聚类,但由于数据量相对较小,最多聚类为五个聚类就足够了。让我们总结一下估算过程中产生的错误:

聚类数 错误率
2 3.3
3 2.17
4 2.7
5 2.13

35 个聚类的错误率最低。错误率在 4 个聚类时上升,随后在 5 个聚类时下降,这可能表明我们没有足够的数据来做出准确的估计。一个自然的预期是,对于 k 大于 2 的值,不应存在局部最大错误值。此外,3 个聚类和 5 个聚类的错误率差异非常小,而且 5 个聚类中的单个聚类要比 3 个聚类中的单个聚类小。因此,我们选择 3 个聚类而非 5 个聚类来估算第 18^(th) 对夫妻的子女数量。

当聚类为三个时,第 18^(th) 对夫妻位于聚类 2。因此,第 18^(th) 对夫妻的估计子女数为 1.25。

第六章:回归分析

回归分析是估计因变量之间关系的过程。例如,如果变量y 线性依赖于变量x,那么回归分析将尝试估算方程中的常数ab,该方程表示变量yx 之间的线性关系。

在本章中,您将学习以下主题:

  • 通过对完美数据进行简单线性回归来执行回归分析的核心思想,以便从第一原理推导华氏度与摄氏度转换示例。

  • 使用 Python 库scipy.linalg中的最小二乘法进行线性回归分析,应用于华氏度与摄氏度转换、基于身高的体重预测以及基于距离的飞行时间预测的完美数据和实际数据。

  • 使用梯度下降算法来找到最佳拟合的回归模型(采用最小均方误差规则),并学习如何在 Python 中实现它。

  • 使用最小二乘法建立非线性回归模型,应用于弹道飞行分析示例和问题 4细菌种群预测

华氏度与摄氏度转换 – 对完美数据的线性回归

华氏度和摄氏度之间的关系是线性的。给定一个包含华氏度和摄氏度数据对的表格,我们可以估计常数,推导出华氏度与摄氏度之间的转换公式,或者反过来:

⁰F ⁰C
5 -15
14 -10
23 -5
32 0
41 5
50 10

从第一原理的分析

我们希望推导出一个将F(华氏度)转换为C(摄氏度)的公式,如下所示:

这里,ab 是需要找到的常数。 函数的图像是一条直线,因此,它唯一由两个点确定。因此,我们实际上只需要表格中的两个点,比如,(F1,C1)(F2,C2)。然后,我们将得到以下内容:

现在,我们有以下内容:

因此,我们得到以下内容:

这里,我们取前两个数据对(F1,C1)=(5,-15)(F2,C2)=(14,-10)。这将给我们以下内容:

因此,从华氏度计算摄氏度的公式如下:

让我们通过表格中的数据验证这一点:

⁰F ⁰C (5/9)*F-160/9
5 -15 -15
14 -10 -10
23 -5 -5
32 0 0
41 5 5
50 10 10

我们可以看到公式完美地拟合了 100%的输入数据。我们所处理的数据是完美的。在后续的示例中,我们将看到我们推导出的公式并不完全符合数据。我们的目标是推导出一个最适合数据的公式,以使预测值与实际数据之间的差异最小化。

线性回归的最小二乘法

给定输入变量 和输出变量 ,我们希望找到变量 a 和 b,使得 对于每个i值从 1 到n都成立。如果数据是完美的,这是可能的。在实际数据中,我们希望* y[i] *尽可能接近 ,也就是说,我们希望最小化 的绝对值。对于一个特定的项,这等价于最小化

通常使用平方函数而不是绝对值,因为平方函数具有更理想的数学性质。

最小二乘法找出 a 和 b,使得以下项(称为误差)最小化:

使用最小二乘法在 Python 中进行分析

我们使用 scipy.linalg Python 库中的 lstsq 最小二乘法来计算摄氏度和华氏度变量之间的线性依赖关系。

lstsq 函数用作 lstsq(M, y),其中 M 是矩阵 M = x[:, np.newaxis]**[0, 1],由输入向量 x 构造,y 是输出向量。换句话说,y 是依赖于 x 的变量。

输入

# source_code/6/fahrenheit_celsius.py
import numpy as np
from scipy.linalg import lstsq

#temperature data
fahrenheit = np.array([5,14,23,32,41,50])
celsius = np.array([-15,-10,-5,0,5,10])

M = fahrenheit[:, np.newaxis]**[0, 1]
model, _, _, _ = lstsq(M,celsius)
print "Intercept =", model[0]
print "fahrenheit =", model[1] 

输出

$ python fahrenheit_celsius.py Intercept = -17.777777777777782
fahrenheit = 0.5555555555555558

因此,我们可以看到摄氏度(C)和华氏度(F)之间的以下近似线性依赖关系:

请注意,这与我们之前的计算相对应。

可视化

这是用于通过直线从华氏度预测摄氏度的线性模型。其含义是,只有当(F,C)点在绿色线上时,F才能转换为C,反之亦然:

根据身高预测体重——使用实际数据的线性回归

这里,我们通过线性回归使用以下数据来预测一个人的体重:

身高(厘米) 体重(公斤)
180 75
174 71
184 83
168 63
178 70
172 ?

我们想要估算一个人的体重,已知他的身高是 172 厘米。

分析

在前一个示例中,华氏度与摄氏度的转换数据完美地符合线性模型。因此,我们甚至可以进行简单的数学分析(求解基本方程)来获得转换公式。然而,现实世界中的大多数数据并不完全符合一个模型。对于这种分析,找到一个拟合给定数据且误差最小的模型是非常有益的。我们可以使用最小二乘法来找到这样的线性模型。

输入

我们将前面表格中的数据放入向量中,并尝试拟合线性模型:

# source_code/6/weight_prediction.py
import numpy as np
from scipy.linalg import lstsq

height = np.array([180,174,184,168,178])
weight = np.array([75,71,83,63,70])

M = height[:, np.newaxis]**[0, 1]
model, _, _, _ = lstsq(M,weight)
print "Intercept =", model[0]
print "height coefficient =", model[1] 

输出

$ python weight_prediction.py Intercept = -127.68817204301082
height coefficient = 1.1317204301075274

因此,表示体重与身高之间线性关系的公式如下:!

因此,我们估计身高为 172 厘米的人体重大约为!

梯度下降算法及其实现

为了更好地理解如何利用线性回归从基本原理出发预测一个值,我们需要研究梯度下降算法,并将其在 Python 中实现。

梯度下降算法

梯度下降算法是一种迭代算法,通过更新模型中的变量来拟合数据,使误差尽可能小。更一般地说,它找到一个函数的最小值。

我们希望通过使用线性公式来表达体重与身高的关系:

我们使用n个数据样本!来估计参数!,以最小化以下平方误差:

梯度下降算法通过在(∂/∂ p[j]) E(p)的方向上更新p[i]参数来实现这一点,具体为:

在这里,learning_rate决定了E(p)收敛速度的最小值。更新p参数将导致E(p)收敛到某个值,前提是learning_rate足够小。在 Python 程序中,我们使用learning_rate为 0.000001。然而,这种更新规则的缺点是E(p)的最小值可能只是局部最小值。

要通过编程更新p参数,我们需要展开对E(p)的偏导数。因此,我们更新p参数如下:

我们将继续更新p参数,直到其变化非常微小;即,直到p[0]p[1]的变化小于某个常数acceptable_error。一旦p参数稳定,我们可以利用它从身高估计体重。

实现

# source_code/6/regression.py
# Linear regression program to learn a basic linear model.
import math
import sys
sys.path.append('../common')
import common # noqa

# Calculate the gradient by which the parameter should be updated.
def linear_gradient(data, old_parameter):
    gradient = [0.0, 0.0]
    for (x, y) in data:
        term = float(y) - old_parameter[0] - old_parameter[1] * float(x)
        gradient[0] += term
        gradient[1] += term * float(x)
    return gradient

# This function will apply gradient descent algorithm
# to learn the linear model.
def learn_linear_parameter(data, learning_rate,
 acceptable_error, LIMIT):
    parameter = [1.0, 1.0]
    old_parameter = [1.0, 1.0]
    for i in range(0, LIMIT):
        gradient = linear_gradient(data, old_parameter)
        # Update the parameter with the Least Mean Squares rule.
        parameter[0] = old_parameter[0] + learning_rate * gradient[0]
        parameter[1] = old_parameter[1] + learning_rate * gradient[1]
        # Calculate the error between the two parameters to compare with
        # the permissible error in order to determine if the calculation
        # is sufficiently accurate.
        if abs(parameter[0] - old_parameter[0]) <= acceptable_error
        and abs(parameter[1] - old_parameter[1]) <= acceptable_error:
            return parameter
        old_parameter[0] = parameter[0]
        old_parameter[1] = parameter[1]
    return parameter

# Calculate the y coordinate based on the linear model predicted.
def predict_unknown(data, linear_parameter):
    for (x, y) in data:
        print(x, linear_parameter[0] + linear_parameter[1] * float(x))

# Program start
csv_file_name = sys.argv[1]
# The maximum number of the iterations in the batch learning algorithm.
LIMIT = 100
# Suitable parameters chosen for the problem given.
learning_rate = 0.0000001
acceptable_error = 0.001

(heading, complete_data, incomplete_data,
 enquired_column) = common.csv_file_to_ordered_data(csv_file_name)
linear_parameter = learn_linear_parameter(
    complete_data, learning_rate, acceptable_error, LIMIT)
print("Linear model:\n(p0,p1)=" + str(linear_parameter) + "\n")
print("Unknowns based on the linear model:")
predict_unknown(incomplete_data, linear_parameter)

输入

我们使用前一个示例中的数据 身高预测体重 并将其保存在 CSV 文件中:

# source_code/6/height_weight.csv
height,weight
180,75 
174,71 
184,83 
168,63 
178,70 
172,? 

输出

$ python regression.py height_weight.csv Linear model:
(p0,p1)=[0.9966468959362077, 0.4096393414704317]

Unknowns based on the linear model:
('172', 71.45461362885045)  

线性模型的输出意味着体重可以通过身高来表示,如下所示:

因此,一个身高为 172 厘米的人预测的体重大约是:

请注意,这个 71.455 公斤的预测与使用最小二乘法预测得到的 67.016 公斤略有不同。这可能是因为 Python 梯度下降算法仅找到了局部最小值。

可视化——最小二乘法与梯度下降算法的比较

通过使用基于身高的体重预测,我们可以可视化最小二乘法和梯度下降算法的线性预测模型,以下是在 Python 中实现的代码:

基于距离的飞行时间预测

给定一个包含航班起点、终点和飞行时间的表格,我们希望估算从斯洛伐克布拉迪斯拉发到荷兰阿姆斯特丹的拟议飞行时间:

起点 终点 距离(公里) 飞行时长 飞行时长(小时)
伦敦 阿姆斯特丹 365 1 小时 10 分钟 1.167
伦敦 布达佩斯 1462 2 小时 20 分钟 2.333
伦敦 布拉迪斯拉发 1285 2 小时 15 分钟 2.250
布拉迪斯拉发 巴黎 1096 2 小时 5 分钟 2.083
布拉迪斯拉发 柏林 517 1 小时 15 分钟 2.250
维也纳 都柏林 1686 2 小时 50 分钟 2.833
维也纳 阿姆斯特丹 932 1 小时 55 分钟 1.917
阿姆斯特丹 布达佩斯 1160 2 小时 10 分钟 2.167
布拉迪斯拉发 阿姆斯特丹 978 ? ?

分析

我们可以推理,飞行时间由两部分组成——第一部分是起飞和着陆的时间;第二部分是飞机在空中以某一速度飞行的时间。第一部分是一个常数。第二部分与飞机的速度成线性关系,我们假设所有航班的速度在表格中是相似的。因此,飞行时间可以通过一个线性公式来表示,涉及飞行距离。

输入

source_code/6/flight_time.py import numpy as np
from scipy.linalg import lstsq

distance = np.array([365,1462,1285,1096,517,1686,932,1160])
time = np.array([1.167,2.333,2.250,2.083,2.250,2.833,1.917,2.167])

M = distance[:, np.newaxis]**[0, 1]
model, _, _, _ = lstsq(M,time)
print "Intercept =", model[0]
print "distance coefficient =", model[1]

输出

$ python flight_time.py Intercept = 1.2335890147536381
distance coefficient = 0.0008386790405704925

根据线性回归,平均飞行的起飞和着陆时间大约为 1.2335890 小时。飞行 1 公里的时间为 0.0008387 小时;换句话说,飞机的速度为每小时 1192 公里。对于像前述表格中的短途航班,飞机的实际通常速度大约是每小时 850 公里。这为我们的估算留出了改进的空间(参见 问题 3)。

因此,我们推导出了以下公式:

利用这个,我们估算从布拉迪斯拉发到阿姆斯特丹的航程(978 公里)大约需要 0.0008387978 + 1.2335890 = 2.0538376* 小时,约为 2 小时 03 分钟,这比从维也纳到阿姆斯特丹的时间(1 小时 55 分钟)稍长,比从布达佩斯到阿姆斯特丹的时间(2 小时 10 分钟)稍短。

弹道飞行分析——非线性模型

一艘星际飞船降落在一个大气层几乎不存在的行星上,并以不同的初速度发射了三枚携带探测机器人的投射物。当机器人降落在表面后,测量它们的距离并记录数据,结果如下:

速度 (米/秒) 距离 (米)
400 38,098
600 85,692
800 152,220
? 300,000

为了使携带第四个机器人的投射物落地在距飞船 300 公里的地方,应该以什么速度发射?

分析

对于这个问题,我们需要理解投射物的轨迹。由于该行星的大气层较弱,轨迹几乎等同于没有空气阻力的弹道曲线。从地面发射的物体所经过的距离 d,忽略行星表面的曲率,近似由以下方程给出:

其中 v 是物体的初速度,τ 是物体发射的角度,g 是行星对物体施加的重力。请注意,角度 τ 和重力 g 是不变的。因此,可以定义一个常量为 。这意味着,可以通过以下方程用速度来解释探测行星上的旅行距离:

尽管 dv 之间没有线性关系,但 dv 的平方之间有线性关系。因此,我们仍然可以应用线性回归来确定 dv 之间的关系。

使用 Python 的最小二乘法进行分析

输入

source_code/6/speed_distance.py import numpy as np
from scipy.linalg import lstsq

distance = np.array([38098, 85692, 152220])
squared_speed = np.array([160000,360000,640000])

M = distance[:, np.newaxis]**[0, 1]
model, _, _, _ = lstsq(M,squared_speed)
print "Intercept =", model[0]
print "distance coefficient =", model[1]

输出

$ python speed_distance.py Intercept = -317.7078806050511
distance coefficient = 4.206199498720391

因此,通过回归可以预测速度平方与距离之间的关系,如下所示:

截距项的存在可能是由测量误差或方程中其他力的影响所造成的。由于它相对较小,最终的速度应该能被合理估计出来。将 300 公里的距离代入方程,我们得到如下结果:

因此,为了使投射物从源头到达 300 公里的距离,我们需要以大约 1123.157 米/秒的速度发射它。

总结

在这一章中,我们学习了回归分析。我们可以将变量看作是相互依赖的函数关系。例如,y 变量是 x 的函数,记作 y=f(x)。函数 f(x) 拥有常数参数。如果 y 线性依赖于 x,那么 f(x)=ax+b*,其中 abf(x) 函数中的常数参数。

我们看到,回归是一种估算这些常数参数的方法,目的是使得估算出的 f(x) 尽可能接近 y。这通常通过计算 f(x)y 之间的平方误差来衡量,以 x 数据样本为基础。

我们还介绍了梯度下降法,通过更新常数参数,沿着最陡的下降方向(即误差的偏导数)来最小化误差,从而确保参数快速收敛到使误差最小的值。

最后,我们学习了scipy.linalg Python 库,它支持使用基于最小二乘法的lstsq函数估算线性回归。

在下一章中,你将把回归分析应用于时间相关数据的分析。

问题

问题 1:云存储费用预测:我们的软件应用程序每月生成数据,并将这些数据与前几个月的数据一起存储在云存储中。我们得到以下云存储费用账单,并希望估算使用云存储的第一年的运营费用:

使用云存储的月份 每月费用(欧元)
1 120.00
2 131.20
3 142.10
4 152.90
5 164.30
1 到 12 ?

问题 2:华氏度与摄氏度转换:在本章前面我们看过的例子中,我们制定了一个将华氏度转换为摄氏度的公式。现在,制定一个将摄氏度转换为华氏度的公式。

问题 3:基于距离的飞行时间预测:你认为为什么线性回归模型得出的飞行速度估算为 1192 km/h,而实际速度大约是 850 km/h?你能提出一种更好的方法来建模基于飞行距离和时间的飞行时长估算吗?

问题 4:细菌种群预测:实验室观察到了一种细菌——大肠杆菌,并通过各种在 5 分钟间隔进行的测量估算了其种群数量,具体数据如下:

时间 种群数量(百万)
10:00 47.5
10:05 56.5
10:10 67.2
10:15 79.9
11:00 ?

假设细菌继续以相同的速度生长,那么在 11:00 时,预计细菌数量是多少?

分析

问题 1:每个月,我们都需要支付存储在云端的数据费用,包括本月新增的数据。我们将使用线性回归来预测一般月份的费用,然后计算前 12 个月的费用来计算全年的费用。

输入

source_code/6/cloud_storage.py
import numpy as np
from scipy.linalg import lstsq

month = np.array([1,2,3,4,5])
bill = np.array([120.0,131.2,142.1,152.9,164.3])

M = month[:, np.newaxis]**[0, 1]
model, _, _, _ = lstsq(M,bill)
print "Intercept =", model[0]
print "month_data =", model[1]

输出

$ python cloud_storage.py Intercept = 109.00999999999992
month_data = 11.030000000000008

这意味着基础费用为base_cost=109.01欧元,然后每月存储新增数据的费用为month_data=11.03欧元。因此,第n^(月)账单的公式如下:

记住,前n个数字的和是 。因此,前n个月的费用将如下所示:

全年的费用将如下所示:

可视化

  1. 在下图中,我们可以观察到模型的线性特性,由蓝线表示。另一方面,线性线上的点的和是二次的,并由线下的区域表示:

问题 2:有多种方法可以获得将摄氏度转换为华氏度的公式。我们可以使用最小二乘法,并从初始的 Python 文件中提取以下几行:

M = fahrenheit[:, np.newaxis]**[0, 1]

model, _, _, _ = lstsq(M,celsius)

然后我们将其更改为以下内容:

M = celsius[:, np.newaxis]**[0, 1]

model, _, _, _ = lstsq(M,fahrenheit)

然后,我们将得到期望的反向模型:

Intercept = 32.000000000000014
celsius = 1.7999999999999998

因此,可以通过以下方式将摄氏度转换为华氏度:

另外,我们可以通过修改以下公式得到前面的公式:

问题 3:估计的速度如此之高,因为即使是短途航班也需要相当长的时间;例如,从伦敦到阿姆斯特丹的航班,两个城市之间的距离仅为 365 公里,但需要约 1.167 小时。然而,另一方面,如果距离只稍微变化,则飞行时间也只会略有变化。这导致我们估计初始设置时间非常长。因此,飞行速度必须非常快,因为只剩下少量时间来完成某个距离的旅行。

如果我们考虑非常长的航班,其中初始设置时间与飞行时间的比率要小得多,我们可以更准确地预测飞行速度。

问题 4:在 5 分钟间隔内,细菌数量分别为 47.5、56.5、67.2 和 79.9 百万。这些数字之间的差值为 9、10.7 和 12.7。序列是递增的。因此,我们需要查看相邻项的比率,以观察序列的增长方式:56.5/47.5=1.18947,67.2/56.5=1.18938,79.9/67.2=1.18899。相邻项的比率接近,因此我们有理由相信,生长中的细菌数量可以通过模型的指数分布进行估算:

其中 n 是细菌数量(单位:百万),b 是常数(底数),字母 m 是指数,表示自 10:00 以来的分钟数,10:00 是第一次测量的时间,47.7 是当时的细菌数量(单位:百万)。

为了估算常数b,我们使用序列项之间的比率。我们知道 b⁵ 大约等于 (56.5/47.5 + 67.2/56.5 + 79.9/67.2)/3=1.18928。因此,常数b 大约等于 b=1.18928^(1/5)=1.03528。于是,细菌数量(单位:百万)如下:

在 11:00,也就是比 10:00 晚 60 分钟时,估算的细菌数量如下:

第七章:时间序列分析

时间序列分析是对时间依赖数据的分析。给定一段时间的数据,目的是预测不同时间段的数据,通常是未来的数据。例如,时间序列分析被用来预测金融市场、地震和天气。

在本章中,我们主要关注的是预测某些量的数值,例如,预测 2030 年的人口。

时间预测的主要元素如下:

  • 趋势:随着时间的推移,变量是趋于上升还是下降?例如,人类人口是增长还是减少?

  • 季节性:数据是否依赖于某些定期事件?例如,餐馆的利润在星期五比星期二要高吗?

将这两个时间序列分析元素结合起来,赋予我们一种强大的方法来进行时间依赖的预测。

在本章中,你将学习以下主题:

  • 通过商业利润的例子,使用回归分析数据趋势

  • 通过电子商店的销售例子,观察和分析数据中的季节性模式

  • 通过电子商店销售的例子,你将学习如何将趋势分析和季节性分析结合起来,预测时间依赖的数据。

  • 使用商业利润和电子商店销售的例子,在 Python 中创建时间依赖的模型

商业利润 – 分析趋势

我们希望预测 2018 年企业的利润,基于其前几年的利润:

年份 利润(美元)
2011 $40,000
2012 $43,000
2013 $45,000
2014 $50,000
2015 $54,000
2016 $57,000
2017 $59,000
2018 ?

分析

在这个例子中,利润一直在增长,所以我们可以将利润看作是一个依赖于时间变量的增长函数,而时间变量用年份表示。各年份之间的利润变化分别为$3,000、$2,000、$5,000、$4,000、$3,000 和$2,000。这些差异似乎不受时间影响,而且它们之间的变化相对较小。因此,我们可以尝试通过进行线性回归来预测未来几年的利润。我们用线性方程表示利润p,它与年份y相关,也叫做趋势线:

我们可以通过线性回归找到常数ab

使用 Python 中的最小二乘法分析趋势

输入

我们将前表中的数据存储在 Python 代码中的yearprofit向量中:

# source_code/7/profit_year.py #Predicting profit based on the year
import numpy as np
from scipy.linalg import lstsq

year = np.array([2011,2012,2013,2014,2015,2016,2017])
profit = np.array([40,43,45,50,54,57,59])

M = year[:, np.newaxis]**[0, 1]
model, _, _, _ = lstsq(M,profit)
print "Intercept =", model[0]
print "year coefficient =", model[1]

输出

$ python profit_year.py Intercept = -6711.571428570175
year coefficient = 3.3571428571422364

可视化

现在,我们将趋势线添加到图表中:

商业利润 - 数据分析

结论

该公司利润的趋势线方程如下:

从这个方程中,我们可以预测 2018 年的利润为:

这个示例很简单——我们仅通过对趋势线进行线性回归就能够做出预测。

在下一个示例中,我们将查看既受趋势又受季节性影响的数据。

电子商店的销售 – 分析季节性

我们有一个小型电子商店从 2010 年到 2017 年的每月销售数据,单位为千美元。我们想要预测 2018 年每个月的销售额:

月份/年份 2010 2011 2012 2013 2014 2015 2016 2017 2018
1 月 10.5 11.9 13.2 14.6 15.1 16.5 18.9 20
2 月 11.9 12.6 14.4 15.4 17.4 17.9 19.5 20.8
3 月 13.4 13.5 16.1 16.2 17.2 19.6 19.8 22.1
4 月 12.7 13.6 14.9 17.8 17.8 20.2 19.7 20.9
5 月 13.9 14.6 15.7 17.8 18.6 19.1 20.8 21.5
6 月 14 14.4 15.3 16.1 18.9 19.7 21.1 22.1
7 月 13.5 15.7 16.8 17.4 18.3 19.7 21 22.6
8 月 14.5 14 15.7 17 17.9 20.5 21 22.7
9 月 14.3 15.5 16.8 17.2 19.2 20.3 20.6 21.9
10 月 14.9 15.8 16.3 17.9 18.8 20.3 21.4 22.9
11 月 16.9 16.5 18.7 20.5 20.4 22.4 23.7 24
12 月 17.4 20.1 19.7 22.5 23 23.8 24.6 26.6

表格中的数据是以千美元为单位的销售额。

分析

为了能够分析这个问题,我们将首先绘制数据图表,以便观察模式并进行分析:

从图表和表格中,我们注意到,从长远来看,销售额呈线性增长。这是趋势。然而,我们也可以看到,12 月的销售额往往比其他任何月份都高。因此,我们有理由相信,销售额也受到月份的影响。

我们如何预测未来几年的月度销售额?首先,我们确定数据的确切长期趋势。然后,我们分析每个月的变化。

使用最小二乘法在 Python 中分析趋势

输入

年份列表包含按小数表示的年份周期 年+月/12。销售列表包含对应周期的千美元销售额。我们将使用线性回归来建立趋势线。从初步图表中,我们观察到趋势呈线性:

# source_code/6/sales_year.py
#Predicting sales based on the period in the year
import numpy as np
from scipy.linalg import lstsq

year = np.array([2010.000000, 2010.083333, 2010.166667, 2010.250000,
    2010.333333, 2010.416667, 2010.500000, 2010.583333,
    2010.666667, 2010.750000, 2010.833333, 2010.916667,
    2011.000000, 2011.083333, 2011.166667, 2011.250000,
    2011.333333, 2011.416667, 2011.500000, 2011.583333,
    2011.666667, 2011.750000, 2011.833333, 2011.916667,
    2012.000000, 2012.083333, 2012.166667, 2012.250000,
    2012.333333, 2012.416667, 2012.500000, 2012.583333,
    2012.666667, 2012.750000, 2012.833333, 2012.916667,
    2013.000000, 2013.083333, 2013.166667, 2013.250000,
    2013.333333, 2013.416667, 2013.500000, 2013.583333,
    2013.666667, 2013.750000, 2013.833333, 2013.916667,
    2014.000000, 2014.083333, 2014.166667, 2014.250000,
    2014.333333, 2014.416667, 2014.500000, 2014.583333,
    2014.666667, 2014.750000, 2014.833333, 2014.916667,
    2015.000000, 2015.083333, 2015.166667, 2015.250000,
    2015.333333, 2015.416667, 2015.500000, 2015.583333,
    2015.666667, 2015.750000, 2015.833333, 2015.916667,
    2016.000000, 2016.083333, 2016.166667, 2016.250000,
    2016.333333, 2016.416667, 2016.500000, 2016.583333,
    2016.666667, 2016.750000, 2016.833333, 2016.916667,
    2017.000000, 2017.083333, 2017.166667, 2017.250000,
    2017.333333, 2017.416667, 2017.500000, 2017.583333,
    2017.666667, 2017.750000, 2017.833333, 2017.916667])
sale = np.array([10.500000, 11.900000, 13.400000, 12.700000, 13.900000,
    14.000000, 13.500000, 14.500000, 14.300000, 14.900000,
    16.900000, 17.400000, 11.900000, 12.600000, 13.500000,
    13.600000, 14.600000, 14.400000, 15.700000, 14.000000,
    15.500000, 15.800000, 16.500000, 20.100000, 13.200000,
    14.400000, 16.100000, 14.900000, 15.700000, 15.300000,
    16.800000, 15.700000, 16.800000, 16.300000, 18.700000,
    19.700000, 14.600000, 15.400000, 16.200000, 17.800000, 
    17.800000, 16.100000, 17.400000, 17.000000, 17.200000,
    17.900000, 20.500000, 22.500000, 15.100000, 17.400000,
    17.200000, 17.800000, 18.600000, 18.900000, 18.300000,
    17.900000, 19.200000, 18.800000, 20.400000, 23.000000,
    16.500000, 17.900000, 19.600000, 20.200000, 19.100000,
    19.700000, 19.700000, 20.500000, 20.300000, 20.300000, 
    22.400000, 23.800000, 18.900000, 19.500000, 19.800000,
    19.700000, 20.800000, 21.100000, 21.000000, 21.000000,
    20.600000, 21.400000, 23.700000, 24.600000, 20.000000,
    20.800000, 22.100000, 20.900000, 21.500000, 22.100000,
    22.600000, 22.700000, 21.900000, 22.900000, 24.000000,
    26.600000])

M = year[:, np.newaxis]**[0, 1]
model, _, _, _ = lstsq(M,sale)
print "Intercept =", model[0]
print "year coefficient =", model[1]

输出

$ python sales_year.py Intercept = -2557.777649371433
year coefficient = 1.2789880745818643

因此,趋势线的方程如下:

可视化

现在,我们将趋势线添加到图表中:

分析季节性

现在,我们可以分析季节性——即数据如何随着月份变化。从我们的观察中,我们知道在某些月份,销售额往往较高,而在其他月份,销售额则较低。我们评估线性趋势与实际销售额之间的差异。根据这些差异中的模式,我们构建了一个季节性模型,以便更准确地预测每个月的销售额:

一月销售额
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均值
实际销售额 10.5 11.9 13.2 14.6 15.1 16.5 18.9 20
趋势线上的销售额 13.012 14.291 15.57 16.849 18.128 19.407 20.686 21.965
差异 -2.512 -2.391 -2.37 -2.249 -3.028 -2.907 -1.786 -1.965 -2.401
二月销售额
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均值
实际销售额 11.9 12.6 14.4 15.4 17.4 17.9 19.5 20.8
趋势线上的销售额 13.1185833333 14.3975833333 15.6765833333 16.9555833333 18.2345833333 19.5135833333 20.7925833333 22.0715833333
差异 -1.2185833333 -1.7975833333 -1.2765833333 -1.5555833333 -0.8345833333 -1.6135833333 -1.2925833333 -1.2715833333 -1.3575833333
三月销售额
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均值
实际销售额 13.4 13.5 16.1 16.2 17.2 19.6 19.8 22.1
趋势线上的销售额 13.2251666667 14.5041666667 15.7831666667 17.0621666667 18.3411666667 19.6201666667 20.8991666667 22.1781666667
差异 0.1748333333 -1.0041666667 0.3168333333 -0.8621666667 -1.1411666667 -0.0201666667 -1.0991666667 -0.0781666667 -0.4641666667
四月销售额
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均值
实际销售额 12.7 13.6 14.9 17.8 17.8 20.2 19.7 20.9
趋势线上的销售额 13.33175 14.61075 15.88975 17.16875 18.44775 19.72675 21.00575 22.28475
差异 -0.63175 -1.01075 -0.98975 0.63125 -0.64775 0.47325 -1.30575 -1.38475 -0.60825
五月销售额
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均值
实际销售额 13.9 14.6 15.7 17.8 18.6 19.1 20.8 21.5
趋势线上的销售额 13.4383333333 14.7173333333 15.9963333333 17.2753333333 18.5543333333 19.8333333333 21.1123333333 22.3913333333
差异 0.4616666667 -0.1173333333 -0.2963333333 0.5246666667 0.0456666667 -0.7333333333 -0.3123333333 -0.8913333333 -0.1648333333
六月销售
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均
实际销售 14 14.4 15.3 16.1 18.9 19.7 21.1 22.1
趋势线上的销售 13.5449166667 14.8239166667 16.1029166667 17.3819166667 18.6609166667 19.9399166667 21.2189166667 22.4979166667
差异 0.4550833333 -0.4239166667 -0.8029166667 -1.2819166667 0.2390833333 -0.2399166667 -0.1189166667 -0.3979166667 -0.3214166667
七月销售
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均
实际销售 13.5 15.7 16.8 17.4 18.3 19.7 21 22.6
趋势线上的销售 13.6515 14.9305 16.2095 17.4885 18.7675 20.0465 21.3255 22.6045
差异 -0.1515 0.7695 0.5905 -0.0885 -0.4675 -0.3465 -0.3255 -0.0045 -0.003
八月销售
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均
实际销售 14.5 14 15.7 17 17.9 20.5 21 22.7
趋势线上的销售 13.7580833333 15.0370833333 16.3160833333 17.5950833333 18.8740833333 20.1530833333 21.4320833333 22.7110833333
差异 0.7419166667 -1.0370833333 -0.6160833333 -0.5950833333 -0.9740833333 0.3469166667 -0.4320833333 -0.0110833333 -0.3220833333
九月销售
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均
实际销售 14.3 15.5 16.8 17.2 19.2 20.3 20.6 21.9
趋势线上的销售 13.8646666667 15.1436666667 16.4226666667 17.7016666667 18.9806666667 20.2596666667 21.5386666667 22.8176666667
差异 0.4353333333 0.3563333333 0.3773333333 -0.5016666667 0.2193333333 0.0403333333 -0.9386666667 -0.9176666667 -0.1161666667
十月销售
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均
实际销售 14.9 15.8 16.3 17.9 18.8 20.3 21.4 22.9
趋势线上的销售 13.97125 15.25025 16.52925 17.80825 19.08725 20.36625 21.64525 22.92425
差异 0.92875 0.54975 -0.22925 0.09175 -0.28725 -0.06625 -0.24525 -0.02425 0.08975
十一月销售
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均
实际销售 16.9 16.5 18.7 20.5 20.4 22.4 23.7 24
趋势线上的销售 14.0778333333 15.3568333333 16.6358333333 17.9148333333 19.1938333333 20.4728333333 21.7518333333 23.0308333333
差异 2.8221666667 1.1431666667 2.0641666667 2.5851666667 1.2061666667 1.9271666667 1.9481666667 0.9691666667 1.8331666667
12 月销售额
年份 2010 2011 2012 2013 2014 2015 2016 2017 平均
实际销售额 17.4 20.1 19.7 22.5 23 23.8 24.6 26.6
趋势线上的销售额 14.1844166667 15.4634166667 16.7424166667 18.0214166667 19.3004166667 20.5794166667 21.8584166667 23.1374166667
差异 3.2155833333 4.6365833333 2.9575833333 4.4785833333 3.6995833333 3.2205833333 2.7415833333 3.4625833333 3.5515833333

我们无法观察到实际销售额与趋势线销售额之间的差异有任何明显的趋势。因此,我们仅计算每个月这些差异的算术平均值。

例如,我们注意到,12 月的销售额通常比趋势线预测的销售额高出大约 3,551.58 美元。类似地,1 月的销售额通常较低,平均低于趋势线预测的销售额约 2,401 美元。

假设月份对实际销售额有影响,这是根据我们观察到的不同月份销售额的变化来得出的,我们采用以下预测规则:

然后我们将其更新为新的规则:

这里,sales 是所选月份和年份的销售额预测值,month_difference 是我们在给定数据中实际销售额与趋势线销售额之间的平均差异。更具体地说,我们得到以下 12 个方程和 2018 年的销售额预测(单位:千美元):

结论

该表格包含了 2018 年基于前面季节性方程的销售数据。

我们可以将预测的数据可视化为图形:

总结

本章中,我们研究了时间依赖数据的分析。此分析中最重要的两个因素是趋势和季节性。

趋势分析可以看作是确定数据分布的函数。利用数据依赖于时间这一事实,可以通过回归来确定该函数。许多现象有线性趋势线,而其他现象可能不遵循线性模式。

我们还了解到,季节性分析试图发现时间中反复出现的规律模式,例如圣诞节前的销售增加。为了检测季节性模式,必须将数据划分为不同的季节,以便模式在同一季节中重复出现。此划分可以将一年分为月份,一周分为天数或工作日和周末等。合理的季节划分和对这些季节中的模式分析是良好季节性分析的关键。

一旦趋势和季节性在数据中被分析完,结合的结果就是对时间依赖数据在未来将遵循的模式的预测。

本章介绍了本书中呈现的最后一种数据分析方法。在本章之后,您可以找到关于统计学、Python 编程语言概念以及数据科学中算法和方法的参考资料。

问题

问题 1确定比特币价格的趋势

a) 我们给出了一个包含 2010-2017 年比特币价格(美元)的表格。请确定这些价格的线性趋势线。每月价格取自每个月的第一天:

日期:年-月-日 比特币价格(美元)
2010-12-01 0.23
2011-06-01 9.57
2011-12-01 3.06
2012-06-01 5.27
2012-12-01 12.56
2013-06-01 129.3
2013-12-01 946.92
2014-06-01 629.02
2014-12-01 378.64
2015-06-01 223.31
2015-12-01 362.73
2016-06-01 536.42
2016-12-01 753.25
2017-06-01 2,452.18

b) 根据 a)部分的线性趋势线,预计 2020 年比特币的价格是多少?

c) 讨论线性趋势线是否适合作为预测比特币未来价格的良好指标。

问题 2电子商店销售: 使用电子商店销售示例中的数据,预测 2019 年每个月的销售情况。

分析

问题 1

输入:

source_code/7/year_bitcoin.py #Determining a linear trend line for Bitcoin
import numpy as np
from scipy.linalg import lstsq

year = np.array([2010.91666666666, 2011.41666666666, 2011.91666666666,
             2012.41666666666, 2012.91666666666, 2013.41666666666,
             2013.91666666666, 2014.41666666666, 2014.91666666666,
             2015.41666666666, 2015.91666666666, 2016.41666666666,
             2016.91666666666, 2017.41666666666])
btc_price = np.array([0.23, 9.57, 3.06, 5.27, 12.56, 129.3, 946.92, 629.02,
                378.64, 223.31, 362.73, 536.42, 753.25, 2452.18])

M = year[:, np.newaxis]**[0, 1]
model, _, _, _ = lstsq(M,btc_price)
print "Intercept =", model[0]
print "year coefficient =", model[1]

输出:

$ python year_bitcoin.py Intercept = -431962.903846178
year coefficient = 214.69081318682586

趋势线:

从 Python 代码的输出中,我们发现比特币美元价格的线性趋势线如下:

这为我们提供了以下趋势线图:

根据趋势线,预计 2020 年 1 月 1 日比特币的价格为 1,731.10 美元。

线性趋势线可能不是比特币价格的一个良好指标或预测工具。这是因为有许多因素在起作用,而且比特币价格有可能呈现出技术趋势中常见的指数性增长。例如,Facebook 活跃用户的数量和低于 1000 美元的最佳消费类 CPU 中的晶体管数量。

有三个重要因素可能促使比特币的指数型采用,从而推动其价格上涨:

  • 技术成熟度(可扩展性):每秒交易数量可以确保即时转账,即使许多人使用比特币进行收付款。

  • 稳定性:一旦卖家不再害怕因收到比特币付款而失去利润,他们会更加愿意接受比特币作为货币。

  • 用户友好性:一旦普通用户能够自然地进行比特币的收付款,就不会有技术障碍,使用比特币就像使用他们习惯的任何其他货币一样。

要分析比特币的价格,我们必须考虑更多的数据,并且很可能其价格不会遵循线性趋势。

问题 2:我们使用示例中的 12 个公式,每个月一个,来预测 2019 年每个月的销售额:

sales_january = 1.279(year+0/12) - 2557.778 - 2.401*

= 1.279(2019 + 0/12) - 2557.778 - 2.401 = 22.122*

sales_february = 1.279(2019+1/12) - 2557.778 - 1.358 = 23.272*

sales_march = 1.279(2019+2/12) - 2557.778 - 0.464 = 24.272*

sales_april = 1.279(2019+3/12) - 2557.778 - 0.608 = 24.234*

sales_may = 1.279(2019+4/12) - 2557.778 - 0.165 = 24.784*

sales_june = 1.279(2019+5/12) - 2557.778 - 0.321 = 24.735*

sales_july = 1.279(2019+6/12) - 2557.778 - 0.003 = 25.160*

sales_august = 1.279(2019+7/12) - 2557.778 - 0.322 = 24.947*

sales_september = 1.279(2019+8/12) - 2557.778 - 0.116 = 25.259*

sales_october = 1.279(2019+9/12) - 2557.778 + 0.090 = 25.572*

sales_november = 1.279(2019+10/12) - 2557.778 + 1.833 = 27.422*

sales_december = 1.279(2019+11/12) - 2557.778 + 3.552 = 29.247*

电子商店销售额 - 数据分析

第八章:Python 参考

介绍

Python 是一种通用编程和脚本语言。其简洁性和丰富的库使得开发出快速符合现代技术需求的应用成为可能。

Python 代码写在以.py为后缀的文件中,可以通过python命令执行。

Python Hello World 示例

Python 中最简单的程序会打印一行文本。

输入

source_code/appendix_c_python/example00_helloworld.py
print "Hello World!"

输出

$ python example00_helloworld.py
Hello World!

注释

注释在 Python 中不会被执行。它们以#字符开头,并以行尾结束。

输入

# source_code/appendix_c_python/example01_comments.py print "This text will be printed because the print statement is executed."
#This is just a comment and will not be executed.
#print "Even commented statements are not executed."
print "But the comment finished with the end of the line."
print "So the 4th and 5th line of the code are executed again."

输出

$ python example01_comments.py This text will be printed because the print statement is executed
But the comment finished with the end of the line.
So the 4th and 5th line of the code are executed again.

数据类型

Python 中可用的一些数据类型如下:

  • 数值数据类型intfloat

  • 文本数据类型str

  • 复合数据类型tuplelistsetdictionary

整型

int数据类型只能存储整数值。

输入

# source_code/appendix_c_python/example02_int.py rectangle_side_a = 10
rectangle_side_b = 5
rectangle_area = rectangle_side_a * rectangle_side_b
rectangle_perimeter = 2*(rectangle_side_a + rectangle_side_b)
print "Let there be a rectangle with the sides of lengths:"
print rectangle_side_a, "and", rectangle_side_b, "cm."
print "Then the area of the rectangle is", rectangle_area, "cm squared."
print "The perimeter of the rectangle is", rectangle_perimeter, "cm."

输出

$ python example02_int.py Let there be a rectangle with the sides of lengths: 10 and 5 cm.
Then the area of the rectangle is 50 cm squared.
The perimeter of the rectangle is 30 cm.

浮点数

float数据类型也可以存储非整数的有理数值。

输入

# source_code/appendix_c_python/example03_float.py pi = 3.14159
circle_radius = 10.2
circle_perimeter = 2 * pi * circle_radius
circle_area = pi * circle_radius * circle_radius
print "Let there be a circle with the radius", circle_radius, "cm."
print "Then the perimeter of the circle is", circle_perimeter, "cm."
print "The area of the circle is", circle_area, "cm squared."

输出

$ python example03_float.py Let there be a circle with the radius 10.2 cm.
Then the perimeter of the circle is 64.088436 cm.
The area of the circle is 326.8510236 cm squared.

字符串

字符串变量可以用来存储文本。

输入

# source_code/appendix_c_python/example04_string.py first_name = "Satoshi"
last_name = "Nakamoto"
full_name = first_name + " " + last_name
print "The inventor of Bitcoin is", full_name, "."

输出

$ python example04_string.py The inventor of Bitcoin is Satoshi Nakamoto .

元组

tuple数据类型类似于数学中的向量;例如,tuple = (整数, 浮点数)

输入

# source_code/appendix_c_python/example05_tuple.py import math

point_a = (1.2,2.5)
point_b = (5.7,4.8)
#math.sqrt computes the square root of a float number.
#math.pow computes the power of a float number.
segment_length = math.sqrt(
        math.pow(point_a[0] - point_b[0], 2) +
        math.pow(point_a[1] - point_b[1], 2))
print "Let the point A have the coordinates", point_a, "cm."
print "Let the point B have the coordinates", point_b, "cm."
print "Then the length of the line segment AB is", segment_length, "cm."

输出

$ python example05_tuple.py Let the point A have the coordinates (1.2, 2.5) cm.
Let the point B have the coordinates (5.7, 4.8) cm.
Then the length of the line segment AB is 5.0537115074 cm.

列表

Python 中的列表是一个有序的值集合。

输入

# source_code/appendix_c_python/example06_list.py some_primes = [2, 3]
some_primes.append(5)
some_primes.append(7)
print "The primes less than 10 are:", some_primes

输出

$ python example06_list.py The primes less than 10 are: [2, 3, 5, 7]

集合

Set是 Python 中的一个无序的数学集合类型。

输入

# source_code/appendix_c_python/example07_set.py from sets import Set

boys = Set(['Adam', 'Samuel', 'Benjamin'])
girls = Set(['Eva', 'Mary'])
teenagers = Set(['Samuel', 'Benjamin', 'Mary'])
print 'Adam' in boys
print 'Jane' in girls
girls.add('Jane')
print 'Jane' in girls
teenage_girls = teenagers & girls #intersection
mixed = boys | girls #union
non_teenage_girls = girls - teenage_girls #difference
print teenage_girls
print mixed
print non_teenage_girls

输出

$ python example07_set.py True
False
True
Set(['Mary'])
Set(['Benjamin', 'Adam', 'Jane', 'Eva', 'Samuel', 'Mary'])
Set(['Jane', 'Eva'])

字典

dictionary是一种数据结构,可以通过键存储对应的值。

输入

# source_code/appendix_c_python/example08_dictionary.py dictionary_names_heights = {}
dictionary_names_heights['Adam'] = 180.
dictionary_names_heights['Benjamin'] = 187
dictionary_names_heights['Eva'] = 169
print 'The height of Eva is', dictionary_names_heights['Eva'], 'cm.'

输出

$ python example08_dictionary.py The height of Eva is 169 cm.

流程控制

Python 编程语言通过使用条件语句、for循环(包括breakcontinue语句)以及函数来控制程序执行流程。

条件语句

如果满足特定条件,可以使用if语句执行一定的代码。如果条件不满足,则可以执行else语句后的代码。如果第一个条件不满足,我们可以设置下一个条件,通过elif语句来执行代码。

输入

# source_code/appendix_c_python/example09_if_else_elif.py x = 10
if x == 10:
        print 'The variable x is equal to 10.'

if x > 20:
        print 'The variable x is greater than 20.'
else:
        print 'The variable x is not greater than 20.'

if x > 10:
        print 'The variable x is greater than 10.'
elif x > 5:
        print 'The variable x is not greater than 10, but greater ' +
              'than 5.'
else:
        print 'The variable x is not greater than 5 or 10.'

输出

$ python example09_if_else_elif.py The variable x is equal to 10.
The variable x is not greater than 20.
The variable x is not greater than 10, but greater than 5.

for循环

for循环可以遍历一组元素中的每个元素,例如,rangepython setlist

范围上的for循环

输入

source_code/appendix_c_python/example10_for_loop_range.py print "The first 5 positive integers are:"
for i in range(1,6):
        print i

输出

$ python example10_for_loop_range.py The first 5 positive integers are:
1
2
3
4
5

列表上的for循环

输入

source_code/appendix_c_python/example11_for_loop_list.py primes = [2, 3, 5, 7, 11, 13]
print 'The first', len(primes), 'primes are:'
for prime in primes:
        print prime

输出

$ python example11_for_loop_list.py The first 6 primes are:
2
3
5
7
11
13

Break 和 continue

for循环可以通过break语句提前退出。使用continue语句可以跳过for循环中的剩余部分。

输入

source_code/appendix_c_python/example12_break_continue.py for i in range(0,10):
        if i % 2 == 1: #remainder from the division by 2
                continue
        print 'The number', i, 'is divisible by 2.'

for j in range(20,100):
        print j
        if j > 22:
                break;

输出

$ python example12_break_continue.py The number 0 is divisible by 2.
The number 2 is divisible by 2.
The number 4 is divisible by 2.
The number 6 is divisible by 2.
The number 8 is divisible by 2.
20
21
22
23

函数

Python 支持函数的定义,这是一种很好的方式,可以在程序的多个地方执行相同的代码。函数是使用def关键字定义的。

输入

source_code/appendix_c_python/example13_function.py def rectangle_perimeter(a, b):
        return 2 * (a + b)

print 'Let a rectangle have its sides 2 and 3 units long.'
print 'Then its perimeter is', rectangle_perimeter(2, 3), 'units.'
print 'Let a rectangle have its sides 4 and 5 units long.'
print 'Then its perimeter is', rectangle_perimeter(4, 5), 'units.'

输出

$ python example13_function.py Let a rectangle have its sides 2 and 3 units long.
Then its perimeter is 10 units.
Let a rectangle have its sides 4 and 5 units long.
Then its perimeter is 18 units.

输入与输出

让我们看看如何向 Python 程序传递参数,以及如何读写文件。

程序参数

我们可以通过命令行向程序传递参数。

输入

source_code/appendix_c_python/example14_arguments.py #Import the system library in order to use the argument list.
import sys

print 'The number of the arguments given is', len(sys.argv),'arguments.'
print 'The argument list is ', sys.argv, '.'

输出

$ python example14_arguments.py arg1 110 The number of the arguments given is 3 arguments.
The argument list is  ['example14_arguments.py', 'arg1', '110'] .

读写文件

以下程序将写入两行到test.txt文件,然后读取它们,最后将其打印到输出。

输入

# source_code/appendix_c_python/example15_file.py #write to the file with the name "test.txt"
file = open("test.txt","w") 
file.write("first line\n")
file.write("second line")
file.close()

#read the file
file = open("test.txt","r")
print file.read()

输出

$ python example15_file.py first line
second line

第九章:统计学

基本概念

符号

两个集合AB的交集,表示为A ∩ B,是集合AB的一个子集,包含所有在AB中都有的元素。换句话说,A ∩ B := { x : x in A and x in B}

两个集合A和 B 的并集,表示为A ∪ B,是一个包含所有在A中或在B中的元素的集合。换句话说,A ∪ B := { x : x in A or x in B}

两个集合AB的差集,表示为A – BA\B,是集合A的一个子集,其中包含所有在A中但不在B中的元素。换句话说,A – B := { x : x in A and x not in B}

求和符号∑表示集合中所有成员的和,例如:

定义与术语

  • 总体:一组相似的数据或项,用于分析。

  • 样本:总体的一个子集。

  • 一组的算术平均数:一组中所有值的总和除以该组的大小。

  • 中位数:一个有序集合中的中间值,例如,集合{x1, …, x2k+1}的中位数,其中x1 <…< x2k+1 是值xk+1

  • 随机变量:从可能结果集合(例如,正面或反面)到值集合(例如,正面为 0,反面为 1)的一个函数。

  • 期望:随机变量的期望是随机变量给定的值的增加集的平均值的极限。

  • 方差:衡量总体与其均值之间的离散程度。从数学角度来看,随机变量X的方差是随机变量与其均值μ之差的平方的期望值,即

  • 标准差:随机变量X的偏差是X变量变化的平方根,即

  • 相关性:随机变量之间依赖关系的度量。从数学角度来看,随机变量XY之间的相关性定义为

  • 因果关系:通过发生一个现象来解释另一个现象的依赖关系。因果关系意味着相关性,但相关性不一定意味着因果关系!

  • 斜率:线性方程y=ax+b中的a*变量。

  • 截距:线性方程y=ax+b中的b*变量。

贝叶斯推断

P(A)P(B)分别为AB的概率,P(A|B)为在给定B的条件下A的条件概率,P(B|A)为在给定A的条件下B的概率。

然后,贝叶斯定理给出了以下公式:

P(A|B)=(P(B|A) * P(A))/P(B)

分布

概率分布是一个从可能结果集合(例如,正面和反面)到这些结果的概率集合(即,正面和反面各 50%)的函数。

正态分布

许多自然现象的随机变量可以通过正态分布来建模。正态分布具有以下概率密度:

其中,μ是分布的均值,是分布的方差。正态分布的图形呈钟形曲线;例如,参见以下均值为 10、标准差为 2 的正态分布图:

均值为 10,标准差为 2 的正态分布

交叉验证

交叉验证是一种验证数据假设的方法。在分析过程开始时,数据被划分为学习数据和测试数据。一个假设被拟合到学习数据上,然后其实际误差在测试数据上进行测量。通过这种方式,我们可以估计假设在未来数据上的表现。减少学习数据的数量也有好处,因为它可以减少假设过拟合的机会。这是指假设被训练到数据的特别狭窄子集上。

K 折交叉验证

原始数据随机分成k个子集。一个子集用于验证,剩余的k-1 个子集用于假设训练。

A/B 测试

A/B 测试是对数据的两个假设进行验证——通常是在真实数据上进行。然后,选择具有更好结果(更低估计误差)的假设作为未来数据的估计器。

第十章:数据科学中的算法与方法词汇表

  • k近邻算法:一种算法,根据与数据项最接近的k个邻居的多数情况来估算未知数据项的分类。

  • 朴素贝叶斯分类器:一种使用贝叶斯定理对数据项进行分类的方法,涉及条件概率P(A|B)=(P(B|A) * P(A))/P(B)。它还假设数据中的变量是独立的,这意味着没有任何变量会影响其他变量取某一值的概率。

  • 决策树:一种通过树叶节点中的类别对数据项进行分类的模型,分类基于树枝与实际数据项之间的匹配属性。

  • 随机决策树:一种决策树,其中每个分支在构建时仅使用可用变量的随机子集。

  • 随机森林:由多棵随机决策树组成的集成算法,这些决策树是在数据的随机子集上进行构建的,并且允许重复采样。每个数据项会根据其树木中的多数投票结果进行分类。

  • K均值算法:一种聚类算法,将数据集划分为k个组,使每个组中的成员尽可能相似,即彼此之间距离最小。

  • 回归分析:一种用于估计功能模型中未知参数的方法,该模型用于从输入变量预测输出变量。例如,估计线性模型y=ax+b中的ab*。

  • 时间序列分析:依赖于时间的数据分析;主要包括趋势和季节性的分析。

  • 支持向量机:一种分类算法,通过找到一个超平面将训练数据划分到给定的类别中。然后,通过这个超平面进一步对数据进行分类。

  • 主成分分析:对给定数据的各个组成部分进行预处理,以便实现更好的准确性,例如根据变量对最终结果的影响程度,调整输入向量中变量的尺度。

  • 文本挖掘:对文本的搜索和提取,并可能将其转化为用于数据分析的数值数据。

  • 神经网络:一种机器学习算法,由一系列简单的分类器构成,分类器根据输入数据或网络中其他分类器的结果做出决策。

  • 深度学习:神经网络提高其学习过程的能力。

  • 先验关联规则:可以在训练数据中观察到的规则,并基于这些规则对未来的数据进行分类。

  • PageRank:一种搜索算法,赋予搜索结果最大的相关性,这些结果来自于具有最多、最相关搜索链接的网页。用数学术语来说,PageRank 计算出一个特定的特征向量,表示这些相关性的度量。

  • 集成学习:一种学习方法,使用不同的学习算法来得出最终结论。

  • 袋装法:通过对训练数据的随机子集训练出的分类器进行多数投票来对数据项进行分类的方法。

  • 遗传算法:受遗传过程启发的机器学习算法,例如,经过训练的分类器中具有最高准确度的分类器将进一步训练。

  • 归纳推理:一种机器学习方法,用于学习生成实际数据的规则。

  • 贝叶斯网络:表示随机变量及其条件依赖关系的图模型。

  • 奇异值分解:矩阵的分解方法,是特征值分解的推广,常用于最小二乘法。

  • 提升法:一种机器学习元算法,通过基于分类器的集成做出预测,从而减少估计的方差。

  • 期望最大化:一种迭代方法,用于搜索模型中能够最大化预测准确度的参数。

posted @ 2025-10-27 09:00  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报