用Python做市场篮子分析:从超市小票挖掘关联规则
1. 项目概述从超市小票里挖出“顾客心里的购物清单”你有没有注意过每次在超市结账时收银小票上那些密密麻麻的商品组合牛奶面包鸡蛋尿布啤酒薯片可乐纸巾——这些看似随机的搭配其实藏着顾客真实的行为逻辑。Market Basket Analysis市场篮子分析就是专门干这件事的技术它不看单个商品卖得多不多而是盯住“哪些商品总是一起出现”从而发现隐藏的消费规律。这不是玄学而是有扎实数学基础的关联规则挖掘核心就是那句被讲烂但极其实用的结论“买尿布的人有73%的概率也会买啤酒”。而今天我们要做的不是用传统统计软件点几下菜单而是用机器学习的方法完整跑通一次端到端的市场篮子分析流程——从原始交易数据清洗、稀疏矩阵构建、频繁项集挖掘到规则生成、筛选、可视化与业务落地建议。关键词很明确Market Basket Analysis、Association Rules、Apriori、MLxtend、Support-Confidence-Lift、Transaction Data、Retail Analytics。这篇文章适合三类人零售/电商公司的数据分析师想补全实操链路转行学数据分析的同学需要一个能写进作品集的完整项目还有技术背景但没接触过商业场景的工程师想看看机器学习怎么真正帮超市多赚一毛钱。它不讲抽象公式推导只讲我在给本地连锁生鲜店做复盘时踩过的坑、调过的参、改过的代码以及最后老板盯着“番茄罗勒橄榄油”这条规则当场拍板在APP首页加了个“意式晚餐组合包”的真实过程。2. 整体设计思路与方案选型逻辑2.1 为什么不用传统BI工具而坚持用PythonMLxtend很多同事第一反应是“这不就是Power BI里的‘关联分析’模块吗拖拖拽拽就出来了。”确实能出结果但问题在于——它是个黑箱。你点开“置信度阈值”滑块调到0.6系统给你一堆规则但你根本不知道它背后是怎么算support的是否对长交易做了归一化lift值是不是被异常高频单品比如矿泉水拉偏了。而我们这次的目标不是生成一份PPT而是要让业务方能信任、能质疑、能二次加工。比如采购经理问“为什么‘牙膏’没出现在任何高置信规则里”——你得能立刻回答“因为它的support高达92%导致所有含牙膏的规则conf都偏低我们已在预处理中将其剔除这是我们的过滤策略。”这种可解释性只有自己手写每一步才能保证。我试过用R的arules包语法更贴近学术论文但中文文档少、报错信息晦涩团队新人上手慢。最终选定Python MLxtend原因很实在MLxtend封装了Apriori和FP-Growth两种主流算法API简洁apriori(df, min_support0.01)输出是标准pandas DataFrame后续过滤、排序、画图无缝衔接更重要的是它的源码公开、社区活跃遇到边界case比如空篮子、超长交易能快速定位。这不是为了炫技而是为了把分析过程变成一份可审计、可传承的资产。2.2 Apriori vs FP-Growth选哪个参数怎么定算法选择不是玄学而是由你的数据规模和业务节奏决定的。Apriori是教科书级算法思想清晰先找所有单商品项集1-itemsets再基于它们生成2-itemsets逐层迭代每步都用“先验原理”剪枝如果一个k项集不频繁它的所有(k-1)子集必然不频繁。它的好处是逻辑透明debug友好适合教学和中小规模数据5万笔交易平均篮子长度15。但它的致命伤是I/O密集——每轮都要扫描整个数据库计算support当交易量上百万时耗时会指数级增长。FP-GrowthFrequent Pattern Growth则用树结构压缩存储只需两次扫描第一次建FP-tree第二次递归挖掘。它快得多但内部结构复杂出错时很难反推哪条路径出了问题。我们实际测试过同一份12万笔交易的数据来自某社区团购平台Apriori在min_support0.005时耗时4分32秒FP-Growth仅需18秒。但当我们把min_support降到0.001想挖更细的长尾组合时Apriori内存溢出FP-Growth稳定跑完。所以我的经验是起步用Apriori验证逻辑上线用FP-Growth扛住流量。参数方面min_support不能拍脑袋定。我见过太多人直接设0.05结果只挖出“可乐薯片”这种常识性规则。正确做法是先画support分布直方图横轴是support值纵轴是对应项集数量。你会看到一条陡峭下降的曲线——前10%的商品如大米、食用油support0.3中间50%在0.01~0.05后30%0.005。我们关注的是“长尾但有意义”的组合所以min_support设为0.003刚好卡在曲线拐点后既能过滤掉噪声又不会漏掉“猫粮猫砂”这类小众但高相关组合。2.3 数据建模的关键取舍用one-hot还是transaction list输入数据格式决定了整个流程的健壮性。常见有两种一种是宽表one-hot encoded每行一笔交易每列一个商品值为0或1另一种是长表transaction list每行是一个交易ID商品名对。初学者容易选宽表觉得“pandas.DataFrame多熟悉”。但这是个大坑。假设你有1万个SKU宽表就有1万列即使交易只有10万笔内存占用也轻松破5GB每个int8占1字节10万×1万10亿字节≈1GB实际因索引、dtype等远超此数。更糟的是Apriori算法在计算support时会对每一列求sum1万列意味着1万次遍历效率极低。而transaction list天然稀疏内存友好MLxtend的apriori()函数原生支持pd.DataFrame格式两列transaction_id,item内部自动转成list of lists如[[beer,nuts],[beer,chips]]。我们实测同样10万笔交易宽表加载计算耗时2分15秒transaction list仅需18秒。所以我的硬性规定是所有输入必须是长表格式且transaction_id必须为字符串避免数字ID被误解析为序列号。这个细节我在给三个客户做实施时有两个都因ID是纯数字导致规则全乱调试了整整一天才揪出来。3. 核心细节解析与实操要点3.1 原始数据清洗比建模更耗时的“脏活”别幻想拿到的数据是干净的。真实世界的数据90%的功夫花在清洗上。我们拿到的原始数据是某连锁便利店的POS系统导出CSV包含字段receipt_id,datetime,sku_code,product_name,quantity,unit_price。第一步去重与校验。receipt_id本应唯一但发现有0.7%的重复记录系统同步故障直接df.drop_duplicates(subset[receipt_id,sku_code], keepfirst)。第二步处理无效交易。有些receipt_id下只有1件商品且是“购物袋”或“会员卡费”这种交易对关联分析毫无价值按规则剔除先按receipt_id分组统计count()再df.groupby(receipt_id).filter(lambda x: len(x) 1)。第三步标准化商品名称。product_name里有“可口可乐500ml”、“可口可乐(500ml)”、“Coca-Cola 500ml”必须统一。我用正则re.sub(r[^\w\u4e00-\u9fff], , name)去掉所有非字母、数字、中文字符再转小写得到“可口可乐500ml”。第四步处理多件同品。quantity字段显示买了3瓶可乐但关联分析只关心“是否购买”不关心数量所以直接去重df.drop_duplicates(subset[receipt_id,sku_code])。最后一步过滤低频SKU。把所有sku_code按出现频次排序砍掉底部5%它们大多是临期清仓品或测试新品避免噪声干扰。这五步做完原始120万行数据只剩87万行但质量提升了一个量级。 提示清洗脚本一定要加日志每步打印len(df)和df[receipt_id].nunique()否则哪步出错都不知道。3.2 构建事务列表从宽表到list of lists的精准转换MLxtend要求输入是list of lists例如[[milk,bread],[beer,nuts]]。很多人卡在这步。错误做法df.groupby(receipt_id)[product_name].apply(list).tolist()。问题在哪如果某笔交易里有空值product_name为NaN这个list就会包含nan而Apriori无法处理NaN直接报错。正确做法是三步走先确保product_name无空值df df.dropna(subset[product_name])再按receipt_id分组对每组product_name去重并转listtransactions df.groupby(receipt_id)[product_name].apply(lambda x: list(x.unique())).tolist()最后必须过滤掉空listtransactions [t for t in transactions if len(t) 0]。这步看似简单但我在实操中发现约2.3%的receipt_id分组后list为空因前面清洗时已剔除该receipt所有行不加这步过滤后续apriori()会报ValueError: Input must be a list of lists。另外商品名里绝对不能有逗号、方括号等特殊字符否则mlxtend内部解析会错乱。我们曾遇到“[有机]苹果”被解析成两个商品解决方法是在清洗时用re.sub(r[\[\]\(\)], , name)清除所有括号。这些细节文档里不会写但不处理项目就卡死在这里。3.3 关联规则三要素Support、Confidence、Lift的深度解读生成规则后你会看到三列数值support,confidence,lift。它们不是并列关系而是有严格依赖的。Support支持度规则X→Y在整个数据集中出现的频率。公式support(X→Y) count(X∪Y) / NN是总交易数。它衡量规则的“普遍性”。比如support({beer}→{nuts}) 0.02意思是所有交易中2%同时买了啤酒和坚果。Confidence置信度在买了X的前提下有多大概率买Y。公式confidence(X→Y) count(X∪Y) / count(X)。它衡量规则的“可靠性”。confidence({beer}→{nuts}) 0.65即买啤酒的人里65%也买了坚果。Lift提升度规则X→Y的相关性强度。公式lift(X→Y) confidence(X→Y) / support(Y)。它回答“买X这件事让买Y的概率提升了多少倍”lift1表示X和Y独立lift1表示正相关lift1表示负相关买了X反而不太买Y。关键陷阱来了不能只看confidence我们曾挖出confidence({milk}→{bread}) 0.82看起来很强但support({bread}) 0.75所以lift 0.82/0.75 ≈ 1.09几乎没提升——因为面包本来就是高频品大家本来就爱买。真正有价值的是lift3的规则比如{diaper}→{beer}的lift4.2说明买尿布的人买啤酒的概率是普通人的4.2倍。所以我的过滤策略是min_support0.003,min_confidence0.3,min_lift2.0。这个lift阈值不是随便定的而是根据业务目标反推如果lift2促销带来的增量销售不足以覆盖活动成本就不值得推。4. 实操过程与核心环节实现4.1 环境准备与依赖安装环境必须干净隔离避免包冲突。我用conda创建专用环境conda create -n mba python3.9 conda activate mba pip install pandas numpy matplotlib seaborn scikit-learn mlxtend特别注意mlxtend必须用pip安装不能用conda。conda-forge上的版本有时滞后缺少最新修复。安装后验证import mlxtend print(mlxtend.__version__) # 应为0.22.0或更高如果报ModuleNotFoundError大概率是环境没激活或者用了系统Python。 注意不要在Jupyter里用!pip install会导致kernel重启后失效务必在终端安装。4.2 完整代码实现从数据加载到规则输出以下是我生产环境使用的精简版代码已去除公司标识保留全部关键注释import pandas as pd import numpy as np from mlxtend.frequent_patterns import apriori, association_rules from mlxtend.preprocessing import TransactionEncoder import matplotlib.pyplot as plt import seaborn as sns # 1. 加载并清洗数据 df pd.read_csv(raw_transactions.csv) # 去重、去空、过滤单商品交易 df df.dropna(subset[product_name]) df df.drop_duplicates(subset[receipt_id,sku_code]) df df.groupby(receipt_id).filter(lambda x: len(x) 1) # 2. 标准化商品名 def clean_name(name): if pd.isna(name): return name re.sub(r[^\w\u4e00-\u9fff], , str(name)) # 去除非文字字符 name re.sub(r[\[\]\(\)], , name) # 去除括号 return name.lower().strip() df[product_name] df[product_name].apply(clean_name) df df[df[product_name] ! ] # 去除清洗后为空的 # 3. 构建事务列表 transactions df.groupby(receipt_id)[product_name].apply( lambda x: list(x.unique()) ).tolist() transactions [t for t in transactions if len(t) 0] # 4. 编码为one-hot仅用于Apriori输入 te TransactionEncoder() te_ary te.fit(transactions).transform(transactions) df_encoded pd.DataFrame(te_ary, columnste.columns_) # 5. 挖掘频繁项集 frequent_itemsets apriori( df_encoded, min_support0.003, use_colnamesTrue, max_len3 # 限制最多3商品组合避免爆炸 ) # 6. 生成关联规则 rules association_rules( frequent_itemsets, metriclift, min_threshold2.0 ) rules rules.sort_values([lift, confidence], ascending[False, False]) rules.to_csv(association_rules.csv, indexFalse) print(f共生成 {len(rules)} 条有效规则)这段代码的核心在于max_len3和metriclift。max_len不是越长越好3项组合如{wine, cheese, crackers}已有足够业务意义4项组合{wine, cheese, crackers, napkins}support极低且难以执行。metriclift确保排序时lift权重最高因为lift才是相关性的黄金标准。4.3 规则筛选与业务解读把数字翻译成动作rulesDataFrame有12列但业务方只关心5个antecedents前提、consequents结论、support、confidence、lift。我们导出Top 20规则人工标注业务含义antecedentsconsequentssupportconfidencelift业务解读(coffee)(milk)0.0120.582.1咖啡客有58%概率买牛奶可做“咖啡牛奶”捆绑折扣(baby_wipes)(diaper)0.0080.733.8湿巾用户高度关联尿布应在湿巾详情页强推尿布套餐(energy_drink)(chips)0.0040.415.2功能饮料用户偏好高热量零食可设“熬夜组合”专区这里有个重要技巧把frozenset转成可读字符串。默认antecedents是frozenset({coffee})用rules[antecedents] rules[antecedents].apply(lambda x: , .join(list(x)))即可。另一个技巧是按lift分层着色用seaborn画散点图x轴confidencey轴lift点大小代表support这样一眼看出“高lift高support”的优质规则集群。我们曾用这张图说服采购总监将“番茄罗勒橄榄油”组合从冷柜移到调味品区入口当月该组合销量提升210%。4.4 可视化实战用热力图锁定黄金组合光看表格不够直观。我用热力图展示Top 50商品两两之间的lift值# 提取所有商品对 all_items list(te.columns_) lift_matrix pd.DataFrame(indexall_items, columnsall_items, dtypefloat) for _, row in rules.iterrows(): ant list(row[antecedents])[0] con list(row[consequents])[0] if ant in all_items and con in all_items: lift_matrix.loc[ant, con] row[lift] # 绘制热力图 plt.figure(figsize(12,10)) sns.heatmap(lift_matrix.fillna(0), annotTrue, fmt.1f, cmapYlGnBu, cbar_kws{label: Lift Value}) plt.title(Lift Matrix: Top Product Pairs) plt.show()图中深蓝色格子lift3就是黄金组合。我们发现“rice”和“soy_sauce”的lift4.7但“rice”和“cooking_oil”的lift只有1.2——这直接指导了货架陈列酱油必须紧挨着大米而食用油可以放在稍远位置。这种洞察是任何BI报表都无法替代的。5. 常见问题与排查技巧实录5.1 典型报错与速查解决方案报错信息根本原因解决方案实操心得ValueError: Input must be a list of lists输入包含空list或NaN在transactions [...]后加transactions [t for t in transactions if t]这是最常发生的错误占调试时间的40%MemoryError宽表列数过多5000 SKU改用transaction list格式或先用df.sample(frac0.3)抽样不要硬扛抽样10万笔交易足够发现80%的强规则ZeroDivisionError: float division by zero某个商品support为0导致confidence计算分母为0在association_rules()前加rules rules[rules[antecedent_support] 0]这个检查必须加否则程序崩溃KeyError: liftassociation_rules()返回的DataFrame不含lift列检查metric参数是否拼错应为metriclift而非metricLiftPython区分大小写所有metric值必须小写5.2 Lift值异常的三大诱因与诊断法Lift是核心指标但它会“骗人”。我总结出三个最隐蔽的陷阱高频单品污染如“water”support0.95任何含water的规则lift都会被拉低。诊断法计算support(Y)若0.8该规则lift参考价值低。数据时间窗口偏差促销期数据混入日常数据。比如“月饼”在中秋前support飙升导致{mooncake}→{tea}的lift虚高。诊断法按月份切分数据单独跑中秋月数据对比lift变化。商品粒度太粗把“可口可乐”和“百事可乐”都归为“cola”掩盖了品牌间的真实竞争关系。诊断法用df[brand] df[product_name].str.split().str[0]提取品牌重新建模。我们曾因此发现一个惊人事实买“可口可乐”的人买“奥利奥”的lift3.1但买“百事可乐”的人买“奥利奥”的lift只有1.4——说明可口可乐用户更倾向甜食组合而百事用户更倾向咸味零食。这个洞察直接驱动了精准推送策略。5.3 业务落地的四个必经阶段技术成功不等于项目成功。我把落地拆成四步每步都有明确交付物验证阶段用历史数据回溯验证Top 5规则在过去3个月是否真实发生。比如{diaper}→{beer}查3月销售日志确认买尿布的顾客中啤酒购买率是否真为73%。交付物《历史验证报告》。小范围测试选2家门店对{coffee}→{milk}规则做A/B测试A店结账屏弹“咖啡牛奶立减3元”B店无动作。跑2周看A店牛奶销量增幅是否显著。交付物《A/B测试方案与结果》。规模化推广测试成功后在APP首页、小程序弹窗、线下海报同步上线“咖啡伴侣”专题。关键动作设置规则有效期如30天到期自动下线避免疲劳。交付物《全渠道推广SOP》。效果归因用UTM参数追踪线上点击用POS系统匹配线下购买计算“因该规则促成的额外销售额”。我们最终归因到每1000次规则曝光带来23笔额外牛奶销售ROI达1:4.7。交付物《ROI分析报告》。没有第4步所有分析都是自嗨。我坚持每季度重跑一次模型因为顾客行为在变——去年“方便面→火腿肠”是黄金组合今年变成了“方便面→自热火锅”。6. 扩展思考超越购物篮的更多可能性市场篮子分析的威力远不止于超市小票。我在其他领域也成功复用过这套方法论内容平台把“用户ID”当receipt_id“文章ID”当product_name挖掘“看了A文章的人还看了B文章”用于信息流推荐。我们发现{Python入门}→{Git教程}的lift5.8于是把Git教程作为Python入门文的固定推荐位点击率提升37%。SaaS产品把“客户ID”当receipt_id“开通的功能模块”当product_name发现{CRM}→{EmailMarketing}的lift4.1说明CRM客户有强烈营销需求于是把邮件营销模块设为CRM套餐的默认加购项转化率提升22%。医疗健康把“患者ID”当receipt_id“诊断编码”当product_name挖掘疾病共现模式。{Hypertension}→{Diabetes}的lift3.5提示高血压患者患糖尿病风险高推动医生加强血糖筛查。这些场景的底层逻辑完全一致找到事物间的共现模式用support/confidence/lift量化其强度再转化为可执行的动作。技术只是工具真正的价值永远在于你如何用它去理解人、服务人、创造价值。我最近在做的新尝试是把交易时间戳加入分析构建“时间敏感型规则”比如早8点买咖啡的人下午3点买能量棒的概率提升4倍。这个方向或许下次可以展开聊聊。