Skip to content

fix(preprocessor): resolve output_format='auto' once per fit - #113

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/73-output-format-auto-at-fit
Open

ChrisW09 wants to merge 1 commit into
mainfrom
fix/73-output-format-auto-at-fit

Conversation

@ChrisW09

@ChrisW09 ChrisW09 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #73.

With output_format="auto", Preprocessor.transform chose between a dense array and a CSR matrix separately on every call, from that batch's density. The same fitted preprocessor therefore returned sparse output for some single rows, and dense-only estimators such as HistGradientBoostingRegressor failed at predict although fit had succeeded.

Root cause

"auto" was resolved inside compute_output_report, which transform calls with each batch's output. fit stored no decision, so the container followed the density of whatever was being transformed. Rows from PLE or one-hot blocks easily fall below the 0.3 threshold on their own.

Fix

  • New resolve_output_format and private _density in pretab/compose/output.py. resolve_output_format resolves "auto" to "dense" or "sparse" from an array's density. compute_output_report uses both; explicit formats behave as before.
  • Preprocessor.fit calls fit_transform on the inner ColumnTransformer instead of fit, at no extra cost because ColumnTransformer.fit runs fit_transform internally. It resolves the format once from the training output and stores it in the new fitted attribute output_format_. A dense training output goes through np.asarray first, as in transform, because a global transform_output="pandas" can make the inner transformer return a DataFrame.
  • Preprocessor.transform passes output_format_ to compute_output_report, so every call returns the container chosen at fit. output_report_ still reports each call's own shape and density. A preprocessor without output_format_ (fitted by 1.0.0 and loaded from a spec or pickle) keeps the per-call choice.

Behaviour changes

  • With output_format="auto", every transform returns the container chosen at fit, including for a single row.
  • New fitted attribute output_format_ ("dense" or "sparse"). Explicit formats are stored as given.
  • output_format_ is part of the fitted state, so to_spec() and fingerprint_ of a newly fitted preprocessor differ from 1.0.0 for every output_format, not only "auto".

Documentation

  • Preprocessor docstring: the output_format parameter says "auto" is decided once at fit from the training output's density; new output_format_ attribute.
  • Docstrings of compute_output_report and the new resolve_output_format.
  • docs/core_concepts/outputs_and_inspection.md: the output_format entry says the choice is made once at fit, from the training output, and stored in output_format_, so every transform returns the same container.

Tests

  • test_auto_format_is_fixed_at_fit_for_dense_training_output: on the diabetes data, output_format_ == "dense", all 442 single rows come back as ndarray, and output_report_ describes the one-row call. Fails on main, passes here.
  • test_auto_format_is_fixed_at_fit_for_sparse_training_output: a 20-level one-hot column gives output_format_ == "sparse", and one row and the full frame both come back sparse. Fails on main, passes here.
  • test_explicit_formats_are_stored_as_resolved[dense], [sparse]: explicit formats are stored as given. Fails on main (no output_format_), passes here.

Reproduction

Output of the script from #73 on this branch:

ndarray ndarray
0 of 442 single rows come back sparse
[118.12429962]

Checks

  • just test: 1442 passed, 59 skipped, 7 xfailed; coverage 93.66% (gate 90%).
  • Minimum dependencies (CI job: Python 3.10, numpy 1.24.4, pandas 2.0.3, scipy 1.10.1, scikit-learn 1.6.0): 1429 passed, 72 skipped, 7 xfailed; scripts/quickstart.py passes.
  • just docs: strict build succeeds.
  • just check: all pre-commit and pre-push hooks pass (ruff format, ruff lint, prettier, file hygiene, pyright with 0 errors).
  • Import direction between subpackages is unchanged.

Merge notes

This change was previously part of #75, which is closed in favour of one pull request per issue.

With output_format='auto' the sparse/dense choice was re-made on every
transform call from that batch's density, so the same fitted preprocessor
returned a dense array for some batches and a CSR matrix for others (often
single rows), breaking dense-only estimators at predict time.

The choice is now made once at fit from the training output (which the inner
ColumnTransformer computes anyway, since its fit runs fit_transform) and
stored in the new fitted attribute output_format_; transform uses it, and
output_report_ still reports each call's own density. Explicit "dense" and
"sparse" are stored unchanged.

Preprocessors fitted by 1.0.0 (loaded from a spec or a pickle) have no
output_format_ and keep the per-call choice. Newly fitted preprocessors carry
the new attribute in their state, so their fingerprint differs from one
computed by 1.0.0.

Fixes #73

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

output_format="auto" chooses sparse vs dense per transform batch, breaking dense-only estimators at predict

1 participant