想象你是城中一家火锅店的经理。柜台上堆着厚厚一叠点菜单——每张菜单记录了一桌客人点了哪些菜。几百张菜单翻下来,你发现「毛肚」和「锅底」几乎总是同时出现。如果能把这种“经常一起出现”的规律找出来,就可以设计套餐、调整进货、给客人推荐菜品,把营业额翻一番。这种从海量记录中挖掘“共现规律”的方法,就叫关联分析,而它最经典的算法就是 Apriori 频繁项集。 Apriori 的思路很像一个聪明的后厨帮手:它不会傻到把菜单上所有菜品两两、三三组合都去试一遍——那样组合数会是天文数字。它先看哪些菜被点的次数多(这叫支持度),然后只在这些热门菜之间继续找组合,一层层往下筛,最终找出所有“经常一起出现”的菜品组合,也就是频繁项集。再进一步,它还能算出“点了锅底的人,有多大概率会加毛肚”——这就是关联规则。 本文会带你从零搭建这套思维:从频繁项集怎么定义,到 Apriori 怎么高效寻找、怎么剪枝,再到更快的 FP-Growth 升级版,最后看它在电商、医疗里的真实应用。读完你会明白,所谓“啤酒与尿布”的传说,不过是一张点菜单的智慧。

岗位关联

关联分析是电商推荐、用户行为挖掘、医疗诊断联动的底层能力。掌握 Apriori,你就掌握了“从交易记录中提取可执行规律”的数据挖掘基本功,这是数据科学家与 AI 产品经理的必备思维。

目录
  1. 可视化演示
  2. 关联分析入门:一张点菜单的智慧
  3. 频繁项集定义:总被一起点的菜
  4. Apriori原理:聪明的逐层排查
  5. 候选项集生成:后厨的搭配草稿
  6. 剪枝策略:划掉没戏的草稿
  7. FP-Growth优化:一张索引卡记住所有点单
  8. 关联规则生成:从“常一起出现”到“如何推荐”
  9. 关联分析应用:把规律变成真金白银
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Apriori算法逐层扫描、向下封闭剪枝、支持度计算及FP-Growth优化对比,通过多模式交互理解关联分析核心原理。

本节配有交互动效(进入视口后自动加载,约 3KB)。

逐层扫描流程 L1扫描所有1-项集并计算支持度 L2用L1频繁项集连接生成2-候选 非频繁项集标记并剪枝 Support(X) = count(X) / |T| 逐层向上直到无新频繁项集

关联分析入门:一张点菜单的智慧

新来的火锅店经理小李,每天面对一叠厚厚的点菜单,心里直犯愁:菜单上十几道菜,哪些菜该一起推荐?哪些菜该备足货?他试着用手工统计,发现「毛肚」和「锅底」几乎形影不离。可几十种组合一个个翻,眼睛都花了。他需要的是一套能自动从海量点菜单里找出“共现规律”的方法——这就是关联分析。

关联分析处理的原始材料叫交易记录。在点菜单比喻里,一张点菜单就是一条交易记录,记录里每道菜叫做一个项,一组菜的集合叫做项集。而形如“点了锅底 → 也会点毛肚”这样的推断,叫做关联规则。关联分析的输入是一堆交易记录,输出是一组有统计支撑的规律。

所有规律都建立在两个基本度量上:支持度和置信度。支持度衡量一个项集“有多常见”,是整个 Apriori 流程的基石。它的定义非常直观:

小李把这个公式在笔记本上写下来,开始逐张菜单数“锅底+毛肚”出现几次。他知道,这只是第一步——有了支持度这把尺子,他才能从众多组合中量出真正的热门。下一节,他就要用这把尺子给所有组合定“门槛”了。

support(X)=count(X)N\displaystyle \text{support}(X) = \frac{\text{count}(X)}{N}
一个项集的支持度,就是包含它的交易记录条数除以总交易条数。
  1. support(X)项集 X 的支持度,回答“X 在所有菜单里占多大比例”。例如 X = {锅底,毛肚},支持度就是同时点了这两样菜的菜单在所有菜单中的占比。
    support(X)\text{support}(X)
  2. count(X)包含项集 X 的交易条数。在比喻里,就是“同时点了锅底和毛肚的菜单张数”。
    count(X)\text{count}(X)
  3. N交易总数,也就是你手里的点菜单总张数。如果小李手上有 1000 张菜单,N = 1000。
    NN
  4. 整体含义支持度 = 出现的次数 ÷ 总次数。支持度越高,说明这个组合越“大众化”,越有分析价值。
    support(X)=count(X)N\displaystyle \text{support}(X) = \frac{\text{count}(X)}{N}

频繁项集定义:总被一起点的菜

小李很快发现,光算支持度还不够。菜单上有十几种菜,两两组合有上百种,他需要一个标准来决定“哪些组合值得关注”。于是他为每条组合设定了一个门槛:如果“锅底+毛肚”出现在超过 40% 的菜单上,他就认为这是值得关注的频繁项集。这个门槛,就是最小支持度阈值(min\_sup)。

在关联分析里,频繁项集就是支持度不低于阈值的项集。阈值设多少,直接影响挖掘结果的“含金量”。阈值太高(比如 0.9),几乎没有组合能达标,你会漏掉很多潜在的搭配;阈值太低(比如 0.05),满屏都是“牛奶+餐巾纸”这种偶然共现,噪声淹没了信号。小李把阈值定为 0.4,因为他希望找的是“四个人里至少两桌会点”的稳定搭配。

“频繁”的判定可以写成一条简洁的准则:

小李用这个标准一筛,发现“锅底+毛肚”“锅底+虾滑”都在频繁项集名单上,心里有底了。但紧接着他又冒出新的烦恼:如果要把“三菜组合”“四菜组合”也一一筛选,工作量会不会爆炸?他需要一种更聪明的方法。

Frequent(X)  ⟺  support(X)≥min_sup\displaystyle \text{Frequent}(X) \iff \text{support}(X) \ge \text{min\_sup}
项集 X 是频繁的,当且仅当 X 的支持度大于等于最小支持度阈值。
  1. Frequent(X)表示“项集 X 是频繁项集”。频繁的意思是“经常一起出现”,它是后续生成规则的基础原料。
    Frequent(X)\text{Frequent}(X)
  2. min\_sup最小支持度阈值。它是人为设定的“热门门槛”,就像餐厅规定“上座率低于 40% 的套餐不予考虑”。
    min_sup\text{min\_sup}
  3. 支持度比较support(X) 是计算出来的客观比例,min\_sup 是主观设定的门槛。两者一比较,就完成了从“数字”到“决策”的跨越。
    support(X)≥min_sup\text{support}(X) \ge \text{min\_sup}
  4. 整体判定超过门槛就入选频繁项集,低于门槛就被淘汰。这就是 Apriori 每一步都在做的事。
    Frequent(X)  ⟺  support(X)≥min_sup\displaystyle \text{Frequent}(X) \iff \text{support}(X) \ge \text{min\_sup}

Apriori原理:聪明的逐层排查

菜单上如果有 20 道菜,三菜组合就有 1140 种,四菜组合更多。如果每一种都去数支持度,后厨就不用干别的了。小李灵机一动,想到了一个反推的逻辑:如果「西蓝花」这道菜单点都很少有人选——支持度只有 5%——那任何包含西蓝花的组合,比如「西蓝花+锅底」,支持度绝不可能超过 5%。既然连单点的西蓝花都达不到 40% 的门槛,包含它的组合更不可能频繁。想通这一点,他就可以大胆地把所有带西蓝花的组合整体跳过。

这就是 Apriori 的先验性质,也叫反单调性:项集的支持度,永远不会随着项集的变大而变大。用数学语言说,对于任意包含 X 的超集 Y:

有了这个“不增定律”,小李决定:先筛热门单品(1-项集),再从热门单品中组合出热门的双拼(2-项集),以此类推。每一层都站在上一层的肩膀上,而不是把整个菜单空间翻个底朝天。这套“逐层排查”的思路,就是 Apriori 算法的灵魂。

∀ Y⊇X: support(Y)≤support(X)\displaystyle \forall\, Y \supseteq X : \ \text{support}(Y) \le \text{support}(X)
包含关系上,父集越大,支持度只减不增。这是整个 Apriori 剪枝的数学基石。
  1. ∀Y ⊇ X对所有的超集 Y,即所有在 X 基础上又加了其它项的大集合。X = {毛肚} 的超集有 {毛肚,锅底}、{毛肚,虾滑} 等。
    ∀ Y⊇X\forall\, Y \supseteq X
  2. support(Y) ≤ support(X)超集的支持度不超过子集。因为凡是点了「毛肚+锅底+虾滑」的客人一定也点了毛肚,所以「三拼」出现的张数不可能比「毛肚」出现的张数还多。
    support(Y)≤support(X)\text{support}(Y) \le \text{support}(X)
  3. 等价表述如果 support(X) 已经低于阈值,那所有超集都低于阈值。这就是“如果某道菜不火,包含它的所有套餐都不火”。
    support(X)<min_sup⇒support(Y)<min_sup\text{support}(X) < \text{min\_sup} \Rightarrow \text{support}(Y) < \text{min\_sup}
  4. 策略意义Apriori 从 1-项集开始,逐层向上。每一层只用上一层的频繁项集来生成候选,避免了穷举所有组合。
    逐层搜索:L1→C2→L2→C3→⋯\displaystyle \text{逐层搜索}: L_1 \to C_2 \to L_2 \to C_3 \to \cdots

候选项集生成:后厨的搭配草稿

有了逐层思路,小李让后厨帮忙生成候选:如果已知两样菜各自都很火,那它们组成的双拼可能也火,值得去数一数。但对于双拼怎么组成三拼,后厨需要一条规则,保证每个组合只被列一次,不重复劳动。小李给出的答案是:两个双拼必须“前缀相同”才能合并。比如「锅底+毛肚」和「锅底+虾滑」都以前缀「锅底」开头,它们合并成「锅底+毛肚+虾滑」;而「毛肚+虾滑」就不会再和「锅底+毛肚」合并,因为它俩前缀不同。

在 Apriori 里,这一步叫自连接。设上一轮的频繁 (k-1)-项集为 L_{k-1},找出其中两个项集,把元素按统一顺序排好后,若前 k-2 个项完全相同,就把它们合并为候选的 k-项集:

后厨按这个规则列草稿,效率和秩序立竿见影。但小李注意到,有些草稿明显没戏——比如某个三拼里的某个双拼根本不是热门双拼。这种草稿还要不要送到数据库去验证?小李决定再加一道工序:剪枝。

Lk−1⋈Lk−1={X∪Y∣X,Y∈Lk−1, X[1..k−2]=Y[1..k−2]}\displaystyle L_{k-1} \bowtie L_{k-1} = \{ X \cup Y \mid X, Y \in L_{k-1},\ X[1..k-2] = Y[1..k-2] \}
候选 k-项集 = 两个前缀相同的 (k-1)-项集合并。
  1. L_{k-1}上一轮挖出的所有频繁 (k-1)-项集。例如 L₂ 是所有频繁双拼的名单。
    Lk−1L_{k-1}
  2. X ∪ Y把两个大小为 k-1 的项集做并集,得到大小为 k 的新项集。如 {锅底,毛肚} ∪ {锅底,虾滑} = {锅底,毛肚,虾滑}。
    X∪YX \cup Y
  3. X[1..k-2] = Y[1..k-2]连接条件。把项按固定顺序排好后,前 k-2 个项必须完全一样。这能保证每对集合最多合并一次,避免生成重复候选项。
    X[1..k−2]=Y[1..k−2]X[1..k-2] = Y[1..k-2]
  4. 整体流程先连接生成候选集 C_k,再扫描数据库验证哪些候选真的频繁。候选不是最终结论,只是“待验证的草稿”。
    Ck=gen(Lk−1)→扫描验证Lk\displaystyle C_{k} = \text{gen}(L_{k-1}) \quad \xrightarrow{\text{扫描验证}} \quad L_{k}

剪枝策略:划掉没戏的草稿

草稿列好后,小李发现三拼草稿「西蓝花+锅底+毛肚」里,随便抽出两样——「西蓝花+锅底」——根本不在热门双拼名单上。按照反向推理,既然双拼都不火,三拼更不可能火,这份草稿可以直接划掉,不必再花力气翻菜单验证。这一步就是先验剪枝:在扫描数据库之前,用“频繁项集的所有子集必频繁”这把尺子,先淘汰一批明显不合格的候选。

剪枝的数学依据,正是反单调性的逆否命题。给定一个候选 k-项集 C,检查它的所有 (k-1)-子集是否都在上一轮的频繁项集 L_{k-1} 中:

剪枝之后,工作量立刻少了大半。但小李的数数效率还是有限:每验证一个幸存候选,他都要把几百张菜单从头翻一遍。他想,要是有一棵“菜单关系树”,一次建好、反复查询,岂不是更快?这正是 FP-Growth 的灵感来源。

剪枝(C): ∃S⊂C, ∣S∣=k−1, S∉Lk−1 ⇒ C 被删除\displaystyle \text{剪枝}(C) : \ \exists S \subset C,\ |S| = k-1,\ S \notin L_{k-1} \ \Rightarrow \ C \text{ 被删除}
候选被剪掉的充要条件:存在一个子集不在上一轮频繁项集名单里。
  1. ∃S ⊂ C, |S| = k-1存在一个 C 的 (k-1)-子集 S。例如三拼 {锅底,毛肚,虾滑} 的 2-子集有 {锅底,毛肚}、{锅底,虾滑}、{毛肚,虾滑}。
    ∃S⊂C, ∣S∣=k−1\exists S \subset C,\ |S| = k-1
  2. S ∉ L_{k-1}S 不在上一轮的频繁项集名单里。如果 “锅底+虾滑” 这个双拼本身就少有人点,那它再配上更多菜的组合也不会火。
    S∉Lk−1S \notin L_{k-1}
  3. C 被删除候选直接划掉,不再进入数据库扫描环节。这一步省下的是实打实的统计工作量。
    C 被删除C \text{ 被删除}
  4. 整体连接生成全部候选 → 剪枝筛掉没戏的 → 只对幸存候选扫描数据库。每轮候选集被大幅压缩,计算量随之骤降。
    生成→剪枝→计数→新频繁项集\displaystyle \text{生成} \to \text{剪枝} \to \text{计数} \to \text{新频繁项集}

FP-Growth优化:一张索引卡记住所有点单

Apriori 每验证一轮候选集,都要重新扫描一次整个交易库。如果交易有几十万条,这个代价相当高。小李的解决办法是:不如一次性把所有点菜单整理成一张“菜品先后索引卡”——用一棵树把所有菜单的内容压缩存起来,之后任何组合的支持度,直接在这棵树上查,不需要再翻原始菜单。这就是 FP-Growth(频繁模式增长)算法的核心思想。

FP-Growth 只需要扫描交易库两次。第一次扫描,统计每个单品的支持度,去掉不频繁的项,并按支持度降序排序;第二次扫描,逐条交易按这个排好的顺序插入一棵树中,同一前缀路径上的节点共享,这就是频繁模式树。此后,支持度查询不再回看数据库,而是沿着树上的节点计数累加:

在树的帮助下,小李不仅能快速查出“锅底+毛肚”的支持度,还能通过条件模式基——即每个项在树上的前缀路径集合——递归地挖出更长的频繁项集。FP-Growth 用“空间换时间”的方式,把 Apriori 反复扫描的痛苦一步到位地化解了。但无论用什么算法,挖出来的频繁项集终究只是“食材”,小李还差最后一道“烹饪”:把它们变成真正能指导推荐的做法。

support(X)=∑所有标签为 X 的节点count(节点)\displaystyle \text{support}(X) = \sum_{\text{所有标签为 } X \text{ 的节点}} \text{count}(\text{节点})
项 X 的支持度 = FP 树中所有标签为 X 的节点计数值之和。
  1. FP 树节点树上的每个节点记录一个项,以及该项在多少条交易路径上经过。节点 count 就是“这条路径上有多少张菜单”。
    node with label X\text{node with label } X
  2. 节点的 count经过该节点的交易数量。比如“毛肚”节点 count = 5,表示有 5 张菜单的路径经过“毛肚”。
    count(node)\text{count}(\text{node})
  3. 求和同一个项在树中可能出现在多个分支,把所有分支上该节点的 count 相加,就得到这个项的总支持度。
    ∑nodescount(node)\sum_{\text{nodes}} \text{count}(\text{node})
  4. 为什么快树将数据库压缩成共享前缀的路径结构,存储和查询都不再依赖原始数据。Apriori 每轮全库扫描的代价,在 FP-Growth 中变成一次性的建树代价。
    support(X)=∑X nodescount(node)\displaystyle \text{support}(X) = \sum_{X \text{ nodes}} \text{count}(\text{node})

关联规则生成:从“常一起出现”到“如何推荐”

小李已经掌握了所有热门组合,但他真正关心的是:客人都点了锅底,我该不该推荐毛肚?推荐成功的把握有多大?这就需要一个新指标——置信度。置信度回答的是条件概率问题:“在点了 X 的客人里,有多大比例还点了 Y”:

但小李很快发现一个陷阱:毛肚本身就是餐厅的招牌菜,80% 的客人都会点。哪怕“锅底→毛肚”的置信度高达 85%,也可能只是“毛肚本身太火”造成的假象。为了识别真正有拉动力的搭配,他引入了提升度:

小李用提升度重新审视所有规则,筛掉了那些“靠招牌菜刷出来的高置信度”,留下真正能拉升客单价的搭配。到这里,一张点菜单的智慧已经被他完整地炼成了生意经。

confidence(X⇒Y)=support(X∪Y)support(X)\displaystyle \text{confidence}(X \Rightarrow Y) = \frac{\text{support}(X \cup Y)}{\text{support}(X)}
提升度 = 实际共现概率 ÷ 假设两者独立时的期望共现概率。
  1. 分子 support(X∪Y)实际观察到的 X、Y 共现比例。
    support(X∪Y)\text{support}(X \cup Y)
  2. 分母如果 X 和 Y 毫无关系(独立),共现的理论概率就是各自概率相乘,即 sup(X) × sup(Y)。
    support(X)×support(Y)\text{support}(X) \times \text{support}(Y)
  3. 整体判断lift > 1 表示正相关,X 的出现会提升 Y 的出现概率;lift = 1 表示独立;lift < 1 表示负相关。
    lift(X⇒Y)>1\text{lift}(X \Rightarrow Y) > 1
  4. 决策价值置信度高 + 提升度大于 1,才是一条真正有商业价值的规则。只靠置信度容易把“本来就火”误当成“被带动”。
    X⇒Y 有拉动价值X \Rightarrow Y \text{ 有拉动价值}

关联分析应用:把规律变成真金白银

小李把整套方法带回了火锅店,生意立刻不一样了。他把“锅底+毛肚”做成套餐,用更优惠的价格引导客人下单;他发现周五晚上的菜单上“啤酒+小吃”高频共现,于是周五推出“深夜小食组合”;他甚至发现,工作日中午的菜单里“单人套餐+酸梅汤”是稳定的频繁项集,于是调整了午市推荐位。一张点菜单上的规律,就这样变成了真金白银。

在现实世界中,关联分析的应用远不止火锅店。电商平台把 Apriori 用在交易流水上,发现“婴儿尿布”与“啤酒”在周五晚间的购买记录中频繁共现,于是策划了“周五奶爸专场”促销;医疗数据分析师用它挖掘检查与用药之间的共现规律,发现某两类疾病的药物组合会带来更高的不良反应概率,从而提前预警;安全工程师则把“登录→批量转账→更换设备”这类行为序列看作序列模式,偏离频繁序列的行为会被判定为可疑。

无论哪种应用,从频繁项集到可用决策,最终都要过“强规则”这一关。一条规则要被信任,需要同时满足覆盖量、准确度和相关性三个条件:

回想最开始,小李面对一堆点菜单手足无措。如今他有了 Apriori 帮他找热门组合,有了剪枝帮他减少无谓劳动,有了 FP-Growth 帮他提升效率,还有置信度、提升度帮他甄别规律的真伪。从一张普通的点菜单出发,他挖出了一整套经营智慧——这就是关联分析的魅力所在。

强规则: support(X∪Y)≥α ∧ confidence(X⇒Y)≥β ∧ lift(X⇒Y)>1\displaystyle \text{强规则} : \ \text{support}(X \cup Y) \ge \alpha \ \wedge \ \text{confidence}(X \Rightarrow Y) \ge \beta \ \wedge \ \text{lift}(X \Rightarrow Y) > 1
支持度保证覆盖量,置信度保证准确性,提升度保证相关性。三者缺一不可。
  1. support(X∪Y) ≥ α规则的覆盖范围足够大,避免仅靠一两条特例下结论。
    support(X∪Y)≥α\text{support}(X \cup Y) \ge \alpha
  2. confidence(X⇒Y) ≥ β推荐成功率足够高,点了 X 的人大概率会接受 Y。
    confidence(X⇒Y)≥β\text{confidence}(X \Rightarrow Y) \ge \beta
  3. lift(X⇒Y) > 1X 对 Y 有真实的拉动作用,而不是单纯因为 Y 本身太火。
    lift(X⇒Y)>1\text{lift}(X \Rightarrow Y) > 1
  4. 整体三个条件构成“可落地的强规则”——量大、精准、有增量。这也解释了为什么“尿布+啤酒”成为经典案例:它们各自不算最火,却在特定时段有显著的正向拉动。
    support≥α∧confidence≥β∧lift>1\displaystyle \text{support} \ge \alpha \wedge \text{confidence} \ge \beta \wedge \text{lift} > 1

AI 应用场景

电商平台的购物篮分析

阶段:数据探索与特征发现

解决的问题:手动分析销售记录很难发现多商品之间的组合规律,运营人员只能靠经验猜测哪些商品该捆绑销售。Apriori 自动挖掘高频商品组合,帮助平台设计捆绑套餐、优化货架摆放和促销排期。

具体案例:某大型电商平台对周五晚间的交易记录做关联分析,发现“婴儿尿布 + 啤酒”的组合支持度在周五显著上升,提升度大于 1。平台据此推出“周五奶爸囤货节”,将尿布与啤酒捆绑打折,该品类周销量提升 18%。

医疗症状与用药关联挖掘

阶段:关联规则挖掘与异常模式发现

解决的问题:病历中隐藏着症状、检查结果、用药之间的共现关系。传统人工分析只能覆盖少数已知不良反应,Apriori 可以在历史病历中自动搜索高频的“诊断-用药”组合。

具体案例:医疗数据分析团队对老年患者病历做关联分析,发现“高血压 + 高血糖”患者使用某类利尿剂后出现下肢关节疼痛的比例显著偏高,提升度 1.6。该线索被提交给临床药师复核,最终促成用药说明的补充。

用户行为序列的异常检测

阶段:序列模式挖掘与安全告警

解决的问题:正常用户的操作行为存在稳定的先后顺序,而欺诈行为往往呈现与高频序列模式明显偏离的轨迹。用关联分析提取频繁序列,就能让偏离者“现形”。

具体案例:某金融风控平台分析登录与交易行为的序列模式,发现“登录 → 连续多笔小额转账 → 更换设备号 → 大额转出”是高频欺诈序列。系统将该序列作为风险特征,在模型中加入序列命中因子,欺诈拦截率提高 22%。

Python 演示

python101 行
from itertools import combinations def load_data():    return [        ['牛奶', '面包', '啤酒'],        ['牛奶', '尿布', '啤酒', '面包'],        ['尿布', '牛奶', '啤酒'],        ['面包', '尿布', '啤酒'],        ['牛奶', '面包', '尿布'],        ['面包', '牛奶']    ] def create_c1(dataset):    c1 = set()    for tx in dataset:        for item in tx:            c1.add(frozenset([item]))    return c1 def scan(dataset, candidates, min_sup):    total = len(dataset)    count = {}    for tx in dataset:        tx_set = set(tx)        for cand in candidates:            if cand.issubset(tx_set):                count[cand] = count.get(cand, 0) + 1    freq = []    sup_map = {}    for cand, cnt in count.items():        sup = cnt / total        if sup >= min_sup:            freq.append(cand)            sup_map[cand] = sup    return freq, sup_map def apriori_gen(freq_list, k):    candidates = set()    freq_list = list(freq_list)    n = len(freq_list)    for i in range(n):        for j in range(i + 1, n):            a = sorted(freq_list[i])            b = sorted(freq_list[j])            if a[:k-2] == b[:k-2]:                candidates.add(frozenset(a) | frozenset(b))    return candidates def prune(candidates, freq_prev):    pruned = set()    for cand in candidates:        ok = True        for sub in combinations(cand, len(cand) - 1):            if frozenset(sub) not in freq_prev:                ok = False                break        if ok:            pruned.add(cand)    return pruned def apriori(dataset, min_sup=0.4):    c1 = create_c1(dataset)    freq1, sup_map = scan(dataset, c1, min_sup)    all_freq = [freq1]    k = 2    while len(all_freq[-1]) > 0:        candidates = apriori_gen(all_freq[-1], k)        candidates = prune(candidates, all_freq[-1])        freq_k, sup_k = scan(dataset, candidates, min_sup)        sup_map.update(sup_k)        if not freq_k:            break        all_freq.append(freq_k)        k += 1    return all_freq, sup_map def generate_rules(all_freq, sup_map, min_conf=0.7):    rules = []    for k in range(1, len(all_freq)):        for itemset in all_freq[k]:            for i in range(1, len(itemset)):                for antecedent in combinations(itemset, i):                    antecedent = frozenset(antecedent)                    consequent = frozenset(itemset) - antecedent                    if not consequent:                        continue                    conf = sup_map[frozenset(itemset)] / sup_map[antecedent]                    if conf >= min_conf:                        rules.append((antecedent, consequent, conf))    return rules if __name__ == '__main__':    dataset = load_data()    freq, sup = apriori(dataset, 0.4)    print('频繁项集:')    for level in freq:        for itemset in level:            print(' ', set(itemset), '支持度=%.2f' % sup[itemset])    print('关联规则(置信度>=0.7):')    for a, b, conf in generate_rules(freq, sup, 0.7):        print(' ', set(a), '->', set(b), '置信度=%.2f' % conf)
目的:用 Python 从零实现 Apriori 算法,挖掘购物篮数据中的频繁项集并生成关联规则。
  1. from itertools import combinations引入组合函数,后面剪枝和规则生成都要用到它。
  2. c1 = set()` 与 `frozenset用不可变的 frozenset 表示项集,这样它才能作为字典的键,也能放进集合里去重。
  3. if cand.issubset(tx_set)判断候选集是否被某条交易记录完全包含,这是支持度计数的核心操作。
  4. if sup >= min_sup支持度达到阈值的候选才进入频繁项集名单。
  5. if a[:k-2] == b[:k-2]连接步:前 k-2 个项相同才合并,这是候选集的去重关键。
  6. candidates = prune(candidates, all_freq[-1])剪枝步:先剔除明显没戏的候选,再扫描数据库。
  7. while len(all_freq[-1]) > 0只要上一层还有频繁项集,就继续往更长的项集扩展。
  8. sup_map[frozenset(itemset)] / sup_map[antecedent]用支持度查表算置信度,避免重复统计。

常见误区

误区我觉得支持度最高的项集就是最有价值的规则,于是直接按支持度排行做商业决策。牛奶和面包几乎出现在每个人的购物篮里,那“牛奶→面包”肯定是最该推广的搭配。

正解支持度高只代表“常见”,不代表“有规律”,更不代表“有因果”。牛奶和面包都太普遍,它们的共现很可能只是各自独立的高频叠加。真正有价值的规则要看提升度——它扣除了“本来就火”的基线效应。就像火锅店里毛肚 80% 的客人都会点,那它和锅底的高共现很可能只是巧合,谈不上拉动。

为何常见:人脑天然被高频事物吸引,看见“出现次数多”就容易脑补出“重要”。关联分析的价值核心是“超出预期的共现”,而不是“简单的共现”。

误区一条规则 confidence(A→B)=0.9,我就觉得 90% 的买家都是因为买了 A 才去买 B,这是因果关系。

正解置信度本质是条件概率 P(B∣A),它只回答“如果买 A,买 B 的概率有多大”,完全不能证明 A 是 B 的原因。两者可能同时受第三个因素驱动——比如深夜的垃圾短信和夜间网购都受“夜间活跃”这个共同因素驱动。要识别“伪关联”,必须结合提升度看是否超过 1,还要用领域知识验证因果。

为何常见:日常语言里“如果……那么……”天然带有因果色彩,而关联规则只是共现统计。从“同时发生”到“因果发生”之间,还隔着变量混杂与时间顺序两座大山。

误区Apriori 生成 3-项集时,如果某个 2-子集不频繁就把候选删掉。我担心这样会漏掉“单个菜不火、但组合起来很火”的情况。比如西蓝花单点的人少,但“西蓝花+锅底”意外受欢迎。

正解Apriori 剪枝建立在反单调性上,是数学上严格的:支持度随项集变大单调不增。若 {西蓝花} 只出现在 1% 的菜单里,那包含西蓝花的 {西蓝花,锅底} 最多也只出现在 1% 的菜单中,永远不可能超过 40% 的阈值。所以剪枝不会误删任何可能频繁的项集,它只是把注定失败的检查提前拦下。

为何常见:生活中“组合创新开辟新市场”的逻辑深入人心,但在统计共现的场景下,支持度是“客观存在的事实”,不随组合方式改变而改变。把商业直觉与统计定律混为一谈,就容易产生这个误解。

误区网上说 FP-Growth 比 Apriori 快很多,只需要扫描两次数据库。我以为它根本不用读原始数据,直接把内存里的东西处理完就出结果了。

正解FP-Growth 依然要扫描数据库两次:第一次统计所有单项支持度并排序,第二次逐条交易构建 FP 树。它省去的是 Apriori 中“每一轮生成候选集后都要重新扫描全库验证”的反复开销。数据库在第一次建树之后被压缩进了树结构,之后的挖掘全部在树上进行,这才是它快的真正原因。

为何常见:“只需两次扫描”这句话被简化传播后,容易让人误解成“零扫描”。任何算法都要读取原始数据,FP-Growth 的智慧不在于跳过读取,而在于把读取一次的成果最大化复用。

面试题

Q1. Apriori 算法属于机器学习中的哪一类任务?

单选题 · easy

  • A.监督学习
  • B.无监督学习← 正确答案
  • C.强化学习
  • D.半监督学习

答案:B。Apriori 挖掘关联规则不需要事先标注好的标签,而是从原始交易记录中自行发现共现规律,属于无监督学习。监督学习需要输入输出对,强化学习需要奖励信号,都不符合关联分析的设定。

Q2. 在一个包含 2000 条购物记录的数据库中,某商品组合出现在 500 条记录中,该组合的支持度是多少?

单选题 · easy

  • A.0.25← 正确答案
  • B.0.40
  • C.0.50
  • D.0.75

答案:A。支持度 = 包含该组合的记录数 ÷ 总记录数 = 500 ÷ 2000 = 0.25。支持度衡量组合的覆盖率,是频繁项集判定的基础。其他选项是对分子分母配比错误或心算失误产生的干扰项。

Q3. Apriori 算法剪枝步骤的理论依据是什么?

单选题 · mid

  • A.基于概率统计的置信区间
  • B.基于反单调性的先验性质← 正确答案
  • C.基于马尔可夫链的平稳分布
  • D.基于贝叶斯定理的后验概率

答案:B。反单调性指出项集的支持度不随项集扩大而增大,因此不频繁项集的任何超集都不频繁。Apriori 据此在扫描数据库前剪掉子集不频繁的候选。置信区间、马尔可夫、贝叶斯都不是剪枝的理论来源。

Q4. 置信度 confidence(X→Y) 的正确定义是什么?

单选题 · mid

  • A.X与Y同时支持度除以X支持度← 正确答案
  • B.X出现支持度除以Y出现支持度
  • C.X与Y同时出现的支持度大小
  • D.Y出现支持度除以X出现支持度

答案:A。置信度 = sup(X∪Y) ÷ sup(X),等价于条件概率 P(Y|X),衡量“买了 X 的人中还买 Y 的比例”。选项 B 是 sup(X)/sup(Y),选项 C 只给分子,选项 D 是反向条件概率,都不是置信度定义。

Q5. 提升度 lift(X→Y) > 1 说明什么?

多选题 · mid

  • A.X与Y呈正相关关系← 正确答案
  • B.X与Y呈负相关关系
  • C.X与Y完全相互独立
  • D.X的出现促进Y出现← 正确答案

答案:AD。lift = sup(X∪Y) ÷ [sup(X)·sup(Y)],大于 1 表示实际共现频率高于独立假设下的期望,即正相关,X 的出现会提升 Y 的出现概率。lift < 1 才是负相关,lift = 1 才表示独立。

Q6. Apriori 连接步中,两个 (k-1)-项集合并为 k-项集需要满足什么条件?

单选题 · mid

  • A.前k-1个项完全相同
  • B.前k-2个项完全相同← 正确答案
  • C.全部k个项完全相同
  • D.任意挑选两个项合并

答案:B。两个 (k-1)-项集按序排好后,前 k-2 个项相同才允许合并,这样可以确保每个候选只生成一次,避免重复。若前 k-1 个项相同,两个集合本身就相等,无法得到新的 k-项集。

Q7. FP-Growth 相比 Apriori 提升效率的原因有哪些?

多选题 · hard

  • A.只需扫描两次数据库构建FP树← 正确答案
  • B.支持度阈值可以自动调整
  • C.利用树结构压缩存储数据库← 正确答案
  • D.不需要设定最小支持度阈值

答案:AC。FP-Growth 只扫描数据库两次,第一次统计频次,第二次建树;FP 树用共享前缀的方式压缩了存储,后续挖掘在树上进行。支持度阈值在任何算法中都必须由人设定,FP-Growth 也不例外。

Q8. 哪些是频繁项集先验性质(反单调性)的正确表述?

多选题 · hard

  • A.子集不频繁则超集不频繁← 正确答案
  • B.子集频繁则超集一定频繁
  • C.超集频繁则子集必然频繁← 正确答案
  • D.超集不频繁则子集不频繁

答案:AC。支持度随项集增大单调不增。因此子集不频繁时超集必不频繁(A 对),超集频繁时子集必频繁(C 对)。B 错在子集频繁推不出超集频繁,D 错在超集不频繁时子集仍可能频繁。

Q9. 关联分析最初最经典的商业应用场景是什么?

单选题 · easy

  • A.图像智能识别
  • B.购物篮分析← 正确答案
  • C.语音识别合成
  • D.机器翻译模型

答案:B。关联分析诞生于零售业,核心任务就是分析购物篮中商品的同时购买关系,经典案例即“啤酒与尿布”。图像识别、语音、机器翻译分别属于计算机视觉、语音处理和自然语言处理,与关联分析无关。

Q10. FP-Growth 挖掘中“条件模式基”指的是什么?

单选题 · extreme

  • A.事务数据库的完整投影副本
  • B.以该项结尾的前缀路径集合← 正确答案
  • C.全部频繁项支持度之和值
  • D.候选事务集的笛卡尔积结果

答案:B。条件模式基是 FP 树中所有以某个项结尾的前缀路径集合,每条路径带有计数值。它用来构建该项的条件 FP 树,从而递归挖掘包含该项的频繁模式。它既不是完整数据库投影,也不是支持度总和或笛卡尔积。