
简介在数据分析和机器学习建模中样本集划分是基础却关键的环节。SXPY 思路结合 SPXY 划分法与蒙特卡罗交叉验证为不均衡数据、复杂特征结构下的训练集构建提供了灵活方案可广泛用于近红外光谱分析、化学计量学等领域的建模任务适合有一定基础、希望提升模型稳健性的学习者。压缩包共 4 个文件以 MATLAB 脚本为主并附一篇方法文献整体仅 291KB轻量易用。已有 1826 人学习下载热度可观。脚本分别实现了 SPXY 样本划分、KS 统计检验以及随机抽样交叉验证配合文献可对照理解各方法原理与参数设置通过多次随机抽样能有效降低单次划分带来的偏差也可直接迁移到橘叶橙皮苷含量测定等实际项目中有助于优化模型精度与泛化能力。1. sxpy方法进行样本集划分先搞懂它在解决什么问题做近红外光谱建模时我吃过一次大亏训练集和验证集按随机方式划分模型在训练集上 R² 到 0.98验证集一测掉到 0.82。问题不在模型而在样本划分——训练集和验证集的特征空间重叠太少。后来把划分方式换成 sxpy常见写法是 SPXY即基于联合 X-Y 距离的样本集划分同样模型、同样数据验证集误差降了一半。这种方法的思路很直接不让验证集“捡漏”而是从特征空间和响应空间两个维度把所有离群样本、边界样本尽量选进验证集让模型在真正困难的样本上接受检验。这套方法尤其适合样本量不大、变量相关性强的场景比如光谱分析、过程分析、小样本回归建模。相比随机划分和只考虑 X 空间的 Kennard-Stone 划分sxpy 多考虑了 Y 值的影响选出来的验证集更有代表性。这篇文章会把原理、可复现的 Python 实现、参数设置和踩坑点全部讲透照着做就能用在自己的数据上。2. 为什么弃用随机划分和 Kennard-Stonesxpy 的选样逻辑2.1 随机划分的最大问题分布不均匀时验证集失真随机划分通常用 train_test_split 完成看似公平实际上它只保证了“数量比例”正确完全不管样本空间覆盖。如果样本在特征空间里分布不均匀比如大多数样本集中在某个区间随机划分很可能让验证集全部落在高密度区而低密度区的样本全进训练集。模型没见过验证集所在区域的边界一上线就翻车。另一个问题是小样本场景下随机性被放大。样本总量只有 300验证集占 25%随机划分一次和另一次的结果可能差异巨大模型评估指标跟着大幅波动。这个现象在光谱建模里尤其明显因为光谱数据经常是几十个样本到几百个样本的小规模数据集。某开发者做过一次对比试验同一批油脂近红外数据随机划分 50 次验证集 RMSE 的波动范围接近 30%而用 sxpy 划分 20 次结果几乎稳定。随机划分不是不能用而是不适合小样本和分布不均衡的数据。2.2 Kennard-Stone 的贡献与局限只覆盖 X 空间Kennard-Stone 方法简称 KS是经典的空间覆盖选样法核心思想是让选出来的验证集样本尽量互相远离覆盖整个特征空间。它的做法是先找出欧氏距离最远的两个样本作为初始点然后迭代地从未选样本中挑选与已选集合最小距离最大的那个样本加入集合。KS 比随机划分可靠的多很多光谱建模项目都用它。但它有个明显盲区只看 X特征距离完全不看 Y响应值。在回归问题里两个样本特征非常相似但 Y 值差异很大——比如水分含量都是 10% 附近的土壤光谱有机质含量一个是 1% 一个是 5%——按 KS 划分这两个样本很容易被分到同一侧验证集就丢了高值段的代表性。这也是为什么后来会有人在 KS 基础上引入 Y 距离把选样逻辑从“空间覆盖”升级成“联合覆盖”这就是 sxpy。2.3 sxpy 的核心联合 X-Y 距离怎么算sxpy 的改进非常朴素就是把 KS 里的欧氏距离换成了联合距离。计算分三步第一步分别计算样本之间在 X 空间的欧氏距离 dx 和在 Y 空间的欧氏距离 dy。第二步将两个距离矩阵各自除以最大值做归一化让 X 和 Y 对选样的贡献处在同一量级。第三步将归一化后的 dx 和 dy 相加得到联合距离 dxy然后套用 KS 的迭代选样逻辑每次从未选样本中挑选与已选集合最小联合距离最大的样本加入集合。用公式表达就是 dxy(i,j) dx(i,j)/max(dx) dy(i,j)/max(dy)。这个公式看起来很普通但它解决了一个实际问题如果直接用原始距离相加X 有几百个变量而 Y 只有一个值dx 的数值天然会比 dy 大几个量级Y 对划分结果的影响会被彻底淹没。除以各自最大值之后X 和 Y 的贡献被拉平Y 空间的差异才能左右选样。从选样逻辑上理解sxpy 等价于“优先把难以预测的样本放进验证集”。Y 偏离主流越远、特征越特殊的样本联合距离越大越容易被选到验证集。这样训练集相对“简单”、验证集相对“困难”模型评估结果更接近真实上线表现而不是一个被随机划分粉饰过的假分数。3. 从零实现 sxpyPython 代码与逐行说明3.1 准备数据归一化与距离矩阵计算先把数据准备好。我习惯把样本划分写成一个独立函数避免在建模脚本里反复改动。下面的代码用 numpy 和 scipy 完成scipy 的 cdist 可以直接算两两之间的距离矩阵比自己写双重循环快不少。import numpy as np from scipy.spatial.distance import cdist from sklearn.preprocessing import StandardScaler def sxpy_split(X, y, test_size0.3, random_state42): 基于联合X-Y距离的样本集划分 X: ndarray, 形状 (n_samples, n_features) y: ndarray, 形状 (n_samples,) test_size: 验证集比例或数量 random_state: 虽然sxpy本身不依赖随机种子但归一化或预处理可能涉及 返回: train_idx, test_idx X np.asarray(X, dtypefloat) y np.asarray(y, dtypefloat).reshape(-1, 1) # 对X做标准化防止量纲差异影响距离计算 scaler_x StandardScaler() X_scaled scaler_x.fit_transform(X) # y同样做标准化 scaler_y StandardScaler() y_scaled scaler_y.fit_transform(y)这里有两个关键点要说明。X 标准化用的是 StandardScaler即 z-score 标准化让每个特征均值为 0、方差为 1。原因是光谱数据不同波数的吸光度尺度接近但其他类型数据可能出现一个特征在 0 到 1 之间、另一个特征在 1000 到 10000 之间的极端情况不标准化的话距离计算会被大尺度特征主宰。y 也做标准化不是必需的但建议做原因后面在避坑部分细说。3.2 核心选样循环基于联合距离迭代添加样本接下来是最核心的部分计算距离矩阵、归一化、联合然后迭代选样。这段逻辑就是 sxpy 的灵魂。# 计算X空间和Y空间的欧氏距离矩阵 dx cdist(X_scaled, X_scaled, metriceuclidean) dy cdist(y_scaled, y_scaled, metriceuclidean) # 分别除以自身最大值归一化到类似量级 dx_norm dx / dx.max() dy_norm dy / dy.max() # 联合距离X距离与Y距离相加 dxy dx_norm dy_norm # 将距离矩阵的对角线设为无穷大避免样本自己选自己 np.fill_diagonal(dxy, np.inf) # 初始化验证集索引列表先找联合距离最大的两个样本 n_samples X.shape[0] max_idx np.unravel_index(np.argmax(dxy), dxy.shape) selected [max_idx[0], max_idx[1]] # 决定验证集大小 if isinstance(test_size, float): n_test max(1, int(n_samples * test_size)) else: n_test int(test_size) # 迭代选样每次从剩余样本中选与已选集合最小距离最大的样本 while len(selected) n_test: remaining [i for i in range(n_samples) if i not in selected] # 对每个剩余样本计算它到已选集合的最小联合距离 min_dist_to_selected [] for i in remaining: dists dxy[i, selected] min_dist_to_selected.append(dists.min()) # 挑选最小距离最大的那个样本 next_sample remaining[int(np.argmax(min_dist_to_selected))] selected.append(next_sample) test_idx np.array(selected) train_idx np.array([i for i in range(n_samples) if i not in test_idx]) return train_idx, test_idx选样循环的核心可以用一句话概括让新加入验证集的样本尽量远离已经选中的所有样本。这里的“远离”同时体现 X 和 Y 两个维度。代码里对每个剩余样本计算它到已选集合的最小联合距离然后选择最小距离最大的那个样本。这就是 max-min 选样策略和 KS 一致只是距离换成了联合距离。需要重点说一下初始点的选择。代码中没有随机初始化而是直接选择联合距离最大的两个样本作为起点。这是 sxpy 相对稳定的关键原因之一。如果随机选起点每一次划分结果都会不同而且很容易把两个非常接近的样本作为起点后面的选样路径就走偏了。固定选择距离最远的两个样本作起点整个划分过程是确定性的数据不变、结果不变任何人复现都会得到同样的划分。3.3 一个完整可跑的示例场景直接贴一段在真实数据上的调用方式方便理解整个流程。继续上面的函数定义加载数据、调用划分、打印结果统计。# 假设已经读取了光谱数据和对应的浓度值 # 这里用随机数据模拟一个光谱数据集 rng np.random.default_rng(42) X rng.random((200, 120)) # 200个样本120个波数点 y rng.random(200) * 10 # 目标变量比如浓度值 train_idx, test_idx sxpy_split(X, y, test_size0.3) print(f训练集样本数: {len(train_idx)}, 验证集样本数: {len(test_idx)}) print(f训练集y范围: {y[train_idx].min():.2f} ~ {y[train_idx].max():.2f}) print(f验证集y范围: {y[test_idx].min():.2f} ~ {y[test_idx].max():.2f})从输出里通常会看到一个现象验证集的 y 范围几乎覆盖训练集的 y 范围甚至可能更大。这是 sxpy 希望在 Y 空间覆盖全局的直接体现。如果随机划分验证集的 y 范围大概率只是训练集 y 范围的一个子集而 sxpy 会把最小值和最大值的样本都尽量选进验证集保证模型在新数据上遇到的极端值在训练阶段已经见识过类似的样本分布。另外test_size 参数既支持 float 也支持 int。传 0.3 表示取 30% 样本作验证集传 60 表示直接取 60 个样本。我一般推荐固定数量而不是固定比例因为小样本数据集里 30% 可能只有 20 个样本指标波动会很大而固定数量更容易控制最终验证集的样本规模。4. sxpy 实战避坑5 个影响划分质量的常见问题4.1 坑 1特征量纲不一致联合距离形同虚设现象X 标准化做了但没做彻底或者只对部分特征做了处理。划分出来的验证集在特征空间上看起来分布均匀模型表现却依然不稳定。原因sxpy 的距离计算依赖欧氏距离。如果某个特征数值范围是 0 到 10000而其他特征都在 0 到 1 之间前者对距离的贡献会占据绝对主导。联合距离实际上等于这个特征的距离其他特征和 Y 的影响全被淹没。光谱数据常见的是所有波数吸光度尺度接近不容易出这种问题但多源数据拼接、手工构造特征后非常容易出现。解决对所有特征统一做标准化。推荐用 StandardScaler 做 z-score 标准化而不是 MinMaxScaler。因为 MinMaxScaler 对异常值非常敏感一个噪声点就可能把其他所有特征压缩到一个很窄的范围距离信息同样被破坏。z-score 对离群值更稳健是这类距离选样方法更可靠的搭档。4.2 坑 2Y 值的量纲和 X 维度不匹配联合距离退化成 KS现象用 sxpy 做了划分但验证集 Y 值范围延伸得并不明显结果看起来和 KS 差不多。原因X 通常有几十上百个特征Y 只有一个标量。如果不做归一化直接相加dx 的数值天然远大于 dy联合距离几乎等于 dxY 的影响被稀释得无足轻重。这种情况等于在跑一个普通的 KS 划分。解决对 X 和 Y 分别做标准化后再计算距离矩阵。严格来说应该用每个样本本身的所有 X 坐标计算 dx对全部样本的 y 值计算 dy然后各自除以最大值。这样 X 和 Y 的贡献被强制拉平到同一量级Y 对选样的影响才能真正打进划分结果里。这也是 sxpy 最核心的细节取舍。4.3 坑 3样本少时初始点选择不当划分结果剧烈抖动现象同样的数据什么参数都没改连续跑两次 sxpy得到的训练集和验证集完全不同模型指标也忽高忽低。原因如果你的代码里初始点是随机挑选两个样本或者使用 sklearn 里的 train_test_split 做内部随机化那么拿到的是随机初始化的 sxpy 变体。在样本量只有 100 左右时不同的起点会导致后续 max-min 选样路径分叉最终结果差异巨大。解决把初始点固定为联合距离最大的两个样本。这一步是确定性的不引入任何随机数。数据不变结果就不会变。如果你希望得到多组划分做集成验证不要随机化初始点而是考虑对 X 或 y 加微小扰动或者直接调整 test_size 比例来获取不同规模的验证集。固定初始点之后每次划分都是一致的排除了随机性对模型评估的干扰。4.4 坑 4验证集比例设得太小模型评估失去参考意义现象验证集只有 15 个样本模型在验证集上的误差很大且波动剧烈甚至换一个模型结构误差就不一样。有人开始怀疑 sxpy 不好用。原因sxpy 的选样策略决定了它偏爱把边界样本和极值样本选进验证集。如果验证集比例太小比如总样本 200 个只选 5%那么这 10 个样本很可能全是极端值模型在这些点上的表现自然很差。这不是 sxpy 的缺陷而是验证集数量太少的必然结果。解决在样本量允许的情况下验证集比例建议设在 15% 到 30% 之间。如果样本总量只有 80那就设 25% 到 30%保证验证集至少有 20 个以上样本。如果样本总量过少比如低于 50 个单纯划分带来的评估噪音太大更推荐用留一交叉验证或重复划分取平均值来评估模型而不是期待单次划分给出稳定结论。4.5 坑 5写代码时忘了固定随机种子别人无法复现你的结果现象你自己跑出的划分结果和同事在同一份数据上复现的结果完全不一致。模型报告中的误差指标无法被核对。原因代码里用了随机采样的函数但没有指定 random_state或者你在数据预处理阶段用了随机数据增强。任何一个环节的随机性都会传导到最终的样本划分。解决在 sxpy 函数入口增加 random_state 参数。尽管 sxpy 本身不依赖随机数但数据预处理阶段如果涉及采样、打乱都需要指定随机种子。调用时固定 random_state42 这样的值并把划分得到的 train_idx 和 test_idx 保存成文件后续建模、调参、出报告都用这组索引确保实验可复现。这个习惯越早养成越好。5. 大批量样本下的 sxpy加速、分批与替代方案5.1 为什么说 O(n²) 复杂度在大样本下会变得难以接受sxpy 有两个明显的计算瓶颈。第一步是计算全部样本之间的两两距离形成一个 n×n 的距离矩阵这里的内存和计算量都是 O(n²)。第二步是迭代选样循环剩余样本数量为 n验证集大小是 m每一步都要对所有剩余样本扫描距离矩阵的对应行总的计算开销大约是 O(n×m)加上每次更新已选集合的 min 操作整体效率不高。当 n 到 5000 时5000×5000 的距离矩阵在内存里占 2500 万行的浮点数大约 200MB勉强能跑但已经开始卡顿n 到 20000 时需要 3.2GB 内存普通机器根本跑不动。光谱建模一般样本量在几百到几千之间问题不大但如果做在线学习样本挑选或者大规模数据预清洗就要考虑优化。5.2 一个实用的分批策略先聚类再局部 sxpy我的常用做法是先用 KMeans 把大样本量数据切成若干个簇然后按簇内样本比例分配验证集名额在每个簇内分别调用 sxpy最后把各簇的验证集合并。这么做有三个好处距离矩阵从整个数据集的 O(n²) 降成每个簇内的 O(cluster_size²)计算量大幅下降每个簇的样本特征相对集中sxpy 在簇内选出的样本覆盖度更有针对性合并且验证集后整体验证集在特征空间里依然保持分布均衡。下面是一个搭配 sklearn 的参考实现只展示分簇和合并的核心逻辑from sklearn.cluster import KMeans def sxpy_split_batched(X, y, test_size0.2, n_clusters4, random_state42): n_samples X.shape[0] # 先聚类把大样本切成多个簇 km KMeans(n_clustersn_clusters, random_staterandom_state, n_init10) labels km.fit_predict(X) train_list [] test_list [] total_test int(n_samples * test_size) # 按每个簇的样本量占比分配验证集名额 for cluster_id in range(n_clusters): idx_in_cluster np.where(labels cluster_id)[0] cluster_size len(idx_in_cluster) if cluster_size 2: continue cluster_test_size max(1, int(round(total_test * cluster_size / n_samples))) X_cluster X[idx_in_cluster] y_cluster y[idx_in_cluster] tr, te sxpy_split(X_cluster, y_cluster, test_sizecluster_test_size, random_staterandom_state) train_list.extend(idx_in_cluster[tr]) test_list.extend(idx_in_cluster[te]) return np.array(train_list), np.array(test_list)这个版本的优点是大样本下依然可控缺点是如果某个簇的样本量很小比如只有 2 个sxpy 在里面无法挑选出足够有代表性的验证集。所以设置 n_clusters 时要保证平均每个簇不少于 50 个样本否则验证集质量反而比直接全量 sxpy 更差。5.3 如果说服不了别人用 sxpy用 KS 和随机划分的对比数据说话实际工作中常听到的质疑是“sxpy 是不是比随机划分好”。与其争辩不如跑三组对比对同一份数据分别用随机划分、KS 划分、sxpy 划分固定同一个模型和同一组超参比较训练集和验证集的误差差异。通常你会看到两个现象。第一sxpy 的验证集误差普遍高于随机划分的验证集误差因为验证集里被放进了更多难预测样本。第二sxpy 划分下训练集和验证集的误差差距更小说明模型没有因为划分侥幸而“虚高”。这种对比结果是说服同事切换划分方式最直接的材料。如果随机划分验证集误差就低得离谱而 sxpy 验证集误差高不少那恰恰说明随机划分的结果不可信模型并没有真正泛化到困难样本上。6. 怎么验证 sxpy 划分好不好可视化与误差对比6.1 用 PCA 把划分结果画出来直观观察覆盖度划分完成后不要急着建模先把训练集和验证集在降维空间里的分布画出来。用 PCA 降到二维或三维用两种颜色区分训练集和验证集。好的 sxpy 划分通常是验证集样本散布在训练集样本的外围和间隙处而不是集中在某一片区域。如果验证集样本大多聚集在角落或者只在某个方向上延伸要回头检查数据预处理和初始点选择。这一步只需要几十行代码但对判断划分质量非常有帮助。6.2 同一个模型多组划分下误差稳定才是硬道理我通常做三次 sxpy 划分分别用不同 test_size比如 0.2、0.25、0.3然后训练同一模型。如果验证集 RMSE 变化在合理范围内说明划分是稳定的如果变化剧烈说明样本本身存在很强的分布不均单次划分的参考价值有限需要结合交叉验证或多次划分取平均。这套流程同样适用于报告模型性能远比只贴一组划分结果可信。6.3 坚持一个划分习惯每次建模先保存索引文件我之前吃过一次亏某次建模时临时改了一个参数重新跑了划分结果模型的评估指标全变了排查了半天才发现是样本划分不同导致。从那以后我把 train_idx 和 test_idx 总是先存成 .npy 文件后续所有实验都从文件里读取同一组索引。模型可以调数据可以换但样本划分一旦固定就不要再随意改动否则所有实验结果之间失去可比性。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取