《數據挖掘概念與技術》 第6章 挖掘頻繁模式

挖掘頻繁模式、關聯和相關性

Apriori算法

Apriori算法是一種用於關聯規則挖掘的代表性算法。從本節開始,我們已經進入了機器學習和數據挖掘相交叉的地帶。

數據挖掘與機器學習

數據挖掘和機器學習的關係就好比,機器學習是數據挖掘的彈藥庫中一類相當龐大的彈藥集。既然是一類彈藥,其實也就是在說數據挖掘中肯定還有其他非機器學習範疇的技術存在。Apriori算法就屬於一種非機器學習的數據挖掘技術。

在非機器學習的數據挖掘技術中,我們並不會去建立這樣一個模型,而是直接從原數據集入手,設法分析出隱匿在數據背後的某些信息或知識。在後續介紹Apriori算法時,你會相當明顯地感受到這一特點。

Apriori算法

我們先從一個例子瞭解一下apriori原理。被大家所熟知的"啤酒尿布"的購買行爲問題,其實就是一個具有關聯性的行爲。而發現這種關聯性行爲的方式,可以用apriori原理來實現。

從大規模數據集中尋找物品間的隱含關係被稱作“關聯分析”或者“關聯規則學習”。而我們需要關注的問題是,如何使用更智能的方法,在更合理的時間範圍內找到我們所需要的關聯規則。
以此,引出了我們的aprori算法。
我們先來介紹幾個概念。
關聯分析,是一種在大規模數據集中尋找有趣關係的任務。在這種關係中,存在着兩種形式:頻繁項集或者關聯規則。
頻繁項集(frequent item sets),是經常出現在一塊的物品的集合;關聯規則(association rules),暗示兩種物品之間可能存在很強的關係。
頻繁項集是我們對原始數據格式化後的源數據集,而關聯規則則是尋找源數據集關係得到的結果數據集。
{}爲集合標識。
在下圖中,{葡萄酒,尿布,豆奶}就是頻繁項集的一個例子。在頻繁項集中,我們也可以找到諸如“{尿布}->{葡萄酒}”的關聯規則。這意味着如果有人買了尿布,那麼他很可能也會買葡萄酒。
在這裏插入圖片描述

那麼應該如何定義這些有趣的關係?誰來定義什麼是有趣?當尋找頻繁項集時,頻繁的定義又是什麼?
一個項集的**支持度(support)**被定義爲數據集中包含該項集的記錄所佔的比例。支持度是針對項集,我們可以定義一個最小支持度,來保留滿足最小支持度的項集。
如在上圖中,{豆奶}的支持度爲4/5,而{豆奶,尿布}的支持度爲3/5
**可信度又稱爲置信度(confidence)**是針對一條關聯規則定義的。
對於關聯規則“{尿布}->{葡萄酒}”,它的可信度被定義爲“支持度({尿布,葡萄酒})/支持度({尿布})”,計算該規則可信度爲0.75,這意味着對於包含“尿布”的所有記錄,我們的規則對其中75%的記錄都適用。
在上面介紹的支持度和可信度被用來量化關聯分析是否成功。但在實際操作的過程中,我們需要對物品所有的組合計算其支持度和可信度,當物品量上萬時,上述的操作會非常非常慢。

我們該如何解決這種問題呢?
我們已經知道,大多數關聯規則挖掘算法通常採用的一種策略是,將關聯規則挖掘任務分解爲如下兩個主要的子任務。
頻繁項集產生:其目標是發現滿足最小支持度閾值的所有項集,這些項集稱作頻繁項集(frequent itemset)。
規則的產生:其目標是從上一步發現的頻繁項集中提取所有高置信度的規則,這些規則稱作強規則(strong rule)。

Apriori算法是生成頻繁集的一種算法。
在這裏插入圖片描述

上圖顯示了4種商品所有可能的組合。對給定的集合項集{0,3},需要遍歷每條記錄並檢查是否同時包含0和3,掃描完後除以記錄總數即可得支持度。對於包含N種物品的數據集共有2的N次方-1種項集組合,即使100種,也會有1.26×10的30次方種可能的項集組成。

爲降低計算時間,可用Apriori原理:如果某個項集是頻繁的,那麼它的所有子集也是頻繁的。而我們需要使用的則是它的逆反定義:如果一個項集是非頻繁集,那麼它的所有超集也是非頻繁的。

在下圖中,已知陰影項集{2,3}是非頻繁的,根據Apriori原理,我們知道{0,2,3},{1,2,3}以及{0,1,2,3}也是非頻繁的,我們就不需要計算其支持度了。
在這裏插入圖片描述
這樣就可以避免項集數目的指數增長,從而找到頻繁項集。
我們已經知道關聯分析的目標分爲:發現頻繁項集發現關聯規則

發現頻繁項集

首先需要找到頻繁項集,然後才能獲得關聯規則。本節我們將重點放在如何發現頻繁項集上。

Apriori算法會首先構建集合C1,C1是大小爲1的所有候選項集的集合,然後掃描數據集來判斷只有一個元素的項集是否滿足最小支持度的要求。那麼滿足最低要求的項集構成集合L1。

而集合L1中的元素相互組合構成C2,C2再進一步過濾變成L2,以此循環直到Lk爲空。

在上述對Apriori算法的描述中,我們可以很清楚的看到,需要構建相應的功能函數來實現該算法:

1.createC1 -構建集合C1

2.scanD -過濾集合C1,構建L1

3.aprioriGen -對集合Lk中的元素相互組合構建Ck+1

4.apriori -集成函數

我們按照該邏輯來實現我們的apriori算法,並找到頻繁項集。

在當前工作目錄下,新建文件 apriori.py,添加如下代碼:

# -*-coding:utf-8 -*-
from numpy import *
"""
函數說明:加載數據集
"""
def loadDataSet():
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]
"""
函數說明:構建集合C1。即所有候選項元素的集合。
parameters:
    dataSet -數據集
return:
    frozenset列表
output:
    [forzenset([1]),forzenset([2]),……]
"""
def createC1(dataSet):
    C1 = []                     #創建一個空列表
    for transaction in dataSet: #對於數據集中的每條記錄
        for item in transaction:#對於每條記錄中的每一個項
            if not [item] in C1:       #如果該項不在C1中,則添加
                C1.append([item])
    C1.sort()                   #對集合元素排序
    return list(map(frozenset,C1))    #將C1的每個單元列表元素映射到forzenset()
"""
函數說明:構建符合支持度的集合Lk
parameters:
    D -數據集
    Ck -候選項集列表
    minSupport -感興趣項集的最小支持度
return:
    retList -符合支持度的頻繁項集合列表L
    supportData -最頻繁項集的支持度
"""
def scanD(D,Ck,minSupport):
    ssCnt = {}                                              #創建空字典
    for tid in D:                                           #遍歷數據集中的所有交易記錄 
        for can in Ck:                                      #遍歷Ck中的所有候選集
            if can.issubset(tid):                           #判斷can是否是tid的子集
                if not can in ssCnt:  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中
            retList.insert(0,key)
        supportData[key] = support                          #並記錄當前支持度,索引值即爲元素值
    return retList,supportData

並添加main函數:

if __name__ == '__main__':
    dataSet = loadDataSet()
    print("數據集:\n",dataSet)
    C1 = createC1(dataSet)
    print("候選集C1:\n",C1)
    D = list(map(set,dataSet))  #將數據轉換爲集合形式
    L1,supportData = scanD(D,C1,0.5)
    print("滿足最小支持度爲0.5的頻繁項集L1:\n",L1)

在當前目錄下,運行文件 python apriori.py,可以看到運行結果:
在這裏插入圖片描述

其中forzenset爲不可變集合,它的值是不可變的,好處是它可以作爲字典的key,也可以作爲其他集合的元素。
我們這裏必須使用forzenset而不是set,就是因爲我們需要將這些集合作爲字典鍵值使用。
至此,我們完成了C1和L1的構建。那麼對於L1中元素的組合,我們需要先引入一個組合定義。
假定我們的L1中元素爲[{0},{1},{2}],我們想要aprioriGen函數生成元素的組合C2,即[{0,1},{0,2},{1,2}]。
這是單個項的組合。那麼我們考慮一下,如果想利用[{0,1},{0,2},{1,2}]來創建三元素的候選項集C3呢?
如果依舊按照單個項的組合方法,將每兩個集合合併,就會得到{0,1,2},{0,1,2},{0,1,2}。也就是說,我們重複操作了3次。
接下來還需要掃描該結果來得到非重複結果,但我們要確保的是遍歷列表的次數最少。
那麼如何解決這個問題呢?我們觀察到,如果比較集合{0,1},{0,2},{1,2}的第一個元素並只對第一個元素相同的集合求並操作,我們會得到{0,1,2}相同的結果,但只操作了1次!
這樣的話就不需要遍歷列表來尋找非重複值。也能夠實現apriori算法的原理。
當我們創建三元素的候選集時,k=3,但此時待組合的頻繁項集元素爲f=2,即我們只需比較前f-1個元素,也就是前k-2個元素。

打開apriori.py文件,在main函數的上面添加如下代碼:

"""
函數說明:構建集合Ck
parameters:
    Lk -頻繁項集列表L
    k -候選集的列表中元素項的個數
return:
    retList -候選集項列表Ck
"""
def aprioriGen(Lk,k):
    retList = []                                            #創建一個空列表
    lenLk = len(Lk)                                         #得到當前頻繁項集合列表中元素的個數
    for i in range(lenLk):                                  #遍歷所有頻繁項集合
        for j in range(i+1,lenLk):                          #比較Lk中的每兩個元素,用兩個for循環實現
            L1 = list(Lk[i])[:k-2]; L2 = list(Lk[j])[:k-2]  #取該頻繁項集合的前k-2個項進行比較
            #[注]此處比較了集合的前面k-2個元素,需要說明原因
            L1.sort(); L2.sort()                            #對列表進行排序
            if L1 == L2:
                retList.append(Lk[i]|Lk[j])                 #使用集合的合併操作來完成 e.g.:[0,1],[0,2]->[0,1,2]
    return retList
"""
函數說明:apriori算法實現
parameters:
    dataSet -數據集
    minSupport -最小支持度
return:
    L -候選項集的列表
    supportData -項集支持度
"""
def apriori(dataSet,minSupport=0.5):
    C1 = createC1(dataSet)
    D = list(map(set,dataSet))                      #將數據集轉化爲集合列表
    L1, supportData = scanD(D,C1,minSupport)    #調用scanD()函數,過濾不符合支持度的候選項集
    L = [L1]                                    #將過濾後的L1放入L列表中
    k = 2                                       #最開始爲單個項的候選集,需要多個元素組合
    while(len(L[k-2])>0):
        Ck = aprioriGen(L[k-2],k)               #創建Ck
        Lk, supK = scanD(D,Ck,minSupport)       #由Ck得到Lk
        supportData.update(supK)                #更新支持度
        L.append(Lk)                            #將Lk放入L列表中
        k += 1                                  #繼續生成L3,L4....
    return L, supportData

並修改main函數:

if __name__ == '__main__':
    dataSet = loadDataSet()
    print("數據集:\n",dataSet)
    C1 = createC1(dataSet)
    print("候選集C1:\n",C1)
    D = list(map(set,dataSet))  #將數據轉換爲集合形式
    L1,supportData = scanD(D,C1,0.5)
    print("滿足最小支持度爲0.5的頻繁項集L1:\n",L1)
    L,suppData = apriori(dataSet)
    print("滿足最小支持度爲0.5的頻繁項集列表L:\n",L)
    print("L2:\n",L[1])
    print("使用L2生成的C3:\n",aprioriGen(L[1],3))
    
    L,suppData = apriori(dataSet)
    print("滿足最小支持度爲0.7的頻繁項集列表L:\n",L)

在當前目錄下,運行文件 python apriori.py,可以看到運行結果:
在這裏插入圖片描述

可以看到,使用L2生成的候選項集C3,只對k-2個項相同的元素合併。
那其實不生成{1,3,5}的原因是,由於{1,5}是非頻繁項,那麼其超集均爲非頻繁項。故此極大減少計算量。

到此我們也得到了我們的最頻繁項集列表。

發現關聯規則

上一節介紹瞭如何使用Apriori算法來發現頻繁集,現在需要解決的問題則是如何找出關聯規則

在原理講解中,我們找到諸如“{尿布}->{葡萄酒}”的關聯規則。這意味着如果有人買了尿布,那麼他很可能也會買葡萄酒。
但是,這一條反過來,卻不一定成立。有可能買葡萄酒的人,根本不會去買尿布。

那麼,成立的定義是什麼呢?還記得我們介紹的可信度度量值,對於關聯規則的量化方法,則是使用可信度。
對於關聯規則“P->H”,它的可信度被定義爲“支持度(P | H)/支持度§”。其中|符合在python中爲合併符。
下圖展示了一個項集{0,1,2,3}能夠產生的所有關聯規則。與找到頻繁項集方法類似,我們對生成的規則進行過濾,對於不滿足最小可信度要求的,則去掉該規則。
我們看到,僅僅4個元素的項集生成了15個關聯規則,如果能夠減少規則數目來確保問題的可解性,那麼計算量也會大大減小。
可以觀察到,如果某條規則不滿足最小可信度要求,那麼該規則的所有子集也不會滿足最小可信度要求
如圖中,規則“0,1,2->3”不滿足要求,那麼任何左部爲{0,1,2}子集的規則均不滿足要求。
在這裏插入圖片描述
找出關聯規則的過程中,首先從一個頻繁項集開始,接着創建一個規則列表,其中規則右部只包含一個元素,然後對這些規則進行測試

接下來,合併所有剩餘規則來創建一個新的規則列表,其中規則右部包含兩個元素。直到規則左右剩餘一個元素。
同樣,我們需要創建相應的功能函數來實現:

1.rulesFromConseq -從頻繁項集生成規則列表

2.calcConf -對規則進行測試並過濾

3.generateRules -集成函數

我們來看下這種方法的實際效果,打開文件apriori.py文件,在main函數的上面添加如下代碼:

"""
函數說明:規則構建函數
parameters:
    freqSet -頻繁項集合
    H -可以出現在規則右部的元素列表
    supportData -支持度字典
    brl -規則列表
    minConf -最小可信度
return:
    null
"""
def rulesFromConseq(freqSet,H,supportData,brl,minConf=0.7):
    m = len(H[0])                                               #得到H中的頻繁集大小m
    if (len(freqSet) > (m+1)):                                  #查看該頻繁集是否大到可以移除大小爲m的子集
        Hmp1 = aprioriGen(H, m+1)                               #構建候選集Hm+1,Hmp1中包含所有可能的規則
        Hmp1 = calcConf(freqSet,Hmp1,supportData,brl,minConf)   #測試可信度以確定規則是否滿足要求
        if (len(Hmp1)>1):                                       #如果不止一條規則滿足要求,使用函數迭代判斷是否能進一步組合這些規則
            rulesFromConseq(freqSet,Hmp1,supportData,brl,minConf)
        
"""
函數說明:計算規則的可信度,找到滿足最小可信度要求的規則
parameters:
    freqSet -頻繁項集合
    H -可以出現在規則右部的元素列表
    supportData -支持度字典
    brl -規則列表
    minConf -最小可信度
return:
    prunedH -滿足要求的規則列表
"""
def calcConf(freqSet,H,supportData,brl,minConf=0.7):
    prunedH = []                                                #爲保存滿足要求的規則創建一個空列表
    for conseq in H: 
        conf = supportData[freqSet]/supportData[freqSet-conseq] #可信度計算[support(PUH)/support(P)]
        if conf>=minConf:
            print(freqSet-conseq,'-->',conseq,'可信度爲:',conf)
            brl.append((freqSet-conseq,conseq,conf))            #對bigRuleList列表進行填充
            prunedH.append(conseq)                              #將滿足要求的規則添加到規則列表
    return prunedH
"""
函數說明:關聯規則生成函數
parameters:
    L -頻繁項集合列表
    supportData -支持度字典
    minConf -最小可信度
return:
    bigRuleList -包含可信度的規則列表
"""
def generateRules(L,supportData,minConf=0.7):
    bigRuleList = []                                        #創建一個空列表
    for i in range(1,len(L)):                               #遍歷頻繁項集合列表
        for freqSet in L[i]:                                #遍歷頻繁項集合
            H1 = [frozenset([item]) for item in freqSet]    #爲每個頻繁項集合創建只包含單個元素集合的列表H1
            if (i>1):                                       #要從包含兩個或者更多元素的項集開始規則構建過程
                rulesFromConseq(freqSet,H1,supportData,bigRuleList,minConf)
            else:                                           #如果項集中只有兩個元素,則計算可信度值,(len(L)=2)
                calcConf(freqSet,H1,supportData,bigRuleList,minConf)
    return bigRuleList

並修改main函數爲:

if __name__ == '__main__':
    dataSet = loadDataSet()
    print("數據集:\n",dataSet)
    C1 = createC1(dataSet)
    print("候選集C1:\n",C1)
    L,suppData = apriori(dataSet)
    print("滿足最小支持度爲0.5的頻繁項集列表L:\n",L)
    print("滿足最小可信度爲0.7的規則列表爲:")
    rules = generateRules(L,suppData,0.7)
    print(rules)
    print("滿足最小可信度爲0.5的規則列表爲:")
    rules1 = generateRules(L,suppData,0.5)
    print(rules1)

在當前目錄下,運行文件 python apriori.py,可以看到運行結果:
在這裏插入圖片描述
更改最小置信度後,可以獲得更多的規則。

FP-Growth算法

我們都用過搜索引擎,會發現這樣一個功能:輸入一個單詞或者單詞的一部分,搜索引擎會自動補全查詢詞項。用戶甚至都不知道搜索引擎推薦的東西是否存在,反而會去查找推薦詞項。比如在百度輸入“怎麼才能”開始查詢時,會出現諸如“怎麼才能減肥”之類的推薦結果。

爲了給出這些推薦查詢詞,搜索引擎公司的研究人員通過查看互聯網上的用詞來找出經常在一塊出現的詞對,這需要一種高效發現頻繁集的方法。

在前面我們講過使用Apriori算法來發現數據集中的頻繁項集。對於不同元素項間的組合,我們使用Apriori原理減少在數據庫上進行檢查的集合的數目,從而避免了大量的計算。但每次增加頻繁項集的大小,由於Apriori算法對於每個潛在的頻繁項集都會掃描數據集判定給定模式是否頻繁,會多次掃描整個數據集。當數據集很大時,這會顯著降低頻繁項集發現的速度。

1.FP-Growth算法

FP-Growth(Frequent-Pattern)算法基於Apriori構建,但在完成相同任務時採用了一些不同的技術。

不同於Apriori算法的”產生-測試”,這裏的任務是將數據集存儲在一個特定的稱做FP樹的結構之後發現頻繁項集或者頻繁項對,即常在一塊出現的元素項的集合FP樹。

常見的頻繁項集挖掘算法有兩類,一類是Apriori算法,另一類是FP-Growth。

Apriori通過不斷的構造候選集、篩選候選集挖掘出頻繁項集,需要多次掃描原始數據,當原始數據較大時,磁盤I/O次數太多,效率比較低下。FP-Growth算法則只需掃描原始數據兩遍,通過FP樹數據結構對原始數據進行壓縮,效率較高。因此FP-Growth算法的執行速度要比Apriori算法快得多。

FP-Growth算法主要分爲兩個步驟:
​ - 構建FP樹
​ - 從FP樹中挖掘頻繁項集

2.FP樹

我們已經知道,FP-Growth算法將數據存儲在一種稱爲FP樹的數據結構中。那麼,FP樹長什麼樣呢?

FP(Frequent Pattern,頻繁模式)樹看上去和其他的樹結構類似,但它通過**鏈接(link)**來連接相似元素,被連起來的元素項可以看作一個鏈表。

如下圖中,虛線所示,每個被虛線相連的元素即爲相似元素。通過這種相似元素相連(node link),我們可以快速的發現相似項的位置。
在這裏插入圖片描述

我們需要注意的是,與搜索樹不同的是,一個元素項可以在一棵FP樹中重複出現。FP樹會存儲項集的出現頻率,每個項集以路徑的方式存儲在樹中。
由於不同的集合可能會有若干個相同的項,因此它們的路徑可能部分重疊,即存在相似元素的集合會共享樹的一部分。只有當集合之間完全不同時,樹纔會分叉。
樹節點上給出集合中的單個元素及其在序列中的出現次數路徑則會給出該序列的出現次數

理論看起來可能有些迷糊。我們通過一個例子來具體瞭解下,下表中爲上圖所示FP樹的數據。
在這裏插入圖片描述

在圖中我們看到,元素項z出現了5次,集合{r,z}出現了1次。路徑的出現次數由路徑的末端節點次數決定。

此時我們可以看出,元素項z除了在集合{r,z}中出現了一次,一定是與其他符合或者自身出現了4次。集合{t,s,y,x,z}出現了2次,集合{t,r,y,x,z}出現了1次,也就是說z一定是單獨出現了1次。
將我們的結論與表中數據比較,發現集合005中{t,r,y,x,z}出現了1次,但少了q和p元素項。那麼在構建FP樹的時候,它們去哪了呢?

還記得我們在Apriori算法中提到的支持度定義,即最小閾值,低於最小閾值的元素項被認爲是不頻繁的。這裏同樣需要對數據項進行過濾,如果項集的出現次數小於我們設定的閾值,則丟掉。因此,q和p被del了。

接下來,我們學習如何構建FP樹以及從FP樹中挖掘頻繁項集。

構建FP樹數據結構

構建FP樹是FP-Growth算法的第一步。我們已經知道,FP樹的節點會存儲節點的值以及出現次數,並需要記錄相似節點的位置。需要爲其定義一個類來封裝這麼多的內容。

在當前目錄下,新建文件,添加如下代碼:

"""
類說明:FP樹數據結構
function:
    __init__ -初始化節點
        nameValue -節點值
        numOccur -節點出現次數
        parentNode -父節點
    inc -對count變量增加給定值
    disp -將樹以文本形式顯示
"""
class treeNode:
    def __init__(self, nameValue, numOccur, parentNode):
        self.name = nameValue   #存放節點名字
        self.count = numOccur   #節點計數值
        self.nodeLink = None    #鏈接相似的元素值
        self.parent = parentNode    #當前節點的父節點
        self.children = {}  #空字典變量,存放節點的子節點
    
    def inc(self, numOccur):
        self.count += numOccur
    
    def disp(self, ind=1): #ind爲節點的深度
        print(' '*ind, self.name, ' ', self.count)
        for child in self.children.values():
            child.disp(ind+1)   #遞歸遍歷樹的每個節點

並添加main函數:

if __name__ == '__main__':
    rootNode = treeNode('pyramid',9,None)
    rootNode.children['eye'] = treeNode('eye',13,None)
    rootNode.disp()

運行後,可以看到,我們構建一個有兩個結點的樹。
其中,nodelink 和 parent目前還未使用。
nodelink記錄相似節點的位置,parent記錄當前節點的父節點,用於尋找頻繁項集。類中還包含一個空字典變量children,用來存放節點的子節點。

構建FP樹

構建好FP樹所需的數據結構之後,下面就可以構建FP樹了。

我們除了要構建FP樹之外,還需要一個頭指針表來指向給定類型的第一個實例。下圖是頭指針表的示意圖。利用頭指針表,可以快速找到FP樹中一個給定類型的所有元素
在這裏插入圖片描述

我們使用一個字典作爲數據結構,來保存頭指針表。該結構還可以用來保存FP樹中每類元素的總數。

FP-Growth算法會掃描數據集兩次。第一次掃描數據集,記錄每個獨立元素項的出現次數,並過濾不滿足最小支持度的元素項,剩餘的元素項即爲頻繁項,存儲在頭指針表中。第二次掃描數據集,讀入每個項集中的頻繁項,並將其添加到一條已經存在的路徑中。如果該路徑不存在,則創建一條新路徑。

這裏,我們需要注意一個問題,FP-Growth算法會儘可能的將數據壓縮,假設有集合{z,x,y}和{y,z,r},會被添加到兩條路徑中。但在FP樹中,相同的項應該只表示一次。如何解決這個問題呢?

在將集合添加到樹之前,需要對每個集合進行排序。排序基於元素項的絕對出現頻率來進行。我們使用頭指針表中存儲的元素項的出現次數,對上節中表數據進行排序,得到以下結果:
在這裏插入圖片描述

這樣,我們就可以構建FP樹了。從空集開始,向其中不斷添加頻繁項集過濾排序後的集合依此加入到樹中。

添加的過程如下圖所示。
在這裏插入圖片描述

我們已經大致瞭解了FP-Growth算法構建FP樹的思想,下面我們需要定義相應的功能函數:
​ 1.createTree -對數據過濾、排序
​ 2.updateTree -將處理後的集合加入到樹中
​ 3.updateHeader -對新加入的類型節點需要更新頭指針表中的實例,並更新該類型節點的nodeLink鏈表

打開文件fp.py,在main函數之前加入如下代碼:

"""
函數說明:FP樹構建函數
parameters:
    dataSet -字典型數據集
    minSup -最小支持度
return:
    retTree -FP樹
    headerTable -頭指針表
"""
def createTree(dataSet, minSup = 1):
    headerTable = {}    #創建空字典,存放頭指針
    for trans in dataSet: #遍歷數據集
        for item in trans:  #遍歷每個元素項
            headerTable[item] = headerTable.get(item,0)+dataSet[trans]  #以節點爲key,節點的次數爲值
    tmpHeaderTab = headerTable.copy()
    for k in tmpHeaderTab.keys():    #遍歷頭指針表
        if headerTable[k] < minSup:     #如果出現次數小於最小支持度
            del(headerTable[k])     #刪掉該元素項
    freqItemSet = set(headerTable.keys())   #將字典的鍵值保存爲頻繁項集合
    if len(freqItemSet) == 0: return None, None #如果過濾後的頻繁項爲空,則直接返回
    for k in headerTable:
        headerTable[k] = [headerTable[k], None] #使用nodeLink
        #print(headerTable)
    retTree = treeNode('Null Set',1,None) #創建樹的根節點
    for tranSet, count in dataSet.items():    #再次遍歷數據集
        localD = {} #創建空字典
        for item in tranSet:
            if item in freqItemSet: #該項是頻繁項
                localD[item] = headerTable[item][0] #存儲該項的出現次數,項爲鍵值
        if len(localD) > 0: 
            orderedItems = [v[0] for v in sorted(localD.items(),key=lambda p: p[1],reverse = True)] #基於元素項的絕對出現頻率進行排序
            updateTree(orderedItems, retTree, headerTable, count)   #使用orderedItems更新樹結構
    return retTree, headerTable

    
"""
函數說明:FP樹生長函數
parameters:
    items -項集
    inTree -樹節點
    headerTable -頭指針表
    count -項集出現次數
return:
    None
"""
def updateTree(items, inTree, headerTable, count):
    if items[0] in inTree.children:     #首先測試items的第一個元素項是否作爲子節點存在
        inTree.children[items[0]].inc(count)  #如果存在,則更新該元素項的計數
    else:
        inTree.children[items[0]] = treeNode(items[0],count,inTree) #如果不存在,創建一個新的treeNode並將其作爲子節點添加到樹中
        if headerTable[items[0]][1] == None:    #將該項存到頭指針表中的nodelink
            headerTable[items[0]][1] = inTree.children[items[0]]    #記錄nodelink
        else:
            updateHeader(headerTable[items[0]][1], inTree.children[items[0]]) #若已經存在nodelink,則更新至鏈表尾
    if len(items) > 1:
        updateTree(items[1::],inTree.children[items[0]],headerTable,count)  #迭代,每次調用時會去掉列表中的第一個元素
    

    
"""
函數說明:確保節點鏈接指向樹中該元素項的每一個實例
parameters:
    nodeToTest -需要更新的頭指針節點
    targetNode -要指向的實例
return:
    None
"""
def updateHeader(nodeToTest, targetNode):
    while(nodeToTest.nodeLink!=None):   #從頭指針表的nodelink開始,直到達到鏈表末尾
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode    #記錄當前元素項的實例
"""
FP樹測試函數
"""        
def testFPtree():
    simpDat = loadSimpDat()
    initSet = createInitSet(simpDat)
    print("字典數據集:\n",initSet)
    myFPtree, myHeaderTab = createTree(initSet,3)
    myFPtree.disp()
    return myHeaderTab

並添加初始的數據集,並將數據轉換成字典類型:

def loadSimpDat():
    simpDat = [['r', 'z', 'h', 'j', 'p'],
               ['z', 'y', 'x', 'w', 'v', 'u', 't', 's'],
               ['z'],
               ['r', 'x', 'n', 'o', 's'],
               ['y', 'r', 'x', 'z', 'q', 't', 'p'],
               ['y', 'z', 'x', 'e', 'q', 's', 't', 'm']]
    return simpDat
"""
函數說明:從列表到字典的類型轉換函數
parameters:
    dataSet -數據集列表
return:
    retDict -數據集字典
"""
def createInitSet(dataSet):
    retDict = {}
    for trans in dataSet:
        retDict[frozenset(trans)] = 1   #將列表項轉換爲forzenset類型並作爲字典的鍵值,值爲該項的出現次數
    return retDict

修改main函數爲:

if __name__ == '__main__':
    testFPtree()

運行文件後,可以看到我們構建的FP樹的結構,我們可以驗證一下與上圖中所示的樹是否等價。
在這裏插入圖片描述
上一節實現了構建FP樹的代碼。有了FP樹,我們就可以抽取頻繁集了。

與Apriori算法類似,首先從單元素項集合開始,然後在此基礎上逐步構建更大的集合。與Apriori算法不同的是,這裏使用FP樹實現上述過程,而不是每次需要遍歷原始數據集來驗證數據的支持度。

從FP樹中抽取頻繁項集主要分爲三個步驟:
​ (1)從FP樹中獲得條件模式基
​ (2)利用條件模式基,爲每一個條件模式基創建相對應的條件FP樹
​ (3)迭代重複上述兩個步驟,直到樹中只包含空集元素項爲止。

1.抽取條件模式基

條件模式基(conditional pattern base,CPB)是以所查找元素項爲結尾的路徑集合。每一條路徑其實都是所查找元素項的前綴路徑(prefix path)。前綴路徑是介於所查找元素項與數根節點之間的所有內容。

下表列出了上節中每一個頻繁項的所有前綴路徑以及路徑頻繁度。每條路徑都與一個計數值關聯,該計數值等於起始元素項的計數值,該計數值給了每條路徑上起始元素項的數據。
在這裏插入圖片描述

抽取條件基的步驟大致如下:

​ 1.從頭指針表(header table)的最下面的元素項開始,構造每個元素項的條件模式基

​ 2.順着頭指針表中元素項的鏈表,找出所有包含該元素項的前綴路徑,這些前綴路徑就是該元素項的條件模式基

​ 3.所有這些條件模式基的計數值爲該路徑上元素項的計數值,也稱作頻繁度

如下圖中,是對頭指針表HeaderTable創建的FP樹,併爲每個頻繁項構建條件模式基。包含p的其中一條路徑是fcamp,該路徑中p的頻繁度爲2,則該條件模式基fcam的頻繁度爲2。
在這裏插入圖片描述

接下來,需要定義相應的功能函數。

該函數應該能完成對頭指針表中包含的指向相同類型元素鏈表的指針進行訪問,遍歷該鏈表,對鏈表上的每個項,向上回溯這棵樹直到根節點爲止。

打開我們的文件fp.py,添加如下代碼:

這裏我們創建了兩個函數,由於對元素項的回溯是一個重複操作,單獨作爲一個函數會更好。在對相同類型元素鏈表進行遍歷時,用到了nodelink變量;在回溯時,用到了parent變量。從代碼中來體會這兩個變量的作用。

"""
函數說明:上溯FP樹
parameters:
    leafNode -節點
    prefixPath -該節點的前綴路徑
return:
    None
"""
def ascendTree(leafNode, prefixPath):
    if leafNode.parent != None: #如果該節點的父節點存在
        prefixPath.append(leafNode.name)    #將其加入到前綴路徑中
        ascendTree(leafNode.parent,prefixPath)  #迭代調用自身上溯
"""
函數說明:遍歷某個元素項的nodelink鏈表
parameters:
    basePat -頭指針表中元素
    treeNode -該元素項的nodelist鏈表節點
return:
    condPats -該元素項的條件模式基
"""
def findPrefixPath(basePat, treeNode):
    condPats = {} #創建空字典,存放條件模式基
    while treeNode != None:
        prefixPath = []
        ascendTree(treeNode, prefixPath)    #尋找該路徑下實例的前綴路徑
        if len(prefixPath)>1:   #如果有前綴路徑
            condPats[frozenset(prefixPath[1:])] = treeNode.count #記錄該路徑的出現次數,出現次數爲該路徑下起始元素項的計數值
            #此處需說明
        treeNode = treeNode.nodeLink
    return condPats
def testPrefix(myHeaderTab):
    print(findPrefixPath('x',myHeaderTab['x'][1]))
    print(findPrefixPath('z',myHeaderTab['z'][1]))
    print(findPrefixPath('r',myHeaderTab['r'][1]))

修改我們的main函數:

if __name__ == '__main__':
    myHeaderTab = testFPtree()    
    testPrefix(myHeaderTab)

運行後,可以看到元素項’x’,‘z’,'r’的條件模式基,可以與表中結果對照。
在這裏插入圖片描述

有了條件模式基之後,我們就可以創建條件FP樹。

2.創建條件FP樹

對於每一個頻繁項,都要爲其創建一棵條件FP樹。

創建條件FP樹的步驟如下:

​ 使用上一步得到的條件模式基作爲輸入數據,來爲每一個條件模式基構造條件FP樹。然後,我們會遞歸的發現頻繁項,發現條件模式基,以及發現另外的條件樹。

下圖是元素項t的條件FP樹的構建過程:
在這裏插入圖片描述

同創建FP樹一樣,條件FP樹同樣會將條件模式基中不符合支持度的元素項過濾。如圖中,{s},{r}在t的條件樹中分別出現了2次和1次,不符合最小支持度爲3,也就是非頻繁的。對t創建好FP樹後,接下來對{t,z},{t,y},{t,x}挖掘對應的條件樹。該過程重複進行,直到條件樹中沒有元素爲止

打開我們的文件fp.py,添加如下代碼:

這裏我們尋找頻繁項集之前,先對頭指針表中的項按照其出現頻率進行從小到大的排序。這是因爲由於更頻繁的元素項放在樹的上層會被更多的共享,否則會造成頻繁出現的元素項出現在樹的分支中,無法共用前綴

如下圖:
在這裏插入圖片描述

{f,a,c,m,p}和{a,f,c,p,m}在FP樹中應被看作是同一路徑,但由於沒有對頭指針表進行排序操作,無法共用前綴。造成多餘的分叉。

"""
函數說明:在FP樹中尋找頻繁項
parameters:
    inTree -FP樹
    headerTable -當前元素前綴路徑的頭指針表
    minSup -最小支持度
    preFix -當前元素的前綴路徑
    freqItemList -頻繁項集
return:
    None
"""
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
    #print("mineTreeHander:",headerTable)
    bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1][0])] #按照出現次數從小到大排序
    #print("bigL:",bigL)
    for basePat in bigL:  #從最少次數的元素開始
        newFreqSet = preFix.copy()  #複製前綴路徑
        newFreqSet.add(basePat)     #將當前元素加入路徑
        #print ('finalFrequent Item: ',newFreqSet)    #append to set
        freqItemList.append(newFreqSet)     #將該項集加入頻繁項集
        condPattBases = findPrefixPath(basePat, headerTable[basePat][1])    #找到當前元素的條件模式基
        #print ('condPattBases :',basePat, condPattBases)
        myCondTree, myHead = createTree(condPattBases, minSup)  #過濾低於閾值的item,基於條件模式基建立FP樹
        #print ('head from conditional tree: ', myHead)
        if myHead != None: #如果FP樹中存在元素項
            #print ('conditional tree for: ',newFreqSet)
            #myCondTree.disp(1)    
            # 遞歸的挖掘每個條件FP樹,累加後綴頻繁項集        
            mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)  #遞歸調用自身函數,直至FP樹中沒有元素
"""
找到頻繁集測試函數
"""
def testAll():
    simpDat = loadSimpDat()
    initSet = createInitSet(simpDat)
    myTree, headerTab = createTree(initSet,3)
    freqItems = []
    mineTree(myTree,headerTab,3,set([]),freqItems)
    print("頻繁項集爲:\n",freqItems)

修改main函數爲:

if __name__ == '__main__':
    testAll()

運行後可以看到,返回的項集即爲我們要尋找的頻繁項集,可以檢查一下返回的項集是否與上節創建的條件樹匹配。
在這裏插入圖片描述

到此,完整的FP-Growth算法已經可以運行。

閉頻繁項集和極大頻繁項集

  • 閉頻繁項集:如果這個頻繁項集的支持度和所有包含該頻繁項集的頻繁項集的支持度不相等,即爲閉頻繁項集。
    舉例:數據庫中僅有兩個事務 {1,2,3},{1,2}。設最小支持度閾值爲1,那麼{1,2,3},{1,2}均爲閉頻繁項集,因爲{1,2}的支持度爲2,{1,2,3}的支持度爲1,不相等。
  • 極大頻繁項集:該頻繁項集的真超項集都是不頻繁的,則該項爲頻繁項集。真超項集,就是A中有B的所有元素,且有B沒有的元素,則稱A是B的真超集。
    舉例:以上個例子中,{1,2,3}爲極大頻繁項集,但不能說{1,2}是極大頻繁項集,因爲它的超集{1,2,3}也是頻繁的。

模式評估度量方法

  1. 支持度和置信度

支持度(Support)
支持度表示項集{X,Y}在總項集裏出現的概率。公式爲:
Support(X→Y) = P(X,Y) / P(I) = P(X∪Y) / P(I) = num(XUY) / num(I)
其中,I表示總事務集。num()表示求事務集裏特定項集出現的次數。
比如,num(I)表示總事務集的個數
num(X∪Y)表示含有{X,Y}的事務集的個數(個數也叫次數)。
置信度 (Confidence)
置信度表示在先決條件X發生的情況下,由關聯規則”X→Y“推出Y的概率。即在含有X的項集中,含有Y的可能性,公式爲:
Confidence(X→Y) = P(Y|X) = P(X,Y) / P(X) = P(XUY) / P(X)

  1. 提升度

提升度表示含有X的條件下,同時含有Y的概率,與Y總體發生的概率之比。
Lift(X→Y) = P(Y|X) / P(Y)
由於提升度Lift(X→Y) =1,表示X與Y相互獨立,即是否有X,對於Y的出現無影響。也就是說,是否購買咖啡,與有沒有購買茶葉無關聯。即規則”茶葉→咖啡“不成立,或者說關聯性很小,幾乎沒有,雖然它的支持度和置信度都高達90%,但它不是一條有效的關聯規則。
滿足最小支持度和最小置信度的規則,叫做“強關聯規則”。然而,強關聯規則裏,也分有效的強關聯規則和無效的強關聯規則。
如果Lift(X→Y)>1,則規則“X→Y”是有效的強關聯規則。
如果Lift(X→Y) <=1,則規則“X→Y”是無效的強關聯規則。
特別地,如果Lift(X→Y) =1,則表示X與Y相互獨立。

  1. 卡方分析
    在這裏插入圖片描述
  2. 全置信度
    在這裏插入圖片描述
    其中,在這裏插入圖片描述是A和B的最大支持度。因此,allconf(A,B)又稱兩個與A和B相關的關聯規則“A=>B”和“B=>A”的最小置信度。
  3. 最大置信度
    在這裏插入圖片描述
  4. Kulczynski
    在這裏插入圖片描述
  5. 餘弦
    在這裏插入圖片描述
  6. 不平衡比
    在這裏插入圖片描述
    IR爲0,則說明A.B兩個方向的蘊含相同。
    使用不同的度量方法,其給出的結果不同,由於數據中極易出現零值,應重點關注度量方法的零不變性。
    除了支持度和置信度是對規則的關聯進行分析,其餘屬性爲判斷規則的相關性分析。
    可以試試Kulc和不平衡比配合使用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章