Python端到端数据挖掘实战:从SQLite数仓到手写Apriori
简介本资源是一份面向高校数据仓库与数据挖掘课程学习者的Python实践项目聚焦频繁模式挖掘核心算法实现适用于期末大作业、课程设计及算法原理巩固。项目基于经典Apriori算法支持Gutenberg与DBLP等多源数据集在任务1活跃作者挖掘、任务2合作组识别、任务3主题关联分析三个维度展开多粒度模式发现并配套完整代码注释、Markdown说明文档与PDF结题报告新手可快速理解逻辑并部署运行。压缩包共41个文件含8个核心Python脚本如Associations.py、task1_active.py等、24个文本类数据/中间结果文件、3张可视化结果图、2份结构化说明文档README.md与项目说明.md、1份PDF报告及1个.gz压缩数据整体5.84MB目录层级清晰模块划分明确src/、data/、result/、img/等。目前已有817人学习下载提供从理论到编码、从数据预处理到结果可视化的全链路参考是掌握关联规则挖掘工程落地的高价值实践样本。1. 这不是“抄个Apriori就能交差”的作业它是一次从数据入库、清洗、建模到可复现报告的端到端数据工程实战你手上的这份“Python实现的数据仓库与数据挖掘大作业”表面看是课程要求的频繁模式挖掘任务实际藏着一条被多数学生跳过的暗线如何让挖掘结果真正可信、可追溯、可复现。我带过三届数据科学方向的课程设计90%的同学卡在第一步——把老师给的CSV直接喂进mlxtend.frequent_patterns.apriori()调完min_support0.1就截图交报告。结果呢支持度阈值拍脑袋定的事务ID没去重导致计数翻倍空值没处理让整个项集生成崩掉最后连“牛奶→面包”这种基础规则都跑不出来。而这份作业真正的价值在于它强制你走完一个微型但完整的数据闭环用Python搭起轻量级数据仓库不是装Hive而是用SQLitePandas构建可版本化、带元数据的本地存储把原始销售流水规整成事务型宽表再用纯Python实现Apriori不依赖mlxtend黑匣子每一步输出中间状态快照最终生成带置信度/提升度校验的PDF报告。它不考你背算法考你能不能让机器“说清楚每一笔支持度是怎么算出来的”。适合正在学《数据挖掘导论》《数据库系统原理》交叉课的大三学生也适合想补足工程落地短板的转行者——因为企业里没人会给你干净的one-hot事务矩阵你要自己从杂乱日志里捞出“谁在什么时间买了什么”。2. 用SQLitePandas搭一个“能写进简历”的轻量级数据仓库不是建库建表而是建数据契约2.1 为什么不用MySQL或PostgreSQL——小项目里“可移植性”比“高并发”重要十倍很多同学一上来就装MySQL配用户权限、开远程连接、写SQLAlchemy连接池……结果交作业时发现导师电脑没装服务端或者Docker镜像拉不下来。我们选SQLite不是因为它“简单”而是因为它零配置、单文件、自带ACID、Python原生支持。更重要的是它强迫你思考数据契约data contract——每个表必须有明确的主键、外键约束、非空字段定义而不是靠代码注释糊弄过去。比如销售事实表sales_fact我们约定transaction_id为主键UUID4生成杜绝人工编号冲突product_id和category_id为外键关联维表保证后续JOIN不飘sale_time必须为datetime类型避免字符串解析歧义quantity和amount设为NOT NULL缺失值必须显式标记为NULL而非0提示SQLite虽不强制外键检查默认关闭但务必在连接时启用sqlite3.connect(db_path, isolation_levelNone)后执行PRAGMA foreign_keys ON;否则维表关联会静默失效。2.2 用Pandas做ETL清洗逻辑必须可复现不能靠Excel手动删空行原始数据常是Excel或CSV含标题行错位、重复订单、商品名大小写混用iPhone vs iphone、数量为负值等。我们用Pandas写确定性清洗函数关键点在于所有转换必须有日志记录和中间快照import pandas as pd import sqlite3 from datetime import datetime def etl_sales_raw_to_fact(raw_path: str, db_path: str): # 步骤1读取并标准化列名防止Excel列名带空格/中文 df pd.read_csv(raw_path, encodingutf-8) df.columns [col.strip().lower().replace( , _) for col in df.columns] # 步骤2强类型转换 异常拦截比try-except更可控 df[sale_time] pd.to_datetime(df[sale_time], errorscoerce) invalid_time_mask df[sale_time].isna() if invalid_time_mask.any(): # 记录问题行到log表而非直接drop log_df df[invalid_time_mask].copy() log_df[error_type] invalid_sale_time log_df[processed_at] datetime.now() log_df.to_sql(etl_log, sqlite3.connect(db_path), if_existsappend, indexFalse) df df[~invalid_time_mask].copy() # 步骤3业务规则清洗示例负数量视为退货单独存入refund_fact表 refund_mask df[quantity] 0 if refund_mask.any(): refund_df df[refund_mask].copy() refund_df[quantity] -refund_df[quantity] # 转为正数便于统计 refund_df.to_sql(refund_fact, sqlite3.connect(db_path), if_existsappend, indexFalse) df df[~refund_mask].copy() # 步骤4写入事实表带ON CONFLICT REPLACE防重复导入 conn sqlite3.connect(db_path) df.to_sql(sales_fact, conn, if_existsreplace, indexFalse) conn.close() return df.shape[0] # 执行清洗 n_rows etl_sales_raw_to_fact(raw_sales.csv, dw.db) print(f✅ 清洗完成写入{ n_rows }条有效销售记录)这段代码的价值不在功能而在可审计性errorscoerce让时间解析失败转为NaT而非抛异常中断流程问题数据不丢弃而是存入etl_log表后续可查“哪天哪些订单时间格式错误”退货逻辑分离到独立表避免在sales_fact中用负数污染关联分析if_existsreplace确保每次重跑ETL时旧数据被完整覆盖杜绝增量累加错误。2.3 维表构建用字典映射代替硬编码让“苹果手机”和“iPhone”自动归一频繁模式挖掘对商品名称敏感——“iPhone13”和“iphone13”会被视为不同项。我们建product_dim维表用product_code如IP13-BLK作为唯一键product_name为标准化名称# 构建产品维表示例映射规则 product_mapping { iphone13: iPhone 13, IP13: iPhone 13, iphone 13 pro: iPhone 13 Pro, xiaomi 12: Xiaomi 12, mi 12: Xiaomi 12 } # 从sales_fact中提取原始product_name映射后写入维表 conn sqlite3.connect(dw.db) df_sales pd.read_sql(SELECT DISTINCT product_name FROM sales_fact, conn) df_sales[standard_name] df_sales[product_name].str.lower().map(product_mapping).fillna(df_sales[product_name]) df_sales[product_code] df_sales[standard_name].apply(lambda x: x.replace( , ).replace(-, ).upper()[:8]) # 简单哈希生成code # 去重后写入维表避免同名不同code df_dim df_sales.drop_duplicates(subset[standard_name]).copy() df_dim.to_sql(product_dim, conn, if_existsreplace, indexFalse) conn.close()关键参数说明fillna(df[product_name])保证未映射项保留原名不丢失数据product_code生成规则用前8位大写无空格字符串既保证可读性又避免MD5过长drop_duplicates(subset[standard_name])防止“iPhone 13”被映射两次生成不同code。3. 从零手写Apriori不调mlxtend看清支持度计算的每一个除法3.1 为什么必须手写——黑盒算法让你无法解释“为什么这个规则支持度是0.237”mlxtend.frequent_patterns.apriori()返回DataFrame但你不知道它内部怎么处理空值、如何计数、是否去重事务。手写Apriori强制你直面三个核心问题事务去重同一张小票多次扫描同一商品应计为1次事务而非多次空值穿透某行product_name为空该事务是否参与计数最小支持度分母是总事务数还是非空事务数我们定义事务数据结构为List[Set[str]]确保每条事务是去重后的商品集合def load_transactions_from_db(db_path: str) - list: 从sales_fact product_dim JOIN获取事务列表 conn sqlite3.connect(db_path) # 关键GROUP BY transaction_id STRING_AGGSQLite 3.35或用Python聚合 # 兼容旧版SQLite先查所有transaction_id再逐个查商品 trans_ids pd.read_sql(SELECT DISTINCT transaction_id FROM sales_fact, conn)[transaction_id].tolist() transactions [] for tid in trans_ids: # 获取该事务下所有标准化商品名 items_df pd.read_sql( SELECT pd.standard_name FROM sales_fact sf JOIN product_dim pd ON sf.product_name pd.product_name WHERE sf.transaction_id ? , conn, params(tid,)) # 过滤空值转为frozenset不可变可hash items frozenset(items_df[standard_name].dropna().str.strip()) if items: # 忽略空事务 transactions.append(items) conn.close() return transactions # 加载事务 transactions load_transactions_from_db(dw.db) print(f 加载{len(transactions)}条有效事务已去重、去空)3.2 Apriori核心逐层生成候选项集用集合运算替代嵌套循环标准Apriori伪代码里“对每个候选项集遍历所有事务计数”效率极低。我们用Python内置set.issubset()和collections.Counter优化from collections import Counter from itertools import combinations def apriori(transactions: list, min_support: float) - dict: 返回 {k: [(itemset, support), ...]}k为项集长度 n_transactions len(transactions) min_count int(min_support * n_transactions) # 向下取整避免浮点误差 # L1单个商品支持度 item_counter Counter() for t in transactions: item_counter.update(t) L1 [(frozenset([item]), count/n_transactions) for item, count in item_counter.items() if count min_count] # 初始化结果 all_frequent {1: L1} Lk L1 # 当前层频繁项集 k 2 while Lk: # 步骤1由Lk-1生成Ck候选项集 Ck set() for i in range(len(Lk)): for j in range(i1, len(Lk)): # 合并两个k-1项集若前k-2项相同则合并Apriori性质 itemset1, _ Lk[i] itemset2, _ Lk[j] union itemset1 | itemset2 if len(union) k: # 确保是k项集 Ck.add(union) # 步骤2扫描事务计数 candidate_counter Counter() for t in transactions: for candidate in Ck: if candidate.issubset(t): # 集合包含判断O(1)平均 candidate_counter[candidate] 1 # 步骤3筛选频繁项集 Lk [(candidate, count/n_transactions) for candidate, count in candidate_counter.items() if count min_count] if Lk: all_frequent[k] Lk k 1 return all_frequent # 执行挖掘min_support0.02即至少2%事务包含该组合 frequent_itemsets apriori(transactions, min_support0.02) print(f 发现{sum(len(v) for v in frequent_itemsets.values())}个频繁项集)参数说明min_count int(min_support * n_transactions)用整数计数避免浮点精度导致的边界错误如0.02100020.0但0.0299919.98→int19实际应为20candidate.issubset(t)比set(candidate) set(t)快3倍因无需构造新setfrozenset作为key保证项集可hash用于Counter计数。3.3 规则生成置信度与提升度必须手算拒绝“黑箱输出”Apriori只给频繁项集关联规则需额外计算。我们定义规则X → Y其中X ∩ Y ∅X ∪ Y是频繁项集def generate_rules(frequent_itemsets: dict, min_confidence: float 0.5) - list: 返回 (antecedent, consequent, confidence, lift) 元组列表 lift confidence / support(Y)lift1表示正相关 rules [] # 只处理长度2的项集否则无法拆分前件后件 for k, itemsets in frequent_itemsets.items(): if k 2: continue for itemset, support in itemsets: # 拆分所有可能的X→Y|X|1, |Y|k-1 是最常用场景 items list(itemset) for i in range(len(items)): antecedent frozenset([items[i]]) consequent frozenset(items[:i] items[i1:]) # 查找antecedent的支持度从L1中获取 ant_support None for item, sup in frequent_itemsets.get(1, []): if item antecedent: ant_support sup break if ant_support is not None: confidence support / ant_support if confidence min_confidence: # 计算提升度lift confidence / support(consequent) cons_support None for item, sup in frequent_itemsets.get(len(consequent), []): if item consequent: cons_support sup break lift confidence / cons_support if cons_support else 0 rules.append((antecedent, consequent, confidence, lift)) return sorted(rules, keylambda x: x[2], reverseTrue) # 按置信度降序 rules generate_rules(frequent_itemsets, min_confidence0.6) print(f⚡ 生成{len(rules)}条高置信度规则min_confidence0.6) for ant, cons, conf, lift in rules[:3]: print(f {set(ant)} → {set(cons)} | conf{conf:.3f}, lift{lift:.2f})关键细节lift confidence / support(Y)提升度1才说明X出现显著提升Y出现概率sorted(..., reverseTrue)优先展示业务价值最高的规则ant_support和cons_support从frequent_itemsets中查找确保所有支持度来源一致。4. 避坑那些让报告被退回重做的5个血泪现场4.1 现象Apriori跑出空结果控制台无报错原因原始数据中transaction_id未去重同一小票被当多条事务处理导致min_support分母虚高。例如1000条物理小票因Excel复制粘贴产生2000行记录min_support0.02实际需40次出现但真实事务仅1000条规则永远达不到阈值。解决在load_transactions_from_db()中加断言assert len(trans_ids) len(set(trans_ids))并打印重复ID样本用pandas.DataFrame.duplicated(subset[transaction_id])定位问题行。4.2 现象规则中出现{ } → {milk}这类空格项原因商品名清洗时未strip空格 milk 存入维表后变成独立项。解决在ETL步骤2后加统一清洗df[product_name] df[product_name].str.strip()并在维表构建前assert df[product_name].str.contains(r^\s$).sum() 0。4.3 现象PDF报告里支持度显示为0.19999999999999998原因浮点数二进制表示误差直接f{support:.3f}会暴露精度问题。解决用round(support, 6)先截断再格式化f{round(support, 6):.3f}或改用decimal.Decimal小项目够用。4.4 现象mlxtend结果和手写结果支持度不一致原因mlxtend.apriori()默认use_colnamesFalse输入是布尔矩阵会把NaN当False计入分母而我们的事务列表明确过滤了空事务。解决手写版以len(transactions)为分母mlxtend版需传入pd.get_dummies()后的one-hot矩阵并确认dropnaFalse。4.5 现象VS Code调试时apriori()函数卡死无响应原因事务量过大10万条且Ck候选项集爆炸k4时组合数达C(n,4)内存溢出。解决加内存监控import psutil; print(f内存使用{psutil.virtual_memory().percent}%)对大数据集启用max_len3参数限制最大项集长度或改用FP-Growth本作业不强制但可备注为优化方向。5. 把挖掘结果变成“老板能看懂”的PDF报告用Jinja2模板Matplotlib图表固化结论5.1 报告结构设计拒绝Word截图用代码生成可复现PDF一份合格的报告PDF必须满足可复现所有图表数据来自当前运行的frequent_itemsets和rules变量可验证附上本次运行的min_support、min_confidence、事务总数等元数据可扩展新增分析如Top10商品支持度柱状图只需改模板不改Python逻辑。我们用Jinja2渲染HTML再用weasyprint转PDF比ReportLab更易上手支持CSSfrom jinja2 import Template import weasyprint # 报告模板内联在代码中避免外部文件依赖 html_template !DOCTYPE html html head meta charsetUTF-8 title频繁模式挖掘报告/title style body { font-family: Segoe UI, sans-serif; margin: 40px; } .section { margin-top: 30px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } .chart { page-break-inside: avoid; } /style /head body h1频繁模式挖掘分析报告/h1 pstrong生成时间/strong{{ now }}/p pstrong数据源/strong{{ db_path }}{{ n_transactions }}条事务/p pstrong参数/strongmin_support{{ min_support }}, min_confidence{{ min_confidence }}/p div classsection h2Top 5 频繁项集按支持度/h2 table trth项集/thth支持度/th/tr {% for itemset, support in top_itemsets %} trtd{{ itemset|join(, ) }}/tdtd{{ %.3f|format(support) }}/td/tr {% endfor %} /table /div div classsection h2Top 5 关联规则按置信度/h2 table trth前件/thth后件/thth置信度/thth提升度/th/tr {% for ant, cons, conf, lift in top_rules %} tr td{{ ant|list|join(, ) }}/td td{{ cons|list|join(, ) }}/td td{{ %.3f|format(conf) }}/td td{{ %.2f|format(lift) }}/td /tr {% endfor %} /table /div div classsection chart h2商品支持度分布Top 10/h2 img srcdata:image/png;base64,{{ chart_base64 }} alt支持度柱状图/ /div /body /html # 生成图表用matplotlib画Top10商品支持度 import matplotlib.pyplot as plt import base64 from io import BytesIO # 提取L1商品支持度 l1_items [(list(item)[0], sup) for item, sup in frequent_itemsets.get(1, [])] l1_items.sort(keylambda x: x[1], reverseTrue) top10 l1_items[:10] plt.figure(figsize(10, 6)) plt.bar([x[0] for x in top10], [x[1] for x in top10]) plt.title(Top 10 商品支持度) plt.ylabel(支持度) plt.xticks(rotation45, haright) plt.tight_layout() # 转base64嵌入HTML buffer BytesIO() plt.savefig(buffer, formatpng, dpi150) buffer.seek(0) chart_base64 base64.b64encode(buffer.read()).decode() # 渲染HTML template Template(html_template) html_output template.render( nowdatetime.now().strftime(%Y-%m-%d %H:%M:%S), db_pathdw.db, n_transactionslen(transactions), min_support0.02, min_confidence0.6, top_itemsets[(list(itemset), round(support, 3)) for itemset, support in frequent_itemsets.get(2, [])[:5]], top_rules[(list(ant), list(cons), round(conf, 3), round(lift, 2)) for ant, cons, conf, lift in rules[:5]], chart_base64chart_base64 ) # 生成PDF pdf weasyprint.HTML(stringhtml_output).write_pdf() with open(frequent_pattern_report.pdf, wb) as f: f.write(pdf) print( PDF报告已生成frequent_pattern_report.pdf)注意weasyprint依赖cairocffi和PangoLinux需sudo apt install libpango-1.0-0 libcairo2Windows用户建议用conda安装conda install -c conda-forge weasyprint。5.2 报告里的“隐藏价值”用代码注释当业务注解不要在PDF里写“算法原理”而要写“这个规则为什么重要”。例如在规则{iPhone 13} → {AirPods} | conf0.72旁加注释“72%购买iPhone 13的顾客同时购买AirPods建议在iPhone 13详情页增加AirPods‘搭配购买’弹窗预计提升AirPods销量。”这种注释不写在代码里而放在Jinja2模板的{% comment %}块中生成时自动忽略但你作为作者知道每条规则对应的业务动作。5.3 最后一道防线用Git提交报告PDF源码数据库文件形成可回溯证据链很多同学交作业只交PDF导师问“支持度怎么算的”答不上来。正确做法是Git仓库根目录放dw.dbSQLite单文件可commitreport/目录放生成的PDFsrc/目录放所有Python脚本README.md写明执行顺序python etl.py→python mining.py→python report.py.gitignore只排除__pycache__/和.vscode/不忽略dw.db小项目数据文件可追踪。这样导师git clone后一键复现你的工作量全在代码里不是靠PPT美化。我带学生做这个作业时总会强调一句数据挖掘的终点不是算法准确率而是业务决策依据的可解释性。当你能指着PDF里的一条规则说出它在数据库里对应哪几张表、哪几行SQL、哪个清洗函数的哪一行代码你就已经跨过了“会调包”和“懂数据”的分水岭。这份作业的源码、文档、PDF不是交差的废纸而是你数据工程能力的第一份可验证资产。希望帮到你。本文还有配套的精品资源点击获取