前言

用python实现了一个没有库依赖的“纯” py-based PrefixSpan算法。

Github 仓库 https://github.com/Holy-Shine/PrefixSpan-py

首先对韩老提出的这个数据挖掘算法不清楚的可以看下这个博客,讲解非常细致。我的实现也是基本照着这个思路。

PrefixSpan算法原理总结

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

假设有多个时间序列串:

串序号 序列串
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就是用来高效地检测出这种潜在的序列模式。

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) 范围内搜索。

具体方法是统计后缀中不同元素分别在当前行是否出现,再统计它们出现的行数,查找过程如下表所示(对应函数清单的前半部分):

查找行/元素候选 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 参数,这里就不细讲了。

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这个元素。上述代码做的就是这个。

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

数据挖掘经典算法PrefixSpan的一个简单Python实现的更多相关文章

  1. 数据挖掘经典算法——K-means算法

    算法描述 K-means算法是一种被广泛使用的基于划分的聚类算法,目的是将n个对象会分成k个簇.算法的具体描述如下: 随机选取k个对象作为簇中心: Do 计算所有对象到这k个簇中心的距离,将距离最近的 ...

  2. 一个简单python爬虫的实现——爬取电影信息

    最近在学习网络爬虫,完成了一个比较简单的python网络爬虫.首先为什么要用爬虫爬取信息呢,当然是因为要比人去收集更高效. 网络爬虫,可以理解为自动帮你在网络上收集数据的机器人. 网络爬虫简单可以大致 ...

  3. 测试网站页面网速的一个简单Python脚本

    无聊之余,下面分享一个Python小脚本:测试网站页面访问速度 [root@huanqiu ~]# vim pywww.py #!/usr/bin/python # coding: UTF-8 imp ...

  4. Java经典实例:实现一个简单堆栈

    堆栈(Stack)是一种常见的数据结构,符合后进先出(First In Last Out)原则,通常用于实现对象存放顺序的逆序.栈的基本操作有push(添加到堆栈),pop(从堆栈删除),peek(检 ...

  5. 数据挖掘经典算法之KNN

    KNN也称为k近邻算法,本质思想:物以类聚. 在分类或者预测中,待分类或预测的样本的类别和走势将直接参考与该样本最“近邻”的k个邻居. 在这种思路下,KNN注定会遇到3个问题: (1): 谁是我的邻居 ...

  6. 用JetBrains PyCharm 开发工具写一个简单python案例

    import urllib.request import re #解析html的内容 def getHtml(url): page=urllib.request.urlopen(url) html=p ...

  7. 一个简单的语义分析算法:单步算法——Python实现

    以前 曾经有一个人教会我一件事 要学会相信一些看似不可能的事  当你真的相信的时候  或许 没有什么事情是不可能的 ——<秦时明月•与子同归> 在编译原理的众多书籍中,陈述了很多生成语法树 ...

  8. ICDM评选:数据挖掘十大经典算法

    原文地址:http://blog.csdn.net/aladdina/article/details/4141177 国际权威的学术组织the IEEE International Conferenc ...

  9. sopt:一个简单的python最优化库

    引言     最近有些朋友总来问我有关遗传算法的东西,我是在大学搞数学建模的时候接触过一些最优化和进化算法方面的东西,以前也写过几篇博客记录过,比如遗传算法的C语言实现(一):以非线性函数求极值为例和 ...

随机推荐

  1. C结构体struct 和 共用体union的使用测试

    #include <stdio.h> struct { char name[10]; char sex; char job; int num; union{ //联合只能共用同一个内存 i ...

  2. AtCoder Grand Contest 019 题解

    传送门 \(A\) 咕咕 int a,b,c,d,n,t; int main(){ scanf("%d%d%d%d%d",&a,&b,&c,&d,& ...

  3. 【微信小程序】如何获取用户绑定手机号

    用户调用wx.login()方法,获取登录用户凭证code wx.login({ success: function(res) { console.log('loginCode', res.code) ...

  4. ORA-01033:ORACLE initialization or shutdown

    ora-01033 错误处理:DOS下依次执行以下命令sqlplus/nologconnect sys/密码@orcl as sysdbashutdown normalstartup mountSQL ...

  5. Hadoop 机架(集群拓扑)设置

    本文通过MetaWeblog自动发布,原文及更新链接:https://extendswind.top/posts/technical/hadoop_rack_awareness Hadoop会通过集群 ...

  6. fork()函数 图解

    code #include<stdio.h> #include <getopt.h> #include<iostream> #include<string&g ...

  7. 模板 - 数学 - 多项式 - 快速数论变换/NTT

    Huffman分治的NTT,常数一般.使用的时候把多项式的系数们放进vector里面,然后调用solve就可以得到它们的乘积.注意这里默认最大长度是1e6,可能需要改变. #include<bi ...

  8. 2、vueJs基础知识02

    vue生命周期: 钩子函数: created -> 实例已经创建 √ beforeCompile -> 编译之前 compiled -> 编译之后 ready -> 插入到文档 ...

  9. JS Array.apply会有内存泄漏问题

    报错内容: Maximum call stack size exceeded 参考:https://www.jianshu.com/p/b9ba0ddd3392 对象较多,前端JS内存溢出: 数组克隆 ...

  10. Flutter ------- WebView加载网页

    在Flutter 加载网页?也是有WebView的哦,和Android一样 1.添加依赖 dependencies: flutter_webview_plugin: ^0.2.1+2 2.导入库 im ...