Repository: sktime/sktime
Pull request: [ENH] fix loss of time zone attribute in `ForecastingHorizon.to_absolute`
Fixes https://github.com/sktime/sktime/issues/5584.

`tz` information can be lost in an intermediate step where the index is coerced to period.
The fix is to restore it later, in this case.

Problem statement:
[BUG] `ForecastingHorizon.to_absolute` is loosing tz information from `cutoff`
**Describe the bug**
The `to_absolute` method from `ForecastingHorizon` does not keep the `tz` information from `cutoff`.

**To Reproduce**

```python
cutoff = pd.Timestamp("2020-01-01", tz="utc")
fh_absolute = ForecastingHorizon(range(100), freq="h").to_absolute(cutoff)
print(fh_absolute[0].tz, cutoff.tz)
# Output: None, UTC
```

**Expected behavior**
The time zone information is kept

Hints from issue discussion:
this may be a previous instance, which might contain a fix in the instance of a single forecaster:

https://github.com/sktime/sktime/pull/5464
I am just adding a link to this relevant resource:
https://xkcd.com/2867/

Commit message: fix fh

Patch to review:
diff --git a/sktime/forecasting/base/_fh.py b/sktime/forecasting/base/_fh.py
index 01cbfca4374..141b5fcebe9 100644
--- a/sktime/forecasting/base/_fh.py
+++ b/sktime/forecasting/base/_fh.py
@@ -870,6 +870,12 @@ def _to_absolute(fh: ForecastingHorizon, cutoff) -> ForecastingHorizon:
         _check_cutoff(cutoff, relative)
         is_timestamp = isinstance(cutoff, pd.DatetimeIndex)
 
+        # remembert timezone to restore it later
+        if hasattr(cutoff, "tz"):
+            old_tz = cutoff.tz
+        else:
+            old_tz = None
+
         if is_timestamp:
             # coerce to pd.Period for reliable arithmetic operations and
             # computations of time deltas
@@ -884,6 +890,9 @@ def _to_absolute(fh: ForecastingHorizon, cutoff) -> ForecastingHorizon:
             # coerce back to DatetimeIndex after operation
             absolute = absolute.to_timestamp(fh.freq)
 
+        if old_tz is not None:
+            absolute = absolute.tz_localize(old_tz)
+
         return fh._new(absolute, is_relative=False, freq=fh.freq)
 
 
diff --git a/sktime/forecasting/base/tests/test_fh.py b/sktime/forecasting/base/tests/test_fh.py
index 5e5ddd9ef31..c2e1d8adef7 100644
--- a/sktime/forecasting/base/tests/test_fh.py
+++ b/sktime/forecasting/base/tests/test_fh.py
@@ -774,3 +774,14 @@ def test_fh_expected_pred():
         [("a", 3, 6), ("a", 3, 7), ("b", 5, 7), ("a", 3, 8), ("b", 5, 8), ("b", 5, 9)]
     )
     assert y_pred_idx.equals(y_pred_idx_expected)
+
+
+def test_tz_preserved():
+    """Test that time zone information is preserved in to_absolute.
+
+    Failure case in issue #5584.
+    """
+    cutoff = pd.Timestamp("2020-01-01", tz="utc")
+    fh_absolute = ForecastingHorizon(range(100), freq="h").to_absolute(cutoff)
+
+    assert fh_absolute[0].tz == cutoff.tz


Code review comments:
diff_hunk: @@ -870,6 +870,12 @@ def _to_absolute(fh: ForecastingHorizon, cutoff) -> ForecastingHorizon:
         _check_cutoff(cutoff, relative)
         is_timestamp = isinstance(cutoff, pd.DatetimeIndex)
 
+        # remembert timezone to restore it later
path: sktime/forecasting/base/_fh.py
text: @user1:
There is a small typo in remembert

@author:
no, this is the friend of rememernie