最后一节数挖实验课,这次老师要我们自己动手写模式挖掘,没错,就是数据挖掘中非常经典的模式挖掘问题,“啤酒和尿布” 的故事相信大家都听过,而模式挖掘就可以找出看似不相关的事物之间隐藏的关联,非常强大,但写起来也着实让人头疼。
实验题目是这样的的,要求使用 Aprior 算法在 Groceries 数据集上进行购物篮分析来找出商品之间有趣的相关关系,其中,Groceries 数据集是某个杂货店一个月真实的交易记录,共有 9835 条消费记录和 169 个商品,对于我这样的初学者来说,数据量有点吓人,不过还是尽量去做了,接下来讲一讲我的思路。
1 数据集的下载与处理
老规矩,第一步,先将数据集弄到手,因为我们老师给出了 Groceries 数据集,而我也本着好东西大家一起分享的态度,就将它放在这里:groceries.csv,不用积分,大家可以直接下载(有时候我上传时设置的是 0 积分,但是过一段时间它又突然需要花积分下载了,不知道为什么,请大家遇到这种情况就在评论区告知一声,我改回来,或者留下 QQ,我私发给你)。
Groceries 数据集是一个典型的事务数据集,我们需要做的就是对该事务数据集进行模式挖掘,找出所有的满足最小支持度和最小置信度的类别关联规则,说人话就是从大量的商品当中根据顾客们的购物篮记录发现一些相关性强的商品,来了解顾客们的购物习惯,并相应地选取更好的商品销售策略。
我们打开文件 groceries.csv 来观察一下 Groceries 数据集,可以看到该数据集有 9386 行与 33 列,第一行是列名,其下的每一行代表一条购物篮记录,第一列中的数据表示每一个购物篮当中商品的数量,其后的若干列则对应不同的商品:
我们可以通过一句代码对数据进行读取:
dataset = numpy.loadtxt("groceries.csv", delimiter=",", usecols=range(1, 33), skiprows=1, dtype=str)
在这里对这句代码稍微解释一下,关于这个函数的详细信息可以链入这个函数的源码去了解一下:
delimiter
:该参数是用来告知函数,要读取的的文件是采用什么样的分隔符进行分隔的,这里传入的参数值是","
,因为 csv 格式的文件实际上是采用英文逗号作为分隔符的,我们可以使用 Notepad++ 打开它一下,或者重命名为 txt 文件然后打开:
从上下两张图片我们可以看到,csv 格式的文件以英文逗号作为分隔符,因此这里将","
作为传入的参数值,如果传入其他的分隔符会出现报错。usecols
:该参数是用来告知函数,要提取文件当中的哪些列,这里传入的参数值是range(1, 33)
,表示要提取 1 到 32 列即去掉第 0 列;skiprows
:该参数是用来告知函数,要跳过文件当中的前多少行,这里传入的参数值是1
,表示跳过行数为 1,即只提取第一行下面的数据,因为第一行对应的是列名。
我们读取数据集之后打印一下看看,发现它是一个 numpy.ndarray
类型的二维数组:
2 发现频繁 1 项集
注:接下来的内容中的一些概念可能会直接拿出来而不会具体进行解释,在此给可能不清楚这些概念的同学道个歉,因为期末了,实在没有时间和精力着重写这些,这段时间忙完后,我会给大家补上的。
一般而言,关联规则的挖掘即模式挖掘是一个两步的过程:
-
获得所有的频繁项集,这些项集的每一个频繁出现的次数需要大于等于预定义的最小支持计数阈值 min_sup;
-
由频繁项集产生强关联规则,这些规则必须满足最小支持度和最小置信度。
因此,我们需要根据数据集找出所有的频繁项集,那怎么找呢?Apriori 算法使用一种称为逐层搜索的迭代方法来解决这个问题,其中,频繁(k - 1)项集用于探索频繁 k 项集。这和高中的数学归纳法感觉有点像,做法也差不多,所以我们要完成这些事情的第一步就是要得到 k = 1 的情况,然后根据 k = 1 的情况进行递推得到 k 为其他值得情况。在我们这里,我们就要先找出频繁 1 项集,然后才可以逐层地得到其他频繁项集,我们可以通过下面的代码发现频繁 1 项集:
def findFrequentOneItemsets(dataset, min_sup):
'''
msg: 发现所有满足最小支持度计数阈值的频繁 1 项集
param {
dataset:numpy.ndarray Groceries 事务数据集
min_sup:int 最小支持度计数阈值
}
return{
frequent_one_itemsets:dict 字典形式的频繁 1 项集的集合,每个键值对的键为包含商品的元组,值为商品对应的支持度计数
}
'''
goods_list = [i for i in dataset.flatten() if i != ''] # 将 numpy.ndarray 类型的数据集平铺并去除所有空字符串 '',得到所有商品的列表
goods_set = set(goods_list)
frequent_one_itemsets = {}
for key in goods_set:
num = goods_list.count(key) # 每件商品的支持度计数
if num < min_sup:
continue # 当前商品的支持度计数小于最小支持度计数阈值,放弃该商品并搜索下一件商品
else:
frequent_one_itemsets[(key, )] = num # 当前商品的支持度计数大于或等于最小支持度计数阈值,将该商品加入频繁 1 项集的集合
return frequent_one_itemsets
3 利用先验性质压缩搜索空间
在有了频繁 1 项集的集合后,我们就可以根据其逐层地得到其他频繁项集的集合,然而得到每一层的频繁 k 项集都会耗费大量资源去搜索。为了提高频繁项集逐层产生的效率,我们可以使用一种先验性质(Apriori property)来压缩搜索空间。
那么这种先验性质是什么呢?我们来探讨一下,根据频繁项集的定义,如果项集 X 不满足最小支持度阈值 min_sup,则 X 不是频繁的,即 sup(X) < min_sup。而如果我们再把一个项 A 添加到项集 X 中,则结果项集 Y 不可能比 X 更频繁地出现,因此,Y 必然不是频繁项集,即 sup(Y) < min_sup。
因此,我们得到一个结论:频繁项集的所有非空子集一定是频繁的,而非频繁项集的所有超集一定是非频繁的。这就是我们的先验性质,它其实属于一类特殊的性质,称为反单调性(antimonotone),意思是如果一个集合不能通过测试,则它的所有超集也都不能通过测试。
上图是利用先验性质压缩搜索空间这一步的具体实现,我们来对它进行分析,该过程有两个重要的步骤:连接步(join step)和剪枝步(prune step),分别对它们进行解释:
-
连接步:为了找出频繁 k 项集,我们需要通过将频繁(k - 1)项集与自身做连接(⋈)产生候选 k 项集的集合。
-
剪枝步:在连接步之后,我们会扫描数据库从候选 k 项集的集合当中一个一个地筛选出满足最小支持度计数阈值的频繁项集,但是如果候选 k 项集很大,这就会导致计算量剧增,因此,为了压缩搜索空间,我们就可以利用刚刚说的先验性质对候选 k 项集的集合做预剪枝。因为任何非频繁的(k - 1)项集都不可能是频繁 k 项集的子集,所以,如果一个候选 k 项集的(k - 1)项子集当中存在非频繁的项集,或者说该候选 k 项集的(k - 1)项子集减去频繁(k - 1)项集后长度不为 0,那么该候选 k 项集也不可能是频繁的,从而可以从候选集合当中删除。
下面是实现上述过程的代码:
def aprioriGen(frequent_k_minus_one_itemsets):
'''
msg: 根据频繁(k - 1)项集的集合得到候选 k 项集的集合
param {
frequent_k_minus_one_itemsets:dict 频繁(k - 1)项集的集合
}
return{
candidate_k_set:set 候选 k 项集的集合
}
'''
k_minus_one_list_sorted = sorted(frequent_k_minus_one_itemsets.keys())
candidate_k_set = set()
for i in k_minus_one_list_sorted:
temp_set = set([
tuple(set(i + j)) for j in k_minus_one_list_sorted if i < j
]) # 连接步:将频繁(k - 1)项集的集合与其自己做连接,得到候选的 k 项集的集合
candidate_k_set.update([
tuple(sorted(j)) for j in temp_set if len(kMinusOneSubset(j) - set(k_minus_one_list_sorted)) == 0
]) # 剪枝步:根据先验性质,删除非频繁的候选 k 项集
return candidate_k_set
上述代码当中 kMinusOneSubset()
是用来求 k 项集的所有 k - 1 项子集的,我们需要对其进行实现,如下:
def kMinusOneSubset(superset):
'''
msg: 得到 k 项集的所有 k - 1 项子集
param {
superset:tuple k 项集
}
return{
k_minus_one_subset:set k 项集的所有 k - 1 项子集
}
'''
sub_sorted = sorted(superset)
k_minus_one_subset = set([tuple(sub_sorted[:i] + sub_sorted[i + 1:]) for i in range(len(sub_sorted))])
return k_minus_one_subset
4 获得所有的频繁项集
好了,所有准备工作完成以后,我们就要进入正题了,我们要使用 Apriori 算法来发现所有的频繁项集,具体实现步骤如下:
上图是伪代码形式的过程,这里对其解释一下:
-
(1):找出频繁 1 项集的集合 L1,对应步骤 2 发现频繁 1 项集;
-
(2):循环产生频繁项集,直到不能再找到频繁项集;
-
(3):从频繁 k - 1 项集当中产生候选,然后使用先验性质删除那些具有非频繁子集的候选,对应步骤 3 利用先验性质压缩搜索空间;
-
(5):得到事务 t 的候选子集,这些候选子集均是候选 k 项集的集合的元素;
-
(7):获得每个候选 k 项集的支持度计数;
-
(9):得到满足最小支持度计数阈值的候选 k 项集。
我们大概了解了该如何进行这一过程,下面使用具体代码对其进行实现:
def apriori(dataset, frequent_one_itemsets, min_sup):
'''
msg: 获得所有的频繁项集
param {
dataset:numpy.ndarray Groceries 事务数据集
frequent_one_itemsets:dict 字典形式的频繁 1 项集的集合,每个键值对的键为包含商品的元组,值为商品对应的支持度计数
min_sup:int 最小支持度计数阈值
}
return{
frequent_itemsets:dict 字典形式的频繁项集的集合
}
'''
frequent_itemsets_list = [frequent_one_itemsets] # 创建一个以每一层的频繁项集的集合为元素的列表
k = 1
while(len(frequent_itemsets_list[k - 1]) != 0): # 判断上一层的(k - 1)项集的集合是否为空,为空则说明不能再找到频繁项集,退出循环,不为空则继续循环
candidate_k_set = aprioriGen(frequent_itemsets_list[k - 1]) # 根据频繁(k - 1)项集的集合得到候选 k 项集的集合
if len(candidate_k_set) == 0: # 判断候选 k 项集的集合是否为空,为空则说明不能再找到频繁项集,退出循环,不为空则继续
frequent_itemsets_list.append({}) # 对应 del frequent_itemsets_list[-1] 语句,防止误删
break
candidate_itemsets_list = [] # 创建一个以各个事务的子集为元素的列表,它们均是候选的
for t in dataset: # t 为事务数据集中的事务
ct = candidateItemsets(candidate_k_set, set(t)) # 得到事务 t 的候选子集,这些候选子集均是候选 k 项集的集合的元素
candidate_itemsets_list.extend(ct)
candidate_set = set(candidate_itemsets_list)
candidate_dict = {} # 创建字典形式的频繁 k 项集的集合
for key in candidate_set:
num = candidate_itemsets_list.count(key) # 每个候选 k 项集的支持度计数
if num < min_sup:
continue # 当前候选 k 项集不满足最小支持度计数阈值,放弃该候选 k 项集并搜索下一候选 k 项集
else:
candidate_dict[key] = num # 当前候选 k 项集满足最小支持度计数阈值,将该候选 k 项集加入频繁 k 项集的集合
frequent_itemsets_list.append(candidate_dict)
k += 1
del frequent_itemsets_list[-1] # 上一层的的项集的集合为空,退出循环并且删除上一层的的项集
frequent_itemsets = {} # 创建字典形式的频繁项集的集合
for i in frequent_itemsets_list:
frequent_itemsets.update(i)
return frequent_itemsets
在上述代码当中,我们可以看到基本完成了所需要的所有步骤,但还有一个函数 candidateItemsets()
没有实现,下面对其进行实现:
def candidateItemsets(candidate_k_set, t):
'''
msg: 得到事务 t 的候选子集,这些候选子集均是候选 k 项集的集合的元素
param {
candidate_k_set:set 候选 k 项集的集合
t:numpy.ndarray Groceries 事务数据集中的事务
}
return{
candidate:list 以事务 t 的候选子集为元素的列表,这些候选子集均是候选 k 项集的集合的元素
}
'''
candidate = []
for i in candidate_k_set:
if t.issuperset(i): # 判断事务 t 是否是候选 k 项集 i 的超集,即判断候选 k 项集 i 是否是事务 t 的子集
candidate.append(tuple(sorted(i)))
return candidate
在完成上述工作之后,我们设置最小支持度计数阈值 min_sup 为 400,然后打印一下结果,可以看到我们成功地完成了获得所有频繁 k 项集的工作,我们以字典形式组织频繁 k 项集的集合,其中每个键值对中的键是元组形式的频繁 k 项集,值是其对应的支持度计数:
5 由频繁项集产生强关联规则
在从 Groceries 事务数据集当中得到所有的频繁 k 项集后,我们的工作已经完成了一大半,接下来就可以直接由这些频繁项集产生强关联规则(强关联规则就是满足最小支持度和最小置信度的规则)。
由于规则由频繁项集产生,而频繁项集是已经满足最小支持度的,因此每个规则都自动满足最小支持度,所以我们要计算规则对应的置信度,以规则 A ⇒ B 为例,公式如下:
c
o
n
f
i
d
e
n
c
e
(
A
⇒
B
)
=
P
(
B
∣
A
)
=
s
u
p
p
o
r
t
_
c
o
u
n
t
(
A
∪
B
)
s
u
p
p
o
r
t
_
c
o
u
n
t
(
A
)
confidence(A \Rightarrow B) = P(B | A) = \frac {support\_count(A \cup B)} {support\_count(A)}
confidence(A⇒B)=P(B∣A)=support_count(A)support_count(A∪B)
可以看到,上面公式中的条件概率用项集对应的支持度计数计算,其中,support_count(A∪B) 是数据集当中包含项集 A∪B 的事务数,而 support_count(A) 是包含项集 A 的事务数。
根据上述公式,我们可以通过如下步骤产生关联规则:
-
对于每个频繁项集 Z,产生 Z 的所有非空真子集;
-
对于 Z 的每个非空子集 S,如果
c o n f i d e n c e ( S ⇒ Z − S ) = s u p p o r t _ c o u n t ( Z ) s u p p o r t _ c o u n t ( S ) ⩾ m i n _ c o n f confidence(S \Rightarrow Z - S) = \frac {support\_count(Z)} {support\_count(S)} \geqslant min\_conf confidence(S⇒Z−S)=support_count(S)support_count(Z)⩾min_conf则输出规则 “S⇒(Z−S)”,其中,min_conf 是最小置信度阈值。
以下是上述过程的具体实现代码,我们来一步步进行编写:
def associationRules(frequent_itemsets, min_conf):
'''
msg: 由频繁项集产生强关联规则
param {
frequent_itemsets:dict 字典形式的频繁项集的集合
min_conf:double 最小置信度阈值
}
return{
rules_list:list 以规则为元素的列表,其中规则以三元组 (频繁项集 Z,子集 S,子集 Z-S) 形式组织,对应规则 S⇒Z−S
}
'''
rules_list = [] # 创建一个以规则为元素的列表,其中规则以三元组 (频繁项集 Z,子集 S,子集 Z-S) 形式组织,对应规则 S⇒Z−S
for frequent_itemset in frequent_itemsets: # 遍历所有的频繁项集
if len(frequent_itemset) == 1: # 判断是否是频繁 1 项集,因为使用频繁 1 项集产生的规则是无用的
continue
else:
proper_subsets = allProperSubset(frequent_itemset) # 得到当前频繁项集的所有非空真子集
frequent_itemset_support = frequent_itemsets[frequent_itemset] # 得到当前频繁项集对应的支持度计数
for proper_subset in proper_subsets: # 遍历当前频繁项集的所有非空真子集并生成对应规则
if frequent_itemset_support / frequent_itemsets[proper_subset] >= min_conf: # 判断当前规则是否满足最小置信度阈值
rules_list.append((
frequent_itemset, proper_subset, tuple(sorted(set(frequent_itemset) - set(proper_subset)))
)) # 当前规则满足最小置信度阈值,将其以三元组形式加入规则列表
return rules_list
然后我们来实现上述代码当中的未实现的函数 allProperSubset()
,该函数是用来获得集合的所有非空真子集的,对该函数的编写我参照了一个大佬的博文 https://blog.youkuaiyun.com/xjtuse123/article/details/99202846,大家也可以去看一看:
def allProperSubset(superset):
'''
msg: 获得集合的所有非空真子集
param {
superset:tuple 元组形式的集合
}
return{
proper_subsets:list 列表形式的集合,列表当中的每个元素均为元组形式的给定集合的非空真子集
}
'''
n = len(superset)
proper_subsets = []
for i in range(1, 2 ** n - 1): # 根据子集个数,循环遍历所有非空真子集
proper_subset = []
for j in range(n):
if (i >> j) % 2: # 判断二进制下标为 j 的位置数是否为 1
proper_subset.append(superset[j])
proper_subsets.append(tuple(proper_subset))
return proper_subsets
我们设置最小支持度计数阈值 min_sup 为 400,最小置信度阈值为 0.3,然后来打印一下代码运行结果,得到规则列表,我们可以看到下面的结果符合我们的预期,说明运行成功:
6 使用提升度评判关联规则
其实在上一步,整个模式挖掘的过程已经基本完成了,我们已经得到了想要的关联规则,但是我们要怎么才能知道自己的模型学出来的关联规则是否是有趣的呢?
一种简单的相关性度量可以评估我们的关联规则,它就是提升度(lift),我们来对它进行解释。如果项集 A 的出现独立于项集 B 的出现,则 P(A∪B) = P(A) P(B),而当项集 A 和 B 是依赖的(dependent)和相关的(correlated),等式将不会成立。
由上述的概念,我们可以引出项集 A 和 B 之间的提升度计算公式:
l
i
f
t
(
A
,
B
)
=
P
(
A
∪
B
)
P
(
A
)
P
(
B
)
=
s
u
p
p
o
r
t
_
c
o
u
n
t
(
A
∪
B
)
s
u
p
p
o
r
t
_
c
o
u
n
t
(
A
)
s
u
p
p
o
r
t
_
c
o
u
n
t
(
B
)
×
t
o
t
a
l
_
n
u
m
lift(A, B) = \frac {P(A \cup B)} {P(A) P(B)} = \frac {support\_count(A \cup B)} {support\_count(A) support\_count(B)} \times total\_num
lift(A,B)=P(A)P(B)P(A∪B)=support_count(A)support_count(B)support_count(A∪B)×total_num
其中,support_count(A∪B) 是数据集当中包含项集 A∪B 的事务数,support_count(A) 是包含项集 A 的事务数,support_count(B) 是包含项集 B 的事务数,total_num 是 Groceries 事务数据集中事务的总数。
提升度如何计算我们已经知道了,那我们如何根据提升度进行分析呢?这里给出相应的情况:
-
如果 lift(A, B) 的值等于 1,则 A 和 B 是独立的,它们之间没有相关性;
-
如果 lift(A, B) 的值大于 1,则 A 和 B 是正相关的,意味着每一个的出现都蕴含着另一个的出现;
-
如果 lift(A, B) 的值小于 1,则 A 和 B 是负相关的,意味着一个的出现可能导致另一个的不出现。
而因为提升度的计算公式可以进行下列转换:
l i f t ( A , B ) = P ( A ∪ B ) P ( A ) P ( B ) = P ( B ∣ A ) P ( B ) = c o n f i d e n c e ( A ⇒ B ) s u p p o r t ( B ) lift(A, B) = \frac {P(A \cup B)} {P(A) P(B)} = \frac {P(B | A)} {P(B)} = \frac {confidence(A \Rightarrow B)} {support(B)} lift(A,B)=P(A)P(B)P(A∪B)=P(B)P(B∣A)=support(B)confidence(A⇒B)
所以 lift(A, B) 也称为关联规则 A⇒B 的提升度,它衡量了一个的出现提升了另一个的出现的程度大小,下面我们使用代码对其进行实现:
def lift(rule, total_num):
'''
msg: 使用提升度评判关联规则
param {
rule:tuple 以三元组 (频繁项集 Z,子集 S,子集 Z-S) 形式组织的规则
total_num:int 事务数据集中所有事务的数量
}
return{
rule_lift:double 关联规则的提升度
}
'''
rule_lift = frequent_itemsets[rule[0]] * total_num / (frequent_itemsets[rule[1]] * frequent_itemsets[rule[2]])
return rule_lift
在知道如何使用提升度评估关联规则的有趣性后,所有的步骤基本实现,我们可以再编写一个输出函数,将我们模型学到的规则以固定格式进行打印:
def printRules(frequent_itemsets, rules_list, total_num, min_sup, min_conf):
'''
msg: 以固定格式打印所有规则
param {
frequent_itemsets:dict 字典形式的频繁项集的集合
rules_list:list 以规则为元素的列表,其中规则以三元组 (频繁项集 Z,子集 S,子集 Z-S) 形式组织,对应规则 S⇒Z−S
total_num:int 事务数据集中所有事务的数量
min_sup:int 最小支持度计数阈值
min_conf:double 最小置信度阈值
}
return: None
'''
min_sup_threshold = min_sup / total_num
for rule in rules_list:
rule_lift = lift(rule, total_num)
print("{: <40}--> {: <22}: support >= {:.2%}, confidence >= {:.2%}, lift = {:.3}" \
.format(str(rule[1]), str(rule[2]), min_sup_threshold, min_conf, rule_lift))
打印结果如下,可以看到我们模型学到的规则的提升度基本都是大于 1,说明规则中的项集之间是正相关的,我们可以信赖这些规则,然后根据这些规则制定销售策略,比如说,有一条规则是 “yogurt”⇒“whole milk”,那我们可以根据这条规则而将酸奶和全脂牛奶摆放地近一些,这样顾客们在买酸奶时看到全脂牛奶的话,他们就有可能顺手带上全脂牛奶,这样就促进了物品们的销售。
7 组合模块形成完整代码
最后,将前面的所有模块进行组合,并添加 main 函数,得到完整代码:
'''
Description: 初步搭建 Apriori 频繁模式挖掘框架
Author: stepondust
Date: 2020-05-31
'''
import numpy as np
def findFrequentOneItemsets(dataset, min_sup):
'''
msg: 发现所有满足最小支持度计数阈值的频繁 1 项集
param {
dataset:numpy.ndarray Groceries 事务数据集
min_sup:int 最小支持度计数阈值
}
return{
frequent_one_itemsets:dict 字典形式的频繁 1 项集的集合,每个键值对的键为包含商品的元组,值为商品对应的支持度计数
}
'''
goods_list = [i for i in dataset.flatten() if i != ''] # 将 numpy.ndarray 类型的数据集平铺并去除所有空字符串 '',得到所有商品的列表
goods_set = set(goods_list)
frequent_one_itemsets = {}
for key in goods_set:
num = goods_list.count(key) # 每件商品的支持度计数
if num < min_sup:
continue # 当前商品的支持度计数小于最小支持度计数阈值,放弃该商品并搜索下一件商品
else:
frequent_one_itemsets[(key, )] = num # 当前商品的支持度计数大于或等于最小支持度计数阈值,将该商品加入频繁 1 项集的集合
return frequent_one_itemsets
def kMinusOneSubset(superset):
'''
msg: 得到 k 项集的所有 k - 1 项子集
param {
superset:tuple k 项集
}
return{
k_minus_one_subset:set k 项集的所有 k - 1 项子集
}
'''
sub_sorted = sorted(superset)
k_minus_one_subset = set([tuple(sub_sorted[:i] + sub_sorted[i + 1:]) for i in range(len(sub_sorted))])
return k_minus_one_subset
def aprioriGen(frequent_k_minus_one_itemsets):
'''
msg: 根据频繁(k - 1)项集的集合得到候选 k 项集的集合
param {
frequent_k_minus_one_itemsets:dict 频繁(k - 1)项集的集合
}
return{
candidate_k_set:set 候选 k 项集的集合
}
'''
k_minus_one_list_sorted = sorted(frequent_k_minus_one_itemsets.keys())
candidate_k_set = set()
for i in k_minus_one_list_sorted:
temp_set = set([
tuple(set(i + j)) for j in k_minus_one_list_sorted if i < j
]) # 连接步:将频繁(k - 1)项集的集合与其自己做连接,得到候选的 k 项集的集合
candidate_k_set.update([
tuple(sorted(j)) for j in temp_set if len(kMinusOneSubset(j) - set(k_minus_one_list_sorted)) == 0
]) # 剪枝步:根据先验性质,删除非频繁的候选 k 项集
return candidate_k_set
def candidateItemsets(candidate_k_set, t):
'''
msg: 得到事务 t 的候选子集,这些候选子集均是候选 k 项集的集合的元素
param {
candidate_k_set:set 候选 k 项集的集合
t:numpy.ndarray Groceries 事务数据集中的事务
}
return{
candidate:list 以事务 t 的候选子集为元素的列表,这些候选子集均是候选 k 项集的集合的元素
}
'''
candidate = []
for i in candidate_k_set:
if t.issuperset(i): # 判断事务 t 是否是候选 k 项集 i 的超集,即判断候选 k 项集 i 是否是事务 t 的子集
candidate.append(tuple(sorted(i)))
return candidate
def apriori(dataset, frequent_one_itemsets, min_sup):
'''
msg: 获得所有的频繁项集
param {
dataset:numpy.ndarray Groceries 事务数据集
frequent_one_itemsets:dict 字典形式的频繁 1 项集的集合,每个键值对的键为包含商品的元组,值为商品对应的支持度计数
min_sup:int 最小支持度计数阈值
}
return{
frequent_itemsets:dict 字典形式的频繁项集的集合
}
'''
frequent_itemsets_list = [frequent_one_itemsets] # 创建一个以每一层的频繁项集的集合为元素的列表
k = 1
while(len(frequent_itemsets_list[k - 1]) != 0): # 判断上一层的(k - 1)项集的集合是否为空,为空则说明不能再找到频繁项集,退出循环,不为空则继续循环
candidate_k_set = aprioriGen(frequent_itemsets_list[k - 1]) # 根据频繁(k - 1)项集的集合得到候选 k 项集的集合
if len(candidate_k_set) == 0: # 判断候选 k 项集的集合是否为空,为空则说明不能再找到频繁项集,退出循环,不为空则继续
frequent_itemsets_list.append({}) # 对应 del frequent_itemsets_list[-1] 语句,防止误删
break
candidate_itemsets_list = [] # 创建一个以各个事务的子集为元素的列表,它们均是候选的
for t in dataset: # t 为事务数据集中的事务
ct = candidateItemsets(candidate_k_set, set(t)) # 得到事务 t 的候选子集,这些候选子集均是候选 k 项集的集合的元素
candidate_itemsets_list.extend(ct)
candidate_set = set(candidate_itemsets_list)
candidate_dict = {} # 创建字典形式的频繁 k 项集的集合
for key in candidate_set:
num = candidate_itemsets_list.count(key) # 每个候选 k 项集的支持度计数
if num < min_sup:
continue # 当前候选 k 项集不满足最小支持度计数阈值,放弃该候选 k 项集并搜索下一候选 k 项集
else:
candidate_dict[key] = num # 当前候选 k 项集满足最小支持度计数阈值,将该候选 k 项集加入频繁 k 项集的集合
frequent_itemsets_list.append(candidate_dict)
k += 1
del frequent_itemsets_list[-1] # 上一层的的项集的集合为空,退出循环并且删除上一层的的项集
frequent_itemsets = {} # 创建字典形式的频繁项集的集合
for i in frequent_itemsets_list:
frequent_itemsets.update(i)
return frequent_itemsets
def allProperSubset(superset):
'''
msg: 获得集合的所有非空真子集
param {
superset:tuple 元组形式的集合
}
return{
proper_subsets:list 列表形式的集合,列表当中的每个元素均为元组形式的给定集合的非空真子集
}
'''
n = len(superset)
proper_subsets = []
for i in range(1, 2 ** n - 1): # 根据子集个数,循环遍历所有非空真子集
proper_subset = []
for j in range(n):
if (i >> j) % 2: # 判断二进制下标为 j 的位置数是否为 1
proper_subset.append(superset[j])
proper_subsets.append(tuple(proper_subset))
return proper_subsets
def associationRules(frequent_itemsets, min_conf):
'''
msg: 由频繁项集产生强关联规则
param {
frequent_itemsets:dict 字典形式的频繁项集的集合
min_conf:double 最小置信度阈值
}
return{
rules_list:list 以规则为元素的列表,其中规则以三元组 (频繁项集 Z,子集 S,子集 Z-S) 形式组织,对应规则 S⇒Z−S
}
'''
rules_list = [] # 创建一个以规则为元素的列表,其中规则以三元组 (频繁项集 Z,子集 S,子集 Z-S) 形式组织,对应规则 S⇒Z−S
for frequent_itemset in frequent_itemsets: # 遍历所有的频繁项集
if len(frequent_itemset) == 1: # 判断是否是频繁 1 项集,因为使用频繁 1 项集产生的规则是无用的
continue
else:
proper_subsets = allProperSubset(frequent_itemset) # 得到当前频繁项集的所有非空真子集
frequent_itemset_support = frequent_itemsets[frequent_itemset] # 得到当前频繁项集对应的支持度计数
for proper_subset in proper_subsets: # 遍历当前频繁项集的所有非空真子集并生成对应规则
if frequent_itemset_support / frequent_itemsets[proper_subset] >= min_conf: # 判断当前规则是否满足最小置信度阈值
rules_list.append((
frequent_itemset, proper_subset, tuple(sorted(set(frequent_itemset) - set(proper_subset)))
)) # 当前规则满足最小置信度阈值,将其以三元组形式加入规则列表
return rules_list
def lift(rule, total_num):
'''
msg: 使用提升度评判关联规则
param {
rule:tuple 以三元组 (频繁项集 Z,子集 S,子集 Z-S) 形式组织的规则
total_num:int 事务数据集中所有事务的数量
}
return{
rule_lift:double 关联规则的提升度
}
'''
rule_lift = frequent_itemsets[rule[0]] * total_num / (frequent_itemsets[rule[1]] * frequent_itemsets[rule[2]])
return rule_lift
def printRules(frequent_itemsets, rules_list, total_num, min_sup, min_conf):
'''
msg: 以固定格式打印所有规则
param {
frequent_itemsets:dict 字典形式的频繁项集的集合
rules_list:list 以规则为元素的列表,其中规则以三元组 (频繁项集 Z,子集 S,子集 Z-S) 形式组织,对应规则 S⇒Z−S
total_num:int 事务数据集中所有事务的数量
min_sup:int 最小支持度计数阈值
min_conf:double 最小置信度阈值
}
return: None
'''
min_sup_threshold = min_sup / total_num
for rule in rules_list:
rule_lift = lift(rule, total_num)
print("{: <40}--> {: <22}: support >= {:.2%}, confidence >= {:.2%}, lift = {:.3}" \
.format(str(rule[1]), str(rule[2]), min_sup_threshold, min_conf, rule_lift))
if __name__ == "__main__":
min_sup = 400
min_conf = 0.3
dataset = np.loadtxt("groceries.csv", delimiter=",", usecols=range(1, 33), skiprows=1, dtype=str)
frequent_one_itemsets = findFrequentOneItemsets(dataset, min_sup)
frequent_itemsets = apriori(dataset, frequent_one_itemsets, min_sup)
rules_list = associationRules(frequent_itemsets, min_conf)
printRules(frequent_itemsets, rules_list, len(dataset), min_sup, min_conf)
好了本文到此就快结束了,以上就是我自己动手写 Apriori 频繁模式挖掘的想法和思路,大家参照一下即可,重要的还是经过自己的思考来编写代码,文章中还有很多不足和不正确的地方,欢迎大家指正(也请大家体谅,写一篇博客真的挺累的,花的时间比我写出代码的时间还要长),我会尽快修改,之后我也会尽量完善本文,尽量写得通俗易懂。
博文创作不易,转载请注明本文地址:https://blog.youkuaiyun.com/qq_44009891/article/details/106423085