从大规模数据集中寻找物品间的隐含关系被称作关联分析(association analysis ) 或者关联规则学习(association rule learning)
频繁项集(frequent item sets)是经常出现在一块的物品的集合。
关联规则(association rules )暗示两种物品之间可能存在很强的关系。
一个项集的支持度(support)(支持度是针对项集来说的)被定义为数据集中包含该项集的记录所占的比例。
可信度或置信度(confidence)是针对一条诸如{尿布}
→
\to
→ {葡萄酒}的关联规则来定义的。这条规则的可信度被定义为 “ 支持度({尿布,葡萄酒})/支持度({尿布}) ” 从图11-1中可以看到,由于 {尿布,葡萄酒}的支持度为3/5,尿布的支持度为4/5,所以“尿布
→
\to
→葡萄酒”的可信度为3/4=0.75。
Apriori原理是说如果某个项集是频繁的,那么它的所有子集也是频繁的。
对于图11-2给出的例子,这意味着如果{0,1}是频繁的,那么{0}、{1}也一定是频繁的。

这个原理直观上并没有什么帮助,但是如果反过来看就有用了,也就是说如果一个项集是非频繁集,那么它的所有超集也是非频繁的。

一旦计算出了{2,3}的支持度,知道它是非频繁的之后,就不需要再计算{0,2,3}、{1,2,3}和 {0,1,2,3}的支持度,因为我们知道这些集合不会满足我们的要求。使用该原理就可以避免项集数目的指数增长,从而在合理时间内计算出频繁项集。
需要找到频繁项集,然后才能获得关联规则。
数据集扫描的伪代码大致如下:
对数据集中的每条交易记录tran
对每个候选项集can:
检查一下can是否是tran的子集:
如果是,则增加can的计数值
对每个候选项集:
如果其支持度不低于最小值,则保留该项集
返回所有频繁项集列表
APriori算法中的辅助函数实现如下:
from numpy import *
#加载数据
def loadDataSet():
return [[1,3,4],[2,3,5],[1,2,3,5],[2,5]]
#用于对数据集构建初始项集
def createC1(dataSet):
# 用来存放初始项集
C1 =[]
#遍历每一个交易记录
for transaction in dataSet:
# 遍历记录中的每一个物品
for item in transaction:
# 以列表作为元素,因为不能单独对一个int执行set
if not [item] in C1:
# 不在则作为新物品添加
C1.append([item])
# 排序函数
C1.sort()
# frozenset是不可变集合,将C1中的每一个物品的变成不可变的集合
return list(map(frozenset,C1))
# 求最频繁项集的支持度
def scanD(D,Ck,minSupport):
ssCnt = {}
# 遍历数据集的所有交易记录
for tid in D:
# 遍历当前的项集
for can in Ck:
# can中的所有元素是否都在tid中
if can.issubset(tid):
# python3废除了has_key方法,可以用get 方法代替
ssCnt[can] = ssCnt.get(can,0) + 1
numItems = float(len(list(D)))
# 用来存放满足支持度的那些项集
retList = []
# 用来存放所有项集与其对应的支持度
supportData = {}
for key in ssCnt:
support1 = ssCnt[key] / numItems
# 如果满足最小支持度的要求
if support1 >= minSupport:
# 将key插入到索引为0的位置
retList.insert(0,key)
supportData[key] = support1
return retList,supportData
dataSet = loadDataSet()
C1 = createC1(dataSet)
D = list(map(set,dataSet ))
#支持度设为0.5
L1,suppData0 = scanD(D, C1, 0.5)
print("候选项集:",C1,end="\n")
print("满足最小支持度的项集:",L1,end="\n")
输出如下:

通过辅助函数我们可以得到设置的满足最小支持度要求的项集。
整个Apriori算法的伪代码如下:
当集合中项的个数大于0时
构建一个K个项组成的候选项集的列表
检查数据以确认每个项集都是频繁的
保留频繁项集并构建k+1项组成的候选项集的列表
Aprioria算法实现如下:
#产生要求的项集元素个数的项集组合
def aprioriGen(Lk,k):
# 用来存放新的项集
retList = []
lenLk = len(Lk)
# 构建新项集的思路是遍历每个元素,两两之间如果只有1个不相同就可以加在一起
# 当前项集中每一个元素的长度为k
for i in range(lenLk):
for j in range(i+1, lenLk):
# 取出前k-1个元素,就是索引到k-2,
# 记得下标索引从0开始
L1 = list(Lk[i])[:k-2]
L2 = list(Lk[j])[:k-2]
L1.sort()
L2.sort()
# 如果相等,说明Lk[i]和Lk[j]只有第k个元素不等
if L1 == L2:
# 采用集合的并方法将其添加到新集合中
retList.append(Lk[i] | Lk[j])
return retList
#apriori函数
def apriori(dataSet,minSupport=0.5):
C1 = createC1(dataSet)
# 这里set不是将dataSet中的重复交易记录去除掉
# 而是将dataSet的每一个交易记录中重复购买的东西去除掉
D = list(map(set,dataSet))
L1, supportData = scanD(D,C1,minSupport)
L = [L1]
k = 2
# 直到下一个项集为空
while len(L[k - 2]) > 0:
# 新的项集
Ck = aprioriGen(L[k-2],k)
# 选取符合最小支持度的
Lk, supK = scanD(D,Ck,minSupport)
# 将新的项集和对应的支持度更新到支持度字典中
supportData.update(supK)
# 直到Lk为空,下一次循环的时候就退出了
L.append(Lk)
k += 1
return L,supportData
dataSet = loadDataSet()
L , suppData = apriori(dataSet)
print("满足最小支持度的频繁项集:",L,end="\n")
输出结果如下:

可以看到,现在将满足我们要求的所有的频繁集输出了。从单个项集到含有三个成员的项集。因为{4}不是频繁项集,所以输出都不包含{4}这项。
量化指标称为可信度。
一条规则P
→
\to
→H 的可信度定义为:
support(P|H) /support ( P)
P | H是指所有出现在集合P或者集合H 中的元素。
箭头左边的集合称作前件,箭头右边的集合称为后件。
如果某条规则并不满足最小可信度要求,那么该规则的所有子集也不会满足最小可信度要求。
关联规则生成函数:
# 生成一个包含可信度的规则列表
def generateRules(L, supportData, minConf = 0.7):
# 用来存储符合条件的规则
bigRuleList = []
# 索引0中只有单个元素无法构建规则,从索引1有两个元素的开始
for i in range(1,len(L)):
# 遍历当前的每一个频繁项集
for freqSet in L[i]:
# 拆成单个元素的集合构成的列表
Hl = [frozenset([item]) for item in freqSet]
# i>1,则说明每个频繁项集大小大于2,那么可能有右端大于1的可能
if i > 1:
rulesFromConseq(freqSet,Hl,supportData,bigRuleList,minConf)
# i=1的话,频繁项集只有2的元素,只有右端为1的可能
else:
calcConf(freqSet,Hl,supportData,bigRuleList,minConf)
return bigRuleList
# 求满足最小可信度要求的规则列表
# freqset为当前的频繁项集,H存放其子集,不是全部的,长度不断增加的
def calcConf(freqSet,H,supportData,brl,minConf = 0.7):
# 用来存放那些规则的可信度满足要求的右端
prunedH = []
# 遍历freqset当前H长度的子集
for conseq in H:
conf = supportData[freqSet] / supportData[freqSet - conseq]
# 计算可信度,-代表集合的去除操作
if conf >= minConf:
print(freqSet-conseq,"--->",conseq,' conf: ',conf)
# 存放所有的满足条件的规则
brl.append((freqSet-conseq,conseq,conf))
# 用来放当前满足的,返回可以进入下一次迭代
prunedH.append(conseq)
return prunedH
# 从项集中生成更多的关联规则
def rulesFromConseq(freqSet, H,supportData,brl,minConf = 0.7):
# 当前子集的长度
m = len(H[0])
# 如果大于说明还能移除大小为m+1的子集来构建规则
if len(freqSet) > (m+1):
# 用H中的集合取拼凑成长度为m+1的子集
Hmp1 = aprioriGen(H,m+1)
Hmp1 = calcConf(freqSet,Hmp1,supportData,brl,minConf)
# 计算当前频繁项集与Hmp1这些子集的规则的可信度,返回的符合条件的规则的右端放入Hmp1中
if len(Hmp1) > 1:
rulesFromConseq(freqSet,Hmp1,supportData,brl,minConf)
dataSet = loadDataSet()
L , suppData = apriori(dataSet)
print("满足最小支持度的频繁项集:",L,end="\n")
rules=generateRules( L , suppData,minConf =.7)
print("关联规则:",rules,end="\n")
输出结果如下:

输出结果展示了三条规则,{1}
→
\to
→{3}。{5}
→
\to
→{2}、{2}
→
\to
→{5}规则{5}
→
\to
→{2}可以互换前件与后件。
降低可信度阈值为0.6后输出结果如下:

可以看到输出的规则有11条。随着可信度下降,我们能得到的规则也越多。
这个示例要数据集没下下来。程序没办法跑。简单过一下算法。
def getActionIds():
actionIdList = []; billTitleList = []
fr = open('./Ch11/recent20bills.txt')
for line in fr.readlines():
# 得到了议案的ID
billNum = int(line.split('\t')[0])
try:
# 得到一个billDetail对象
billDetail = votesmart.votes.getBill(billNum)
# 遍历议案中的所有行为
for action in billDetail.actions:
if action.level == 'House' and \
(action.stage == 'Passage' or action.stage == 'Amendment Vote'):
actionId = int(action.actionId)
print ('bill: %d has actionId: %d' % (billNum, actionId))
actionIdList.append(actionId)
billTitleList.append(line.strip().split('\t')[1])
# API调用时发生错误
except:
print ("problem getting bill %d" % billNum)
# 礼貌访问网站而做出些延迟,避免过度访问
sleep(1)
return actionIdList, billTitleList
这部分代码是用来获取数据的,得到了ID号列表与投票信息列表。
首先创建一个字典,字典中使用政客的名字作为键值,当某政客首次出现时,将他及其所属政党(民主党或者共和党)添加到字典中,这里使用0来代表民主党,1来代表共和党。然后对投票进行编码,其对每条议案使用两个条目:bill+’Yea’以及bill+’Nay’。
使用actionId串作为输入并利用voteSmart的API来抓取投票记录的函数。然后将每个选举人的投票转化为一个项集 。每个选举人对应于一行或者说事务数据库中的一条记录。

# 基于投票数据的事物列表填充函数
def getTransList(actionIdList, billTitleList):
# 创建一个含义列表
itemMeaning = ['Republican', 'Democratic']
# 遍历所有的议案
for billTitle in billTitleList:
# 在议案标题后面添加Nay(反对)
itemMeaning.append('%s -- Nay' % billTitle)
# 在议案标题后添加Yea(同意)
itemMeaning.append('%s -- Yea' % billTitle)
# 用于加入元素项
transDict = {}
voteCount = 2
# 遍历getActionIds()返回的每一个actionId
for actionId in actionIdList:
# 延迟访问,防止过于频繁的API调用
sleep(3)
print ('getting votes for actionId: %d' % actionId)
try:
# 获得某个特定的actionId的所有投票信息
voteList = votesmart.votes.getBillActionVotes(actionId)
# 遍历投票信息
for vote in voteList:
# 如果没有该政客的名字
if not transDict.has_key(vote.candidateName):
# 用该政客的名字作为键来填充transDict
transDict[vote.candidateName] = []
# 获取该政客的政党信息
if vote.officeParties == 'Democratic':
transDict[vote.candidateName].append(1)
elif vote.officeParties == 'Republican':
transDict[vote.candidateName].append(0)
if vote.action == 'Nay':
transDict[vote.candidateName].append(voteCount)
elif vote.action == 'Yea':
transDict[vote.candidateName].append(voteCount + 1)
except:
print ("problem getting actionId: %d" % actionId)
voteCount += 2
# 返回事物字典和元素项含义列表
return transDict, itemMeaning
实现代码如下:
#输出蘑菇特征频繁集
def mushroomChara():
fr = open("./Ch11/mushroom.dat")
#按行读取并拆分数据
mushDateSet = [line.split() for line in fr.readlines()]
L,suppData = apriori(mushDateSet,minSupport=0.3)
print(L[1])
for item in L[1]:
# 搜索包含有毒特征值2的频繁项集
# 返回集合item和集合{“2”}的交集
if(item.intersection('2')):
print(item)
return
mushroomChara()
输出结果如下:

实现很简单,导入数据集,根据数据集产生频繁项集。最后打印出频繁项集中所包含的所需特征的组合。
本章主要是两部分,首先是创建频繁项集。之后是通过频繁项集根据我们设置的可信度,找到对应的关联规则。
每次增加频繁项集的大小,apriori算法都会重新扫描整个数据集。当数据集很大时,这会显著降低频繁项集发现的速度。