/ pyod / test / test_pyg_scan.py
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()