Apriori算法以及统计学基础
- 什么是关联分析
- 简单的统计学基础
- Apriori输出频繁集
- 从频繁项集中挖掘关联规则
什么是关联分析
从大规模数据集中寻找物品间的隐含关系被称作关联分析。而寻找物品的不同组合是一项十分耗时的任务,所需的计算代价很高。通过统计学方法,Apriori算法正可以解决这一问题。
物品之间的关系一般可以有两种形式:频繁项集和关联规则。
- 频繁项集:数据集中经常出现在一块的物品的集合。
- 关联规则:两种物品之间可能存在很强的关系。
下面借用一个在 《机器学习实战》第11章上的例子:
交易单号 | 商品 |
0 | 豆奶、莴苣 |
1 | 莴苣、尿布、葡萄酒、甜菜 |
2 | 豆奶、尿布、葡萄酒、橙汁 |
3 | 莴苣、豆奶、尿布、葡萄酒 |
4 | 莴苣、豆奶、尿布、橙汁 |
我们观察这5笔交易,发现集合就是频繁项集的其中一个例子, 当然
,
,
等还有很多集合也是频繁项集, 主要是取决于用户所定的标准。而关联规则是什么呢?例如如:
这就是一条关联规则,它意味着如果有人买了尿布,那么他很有可能也会买了葡萄酒。
简单的统计学基础
如何度量这些关系呢?项集怎么样才算是频繁?什么样的规则才能让我们信服?我们用支持度和可信度来度量这些有趣的关系,以及用非频繁集与超集的关系来减少计算机的计算量。
支持度:项集X出现的概率。例如,
可信度:项集A出现的条件下项集B出现的概率。还是应用上面的例子
∵,
∴
这意味着如果用户已经购买了尿布的情况下,再购买葡萄酒的概率是75%
频繁集:出现的概率满足用户给定的概率标准的项集称频繁集。
超集:如果一个集合S2中的每一个元素都在集合S1中,且集合S1中可能包含S2中没有的元素,则集合S1就是S2的一个超集。例如则可以说S1是S2的一个超集。
关联关系: 实际上就是先验概率与后验概率的关系。
根据贝叶斯公式得: 拆解分式:分母是全概率公式=
,分子是关于B和
联合概率=
,即得
,后面将会用这个公式来实现挖掘关联规则。
补充:若不动分子,将分母全概率移项可得
可以更好地理解贝叶斯公式。
怎么用呢?假设一个学校里有60%男生和40%女生。女生穿裤子的人数和穿裙子的人数相等,所有男生穿裤子。一个人在远处随机看到了一个穿裤子的学生。那么这个学生是女生的概率是多少?
使用贝叶斯定理,事件A是看到女生,事件B是看到一个穿裤子的学生。我们所要计算的是P(A|B)。
P(A)是忽略其它因素,看到女生的概率,在这里是40%
P(A’)是忽略其它因素,看到不是女生(即看到男生)的概率,在这里是60%
P(B|A)是女生穿裤子的概率,在这里是50%
P(B|A’)是男生穿裤子的概率,在这里是100%
P(B)是忽略其它因素,学生穿裤子的概率,P(B) = P(B|A)P(A) + P(B|A’)P(A’),在这里是0.5×0.4 + 1×0.6 = 0.8。
根据贝叶斯定理,我们计算出后验概率P(A|B)
P(A|B)=P(B|A)*P(A)/P(B)=0.25, 即25%可能看到的这个穿裤子的学生是女生。
讨论:为什么如果一个项集是非频繁集,那么它的所有超集也是非频繁的。
[解析]:还是应用上面的例子,假设的支持度已经降至
,相对应的
的支持度对应也将为
。通过计算尿布与葡萄酒的可信度
,虽然尿布与葡萄酒的可信度达到了50%,但是尿布的支持度太低,所以项集
出现的概率也是很低。
就是一个项集,而
则是
的一个超集。
将概率公式移项可以更容易看出,即使让
的概率高达100%,若
正是利用这个特性,可以过滤掉一些非频繁集,减少非频繁集以及非频繁集的超集的计算。
讨论:为什么如果一个项集是低可信度的,那么它的所有后件也是低可信度的。
[解析]:略,有时间会补充证明。
生成候选项(单品)的代码和计算输入频繁项支持度的代码示例:
foods = {1: '辣条', 2: '龟苓膏', 3: '薯片', 4: '可乐', 5: '面条'}
data = [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]
def createC1(dataSet):
# 生成每个单元素的集合, 且每个单品不可变是frozenset对象
C1 = []
for transaction in dataSet:
for item in transaction:
if not [item] in C1:
C1.append([item])
C1.sort()
return [i for i in map(frozenset, C1)]
def scanData(Data, Ck, minSupport):
# 剪枝函数, 若传入的Ck中,里面的候选项的支持度在数据集Data中不满足最小支持度minSupport,则被剪掉
ssCnt = dict()
for tid in Data: # 遍历给定的数据集清单中的数据: [[1, 2], [2, 3],[2, 3, 5]]
for can in Ck: # 遍历所有的候选项集合
if can.issubset(tid): # 判断候选项是否在数据集清单中 -> 返回bool
if can in ssCnt: # 如果True,增加候选项出现次数
ssCnt[can] += 1
else:
ssCnt[can] = 1
numItems = float(len(Data)) # 获取所有商品组合的长度
retList = list()
supportData = dict()
for key in ssCnt: # 遍历关于{候选项: 次数}的字典
support = ssCnt[key] / numItems # 通过 该候选项出现次数 / 数据集总条数 = 候选项支持度
if support >= minSupport: # 判断该候选项的支持度是否满足输入的最小支持度
retList.insert(0, key) # 满足则放入return list的集合中, 最后返回满足最小支持度的候选项
supportData[key] = support # 不管满不满足最小支持度,该候选项的支持度都会写入写入关于{候选项: 支持度}的字典,以便用户分析
# 最后返回满足最小支持度的候选项, 关于关于{候选项: 支持度}的字典
return retList, supportData
c1 = createC1(data)
result, resultSupport = scanData(data, c1, 0.5)
print([[foods[j] for j in i] for i in data])
print([foods[[j for j in i][0]] for i in c1])
print([foods[[j for j in i][0]] for i in result])
print({foods[[j for j in i0][0]]: resultSupport[i0] for i0 in resultSupport})
运行结果
商品交易清单: [['辣条', '薯片', '可乐'], ['龟苓膏', '薯片', '面条'], ['辣条', '龟苓膏', '薯片', '面条'], ['龟苓膏', '面条']]
商品的单品清单: ['辣条', '龟苓膏', '薯片', '可乐', '面条']
满足最小支持度的商品单品清单: ['面条', '龟苓膏', '薯片', '辣条']
总的商品单品的支持度: {'辣条': 0.5, '薯片': 0.75, '可乐': 0.25, '龟苓膏': 0.75, '面条': 0.75}
Apriori输出频繁集
Apriori就是通过计算支持度与可信度来实现的。这里再次引用上面的图,其实Apriori算法就是算最大长度的频繁项。在图中,最大频繁项就是{1,2,3} {1,2,4}
这里借用《机器学习实战》第11章清单11.2的代码
def aprioriGen(Lk, k):
# 通过Lk 生成k个元素的候选项,有一定的过滤能力
# 例如传入[{1, 2}, {1, 3}, {2, 3}], 返回[{1, 2, 3}]的组合
retList = []
lenLk = len(Lk)
# 将Lk集合的元素进行k组合
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):
# 生成dataSet的初始候选项, 即数据集的最小元素,仅有1个元素的集合。
C1 = createC1(dataSet)
# 将dataSet的列表变成集合
D = [i for i in map(set, dataSet)]
# 过滤掉不满足最小可信度的候选项, 初次过滤
L1, supportData = scanData(D, C1, minSupport)
# 初始化最终频繁项的集合
L = [L1]
k = 2 # 将初始组合为2个元素的候选项
while len(L[k-2]) > 0: # 如果新增的集合不是空的,则继续产生候选项
Ck = aprioriGen(L[k-2], k) # 生成第K个元素的候选项
Lk, supK = scanData(D, Ck, minSupport) # 将产生的候选项进行剪枝
supportData.update(supK) # 更新最终返回的支持度字典
L.append(Lk)
k += 1 # 增加下一个候选项的元素荷属
# 频繁项, 关于{算法产生的所有候选项: 支持度}的字典
# while 的停止是通过L最后一项是空确定的,所以返回L[:-1]
return L[:-1], supportData
但是我对aprioriGen有个疑问,这个算法中经过不同的例子实验,发现这并没有充分的“剪枝”作用。
aprioriGen算法解析:
功能:生成新的k元素组合的候选项,有一定的过滤能力。
核心:它是通过判断两个L1,L2数组的前k-2项是否相同来对数组进行合并,例如[1,2]和[1, 3],要合成3项数组。因为前3-2项是相同的,所以可以合并为[1, 2, 3];又如[1,2,4]和[1, 2, 3],要合成4项数组。因为前4-2项是相同的,所以合并可以为[1, 2, 3, 4]。而[1, 2], [2, 3]并不能合并成[1, 2, 3]。
测试集合一:
a = [frozenset({2, 3}), frozenset({3, 5}), frozenset({2, 5}), frozenset({1, 3})]
aprioriGen(a, 3)
a = [frozenset({1, 2}), frozenset({1, 3}), frozenset({1, 4}), frozenset({2, 3}), frozenset({2, 4})]
aprioriGen(a, 3)
>> [frozenset({2, 3, 5})]
>> [frozenset({1, 2, 3}), frozenset({1, 2, 4}), frozenset({1, 3, 4}), frozenset({2, 3, 4})]
一个M个元素的频繁项集合,需要M个子集同时指向才可成立并且这M个子集都得是频繁项。即若其中一个是非频繁项,该超集肯定不是频繁项,原因上文已经有讨论过了。通过归纳法可以发现,M个频繁的子项中,必定存在任意的2个子集,其前M-2个元素都是相同的,就是最后一个元素不同。继续采用上图,如果要得到 3个元素的集合是频繁项,必定会存在两个频繁项子集的前3-1个元素是相同的,在这中就可以看到便是子集
和 子集
。且若要想
是频繁项,还得需要子集
也是频繁项,即若想生成3个元素的集合是频繁项,则必须需要有3个是频繁项的子集。(若想继续推导,可以将上图的例子增加元素个数)。所以就可以说如果不存在一个集合中的任意两个子集,其前M-2个元素都是相同的,则可以判断,其超集肯定是非频繁项。
[测试解析] 所以在此算法测试中,例子一中只有一对子集满足条件,且第三个子集也同时存在,所以只生成了频繁项 。而例子二中产生了非频繁项
和
,这是因为算法正式通过判断子集前M-2个元素是否相同来产生组合的,而正好数组里面存在着4对子集是满足条件的,故生成了4个候选项。
结论:该算法有一定的过滤能力(即“剪枝能力”——剪掉非频繁项),但如果输入的子集越完整,过滤能力越差。
apriori算法解析
功能:通过aprioriGen产生一轮又一轮的候选项,并通过scanData进行进一步的”剪枝“,直到不再生成候选项为止。
返回:全部频繁项,关于{算法产生的所有候选项: 支持度}的字典
代码测试:
data = [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]
a1, a2 = apriori(data)
> a1
>> [[frozenset({5}), frozenset({2}), frozenset({3}), frozenset({1})],
>> [frozenset({2, 3}), frozenset({3, 5}), frozenset({2, 5}), frozenset({1, 3})],
>> [frozenset({2, 3, 5})]]
> a2
>> {frozenset({1}): 0.5, frozenset({3}): 0.75,
>> frozenset({4}): 0.25, frozenset({2}): 0.75,
>> frozenset({5}): 0.75, frozenset({1, 3}): 0.5,
>> frozenset({2, 5}): 0.75, frozenset({3, 5}): 0.5,
>> frozenset({2, 3}): 0.5, frozenset({1, 5}): 0.25,
>> frozenset({1, 2}): 0.25, frozenset({2, 3, 5}): 0.5}
从频繁项集中挖掘关联规则
既然通过重重计算,既然拿到数据集中符合支持度要求的所有频繁项,那么就可以根据关联关系的计算,将频繁项用作挖掘关联规则。(关联关系的定义在上文有解释道)。
∵上文可知,关联关系是通过先验概率与后验概率的关系算出来的。又因这一重要贝叶斯定理的公式 ,就可以从上面我们拿到的频繁项集和频繁项对应的支持度算出对应的可信度。例如
的可信度
, 又如
的可信度
有一点需要明白的是,的可信度未必会等于
,前者是
,后者是
,而
未必等于
还是这张图,需要求的关联规则有很多,例如:
第二层:
第三层:
但是根据上文简单统计学知识可得,频繁项关联规则关系可得,又可以对低可信度的规则进行剪枝从而减少不必要的计算量。
这里再次借用《机器学习实战》第11章清单11.3的代码
def generateRules(L, supportData, minConf=0.7):
"""
通过频繁项与关于{频繁项集:支持度}的字段,算出可信度(后验概率)满足minConf的关联项
:param L: 频繁项集
:param supportData: {频繁项集:支持度}
:param minConf:
:return: 返回[(事件A,事件B,出现事件A的情况下出现事件B的概率(可信度))]
"""
# 保存所有的强关联规则
bigRuleList = []
for i in range(1, len(L)): # 从最小2个元素的频繁项组合开始拿,例:[[{1}, {2}, {3}], [{1, 2}, {2, 3}]从[{1, 2}, {2, 3}]开始
for freqSet in L[i]: # 遍历频繁项组合,例如上面的[{1, 2}, {2, 3}] 遍历此数组, freqSet拿到的就是{1, 2}
# 将频繁项初始化为单个frozenset对象即[frozenset({1}), frozenset({2})]
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):
# freqSet:{1, 2} H:[{1}, [2}] supportData:{{1}: 0.5, {2}: 0.5, {1, 2}:0.5, ...}
# brl: 保存最终强关联规则的数组
# 初始化满足最小可信度的规则
prunedH = []
# 通过遍历H里面的集合conseq, 算出与freqSet的可信度
for conseq in H:
# 计算可信度
conf = supportData[freqSet] / supportData[freqSet - conseq] # P(A|B) = P(A,B) / P(B)
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):
# freqSet:{1, 2, 3} H:[{1}, [2}, {3}] supportData:{{1}: 0.5, {2}: 0.5, {1, 2}:0.5, ...}
# brl: 保存最终强关联规则的数组
m = len(H[0])
if len(freqSet) > (m + 1):
# 传入H, 生成m+1个元素的候选项, aprioriGen有一定的“剪枝”能力
# 即若H:[{1}, [2}, {3}]->[{1, 2}, {1, 3}, {2, 3}]...
Hmp1 = aprioriGen(H, m + 1)
# 即freqSet: {1, 2, 3}, Hmp1=[{1, 2}, {1, 3}, {2, 3}]
# 算出Hmp1所有候选集与freqSet的可信度
Hmp1 = calcConf(freqSet, Hmp1, supportData, brl, minConf)
# 如果Hmp1含有的候选集不少于1个
# 则继续递归, 递归后新的Hmp1将是[{1, 2, 3}]
if len(Hmp1) > 1:
rulesFromConseq(freqSet, Hmp1, supportData, brl, minConf)
generateRules解析:
遍历拿到的L(频繁项集), 计算频繁项集中的频繁集的所有后验概率。例如L=[[{1}, {2}, {3}], [{1, 2}, {2, 3}, …]。从L的[{1, 2}, {2, 3}]后面的数据开始算,即计算
参数L: 从apriori拿到的频繁集项。
- 格式为[[frozenset({1}), frozenset({2})], [frozenset({1, 2})], […]]
参数supportData: 从apriori拿到的支持度数据。
- 格式为{frozenset({1}): 0.5, frozenset({2}): 0.5, frozenset({1, 2}): 0.75, …: …}
calcConf解析:
用于专门计算freqSet与候选项集合H中所有候选项的后验概率。
参数freqSet: 频繁项。
- 格式为{1, 2}, {1, 2, 3}
参数H: 频繁项的候选项。
- 格式为[{1}, [2}], [{1, 2}, {1, 3}, {2, 3}]
参数supportData: 从apriori拿到的支持度数据。
- 格式为{frozenset({1}): 0.5, frozenset({2}): 0.5, frozenset({1, 2}): 0.5, …: …}
参数brl: 用于存放最终的关联规则。
- 格式为[({1}, {2}, 1.0), ({2}, {1}, 1.0)]
rulesFromConseq解析:
用于专门生成新候选项并传入到calcConf计算可信的的函数。
例如传入freqSet:{1, 2, 3} ,H:[{1}, [2}, {3}],supportData:{{1}: 0.5, {2}: 0.5, {1, 2}:0.5}
它会先用H与freqSet算一次可信度,再生成新的H=[{1, 2}, {1, 3}, {2, 3}]与freqSet算可信度,再生成新的H=[{1, 2, 3}],但由于代码中有len(freqSet) > (len(H[0]) + 1)的限制,所以算法就结束,即所有的后验概率都已算完。
参数freqSet: 频繁项。
- 格式为{1, 2}, {1, 2, 3}
参数H: 频繁项的候选项。
- 格式为[{1}, [2}], [{1, 2}, {1, 3}, {2, 3}]
参数supportData: 从apriori拿到的支持度数据。
- 格式为{frozenset({1}): 0.5, frozenset({2}): 0.5, frozenset({1, 2}): 0.5, …: …}
参数brl: 用于存放最终的关联规则。
- 格式为[({1}, {2}, 1.0), ({2}, {1}, 1.0)]
测试结果
data = [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]
a1, a2 = apriori(data, minSupport=0.5)
result = generateRules(a1, a2, minConf=0.5)
print(result)
frozenset({3}) --> frozenset({2}) conf: 0.6666666666666666
frozenset({2}) --> frozenset({3}) conf: 0.6666666666666666
frozenset({5}) --> frozenset({3}) conf: 0.6666666666666666
frozenset({3}) --> frozenset({5}) conf: 0.6666666666666666
frozenset({5}) --> frozenset({2}) conf: 1.0
frozenset({2}) --> frozenset({5}) conf: 1.0
frozenset({3}) --> frozenset({1}) conf: 0.6666666666666666
frozenset({1}) --> frozenset({3}) conf: 1.0
frozenset({5}) --> frozenset({2, 3}) conf: 0.6666666666666666
frozenset({3}) --> frozenset({2, 5}) conf: 0.6666666666666666
frozenset({2}) --> frozenset({3, 5}) conf: 0.6666666666666666
[(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就可以通过以上的代码对数据集进行关联分析。若有些部分解释有误,请提醒提醒作者!