普遍联系是为唯物辩证法的一个基本观点。在日常生活中,联系是普遍存在的,一些销售冠军经常提到的关于啤酒和尿布的经典案例,便是联系的一种具体体现。经过一个多月的学习,这几天进入到了无监督学习的关联分析部分,与之前学习算法不同,这几天感觉到特别吃力(状态不佳,心浮气躁),本来想看完书后再找一些案例来充实一下这部分的内容,考虑了一下,还是先静下心来写篇博客总结一下吧,为接下来进行深入的学习打好基础。
先来了解一些什么是关联分析。
从大规模的数据中分析物品之间的隐含关系就是关联分析(Association Analysis),也可以叫做关联规则学习(Assocation Rule Learning)。要想找出物品之间的关系,首先需要找出物品的组合,然后再分析这些组合,确定物品之间的关系。但是这些操作非常的复杂,通过常规的算法很难达到技术要求,于是聪明的人类发明了Apriori算法,可以帮助我们快速的找到频繁项集合(Frequent Item Set),然后我们就可以从频繁项集中抽取出关联规则,这就是我们想要的东西。
频繁项集就是经常出现在一起的物品的集合,而关联规则则是按时这些物品之间可能存在很强的关系,下面用一个例子来说明:
这是来自某个商店的商品交易清单,第一列是交易的编号,第二列是商品。
先来学习几个单词吧.
soy milk 豆奶
lettuce 莴苣,生菜(不知道到底是莴苣还是生菜,如果是莴苣的话,外国人会炒着吃吗?)
diaper 尿布(似曾相识的赶脚)
wine 葡萄酒
chard 甜菜(不明所以)
orange juice 橙汁
好了。
从上面的表格中,我们可以直观的看出{soy milk, wine, diaper}是一个频繁项,并且可以找到诸如diaper->wine的关联规则,这意味着,如果有人买了尿布,那他很有可能还会买葡萄酒。
那么,怎么样的集合才能算是频繁的呢?怎么样的规则才能算是有关联呢?或者说是靠谱的呢?我们有支持度(support)和可信度(confidence)两个指标,分别用于频繁项集的发现和关联规则的生成。
某项集支持度被定义为在数据集中包含该项集的记录所占的比例。以上面的表格为例,项集{diaper}的支持度为4/5,项集{diaper, wine}的支持度为3/5。支持度是针对项集所言,因此可以定义一个最小的支持度,只保留一些比较频繁的项集。
假设一条关联规则为{diaper}->{wine},我们将这条关联规则的可信度表示为support({diaper, wine})/support({diaper}),那么我们可以计算出这条规则的可信度的0.75。同样,我们可以设置一个最小的可信度,只保留一些可信度比较高的关联规则。
假设我们有{0,1,2,3}四种物品,物品所有的组合关系是什么样的呢?如下图:
仅仅是4件物品,其组合关系看起来就比较复杂了,更不用说大的数据集了。但是仔细分析我们会发现如果{0,1}是频繁的,那么{0}和{1}自然也是频繁的。这就是著名的Apriori原理:如果某个项集是频繁的,那么它的子集肯定也是频繁的。我们可以换一种说法,如果某项集的子集不是频繁的,那么该项集注定不会频繁,那么我们就可以有小到大,不断排除一些不可能的组合,得到频繁项集。
上图中23是不频繁的,那么023,123,0123注定也是不频繁的,可以排除。
在使用Apriori之前,我们需要先创建几个辅助函数。
假设我们已经拥有了一个候选项集,如何得到频繁项集呢?如下:
对于数据集中的每条交易tran
对于每一个候选项集set
如果set是tran的子集,则增加set的计数
对于每个候选项集
计算支持度
如果对于最小支持度,则保留该项集
返回频繁项集和候选项集的支持度
下面给出代码实现(变量已重新命名,代码基于Python3,与示例代码不同)以及说明:
# 加载数据集
def load_data_arr():
return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]
# 初始化候选项集,创建元素个数为1的候选项集
def create_set_0(data_arr):
# 数组
set_0 = []
# 对每一条交易记录
for transaction in data_arr:
# 对交易记录中的每件物品
for item in transaction:
# 如果该物品没有出现在候选项集中,则添加
if not [item] in set_0:
set_0.append([item])
set_0.sort()
# 返回一个冻结的集合,集合冻结后不能再添加或者删除元素
return map(frozenset, set_0)
# 扫描交易记录集,计算每个候选项集的支持度,返回满足条件的频繁项集以及所有候选项集的支持度
def scan_data_set(data_set_list, set_k, min_support):
set_k_list = list(set_k)
# 候选项集计数
items_cnt = {}
for transaction in data_set_list:
for items in set_k_list:
if items.issubset(transaction):
if items not in items_cnt:
items_cnt[items] = 1
else:
items_cnt[items] += 1
num_transaction = len(data_set_list)
# 数组
return_set_k = []
# 字典
support_data = {}
for items in items_cnt.keys():
# 计算每个候选项集的支持度
support = items_cnt[items]/num_transaction
# 判断项集的支持度是否满足最小支持度
if support >= min_support:
return_set_k.append(items)
# 保存每个候选项集的支持度
support_data[items] = support
return return_set_k, support_data
运行代码:
# 加载数据
data_arr = load_data_arr()
# 创建项个数为1 的候选项集
set_0 = create_set_0(data_arr)
# 扫描项个数为1的候选项集,生成项个数为1 的频繁项集,以及所有候选项集的支持度
set_0_scanned, support_data = scan_data_set(list(map(set, data_arr)), set_0, 0.5)
结果输出为:
[frozenset({1}), frozenset({3}), frozenset({2}), frozenset({5})]
{frozenset({1}): 0.5,
frozenset({3}): 0.75,
frozenset({4}): 0.25,
frozenset({2}): 0.75,
frozenset({5}): 0.75}
由于这个例子比较简单,读者可以根据原始数据集进行计算验证。
到目前为止,我们已经得到项个数为1的频繁项集了,那么如何得到项个数为2,3…的频繁项集呢?这时候就可以使用Apriori算法了,我们基于如果一个项集的子集不是频繁的,那么该项集注定不是频繁的,逐步得到项个数更多的项集,直到所有的项集的都被处理完毕(频繁保留或者不频繁排除)。
Apriori算法的伪代码如下:
当还有未处理完的候选项集
构建含有k+1个项的候选项集
扫描该候选项集得到频繁项集以及支持度
更新频繁项集以及支持度
“当还有未处理完的候选项集”,这句话可能不太好理解,下面给出代码实现,结合代码解释。
# 根据项个数为k+1的频繁项集set_k,生成项个数为k+2的候选项集set_k_plus
def apriori_gen(set_k, k):
return_set_k_plus = []
num_set_k = len(set_k)
for i in range(num_set_k):
for j in range(i+1, num_set_k):
l1 = list(set_k[i])[:k]
l2 = list(set_k[j])[:k]
l1.sort()
l2.sort()
if l1 == l2:
return_set_k_plus.append(set_k[i] | set_k[j]) #set union
return return_set_k_plus
先来看apriori_gen(set_k, k)函数,该函数的作用是根据项个数为k+1的频繁项集set_k,生成项个数为k+2的候选项集set_k_plus,k值的设置比较拗口,之所以比较拗口,是为了调用它的函数考虑的,但是作用还是比较明朗的。
我们将频繁项集中的项集两两比较,由于他们的项个数都是相同的,因此我们只比较他们的前k个项,最后一个项不比较,如果前k个项是相同的,那么最后一项肯定不同(频繁项集不重复),那么我么可以考虑将这两个频繁项集合并,k+1+1,得到含有k+2个项的候选项集。
我们假设k=0,那么该函数的作用就是根据项个数为1的频繁项集set_k,生成项个数为2的候选项集set_k_plus。读者可以自己验证一下。
下面给出重量级Apriori算法的实现。
def apriori(data_arr, min_support=0.5):
# 创建项个数为1的候选项集
set_0 = create_set_0(data_arr)
data_set_list = list(map(set, data_arr))
# 计算项个数为1的频繁项集
set_0_scanned, all_support = scan_data_set(data_set_list, set_0, min_support)
# 将项个数为1的频繁项集放入到总的频繁项集中,下标为0
s = [set_0_scanned]
k = 0
while len(s[k]) > 0:
# 根据项个数为k+1的频繁项集set_k,生成项个数为k+2的候选项集set_k_plus
set_k_plus = apriori_gen(s[k], k)
# 计算项个数为k+2的频繁项集set_k_plus_scanned
set_k_plus_scanned, k_plus_support
= scan_data_set(data_set_list, set_k_plus, min_support)
# 更新
all_support.update(k_plus_support)
s.append(set_k_plus_scanned)
k += 1
return s, all_support
如果读者对while len(s[k]) > 0有疑问,可以试着自己模拟运行一下。
运行代码:
data_arr = load_data_arr()
s, all_support = apriori(data_arr, 0.5)
执行结果:
[
[frozenset({1}), frozenset({3}), frozenset({2}), frozenset({5})],
[frozenset({1, 3}), frozenset({2, 3}), frozenset({3, 5}), frozenset({2, 5})],
[frozenset({2, 3, 5})],
[]
]
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, 3}) : 0.5
frozenset({3, 5}) : 0.5
frozenset({2, 5}) : 0.75
frozenset({1, 2}) : 0.25
frozenset({1, 5}) : 0.25
frozenset({2, 3, 5}) : 0.5
可以改变最小支持度,看频繁项集有什么变化。
到目前为止,我们已经得到频繁项集以及候选项集的支持度,那么就可以着手关联规则的发掘了。
从前面商店的购物清单的例子中我们可以发现,{diaper,wine}是一个频繁项集,那么就可能有两条关联规则,diaper->wine和wine->diaper,前面我们已经给出了关联规则可信度的计算公式,再加上我们已经求得了支持度,那么计算关联规则的可信度应该是一件比较简单的事情。
可以,我们现在面临一个问题,即如何从一个频繁项集中找出所有可能的关联规则呢?我们以频繁项集{0,1,2,3}为例,构造所有的关联规则,如下图。
哇,又是一个这么复杂的图,我们现在分析一下这张图,再总结计算方法。
注意看每一条规则的右侧,在第0层,右侧的项数是0(空集),在第1层,右侧的项数是1,在第2层,右侧的项数是2,在第三层,右侧的项数是3。这种规律是否似曾相识?是的,在前面,生成频繁项集的时候,也是由小到大,不断地合并而成的。关联规则的右侧也是满足这种规律的,那么我们可以从小到大,不断的合并关联规则的右侧,生成所有的关联规则。那关联规则的左侧怎么生成呢?通过频繁项集减去规则右侧即规则左侧。上述做法叫做分级法。
再注意看图中的阴影部分,如果012->3不成立,那么下面与之关联的3条规则也不成立。即如果一条规则不可信,那么由该规则的右侧合并(与其他可信规则的右侧合并)出的规则同样不可信。利用这个属性,我们可以大大减少需要测试的关联规则。
(原书写的是,如果一条规则不可信,那么该条规则的子集同样是不可信的。这个子集有些不好理解)
在给出代码之前,我们先分析一下生成关联规则的开始和结束。假设某频繁项集有m个项,那么规则右侧的项数应该是1~m-1,这就是程序开始结束条件。
下面给出代码:
# 计算规则的可信度
def calculate_conf(freq_item, H, support_data, big_rules_list, min_conf=0.5):
pruned_H = []
for consequence in H:
# 通过支持度计算可信度
# 规则的左侧通过作差求得
conf = support_data[freq_item]/support_data[freq_item - consequence]
if conf >= min_conf:
big_rules_list.append((freq_item - consequence, consequence, conf))
# 需要返回可信的规则的右侧,以便再次进行合并,同时抛弃不可信规则的右侧
pruned_H.append(consequence)
return pruned_H
# 从序列中提取规则
# H是规则的右侧
def rules_from_consequence(freq_item, H, support_data, big_rules_list, min_conf=0.5):
m = len(H[0])
# 原书代码中此处没有等号,是不对的,读者可以结合前面给出的结束条件,判断此处是否需要添加等号
if len(freq_item) >= (m+1):
# 下面的两行代码,原书给出的顺序是错误的,应该先计算可信度,再合并
# 读者可以根据计算出的频繁项集和支持度手动计算一下,可以发现原执行顺序会漏掉一部分规则
hmp1 = calculate_conf(freq_item, H, support_data, big_rules_list, min_conf)
hmp1 = apriori_gen(hmp1, m - 1)
# 下面这行代码使没有用的,因为合并右侧后,肯定是需要去提取规则并计算规则可信度的。
# if len(hmp1) > 1:
rules_from_consequence(freq_item, hmp1, support_data, big_rules_list, min_conf)
# 生成关联规则
def generate_rules(s, support_data, min_conf=0.5):
big_rules_list = []
for i in range(1, len(s)):
for freq_set in s[i]:
h1 = [frozenset([item]) for item in freq_set]
if i > 1:
# 项数大于2,至少为3,可以组合项,提取规则
rules_from_consequence(freq_set, h1, support_data, big_rules_list, min_conf)
else:
calculate_conf(freq_set, h1, support_data, big_rules_list, min_conf)
return big_rules_list
执行程序:
L, supportData = apriori(load_data_arr())
bigRuleList = generate_rules(L, supportData, 0.5)
执行结果:
(frozenset({3}), frozenset({1}), 0.6666666666666666)
(frozenset({1}), frozenset({3}), 1.0)
(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, 5}), frozenset({2}), 1.0)
(frozenset({2, 5}), frozenset({3}), 0.6666666666666666)
(frozenset({2, 3}), frozenset({5}), 1.0)
(frozenset({5}), frozenset({2, 3}), 0.6666666666666666)
(frozenset({3}), frozenset({2, 5}), 0.6666666666666666)
(frozenset({2}), frozenset({3, 5}), 0.6666666666666666)
此结果与书中给出的结果不一致,但是经过作者手动计算,此结果是正确的,读者可以自行验证。
至此,本文的主要内容就结束了,下面简单的回顾一下本文的主要内容。
首先是频繁项集和关联规则,这是两个重要的主题;
其次是支持度和可信度,这是两个重要的指标;
著名的Apriori算法,生成候选项集,计算支持度,得到频繁项集;
可信度的计算公式;
生成关联规则,计算可信度,得到感兴趣的关联规则。
本文基于一个小小的数据集,后期会在一个较大的数据集上运行本文的算法,敬请期待!
文中若有不足之处,欢迎各位读者指正,谢谢!