Bootstrap

Apriori算法及超市数据集挖掘实战

完整实现代码在文末哦 !

原理

定义

◆ 事务T是项i的集合: T = { i a , i b , … , i t } T=\{i_a, i_b,…,i_t\} T={ia,ib,,it}
◆ 如果I是所有项的集合,则T是I的子集。
◆ 数据集D是T的集合。
◆ 项集:项的集合。
◆ k项集:k个项的集合。

支持度:描述发生频次
置信度:衡量规则强度

关联规则支持度:
𝑆 𝑢 𝑝 𝑝 𝑜 𝑟 𝑡 𝑋 → 𝑌 = ( 𝑋 ∪ 𝑌 ) 𝑛 𝑆𝑢𝑝𝑝𝑜𝑟𝑡 𝑋 → 𝑌 = \frac{(𝑋 ∪ 𝑌)}{ 𝑛} SupportXY=n(XY)

◆ 关联规则置信度:
𝐶 𝑜 𝑛 𝑓 𝑖 𝑑 𝑒 𝑛 𝑐 𝑒 𝑋 → 𝑌 = ( 𝑋 ∪ 𝑌 ) ( 𝑋 ) 𝐶𝑜𝑛𝑓𝑖𝑑𝑒𝑛𝑐𝑒 𝑋 → 𝑌 = \frac{ (𝑋 ∪ 𝑌)} {(𝑋)} ConfidenceXY=(X)(XY)
𝐶 𝑜 𝑛 𝑓 𝑖 𝑑 𝑒 𝑛 𝑐 𝑒 𝑋 → 𝑌 = 𝑆 𝑢 𝑝 𝑝 𝑜 𝑟 𝑡 ( 𝑋 ∪ 𝑌 ) 𝑆 𝑢 𝑝 𝑝 𝑜 𝑟 𝑡 ( 𝑋 ) 𝐶𝑜𝑛𝑓𝑖𝑑𝑒𝑛𝑐𝑒 𝑋 → 𝑌 = \frac{𝑆𝑢𝑝𝑝𝑜𝑟𝑡(𝑋 ∪ 𝑌)}{𝑆𝑢𝑝𝑝𝑜𝑟𝑡(𝑋)} ConfidenceXY=Support(X)Support(XY)

类似于条件概率(在Y出现的情况下X发生的概率) P ( X ∣ Y ) = P ( X Y ) P ( Y ) P(X|Y) = \frac{P(XY)}{P(Y)} P(XY)=P(Y)P(XY)

课上举的栗子:

在这里插入图片描述
Bread -> Milk :
s u p p o r t = 所 有 项 集 中 出 现 的 该 项 的 次 数 项 集 总 个 数 support = \frac{所有项集中出现的该项的次数}{项集总个数} support=
support:面包和牛奶一起出现的次数 = 2 ; 总项集数 = 8 ;
c o n f i d e n c e = X 和 Y 一 起 出 现 的 次 数 所 有 项 集 中 出 现 的 X 的 次 数 confidence = \frac{X和Y一起出现的次数}{所有项集中出现的X的次数} confidence=XXY
confidence:有面包出现的项集数 = 6 ;面包牛奶一起出现的次数为2

Milk -> Bread 同理可得


频繁项集:支持度大于 σ \sigma σ(最小支持度的阈值)的项集
强规则:是频繁项集且置信度大于Φ(最小置信度)

Task:给定I,D,σ和Φ,挖掘所有强规则的规则

Apriori算法过程:

  1. 创建并找出所有的频繁项集
    • 从1到k挨个生成
    • 支持度大于σ认可
  2. 创建并找出关联规则
    • 所有可能的关联规则
    • 关联规则的置信度大于Φ -> 认可

一个项集的子项集数: M = 2 m − 1 M = 2^m-1 M=2m1

总项级数: I = ∑ i N ( m ∗ M ) I=\sum{_i^N}(m*M) I=iN(mM)

m : 项集数 ;N:事务数

基本思想:

  • 任何非频繁项集的超集是不频繁的
  • 频繁项集的所有非空子集一定是频繁的

咋一看这两条基本思想没鸟用,但是真的很有用

随着物品的增加,计算的次数呈指数的形式增长 …如果减少计数次数?
靠这两条规则

  • 假设现在数据集={0,1,2,3}
  • 如果 {0, 1} 是频繁的,那么 {0}, {1} 也是频繁的
  • {2,3} 是 非频繁项集,那么利用上面的知识,我们就可以知道 {0,2,3} {1,2,3} {0,1,2,3} 都是 非频繁的。 也就是说,计算出 {2,3} 的支持度,知道它是 非频繁 的之后,就不需要再计算 {0,2,3} {1,2,3} {0,1,2,3} 的支持度
    在这里插入图片描述

流程图:

在这里插入图片描述


代码实现

核心代码:

def returnItemsWithMinSupport(itemSet, transactionList, minSupport, freqSet):
    """
    计算子项集的支持度,返回频繁项集
    """
    _itemSet = set()
    localSet = defaultdict(int)

    for item in itemSet:
        for transaction in transactionList:
            if item.issubset(transaction):
                freqSet[item] += 1
                localSet[item] += 1

    for item, count in localSet.items():
        support = float(count) / len(transactionList)

        if support >= minSupport:
            _itemSet.add(item)

    return _itemSet


def getItemSetTransactionList(data_iterator):
	"""
	从候选集中获取所有子项集
	"""
    transactionList = list()
    itemSet = set()
    for record in data_iterator:
        transaction = frozenset(record)
        transactionList.append(transaction)
        for item in transaction:
            itemSet.add(frozenset([item]))  
    return itemSet, transactionList

参数设置:最小支持度:0.15 最小置信度: 0.6

  • 城市名字数据集结果如下:
    在这里插入图片描述
    换一个超市销售数据集:

频繁项集:
在这里插入图片描述

  • 关联关系挖掘结果

在这里插入图片描述

挺好玩的,代码不是很难,但是网上的代码都年久失修,我调了很久,更新一些地方的写法(很暴力的循环,可能增加了复杂度和开销,太菜了能跑起来就是万幸了呜呜呜~)

别忘辽~ 记得给个赞!


优化策略

  1. 将置信度换成提升度 提升度: l i f t ( A − > B ) = c o n f i d e n c e ( A − > B ) p ( B ) = s u p p o r t ( B A ) s u p p o r t ( A ) ∗ P ( B ) lift(A ->B) = \frac{confidence(A->B)}{p(B)}= \frac{support(BA)}{support(A)*P(B)} lift(A>B)=p(B)confidence(A>B)=support(A)P(B)support(BA)
    但是提升度会收到零事务的影响
  2. 不用提升度,不受零事务影响但是会受到不平衡数据的影响
  3. 使用哈希树来分组计数
    * 减少支持度的计算开销
    * 项集和计数同时都在叶子节点

事务:{1,2,3,5,6}

项集:在这里插入图片描述
在Apriori算法中,当查看一个候选集是否是频繁项集,需要将该候选集与DB中的每个事务进行比较,如果该候选集在这个事务中出现了,就将其支持度加1。当DB中有5个事务,而候选项集为3个的时候,其总的比较次数就是3×5=15次

为了减少比较的次数,通过以Hash树的结构来存储候选集,每一个事务不再和每个候选集进行比较,而是和Hash树中特定的候选集进行比较


构建哈希树:

  1. 设置最大叶子节点数,大于这个阈值就要分裂,这里我们的Max_leaf_size选择为3。
  2. 通过取模运算获得哈希位置
  3. 分成3个数,最大哈希3次

举例说明:

Step1: 遍历候选集(也就是上面的15个项集)

对于项集{1,4,5}来说,对第一项 1 来说根据hash函数,应该放在左边
在这里插入图片描述

对于项集{1,2,4}来说,其第一项为1,也放在左边。

在这里插入图片描述

  • 也就是说,第一项取模为1的,都放在根节点的左边

对于项集{4,5,7}来说,第一项为4,放在左边,但这时因为左边有3个候选集,需要进行分裂,这时我们根据候选集的第二项进行hash
在这里插入图片描述
注意:如果发现一个叶子节点的数目>=3,如果可以hash进行分类就分裂,如果三个数hash结果都一样,那就放在一起以线性表的结构存起来

  • 其余节点依次类推…

Step 2 :

求一个事务可能的子项集,以事务{1,2,3,5,6}为例,求该事务可能存在的三项集。可能的三项集数目应该是 C 5 3 C_5^3 C53共10个。

  • 这一步其实就是对可能的三项集提前进行分桶(为了后面哈希树的快速查找打好基础)
    在这里插入图片描述
  • 这样以来,10个可能的三项集变成6个桶了,计算支持度的时候直接去对应hash桶里找,然后比较

使用Hash树进行支持度计数

构建的hash树就长这样
在这里插入图片描述

分桶查找

待支持度计数的项集
在这里插入图片描述


  • 对于事务{1,2,3,5,6},首项为1的项集,应该投递在左边,而首项为2的投递在中间,首项为3的投递在右边
  1. 对于前两项为1,2的其可能的项集为{1,2,3},{1,2,5},{1,2,6}
  • {1,2,3},{1,2,6}应该投递在右,与{1,5,9}叶子节点中数据进行比较,没有相同的,支持度为0
  • {1,2,5}应该投递在中间,与{1,2,5},{4,5,8}结点进行比较,存在项集{1,2,5},因此{1,2,5}的支持度计数加1
  1. 对于项集前两项为1,3的前提下,因为只有一个叶子节点,因此不需要再对第三项进行投递,因此直接和{1,3,6}叶子节点进行比较,{1,3,6}有相同的,{1,3,6}支持度+1

  2. 对于项集前两项为1,5的前提下,只有一个叶子节点{1,5,6},不需要投递,直接和{1,5,6}比较,不相同,支持度为0

  3. 对于项集前两项为{2,3},{2,5}的前提下,只有两个节点{2,3,4}{5,6,7},没有相同的,支持度为0

  4. 对于项集前两项为3,5的前提下,有三个节点{3,5,6},{3,5,7},{6,8,9},有一个相同的,{3,5,6}支持度+1

  • 红框内就是需要比较的项集,红框内一共9个项集,也就是算支持度的时候只有比较9次,而不是逐一遍历的15次

在这里插入图片描述

所以,最后得到的频繁项集为{1,2,5},{3,5,6},{1,3,6}

完整超市购物数据挖掘

算法参数:

置信度Confidence = 0.4 支持度Support = 0.20

  • 频繁项集支持度计算:
    在这里插入图片描述
    注意到Lassi了么!!!, 我反正没见过,这是美国的超市数据集

在这里插入图片描述

  • wiki以下Lassi


    在这里插入图片描述
    在这里插入图片描述
    查阅Lassi的资料和组成成分发现,,Lassi与Milk、Sweet的组合出现不是偶然,是必然!

挖掘结果

  • Milk 单品销售最频繁,单品销售冠军;Milk的销售组合中,与Sugar、Lassi的组合销售占最高,可以通过Milk来间接增加Sugar与Lassi的销售量
  • Sweet与Lassi的组合在各频繁项集中support最高,且单品support也很高,说明在购物数据集中以上集中商品,Sweet+Lassi可以出现在各个销售组合中,是组合销售冠军,可以通过Sweet和Lassi的与其他商品捆绑做打折促销,能最大程度的将卖不出去的其他商品清仓,以获取最高的利润

完整实现代码

用命令行来启动:python apriori.py 数据集 -c 置信度 -s 支持度



import sys

from itertools import chain, combinations
from collections import defaultdict
from optparse import OptionParser


def subsets(arr):

    return chain(*[combinations(arr, i + 1) for i, a in enumerate(arr)])


def returnItemsWithMinSupport(itemSet, transactionList, minSupport, freqSet):

    _itemSet = set()
    localSet = defaultdict(int)

    for item in itemSet:
        for transaction in transactionList:
            if item.issubset(transaction):
                freqSet[item] += 1
                localSet[item] += 1

    for item, count in localSet.items():
        support = float(count) / len(transactionList)

        if support >= minSupport:
            _itemSet.add(item)

    return _itemSet


def joinSet(itemSet, length):
    """Join a set with itself and returns the n-element itemsets"""
    return set(
        [i.union(j) for i in itemSet for j in itemSet if len(i.union(j)) == length]
    )


def getItemSetTransactionList(data_iterator):
    transactionList = list()
    itemSet = set()
    for record in data_iterator:
        transaction = frozenset(record)
        transactionList.append(transaction)
        for item in transaction:
            itemSet.add(frozenset([item]))  # Generate 1-itemSets
    return itemSet, transactionList


def runApriori(data_iter, minSupport, minConfidence):

    itemSet, transactionList = getItemSetTransactionList(data_iter)

    freqSet = defaultdict(int)
    largeSet = dict()


    assocRules = dict()


    oneCSet = returnItemsWithMinSupport(itemSet, transactionList, minSupport, freqSet)

    currentLSet = oneCSet
    k = 2
    while currentLSet != set([]):
        largeSet[k - 1] = currentLSet
        currentLSet = joinSet(currentLSet, k)
        currentCSet = returnItemsWithMinSupport(
            currentLSet, transactionList, minSupport, freqSet
        )
        currentLSet = currentCSet
        k = k + 1

    def getSupport(item):
        """local function which Returns the support of an item"""
        return float(freqSet[item]) / len(transactionList)

    toRetItems = []
    for key, value in largeSet.items():
        toRetItems.extend([(tuple(item), getSupport(item)) for item in value])

    toRetRules = []
    for key, value in list(largeSet.items())[1:]:
        for item in value:
            _subsets = map(frozenset, [x for x in subsets(item)])
            for element in _subsets:
                remain = item.difference(element)
                if len(remain) > 0:
                    confidence = getSupport(item) / getSupport(element)
                    if confidence >= minConfidence:
                        toRetRules.append(((tuple(element), tuple(remain)), confidence))
    return toRetItems, toRetRules


def printResults(items, rules):

    for item, support in sorted(items, key=lambda x: x[1]):
        print("item: %s , %.3f" % (str(item), support))
    print("\n------------------------ RULES:")
    for rule, confidence in sorted(rules, key=lambda x: x[1]):
        pre, post = rule
        print("Rule: %s ==> %s , %.3f" % (str(pre), str(post), confidence))


def to_str_results(items, rules):

    i, r = [], []
    for item, support in sorted(items, key=lambda x: x[1]):
        x = "item: %s , %.3f" % (str(item), support)
        i.append(x)

    for rule, confidence in sorted(rules, key=lambda x: x[1]):
        pre, post = rule
        x = "Rule: %s ==> %s , %.3f" % (str(pre), str(post), confidence)
        r.append(x)

    return i, r


def dataFromFile(fname):

    with open(fname, "rU") as file_iter:
        for line in file_iter:
            line = line.strip().rstrip(",")  # Remove trailing comma
            record = frozenset(line.split(","))
            yield record


if __name__ == "__main__":

    optparser = OptionParser()
    optparser.add_option(
        "-f", "--inputFile", dest="input", help="filename containing csv", default=None
    )
    optparser.add_option(
        "-s",
        "--minSupport",
        dest="minS",
        help="minimum support value",
        default=0.15,
        type="float",
    )
    optparser.add_option(
        "-c",
        "--minConfidence",
        dest="minC",
        help="minimum confidence value",
        default=0.6,
        type="float",
    )

    (options, args) = optparser.parse_args()

    inFile = None
    if options.input is None:
        inFile = sys.stdin
    elif options.input is not None:
        inFile = dataFromFile(options.input)
    else:
        print("No dataset filename specified, system with exit\n")
        sys.exit("System will exit")

    minSupport = options.minS
    minConfidence = options.minC

    items, rules = runApriori(inFile, minSupport, minConfidence)

    printResults(items, rules)

;