Repository: felipeangelimvieira/prophetverse

Issue:
[ENH] Remove `_input_feature_column_names` and make optional to call `fit` before `transform`
The attribute `_input_feature_column_names` of BaseEffects is not needed anymore since the input columns are filtered before calling the effects. In addition, some effects do not have a fit method, that could be skipped.


Discussion:

Patch:
diff --git a/src/prophetverse/effects/base.py b/src/prophetverse/effects/base.py
index a592865..d2b1b2e 100644
--- a/src/prophetverse/effects/base.py
+++ b/src/prophetverse/effects/base.py
@@ -1,6 +1,6 @@
 """Module that stores abstract class of effects."""
 
-from typing import Any, Dict, List, Literal, Optional
+from typing import Any, Dict, Literal, Optional
 
 import jax.numpy as jnp
 import pandas as pd
@@ -63,8 +63,6 @@ class BaseEffect(BaseObject):
 
     Attributes
     ----------
-    input_feature_column_names : List[str]
-        The names of the input feature columns. This is set during the `fit()` method.
     should_skip_predict : bool
         If True, the effect should be skipped during prediction. This is determined by
         the `skip_predict_if_no_match` tag and the presence of input feature columns
@@ -80,32 +78,13 @@ class BaseEffect(BaseObject):
         # Should only the indexes related to the forecasting horizon be passed to
         # _transform?
         "filter_indexes_with_forecating_horizon_at_transform": True,
+        # Is fit() required before calling transform()?
+        "requires_fit_before_transform": False,
     }
 
     def __init__(self):
-        self._input_feature_column_names: List[str] = []
         self._is_fitted: bool = False
 
-    @property
-    def input_feature_column_names(self) -> List[str]:
-        """Return the input feature columns names."""
-        return self._input_feature_column_names
-
-    @property
-    def should_skip_predict(self) -> bool:
-        """Return if the effect should be skipped by the forecaster.
-
-        Returns
-        -------
-        bool
-            If the effect should be skipped by the forecaster.
-        """
-        if not self._input_feature_column_names and self.get_tag(
-            "skip_predict_if_no_match", True
-        ):
-            return True
-        return False
-
     def fit(self, y: pd.DataFrame, X: pd.DataFrame, scale: float = 1.0):
         """Initialize the effect.
 
@@ -145,11 +124,6 @@ class BaseEffect(BaseObject):
                     + "support multivariate data"
                 )
 
-        if X is None or X.empty:
-            self._input_feature_column_names = []
-        else:
-            self._input_feature_column_names = X.columns.tolist()
-
         self._fit(y=y, X=X, scale=scale)
         self._is_fitted = True
 
@@ -205,19 +179,14 @@ class BaseEffect(BaseObject):
         ValueError
             If the effect has not been fitted.
         """
-        if not self._is_fitted:
+        if not self._is_fitted and self.get_tag("requires_fit_before_transform", True):
             raise ValueError("You must call fit() before calling this method")
 
-        # If apply should be skipped, return an empty dictionary
-        if self.should_skip_predict:
-            return {}
-
         if self.get_tag("filter_indexes_with_forecating_horizon_at_transform", True):
             # Filter when index level -1 is in fh
             if X is not None:
                 X = X.loc[X.index.get_level_values(-1).isin(fh)]
 
-        X = X[self.input_feature_column_names]
         return self._transform(X, fh)
 
     def _transform(
diff --git a/src/prophetverse/effects/lift_experiment.py b/src/prophetverse/effects/lift_experiment.py
index 126b9f3..c5416c2 100644
--- a/src/prophetverse/effects/lift_experiment.py
+++ b/src/prophetverse/effects/lift_experiment.py
@@ -1,6 +1,6 @@
 """Composition of effects (Effects that wrap other effects)."""
 
-from typing import Any, Dict, List
+from typing import Any, Dict
 
 import jax.numpy as jnp
 import numpyro
@@ -141,8 +141,3 @@ class LiftExperimentLikelihood(BaseEffect):
             )
 
         return x
-
-    @property
-    def input_feature_column_names(self) -> List[str]:
-        """Return the input feature columns names."""
-        return self.effect._input_feature_column_names
diff --git a/src/prophetverse/sktime/base.py b/src/prophetverse/sktime/base.py
index 9810f23..e7a6b52 100644
--- a/src/prophetverse/sktime/base.py
+++ b/src/prophetverse/sktime/base.py
@@ -1100,8 +1100,9 @@ class BaseProphetForecaster(_HeterogenousMetaEstimator, BaseBayesianForecaster):
         out = OrderedDict()
         for effect_name, effect, columns in self.exogenous_effects_:
             # If no columns are found, skip
-            if effect.should_skip_predict:
-                continue
+            if columns is None or len(columns) == 0:
+                if effect.get_tag("skip_predict_if_no_match"):
+                    continue
 
             data: Dict[str, jnp.ndarray] = effect.transform(X[columns], fh=fh)
             out[effect_name] = data
@@ -1120,8 +1121,8 @@ class BaseProphetForecaster(_HeterogenousMetaEstimator, BaseBayesianForecaster):
         """
         return {
             effect_name: effect
-            for effect_name, effect, _ in self.exogenous_effects_
-            if not effect.should_skip_predict
+            for effect_name, effect, columns in self.exogenous_effects_
+            if len(columns) > 0 or not effect.get_tag("skip_predict_if_no_match")
         }
 
     def _get_trend_model(self):


Test patch:
diff --git a/tests/effects/test_base.py b/tests/effects/test_base.py
index 6a93917..97a71ff 100644
--- a/tests/effects/test_base.py
+++ b/tests/effects/test_base.py
@@ -29,5 +29,14 @@ def test_bad_effect_mode():
 
 
 def test_not_fitted():
+
+    class EffectNotFitted(BaseEffect):
+        _tags = {"requires_fit_before_transform": False}
+
+    EffectNotFitted().transform(pd.DataFrame(), fh=pd.Index([]))
+
+    class EffectMustFit(BaseEffect):
+        _tags = {"requires_fit_before_transform": True}
+
     with pytest.raises(ValueError):
-        BaseEffect().transform(pd.DataFrame(), fh=pd.Index([]))
+        EffectMustFit().transform(pd.DataFrame(), fh=pd.Index([]))