A clustering dashboard can be wrong while the clustering behind it is unchanged. Fit one KMeans estimator and label the left-hand group segment-alpha and the right-hand group segment-beta. Fit an equivalent estimator with the same four points, the same two geometric groups and the same inertia, then hand its numeric outputs to the old integer-to-name dictionary. The dashboard can now display the opposite names for every row.
That is not evidence that K-Means changed its statistical conclusion. It is evidence that the consumer treated a center index as a durable business identifier. A 0 or 1 has meaning only through the fitted estimator and the mapping reviewed with that estimator. When those artifacts become detached, partition agreement and business interpretation diverge.
This playbook uses a deliberately small synthetic fixture: four fixed float64 points, two explicit initialization arrays and two fits whose centers are identical but reversed. The KMeans cluster label permutation is controlled rather than discovered by seed hunting. It proves partition equivalence independently of numeric label agreement, demonstrates the defective lookup, and admits one narrowly bounded repair: an exact, one-to-one permutation of distinct centers under an unchanged feature schema. It then rejects drift, duplicate-center ambiguity, a stale model/mapping pair and a changed schema order.
The result is not a universal cluster-tracking method. It is a deterministic qualification contract for the special case in which fitted geometry is exactly unchanged and only center order differs.
Define the consumer meaning before fitting a model
The feature contract is the ordered pair ("coordinate_x", "coordinate_y"). That order is part of the fixture, not descriptive decoration. The points are interpreted in exactly those coordinates:
Row | coordinate_x | coordinate_y | Authored semantic output |
P1 | 0.0 | 0.0 | segment-alpha |
P2 | 0.0 | 2.0 | segment-alpha |
P3 | 10.0 | 0.0 | segment-beta |
P4 | 10.0 | 2.0 | segment-beta |
The names are fixture labels. They are not inferred customer traits, discovered personas or claims about a production population. Before either estimator is inspected, the semantic oracle is therefore:
[segment-alpha, segment-alpha, segment-beta, segment-beta]
That separation matters. The statistical task asks which observations share a cluster. The application task asks which reviewed name the consumer should emit for each cluster produced by one particular fitted state. A partition metric can answer the first question without answering the second.
This article assumes the reader is comfortable with arrays, vectorized comparisons and deterministic test fixtures. Readers building that base can review numerical Python foundations, but the present contract is intentionally narrower: no library comparison, no feature engineering and no attempt to discover segment meaning from the coordinates.
Record the deterministic geometry and estimator settings
The data matrix is [[0,0],[0,2],[10,0],[10,2]]. Both fits request two clusters, n_init=1, Lloyd iterations, max_iter=100 and tol=1e-6. Model A starts from [[0,0],[10,0]]; Model B starts from [[10,0],[0,0]]. The random_state=0 value is recorded for completeness, but it is not a promise of stable KMeans cluster identity. The scikit-learn 1.8 KMeans reference, consulted on 2026-10-14, documents array-like init as the supplied initial centers, n_init as the number of runs, and random_state as control over randomness in centroid initialization. It also defines cluster_centers_, labels_ and inertia_ as fitted attributes.
The supplied commissioning record identifies Python 3.13.5 with NumPy 2.3.5, SciPy 1.17.0 and scikit-learn 1.8.0. Keep installed versions separate from documentation headers. The commissioning source notes record a 1.8.0rc1 header for one retrieval of the ARI page. The linked adjusted_rand_score reference displayed 1.8.0 when checked on 2026-10-14. That header difference does not change the runtime package version recorded for the laboratory.
For broader context on where clustering sits among learning methods, see an introduction to machine-learning algorithm families. This fixture does not choose an algorithm or choose k; it qualifies one output contract.
Use reversed initialization as a controlled cause
The laboratory does not search random seeds until a visually convenient swap appears. Reversing two explicit initial-center rows supplies a controlled cause for reversed center order while preserving the obvious separated geometry. That makes the test useful for contract diagnosis: all input points and settings remain fixed, and the intended difference is the order in which equivalent centers appear.
The exact fitted arrays are still observations from the recorded versions, not a universal promise about every implementation or future release. A new run must record its own environment, source identity and result ledger.
Run the complete ten-case identity fixture
Use Python 3.13.5 to match the commissioning interpreter. The command name python3.13 alone does not identify a patch release, so inspect the printed version. In a writable working directory, save the complete driver below as kmeans_identity_fixture.py. Create an isolated environment, install the pinned direct packages with their declared dependencies, and record the installed environment before running the model assertions:
python3.13 -m venv .venv
. .venv/bin/activate
python -m pip install numpy==2.3.5 scipy==1.17.0 scikit-learn==1.8.0
python -m pip check
python -m pip freeze > dependency-lock.txt
python - <<'PY'
import platform
import numpy
import scipy
import sklearn
print("python", platform.python_version())
print("numpy", numpy.__version__)
print("scipy", scipy.__version__)
print("sklearn", sklearn.__version__)
PY
python kmeans_identity_fixture.pyInstallation and dependency checks are not evidence of fitted outputs. The driver uses threadpool_limits(limits=1) to limit supported native thread pools while running the fits and checks. It creates a fresh evidence directory beside the source, records actual versions and hashes the source bytes. After report initialization, its finally block attempts to write results.json even when a case raises. Cases already appended remain available, provided the process reaches that block and the write succeeds. Import failures, directory-creation failures and abrupt process termination are not covered by that retention path.
Keep this known-answer manifest beside the test plan rather than generating it from the candidate estimator:
{
"schema": ["coordinate_x", "coordinate_y"],
"points": [[0, 0], [0, 2], [10, 0], [10, 2]],
"expected_names": ["segment-alpha", "segment-alpha",
"segment-beta", "segment-beta"],
"model_a_centers": [[0, 1], [10, 1]],
"model_a_labels": [0, 0, 1, 1],
"model_b_centers": [[10, 1], [0, 1]],
"model_b_labels": [1, 1, 0, 0],
"expected_inertia": 4,
"probe_points": [[0, 1], [10, 1], [1, 1], [9, 1]],
"probe_names": ["segment-alpha", "segment-beta",
"segment-alpha", "segment-beta"],
"ari": 1,
"numeric_agreement": 0,
"co_membership_equal": true
}The source digest identifies the executed bytes; changing a comment, line ending or assertion requires a new report with the new digest. Thread limits do not imply byte-identical reports across platforms. Preserve environment fields, case rows and timestamps together. The temporary evidence directory is not automatically removed by this driver. Retain it until review is complete, and distinguish its observed case results from the separate known-answer manifest.
"""A same-center permutation laboratory, not general cluster-lineage tracking."""
import hashlib
import json
import platform
import tempfile
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import scipy
import sklearn
from scipy.optimize import linear_sum_assignment
from sklearn.cluster import KMeans
from sklearn.metrics import adjusted_rand_score
from threadpoolctl import threadpool_limits
X = np.array([[0., 0.], [0., 2.], [10., 0.], [10., 2.]])
SCHEMA = ("coordinate_x", "coordinate_y")
NAMES = ("segment-alpha", "segment-beta")
EXPECTED_NAMES = ["segment-alpha", "segment-alpha", "segment-beta", "segment-beta"]
def require(ok, message):
if not ok:
raise RuntimeError(message)
def fingerprint(model, schema):
# A narrow state stamp for this fixture, not an authenticated model signature.
value = {"centers": model.cluster_centers_.tolist(), "schema": list(schema)}
raw = json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def align(reference, candidate, names):
if (reference.ndim != 2 or candidate.shape != reference.shape or
len(names) != len(reference) or not np.isfinite(reference).all() or
not np.isfinite(candidate).all()):
raise ValueError("Incompatible centers or semantic manifest")
equal = (reference[:, None, :] == candidate[None, :, :]).all(axis=2)
if not (np.all(equal.sum(axis=0) == 1) and np.all(equal.sum(axis=1) == 1)):
raise ValueError("Not an unambiguous exact center permutation")
cost = ((reference[:, None, :] - candidate[None, :, :]) ** 2).sum(axis=2)
old_ids, new_ids = linear_sum_assignment(cost)
if len(old_ids) != len(reference) or not np.all(cost[old_ids, new_ids] == 0):
raise ValueError("Assignment violates exact-permutation contract")
mapped = [None] len(reference)
for old_id, new_id in zip(old_ids, new_ids, strict=True):
mapped[int(new_id)] = names[int(old_id)]
return tuple(mapped)
def serve(model, bundle, schema, points):
if tuple(schema) != tuple(bundle["schema"]) or fingerprint(model, schema) != bundle["state"]:
raise ValueError("Model and semantic manifest are not the reviewed pair")
return [bundle["names"][int(label)] for label in model.predict(points)]
def expect_rejection(row, operation):
try:
operation()
except ValueError as error:
row["exception"] = type(error).__name__
row["reason"] = str(error)
return
raise RuntimeError("Required rejection did not occur")
def main():
source = Path(__file__).resolve()
directory = Path(tempfile.mkdtemp(prefix="kmeans-identity-", dir=source.parent))
report = {"fixture": "kmeans-identity-v1", "status": "FAIL", "cases": [],
"started_utc": datetime.now(timezone.utc).isoformat(),
"python": platform.python_version(), "platform": platform.platform(),
"numpy": np.__version__, "scipy": scipy.__version__, "sklearn": sklearn.__version__,
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest()}
def case(identity, operation):
row = {"id": identity, "status": "FAIL"}
report["cases"].append(row)
operation(row)
row["status"] = "PASS"
def check_fit(row, model, centers, labels):
row.update(centers=model.cluster_centers_.tolist(), labels=model.labels_.tolist(),
inertia=float(model.inertia_), iterations=int(model.n_iter_))
require(np.array_equal(model.cluster_centers_, centers), "Wrong centers")
require(model.labels_.tolist() == labels and model.inertia_ == 4., "Wrong labels/inertia")
require(model.predict(X).tolist() == labels, "Training prediction mismatch")
try:
with threadpool_limits(limits=1):
options = dict(n_clusters=2, n_init=1, random_state=0, algorithm="lloyd",
max_iter=100, tol=1e-6)
a = KMeans(init=np.array([[0., 0.], [10., 0.]]), *options).fit(X)
b = KMeans(init=np.array([[10., 0.], [0., 0.]]), **options).fit(X)
case("K1", lambda r: check_fit(r, a, [[0., 1.], [10., 1.]], [0, 0, 1, 1]))
case("K2", lambda r: check_fit(r, b, [[10., 1.], [0., 1.]], [1, 1, 0, 0]))
def partition(row):
row["ari"] = adjusted_rand_score(a.labels_, b.labels_)
row["numeric_agreement"] = float(np.mean(a.labels_ == b.labels_))
same_a = a.labels_[:, None] == a.labels_[None, :]
same_b = b.labels_[:, None] == b.labels_[None, :]
row["co_membership_equal"] = bool(np.array_equal(same_a, same_b))
require(row["ari"] == 1. and row["numeric_agreement"] == 0. and
row["co_membership_equal"], "Partition control failed")
case("K3", partition)
def naive(row):
row["names"] = [NAMES[int(label)] for label in b.labels_]
require(row["names"] == ["segment-beta", "segment-beta", "segment-alpha", "segment-alpha"],
"Wrong negative control")
require(row["names"] != EXPECTED_NAMES, "Naive semantic map unexpectedly accepted")
case("K4", naive)
mapped = align(a.cluster_centers_, b.cluster_centers_, NAMES)
bundle = {"schema": SCHEMA, "state": fingerprint(b, SCHEMA), "names": mapped}
def aligned(row):
row["names_by_label"] = list(mapped)
row["served_names"] = serve(b, bundle, SCHEMA, X)
require(mapped == ("segment-beta", "segment-alpha") and
row["served_names"] == EXPECTED_NAMES, "Aligned semantics mismatch")
case("K5", aligned)
def probes(row):
points = np.array([[0., 1.], [10., 1.], [1., 1.], [9., 1.]])
row["names"] = serve(b, bundle, SCHEMA, points)
require(row["names"] == ["segment-alpha", "segment-beta", "segment-alpha", "segment-beta"],
"Probe mismatch")
case("K6", probes)
case("K7", lambda r: expect_rejection(r, lambda: align(a.cluster_centers_,
np.array([[10.1, 1.], [0., 1.]]), NAMES)))
case("K8", lambda r: expect_rejection(r, lambda: align(a.cluster_centers_,
np.array([[0., 1.], [0., 1.]]), NAMES)))
stale = {"schema": SCHEMA, "state": fingerprint(a, SCHEMA), "names": NAMES}
case("K9", lambda r: expect_rejection(r, lambda: serve(b, stale, SCHEMA, X)))
case("K10", lambda r: expect_rejection(r, lambda: serve(b, bundle, SCHEMA[::-1], X)))
require(len(report["cases"]) == 10, "Incomplete ledger")
report["status"] = "PASS_10_CASES"
except Exception as error:
report["error"] = {"type": type(error).__name__, "message": str(error)}
raise
finally:
report["finished_utc"] = datetime.now(timezone.utc).isoformat()
path = directory / "results.json"
path.write_text(json.dumps(report, indent=2, allow_nan=False) + "\n", encoding="utf-8")
print(path)
if name == "__main__":
main()The supplied commissioning record reports PASS_10_CASES, starting at 2026-10-14T06:42:19.612052+00:00 and finishing at 2026-10-14T06:42:19.625021+00:00. It identifies Linux-6.18.44-x86_64-with-glibc2.41, the package versions above and source SHA-256 b6b09460479f325805747a23fefa0a3fb57dc0ed3fb9f5cbce56e9b40feceac4. These timestamps identify that reported execution, not a benchmark. The laboratory exercises no persisted model, production population, network service or deployed serving system. A reader's rerun must retain its own actual report rather than reuse these timestamps.
Read the independent oracles before the result ledger
The fixture is useful because its expected answers exist before the candidate labels are interpreted. Model A’s known centers are [[0,1],[10,1]]; Model B’s are the same rows reversed. The expected semantic outputs for the four training points remain alpha, alpha, beta, beta. The probe answers are also literal: center-left and near-left are alpha; center-right and near-right are beta.
The report records actual outputs; the assertions compare them with independent literals rather than deriving business expectations from Model B's labels. K4 passes diagnostically when it demonstrates the wrong output. K7 through K10 pass when ValueError is raised. The helper records the reason but does not assert its exact text, so review the recorded reasons as well as status. PASS_10_CASES means the ten assertions behaved as designed, not that every represented operation is approved.
Verify the first fitted labeling
K1 qualifies Model A as the reference state. The fitted centers are:
label 0 -> [0.0, 1.0]
label 1 -> [10.0, 1.0]The training labels are [0,0,1,1], predict(X) returns the same sequence, and the inertia is 4.0. That inertia is directly auditable from the geometry: each point is one unit above or below its assigned center, so each squared distance is 1, and four such contributions sum to 4.
The important interface fact is not that the left cluster “should naturally be zero.” The shorthand “KMeans label numbers arbitrary” means that valid center indices are not durable business IDs. scikit-learn's KMeans prediction contract defines each predicted label as the index of the closest center. Its position in cluster_centers_ supplies the immediate numeric interpretation, not the external name segment-alpha or segment-beta.
For Model A, the authored manifest happens to be:
numeric label 0 -> segment-alpha
numeric label 1 -> segment-betaThat association comes from comparing Model A’s reviewed centers with the predeclared semantic oracle. It is not an output that K-Means discovered. This distinction is the core of the cluster ID versus business segment problem: the estimator can return a valid center index while the application still needs a separate, reviewed interpretation layer.
K1 is therefore evidence, not permission to detach the two-entry dictionary and reuse it forever. Its lookup is valid only for the fitted state whose center rows it describes.
Reverse the center order without changing the partition
K2 qualifies Model B under the reversed explicit initialization. Its fitted center array is:
label 0 -> [10.0, 1.0]
label 1 -> [0.0, 1.0]The labels are [1,1,0,0], predict(X) agrees with those labels, and inertia is again 4.0. Every point remains assigned to the same geometric group as in Model A. What changed is the row position of each center and, consequently, the integer used to refer to that row. This is a KMeans centers label order change, not a change in the two geometric groups.
Compare center arrays as ordered structures. Treating them as an unordered set would correctly show that the same two coordinate vectors exist, but it would discard the very relation the consumer needs: which candidate label indexes which center. Flattening, sorting all scalar values or comparing only aggregate sums would lose even more information.
Neither numbering is intrinsically correct. Model A’s 0 is left and Model B’s 0 is right. Requiring every future fit to reproduce Model A’s labels would confuse one observed representation with the application’s actual requirement. The correct requirement is that any accepted candidate must carry an approved mapping from its own center indices to semantic names.
An equal objective does not establish a consumer identity
“KMeans equal inertia different labels” is a useful symptom, not a complete proof. Equal inertia can occur for distinct center configurations, especially outside this trivial geometry. A scalar objective does not identify center rows, preserve membership for every observation or approve a semantic mapping.
This fixture combines three stronger forms of evidence: exact center equality under a permutation, equality of the full pairwise co-membership relation, and literal consumer-output oracles. Inertia confirms that the objective value did not change, but it is not promoted into an identity certificate.
Measure membership separately from numeric agreement
K3 compares the two outputs in three complementary ways. First, it forms a Boolean co-membership matrix for each model:
same_a = a.labels_[:, None] == a.labels_[None, :]
same_b = b.labels_[:, None] == b.labels_[None, :]Entry (i,j) answers whether observations i and j share a cluster. Because the numeric names cancel out of that relation, the matrices are identical. Each model groups P1 with P2, P3 with P4, and separates every left-right pair.
There are six unordered pairs of distinct observations: two within-group pairs and four cross-group pairs. Both models give the same answer for all six. The Boolean matrices also contain diagonal and mirrored entries, but those repetitions do not change the partition comparison. This is a complete comparison of these four rows, not a sample of an unobserved population.
Second, K3 computes adjusted_rand_score(a.labels_, b.labels_) and obtains 1.0. The adjusted Rand score API defines a pair-based comparison that scores identical partitions 1.0 even under label permutation. The clustering evaluation guide explains the same invariance and the use of these symmetric scores to compare label assignments. Here, Model A is a comparison reference, not evidence of independently known real-world classes.
Third, K3 computes direct elementwise numeric agreement:
float(np.mean(a.labels_ == b.labels_)) # 0.0There is no contradiction. ARI answers whether the partitions agree; direct equality asks whether the same integers occupy the same positions. This is the adjusted Rand score label permutation case in its cleanest form.
Do not call the zero raw agreement “zero accuracy.” There is no supervised class-label contract in this synthetic clustering comparison. For wider discussion of evidence design, see model evaluation and validation practices. Here the operational rule is narrower: use partition evidence for membership and a separate oracle for consumer meaning.
Expose the stale semantic lookup
K4 recreates the defect by applying Model A’s detached tuple directly to Model B’s labels:
NAMES = ("segment-alpha", "segment-beta")
wrong = [NAMES[int(label)] for label in b.labels_]Because Model B emits [1,1,0,0], the result is:
[segment-beta, segment-beta, segment-alpha, segment-alpha]The authored expected output remains:
[segment-alpha, segment-alpha, segment-beta, segment-beta]Every semantic value is reversed. Yet no observation disappeared, no coordinate moved, no threshold changed and no cluster membership changed. The defect sits entirely at the model-to-consumer boundary. The application replaced the estimator but preserved a lookup whose integer keys belonged to the prior estimator.
This is why “KMeans cluster labels change” is often the wrong operational description by itself. The labels did change numerically, but the consumer failure came from assuming those numbers were durable names. The repair target is the broken association between fitted center index and reviewed meaning, not the K-Means objective.
Keep a partition metric out of the naming contract
ARI correctly reports that the partitions are equivalent. It ignores absolute label values by design; that is useful statistical behavior, not a defect to work around. A release process should not search for a different clustering metric merely because the consumer lookup is stale.
Instead, keep two gates. The membership gate asks whether the candidate partition meets the relevant statistical evidence. The semantic gate asks whether each candidate center index has an approved business name bound to the reviewed fitted state. A green membership metric cannot waive a red semantic gate, and a diagnostic K4 pass cannot convert its wrong names into accepted output.
Accept only an exact and unambiguous center permutation
For the numeric NumPy matrices used here, align implements deliberately strict KMeans exact center permutation validation. It rejects incompatible shapes, a name count differing from the reference-center count, and non-finite values. It assumes array-like attributes and numeric operations are available; it is not a general validator for arbitrary objects or every possible numeric range.
Next it constructs an exact-equality matrix:
equal = (reference[:, None, :] == candidate[None, :, :]).all(axis=2)For this fixture, the matrix is:
[[False, True ],
[True, False]]Every reference row must have exactly one equal candidate row, and every candidate column must have exactly one equal reference row. That bidirectional uniqueness requirement proves that the candidate contains the same distinct centers exactly once.
Only then does the function form squared-distance costs and call linear_sum_assignment. The selected pairs must cover all reference centers and every selected cost must equal zero. The resulting correspondence is:
reference 0 [0,1] -> candidate 1 [0,1]
reference 1 [10,1] -> candidate 0 [10,1]The new mapping is written by candidate index, producing:
candidate label 0 -> segment-beta
candidate label 1 -> segment-alphaThe consumer indexes by the candidate's predicted label, so mapped[new_id] receives names[old_id]. However, the two-label swap is its own inverse. An implementation that confuses the two permutation directions can still pass this particular geometry. K5 checks the literal tuple and served names, but it does not establish detection of every inverse-mapping bug for larger permutations. Do not extend that test-coverage claim beyond the two-center fixture.
The guard also refuses a merely compatible assignment. It does not sort centers by the first coordinate, by magnitude or by a human-readable name. Such sorting rules can fail when geometry changes, coordinates cross or a different feature schema is introduced. The accepted evidence is the reviewed reference-to-candidate equality relation, not an incidental ordering heuristic.
Exact floating-point equality is appropriate here because the authored centers are exactly representable integer-valued coordinates and the allowed contract is literal identity under permutation. This is not a recommendation to use exact equality for general refits, noisy centroids or evolving populations. Relaxing the comparison would define a different policy that needs its own semantic review and rejection controls.
Bind the mapping to the reviewed center state
K5 does more than compute ("segment-beta", "segment-alpha"). It packages that tuple with the feature schema and a state stamp derived from Model B’s ordered centers plus that schema:
bundle = {
"schema": ("coordinate_x", "coordinate_y"),
"state": fingerprint(b, SCHEMA),
"names": ("segment-beta", "segment-alpha"),
}The serve function rejects a mismatched declared schema or center-state stamp before prediction. With the reviewed tuple unchanged, K5 returns alpha, alpha, beta, beta. K6 uses four fixed, non-tie probes, [0,1], [10,1], [1,1] and [9,1], and obtains alpha, beta, alpha, beta.
The probes add behavioral evidence, not coverage of every point. In particular, [5,1] is equally distant from both centers. Identical distinct centers do not remove such prediction ties, and this driver neither tests nor rejects tied points. Its accepted semantic outputs are the literal non-tie training and probe cases. A wider serving contract needs a separately reviewed tie policy; do not infer one from this remapping.
The SHA-256 state stamp covers only ordered centers and the declared schema. It excludes bundle["names"]: changing that tuple alone does not change the stamp or force rejection. The laboratory therefore assumes the tuple produced by align remains unchanged. This narrow KMeans mapping model fingerprint is not an authenticated bundle, complete estimator identity or proof of trusted code or production input meaning.
Do not confuse an assignment solver with semantic approval
The SciPy 1.17.0 assignment reference, consulted on 2026-10-14, documents linear_sum_assignment as minimizing assignment cost and returning corresponding row and column indices. An optimum can also be returned for drifted centers; minimum cost alone does not mean “same business segment.”
The authored uniqueness and zero-cost guards supply this laboratory’s admission rule. They require one exact partner for each center before the mapping can be published. Without those guards, a linear sum assignment cluster mapping is only a mathematical matching. It is not evidence that domain owners approve the resulting names.
For these centers, the cost matrix is [[100,0],[0,100]], so the two accepted pair costs sum to zero. Even without the solver, the exact-equality matrix already identifies the unique correspondence. The assignment step is a checked implementation choice, not an extra source of semantic authority.
Reject drift and duplicate-center ambiguity
K7 passes an artificial candidate center array [[10.1,1],[0,1]]. The left center still has an exact partner, but the right reference center [10,1] does not. The equality matrix therefore fails the one-match-per-row and one-match-per-column rule, and align raises ValueError("Not an unambiguous exact center permutation").
K8 supplies [[0,1],[0,1]]. Now the left reference center has two equal candidate partners, the right reference center has none, and each repeated candidate is indistinguishable under exact coordinates. The same guard rejects the attempted mapping before any names are published.
These rows are synthetic negative controls, not observations from additional fitted models. Their purpose is to prove that the admission function can reject two tempting shortcuts: treating a near center as the same identity and assigning names through duplicate-center ambiguity.
The correct action for both is HOLD. A center at 10.1 might represent harmless numerical movement, population change or a materially different segment; this fixture does not decide. Duplicate or near-duplicate centers may require a tie policy, model redesign or domain adjudication. Split and merge behavior needs still another lineage contract. To reject ambiguous cluster alignment honestly, keep those questions outside this exact-permutation proof rather than silently increasing a tolerance until the release turns green.
Reject a stale bundle or changed feature contract
K9 combines Model B with a bundle stamped from Model A. The name tuple in that stale bundle is ("segment-alpha", "segment-beta"), and its schema text still looks correct. Nevertheless, the ordered center state differs, so serve raises ValueError("Model and semantic manifest are not the reviewed pair").
K9 demonstrates detection of the stale center-state stamp. It does not establish that every mapping edit is detected, because the names are outside that stamp. Keep the reviewed names with the model/schema pair and control changes to the whole bundle. A plausible mapping file is not enough to establish that association.
K10 passes the reviewed Model B bundle but reverses the declared schema to ("coordinate_y", "coordinate_x"). The function rejects before serving. The fixture does not attempt to infer the intended columns, permute input arrays or repair feature order. Those are separate concerns addressed by input feature-identity validation. Here, schema order is simply part of the identity stamp and admission check.
Schema order matters because each center coordinate refers to one declared feature position. K10 changes only the schema argument, not the points. The guard compares that declaration; it does not inspect column names inside a plain NumPy array. An incorrectly ordered array accompanied by the expected schema string is outside this test. No feature-order adapter or repair is qualified here.
The bounded serving contract combines checked conditions with an explicit trust assumption:
The ordered centers must reproduce the bundle's state stamp; the function checks this before prediction.
The declared schema and order must match the bundle; the function checks that declaration, not upstream feature meaning.
The names must remain the reviewed tuple for that pair. This is an assumption here, not a condition enforced by the state stamp.
Keep acceptance limited to the checked non-tie inputs. A broader input or artifact-integrity policy requires separate qualification.
A stale model/mapping pair or an altered schema is HOLD, not a request to guess the intended association.
Reconcile the ten cases and decide the release state
Keep diagnostic status separate from release action. The JSON records case IDs, diagnostic status and case-specific observations; the actions below are the review decisions, not additional fields written by the driver. In the matrix, alpha and beta abbreviate the full segment-alpha and segment-beta strings retained in the code and oracles.
Case | Center / label or guard evidence | Partition evidence | Semantic evidence | Action |
K1 | A: [[0,1],[10,1]]; [0,0,1,1]; inertia 4 | Reference fit | Expected reference names: alpha, alpha, beta, beta | EVIDENCE_ONLY |
K2 | B: [[10,1],[0,1]]; [1,1,0,0]; inertia 4 | Compared in K3 | Numeric label order reverses; no names approved here | EVIDENCE_ONLY |
K3 | A and B label vectors compared | Co-membership equal; ARI 1; numeric agreement 0 | Partition agreement, not semantic approval | EVIDENCE_ONLY |
K4 | A lookup applied unchanged to B | Same partition from K3 | Wrong: beta, beta, alpha, alpha | REPAIR |
K5 | Exact distinct centers; matching stamp; reviewed tuple | Exact permutation | Correct: alpha, alpha, beta, beta | ACCEPT_BOUNDED |
K6 | B plus the K5 bundle; four non-tie probes | No new fit or population test | Correct: alpha, beta, alpha, beta | ACCEPT_BOUNDED |
K7 | Synthetic [10.1,1] replaces [10,1] | Not an observed refit | ValueError: mapping refused | HOLD |
K8 | Synthetic duplicate [0,1] centers | Not an observed refit | ValueError: ambiguity refused | HOLD |
K9 | B plus A center-state stamp | Fit B unchanged | ValueError: serving refused | HOLD |
K10 | B bundle plus reversed schema declaration | Points unchanged | ValueError: serving refused | HOLD |
Use ACCEPT_BOUNDED for K5 and K6 only: identical distinct centers under an exact permutation, unchanged declared feature order, the unchanged reviewed names tuple, a matching center/schema stamp and the tested non-tie outputs. This does not qualify every possible request or mapping mutation. K4 requires REPAIR; K7 through K10 remain HOLD because drift, ambiguity or the tested state/schema mismatches fall outside the admitted contract.
Treat these states as operational instructions, not grades. REPAIR means the statistical candidate can remain under review while the detached consumer table is rebuilt and rebound. HOLD means the available evidence cannot authorize semantic output; it does not assert that the model is intrinsically bad. ACCEPT_BOUNDED should be stored with the exact report, source hash, model state and manifest so its scope cannot be enlarged later by paraphrase. An aggregate green badge without those case-level distinctions would erase the most important evidence in the fixture.
A successful remapping is not general cluster lineage
The accepted result says only that Model B contains the same two distinct centers as Model A in the opposite row order and that the reviewed names were transferred through that exact correspondence. It does not prove that future customer populations are stable, that segments have ground-truth identities, that a near centroid is semantically continuous, or that splits and merges can be resolved automatically.
It also does not authorize downstream targeting, pricing or policy decisions. Those require their own domain, fairness, governance and operational review. The narrow proof should remain narrow precisely so a green result cannot be mistaken for broader business approval.
Recover semantic outputs without rewriting history blindly
Repair the serving boundary first. A KMeans refit semantic mapping must be reviewed and shipped with the candidate estimator, not copied forward as a free-standing dictionary. Publish the candidate estimator and its reviewed name bundle as one release unit, and prevent consumers from loading either artifact independently. Then address historical outputs according to the evidence that existed when each value was produced.
A safe recovery sequence is:
Inventory each emitted field. Record whether a stored value is a raw numeric cluster label, a semantic name, or both. Identify the model version, ordered feature schema and mapping version used at emission time.
Preserve the original association. Retain the model-to-name bundle that generated an output. A new mapping for Model B does not retroactively change what Model A’s 0 meant.
Recompute only with complete provenance. Historical semantic values can be regenerated when trustworthy input features, their exact schema order, the fitted model state and the mapping bound to that state are all available.
Quarantine unknown lineage. When the original model or mapping cannot be identified, do not assume every old 0 should become 1, or vice versa. Mixed deployments, manual edits and partial backfills can make a global swap corrupt otherwise correct records.
Rerun qualification after relevant change. A refit, center change, schema change, mapping edit or serving-code change requires a fresh ledger and source identity.
A mapping update repairs future interpretation; it does not rewrite history by itself. If a dashboard cached semantic names, changing the lookup will not correct those strings. If a warehouse stored only integers without model provenance, the missing association may be irrecoverable from the number alone. The honest state is HOLD until stronger evidence appears.
When correction is justified, prefer an auditable migration over an in-place overwrite. Preserve the original value, add the qualified semantic value, record the bundle used for recomputation and make downstream readers choose the corrected field explicitly. That distinguishes an originally served name from a later reconstruction and makes subsequent reinterpretations reviewable. The laboratory needs no personal or production data; the real pipeline needs enough artifact identity to explain how a number became a name.
Ownership should also be explicit. The ML owner qualifies the fitted geometry and partition evidence. The domain owner approves what segment-alpha and segment-beta mean in the synthetic or real application context. The serving owner binds model, schema and manifest and enforces rejection at load or request time. QA owns repeatable fixtures, retained reports and negative controls; repeatable model regression evidence is useful context for making those artifacts reviewable without turning this article into a generic test strategy.
This division prevents an optimization routine from becoming an accidental naming authority. It also makes the release decision inspectable: the model can be statistically acceptable while the consumer integration remains blocked, or the integration can be mechanically correct while domain names still await approval.
Develop the numerical foundations behind model review
Engineers developing Python, statistical-modeling and machine-learning foundations can review Refonte Learning's Data Science & AI programme. Its Program Specifics section, consulted on 2026-10-14, lists three months and 12–14 hours per week. Advertised themes include Python Data Science, statistical modelling, machine learning, predictive modelling, optimization and industry projects; the FAQ names NumPy and scikit-learn among the tools used.
Those broad foundations are relevant to reading a fixture like this, but the programme page does not establish that this exact SciPy alignment laboratory or these package versions are taught. The operational lesson stands independently: do not ship an unexplained K-Means integer as a permanent segment ID. Ship a reviewed model, an ordered feature contract and a semantic manifest whose association is tested and bound to the fitted state.
