FP Growth算法

Apriori:

  Apriori是关联分析中比较早的一种方法,主要用来挖掘那些频繁项集合。其思想是:

  1. 如果一个项目集合不是频繁集合,那么任何包含它的项目集合也一定不是频繁集合;

  2. 如果一个项目集合是频繁集合,那么它的任何非空子集也是频繁集合;

  Aprioir需要扫描项目表多遍,从一个项目开始扫描,舍去掉那些不是频繁的项目,得到的集合称为L,然后对L中的每个元素进行自组合,生成比上次扫描多一个项目的集合,该集合称为C,接着又扫描去掉那些非频繁的项目,重复…

  看下面这个例子:

  元素项目表格:

   

  如果每个步骤不去掉非频繁项目集,则其扫描过程的树形结构如下:

   

  在其中某个过程中,可能出现非频繁的项目集,将其去掉(用阴影表示)为:

   

  上面的内容主要参考的是machine learning in action这本书。

  FP Growth:

  FP Growth是一种比Apriori更高效的频繁项挖掘方法,它只需要扫描项目表2次。其中第1次扫描获得当个项目的频率,去掉不符合支持度要求的项,并对剩下的项排序。第2遍扫描是建立一颗FP-Tree(frequent-patten tree)。

  接下来的工作就是在FP-Tree上进行挖掘。

  比如说有下表:

   

  它所对应的FP_Tree如下:

   

  然后从频率最小的单项P开始,找出P的条件模式基,用构造FP_Tree同样的方法来构造P的条件模式基的FP_Tree,在这棵树上找出包含P的频繁项集。

  依次从m,b,a,c,f的条件模式基上挖掘频繁项集,有些项需要递归的去挖掘,比较麻烦,比如m节点,具体的过程可以参考博客:Frequent Pattern 挖掘之二(FP Growth算法),里面讲得很详细。

FP树构造

FP Growth算法利用了巧妙的数据结构,大大降低了Aproir挖掘算法的代价,他不需要不断得生成候选项目队列和不断得扫描整个数据库进行比对。为了达到这样的效果,它采用了一种简洁的数据结构,叫做frequent-pattern tree(频繁模式树)。下面就详细谈谈如何构造这个树,举例是最好的方法。请看下面这个例子:

BA0B50B830144EEC740C6FFB7A947BBE9259DDF0

这张表描述了一张商品交易清单,abcdefg代表商品,(ordered)frequent items这一列是把商品按照降序重新进行了排列,这个排序很重要,我们操作的所有项目必须按照这个顺序来,这个顺序的确定非常简单,只要对数据库进行一次扫描就可以得到这个顺序。由于那些非频繁的项目在整个挖掘中不起任何作用,因此在这一列中排除了这些非频繁项目。我们在这个例子中设置最小支持阈值(minimum support threshold)为3。

我们的目标是为整个商品交易清单构造一颗树。我们首先定义这颗树的根节点为null,然后我们开始扫描整个数据库的每一条记录开始构造FP树。

第一步:扫描数据库的第一个交易,也就是TID为100的交易。那么就会得到这颗树的第一个分支<(f:1),(c:1),(a:1),(m:1),(p:1)>。注意这个分支一定是要按照降频排列的。

 9DD1C6EE4721C0BFAD5EC7014A1C832E94F053F9

第二步:扫描第二条交易记录(TID=200),我们会有这么一个频繁项目集合<f,c,a,b,m>。仔细观察这个队列,你会发现这个集合的前3项<f,c,a>与第一步产生的路径<f,c,a,m,p>的前三项是相同的,也就是说他们可以共享一个前缀。于是我们在第一步产生的路径的基础上,把<f,c,a>三个节点的数目加1,然后将<(b:1),(m:1)>作为一个分支加在(a:2)节点的后面,成为它的子节点。看下图

 D5DDCF44354434023713EA37A49B1C3F0489C580

第三步:接着扫描第三条交易记录(TID=300),你会看到这条记录的集合是<f, b>,与已存在的路径相比,只有f是共有的前缀,那么f节点加1,同时再为f节点生成一个新的字节点(b:1).就会有下图:

 893843BA5D690D85733AC3D1F226C38864A75F78

第四步:继续看第四条交易记录,它的集合是<c,b,p>,哦,这回不一样了。你会发现这个集合的第一个元素是c,与现存的已知路径的第一个节点f不一样,那就不用往下比了,没有任何公共前缀。直接将该集合作为根节点的子路径附加上去。就得到了下图(图1):

 6DC384067999DF5454C654F24D7BE2E3D6EBEAC9

第五步:最后一条交易记录来了,你看到了一条集合<f,c,a,m,p>。你惊喜得发现这条路径和树现有最左边的路径竟然完全一样。那么,这整条路径都是公共前缀,那么这条路径上的所有点都加1好了。就得到了最终的图(图2)。

 6F7A3F3CEC4D76C1D4072263B9C360557D25A781

好了,一颗FP树就已经基本构建完成了。等等,还差一点。上述的树还差一点点就可以称之为一个完整的FP树啦。为了便于后边的树的遍历,我们为这棵树又增加了一个结构-头表,头表保存了所有的频繁项目,并且按照频率的降序排列,表中的每个项目包含一个节点链表,指向树中和它同名的节点。罗嗦了半天,可能还是不清楚,好吧直接上图,一看你就明白:

 F8F9DABF0551A8CE0106A82E3AC4FF3E9EA378DC

以上就是整个FP树构造的完整过程。聪明的读者一定不难根据上述例子归纳总结出FP树的构造算法。这里就不再赘述。详细的算法参考文献1。

FP树的挖掘

下面就是最关键的了。我们已经有了一个非常简洁的数据结构,下一步的任务就是从这棵树里挖掘出我们所需要的频繁项目集合而不需要再访问数据库了。还是看上面的例子。

第一步:我们的挖掘从头表的最后一项p开始,那么一个明显的直接频繁集是(p:3)了。根据p的节点链表,它的2个节点存在于2条路径当中:路径<f:4,c:3,a:3,m:2,p:2>和路径<c:1,b:1,p:1>.从路径<f:4,c:3,a:3,m:2,p:2>我们可以看出包含p的路径<f,c,a,m,p>出现了2次,同时也会有<f,c,a>出现了3次,<f>出现了4次。但是我们只关注<f,c,a,m,p>,因为我们的目的是找出包含p的所有频繁集合。同样的道理我们可以得出<c,b,p>在数据库中出现了1次。于是,p就有2个前缀路径{(fcam:2),(cb:1)}。这两条前缀路径称之为p的子模式基(subpattern-base),也叫做p的条件模式基(之所以称之为条件模式基是因为这个子模式基是在p存在的前提条件下)。接下来我们再为这个条件子模式基构造一个p的条件FP树。再回忆一下上面FP树的构造算法,很容易得到下面这棵树:

 7584BFD589D24E20DB04FD88DB133B8A54F97A01

但是由于频繁集的阈值是3。那么实际上这棵树经过剪枝之后只剩下一个分支(c:3),所以从这棵条件FP树上只能派生出一个频繁项目集{cp:3}.加上直接频繁集(p:3)就是最后的结果.

第二步:我们接下来开始挖掘头表中的倒数第二项m,同第一步一样,显然有一个直接的频繁集(m:3).再查看它在FP树中存在的两条路径<f:4,c:3,a:3,m:2>和<f:4,c:3,a:3,b1,m:1>.那么它的频繁条件子模式基就是{ (fca:2),(fcab:1)}.为这个子模式基构造FP树,同时舍弃不满足最小频繁阈值的分支b,那么其实在这棵FP树中只存在唯一的一个频繁路径<f:3,c:3,a:3>.既然这颗子FP树是存在的,并且不是一颗只有一个节点的特殊的树,我们就继续递归得挖掘这棵树.这棵子树是单路径的子树,我们可以简化写成mine(FP tree|m)=mine(<f:3,c:3,a:3>|m:3).

下面来阐述如何挖掘这颗FP子树,我们需要递归.递归子树也需要这么几个步骤:

1这颗FP子树的头表最后一个节点是a,结合递归前的节点m,那么我们就得到am的条件子模式基{(fc:3)},那么此子模式基构造的FP树(我们称之为m的子子树)实际上也是一颗单路径的树<f:3,c:3>,接下也继续继续递归挖掘子子树mine(<f:3,c:3>|am:3). (子子树的递归分析暂时打住.因为再分析子子树的递归的话文字就会显得太混乱)

2同样,FP子树头表的倒数第二个节点是c,结合递归前节点m,就有我们需要递归挖掘mine(<f:3>|cm:3).

3 FP子树的倒数第三个节点也是最后一个节点是f,结合递归前的m节点,实际上需要递归挖掘mine(null|fm:3),实际上呢这种情况下的递归就可以终止了,因为子树已经为空了.因此此情况下就可以返回频繁集合<fm:3>

注意:这三步其实还包含了它们直接的频繁子模式<am:3>,<cm:3>,<fm:3>,这在每一步递归调用mine<FPtree>都是一样的,就不再罗嗦得一一重新指明了.

实际上这就是一个很简单的递归过程,就不继续往下分析了,聪明的读者一定会根据上面的分析继续往下推导递归,就会得到下面的结果.

mine(<f:3,c:3>|am:3)=><cam:3>,<fam:3>,<fcam:3>

mine(<f:3>|cm:3)=><fcm:3>

mine(null|fm:3)=><fm:3>

这三步还都包含了各自直接的频繁子模式<am:3>,<cm:3>,<fm:3>.

最后再加上m的直接频繁子模式<m:3>,就是整个第二步挖掘m的最后的结果。请看下图

 6ED0926EF2455D309C439385B7B1061E46278D88

第三步:来看看头表倒数第三位<b:3>的挖掘,它有三条路径<f:4,c:3,a:3,b:1>,<f:4,b:1>,<c:1,b:1>,形成的频繁条件子模式基为{(fca:1),(f:1),(c:1)},构建成的FP树中的所有节点的频率均小于3,那么FP树为空,结束递归.这一步得到的频繁集就只有直接频繁集合<b:3>

第四步:头表倒数第四位<a:3>,它有一条路径<f:4,c:3>,频繁条件子模式基为{(fc:3)},构成一个单路径的FP树.实际上可能有人早已经发现了,这种单路径的FP树挖掘其实根本不用递归这么麻烦,只要进行排列组合就可以直接组成最后的结果.实际上也确实如此.那么这一步最后的结果根据排列组合就有:{(fa:3),(ca:3),(fca:3),(a:3)}

第五步:头表的倒数第五位<c:4>,它只有一条路径<f:4>,频繁条件子模式基为{(f:3)},那么这一步的频繁集也就很明显了:{(fc:3),(c:4)}

第六步:头表的最后一位<f:4>,没有条件子模式基,那么只有一个直接频繁集{(f:4)}

这6步的结果加在一起,就得到我们所需要的所有频繁集.下图给出了每一步频繁条件模式基.

 91DC9EEA79E1822039DC1DBC84E3457479F6F27A

其实,通过上面的例子,估计早有人看出来了,这种单路径的FP树挖掘其实是有规律的,根本不用递归这么复杂的方法,通过排列组合可以直接生成.的确如此,Han Jiawei针对这种单路径的情况作了优化.如果一颗FP树有一个很长的单路径,我们将这棵FP树分成两个子树:一个子树是由原FP树的单路径部分组成,另外一颗子树由原FP树的除单路径之外的其余部分组成.对这两个子树分别进行FP Growth算法,然后对最后的结果进行组合就可可以了.

通过上面博主不厌其烦,孜孜不倦,略显罗嗦的分析,相信大家已经知道FP Growth算法的最终奥义.实际上该算法的背后的思想很简单,用一个简洁的数据结构把整个数据库进行FP挖掘所需要的信息都包含了进去,通过对数据结构的递归就可以完成整个频繁模式的挖掘.由于这个数据结构的size远远小于数据库,因此可以保存在内存中,那么挖掘速度就可以大大提高.

也许有人会问?如果这个数据库足够大,以至于构造的FP树大到无法完全保存在内存中,这该如何是好.这的确是个问题. Han Jiawei在论文中也给出了一种思路,就是通过将原来的大的数据库分区成几个小的数据库(这种小的数据库称之为投射数据库),对这几个小的数据库分别进行FP Growth算法.

还是拿上面的例子来说事,我们把包含p的所有数据库记录都单独存成一个数据库,我们称之为p-投射数据库,类似的m,b,a,c,f我们都可以生成相应的投射数据库,这些投射数据库构成的FP树相对而言大小就小得多,完全可以放在内存里.

在现代数据挖掘任务中,数据量越来越大,因此并行化的需求越来越大,上面提出的问题也越来越迫切.下一篇博客,博主将分析一下,FP Growth如何在MapReduce的框架下并行化.

[1]Mining Frequent Patterns without Candidate Generation: AFrequent-Pattern Tree Approach

在MapReduce框架下进行并行FP挖掘,它主要的算法在文献1中有详细描述。

如何进行FP Growth的并行化呢?一个很自然的想法就是,将原始的数据库划分成几个分区,这几个分区分别在不同的机器上,这样的话我们就可以对不同数据分区并行得进行FP Growth挖掘,最后将不同机器上的结果结合起来得到最终的结果。的确,这是一个正确的思路。但问题是:我们按照什么样的方法来把数据库划分成区块呢?如果FP Growth能够真正的独立进行并行化,那么就需要这些数据分区必须能够互相独立,也就是这些分区针对某一部分项目来说是完备的。于是就有一种方法:通过对数据库的一次扫描,构造一个Frequent Item列表F_List = {I1:count1, I2:count2, I3:count3…} ^ (count1> count2 > count3>…),然后将F_List分成几个Group,形成几个G_List.这时候我们再扫描数据库的每一条Transaction,如果这条Transaction中包含一条G_List中的Item,那么这条transaction就被添加到该group对应的数据库分区中去,这样就形成了几个数据库分区,每个数据库分区对应一个group和一个group_list。这种分区方法就保证对group_list里面的item而言,数据库分区是完备的。这种分区方式会导致数据会有冗余,因为一条transaction可能会在不同的分区中都有备份,但为了保持数据的独立性,这是一个不得已方法。

下面就简单谈谈该算法的步骤:

第一步:数据库分区.把数据库分成连续的不同的分区,每一个分区分布在不同的机器上.每一个这样的分区称之为shard。

第二步:计算F_list,也就是所有item的support count.这个计算通过一个MapReduce就可以完成.想想hadoop上word count的例子,本质上和这一步是一样的.

第三步:条目分组.将F_list里的条目分成Q个组,这样的话就行成了一个group_list,group_list里的每一个group都被分配一个group_id,每个group_list都包含一组item的集合.

第四步:并行FP Growth.这一步是关键.它也是由一个MapReduce来完成的.具体来看看.

Mapper:

这个Mapper完成的主要功能是数据库分区。它和第一步中的shard有所不同,它利用第一步shard的数据库分区,一个一个处理shard数据库分区中的每一条transaction,将transaction分成一个一个item,每一个item根据group_list映射到合适的group里去。这样的话,通过mapper,属于同一个group的item集合都被聚合到一台机器上,这样就形成了我们前面讲到的完备数据集,在下一步的reducer中就可以并行得进行FP Growth算法了。

Reducer:

基于mapper形成的完备数据集,进行local的FP_Growth算法

第五步:聚合,将各台机器上的结果聚合成最终我们需要的结果。

前面的博客分析了关联分析中非常重要的一个算法-FP Growth.该算法根据数据库在内存中构造一个精巧的数据结构-FP Tree,通过对FP Tree不断的递归挖掘就可以得到所有的完备Frequent Patterns.但是在目前海量数据的现状下,FP Tree已经大到无法驻留在计算机的内存中。因此,并行化是唯一的选择。这篇博客主要讲一下如何在MapReduce框架下进行并行FP挖掘,它主要的算法在文献1中有详细描述。

如何进行FP Growth的并行化呢?一个很自然的想法就是,将原始的数据库划分成几个分区,这几个分区分别在不同的机器上,这样的话我们就可以对不同数据分区并行得进行FP Growth挖掘,最后将不同机器上的结果结合起来得到最终的结果。的确,这是一个正确的思路。但问题是:我们按照什么样的方法来把数据库划分成区块呢?如果FP Growth能够真正的独立进行并行化,那么就需要这些数据分区必须能够互相独立,也就是这些分区针对某一部分项目来说是完备的。于是就有一种方法:通过对数据库的一次扫描,构造一个Frequent Item列表F_List = {I1:count1, I2:count2, I3:count3…} ^ (count1> count2 > count3>…),然后将F_List分成几个Group,形成几个G_List.这时候我们再扫描数据库的每一条Transaction,如果这条Transaction中包含一条G_List中的Item,那么这条transaction就被添加到该group对应的数据库分区中去,这样就形成了几个数据库分区,每个数据库分区对应一个group和一个group_list。这种分区方法就保证对group_list里面的item而言,数据库分区是完备的。这种分区方式会导致数据会有冗余,因为一条transaction可能会在不同的分区中都有备份,但为了保持数据的独立性,这是一个不得已方法。

下面就简单谈谈该算法的步骤:

第一步:数据库分区.把数据库分成连续的不同的分区,每一个分区分布在不同的机器上.每一个这样的分区称之为shard。

第二步:计算F_list,也就是所有item的support count.这个计算通过一个MapReduce就可以完成.想想hadoop上word count的例子,本质上和这一步是一样的.

第三步:条目分组.将F_list里的条目分成Q个组,这样的话就行成了一个group_list,group_list里的每一个group都被分配一个group_id,每个group_list都包含一组item的集合.

第四步:并行FP Growth.这一步是关键.它也是由一个MapReduce来完成的.具体来看看.

Mapper:

这个Mapper完成的主要功能是数据库分区。它和第一步中的shard有所不同,它利用第一步shard的数据库分区,一个一个处理shard数据库分区中的每一条transaction,将transaction分成一个一个item,每一个item根据group_list映射到合适的group里去。这样的话,通过mapper,属于同一个group的item集合都被聚合到一台机器上,这样就形成了我们前面讲到的完备数据集,在下一步的reducer中就可以并行得进行FP Growth算法了。

Reducer:

基于mapper形成的完备数据集,进行local的FP_Growth算法

第五步:聚合,将各台机器上的结果聚合成最终我们需要的结果。

52C3190490D685A74CB772C81C5E0EEEF18E5288

上面的图就给出了算法步骤的框图。有了这个框图,大家可能对算法的步骤就有一定的认识了。后面的博客就针对每一步进行具体的分析。

上面的图就给出了算法步骤的框图。有了这个框图,大家可能对算法的步骤就有一定的认识了。后面的博客就针对每一步进行具体的分析。 

代码实现:

# FP树的类定义
class treeNode:
    def __init__(self, nameValue, numOccur, parentNode):
        # 节点名称
        self.name = nameValue
        # 节点出现次数
        self.count = numOccur
        # 不同项集的相同项通过nodeLink连接在一起
        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)


"""
函数说明:构建FP-tree

Parameters:
    dataSet - 需要处理的数据集合
    minSup - 最少出现的次数(支持度)
    
Returns:
    retTree - 树
    headerTable - 头指针表

Modify:
    2018-08-06
"""
def createTree(dataSet, minSup=1):
    headerTable = {}
    # 遍历数据表中的每一行数据
    for trans in dataSet:
        # 遍历每一行数据中的每一个数据元素
        # 统计每一个字母出现的次数,将次数保存在headerTable中
        for item in trans:
            # 字典get()函数返回指定键的值,如果值不在字典中返回0。
            # 由于dataSet里的每个列表均为frozenset所以每一个列表的值均为1即dataSet[trans]=1
            headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
    # 遍历headerTable中的每一个字母
    # 若headerTable中的字母出现的次数小于minSup则把这个字母删除处理
    lessThanMinsup = list(filter(lambda k:headerTable[k] < minSup, headerTable.keys()))
    for k in lessThanMinsup: del(headerTable[k])
    for k in list(headerTable):
        if headerTable[k] < minSup:
            del(headerTable[k])
    # 将出现次数在minSup次以上的字母保存在freqItemSet中
    freqItemSet = set(headerTable.keys())
    # 如果没有达标的则返回None
    if len(freqItemSet) == 0:
        return None, None
    # 此时的headerTable中存放着出现次数在minSup以上的字母以及每个字母出现的次数
    # headerTable这个字典被称为头指针表
    for k in headerTable:
        # 保存计数值及指向每种类型第一个元素的指针
        headerTable[k] = [headerTable[k], None]
    # 初始化tree
    retTree = treeNode('Null Set', 1, None)
    # 遍历dataSet的每一组数据以及这组数据出现的次数
    for tranSet, count in dataSet.items():
        localD = {}
        # 遍历一组数据中的每一个字母
        for item in tranSet:
            # 如果这个字母出现在头指针表中
            if item in freqItemSet:
                # 将这个字母以及它在头指针表中出现的次数存储在localD中
                localD[item] = headerTable[item][0]
        # localD中存放的字母多于一个
        if len(localD) > 0:
            # 将字母按照出现的次数按降序排列
            ordereItems = [v[0] for v in sorted(localD.items(), key=lambda p:(p[1], p[0]), reverse=True)]
            # 对树进行更新
            updateTree(ordereItems, retTree, headerTable, count)
    # 返回树和头指针表
    return retTree, headerTable


"""
函数说明:更新树

Parameters:
    items - 将字母按照出现的次数按降序排列
    inTree - 树
    headerTable - 头指针表
    count - dataSet的每一组数据出现的次数,在本例中均为1
    
Returns:
    None

Modify:
    2018-08-06
"""
def updateTree(items, inTree, headerTable, 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]])
    # 仍有未分配完的树,迭代
    if len(items) > 1:
        updateTree(items[1:], inTree.children[items[0]], headerTable, count)


"""
函数说明:更新树

Parameters:
    nodeToTest - 需要插入的节点
    targetNode - 目标节点
    
Returns:
    None

Modify:
    2018-08-06
"""
def updateHeader(nodeToTest, targetNode):
    while(nodeToTest.nodeLink != None):
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode


"""
函数说明:创建数据集

Parameters:
    None
    
Returns:
    simpDat - 返回生成的数据集

Modify:
    2018-08-06
"""
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


"""
函数说明:将数据集数据项转换为frozenset并保存在字典中,其值均为1

Parameters:
    dataSet - 生成的数据集
    
Returns:
    retDict - 保存在字典中的数据集

Modify:
    2018-08-06
"""
def createInitSet(dataSet):
    retDict = {}
    for trans in dataSet:
        fset = frozenset(trans)
        retDict.setdefault(fset, 0)
        retDict[fset] += 1
        # retDict[frozenset(trans)] = 1
    return retDict


"""
函数说明:寻找当前非空节点的前缀

Parameters:
    leafNode - 当前选定的节点
    prefixPath - 当前节点的前缀
    
Returns:
    None

Modify:
    2018-08-06
"""
def ascendTree(leafNode, prefixPath):
    # 当前节点的父节点不为空
    if leafNode.parent != None:
        # 当前节点添加入前缀列表
        prefixPath.append(leafNode.name)
        # 递归遍历所有前缀路线节点
        ascendTree(leafNode.parent, prefixPath)


"""
函数说明:返回条件模式基

Parameters:
    basePat - 头指针列表中的元素
    treeNode - 树中的节点
    
Returns:
    condPats - 返回条件模式基

Modify:
    2018-08-06
"""
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


"""
函数说明:递归查找频繁项集

Parameters:
    inTree - 初始创建的FP树
    headerTable - 头指针表
    minSup - 最小支持度
    preFix - 前缀
    freqItemList - 条件树
    
Returns:
    None

Modify:
    2018-08-06
"""
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
    # 从头指针表的底端开始
    bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: str(p[1]))]
    for basePat in bigL:
        # 加入频繁项表
        newFreqSet = preFix.copy()
        newFreqSet.add(basePat)
        # print('finalFrequent Item: ', newFreqSet)
        freqItemList.append(newFreqSet)
        # 创造条件基
        condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
        # print('condPattBases: ', basePat, condPattBases)
        # 从条件模式来构建条件树
        myContTree, myHead = createTree(condPattBases, minSup)
        # print('head from conditional tree: ', myHead)
        # 挖掘条件FP树,直到条件树中没有元素为止
        if myHead != None:
            print('conditional tree for: ', newFreqSet)
            myContTree.disp(1)
            mineTree(myContTree, myHead, minSup, newFreqSet, freqItemList)
            

In [5]:

# 导入数据
parsedDat = [line.split() for line in open('kosarak.dat').readlines()]
parsedDat[:10]

Out[5]:

[['1', '2', '3'],
 ['1'],
 ['4', '5', '6', '7'],
 ['1', '8'],
 ['9', '10'],
 ['11', '6', '12', '13', '14', '15', '16'],
 ['1', '3', '7'],
 ['17', '18'],
 ['11', '6', '19', '20', '21', '22', '23', '24'],
 ['1', '25', '3']]

In [ ]:

# 数据初始化为字典
initSet = createInitSet(parsedDat)
initSet

{frozenset({'1', '2', '3'}): 180, frozenset({'1'}): 9766, frozenset({'4', '5', '6', '7'}): 1, frozenset({'1', '8'}): 1, frozenset({'10', '9'}): 1, frozenset({'11', '12', '13', '14', '15', '16', '6'}): 1, frozenset({'1', '3', '7'}): 149, frozenset({'17', '18'}): 2, frozenset({'11', '19', '20', '21', '22', '23', '24', '6'}): 1, frozenset({'1', '25', '3'}): 7,

In [12]:

# 创建FP树
myFPtree, myHeaderTab = createTree(initSet, 100000)

In [14]:

myFPtree.children

Out[14]:

{'1': <__main__.treeNode at 0x1ff7c269668>,
 '11': <__main__.treeNode at 0x1ff7c269780>,
 '3': <__main__.treeNode at 0x1ff4796cbe0>,
 '6': <__main__.treeNode at 0x1ff7c269588>}

In [15]:

myHeaderTab

Out[15]:

{'1': [197522, <__main__.treeNode at 0x1ff7c269630>],
 '11': [364065, <__main__.treeNode at 0x1ff7c269358>],
 '3': [450031, <__main__.treeNode at 0x1ff4796cbe0>],
 '6': [601374, <__main__.treeNode at 0x1ff7c269588>]}

In [16]:

myFreqList = []
# 查找频繁项集,查找至少被10万人浏览过的新闻报道
mineTree(myFPtree, myHeaderTab, 100000, set([]), myFreqList)
# 打印频繁项集(哪些报道浏览量达到支持度)
print(myFreqList)
conditional tree for:  {'1'}
   Null Set   1
     6   132113
conditional tree for:  {'11'}
   Null Set   1
     6   324013
       3   143682
     3   17604
conditional tree for:  {'11', '3'}
   Null Set   1
     6   143682
conditional tree for:  {'3'}
   Null Set   1
     6   265180
[{'1'}, {'6', '1'}, {'11'}, {'11', '3'}, {'11', '3', '6'}, {'11', '6'}, {'3'}, {'3', '6'}, {'6'}]
 

猜你喜欢

转载自blog.csdn.net/u014033218/article/details/89289326