Python-真实世界的数据科学-七-

33 阅读1小时+

Python 真实世界的数据科学(七)

原文:Python: Real-World Data Science

协议:CC BY-NC-SA 4.0

二十二、数据挖掘入门

我们正在以人类历史上从未见过的规模收集信息,并越来越重视在日常生活中使用这些信息。 我们希望我们的计算机将网页翻译成其他语言,预测天气,建议我们想要的书并诊断我们的健康问题。 这些期望将会增加,无论是应用数量还是预期的功效。 数据挖掘是一种方法,可以用来训练计算机使用数据进行决策,并且构成了当今许多高科技系统的骨干。

有充分的理由,Python 语言正在迅速普及。 它给程序员很大的灵活性。 它具有大量执行不同任务的模块; Python 代码通常比其他任何语言都更具可读性和简洁性。 有大量活跃的研究人员,从业人员和初学者使用 Python 进行数据挖掘。

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

  • 什么是数据挖掘,可在哪里使用?
  • 设置基于 Python 的环境以执行数据挖掘
  • 亲和力分析示例,根据购买习惯推荐产品
  • (经典)分类问题的一个示例,根据其度量预测植物种类

引入数据挖掘

数据挖掘为计算机提供了一种学习如何对数据进行决策的方法。 这个决定可能是预测明天的天气,阻止垃圾邮件进入您的收件箱,检测网站的语言或在约会网站上找到新的恋情。 数据挖掘有许多不同的应用,新的应用一直在被发现。

数据挖掘是算法,统计,工程,优化和计算机科学的一部分。 我们还使用其他领域的概念和知识,例如语言学,神经科学或城市规划。 有效地应用它通常需要将此特定于领域的知识与算法集成在一起。

大多数数据挖掘应用都使用相同的高级视图,尽管细节通常会发生相当大的变化。 我们通过创建描述真实世界的一个方面的数据集来开始数据挖掘过程。 数据集包含两个方面的:

  • 是现实世界中的对象的样本。 这可以是书,照片,动物,人或任何其他物体。
  • 的功能是对数据集中样本的描述。 特征可以是给定单词的长度,频率,支路数量,创建日期等。

下一步是调整数据挖掘算法。 每个数据挖掘算法都有参数,这些参数可以在算法内,也可以由用户提供。 通过这种调整,算法可以学习如何制定有关数据的决策。

作为一个简单的示例,我们希望计算机能够将人们分类为“矮”或“高”。 我们从收集数据集开始,该数据集包括不同人的身高以及他们的矮矮或高矮:

|

|

高度

|

矮还是高?

| | --- | --- | --- | | 1 | 155 厘米 | 短的 | | 2 | 165 厘米 | 短的 | | 3 | 175 厘米 | 高的 | | 4 | 185 厘米 | 高的 |

下一步涉及调整我们的算法。 作为一种简单的算法; 如果身高大于x,则该人个子高,否则个子矮。 然后,我们的训练算法将查看数据并确定x的合适值。 对于前面的数据集,合理的值为 170 cm。 该算法认为身高高于 170 厘米的任何人。 其他人被认为是矮子。

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

注意

在本模块中,我们将介绍通过 Python 进行数据挖掘。 在某些情况下,我们选择代码和工作流程的清晰度,而不是最优化的方式来做到这一点。 该有时涉及跳过一些细节,这些细节可以提高算法的速度或有效性。

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

在本节中,我们跳入第一个示例。 数据挖掘的一个常见用例是通过询问正在购买产品的客户是否也想要其他类似产品来提高销售。 这可以通过亲和力分析来完成,这是对事物何时存在的研究。

什么是亲和力分析?

相似性分析是一种数据挖掘类型,可以使样本(对象)之间具有相似性。 这可能是以下两者之间的相似之处:

  • 网站上的用户,以提供各种服务或针对性的广告
  • 出售给这些用户的商品,以便提供推荐的电影或产品
  • 人类基因,以便找到拥有相同祖先的人

我们可以通过多种方式测量亲和力。 例如,我们可以记录两次购买产品的频率。 我们还可以记录一个人购买对象 1 以及购买对象 2 时语句的准确性。其他衡量亲和力的方法包括计算样本之间的相似度,我们将在后面的章节中介绍。

产品推荐

将传统业务(例如商务)在线移动的问题之一是,过去人类必须完成的任务需要自动化,以便在线业务得以扩展。 一个例子就是向上销售,或向已经购买的客户出售额外的物品。 通过数据挖掘进行自动产品推荐是电子商务革命的推动力之一,电子商务革命每年将数十亿美元转化为收入。

在此示例中,我们将专注于基本的产品推荐服务。 我们根据以下想法进行设计:历史上两个物品一起购买时,将来很有可能一起购买。 这种想法是在线和离线企业中许多产品推荐服务的背后。

对于这种类型的产品推荐算法,一种非常简单的算法是简单地找到用户带来商品的任何历史案例,并推荐历史用户带来的其他商品。 在实践中,像这样的简单算法效果很好,至少比选择要推荐的随机项目更好。 但是,可以极大地改进它们,这就是数据挖掘的用武之地。

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

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

涉及多个项目的更复杂的规则将无法涵盖,例如购买香肠和汉堡的人更有可能购买番茄酱。

使用 NumPy 加载数据集

可以从课程随附的代码包中下载数据集。 下载此文件并将其保存在计算机上,并注意数据集的路径。 对于本示例,建议您在计算机上创建一个新文件夹以放入数据集和代码。从此处打开 IPython Notebook,导航至该文件夹并创建一个新笔记本。

我们将在此示例中使用的数据集是 NumPy 二维数组,该数组是格式,是该模块其余部分中大多数示例的基础。 该数组看起来像一个表格,其中行代表不同的样本,列代表不同的特征。

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

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

对于此示例,运行 IPython Notebook 并创建 IPython Notebook。 在笔记本的第一个单元格中输入上述代码。 然后,您可以通过按 Shift + 输入来运行代码(这还将为下一批代码添加新的单元格)。 运行代码后,第一个单元格左侧的方括号将被分配一个递增数字,让您知道此单元格已完成。 第一个单元格应如下所示:

Loading the dataset with NumPy

对于以后将花费更多时间运行的代码,此处将使用星号表示此代码正在运行或已计划运行。 代码运行完毕后,该星号将替换为数字。

您将需要将数据集保存到与 IPython Notebook 相同的目录中。 如果选择将其存储在其他位置,则需要将dataset_filename值更改为新位置。

接下来,我们可以显示数据集的某些行,以了解数据集的外观。 在下一个单元格中输入以下代码行并运行它,以便打印数据集的前五行:

print(X[:5])

结果将显示在前五个交易中购买了哪些物品:

Loading the dataset with NumPy

一次查看每行(水平线)即可读取数据集。 第一行(0, 0, 1, 1, 1)显示在第一笔交易中购买的物品。 每一列(垂直行)代表每个项目。 它们分别是面包,牛奶,奶酪,苹果和香蕉。 因此,在第一笔交易中,该人购买了奶酪,苹果和香蕉,但没有买面包或牛奶。

这些功能中的每一个都包含二进制值,仅说明是否购买了商品,而不说明购买了多少商品。 1 表示“至少购买了 1 个”此类商品,而 0 表示绝对未购买任何此类商品。

实施规则的简单排名

我们希望找到类型为的规则。如果某人购买产品 X,那么他们很可能会购买产品 Y。 通过简单地查找同时购买两种产品的所有情况,我们可以很容易地在数据集中创建所有规则的列表。 但是,我们然后需要一种从坏规则中确定好的规则的方法。 这将使我们能够选择要推荐的特定产品。

可以用很多方法来衡量这种类型的规则,其中我们将把重点放在两个方面:支持置信度

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

在支持测量规则存在的频率时,置信度测量规则在使用时的准确性。 可以通过确定前提适用时规则适用的次数百分比来计算。 我们首先计算一个规则在我们的数据集中适用的次数,然后将其除以前提(if语句)出现的样本数。

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

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

Implementing a simple ranking of rules

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

现在,我们需要为数据库中的所有规则计算这些统计信息。 为此,我们将为有效规则无效规则创建字典。 该词典的关键是元组(前提和结论)。 我们将存储索引,而不是实际的特征名称。 因此,我们将存储(3 和 4)以表示先前的规则*。如果某人购买苹果,他们还将购买香蕉*。 如果给出前提和结论,则该规则被视为有效。 尽管给出了前提但没有得出结论,但该规则被认为对该样本无效。

为了计算所有可能规则的置信度和支持度,我们首先设置一些字典来存储结果。 我们将为此使用defaultdict,如果访问的键尚不存在,它将设置默认值。 我们记录有效规则,无效规则的数量以及每个前提的出现:

from collections import defaultdict
valid_rules = defaultdict(int)
invalid_rules = defaultdict(int)
num_occurances = defaultdict(int)

接下来,我们在一个大循环中计算这些值。 我们遍历数据集中的每个样本和特征。 第一个功能构成规则的前提-如果某人购买产品前提:

for sample in X:
  for premise in range(4):

我们检查该样本是否存在前提。 如果不是这样,我们就不再需要对此样本/前提组合进行任何处理,然后移至循环的下一个迭代:

  if sample[premise] == 0: continue

如果前提对于此样本有效(其值为 1),则我们记录此情况并检查规则的每个结论。 我们跳过与前提相同的任何结论,这将给我们一些规则,例如,如果某人购买苹果,那么他们购买苹果,这显然对我们没有多大帮助;

  num_occurances[premise] += 1
  for conclusion in range(n_features):
      if premise == conclusion: continue

如果存在该样本的结论,则我们将增加此规则的有效计数。 如果没有,我们将为该规则增加无效计数:

  if sample[conclusion] == 1:valid_rules[(premise, conclusion)] += 1else:
    invalid_rules[(premise, conclusion)] += 1

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

support = valid_rules

置信度的计算方法相同,但是我们必须遍历每个规则才能计算出此置信度:

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

现在,我们有了一本字典,其中包含对每个规则的支持和信心。 我们可以创建一个函数,以一种可读的格式打印出规则。 规则的签名包含前提和结论索引,我们刚刚计算的支持和置信度字典以及可以告诉我们特征含义的特征数组:

def print_rule(premise, conclusion,
              support, confidence, features):

我们获取前提和结论的特征名称,并以可读格式打印规则:

    premise_name = features[premise]conclusion_name = features[conclusion]print("Rule: If a person buys {0} they will also buy {1}".format(premise_name, conclusion_name))

然后我们打印出此规则的SupportConfidence

    print(" - Support: {0}".format(support[(premise,
                                            conclusion)]))
    print(" - Confidence: {0:.3f}".format(confidence[(premise,
                                                    conclusion)]))

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

Implementing a simple ranking of rules

排名以找到最佳规则

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

为了找到支持最高的规则,我们首先对支持字典进行排序。 默认情况下,字典不支持排序。 items()函数为我们提供了一个包含字典中数据的列表。 我们可以使用itemgetter类作为键对列表进行排序,从而可以对嵌套列表(例如此列表)进行排序。 使用itemgetter(1)可以使我们根据值进行排序。 设置reverse=True首先为我们提供最高值:

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

然后,我们可以打印出最重要的五个规则:

for index in range(5):
    print("Rule #{0}".format(index + 1))
    premise, conclusion = sorted_support[index][0]
    print_rule(premise, conclusion, support, confidence, features)

结果将如下所示:

Ranking to find the best rules

同样,我们可以根据置信度打印最重要的规则。 首先,计算排序后的置信度列表:

sorted_confidence = sorted(confidence.items(), key=itemgetter(1), reverse=True)

接下来,使用与以前相同的方法将它们打印出来。 注意第三行对sorted_confidence的更改;

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

Ranking to find the best rules

两个列表的顶部附近有两个规则。 第一个是如果一个人购买苹果,他们还将购买奶酪,第二个是如果一个人购买奶酪,他们还将购买香蕉。 商店经理可以使用此类规则来组织商店。 例如,如果本周销售苹果,则在附近摆放奶酪。 同样,将两种香蕉与奶酪同时出售也没有意义,因为将近 66%的购买奶酪的人仍然会购买香蕉-我们的销售不会增加香蕉的购买量。

在这样的示例中,数据挖掘具有强大的探索能力。 一个人可以使用数据挖掘技术来探索其数据集中的关系以找到新的见解。 在下一节中,我们将数据挖掘用于其他目的:预测。

Ranking to find the best rules

一个简单的分类示例

在相似性分析示例中,我们在数据集中寻找了不同变量之间的相关性。 在分类中,我们只具有一个我们感兴趣的变量,我们将其称为(也称为目标)。 如果在上一个示例中,我们对如何使人们购买更多苹果感兴趣,则可以将该变量设置为作为类别,并寻找实现该目标的分类规则。 然后,我们将仅寻找与该目标相关的规则。

什么是分类?

无论是在实际应用还是在研究中,分类都是数据挖掘的最大用途之一。 和以前一样,我们有一组样本来表示我们感兴趣的分类对象或事物。 我们还有一个新的数组,类值。 这些类别值为我们提供了样本的分类。 一些示例如下:

  • 通过查看植物的尺寸来确定植物的种类。 这里的分类值是这是哪个物种?
  • 确定图像中是否包含狗。 这个班级是此图片上有只狗吗?
  • 根据测试结果确定患者是否患有癌症。 类别为该患者患有癌症吗?

尽管上面的许多示例都是二元(是/否)问题,但不一定非要如此,就像本节中植物种类的分类一样。

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

加载和准备数据集

我们将在此示例中使用的数据集是著名的植物分类 Iris 数据库。 在此数据集中,我们有 150 个植物样品,每个样品有四个测量值:萼片长度萼片宽度花瓣长度花瓣宽度(均以厘米为单位)。 这个经典数据集(最早在 1936 年使用!)是数据挖掘经典数据集中的一个。 分为三类:鸢尾鸢尾鸢尾花鸢尾花。 目的是通过检查其测量值来确定样品属于哪种植物。

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

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

您也可以打印(dataset.DESCR)以查看数据集的轮廓,包括有关要素的一些详细信息。

此数据集中的特征是连续值,这意味着它们可以采用任何范围的值。 测量是此类功能的一个很好的示例,其中测量可以取值为 1、1.2 或 1.25,依此类推。 关于连续特征的另一个方面是彼此接近的特征值指示相似性。 萼片长度为 1.2 厘米的植物就像萼片宽度为 1.25 厘米的植物一样。

相反,是分类特征。 这些功能通常用数字表示,但不能以相同的方式进行比较。 在虹膜数据集中,类值是分类特征的示例。 类 0 代表鸢尾鸢尾花,类 1 代表鸢尾花,类 2 代表鸢尾花。 这并不意味着 Iris Setosa 与 Iris Versicolour 比与 Iris Virginica 更相似,尽管其类值更相似。 此处的数字代表类别。 我们只能说类别是相同还是不同。

也有其他类型的功能,其中一些功能将在后面的章节中介绍。

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

一种简单的离散化算法是选择一些阈值,并且该阈值以下的任何值都给定值 0。与此同时,任何高于此阈值的值都给定值 1。对于我们的阈值,我们将计算该特征的均值(平均值) 。 首先,我们计算每个特征的均值:

attribute_means = X.mean(axis=0)

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

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

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

Loading and preparing the dataset

实现 OneR 算法

OneR 是一种简单的算法,它通过查找特征值最频繁的类别来简单地预测样本的类别。 OneR 是的缩写,是一个规则,表示通过选择性能最佳的功能,我们仅对该分类使用一条规则。 尽管某些后来的算法明显更复杂,但该简单算法已在许多实际数据集中表现出良好的性能。

该算法从迭代每个功能的每个值开始。 对于该值,计算每个类别具有该特征值的样本数。 记录最频繁出现的特征值类别以及该预测的错误。

例如,如果某个要素具有两个值01,则我们首先检查所有值为0的样本。 对于该值,我们可能在A类中有 20 个,在B类中有 60 个,在C类中又有 20 个。 此值最常见的类别是B,并且有 40 个实例具有差异类别。 该特征值的预测为B,误差为 40,因为有 40 个样本的类别与预测不同。 然后,针对此功能的1值,然后对所有其他功能值组合执行相同的过程。

一旦计算了所有这些组合,我们就可以通过汇总该功能的所有值的误差来计算每个功能的误差。 总误差最低的特征被选为一个规则,然后用于对其他实例进行分类。

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

from collections import defaultdict
from operator import itemgetter

接下来,我们创建函数定义,它需要数据集,类,我们感兴趣的特征的索引以及我们正在计算的值:

def train_feature_value(X, y_true, feature_index, value):

然后,我们遍历数据集中的所有样本,计算具有该特征值的每个样本的实际类别:

    class_counts = defaultdict(int)
    for sample, y in zip(X, y_true):
        if sample[feature_index] == value:
            class_counts[y] += 1

然后,我们通过对class_counts字典进行排序并找到最大值来找到分配最频繁的类:

    sorted_class_counts = sorted(class_counts.items(),key=itemgetter(1), reverse=True)most_frequent_class = sorted_class_counts[0][0]

最后,我们计算该规则的误差。 在 OneR 算法中,具有此特征值的任何样本都将被预测为最频繁的类别。 因此,我们通过对其他类别(不是最常见)的计数求和来计算误差。 这些代表该规则不适用的训练样本:

incorrect_predictions = [class_count for class_value, class_count in class_counts.items()if class_value != most_frequent_class]
error = sum(incorrect_predictions)

最后,我们既返回该特征值的预测类别,又返回该规则的分类错误的训练样本数,错误:

    return most_frequent_class, error

使用此功能,我们现在可以通过遍历该功能的所有值,对错误求和并记录每个值的预测类来计算整个功能的误差。

函数头需要我们感兴趣的数据集,类和特征索引:

def train_on_feature(X, y_true, feature_index):

接下来,我们找到给定功能采用的所有唯一值。 下一行的索引将查看给定功能的整列,并将其作为数组返回。 然后,我们使用 set 函数仅查找唯一值:

    values = set(X[:,feature_index])

接下来,我们创建将存储预测变量的字典。 该词典将具有要素值作为键,并将分类作为值。 键值为 1.5 且值为 2 的条目表示,当要素的值设置为 1.5 时,将其归为 2 类。我们还创建了一个列表,用于存储每个要素值的错误:

    predictors = {}
    errors = []

作为此功能的主要部分,我们迭代此功能的所有唯一值,并使用先前定义的函数train_feature_value()查找给定功能值的最常见类别和错误。 我们存储上述结果:

    for current_value in values:
      most_frequent_class, error = train_feature_value(X, y_true, feature_index, current_value)
      predictors[current_value] = most_frequent_class
      errors.append(error)

最后,我们计算此规则的总误差,并返回预测值和该值:

    total_error = sum(errors)
    return predictors, total_error

测试算法

当我们评估最后一部分的亲和力分析算法时,我们的目的是探索当前数据集。 通过这种分类,我们的问题就不同了。 我们想要建立一个模型,通过将它们与我们对问题的了解进行比较,从而对我们之前看不见的样本进行分类。

因此,我们将机器学习工作流程分为两个阶段:训练和测试。 在训练中,我们获取一部分数据集并创建模型。 在测试中,我们应用该模型并评估其在数据集上的有效性。 由于我们的目标是创建一个能够对以前看不见的样本进行分类的模型,因此我们不能使用测试数据来训练模型。 如果这样做,我们将面临过度拟合的风险。

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

scikit-learn库包含一个将数据分为训练和测试组件的功能:

from sklearn.cross_validation import train_test_split

此功能将根据给定的比率(默认情况下使用数据集的 25%进行测试)将数据集分为两个子数据集。 它是随机执行的,从而提高了对该算法进行适当测试的信心:

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

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

我们还指定了一个特定的random_state。 设置随机状态将在每次输入相同的值时进行相同的分割。 看起来是随机的,但是所使用的算法是确定性的,输出将保持一致。 对于此模块,我建议将随机状态设置为与我相同的值,因为它将为您提供与我相同的结果,从而使您可以验证结果。 要获得每次运行都会改变的真正随机结果,请将random_state设置为none

接下来,我们为数据集的所有特征计算预测变量。 请记住,仅在此过程中使用训练数据。 我们遍历数据集中的所有特征,并使用我们先前定义的函数来训练预测变量并计算误差:

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

接下来,我们通过找到错误最少的功能来找到用作“一个规则”的最佳功能:

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

然后,我们通过存储最佳功能的预测变量来创建model

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

我们的模型是一个字典,它告诉我们一个规则使用哪个功能以及根据其具有的值进行的预测。 在这种模型的情况下,我们可以通过查找特定特征的值并使用适当的预测变量来预测以前未见过的样本的类别。 以下代码针对给定的示例执行此操作:

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

通常,我们想一次预测许多新样本,我们可以使用以下函数进行预测: 我们使用上面的代码,但是迭代数据集中的所有样本,以获得每个样本的预测:

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

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

y_predicted = predict(X_test, model)

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

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

这给出了 68%的准确度,对于单个规则来说还不错!

Testing the algorithm

Testing the algorithm

Testing the algorithm

Testing the algorithm

二十三、将 scikit-learn 估计器用于分类

scikit-learn库是数据挖掘算法的集合,这些数据挖掘算法是用 Python 编写的,并使用通用编程接口。 这使用户可以轻松尝试不同的算法,并利用标准工具进行有效的测试和参数搜索。 scikit-learn 中有大量算法和实用程序。

在本章中,我们着重于为运行数据挖掘过程建立一个良好的框架。 这将在后面的章节中使用,所有这些章节都将重点放在那些情况下的应用和技术上。

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

  • 估算器:此用于执行分类,聚类和回归
  • 提升器:此用于执行预处理和数据更改
  • 管道:这是,可将您的工作流程整合为可复制的格式

scikit 学习估算器

估计器是scikit-learn's抽象,允许大量分类算法的标准化实现。 估计器用于分类。 估算器具有以下两个主要功能:

  • fit():此进行算法训练并设置内部参数。 它需要两个输入,训练样本数据集和这些样本的相应类。
  • predict():此预测作为输入给出的测试样本的类别。 此函数返回一个数组,其中包含每个输入测试样本的预测。

大多数scikit-learn估算器都将NumPy数组或相关格式用于输入和输出。

scikit-learn 中有大量估计量。 这些包括支持向量机SVM),随机 森林神经网络。 这些算法中的许多算法将在的后续章节中使用。 在本章中,我们将使用与scikit-learn不同的估算器:最近邻居

注意

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

$pip3 install matplotlib

如果您在安装matplotlib时遇到任何困难,请在这个页面中查找官方安装说明。

最近的邻居

最近的邻居是,可能是标准数据挖掘算法集中最直观的算法之一。 为了预测新样本的类别,我们在训练数据集中浏览了与新样本最相似的样本。 我们采用最相似的样本,并预测大多数样本所具有的类别。

例如,我们希望根据三角形的类更相似来预测它的类(此处通过将相似的对象靠得更近来表示)。 我们寻找三个最近的邻居,即两个菱形和一个正方形。 菱形多于圆形,因此三角形的预测类别为菱形:

Nearest neighbors

最近邻居几乎可用于任何数据集,但是,计算所有样本对之间的距离在计算上非常昂贵。 例如,如果数据集中有 10 个样本,则有 45 个唯一距离要计算。 但是,如果有 1000 个样本,则将近 500,000! 存在多种方法可以大大提高该速度。 其中一些内容将在本模块的后续章节中介绍。

它在基于分类的数据集中的表现也很差,应该使用其他算法代替。

Nearest neighbors

距离指标

数据挖掘中的一个关键基础概念是距离。 如果我们有两个样本,则需要知道它们彼此之间有多近。 此外,我们需要回答一些问题,例如这两个样本是否比其他两个样本更相似? 回答这样的问题对于案件的结果很重要。

人们知道的最常见的距离度量是欧几里德距离,它是现实世界中的距离。 如果要在图形上绘制这些点并使用直尺测量距离,则结果将是欧几里得距离。 更正式地说,它是每个要素的平方距离之和的平方根。

欧几里得距离很直观,但是如果某些要素的值大于其他要素,则精度会较差。 当许多特征的值为 0(称为稀疏矩阵)时,结果也会很差。 还有其他距离度量标准正在使用中。 常用的两个是曼哈顿距离和余弦距离。

曼哈顿距离是每个要素的绝对差之和(不使用平方距离)。 直观上,可以将中的白嘴鸦棋子(或城堡)在点之间移动所需要的移动次数考虑在内,前提是一次只能移动一个正方形。 如果某些要素具有比其他要素更大的值,则曼哈顿距离确实会受到影响,但效果却不如欧几里得。

余弦距离更适合,适用于某些特征大于其他特征且数据集中存在大量零的情况。 直观地,我们从原点到每个样本绘制一条直线,并测量这些直线之间的角度。 在下图中可以看到:

Distance metrics

在此示例中,每个灰色圆圈与白色圆圈的距离相同。 在(a)中,距离是欧几里得距离,因此,类似的距离适用于一个圆。 可以使用标尺测量该距离。 在(b)中,距离为曼哈顿,也称为城市街区。 我们通过在行和列之间移动来计算距离,类似于国际象棋中的白嘴鸦(城堡)如何移动。 最后,在(c)中,我们具有余弦距离,该余弦距离是通过计算从样本绘制到向量的直线之间的角度而测得的,而忽略了直线的实际长度。

选择的距离指标可能会对最终效果产生很大影响。 例如,如果您有许多功能,则随机样本之间的欧几里德距离接近相同的值。 由于距离相同,因此很难比较样本! 在某些情况下,曼哈顿距离可能会更稳定,但是如果某些要素的值很大,则可能会推翻在其他要素中的许多相似性。 最后,余弦距离是比较具有大量特征的项目的一个很好的指标,但是它会丢弃一些有关向量长度的信息,这在某些情况下很有用。

在本章中,我们将在后面的章节中使用其他度量来保持欧几里得距离。

Distance metrics

加载数据集

我们将要使用的数据集被称为电离层,其中是许多高频天线的记录。 天线的目的是确定电离层中是否存在结构以及高层大气中是否存在区域。 具有结构的那些被认为是好的,而没有结构的那些被认为是坏的。 此应用的目的是建立一个数据挖掘分类器,该分类器可以确定图像的好坏。

Loading the dataset

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

可以从 UCL 机器学习数据存储库下载,其中包含用于不同数据挖掘应用的大量数据集。 转到这个页面并单击数据文件夹。 将,ionosphere.dataionosphere.names文件下载到计算机上的文件夹中。 对于此示例,我假设您已将数据集放在主文件夹中名为 Data 的目录中。

注意

主文件夹的位置取决于您的操作系统。 对于 Windows,通常为C:\Documents and Settings\username。 对于 Mac 或 Linux 机器,通常为/home/username。 您可以通过运行以下 python 代码获取主文件夹:

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

对于数据集中的每一行,有 35 个值。 前 34 个是从 17 根天线(每个天线有两个值)获取的测量值。 最后一个是“ g”或“ b”; 分别代表好与坏。

启动 IPython Notebook 服务器,并为本章创建一个名为电离层最近邻居的新笔记本。

首先,我们加载代码所需的NumPycsv库:

import numpy as np
import csv

要加载数据集,我们首先获取数据集的文件名。 首先,从数据文件夹中获取存储数据集的文件夹:

data_filename = os.path.join(data_folder, "Ionosphere", "ionosphere.data")

然后,我们创建Xy NumPy 数组来存储数据集。这些数组的大小可以从数据集中得知。 如果您不知道将来的数据集的大小,请不要担心-我们将使用其他方法在以后的章节中加载数据集,并且您无需事先知道此大小:

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

数据集采用逗号分隔值CSV)格式,这是数据集的常用格式。 我们将使用csv模块加载该文件。 导入它并设置一个csv阅读器对象:

with open(data_filename, 'r') as input_file:
    reader = csv.reader(input_file)

接下来,我们遍历文件中的各行。 每行代表一组新的度量,这是此数据集中的一个样本。 我们也使用 enumerate 函数来获取行的索引,因此我们可以更新数据集中的适当样本(X):

    for i, row in enumerate(reader):

我们从该样本中获取前 34 个值,将每个值转换为浮点数,然后将其保存到我们的数据集中:

  data = [float(datum) for datum in row[:-1]]
  X[i] = data

最后,我们获取行的最后一个值并设置类。 如果它是一个好的样本,我们将其设置为 1(或True),否则将其设置为 0:

  y[i] = row[-1] == 'g'

现在,我们在X,中具有示例和特征的数据集,在y中具有相应的类,就像在第 1 章的分类示例中所做的一样。

Loading the dataset

迈向标准工作流程

scikit-learn中的估计器具有两个主要功能:fit()predict()。 我们使用fit方法和我们的训练集来训练算法。 我们在测试集上使用predict方法对其进行评估。

首先,我们需要创建这些训练和测试集。 和以前一样,导入并运行train_test_split函数:

from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=14)

然后,我们导入最近的邻居类并为其创建一个实例。 现在,我们将参数保留为默认值,本章稍后将选择合适的参数。 默认情况下,该算法将选择五个最近的邻居来预测测试样本的类别:

from sklearn.neighbors import KNeighborsClassifierestimator = KNeighborsClassifier()

创建估算器后,我们必须将其拟合到训练数据集中。 对于最近的邻居类,这记录了我们的数据集,从而允许我们通过将新点与训练数据集进行比较来找到新数据点的最近邻居:

estimator.fit(X_train, y_train)

然后,我们使用测试集训练算法,并使用测试集进行评估:

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

准确率达到 86.4%,对于默认算法和仅几行代码而言,这是令人印象深刻的! 显式选择了大多数scikit-learn默认参数,以与一系列数据集配合使用。 但是,您始终应该基于应用实验的知识来选择参数。

运行算法

在我们的早期实验中,我们将数据集的一部分留作测试集,其余作为训练集。 我们在训练集上训练算法,并根据测试集评估算法的有效性。 但是,如果我们幸运并选择一个简单的测试集会发生什么? 或者,如果它特别麻烦怎么办? 由于数据的这种“不幸”分裂而导致的不良结果,我们可以丢弃一个好的模型。

交叉折叠验证框架是解决数据挖掘中选择测试集和标准方法的问题的一种方法。 该过程通过对不同的训练和测试分组进行大量实验来工作,但是每个样本仅在测试集中使用一次。 步骤如下:

  1. 将整个数据集拆分为多个称为折叠的部分。

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

    • 将该折叠设置为当前测试集
    • 在其余折叠上训练算法
    • 评估 当前测试集
  3. 报告所有评估分数,包括平均分数。

  4. 在此过程中,每个样本仅在测试集中使用一次。 这样可以减少(但不能完全消除)选择幸运测试集的可能性。

    注意

    在整个模块中,代码示例在一个章中相互构建。 除非另有说明,否则各章的代码应输入到同一 IPython 笔记本中。

scikit-learn库包含许多交叉折叠验证方法。 给出了执行前面过程的helper功能。 我们现在可以将其导入 IPython Notebook:

from sklearn.cross_validation import cross_val_score

注意

默认为,cross_val_score使用一种称为分层 K 折的特定方法将数据集拆分为折叠。 这样创建的折页在每个折页中的类别比例几乎相同,再次降低了选择不良折页的可能性。 这是一个很好的默认设置,因此我们现在不会对其进行处理。

接下来,我们使用此函数,传递原始(完整)数据集和类:

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

这给出了 82.3%的适度结果,但是考虑到我们还没有尝试设置更好的参数,它还是相当不错的。 在下一节中,我们将看到如何更改参数以获得更好的结果。

设置参数

几乎所有数据挖掘算法都具有用户可以设置的参数。 这通常是使算法通用化的原因,以使其可用于多种情况。 设置这些参数可能非常困难,因为选择良好的参数值通常高度依赖于数据集的特征。

最近邻居算法具有多个参数,但是最重要的一个参数是预测未知属性类别时要使用的最近邻居数量。 在 scikit-learn中,此参数称为n_neighbors。 在下图中,我们显示了当此数字太低时,随机标记的样本可能会导致错误。 相反,当它太高时,实际最近的邻居对结果的影响较小:

Setting parameters

在图(a)的左侧,我们通常希望测试样品(三角形)被分类为圆形。 但是,如果n_neighbors为 1,则该区域中的单个红色菱形(可能是有噪声的样本)会导致该样本被预测为菱形,而它似乎位于红色区域中。 在图(b)中,我们通常希望测试样品被归类为钻石。 但是,如果n_neighbors为 7,则三个最接近的邻居(都是菱形)将被大量的圆形样本覆盖。

如果我们要测试n_neighbors参数的多个值,例如,每个值从 1 到 20,则可以通过设置n_neighbors并观察结果来多次重新运行实验:

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

计算平均值并将其存储在我们的分数列表中。 我们还将存储完整的分数集,以供以后分析:

    avg_scores.append(np.mean(scores))
    all_scores.append(scores)

然后我们可以绘制n_neighbors的值和精度之间的关系。 首先,我们告诉 IPython 笔记本我们要在笔记本本身中内联显示图:

%matplotlib inline

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

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

Setting parameters

尽管有很多方差,但该图显示出随着邻居数量增加而减少的趋势。

Setting parameters

使用管道进行预处理

当进行现实世界物体的测量时,我们通常可以获得范围非常不同的特征。 例如,如果我们正在测量动物的品质,则可能具有以下几个特征:

  • 腿数:对于大多数动物,这是介于 0-8 之间的范围,而有些动物的更多!
  • 重量:这仅在几微克的范围内,一直到重量为 190,000 公斤的蓝鲸!
  • 心数:如果是。,则可以在零到五之间。

对于基于数学的算法来比较每个特征,规模,范围和单位的差异可能难以解释。 如果我们在许多算法中使用上述功能,则权重可能是最有影响力的功能,因为仅是数量较大,而与功能的实际有效性无关。

解决此问题的方法之一是使用称为标准化功能的预处理过程,以使它们具有相同的范围,或被归类为等类别,中型。 突然之间,特征类型的巨大差异对算法的影响较小,并且可能导致准确性的大幅提高。

预处理还可以用于仅选择更有效的功能,创建新功能等。 scikit-learn中的预处理是通过Transformer对象完成的,这些对象以一种形式获取数据集,并在对数据进行某种转换后返回更改后的数据集。 这些不必一定是数字,因为变形金刚也用于提取特征,但是在本节中,我们将坚持预处理。

一个例子

我们可以通过破坏 Ionosphere数据集来显示问题的示例。 尽管这只是一个示例,但许多现实世界的数据集都存在这种形式的问题。 首先,我们创建数组的副本,以便不更改原始数据集:

X_broken = np.array(X)

接下来,我们通过将第二个特征除以10来打破数据集:

X_broken[:,::2] /= 10

从理论上讲,这不会对结果产生太大影响。 毕竟,这些功能的值仍然相对相同。 主要问题是比例尺已更改,奇数特征现在比偶数特征大。 我们可以通过计算精度来看到此效果:

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

原始数据集的得分为 82.3%,在损坏的数据集上下降为 71.5%。 我们可以通过将所有功能缩放到01范围来解决此问题。

标准预处理

我们将为执行的预处理通过MinMaxScaler类称为基于特征的归一化。 继续本章其余部分的 IPython 笔记本,首先,我们导入此类:

from sklearn.preprocessing import MinMaxScaler

此类采用每个功能并将其缩放到01范围。 最小值替换为0,最大值替换为1,而其他值介于两者之间。

要应用预处理器,我们在其上运行 transform 函数。 尽管MinMaxScaler没有,但是某些提升器需要首先以与分类器相同的方式进行训练。 我们可以通过运行fit_transform函数来组合这些步骤:

X_transformed = MinMaxScaler().fit_transform(X)

在此,X_transformed将具有与X相同的形状。 但是,每列的最大值为 1,最小值为 0。

以这种方式进行标准化的其他各种形式,对其他应用和要素类型也有效:

  • 使用sklearn.preprocessing.Normalizer确保每个样本的值总和等于 1
  • 使用sklearn.preprocessing.StandardScaler强制每个特征均值为零且方差为 1,这是标准化的常用起点
  • 使用sklearn.preprocessing.Binarizer将数字特征转换为二进制特征,其中高于阈值的任何值均为 1,低于阈值的任何值为 0

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

全部放在一起

现在,我们可以通过使用先前计算的分解数据集来组合上一部分中的代码来创建工作流程:

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

这使我们获得了 82.3%的准确率。 MinMaxScaler产生的特征具有相同的比例,这意味着没有任何特征会因为仅仅是更大的值而超过其他特征。 尽管可以将“最近邻居”算法与较大的功能混淆,但某些算法可以更好地处理比例差异。 相反,有些情况更糟!

管道

随着实验的发展,操作的复杂性也在增加。 我们可能会拆分数据集,对特征进行二值化,执行基于特征的缩放,执行基于样本的缩放以及更多其他操作。

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

另一个问题是代码的顺序。 在上一节中,我们创建了X_transformed数据集,然后创建了用于交叉验证的新估算器。 如果我们有多个步骤,则需要在代码中跟踪所有对数据集的更改。

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

from sklearn.pipeline import Pipeline

管道将步骤列表作为输入,代表数据挖掘应用的链。 最后一步需要是Estimator,而所有先前的步骤都是Transformers。 每个Transformer都会更改输入数据集,其中一步的输出是下一步的输入。 最后,样本由最后一步的估算器分类。 在我们的管道中,我们有两个步骤:

  1. 使用MinMaxScaler将特征值从 0 缩放到 1
  2. 使用KNeighborsClassifier作为分类算法

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

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

这里的关键是元组列表。 第一个元组是我们的缩放步骤,第二个元组是预测步骤。 我们为每个步骤指定一个名称:第一个我们称为scale,第二个我们称为predict,但是您可以选择自己的名称。 元组的第二部分是实际的 Transformer 或 estimator 对象。

现在,使用之前的交叉验证代码,运行此管道非常容易:

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

由于我们正在有效地执行相同的步骤,因此我们得到的分数与以前相同(82.3%)。

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

Pipelines

Pipelines

Pipelines

Pipelines

二十四、使用决策树预测体育获胜者

在本章中,我们将研究使用不同类型的分类算法:决策树来预测体育比赛的获胜者。 与其他算法相比,这些算法具有许多优势。 主要优点之一是它们可以被人类读取。 这样,决策树可用于学习过程,然后可以将其提供给人工执行(如果需要)。 另一个优点是它们可以使用多种功能,我们将在本章中看到这些功能。

我们将在本章介绍以下主题:

  • 使用 pandas 库加载和处理数据
  • 决策树
  • 随机森林
  • 在数据挖掘中使用真实数据集
  • 创建新功能并在强大的框架中对其进行测试

加载数据集

在本章中,我们将预测国家篮球协会NBA)比赛的获胜者。 NBA 中的比赛通常很接近,可以在最后一刻决定,这很难预测获胜者。 许多运动都有此特征,因此预期冠军可能会在适当的时候被另一支球队击败。

有关预测获胜者的各种研究表明,运动结果预测的准确性可能存在上限,视运动而定,其准确性介于 70%到 80%之间。 通常通过数据挖掘或基于统计的方法,对运动预测进行了大量研究。

收集数据

我们将使用的数据是 2013-2014 赛季 NBA 的比赛历史数据,包含从 NBA 和其他联赛收集的大量资源和统计信息。 要下载数据集,请执行以下步骤:

  1. 在您的网络浏览器中导航到这个页面
  2. 单击常规季节标题旁边的导出按钮。
  3. 将文件下载到您的数据文件夹并记下路径。

这将下载 CSV逗号分隔值的缩写)文件,其中包含 NBA 常规赛季 1,230 场比赛的结果。

CSV 文件是简单的文本文件,其中每行包含一个新行,并且每个值都用逗号分隔(因此而得名)。 只需输入文本编辑器并以.csv扩展名保存,即可手动创建 CSV 文件。 它们也可以在任何可以读取文本文件的程序中打开,但是也可以在 Excel 中作为电子表格打开。

我们将使用 pandasPython 数据分析的缩写)库加载文件,该库对于处理数据非常有用。 Python 还包含一个名为csv的内置库,该库支持读写 CSV 文件。 但是,我们将使用 pandas,它提供了更强大的功能,我们将在本章的后面部分使用它们来创建新功能。

注意

对于本章,您将需要安装 Pandas。 最简单的安装方法是使用pip3,就像在第 1 章 “数据挖掘入门”中所做的那样,安装scikit-learn

$pip3 install pandas

如果您在安装[Pandas]时遇到困难,请访问Pandas 网站 并阅读系统的安装说明。

使用 Pandas 加载数据集

pandas 库是用于加载,管理和处理数据的库。 它处理后台数据结构并支持分析方法,例如计算均值。

在进行多个数据挖掘实验时,您会发现您一次又一次地编写了许多相同的功能,例如读取文件和提取功能。 每次重新实现时,您都有引入错误的风险。 使用诸如 pandas 之类的高级库可以显着减少执行这些功能所需的工作量,还可以使您对使用经过良好测试的代码更有信心。

在整个模块中,我们将大量使用 Pandas,并在此过程中介绍用例。

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

import pandas as pd
dataset = pd.read_csv(data_filename)

结果是 Pandas数据帧,它具有一些有用的功能,稍后我们将使用它们。 查看结果数据集,我们可以看到一些问题。 键入以下内容并运行代码以查看数据集的前五行:

dataset.ix[:5]

这是输出:

Using pandas to load the dataset

这实际上是一个可用的数据集,但是它包含一些问题,我们将尽快解决。

清理数据集

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

  • 日期只是一个字符串,而不是日期对象
  • 第一行是空白
  • 通过目视检查结果,标题不完整或不正确

这些问题来自数据,我们可以通过更改数据本身来解决此问题。 但是,这样做时,我们可能会忘记采取的步骤或错误地使用了这些步骤; 也就是说,我们无法复制结果。 与上一节中使用管道跟踪对数据集所做的转换一样,我们将使用 pandas 将转换应用于原始数据本身。

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

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

结果有了显着改善,因为我们可以看到是否打印出了结果数据帧:

dataset.ix[:5]

输出如下:

Cleaning up the dataset

即使在诸如此类的经过良好编译的数据源中,您也需要进行一些调整。 不同的系统具有不同的细微差别,导致数据文件彼此之间不太兼容。

现在我们有了数据集,我们可以计算基线了。 基线是一种准确度,表示获得良好准确度的简便方法。 任何数据挖掘解决方案都应该胜过这一点。

在每场比赛中,我们都有两支球队:主队和客队。 一个明显的基线(称为机会率)为 50%。 (随时间推移)随机选择将导致 50%的准确性。

提取新功能

现在,我们可以通过合并和比较现有数据从此数据集中提取特征。 首先,我们需要指定我们的类值,这将为我们的分类算法提供一些参考,以比较其预测是否正确。 这可以用多种方式进行编码。 但是,对于此应用,如果主队获胜,我们将类别指定为 1,如果访问者队获胜,则将类别指定为 0。 在篮球比赛中,得分最高的球队获胜。 因此,尽管数据集未指定谁获胜,但我们可以轻松地进行计算。

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

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

然后,我们将这些值复制到NumPy数组中,以供以后用于scikit-learn分类器。 Pandas 和 scikit-learn 之间目前尚无干净的集成,但是它们可以通过使用NumPy数组很好地协同工作。 虽然我们将使用 Pandas 提取特征,但我们需要提取值以将其与 scikit-learn 结合使用:

y_true = dataset["HomeWin"].values

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

我们还可以开始创建一些可用于数据挖掘的功能。 尽管有时我们只是将原始数据扔进分类器中,但我们经常需要推导连续的数值或分类特征。

我们要创建的前两个功能可以帮助我们预测哪支球队会获胜,这就是这两支球队中的哪支球队是否赢得了最后一场比赛。 这大致可以估算出哪支球队表现出色。

我们将通过依次遍历各行并记录获胜团队来计算此功能。 当我们进入新的一行时,我们将检查团队是否在上次看到他们时赢了。

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

from collections import defaultdict 
won_last = defaultdict(int)

这本词典的关键是团队,价值在于他们是否赢得了上一场比赛。 然后,我们可以遍历所有行,并使用团队的最后结果更新当前行:

for index, row in dataset.iterrows():
    home_team = row["Home Team"]
    visitor_team = row["Visitor Team"]
    row["HomeLastWin"] = won_last[home_team]
    row["VisitorLastWin"] = won_last[visitor_team]
    dataset.ix[index] = row

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

然后,在下次看到这些团队时,使用每个团队的结果(在此行中)设置字典。 代码如下:

    won_last[home_team] = row["HomeWin"]
    won_last[visitor_team] = not row["HomeWin"]

在前面的代码运行之后,我们将具有两个新功能:HomeLastWinVisitorLastWin。 我们可以看一下数据集。 不过,看一下前五款游戏并没有多大意义。 由于我们的代码运行方式,当时我们没有用于它们的数据。 因此,直到一支球队第二个赛季,我们才知道他们目前的状态。 我们可以改为查看列表中的其他位置。 以下代码将显示本赛季第 20 到 25 场比赛:

dataset.ix[20:25]

这是输出:

Extracting new features

您可以更改这些索引以查看数据的其他部分,因为我们的数据集中有 1000 多个游戏!

当前,这给所有团队(包括前一年的冠军!)初次见到的时候都是虚假的价值。 我们可以使用上一年的数据来改进此功能,但本章将不做。

决策树

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

Decision trees

与大多数分类算法一样,有两个组件:

  • 第一个是训练阶段,其中使用训练数据构建一棵树。 虽然上一章中最接近的邻居算法没有训练阶段,但决策树需要它。 这样,最近邻居算法是一个懒惰的学习者,仅在需要进行预测时才做任何工作。 相反,决策树像大多数分类方法一样,都是渴望学习的人,在训练阶段就开始工作。
  • 第二个是预测阶段,其中训练有素的树用于预测新样本的分类。 使用前面的示例树,数据点["is raining", "very windy"]将被分类为"bad weather"

创建决策树的算法很多。 这些算法很多都是迭代的。 它们从基础节点开始,并确定要用于第一个决策的最佳功能,然后转到每个节点并选择下一个最佳功能,依此类推。 当确定无法进一步扩展树时,该过程将在某个点停止。

scikit-learn程序包将 CART分类和回归树)算法实现为其默认决策树类,该算法可以同时使用分类和连续功能。

决策树中的参数

决策树的最重要的特征之一就是停止标准。 当构建一棵树时,最后的几个决策通常在某种程度上是任意的,并且仅依靠少量样本做出决策。 使用这样的特定节点可能会导致树大大超出训练数据。 相反,可以使用停止标准来确保决策树未达到此准确性。

除了使用停止条件外,还可以完整地创建树,然后进行修剪。 此修整过程将删除不会为整个过程提供太多信息的节点。 这被称为修剪。

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

  • min_samples_split:这指定需要多少样本才能在决策树中创建一个新节点
  • min_samples_leaf:这指定从节点留下的样本必须为多少

第一个指示是否将创建决策节点,而第二个指示是否将保留决策节点。

决策树的另一个参数是创建决策的标准。 基尼杂质和信息增益是两个流行的:

  • 基尼杂质:这是衡量决策节点错误地预测样本类别的频率的方法
  • 信息增益:这使用基于信息论的熵来指示决策节点获得了多少额外信息

使用决策树

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

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

提示

我们再次将 14 用于random_state,并将在大多数模块中使用。 使用相同的随机种子可以复制实验。 但是,在进行实验时,应混合使用随机状态,以确保算法的性能不受特定值的限制。

现在,我们需要从我们的 Pandas 数据框中提取数据集,以便将其与scikit-learn分类器一起使用。 为此,我们指定希望使用的列,并使用数据框视图的values参数。 以下代码使用我们的主队和客队的最后获胜值创建一个数据集:

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

决策树是估计器,如在第 2 章,“使用 scikit-learn 估计器进行分类”中,因此具有fitpredict方法。 我们还可以使用cross_val_score方法来获得平均分数(就像我们之前所做的那样):

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

得分 56.1%:我们比随机选择更好! 我们应该能够做得更好。 特征工程是数据挖掘中最困难的任务之一,选择好的特征是获得好的结果的关键-比选择正确的算法还重要!

Using decision trees

运动成绩预测

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

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

  • 一般认为哪个小组更好?
  • 哪支球队赢得了最后一场比赛?

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

全部放在一起

对于第一个功能,我们将创建一个功能来告诉我们主队通常是否比访问者更好。 为此,我们将在上个赛季从 NBA 获得排名(在某些运动中也称为阶梯)。 如果一个团队在 2013 年排名高于其他团队,就会被认为会更好。

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

  1. 在您的网络浏览器中导航到这个页面
  2. 选择扩展排名以获取整个联赛的一个列表。
  3. 单击导出链接。
  4. 将下载的文件保存在您的数据文件夹中。

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

standings_filename = os.path.join(data_folder, "leagues_NBA_2013_standings_expanded-standings.csv")
standings = pd.read_csv(standings_filename, skiprows=[0,1])

您可以通过在新单元格中键入standings并运行代码来查看梯形图:

Standings

输出如下:

Putting it all together

接下来,我们使用与先前功能相似的模式创建一个新功能。 我们遍历行,查找主队和客队的排名。 代码如下:

dataset["HomeTeamRanksHigher"] = 0
for index, row in dataset.iterrows():
    home_team = row["Home Team"]
    visitor_team = row["Visitor Team"]

为了对数据进行重要调整,在 2013 和 2014 赛季之间改名了一支球队(但仍然是同一支球队)。 这是尝试集成数据时可能发生的许多不同事情之一的示例! 我们将需要调整团队查找,以确保获得正确的团队排名:

    if home_team == "New Orleans Pelicans":
        home_team = "New Orleans Hornets"
    elif visitor_team == "New Orleans Pelicans":
        visitor_team = "New Orleans Hornets"

现在我们可以获取每个团队的排名。 然后,我们将它们进行比较并更新该行中的功能:

  home_rank = standings[standings["Team"] == home_team]["Rk"].values[0]
        visitor_rank = standings[standings["Team"] == visitor_team]["Rk"].values[0]
        row["HomeTeamRanksHigher"] = int(home_rank > visitor_rank)
        dataset.ix[index] = row

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

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

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

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

现在,该分数为 60.3%,甚至比我们之前的结果还要好。 我们可以做得更好吗?

接下来,让我们测试两支球队中的哪支赢得了最后一场比赛。 虽然排名可以暗示谁是赢家(排名较高的球队更有可能获胜),但有时球队与其他球队的比赛表现更好。 造成这种情况的原因很多,例如,某些团队可能制定了与其他团队非常有效的策略。 按照我们以前的模式,我们创建了一个字典来存储过去游戏的获胜者,并在数据框中创建一个新功能。 代码如下:

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

然后,我们遍历每一行并获得主队和访客队:

for index, row in dataset.iterrows():
    home_team = row["Home Team"]
    visitor_team = row["Visitor Team"]

我们想看看谁赢得了这两支球队之间的最后一场比赛,而与哪支球队在家中比赛无关。 因此,我们按字母顺序对团队名称进行排序,从而为这两个团队提供一致的密钥:

      teams = tuple(sorted([home_team, visitor_team]))

我们在字典中查询,看谁赢得了两支队伍之间的最后一场比赛。 然后,我们更新数据集数据框中的行:

      row["HomeTeamWonLast"] = 1 if last_match_winner[teams] == row["Home Team"] else 0
        dataset.ix[index] = row

最后,我们用该游戏的赢家更新了我们的词典,以便计算这两个团队下次见面时的功能:

    winner = row["Home Team"] if row["HomeWin"] else row["Visitor Team"]
    last_match_winner[teams] = winner

接下来,我们将仅创建具有两个功能的数据集。 您可以尝试不同的功能组合,以查看它们是否获得不同的结果。 代码如下:

X_lastwinner =  dataset[["HomeTeamRanksHigher", "HomeTeamWonLast"]].values
clf = DecisionTreeClassifier(random_state=14)
scores = cross_val_score(clf, X_lastwinner, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))

这占 60.6%。 我们的结果越来越好。

最后,我们将检查如果在决策树上投入大量数据会发生什么,并查看它是否仍然可以学习有效的模型。 我们将使团队进入树状结构,并检查决策树是否可以学习合并这些信息。

尽管决策树能够从分类特征中学习,但是 scikit-learn 中的实现要求首先对那些特征进行编码。 我们可以使用LabelEncoder转换器在基于字符串的团队名称之间转换为整数。 代码如下:

from sklearn.preprocessing import LabelEncoder
encoding = LabelEncoder()

我们将把此转换器安装到主队中,以便为每个队学习一个整数表示形式:

encoding.fit(dataset["Home Team"].values)

我们提取主队和访客队的所有标签,然后将它们加入(在NumPy中称为堆叠),以创建一个矩阵,对每个主队和访客队进行编码 游戏。 代码如下:

home_teams = encoding.transform(dataset["Home Team"].values)
visitor_teams = encoding.transform(dataset["Visitor Team"].values)
X_teams = np.vstack([home_teams, visitor_teams]).T

这些整数可以输入到决策树中,但是DecisionTreeClassifier仍会将它们解释为连续特征。 例如,可以为团队分配 0 到 16 的整数。算法将使团队 1 和 2 相似,而团队 4 和 10 则不同-但这对所有人都没有意义。 所有团队都互不相同-两个团队要么相同,要么不一样!

要解决此不一致的问题,我们使用OneHotEncoder转换器将这些整数编码为许多二进制特征。 每个二进制特征将是该特征的单个值。 例如,如果LabelEncoder将 NBA 芝加哥公牛队分配为整数 7,则如果该团队是芝加哥公牛队,则OneHotEncoder返回的第七个特征将为 1,而所有球队均为 0 其他团队。 针对每个可能的值执行此操作,从而导致更大的数据集。 代码如下:

from sklearn.preprocessing import OneHotEncoder
onehot = OneHotEncoder()

我们对同一个数据集进行拟合和变换,保存结果:

X_teams_expanded = onehot.fit_transform(X_teams).todense()

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

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

得分准确性为 60%。 分数比基线好,但不及以前。 决策树可能无法正确处理大量特征。 因此,我们将尝试更改算法,看看是否有帮助。 数据挖掘可以是尝试新算法和功能的迭代过程。

随机森林

单个决策树可以学习非常复杂的功能。 但是,在许多方面,它很容易过拟合-仅适用于训练集的学习规则。 我们可以对此进行调整的方法之一是限制它学习的规则数量。 例如,我们可以将树的深度限制为仅三层。 这样的树将学习在全局范围内分割数据集的最佳规则,但不会学习将数据集分为高度精确的组的高度特定的规则。 这种权衡导致树可能具有良好的泛化性,但总体性能稍差。

为了弥补这一点,我们可以创建许多决策树,然后要求每个决策树预测类值。 我们可以进行多数表决,并将该答案用作我们的总体预测。 随机森林按照这一原则开展工作。

上述过程存在两个问题。 第一个问题是,构建决策树在很大程度上是确定性的-使用相同的输入将每次产生相同的输出。 我们只有一个训练数据集,这意味着如果我们尝试构建多棵树,我们的输入(以及输出)将是相同的。 我们可以通过选择数据集的随机子样本来解决此问题,从而有效地创建new训练集。 该处理称为套袋

的第二个问题是,用于树中前几个决策节点的功能将非常好。 即使我们选择训练数据的随机子样本,建立的决策树在很大程度上仍将是相同的。 为了弥补这一点,我们还选择了特征的随机子集来执行数据拆分。

然后,我们使用(几乎)随机选择的特征,使用随机选择的样本随机构建了树。 这是一个随机森林,也许是而不是,该算法在许多数据集上非常有效。

集成如何工作?

随机森林中固有的随机性可能使我们似乎将算法的结果留给了机会。 但是,我们将平均的好处应用于几乎随机建立的决策树,从而产生了一种可以减少结果差异的算法。

方差是算法中训练数据集变化引起的误差。 训练数据集的变化会极大地影响方差较大的算法(例如决策树)。 这导致模型存在过度拟合的问题。

注意

与相反,偏差是算法中的假设所引入的误差,而不是与数据集有关的误差,也就是说,如果我们有一个算法假定所有特征都是正态分布的,那么我们 如果功能不具备,则算法可能会有很高的错误。 通过分析数据以查看分类器的数据模型是否与实际数据相匹配,可以减少偏差带来的负面影响。

通过平均大量决策树,可以大大减少这种差异。 这导致模型具有更高的整体精度。

总的来说,合奏是基于这样的假设,即预测中的误差实际上是随机的,并且各个分类器之间的误差完全不同。 通过对许多模型的结果求平均值,可以消除这些随机误差,从而留下真实的预测结果。 在本书的其余部分中,我们将看到更多的合奏。

随机森林中的参数

scikit-learn 中的随机森林实现称为RandomForestClassifier,它具有许多参数。 由于随机森林使用的许多实例,因此它们共享许多相同的参数,例如标准(基尼杂质或熵/信息增益),max_featuresmin_samples_split

此外,合奏过程中使用了一些新参数:

  • n_estimators:这指示应构建多少个决策树。 较高的值将花费更长的时间运行,但(可能)会导致较高的精度。
  • oob_score:如果为 true,则使用不在用于训练决策树的随机子样本中的样本对方法进行测试。
  • n_jobs:此指定在并行训练决策树时要使用的核心数。

scikit-learn程序包使用名为Joblib的库进行内置并行化。 此参数决定要使用多少个内核。 默认情况下,仅使用单个核心-如果您具有更多核心,则可以增加该核心,或将其设置为-1 以使用所有核心。

应用随机森林

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

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

通过交换分类器,可以立即带来 60.6%的收益,提高了 0.6 个百分点。

使用特征子集的随机森林应该能够比正常决策树更有效地学习更多特征。 我们可以通过在算法上添加更多功能并查看其运行方式来进行测试:

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

结果是 61.1%,甚至更好! 我们还可以使用类尝试其他一些参数,如我们在第 2 章,“使用 scikit-learn 估计器进行分类”中介绍的那样:

parameter_space = {
  "max_features": [2, 10, 'auto'],
  "n_estimators": [100,],
  "criterion": ["gini", "entropy"],
  "min_samples_leaf": [2, 4, 6],
}
clf = RandomForestClassifier(random_state=14)
grid = GridSearchCV(clf, parameter_space)
grid.fit(X_all, y_true)
print("Accuracy: {0:.1f}%".format(grid.best_score_ * 100))

准确率高达 64.2%!

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

print(grid.best_estimator_)

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

RandomForestClassifier(bootstrap=True, compute_importances=None,criterion='entropy', max_depth=None, max_features=2,max_leaf_nodes=None, min_density=None, min_samples_leaf=6,min_samples_split=2, n_estimators=100, n_jobs=1,oob_score=False, random_state=14, verbose=0)

工程新功能

在的前面几个示例中,我们看到更改功能可能会对算法的性能产生很大影响。 通过少量的测试,我们仅在功能上就有超过 10%的差异。

您可以通过执行以下操作来创建来自 Pandas 中的简单功能的功能:

dataset["New Feature"] = feature_creator()

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

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

您可以通过将所有值设置为单个“默认”值来更直接地创建这些功能,例如下一行中的 0:

dataset["My New Feature"] = 0

然后,您可以遍历数据集,随时进行特征计算。 在本章中,我们使用了这种格式来创建许多功能:

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

请记住,这种模式不是很有效。 如果要执行此操作,请立即尝试所有功能。 常见的“最佳实践”是尽可能少地触摸每个样本,最好只接触一次。

您可以尝试实现的一些示例功能如下:

  • 自每支球队上一场比赛以来已有多少天? 如果团队在短时间内玩太多游戏,可能会感到疲倦。
  • 每支球队在最近五场比赛中赢了几场? 这将为我们较早提取的HomeLastWinVisitorLastWin特征提供更稳定的形式(并且可以非常相似的方式提取)。
  • 拜访某些其他团队时,团队是否有良好记录? 例如,即使是访客,一支球队也可能在特定的体育场打得很好。

如果在提取这些类型的功能时遇到麻烦,请查看这个页面上的 Pandas 文档。 或者,您可以尝试使用在线论坛(例如 Stack Overflow)寻求帮助。

更极端的例子可能是使用球员数据来估算每支球队的实力来预测谁获胜。 赌徒和体育博彩公司每天都使用这些类型的复杂功能,通过预测体育比赛的结果来尝试获利。

Engineering new features

Engineering new features

Engineering new features

Engineering new features

二十五、使用亲和力分析推荐电影

在本章中,我们将研究确定对象何时频繁出现的亲和力分析。 在确定何时一起购买物品的用例之一之后,通俗地称为“市场篮子分析”。

在第 3 章,“用决策树”预测运动优胜者中,我们将一个对象视为焦点,并使用特征来描述该对象。 在本章中,数据具有不同的形式。 我们有一些交易,其中以某种方式在这些交易中使用了感兴趣的对象(本章中的电影)。 目的是发现对象同时发生的时间。 在此示例中,我们希望确定何时由同一位审阅者推荐两部电影。

本章的关键概念如下:

  • 亲和力分析
  • 使用 Apriori 算法进行特征关联挖掘
  • 电影推荐
  • 稀疏数据格式

亲和力分析

亲和力分析是确定何时以类似方式使用对象的任务。 在上一章中,我们集中于对象本身是否相似。 用于亲和力分析的数据通常以事务的形式描述。 直观地讲,这来自商店的交易-确定何时一起购买物品。

但是,它可以应用于许多过程:

  • 欺诈识别
  • 客户细分
  • 软件优化
  • 产品推荐

亲和力分析通常比分类更具探索性。 我们常常没有许多分类任务所需的完整数据集。 例如,在电影推荐中,我们对不同电影的评论来自不同的人。 但是,不太可能让每个审阅者审阅数据集中的所有电影。 这在亲和力分析中留下了一个重要而困难的问题。 如果评论者没有评论过电影,是否表明他们对电影不感兴趣(因此不推荐),或者仅仅是他们还没有评论过?

我们不会在本章中回答该问题,但是考虑数据集中的空白可能会导致类似的问题。 反过来,这可能会导致可能有助于提高方法效率的答案。

亲和力分析算法

我们在第 1 章,“数据挖掘入门”中引入了一种用于亲和力分析的基本方法,该方法测试了所有可能的规则组合。 我们计算了每个规则的置信度和支持度,从而使我们能够对它们进行排名,以找到最佳规则。

但是,这种方法效率不高。 我们在第 1 章,“数据挖掘入门”中的数据集仅售出 5 件。 我们可以预期,即使是一家小商店,也会有数百种商品待售,而许多在线商店将拥有成千上万(甚至数百万!)。 通过天真​​的规则创建(例如我们之前的算法),计算这些规则所需的时间增长呈指数增长。 随着我们添加更多项目,计算所有规则所需的时间显着增加。 具体而言,规则的总数为 2n-1。 对于我们的五项数据集,有 31 条可能的规则。 对于 10 个项目,它是 1023。对于仅 100 个项目,该数字具有 30 位数字。 甚至计算能力的急剧增加也无法跟上在线存储物品数量的增加。 因此,我们需要更智能的算法,而不是更辛苦的计算机。

用于亲和力分析的经典算法称为 Apriori 算法。 它解决了创建在数据库中频繁出现的项目集(称为频繁项目集)的指数问题。 一旦发现了这些频繁的项目集,就很容易创建关联规则。

Apriori 的直觉既简单又聪明。 首先,我们确保规则在数据集中具有足够的支持。 定义最低支持级别是 Apriori 的关键参数。 若要建立一个频繁的项目集,要使一个项目集(A,B)的支持至少达到 30,A 和 B 在数据库中都必须发生至少 30 次。 此属性也扩展到更大的集合。 对于被认为是频繁的项目集(A,B,C,D),集合(A,B,C)也必须是频繁的(与 D 一样)。

可以构建这些频繁项目集,并且不会测试不频繁(可能有很多)的可能项目集。 这节省了测试新规则的大量时间。

用于亲和力分析的其他示例算法包括 EclatFP-growth 算法。 数据挖掘文献中对这些算法进行了许多改进,进一步提高了该方法的效率。 在本章中,我们将重点介绍基本的 Apriori 算法。

选择参数

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

对于第一阶段,Apriori 算法需要一个值,该值用于将项目集视为频繁的最小支持。 任何支持较少的项目集将不予考虑。 将此最低支持设置得太低将导致 Apriori 测试大量项目集,从而减慢算法速度。 设置得太高将导致较少的项目集被认为是频繁的。

在第二阶段,在发现频繁项集之后,将根据其置信度对关联规则进行测试。 我们可以选择最低置信度,可以选择返回多个规则,也可以简单地返回所有规则,然后让用户决定如何处理它们。

在本章中,我们将仅返回给定置信度以上的规则。 因此,我们需要设置最低置信度。 将此值设置得太低将导致规则具有较高的支持度,但并不十分准确。 将此值设置得更高将导致仅返回更准确的规则,但发现的规则却更少。

电影推荐问题

产品推荐是一项大生意。 在线商店通过推荐其他客户可以购买的产品,将其用于向客户进行向上销售。 提出更好的建议可以带来更好的销售。 当在线购物每年销售给数百万个客户时,向这些客户销售更多商品会产生很多潜在的收益。

产品推荐已经研究了很多年。 但是,当 Netflix 在 2007 年至 2009 年间获得 Netflix 奖时,该领域获得了巨大的推动。该竞赛旨在确定是否有人能够预测用户对电影的评级要好于 Netflix 当前的表现。 奖杯比目前的解决方案高出 10%以上。 尽管这似乎不是很大的改进,但这种改进可以通过更好的电影推荐为 Netflix 带来数百万美元的收入。

获取数据集

自从成立 Netflix 奖以来,明尼苏达大学的研究小组 Grouplens 已发布了多个数据集,这些数据集通常用于测试该领域的算法。 他们发布了电影分级数据集的多个版本,大小各异。 有一个具有 100,000 条评论的版本,一个具有 100 万条评论,一个具有 1000 万条评论。

数据集可从这个页面获得,我们将在本章中使用的数据集是 MovieLens 100 万数据集。 下载此数据集并将其解压缩到您的数据文件夹中。 启动一个新的 IPython Notebook 并输入以下代码:

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

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

加载 Pandas

MovieLens 数据集状态良好; 但是,我们需要对pandas.read_csv中的默认选项进行一些更改。 首先,数据由制表符(而不是逗号)分隔。 接下来,没有标题行。 这意味着文件中的第一行实际上是数据,我们需要手动设置列名。

加载文件时,我们将 delimiter 参数设置为制表符,告诉 Pandas 不要读取第一行作为标题(带有header=None),并设置列名。 让我们看下面的代码:

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

尽管我们将在本章中不使用它,但是您可以使用以下行正确地解析日期时间戳:

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

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

all_ratings[:5]

结果将看起来像这样:

|   |

用户身份

|

电影 ID

|

评分

|

约会时间

| | --- | --- | --- | --- | --- | |0| 196 | 242 | 3 | 1997-12-04 15:55:49 | |1| 186 | 302 | 3 | 1998-04-04 19:22:22 | |2| 22 | 377 | 1 | 1997-11-07 07:18:36 | |3| 244 | 51 | 2 | 1997-11-27 05:02:03 | |4| 166 | 346 | 1 | 1998-02-02 05:33:16 |

稀疏数据格式

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

此数据集中有 1,000 个用户和 1,700 部电影,这意味着完整的矩阵将非常大。 我们可能会遇到将整个矩阵存储在内存中并在其上进行计算的麻烦。 但是,此矩阵具有大多数单元为空的属性,也就是说,大多数用户对大多数电影都没有评论。 但是,对于用户#213,没有针对电影#675的评论,对于用户和电影的大多数其他组合,则没有评论。

此处给出的格式代表完整的矩阵,但格式更紧凑。 第一行表示用户#196观看了电影#242,在 1997 年 12 月 4 日,该电影的排名为 3(五分之三)。

假定该数据库中没有用户和电影的任何组合,则不存在。 与在内存中存储一​​堆零相反,此节省了大量空间。 这种格式称为稀疏矩阵格式。 根据经验,如果您希望约 60%或更多的数据集为空或为零,则稀疏格式将占用较少的存储空间。

在稀疏矩阵上进行计算时,通常不将重点放在我们没有的数据上,而是比较所有零。 我们通常关注于我们拥有的数据并进行比较。

Sparse data formats

Apriori 实现

本章的目标旨在产生以下格式的规则:如果有人推荐这些电影,他们也会推荐该电影。 我们还将讨论如果有人推荐一组电影可能会推荐另一部特定电影的扩展。

为此,我们首先需要确定一个人是否推荐电影。 为此,我们可以创建一个新功能Favorable,如果该人对电影给予了好评,则该功能为True

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

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

all_ratings[10:15]
|   |

用户身份

|

电影 ID

|

评分

|

约会时间

|

有利

| | --- | --- | --- | --- | --- | --- | |10| 62 | 257 | 2 | 1997-11-12 22:07:14 | 错误的 | |11| 286 | 1014 | 5 | 1997-11-17 15:38:45 | 真的 | |12| 200 | 222 | 5 | 1997-10-05 09:05:40 | 真的 | |13| 210 | 40 | 3 | 1998-03-27 21:59:54 | 错误的 | |14| 224 | 29 | 3 | 1998-02-21 23:40:57 | 错误的 |

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

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

接下来,我们可以创建样本中仅包含好评的数据集:

favorable_ratings = ratings[ratings["Favorable"]]

我们将搜索用户对我们的商品集的好评。 因此,接下来我们需要的是每个用户都喜欢的电影。 我们可以通过按用户 ID 对数据集进行分组并遍历每个组中的电影来进行计算:

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

在前面的代码中,我们将值存储为frozenset,从而使我们能够快速检查电影是否已被用户评级。 对于这种类型的操作,集合比列表快得多,我们将在以后的代码中使用它们。

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

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

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

num_favorable_by_movie.sort("Favorable", ascending=False)[:5]

让我们看一下前五部电影列表:

|

电影 ID

|

有利

| | --- | --- | | 50 | 100 | | 100 | 89 | | 258 | 83 | | 181 | 79 | | 174 | 74 |

Apriori 算法

Apriori 算法是我们的​​亲和力分析的一部分,专门用于查找数据中的频繁项集。 Apriori 的基本过程是从以前发现的频繁项目集构建新的候选项目集。 对这些候选项进行测试以查看它们是否频繁出现,然后算法按以下说明进行迭代:

  1. 通过将每个项目放置在其自己的项目集中来创建初始频繁项目集。 在此步骤中,仅使用支持最少的项目。
  2. 通过查找现有频繁项目集的超集,从最近发现的频繁项目集创建新的候选项目集。
  3. 测试所有候选项目集以查看它们是否频繁。 如果候选人不频繁,则将其丢弃。 如果此步骤中没有新的频繁项目集,请转到最后一步。
  4. 存储新发现的频繁项目集,然后转到第二步。
  5. 返回所有发现的频繁项目集。

以下工作流概述了此过程:

The Apriori algorithm

实施

在 Apriori 的第一次迭代中,新发现的项目集的长度为 2,因为它们是第一步中创建的初始项目集的超集。 在第二次迭代中(应用第四步之后),新发现的项目集的长度为 3。这使我们能够根据第二步的需要快速识别新发现的项目集。

我们可以将发现的频繁项集存储在字典中,其中的关键是项集的长度。 这使我们能够在以下代码的帮助下快速访问给定长度的项目集,从而可以访问最近发现的频繁项目集:

frequent_itemsets = {}

我们还需要定义一个项目集被视为频繁的最低支持。 该值是根据数据集选择的,但是可以尝试使用其他值。 不过,我建议一次只将其更改 10%,因为算法运行所需的时间会大大不同! 让我们应用最低限度的支持:

min_support = 50

为了实现 Apriori 算法的第一步,我们分别为每个电影创建一个项目集,并测试该项目集是否频繁。 我们使用frozenset,因为它们允许我们稍后执行设置操作,它们也可以用作计数字典中的键(常规设置不能)。 让我们看下面的代码:

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

为了实现效率,我们共同创建了第二和第三步,方法是创建一个函数,该函数采用新发现的频繁项集,创建超集,然后测试它们是否频繁。 首先,我们设置函数和计数字典:

from collections import defaultdict
def find_frequent_itemsets(favorable_reviews_by_users, k_1_itemsets, min_support):
    counts = defaultdict(int)

为了尽可能少地读取数据,我们每次调用此函数都会对数据集进行一次迭代。 尽管在此实现中这无关紧要(我们的数据集相对较小),但是对于大型应用来说,这是一个好习惯。 我们遍历所有用户及其评论:

    for user, reviews in favorable_reviews_by_users.items():

接下来,我们遍历每个先前发现的项目集,并查看它是否是当前评论集的子集。 如果是,则表示用户已查看项目集中的每个电影。 让我们看一下代码:

        for itemset in k_1_itemsets:
            if itemset.issubset(reviews):

然后,我们可以浏览用户查看过的每个未在项目集中的单个电影,从中创建一个超集,并在计数字典中记录我们看到的该特定项目集。 让我们看一下代码:

                for other_reviewed_movie in reviews - itemset:
                    current_superset = itemset | frozenset((other_reviewed_movie,))
                    counts[current_superset] += 1

我们通过测试哪些候选项目集有足够的支持被认为是频繁的来结束我们的功能,并仅返回那些:

    return dict([(itemset, frequency) for itemset, frequency in counts.items() if frequency >= min_support])

为了运行我们的代码,我们现在创建一个循环,循环遍历 Apriori 算法的各个步骤,并在进行过程中存储新的项目集。 在此循环中,k表示即将被发现的频繁项集的长度,从而使我们可以通过使用键 k 在frequent_itemsets字典中查找来访问先前发现最多的项- 1。 我们创建常用项目集,并将它们按其长度存储在我们的字典中。 让我们看一下代码:

for k in range(2, 20):
    cur_frequent_itemsets = find_frequent_itemsets(favorable_reviews_by_users, frequent_itemsets[k-1],
          min_support)
     frequent_itemsets[k] = cur_frequent_itemsets

如果我们没有找到任何新的频繁项目集,我们想打破前面的循环(并打印一条消息,让我们知道发生了什么事情):

    if len(cur_frequent_itemsets) == 0:
        print("Did not find any frequent itemsets of length {}".format(k))
        sys.stdout.flush()
        break

注意

我们使用sys.stdout.flush()来确保在代码仍在运行时进行打印输出。 有时,在特定单元格中的大型循环中,直到代码完成后才进行打印输出。 以这种方式刷新输出可确保在需要时进行打印输出。 不过,不要做太多—刷新操作会带来计算成本(与打印一样),这会降低程序速度。

如果我们确实找到了频繁的项目集,我们将打印一条消息,让我们知道循环将再次运行。 该算法可能需要一段时间才能运行,因此在等待代码完成时知道代码仍在运行非常有帮助! 让我们看一下代码:

    else:
        print("I found {} frequent itemsets of length {}".format(len(cur_frequent_itemsets), k))
        sys.stdout.flush()

最终,在循环结束之后,我们不再对第一组项目集感兴趣-这些项目集的长度为 1,这对我们创建关联规则没有帮助-我们至少需要两个项目来创建关联规则。 让我们删除它们:

del frequent_itemsets[1]

您现在可以运行此代码。 这可能需要几分钟,如果您使用的是较旧的硬件,则可能需要更多时间。 如果发现在运行任何代码示例时遇到问题,请查看使用在线云提供商以提高速度。 有关使用云进行工作的详细信息,请参见第 13 章,“后续步骤”。

前面的代码返回了 1,718 个不同长度的频繁项集。 您会注意到,项目集的数量在长度减小之前就随着长度的增加而增加。 由于可能的规则越来越多,所以它不断增长。 一段时间后,大量的组合不再具有被认为是频繁出现的必要支持。 这导致数量减少。 这种缩小是 Apriori 算法的好处。 如果我们搜索所有可能的项目集(而不仅仅是频繁项目集的超集),那么我们将搜索成千上万次的项目集,以查看它们是否频繁。

提取关联规则

在 Apriori 算法完成后,我们将获得一个频繁项集列表。 这些并不完全是关联规则,但与之相似。 频繁项集是一组具有最小支持的项,而关联规则具有前提和结论。

我们可以通过将项目集中的一部电影作为结论来从频繁项目集中制定关联规则。 项目集中的其他电影将作为前提。 这将形成以下形式的规则:如果审阅者推荐前提条件中的所有电影,他们还将推荐结论

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

在代码中,我们首先遍历每个发现的每个长度的频繁项集,从每个频繁项集生成所有规则的列表:

candidate_rules = []
for itemset_length, itemset_counts in frequent_itemsets.items():
    for itemset in itemset_counts.keys():

然后,我们迭代此项目集中的每个电影,并以此作为结论。 项目集中的其余电影是前提。 我们将前提和结论保存为我们的候选规则:

        for conclusion in itemset:
            premise = itemset - set((conclusion,))
            candidate_rules.append((premise, conclusion))

这将返回大量候选规则。 通过打印出列表中的前几个规则,我们可以看到一些内容:

print(candidate_rules[:5])

结果输出显示获得的规则:

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

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

接下来,我们计算每个规则的置信度。 此操作非常类似于第 1 章,“数据挖掘入门”,唯一的更改是使用新数据格式进行计算所需的更改。

该过程从创建字典开始,以存储我们看到得出结论的前提的次数(规则的正确示例)和不存在的次数(错误) 例子)。 让我们看一下代码:

correct_counts = defaultdict(int)
incorrect_counts = defaultdict(int)

我们遍历所有用户,他们的好评以及每条候选关联规则:

for user, reviews in favorable_reviews_by_users.items():
    for candidate_rule in candidate_rules:
        premise, conclusion = candidate_rule

然后,我们测试一下前提是否适用于该用户。 换句话说,用户是否在前提条件下浏览了所有电影? 让我们看一下代码:

        if premise.issubset(reviews):

如果前提适用,我们将看看结论电影是否也获得了好评。 如果是这样,则该规则在这种情况下是正确的。 如果不是,则不正确。 让我们看一下代码:

        if premise.issubset(reviews):
            if conclusion in reviews:
                correct_counts[candidate_rule] += 1
            else:
                incorrect_counts[candidate_rule] += 1

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

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

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

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

结果如下:

Rule #1
Rule: If a person recommends frozenset({64, 56, 98, 50, 7}) they will also recommend 174
 - Confidence: 1.000

Rule #2
Rule: If a person recommends frozenset({98, 100, 172, 79, 50, 56}) they will also recommend 7
 - Confidence: 1.000

Rule #3
Rule: If a person recommends frozenset({98, 172, 181, 174, 7}) they will also recommend 50
 - Confidence: 1.000

Rule #4
Rule: If a person recommends frozenset({64, 98, 100, 7, 172, 50}) they will also recommend 174
 - Confidence: 1.000

Rule #5
Rule: If a person recommends frozenset({64, 1, 7, 172, 79, 50}) they will also recommend 181
 - Confidence: 1.000

产生的打印输出仅显示电影 ID,如果没有电影名称也没有太大帮助。 数据集附带一个名为u.items的文件,该文件存储电影名称及其对应的MovieID(以及其他信息,例如流派)。

我们可以使用 pandas 从该文件中加载标题。 数据集随附的自述文件中提供了有关文件和类别的其他信息。 文件中的数据为 CSV 格式,但数据之间用|符号分隔; 它没有标题,并且编码很重要。 在README文件中找到了列名。

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

获取电影标题很重要,因此我们将创建一个函数,该函数将从MovieID中返回电影的标题,从而避免了每次查找电影的麻烦。 让我们看一下代码:

def get_movie_name(movie_id):

我们为给定的MovieID查找movie_name_data DataFrame,仅返回标题列:

    title_object = movie_name_data[movie_name_data["MovieID"] == movie_id]["Title"]

我们使用 values 参数获取实际值(而不是当前存储在title_object中的 PandasSeries对象)。 我们只对第一个值感兴趣-无论如何,给定的MovieID应该只有一个标题!

    title = title_object.values[0]

我们根据需要返回标题来结束该函数。 让我们看一下代码:

    return title

在新的 IPython Notebook 单元中,我们调整了先前的代码以打印出最高规则,同时还包括标题:

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

结果更具可读性(仍然存在一些问题,但是我们暂时可以忽略它们):

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

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

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

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

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

评估

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

为此,我们将计算测试集的置信度,即每个规则对测试集的置信度。

在这种情况下,我们不会应用正式的评估指标; 我们只研究规则并寻找好的例子。

首先,我们提取测试数据集,这是我们在训练集中未使用的所有记录。 我们将前 200 个用户(按 ID 值)用于训练集,并将其余所有用户用于测试数据集。 与训练集一样,我们还将获得该数据集中每个用户的好评。 让我们看一下代码:

test_dataset = all_ratings[~all_ratings['UserID'].isin(range(200))]
test_favorable = test_dataset[test_dataset["Favorable"]]
test_favorable_by_users = dict((k, frozenset(v.values)) for k, v in test_favorable.groupby("UserID")["MovieID"])

然后,我们以与之前相同的方式计算前提得出结论的正确实例。 唯一的变化是使用测试数据而不是训练数据。 让我们看一下代码:

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

接下来,我们从正确的计数中计算每个规则的置信度。 让我们看一下代码:

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

最后,我们打印出具有标题而不是影片 ID 的最佳关联规则。

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

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

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

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

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

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

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

例如,第二条规则对训练数据具有完全的信心,但仅在 60%的情况下对测试数据准确。 不过,前 10 名中的许多其他规则对测试数据都抱有很高的信心,这使它们成为提出建议的良好规则。

注意

如果您正在浏览其余规则,则某些规则的测试置信度为-1。 置信度值始终在 0 到 1 之间。该值表示在测试数据集中根本找不到该特定规则。

Evaluation

Evaluation

Evaluation

二十六、使用提升器提取特征

到目前为止,我们已经使用的数据集已按功能进行了描述。 在上一章中,我们使用了以事务为中心的数据集。 但是,最终这只是表示基于特征的数据的另一种格式。

还有许多其他类型的数据集,包括文本,图像,声音,电影,甚至是真实对象。 但是,大多数数据挖掘算法都依赖于具有数字或分类特征。 这意味着我们需要一种表示这些类型的方法,然后再将它们输入数据挖掘算法。

在本章中,我们将讨论如何提取数字和分类特征,并在拥有特征时选择最佳特征。 我们将讨论一些常见的特征提取模式和技术。

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

  • 从数据集中提取特征
  • 创建新功能
  • 选择好的功能
  • 为自定义数据集创建自己的转换器

特征提取

提取功能是数据挖掘中最关键的任务之一,并且与选择数据挖掘算法相比,它通常对最终结果的影响更大。 不幸的是,对于选择可导致高性能数据挖掘的功能没有严格的规定。 在许多方面,这就是数据挖掘科学成为一门艺术的地方。 创建好的功能依赖于直觉,领域专业知识,数据挖掘经验,反复试验,有时还需要运气。

代表模型中的现实

并非所有数据集都以特征表示。 有时,数据集仅由给定作者撰写的所有书籍组成。 有时,它是 1979 年发行的每部电影的电影。有时,它是有趣的历史文物的图书馆集合。

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

为了在这里为我们提供帮助的数据挖掘算法,我们需要将其表示为特征。 特征是一种创建模型的方式,而模型以数据挖掘算法可以理解的方式提供了逼真的近似。 因此,模型只是现实世界中某些方面的简化版本。 例如,国际象棋是历史战争的简化模型。

选择功能还有另一个优点:将特征的复杂性降低到更易于管理的模型中。 想象一下,要正确,准确和完整地向不具备该项目背景知识的人描述真实世界的对象需要多少信息。 您需要描述尺寸,重量,质地,成分,年龄,缺陷,目的,来源等。

对于当前算法而言,真实对象的复杂性太大,因此我们改用这些更简单的模型。

这种简化还将我们的意图集中在数据挖掘应用中。 在后面的章节中,我们将研究集群及其在哪些方面至关重要。 如果放入随机特征,则会得到随机结果。

但是,这样做有一个弊端,因为这种简化会减少细节,或者可能会删除我们希望对其进行数据挖掘的事物的良好指示。

应该始终考虑如何以模型的形式表示现实。 您不仅需要使用过去使用过的方法,还需要考虑数据挖掘工作的目标。 您想达到什么目的? 在第 3 章,“用决策树”预测运动优胜者中,我们通过考虑目标(预测优胜者)来创建功能,并使用了一点点领域知识来提出新功能的想法。

注意

并非所有功能都必须是数字或分类的。 已经开发了可以直接在文本,图形和其他数据结构上运行的算法。 不幸的是,这些算法不在本模块的范围之内。 在本模块中,我们主要使用数字或分类特征。

Adult数据集是采用复杂现实并尝试使用功能对其建模的一个很好的例子。 在此数据集中,目标是估计某人的年收入是否超过 50,000 美元。 要下载数据集,请导航至这个页面,然后单击数据文件夹链接。 将adult.dataadult.names下载到数据文件夹中名为Adult的目录中。

该数据集承担一项复杂的任务,并在功能中对其进行了描述。 这些功能描述了人,他们的环境,他们的背景和他们的生活状况。

在本章中打开一个新的 IPython Notebook,并设置数据的文件名并导入 pandas 以加载文件:

import os
import pandas as pd
data_folder = os.path.join(os.path.expanduser("~"), "Data", "Adult")
adult_filename = os.path.join(data_folder, "adult.data")
Using pandas as before, we load the file with read_csv:
adult = pd.read_csv(adult_filename, header=None,
    names=["Age", "Work-Class", "fnlwgt",
    "Education", "Education-Num",
    "Marital-Status", "Occupation",
    "Relationship", "Race", "Sex",
    "Capital-gain", "Capital-loss",
    "Hours-per-week", "Native-Country",
    "Earnings-Raw"])

大多数代码与前面的章节相同。

adult文件本身在文件末尾包含两个空行。 默认情况下,Pandas 会将倒数第二行解释为空(但有效)行。 要删除此行,我们删除任何行号无效的行(使用inplace只是确保同一数据框受到影响,而不是创建一个新的行):

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

看一下数据集,我们可以从adult.columns中看到各种功能:

adult.columns

结果显示了存储在pandasIndex对象中的每个功能名称:

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

共同特征模式

尽管有数百万种创建特征的方法,但跨不同学科采用的某些常见模式。 但是,选择合适的功能很棘手,值得考虑一下功能如何与最终结果相关。 就像谚语所说的那样,不要凭封面来判断一本书-如果您对其中包含的信息感兴趣,那么就不值得考虑一本书的大小。

一些常用功能着眼于正在研究的现实世界对象的物理属性,例如:

  • 空间属性,例如对象的长度,宽度和高度
  • 物体的重量和/或密度
  • 对象或其组件的年龄
  • 对象的类型
  • 物体的质量

其他功能可能取决于对象的用法或历史记录:

  • 对象的生产者,发行者或创建者
  • 制造年份
  • 使用对象

其他功能根据其组成部分描述数据集:

  • 给定子组件的频率,例如书中的单词
  • 子组件数和/或不同子组件数
  • 子组件的平均大小,例如平均句子长度

序数功能使我们能够对相似值进行排名,排序和分组。 正如我们在前几章中看到的那样,特征可以是数字的或分类的。 数字特征通常被描述为序数。 例如,三个人爱丽丝,鲍勃和查理可能身高分别为 1.5 m,1.6 m 和 1.7 m。 我们可以说爱丽丝和鲍勃的身高比爱丽丝和查理高。

我们在上一节中加载的 Adult 数据集包含连续,序数特征的示例。 例如,Hours-per-week功能可跟踪人们每周工作多少小时。 在这样的功能上,某些操作是有意义的。 它们包括计算平均值,标准偏差,最小值和最大值。 Pandas 中有一个函数可以提供这种类型的一些基本摘要统计信息:

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

结果告诉我们有关此功能的一些信息。

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

这些操作中的某些对其他功能没有意义。 例如,计算教育状况的总和是没有意义的。

也有一些特征不是数字的,但仍然是序数。 Adult 数据集中的Education功能就是一个示例。 例如,学士学位比未完成高中的学历更高,而未完成高中的学历更高。 计算这些值的平均值并没有多大意义,但是我们可以通过取中值来创建一个近似值。 该数据集提供了一个有用的功能Education-Num,该功能分配的数字基本上等于完成的教育年限。 这使我们可以快速计算中位数:

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

结果是 10,或高中毕业一年。 如果没有,我们可以通过对教育值进行排序来计算中位数。

功能也可以是分类的。 例如,球可以是网球板球足球或任何其他类型的球。 分类特征也称为标称特征。 对于名义特征,值可以相同或不同。 尽管我们可以按大小或重量对球进行排名,但仅凭类别还不足以比较事物。 网球不是板球,也不是足球。 我们可以争辩说,网球与板球(在尺寸上)更相似,但仅凭类别并不能与之区分开来-它们是相同的,或者不是。

如第 3 章,“用决策树预测体育获胜者”中所述,我们可以使用一键编码将类别特征转换为数字特征。 对于上述类别的球,我们可以创建三个新的二进制特征:is a tennis ballis a cricket ballis a football。 对于网球,向量将为[1、0、0]。 板球的值为[0,1,0],而足球的值为[0,0,1]。 这些特征是二进制的,但是许多算法都可以将它们用作连续特征。 这样做的一个关键原因是它很容易实现直接的数值比较(例如计算样本之间的距离)。

Adult 数据集包含几个分类特征,例如Work-Class。 虽然我们可以争辩说某些价值观比其他价值观具有更高的地位(例如,有工作的人可能比没有工作的人收入更高),但这并非对所有价值观都有意义。 例如,在州政府工作的人比在私营部门工作的人收入的可能性不大或少。

我们可以使用unique()函数在数据集中查看此功能的唯一值:

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

结果在此列中显示唯一值:

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

前面的数据集中有一些缺失值,但是在此示例中它们不会影响我们的计算。

类似地,我们可以通过称为离散化的过程将数字特征转换为分类特征,正如我们在第 4 章,“使用亲和力分析”推荐电影中所看到的那样。 我们可以称呼任何身高高于 1.7 m 的人,以及任何身高小于 1.7 m 的人。 这给了我们一种分类特征(尽管仍然是序数特征)。 我们在这里确实丢失了一些数据。 例如,两个人,一个身高 1.69 m,一个身高 1.71 m,将处于两个不同的类别,并且认为彼此完全不同。 相反,身高 1.2 m 的人将被认为与身高 1.69 m 的人“大致相同的身高”! 细节上的损失是离散化的副作用,这是我们在创建模型时要处理的问题。

在成人数据集中,我们可以创建LongHours功能,该功能可以告诉我们一个人每周工作时间是否超过 40 小时。 这将我们的连续功能(Hours-per-week)变成了一个绝对的功能:

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

创建良好的功能

建模以及简化所导致的信息丢失是我们没有可以仅应用于任何数据集的数据挖掘方法的原因。 优秀的数据挖掘从业人员将在其应用数据挖掘的领域中拥有或获得领域知识。 他们将研究问题,可用的数据,并提出一个代表他们试图实现的目标的模型。

例如,身高特征可能描述一个人的一个组成部分,但可能无法很好地描述他们的学习成绩。 如果我们试图预测一个人的等级,我们可能不会费心测量每个人的身高。

在这里,数据挖掘变得比艺术更具艺术性。 提取好的特征是困难的,并且是正在进行的重要研究的主题。 选择更好的分类算法可以提高数据挖掘应用的性能,但是选择更好的功能通常是更好的选择。

在所有数据挖掘应用中,您应先概述要查找的内容,然后再开始设计可以找到它的方法。 这将决定您要针对的功能类型,可以使用的算法类型以及对最终结果的期望。

Creating good features

功能选择

通常会有很多功能可供选择,但是我们只希望选择一小部分。 有许多可能的原因:

  • 降低复杂度:随着数据数量的增加,许多数据挖掘算法需要更多的时间和资源。 减少功能部件的数量是一种使算法运行更快或资源更少的好方法。
  • 降低噪音:添加额外的功能并不总是可以带来更好的性能。 额外的功能可能会使算法感到困惑,从而发现没有意义的相关性和模式(这在较小的数据集中很常见)。 仅选择适当的特征是减少没有实际意义的随机相关性的好方法。
  • 创建可读模型:尽管许多数据挖掘算法将为具有数千个特征的模型愉快地计算答案,但结果可能难以为人类解释。 在这些情况下,值得使用较少的功能并创建人类可以理解的模型。

一些分类算法可以处理诸如此类的数据。 获得正确的数据并获得有效描述正在建模的数据集的功能仍可以帮助算法。

我们可以执行一些基本测试,例如确保功能至少有所不同。 如果要素的值都相同,则无法为我们提供额外的信息来执行数据挖掘。

例如,scikit-learn 中的VarianceThreshold转换器将删除至少没有最小方差值的任何功能。 为了展示它是如何工作的,我们首先使用 NumPy 创建一个简单的矩阵:

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

结果是三列 10 行中的数字 0 到 29。 这代表具有 10 个样本和三个特征的综合数据集:

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

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

X[:,1] = 1

结果在第一行和第三行中有很多方差,但是在第二行中没有方差:

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

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

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

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

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

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

print(vt.variances_)

结果表明,尽管第一列和第三列至少包含一些信息,但第二列没有差异:

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

像这样的简单明了的测试总是很适合在初次查看数据时运行。 没有差异的要素不会为数据挖掘应用增加任何价值; 但是,它们会降低算法的性能。

选择最佳的个人功能

如果我们具有许多功能,那么找到最佳子集的问题将是一项艰巨的任务。 它涉及多次解决数据挖掘问题本身。 正如我们在第 4 章,“使用亲和力分析推荐电影”一样,基于子集的任务随着特征数量的增加而呈指数增长。 寻找所需的最佳特征子集所需的时间呈指数增长。

解决此问题的方法不是寻找一个可以协同工作的子集,而不仅仅是寻找最佳的个别功能。 这个单变量特征选择会根据一个特征本身的执行情况给我们一个分数。 这通常是针对分类任务完成的,并且我们通常会测量变量和目标类之间的某种类型的相关性。

scikit-learn程序包具有许多用于执行单变量特征选择的转换器。 它们包括SelectKBestSelectPercentile,它们返回k最佳性能特征,而SelectPercentile则返回最高 *r%*特征。 在这两种情况下,都有许多计算要素质量的方法。

有许多不同的方法可以计算单个要素与类值的关联效率。 常用的方法是卡方检验(χ2)。 其他方法包括互信息和熵。

我们可以使用Adult数据集观察单个功能的测试。 首先,我们从 PandasDataFrame中提取数据集和类值。 我们可以选择以下功能:

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

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

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

接下来,我们使用chi2函数和SelectKBest提升器创建提升器:

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

运行fit_transform将调用 fit,然后使用相同的数据集进行转换。 结果将创建一个新的数据集,仅选择最佳的三个特征。 让我们看一下代码:

Xt_chi2 = transformer.fit_transform(X, y)

现在,生成的矩阵仅包含三个特征。 我们还可以获取每列的分数,从而找出所使用的功能。 让我们看一下代码:

print(transformer.scores_)

列印结果给我们这些分数:

[  8.60061182e+03   2.40142178e+03   8.21924671e+07   1.37214589e+06
   6.47640900e+03]

第一,第三和第四列的最大值与AgeCapital-GainCapital-Loss功能相关。 基于单变量特征选择,这些是最佳选择。

注意

如果您想了解有关成人数据集中特征的更多信息,请查看数据集随附的adult.names文件及其引用的学术论文。

我们还可以实现其他相关性,例如 Pearson 相关性系数。 这在 SciPy 中实现,SciPy 是用于科学计算的库(scikit-learn 将其用作基础)。

注意

如果您的计算机上正在运行 scikit-learn,则 SciPy 也是如此。 您无需进一步安装任何组件即可使此示例正常工作。

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

from scipy.stats import pearsonr

前面的功能几乎适合 scikit-learn 的单变量转换器所需的接口。 该函数需要接受两个数组(在我们的示例中为xy)作为参数,并返回两个数组,即每个要素的得分和相应的 p 值。 我们先前使用的chi2函数仅使用所需的接口,这使我们可以将其直接传递给 SelectKBest。

SciPy 中的pearsonr函数接受两个数组。 但是,它接受的 X 数组只是一个维度。 我们将编写一个包装函数,使我们可以将其用于多变量数组,就像我们拥有的那样。 让我们看一下代码:

def multivariate_pearsonr(X, y):

我们创建scorespvalues数组,然后遍历数据集的每一列:

    scores, pvalues = [], []
    for column in range(X.shape[1]):

我们仅计算此列的 Pearson 相关性,并记录得分和 p 值。

        cur_score, cur_p = pearsonr(X[:,column], y)
        scores.append(abs(cur_score))
        pvalues.append(cur_p)

注意

皮尔逊值可以在-11之间。 1的值表示两个变量之间的完美相关,而值-1 表示一个完美的负相关,即,一个变量的高值给出另一个变量的低相关值,反之亦然。 拥有这些功能确实很有用,但是将被丢弃。 因此,我们将绝对值而不是原始有符号值存储在scores数组中。

最后,我们在一个元组中返回分数和p-values

    return (np.array(scores), np.array(pvalues))

现在,我们可以像以前一样使用变形器类使用 Pearson 相关系数对特征进行排名:

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

这将返回一组不同的功能! 以这种方式选择的功能是第一,第二和第五列:AgeEducationHours-per-week有效。 这表明最好的功能是什么并没有确切的答案,这取决于度量标准。

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

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

这里的chi2平均值为 0.83,而 Pearson 得分较低,为 0.77。 对于此组合,chi2返回更好的结果!

值得记住此数据挖掘活动的目标:预测财富。 结合良好的功能和功能选择,仅使用一个人的三个功能就可以达到 83%的准确性!

Selecting the best individual features

功能创建

有时,仅从现有功能中选择功能是不够的。 我们可以用不同于已有的功能来创建功能。 我们之前看到的单热编码方法就是一个例子。 代替使用带有ABC选项的类别特征,我们将创建三个新特征是 A 吗?是 B 吗?是 C 吗?

创建新功能似乎没有必要,也没有明显的好处-毕竟,信息已经在数据集中,我们只需要使用它即可。 但是,某些算法在要素之间具有显着相关性或存在冗余要素时会遇到困难。 如果有多余的功能,它们也可能会遇到困难。

因此,有多种方法可以从现有功能中创建新功能。

我们将加载一个新的数据集,因此现在是启动一个新的 IPython Notebook 的好时机。 从这个页面下载广告数据集,并将其保存到Data文件夹中。

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

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

此数据集存在两个问题,使我们无法轻松加载它。 首先,前几个特征是数字,但是 Pandas 会将它们作为字符串加载。 为了解决这个问题,我们需要编写一个转换函数,该函数将字符串转换为数字(如果可能)。 否则,我们将得到 NaN(这是的缩写,不是数字),这是一个特殊的值,指示该值不能解释为数字。 与其他编程语言中的null 相似。

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

我们将创建一个函数来为我们执行此转换:

def convert_number(x):

首先,我们要将字符串转换为数字,然后查看是否失败。 然后,我们将转换包含在try/except块中,并捕获ValueError异常(如果无法通过这种方式将字符串转换为数字,则会抛出该异常):

    try:
        return float(x)
    except ValueError:

最后,如果转换失败,我们将从先前导入的 NumPy 库中获得一个 NaN:

        return np.nan

现在,我们为转换创建字典。 我们希望将所有功能转换为浮点数:

converters = defaultdict(convert_number

另外,我们想将最后一列(列索引#1558)设置为二进制功能。 在成人数据集中,我们为此创建了一个新功能。 在数据集中,我们将在加载特征时对其进行转换。

converters[1558] = lambda x: 1 if x.strip() == "ad." else 0

现在我们可以使用read_csv加载数据集。 我们使用 converters 参数将自定义转换传递给 Pandas:

ads = pd.read_csv(data_filename, header=None, converters=converters)

结果数据集非常大,具有 1,559 个要素和 2,000 多个行。 以下是通过将ads[:5]插入新单元格来打印的前五个特征值:

Feature creation

数据集描述了网站上的图像,目的是确定给定图像是否为广告。

该数据集中的特征未按其标题很好地描述。 ad.data文件随附两个文件,它们具有更多信息:ad.DOCUMENTATIONad.names。 前三个功能是图像尺寸的高度,宽度和比率。 如果是广告,则最终功能为 1,否则为 0。

其他功能是 1,表示 URL 中的某些单词,替代文本或图像的标题。 这些单词(例如单词赞助商)用于确定图片是否可能是广告。 许多功能重叠很多,因为它们是其他功能的组合。 因此,该数据集具有很多冗余信息。

将数据集加载到 Pandas 中后,我们现在将提取xy数据用于分类算法。 x矩阵将是我们数据框中的所有列,但最后一列除外。 相反,y 数组将仅是最后一列(特征#1558)。 让我们看一下代码:

X = ads.drop(1558, axis=1).values
y = ads[1558]

创建自己的转换器

随着复杂性和数据集类型的变化,您可能会发现找不到适合您需求的现有特征提取转换器。 我们将在第 7 章,“使用图形挖掘”发现要遵循的帐户中看到一个示例,在此我们从图形中创建新功能。

提升器类似于转换功能。 它以一种形式的数据作为输入,并以另一种形式的数据作为输出。 可以使用一些训练数据集来训练提升器,并且可以使用这些训练后的参数来转换测试数据。

转换器 API 非常简单。 它以特定格式的数据作为输入,并返回另一种格式(与输入相同或不同)的数据作为输出。 程序员不需要太多其他内容。

Creating your own transformer

提升器 API

提升器具有两个关键功能:

  • fit():此以训练数据集作为输入并设置内部参数
  • transform():此本身执行转换。 这可以采用训练数据集,也可以采用相同格式的新数据集

fit()transform()功能应采用与输入相同的数据类型,但是transform()可以返回不同类型的数据。

我们将创建一个琐碎的转换器来展示该 API 的作用。 转换器将 NumPy 数组作为输入,并根据均值将其离散化。 高于(训练数据的)平均值的任何值将被赋予值 1,而低于或等于平均值​​的任何值将被赋予值 0。

我们使用 Pandas 对 Adult 数据集进行了相似的转换:如果值大于每周 40 小时,则采用Hours-per-week功能并创建了LongHours功能。 该提升器不同有两个原因。 首先,代码将符合 scikit-learn API,使我们可以在管道中使用它。 第二,代码将学习平均值,而不是将其作为固定值(例如LongHours示例中为 40)。

实施细节

首先,打开我们用于成人数据集的 IPython Notebook 的。 然后,单击单元菜单项,然后选择 Run All。 这将重新运行所有单元,并确保笔记本计算机是最新的。

首先,我们导入TransformerMixin,这将为我们设置 API。 尽管 Python 没有严格的接口(与 Java 之类的语言相对),但使用mixin这样的 scikit-learn 可以确定该类实际上是一个转换器。 我们还需要导入一个函数来检查输入的有效类型。 我们将尽快使用它。

让我们看一下代码:

from sklearn.base import TransformerMixin
from sklearn.utils import as_float_array

现在,创建一个新类,作为我们mixin的子类:

class MeanDiscrete(TransformerMixin):

我们需要定义 fit 和 transform 函数以符合 API。 在fit函数中,我们找到数据集的平均值,并设置一个内部变量以记住该值。 让我们看一下代码:

    def fit(self, X):

首先,我们使用as_float_array函数确保X是我们可以使用的数据集(如果可以,例如X是浮点列表,它也会转换X):

        X = as_float_array(X)

接下来,我们计算数组的平均值并设置一个内部参数以记住该值。 当X是一个多元数组时,self.mean将是一个包含每个特征均值的数组:

        self.mean = X.mean(axis=0)

fit函数还需要返回类本身。 此要求确保我们可以在转换器中执行功能链(例如调用transformer.fit(X).transform(X))。 让我们看一下代码:

        return self

接下来,我们定义转换函数,它采用与拟合函数相同类型的数据集,因此我们需要检查是否输入正确:

    def transform(self, X):
        X = as_float_array(X)

我们也应该在这里执行另一项检查。 尽管我们需要输入为 NumPy 数组(或等效数据结构),但形状也必须保持一致。 该数组中的要素数量必须与该类在其上进行训练的要素数量相同。

        assert X.shape[1] == self.mean.shape[0]

现在,我们仅通过测试X中的值是否大于存储的平均值来执行实际的转换。

        return X > self.mean

然后,我们可以创建此类的实例,并使用它来转换我们的X数组:

mean_discrete = MeanDiscrete()
X_mean = mean_discrete.fit_transform(X)

单元测试

当创建自己的函数和类时,进行单元测试总是一个好主意。 单元测试旨在测试代码的单个单元。 在这种情况下,我们要测试我们的提升器是否按需进行。

好的测试应该是独立可验证的。 确认测试合法性的一个好方法是使用另一种计算机语言或方法来执行计算。 在这种情况下,我使用 Excel 创建数据集,然后计算每个单元的平均值。 然后将这些值转移到此处。

单元测试也应该很小并且可以快速运行。 因此,所使用的任何数据都应具有较小的大小。 我用于创建测试的数据集存储在先前的Xt变量中,我们将在测试中重新创建它。 这两个特征的平均值分别为 13.5 和 15.5。

为了创建我们的单元测试,我们从 NumPy 的测试中导入assert_array_equal函数,该函数检查两个数组是否相等:

from numpy.testing import assert_array_equal

接下来,我们创建函数。 测试名称以test_开头很重要,因为此名称是用于自动查找和运行测试的工具。 我们还设置了测试数据:

def test_meandiscrete():
    X_test = np.array([[ 0,  2],
                        [ 3,  5],
                        [ 6,  8],
                        [ 9, 11],
                        [12, 14],
                        [15, 17],
                        [18, 20],
                        [21, 23],
                        [24, 26],
                        [27, 29]])

然后,我们创建提升器实例并使用以下测试数据进行拟合:

    mean_discrete = MeanDiscrete()
    mean_discrete.fit(X_test)

接下来,我们将内部均值参数与我们独立验证的结果进行比较,以检查内部均值参数是否设置正确:

    assert_array_equal(mean_discrete.mean, np.array([13.5, 15.5]))

然后,我们运行转换以创建转换后的数据集。 我们还使用输出的期望值创建一个(独立计算的)数组:

    X_transformed = mean_discrete.transform(X_test)
    X_expected = np.array([[ 0,  0],
                            [ 0, 0],
                            [ 0, 0],
                            [ 0, 0],
                            [ 0, 0],
                            [ 1, 1],
                            [ 1, 1],
                            [ 1, 1],
                            [ 1, 1],
                            [ 1, 1]])

最后,我们测试返回的结果确实是我们期望的结果:

    assert_array_equal(X_transformed, X_expected)

我们可以通过简单地运行函数本身来运行测试:

test_meandiscrete()

如果没有错误,则测试运行没有问题! 您可以通过将某些测试更改为故意不正确的值并查看测试失败来验证这一点。 记住将它们改回来,以便测试通过。

如果我们有多个测试,那么值得使用一个名为nose的测试框架来运行我们的测试。

全部放在一起

现在我们已经测试了提升器,是时候将其付诸实践了。 使用到目前为止所学的知识,我们创建Pipeline,第一步设置为MeanDiscrete转换器,第二步设置为决策树分类器。 然后,我们进行交叉验证并打印出结果。 让我们看一下代码:

from sklearn.pipeline import Pipeline
pipeline = Pipeline([('mean_discrete', MeanDiscrete()),
  ('classifier', DecisionTreeClassifier(random_state=14))])
  scores_mean_discrete = cross_val_score(pipeline, X, y, scoring='accuracy')
  print("Mean Discrete performance:
{0:.3f}".format(scores_mean_discrete.mean()))

结果为 0.803,不如以前好,但对于简单的二进制功能也不错。

Putting it all together

Putting it all together

Putting it all together