数据挖掘经典算法PrefixSpan的一个简单Python实现

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

数据挖掘经典算法PrefixSpan的⼀个简单Python实现
前⾔
⽤python实现了⼀个没有库依赖的“纯” py-based PrefixSpan算法。

Github 仓库
⾸先对韩⽼提出的这个数据挖掘算法不清楚的可以看下这个博客,讲解⾮常细致。

我的实现也是基本照着这个思路。

再简单提⼀下这个算法做了⼀件什么事。

假设有多个时间序列串:
串序号序列串
01, 4, 2, 3
10, 1, 2, 3
21, 2, 1, 3, 4
32, 1, 2, 4, 4
41, 1, 1, 2, 3
查看上⾯的5条记录串,可以发现 (1,2,3) 这个⼦序列频繁出现,那么这很有可能就是所有串中潜在的⼀种序列模式。

举个病毒攻击的例⼦来说,所有设备在⼀天内遭受到的攻击序列有⼀个公共⼦序列(攻击类型,攻击发起者ip),那么这种⼦序列很有可能是同⼀个⿊客组织发起的⼀次⼤规模的攻击,PrefixSpan就是⽤来⾼效地检测出这种潜在的序列模式。

1. 代码概览
整个算法实现⼤概120来⾏代码,关键函数就4个,结构如下:
|__PrefixSpan(...) # PrefixSpan
|__createC1(...) # ⽣成初始前缀集合
|__rmLowSup(...) # 删除初始前缀集合中的低support集
|__psGen(...) # ⽣成新的候选前缀集
|__genNewPostfixDic(..) # 根据候选集⽣成新的后缀集合
2. 实现细节
假设我们的数据集长这样(对应上述表格):
D = [
[1,4,2,3],
[0, 1, 2, 3],
[1, 2, 1, 3, 4],
[2, 1, 2, 4, 4],
[1, 1, 1, 2, 3],
]
其中每条数据表⽰⼀个序列。

算法流程⼤致如下:
# ⽣成初始前缀集合和初始后缀集合
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
2.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
参数:
D:数据集
minSup: PrefixSpan算法的关键参数min_support
返回值:
L1:剔除低support集后的候选前缀集合
postfixDic: 对应候选集合的后缀集
前缀集合C1
初始前缀集合包含只含单个元素的集合,在调⽤rmLowSup⽅法前,上述代码的初始前缀集合C1的结果为:[(0,),(1,),(2),(3,),(4,)](其中每个前缀⽤tuple的形式,主要是为了能够hash);
后缀集合postfixDic
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 前缀
删除低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将被剔除。

2.2 ⽣成新的候选前缀集合
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) 范围内搜索。

具体⽅法是统计后缀中不同元素分别在当前⾏是否出现,再统计它们出现的⾏数,查找过程如下表所⽰(对应函数清单的前半部分):
查找⾏/元素候选01234
000010
100010
201011
300101
400010
总计01142
可以看到候选元素3,4分别出现了4次和2次,则表⽰候选前缀(1,2,3)和(1,2,4)在5⾏序列中的4⾏和2⾏中出现,可以很快计算得到它们的support值为0.8和0.5。

传统的PrefixSpan只在这⾥⽤min_support的策略过滤候选前缀集,⽽代码⾥同时⽤了min_confidence 参数,这⾥就不细讲了。

2.3 更新后缀集和postfixDic
同样来看下代码清单:
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这个元素。

上述代码做的就是这个。

以此类推我们就可以得到所有符合条件的⼦序列。

相关文档
最新文档