[关联规则]FP-growth算法(介绍)

611 阅读6分钟

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. 算法原理

基于分治策略树结构压缩

  1. 第一次扫描:统计各项频率,删除非频繁项
  2. 第二次扫描:按频率降序排列事务项,构建FP-tree
  3. 挖掘FP-tree:从低频率项开始,递归构建条件模式基和条件FP-tree

5. 算法流程

  1. 构建头指针表

    • 扫描数据集,统计各项支持度
    • 删除支持度<min_support的项
    • 按支持度降序排序
  2. 构建FP-tree

    • 创建根节点(null)
    • 对每条事务:
      • 按头指针表顺序排序项
      • 插入树中(共享前缀路径)
    • 更新头指针表的链表指针
  3. 挖掘频繁项集

    • 从头指针表底部(最低频率)开始
    • 对每个项:
      • 生成条件模式基
      • 构建条件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

关键点解析

  1. FP-tree结构

    • 根节点为"Null"
    • 每个节点存储项名和计数
    • 相同项通过链表连接(头指针表)
  2. 构建过程

    graph TD
      A[第一次扫描数据集] --> B[创建头指针表]
      B --> C[过滤低频项]
      C --> D[按频率排序事务]
      D --> E[插入FP-tree]
      E --> F[更新头指针表]
    
  3. 挖掘过程

    graph LR
      A[从头指针表底部开始] --> B[获取条件模式基]
      B --> C[构建条件FP-tree]
      C --> D{树是否为空?}
      D -- 是 --> E[返回频繁项]
      D -- 否 --> B
    
  4. 性能对比

    特性AprioriFP-Growth
    扫描次数多次2次
    候选项集生成不生成
    内存使用中-低
    时间复杂度O(2ᴺ)O(N²)
    适用数据集中小型大型

应用场景

  1. 购物篮分析:发现商品关联规则(如啤酒→尿布)
  2. 推荐系统:基于频繁模式生成推荐
  3. 生物信息学:发现基因共现模式
  4. 网络日志分析:识别用户行为模式
  5. 异常检测:发现异常事件序列

注意:FP-Growth在大数据集上效率显著优于Apriori,但当数据集非常稀疏时,FP-tree可能不会有效压缩数据。实际应用中可结合具体数据特性选择算法。