Repository: scikit-activeml/scikit-activeml

Issue:
BUG: Mean criterion kernel for ParzenWindowclassifier does not update after first fit
### Describe the issue:

The `gamma` argument is replaced in self.metric_dict, which causes the mean kernel to not adapt to new unlabeled data when fit is used again.

### Reproduce the bug via a code example:

```python
from sklearn.datasets import make_classification
from skactiveml.classifier import ParzenWindowClassifier
X, y_true = make_classification(n_features=2, n_redundant=0, random_state=0)
bound = [[min(X[:, 0]), min(X[:, 1])], [max(X[:, 0]), max(X[:, 1])]]
clf = ParzenWindowClassifier(metric_dict={'gamma':'mean'})
print(clf.metric_dict)
clf.fit(X, y_true)
print(clf.metric_dict)
```

### Error message:

```shell
{'gamma': 'mean'}
{'gamma': np.float64(2.1974501703775795)}
```

### Context for the issue:

_No response_

Hints from issue comments:

Solution patch:
diff --git a/skactiveml/classifier/_parzen_window_classifier.py b/skactiveml/classifier/_parzen_window_classifier.py
index b1db90c..f6128d2 100644
--- a/skactiveml/classifier/_parzen_window_classifier.py
+++ b/skactiveml/classifier/_parzen_window_classifier.py
@@ -18,6 +18,8 @@ from ..utils import (
     check_n_features,
 )
 
+from copy import deepcopy
+
 
 class ParzenWindowClassifier(ClassFrequencyEstimator):
     """Parzen Window Classifier (PWC)
@@ -151,7 +153,7 @@ class ParzenWindowClassifier(ClassFrequencyEstimator):
 
         # Ensure that metric_dict is a Python dictionary.
         self.metric_dict_ = (
-            self.metric_dict if self.metric_dict is not None else {}
+            deepcopy(self.metric_dict) if self.metric_dict is not None else {}
         )
         if (
             "gamma" in self.metric_dict_
diff --git a/skactiveml/pool/_regression_tree_based_al.py b/skactiveml/pool/_regression_tree_based_al.py
index b9a8f52..6acd8fd 100644
--- a/skactiveml/pool/_regression_tree_based_al.py
+++ b/skactiveml/pool/_regression_tree_based_al.py
@@ -1,6 +1,8 @@
 import warnings
 
 import numpy as np
+
+from copy import copy
 from sklearn import clone
 from sklearn.cluster import KMeans
 from sklearn.metrics import (
@@ -287,7 +289,7 @@ class RegressionTreeBasedAL(SingleAnnotatorPoolQueryStrategy):
 
             batch_utilities_cand = np.full((batch_size, len(X_cand)), -np.inf)
             for i in range(self.max_iter_representativity):
-                prev_best_indices = query_indices
+                prev_best_indices = copy(query_indices)
                 for l_idx in range(batch_size):
                     # Update DELTA using the current centroids.
                     X_M = X[labeled_idxs]


Test patch:
diff --git a/skactiveml/classifier/tests/test_parzen_window_classifier.py b/skactiveml/classifier/tests/test_parzen_window_classifier.py
index e2dbd18..b349634 100644
--- a/skactiveml/classifier/tests/test_parzen_window_classifier.py
+++ b/skactiveml/classifier/tests/test_parzen_window_classifier.py
@@ -5,6 +5,7 @@ from sklearn.utils.validation import NotFittedError
 from skactiveml.tests.template_estimator import TemplateClassFrequencyEstimator
 
 from skactiveml.classifier import ParzenWindowClassifier
+from copy import deepcopy
 
 
 class TestParzenWindowClassifier(
@@ -80,6 +81,17 @@ class TestParzenWindowClassifier(
         np.testing.assert_array_equal(
             [[0, 0, 2], [0, 0, 0], [0, 1, 0]], pwc.V_
         )
+        # check that metric_dict is not changed after fit
+        pwc = ParzenWindowClassifier(
+            classes=["tokyo", "paris", "new york"],
+            missing_label="nan",
+            metric="rbf",
+            metric_dict={"gamma": "mean"},
+        )
+        before_metric_dict = deepcopy(pwc.metric_dict)
+        pwc.fit(X=self.fit_default_params["X"], y=self.y_nan)
+        after_metric_dict = deepcopy(pwc.metric_dict)
+        self.assertEqual(before_metric_dict, after_metric_dict)
 
     def test_predict_freq(self):
         pwc = ParzenWindowClassifier(
diff --git a/skactiveml/pool/tests/test_regression_tree_based_al.py b/skactiveml/pool/tests/test_regression_tree_based_al.py
index dc46f4f..fdd0d5d 100644
--- a/skactiveml/pool/tests/test_regression_tree_based_al.py
+++ b/skactiveml/pool/tests/test_regression_tree_based_al.py
@@ -2,6 +2,7 @@ import unittest
 
 import numpy as np
 from sklearn.metrics import pairwise_distances_argmin
+from sklearn.datasets import make_regression
 
 from skactiveml.pool import RegressionTreeBasedAL
 from skactiveml.pool._regression_tree_based_al import (
@@ -175,6 +176,24 @@ class TestRegressionTreeBasedAL(
                 )
                 self.assertTrue((np.isnan(utilities) + (utilities == 1)).all())
 
+        # reg = SklearnRegressor(DecisionTreeRegressor(), missing_label=np.nan)
+        X, y = make_regression(n_samples=500, n_features=100, random_state=0)
+        y[100:] = np.nan
+        query_indices = []
+        for i in [1, 10]:
+            qs = RegressionTreeBasedAL(
+                method="representativity",
+                max_iter_representativity=i,
+                random_state=0,
+            )
+            query_indices.append(qs.query(X, y, reg=self.reg, batch_size=5))
+        np.testing.assert_raises(
+            AssertionError,
+            np.testing.assert_array_equal,
+            query_indices[0],
+            query_indices[1],
+        )
+
 
 class _DummyRegressor(DecisionTreeRegressor):
     centers = np.array([1, 11, 21]).reshape(-1, 1)