數據挖掘——關聯規則算法之Apriori
一、關聯規則的基本概念
設爲所有項目的集合,D爲事務數據庫,事務T是一個項目子集()。每一個事務具有唯一的事務標識TID。設A是一個由項目構成的集合,稱爲項集。事務T包含項集A,當且僅當。如果項集A中包含k個項目,則稱其爲k項集。
-------------------------------------------------------------------------------
用通俗的方式解釋下上面的定義:
以超市的情況爲例 ,該超市有的那些商品的集合可以被看成I(=牛奶,=麪包,……);事務T可以看成每一個顧客單詞購買的所有的商品;那麼需要我們挖掘的就是項集,牛奶和麪包就是二項集,其他以此類推。
-------------------------------------------------------------------------------
再給兩個定義:
項集A在事務數據庫D中出現的次數佔D中總事務的百分比叫做項集的支持度。
如果項集的支持度超過用戶給定的最小支持閾值,就稱該項集是頻繁項集(或頻集)。
-------------------------------------------------------------------------------
再用通俗的方式解釋下上面的兩個定義:
還是以超市的情況爲例 ,假設有1000個顧客,其中有200個人購買了牛奶,那麼此時的支持度:200/1000 = 20%,它能夠體現出牛奶在所有人購物中的頻度,只有當這個頻度高到一定閾值的時候 ,才認爲該商品是對挖掘有意義的。
-------------------------------------------------------------------------------
關聯規則是形如的邏輯蘊含式,其中。
如果事務數據庫D中有s%的事務包含,則稱關聯規則的支持度爲s%。
關聯規則的信任度爲,也就是:
說的太官方,看的雲裏霧裏。那麼簡單地來表達:在進行關聯規則挖掘之前要找到一個東西,這個東西就是頻繁項集,根據這個頻繁項集找到哪些商品出現的次數多,在那些次數大於閾值的項集上再去進行有意義的挖掘人物。看上面的公式可知,支持度表示的是兩種商品同時出現的概率。 信任度時用來產出規則的,用來評價X對Y的影響大,還是Y對X的影響大。
二、強關聯規則
強關聯規則就是支持度和信任度分別滿足用戶給定閾值的規則。
------------------------------------------------------------------------------------
舉例:
交易ID | 購買的商品 |
---|---|
2000 | A,B,C |
1000 | A,C |
4000 | A,D |
5000 | B,E,F |
設最小支持度爲50%,最小可信度爲50%,則可得到:
(1)先計算最小支持度:
由以上結果可知,按照支持度的定義,此時分析商品A,B,C是有意義的。
(2)計算可信度(只計算A和C):
那麼就可以說,在滿足最小支持度的基礎上,一位顧客購買A再購買C的可能是66.6%,而購買C再購買A的可能是100%。
三、關聯規則挖掘算法
這裏僅列出,本文未一一詳述,只介紹Apriori算法,如有可能會在本人的其他博客中再詳細說明。
- AIS、Apriori 和AprioriTid
- SETM
- DHP
- PARTITION
- FPGrowth
其中,最有效和有影響的算法已用加粗字體標出。
下面開始說Apriori算法
Apriori算法將發現關聯規則的過程分爲兩個步驟:
- 通過迭代,檢索出事務數據庫中的所有頻繁項集,即支持度不低於用戶設定的閾值的項集。在這個過程中連接步和剪枝步互相融合,最終得到最大頻繁項集
(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項集的集合的成員,計算每一項的支持度。
這樣就得到了事務中的1項集
(2)對中個項集的支持度與預先設定的最小支持度閾值進行比較,保留大於或等於該閾值的項,得1項頻繁集
1項頻繁集 | 支持度 |
---|---|
{a} | 0.7 |
{b} | 0.8 |
{c} | 0.7 |
{d} | 0.2 |
{e} | 0.3 |
(3)掃描所有事務,和連接的候選2項集,並計算每一項的支持度,如。接下來就是剪枝步,由於的每個子集(即L1)都是頻繁項集,所以沒有項集衝中剔除。
(4)對中各項集的支持度與預先設定的最小支持度閾值進行比較,保留大於或者等於該閾值的項,得2項頻繁集:
項集 | 支持度 |
---|---|
{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 |
根據閾值,得
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)掃描所有事務,和連接的候選3項集,並計算每一項的支持度,如:。
接下來就是剪枝步,與連接的所有項集爲,根據Apriori算法,頻繁集的所有非空子集也必須是頻繁集,因爲不包含在b項頻繁中,即不是頻繁集,應剔除,最後的中的項集只有{a,b,c}和{a,c,e}
項集 | 支持度 |
---|---|
{a,b,c} | 0.3 |
{a,c,e} | 0.3 |
(6)對中各項集的支持度與預先設定的最小支持度進行比較,保留大於等於該閾值的項,得3項頻繁集
(7)與連接得到候選4項集,易得剪枝後爲空集。所以最後得到最大3項頻繁集{a,b,c}和{a,c,e}。
過程二 由頻繁集產生關聯規則
其中是包含項集的事務數,是包含項集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)