From 067f000ffa597492db309397ebee69c6348e5352 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Lamercerie?=
 <aurelien.lamercerie@tetras-libre.fr>
Date: Thu, 10 Aug 2023 17:32:52 +0200
Subject: [PATCH] Add metric_score (metrics submodule)

---
 ontoScorer/metric_score.py | 68 +++++++++++++++++++++++++++++
 ontoScorer/metrics.py      | 89 +++++++++++++++++++++-----------------
 tests/test_metrics.py      | 25 +++++++----
 3 files changed, 134 insertions(+), 48 deletions(-)
 create mode 100644 ontoScorer/metric_score.py

diff --git a/ontoScorer/metric_score.py b/ontoScorer/metric_score.py
new file mode 100644
index 0000000..d25555f
--- /dev/null
+++ b/ontoScorer/metric_score.py
@@ -0,0 +1,68 @@
+# -*-coding:Utf-8 -*
+
+"""
+metric_score: Ontology Scoring Helper Module
+------------------------------------------------------------------------------
+This module defines the Score class, which helps to encapsulate and compute 
+individual scoring metrics such as precision, recall, and F1 score. It also 
+keeps track of the number of total elements and matched elements.
+"""
+
+from sklearn.metrics import precision_score, recall_score, f1_score
+
+
+class Score:
+    """Class to encapsulate individual scoring metrics."""
+        
+    #--------------------------------------------------------------------------
+    # Constructor(s)
+    #--------------------------------------------------------------------------
+
+    def __init__(self):
+        """Initialize an empty score object with default metrics."""
+        self.precision = None
+        self.recall = None
+        self.f1 = None
+        self.total_elements = 0
+        self.matched_elements = 0
+
+
+    #--------------------------------------------------------------------------
+    # Computing Method(s)
+    #--------------------------------------------------------------------------
+    
+    def compute(self, y_true, y_pred):
+        """
+        Compute and update the precision, recall, and F1 score based on true and predicted labels.
+
+        Args:
+            y_true (list[int]): List of ground truth (correct) labels.
+            y_pred (list[int]): List of predicted labels.
+
+        Returns:
+            None
+        """
+        self.precision = precision_score(y_true, y_pred)
+        self.recall = recall_score(y_true, y_pred)
+        self.f1 = f1_score(y_true, y_pred)
+        self.total_elements = len(y_true)
+        self.matched_elements = sum([1 for true, pred in zip(y_true, y_pred) if true == pred])
+        
+        
+    #--------------------------------------------------------------------------
+    # Printing Method(s)
+    #--------------------------------------------------------------------------
+    
+    def _format_metric(self, metric_value):
+        return f"{metric_value:.4f}" if metric_value is not None else "NA"
+
+    def __str__(self):
+        metrics = [
+        f"\tPrecision: {self._format_metric(self.precision)}",
+        f"\tRecall: {self._format_metric(self.recall)}",
+        f"\tF1 Score: {self._format_metric(self.f1)}",
+        f"\tTotal Elements: {self.total_elements}",
+        f"\tMatched Elements: {self.matched_elements}"
+        ]
+        return "\n".join(metrics)
+
diff --git a/ontoScorer/metrics.py b/ontoScorer/metrics.py
index e6e06c5..b3c2b38 100644
--- a/ontoScorer/metrics.py
+++ b/ontoScorer/metrics.py
@@ -1,85 +1,96 @@
 #!/usr/bin/python3.10
 # -*-coding:Utf-8 -*
 
-#==============================================================================
-# ontoScorer: Ontology Scoring Module
-#------------------------------------------------------------------------------
-# This module provides metrics to evaluate and compare different ontologies.
-# It calculates precision, recall, and F1 score for various ontology elements
-# such as classes, object properties, data properties, restrictions, individuals,
-# and annotations. It also computes an overall score taking into account all
-# the ontology elements. The comparison is performed between a reference ontology
-# and a generated ontology, allowing users to evaluate how well the generated 
-# ontology matches the reference.
-#==============================================================================
+"""
+ontoScorer: Ontology Scoring Module
+------------------------------------------------------------------------------
+This module provides metrics to evaluate and compare different ontologies. It 
+calculates precision, recall, and F1 score for various ontology elements such 
+as classes, object properties, data properties, restrictions, individuals, and 
+annotations. The comparison is performed between a reference ontology and a 
+generated ontology, allowing users to evaluate how well the generated ontology 
+matches the reference.
+"""
 
-
-from sklearn.metrics import precision_score, recall_score, f1_score
 from ontoScorer.ontology import Ontology
+from ontoScorer.metric_score import Score
 
 class Metrics:
+    """
+    Metrics class provides functionalities to compute scores for ontology
+    elements based on a reference and generated ontology.
+    """
     
     #--------------------------------------------------------------------------
     # Constructor(s)
     #--------------------------------------------------------------------------
     
     def __init__(self):
+        """
+        Initializes score categories for various ontology elements.
+        """
         self.scores = {
-            "class": {"precision": 0, "recall": 0, "f1": 0},
-            "object_property": {"precision": 0, "recall": 0, "f1": 0},
-            "data_property": {"precision": 0, "recall": 0, "f1": 0},
-            "restriction": {"precision": 0, "recall": 0, "f1": 0},
-            "individual": {"precision": 0, "recall": 0, "f1": 0},
-            "annotation": {"precision": 0, "recall": 0, "f1": 0},
-            "overall": {"precision": 0, "recall": 0, "f1": 0}
+            "class": Score(),
+            "object_property": Score(),
+            "data_property": Score(),
+            "restriction": Score(),
+            "individual": Score(),
+            "annotation": Score(),
+            "overall": Score()
         }
 
 
     #--------------------------------------------------------------------------
     # Computing Method(s)
     #--------------------------------------------------------------------------
-
+    
     def calculate(self, reference_ontology, generated_ontology):
+        """
+        Compute scores (precision, recall, f1) for each ontology element category.
+        
+        Args:
+        - reference_ontology: Ontology object representing the reference ontology.
+        - generated_ontology: Ontology object representing the generated ontology.
+        """
         methods = [
             ("class", "get_classes"),
             ("object_property", "get_object_properties"),
+            # Additional methods can be uncommented as needed
             #("data_property", "get_data_properties"),
             #("restriction", "get_restrictions"),
             ("individual", "get_individuals"),
             #("annotation", "get_annotations")
         ]
-
+    
         y_true_overall = []
         y_pred_overall = []
-
+    
         for score_name, method_name in methods:
             reference_elements = set([elem.name() for elem in getattr(reference_ontology, method_name)()])
             generated_elements = set([elem.name() for elem in getattr(generated_ontology, method_name)()])
-
+    
             all_elements = reference_elements.union(generated_elements)
             y_true = [1 if elem in reference_elements else 0 for elem in all_elements]
             y_pred = [1 if elem in generated_elements else 0 for elem in all_elements]
-
-            self.scores[score_name]["precision"] = precision_score(y_true, y_pred)
-            self.scores[score_name]["recall"] = recall_score(y_true, y_pred)
-            self.scores[score_name]["f1"] = f1_score(y_true, y_pred)
-
+    
+            self.scores[score_name].compute(y_true, y_pred)
+    
             y_true_overall.extend(y_true)
             y_pred_overall.extend(y_pred)
+    
+        self.scores["overall"].compute(y_true_overall, y_pred_overall)
+
+
 
-        self.scores["overall"]["precision"] = precision_score(y_true_overall, y_pred_overall)
-        self.scores["overall"]["recall"] = recall_score(y_true_overall, y_pred_overall)
-        self.scores["overall"]["f1"] = f1_score(y_true_overall, y_pred_overall)
-        
-        
     #--------------------------------------------------------------------------
     # Printing Method(s)
     #--------------------------------------------------------------------------
     
     def print_scores(self):
-        for element, scores in self.scores.items():
+        """
+        Prints the scores (precision, recall, f1) for each ontology element category.
+        """
+        for element, score in self.scores.items():
             print(f"Metrics for {element.capitalize()}:")
-            print(f"\tPrecision: {scores['precision']:.4f}")
-            print(f"\tRecall: {scores['recall']:.4f}")
-            print(f"\tF1 Score: {scores['f1']:.4f}")
-            print("----------------------------")
\ No newline at end of file
+            print(score)
+            print("----------------------------")
diff --git a/tests/test_metrics.py b/tests/test_metrics.py
index 61327bc..0eb3365 100644
--- a/tests/test_metrics.py
+++ b/tests/test_metrics.py
@@ -1,11 +1,11 @@
 #!/usr/bin/python3.10
 # -*-coding:Utf-8 -*
 
-#==============================================================================
-# test_metrics: Metrics Testing Module
-#------------------------------------------------------------------------------
-# Contains tests for verifying functionality of the Metrics class.
-#==============================================================================
+"""
+test_metrics: Metrics Testing Module
+------------------------------------------------------------------------------
+Contains tests for verifying functionality of the Metrics class.
+"""
 
 import unittest
 import os
@@ -26,16 +26,23 @@ class TestMetrics(unittest.TestCase):
         self.onto2 = Ontology(self.ontology2_path)
         self.metrics = Metrics()
 
+
     def test_calculate_scores(self):
         self.metrics.calculate(self.onto1, self.onto2)
-        for key in self.metrics.scores:
-            self.assertTrue(0 <= self.metrics.scores[key]["precision"] <= 1)
-            self.assertTrue(0 <= self.metrics.scores[key]["recall"] <= 1)
-            self.assertTrue(0 <= self.metrics.scores[key]["f1"] <= 1)
+        for element, score in self.metrics.scores.items():
+            if score.total_elements == 0:
+                self.assertIsNone(score.precision, f"Precision for {element} should be None when no elements are present")
+                self.assertIsNone(score.recall, f"Recall for {element} should be None when no elements are present")
+                self.assertIsNone(score.f1, f"F1 score for {element} should be None when no elements are present")
+            else:
+                self.assertTrue(0 <= score.precision <= 1, f"Invalid precision for {element}")
+                self.assertTrue(0 <= score.recall <= 1, f"Invalid recall for {element}")
+                self.assertTrue(0 <= score.f1 <= 1, f"Invalid F1 score for {element}")
 
 
     def test_print_scores(self):
         self.metrics.calculate(self.onto1, self.onto2)
+        print()
         self.metrics.print_scores()
 
 
-- 
GitLab