Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
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
101 changes: 101 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 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.

## 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. When a function already returns a strict bool (e.g.
`nwd.is_pandas_dataframe(X)`), assign it directly:
`is_pandas = nwd.is_pandas_dataframe(X)`, not
`is_pandas = nwd.is_pandas_dataframe(X) is True`. The `if`/`while` site
that later consumes `is_pandas` still spells out `if is_pandas is True:`.

## Comments

Max 2 lines. Only explain a non-obvious WHY (a hidden constraint, a subtle
backend difference, a workaround) — 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.

## 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.

## Tests

- `pytest.raises(ExceptionType, match=msg)`, never
`with pytest.raises() as record: ... assert str(record.value) == msg`.
- Dataframe-agnostic means one test, both backends: parametrize each
behavior over `@pytest.mark.parametrize("make_df", [pd.DataFrame,
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 nullable extension dtypes).

## 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.
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
54 changes: 54 additions & 0 deletions docs/user_guide/creation/CyclicalFeatures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,60 @@ This returns the name of all the variables in the final output:
['day_sin', 'day_cos', 'months_sin', 'months_cos']


With polars
-----------

:class:`CyclicalFeatures()` works in the same way with a polars dataframe.
Let's create an equivalent toy dataframe:

.. code:: python

import polars as pl
from feature_engine.creation import CyclicalFeatures

df = pl.DataFrame({
"day": [6, 7, 5, 3, 1, 2, 4],
"months": [3, 7, 9, 12, 4, 6, 12],
})

cyclical = CyclicalFeatures(variables=None, drop_original=False)
X = cyclical.fit_transform(df)

cyclical.max_values_

The maximum values match those found with pandas:

.. code:: python

{'day': 7, 'months': 12}

And the transformed dataframe contains the same cyclical features:

.. code:: python

print(X)

.. code:: text

shape: (7, 6)
┌─────┬────────┬─────────────┬───────────┬─────────────┬─────────────┐
│ day ┆ months ┆ day_sin ┆ day_cos ┆ months_sin ┆ months_cos │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞═════╪════════╪═════════════╪═══════════╪═════════════╪═════════════╡
│ 6 ┆ 3 ┆ -0.781831 ┆ 0.62349 ┆ 1.0 ┆ 6.1232e-17 │
│ 7 ┆ 7 ┆ -2.4493e-16 ┆ 1.0 ┆ -0.5 ┆ -0.866025 │
│ 5 ┆ 9 ┆ -0.974928 ┆ -0.222521 ┆ -1.0 ┆ -1.8370e-16 │
│ 3 ┆ 12 ┆ 0.433884 ┆ -0.900969 ┆ -2.4493e-16 ┆ 1.0 │
│ 1 ┆ 4 ┆ 0.781831 ┆ 0.62349 ┆ 0.866025 ┆ -0.5 │
│ 2 ┆ 6 ┆ 0.974928 ┆ -0.222521 ┆ 1.2246e-16 ┆ -1.0 │
│ 4 ┆ 12 ┆ -0.433884 ┆ -0.900969 ┆ -2.4493e-16 ┆ 1.0 │
└─────┴────────┴─────────────┴───────────┴─────────────┴─────────────┘

`drop_original=True` and `get_feature_names_out()` work identically to the
pandas example above.


Understanding cyclical encoding
-------------------------------

Expand Down
Loading