Cached scikit-learn pipeline showing transformer cloning, fitted-step inspection, and intermediate cache reuse

Which Transformer Did Your Cached Pipeline Actually Fit?

Thu, Oct 22, 2026

A subtle failure mode appears when an engineer adds Pipeline(memory=...) to an otherwise familiar scikit-learn training path and then keeps inspecting, validating, or exporting the transformer object that was originally passed to the Pipeline constructor. With caching disabled, that reference may indeed be the fitted step. With caching enabled for an intermediate transformer, scikit-learn documents a different ownership boundary: it clones intermediate transformers before fitting, and directs inspection to named_steps or steps; the last pipeline step is never cached.

That distinction is operational, not cosmetic. A constructor reference can be unfitted, can preserve parameters that no longer match the selected pipeline step, or can even remain fitted to an older dataset while the completed pipeline contains a different fitted clone. The release question is therefore not “does this object have mean_?” It is “which fitted object did this completed pipeline select, and does its learned state match the data, parameters, and transform we intend to release?”

This playbook answers that question with a deliberately tiny CPU-only fixture: CPython 3.13.5, NumPy 2.3.5, scikit-learn 1.8.0, joblib 1.5.3, StandardScaler, LinearRegression, dense arrays, and owned temporary cache directories. The validation fixture and its preparation record define nine cases, P1 through P9. The recorded rerun used those exact script bytes in the specified environment on October 22, 2026; the source SHA-256 matched 3bac0b389df17eef21c135f8ee5f11d718bcc6e78e359786fa5ebf71a9f61a58, and all nine cases passed. The point is provenance validation, not model quality or cache performance.

Define which fitted preprocessing state the release must contain

Before touching the cache, define the release object precisely. The release candidate is not “the scaler variable.” It is the fitted scale step selected by a particular fitted pipeline, under a particular parameterization, after fitting a particular training matrix. In code, that means the authoritative in-process reference is obtained after fit from pipe.named_steps['scale']. The scikit-learn 1.8.0 Pipeline reference explicitly says that enabling caching clones transformers before fitting and that the supplied transformer cannot then be inspected directly; named_steps or steps should be used instead.

For this fixture, the declared training data are:

X = [[-2, 0],
     [ 0, 2],
     [ 2, 4],
     [ 4, 6]]
y = [0, 1, 2, 3]

The release contract for the default scaler is independently calculable. Feature means are [1, 3]. Population variances are [5, 5], so scale_ should be [sqrt(5), sqrt(5)]. With centering enabled, the probe transform is (X - [1, 3]) / sqrt(5). scikit-learn’s 1.8.0 StandardScaler reference defines the standard score as (x - u) / s, stores training-set statistics for later transforms, and documents scale_, mean_, and var_; it also states that its standard-deviation calculation corresponds to ddof=0.

Those learned arrays are necessary evidence, but they are not sufficient provenance. A scaler fitted yesterday on different data may have perfectly valid mean_, var_, and scale_ attributes. The acceptance unit therefore combines four facts: the selected object came from the fitted pipeline, its fitted parameters match the intended pipeline parameters, its learned arrays match the independently derived training contract, and its fixed probe transform matches the expected numeric output within rtol=0, atol=1e-12.

The decision vocabulary is intentionally strict. ACCEPT the selected fitted step only after those numeric and provenance checks. REPAIR any utility that inspects or exports the constructor reference instead of the selected step. RERUN when the environment changes or the provenance record is incomplete. HOLD when cache ownership is unknown or the held artifacts disagree.

Freeze the matrix, estimator and cache ownership

Object-provenance debugging becomes unreliable if the experiment changes data, dependencies, estimators, and cache location at the same time. Freeze the variables that are not under test. The validation fixture uses only two deterministic numeric features, four rows, a StandardScaler-derived counter class, a LinearRegression-derived counter class, and temporary directories created by tempfile.TemporaryDirectory. The counters instrument whether fit executes; they are not part of predictive state and do not alter the scaler’s numerical function.

The exact rerun environment was CPython 3.13.5, NumPy 2.3.5, scikit-learn 1.8.0, joblib 1.5.3, Linux 6.18.55-x86_64-with-glibc2.41. The exact fixture bytes were used without adapting the code; their SHA-256 matched the preparation record’s digest. The rerun started at 2026-10-22T07:11:07.885651+00:00 and finished at 2026-10-22T07:11:07.989703+00:00 with PASS_9_CASES. That fresh run does not turn the result into a universal contract; it says the documented behavior and the nine asserted cases agreed in this pinned environment.

This narrowness matters. An adjacent Refonte playbook on imputer output-schema validation focuses on whether preprocessing changes the feature interface. This article deliberately does not repeat that problem. Its matrix has a fixed two-column shape throughout; the variable is which estimator instance owns the fitted preprocessing state. The neighboring article’s current published page likewise frames its own test as schema validity rather than model performance.

Cache ownership is equally important. Use a directory created and controlled by the test process, and delete it through the same ownership path. Do not point exploratory code at a shared production cache. Joblib Memory persists cached function outputs to storage and reloads matching calls rather than recomputing them; a local cache therefore participates in program state even though it is not a model registry. The current stable joblib.Memory API describes filesystem-backed caching and the special location=None mode in which no caching is performed.

The preparation record identified the stable joblib Memory documentation as the 1.5.3 edition. At the time of the recorded rerun, the stable API page was served as 1.6.0, while the local runtime remained 1.5.3. The versioned scikit-learn 1.8.0 pages establish the documented Pipeline behavior; the local joblib 1.5.3 execution establishes the observed behavior, and the joblib page supports the documented Memory semantics. A documentation selector is not a runtime lock.

Establish the no-cache reference case

P1 is the control that shows why old inspection code can appear correct for a long time. Create plain = CountedScaler(), put it into a pipeline with memory=None, fit the pipeline, and compare the constructor reference with p0.named_steps['scale']. In the rerun, the identity assertion passed: the originally supplied scaler was the fitted named step. Its mean_ was [1.0, 3.0], and its transform matched the independently computed expected array.

That behavior is consistent with the documented caching boundary: scikit-learn says enabling caching triggers cloning of transformers. With caching disabled, that trigger is absent. The important engineering lesson is not that constructor inspection is “always wrong.” It is that constructor inspection is conditional on pipeline configuration, which makes it a poor release invariant. Code that happens to be correct under memory=None can silently point at the wrong object after caching is enabled.

For the independent arithmetic, each column has four values centered at 1 and 3 respectively. The squared deviations in either feature are 9, 1, 1, 9, summing to 20. Dividing by four gives the population variance 5, and the standard deviation is sqrt(5). The expected transformed rows are therefore approximately:

Input row

Expected transformed row

[-2, 0]

[-1.3416407864998738, -1.3416407864998738]

[0, 2]

[-0.4472135954999579, -0.4472135954999579]

[2, 4]

[0.4472135954999579, 0.4472135954999579]

[4, 6]

[1.3416407864998738, 1.3416407864998738]

Those numbers come from the fixture’s declared algebra, not from calling transform a second time on whichever object is under review. That separation matters because a self-comparison can only prove consistency with itself.

Make the numeric transform the oracle

Treat the known transform as an oracle for numeric function, not object membership. A selected scaler that produces the expected transform has the correct learned behavior for this tiny probe. It still must be tied to the completed pipeline through named_steps. Conversely, object identity alone is not enough: the selected object could be the right reference but hold the wrong learned statistics after an unexpected training change.

The strongest check is therefore conjunctive:

selected = pipe.named_steps["scale"]
np.testing.assert_allclose(selected.mean_, [1.0, 3.0], rtol=0, atol=1e-12)
np.testing.assert_allclose(selected.var_, [5.0, 5.0], rtol=0, atol=1e-12)
np.testing.assert_allclose(selected.scale_, np.sqrt([5.0, 5.0]), rtol=0, atol=1e-12)
np.testing.assert_allclose(selected.transform(X), expected, rtol=0, atol=1e-12)

StandardScaler documents these fitted attributes and the transform formula, so they are appropriate state checks for this estimator. They are not proof that the object belongs to this pipeline; P9 will demonstrate that distinction directly.

Enable memory and locate the fitted clone

P2 turns on the condition that breaks constructor-reference inspection. A new CountedScaler named supplied is passed into a pipeline whose memory points to an owned temporary directory. After fitting, the rerun established four facts: supplied is not p.named_steps['scale']; check_is_fitted(supplied) raises NotFittedError; the named scale step has mean [1.0, 3.0]; and its transform matches the independent oracle. The pipeline also predicts y for this tiny linear fixture, but prediction is only a consistency check here, not the acceptance criterion for the preprocessor.

This is precisely the side effect described in scikit-learn’s 1.8.0 composite-estimator guide. Its caching section states that enabling caching clones transformers before fitting and provides an example in which the originally supplied PCA remains unfitted while the fitted transformer is inspected through cached_pipe.named_steps[...].

A useful ledger separates constructor references from selected fitted steps:

Case

Constructor reference after fit

named_steps reference after fit

Export candidate

P1, memory=None

Fitted and identical to selected step

Same object

Selected step; constructor happens to alias it

P2, owned disk cache

Unfitted intermediate object

Distinct fitted clone

named_steps['scale']

P7, Memory(location=None)

Fitted and identical to selected step

Same object

Selected step; constructor happens to alias it

P8, final transformer

Final constructor object retained and fitted

Same final object

Final selected step

P9, stale fitted constructor

Still fitted to old data

Distinct fitted clone for current data

Current named_steps['scale'] only

The ledger is intentionally about local relationships, not durable object IDs. Python’s id() can help a diagnostic print show that two live objects differ, but it is not a persistent artifact identifier and should not enter an export manifest. Python documents id() as unique and constant only for an object's lifetime, with reuse possible for non-overlapping lifetimes. Persist data fingerprints, parameter values, source revision, environment metadata, and learned state instead.

The adjacent Refonte article on feature identity at the model boundary asks whether values reached the coordinates learned for the right features. This playbook keeps that boundary fixed and instead asks whether inspection reached the estimator instance selected by the fitted pipeline. The neighboring article itself distinguishes feature mapping from model-quality evaluation, which is the same kind of scoping discipline applied here.

An unfitted constructor object is not a failed pipeline fit

P2 can look alarming in a debugger: supplied.mean_ does not exist after p.fit(X, y). The wrong conclusion is “the scaler did not fit.” The correct conclusion is “that reference was not the object the cached pipeline fitted.” The fitted result lives in the completed pipeline.

That difference should change inspection utilities. A generic utility that receives a fitted pipeline should derive its step from the pipeline itself:

selected = fitted_pipe.named_steps["scale"]
check_is_fitted(selected)

It should not require callers to separately preserve and pass the original constructor object. Once caching is in play, that extra reference is at best redundant and at worst misleading. The pipeline is the ownership boundary for the selected fitted estimator, as the scikit-learn API and guide both direct.

Measure reuse without turning the article into a benchmark

P3 fits the same cached pipeline again with identical data and unchanged parameters. The instrumentation records the change in class-level fit counters before and after the call. In the rerun, the intermediate scaler fit delta was 0, while the final regressor fit delta was 1. The transformed probe still matched the known answer.

This is a narrow observation about reuse in this fixture. Scikit-learn documents that memory caches fitted transformers and that the last step is never cached, even if it is a transformer. Joblib describes Memory as caching return values for calls with the same input arguments. Taken together, those contracts explain why an unchanged intermediate fit can be reused while the final estimator fits again.

Do not inflate that result into a performance claim. The counter tells us whether the instrumented fit method executed, not how much time was saved, what I/O occurred, whether hashing dominated, or how a larger pipeline would behave. This article is not a cache benchmark. It also does not establish a universal promise that all conceptually “same” data will map to the same cache entry across environments. Joblib’s current Memory guide notes that cache keys depend on representations of inputs and may be invalidated across joblib or third-party-library updates.

The instrumentation itself must stay semantically inert. A fit counter is acceptable because it records a side observation while returning the same fitted scaler/regressor behavior. It would be invalid to make the numerical transformation depend on mutable external state that the cache key cannot see and then use a counter result as proof of correctness. The cache should not be asked to infer hidden dependencies.

A compact state-flow diagram captures the P2/P3 distinction:

capture

The diagram is conceptual. The acceptance evidence remains the object relationship, fit deltas, fitted arrays, and fixed transform probe observed in the pinned run.

A cache hit and a final fit can occur in the same call

This is the operational point P3 is designed to make. A second Pipeline.fit is not all-or-nothing. Reuse of an intermediate transformer does not imply that the final estimator was skipped. The Pipeline API explicitly excludes the last step from caching.

That matters for logging. “Pipeline cache hit” is too coarse to be a release record because different steps have different caching rules. Record step-level evidence: which intermediate state was reused, which final estimator fit ran, and which selected fitted step is present after completion. In this fixture, the answer is unambiguous: cached intermediate reuse and a fresh final fit coexist.

Change the selected parameters rather than an abandoned reference

P4 and P5 show why parameter changes must be made through the pipeline once the original intermediate object has ceased to be authoritative.

After P2/P3, the external constructor object supplied is still separate from the selected fitted clone. P4 calls supplied.set_params(with_mean=False) and fits the cached pipeline again. The rerun shows that supplied.with_mean is now false while p.named_steps['scale'].with_mean remains true; the selected transform still performs centering and matches (X - [1, 3]) / sqrt(5). The mutation changed an abandoned external reference, not the pipeline’s selected step.

That result follows from the same cloning boundary. It also demonstrates why “I changed the scaler” is ambiguous language. Engineers should say which owner they changed. If the intent is to change the pipeline component, use the pipeline’s nested parameter interface. The 1.8.0 Pipeline documentation describes parameters of contained estimators using the step name plus __ plus the parameter name.

P5 therefore calls:

p.set_params(scale__with_mean=False).fit(X, y)

The rerun records an intermediate fit delta of 1, and the selected step’s with_mean is false. The expected transform changes accordingly. StandardScaler defines u as zero when with_mean=False, while s remains the learned standard deviation when with_std=True; for this fixture the expected result is X / sqrt(5).

This comparison is stronger than checking a Boolean parameter alone. P4 proves that mutating the constructor object does not select the desired behavior; P5 proves that changing scale__with_mean on the pipeline leads to a newly fitted selected intermediate with the expected numerical function. Parameter evidence and transform evidence reconcile.

For inspection and export code, the repair is straightforward:

# Fragile once caching is enabled
export_scaler(supplied)

# Correct ownership boundary after fit
selected = p.named_steps["scale"]
export_scaler(selected)

The export call still needs the provenance checks described later. The point here is that parameter intent must flow through the object that owns pipeline configuration. Editing a detached constructor reference is not an alternative configuration path.

Revalidate when input data or caching mode changes

P6 changes the training matrix by adding 100 to every value while keeping the selected configuration at with_mean=False. The intermediate fit counter increases by one, and the selected scaler records mean_ = [101.0, 103.0]. That mean is still learned even though centering is disabled in the transform; the exact StandardScaler fitted-attribute conditions depend on the with_mean/with_std combination, and the StandardScaler API documents how mean_, var_, and scale_ are defined.

The important provenance result is that new input arguments invalidate the previous reuse path in this fixture and produce state tied to the shifted data. The correct response to changed training data is not to “trust the cache” or to keep exporting the previous selected scaler. It is to collect a new evidence unit: data fingerprint, selected parameters, learned arrays, and probe transform for the new fit.

This is also the boundary between state validation and broader model evaluation beyond a passing fit. Refonte’s evaluation guide treats generalization, baselines, metrics, and unseen data as a separate discipline. This fixture does none of that; its numeric checks only establish which preprocessing state was selected and whether that state matches the declared tiny training input.

P7 changes something different: it supplies a joblib.Memory(location=None, verbose=0) object. The rerun shows that the constructor scaler is again the fitted named step. The current joblib Memory API says that when location=None, no caching is performed and the Memory object is transparent. That makes P7 an important control: the mere presence of a Memory instance does not prove that an active storage-backed cache exists.

This is why release metadata should record the effective cache mode, not just type(pipe.memory).__name__. For a disk-backed string path or a Memory object with a location, the intermediate cloning/caching boundary is active under the tested scikit-learn behavior. For Memory(location=None), P7 observes the no-storage boundary and the constructor reference remains the selected fitted step.

A Memory object does not always mean caching is enabled

Treat Memory(location=None) as a deliberate disabled-caching case. Do not infer “cached pipeline” from object type alone, and do not infer “uncached pipeline” from the absence of files after cleanup. Inspect the effective configuration before fit and the selected estimator after fit.

The rule for export remains stable across both modes: always resolve the selected step from the completed pipeline. In P7, doing so returns the same object as the constructor reference. In P2, it returns a distinct fitted clone. Using named_steps in both cases removes the need for export code to branch on object identity.

Test the final-transformer exception

P8 prevents another overgeneralization: “memory means every transformer is cloned and cached.” The Pipeline contract says the last step is never cached, even if it is a transformer. The fixture tests exactly that boundary by making the first step a counted scaler and the final step another StandardScaler subclass named FinalScaler.

After the first fit with an owned cache directory, the rerun shows that the first intermediate step is not the original first object, while the final selected step is the original last object. On an identical second fit, the intermediate scaler fit delta is zero and the final scaler fit delta is one. Position, not merely transformer capability, determines the caching treatment.

That distinction matters for tooling that walks a pipeline. A rule such as “all transformer objects are clones when memory is set” is wrong. The correct rule under scikit-learn 1.8.0 is positional: intermediate transformers are the cacheable stages; the last step is excluded from caching. A final step can itself expose transform, making the entire pipeline transformer-like, without changing that exclusion. The composite-estimator guide explicitly notes that a pipeline exposes methods provided by its final estimator and separately states that the final step is never cached.

Inspection code should therefore avoid reconstructing ownership from estimator classes. The robust procedure is simpler:

for name, selected_step in fitted_pipe.named_steps.items():
    inspect_selected(name, selected_step)

Then interpret each selected step according to its role and fitted contract. For a known intermediate scaler, validate the appropriate learned state and probe transform. For a final estimator, validate the final-estimator evidence required by that release process. In this article, the final step is only present to prove the positional exception; it is not being promoted into a general transformer-export framework.

P8 also clarifies what the fit counters mean. The fact that FinalScaler.fits increments again does not prove anything about predictive quality. It establishes that the final object’s fit method executed again in the controlled case. The fact that the intermediate counter does not increment establishes reuse in that same fixture. Those are state-transition observations, not performance measurements.

Expose a stale object that still passes a fitted check

P9 is the most dangerous case because it defeats a common defensive check. Before building the cached pipeline, the constructor scaler is explicitly fitted on X + 50. That gives it mean_ = [51.0, 53.0]. The already-fitted scaler is then passed as the intermediate step of a cached pipeline and the pipeline is fit on the original X.

After completion, check_is_fitted(stale) passes. If an export utility equates “fitted” with “the fitted object used here,” it will approve the wrong scaler. The rerun shows the decisive divergence: the external constructor object still has mean [51.0, 53.0], while p9.named_steps['scale'] has mean [1.0, 3.0]. Their transforms on the same X are not numerically equal. The fixture records naive_export_approved=False.

This negative case is why fitted attributes must never be described as proof of pipeline membership. check_is_fitted answers whether an estimator appears fitted according to scikit-learn’s fitted-state conventions. It does not encode the provenance statement “this exact estimator instance was the intermediate step used by this fitted cached pipeline.” The Pipeline documentation supplies the missing ownership rule: inspect the selected estimator through the fitted pipeline when caching is enabled.

The stale object is especially plausible in real maintenance code because constructor objects may live longer than one training call: a notebook cell, factory, test fixture, or module variable can reuse them. The correct repair is not to invent more fitted-attribute checks on that stale reference. It is to stop treating the constructor reference as authoritative after pipeline fitting.

Fitted is not the same as fitted for this pipeline

P9 gives a precise three-part failure signature:

Check

Stale constructor

Selected pipeline step

check_is_fitted(...)

Passes

Passes

mean_

[51, 53]

[1, 3]

Transform on original X

Does not match release oracle

Matches release oracle

The stale candidate fails provenance and numeric reconciliation even though its fitted-state check succeeds. That is enough to reject it as an export candidate. A diagnostic assertion proving the candidate is wrong is not, by itself, approval of the other candidate; the selected step still has to satisfy the positive checks.

Repair inspection and export around named_steps

The repair should make it difficult to separate exported preprocessing state from the fitted pipeline that selected it. Start with the fitted pipeline object, resolve the step by name, and build a small manifest from independently meaningful evidence. Do not start from a free-floating StandardScaler reference and attempt to infer later which pipeline it might have belonged to.

A bounded manifest for this fixture can include:

selected = pipe.named_steps["scale"]
manifest = {
    "step_name": "scale",
    "params": selected.get_params(deep=False),
    "mean": selected.mean_.tolist(),
    "var": selected.var_.tolist(),
    "scale": selected.scale_.tolist(),
    "probe_input": X.tolist(),
    "probe_output": selected.transform(X).tolist(),
    "training_data_sha256": sha256(X.tobytes()).hexdigest(),
    "python": sys.version,
    "sklearn": sklearn.__version__,
    "numpy": np.__version__,
    "joblib": joblib.__version__,
}

The precise serialization format is outside this fixture; what matters is the binding. The selected step must come from the completed pipeline, and the manifest must carry enough evidence to reproduce why that state was accepted. Add source revision or script digest as appropriate. The validation fixture already prints a source SHA-256 and version metadata, which is a strong pattern for a local validation artifact.

For data fingerprints, define the byte representation rather than writing “data hash” abstractly. In a production training system that may mean hashing an immutable dataset snapshot identifier plus preprocessing-relevant metadata instead of raw in-memory bytes. In this tiny dense fixture, hashing the known array bytes is enough to illustrate the principle. The hash is provenance metadata; it does not prove semantic correctness of the data.

The fixed transform probe is equally important because it connects learned state to behavior. Compare expected and observed output using the declared tolerances. Keep the probe small and deterministic. A prediction from the final regressor is not a substitute: a downstream estimator can sometimes mask or compensate for preprocessing differences, while the export contract here is specifically the preprocessing transform.

This local evidence unit fits inside the wider MLOps artifact lifecycle, but it should not be confused with a registry or orchestration system. The linked Refonte article discusses Kubernetes, workflow orchestration, deployment, and registries at a broader infrastructure layer. This playbook remains in one CPU process and treats the joblib cache only as an implementation cache.

A practical release rule is: export only the selected fitted step whose pipeline provenance, parameters, learned arrays, and probe transform reconcile. If the surrounding product needs the preprocessor and model together, prefer exporting the fitted pipeline as the paired artifact and derive inspection metadata from its selected steps. Do not export a constructor object merely because its class and hyperparameters look familiar.

Handle cache removal and trust boundaries

Cache lifecycle and estimator lifecycle are separate. Deleting an owned joblib cache directory removes reusable cached entries; it does not reach into an already held fitted pipeline object to refit or replace its selected estimators, or remove their fitted state. A live pipeline can continue to hold fitted state after its cache directory has been removed. That is why “we cleared the cache” is not a provenance repair for an already questionable exported scaler.

When evidence is inconsistent, rerun the fit under a controlled cache and rebuild the artifact from the completed pipeline. When the environment changes, rerun rather than assuming cache keys or serialized internals are stable. Joblib’s Memory guide notes that cache entries can be invalidated by environment updates and that joblib.hash() values are not guaranteed to remain constant across joblib versions. Scikit-learn’s model-persistence guidance likewise warns that loading models across different scikit-learn versions is unsupported and recommends recording training data references, source code, and dependency versions so the environment can be reconstructed.

The trust boundary is stricter than ordinary staleness. Joblib persistence is pickle-based, and its documentation warns that loading untrusted serialized content can execute arbitrary Python code. The current joblib.load reference says never to load files from untrusted sources, and scikit-learn gives the same warning for pickle/joblib-based model persistence. A cache directory therefore needs an ownership story before you let a process deserialize entries from it.

This does not mean a joblib cache is a model registry. A registry usually carries explicit artifact identity, lineage, promotion state, access controls, and release semantics. Memory is a computation cache keyed around function calls and arguments. Treating its directory as a release store creates a category error: reuse mechanics become mistaken for governance.

The safe operational responses are simple. If the cache is owned and the environment/provenance record is complete, run the validation cases and use the selected pipeline state. If the cache ownership is unknown, do not load from it; HOLD and recreate the cache under controlled ownership. If the environment or source has changed, RERUN and collect fresh evidence. If only inspection/export points at the constructor reference, REPAIR that code and validate the selected step.

Do not treat an arbitrary cache directory as a trusted artifact store

Unknown cache contents are not just a correctness risk; because joblib uses pickle-based loading, they are a code-execution trust risk. “The path exists” and “the files were produced by something that looks like our pipeline” are not sufficient ownership evidence.

Keep test caches disposable and process-owned, as the fixture does. For long-lived engineering caches, establish filesystem ownership and access controls outside the cache API, record which code/environment generated the entries, and make invalidation/rebuild possible. If those facts are missing at review time, the correct release decision is HOLD, not an exploratory load.

Reconcile the nine cases and choose the release decision

The nine cases form one provenance argument rather than nine isolated curiosities. P1 establishes the familiar no-cache alias. P2 shows the cache-triggered fitted clone. P3 proves intermediate reuse can coexist with a fresh final fit. P4 shows that mutating the detached constructor object does not change the selected pipeline step. P5 shows that changing the pipeline’s nested parameter does. P6 ties new data to newly fitted selected state. P7 separates a Memory object from active storage. P8 proves the last-step exception. P9 proves that fitted-state checks alone can approve a stale constructor object.

The fresh rerun produced this evidence summary:

Case

Observed evidence in pinned rerun

Release implication

P1

Original scaler is selected fitted step; mean [1,3]

Baseline control; constructor alias is incidental

P2

Original remains unfitted; selected step is distinct and fitted

REPAIR constructor-reference inspection/export

P3

Intermediate fit delta 0; final fit delta 1

Accept reuse evidence only at step scope

P4

External with_mean=False; selected remains True

External mutation is not pipeline configuration

P5

Pipeline scale__with_mean=False; fit delta 1; transform X/sqrt(5)

Selected parameter change reconciles numerically

P6

Shifted input; fit delta 1; selected mean [101,103]

New data requires new selected-state evidence

P7

Memory(location=None); original is selected fitted step

Storage disabled; still inspect via named_steps

P8

Final original retained; final fit delta 1; intermediate reused

Final-step position overrides “transformer” intuition

P9

Stale constructor mean [51,53]; selected mean [1,3]

Reject naive fitted-object export

The decision policy is therefore:

Decision

Evidence condition

ACCEPT

Selected step comes from the completed pipeline, parameters match intent, learned arrays reconcile, fixed probe transform matches, and provenance/cache ownership are known

REPAIR

Inspection or export uses the constructor reference instead of the selected fitted step

RERUN

Runtime/dependencies/source/data provenance changed or cannot be reconstructed with confidence

HOLD

Cache ownership is unknown, artifacts conflict, or provenance/numeric evidence cannot be reconciled

A successful negative assertion can justify rejection, but acceptance requires the positive evidence set. P9 rejects the stale scaler; it does not automatically bless every object reachable through named_steps in every system. The selected object still has to pass the declared numeric and provenance checks.

Assign ownership for preprocessing provenance

A reproducible workflow needs clear responsibility for the evidence, not just correct library calls. The training owner controls the fitted pipeline, data revision, parameterization, source revision, runtime lock, and cache location used during the run. The inspection/export owner must resolve fitted components from that pipeline rather than from constructor references and must preserve the manifest that ties learned arrays and probe transforms to the selected step. The release owner reviews the evidence and applies ACCEPT, REPAIR, RERUN, or HOLD.

That division prevents a familiar handoff failure: training is correct, but a separate utility receives the original scaler variable and serializes it because “it is fitted” or because the utility predates caching. P2 and P9 show both ways that assumption fails. The repair belongs to the inspection/export path, while the training owner supplies the fitted pipeline and provenance. Release should not patch the artifact by guessing which scaler was intended.

Environment upgrades require fresh evidence without assuming that every upgrade necessarily breaks the workflow. Scikit-learn’s persistence documentation recommends matching training and loading dependency versions and explicitly says cross-version loading is unsupported; joblib documents that cache invalidation can occur after joblib or dependency updates. That supports a RERUN policy after a changed environment, not a claim that a specific upgrade must fail.

The broader data-science toolkit in context includes many technologies and practices, but this provenance check is intentionally narrower. The linked article surveys languages, libraries, infrastructure, and professional skills; here the release owner needs only a small subset: reproducible Python execution, scikit-learn estimator semantics, joblib cache trust, deterministic numeric testing, and disciplined artifact metadata.

One final ownership rule is worth making explicit: id() values belong only in transient debugging. They can help a developer see that two live references are different in P2, but they should not be stored as artifact identity because Python only promises uniqueness and constancy during the object's lifetime. Persist fingerprints and declared relationships that survive process boundaries instead.

Develop reproducible ML practice with the Data Science Program

The core habit in this playbook is simple but transferable: do not infer model state from a convenient Python reference; make the fitted object, its inputs, its parameters, and its reproducible evidence explicit. That discipline sits naturally alongside practical Python and scikit-learn work.

Refonte Learning’s Data Science Program currently lists a three-month format with an estimated 12–14 hours per week, describes practical learning and virtual internship opportunities, and names Python Data Science, statistical modeling, machine learning, and predictive modeling among its areas. Its FAQ explicitly lists Python, Jupyter Notebook, Pandas, NumPy, Matplotlib, scikit-learn, and TensorFlow as tools used in the course.

The program page does not establish that Pipeline(memory=...) cloning, joblib cache auditing, or this exact preprocessing-export workflow is a dedicated course module. The grounded connection is narrower: engineers who practice reproducible preprocessing validation are exercising the same Python and scikit-learn foundations the program explicitly names.

For the fixture studied here, the final release call is equally grounded: ACCEPT the selected fitted step after numeric and provenance checks; REPAIR constructor-reference inspection and export; RERUN when environment or provenance changes; and HOLD when cache ownership or artifact consistency is unknown. The decisive object is the one the completed pipeline selected, not the one that happened to exist before fit began.