diff --git a/datasieve/pipeline.py b/datasieve/pipeline.py index 06e3a53..b9ba57c 100644 --- a/datasieve/pipeline.py +++ b/datasieve/pipeline.py @@ -23,6 +23,7 @@ def __init__(self, steps: List[Tuple] = [], self.fitparams: Dict[str, dict] = self._validate_fitparams(fitparams, steps) self.pandas_types: bool = False self.feature_list: list = [] + self.features_in: list = [] self.label_list: list = [] self.step_strings: list = [] @@ -164,7 +165,14 @@ def _validate_arguments(self, X, y, sample_weight, fit=False, outlier_check=Fals else: self.label_list = [0] elif isinstance(X, pd.DataFrame) and not fit: - if list(X.columns) != list(self.features_in): + # If features_in is empty (pipeline loaded from cached state or + # never fit'd with DataFrame), populate from first call as a + # graceful fallback. Otherwise, validate. + if not list(self.features_in): + self.features_in = X.columns + self.feature_list = X.columns + self.pandas_types = True + elif list(X.columns) != list(self.features_in): raise Exception(f"Pipeline expected {self.features_in} but got {X.columns}.") elif not isinstance(X, pd.DataFrame) and not fit and self.pandas_types: X = pd.DataFrame(X, columns=self.features_in) diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index e5c90f4..5219a4d 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -103,3 +103,37 @@ def test_getitem(dummy_array_without_nans, dummy_array2_without_nans): di_values = pipeline["di"].di_values assert di_values.shape[0] == 100 + + +def test_pipeline_features_in_initialized_empty(dummy_df_without_nans): + """ + Pipeline.features_in must be initialized as empty list (not undefined). + Regression test for issue #13167 (freqtrade): predict before fit raised + AttributeError because features_in did not exist. + """ + pipeline = Pipeline([]) + # Should not raise AttributeError + assert pipeline.features_in == [] + assert pipeline.feature_list == [] + + +def test_pipeline_transform_without_prior_fit_graceful(dummy_df_without_nans): + """ + If pipeline.transform() is called and features_in is empty (e.g. pipeline + loaded from cached state where attribute didn't persist), the validation + should gracefully populate from the first call instead of raising. + + Regression test for issue #13167 (freqtrade). + """ + import datasieve.transforms as ts + pipeline = Pipeline([("detect_constants", ts.VarianceThreshold(threshold=0))]) + + df = dummy_df_without_nans.copy() + Xdf, ydf = extract_features_and_labels(df) + + # Simulate "pipeline loaded fresh" state (features_in empty) + assert pipeline.features_in == [] + + # transform should populate features_in instead of raising + Xdf_t, ydf_t, _ = pipeline.transform(Xdf, ydf) + assert list(pipeline.features_in) == list(Xdf.columns)