Repository: sktime/sktime

Issue:
[BUG] `ConformalIntervals` update does not update residuals_matrix
**Describe the bug**

`ConformalIntervals` update only updates the forecaster, not the residuals_matrix for predicting intervals or quantiles.


**To Reproduce**

`_update` is defined as:

```
    def _update(self, y, X=None, update_params=True):
        self.forecaster_.update(y, X, update_params=update_params)
        return self
```

which is only updating the forecaster and no the residuals_matrix. Since the forecaster is already fitted when `update` is called, the residuals_matrix is set to the fitted one with:

```
  if self.fh_early_:
      residuals_matrix = self.residuals_matrix_
```

**Expected behavior**

The residuals_matrix should be updated with the new data.


<!-- Thanks for contributing! -->



Discussion:

Resolving patch:
diff --git a/sktime/forecasting/conformal.py b/sktime/forecasting/conformal.py
--- a/sktime/forecasting/conformal.py
+++ b/sktime/forecasting/conformal.py
@@ -107,7 +107,7 @@ def __init__(
         self,
         forecaster,
         method="empirical",
-        initial_window=1,
+        initial_window=None,
         sample_frac=None,
         verbose=False,
         n_jobs=None,
@@ -162,7 +162,16 @@ def _predict(self, fh, X=None):
 
     def _update(self, y, X=None, update_params=True):
         self.forecaster_.update(y, X, update_params=update_params)
-        return self
+
+        if update_params and len(y.index.difference(self.residuals_matrix_.index)) > 2:
+            self.residuals_matrix_ = self._compute_sliding_residuals(
+                y,
+                X,
+                self.forecaster_,
+                self.initial_window,
+                self.sample_frac,
+                update=True,
+            )
 
     def _predict_interval(self, fh, X=None, coverage=None):
         """Compute/return prediction quantiles for a forecast.
@@ -289,7 +298,12 @@ def _parse_initial_window(self, y, initial_window=None):
         n_samples = len(y)
 
         if initial_window is None:
-            initial_window = max(10, 0.1 * n_samples)
+            if int(floor(0.1 * n_samples)) > 10:
+                initial_window = int(floor(0.1 * n_samples))
+            elif n_samples > 10:
+                initial_window = 10
+            else:
+                initial_window = n_samples - 1
 
         initial_window_type = np.asarray(initial_window).dtype.kind
 
@@ -317,7 +331,9 @@ def _parse_initial_window(self, y, initial_window=None):
 
         return n_initial_window
 
-    def _compute_sliding_residuals(self, y, X, forecaster, initial_window, sample_frac):
+    def _compute_sliding_residuals(
+        self, y, X, forecaster, initial_window, sample_frac, update=False
+    ):
         """Compute sliding residuals used in uncertainty estimates.
 
         Parameters
@@ -336,9 +352,12 @@ def _compute_sliding_residuals(self, y, X, forecaster, initial_window, sample_fr
             initial window.
             If None, the value is set to the larger of 0.1*len(y) and 10
         sample_frac : float
-            For speeding up computing of residuals matrix.
+            for speeding up computing of residuals matrix.
             sample value in range (0, 1) to obtain a fraction of y indices to
             compute residuals matrix for
+        update : bool
+            Whether residuals_matrix has been calculated previously and just
+            needs extending. Default = False
         Returns
         -------
         residuals_matrix : pd.DataFrame, row and column index = y.index[initial_window:]
@@ -351,12 +370,26 @@ def _compute_sliding_residuals(self, y, X, forecaster, initial_window, sample_fr
 
         n_initial_window = self._parse_initial_window(y, initial_window=initial_window)
 
-        y_index = y.iloc[n_initial_window:].index
+        full_y_index = y.iloc[n_initial_window:].index
 
-        residuals_matrix = pd.DataFrame(columns=y_index, index=y_index, dtype="float")
+        residuals_matrix = pd.DataFrame(
+            columns=full_y_index, index=full_y_index, dtype="float"
+        )
+
+        if update and hasattr(self, "residuals_matrix_"):
+            remaining_y_index = full_y_index.difference(self.residuals_matrix_.index)
+            if len(remaining_y_index) != len(full_y_index):
+                residuals_matrix.loc[
+                    self.residuals_matrix_.index, self.residuals_matrix_.columns
+                ] = self.residuals_matrix_
+            y_index = remaining_y_index
+        else:
+            y_index = full_y_index
 
         if sample_frac:
-            y_index = y_index.to_series().sample(frac=sample_frac)
+            y_sample = y_index.to_series().sample(frac=sample_frac)
+            if len(y_sample) > 2:
+                y_index = y_sample
 
         def _get_residuals_matrix_row(forecaster, y, X, id):
             y_train = get_slice(y, start=None, end=id)  # subset on which we fit
@@ -364,7 +397,6 @@ def _get_residuals_matrix_row(forecaster, y, X, id):
 
             X_train = get_slice(X, start=None, end=id)
             X_test = get_slice(X, start=id, end=None)
-
             forecaster.fit(y_train, X=X_train, fh=y_test.index)
 
             try:
@@ -380,7 +412,6 @@ def _get_residuals_matrix_row(forecaster, y, X, id):
             delayed(_get_residuals_matrix_row)(forecaster.clone(), y, X, id)
             for id in y_index
         )
-
         for idx, id in enumerate(y_index):
             residuals_matrix.loc[id] = all_residuals[idx]
 


Test patch: