简介面向数据分析与数据挖掘初学者这份小体积资料提供Apriori算法的Python实现及配套交易数据集帮助理解关联规则、频繁项集与支持度、置信度的完整流程并掌握“购买啤酒也可能购买尿布”这类规则背后的计算逻辑。压缩包内共两个文件包含一个可直接运行的Python脚本和一个TXT格式的交易数据文件脚本基于apyori库编写数据结构清晰适合对照真实交易记录验证算法效果跑通从原始数据到关联规则输出的完整案例。包体仅3KB轻量易用目前已有9650人学习下载。通过运行这份代码可掌握频繁项集生成、候选集迭代与支持度/置信度筛选的落地实现同时理解Apriori“若非频繁项集其超集也非频繁”的剪枝策略还能自由调整阈值参数将算法迁移到商品推荐、购物篮分析等业务场景为后续数据挖掘实践夯实基础。1. Apriori算法Python实现这份带数据集的代码到底能帮你跑出什么Apriori算法Python实现带数据集网上一搜一大把但真正把代码下载下来跑通的人大部分卡住的地方不是算法原理而是数据格式、候选集生成和剪枝细节。这个标题里最有价值的不是“Apriori”三个字而是“含数据集”这个前提——没有一份能直接喂给算法的数据任何demo都很难落地。这篇文章我从一份购物篮数据集讲起用纯Python写完整实现覆盖事务数据格式、频繁项集挖掘、关联规则生成、参数调整和常见坑。适合第一次接触关联规则挖掘、想在本机快速跑出结果、并且知道怎么把结果套到实际业务场景里的从业者。读完你手里会有一份能直接改数据路径就能跑的代码而不是只能看不能用的片段。2. Apriori的核心逻辑先验性质、连接步与剪枝步2.1 先验性质为什么频繁项集的子集一定频繁Apriori名字里的“先验”来自它利用了一个很朴素的观察如果一个项集是频繁的那么它的所有非空子集一定也是频繁的。反过来才是关键如果一个候选项集存在哪怕一个非空子集已经不频繁那这个候选项集就不可能是频繁的可以直接剪掉不用浪费一次数据库扫描去统计它。这里要先定义清楚“频繁”的衡量标准。设数据集总事务数为N某个项集X在其中出现的事务数为count(X)则支持度support(X)count(X)/N。再给定一个最小支持度阈值min_support凡是support(X)min_support的项集都叫频繁项集。举个例子8笔购物事务里项集{牛奶,面包}出现了4次支持度就是4/80.5如果min_support0.375那它就是频繁项集。基于这个先验性质Apriori的挖掘方式就很清晰了先找出所有频繁1项集也就是单个商品再由它们连接生成候选2项集扫描计数后过滤出频繁2项集再由频繁2项集连接生成候选3项集逐层往上走直到某层没有新的频繁项集为止。这个逐层搜索的策略使得Apriori不需要枚举全部可能的项集组合复杂度大幅下降。2.2 连接步Lk-1如何生成候选集Ck假设已经得到频繁(k-1)项集的集合Lk-1下一步是生成候选k项集的集合Ck。这一步常见的做法是“连接”取Lk-1中的任意两个项集如果它们在排序之后的前k-2个项完全相同那么就把这两个项集合并成一个新的k项集。比如频繁2项集里有{牛奶,面包}和{牛奶,黄油}排序后前1项都是“牛奶”满足连接条件合并得到候选3项集{牛奶,面包,黄油}。再比如{牛奶,黄油}和{面包,黄油}排序后前1项分别是“牛奶”和“面包”不相等则不能连接。这个“排序后比较”非常关键。如果项集内部顺序不一致比如一个存成{牛奶,面包}另一个存成{面包,牛奶}它们本质是同一个项集但排序前比较前k-2项会得到错误结论。所以我在实现里一律先把项集内的元素sorted再比较再合并保证同一项集只有一种排序形态被参与连接。2.3 剪枝步候选集生成后必须做子集检查连接步生成候选Ck之后不能直接拿去扫描计数还必须做一次剪枝对Ck里的每个候选项集检查它的所有(k-1)项子集是否都存在于Lk-1中如果有一个子集不在就把这个候选项集从Ck里删掉。剪枝依据仍然是先验性质。假设候选3项集{牛奶,面包,啤酒}它的3个子集{牛奶,面包}、{牛奶,啤酒}、{面包,啤酒}必须全部是频繁2项集。如果其中{面包,啤酒}在L2中不存在说明{面包,啤酒}的支持度低于min_support那么{牛奶,面包,啤酒}的支持度必然也低于min_support保留它只会让后续扫描白算一次。这一步在代码里是一句all(sub in Lk_prev for sub in subsets)但实际运行中少了它和写了它候选集数量可能差一个数量级。我见过不少Apriori实现在数据量稍微大一点就卡死十有八九是连接步之后没做剪枝。2.4 三个核心指标支持度、置信度、提升度Apriori最终产出的是频繁项集和关联规则而衡量一条规则有价值与否主要看三个数支持度support(X→Y)count(X∪Y)/N表示X和Y同时出现的概率反映规则的覆盖面置信度confidence(X→Y)count(X∪Y)/count(X)表示在包含X的事务中同时包含Y的比例反映规则的可靠性提升度lift(X→Y)confidence(X→Y)/support(Y)表示包含X的条件下出现Y的概率与Y全局出现概率的比值。lift等于1说明两者独立大于1才是正向关联小于1反而是负相关。理解了这三个指标后续调参就有了方向。支持度卡太严频繁项集太少规则也少置信度卡太松出来的规则自己都不敢用提升度则用来筛掉那些“看着置信度高其实是误导”的规则。具体怎么在代码里计算和筛选我放到第4章和第6章细讲。3. 数据集准备从原始订单表到Apriori输入格式3.1 事务数据的标准格式列表的列表Apriori的输入数据是一个“事务列表”每一笔事务是一个商品集合。我一般用Python的列表的列表来承载形如transactions [[...], [...]]其中每一行代表一笔订单内部是这次订单包含的商品。这里给出后续全文都要用到的示例数据集用购物篮场景来理解最直观transactions [ [牛奶, 面包, 黄油], [牛奶, 面包], [面包, 黄油, 啤酒], [牛奶, 黄油, 啤酒], [面包, 黄油], [牛奶, 面包, 啤酒], [牛奶, 面包, 黄油, 啤酒], [啤酒, 尿布], ]逻辑说明这份数据共8笔事务、5种商品分别是牛奶、面包、黄油、啤酒、尿布。每笔事务是一个列表内部商品顺序无所谓但建议统一排序。第8笔只有啤酒和尿布看起来就是经典购物篮里常被引用的组合不过这里尿布只出现一次按后面的min_support大概率被过滤掉。参数说明min_support初始取0.375意味着一个项集至少要出现在3笔事务中才算频繁min_confidence初始取0.6即前件出现的事务里后件至少要有60%的出现比例才保留规则。这两个值并不普适后续可以按业务需要调整。3.2 从CSV原始订单表到事务列表的转换函数实际业务里数据很少直接就是这个嵌套列表格式。更常见的是数据库里一张订单明细表一行是一条订单的一个商品同一订单多行。因此第一步是把原始表聚合成事务列表。我一般会写一个专门的加载函数避免每换一份数据就改主流程代码import csv from collections import defaultdict def load_transactions(csv_path, order_colorder_id, item_colitem_id): order_items defaultdict(set) with open(csv_path, newline, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: order_items[row[order_col]].add(row[item_col]) transactions [sorted(items) for items in order_items.values()] return transactions逻辑说明defaultdict(set)用于把同一订单的商品收集到一个set中天然去重。因为Apriori统计的是某商品“有没有出现在事务里”同一笔订单买了两件同款商品也只算一次。sorted(items)让每笔事务内商品排序为后续连接步的排序比较打好基础。参数说明order_col和item_col分别对应原始CSV里的订单列名和商品列名默认值是按常见命名写的实际表头不同时按需修改。字符串商品名直接存成字符串即可如果是数字ID也建议统一转成str避免后面出现“1”和1被当成两个不同商品的问题这个坑在第5章还会细说。3.3 空事务与脏数据进入算法前必须做的清洗清洗这一步看似简单实际决定算法能不能跑出正常结果。我通常会在load_transactions之后再做三个处理过滤空事务、去首尾空格、统一大小写。def clean_transactions(transactions): cleaned [] for t in transactions: items [str(item).strip().lower() for item in t] items list(set(items)) if items: cleaned.append(sorted(items)) return cleaned逻辑说明strip和lower保证“牛奶”和“ 牛奶 ”、“牛奶”和“牛奶”不会被视为两个商品set(items)再次去重避免上游数据里同一订单商品重复出现多行if items过滤掉空事务因为空事务对所有项集的支持度贡献都是零留着只会让scan多白算一轮。参数说明如果你要保留商品的大小写区别可以把lower去掉如果商品编码是数字IDstr()之后不建议再改大小写直接把它当作不透明字符串处理即可。这里说一个实际体会数据清洗排错往往比算法本身更耗时间。有次我在一份日志数据上跑出的频繁项集全是“nan”排查了半天才发现源文件里某些订单的商品字段是空字符串被读成了字符串nan清洗时没有把这部分过滤掉。所以加载之后打印一下前几笔事务是省时间的好习惯。3.4 初始参数怎么定先跑统计再拍min_support和min_confidence最小支持度和最小置信度没有万能值但有经验性的起点。对几千到几万笔的事务数据集我一般先把min_support从0.1开始试频繁项集太少就往下调候选集爆炸就往上调。对百万级以上的大数据集min_support经常要压到0.01甚至更低不然高频组合根本浮现不出来。一个实用技巧是先用简单的统计帮自己定参考线n len(transactions) unique_items len({item for t in transactions for item in t}) print(f事务数: {n}, 商品种类数: {unique_items})建议先算单商品支持度分布看看大多数商品出现在多少笔事务里。如果紧俏商品只出现在1%~2%的事务中min_support就必须低于这个值否则这些商品永远不会进入L1。min_confidence通常放在0.5~0.8之间太低规则噪声大太高规则几乎产不出。业务上如果目标是做推荐“买A的人有多少概率会买B”这个概率最好到0.6以上才敢用。4. Python手写Apriori从C1到关联规则的完整代码4.1 生成C1与L1第一次扫描数据集Apriori的第一步是找出所有频繁1项集。先构建候选1项集C1再扫描全部事务统计每个单商品出现在多少个事务里过滤掉低于min_support的得到L1。def create_C1(transactions): C1 set() for t in transactions: for item in t: C1.add(frozenset([item])) return C1 def scan_transactions(transactions, Ck, min_support): support_count {} for t in transactions: for candidate in Ck: if candidate.issubset(t): support_count[candidate] support_count.get(candidate, 0) 1 n len(transactions) frequent {} for candidate, count in support_count.items(): support count / n if support min_support: frequent[candidate] support return frequent C1 create_C1(transactions) L1 scan_transactions(transactions, C1, 0.375) print(L1)逻辑说明create_C1把每个商品包成单元素frozenset这样后续所有层级的项集都是frozenset类型统一。scan_transactions遍历每个事务对每个候选项集调用issubset判断是否被当前事务包含统计出现次数后除以总事务数得到支持度。参数说明min_support传入0.375时这组示例数据里尿布只出现1次支持度0.125会被过滤而牛奶、面包、黄油、啤酒都出现5次以上全部进入L1。注意字典的键是frozenset因为frozenset可哈希能作为字典键而普通set不行。4.2 连接步与剪枝步apriori_gen实现候选集生成拿到Lk-1之后下一步是生成候选集Ck。这一步我的实现分为两半先连接生成候选再对每个候选做子集剪枝。from itertools import combinations def apriori_gen(Lk_prev, k): Ck set() Lk_prev_list list(Lk_prev.keys()) n len(Lk_prev_list) for i in range(n): for j in range(i 1, n): items_i sorted(Lk_prev_list[i]) items_j sorted(Lk_prev_list[j]) if items_i[:k - 2] items_j[:k - 2]: candidate frozenset(items_i [items_j[-1]]) all_subsets [ frozenset(comb) for comb in combinations(candidate, k - 1) ] if all(sub in Lk_prev for sub in all_subsets): Ck.add(candidate) return Ck逻辑说明外层两层循环遍历Lk-1中所有项集对只比较一次避免重复。items_i[:k-2] items_j[:k-2]是连接条件保证两个项集共享前k-2项这样合并出的candidate长度为k。candidate生成后用combinations(candidate, k-1)枚举它的所有k-1子集逐个确认都在Lk_prev中。参数说明k是当前要生成的目标项集大小比如由L2生成C3时k3。这里一个常见的细节是items_i [items_j[-1]]能保证新候选不为空且长度为k是因为items_i和items_j已经sorted且两者最后一项不同。如果最后一项相同说明两个项集完全相同不会出现在ij的两两遍历里。4.3 主循环逐层挖掘直到没有新的频繁项集有了L1生成和候选生成接下来就是主循环不断迭代直到某层生成的Ck为空或过滤后Lk为空。def apriori(transactions, min_support): C1 create_C1(transactions) L1 scan_transactions(transactions, C1, min_support) L [L1] k 2 while True: Ck apriori_gen(L[-1], k) if not Ck: break Lk scan_transactions(transactions, Ck, min_support) if not Lk: break L.append(Lk) k 1 return L freq_itemsets apriori(transactions, 0.375)逻辑说明L是一个列表L[0]是频繁1项集L[1]是频繁2项集依此类推。while循环里先由L[-1]生成候选集Ck再扫数据集得到Lk。两个if分别处理“候选为空”和“频繁项集为空”的情况防止死循环。参数说明min_support越低循环层数和每层项集数都会增加。跑数据量大的集时建议在循环里打印len(Ck)和len(Lk)观察候选集增长情况。如果某一层Ck数量突然比上一层大出几十倍先检查剪枝逻辑有没有生效。4.4 生成关联规则从频繁项集到置信度筛选频繁项集只是中间产物关联规则才是业务上真正能用的东西。对每个频繁项集S枚举它的所有非空真子集A作为前件S-A作为后件置信度为support(S) / support(A)不低于min_confidence则保留。def generate_rules(L, min_confidence): support_map {} for level in L: for itemset, sup in level.items(): support_map[itemset] sup rules [] for level in L[1:]: for itemset, support in level.items(): items list(itemset) for r in range(1, len(items)): for antecedent_tuple in combinations(items, r): antecedent frozenset(antecedent_tuple) consequent itemset - antecedent if not consequent: continue conf support / support_map[antecedent] if conf min_confidence: rules.append({ antecedent: sorted(antecedent), consequent: sorted(consequent), support: round(support, 3), confidence: round(conf, 3) }) return rules rules generate_rules(freq_itemsets, 0.6) for rule in rules: print(rule)逻辑说明support_map把每一层的频繁项集及其支持度拍平成一个大字典这样无论前件A出现在频繁1项集还是频繁2项集都能O(1)取到支持度。组合枚举用itertools.combinationsr从1到len(items)-1避免前件为空或后件为空。参数说明min_confidence取0.6时示例数据中面包→牛奶这条规则置信度约0.667会被保留而面包→啤酒置信度只有0.5会被过滤。输出里antecedent和consequent都用sorted转成了普通列表方便阅读打印顺序不再受frozenset的哈希影响。5. Apriori避坑与排查五个高频问题的现象、原因与处理5.1 现象支持度计数全为0或明显偏低有次我用一份真实订单数据跑Apriori第一层L1几乎是空的只有一个商品勉强达标。检查代码逻辑没问题后来打印处理后的前几笔事务才发现所有商品名后面都带着不可见字符比如换行符或全角空格导致“牛奶”和“牛奶\n”被当成两个商品。原因是数据加载时没有做stripCSV里某些字段带特殊字符字符串匹配不上。解决方法是进入算法前统一清洗在load_transactions的循环里对item调用.strip()必要时再加.encode(utf-8).decode(utf-8-sig)处理BOM头。清洗后重新统计单商品支持度值才恢复正常。5.2 现象候选集数量爆炸程序卡死数据量稍大就卡死这是Apriori最常见的翻车现场。出现这种情况第一个要怀疑的是剪枝步没生效。我排查过一次某开发者的代码apriori_gen里候选集生成后直接Ck.add(candidate)完全没做子集检查。当频繁2项集有几千个时连接生成的候选3项集直接爆炸到几十万个再扫一遍数据集当然卡死。解决方法是确认apriori_gen中all(sub in Lk_prev for sub in subsets)这段在候选入Ck之前执行。如果确认剪枝没问题但仍卡死就把min_support往上提一档频繁项集数量通常会断崖式下降。另外一个辅助手段是先将只出现在一两笔事务里的低频商品从transactions里剔除因为它们不可能进入L1留着只会让每层扫描多浪费时间。5.3 现象连接步生成重复候选集有时候Ck里出现重复项集导致支持度计数被翻倍。这个问题在frozenset作为集合元素时其实不会重复存储但如果代码里用list存候选就可能出现{牛奶,面包,黄油}和{面包,牛奶,黄油}同时存在的状态。根本原因是连接前没有对项集内元素排序。两个内部顺序不同的相同项集排序后前k-2项比较会发现它们其实是同一个项集但如果不排序比较逻辑可能误判它们为不同从而把相同项集重复加入。解决方法是所有项集统一走sorted再比较并且全程用frozenset作为候选存储容器利用它自动去重的特性兜底。代码里用frozenset(items_i [items_j[-1]])而不是普通set就能顺带解决重复问题。5.4 现象生成规则时抛KeyErrorgenerate_rules在计算置信度时support_map[antecedent]偶尔会报KeyError或者算出的置信度大于1。原因通常是support_map里没有某个前件项集。比如一个3项集{牛奶,面包,黄油}是频繁的但它的子集{牛奶,黄油}可能因为支持度不达标不在L2里而规则生成时却枚举了它作为前件于是查不到支持度。解决方法是规则生成时不要把前件局限在当前层L中查找而是要从全部频繁项集层级构建support_map。另一个更严谨的做法是在枚举前件前加一个判断如果antecedent not in support_map直接跳过这条规则因为它本身不频繁用它做前件没有统计基础。5.5 现象商品ID类型不统一导致同物变两物数据里商品ID有的是数字1有的是字符串1在CSV里某些列被读成int另一些被读成str。frozenset({1})和frozenset({1})在Python看来是完全不同的两个集合所以同一件商品被拆成了两个项支持度被稀释频繁项集结果面目全非。这种问题现象不明显因为代码不会报错只是结果不对特别容易被当成“算法效果不好”而忽略。解决方式在数据加载阶段统一类型我在load_transactions里会强制str(item)后再加入集合保证所有商品ID都走同一种类型。如果你希望保留数字排序语义也可以在读取时统一int()但要做好非数字字段报错的处理。6. 规则评估用提升度筛掉无意义规则6.1 高置信度不等于规则有用置信度只能说明“买了前件的人里有多少比例买了后件”但忽略了后件本身在整个数据集里的基础出现概率。假设1000笔事务里800笔买面包买啤酒的200笔中有160笔也买了面包那规则“啤酒→面包”置信度高达0.8看起来很不错。可面包本身有80%出现率买啤酒的人买面包的比例并不比普通顾客高这条规则其实就是废话。这个问题要靠提升度lift来解决lift置信度/后件支持度。lift大于1才是正向关联等于1是相互独立小于1反而是负相关。示例数据集里啤酒→牛奶的置信度0.6后件牛奶支持度0.625提升度约0.96小于1说明买啤酒反而略微压低了买牛奶的概率。6.2 把提升度过滤加进代码几行代码的事我实现Apriori时会在generate_rules里加一个min_lift参数默认1.2只有当提升度达到这个阈值才保留规则def generate_rules(L, min_confidence, min_lift1.2): # ... 前面的支持度收集和规则枚举逻辑不变 ... conf support / support_map[antecedent] lift conf / support_map[consequent] if conf min_confidence and lift min_lift: rules.append({ antecedent: sorted(antecedent), consequent: sorted(consequent), support: round(support, 3), confidence: round(conf, 3), lift: round(lift, 3) }) return rules用min_lift过滤后示例数据里大部分规则的lift只有0.96或1.07会被筛掉只剩几组相对有信号意义的组合。这样虽然输出的规则变少了但每条都有可能真正指导业务而不是让运营在一堆高置信度废规则里做人力筛选。我最早写Apriori时也迷信置信度觉得0.6以上就能用结果在某公司的活动分析里推出一条“酸奶→牛奶”的强规则置信度0.7实际上酸奶和牛奶在整体购买人群里出现率都很高提升度只有1.05业务上根本不具备可操作空间。那次之后我养成了一个习惯任何关联规则结果产出后第一件事就是按提升度排序低于1的直接丢弃再谈后续分析。希望这个习惯也能帮到你省去一些无效排查的时间让这份带数据集的实现真正产出能落地的结果。本文还有配套的精品资源点击获取