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,248 @@
|
|
1
|
+
from ....base.Metric import Metric
|
2
|
+
from ....utils.functions_conversion import full_series_to_segmentwise
|
3
|
+
import math
|
4
|
+
class TimeseriesAwareFScore(Metric):
|
5
|
+
"""
|
6
|
+
Calculate time series aware F-score for anomaly detection in time series.
|
7
|
+
|
8
|
+
This metric is based on the range_based_f_score, but introduces two key modifications.
|
9
|
+
First, a predicted anomalous segment is only counted as a true positive if it covers at least a fraction
|
10
|
+
:math:`{\\theta}` of the ground‑truth anomaly range. Second, each labeled anomaly is extended by a tolerance window of
|
11
|
+
length :math:`{\\delta}` at its end, within which any overlap contribution decays linearly from full weight down to zero.
|
12
|
+
Unlike the original range-based formulation, this variant omits cardinality and positional bias terms,
|
13
|
+
focusing solely on overlap fraction and end‑tolerance decay.
|
14
|
+
|
15
|
+
Implementation of https://link.springer.com/article/10.1007/s10618-023-00988-8
|
16
|
+
|
17
|
+
For more information, see the original paper:
|
18
|
+
https://doi.org/10.1145/3357384.3358118
|
19
|
+
|
20
|
+
Parameters:
|
21
|
+
y_true (np.array):
|
22
|
+
The ground truth binary labels for the time series data.
|
23
|
+
y_pred (np.array):
|
24
|
+
The predicted binary labels for the time series data.
|
25
|
+
beta (float):
|
26
|
+
The beta value, which determines the weight of precision in the combined score.
|
27
|
+
Default is 1, which gives equal weight to precision and recall.
|
28
|
+
alpha (float):
|
29
|
+
Relative importance of the existence reward versus overlap reward (:math:`{0 \\leq \\alpha \\leq 1}`).
|
30
|
+
delta (float):
|
31
|
+
Tolerance window length at the end of each true anomaly segment.
|
32
|
+
- If past_range is True, :math:`{\\delta}` must be a float in (0, 1], representing the fraction of the segment’s
|
33
|
+
length to extend. E.g., :math:`{\\delta}` = 0.5 extends a segment of length 10 by 5 time steps.
|
34
|
+
- If past_range is False, :math:`{\\delta}` must be a non-negative integer, representing an absolute number of
|
35
|
+
time steps to extend each segment.
|
36
|
+
theta (float):
|
37
|
+
Minimum fraction (:math:`{ 0 \\leq \\theta \\leq 1}`) of the true anomaly range that must be overlapped by
|
38
|
+
predictions for the segment to count as detected.
|
39
|
+
past_range (bool):
|
40
|
+
Determines how :math:`{\\delta}` is interpreted.
|
41
|
+
- True: :math:`{\\delta}` is treated as a fractional extension of each segment’s length.
|
42
|
+
- False: :math:`{\\delta}` is treated as an absolute number of time steps.
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
float: The time series aware F-score, which is the harmonic mean of precision and recall, adjusted by the beta value.
|
46
|
+
"""
|
47
|
+
name = "taf"
|
48
|
+
binary_prediction = True
|
49
|
+
param_schema = {
|
50
|
+
"beta": {
|
51
|
+
"default": 1.0,
|
52
|
+
"type": float
|
53
|
+
},
|
54
|
+
"alpha": {
|
55
|
+
"default": 0.5,
|
56
|
+
"type": float
|
57
|
+
},
|
58
|
+
"delta": {
|
59
|
+
"default": 5,
|
60
|
+
"type": float
|
61
|
+
},
|
62
|
+
"theta": {
|
63
|
+
"default": 0.5,
|
64
|
+
"type": float
|
65
|
+
},
|
66
|
+
"past_range": {
|
67
|
+
"default": False,
|
68
|
+
"type": bool
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
def __init__(self, **kwargs):
|
73
|
+
super().__init__(name="taf", **kwargs)
|
74
|
+
|
75
|
+
def _gen_ambiguous(self, y_true_sw, y_pred_sw):
|
76
|
+
ambiguous_inst = []
|
77
|
+
delta = self.params["delta"]
|
78
|
+
past_range = self.params["past_range"]
|
79
|
+
for i in range(len(y_true_sw)):
|
80
|
+
start_id = y_true_sw[i][1] + 1
|
81
|
+
end_id = end_id = start_id + delta
|
82
|
+
|
83
|
+
if past_range:
|
84
|
+
end_id = start_id + int(delta * (y_true_sw[i][1] - y_true_sw[0]))
|
85
|
+
|
86
|
+
#if the next anomaly occurs during the theta, update the end_id
|
87
|
+
if i+1 < len(y_true_sw) and end_id > y_true_sw[i+1][0]:
|
88
|
+
end_id = y_true_sw[i+1][0] - 1
|
89
|
+
|
90
|
+
if start_id > end_id:
|
91
|
+
start_id = -2
|
92
|
+
end_id = -1
|
93
|
+
|
94
|
+
ambiguous_inst.append([start_id, end_id])
|
95
|
+
return ambiguous_inst
|
96
|
+
|
97
|
+
|
98
|
+
def _min_max_norm(self, value, org_min, org_max, new_min, new_max) -> float:
|
99
|
+
if org_min == org_max:
|
100
|
+
return new_min
|
101
|
+
else:
|
102
|
+
return (float)(new_min) + (float)(value - org_min) * (new_max - new_min) / (org_max - org_min)
|
103
|
+
|
104
|
+
def _decaying_func(self, val: float) -> float:
|
105
|
+
assert (-6 <= val <= 6)
|
106
|
+
return 1 / (1 + math.exp(val))
|
107
|
+
|
108
|
+
def _ascending_func(self, val: float) -> float:
|
109
|
+
assert (-6 <= val <= 6)
|
110
|
+
return 1 / (1 + math.exp(val * -1))
|
111
|
+
|
112
|
+
def _uniform_func(self, val: float) -> float:
|
113
|
+
return 1.0
|
114
|
+
|
115
|
+
def _sum_of_func(self, start_time, end_time, org_start, org_end,
|
116
|
+
func) -> float:
|
117
|
+
val = 0.0
|
118
|
+
for timestamp in range(start_time, end_time + 1):
|
119
|
+
val += func(self._min_max_norm(timestamp, org_start, org_end, -6, 6))
|
120
|
+
return val
|
121
|
+
|
122
|
+
def _overlap_and_subsequent_score(self, anomaly, ambiguous, prediction) -> float:
|
123
|
+
score = 0.0
|
124
|
+
|
125
|
+
detected_start = max(anomaly[0], prediction[0])
|
126
|
+
detected_end = min(anomaly[1], prediction[1])
|
127
|
+
|
128
|
+
score += self._sum_of_func(detected_start, detected_end,
|
129
|
+
anomaly[0], anomaly[1], self._uniform_func)
|
130
|
+
|
131
|
+
if ambiguous[0] < ambiguous[1]:
|
132
|
+
detected_start = max(ambiguous[0], prediction[0])
|
133
|
+
detected_end = min(ambiguous[1], prediction[1])
|
134
|
+
|
135
|
+
score += self._sum_of_func(detected_start, detected_end,
|
136
|
+
ambiguous[0], ambiguous[1], self._decaying_func)
|
137
|
+
return score
|
138
|
+
def _TaP_dp_value(self, y_true_sw, y_pred_sw):
|
139
|
+
|
140
|
+
ambiguous_inst = self._gen_ambiguous(y_true_sw, y_pred_sw)
|
141
|
+
threshold = self.params["theta"]
|
142
|
+
correct_predictions = []
|
143
|
+
total_score = 0.0
|
144
|
+
total_score_p = 0.0
|
145
|
+
for prediction_id in range(len(y_pred_sw)):
|
146
|
+
max_score = y_pred_sw[prediction_id][1] - y_pred_sw[prediction_id][0] + 1
|
147
|
+
|
148
|
+
score = 0.0
|
149
|
+
for anomaly_id in range(len(y_true_sw)):
|
150
|
+
anomaly = y_true_sw[anomaly_id]
|
151
|
+
ambiguous = ambiguous_inst[anomaly_id]
|
152
|
+
|
153
|
+
score += self._overlap_and_subsequent_score(anomaly, ambiguous, y_pred_sw[prediction_id])
|
154
|
+
total_score_p += score / max_score
|
155
|
+
if (score/max_score) >= threshold:
|
156
|
+
total_score += 1.0
|
157
|
+
correct_predictions.append(prediction_id)
|
158
|
+
|
159
|
+
if len(y_pred_sw) == 0:
|
160
|
+
return 0.0, 0.0
|
161
|
+
|
162
|
+
else:
|
163
|
+
TaP_p_value = total_score_p / len(y_pred_sw)
|
164
|
+
return TaP_p_value, total_score / len(y_pred_sw)
|
165
|
+
|
166
|
+
|
167
|
+
def _TaR_dp_value(self, y_true_sw, y_pred_sw):
|
168
|
+
ambiguous_inst = self._gen_ambiguous(y_true_sw, y_pred_sw)
|
169
|
+
threshold = self.params["theta"]
|
170
|
+
total_score = 0.0
|
171
|
+
detected_anomalies = []
|
172
|
+
total_score_p = 0.0
|
173
|
+
for anomaly_id in range(len(y_true_sw)):
|
174
|
+
anomaly = y_true_sw[anomaly_id]
|
175
|
+
ambiguous = ambiguous_inst[anomaly_id]
|
176
|
+
|
177
|
+
max_score = self._sum_of_func(anomaly[0], anomaly[1],
|
178
|
+
anomaly[0], anomaly[1], self._uniform_func)
|
179
|
+
|
180
|
+
score = 0.0
|
181
|
+
for prediction in y_pred_sw:
|
182
|
+
score += self._overlap_and_subsequent_score(anomaly, ambiguous, prediction)
|
183
|
+
|
184
|
+
total_score_p += min(1.0, score/max_score)
|
185
|
+
if min(1.0, score / max_score) >= threshold:
|
186
|
+
total_score += 1.0
|
187
|
+
detected_anomalies.append(anomaly_id)
|
188
|
+
|
189
|
+
if len(y_true_sw) == 0:
|
190
|
+
return 0.0, 0.0
|
191
|
+
else:
|
192
|
+
TaR_p_value = total_score_p / len(y_true_sw)
|
193
|
+
return TaR_p_value, total_score / len(y_true_sw)
|
194
|
+
|
195
|
+
|
196
|
+
def _compute_precision(self, y_true_sw, y_pred_sw):
|
197
|
+
"""
|
198
|
+
Calculate precision for time series aware F-score.
|
199
|
+
|
200
|
+
Parameters:
|
201
|
+
y_true_sw (np.array): Ground truth binary labels in segment-wise format.
|
202
|
+
y_pred_sw (np.array): Predicted binary labels in segment-wise format.
|
203
|
+
Returns:
|
204
|
+
float: The precision value.
|
205
|
+
"""
|
206
|
+
tapp_value, tapd_value = self._TaP_dp_value(y_true_sw, y_pred_sw)
|
207
|
+
|
208
|
+
alpha = self.params["alpha"]
|
209
|
+
return alpha * tapd_value + (1 - alpha) * tapp_value
|
210
|
+
|
211
|
+
def _compute_recall(self, y_true_sw, y_pred_sw):
|
212
|
+
"""
|
213
|
+
Calculate recall for time series aware F-score.
|
214
|
+
|
215
|
+
Parameters:
|
216
|
+
y_true_sw (np.array): Ground truth binary labels in segment-wise format.
|
217
|
+
y_pred_sw (np.array): Predicted binary labels in segment-wise format.
|
218
|
+
Returns:
|
219
|
+
float: The recall value.
|
220
|
+
"""
|
221
|
+
tarp_value, tard_value = self._TaR_dp_value(y_true_sw, y_pred_sw)
|
222
|
+
|
223
|
+
alpha = self.params["alpha"]
|
224
|
+
return alpha * tard_value + (1 - alpha) * tarp_value
|
225
|
+
|
226
|
+
def _compute(self, y_true, y_pred):
|
227
|
+
"""
|
228
|
+
Calculate the time series aware F-score.
|
229
|
+
|
230
|
+
Parameters:
|
231
|
+
y_true (np.array): Ground truth binary labels.
|
232
|
+
y_pred (np.array): Predicted binary labels.
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
float: The time series aware F-score.
|
236
|
+
"""
|
237
|
+
y_true_sw = full_series_to_segmentwise(y_true)
|
238
|
+
y_pred_sw = full_series_to_segmentwise(y_pred)
|
239
|
+
|
240
|
+
|
241
|
+
|
242
|
+
precision = self._compute_precision(y_true_sw, y_pred_sw)
|
243
|
+
recall = self._compute_recall(y_true_sw, y_pred_sw)
|
244
|
+
if precision == 0 or recall == 0:
|
245
|
+
return 0
|
246
|
+
|
247
|
+
beta = self.params["beta"]
|
248
|
+
return ((1 + beta**2) * precision * recall) / (beta**2 * precision + recall)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
from ....base.Metric import Metric
|
2
|
+
from ....utils.functions_counting_metrics import counting_method
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
class TotalDetectedInRange(Metric):
|
6
|
+
"""
|
7
|
+
Calculate total detected in range for anomaly detection in time series.
|
8
|
+
|
9
|
+
This metric measures the proportion of true anomaly events that are correctly detected.
|
10
|
+
It is defined as:
|
11
|
+
|
12
|
+
.. math::
|
13
|
+
\\text{TDIR} = \\frac{EM + DA}{EM + DA + MA}
|
14
|
+
|
15
|
+
Where:
|
16
|
+
|
17
|
+
- EM (Exact Match):
|
18
|
+
Number of predicted anomaly segments that exactly match a true anomaly segment.
|
19
|
+
- DA (Detected Anomaly):
|
20
|
+
Number of true anomaly points not exactly matched where at least one prediction falls
|
21
|
+
within a window [i-k, i+k] around the true point index i or within the true segment range.
|
22
|
+
- MA (Missed Anomaly):
|
23
|
+
Number of true anomaly segments that do not overlap any predicted anomaly segment
|
24
|
+
even within a k-step tolerance window around true points.
|
25
|
+
|
26
|
+
For more information, see the original paper:
|
27
|
+
https://acta.sapientia.ro/content/docs/evaluation-metrics-for-anomaly-detection.pdf
|
28
|
+
|
29
|
+
Parameters:
|
30
|
+
k (int):
|
31
|
+
Half-window size for tolerance around each true anomaly point. A prediction within k
|
32
|
+
time steps of a true point counts toward detection.
|
33
|
+
"""
|
34
|
+
name = "tdir"
|
35
|
+
binary_prediction = True
|
36
|
+
param_schema = {
|
37
|
+
"k": {
|
38
|
+
"default": 5,
|
39
|
+
"type": int
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
def __init__(self, **kwargs):
|
44
|
+
super().__init__(name="tdir", **kwargs)
|
45
|
+
|
46
|
+
def _compute(self, y_true, y_pred):
|
47
|
+
"""
|
48
|
+
Calculate the total detected in range score.
|
49
|
+
|
50
|
+
Parameters:
|
51
|
+
y_true (np.array):
|
52
|
+
The ground truth binary labels for the time series data.
|
53
|
+
y_pred (np.array):
|
54
|
+
The predicted binary labels for the time series data.
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
float: The total detected in range score.
|
58
|
+
"""
|
59
|
+
|
60
|
+
if np.sum(y_pred) == 0:
|
61
|
+
return 0
|
62
|
+
|
63
|
+
k = self.params["k"]
|
64
|
+
em, da, ma, _ = counting_method(y_true, y_pred, k)
|
65
|
+
return (em + da) / (em + da + ma)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
from ....base.Metric import Metric
|
2
|
+
import numpy as np
|
3
|
+
from ....utils.functions_counting_metrics import counting_method
|
4
|
+
|
5
|
+
class WeightedDetectionDifference(Metric):
|
6
|
+
"""
|
7
|
+
Calculate weighted detection difference for anomaly detection in time series.
|
8
|
+
|
9
|
+
For each true anomaly segment, each point in the segment is assigned a weight based on a
|
10
|
+
Gaussian function centered at the segment’s midpoint: points closer to the center receive higher
|
11
|
+
weights, which decay with distance according to the standard deviation sigma. These weights form
|
12
|
+
the basis for scoring both correct detections and false alarms.
|
13
|
+
|
14
|
+
WS (Weighted Sum) is defined as the sum of Gaussian weights for all predicted anomaly points that
|
15
|
+
fall within any true anomaly segment (extended by delta time steps at the ends).
|
16
|
+
WF (False Alarm Weight) is the sum of Gaussian weights for all predicted anomaly points that do
|
17
|
+
not overlap any true anomaly segment (within the same extension).
|
18
|
+
|
19
|
+
The final score is:
|
20
|
+
|
21
|
+
.. math::
|
22
|
+
\\text{WDD} = \\text{WS} - \\text{WF} \\cdot \\text{FA}
|
23
|
+
|
24
|
+
Where:
|
25
|
+
|
26
|
+
- WS:
|
27
|
+
Sum of Gaussian weights for all predicted anomaly points that fall
|
28
|
+
within any true anomaly segment (extended by delta time steps at the ends).
|
29
|
+
- WF:
|
30
|
+
Sum of Gaussian weights for all predicted anomaly points that do not
|
31
|
+
overlap any true anomaly segment (within the same extension).
|
32
|
+
- FA (False Anomaly):
|
33
|
+
Number of predicted anomaly segments that do not overlap any true anomaly segment
|
34
|
+
even within a k-step tolerance window around true points.
|
35
|
+
|
36
|
+
For more information, see the original paper:
|
37
|
+
https://acta.sapientia.ro/content/docs/evaluation-metrics-for-anomaly-detection.pdf
|
38
|
+
|
39
|
+
Parameters:
|
40
|
+
k (int):
|
41
|
+
The maximum number of time steps within which an anomaly must be predicted to be considered detected.
|
42
|
+
"""
|
43
|
+
name = "wdd"
|
44
|
+
binary_prediction = True
|
45
|
+
param_schema = {
|
46
|
+
"k": {
|
47
|
+
"default": 5,
|
48
|
+
"type": int
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
def __init__(self, **kwargs):
|
53
|
+
super().__init__(name="wdd", **kwargs)
|
54
|
+
|
55
|
+
def _compute(self, y_true, y_pred):
|
56
|
+
"""
|
57
|
+
Calculate the weighted detection difference.
|
58
|
+
|
59
|
+
Parameters:
|
60
|
+
y_true (np.array):
|
61
|
+
The ground truth binary labels for the time series data.
|
62
|
+
y_pred (np.array):
|
63
|
+
The predicted binary labels for the time series data.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
float: The weighted detection difference.
|
67
|
+
"""
|
68
|
+
|
69
|
+
if np.sum(y_pred) == 0:
|
70
|
+
return 0
|
71
|
+
|
72
|
+
def gaussian(dt, tmax):
|
73
|
+
if dt < tmax:
|
74
|
+
return 1 - dt / tmax
|
75
|
+
else:
|
76
|
+
return -1
|
77
|
+
|
78
|
+
tmax = len(y_true)
|
79
|
+
ones_indices = np.where(y_true == 1)[0]
|
80
|
+
y_modified = y_true.astype(float).copy()
|
81
|
+
|
82
|
+
for i in range(len(y_true)):
|
83
|
+
if y_true[i] == 0:
|
84
|
+
dt = np.min(np.abs(ones_indices - i)) if len(ones_indices) > 0 else tmax
|
85
|
+
y_modified[i] = gaussian(dt, tmax)
|
86
|
+
|
87
|
+
ws = 0
|
88
|
+
wf = 0
|
89
|
+
for i in range(len(y_pred)):
|
90
|
+
if y_pred[i] != 1:
|
91
|
+
ws += y_modified[i]
|
92
|
+
else:
|
93
|
+
wf += y_modified[i]
|
94
|
+
|
95
|
+
_, _, _, fa = counting_method(y_true, y_pred, int(self.params["k"]))
|
96
|
+
|
97
|
+
return ws - wf * fa
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from .AverageDetectionCount import AverageDetectionCount
|
2
|
+
from .DetectionAccuracyInRange import DetectionAccuracyInRange
|
3
|
+
from .PointadjustedAtKFScore import PointadjustedAtKFScore
|
4
|
+
from .TimeseriesAwareFScore import TimeseriesAwareFScore
|
5
|
+
from .TotalDetectedInRange import TotalDetectedInRange
|
6
|
+
from .WeightedDetectionDifference import WeightedDetectionDifference
|
7
|
+
__all__ = ['AverageDetectionCount',
|
8
|
+
'DetectionAccuracyInRange',
|
9
|
+
'PointadjustedAtKFScore',
|
10
|
+
'TimeseriesAwareFScore',
|
11
|
+
'WeightedDetectionDifference',
|
12
|
+
'TotalDetectedInRange']
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from ....base.Metric import Metric
|
2
|
+
from ....utils.functions_conversion import full_series_to_segmentwise, full_series_to_pointwise
|
3
|
+
|
4
|
+
class AbsoluteDetectionDistance(Metric):
|
5
|
+
"""
|
6
|
+
Calculate absolute detection distance for anomaly detection in time series.
|
7
|
+
|
8
|
+
This metric computes, for each predicted anomaly point that overlaps a ground-truth anomaly segment,
|
9
|
+
the relative distance from that point to the temporal center of the corresponding segment. It then sums all
|
10
|
+
those distances and divides by the total number of such matching predicted points, yielding the
|
11
|
+
mean distance to segment centers for correctly detected points.
|
12
|
+
|
13
|
+
For more information, see the original paper:
|
14
|
+
https://ceur-ws.org/Vol-1226/paper31.pdf
|
15
|
+
"""
|
16
|
+
name = "add"
|
17
|
+
binary_prediction = True
|
18
|
+
param_schema = {}
|
19
|
+
def __init__(self, **kwargs):
|
20
|
+
super().__init__(name="add", **kwargs)
|
21
|
+
|
22
|
+
def _compute(self, y_true, y_pred):
|
23
|
+
"""
|
24
|
+
Calculate the absolute detection distance.
|
25
|
+
|
26
|
+
Parameters:
|
27
|
+
y_true (np.array):
|
28
|
+
The ground truth binary labels for the time series data.
|
29
|
+
y_pred (np.array):
|
30
|
+
The predicted binary labels for the time series data.
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
float: The absolute detection distance.
|
34
|
+
"""
|
35
|
+
|
36
|
+
azs = full_series_to_segmentwise(y_true)
|
37
|
+
a_points = full_series_to_pointwise(y_pred)
|
38
|
+
if len(a_points) == 0:
|
39
|
+
return 0
|
40
|
+
|
41
|
+
distance = 0
|
42
|
+
for az in azs:
|
43
|
+
for ap in a_points:
|
44
|
+
if az[0] <= ap <= az[1]:
|
45
|
+
center = int((az[0] + az[1]) / 2)
|
46
|
+
distance += abs(ap - center) / max(1, center)
|
47
|
+
|
48
|
+
return distance / len(a_points)
|