Repository navigation
Conversation
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 was referenced Oct 16, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #94.
Preprocessor.fitandtransformfailed with anAttributeErroron a polars DataFrame. Every scikit-learnPipelinewithset_output(transform="polars")and a step before thePreprocessortherefore crashed, and so didRepresentationSearchCV.fiton a polars frame.Root cause
to_dataframetreated every input that was not a dict or a NumPy array as a pandas DataFrame. A polars frame's.columnsis a list, so the duplicate-label check (.columns.duplicated()) failed atfit, and a polars frame has no.copy(), sotransformfailed too.RepresentationSearchCV.fittook fold rows with_row_subset, which turns anything without.ilocinto 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 theto_dataframefailure.Fix
pretab/core/validation.py: newis_polars_frame, which detects a polars frame throughsys.modulesand never imports polars. Also newpolars_to_pandas, which converts column by column withSeries.to_numpy, sopyarrowis not needed (polars.DataFrame.to_pandasrequires 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, becomeobjectcolumns, with nulls mapped toNaN.to_dataframe(pretab/compose/feature_detection.py) converts a polars frame withpolars_to_pandas. Input without column labels (newhas_column_labels) is read withnp.asarrayand must be 2D, so a list of rows works like an array. A pandas Series is rejected with aPretabDataError.RepresentationSearchCV.fitconverts 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
feature_0,feature_1, ...). It used to fail with anAttributeError.PretabDataError("Expected 2D input (a DataFrame or a 2D array), got ...", with aFix:hint) instead of anAttributeErrororIndexError.has_column_labels(inpretab.compose.feature_detection.__all__) andis_polars_frame/polars_to_pandas(inpretab.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 nopyarrowis needed and that temporal columns are not supported. It ends with aPipeline/set_output(transform="polars")example.docs/getting_started/installation.md: thepolarsextra note says a polars DataFrame is also accepted as input and links that section.README.md(two places) anddocs/homepage.md: the supported input types includepolars.DataFrame.feature_detectionmodule,to_dataframe(includingRaises),has_column_labels,is_polars_frame,polars_to_pandasandRepresentationSearchCV.fit. InPreprocessor, the class feature list and theXtype offit,transform,fit_transform,estimate_output_shapeandestimate_memory.Tests
test_to_dataframe_converts_polars_frame_with_names_order_and_dtypes: names, order, exact dtypes and null-to-NaNcounts for Float64, Int64 (with and without null), Boolean (with and without null), String and Categorical columns. Fails onmain, 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 onmain, passes here.test_to_dataframe_reads_a_list_of_rows_as_an_array: a list of rows becomesfeature_0,feature_1. Fails onmain, passes here.test_to_dataframe_rejects_input_that_is_not_2d[Series, 1D array, flat list]:PretabDataErrorwith the 2D message. Fails onmain, passes here.test_polars_input_matches_pandas_input[minmax, bspline, ple + one-hot]: a polars frame gives the same detected column types, output names, andtransform/fit_transformvalues as the pandas frame. A pandas-fittedPreprocessortransforms the polars frame identically, and a reordered polars frame is matched by name. Fails onmain, passes here.test_polars_input_with_polars_output: polars in, polars out, with the fitted output names and width. Fails onmain, passes here.test_polars_nulls_are_missing_values: a polars null is imputed and gets a missing indicator, not a"None"category. Fails onmain, passes here.test_representation_search_on_polars_matches_pandas:cv_results_,best_method_and predictions equal those for the pandas frame. Fails onmain, and also with only theto_dataframechange. 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 onmain, passes here.test_a_list_of_rows_is_read_like_an_array: aPreprocessorfitted on a list of rows names its columnsfeature_0,feature_1and transforms the list like the array. Fails onmain, passes here.test_pipeline_set_output_hands_frames_to_the_preprocessor[polars]: aSimpleImputer→Preprocessorpipeline returns a polars frame with columnsnum_income,num_visits. Fails onmain, 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.fitcall printsokwhen it succeeds. Two calls are added for the other failures the issue describes:transformof a polars frame after a pandas fit, andRepresentationSearchCV.fiton a polars frame, compared with pandas. Onmainthe last four lines areAttributeErrors. The environment has nopyarrow.Checks
just test: 1454 passed, 59 skipped, 7 xfailed; coverage 93.70% (gate 90%).scripts/quickstart.pypasses.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).Merge notes
main.tests/integration/test_preprocessor.py: the PRs for Preprocessor.fit crashes on any bool column under default settings #54 (fix(preprocessor): encode boolean columns as binary categoricals #99), Preprocessor(numerical_method="box-cox") raises at transform for values below the training minimum #59 (fix(preprocessor): keep box-cox finite below the training minimum #104), Preprocessor.get_feature_names_out(input_features) fails after fitting on an ndarray, breaking Pipeline.get_feature_names_out() #65 (fix(preprocessor): accept input_features after fitting on a NumPy array #108), Preprocessor.transform does not check n_features_in_ for ndarray input; extra columns are silently misaligned #72 (fix(preprocessor): reject NumPy input of the wrong width at transform #112), Preprocessor fitted on a DataFrame rejects a same-width NumPy array at transform (and vice versa) #78 (fix(preprocessor): match array input by position after a named fit #116) and A 2-D y (multi-output target) crashes the B/M/I splines even when unsupervised, and target-aware methods with unrelated errors #88 (fix(splines): ignore y when unsupervised and reject a multi-output target #123) also append a block at the end of the file. Keep every block.pretab/compose/feature_detection.py,__all__: this PR addshas_column_labels(with the function below it), the PR for Preprocessor.fit crashes on any bool column under default settings #54 (fix(preprocessor): encode boolean columns as binary categoricals #99) addsbool_columns_as_objectand the PR for Preprocessor routes integer column labels positionally, silently swapping columns #55 (fix(preprocessor): route columns to the ColumnTransformer by label #100) addswith_string_labels. Keep all the names and functions.pretab/core/validation.py, imports,__all__and the new functions after it: the PR for Standalone transformers drop DataFrame feature names, so reordered columns are silently misaligned #85 (fix(transformers): record and check DataFrame feature names #135) addsresolve_input_featuresand extends thesklearn.utils.validationimport, and the PR for A 2-D y (multi-output target) crashes the B/M/I splines even when unsupervised, and target-aware methods with unrelated errors #88 (fix(splines): ignore y when unsupervised and reject a multi-output target #123) addssingle_target. Keep all of them, plus this PR'simport sys/import pandas as pd.Preprocessor.transformdocstring: the PRs for Preprocessor routes integer column labels positionally, silently swapping columns #55 (fix(preprocessor): route columns to the ColumnTransformer by label #100) and Preprocessor fitted on a DataFrame rejects a same-width NumPy array at transform (and vice versa) #78 (fix(preprocessor): match array input by position after a named fit #116) each extend the description ofX. Keep their text, with this PR'spolars.DataFramein the type line.docs/core_concepts/configuration.md: the PR for Preprocessor fitted on a DataFrame rejects a same-width NumPy array at transform (and vice versa) #78 (fix(preprocessor): match array input by position after a named fit #116) adds a paragraph on positional matching at the place where this PR starts its "Polars DataFrame input" section. Keep Preprocessor fitted on a DataFrame rejects a same-width NumPy array at transform (and vice versa) #78's paragraph first (it continues the array-input text) and the new section after it.RepresentationSearchCV.fitdocstring: the PR for RepresentationSearchCV cannot use group-based CV splitters and accepts no groups or fit parameters #91 (fix(search): pass groups to the splitter and fit params to the estimator #126) addsParameters/Returnssections. Keep them, withX : pandas.DataFrame, polars.DataFrame or array-like of shape (n_samples, n_features), and keep this PR's paragraph on the polars conversion.RangeIndexin pandas output, because polars has no index.CrossFittedTransformerturns pandas and polars frames alike into NumPy arrays onmain, so it is not changed here. After CrossFittedTransformer converts DataFrames to ndarray; a wrapped Preprocessor treats every column as categorical #58, its_as_2dpasses pandas frames through, so addif is_polars_frame(X): return polars_to_pandas(X)there._as_2druns once before the fold rows are taken, so the frame is converted once. Then nameCrossFittedTransformerin the configuration.md section and addtest_cross_fitted_preprocessor_on_polars_matches_pandasandtest_cross_fitting_converts_a_polars_frame_once._align_inputdecides positional matching withisinstance(X, np.ndarray). Usenot has_column_labels(X)there, so a list of rows passed totransformafter a frame fit is matched by position like an array. Addtest_a_list_of_rows_is_matched_by_position_after_a_frame_fit, and add the sentence "A frame or dict is matched by column name, an array (or a list of rows) by position" to thefitdocstring.fitrecordsfeature_names_in_unlessisinstance(X, np.ndarray), so a list of rows would record the syntheticfeature_0, ... names. Usenot has_column_labels(X)there as well, and add the assertionslist(from_polars.feature_names_in_) == polars_frame.columns(polars test) andnot hasattr(pre, "feature_names_in_")(list-of-rows test).maina boolean column fails inSimpleImputerfor pandas and polars input alike, so the polars test fixture uses String and Categorical columns. Once Preprocessor.fit crashes on any bool column under default settings #54 lands, add a polarsBooleancolumn to the fixture.This change was previously part of #97, which is closed in favour of one pull request per issue.