tsadmetrics 0.1.17__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. {docs_manual → docs/api_doc}/conf.py +3 -26
  2. docs/{conf.py → full_doc/conf.py} +1 -1
  3. {docs_api → docs/manual_doc}/conf.py +3 -26
  4. examples/example_direct_data.py +28 -0
  5. examples/example_direct_single_data.py +25 -0
  6. examples/example_file_reference.py +24 -0
  7. examples/example_global_config_file.py +13 -0
  8. examples/example_metric_config_file.py +19 -0
  9. examples/example_simple_metric.py +8 -0
  10. examples/specific_examples/AbsoluteDetectionDistance_example.py +24 -0
  11. examples/specific_examples/AffiliationbasedFScore_example.py +24 -0
  12. examples/specific_examples/AverageDetectionCount_example.py +24 -0
  13. examples/specific_examples/CompositeFScore_example.py +24 -0
  14. examples/specific_examples/DelayThresholdedPointadjustedFScore_example.py +24 -0
  15. examples/specific_examples/DetectionAccuracyInRange_example.py +24 -0
  16. examples/specific_examples/EnhancedTimeseriesAwareFScore_example.py +24 -0
  17. examples/specific_examples/LatencySparsityawareFScore_example.py +24 -0
  18. examples/specific_examples/MeanTimeToDetect_example.py +24 -0
  19. examples/specific_examples/NabScore_example.py +24 -0
  20. examples/specific_examples/PateFScore_example.py +24 -0
  21. examples/specific_examples/Pate_example.py +24 -0
  22. examples/specific_examples/PointadjustedAtKFScore_example.py +24 -0
  23. examples/specific_examples/PointadjustedAucPr_example.py +24 -0
  24. examples/specific_examples/PointadjustedAucRoc_example.py +24 -0
  25. examples/specific_examples/PointadjustedFScore_example.py +24 -0
  26. examples/specific_examples/RangebasedFScore_example.py +24 -0
  27. examples/specific_examples/SegmentwiseFScore_example.py +24 -0
  28. examples/specific_examples/TemporalDistance_example.py +24 -0
  29. examples/specific_examples/TimeTolerantFScore_example.py +24 -0
  30. examples/specific_examples/TimeseriesAwareFScore_example.py +24 -0
  31. examples/specific_examples/TotalDetectedInRange_example.py +24 -0
  32. examples/specific_examples/VusPr_example.py +24 -0
  33. examples/specific_examples/VusRoc_example.py +24 -0
  34. examples/specific_examples/WeightedDetectionDifference_example.py +24 -0
  35. tests/test_dpm.py +212 -0
  36. tests/test_ptdm.py +366 -0
  37. tests/test_registry.py +58 -0
  38. tests/test_runner.py +185 -0
  39. tests/test_spm.py +213 -0
  40. tests/test_tmem.py +198 -0
  41. tests/test_tpdm.py +369 -0
  42. tests/test_tstm.py +338 -0
  43. tsadmetrics/__init__.py +0 -21
  44. tsadmetrics/base/Metric.py +188 -0
  45. tsadmetrics/evaluation/Report.py +25 -0
  46. tsadmetrics/evaluation/Runner.py +253 -0
  47. tsadmetrics/metrics/Registry.py +141 -0
  48. tsadmetrics/metrics/__init__.py +2 -0
  49. tsadmetrics/metrics/spm/PointwiseAucPr.py +62 -0
  50. tsadmetrics/metrics/spm/PointwiseAucRoc.py +63 -0
  51. tsadmetrics/metrics/spm/PointwiseFScore.py +86 -0
  52. tsadmetrics/metrics/spm/PrecisionAtK.py +81 -0
  53. tsadmetrics/metrics/spm/__init__.py +9 -0
  54. tsadmetrics/metrics/tem/dpm/DelayThresholdedPointadjustedFScore.py +83 -0
  55. tsadmetrics/metrics/tem/dpm/LatencySparsityawareFScore.py +76 -0
  56. tsadmetrics/metrics/tem/dpm/MeanTimeToDetect.py +47 -0
  57. tsadmetrics/metrics/tem/dpm/NabScore.py +60 -0
  58. tsadmetrics/metrics/tem/dpm/__init__.py +11 -0
  59. tsadmetrics/metrics/tem/ptdm/AverageDetectionCount.py +53 -0
  60. tsadmetrics/metrics/tem/ptdm/DetectionAccuracyInRange.py +66 -0
  61. tsadmetrics/metrics/tem/ptdm/PointadjustedAtKFScore.py +80 -0
  62. tsadmetrics/metrics/tem/ptdm/TimeseriesAwareFScore.py +248 -0
  63. tsadmetrics/metrics/tem/ptdm/TotalDetectedInRange.py +65 -0
  64. tsadmetrics/metrics/tem/ptdm/WeightedDetectionDifference.py +97 -0
  65. tsadmetrics/metrics/tem/ptdm/__init__.py +12 -0
  66. tsadmetrics/metrics/tem/tmem/AbsoluteDetectionDistance.py +48 -0
  67. tsadmetrics/metrics/tem/tmem/EnhancedTimeseriesAwareFScore.py +252 -0
  68. tsadmetrics/metrics/tem/tmem/TemporalDistance.py +68 -0
  69. tsadmetrics/metrics/tem/tmem/__init__.py +9 -0
  70. tsadmetrics/metrics/tem/tpdm/CompositeFScore.py +104 -0
  71. tsadmetrics/metrics/tem/tpdm/PointadjustedAucPr.py +123 -0
  72. tsadmetrics/metrics/tem/tpdm/PointadjustedAucRoc.py +119 -0
  73. tsadmetrics/metrics/tem/tpdm/PointadjustedFScore.py +96 -0
  74. tsadmetrics/metrics/tem/tpdm/RangebasedFScore.py +236 -0
  75. tsadmetrics/metrics/tem/tpdm/SegmentwiseFScore.py +73 -0
  76. tsadmetrics/metrics/tem/tpdm/__init__.py +12 -0
  77. tsadmetrics/metrics/tem/tstm/AffiliationbasedFScore.py +68 -0
  78. tsadmetrics/metrics/tem/tstm/Pate.py +62 -0
  79. tsadmetrics/metrics/tem/tstm/PateFScore.py +61 -0
  80. tsadmetrics/metrics/tem/tstm/TimeTolerantFScore.py +85 -0
  81. tsadmetrics/metrics/tem/tstm/VusPr.py +51 -0
  82. tsadmetrics/metrics/tem/tstm/VusRoc.py +55 -0
  83. tsadmetrics/metrics/tem/tstm/__init__.py +15 -0
  84. tsadmetrics/{_tsadeval/affiliation/_integral_interval.py → utils/functions_affiliation.py} +377 -9
  85. tsadmetrics/utils/functions_auc.py +393 -0
  86. tsadmetrics/utils/functions_conversion.py +63 -0
  87. tsadmetrics/utils/functions_counting_metrics.py +26 -0
  88. tsadmetrics/{_tsadeval/latency_sparsity_aware.py → utils/functions_latency_sparsity_aware.py} +1 -1
  89. tsadmetrics/{_tsadeval/nabscore.py → utils/functions_nabscore.py} +15 -1
  90. tsadmetrics-1.0.0.dist-info/METADATA +69 -0
  91. tsadmetrics-1.0.0.dist-info/RECORD +99 -0
  92. tsadmetrics-1.0.0.dist-info/top_level.txt +4 -0
  93. entorno/bin/activate_this.py +0 -32
  94. entorno/bin/rst2html.py +0 -23
  95. entorno/bin/rst2html4.py +0 -26
  96. entorno/bin/rst2html5.py +0 -33
  97. entorno/bin/rst2latex.py +0 -26
  98. entorno/bin/rst2man.py +0 -27
  99. entorno/bin/rst2odt.py +0 -28
  100. entorno/bin/rst2odt_prepstyles.py +0 -20
  101. entorno/bin/rst2pseudoxml.py +0 -23
  102. entorno/bin/rst2s5.py +0 -24
  103. entorno/bin/rst2xetex.py +0 -27
  104. entorno/bin/rst2xml.py +0 -23
  105. entorno/bin/rstpep2html.py +0 -25
  106. tests/test_binary.py +0 -946
  107. tests/test_non_binary.py +0 -450
  108. tests/test_utils.py +0 -49
  109. tsadmetrics/_tsadeval/affiliation/_affiliation_zone.py +0 -86
  110. tsadmetrics/_tsadeval/affiliation/_single_ground_truth_event.py +0 -68
  111. tsadmetrics/_tsadeval/affiliation/generics.py +0 -135
  112. tsadmetrics/_tsadeval/affiliation/metrics.py +0 -114
  113. tsadmetrics/_tsadeval/auc_roc_pr_plot.py +0 -295
  114. tsadmetrics/_tsadeval/discontinuity_graph.py +0 -109
  115. tsadmetrics/_tsadeval/eTaPR_pkg/DataManage/File_IO.py +0 -175
  116. tsadmetrics/_tsadeval/eTaPR_pkg/DataManage/Range.py +0 -50
  117. tsadmetrics/_tsadeval/eTaPR_pkg/DataManage/Time_Plot.py +0 -184
  118. tsadmetrics/_tsadeval/eTaPR_pkg/__init__.py +0 -0
  119. tsadmetrics/_tsadeval/eTaPR_pkg/etapr.py +0 -386
  120. tsadmetrics/_tsadeval/eTaPR_pkg/tapr.py +0 -362
  121. tsadmetrics/_tsadeval/metrics.py +0 -698
  122. tsadmetrics/_tsadeval/prts/__init__.py +0 -0
  123. tsadmetrics/_tsadeval/prts/base/__init__.py +0 -0
  124. tsadmetrics/_tsadeval/prts/base/time_series_metrics.py +0 -165
  125. tsadmetrics/_tsadeval/prts/basic_metrics_ts.py +0 -121
  126. tsadmetrics/_tsadeval/prts/time_series_metrics/__init__.py +0 -0
  127. tsadmetrics/_tsadeval/prts/time_series_metrics/fscore.py +0 -61
  128. tsadmetrics/_tsadeval/prts/time_series_metrics/precision.py +0 -86
  129. tsadmetrics/_tsadeval/prts/time_series_metrics/precision_recall.py +0 -21
  130. tsadmetrics/_tsadeval/prts/time_series_metrics/recall.py +0 -85
  131. tsadmetrics/_tsadeval/tests.py +0 -376
  132. tsadmetrics/_tsadeval/threshold_plt.py +0 -30
  133. tsadmetrics/_tsadeval/time_tolerant.py +0 -33
  134. tsadmetrics/binary_metrics.py +0 -1652
  135. tsadmetrics/metric_utils.py +0 -98
  136. tsadmetrics/non_binary_metrics.py +0 -372
  137. tsadmetrics/scripts/__init__.py +0 -0
  138. tsadmetrics/scripts/compute_metrics.py +0 -42
  139. tsadmetrics/utils.py +0 -124
  140. tsadmetrics/validation.py +0 -35
  141. tsadmetrics-0.1.17.dist-info/METADATA +0 -54
  142. tsadmetrics-0.1.17.dist-info/RECORD +0 -66
  143. tsadmetrics-0.1.17.dist-info/entry_points.txt +0 -2
  144. tsadmetrics-0.1.17.dist-info/top_level.txt +0 -6
  145. /tsadmetrics/{_tsadeval → base}/__init__.py +0 -0
  146. /tsadmetrics/{_tsadeval/affiliation → evaluation}/__init__.py +0 -0
  147. /tsadmetrics/{_tsadeval/eTaPR_pkg/DataManage → metrics/tem}/__init__.py +0 -0
  148. /tsadmetrics/{_tsadeval/vus_utils.py → utils/functions_vus.py} +0 -0
  149. {tsadmetrics-0.1.17.dist-info → tsadmetrics-1.0.0.dist-info}/WHEEL +0 -0
tsadmetrics/__init__.py CHANGED
@@ -1,21 +0,0 @@
1
- from .binary_metrics import *
2
- from .non_binary_metrics import *
3
- from .utils import *
4
-
5
-
6
-
7
-
8
- __author__ = 'Pedro Rafael Velasco Priego i12veprp@uco.es'
9
- __version__ = "0.1.3"
10
- __all__ = ['point_wise_recall', 'point_wise_precision', 'point_wise_f_score','point_adjusted_recall',
11
- 'point_adjusted_precision', 'point_adjusted_f_score', 'segment_wise_recall', 'segment_wise_precision',
12
- 'segment_wise_f_score','delay_th_point_adjusted_recall', 'delay_th_point_adjusted_precision',
13
- 'delay_th_point_adjusted_f_score','point_adjusted_at_k_recall','point_adjusted_at_k_precision',
14
- 'point_adjusted_at_k_f_score','latency_sparsity_aw_recall', 'latency_sparsity_aw_precision',
15
- 'latency_sparsity_aw_f_score','composite_f_score','time_tolerant_recall','time_tolerant_precision',
16
- 'time_tolerant_f_score','range_based_recall','range_based_precision','range_based_f_score','ts_aware_recall',
17
- 'ts_aware_precision','ts_aware_f_score','enhanced_ts_aware_recall','enhanced_ts_aware_precision','enhanced_ts_aware_f_score',
18
- 'affiliation_based_recall','affiliation_based_precision','affiliation_based_f_score','nab_score','temporal_distance',
19
- 'average_detection_count','absolute_detection_distance','total_detected_in_range','detection_accuracy_in_range','weighted_detection_difference',
20
- 'binary_pate','real_pate','mean_time_to_detect',
21
- 'precision_at_k','auc_roc_pw','auc_pr_pw','auc_roc_pa','auc_pr_pa','vus_roc','vus_pr', 'compute_metrics', 'compute_metrics_from_file']
@@ -0,0 +1,188 @@
1
+ import yaml
2
+ import numpy as np
3
+
4
+ class Metric:
5
+ """
6
+ Base class for time series anomaly detection metrics.
7
+
8
+ This class provides common functionality for metric configuration, including
9
+ parameter validation from a YAML configuration file and support for a parameter
10
+ schema defined in each subclass.
11
+
12
+ Parameters:
13
+ name (str, optional):
14
+ The name of the metric. If not provided, it defaults to the lowercase
15
+ name of the subclass.
16
+ config_file (str, optional):
17
+ Path to a YAML configuration file. Parameters defined in the file under
18
+ the metric's name will be loaded automatically.
19
+ \*\*params:
20
+ Additional parameters passed directly to the metric. These override
21
+ those loaded from the configuration file.
22
+
23
+ Attributes:
24
+ name (str):
25
+ Name of the metric instance.
26
+ params (dict):
27
+ Dictionary of parameters used by the metric.
28
+ binary_prediction (bool):
29
+ Whether the metric expects binary predictions (True) or continuous scores (False).
30
+
31
+ Raises:
32
+ ValueError:
33
+ If a required parameter is missing or if the configuration file is not found.
34
+ TypeError:
35
+ If a parameter does not match its expected type as defined in the schema.
36
+ """
37
+
38
+ def __init__(self, name=None, config_file=None, **params):
39
+ self.name = name or self.__class__.__name__.lower()
40
+
41
+ # Ensure subclasses define binary_prediction
42
+ if not hasattr(self.__class__, "binary_prediction"):
43
+ raise ValueError(
44
+ f"Subclass {self.__class__.__name__} must define class attribute 'binary_prediction' (True/False)."
45
+ )
46
+ if not isinstance(self.__class__.binary_prediction, bool):
47
+ raise TypeError(
48
+ f"'binary_prediction' in {self.__class__.__name__} must be of type bool."
49
+ )
50
+
51
+ self.binary_prediction = self.__class__.binary_prediction
52
+ self.params = {}
53
+ self.configure(config_file=config_file, **params)
54
+
55
+
56
+ def configure(self, config_file=None, **params):
57
+ """
58
+ Load and validate metric parameters from a YAML configuration file
59
+ and/or from explicit keyword arguments.
60
+
61
+ Parameters:
62
+ config_file (str, optional):
63
+ Path to the configuration file. If provided, it will load parameters
64
+ under the section with the metric's name.
65
+ \*\*params:
66
+ Parameters passed directly to the metric instance.
67
+
68
+ Raises:
69
+ ValueError:
70
+ If a required parameter is not specified or the configuration file is missing.
71
+ TypeError:
72
+ If a parameter value does not match the expected type.
73
+ """
74
+ if config_file:
75
+ try:
76
+ with open(config_file, 'r') as f:
77
+ config = yaml.safe_load(f)
78
+ file_params = config.get(self.name.lower(), {})
79
+ self.params.update(file_params)
80
+ except FileNotFoundError:
81
+ raise ValueError(f"Configuration file '{config_file}' not found.")
82
+
83
+ self.params.update(params)
84
+
85
+ schema = getattr(self.__class__, 'param_schema', {})
86
+ for key, rules in schema.items():
87
+ if key not in self.params:
88
+ if 'default' in rules:
89
+ self.params[key] = rules['default']
90
+ else:
91
+ raise ValueError(f"Required parameter '{key}' not specified.")
92
+
93
+ if 'type' in rules and key in self.params:
94
+ expected_type = rules['type']
95
+ if expected_type is float:
96
+ if not isinstance(self.params[key], (float, int)):
97
+ raise TypeError(f"Parameter '{key}' must be of type float, got {type(self.params[key]).__name__} instead.")
98
+ else:
99
+ if not isinstance(self.params[key], expected_type):
100
+ raise TypeError(f"Parameter '{key}' must be of type {expected_type.__name__}, got {type(self.params[key]).__name__} instead.")
101
+
102
+ def _validate_inputs(self, y_true, y_pred):
103
+ """
104
+ Validate that y_true and y_pred are valid sequences of the same length.
105
+
106
+ If binary_prediction = True:
107
+ Both y_true and y_pred must be binary (0 or 1).
108
+ If binary_prediction = False:
109
+ y_true must be binary (0 or 1), y_pred can be continuous values.
110
+
111
+ Raises:
112
+ ValueError: If lengths differ or values are not valid.
113
+ TypeError: If inputs are not array-like.
114
+ """
115
+ y_true = np.asarray(y_true)
116
+ y_pred = np.asarray(y_pred)
117
+
118
+ if y_true.shape != y_pred.shape:
119
+ raise ValueError(
120
+ f"Shape mismatch: y_true has shape {y_true.shape}, y_pred has shape {y_pred.shape}."
121
+ )
122
+
123
+ if y_true.ndim != 1 or y_pred.ndim != 1:
124
+ raise ValueError("y_true and y_pred must be 1D arrays.")
125
+
126
+ if not np.isin(y_true, [0, 1]).all():
127
+ raise ValueError("y_true must contain only 0 or 1.")
128
+
129
+ if self.binary_prediction:
130
+ if not np.isin(y_pred, [0, 1]).all():
131
+ raise ValueError("y_pred must contain only 0 or 1 (binary_prediction=True).")
132
+
133
+ return y_true, y_pred
134
+
135
+ def _compute(self, y_true, y_pred):
136
+ """
137
+ Compute the value of the metric (core implementation).
138
+
139
+ This method contains the actual logic of the metric and must be
140
+ implemented by subclasses. It is automatically called by
141
+ `compute()` after input validation.
142
+
143
+ Parameters:
144
+ y_true (array-like):
145
+ Ground truth binary labels.
146
+ y_pred (array-like):
147
+ Predicted binary labels.
148
+
149
+ Returns:
150
+ float: The value of the metric.
151
+
152
+ Raises:
153
+ NotImplementedError: If the method is not overridden by a subclass.
154
+ """
155
+ raise NotImplementedError("Subclasses must implement _compute().")
156
+
157
+
158
+
159
+ def compute(self, y_true, y_pred):
160
+ """
161
+ Compute the value of the metric (wrapper method).
162
+
163
+ This method performs input validation and then calls the internal
164
+ `_compute()` method, which contains the actual metric logic.
165
+
166
+ **Important:** Subclasses **should not override** this method.
167
+ Instead, implement `_compute()` to define the behavior of the metric.
168
+
169
+ Parameters
170
+ ----------
171
+ y_true : array-like
172
+ Ground truth binary labels.
173
+ y_pred : array-like
174
+ Predicted binary labels.
175
+
176
+ Returns
177
+ -------
178
+ float
179
+ The value of the metric.
180
+
181
+ Raises
182
+ ------
183
+ NotImplementedError
184
+ If `_compute()` is not implemented by the subclass.
185
+ """
186
+ y_true, y_pred = self._validate_inputs(y_true, y_pred)
187
+ return self._compute(y_true, y_pred)
188
+
@@ -0,0 +1,25 @@
1
+ import pandas as pd
2
+
3
+ class Report:
4
+ def __init__(self):
5
+ pass
6
+ def generate_report(self, results, output_file):
7
+ """
8
+ Generate a report from the evaluation results.
9
+
10
+ Parameters:
11
+ results (dict):
12
+ Dictionary containing evaluation results.
13
+ output_file (str):
14
+ Path to the output file where the report will be saved.
15
+ """
16
+
17
+ if type(results) is dict:
18
+ df = pd.DataFrame.from_dict(results, orient='index')
19
+
20
+ df.index.name = 'dataset'
21
+ df.reset_index(inplace=True)
22
+
23
+ df.to_csv(output_file, index=False, sep=';')
24
+ else:
25
+ results.to_csv(output_file, sep=';')
@@ -0,0 +1,253 @@
1
+ import warnings
2
+ from ..metrics.Registry import Registry
3
+ import numpy as np
4
+ from .Report import Report
5
+ import pandas as pd
6
+ import yaml
7
+ class Runner:
8
+ """
9
+ Orchestrates the evaluation of datasets using a set of metrics.
10
+
11
+ The `Runner` class provides functionality to:
12
+
13
+ - Load datasets from direct data, file references, or a global YAML configuration file.
14
+ - Load metrics either directly from a list or from a configuration file.
15
+ - Evaluate all datasets against all metrics.
16
+ - Optionally generate a report summarizing the evaluation results.
17
+
18
+ Parameters
19
+ ----------
20
+ dataset_evaluations : list or str
21
+ Accepted formats:
22
+
23
+ 1. **Global config file (str)**
24
+
25
+ If a string is provided and `metrics` is None, it is assumed to be
26
+ the path to a configuration file that defines both datasets and metrics.
27
+
28
+ 2. **Direct data (list of tuples)**
29
+
30
+ Example::
31
+
32
+ [
33
+ ("dataset1", y_true1, (y_pred_binary1, y_pred_continuous1)),
34
+ ("dataset2", y_true2, (y_pred_binary2, y_pred_continuous2)),
35
+ ("dataset3", y_true3, y_pred3)
36
+ ]
37
+
38
+ where `y_pred` may be binary or continuous.
39
+
40
+ 3. **File references (list of tuples)**
41
+
42
+ Example::
43
+
44
+ [
45
+ ("dataset1", "result1.csv"),
46
+ ("dataset2", "result2.csv")
47
+ ]
48
+
49
+ Each file must contain:
50
+
51
+ - `y_true`
52
+ - Either:
53
+ * (`y_pred_binary` and `y_pred_continuous`)
54
+ * or (`y_pred`)
55
+
56
+ metrics : list or str, optional
57
+ - **List of metrics**: Each element is a tuple:
58
+
59
+ [(metric_name, {param_name: value, ...}), ...]
60
+
61
+ Example::
62
+
63
+ [
64
+ ("pwf", {"beta": 1.0}),
65
+ ("rpate", {"alpha": 0.5}),
66
+ ("adc", {})
67
+ ]
68
+
69
+ - **Config file (str)**: Path to a YAML file containing metric definitions.
70
+
71
+ Attributes
72
+ ----------
73
+ dataset_evaluations : list
74
+ Loaded datasets in normalized format:
75
+ (name, y_true, y_pred_binary, y_pred_continuous, y_pred)
76
+
77
+ metrics : list
78
+ List of metrics with their configurations.
79
+
80
+ Raises
81
+ ------
82
+ ValueError
83
+ If a configuration file is invalid or required fields are missing.
84
+ """
85
+ def __init__(self, dataset_evaluations, metrics=None):
86
+
87
+
88
+ # Case 1: global config file -> load datasets and metrics from config
89
+ if isinstance(dataset_evaluations, str) and metrics is None:
90
+ config_file = dataset_evaluations
91
+ with open(config_file, "r") as f:
92
+ config = yaml.safe_load(f)
93
+
94
+ # unir lista de dicts en un solo dict
95
+ if isinstance(config, list):
96
+ merged_config = {}
97
+ for entry in config:
98
+ if not isinstance(entry, dict):
99
+ raise ValueError(f"Invalid entry in config file: {entry}")
100
+ merged_config.update(entry)
101
+ config = merged_config
102
+
103
+ if not isinstance(config, dict):
104
+ raise ValueError("Global config file must define datasets and metrics_config as a mapping.")
105
+
106
+ if "metrics_config" not in config:
107
+ raise ValueError("Global config file must contain 'metrics_config'.")
108
+
109
+ # separar datasets de la ruta de métricas
110
+ datasets = [(name, path) for name, path in config.items() if name != "metrics_config"]
111
+
112
+ self.dataset_evaluations = self._load_datasets(datasets)
113
+ self.metrics = Registry.load_metrics_from_file(config["metrics_config"])
114
+ return
115
+
116
+ # Case 2: datasets provided directly, metrics may be list or str
117
+ self.dataset_evaluations = self._load_datasets(dataset_evaluations)
118
+
119
+ if isinstance(metrics, str):
120
+ self.metrics = Registry.load_metrics_from_file(metrics)
121
+ else:
122
+ self.metrics = metrics
123
+
124
+ def _load_datasets(self, dataset_evaluations):
125
+ loaded = []
126
+ for entry in dataset_evaluations:
127
+ name, data = entry[0], entry[1:]
128
+ if len(data) == 1 and isinstance(data[0], str):
129
+ # Case: File reference
130
+ df = pd.read_csv(data[0], sep=';')
131
+ y_true = df["y_true"].values
132
+
133
+ if "y_pred_binary" in df.columns and "y_pred_continuous" in df.columns:
134
+ y_pred_binary = df["y_pred_binary"].values
135
+ y_pred_continuous = df["y_pred_continuous"].values
136
+ elif "y_pred" in df.columns:
137
+ y_pred = df["y_pred"].values
138
+ y_pred_binary, y_pred_continuous = None, None
139
+ else:
140
+ raise ValueError(
141
+ f"File {data[0]} must contain either "
142
+ f"(y_pred_binary, y_pred_continuous) or y_pred column."
143
+ )
144
+ else:
145
+ # Case: Direct data
146
+ if len(data) == 2 and isinstance(data[1], tuple):
147
+ # Format: y_true, (y_pred_binary, y_pred_continuous)
148
+ y_true, (y_pred_binary, y_pred_continuous) = data
149
+ elif len(data) == 2:
150
+ # Format: y_true, y_pred
151
+ y_true, y_pred = data
152
+ y_pred_binary, y_pred_continuous = None, None
153
+ else:
154
+ raise ValueError("Invalid dataset format.")
155
+ loaded.append(
156
+ (name, y_true, y_pred_binary, y_pred_continuous, locals().get("y_pred", None))
157
+ )
158
+ return loaded
159
+
160
+ def run(self, generate_report=False, report_file="evaluation_report.csv"):
161
+ """
162
+ Run the evaluation for all datasets and metrics.
163
+
164
+ Returns:
165
+ pd.DataFrame: DataFrame structured as follows:
166
+
167
+ - The **first row** contains the parameters of each metric.
168
+ - The **subsequent rows** contain the metric values for each dataset.
169
+ - The **index** column represents the dataset names, with the first row labeled as 'params'.
170
+
171
+ Example::
172
+
173
+ dataset | metric1 | metric2
174
+ ----------|---------------|--------
175
+ params | {'param1':0.2}| {}
176
+ dataset1 | 0.5 | 1.0
177
+ dataset2 | 0.125 | 1.0
178
+ """
179
+ results = {}
180
+ metric_keys = {}
181
+
182
+ for dataset_name, y_true, y_pred_binary, y_pred_continuous, y_pred in self.dataset_evaluations:
183
+ dataset_results = {}
184
+ for metric_name, params in self.metrics:
185
+ metric = Registry.get_metric(metric_name, **params)
186
+
187
+ # Computar valor según tipo de métrica
188
+ if getattr(metric, "binary_prediction", False):
189
+ if y_pred_binary is not None:
190
+ value = metric.compute(y_true, y_pred_binary)
191
+ elif y_pred is not None and set(np.unique(y_pred)).issubset({0, 1}):
192
+ value = metric.compute(y_true, y_pred)
193
+ else:
194
+ warnings.warn(
195
+ f"Metric {metric_name} requires binary input, "
196
+ f"but dataset {dataset_name} provided non-binary predictions. Skipped.",
197
+ UserWarning
198
+ )
199
+ value = None
200
+ else:
201
+ if y_pred_continuous is not None:
202
+ value = metric.compute(y_true, y_pred_continuous)
203
+ elif y_pred is not None and not set(np.unique(y_pred)).issubset({0, 1}):
204
+ value = metric.compute(y_true, y_pred)
205
+ else:
206
+ warnings.warn(
207
+ f"Metric {metric_name} requires continuous input, "
208
+ f"but dataset {dataset_name} provided binary predictions. Skipped.",
209
+ UserWarning
210
+ )
211
+ value = None
212
+
213
+ # Generar clave única usando parámetros si ya existe
214
+ base_key = metric_name
215
+ key = f"{metric_name}({params})" if params else metric_name
216
+ if key in metric_keys and metric_keys[key] != params:
217
+ key = f"{metric_name}({params})"
218
+ metric_keys[key] = params
219
+
220
+ dataset_results[key] = value
221
+
222
+ results[dataset_name] = dataset_results
223
+
224
+ # Construir DataFrame con primera fila = parámetros
225
+ if results:
226
+ first_dataset = next(iter(results.values()))
227
+ metric_names = []
228
+ metric_params = []
229
+
230
+ for metric_config in first_dataset.keys():
231
+ if "(" in metric_config:
232
+ name, params_str = metric_config.split("(", 1)
233
+ params_str = params_str.rstrip(")")
234
+ else:
235
+ name, params_str = metric_config, ""
236
+ metric_names.append(name)
237
+ metric_params.append(params_str if params_str != "{}" else "")
238
+
239
+ df_data = [metric_params] # primera fila = parámetros
240
+ for dataset_metrics in results.values():
241
+ df_data.append([dataset_metrics[m] for m in dataset_metrics.keys()])
242
+
243
+ df = pd.DataFrame(df_data, columns=metric_names)
244
+ df.index = ["params"] + list(results.keys())
245
+ df.index.name = 'dataset'
246
+ else:
247
+ df = pd.DataFrame()
248
+
249
+ if generate_report:
250
+ report = Report()
251
+ report.generate_report(df, report_file)
252
+
253
+ return df
@@ -0,0 +1,141 @@
1
+ from typing import Type
2
+ import yaml
3
+ from ..base.Metric import Metric
4
+ from .spm import *
5
+ from .tem.tpdm import *
6
+ from .tem.ptdm import *
7
+ from .tem.tmem import *
8
+ from .tem.dpm import *
9
+ from .tem.tstm import *
10
+
11
+ class Registry:
12
+ """
13
+ Central registry for anomaly detection metrics.
14
+
15
+ This class provides a centralized interface to register, retrieve,
16
+ and load metric classes for anomaly detection tasks.
17
+ """
18
+
19
+ _registry = {}
20
+
21
+ @classmethod
22
+ def register(cls, metric_cls: Type[Metric]):
23
+ """
24
+ Register a metric class using its `name` attribute.
25
+
26
+ Args:
27
+ metric_cls (Type[Metric]): The metric class to register.
28
+ The class must define a ``name`` attribute.
29
+
30
+ Raises:
31
+ ValueError: If the metric class does not define a ``name``
32
+ attribute or if a metric with the same name is already registered.
33
+ """
34
+ if not hasattr(metric_cls, "name"):
35
+ raise ValueError(f"Metric class {metric_cls.__name__} must define a 'name' attribute.")
36
+
37
+ name = metric_cls.name
38
+ if name in cls._registry:
39
+ raise ValueError(f"Metric '{name}' is already registered.")
40
+
41
+ cls._registry[name] = metric_cls
42
+
43
+ @classmethod
44
+ def get_metric(cls, name: str, **params) -> Metric:
45
+ """
46
+ Retrieve and instantiate a registered metric by name.
47
+
48
+ Args:
49
+ name (str): Name of the metric to retrieve.
50
+ \*\*params: Parameters to initialize the metric instance.
51
+
52
+ Returns:
53
+ Metric: An instance of the requested metric.
54
+
55
+ Raises:
56
+ ValueError: If the metric name is not registered.
57
+ """
58
+ if name not in cls._registry:
59
+ raise ValueError(f"Metric '{name}' is not registered.")
60
+ return cls._registry[name](**params)
61
+
62
+ @classmethod
63
+ def available_metrics(cls):
64
+ """
65
+ List all registered metric names.
66
+
67
+ Returns:
68
+ list[str]: A list of registered metric names.
69
+ """
70
+ return list(cls._registry.keys())
71
+
72
+ @classmethod
73
+ def load_metrics_info_from_file(cls, filepath: str):
74
+ """
75
+ Load metric definitions (names and parameters) from a YAML configuration file.
76
+
77
+ Args:
78
+ filepath (str): Path to the YAML file.
79
+
80
+ Returns:
81
+ list[tuple[str, dict]]: A list of tuples containing the metric name and
82
+ its parameters, e.g. ``[("metric_name", {"param1": value, ...}), ...]``.
83
+
84
+ Raises:
85
+ ValueError: If the YAML file contains invalid entries or unsupported format.
86
+ """
87
+ with open(filepath, "r") as f:
88
+ config = yaml.safe_load(f)
89
+
90
+ metrics_info = []
91
+
92
+ if isinstance(config, list):
93
+ # If YAML is a list of metric names or dicts
94
+ for entry in config:
95
+ if isinstance(entry, str):
96
+ metrics_info.append((entry, {}))
97
+ elif isinstance(entry, dict):
98
+ for name, params in entry.items():
99
+ metrics_info.append((name, params or {}))
100
+ else:
101
+ raise ValueError(f"Invalid metric entry: {entry}")
102
+ elif isinstance(config, dict):
103
+ # If YAML is a dictionary: {metric: params}
104
+ for name, params in config.items():
105
+ if params is None:
106
+ params = {}
107
+ metrics_info.append((name, params))
108
+ else:
109
+ raise ValueError("YAML format must be a list or dict.")
110
+ return metrics_info
111
+
112
+ @classmethod
113
+ def load_metrics_from_file(cls, filepath: str):
114
+ """
115
+ Load and instantiate metrics from a YAML configuration file.
116
+
117
+ Args:
118
+ filepath (str): Path to the YAML configuration file.
119
+
120
+ Returns:
121
+ list[tuple[str, dict]]: A list of tuples containing the metric name and
122
+ the parameters used to instantiate it.
123
+ """
124
+ metrics_info = cls.load_metrics_info_from_file(filepath)
125
+ metrics = []
126
+ for name, params in metrics_info:
127
+ metric = cls.get_metric(name, **params)
128
+ metrics.append((name, params))
129
+ return metrics
130
+
131
+
132
+ # --- Auto-discovery
133
+ def auto_register():
134
+ """
135
+ Automatically register all subclasses of ``Metric`` found in the project.
136
+
137
+ This function inspects the current inheritance tree of ``Metric`` and
138
+ registers each subclass in the central registry.
139
+ """
140
+ for metric_cls in Metric.__subclasses__():
141
+ Registry.register(metric_cls)
@@ -0,0 +1,2 @@
1
+ from .Registry import auto_register
2
+ auto_register()