From 2dcb068f6cbfaf5350e476caefe022d44f1d7250 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Fri, 24 Jul 2026 16:06:13 +0530 Subject: [PATCH 01/14] Merge LogCpTransformer into LogTransformer with C parameter --- feature_engine/transformation/log.py | 303 ++++++------------ .../test_log_transformer.py | 15 + .../test_logcp_transformer.py | 2 +- 3 files changed, 110 insertions(+), 210 deletions(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 1e34f3308..17b1b0e36 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -42,13 +42,18 @@ fit_transform=_fit_transform_docstring, inverse_transform=_inverse_transform_docstring, ) -class LogTransformer(BaseNumericalTransformer): +class LogTransformer(BaseNumericalTransformer, FitFromDictMixin): """ The LogTransformer() applies the natural logarithm or the base 10 logarithm to - numerical variables. The natural logarithm is the logarithm in base e. + numerical variables, optionally after adding a constant C, i.e., log(x + C). - The LogTransformer() only works with positive values. If the variable - contains a zero or a negative value the transformer will return an error. + By default, C=0, so LogTransformer() only works with positive values and + behaves exactly as it always has: if a variable contains a zero or a negative + value, the transformer raises an error. + + To transform variables that contain zero or negative values, pass a non-zero + C: either an explicit constant, "auto" to let the transformer determine a + shift per variable, or a dictionary mapping each variable to its own constant. A list of variables can be passed as an argument. Alternatively, the transformer will automatically select and transform all variables of type numeric. @@ -65,10 +70,26 @@ class LogTransformer(BaseNumericalTransformer): Indicates if the natural or base 10 logarithm should be applied. Can take values 'e' or '10'. + C: "auto", int, float or dict, default=0 + The constant C to add to the variable before the logarithm, i.e., log(x + C). + + - If 0 (the default), no constant is added and the variable must be + strictly positive, matching the transformer's original behavior. + - If int or float, then log(x + C). + - If "auto", then C = abs(min(x)) + 1. + - If dict, dictionary mapping the constant C to apply to each variable. + + Note, when C is a dictionary, the parameter `variables` is ignored. + Attributes ---------- {variables_} + C_: + The constant C added to each variable. Equal to C, unless C = "auto", in + which case it is a dictionary with C = abs(min(variable)) + 1. For strictly + positive variables, C = 0. + {feature_names_in_} {n_features_in_} @@ -109,24 +130,33 @@ def __init__( variables: Union[None, int, str, List[Union[str, int]]] = None, return_empty: bool = False, base: str = "e", + C: Union[int, float, str, Dict[Union[str, int], Union[float, int]]] = 0, ) -> None: if base not in ["e", "10"]: raise ValueError("base can take only '10' or 'e' as values") + if not isinstance(C, (int, float, dict)) and C != "auto": + raise ValueError( + f"C can take only 'auto', integers or floats. Got {C} instead." + ) + _check_return_empty_is_bool(return_empty) self.variables = _check_variables_input_value(variables) self.return_empty = return_empty self.base = base + self.C = C def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): """ - This transformer does not learn parameters. + Learn the constant C to add to the variable before the logarithm + transformation, if C="auto". Otherwise, this transformer does not learn + parameters. - Selects the numerical variables and determines whether the logarithm - can be applied on the selected variables, i.e., it checks that the variables - are positive. + Selects the numerical variables and, when C=0 (the default), determines + whether the logarithm can be applied on the selected variables, i.e., it + checks that the variables are positive. Parameters ---------- @@ -139,10 +169,28 @@ def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): """ # check input dataframe - X = super().fit(X) + if isinstance(self.C, dict): + X = super()._fit_from_dict(X, self.C) + else: + X = super().fit(X) + + self.C_ = self.C + + # calculate C to add to each variable + if self.C == "auto": + # we add 0 to positive variables + c_dict = {var: 0 for var in self.variables_ if X[var].min() > 0} - # check contains zero or negative values - if (X[self.variables_] <= 0).any().any(): + # we add the minimum plus 1 to non-positive variables + non_positive_vars = [ + var for var in self.variables_ if var not in c_dict.keys() + ] + c_dict.update(dict(X[non_positive_vars].min(axis=0).abs() + 1)) + self.C_ = c_dict # type:ignore + + # C=0 is the original LogTransformer contract: no constant is added, + # so fail fast at fit time exactly as before this class supported C. + if self.C_ == 0 and (X[self.variables_] <= 0).any().any(): raise ValueError( "Some variables contain zero or negative values, can't apply log" ) @@ -151,7 +199,7 @@ def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): def transform(self, X: pd.DataFrame) -> pd.DataFrame: """ - Transform the variables with the logarithm. + Transform the variables with the logarithm of x plus the constant C. Parameters ---------- @@ -167,19 +215,26 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: # check input dataframe and if class was fitted X = self._check_transform_input_and_state(X) - # check contains zero or negative values - if (X[self.variables_] <= 0).any().any(): - raise ValueError( + if self.C_ == 0: + error_msg = ( "Some variables contain zero or negative values, can't apply log" ) + else: + error_msg = ( + "Some variables contain zero or negative values after adding" + + " constant C, can't apply log." + ) + + if (X[self.variables_] + self.C_ <= 0).any().any(): + raise ValueError(error_msg) X[self.variables_] = X[self.variables_].astype(float) # transform if self.base == "e": - X.loc[:, self.variables_] = np.log(X.loc[:, self.variables_]) + X.loc[:, self.variables_] = np.log(X.loc[:, self.variables_] + self.C_) elif self.base == "10": - X.loc[:, self.variables_] = np.log10(X.loc[:, self.variables_]) + X.loc[:, self.variables_] = np.log10(X.loc[:, self.variables_] + self.C_) return X @@ -203,17 +258,17 @@ def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame: # inverse_transform if self.base == "e": - X.loc[:, self.variables_] = np.exp(X.loc[:, self.variables_]) + X.loc[:, self.variables_] = np.exp(X.loc[:, self.variables_]) - self.C_ elif self.base == "10": - X.loc[:, self.variables_] = np.array(10 ** X.loc[:, self.variables_]) + X.loc[:, self.variables_] = 10 ** X.loc[:, self.variables_] - self.C_ return X def _more_tags(self): tags_dict = _return_tags() # ======= this tests fail because the transformers throw an error - # when the values are 0. Nothing to do with the test itself but - # mostly with the data created and used in the test + # when the values are 0 and C=0 (the default). Nothing to do with the + # test itself but mostly with the data created and used in the test msg = ( "transformers raise errors when data contains zeroes, thus this check fails" ) @@ -231,80 +286,18 @@ def __sklearn_tags__(self): return tags -@Substitution( - return_empty=_return_empty_docstring, - variables_=_variables_attribute_docstring, - feature_names_in_=_feature_names_in_docstring, - n_features_in_=_n_features_in_docstring, - fit_transform=_fit_transform_docstring, - inverse_transform=_inverse_transform_docstring, -) -class LogCpTransformer(BaseNumericalTransformer, FitFromDictMixin): +class LogCpTransformer(LogTransformer): """ LogCpTransformer() applies the transformation log(x + C), where x is the - variable to transform and C is a positive constant. It can apply the natural - logarithm or the base 10 logarithm, where the natural logarithm is logarithm in - base e. - - As the logarithm can only be applied to numerical non-negative values, - LogCpTransformer() extends the functionality of LogTransformer, by adding a - constant to shift the distribution of the variables towards positive values. + variable to transform and C is a positive constant. - Note that if the variable contains a zero or a negative value after adding a - constant C, the transformer will return an error. This can occur if the values of - the variables in the test set are smaller than those seen during `fit()`. + .. deprecated:: (next release) + `LogCpTransformer` is deprecated and will be removed in a future release. + Use :class:`LogTransformer` with ``C="auto"`` instead, i.e. + ``LogTransformer(C="auto")`` reproduces `LogCpTransformer`'s default + behavior exactly. - A list of variables can be passed as an argument. Alternatively, the transformer - will automatically select and transform all variables of type numeric. - - More details in the :ref:`User Guide `. - - Parameters - ---------- - variables: list, default=None - The list of numerical variables to transform. If None, the transformer - will find and select all numerical variables. If C is a dictionary, then this - parameter is ignored and the variables to transform are selected from the - dictionary keys. - - {return_empty} - - base: string, default='e' - Indicates if the natural or base 10 logarithm should be applied. Can take - values 'e' or '10'. - - C: "auto", int or dict, default="auto" - The constant C to add to the variable before the logarithm, i.e., log(x + C). - - - If int, then log(x + C) - - If "auto", then C = abs(min(x)) + 1 - - If dict, dictionary mapping the constant C to apply to each variable. - - Note, when C is a dictionary, the parameter `variables` is ignored. - - Attributes - ---------- - {variables_} - - C_: - The constant C to add to each variable. If C = "auto" a dictionary with - C = abs(min(variable)) + 1. For strictly positive variables, C = 0. - - {feature_names_in_} - - {n_features_in_} - - Methods - ------- - fit: - Learn the constant C. - - {fit_transform} - - {inverse_transform} - - transform: - Transform the variables with the logarithm of x plus C. + See :class:`LogTransformer` for the full parameter and attribute reference. Examples -------- @@ -335,124 +328,16 @@ def __init__( base: str = "e", C: Union[int, float, str, Dict[Union[str, int], Union[float, int]]] = "auto", ) -> None: - - if base not in ["e", "10"]: - raise ValueError( - f"base can take only '10' or 'e' as values. Got {base} instead." - ) - - if not isinstance(C, (int, float, dict)) and C != "auto": - raise ValueError( - f"C can take only 'auto', integers or floats. Got {C} instead." - ) - - _check_return_empty_is_bool(return_empty) - - self.variables = _check_variables_input_value(variables) - self.return_empty = return_empty - self.base = base - self.C = C - - def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): - """ - Learn the constant C to add to the variable before the logarithm transformation - if C="auto". - - Select the numerical variables or check that the variables entered by the user - are numerical. Then check that the selected variables are positive after - addition of C. - - Parameters - ---------- - X: pandas DataFrame of shape = [n_samples, n_features]. - The training input samples. Can be the entire dataframe, not just the - variables to transform. - - y: pandas Series, default=None - It is not needed in this transformer. You can pass y or None. - """ - - # check input dataframe - if isinstance(self.C, dict): - X = super()._fit_from_dict(X, self.C) - else: - X = super().fit(X) - - self.C_ = self.C - - # calculate C to add to each variable - if self.C == "auto": - # we add 0 to positive variables - c_dict = {var: 0 for var in self.variables_ if X[var].min() > 0} - - # we add the minimum plus 1 to non-positive variables - non_positive_vars = [ - var for var in self.variables_ if var not in c_dict.keys() - ] - c_dict.update(dict(X[non_positive_vars].min(axis=0).abs() + 1)) - self.C_ = c_dict # type:ignore - - return self - - def transform(self, X: pd.DataFrame) -> pd.DataFrame: - """ - Transform the variables with the logarithm of x plus a constant C. - - Parameters - ---------- - X: pandas DataFrame of shape = [n_samples, n_features] - The data to be transformed. - - Returns - ------- - X_new: pandas dataframe - The dataframe with the transformed variables. - """ - - # check input dataframe and if class was fitted - X = self._check_transform_input_and_state(X) - - # check variable is positive after adding c - error_msg = ( - "Some variables contain zero or negative values after adding" - + " constant C, can't apply log." + super().__init__( + variables=variables, return_empty=return_empty, base=base, C=C ) - if (X[self.variables_] + self.C_ <= 0).any().any(): - raise ValueError(error_msg) - - X[self.variables_] = X[self.variables_].astype(float) - - # transform - if self.base == "e": - X.loc[:, self.variables_] = np.log(X.loc[:, self.variables_] + self.C_) - else: - X.loc[:, self.variables_] = np.log10(X.loc[:, self.variables_] + self.C_) - - return X - - def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame: - """ - Convert the data back to the original representation. - - Parameters - ---------- - X: pandas DataFrame of shape = [n_samples, n_features] - The data to be transformed. - - Returns - ------- - X_tr: pandas dataframe - The dataframe with the transformed variables. - """ - - # check input dataframe and if class was fitted - X = self._check_transform_input_and_state(X) - - # inverse transform - if self.base == "e": - X.loc[:, self.variables_] = np.exp(X.loc[:, self.variables_]) - self.C_ - else: - X.loc[:, self.variables_] = 10 ** X.loc[:, self.variables_] - self.C_ + def _more_tags(self): + # LogCpTransformer's default ("auto") always finds a valid shift, so it + # doesn't hit the zero-value errors LogTransformer's C=0 default does. + # Restore the un-xfailed tags rather than inheriting LogTransformer's. + return _return_tags() - return X + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/tests/test_transformation/test_log_transformer.py b/tests/test_transformation/test_log_transformer.py index d1ccf4a2e..74fbe5c85 100644 --- a/tests/test_transformation/test_log_transformer.py +++ b/tests/test_transformation/test_log_transformer.py @@ -137,3 +137,18 @@ def test_inverse_e_plus_user_passes_var_list(df_vartypes): assert transformer.n_features_in_ == 5 # test transform output pd.testing.assert_frame_equal(X, df_vartypes) + +def test_default_C_preserves_original_fail_fast_behavior(): + """LogTransformer()'s default C=0 must raise at fit() time, with the + original exact message, matching pre-merge behavior. See #957.""" + df = pd.DataFrame({"x": [1, 2, 0, 4]}) + tr = LogTransformer() + + assert tr.C == 0 + + with pytest.raises(ValueError) as record: + tr.fit(df) + + assert str(record.value) == ( + "Some variables contain zero or negative values, can't apply log" + ) diff --git a/tests/test_transformation/test_logcp_transformer.py b/tests/test_transformation/test_logcp_transformer.py index 7808c52b4..76be1d68f 100644 --- a/tests/test_transformation/test_logcp_transformer.py +++ b/tests/test_transformation/test_logcp_transformer.py @@ -14,7 +14,7 @@ def test_base_parameter(base): @pytest.mark.parametrize("base", [False, 1, 10]) def test_base_raises_error(base): - msg = f"base can take only '10' or 'e' as values. Got {base} instead." + msg = "base can take only '10' or 'e' as values" with pytest.raises(ValueError) as record: LogCpTransformer(base=base) assert str(record.value) == msg From dc3cc3c2184c362d67f5c4cc032195e978ebcf36 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Fri, 24 Jul 2026 16:14:15 +0530 Subject: [PATCH 02/14] Merge LogCpTransformer into LogTransformer with C parameter --- feature_engine/transformation/log.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 17b1b0e36..1d0da3e9c 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -291,11 +291,10 @@ class LogCpTransformer(LogTransformer): LogCpTransformer() applies the transformation log(x + C), where x is the variable to transform and C is a positive constant. - .. deprecated:: (next release) - `LogCpTransformer` is deprecated and will be removed in a future release. - Use :class:`LogTransformer` with ``C="auto"`` instead, i.e. - ``LogTransformer(C="auto")`` reproduces `LogCpTransformer`'s default - behavior exactly. + .. note:: + `LogCpTransformer` is being consolidated into `LogTransformer`. New code + should prefer ``LogTransformer(C="auto")``, which reproduces + `LogCpTransformer`'s default behavior exactly. See :class:`LogTransformer` for the full parameter and attribute reference. From 07aca63d3331605bdd44ae4dfe6fb427d6a7f827 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Fri, 24 Jul 2026 16:25:22 +0530 Subject: [PATCH 03/14] Fix style: trailing newline, blank line spacing, and note directive indentation --- feature_engine/transformation/log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 1d0da3e9c..495e8677f 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -339,4 +339,5 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags + \ No newline at end of file From e5024f8334eae7f1b1d78005e7c5259cadedca13 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Fri, 24 Jul 2026 16:26:17 +0530 Subject: [PATCH 04/14] Fix style: trailing newline, blank line spacing, and note directive indentation --- feature_engine/transformation/log.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 495e8677f..870f877f8 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -292,9 +292,9 @@ class LogCpTransformer(LogTransformer): variable to transform and C is a positive constant. .. note:: - `LogCpTransformer` is being consolidated into `LogTransformer`. New code - should prefer ``LogTransformer(C="auto")``, which reproduces - `LogCpTransformer`'s default behavior exactly. + `LogCpTransformer` is being consolidated into `LogTransformer`. New + code should prefer ``LogTransformer(C="auto")``, which reproduces + `LogCpTransformer`'s default behavior exactly. See :class:`LogTransformer` for the full parameter and attribute reference. @@ -340,4 +340,3 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() return tags - \ No newline at end of file From 17ca81a90c42f4ecb213eca3efd7b8c44aae53e7 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Fri, 24 Jul 2026 16:27:39 +0530 Subject: [PATCH 05/14] Fix style: trailing newline, blank line spacing, and note directive indentation --- feature_engine/transformation/log.py | 1 + 1 file changed, 1 insertion(+) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 870f877f8..4ca9f779b 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -340,3 +340,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() return tags + From b34011e1f9187cdb13be24eb836a3fc188ddd506 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Fri, 24 Jul 2026 16:31:35 +0530 Subject: [PATCH 06/14] Fix E302: add missing blank line before new test --- tests/test_transformation/test_log_transformer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_transformation/test_log_transformer.py b/tests/test_transformation/test_log_transformer.py index 74fbe5c85..ed6712deb 100644 --- a/tests/test_transformation/test_log_transformer.py +++ b/tests/test_transformation/test_log_transformer.py @@ -138,6 +138,7 @@ def test_inverse_e_plus_user_passes_var_list(df_vartypes): # test transform output pd.testing.assert_frame_equal(X, df_vartypes) + def test_default_C_preserves_original_fail_fast_behavior(): """LogTransformer()'s default C=0 must raise at fit() time, with the original exact message, matching pre-merge behavior. See #957.""" From fd17522d6a443258157910ffcef086446b0feec6 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Fri, 24 Jul 2026 16:42:41 +0530 Subject: [PATCH 07/14] Fix W293: remove trailing whitespace from blank line --- feature_engine/transformation/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 4ca9f779b..38c86c8e1 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -340,4 +340,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() return tags - + From bf16b34648266204601364f7942497c07653ef55 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Fri, 24 Jul 2026 16:49:15 +0530 Subject: [PATCH 08/14] Normalize log.py line endings to LF, matching repo convention --- feature_engine/transformation/log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 38c86c8e1..870f877f8 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -340,4 +340,3 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() return tags - From 8dd637c38d0c02c45e8e8e43559b90af7a0a2750 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Sun, 26 Jul 2026 12:44:22 +0530 Subject: [PATCH 09/14] Apply review feedback to log.py and deprecate LogCpTransformer - Emit a FutureWarning on LogCpTransformer instantiation, announcing its removal in version 2.1.0 in favour of LogTransformer(C=auto). - Align the LogCpTransformer docstring note with that warning: it is deprecated in 2.0.0 and removed in 2.1.0, not deprecated in 2.1.0. - Explain why `variables` is ignored when C is a dictionary (the variables are taken from the dictionary keys) instead of restating that C accepts dictionaries, which the parameter list above already documents. - Mark the pending removal with a TODO above the class statement so it stays a developer note rather than rendered documentation text. - Note in LogTransformer's docstring that C will default to auto in 2.1.0. Co-Authored-By: Claude Opus 5 --- feature_engine/transformation/log.py | 33 +++++++++++++++++----------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 870f877f8..eabf30c07 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -1,6 +1,7 @@ # Authors: Soledad Galli # License: BSD 3 clause +import warnings from typing import Dict, List, Optional, Union import numpy as np @@ -47,9 +48,10 @@ class LogTransformer(BaseNumericalTransformer, FitFromDictMixin): The LogTransformer() applies the natural logarithm or the base 10 logarithm to numerical variables, optionally after adding a constant C, i.e., log(x + C). - By default, C=0, so LogTransformer() only works with positive values and - behaves exactly as it always has: if a variable contains a zero or a negative - value, the transformer raises an error. + By default, C=0, so LogTransformer() only works with positive values. + If a variable contains a zero or a negative value, the transformer raises + an error. Note that the default value of C will change from 0 to "auto" + in version 2.1.0. To transform variables that contain zero or negative values, pass a non-zero C: either an explicit constant, "auto" to let the transformer determine a @@ -74,19 +76,20 @@ class LogTransformer(BaseNumericalTransformer, FitFromDictMixin): The constant C to add to the variable before the logarithm, i.e., log(x + C). - If 0 (the default), no constant is added and the variable must be - strictly positive, matching the transformer's original behavior. + strictly positive. - If int or float, then log(x + C). - If "auto", then C = abs(min(x)) + 1. - If dict, dictionary mapping the constant C to apply to each variable. - Note, when C is a dictionary, the parameter `variables` is ignored. + Note, when C is a dictionary, the parameter `variables` is ignored, because + the variables to transform are taken from the dictionary keys. Attributes ---------- {variables_} C_: - The constant C added to each variable. Equal to C, unless C = "auto", in + The constant C added to each variable. Equal to `C`, unless `C = "auto"`, in which case it is a dictionary with C = abs(min(variable)) + 1. For strictly positive variables, C = 0. @@ -154,10 +157,6 @@ def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): transformation, if C="auto". Otherwise, this transformer does not learn parameters. - Selects the numerical variables and, when C=0 (the default), determines - whether the logarithm can be applied on the selected variables, i.e., it - checks that the variables are positive. - Parameters ---------- X: pandas DataFrame of shape = [n_samples, n_features]. @@ -286,15 +285,17 @@ def __sklearn_tags__(self): return tags +# TODO: remove in version 2.1.0 class LogCpTransformer(LogTransformer): """ LogCpTransformer() applies the transformation log(x + C), where x is the variable to transform and C is a positive constant. .. note:: - `LogCpTransformer` is being consolidated into `LogTransformer`. New - code should prefer ``LogTransformer(C="auto")``, which reproduces - `LogCpTransformer`'s default behavior exactly. + `LogCpTransformer` is consolidated into `LogTransformer` and deprecated + in version 2.0.0. It will be removed in version 2.1.0. New code should prefer + ``LogTransformer(C="auto")``, which reproduces `LogCpTransformer`'s + default behavior exactly. See :class:`LogTransformer` for the full parameter and attribute reference. @@ -330,6 +331,12 @@ def __init__( super().__init__( variables=variables, return_empty=return_empty, base=base, C=C ) + warnings.warn( + "LogCpTransformer was deprecated in version 2.0.0 in favour of " + "LogTransformer and will be removed in version 2.1.0. " + 'Use LogTransformer(C="auto") instead.', + FutureWarning, + ) def _more_tags(self): # LogCpTransformer's default ("auto") always finds a valid shift, so it From bcde39d394839aeae9caf7491604cb893c1a6b14 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Sun, 26 Jul 2026 12:45:16 +0530 Subject: [PATCH 10/14] Add tests and user guide docs for LogTransformer's C parameter Port the C-parameter tests from test_logcp_transformer.py to test_log_transformer.py, covering parameter validation, C=auto/dict/int at fit time, the transform-time positivity check, both logarithm bases and inverse_transform. Uses a `df_c` fixture to avoid colliding with the existing conftest fixtures. Document the new parameter in the user guide: replace the note stating that non-positive values always raise and pointing to the now-deprecated LogCpTransformer, and add a section demonstrating C=auto on the diabetes dataset. Co-Authored-By: Claude Opus 5 --- .../transformation/LogTransformer.rst | 63 +++++++++- .../test_log_transformer.py | 108 ++++++++++++++++++ 2 files changed, 169 insertions(+), 2 deletions(-) diff --git a/docs/user_guide/transformation/LogTransformer.rst b/docs/user_guide/transformation/LogTransformer.rst index 133b7ba8e..20a1bb6be 100644 --- a/docs/user_guide/transformation/LogTransformer.rst +++ b/docs/user_guide/transformation/LogTransformer.rst @@ -28,9 +28,10 @@ LogTransformer .. note:: - Note that the logarithm can only be applied to positive values. Thus, if the variable contains 0 or negative values, this transformer will return an error. + By default (``C=0``), the logarithm can only be applied to positive values. If the variable contains 0 or negative values, the transformer will return an error, unless you use the ``C`` parameter described below. -To transform non-positive variables you can add a constant to shift the data points towards positive values. You can do this by using :class:`LogCpTransformer()`. +To transform non-positive variables, pass a value to the ``C`` parameter to shift +the data points towards positive values, as shown in the section below. .. attention:: @@ -161,6 +162,64 @@ In the following plots we see histograms showing the variables in their original Following the transformations with scatter plots and residual analysis of the regression models helps understand if the transformations are useful in our regression analysis. +Transforming non-positive variables with C +------------------------------------------ + +:class:`LogTransformer()` can also transform variables that contain zero or +negative values, by adding a constant ``C`` before applying the logarithm, +i.e. ``log(x + C)``. + +Let's load the diabetes dataset that comes with scikit-learn, which contains +variables with negative values: + +.. code:: python + + from sklearn.model_selection import train_test_split + from sklearn.datasets import load_diabetes + from feature_engine.transformation import LogTransformer + + # Load dataset + X, y = load_diabetes(return_X_y=True, as_frame=True) + + # Separate into train and test sets + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.3, random_state=0) + +Let's set up :class:`LogTransformer()` with ``C="auto"``, so it automatically +finds the constant needed to shift each variable's distribution to positive +values before applying the logarithm: + +.. code:: python + + logt = LogTransformer(variables=["bmi", "s3"], C="auto") + logt.fit(X_train) + +We can inspect the constant values that will be added to each variable in the +``C_`` attribute: + +.. code:: python + + logt.C_ + +Since these variables are not strictly positive, the transformer found the +minimum value needed to make their values positive: + +.. code:: python + + {'bmi': 1.0848862355291056, 's3': 1.102307050517416} + +We can now transform the data: + +.. code:: python + + train_t = logt.transform(X_train) + test_t = logt.transform(X_test) + +You can also pass a fixed constant to all variables (``C=5``), or a dictionary +mapping each variable to its own constant (``C={"bmi": 2, "s3": 3}``), the same +way you would with the deprecated :class:`LogCpTransformer()`. + + Additional resources -------------------- diff --git a/tests/test_transformation/test_log_transformer.py b/tests/test_transformation/test_log_transformer.py index ed6712deb..2695f526b 100644 --- a/tests/test_transformation/test_log_transformer.py +++ b/tests/test_transformation/test_log_transformer.py @@ -153,3 +153,111 @@ def test_default_C_preserves_original_fail_fast_behavior(): assert str(record.value) == ( "Some variables contain zero or negative values, can't apply log" ) + + +@pytest.fixture(scope="module") +def df_c(): + df = pd.DataFrame( + { + "vara": [0, 1, 2, 3], + "varb": [5, 5, 6, 7], + "varc": [-2, -1, 0, 4], + "vard": [-3, -2, -1, -5], + "vare": ["a", "b", "c", "d"], + } + ) + return df + + +@pytest.mark.parametrize("c", [1, 0.1, {"var1": 1, "var2": 2}, "auto"]) +def test_c_parameter(c): + tr = LogTransformer(C=c) + assert tr.C == c + + +@pytest.mark.parametrize("c", ["string", [1, 2]]) +def test_c_raises_error(c): + msg = f"C can take only 'auto', integers or floats. Got {c} instead." + with pytest.raises(ValueError) as record: + LogTransformer(C=c) + assert str(record.value) == msg + + +def test_C_when_auto(df_c): + tr = LogTransformer(C="auto") + tr.fit(df_c) + c = {"vara": 1, "varb": 0, "varc": 3, "vard": 6} + assert tr.C_ == c + + +def test_C_when_dict(df_c): + c = {"vara": 1, "varb": 0, "varc": 3, "vard": 6} + tr = LogTransformer(C=c) + tr.fit(df_c) + assert tr.C_ == c + + +def test_C_when_int(df_c): + tr = LogTransformer(C=10) + tr.fit(df_c) + assert tr.C_ == 10 + + +def test_raises_error_when_transformed_data_has_negative_values_with_C(df_c): + tr = LogTransformer(C="auto") + tr.fit(df_c) + dft = df_c.copy() + dft["vara"] = dft["vara"] - 2 + msg = ( + "Some variables contain zero or negative values after adding constant C, " + "can't apply log." + ) + with pytest.raises(ValueError) as record: + tr.transform(dft) + assert str(record.value) == msg + + +def test_log_base_e_with_C(df_c): + dft = LogTransformer(C="auto").fit_transform(df_c) + exp = np.log( + df_c[["vara", "varb", "varc", "vard"]] + + {"vara": 1, "varb": 0, "varc": 3, "vard": 6} + ) + exp["vare"] = df_c["vare"] + pd.testing.assert_frame_equal(dft, exp) + + dft = LogTransformer(C=10).fit_transform(df_c) + exp = np.log(df_c[["vara", "varb", "varc", "vard"]] + 10) + exp["vare"] = df_c["vare"] + pd.testing.assert_frame_equal(dft, exp) + + +def test_log_base_10_with_C(df_c): + dft = LogTransformer(C="auto", base="10").fit_transform(df_c) + exp = np.log10( + df_c[["vara", "varb", "varc", "vard"]] + + {"vara": 1, "varb": 0, "varc": 3, "vard": 6} + ) + exp["vare"] = df_c["vare"] + pd.testing.assert_frame_equal(dft, exp) + + dft = LogTransformer(C=10, base="10").fit_transform(df_c) + exp = np.log10(df_c[["vara", "varb", "varc", "vard"]] + 10) + exp["vare"] = df_c["vare"] + pd.testing.assert_frame_equal(dft, exp) + + +def test_inverse_transform_with_C(df_c): + tr = LogTransformer(C="auto", base="10") + dft = tr.fit_transform(df_c) + orig = tr.inverse_transform(dft) + pd.testing.assert_frame_equal( + orig, df_c, check_dtype=False, check_exact=False, rtol=0.1 + ) + + tr = LogTransformer(C=10, base="e") + dft = tr.fit_transform(df_c) + orig = tr.inverse_transform(dft) + pd.testing.assert_frame_equal( + orig, df_c, check_dtype=False, check_exact=False, rtol=0.1 + ) From 2b5c9b9a33cd0c912dc94b2457aaace752b0a9d2 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Sun, 26 Jul 2026 13:11:13 +0530 Subject: [PATCH 11/14] Add the offending value to the base validation error message --- feature_engine/transformation/log.py | 4 +++- tests/test_transformation/test_log_transformer.py | 5 ++++- tests/test_transformation/test_logcp_transformer.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index eabf30c07..e07fed4df 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -137,7 +137,9 @@ def __init__( ) -> None: if base not in ["e", "10"]: - raise ValueError("base can take only '10' or 'e' as values") + raise ValueError( + f"base can take only '10' or 'e' as values. Got {base} instead." + ) if not isinstance(C, (int, float, dict)) and C != "auto": raise ValueError( diff --git a/tests/test_transformation/test_log_transformer.py b/tests/test_transformation/test_log_transformer.py index 2695f526b..31dffce87 100644 --- a/tests/test_transformation/test_log_transformer.py +++ b/tests/test_transformation/test_log_transformer.py @@ -78,8 +78,11 @@ def test_log_base_10_plus_user_passes_var_list(df_vartypes): def test_error_if_base_value_not_allowed(): - with pytest.raises(ValueError): + with pytest.raises(ValueError) as record: LogTransformer(base="other") + assert str(record.value) == ( + "base can take only '10' or 'e' as values. Got other instead." + ) def test_fit_raises_error_if_na_in_df(df_na): diff --git a/tests/test_transformation/test_logcp_transformer.py b/tests/test_transformation/test_logcp_transformer.py index 76be1d68f..7808c52b4 100644 --- a/tests/test_transformation/test_logcp_transformer.py +++ b/tests/test_transformation/test_logcp_transformer.py @@ -14,7 +14,7 @@ def test_base_parameter(base): @pytest.mark.parametrize("base", [False, 1, 10]) def test_base_raises_error(base): - msg = "base can take only '10' or 'e' as values" + msg = f"base can take only '10' or 'e' as values. Got {base} instead." with pytest.raises(ValueError) as record: LogCpTransformer(base=base) assert str(record.value) == msg From 7eae67fda331a902c83162097dea42f836616d92 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Sun, 26 Jul 2026 14:29:44 +0530 Subject: [PATCH 12/14] Include dictionaries in the C validation error message --- feature_engine/transformation/log.py | 3 ++- tests/test_transformation/test_log_transformer.py | 2 +- tests/test_transformation/test_logcp_transformer.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index e07fed4df..4e4024ac8 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -143,7 +143,8 @@ def __init__( if not isinstance(C, (int, float, dict)) and C != "auto": raise ValueError( - f"C can take only 'auto', integers or floats. Got {C} instead." + "C can take only 'auto', integers, floats or dictionaries. " + f"Got {C} instead." ) _check_return_empty_is_bool(return_empty) diff --git a/tests/test_transformation/test_log_transformer.py b/tests/test_transformation/test_log_transformer.py index 31dffce87..23a74104c 100644 --- a/tests/test_transformation/test_log_transformer.py +++ b/tests/test_transformation/test_log_transformer.py @@ -180,7 +180,7 @@ def test_c_parameter(c): @pytest.mark.parametrize("c", ["string", [1, 2]]) def test_c_raises_error(c): - msg = f"C can take only 'auto', integers or floats. Got {c} instead." + msg = f"C can take only 'auto', integers, floats or dictionaries. Got {c} instead." with pytest.raises(ValueError) as record: LogTransformer(C=c) assert str(record.value) == msg diff --git a/tests/test_transformation/test_logcp_transformer.py b/tests/test_transformation/test_logcp_transformer.py index 7808c52b4..d2f64e1c1 100644 --- a/tests/test_transformation/test_logcp_transformer.py +++ b/tests/test_transformation/test_logcp_transformer.py @@ -28,7 +28,7 @@ def test_c_parameter(c): @pytest.mark.parametrize("c", ["string", [1, 2]]) def test_c_raises_error(c): - msg = f"C can take only 'auto', integers or floats. Got {c} instead." + msg = f"C can take only 'auto', integers, floats or dictionaries. Got {c} instead." with pytest.raises(ValueError) as record: LogCpTransformer(C=c) assert str(record.value) == msg From 9d579d162592aeb6f1f7d2b81969bafe52498174 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Tue, 28 Jul 2026 23:07:07 +0530 Subject: [PATCH 13/14] Remove LogCpTransformer from docs listings and document the C parameter --- README.md | 1 - docs/api_doc/transformation/index.rst | 1 - docs/index.rst | 1 - .../transformation/LogCpTransformer.rst | 6 +++++ .../transformation/LogTransformer.rst | 9 +++---- .../transformation/PowerTransformer.rst | 1 - docs/user_guide/transformation/index.rst | 24 +++++++++---------- .../test_logcp_transformer.py | 11 +++++++++ 8 files changed, 31 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 76a9056bc..6e80a58fd 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,6 @@ Please share your story by answering 1 quick question ### Variable Transformation methods * LogTransformer -* LogCpTransformer * ReciprocalTransformer * ArcsinTransformer * PowerTransformer diff --git a/docs/api_doc/transformation/index.rst b/docs/api_doc/transformation/index.rst index a6b273551..1850bdcab 100644 --- a/docs/api_doc/transformation/index.rst +++ b/docs/api_doc/transformation/index.rst @@ -10,7 +10,6 @@ mathematical transformations. :maxdepth: 1 LogTransformer - LogCpTransformer ReciprocalTransformer ArcsinTransformer ArcSinhTransformer diff --git a/docs/index.rst b/docs/index.rst index 6a05f7e9e..6ffbe9747 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -237,7 +237,6 @@ We normally use variance stabilising transformations to make the data meet the a like ANOVA, and machine learning models, like linear regression. Feature-engine supports the following transformations: - :doc:`api_doc/transformation/LogTransformer`: performs logarithmic transformation of numerical variables -- :doc:`api_doc/transformation/LogCpTransformer`: performs logarithmic transformation after adding a constant value - :doc:`api_doc/transformation/ReciprocalTransformer`: performs reciprocal transformation of numerical variables - :doc:`api_doc/transformation/PowerTransformer`: performs power transformation of numerical variables - :doc:`api_doc/transformation/BoxCoxTransformer`: performs Box-Cox transformation of numerical variables diff --git a/docs/user_guide/transformation/LogCpTransformer.rst b/docs/user_guide/transformation/LogCpTransformer.rst index 906e50cd9..755d0a782 100644 --- a/docs/user_guide/transformation/LogCpTransformer.rst +++ b/docs/user_guide/transformation/LogCpTransformer.rst @@ -13,6 +13,12 @@ positive values. adding a constant to move distributions towards positive values. For more details about the logarithm transformation, check out the :ref:`LogTransformer()'s user Guide `. +.. warning:: + + :class:`LogCpTransformer()` was deprecated in version 2.0.0 and will be removed in + version 2.1.0. Use ``LogTransformer(C="auto")`` instead, which reproduces + :class:`LogCpTransformer()`'s default behavior exactly. + Defining C ---------- diff --git a/docs/user_guide/transformation/LogTransformer.rst b/docs/user_guide/transformation/LogTransformer.rst index 20a1bb6be..b73d317d8 100644 --- a/docs/user_guide/transformation/LogTransformer.rst +++ b/docs/user_guide/transformation/LogTransformer.rst @@ -28,10 +28,7 @@ LogTransformer .. note:: - By default (``C=0``), the logarithm can only be applied to positive values. If the variable contains 0 or negative values, the transformer will return an error, unless you use the ``C`` parameter described below. - -To transform non-positive variables, pass a value to the ``C`` parameter to shift -the data points towards positive values, as shown in the section below. + The logarithm can only be applied to positive values; if the variable contains 0 or negative values, the transformer will raise an error. To avoid this, add a constant to the variables by using the `C` parameter (more details below). .. attention:: @@ -163,7 +160,7 @@ In the following plots we see histograms showing the variables in their original Transforming non-positive variables with C ------------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :class:`LogTransformer()` can also transform variables that contain zero or negative values, by adding a constant ``C`` before applying the logarithm, @@ -174,8 +171,8 @@ variables with negative values: .. code:: python - from sklearn.model_selection import train_test_split from sklearn.datasets import load_diabetes + from sklearn.model_selection import train_test_split from feature_engine.transformation import LogTransformer # Load dataset diff --git a/docs/user_guide/transformation/PowerTransformer.rst b/docs/user_guide/transformation/PowerTransformer.rst index 13ac20087..7aef1c6d4 100644 --- a/docs/user_guide/transformation/PowerTransformer.rst +++ b/docs/user_guide/transformation/PowerTransformer.rst @@ -74,7 +74,6 @@ Other transformers Feature-engine also provides the following power transformers: - :class:`LogTransformer` -- :class:`LogCpTransformer` - :class:`ReciprocalTransformer` - :class:`ArcsinTransformer` diff --git a/docs/user_guide/transformation/index.rst b/docs/user_guide/transformation/index.rst index 41a3eedb1..57634feac 100644 --- a/docs/user_guide/transformation/index.rst +++ b/docs/user_guide/transformation/index.rst @@ -24,18 +24,17 @@ See the following illustration: Supported transformations ------------------------- -================================ ================================================ ============================================================================================= ==================== -Transformer Description Suitable for Limitations -================================ ================================================ ============================================================================================= ==================== -:class:`LogTransformer()` Applies natural or decimal logarithm. Positive continuous variables with right skew. Not valid for x<=0 -:class:`LogCpTransformer()` Applies logarithm after adding a constant value. Continuous variables with a right skew. None -:class:`ReciprocalTransformer()` Applies the reciprocal transformation: 1/x. Variables representing ratios or proportions, like tons per acre. Not defined for x=0 -:class:`ArcsinTransformer()` Applies the arcsin square root transformation. Probabilities or proportion variables with values between 0 and 1. 0<= x <= 1 -:class:`ArcSinhTransformer()` Applies the inverse hyperbolic sine function. Similar to log but retaining zero values in a variable. None -:class:`PowerTransformer()` Applies any power transformation x = x**n. Square root is suitable for count variables. Other powers vary. None -:class:`BoxCoxTransformer()` Applies the Box-Cox transformation. Positive continuous variables when the optimal transformation is unknown. Not defined for x<=0 -:class:`YeoJohnsonTransformer()` Applies the Yeo-Johnson transformation. Continuous variables with zero or negative values when the optimal transformation is unknown. None -================================ ================================================ ============================================================================================= ==================== +================================ ===================================================================== ============================================================================================= ====================================================== +Transformer Description Suitable for Limitations +================================ ===================================================================== ============================================================================================= ====================================================== +:class:`LogTransformer()` Applies natural or decimal logarithm, optionally adding a constant C. Positive continuous variables with right skew. Not valid for x<=0 unless C is used to shift the data. +:class:`ReciprocalTransformer()` Applies the reciprocal transformation: 1/x. Variables representing ratios or proportions, like tons per acre. Not defined for x=0 +:class:`ArcsinTransformer()` Applies the arcsin square root transformation. Probabilities or proportion variables with values between 0 and 1. 0<= x <= 1 +:class:`ArcSinhTransformer()` Applies the inverse hyperbolic sine function. Similar to log but retaining zero values in a variable. None +:class:`PowerTransformer()` Applies any power transformation x = x**n. Square root is suitable for count variables. Other powers vary. None +:class:`BoxCoxTransformer()` Applies the Box-Cox transformation. Positive continuous variables when the optimal transformation is unknown. Not defined for x<=0 +:class:`YeoJohnsonTransformer()` Applies the Yeo-Johnson transformation. Continuous variables with zero or negative values when the optimal transformation is unknown. None +================================ ===================================================================== ============================================================================================= ====================================================== .. note:: @@ -49,7 +48,6 @@ Transformers :maxdepth: 1 LogTransformer - LogCpTransformer ReciprocalTransformer ArcsinTransformer ArcSinhTransformer diff --git a/tests/test_transformation/test_logcp_transformer.py b/tests/test_transformation/test_logcp_transformer.py index d2f64e1c1..753cc43bf 100644 --- a/tests/test_transformation/test_logcp_transformer.py +++ b/tests/test_transformation/test_logcp_transformer.py @@ -34,6 +34,17 @@ def test_c_raises_error(c): assert str(record.value) == msg +def test_instantiation_raises_future_warning(): + msg = ( + "LogCpTransformer was deprecated in version 2.0.0 in favour of " + "LogTransformer and will be removed in version 2.1.0. " + 'Use LogTransformer(C="auto") instead.' + ) + with pytest.warns(FutureWarning) as record: + LogCpTransformer() + assert str(record[0].message) == msg + + @pytest.fixture(scope="module") def df(): df = pd.DataFrame( From 609ef8161862a5b6475a62fd7d182e5d5e4fa663 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Tue, 28 Jul 2026 23:25:47 +0530 Subject: [PATCH 14/14] Mark orphaned LogCpTransformer doc pages to satisfy Sphinx --- docs/api_doc/transformation/LogCpTransformer.rst | 2 ++ docs/user_guide/transformation/LogCpTransformer.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/api_doc/transformation/LogCpTransformer.rst b/docs/api_doc/transformation/LogCpTransformer.rst index b5161de6a..b5f050ff0 100644 --- a/docs/api_doc/transformation/LogCpTransformer.rst +++ b/docs/api_doc/transformation/LogCpTransformer.rst @@ -1,3 +1,5 @@ +:orphan: + LogCpTransformer ================ diff --git a/docs/user_guide/transformation/LogCpTransformer.rst b/docs/user_guide/transformation/LogCpTransformer.rst index 755d0a782..89982dbcb 100644 --- a/docs/user_guide/transformation/LogCpTransformer.rst +++ b/docs/user_guide/transformation/LogCpTransformer.rst @@ -1,3 +1,5 @@ +:orphan: + .. _log_cp: .. currentmodule:: feature_engine.transformation