Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions feature_engine/_base_transformers/base_numerical.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" The base transformer provides functionality that is shared by most transformer
"""The base transformer provides functionality that is shared by most transformer
classes. Provides the base functionality within the fit() and transform() methods
shared by most transformers, like checking that input is a df, the size, NA, etc.
"""
Expand Down Expand Up @@ -60,7 +60,9 @@ def fit(self, X: pd.DataFrame) -> pd.DataFrame:

# find or check for numerical variables
if self.variables is None:
self.variables_ = find_numerical_variables(X)
self.variables_ = find_numerical_variables(
X, return_empty=self.return_empty
)
else:
self.variables_ = check_numerical_variables(X, self.variables)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ def _check_param_drop_original(drop_original):
"drop_original takes only boolean values True and False. "
f"Got {drop_original} instead."
)


def _check_return_empty_is_bool(return_empty):
if not isinstance(return_empty, bool):
raise ValueError(
"return_empty takes only boolean values True and False. "
f"Got {return_empty} instead."
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@
contain missing values. If `'ignore'`, missing data will be ignored when
learning parameters or performing the transformation.
""".rstrip()

_return_empty_docstring = """return_empty : bool, default=False
Whether to return an empty list when no variables of the required type are
found. If False, the transformer raises an error. This parameter is only
used when `variables` is `None`.

.. versionadded:: 2.0
`return_empty` currently defaults to False. The default will change to
True in version 2.1. To keep the current behaviour and silence the
warning, explicitly set `return_empty=False` instead of relying on the
default.
""".rstrip()
20 changes: 13 additions & 7 deletions feature_engine/_docstrings/substitute.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Utilities for docstring in Feature-engine.

Taken from the project imbalanced-learn:
Adapted from the project imbalanced-learn:

https://github.com/scikit-learn-contrib/imbalanced-learn/blob/
imblearn/utils/_docstring.py#L7
Expand All @@ -10,16 +10,22 @@
class Substitution:
"""Decorate a function's or a class' docstring to perform string
substitution on it.

Only the placeholders whose names are passed as keyword arguments are
replaced, so literal braces in the docstring (e.g., in code examples)
are left untouched.

This decorator should be robust even if obj.__doc__ is None
(for example, if -OO was passed to the interpreter).
"""

def __init__(self, *args, **kwargs):
if args and kwargs:
raise AssertionError("Only positional or keyword args are allowed")

self.params = args or kwargs
def __init__(self, **kwargs):
self.params = kwargs

def __call__(self, obj):
obj.__doc__ = obj.__doc__.format(**self.params)
doc = obj.__doc__
if doc:
for key, value in self.params.items():
doc = doc.replace("{" + key + "}", value)
obj.__doc__ = doc
return obj
1 change: 1 addition & 0 deletions feature_engine/creation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
The module creation includes classes to create new variables by combination of existing
variables in the dataframe.
"""

from .cyclical_features import CyclicalFeatures
from .decision_tree_features import DecisionTreeFeatures
from .geo_features import GeoDistanceFeatures
Expand Down
10 changes: 9 additions & 1 deletion feature_engine/creation/cyclical_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from feature_engine._check_init_parameters.check_init_input_params import (
_check_param_drop_original,
_check_return_empty_is_bool,
)
from feature_engine._check_init_parameters.check_input_dictionary import (
_check_numerical_dict,
Expand All @@ -22,8 +23,9 @@
_n_features_in_docstring,
_variables_attribute_docstring,
)
from feature_engine._docstrings.init_parameters.all_trasnformers import (
from feature_engine._docstrings.init_parameters.all_transformers import (
_drop_original_docstring,
_return_empty_docstring,
_variables_numerical_docstring,
)
from feature_engine._docstrings.methods import (
Expand All @@ -36,6 +38,7 @@
@Substitution(
variables=_variables_numerical_docstring,
drop_original=_drop_original_docstring,
return_empty=_return_empty_docstring,
variables_=_variables_attribute_docstring,
feature_names_in_=_feature_names_in_docstring,
n_features_in_=_n_features_in_docstring,
Expand Down Expand Up @@ -66,6 +69,8 @@ class CyclicalFeatures(
----------
{variables}

{return_empty}

max_values: dict, default=None
A dictionary with the maximum value of each variable to transform. Useful when
the maximum value is not present in the dataset. If None, the transformer will
Expand Down Expand Up @@ -122,14 +127,17 @@ class CyclicalFeatures(
def __init__(
self,
variables: Union[None, int, str, List[Union[str, int]]] = None,
return_empty: bool = False,
max_values: Optional[Dict[str, Union[int, float]]] = None,
drop_original: Optional[bool] = False,
) -> None:

_check_numerical_dict(max_values)
_check_param_drop_original(drop_original)
_check_return_empty_is_bool(return_empty)

self.variables = _check_variables_input_value(variables)
self.return_empty = return_empty
self.max_values = max_values
self.drop_original = drop_original

Expand Down
12 changes: 10 additions & 2 deletions feature_engine/creation/decision_tree_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from feature_engine._check_init_parameters.check_init_input_params import (
_check_param_drop_original,
_check_param_missing_values,
_check_return_empty_is_bool,
)
from feature_engine._check_init_parameters.check_variables import (
_check_variables_input_value,
Expand All @@ -22,9 +23,10 @@
_n_features_in_docstring,
_variables_attribute_docstring,
)
from feature_engine._docstrings.init_parameters.all_trasnformers import (
from feature_engine._docstrings.init_parameters.all_transformers import (
_drop_original_docstring,
_missing_values_docstring,
_return_empty_docstring,
_variables_numerical_docstring,
)
from feature_engine._docstrings.init_parameters.creation import _features_to_combine
Expand Down Expand Up @@ -52,6 +54,7 @@
features_to_combine=_features_to_combine,
missing_values=_missing_values_docstring,
drop_original=_drop_original_docstring,
return_empty=_return_empty_docstring,
variables_=_variables_attribute_docstring,
feature_names_in_=_feature_names_in_docstring,
n_features_in_=_n_features_in_docstring,
Expand Down Expand Up @@ -80,6 +83,8 @@ class DecisionTreeFeatures(TransformerMixin, BaseEstimator, GetFeatureNamesOutMi
----------
{variables}

{return_empty}

{features_to_combine}

precision: int, default=None
Expand Down Expand Up @@ -210,6 +215,7 @@ class DecisionTreeFeatures(TransformerMixin, BaseEstimator, GetFeatureNamesOutMi
def __init__(
self,
variables: Union[None, int, str, List[Union[str, int]]] = None,
return_empty: bool = False,
features_to_combine: Optional[Union[Iterable[Any], int]] = None,
precision: Union[int, None] = None,
cv=3,
Expand All @@ -232,10 +238,12 @@ def __init__(
f"regression must be a boolean value. Got {regression} instead."
)

_check_return_empty_is_bool(return_empty)
_check_param_missing_values(missing_values)
_check_param_drop_original(drop_original)

self.variables = _check_variables_input_value(variables)
self.return_empty = return_empty
self.features_to_combine = features_to_combine
self.precision = precision
self.cv = cv
Expand Down Expand Up @@ -276,7 +284,7 @@ def fit(self, X: pd.DataFrame, y: pd.Series):

# find or check for numerical variables
if self.variables is None:
variables_ = find_numerical_variables(X)
variables_ = find_numerical_variables(X, return_empty=self.return_empty)
else:
variables_ = check_numerical_variables(X, self.variables)

Expand Down
34 changes: 31 additions & 3 deletions feature_engine/creation/math_features.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from typing import Any, List, Optional, Union

import numpy as np
import pandas as pd

from feature_engine._docstrings.fit_attributes import (
_feature_names_in_docstring,
_n_features_in_docstring,
_variables_attribute_docstring,
)
from feature_engine._docstrings.init_parameters.all_trasnformers import (
from feature_engine._docstrings.init_parameters.all_transformers import (
_drop_original_docstring,
_missing_values_docstring,
)
Expand All @@ -19,6 +20,26 @@
from feature_engine._docstrings.substitute import Substitution
from feature_engine.creation.base_creation import BaseCreation

_PANDAS_LT_3 = int(pd.__version__.split(".")[0]) < 3

# In pandas < 3, agg() maps these callables to the pandas methods and warns that
# this will change; the string alias keeps that behavior (e.g., np.std ->
# Series.std with ddof=1) without the warning. In pandas >= 3 the callables are
# used directly (np.std applies ddof=0), so they must not be aliased.
_FUNC_TO_STRING_ALIAS = {
sum: "sum",
min: "min",
max: "max",
np.sum: "sum",
np.mean: "mean",
np.std: "std",
np.var: "var",
np.median: "median",
np.min: "min",
np.max: "max",
np.prod: "prod",
}


@Substitution(
missing_values=_missing_values_docstring,
Expand Down Expand Up @@ -206,10 +227,17 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame:

new_variable_names = self._get_new_features_name()

func = self.func
if _PANDAS_LT_3:
if isinstance(func, list):
func = [_FUNC_TO_STRING_ALIAS.get(fun, fun) for fun in func]
else:
func = _FUNC_TO_STRING_ALIAS.get(func, func)

if len(new_variable_names) == 1:
X[new_variable_names[0]] = X[self.variables].agg(self.func, axis=1)
X[new_variable_names[0]] = X[self.variables].agg(func, axis=1)
else:
X[new_variable_names] = X[self.variables].agg(self.func, axis=1)
X[new_variable_names] = X[self.variables].agg(func, axis=1)

if self.drop_original:
X.drop(columns=self.variables, inplace=True)
Expand Down
2 changes: 1 addition & 1 deletion feature_engine/creation/relative_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
_n_features_in_docstring,
_variables_attribute_docstring,
)
from feature_engine._docstrings.init_parameters.all_trasnformers import (
from feature_engine._docstrings.init_parameters.all_transformers import (
_drop_original_docstring,
_missing_values_docstring,
_variables_numerical_docstring,
Expand Down
20 changes: 19 additions & 1 deletion feature_engine/datetime/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@
from sklearn.utils.validation import check_is_fitted

from feature_engine._base_transformers.mixins import GetFeatureNamesOutMixin
from feature_engine._check_init_parameters.check_init_input_params import (
_check_return_empty_is_bool,
)
from feature_engine._check_init_parameters.check_variables import (
_check_variables_input_value,
)
from feature_engine._docstrings.fit_attributes import (
_feature_names_in_docstring,
_n_features_in_docstring,
)
from feature_engine._docstrings.init_parameters.all_transformers import (
_return_empty_docstring,
)
from feature_engine._docstrings.methods import (
_fit_not_learn_docstring,
_fit_transform_docstring,
Expand All @@ -40,6 +46,7 @@


@Substitution(
return_empty=_return_empty_docstring,
feature_names_in_=_feature_names_in_docstring,
n_features_in_=_n_features_in_docstring,
fit=_fit_not_learn_docstring,
Expand Down Expand Up @@ -88,6 +95,8 @@ class DatetimeFeatures(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin)
If "index", the transformer will extract datetime features from the
index of the dataframe.

{return_empty}

features_to_extract: list, default=None
The list of date features to extract. If None, the following features will be
extracted: "month", "year", "day_of_week", "day_of_month", "hour",
Expand Down Expand Up @@ -178,6 +187,7 @@ class DatetimeFeatures(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin)
def __init__(
self,
variables: Union[None, int, str, List[Union[str, int]]] = None,
return_empty: bool = False,
features_to_extract: Union[None, str, List[str]] = None,
drop_original: bool = True,
missing_values: str = "raise",
Expand Down Expand Up @@ -218,7 +228,10 @@ def __init__(
if utc is not None and not isinstance(utc, bool):
raise ValueError("utc takes only booleans or None. " f"Got {utc} instead.")

_check_return_empty_is_bool(return_empty)

self.variables = _check_variables_input_value(variables)
self.return_empty = return_empty
self.drop_original = drop_original
self.missing_values = missing_values
self.dayfirst = dayfirst
Expand Down Expand Up @@ -260,7 +273,9 @@ def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
self.variables_ = []

elif self.variables is None:
self.variables_ = find_datetime_variables(X)
self.variables_ = find_datetime_variables(
X, return_empty=self.return_empty
)

else:
self.variables_ = check_datetime_variables(X, self.variables)
Expand Down Expand Up @@ -341,6 +356,9 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame:
if self.missing_values == "raise":
_check_contains_na(X, self.variables_)

if len(self.variables_) == 0:
return X

# convert datetime variables
datetime_df = pd.concat(
[
Expand Down
Loading