手写数据挖掘十大算法:k-means、kNN、CART源码实战与避坑指南
发布时间:2026/10/2 5:21:32来源:尧图网络
简介数据挖掘十大经典算法的Python实现源代码包覆盖关联规则、分类、回归、聚类、混合模型与链接分析等核心任务面向数据挖掘初学者、课程设计学生和算法研发人员帮助解决从理论到代码落地之间的难题。压缩包仅15个文件含7个Python算法脚本、测试数据集、说明文档及少量工程配置文件整体仅14KB轻量易用脚本分别实现Apriori、C4.5、CART、EM、K-means、KNN、PageRank等算法testset用于验证md提供使用说明。包内按算法分目录组织结构清晰便于按需查阅。已有1149人浏览学习。读者可直接运行或参考这些代码结合sklearn、networkx等库理解信息增益、基尼系数、期望最大化、距离度量、近邻分类和网页排名等关键机制也可将其修改到自有数据集上用于课程作业、论文复现或算法对比实验能显著缩短代码编写时间。1. 数据挖掘十大算法为什么值得读 Python 源码先从 k-means 咬一口“数据挖掘十大算法”这个叫法是 2006 年 IEEE ICDM 会议上被反复引用的那份经典清单里面是 C4.5、k-means、SVM、Apriori、EM、PageRank、AdaBoost、kNN、朴素贝叶斯、CART。“数据挖掘十大算法源代码Python”要解决的问题很直接不是让你背十个库的 fit 方法而是逼你把每个算法拆成自己能手写能改的 Python 代码。实际业务里你迟早会碰到文档没写的坑比如数据泄漏、类别型变量距离错乱、随机种子不固定导致复现不了这时候只有源码能救你。这篇文章按我自己的重写顺序来先啃 k-means、kNN、CART 这三个上手最快的再讲复现时必踩的参数坑最后给一套验证脚本。适合两类人被面试官追问“底层怎么实现”的和准备把经典算法搬进自研服务的。2. 先拧 k-means最小 Python 实现与源码里的三个关键参数2.1 初始化中心点为什么 k-means 是源码的第一道坎k-means 的算法主循环只有两步算距离、更新中心。很多人手写第一版都栽在初始化上——直接用np.random.choice(X, k)随机挑 k 个样本当中心聚类结果会随随机种子剧烈抖动运气差时直接收敛到了局部最优簇之间几乎是空的。k-means 的思路是“让初始中心互相远一点”第一个中心完全随机之后每个新中心按“离已有中心越远、被选中的概率越大”来抽样。这个策略不是玄学它保证初始中心在数据空间里铺开后续迭代次数少结果也更稳定。你去看 sklearn 的KMeans源码initk-means是默认值它专门调用了_kmeans_plusplus函数来做这一步如果你用initrandom在n_init次数不够多的情况下翻车的概率会明显上升。2.2 纯 numpy 实现 k-means 的骨架代码import numpy as np def kmeans_pp_init(X, k, rng): k-means 初始化先随机第一个中心再按距离概率补足 k 个中心 centers [X[rng.integers(0, len(X))]] for _ in range(1, k): # 每个样本到已选中心的最近距离平方 d2 np.min((X[:, None, :] - np.array(centers)) ** 2, axis1).sum(axis1) prob d2 / d2.sum() centers.append(X[rng.choice(len(X), pprob)]) return np.array(centers) def kmeans(X, k, max_iter100, tol1e-4, seed42): rng np.random.default_rng(seed) centers kmeans_pp_init(X, k, rng) for i in range(max_iter): # 分配阶段每个样本归到距离最近的中心 dist ((X[:, None, :] - centers[None, :, :]) ** 2).sum(axis-1) labels dist.argmin(axis1) # 更新阶段用簇内均值作为新中心 new_centers [] for c in range(k): member X[labels c] if len(member) 0: new_centers.append(member.mean(axis0)) else: # 空簇保留原中心避免直接崩掉 new_centers.append(centers[c]) new_centers np.array(new_centers) # 收敛判断中心位移小于 tol 就提前退出 if np.max(np.abs(new_centers - centers)) tol: return centers, labels, i 1 centers new_centers return centers, labels, max_iter逻辑说明argmin是在做硬聚类分配距离就是欧氏距离的平方省一次np.sqrt不影响类别归属。均值更新那行用的是布尔索引X[labels c]比循环分支清晰。空簇处理是生产环境里容易漏的细节小数据集上某些中心可能一个样本都分不到直接mean会得到nan我一般选择保留旧中心至少保证算法不中断。参数说明max_iter控制最大迭代次数默认 100 对多数中小数据集足够tol是中心点位移阈值设太小会多跑很多空转的迭代seed固定后整个流程可复现。注意 k 必须由你预先指定k-means 不会自己猜簇数。判断 k 是否合理跑一遍轮廓系数扫描比翻十篇论文都好用。2.3 读 sklearn.cluster.KMeans 源码该盯哪几个字段看封装源码时我一般只盯四个字段init、n_init、tol、algorithm。init决定用什么方式生产初始中心n_init默认是 10含义是“用不同随机种子跑 10 遍取惯性最小的那次”这就是 sklearn 比初学者手写版稳定的主要原因tol的收敛判断标准是中心变化相对均值algorithm在旧版本里还是full和elkan两种新版本默认lloyd。我建议手写版本至少加上n_init3的封装外层循环换随机种子跑三次取 SSE簇内平方误差和最小的结果。这能大幅降低局部最优的概率代码只多六行效果立竿见影。源码里还有一个容易忽视的地方是labels_与predict的区别predict用的是最近中心映射不是重新聚类所以当你拿训练好的模型去预测新样本时必须直接用同一个centers。3. 手写 kNN 再找差距分类算法源码的分工与缓存模式3.1 距离矩阵的内存账kNN 源码为什么先把样本量算清楚kNN 的核心逻辑简单到一句话找最近的 k 个邻居投票决定类别。但它有个致命问题——如果直接用暴力法建全量距离矩阵n个样本就要开一块n * n的浮点矩阵。n 等于 10000 时这块内存是 800MB多数单机任务直接卡死。所以读 kNN 相关源码时你第一件该看的事不是分类边界而是它怎么处理距离计算。常见做法是把距离矩阵分块计算一次只算 256 或 1024 个测试样本对全部训练样本的距离随算随丢。另一个做法是用 KD-Tree 或 Ball-Tree 做近邻搜索把大量不可能成为近邻的点在树结构里剪掉这样单次查询复杂度从 O(n) 降到 O(log n) 左右。源码的“分工”就在这里分类逻辑只是最后那层投票真正的工程量在索引结构。3.2 用 numpy 写 kNN 近邻搜索的极简版import numpy as np def knn_predict(X_train, y_train, X_test, k3): # 用广播一次算出所有测试样本与训练样本的距离矩阵 dists np.linalg.norm(X_test[:, None, :] - X_train[None, :, :], axis2) # 按距离升序取前 k 个邻居的索引 top_k_idx np.argsort(dists, axis1)[:, :k] top_k_labels y_train[top_k_idx] # 每行做多数投票 preds [] for row in top_k_labels: labels, counts np.unique(row, return_countsTrue) preds.append(labels[counts.argmax()]) return np.array(preds)逻辑说明X_test[:, None, :]形状是(m, 1, d)X_train[None, :, :]形状是(1, n, d)广播后得到(m, n, d)的差张量再在最后一维求范数就得到(m, n)距离矩阵。argsort返回近邻索引然后当作y_train的索引去取标签避免一层层循环。投票部分用np.unique统计票数比Counter在数组处理上更直观。参数说明k是超参数常见取 3 或 5如果类别不平衡多数投票容易被大类别带偏这时可以改成加权投票权重取1 / (d eps)距离越近票越重。还有一个容易被忽略的问题特征没有归一化时量级大的特征会主导距离。比如年龄 0-100、收入 0-100000收入会把年龄完全盖住kNN 退化成一维分类器。3.3 源码级对照sklearn.neighbors 里额外替你处理了什么把 sklearn 里KNeighborsClassifier的源码摊开看你会发现它默认algorithmauto内部会根据数据量决定用 brute 还是 KD-Tree。它还处理了两个手写版没考虑的问题一是当k大于训练样本数时直接抛出异常否则argsort会越界二是权重参数weights默认uniform改成distance就是上面说的加权投票。sklearn 源码里还有个细节叫“缓存最近邻结构”如果先调用了fit再反复kneighbors树结构会被保留多次查询不用重复构建。你自己的代码如果只跑一次预测感觉不到差距一旦放到线上做实时检索这个树结构的复用就是性能的关键。所以手写的意义不是替代 sklearn而是搞清楚它替你藏起来的那些判断。4. 决策树 CART 源码拆解纯度函数与剪枝为什么不能乱抄4.1 信息增益、增益比、基尼系数在源码里如何切换CART 决策树在源码层面的核心就一件事——选特征和阈值让切分后的子节点“纯度”最高。纯度的度量函数有三种常见选择信息熵entropy、基尼系数gini、分类误差率。信息熵对分布变化更敏感倾向于多分叉基尼系数是熵的近似计算里没有对数速度更快所以 sklearn 默认criteriongini。增益比是 C4.5 的改进用来惩罚那些取值特别多的特征但 CART 源码里基本用不到因为 CART 强制是二叉树取值多的特征会被阈值切分天然压住。选纯度函数不是玄学而是速度与敏感度的取舍。同样的数据用 entropy 和 gini 剪出来的树形状经常相似但 gini 在大样本上明显更快。如果特征里含大量离散取值用信息增益比更稳但手写成本更高。我的建议第一版直接用 gini跑通后再切到 entropy 对比效果。4.2 手写一棵最小 CART切分逻辑与叶子标签import numpy as np def gini(y): 计算基尼系数1 - sum(p_i^2)p_i 是第 i 类的占比 _, counts np.unique(y, return_countsTrue) p counts / counts.sum() return 1 - (p ** 2).sum() def best_split(X, y): 遍历所有特征的所有取值选加权基尼最小的切分点 best_gini gini(y) best None n len(y) for col in range(X.shape[1]): values np.unique(X[:, col]) for v in values[:-1]: # 阈值取相邻两个值的中点 mask X[:, col] v if mask.sum() 0 or (~mask).sum() 0: continue left_gini gini(y[mask]) right_gini gini(y[~mask]) weighted (mask.sum() * left_gini (~mask).sum() * right_gini) / n if weighted best_gini: best_gini weighted best (col, v, weighted) return best逻辑说明这段代码只负责找“当前节点最优切分”是 CART 源码里最核心的搜索逻辑。外层循环遍历每个特征内层循环遍历该特征的每个候选阈值然后按和分成左右两个子集。gini(y[mask])计算左子集的基尼gini(y[~mask])计算右子集最后按样本量加权求和。只要加权基尼比父节点小就更新最优切分。参数说明阈值只取在values[:-1]意思是每个唯一取值当作切分候选真正切分点是这个值和下一个值的中间位置。这种做法的好处是把连续特征的搜索复杂度从 O(n^2) 压到 O(m log m)m 是唯一值个数。空子集要被跳过否则会出现除零。递归停止条件需要在外面自己写深度到了、叶子纯度是 1、样本数少于阈值三个条件满足其一就返回多数类别。4.3 sklearn 决策树的 pre-pruning 参数怎么映射到源码读DecisionTreeClassifier源码时fit里很长一段是预剪枝参数的处理。max_depth控制最大深度源码里每递归一层深度计数器加一到了上限就强制返回叶子min_samples_split是节点最少样本数低于这个数就不再尝试切分min_samples_leaf是叶子至少样本数它比前者更严格因为它要求切完后左右两边都达标。这三个参数你最好全部显式设置别只依赖默认值。默认的max_depthNone才是真正的黑匣子——树会一直长到所有叶子纯净训练集上接近满分测试集严重过拟合。我一般从max_depth4起步配合min_samples_leaf5效果比“先不剪枝再事后剪枝”更容易控制。sklearn 还提供ccp_alpha做代价复杂度剪枝那是事后剪枝路线代码上比预剪枝复杂一个量级不建议新手第一版就上。5. 避坑手写十大算法源码时最容易翻车的五类现场5.1 现象归一化边界在预测时弄丢了训练时对特征做了(X - mean) / std归一化模型离线评估分数漂亮上线之后预测结果却烂得离谱。原因你计算mean和std用的对象在训练结束后被销毁了预测新样本时如果只对测试集单独归一化数据分布对不上模型期望。解决把标准化器保存成独立对象和模型一起序列化。比如在 sklearn 里就用Pipeline包起来fit训练时它记住了均值方差predict时自动用同一套参数转换。手写代码时至少写一个类把训练集的均值方差存成属性。5.2 现象随机种子不固定导致复现性翻车同一份数据、同一段代码今天跑的结果和昨天对不上。原因你只在最外层调了np.random.seed(42)但 k-means、决策树、测试集切分都各自消费随机数调用顺序一变结果就漂了或者你用的是全局随机数生成器被其他模块悄悄消耗了。解决每个组件使用独立的RandomState或default_rng实例不要依赖全局种子。训练测试切分、算法内部初始化、交叉验证折叠各自的随机源互相隔离。这样即使别人在你的代码前面多加了一段随机操作后面结果也不会变。5.3 现象朴素贝叶斯零概率把整体结果带崩朴素贝叶斯在计算条件概率连乘时如果测试样本里某个特征取值在训练集中没出现过概率直接为 0整体乘积归零后验概率变成nan或 0预测全废。原因没做平滑处理。零概率不是“稀有”的体现而是训练样本不足不该得到绝对否定。解决加拉普拉斯平滑也就是在分子加 1、分母加类别数更工程的做法是把概率转换到对数空间用求和代替连乘避免浮点下溢。平滑系数 alpha 不是可选项哪怕设成 0.01也比不设强。5.4 现象类别型特征直接塞进欧氏距离计算kNN 或 k-means 在特征里混有“地区编码”这种离散值时会翻车。原因离散值 0、1、2 自带顺序感模型会认为类别 1 比类别 2 离类别 0 更近但实际类别之间没有这种度量关系。按one-hot编码后类别间距离变成汉明距离这是对的如果不做处理直接跑距离算法特征越细错误越明显。解决类别型特征要么做 one-hot要么换成支持混合距离的度量方式。注意 one-hot 会拉高特征维度对决策树反而有害所以这个坑更多集中在 k-means、kNN、SVM 这些依赖距离的算法上。5.5 现象数据切分顺序造成的信息泄漏交叉验证得分高得离谱但换到真实测试集上效果马上缩水。原因在切分训练集和测试集之前你就对全量数据做了标准化、缺失值填充或特征筛选。测试集的信息混进了训练集的统计量等于你提前“偷看”了答案。解决先切分再做任何需要拟合的变换。标准化的均值方差只能在训练集上计算再用它转测试集。缺失值填充也一样用训练集的众数或中位数填测试集。这个坑在源码里看不见但一旦踩进去后面所有算法的评估结果都要重算。6. 验证收尾10 行代码把挖掘效果钉死再把这份源码笔记沉淀下来from sklearn.model_selection import cross_val_score from sklearn.tree import DecisionTreeClassifier # 5 折交叉验证每次都会重新 fit避免单次拆分运气 scores cross_val_score( DecisionTreeClassifier(max_depth4, min_samples_leaf5, random_state42), X, y, cv5, scoringf1_macro ) print(scores.mean(), scores.std())逻辑说明cross_val_score会自动完成 5 次切分、训练、预测、打分。重点看两点scores.mean()是平均效果scores.std()是波动幅度。平均值高但标准差也高的模型说明结果依赖特定数据切分不稳标准差低才是值得往下走的方向。我自己的习惯是每次重写一个算法都同时留三样东西可运行的极简代码、可复现的固定种子、一份记录参数实验的 markdown 笔记。所谓“数据挖掘十大算法源代码Python”真正值钱的地方不是把十个算法都抄一遍而是在抄完 k-means、kNN、CART 之后你能开始改源码、加约束、调参数面对新业务时能判断哪个算法有什么边界。剩余五个算法里Apriori 要重点看频繁项集的剪枝逻辑SVM 要重点看核函数和松弛变量EM 要重点看对数似然的上界推导AdaBoost 和 PageRank 的源码反而短核心分别是权重更新和迭代收敛。希望这份实打实的手写顺序能帮你少走弯路希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网