关联规则--FpGrowth算法思想及编程实现
本文为博主原创文章,转载请注明出处,并附上原文链接。
原文链接:https://blog.csdn.net/qq_39872846/article/details/106042796
FpGrowth算法,全称:Frequent Pattern Growth—-频繁模式增长,该算法是Apriori算法的改进版本(若是不会这个算法或是有点遗忘了,可以移步到我的这篇博客大白话解析Apriori算法python实现(含源代码详解)),我们知道Apriori算法为了产生频繁模式项集,需要对数据库多次扫描,当数据库内容太大,那么算法运行的时间是难以忍受的,因此有人提出了FpGrowth算法,只须扫描数据库两次即可求出频繁项集,大大的缩减了扫描数据库的时间,下面我会尽量用简单易懂的语言描述这个算法所需要的概念及算法思想,希望对读者有帮助!
构建FpTree
(以下文章中所用到的概念在上一篇文章中已经详细解释过了,这里不再赘述,如果对概念有疑问,请移步这篇文章大白话解析Apriori算法python实现(含源代码详解))
FpGrowth算法最经典的思想就是构建一颗树来压缩大量数据记录,如何把多条数据记录压缩到一颗树中呢?用一个例子就可以清晰表达出这个思想。
我们以下面的数据记录为例:
(假设某超市有6种商品,以下是5个顾客的购买情况,我们依旧不关心购买数量,只关心购买种类)
| 顾客ID | 购买种类(Item) |
|---|---|
| T1 | 牛奶,面包 |
| T2 | 面包, 尿布, 啤酒, 鸡蛋 |
| T3 | 牛奶, 尿布, 啤酒, 可乐 |
| T4 | 面包 ,牛奶, 尿布, 啤酒 |
| T5 | 面包, 牛奶, 尿布, 可乐 |
为了方便程序实现,把这些商品种类替换为字母表示:
| 牛奶 --> a | 面包 --> b | 尿布 --> c |
| 啤酒 --> d | 可乐 --> e | 鸡蛋 --> f |
替换后数据记录如下:
| 顾客ID | 购买种类(Item) |
|---|---|
| T1 | a, b |
| T2 | b, c, d, f |
| T3 | a, c, d, e |
| T4 | b, a, c, d |
| T5 | b, a, c, e |
【注意】:为了方便程序实现简化代码,我依旧只使用支持度来判断。
-
对数据库进行第一次扫描,找出所给数据记录中商品的种类,并且对每一种商品出现的次数进行计数,即可得出第一次扫描结果,结果如下:
(这里有个细节,我们求出每种商品出现的次数后,需要按照 出现次数 的大小,由大到小排列,如果出现次数相同,则先后顺序无所谓)

这个时候,就要用到支持度的概念了,我们知道,如果某一种商品组合(这个组合,可以是1个,或者2个,甚至3个以上的不同种类商品的组合),在整条数据库中出现的次数太少,那我就认为他们没有关联, 这个道理很显然。就是对应与Apriori两个定理之一,非频繁项集的超集一定不是频繁项集。所以,这里我设置最小支持度为3,只要支持度大于等于3,我就认为这个商品组合是频繁项集。
因此,对于上表,去除不符合条件的项集 e 和 f,结果如下:

-
第二次扫描数据库,开始创建FpTree, 初始时,先创建一个根节点,记为null。
首先对于每一条数据记录,先对里面的商品种类按照 “某种顺序” 排序,(这个某种顺序是,在第一次扫描数据库后,按照其商品出现次数,由大到小排列后,其对应的商品种类顺序。这个例子有点巧合,按照各个商品出现次数由大到小排序后,商品种类的顺序恰好是字母顺序 abcdef ,在真实的数据记录中,不一定是这样。比如,现在有一个新的数据记录,进行统计后,发现,a出现2次,b出现5次,c出现10次,d出现1次,e出现15次,f出现6次,那么,按照出现次数由大到小排序后,这个 “某种顺序” ,就是 ecfbad ,每条数据记录都要按照这个奇怪的顺序排列,不再是按照字母表顺序了 )。
现在对于第一条已经排好序的记录,就是(a,b)这条记录,先创建一个节点,命名为a,将其插入根节点null下,并且在这个节点内,设置一个count变量,令count=1,接着在创建一个节点,命名为b,将其插入节点a下,同样的,在节点b内,令它的count=1,至此,第一条记录扫描完成,形成的树见下图:

对于二条已经排好序的记录,就是(b, c, d, f),我们要先去除那些不是频繁项集的字母,也就是f,f的支持度为1,比最小支持度小。过滤掉这些非频繁项集后,第二条记录变为了(b, c, d)。现在开始插入节点,从根节点开始看,由于根节点的孩子中没有b这个孩子,那么现在创建一个节点b,把它插入根节点下,令这个b节点的count=1。在创建一个c节点,插入刚才的b节点下,令c节点count=1。在创建一个d节点,将其插入刚才的c节点下,令d节点的count=1。至此第二条记录扫描完毕,此时FpTree的结构如下图:

对于第三条已经排好序的记录,就是(a, c, d, e)同样的,我们要过滤到非频繁项集,所以第三条记录变为了(a, c, d)。现在开始插入节点,从根节点开始看,发现根节点null的孩子中有a节点,那么我们就不创建新节点了,我们将这个的a节点的count + 1,即现在a节点的count=2。接着看a节点的孩子中有没有c节点,发现找不到,则创建一个c节点,令count=1,将其插入a节点下。最后,创建一个d节点,令count=1,将其插入刚刚创建的c节点下。至此,第三条记录扫描完毕,现在的FpTree形状如下:

对于第四条已经排好序的记录,就是(a, b, c, d),过滤掉非频繁项集,第四条记录变为了(a, b, c, d)。现在开始插入节点,依旧从根节点开始看,发现根节点null的孩子中有a节点,则不创建新节点了,我们将这个的a节点的count + 1,即现在a节点的count=3。然后看a节点的孩子中有没有b节点,发现有b节点,那么不用创建了,直接对这个b节点的count + 1即可,现在这个b节点count=2。接着看这个b节点的孩子中有没有c,我们发现找不到c,则创建一个c节点,令count=1,插入刚才的b节点中。最后,在创建一个d节点,令count=1,插入刚才在c节点中。至此,第四条记录扫描完毕,现在的FpTree形状如下:
对于第5条排好序的数据记录,即(a, b, c, e),过滤到非频繁项集,得(a, b, c)。从根节点开始看起,发现根节点的孩子中存在a节点,因此,对这个a节点的count+1,即count=4。在看这个a节点的孩子中存在b节点,太好了,我们又不用创建b节点了,直接令这个b节点的count+1,即现在count=3。最后,这个b节点的孩子中存在c节点,我们依旧不用创建新的c节点了,直接令这个c节点的count+1,即现在c节点count=2。至此,整个数据库扫描完毕,最后生成的FpTree形状如下:

经过上面的学习,我们就可以大致猜出,每个节点所包含的信息有哪些了,请看下图:

idName:节点名字
childs: 指向该节点所有孩子节点的地址
parent: 指向该节点的父亲节点(在挖掘关联规则时,需要找到父亲节点)
nextCommonId: 指向下一个节点,这个节点与该节点名字相同(用于构造线索,文章下文有说明)
idCount: 就是在刚才构造这颗树时,我们说的count,用于计数
FpTree线索的构造
刚才我们已经构建出了FpTree,这个树有什么意义呢?
我们知道在最开始的数据记录中,数据松散不堪,每条记录中,各个商品只出现1次。那么经过这样的变换,我们可以通过某种特殊方式,遍历这个树,就可以还原最开始的数据集,(这个特殊方法是,对这颗树,我们从根节点开始,随意找一条路径,这条路径的结尾节点必须是尾节点(尾节点就是,该节点没有孩子节点),那么这条路径就是一条记录,将这条记录取出后,别忘了对这条路径上的每个节点的count-1。在这个过程中,如果某个节点的count=0了,那么就移除这个节点。现在清楚了,只要按照这个方法,就可以完美还原出初始的数据集了,显然,这种树状的数据存储方式极大的压缩了原来的数据集,非常实用)。
但是,您应当已经注意到了,这颗树的节点,有许多名字相同的节点,其实,在最后,我们对这个树进行挖掘关联规则时,您就知道这个现象的用处了。
事实上,您如果把相同名字节点的count加起来,其结果就是该商品在总数据记录中出现的总次数。比如 b节点,在FpTree中有2个节点,其count值相加,即 3 + 1 = 4 ,正好是b在总记录中出现的次数。其他节点也是如此。
为了方便我们后续用这颗树挖掘关联规则,我们需要建立一个线索,把这些节点名字相同的节点用链表串起来。
我们可以创建一个表头,以它为链表的头部,把名字相同的节点串起来。

挖掘关联规则
频繁模式树FpTree建好后,线索也建立了,现在开始挖掘关联规则。
注意:在这里,我只认为支持度大于最小支持度就认为它是频繁项,最小支持度依旧为3。

上图就是第一次扫描数据库后,按照商品出现次数排序后的表格,挖掘关联规则时,要首先从表尾开始挖掘。
-
在这个例子中,就是从d节点开始,根据我们创建的线索表,可以很轻松的找出所有相同节点,然后可以写出这些节点所在的节点分支,即(a,b,c,d :1),(a,c,d :1),(b,c,d :1),后面的数字 1 是每条分支的最后一个节点的count值。这里要注意一下,虽然 a 出现了 4 次(即a的count=4),但是 a,b,c,d 这个整体只出现了 1 次,这取决与,在这条分支上,某个节点的最小count值。
现在,我们去除d节点,(直接把所有的d节点删除),那么就可以得到d的前缀节点(d的前缀节点就是d节点前面的节点),即{ (a,b,c :1),(a,c :1),(b,c :1) }, 还要注意一下,这里的数字 ”1“,依旧是原来的d节点的count值,不是c节点的count值。
好了,做到这步,我们其实得到了一个全新的数据记录:Id 商品种类 T1 a,b,c T2 a,c T3 b,c 看到这个类似的表,是不是很熟悉,我们需要根据现在这个新的数据记录表,开始构建一个新的FpTree,方法依旧和上文一样,这里只给出新的FpTree的结构:

这个新的FpTree已经建立好了,只要这个新的FpTree的路径不是1条,那我们就继续递归的挖掘。
如果新的FpTree的路径恰好就1条,那么这条路径上所有节点的组合就是条件频繁项集,假设 d 节点的条件频繁项集是 x,y,z ,那么a的频繁项集就是 (x,d),(y,d),(z,d),可以看到这里的每个项集都有a这个后缀,因为您本来就是依据d节点建立的新数据集,求出这个新的数据集的条件频繁项集后,要求出原来的数据集的频繁项集,就要在这个条件频繁项集的后面加上 d节点。回到这个具体的例子,可以看到新的数据记录构建的FpTree,只有一条路径,那么递归结束,这条路径有两个节点(null,c),一定不要忘记null节点,null就是表示一个空节点。
两个节点的所有组合就是条件频繁项集,即{ null,c },有一点需要注意,(null,c)和(c)是一样的,这两个没有区别。那么,最后,d的频繁项集就是{ (d),(c,d) }。 -
接下来看表头的倒数第二个项,即 c 节点,根据构建的线索,找出所有的c节点,节点所在的所有分支为 {(a,b,c :2),(a,c :1),(b,c :1)},(提示:后面的数字是c节点的count值,数字 ”2“ 代表这条分支出现了2次),同样的,去除掉c节点,(去除c节点时,c节点后面的节点同样也被去掉了),故可得新是数据集{(a,b :2),(a :1),(b :1)},做个表格,如下:
ID 商品种类 T1 a,b T2 a,b T3 a T4 b a,b这个组合在表格中出现了2次,因为它在分支中出现了2次,(在这里,我之所以把它重复2次的写入数据记录表中,其实是为了能复用我们之前的代码,这样就不用重新在写一个函数了。在实际的数据情况中,如果a,b组合出现了1000次,我如果把它展开到数据记录中,这个数据记录就有1000行相同信息的记录了,这样显然不合适,我们需要在编写一个函数,在构建FpTree时,直接把这条数据的count设为1000即可,这样会大大节省时间)
对上面这个新的表构建FpTree,结构如下图:

它同样是一条单一路径,路径上节点的所有组合即为条件频繁项集,别忘了空节点null,条件频繁项集为(null),(a),(b),(a,b), 那么c的频繁项集就是 { (c),(a,c),(b,c),(a,b,c) }。
显然,这一组的频繁项集始终有一个相同的后缀 c,因此这一组的频繁项集永远不会和上一组重复。 -
在看倒数第3个节点,b节点,处理方式和前两个一样,留给读者做练习,这里直接给出该节点的频繁模式树FpTree:

-
最后一个节点,a节点,它的条件频繁项集是 { null },频繁项集就是{ (a) }。
-
至此,整个算法流程已经叙述完了,补充一点,在不断递归求新的数据记录,建立的新的FpTree时,只要新的FpTree路径不是1条,就可以继续递归,直到新的FpTree路径为1时,结束递归,进行处理。当然也可以用迭代,只不过代码会显的一点多,不如递归清晰。
python代码实现
运行环境 python3.6 PyCharm
先给出运行结果:

相应代码如下:
这颗树的节点结构,用python类的表示:
'''
Frequent Pattern Tree 繁模式树
牛奶 a 面包 b 尿布 c 啤酒 d 可乐 e 鸡蛋 f
所用数据集
T1 {牛奶,面包}
T2 {面包,尿布,啤酒,鸡蛋}
T3 {牛奶,尿布,啤酒,可乐}
T4 {面包,牛奶,尿布,啤酒}
T5 {面包,牛奶,尿布,可乐}
转化为字母表示,即
数据集
database = [ ['a', 'b'],
['b', 'c', 'd', 'f'],
['a', 'c', 'd', 'e'],
['b', 'a', 'c', 'd'],
['b', 'a', 'c', 'e'] ]
'''
class FpNode():
def __init__(self, name='', childs={
}, parent={
}, nextCommonId={
}, idCount=0):
self.idName = name # 名字
self.childs = childs # 所有孩子结点
self.parent = parent # 父节点
self.nextCommonId = nextCommonId # 下一个相同的 id名字 结点
self.idCount = idCount # id 计数
def getName(self): #获取该节点名字
return self.idName
def getAllChildsName(self): #获取该节点所有孩子节点的名字
ch = self.childs
keys = list(ch.keys())
names = []
for i in keys:
names.append( list(i))
return names
def printAllInfo(self):

最低0.47元/天 解锁文章
7328





