关联分析是数据挖掘中的重要组成部分,旨在挖掘数据中的频繁模式。我们可以通过一个案例数据库挖掘著名案例来大致了解挖掘频繁项集并产生关联规则。
关联分析:在大规模数据集中寻找有趣的关系
频繁项集:经常出现在一起的物品集合,即包含0个或者多个项的集合
关联规则:暗示两个物品之间可能存在很强的关系,形如A->B的表达式,规则A->B的度量包括支持度和置信度
支持度:数据集中包含该项集的记录所占的比例,是针对项集来说
置信度:出现某些物品时,另外一些物品必定出现的概率,简单点来说就是在一个物品出现的条件下,另外一个物品出现的概率,是针对规则而言
support(A⇒B)=support_count(A∪B)/N
confidence(A⇒B)=support_count(A∪B)/support_count(A)
在这里再补充说一下闭频繁项集和极大频繁项集的概念
闭频繁项集:如果不存在真超项集Y,使得Y与X在D中具有相同的支持度计数,则项集X是数据集D中的闭频繁项集
极大频繁项集:如果X是频繁的,并且不存在超项集Y使得X是Y的子集,并且Y在D中是频繁的,那么X就是极大频繁项集
这两个概念理解起来有点困难,可以参考频繁项集,闭频繁项集,极大频繁项集这篇文章
简单关联关系:简单关联关系可以从经典的购物中进行分析,购买面包的顾客80%都会购买牛奶,由于面包和牛奶是早餐搭配的必需品,二者搭配构成了早餐的组成部分,这就是一种简单的关联关系。算法思想基础为:如果某个项集是频繁的,那么它的所有子集也是频繁的,更常用的是它的逆否命题,即如果一个项集是非频繁的,那么它的所有超集也是非频繁的。
序列关联关系:序列关联规则的核心就是找到事务发展的前后关联性。
1:发现频繁项集:即计算所有可能组合数的支持度,找出不少于认为设定的最小支持度的集合
2:发现关联规则:这些规则必须满足最小支持度和最小置信度。
Apriori算法使用一种称为逐层搜索的迭代方法,其中k项集用于搜索(k+1)项集。首先,通过扫描数据库,累计每个项的计数,并收集满足最小支持度的项,找出频繁1项集。该项集记为L1,。然后使用L1找出频繁2项集的集合L2,使用L2找出L3,如此下去,直到不能再找出频繁k项集。找出每个Lk需要一次数据库的完整扫描。
Apriori算法使用频繁项集的先验知识,先验知识:频繁项集的所有非空子集也一定是频繁的。例如一个项集为{A,B,C}为频繁的,那么他的子集{A},{B},{C},{A,B},{A,C},{B,C},{A,B,C}都是频繁的。
Apri算法只是会挖掘频繁模式,并不会找出关联规则,关联规则需要在找到频繁项集之后再来进行计算。
Apriori算法的两个输入参数分别是数据集和最小支持度。该算法首先会生成所有单个元素的项集列表。接着扫描数据集来查看哪些项集满足最小支持度要求,那些不满足最小支持度的集合会被去掉。然后,对剩下来的集合进行组合以生成包含两个元素的项集。接下来,再重新扫描交易记录,去掉不满足最小支持度的项集。该过程重复进行直到所有项集都被去掉。
该算法需要不断寻找候选集,然后剪枝即去掉非频繁子集的候选集,时间复杂度由暴力枚举所有子集的指数级别O(n2)降为多项式级别,多项式具体系数视底层实现情况而定的。
Ariori算法有两个主要步骤:
1、连接:(将项集进行两两连接形成新的候选集)
利用已经找到的k个项的频繁项集Lk,通过两两连接得出候选集Ck+1,注意进行连接的Lk[i]Lk[j],必须有k−1个属性值相同,然后另外两个不同的分别分布在Lk[i]Lk[i],中,这样的求出的Ck+1为Lk+1的候选集。
2、剪枝:(去掉非频繁项集)
候选集 Ck+1中的并不都是频繁项集,必须剪枝去掉,越早越好以防止所处理的数据无效项越来越多。只有当子集都是频繁集的候选集才是频繁集,这是剪枝的依据。
下图是《数据挖掘概念与分析》中Apriori算法的过程图
算法的实现:
from numpy import *
def loadDataSet():
return [[1,3,4],[2,3,5],[1,2,3,5],[2,5]]
#获取候选1项集,dataset为事务集,返回一个list,每个元素都是set集合
def createC1(dataset):
C1=[] #元素个数为1的项集(非频繁项集,因为还没有同最小支持度比较)
for transaction in dataset:
for item in transaction:
if [item] not in C1:
C1.append([item])
C1.sort() #这里排序是为了生成新的候选集时可以直接认为两个n项候选集前面的部分相同,
#因为除了候选1项集外其他的候选n项集都是以二维列表的形式存在,所以要将候选1项集的每一个元素都转换为一个单独的集合
return list(map(frozenset,C1)) #map(frozenset, C1)的语义是将C1由Python列表转换为不变集合(frozenset,Python中的数据结构)
#找出候选集中的频繁项集
#dataSet为全部数据集,CK为大小为k(包含k个元素)的候选项集,minSupport为设定的最小支持度
def scanD(dataSet,CK,minSupport):
ssCnt={} #记录每个候选项的个数
for tid in dataSet:
for can in CK:
if can.issubset(tid): #判断can是不是tid的子集
ssCnt[can]=ssCnt.get(can,0)+1 #计算每一个项集出现的频率
numItems=float(len(dataSet))
retList=[]
supportData={}
for key in ssCnt:
support=ssCnt[key] / numItems
if support>minSupport:
retList.insert(0,key) #将频繁项集插入返回列表的首部,0表示插入的位置
supportData[key]=support
return retList,supportData #retList为在CK中找出的频繁项集(支持度大于minSupport),supportData记录各频繁集的支持度
#通过频繁项集列表Lk和项集个数k生成项集C(k+1)
def aprioriGen(Lk,k):
retList=[]
lenLK=len(Lk)
for i in range(lenLK):
for j in range(i+1,lenLK): #双重循环遍历除自己以外的项集
#前k-1项相同时,才将两个集合合并,合并后才能生成k+1项
L1=list(Lk[i])[:k-2]
L2=list(Lk[j])[:k-2]
L1.sort()
L2.sort()
if L1==L2:
retList.append(Lk[i] | Lk[j]) #去掉公共部分
return retList
#获取事务中的所有频繁项集
#CK表示项数为k的候选项集,最初的C1通过createC1函数生成,LK表示项数为K的频繁项集,supK为其支持度,LK和supK由scanD()函数通过CK计算而来
def apriori(dataSet,minSupport=0.5):
C1=createC1(dataSet) #从事务中获取候选1项集
D=list(map(set,dataSet)) #将事务中的每个元素转换为集合
L1,supportData=scanD(D,C1,minSupport) #获取频繁1项集和对应的支持度
L=[L1] #L用来存储所有的频繁项集
k=2
while(len(L[k-2])>0): #一直迭代到项集数目过大而在事务中不存在这种n项集
CK=aprioriGen(L[k-2],k) #根据频繁项集生成新的候选项集,CK表示项数为k的候选项集
LK,supK=scanD(D,CK,minSupport) #LK表示项数为k的频繁项集,supK表示其支持度
L.append(LK)
supportData.update(supK) #添加新频繁项集和她们的支持度
k+=1
return L,supportData
if __name__ == '__main__':
dataSet=loadDataSet()#获取事务集。每个元素都是列表
# C1=createC1(dataSet) #获取候选1项集,每个元素都是集合
# D=list(map(set,dataSet)) #转换事务格式,每个事务转换为list集合
# L1,suppDat=scanD(D,C1,0.5)
# print(L1,suppDat)
L,suppData=apriori(dataSet,minSupport=0.7)
print(L,suppData)
在实现该算法的时候需要注意以下两点:
1:由于Apriori算法中假定项集中的项是按照字典排序的,而集合本身是无序的,我们在必要的时候需要进行list和set的转换
2:由于要使用字典来记录项集的支持度,需要用项集做key,而可变集合是无法做key的,因此在适当的时机应该将项集转换为固定集合frozenset
1:基于散列的技术:一种基于散列的技术可以用于压缩候选k项集的集合Ck
2:事务压缩:不包含任何频繁k项集的事务不能包含任何(k+1)项集
3:划分:可以使用划分技术,它只需要两次数据库扫描就可以挖掘频繁项集
4:抽样:选定给定数据库D的随即样本S,然后在S而不是在D中搜索频繁项集
5:动态项集计数(在扫描的不同点添加候选项集):动态项集计数将数据库划分为用开始点标记的块。
Apriori算法可能受到两种非凡的开销:
1:可能仍然需要产生大量的候选项集
2:可能需要重复地扫描整个数据库,通过模式匹配查找一个很大的候选集、
FP-growth算法就无需这种代价昂贵的候选产生过程,它采用一种分治策略
1:将代表频繁项集的数据库压缩到一根频繁模式树,该树保留项集的关联信息
2:把这种压缩后的数据库划分为一组条件数据库,每个数据库关联一个频繁项集或“模式段”,分别挖掘每个条件数据库
FP-growth的基本过程:
1:构建FP树(需要对原始数据集扫描两遍,第一遍对所有元素项进行计数,第二遍只考虑频繁项集)
2:从Fp树中挖掘频繁项集
这里介绍该算法中的两个概念:
1:头指针表:用来记录每个元素项的总出现次数的数组,再附带一个指针指向FP树中该元素的第一个节点,这样,每个元素都构成一条单链表。第一次遍历数据集会获得每个元素项出现的频率,去掉不满足最小支持度的元素项,生成这个头指针表。
这里使用Python的字典作为数据结构,用来保存头指针表。以元素项为键值,保存出现的总次数和指向第一个相似元素项的指针。
2:元素项排序:频繁项的集合按支持度计数的逆减序排序,第二次遍历数据集时,会读入每个项集,去掉不满足最小支持度的元素项,然后对元素进行排序(过滤后重排序)
输入:数据集,最小支持度
输出:FP树,头指针表
1:遍历数据集,统计各元素项出现的次数,创建头指针表
2:移除头指针表中不能满足最小支持度的元素项
3:第二次遍历数据集,创建FP树,对每个数据集中的项集:
3.1:初始化FP树
3.2:对每项进行过滤和重排序
3.3:使用这个项集更新FP树,从FP树的根节点开始:
3.3.1:如果当前项集的第一个元素项存在于FP树当前节点的子结点中,则更新这个子节点的计数值
3.3.2:否咋创建新的子节点,更新头指针表
3.3.3:对当前项集的其余元素项和当前元素项的对应子节点递归3.3过程
从FP树中挖掘频繁项集的3个步骤:
1:从FP树中获取条件基模式
2:利用条件基模式创建条件FP树
3:迭代重复1,2,直到树包含一个元素项为止
条件模式基是以查找元素为结尾的路径集合,每一条路径其实都是一条前缀路径。对于每一个频繁项集,都要创建一颗条件FP树,以刚才发现的条件模式基作为输入,并通过相同的建树代码来构建这些树。
则生成的条件模式基为:
频繁项 | 前缀路径 |
---|---|
z | {}: 5 |
r | {x, s}: 1, {z, x, y}: 1, {z}: 1 |
x | {z}: 3, {}: 1 |
y | {z, x}: 3 |
s | {z, x, y}: 2, {x}: 1 |
t | {z, x, y, s}: 2, {z, x, y, r}: 1 |
在创建FP树的条件下,我们就可以递归查找频繁项集了
输入:我们当前已有的数据集的FP树(inTree,headerTable)
1:初始化一个空列表preFix表示前缀
2:初始化一个空列表freqItemList,用来接收生成的频繁项集
3: 对headerTable中的每个元素basePat(由小到大)递归:
3.1:记basePat +preFix为当前项的newFreqSet
3.2:将newFreqSet添加到freqItemList中
3.3:计算t的条件FP树 (myCondTree,myHead)
3.4:当条件FP树不为空时,继续下一步,否则退出递归
3.5:以t条件树为新输入,以newFreqSet为新的preFix,外加freqItemList递归这个过程
实例说明:
数据集为:
TID | 商品ID的列表 |
T100 | I1,I2,I5 |
T200 | I2,I4 |
T300 | I2,I3 |
T400 | I1,I2,I4 |
T500 | I1,I3 |
T600 | I2,I3 |
T700 | I1,I3 |
T800 | I1,I2,I3,I5 |
T900 | I1,I2,I3 |
构建FP树:
1:扫描数据集,获取1项集
I1 | I2 | I3 | I4 | I5 |
6 | 7 | 6 | 2 | 2 |
2:删除不满足最小支持度的项,(假定minSup=20%),并对其进行排序
I1 | I2 | I3 | I4 | I5 |
6 | 7 | 6 | 2 | 2 |
3:根据元素项出现的次数对数据集中的项集进行排序
TID | 商品ID的列表 |
T100 | I2,I1,I5 |
T200 | I2,I4 |
T300 | I2,I3 |
T400 | I2,I1,I4 |
T500 | I1,I3 |
T600 | I2,I3 |
T700 | I1,I3 |
T800 | I2,I1,I3,I5 |
T900 | I2,I1,I3 |
构建FP树
挖掘频繁项集
对于每一个元素项,获取其对应的条件模式基(conditional pattern base)。条件模式基是以所查找元素项为结尾的路径集合。每一条路径其实都是一条前缀路径。
项 | 条件模式基 | 条件FP树 | 产生的频繁模式 |
I5 | {{I2,I1:1},{I2,I1,I3:1}} | {I2,I5:2}、{I1,I5:2}、{I2,I1,I5:2} | |
I4 | {{I2,I1:1},{I2:1}} | {I2,I4:2} |
|
I3 | {{I2,I1:2},{I2:2},{I1:2}} | {I2,I3:4},{I1,I3:4},{I2,I1,I3:2} | |
I1 | {{I2:4}} | {I2,I1:4} |
算法实现:
#FP树中节点的类定义
class treeNode:
def __init__(self, nameValue, numOccur, parentNode):
self.name = nameValue #节点元素名称,在构造时初始化为给定值
self.count = numOccur #出现的次数,在构造时初始化为给定值
self.nodeLink = None #nodeLink 变量用于链接相似的元素项
self.parent = parentNode #指向当前节点的父节点
self.children = {} #空字典,存放节点的子节点
#增加节点出现的次数值
def inc(self, numOccur):
self.count += numOccur
#将树以文本形式显示,输出节点和子结点的FP树结构
def disp(self,ind=1):
print(' ' *ind,self.name,' ',self.count)
for child in self.children.values():
child.disp(ind+1)
#=============================构建FP树====================================================
#构建FP-tree ,dataSet为事务集,为一个字典,键为每一个事务,值为该事务出现的次数,minSup为最低支持度
def createTree(dataset,minSup=1):
#第一次遍历数据集,创建头指针表
headerTable={}
for trans in dataset:
for item in trans:
headerTable[item]=headerTable.get(item,0)+dataset[trans] #get方法返回指定键的内容,如果不存在则返回0
#移除不满足最小支持度的元素项
for k in list(headerTable.keys()): #因为字典要求在迭代中不能进行修改,所以转换为列表
if headerTable[k] < minSup:
del (headerTable[k])
#空元素集,返回为空
freqItemSet=set(headerTable.keys())
print('freqItemSet:' ,freqItemSet)
if len(freqItemSet) == 0:
return None ,None
#增加一个元素项,用于存放指向相似元素项指针
for k in headerTable:
headerTable[k]=[headerTable[k],None] #每个键的值,第一个数为个数,第二个为下一个节点的位置
print('headerTable :' ,headerTable)
retTree=treeNode('Null set ',1,None) #根节点
#第二次遍历数据集,创建FP树
for tranSet, count in dataset.items():
print(tranSet)
localID={} #用于记录频繁1项集的全部频率,用于排序
for item in tranSet:
if item in freqItemSet: #只考虑频繁项集
localID[item]=headerTable[item][0] #这个[0],因为之前加过一个数据项 ,headerTable中即存储了键,也存储了出现的次数
print('localIDKeys :',localID.keys())
print('localIdItems: ',localID.items())
if len(localID) > 0:
orderedItems=[v[0] for v in sorted(localID.items(), key=lambda p: p[1], reverse=True)] #排序,True表示降序
updateTree(orderedItems, retTree, headerTable, count) #更新FP树
print('orderItems:' ,orderedItems)
return retTree ,headerTable #返回树和头指针表
#根据一个排序过滤后的频繁项更新FP树
def updateTree(items,inTree,headerTable,count): #items是排序锅炉后的频繁项集,inTree表示FP树,headerTable为头指针表,count表示出现的次数
if items[0] in inTree.children:
#有该元素项时计数值+1
inTree.children[items[0]].inc(count)
else:
#没有这个元素项时创建一个新节点
inTree.children[items[0]]=treeNode(items[0],count,inTree)
#更新头指针表或前一个相似元素项节点的指针指向新节点
if headerTable[items[0]][1] == None: #如果是第一次出现,则在头指针表中增加对该节点的指向
headerTable[items[0]][1] == inTree.children[items[0]]
else:
updateHeader(headerTable[items[0]][1],inTree.children[items[0]])
if len(items) >1:
#对剩下的元素项迭代调用updateTree函数
updateTree(items[1::],inTree.children[items[0]],headerTable,count)
#对不是第一个出现的节点,更新头指针快块,就是添加到相似元素链表的尾部
def updateHeader(nodeToTest ,targetNode):
while (nodeToTest.nodeLink != None):
nodeToTest = nodeToTest.nodeLink
nodeToTest.nodeLink =targetNode
#===================================查找元素条件模式基===========================
#直接修改prefixPath的值,将当前节点leafNode添加到prefixPath的末尾,然后递归添加其父节点
#prefixPath就是一条从treeNode到根节点(不包含根节点)的路径
def ascnedTree(leafNode,prefixPath):
if leafNode.parent != None:
prefixPath.append(leafNode.name)
ascnedTree(leafNode.parent,prefixPath)
#为给定元素项生成一个条件模式基(前缀路径),basePet表示输入的频繁项,treeNode为当前FP树中对应的第一个节点
#函数返回值即为条件模式基condPats,用一个字典表示,键为前缀路径,值为计数值
def findPrefixPath(basePat,treeNode):
condPats={} #存储条件模式基
while treeNode != None:
prefixPath=[] #用于存储前缀路径
ascnedTree(treeNode,prefixPath) #生成前缀路径
if len(prefixPath) > 1:
condPats[frozenset(prefixPath[1:])] = treeNode.count #出现的数量就是当前叶子节点的数量
treeNode = treeNode.nodeLink #遍历下一个相同元素
return condPats
#=============================递归查找频繁项集==========================
#根据十五级获取FP树和频繁项
#遍历频繁项,生成每个频繁项的条件FP树和条件FP树的频繁项
#这样每个频繁项与他条件FP树的频繁项都构成了频繁项集
#inTree和headerTable是由createTree函数生成的事务集的FP树
#minSup表示最小支持度
#prefix请传入一个空集合,将在函数中用于保存当前前缀
#freqItemList请传入一个空列表([]),将用来存储生成的频繁项集
def mineTree(inTree,headerTable,minSup,preFix,freqItemList):
#对频繁项按出现的数量进行排序
sorted_headerTable=sorted(headerTable.items(),key=lambda p: p[1][0]) #返回重新排序的列表,每个元素是一个元组,[(key,[num,treeNode,()])]
bigL=[v[0] for v in sorted_headerTable] #获取频繁项
for basePat in bigL:
newFreqSet=preFix.copy() #新的频繁项集
newFreqSet.add(basePat) #当前前缀添加一个新元素
print('finalFrequent Item: ',newFreqSet)
freqItemList.append(newFreqSet) #所有的频繁项集列表
condPattBases=findPrefixPath(basePat,headerTable[basePat][1]) #获取条件模式基,就是basePat元素的所有前缀路径,就像一个新的事务集
print('condPattBases: ',basePat,condPattBases)
myCondTree,myHead =createTree(condPattBases,minSup) #创建条件FP树
print('head from conditional tree: ',myHead)
if myHead != None:
#用于测试
print('conditional tree for :',newFreqSet)
myCondTree.disp()
mineTree(myCondTree,myHead,minSup,newFreqSet,freqItemList) #递归知道不再有元素
#生成数据集
def loadSimpDate():
simpDate= [['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 simpDate
#将数据集转化为目标格式,转换后才可以在createTree函数中使用dataSet[trans]
def createInitSet(dataSet):
retDict={}
for trans in dataSet:
retDict[frozenset(trans)]=1
return retDict
def fpGrowth(dataSet,minSup=3):
initSet=createInitSet(dataSet) #转化为符合格式的事务集
myFPtree ,myHeaderTab =createTree(initSet,minSup) #形成FP树
freqItems= [] #用于存储频繁项集
mineTree(myFPtree,myHeaderTab,minSup,set([]),freqItems) #获取频繁项集
return freqItems #返回频繁项集
if __name__ == '__main__':
minSup=3
dataSet=loadSimpDate() #加载数据集
print(fpGrowth(dataSet))
例如,一个谷类早餐的零售商对 5000 名 学生的调查的案例。 用来研究是否在学生打完篮球后向学生推荐早餐。
数据表明:
60%的学生早上会先打篮球,
75%的学生吃这类早餐(包含打篮球后吃早餐的和不打篮球直接吃早餐的)
40% 的学生既打篮球又吃这类早餐。
假设支持度阈值 s=0.4,置信度阈值 c=60%。基于上面数据和假设我们可挖掘出强关联规则“(打篮球)→(吃早餐)”, 因为其(打篮球) 和(吃早餐)的支持度都大于支持度阈值,都是频繁项,而规则的置信度 c=40%60%=66.6%c=40%60%=66.6%也大于置信度阈值。
然而,以上的关联规则很容易产生误解,因为吃早餐的比例为 75%,大于 66%。
也就是说,本来不打篮球先选择吃这种早餐的概率大于75%,但是打完篮球的学生就不想吃这种早餐或者不吃早餐了。因为打球后的学生吃这种早餐的概率降到了66%。
所以打篮球与吃早餐实际上是负关联的。
所以强关联不一定是有趣的。
我们应该使用相关性度量(这里使用提升读度量)来表征关联提升。
也就是P(B/A)/P(B)P(B/A)/P(B)来表示在A出现的情况下推荐B是否比没出现A之前推荐B更好。
公式等价于lift(A,B)=P(A⋃B)P(A)P(B)
参考文献:
本篇博客只是用来记录,其中自己的见解很少,绝大部分来自博客:https://blog.csdn.net/luanpeng825485697/article/details/79383404
数据挖掘概念与技术第3版第六章
FP-growth算法