Apriori

Apriori

从大规模数据集中寻找物品间的隐含关系被称作关联分析(association analysis)或者关联规则学习(association rule learning)。

那么什么是关联分析呢?关联分析是一种在大规模数据集中寻找有趣关系的任务。这些任务包括两项:发现频繁项集和从频繁项集中发现关联规则。

  • 频繁项集(frequent item sets):是经常出现在一块的物品的集合,例如香烟和打火机。
  • 关联规则(association rules):暗示两种物品之间可能存在很强的关系,通常表示物品之间的“如果…那么”关系,例如“如果购买香烟,那么有很大概率会购买打火机”。

那么如何定量地衡量一物品集合是否频繁的呢?以及如何定量地衡量两种物品之间的关系?在这里就需要引入一些新的概念:

  • 项集:项的集合,项可以是商品,那么项集就是商品的集合。
  • 支持度:数据集中包含该项集的记录所占的比例,也就是该项集在数据集中的出现频率,用以衡量项集的频繁程度。
  • 可信度:又称置信度,是针对关联规则来定义的,表示某项集在指定条件下的出现概率,用以衡量物品之间的关系。

下面我们结合实例来说明如何进行关联分析。

【示例】:杂货店的交易清单。

交易号码商品
0豆奶,莴苣
1莴苣,尿布,葡萄酒,甜菜
2豆奶,尿布,葡萄酒,橙汁
3莴苣,豆奶,尿布,葡萄酒
4莴苣,豆奶,尿布,橙汁

从数据集中可以得到,{豆奶} 分别在交易号码(以下简称编号 0、2、3、4)中出现,根据支持度的定义“数据集中包含该项集的记录所占的比例”,数据集中包含 {豆奶} 的记录共有 4 条,总数据数为 5 ,因此可求出 {豆奶} 的支持度为 0.8。同理,我们可求出 {甜菜} 的支持度为 0.2。

我们已经计算出 {豆奶} 和 {甜菜} 这两个项集的支持度,那么它们都是频繁的吗?这时候就需要我们设置一个阈值,如果项集的支持度大于等于该阈值,则我们认为该项集是一个频繁项集,例如设置阈值为 0.6,那么 {豆奶} 就是一个频繁项集,而 {甜菜} 就不是频繁项集。上面我们设置的阈值通常被称为最小支持度。有了最小支持度之后,我们就可以保留满足最小支持度要求的项集。

在最小支持度为 0.6 的前提下,项集 {尿布,葡萄酒} 是一个频繁项集,我们可以根据该频繁项集找出关联规则,例如 {尿布}->{葡萄酒}。根据可信度的定义“表示某项集在指定条件下的出现概率”,在这里“指定条件”指的是 {尿布} 的支持度,那么这条规则的可信度可被定义为“支持度 {尿布,葡萄酒} / 支持度 {尿布}”。从数据集中可以看到,{尿布,葡萄酒} 的支持度为 0.6,尿布的支持度为 0.8,所以 {尿布->葡萄酒} 的可信度度为 0.75。这意味着对于包含“尿布”的所有记录,我们的规则对其中 75% 的记录都适用,也就是说如果有人买了尿布,那么他很可能也会买葡萄酒。

使用频繁项集和关联规则,商家就可以更好地理解他们的顾客。

通过上面的分析可以知道,支持度和可信度是用来量化关联分析是否成功的方法。假设想找到支持度大于 0.8 的所有项集,应该如何去做?一个办法是生成一个物品所有可能组合的清单,然后对每一种组合统计它出现的频繁程度,但当物品成千上万时,上述做法非常非常慢,且所需的计算代价很高,蛮力搜索方法并不能解决这个问题,所以需要用更智能的方法在合理的时间范围内找到频繁项集。此时可采用 Apriori 原理,以减少关联规则学习时所需的计算量。

Apriori 原理

假设我们在经营一家商品种类并不多的杂货店,我们对那些经常在一起被购买的商品非常感兴趣。我们只有 4 种商品:商品 0,商品 1,商品 2 和商品 3。我们不关系客户购买某一件商品多少件,我们只关心客户购买不同种类商品。

下图显示了物品之间所有可能的组合。图中从上往下的第一个集合是空集,表示不包含任何物品的集合。物品集合之间的连线表明两个或者更多集合可以组合形成一个更大的集合。

项集组合.jpg

在计算项集支持度时,我们需要遍历每条记录并检查该记录是否包含项集中的元素。在扫描完所有数据之后,使用统计得到的项集的记录总数除以总的交易记录数,就可以得到该项集的支持度。观察上图可以发现,即使对于仅有 4 种物品的集合,也需要遍历数据 15 次。而随着物品数目的增加遍历次数会急剧增长。对于包含 N 种物品的数据集共有 2 N − 1 2^N - 1 2N1 种项集组合。当物品的数量增加时,可能的项集组合也随着称指数级增长,对于现代的计算机而言,需要很长的时间才能完成运算。

为了降低所需的计算时间,研究人员发现一种所谓的 Apriori 原理,该原理可以帮我们减少可能感兴趣的项集。

  • 如果某个项集是频繁的,那么它的所有子集也是频繁的,见上图左图;
  • 如果某个项集是非频繁的,那么它的所有超集也是非频繁的,见上图右图。

该原理是如何推导得出的?

  • 假设项集 {1,2,3} 为频繁项集,支持度为 S。
    P ( 123 ) ≥ S P ( 12 ) ≥ P ( 123 ) = P ( 12 ) P ( 3 ∣ 12 ) → P ( 12 ) ≥ S P(123) \geq S \quad P(12) \geq P(123) = P(12)P(3|12) \rightarrow P(12) \geq S P(123)SP(12)P(123)=P(12)P(312)P(12)S
  • 假设项集 {2,3} 为非频繁项集,支持度为 S。
    P ( 23 ) &lt; S P ( 123 ) = P ( 23 ) P ( 1 ∣ 23 ) ≤ P ( 23 ) → P ( 123 ) &lt; S P(23) \lt S \quad P(123) = P(23)P(1|23) \leq P(23) \rightarrow P(123) \lt S P(23)<SP(123)=P(23)P(123)P(23)P(123)<S
    根据该原理,假设知道项集 {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 算法的原因。Apriori 在拉丁语中指“来自以前”。当定义问题时,通常会使用先验知识或者假设,这被称作“一个先验”(a priori)。先验知识可能来自领域知识,先前的一些测量结果等等。在关联分析中,我们运用先验知识去判断后续的项集是否频繁。

Apriori 工作过程

如何将 Apriori 原理应用于算法中?难点在于我们该如何根据已有的项集去组合新的项集。

Apriori算法工作流程

  • C1,C2,…,Ck 分别表示 1-项集,2-项集,…,k-项集;
  • L1,L2,…,Lk 分别表示对应项集经过“过滤”后的频繁项集;
  • Scan:表示数据项扫描函数,该函数过滤不满足最小支持度的项集。

观察上面的 Apriori 算法的工作流程我们可以发现,Apriori 算法首先扫描一遍数据集,从中生成 1-项集 C1。接着调用 Scan 函数扫描 C1,过滤不满足最小支持度的项集,最后留下的项集就是频繁项集 L1。根据 Apriori 原理可知,非频繁项集的所有超集也都是非频繁的,那么我们就没有必要球这些非频繁项集的组合。因此,第二轮迭代中,只需要对上一轮迭代产生的频繁项集进行新的组合即可,然后接着调用 Scan 函数检查新组合的支持度是否满足最小支持度要求,将不满足的新组合给过滤。如此循环,直到没有新组合可生成为止。

  • 连接步:分为两种情况,第一是从数据集中生成 C1,第二是根据 L k − 1 L_{k-1} Lk1 生成 Ck。简单地说,连接步就是产生项集的过程。
  • 剪枝步:剔除不满足最小支持度的项集。从图中来看就是从 Ck 到 Lk 的过程。

示例说明

假设现在数据库中有 4 条交易记录,其中有 5 件不同种类的商品,分别用编号 1、2、3、4、5 表示,最小支持度为 0.5。

TIDItems
1001 3 4
2002 3 5
3001 2 3 5
4002 5
  • 第一轮:先从数据库中扫描,生成 1-项集 C1。
Itemssupport
{1}0.50
{2}0.75
{3}0.75
{4}0.25
{5}0.75
  • 第一轮:调用 Scan 函数,过滤支持度小于 0.5 的项集。可以看到上表中商品 4 的支持度小于 0.5,因此将 {4} 过滤,得到 L1。
Itemssupport
{1}0.50
{2}0.75
{3}0.75
{5}0.75
  • 第二轮:根据 L1,生成 2-项集 C2。
Itemssupport
{1, 2}0.25
{1, 3}0.50
{1, 5}0.25
{2, 3}0.50
{2, 5}0.75
{3, 5}0.50
  • 第二轮:调用 Scan 函数,过滤支持度小于 0.5 的项集,可以看到项集 {1, 2} 和 {1, 5} 不满足最小支持度要求,因此得到 L2。
Itemssupport
{1, 3}0.50
{2, 3}0.50
{2, 5}0.75
{3, 5}0.50
  • 第三轮:根据 L2,生成 3-项集 C3。
Itemssupport
{1, 2, 3}0.25
{1, 2, 5}0.25
{1, 3, 5}0.25
{2, 3, 5}0.50
  • 第三轮:调用 Scan 函数,只有项集 {2, 3, 5} 满足要求,得到 L3。
Itemssupport
{2, 3, 5}0.50
  • 第四轮:根据 L3 生成 C4,此时 L3 也不能产生新的组合,循环结束。

从上面的过程中我们可以发现,Apriori 算法工作过程中的连接步,在每次执行时都需要扫描一遍数据库,来计算每个新组合(项集)的支持度。借此,我们可以发现 Apriori 算法的优缺点以及适用的数据类型。

  • 优点:易编码实现;
  • 缺点:在大数据集上可能较慢;
  • 适用数据类型:数值型或者标称型数据。

Apriori 算法实现

Apriori 算法是发现频繁项集的一种方法。Apriori 算法的两个输入参数分别是最小支持度和数据集。

【过程】:该算法首先会生成所有单个物品的项集列表。接着扫描交易记录来查看哪些项集满足最小支持度要求,那些不满足最小支持度的集合会被去掉。然后,对剩下来的集合进行组合以生成包含两个元素的项集。接下来,再重新扫描交易记录,去掉不满足最小支持度的项集。该过程重复进行直到所有项集都被去掉。

生成候选项集

Apriori 算法首先构建集合 c1,然后扫描数据集来判断这些只有一个元素的项集是否满足最小支持度的要求。那些满足要求的项集构成集合 L1。而 L1 中的元素相互组合构成 c2,c2 再进一步过滤变为 L2。

【伪代码】:

对数据集中的每条交易记录 tran
对每个候选项集 can:
    检查一下 can 是否是 tran 的子集:
    如果是,则增加 can 的计数值
    对每个候选项集:
    如果其支持度不低于最小值,则保留该项集
    返回所有频繁项集列表

create_c1()

create_c1() 函数构建大小为 1 的所有候选项集的集合。

def create_c1(dataset):
    c1 = []
    for transaction in dataset:
        for item in transaction:
            if not [item] in c1:
                c1.append([item])
    c1.sort()
    # 对 c1 中每个项构建一个不变集合
    return list(np.map(frozenset, c1))
  • 首先创建一个空列表 c1,用来存储所有不重复的项值。
  • 接下来遍历数据集中的所有交易记录。对每一条记录,遍历记录中的每一个项。
for transaction in dataset:
    for item in transaction:
    // ...
  • 如果某个物品项没有在 c1 中出现,则将其添加到 c1 中。需要注意的是,这里并不是简单地添加每个物品项,而是添加只包含该物品项的一个列表。目的在于为每个物品项构建一个集合。因为在 Apriori 算法的后续处理中,需要做集合操作,并且 Python 不能创建只有一个整数的集合,因此这里必须使用列表。
if not [item] in c1:
    c1.append([item])
  • 接着对 c1 进行排序,并将其中的每个单元素列表映射到 frozenset(),最后返回 frozenset 的列表。需要注意的是这里使用的数据结构是 Python 中的 frozenset 类型。frozenset 是指被“冰冻”的集合,就是说它们是不可改变的,即用户不能修改它们。这里必须要使用 frozenset 而不是 set 类型,因为之后必须要将这些集合作为字典键值使用,使用 frozenset 可以实现这一点,而 set 却做不到。
c1.sort()
return list(map(frozenset, c1)
  • 需要注意 Python 3.x 和 Python 2.x 不同,map() 函数返回的是可迭代对象,而不是列表。

scan_D()

scan_D() 有三个参数,分别是数据集 D、候选项集列表 ck 以及感兴趣项集的最小支持度 min_support,用于从 c1 生成 L1。另外,该函数返回 L1 和包含支持度值的字典以备后用。

def scan_D(D, ck, min_support):
    ss_cnt = {}
    for tid in D:
        for can in ck:
            if can.issubset(tid):
                if not ss_cnt.__contains__(can): 
                    ss_cnt[can] = 1
                else:
                    ss_cnt[can] += 1
    num_items = float(len(D))
    ret_list = []
    support_data = {}
    for key in ss_cnt:
        support = ss_cnt[key] / num_items
        if support >= min_support:
            ret_list.append(key)
        support_data[key] = support
    return ret_list, support_data
  • 首先创建一个空字典 ss_cnt,然后遍历数据集中的所有交易记录以及 c1 中的所有候选项集。如果 c1 中的集合是记录的一部分,那么增加字典中对应的计数值。不存在,则将当前集合添加到字典中,并将计数值设置为 1。需要注意的是 Python 3.x 没有 has_key() 方法,可以用 __contains__() 方法代替。
ss_cnt = {}
for tid in D:
    for can in ck:
        if can.issubset(tid):
            if not ss_cnt.__contains__(can): 
                ss_cnt[can] = 1
            else:
                ss_cnt[can] += 1
  • 当扫描完数据集中的所有项以及所有候选集时,就需要计算支持度。不满足最小支持度要求的集合不会输出。
num_items = float(len(D))
ret_list = []
support_data = {}
for key in ss_cnt:
    support = ss_cnt[key] / num_items
    if support >= min_support:
        ret_list.append(key)
    support_data[key] = support
return ret_list, support_data

函数测试

  • 先导入测试数据集
>>> dataset = [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]
  • 构建第一个候选项集集合 c1
>>> c1 = create_c1(dataset)
>>> c1
[frozenset([1]), frozenset([2]), frozenset([3]), frozenset([4]), frozenset([5])]
  • 可以看到,c1 包含了每个 fronzenset 中的单个物品项。下面构建集合表示的数据集 D。
>>> D = list(map(set, dataset))
>>> D
[set([1, 3, 4]), set([2, 3, 5]), set([1, 2, 3, 5]), set([2, 5])]
  • 有了集合形式的数据,issubset() 函数就能够派上用场,从而可以去掉那些不满足最小支持度的项集。对上面的例子,我们使用 0.5 作为最小支持度水平。
>>> L1, supp_data = scan_D(D, c1, 0.5)
>>> L1
[frozenset([1]), frozenset([3]), frozenset([2]), frozenset([5])]

上述 4 个项集构成了 L1 列表,该列表中的每个单物品项集至少出现在 50% 以上的记录中。由于物品 4 并没有达到最小支持度,所以没有包含在 L1 中。通过去掉这件物品,减少了查找物品项集的工作量。

组织完整的 Apriori 算法

【伪代码】:

当集合中项的个数大于 0 时:
    构建一个 k 个项组成的候选项集的列表
    检查数据以确认每个项集都是频繁的
    保留频繁项集并构建 k + 1 项组成的候选项集的列表

apriori_gen()

apriori_gen() 函数有两个输入参数,分别为频繁项集列表 lk 与项集元素个数 k,输出为 ck。例如,该函数以 {0}、{1}、{2} 作为输入,会生成 {0,1}、{0,2} 以及 {1,2}。

def apriori_gen(lk, k):
    ret_list = []
    len_lk = len(lk)
    for i in range(len_lk):
        for j in range(i + 1, len_lk):
            l1 = list(lk[i])[:k-2]
            l2 = list(lk[j])[:k-2]
            l1.sort()
            l2.sort()
            if l1 == l2:
                ret_list.append(lk[i] | lk[j])
    return ret_list
  • 首先创建一个空列表,然后计算 lk 中的元素数目。
ret_list = []
len_lk = len(lk)
  • 接下来,比较 lk 中的每一个元素与其他元素,这可以通过两个 for 循环来实现。紧接着,取列表中的两个集合进行比较。如果这两个集合的前 k - 2 个元素都相等,那么就将这两个集合合成一个大小为 k 的集合。这里使用集合的并操作来完成,在 Python 中对应操作符 |。
for i in range(len_lk):
    for j in range(i + 1, len_lk):
        l1 = list(lk[i])[:k-2]
        l2 = list(lk[j])[:k-2]
        l1.sort()
        l2.sort()
        if l1 == l2:
            ret_list.append(lk[i] | lk[j])
return ret_list

【问】:为什么要取列表(l1、l2)的前 k - 2 个数据?

【答】:假设现在要将 {0,1}、{0,2}、{1,2} 来创建三元素项集,如果仅仅将两个项集合并,就会得到三个 {0,1,2}。也就是说,同样的结果会重复 3 次,我们要做的是确保遍历列表的次数最少。现在,如果只比较集合的第 1 个元素,并且只对第 1 个元素相同的集合求并操作,同样可以得到 {0,1,2},且只需要一次操作。

apriori()

apriori() 函数有两个输入参数,数据集以及支持度,函数会生成候选项集的列表以及支持度数据并返回。

def apriori(dataset, min_support=0.5):
    c1 = create_c1(dataset)
    D = list(map(set, dataset))
    l1, support_data = scan_D(D, c1, min_support)
    L = [l1]
    k = 2
    while len(L[k-2]) > 0:
        ck = apriori_gen(L[k-2], k)
        lk, supk = scan_D(D, ck, min_support)
        support_data.update(supk)
        L.append(lk)
        k += 1
    return L, support_data
  • 首先创建 c1,然后读入数据集将其转化为 D(集合列表)。
c1 = create_c1(dataset)
D = list(map(set, dataset))
  • 接下来,使用 scan_D() 函数来创建 l1,并将 l1 放入列表 L 中。L 会包含 l1、l2、l3 …。
l1, support_data = scan_D(D, c1, min_support)
L = [l1]
k = 2
  • 继续寻找后续的多元项集,从而创建包含更大项集的列表,直到下一个大的项集为空。
while len(L[k-2]) > 0:
    ck = apriori_gen(L[k-2], k)
    lk, supk = scan_D(D, ck, min_support)
    support_data.update(supk)
    L.append(lk)
    k += 1
return L, support_data

从频繁项集中挖掘关联规则

现在需要解决的问题是如何找出关联规则?要找到关联规则,我们首先从一个频繁项集开始。我们知道集合中的元素是不重复的,但我们想知道基于这些元素能否获得其他内容。某个元素或者某个元素集合可能会推导出另一个元素。从杂货店的例子可以得到,如果有一个频繁项集 {豆奶,莴苣},那么就可能有一条关联规则“豆奶->莴苣”。这意味着如果有人购买了豆奶,那么在统计上他会购买莴苣的概率较大。但是,这一条反过来并不总是成立。也就是说,即使“豆奶->莴苣”统计上显著,那么“莴苣->豆奶”也不一定成立。

最小支持度要求是频繁项集的量化定义;对于关联规则,这种量化指标称为可信度。现在要获得可信度,所需要做的只是取出那些支持度值做一次除法运算。

从一个频繁项集中可以产生多少条关联规则?下图显示从频繁项集 {0,1,2,3} 产生的所有关联规则,阴影区域给出的是低可信度的规则。为找到感兴趣的规则,我们先生成一个可能的规则列表,然后测试每条规则的可信度。如果可信度不满足最小要求,则去掉该规则。

关联规则找寻与性质

观察上图可一发现,如果某条规则并不满足最小可信度要求,那么该规则的所有子集也不会满足最小可信度要求。例如,{0,1,2} -> 3 不满足最小可信度要求,那么任何左部为 {0,1,2} 子集的规则也不会满足最小可信度要求。

【证明】:令最小可信度要求为 C

P(3|012) = \frac{P(0123)}{P(012)} \quad P(13|02) = \frac{P(0123)}{P(02)}

P(02) \geq P(012) \quad C \geq P(3|012) \geq P(13|02)

因此我们可以利用关联规则的这条性质来减少需要测试的规则数目。

【做法】:

  1. 首先从一个频繁项集开始;
  2. 接着创建一个规则列表,其中规则右部只包含一个元素;
  3. 然后对这些规则进行测试;
  4. 接下来合并所有剩余规则来创建一个新的规则列表,其中规则右部包含两个元素。这种方法也被称作分级法。

cal_conf()

cal_conf() 函数计算规则的可信度以及找到满足最小可信度要求的规则。函数接受五个参数:

  • freq_set:频繁项集
  • h:出现在规则右部的元素列表
  • support_data:包含频繁项集支持度数据的字典
  • brl:通过最小可信度要求的规则列表
  • min_\conf:最小可信度

函数会返回一个满足最小可信度要求的规则列表。

def calc_conf(freq_set, h, support_data, br1, min_conf=0.7):
    pruned_h = []
    for conseq in h:
        conf = support_data[freq_set] / support_data[freq_set - conseq]
        if conf >= min_conf:
            print(freq_set - conseq, '-->', conseq, 'conf:', conf)
            br1.append((freq_set - conseq, conseq, conf))
            pruned_h.append(conseq)
    return pruned_h
  • 首先创建一个空列表 pruned_h 用以保存满足要求的规则。
  • 接下来,遍历 h 中的所有项集并计算它们的可信度。可信度计算时使用 support_data 中的支持度数据,可以节省大量计算时间。
pruned_h = []
for conseq in h:
    conf = support_data[freq_set] / support_data[freq_set - conseq]
    // ...
  • 再判断当前项集的可信度是否满足最小可信度要求,若满足则先输出到屏幕,然后添加到 pruned_h 和 brl 列表中,最后将 pruned_h 列表返回。
if conf >= min_conf:
    print(freq_set - conseq, '-->', conseq, 'conf:', conf)
    br1.append((freq_set - conseq, conseq, conf))
    pruned_h.append(conseq)

rules_from_conseq()

rules_from_conseq() 函数从最初的项集中生成更多的关联规则。该函数接受的参数同 cal_conf() 函数。

def rules_from_conseq(freq_set, h, support_data, br1, min_conf=0.7):
    m = len(h[0])
    if len(freq_set) > (m + 1):
        hmp1 = apriori_gen(h, m + 1)
        hmp1 = calc_conf(freq_set, hmp1, support_data, br1, min_conf)
        if len(hmp1) > 1:
            rules_from_conseq(freq_set, hmp1, support_data, br1, min_conf)

【说明】:先计算 h 中的频繁项集大小 m,然后查看该频繁项集是否大到可以移除大小为 m 的子集。如果不可以的话,则生成 h 中元素的无重复组合,将结果存储在 hmp1 中,这也是下一次迭代的 h 列表。怎么理解呢?例如频繁项集 {1, 2, 3},此时 h 为 [{1}, {2}, {3}],除了可以生成 {1, 2} -> {3},{1, 3} -> {2},{2, 3} -> {1} 之外,也可以生成 {1} -> {2, 3}。所以我们需要递归调用 rules_from_conseq() 函数来生成新的 h,并判断当前规则是否满足最小可信度要求。

generate_rules()

generate_rules() 函数有三个输入参数,频繁项集列表 l,包含频繁项集支持度数据的字典 support_data,最小可信度阈值 min_conf。函数最后返回一个包含可信度的规则列表。

def generate_rules(l, support_data, min_conf=0.7):
    big_rule_list = []
    for i in range(1, len(l)):
        for freq_set in l[i]:
            h1 = [frozenset([item]) for item in freq_set]
            if i > 1:
                rules_from_conseq(freq_set, h1, support_data, big_rule_list, min_conf)
            else:
                calc_conf(freq_set, h1, support_data, big_rule_list, min_conf)
    return big_rule_list
  • 该函数遍历频繁项集列表中的每一个频繁项集,并对每个频繁项集创建只包含单个元素集合的列表 h1,目的在于构建形如 {1,2} -> {3} 的关联规则。
# 循环频繁项集列表,依次对 X-频繁项集执行操作
for i in range(1, len(l)):
    # 循环当前 X-频繁项集的各项集
    for freq_set in l[i]:
        h1 = [frozenset([item]) for item in freq_set]
  • 因为无法从 1-项集中构建关联规则,所以要从包含两个或者更多元素的项集开始构建关联规则。如果从集合 {0, 1, 2} 开始,那么 h1 应该是 [{0}, {1}, {2}]。如果频繁项集的数目超过 2,那么考虑对其做进一步的合并(低啊用 rules_from_conseq() 函数)。如果项集中只有两个元素,则可以直接调用 cal_conf() 函数来计算可信度值。
if i > 1:
    rules_from_conseq(freq_set, h1, support_data, big_rule_list, min_conf)
else:
    calc_conf(freq_set, h1, support_data, big_rule_list, min_conf)

大家可以自行对代码进行测试,观察不同可信度下得出的规则,并且可以看到规则互换前件和后件,规则不一定会成立。

【完整代码】:传送门

class Apriori:
    
    def __init__(self):
        pass
    
    def _create_c1(self, 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 _scan_D(self, D, ck, min_support):
        ss_cnt = {}
        for tid in D:
            for can in ck:
                if can.issubset(tid):
                    if not ss_cnt.__contains__(can): 
                        ss_cnt[can] = 1
                    else:
                        ss_cnt[can] += 1
        num_items = float(len(D))
        ret_list = []
        support_data = {}
        for key in ss_cnt:
            support = ss_cnt[key] / num_items
            if support >= min_support:
                ret_list.insert(0, key)
            support_data[key] = support
        return ret_list, support_data
    
    def _apriori_gen(self, lk, k):
        # creates CK
        ret_list = []
        len_lk = len(lk)
        for i in range(len_lk):
            for j in range(i + 1, len_lk):
                l1 = list(lk[i])[:k-2]
                l2 = list(lk[j])[:k-2]
                l1.sort()
                l2.sort()
                if l1 == l2:
                    ret_list.append(lk[i] | lk[j])
        return ret_list
    
    def apriori(self, dataset, min_support=0.5):
        c1 = self._create_c1(dataset)
        D = list(map(set, dataset))
        l1, support_data = self._scan_D(D, c1, min_support)
        l = [l1]
        k = 2
        while len(l[k-2]) > 0:
            ck = self._apriori_gen(l[k-2], k)
            lk, supk = self._scan_D(D, ck, min_support)
            support_data.update(supk)
            l.append(lk)
            k += 1
        return l, support_data
    
    def generate_rules(self, l, support_data, min_conf=0.7):
        big_rule_list = []
        for i in range(1, len(l)):
            for freq_set in l[i]:
                h1 = [frozenset([item]) for item in freq_set]
                if i > 1:
                    self._rules_from_conseq(freq_set, h1, support_data, big_rule_list, min_conf)
                else:
                    self._calc_conf(freq_set, h1, support_data, big_rule_list, min_conf)
        return big_rule_list
    
    def _calc_conf(self, freq_set, h, support_data, br1, min_conf=0.7):
        pruned_h = []
        for conseq in h:
            conf = support_data[freq_set] / support_data[freq_set - conseq]
            if conf >= min_conf:
                print(freq_set - conseq, '-->', conseq, 'conf:', conf)
                br1.append((freq_set - conseq, conseq, conf))
                pruned_h.append(conseq)
        return pruned_h
    
    def _rules_from_conseq(self, freq_set, h, support_data, br1, min_conf=0.7):
        m = len(h[0])
        if len(freq_set) > (m + 1):
            hmp1 = self._apriori_gen(h, m + 1)
            hmp1 = self._calc_conf(freq_set, hmp1, support_data, br1, min_conf)
            if len(hmp1) > 1:
                self._rules_from_conseq(freq_set, hmp1, support_data, br1, min_conf)

后话

如前面所介绍的,Apriori 算法在产生频繁模式完全集前需要对数据库进行多次扫描,同时产生大量的候选频繁项集,而且每次增加频繁项集的大小,Apriori 算法都会重新扫描整个数据集,这就使得 Apriori 算法时间和空间复杂度较大。当数据集很大时这会显著降低频繁项集的发现速度。

可以看出,Apriori 算法的主要时间和空间开销集中于数据集的多次全部访问,以及产生大量的频繁候选集。那么基于此有没有更好的方法用于改进 Apriori 算法,从而提高算法的效率呢?

一些学者在基于 Apriori 算法思想的条件下,主要提出了 FP-growth,GSP,CBA 等算法,事实上,在实际使用当中,也很少直接使用 Apriori 算法,但是理解 Apriori 算法是理解其他 Apriori 类算法的前提。

参考

  • 《机器学习实战》
  • 《数据挖掘:概念与技术第三版》
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页