使用FP-growth算法来高效发现频繁项集(python2)
- 搜索引擎推荐问题:通过查看互联网上的用词来找出经常在一块出现的词对。
- FP-growth是一种高效发现频繁集的方法。它只需要对数据库进行两次扫描,第一遍扫描是对所有元素项的出现次数进行计数,统计出现的频率,第二遍扫描只考虑那些频繁元素。
- 适用数据类型:标称型数据
- 优点:一般快于Apriori
算法介绍
发现频繁集的基本过程为:
- 构建FP树
- 从FP树种挖掘频繁项集
FP树
FP(Frequent Pattern)树:通过链接link来连接相似元素,被连接的元素项可以看成一个链表。
创建FP树的数据结构
class treeNode: def __init__(self, nameValue, numOccur, parentNode): self.name = nameValue self.count = numOccur self.nodeLink = None self.parent = parentNode #needs to be updated self.children = {} def inc(self, numOccur): #对count变量增加给定值 self.count += numOccur def disp(self, ind=1):#将树以文本形式展示 print ' '*ind, self.name, ' ', self.count for child in self.children.values(): child.disp(ind+1)
构建FP树
- 头指针指向给定类型的第一个实例,同时保存Fp树中每类元素的总数。利用头指针可以快速访问FP树种一个给定类型的所有元素。
- 移除不满足最小支持度的元素项
- 根据全局频率对每个事务中的元素进行排序
- 从空集开始,向其中不断添加频繁项集。过滤、排序后的事务依次添加到树中,如果树中已存在现有元素,则增加现有元素的值;如果现有元素不存在,则向树添加一个分枝。
- 对数据库进行2次扫描,第1次扫描,对元素项计数并过滤处理;第2次扫描,对每条事务先排序然后再依次添加树的结点。
- createTree(main):过滤,排序,对每条事务进行处理构建树
- updateTree:对一条事务构建树的一条路径,同时updateHeader
- updateHeader:更新头指针字典表,对每个频繁项生成链表
def createTree(dataSet, minSup=1): #create FP-tree from dataset but don't mine """ dataSet:{frozenset(items):1,...},其中items表示一条事务,即一行数据 minsup:最小支持度 """ headerTable = {} #go over dataSet twice #第一次扫描,对元素计数 for trans in dataSet:#first pass counts frequency of occurance for item in trans: headerTable[item] = headerTable.get(item, 0) + dataSet[trans] #移除不满足最小支持度minSup的元素 for k in headerTable.keys(): #remove items not meeting minSup if headerTable[k] < minSup: del(headerTable[k]) freqItemSet = set(headerTable.keys()) #print 'freqItemSet: ',freqItemSet if len(freqItemSet) == 0: return None, None #if no items meet min support -->get out for k in headerTable: headerTable[k] = [headerTable[k], None] #reformat headerTable to use Node link #print 'headerTable: ',headerTable #从空集开始创建树 retTree = treeNode('Null Set', 1, None) #create tree,retTree始终指向根节点(空集) #第2次扫描数据库,按照头指针字典经过过滤的数据来对每一条事务排序 #再将经过过滤和排序的每条事务加入树中(这个调用updateTree来实现) for tranSet, count in dataSet.items(): #go through dataset 2nd time localD = {} for item in tranSet: #put transaction items in order 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)] #sorted(localD.items(), key=lambda p: p[1], reverse=True)是指按照元组中的index=1的元素来由大到小排序 updateTree(orderedItems, retTree, headerTable, count)#populate tree with ordered freq itemset return retTree, headerTable #return tree and header table #在更新树的同时更新头指针字典(调用updateHeader函数) def updateTree(items, inTree, headerTable, count): """ items:Lst inTree:treeNode headerTable:dict{item:treeNode} count:int 如果树中已存在现有元素,那么增加现有元素的值; 如果树中不存在现有元素,则向树添加一个分枝 """ if items[0] in inTree.children:#check if orderedItems[0] in retTree.children inTree.children[items[0]].inc(count) #incrament count else: #add items[0] to inTree.children inTree.children[items[0]] = treeNode(items[0], count, inTree) if headerTable[items[0]][1] == None: #update header table headerTable[items[0]][1] = inTree.children[items[0]] else: updateHeader(headerTable[items[0]][1], inTree.children[items[0]]) if len(items) > 1:#call updateTree() with remaining ordered items updateTree(items[1::], inTree.children[items[0]], headerTable, count) """ 每次updateTree都只对事务items的第一个元素进行操作, 若后面items还有元素则再次递归调用updateTree 注意inTree是向下递归调用的 """ def updateHeader(nodeToTest, targetNode): #this version does not use recursion """ nodeToTest:treeNode targetNode:treeNode 头指针的键值保存单元素的频繁项,键值的vlue保存的是节点链接nodeLink。 从头指针的nodeLink开始,一直沿着nodeLink直到到达链表末尾 不能使用迭代,是因为如果链表很长那么可能会遇到迭代调用的次数限制 """ while (nodeToTest.nodeLink != None): #Do not use recursion to traverse a linked list! nodeToTest = nodeToTest.nodeLink nodeToTest.nodeLink = targetNode
从FP树中挖掘频繁项集
- 从FP树种获取条件模式基
- 利用条件模式基,构建一个条件FP树
- 迭代上面两个步骤,知道树包含一个元素项为止
抽取条件模式基
- 条件模式基conditional pattern base:以所查找元素项为结尾的路径的集合
- 前缀路径prefix path:每一条路径其实都是一条前缀路径,即介于根结点和所查找元素项之间的所有内容。每一条前缀路径都与一个计数值关联,并同时储存,该计数值给了每条路径上该元素项在这条路径上的数目。
- 从保存在头指针表的单个元素项开始,对每一个元素项获取其条件模式基。
- ascendTree从某个元素项的某个结点开始上溯整棵树得到一条前缀路径
- findPrefixPath从某个元素项的头指针指向的链表的头结点开始调用ascendTree向下获取所有前缀路径
构建条件FP树
- 根据条件模式基构建条件FP树
- 对上面生成的某个频繁项的所有前缀路径(当作是事务items),生成该频繁项的条件FP树。
- 迭代递归上面1,2步骤,直到参与递归的所有频繁项的条件FP树的头指针表myHead为空为止,即直到FP树只包含一个元素为止。
- mineTree(main)对所有频繁项,包括条件FP树的频繁项生成条件FP树,同时记录频繁项。
def ascendTree(leafNode, prefixPath): #ascends from leaf node to root """ leafNode:treeNode,表示一个频繁项的结点 prefixPath:一条前缀路径 由某个频繁项的头指针的链表所指向的结点上溯一条前缀路径 """ if leafNode.parent != None: prefixPath.append(leafNode.name) ascendTree(leafNode.parent, prefixPath) def findPrefixPath(basePat, treeNode): #treeNode comes from header table """ basePat:表示的是一个频繁项 treeNode:指向的是头指针中该频繁项的头结点(一个频繁项可能存在于多条路径,那么其链表的长度也可能大于1) 对输入的一个频繁项,由该频繁项的头指针指向的头结点开始往下迭代调用ascendTree函数生成所有前缀路径 返回condPats:dict{frzenset:treeNode.count} 每条前缀路径都带一个计数值,计数值等于该频繁项在该条前缀路径上的数目 """ condPats = {} #条件模式基conditional pattern base while treeNode != None: prefixPath = [] ascendTree(treeNode, prefixPath) if len(prefixPath) > 1: condPats[frozenset(prefixPath[1:])] = treeNode.count treeNode = treeNode.nodeLink return condPats def mineTree(inTree, headerTable, minSup, preFix, freqItemList): """ 1.获取条件模式基 2.创建条件FP树 3.迭代1,2 迭代中的条件FP树的myHead头指针表==None为止 """ #按照全局频率来排序该条事务 bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1])]#(sort header table) #对所有的频繁项上溯整棵树生成一个条件模式基(即所有的前缀路径) for basePat in bigL: #start from bottom of header table 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 #2. construct cond FP-tree from cond. pattern base myCondTree, myHead = createTree(condPattBases, minSup) #print 'head from conditional tree: ', myHead if myHead != None: #3. mine cond. FP-tree #print 'conditional tree for: ',newFreqSet #myCondTree.disp(1) mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
简单示例:
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 = {} for trans in dataSet: retDict[frozenset(trans)] = 1 return retDict dataSet=loadSimpDat() initSet=createInitSet(dataSet) #对dataSet进行格式化处理 myFPtree,myHeaderTab=createTree(initSet,3) myFPtree.disp() mineTree(myFPtree,myHeaderTab,3,set([]),freqItems) print(freqItems)
简单示例:从新闻网站点击流中挖掘
koarak.dat文件包含近100万条记录。该文件种的每一行包含某个用户浏览过的新闻报导,一些用户只看过一篇报道,而有的客户看过2498篇报道。用户和报道被编码成整数,所以查看频繁项集很难得到更多的东西。文件的前几行如下:
1 2 3
1
4 5 6 7
1 8
9 10
11 6 12 13 14 15 16
import fpGrowth parsedDat=[line.split() for line in open('kosarak.dat').readlines()] #将数据导入到列表 initSet=fpGrowth.createInitSet(parsedDat) #对初始集合格式化 myFPtree,myHeaderTab=fpGrowth.createTree(initSet,100000) #构建FP树,并从中寻找那些至少被10万人浏览过的新闻报道 myFreqList=[] #用来保存频繁集 fpGrowth.mineTree(myFPtree,myHeaderTab,100000,set([]),myFreqList) print(myFreqList)
结果如下:
[{'1'}, {'1', '6'}, {'3'}, {'11', '3'}, {'11', '3', '6'}, {'3', '6'}, {'11'}, {'11', '6'}, {'6'}]
示例:在Twitter源中发现一些共现词(略)
注:
- Apriori对于每个潜在的频繁集都会扫描数据集判定给定模式是否频繁,当数据集很大时,这会显著降低频繁集发现的速度。
- FP-growth算法比Apriori更快。因为只对数据库扫描了2次
- 应用: 可以使用FP-growth在多种文本文档中查找频繁单词。频繁项集的生成还有其他的一些应用:比如购物交易、医学诊断、大气研究等。
- FP-growth算法还有一个map-reduce版本的实现,可以扩展到多台机器上运行。Google使用该算法通过遍历大量文本来发现频繁共现词。