數據挖掘——關聯規則算法之Apriori

一、關聯規則的基本概念

I=i1,i2,...,imI={i_{1},i_{2},...,i_{m}}爲所有項目的集合,D爲事務數據庫,事務T是一個項目子集(TIT\sqsubseteq I)。每一個事務具有唯一的事務標識TID。設A是一個由項目構成的集合,稱爲項集。事務T包含項集A,當且僅當ATA\sqsubseteq T。如果項集A中包含k個項目,則稱其爲k項集。

-------------------------------------------------------------------------------

用通俗的方式解釋下上面的定義:
以超市的情況爲例 ,該超市有的那些商品的集合可以被看成I(i1i_{1}=牛奶,i2i_{2}=麪包,……);事務T可以看成每一個顧客單詞購買的所有的商品;那麼需要我們挖掘的就是項集,牛奶和麪包就是二項集,其他以此類推。

-------------------------------------------------------------------------------

再給兩個定義:
項集A在事務數據庫D中出現的次數佔D中總事務的百分比叫做項集的支持度
如果項集的支持度超過用戶給定的最小支持閾值,就稱該項集是頻繁項集(或頻集)。

-------------------------------------------------------------------------------

再用通俗的方式解釋下上面的兩個定義:
還是以超市的情況爲例 ,假設有1000個顧客,其中有200個人購買了牛奶,那麼此時的支持度:200/1000 = 20%,它能夠體現出牛奶在所有人購物中的頻度,只有當這個頻度高到一定閾值的時候 ,才認爲該商品是對挖掘有意義的。

-------------------------------------------------------------------------------

關聯規則是形如XYX\Rightarrow Y的邏輯蘊含式,其中XI,YI,XYX\sqsubset I,Y\sqsubset I,且X \cap Y \neq \varnothing
如果事務數據庫D中有s%的事務包含XYX\cup Y,則稱關聯規則XYX\Rightarrow Y的支持度爲s%。

關聯規則的信任度爲support(XY)/support(X)support(X\cup Y)/support(X),也就是:
support(XY)=P(XY)confidence(XY)=P(YX)P(X,Y)P(X)support(X\Rightarrow Y)=P(X\cup Y)\\ confidence(X\Rightarrow Y)=P(Y|X)\frac{P(X,Y)}{P(X)}

說的太官方,看的雲裏霧裏。那麼簡單地來表達:在進行關聯規則挖掘之前要找到一個東西,這個東西就是頻繁項集,根據這個頻繁項集找到哪些商品出現的次數多,在那些次數大於閾值的項集上再去進行有意義的挖掘人物。看上面的公式可知,支持度表示的是兩種商品同時出現的概率。 信任度時用來產出規則的,用來評價X對Y的影響大,還是Y對X的影響大。

二、強關聯規則

強關聯規則就是支持度和信任度分別滿足用戶給定閾值的規則。
------------------------------------------------------------------------------------
舉例

交易ID 購買的商品
2000 A,B,C
1000 A,C
4000 A,D
5000 B,E,F

設最小支持度爲50%,最小可信度爲50%,則可得到:
(1)先計算最小支持度:
support(A)=34=0.75=75%50%support(B)=24=0.5=50%50%support(C)=24=0.5=50%50%support(D)=support(E)=support(F)=14=0.25=25%50%support(A)=\frac{3}{4}=0.75= 75\% \rightarrow 大於最小支持度50\%\\support(B)=\frac{2}{4}=0.5= 50\%\rightarrow 等於最小支持度50\%\\ support(C)=\frac{2}{4}=0.5= 50\% \rightarrow 等於最小支持度50\%\\ support(D)=support(E)=support(F)=\frac{1}{4}=0.25= 25\% \rightarrow 小於最小支持度50\%
由以上結果可知,按照支持度的定義,此時分析商品A,B,C是有意義的。
(2)計算可信度(只計算A和C):

AC(50%,66.6%(2/3))CA(50%,100%(2/2))A\Rightarrow C (50 \%,66.6\%(2/3))\\ C \Rightarrow A (50\%,100\%(2/2))
那麼就可以說,在滿足最小支持度的基礎上,一位顧客購買A再購買C的可能是66.6%,而購買C再購買A的可能是100%。

三、關聯規則挖掘算法

這裏僅列出,本文未一一詳述,只介紹Apriori算法,如有可能會在本人的其他博客中再詳細說明。

  • AIS、Apriori 和AprioriTid
  • SETM
  • DHP
  • PARTITION
  • FPGrowth

其中,最有效和有影響的算法已用加粗字體標出。

下面開始說Apriori算法

Apriori算法將發現關聯規則的過程分爲兩個步驟:

  1. 通過迭代,檢索出事務數據庫中的所有頻繁項集,即支持度不低於用戶設定的閾值的項集。在這個過程中連接步和剪枝步互相融合,最終得到最大頻繁項集LkL_{k}
    (1)連接步:
    連接步的目的是找到K項集。對於給定的最小支持度閾值,分別對1項候選集C1,剔除小於該閾值的項集得到1項頻繁集L1;下一步由L1自身連接產生2項候選集C2,保留C2中滿足約束條件的項集得到2項頻繁集,記爲L2;再下一步 有L2與L3連接產生3項候選集C3,保留C3中滿足約束條件的項集得到3項頻繁集,記爲L3,……這樣循環下去,得到最大頻繁集Lk。
    (2)剪枝步:
    剪枝步緊接着連接步,在產生候選項Ck的過程中起到減小搜索空間的目的。由於Ck是Lk-1和L1連接產生的,根據Apriori算法的性質頻繁項集的所有非空子集也必須是頻繁項集,所以不滿足該性質的項集不會存在於Ck中。
    2.由頻繁項集產生強關聯規則:由過程1可知未超過預定的最小支持度閾值的項集已經被剔除,如果剩下這些規則又滿足了預定的最小可信度閾值,那麼就挖掘出了強關聯規則。

注:挖掘或識別出所有頻繁項集是該算法的核心,佔整個計算量的大部分


舉例:
下面結合餐飲行業的實例來講解Apriori關聯規則算法挖掘的實現過程。數據庫中部分點餐數據見下表:

序列 時間 訂單號 菜品id 菜品名稱
1 2014/8/21 101 18491
2 2014/8/21 101 8693 啦啦
3 2014/8/21 101 8705 啦啦啦
4 2014/8/21 102 8842
5 2014/8/21 102 7794 餓餓
6 2014/8/21 103 8842 餓餓餓餓
7 2014/8/21 103 8693

將上表的事務數據整理成關聯規則模型所需的數據結構,從中抽取10個點餐訂單最爲事務數據庫,設支持度爲0.2(10個數據,那麼支持度計數爲2),爲方便起見將菜品{18491,8842,8693,7794,8705}分別簡記爲{a,b,c,d,e},見下表:

訂單號 菜品id 菜品id
1 18491,8693,8705 a,c,e
2 8842,7794 b,d
3 8842,8693 b,c
4 18491,8842,8693,7794 a,b,c,d
5 18491,8842 a,b
6 8842,8693 b,c
7 18491,8842 a,b
8 18491,8842,8693,8705 a,b,c,e
9 18491,8842,8693 a,b,c
10 18491,8693,8705 a,c,e

過程一: 找到最大k項頻繁集

(1)算法簡單掃描所有的事務,事務中的每一項都是候選集1項集的集合C1C_{1}的成員,計算每一項的支持度。
P(a)=a=710=0.7P(b)=810=0.8P(c)=710=0.7P(d)=210=0.2P(e)=310=0.3P({a})=\frac{項集{a}的支持度計數}{所有事務個數}=\frac{7}{10}=0.7\\ P({b})= \frac{8}{10}=0.8\\ P({c})=\frac{7}{10}=0.7\\ P({d})=\frac{2}{10}=0.2\\ P({e})=\frac{3}{10}=0.3
這樣就得到了事務中的1項集C1C_{1}
(2)對C1C_{1}中個項集的支持度與預先設定的最小支持度閾值進行比較,保留大於或等於該閾值的項,得1項頻繁集L1L_{1}
L1L_{1}\Rightarrow

1項頻繁集 支持度
{a} 0.7
{b} 0.8
{c} 0.7
{d} 0.2
{e} 0.3

(3)掃描所有事務,L1L_{1}L1L_{1}連接的候選2項集C2C_{2},並計算每一項的支持度,如P(a,b)=a,b=510=0.5P({a,b})= \frac{項集{a,b}的支持度計數}{所有事務個數}=\frac{5}{10}=0.5。接下來就是剪枝步,由於C2C_{2}的每個子集(即L1)都是頻繁項集,所以沒有項集衝C2C_{2}中剔除。
(4)對C2C_{2}中各項集的支持度與預先設定的最小支持度閾值進行比較,保留大於或者等於該閾值的項,得2項頻繁集L2L_{2}:

C2C_{2}\Rightarrow

項集 支持度
{a,b} 0.5
{a,c} 0.5
{a,d} 0.1
{a,e} 0.3
{b,c} 0.5
{b,d} 0.2
{b,e} 0.1
{c,d} 0.1
{c,e} 0.3
{d,e} 0

根據閾值,得L2L_{2}\Rightarrow

2項頻繁集 支持度
{a,b} 0.5
{a,c} 0.5
{a,e} 0.3
{b,c} 0.5
{b,d} 0.2
{c,e} 0.3

(5)掃描所有事務,L2L_{2}L1L_{1}連接的候選3項集C3C_{3},並計算每一項的支持度,如:P(a,b,c)=a,b,c=310=0.3P({a,b,c})=\frac{項集{a,b,c}的支持度個數}{所有事務個數}=\frac{3}{10}=0.3
接下來就是剪枝步,L2L_{2}L1L_{1}連接的所有項集爲{a,b,c},{a,b,d},{a,b,e},{a,c,d},{a,c,e},{b,c,d},{b,c,e}\{a,b,c\},\{a,b,d\},\{a,b,e\},\{a,c,d\},\{a,c,e\},\{b,c,d\},\{b,c,e\},根據Apriori算法,頻繁集的所有非空子集也必須是頻繁集,因爲{b,d},{b,d},{b,e},{c,d}\{b,d\},\{b,d\},\{b,e\},\{c,d\}不包含在b項頻繁L2L_{2}中,即不是頻繁集,應剔除,最後的C3C_{3}中的項集只有{a,b,c}和{a,c,e}
C3C_{3}\Rightarrow

項集 支持度
{a,b,c} 0.3
{a,c,e} 0.3

(6)對C3C_{3}中各項集的支持度與預先設定的最小支持度進行比較,保留大於等於該閾值的項,得3項頻繁集L3L_{3}
(7)L3L_{3}L2L_{2}連接得到候選4項集C4C_{4},易得剪枝後爲空集。所以最後得到最大3項頻繁集{a,b,c}和{a,c,e}。

過程二 由頻繁集產生關聯規則
Confidence(AB)=P(AB)=Support(AB)Support(A)=Supportcount(AB)Supportcount(A)Confidence(A \Rightarrow B)=P(A|B)=\frac{Support(A\cap B)}{Support(A)}=\frac{Support_count(A\cap B)}{Support_count(A)}

其中Supportcount(AB)Support_count(A\cap B)是包含項集ABA\cap B的事務數,Supportcount(A)Support_count(A)是包含項集A的事務數,根據該公式,產生如下關聯規則:

Rule (Support,Confidence)
a->b (50%,71.4286%)
b->a (50%62.5%)
a->c (50%,71.4286%)
c->a (30%,71.4286%)
b->c (50%,62.5%)
c->b (50%,71.4286%)
e->a (30%,100%)
e->c (30%,100%)
a,b->c (30%,60%)
a,c->b (30%,60%)
b,c->a (30%,60%)
e->a,c (30%,100%)
a,c->e (30%,60%)
a,e->c (30%,100%)
c,e->a (30%,100%)
d->b (20%,100%)

**解釋:**客戶同時點菜品a和b的概率是50%,點了菜品a,再點菜品b的概率是71.4286%。其他以此類推。

Apriori算法的不足

1)交易數據庫可能非常大,比如頻集最多包含10個項,那麼就需要掃描交易數據10遍。
2)需要很大的I/O負載

代碼

# -*- coding: utf-8 -*-

# apriori算法的一個簡單實現
from sys import exit, exc_info
from itertools import combinations
from collections import defaultdict
from time import clock
from optparse import OptionParser


def parse_arguments(parser):
    '''
        解析命令行給定的參數,運行apriori算法
    '''
    parser.add_option('-i', '--input', type='string', help='input file',
                      dest='input')
    parser.add_option('-s', '--support', type='float', help='min support',
                      dest='support')
    parser.add_option('-c', '--confidence', type='float',
                      help='min confidence', dest='confidence')

    (options, args) = parser.parse_args()
    if not options.input:
        parser.error('Input filename not given')
    if not options.support:
        parser.error('Support not given')
    if not options.confidence:
        parser.error('Confidence not given')
    return(options, args)


def get_transactions_from_file(file_name):
    '''
        讀取文件,返回所有“購物籃”的結果
        返回的格式是list,其中每個元素都是一個“購物籃”中的“商品”組成的frozenset
    '''
    try:
        with open(file_name) as f:
            content = f.readlines()
            f.close()
    except IOError as e:
        print 'I/O error({0}): {1}'.format(e.errno, e.strerror)
        exit()
    except:
        print 'Unexpected error: ', exc_info()[0]
        exit()
    transactions = []
    for line in content:
        transactions.append(frozenset(line.strip().split()))
    return transactions


def print_results(itemsets_list, rules, transactions):
    '''
        輸出結果
    '''
    for idx, itemsets in enumerate(itemsets_list):
        if len(itemsets) == 0:
            continue
        print 'Itemsets of size', idx + 1
        formatted_itemsets = []
        for itemset, freq in itemsets.iteritems():
            support = float(freq) / len(transactions)
            formatted_itemsets.append((','.join(sorted(map(str, itemset))),
                                       round(support, 3)))
        sorted_itemsets = sorted(formatted_itemsets,
                                 key=lambda tup: (-tup[1], tup[0]))
        for itemset, support in sorted_itemsets:
            print itemset, '{0:.3f}'.format(support)

        print

    print 'RULES'
    formatted_rules = [(','.join(sorted(map(str, rule[0]))) + ' -> ' +
                        ','.join(sorted(map(str, rule[1]))),
                       round(acc, 3))
                       for rule, acc in rules]
    sorted_rules = sorted(formatted_rules, key=lambda tup: (-tup[1], tup[0]))
    for rule, acc in sorted_rules:
        print rule, '{0:.3f}'.format(acc)


def remove_itemsets_without_min_support(itemsets, min_sup, transactions):
    '''
        刪除不滿足最小支持度的itemsets
    '''
    for itemset, freq in itemsets.items():
        if float(freq) / len(transactions) < min_sup:
                del itemsets[itemset]


def generate_itemsets(itemsets_list, min_sup):
    '''
        給定1-項集,這個函數會通過和自己join生成itemsets,然後刪除不滿足最小支持度的itemsets
        參數:
        1)itemsets_list: 一個包含1-項集的list
        2)min_sup:最小支持度
    '''
    try:
        next_candidate_item_sets = self_join(itemsets_list[0])
    except IndexError:
        return
    while(len(next_candidate_item_sets) != 0):
        itemsets_list.append(defaultdict(int))
        for idx, item_set in enumerate(next_candidate_item_sets):
            for transaction in transactions:
                if item_set.issubset(transaction):
                    itemsets_list[-1][item_set] += 1

        remove_itemsets_without_min_support(itemsets_list[-1], min_sup,
                                            transactions)
        try:
            next_candidate_item_sets = self_join(itemsets_list[-1])
        except IndexError:
            break


def build_k_minus_one_members_and_their_occurrences(itemsets, k):

    k_minus_one_members_and_occurrences = defaultdict(list)
    for itemset in itemsets:
        # small cheat to make a list a hashable type
        k_minus_one_members = ''.join(sorted(itemset)[:k - 1])
        k_minus_one_members_and_occurrences[k_minus_one_members].\
            append(itemset)
    return k_minus_one_members_and_occurrences


def generate_itemsets_from_kmomo(kmomo):

    new_itemsets = []
    for k_minus_one_members, occurrences in kmomo.items():
        if len(occurrences) < 2:
            # delete those k_minus_one_members that have only one occurrence
            del kmomo[k_minus_one_members]
        else:  # generate the new itemsets
            for combination in combinations(occurrences, 2):
                union = combination[0].union(combination[1])
                new_itemsets.append(union)
    return new_itemsets


def self_join(itemsets):

    itemsets = itemsets.keys()  # we are only interested on the itemsets
                                # themselves, not the frequencies
    k = len(itemsets[0])  # length of the itemsets
    # making sure all the itemsets have the same length
    assert(all(len(itemset) == k for itemset in itemsets))
    kmomo = build_k_minus_one_members_and_their_occurrences(itemsets, k)
    return generate_itemsets_from_kmomo(kmomo)


def build_one_consequent_rules(itemset, freq, itemsets_list):

    accurate_consequents = []
    rules = []
    for combination in combinations(itemset, 1):
        consequent = frozenset(combination)
        antecedent = itemset - consequent
        ant_len_itemsets = itemsets_list[len(antecedent) - 1]
        conf = float(freq) / ant_len_itemsets[antecedent]
        if conf >= min_conf:
            accurate_consequents.append(consequent)
            rules.append(((antecedent, consequent), conf))
    return accurate_consequents, rules


def build_n_plus_one_consequent_rules(itemset, freq, accurate_consequents,
                                      itemsets_list):

    rules = []
    consequent_length = 2
    while(len(accurate_consequents) != 0 and
          consequent_length < len(itemset)):
        new_accurate_consequents = []
        for combination in combinations(accurate_consequents, 2):
            consequent = frozenset.union(*combination)
            if len(consequent) != consequent_length:
                # combined itemsets must share n-1 common items
                continue
            antecedent = itemset - consequent
            ant_len_itemsets = itemsets_list[len(antecedent) - 1]
            conf = float(freq) / ant_len_itemsets[antecedent]
            if conf >= min_conf:
                new_accurate_consequents.append(consequent)
                rules.append(((antecedent, consequent), conf))
        accurate_consequents = new_accurate_consequents
        consequent_length += 1
    return rules


def generate_rules(itemsets, min_conf, itemsets_list):
    '''
        參數
        1)itemsets: 用於生成規則的相同長度的itemsets
        2)min_conf: 最小信任度
        3)itemsets_list: 相同長度的itemsets組成的字典
        返回結果
        4)返回的規則: 規則是這樣的格式爲 [((antecedent, consequent), confidence), ...]
    '''
    rules = []
    for itemset, freq in itemsets.iteritems():
        accurate_consequents, new_rules = \
            build_one_consequent_rules(itemset, freq, itemsets_list)
        rules += new_rules
        # 如果現在itemset大小已經小於1,直接continue
        if len(itemset) <= 2:
            continue

        rules += build_n_plus_one_consequent_rules(itemset, freq,
                                                   accurate_consequents,
                                                   itemsets_list)
    return list(set(rules))


if __name__ == '__main__':
    usage_text = 'Usage: %prog -s minsup -c minconf [-a minatm]'
    parser = OptionParser(usage=usage_text)
    (options, args) = parse_arguments(parser)
    min_sup = options.support
    min_conf = options.confidence
    t1 = clock()

    transactions = get_transactions_from_file(options.input)
    itemsets_list = [defaultdict(int)]

    # 生成長度爲1的itemsets
    for transaction in transactions:
        for item in transaction:
            itemsets_list[0][frozenset([item])] += 1
    remove_itemsets_without_min_support(itemsets_list[0], min_sup,
                                        transactions)

    # 生成長度>1的itemsets
    generate_itemsets(itemsets_list, min_sup)

    # 生成規則
    rules = []
    for itemsets in list(reversed(itemsets_list))[:-1]:
        rules += generate_rules(itemsets, min_conf, itemsets_list)

    t2 = clock()
    print_results(itemsets_list, rules, transactions)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章