Apriori算法是一种用于关联规则挖掘的代表性算法。从本节开始,我们已经进入了机器学习和数据挖掘相交叉的地带。
数据挖掘和机器学习的关系就好比,机器学习是数据挖掘的弹药库中一类相当庞大的弹药集。既然是一类弹药,其实也就是在说数据挖掘中肯定还有其他非机器学习范畴的技术存在。Apriori算法就属于一种非机器学习的数据挖掘技术。
在非机器学习的数据挖掘技术中,我们并不会去建立这样一个模型,而是直接从原数据集入手,设法分析出隐匿在数据背后的某些信息或知识。在后续介绍Apriori算法时,你会相当明显地感受到这一特点。
我们先从一个例子了解一下apriori原理。被大家所熟知的"啤酒尿布"的购买行为问题,其实就是一个具有关联性的行为。而发现这种关联性行为的方式,可以用apriori原理来实现。
从大规模数据集中寻找物品间的隐含关系被称作“关联分析”或者“关联规则学习”。而我们需要关注的问题是,如何使用更智能的方法,在更合理的时间范围内找到我们所需要的关联规则。
以此,引出了我们的aprori算法。
我们先来介绍几个概念。
关联分析,是一种在大规模数据集中寻找有趣关系的任务。在这种关系中,存在着两种形式:频繁项集或者关联规则。
频繁项集(frequent item sets),是经常出现在一块的物品的集合;关联规则(association rules),暗示两种物品之间可能存在很强的关系。
频繁项集是我们对原始数据格式化后的源数据集,而关联规则则是寻找源数据集关系得到的结果数据集。
{}为集合标识。
在下图中,{葡萄酒,尿布,豆奶}就是频繁项集的一个例子。在频繁项集中,我们也可以找到诸如“{尿布}->{葡萄酒}”的关联规则。这意味着如果有人买了尿布,那么他很可能也会买葡萄酒。
那么应该如何定义这些有趣的关系?谁来定义什么是有趣?当寻找频繁项集时,频繁的定义又是什么?
一个项集的**支持度(support)**被定义为数据集中包含该项集的记录所占的比例。支持度是针对项集,我们可以定义一个最小支持度,来保留满足最小支持度的项集。
如在上图中,{豆奶}的支持度为4/5,而{豆奶,尿布}的支持度为3/5
**可信度又称为置信度(confidence)**是针对一条关联规则定义的。
对于关联规则“{尿布}->{葡萄酒}”,它的可信度被定义为“支持度({尿布,葡萄酒})/支持度({尿布})”,计算该规则可信度为0.75,这意味着对于包含“尿布”的所有记录,我们的规则对其中75%的记录都适用。
在上面介绍的支持度和可信度被用来量化关联分析是否成功。但在实际操作的过程中,我们需要对物品所有的组合计算其支持度和可信度,当物品量上万时,上述的操作会非常非常慢。
我们该如何解决这种问题呢?
我们已经知道,大多数关联规则挖掘算法通常采用的一种策略是,将关联规则挖掘任务分解为如下两个主要的子任务。
频繁项集产生:其目标是发现满足最小支持度阈值的所有项集,这些项集称作频繁项集(frequent itemset)。
规则的产生:其目标是从上一步发现的频繁项集中提取所有高置信度的规则,这些规则称作强规则(strong rule)。
上图显示了4种商品所有可能的组合。对给定的集合项集{0,3},需要遍历每条记录并检查是否同时包含0和3,扫描完后除以记录总数即可得支持度。对于包含N种物品的数据集共有2的N次方-1种项集组合,即使100种,也会有1.26×10的30次方种可能的项集组成。
为降低计算时间,可用Apriori原理:如果某个项集是频繁的,那么它的所有子集也是频繁的。而我们需要使用的则是它的逆反定义:如果一个项集是非频繁集,那么它的所有超集也是非频繁的。
在下图中,已知阴影项集{2,3}是非频繁的,根据Apriori原理,我们知道{0,2,3},{1,2,3}以及{0,1,2,3}也是非频繁的,我们就不需要计算其支持度了。
这样就可以避免项集数目的指数增长,从而找到频繁项集。
我们已经知道关联分析的目标分为:发现频繁项集和发现关联规则。
首先需要找到频繁项集,然后才能获得关联规则。本节我们将重点放在如何发现频繁项集上。
Apriori算法会首先构建集合C1,C1是大小为1的所有候选项集的集合,然后扫描数据集来判断只有一个元素的项集是否满足最小支持度的要求。那么满足最低要求的项集构成集合L1。
而集合L1中的元素相互组合构成C2,C2再进一步过滤变成L2,以此循环直到Lk为空。
在上述对Apriori算法的描述中,我们可以很清楚的看到,需要构建相应的功能函数来实现该算法:
1.createC1 -构建集合C1
2.scanD -过滤集合C1,构建L1
3.aprioriGen -对集合Lk中的元素相互组合构建Ck+1
4.apriori -集成函数
我们按照该逻辑来实现我们的apriori算法,并找到频繁项集。
在当前工作目录下,新建文件 apriori.py,添加如下代码:
# -*-coding:utf-8 -*-
from numpy import *
"""
函数说明:加载数据集
"""
def loadDataSet():
return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]
"""
函数说明:构建集合C1。即所有候选项元素的集合。
parameters:
dataSet -数据集
return:
frozenset列表
output:
[forzenset([1]),forzenset([2]),……]
"""
def createC1(dataSet):
C1 = [] #创建一个空列表
for transaction in dataSet: #对于数据集中的每条记录
for item in transaction:#对于每条记录中的每一个项
if not [item] in C1: #如果该项不在C1中,则添加
C1.append([item])
C1.sort() #对集合元素排序
return list(map(frozenset,C1)) #将C1的每个单元列表元素映射到forzenset()
"""
函数说明:构建符合支持度的集合Lk
parameters:
D -数据集
Ck -候选项集列表
minSupport -感兴趣项集的最小支持度
return:
retList -符合支持度的频繁项集合列表L
supportData -最频繁项集的支持度
"""
def scanD(D,Ck,minSupport):
ssCnt = {} #创建空字典
for tid in D: #遍历数据集中的所有交易记录
for can in Ck: #遍历Ck中的所有候选集
if can.issubset(tid): #判断can是否是tid的子集
if not can in ssCnt: ssCnt[can] = 1 #如果是记录的一部分,增加字典中对应的计数值。
else: ssCnt[can] += 1
numItems = float(len(D)) #得到数据集中交易记录的条数
retList = [] #新建空列表
supportData = {} #新建空字典用来存储最频繁集和支持度
for key in ssCnt:
support = ssCnt[key] / numItems #计算每个元素的支持度
if support >= minSupport: #如果大于最小支持度则添加到retList中
retList.insert(0,key)
supportData[key] = support #并记录当前支持度,索引值即为元素值
return retList,supportData
并添加main函数:
if __name__ == '__main__':
dataSet = loadDataSet()
print("数据集:\n",dataSet)
C1 = createC1(dataSet)
print("候选集C1:\n",C1)
D = list(map(set,dataSet)) #将数据转换为集合形式
L1,supportData = scanD(D,C1,0.5)
print("满足最小支持度为0.5的频繁项集L1:\n",L1)
在当前目录下,运行文件 python apriori.py,可以看到运行结果:
其中forzenset为不可变集合,它的值是不可变的,好处是它可以作为字典的key,也可以作为其他集合的元素。
我们这里必须使用forzenset而不是set,就是因为我们需要将这些集合作为字典键值使用。
至此,我们完成了C1和L1的构建。那么对于L1中元素的组合,我们需要先引入一个组合定义。
假定我们的L1中元素为[{0},{1},{2}],我们想要aprioriGen函数生成元素的组合C2,即[{0,1},{0,2},{1,2}]。
这是单个项的组合。那么我们考虑一下,如果想利用[{0,1},{0,2},{1,2}]来创建三元素的候选项集C3呢?
如果依旧按照单个项的组合方法,将每两个集合合并,就会得到{0,1,2},{0,1,2},{0,1,2}。也就是说,我们重复操作了3次。
接下来还需要扫描该结果来得到非重复结果,但我们要确保的是遍历列表的次数最少。
那么如何解决这个问题呢?我们观察到,如果比较集合{0,1},{0,2},{1,2}的第一个元素并只对第一个元素相同的集合求并操作,我们会得到{0,1,2}相同的结果,但只操作了1次!
这样的话就不需要遍历列表来寻找非重复值。也能够实现apriori算法的原理。
当我们创建三元素的候选集时,k=3,但此时待组合的频繁项集元素为f=2,即我们只需比较前f-1个元素,也就是前k-2个元素。
打开apriori.py文件,在main函数的上面添加如下代码:
"""
函数说明:构建集合Ck
parameters:
Lk -频繁项集列表L
k -候选集的列表中元素项的个数
return:
retList -候选集项列表Ck
"""
def aprioriGen(Lk,k):
retList = [] #创建一个空列表
lenLk = len(Lk) #得到当前频繁项集合列表中元素的个数
for i in range(lenLk): #遍历所有频繁项集合
for j in range(i+1,lenLk): #比较Lk中的每两个元素,用两个for循环实现
L1 = list(Lk[i])[:k-2]; L2 = list(Lk[j])[:k-2] #取该频繁项集合的前k-2个项进行比较
#[注]此处比较了集合的前面k-2个元素,需要说明原因
L1.sort(); L2.sort() #对列表进行排序
if L1 == L2:
retList.append(Lk[i]|Lk[j]) #使用集合的合并操作来完成 e.g.:[0,1],[0,2]->[0,1,2]
return retList
"""
函数说明:apriori算法实现
parameters:
dataSet -数据集
minSupport -最小支持度
return:
L -候选项集的列表
supportData -项集支持度
"""
def apriori(dataSet,minSupport=0.5):
C1 = createC1(dataSet)
D = list(map(set,dataSet)) #将数据集转化为集合列表
L1, supportData = scanD(D,C1,minSupport) #调用scanD()函数,过滤不符合支持度的候选项集
L = [L1] #将过滤后的L1放入L列表中
k = 2 #最开始为单个项的候选集,需要多个元素组合
while(len(L[k-2])>0):
Ck = aprioriGen(L[k-2],k) #创建Ck
Lk, supK = scanD(D,Ck,minSupport) #由Ck得到Lk
supportData.update(supK) #更新支持度
L.append(Lk) #将Lk放入L列表中
k += 1 #继续生成L3,L4....
return L, supportData
并修改main函数:
if __name__ == '__main__':
dataSet = loadDataSet()
print("数据集:\n",dataSet)
C1 = createC1(dataSet)
print("候选集C1:\n",C1)
D = list(map(set,dataSet)) #将数据转换为集合形式
L1,supportData = scanD(D,C1,0.5)
print("满足最小支持度为0.5的频繁项集L1:\n",L1)
L,suppData = apriori(dataSet)
print("满足最小支持度为0.5的频繁项集列表L:\n",L)
print("L2:\n",L[1])
print("使用L2生成的C3:\n",aprioriGen(L[1],3))
L,suppData = apriori(dataSet)
print("满足最小支持度为0.7的频繁项集列表L:\n",L)
在当前目录下,运行文件 python apriori.py,可以看到运行结果:
可以看到,使用L2生成的候选项集C3,只对k-2个项相同的元素合并。
那其实不生成{1,3,5}的原因是,由于{1,5}是非频繁项,那么其超集均为非频繁项。故此极大减少计算量。
到此我们也得到了我们的最频繁项集列表。
上一节介绍了如何使用Apriori算法来发现频繁集,现在需要解决的问题则是如何找出关联规则。
在原理讲解中,我们找到诸如“{尿布}->{葡萄酒}”的关联规则。这意味着如果有人买了尿布,那么他很可能也会买葡萄酒。
但是,这一条反过来,却不一定成立。有可能买葡萄酒的人,根本不会去买尿布。
那么,成立的定义是什么呢?还记得我们介绍的可信度度量值,对于关联规则的量化方法,则是使用可信度。
对于关联规则“P->H”,它的可信度被定义为“支持度(P | H)/支持度§”。其中|符合在python中为合并符。
下图展示了一个项集{0,1,2,3}能够产生的所有关联规则。与找到频繁项集方法类似,我们对生成的规则进行过滤,对于不满足最小可信度要求的,则去掉该规则。
我们看到,仅仅4个元素的项集生成了15个关联规则,如果能够减少规则数目来确保问题的可解性,那么计算量也会大大减小。
可以观察到,如果某条规则不满足最小可信度要求,那么该规则的所有子集也不会满足最小可信度要求。
如图中,规则“0,1,2->3”不满足要求,那么任何左部为{0,1,2}子集的规则均不满足要求。
在找出关联规则的过程中,首先从一个频繁项集开始,接着创建一个规则列表,其中规则右部只包含一个元素,然后对这些规则进行测试。
接下来,合并所有剩余规则来创建一个新的规则列表,其中规则右部包含两个元素。直到规则左右剩余一个元素。
同样,我们需要创建相应的功能函数来实现:
1.rulesFromConseq -从频繁项集生成规则列表
2.calcConf -对规则进行测试并过滤
3.generateRules -集成函数
我们来看下这种方法的实际效果,打开文件apriori.py文件,在main函数的上面添加如下代码:
"""
函数说明:规则构建函数
parameters:
freqSet -频繁项集合
H -可以出现在规则右部的元素列表
supportData -支持度字典
brl -规则列表
minConf -最小可信度
return:
null
"""
def rulesFromConseq(freqSet,H,supportData,brl,minConf=0.7):
m = len(H[0]) #得到H中的频繁集大小m
if (len(freqSet) > (m+1)): #查看该频繁集是否大到可以移除大小为m的子集
Hmp1 = aprioriGen(H, m+1) #构建候选集Hm+1,Hmp1中包含所有可能的规则
Hmp1 = calcConf(freqSet,Hmp1,supportData,brl,minConf) #测试可信度以确定规则是否满足要求
if (len(Hmp1)>1): #如果不止一条规则满足要求,使用函数迭代判断是否能进一步组合这些规则
rulesFromConseq(freqSet,Hmp1,supportData,brl,minConf)
"""
函数说明:计算规则的可信度,找到满足最小可信度要求的规则
parameters:
freqSet -频繁项集合
H -可以出现在规则右部的元素列表
supportData -支持度字典
brl -规则列表
minConf -最小可信度
return:
prunedH -满足要求的规则列表
"""
def calcConf(freqSet,H,supportData,brl,minConf=0.7):
prunedH = [] #为保存满足要求的规则创建一个空列表
for conseq in H:
conf = supportData[freqSet]/supportData[freqSet-conseq] #可信度计算[support(PUH)/support(P)]
if conf>=minConf:
print(freqSet-conseq,'-->',conseq,'可信度为:',conf)
brl.append((freqSet-conseq,conseq,conf)) #对bigRuleList列表进行填充
prunedH.append(conseq) #将满足要求的规则添加到规则列表
return prunedH
"""
函数说明:关联规则生成函数
parameters:
L -频繁项集合列表
supportData -支持度字典
minConf -最小可信度
return:
bigRuleList -包含可信度的规则列表
"""
def generateRules(L,supportData,minConf=0.7):
bigRuleList = [] #创建一个空列表
for i in range(1,len(L)): #遍历频繁项集合列表
for freqSet in L[i]: #遍历频繁项集合
H1 = [frozenset([item]) for item in freqSet] #为每个频繁项集合创建只包含单个元素集合的列表H1
if (i>1): #要从包含两个或者更多元素的项集开始规则构建过程
rulesFromConseq(freqSet,H1,supportData,bigRuleList,minConf)
else: #如果项集中只有两个元素,则计算可信度值,(len(L)=2)
calcConf(freqSet,H1,supportData,bigRuleList,minConf)
return bigRuleList
并修改main函数为:
if __name__ == '__main__':
dataSet = loadDataSet()
print("数据集:\n",dataSet)
C1 = createC1(dataSet)
print("候选集C1:\n",C1)
L,suppData = apriori(dataSet)
print("满足最小支持度为0.5的频繁项集列表L:\n",L)
print("满足最小可信度为0.7的规则列表为:")
rules = generateRules(L,suppData,0.7)
print(rules)
print("满足最小可信度为0.5的规则列表为:")
rules1 = generateRules(L,suppData,0.5)
print(rules1)
在当前目录下,运行文件 python apriori.py,可以看到运行结果:
更改最小置信度后,可以获得更多的规则。
我们都用过搜索引擎,会发现这样一个功能:输入一个单词或者单词的一部分,搜索引擎会自动补全查询词项。用户甚至都不知道搜索引擎推荐的东西是否存在,反而会去查找推荐词项。比如在百度输入“怎么才能”开始查询时,会出现诸如“怎么才能减肥”之类的推荐结果。
为了给出这些推荐查询词,搜索引擎公司的研究人员通过查看互联网上的用词来找出经常在一块出现的词对,这需要一种高效发现频繁集的方法。
在前面我们讲过使用Apriori算法来发现数据集中的频繁项集。对于不同元素项间的组合,我们使用Apriori原理减少在数据库上进行检查的集合的数目,从而避免了大量的计算。但每次增加频繁项集的大小,由于Apriori算法对于每个潜在的频繁项集都会扫描数据集判定给定模式是否频繁,会多次扫描整个数据集。当数据集很大时,这会显著降低频繁项集发现的速度。
FP-Growth(Frequent-Pattern)算法基于Apriori构建,但在完成相同任务时采用了一些不同的技术。
不同于Apriori算法的”产生-测试”,这里的任务是将数据集存储在一个特定的称做FP树的结构之后发现频繁项集或者频繁项对,即常在一块出现的元素项的集合FP树。
常见的频繁项集挖掘算法有两类,一类是Apriori算法,另一类是FP-Growth。
Apriori通过不断的构造候选集、筛选候选集挖掘出频繁项集,需要多次扫描原始数据,当原始数据较大时,磁盘I/O次数太多,效率比较低下。FP-Growth算法则只需扫描原始数据两遍,通过FP树数据结构对原始数据进行压缩,效率较高。因此FP-Growth算法的执行速度要比Apriori算法快得多。
FP-Growth算法主要分为两个步骤:
- 构建FP树
- 从FP树中挖掘频繁项集
我们已经知道,FP-Growth算法将数据存储在一种称为FP树的数据结构中。那么,FP树长什么样呢?
FP(Frequent Pattern,频繁模式)树看上去和其他的树结构类似,但它通过**链接(link)**来连接相似元素,被连起来的元素项可以看作一个链表。
如下图中,虚线所示,每个被虚线相连的元素即为相似元素。通过这种相似元素的相连(node link),我们可以快速的发现相似项的位置。
我们需要注意的是,与搜索树不同的是,一个元素项可以在一棵FP树中重复出现。FP树会存储项集的出现频率,每个项集以路径的方式存储在树中。
由于不同的集合可能会有若干个相同的项,因此它们的路径可能部分重叠,即存在相似元素的集合会共享树的一部分。只有当集合之间完全不同时,树才会分叉。
树节点上给出集合中的单个元素及其在序列中的出现次数,路径则会给出该序列的出现次数。
理论看起来可能有些迷糊。我们通过一个例子来具体了解下,下表中为上图所示FP树的数据。
在图中我们看到,元素项z出现了5次,集合{r,z}出现了1次。路径的出现次数由路径的末端节点次数决定。
此时我们可以看出,元素项z除了在集合{r,z}中出现了一次,一定是与其他符合或者自身出现了4次。集合{t,s,y,x,z}出现了2次,集合{t,r,y,x,z}出现了1次,也就是说z一定是单独出现了1次。
将我们的结论与表中数据比较,发现集合005中{t,r,y,x,z}出现了1次,但少了q和p元素项。那么在构建FP树的时候,它们去哪了呢?
还记得我们在Apriori算法中提到的支持度定义,即最小阈值,低于最小阈值的元素项被认为是不频繁的。这里同样需要对数据项进行过滤,如果项集的出现次数小于我们设定的阈值,则丢掉。因此,q和p被del了。
接下来,我们学习如何构建FP树以及从FP树中挖掘频繁项集。
构建FP树是FP-Growth算法的第一步。我们已经知道,FP树的节点会存储节点的值以及出现次数,并需要记录相似节点的位置。需要为其定义一个类来封装这么多的内容。
在当前目录下,新建文件,添加如下代码:
"""
类说明:FP树数据结构
function:
__init__ -初始化节点
nameValue -节点值
numOccur -节点出现次数
parentNode -父节点
inc -对count变量增加给定值
disp -将树以文本形式显示
"""
class treeNode:
def __init__(self, nameValue, numOccur, parentNode):
self.name = nameValue #存放节点名字
self.count = numOccur #节点计数值
self.nodeLink = None #链接相似的元素值
self.parent = parentNode #当前节点的父节点
self.children = {} #空字典变量,存放节点的子节点
def inc(self, numOccur):
self.count += numOccur
def disp(self, ind=1): #ind为节点的深度
print(' '*ind, self.name, ' ', self.count)
for child in self.children.values():
child.disp(ind+1) #递归遍历树的每个节点
并添加main函数:
if __name__ == '__main__':
rootNode = treeNode('pyramid',9,None)
rootNode.children['eye'] = treeNode('eye',13,None)
rootNode.disp()
运行后,可以看到,我们构建一个有两个结点的树。
其中,nodelink 和 parent目前还未使用。
nodelink记录相似节点的位置,parent记录当前节点的父节点,用于寻找频繁项集。类中还包含一个空字典变量children,用来存放节点的子节点。
构建好FP树所需的数据结构之后,下面就可以构建FP树了。
我们除了要构建FP树之外,还需要一个头指针表来指向给定类型的第一个实例。下图是头指针表的示意图。利用头指针表,可以快速找到FP树中一个给定类型的所有元素。
我们使用一个字典作为数据结构,来保存头指针表。该结构还可以用来保存FP树中每类元素的总数。
FP-Growth算法会扫描数据集两次。第一次扫描数据集,记录每个独立元素项的出现次数,并过滤不满足最小支持度的元素项,剩余的元素项即为频繁项,存储在头指针表中。第二次扫描数据集,读入每个项集中的频繁项,并将其添加到一条已经存在的路径中。如果该路径不存在,则创建一条新路径。
这里,我们需要注意一个问题,FP-Growth算法会尽可能的将数据压缩,假设有集合{z,x,y}和{y,z,r},会被添加到两条路径中。但在FP树中,相同的项应该只表示一次。如何解决这个问题呢?
在将集合添加到树之前,需要对每个集合进行排序。排序基于元素项的绝对出现频率来进行。我们使用头指针表中存储的元素项的出现次数,对上节中表数据进行排序,得到以下结果:
这样,我们就可以构建FP树了。从空集开始,向其中不断添加频繁项集。过滤、排序后的集合依此加入到树中。
添加的过程如下图所示。
我们已经大致了解了FP-Growth算法构建FP树的思想,下面我们需要定义相应的功能函数:
1.createTree -对数据过滤、排序
2.updateTree -将处理后的集合加入到树中
3.updateHeader -对新加入的类型节点需要更新头指针表中的实例,并更新该类型节点的nodeLink链表
打开文件fp.py,在main函数之前加入如下代码:
"""
函数说明:FP树构建函数
parameters:
dataSet -字典型数据集
minSup -最小支持度
return:
retTree -FP树
headerTable -头指针表
"""
def createTree(dataSet, minSup = 1):
headerTable = {} #创建空字典,存放头指针
for trans in dataSet: #遍历数据集
for item in trans: #遍历每个元素项
headerTable[item] = headerTable.get(item,0)+dataSet[trans] #以节点为key,节点的次数为值
tmpHeaderTab = headerTable.copy()
for k in tmpHeaderTab.keys(): #遍历头指针表
if headerTable[k] < minSup: #如果出现次数小于最小支持度
del(headerTable[k]) #删掉该元素项
freqItemSet = set(headerTable.keys()) #将字典的键值保存为频繁项集合
if len(freqItemSet) == 0: return None, None #如果过滤后的频繁项为空,则直接返回
for k in headerTable:
headerTable[k] = [headerTable[k], None] #使用nodeLink
#print(headerTable)
retTree = treeNode('Null Set',1,None) #创建树的根节点
for tranSet, count in dataSet.items(): #再次遍历数据集
localD = {} #创建空字典
for item in tranSet:
if item in freqItemSet: #该项是频繁项
localD[item] = headerTable[item][0] #存储该项的出现次数,项为键值
if len(localD) > 0:
orderedItems = [v[0] for v in sorted(localD.items(),key=lambda p: p[1],reverse = True)] #基于元素项的绝对出现频率进行排序
updateTree(orderedItems, retTree, headerTable, count) #使用orderedItems更新树结构
return retTree, headerTable
"""
函数说明:FP树生长函数
parameters:
items -项集
inTree -树节点
headerTable -头指针表
count -项集出现次数
return:
None
"""
def updateTree(items, inTree, headerTable, count):
if items[0] in inTree.children: #首先测试items的第一个元素项是否作为子节点存在
inTree.children[items[0]].inc(count) #如果存在,则更新该元素项的计数
else:
inTree.children[items[0]] = treeNode(items[0],count,inTree) #如果不存在,创建一个新的treeNode并将其作为子节点添加到树中
if headerTable[items[0]][1] == None: #将该项存到头指针表中的nodelink
headerTable[items[0]][1] = inTree.children[items[0]] #记录nodelink
else:
updateHeader(headerTable[items[0]][1], inTree.children[items[0]]) #若已经存在nodelink,则更新至链表尾
if len(items) > 1:
updateTree(items[1::],inTree.children[items[0]],headerTable,count) #迭代,每次调用时会去掉列表中的第一个元素
"""
函数说明:确保节点链接指向树中该元素项的每一个实例
parameters:
nodeToTest -需要更新的头指针节点
targetNode -要指向的实例
return:
None
"""
def updateHeader(nodeToTest, targetNode):
while(nodeToTest.nodeLink!=None): #从头指针表的nodelink开始,直到达到链表末尾
nodeToTest = nodeToTest.nodeLink
nodeToTest.nodeLink = targetNode #记录当前元素项的实例
"""
FP树测试函数
"""
def testFPtree():
simpDat = loadSimpDat()
initSet = createInitSet(simpDat)
print("字典数据集:\n",initSet)
myFPtree, myHeaderTab = createTree(initSet,3)
myFPtree.disp()
return myHeaderTab
并添加初始的数据集,并将数据转换成字典类型:
def loadSimpDat():
simpDat = [['r', 'z', 'h', 'j', 'p'],
['z', 'y', 'x', 'w', 'v', 'u', 't', 's'],
['z'],
['r', 'x', 'n', 'o', 's'],
['y', 'r', 'x', 'z', 'q', 't', 'p'],
['y', 'z', 'x', 'e', 'q', 's', 't', 'm']]
return simpDat
"""
函数说明:从列表到字典的类型转换函数
parameters:
dataSet -数据集列表
return:
retDict -数据集字典
"""
def createInitSet(dataSet):
retDict = {}
for trans in dataSet:
retDict[frozenset(trans)] = 1 #将列表项转换为forzenset类型并作为字典的键值,值为该项的出现次数
return retDict
修改main函数为:
if __name__ == '__main__':
testFPtree()
运行文件后,可以看到我们构建的FP树的结构,我们可以验证一下与上图中所示的树是否等价。
上一节实现了构建FP树的代码。有了FP树,我们就可以抽取频繁集了。
与Apriori算法类似,首先从单元素项集合开始,然后在此基础上逐步构建更大的集合。与Apriori算法不同的是,这里使用FP树实现上述过程,而不是每次需要遍历原始数据集来验证数据的支持度。
从FP树中抽取频繁项集主要分为三个步骤:
(1)从FP树中获得条件模式基
(2)利用条件模式基,为每一个条件模式基创建相对应的条件FP树
(3)迭代重复上述两个步骤,直到树中只包含空集元素项为止。
条件模式基(conditional pattern base,CPB)是以所查找元素项为结尾的路径集合。每一条路径其实都是所查找元素项的前缀路径(prefix path)。前缀路径是介于所查找元素项与数根节点之间的所有内容。
下表列出了上节中每一个频繁项的所有前缀路径以及路径频繁度。每条路径都与一个计数值关联,该计数值等于起始元素项的计数值,该计数值给了每条路径上起始元素项的数据。
抽取条件基的步骤大致如下:
1.从头指针表(header table)的最下面的元素项开始,构造每个元素项的条件模式基
2.顺着头指针表中元素项的链表,找出所有包含该元素项的前缀路径,这些前缀路径就是该元素项的条件模式基
3.所有这些条件模式基的计数值为该路径上元素项的计数值,也称作频繁度
如下图中,是对头指针表HeaderTable创建的FP树,并为每个频繁项构建条件模式基。包含p的其中一条路径是fcamp,该路径中p的频繁度为2,则该条件模式基fcam的频繁度为2。
接下来,需要定义相应的功能函数。
该函数应该能完成对头指针表中包含的指向相同类型元素链表的指针进行访问,遍历该链表,对链表上的每个项,向上回溯这棵树直到根节点为止。
打开我们的文件fp.py,添加如下代码:
这里我们创建了两个函数,由于对元素项的回溯是一个重复操作,单独作为一个函数会更好。在对相同类型元素链表进行遍历时,用到了nodelink变量;在回溯时,用到了parent变量。从代码中来体会这两个变量的作用。
"""
函数说明:上溯FP树
parameters:
leafNode -节点
prefixPath -该节点的前缀路径
return:
None
"""
def ascendTree(leafNode, prefixPath):
if leafNode.parent != None: #如果该节点的父节点存在
prefixPath.append(leafNode.name) #将其加入到前缀路径中
ascendTree(leafNode.parent,prefixPath) #迭代调用自身上溯
"""
函数说明:遍历某个元素项的nodelink链表
parameters:
basePat -头指针表中元素
treeNode -该元素项的nodelist链表节点
return:
condPats -该元素项的条件模式基
"""
def findPrefixPath(basePat, treeNode):
condPats = {} #创建空字典,存放条件模式基
while treeNode != None:
prefixPath = []
ascendTree(treeNode, prefixPath) #寻找该路径下实例的前缀路径
if len(prefixPath)>1: #如果有前缀路径
condPats[frozenset(prefixPath[1:])] = treeNode.count #记录该路径的出现次数,出现次数为该路径下起始元素项的计数值
#此处需说明
treeNode = treeNode.nodeLink
return condPats
def testPrefix(myHeaderTab):
print(findPrefixPath('x',myHeaderTab['x'][1]))
print(findPrefixPath('z',myHeaderTab['z'][1]))
print(findPrefixPath('r',myHeaderTab['r'][1]))
修改我们的main函数:
if __name__ == '__main__':
myHeaderTab = testFPtree()
testPrefix(myHeaderTab)
运行后,可以看到元素项’x’,‘z’,'r’的条件模式基,可以与表中结果对照。
有了条件模式基之后,我们就可以创建条件FP树。
对于每一个频繁项,都要为其创建一棵条件FP树。
创建条件FP树的步骤如下:
使用上一步得到的条件模式基作为输入数据,来为每一个条件模式基构造条件FP树。然后,我们会递归的发现频繁项,发现条件模式基,以及发现另外的条件树。
同创建FP树一样,条件FP树同样会将条件模式基中不符合支持度的元素项过滤。如图中,{s},{r}在t的条件树中分别出现了2次和1次,不符合最小支持度为3,也就是非频繁的。对t创建好FP树后,接下来对{t,z},{t,y},{t,x}挖掘对应的条件树。该过程重复进行,直到条件树中没有元素为止。
打开我们的文件fp.py,添加如下代码:
这里我们寻找频繁项集之前,先对头指针表中的项按照其出现频率进行从小到大的排序。这是因为由于更频繁的元素项放在树的上层会被更多的共享,否则会造成频繁出现的元素项出现在树的分支中,无法共用前缀。
{f,a,c,m,p}和{a,f,c,p,m}在FP树中应被看作是同一路径,但由于没有对头指针表进行排序操作,无法共用前缀。造成多余的分叉。
"""
函数说明:在FP树中寻找频繁项
parameters:
inTree -FP树
headerTable -当前元素前缀路径的头指针表
minSup -最小支持度
preFix -当前元素的前缀路径
freqItemList -频繁项集
return:
None
"""
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
#print("mineTreeHander:",headerTable)
bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1][0])] #按照出现次数从小到大排序
#print("bigL:",bigL)
for basePat in bigL: #从最少次数的元素开始
newFreqSet = preFix.copy() #复制前缀路径
newFreqSet.add(basePat) #将当前元素加入路径
#print ('finalFrequent Item: ',newFreqSet) #append to set
freqItemList.append(newFreqSet) #将该项集加入频繁项集
condPattBases = findPrefixPath(basePat, headerTable[basePat][1]) #找到当前元素的条件模式基
#print ('condPattBases :',basePat, condPattBases)
myCondTree, myHead = createTree(condPattBases, minSup) #过滤低于阈值的item,基于条件模式基建立FP树
#print ('head from conditional tree: ', myHead)
if myHead != None: #如果FP树中存在元素项
#print ('conditional tree for: ',newFreqSet)
#myCondTree.disp(1)
# 递归的挖掘每个条件FP树,累加后缀频繁项集
mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList) #递归调用自身函数,直至FP树中没有元素
"""
找到频繁集测试函数
"""
def testAll():
simpDat = loadSimpDat()
initSet = createInitSet(simpDat)
myTree, headerTab = createTree(initSet,3)
freqItems = []
mineTree(myTree,headerTab,3,set([]),freqItems)
print("频繁项集为:\n",freqItems)
修改main函数为:
if __name__ == '__main__':
testAll()
运行后可以看到,返回的项集即为我们要寻找的频繁项集,可以检查一下返回的项集是否与上节创建的条件树匹配。
到此,完整的FP-Growth算法已经可以运行。
支持度(Support)
支持度表示项集{X,Y}在总项集里出现的概率。公式为:
Support(X→Y) = P(X,Y) / P(I) = P(X∪Y) / P(I) = num(XUY) / num(I)
其中,I表示总事务集。num()表示求事务集里特定项集出现的次数。
比如,num(I)表示总事务集的个数
num(X∪Y)表示含有{X,Y}的事务集的个数(个数也叫次数)。
置信度 (Confidence)
置信度表示在先决条件X发生的情况下,由关联规则”X→Y“推出Y的概率。即在含有X的项集中,含有Y的可能性,公式为:
Confidence(X→Y) = P(Y|X) = P(X,Y) / P(X) = P(XUY) / P(X)
提升度表示含有X的条件下,同时含有Y的概率,与Y总体发生的概率之比。
Lift(X→Y) = P(Y|X) / P(Y)
由于提升度Lift(X→Y) =1,表示X与Y相互独立,即是否有X,对于Y的出现无影响。也就是说,是否购买咖啡,与有没有购买茶叶无关联。即规则”茶叶→咖啡“不成立,或者说关联性很小,几乎没有,虽然它的支持度和置信度都高达90%,但它不是一条有效的关联规则。
满足最小支持度和最小置信度的规则,叫做“强关联规则”。然而,强关联规则里,也分有效的强关联规则和无效的强关联规则。
如果Lift(X→Y)>1,则规则“X→Y”是有效的强关联规则。
如果Lift(X→Y) <=1,则规则“X→Y”是无效的强关联规则。
特别地,如果Lift(X→Y) =1,则表示X与Y相互独立。