资讯动态

Apriori与C4.5联动实战:从关联规则到可解释决策树

发布时间:2026/10/3 9:35:48 来源:尧图企业网站定制
简介本资源是一份面向数据挖掘初学者与Python实践者的经典算法实现合集聚焦关联规则与决策树两大核心方向助力读者深入理解Apriori、ID3、C4.5及FP树等关键算法原理并快速上手编码。压缩包共15个文件含9个可直接运行的Python源码如AOK---Apriori.py、AOK---C4.5.py、AOK---FP树.py等、4个文本类说明与测试数据txt以及2个Excel格式的模型训练/测试数据xls总大小仅49KB轻量易用适合作为课程实验、算法复现或面试准备的参考基线代码。目前已有208人学习下载所有脚本均采用模块化结构附带清晰注释与典型数据示例覆盖数据预处理、算法主逻辑、结果评估等完整环节特别适合在无框架依赖环境下动手调试、对比不同算法的实现差异与性能表现。1. 这不是“算法合集”压缩包它是一套可跑通、可调试、可落地的 Apriori C4.5 实战流水线你双击打开数据挖掘各类算法.zip看到Apriori_c4.5_python_数据挖掘_算法这个文件夹名——别急着解压复制粘贴。这不是网上泛滥的“Python 数据挖掘100例”式拼凑代码而是一条从原始事务表出发先挖频繁项集与关联规则再用结构化规则驱动决策树建模的闭环路径。它解决的不是“怎么写 for 循环”而是“超市购物篮里哪些商品组合真正影响顾客结账行为这些组合能否反向指导货架陈列或促销捆绑”——真实业务中Apriori 不是孤立跑个支持度阈值C4.5 也不是调个sklearn.tree.DecisionTreeClassifier就完事。这套代码强制你面对事务数据如何清洗成标准格式最小支持度怎么定才不漏信号也不爆内存C4.5 的信息增益比Gain Ratio和 ID3 的纯信息增益Information Gain在实际数据上差多少剪枝策略选预剪枝还是后剪枝本篇不讲公式推导只讲你明天就能在本地 Python 环境里跑通、改参数、看结果、调效果的每一步。适合刚学完《数据挖掘导论》第5章、手头有销售日志/用户点击流/医疗诊断记录想验证规则分类联动的同学。2. 用原生 Python 实现 Apriori不依赖 mlxtend自己写清楚每层候选集生成逻辑Apriori 的核心不是“找频繁项集”而是用已知的 k-1 频繁项集高效生成 k 阶候选集并通过单次数据库扫描完成计数。很多教程直接调mlxtend.frequent_patterns.apriori但一旦你的事务数据含缺失值、嵌套结构或需要自定义支持度计算比如按用户ID加权黑盒就失效了。我们从零实现重点落在“怎么让候选集生成不爆炸”和“怎么避免重复扫描”。2.1 输入数据预处理把原始 CSV 转成标准事务列表假设你拿到的是某连锁超市的销售日志sales_log.csv字段为order_id, product_name, quantity, timestamp。Apriori 要求输入是“事务集合”即每个订单一行该行是商品名列表如[牛奶, 面包, 鸡蛋]。不能直接用pandas.groupby(order_id)[product_name].list()—— 因为同一订单内同商品可能多次出现如买了3瓶可乐需去重import pandas as pd # 读取原始数据 df pd.read_csv(sales_log.csv) # 按 order_id 分组取 product_name 去重列表注意quantity 不影响关联规则只关心是否购买 transactions df.groupby(order_id)[product_name].apply(lambda x: list(set(x))).tolist() # 验证前3条 print(transactions[:3]) # 输出示例: [[牛奶, 面包], [啤酒, 尿布, 薯片], [牛奶, 尿布]]提示若商品名含空格、特殊字符或大小写混杂如Milk和milk必须统一清洗。常见做法是.str.strip().str.lower()后再去重否则Apple和apple会被视为不同项。2.2 Apriori 主循环逐层生成候选集并剪枝关键点在于candidate_gen函数——它接收上一层频繁项集L[k-1]生成C[k]k阶候选集但必须满足两个条件连接步Join Step仅当两个 (k-1)-项集前 k-2 个元素相同才合并剪枝步Prune Step若候选集的任意 (k-1)-子集不在L[k-1]中则剔除。这是 Apriori 高效的核心避免生成大量无效候选集from itertools import combinations def apriori(transactions, min_support0.01): # 步骤1生成1-项集候选集 C1 C1 {} for transaction in transactions: for item in transaction: C1[item] C1.get(item, 0) 1 # 计算支持度并过滤 L1 {item: cnt/len(transactions) for item, cnt in C1.items() if cnt/len(transactions) min_support} L [None, L1] # L[1] 是1-项集频繁集 k 2 while True: # 步骤2由 L[k-1] 生成 C[k] Ck {} items list(L[k-1].keys()) # 连接步两两组合要求前 k-2 元素相同对k2直接全组合 if k 2: candidates list(combinations(items, k)) else: # 对k3先排序再比较前缀 sorted_items [sorted(list(item)) for item in items] candidates [] for i in range(len(sorted_items)): for j in range(i1, len(sorted_items)): if sorted_items[i][:-1] sorted_items[j][:-1]: # 前k-2相同 merged tuple(sorted(set(sorted_items[i] sorted_items[j]))) if len(merged) k: # 确保无重复 candidates.append(merged) # 剪枝步检查每个候选集的所有(k-1)-子集是否都在 L[k-1] 中 valid_candidates [] for candidate in candidates: # 生成所有(k-1)-子集 subsets list(combinations(candidate, k-1)) # 若所有子集都在 L[k-1] 的 keys 中注意L[k-1] 的 key 是 tuple 或 frozenset if all(tuple(subset) in L[k-1] or frozenset(subset) in L[k-1] for subset in subsets): valid_candidates.append(candidate) # 计数扫描所有事务统计 valid_candidates 出现次数 for transaction in transactions: transaction_set set(transaction) for candidate in valid_candidates: if set(candidate).issubset(transaction_set): Ck[candidate] Ck.get(candidate, 0) 1 # 过滤支持度 Lk {candidate: cnt/len(transactions) for candidate, cnt in Ck.items() if cnt/len(transactions) min_support} if not Lk: break L.append(Lk) k 1 return L # 运行示例min_support 设为 0.02即至少出现在2%的订单中 frequent_itemsets apriori(transactions, min_support0.02) print(f共找到 {sum(len(L) for L in frequent_itemsets[1:])} 个频繁项集)参数说明min_support支持度阈值建议从 0.011%开始试若结果为空则下调若项集爆炸则上调transactions必须是list[list[str]]格式内层列表不能含重复项返回L是列表L[1]是1-项集字典key为字符串L[2]是2-项集字典key为tuple以此类推。3. 从频繁项集生成强关联规则用置信度提升度双过滤避开“啤酒→尿布”的玄学陷阱Apriori 找到频繁项集只是第一步。真正驱动业务的是规则X → Y表示“如果买了 X大概率也会买 Y”。但直接枚举所有子集组合会爆炸——一个5项频繁集有2^5 - 2 30种非空真子集组合排除 X∅ 和 Y∅。更糟的是高支持度规则未必有意义。比如{牛奶} → {面包}支持度0.15但若面包本身购买率就0.14这条规则毫无价值。必须用置信度Confidence和提升度Lift双重卡控。3.1 规则生成函数只对频繁项集的非空真子集生成规则我们只对L[k]中每个频繁k-项集I枚举其所有非空真子集X令Y I - X计算conf(X→Y) support(I)/support(X)。注意support(X)必须来自L[len(X)]因为 X 是频繁的否则规则无效def generate_rules(frequent_itemsets, min_confidence0.5, min_lift1.0): rules [] # 从2-项集开始1-项集无法拆分出有效规则 for k in range(2, len(frequent_itemsets)): if not frequent_itemsets[k]: continue for itemset, support_I in frequent_itemsets[k].items(): itemset_set set(itemset) # 枚举所有非空真子集 X for i in range(1, len(itemset)): for X in combinations(itemset, i): X tuple(sorted(X)) Y tuple(sorted(itemset_set - set(X))) # 获取 support(X)必须存在于 L[len(X)] 中 if len(X) 1: support_X frequent_itemsets[1].get(X[0], 0) else: support_X frequent_itemsets[len(X)].get(X, 0) if support_X 0: continue confidence support_I / support_X # 计算 liftlift conf(X→Y) / support(Y) if len(Y) 1: support_Y frequent_itemsets[1].get(Y[0], 0) else: support_Y frequent_itemsets[len(Y)].get(Y, 0) lift confidence / support_Y if support_Y 0 else 0 if confidence min_confidence and lift min_lift: rules.append({ antecedent: list(X), consequent: list(Y), support: support_I, confidence: confidence, lift: lift }) return rules # 生成规则置信度≥50%提升度≥1.0 rules generate_rules(frequent_itemsets, min_confidence0.5, min_lift1.0) print(f生成 {len(rules)} 条强规则) # 示例输出{antecedent: [牛奶], consequent: [面包], support: 0.15, confidence: 0.62, lift: 1.85}为什么 lift 1.0 是硬门槛lift 1 表示 X 和 Y 独立买牛奶不影响买面包概率lift 1 表示正相关买牛奶的人买面包的概率是随机人群的 lift 倍lift 1 表示负相关买牛奶的人反而不太买面包。很多教程只设min_confidence结果导出一堆“盐→水”因为盐和水都高频但无关——lift 是防玄学的关键。3.2 规则解读与业务落地三类典型规则及应对策略规则类型示例业务含义落地动作互补型{纸巾} → {洗手液}lift3.2两者常被同时购买属清洁场景闭环组合促销、货架相邻陈列、捆绑定价替代型{可口可乐} → {百事可乐}lift0.4买可乐的人很少买百事存在品牌替代避免同区域陈列做差异化赠品引导型{婴儿奶粉} → {纸尿裤}lift2.8强关联但纸尿裤购买频次更高对奶粉购买者推送纸尿裤优惠券注意规则方向不可逆。X→Y和Y→X是两条不同规则lift 值通常不同。务必按业务目标选择前件antecedent——你想影响什么行为推奶粉时搭纸尿裤还是推纸尿裤时搭奶粉4. 用 C4.5 实现决策树分类手写信息增益比计算理解剪枝如何防止过拟合C4.5 是 ID3 的进化版核心改进两点用信息增益比Gain Ratio替代信息增益Information Gain解决 ID3 对取值多的属性如用户ID的偏好内置剪枝机制Pruning通过悲观误差估计Pessimistic Error Estimate自动裁剪子树。本节不调sklearn而是用原生 Python 实现 C4.5 的核心分裂逻辑让你看清为什么outlooksunny分支下humidityhigh会被剪掉为什么temperature属性在 Gain Ratio 排名第三却成了根节点4.1 数据准备将关联规则转化为特征工程输入Apriori 输出的是规则C4.5 需要结构化表格。我们把每条规则的前件antecedent转为二元特征是否购买该商品后件consequent作为标签是否购买该商品。例如规则[牛奶] → [面包]则构造新列has_milk0/1和标签buy_bread0/1import numpy as np # 假设我们选定 top 5 规则用于建模 top_rules sorted(rules, keylambda x: x[lift], reverseTrue)[:5] feature_names [] for rule in top_rules: feature_names.extend([fhas_{_.join(rule[antecedent]).replace( , _)}]) # 标签列名buy_XXX label_col fbuy_{_.join(rule[consequent]).replace( , _)} # 构造特征矩阵 X 和标签 y X [] y [] for transaction in transactions: row_x [] for rule in top_rules: # 检查 antecedent 是否全在 transaction 中 antecedent_set set(rule[antecedent]) has_antecedent 1 if antecedent_set.issubset(set(transaction)) else 0 row_x.append(has_antecedent) # 标签consequent 是否在 transaction 中 consequent_set set(top_rules[0][consequent]) # 这里以第一条规则的 consequent 为标签 buy_consequent 1 if consequent_set.issubset(set(transaction)) else 0 y.append(buy_consequent) X.append(row_x) X np.array(X) y np.array(y) print(f特征矩阵形状: {X.shape}, 标签长度: {len(y)})4.2 C4.5 树构建手写 Gain Ratio 计算与分裂选择Gain Ratio Information Gain / Split Information。其中 Split Information 惩罚取值多的属性def entropy(labels): 计算信息熵 if len(labels) 0: return 0 _, counts np.unique(labels, return_countsTrue) probs counts / len(labels) return -np.sum([p * np.log2(p) for p in probs if p 0]) def gain_ratio(data, labels, feature_idx): 计算第 feature_idx 列特征的信息增益比 values np.unique(data[:, feature_idx]) if len(values) 1: return 0 # 计算父节点熵 parent_entropy entropy(labels) # 计算信息增益 weighted_child_entropy 0 split_info 0 for val in values: mask data[:, feature_idx] val child_labels labels[mask] weight len(child_labels) / len(labels) weighted_child_entropy weight * entropy(child_labels) split_info - weight * np.log2(weight) info_gain parent_entropy - weighted_child_entropy if split_info 0: return 0 return info_gain / split_info # 寻找最佳分裂特征 def best_feature_split(data, labels): gains [] for i in range(data.shape[1]): gr gain_ratio(data, labels, i) gains.append(gr) return np.argmax(gains), max(gains) # 示例找第一个分裂点 best_feat, best_gr best_feature_split(X, y) print(f最佳分裂特征索引: {best_feat}, Gain Ratio: {best_gr:.3f})参数说明data二维 numpy 数组每行一个样本每列一个特征0/1labels一维数组类别标签0/1feature_idx当前评估的特征列索引返回best_feat是使 Gain Ratio 最大的特征索引best_gr是其值。4.3 后剪枝实现用验证集误差估计决定是否剪枝C4.5 采用悲观剪枝Pessimistic Pruning对每个非叶节点估算其子树在验证集上的误差若剪枝后误差不增考虑置信度则剪掉。我们简化为用训练集 20% 作验证集若子树预测错误数 单节点预测错误数则剪枝from sklearn.model_selection import train_test_split # 划分训练/验证集 X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42) # 构建树此处省略递归建树代码聚焦剪枝逻辑 def prune_tree(node, X_val, y_val, alpha0.25): 后剪枝alpha 为剪枝阈值越大越激进 if node.is_leaf: return # 递归剪枝子树 for child in node.children: prune_tree(child, X_val, y_val, alpha) # 计算剪枝前误差 pred_before predict_tree(node, X_val) error_before np.mean(pred_before ! y_val) # 计算剪枝后误差用该节点的多数类代替整个子树 majority_class np.bincount(y_train[node.sample_indices]).argmax() pred_after np.full(len(y_val), majority_class) error_after np.mean(pred_after ! y_val) # 若剪枝后误差增加不超过 alpha则剪枝 if error_after error_before alpha: node.children [] node.is_leaf True node.class_label majority_class血泪经验alpha是关键超参。设为 0.05 太保守几乎不剪0.5 太激进树变浅欠拟合。我一般从 0.2 开始试观察验证集准确率变化曲线——拐点处即最优。5. 避坑指南Apriori 与 C4.5 联动开发中 4 个真实翻车现场Apriori 和 C4.5 看似独立但串联使用时隐藏着数据流断裂、指标错位、边界溢出等致命坑。以下是我在线上项目中踩过的、导致模型上线后效果暴跌的 4 个典型问题附带定位方法和修复命令。5.1 现象Apriori 运行 10 分钟后内存爆满进程被 kill原因候选集爆炸。当min_support设得过低如 0.001且事务平均长度 10 时C3候选集数量可达百万级C4直接 OOM。解决前置过滤用pandas先筛掉低频商品出现次数 min_support * len(transactions)限制最大项集长度在apriori()函数中加if k 5: break业务中 5 项组合已极少见改用 FP-Growth对大数据集替换为fpgrowthmlxtend提供内存占用降 70%。pip install mlxtend # 替换原 Apriori 调用 from mlxtend.frequent_patterns import fpgrowth frequent_itemsets fpgrowth(df_onehot, min_support0.02, use_colnamesTrue)5.2 现象C4.5 树深度达 20训练准确率 99%验证准确率仅 65%原因未剪枝 特征稀疏。Apriori 生成的二元特征矩阵极度稀疏大部分为 0C4.5 在稀疏数据上易过拟合。解决强制预剪枝建树时设max_depth6,min_samples_split20特征降维对X矩阵做 PCA保留 95% 方差或用SelectKBest选 top 10 特征改用 Random Forest单棵 C4.5 易过拟合集成后鲁棒性大幅提升。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier(n_estimators100, max_depth8, min_samples_split15, random_state42) rf.fit(X_train, y_train)5.3 现象规则X→Y置信度 0.95但用 C4.5 预测X时Y预测准确率仅 0.4原因数据分布偏移。Apriori 在全量事务上统计C4.5 训练集是规则子集如只取含X的事务二者分布不一致。解决统一数据视图C4.5 的X和y必须从同一事务子集抽取如transactions_with_X [t for t in transactions if set(X).issubset(set(t))]添加负样本对不含X的事务也构造(X0, y0)样本平衡数据用规则置信度初始化先验在 C4.5 叶节点不直接用多数类而用conf(X→Y)作为P(Y1|X)的贝叶斯先验。5.4 现象sklearn的DecisionTreeClassifier和手写 C4.5 输出完全不同原因sklearn默认用gini不纯度且criterionentropy时用的是 ID3 的信息增益非 C4.5 的 Gain Ratio。解决确认指标sklearn无原生 Gain Ratio必须手写或换库如dtree包验证分裂逻辑打印sklearn树的tree_.feature和tree_.threshold对比手写代码的best_feature_split输出统一基线若必须用sklearn改用criteriongini并接受它是 CART 变种勿强行对标 C4.5。6. 进阶技巧用 Apriori 规则约束 C4.5 分裂让树结构可解释、可审计C4.5 的树结构常被诟病“黑匣子”——业务方看不懂为什么humiditynormal会导向playyes。但如果我们把 Apriori 规则作为分裂的硬约束就能生成“每层分裂都对应一条业务规则”的决策树。这不仅是技术炫技更是合规刚需如金融风控需解释每个拒绝理由。6.1 规则驱动分裂修改 C4.5 的最佳特征选择逻辑核心思想不从所有特征中找 Gain Ratio 最大者而是只在与当前节点匹配的 Apriori 规则前件中选特征。例如根节点对应全量数据我们筛选所有规则中antecedent长度1 的规则如[牛奶],[啤酒]将其对应特征列为候选def rule_constrained_split(data, labels, rules, current_depth0, max_rule_len3): 用规则约束的分裂只允许规则前件中的特征参与分裂 # 获取当前深度允许的规则depth0 用1项规则depth1 用2项规则... candidate_rules [r for r in rules if len(r[antecedent]) min(current_depth1, max_rule_len)] if not candidate_rules: return None, 0 # 提取这些规则对应的特征索引 candidate_features [] for rule in candidate_rules: # 找到 rule[antecedent] 在 feature_names 中的索引 feat_name fhas_{_.join(rule[antecedent]).replace( , _)} if feat_name in feature_names: candidate_features.append(feature_names.index(feat_name)) if not candidate_features: return None, 0 # 在候选特征中找 Gain Ratio 最大者 gains [] for idx in candidate_features: gr gain_ratio(data, labels, idx) gains.append(gr) if not gains: return None, 0 best_idx candidate_features[np.argmax(gains)] return best_idx, max(gains) # 使用示例 best_feat, best_gr rule_constrained_split(X_train, y_train, top_rules, current_depth0) print(f规则约束下最佳特征: {feature_names[best_feat]}, GR: {best_gr:.3f})6.2 可解释性增强树节点标注对应规则与 Lift 值在每个非叶节点存储其分裂所依据的规则及 Lift导出 HTML 树时直接显示class RuleNode: def __init__(self, feature_idx, threshold, children, ruleNone, lift0.0): self.feature_idx feature_idx self.threshold threshold self.children children self.rule rule # 如 {antecedent: [牛奶], consequent: [面包], lift: 1.85} self.lift lift # 构建节点时传入规则 node RuleNode( feature_idxbest_feat, threshold1, children[left_child, right_child], ruletop_rules[0], lifttop_rules[0][lift] )导出可视化时用graphviz节点 label 可设为has_milk1\\nLift: 1.85\\n→ buy_bread业务方一眼看懂这个分支成立是因为“买牛奶的人买面包的概率是随机人群的 1.85 倍”。6.3 验证效果用 SHAP 值量化规则贡献度即使树结构受规则约束仍需验证规则是否真起作用。用 SHAPSHapley Additive exPlanations计算每个特征即每条规则前件对最终预测的贡献import shap # 训练一个简单树depth3 tree DecisionTreeClassifier(max_depth3, criterionentropy) tree.fit(X_train, y_train) # 计算 SHAP 值 explainer shap.TreeExplainer(tree) shap_values explainer.shap_values(X_val) # 查看第一条验证样本的解释 shap.waterfall_plot(explainer.expected_value[1], shap_values[1][0], X_val[0], feature_names)关键洞察若某条规则前件如has_milk的 SHAP 值长期接近 0说明该规则在树中未被激活——要么规则本身 lift 低要么数据中has_milk1的样本太少。此时应回溯 Apriori 步骤调整min_support或清洗数据。我坚持在每个数据挖掘项目里先跑通 Apriori-C4.5 流水线再谈深度学习。因为规则驱动的决策树能让你在老板问“为什么拒绝这笔贷款”时指着屏幕说“因为客户同时满足‘收入5k’和‘负债率80%’这两条高风险规则lift 值 2.3误判率低于 5%。”——这比“模型输出 0.87”有力得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑