Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
fbbf160
start narwhals migration
solegalli Jul 21, 2026
7f33552
fix doc test
solegalli Jul 21, 2026
ce81a1e
refactor dataframe checks to accept narwahls dfs (#966)
solegalli Jul 26, 2026
4025eec
Remove sklearn<=1.6 compatibility code (#985)
solegalli Jul 31, 2026
a814ae4
address review feedback on dataframe_checks.py (#989)
solegalli Aug 24, 2026
ff5d212
refactor variable handling module for narwahls (#978)
solegalli Aug 24, 2026
45b9d1a
refactor creation base for narwhals compatibility (#990)
solegalli Aug 24, 2026
0d32d83
Migrate CyclicalFeatures to narwhals, add polars support (#991)
solegalli Aug 24, 2026
13187db
Migrate GeoDistanceFeatures to narwhals, add polars support (#993)
solegalli Aug 24, 2026
85be399
Migrate MathFeatures to narwhals, add polars support (#994)
solegalli Aug 24, 2026
295f9d1
Migrate RelativeFeatures to narwhals+numpy, add polars support (#995)
solegalli Aug 24, 2026
ed91d48
Migrate DecisionTreeFeatures to narwhals, add polars support (#996)
solegalli Aug 24, 2026
b766a5b
Migrate ReciprocalTransformer to narwhals, add polars support (#997)
solegalli Aug 25, 2026
53a11b9
Migrate ArcsinTransformer to narwhals, add polars support (#998)
solegalli Aug 25, 2026
9b3b226
Migrate ArcSinhTransformer to narwhals, add polars support (#1000)
solegalli Aug 25, 2026
0e105a9
Migrate PowerTransformer to narwhals, add polars support (#1005)
solegalli Aug 25, 2026
08a06a0
Migrate BoxCoxTransformer to narwhals, add polars support (#1006)
solegalli Aug 25, 2026
5695fa7
Migrate YeoJohnsonTransformer to narwhals, add polars support (#1007)
solegalli Aug 25, 2026
ddbb256
Migrate LogTransformer/LogCpTransformer to narwhals, add polars suppo…
solegalli Aug 25, 2026
fd4f8ee
Short-circuit _check_contains_na when variables list is empty (#1018)
solegalli Aug 30, 2026
ec26810
Return narwhals frame from check_X / check_X_y (#1019)
solegalli Aug 30, 2026
50b2d3a
Adapt creation transformers to narwhals-returning check_X (#1020)
solegalli Aug 30, 2026
0fca9c6
Adapt numerical base transformers to narwhals-returning check_X (#1022)
solegalli Aug 30, 2026
04c88dc
Migrate BaseImputer to narwhals, add polars support (#1002)
solegalli Aug 30, 2026
211d79e
update base imputer again (#1023)
solegalli Aug 30, 2026
73c0041
Narwhals mean median imputer (#1015)
solegalli Aug 30, 2026
6e0a299
Narwhals end tail imputer (#1013)
solegalli Aug 30, 2026
8049df6
Migrate ArbitraryImputer to narwhals, add polars support (#1003)
solegalli Aug 30, 2026
614493f
migrate missing indicator to narwhals, add polar support (#1001)
solegalli Aug 30, 2026
13ac25d
Narwhals random sample imputer (#1014)
solegalli Aug 30, 2026
2f24611
Narwhals drop missing data (#1016)
solegalli Aug 30, 2026
1fc1ea7
Narwhals categorical imputer (#1009)
solegalli Aug 30, 2026
6def4de
Fix RandomSampleImputer and DropMissingData on non-pandas backends (#…
solegalli Aug 30, 2026
a98990a
Migrate DatetimeOrdinal to narwhals+numpy, add polars support (#1010)
solegalli Aug 30, 2026
d50bb28
Migrate DatetimeFeatures to narwhals, add polars support (#1011)
solegalli Aug 30, 2026
2b8bcf3
Migrate DatetimeSubtraction to narwhals+numpy, add polars support (#1…
solegalli Aug 30, 2026
b430464
Migrate CategoricalMethodsMixin (encoding base) to narwhals, add pola…
solegalli Aug 30, 2026
dd77a3b
Migrate CountEncoder/CountFrequencyEncoder to narwhals, add polars su…
solegalli Sep 14, 2026
c6ee2d7
Add shared backend test fixtures and helpers for narwhals-migrated te…
solegalli Sep 15, 2026
d6e2bf9
Use shared backend test structure in imputation tests, fix DropMissin…
solegalli Sep 15, 2026
b63a13b
Migrate MeanEncoder.fit() to narwhals, add polars support (#1027)
solegalli Sep 15, 2026
79d9210
Add add_target_to_X helper and check init parameter types in encoders…
solegalli Sep 15, 2026
f83c278
Migrate OrdinalEncoder.fit() to narwhals, add polars support (#1029)
solegalli Sep 15, 2026
9e4e99b
Migrate WoEEncoder to narwhals, add polars support (#1032)
solegalli Sep 15, 2026
3dca47a
Add conventions from encoder and imputer work to AGENTS.md (#1048)
solegalli Sep 15, 2026
e2e3ff3
Shorten n_jobs docstring in DecisionTreeFeatures (#1049)
solegalli Sep 15, 2026
e8a5ec7
Migrate OneHotEncoder to narwhals, add polars support (#1028)
solegalli Sep 15, 2026
d2cf470
Migrate RareLabelEncoder to narwhals, add polars support (#1030)
solegalli Sep 16, 2026
9645583
Migrate StringSimilarityEncoder to narwhals, add polars support (#1031)
solegalli Sep 16, 2026
0cd65c3
Migrate BaseDiscretiser to narwhals, add polars support (#1037)
solegalli Sep 18, 2026
d2bd9a1
Migrate ArbitraryDiscretiser to narwhals, add polars support (#1038)
solegalli Sep 18, 2026
1003a7d
Migrate EqualWidthDiscretiser to narwhals, add polars support (#1040)
solegalli Sep 18, 2026
c5e5a2d
Migrate EqualFrequencyDiscretiser.fit() to narwhals, add polars suppo…
solegalli Sep 18, 2026
c3cfc9d
Migrate GeometricWidthDiscretiser.fit() to narwhals, add polars suppo…
solegalli Sep 18, 2026
9a875cf
Note typical data sizes per backend for benchmarks in AGENTS.md (#1052)
solegalli Sep 18, 2026
441ed3e
Migrate DecisionTreeDiscretiser to narwhals, add polars support (#1042)
solegalli Sep 18, 2026
02dc270
Support integer column names in DecisionTreeDiscretiser and EqualWidt…
solegalli Sep 18, 2026
7df586c
Migrate DecisionTreeEncoder to narwhals, add polars support (#1026)
solegalli Sep 18, 2026
491f945
Call is_pandas_dataframe in conditions instead of storing it in creat…
solegalli Sep 18, 2026
e991a95
[MNT] migrate scaling module to narwhals (#1004)
ojassharma7 Sep 18, 2026
4241811
Move discretiser helper functions into their classes as private metho…
solegalli Sep 18, 2026
62cbea8
Note in AGENTS.md that user-facing docs are written for users, not ma…
solegalli Sep 18, 2026
be430f6
Note in AGENTS.md to call boolean checks directly in conditions (#1056)
solegalli Sep 18, 2026
2e0841f
Use the numpydoc Returns section header so return values render (#1044)
VenishPaneliya Sep 18, 2026
25a76f3
Migrate BaseOutlier and WinsorizerBase to narwhals, add polars suppor…
solegalli Sep 19, 2026
16262df
Migrate Winsoriser/Winsorizer to narwhals, add polars support (#1036)
solegalli Sep 19, 2026
74958e0
Return narwhals frames from BaseNumericalTransformer's fit and transf…
solegalli Sep 19, 2026
a54622a
Migrate ArbitraryOutlierCapper to narwhals, add polars support (#1034)
solegalli Sep 19, 2026
bd6c064
Migrate OutlierTrimmer to narwhals, add polars support (#1035)
solegalli Sep 19, 2026
a957f70
Merge remote-tracking branch 'origin/main' into merge-main-into-narwhals
solegalli Sep 19, 2026
7c22dfa
Merge pull request #1062 from feature-engine/merge-main-into-narwhals
solegalli Sep 19, 2026
b0ec8b5
Speed up the missing-value check (#1072)
solegalli Sep 19, 2026
c8ae828
Migrate MatchVariables to narwhals, add polars support (#1063)
solegalli Sep 19, 2026
35fe69e
Migrate MatchCategories to narwhals, add polars support (#1064)
solegalli Sep 19, 2026
91d01de
Migrate TextFeatures to narwhals, add polars support (#1074)
solegalli Sep 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 4 additions & 64 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ orbs:

defaults: &defaults
docker:
- image: cimg/python:3.10.0
- image: cimg/python:3.12.1
working_directory: ~/project

prepare_tox: &prepare_tox
Expand Down Expand Up @@ -42,58 +42,6 @@ jobs:
# Test matrix
# ------------------------

test_feature_engine_py39:
docker:
- image: cimg/python:3.9.0
working_directory: ~/project
steps:
- checkout:
path: ~/project
- *prepare_tox
- run:
name: Run tests (Python 3.9)
command: |
tox -e py39

test_feature_engine_py310:
docker:
- image: cimg/python:3.10.0
working_directory: ~/project
steps:
- checkout:
path: ~/project
- *prepare_tox
- run:
name: Run tests (Python 3.10)
command: |
tox -e py310

test_feature_engine_py311_sklearn150:
docker:
- image: cimg/python:3.11.7
working_directory: ~/project
steps:
- checkout:
path: ~/project
- *prepare_tox
- run:
name: Run tests (Python 3.11, scikit-learn 1.5)
command: |
tox -e py311-sklearn150

test_feature_engine_py311_sklearn160:
docker:
- image: cimg/python:3.11.7
working_directory: ~/project
steps:
- checkout:
path: ~/project
- *prepare_tox
- run:
name: Run tests (Python 3.11, scikit-learn 1.6)
command: |
tox -e py311-sklearn160

test_feature_engine_py311_sklearn170:
docker:
- image: cimg/python:3.11.7
Expand Down Expand Up @@ -166,7 +114,7 @@ jobs:

test_style:
docker:
- image: cimg/python:3.10.0
- image: cimg/python:3.12.1
working_directory: ~/project
steps:
- checkout:
Expand All @@ -179,7 +127,7 @@ jobs:

test_docs:
docker:
- image: cimg/python:3.10.0
- image: cimg/python:3.12.1
working_directory: ~/project
steps:
- checkout:
Expand All @@ -192,7 +140,7 @@ jobs:

test_type:
docker:
- image: cimg/python:3.10.0
- image: cimg/python:3.12.1
working_directory: ~/project
steps:
- checkout:
Expand Down Expand Up @@ -277,10 +225,6 @@ workflows:

test-all:
jobs:
- test_feature_engine_py39
- test_feature_engine_py310
- test_feature_engine_py311_sklearn150
- test_feature_engine_py311_sklearn160
- test_feature_engine_py311_sklearn170
- test_feature_engine_py312_pandas230
- test_feature_engine_py312_pandas300
Expand All @@ -298,10 +242,6 @@ workflows:

- package_and_upload_to_pypi:
requires:
- test_feature_engine_py39
- test_feature_engine_py310
- test_feature_engine_py311_sklearn150
- test_feature_engine_py311_sklearn160
- test_feature_engine_py311_sklearn170
- test_feature_engine_py312_pandas230
- test_feature_engine_py312_pandas300
Expand Down
205 changes: 205 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# AGENTS.md

Conventions for working in this repo. Optimize for readability and speed,
in that order of how you decide, but don't ship a slow default when a
fast one is free.

## Inputs

Feature-engine transformers take dataframes (pandas, polars, or any other
narwhals-supported backend) as input, not numpy arrays. Don't add
handling for array input.

## Never import pandas in library code

pandas is an optional dependency (see `pyproject.toml` — it lives under
`[project.optional-dependencies]`, not core `dependencies`), so `import
pandas` must never appear anywhere in `feature_engine/`, not at module level
and not locally/lazily inside a function either — importing the module
itself would break a polars-only install regardless of which class is used.

Backend checks go through `narwhals.dependencies` (`nwd.is_pandas_dataframe`,
`nwd.is_pandas_series`, `nwd.is_pandas_index`, `nwd.is_into_series`, etc.).
Once a branch is confirmed pandas, call its methods/attributes directly on
the object already in hand (`.loc`, `.columns`, `.index`, `.select_dtypes`,
...) — no import needed for that, since Python only needs a module imported
to reference the module itself (`pd.something`), not to call methods on an
object that's already an instance of that module's class.

## Transformer code

- `check_X` and `check_X_y` return a narwhals dataframe. Bind it as
`nw_X = check_X(X)`, pass the native `X` to the variable, NaN and feature-name
helpers, compute on `nw_X`, and return `.to_native()`.
- When pandas keeps a native fast path, branch with
`if nwd.is_pandas_dataframe(X):`, comment it with
`# pandas is faster than narwhals.` and put the narwhals code in `else`.
- To use the target together with `X`, call `add_target_to_X(nw_X, y)` from
`feature_engine/encoding/_helper_functions.py` and read it with `TARGET_NAME`.
It works for series, list and array targets, and with pandas the column takes
the index of `X`. Don't write this pairing again in a transformer.
- In the narwhals path, prefer narwhals expressions over Python loops on grouped
results: aggregate with simple aggregations, then combine the columns in a
`select`.
- narwhals expressions need string column names, while pandas allows integers.
Take such columns with `get_column` and rename them before using expressions.
- Name temporary columns with double underscores (`__mean__`, `__count__`) so
they can't clash with the user's columns.
- Don't add `# type: ignore`. If mypy complains because a parameter typed
`Optional` is reassigned, store the value under a new name instead (for
example `y_pd`).

## Init parameters

- Validate parameters in `__init__` only. Don't check them again in `fit` or
`transform`, and don't test for errors raised by changing an attribute after
init.
- For parameters that take a set of strings, check the type before the
membership test, so lists, tuples, `None` and numbers raise the same error:

```python
if not isinstance(encoding_method, str) or encoding_method not in [
"ordered",
"arbitrary",
]:
```

- Error messages follow the scikit-learn convention and end with
`f"Got {param} instead."`.

## Booleans and control flow

- Compare booleans explicitly: `if x is True:` / `if x is False:`, never
`if x:` / `if not x:`.
- Check container emptiness with `len(x) == 0`, never `if not x:`.
- `isinstance(...)` checks and `in`/`not in` membership tests are already
explicit — leave them as-is, this rule isn't about those.
- The explicit `is True`/`is False` comparison is for flow control
(`if`/`while` conditions) only — don't tack it onto a variable
assignment.
- Call boolean checks such as `nwd.is_pandas_dataframe(X)` directly in the
condition, `if nwd.is_pandas_dataframe(X) is True:`, instead of storing the
result in a variable (`is_pandas = ...`) and testing that later.

## Comments

One line, two at most, in source code and tests. Only explain a non-obvious
WHY (a hidden constraint, a subtle backend difference, a workaround) — what the
reader needs to know about the code — never describe WHAT the code does.

## Don't anticipate errors

Don't add error handling or validation for scenarios that can't happen. If
unsure whether something can happen, check it (grep, run a quick repro) or
ask — don't guess and defensively code around it.

## Redundant lists/sets

- Narwhals' `.columns` is already `list[str]` — don't wrap it in `list()`.
- pandas' `.columns` is an `Index`, not a list — `list()` is required there
(an `Index == list` comparison is elementwise, not a clean bool).

## Keep tests passing when you change a function or class

Whenever you change a function or class, run its corresponding tests. If
they fail, resolve it — don't leave it — by figuring out whether the test
needs updating (e.g. it exercised behavior that's no longer supported) or
the implementation has a real bug, and fixing whichever one is wrong.

## Keep docs in sync with transformer changes

When new functionality is introduced in a transformer, update its
corresponding `docs/user_guide/<module>/<ClassName>.rst` with a short
worked example showing the new functionality. When behaviour changes, check
that the outputs shown in the user guide examples are still correct.

User guides and other user-facing documentation are written for users:
assume readers don't know the source code, and certainly not narwhals. Explain
what a feature does and when to use it, in plain terms, without implementation
details or references to how the code used to behave.

## Verify before applying

Benchmark before claiming a speedup, and diff old-vs-new output across
realistic and edge cases (empty/all-NaN, both backends, both dtype
branches) before trusting a rewrite — logic mistakes here are easy to make
and easy to miss without an actual comparison. Compare like with like: time
the same work (for example the whole `fit()`) before and after.

Benchmark a range of data sizes, but base the decision mainly on the sizes
each backend is typically used with: 10k to 500k rows for pandas, and 500k
rows and more for polars. Smaller and larger sizes are worth measuring, but
they weigh less in the decision.

## Tests

Every transformer test file has the same structure, so they are easy to
maintain:

```python
# init parameters
def test_error_if_<param>_not_allowed(...) # one test per error message
def test_init_param_assignment(...) # several valid value combinations

# fit and transform
...
```

- Init error tests are parametrized with wrong values and wrong types.
- `test_init_param_assignment` checks every init parameter except `variables`
and `return_empty`, which are tested elsewhere.
- Fit and transform tests don't assert init parameters.
- Every `pytest.raises` and `pytest.warns` matches the full message with
`match=re.escape(msg)`, including `NotFittedError` and messages that come
from scikit-learn. Never use
`with pytest.raises() as record: ... assert str(record.value) == msg`.
Matching the full message catches tests that pass for the wrong reason.

Backends and data:

- Dataframe-agnostic means one test, both backends: request the `make_df`
fixture from `tests/conftest.py`, which runs the test with `pd.DataFrame`
and `pl.DataFrame`, and assert the same input produces the same output
values on both. Never write a separate pandas-only test and a
separate polars-only test for the same behavior — that duplicates
the test and hides the point of being dataframe-agnostic, which is
that the same input gives the same output regardless of backend.
Keep a test single-backend only when the behavior itself is
backend-specific (e.g. integer column names, which polars doesn't
support; pandas category or nullable extension dtypes), and check those
with `pd.testing.assert_frame_equal`.
- Use the helpers in `tests/backend_helpers.py`: `frame_to_dict`, `null_count`
and `make_series`. Don't add per-file helpers that do the same.
- Data used by several test files of a module lives in that module's
`conftest.py`, as fixtures that return plain dicts, with `None` for missing
values. Data used by one file stays in that file.
- Pass the target as a series built with `make_series`, and add one test with
the target as a list and as a numpy array.
- Check outputs with `assert isinstance(Xt, make_df)` and compare
`frame_to_dict(Xt)` with a dict. Compare floats with `pytest.approx`.
- Don't call polars' `to_pandas()` in tests: pyarrow is not installed locally
or in CI.
- Name helpers after what they return (`frame_to_dict`, not `_cols`).

## API changes

- New parameters default to preserve current behavior.
- When adding a parameter to a function called from multiple sites (or a
shared private helper), thread it through every call site, not just the
one you're looking at.

## Before pushing

- Run the tests of the changed code, `flake8 feature_engine tests` (lines of 88
characters at most) and `mypy feature_engine`. Running mypy on single files
ignores the exclusions in `pyproject.toml`.
- If the target branch already has failing tests, compare the failing tests
before and after the change instead of expecting a clean run.

## Pull requests

- When a PR is built on another open PR and that one is squash-merged, rebase
with `git rebase --onto origin/<target branch> <old base tip>` so the PR shows
only its own files. Push with `--force-with-lease`.
- Don't end PR descriptions with an AI tool attribution line, such as
"Generated with Claude Code".
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ Feature-engine documentation is built using [Sphinx](https://www.sphinx-doc.org)

To build the documentation make sure you have the dependencies installed: from the root directory:
```
pip install -r docs/requirements.txt
pip install -e ".[docs]"
```

Now you can build the docs using:
Expand Down
2 changes: 1 addition & 1 deletion docs/contribute/contribute_code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ To do this, first make sure you have all the documentation dependencies installe
set up the environment as we described previously, they should be installed. Alternatively,
from the windows cmd or mac terminal, run::

$ pip install -r docs/requirements.txt
$ pip install -e ".[docs]"

Make sure you are within the feature_engine module when you run the previous command.

Expand Down
2 changes: 1 addition & 1 deletion docs/contribute/contribute_docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ dependencies. If you set up the development environment as we described in the
Alternatively, first activate your environment. Then navigate to the root folder of
feature-engine. And now install the requirements for the documentation::

$ pip install -r docs/requirements.txt
$ pip install -e ".[docs]"

To build the documentation (and test if it is working properly) run::

Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ The following characteristics make feature-engine unique:
Installation
------------

Feature-engine is a Python 3 package and works well with 3.9 or later.
Feature-engine is a Python 3 package and works well with 3.11 or later.

The simplest way to install feature-engine is from PyPI with pip:

Expand Down
Loading