使用一种你熟悉的程序设计语言,实现(1)Apriori算法和(2)FP-growth算法。
目录
- 1、Apriori算法
- 2、F-Growth算法
- 3、两种算法比较
1、Apriori算法
def item(dataset): # 求第一次扫描数据库后的 候选集,(它没法加入循环)c1 = [] # 存放候选集元素for x in dataset: # 就是求这个数据库中出现了几个元素,然后返回for y in x:if [y] not in c1:c1.append([y])c1.sort()# print(c1)return c1def get_frequent_item(dataset, c, min_support):cut_branch = {} # 用来存放所有项集的支持度的字典for x in c:for y in dataset:if set(x).issubset(set(y)): # 如果 x 不在 y中,就把对应元素后面加 1cut_branch[tuple(x)] = cut_branch.get(tuple(x),0) + 1 # cut_branch[y] = new_cand.get(y, 0)表示如果字典里面没有想要的关键词,就返回0# print(cut_branch)Fk = [] # 支持度大于最小支持度的项集, 即频繁项集sup_dataK = {} # 用来存放所有 频繁 项集的支持度的字典for i in cut_branch:if cut_branch[i] >= min_support: # Apriori定律1 小于支持度,则就将它舍去,它的超集必然不是频繁项集Fk.append(list(i))sup_dataK[i] = cut_branch[i]return Fk, sup_dataKdef get_candidate(Fk, K): # 求第k次候选集ck = [] # 存放产生候选集for i in range(len(Fk)):for j in range(i + 1, len(Fk)):L1 = list(Fk[i])[:K - 2]L2 = list(Fk[j])[:K - 2]L1.sort()L2.sort() # 先排序,在进行组合if L1 == L2:if K > 2: # 第二次求候选集,不需要进行减枝,因为第一次候选集都是单元素,且已经减枝了,组合为双元素肯定不会出现不满足支持度的元素new = list(set(Fk[i]) ^ set(Fk[j])) # 集合运算 对称差集 ^ (含义,集合的元素在t或s中,但不会同时出现在二者中)# new表示,这两个记录中,不同的元素集合# 为什么要用new? 比如 1,2 1,3 两个合并成 1,2,3 我们知道1,2 和 1,3 一定是频繁项集,但 2,3呢,我们要判断2,3是否为频繁项集# Apriori定律1 如果一个集合不是频繁项集,则它的所有超集都不是频繁项集else:new = set()for x in Fk:if set(new).issubset(set(x)) and list(set(Fk[i]) | set(Fk[j])) not in ck: # 减枝 new是 x 的子集,并且 还没有加入 ck 中ck.append(list(set(Fk[i]) | set(Fk[j])))# print(ck)return ckdef Apriori(dataset, min_support=2):c1 = item(dataset) # 返回一个二维列表,里面的每一个一维列表,都是第一次候选集的元素f1, sup_1 = get_frequent_item(dataset, c1, min_support) # 求第一次候选集F = [f1] # 将第一次候选集产生的频繁项集放入 F ,以后每次扫描产生的所有频繁项集都放入里面sup_data = sup_1 # 一个字典,里面存放所有产生的候选集,及其支持度K = 2 # 从第二个开始循环求解,先求候选集,在求频繁项集while (len(F[K - 2]) > 1): # k-2是因为F是从0开始数的 #前一个的频繁项集个数在2个或2个以上,才继续循环,否则退出ck = get_candidate(F[K - 2], K) # 求第k次候选集fk, sup_k = get_frequent_item(dataset, ck, min_support) # 求第k次频繁项集F.append(fk) # 把新产生的候选集假如Fsup_data.update(sup_k) # 字典更新,加入新得出的数据K += 1return F, sup_data # 返回所有频繁项集, 以及存放频繁项集支持度的字典if __name__ == '__main__':n = int(input("共有几组数据?"))testList = [[] for i in range(n)]for i in range(n):testList[i] = list(map(str, input().split(' '))) # 装入数据 二维列表print(testList)F, sup_data = Apriori(testList, min_support=2) # 最小支持度设置为2print("具有关联的商品是{}".format(F)) # 带变量的字符串输出,必须为字典符号表示print('------------------')print("对应的支持度为{}".format(sup_data))
2、F-Growth算法
import copyclass FpNode():def __init__(self, name='', childs={}, parent={}, nextCommonId={}, idCount=0):self.idName = name # 名字self.childs = childs # 所有孩子结点self.parent = parent # 父节点self.nextCommonId = nextCommonId # 下一个相同的 id名字 结点self.idCount = idCount # id 计数def getName(self): # 获取该节点名字return self.idNamedef getAllChildsName(self): # 获取该节点所有孩子节点的名字ch = self.childskeys = list(ch.keys())names = []for i in keys:names.append(list(i))return namesdef printAllInfo(self): # 打印该节点所有信息print(self.idName, self.idCount, list(self.childs.keys()), list(self.parent.keys()), self.nextCommonId.items())@classmethoddef checkFirstTree(cls, rootNode): # 前序遍历整个树(这不是二叉树,没有中序遍历)if rootNode is None:return ''# parent1 = rootNode.parent.keys() #要加一个 强转 ,否则它会变成 Nopetype 型,rootNode.printAllInfo() # print(rootNode.idName, type(rootNode.parent)) 报错 root <class 'NoneType'>if rootNode.childs is not None:keys = list(rootNode.childs.keys())for i in keys:cls.checkFirstTree(rootNode.childs[i])@classmethoddef checkBehindTree(cls, rootNode): # 后序遍历整个树if rootNode is None:return ''if rootNode.childs is not None:keys = list(rootNode.childs.keys())for i in keys:cls.checkBehindTree(rootNode.childs[i])rootNode.printAllInfo()def scan1_getCand1(database): # 第一次扫描统计出现的次数c1 = {} # 候选集for i in database:for j in i:c1[j] = c1.get(j, 0) + 1 # 表示如果字典里面没有想要的关键词,就返回0# print(c1)return c1# 返回排好序的字典# 对数据进行排序,按支持度由大到小排列
def sortData(**d): # 形参前添加两个 '*'——字典形式 形参前添加一个 '*'——元组形式sortKey = list(d.keys()) # 直接使用sorted(my_dict.keys())就能按key值对字典排序sortValue = list(d.values())length = len(sortKey)for i in range(length - 1): # 按照支持度大小,由大到小排序的算法for j in (i, length - 1 - 1): # 必须 -1 (1,len)虽然不包含 len本身 但是数组【len-1】时最后一个元素,必须减去这个元素if sortValue[i] < sortValue[j + 1]:sortValue[i], sortValue[j + 1] = sortValue[j + 1], sortValue[i] # 如果它的支持度小与另一个,交换位置sortKey[i], sortKey[j + 1] = sortKey[j + 1], sortKey[i]new_c1 = {} # 存放排完序的数据记录for i in range(length):new_c1[sortKey[i]] = sortValue[i]return new_c1 # 返回排好序的字典# 得到 database 的频繁项集
def getFreq(database, minSup=3, **c1): # 返回频繁项集,和频繁项集的支持度c1 = scan1_getCand1(database) # 第一次扫面数据库,求第一次候选集,返回的是字典new_c1 = sortData(**c1) # 排序,大到小keys = list(new_c1.keys())for i in keys:if new_c1[i] < minSup: # 若支持度小于最小支持度,则删除该商品del new_c1[i]f1 = [] # 第一次频繁项集new_keys = list(new_c1.keys())for i in new_keys:if [i] not in f1:f1.append([i]) # 每个元素自成一项# print(f1,new_c1)return f1, new_c1def createRootNode(): # 创建一个根节点rootNode = FpNode('root', {}, {}, {}, -1) # name, childs, parent, nextCommonId, idCountreturn rootNodedef buildTree(database, rootNode, f1): # 构建频繁模式树 FpTreefor i in database: # 第二次扫描数据库present = rootNode # 指向当前节点next = FpNode(name='', childs={}, parent={}, nextCommonId={}, idCount=0) # 创建一个新节点,并初始化for j in f1: # 按支持度从大到小的顺序进行构建节点if set(j).issubset(set(i)): # j如果在i里面if (present.getName() == 'root') and j not in rootNode.getAllChildsName():next.idName = str(j[0]) # 对新创建的节点进行赋值next.idCount = next.idCount + 1next.nextCommonId = {str(j[0]): 0}next.parent.update({rootNode.idName: rootNode})temp = copy.copy(next)rootNode.childs.update({str(j[0]): temp}) # 往它插入父亲节点##print(temp.parent)present = temp # present = next 这样直接赋值是 引用 ,一定要注意next = FpNode(name='', childs={}, parent={}, nextCommonId={}, idCount=0) # 创建并初始化下一个新节点else:if j in present.getAllChildsName(): # 如果需要插入的节点已经存在temp2 = present.childs[str(j[0])]present = temp2present.idCount = present.idCount + 1 # count+1即可else:next.idName = str(j[0]) # 对新插入的节点赋值next.idCount = next.idCount + 1next.nextCommonId = {str(j[0]): 0}next.parent.update({present.idName: present})# temp3 = copy.copy(next)present.childs.update({str(j[0]): next}) # 往它插入父亲节点# temp3.childs = {}present = nextnext = FpNode(name='', childs={}, parent={}, nextCommonId={}, idCount=0)# present = next# next = FpNode()# print(rootNode.getAllChildsName())# print('前序遍历如下:')# FpNode.checkFirstTree(rootNode)# print('后序遍历如下:')# FpNode.checkBehindTree(rootNode)return None# 构建线索,填节点的nextCommonId这个属性
def buildIndex(rootNode, d1): # 传 列表或字典时,列表前,加*, 字典前加 ** 表示传给函数的是一个地址,在函数内部改变这个参数,不会影响到函数外的变量if rootNode is None:return ''next = rootNode # 指向下一个节点,当前赋值为根节点value = rootNode.idName# print(value)# print(d1[str(value)]) #d1[value] {KeyError}'a'??????????????? 如果value是根节点root,就会出错,表中本来就没有root这个值# print(d1)if value != 'root':indexAds1 = {value: d1[value]}if d1[value] == 0: # 线索构造 我已经把初始化了所有的 nextCommonId 为 {'': 0}# 所以后面只要 这个节点的 nextCommonId字典的值为0,就说明这个字典就是构建的链表链尾d1[value] = next# print(indexAds1)else:while indexAds1[value] != 0:indexAds1 = indexAds1[value].nextCommonId # 以链表形式把最后一个 表尾元素找出来# print(indexAds1)indexAds1[value] = next # 这个元素后面加入 当前所在树的这个节点的地址# print(next.nextCommonId)if rootNode.childs is not None: # 根节点孩子不是null,则对它的每个孩子,依次递归进行线索构建keys = list(rootNode.childs.keys())for i in keys:buildIndex(rootNode.childs[i], d1)def createIndexTableHead(**indexTableHead): # 创建一个表头,用来构建线索,表头的名字是相应节点的名字keys = list(indexTableHead.keys())# print(keys)for i in keys:indexTableHead[i] = 0return indexTableHeaddef getNewRecord(idK, **indexTableHead): # 得到新的数据记录newData = []address = indexTableHead[idK]while address != 0:times = 0times = address.idCount # 当前节点count数l = [] # 临时存放这个分支上的所有节点元素,单个单个存储 二维列表getOneNewR = [] # 和l一样,是l的倒叙,因为l本来是倒叙的,现在把它改成倒叙# print(list(address.parent.keys())[0]) #这样写才是 字符 c 而不是 'c'nextAdress = copy.copy(address) # 一个指针,指向父亲节点,初始化为表头第一个的地址while list(nextAdress.parent.keys())[0] != 'root': # 该节点发父亲节点不是根节点。则# print(address.parent)l.append(list(nextAdress.parent.keys())) # 把它的父亲节点加入l中parentIdName = list(nextAdress.parent.keys())[0] # 父亲节的名字nextAdress = nextAdress.parent[parentIdName] # 指向该节点父亲节点if l != []:for j in l:getOneNewR.append(j[0])if getOneNewR != []:for k in range(times): # 若最后的那个 idk 计数为多次,要把它多次添加到新产生的newData中newData.append(list(getOneNewR))# 把得到的记录加入新的数据集中address = address.nextCommonId[idK] # 指向下一个表头元素的开始地址,进行循环return newData# idK表示当前新产生的数据集是在去除这个字母后形成的, fk是去除掉idk后,新的第一次频繁项集 dk是fk的支持度
def getAllConditionBase(newDatabase, idK, fk, minSup, **dk): # 返回条件频繁项集 base, 和支持度if fk != []: # 频繁项集非空newRootNode = createRootNode() # 创建新的头节点buildTree(newDatabase, newRootNode, fk)# newIndexTableHead = {} #创建新表头newIndexTableHead = createIndexTableHead(**dk) # **dk 就是传了个值,给了它一个拷贝,修改函数里面的这个拷贝,不会影响到外面的这个变量的值buildIndex(newRootNode, newIndexTableHead)else:return [idK], {idK: 9999} # 频繁项集是空的,则返回idk的名字,支持度设为最大值9999,这样会出现一些问题,最后已经解决了,在主函数代码中有表现出来if len(newRootNode.getAllChildsName()) < 2: # 新的FpTree只有1条分支,(这里只认为根节点只有1个孩子,就说他只有一条分支)# 若是实际数据,就不能这样写了,应当在写一个函数,从根节点开始遍历,确保每个节点都只有1个孩子,才能认为只有1条分支base = [[]] # 条件基node = newRootNodewhile node.getAllChildsName() != []: # 当前节点有孩子节点childName = list(node.childs.keys()) # 一个列表,孩子节点的所有名字,其实就1个孩子,前面已经判断了是单节点base.append(list(childName[0])) # 把孩子节点加入条件基# print(node.childs)# print(childName)node = node.childs[childName[0]] # 指向下一个节点# print(base)itemSup = {node.idName: node.idCount} # 这一条分支出现的次数,最后求频繁项集支持度需要用到# print(itemSup)return base, itemSup # 返回条件基,还有这一条分支出现的次数,else: # 分支不止1条,进行递归查找,重复最开始的操作base = [[]]for commonId in fk[-1::-1]: # 倒叙进行newIdK = str(commonId[0])newDataK = getNewRecord(newIdK, **newIndexTableHead) # 传入这个表头的一个拷贝fk2, dk2 = getFreq(newDataK, minSup)conditionBase, itemSup = getAllConditionBase(newDataK, newIdK, fk2, minSup, **dk2) # 得到该条件基下的条件基,及各个分支出现次数# 递归进行base.append(conditionBase)return base, itemSup# FpGrowth算法本身(Frequent Pattern Growth—-频繁模式增长)
def FpGrowth(database, minSup=3):f1, d1 = getFreq(database, minSup) # 求第一次频繁项集,并返回一个字典存放支持度,且按大到小排序,返回频繁项和存放频繁项支持度的字典rootNode = createRootNode() # 创建根节点# print(f1,d1) #[['a'], ['b'], ['c'], ['d']] {'a': 4, 'b': 4, 'c': 4, 'd': 3}# 第一步建造树buildTree(database, rootNode, f1)# indexTableHead = {} #创建线索的表头,一个链表indexTableHead = createIndexTableHead(**d1) # **d1 就是传了个值,给了它一个拷贝,修改函数里面的这个拷贝,不会影响到外面的这个变量的值buildIndex(rootNode, indexTableHead) # 创建线索,用这个表头# print('构建线索后,前序遍历如下:')# FpNode.checkFirstTree(rootNode)# print('构建线索后,后序遍历如下:')# FpNode.checkBehindTree(rootNode)freAll = [] # 所有频繁项集freAllDic = {} # 所有频繁项集的支持度# 第二步 进行频繁项集的挖掘,从表头header的最后一项开始。for commonId in f1[-1::-1]: # 倒叙 从支持度小的到支持度大的,进行挖掘idK = str(commonId[0])newDataK = getNewRecord(idK, **indexTableHead) # 传入这个表头的一个拷贝, 函数返回挖掘出来的新记录fk, dk = getFreq(newDataK, minSup) # 对新数据集求频繁项集# print(fk,dk)base, itemSup = getAllConditionBase(newDataK, idK, fk, minSup, **dk) # 得到当前节点的条件频繁模式集,返回# 有可能会发生这样一种情况,条件基是 a ,然后fk,dk为空,结果这个函数又返回了 a,那么最后的结果中,就会出现 a,a 这种情况,处理方法请往下看# print(base,idK)for i in base:# print(i)t = list(i)t.append(idK)t = set(t) # 为了防止出现 重复 的情况,因为我的getAllConditionBase(newDataK, idK, fk, minSup, **dk)方法的编写,可能会形成重复,如 a,at = list(t)freAll.append(t)itemSupValue = list(itemSup.values())[0]x = tuple(t) # 列表不能做字典的关键字,因为他可变,,而元组可以# <class 'list'>: ['c', 'd']# print(t[0]) # t是列表,字典的关键字不能是可变的列表, 所以用 t[0] 来取出里面的值freAllDic[x] = min(itemSupValue, d1[idK])# print(freAll)# print(freAllDic)return freAll, freAllDicif __name__ == '__main__':n = int(input("共有几组数据?"))testList = [[] for i in range(n)]for i in range(n):testList[i] = list(map(str, input().split(' '))) # 装入数据 二维列表print("原始数据:", testList)freAll, freAllDic = FpGrowth(testList, minSup=3) # 设置最小支持度为3print("频繁项集", freAll)print("各个频繁项集的支持度依次为:")for i in freAllDic.keys():print(i, ":", freAllDic[i])
3、两种算法比较
在以下情况下,FP-growth算法比Apriori算法好:
①数据量大。因为Apriori算法需要生成大量的候选项集,并对每个候选项集进行频繁项集的计数,这会消耗大量的时间和内存,而FP-growth算法通过构建FP树,将数据集压缩成了一个紧凑的数据结构,大大减少了存储空间和计算时间的开销。
②数据分布不均匀。因为Apriori算法在生成候选项集时,需要遍历数据集多次,对于数据分布不均匀的情况,频繁项集的计算会受到影响。而FP-growth算法通过构建FP树,将频繁项集的计算转化为对FP树的遍历,不受数据分布的影响。
③最小支持度阈值较低。因为Apriori算法在生成候选项集时,需要对每个候选项集进行频繁项集的计数,当最小支持度阈值较低时,需要计算大量的候选项集,导致计算时间较长。而FP-growth算法通过构建FP树,只需要遍历一次数据集,计算频繁项集,所以FP-growth算法更快速。