API reference¶
Auto-generated from the source docstrings. Everything below is also importable
directly from the top-level splytters namespace (e.g.
from splytters import cluster_split).
Adversarial splitters¶
adversarial ¶
Adversarial splitting algorithms that minimize train-test similarity.
These methods create "hard" evaluation sets where test samples are dissimilar from training samples, testing model generalization.
cluster_split ¶
cluster_split(embeddings: ArrayLike, train_size: float | int = 0.7, method: str = 'kmeans', n_clusters: int = 10, random_state: int = 42, *, strategy: str = 'size', y: ArrayLike | None = None, cluster_range: tuple[int, int] | None = None, fill_individual: bool = False, fill_anchor: str = 'cluster_centroids', **cluster_kwargs: Any) -> tuple[np.ndarray, np.ndarray]
Split dataset by assigning entire clusters to train or test.
Prevents 'cluster leakage' where similar samples end up on both sides. The
embeddings are clustered (method), then whole clusters are assigned to
train/test according to strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
method
|
str
|
clustering algorithm, 'kmeans' or 'dbscan' |
'kmeans'
|
n_clusters
|
int
|
number of clusters (kmeans only) |
10
|
random_state
|
int
|
for reproducibility |
42
|
strategy
|
str
|
cluster -> train/test assignment policy:
- |
'size'
|
y
|
ArrayLike | None
|
class labels of shape (n_samples,). Required for |
None
|
cluster_range
|
tuple[int, int] | None
|
|
None
|
fill_individual
|
bool
|
|
False
|
fill_anchor
|
str
|
what the fill measures proximity to. The paper's prose and
its released code disagree here, so both are offered:
- |
'cluster_centroids'
|
**cluster_kwargs
|
Any
|
passed to the clustering algorithm |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Raises:
| Type | Description |
|---|---|
ValueError
|
on an unknown |
Warns:
| Type | Description |
|---|---|
UserWarning
|
if whole-cluster assignment would leave train or test empty. The function returns a seeded exact-size sample split because no cluster-coherent non-empty split exists. |
References
The "subset_sum" and "closest" strategies implement SUBSET-SUM-SPLIT
and CLOSEST-SPLIT from Züfle, Dankers & Titov (2023), "Latent Feature-based
Data Splits to Improve Generalisation Evaluation," GenBench Workshop @ EMNLP,
pp. 112-129, https://aclanthology.org/2023.genbench-1.9 . They cluster a
model's hidden representations and assign whole clusters to test to expose
latent-space "blind spots"; their analysis finds that the resulting
difficulty does not correlate with surface-level properties (e.g. length),
complementing the surface-based :mod:splytters.sorters.
Faithful reproduction (matched to the authors' reference code):
SUBSET-SUM-SPLIT solves the multidimensional subset-sum over cluster
class-count vectors exactly by dynamic programming (a greedy fallback
engages only past an internal state cap for large / high-class-count
problems) and completes the remaining per-class shortfall with random
individual examples, so the test set always hits the exact class-balanced
target. CLOSEST-SPLIT seeds the cluster farthest from the centroid of all
centroids, grows the pocket by single-linkage nearest neighbour, then
(with fill_individual=True) fills to the exact target with the
individual examples closest to the fill_anchor. On the fill anchor
the paper and its released code diverge: the paper's prose says examples
"closest to one of the test set centroids" (the default,
fill_anchor="cluster_centroids"), while the released code measures
distance to the mean of all current test embeddings
(fill_anchor="test_mean"). To reproduce the paper's cluster-count
search, pass cluster_range=(3, 50): it keeps the k requiring the
fewest individually-added examples (Section "Splitting methods").
subset_sum enforces equal train/test class distributions as a hard
constraint (the test set exactly matches
target_test * class_proportions); closest tracks that target
class-aware during the fill but pocket growth is capped by total size
only, so a class can overshoot its per-class target by a small margin.
Precondition for full faithfulness: the paper extracts the [CLS]
representations of a language model finetuned on the task (its RQ1 shows
this matters); pass such embeddings to reproduce the paper. On arbitrary
or pretrained-only embeddings the same procedure runs but is an adaptation.
The fixed-n_clusters path (cluster_range=None) runs the same exact
assignment at a single caller-chosen k instead of the paper's search.
The label-balanced idea behind "subset_sum" traces to the earlier
ClusterDataSplit of Wecker, Friedrich & Adel (2020), "ClusterDataSplit:
Exploring Challenging Clustering-Based Data Splits for Model Performance
Evaluation," Eval4NLP @ COLING, which clusters data into splits that
differ lexically from train while keeping the label distribution fixed.
https://aclanthology.org/2020.eval4nlp-1.15 . See :func:cluster_kfold
for their cross-validation variant.
Napoli & White (2025), "Clustering-Based Validation Splits for Model
Selection under Domain Shift," TMLR, ground this family theoretically:
maximizing the MMD between the two sets is equivalent to kernel k-means
clustering (k=2), and they replace ClusterDataSplit's greedy
class-balancing with a linear program (with convergence guarantees).
https://openreview.net/forum?id=Q692C0WtiD . See
:func:mmd_maximized_split for the MMD-maximizing objective.
Seed stability: varies with the seed -- the KMeans clustering, and which whole clusters are held out, shift with the seed, so the test set can differ substantially between runs (from near-identical to nearly disjoint) even though the clusters themselves are similar.
Source code in splytters/adversarial.py
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 | |
centroid_adversarial_split ¶
centroid_adversarial_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_clusters: int = 10, random_state: int = 42, **cluster_kwargs: Any) -> tuple[np.ndarray, np.ndarray]
Adversarial cluster split: assign clusters nearest to global centroid to train, furthest clusters to test.
Combines centroid-distance ranking with cluster-based splitting to maximize train-test dissimilarity while keeping similar samples together.
Thin wrapper over cluster_split(strategy="centroid").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_clusters
|
int
|
number of clusters |
10
|
random_state
|
int
|
for reproducibility |
42
|
**cluster_kwargs
|
Any
|
passed to KMeans |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed -- built on KMeans clustering, so which whole clusters land in test shifts between seeds and the held-out set can change substantially.
Source code in splytters/adversarial.py
cluster_kfold ¶
cluster_kfold(embeddings: ArrayLike, y: ArrayLike, n_folds: int = 5, method: str = 'kmeans', n_clusters: int | None = None, random_state: int = 42, **cluster_kwargs: Any) -> np.ndarray
Partition data into challenging, label-balanced cross-validation folds.
Clusters the embeddings, then assigns whole clusters to n_folds folds so
that each fold is lexically coherent -- similar samples share a fold, making
every fold differ from the data trained on for it -- while its label
distribution stays close to the global one. This yields a "challenging" CV:
each held-out fold is a cluster-based blind spot rather than a random sample.
The result is a per-sample fold id, usable directly with
:class:sklearn.model_selection.PredefinedSplit::
from sklearn.model_selection import PredefinedSplit, cross_validate
folds = cluster_kfold(embeddings, y, n_folds=5)
cross_validate(model, X, y, cv=PredefinedSplit(folds))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
y
|
ArrayLike
|
class labels of shape (n_samples,); used to balance folds by label |
required |
n_folds
|
int
|
number of cross-validation folds (default 5) |
5
|
method
|
str
|
clustering algorithm. |
'kmeans'
|
n_clusters
|
int | None
|
number of clusters for the |
None
|
random_state
|
int
|
for reproducibility |
42
|
**cluster_kwargs
|
Any
|
passed to the clustering algorithm. For
|
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
fold_ids |
ndarray
|
integer ndarray of shape (n_samples,), values in |
ndarray
|
|
|
ndarray
|
and the training set is the rest. |
Raises:
| Type | Description |
|---|---|
ValueError
|
on an unknown |
References
Wecker, Friedrich & Adel (2020), "ClusterDataSplit: Exploring Challenging Clustering-Based Data Splits for Model Performance Evaluation," Eval4NLP @ COLING. https://aclanthology.org/2020.eval4nlp-1.15 Reference code (archived): https://github.com/boschresearch/clusterdatasplit_eval4nlp-2020
The default 'kmeans'/'dbscan' modes are a cluster-coherent,
label-aware fold-assignment heuristic inspired by ClusterDataSplit: they
cluster with off-the-shelf KMeans/DBSCAN and greedily pack whole clusters
into folds that are lexically distinct from train while preserving label
balance. They do NOT implement the paper's SDS K-means.
method='sds_kmeans' implements the paper's SDS K-means (Size and
Distribution Sensitive K-means, an extension of Same-Size K-means). The
n_folds clusters are the folds directly. Each label's examples are
spread over the clusters as evenly as possible (label-specific
capacities), so every fold is ~n/n_folds in size with a label
distribution close to global. Points are placed by an order-measure
assignment (nearest-minus-farthest squared distance) that fills clusters
up to their per-label capacity, then refined by 1-on-1 swaps: two points
in different clusters that each want the other's cluster are exchanged,
which lowers overall cluster-internal variance while keeping sizes fixed.
Iteration stops when a full pass yields no swaps; the best of n_init
k-means++ initializations (by inertia) is returned.
Fidelity notes vs. the reference code: the reference exposes
max_ids_labels for the caller to specify per-cluster/per-label
capacities; here they are derived automatically from the global label
distribution (the paper's default SDS case, which also gives equal-size
clusters as in Same-Size K-means). The reference uses
sklearn's private _init_centroids; this uses the public
kmeans_plusplus. Assignment, capacity handling, the distance-sorted
1-on-1 swap update, the inertia objective (sum of per-cluster mean
squared distance), and the "stop when no swaps" rule follow the reference
implementation.
Seed stability: structure-stable. For the default modes, fold assignment
varies with the KMeans clustering, though the cluster-coherent,
label-balanced structure is preserved. For 'sds_kmeans', fold SIZES and
per-label counts are fixed by the capacities (seed-invariant); only which
samples land in which fold varies with the seed.
Source code in splytters/adversarial.py
993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 | |
minority_route ¶
minority_route(cluster_labels: ArrayLike, y: ArrayLike, minority_labels: str = 'all_but_majority') -> tuple[np.ndarray, np.ndarray]
Route a precomputed clustering into a bias-amplified minority split.
The label-only half of :func:minority_split: given an existing cluster
assignment (from any source -- kmeans, Ward, or an external clusterer such as a
faithful DEEP CLUSTER pass), send each cluster's majority-label instances to
train and its minority-label instances to test. Pure and gradient-free -- it
inspects only per-cluster label counts. Exposed so heavier, out-of-library
clusterers can reuse the exact same faithful routing (incl. footnote 10).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cluster_labels
|
ArrayLike
|
integer cluster id per sample, shape (n_samples,). |
required |
y
|
ArrayLike
|
class labels, shape (n_samples,). |
required |
minority_labels
|
str
|
|
'all_but_majority'
|
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
(train_indices, test_indices), each a sorted index array. |
Raises:
| Type | Description |
|---|---|
ValueError
|
on an unknown |
Source code in splytters/adversarial.py
1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 | |
minority_split ¶
minority_split(embeddings: ArrayLike, y: ArrayLike, n_clusters: int = 10, method: str = 'kmeans', random_state: int = 42, minority_labels: str = 'all_but_majority', **cluster_kwargs: Any) -> tuple[np.ndarray, np.ndarray]
Bias-amplified split via per-cluster minority labels.
Clusters the embeddings, then for each cluster routes instances whose label is the cluster's majority label to train (the "biased" majority a shortcut-learning model would exploit) and instances with any minority label to test (the "anti-biased" examples that defy the local pattern), yielding a hard, bias-amplified evaluation set.
Unlike the size-driven splitters, the train/test sizes are determined by the
data's bias structure (test = the minority-label instances), so this function
takes no train_size. It inspects per-cluster label counts only; it never
references global class frequencies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
y
|
ArrayLike
|
class labels of shape (n_samples,) |
required |
n_clusters
|
int
|
number of clusters (ignored by 'dbscan') |
10
|
method
|
str
|
clustering algorithm. |
'kmeans'
|
random_state
|
int
|
for reproducibility (used by 'kmeans' and 'deepcluster-lite') |
42
|
minority_labels
|
str
|
which of a cluster's labels are anti-biased (test).
|
'all_but_majority'
|
**cluster_kwargs
|
Any
|
passed to the clustering algorithm |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
majority-label ("biased") instances, pooled over clusters |
test_indices |
ndarray
|
minority-label ("anti-biased") instances, pooled over clusters |
Raises:
| Type | Description |
|---|---|
ValueError
|
on an unknown |
Warns:
| Type | Description |
|---|---|
UserWarning
|
if the test set is degenerately small (under 5% of the
samples). The clusters are then nearly label-pure, so the split has
too few minority examples to be informative -- prefer a different
|
References
Implements the "minority examples" notion of bias from Reif & Schwartz (2023), "Fighting Bias with Bias: Promoting Model Robustness by Amplifying Dataset Biases," Findings of ACL, pp. 13169-13189 (https://aclanthology.org/2023.findings-acl.833): cluster the data, treat each cluster's majority label as biased (train) and its minority labels as anti-biased (test). Their other two notions -- dataset cartography (training dynamics) and partial-input models -- are model-in-the-loop / task-specific and out of scope for a static splitter.
Faithfulness caveat: the paper deliberately does NOT cluster with a standard
algorithm. It reports that k-means/Ward produce label-homogeneous clusters
(too few minority examples) and instead uses DEEP CLUSTER (Caron et al.,
2018) -- a fine-tuned encoder trained on cluster pseudo-labels -- to obtain
label-diverse clusters. Our default (method='kmeans') is the standard
clusterer the paper rejects, so it can under-produce minority examples and
yield a small test set (hence the degenerate-size warning above).
method='ward' matches the paper's base clusterer (still homogeneous);
method='deepcluster-lite' is a static-feature stand-in for the full DEEP
CLUSTER step (no encoder fine-tuning -- it under-fits a small MLP on
pseudo-labels and re-clusters its hidden representation) that recovers some
label diversity at CPU cost.
Seed stability: with 'kmeans'/'deepcluster-lite', nearly deterministic given a
fixed random_state (the per-cluster minority labels are stable; only the
clustering wobbles slightly). 'ward' is fully deterministic (no seed).
Source code in splytters/adversarial.py
1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 | |
minority_grow_split ¶
minority_grow_split(embeddings: ArrayLike, y: ArrayLike, train_size: float | int = 0.7, n_clusters: int = 10, method: str = 'kmeans', metric: str = 'euclidean', random_state: int = 42, stratify: str = 'none', minority_labels: str = 'all_but_majority', **cluster_kwargs: Any) -> tuple[np.ndarray, np.ndarray]
Region-grown variant of :func:minority_split with a target test size.
Seeds the test set with the per-cluster minority-label ("anti-biased")
instances found by :func:minority_split, then greedily grows it: at each
step the not-yet-test sample closest to the current test set (single-link
distance to its nearest test member) is moved to test, until the test set
reaches the train_size-implied target. This keeps the hard, bias-defying
flavor of the minority seed while letting you dial the test fraction --
unlike :func:minority_split, whose size is dictated by the data and is
often degenerately small.
Class balance is controlled by stratify (mirrors cluster_split's
strategy string convention):
"none"(default): growth is purely geometric and ignoresy, so the test set is generally not class-balanced -- it expands into one region and inherits that region's dominant class (same un-balanced behavior as :func:minority_split)."global": one shared proximity frontier, but each class is capped at its data-proportional share of the test set; a class drops out of the frontier once full. The test set stays one contiguous region whose label mix tracks the data."per_class": each class grows its own nearest-to-seed neighborhood up to its quota, independently. The test set is the union of these per-class regions (so not a single contiguous blob), but every class contributes its geometrically-tightest hard samples. A class with no seed member is anchored at its sample nearest the overall seed.
For both stratified modes the quotas are data-proportional and the minority seed is always kept, so a class whose seed already over-fills its quota may still exceed its share.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
y
|
ArrayLike
|
class labels of shape (n_samples,) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set;
sets the target test size ( |
0.7
|
n_clusters
|
int
|
number of clusters for the minority seed (ignored by 'dbscan') |
10
|
method
|
str
|
clustering algorithm for the seed -- see :func: |
'kmeans'
|
metric
|
str
|
distance metric ( |
'euclidean'
|
random_state
|
int
|
for reproducibility of the clustering seed (and of any oversized-seed subsampling) |
42
|
stratify
|
str
|
class-balance policy for growth, one of |
'none'
|
minority_labels
|
str
|
which cluster labels seed the test set, |
'all_but_majority'
|
**cluster_kwargs
|
Any
|
passed to the seed clustering algorithm |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for the training set |
test_indices |
ndarray
|
ndarray of indices for the test set (the minority seed plus its grown neighborhood). A seed larger than the target is subsampled down to it (round-robin across classes to preserve coverage); a seed equal to the target is returned as-is. Under a stratified mode the test set may still fall short of the target if every class hits its quota first. |
Raises:
| Type | Description |
|---|---|
ValueError
|
on an unknown |
Warns:
| Type | Description |
|---|---|
UserWarning
|
if the resulting test set contains no samples for one or more
classes (those classes then can't be evaluated). Prefer a stratified
mode or a larger |
References
Extends the bias-amplified minority seed of Reif & Schwartz (2023); see
:func:minority_split. The proximity growth is the single-linkage region
growth also used by cluster_split(strategy="closest").
Seed stability: nearly deterministic -- the minority seeds and proximity
growth are largely fixed; only the underlying clustering wobbles between
seeds. This holds in every stratify mode (none / global / per-class).
Source code in splytters/adversarial.py
1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 | |
class_boundary_split ¶
class_boundary_split(embeddings: ArrayLike, y: ArrayLike, train_size: float | int = 0.7, metric: str = 'euclidean', reference: str = 'centroids', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Class-stratified adversarial split on per-class decision boundaries.
Within each class, ranks samples by how close they sit to other classes and routes the closest (most confusable, near-boundary) ones to the test set until that class's test quota is filled. The remaining, more class-typical samples go to train. Because the quota is applied per class, the test set stays label-balanced (stratified) while still being hard: it concentrates the examples a model is most likely to confuse across categories.
"Closeness to other classes" is measured by reference:
"centroids"(default): distance to the nearest other class centroid. Cheap -- O(n*K) for K classes."samples": nearest-enemy distance, i.e. distance to the single closest sample belonging to any other class. Sharper boundaries, but O(n^2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
y
|
ArrayLike
|
class labels of shape (n_samples,) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count, applied per class to size each class's train portion. A fraction is recommended; an absolute count is clamped to each class's size. |
0.7
|
metric
|
str
|
distance metric passed to |
'euclidean'
|
reference
|
str
|
|
'centroids'
|
random_state
|
int
|
accepted for API consistency; this split is deterministic |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of class-typical (interior) sample indices |
test_indices |
ndarray
|
ndarray of near-boundary, cross-class-confusable indices |
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
References
A label-stratified, per-class variant of the adversarial-distance idea of
Søgaard, Ebert, Bastings & Filippova (2021), "We Need to Talk About
Random Splits," EACL (https://aclanthology.org/2021.eacl-main.156): hard
splits push dissimilar samples into test. Here "dissimilar" is measured
toward other classes rather than toward the training set, yielding a
boundary-focused test set; contrast :func:distance_adversarial_split
(unsupervised, distance from the global centroid).
Seed stability: deterministic -- closeness to other classes is a fixed geometric quantity, so the seed has no effect (it is accepted only for API consistency).
Source code in splytters/adversarial.py
1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 | |
decision_boundary_split ¶
decision_boundary_split(embeddings: ArrayLike, y: ArrayLike, train_size: float | int = 0.7, *, model: str = 'linear_svc', score: str = 'confidence', stratify: str = 'per_class', cv: int = 5, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Supervised adversarial split on a learned decision boundary.
Fits a fast linear classifier under cross-validation and routes the samples
a real model finds hardest -- those closest to the learned decision boundary
-- to the test set, leaving the confident, class-interior samples for train.
The learned-boundary counterpart to :func:class_boundary_split (which
instead measures geometric distance to other classes).
Hardness is scored out of fold via
:class:~sklearn.model_selection.StratifiedKFold: each sample's margin comes
from a model that did not train on it, so the score reflects genuine
difficulty rather than in-sample memorization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim). |
required |
y
|
ArrayLike
|
class labels of shape (n_samples,). |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count. With
|
0.7
|
model
|
str
|
surrogate classifier -- |
'linear_svc'
|
score
|
str
|
hardness measure. |
'confidence'
|
stratify
|
str
|
|
'per_class'
|
cv
|
int
|
number of cross-validation folds, clamped to the smallest class count. Each fold supplies the out-of-fold margins for its held-out samples. |
5
|
random_state
|
int
|
seeds the fold shuffling and the classifier; the split is deterministic (ties broken by index). |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of confident, class-interior sample indices. |
test_indices |
ndarray
|
ndarray of near-boundary, model-confusable indices. |
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
References
Uncertainty / margin sampling from active learning (Lewis & Gale, 1994;
Scheffer et al., 2001; Settles, 2009). Like :func:class_boundary_split,
a label-aware variant of the hard-split motivation of Soegaard et al.
(2021), "We Need to Talk About Random Splits"
(https://aclanthology.org/2021.eacl-main.156), but using a learned
boundary rather than embedding geometry.
Seed stability: structure-stable -- the cross-validation folds depend on the
seed, so which samples sit nearest the learned boundary (and go to test)
shifts somewhat between seeds. This holds in both stratify modes
(per-class and global wobble about equally).
Source code in splytters/adversarial.py
1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 | |
distance_adversarial_split ¶
distance_adversarial_split(embeddings: ArrayLike, train_size: float | int = 0.7, metric: str = 'euclidean', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Adversarial split based on individual sample distance from centroid.
Samples closest to centroid go to train, furthest go to test. Unlike cluster-based methods, this operates on individual samples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
metric
|
str
|
distance metric |
'euclidean'
|
random_state
|
int
|
accepted for API consistency; this split is deterministic |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: deterministic -- samples are ranked by distance from the centroid, so the seed has no effect (it is accepted only for API consistency).
Source code in splytters/adversarial.py
density_adversarial_split ¶
density_adversarial_split(embeddings: ArrayLike, train_size: float | int = 0.7, metric: str = 'euclidean', k: int = 10, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Adversarial split based on local density.
Samples in dense regions go to train, isolated samples go to test.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
metric
|
str
|
distance metric |
'euclidean'
|
k
|
int
|
number of neighbors for density estimation |
10
|
random_state
|
int
|
accepted for API consistency; this split is deterministic |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: deterministic -- samples are ranked by local density, so the seed has no effect.
Source code in splytters/adversarial.py
outlier_adversarial_split ¶
outlier_adversarial_split(embeddings: ArrayLike, train_size: float | int = 0.7, contamination: float = 0.1, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Adversarial split using outlier detection.
Normal samples go to train, outliers go to test.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
contamination
|
float
|
expected proportion of outliers |
0.1
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: nearly deterministic -- the outlier ranking is fixed; only samples right at the train/test cutoff can change between seeds.
Source code in splytters/adversarial.py
min_cut_split ¶
min_cut_split(embeddings: ArrayLike, train_size: float | int = 0.7, similarity_threshold: float | None = None, metric: str = 'euclidean', method: str = 'spectral', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Adversarial split using graph min-cut.
Builds a similarity graph and finds a partition that minimizes edges (similarity) crossing the train/test boundary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
similarity_threshold
|
float | None
|
only connect samples above this kernel similarity (default: median similarity) |
None
|
metric
|
str
|
distance metric |
'euclidean'
|
method
|
str
|
|
'spectral'
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: deterministic for successful spectral and Stoer-Wagner paths. The spectral split follows a Fiedler vector from a dense eigendecomposition (no random start vector), whose otherwise-arbitrary sign is oriented deterministically. The only seeded branch is the warned random fallback after a spectral solver failure.
Source code in splytters/adversarial.py
2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 | |
normalized_cut_split ¶
normalized_cut_split(embeddings: ArrayLike, train_size: float | int = 0.7, metric: str = 'euclidean', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Adversarial split using normalized graph cut.
Normalized cut balances the cut value with partition sizes, avoiding trivially small partitions.
NCut(A,B) = cut(A,B)/vol(A) + cut(A,B)/vol(B)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
metric
|
str
|
distance metric |
'euclidean'
|
random_state
|
int
|
accepted for API consistency; this split is deterministic |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: deterministic -- the normalized-cut partition uses a dense eigendecomposition (eigh), which is fixed, so the seed has no effect.
Source code in splytters/adversarial.py
2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 | |
wasserstein_adversarial_split ¶
wasserstein_adversarial_split(embeddings: ArrayLike, train_size: float | int = 0.7, leaf_size: int = 40, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Adversarial split via Wasserstein nearest-neighbors.
Treats each embedding row as a 1-D distribution and selects, as the test
set, the n_test points nearest (in 1-D Wasserstein / earth-mover
distance) to a random anchor — yielding a tight, hard-to-generalize-to test
neighborhood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
leaf_size
|
int
|
BallTree leaf size (higher = slower but less memory) |
40
|
random_state
|
int
|
for reproducibility (anchor sampling) |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
References
Adapted from the adversarial split of Søgaard, Ebert, Bastings & Filippova (2021), "We Need to Talk About Random Splits," EACL, which constructs hard splits by (approximately) maximizing the Wasserstein distance between train and test. https://aclanthology.org/2021.eacl-main.156
Seed stability: varies with the seed like a random split -- the test pocket is grown around a randomly chosen anchor point.
Source code in splytters/adversarial.py
mmd_maximized_split ¶
mmd_maximized_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_iterations: int = 500, kernel: str = 'rbf', gamma: float | None = None, random_state: int = 42, method: str = 'swap', y: ArrayLike | None = None, groups: ArrayLike | None = None) -> tuple[np.ndarray, np.ndarray]
Maximize Maximum Mean Discrepancy (MMD) between train and test.
The adversarial dual of :func:splytters.mmd_minimized_split: instead of
matching the two distributions, it pushes them as far apart as possible in
kernel/MMD terms, yielding a hard, domain-shifted held-out set. Motivated by
model selection under domain shift -- a maximally-shifted validation set
tends to select more robust hyperparameters.
Two methods are available:
method="swap"(default): the historical swap-optimized approximation of the max-MMD objective -- a random split refined by accepting train/test swaps that raise the MMD.method="kernel_kmeans": a full-kernel implementation derived from Napoli & White (see References) -- constrained kernel k-means withk = 2whose assignment step is a linear program (LP). Pass optionalyand/orgroupsto enforce the paper's per-label / per-group distribution constraints in both sides of the split; the size constraint comes fromtrain_size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_iterations
|
int
|
swap attempts ( |
500
|
kernel
|
str
|
kernel type ('rbf' or 'linear') |
'rbf'
|
gamma
|
float | None
|
RBF kernel parameter (default: 1/n_features) |
None
|
random_state
|
int
|
for reproducibility |
42
|
method
|
str
|
|
'swap'
|
y
|
ArrayLike | None
|
optional labels; only valid with |
None
|
groups
|
ArrayLike | None
|
optional group ids; only valid with |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
References
Napoli & White (TMLR 2025; arXiv 2024), "Clustering-Based Validation
Splits for Model Selection under Domain Shift"
(https://openreview.net/forum?id=Q692C0WtiD): the train/validation split
should maximize the MMD between the two sets. Under
method="kernel_kmeans" implements their kernel k-means objective and
solves both disjunctive LP orientations from Eq. 15 before keeping the
lower-cost assignment. Maximizing MMD is equivalent to minimizing the
k = 2 kernel k-means scatter (their Theorem 1). This full-kernel variant
uses exact-size largest-remainder group targets and omits their Nyström
approximation for very large n; those departures are documented in
:func:splytters.utils.constrained_kernel_kmeans_split. The default
method="swap" remains a swap-optimized approximation of the same
objective.
Seed stability: with method="swap", varies with the seed like a random
split -- the swap optimization starts from a random split and many
assignments reach a similar MMD. With method="kernel_kmeans" the result
is deterministic given random_state (which seeds only the initial
partition).
Source code in splytters/adversarial.py
2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 | |
get_cluster_info ¶
get_cluster_info(embeddings: ArrayLike, train_indices: ArrayLike, test_indices: ArrayLike, n_clusters: int = 10, random_state: int = 42) -> dict[str, Any]
Utility to analyze cluster distribution across train/test split.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
dict with cluster statistics including leakage info |
Source code in splytters/adversarial.py
maximin_split ¶
maximin_split(embeddings: ArrayLike, train_size: float | int = 0.7, metric: str = 'euclidean', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Adversarial split via farthest-point (k-center) test selection.
Builds the test set by greedy farthest-first traversal: seed with one point,
then repeatedly add the point farthest (metric) from everything already
selected. The result is a maximally spread-out test set that covers the
corners and extremes of the embedding space — a diverse, hard evaluation
that, unlike a random sample, never under-represents sparse / outlying
regions. Train is the (denser) remainder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim). |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set. |
0.7
|
metric
|
str
|
distance metric passed to |
'euclidean'
|
random_state
|
int
|
seeds the starting point (the traversal is otherwise deterministic). |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
the denser interior remainder. |
test_indices |
ndarray
|
a farthest-point-sampled, space-covering test set. |
Seed stability: structure-stable -- only the starting point of the farthest-first traversal is random, so different seeds give overlapping but not identical space-covering test sets.
Source code in splytters/adversarial.py
Overlap splitters¶
overlap ¶
High-overlap splitting algorithms that maximize train-test similarity.
These methods create "easy" evaluation sets where test samples are similar to training samples, useful for sanity checks and debugging.
cluster_leak_split ¶
cluster_leak_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_clusters: int = 10, random_state: int = 42, **cluster_kwargs: Any) -> tuple[np.ndarray, np.ndarray]
Split clusters across train/test to maximize similarity.
Instead of assigning entire clusters to one set (adversarial), this splits each cluster proportionally between train and test, ensuring similar samples appear in both sets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_clusters
|
int
|
number of clusters |
10
|
random_state
|
int
|
for reproducibility |
42
|
**cluster_kwargs
|
Any
|
passed to KMeans |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- each cluster is shuffled before being split, so which members go to test differs run to run.
Source code in splytters/overlap.py
neighbor_coverage_split ¶
neighbor_coverage_split(embeddings: ArrayLike, train_size: float | int = 0.7, k: int = 5, metric: str = 'euclidean', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Ensure each test sample has k similar samples in train.
Solves a binary feasibility problem whose constraints require every test
sample to have at least k train neighbors within the median pairwise
distance. Raises when no split of the requested size can meet that promise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
k
|
int
|
minimum number of similar train samples for each test sample |
5
|
metric
|
str
|
distance metric |
'euclidean'
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed. The constraints are fixed, while a seeded random linear objective selects among multiple feasible solutions.
Source code in splytters/overlap.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | |
centroid_matched_split ¶
centroid_matched_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_iterations: int = 100, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Minimize distance between train and test centroids.
Uses iterative optimization to find a split where the centroids of train and test sets are as close as possible.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_iterations
|
int
|
number of swap iterations |
100
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- the swap optimization has many assignments with equally matched centroids.
Source code in splytters/overlap.py
stratified_similarity_split ¶
stratified_similarity_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_bins: int = 10, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Stratify by distance from centroid, ensuring similar distribution in both sets.
Bins samples by their distance from the centroid and samples proportionally from each bin for both train and test.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_bins
|
int
|
number of distance bins for stratification |
10
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- samples are shuffled within each distance bin before the proportional split.
Source code in splytters/overlap.py
nearest_neighbor_split ¶
nearest_neighbor_split(embeddings: ArrayLike, train_size: float | int = 0.7, metric: str = 'euclidean', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
For each test sample, ensure its nearest neighbor is in train.
Greedily builds test set by moving points whose nearest neighbor is already confirmed in train. Uses sklearn's NearestNeighbors which auto-selects the best algorithm (kd-tree, ball tree, or brute force) based on dimensionality.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
metric
|
str
|
distance metric |
'euclidean'
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- points are processed in a random order when building the test set.
Source code in splytters/overlap.py
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | |
duplicate_spread_split ¶
duplicate_spread_split(embeddings: ArrayLike, train_size: float | int = 0.7, similarity_threshold: float | None = None, metric: str = 'euclidean', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Intentionally put near-duplicates in both train and test.
Identifies clusters of near-duplicates and ensures at least one sample from each cluster appears in both sets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
similarity_threshold
|
float | None
|
distance threshold for near-duplicates (default: 10th percentile of distances) |
None
|
metric
|
str
|
distance metric |
'euclidean'
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- each near-duplicate group is shuffled before being split across both sides.
Source code in splytters/overlap.py
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 | |
max_coverage_split ¶
max_coverage_split(embeddings: ArrayLike, train_size: float | int = 0.7, radius: float | None = None, metric: str = 'euclidean', random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Maximize the coverage of test set by train set.
Coverage = fraction of test samples with at least one train sample within radius.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
radius
|
float | None
|
distance threshold for coverage (default: median distance) |
None
|
metric
|
str
|
distance metric |
'euclidean'
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- it starts from a random split and greedily swaps, reaching different high-coverage solutions.
Source code in splytters/overlap.py
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | |
Balanced splitters¶
balanced ¶
Balanced splitting algorithms that match distributions between train/test.
These methods create splits where train and test have similar statistical properties, useful for fair evaluation without distribution shift.
distribution_matched_split ¶
distribution_matched_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_iterations: int = 1000, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Minimize distribution divergence between train and test.
Uses iterative optimization to match the marginal distributions of each feature dimension.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_iterations
|
int
|
number of optimization iterations |
1000
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- the swap optimization starts from a random split and many assignments match the distribution equally well.
Source code in splytters/balanced.py
moment_matched_split ¶
moment_matched_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_iterations: int = 1000, match_variance: bool = True, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Match mean (and optionally variance) between train and test.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_iterations
|
int
|
number of optimization iterations |
1000
|
match_variance
|
bool
|
if True, also match variance |
True
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- the swap optimization reaches different assignments with equally matched moments.
Source code in splytters/balanced.py
histogram_matched_split ¶
histogram_matched_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_bins: int = 10, n_iterations: int = 1000, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Match feature histograms between train and test.
Minimizes the sum of histogram differences across all dimensions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_bins
|
int
|
number of histogram bins per dimension |
10
|
n_iterations
|
int
|
number of optimization iterations |
1000
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- the swap optimization reaches different assignments with equally matched histograms.
Source code in splytters/balanced.py
stratified_random_split ¶
stratified_random_split(embeddings: ArrayLike, y: ArrayLike, train_size: float | int = 0.7, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Standard stratified split maintaining label proportions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
y
|
ArrayLike
|
array of labels for stratification (aligned with sklearn's |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- it is a seeded stratified random draw.
Source code in splytters/balanced.py
density_balanced_split ¶
density_balanced_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_bins: int = 10, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Balance local density distribution between train and test.
Bins samples by local density and samples proportionally from each bin for both sets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_bins
|
int
|
number of density bins |
10
|
random_state
|
int
|
for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: varies with the seed like a random split -- samples are shuffled within each density bin before the proportional split.
Source code in splytters/balanced.py
mmd_minimized_split ¶
mmd_minimized_split(embeddings: ArrayLike, train_size: float | int = 0.7, n_iterations: int = 500, kernel: str = 'rbf', gamma: float | None = None, random_state: int = 42, method: str = 'swap', y: ArrayLike | None = None, groups: ArrayLike | None = None) -> tuple[np.ndarray, np.ndarray]
Minimize Maximum Mean Discrepancy between train and test.
MMD is a kernel-based measure of distribution difference. Lower MMD indicates more similar distributions.
Two methods are available:
method="swap"(default): the historical swap-optimized approximation that lowers the MMD by accepting train/test swaps that reduce it.method="kernel_kmeans": the min-MMD dual of the constrained kernel k-means (k = 2) method of Napoli & White (see References). The assignment step is a linear program (LP); it maximizes the kernel scatter (anti-clustering) so the two sides resemble each other. Pass optionalyand/orgroupsto enforce per-label / per-group distribution constraints; the size constraint comes fromtrain_size. Note: while the resulting MMD is far below a random split's, in practice it is typically around 2x the MMD reached by the default swap optimizer, so prefermethod="swap"when the absolute lowest MMD matters andmethod="kernel_kmeans"when the label/group constraints do.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
n_iterations
|
int
|
swap attempts ( |
500
|
kernel
|
str
|
kernel type ('rbf' or 'linear') |
'rbf'
|
gamma
|
float | None
|
RBF kernel parameter (default: 1/n_features) |
None
|
random_state
|
int
|
for reproducibility |
42
|
method
|
str
|
|
'swap'
|
y
|
ArrayLike | None
|
optional labels; only valid with |
None
|
groups
|
ArrayLike | None
|
optional group ids; only valid with |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
References
Napoli & White (TMLR 2025; arXiv 2024), "Clustering-Based Validation
Splits for Model Selection under Domain Shift"
(https://openreview.net/forum?id=Q692C0WtiD) study the adversarial dual
-- maximizing train/validation MMD -- via constrained kernel k-means
with k = 2 solved by linear programming (their Algorithm 1). This
splitter minimizes MMD instead. Under method="kernel_kmeans" it uses
the same constrained-LP kernel k-means machinery with the objective sign
flipped (maximizing the k = 2 scatter, the min-MMD dual). The default
method="swap" remains a swap-optimized approximation of the same
objective, matching :func:splytters.mmd_maximized_split. Their Nyström
scaling for very large n is not implemented (out of scope).
Seed stability: with method="swap", varies with the seed like a random
split -- the swap optimization reaches different assignments with similarly
low MMD. With method="kernel_kmeans" the result is deterministic given
random_state (which seeds only the initial partition).
Source code in splytters/balanced.py
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | |
Curriculum splits¶
curriculum ¶
Curriculum / ordering-driven splitting.
Unlike the embedding-based splitters (adversarial / overlap / balanced), these
take an explicit sort order — typically a :mod:splytters.sorters ranking — and
partition each class along it. The classic use is a "train on easy, test on
hard" curriculum split: sort by a difficulty metric, then take the first
train_size fraction of each class as train.
sorted_stratified_split ¶
sorted_stratified_split(order: Sequence[Any], y: ArrayLike, train_size: float | int = 0.7, *, largest_first: bool = False) -> tuple[np.ndarray, np.ndarray]
Per-class curriculum split driven by a sort order.
Within each class, samples are visited in the given order and the first
train_size fraction (or absolute count) go to train, the rest to test.
Pair it with a :mod:splytters.sorters ranking to split along an
interpretable difficulty axis — e.g. "train on easy, test on hard".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
order
|
Sequence[Any]
|
the sort order over samples — either a flat sequence of sample
indices, or a sorter result (sequence of |
required |
y
|
ArrayLike
|
class labels aligned to the original samples (length n_samples). The split is performed independently within each class. |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count, applied per class. |
0.7
|
largest_first
|
bool
|
if True, reverse |
False
|
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Source code in splytters/curriculum.py
scikit-learn compatibility¶
sklearn_api ¶
scikit-learn compatibility layer for splytters.
Provides:
- :class:
SplytterSplit— a single-split cross-validator wrapping any splytter splitting function, usable ascv=in :func:sklearn.model_selection.cross_validateand :class:~sklearn.model_selection.GridSearchCV. *_train_test_splitconvenience functions mirroring :func:sklearn.model_selection.train_test_splitfor drop-in swaps.
The functional splitters in :mod:splitters remain the source of truth; these
wrappers are purely additive.
SplytterSplit ¶
Bases: BaseCrossValidator
A scikit-learn cross-validator backed by a splytter splitting function.
Produces n_splits train/test partitions (one by default, like
:class:~sklearn.model_selection.PredefinedSplit). Each partition is the
output of splitter(embeddings, train_size=..., random_state=...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
splitter
|
Splitter
|
any splytter splitter taking
|
cluster_split
|
embeddings
|
Any
|
array-like of shape (n_samples, n_features), optional.
Embeddings to split on. If |
None
|
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set (default 0.7). |
0.7
|
random_state
|
int | None
|
base seed (default 42); the i-th repeat uses
|
42
|
n_splits
|
int
|
number of (repeated) partitions to yield (default 1). |
1
|
**splitter_kwargs
|
Any
|
extra keyword arguments forwarded to |
{}
|
Examples:
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.model_selection import cross_validate
>>> cv = SplytterSplit(embeddings=X)
>>> cross_validate(LogisticRegression(), X, y, cv=cv)
Source code in splytters/sklearn_api.py
splytter_train_test_split ¶
splytter_train_test_split(*arrays: Any, splitter: Splitter = cluster_split, embeddings: Any = None, train_size: float | int = 0.7, random_state: int | None = 42, **splitter_kwargs: Any) -> list[Any] | tuple[np.ndarray, np.ndarray]
Split arrays into train/test subsets using a splytter splitter.
Mirrors :func:sklearn.model_selection.train_test_split: for inputs
a, b returns [a_train, a_test, b_train, b_test].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*arrays
|
Any
|
array-likes (numpy, list, pandas, etc.) to split, all of the same length (n_samples). |
()
|
splitter
|
Splitter
|
splytter splitting function. Defaults to
:func: |
cluster_split
|
embeddings
|
Any
|
array-like to compute the split on, optional. Defaults to the first array. |
None
|
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set. |
0.7
|
random_state
|
int | None
|
seed forwarded to |
42
|
**splitter_kwargs
|
Any
|
extra keyword arguments forwarded to |
{}
|
Returns:
| Type | Description |
|---|---|
list[Any] | tuple[ndarray, ndarray]
|
|
list[Any] | tuple[ndarray, ndarray]
|
are passed, returns |
Source code in splytters/sklearn_api.py
Framework interop¶
interop ¶
Framework interop helpers for splytters.
Thin adapters that take an embedding-based splitter and return native objects for popular ecosystems. Heavy dependencies (pandas, torch, datasets) are imported lazily inside each function, so importing this module never requires them — only calling the relevant helper does.
split_dataframe ¶
split_dataframe(df: Any, embeddings: Any, splitter: Splitter = cluster_split, *, train_size: float | int = 0.7, random_state: int | None = 42, **splitter_kwargs: Any) -> tuple[Any, Any]
Split a pandas DataFrame into (train_df, test_df) by position.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
Any
|
pandas |
required |
embeddings
|
Any
|
array-like of shape (n_samples, n_features). |
required |
splitter
|
Splitter
|
splytter splitting function. |
cluster_split
|
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set. |
0.7
|
random_state
|
int | None
|
seed forwarded to |
42
|
**splitter_kwargs
|
Any
|
extra keyword arguments forwarded to |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
|
Any
|
index labels are preserved). |
Source code in splytters/interop.py
to_torch_subsets ¶
to_torch_subsets(dataset: Any, embeddings: Any, splitter: Splitter = cluster_split, *, train_size: float | int = 0.7, random_state: int | None = 42, **splitter_kwargs: Any) -> tuple[Any, Any]
Split a torch Dataset into (train_subset, test_subset).
Returns two :class:torch.utils.data.Subset views over dataset.
Source code in splytters/interop.py
split_dataset ¶
split_dataset(ds: Any, embeddings: Any, splitter: Splitter = cluster_split, *, train_size: float | int = 0.7, random_state: int | None = 42, **splitter_kwargs: Any) -> Any
Split a HuggingFace datasets.Dataset into a DatasetDict.
Returns DatasetDict({"train": ds.select(train_idx),
"test": ds.select(test_idx)}).
Source code in splytters/interop.py
Split-quality reporting¶
report ¶
Split-quality reporting.
Quantifies how adversarial, overlapping, or balanced a produced train/test
split actually is — so users can compare splitters and so papers can report a
single, interpretable table. Builds on :func:splytters.utils.compute_split_similarity
and :func:splytters.adversarial.get_cluster_info, adding distribution-distance
metrics (MMD, energy, mean 1-D and sliced Wasserstein, KS, Fréchet), a
classifier two-sample test (c2st_auc), k-NN manifold precision/recall, and
label balance.
split_report ¶
split_report(embeddings: ArrayLike, train_indices: ArrayLike, test_indices: ArrayLike, y: ArrayLike | None = None, *, texts: list[str] | None = None, metric: str = 'euclidean', n_clusters: int = 10, max_samples: int | None = 2000, random_state: int | None = 42) -> dict[str, float]
Summarize how similar/dissimilar a train/test split is.
Larger distribution distances (mmd_rbf, energy_distance,
wasserstein_mean, sliced_wasserstein, ks_mean,
frechet_distance) and mean_cross_distance indicate a more
adversarial (harder) split; values near a random split indicate a
balanced split; near-zero indicates overlap.
c2st_auc is a classifier two-sample test — the out-of-fold AUC of a
random-forest classifier trained to tell train from test points: ~0.5 means
the two sides are indistinguishable (overlap), →1.0 means trivially separable
(adversarial). manifold_precision / manifold_recall (k-NN support
coverage) report how much of test lies inside the train manifold and vice
versa.
Alongside these train↔test distance metrics, train_diversity and
test_diversity report the within-split spread (mean distance to each
side's own centroid) — useful for spotting when a split concentrates one
side (e.g. an adversarial test drawn from a single outlier cluster).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, n_features). |
required |
train_indices
|
ArrayLike
|
integer index array for the training split. |
required |
test_indices
|
ArrayLike
|
integer index array for the test split. |
required |
y
|
ArrayLike | None
|
array-like of labels, optional. If given, adds
|
None
|
texts
|
list[str] | None
|
per-sample raw strings, optional and aligned with |
None
|
metric
|
str
|
distance metric (default |
'euclidean'
|
n_clusters
|
int
|
clusters used for the leakage statistic (default 10). |
10
|
max_samples
|
int | None
|
cap per side for the O(n²) distribution metrics
(subsampled for speed); |
2000
|
random_state
|
int | None
|
seed for subsampling (default 42). |
42
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
A dict of structural, geometric, distributional, diversity, and |
dict[str, float]
|
(optional) label metrics. |
Source code in splytters/report.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
compare_splitters ¶
compare_splitters(embeddings: ArrayLike, splitters: dict[str, Splitter], y: ArrayLike | None = None, *, train_size: float | int = 0.7, random_state: int | None = 42, **report_kwargs: Any) -> dict[str, dict[str, float]]
Run several splitters and return {name: split_report(...)}.
Handy for generating a comparison table (e.g. random vs adversarial vs balanced) in one call.
Source code in splytters/report.py
Sorters¶
Ranking functions that order samples by interpretable difficulty/quality
metrics, grouped by modality. Pair a ranking with
sorted_stratified_split for a curriculum split. Each
modality imports its heavy dependency lazily, so importing a sorter module needs
no optional extra.
Embedding sorters¶
embedding_sorters ¶
Sorting algorithms for adversarial dataset partitioning based on embeddings.
These functions rank samples by embedding-based criteria (distance to centroid, nearest neighbors, density, outlier scores) to enable train-test splits that maximize dissimilarity.
dist_euclidean ¶
distance_to_mean ¶
distance_to_mean(embeddings: ndarray, distance: Callable[[ndarray, ndarray], float] = dist_euclidean) -> list[tuple[int, float]]
Sort samples by distance from the dataset centroid.
Samples closest to the centroid (most "typical") appear first. Useful for adversarial splits: assign nearby samples to train, distant samples to test.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ndarray
|
np.array of shape (n_samples, embedding_dim) |
required |
distance
|
Callable[[ndarray, ndarray], float]
|
distance function taking two vectors, default Euclidean |
dist_euclidean
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, distance) tuples sorted by distance ascending. |
Source code in splytters/sorters/embedding_sorters.py
distance_to_nearest_neighbor ¶
distance_to_nearest_neighbor(embeddings: ArrayLike, metric: str = 'euclidean') -> list[tuple[int, float]]
Sort samples by distance to their nearest neighbor.
Samples in dense regions (close to neighbors) appear first. Isolated samples (far from all neighbors) appear last.
Useful for adversarial splits: train on samples in dense clusters, test on isolated/unique samples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
np.array of shape (n_samples, embedding_dim) |
required |
metric
|
str
|
distance metric for |
'euclidean'
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, distance) tuples sorted by nearest neighbor distance ascending. |
Source code in splytters/sorters/embedding_sorters.py
local_density ¶
local_density(embeddings: ArrayLike, radius: float | None = None, metric: str = 'euclidean', low_first: bool = True) -> list[tuple[int, int]]
Sort samples by local density (number of neighbors within radius).
Samples in dense regions have many neighbors; isolated samples have few.
Useful for adversarial splits: train on high-density regions, test on sparse/low-density regions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
np.array of shape (n_samples, embedding_dim) |
required |
radius
|
float | None
|
distance threshold for counting neighbors. If None, uses median pairwise distance. |
None
|
metric
|
str
|
distance metric for cdist (default 'euclidean') |
'euclidean'
|
low_first
|
bool
|
if True, sparse/isolated samples first; if False, dense samples first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
List of (index, neighbor_count) tuples sorted by density. |
Source code in splytters/sorters/embedding_sorters.py
outlier_score ¶
outlier_score(embeddings: ArrayLike, method: str = 'isolation_forest', low_first: bool = True, random_state: int = 42, **kwargs: Any) -> list[tuple[int, float]]
Sort samples by anomaly/outlier score.
Higher outlier scores indicate samples that are unusual or don't fit the overall data distribution.
Useful for adversarial splits: train on normal/typical samples, test on outliers/anomalies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
np.array of shape (n_samples, embedding_dim) |
required |
method
|
str
|
outlier detection algorithm, one of: - 'isolation_forest': Isolation Forest (fast, good for high dimensions) - 'lof': Local Outlier Factor (density-based) |
'isolation_forest'
|
low_first
|
bool
|
if True, normal/inlier samples first; if False, outliers/anomalies first |
True
|
random_state
|
int
|
seed for isolation_forest (ignored by lof, which is
deterministic). Exposed as its own parameter so it can't collide
with a |
42
|
**kwargs
|
Any
|
additional arguments passed to the outlier detector |
{}
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, outlier_score) tuples sorted by outlier score. |
list[tuple[int, float]]
|
For isolation_forest: more negative = more normal, more positive = more outlier. |
list[tuple[int, float]]
|
For lof: scores > 1 indicate outliers, < 1 indicate inliers. |
Source code in splytters/sorters/embedding_sorters.py
mahalanobis_distance_to_mean ¶
mahalanobis_distance_to_mean(embeddings: ArrayLike, low_first: bool = True) -> list[tuple[int, float]]
Sort samples by Mahalanobis distance from the dataset centroid.
Like :func:distance_to_mean, but covariance-aware: atypicality is measured
relative to the data's own spread and feature correlations rather than in
raw Euclidean units. Most typical samples first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array of shape (n_samples, embedding_dim). |
required |
low_first
|
bool
|
if True, the most typical (smallest distance) samples first. |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, mahalanobis_distance) tuples. |
Source code in splytters/sorters/embedding_sorters.py
knn_label_disagreement ¶
knn_label_disagreement(embeddings: ArrayLike, y: ArrayLike, k: int = 5, metric: str = 'euclidean', low_first: bool = True) -> list[tuple[int, float]]
Sort samples by the fraction of their k nearest neighbors with a different label — a class-boundary / ambiguity score.
A point surrounded by same-label neighbors is class-typical (interior); one
whose neighbors disagree sits on a boundary and is harder. This is the
label-aware (supervised) sorter — pair it with
:func:splytters.sorted_stratified_split for a difficulty curriculum.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array of shape (n_samples, embedding_dim). |
required |
y
|
ArrayLike
|
class labels of shape (n_samples,). |
required |
k
|
int
|
number of neighbors to inspect (the point itself is excluded). |
5
|
metric
|
str
|
distance metric for the neighbor search. |
'euclidean'
|
low_first
|
bool
|
if True, the most class-typical (low-disagreement) first. |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, disagreement_fraction) tuples, each in [0, 1]. |
Source code in splytters/sorters/embedding_sorters.py
Text sorters¶
text_sorters ¶
Sorting algorithms for adversarial text dataset partitioning.
These functions rank text samples by various criteria (length, complexity, perplexity, readability, vocabulary) to enable train-test splits that maximize dissimilarity.
simple_tokenizer ¶
character_length ¶
Sort texts by character count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
list of strings |
required |
low_first
|
bool
|
if True, shortest texts first; if False, longest first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
List of (index, character_count) tuples sorted by length. |
References
Splitting by length to test generalization (put long texts in test): - Søgaard, Ebert, Bastings & Filippova (2021), "We Need to Talk About Random Splits," EACL. Heuristic splits via a sentence-length threshold. https://aclanthology.org/2021.eacl-main.156 - Varis & Bojar (2021), "Sequence Length is a Domain: Length-based Overfitting in Transformer Models," EMNLP. https://aclanthology.org/2021.emnlp-main.650 - Lake & Baroni (2018), "Generalization without Systematicity," ICML, introduced the SCAN length split (train short, test long) -- a canonical length-generalization test (on output sequence length, synthetic data). https://arxiv.org/abs/1711.00350 - Søgaard et al. trace the idea of testing generalization to longer sequences back to Siegelmann & Sontag (1992), "On the Computational Power of Neural Nets," COLT, pp. 440-449. https://doi.org/10.1145/130385.130432
Source code in splytters/sorters/text_sorters.py
tokens_length ¶
tokens_length(texts: list[str], low_first: bool = True, tokenizer: Callable[[str], list[str]] = simple_tokenizer) -> list[tuple[int, int]]
Sort texts by token count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
list of strings |
required |
low_first
|
bool
|
if True, fewest tokens first; if False, most tokens first |
True
|
tokenizer
|
Callable[[str], list[str]]
|
function that splits text into tokens, default whitespace split |
simple_tokenizer
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
List of (index, token_count) tuples sorted by token count. |
References
A token-count variant of length-based splitting; see character_length
for the motivation and references (Søgaard et al. 2021, EACL; Varis &
Bojar 2021, EMNLP; Lake & Baroni 2018, ICML; Siegelmann & Sontag 1992,
COLT).
Source code in splytters/sorters/text_sorters.py
sentence_count ¶
sentence_count(texts: list[str], language: str = 'en', low_first: bool = True) -> list[tuple[int, int]]
Sort texts by number of sentences.
Uses pysbd for robust sentence boundary detection across languages.
Useful for adversarial splits: train on single-sentence texts, test on multi-sentence/complex texts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
list of strings |
required |
language
|
str
|
language code for sentence segmentation (default 'en') |
'en'
|
low_first
|
bool
|
if True, fewer sentences first; if False, more sentences first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
List of (index, sentence_count) tuples sorted by sentence count. |
Source code in splytters/sorters/text_sorters.py
lexical_diversity ¶
lexical_diversity(texts: list[str], tokenizer: Callable[[str], list[str]] = simple_tokenizer, low_first: bool = True) -> list[tuple[int, float]]
Sort texts by lexical diversity (type-token ratio).
Type-token ratio = unique tokens / total tokens. Higher values indicate more diverse vocabulary; lower values indicate more repetitive text.
Useful for adversarial splits: train on repetitive/simple vocabulary, test on diverse/rich vocabulary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
list of strings |
required |
tokenizer
|
Callable[[str], list[str]]
|
function that splits text into tokens |
simple_tokenizer
|
low_first
|
bool
|
if True, repetitive texts first; if False, diverse texts first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, ttr) tuples sorted by type-token ratio. |
list[tuple[int, float]]
|
Texts with no tokens receive a score of 0. |
Source code in splytters/sorters/text_sorters.py
vocabulary_rarity ¶
vocabulary_rarity(texts: list[str], language: str = 'en', tokenizer: Callable[[str], list[str]] = simple_tokenizer, low_first: bool = True) -> list[tuple[int, float]]
Sort texts by average word rarity.
Uses word frequency data to score each word's rarity. Rarer words have lower frequency, so we use (1 - frequency) as the rarity score.
Useful for adversarial splits: train on common vocabulary, test on rare/specialized vocabulary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
list of strings |
required |
language
|
str
|
language code for frequency lookup (default 'en') |
'en'
|
tokenizer
|
Callable[[str], list[str]]
|
function that splits text into tokens |
simple_tokenizer
|
low_first
|
bool
|
if True, common vocabulary first; if False, rare vocabulary first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, avg_rarity) tuples sorted by average word rarity. |
list[tuple[int, float]]
|
Texts with no tokens receive a score of 0. |
Source code in splytters/sorters/text_sorters.py
perplexity_score ¶
perplexity_score(texts: list[str], model: GPT2LMHeadModel | None = None, tokenizer: GPT2TokenizerFast | None = None, low_first: bool = True, scoring: str = 'perplexity') -> list[tuple[int, float]]
Sort texts by how typical a language model finds them.
A causal LM is run over each text to measure how "surprised" it is. Two
scoring modes are offered (see scoring); both rank the same way — typical
text is ordered before unusual text — but they put different things in the
difficult tail, so the returned score values differ.
Useful for adversarial / curriculum splits: train on typical text, test on
unusual text. Pair with :func:splytters.sorted_stratified_split to build
"Likelihood Splits" (train on the high-likelihood head, test on the tail).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
list of strings to score |
required |
model
|
GPT2LMHeadModel | None
|
HuggingFace causal LM (defaults to GPT-2 if None) |
None
|
tokenizer
|
GPT2TokenizerFast | None
|
HuggingFace tokenizer (defaults to GPT-2 if None) |
None
|
low_first
|
bool
|
if True, typical/predictable texts first; if False, unusual/surprising texts first. The ordering is by typicality, so this holds for both scoring modes despite their opposite raw-value directions. |
True
|
scoring
|
str
|
how to score each text.
- |
'perplexity'
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of |
list[tuple[int, float]]
|
ordered by typicality per |
list[tuple[int, float]]
|
the most-difficult sentinel ( |
list[tuple[int, float]]
|
log-likelihood), so they always land in the tail. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
References
Godbole & Jia (2023), "Benchmarking Long-tail Generalization with
Likelihood Splits," Findings of the ACL: EACL, pp. 963-983 —
https://aclanthology.org/2023.findings-eacl.71 . They split datasets by
LM likelihood (head -> train, tail -> test). Note they deliberately use
total log-likelihood rather than perplexity: perplexity normalizes for
length and over-corrects toward short examples in the tail (their fn. 3).
Use scoring="log_likelihood" to reproduce their choice.
Source code in splytters/sorters/text_sorters.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
readability_score ¶
readability_score(texts: list[str], metric: str = 'flesch_kincaid', low_first: bool = True) -> list[tuple[int, float]]
Sort texts by readability score.
Readability scores estimate the education level required to understand text. Higher scores generally indicate more complex/difficult text.
Useful for adversarial splits: train on simple text, test on complex text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
list of strings to score |
required |
metric
|
str
|
readability formula to use, one of: - 'flesch_kincaid': Flesch-Kincaid Grade Level - 'flesch': Flesch Reading Ease (higher = easier, inverted scale) - 'gunning_fog': Gunning Fog Index - 'coleman_liau': Coleman-Liau Index - 'dale_chall': Dale-Chall Readability Score - 'ari': Automated Readability Index - 'linsear_write': Linsear Write Formula - 'smog': SMOG Index |
'flesch_kincaid'
|
low_first
|
bool
|
if True, easier/simpler texts first; if False, harder/complex texts first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, score) tuples sorted by readability. |
list[tuple[int, float]]
|
Texts too short to score receive a score of infinity. |
Source code in splytters/sorters/text_sorters.py
gzip_complexity ¶
Sort texts by gzip compression ratio — a model-free complexity proxy.
Redundant / repetitive / simple text compresses well (low ratio); varied,
information-dense text compresses poorly (high ratio). Needs no model, so
unlike :func:perplexity_score it runs without any heavy dependency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
texts
|
list[str]
|
list of strings. |
required |
low_first
|
bool
|
if True, the simplest (most compressible) texts first. |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, compression_ratio) tuples (compressed / raw bytes). |
Note
gzip carries ~18 bytes of header overhead, so the ratio is noisy for very short strings; it is most meaningful across texts of comparable length.
Source code in splytters/sorters/text_sorters.py
Image sorters¶
image_sorters ¶
Sorting algorithms for adversarial image dataset partitioning.
These functions rank images by visual properties (brightness, contrast, color, complexity) to enable train-test splits that maximize dissimilarity.
All functions accept either file paths or PIL Image objects.
mean_brightness ¶
Sort images by mean brightness (average pixel intensity).
Converts images to grayscale and computes mean intensity (0-255 scale).
Useful for adversarial splits: train on mid-tone images, test on very dark or very bright images.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
images
|
list[ImageInput]
|
list of file paths or PIL Image objects |
required |
low_first
|
bool
|
if True, darker images first; if False, brighter images first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, brightness) tuples sorted by mean brightness. |
Source code in splytters/sorters/image_sorters.py
contrast ¶
Sort images by contrast (standard deviation of pixel intensities).
Higher contrast means more variation between light and dark regions. Lower contrast means flatter, more uniform images.
Useful for adversarial splits: train on normal contrast images, test on very flat or very high contrast images.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
images
|
list[ImageInput]
|
list of file paths or PIL Image objects |
required |
low_first
|
bool
|
if True, low contrast (flat) images first; if False, high contrast images first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, contrast) tuples sorted by contrast. |
Source code in splytters/sorters/image_sorters.py
color_variance ¶
Sort images by color variance (spread across RGB channels).
Measures how much the RGB channels differ from each other. Higher variance indicates more colorful/saturated images. Lower variance indicates grayscale-like or desaturated images.
Useful for adversarial splits: train on neutral/desaturated images, test on highly colorful/saturated images.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
images
|
list[ImageInput]
|
list of file paths or PIL Image objects |
required |
low_first
|
bool
|
if True, desaturated images first; if False, colorful images first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, color_variance) tuples sorted by color variance. |
Source code in splytters/sorters/image_sorters.py
dominant_color ¶
dominant_color(images: list[ImageInput], n_clusters: int = 5, target_color: list[int] | None = None, low_first: bool = True, random_state: int = 42, method: str = 'quantize') -> list[tuple[int, float]]
Sort images by their dominant color.
Uses deterministic color quantization by default to find dominant colors,
then sorts by distance from a target color (default: neutral gray). Pass
method="kmeans" for the legacy k-means estimator.
Useful for adversarial splits: train on images with one color palette, test on images with different dominant colors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
images
|
list[ImageInput]
|
list of file paths or PIL Image objects |
required |
n_clusters
|
int
|
number of color clusters to find (default 5) |
5
|
target_color
|
list[int] | None
|
RGB tuple to measure distance from (default [128, 128, 128] gray) |
None
|
low_first
|
bool
|
if True, images closest to target color first; if False, images furthest from target first |
True
|
random_state
|
int
|
seed for the pixel subsample and k-means (default 42) |
42
|
method
|
str
|
dominant-color estimator, either "quantize" (deterministic, default) or "kmeans" (legacy k-means behavior) |
'quantize'
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, distance) tuples sorted by distance from target color. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Source code in splytters/sorters/image_sorters.py
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |
compression_ratio ¶
compression_ratio(images: list[ImageInput], quality: int = 85, low_first: bool = True) -> list[tuple[int, float]]
Sort images by JPEG compression ratio.
Simple/uniform images compress well (high ratio). Complex/detailed images compress poorly (low ratio).
Useful for adversarial splits: train on simple/compressible images, test on complex/incompressible images.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
images
|
list[ImageInput]
|
list of file paths or PIL Image objects |
required |
quality
|
int
|
JPEG quality setting for compression test (default 85) |
85
|
low_first
|
bool
|
if True, complex images first (low ratio); if False, simple images first (high ratio) |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, ratio) tuples sorted by compression ratio. |
list[tuple[int, float]]
|
Ratio = uncompressed size / compressed size. |
Source code in splytters/sorters/image_sorters.py
frequency_content ¶
Sort images by high-frequency content (texture/detail level).
Uses FFT to analyze frequency distribution. Higher values indicate more high-frequency content (edges, textures, fine details). Lower values indicate smoother, low-frequency images.
Useful for adversarial splits: train on smooth/simple images, test on textured/detailed images.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
images
|
list[ImageInput]
|
list of file paths or PIL Image objects |
required |
low_first
|
bool
|
if True, smooth/low-frequency images first; if False, textured/high-frequency images first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, high_freq_ratio) tuples sorted by frequency content. |
Source code in splytters/sorters/image_sorters.py
sharpness ¶
Sort images by sharpness — the variance of the Laplacian (a blur detector).
A blurry image has little high-frequency detail and a low Laplacian
variance; a sharp, detailed image has a high one. Distinct from
contrast / frequency_content in isolating edge focus.
Useful for adversarial / curriculum splits: train on sharp images, test on blurry ones (or vice versa).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
images
|
list[ImageInput]
|
list of file paths or PIL Image objects. |
required |
low_first
|
bool
|
if True, the blurriest images first. |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, laplacian_variance) tuples. |
Source code in splytters/sorters/image_sorters.py
Audio sorters¶
audio_sorters ¶
Sorting algorithms for adversarial audio dataset partitioning.
These functions rank audio samples by various criteria (loudness, spectral features, rhythm, timbre) to enable train-test splits that maximize dissimilarity.
All functions accept either file paths or pre-loaded audio arrays. Requires librosa for audio processing.
duration ¶
duration(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by total duration in seconds.
Useful for adversarial / curriculum splits: train on short utterances,
test on long ones (or vice versa). Pair with
:func:splytters.sorted_stratified_split for a length-based curriculum.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, shortest audio first; if False, longest first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, seconds) tuples sorted by duration. |
References
Liu, Spence & Prud'hommeaux (2023), "Investigating data partitioning strategies for crosslinguistic low-resource ASR evaluation," EACL, pp. 123-131. https://aclanthology.org/2023.eacl-main.10 . They build heuristic test sets from per-utterance features -- utterance duration, average intensity, average pitch, transcript token count and perplexity -- and find utterance duration and intensity to be the most predictive factors of word-error-rate variability across data splits. Caveat: in their very low-resource setting, heuristic and adversarial splits behaved much like random splits, which were the more reliable choice under data sparsity -- so prefer random/averaged splits when data is scarce.
Source code in splytters/sorters/audio_sorters.py
mean_amplitude ¶
mean_amplitude(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by mean absolute amplitude.
Useful for adversarial splits: train on mid-volume audio, test on very quiet or very loud audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, quieter audio first; if False, louder first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, amplitude) tuples sorted by mean amplitude. |
References
A loudness/intensity feature; see :func:duration for the
Liu, Spence & Prud'hommeaux (2023) ASR data-partitioning study, which
uses average intensity as a heuristic split feature.
Source code in splytters/sorters/audio_sorters.py
rms_energy ¶
rms_energy(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by root mean square energy.
RMS energy is a common measure of audio loudness that better correlates with perceived volume than simple amplitude.
Useful for adversarial splits: train on consistent energy levels, test on very quiet or very loud audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, lower energy first; if False, higher energy first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, rms) tuples sorted by RMS energy. |
References
RMS energy is an "average intensity" measure; see :func:duration for
the Liu, Spence & Prud'hommeaux (2023) ASR data-partitioning study, which
found intensity among the most predictive features of WER variability.
Source code in splytters/sorters/audio_sorters.py
dynamic_range ¶
dynamic_range(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by dynamic range (difference between max and min amplitude).
Higher dynamic range indicates more variation between loud and quiet parts. Lower dynamic range indicates more compressed/consistent audio.
Useful for adversarial splits: train on compressed audio, test on highly dynamic audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, compressed audio first; if False, dynamic audio first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, range) tuples sorted by dynamic range. |
Source code in splytters/sorters/audio_sorters.py
peak_to_average_ratio ¶
peak_to_average_ratio(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by peak-to-average ratio (crest factor).
Higher values indicate peaky/transient audio (e.g., percussion). Lower values indicate more sustained/compressed audio.
Useful for adversarial splits: train on sustained sounds, test on transient/percussive sounds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, sustained audio first; if False, peaky audio first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, ratio) tuples sorted by peak-to-average ratio. |
Source code in splytters/sorters/audio_sorters.py
spectral_centroid ¶
spectral_centroid(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by spectral centroid ("center of mass" of the spectrum).
Higher values indicate brighter, treble-heavy audio. Lower values indicate darker, bass-heavy audio.
Useful for adversarial splits: train on mid-frequency audio, test on very bright or very dark audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, darker audio first; if False, brighter audio first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, centroid_hz) tuples sorted by spectral centroid. |
Source code in splytters/sorters/audio_sorters.py
spectral_bandwidth ¶
spectral_bandwidth(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by spectral bandwidth (spread around the centroid).
Higher values indicate wider frequency spread (broadband). Lower values indicate narrower frequency content (tonal).
Useful for adversarial splits: train on narrow-band audio, test on broadband audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, narrow-band first; if False, broadband first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, bandwidth_hz) tuples sorted by spectral bandwidth. |
Source code in splytters/sorters/audio_sorters.py
spectral_rolloff ¶
spectral_rolloff(audios: list[AudioInput], sr: int = 22050, roll_percent: float = 0.85, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by spectral rolloff frequency.
The rolloff frequency is the frequency below which a specified percentage of total spectral energy lies.
Higher values indicate more high-frequency content. Lower values indicate more low-frequency content.
Useful for adversarial splits: train on one frequency profile, test on different frequency profiles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
roll_percent
|
float
|
energy percentage threshold (default 0.85) |
0.85
|
low_first
|
bool
|
if True, low rolloff first; if False, high rolloff first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, rolloff_hz) tuples sorted by spectral rolloff. |
Source code in splytters/sorters/audio_sorters.py
spectral_flatness ¶
spectral_flatness(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by spectral flatness (Wiener entropy).
Values close to 1 indicate noise-like audio (flat spectrum). Values close to 0 indicate tonal audio (peaked spectrum).
Useful for adversarial splits: train on tonal audio, test on noisy audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, tonal audio first; if False, noisy audio first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, flatness) tuples sorted by spectral flatness. |
Source code in splytters/sorters/audio_sorters.py
zero_crossing_rate ¶
zero_crossing_rate(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by zero crossing rate.
Higher values indicate noisier or more percussive audio. Lower values indicate smoother, more tonal audio.
Useful for adversarial splits: train on smooth audio, test on noisy/percussive audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, smooth audio first; if False, noisy audio first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, zcr) tuples sorted by zero crossing rate. |
Source code in splytters/sorters/audio_sorters.py
fundamental_frequency ¶
fundamental_frequency(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by estimated fundamental frequency (pitch).
Uses pYIN algorithm for robust pitch estimation.
Useful for adversarial splits: train on mid-pitch audio, test on very high or very low pitch audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, lower pitch first; if False, higher pitch first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, f0_hz) tuples sorted by fundamental frequency. |
list[tuple[int, float]]
|
Audio with no detectable pitch receives f0 of 0. |
References
Pitch is one of the per-utterance heuristic split features in
Liu, Spence & Prud'hommeaux (2023); see :func:duration for the full
citation.
Source code in splytters/sorters/audio_sorters.py
mfcc_mean ¶
mfcc_mean(audios: list[AudioInput], sr: int = 22050, n_mfcc: int = 13, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by mean of first MFCC coefficient.
The first MFCC coefficient relates to overall energy/loudness. Higher MFCCs capture timbral characteristics.
Useful for adversarial splits: separate by timbral characteristics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
n_mfcc
|
int
|
number of MFCC coefficients (default 13) |
13
|
low_first
|
bool
|
if True, lower mean MFCC first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, mfcc_mean) tuples sorted by mean of first MFCC. |
Source code in splytters/sorters/audio_sorters.py
mfcc_variance ¶
mfcc_variance(audios: list[AudioInput], sr: int = 22050, n_mfcc: int = 13, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by variance of MFCC coefficients over time.
Higher variance indicates more timbral variation over time. Lower variance indicates more consistent timbre.
Useful for adversarial splits: train on stable timbre, test on varying timbre.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
n_mfcc
|
int
|
number of MFCC coefficients (default 13) |
13
|
low_first
|
bool
|
if True, stable timbre first; if False, varying timbre first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, mfcc_var) tuples sorted by MFCC variance. |
Source code in splytters/sorters/audio_sorters.py
tempo ¶
Sort audio by estimated tempo (beats per minute).
Useful for adversarial splits: train on mid-tempo audio, test on very slow or very fast audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, slower tempo first; if False, faster tempo first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, bpm) tuples sorted by tempo. |
Source code in splytters/sorters/audio_sorters.py
beat_strength ¶
beat_strength(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by beat/onset strength.
Higher values indicate more percussive, rhythmically strong audio. Lower values indicate more ambient, less rhythmic audio.
Useful for adversarial splits: train on strong beat audio, test on ambient/weak beat audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, weak beats first; if False, strong beats first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, strength) tuples sorted by beat strength. |
Source code in splytters/sorters/audio_sorters.py
harmonic_ratio ¶
harmonic_ratio(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by harmonic-to-percussive ratio.
Higher values indicate more melodic/harmonic content. Lower values indicate more percussive/rhythmic content.
Useful for adversarial splits: train on melodic audio, test on percussive audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, percussive audio first; if False, harmonic audio first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, ratio) tuples sorted by harmonic ratio. |
Source code in splytters/sorters/audio_sorters.py
compression_ratio ¶
compression_ratio(audios: list[AudioInput], sr: int = 22050, low_first: bool = True) -> list[tuple[int, float]]
Sort audio by compression ratio (uncompressed / compressed size).
Simple/repetitive audio compresses well (high ratio). Complex/varied audio compresses poorly (low ratio).
Useful for adversarial splits: train on simple audio, test on complex audio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
audios
|
list[AudioInput]
|
list of file paths or (samples, sr) tuples |
required |
sr
|
int
|
sample rate for loading files (default 22050) |
22050
|
low_first
|
bool
|
if True, complex audio first; if False, simple audio first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, float]]
|
List of (index, ratio) tuples sorted by compression ratio. |
Source code in splytters/sorters/audio_sorters.py
Tabular sorters¶
tabular_sorters ¶
Sorting algorithms for adversarial tabular dataset partitioning.
These functions rank rows in tabular data (pandas DataFrames) by various criteria (missing values, outliers, sparsity, column values) to enable train-test splits that maximize dissimilarity.
All functions accept pandas DataFrames and return sorted index lists.
column_value ¶
Sort rows by values in a specified column.
Generic sorting function that allows users to sort by any column.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
column
|
str
|
column name to sort by |
required |
low_first
|
bool
|
if True, lowest values first; if False, highest values first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, Any]]
|
List of (index, value) tuples sorted by column value. |
list[tuple[Hashable, Any]]
|
NaN values are placed at the end. |
Source code in splytters/sorters/tabular_sorters.py
column_rank ¶
Sort rows by percentile rank within a column.
Useful when you want to split by relative position rather than absolute values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
column
|
str
|
column name to rank by |
required |
low_first
|
bool
|
if True, lowest percentile first; if False, highest first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, percentile) tuples sorted by percentile rank (0-100). |
Source code in splytters/sorters/tabular_sorters.py
column_zscore ¶
Sort rows by z-score (standard deviations from mean) in a column.
Useful for finding outliers in a specific column.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
column
|
str
|
column name to compute z-scores for |
required |
low_first
|
bool
|
if True, lowest z-scores first (below mean); if False, highest z-scores first (above mean) |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, zscore) tuples sorted by z-score. |
list[tuple[Hashable, float]]
|
NaN values receive z-score of infinity. |
Source code in splytters/sorters/tabular_sorters.py
column_absolute_zscore ¶
column_absolute_zscore(df: DataFrame, column: str, low_first: bool = True) -> list[tuple[Hashable, float]]
Sort rows by absolute z-score (distance from mean) in a column.
Useful for adversarial splits: train on typical values (low |z|), test on extreme values (high |z|).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
column
|
str
|
column name to compute z-scores for |
required |
low_first
|
bool
|
if True, typical values first; if False, extreme values first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, abs_zscore) tuples sorted by absolute z-score. |
Source code in splytters/sorters/tabular_sorters.py
missing_value_ratio ¶
Sort rows by proportion of missing (NaN/None) values.
Useful for adversarial splits: train on complete rows, test on rows with missing data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
low_first
|
bool
|
if True, complete rows first; if False, sparse rows first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, missing_ratio) tuples sorted by missing ratio (0-1). |
Source code in splytters/sorters/tabular_sorters.py
row_sparsity ¶
row_sparsity(df: DataFrame, zero_threshold: float = 1e-10, low_first: bool = True) -> list[tuple[Hashable, float]]
Sort rows by proportion of zero (or near-zero) values.
Useful for adversarial splits: train on dense rows, test on sparse rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame (numeric columns only) |
required |
zero_threshold
|
float
|
values with abs < threshold are considered zero |
1e-10
|
low_first
|
bool
|
if True, dense rows first; if False, sparse rows first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, sparsity) tuples sorted by sparsity ratio (0-1). |
Source code in splytters/sorters/tabular_sorters.py
outlier_score ¶
outlier_score(df: DataFrame, method: str = 'isolation_forest', low_first: bool = True, random_state: int = 42, **kwargs: Any) -> list[tuple[Hashable, float]]
Sort rows by anomaly/outlier score.
Uses outlier detection algorithms to score how unusual each row is.
Useful for adversarial splits: train on normal/typical rows, test on outliers/anomalies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame (numeric columns only, NaN will be filled with median) |
required |
method
|
str
|
outlier detection algorithm, one of: - 'isolation_forest': Isolation Forest (fast, good for high dimensions) - 'lof': Local Outlier Factor (density-based) - 'zscore': Mean absolute z-score across columns |
'isolation_forest'
|
low_first
|
bool
|
if True, normal rows first; if False, outliers first |
True
|
random_state
|
int
|
seed for isolation_forest (ignored by lof/zscore).
Exposed separately so it can't collide with a |
42
|
**kwargs
|
Any
|
additional arguments passed to the outlier detector |
{}
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, outlier_score) tuples sorted by outlier score. |
Source code in splytters/sorters/tabular_sorters.py
numerical_range_score ¶
Sort rows by how extreme their numerical values are (distance from median).
Computes the mean percentile distance from 50th percentile across all numeric columns.
Useful for adversarial splits: train on typical values, test on extreme values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
low_first
|
bool
|
if True, typical rows first; if False, extreme rows first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, extremity) tuples sorted by extremity score (0-50). |
Source code in splytters/sorters/tabular_sorters.py
categorical_rarity ¶
categorical_rarity(df: DataFrame, column: str, low_first: bool = True) -> list[tuple[Hashable, float]]
Sort rows by rarity of categorical value in a column.
Useful for adversarial splits: train on common categories, test on rare categories.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
column
|
str
|
categorical column name |
required |
low_first
|
bool
|
if True, common categories first; if False, rare categories first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, frequency) tuples sorted by category frequency. |
list[tuple[Hashable, float]]
|
Higher frequency = more common. |
Note
Unlike most sorters (whose score is a difficulty/rarity measure
where low_first orders ascending), the score here is frequency
(high = common). low_first=True therefore orders by commonness —
common categories first — which is the natural "train on common,
test on rare" direction for an adversarial split.
Source code in splytters/sorters/tabular_sorters.py
feature_entropy ¶
Sort rows by entropy of categorical features.
Rows with rare combinations of categorical values have higher "entropy" in the sense that they're less predictable.
Approximated by summing -log(freq) for each categorical value.
Useful for adversarial splits: train on common patterns, test on rare patterns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
low_first
|
bool
|
if True, common patterns first; if False, rare patterns first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, neg_log_freq) tuples sorted by rarity score. |
Source code in splytters/sorters/tabular_sorters.py
row_distance_to_mean ¶
Sort rows by Euclidean distance from the column-wise mean.
Similar to embedding distance_to_mean but operates directly on tabular features.
Useful for adversarial splits: train on typical rows, test on atypical rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame (numeric columns only) |
required |
low_first
|
bool
|
if True, typical rows first; if False, atypical rows first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, distance) tuples sorted by distance from mean. |
Source code in splytters/sorters/tabular_sorters.py
multi_column_sort ¶
multi_column_sort(df: DataFrame, columns: list[str], weights: list[float] | None = None, low_first: bool = True) -> list[tuple[Hashable, float]]
Sort rows by weighted combination of multiple columns.
Useful when you want to combine multiple criteria into a single sort.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
pandas DataFrame |
required |
columns
|
list[str]
|
list of column names to sort by |
required |
weights
|
list[float] | None
|
list of weights for each column (default: equal weights) Positive weight = higher value increases score Negative weight = higher value decreases score |
None
|
low_first
|
bool
|
if True, lowest combined score first |
True
|
Returns:
| Type | Description |
|---|---|
list[tuple[Hashable, float]]
|
List of (index, combined_score) tuples sorted by combined score. |
Source code in splytters/sorters/tabular_sorters.py
Utilities¶
utils ¶
Shared utilities for splitting algorithms.
accepts_random_state ¶
Whether fn takes a random_state keyword (directly or via **kwargs).
Used to decide whether to forward random_state to a user-supplied
splitter: passing it to one that doesn't accept it raises TypeError.
Source code in splytters/utils.py
to_numpy ¶
Best-effort conversion of framework tensors to numpy.
Handles PyTorch tensors (including those on GPU / requiring grad) by
detaching, moving to CPU, and converting. Any other input is returned
unchanged so the caller's downstream check_array/np.asarray can
handle lists, numpy arrays, pandas DataFrames, etc.
Source code in splytters/utils.py
validate_split_inputs ¶
validate_split_inputs(embeddings: ArrayLike, train_size: float | int, min_samples: int = 2) -> np.ndarray
Validate and coerce inputs shared by every splitting function.
Accepts any array-like (numpy, list, pandas, torch tensor), converts it to
a finite 2-D float ndarray, and validates train_size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, n_features). |
required |
train_size
|
float | int
|
fraction in the open interval (0, 1), or an absolute count
in |
required |
min_samples
|
int
|
minimum number of samples required to form a split (default 2). |
2
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The validated, finite, float embeddings as an ndarray of shape |
ndarray
|
(n_samples, n_features). |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Source code in splytters/utils.py
resolve_n_train ¶
Resolve train_size (fraction or absolute count) to an int count.
A fractional train_size is clamped to [1, n_samples - 1] so the
resolved count never collapses one side of the split to empty (e.g.
n_samples=2, train_size=0.3 would otherwise truncate to 0).
Source code in splytters/utils.py
apportion_train ¶
Split n_train_total train slots across bins via largest-remainder.
Each bin gets floor(size * n_train_total / total) slots, then the
leftover slots go to the bins with the largest fractional remainders. The
per-bin counts sum exactly to n_train_total (clamped to [0, total]),
avoiding both the per-bin int() truncation bias that undershoots the
requested train fraction and the all-to-train rounding that can empty the
test set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bin_sizes
|
list[int]
|
number of samples in each (non-empty) bin. |
required |
n_train_total
|
int
|
total samples to assign to train across all bins. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Integer ndarray of per-bin train counts, aligned with |
Source code in splytters/utils.py
as_index_array ¶
compute_pairwise_distances ¶
Compute pairwise distance matrix.
.. warning:: Materializes a full O(n²) matrix. For large datasets, prefer NearestNeighbors or chunked computation.
Source code in splytters/utils.py
kneighbors_excluding_self ¶
kneighbors_excluding_self(X: ArrayLike, k: int, metric: str = 'euclidean') -> tuple[np.ndarray, np.ndarray]
Return each sample's k nearest other samples.
NearestNeighbors.kneighbors(X) does not guarantee that the query sample
appears in column zero when several points tie at distance zero. Query one
extra neighbor, remove the query by its index, and only then take the first
k results. This keeps duplicate embeddings correct without materializing
an n x n distance matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
ArrayLike
|
array-like of shape |
required |
k
|
int
|
positive number of non-self neighbors, clamped to |
required |
metric
|
str
|
metric accepted by :class: |
'euclidean'
|
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
|
Source code in splytters/utils.py
compute_centroid ¶
compute_split_centroids ¶
compute_split_centroids(X: ArrayLike, train_indices: ArrayLike, test_indices: ArrayLike) -> tuple[np.ndarray | None, np.ndarray | None]
Compute centroids of train and test sets.
Source code in splytters/utils.py
cluster_embeddings ¶
cluster_embeddings(X: ArrayLike, n_clusters: int = 10, method: str = 'kmeans', random_state: int = 42, **kwargs: Any) -> tuple[np.ndarray, dict[int, list[int]], np.ndarray]
Cluster embeddings and return labels and cluster info.
Returns:
| Name | Type | Description |
|---|---|---|
labels |
ndarray
|
cluster label for each sample |
cluster_to_indices |
dict[int, list[int]]
|
dict mapping cluster_id to list of indices |
cluster_centers |
ndarray
|
cluster centroids (for kmeans) |
Source code in splytters/utils.py
random_split ¶
random_split(embeddings: ArrayLike, train_size: float | int = 0.7, random_state: int = 42) -> tuple[np.ndarray, np.ndarray]
Simple random train/test split (baseline).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ArrayLike
|
array-like of shape (n_samples, embedding_dim) |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
0.7
|
random_state
|
int
|
int, RandomState, or None for reproducibility |
42
|
Returns:
| Name | Type | Description |
|---|---|---|
train_indices |
ndarray
|
ndarray of indices for training set |
test_indices |
ndarray
|
ndarray of indices for test set |
Seed stability: fully random -- every seed gives a different train/test split; this is the baseline the other splitters' seed stability is measured against.
Source code in splytters/utils.py
compute_split_similarity ¶
compute_split_similarity(X: ArrayLike, train_indices: ArrayLike, test_indices: ArrayLike, metric: str = 'euclidean') -> dict[str, float]
Compute similarity metrics between train and test splits.
Returns dict with
- centroid_distance: distance between train/test centroids
- mean_cross_distance: mean distance from test to nearest train
- coverage: fraction of test samples with train neighbor within median distance
Source code in splytters/utils.py
optimized_split ¶
optimized_split(embeddings: ndarray, train_size: float | int, n_iterations: int, score_fn: Callable[[ndarray, list[int], list[int]], float], random_state: int = 42, minimize: bool = True) -> tuple[np.ndarray, np.ndarray]
Iterative swap-optimization to find a split that optimizes a score.
Starts from a random split and repeatedly swaps one train/test pair, keeping the swap only if the score improves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ndarray
|
array of shape (n_samples, embedding_dim), already np.ndarray |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set |
required |
n_iterations
|
int
|
number of swap attempts |
required |
score_fn
|
Callable[[ndarray, list[int], list[int]], float]
|
callable(embeddings, train_indices, test_indices) -> float |
required |
random_state
|
int
|
int, RandomState, or None for reproducibility |
42
|
minimize
|
bool
|
if True, accept swaps that lower the score; if False, accept swaps that raise it |
True
|
Source code in splytters/utils.py
optimized_mmd_split ¶
optimized_mmd_split(embeddings: ndarray, train_size: float | int, n_iterations: int, *, kernel: str = 'rbf', gamma: float | None = None, random_state: int = 42, minimize: bool = True) -> tuple[np.ndarray, np.ndarray]
Optimize biased empirical MMD with exact incremental swap updates.
The kernel matrix is invariant across swap attempts, so compute it once and
maintain the train/train, test/test, and train/test kernel sums. A proposed
swap is then scored from row sums instead of rebuilding three kernel matrices.
Accepted swaps update those row sums in O(n); rejected swaps preserve the
historical set-mutation order so seeded results stay backward compatible.
Source code in splytters/utils.py
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 | |
constrained_kernel_kmeans_split ¶
constrained_kernel_kmeans_split(embeddings: ndarray, train_size: float | int, kernel: str = 'rbf', gamma: float | None = None, y: ArrayLike | None = None, groups: ArrayLike | None = None, random_state: int = 42, n_iterations: int = 100, maximize_mmd: bool = True) -> tuple[np.ndarray, np.ndarray]
Constrained kernel k-means (k=2) train/validation split via an LP.
Full-kernel, paper-derived implementation of the partitioning method of Napoli & White (TMLR 2025; arXiv 2024), "Clustering-Based Validation Splits for Model Selection under Domain Shift" (https://openreview.net/forum?id=Q692C0WtiD).
The paper (their Theorem 1, Eq. 8-11) proves that maximizing the MMD between
the two sets is equivalent to minimizing the kernel k-means objective
Psi(T, V) = SSq(T) + SSq(V) with k = 2 (the within-cluster kernel
scatter), because MMD^2 = c * (SSq(S) - Psi(T, V)) with SSq(S) and the
cluster sizes held constant. It solves the constrained clustering with a
Lloyd-style alternation (their Algorithm 1):
- Distance update -- kernel distance from every point
ito each cluster centroidjvia the kernel trick,D_ij = K_ii - (2 / s_j) * sum_l U_lj K_il + (1 / s_j^2) * sum_lm U_lj U_mj K_lmwheres_j = sum_l U_ljis the cluster mass. - Constrained assignment -- solve the assignment LP (their Eq. 12-16)
argmin_U sum_ij U_ij D_ijsubject tosum_j U_ij = 1(Eq. 14), the group-balance equalitiessum_{i in g} U_ij = round(h |g|)for every groupgand either clusterj(the disjunction in Eq. 15), and the relaxed box constraint0 <= U_ij <= 1(Eq. 16). As prescribed by the paper, both cluster orientations are solved and the lower-cost LP is kept. The paper drops the integrality constraint (Eq. 13) because the constraint matrix meets Hoffman's total-unimodularity conditions, so the LP has integral optima.
Groups g are the Cartesian product of the label y and groups
inputs (their Y x D); with neither, there is a single group and Eq. 15
reduces to the size constraint alone. The holdout fraction h is derived
from train_size; per-group targets are apportioned with largest-remainder
so they sum exactly to the requested validation size.
Rounding: even though the LP optima are (near-)integral, fractional values can
appear under solver degeneracy, so each iteration rounds by selecting, within
every group, the round(h |g|) points with the largest validation weight
U[:, 1]. This guarantees an exact, feasible hard partition.
Convergence (their Proposition 2): Psi is bounded below and monotone under
the alternation and there are finitely many partitions, so the max-MMD
iteration (full-step assignment) is run until the hard assignment stops
changing (capped at n_iterations), keeping the best partition seen.
The min-MMD case (maximize_mmd=False) is the library's dual, not part of
the paper. MMD^2 is convex in the relaxed assignment, so minimizing it
is a convex program: a full assignment step oscillates between the two
segregated vertices, so this uses a damped Frank-Wolfe update
v <- (1 - gamma) v + gamma * u* with gamma = 2 / (k + 2) (u* the
LP vertex), then rounds the fractional membership at the end. The rounded
result lands far below a random split's MMD but, in practice, typically
around 2x the MMD reached by the swap optimizer -- prefer the swap method
when the absolute lowest MMD matters and this path when the label/group
constraints do.
Fidelity notes: the paper's Nyström scaling (Algorithm 1, Step 1; an
O(qn) random submatrix approximation for very large n) is not
implemented here, so this helper materializes the full n x n kernel and
is intended for moderate n. It also uses largest-remainder apportionment
rather than independently rounding every group target, guaranteeing that the
group constraints sum to the caller's exact requested validation size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
ndarray
|
validated float ndarray of shape (n_samples, n_features). |
required |
train_size
|
float | int
|
fraction in (0, 1) or absolute count for the training set. |
required |
kernel
|
str
|
'rbf' or 'linear'. |
'rbf'
|
gamma
|
float | None
|
RBF kernel parameter (default: 1 / n_features). |
None
|
y
|
ArrayLike | None
|
optional labels; enforces per-label proportions in both sides. |
None
|
groups
|
ArrayLike | None
|
optional group ids; enforces per-group proportions in both sides. |
None
|
random_state
|
int
|
seeds the initial random (feasible) partition. |
42
|
n_iterations
|
int
|
maximum Lloyd-style iterations. |
100
|
maximize_mmd
|
bool
|
if True, minimize the kernel k-means scatter (max-MMD, the paper's objective); if False, maximize the scatter (the min-MMD dual, an anti-clustering that makes the two sets resemble each other). |
True
|
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
(train_indices, test_indices) as sorted 1-D integer ndarrays. |
Source code in splytters/utils.py
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 | |
greedy_assign_to_target ¶
greedy_assign_to_target(items_with_sizes: list[tuple[int, int]], target_size: int) -> tuple[list[int], list[int]]
Greedily assign items to reach target size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items_with_sizes
|
list[tuple[int, int]]
|
list of (item_id, size) tuples |
required |
target_size
|
int
|
target total size |
required |
Returns:
| Name | Type | Description |
|---|---|---|
selected |
list[int]
|
list of item_ids assigned |
remaining |
list[int]
|
list of item_ids not assigned |
Source code in splytters/utils.py
Embedders¶
embedders ¶
Embedder ¶
Bases: ABC
Base class for all embedders.
Source code in splytters/embedders.py
embed
abstractmethod
¶
TextEmbedder ¶
Bases: Embedder
Embed text using a SentenceTransformer model.
Source code in splytters/embedders.py
CLIPTextEmbedder ¶
Bases: Embedder
Embed text using a CLIP model.
Source code in splytters/embedders.py
CLIPImageEmbedder ¶
Bases: Embedder
Embed images using a CLIP model.
Source code in splytters/embedders.py
OpenAIEmbedder ¶
Bases: Embedder
Embed text using the OpenAI embeddings API.
Source code in splytters/embedders.py
list_embedders ¶
Return the names of the available concrete embedder classes.
These live in :mod:splytters.embedders (the [embedders] extra). The
underlying model libraries (sentence-transformers, transformers, openai)
are imported lazily only when an embedder is constructed, so listing pulls
in no optional dependency.
Returns:
| Type | Description |
|---|---|
list[str]
|
Embedder class names, e.g. ``["TextEmbedder", "CLIPTextEmbedder", |
list[str]
|
"CLIPImageEmbedder", "OpenAIEmbedder"]``. |
Source code in splytters/embedders.py
Introspection¶
list_splitters ¶
Return the names of all available splitter functions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
by_family
|
bool
|
if True, return a dict mapping each family ("adversarial", "overlap", "balanced", "baseline", "grouped") to its list of splitter names. If False (default), return a flat list of every splitter name, in family order. |
False
|
Returns:
| Type | Description |
|---|---|
list[str] | dict[str, list[str]]
|
A flat list of names, or a dict of family -> names. |
Source code in splytters/__init__.py
list_sorters ¶
Return the names of all available sorters.
Listing pulls in no optional dependency — only the names are returned; the sorters themselves stay lazily imported until first accessed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
by_modality
|
bool
|
if True, return a dict mapping each modality ("embedding", "text", "image", "audio", "tabular") to its sorted list of sorter names. If False (default), return a flat sorted list of every sorter name. |
False
|
Returns:
| Type | Description |
|---|---|
list[str] | dict[str, list[str]]
|
A sorted list of names, or a dict of modality -> sorted names. |
Source code in splytters/sorters/__init__.py
list_embedders ¶
Return the names of the available concrete embedder classes.
These live in :mod:splytters.embedders (the [embedders] extra). The
underlying model libraries (sentence-transformers, transformers, openai)
are imported lazily only when an embedder is constructed, so listing pulls
in no optional dependency.
Returns:
| Type | Description |
|---|---|
list[str]
|
Embedder class names, e.g. ``["TextEmbedder", "CLIPTextEmbedder", |
list[str]
|
"CLIPImageEmbedder", "OpenAIEmbedder"]``. |