1. 程式人生 > >關聯分析之Apriori演算法

關聯分析之Apriori演算法

1.資料探勘與關聯分析

資料探勘是一個比較龐大的領域,它包括資料預處理(清洗去噪)、資料倉庫、分類聚類、關聯分析等。關聯分析可以算是資料探勘最貼近我們生活的一部分了,開啟卓越亞馬遜,當挑選一本《Android4高階程式設計》時,它會不失時機的列出你可能還會感興趣的書籍,比如Android遊戲開發、Cocos2d-x引擎等,讓你的購物車又豐富了些,而錢包又空了些。

關聯分析,即從一個數據集中發現項之間的隱藏關係。本篇文章Apriori演算法主要是基於頻繁集的關聯分析,所以本文中所出現的關聯分析預設都是指基於頻繁集的關聯分析。

有了一個感性的認識,我們來一段理性的形式化描述:

令項集I={i1,i2,...in}

且有一個數據集合D,它其中的每一條記錄T,都是I的子集

那麼關聯規則都是形如A->B的表示式,A、B均為I的子集,且A與B的交集為空

這條關聯規則的支援度:support = P(A並B)

這條關聯規則的置信度:confidence = support(A並B)/suport(A)

如果存在一條關聯規則,它的支援度和置信度都大於預先定義好的最小支援度與置信度,我們就稱它為強關聯規則。強關聯規則就可以用來了解項之間的隱藏關係。所以關聯分析的主要目的就是為了尋找強關聯規則,而Apriori演算法則主要用來幫助尋找強關聯規則。

注意:因為頻率=事件出現次數/總事件次數,為了方便,我們在以下都用事件出現的頻數而非頻率來作為支援度。


2.Apriori演算法描述

Apriori演算法指導我們,如果要發現強關聯規則,就必須先找到頻繁集。所謂頻繁集,即支援度大於最小支援度的項集。如何得到資料集合D中的所有頻繁集呢?

有一個非常土的辦法,就是對於資料集D,遍歷它的每一條記錄T,得到T的所有子集,然後計算每一個子集的支援度,最後的結果再與最小支援度比較。且不論這個資料集D中有多少條記錄(十萬?百萬?),就說每一條記錄T的子集個數({1,2,3}的子集有{1},{2},{3},{1,2},{2,3},{1,3},{1,2,3},即如果記錄T中含有n項,那麼它的子集個數是2^n-1)。計算量非常巨大,自然是不可取的。

所以Aprior演算法提出了一個逐層搜尋的方法,如何逐層搜尋呢?包含兩個步驟:

1.自連接獲取候選集。第一輪的候選集就是資料集D中的項,而其他輪次的候選集則是由前一輪次頻繁集自連線得到(頻繁集由候選集剪枝得到)。

2.對於候選集進行剪枝。如何剪枝呢?候選集的每一條記錄T,如果它的支援度小於最小支援度,那麼就會被剪掉;此外,如果一條記錄T,它的子集有不是頻繁集的,也會被剪掉。

演算法的終止條件是,如果自連線得到的已經不再是頻繁集,那麼取最後一次得到的頻繁集作為結果。

需要值得注意的是:

Apriori演算法為了進一步縮小需要計算支援度的候選集大小,減小計算量,所以在取得候選集時就進行了它的子集是否有非頻繁集的判斷。(參見《資料探勘:概念與技術》一書)。

另外,兩個K項集進行連線的條件是,它們至少有K-1項相同。

知道了這些可以方便我們寫出高效的程式。

3.Apriori演算法例子推導

上面的描述是不是有點抽象,例子是最能幫助理解的良方。(假設最小支援度為2,最小置信度為0.6,最小支援度和置信度都是人定的,可以根據實驗結果的優劣對這兩個引數進行調整)

假設初始的資料集D如下:

  

  那麼第一輪候選集和剪枝的結果為:

 

可以看到,第一輪時,其實就是用的資料集中的項。而因為最小支援度是2的緣故,所以沒有被剪枝的,所以得到的頻繁集就與候選集相同。

第二輪的候選集與剪枝結果為:

  

可以看到,第二輪的候選集就是第一輪的頻繁集自連線得到的(進行了去重),然後根據資料集D計算得到支援度,與最小支援度比較,過濾了一些記錄。頻繁集已經與候選集不同了。

第三輪候選集與頻繁集結果為:

 

可以看到,第三輪的候選集發生了明顯的縮小,這是為什麼呢?

請注意取候選集的兩個條件:

1.兩個K項集能夠連線的兩個條件是,它們有K-1項是相同的。所以,(I2,I4)和(I3,I5)這種是不能夠進行連線的。縮小了候選集。

2.如果一個項集是頻繁集,那麼它不存在不是子集的頻繁集。比如(I1,I2)和(I1,I4)得到(I1,I2,I4),而(I1,I2,I4)存在子集(I1,I4)不是頻繁集。縮小了候選集。

第三輪得到的2個候選集,正好支援度等於最小支援度。所以,都算入頻繁集。

這時再看第四輪的候選集與頻繁集結果:

可以看到,候選集和頻繁集居然為空了!因為通過第三輪得到的頻繁集自連線得到{I1,I2,I3,I5},它擁有子集{I2,I3,I5},而{I2,I3,I5}不是頻繁集,不滿足:頻繁集的子集也是頻繁集這一條件,所以被剪枝剪掉了。所以整個演算法終止,取最後一次計算得到的頻繁集作為最終的頻繁集結果:

也就是:['I1,I2,I3', 'I1,I2,I5']

那麼,如何得到強規則呢?

比如對於:I1,I2,I3這個頻繁集,我們可以得到它的子集:{I1},{I2},{I3},{I1,I2},{I1,I3},{I2,I3},那麼可以得到的規則如下:

I1->I3^I2 0.333333333333
I2->I1^I3 0.285714285714
I3->I1^I2 0.333333333333
I1^I2->I3 0.5
I1^I3->I2 0.5
I2^I3->I1 0.5
左邊是規則,右邊是置信度。conf(I1->I3^I2) = support(I1,I2,I3)/support(I1)

與最小置信度相比較,我們就可以得到強規則了。

所以對於頻繁集['I1,I2,I3', 'I1,I2,I5']

我們得到的規則為:

I1->I3^I2 0.333333333333
I2->I1^I3 0.285714285714
I3->I1^I2 0.333333333333
I1^I2->I3 0.5
I1^I3->I2 0.5
I2^I3->I1 0.5
I1->I2^I5 0.333333333333
I2->I1^I5 0.285714285714
I5->I1^I2 1.0
I1^I2->I5 0.5

I1^I5->I2 1.0
 I2^I5->I1 1.0

從而得到強規則為:

I5->I1^I2 : 1.0
I1^I5->I2 : 1.0
I2^I5->I1 : 1.0

意味著如果使用者買了商品I5,則極有可能買商品I1,I2;買了I1和I5,則極有可能買I2,如果買了I2,I5,則極有可能買I1。所以,你就知道在頁面上該如何推薦了。

4.Apriori演算法原始碼及輸出

為了加強對於Apriori演算法的瞭解,我用python寫了個簡單的程式(時間倉促,沒有考慮時間空間複雜度,也沒有非常嚴謹的測試正確性),希望以後能對此程式進行修改和優化。

演算法原始碼如下:

#coding:utf-8
samples = [
    ["I1","I2","I5"],
    ["I2","I4"],
    ["I2","I3"],
    ["I1","I2","I4"],
    ["I1","I3"],
    ["I2","I3"],
    ["I1","I3"],
    ["I1","I2","I3","I5"],
    ["I1","I2","I3"]
]
min_support = 2
min_confidence = 0.6
fre_list = list()
def get_c1():
    global record_list
    global record_dict
    new_dict = dict()
    for row in samples:
        for item in row:
            if item not in fre_list:
                fre_list.append(item)
                new_dict[item] = 1
            else:
                new_dict[item] = new_dict[item] + 1
    fre_list.sort()
    print "candidate set:"
    print_dict(new_dict)
    for key in fre_list:
        if new_dict[key] < min_support:
            del new_dict[key]
    print "after pruning:"
    print_dict(new_dict)
    record_list = fre_list
    record_dict = record_dict
def get_candidateset():
    new_list = list()
    #自連線
    for i in range(0,len(fre_list)):
        for j in range(0,len(fre_list)):
            if i == j:
                continue
            #如果兩個k項集可以自連線,必須保證它們有k-1項是相同的
            if has_samesubitem(fre_list[i],fre_list[j]):
                curitem = fre_list[i] + ',' + fre_list[j]
                curitem = curitem.split(",")
                curitem = list(set(curitem))
                curitem.sort()
                curitem = ','.join(curitem)
                #如果一個k項集要成為候選集,必須保證它的所有子集都是頻繁的
                if has_infresubset(curitem) == False and already_constains(curitem,new_list) == False:
                    new_list.append(curitem)
    new_list.sort()
    return new_list
def has_samesubitem(str1,str2):
    str1s = str1.split(",")
    str2s = str2.split(",")
    if len(str1s) != len(str2s):
        return False
    nums = 0
    for items in str1s:
        if items in str2s:
            nums += 1
            str2s.remove(items)
    if nums == len(str1s) - 1:
        return True
    else:
        return False
def judge(candidatelist):
    # 計算候選集的支援度
    new_dict = dict()
    for item in candidatelist:
        new_dict[item] = get_support(item)
    print "candidate set:"
    print_dict(new_dict)
    #剪枝
    #頻繁集的支援度要大於最小支援度
    new_list = list()
    for item in candidatelist:
        if new_dict[item] < min_support:
            del new_dict[item]
            continue
        else:
            new_list.append(item)
    global fre_list
    fre_list = new_list
    print "after pruning:"
    print_dict(new_dict)
    return new_dict
def has_infresubset(item):
    # 由於是逐層搜尋的,所以對於Ck候選集只需要判斷它的k-1子集是否包含非頻繁集即可
    subset_list = get_subset(item.split(","))
    for item_list in subset_list:
        if already_constains(item_list,fre_list) == False:
            return True
    return False
def get_support(item,splitetag=True):
    if splitetag:
        items = item.split(",")
    else:
        items = item.split("^")
    support = 0
    for row in samples:
        tag = True
        for curitem in items:
            if curitem not in row:
                tag = False
                continue
        if tag:
            support += 1
    return support
def get_fullpermutation(arr):
    if len(arr) == 1:
        return [arr]
    else:
        newlist = list()
        for i in range(0,len(arr)):
            sublist = get_fullpermutation(arr[0:i]+arr[i+1:len(arr)])
            for item in sublist:
                curlist = list()
                curlist.append(arr[i])
                curlist.extend(item)
                newlist.append(curlist)
        return newlist
def get_subset(arr):
    newlist = list()
    for i in range(0,len(arr)):
        arr1 = arr[0:i]+arr[i+1:len(arr)]
        newlist1 = get_fullpermutation(arr1)
        for newlist_item in newlist1:
            newlist.append(newlist_item)
    newlist.sort()
    newlist = remove_dumplicate(newlist)
    return newlist
def remove_dumplicate(arr):
    newlist = list()
    for i in range(0,len(arr)):
        if already_constains(arr[i],newlist) == False:
            newlist.append(arr[i])
    return newlist
def already_constains(item,curlist):
    import types
    items = list()
    if type(item) is types.StringType:
        items = item.split(",")
    else:
        items = item
    for i in range(0,len(curlist)):
        curitems = list()
        if type(curlist[i]) is types.StringType:
            curitems = curlist[i].split(",")
        else:
            curitems = curlist[i]
        if len(set(items)) == len(curitems) and len(list(set(items).difference(set(curitems)))) == 0:
            return True
    return False
def print_dict(curdict):
    keys = curdict.keys()
    keys.sort()
    for curkey in keys:
        print "%s:%s"%(curkey,curdict[curkey])
# 計算關聯規則的方法
def get_all_subset(arr):
    rtn = list()
    while True:
        subset_list = get_subset(arr)
        stop = False
        for subset_item_list in subset_list:
            if len(subset_item_list) == 1:
                stop = True
            rtn.append(subset_item_list)
        if stop:
            break
    return rtn
def get_all_subset(s):
    from itertools import combinations
    return sum(map(lambda r: list(combinations(s, r)), range(1, len(s)+1)), [])
def cal_associative_rule(frelist):
    rule_list = list()
    rule_dict = dict()
    for fre_item in frelist:
        fre_items = fre_item.split(",")
        subitem_list = get_all_subset(fre_items)
        for subitem in subitem_list:
            # 忽略為為自身的子集
            if len(subitem) == len(fre_items):
                continue
            else:
                difference = set(fre_items).difference(subitem)
                rule_list.append("^".join(subitem)+"->"+"^".join(difference))
    print "The rule is:"
    for rule in rule_list:
        conf = cal_rule_confidency(rule)
        print rule,conf
        if conf >= min_confidence:
            rule_dict[rule] = conf
    print "The associative rule is:"
    for key in rule_list:
        if key in rule_dict.keys():
            print key,":",rule_dict[key]
def cal_rule_confidency(rule):
    rules = rule.split("->")
    support1 = get_support("^".join(rules),False)
    support2 = get_support(rules[0],False)
    if support2 == 0:
        return 0
    rule_confidency = float(support1)/float(support2)
    return rule_confidency
if __name__ == '__main__':
    record_list = list()
    record_dict = dict()
    get_c1()
    # 不斷進行自連線和剪枝,直到得到最終的頻繁集為止;終止條件是,如果自連線得到的已經不再是頻繁集
    # 那麼取最後一次得到的頻繁集作為結果
    while True:
        record_list = fre_list
        new_list = get_candidateset()
        judge_dict = judge(new_list)
        if len(judge_dict) == 0:
            break
        else:
            record_dict = judge_dict
    print "The final frequency set is:"
    print record_list
    
    # 根據頻繁集計算關聯規則
    cal_associative_rule(record_list)

演算法的輸出如下:
candidate set:
I1:6
I2:7
I3:6
I4:2
I5:2
after pruning:
I1:6
I2:7
I3:6
I4:2
I5:2
candidate set:
I1,I2:4
I1,I3:4
I1,I4:1
I1,I5:2
I2,I3:4
I2,I4:2
I2,I5:2
I3,I4:0
I3,I5:1
I4,I5:0
after pruning:
I1,I2:4
I1,I3:4
I1,I5:2
I2,I3:4
I2,I4:2
I2,I5:2
candidate set:
I1,I2,I3:2
I1,I2,I5:2
after pruning:
I1,I2,I3:2
I1,I2,I5:2
candidate set:
after pruning:
The final frequency set is:
['I1,I2,I3', 'I1,I2,I5']
The rule is:
I1->I3^I2 0.333333333333
I2->I1^I3 0.285714285714
I3->I1^I2 0.333333333333
I1^I2->I3 0.5
I1^I3->I2 0.5
I2^I3->I1 0.5
I1->I2^I5 0.333333333333
I2->I1^I5 0.285714285714
I5->I1^I2 1.0
I1^I2->I5 0.5
I1^I5->I2 1.0
I2^I5->I1 1.0
The associative rule is:
I5->I1^I2 : 1.0
I1^I5->I2 : 1.0
I2^I5->I1 : 1.0