文章导航
大四上大数据分析与知识发现大作业——apriori算法
本报告主要工作:本次大作业使用python自己编程实现了apriori算法,并分别从分布式实现和事务压缩处理的角度进行了算法优化。从中医药网站爬取了两万多条药方作为数据集,用来挖掘各种中药材之间的关联规则,分别使用不同的支持度、置信度进行实验,取得了比较有趣的结果。同时也使用老师给的百货商店数据集进行了测试。
代码运行环境为Python 3.7.4,IDE为Pycharm,所有代码均为自己编写。参考书籍:《数据挖掘概念与技术》(原书第二版)、《机器学习实战》。
一、 关于关联规则的简要介绍
要介绍关联规则,首先要引入两个概念:事务和频繁项集。
事务 :数据集中的一条内容,可以是一次消费的购买记录,也可以是一次测量的返回结果,或者是一个药方中的药品。可以理解为与某个事情相关的名词的集合。
频繁项集: 要想介绍频繁项集,首先要介绍“项集”。顾名思义,“项集”就是项的集合,项就是每一条事务中的一个名词,可以代表一个物品或者一个事件。“频繁项集”就是在数据集中频繁出现的项集,即一种频繁的模式,这个项集可大可小,依据其在数据集中出现的频率而判断是否为频繁项集。
而关联规则是基于这二者进行计算的,它的基本定义为:
关联规则: 两个项集之间所具有的某种相关关系,其紧密程度由支持度和置信度来衡量。我们将关联规则中前面的项集叫做前件 ,后面的项集叫做后件 ,规则的含义可以表示为:前件出现,后件将有很大概率出现。
下面介绍衡量关联规则的两条准则:支持度与置信度。
支持度: 一个项集的支持度被定义为数据集中包含该项集的记录所占的比例
置信度: 两个项集之间关联规则紧密程度的置信度可以理解为前后件的并集所代表的项集的支持度与前件支持度的比,即:出现前件的条件下出现后件的概率。
二、Apriori算法主要流程
为了寻找频繁项集,就必须计算每个项集的支持度,当事务中项的数目很多时,它们之间互相的排列组合就变得及其复杂。当数据集庞大时,每个事项的检测都包含的大量的比对和筛选,算法复杂度变得很高,计算时间成本极大。
为了解决这个问题,我们提出了apriori原理:如果某个项集是频繁项集,那么它所有的子集也是频繁的。 即如果 {0,1} 是频繁的,那么 {0}, {1} 也一定是频繁的。这个规则看似直观,但需要经过极为严格的证明,所幸前人已经为我们证明好了这个定理。乍一看它似乎没什么用,但是把这个定理反过来,它的逆否命题为:如果某个项集非频繁,那么它的超集也非频繁。 那么我们就可以在由小项集组合大项集时提前筛掉那些不频繁的项集,从而大大减少计算量。
算法的主要步骤为:
寻找频繁项集
① 扫描整个数据集获取所有单个名词的项集列表——候选1-项集。
② 扫描数据集计算每个候选1-项集的支持度,删除不满足最小支持度的项集,形成频繁1-项集,此时k=2。
③ 对此时的频繁(k-1)-项集进行组合,形成候选的k-项集。若k-项集为空,退出。
④ 扫描数据集计算每个候选k-项集的支持度,筛选出符合条件的k-项集,形成频繁k-项集。返回第三步。
寻找关联规则
使用递归的方法,遍历所有大于1的频繁项集,求取它们的1项集子集:
—如果是频繁2项集:
——使用差集的方法计算置信度,符合的关联规则直接输出。
—或者项集中项数大于等于3:
——如果子集可以继续组合成能生成其他规则的新子集:(1)
———子集进行组合,测试新生成的规则是否满足条件,符合的直接输出。
———如果还可以继续组合,递归调用(1)
否则:
———继续遍历下一个项集
一些优化方法 :
使用分布式的方法,将数据集分为多个块,在遍历数据集时利用python中的map函数,将比较统计的部分分开进行,再使用reduce函数整合每个块统计的结果,形成最终的答案。这对大规模数据集比较适用。
使用事务压缩的方法,在遍历数据集时,如果某个事务已经不包含当前的频繁k-项集,那么它们必然不包含由这频繁k-项集组成的候选k+1-项集,那么在下一次遍历时就可以跳过这些事务,不必再进行子集计算,节省了运行时间。这对大部分情况都比较适用。
三、 所采用的数据集简介
中医药的理论博大精深,各种药材之间的配合在治病的疗效上会起到1+1>2的效果。比如茯苓和人参搭配,具有益心力,除谬忘,能饮食,延年益寿之功效。主治上气,胸胁满闷,霍乱,积痢。其他的相似组合还有很多,进行关联规则的发现可以帮助我们更系统地归纳古人留下的药方中的知识,便于将中医药应用于现代疾病的治疗中。
所用我从某中医网站爬取了2万多条药方的数据,作为待挖掘的数据集,意在找出哪些药材之间的关联规则比较大,哪些药材常常出现并且被搭配使用在药方当中。爬取数据集的代码放在crawler.py文件中,2万条药方的网址已经提前爬出可以直接导入进行读取,节省时间。(url.csv)
中医网站页面(网址:http://zhongyaofangji.com/all.html)
药方详细页面
数据集展示(prescription.csv)
四、 实验结果分析
(1) 三种方式运行时间比较
选取prescription.csv药方数据(2万余条),最小支持度和最小置信度都为0.01
不同改进算法的运行时间比较表
基本算法
36.0405
35.8101
34.1877
35.9658
33.7644
35.1537
分布式
34.2813
33.9013
34.2965
34.5172
34.3388
34.26702
事务压缩
31.0910
32.0156
31.2278
31.0221
31.6232
31.39594
从运行时间可以大致看出,分布式算法由于使用mapreduce并行执行,运行时间略有减少,但是并行运行会占用更多的内存,分布式的主要用处是在数据集过大无法全部导入内存时可以进行计算,在节省时间方面不是主流。而基于事务压缩的方法可以避免多次无用的遍历,所以在时间节约上也比较明显。
(2)修改支持度、置信度的结果
不同支持度、置信度比较表
0.01
频繁项集
419
419
419
419
419
419
关联规则
679
648
515
288
156
88
运行时间
30.6290
33.8207
32.0566
30.7340
31.0242
30.5041
.015
频繁项集
207
207
207
207
207
207
关联规则
255
255
219
139
82
51
运行时间
19.0702
20.9975
18.1798
18.6537
18.4798
18.5693
0.02
频繁项集
126
126
126
126
126
126
关联规则
119
119
113
79
48
31
运行时间
14.9484
14.8706
14.7600
14.6400
14.8406
14.9926
0.03
频繁项集
59
59
59
59
59
59
关联规则
36
36
36
29
21
15
运行时间
11.9174
11.9613
12.1243
12.0313
12.0253
11.8591
0.04
频繁项集
38
38
38
38
38
38
关联规则
22
22
22
18
13
10
运行时间
11.3005
11.3198
11.2225
11.3224
11.0737
11.3320
0.05
频繁项集
23
23
23
23
23
23
关联规则
10
10
10
10
7
6
运行时间
10.7939
11.0177
10.8461
10.9094
10.7684
10.7101
0.06
频繁项集
15
15
15
15
15
15
关联规则
4
4
4
4
2
2
运行时间
10.7497
10.5669
10.6482
10.7804
10.6141
10.6488
从上表可以看出: 支持度相同,最小置信度越高,关联规则数越少; 置信度相同,最小支持度越高,频繁项集数越少; 运行时间和最小支持度关系最大,最小支持度越大,运行时间越短; 关联规则的挖掘几乎不会影响到运行时间,因为比较的频繁项集数量级都很小; 随着最小支持度的增大,最小置信度的变化对发现的关联规则数量的影响减小; 最小置信度变化幅度不大时对发现的关联规则数目影响不大; 频繁项集和支持度的相关关系是非线性的,在某一区间内变化很明显。
(3)挖掘出的有趣规则
支持度、置信度均为0.01时前25项挖掘出的关联规则
我们随机取其中的五条:没药->乳香;川芎->当归;柴胡->甘草;熟地->当归;
荆芥->防风。在搜索引擎中分别搜索这几种组合。
可以看出挖掘出的关联规则中的药物都有着很深的关系,至少人们经常把它们放在一起作为药方。
测试该条件下置信度最弱的一条关联规则:甘草->桂枝
即使是预设条件下关联规则最弱的一条也可以在网上找到相应的搭配,可见我们挖掘出的关联规则在统计意义下是有效的。
我们也注意到,在挖掘出的关联规则中,经常出现的中药材有甘草、人参、当归、白术、白芍、川芎等等,我们搜索中药方中最常用的药材:
相关的内容也几乎匹配。
(4)使用老师提供的百货商店数据集挖掘出的一些规则
支持度、置信度均为0.01时前25项挖掘出的关联规则
对上述关联规则进行一定的分析可以发现一个比较有趣的事情:美国人在购物时比较偏爱“whole milk”,即全脂牛奶,在购买黄油、凝乳、鸡蛋、糖、汉堡肉、蔬菜等等食物之后都偏爱购买牛奶,这与他们的生活习惯不无关系。搜索相关内容也会发现类似的报道。
附录(源代码)
1. 基本Apriori算法代码:(basic.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 from numpy import *from csv import readerimport timedef load_dataset (fileName ): with open (fileName, 'rt' , encoding='UTF-8' ) as raw_data: readers = reader(raw_data, delimiter=',' ) dataset = list (map (frozenset , readers)) return dataset def get_one_itemset (dataset ): itemset = [] for affair in dataset: for item in affair: if [item] not in itemset: itemset.append([item]) return list (map (frozenset , itemset)) def get_freqset (dataset, itemset, minSupport ): supportData = {} count = {} freqset = [] for affair in dataset: for item in itemset: if item.issubset(affair): if item in count: count[item] += 1 else : count[item] = 1 number = len (dataset) for key in count: support = count[key]/number if support >= minSupport: freqset.append(key) supportData[key] = support return freqset, supportData def get_itemset (freqset, k ): itemset = [] freqlen = len (freqset) for i in range (freqlen): for j in range (i+1 , freqlen): item = freqset[i] | freqset[j] if len (item) == k and item not in itemset: itemset.append(item) return itemset def get_all_freqset (dataset, minSupport=0.001 ): C1 = get_one_itemset(dataset) L1, supportData = get_freqset(dataset, C1, minSupport) print ('已经挖掘频繁' , 1 , '项集...' ) freqset = [L1] k = 0 while len (freqset[k]) > 0 : Ck = get_itemset(freqset[k], k+2 ) Lk, support = get_freqset(dataset, Ck, minSupport) freqset.append(Lk) supportData.update(support) k += 1 print ('已经挖掘频繁' , k+1 , '项集...' ) return freqset, supportData def test_rule (freqset, subset, supportData, rules, minConfi ): trueSubset = [] for item in subset: confi = supportData[freqset]/supportData[freqset-item] if confi >= minConfi: rules.append((freqset-item, item, confi)) trueSubset.append(item) return trueSubset def form_subset (freqset, subset, supportData, rules, minConfi ): itemNum = len (subset[0 ]) if len (freqset) > (itemNum+1 ): newSubset = get_itemset(subset, itemNum+1 ) newSubset = test_rule(freqset, newSubset, supportData, rules, minConfi) if len (newSubset) > 1 : form_subset(freqset, newSubset, supportData, rules, minConfi) def get_rules (freqset, supportData, minConfi=0.01 ): rules = [] for i in range (1 , len (freqset)): for item in freqset[i]: subset = [frozenset ([unit]) for unit in item] if i > 1 : form_subset(item, subset, supportData, rules, minConfi) else : test_rule(item, subset, supportData, rules, minConfi) return rules minSupport = 0.01 minConfi = 0.01 dataset = load_dataset('prescription.csv' ) time_start = time.time() L, supportData = get_all_freqset(dataset, minSupport) print ('所有的频繁项集为:\n' , L)print ('频繁项集的支持度为:\n' , supportData)print ('频繁项集总数为:' , len (supportData))time_end1 = time.time() print ('求频繁项集所用时间:' , time_end1-time_start, '秒' )rules = get_rules(L, supportData, minConfi) print ('挖掘出的关联规则为:' )rules.sort(key=lambda x: x[2 ], reverse=True ) print ('关联规则总数为:' , len (rules))time_end2 = time.time() print ('求关联规则所用时间:' , time_end2-time_end1, '秒' )
2. 分布式Apriori代码:(distributed.py)
(以下主要为新加入的内容)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 from functools import partial from functools import reduce def block_gen (dataset ): blockData = [] num = len (dataset) blockNum = int (sqrt(num)) + 1 k = blockNum block = [] for affair in dataset: if k > 0 : block.append(affair) k -= 1 else : block.append(affair) blockData.append(block) k = blockNum block = [] blockData.append(block) return blockDatadef map_freq (block, itemset, count ): for affair in block: for item in itemset: if item.issubset(affair): if item in count: count[item] += 1 else : count[item] = 1 return count def get_freqset (blockData, itemset, minSupport ): supportData = {} count = {} freqset = [] partial_func = partial(map_freq, itemset=itemset, count=count) list (map (partial_func, blockData)) number = reduce(add, map (len , blockData)) for key in count: support = count[key]/number if support >= minSupport: freqset.append(key) supportData[key] = support return freqset, supportData def get_all_freqset (dataset, minSupport=0.001 ): blockData = block_gen(dataset) C1 = get_one_itemset(dataset) L1, supportData = get_freqset(blockData, C1, minSupport) print ('已经挖掘频繁' , 1 , '项集...' ) freqset = [L1] k = 0 while len (freqset[k]) > 0 : Ck = get_itemset(freqset[k], k+2 ) Lk, support = get_freqset(blockData, Ck, minSupport) freqset.append(Lk) supportData.update(support) k += 1 print ('已经挖掘频繁' , k+1 , '项集...' ) return freqset, supportData
3. 事务压缩方法:(affairs_comp.py)
(主要修改部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 def get_freqset (dataset, itemset, minSupport, flag ): supportData = {} count = {} freqset = [] i = 0 for affair in dataset: if not flag[i]: i += 1 continue flag_temp = False for item in itemset: if item.issubset(affair): flag_temp = True if item in count: count[item] += 1 else : count[item] = 1 if not flag_temp: flag[i] = False i += 1 number = len (dataset) for key in count: support = count[key]/number if support >= minSupport: freqset.append(key) supportData[key] = support return freqset, supportDatadef get_all_freqset (dataset, minSupport=0.001 ): flag = [True for _ in range (len (dataset))] C1 = get_one_itemset(dataset) L1, supportData = get_freqset(dataset, C1, minSupport, flag) print ('已经挖掘频繁' , 1 , '项集...' ) freqset = [L1] k = 0 while len (freqset[k]) > 0 : Ck = get_itemset(freqset[k], k+2 ) Lk, support = get_freqset(dataset, Ck, minSupport, flag) freqset.append(Lk) supportData.update(support) k += 1 print ('已经挖掘频繁' , k+1 , '项集...' ) return freqset, supportData
4. 爬取数据集代码:(crawler.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import requestsfrom lxml import etreeimport csvhtml = requests.get("http://zhongyaofangji.com/all.html" ) print ('访问成功...' )etree_html = etree.HTML(html.text) content = etree_html.xpath('//*[@id="divMain"]/div/div/ul/li/a/@href' ) with open ('url.csv' , 'w' , newline="" , encoding='utf-8' ) as f: csv_write = csv.writer(f) for each in content: temp = [str (each)] csv_write.writerow(temp) print ('url保存成功...' )with open ("prescription.csv" , 'a' , newline="" , encoding="utf-8" ) as input_csv: csv_write = csv.writer(input_csv) num = len (content) for i in range (0 , num): print ('第' , i+1 , '项正在访问:' , content[i]) html = requests.get(content[i]) html.encoding = 'GBK' etree_html = etree.HTML(html.text) herb = etree_html.xpath('//*[@id="divMain"]/div/div[2]/div[2]/div[2]/div[1]/p[1]/a/text()' ) csv_write.writerow(herb) print (herb)