"""Tests for eval/ts_metrics.py (T5.2).""" import numpy as np import pytest from tsmm.eval.ts_metrics import ( affiliation_f1, auc_pr, point_f1, point_f1_with_pa, vus_pr, ) def labels(segs, T=100): y = np.zeros(T, dtype=int) for a, b in segs: y[a:b + 1] = 1 return y class TestPointF1: def test_perfect(self): y = labels([(20, 40)], T=100) s = np.zeros(100); s[20:41] = 1.0 f1, prec, rec = point_f1(s, y) assert f1 == pytest.approx(1.0, abs=1e-6) def test_no_overlap(self): y = labels([(20, 40)]) s = np.zeros(100); s[60:70] = 1.0 f1, _, _ = point_f1(s, y) assert f1 == pytest.approx(0.0, abs=1e-6) def test_partial(self): y = labels([(20, 40)]) s = np.zeros(100); s[30:41] = 0.9 f1, prec, rec = point_f1(s, y, threshold=0.5) # predicted 11 anomalous all in true region: precision 1, recall 11/21 assert prec == pytest.approx(1.0, abs=1e-6) assert rec == pytest.approx(11 / 21, abs=1e-3) class TestAUCPR: def test_perfect_ranking(self): y = labels([(10, 20)]) s = np.zeros(100); s[10:21] = 1.0 assert auc_pr(s, y) == pytest.approx(1.0, abs=1e-6) def test_inverse_ranking_low(self): y = labels([(10, 20)]) s = np.ones(100); s[10:21] = 0.0 # anomalies score LOW assert auc_pr(s, y) < 0.5 class TestPAF1: def test_pa_inflates(self): # PA: if ANY predicted point is in the true anomaly, the whole segment # counts as caught → much higher F1 than point-wise y = labels([(20, 40)]) s = np.zeros(100); s[40] = 1.0 # single overlap point f1_pa, _, _ = point_f1_with_pa(s, y) f1_pt, _, _ = point_f1(s, y) assert f1_pa > f1_pt class TestAffiliationF1: def test_perfect(self): y = labels([(20, 40)]) s = np.zeros(100); s[20:41] = 1.0 f1, _, _ = affiliation_f1(s, y) assert f1 == pytest.approx(1.0, abs=1e-6) def test_far_predictions_lower(self): y = labels([(20, 40)]) s_near = np.zeros(100); s_near[41:45] = 1.0 s_far = np.zeros(100); s_far[90:95] = 1.0 f1_near, _, _ = affiliation_f1(s_near, y) f1_far, _, _ = affiliation_f1(s_far, y) assert f1_near > f1_far class TestVUSPR: def test_perfect_high(self): y = labels([(20, 40)]) s = np.zeros(100); s[20:41] = 1.0 v = vus_pr(s, y) assert v == pytest.approx(1.0, abs=1e-3) def test_random_scores_moderate(self): rng = np.random.default_rng(0) y = labels([(20, 40)]) s = rng.random(100) v = vus_pr(s, y) # random ranking → VUS-PR around the anomaly prevalence (~0.21) assert 0.0 < v < 0.6 def test_all_report_not_inflated(self): # spec REQUIREMENT: "全报异常" trivial baseline must NOT have inflated VUS-PR y = labels([(20, 40)], T=100) s = np.ones(100) # report everything as anomalous v = vus_pr(s, y) # prevalence = 21/100; all-ones gives AUC-PR == prevalence everywhere assert v == pytest.approx(21 / 100, abs=1e-3) def test_returns_scalar(self): y = labels([(5, 10)]) s = np.random.default_rng(1).random(100) v = vus_pr(s, y) assert np.isscalar(v) or v.shape == ()