上一章我们学习了apriori算法,该算法的优点在于能够发现关联规则,但是由于它对每个潜在的频繁项集都要扫描数据集判定其是否频繁,因此在处理大型数据时效率非常低。为了解决这个问题,我们引入了FP-growth算法。它虽然不能够发现关联规则,而且实现较为困难,且在某些数据集上性能会下降,但它在发现频繁项集时只有扫描两遍数据集,效率比起apriori算法就要高出很多了。其样式如下:
与搜索树不同,一个单元数据可以在一颗FP树中重复出现。FP树会存储项集的出现频率,并以路径的形式存储在树中。存在相似元素的集合会共享树的一部分,只有当集合间完全不同时,才会出现分叉。每个结点存储单元数据名及其在该路径上的出现次数。如上图,可以看出数据为(FP树是经过筛选和排序的,所以原始数据中会有一些被筛掉的数据,数据行的顺序以及每行数据中被保留数据的顺序可能与下列数据不同):
z、r
z、x、y、s、t
z、x、y、s、t
z、x、y、r、t
z
x、s、r
FP-growth算法的工作流程如下:
首先构建FP树,然后利用它来挖掘频繁项集。为构建FP树,需要对原始数据集扫描两遍。第一遍对所有单元数据的出现次数进行计数,第二遍则只考虑那些频繁的单元数据,并用它们构建FP树。代码如下:
#创建fpGrowth树结点类
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 loadSimpleDat():
simpleDat = [
['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 simpleDat
#用一个dict的key保存数据集的值,value保存其出现次数,并初始化为1
def createInitSet(dataSet):
retDict = {}
for trans in dataSet:
retDict[frozenset(trans)] = 1
return retDict
#创建fpgrowth树
#dataSet:数据集
#minSup:容许保存在树里的单元数据的最小出现次数
def createTree(dataSet,minSup = 1):
#建立头指针表,记录每个单元数据的出现次数及一个指向该类单元数据中第一个加入树的单元数据的指针
headerTable = {}
#第一次遍历数据,记录每个单元数据的出现次数,得到头指针表
#这里的dataSet[trans]值均为1,但没写为1的原因应该是1:有些记录里会有重复行;2:行之间的重要性并不相同
for trans in dataSet:
for item in trans:
headerTable[item] = headerTable.get(item,0) + dataSet[trans]
#遍历头指针表,若出现次数不符合要求,则删去该单元数据
for k in list(headerTable.keys()):
if headerTable[k] < minSup:
del(headerTable[k])
#①
else:
headerTable[k] = [headerTable[k], None]
#若所有单元数据都不符合最低出现次数的要求,则不需要继续进行处理
#书里在这里新建了freqItemSet变量,但我觉得没有必要,就删去了
#freqItemSet = set(headerTable)
if len(set(headerTable)) == 0:
return None,None
#书里在这里用一个for给headerTable添加了一列,之所以不在一开始做是因为那时候还没删除不符合要求的点,
#如果那时就添加的话,在处理大型数据时会导致大量的无效运算。
#但既然如此,为何不在第一次遍历时就处理呢?这样这次的遍历时间也可以节省了。于是我在①处添加了else
#for k in headerTable:
#headerTable[k] = [headerTable[k],None]
#初始化根结点
retTree = treeNode('NULL Set',1,None)
#第二次遍历数据,开始构建树
#②
for tranSet,count in dataSet.items():
#建立一个局部dict,用于保存一条数据行
localD = {}
#遍历该行数据
for item in tranSet:
#if item in freqItemSet:
#若该行数据中的单元数据在头指针表中,则将它存储到localD dict中
if item in headerTable.keys():
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)
return retTree,headerTable
#将结点加入树
#items:由某一数据行中所有符合要求的单元数据所组成的列表
#inTree:将要加入树的单元数据的父结点
#headerTable:头指针表
#count:该行数据出现的次数
def updateTree(items,inTree,headerTable,count):
#因为items中可能不止一个单元数据,所以先处理第一个单元数据
#若父结点的子结点中已有该数据,则将其出现次数增加count
if items[0] in inTree.children:
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]])
#若items中不止一个单元数据,则将除第一个单元数据外的数据作为新的items,
#并将第一个数据作为之后数据的父结点,迭代调用updateTree()
if len(items) > 1:
updateTree(items[1::],inTree.children[items[0]],headerTable,count)
#更新头指针
#nodeToTest:头指针表中该类数据的指针域
def updateHeader(nodeToTest,targetNode):
#依次访问指针域,直到找到上一个加入树的该类单元数据
while nodeToTest.nodeLink != None:
nodeToTest = nodeToTest.nodeLink
#使其指向新结点
nodeToTest.nodeLink = targetNode
#主函数
if __name__ == "__main__":
simpleDat= loadSimpleDat()
initSet = createInitSet(simpleDat)
myFPtree,myHeaderTab = createTree(initSet,3)
myFPtree.disp()
#②:https://www.runoob.com/python/att-dictionary-items.html
#③:https://www.cnblogs.com/brad1994/p/6697196.html