目录
算法简介
构建FP树
挖掘频繁项集
FP-growth算法的应用我们经常接触到。比如,你在百度的搜索框内输入某个字或词,搜索引擎会自动补全查询词项,而这些词项都是和搜索词经常一起出现的。
FP-growth算法被用来挖掘频繁项集,也就是说从已给的多条数据记录中挖掘出哪些项是频繁一起出现的。该算法适用于标称型数据,即离散型数据。它比Apriori算法更高效,因为该算法只需要对数据库进行两次扫描,而Apriori算法对于每个潜在的频繁项集都会扫描数据集判定给定模式是否频繁。
注:最后有python代码汇总。
举个例子说明什么是项,项集,频繁项集,以及支持度。
有下面这样一份数据记录。
事务ID | 事务中的元素项 |
---|---|
001 | r,z,h,j,p |
002 | z,y,x,w,v,u,t,s |
003 | z |
004 | r,x,n,o,s |
005 | y,r,x,z,q,t,p |
006 | y,z,x,e,q,s,t,m |
这份数据一共有6条记录,每条记录中的元素就是项,第1条记录中有5个项,分别为:r,z,h,j,p。项的集合就是项集,比如,[r]是一个项集,[r,z]是一个项集,[r,z,h,j,p]也是一个项集,项集是指项的任意组合。而频繁项集是指,那些在记录中经常一起出现的项组合成的集合。那么,“经常”是怎么衡量的呢?这里就涉及到支持度的概念。支持度是说出现的次数,它可以针对单个项,也可以针对项的组合,在这6条数据记录中,r 一共出现了3次,所以 r 的支持度是3,项集(r,x)出现了2次,所以(r,x)的支持度是2。
FP-growth算法挖掘频繁项集的基本过程如下:
(1)构建FP树。
(2)从FP树中挖掘频繁项集。
FP代表频繁模式(Frequent Pattern)。
我们先看看FP树长什么样子。以下这棵FP树是根据上面那份数据记录建立的。
可以看出,一棵FP树看上去与计算机科学中的其他树结构类似,但是它包含着连接相似节点的链接(图中的红色虚线部分)。 相似节点是指前缀路径不同的项,如在上面的FP树中 r 的前缀路径有3个,分别为(z),(z,x,y),(x,s),于是,这些 r 们就叫做相似节点。后面用python构建FP树时会创建一个字典结构存储这些相似元素。
FP树是怎么构建的呢?
在构建之前,我们要先定义一个类,用来保存树的每一个节点。
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):
print ' '*ind,self.name,' ',self.count #展示节点名称和出现的次数
for child in self.children.values():
child.disp(ind+1) #打印时,子节点的缩进比父节点更深一级
运行下面这段代码:
rootNode=treeNode('pyramid',9,None) #创建节点
rootNode.children['eye']=treeNode('eye',13,None) #增加子节点
rootNode.children['phoenix']=treeNode('phoenix',3,None) #增加另一个子节点
rootNode.disp() #展示树
运行结果:
由于“eye”和“phoenix”都是”pyramid“,所以在展示树的结构时,“eye”和“phoenix”的缩进深度相同,都比”pyramid“的缩进深度更深一级。
除此之外,我们还需要把原始事务数据集处理成字典的形式,方面后面的函数调用。
定义两个函数,如下:
from collections import OrderedDict
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
def createInitSet(dataSet):
retDict=OrderedDict()
for trans in dataSet:
retDict[frozenset(trans)]=1
return retDict
函数loadSimpDat()把多条数据记录存储成列表的形式,函数createInitSet(dataSet)把每条数据记录冻结(frozenset函数)后作为字典的键,而每个键对应的值都是1。
我们运行一下看看结果。
simpDat=loadSimpDat()
simpDat
initSet=createInitSet(simpDat)
initSet
下面开始构建FP树。(主要从解读实现代码的角度来介绍这个过程)
FP树第一次扫描数据库是为了获得每个元素项的出现频率。实现这一步的代码如下(注:代码中的dataSet 是经过上面所说的createInitSet(dataSet)函数处理后的数据结果,即一个字典结构):
headerTable={}#用来存储每项元素及其出现次数
for trans in dataSet:#遍历每条记录
for item in trans:#遍历每条记录的每项元素
headerTable[item]=headerTable.get(item,0)+dataSet[trans]#计算每项元素的出现次数
print headerTable
print ("headerTable's length: %s" % len(headerTable))
这时headerTable包含17个元素:
接下来,去掉不满足最小支持度的元素项。
for k in headerTable.keys():
if headerTable[k]<3:#这里的3是指最小支持度的取值,可根据实际情况改变
del(headerTable[k])#如果某项元素的支持度小于最小支持度,从headerTable中删掉该元素
freqItemSet=set(headerTable.keys())#freqItemSet中的每一项元素的支持度均大于或等于最小支持度
print headerTable
print ("headerTable's length: %s" % len(headerTable))
去掉不满足最小支持度的元素项后,headerTable已由原来的17个元素减少为6个:
再下一步构建FP树。
freqItemSet存储的都是符合条件的元素。如果freqItemSet非空,证明确实有符合条件的元素。从前面的运行结果可以知道,headerTable里面每一个键对应的值是该元素在所有的事务数据记录中出现的次数。现在,我们在每个键对应的值中增加一个“None”,为后面的存储相似元素做准备。
if len(freqItemSet)!=0:
for k in headerTable:
headerTable[k]=[headerTable[k],None]
headerTable
headerTable调整后结果如下:
构建FP树的思路是这样的:读入每个项集也就是每条记录,并将其添加到一条已经存在的路径中。如果该路径不存在,则创建一条新路径。假设有集合{z,x,y}和{y,z,r},为了保证相同项只出现一次,需要对每条记录里的元素项进行排序。在每条记录中,这种排序是根据每个元素出现的次数进行的,也就是说出现次数越多,排位越前。
for tranSet,count in dataSet.items():#遍历每一条事务数据
localD={}
for item in tranSet:#遍历这条数据中的每个元素
if item in freqItemSet:#过滤每条记录中支持度小于最小支持度的元素
localD[item]=headerTable[item][0]#把headerTable中记录的该元素的出现次数赋值给localD中的对应键
if len(localD)>0:#如果该条记录有符合条件的元素
orderedItems=[v[0] for v in sorted(localD.items(),key=lambda p:p[1],reverse=True)]#元素按照支持度排序,支持度越大,排位越靠前
print orderedItems
每条数据过滤掉不符合条件的元素并重新排序后结果如下:
对比着原始事务数据来看:
在对事务数据过滤和排序之后,就可以构建FP树了。
从空集(符号为Φ)开始,依次遍历过滤,排序后的每一条记录,如果树中已经存在记录中的某个元素,那么已存在的元素增加值即可,如果树中不存在记录中的元素,那么增加一个分支。我们通过添加前两条事务为例,加深对建树的过程理解。
下面的代码用于实现FP树的构建过程,其中一部分的代码在前面已经解释过了。
def createTree(dataSet,minSup=1):
headerTable={}#用来存储每项元素及其出现次数
for trans in dataSet:#遍历每条记录
for item in trans:#遍历每条记录的每项元素
headerTable[item]=headerTable.get(item,0)+dataSet[trans]#计算每项元素的出现次数
for k in headerTable.keys():
if headerTable[k]0:#如果该条记录有符合条件的元素
orderedItems=[v[0] for v in sorted(localD.items(),key=lambda p:p[1],reverse=True)]#元素按照支持度排序,支持度越大,排位越靠前
updateTree(orderedItems,retTree,headerTable,count)
return retTree,headerTable
def updateTree(items,inTree,headerTable,count):
if items[0] in inTree.children:#如果inTree的子节点中已经存在该元素
inTree.children[items[0]].inc(count)#树中该元素增加值,增加的值为该元素所在记录的出现次数
else:
inTree.children[items[0]]=treeNode(items[0],count,inTree)#如果树中不存在该元素,重新创建一个节点
if headerTable[items[0]][1]==None:#如果在相似元素的字典headerTable中,该元素键对应的列表值中,起始元素为None
headerTable[items[0]][1]=inTree.children[items[0]]#把新创建的这个节点赋值给起始元素
else:
updateHeader(headerTable[items[0]][1],inTree.children[items[0]])#如果在相似元素字典headerTable中,该元素键对应的值列表中已经有了起始元素,那么把这个新建的节点放到值列表的最后
if len(items)>1:#如果在这条记录中,符合条件的元素个数大于1
updateTree(items[1::],inTree.children[items[0]],headerTable,count)#从第二个元素开始,递归调用updateTree函数。
def updateHeader(nodeToTest,targetNode):#该函数实现把targetNode放到链接的末端
while (nodeToTest.nodeLink!=None):
nodeToTest=nodeToTest.nodeLink
nodeToTest.nodeLink=targetNode
可以看出,以上这段代码中,函数updateHeader和函数updateTree都是为了给函数createTree调用的。
既然知道怎么构建FP树了,我们运行试试。
和前面说的一样,先处理数据集,再调用createTree函数。
simpDat=loadSimpDat()
initSet=createInitSet(simpDat)
myFPtree,myHeaderTab=createTree(initSet,3)
myFPtree.disp()
结果如下:
这个结果与刚开始展示的FP树结果一致。
从一棵FP树中挖掘频繁项集的三个基本步骤如下:
(1)从FP树中获得条件模式基;
(2)利用条件模式基,构建一个条件FP树;
(3)迭代重复步骤(1)和(2),直到树包含一个元素项为止。
先介绍条件模式基的概念。条件模式基是相对于树中的某个元素来说的,它指的是该元素在树中的前缀路径,是介于该元素与树根节点之间的所有内容。
比如,在我们构建好的FP树中:
每个元素的前缀路径如下表:
频繁项 | 前缀路径 |
---|---|
z | {} 5 |
r | {x,s} 1,{z,x,y} 1,{z} 1 |
x | {z} 3,{} 1 |
y | {z,x} 3 |
s | {z,x,y} 2,{x} 1 |
t | {z,x,y,s} 2,{z,x,y,r} 1 |
以下的代码实现查找一棵FP树中某个元素节点及其全部相似元素节点的前缀路径。把前缀路径冻结作为字典的键,前缀路径的计数取值与该元素节点的计数一样。
def ascendTree(leafNode,prefixPath):#该函数找出元素节点leafNode的所有前缀路径,并把包括该leafNode及其前缀路径的各个节点的名称保存在prefixPath中
if leafNode.parent!=None:
prefixPath.append(leafNode.name)
ascendTree(leafNode.parent,prefixPath)
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#condPats存储的是元素节点treeNode及其所有相似元素节点的前缀路径和它的计数
把FP树中所有元素的前缀路径都找出来:
for item in freqItemSet:
condPats=findPrefixPath(item,myHeaderTab[item][1])
print item
print condPats
结果:
这些元素的前缀路径与前面总结的一样,区别是当该元素的前缀路径为空时不显示,当然,还有一点,前缀路径的元素没有按顺序排列,但是不会影响后面的构建条件FP树。
t 的条件FP树如下图:
在构建条件FP树的过程中,每加入一条记录,都会调用createTree函数,这样的作用之一是可以过滤掉非频繁项,如上面的s和r。频繁项集在这个过程中就逐渐出来了。
挖掘频繁项集的代码如下:
def mineTree(inTree,headerTable,minSup,preFix,freqItemList):
bigL=[v[0] for v in sorted(headerTable.items(),key=lambda p:p[1])]#排序,从频率低到频率高排列树中的元素
for basePat in bigL:#遍历inTree中的所有元素
newFreqSet=preFix.copy()
newFreqSet.add(basePat)
freqItemList.append(newFreqSet)
conPattBases=findPrefixPath(basePat,headerTable[basePat][1])#寻找元素basePat及其相似元素的所有前缀路径,并以字典的形式存储它们
myCondTree,myHead=createTree(conPattBases,minSup)
if myHead!=None:#只要FP树中还有元素,递归调用mineTree函数
mineTree(myCondTree,myHead,minSup,newFreqSet,freqItemList)
运行以下代码:
freqItems=[]
mineTree(myFPtree,myHeaderTab,3,set([]),freqItems)
freqItems
结果:
以上的结果就是基于已构建的FP树通过mineTree函数挖掘得到的频繁项集。
python代码汇总如下:
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):
print ' '*ind, self.name, ' ', self.count
for child in self.children.values():
child.disp(ind+1)
def updateHeader(nodeToTest, targetNode):
while nodeToTest.nodeLink != None:
nodeToTest = nodeToTest.nodeLink
nodeToTest.nodeLink = targetNode
def updateFPtree(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)
# 更新相应频繁项集的链表,往后添加
if headerTable[items[0]][1] == None:
headerTable[items[0]][1] = inTree.children[items[0]]
else:
updateHeader(headerTable[items[0]][1], inTree.children[items[0]])
# 递归
if len(items) > 1:
updateFPtree(items[1::], inTree.children[items[0]], headerTable, count)
def createFPtree(dataSet, minSup=1):
headerTable = {}
for trans in dataSet:
for item in trans:
headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
for k in headerTable.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] # element: [count, node]
retTree = treeNode('Null Set', 1, None)
for tranSet, count in dataSet.items():
# dataSet:[element, count]
localD = {}
for item in tranSet:
if item in freqItemSet: # 过滤,只取该样本中满足最小支持度的频繁项
localD[item] = headerTable[item][0] # element : count
if len(localD) > 0:
# 根据全局频数从大到小对单样本排序
orderedItem = [v[0] for v in sorted(localD.items(), key=lambda p:p[1], reverse=True)]
# 用过滤且排序后的样本更新树
updateFPtree(orderedItem, retTree, headerTable, count)
return retTree, headerTable
def loadSimpDat():
simDat = [['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 simDat
# 构造成 element : count 的形式
def createInitSet(dataSet):
retDict={}
for trans in dataSet:
key = frozenset(trans)
if retDict.has_key(key):
retDict[frozenset(trans)] += 1
else:
retDict[frozenset(trans)] = 1
return retDict
# 数据集
def loadSimpDat():
simDat = [['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 simDat
# 构造成 element : count 的形式
def createInitSet(dataSet):
retDict={}
for trans in dataSet:
key = frozenset(trans)
if retDict.has_key(key):
retDict[frozenset(trans)] += 1
else:
retDict[frozenset(trans)] = 1
return retDict
# 递归回溯
def ascendFPtree(leafNode, prefixPath):
if leafNode.parent != None:
prefixPath.append(leafNode.name)
ascendFPtree(leafNode.parent, prefixPath)
# 条件模式基
def findPrefixPath(basePat, myHeaderTab):
treeNode = myHeaderTab[basePat][1] # basePat在FP树中的第一个结点
condPats = {}
while treeNode != None:
prefixPath = []
ascendFPtree(treeNode, prefixPath) # prefixPath是倒过来的,从treeNode开始到根
if len(prefixPath) > 1:
condPats[frozenset(prefixPath[1:])] = treeNode.count # 关联treeNode的计数
treeNode = treeNode.nodeLink # 下一个basePat结点
return condPats
def mineFPtree(inTree, headerTable, minSup, preFix, freqItemList):
# 最开始的频繁项集是headerTable中的各元素
bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p:p[1])] # 根据频繁项的总频次排序
for basePat in bigL: # 对每个频繁项
newFreqSet = preFix.copy()
newFreqSet.add(basePat)
freqItemList.append(newFreqSet)
condPattBases = findPrefixPath(basePat, headerTable) # 当前频繁项集的条件模式基
myCondTree, myHead = createFPtree(condPattBases, minSup) # 构造当前频繁项的条件FP树
if myHead != None:
# print 'conditional tree for: ', newFreqSet
# myCondTree.disp(1)
mineFPtree(myCondTree, myHead, minSup, newFreqSet, freqItemList) # 递归挖掘条件FP树