用Apriori算法做购物篮分析:从杂货店数据挖掘关联规则

📅 发布时间:2026/9/16 6:20:56
用Apriori算法做购物篮分析:从杂货店数据挖掘关联规则
开头杂货店的交易流水看起来就是一张张密密麻麻的小票但如果把这些小票按行拆开、按商品摞起来再让计算机自动找规律你会发现一件很有意思的事买啤酒的人往往同时拿一包花生买婴幼儿奶粉的订单里大概率出现纸巾甚至还有买狗粮的顾客顺手带走了罐头。这不只是经验直觉而是关联规则挖掘在悄悄干活。我这次做的实验就是用一份模拟的杂货店商品数据集跑通完整的关联规则挖掘流程从数据预处理开始到Apriori算法提取频繁项集再到生成关联规则并解读结果。这个实验非常适合刚学数据挖掘的同学、想入门Python数据分析的从业者以及做零售选品、货架陈列、促销捆绑的运营或产品人员。做完之后你能真正理解支持度、置信度、提升度这三个指标分别解决什么问题为什么有时候置信度很高的规则反而是个坑以及如何把挖掘结果翻译成能落地的经营动作。1. 整体设计与思路拆解1.1 实验目标与场景还原这个实验要回答的核心问题是在一家普通杂货店的历史订单中哪些商品之间存在共同出现的稳定规律。翻译成业务语言就是——顾客把哪些东西放进同一个购物篮放进去的几率有多大这种组合是否值得做成套餐、摆在一起或者发优惠券我把场景设定为一家开在社区门口的杂货店营业时间早七点到晚十一点客群主要是周边居民。针对这种店用户的购买行为有几个特点购买频次高、单笔金额不大、购买品类相对固定、冲动型消费占比高。这就决定了数据集里不会出现一次买20件的大订单反而经常出现2到5件的小组合比如牛奶面包、可乐薯片这类高频搭配。数据集设计成包含约1000条订单记录每一条都对应一张真实小票字段包括订单编号、商品名称、商品数量、订单时间。为了更贴近真实环境我还故意加了一些噪声数据比如同一张订单里出现了重复商品行的记录、个别商品名称拼写不一致、有若干条空订单。处理这些脏数据本身就是这个实验的一个重要环节。提示这个实验并不是要用算法猜顾客会不会买某件商品而是要发现哪几件商品经常一起被购买。前者属于预测问题后者属于描述性问题。关联规则天生擅长后者不擅长前者这一点在选型时要拎清楚。1.2 为什么是Apriori而不是其他算法做关联规则挖掘可选的算法不算多主流的就是Apriori、FP-Growth和Eclat。这个实验选择Apriori核心原因是它在教学和业务理解层面最直观代码实现门槛也最低。Apriori算法的思路可以概括成一句大白话如果某个商品组合出现的次数连最低要求都达不到那包含它的任何更大的组合出现次数只会更少干脆直接剪掉不去算它。这个先筛掉小集合、再往大集合扩展的思路极大地减少了计算量也是Apriori最核心的地方。FP-Growth虽然性能更好它通过构建频繁模式树来避免反复扫描数据集在处理百万级订单时优势明显。但在千级规模的数据集上Apriori的性能差异几乎可以忽略。Eclat则使用垂直数据格式按商品ID来组织交易记录代码逻辑和常见的数据处理习惯差异较大理解门槛高了不少。所以这个实验用Apriori本质上是用最简单的方式把关联规则的全链路打通。等你真的需要处理海量数据时再从Apriori平滑切换到FP-Growth思路完全一致只是底层数据结构不同。2. 核心原理深入拆解2.1 支持度、置信度、提升度一次讲透关联规则里有三个绕不开的指标支持度Support、置信度Confidence、提升度Lift。很多人一开始背公式背得滚瓜烂熟一遇到实际数据就不知道怎么设阈值原因在于不理解这三个指标的物理含义。先看支持度。它衡量的是规则覆盖的人群有多广。比如牛奶→面包的支持度同时购买牛奶和面包的订单数/总订单数。如果总订单1000笔牛奶面包一起出现的有80笔支持度就是8%。这8%意味着如果你给所有顾客推荐牛奶面包套餐100个人里有8个人本来就会接受。支持度太低说明这个规则只是个例不值得投入资源去运营。置信度衡量的是当顾客买了A时买B的概率有多大。还是上面那条规则买了牛奶的人里面又有多少人买了面包如果买牛奶的有200人其中80人也买了面包置信度就是40%。这40%的意义是你把面包摆在牛奶旁边大约四成的牛奶购买者会顺势拿一个面包。提升度是这三个指标里最容易被忽略、也最容易出问题的。它的公式是提升度 规则置信度 / 后件单独出现的支持度。如果提升度大于1说明买A显著提升了买B的可能性规则有效等于1说明A和B独立小于1说明A的出现反而压低了B的购买概率这时候称A抑制B更准确。在实际应用里我的习惯是支持度优先卡掉那些订单量太少的长尾组合置信度用来筛选值得去运营的强关联而提升度才是判断规则是否真有价值的最终标准。2.2 Apriori算法的两阶段流程与剪枝逻辑Apriori算法的执行过程可以拆成两步第一步找频繁项集第二步根据频繁项集生成关联规则。找频繁项集是从1项集开始的。先统计每种商品出现在多少个订单里计算出各自的绝对支持度把小于最小支持度的商品直接扔掉。接下来把剩下的商品两两组合成2项集再统计每对组合的出现次数继续剪枝。再往后是3项集、4项集直到没法生成新的组合为止。这里有一个很多资料没有讲透的关键点就是剪枝的下一个步骤——自连接。以2项集生成为例假设频繁1项集是{ABC}两两组合可以得到AB、AC、BC共三个候选2项集。Apriori在这时候做了一件事在生成候选3项集时它不会把AB、AC、BC一次性全往里面扔而是要求所有候选3项集的子集都必须是频繁项集。这就是先验原理Apriori Property的价值所在现在BC是频繁的AB是频繁的AC也是频繁的那么ABC才可能成为候选。如果AC不频繁ABC根本没资格进入扫描阶段。实际计算中你并不需要手动去枚举这些子集mlxtend等Python库已经帮我们把这一步封装好了。但是你要清楚算法在后台就是靠这套由小到大、逐层剪枝的逻辑来降低运算量。生成规则阶段就更简单了对上一步得到的每个频繁项集把里面的商品拆成前件后件的组合然后计算置信度和提升度。例如频繁项集{牛奶面包鸡蛋}可以生成牛奶→面包鸡蛋、面包牛奶→鸡蛋等若干条规则再根据最小置信度阈值完成最终筛选。2.3 数据预处理的三个真实痛点关联规则实验里很多人把精力全花在调参上结果数据预处理没过关白白浪费大量时间。我这次就踩了三个典型的坑值得单独拿出来说。第一个痛点是订单数据与购物篮数据格式不一致。原始的销售明细表是一行一条记录每个商品是独立的行同一条订单有多行每一行只有单一商品和数量。而关联规则算法要求我们提供的是一个订单一行该行的内容是所有购买商品的集合。所以必须做一次数据整形按订单编号分组把该订单的所有商品汇总到同一个列表里。第二个痛点是重复商品的处理。模拟数据里我故意让同一张订单出现了两行可乐如果不去重统计出的商品组合次数就会失真。解决办法很简单分组后对商品列表取set去重。第三个痛点是脏数据与缺失值的清理。比如商品名里有前后空格、牛 奶这种被误拆成两个字的写法都会让算法把同一种商品当成两类处理。数据清洗是这类实验的隐形门槛宁可多做一轮清洗也不要图省事直接跑算法。3. 实操全过程从数据到关联规则3.1 构造杂货店交易数据集实验的第一步是准备数据。为了让结果可复现我用Python随机生成了1000条订单商品池子里放入了16种常见杂货包括牛奶、面包、鸡蛋、可乐、薯片、啤酒、花生、纸巾、洗洁精、牙膏、方便面、火腿肠、饼干、巧克力、生抽、垃圾袋。每一笔订单的商品数量设置在2到6件之间购买概率按照主食和饮料高频、日用品中频、调味品低频的思路设计。这样生成的模拟数据既有高频组合可乐薯片也有低频但稳定出现的组合尿布湿巾挖掘起来结果更丰富。import random import pandas as pd random.seed(42) products [牛奶, 面包, 鸡蛋, 可乐, 薯片, 啤酒, 花生, 纸巾, 洗洁精, 牙膏, 方便面, 火腿肠, 饼干, 巧克力, 生抽, 垃圾袋] weights { 牛奶: 0.25, 面包: 0.30, 鸡蛋: 0.20, 可乐: 0.25, 薯片: 0.20, 啤酒: 0.15, 花生: 0.10, 纸巾: 0.18, 洗洁精: 0.08, 牙膏: 0.08, 方便面: 0.15, 火腿肠: 0.12, 饼干: 0.12, 巧克力: 0.10, 生抽: 0.05, 垃圾袋: 0.06 } orders [] for order_id in range(1, 1001): n_items random.randint(2, 6) items random.choices(list(products), weights[weights[p] for p in products], kn_items) orders.append([order_id, items]) df pd.DataFrame(orders, columns[订单编号, 商品列表]) df.head()生成完的数据大概长这样订单编号从1到1000每条订单的商品列表是一个list对象。需要注意我这里直接生成了一个订单一行、商品是列表的格式目的是跳过原始明细数据的分组聚合。如果你手头的是销售明细表一定得先按订单编号分组再聚合原理是一样的。3.2 数据清洗与格式转换拿到数据集后第一步是看看有没有空订单和重复商品。因为生成时设置的最小商品数是2理论上不会有空订单但为了真实性我额外添加了几条空记录来模拟真实场景。清洗规则很简单将商品列表转为set去重删除空列表的行确保每条订单里每个商品只出现一次。对于真实数据还要做商品名去空格、统一大小写、修正同义名称等操作。# 去重、去除空订单 df[商品列表] df[商品列表].apply(lambda x: list(set(x))) df df[df[商品列表].apply(len) 0].reset_index(dropTrue) print(f清洗后剩余订单数: {len(df)})需要注意的是关联规则算法处理的是商品是否出现在订单中而不是商品在订单中出现了几次。一笔订单买了两瓶可乐在算法看来仍然只是包含可乐。所以做set去重不只是为了让数据整洁更是在贴合算法的计算逻辑。清洗完之后还要做一次编码。mlxtend库接受三种输入格式中的任意一种嵌套列表的购物篮格式、one-hot格式的DataFrame、以及事务编码后的DataFrame。我最常用的是嵌套列表转one-hot格式这样便于后面的可视化观察。from mlxtend.preprocessing import TransactionEncoder te TransactionEncoder() te_ary te.fit(df[商品列表]).transform(df[商品列表]) df_encoded pd.DataFrame(te_ary, columnste.columns_) df_encoded.head()转换完成后你会看到一个每行代表一个订单、每列代表一种商品的布尔矩阵值为True表示该订单购买了该商品False表示没有。这个矩阵就是Apriori算法的直接输入。3.3 利用mlxtend跑频繁项集与关联规则核心计算阶段我直接用mlxtend.frequent_patterns里的apriori函数。这里最关键的是设置最小支持度阈值min_support。我试过几组不同的值从0.01到0.1都跑了一遍最终选择了0.02。选择0.02的判断逻辑是1000笔订单中某组合至少出现20次才值得关注。如果设置成0.01会产生大量只有十几次出现次数的边缘组合噪音很大如果设置成0.05又太严格很多有业务价值的低频组合比如尿布湿巾会被过滤掉。所以在实际项目中阈值一定要结合数据规模和业务目标来回试而不是套用某个默认值。from mlxtend.frequent_patterns import apriori, association_rules frequent_itemsets apriori(df_encoded, min_support0.02, use_colnamesTrue) frequent_itemsets.sort_values(bysupport, ascendingFalse).head(10)跑出来的频繁项集会包含每个组合的支持度。我这次的结果里可乐薯片的支持度最高达到了9.1%左右说明这个组合的覆盖率相当广几乎每11笔订单就有1笔包含这两个商品。有了频繁项集下一步就是生成关联规则。这里需要同时设置置信度阈值和提升度筛选条件。我直接使用了association_rules函数设置min_threshold0.3然后用提升度大于1来过滤。rules association_rules(frequent_itemsets, metricconfidence, min_threshold0.3) rules rules[rules[lift] 1] rules rules.sort_values(bylift, ascendingFalse) rules.head(10)这个函数会自动输出前件antecedents、后件consequents、前件支持度、后件支持度、规则支持度、置信度、提升度等字段。数据量不大时运行速度几乎是一瞬间完成。3.4 结果解读与业务转化拿跑出的规则举例假设有一条薯片 → 可乐支持度9.1%置信度45%提升度1.8。怎么读懂它支持度9.1%说明在所有订单里同时包含薯片和可乐的订单占了接近十分之一这个搭配非常普遍。置信度45%说明买薯片的人里有接近一半会顺手买可乐背后的逻辑可能有两种一是两者都是看剧、聚会场景的标配二是杂货店里可乐和薯片经常摆在一起形成了某种陈列暗示。提升度1.8说明买薯片这个行为对买可乐有显著的带动作用——整体顾客里只有25%会买可乐但在买薯片的人群里这个比例拉高到了45%。业务动作上可以落地三个方向。第一货架关联把可乐和薯片就近摆放或者在促销台组合陈列顺势提高客单价。第二捆绑套餐设计薯片可乐组合价既然45%的人本来就会一起买那门槛很低的组合优惠很容易驱动剩下55%的薯片购买者也加购一罐可乐。第三购物篮推荐在收银小程序上顾客扫码结算薯片之后弹窗推送可乐优惠券转化率通常高于随机发放。再举一条不那么直观的规则方便面火腿肠 → 鸡蛋置信度0.52提升度2.3。这条规则背后是在家煮面这个消费场景煮方便面时加个鸡蛋是很多人的习惯动作。这种场景化规则非常适合做主题营销比如宵夜套餐一包方便面一根火腿肠两个鸡蛋。4. 常见问题与排查技巧实录4.1 支持度阈值到底怎么定这个问题的标准答案永远是看数据、看业务。但落到操作层面我建议从小阈值开始尝试比如从0.01跑起看频繁项集数量是否爆炸。如果频繁项集有几千个再把阈值往上抬如果只有几十个说明阈值可能太高了。一般来说频繁项集数量控制在50到200之间后续人工审核规则的负担比较合理。注意如果你做的是大型超市的海量订单数据支持度0.01甚至0.005都不奇怪。因为订单量越大低频组合的绝对数量越多挖掘出长尾价值的机会也越多。但如果是几千笔订单的小店数据0.02可能就合适了。千万不要照搬别人的参数。4.2 置信度高但提升度小于1被忽略的陷阱很多人在生成规则后只看置信度排序这是最容易出错的环节。我举个例子假设大米 → 食用油这条规则置信度高达60%这意味着所有买大米的订单里六成都会买食用油。听起来非常强但如果全店订单里有55%都买了食用油那买大米的人群里出现食用油的比例只比全员平均水平高了5%提升度只有1.09。换句话说顾客买不买油跟买不买大米的关系不大只是因为油本身是高频商品几乎人人都会买。这种情况下你如果把大米和食用油捆绑营销效果不会好。因为顾客本来就会买油捆绑价反而可能压缩利润。真正值得关注的是高置信度且高提升度的规则前者保证稳定性后者保证这个关联确实存在而不是巧合。4.3 频繁项集组合爆炸导致内存不足当商品种类多、支持度阈值低时频繁项集的数量会指数级增长程序跑起来特别慢甚至内存溢出。有一个技巧是先用商品维度做一次粗筛比如把每1000笔订单里出现次数少于10次的商品提前过滤掉。这样可以大幅压缩项集的搜索空间而且通常不影响业务分析的准确性。如果数据量大到单机跑不动就要换FP-Growth算法它在处理稀疏的长尾商品数据时性能提升非常明显。Apriori每生成一层候选集就要扫一遍全量数据FP-Growth只需要扫两遍在百万级订单场景下这就是几分钟和小时的差距。4.4 数据格式转换时踩过的坑mlxtend读取数据时对格式相当敏感。曾经有朋友直接把自己整理好的商品列表塞进apriori函数结果报了一堆错。其实apriori函数接收的是one-hot编码的DataFrame不是嵌套列表。如果你手头是嵌套列表格式必须先套一层TransactionEncoder做转换。另一个坑是one-hot矩阵中的列名尽量不要用数字开头的字符串个别版本的pandas和mlxtend会解析异常建议统一用中文或字母命名的商品名跑起来更省心。常见问题原因解决办法频繁项集过多支持度阈值太低上调min_support控制在50~200项置信度高但业务无效后件本身是高频商品按提升度筛选只保留lift1.2的规则运行慢/内存溢出数据量大但算法仍是Apriori提前过滤低频商品或改用FP-Growth编码报错输入格式错误使用TransactionEncoder转为one-hot格式结果全是一般组合缺少业务维度过滤按品类或价格带分组单独挖掘4.5 规则太多看不过来时怎么取舍规则数量少则几十条多则上千条怎么挑出最有用的那几条我自己的习惯是分四步操作第一步限定提升度大于1.2先把虚高或虚低的规则干掉第二步限定置信度大于某个业务可接受水平比如0.4第三步按支持度降序排列优先看覆盖人数多的规则第四步人工过一遍剩下的前20条结合业务判断是否符合常理。这个筛选顺序里的核心是先看靠不靠谱再看影响面大不大。一条支持度8%但提升度2.0的规则价值一定高于支持度0.5%但提升度10.0的规则因为后者的样本量太少了偶然性太高。结尾做完整个实验我最大的感受是跑通算法只需要十几分钟真正的功夫全花在数据清洗、阈值调试和结果解释上。如果你也想拿真实数据练手可以找自己店里或朋友的收银系统导一份半年以上的流水字段只需要订单编号、商品名称、下单时间三列就够了。跑出来的规则先别急着全信挑几条带回去跟老店长的经验对一下——常常会发现算法找出来的规律正是老店长嘴里好像有这回事但一直没量化过的那些组合。我后来把这次实验的脚本封装成了一个可复用的函数输入一张原始销售明细表输出一份按提升度排序的关联规则清单整个过程只要几秒。后续如果你想把玩法升级可以考虑给商品打上品类标签后再跑一次规则看品类间的关联或者按时间段拆分数据观察早餐时段和宵夜时段各自的购物篮差异。关联规则这个工具越用越觉得它不像一个高冷的算法更像一双能帮你重新看懂自己生意的眼睛。