ML Foundations
Make NumPy top-k results deterministic when scores tie
argpartition selects a boundary efficiently, but tied values can leave membership or order unspecified. Add an explicit key when repeatable top-k output matters.
Evidence and testing
Review record: Tensor Almanac automated evidence gate (no independent human review). Example versions, recorded outputs and limitations appear below.
Partial selection does not decide every tie
Top-k selection is often written with `np.argpartition` because it can put the requested boundary into its sorted position without sorting the whole array. NumPy documents, however, that the order inside the partitions is undefined. When equal scores straddle the boundary, the API does not define which tied item belongs in the selected k. [1]
Sorting only the selected indices afterward can order the items that survived selection, but it cannot restore a tied item that was left outside the selected subset. If downstream code treats top-k as a stable shortlist, this boundary detail can make repeated results surprising.
References: [1]
Choose a tie rule that belongs to the task
Decide what should happen when two scores are equal. For predictions, a stable class or record identifier may be a reasonable secondary key; in another workflow, the application may require preserving input order. The rule should reflect the intended behavior rather than an incidental order produced by a partial-selection algorithm.
The example uses ascending item ID as its explicit tie-break after descending score. `np.lexsort` uses its last key as the primary key, so `(-scores, item_ids)` ranks higher scores first and smaller IDs first for ties. [2]
Executed with Python 3.12 and NumPy 2.3.5; bounded five-score synthetic example; exact stdout and source-byte SHA-256 are recorded in the private review.
import numpy as np
scores = np.array([0.92, 0.81, 0.92, 0.40, 0.81])
item_ids = np.array([104, 102, 101, 105, 103])
k = 3
partial = np.argpartition(scores, -k)[-k:]
partial_scores = partial[np.argsort(-scores[partial], kind="stable")]
deterministic = np.lexsort((item_ids, -scores))[:k]
print("scores:", scores.tolist())
print("partial selection, then score sort:", partial_scores.tolist())
print("partial boundary IDs:", sorted(item_ids[partial_scores].tolist()))
print("score then ID tie-break:", deterministic.tolist())
print("selected IDs:", item_ids[deterministic].tolist())
print("selected scores:", scores[deterministic].tolist())
Recorded output
scores: [0.92, 0.81, 0.92, 0.4, 0.81] partial selection, then score sort: [0, 2, 4] partial boundary IDs: [101, 103, 104] score then ID tie-break: [2, 0, 1] selected IDs: [101, 104, 102] selected scores: [0.92, 0.92, 0.81]
Pay for a full ordering when repeatability matters
The explicit lexicographic sort examines the whole score array, so it gives up the usual partial-selection advantage. That can be acceptable for small outputs or when reproducible membership matters more than selection cost. For large arrays where partial selection is necessary, boundary ties need a separate policy; sorting the selected subset alone is not enough.
Stable sorting preserves the relative order of equal values, but that only helps if the input order itself is meaningful and repeatable. NumPy documents the stability option for `argsort`; a stable sort does not invent a domain-specific tie-break for you. [3]