Repository: sktime/sktime

Issue:
[BUG] missing `forecaster_X_` attribute in `ForecastX` estimator in case of multivariate target
Based on [documenation](https://www.sktime.net/en/stable/api_reference/auto_generated/sktime.forecasting.compose.ForecastX.html), `forecaster_X_` should be available whenever `behaviour="update"`, which is the default behaviour. It seems that is only true when `y` is univariate.

## Reproduible Example

```python3
import pandas
from sktime.forecasting.compose import ForecastX
from sktime.forecasting.naive import NaiveForecaster

df = pandas.DataFrame(
    {
        "a": ["series_1", "series_1", "series_1"],
        "b": pandas.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]),
        "c": [1, 2, 3],
        "d": [4, 5, 6],
        "e": [7, 8, 9],
    }
)
df = df.set_index(["a", "b"])

model = ForecastX(NaiveForecaster(), NaiveForecaster())

model_1 = model.clone()
model_1.fit(df[["c"]], X=df[["d", "e"]], fh=[1, 2, 3])
assert hasattr(model_1, "forecaster_X_")

model_2 = model.clone()
model_2.fit(df[["c", "d"]], X=df[["e"]], fh=[1, 2, 3])
assert not hasattr(model_2, "forecaster_X_")
```

The difference in `model_1` and `model_2` is only that the former is trained with univariate `y`, and the latter is trained with multivariate. Based on the two `assert` behaviours, the `forecaster_X_` attribute is present for `model_1`, but missing for `model_2`.

## Versions

python 3.9.16, sktime 0.19.1, macOS/Ubuntu

## Context

I was trying to get the `X` predictions which are used to eventually predict for `y`. Since there seemed to be no other public method, I was using this attribute to predict with same `fh` passed for `y` predictions. This was working for univariate target, but multivariate started to fail because of the above issue.


Discussion:

Resolving patch:
diff --git a/sktime/forecasting/compose/_pipeline.py b/sktime/forecasting/compose/_pipeline.py
--- a/sktime/forecasting/compose/_pipeline.py
+++ b/sktime/forecasting/compose/_pipeline.py
@@ -1229,6 +1229,7 @@ class ForecastX(BaseForecaster):
     _tags = {
         "X_inner_mtype": SUPPORTED_MTYPES,
         "y_inner_mtype": SUPPORTED_MTYPES,
+        "scitype:y": "both",
         "X-y-must-have-same-index": False,
         "fit_is_empty": False,
         "ignores-exogeneous-X": False,


Test patch:
diff --git a/sktime/forecasting/compose/tests/test_pipeline.py b/sktime/forecasting/compose/tests/test_pipeline.py
--- a/sktime/forecasting/compose/tests/test_pipeline.py
+++ b/sktime/forecasting/compose/tests/test_pipeline.py
@@ -480,3 +480,45 @@ def test_forecastx_logic():
 
     # compare that test and comparison case results are equal
     assert np.allclose(y_pred, y_pred_manual)
+
+
+def test_forecastx_attrib_broadcast():
+    """Test ForecastX broadcasting and forecaster attributes."""
+    from sktime.forecasting.compose import ForecastX
+    from sktime.forecasting.naive import NaiveForecaster
+
+    df = pd.DataFrame(
+        {
+            "a": ["series_1", "series_1", "series_1"],
+            "b": pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]),
+            "c": [1, 2, 3],
+            "d": [4, 5, 6],
+            "e": [7, 8, 9],
+        }
+    )
+    df = df.set_index(["a", "b"])
+
+    model = ForecastX(NaiveForecaster(), NaiveForecaster())
+
+    model_1 = model.clone()
+    model_1.fit(df[["c"]], X=df[["d", "e"]], fh=[1, 2, 3])
+
+    assert hasattr(model_1, "forecaster_X_")
+    assert isinstance(model_1.forecaster_X_, NaiveForecaster)
+    assert model_1.forecaster_X_.is_fitted
+
+    assert hasattr(model_1, "forecaster_y_")
+    assert isinstance(model_1.forecaster_y_, NaiveForecaster)
+    assert model_1.forecaster_y_.is_fitted
+
+    model_2 = model.clone()
+    model_2.fit(df[["c", "d"]], X=df[["e"]], fh=[1, 2, 3])
+    assert hasattr(model_2, "forecaster_X_")
+
+    assert hasattr(model_2, "forecaster_X_")
+    assert isinstance(model_2.forecaster_X_, NaiveForecaster)
+    assert model_2.forecaster_X_.is_fitted
+
+    assert hasattr(model_2, "forecaster_y_")
+    assert isinstance(model_2.forecaster_y_, NaiveForecaster)
+    assert model_2.forecaster_y_.is_fitted