报告根据课本《数据挖掘概念与技术原书第三版》学习撰写
一.实验目的
1、熟悉Apriori算法和FP算法的具体实现过程。
2、体会挖掘频繁项集在营销模式中的作用。
3、熟悉频繁项集挖掘方法,并体会FP算法对于Apriori算法的效率提升。
二.实验内容
如表 1 所示,数据库中有 7 个交易记录,设最小支持度计数为 3,分别使用
Apriori 和 FP 增长算法找出所有的频繁项集,并比较两种挖掘过程的效率。
三.实验过程
1、算法/思路说明
Apriori算法:
1)如图所示,该数据库有7个事务。在算法第一次迭代时,每个项都是候选1项集C1的成员。简单扫描所有事物,计数他们所出现的次数。
2)根据题意,最小支持度计数为3,即min_sup=3,对应的相对支持度为3/7=0.43。由此确定频繁1项集的集合L1。它由满足最小支持度的候选1项集组成。在该题中,所有事物都满足最小支持度。
3)为了发现频繁2项集的集合L2,算法使用连接L1XL2产生的候选2项集的集合C2。在剪枝步,没有候选从C2删除,因为这些候选的每个子集也是频繁的。
4)扫描所有事务,累计C2中的每个候选项集的支持计数。并确定频繁2项集的集合L2。
5)产生候选3项集的集合C3,根据先验性质,频繁项集的所有子集必须是频繁的,进行剪枝。并确定L3。
6)算法使用L3XL3产生候选4项集的集合C4。尽管产生连接结果但是子集{a,b}不是频繁的。故C4为空,因此算法终止了,找出了所有的的频繁项集。
Apriori算法的具体推导过程如下:
FP算法:
1)同Apriori算法,该数据库有7个事务。在算法第一次迭代时,每个项都是候选1项集C1的成员。简单扫描所有事物,计数他们所出现的次数。
2)频繁项的集合按支持度计数的递减序排序。
3)FP树构造:首先,创建根结点,用“null”标记。第二次扫描数据库D,每个事务的项都按次序处理,并对每个事务创建一个分枝。当为一个事务考虑增加分枝时,沿共同前缀上的每个结点的计数增加1,为前缀之后的项创建结点和链接。
4)创建一个项头表,使每项通过一个结点链指向它在树中的位置。将数据库频繁模式的挖掘问题转换为挖掘FP树的问题。
5)由长度为1的频繁模式开始,构造它的条件模式基。然后构造它的FP数,并递归地在该树上进行挖掘。模式增长通过后缀模式与条件FP数产生的频繁模式连接完成。
该FP树的挖掘过程如下:
2、功能模块说明
Apriori算法:
1)构建初始候选项集的列表C1,C1是大小为1的所有候选项集的集合。
2)构建初始的频繁项集,即所有项集只有一个元素。
3)剪枝。
4)候选支持度计数和min_sup进行比较。
5)将新的项集的支持度数据加入原来的总支持度字典中。
6)将符合最小支持度要求的项集加入L。
7)新生成项集。
8)重复3-7直到停止。
9)返回所有满足条件的频繁项集的列表。
FP算法:
1)获取频繁1项,并排序。
2)将事务中的项插入到FP树中,并返回插入节点的id。
3)构建项头表以及FP树, 第二次扫描数据集。
4)构建后缀为一个项的条件模式基。
5)根据条件模式基,提取频繁项集。
6)构建以每个频繁1项为后缀的频繁项集。
7)宽度优先,递增构建频繁k项集。
8)深度优先,递归构建以某个项为后缀的频繁k项集。
四.实验结果
1)实验环境:
Anaconda3 Jupyter Notebook Python3.6
2)实验结果:(略)
两次结果一致,且均与推出来的结果一致,结果正确。
五.分析与讨论
关于两种算法挖掘过程的效率,Apriori算法虽然简单易实现,效果也不错,但是需要频繁地扫描数据集,IO费用很大。FP树增长算法有效地解决了这一问题,其通过两次扫描数据集构建FP树,然后通过FP树挖掘频繁项集。它将发现长频繁模式的问题转换成在较小的条件数据库中递归搜素一些较短的模式,然后连接后缀。它使用最不频繁的项做后缀,提供很好的的选择性。FP_Growth极大的降低了搜索效率。
Apriori算法代码来源于:Apriori算法
FP_Growth算法代码来源于:FP_growth算法
代码:
#From:https://blog.csdn.net/u013185349/article/details/108293202
#1、Apriori算法实现挖掘频繁项集
import copy
def PowerSetsBinary(items):
"""
找出集合的所有子集
"""
#generate all combination of N items
N = len(items)
#enumerate the 2**N possible combinations
for i in range(2**N):
combo = []
for j in range(N):
#test jth bit of integer i
if(i >> j ) % 2 == 1:
combo.append(items[j])
yield combo
def loadDataSet():
"""
创建一个用于测试的简单的数据集
"""
D = [['a', 'b', 'd','e'], ['b', 'c','d'], ['a', 'c','d','e'], ['a', 'b', 'd','e'], ['b', 'c','d','e'], ['b', 'd','e'], ['c','d']]
return D
def createC1(dataSet):
"""
构建初始候选项集的列表,即所有候选项集只包含一个元素,
C1是大小为1的所有候选项集的集合
"""
C1 = []
for transaction in dataSet:
for item in transaction:
if [item] not in C1:
C1.append([item])
C1.sort()
# return map( frozenset, C1 )
# return [var for var in map(frozenset,C1)]
return [frozenset(var) for var in C1]
def scanDataSet(D, Ck, minSupport):
"""
计算Ck中的项集在数据集合D(记录或者transactions)中的支持度,
返回满足最小支持度的项集的集合,和所有项集支持度信息的字典。
"""
subSetCount = {}
# D=[{},{},{}] tid.type==set
for tid in D:
# Ck = [{},{},{}],can.type==frozenset
for can in Ck:
# 检查候选k项集中的每一项的所有元素是否都出现在每一个事务中,若true,则加1
if can.issubset(tid):
# subSetCount为候选支持度计数,get()返回值,如果值不在字典中则返回默认值0。
subSetCount[can] = subSetCount.get(can, 0) + 1
numItems = float(len(D))
returnList = []
# 选择出来的频繁项集,未使用先验性质
supportData = {}
for key in subSetCount:
# 计算绝对支持度。
support = subSetCount[key] / numItems # 每个项集的支持度
if support >= minSupport: # 将满足最小支持度的项集,加入returnList
returnList.insert(0, key)
supportData[key] = support # 汇总支持度数据
return returnList, supportData
def aprioriGen(Lk, k): # Aprior算法
"""
由初始候选项集的集合Lk生成新的生成候选项集,
k表示生成的新项集中所含有的元素个数
"""
returnList = []
for i in range(len(Lk)):
L1 = list(Lk[i])[: k - 2]
for j in range(i + 1, len(Lk)):
# Lk[i].type == frozenset
# 只需取前k-2个元素相等的候选频繁项集即可组成元素个数为k+1的候选频繁项集
L2 = list(Lk[j])[: k - 2]
L1.sort()
L2.sort()
if L1 == L2:
# print("k:{}---L1:{}---L2:{}".format(k, Lk[i], Lk[j]))
# 返回一个包含Lk[i]和Lk[j]中每一个元素的集合set,相当于集合的union方法
returnList.append(Lk[i] | Lk[j])
# print("returnList:{}".format(returnList))
return returnList
def has_infrequent_subset(L, Ck, k):
Ckc = copy.deepcopy(Ck)
for i in Ck:
p = [t for t in i]
i_subset = PowerSetsBinary(p)
subsets = [i for i in i_subset]
# print(subsets)
for each in subsets:
# print(each)
if each!=[] and each!=p and len(each)<k:
# [t for z in L for t in z]将列表中的frozenset全部移到一层中
if frozenset(each) not in [t for z in L for t in z]:
Ckc.remove(i)
break
return Ckc
def apriori(dataSet, minSupport):
# 构建初始候选项集C1
C1 = createC1(dataSet)
# 将dataSet集合化,以满足scanDataSet的格式要求
D = [set(var) for var in dataSet]
# 构建初始的频繁项集,即所有项集只有一个元素
L1, suppData = scanDataSet(D, C1, minSupport)
# 最初的L1中的每个项集含有一个元素,新生成的
L = [L1]
# 项集应该含有2个元素,所以 k=2
k = 2
while (len(L[k - 2]) > 0):
Ck = aprioriGen(L[k - 2], k)
# 剪枝
Ck2 = has_infrequent_subset(L, Ck, k)
# 候选支持度计数和min_sup进行比较
Lk, supK = scanDataSet(D, Ck2, minSupport)
# 将新的项集的支持度数据加入原来的总支持度字典中
suppData.update(supK)
# 将符合最小支持度要求的项集加入L
L.append(Lk)
# 新生成的项集中的元素个数应不断增加
k += 1
# 返回所有满足条件的频繁项集的列表,和所有候选项集的支持度信息
return L[:-1], suppData
if __name__ == '__main__':
myDat = loadDataSet()
#根据题意min_sup=3,故相对支持度为3/7
L, suppData = apriori(myDat, 3/7)
print("频繁项集L:", L)
#From:https://blog.csdn.net/slx_share/article/details/80232317
#2、FP树增长算法发现频繁项集
from collections import defaultdict, Counter, deque
import math
import copy
class node:
def __init__(self, item, count, parent): # 本程序将节点之间的链接信息存储到项头表中,后续可遍历项头表添加该属性
self.item = item # 该节点的项
self.count = count # 项的计数
self.parent = parent # 该节点父节点的id
self.children = [] # 该节点的子节点的list
class FP:
def __init__(self, minsup=0.5):
self.minsup = minsup
self.minsup_num = None # 支持度计数
self.N = None
self.item_head = defaultdict(list) # 项头表
self.fre_one_itemset = defaultdict(lambda: 0) # 频繁一项集,值为支持度
self.sort_rules = None # 项头表中的项排序规则,按照支持度从大到小有序排列
self.tree = defaultdict() # fp树, 键为节点的id, 值为node
self.max_node_id = 0 # 当前树中最大的node_id, 用于插入新节点时,新建node_id
self.fre_itemsets = [] # 频繁项集
self.fre_itemsets_sups = [] # 频繁项集的支持度计数
def init_param(self, data):
self.N = len(data)
self.minsup_num = math.ceil(self.minsup * self.N)
self.get_fre_one_itemset(data)
self.build_tree(data)
return
def get_fre_one_itemset(self, data):
# 获取频繁1项,并排序,第一次扫描数据集
c = Counter()
for t in data:
c += Counter(t)
for key, val in c.items():
if val >= self.minsup_num:
self.fre_one_itemset[key] = val
sort_keys = sorted(self.fre_one_itemset, key=self.fre_one_itemset.get, reverse=True)
self.sort_rules = {k: i for i, k in enumerate(sort_keys)} # 频繁一项按照支持度降低的顺序排列,构建排序规则
return
def insert_item(self, parent, item):
# 将事务中的项插入到FP树中,并返回插入节点的id
children = self.tree[parent].children
for child_id in children:
child_node = self.tree[child_id]
if child_node.item == item:
self.tree[child_id].count += 1
next_node_id = child_id
break
else: # 循环正常结束,表明当前父节点的子节点中没有项与之匹配,所以新建子节点,更新项头表和树
self.max_node_id += 1
next_node_id = copy.copy(self.max_node_id) # 注意self.max_node_id 是可变的,引用时需要copy
self.tree[next_node_id] = node(item=item, count=1, parent=parent) # 更新树,添加节点
self.tree[parent].children.append(next_node_id) # 更新父节点的孩子列表
self.item_head[item].append(next_node_id) # 更新项头表
return next_node_id
def build_tree(self, data):
# 构建项头表以及FP树, 第二次扫描数据集
one_itemset = set(self.fre_one_itemset.keys())
self.tree[0] = node(item=None, count=0, parent=-1)
for t in data:
t = list(set(t) & one_itemset) # 去除该事务中非频繁项
if len(t) > 0:
t = sorted(t, key=lambda x: self.sort_rules[x]) # 按照项的频繁程度从大到小排序
parent = 0 # 每个事务都是从树根开始插起
for item in t:
parent = self.insert_item(parent, item) # 将排序后的事务中每个项依次插入FP树
return
def get_path(self, pre_tree, condition_tree, node_id, suffix_items_count):
# 根据后缀的某个叶节点的父节点出发,选取出路径,并更新计数。suffix_item_count为后缀的计数
if node_id == 0:
return
else:
if node_id not in condition_tree.keys():
current_node = copy.deepcopy(pre_tree[node_id])
current_node.count = suffix_items_count # 更新计数
condition_tree[node_id] = current_node
else: # 若叶节点有多个,则路径可能有重复,计数叠加
condition_tree[node_id].count += suffix_items_count
node_id = condition_tree[node_id].parent
self.get_path(pre_tree, condition_tree, node_id, suffix_items_count) # 递归构建路径
return
def get_condition_tree(self, pre_tree, suffix_items_ids):
# 构建后缀为一个项的条件模式基。可能对应多个叶节点,综合后缀的各个叶节点的路径
condition_tree = defaultdict() # 字典存储条件FP树,值为父节点
for suffix_id in suffix_items_ids: # 从各个后缀叶节点出发,综合各条路径形成条件FP树
suffix_items_count = copy.copy(pre_tree[suffix_id].count) # 叶节点计数
node_id = pre_tree[suffix_id].parent # 注意条件FP树不包括后缀
if node_id == 0:
continue
self.get_path(pre_tree, condition_tree, node_id, suffix_items_count)
return condition_tree
def extract_suffix_set(self, condition_tree, suffix_items):
# 根据条件模式基,提取频繁项集, suffix_item为该条件模式基对应的后缀
# 返回新的后缀,以及新添加项(将作为下轮的叶节点)的id
new_suffix_items_list = [] # 后缀中添加的新项
new_item_head = defaultdict(list) # 基于当前的条件FP树,更新项头表, 新添加的后缀项
item_sup_dict = defaultdict(int)
for key, val in condition_tree.items():
item_sup_dict[val.item] += val.count # 对项出现次数进行统计
new_item_head[val.item].append(key)
for item, sup in item_sup_dict.items():
if sup >= self.minsup_num: # 若条件FP树中某个项是频繁的,则添加到后缀中
current_item_set = [item] + suffix_items
self.fre_itemsets.append(current_item_set)
self.fre_itemsets_sups.append(sup)
new_suffix_items_list.append(current_item_set)
else:
new_item_head.pop(item)
return new_suffix_items_list, new_item_head.values()
def get_fre_set(self, data):
# 构建以每个频繁1项为后缀的频繁项集
self.init_param(data)
suffix_items_list = []
suffix_items_id_list = []
for key, val in self.fre_one_itemset.items():
suffix_items = [key]
suffix_items_list.append(suffix_items)
suffix_items_id_list.append(self.item_head[key])
self.fre_itemsets.append(suffix_items)
self.fre_itemsets_sups.append(val)
pre_tree = copy.deepcopy(self.tree) # pre_tree 是尚未去除任何后缀的前驱,若其叶节点的项有多种,则可以形成多种条件FP树
self.dfs_search(pre_tree, suffix_items_list, suffix_items_id_list)
return
def bfs_search(self, pre_tree, suffix_items_list, suffix_items_id_list):
# 宽度优先,递增构建频繁k项集
q = deque()
q.appendleft((pre_tree, suffix_items_list, suffix_items_id_list))
while len(q) > 0:
param_tuple = q.pop()
pre_tree = param_tuple[0]
for suffix_items, suffix_items_ids in zip(param_tuple[1], param_tuple[2]):
condition_tree = self.get_condition_tree(pre_tree, suffix_items_ids)
new_suffix_items_list, new_suffix_items_id_list = self.extract_suffix_set(condition_tree, suffix_items)
if new_suffix_items_list:
q.appendleft(
(condition_tree, new_suffix_items_list, new_suffix_items_id_list)) # 储存前驱,以及产生该前驱的后缀的信息
return
def dfs_search(self, pre_tree, suffix_items_list, suffix_items_id_list):
# 深度优先,递归构建以某个项为后缀的频繁k项集
for suffix_items, suffix_items_ids in zip(suffix_items_list, suffix_items_id_list):
condition_tree = self.get_condition_tree(pre_tree, suffix_items_ids)
new_suffix_items_list, new_suffix_items_id_list = self.extract_suffix_set(condition_tree, suffix_items)
if new_suffix_items_list: # 如果后缀有新的项添加进来,则继续深度搜索
self.dfs_search(condition_tree, new_suffix_items_list, new_suffix_items_id_list)
return
if __name__ == '__main__':
data2 = [list('abde'), list('bcd'), list('acde'), list('abde'), list('bcde'),
list('bde'), list('cd')]
fp = FP(minsup=3/7)
fp.get_fre_set(data2)
for itemset, sup in zip(fp.fre_itemsets, fp.fre_itemsets_sups):
print(itemset, sup)