详解十大经典数据挖掘算法之——Apriori

本文始发于我的公众号:TechFlow,原创不易,求个关注web


今天是机器学习专题的第19篇文章,咱们来看经典的Apriori算法。算法

Apriori算法号称是十大数据挖掘算法之一,在大数据时代威风无两,哪怕是没有据说过这个算法的人,对于那个著名的啤酒与尿布的故事也耳熟能详。但遗憾的是,随着时代的演进,大数据这个概念很快被机器学习、深度学习以及人工智能取代。即便是拉拢投资人的创业者也不多会讲到这个故事了,虽然时代的变迁使人唏嘘,可是这并不妨碍它是一个优秀的算法。安全

咱们来简单回顾一下这个故事,听说在美国的沃尔玛超市当中,啤酒和尿布常常被摆放在同一个货架当中。若是你仔细观察就会以为很奇怪,啤酒和尿布不管是从应用场景仍是商品自己的属性来分都不该该被放在一块儿,为何超市要这么摆放呢?数据结构

看似不合理的现象背后每每都有更深层次的缘由,听说是沃尔玛引进了一种全新的算法,它分析了全部顾客在超市消费的记录,而后计算商品之间的关联性,发现这两件商品的关联性很是高。也就是说有大量的顾客会同时购买啤酒和尿布这两种商品,因此通过数据的分析,沃尔玛下令将这两个商品放在同一个货架上进行销售。果真这么一搞以后,两种商品的销量都提高了app

这个在背后分析数据,出谋划策充当军师同样决策的算法就是Apriori算法。机器学习

关联分析与条件几率

咱们先把这个故事的真假放在一边,先来分析一下故事背后折射出来的信息。咱们把问题进行抽象,沃尔玛超市当中的商品种类大概有数万种,咱们的算法作的实际上是根据售卖数据计算分析这些种类之间的相关性。专业术语叫作关联分析,这个从字面上很好理解。但从关联分析这个角度出发,咱们会有些不同的洞见。编辑器

咱们以前都学过条件几率,咱们是否是能够用条件几率来反应两个物品之间的关联性呢?好比,咱们用A表示一种商品,B表示另一种商品,咱们彻底能够根据全部订单的状况计算出P(A|B)和P(B|A),也就是说用户在购买了A的状况下会购买B的几率以及在购买B的状况下会购买A的几率。这样作看起来也很科学啊,为何不这样作呢,还要引入什么新的算法呢?ide

这也就是算法必要性问题,这个问题解决不了,咱们好像会很难说服本身去学习一门新的算法。其实回答这个问题很简单,就是成本。大型超市当中的商品通常都有几万种,而这几万种商品的销量差别巨大。一些热门商品好比水果、蔬菜的销量多是冷门商品,好比冰箱、洗衣机的上千倍甚至是上万倍。若是咱们要计算两两商品之间的相关性显然是一个巨大的开销,由于对于每两个商品的组合,咱们都须要遍历一遍整个数据集,拿到商品之间共同销售的记录,从而计算条件几率。学习

咱们假设商品的种类数是一万,超市的订单量也是一万好了,那么两两商品之间的组合就有一亿条,若是再乘上每次计算须要遍历一次整个数据集,那么整个运算的复杂度大概会是一万亿。若是再考虑多个商品的组合,那这个数字更加可怕。测试

但实际上一个大型超市订单量确定不是万级别的,至少也是十万或者是百万量级甚至更多。因此这个计算的复杂度是很是庞大的,若是考虑计算带来的开销,这个问题在商业上就是不可解的。由于即便算出来结果带来的收益也远远没法负担付出的计算代价,这个计算代价可能比不少人想得大得多,即便是使用现成的云计算服务,也会带来极为昂贵的开销。若是考虑数据安全,不能使用其余公司的计算服务的话,那么本身维护这些数据和人工带来的消耗也是常人不可思议的。

若是想要得出切实可行的结果,那么优化算法必定是必须的,不然可能没有一家超市愿意付出这样的代价。

在咱们介绍Apriori算法以前,咱们能够先试着本身思考一下这个问题的解法。我真的试着想过,可是我没有获得很好的答案,对比一下Apriori算法我才发现,这并不是是我我的的问题,而是由于咱们的思惟有误区。

若是你作过LeetCode,学过算法导论和数据结构,那么你在思考问题的时候,每每会不由自主地从最优解以及最佳解的方向出发。反应在这个问题当中,你可能会倾向于找到全部高关联商品,或者是计算出全部商品对之间的关联性。可是在这个问题当中,前提可能就是错的。由于答案的完备性和复杂度之间每每是挂钩的,找出全部的答案必然会带来更多的开销,并且落实在实际当中,牺牲一些完备性也是有道理的。由于对于超市而言,更加关注高销量的商品,好比电冰箱和洗衣机,即便得出结论它们和某些商品关联性很高对超市来讲也没有太大意义,由于电冰箱和洗衣机一天总共也卖不出多少台。

你仔细思考就会发现这个问题和算法的背景比咱们一开始想的和理解的要深入得多,因此让咱们带着一点点敬畏之心来看看这个算法的详细吧。

频繁项集与关联规则

在咱们具体了解算法的原理以前,咱们先来熟悉两个术语。第一个属于叫作频繁项集,英文是frequent item sets。这个翻译很接地气,咱们直接看字面意思应该就能理解。意思是常常会出如今一块儿的物品的集合。第二个属于叫作关联规则,也就是两个物品之间可能存在很强的关联关系。

用啤酒和尿布的故事举个例子,好比不少人常常一块儿购买啤酒和尿布,那么啤酒和尿布就常常出如今人们的购物单当中。因此啤酒和尿布就属于同一个频繁项集,而一我的买了啤酒颇有可能还会购买尿布,啤酒和尿布之间就存在一个关联规则。表示它们之间存在很强的内在联系。

有了频繁项集和关联规则咱们会作什么事情?很简单会去计算它们的几率

对于一个集合而言,咱们要考虑的是整个集合出现的几率。在这个问题场景当中,它的计算很是简单。即用集合当中全部元素一块儿出现的次数,除以全部的数据条数。这个几率也有一个术语,叫作支持度,英文称做support。

对于一个关联规则而言,它指的是A物品和B物品之间的内在关系,其实也就是条件几率。因此A->B关联规则的几率就是P(AB)/P(A)和条件几率的公式同样,不过在这个问题场景当中,也有一个术语,叫作置信度,英文是confidence。

其实confidence也好,support也罢,咱们均可以简单地理解成出现的几率。这是一个计算几率的模型,能够认为是条件几率运算的优化。其中关联规则是基于频繁项集的,因此咱们能够先把关联规则先放一放,先来主要看频繁项集的求解过程。既然频繁项集的支持度本质上也是一个几率,那么咱们就可使用一个阈值来进行限制了。好比咱们规定一个阈值是0.5,那么凡是支持度小于0.5的集合就不用考虑了。咱们先用这个支持度过一遍全体数据,找出知足支持度限制的单个元素的集合。以后当咱们寻找两个元素的频繁项集的时候,它的候选集就再也不是全体商品了,而只有那些包含单个元素的频繁项集。

同理,若是咱们要寻找三项的频繁项集,它的候选集就是含有两项元素的频繁项集,以此类推。表面上看,咱们是把候选的范围限制在了频繁项集内从而简化了运算。其实它背后有一个很深入的逻辑,即不是频繁项集的集合,必定不可能构成其余的频繁项集。好比电冰箱天天的销量很低,它和任何商品都不可能构成频繁项集。这样咱们就能够排除掉全部那些不是频繁项集的全部状况,大大减小了运算量。

上图当中的23不是频繁项集,那么对应的123和023显然也都不是频繁项集。其实咱们把这些非频繁的项集去掉,剩下的就是频繁项集。因此咱们从正面或者是反面理解均可以,逻辑的内核是同样的。

Apriori算法及实现

其实Apriori的算法精髓就在上面的表述当中,也就是根据频繁项集寻找新的频繁项集。咱们整理一下整个算法的流程,而后一点点用代码来实现它,对照代码和流程很容易就搞清楚了。

首先,咱们来建立一批假的数据用来测试:

def create_dataset():
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]

下面咱们要生成只有一个项的全部集合。这一步很好理解,咱们须要对全部有交易的商品生成一个清单,也就是将全部交易记录中的商品购买记录进行去重。因为咱们生成的结果在后序会做为dict的key,而且咱们知道set也是可变对象,也是不能够做为dict中的key的。因此咱们要作一点转换,将它转换成frozenset,它能够认为是不能够修改的set。

def individual_components(dataset):
    ret = []
    for data in dataset:
        for i in data:
            ret.append((i))
    # 将list转化成set便是去重操做
    ret = set(ret)
    return [frozenset((i, )) for i in ret]

执行事后,咱们会获得这样一个序列:

[frozenset({1}), frozenset({2}), frozenset({3}), frozenset({4}), frozenset({5})]

上面的这个序列是长度为1的全部集合,咱们称它为C1,这里的C就是component,也就是集合的意思。下面咱们要生成的f1,也就是长度为1的频繁集合。频繁集合的选取是根据最小支持度过滤的,因此咱们下面要实现的就是计算Ck中每个集合的支持度,而后过滤掉那些支持度不知足要求的集合。这个应该也很好理解:

def filter_components_with_min_support(dataset, components, min_support): # 咱们将数据集中的每一条转化成set # 一方面是为了去重,另外一方面是为了进行集合操做 dataset = list(map(set, dataset)) # 记录每个集合在数据集中的出现次数 components_dict = defaultdict(int) for data in dataset: for i in components: # 若是集合是data的子集 # 也就是data包含这个集合 if i <= data: components_dict[i] += 1
rows = len(dataset)
frequent_components = []
supports = {}
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> k,v <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> components_dict.items():
    <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 支持度就是集合在数据集中的出现次数除以数据总数</span>
    support = v / rows
    <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 保留知足支持度要求的数据</span>
    <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> support &gt;= min_support:
        frequent_components.append(k)
    supports[k] = support
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> frequent_components, supports
rows = len(dataset) frequent_components = [] supports = {} <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> k,v <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> components_dict.items(): <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 支持度就是集合在数据集中的出现次数除以数据总数</span> support = v / rows <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 保留知足支持度要求的数据</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> support &gt;= min_support: frequent_components.append(k) supports[k] = support <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> frequent_components, supports

咱们将支持度设置成0.5来执行一下,会获得如下结果:

能够发现数据中的4被过滤了,由于它只出现了1次,支持度是0.25,达不到咱们设置的阈值,和咱们的预期一致。

如今咱们有了方法建立长度为1的项集,也有了方法根据支持度过滤非频繁的项集,接下来要作的已经很明显了,咱们要根据长度为1的频繁项集生成长度为2的候选集,而后再利用上面的方法过滤获得长度为2的频繁项集,再经过长度为2的频繁项集生成长度为3的候选集,如此往复,直到全部的频繁项集都被挖掘出来为止。

根据这个思路,咱们接下来还有两个方法要作,一个是根据长度为n的频繁项集生成长度n+1候选集的方法,另外一个方法是利用这些方法挖掘全部频繁项集的方法。

咱们先来看根据长度为n的项集生成n+1候选集的方法,这个也很好实现,咱们只须要用全部元素依次加入现有的集合当中便可。

def generate_next_componets(components): # 获取集合当中全部单个的元素 individuals = individual_components(components) storage = set() for i in individuals: for c in components: # 若是i已经在集合中了则跳过 if i <= c: continue cur = i | c # 不然合并,加入存储 if cur not in storage: storage.add(cur)
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> list(storage)
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> list(storage)

这些方法都有了以后,剩下的就很好办了,咱们只须要重复调用上面的方法,直到找不到更长的频繁项集为止。咱们直接来看代码:

def apriori(dataset, min_support):
    # 生成长度1的候选集合
    individuals = individual_components(dataset)
    # 生成长度为1的频繁项集
    f1, support_dict = filter_components_with_min_support(dataset, individuals, min_support)
    frequent = [f1]
    while True:
        # 生成长度+1的候选集合
        next_components = generate_next_componets(frequent[-1])
        # 根据支持度筛选出频繁项集
        components, new_dict = filter_components_with_min_support(dataset, next_components, min_support)
        # 若是筛选结果为空,说明不存在更长的频繁项集了
        if len(components) == 0:
            break
        # 更新结果
        support_dict.update(new_dict)
        frequent.append(components)
    return frequent, support_dict

最后,咱们运行一下这个方法查看一下结果:

红色框中就是咱们从数据集合当中挖掘出的频繁项集了。在一些场景当中咱们除了想要知道频繁项集以外,可能还会想要知道关联规则,看看哪些商品之间存在隐形的强关联。咱们根据相似的思路能够设计出算法来实现关联规则的挖掘。

关联规则

理解了频繁项集的概念以后再来算关联规则就简单了,咱们首先来看一个很简单的变形。因为咱们须要计算频繁项集之间的置信度,也就是条件几率。咱们都知道P(A|B) = P(AB) / P(B),这个是条件几率的基本公式。这里的P(A) = 出现A的数据条数/ 总条数,其实也就是A的支持度。因此咱们能够用支持度来计算置信度,因为刚刚咱们在计算频繁项集的时候算出了全部频繁项集的支持度,因此咱们能够用这份数据来计算置信度,这样会简单不少。

咱们先来写出置信度的计算公式,它很是简单:

def calculate_confidence(comp, subset, support_dict):
    return float(support_dict[comp]) / support_dict[comp-subset]

这里的comp表示集合,subset表示咱们要推断的项。也就是咱们挖掘的是comp-item这个集合与subset集合之间的置信度。

接着咱们来看候选规则的生成方法,它和前面生成候选集合的逻辑差很少。咱们拿到频繁项集以后,扣除其中的一个子集,将它做为一个候选的规则。

def generate_rules(components, subset): all_set = [] # 生成全部子集,也就是长度更小的频繁项集的集合 for st in subset: all_set += st
rules = []
<span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 遍历全部子集</span>
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> i <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> all_set:
    <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 遍历频繁项集</span>
    <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> comp <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> components:
        <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 若是子集关系成立,则生成了一条候选规则</span>
        <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> i &lt;= comp:
            rules.append((comp, i))
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> rules
rules = [] <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 遍历全部子集</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> i <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> all_set: <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 遍历频繁项集</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">for</span> comp <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">in</span> components: <span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;"># 若是子集关系成立,则生成了一条候选规则</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> i &lt;= comp: rules.append((comp, i)) <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> rules

最后,咱们把上面两个方法串联在一块儿,先生成全部的候选规则,再根据置信度过滤掉符合条件的关联规则。利用以前频繁项集时候生成的数据,很容易实现这点。

def mine_rules(frequent, support_dict, min_confidence):
    rules = []
    # 遍历长度大于等于2的频繁项集
    for i in range(1, len(frequent)):
        # 使用长度更小的频繁项集做为本身,构建候选规则集
        candidate_rules = generate_rules(frequent[i], frequent[:i])
        # 计算置信度,根据置信度进行过滤
        for comp, item in candidate_rules:
            confidence = calculate_confidence(comp, item, support_dict)
            if confidence >= min_confidence:
                  rules.append([comp-item, item, confidence])
    return rules

咱们运行一下这个方法,看一下结果:

从结果来看还不错,咱们挖掘出了全部的关联规则。要注意一点A->B和B->A是两条不一样的规则,这并不重复。举个简单的例子,买乒乓拍的人每每都会买乒乓球,可是买乒乓球的人却并不必定会买乒乓拍,由于乒乓拍比乒乓球贵得多。并且乒乓球是消耗品,乒乓拍不是。因此乒乓拍能够关联乒乓球,但反之不必定成立。

结尾、升华

到这里,Apriori算法和它的应用场景就讲完了。这个算法的原理并不复杂,代码也不困难,没有什么高深的推导或者是晦涩的运算,可是算法背后的逻辑并不简单。怎么样为一个复杂的场景涉及简单的指标?怎么样缩小咱们计算的范围?怎么样衡量数据的价值?其实这些并非空穴来风,显然算法的设计者是付出了大量思考的。

若是咱们顺着解法出发去试着倒推当时设计者的思考过程,你会发现看似简单的问题背后其实并不简单,看似天然而然的道理,也并不天然,这些看似寻常的背后都隐藏着逻辑,这些背后的思考和逻辑,才是算法真正宝贵的部分。

今天的文章就到这里,原创不易,须要你的一个关注,你的举手之劳对我来讲很重要。

相关文章
相关标签/搜索