# Source code for gluonts.nursery.anomaly_detection.supervised_metrics._segment_precision_recall

# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# You may not use this file except in compliance with the License.
# A copy of the License is located at
#
#
# or in the "license" file accompanying this file. This file is distributed
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied. See the License for the specific language governing
# permissions and limitations under the License.

from typing import List, Tuple

import numpy as np

from .utils import labels_to_ranges, range_overlap

[docs]def segment_precision_recall(
real_ranges: List[range], pred_ranges: List[range]
) -> Tuple[float, float]:
"""
Segment based metric is less lenient than the range based metric.

This metric counts

* a ground truth anomaly range as true-positive as long as there is an overlapping predicted
anomaly range; it does not penalize the position of the overlap. If there is no overlapping predicted range
for given ground truth range, then it counts as one false-negative irrespective of its size.

* a predicted anomaly range as false positive if it has a nonempty overlap with a "normal" range.

Parameters
----------
real_ranges
List of ranges corresponding to ground truth anomalies.
pred_ranges
List of predicted anomaly ranges.

Returns
-------
Tuple containing segment based precision and recall.

"""
# Compute the number of true positives and false negatives by going over each of the ground truth anomaly ranges
tp, fn = 0, 0
for tr in real_ranges:
tp_found = False
for pr in pred_ranges:
if range_overlap(tr, pr):
tp_found = True
break

if tp_found:
tp += 1
else:
fn += 1

# Deduce the ranges corresponding to normal behavior
if real_ranges and real_ranges[-1]:
max_ix_real_range = real_ranges[-1][-1]
else:
max_ix_real_range = -1

if pred_ranges and pred_ranges[-1]:
max_ix_pred_range = pred_ranges[-1][-1]
else:
max_ix_pred_range = -1

num_time_points = max(max_ix_real_range, max_ix_pred_range) + 1
normal_ind = np.array([True] * num_time_points)
for tr in real_ranges:
normal_ind[tr] = False
normal_ranges = labels_to_ranges(normal_ind)

# Compute the number of false positives and true negatives by going over each of the ground truth normal ranges
fp, tn = 0, 0
for nr in normal_ranges:
fp_found = False
for pr in pred_ranges:
if range_overlap(nr, pr):
fp_found = True
fp += 1

if not fp_found:
tn += 1

# avoid division by zero.
return tp / max(tp + fp, 1), tp / max(tp + fn, 1)