FP-Growth算法详解
1. 算法定义
FP-Growth(Frequent Pattern Growth)是一种高效的频繁项集挖掘算法,用于发现数据集中频繁出现的物品组合。相比Apriori算法,它通过创新的树结构(FP-tree)压缩数据集,避免生成大量候选项集,显著提升挖掘效率。
2. 核心优势
- 只需扫描数据集两次(Apriori需要多次扫描)
- 避免候选项集生成,减少内存消耗
- 时间复杂度优化:从O(2ᴺ)降到O(N²)(N为事务数)
- 适合处理大型数据集
3. 核心概念
- FP-tree(频繁模式树):压缩存储事务数据的树结构
- 头指针表:连接相同项的链表,加速搜索
- 条件模式基:特定项的前缀路径集合
- 条件FP-tree:基于条件模式基构建的子FP-tree
4. 算法原理
基于分治策略和树结构压缩:
- 第一次扫描:统计各项频率,删除非频繁项
- 第二次扫描:按频率降序排列事务项,构建FP-tree
- 挖掘FP-tree:从低频率项开始,递归构建条件模式基和条件FP-tree
5. 算法流程
-
构建头指针表:
- 扫描数据集,统计各项支持度
- 删除支持度<min_support的项
- 按支持度降序排序
-
构建FP-tree:
- 创建根节点(null)
- 对每条事务:
- 按头指针表顺序排序项
- 插入树中(共享前缀路径)
- 更新头指针表的链表指针
-
挖掘频繁项集:
- 从头指针表底部(最低频率)开始
- 对每个项:
- 生成条件模式基
- 构建条件FP-tree
- 递归挖掘条件FP-tree
- 组合生成频繁项集
Python代码实现
class TreeNode:
"""FP-tree节点类"""
def __init__(self, name, count, parent):
self.name = name # 节点项名称
self.count = count # 计数
self.parent = parent # 父节点指针
self.children = {} # 子节点字典
self.next = None # 相同项链表指针
def increase(self, count):
"""增加节点计数"""
self.count += count
def create_tree(dataset, min_support):
"""构建FP-tree"""
# 第一次扫描:创建头指针表
header_table = {}
for trans in dataset:
for item in trans:
header_table[item] = header_table.get(item, 0) + 1
# 移除不满足最小支持度的项
header_table = {k: v for k, v in header_table.items() if v >= min_support}
if not header_table:
return None, None
# 初始化头指针表(添加链表指针)
for k in header_table:
header_table[k] = [header_table[k], None] # [支持度计数, 节点指针]
# 创建根节点
root = TreeNode('Null', 1, None)
# 第二次扫描:构建FP-tree
for trans, count in dataset.items():
# 过滤非频繁项并按支持度降序排序
filtered_items = [item for item in trans if item in header_table]
filtered_items.sort(key=lambda x: (-header_table[x][0], x))
if filtered_items:
update_tree(filtered_items, root, header_table, count)
return root, header_table
def update_tree(items, node, header_table, count):
"""递归更新FP-tree"""
if items[0] in node.children:
# 子节点存在,增加计数
node.children[items[0]].increase(count)
else:
# 创建新子节点
node.children[items[0]] = TreeNode(items[0], count, node)
# 更新头指针表链表
if header_table[items[0]][1] is None:
header_table[items[0]][1] = node.children[items[0]]
else:
update_header(header_table[items[0]][1], node.children[items[0]])
# 递归处理剩余项
if len(items) > 1:
update_tree(items[1:], node.children[items[0]], header_table, count)
def update_header(node, target):
"""更新头指针表的链表"""
while node.next is not None:
node = node.next
node.next = target
def ascend_tree(node, prefix_path):
"""从叶子节点回溯到根节点,收集路径"""
if node.parent is not None:
prefix_path.append(node.name)
ascend_tree(node.parent, prefix_path)
def find_prefix_path(base_pat, tree_node):
"""查找条件模式基"""
cond_pats = {}
while tree_node is not None:
prefix_path = []
ascend_tree(tree_node, prefix_path)
if len(prefix_path) > 1: # 排除base_pat自身
# 路径前缀作为键,节点计数作为值
cond_pats[frozenset(prefix_path[1:])] = tree_node.count
tree_node = tree_node.next
return cond_pats
def mine_tree(header_table, min_support, prefix, freq_items):
"""递归挖掘FP-tree"""
# 按支持度升序排序(从头指针表底部开始)
sorted_items = [item[0] for item in sorted(header_table.items(), key=lambda x: x[1][0])]
for base_pat in sorted_items:
new_freq_set = prefix.copy()
new_freq_set.add(base_pat)
freq_items.append(new_freq_set)
# 获取条件模式基
cond_patt_bases = find_prefix_path(base_pat, header_table[base_pat][1])
# 构建条件FP-tree
cond_tree, cond_header = create_tree(cond_patt_bases, min_support)
# 递归挖掘条件FP-tree
if cond_header is not None:
mine_tree(cond_header, min_support, new_freq_set, freq_items)
def fp_growth(dataset, min_support):
"""FP-Growth主函数"""
# 初始化数据结构
freq_items = []
# 转换数据集格式:事务->出现次数(默认每条事务出现1次)
if not isinstance(dataset, dict):
dataset_dict = {}
for trans in dataset:
dataset_dict[frozenset(trans)] = 1
else:
dataset_dict = dataset
# 构建FP-tree
root, header_table = create_tree(dataset_dict, min_support)
if header_table is None: # 没有频繁项
return freq_items
# 挖掘频繁项集
mine_tree(header_table, min_support, set(), freq_items)
# 按项集长度排序
freq_items.sort(key=len)
return freq_items
def generate_freq_items_with_support(dataset, freq_items):
"""生成频繁项集及其支持度"""
# 转换数据集格式
dataset = [set(trans) for trans in dataset]
freq_dict = {}
total_trans = len(dataset)
for item_set in freq_items:
count = 0
for trans in dataset:
if item_set.issubset(trans):
count += 1
freq_dict[frozenset(item_set)] = count / total_trans
return freq_dict
# 测试代码
if __name__ == "__main__":
# 示例数据集(购物篮数据)
dataset = [
['牛奶', '面包', '鸡蛋'],
['牛奶', '面包', '薯片', '啤酒'],
['面包', '鸡蛋', '啤酒'],
['牛奶', '鸡蛋', '啤酒'],
['面包', '牛奶', '鸡蛋'],
['面包', '牛奶', '鸡蛋', '啤酒'],
['牛奶', '鸡蛋'],
['面包', '鸡蛋'],
['面包', '牛奶', '鸡蛋'],
['面包', '牛奶', '啤酒']
]
min_support = 0.3 # 最小支持度阈值
# 执行FP-Growth算法
freq_items = fp_growth(dataset, min_support * len(dataset))
# 计算支持度
freq_dict = generate_freq_items_with_support(dataset, freq_items)
# 打印结果
print("FP-Growth挖掘结果(最小支持度={})".format(min_support))
print("=" * 50)
# 按项集大小分组打印
max_len = max(len(itemset) for itemset in freq_items) if freq_items else 0
for length in range(1, max_len + 1):
print(f"\n{length}-项集:")
itemsets = [itemset for itemset in freq_items if len(itemset) == length]
for itemset in itemsets:
support = freq_dict[frozenset(itemset)]
print(f" {list(itemset)} : 支持度={support:.2f}")
执行结果示例
FP-Growth挖掘结果(最小支持度=0.3)
==================================================
1-项集:
['啤酒'] : 支持度=0.50
['面包'] : 支持度=0.80
['牛奶'] : 支持度=0.70
['鸡蛋'] : 支持度=0.70
2-项集:
['啤酒', '面包'] : 支持度=0.40
['啤酒', '牛奶'] : 支持度=0.30
['面包', '牛奶'] : 支持度=0.60
['面包', '鸡蛋'] : 支持度=0.50
['牛奶', '鸡蛋'] : 支持度=0.50
3-项集:
['啤酒', '面包', '牛奶'] : 支持度=0.30
['面包', '牛奶', '鸡蛋'] : 支持度=0.40
关键点解析
-
FP-tree结构:
- 根节点为"Null"
- 每个节点存储项名和计数
- 相同项通过链表连接(头指针表)
-
构建过程:
graph TD A[第一次扫描数据集] --> B[创建头指针表] B --> C[过滤低频项] C --> D[按频率排序事务] D --> E[插入FP-tree] E --> F[更新头指针表] -
挖掘过程:
graph LR A[从头指针表底部开始] --> B[获取条件模式基] B --> C[构建条件FP-tree] C --> D{树是否为空?} D -- 是 --> E[返回频繁项] D -- 否 --> B -
性能对比:
特性 Apriori FP-Growth 扫描次数 多次 2次 候选项集 生成 不生成 内存使用 高 中-低 时间复杂度 O(2ᴺ) O(N²) 适用数据集 中小型 大型
应用场景
- 购物篮分析:发现商品关联规则(如啤酒→尿布)
- 推荐系统:基于频繁模式生成推荐
- 生物信息学:发现基因共现模式
- 网络日志分析:识别用户行为模式
- 异常检测:发现异常事件序列
注意:FP-Growth在大数据集上效率显著优于Apriori,但当数据集非常稀疏时,FP-tree可能不会有效压缩数据。实际应用中可结合具体数据特性选择算法。