Repository: ourownstory/neural_prophet

Issue:
[enhancement]  remove regressor if training set has one unique value
Instead of throwing an error, simply remove the problematic regressor from the model with an error log message.
https://github.com/ourownstory/neural_prophet/blob/ea717ad31dec216ba670492f9b89fd075621dc3a/neuralprophet/df_utils.py#L318

This has caused issues for people in the past.
e.g. #809

This  must also address:
- reduce error to warning when unused column in df ( training or predicting have unused cols not in model definition)


Discussion:
Note: we discussed that we would simply remove a regressor with non-unique values from the model and throw a warning instead of manipulating the models weights.

Resolving patch:
diff --git a/neuralprophet/df_utils.py b/neuralprophet/df_utils.py
--- a/neuralprophet/df_utils.py
+++ b/neuralprophet/df_utils.py
@@ -333,7 +333,6 @@ def init_data_params(
 
 def auto_normalization_setting(array):
     if len(np.unique(array)) < 2:
-        log.error("Encountered variable with singular value in training set. Please remove variable.")
         raise ValueError("Encountered variable with singular value in training set. Please remove variable.")
     # elif set(series.unique()) in ({True, False}, {1, 0}, {1.0, 0.0}, {-1, 1}, {-1.0, 1.0}):
     elif len(np.unique(array)) == 2:
@@ -444,6 +443,14 @@ def check_single_dataframe(df, check_y, covariates, regressors, events):
         raise ValueError("Column ds has timezone specified, which is not supported. Remove timezone.")
     if len(df.ds.unique()) != len(df.ds):
         raise ValueError("Column ds has duplicate values. Please remove duplicates.")
+    regressors_to_remove = []
+    if regressors is not None:
+        for reg in regressors:
+            if len(df[reg].unique()) < 2:
+                log.warning(
+                    "Encountered future regressor with only unique values in training set. Automatically removed variable."
+                )
+                regressors_to_remove.append(reg)
 
     columns = []
     if check_y:
@@ -479,7 +486,7 @@ def check_single_dataframe(df, check_y, covariates, regressors, events):
         df.index.name = None
     df = df.sort_values("ds")
     df = df.reset_index(drop=True)
-    return df
+    return df, regressors_to_remove
 
 
 def check_dataframe(df, check_y=True, covariates=None, regressors=None, events=None):
@@ -507,11 +514,18 @@ def check_dataframe(df, check_y=True, covariates=None, regressors=None, events=N
     """
     df, _, _, _, _ = prep_or_copy_df(df)
     checked_df = pd.DataFrame()
+    regressors_to_remove = []
     for df_name, df_i in df.groupby("ID"):
-        df_aux = check_single_dataframe(df_i, check_y, covariates, regressors, events).copy(deep=True)
+        df_aux, reg = check_single_dataframe(df_i, check_y, covariates, regressors, events)
+        df_aux = df_aux.copy(deep=True)
+        if len(reg) > 0:
+            regressors_to_remove.append(*reg)
         df_aux["ID"] = df_name
         checked_df = pd.concat((checked_df, df_aux), ignore_index=True)
-    return checked_df
+    if len(regressors_to_remove) > 0:
+        regressors_to_remove = list(set(regressors_to_remove))
+        checked_df = checked_df.drop(*regressors_to_remove, axis=1)
+    return checked_df, regressors_to_remove
 
 
 def _crossvalidation_split_df(df, n_lags, n_forecasts, k, fold_pct, fold_overlap_pct=0.0):
diff --git a/neuralprophet/forecaster.py b/neuralprophet/forecaster.py
--- a/neuralprophet/forecaster.py
+++ b/neuralprophet/forecaster.py
@@ -2152,13 +2152,17 @@ def _check_dataframe(self, df, check_y=True, exogenous=True):
                 checked dataframe
         """
         df, _, _, _, _ = df_utils.prep_or_copy_df(df)
-        return df_utils.check_dataframe(
+        df, regressors_to_remove = df_utils.check_dataframe(
             df=df,
             check_y=check_y,
             covariates=self.config_lagged_regressors if exogenous else None,
             regressors=self.config_regressors if exogenous else None,
             events=self.config_events if exogenous else None,
         )
+        for reg in regressors_to_remove:
+            log.warning(f"Removing regressor {reg} because it is not present in the data.")
+            self.config_regressors.pop(reg)
+        return df
 
     def _validate_column_name(self, name, events=True, seasons=True, regressors=True, covariates=True):
         """Validates the name of a seasonality, event, or regressor.


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -47,7 +47,7 @@ def test_train_eval_test():
         learning_rate=LR,
     )
     df = pd.read_csv(PEYTON_FILE, nrows=95)
-    df = df_utils.check_dataframe(df, check_y=False)
+    df, _ = df_utils.check_dataframe(df, check_y=False)
     df = m._handle_missing_data(df, freq="D", predicting=False)
     df_train, df_test = m.split_df(df, freq="D", valid_p=0.1)
     metrics = m.fit(df_train, freq="D", validation_df=df_test)
@@ -59,7 +59,7 @@ def test_train_eval_test():
 def test_df_utils_func():
     log.info("testing: df_utils Test")
     df = pd.read_csv(PEYTON_FILE, nrows=95)
-    df = df_utils.check_dataframe(df, check_y=False)
+    df, _ = df_utils.check_dataframe(df, check_y=False)
 
     # test find_time_threshold
     df, _, _, _, _ = df_utils.prep_or_copy_df(df)
@@ -1335,6 +1335,28 @@ def test_global_modeling_with_events_and_future_regressors():
             fig3 = m.plot_parameters()
 
 
+def test_auto_normalization():
+    length = 100
+    days = pd.date_range(start="2017-01-01", periods=length)
+    y = np.ones(length)
+    y[1] = 0
+    y[2] = 2
+    y[3] = 3.3
+    df = pd.DataFrame({"ds": days, "y": y})
+    df["future_constant"] = 1.0
+    df["future_dynamic"] = df["y"] * 2
+    m = NeuralProphet(
+        epochs=EPOCHS,
+        batch_size=BATCH_SIZE,
+        learning_rate=LR,
+        n_forecasts=5,
+        normalize="auto",
+    )
+    m = m.add_future_regressor("future_constant")
+    m = m.add_future_regressor("future_dynamic")
+    _ = m.fit(df, freq="D")
+
+
 def test_minimal():
     log.info("testing: Plotting")
     df = pd.read_csv(PEYTON_FILE, nrows=NROWS)
diff --git a/tests/test_regularization.py b/tests/test_regularization.py
--- a/tests/test_regularization.py
+++ b/tests/test_regularization.py
@@ -50,7 +50,7 @@ def test_reg_func_abs():
 
 def test_regularization_holidays():
     df = generate_holiday_dataset(y_holidays_override=Y_HOLIDAYS_OVERRIDE)
-    df = df_utils.check_dataframe(df, check_y=False)
+    df, _ = df_utils.check_dataframe(df, check_y=False)
 
     m = NeuralProphet(
         epochs=20,
@@ -81,7 +81,7 @@ def test_regularization_holidays():
 
 def test_regularization_events():
     df, events = generate_event_dataset(y_events_override=Y_EVENTS_OVERRIDE)
-    df = df_utils.check_dataframe(df, check_y=False)
+    df, _ = df_utils.check_dataframe(df, check_y=False)
 
     m = NeuralProphet(
         epochs=50,
@@ -132,7 +132,7 @@ def test_regularization_lagged_regressor():
     components are turned off to avoid side effects.
     """
     df, lagged_regressors = generate_lagged_regressor_dataset(periods=100)
-    df = df_utils.check_dataframe(df, check_y=False)
+    df, _ = df_utils.check_dataframe(df, check_y=False)
 
     m = NeuralProphet(
         epochs=30,
diff --git a/tests/test_unit.py b/tests/test_unit.py
--- a/tests/test_unit.py
+++ b/tests/test_unit.py
@@ -76,7 +76,7 @@ def test_time_dataset():
     config_missing = configure.MissingDataHandling()
     df_train, df_val = df_utils.split_df(df_in, n_lags, n_forecasts, valid_p)
     # create a tabularized dataset from time series
-    df = df_utils.check_dataframe(df_train)
+    df, _ = df_utils.check_dataframe(df_train)
     local_data_params, global_data_params = df_utils.init_data_params(df=df, normalize="minmax")
     df = df.drop("ID", axis=1)
     df = df_utils.normalize(df, global_data_params)
@@ -224,7 +224,7 @@ def check_split(df_in, df_len_expected, n_lags, n_forecasts, freq, p=0.1):
             n_lags=n_lags,
             n_forecasts=n_forecasts,
         )
-        df_in = df_utils.check_dataframe(df_in, check_y=False)
+        df_in, _ = df_utils.check_dataframe(df_in, check_y=False)
         df_in = m._handle_missing_data(df_in, freq=freq, predicting=False)
         assert df_len_expected == len(df_in)
         total_samples = len(df_in) - n_lags - 2 * n_forecasts + 2
@@ -848,7 +848,7 @@ def test_too_many_NaN():
         limit_linear=config_missing.impute_linear,
         rolling=config_missing.impute_rolling,
     )
-    df = df_utils.check_dataframe(df)
+    df, _ = df_utils.check_dataframe(df)
     local_data_params, global_data_params = df_utils.init_data_params(df=df, normalize="minmax")
     df = df.drop("ID", axis=1)
     df = df_utils.normalize(df, global_data_params)