第 1 章 数据挖掘入门
我们正在以人类历史上前所未有的规模收集现实世界的数据。伴随着这种趋势,日常生活对于这些信息的依赖也与日俱增。我们现在期待计算机能够完成各种工作——翻译网页、精准地预报天气、推荐我们可能感兴趣的书、诊断健康问题。这种期待在未来会对应用的广度和效率有更高的要求。数据挖掘是一套用数据来训练计算机做出决策的方法论。它已经成为支撑当今许多高科技系统的骨干技术。
Python在当下大为流行不无原因。它既给予开发人员相当大的灵活性,还包含执行各种任务的众多模块,并且Python代码比用其他语言编写的代码更为简洁可读。Python在数据挖掘领域也形成了规模庞大、气氛活跃的社区,容纳了初学者、从业者、学术研究人员等各种身份的人士。
本章会介绍如何用Python进行数据挖掘工作,其中包含以下几个话题。
- 什么是数据挖掘?数据挖掘的适用场景有哪些?
- 搭建用于数据挖掘的Python环境。
- 亲和性分析示例:根据消费习惯推荐商品。
- 分类问题示例:根据尺寸推断植物种类。
1.1 什么是数据挖掘
数据挖掘提供了一种让计算机基于数据做出决策的方法。所谓的决策可以是预测明天的天气、拦截垃圾邮件、识别网站的语种和在交友网站上找到心仪人选。数据挖掘的应用场景有很多,而且人们还在不断地发掘扩充。
数据挖掘涉及众多领域,包括算法设计、统计学、工程学、最优化理论和计算机科学。尽管数据挖掘结合了这些领域的基础技能,但我们在特定领域中应用数据挖掘时,仍需要结合相应的领域知识(即专业知识)。领域知识会在数据挖掘中起到画龙点睛的作用。要想提升数据挖掘的效益,免不了要把领域知识与算法相结合。
虽然数据挖掘的应用实现细节通常差异很大,但从同样的高度来看,它们都是用一部分数据训练模型,然后再把模型应用到其他数据中。
数据挖掘的应用包含创建数据集和算法调参两部分工作,步骤如下。
(1) 首先创建数据集,用来描述现实世界中的某一方面。数据集由两个方面组成。
- 样本,现实世界中的对象,比如一本书、一张相片、一只动物、一个人。在其他命名规范中,样本也可能被称为观测(observation)、记录或行。
- 特征,数据集中样本的描述或测量值。特征可以是长度、词频、动物身上腿的数量、样本的创建日期等。在其他命名规范中,特征也可能被称为变量、列、属性或共变(covariant)。
(2) 接下来是算法调参。每个数据挖掘算法都有参数,要么是算法自带的,要么是用户提供的。调整参数即影响算法基于数据做出决策的过程。
举个简单的例子,假设我们希望计算机可以把人按身高分成两类:高与矮。一开始要采集数据集,这个数据集应包含不同人的身高以及判定高矮的条件,如表1-1所示。
表 1-1
人员 |
身高 |
高还是矮 |
---|---|---|
1 |
155 cm |
矮 |
2 |
165 cm |
矮 |
3 |
175 cm |
高 |
4 |
185 cm |
高 |
接下来则是算法调参。此处使用一种简单的算法:如果身高大于,则判定此人高;否则判定此人矮。该训练算法会依数据为
取一个合适的值。对于表中的数据而言,合理阈值应是170 cm。算法会把身高170 cm以上的人判定为高,而把身高低于此值的人判定为矮。这样,我们的算法就可以为新数据分类。假如有一个身高为167 cm的人,尽管之前在数据集中并没有见到这样的人,但算法依然可以对其分类。
表1-1中数据的特征显然是身高。要确定人的高矮,就要采集身高数据。特征工程(feature engineering)是数据挖掘中的一个关键问题。在后面的章节里,我们会讨论如何选择适宜采集到数据集中的特征。这个步骤往往最终需要引入领域知识,或者至少要经过反复尝试才能取得成效。
本书用Python来介绍数据挖掘。为便于理解,本书有时更加关注代码和工作流程是否清晰易懂,而不是所采用的方法效率是否最优。因此,我们有时会跳过提高算法速度或效率的细节。
1.2 使用Python和Jupyter Notebook
本节会介绍如何安装Python和本书大部分内容所需的Jupyter Notebook环境。除此之外,我们还会安装NumPy模块。它将被用于第一组数据。
就在不久前,Jupyter Notebook的名字还是IPython Notebook。你会在搜索引擎页面中见到该项目搜索词的变化。Jupyter是新的名字,表示该项目外延扩大,不再局限于Python。
1.2.1 安装Python
Python是一门出色的编程语言,功能全面,易于上手。
本书使用Python 3.5,在Python的官方网站上可以找到对应你的操作系统的版本。不过我还是推荐用Anaconda安装Python。
这时你会面临两个主版本Python的选择:Python 3.5和Python 2.7。请下载并安装Python 3.5,因为这是本书测试过的版本。请按照网站上相应操作系统的指南完成安装。如果你足够充分的理由选择Python 2,那么请下载Python 2.7。不过要小心,本书中的某些代码可能需要一些额外的处理才能正常运行。
本书假定你已具备编程和Python的相关知识。虽然掌握一定程度的相关知识会加快你的学习进度,但你无须成为Python专家。除非出现不同于一般Python的编码实践,否则本书不会解释代码的总体架构和语法。
如果你没有任何编程经验,那么我建议你先看一下Packt出版的Learning Python,或是www.diveintopython3.net上的Dive into Python。
此外,你还可以在线阅览由Python社区维护的两份Python入门教程。
- 给没有编程经验的人准备的入门教程:https://wiki.python.org/moin/BeginnersGuide/NonProgrammers。
- 给不了解Python的程序员准备的入门教程:https://wiki.python.org/moin/BeginnersGuide/Programmers。
Windows用户需要设置环境变量才能在命令行中使用Python,而其他系统的用户通常可以直接使用Python。按照下面的步骤设置环境变量。
(1) 找出Python 3的安装位置,默认位置是C:\Python35
。
(2) 接下来在命令行(cmd
程序)中输入:set PATH=%PATH%;C:\Python35
1。
1因为这个方法设置的环境变量会在cmd
程序关闭后失效,所以推荐使用官方文档中的方法:https://docs.python.org/3.5/using/windows.html#finding-the-python-executable。——译者注
如果你把Python安装到了别的目录,那么请记得把上文中的
C:\Python35
替换成这个目录的路径。
安装好Python之后,你就可以在命令行中运行下面的代码来检验是否正确安装。
$ python
Python 3.5.1 (default, Apr 11 2014, 13:05:11)
[GCC 4.8.2] on Linux
Type "help", "copyright", "credits", or "license" for more information.
>>> print("Hello, world!")
Hello, world!
>>> exit()
注意美元符号($
)后的命令才是你要输入到终端(即Mac或Linux中的shell
,或Windows中的cmd
)中的内容。美元符号(和其他已经显示在你屏幕上的内容)是不需要输入的,你只需要输入后面的部分然后按回车键。
成功运行上面的“Hello, world!
”示例后退出程序,接下来安装一个用于运行 Python的更为先进的环境:Jupyter Notebook。
Python 3.5包含了
pip
程序,这是一个包管理器,有助于在系统上安装新的Python库。运行pip freeze
命令即可验证pip
是否可用,这个命令也会列出系统上已经安装的包。Anaconda也自带了包管理器conda
。要是不确定用哪个包管理器,请先用conda
,运行失败的话再用pip
。
1.2.2 安装Jupyter Notebook
Jupyter是一个Python开发平台,包含了一些运行Python的工具和环境,功能较标准 Python解释器更为丰富。它包含了功能强大的Jupyter Notebook,让你可以在Web浏览器中编写程序。它还会格式化代码、展示输出、给代码添加注释。这是一种探索数据集的绝佳工具,本书中会将它作为编写、运行代码的主要环境。
要安装Jupyter Notebook,请在命令行窗口(而不是Python)中输入下面的命令。
$ conda install jupyter notebook
这样安装Jupyter Notebook时,Anaconda会把包存放在用户目录下,因此不需要管理员权限。
安装好Jupyter Notebook之后,像下面这样运行它。
$ jupyter notebook
这条命令会做两件事:一是在后台创建一个Jupyter Notebook实例,它会在你刚才开启的命令行中运行;二是如图1-1所示,打开Web浏览器访问这个实例。这之后你就可以创建新笔记本(notebook),不过你需要用当前的工作目录取代/home/bob
。
图 1-1
要终止Jupyter Notebook的运行,需要打开运行Jupyter Notebook实例(之前运行过jupyter notebook
命令)的命令行窗口,然后按Ctrl+C。这样就能看到Shutdown this notebook server(y/[n]?)
的提示。此时输入y
并按回车,就可以关闭Jupyter Notebook了。
1.2.3 安装scikit-learn
scikit-learn
包虽然是一个用Python编写的机器学习库,但也包含其他语言的代码。它包含数目众多的用于机器学习的算法、数据集、工具和框架。scikit-learn
基于Python科学计算领域的技术栈构建,其中包括NumPy和SciPy这样的高性能库。scikit-learn
性能极佳,可扩展到多个节点,适合从新手到高级研究员等各种水平的用户使用。第2章会详细介绍scikit-learn
的用法。
要安装scikit-learn
,请使用Anaconda自带的conda
工具。如果缺少NumPy和SciPy库,它会替你一并安装。打开终端,输入下面的命令。
$ conda install scikit-learn
主要Linux发行版(比如Ubuntu或Red Hat)的用户可能会希望通过系统的包管理器安装官方包。
本书所需的最低版本是0.14,但不是每个发行版都提供最新版本的
scikit-learn
,因此在安装前请留意其版本。我建议用Anaconda安装、管理scikit-learn
,而不是用系统的包管理器。
若想从源码编译安装最新版本的scikit-learn
,或是需要一份详细的安装说明,请参考官方文档。
1.3 亲和性分析的简单示例
本节我们将学习第一个示例,它也是数据挖掘的一个常见应用场景。在消费者购买商品时,商家会收集关于消费者对同类商品的喜好倾向的信息,用以改进销售策略。在这个过程中,亲和性分析就会被用来判定哪些商品应该同时展现给消费者,即分析商品之间的相关程度。
在统计学课堂上有一句名言:相关不蕴含因果。因此亲和性分析的结果不能解释该现象的成因。在接下来的例子中,我们会对一些产品进行亲和性分析。在这个例子里,即使结果表明消费者会同时购买某些商品,也不能据此推断只要卖出某一商品,就能卖出另一商品。在用亲和性分析的结果指导业务流程的时候,这一差异尤为重要。
什么是亲和性分析
亲和性分析(affinity analysis)是一种用于计算样本(对象)相似度的数据挖掘方法。这个相似度可以出自下面几种场景:
- 网站的用户,扩展服务项目或定向投放广告;
- 销售的商品,推荐电影或其他商品;
- 人类基因,寻找拥有共同祖先的人们。
计算亲和性的方法有好几种。比如在记录两件商品同时售出的频率的同时,记录预测消费者购买商品1和购买商品2的准确率。我们也可以计算样本间的相似度,后面的章节中将会阐述这个方法。
1.4 商品推荐
传统业务向线上转移时,免不了要把像追加销售(up-selling,向消费者销售商品的升级品和附加品)这样的人工任务交由计算机自动完成,因为只有这样才能顺利扩张业务,与竞争对手分庭抗礼。用数据挖掘实现的商品推荐已经成为电子商务革命的强大推力,为企业增添了数以亿计的年利润。
本例的基础商品推荐服务遵循这样的思路:以前就能一并售出的商品,以后更可能一并售出。其实无论是在线上还是线下,很多商品推荐服务是基于这样的思路。
这样的商品推荐算法可以只简单检索用户购买商品的历史案例,并向用户推荐其历史上一并购买的其他商品。即使使用这样的简单算法,其效果也比随机推荐商品要好得多。不过这样的算法还有很大的改进余地,而这就是数据挖掘的用武之地。
为简化代码,这里只考虑同时购买两件商品的情况,比如用户在超市同时购买了面包和牛奶。从本例入手,我们希望能够得出以下这种形式的简易规则:
用户如果购买了商品X,那么也倾向于购买商品Y。
像“买香肠和汉堡的人更愿意再买一份番茄酱”这样的复杂规则涉及两件以上的商品,本例中不会介绍这种复杂情况。
1.4.1 用NumPy加载数据集
你可以在本书提供的代码包中找到本例用到的数据集,也可以从官方GitHub仓库中下载该数据集:https://github.com/PacktPublishing/Learning-Data-Mining-with-Python-Second-Edition。
请下载文件并将其保存在你的计算机中。注意数据集保存的路径。虽然我们可以从任何位置加载数据集,但把数据集与代码放在同一目录下会更方便。
本例中,我建议你为数据集和代码创建一个新文件夹,然后打开Jupyter Notebook,导航到该文件夹,并创建新的笔记本。
本例的数据集是一个NumPy二维数组,本书的示例大多使用这种格式。这个二维数组与表类似,里面的行表示样本,列表示特征,单元格表示样本的特征值。为了展示这种结构,请用下面的代码加载这个数据集。
import numpy as np
dataset_filename = "affinity_dataset.txt"
X = np.loadtxt(dataset_filename)
在笔记本中的第一个框中输入上面的代码,然后按Shift加回车键运行代码(这样也会在下面新增一个框,用来输入下一节代码)。代码运行完成后,会被分配一个序号,该序号会被填入到第一个框左边的方括号中。出现这个序号意味着代码运行完成了。第一个框如图1-2所示。
图 1-2
在代码运行时间较长的情况下,当代码处于运行中或计划运行的状态时,方括号中会是一个星号(*)。当代码运行完成后,方括号中就会出现序号,即便代码运行失败也是如此。
这个数据集包含100个样本和5个特征,而后面的代码会用到这两个值。为了提取这两个值,运行以下代码。
n_samples, n_features = X.shape
如果你的数据集与这个笔记本不在同一目录,那么你需要把dataset_filename
换成相应的路径。
下面展示数据集中的几行以便于加深理解。在下一个框中输入下面的这行代码,即可输出数据集的前5行。
print(X[:5])
输出结果列出了前5次交易中购买的商品。
[[ 0. 1. 0. 0. 0.]
[ 1. 1. 0. 0. 0.]
[ 0. 0. 1. 0. 1.]
[ 1. 1. 0. 0. 0.]
[ 0. 0. 1. 1. 1.]]
下载示例代码
只要是Packt出版的书,都可以通过在https://www.packtpub.com/登录账号来下载相应的示例代码。如果你是在别处购买的本书,那么你也可以访问http://www.packtpub.com/support并注册账号,我们将用邮件把示例代码发给你。我也创建了一个GitHub仓库,用于托管在线版本的代码,其中包含补丁、更新等。在这个仓库中可以找到示例代码和数据集。仓库地址为:https://github.com/PacktPublishing/Learning-Data-Mining-with-Python-Second-Edition。
横向看,第一行(0, 1, 0, 0, 0)
显示首次交易中的商品。纵向看,各列代表不同的商品:面包、牛奶、奶酪、苹果、香蕉。可以看出在这次交易中,消费者只购买了牛奶,而没有买别的东西。在下一个输入框中输入下面这行代码,把特征值转换成对应的单词。
features = ["bread", "milk", "cheese", "apples", "bananas"]
该数据集中的特征用二进制值表示,只能表明交易中是否购买了该特征对应的商品,而不能表明购买的数量。如果值是1,那么交易中购买了至少1份该商品,而值为0则表示没有购买该商品。而现实中的数据集会需要采用精准数值或更高的阈值。
1.4.2 实现规则的简单排序
我们希望找出消费者如果购买了商品X,那么也倾向于购买商品Y这样的规则。虽然在数据集中找出所有同时购买两件商品的情况并列出所有规则很容易,但我们需要区分规则的优劣,才能最终确定要推荐的商品。
规则的评价方法有多种,这里我们选定这两种:支持度(support)和置信度(confidence)。
支持度是规则在数据集中出现的次数,即匹配规则的样本数。有时我们会对其进行归一化处理,即将其除以该规则中的前提(premise)成立的总次数。方便起见,此处我们采用未归一化的数值。
前提(premise)是规则匹配的必要条件。结论(conclusion)则是规则的输出。以前文的规则“消费者如果购买了苹果,那么也倾向于购买香蕉”为例,该规则只有在满足消费者购买了苹果的前提时才能匹配。此时规则的结论为这位消费者也会购买香蕉。
支持度衡量规则匹配的频度,而置信度则衡量规则匹配的准确度。当满足前提时,规则匹配的情况的占比即为置信度。我们首先统计规则匹配数据的次数,然后除以满足前提的样本数(会用到if
语句)。
下面以“消费者如果购买了苹果,那么也倾向于购买香蕉”为例,计算支持度和置信度。
如下例所示,取由二维数组表示的矩阵中的一行作为样本,即sample[3]
。它的值就表示交易中是否购买了苹果。
sample = X[3]
同样,我们也可以通过sample[4]
获知交易中是否购买了香蕉(以此类推)。现在可以计算规则在数据集中出现的次数,并由此推出支持度和置信度。
现在我们要从数据集中计算出所有规则的统计量。因此,我们需要创建两个字典,分别用于匹配的规则(valid rules)和失配的规则(invalid rules)。字典的键是由前提和结论组成的元组(tuple)。元组的值是特征的数组下标,而非实际的特征名。对于前文的规则“消费者如果购买了苹果,那么也倾向于购买香蕉”而言,这个值是3和4。如果前提和结论都符合,那么就认为规则匹配;如果只有前提符合,那么对这个样本而言,规则失配。
下面逐步计算支持度和置信度。以下步骤适用于所有可能的规则。
(1) 首先,创建多个存放结果用的字典并将其初始化,以记录匹配的规则、失配的规则和前提的出现次数。此处用到了defaultdict
,它可以设定访问不存在的键时返回的默认值。
from collections import defaultdict
valid_rules = defaultdict(int)
invalid_rules = defaultdict(int)
num_occurences = defaultdict(int)
(2) 接下来在一个大循环中计算这些值。在迭代数据集中的每个样本时,不仅要迭代作为前提的特征,还要同时迭代作为结论的特征。这样就能映射出前提和结论之间的关系。如果样本的前提和结论都与规则匹配,那么把前提和结论的元组记录到valid_rules
中;如果规则失配,则记录到invalid_rules
中。
(3) 以数据集中的样本为例。
for sample in X:
for premise in range(n_features):
if sample[premise] == 0: continue
# 记录另一交易中购买的“前提”
num_occurences[premise] += 1
for conclusion in range(n_features):
# 当“前提”和“结论”均为同一件商品时,计算没有意义
if premise == conclusion:
continue
if sample[conclusion] == 1:
# 如果消费者也购买了“结论”
valid_rules[(premise, conclusion)] += 1
else:
# 如果消费者只购买了“前提”,而没有购买“结论”
invalid_rules[(premise, conclusion)] += 1
代码会记录样本满足前提(特征值为1)的情况,然后检查各个结论是否匹配规则,并且跳过前提和结论相同的情况,因为这将匹配:“消费者如果购买了苹果,那么也倾向于购买苹果”。这显然是毫无意义的。
至此用于计算支持度和置信度的统计量已经备齐。如前文所述,本例采用未归一化的支持度,即直接取valid_rules
的值。
support = valid_rules
置信度的计算方法也是一样,只是要迭代规则。
confidence = defaultdict(float)
for premise, conclusion in valid_rules.keys():
rule = (premise, conclusion)
confidence[rule] = valid_rules[rule] / num_occurences [premise]
现在我们就有了分别包含规则支持度和置信度的两个字典。创建一个能把规则输出成可读格式的函数,它可以接受5个参数:前提和结论的数组下标、刚才计算好的支持度和置信度字典,以及特征名称列表。这样我们就可以输出规则的支持度和置信度了。
def print_rule(premise, conclusion, support, confidence, features):
for premise, conclusion in confidence:
premise_name = features[premise]
conclusion_name = features[conclusion]
print("Rule: If a person buys {0} they will also
buy{1}".format(premise_name, conclusion_name))
print(" - Confidence: {0:.3f}".format
(confidence[(premise,conclusion)]))
print(" - Support: {0}".format(support[(premise, conclusion)]))
print("")
之后,你就可以像这样随意替换前提和结论,调用这个函数测试代码功能。
premise = 1
conclusion = 3
print_rule(premise, conclusion, support, confidence, features)
1.4.3 挑选最佳规则
我们已经计算出所有规则的支持度和置信度。为了挑选出最佳的规则,我们现在为规则排名,并输出排名最高的规则,排名将按照支持度和置信度进行。
为了找出支持度最高的规则,首先要对支持度字典排序。由于字典默认不支持排序,因而可以先用字典的items()
方法生成字典内容的列表,然后再用itemgetter(1)
取出的字典的值作为排序键进行排序。就像本例中这样,itemgetter
类可以用于嵌套列表的排序。参数reverse=True
表示从高到低排序。
from operator import itemgetter
sorted_support = sorted(support.items(), key=itemgetter(1), reverse=True)
然后就可以输出前5名的规则。
for index in range(5):
print("Rule #{0}".format(index + 1))
(premise, conclusion) = sorted_support[index][0]
print_rule(premise, conclusion, support, confidence, features)
输出的结果会是下面这样。
Rule #1
Rule: If a person buys bananas they will also buy milk
- Support: 27
- Confidence: 0.474
Rule #2
Rule: If a person buys milk they will also buy bananas
- Support: 27
- Confidence: 0.519
Rule #3
Rule: If a person buys bananas they will also buy apples
- Support: 27
- Confidence: 0.474
Rule #4
Rule: If a person buys apples they will also buy bananas
- Support: 27
- Confidence: 0.628
Rule #5
Rule: If a person buys apples they will also buy cheese
- Support: 22
- Confidence: 0.512
同样,代码也可以输出置信度前5名的规则。我们首先按置信度对规则进行排序,然后用同样的方法输出结果。
sorted_confidence = sorted(confidence.items(), key=itemgetter(1), reverse=True)
for index in range(5):
print("Rule #{0}".format(index + 1))
premise, conclusion = sorted_confidence[index][0]
print_rule(premise, conclusion, support, confidence, features)
可以发现“消费者如果购买了苹果,那么也倾向于购买奶酪”和“消费者如果购买了奶酪,那么也倾向于购买香蕉”这两条规则,在两份排名中都位居前列。这两条规则也就可以作为销售经理调整商品摆放策略或价格策略的参考。比如,如果本周苹果特价出售,就把奶酪放在苹果附近。而香蕉和奶酪同时促销就没什么意义,因为在购买了香蕉的消费者中,有66%的人会同时购买奶酪,因此这样的促销对提升香蕉销量并没有多少助益。
Jupyter Notebook可以在笔记本中展示内联的图表。不过有的时候默认不启用这个功能。可以用这行代码启用该功能:
%matplotlib inlne
。
用matplotlib
库可以展示可视化结果。
下面按置信度的顺序,用折线图展示各条规则的置信度。matplotlib
可以轻松实现这一需求:只需传入数值,它就能画出一张简单实用的折线图(见图1-3)。
from matplotlib import pyplot as plt
plt.plot([confidence[rule[0]] for rule in sorted_confidence])
图 1-3
从图1-3中可以看出,前5条规则的置信度相当优秀,而后面的规则置信度则快速跌落。此迹象表明,仅前5条规则可以用于指导经营决策。从根本上讲,像这样的数据挖掘技术,其实结果是取决于用户的。
本例展示了数据挖掘技术在数据集中探究关系、洞察现象的强大能力。下一节将介绍数据挖掘技术的另外两个用途:预测和分类。
1.5 分类的简单示例
在前面的亲和性分析示例中,如果想让消费者购买更多苹果,那么就要研究关联苹果的规则,以制定销售策略。这时,我们关注的是数据集中不同变量的相关性。而在分类问题中,我们只关注一个变量,也就是类别(class),有时也称作目标(target)。
1.6 什么是分类
无论在研究中还是在实际应用中,分类都是数据挖掘相当常见的用法。如前面的内容一样,我们分类的对象是代表现实事物的样本集合。不同的是,在分类问题中我们要维护一个新的数组——类别值,它表示样本的归类情况。什么时候会用到分类呢?可以举几个例子。
- 根据植物的尺寸辨别物种。这里的类别值是物种。
- 判断图像中是否有一只狗。这里的类别值是是否有狗。
- 根据特定的实验结果,诊断病人是否罹患癌症。这里的类别值是是否患癌症。
虽然上面的例子多是二元问题(是与否),但其实也有植物物种分类这样的分类问题。本节就会解决这一问题。
分类的应用目标是用类别已知的样本集合训练一个模型,然后让该模型来处理类别未知的样本集合。例如,基于历史邮件(其中垃圾邮件已标出)训练一个垃圾邮件分类器,然后用该分类器自动判断以后收到的邮件是不是垃圾邮件。
1.6.1 准备数据集
本例中,我们用著名的鸢尾数据集(Iris database)来给植物分类。这个数据集包含多达150个植物样本,以厘米(cm)为单位描述了植物的4项尺寸,花萼长度、花萼宽度、花瓣长度和花瓣宽度。这个数据集在数据挖掘领域很经典,其首次使用可以追溯到1936年。这个数据集包含3个类别:山鸢尾(Iris Setosa)、变色鸢尾(Iris Versicolour)和维吉尼亚鸢尾(Iris Virginica)。下面我们将通过检视植物的4项尺寸来判断物种。
scikit-learn
库自带这个数据集,直接加载即可。
from sklearn.datasets import load_iris
dataset = load_iris()
X = dataset.data
y = dataset.target
你可以用print(dataset.DESCR)
查看数据集的大体情况,里面会有特征的详细说明。
该数据集的特征是连续值,也就是说可以取任意范围的值。尺寸就是一个不错的连续特征,尺寸可以取1、1.2、1.25等这样的值。连续特征的另一个性质是,两个特征值的差值可以体现样本的相似程度,差值越小则越相似。花萼宽度1.2 cm的植物与花萼宽度1.25 cm的植物很可能是同一物种。
与之相反的是分类特征(categorical feature),它通常以数字形式表示,不能比较差值。鸢尾数据集中的类别值就是分类特征。类别0代表山鸢尾,类别1代表变色鸢尾,类别2代表维吉尼亚鸢尾。分类特征的差值不能体现相似程度,即此处不能由数值推出“比起维吉尼亚鸢尾,山鸢尾与变色鸢尾较为相似”这样的结论。数值只代表分类,我们只能说类别是否相同。
特征的类型不止这两种,还包括像素灰度、词频和元语法分析,我们会在后面的章节中介绍。
下面用到的算法需要分类特征,而鸢尾数据集的特征却是连续的,这就需要我们把连续特征转换成分类特征,这个过程被称为离散化。
最简单的离散化方法就是取一个阈值,低于阈值返回0,反之则返回1。这里我们取特征的均值作为阈值。下面先计算各个特征的均值。
attribute_means = X.mean(axis=0)
这行代码的结果是个数组,数组的长度就是特征的数量,在本例中是4。数组的第一个值就是第一个特征的均值,以此类推。然后把这些值一一转换为离散的分类特征。
assert attribute_means.shape == (n_features,)
X_d = np.array(X >= attribute_means, dtype='int')
之后就可以用新数据集X_d
(X discretized的缩写,表示“离散化的”)进行训练和测试,而不用原始数据集
。
1.6.2 实现OneR算法
OneR是一种简单预测样本类别的算法,它能为每一特征值找出最常见的类别。 OneR是One Rule的缩写,意为选择最显著的特征作为分类的唯一规则。虽然后文将介绍的算法会比它复杂得多,但对于某些现实中的数据集,这个算法的效果已经足够好了。
这个算法从迭代每个特征的每个取值开始,求出在各个类别中具有该特征值样本的数量,并记录特征值最常见的类别和预测错误的情况。
例如,如果某特征可以取0和1两个值,则首先找出所有该特征为0的样本。该特征为0的所有样本中,归类为A的样本有20个,归类为B的有60个,归类为C的有20个。那么类别B是该特征为0时最常见的类别,此时还有40个样本被归为其他的类。在该特征为0时,预测其类别为B,就会有40个样本与预测相悖,即错误率是40%。然后为该特征值为1的情况重复这一过程,再将之推广到其他特征中。
完成这一步后,求出错误累计数:对分布在错误类别中的各个特征值的样本数量求和。由此找出累计错误最少的特征,而它就是前面提到的One Rule,之后就可以用它为新样本分类。
在本例代码中,我们用一个函数来预测类别、计算特征值的错误累计值。实现这个函数需要导入两个依赖:defaultdict
和itemgetter
,在之前的代码中介绍过它们的用法。
from collections import defaultdict
from operator import itemgetter
然后我们创建函数定义,把数据集、所有样本的类别数组、指定特征的索引值和特征值作为参数。这个函数会迭代样本,然后统计每一特征值对应到指定类别的次数。再选出对于当前这对特征和值而言最常见的类别。
def train_feature_value(X, y_true, feature, value):
# 创建一个字典,统计预测为各个类别的频次
class_counts = defaultdict(int)
# 迭代样本,统计类别/值对的频次
for sample, y in zip(X, y_true):
if sample[feature] == value:
class_counts[y] += 1
# 从高到低排序,选择排名第一的类别
sorted_class_counts = sorted(class_counts.items(), key=itemgetter(1),
reverse=True)
most_frequent_class = sorted_class_counts[0][0]
# error是特征值与feature一致但没有归入最常见类的样本的数目
n_samples = X.shape[1]
error = sum([class_count for class_value, class_count in class_counts.items()
if class_value != most_frequent_class])
return most_frequent_class, error
最后一步是计算错误累计值。因为样本如果有上面指定的特征值,则会被OneR算法预测为最常见的类别,所以我们汇总所有归到其他类别(即非最常见类别)的情况作为错误累计值,用以指示训练样本分类错误的情况。
用这个函数迭代所有指定特征的值,汇总错误,记录每一个值的预测类别,然后就能计算出某特征的错误累计值。
这个函数以数据集、所有样本的类别数组和指定特征的索引值为参数,迭代所有特征值,然后找出最准确的特征值作为OneR。
def train(X, y_true, feature):
# 检查feature变量是否为有效值
n_samples, n_features = X.shape
assert 0 <= feature < n_features
# 列出这个特征的所有可能取值
values = set(X[:,feature])
# 存储函数返回的预测器数组
predictors = dict()
errors = []
for current_value in values:
most_frequent_class, error = train_feature_value
(X, y_true, feature, current_value)
predictors[current_value] = most_frequent_class
errors.append(error)
# 汇总用feature分类产生的错误
total_error = sum(errors)
return predictors, total_error
接下来详细看一下这个函数。
首先校验变量是否有效,然后列出指定特征的所有可能取值。下一行中的索引值从数据集中取出指定的特征列,并以数组形式将其返回。然后用集合函数去重。
values = set(X[:,feature_index])
然后创建一个存放预测器的字典,该字典以特征值为键,类别为值。比如键为1.5且值为2,表示当该特征的值为1.5时,样本会被分类为类别2。还要创建一个存放各类别累计错误值的列表。
predictors = dict()
errors = []
这个函数的主要功能是迭代特征的所有可能取值,传给之前所定义的train_feature_value()
函数,计算出特征最常见的类别和错误值并保存在上面的字典或列表中。
最后,汇总这条规则的累计错误值,并与预测类别一起作为返回值。
total_error = sum(errors)
return predictors, toral_error
1.6.3 测试算法功能
亲和性分析算法旨在探究数据集中内含的关联。而分类算法则旨在构建一个模型,使其能通过与已知样本比较给未知样本分类。两个算法用途迥异,评估方法也不一样。
为此,机器学习的工作流程会被划为两个阶段:训练阶段和测试阶段。在训练中,我们用数据集中的一部分样本训练模型。在测试中,我们要评估模型在数据集上的分类效果。因为模型是用来给未知样本分类的,若用测试数据训练模型会导致过拟合(overfitting),所以应该避免这一情况。
模型在训练数据集中表现优异,而在新样本中表现较差的情况就属于过拟合。只要记住一个原则,即可避免这一问题:不要用训练数据集测试算法。在后面的章节中,我们会介绍这个原则的复杂变体;而目前,评估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_train
和y_test
分别对应两份数据集中的所有样本的类别数组。
然后指定random_state
。指定random_state
可以使我们在输入同样的值时得出同样的划分。尽管这种划分看起来是随机的,然而其算法是确定的,输出的结果也是一致的。我建议你使用本书所取的random_state
值,这样你的结果才能与本书中的一致,这有助于你核对结果。如果你想使每次运行结果都不一样,那么把random_state
设为None
即可。
接下来,算出数据集中所有特征的预测器。注意此步只能使用测试数据集。迭代数据集中的所有特征,然后用之前定义好的函数训练预测器并汇总错误累计值。
all_predictors = {}
errors = {}
for feature_index in range(Xd_train.shape[1]):
predictors, total_error = train(Xd_train,
y_train,
feature_index)
all_predictors[feature_index] = predictors
errors[feature_index] = total_error
挑选出累计错误最低的特征作为One Rule。
best_feature, best_error = sorted(errors.items(), key=itemgetter(1))[0]
然后取这个特征的预测器作为model
。
model = {'feature': best_feature,
'predictor': all_predictors[best_feature]}
这里的模型是一个字典,表示作为One Rule的特征和基于该特征值的预测器是哪个。我们可以用这个模型选择未知样本中指定特征的值,然后再使用预测器,这样就得到了该样本的类别。下例代码即为这个过程。
variable = model['feature']
predictor = model['predictor']
prediction = predictor[int(sample[variable])]
为了一次预测多个样本的类别,我们可以把这个功能写成函数。我们还是用上面的这段代码,只不过要迭代数据集中的所有样本,以获取每个样本的预测类别。
def predict(X_test, model):
variable = model['feature']
predictor = model['predictor']
y_predicted = np.array([predictor
[int(sample[variable])] for sample
in X_test])
return y_predicted
把测试数据集传给这个函数进行预测。
y_predicted = predict(Xd_test, model)
与已知的样本类别相比较,计算模型的命中率。
accuracy = np.mean(y_predicted == y_test) * 100
print("The test accuracy is {:.1f}%".format(accuracy))
这个只有一条规则的算法命中率达到了65.8%,还算不赖!
1.7 本章小结
本章概述了用Python进行数据挖掘的一些概念与方法。如果你运行了本章示例中的代码(本书提供的代码包中包含了完整示例代码),那么其实你已经配置好了本书大多数示例代码所需的运行环境。然而术业有专攻,后面的章节也会用到其他的Python库,以执行特定领域的任务。
在本章中,我们用Jupyter Notebook运行代码,以即时展示小段代码的结果。它也是全书中都会用到的一种实用工具。
本章介绍了亲和性分析,并用它找出经常一起售出的商品。这种探究数据集内在关联的分析方法让我们可以洞察某套业务流程、某种环境或是某个场景中的现象。这种分析得出的信息会成为业务流程、医药研发和下一代人工智能的关键突破点。
另外,本章还以OneR算法为例介绍了分类问题。这个算法很简单,它只是从训练数据集中挑选出最佳特征值,并以此值的最常见类别作为预测类别。
想想如何实现一种OneR算法的变体,而该变体能同时考虑多对特征与值。请尝试实现并评估这种变体算法,以加深对本章内容的理解。注意,要把测试数据集和训练数据集隔离开来,否则会出现过拟合问题。
接下来的几章会展开介绍分类问题与亲和性分析中的概念,我们会用scikit-learn
包中的分类器进行机器学习,而不是亲自动手写算法。