上一章我们讨论了从数据集中获取有趣信息的方法,最常用的两种分别是频繁项集与关联规则。第11章中介绍了发现频繁项集与关键规则的算法,本章将继续关注发现频繁项集这一任务。我们会深人探索该任务的解决方法,并应用FP-growth算法进行处理,该算法能够更有效地挖掘数据。这种算法虽然能更为高效地发现频繁项集,但不能用于发现关联规则。
FP-growth算法只需要对数据库进行两次扫描,而Apriori算法对于每个潜在的频繁项集都会扫描数据集判定给定模式是否频繁,因此FP-growth算法的速度要比Apriori算法快。在小规模数据集上,这不是什么问题,但当处理更大数据集时,就会产生较大问题。
FP-growth只会扫描数据集两次,它发现频繁项集的基本过程如下:
(1) 构建FP树
(2)从FP树中挖掘频繁项集
12.1 FP树:用于编码数据集的有效方式
FP-growth算法
优点:一般要快于Apriori。
缺点:实现比较困难,在某些数据集上性能会下降。
适用数据类型:标称型数据。
FP-growth算法将数据存储在一种称为FP树的紧凑数据结构中。FP代表频繁模式(Frequent Pattern)。一棵FP树看上去与计算机科学中的其他树结构类似,但是它通过链接(link)来连接相似元素,被连起来的元素项可以看成一个链表。图12-1给出了FP树的一个例子。
同搜索树不同的是,一个元素项可以在一棵FP树中出现多次。FP树会存储项集的出现频率,而每个项集会以路径的方式存储在树中。存在相似元素的集合会共享树的一部分。只有当集合之间完全不同时,树才会分叉。 树节点上给出集合中的单个元素及其在序列中的出现次数,路径会给出该序列的出现次数。
相似项之间的链接即节点链接(node link),用于快速发现相似项的位置。
表12-1给出了用于生成图12-1中所示FP树的数据。
在图12-1中,元素项z出现了5次 ,集合{r,z}出现了1次。于是可以得出结论:z一定是
自己本身或者和其他符号一起出现了4次。我们再看下z其他可能性。集 合{t,s,y,x,z}出现了2次
,集合{t,r,y,x,z}出现了1次。元素项z的右边标的是5,表示z出现了5次
,其中刚才已经给出了4次出现,所以它一定单独出现过1次。通过观察表12-1看看刚才的结论是否正确。前面提到{t,r,y,x,z}只出现过1次
,在事务数据集中我们看到005号记录上却是{y,r,x,z,q,t,p}。那么,q和p去哪儿了呢?
这里使用第11章给出的支持度定义,该指标对应一个最小阈值,低于最小阈值的元素项被认为是不频繁的。如果将最小支持度设为3,然后应用频繁项分析算法,就会获得出现3次或3次以上的项集。上面在生成图12-1中的FP树时,使用的最小支持度为3,因此q和p并没有出现在最后的树中。
FP-growth算法的工作流程如下。首先构建FP树 ,然后利用它来挖掘频繁项集。为构建FP树 ,需要对原始数据集扫描两遍。第一遍对所有元素项的出现次数进行计数。记住第11章中给出Apriori原 理,即如果某元素是不频繁的,那么包含该元素的超集也是不频繁的,所以就不需要考虑这些超集。数据库的第一遍扫描用来统计出现的频率,而第二遍扫描中只考虑那些频繁元素。
FP-growth的一般流程
⑴收集数据 :使用任意方法。
⑵准 备数据: 由于存储的是集合,所以需要离散数据。如果要处理连续数据,需要将它们量化为离散值。
(3)分析数据:使用任意方法。
(4)训练算法:构建一个FP树 ,并对树进行挖据。
(5)测试算法:没有测试过程。
(6)使用算法: 可用于识别经常出现的元素项,从而用于制定决策、推荐元素或进行预测
等应用中。
12.2构建FP树
在第二次扫描数据集时会构建一棵树。为构建一棵树,需要一个容器来保存树。
12.2.1 创建FP树的数据结构
FP树的类定义,代码如下:
class treeNode:
#name存放节点名称
#count存放计数值
#nodeLink链接相似元素
#parent存放父节点
#children存放子节点
def __init__(self, nameValue, numOccur, parentNode):
self.name = nameValue
self.count = numOccur
self.nodeLink = None
self.parent = parentNode #needs to be updated
self.children = {}
def inc(self, numOccur):
self.count += numOccur
#展示树结构
def disp(self, ind=1):
#' '*ind表示输出ind个' '
print(' '*ind, self.name, ' ', self.count)
for child in self.children.values():
#注意使用了递归的方式对不同层级的node输出不同个数的空格
child.disp(ind+1)
测试截图如下:
12.2.2 构建FP树
除了图12-1给出的FP树之外,还需要一个头指针表来指向给定类型的第一个实例。利用头指针表 ,可以快速访问FP树中一个给定类型的所有元素。图12-2给出了一个头指针表的示意图。
这里使用一个字典作为数据结构,来保存头指针表。除了存放指针外,头指针表还可以用来保存FP树中每类元素的总数。
第一次遍历数据集会获得每个元素项的出现频率。接下来,去掉不满足最小支持度的元素项。再下一步构建FP树。在构建时,读人每个项集并将其添加到一条已经存在的路径中。如果该路径不存在,则创建一条新路径。每个事务就是一个无序集合。假设有集合{z,x,y}和{y,z,r}
,那么在FP树中 , 相同项会只表示一次。为了解决此问题,在将集合添加到树之前,需要对每个集合进行排序。排序基于元素项的绝对出现频率来进行。使用图12-2中的头指针节点值,对表12-1中数据进行过滤、重排序后的数据显示在表12-2中。
在对事务记录过滤和排序之后,就可以构建FP树了。从空集(符号为∅
FP树构建函数, 代码如下:
#create FP-tree from dataset but don't mine
def createTree(dataSet, minSup=1):
headerTable = {}
#go over dataSet twice
#first pass counts frequency of occurance
#遍历两次数据集,第一次过滤出满足最小支持度的项
for trans in dataSet:
for item in trans:
headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
#remove items not meeting minSup
#删除不满足最小支持度的项,这里我与书里写的不一样,因为按书里写的会报dictionary changed size during iteratation错,这样就没问题了
headerTable = {k:v for k,v in headerTable.items() if v >= minSup}
freqItemSet = set(headerTable.keys())
#print 'freqItemSet: ',freqItemSet
if len(freqItemSet) == 0: return None, None #if no items meet min support -->get out
for k in headerTable:
headerTable[k] = [headerTable[k], None] #reformat headerTable to use Node link
#print 'headerTable: ',headerTable
#create tree
#创建根节点
retTree = treeNode('Null Set', 1, None)
#go through dataset 2nd time
#第二次遍历数据集,创建树
for tranSet, count in dataSet.items():
#localD存储tranSet中满足最小支持度的项和对应的出现频率
localD = {}
for item in tranSet: #put transaction items in order
if item in freqItemSet:
localD[item] = headerTable[item][0]
if len(localD) > 0:
#这段代码表示,localD中没项通过计数值进行逆序排序,然后将排好序的键存放到新建的列表orderedItems中
orderedItems = [v[0] for v in sorted(localD.items(), key=lambda p: p[1], reverse=True)]
#populate tree with ordered freq itemset
#用排好序的频繁项集创建树
updateTree(orderedItems, retTree, headerTable, count)
return retTree, headerTable #return tree and header table
def updateTree(items, inTree, headerTable, count):
#如果items[0]项在树中,那么增加树种对应节点计数值
if items[0] in inTree.children:#check if orderedItems[0] in retTree.children
inTree.children[items[0]].inc(count) #incrament count
#如果items[0]不在树中,那么创建新建对应节点,创建分支,更新headerTable头指针
else: #add items[0] to inTree.children
inTree.children[items[0]] = treeNode(items[0], count, inTree)
#items[0]对应的头指针为空,将items[0]项对应的树节点放入headerTable中
if headerTable[items[0]][1] == None: #update header table
headerTable[items[0]][1] = inTree.children[items[0]]
#如果items[0]对应的头指针非空,调用updateHeader()更新头指针
else:
updateHeader(headerTable[items[0]][1], inTree.children[items[0]])
#如果频繁项集长度大于一,那么递归调用updateTree(),items[1::]表示使用频繁项集中第二项之后的所有项,inTree.children[itemes[0]]表示使用子节点
if len(items) > 1:#call updateTree() with remaining ordered items
updateTree(items[1::], inTree.children[items[0]], headerTable, count)
#this version does not use recursion
def updateHeader(nodeToTest, targetNode):
#Do not use recursion to traverse a linked list!
#迭代指针链表,直到nodeToTest的指针为空
while (nodeToTest.nodeLink != None):
nodeToTest = nodeToTest.nodeLink
#将targetNode加入指针链表
nodeToTest.nodeLink = targetNode
笔者亲测[v[0] for v in sorted(localD.items(), key=lambda p: p[1], reverse=True)]
简单数据集及数据包装器,代码如下:
def loadSimpDat():
simpDat = [['r', 'z', 'h', 'j', 'p'],
['z', 'y', 'x', 'w', 'v', 'u', 't', 's'],
['z'],
['r', 'x', 'n', 'o', 's'],
['y', 'r', 'x', 'z', 'q', 't', 'p'],
['y', 'z', 'x', 'e', 'q', 's', 't', 'm']]
return simpDat
#将数据集转化为字典形式,注意使用的frozenset(),因为以集合作为key,必须为frozenset类型
def createInitSet(dataSet):
retDict = {}
for trans in dataSet:
#将每个集合对应计数设置为1
retDict[frozenset(trans)] = 1
return retDict
测试截图如下:
12.3 从一棵FP树中挖掘频繁项集
有了FP树之后,就可以抽取频繁项集了。这里的思路与Apriori算法大致类似,首先从单元素项集合开始,然后在此基础上逐步构建更大的集合。当然这里将利用FP树来做实现上述过程 ,不再需要原始数据集了。
从FP树中抽取频繁项集的三个基本步骤如下:
(1)从FP树中获得条件模式基;
(2)利用条件模式基,构建一个条件FP树 ;
(3)迭代重复步骤(1)步骤( 2 ) ,直到树包含一个元素项为止。
接下来重点关注第(1)步 ,即寻找条件模式基的过程。之后,为每一个条件模式基创建对应的条件FP树。最后需要构造少许代码来封装上述两个函数,并从FP树中获得频繁项集。
12.3.1 抽取条件模式基
首先从上一节发现的巳经保存在头指针表中的单个频繁元素项开始。对于每一个元素项,获得其对应的条件模式基(conditionalpattembase )。条件模式基是以所查找元素项为结尾的路径集合。每一条路径其实都是一条前辍路径(prefix path)。简而言之,一条前缀路径是介于所査找元素项与树根节点之间的所有内容。
回到图12-2,符号r的前缀路径是{x,s}、{z,x,y} 和{z}。每一条前缀路径都与一个计数值关联。该计数值等于起始元素项的计数值,该计数值给了每条路径上的数目。表12-3列出了上例当中每一个频繁项的所有前缀路径。
为了获得这些前缀路径,可以对树进行穷举式搜索,直到获得想要的频繁项为止,或者使用一个更有效的方法来加速搜索过程。可以利用先前创建的头指针表来得到一种更有效的方法。头指针表包含相同类型元素链表的起始指针。一旦到达了每一个元素项,就可以上溯这棵树直到根节点为止。
发现以给定元素项结尾的所有路径的函数,代码如下:
#ascends from leaf node to root
#从叶子节点回溯到根节点
def ascendTree(leafNode, prefixPath):
if leafNode.parent != None:
prefixPath.append(leafNode.name)
ascendTree(leafNode.parent, prefixPath)
def findPrefixPath(basePat, treeNode): #treeNode comes from header table
condPats = {}
#通过链表遍历相似元素项,调用ascendTree获取相似元素项的前缀路径
while treeNode != None:
prefixPath = []
ascendTree(treeNode, prefixPath)
#注意,盗用ascendTree会把叶子节点自己也添加到prefixPath中
if len(prefixPath) > 1:
#注意,这里使用了数组过滤,把叶子节点过滤掉,得出前缀路径,放入字典中,{prefixPath:count}
condPats[frozenset(prefixPath[1:])] = treeNode.count
treeNode = treeNode.nodeLink
return condPats
测试代码如下:
12.3.2创建条件FP树
对于每一个频繁项,都要创建一棵条件FP树。我们会为z、x以及其他频繁项构建条件树。可以使用刚才发现的条件模式基作为输入数据,并通过相同的建树代码来构建这些树。然后
,我们会递归地发现频繁项、发现条件模式基,以及发现另外的条件树。举个例子来说,假定为频繁项测建一个条件FP树,然
后对{t,y}、{t,x}、…重复该过程。元素项t的条件FP树的构建过程如图12-4所示。
在图12-4中,注意到元素项s以及r是条件模式基的一部分,但是它们并不属于条件FP树。原因是什么?如果讨论s以及r的话,它们难道不是频繁项吗?实际上单独来看它们都是频繁项,但是在t的条件树中,它们却不是频繁的,也就是说,{t,r}及{t,s}是不频繁的。
接下来,对集{t,z}、{t,x}以及{t,y}来挖掘对应的条件树。这会产生更复杂的频繁项集。该过程重复进行,直到条件树中没有元素为止,然后就可以停止了。实现代码相对比较直观,使用一些递归加上之前写的代码就可以完成。
递归查找频繁项集的mineTree的函数,代码如下:
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
#(sort header table)
#这一行与书中不一样在于p[1][0],通过headerTable中项对应的个数来排序
bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1][0])]
#start from bottom of header table
#迭代满足最小支持度的频繁项,找出条件基,递归构建条件树,挖掘出频繁项集
for basePat in bigL:
#注意这里使用了preFix.copy(),因为用到了递归,preFix在同一层的项中重复使用,如果直接引用会出问题
newFreqSet = preFix.copy()
#将当前频繁项加入新的频繁项集
newFreqSet.add(basePat)
#print 'finalFrequent Item: ',newFreqSet
#append to set
#将新频繁项集加入频繁项集列表
freqItemList.append(newFreqSet)
#找出当前频繁项的前缀路径,用于构建FP条件树挖掘新的频繁项集
condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
#print 'condPattBases :',basePat, condPattBases
#2. construct cond FP-tree from cond. pattern base
#使用当前频繁项的前缀路径构建FP条件树
myCondTree, myHead = createTree(condPattBases, minSup)
#print 'head from conditional tree: ', myHead
#如果FP条件树存在,那么递归
if myHead != None: #3. mine cond. FP-tree
print 'conditional tree for: ',newFreqSet
myCondTree.disp(1)
mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
测试截图如下:
12.4 示例 :在Twitter源中发现一些共现词(笔者没有翻墙,汗,感兴趣的朋友可以自己参考书中的代码尝试一下)
我们会用到一个叫做python-twitter的python库,其源代码可以在http://code.googleconVp/python-twitter/下载。正如你猜到的那样,借助它,我们可以使用Python来访问Twitter。Twitter.com实际上是一个和其他人进行交流的通道,其上发表的内容被限制在140个字符以内,发表的一条信息称为推文(tweet)。
有关Twitter API的文档可以在http://dev.twitter.com/doc找到。
示例:发现Twitter源中的共现词(co-occurring word)
(1)收集数据:.使用Python-twitter模块来访问推文。
(2)准备数据:编写一个函数来去掉URL、去掉标点、转换成小写并从字符串中建立一个
单词集合。
(3)分析数据:在python提示符下查看准备好的数据,确保它的正确性。
(4)训练算法:使用本章前面开发的createTree() 与 mineTree()函数执行FP-growth算法。
(5)测试算法:这里不适用。
(6)使用算法:本例中没有包含具体应用,可以考虑用于情感分析或者查询推荐领域。
在使用API之前,需要两个证书集合。第一个集合是consumer_key和consumer_secret,当注册开发app时 (https://dev.twitter.com/apps/new),可以从了Twitter开发服务网站获得。这些key对于要编写的app是特定的。第二个集合是access_token_key和access_token_secret,它们是针对特定Twitter用户的。为了获得这些key,需要查看Twitter-Pytho安装包中的get_access_token.py文 件 (或者从Twitter开发网站中获得)。这是一个命令行的Python脚本,该脚本使用OAuth来告诉Twitter应用程序具有用户的权限来发布信息。一旦完成上述工作之后,可以将获得的值放人前面的代码中开始工作。对于给定的搜索词,下面要使用FP一growth算法来发现推文中的频繁单词集合。要提取尽可能多的推文(1400条 )然后放到FP-growth算法中运行。
访问Twitter Python库的代码如下:
import twitter
from time import sleep
import re
def getLotsOfTweets(searchStr):
CONSUMER_KEY = ''
CONSUMER_SECRET = ''
ACCESS_TOKEN_KEY = ''
ACCESS_TOKEN_SECRET = ''
api =twitter.Api(consumer_key=CONSUMER_KEY,consumer_secret=CONSUMER_SECRET,
access_token_key=ACCESS_TOKEN_KEY,
access_token_secret=ACCESS_TOKEN_SECRET)
#you can get 1500 results 15 pages * 100 per page
resultsPages = []
for i in range(1,15):
print("fetching page %d" % i)
searchResults = api.GetSearch(searchStr, per_page=100, page=i)
resultsPages.append(searchResults)
sleep(6)
return resultsPages
函数getLotsOfTweets( ) 处理认证然后创建一个空列表。搜索API可以一次获得100条推
文。每100条推文作为一页,而Twitter允许一次访问14页。在完成搜索调用之后,有一个6秒钟的睡眠延迟,这样做是出于礼貌,避免过于频繁的访问请求。print语句用于表明程序仍在执行没有死掉。
测试截图如下:
正如所看到的那样’有些人会在推文中放人URL。这样在解析时,结果就会比较乱。因此必须去除U R L , 以便可以获得推文中的单词。下面程序清单中的一部分代码用来将推文解析成字符串列表 ,另一部分会在数据集上运行FP-growth算法
文本解析及合成代码如下:
def textParse(bigString):
urlsRemoved = re.sub('(http:[/][/]|www.)([a-z]|[A-Z]|[0-9]|[/.]|[~])*', '', bigString)
listOfTokens = re.split(r'W*', urlsRemoved)
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def mineTweets(tweetArr, minSup=5):
parsedList = []
for i in range(14):
for j in range(100):
parsedList.append(textParse(tweetArr[i][j].text))
initSet = createInitSet(parsedList)
myFPtree, myHeaderTab = createTree(initSet, minSup)
myFreqList = []
mineTree(myFPtree, myHeaderTab, minSup, set([]), myFreqList)
return myFreqList
效果截图如下:
看看频繁项集,截图如下:
12.5 示例:从新闻网站点击流中挖掘
在源数据集合中,有kosarak.dat文件,它包含将近100万条记录。该文件中的每一行包含某个用户浏览过的新闻报道。一些用户只看过一篇报道,而有些用户看过2498篇报道。用户和报道被编码成整数,所以查看频繁项集很难得到更多的东西,但是该数据对于展示FP-growth算法的速度十分有效。
可以看一下数据集是怎么样的(确实挺大的,加载了有一会儿):
测试代码如下:
如果降低一下置信度(这里指阅读次数):
总结:
FP-growth算法利用Apriori原则,执行更快。Apriori算法产生候选项集,然后扫描数据集来检查它们是否频繁。由于只对数据集扫描两次,因此FP-growth算法执行更快。在FP-growth算法中,数据集存储在一个称为FP树的结构中。FP树构建完成后,可以通过查找元素项的条件基及构建条件FP树来发现频繁项集。该过程不断以更多元素作为条件重复进行,
直到FP树只包含一个元素为止。