
简介十大经典算法的Python实现被浓缩为15个轻量文件。这份源码包面向数据挖掘初学者与进阶开发者覆盖Apriori、C4.5、CART、EM、K-means、KNN、PageRank等核心算法便于对照算法原理快速上手。压缩包容量仅14KB内含7个.py脚本、4个xml配置、2个testset测试数据及md说明文档可直接运行并观察输出结果。目前已有1149人学习是轻量但完整的算法练习包。通过阅读源码并结合测试集可清晰理解频繁项集挖掘、决策树分裂、聚类迭代、图排序等关键步骤每个脚本结构独立适合按需摘取、二次改造或嵌入自身数据挖掘流程对掌握Python实现细节和调参思路很有帮助。1. 数据挖掘十大算法源代码Python讲的是什么一套能复现、能改、能答辩的经典实现数据挖掘十大算法源代码Python这个标题对应的是数据挖掘领域最经典的一套入门全家桶2006 年 ICDM 评选出的十个算法覆盖分类、聚类、关联规则、图算法和集成学习。常见面试场合算法工程师会被要求不仅能说出这些算法的名字还能当场写出能跑的 Python 实现。这篇文章把这十个算法按用途分组给出手写核心逻辑和 scikit-learn 组合写法并记录了我复现过程中踩过的大部分坑。适合准备面试的算法工程师、开题阶段的研究生以及想在公司内部快速搭一个基线模型做验证的从业者。照着复制就能跑读完还能知道每个参数在控制什么。2. 把十大算法在本地跑通环境准备与十个脚本的统一骨架2.1 先用 pip 把依赖装齐numpy、pandas、scikit-learn 一个都不能少我的习惯是先用虚拟环境把项目隔离起来避免把系统的 Python 弄乱尤其是机器上还跑着别的深度学习框架时。python 安装教程里最容易被忽略的一步是 pip 源切换不换源的话在国内网络环境下装 scikit-learn 能等上好几分钟还经常超时。下面是完整的环境初始化命令python -m venv dm_env source dm_env/bin/activate pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy pandas scikit-learn matplotlib说明python -m venv dm_env创建名为 dm_env 的虚拟环境Windows 下激活命令是dm_env\Scripts\activateLinux 和 macOS 用source dm_env/bin/activate。-i参数指定清华源只影响下载速度不影响代码运行结果。numpy 提供数组运算pandas 负责读表和清洗scikit-learn 提供 SVM、KNN、决策树、AdaBoost、高斯混合模型这些成熟实现matplotlib 在最后画聚类散点图和收敛曲线用。装完先验证版本确认环境没问题再往下走python -c import numpy, pandas, sklearn; print(numpy.__version__, pandas.__version__, sklearn.__version__)能把三个版本号打出来就说明环境这一步过关了。常见的翻车点不是算法本身而是 pip 版本太旧导致装包失败执行pip install --upgrade pip能解决九成安装问题。另外不要图省事把依赖装到系统 Python 里后面你换项目、换 Python 小版本时会有数不清的隐藏冲突。2.2 十个脚本怎么组织输入数据、模型逻辑和评估指标分开十大算法里五个是分类两个是聚类一个是关联规则一个是图算法一个是集成学习。如果全部堆在一个文件里调试时找函数都是一场灾难。我采用下面这种目录结构每个算法独立成文件评估入口单独放dm-algorithms/ ├── data/ # 存放数据集 │ ├── iris.csv │ └── market_basket.csv ├── algorithms/ │ ├── c45.py │ ├── cart.py │ ├── svm.py │ ├── knn.py │ ├── naive_bayes.py │ ├── kmeans.py │ ├── em.py │ ├── apriori.py │ ├── pagerank.py │ └── adaboost.py ├── evaluate.py # 统一评估入口 └── README.md这个结构的第一个好处是出问题时不用翻整个项目哪个算法报错就改哪个文件第二个好处是面试或答辩时你可以顺着文件名逐个讲逻辑非常清晰。评估指标统一放在 evaluate.py 里分类算法看准确率、精确率和召回率聚类看轮廓系数关联规则看支持度和置信度PageRank 看迭代收敛曲线。数据集默认用鸢尾花 iris只有 150 条样本、4 个特征、3 个类别除了 Apriori 要换成购物篮数据其他九个算法可以直接复用。下面这张表是我把十个算法分好组的对照表后面几章就按这个分组展开算法类型核心实现本文对应章节C4.5分类决策树手写信息增益比 sklearn 决策树3.1CART分类/回归树sklearn DecisionTreeRegressor3.2SVM分类/回归sklearn SVC 标准化管道3.3KNN分类sklearn KNeighborsClassifier3.4Naive Bayes分类sklearn GaussianNB3.4K-Means聚类手写 numpy 实现4.1Apriori关联规则手写频繁项集扫描4.2EM聚类sklearn GaussianMixture4.3PageRank图算法手写迭代求解4.4AdaBoost集成学习sklearn AdaBoostClassifier4.4最后再强调一点所有分类算法都暴露fit(X_train, y_train)和predict(X_test)两个方法这个约定让 evaluate.py 能写出非常短且通用的一键验证脚本。后面章节的所有实现都服从这个接口。3. 分类与回归家族C4.5、CART、SVM、KNN、朴素贝叶斯的 Python 实现3.1 C4.5 核心是信息增益比手写分裂点选择C4.5 和 ID3 的最大区别是用信息增益比代替信息增益。ID3 用信息增益选特征时天然偏爱取值很多的属性“客户ID”这种每个值只有一个样本的特征会拿到最大增益但完全没有泛化能力。C4.5 把增益除以分裂信息做惩罚照顾了这类情况。手写核心只需要三步算熵、算条件熵、算增益比。import numpy as np def entropy(y): _, counts np.unique(y, return_countsTrue) probs counts / counts.sum() return -np.sum(probs * np.log2(probs)) def info_gain_ratio(X_feature, y): base_entropy entropy(y) values, counts np.unique(X_feature, return_countsTrue) total counts.sum() weighted_entropy 0.0 split_info 0.0 for v, cnt in zip(values, counts): subset y[X_feature v] weighted_entropy (cnt / total) * entropy(subset) if cnt 0: split_info - (cnt / total) * np.log2(cnt / total) gain base_entropy - weighted_entropy # split_info 为 0 说明该特征只有一个取值没有分裂价值 return gain / split_info if split_info 0 else 0.0 def best_split(X, y): best_gain 0.0 best_feature None for i in range(X.shape[1]): gain info_gain_ratio(X[:, i], y) if gain best_gain: best_gain gain best_feature i return best_feature, best_gain逻辑说明entropy用类别频率算香农熵info_gain_ratio先按特征取值把数据集切块算加权熵再用分裂信息归一化best_split遍历所有特征选出增益比最大的那个。这是决策树生长最耗时的部分也是面试官最喜欢追问的部分。后面接完整的 sklearn 决策树时只需把数据喂给DecisionTreeClassifier(criterionentropy)。criterion可以选 entropy 或 giniC4.5 风格对应前者CART 默认用 gini。max_depth默认 None 会让树长到所有叶子纯净为止实战里限制到 58 层泛化更好min_samples_split默认 2数据噪声大时调到 5 或 10。需要说明的是 sklearn 没有直接提供 C4.5 实现所以手写核心、用库跑全流程是业界最常见的做法。3.2 CART 回归与剪枝用决策树做房价预测的参考代码CART 和 C4.5 最大的不同是它生成严格的二叉树每个节点只做一次“特征 阈值”的二分既能分类又能回归。GDP 预测、量化策略里的价格序列预测这类任务CART 回归是最常用的基线之一。这里直接用 sklearn 的DecisionTreeRegressor重点看剪枝参数怎么设。from sklearn.tree import DecisionTreeRegressor from sklearn.model_selection import train_test_split import pandas as pd df pd.read_csv(data/house_sales.csv) X df[[sqft_living, bedrooms, bathrooms, floors]] y df[price] # random_state 固定结果才可复现 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model DecisionTreeRegressor(max_depth5, min_samples_leaf10, random_state42) model.fit(X_train, y_train) print(R^2:, model.score(X_test, y_test))逻辑说明max_depth限制树的高度min_samples_leaf要求叶子节点至少 10 个样本这两者合起来相当于做了后剪枝。如果不设限制CART 会把训练集切到每个叶子只有一个样本训练集表现近乎满分测试集一塌糊涂这是决策树最常见的翻车场景。参数说明max_depth从 3 开始往上加每加一层看测试集 R^2 的变化涨不动就停。min_samples_leaf官方默认是 1回归任务建议设成样本总数的 1% 左右。判断是否过拟合直接比较训练集和测试集 R^2 的差值差值超过 0.1 就该加大min_samples_leaf或减小max_depth。另外random_state42这种写法不是玄学它保证你反复运行得到同一棵结构相同的树方便和同事对齐结果。3.3 SVM 的核函数不是玄学RBF 的两个参数到底在干什么SVM 在分类任务里出场率极高但我见过太多实现失败案例原因不是算法复杂度而是忘了特征标准化。SVM 的距离计算高度依赖特征尺度一个人的身高和体重数值范围不一致时距离会被大数值的分量主导模型等于只看到了体重。所以正确姿势是把StandardScaler和SVC一起放进管道。from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline X [[170, 70], [160, 50], [180, 80], [155, 45]] y [0, 1, 0, 1] # 示意标签实际请替换成真实数据 model make_pipeline(StandardScaler(), SVC(C1.0, gammascale, kernelrbf, random_state42)) model.fit(X, y)逻辑说明make_pipeline会保证 fit 时先标准化再训练predict 时对测试数据也做同样的标准化避免你忘了在预测前再次处理。kernelrbf是默认核函数适合特征和标签呈非线性关系的场景。参数说明C是误分类惩罚系数C 越大模型对错分样本的容忍度越低决策边界越复杂容易过拟合C 越小边界越平滑但可能欠拟合。gamma默认 scale表示按特征数量自动计算为1 / (n_features * X.var())如果用固定值如gamma0.1值越小每个样本的影响范围越大、边界越平滑值越大边界越弯曲。调参顺序建议固定 gamma先把 C 从 0.1 到 100 按对数网格扫一遍再回头微调 gamma。很多人把 SVM 当作黑匣子其实这两个参数的含义就完全解释了它在做什么。3.4 KNN 和朴素贝叶斯两个最“懒”却完全不同的模型KNN 是实例型学习训练阶段不学任何参数只是把样本存下来预测时逐条算距离朴素贝叶斯走概率路线假设特征间相互独立用贝叶斯公式算后验概率。两个模型都适合小样本冷启动但在高维稀疏特征上表现迥异KNN 超过几十维后距离度量基本失效而朴素贝叶斯在文本分类里依然是强基线。这是选型时最该记住的边界。from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import GaussianNB from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split X, y load_iris(return_X_yTrue) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state7) # KNN: n_neighbors 是最核心参数 knn KNeighborsClassifier(n_neighbors5, weightsdistance, p2) knn.fit(X_train, y_train) print(KNN acc:, knn.score(X_test, y_test)) # 高斯朴素贝叶斯适合连续特征 nb GaussianNB() nb.fit(X_train, y_train) print(NB acc:, nb.score(X_test, y_test))逻辑说明KNN 的n_neighbors5表示用 5 个最近邻居投票决定分类weightsdistance表示按距离倒数加权避免大类别样本淹没少数类别。p2是欧氏距离p1是曼哈顿距离。高斯朴素贝叶斯假设每个特征服从高斯分布所以直接适配 iris 这类连续数值特征如果换成文本 TF-IDF 特征需要改用MultinomialNB。参数说明n_neighbors选奇数是为了避免平票iris 这种干净数据集上 5 到 15 差异不大真实噪声数据里 K 值要结合交叉验证调。朴素贝叶斯几乎没有可调参数它更依赖“特征独立”这个假设业务特征之间强相关时会系统性偏差这也决定了它适合做基线而不是最终模型。4. 聚类、关联规则与图算法K-Means、Apriori、EM、PageRank、AdaBoost 的实现要点4.1 手写 K-Means从初始化到收敛的三十行代码K-Means 流程一句话就能说清随机选 K 个中心交替做“分配样本到最近中心”和“重算中心”直到中心不再移动。手写一遍能让你真正理解为什么初始化和 K 的选择那么关键。import numpy as np def kmeans(X, k, max_iter100, tol1e-4): # 随机选 k 个样本作为初始中心 centers X[np.random.choice(len(X), k, replaceFalse)] for _ in range(max_iter): # 分配每个样本归到距离最近的中心 dists np.linalg.norm(X[:, None, :] - centers[None, :, :], axis2) labels np.argmin(dists, axis1) # 更新按簇均值重算中心空簇保留旧中心 new_centers np.zeros_like(centers) for j in range(k): if np.any(labels j): new_centers[j] X[labels j].mean(axis0) else: new_centers[j] centers[j] if np.linalg.norm(new_centers - centers) tol: break centers new_centers return centers, labels逻辑说明np.linalg.norm一次算出所有样本和所有中心的距离矩阵形状是(n_samples, k)argmin得到每个样本最近的簇中心。更新中心时最常见的翻车点是某个簇一个样本都没有X[labels j].mean会直接产生 NaN所以我在这里单独加了空簇判断保留上一轮中心。参数说明max_iter默认 100实际数据往往在 30 轮内收敛。tol1e-4表示中心移动距离小于该值就停止越小越精确但跑得越久。随机初始化导致结果不稳定正式实验里你应该改用 sklearn 的KMeans(initk-means, random_state42)k-means 让初始中心尽量分散能从根源上减少“第一次跑好、第二次跑差”的血泪经验。4.2 Apriori 关联规则支持度、置信度与购物篮分析Apriori 是十大算法里唯一围绕事务型数据设计的。它先找频繁项集再生成关联规则核心思想是“频繁项集的子集也频繁”用这个性质剪掉大量候选。手写时需要注意的是输入数据必须是一行一个购物篮里面是商品集合或编码。from collections import defaultdict def apriori(transactions, min_sup0.01): n len(transactions) # 第一轮统计单个商品的支持度 cnt defaultdict(int) for trans in transactions: for item in trans: cnt[frozenset([item])] 1 freq {itemset for itemset, c in cnt.items() if c min_sup * n} all_itemsets [] k 2 while freq: all_itemsets.extend(freq) # 由长度为 k-1 的频繁项集连接生成长度为 k 的候选 freq_list list(freq) candidates set() for i in range(len(freq_list)): for j in range(i 1, len(freq_list)): merged freq_list[i] | freq_list[j] if len(merged) k: candidates.add(merged) # 扫描事务统计候选支持度 cnt defaultdict(int) for trans in transactions: trans_set frozenset(trans) for cand in candidates: if cand.issubset(trans_set): cnt[cand] 1 freq {itemset for itemset, c in cnt.items() if c min_sup * n} k 1 return all_itemsets逻辑说明代码里用frozenset作为项集的数据结构是因为 set 不能作为 dict 的键。连接步骤只合并长度相同的频繁项集合并后元素个数恰好为 k 才作为候选这保证了候选集不会无限膨胀。内层循环用issubset判断候选是否出现在事务里完整模拟了 Apriori 的逐轮扫描过程。参数说明min_sup是最重要的参数表示商品至少出现在多少比例的订单里才保留。购物篮数据有大量长尾商品min_sup设到 0.001 级别可能输出几十万个频繁项集内存直接撑爆。我的血泪经验是从 0.05 开始试看频繁项集数量是否在可接受范围再逐步下调。关联规则阶段还有第二个阈值min_conf控制“买了 X 的人有多大比例买 Y”一般设在 0.5 左右太低会产生大量无意义规则。4.3 EM 算法用高斯混合模型来理解Expectation 在算什么EM 是十大算法里最抽象的一个只讲公式很难建立体感。实际操作里 EM 最常见的形态是高斯混合模型 GMM。E 步计算每个样本属于每个高斯分布的后验概率M 步用这些概率加权重新估计均值和协方差循环交替就是“猜归属 → 按归属更新参数 → 再猜”的过程。手写一维核心逻辑能把这个循环看清楚import numpy as np def em_step(X, mu1, mu2, sigma1, sigma2, w1): # E 步算每个点来自分布 1 的后验概率 g1 w1 * np.exp(-(X - mu1)**2 / (2 * sigma1**2)) g2 (1 - w1) * np.exp(-(X - mu2)**2 / (2 * sigma2**2)) gamma g1 / (g1 g2) # M 步按后验概率加权更新均值和权重方差更新从简 mu1_new np.sum(gamma * X) / np.sum(gamma) w1_new np.mean(gamma) return mu1_new, w1_new逻辑说明E 步输出的gamma是一个 0 到 1 之间的软标签表示样本属于第 1 个分布的概率M 步用这个软标签加权更新均值和权重。为了保留可读性上面的方差更新被省略了完整实现里 sigma 也要按同样的加权方式一起更新。实际项目里不需要手写完整 GMM用 sklearn 一行接入即可from sklearn.mixture import GaussianMixture X np.concatenate([ np.random.normal(0, 1, (300, 2)), np.random.normal(5, 1.5, (300, 2)) ]) gmm GaussianMixture(n_components2, covariance_typefull, random_state42) gmm.fit(X) print(均值:, gmm.means_) print(权重:, gmm.weights_) labels gmm.predict(X)参数说明n_components对应假设的高斯分布个数可以用 BIC 或 AIC 曲线取拐点确定。covariance_type有 full、tied、diag、spherical 四种full 允许每个簇有自己的完整协方差矩阵拟合能力最强样本量小时用 diag 或 spherical 更稳不容易过拟合。random_state同样要固定否则每次跑出的聚类结果会有细微差异这在做论文实验时很致命。4.4 PageRank 与 AdaBoost两个“迭代”思想一个在图网络一个在集成学习PageRank 通过反复迭代让每个节点的权重收敛到稳定值K-Means、EM 也都是迭代式算法但 PageRank 的迭代对象是图结构。AdaBoost 则是另一种迭代每一轮把被错分的样本权重调高下一个弱分类器重点学习这些样本最后按错误率加权组合。两者都值得手写核心面试追问频率很高。import numpy as np def pagerank(adj, damping0.85, max_iter100, tol1e-6): n adj.shape[0] out_deg adj.sum(axis1).reshape(-1, 1) # 悬挂节点出度为 0除零前先置为 1 out_deg[out_deg 0] 1 pr np.ones(n) / n for _ in range(max_iter): new_pr (1 - damping) / n damping * ((adj / out_deg).T pr) if np.linalg.norm(new_pr - pr, ord1) tol: break pr new_pr return prfrom sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.ensemble import AdaBoostClassifier from sklearn.tree import DecisionTreeClassifier X, y load_iris(return_X_yTrue) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42) model AdaBoostClassifier( estimatorDecisionTreeClassifier(max_depth1), n_estimators50, learning_rate1.0, random_state42 ) model.fit(X_train, y_train) print(AdaBoost acc:, model.score(X_test, y_test))逻辑说明PageRank 的damping0.85是 Page 论文里的经典取值表示用户按链接跳转的概率剩下 0.15 随机跳转到任意节点。悬挂节点一旦出现out_deg为 0转移矩阵出现除零结果直接 NaN这是复现时最容易忽略的细节。AdaBoost 的基学习器用max_depth1的决策树桩因为弱分类器简单Boosting 的提升空间才大一上来就换强学习器第二轮基本就过拟合了。参数说明AdaBoost 的n_estimators控制弱分类器数量50 在 iris 上足够如果你发现训练误差还在下降可以加到 200但要注意观察测试集是否开始变差。learning_rate与n_estimators成反比关系官方建议“减小学习率的同时增大估计器数量”。如果你的场景是对抗样本较多或标签噪声大AdaBoost 对噪声敏感的问题会暴露出来那种情况下换成梯度提升树更稳。5. 复现十大算法源代码时最常遇到的 5 个坑现象、原因与解决5.1 中文数据和路径引发的编码错误现象用 pandas 读 CSV 报UnicodeDecodeError或者在 Windows 控制台打印决策树文字时出现乱码。原因Windows 下 Excel 导出的 CSV 默认是 GBK 编码而 Python 3 默认按 UTF-8 读文件。这是环境相关而非算法本身的问题却最容易卡住刚配好环境的人。解决读文件时显式指定编码最常见做法是pd.read_csv(data/iris.csv, encodingutf-8-sig)utf-8-sig会自动去掉 BOM 头如果明确知道文件是 GBK就换成encodinggbk。代码文件本身统一用 UTF-8 保存并在文件头部加一行# -*- coding: utf-8 -*-作为保险这样在旧编辑器里也不会乱。5.2 numpy 版本迭代导致的废弃接口报错现象运行老代码报AttributeError: module numpy has no attribute float或者出现np.matrix相关警告。原因numpy 1.20 之后移除了np.float、np.int、np.bool这些别名很多十年前博客里的代码直接搬过来就会踩中。解决全局搜索代码中的np.float、np.bool、np.int统一替换成内置类型float、bool、int。另一个做法是把 numpy 固定到 1.19.5但新版本 scikit-learn 会强制要求更高版本的 numpy所以最稳的还是改代码而不是降版本。这也是“数据挖掘十大算法源代码”项目里最常见的复现失败点和算法本身毫无关系。5.3 Apriori 的候选集爆炸把内存撑爆现象跑 Apriori 时程序卡死或者内存占用一路飙升到几十 GB风扇狂转。原因min_sup设置过低导致频繁项集数量指数增长逐轮扫描全量事务又进一步拖慢速度。解决先把min_sup调到 0.02确认频繁项集条数在可控范围再逐步下降。如果业务确实需要挖掘大量长尾规则改用 FP-Growth它只需扫描两遍事务数据库结构上规避了候选集爆炸问题。这是实现层面的取舍不是 Apriori 本身的缺陷面试时讲清楚这一点反而加分。5.4 K-Means 每次运行结果都不一样现象同样的数据、同样的 K跑两次聚类结果不同簇中心顺序也变了评估指标跟着波动。原因随机初始化中心不同加上 sklearn 默认n_init10即使会在多次尝试里取最优随机种子没固定结果依然不可复现。解决在 KMeans 里显式设置random_state并把n_init从默认的 10 提高到 20用更多初始点弥补 k-means 选中心的随机性。论文实验和工程验证都必须固定随机种子否则同事或评审复现不了你的结果这属于文档里不会写但实战里永远存在的坑。5.5 PageRank 迭代不收敛或出现 NaN现象PageRank 跑了几百轮都不收敛或者结果里出现 NaN导致后续排序结果全部无效。原因图里有悬挂节点出度为 0 的节点阻尼因子被调成 1导致转移矩阵没有随机跳转项迭代永远在子图之间循环。解决把出度为零的节点度数设为 1这是上一章代码里已经处理过的地方。damping保持在 0.85 上下。迭代终止条件用一阶范数差sum(|new_pr - pr|)比二阶范数更直观。调阻尼因子时先画一条收敛曲线看每轮差值是否稳定下降如果振荡说明图结构本身有问题优先回头检查悬挂节点和邻接矩阵归一化。6. 把十段代码串成一个能交付的脚本统一接口、一键验证十段代码如果各自各的写法验证时就是一场灾难。我让所有分类算法实现同样的fit(X, y)、predict(X)、score(X, y)接口这样 evaluate.py 就能把它们排成一列直接跑# evaluate.py from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from algorithms import c45, cart, svm, knn, naive_bayes, adaboost X, y load_iris(return_X_yTrue) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42) benchmarks { C4.5: c45.C45Classifier(), CART: cart.CARTClassifier(), SVM: svm.SVMClassifier(), KNN: knn.KNNClassifier(), NaiveBayes: naive_bayes.NBClassifier(), AdaBoost: adaboost.AdaBoostClassifier(), } for name, model in benchmarks.items(): model.fit(X_train, y_train) acc model.score(X_test, y_test) print(f{name:12s} {acc:.4f})逻辑说明这段代码只依赖每个模型统一的score方法具体算法封装在各自的文件里。K-Means 和 GMM 是聚类不走这个脚本用轮廓系数单独验证Apriori 要换数据集PageRank 看收敛曲线。这样就形成了两个验证通道互不干扰。验证聚类算法时我习惯加上时间统计和轮廓系数两项避免只看一个数字import time from sklearn.metrics import silhouette_score start time.perf_counter() labels kmeans_predict(X_scaled, k3) elapsed time.perf_counter() - start score silhouette_score(X_scaled, labels) print(fKMeans silhouette{score:.4f}, time{elapsed:.3f}s)说明X_scaled先做标准化再送进 K-Means否则轮廓系数会被大数值特征带偏。time.perf_counter比time.time精度更高适合这种毫秒级调用。我的个人习惯是每个算法备两份代码一份纯手写版长度控制在 50 行以内用来面试讲原理一份基于 scikit-learn 的精简版用来快速出结果和跟同事对齐。手写版里绝不依赖黑匣子每一行都能说清在算什么精简版里所有random_state都显式写上方便别人复现。很多时候算法本身没问题最后翻车都翻在版本、种子和编码这些小事上提前把这些固定下来等于给自己留了后悔药希望帮到你。本文还有配套的精品资源点击获取