• 搜索引擎推荐问题:通过查看互联网上的用词来找出经常在一块出现的词对。
  • FP-growth是一种高效发现频繁集的方法。它只需要对数据库进行两次扫描,第一遍扫描是对所有元素项的出现次数进行计数,统计出现的频率,第二遍扫描只考虑那些频繁元素。
  • 适用数据类型:标称型数据
  • 优点:一般快于Apriori

算法介绍

发现频繁集的基本过程为:

  1. 构建FP树
  2. 从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)
View Code

构建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
View Code

从一颗FP树中挖掘频繁项集

  • 从FP树种获取条件模式基
  • 利用条件模式基,构建一个条件FP树
  • 迭代上面两个步骤,知道树包含一个元素项为止

抽取条件模式基

  • 条件模式基conditional pattern base:以所查找元素项为结尾的路径的集合
  • 前缀路径prefix path:每一条路径其实都是一条前缀路径,即介于根结点和所查找元素项之间的所有内容。每一条前缀路径都与一个计数值关联,并同时储存,该计数值给了每条路径上该元素项在这条路径上的数目。
  • 从保存在头指针表的单个元素项开始,对每一个元素项获取其条件模式基。
  • ascendTree从某个元素项的某个结点开始上溯整棵树得到一条前缀路径
  • findPrefixPath从某个元素项的头指针指向的链表的头结点开始调用ascendTree向下获取所有前缀路径

构建条件FP树

  1. 根据条件模式基构建条件FP树
  2. 对上面生成的某个频繁项的所有前缀路径(当作是事务items),生成该频繁项的条件FP树。
  3. 迭代递归上面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)
View Code

简单示例:

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)
View Code

简单示例:从新闻网站点击流种挖掘

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)
View Code

结果如下:

[{'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使用该算法通过遍历大量文本来发现频繁共现词。
02-11 13:32