tsadmetrics 0.1.17__py3-none-any.whl → 1.0.1__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.
- {docs_api → docs/add_docs/api_doc}/conf.py +3 -26
- {docs_manual → docs/add_docs/full_doc}/conf.py +2 -25
- docs/add_docs/manual_doc/conf.py +67 -0
- docs/conf.py +1 -1
- examples/example_direct_data.py +28 -0
- examples/example_direct_single_data.py +25 -0
- examples/example_file_reference.py +24 -0
- examples/example_global_config_file.py +13 -0
- examples/example_metric_config_file.py +19 -0
- examples/example_simple_metric.py +8 -0
- examples/specific_examples/AbsoluteDetectionDistance_example.py +24 -0
- examples/specific_examples/AffiliationbasedFScore_example.py +24 -0
- examples/specific_examples/AverageDetectionCount_example.py +24 -0
- examples/specific_examples/CompositeFScore_example.py +24 -0
- examples/specific_examples/DelayThresholdedPointadjustedFScore_example.py +24 -0
- examples/specific_examples/DetectionAccuracyInRange_example.py +24 -0
- examples/specific_examples/EnhancedTimeseriesAwareFScore_example.py +24 -0
- examples/specific_examples/LatencySparsityawareFScore_example.py +24 -0
- examples/specific_examples/MeanTimeToDetect_example.py +24 -0
- examples/specific_examples/NabScore_example.py +24 -0
- examples/specific_examples/PateFScore_example.py +24 -0
- examples/specific_examples/Pate_example.py +24 -0
- examples/specific_examples/PointadjustedAtKFScore_example.py +24 -0
- examples/specific_examples/PointadjustedAucPr_example.py +24 -0
- examples/specific_examples/PointadjustedAucRoc_example.py +24 -0
- examples/specific_examples/PointadjustedFScore_example.py +24 -0
- examples/specific_examples/RangebasedFScore_example.py +24 -0
- examples/specific_examples/SegmentwiseFScore_example.py +24 -0
- examples/specific_examples/TemporalDistance_example.py +24 -0
- examples/specific_examples/TimeTolerantFScore_example.py +24 -0
- examples/specific_examples/TimeseriesAwareFScore_example.py +24 -0
- examples/specific_examples/TotalDetectedInRange_example.py +24 -0
- examples/specific_examples/VusPr_example.py +24 -0
- examples/specific_examples/VusRoc_example.py +24 -0
- examples/specific_examples/WeightedDetectionDifference_example.py +24 -0
- tsadmetrics/__init__.py +0 -21
- tsadmetrics/base/Metric.py +188 -0
- tsadmetrics/evaluation/Report.py +25 -0
- tsadmetrics/evaluation/Runner.py +253 -0
- tsadmetrics/metrics/Registry.py +141 -0
- tsadmetrics/metrics/__init__.py +2 -0
- tsadmetrics/metrics/spm/PointwiseAucPr.py +62 -0
- tsadmetrics/metrics/spm/PointwiseAucRoc.py +63 -0
- tsadmetrics/metrics/spm/PointwiseFScore.py +86 -0
- tsadmetrics/metrics/spm/PrecisionAtK.py +81 -0
- tsadmetrics/metrics/spm/__init__.py +9 -0
- tsadmetrics/metrics/tem/dpm/DelayThresholdedPointadjustedFScore.py +83 -0
- tsadmetrics/metrics/tem/dpm/LatencySparsityawareFScore.py +76 -0
- tsadmetrics/metrics/tem/dpm/MeanTimeToDetect.py +47 -0
- tsadmetrics/metrics/tem/dpm/NabScore.py +60 -0
- tsadmetrics/metrics/tem/dpm/__init__.py +11 -0
- tsadmetrics/metrics/tem/ptdm/AverageDetectionCount.py +53 -0
- tsadmetrics/metrics/tem/ptdm/DetectionAccuracyInRange.py +66 -0
- tsadmetrics/metrics/tem/ptdm/PointadjustedAtKFScore.py +80 -0
- tsadmetrics/metrics/tem/ptdm/TimeseriesAwareFScore.py +248 -0
- tsadmetrics/metrics/tem/ptdm/TotalDetectedInRange.py +65 -0
- tsadmetrics/metrics/tem/ptdm/WeightedDetectionDifference.py +97 -0
- tsadmetrics/metrics/tem/ptdm/__init__.py +12 -0
- tsadmetrics/metrics/tem/tmem/AbsoluteDetectionDistance.py +48 -0
- tsadmetrics/metrics/tem/tmem/EnhancedTimeseriesAwareFScore.py +252 -0
- tsadmetrics/metrics/tem/tmem/TemporalDistance.py +68 -0
- tsadmetrics/metrics/tem/tmem/__init__.py +9 -0
- tsadmetrics/metrics/tem/tpdm/CompositeFScore.py +104 -0
- tsadmetrics/metrics/tem/tpdm/PointadjustedAucPr.py +123 -0
- tsadmetrics/metrics/tem/tpdm/PointadjustedAucRoc.py +119 -0
- tsadmetrics/metrics/tem/tpdm/PointadjustedFScore.py +96 -0
- tsadmetrics/metrics/tem/tpdm/RangebasedFScore.py +236 -0
- tsadmetrics/metrics/tem/tpdm/SegmentwiseFScore.py +73 -0
- tsadmetrics/metrics/tem/tpdm/__init__.py +12 -0
- tsadmetrics/metrics/tem/tstm/AffiliationbasedFScore.py +68 -0
- tsadmetrics/metrics/tem/tstm/Pate.py +62 -0
- tsadmetrics/metrics/tem/tstm/PateFScore.py +61 -0
- tsadmetrics/metrics/tem/tstm/TimeTolerantFScore.py +85 -0
- tsadmetrics/metrics/tem/tstm/VusPr.py +51 -0
- tsadmetrics/metrics/tem/tstm/VusRoc.py +55 -0
- tsadmetrics/metrics/tem/tstm/__init__.py +15 -0
- tsadmetrics/{_tsadeval/affiliation/_integral_interval.py → utils/functions_affiliation.py} +377 -9
- tsadmetrics/utils/functions_auc.py +393 -0
- tsadmetrics/utils/functions_conversion.py +63 -0
- tsadmetrics/utils/functions_counting_metrics.py +26 -0
- tsadmetrics/{_tsadeval/latency_sparsity_aware.py → utils/functions_latency_sparsity_aware.py} +1 -1
- tsadmetrics/{_tsadeval/nabscore.py → utils/functions_nabscore.py} +15 -1
- tsadmetrics-1.0.1.dist-info/METADATA +83 -0
- tsadmetrics-1.0.1.dist-info/RECORD +91 -0
- tsadmetrics-1.0.1.dist-info/top_level.txt +3 -0
- entorno/bin/activate_this.py +0 -32
- entorno/bin/rst2html.py +0 -23
- entorno/bin/rst2html4.py +0 -26
- entorno/bin/rst2html5.py +0 -33
- entorno/bin/rst2latex.py +0 -26
- entorno/bin/rst2man.py +0 -27
- entorno/bin/rst2odt.py +0 -28
- entorno/bin/rst2odt_prepstyles.py +0 -20
- entorno/bin/rst2pseudoxml.py +0 -23
- entorno/bin/rst2s5.py +0 -24
- entorno/bin/rst2xetex.py +0 -27
- entorno/bin/rst2xml.py +0 -23
- entorno/bin/rstpep2html.py +0 -25
- tests/test_binary.py +0 -946
- tests/test_non_binary.py +0 -450
- tests/test_utils.py +0 -49
- tsadmetrics/_tsadeval/affiliation/_affiliation_zone.py +0 -86
- tsadmetrics/_tsadeval/affiliation/_single_ground_truth_event.py +0 -68
- tsadmetrics/_tsadeval/affiliation/generics.py +0 -135
- tsadmetrics/_tsadeval/affiliation/metrics.py +0 -114
- tsadmetrics/_tsadeval/auc_roc_pr_plot.py +0 -295
- tsadmetrics/_tsadeval/discontinuity_graph.py +0 -109
- tsadmetrics/_tsadeval/eTaPR_pkg/DataManage/File_IO.py +0 -175
- tsadmetrics/_tsadeval/eTaPR_pkg/DataManage/Range.py +0 -50
- tsadmetrics/_tsadeval/eTaPR_pkg/DataManage/Time_Plot.py +0 -184
- tsadmetrics/_tsadeval/eTaPR_pkg/DataManage/__init__.py +0 -0
- tsadmetrics/_tsadeval/eTaPR_pkg/__init__.py +0 -0
- tsadmetrics/_tsadeval/eTaPR_pkg/etapr.py +0 -386
- tsadmetrics/_tsadeval/eTaPR_pkg/tapr.py +0 -362
- tsadmetrics/_tsadeval/metrics.py +0 -698
- tsadmetrics/_tsadeval/prts/__init__.py +0 -0
- tsadmetrics/_tsadeval/prts/base/__init__.py +0 -0
- tsadmetrics/_tsadeval/prts/base/time_series_metrics.py +0 -165
- tsadmetrics/_tsadeval/prts/basic_metrics_ts.py +0 -121
- tsadmetrics/_tsadeval/prts/time_series_metrics/__init__.py +0 -0
- tsadmetrics/_tsadeval/prts/time_series_metrics/fscore.py +0 -61
- tsadmetrics/_tsadeval/prts/time_series_metrics/precision.py +0 -86
- tsadmetrics/_tsadeval/prts/time_series_metrics/precision_recall.py +0 -21
- tsadmetrics/_tsadeval/prts/time_series_metrics/recall.py +0 -85
- tsadmetrics/_tsadeval/tests.py +0 -376
- tsadmetrics/_tsadeval/threshold_plt.py +0 -30
- tsadmetrics/_tsadeval/time_tolerant.py +0 -33
- tsadmetrics/binary_metrics.py +0 -1652
- tsadmetrics/metric_utils.py +0 -98
- tsadmetrics/non_binary_metrics.py +0 -372
- tsadmetrics/scripts/__init__.py +0 -0
- tsadmetrics/scripts/compute_metrics.py +0 -42
- tsadmetrics/utils.py +0 -124
- tsadmetrics/validation.py +0 -35
- tsadmetrics-0.1.17.dist-info/METADATA +0 -54
- tsadmetrics-0.1.17.dist-info/RECORD +0 -66
- tsadmetrics-0.1.17.dist-info/entry_points.txt +0 -2
- tsadmetrics-0.1.17.dist-info/top_level.txt +0 -6
- {tests → tsadmetrics/base}/__init__.py +0 -0
- /tsadmetrics/{_tsadeval → evaluation}/__init__.py +0 -0
- /tsadmetrics/{_tsadeval/affiliation → metrics/tem}/__init__.py +0 -0
- /tsadmetrics/{_tsadeval/vus_utils.py → utils/functions_vus.py} +0 -0
- {tsadmetrics-0.1.17.dist-info → tsadmetrics-1.0.1.dist-info}/WHEEL +0 -0
@@ -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,62 @@
|
|
1
|
+
from ...base.Metric import Metric
|
2
|
+
import numpy as np
|
3
|
+
from ...utils.functions_auc import precision_recall_curve
|
4
|
+
|
5
|
+
class PointwiseAucPr(Metric):
|
6
|
+
"""
|
7
|
+
Point-wise Area Under the Precision-Recall Curve (AUC-PR) for anomaly detection.
|
8
|
+
|
9
|
+
This metric computes the standard Area Under the Precision-Recall Curve (AUC-PR)
|
10
|
+
in a **point-wise manner**. Each time-series data point is treated independently
|
11
|
+
when calculating precision and recall, making this suitable for anomaly detection tasks
|
12
|
+
where anomalies are labeled at the individual point level.
|
13
|
+
|
14
|
+
Reference:
|
15
|
+
Implementation based on:
|
16
|
+
https://link.springer.com/article/10.1007/s10618-023-00988-8
|
17
|
+
|
18
|
+
Attributes:
|
19
|
+
name (str):
|
20
|
+
Fixed name identifier for this metric: `"pw_auc_pr"`.
|
21
|
+
binary_prediction (bool):
|
22
|
+
Indicates whether this metric expects binary predictions. Always `False`
|
23
|
+
since it requires continuous anomaly scores.
|
24
|
+
|
25
|
+
Raises:
|
26
|
+
ValueError:
|
27
|
+
If input arrays are invalid or improperly shaped (handled by the base class).
|
28
|
+
TypeError:
|
29
|
+
If inputs are not array-like.
|
30
|
+
"""
|
31
|
+
|
32
|
+
name = "pw_auc_pr"
|
33
|
+
binary_prediction = False
|
34
|
+
def __init__(self, **kwargs):
|
35
|
+
"""
|
36
|
+
Initialize the PointwiseAucPr metric.
|
37
|
+
|
38
|
+
Parameters:
|
39
|
+
**kwargs:
|
40
|
+
Additional keyword arguments passed to the base `Metric` class.
|
41
|
+
These may include configuration parameters or overrides.
|
42
|
+
"""
|
43
|
+
super().__init__(name="pw_auc_pr", **kwargs)
|
44
|
+
|
45
|
+
def _compute(self, y_true, y_anomaly_scores):
|
46
|
+
"""
|
47
|
+
Compute the point-wise AUC-PR score.
|
48
|
+
|
49
|
+
Parameters:
|
50
|
+
y_true (np.ndarray):
|
51
|
+
Ground-truth binary labels for the time series.
|
52
|
+
Values must be 0 (normal) or 1 (anomaly).
|
53
|
+
y_anomaly_scores (np.ndarray):
|
54
|
+
Continuous anomaly scores assigned to each point in the series.
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
float:
|
58
|
+
The computed point-wise AUC-PR score.
|
59
|
+
"""
|
60
|
+
|
61
|
+
precision, recall, _ = precision_recall_curve(y_true, y_anomaly_scores)
|
62
|
+
return -np.sum(np.diff(recall) * np.array(precision)[:-1])
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from ...base.Metric import Metric
|
2
|
+
import numpy as np
|
3
|
+
from ...utils.functions_auc import roc_curve, auc
|
4
|
+
|
5
|
+
class PointwiseAucRoc(Metric):
|
6
|
+
"""
|
7
|
+
Point-wise Area Under the Receiver Operating Characteristic Curve (AUC-ROC) for anomaly detection.
|
8
|
+
|
9
|
+
This metric computes the standard Area Under the ROC Curve (AUC-ROC)
|
10
|
+
in a **point-wise manner**. Each time-series data point is treated independently
|
11
|
+
when calculating true positives, false positives, and false negatives.
|
12
|
+
It is widely used to evaluate the ability of anomaly scoring functions
|
13
|
+
to distinguish between normal and anomalous points.
|
14
|
+
|
15
|
+
Reference:
|
16
|
+
Implementation based on:
|
17
|
+
https://link.springer.com/article/10.1007/s10618-023-00988-8
|
18
|
+
|
19
|
+
Attributes:
|
20
|
+
name (str):
|
21
|
+
Fixed name identifier for this metric: `"pw_auc_roc"`.
|
22
|
+
binary_prediction (bool):
|
23
|
+
Indicates whether this metric expects binary predictions. Always `False`
|
24
|
+
since it requires continuous anomaly scores.
|
25
|
+
|
26
|
+
Raises:
|
27
|
+
ValueError:
|
28
|
+
If input arrays are invalid or improperly shaped (validated in the base class).
|
29
|
+
TypeError:
|
30
|
+
If inputs are not array-like.
|
31
|
+
"""
|
32
|
+
|
33
|
+
name = "pw_auc_roc"
|
34
|
+
binary_prediction = False
|
35
|
+
|
36
|
+
def __init__(self, **kwargs):
|
37
|
+
"""
|
38
|
+
Initialize the PointwiseAucRoc metric.
|
39
|
+
|
40
|
+
Parameters:
|
41
|
+
**kwargs:
|
42
|
+
Additional keyword arguments passed to the base `Metric` class.
|
43
|
+
These may include configuration parameters or overrides.
|
44
|
+
"""
|
45
|
+
super().__init__(name="pw_auc_roc", **kwargs)
|
46
|
+
|
47
|
+
def _compute(self, y_true, y_anomaly_scores):
|
48
|
+
"""
|
49
|
+
Compute the point-wise AUC-ROC score.
|
50
|
+
|
51
|
+
Parameters:
|
52
|
+
y_true (np.ndarray):
|
53
|
+
Ground-truth binary labels for the time series.
|
54
|
+
Values must be 0 (normal) or 1 (anomaly).
|
55
|
+
y_anomaly_scores (np.ndarray):
|
56
|
+
Continuous anomaly scores assigned to each point in the series.
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
float:
|
60
|
+
The computed point-wise AUC-ROC score.
|
61
|
+
"""
|
62
|
+
fpr, tpr, _ = roc_curve(y_true, y_anomaly_scores)
|
63
|
+
return auc(fpr, tpr)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
from ...base.Metric import Metric
|
2
|
+
import numpy as np
|
3
|
+
|
4
|
+
class PointwiseFScore(Metric):
|
5
|
+
"""
|
6
|
+
Point-wise F-score for anomaly detection in time series.
|
7
|
+
|
8
|
+
This metric computes the classical F-score without considering temporal context,
|
9
|
+
treating each time-series point independently. It balances precision and recall
|
10
|
+
according to the configurable parameter `beta`.
|
11
|
+
|
12
|
+
Reference:
|
13
|
+
Implementation based on:
|
14
|
+
https://link.springer.com/article/10.1007/s10618-023-00988-8
|
15
|
+
|
16
|
+
Parameters:
|
17
|
+
beta (float, optional):
|
18
|
+
The beta value determines the relative weight of recall compared to precision.
|
19
|
+
A value of 1.0 gives equal weight (F1-score).
|
20
|
+
Default is 1.0.
|
21
|
+
|
22
|
+
Attributes:
|
23
|
+
name (str):
|
24
|
+
Fixed name identifier for this metric: `"pwf"`.
|
25
|
+
binary_prediction (bool):
|
26
|
+
Indicates whether this metric expects binary predictions. Always `True`.
|
27
|
+
param_schema (dict):
|
28
|
+
Schema for supported parameters:
|
29
|
+
- `beta` (float, default=1.0).
|
30
|
+
|
31
|
+
Raises:
|
32
|
+
ValueError:
|
33
|
+
If required parameters are missing (validated by the base class).
|
34
|
+
TypeError:
|
35
|
+
If parameter types do not match the schema (validated by the base class).
|
36
|
+
"""
|
37
|
+
|
38
|
+
name = "pwf"
|
39
|
+
binary_prediction = True
|
40
|
+
param_schema = {
|
41
|
+
"beta": {
|
42
|
+
"default": 1.0,
|
43
|
+
"type": float
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
def __init__(self, **kwargs):
|
48
|
+
"""
|
49
|
+
Initialize the PointwiseFScore metric.
|
50
|
+
|
51
|
+
Parameters:
|
52
|
+
**kwargs:
|
53
|
+
Additional keyword arguments passed to the base `Metric` class.
|
54
|
+
These may include configuration parameters such as `beta`.
|
55
|
+
"""
|
56
|
+
super().__init__(name="pwf", **kwargs)
|
57
|
+
|
58
|
+
def _compute(self, y_true, y_pred):
|
59
|
+
"""
|
60
|
+
Compute the point-wise F-score.
|
61
|
+
|
62
|
+
Parameters:
|
63
|
+
y_true (np.ndarray):
|
64
|
+
Ground-truth binary labels for the time series.
|
65
|
+
Values must be 0 (normal) or 1 (anomaly).
|
66
|
+
y_pred (np.ndarray):
|
67
|
+
Predicted binary labels for the time series.
|
68
|
+
Values must be 0 (normal) or 1 (anomaly).
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
float:
|
72
|
+
The computed point-wise F-score.
|
73
|
+
Returns 0 if either precision or recall is 0.
|
74
|
+
"""
|
75
|
+
tp = np.sum(y_pred * y_true)
|
76
|
+
fp = np.sum(y_pred * (1 - y_true))
|
77
|
+
fn = np.sum((1 - y_pred) * y_true)
|
78
|
+
|
79
|
+
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
|
80
|
+
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
|
81
|
+
|
82
|
+
if precision == 0 or recall == 0:
|
83
|
+
return 0
|
84
|
+
|
85
|
+
beta = self.params['beta']
|
86
|
+
return ((1 + beta**2) * precision * recall) / (beta**2 * precision + recall)
|
@@ -0,0 +1,81 @@
|
|
1
|
+
from ...base.Metric import Metric
|
2
|
+
import numpy as np
|
3
|
+
|
4
|
+
class PrecisionAtK(Metric):
|
5
|
+
"""
|
6
|
+
Precision at K (P@K) for anomaly detection in time series.
|
7
|
+
|
8
|
+
This metric evaluates how many of the top-`k` points with the highest anomaly
|
9
|
+
scores correspond to true anomalies. It is particularly useful when focusing
|
10
|
+
on identifying the most anomalous points rather than setting a global threshold.
|
11
|
+
|
12
|
+
By definition, `k` is automatically set to the number of true anomalies present
|
13
|
+
in `y_true`.
|
14
|
+
|
15
|
+
.. math::
|
16
|
+
k = \sum(y\_true)
|
17
|
+
|
18
|
+
Reference:
|
19
|
+
Implementation based on:
|
20
|
+
https://link.springer.com/article/10.1007/s10618-023-00988-8
|
21
|
+
|
22
|
+
Attributes:
|
23
|
+
name (str):
|
24
|
+
Fixed name identifier for this metric: `"pak"`.
|
25
|
+
binary_prediction (bool):
|
26
|
+
Indicates whether this metric expects binary predictions. Always `False`
|
27
|
+
since it requires continuous anomaly scores.
|
28
|
+
|
29
|
+
Raises:
|
30
|
+
AssertionError:
|
31
|
+
- If the number of true anomalies (`k`) is zero.
|
32
|
+
- If the number of predicted positives is less than `k`.
|
33
|
+
ValueError:
|
34
|
+
If input arrays are invalid or improperly shaped (validated in the base class).
|
35
|
+
TypeError:
|
36
|
+
If inputs are not array-like.
|
37
|
+
"""
|
38
|
+
|
39
|
+
name = "pak"
|
40
|
+
binary_prediction = False
|
41
|
+
|
42
|
+
def __init__(self, **kwargs):
|
43
|
+
"""
|
44
|
+
Initialize the PrecisionAtK metric.
|
45
|
+
|
46
|
+
Parameters:
|
47
|
+
**kwargs:
|
48
|
+
Additional keyword arguments passed to the base `Metric` class.
|
49
|
+
These may include configuration parameters or overrides.
|
50
|
+
"""
|
51
|
+
super().__init__(name="pak", **kwargs)
|
52
|
+
|
53
|
+
def _compute(self, y_true, y_anomaly_scores):
|
54
|
+
"""
|
55
|
+
Compute the Precision at K (P@K) score.
|
56
|
+
|
57
|
+
Parameters:
|
58
|
+
y_true (np.ndarray):
|
59
|
+
Ground-truth binary labels for the time series.
|
60
|
+
Values must be 0 (normal) or 1 (anomaly).
|
61
|
+
y_anomaly_scores (np.ndarray):
|
62
|
+
Continuous anomaly scores assigned to each point in the series.
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
float:
|
66
|
+
The precision at K score, where K = number of anomalies in `y_true`.
|
67
|
+
|
68
|
+
Raises:
|
69
|
+
AssertionError:
|
70
|
+
If `k = sum(y_true)` is 0.
|
71
|
+
If fewer than `k` points are predicted as anomalies.
|
72
|
+
"""
|
73
|
+
k = int(sum(y_true))
|
74
|
+
assert k > 0, "The number of true anomalies (k) must be greater than zero."
|
75
|
+
threshold = np.sort(y_anomaly_scores)[-k]
|
76
|
+
|
77
|
+
pred = y_anomaly_scores >= threshold
|
78
|
+
assert sum(pred) >= k, (
|
79
|
+
f"Number of predicted positives ({sum(pred)}) should be >= k ({k})."
|
80
|
+
)
|
81
|
+
return np.dot(pred, y_true) / sum(pred)
|