Skip to content

fix(preprocessor): accept polars DataFrames as input - #129

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/94-polars-input
Open

ChrisW09 wants to merge 1 commit into
mainfrom
fix/94-polars-input

Conversation

@ChrisW09

@ChrisW09 ChrisW09 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #94.

Preprocessor.fit and transform failed with an AttributeError on a polars DataFrame. Every scikit-learn Pipeline with set_output(transform="polars") and a step before the Preprocessor therefore crashed, and so did RepresentationSearchCV.fit on a polars frame.

Root cause

to_dataframe treated every input that was not a dict or a NumPy array as a pandas DataFrame. A polars frame's .columns is a list, so the duplicate-label check (.columns.duplicated()) failed at fit, and a polars frame has no .copy(), so transform failed too. RepresentationSearchCV.fit took fold rows with _row_subset, which turns anything without .iloc into a NumPy array. The folds therefore ran on unnamed arrays (an object array for mixed-type data, so every column became categorical), and the final refit on the polars frame hit the to_dataframe failure.

Fix

  • pretab/core/validation.py: new is_polars_frame, which detects a polars frame through sys.modules and never imports polars. Also new polars_to_pandas, which converts column by column with Series.to_numpy, so pyarrow is not needed (polars.DataFrame.to_pandas requires it). Names and order are kept. Integer, float and boolean columns keep their NumPy dtype, and an integer column with nulls becomes float, as in pandas. String, categorical and enum columns, and boolean columns with nulls, become object columns, with nulls mapped to NaN.
  • to_dataframe (pretab/compose/feature_detection.py) converts a polars frame with polars_to_pandas. Input without column labels (new has_column_labels) is read with np.asarray and must be 2D, so a list of rows works like an array. A pandas Series is rejected with a PretabDataError.
  • RepresentationSearchCV.fit converts a polars frame once, before taking fold rows. Every fold then sees the column types of the whole frame, as the refit does. Converting each fold's rows separately could give a different dtype, for example an integer column whose only null lies outside the fold.

Behaviour changes

  • A list of rows is accepted like a 2D array (columns feature_0, feature_1, ...). It used to fail with an AttributeError.
  • A pandas Series, a 1D array or a flat list raises PretabDataError ("Expected 2D input (a DataFrame or a 2D array), got ...", with a Fix: hint) instead of an AttributeError or IndexError.
  • New helpers has_column_labels (in pretab.compose.feature_detection.__all__) and is_polars_frame / polars_to_pandas (in pretab.core.validation.__all__).

Documentation

  • docs/core_concepts/configuration.md: new "Polars DataFrame input" section. It covers where polars input is accepted (Preprocessor, RepresentationSearchCV), the dtype mapping and column-type detection, nulls as missing values, that no pyarrow is needed and that temporal columns are not supported. It ends with a Pipeline / set_output(transform="polars") example.
  • docs/getting_started/installation.md: the polars extra note says a polars DataFrame is also accepted as input and links that section.
  • README.md (two places) and docs/homepage.md: the supported input types include polars.DataFrame.
  • Docstrings: the feature_detection module, to_dataframe (including Raises), has_column_labels, is_polars_frame, polars_to_pandas and RepresentationSearchCV.fit. In Preprocessor, the class feature list and the X type of fit, transform, fit_transform, estimate_output_shape and estimate_memory.

Tests

  • test_to_dataframe_converts_polars_frame_with_names_order_and_dtypes: names, order, exact dtypes and null-to-NaN counts for Float64, Int64 (with and without null), Boolean (with and without null), String and Categorical columns. Fails on main, passes here.
  • test_detect_column_types_on_polars_frame: float and high-cardinality integer columns are numerical. Low-cardinality integer, String, Boolean and Categorical columns are categorical. Fails on main, passes here.
  • test_to_dataframe_reads_a_list_of_rows_as_an_array: a list of rows becomes feature_0, feature_1. Fails on main, passes here.
  • test_to_dataframe_rejects_input_that_is_not_2d[Series, 1D array, flat list]: PretabDataError with the 2D message. Fails on main, passes here.
  • test_polars_input_matches_pandas_input[minmax, bspline, ple + one-hot]: a polars frame gives the same detected column types, output names, and transform / fit_transform values as the pandas frame. A pandas-fitted Preprocessor transforms the polars frame identically, and a reordered polars frame is matched by name. Fails on main, passes here.
  • test_polars_input_with_polars_output: polars in, polars out, with the fitted output names and width. Fails on main, passes here.
  • test_polars_nulls_are_missing_values: a polars null is imputed and gets a missing indicator, not a "None" category. Fails on main, passes here.
  • test_representation_search_on_polars_matches_pandas: cv_results_, best_method_ and predictions equal those for the pandas frame. Fails on main, and also with only the to_dataframe change. Passes here.
  • test_search_converts_a_polars_frame_once: with an Int64 column holding a single null, the search scores equal those for the pandas frame. This pins the single conversion before the folds are taken. Fails on main, passes here.
  • test_a_list_of_rows_is_read_like_an_array: a Preprocessor fitted on a list of rows names its columns feature_0, feature_1 and transforms the list like the array. Fails on main, passes here.
  • test_pipeline_set_output_hands_frames_to_the_preprocessor[polars]: a SimpleImputer → Preprocessor pipeline returns a polars frame with columns num_income, num_visits. Fails on main, passes here. [pandas] is the same check for pandas output. Guard test: passes on both by design.

Reproduction

Output of the script from #94 on this branch. The Preprocessor.fit call prints ok when it succeeds. Two calls are added for the other failures the issue describes: transform of a polars frame after a pandas fit, and RepresentationSearchCV.fit on a polars frame, compared with pandas. On main the last four lines are AttributeErrors. The environment has no pyarrow.

sklearn ColumnTransformer, polars input -> (200, 2)
Pipeline.set_output('pandas') -> pandas.core.frame
Pipeline.set_output('polars') -> polars.dataframe.frame
Preprocessor.fit(polars DataFrame) -> ok
pandas fit, transform(polars DataFrame) -> equal to transform(pandas): True
RepresentationSearchCV.fit(polars DataFrame) -> best_method_: minmax | cv_results_ equal to pandas: True

Checks

  • just test: 1454 passed, 59 skipped, 7 xfailed; coverage 93.70% (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): 1431 passed, 76 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 #97, which is closed in favour of one pull request per issue.

to_dataframe treated every input that was not a dict or NumPy array as a
pandas frame, so Preprocessor.fit / transform on a polars DataFrame
failed with "AttributeError: 'list' object has no attribute
'duplicated'". A scikit-learn Pipeline with set_output(transform="polars")
therefore crashed as soon as it handed its polars output to the
Preprocessor.

A polars frame (detected via sys.modules, so polars is never imported) is
now converted to pandas column by column with Series.to_numpy, which
needs no pyarrow: names, order and numeric / boolean dtypes are kept,
string, categorical and enum columns become object columns, and nulls
become NaN so imputation and missing_policy see them as missing.
Column-type detection, get_feature_names_out, label matching at transform
and polars output work as for pandas input.

RepresentationSearchCV converts a polars frame once, before taking the
fold rows, so every fold's Preprocessor sees the column types of the
whole frame, as the final refit does, instead of an object array that
turns every column categorical and scores a different model than the
one refit.

Input without column labels is now read with np.asarray, so a list of
rows works like an array, while a pandas Series or 1D input raises a
PretabDataError instead of an AttributeError.

Fixes #94

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.

Preprocessor rejects polars DataFrames, so pipelines with set_output(transform="polars") crash

1 participant