Repository: sktime/sktime (python)

Issue:
[BUG] Aggregator's inverse_transform does not invert the transformation
**Describe the bug**
<!--
A clear and concise description of what the bug is.
-->
Aggregator's inverse_transform method does nothing, but an _inverse_transform is implemented.

**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
-->

```python
from sktime.transformations.hierarchical.aggregate import Aggregator
from sktime.utils._testing.hierarchical import _bottom_hier_datagen

aggregator = Aggregator()

X = _bottom_hier_datagen(
    no_bottom_nodes=3,
    no_levels=1,
)

X_transform = aggregator.fit_transform(X)
X_inv_transform = aggregator.inverse_transform(X_transform)

print("__total" in X_transform.index.get_level_values(0))
print("__total" in X_inv_transform.index.get_level_values(0))
```

which outputs
```
True
True
```

**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
I expect inverse_transform to remove the special tag "__total" from the 1st index level.


**Additional context**
<!--
Add any other context about the problem here.
-->
The removal of the special tag "__total" from the 1st index level is what happens when I run 

```python
for i in range(X_inv_transform.index.nlevels - 1):
    X_inv_transform = X_inv_transform.drop(index="__total", level=i)
```
as seen in `_inverse_transform` https://github.com/sktime/sktime/blob/9ba65b6f1b983a7bb167d14738af16f4ea90615b/sktime/transformations/hierarchical/aggregate.py#L187


**Versions**
<details>
System:
    python: 3.10.14 | packaged by conda-forge | (main, Mar 20 2024, 12:40:08) [MSC v.1938 64 bit (AMD64)]
executable: [c:\Users\xxx.xxx\Miniforge3\envs\xxx\python.exe](file:///C:/Users/xxx.xxx/Miniforge3/envs/xxx/python.exe)
   machine: Windows-10-10.0.22631-SP0

Python dependencies:
          pip: 24.1.2
       sktime: 0.34.0
      sklearn: 1.5.1
       skbase: 0.8.1
        numpy: 1.26.4
        scipy: 1.14.0
       pandas: 2.1.4
   matplotlib: 3.9.2
       joblib: 1.4.2
        numba: None
  statsmodels: 0.14.2
     pmdarima: 2.0.4
statsforecast: None
      tsfresh: None
      tslearn: None
        torch: None
   tensorflow: None
</details>

<!-- Thanks for contributing! -->


Pull request:
[BUG] Fix Aggregator inverse_transform being skipped
#### Reference Issues/PRs

Fixes #7614 

#### What does this implement/fix? Explain your changes.

* Add `__init__` argument to activate `inverse_transform`, for backward compatibility
* Fix the inverse transform so that it does not fail when a given level does not have aggregated value


#### What should a reviewer concentrate their feedback on?

Is the name of the argument good?

#### Did you add any tests for the change?
Yes

#### Any other comments?

This PR should be merged before #7697 

Patch:
diff --git a/sktime/transformations/hierarchical/aggregate.py b/sktime/transformations/hierarchical/aggregate.py
index aa7097075..95b955be5 100644
--- a/sktime/transformations/hierarchical/aggregate.py
+++ b/sktime/transformations/hierarchical/aggregate.py
@@ -25,6 +25,10 @@ class Aggregator(BaseTransformer):
     flatten_single_level : boolean (default=True)
         Remove aggregate nodes, i.e. ("__total"), where there is only a single
         child to the level
+    bypass_inverse_transform : boolean (default=True)
+        If True, the inverse_transform method is skipped. If False, the
+        inverse_transform method is implemented and can be used to remove
+        aggregate levels from the data.
 
     See Also
     --------
@@ -76,11 +80,21 @@ class Aggregator(BaseTransformer):
         "transform-returns-same-time-index": False,
     }
 
-    def __init__(self, flatten_single_levels=True):
+    def __init__(self, flatten_single_levels=True, bypass_inverse_transform=True):
         self.flatten_single_levels = flatten_single_levels
+        self.bypass_inverse_transform = bypass_inverse_transform
 
         super().__init__()
 
+        if not self.bypass_inverse_transform:
+            self.set_tags(
+                **{
+                    "skip-inverse-transform": False,
+                    "capability:inverse_transform": True,
+                    "capability:inverse_transform:exact": False,
+                }
+            )
+
     def _transform(self, X, y=None):
         """Transform X and return a transformed version.
 
@@ -185,7 +199,7 @@ class Aggregator(BaseTransformer):
             )
         else:
             for i in range(X.index.nlevels - 1):
-                X = X.drop(index="__total", level=i)
+                X = X.drop(index="__total", level=i, errors="ignore")
         return X
 
     @classmethod


Test patch:
diff --git a/sktime/transformations/hierarchical/tests/test_aggregate.py b/sktime/transformations/hierarchical/tests/test_aggregate.py
index ba963c657..5e76720ae 100644
--- a/sktime/transformations/hierarchical/tests/test_aggregate.py
+++ b/sktime/transformations/hierarchical/tests/test_aggregate.py
@@ -6,6 +6,7 @@ __author__ = ["ciaran-g"]
 
 import pytest
 
+from sktime.datatypes import get_examples
 from sktime.tests.test_switch import run_test_for_class
 from sktime.transformations.hierarchical.aggregate import Aggregator
 from sktime.utils._testing.hierarchical import _bottom_hier_datagen
@@ -79,3 +80,32 @@ def test_aggregator_flatten():
         "with the time index removed, for random_seed=111."
     )
     assert len(X_agg_flat.droplevel(-1).index.unique()) == 17, msg
+
+
+@pytest.mark.skipif(
+    not run_test_for_class(Aggregator),
+    reason="run test only if softdeps are present and incrementally (if requested)",
+)
+def test_aggregator_inverse_transform():
+    """Tests inverse_transform of aggregator function.
+
+    This test asserts that the output of Aggregator using inverse_transform() is equal
+    to the original input data.
+    """
+
+    y = get_examples("pd_multiindex_hier")[1]
+
+    agg_bypass_invt = Aggregator(bypass_inverse_transform=True)
+    agg = Aggregator(bypass_inverse_transform=False)
+
+    yt_bypass_invt = agg_bypass_invt.fit_transform(y)
+    yt = agg.fit_transform(y)
+
+    # inverse transform with bypass
+    yinvt_bypass_invt = agg_bypass_invt.inverse_transform(yt_bypass_invt)
+    yinvt = agg.inverse_transform(yt)
+
+    # When bypassing, totals are kept in the output.
+    ## When not bypassing, we should find the same indexes as before
+    assert yinvt_bypass_invt.index.droplevel(-1).nunique() == 9
+    assert yinvt.index.droplevel(-1).nunique() == y.index.droplevel(-1).nunique()