Skip to content

Commit 712eb9b

Browse files
committed
refactor find variables:
1 parent 8a0978c commit 712eb9b

2 files changed

Lines changed: 75 additions & 27 deletions

File tree

feature_engine/variable_handling/find_variables.py

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,27 @@
1212
)
1313

1414

15-
def _find_nw_categoricals(nw_df) -> List[Union[str, int]]:
15+
def _find_nw_categoricals(
16+
nw_df, exclude_datetime: bool = True
17+
) -> List[Union[str, int]]:
1618
_NW_SELECTOR = (
17-
nw.selectors.categorical()
18-
| nw.selectors.enum()
19-
| nw.selectors.string()
20-
| nw.selectors.by_dtype(nw.Object)
19+
nw.selectors.categorical()
20+
| nw.selectors.enum()
21+
| nw.selectors.string()
22+
| nw.selectors.by_dtype(nw.Object)
2123
)
24+
# `|`-combined selectors don't preserve the dataframe's column order (each
25+
# sub-selector's matches are concatenated in selector-declaration order,
26+
# not column position), so re-filter over nw_df.columns to restore it.
2227
matched = set(nw_df.select(_NW_SELECTOR).columns)
23-
vars = [column for column in nw_df.columns if column in matched]
24-
variables = [
25-
column
26-
for column in vars
27-
if _is_categorical_and_is_not_datetime(nw_df.get_column(column))
28-
]
28+
variables = [column for column in nw_df.columns if column in matched]
29+
30+
if exclude_datetime is True:
31+
variables = [
32+
column
33+
for column in variables
34+
if _is_categorical_and_is_not_datetime(nw_df.get_column(column))
35+
]
2936
return variables
3037

3138

@@ -73,7 +80,7 @@ def find_numerical_variables(
7380
['var_num']
7481
"""
7582
nw_X = nw.from_native(X, eager_only=True)
76-
variables = list(nw_X.select(nw.selectors.numeric()).columns)
83+
variables = nw_X.select(nw.selectors.numeric()).columns
7784

7885
if len(variables) == 0:
7986
if return_empty is False:
@@ -93,6 +100,7 @@ def find_numerical_variables(
93100
def find_categorical_variables(
94101
X: IntoDataFrame,
95102
return_empty: bool = False,
103+
exclude_datetime: bool = True,
96104
) -> List[Union[str, int]]:
97105
"""
98106
Returns a list with the names of all the categorical variables in a dataframe.
@@ -117,6 +125,9 @@ def find_categorical_variables(
117125
warning, explicitly set `return_empty=False` instead of relying on the
118126
default.
119127
128+
exclude_datetime: bool, default=True
129+
Whether to exclude variables that can be parsed as datetime.
130+
120131
Returns
121132
-------
122133
variables: List
@@ -136,7 +147,7 @@ def find_categorical_variables(
136147
['var_cat']
137148
"""
138149
nw_X = nw.from_native(X, eager_only=True)
139-
variables = _find_nw_categoricals(nw_X)
150+
variables = _find_nw_categoricals(nw_X, exclude_datetime=exclude_datetime)
140151

141152
if len(variables) == 0:
142153
if return_empty is False:
@@ -209,8 +220,7 @@ def find_datetime_variables(
209220
['var_date']
210221
"""
211222
nw_X = nw.from_native(X, eager_only=True)
212-
numeric_cols = set(nw_X.select(nw.selectors.numeric()).columns)
213-
non_numeric = [column for column in nw_X.columns if column not in numeric_cols]
223+
non_numeric = nw_X.select(~nw.selectors.numeric()).columns
214224

215225
datetime_cols = set(
216226
nw_X.select(nw.selectors.by_dtype(nw.Date, nw.Datetime)).columns
@@ -287,19 +297,16 @@ def find_all_variables(
287297
"""
288298
nw_X = nw.from_native(X, eager_only=True)
289299
if exclude_datetime is True:
290-
datetime_cols = set(
291-
nw_X.select(nw.selectors.by_dtype(nw.Date, nw.Datetime)).columns
292-
)
300+
variables = nw_X.select(~nw.selectors.by_dtype(nw.Date, nw.Datetime)).columns
293301
numeric_cols = set(nw_X.select(nw.selectors.numeric()).columns)
294-
variables = [var for var in nw_X.columns if var not in datetime_cols]
295302
variables = [
296303
var
297304
for var in variables
298305
if var in numeric_cols
299306
or not _is_categorical_and_is_datetime(nw_X.get_column(var))
300307
]
301308
else:
302-
variables = list(nw_X.columns)
309+
variables = nw_X.columns
303310

304311
if len(variables) == 0:
305312
if return_empty is False:
@@ -319,6 +326,7 @@ def find_categorical_and_numerical_variables(
319326
X: IntoDataFrame,
320327
variables: Union[None, int, str, List[Union[str, int]]] = None,
321328
return_empty: bool = False,
329+
exclude_datetime: bool = True,
322330
) -> Tuple[List[Union[str, int]], List[Union[str, int]]]:
323331
"""
324332
Find numerical and categorical variables in a dataframe or from a list.
@@ -349,6 +357,9 @@ def find_categorical_and_numerical_variables(
349357
warning, explicitly set `return_empty=False` instead of relying on the
350358
default.
351359
360+
exclude_datetime: bool, default=True
361+
Whether to exclude variables that can be parsed as datetime.
362+
352363
Returns
353364
-------
354365
variables: tuple
@@ -375,9 +386,11 @@ def find_categorical_and_numerical_variables(
375386
# If the user passes just 1 variable outside a list.
376387
if isinstance(variables, (str, int)):
377388
s = nw_X.get_column(variables)
378-
is_cat = isinstance(
379-
s.dtype, (nw.Categorical, nw.Enum)
380-
) or _is_categorical_and_is_not_datetime(s)
389+
is_cat = bool(
390+
_find_nw_categoricals(
391+
nw_X.select([variables]), exclude_datetime=exclude_datetime
392+
)
393+
)
381394
is_num = s.dtype.is_numeric()
382395

383396
if is_cat:
@@ -404,8 +417,8 @@ def find_categorical_and_numerical_variables(
404417

405418
# If user leaves default None parameter.
406419
elif variables is None:
407-
variables_cat = _find_nw_categoricals(nw_X)
408-
variables_num = list(nw_X.select(nw.selectors.numeric()).columns)
420+
variables_cat = _find_nw_categoricals(nw_X, exclude_datetime=exclude_datetime)
421+
variables_num = nw_X.select(nw.selectors.numeric()).columns
409422

410423
if len(variables_num) == 0 and len(variables_cat) == 0:
411424
if return_empty is False:
@@ -442,7 +455,9 @@ def find_categorical_and_numerical_variables(
442455

443456
else:
444457
sub_X = nw_X.select(variables)
445-
variables_cat = _find_nw_categoricals(sub_X)
446-
variables_num = list(sub_X.select(nw.selectors.numeric()).columns)
458+
variables_cat = _find_nw_categoricals(
459+
sub_X, exclude_datetime=exclude_datetime
460+
)
461+
variables_num = sub_X.select(nw.selectors.numeric()).columns
447462

448463
return variables_cat, variables_num

tests/test_variable_handling/test_find_variables.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,36 @@ def test_numcat_vars_as_category(make_df):
334334
["Age", "Marks"],
335335
)
336336
assert find_categorical_and_numerical_variables(df, "City") == (["City"], [])
337+
338+
339+
@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame])
340+
def test_numcat_agrees_with_find_categorical_on_date_like_category(make_df):
341+
# Regression test: the single-variable branch of
342+
# find_categorical_and_numerical_variables used to short-circuit past the
343+
# datetime check for Categorical-dtype columns, so it disagreed with
344+
# find_categorical_variables on a category column holding date-like
345+
# strings. All three calls below must now exclude it consistently.
346+
df = make_df({"date_cat": DATETIME_DATA["date_obj0"], "num": BASIC_DATA["Age"]})
347+
df = cast_categorical(df, ["date_cat"])
348+
349+
assert find_categorical_variables(df, return_empty=True) == []
350+
assert find_categorical_and_numerical_variables(df, None) == ([], ["num"])
351+
assert find_categorical_and_numerical_variables(
352+
df, "date_cat", return_empty=True
353+
) == ([], [])
354+
355+
356+
@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame])
357+
def test_numcat_exclude_datetime_false_keeps_date_like_category(make_df):
358+
# exclude_datetime=False must be honoured consistently across all three
359+
# entry points, including the single-variable branch.
360+
df = make_df({"date_cat": DATETIME_DATA["date_obj0"], "num": BASIC_DATA["Age"]})
361+
df = cast_categorical(df, ["date_cat"])
362+
363+
assert find_categorical_variables(df, exclude_datetime=False) == ["date_cat"]
364+
assert find_categorical_and_numerical_variables(
365+
df, None, exclude_datetime=False
366+
) == (["date_cat"], ["num"])
367+
assert find_categorical_and_numerical_variables(
368+
df, "date_cat", exclude_datetime=False
369+
) == (["date_cat"], [])

0 commit comments

Comments
 (0)