test_pyg_scan.py
1 # -*- coding: utf-8 -*- 2 """Tests for SCAN graph anomaly detector.""" 3 4 import os 5 import sys 6 import unittest 7 8 import numpy as np 9 10 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 11 12 from pyod.utils.data import generate_graph_data 13 14 try: 15 import torch 16 from torch_geometric.data import Data 17 from pyod.models.pyg_scan import SCAN 18 HAS_PYG = True 19 except ImportError: 20 HAS_PYG = False 21 22 23 @unittest.skipUnless(HAS_PYG, "torch_geometric not installed") 24 class TestSCAN(unittest.TestCase): 25 def setUp(self): 26 self.X, self.edge_index, self.y = generate_graph_data( 27 n_nodes=200, n_features=16, contamination=0.1, 28 random_state=42) 29 self.data = Data( 30 x=torch.FloatTensor(self.X), 31 edge_index=torch.LongTensor(self.edge_index)) 32 33 def test_fit_pyg_data(self): 34 clf = SCAN(epsilon=0.5, mu=2, contamination=0.1) 35 clf.fit(self.data) 36 assert hasattr(clf, 'decision_scores_') 37 assert hasattr(clf, 'labels_') 38 assert hasattr(clf, 'threshold_') 39 assert len(clf.decision_scores_) == 200 40 assert len(clf.labels_) == 200 41 42 def test_fit_numpy(self): 43 clf = SCAN(epsilon=0.5, mu=2, contamination=0.1) 44 clf.fit(self.X, edge_index=self.edge_index) 45 assert len(clf.decision_scores_) == 200 46 47 def test_scores_nonnegative(self): 48 clf = SCAN(contamination=0.1) 49 clf.fit(self.data) 50 assert np.all(clf.decision_scores_ >= 0) 51 52 def test_transductive_no_decision_function(self): 53 clf = SCAN() 54 clf.fit(self.data) 55 with self.assertRaises(NotImplementedError): 56 clf.decision_function(self.data) 57 58 def test_transductive_no_predict(self): 59 clf = SCAN() 60 clf.fit(self.data) 61 with self.assertRaises(NotImplementedError): 62 clf.predict(self.data) 63 64 def test_structure_only(self): 65 """SCAN works without node features.""" 66 data_no_feat = Data( 67 edge_index=torch.LongTensor(self.edge_index), 68 num_nodes=200) 69 clf = SCAN(contamination=0.1) 70 clf.fit(data_no_feat) 71 assert len(clf.decision_scores_) == 200 72 73 def test_empty_graph(self): 74 """Isolated nodes with no edges all score 1.0.""" 75 data_empty = Data( 76 edge_index=torch.zeros(2, 0, dtype=torch.long), 77 num_nodes=10) 78 clf = SCAN(contamination=0.1) 79 clf.fit(data_empty) 80 assert len(clf.decision_scores_) == 10 81 assert np.all(clf.decision_scores_ == 1.0) 82 83 def test_epsilon_mu_affect_scores(self): 84 """Different epsilon/mu values produce different scores.""" 85 clf1 = SCAN(epsilon=0.3, mu=1, contamination=0.1) 86 clf1.fit(self.data) 87 clf2 = SCAN(epsilon=0.9, mu=5, contamination=0.1) 88 clf2.fit(self.data) 89 assert not np.allclose( 90 clf1.decision_scores_, clf2.decision_scores_) 91 92 93 if __name__ == '__main__': 94 unittest.main()