用Numpy实现高效的Apriori算法
By 苏剑林 | 2018-05-10 | 95612位读者 |三年前笔者曾写了《用Pandas实现高效的Apriori算法》,里边给出了Apriori算法的Python实现,并得到了一些读者的认可。然而,笔者当时的Python还学得并不好,所以现在看来那个实现并不优雅(但速度还过得去),而且还不支持变长的输入数据。而之前承诺过会重写这个算法,把上述问题解决掉,而现在总算完成了~
关于Apriori算法就不重复介绍了,直接放出代码:
#! -*- coding: utf-8 -*-
import numpy as np
class Apriori:
def __init__(self, min_support, min_confidence):
self.min_support = min_support # 最小支持度
self.min_confidence = min_confidence # 最小置信度
def count(self, filename='apriori.txt'):
self.total = 0 # 数据总行数
items = {} # 物品清单
# 统计得到物品清单
with open(filename) as f:
for l in f:
self.total += 1
for i in l.strip().split(','): # 以逗号隔开
if i in items:
items[i] += 1.
else:
items[i] = 1.
# 物品清单去重,并映射到ID
self.items = {i:j/self.total for i,j in items.items() if j/self.total > self.min_support}
self.item2id = {j:i for i,j in enumerate(self.items)}
# 物品清单的0-1矩阵
self.D = np.zeros((self.total, len(items)), dtype=bool)
# 重新遍历文件,得到物品清单的0-1矩阵
with open(filename) as f:
for n,l in enumerate(f):
for i in l.strip().split(','):
if i in self.items:
self.D[n, self.item2id[i]] = True
def find_rules(self, filename='apriori.txt'):
self.count(filename)
rules = [{(i,):j for i,j in self.items.items()}] # 记录每一步的频繁项集
l = 0 # 当前步的频繁项的物品数
while rules[-1]: # 包含了从k频繁项到k+1频繁项的构建过程
rules.append({})
keys = sorted(rules[-2].keys()) # 对每个k频繁项按字典序排序(核心)
num = len(rules[-2])
l += 1
for i in range(num): # 遍历每个k频繁项对
for j in range(i+1,num):
# 如果前面k-1个重叠,那么这两个k频繁项就可以组合成一个k+1频繁项
if keys[i][:l-1] == keys[j][:l-1]:
_ = keys[i] + (keys[j][l-1],)
_id = [self.item2id[k] for k in _]
support = 1. * sum(np.prod(self.D[:, _id], 1)) / self.total # 通过连乘获取共现次数,并计算支持度
if support > self.min_support: # 确认是否足够频繁
rules[-1][_] = support
# 遍历每一个频繁项,计算置信度
result = {}
for n,relu in enumerate(rules[1:]): # 对于所有的k,遍历k频繁项
for r,v in relu.items(): # 遍历所有的k频繁项
for i,_ in enumerate(r): # 遍历所有的排列,即(A,B,C)究竟是 A,B -> C 还是 A,B -> C 还是 A,B -> C ?
x = r[:i] + r[i+1:]
confidence = v / rules[n][x] # 不同排列的置信度
if confidence > self.min_confidence: # 如果某种排列的置信度足够大,那么就加入到结果
result[x+(r[i],)] = (confidence, v)
return sorted(result.items(), key=lambda x: -x[1][0]) # 按置信度降序排列
使用方法:
from pprint import pprint
# 测试文件来自 https://kexue.fm/archives/3380
model = Apriori(0.06, 0.75)
pprint(model.find_rules('apriori.txt'))
输出结果:
[(('A3', 'F4', 'H4'), (0.8795180722891566, 0.07849462365591398)),
(('C3', 'F4', 'H4'), (0.875, 0.07526881720430108)),
(('B2', 'F4', 'H4'), (0.7945205479452054, 0.06236559139784946)),
(('C2', 'E3', 'D2'), (0.7543859649122807, 0.09247311827956989)),
(('D2', 'F3', 'H4', 'A2'), (0.7532467532467533, 0.06236559139784946))]
结果的含义是前n-1项推出第n项,也就是A3 + F4 --> H4、 D2 + F3 + H4 --> A2等。
这次的实现相对简洁一点,去掉了Pandas库的依赖,只用到了Numpy库,变量命名也更清晰一些,编程思路没有变化,所以理论上效率不会降低,甚至有可能会提升。应该兼容Python 2.x和3.x,如果有问题欢迎继续指出。
转载到请包括本文地址:https://kexue.fm/archives/5525
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (May. 10, 2018). 《用Numpy实现高效的Apriori算法 》[Blog post]. Retrieved from https://kexue.fm/archives/5525
@online{kexuefm-5525,
title={用Numpy实现高效的Apriori算法},
author={苏剑林},
year={2018},
month={May},
url={\url{https://kexue.fm/archives/5525}},
}
May 15th, 2018
想请教一个问题,目前我在做在线学习系统中判断学习者注意力的问题,想法是基于摄像头获得图像,然后基于图像中的眼睛信息得到学习者在屏幕上的关注点,基于一段时间内的关注点轨迹来判断学习者的注意力是否集中。目前已经能够获得关注点轨迹了,请问依您来看基于基于关注点轨迹来判断注意力是否集中,使用什么方法比较合适呢?
这个要具体问题具体分析吧,我没做过相关的研究,不好给意见呀~
May 21st, 2018
请问为什么我复制的代码,报错:name 'min_support' is not defined
不好意思,之前疏忽了,已经修改过来了~
May 22nd, 2018
请问27行self.items = {i:j/self.total for i,j in items.items() if j/self.total > self.min_support};这是在建立{物品:支持度}的字典吗?
38行self.D[n, self.item2id[i]] = True,这是在干嘛
27行,你理解没错
38行,前面已经注释了是在构建0-1矩阵,如果你实在不了解,你把self.D打印出来看看不就行了~
May 25th, 2018
前面加self.
May 25th, 2018
与python包apyori(from apyori import apriori)中的算法对比了下
结果一致 速度慢了些 0.125999927521s VS 0.0299999713898s
# test
# 1 使用自己的model
# (min_support,min_confidence)
import time
t0=time.time()
model=Apriori(0.06,0.75)
res=model.find_rules('apriori.txt')
t1=time.time()
print t1-t0
for i in res:
print i
# # 打印所有频繁项(>min_support)的support 分层
# for i,item in enumerate(model.rules):
# print i+1,sorted(item.items(),key=lambda x:x[1])
#=================
# 2使用工具包
from apyori import apriori
t0=time.time()
transactions=[]
with open('apriori.txt') as f:
for line in f:
transactions.append(line.strip().split(','))
res=list(apriori(transactions,min_support=0.06,min_confidence=0.75))
t1=time.time()
print t1-t0
for i in res:
print i
#================
console:
0.125999927521
(('A3', 'F4', 'H4', 0), (0.8795180722891566, 0.07849462365591398))
(('C3', 'F4', 'H4', 0), (0.875, 0.07526881720430108))
(('B2', 'F4', 'H4', 0), (0.7945205479452054, 0.06236559139784946))
(('C2', 'E3', 'D2', 0), (0.7543859649122807, 0.09247311827956989))
(('D2', 'F3', 'H4', 'A2', 0), (0.7532467532467533, 0.06236559139784946))
0.0299999713898
RelationRecord(items=frozenset(['A3', 'F4', 'H4']), support=0.07849462365591398, ordered_statistics=[OrderedStatistic(items_base=frozenset(['A3', 'F4']), items_add=frozenset(['H4']), confidence=0.8795180722891566, lift=1.9709682101901582)])
RelationRecord(items=frozenset(['F4', 'H4', 'B2']), support=0.06236559139784946, ordered_statistics=[OrderedStatistic(items_base=frozenset(['F4', 'B2']), items_add=frozenset(['H4']), confidence=0.7945205479452054, lift=1.7804918303350388)])
RelationRecord(items=frozenset(['C2', 'D2', 'E3']), support=0.09247311827956989, ordered_statistics=[OrderedStatistic(items_base=frozenset(['C2', 'E3']), items_add=frozenset(['D2']), confidence=0.7543859649122807, lift=1.8708771929824564)])
RelationRecord(items=frozenset(['C3', 'F4', 'H4']), support=0.07526881720430108, ordered_statistics=[OrderedStatistic(items_base=frozenset(['C3', 'F4']), items_add=frozenset(['H4']), confidence=0.875, lift=1.9608433734939759)])
RelationRecord(items=frozenset(['A2', 'D2', 'H4', 'F3']), support=0.06236559139784946, ordered_statistics=[OrderedStatistic(items_base=frozenset(['F3', 'D2', 'H4']), items_add=frozenset(['A2']), confidence=0.7532467532467533, lift=1.9732943113224803)])
[Finished in 0.5s]
我看了一下apyori的实现,纯py实现,很不错。
我这里调用了numpy包,在数据转换上花了一定时间。估计会对大数据集上会有优势,但不确定。
实验了一下,41万条数据,2千项,在博主的源代码的基础上,使用csr_matrix替换numpy,可以提升3倍速度。但是使用apyori可以提升几十倍。
June 15th, 2018
非常感谢你的代码,因为我正好需要使用, 另外我还搜索了其他算法,如 Eclat算法实现(效率高):
https://www.cnblogs.com/infaraway/p/6774521.html 这个网址的代码输出结果我不能理解,且似乎都没有置信度选项. 反正我对算法基本是眼盲。另外我试图对你的代码做进一步速度优化,主要是去掉了不必要的循环,但是在后面算法部分,做不到了。代码贴在下面:
x = np.array([[1,3,4],[2,3,5],[1,2,3,5],[2,5]])
min_support = 0.28 ; min_confidence = 0.2
# 取得记录数量
total = len(x)
# 商品去重,并取得每件商品出现的次数 (返回商品列表/出现次数)
sp, cs =np.unique(np.concatenate(x),return_counts=True)
# 计算最小支持度 ; 支持度mask(物品出现次数在交易次数中的占比)
cs = cs/float(total) ; mask = cs > min_support
# 重新整理商品列表,及支持度表
item = sp[mask] ; supp = cs[mask]
# 生成一个索引表
itiD = np.arange(item.size,dtype=np.int32)
# 物品清单的0-1矩阵 记录行/商品列
D = np.zeros((total, item.size), dtype=bool)
# 遍历所有记录行 (因为记录不等长,矢量方式费劲,因此用循环遍历)
for n,l in enumerate(x):
# 行商品有效标记
D[n,np.where(np.in1d(item,l))[0]] = True
#!!! 这里没用商品名,直接用商品序号代入,省去后面获取id操作 !!!
l = 0 ; rules = [{(i,):j for i,j in zip(itiD,supp)}]# 记录每一步的频繁项集
# 包含了从k频繁项到k+1频繁项的构建过程
while rules[-1]:
rules.append({})
keys = sorted(rules[-2].keys()) # 对每个k频繁项按字典序排序(核心)
num = len(rules[-2])
l += 1
for i in range(num): # 遍历每个k频繁项对
for j in range(i+1,num):
# 如果前面k-1个重叠,那么这两个k频繁项就可以组合成一个k+1频繁项
if keys[i][:l-1] == keys[j][:l-1]:
_ = keys[i] + (keys[j][l-1],)
support = 1. * sum(np.prod(D[:, _], 1))/total # 通过连乘获取共现次数,并计算支持度
if support > min_support: # 确认是否足够频繁
rules[-1][_] = support
# 遍历每一个频繁项,计算置信度
result = {}
for n,relu in enumerate(rules[1:]): # 对于所有的k,遍历k频繁项
for r,v in relu.items(): # 遍历所有的k频繁项
for i,_ in enumerate(r): # 遍历所有的排列,即(A,B,C)究竟是 A,B->C 还是 A,B->C 还是 A,B->C ?
x = r[:i] + r[i+1:]
confidence = v / rules[n][x] # 不同排列的置信度
if confidence > min_confidence: # 如果某种排列的置信度足够大,那么就加入到结果
# 这里将索引转换回商品名
ii = tuple(item[list(x+(r[i],))])
result[ii] = (confidence, v)
pprint (sorted(result.items(), key=lambda x: -x[1][0])) # 按置信度降序排列
不好意思 测试完才发现速度很慢
for n,l in enumerate(x):
# 行商品有效标记
D[n,np.where(np.in1d(item,l))[0]] = True
主要是这里 np.in1d函数很慢,我尝试了将数据全部转换为索引,这样就可以直接填入不需要转换,即使如此还是要比你的慢一点。只是已经慢的不多了。在复杂数据情况下,工具包提供的函数速度要慢4倍,嗯嗯这是一点点安慰,总之优化不怎么成功,算是熟悉了代码
感谢你的测试研究哈~我也向你学习一波
July 14th, 2018
数据为中文时好像运行出错?
September 17th, 2018
请教大佬,confidence = v / rules[n][x] # 不同排列的置信度
这里为什么是n,x不是少了一个项么 ,为什么不是n-1呢
@风影|comment-9780
哦哦,懂了,n是从0开始的。。
因为
for n,relu in enumerate(rules[1:])
这里的n相对于rules的真正下标,本来就已经减去了1
May 4th, 2019
您好,你这里的apriori算法还像差一个步骤---判断候选集的子集是否是是频繁项集。
如果子集不是频繁项集那么候选项集就一定不是频繁项集了。
May 13th, 2019
请问一下,当数据量没有你那么多的时候,出来的结果会有非常多,这个是怎么回事
调整支持度和置信度