FP-growth算法将数据存储在一种称为FP树的紧凑数据结构中。FP代表频繁模式。一棵FP树看上去和其他的树类似,但是它通过链接来连接相似的元素,被连接的元素可以看作一个链表。下面是FP树的一个例子:
同搜索树不同,一个元素可以在FP树中出现多次,FP树会存储项集出现的频率,每个项集会以路径形式存在树中。存在相似元素的集合会共享树的一部分。树节点给出集合中单个元素及其在序列中出现的次数,路径会给出该序列出现的次数。相似项之间的链接即为节点连接,用于快速发现相似项的位置。链接在图中用虚曲线表示,它链接了相同的元素。
下面是构成这个FP树所用到的数据:
ID | 元素项 |
---|---|
1 | rzhjp |
2 | zyxwvuts |
3 | z |
4 | rxnos |
5 | yrxzqtp |
6 | yzxeqstm |
在上面的那棵树中,项集中的元素是某个节点到根节点之间路径上所有元素的集合,该项集出现的频率是第一个节点旁边代表的数字。比如集合{z}一共出现了5次,集合{r,z}一共出现了一次,这和表中的数据结论是相吻合的。至于树中没有出现的元素,是因为我们同样对这棵树设置了最小支持度,小于该值的项集不会出现。这棵树设置的最小支持度为3,所以其他的元素并没有出现在这棵树上。
FP-growth算法的工作流程如下:首先构成FP树,然后利用它来挖掘频繁项集。
为了构建FP树,需要对原始数据扫描两遍。第一遍对所有元素项出现的次数进行统计,把那些不符合支持度的元素去掉。数据库的第一遍扫描用于统计出现的频率,第二次扫描只考虑那些频繁元素。
因为最终结果是要构建一棵FP树,为成功构建该树,需要一个容器来保存。
由于树中需要保存大量信息,为了方便之后的计算,我们将树节点存储到一个类中。
class TreeNode:
def __init__(self,nameVal,numCnt,parentNode):
self.name=nameVal #元素名
self.cnt=numCnt #出现次数
self.nodeLink=None #节点链接
self.parent=parentNode #因为路径是从下到上遍历节点,所以需要父节点
self.children={} #孩子字典
def inc(self,numCnt): #自增运算
self.cnt+=numCnt
def display(self,ind=1): #结构展示
print(' '*ind,self.name,' ',self.cnt)
for child in self.children.values():
child.display(ind+1)
现在的FP树数据结构已经构建好了,下面就可以构建FP树了
除了构建FP树所需的数据结构,还需要一个头指针来指向给定类型的第一个实例。利用头指针,可以快速访问树中给定类型的所有元素,下面是一个头指针表的示意图:
这里的头指针表用字典的形式来存储。除了用于保存指针外,头节点还用来保存FP树种每类元素d总数。
第一次遍历数据集会获得每个元素项出现的频率。接下来去掉不满足最小支持度的元素项。再下一步构建FP树。在构建时,读入每个项集并将其添加到已经存在的路径中,如果路径不存在,则创建一条新路径。
每个事务是一个无序集合,假设有集合{z,y}和{y,z},那么在FP树中,相同项只会表示一次。为了解决这个问题,将集合添加到树之前,需要对每个集合进行排序。排序基于元素项的出现频率来进行。上面的数据经过去除非频繁项集和排序后得到以下结果:
ID | 元素项 | 处理后 |
---|---|---|
1 | rzhjp | zr |
2 | zyxwvuts | zxyst |
3 | z | z |
4 | rxnxsros | xsr |
5 | yrxzqtp | zxyrt |
6 | yzxeqstm | zxyst |
在对集合进行过滤排序之后,就可以构建FP树了。从空集开始,向其不断添加频繁项集。过滤排序后的事务依次添加到树中,如果树中已存在现有的元素,则增加现有元素的值。如果元素不存在,则向树中添加一个分支。对前面数据中前两条进行添加的过程如下:
构建树的详细代码如下:
def create_tree(dataSet,minSup=1):
#dataSet较为特殊,它是一个字典形式,存放项集和对应的出现次数,在一开始出现次数都为1
#minSup为最小支持度
headerTable={} #存放头指针表
#第一次遍历数据集
for tran in dataSet: #由于dataSet是字典,这个循环是对于字典中的每一个键值
for item in tran: #对于键值中的每一个元素
headerTable[item]=headerTable.get(item,0)+dataSet[tran] #更新该元素在全局中出现的次数
#第一次遍历结束
for key in list(headerTable.keys()): #元素去重,对于每一个候元素来说
if headerTable[key]<minSup: #如果当前元素支持度小于最小值
del(headerTable[key]) #去除
freItemSet=set(headerTable.keys()) #去重后的元素
if len(freItemSet)==0: return None,None #如果没有符合的元素,返回空值
for key in list(headerTable.keys()): #对于每一个元素
headerTable[key]=[headerTable[key],None] #将头指针表扩充一列,第一列存放元素出现次数,第二列存放节点连接,用于连接相同元素
#print(headerTable)
rootTree=TreeNode('root',1,None) #构建根节点
#第二次遍历数据集
for tran,cnt in dataSet.items(): #对于数据集中每一个数据和其出现次数
localD={} #存放频繁项集元素
for item in tran:#对与数据中每一个元素
if item in freItemSet: #如果该元素在频繁项集中
localD[item]=headerTable[item][0] #加入队列
#print(localD)
if len(localD)>0: #该条数据中中存在频繁项集
sortItem=[v[0] for v in sorted(localD.items(),key=lambda x:(x[1],x[0]),reverse=True)] #对其进行排序
#print(sortItem)
updateTree(sortItem, rootTree, headerTable, cnt) #更新FP树
#第二次遍历结束
return rootTree,headerTable
树构建的过程中遍历两遍数据集,第一次遍历数据集获取每个元素出现的次数,并去除不符合要求的元素。第二次遍历只对频繁项集,将每条数据中的频繁项集取出,之后对其排序,最后添加到树中。
排序算法根据出现次数从大到小排序
第一次写的时候发现排序结果和树中的不一样,排查后发现是对出现次数相同的元素排序规则不一样。不过这个并不影响最终结果,如果想改成书中那样,只需要在排序中key的选择写成key=lambda x:(x[1],x[0]),先对次数进行排序,同次数的再根据元素ascII码排序。
之后就是要将处理好的集合添加到树中,具体代码如下:
def updateTree(items,intree,headerTable,cnt):
'''
:param items: 频繁项集集合
:param intree: 要添加进去的树
:param headerTable: 头指针表
:param cnt: 整个项集出现的次数
:return:
'''
#递归添加,每次添加第一个元素
if items[0] in intree.children: #如果该元素存在于该树的孩子节点中
intree.children[items[0]].inc(cnt) #对应节点自增
else:
intree.children[items[0]]=TreeNode(items[0],cnt,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: #如果长度大于1,递归执行
updateTree(items[1:],intree.children[items[0]],headerTable,cnt) #到这个元素对应的树节点上递归执行
def updateHeader(node,targetNode): #整个函数的作用就是为了使树节点中d节点连接可以把相同元素全部连接上
'''
:param node: 当前头节点表指向的第一个实例
:param targetNode: 新加入的节点
'''
while node.nodeLink!=None:
node=node.nodeLink #循环到队尾
node.nodeLink=targetNode#将该节点加入
这样整个FP树就创建完毕了,接下来导入数据进行测试,由于数据集的格式和其他的都不一样,这里先创建数据集的处理函数
def create_data():
x=[
['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 x
def createInPut(dataset):
reDict={} #将数据集转换成字典形式
for trans in dataset: #对于每行数据
reDict[frozenset(trans)]=1 #保存数据集的出现此时
return reDict
处理好的数据格式如下:
{
frozenset({'j', 'p', 'r', 'h', 'z'}): 1,
frozenset({'u', 'y', 'x', 'w', 't', 's', 'v', 'z'}): 1,
frozenset({'z'}): 1,
frozenset({'o', 'x', 'r', 's', 'n'}): 1,
frozenset({'q', 'p', 'y', 'x', 't', 'r', 'z'}): 1,
frozenset({'q', 'm', 'e', 'y', 'x', 't', 's', 'z'}): 1
}
对这个数据集进行处理,生成了FP树,展示如下:
root 1
z 5
r 1
x 3
y 3
t 3
s 2
r 1
x 1
s 1
r 1
同一级的缩进一样,好像还是对不上书里面给出的FP树,不过并不影响最后结果。
有了FP树之后,就可以抽取频繁项集了。这里的思路和Apriori算法大体一致,首先从单元素集合开始,然后在此基础上逐步构建更大的集合。这里利用到了FP树,就不需要原始数据集了。
从FP树中抽取频繁项集的三个基本步骤如下:
首先从头指针表的单个元素开始。对于每一个元素项,获得其对于的条件模式基。条件模式基是以所查找的元素为结尾的路径的集合每条路径其实都是一条前缀路径。
就上面给出的FP树来说,符号r的前缀路径是{x,s}、{z,x,y}和{z}。每一条前缀路径都与一个计数值关联,该计数值等于起始元素的计数值,该计数值给了每条路径上r的数目。
前缀路径被用于构建条件FP树。如何获取前缀路径,可以利用我们之前创建树节点的时候使用的父节点属性,从开始逐步向上回溯到这棵树的根节点。具体代码如下:
def ascendTree(leafNode,prefixPath): #prefixPaht用于存放路径,它是一个数组
if leafNode.parent !=None: #存在父节点
prefixPath.append(leafNode.name) #将该节点的名字加入
ascendTree(leafNode.parent,prefixPath)#递归回溯
def findPrefixPath(treeNode): #该参数为头指针表指向的该元素的第一个实例
condPats={} #存放前缀路径和计数值
while treeNode!=None: #如果当前节点不为空
prefixPath=[] #当前节点作为叶节点
ascendTree(treeNode,prefixPath) #向上回溯整棵树并把路径存入
if len(prefixPath)>1: #因为存储的路径包含该元素本身,要用前缀需要去掉这个元素
condPats[frozenset(prefixPath[1:])]=treeNode.cnt #将前缀信息存入字典中
treeNode=treeNode.nodeLink #找到下一个相同元素
return condPats #返回该字典
我们测试一些数据,因为树的建立和书中不一样,可能存在某些元素的前缀路径不同。
findPrefixPath(header['r'][1])
结果为
{
frozenset({'z'}): 1,
frozenset({'s', 'x'}): 1,
frozenset({'x', 'y', 'z', 't'}): 1
}
有了这些条件基后,就可以创建条件FP树。
对于每一个频繁项,都要创建一棵条件FP树。可以使用刚才构建的条件模式基作为输入数据,并通过相同的建树代码来构建这些树。然后递归的发现频繁项、发现条件模式基,以及发现另外的条件树。该过程不断重复,直到条件树中没有元素为止,就可以停止了。具体的实现代码如下:
def mineTree(inTree,headerTable,minSup,preFix,freqItemList):
'''
:param inTree:要输入的整棵树
:param headerTable: 头指针表
:param minSup: 最小支持度
:param preFix: 存放前缀的集合,set()集合
:param freqItemList: 频繁项集
:return:
'''
#print(headerTable.items())
bigL=[v[0] for v in sorted(headerTable.items(),key=lambda x:x[1][0])] #表中都是频繁项集,对其元素进行排序,按频率递增排序
#print(bigL)
for basePat in bigL: #对于每一个元素
newFreqSet=preFix.copy() #将前缀复制一份,因为用到递归,这个变量会变化
newFreqSet.add(basePat) #将频繁项集加入
freqItemList.append(newFreqSet) #存入列表中
condPattBases=findPrefixPath(headerTable[basePat][1]) #获取条件模式基
myCondTree,myHead=create_tree(condPattBases,minSup) #根据该条件模式基进行建立条件树
if myHead!=None: #如果条件树不空
mineTree(myCondTree,myHead,minSup,newFreqSet,freqItemList) #基于此条件树递归调用
对上面的数据进行测试,得到频繁项集如下:
[{'r'}, {'y'}, {'x', 'y'}, {'x', 'y', 'z'}, {'y', 'z'}, {'t'}, {'x', 't'}, {'x', 'y', 't'}, {'x', 'z', 'y', 't'}, {'x', 'z', 't'}, {'y', 't'}, {'z', 'y', 't'}, {'z', 't'}, {'s'}, {'x', 's'}, {'x'}, {'x', 'z'}, {'z'}]
与结果一致
我们现在有一组新闻浏览记录,包含了近100万条左右。每行表示某个用户浏览过的新闻报道。用户和报道被编码成整数,我们现在想知道至少被10万人浏览过的新闻报道,代码实现如下:
minsup=100000
parseDat=[line.split() for line in open('kosarak.dat').readlines()]
initSet=fp.createInPut(parseDat)
tree,header=fp.create_tree(initSet,minsup)
treeList=[]
fp.mineTree(tree,header,minsup,set([]),treeList)
print(treeList)
结果如下:
[{'1'}, {'6', '1'}, {'3'}, {'11', '3'}, {'11', '6', '3'}, {'6', '3'}, {'11'}, {'11', '6'}, {'6'}]
可以看出执行速度较快