机器学习实战(十)Apriori算法进行关联分析

一、前言

      在去杂货店买东西的过程,实际包含了许多机器学习的当前及未来应用,这包括物品的展示方式、购物之后优惠券的提供以及用户忠诚度计划,等等。它们都离不开对大量数据的分析。
      通过查看哪些商品经常在一起购买,可以帮助商店了解用户的购买行为。这种从数据海洋中抽取的知识可以用于商品定价、市场促销、存货管理等环节。从大规模数据集中寻找物品间的隐含关系被称作关联分析(association analysis ) 或者关联规则学习(association rule learning)。这里的主要问题在于,寻找物品的不同组合是一项十分耗时的任务,所需的计算代价很高,蛮力搜索方法并不能解决这个问题,所以需要用更智能的方法在合理的时间范围内找到频繁项集。
      下面首先详细讨论关联分析,然后讨论Apriori原 理,Apriori算法正是基于该原理得到的。接下来创建函数频繁项集髙效发现的函数,然后从频繁项集中抽取出关联规则。

1.1 关联分析

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

关联分析是一种在大规模数据集中寻找有趣关系的任务。这些关系可以有两种形式:频繁项集或者关联规则频 繁 项 集 (frequent item sets)是经常出现在一块的物品的集合关联规则(association rules )暗示两种物品之间可能存在很强的关系。下面会用一个例子来说明这两种概念。图11-1给出了某个杂货店的交易清单。   
                              这里写图片描述

频繁项集是指那些经常出现在一起的物品集合,图11-1中的集合{葡萄酒,尿布,豆奶丨就是频繁项集的一个例子(回想一下,集合是由一对大括号“{ }”来表示的)。从下面的数据集中也可以找到诸如尿布―葡萄酒的关联规则。这意味着如果有人买了尿布,那么他很可能也会买葡萄酒

应该如何定义这些有趣的关系?谁来定义什么是有趣?当寻找频繁项集时,频 繁(frequent) 的定义是什么?有许多概念可以解答上述问题,不过其中最重要的是支持度和可信度

一个项集的支持度(support)被定义为数据集中包含该项集的记录所占的比例。从图11-1中可以得到,{豆奶)的支持度为4/5。而在5条交易记录中有3条包含{豆奶,尿布} , 因此{豆奶,尿布}的支持度为3/5。支持度是针对项集来说的,因此可以定义一个最小支持度,而只保留满足最小支持度的项集。

可信度或置信度(confidence)是针对一条诸如{尿布}->{葡萄酒}的关联规则来定义的。这条规则的可信度被定义为“支持度({尿布,葡萄酒})/支持度({尿布})”。从图11-1中可以看到,由于 {尿布,葡萄酒}的支持度为3/5,尿布的支持度为4/5,所以“尿 布 — 葡萄酒”的可信度为3/4=0.75。这意味着对于包含“尿布”的所有记录,我们的规则对其中75%的记录都适用

支持度和可信度是用来量化关联分析是否成功的方法。

1.2 Apriori原理


假设我们在经营一家商品种类并不多的杂货店,我们对那些经常在一起被购买的商品非常感兴趣。我们只有4种商品:商品0,商品1,商品2和商品3。那么所有可能被一起购买的商品组合都有哪些?这些商品组合可能只有一种商品,比如商品0 , 也可能包括两种、三种或者所有四种商品。我们并不关心某人买了两件商品0以及四件商品2的情况,我们只关心他购买了一种或多种商品。(典型的高中的组合问题,C1N+C2N+...CNNCN1+CN2+...CNN,总共为2N−1种组合2N−1种组合)

Apriori算法的一般过程
⑴ 收集数据:使用任意方法。
(2)准备数据:任何数据类型都可以,因为我们只保存集合。
(3)分析数据:使用任意方法。
(4)训练算法:使用Apriori算法来找到频繁项集。
(5)测试算法:不需要测试过程。
(6)使用算法:用于发现频繁项集以及物品之间的关联规则。

图11-2显示了物品之间所有可能的组合。为了让该图更容易懂,图中使用物品的编号0来取代物品0本身。另外,图中从上往下的第一个集合是∅∅,表示空集或不包含任何物品的集合。物品集合之间的连线表明两个或者更多集合可以组合形成一个更大的集合。
前面说过,我们的目标是找到经常在一起购买的物品集合。而在11.1节中,我们使用集合的支持度来度量其出现的频率。一个集合的支持度是指有多少比例的交易记录包含该集合。如何对一个给定的集合,比如{0,3},来计算其支持度?我们遍历毎条记录并检查该记录包含0和3,如果记录确实同时包含这两项,那么就增加总计数值。在扫描完所有数据之后,使用统计得到的总数除以总的交易记录数,就可以得到支持度。上述过程和结果只是针对单个集合{0,3} 。要获得每种可能集合的支持度就需要多次重复上述过程。我们可以数一下图11-2中的集合数目,会发现即使对于仅有4种物品的集合,也需要遍历数据15次。而随着物品数目的增加遍历次数会急剧增长。对于包含— 物品的数据集共有2N−12N−1种项集组合。事实上,出售10 000或更多种物品的商店并不少见。即使只出售100种商品的商店也会有1.26∗10301.26∗1030种可能的项集组合。对于现代的计算机而言,需要很长的时间才能完成运算。
                                 这里写图片描述

扫描二维码关注公众号,回复: 10996294 查看本文章

为了降低所需的计算时间,研究人员发现一种所谓的Apriori原理。Apriori原理可以帮我们减少可能感兴趣的项集。Apri0ri原理是说如果某个项集是频繁的,那么它的所有子集也是频繁的。对于图11-2给出的例子,这意味着如果{0,1}是频繁的,那么{0}、{1}也一定是频繁的。这个原理直观上并没有什么帮助,但是如果反过来看就有用了,也就是说如果一个项集是非频繁集,那么它的所有超集也是非频繁的(如图11-3所示)(很容易理解,如果一个项集为非频繁集,那么它的支持度小于给定最小支持度,那么它的所有超集也必定小于最小支持度,因此它的所有超集也是非频繁的)。

在 图11-3中 ,已知阴影项集{2,3}是非频繁的。利用这个知识,我们就知道项{0,2,3} ,{1,2,3}以及{0,1,2,3}也是非频繁的。这也就是说,一旦计算出了{2,3}的支持度,知道它是非频繁的 之后,就不需要再计算{0,2,3}、{1,2,3}和 {0,1,2,3}的支持度,因为我们知道这些集合不会满足我们的要求。使用该原理就可以避免项集数目的指数增长,从而在合理时间内计算出频繁项集。   
这里写图片描述

1.3 使用Apriori算法来发现频繁集


1.1节提到,关联分析的目标包括两项:发现频繁项集和发现关联规则。首先需要找到频繁项集,然后才能获得关联规则。本节将只关注于发现频繁项集。
Apriori 算法是发现频繁项集的一种方法。Apriori算法的两个输人参数分别是最小支持度和数据集。该算法首先会生成所有单个物品的项集列表。接着扫描交易记录来查看哪些项集满足最小支持度要求,那些不满足最小支持度的集合会被去掉。然 后 ,对剩下来的集合进行组合以生成包含两个元素的项集。接下来,再重新扫描交易记录,去掉不满足最小支持度的项集。该过程重复进行直到所有项集都被去掉。

1)生成候选项集
在使用Python对整个程序编码之前,需要创建一些辅助函数。下面会创建一个用于构建初始集合的函数,也会创建一个通过扫描数据集以寻找交易记录子集的函数。数据集扫描的伪代码大致如下:
对数据集中的每条交易记录tran
  对每个候选项集can:
    检查一下can是否是tran的 子 集 :
    如果是,则增加can的计数值
对每个候选项集:
如果其支持度不低于最小值,则保留该项集
返回所有频繁项集列表

Apriori算法中的辅助函数,代码如下:
 



def loadDataSet():
    """
    加载数据集
    :return: 列表的列表
    """
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]

#生成所有单个物品的项集列表
def createC1(dataSet):
    """
    :param dataSet:
    :return:
    """
    C1 = []
    for transaction in dataSet:
        for item in transaction:
            if not [item] in C1:
                C1.append([item])
    C1.sort()  # 对 C1 进行降序排序
    # 将C1中的每个集合映射为frozenset,之后可以作为字典的键,笔者使用的是python3,所以将书中的map(frozenset,C1)替换为了list(map(frozenset,C1))
    return list(map(frozenset, C1))


def scanD(D, Ck, minSupport):
    ssCnt = {} # 定义一个字典
    # 对数据集中的每个样本,对候选项集中的每个集合,如果该集合为样本子集,那么对应计数+1(注意使用了字典,这个时候frozenset就起到用处了,不然会报错)
    for tid in D:
        for can in Ck:
            if can.issubset(tid):
                if not can in ssCnt :
                    ssCnt[can] = 1
                else:
                    ssCnt[can] += 1
    numItems = float(len(D))
    retList = []
    supportData = {}
    # 遍历ssCnt字典,找出满足最小支持度的候选项集,将其添加到retList中,并且将项集的支持度保存在supportData字典中,之后求关联关系的时候会用到
    for key in ssCnt:
        support = ssCnt[key] / numItems
        if support >= minSupport:
            retList.insert(0, key)
            # 书中这一行在if块外面,我认为应该放在里面,只取频繁项集的支持度,而且经过测试,确实与放在外面效果完全一样。
            supportData[key] = support
    return retList, supportData


if __name__ == '__main__':
    dataSet=loadDataSet()
    Ck=createC1(dataSet)
    retList, supportData=scanD(dataSet,Ck,0.5)
    print('%s\n%s'%(retList,supportData))

2)组织完整的Apriori算法

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



def loadDataSet():
    """
    加载数据集
    :return: 列表的列表
    """
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]

#生成所有单个物品的项集列表
def createC1(dataSet):
    """
    :param dataSet:
    :return:
    """
    C1 = []
    for transaction in dataSet:
        for item in transaction:
            if not [item] in C1:
                C1.append([item])
    C1.sort()  # 对 C1 进行降序排序
    # 将C1中的每个集合映射为frozenset,之后可以作为字典的键,笔者使用的是python3,所以将书中的map(frozenset,C1)替换为了list(map(frozenset,C1))
    return list(map(frozenset, C1))


def scanD(D, Ck, minSupport):
    ssCnt = {} # 定义一个字典
    # 对数据集中的每个样本,对候选项集中的每个集合,如果该集合为样本子集,那么对应计数+1(注意使用了字典,这个时候frozenset就起到用处了,不然会报错)
    for tid in D:
        for can in Ck:
            if can.issubset(tid):
                if not can in ssCnt :
                    ssCnt[can] = 1
                else:
                    ssCnt[can] += 1
    numItems = float(len(D))
    retList = []
    supportData = {}
    # 遍历ssCnt字典,找出满足最小支持度的候选项集,将其添加到retList中,并且将项集的支持度保存在supportData字典中,之后求关联关系的时候会用到
    for key in ssCnt:
        support = ssCnt[key] / numItems
        if support >= minSupport:
            retList.insert(0, key)
            # 书中这一行在if块外面,我认为应该放在里面,只取频繁项集的支持度,而且经过测试,确实与放在外面效果完全一样。
            supportData[key] = support
    return retList, supportData


# 创造k个项组成的候选项集列表的函数
# creates Ck
def aprioriGen(Lk, k):
    retList = []
    lenLk = len(Lk)
    for i in range(lenLk):
        for j in range(i + 1, lenLk):
            # 如果这两个集合的前面k-2个元素都相等,那么就将这两个集合合成一个大小为k的集合
            L1 = list(Lk[i])[:k - 2];
            L2 = list(Lk[j])[:k - 2]
            L1.sort();
            L2.sort()
            if L1 == L2:  # if first k-2 elements are equal
                # set union
                retList.append(Lk[i] | Lk[j])
    return retList


def apriori(dataSet, minSupport=0.5):
    # 生成所有单个物品的项集列表
    C1 = createC1(dataSet)
    # 将数据集映射为set的集合,笔者使用的是python3,所以与书中代码略有不同
    D = list(map(set, dataSet))
    # 调用scanD,找出支持度>=C1的项集
    L1, supportData = scanD(D, C1, minSupport)
    # L存储所有频繁项集
    L = [L1]
    # K为用aprioriGen创建的项集的项数
    k = 2
    # 迭代L,直到L[k-1]为空
    while (len(L[k - 2]) > 0):
        Ck = aprioriGen(L[k - 2], k)
        Lk, supK = scanD(D, Ck, minSupport)  # scan DB to get Lk
        supportData.update(supK)
        L.append(Lk)
        k += 1
    return L, supportData

if __name__ == '__main__':
    dataSet=loadDataSet()
    Ck=createC1(dataSet)
    retList, supportData=scanD(dataSet,Ck,0.5)
    # print('%s\n%s'%(retList,supportData))
    L, supportData=apriori(dataSet)
    print('%s\n%s'%(L,supportData))

结果显示:

{frozenset({1}): 0.5, frozenset({3}): 0.75, 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({2, 3, 5}): 0.5}

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

      要找到关联规则,我们首先从一个频繁项集开始。我们知道集合中的元素是不重复的,但我们想知道基于这些元素能否获得其他内容。某个元素或者某个元素集合可能会推导出另一个元素。从杂货店的例子可以得到,如果有一个频繁项集{豆奶,莴苣 },那么就可能有一条关联规则”豆奶 — 莴苣”。这意味着如果有人购买了豆奶,那么在统计上他会购买莴苣的概率较大。但是,这一条反过来并不总是成立。也就是说,即 使 “豆 奶 — 莴苣”统计上显著,那 么 “莴苣 —豆奶”也不一定成立。(从逻辑研究上来讲,箭头左边的集合称作前件,箭头右边的集合称为后件。)

      1.3节给出了频繁项集的量化定义,即它满足最小支持度要求。对于关联规则,我们也有类似的量化方法,这种量化指标称为可信度。一条规则P->H的可信度定义为support(P|H)/support(P),记住,在Python中,操作符丨表示集合的并操作,而数学上集合并的符号是U。P | H是指所有出现在集合P或者集合H 中的元素。

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

                      这里写图片描述

类似于上一节的频繁项集生成,我们可以为每个频繁项集产生许多关联规则。如果能够减少规则数目来确保问题的可解性,那么计算起来就会好很多。可以观察到,如果某条规则并不满足最小可信度要求,那么该规则的所有子集也不会满足最小可信度要求。以图11-4为例,假设规则{0,1,2}->{3}并不满足最小可信度要求,那么就知道任何左部为{0,1,2}子集的规则也不会满足最小可信度要求。在图11-4中这些规则上都加了阴影来表示。(其实用后件来说比较好理解,如果某条规则并不满足最小可信度要求,那么以该规则的后件的所有超集为后件的规则也不会满足最小可信度,而这也导致了求关联规则跟之前求频繁项集的方法类似。p(频繁项集)不变,以该规则的后件的所有超集为后件的规则的前件为该规则的子集,因此p(前件)增大,分子不变,分母增大)

可以利用关联规则的上述性质属性来减少需要测试的规则数目。类似于程序清单11-2中的Apriori算法,可以首先从一个频繁项集开始,接着创建一个规则列表,其中规则右部只包含一个元素,然后对这些规则进行测试。接下来合并所有剩余规则来创建一个新的规则列表,其中规则右部包含两个元素。这种方法也被称作分级法。

关联规则生成函数,代码如下;



def loadDataSet():
    """
    加载数据集
    :return: 列表的列表
    """
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]

#生成所有单个物品的项集列表
def createC1(dataSet):
    """
    :param dataSet:
    :return:
    """
    C1 = []
    for transaction in dataSet:
        for item in transaction:
            if not [item] in C1:
                C1.append([item])
    C1.sort()  # 对 C1 进行降序排序
    # 将C1中的每个集合映射为frozenset,之后可以作为字典的键,笔者使用的是python3,所以将书中的map(frozenset,C1)替换为了list(map(frozenset,C1))
    return list(map(frozenset, C1))


def scanD(D, Ck, minSupport):
    ssCnt = {} # 定义一个字典
    # 对数据集中的每个样本,对候选项集中的每个集合,如果该集合为样本子集,那么对应计数+1(注意使用了字典,这个时候frozenset就起到用处了,不然会报错)
    for tid in D:
        for can in Ck:
            if can.issubset(tid):
                if not can in ssCnt :
                    ssCnt[can] = 1
                else:
                    ssCnt[can] += 1
    numItems = float(len(D))
    retList = []
    supportData = {}
    # 遍历ssCnt字典,找出满足最小支持度的候选项集,将其添加到retList中,并且将项集的支持度保存在supportData字典中,之后求关联关系的时候会用到
    for key in ssCnt:
        support = ssCnt[key] / numItems
        if support >= minSupport:
            retList.insert(0, key)
            # 书中这一行在if块外面,我认为应该放在里面,只取频繁项集的支持度,而且经过测试,确实与放在外面效果完全一样。
            supportData[key] = support
    return retList, supportData


# 创造k个项组成的候选项集列表的函数
# creates Ck
def aprioriGen(Lk, k):
    retList = []
    lenLk = len(Lk)
    for i in range(lenLk):
        for j in range(i + 1, lenLk):
            # 如果这两个集合的前面k-2个元素都相等,那么就将这两个集合合成一个大小为k的集合
            L1 = list(Lk[i])[:k - 2];
            L2 = list(Lk[j])[:k - 2]
            L1.sort();
            L2.sort()
            if L1 == L2:  # if first k-2 elements are equal
                # set union
                retList.append(Lk[i] | Lk[j])
    return retList


def apriori(dataSet, minSupport=0.5):
    # 生成所有单个物品的项集列表
    C1 = createC1(dataSet)
    # 将数据集映射为set的集合,笔者使用的是python3,所以与书中代码略有不同
    D = list(map(set, dataSet))
    # 调用scanD,找出支持度>=C1的项集
    L1, supportData = scanD(D, C1, minSupport)
    # L存储所有频繁项集
    L = [L1]
    # K为用aprioriGen创建的项集的项数
    k = 2
    # 迭代L,直到L[k-1]为空
    while (len(L[k - 2]) > 0):
        Ck = aprioriGen(L[k - 2], k)
        Lk, supK = scanD(D, Ck, minSupport)  # scan DB to get Lk
        supportData.update(supK)
        L.append(Lk) # 存储频繁项集
        k += 1
    return L, supportData

#supportData is a dict coming from scanD
def generateRules(L, supportData, minConf=0.7):
    bigRuleList = []
    #only get the sets with two or more items
    #注意,i从1开始,表示只取项数大于等于2的项集
    for i in range(1, len(L)):
        for freqSet in L[i]:
            #对每个频繁项集集合的频繁项集,生成单项集合,注意使用了frozenset,因为之后要用[item]作为key获取支持度
            H1 = [frozenset([item]) for item in freqSet]
            #项数多于2,调用rulesFromConseq
            if (i > 1):
                rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf)
            #项数等于2,调用calcConf
            else:
                calcConf(freqSet, H1, supportData, bigRuleList, minConf)
    return bigRuleList

def calcConf(freqSet, H, supportData, brl, minConf=0.7):
    #create new list to return
    #存储满足最小可信度的规则的后件集合,以便之后再次合并时使用
    prunedH = []
    #对候选项集H进行迭代,选择出符合最小可信度的关联规则
    for conseq in H:
        #因为关联规则每次都是对一个项集而言,因此直接用项集与后件做差,就可以得出前件
        conf = supportData[freqSet]/supportData[freqSet-conseq] #calc confidence
        print(freqSet - conseq,freqSet,supportData[freqSet-conseq],supportData[freqSet])
        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):
    #获取候选项集项数
    m = len(H[0])
    #try further merging
    if (len(freqSet) > (m + 1)):
        #create Hm+1 new candidates
        #百思不得起解,如果直接合并,那么对于项数大于等于3的项集的后件只能大于等于2,而没有1的情况,可能是由于项数为2时已经可以得出所有项数为1的后件??
        Hmp1 = aprioriGen(H, m+1)
        #得出满足最小可信度的候选关联规则
        Hmp1 = calcConf(freqSet, Hmp1, supportData, brl, minConf)
        if (len(Hmp1) > 1):    #need at least two sets to merge
        #如果满足最小可信度的候选关联规则数目大于1,那么递归,将项数+1,继续进行过滤,直到候选关联规则数目小于等于1或者freqSet数目<=m+1,例如{1,2,3}不能以{1,2,3}为后件
            rulesFromConseq(freqSet, Hmp1, supportData, brl, minConf)


if __name__ == '__main__':
    dataSet=loadDataSet()
    # 生成只有一个元素的集合
    Ck=createC1(dataSet)
    # 根据给定的支持度获取只有一个频繁项集
    retList, supportData=scanD(dataSet,Ck,0.5)
    # 获取最小支持度是0.5 多个元素频繁项集的集合
    L, supportData=apriori(dataSet)
    print('%s'%(supportData))
    generateRules(L,supportData,minConf=0.7)

结果显示:

调整可信度阈值为0.5,截图如下:

发布了110 篇原创文章 · 获赞 22 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/yangshaojun1992/article/details/105018781