挖掘频繁模式、关联和Apriori算法
1. 引入
1.1 基本概念
频繁模式:频繁出现在数据集中的模式
频繁模式挖掘:获取到给定数据集中反复出现的联系
注:模式其实可以理解为,你在淘宝购物,你的购物车里面会购置的不同商品的集合种类,因为不止是只有你,还有其他用户也在买,因此大家购物车中购置的商品也不尽相同,因此可以把不同商品的组合集合看作为一种模式,而某种集合出现的次数较多,则可能视为频繁模式
非空项集 :T={I1,I2,I3,I4,I5.....}T = \{I_1,I_2,I_3,I_4,I_5.....\}T={I1,I2,I3,I4,I5.....}
k
-项集: 项集中项的个数为k
。 如:2-项集 {1,2}\{1,2\}{1,2}
关联规则:可以表示模式。支持度(support
)和置信度(confidence
)则是规则兴趣度的两种度量,可以反应出所发现规则的有用性和确定性。
支持度 support:
support(A=>B) = P(A U B)
置信度 confidence:confidence(A=>B) = P(B|A) = support(AUB) / support(A)
注:这儿的的AUB与概率统计中不一样,这儿的AUB表示 A和B的交集
A=>B 代表 B|A
引入一个重要概念: 先验性质
先验性质:频繁项集所有的非空子集都是频繁的
换句话说(逆否):一个项集不频繁则其所有超集都不是频繁项集
1.2 关联挖掘的过程
- 找出所有的频繁项集(项集必须满足最小支持度要求
min_support
) - 由频繁项集产生强关联规则(项集必须满足最小支持度要求
min_support
和最小置信度min_confidence
要求)
2. Apriori介绍
Apriori算法是发现频繁项集的基本算法
2.1 Apriori算法特点:
- 利用先验性质
- 逐层迭代的方式
- 由Lk−1L_{k-1}Lk−1到LkL_kLk。(
k-1
项集到k
项集) - 反单调性(先验性质的逆否说法)
- 连接步骤
连接:使用 k−1k-1k−1 项集 找出 kkk 项候选集(集合的并操作)
::: 为找出频繁k项集集合,通过将所有的频繁k-1项集集合与自身连接
- 设 l1l_1l1 和 l2l_2l2 是频繁 k−1k-1k−1 项集中的成员。记 li[j]l_i[ j ]li[j] 表示 lil_ili 中的第 jjj 项。
- 假设
Apriori
算法对 事务 或 项集中的项 按字典次序排序。- 即对于 k−1k-1k−1 项集 li,li[1]<li[2]<……….<li[k−1]l_i,l_i[1]<l_i[2]<……….<l_i[k-1]li,li[1]<li[2]<……….<li[k−1]。
- 将Lk−1L_{k-1}Lk−1与自身连接。
- 如果( l1[1]l_1[1]l1[1] = l2[1]l_2[1]l2[1] )&&( l1[2]=l2[2]l_1[2] = l_2[2]l1[2]=l2[2] ) &&………&& ( l1[k−2]=l2[k−2]l_1[k-2] = l_2[k-2]l1[k−2]=l2[k−2] )&&( l1[k−1]<l2[k−1]l_1[k-1] < l_2[k-1]l1[k−1]<l2[k−1] ),那认为l1l_1l1 和 l2l_2l2 是可连接;(l1[k−1]<l2[k−1])( l_1[k-1] < l_2[k-1] )(l1[k−1]<l2[k−1]) 是为了简单确保不重复。
- 连接l1l_1l1 和 l2l_2l2 产生的结果是 {l1[1],l1[2],……,l1[k−1],l2[k−1]}\{l_1[1],l_1[2],……,l_1[k-1],l_2[k-1]\}{l1[1],l1[2],……,l1[k−1],l2[k−1]}。
产生所有候选 kkk 项集。候选集合记作CkC_kCk。
- 剪枝步骤
剪枝:利用先验性质的逆否
CKCKCK是LKLKLK的超集,也就是说,CK的成员可能是也可能不是频繁的。
有两种方式:
- 通过扫描所有的事务,确定CK中每个候选的计数,判断是否小于最小支持度计数,如果不是,则认为该候选是频繁的。
- 为了压缩Ck,可以利用Apriori性质:任一频繁项集的所有非空子集也必须是频繁的,反之,如果某个候选的非空子集不是频繁的,那么该候选肯定不是频繁的,从而可以将其从CK中删除。
2.2 Apriori伪代码:
获取候选1-项集
剪枝1-项候选集得到 1 项集和对应support
存在1项集(k+1项集)满足最小支持度
对 1 项集(k项集)进行连接操作-->获取连接后的 2 项集(k+1项集)
对连接后的 2 项集(k+1项集)进行剪枝操作-->获取候选 k+1 项集(k+1项集):(先验性质)
计算出 2 项集(k+1项集)的支持度(符合最小支持度min_support要求)
2.3 Apriori实现代码
# -*- coding: utf-8 -*-
"""
Created on Fri Jun 7 09:54:03 2019
@author: ALVIN
"""
#创建一个数据集合
def loadDataSet():
return [[1,3,4],[2,3,5],[1,2,3,5],[2,5]]
# 创建一项集
# 所有元素转换为不可变的字典,放到列表中
def createC1(dataSet):
c1 = []
for trans in dataSet:
for item in trans:
if not [item] in c1:
c1.append([item])
c1.sort()
# [1,2,3,4,5]
return list(map(frozenset,c1)) #[{1},{2},{3},{4},{5}]
# 获取满足最小支持度的候选集与其支持度
# 过滤掉不符合支持度的集合
# 返回 频繁项集列表retList 所有元素的支持度字典
# D = [{1, 3, 4}, {2, 3, 5}, {1, 2, 3, 5}, {2, 5}]
# Ck 是k项集 example:1-项集 [{1},{2},{3},{4},{5}]
# minSupport最小支持度
def getSetAndSupport(D, Ck, minSupport):
subSetSupportNum = {}
for tid in D:
for oneOfCk in Ck: #取出k-项集 中一项
if oneOfCk.issubset(tid): # 判断can是否是tid的《子集》 (这里使用子集的方式来判断两者的关系)
if oneOfCk not in subSetSupportNum: # 统计该值在整个记录中满足子集的次数(以字典的形式记录,frozenset为键)
subSetSupportNum[oneOfCk] = 1
else:
subSetSupportNum[oneOfCk] += 1
#之前是对每一个 集合进行计数 用于计算支持度
numItems = float(len(D)) # 获取事务数目
retList = [] # 重新记录满足条件的数据值(即支持度大于阈值的数据)
supportData = {} # 每个数据值的支持度
for subSet in subSetSupportNum:
support = subSetSupportNum[subSet] / numItems
if support >= minSupport: # 只是将满足最小支持度的项集插入
retList.insert(0, subSet)
supportData[subSet] = support
return retList, supportData # 排除不符合支持度元素后的元素 每个元素支持度
# 连接与剪枝
# 生成所有可以组合的集合
# 频繁项集列表Lk 项集元素个数k [frozenset({2, 3}), frozenset({3, 5})] -> [frozenset({2, 3, 5})]
# Lk 是k项集
# linkCK和cutCK都其实是K+1项候选 只是为了方便写成linkCK和cutCK
def aprioriGen(Lk, k,supportData,minSupport = 0.5):
Ck_1 = []
linkCK = {}
cutCK = []
lenLk = len(Lk)
#连接
for i in range(lenLk): # 两层循环比较Lk中的每个元素与其它元素
for j in range(i+1, lenLk):
#****连接操作****
Ck_1.append(Lk[i] | Lk[j]) # 求并集
linkCK = set(Ck_1) # 去除Ck_1中相同的集合
linkCK = list(linkCK)
#剪枝
for oneOfCk in linkCK: # 两层循环判断 连接后的集合列表
flag = 1
for sup in supportData:
if supportData[sup] < minSupport:
if sup.issubset(oneOfCk):
flag = 0
if flag == 1:
cutCK.append(oneOfCk)
#print(cutCK)
return cutCK # 返回频繁项集列表Ck
# 封装所有步骤的函数
# 返回 所有满足大于阈值的组合 集合支持度列表
def apriori(dataSet, minSupport = 0.5):
Data = list(map(set, dataSet)) # 转换列表记录为字典 [{1, 3, 4}, {2, 3, 5}, {1, 2, 3, 5}, {2, 5}]
C1 = createC1(dataSet) # 将每个元素转会为frozenset字典
# [frozenset({1}), frozenset({2}), frozenset({3}), frozenset({4}), frozenset({5})]
supportAll = [] # supportAll 用于保存所有的支持度 用supportData 保存k-项集的支持度
# supportData在aprioriGen的剪枝操作中时候会用到
L1, supportData = getSetAndSupport(Data, C1, minSupport) # 过滤数据 获取1-项集 和其对应的支持度
supportAll.append(supportData) #将1-项集加入到支持度的列表中
L = [L1] # L 会存储所有的的项集
k = 0
while (len(L[k]) > 0): # 若仍有满足支持度的集合则继续做关联分析
print(k,end='\n')
Ck = aprioriGen(L[k], k, supportData) # Ck候选频繁项集
Lk, supportData = getSetAndSupport(Data, Ck, minSupport) # Lk频繁项集
#supportData.update(supK) # 更新字典(把新出现的集合:支持度加入到supportData中)
supportAll.append(supportData)
L.append(Lk) # 将Lk 放到 L 列表中
k += 1 # 每次新组合的元素都只增加了一个,所以k也+1(k表示元素个数)
return L, supportAll
#获取到数据集合
dataSet = loadDataSet()
#Apriori算法 所有满足大于阈值的组合 集合支持度列表
L,suppAll = apriori(dataSet)
for lk in L:
print(lk)
print()
#打印出所有支持度
for supp in suppAll:
for sup in supp:
print(sup,supp[sup])
3 算法的改进与提升
3.1 Apriori效率提高
- 基于散列的技术
将每个项集通过相应的hash函数映射到hash表中的不同的桶中,这样可以通过将桶中的项集技术跟最小支持计数相比较先淘汰一部分项集。
- 事务压缩
不包含任何k-项集的事务不可能包含任何(k+1)-项集,这种事务在下一步的计算中可以加上标记或删除
- 划分
挖掘频繁项集只需进行两次数据扫描
任何频繁项集必须作为局部频繁项集至少出现在一个部分中(不可缺失)。
第一次扫描:将事务划分为多个部分并找到局部频繁项集
第二次扫描:评估每个候选项集的实际支持度,以确定全局频繁项集。(局部推全局)
- 抽样
选择原始数据的一个样本,在这个样本上用Apriori算法挖掘频繁模式
通过牺牲精确度来减少算法开销,为了提高效率,样本大小应该以可以放在内存中为宜,可以适当降低最小支持度来减少遗漏的频繁模式
可以通过一次全局扫描来验证从样本中发现的模式
可以通过第二此全局扫描来找到遗漏的模式
- 动态项集计数
在扫描的不同点添加候选项集,这样,如果一个候选项集已经满足最少支持度,则在可以直接将它添加到频繁项集,而不必在这次扫描的以后对比中继续计算。
3.2 Apriori缺点
- Apriori算法将会产生大量的候选集
- 重复的扫描数据库,系统开销极大
改进: 频繁模式增长(Frequent-Pattern Growth,FP-Growth)
- 分而治之
- 频繁模式树
- 模式片段
- 。。。
具体见下一篇FP-Growth