基于BreadBasket数据的关联规则实战:从清洗到2月18日单日对比
2026/9/10 18:10:54 网站建设 项目流程

做数据分析这几年,我见过太多人一上来就调包跑模型,跑完却完全说不清结果在说什么。今天拿一个非常经典的数据集——BreadBasket 面包店交易数据——把关联分析(也就是常说的购物篮分析)从数据清洗到规则解读完整走一遍,重点落在 2月18日 这个分析窗口上。这篇文章不会只贴代码,而是把每一步为什么这么做、踩过哪些坑都讲清楚。想入门关联规则、准备面试、或者真要在零售场景里做商品捆绑分析的,都值得花十分钟看完。

先说清楚标题里的“2.18”是什么。这不是算法版本号,是我在数据集里单独切出来的一个日期窗口。我当时只是想对比一下“单日规则”和“全量规则”有多大差异,结果发现拿单日数据跑关联分析时,阈值体系和全量数据完全不能混着用。这个坑后面会详细讲。

1. 先搞清楚 BreadBasket 数据集到底装了什么

1.1 数据从哪来、长什么样

BreadBasket 是一个公开的面包店/咖啡店交易数据集,最早流传于各类数据挖掘课程和竞赛社区里。它记录了一家店一段时间内每笔交易的明细流水,每一行代表“某个交易编号下的某个商品”,所以同一笔交易如果买了三样东西,就会有三行记录。

字段结构非常简洁:

字段名含义示例
Transaction交易编号1
Item商品名称Coffee
Date交易日期2017-02-18
Time交易时间09:30:00

整个数据集大概有两万多条流水,覆盖几个月的数据量。商品种类不是特别多,常见的有 Coffee、Bread、Cake、Tea、Toast、Pastry、Sandwich、Juice、Soup、Cookies、Muffin 等,几十种。数据量不大,家用笔记本完全跑得动,用来练手非常合适。

我这次实战的思路是:先把全量数据清洗干净,构造出“购物篮”,然后跑全量关联规则作为基线,再单独切出 2月18日 当天的数据跑一遍单日规则,最后对比两组规则差异,从中解读业务含义。

1.2 用 pandas 做一次数据体检

拿到数据第一步不是直接跑模型,而是先看清楚数据质量。我习惯先加载数据,随手做一轮基础体检。

import pandas as pd df = pd.read_csv("BreadBasket_DMS.csv") print(df.shape) print(df.head()) print(df.info())

这一步看起来简单,但信息量很大。你会看到数据量、字段类型、缺失情况,基本能判断后续清洗要花多大力气。

我跑完df.info()之后发现几个很明显的问题:

  • 数据集里有空值记录;
  • Item字段里混入了NONEngrams这样的非法值;
  • Transaction编号存在大量重复。

前两个问题好理解,脏数据嘛,过滤掉就行。第三个问题最容易翻车,后面专门讲。千万不要跳过体检,我见过太多人在不干净的数据上直接跑 Apriori,出来的规则全是脏数据制造出来的假象。

2. 动手之前:关联规则那点事

2.1 三个核心指标:支持度、置信度、提升度

购物篮分析的目标很简单:发现“顾客买了 A 之后,还倾向于买 B”的规律。在 BreadBasket 里,最典型的就是买了咖啡的人是不是大概率也会拿一块蛋糕。要衡量这种规律,关联规则引入了三个指标,名字听起来吓人,其实理解起来非常生活化。

支持度(Support),衡量的是“这个商品组合在多少笔交易里出现过”。比如 100 笔交易里,有 30 笔同时包含咖啡和蛋糕,那{Coffee, Cake}的支持度就是 0.3。支持度太低,说明这个组合太罕见,即使规则成立也没有商业价值。公式表达就是:

Support(A → B) = (包含 A 和 B 的交易数) / (总交易数)

置信度(Confidence),衡量的是“买了 A 的交易里,有多大比例也买了 B”。如果 40 笔买咖啡的交易里 30 笔买了蛋糕,那Coffee → Cake的置信度就是 0.75。置信度反映的是规则的可靠性,越高说明这个连带关系越强。

Confidence(A → B) = (包含 A 和 B 的交易数) / (包含 A 的交易数)

提升度(Lift),是我最看重的指标。它衡量的是“买了 A 之后买 B 的概率,比没买 A 的人买 B 的概率高多少”。如果提升度大于 1,说明 A 和 B 存在正相关;等于 1,说明两者独立;小于 1,说明反而互相排斥。

Lift(A → B) = Confidence(A → B) / Support(B)

举个例子:假设全店有 20% 的交易包含蛋糕,而买了咖啡的交易里有 60% 都买了蛋糕,那提升度就是 0.6 / 0.2 = 3。这个 3 意味着“买咖啡的人买蛋糕的可能性,是随机顾客的 3 倍”,这才是真正有价值的信号。

2.2 算法选型:为什么用 Apriori

购物篮分析最常见的算法是 Apriori,它的核心思想是先找频繁项集,再生成规则。所谓“频繁项集”,就是支持度超过阈值的商品组合。Apriori 有个很聪明的剪枝策略:如果一个商品组合不频繁,那它的任何超集也一定不频繁。也就是说,{Coffee, 鱼子酱}如果没人买,那{Coffee, 鱼子酱, 面包}肯定也不会有人买。这样就能大幅缩小搜索空间。

有人会问,为什么不直接上 FP-Growth?FP-Growth 确实更快,它只需要扫描两遍数据库,不需要像 Apriori 那样反复生成候选项集。但 BreadBasket 这个数据量实在太小了,几万条流水而已,Apriori 跑起来毫无压力。而且 Apriori 更好理解、更容易调试,作为学习案例非常合适。

阈值怎么设,是这里最值得说的地方。我一开始按全量数据跑,min_support设成 0.01,事务数大概几千笔,相当于要求这个商品组合至少出现几十次,低于这个次数统计意义就不大,跑出来的规则也没法信。后来切到 2月18日 单日数据时,事务数直接缩了一个数量级,同样 0.01 的支持度,可能只有一两笔交易,规则立刻变得不稳定。所以单日分析时我会把支持度阈值往上提,至少保证每个频繁项集有足够的样本支撑。

3. 数据清洗与购物篮构造

3.1 准备环境

这次实战用到的库不多,pandas 做数据处理,mlxtend 跑 Apriori,matplotlib 和 seaborn 做可视化。

pip install pandas mlxtend matplotlib seaborn

mlxtend 是一个机器学习扩展库,里面封装了aprioriassociation_rules两个函数,省去了手写算法的麻烦。我建议初学者先用封装好的库理解整个流程,之后再尝试自己实现一遍算法,理解会更深刻。

3.2 清洗脏数据

回到数据本身,第一件事就是清掉Item字段里的非法值。

df = df.dropna() df = df[df["Item"] != "NONE"] df = df[df["Item"] != "ngrams"]

这里的NONE一般是系统生成的无意义记录,ngrams则是数据采集时混进去的噪声。这两类值必须在构造购物篮之前清干净,否则它们在 Apriori 里会形成虚假的商品项,干扰关联规则的输出。

清洗完之后,可以顺手看一眼商品频次,确认数据是否符合直觉。

print(df["Item"].value_counts().head(10))

正常情况下,Coffee 一定排在第一位,接下来是 Bread、Tea、Cake 这些常见品类。如果排序结果很奇怪,比如某种冷门商品排在最前面,就要回头检查是不是清洗步骤出了问题。

3.3 生成唯一交易编号

这个坑我必须单独拿出来说,因为真的很容易踩。BreadBasket 数据集里的Transaction字段并不是全局唯一的,它可能在每天都会重新计数。也就是说,可能第一天有交易编号 1,第二天也有交易编号 1。如果直接把这个字段当作购物篮的分组依据,就会把不同日期、不同顾客的购买记录混成一个虚假的购物篮,后面所有分析全部白做。

解决办法是组合TransactionDate两个字段,生成一个全局唯一的交易标识。

df["BasketID"] = df["Transaction"].astype(str) + "_" + df["Date"].astype(str)

跑关联分析之前,一定要先用这个组合字段确认一下唯一性,没有这一步,后面算出来的支持度、置信度全都不作数。

3.4 切出 2月18日 的数据并构造购物篮

构造购物篮的本质是把“长表”转换成“宽表”。原始数据一行是一个商品,现在要变成一行是一笔交易,每一列是一个商品,交易里包含哪个商品就填 1,没有就填 0。这个 0/1 矩阵就是 Apriori 的输入。

df_feb18 = df[df["Date"] == "2017-02-18"] def create_basket(df_sub): basket = df_sub.groupby(["BasketID", "Item"])["Item"].count().unstack().fillna(0) def encode(x): return (x > 0).astype(int) basket = basket.apply(encode) return basket basket_all = create_basket(df) basket_feb18 = create_basket(df_feb18)

这里用groupby按交易和商品分组,然后unstack把商品变成列。因为同一个商品在同一笔交易里只算一次,所以无论买了几份,都统一编码成 1。

print(basket_all.shape) print(basket_feb18.shape)

看到两个矩阵的形状差异,就能直观感受到单日数据量有多小。这也是为什么后面跑单日关联规则时,必须单独调整支持度阈值。

4. 跑 Apriori:频繁项集与关联规则

4.1 先找频繁项集

购物篮矩阵构造好之后,就可以直接调用 mlxtend 的apriori函数了。

from mlxtend.frequent_patterns import apriori, association_rules frequent_itemsets = apriori( basket_all, min_support=0.01, use_colnames=True ) print(frequent_itemsets.sort_values("support", ascending=False).head(10))

use_colnames=True的意思是让输出里的itemsets列直接显示商品名称,而不是编号,看起来更直观。

这里min_support=0.01的选择是有讲究的。如果这个数据集有几千笔有效交易,那 0.01 的支持度意味着这个组合至少出现了几十次,算下来是有统计基础的。如果你发现频繁项集数量太少或太多,就上下调整这个阈值,一般从 0.01 开始试比较合理。

跑完之后看输出,最高频的项集一定和商品频次排名一致,比如{Coffee}单独的支持度很高。然后是几个高频组合,比如{Coffee, Bread}{Coffee, Cake},这些都是后面生成规则的原料。

4.2 生成关联规则并按提升度排序

有了频繁项集,接下来用association_rules生成规则。

rules = association_rules( frequent_itemsets, metric="lift", min_threshold=1 ) rules = rules.sort_values("lift", ascending=False) print(rules.head(10))

这里metric="lift"min_threshold=1表示只保留提升度大于 1 的规则,也就是只留下存在正相关的组合。提升度大于 1 是底线,小于等于 1 的规则没有业务价值,可以直接丢弃。

如果你的 mlxtend 版本比较新,有可能会提示需要传入num_itemsets参数,遇到这种情况补上就行。

rules = association_rules( frequent_itemsets, metric="lift", min_threshold=1, num_itemsets=len(basket_all) )

4.3 单日窗口和全量数据对比

全量规则跑完之后,用同样的流程对 2月18日 的数据单独跑一遍。注意,单日事务数少,支持度阈值要相应提高,我这次直接设成了 0.03,否则组合样本量太小,规则没有参考价值。

对比两组规则之后,能看到非常有意思的现象:

对比维度全量数据2月18日单日
事务数量几千笔几十到几百笔
支持度阈值0.010.03(需上调)
规则稳定性较稳定波动大,易受个别顾客影响
典型规则Coffee → Cake受当天天气、活动影响明显

全量规则代表的是“店里的长期规律”,适合做常规陈列和套餐设计。单日规则代表的是“当天发生了什么特殊事情”,比如某一天天气冷,热咖啡和汤类的关联可能特别强;某一天是节日,甜点和礼品类组合的置信度会明显上升。

这个对比视角,是我这次实战最大的收获。单日数据不适合单独看“准不准”,而更适合用来找“差异点”。一旦发现全量规则和单日规则的排序差异很大,就去倒查当天的业务动作、天气、促销活动,往往能对得上号。

5. 结果怎么用:从规则到经营动作

5.1 读懂几条典型规则

跑完关联规则后,千万不能只看表格里的数字,要回到真实场景里去解释。以 BreadBasket 为例,常见的高提升度规则大致这样:

  • Coffee → Cake:这是店铺里最经典的搭配。买咖啡的人买蛋糕的意愿远高于随机顾客,提升度通常很高。
  • Toast → Coffee:早餐场景的典型组合。吐司和咖啡几乎是一对固定搭配。
  • Bread → Juice:这说明有一部分顾客在买面包时倾向于搭配饮品,而且可能不是咖啡类,而是果汁类。

解读规则的时候,我会从顾客动线的角度去推演。顾客进门买咖啡,等待出杯的几十秒里,眼睛很容易扫到收银台旁边的蛋糕柜,顺手就带一块——这个行为链完全解释得通。

5.2 从规则到具体经营动作

关联规则分析最大的价值是落地到业务动作,不然就是白跑。根据跑出来的规则,至少可以做三件事:

第一,重新设计套餐。既然Coffee → Cake的关联强度很高,就可以直接设计“咖啡 + 蛋糕”套餐价,用套餐价引导顾客做出更符合店铺利益的购买决策。套餐价格不是简单打折,而是要把毛利更高的商品和引流商品绑在一起。

第二,改造商品陈列。把高频关联的商品放在同一视线范围、同一动线区域。比如在咖啡出杯处放小蛋糕架,把吐司和果酱放在同一个货架上,减少顾客寻找成本。

第三,优化推荐系统。如果店里上线点单系统,可以在顾客选完咖啡后弹窗推荐蛋糕,推荐逻辑直接采用关联规则的结果。这种推荐不需要复杂模型,一张规则表就能跑得很好。

5.3 再进一步:分时段、分场景的拓展

做完单日和全量对比后,我还习惯按时间维度继续拆。比如把数据分成上午、下午、晚上三个时段,分别跑关联规则。上午时段早餐组合会非常强,比如Coffee → ToastSandwich → Coffee;下午时段甜点组合会上升;晚上则可能出现Soup → Bread这类偏正餐的搭配。

这个拓展不需要改代码,只要把create_basket里的过滤条件从日期改成时间段就行。如果你发现不同时段的规则差异很明显,就能据此安排员工排班、备货比例和推荐策略,这才是数据分析该有的姿态。

6. 踩坑记录与常见问题速查

6.1 数据集本身的坑

BreadBasket 这个数据集最经典的两个坑,一个是NONEngrams脏数据,一个是Transaction编号重复。前者直接污染商品项,后者直接污染购物篮结构。如果不处理这两个问题,就算代码跑通,出来的规则也是错的。

另外,日期格式也值得留意。不同来源的数据集日期格式可能不一样,有的是2017-02-18,有的可能是18-02-2017,读取之后一定要先用pd.to_datetime统一格式,再去做筛选,否则很容易因为字符串格式不同导致切不出来数据。

6.2 算法参数与实现上的坑

Apriori 跑出来的规则数量非常受阈值影响。我见过有人把min_support设成 0.001,结果频繁项集爆炸式增长,规则上百万条,根本看不过来。支持度阈值不要一味求低,要结合业务判断:商品组合出现次数太少,即便统计上相关,也不值得投入运营资源。

还有一点要提醒,拿到规则表之后,不要只看置信度排序,也不要只看提升度排序。置信度高有时候只是因为商品本身热卖,比如咖啡和蛋糕都很热销,随机共现的概率本来就高。提升度高才说明组合确实有特殊关系。但如果提升度极度偏高,比如超过 10,往往是小样本造成的假象,优先级反而要往后放。

另外,关联规则和灰色关联分析是两个完全不同的东西。灰色关联分析解决的是“多个因素序列之间的关联程度”问题,常用于系统分析和评价,和购物篮里的频繁项集挖掘不是一回事。搜资料的时候别搞混。

6.3 业务解读的坑

关联规则只能说明“经常一起出现”,不能直接证明因果。Coffee → Cake可能只是因为咖啡本来就是引流品,顾客买咖啡多,顺手买蛋糕的概率自然就高。不代表只要把咖啡价格降低,蛋糕销量就会跟着涨。想要验证因果关系,得再做 AB 测试或者专门的因果推断分析。

解读单日规则时也要格外小心。单日数据量小,几个团体顾客的购买行为就可能大幅拉高某个组合的置信度。所以单日规则只能作为线索,用于生成假设,验证还得靠更长周期的数据。

最后再分享一个我个人的实操习惯:每次跑完关联规则,先把 Top 20 规则原样打印出来,用业务常识快速过一遍。凡是连自己这关都过不了、解释不通的规则,直接放一边。真正有价值的,是那些既在数据上显著、又在业务上说得通、还能马上落地成动作的规则。带着这个标准去看结果,你才不会在几千条规则里迷失方向。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询