Skip to content

Commit ff5d212

Browse files
solegalliclaude
andcommitted
refactor variable handling module for narwahls (#978)
* update variable handling module for narwahls * creating own datetime parser * Improve readability of narwhals date/type-check helpers, add missing tests Replace the double-parse-with-disagreeing-defaults trick in _looks_like_date_string with a direct call to dateutil's parser()._parse(), which exposes which date/time fields were actually found in a string without needing to approximate it - this also drops the now-unneeded sentinel default datetimes and the defensive str() coercion at its call site. Make truthiness checks and compound boolean returns explicit throughout the module, and restore the pre-narwhals function names that PR #978 had prefixed with _nw_ for no continuing reason. Rename test_fe_type_checks.py to test_variable_type_checks.py to match the module it tests, add docstrings, and add coverage for _looks_like_date_string and _is_categories_num, the two functions that previously had no direct tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Replace per-column schema access with bulk narwhals selectors for speed nw_X.schema is not cached - every access re-derives the full schema from the underlying native dataframe, so checking dtype-based conditions (is_numeric(), native Date/Datetime, categorical/enum/string) one column at a time inside a loop was quadratic instead of linear. Replace each such loop with a single nw_df.select(<selector>).columns call converted to a set, then a plain membership test per column - confirmed old vs new give identical results, and measured 8x-120x speedups depending on backend and column count. Also use by_dtype(Date, Datetime) to bulk-detect native datetime columns in one pass, only falling back to the expensive per-value _is_categorical_and_is_datetime check for columns that aren't already known to be numeric or natively datetime. Drop the now-unused _is_date_or_datetime import from both files. Simplify _looks_like_date_string's comment to link directly to the pandas source it mirrors, and instantiate dateutil's parser() per call instead of reusing a module-level instance. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refactor find variables: * refactor check_variables * final refactor of find and check variables * finalise migration of variable handling module * update user guide * Trim backend-difference notes from docs, revert datetime.py out of scope Removes the trailing pandas/polars note blocks from the check/find categorical and datetime variable docs, keeping them focused on the walkthrough. Reverts feature_engine/datetime/datetime.py to main - the DatetimeFeatures index-datetime fix needed there for the narwhals migration belongs in a separate datetime-module PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent a814ae4 commit ff5d212

24 files changed

Lines changed: 1488 additions & 520 deletions

‎AGENTS.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# AGENTS.md
2+
3+
Conventions for working in this repo. Optimize for readability and speed,
4+
in that order of how you decide, but don't ship a slow default when a
5+
fast one is free.
6+
7+
## Inputs
8+
9+
Feature-engine transformers take dataframes (pandas, polars, or any other
10+
narwhals-supported backend) as input, not numpy arrays. Don't add
11+
handling for array input.
12+
13+
## Booleans and control flow
14+
15+
- Compare booleans explicitly: `if x is True:` / `if x is False:`, never
16+
`if x:` / `if not x:`.
17+
- Check container emptiness with `len(x) == 0`, never `if not x:`.
18+
- `isinstance(...)` checks and `in`/`not in` membership tests are already
19+
explicit — leave them as-is, this rule isn't about those.
20+
21+
## Comments
22+
23+
Max 2 lines. Only explain a non-obvious WHY (a hidden constraint, a subtle
24+
backend difference, a workaround) — never describe WHAT the code does.
25+
26+
## Don't anticipate errors
27+
28+
Don't add error handling or validation for scenarios that can't happen. If
29+
unsure whether something can happen, check it (grep, run a quick repro) or
30+
ask — don't guess and defensively code around it.
31+
32+
## Redundant lists/sets
33+
34+
- Narwhals' `.columns` is already `list[str]` — don't wrap it in `list()`.
35+
- pandas' `.columns` is an `Index`, not a list — `list()` is required there
36+
(an `Index == list` comparison is elementwise, not a clean bool).
37+
38+
## Verify before applying
39+
40+
Benchmark before claiming a speedup, and diff old-vs-new output across
41+
realistic and edge cases (empty/all-NaN, both backends, both dtype
42+
branches) before trusting a rewrite — logic mistakes here are easy to make
43+
and easy to miss without an actual comparison.
44+
45+
## Tests
46+
47+
- `pytest.raises(ExceptionType, match=msg)`, never
48+
`with pytest.raises() as record: ... assert str(record.value) == msg`.
49+
50+
## API changes
51+
52+
- New parameters default to preserve current behavior.
53+
- When adding a parameter to a function called from multiple sites (or a
54+
shared private helper), thread it through every call site, not just the
55+
one you're looking at.

‎docs/user_guide/variable_handling/check_all_variables.rst‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ Now, we create the dataset:
3838
X["cat_var1"] = ["Hello"] * 1000
3939
X["cat_var2"] = ["Bye"] * 1000
4040
41-
X["date1"] = pd.date_range("2020-02-24", periods=1000, freq="T")
42-
X["date2"] = pd.date_range("2021-09-29", periods=1000, freq="H")
41+
X["date1"] = pd.date_range("2020-02-24", periods=1000, freq="min")
42+
X["date2"] = pd.date_range("2021-09-29", periods=1000, freq="h")
4343
X["date3"] = ["2020-02-24"] * 1000
4444
4545
print(X.head())
@@ -89,4 +89,48 @@ Below we see the error message:
8989

9090
.. code:: python
9191
92-
KeyError: 'Some of the variables are not in the dataframe.'
92+
KeyError: 'Some of the variables are not in the dataframe.'
93+
94+
With polars
95+
-----------
96+
97+
:class:`check_all_variables()` works in the same way with a polars dataframe.
98+
Let's create an equivalent toy dataset:
99+
100+
.. code:: python
101+
102+
import polars as pl
103+
from datetime import datetime, timedelta
104+
from sklearn.datasets import make_classification
105+
from feature_engine.variable_handling import check_all_variables
106+
107+
X, y = make_classification(
108+
n_samples=1000,
109+
n_features=4,
110+
n_redundant=1,
111+
n_clusters_per_class=1,
112+
weights=[0.50],
113+
class_sep=2,
114+
random_state=1,
115+
)
116+
117+
colnames = [f"num_var_{i+1}" for i in range(4)]
118+
X = pl.DataFrame(X, schema=colnames)
119+
120+
X = X.with_columns(
121+
pl.lit("Hello").alias("cat_var1"),
122+
pl.lit("Bye").alias("cat_var2"),
123+
pl.Series("date1", [datetime(2020, 2, 24) + timedelta(minutes=i) for i in range(1000)]),
124+
pl.Series("date2", [datetime(2021, 9, 29) + timedelta(hours=i) for i in range(1000)]),
125+
pl.lit("2020-02-24").alias("date3"),
126+
)
127+
128+
checked_vars = check_all_variables(X, ["num_var_1", "cat_var1", "date1"])
129+
130+
checked_vars
131+
132+
The output is the list of variable names passed to the function:
133+
134+
.. code:: python
135+
136+
['num_var_1', 'cat_var1', 'date1']

‎docs/user_guide/variable_handling/check_categorical_variables.rst‎

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ Now, we create the dataset:
3838
X["cat_var1"] = ["Hello"] * 1000
3939
X["cat_var2"] = ["Bye"] * 1000
4040
41-
X["date1"] = pd.date_range("2020-02-24", periods=1000, freq="T")
42-
X["date2"] = pd.date_range("2021-09-29", periods=1000, freq="H")
41+
X["date1"] = pd.date_range("2020-02-24", periods=1000, freq="min")
42+
X["date2"] = pd.date_range("2021-09-29", periods=1000, freq="h")
4343
X["date3"] = ["2020-02-24"] * 1000
4444
4545
print(X.head())
@@ -89,4 +89,49 @@ Below we see the error message:
8989
.. code:: python
9090
9191
TypeError: Some of the variables are not categorical. Please cast them as object
92-
or categorical before using this transformer.
92+
or categorical before using this transformer.
93+
94+
With polars
95+
-----------
96+
97+
:class:`check_categorical_variables()` works in the same way with a polars dataframe.
98+
Let's create an equivalent toy dataset:
99+
100+
.. code:: python
101+
102+
import polars as pl
103+
from datetime import datetime, timedelta
104+
from sklearn.datasets import make_classification
105+
from feature_engine.variable_handling import check_categorical_variables
106+
107+
X, y = make_classification(
108+
n_samples=1000,
109+
n_features=4,
110+
n_redundant=1,
111+
n_clusters_per_class=1,
112+
weights=[0.50],
113+
class_sep=2,
114+
random_state=1,
115+
)
116+
117+
colnames = [f"num_var_{i+1}" for i in range(4)]
118+
X = pl.DataFrame(X, schema=colnames)
119+
120+
X = X.with_columns(
121+
pl.lit("Hello").alias("cat_var1"),
122+
pl.lit("Bye").alias("cat_var2"),
123+
pl.Series("date1", [datetime(2020, 2, 24) + timedelta(minutes=i) for i in range(1000)]),
124+
pl.Series("date2", [datetime(2021, 9, 29) + timedelta(hours=i) for i in range(1000)]),
125+
pl.lit("2020-02-24").alias("date3"),
126+
)
127+
128+
var_cat = check_categorical_variables(X, ["cat_var1", "date3"])
129+
130+
var_cat
131+
132+
Both variables are of type string and hence, will be in the resulting list:
133+
134+
.. code:: python
135+
136+
['cat_var1', 'date3']
137+

‎docs/user_guide/variable_handling/check_datetime_variables.rst‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ Now, we create the dataset:
3838
X["cat_var1"] = ["Hello"] * 1000
3939
X["cat_var2"] = ["Bye"] * 1000
4040
41-
X["date1"] = pd.date_range("2020-02-24", periods=1000, freq="T")
42-
X["date2"] = pd.date_range("2021-09-29", periods=1000, freq="H")
41+
X["date1"] = pd.date_range("2020-02-24", periods=1000, freq="min")
42+
X["date2"] = pd.date_range("2021-09-29", periods=1000, freq="h")
4343
X["date3"] = ["2020-02-24"] * 1000
4444
4545
print(X.head())
@@ -92,3 +92,49 @@ Below the error message:
9292
.. code:: python
9393
9494
TypeError: Some of the variables are not or cannot be parsed as datetime.
95+
96+
With polars
97+
-----------
98+
99+
:class:`check_datetime_variables()` works in the same way with a polars dataframe.
100+
Let's create an equivalent toy dataset:
101+
102+
.. code:: python
103+
104+
import polars as pl
105+
from datetime import datetime, timedelta
106+
from sklearn.datasets import make_classification
107+
from feature_engine.variable_handling import check_datetime_variables
108+
109+
X, y = make_classification(
110+
n_samples=1000,
111+
n_features=4,
112+
n_redundant=1,
113+
n_clusters_per_class=1,
114+
weights=[0.50],
115+
class_sep=2,
116+
random_state=1,
117+
)
118+
119+
colnames = [f"num_var_{i+1}" for i in range(4)]
120+
X = pl.DataFrame(X, schema=colnames)
121+
122+
X = X.with_columns(
123+
pl.lit("Hello").alias("cat_var1"),
124+
pl.lit("Bye").alias("cat_var2"),
125+
pl.Series("date1", [datetime(2020, 2, 24) + timedelta(minutes=i) for i in range(1000)]),
126+
pl.Series("date2", [datetime(2021, 9, 29) + timedelta(hours=i) for i in range(1000)]),
127+
pl.lit("2020-02-24").alias("date3"),
128+
)
129+
130+
var_date = check_datetime_variables(X, ["date2", "date3"])
131+
132+
var_date
133+
134+
In this case, both variables, if they can be parsed as datetime, will be in the
135+
resulting list:
136+
137+
.. code:: python
138+
139+
['date2', 'date3']
140+

‎docs/user_guide/variable_handling/check_numerical_variables.rst‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Now, we create the dataset:
2525
"City": ["London", "Manchester", "Liverpool", "Bristol"],
2626
"Age": [20, 21, 19, 18],
2727
"Marks": [0.9, 0.8, 0.7, 0.6],
28-
"dob": pd.date_range("2020-02-24", periods=4, freq="T"),
28+
"dob": pd.date_range("2020-02-24", periods=4, freq="min"),
2929
})
3030
3131
print(df.head())
@@ -67,3 +67,32 @@ Below we see the error message:
6767
6868
TypeError: Some of the variables are not numerical. Please cast them as numerical
6969
before using this transformer.
70+
71+
With polars
72+
-----------
73+
74+
:class:`check_numerical_variables()` works in the same way with a polars dataframe:
75+
76+
.. code:: python
77+
78+
import polars as pl
79+
from datetime import datetime
80+
from feature_engine.variable_handling import check_numerical_variables
81+
82+
df = pl.DataFrame({
83+
"Name": ["tom", "nick", "krish", "jack"],
84+
"City": ["London", "Manchester", "Liverpool", "Bristol"],
85+
"Age": [20, 21, 19, 18],
86+
"Marks": [0.9, 0.8, 0.7, 0.6],
87+
"dob": [datetime(2020, 2, 24, 0, i) for i in range(4)],
88+
})
89+
90+
var_num = check_numerical_variables(df, ['Age', 'Marks'])
91+
92+
var_num
93+
94+
If the variables are numerical, the function returns their names in a list:
95+
96+
.. code:: python
97+
98+
['Age', 'Marks']

‎docs/user_guide/variable_handling/find_all_variables.rst‎

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,68 @@ However, this command returns an empty list:
125125
X[[ 'date1', 'date2', 'date3']],
126126
exclude_datetime=True,
127127
return_empty=True,
128-
)
128+
)
129+
130+
With polars
131+
-----------
132+
133+
:class:`find_all_variables()` works in the same way with a polars dataframe.
134+
Let's create an equivalent toy dataset:
135+
136+
.. code:: python
137+
138+
import polars as pl
139+
from datetime import datetime, timedelta
140+
from sklearn.datasets import make_classification
141+
from feature_engine.variable_handling import find_all_variables
142+
143+
X, y = make_classification(
144+
n_samples=1000,
145+
n_features=4,
146+
n_redundant=1,
147+
n_clusters_per_class=1,
148+
weights=[0.50],
149+
class_sep=2,
150+
random_state=1,
151+
)
152+
153+
colnames = [f"num_var_{i+1}" for i in range(4)]
154+
X = pl.DataFrame(X, schema=colnames)
155+
156+
X = X.with_columns(
157+
pl.lit("Hello").alias("cat_var1"),
158+
pl.lit("Bye").alias("cat_var2"),
159+
pl.Series("date1", [datetime(2020, 2, 24) + timedelta(minutes=i) for i in range(1000)]),
160+
pl.Series("date2", [datetime(2021, 9, 29) + timedelta(hours=i) for i in range(1000)]),
161+
pl.lit("2020-02-24").alias("date3"),
162+
)
163+
164+
vars_all = find_all_variables(X)
165+
166+
vars_all
167+
168+
We see the variable names in the list below:
169+
170+
.. code:: python
171+
172+
['num_var_1',
173+
'num_var_2',
174+
'num_var_3',
175+
'num_var_4',
176+
'cat_var1',
177+
'cat_var2',
178+
'date1',
179+
'date2',
180+
'date3']
181+
182+
And, as with pandas, we can exclude the datetime variables:
183+
184+
.. code:: python
185+
186+
vars_all = find_all_variables(X, exclude_datetime=True)
187+
188+
vars_all
189+
190+
.. code:: python
191+
192+
['num_var_1', 'num_var_2', 'num_var_3', 'num_var_4', 'cat_var1', 'cat_var2']

0 commit comments

Comments
 (0)