用python實現了一個沒有庫依賴的「純」 py-based PrefixSpan算法。html
Github 倉庫 https://github.com/Holy-Shine/PrefixSpan-pypython
首先對韓老提出的這個數據挖掘算法不清楚的能夠看下這個博客,講解很是細緻。個人實現也是基本照着這個思路。git
PrefixSpan算法原理總結github
再簡單提一下這個算法作了一件什麼事。算法
假設有多個時間序列串:app
串序號 | 序列串 |
---|---|
0 | 1, 4, 2, 3 |
1 | 0, 1, 2, 3 |
2 | 1, 2, 1, 3, 4 |
3 | 2, 1, 2, 4, 4 |
4 | 1, 1, 1, 2, 3 |
查看上面的5條記錄串,能夠發現 ==(1,2,3)== 這個子序列頻繁出現,那麼這頗有可能就是全部串中潛在的一種==序列模式==。舉個病毒攻擊的例子來講,全部設備在一天內遭受到的攻擊序列有一個公共子序列(攻擊類型,攻擊發起者ip),那麼這種子序列頗有多是同一個黑客組織發起的一次大規模的攻擊,PrefixSpan就是用來高效地檢測出這種潛在的序列模式。ide
整個算法實現大概120來行代碼,關鍵函數就4個,結構以下:函數
|__PrefixSpan(...) # PrefixSpan |__createC1(...) # 生成初始前綴集合 |__rmLowSup(...) # 刪除初始前綴集合中的低support集 |__psGen(...) # 生成新的候選前綴集 |__genNewPostfixDic(..) # 根據候選集生成新的後綴集合
假設咱們的數據集長這樣(對應上述表格):post
D = [ [1,4,2,3], [0, 1, 2, 3], [1, 2, 1, 3, 4], [2, 1, 2, 4, 4], [1, 1, 1, 2, 3], ]
其中每條數據表示一個序列。code
算法流程大體以下:
# 生成初始前綴集合和初始後綴集合 L1, postfixDic = createC1(D, minSup) # 定義結果集 L,放入初始後綴集和 L = [], k = 2 L.append(L1) # 前綴不斷增加1,生成新的前綴,當新的前綴集合大小=0的時候,循環退出 while len(L[k-2]) > 0: # 生成新的候選前綴集合(長度比以前的大1) Lk = psGen() # 根據前綴更新後綴集和 posfixDic = genNewPostfixDic() # 加入到結果集中 L.append(Lk) k+=1
首先來看下createC1
的代碼清單:
def createC1(D, minSup): '''生成第一個候選序列,即長度爲1的集合序列 ''' C1 = [] postfixDic={} lenD = len(D) for i in range(lenD): for idx, item in enumerate(D[i]): if tuple([item]) not in C1: postfixDic[tuple([item])]={} C1.append(tuple([item])) if i not in postfixDic[tuple([item])].keys(): postfixDic[tuple([item])][i]=idx L1, postfixDic = rmLowSup(D, C1, postfixDic, minSup) return L1, postfixDic
參數:
返回值:
初始前綴集合包含只含單個元素的集合,在調用rmLowSup
方法前,上述代碼的初始前綴集合C1
的結果爲:[(0,),(1,),(2),(3,),(4,)]
(其中每一個前綴用tuple的形式,主要是爲了可以hash);
postfixDic
是前綴集合C1
的後綴,它是一個Python字典,每一個元素表示當前前綴在數據集中某一條序列中最先出現的結尾位置(這樣處理,後續訪問後綴的時候,就不須要從頭開始遍歷了),例如運行完上述代碼後:
postfixDic[(1,)]={0:0, 1:1, 2:0, 3:1, 4:0}
回顧數據集D,能夠發現1在每一行都出現了,且在第一行(下標爲0)出現的結尾爲0,第二行位置爲1... (位置從0開始)
依次類推:
postfixDic[(1,2,3)]={0:3, 1:3, 2:3, 4:4}
表示前綴 (1,2,3)
在第 0,1,2,4 行都出現了,在第一行的結尾爲3,第二行爲3...
同時咱們能夠發現調用 len(postfixDic[prefix])
就能夠知道前綴prefix
在多少序列中出現了,據此能夠刪除低support 前綴
rmLowSup
函數清單以下:
def rmLowSup(D,Cx, postfixDic,minSup): ''' 根據當前候選集合刪除低support的候選集 ''' Lx = Cx for iset in Cx: if len(postfixDic[iset])/len(D) < minSup: Lx.remove(iset) postfixDic.pop(iset) return Lx, postfixDic
根據後綴集和postfixDic
的說明,前綴prefix
的支持度爲: len(postfixDic[prefix])/len(D)
, 例如上述前綴(1,2,3)
的支持度爲 4/5=0.8,低於閾值minSup
的前綴和其相應在postfixDic
中的key將被剔除。
psGen
代碼清單以下:
def psGen(D, Lk, postfixDic, minSup, minConf): '''生成長度+1的新的候選集合 ''' retList = [] lenD = len(D) # 訪問每個前綴 for Ck in Lk: item_count = {} # 統計item在多少行的後綴裏出現 # 訪問出現前綴的每一行 for i in postfixDic[Ck].keys(): # 從前綴開始訪問每一個字符 item_exsit={} for j in range(postfixDic[Ck][i]+1, len(D[i])): if D[i][j] not in item_count.keys(): item_count[D[i][j]]=0 if D[i][j] not in item_exsit: item_count[D[i][j]]+=1 item_exsit[D[i][j]]=True c_items = [] # 根據minSup和minConf篩選候選字符 for item in item_count.keys(): if item_count[item]/lenD >= minSup and item_count[item]/len(postfixDic[Ck])>=minConf: c_items.append(item) # 將篩選後的字符組成新的候選前綴,加入候選前綴集合 for c_item in c_items: retList.append(Ck+tuple([c_item])) return retList
對於當前前綴集(長度相等)中的每一個前綴,經過後綴集合postfixDic
能挖掘到其可能的下一個字符。例如前綴 (1,2)
的 postfixDic[(1,2)]={0:2, 1:2, 2:1, 3:2, 4:3}
, 表示在第0,1,2,3,4行都存在前綴(1,2)
, 經過其在每行的前綴結尾位置,例如第0行的結尾位置,能夠在[postfixDic[(1,2)][0], len(D[0]))
範圍內查找是否有符合條件的新元素,即第0行的 [2, 4) 範圍內搜索。
具體方法是統計後綴中不一樣元素分別在當前行是否出現,再統計它們出現的行數,查找過程以下表所示(對應函數清單的前半部分):
查找行/元素候選 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 0 |
1 | 0 | 0 | 0 | 1 | 0 |
2 | 0 | 1 | 0 | 1 | 1 |
3 | 0 | 0 | 1 | 0 | 1 |
4 | 0 | 0 | 0 | 1 | 0 |
總計 | 0 | 1 | 1 | 4 | 2 |
能夠看到候選元素3,4分別出現了4次和2次,則表示候選前綴 (1,2,3)
和(1,2,4)
在5行序列中的4行和2行中出現,能夠很快計算獲得它們的support值爲0.8和0.5。
傳統的PrefixSpan只在這裏用min_support的策略過濾候選前綴集,而代碼裏同時用了min_confidence 參數,這裏就不細講了。
一樣來看下代碼清單:
def genNewPostfixDic(D,Lk, prePost): '''根據候選集生成相應的PrefixSpan後綴 參數: D:數據集 Lk: 選出的集合 prePost: 上一個前綴集合的後綴 基於假設: (1,2)的後綴只能出如今 (1,)的後綴列表裏 e.g. postifixDic[(1,2)]={1:2,2:3} 前綴 (1,2) 在第一行的結尾是2,第二行的結尾是3 ''' postfixDic = {} for Ck in Lk: # (1,2)的後綴只能出如今 (1,)的後綴列表裏 postfixDic[Ck]={} tgt = Ck[-1] prePostList = prePost[Ck[:-1]] for r_i in prePostList.keys(): for c_i in range(prePostList[r_i]+1, len(D[r_i])): if D[r_i][c_i]==tgt: postfixDic[Ck][r_i] = c_i break return postfixDic
如今咱們要根據新的候選前綴集合更新後綴集和postfixDic
,爲此咱們須要舊的前綴集合 postfixDic
做爲輔助,能夠大大減少時間複雜度。
例如咱們更新前綴 (1,2,3)
的後綴,咱們不須要再從 D的第0行開始遍歷全部的序列。
由於(1,2,3)
必然來自前綴 (1,2)
,所以只要遍歷出現前綴 (1,2)
的行進行查找便可,這也是咱們須要舊的前綴集合的緣由。
接下來就簡單了,只要在這些行,找到新的元素的位置便可。例如對於前綴 (1,2)
,其後綴postfixDic[(1,2)]={0: 2, 1: 2, 2: 1, 3: 2, 4: 3}
,因此在0,1,2,3,4行的這些位置+1開始尋找是否存在3這個元素。上述代碼作的就是這個。
以此類推咱們就能夠獲得全部符合條件的子序列。