用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这个元素。上述代码作的就是这个。
以此类推咱们就能够获得全部符合条件的子序列。