Repository: DashAISoftware/DashAI (language: python)

Issue:
Metrics for classification tasks
# Summary
This branch depends on the PR #77 .
The metrics class was created as well as a register for them.
Incorporated accuracy, recall, precision and f1score metrics.
Tests were made for the metrics

## Type of change
- Back end new feature.

## Changes
- Changes in core/config files

## How to Test
Tests were performed on the test_classification_metrics.py

## Notes
By approving this PR you would already have a complete back end pipeline to train and evaluate a model. But only the task of tabulating classification, sklearn models and the mentioned metrics.


Discussion / hints:

Fix patch:
diff --git a/DashAI/back/core/config.py b/DashAI/back/core/config.py
index a5032f13..3de08519 100644
--- a/DashAI/back/core/config.py
+++ b/DashAI/back/core/config.py
@@ -1,6 +1,7 @@
 from pydantic import BaseSettings
 
 from DashAI.back.dataloaders import CSVDataLoader, JSONDataLoader
+from DashAI.back.metrics import F1, Accuracy, Precision, Recall
 from DashAI.back.models import SVC, KNeighborsClassifier, RandomForestClassifier
 from DashAI.back.registries.component_registry import ComponentRegistry
 from DashAI.back.tasks import (
@@ -11,14 +12,22 @@
 
 component_registry = ComponentRegistry(
     initial_components=[
+        # Tasks
         TabularClassificationTask,
         TextClassificationTask,
         TranslationTask,
+        # Models
         SVC,
         KNeighborsClassifier,
         RandomForestClassifier,
+        # Dataloaders
         CSVDataLoader,
         JSONDataLoader,
+        # Metrics
+        F1,
+        Accuracy,
+        Precision,
+        Recall,
     ],
 )
 
diff --git a/DashAI/back/metrics/__init__.py b/DashAI/back/metrics/__init__.py
new file mode 100644
index 00000000..afd9c5c2
--- /dev/null
+++ b/DashAI/back/metrics/__init__.py
@@ -0,0 +1,5 @@
+# flake8: noqa
+from DashAI.back.metrics.classification.accuracy import Accuracy
+from DashAI.back.metrics.classification.f1 import F1
+from DashAI.back.metrics.classification.precision import Precision
+from DashAI.back.metrics.classification.recall import Recall
diff --git a/DashAI/back/metrics/base_metric.py b/DashAI/back/metrics/base_metric.py
new file mode 100644
index 00000000..924d974f
--- /dev/null
+++ b/DashAI/back/metrics/base_metric.py
@@ -0,0 +1,9 @@
+from typing import Final
+
+
+class BaseMetric:
+    """
+    Abstract class of all metrics
+    """
+
+    TYPE: Final[str] = "Metric"
diff --git a/DashAI/back/metrics/classification/__init__.py b/DashAI/back/metrics/classification/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/DashAI/back/metrics/classification/accuracy.py b/DashAI/back/metrics/classification/accuracy.py
new file mode 100644
index 00000000..91f5345b
--- /dev/null
+++ b/DashAI/back/metrics/classification/accuracy.py
@@ -0,0 +1,33 @@
+import numpy as np
+from sklearn.metrics import accuracy_score
+
+from DashAI.back.metrics.classification_metric import (
+    ClassificationMetric,
+    validate_inputs,
+)
+
+
+class Accuracy(ClassificationMetric):
+    """
+    Accuracy metric to classification tasks
+    """
+
+    @staticmethod
+    def score(true_labels: list, probs_pred_labels: list):
+        """Calculates the accuracy between true labels and predicted labels
+
+        Parameters
+        ----------
+        true_labels : list
+            True labels
+        probs_pred_labels : list
+            Probabilities of belonging to the class according to the model
+
+        Returns
+        -------
+        float
+            Accuracy score between true labels and predicted labels
+        """
+        validate_inputs(true_labels, probs_pred_labels)
+        pred_labels = np.argmax(probs_pred_labels, axis=1)
+        return accuracy_score(true_labels, pred_labels)
diff --git a/DashAI/back/metrics/classification/f1.py b/DashAI/back/metrics/classification/f1.py
new file mode 100644
index 00000000..4a28fbaa
--- /dev/null
+++ b/DashAI/back/metrics/classification/f1.py
@@ -0,0 +1,37 @@
+import numpy as np
+from sklearn.metrics import f1_score
+
+from DashAI.back.metrics.classification_metric import (
+    ClassificationMetric,
+    validate_inputs,
+)
+
+
+class F1(ClassificationMetric):
+    """
+    F1 score to classification tasks
+    """
+
+    @staticmethod
+    def score(true_labels: list, probs_pred_labels: list):
+        """Calculates the f1 score between true labels and predicted labels
+
+        Parameters
+        ----------
+        true_labels : list
+            True labels
+        probs_pred_labels : list
+            Probabilities of belonging to the class according to the model
+
+        Returns
+        -------
+        float
+            f1 score between true labels and predicted labels
+        """
+        validate_inputs(true_labels, probs_pred_labels)
+        pred_labels = np.argmax(probs_pred_labels, axis=1)
+        multiclass = len(np.unique(true_labels)) > 2
+        if multiclass:
+            return f1_score(true_labels, pred_labels, average="micro")
+        else:
+            return f1_score(true_labels, pred_labels, average="binary")
diff --git a/DashAI/back/metrics/classification/precision.py b/DashAI/back/metrics/classification/precision.py
new file mode 100644
index 00000000..b693a1a1
--- /dev/null
+++ b/DashAI/back/metrics/classification/precision.py
@@ -0,0 +1,37 @@
+import numpy as np
+from sklearn.metrics import precision_score
+
+from DashAI.back.metrics.classification_metric import (
+    ClassificationMetric,
+    validate_inputs,
+)
+
+
+class Precision(ClassificationMetric):
+    """
+    Precision metric to classification tasks
+    """
+
+    @staticmethod
+    def score(true_labels: list, probs_pred_labels: list):
+        """Calculates the precision between true labels and predicted labels
+
+        Parameters
+        ----------
+        true_labels : list
+            True labels
+        probs_pred_labels : list
+            Probabilities of belonging to the class according to the model
+
+        Returns
+        -------
+        float
+            Precision score between true labels and predicted labels
+        """
+        validate_inputs(true_labels, probs_pred_labels)
+        pred_labels = np.argmax(probs_pred_labels, axis=1)
+        multiclass = len(np.unique(true_labels)) > 2
+        if multiclass:
+            return precision_score(true_labels, pred_labels, average="micro")
+        else:
+            return precision_score(true_labels, pred_labels, average="binary")
diff --git a/DashAI/back/metrics/classification/recall.py b/DashAI/back/metrics/classification/recall.py
new file mode 100644
index 00000000..26c2bfa9
--- /dev/null
+++ b/DashAI/back/metrics/classification/recall.py
@@ -0,0 +1,37 @@
+import numpy as np
+from sklearn.metrics import recall_score
+
+from DashAI.back.metrics.classification_metric import (
+    ClassificationMetric,
+    validate_inputs,
+)
+
+
+class Recall(ClassificationMetric):
+    """
+    Recall metric to classification tasks
+    """
+
+    @staticmethod
+    def score(true_labels: list, probs_pred_labels: list):
+        """Calculates the recall between true labels and predicted labels
+
+        Parameters
+        ----------
+        true_labels : list
+            True labels
+        probs_pred_labels : list
+            Probabilities of belonging to the class according to the model
+
+        Returns
+        -------
+        float
+            recall score between true labels and predicted labels
+        """
+        validate_inputs(true_labels, probs_pred_labels)
+        pred_labels = np.argmax(probs_pred_labels, axis=1)
+        multiclass = len(np.unique(true_labels)) > 2
+        if multiclass:
+            return recall_score(true_labels, pred_labels, average="micro")
+        else:
+            return recall_score(true_labels, pred_labels, average="binary")
diff --git a/DashAI/back/metrics/classification_metric.py b/DashAI/back/metrics/classification_metric.py
new file mode 100644
index 00000000..abbdbc2b
--- /dev/null
+++ b/DashAI/back/metrics/classification_metric.py
@@ -0,0 +1,23 @@
+from DashAI.back.metrics.base_metric import BaseMetric
+
+
+class ClassificationMetric(BaseMetric):
+    """
+    Class for metrics associated to classification models
+    """
+
+    COMPATIBLE_COMPONENTS = ["TabularClassificationTask"]
+
+
+def validate_inputs(true_labels: list, pred_labels: list):
+    """Validate inputs.
+
+    Parameters
+    ----------
+    true_labels : list
+        True labels
+    pred_labels : list
+        Predict labels by the model
+    """
+    if len(true_labels) != len(pred_labels):
+        raise ValueError("The length of the true and predicted labels must be equal.")


Test patch:
diff --git a/tests/back/metrics/__init__.py b/tests/back/metrics/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/back/metrics/test_classification_metrics.py b/tests/back/metrics/test_classification_metrics.py
new file mode 100644
index 00000000..60a7c5d8
--- /dev/null
+++ b/tests/back/metrics/test_classification_metrics.py
@@ -0,0 +1,74 @@
+import pytest
+
+from DashAI.back.dataloaders.classes.dashai_dataset import load_dataset
+from DashAI.back.metrics.classification.accuracy import Accuracy
+from DashAI.back.metrics.classification.f1 import F1
+from DashAI.back.metrics.classification.precision import Precision
+from DashAI.back.metrics.classification.recall import Recall
+from DashAI.back.models.scikit_learn.random_forest_classifier import (
+    RandomForestClassifier,
+)
+from DashAI.back.models.scikit_learn.sklearn_like_model import SklearnLikeModel
+from DashAI.back.tasks.tabular_classification_task import TabularClassificationTask
+
+
+@pytest.fixture(scope="module", name="datasetdashai_tabular_classification")
+def fixture_datasetdashai_tab_class_and_fit_model():
+    tabular_task = TabularClassificationTask.create()
+    dashai_datasetdict = load_dataset("tests/back/dataloaders/dashaidataset")
+    name_datasetdict = "Iris"
+    dashai_datasetdict = tabular_task.prepare_for_task(dashai_datasetdict)
+    tabular_task.validate_dataset_for_task(dashai_datasetdict, name_datasetdict)
+    rf = RandomForestClassifier()
+    dataset_prepared = rf.format_data(dashai_datasetdict)
+    rf.fit(dataset_prepared["train"]["input"], dataset_prepared["train"]["output"])
+    rf.save("tests/back/metrics/rf_model")
+    yield dataset_prepared
+
+
+def test_accuracy(datasetdashai_tabular_classification: dict):
+    dataset_prepared = datasetdashai_tabular_classification
+    model_rf = SklearnLikeModel.load("tests/back/metrics/rf_model")
+    pred_ref = model_rf.predict(dataset_prepared["test"]["input"])
+    try:
+        isinstance(Accuracy.score(dataset_prepared["test"]["output"], pred_ref), float)
+    except Exception as e:
+        pytest.fail(f"Unexpected error in test_accuracy: {repr(e)}")
+
+
+def test_precision(datasetdashai_tabular_classification: dict):
+    dataset_prepared = datasetdashai_tabular_classification
+    model_rf = SklearnLikeModel.load("tests/back/metrics/rf_model")
+    pred_ref = model_rf.predict(dataset_prepared["test"]["input"])
+    try:
+        isinstance(Precision.score(dataset_prepared["test"]["output"], pred_ref), float)
+    except Exception as e:
+        pytest.fail(f"Unexpected error in test_precision: {repr(e)}")
+
+
+def test_recall(datasetdashai_tabular_classification: dict):
+    dataset_prepared = datasetdashai_tabular_classification
+    model_rf = SklearnLikeModel.load("tests/back/metrics/rf_model")
+    pred_ref = model_rf.predict(dataset_prepared["test"]["input"])
+    try:
+        isinstance(Recall.score(dataset_prepared["test"]["output"], pred_ref), float)
+    except Exception as e:
+        pytest.fail(f"Unexpected error in test_recall: {repr(e)}")
+
+
+def test_f1score(datasetdashai_tabular_classification: dict):
+    dataset_prepared = datasetdashai_tabular_classification
+    model_rf = SklearnLikeModel.load("tests/back/metrics/rf_model")
+    pred_ref = model_rf.predict(dataset_prepared["test"]["input"])
+    try:
+        isinstance(F1.score(dataset_prepared["test"]["output"], pred_ref), float)
+    except Exception as e:
+        pytest.fail(f"Unexpected error in test_f1score: {repr(e)}")
+
+
+def test_wrong_size_metric(datasetdashai_tabular_classification: dict):
+    dataset_prepared = datasetdashai_tabular_classification
+    model_rf = SklearnLikeModel.load("tests/back/metrics/rf_model")
+    pred_ref = model_rf.predict(dataset_prepared["validation"]["input"])
+    with pytest.raises(ValueError):
+        Accuracy.score(dataset_prepared["test"]["output"], pred_ref)