关联规则分析实战:基于Flask和Apriori的购物篮挖掘系统
2026/9/7 12:19:22 网站建设 项目流程

实际做超市购物数据分析时,最典型的需求并不是“统计哪个商品卖得最多”,而是“找出哪些商品经常一起被购买”。这类问题属于关联规则分析中的购物篮分析,核心算法是 Apriori。很多人一开始以为,只要把算法跑起来就能自动得到有价值结论,等到动手才发现:原始交易流水和算法要求的输入格式差距很大,支持度、置信度、提升度三个参数调不好,结果要么为空,要么全是噪音,最后还要把规则以页面方式交给非技术同事使用。这篇文章用一个完整的 Flask 项目串联起整条链路:从 CSV 交易数据出发,完成数据清洗和事务矩阵转换,调用 mlxtend 库的 Apriori 算法挖掘频繁项集,再通过置信度和提升度筛选规则,最终用 Flask 页面展示分析结果。做完之后,你既能理解关联规则分析的完整流程,也能直接把这个项目改造成自己的数据分析工具。

1. 先理解关联规则分析与 Flask 为什么会出现在同一个项目里

1.1 关联规则分析要回答的是商品组合问题

关联规则分析是一种无监督学习方法,用于从大量事务数据中发现项集之间的关联关系。在超市场景中,“事务”就是一张购物小票,“项”就是小票里的具体商品。算法的目标是从成千上万张小票中找出稳定性足够高的商品组合规则,例如:

全脂牛奶 -> 面包

这条规则表示,购买全脂牛奶的顾客,有较大概率同时购买面包。规则左侧称为前件,右侧称为后件。真正有价值的信息不是“这个顾客买了什么”,而是“买了 A 的人还经常买什么”,以及“这种关联是否比随机购买更显著”。

Flask 在这个场景中的角色是展示层和交互层。数据挖掘算法本身不依赖 Flask,但实际业务中,运营人员不可能每次都在命令行里修改参数、执行脚本。比较合理的做法是把数据上传、参数设置、规则展示做成 Web 界面,让运营人员上传交易流水、设定阈值,直接在页面上查看推荐组合。这也是这篇文章选择 Flask 的原因:它轻量、简单,适合把数据分析脚本包装成可交互的小系统。

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

关联规则的质量不能靠肉眼判断,需要用三个统计指标来量化。

支持度(Support)表示规则出现的普遍程度,计算公式为:

support(X -> Y) = 包含 X 和 Y 的事务数 / 总事务数

支持度衡量的是一个组合在所有订单里出现的频率。比如总共有 10000 张订单,其中“全脂牛奶 + 面包”同时出现 300 次,支持度就是 0.03。如果支持度太低,说明这个组合非常少见,不具备普遍意义。

置信度(Confidence)表示前件成立时后件也成立的比率:

confidence(X -> Y) = 包含 X 和 Y 的事务数 / 包含 X 的事务数

置信度衡量的是规则的可靠性。比如购买全脂牛奶的 600 个顾客里有 330 人也买了面包,置信度就是 0.55。置信度高,说明从 X 推到 Y 的可信度强。

提升度(Lift)衡量的是关联强度是否高于随机水平:

lift(X -> Y) = confidence(X -> Y) / support(Y)

如果提升度等于 1,说明 X 和 Y 互相独立;大于 1 表示正相关,买 X 会提升买 Y 的概率;小于 1 表示负相关。通常只保留提升度明显大于 1 的规则,比如大于 1.0 甚至大于 2.0。

这三个指标解决的是不同问题:支持度防止找到偶然组合,置信度保证规则可靠性,提升度剔除虚假关联。实际筛选中,三个指标需要同时满足阈值条件,这一点在后面参数调试部分会展开说明。

1.3 Flask 项目雏形要解决什么问题

把上述概念放进项目里,这个 Flask 应用需要完成四件事:接收交易流水文件、把流水整理成算法需要的矩阵、执行频繁项集挖掘和规则生成、把结果渲染到页面。围绕这四个任务,项目可以拆成数据处理模块、规则挖掘模块、Web 路由模块和模板文件。这样拆分之后,算法逻辑可以独立测试,Web 层只负责调用和展示,以后想换成 FP-Growth 或者增加可视化,都不用改动全部代码。

2. 环境准备:先让 Flask、pandas、mlxtend 这套组合能跑起来

2.1 安装依赖库

推荐的 Python 版本是 3.9 以上。关联规则挖掘的主要依赖如下:

依赖库用途说明
FlaskWeb 框架提供上传、路由、模板渲染服务
pandas数据处理读取 CSV、做分组清洗、生成事务矩阵
mlxtend关联规则算法提供aprioriassociation_rules函数
openpyxlExcel 写入需要导出 Excel 时使用

建议先创建虚拟环境,然后安装依赖:

python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install flask pandas openpyxl mlxtend

安装完成后,在当前虚拟环境里执行下面的命令,确认版本没有冲突:

python -c "import flask, pandas, mlxtend; print(flask.__version__); print(pandas.__version__)"

如果输出正常,说明基础环境已经就绪。这里要注意,mlxtend 对 pandas 版本有一定要求,建议安装时不要固定过旧的 pandas 版本。若遇到版本冲突,优先升级 pandas 和 numpy。

2.2 项目目录结构

项目命名为supermarket_association,目录结构如下:

supermarket_association/ ├── app.py ├── requirements.txt ├── data/ │ └── market_data.csv ├── analysis/ │ ├── __init__.py │ ├── preprocess.py │ └── association.py └── templates/ ├── index.html └── result.html

目录职责划分:

  • app.py:Flask 应用入口,负责页面路由和文件上传处理。
  • analysis/preprocess.py:数据清洗、事务矩阵转换。
  • analysis/association.py:Apriori 频繁项集挖掘、关联规则筛选。
  • templates/:前端页面模板,使用 Jinja2 渲染。
  • data/:存放 CSV 格式交易流水,也可以由用户上传。

之所以把算法逻辑从app.py中拆出来,是为了保证单测的独立性。Web 层只负责参数解析和结果返回,数据处理与挖掘逻辑不依赖 Flask,在 Jupyter Notebook 或命令行中也能单独测试。

2.3 准备一份交易流水数据

为了让项目可运行,需要准备 CSV 格式的原始数据。最简格式只需要两列:订单编号和商品名。实际超市数据往往还有数量、价格、时间字段,分析时可以按需保留。

示例market_data.csv

TransactionID,Product 1001,全脂牛奶 1001,面包 1002,全脂牛奶 1002,黄油 1003,啤酒 1003,面包 1003,全脂牛奶 1004,黄油 1005,全脂牛奶 1005,面包 1005,香肠

这份数据只有 5 张订单,适合做功能验证。真实项目建议准备几千到几万笔订单,否则频繁项集很难体现统计意义,后面参数调优部分会专门说明数据量的影响。

3. 数据预处理:把订单流水转换成关联规则所需的事务矩阵

3.1 原始数据清洗阶段需要处理的问题

关联规则算法接收的对象不是流水明细,而是“每笔订单购买了什么商品”的矩阵。因此第一步先清洗数据。analysis/preprocess.py中可以这样实现:

import pandas as pd def load_transactions(file_path_or_buffer): """读取 CSV 交易流水,校验必备列。""" df = pd.read_csv(file_path_or_buffer, encoding='utf-8') required_cols = ['TransactionID', 'Product'] for col in required_cols: if col not in df.columns: raise ValueError(f"缺少必需列: {col}") return df

读取时容易踩的坑有两个。第一个是编码问题,很多国内超市导出的 CSV 是 GBK 编码,直接pd.read_csv会报UnicodeDecodeError,此时需要指定encoding='gbk'。第二个是空行和空值问题,可以先用dropna(subset=['TransactionID', 'Product'])去掉缺失行,再删除商品名为空的记录。

3.2 生成 One-Hot 编码的事务矩阵

Apriori 算法需要的输入格式是:行代表订单,列代表商品,单元格值为 1 或 0,表示该订单是否购买了对应商品。这种结构通常称为 One-Hot 编码矩阵,或者叫“篮子矩阵”。

preprocess.py中实现转换函数:

def build_basket(df, transaction_col='TransactionID', product_col='Product'): """将订单流水转换为事务矩阵。 每一行是订单编号,每一列是商品,单元格为 1 表示该订单包含该商品。 """ # 只保留必要字段 df = df[[transaction_col, product_col]].dropna() # 统计每个订单中每个商品的出现次数 grouped = ( df.groupby([transaction_col, product_col]) .size() .unstack(fill_value=0) ) # 出现次数大于 0 即视为购买,转换为 0/1 basket = (grouped > 0).astype(int) return basket

关键点在于groupby(cols).size().unstack(fill_value=0)这段写法。groupby按订单编号和商品名称聚合,.size()统计每个组合出现的次数;当同一订单中同一个商品出现多行时,例如买了 3 瓶牛奶记录成 3 行,这里会统计成数量 3;unstack把商品名变成列,订单编号变成行;fill_value=0解决缺失单元格的问题;最后用astype(int)转成 0/1 数值,避免后续算法读到缺失值。

运行后,上面的示例数据会转成类似这样的矩阵:

全脂牛奶 面包 黄油 啤酒 香肠 TransactionID 1001 1 1 0 0 0 1002 1 0 1 0 0 1003 1 1 0 1 0 1004 0 0 1 0 0 1005 1 1 0 0 1

3.3 为什么 mlxtend 只认这种输入格式

mlxtend 的apriori函数要求输入是 DataFrame,而且每个单元格必须是布尔值 True/False 或者数值 0/1。程序内部会检查数据是否满足这个要求。

这里要注意两点。第一,不要把商品名作为 DataFrame 的列名之后又混入数值型字段,算法会把所有非布尔列都当成商品参与计算。第二,矩阵里不要保留订单号和价格等无关列,否则算法会把“订单号”或“价格”当作商品来处理。也就是说,build_basket的返回值应该已经是纯粹的 0/1 矩阵,不能保留其他业务字段。

学习阶段可以写一个简单断言:

assert basket.isin([0, 1]).all().all(), "事务矩阵必须只包含 0 和 1" assert (basket.sum(axis=1) > 0).all(), "存在无任何商品的订单,请清理"

在生产环境里,也可以在预处理模块中主动做这两个校验,避免脏数据进入算法。

4. 核心算法模块:用 mlxtend 完成频繁项集挖掘和规则筛选

4.1 先通过支持度挖掘候选频繁项集

Apriori 算法的第一步是产生频繁项集。频繁项集是指支持度大于等于指定阈值的商品组合。association.py中封装如下:

from mlxtend.frequent_patterns import apriori from mlxtend.frequent_patterns import association_rules def generate_frequent_itemsets(basket, min_support=0.03): """生成频繁项集。""" if basket.empty: return None frequent_itemsets = apriori( basket, min_support=min_support, use_colnames=True, max_len=None ) return frequent_itemsets

use_colnames=True会把项集中的商品 ID 改为商品名称,例如用(全脂牛奶, 面包)替代(12, 15)这样的列索引,结果更可读。min_support越小,产生的频繁项集越多,计算时间越长;如果设置为 0,可能生成大量长度为 1 和 2 的组合,性能会明显下降。实际项目中,第一次可以从 0.01 到 0.05 之间取一个初始值。

4.2 生成关联规则并同时筛选置信度和提升度

得到频繁项集之后,就可以生成规则了。association_rules会根据频繁项集推导出所有前后件组合,并计算支持度、置信度、提升度等指标。封装成函数:

def generate_rules(basket, min_support=0.03, min_confidence=0.30, min_lift=1.00): """挖掘关联规则,并返回按提升度排序的数据表。""" frequent_itemsets = generate_frequent_itemsets(basket, min_support) if frequent_itemsets is None or frequent_itemsets.empty: return None rules = association_rules( frequent_itemsets, metric="confidence", min_threshold=min_confidence ) rules = rules[rules['lift'] >= min_lift] rules = rules.sort_values('lift', ascending=False).reset_index(drop=True) return rules

这里有一个很重要的顺序问题。association_rules中只设置metric="confidence"min_threshold,是因为该函数只能设置一个筛选指标,不能同时把 confidence 和 lift 都写进去。所以正确的做法是:先按置信度筛选出基本可靠的规则,再在结果上按提升度进一步过滤。如果先用 lift 筛选,再按 confidence 阈值过滤,会导致部分规则虽然 lift 很高但置信度很低,仍然被误选。

输出结果的主要列如下:

列名含义
antecedents规则前件商品集合
consequents规则后件商品集合
support支持度
confidence置信度
lift提升度
leverage杠杆率,表示组合出现概率比独立情况高多少
conviction确信度,表示前件对后件的影响程度

实际展示时,通常只保留前五列。

4.3 一个可以直接测试的独立模块

为了让读者能快速验证“代码是否能跑通”,这里给出一个可以直接在命令行执行的测试脚本test_association.py

import pandas as pd from analysis.preprocess import load_transactions, build_basket from analysis.association import generate_rules if __name__ == "__main__": df = load_transactions("data/market_data.csv") basket = build_basket(df) print("事务矩阵维度:", basket.shape) rules = generate_rules(basket, min_support=0.2, min_confidence=0.5, min_lift=1.0) if rules is None: print("没有生成规则,请降低阈值") else: print(rules[['antecedents', 'consequents', 'support', 'confidence', 'lift']])

对前面那份 5 条订单的测试数据,如果阈值设得过高,可能一条规则都没有,这是正常现象。可以把min_support降到 0.2,min_confidence降到 0.5,再观察输出。这一步能帮助理解参数与结果数量的关系。

5. Flask 页面与接口:让运营人员可以直接上传数据看结果

5.1 配置 Flask 应用入口

app.py是 Web 层入口,负责文件上传、参数解析和模板渲染:

import pandas as pd from flask import Flask, render_template, request from analysis.preprocess import load_transactions, build_basket from analysis.association import generate_rules app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 限制上传文件 10MB @app.route("/", methods=["GET"]) def index(): return render_template("index.html") @app.route("/analyze", methods=["POST"]) def analyze(): file = request.files.get("file") if file is None or file.filename == "": return "请选择要上传的 CSV 文件", 400 try: min_support = float(request.form.get("min_support", 0.03)) min_confidence = float(request.form.get("min_confidence", 0.30)) min_lift = float(request.form.get("min_lift", 1.00)) except ValueError: return "阈值参数必须为数值", 400 try: df = load_transactions(file) basket = build_basket(df) if basket.shape[0] < 10: return "订单数量太少,分析结果可能无统计意义", 400 rules = generate_rules(basket, min_support, min_confidence, min_lift) except Exception as exc: return f"分析失败: {exc}", 500 rule_list = [] if rules is not None and not rules.empty: for _, row in rules.iterrows(): rule_list.append({ "antecedents": "、".join(list(row["antecedents"])), "consequents": "、".join(list(row["consequents"])), "support": round(row["support"], 4), "confidence": round(row["confidence"], 4), "lift": round(row["lift"], 4), }) return render_template( "result.html", rules=rule_list, min_support=min_support, min_confidence=min_confidence, min_lift=min_lift ) if __name__ == "__main__": app.run(debug=True, host="127.0.0.1", port=5000)

这里加入了几处防御性代码:设置上传文件大小上限,避免用户上传超大文件导致内存压力;对参数做类型转换,避免字符串传入导致崩溃;订单数量少于 10 时直接返回提示,因为过少的样本无法支撑支持度统计。

5.2 编写上传页面与结果页面

templates/index.html负责提供文件上传和参数输入:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>超市客户购物习惯关联规则分析</title> </head> <body> <h1>上传超市交易流水</h1> <form action="/analyze" method="post" enctype="multipart/form-data"> <p> <label>CSV 文件</label> <input type="file" name="file" accept=".csv" required> </p> <p> <label>支持度阈值</label> <input type="text" name="min_support" value="0.03"> </p> <p> <label>置信度阈值</label> <input type="text" name="min_confidence" value="0.30"> </p> <p> <label>提升度阈值</label> <input type="text" name="min_lift" value="1.00"> </p> <button type="submit">开始分析</button> </form> </body> </html>

templates/result.html负责展示规则表:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>关联规则结果</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ccc; padding: 8px; text-align: center; } th { background-color: #f2f2f2; } </style> </head> <body> <h1>关联规则分析结果</h1> <p>当前设置:支持度 {{ min_support }},置信度 {{ min_confidence }},提升度 {{ min_lift }}</p> <p>共发现 {{ rules|length }} 条规则</p> {% if rules %} <table> <tr> <th>前件</th> <th>后件</th> <th>支持度</th> <th>置信度</th> <th>提升度</th> </tr> {% for rule in rules %} <tr> <td>{{ rule.antecedents }}</td> <td>{{ rule.consequents }}</td> <td>{{ rule.support }}</td> <td>{{ rule.confidence }}</td> <td>{{ rule.lift }}</td> </tr> {% endfor %} </table> {% else %} <p>没有满足条件的规则,请降低支持度或置信度阈值。</p> {% endif %} <p><a href="/">重新分析</a></p> </body> </html>

参数回显是一个容易被忽略的细节。结果页把本轮使用的阈值显示出来,运营人员可以快速判断“为什么规则数量变多或变少”。除此之外,无规则时也要给出明确提示,并建议降低阈值,而不是让页面显示一张空表。

5.3 页面、路由和算法层的职责边界

三层职责简单总结如下:

  • 模板层只负责展示,不处理数据。
  • 路由层负责参数解析、文件读取、调用算法函数、捕获异常。
  • 算法层负责清洗、转换、挖掘和规则筛选,不关心文件是从哪里上传的。

这样的边界让代码更容易测试。关联规则挖掘逻辑可以在没有 Flask 的情况下单独跑,Web 层哪怕更换模板或前端框架,也不会影响核心算法。

6. 运行验证与参数调试:支持度、置信度、提升度怎么调才合理

6.1 启动服务并验证完整流程

在项目根目录执行:

python app.py

浏览器访问http://127.0.0.1:5000,选择data/market_data.csv上传,填入适当阈值,点击开始分析。正常流程如下:

  1. 服务读取上传文件。
  2. load_transactions校验列名并读入 DataFrame。
  3. build_basket生成事务矩阵,返回值是一个行列结构清晰的 0/1 表。
  4. generate_rules挖掘频繁项集并筛选规则。
  5. 结果页展示规则列表,包括前件、后件、支持度、置信度和提升度。

如果启动后页面无响应,打开终端检查 Flask 是否输出访问日志;如果上传后返回 500,查看异常信息是否为列名不匹配或编码错误。

6.2 三个阈值之间的关系与调节方法

阈值调参是这个项目的核心操作。三个参数并不是越大越好,也不存在万能固定值,需要结合数据规模、业务目标和结果数量来调整。

参数默认参考调大后的效果调小后的效果常见应用场景
支持度0.01 ~ 0.05规则数量减少,只保留高频组合规则数量增加,会混入低频组合高频商品组合,剔除长尾
置信度0.30 ~ 0.60规则更可靠,但可能过少规则更多,但可靠性降低促销捆绑、货架关联摆放
提升度1.0 ~ 1.5只保留强关联,规则少规则多,但很多是无意义关联挖掘交叉销售机会

实际调试顺序建议是:

  1. 先大致确定支持度,使频繁项集数量在几十到几百条之间。
  2. 再设置置信度,过滤掉那些前件成立但后件经常不成立的情况。
  3. 最后用提升度去掉低于 1 的负相关和接近 1 的弱相关规则。

这里有一个常见的误区:如果一上来就把支持度设成 0.5,可能一条规则都得不到。因为同时购买两个商品的订单数往往远小于单独购买某一个商品的订单数。越大的数据集,支持度阈值可以设得越小;几万条订单时,0.01 是常见起点,几百条订单时则要从 0.1 左右尝试。

6.3 规则结果如何反哺业务决策

拿到规则表之后,分析并没有结束。把规则翻译成业务语言,才是关联规则分析的价值所在。

以规则全脂牛奶 -> 面包为例,支持度 0.03 表示两者同时出现的订单占全部订单的 3%,置信度 0.55 表示买全脂牛奶的人里有 55% 也会买面包,提升度 1.63 表示这个关联比随机情况高出 63%。业务上可以这样应用:

  • 将高支持度高置信度的规则用于货架摆放,把关联商品放置在相邻位置。
  • 将高提升度规则用于套餐设计和促销组合,采用“买前件减后件价格”的方式提升客单价。
  • 将规则结果整理成 Excel 报表,供运营人员做品类管理,不必每次都跑算法。

需要注意的是,关联规则只是发现“一起买”的现象,并不能证明因果。牛奶和面包一起买也可能只是早餐场景造成的共现,不能说“牛奶导致了买面包”。因此业务解读时要结合常识,避免过度解读。

7. 常见报错、生产化建议与后续扩展

7.1 高频问题排查表

在实际运行过程中,下列问题比较常见:

问题现象可能原因检查与解决方式
UnicodeDecodeError文件编码不是 UTF-8,而是 GBK 等encoding='gbk'或先用编辑器转换编码
报错“缺少必需列”CSV 列名不一致,或使用了中文表头统一列名为TransactionIDProduct,并在代码中做校验
运行后无规则支持度、置信度设得过高调低支持度和置信度,先输出频繁项集数量观察结果
ValueError: The truth value of a DataFrame is ambiguous对多行 DataFrame 做了if df判断使用df.emptylen(df) == 0判断空表
上传 10MB 文件时直接失败Flask 的MAX_CONTENT_LENGTH限制调大限制,或改为异步任务处理大文件
结果中出现frozenset({...})对象mlxtend 的antecedents是 frozenset展示时用list(row['antecedents'])转成商品列表

排查顺序通常从数据开始。先确认文件能正常读取,再确认事务矩阵维度是否符合预期,然后才去检查算法阈值。如果矩阵行数等于订单数、列数等于商品种类数,说明预处理通过,问题往往出在阈值设置上。

7.2 从本地项目走向生产环境的改造点

这篇文章里的项目是典型的学习型跑通方案,进入生产环境还需要做以下改造。

第一,分析任务异步化。当交易流水到达几百万行时,在 Flask 请求里同步执行 Apriori 会卡住几十秒甚至更久。生产环境建议使用 Celery 或 Redis 队列,把上传和分析拆成两个异步任务,页面先返回“分析中”状态,完成后通知用户查看结果。

第二,数据源切换。不要让运营人员重复上传 CSV,而是让程序直接从 MySQL、PostgreSQL 或数据仓库读取订单数据,用 SQL 提前完成交易明细聚合,减少传输和内存开销。

第三,缓存频繁项集结果。同一批数据在几组阈值下反复分析,频繁项集计算成本很高。可以先把支持度较低时得到的频繁项集缓存下来,后续调整置信度和提升度时直接从缓存中筛选规则,无需重新运行 Apriori。

第四,增加权限与审计。生产环境要记录谁上传了数据、使用了什么参数、生成了多少规则。Flask-Login 做登录管理,操作日志写入数据库,防止误操作和分析结果被随意覆盖。

7.3 后续可以扩展的分析方向

这个项目完成之后,可以沿着几个方向继续深入。

算法层面,当数据量变大时,Apriori 会产生大量候选项集,性能明显下降,可以换成 FP-Growth 算法,它通过构建 FP 树压缩数据,不需要反复扫描数据库。

分析维度层面,可以加入时间维度,分析工作日和周末的关联规则差异,或者按季度观察商品组合变化;也可以引入客户维度,区分会员和非会员的购买习惯。

产品形态层面,可以为每个商品设计“你可能会喜欢”推荐模块,把规则映射成推荐结果,在下单页、结算页推荐关联商品,这就从“分析工具”进化成了“推荐系统”。

可视化层面,规则集合并不仅仅适合表格展示,商品的频繁项集和强关联关系非常适合用网络图展示,其中节点是商品,连线粗细表示支持度或置信度。可以结合 ECharts 或 D3 把规则图画出来,运营人员可以更加直观地发现商品组团。

关联规则分析的核心价值不在于把 Apriori 跑通,而在于把业务数据转换成可操作的商品组合建议。从这个 Flask 项目开始,逐步完善数据质量、参数调优、异步任务和可视化,最终会形成一套能够支撑运营决策的迷你数据产品。

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

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

立即咨询