一、 搞懂关联规则中的几个概念
关联规则这个概念,最早是由 Agrawal 等人在 1993 年提出的。在 1994 年 Agrawal 等人又提出了基于关联规则的 Apriori 算法,至今 Apriori 仍是关联规则挖掘的重要算法。
/*请尊重作者劳动成果,转载请标明原文链接:*/
/* https://www.cnblogs.com/jpcflyer/p/11146587.html * /
关联规则挖掘可以让我们从数据集中发现项与项(item 与 item)之间的关系,它在我们的生活中有很多应用场景,“购物篮分析”就是一个常见的场景,这个场景可以从消费者交易记录中发掘商品与商品之间的关联关系,进而通过商品捆绑销售或者相关推荐的方式带来更多的销售量。所以说,关联规则挖掘是个非常有用的技术。
我举一个超市购物的例子,下面是几名客户购买的商品列表:
什么是支持度呢?
支持度是个百分比,它指的是某个商品组合出现的次数与总次数之间的比例。支持度越高,代表这个组合出现的频率越大。
在这个例子中,我们能看到“牛奶”出现了 4 次,那么这 5 笔订单中“牛奶”的支持度就是 4/5=0.8。
同样“牛奶 + 面包”出现了 3 次,那么这 5 笔订单中“牛奶 + 面包”的支持度就是 3/5=0.6。
什么是置信度呢?
它指的就是当你购买了商品 A,会有多大的概率购买商品 B,在上面这个例子中:
置信度(牛奶→啤酒)=2/4=0.5,代表如果你购买了牛奶,有多大的概率会购买啤酒?
置信度(啤酒→牛奶)=2/3=0.67,代表如果你购买了啤酒,有多大的概率会购买牛奶?
我们能看到,在 4 次购买了牛奶的情况下,有 2 次购买了啤酒,所以置信度 (牛奶→啤酒)=0.5,而在 3 次购买啤酒的情况下,有 2 次购买了牛奶,所以置信度(啤酒→牛奶)=0.67。
所以说置信度是个条件概念,就是说在 A 发生的情况下,B 发生的概率是多少。
什么是提升度呢?
我们在做商品推荐的时候,重点考虑的是提升度,因为提升度代表的是“商品 A 的出现,对商品 B 的出现概率提升的”程度。
还是看上面的例子,如果我们单纯看置信度 (可乐→尿布)=1,也就是说可乐出现的时候,用户都会购买尿布,那么当用户购买可乐的时候,我们就需要推荐尿布么?
实际上,就算用户不购买可乐,也会直接购买尿布的,所以用户是否购买可乐,对尿布的提升作用并不大。我们可以用下面的公式来计算商品 A 对商品 B 的提升度:
提升度 (A→B)= 置信度 (A→B)/ 支持度 (B)
这个公式是用来衡量 A 出现的情况下,是否会对 B 出现的概率有所提升。
所以提升度有三种可能:
提升度 (A→B)>1:代表有提升;
提升度 (A→B)=1:代表有没有提升,也没有下降;
提升度 (A→B)<1:代表有下降。
二、 Apriori 的工作原理
明白了关联规则中支持度、置信度和提升度这几个重要概念,我们来看下 Apriori 算法是如何工作的。
首先我们把上面案例中的商品用 ID 来代表,牛奶、面包、尿布、可乐、啤酒、鸡蛋的商品 ID 分别设置为 1-6,上面的数据表可以变为:
Apriori 算法其实就是查找频繁项集 (frequent itemset) 的过程,所以首先我们需要定义什么是频繁项集。
频繁项集就是支持度大于等于最小支持度 (Min Support) 阈值的项集,所以小于最小值支持度的项目就是非频繁项集,而大于等于最小支持度的项集就是频繁项集。
项集这个概念,英文叫做 itemset,它可以是单个的商品,也可以是商品的组合。我们再来看下这个例子,假设我随机指定最小支持度是 50%,也就是 0.5。
我们来看下 Apriori 算法是如何运算的。
首先,我们先计算单个商品的支持度,也就是得到 K=1 项的支持度:
因为最小支持度是 0.5,所以你能看到商品 4、6 是不符合最小支持度的,不属于频繁项集,于是经过筛选商品的频繁项集就变成:
在这个基础上,我们将商品两两组合,得到 k=2 项的支持度:
我们再筛掉小于最小值支持度的商品组合,可以得到:
我们再将商品进行 K=3 项的商品组合,可以得到:
再筛掉小于最小值支持度的商品组合,可以得到:
到这里,你已经和我模拟了一遍整个 Apriori 算法的流程,下面我来给你总结下 Apriori 算法的递归流程:
K=1,计算 K 项集的支持度;
筛选掉小于最小支持度的项集;
如果项集为空,则对应 K-1 项集的结果为最终结果。
否则 K=K+1,重复 1-3 步。
三、 Apriori 的改进算法:FP-Growth 算法
能看到 Apriori 在计算的过程中有以下几个缺点:
可能产生大量的候选集。因为采用排列组合的方式,把可能的项集都组合出来了;
每次计算都需要重新扫描数据集,来计算每个项集的支持度。
所以 Apriori 算法会浪费很多计算空间和计算时间,为此人们提出了 FP-Growth 算法,它的特点是:
创建了一棵 FP 树来存储频繁项集。在创建前对不满足最小支持度的项进行删除,减少了存储空间。我稍后会讲解如何构造一棵 FP 树;
整个生成过程只遍历数据集 2 次,大大减少了计算量。
所以在实际工作中,我们常用 FP-Growth 来做频繁项集的挖掘,下面我给你简述下 FP-Growth 的原理。
1. 创建项头表(item header table)
创建项头表的作用是为 FP 构建及频繁项集挖掘提供索引。
这一步的流程是先扫描一遍数据集,对于满足最小支持度的单个项(K=1 项集)按照支持度从高到低进行排序,这个过程中删除了不满足最小支持度的项。
项头表包括了项目、支持度,以及该项在 FP 树中的链表。初始的时候链表为空。
2. 构造 FP 树
FP 树的根节点记为 NULL 节点。
整个流程是需要再次扫描数据集,对于每一条数据,按照支持度从高到低的顺序进行创建节点(也就是第一步中项头表中的排序结果),节点如果存在就将计数 count+1,如果不存在就进行创建。同时在创建的过程中,需要更新项头表的链表。
3. 通过 FP 树挖掘频繁项集
到这里,我们就得到了一个存储频繁项集的 FP 树,以及一个项头表。我们可以通过项头表来挖掘出每个频繁项集。
具体的操作会用到一个概念,叫“条件模式基”,它指的是以要挖掘的节点为叶子节点,自底向上求出 FP 子树,然后将 FP 子树的祖先节点设置为叶子节点之和。
我以“啤酒”的节点为例,从 FP 树中可以得到一棵 FP 子树,将祖先节点的支持度记为叶子节点之和,得到:
你能看出来,相比于原来的 FP 树,尿布和牛奶的频繁项集数减少了。这是因为我们求得的是以“啤酒”为节点的 FP 子树,也就是说,在频繁项集中一定要含有“啤酒”这个项。你可以再看下原始的数据,其中订单 1{牛奶、面包、尿布}和订单 5{牛奶、面包、尿布、可乐}并不存在“啤酒”这个项,所以针对订单 1,尿布→牛奶→面包这个项集就会从 FP 树中去掉,针对订单 5 也包括了尿布→牛奶→面包这个项集也会从 FP 树中去掉,所以你能看到以“啤酒”为节点的 FP 子树,尿布、牛奶、面包项集上的计数比原来少了 2。
条件模式基不包括“啤酒”节点,而且祖先节点如果小于最小支持度就会被剪枝,所以“啤酒”的条件模式基为空。
同理,我们可以求得“面包”的条件模式基为:
所以可以求得面包的频繁项集为{尿布,面包},{尿布,牛奶,面包}。同样,我们还可以求得牛奶,尿布的频繁项集,这里就不再计算展示。
四、 如何使用 Apriori 工具包
Apriori 虽然是十大算法之一,不过在 sklearn 工具包中并没有它,也没有 FP-Growth 算法。这里教你个方法,来选择 Python 中可以使用的工具包,你可以通过 https://pypi.org/ 搜索工具包。
这个网站提供的工具包都是 Python 语言的,你能找到 8 个 Python 语言的 Apriori 工具包,具体选择哪个呢?建议你使用第二个工具包,即 efficient-apriori。后面我会讲到为什么推荐这个工具包。
首先你需要通过 pip install efficient-apriori 安装这个工具包。
然后看下如何使用它,核心的代码就是这一行:
1 itemsets, rules = apriori(data, min_support, min_confidence)
其中 data 是我们要提供的数据集,它是一个 list 数组类型。min_support 参数为最小支持度,在 efficient-apriori 工具包中用 0 到 1 的数值代表百分比,比如 0.5 代表最小支持度为 50%。min_confidence 是最小置信度,数值也代表百分比,比如 1 代表 100%。
接下来我们用这个工具包,跑一下前面讲到的超市购物的例子。下面是客户购买的商品列表:
具体实现的代码如下:
1 from efficient_apriori importapriori2 #设置数据集
3 data = [('牛奶','面包','尿布'),4 ('可乐','面包', '尿布', '啤酒'),5 ('牛奶','尿布', '啤酒', '鸡蛋'),6 ('面包', '牛奶', '尿布', '啤酒'),7 ('面包', '牛奶', '尿布', '可乐')]8 #挖掘频繁项集和频繁规则
9 itemsets, rules = apriori(data, min_support=0.5, min_confidence=1)10 print(itemsets)11 print(rules)
运行结果:
1 {1: {('啤酒',): 3, ('尿布',): 5, ('牛奶',): 4, ('面包',): 4}, 2: {('啤酒', '尿布'): 3, ('尿布', '牛奶'): 4, ('尿布', '面包'): 4, ('牛奶', '面包'): 3}, 3: {('尿布', '牛奶', '面包'): 3}}2 [{啤酒} -> {尿布}, {牛奶} -> {尿布}, {面包} -> {尿布}, {牛奶, 面包} -> {尿布}]
你能从代码中看出来,data 是个 List 数组类型,其中每个值都可以是一个集合。实际上你也可以把 data 数组中的每个值设置为 List 数组类型,比如:
1 data = [['牛奶','面包','尿布'],2 ['可乐','面包', '尿布', '啤酒'],3 ['牛奶','尿布', '啤酒', '鸡蛋'],4 ['面包', '牛奶', '尿布', '啤酒'],5 ['面包', '牛奶', '尿布', '可乐']]
两者的运行结果是一样的,efficient-apriori 工具包把每一条数据集里的项式都放到了一个集合中进行运算,并没有考虑它们之间的先后顺序。因为实际情况下,同一个购物篮中的物品也不需要考虑购买的先后顺序。
而其他的 Apriori 算法可能会因为考虑了先后顺序,出现计算频繁项集结果不对的情况。所以这里采用的是 efficient-apriori 这个工具包。
五、 挖掘导演是如何选择演员的
在实际工作中,数据集是需要自己来准备的,比如我们要挖掘导演是如何选择演员的数据情况,但是并没有公开的数据集可以直接使用。因此我们需要使用之前讲到的 Python 爬虫进行数据采集。
不同导演选择演员的规则是不同的,因此我们需要先指定导演。数据源我们选用豆瓣电影。
先来梳理下采集的工作流程。
首先我们先在 https://movie.douban.com 搜索框中输入导演姓名,比如“宁浩”。
页面会呈现出来导演之前的所有电影,然后对页面进行观察,你能观察到以下几个现象:
页面默认是 15 条数据反馈,第一页会返回 16 条。因为第一条数据实际上这个导演的概览,你可以理解为是一条广告的插入,下面才是真正的返回结果。
每条数据的最后一行是电影的演出人员的信息,第一个人员是导演,其余为演员姓名。姓名之间用“/”分割。
有了这些观察之后,我们就可以编写抓取程序了。在代码讲解中你能看出这两点观察的作用。抓取程序的目的是为了生成宁浩导演(你也可以抓取其他导演)的数据集,结果会保存在 csv 文件中。完整的抓取代码如下:
1 #-*- coding: utf-8 -*-
2 #下载某个导演的电影数据集
3 from efficient_apriori importapriori4 from lxml importetree5 importtime6 from selenium importwebdriver7 importcsv8 driver =webdriver.Chrome()9 #设置想要下载的导演 数据集
10 director = u'宁浩'
11 #写 CSV 文件
12 file_name = './' + director + '.csv'
13 base_url = 'https://movie.douban.com/subject_search?search_text='+director+'&cat=1002&start='
14 out = open(file_name,'w', newline='', encoding='utf-8-sig')15 csv_write = csv.writer(out, dialect='excel')16 flags=[]17 #下载指定页面的数据
18 defdownload(request_url):19 driver.get(request_url)20 time.sleep(1)21 html = driver.find_element_by_xpath("//*").get_attribute("outerHTML")22 html =etree.HTML(html)23 #设置电影名称,导演演员 的 XPATH
24 movie_lists = html.xpath("/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='title']/a[@class='title-text']")25 name_lists = html.xpath("/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='meta abstract_2']")26 #获取返回的数据个数
27 num =len(movie_lists)28 if num > 15: #第一页会有 16 条数据
29 #默认第一个不是,所以需要去掉
30 movie_lists = movie_lists[1:]31 name_lists = name_lists[1:]32 for (movie, name_list) inzip(movie_lists, name_lists):33 #会存在数据为空的情况
34 if name_list.text isNone:35 continue
36 #显示下演员名称
37 print(name_list.text)38 names = name_list.text.split('/')39 #判断导演是否为指定的 director
40 if names[0].strip() == director and movie.text not inflags:41 #将第一个字段设置为电影名称
42 names[0] =movie.text43 flags.append(movie.text)44 csv_write.writerow(names)45 print('OK') #代表这页数据下载成功
46 print(num)47 if num >= 14: #有可能一页会有 14 个电影
48 #继续下一页
49 returnTrue50 else:51 #没有下一页
52 returnFalse53
54 #开始的 ID 为 0,每页增加 15
55 start =056 while start<10000: #最多抽取 1 万部电影
57 request_url = base_url +str(start)58 #下载数据,并返回是否有下一页
59 flag =download(request_url)60 ifflag:61 start = start + 15
62 else:63 break
64 out.close()65 print('finished')
代码中涉及到了几个模块,我简单讲解下这几个模块。
在引用包这一段,我们使用 csv 工具包读写 CSV 文件,用 efficient_apriori 完成 Apriori 算法,用 lxml 进行 XPath 解析,time 工具包可以让我们在模拟后有个适当停留,代码中我设置为 1 秒钟,等 HTML 数据完全返回后再进行 HTML 内容的获取。使用 selenium 的 webdriver 来模拟浏览器的行为。
在读写文件这一块,我们需要事先告诉 python 的 open 函数,文件的编码是 utf-8-sig(对应代码:encoding=‘utf-8-sig’),这是因为我们会用到中文,为了避免编码混乱。
编写 download 函数,参数传入我们要采集的页面地址(request_url)。针对返回的 HTML,我们需要用到之前讲到的 Chrome 浏览器的 XPath Helper 工具,来获取电影名称以及演出人员的 XPath。我用页面返回的数据个数来判断当前所处的页面序号。如果数据个数 >15,也就是第一页,第一页的第一条数据是广告,我们需要忽略。如果数据个数 =15,代表是中间页,需要点击“下一页”,也就是翻页。如果数据个数 <15,代表最后一页,没有下一页。
在程序主体部分,我们设置 start 代表抓取的 ID,从 0 开始最多抓取 1 万部电影的数据(一个导演不会超过 1 万部电影),每次翻页 start 自动增加 15,直到 flag=False 为止,也就是不存在下一页的情况。
你可以模拟下抓取的流程,获得指定导演的数据,比如我上面抓取的宁浩的数据。这里需要注意的是,豆瓣的电影数据可能是不全的,但基本上够我们用。
有了数据之后,我们就可以用 Apriori 算法来挖掘频繁项集和关联规则,代码如下:
1 #-*- coding: utf-8 -*-
2 from efficient_apriori importapriori3 importcsv4 director = u'宁浩'
5 file_name = './'+director+'.csv'
6 lists = csv.reader(open(file_name, 'r', encoding='utf-8-sig'))7 #数据加载
8 data =[]9 for names inlists:10 name_new =[]11 for name innames:12 #去掉演员数据中的空格
13 name_new.append(name.strip())14 data.append(name_new[1:])15 #挖掘频繁项集和关联规则
16 itemsets, rules = apriori(data, min_support=0.5, min_confidence=1)17 print(itemsets)18 print(rules)
代码中使用的 apriori 方法和开头中用 Apriori 获取购物篮规律的方法类似,比如代码中都设定了最小支持度和最小置信系数,这样我们可以找到支持度大于 50%,置信系数为 1 的频繁项集和关联规则。
这是最后的运行结果:
1 {1: {('徐峥',): 5, ('黄渤',): 6}, 2: {('徐峥', '黄渤'): 5}}2 [{徐峥} -> {黄渤}]
你能看出来,宁浩导演喜欢用徐峥和黄渤,并且有徐峥的情况下,一般都会用黄渤。你也可以用上面的代码来挖掘下其他导演选择演员的规律。