Repository: GAA-UAM/scikit-fda

Issue:
Add `root_mean_squared_error` and `root_mean_squared_log_error`
### Motivation

The functions `root_mean_squared_error` and `root_mean_squared_log_error` were added in scikit-learn 1.4 and the `squared` parameter was deprecated in the functions `mean_squared_error` and `mean_squared_log_error`, and removed in version 1.6.

We need to add a `FData`-aware version of these functions and deprecate that parameter.

### Desired functionality

- Add `FData`-aware functions `root_mean_squared_error` and `root_mean_squared_log_error` to the `scoring` module.
- Deprecate `squared` parameter in functions `mean_squared_error` and `mean_squared_log_error`.

### Alternatives

_No response_

### Additional context

_No response_

Hints from issue comments:
@rosignol08: Hey I'm a CS student and I wonder if I could work on this issue ?@vnmabus: Sure. I will assign you this issue. If you are no longer interested just tell me, so I can de-assign you.@rosignol08: Thank you I will try my best.@rosignol08: Hello, I'm working on the project now and I have a few questions to ask. First, you said that we have to deprecate the function, so should I just add a warning like in Scikit-learn version 1.4, or should I remove it like in Scikit-learn version 1.6? And for the `root_mean_squared_error` and `root_mean_squared_log_error,` do I need to copy the Scikit-learn 1.6 version of the function and make it `FData`-aware by adapting it to be compatible with the FDA types?@vnmabus: > First, you said that we have to deprecate the function, so should I just add a warning like in Scikit-learn version 1.4, or should I remove it like in Scikit-learn version 1.6?

A warning should be added, as well as a deprecation note to the parameter in the docstring.

> And for the `root_mean_squared_error` and `root_mean_squared_log_error,` do I need to copy the Scikit-learn 1.6 version of the function and make it `FData`-aware by adapting it to be compatible with the FDA types?

That would be a possible approach. But if the original functions with `squared=False` do the same (I am not sure, we would have to check), then maybe it is possible to do whatever those calls do now, without having to "translate" the scikit-learn functions again. It is possible that this requires encapsulating some code in "private" functions, so that it can be shared between the old and new functions.@rosignol08: Sorry for the delay I have a lot of work to do but I managed to add the depreciation parameter in the doc file `scoring.rst` via thoses line : ` .. warning::

    The `squared` parameter in `mean_squared_error` and `mean_squared_log_error`
    is deprecated and will be removed in a future version. Use
    `root_mean_squared_error` and `root_mean_squared_log_error` instead.
 and about the `root_mean_squared_error` and `root_mean_squared_log_error` I have to create them in the `scroring.py` file ?@vnmabus: Yes, they should be in `scoring.py`.

> Sorry for the delay

I think you are new to open source development, so just to clarify: there is no delay. As long as nobody pays you for it, you do not "owe" anything to anyone, nor nobody can demand any kind of deadline or compromise (or even that you finish the work at all!). You are doing it on your own volition, and you can take the time you need. Of course, this works both ways: if the maintainer thinks that the work would be better done by other person, or not done at all, you are not entitled to have your work merged or released (but you can always fork your own version).

In this case, I am happy of you contributing and I don't mind that it takes time, as it is not urgent.@rosignol08: Thank you very much for your kindness, I will continue to work on this as soon as I have the time to.@rosignol08: Hello I have added the function modified documentation accordingly. have committed the changes to my fork. I create a merge request for you to review, or would you prefer to check my fork directly? Thank.@vnmabus: Please create a pull request.@rosignol08: Done: #649 @vnmabus: Closed in #649.

Solution patch:
diff --git a/docs/modules/misc/scoring.rst b/docs/modules/misc/scoring.rst
index b1b5e2fd..4c86047e 100644
--- a/docs/modules/misc/scoring.rst
+++ b/docs/modules/misc/scoring.rst
@@ -16,3 +16,11 @@ Only scores that support multioutput are included.
      skfda.misc.scoring.mean_squared_error
      skfda.misc.scoring.mean_squared_log_error
      skfda.misc.scoring.r2_score
+     skfda.misc.scoring.root_mean_squared_error
+     skfda.misc.scoring.root_mean_squared_log_error
+
+.. warning::
+
+    The `squared` parameter in `mean_squared_error` and `mean_squared_log_error`
+    is deprecated and will be removed in a future version. Use
+    `root_mean_squared_error` and `root_mean_squared_log_error` instead.
diff --git a/skfda/misc/scoring.py b/skfda/misc/scoring.py
index 706440e4..c0f59b70 100644
--- a/skfda/misc/scoring.py
+++ b/skfda/misc/scoring.py
@@ -731,6 +731,8 @@ def mean_squared_error(
             are taken.
         multioutput: Defines format of the return.
         squared: If True returns MSE value, if False returns RMSE value.
+            Deprecated since version 1.X: The 'squared' parameter will be removed in a future version.
+            Use `root_mean_squared_error` instead if `squared=False` was used.
 
     Returns:
         Mean squared error.
@@ -750,6 +752,13 @@ def mean_squared_error(
         multioutput = 'raw_values', ndarray.
 
     """
+    if not squared:
+        warnings.warn(
+            "The 'squared' parameter is deprecated and will be removed in a future version. "
+            "Use `root_mean_squared_error` instead.",
+            FutureWarning,
+            stacklevel=2,
+        )
     function = (
         sklearn.metrics.mean_squared_error
         if squared
@@ -826,6 +835,116 @@ def _mean_squared_error_fdatabasis(
 
     return _multioutput_score_basis(y_true, multioutput, _mse_func)
 
+@overload
+def root_mean_squared_error(
+    y_true: np.ndarray,
+    y_pred: np.ndarray,
+    *,
+    sample_weight: np.ndarray | None = ...,
+    multioutput: Literal['uniform_average'] = ...,
+) -> float:
+    pass  # noqa: WPS428
+
+@overload
+def root_mean_squared_error(
+    y_true: np.ndarray,
+    y_pred: np.ndarray,
+    *,
+    sample_weight: np.ndarray | None = ...,
+    multioutput: Literal['raw_values'],
+) -> np.ndarray:
+    pass  # noqa: WPS428
+
+@overload
+def root_mean_squared_error(
+    y_true: FData,
+    y_pred: FData,
+    *,
+    sample_weight: np.ndarray | None = ...,
+    multioutput: Literal['uniform_average'] = ...,
+) -> float:
+    pass  # noqa: WPS428
+
+@overload
+def root_mean_squared_error(
+    y_true: FData,
+    y_pred: FData,
+    *,
+    sample_weight: np.ndarray | None = ...,
+    multioutput: Literal['raw_values'],
+) -> FData:
+    pass  # noqa: WPS428
+
+@singledispatch
+def root_mean_squared_error(
+    y_true: DataType,
+    y_pred: DataType,
+    *,
+    sample_weight: NDArrayFloat | None = None,
+    multioutput: MultiOutputType = "uniform_average",
+) -> float | DataType:
+    r"""Root Mean Squared Error for :class:`~skfda.representation.FData`.
+    With :math:`y\_true = (X_1, X_2, ..., X_n)` being the real values,
+    :math:`t\_pred = (\hat{X}_1, \hat{X}_2, ..., \hat{X}_n)` being the
+    estimated and :math:`sample\_weight = (w_1, w_2, ..., w_n)`, the error is
+    calculated as
+    
+    .. math::
+        RMSE(y\_true, y\_pred)(t) = \sqrt{\frac{1}{\sum w_i}
+        \sum_{i=1}^n w_i(X_i(t) - \hat{X}_i(t))^2}
+    
+    This is the square root of MSE (Mean Squared Error).
+    The values of test y_true = [3, -0.5, 2, 7], y_pred = [2.5, 0.0, 2, 8] come from
+    https://scikit-learn.org/stable/modules/generated/sklearn.metrics.root_mean_squared_error.html
+
+    Args:
+        y_true: Correct target values.
+        y_pred: Estimated values.
+        sample_weight: Sample weights. By default, uniform weights
+            are taken.
+        multioutput: Defines format of the return.
+    
+    Returns:
+        Root mean squared error.
+    """
+    # Call sklearn's implementation for numpy arrays
+    if isinstance(y_true, np.ndarray) and isinstance(y_pred, np.ndarray):
+        return sklearn.metrics.root_mean_squared_error(
+            y_true,
+            y_pred,
+            sample_weight=sample_weight,
+            multioutput=multioutput,
+        )
+    
+    # For FData objects, I use our own implementation without any warnings
+    return mean_squared_error(
+        y_true,
+        y_pred,
+        sample_weight=sample_weight,
+        multioutput=multioutput,
+        squared=False,
+    )
+
+@root_mean_squared_error.register
+def _(y_true: np.ndarray, y_pred: np.ndarray, *, sample_weight=None, multioutput: MultiOutputType = "uniform_average"):
+    """For the NumPy array."""
+    try:
+        #We try to use the function if available in scikit-learn
+        return sklearn.metrics.root_mean_squared_error(
+            y_true, y_pred, sample_weight=sample_weight, multioutput=multioutput
+        )
+    except AttributeError:
+        # Fallback for older scikit-learn versions
+        return np.sqrt(sklearn.metrics.mean_squared_error(
+            y_true, y_pred, sample_weight=sample_weight, multioutput=multioutput
+        ))
+
+@root_mean_squared_error.register
+def _(y_true: FData, y_pred: FData, *, sample_weight=None, multioutput="uniform_average"):
+    """For FData."""
+    mse = mean_squared_error(y_true, y_pred, sample_weight=sample_weight, multioutput=multioutput,squared=False)
+    return mse 
+
 
 @overload
 def mean_squared_log_error(
@@ -924,6 +1043,13 @@ def mean_squared_log_error(
         multioutput = 'raw_values', ndarray.
 
     """
+    if not squared:
+        warnings.warn(
+            "The 'squared' parameter is deprecated and will be removed in a future version. "
+            "Use `root_mean_squared_log_error` instead.",
+            FutureWarning,
+            stacklevel=2,
+        )
     function = (
         sklearn.metrics.mean_squared_log_error
         if squared
@@ -1025,6 +1151,144 @@ def _mean_squared_log_error_fdatabasis(
 
     return _multioutput_score_basis(y_true, multioutput, _msle_func)
 
+@overload
+def root_mean_squared_log_error(
+    y_true: DataType,
+    y_pred: DataType,
+    *,
+    sample_weight: NDArrayFloat | None = ...,
+    multioutput: Literal['uniform_average'] = ...,
+) -> float:
+    pass  # noqa: WPS428
+
+
+@overload
+def root_mean_squared_log_error(
+    y_true: DataType,
+    y_pred: DataType,
+    *,
+    sample_weight: NDArrayFloat | None = ...,
+    multioutput: Literal['raw_values'],
+) -> DataType:
+    pass  # noqa: WPS428
+
+
+@singledispatch
+def root_mean_squared_log_error(
+    y_true: DataType,
+    y_pred: DataType,
+    *,
+    sample_weight: NDArrayFloat | None = None,
+    multioutput: MultiOutputType = 'uniform_average',
+) -> float | DataType:
+    r"""Root Mean Squared Log Error for :class:`~skfda.representation.FData`.
+
+    This function applies the same logic as `mean_squared_log_error`, but
+    directly takes the square root of the result.
+    The values of test y_true = [3, 5, 2.5, 7], y_pred = [2.5, 5, 4, 8] come from
+    https://scikit-learn.org/stable/modules/generated/sklearn.metrics.root_mean_squared_log_error.html
+
+    Args:
+        y_true: True target values.
+        y_pred: Predicted values.
+        sample_weight: Sample weights.
+        multioutput: Return format (raw values or uniform average).
+
+    Returns:
+        Root mean squared logarithmic error.
+    """
+    try:
+        #We try to use the function if available in scikit-learn
+        return sklearn.metrics.root_mean_squared_log_error(
+            y_true,
+            y_pred,
+            sample_weight=sample_weight,
+            multioutput=multioutput,
+        )
+    except AttributeError:
+        # Fallback for older sklearn versions that don't have root_mean_squared_log_error
+        return np.sqrt(sklearn.metrics.mean_squared_log_error(
+            y_true,
+            y_pred,
+            sample_weight=sample_weight,
+            multioutput=multioutput,
+        ))
+
+@root_mean_squared_log_error.register  # type: ignore[attr-defined, misc]
+def _root_mean_squared_log_error_fdatagrid(
+    y_true: FDataGrid,
+    y_pred: FDataGrid,
+    *,
+    sample_weight: NDArrayFloat | None = None,
+    multioutput: MultiOutputType = 'uniform_average',
+) -> float | FDataGrid:
+    
+    if np.any(y_true.data_matrix < 0) or np.any(y_pred.data_matrix < 0):
+        raise ValueError(
+            "Root Mean Squared Logarithmic Error cannot be used when "
+            "targets functions have negative values.",
+        )
+
+    return root_mean_squared_error(
+        np.log1p(y_true),
+        np.log1p(y_pred),
+        sample_weight=sample_weight,
+        multioutput=multioutput,
+    )
+
+@root_mean_squared_log_error.register  # type: ignore[attr-defined, misc]
+def _root_mean_squared_log_error_fdatairregular(
+    y_true: FDataIrregular,
+    y_pred: FDataIrregular,
+    *,
+    sample_weight: NDArrayFloat | None = None,
+    multioutput: MultiOutputType = 'uniform_average',
+) -> float:
+    
+    if np.any(y_true.values < 0) or np.any(y_pred.values < 0):
+        raise ValueError(
+            "Root Mean Squared Logarithmic Error cannot be used when "
+            "targets functions have negative values.",
+        )
+
+    return root_mean_squared_error(
+        np.log1p(y_true),
+        np.log1p(y_pred),
+        sample_weight=sample_weight,
+        multioutput=multioutput,
+    )
+
+@root_mean_squared_log_error.register  # type: ignore[attr-defined, misc]
+def _root_mean_squared_log_error_fdatabasis(
+    y_true: FDataBasis,
+    y_pred: FDataBasis,
+    *,
+    sample_weight: NDArrayFloat | None = None,
+    multioutput: MultiOutputType = 'uniform_average',
+) -> float:
+
+    def _rmsle_func(x: EvalPointsType) -> NDArrayFloat:
+        y_true_eval = y_true(x)
+        y_pred_eval = y_pred(x)
+
+        if np.any(y_true_eval < 0) or np.any(y_pred_eval < 0):
+            raise ValueError(
+                "Root Mean Squared Logarithmic Error cannot be used when "
+                "targets functions have negative values.",
+            )
+
+        error: NDArrayFloat = np.sqrt(np.average(
+            (np.log1p(y_true_eval) - np.log1p(y_pred_eval)) ** 2,
+            weights=sample_weight,
+            axis=0,
+        ))
+
+        # Verify that the error only contains 1 input point
+        assert error.shape[0] == 1
+        return error[0]  # type: ignore [no-any-return]
+
+    return _multioutput_score_basis(y_true, multioutput, _rmsle_func)
+
 
 @overload
 def r2_score(


Test patch:
diff --git a/skfda/tests/test_scoring.py b/skfda/tests/test_scoring.py
index ff8bb9d9..d66c90c2 100644
--- a/skfda/tests/test_scoring.py
+++ b/skfda/tests/test_scoring.py
@@ -16,6 +16,8 @@ from skfda.misc.scoring import (
     mean_squared_error,
     mean_squared_log_error,
     r2_score,
+    root_mean_squared_error,
+    root_mean_squared_log_error
 )
 from skfda.representation.basis import (
     BSplineBasis,
@@ -31,13 +33,15 @@ score_functions: Sequence[ScoreFunction] = (
     mean_squared_error,
     mean_squared_log_error,
     r2_score,
+    root_mean_squared_error,
+    root_mean_squared_log_error
 )
 
 irregular_score_functions: Sequence[ScoreFunction] = (
     mean_absolute_error,
     mean_absolute_percentage_error,
     mean_squared_error,
-    mean_squared_log_error,
+    mean_squared_log_error
 )
 
 
@@ -219,7 +223,28 @@ class TestScoreFunctionsBasis(unittest.TestCase):
 
         # integrate 1/2 * (-x + x^2)^2
         self.assertAlmostEqual(mse, 2.85)
+    
+    def test_root_mean_squared_error_nparray(self) -> None:
+        """Test Root Mean Squared Error for ndarray."""
+        y_true = np.array([3.0, -0.5, 2.0, 7.0])
+        y_pred = np.array([2.5, 0.0, 2.1, 7.8])
+
+        # Test without sample_weight
+        rmse_np = root_mean_squared_error(y_true, y_pred)
+        
+        # Expected result manually
+        expected_result = np.sqrt(np.mean((y_true - y_pred) ** 2))
+        self.assertAlmostEqual(rmse_np, expected_result)
+    
+    def test_root_mean_squared_error_basis(self) -> None:
+        """Test Root Mean Squared Error for FDataBasis."""
+        y_true, y_pred = _create_data_basis()
 
+        rmse = root_mean_squared_error(y_true, y_pred)
+
+        # integrate sqrt(1/2 * (-x + x^2)^2)
+        self.assertAlmostEqual(rmse, 1.1392275919128432)
+        
     def test_mean_squared_log_error_basis(self) -> None:
         """Test Mean Squared Log Error for FDataBasis."""
         y_true, y_pred = _create_data_basis()
@@ -228,6 +253,26 @@ class TestScoreFunctionsBasis(unittest.TestCase):
 
         # integrate 1/2*(log(1 + 4 + 5x + 6x^2) - log(1 + 4 + 6x + 5x^2))^2
         self.assertAlmostEqual(msle, 0.00107583)
+    def test_root_mean_squared_log_error_nparray(self) -> None:
+        """Test Root Mean Squared Log Error for ndarray."""
+        y_true = np.array([3.0, -0.5, 2.0, 7.0])
+        y_pred = np.array([2.5, 0.0, 2.1, 7.8])
+
+        # Test without sample_weight
+        rmsle_np = root_mean_squared_log_error(y_true, y_pred)
+        
+        # Expected result manually
+        expected_result = np.sqrt(np.mean((np.log1p(y_pred) - np.log1p(y_true)) ** 2))
+        self.assertAlmostEqual(rmsle_np, expected_result)
+          
+    def test_root_mean_squared_log_error_basis(self) -> None:
+        """Test Mean Squared Log Error for FDataBasis."""
+        y_true, y_pred = _create_data_basis()
+
+        rmsle = root_mean_squared_log_error(y_true, y_pred)
+
+        # integrate sqrt(1/2*(log(1 + 4 + 5x + 6x^2) - log(1 + 4 + 6x + 5x^2))^2)
+        self.assertAlmostEqual(rmsle, 0.0276305701684461)
 
     def test_r2_score_basis(self) -> None:
         """Test R2 Score for FDataBasis."""