數挖 | 關聯分析#
關聯分析#
概念#
在數據挖掘中,關聯分析是一種較為常用的無監督學習算法,與分類、聚類等算法不同的是,這一類算法的主要目的在於發掘數據內在結構特徵之間的關聯性。關聯規則大致包含以下內容,本文主要介紹其中的簡單關聯分析及 Apriori 算法。
簡單一點來說,關聯規則就是在交易數據、關係數據或其他信息載體中,查找存在於項目集合或對象集合之間的頻繁模式、關聯、相關性或因果結構。
或者說,關聯分析是發現交易數據庫中不同商品(項)之間的關係
。
這些關係可以有兩種形式:頻繁項集或者關聯規則
- 頻繁項集(frequent item sets)是經常出現在一塊的物品的集合;
- 關聯規則(association rules)暗示兩種物品之間可能存在很強的關係。
下面舉個例子來說明這兩種概念,下表展示了某個雜貨鋪的交易清單:
交易代碼 | 商品 |
---|---|
0 | 豆奶,莴苣 |
1 | 莴苣,尿布,葡萄酒,甜菜 |
2 | 豆奶,尿布,葡萄酒,橙汁 |
3 | 莴苣,豆奶,尿布,葡萄酒 |
4 | 莴苣,豆奶,尿布,橙汁 |
頻繁項集是指那些經常出現在一起的物品集合,表中的集合 ${葡萄酒,尿布,豆奶}$ 就是頻繁項集的一個例子。從數據集中也可以找到諸如 $ 尿布 \rightarrow 葡萄酒 $ 的關聯規則。這意味著如果有人買了尿布,那麼他很可能也會買葡萄酒。使用頻繁項集和關聯規則,商家可以更好地理解他們的顧客。
應該如何定義這些有趣的關係?誰來定義什麼是有趣?當尋找頻繁項集時,頻繁(frequent)的定義是什麼?有許多概念可以解答上述問題,不過其中最重要的是
支持度和可信度
。
有效性指標#
支持度#
一個項集的支持度(support)被定義為數據集中包含該項集的記錄所占的比例,即事件發生的頻率。
從上表中可以得到,$\{豆奶 \}$ 的支持度為 4/5。而在 5 條交易記錄中有 3 條包含 $\{豆奶,尿布 \}$,因此 $\{豆奶,尿布 \}$ 的支持度為 3/5。支持度是針對項集來說的,因此可以定義一個最小支持度,而只保留滿足最小支持度的項集。
可信度#
可信度或置信度(confidence)是針對一條關聯規則來定義的,揭示了如果事件 A 發生了,則事件 B 發生的的概率。
比如 ${尿布}\rightarrow {葡萄酒}$ 這條規則的可信度被定義為 $ 支持度 ({尿布,葡萄酒}) / 支持度 ({尿布})$ 。從表中可以看到,由於 ${尿布,葡萄酒}$ 的支持度為 3/5,${尿布}$ 的支持度為 4/5,所以 $ 尿布 \rightarrow 葡萄酒 $ 的可信度為 3/4=0.75。
這意味著對於包含「尿布」的所有記錄,我們的規則對其中 75% 的記錄都適用。
支持度和可信度是用來量化關聯分析是否成功的方法。假設想找到支持度大於 0.8 的所有項集,應該如何去做?一個辦法是生成一個物品所有可能組合的清單,然後對每一種組合統計它出現的頻繁程度,但當物品成千上萬時,上述做法非常非常慢。下一節會詳細分析這種情況並討論 Apriori 原理,該原理會減少關聯規則學習時所需的計算量。
Apriori 算法發現頻繁項集#
上文提到,關聯分析的目標包括兩項:發現頻繁項集和發現關聯規則。首先需要找到頻繁項集,然後才能獲得關聯規則。本節將只關注於發現頻繁項集。
頻繁項集定理#
頻繁項集具有以下兩條定理,這兩條定理將應用於我們後面頻繁項集及其關聯規則的尋找中:
定理 1: 頻繁項集的子集必為頻繁項集(假設項集 $\{A,C\}$ 是頻繁項集,那麼 $\{A\}$ 和 $\{C\}$ 也為頻繁項集)
定理 2: 非頻繁集的超集一定也是非頻繁的(假設項集 $\{D\}$ 不是頻繁項集,那麼 $\{A,D\}$ 和 $\{C,D\}$ 也不是頻繁項集)
定理 1 比較容易證明,因為某項集的子集的支持度一定不小於該項集,定理 2 是定理 1 的逆反定理。
圖中給出了所有可能的項集,其中非頻繁項集用灰色表示。由於集合 $\{2,3\}$ 是非頻繁的, 因此 $\{0,2,3\}$、$\{1,2,3\}$ 和 $\{0,1,2,3\}$ 也是非頻繁的,它們的支持度根本不需要計算。
頻繁項集生成#
Apriori 算法是發現頻繁項集的一種方法。Apriori 算法的兩個輸入參數分別是最小支持度和數據集。該算法首先會生成所有單個物品的項集列表。接著掃描交易記錄來查看哪些項集滿足最小 支持度要求,那些不滿足最小支持度的集合會被去掉。然後,對剩下來的集合進行組合以生成包含兩個元素的項集。接下來,再重新掃描交易記錄,去掉不滿足最小支持度的項集。該過程重複進行直到所有項集都被去掉。
綜上所述,Apriori 算法採用迭代的方式逐層尋找下層的超集,並在超集中發現頻繁項集。經過層層迭代,直到最頂層得到最大頻繁項集為止。在每一輪的迭代中都包含以下兩個步驟:
- 產生候選集 $C_k$,它是有可能成為頻繁項集的項目集合;
- 修剪候選集 $C_k$,即基於 $C_k$ 計算相應的支持度,並依據最小支持度 $S_{min}$ 對候選集 $C_k$ 進行刪減,得到新的候選集 $C_{k+1}$,如此循環迭代,直到無法產生候選項集為止,這樣最後一輪所得到的頻繁項集就是 Apriori 所要求的最大頻繁項集。
整個 Apriori 算法的伪代码如下:
當集合中項的個數大於0時
構建一個k個項組成的候選項集的列表
檢查數據以確認每個項集都是頻繁的
對數據集中的每條交易記錄tran
對每個候選項集can:
檢查一下can是否是tran的子集:
如果是,則增加can的計數值
對每個候選項集:
如果其支持度不低於最小值,則保留該項集
返回所有頻繁項集列表
保留頻繁項集並構建k+1項組成的候選項集的列表
頻繁項集第二定理:非頻繁項集的超集一定也是非頻繁的
從頻繁項集中挖掘關聯規則#
得到最大頻繁項集並不是最終的目的。之前在判斷關聯規則的有效性時,我們學習了置信度與支持度兩個指標。其中,支持度已經在尋找最大頻繁項集的過程中發揮了作用,那麼,在接下來關聯規則的產生上,就輪到置信度大顯身手了。
從一個頻繁項集中可以產生多少條關聯規則?下圖的網格圖給出的是從項集 $\{0,1,2,3\}$ 產生的所有關聯規則。為找到感興趣的規則,我們先生成一個可能的規則列表,然後測試每條規則的置信度。如果置信度不滿足最小要求,則去掉該規則。
類似於上一節的頻繁項集生成,我們可以為每個頻繁項集產生許多關聯規則。如果能夠減少規則數目來確保問題的可解性,那麼計算起來就會好很多。可以觀察到,如果某條規則並不滿足 最小可信度要求,那麼該規則的所有子集也都不會滿足最小可信度要求。以上圖為例,假設規則 $0,1,2 \rightarrow 3$ 並不滿足最小可信度要求,那麼就知道任何左部為 $\{0,1,2\}$ 子集的規則也不會滿足最小可 可信度要求。在圖中這些規則上都加了陰影來表示。
可以利用關聯規則的上述性質屬性來減少需要測試的規則數目。類似於
Apriori 算法,可以首先從一個頻繁項集開始,接著創建一個規則列表,其中規則右部只包含一個元素,然後對這些規則進行測試。接下來合併所有剩餘規則來創建一個新的規則列表,其中規則右部包含兩個元素。這種方法也被稱作分級法。
程序清單#
Python#
# Apriori算法中的輔助函數
def loadDataSet():
"""
創建一個用於測試的簡單數據集
"""
return [[1,3,4], [2,3,5], [1,2,3,5], [2,5]]
def creatC1(dataSet):
"""
輸入:數據集dataSet
輸出:C1是大小為1的所有候選項集的集合
过程:首先創建一個空列表C1,它用來存儲所有不重複的項值。接下來遍歷數據集中的所有交易記錄。對每一條記錄,遍歷記錄中的每一個項。如果某個物品項沒有在C1中出現,則將其添加到C1中。這裡並不是簡單地添加每個物品項,而是添加只包含該物品項的一個列表。這樣做的目的是為每個物品項構建一個集合。因為在Apriori算法的後續處理中,需要做集合操作。Python不能創建只有一個整數的集合,因此這裡實現必須使用列表(有興趣的讀者可以試一下)。這就是我們使用一個由單物品列表組成的大列表的原因。最後,對大列表進行排序並將其中的每個單元素列表映射到frozenset(),最後返回frozenset的列表
細節:由於算法一開始是從輸入數據中提取候選項集列表,所以這裡需要一個特殊的函數來處理,而後續的項集列表則是按一定的格式存放的。這裡使用的格式就是Python中的frozenset類型。frozenset是指被“冰凍”的集合,就是說它們是不可改變的,即用户不能修改它們。這裡必須要使用frozenset而不是set類型,因為之後必須要將這些集合作為字典鍵值使用,使用frozenset可以實現這一點,而set卻做不到
"""
C1 = []
for transaction in dataSet:
for item in transaction:
if not [item] in C1:
C1.append([item])
C1.sort()
return map(frozenset, C1)
def scanD(D, Ck, minSupport):
"""
輸入:數據集,候選項集列表Ck,感興趣項集的最小支持度minSupport
輸出:該函數用於從Ck生成Lk,另外,該函數會返回一個 包含支持度值的字典以備後用
过程:函數首先創建一個空字典ssCnt,然後遍歷數據集中的所有交易記錄以及C1中的所有候選集。如果C1中的集合是記錄的一部分,那麼增加字典中對應的計數值。這裡字典的鍵就是集合。當掃描完數據集中的所有項以及所有候選集時,就需要計算支持度。不滿足最小支持度要求的集合不會輸出。函數也會先構建一個空列表,該列表包含滿足最小支持度要求的集合。下個循環遍歷字典中的每個元素並且計算支持度。如果支持度滿足最小支持度要求,則將字典元素添加到retList中,函數最後返回最頻繁項集的支持度supportData
細節:可以使用語句retList.insert(0,key) 在列表的首部插入任意新的集合。當然也不一定非要在首部插入,這只是為了讓列表看起來有組織
"""
ssCnt = {}
for tid in D:
for can in Ck:
if can.issubset(tid):
if not ssCnt.has_key(can): ssCnt[can]=1
else: ssCnt[can] += 1
numItems = float(len(D))
retList = []
supportData = {}
for key in ssCnt:
support = ssCnt[key]/numItems
if support >= minSupport:
retList.insert(0, key)
supportData[key] = support
return retList, supportData
# Apriori算法
def aprioriGen(Lk, k):
"""
輸入:頻繁項集列表Lk,項集元素個數k
輸出:候選項集Ck
舉例:以{0}、{1}、{2}作為輸入,會生成{0,1}、{0,2}以及{1,2}
过程:首先創建一個空列表,然後計算Lk中的元素數目。接下來,比較Lk中的每一個元素與其他元素,這可以通過兩個for循環來實現。緊接著,取列表中的兩個集合進行比較。如果這兩個集合的前面k-2個元素都相等,那麼就將這兩個集合合成一個大小為k的集合
細節:k-2有點讓人疑惑。接下來再進一步討論細節。當利用{0}、{1}、{2}構建{0,1}、{0,2}、{1,2}時,這實際上是將單個項組合到一塊。現在如果想利用{0,1}、{0,2}、{1,2}來創建三元素項集,應該怎麼做?如果將每兩個集合合併,就會得到{0,1,2}、{0,1,2}、{0,1,2}。也就是說,同樣的結果集合會重複3次。接下來需要掃描三元素項集列表來得到非重複結果,我們要做的是確保遍歷列表的次數最少。現在,如果比較集合{0,1}、{0,2}、{1,2}的第1個元素並只對第1個元素相同的集合求並操作,又會得到什麼結果?{0,1,2},而且只有一次操作!這樣就不需要遍歷列表來尋找非重複值
"""
retList = []
lenLk = len(Lk)
for i in range(lenLk):
for j in range(i+1, lenLk):
L1 = list(Lk[i])[:k-2]; L2 = list(Lk[j])[:k-2]
L1.sort(); L2.sort()
if L1==L2:
retList.append(Lk[i] | Lk[j])
return retList
def apriori(dataSet, minSupport = 0.5):
"""
輸入:數據集dataSet,支持度minSupport
輸出:L包含滿足最小支持度的頻繁項集列表,suppData是包含項集的支持度值字典
过程:函數會生成候選項集的列表,這通過首先創建C1然後讀入數據集將其轉化為D(集合列表)來完成。程序中使用map函數將set()映射到dataSet列表中的每一項。接下來,使用scanD()函數來創建L1,並將L1放入列表L中。L會包含L1、L2、L3…。現在有了L1,後面會繼續找L2,L3…,這可以通過while循環來完成,它創建包含更大項集的更大列表,直到下個大的項集為空。如果這聽起來讓人有點困惑的話,那麼等一下你會看到它的工作流程。首先使用aprioriGen()來創建Ck,然後使用scanD()基於Ck來創建Lk。Ck是一個候選項集列表,然後scanD()會遍歷Ck,丟掉不滿足最小支持度要求的項集。Lk列表被添加到L,同時增加k的值,重複上述過程。最後,當Lk為空時,程序返回L並退出。
"""
C1 = creatC1(dataSet)
D = map(set, dataSet)
L1, supportData = scanD(D, C1, minSupport)
L = [L1]
k = 2
while (len(L[k-2]) > 0):
Ck = aprioriGen(L[k-2], k)
Lk, supK = scanD(D, Ck, minSupport)
supportData.update(supK)
L.append(Lk)
k += 1
return L, supportData
# 關聯規則生成函數
def generateRules(L, supportData, minConf=0.7):
"""
輸入:頻繁項集列表、包含那些頻繁項集支持數據的字典、最小可信度閾值
輸出:生成一個包含可信度的規則列表,後面可以基於可信度對它們進行排序
过程:該函數遍歷L中的每一個頻繁項集並對每個頻繁項集創建只包含單個元素集合的列表H1。因為無法從單元素項集中構建關聯規則,所以要從包含兩個或者更多元素的項集開始規則構建過程。如果從集合{0,1,2}開始,那麼H1應該是[{0},{1},{2}]。如果頻繁項集的元素數目超過2,那麼會考慮對它做進一步的合併。具體合併可以通過函數rulesFromConseq()來完成,後面會詳細討論合併過程。如果項集中只有兩個元素,那麼使用函數calcConf()來計算可信度值
"""
bigRuleList = []
for i in range(1, len(L)):
for freqSet in L[i]:
H1 = [frozenset([item]) for item in freqSet]
if (i > 1):
rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf)
else:
calcConf(freqSet, H1, supportData, bigRuleList, minConf)
return bigRuleList
def calcConf(freqSet, H, supportData, brl, minConf=0.7):
"""
作用:對規則進行評估,計算規則的可信度以及找到滿足最小可信度要求的規則
过程:函數會返回一個滿足最小可信度要求的規則列表,為了保存這些規則,需要創建一個空列表prunedH。接下來,遍歷H中的所有項集並計算它們的可信度值。可信度計算時使用supportData中的支持度數據。通過導入這些支持度數據,可以節省大量計算時間。如果某條規則滿足最小可信度值,那麼將這些規則輸出到螢幕顯示。通過檢查的規則也會被返回,並被用在下個函數rulesFromConseq()中。同時也需要對列表brl進行填充,而brl是前面通過檢查的bigRuleList
"""
prunedH = []
for conseq in H:
conf = supportData[freqSet]/supportData[freqSet-conseq]
if conf >= minconf:
print(freqSet-conseq,'-->',conseq,'conf',conf)
brl.append((freqSet-conseq, conseq, conf))
prunedH.append(conseq)
return prunedH
def rulesFromconseq(freqSet, H, supportData, brl, minConf=0.7):
"""
作用:生成候選規則集合
过程:為從最初的項集中生成更多的關聯規則,可以使用rulesFromConseq()函數。該函數有2個參數:一個是頻繁項集,另一個是可以出現在規則右部的元素列表H。函數先計算H中的頻繁集大小m。接下來查看該頻繁項集是否大到可以移除大小為m的子集。如果可以的話,則將其移除。可以使用函數aprioriGen()來生成H中元素的無重複組合。該結果會存儲在Hmp1中,這也是下一次迭代的H列表。Hmp1包含所有可能的規則。可以利用calcConf()來測試它們的可信度以確定規則是否滿足要求。如果不止一條規則滿足要求,那麼使用Hmp1迭代調用函數rulesFromConseq()來判斷是否可以進一步組合這些規則
"""
m = len(H[0])
if (len(freqSet) > (m+1)):
Hmpl = aprioriGen(H, m+1)
Hmpl = calcConf(freqSet, Hmpl, supportData, brl, minConf)
if (len(Hmpl) > 1):
rulesFromConseq(freqSet, Hmpl, supportData, brl, minConf)
R#
詳見 用 R 語言進行關聯分析
參考文獻:#