很多人都听说过“尿布和啤酒”的故事:据说,美国中西部的一家连锁店发现,男人们去超市买尿布的同时,往往会顺便给自己购买啤酒。由此,卖场开始把啤酒和尿布摆放在相同区域,让男人可以同时找到这两件商品,从而获得了很好的销售收入。虽然并没有商店真的把这两样东西放在一起,但是很多商家确实将大家经常购买的物品放在一起捆绑销售以鼓励大家购买。那么我们如何在繁杂的数据发现这些隐含关系呢?这就需要关联分析(association analysis),本文所讨论的Apriori便是其中一种关联分析算法。
基本概念 关联分析是一种在大规模数据集中寻找有趣关系的任务。这些关系有两种形式:频繁项集、关联规则。频繁项集(frequent item sets)是经常出现在一块的物品的集合;关联规则(association rules)暗示两种物品之间可能存在很强的关系。
以下是某个杂货店的交易清单:
交易号码
商品
0
豆奶,莴苣
1
莴苣,尿布,葡萄酒,甜菜
2
豆奶,尿布,葡萄酒,橙汁
3
莴苣,豆奶,尿布,葡萄酒
4
莴苣,豆奶,尿布,橙汁
频繁项集:经常出现在一起的物品集合,如{葡萄酒,尿布,豆奶}就是一个频繁项集。
支持度(support):如何有效定义频繁?其中最重要的两个概念是支持度和可信度。一个项集的支持度被定义为数据集中包含该项集的记录所占的比例。还是上面的例子,豆奶在5条交易中出现了4次,因此{豆奶}的支持度为4/5,同理可知,{豆奶,尿布}的支持度为3/5。我们可以定义一个最小支持度,从而只保留满足最小支持度的项集。
可信度或置信度(confidence):是针对一条关联规则来定义的。例如:我们要讨论{尿布}→{葡萄酒}的关联规则,它的可信度被定义为“支持度({尿布,葡萄酒}) / 支持度({尿布})”。因为{尿布, 葡萄酒}的支持度为3/5,{尿布}的支持度为4/5,所以“尿布→葡萄酒”的可信度为3/4=0.75。
Apriori原理 假设我们在经营一家商品种类并不多的杂货店,我们对那些经常一起被购买的商品很感兴趣。我们只有4种商品:商品0,商品1,商品2和商品3。那么所有可能被一起购买的商品组合有哪些?下图显示了物品之间所有可能的组合。
如何对一条给定的集合,如{0,3},来计算其支持度?通常我们遍历每条记录并检查该记录包含0和3,如果记录确实包含两项,那么就增加总计数值。在扫描完每条数据后,使用统计的总数除以总交易记录数,就可以得到支持率。同样地,如果要获得每种可能集合的支持度就要多次重复上述过程。对于包含4种物品的集合,需要遍历数据15次。而随着物品数目的增加,遍历次数会急剧增长。对于包含N中物品的数据集共有2N −1中项集组合,对于只出售100中商品的商店也会有1.26×1030 中可能的项集组合。对于现代计算机,需要很长的时间才能完成运算。
Apriori原理可以帮助我们减少感兴趣的项集。Apriori原理是指如果某个项集是频繁的,那么它的所有子集也是频繁的。反过来,如果一个项集是非频繁集,那么它的所有超集也是非频繁的。
上述例子中,已知阴影项集{2,3}是非频繁的。利用这个知识,我们就知道项集{0,2,3} ,{1,2,3}以及{0,1,2,3}也是非频繁的。这也就是说,一旦计算出了{2,3}的支持度,知道它是非频繁的之后,就不需要再计算{0,2,3}、{1,2,3}和{0,1,2,3}的支持度,因为我们知道这些集合不会满足我们的要求。使用该原理就可以避免项集数目的指数增长,从而在合理时间内计算出频繁项集。
Apriori算法
发现频繁项集的过程如上图所示:
由数据集生成候选项集C1(1表示每个候选项仅有一个数据项);再由C1通过支持度过滤,生成频繁项集L1(1表示每个频繁项仅有一个数据项)。
将L1的数据项两两拼接成C2。
从候选项集C2开始,通过支持度过滤生成L2。L2根据Apriori原理拼接成候选项集C3;C3通过支持度过滤生成L3……直到Lk中仅有一个或没有数据项为止。
回到上面的杂货店例子,令最小支持度为0.4,结果如下图:
值得注意的是L3到C4这一步并没有得到候选项集,这是由于Apriori算法由两部分组成(在这里假定购买商品是有顺序的)。
连接:对K-1项集中的每个项集中的项排序,只有在前K-1项相同时才将这两项合并,形成候选K项集(因为必须形成K项集,所以只有在前K-1项相同,第K项不相同的情况下才合并。)
剪枝:对于候选K项集,要验证所有项集的所有K-1子集是否频繁(是否在K-1项集中),去掉不满足的项集,就形成了K项集。比如C4连接的{尿布,莴苣,葡萄酒,豆奶}的子集{莴苣,葡萄酒,豆奶}不存在于L3,因此要去掉。
实现Apriori代码 根据以上原理构造数据集扫描的Python代码,其伪代码大致如下:
1 2 3 4 5 6 7 对数据集中的每条交易记录tran 对每个候选项集can : 检查一下can是否是tran的子集 : 如果是,则增加can的计数值 对每个候选项集 : 如果其支持度不低于最小值,则保留该项集 返回所有频繁项集列表
建立辅助函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 def loadDataSet () : return [[1 ,3 ,4 ], [2 ,3 ,5 ], [1 ,2 ,3 ,5 ], [2 ,5 ]] def createC1 (dataSet) : C1 = [] for transaction in dataSet : for item in transaction : if not [item] in C1 : C1.append([item]) C1.sort() return list(map(frozenset,C1)) def scanD (D, Ck, minSupport) : ssCnt = {} for tid in D : for can in Ck : if can.issubset(tid) : if not can in ssCnt: ssCnt[can]=1 else : ssCnt[can] += 1 numItems = float(len(D)) retList = [] supportData = {} for key in ssCnt : support = ssCnt[key]/numItems if support >= minSupport : retList.insert(0 , key) supportData[key] = support return retList, supportData
保存为apriori.py,运行效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 >>> import apriori>>> dataSet = apriori.loadDataSet()>>> dataSet[[1 , 3 , 4 ], [2 , 3 , 5 ], [1 , 2 , 3 , 5 ], [2 , 5 ]] >>> C1 = apriori.createC1(dataSet)>>> C1[frozenset([1 ]), frozenset([2 ]), frozenset([3 ]), frozenset([4 ]), frozenset([5 ])] >>> D = list(map(set, dataSet))>>> D[{1 , 3 , 4 }, {2 , 3 , 5 }, {1 , 2 , 3 , 5 }, {2 , 5 }] >>> L1, suppData0 = apriori.scanD(D, C1, 0.5 )>>> L1[frozenset([5 ]), frozenset([2 ]), frozenset([3 ]), frozenset([1 ])]
整个Apriori算法的伪代码如下:
1 2 3 4 当集合中项的个数大于0时 构建一个k个项组成的候选项集的列表 检查数据以确认每个项集都是频繁的 保留频繁项集并构建k+1项组成的候选项集的列表
将如下算法代码加入apriori.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 def aprioriGen (Lk, k) : retList = [] lenLk = len(Lk) for i in range(lenLk) : for j in range(i+1 , lenLk) : L1 = list(Lk[i])[:k-2 ] L2 = list(Lk[j])[:k-2 ] L1.sort() L2.sort() if L1==L2 : retList.append(Lk[i] | Lk[j]) return retList def apriori (dataSet, minSupport = 0.5 ) : C1 = createC1(dataSet) D = list(map(set, dataSet)) L1, supportData = scanD(D, C1, minSupport) L = [L1] k = 2 while (len(L[k-2 ]) > 0 ) : Ck = aprioriGen(L[k-2 ], k) Lk, supK = scanD(D, Ck, minSupport) supportData.update(supK) L.append(Lk) k += 1 return L, supportData
上面的k-2可能会令人困惑,接下来讨论其细节。当利用{0}、{1}、{2}构建{0,1}、{0,2}、{1,2}时,实际上是将单个项组合到一块。现在如果想利用{0,1}、{0,2}、{1,2}来创建三元素项集,应该怎么做?如果将每两个集合合并,就会得到{0,1,2}、{0,1,2}、{0,1,2}。也就是同样的结果集合会重复3次。接下来需要扫描三元素项集列表来得到非重复结果,我们要做的是确保遍历列表的次数最少。现在,如果比较集合{0,1}、{0,2}、{1,2}的第一个元素并只对第一个元素相同的集合求并操作,又会得到什么结果?{0,1,2},而且只有一次操作,这样就不用遍历列表来寻找非重复值了。
保存后运行效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 >>> L, supportData = apriori.apriori(dataSet)>>> L[[frozenset([5 ]), frozenset([2 ]), frozenset([3 ]), frozenset([1 ])], [frozenset([2 , 3 ]), frozenset([3 , 5 ]), frozenset([2 , 5 ]), frozenset([1 , 3 ])], [frozenset([2 , 3 , 5 ])], []]>>> L[0 ][frozenset([5 ]), frozenset([2 ]), frozenset([3 ]), frozenset([1 ])] >>> L[1 ][frozenset([2 , 3 ]), frozenset([3 , 5 ]), frozenset([2 , 5 ]), frozenset([1 , 3 ])] >>> L[2 ][frozenset([2 , 3 , 5 ])] >>> L[3 ][] >>> apriori.aprioriGen(L[0 ], 2 )[frozenset([2 , 5 ]), frozenset([3 , 5 ]), frozenset([1 , 5 ]), frozenset([2 , 3 ]), fro zenset([1 , 2 ]), frozenset([1 , 3 ])] >>> L,support = apriori.apriori(dataSet, minSupport=0.7 )>>> L[[frozenset([5 ]), frozenset([2 ]), frozenset([3 ])], [frozenset([2 , 5 ])], []]
从频繁项集中挖掘关联规则 关联分析的两个重要目标是发现频繁项集 与关联规则 。要找到关联规则,首先从一个频繁项集开始,集合中的元素是不重复的,但我们想知道基于这些元素能否获得其他内容。某个元素或者某个元素集合可能会推导出另一个元素。例如,一个频繁项集{豆奶, 莴苣},可能有一条关联规则“豆奶→莴苣”,这意味着如果有人购买了豆奶,那么在统计上他购买莴苣的概率较大。但是这条反过来并不总是成立。换言之,即使“豆奶→莴苣”统计上显著,那么“莴苣→豆奶”也不一定成立。箭头的左边集合称作前件 ,箭头右边的集合称为后件 。
上节我们给出了繁琐项集的量化定义,即它满足最小支持度要求。对于关联规则,我们也有类似量化方法,这种量化标准称为可信度。一条规则P→H的可信度定义为support(P | H) / support(P)。在前面我们已经计算了所有繁琐项集支持度,要想获得可信度,只需要再做一次除法运算。
从一个繁琐项集中可以产生多少条关联规则?下图给出了从项集{0,1,2,3}产生的所有关联规则。为了找到感兴趣的规则,我们先生成一个可能的规则列表,然后测试每条规则可信度。如果可信度不满足最小要求,则去掉该规则。
可以观察到,如果某条规则并不满足最小可信度要求,那么该规则的所有子集也不会满足最小可信度要求。具体而言,如果012→3是一条低可信度规则,则所有其它3为后件的规则都是低可信度。这需要从可信度的概念去理解,Confidence(012→3) = P(3|0,1,2), Confidence(01→23)=P(2,3|0,1),P(3|0,1,2) >= P(2,3|0,1)。由此可以对关联规则做剪枝处理。
还是以之前的杂货店交易数据为例,我们发现了以下频繁项集:
对于寻找关联规则来说,频繁1项集L1没有用处,因为L1中的每个集合仅有一个数据项,至少有两个数据项才能生成A→B这样的关联规则。取置信度为0.7,最终从L2发掘出10条关联规则:
接下来是L3:
假设有一个L4项集(文中的数据不能生成L4),其挖掘过程如下:
利用此性质来减少测试的规则数目,可以先从一个频繁项集开始,接着创建一个规则列表,其中规则右部只包含一个元素,然后对这些规则测试。接下来合并所有剩余规则来创建一个新的规则列表,其中右部包含两个元素。这种方法称为分级法 。打开apriori.py,加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 def generateRules (L, supportData, minConf=0.7 ) : bigRuleList = [] for i in range(1 , len(L)) : for freqSet in L[i] : H1 = [frozenset([item]) for item in freqSet] if i > 1 : rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf) else : calcConf(freqSet, H1, supportData, bigRuleList, minConf) return bigRuleList def calcConf (freqSet, H, supportData, brl, minConf=0.7 ) : prunedH = [] for conseq in H : conf = supportData[freqSet] / supportData[freqSet - conseq] if conf >= minConf : print(freqSet-conseq, '-->' , conseq, 'conf:' , conf) brl.append((freqSet-conseq, conseq, conf)) prunedH.append(conseq) return prunedH def rulesFromConseq (freqSet, H, supportData, brl, minConf=0.7 ) : m = len(H[0 ]) if (len(freqSet) > (m+1 )) : Hmp1 = aprioriGen(H, m+1 ) Hmp1 = calcConf(freqSet, Hmp1, supportData, brl, minConf) if (len(Hmp1) > 1 ) : rulesFromConseq(freqSet, Hmp1, supportData, brl, minConf)
检验运行效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 >>> import apriori>>> dataSet = apriori.loadDataSet()>>> dataSet[[1 , 3 , 4 ], [2 , 3 , 5 ], [1 , 2 , 3 , 5 ], [2 , 5 ]] >>> L, supportData = apriori.apriori(dataSet, minSupport=0.5 )>>> rules = apriori.generateRules(L, supportData, minConf=0.7 )frozenset([5]) --> frozenset([2]) conf: 1.0 frozenset([2]) --> frozenset([5]) conf: 1.0 frozenset([1]) --> frozenset([3]) conf: 1.0 >>> rules[(frozenset([5 ]), frozenset([2 ]), 1.0 ), (frozenset([2 ]), frozenset([5 ]), 1.0 ), ( frozenset([1 ]), frozenset([3 ]), 1.0 )] >>> rules = apriori.generateRules(L, supportData, minConf=0.5 )frozenset([3]) --> frozenset([2]) conf: 0.666666666667 frozenset([2]) --> frozenset([3]) conf: 0.666666666667 frozenset([5]) --> frozenset([3]) conf: 0.666666666667 frozenset([3]) --> frozenset([5]) conf: 0.666666666667 frozenset([5]) --> frozenset([2]) conf: 1.0 frozenset([2]) --> frozenset([5]) conf: 1.0 frozenset([3]) --> frozenset([1]) conf: 0.666666666667 frozenset([1]) --> frozenset([3]) conf: 1.0 frozenset([5]) --> frozenset([2, 3]) conf: 0.666666666667 frozenset([3]) --> frozenset([2, 5]) conf: 0.666666666667 frozenset([2]) --> frozenset([3, 5]) conf: 0.666666666667 >>> rules[(frozenset({3 }), frozenset({2 }), 0.6666666666666666 ), (frozenset({2 }), frozenset({3 }), 0.6666666666666666 ), (frozenset({5 }), frozenset({3 }), 0.6666666666666666 ), (frozenset({3 }), frozenset({5 }), 0.6666666666666666 ), (frozenset({5 }), frozenset({2 }), 1.0 ), (frozenset({2 }), frozenset({5 }), 1.0 ), (frozenset({3 }), frozenset({1 }), 0.6666666666666666 ), (frozenset({1 }), frozenset({3 }), 1.0 ), (frozenset({5 }), frozenset({2 , 3 }), 0.6666666666666666 ), (frozenset({3 }), frozenset({2 , 5 }), 0.6666666666666666 ), (frozenset({2 }), frozenset({3 , 5 }), 0.6666666666666666 )]
Apriori应用 之前我们在小数据上应用了apriori算法,接下来要在更大的真实数据集上测试效果。那么可以使用什么样的数据呢?比如:购物篮分析,搜索引擎的查询词,国会投票,毒蘑菇的相似特征提取等;
示例:发现毒蘑菇的相似特征 从此处 下载mushroom.dat,其前几行如下:
1 2 3 4 5 6 7 1 3 9 13 23 25 34 36 38 40 52 54 59 63 67 76 85 86 90 93 98 107 113 2 3 9 14 23 26 34 36 39 40 52 55 59 63 67 76 85 86 90 93 99 108 114 2 4 9 15 23 27 34 36 39 41 52 55 59 63 67 76 85 86 90 93 99 108 115 1 3 10 15 23 25 34 36 38 41 52 54 59 63 67 76 85 86 90 93 98 107 113 2 3 9 16 24 28 34 37 39 40 53 54 59 63 67 76 85 86 90 94 99 109 114 2 3 10 14 23 26 34 36 39 41 52 55 59 63 67 76 85 86 90 93 98 108 114 2 4 9 15 23 26 34 36 39 42 52 55 59 63 67 76 85 86 90 93 98 108 115
第一个特征表示有毒或者可食用,有毒为2,无毒为1。下一个特征是蘑菇伞的形状,有六种可能的值,分别用整数3-8来表示。
为了找到毒蘑菇中存在的公共特征,可以运行Apriori算法来寻找包含特征值为2的频繁项集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 >>> import apriori>>> mushDatSet = [line.split() for line in open('mushroom.dat' ).readlines()]>>> L, suppData = apriori.apriori(mushDatSet, minSupport=0.3 )>>> for item in L[1 ] :... if item.intersection('2' ): print(item) ... frozenset({'28' , '2' }) frozenset({'2' , '53' }) frozenset({'2' , '23' }) frozenset({'2' , '34' }) frozenset({'2' , '36' }) frozenset({'59' , '2' }) frozenset({'63' , '2' }) frozenset({'67' , '2' }) frozenset({'2' , '76' }) frozenset({'2' , '85' }) frozenset({'2' , '86' }) frozenset({'2' , '90' }) frozenset({'93' , '2' }) frozenset({'2' , '39' }) >>> for item in L[3 ] :... if item.intersection('2' ) : print(item)... frozenset({'2' , '28' , '59' , '34' }) frozenset({'2' , '28' , '59' , '85' }) frozenset({'2' , '86' , '28' , '59' }) frozenset({'2' , '28' , '59' , '90' }) frozenset({'2' , '28' , '59' , '39' }) frozenset({'63' , '2' , '28' , '39' }) frozenset({'63' , '2' , '28' , '34' }) frozenset({'63' , '2' , '28' , '59' }) frozenset({'63' , '2' , '28' , '85' }) frozenset({'63' , '2' , '86' , '28' }) frozenset({'2' , '28' , '85' , '34' }) frozenset({'2' , '86' , '28' , '34' }) frozenset({'2' , '86' , '28' , '85' }) frozenset({'34' , '2' , '28' , '90' }) frozenset({'2' , '28' , '85' , '90' }) frozenset({'2' , '86' , '28' , '90' }) frozenset({'2' , '28' , '34' , '39' }) frozenset({'2' , '28' , '85' , '39' }) frozenset({'2' , '86' , '28' , '39' }) frozenset({'2' , '28' , '90' , '39' }) frozenset({'34' , '2' , '28' , '53' }) frozenset({'2' , '28' , '85' , '53' }) frozenset({'2' , '86' , '28' , '53' }) frozenset({'90' , '2' , '28' , '53' }) frozenset({'2' , '28' , '53' , '39' }) ......
接下来你需要观察这些特征,以便知道蘑菇的各个方面,如果看到其中任何一个特征,那么这些蘑菇就很有可能有毒。
小结 关联特征是用于发现大数据集元素间有趣关系的一个工具集,可以采用两种方法来量化这些有趣关系。第一种方法是使用频繁项集,它会给出经常在一起出现的元素项。第二种方式是关联规则,每条关联规则意味着元素之前的“如果……那么”关系。
发现元素间不同的组合是个非常耗时的任务,不可避免需要大量昂贵的计算资源,这就需要一些更智能的方法在合适的时间范围内找到频繁项集。其中一个方法是Apriori算法,它使用Apriori原理减少在数据库上进行检查的集合的数目。Apriori原理是说如果一个元素项是不频繁的,那么那些包含该元素的超集也是不频繁的。Apriori算法从单元项集开始,通过组合满足最小支持度要求的项集来形成更大的集合。支持度用来度量一个集合在原始数据中出现的频率。
关联分析可以用在许多不同物品上。商店中的商品以及网站的访问页面是其中比较常见的例子。关联分析也曾用于查看选举人及法官的投票历史。
缺点是每次增加频繁项集的大小,Apriori算法都会重新扫描整个数据集,当数据集很大时,这会显著降低频繁项集发现的速度。
参考资料:《机器学习实战》、《数据挖掘导论》