Repository: solegalli/feature_engine

Issue:
allow  MathematicalCombination to take in null values
introduce parameter to be able to select whether to ignore or raise the missing values

Comments:

Patch:
diff --git a/feature_engine/creation/mathematical_combination.py b/feature_engine/creation/mathematical_combination.py
index fda42a6..e762d70 100644
--- a/feature_engine/creation/mathematical_combination.py
+++ b/feature_engine/creation/mathematical_combination.py
@@ -49,6 +49,11 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
     **total_number_payments** and **mean_number_payments**, plus the original set of
     variables.
 
+    Attention, if some of the variables to combine have missing data and
+    missing_values = 'ignore', the value will be ignored in the computation. The
+    computation will be then performed using the remaining variables to combine, for
+    observations with NA.
+
     Parameters
     ----------
 
@@ -87,6 +92,12 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
         to the newly created features starting by the name of the mathematical
         operation, followed by the variables combined separated by -.
 
+    missing_values : string, default='raise'
+        Indicates if missing values should be ignored or raised. If
+        `missing_values='raise'` the transformer will return an error if the
+        the datasets to fit or transform contain missing values. Alternatively, use
+        'ignore'.
+
     Attributes
     ----------
     combination_dict_ :
@@ -125,6 +136,7 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
         variables_to_combine: List[Union[str, int]],
         math_operations: Optional[List[str]] = None,
         new_variables_names: Optional[List[str]] = None,
+        missing_values: str = "raise",
     ) -> None:
 
         # check input types
@@ -160,6 +172,9 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
                     "Choose one or more of ['sum', 'prod', 'mean', 'std', 'max', 'min']"
                 )
 
+        if missing_values not in ["raise", "ignore"]:
+            raise ValueError("missing_values takes only values 'raise' or 'ignore'")
+
         # check input logic
         if len(variables_to_combine) <= 1:
             raise KeyError(
@@ -180,6 +195,7 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
         self.variables_to_combine = variables_to_combine
         self.new_variables_names = new_variables_names
         self.math_operations = math_operations
+        self.missing_values = missing_values
 
     def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
         """
@@ -203,7 +219,7 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
            - If the input is not a Pandas DataFrame
            - If any user provided variables in variables_to_combine are not numerical
         ValueError
-           If the variable(s) contain null values
+           If the variable(s) contain null values when missing_values = raise
 
         Returns
         -------
@@ -219,7 +235,8 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
         )
 
         # check if dataset contains na
-        _check_contains_na(X, self.variables_to_combine)
+        if self.missing_values == "raise":
+            _check_contains_na(X, self.variables_to_combine)
 
         if self.math_operations is None:
             self.math_operations_ = ["sum", "prod", "mean", "std", "max", "min"]
@@ -260,7 +277,7 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
         TypeError
            If the input is not a Pandas DataFrame
         ValueError
-           - If the variable(s) contain null values
+           - If the variable(s) contain null values when missing_values = raise
            - If the dataframe is not of the same size as that used in fit()
 
         Returns
@@ -276,7 +293,8 @@ class MathematicalCombination(BaseEstimator, TransformerMixin):
         X = _is_dataframe(X)
 
         # check if dataset contains na
-        _check_contains_na(X, self.variables_to_combine)
+        if self.missing_values == "raise":
+            _check_contains_na(X, self.variables_to_combine)
 
         # Check if input data contains same number of columns as dataframe used to fit.
         _check_input_matches_training_df(X, self.input_shape_[1])


Test patch:
diff --git a/tests/test_creation/test_mathematical_combination.py b/tests/test_creation/test_mathematical_combination.py
index 2640789..19cde4d 100644
--- a/tests/test_creation/test_mathematical_combination.py
+++ b/tests/test_creation/test_mathematical_combination.py
@@ -1,3 +1,4 @@
+import numpy as np
 import pandas as pd
 import pytest
 
@@ -271,3 +272,55 @@ def test_variable_names_when_df_cols_are_integers(df_numeric_columns):
     }
     # transform params
     pd.testing.assert_frame_equal(X, ref)
+
+
+def test_error_when_null_values_in_variable(df_vartypes):
+
+    df_na = df_vartypes.copy()
+    df_na.loc[1, 'Age'] = np.nan
+
+    with pytest.raises(ValueError):
+        math_combinator = MathematicalCombination(
+            variables_to_combine=["Age", "Marks"],
+            math_operations=["sum", "mean"],
+            missing_values="raise",
+        )
+        math_combinator.fit(df_na)
+
+    with pytest.raises(ValueError):
+
+        math_combinator = MathematicalCombination(
+            variables_to_combine=["Age", "Marks"],
+            math_operations=["sum", "mean"],
+            missing_values="raise",
+        )
+        math_combinator.fit(df_vartypes)
+        math_combinator.transform(df_na)
+
+
+def test_no_error_when_null_values_in_variable(df_vartypes):
+
+    df_na = df_vartypes.copy()
+    df_na.loc[1, 'Age'] = np.nan
+
+    transformer = MathematicalCombination(
+        variables_to_combine=["Age", "Marks"],
+        math_operations=["sum", "mean"],
+        missing_values="ignore",
+    )
+
+    X = transformer.fit_transform(df_na)
+
+    ref = pd.DataFrame.from_dict(
+        {
+            "Name": ["tom", "nick", "krish", "jack"],
+            "City": ["London", "Manchester", "Liverpool", "Bristol"],
+            "Age": [20, np.nan, 19, 18],
+            "Marks": [0.9, 0.8, 0.7, 0.6],
+            "dob": pd.date_range("2020-02-24", periods=4, freq="T"),
+            "sum(Age-Marks)": [20.9, 0.8, 19.7, 18.6],
+            "mean(Age-Marks)": [10.45, 0.8, 9.85, 9.3],
+        }
+    )
+    # transform params
+    pd.testing.assert_frame_equal(X, ref)