scikit-network 0.31.0__cp310-cp310-win_amd64.whl → 0.33.0__cp310-cp310-win_amd64.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.

Potentially problematic release.


This version of scikit-network might be problematic. Click here for more details.

Files changed (126) hide show
  1. {scikit_network-0.31.0.dist-info → scikit_network-0.33.0.dist-info}/AUTHORS.rst +3 -1
  2. {scikit_network-0.31.0.dist-info → scikit_network-0.33.0.dist-info}/METADATA +27 -5
  3. scikit_network-0.33.0.dist-info/RECORD +228 -0
  4. {scikit_network-0.31.0.dist-info → scikit_network-0.33.0.dist-info}/WHEEL +1 -1
  5. sknetwork/__init__.py +1 -1
  6. sknetwork/classification/base.py +1 -1
  7. sknetwork/classification/base_rank.py +3 -3
  8. sknetwork/classification/diffusion.py +25 -16
  9. sknetwork/classification/knn.py +23 -16
  10. sknetwork/classification/metrics.py +4 -4
  11. sknetwork/classification/pagerank.py +12 -8
  12. sknetwork/classification/propagation.py +25 -17
  13. sknetwork/classification/tests/test_diffusion.py +10 -0
  14. sknetwork/classification/vote.cp310-win_amd64.pyd +0 -0
  15. sknetwork/classification/vote.cpp +14549 -8668
  16. sknetwork/clustering/__init__.py +3 -1
  17. sknetwork/clustering/base.py +1 -1
  18. sknetwork/clustering/kcenters.py +253 -0
  19. sknetwork/clustering/leiden.py +242 -0
  20. sknetwork/clustering/leiden_core.cp310-win_amd64.pyd +0 -0
  21. sknetwork/clustering/leiden_core.cpp +31564 -0
  22. sknetwork/clustering/leiden_core.pyx +124 -0
  23. sknetwork/clustering/louvain.py +118 -83
  24. sknetwork/clustering/louvain_core.cp310-win_amd64.pyd +0 -0
  25. sknetwork/clustering/louvain_core.cpp +21876 -16332
  26. sknetwork/clustering/louvain_core.pyx +86 -94
  27. sknetwork/clustering/postprocess.py +2 -2
  28. sknetwork/clustering/propagation_clustering.py +4 -4
  29. sknetwork/clustering/tests/test_API.py +7 -3
  30. sknetwork/clustering/tests/test_kcenters.py +60 -0
  31. sknetwork/clustering/tests/test_leiden.py +34 -0
  32. sknetwork/clustering/tests/test_louvain.py +2 -3
  33. sknetwork/data/__init__.py +1 -1
  34. sknetwork/data/base.py +7 -2
  35. sknetwork/data/load.py +20 -25
  36. sknetwork/data/models.py +15 -15
  37. sknetwork/data/parse.py +57 -34
  38. sknetwork/data/tests/test_API.py +3 -3
  39. sknetwork/data/tests/test_base.py +2 -2
  40. sknetwork/data/tests/test_parse.py +9 -12
  41. sknetwork/data/tests/test_toy_graphs.py +33 -33
  42. sknetwork/data/toy_graphs.py +35 -43
  43. sknetwork/embedding/__init__.py +0 -1
  44. sknetwork/embedding/base.py +23 -19
  45. sknetwork/embedding/force_atlas.py +3 -2
  46. sknetwork/embedding/louvain_embedding.py +1 -27
  47. sknetwork/embedding/random_projection.py +5 -3
  48. sknetwork/embedding/spectral.py +0 -73
  49. sknetwork/embedding/svd.py +0 -4
  50. sknetwork/embedding/tests/test_API.py +4 -28
  51. sknetwork/embedding/tests/test_louvain_embedding.py +13 -13
  52. sknetwork/embedding/tests/test_spectral.py +2 -5
  53. sknetwork/embedding/tests/test_svd.py +7 -1
  54. sknetwork/gnn/base_layer.py +3 -3
  55. sknetwork/gnn/gnn_classifier.py +41 -87
  56. sknetwork/gnn/layer.py +1 -1
  57. sknetwork/gnn/loss.py +1 -1
  58. sknetwork/gnn/optimizer.py +4 -3
  59. sknetwork/gnn/tests/test_base_layer.py +4 -4
  60. sknetwork/gnn/tests/test_gnn_classifier.py +12 -39
  61. sknetwork/gnn/utils.py +8 -8
  62. sknetwork/hierarchy/base.py +27 -0
  63. sknetwork/hierarchy/louvain_hierarchy.py +55 -47
  64. sknetwork/hierarchy/paris.cp310-win_amd64.pyd +0 -0
  65. sknetwork/hierarchy/paris.cpp +27667 -20915
  66. sknetwork/hierarchy/paris.pyx +11 -10
  67. sknetwork/hierarchy/postprocess.py +16 -16
  68. sknetwork/hierarchy/tests/test_algos.py +5 -0
  69. sknetwork/hierarchy/tests/test_metrics.py +4 -4
  70. sknetwork/linalg/__init__.py +1 -1
  71. sknetwork/linalg/diteration.cp310-win_amd64.pyd +0 -0
  72. sknetwork/linalg/diteration.cpp +13916 -8050
  73. sknetwork/linalg/{normalization.py → normalizer.py} +17 -14
  74. sknetwork/linalg/operators.py +1 -1
  75. sknetwork/linalg/ppr_solver.py +1 -1
  76. sknetwork/linalg/push.cp310-win_amd64.pyd +0 -0
  77. sknetwork/linalg/push.cpp +23187 -16973
  78. sknetwork/linalg/tests/test_normalization.py +3 -7
  79. sknetwork/linalg/tests/test_operators.py +2 -6
  80. sknetwork/linalg/tests/test_ppr.py +1 -1
  81. sknetwork/linkpred/base.py +12 -1
  82. sknetwork/linkpred/nn.py +6 -6
  83. sknetwork/path/distances.py +11 -4
  84. sknetwork/path/shortest_path.py +1 -1
  85. sknetwork/path/tests/test_distances.py +7 -0
  86. sknetwork/path/tests/test_search.py +2 -2
  87. sknetwork/ranking/base.py +11 -6
  88. sknetwork/ranking/betweenness.cp310-win_amd64.pyd +0 -0
  89. sknetwork/ranking/betweenness.cpp +5256 -2190
  90. sknetwork/ranking/pagerank.py +13 -12
  91. sknetwork/ranking/tests/test_API.py +0 -2
  92. sknetwork/ranking/tests/test_betweenness.py +1 -1
  93. sknetwork/ranking/tests/test_pagerank.py +11 -5
  94. sknetwork/regression/base.py +18 -1
  95. sknetwork/regression/diffusion.py +30 -14
  96. sknetwork/regression/tests/test_diffusion.py +8 -0
  97. sknetwork/topology/__init__.py +3 -1
  98. sknetwork/topology/cliques.cp310-win_amd64.pyd +0 -0
  99. sknetwork/topology/cliques.cpp +23528 -16848
  100. sknetwork/topology/core.cp310-win_amd64.pyd +0 -0
  101. sknetwork/topology/core.cpp +22849 -16581
  102. sknetwork/topology/cycles.py +243 -0
  103. sknetwork/topology/minheap.cp310-win_amd64.pyd +0 -0
  104. sknetwork/topology/minheap.cpp +19495 -13469
  105. sknetwork/topology/structure.py +2 -42
  106. sknetwork/topology/tests/test_cycles.py +65 -0
  107. sknetwork/topology/tests/test_structure.py +2 -16
  108. sknetwork/topology/triangles.cp310-win_amd64.pyd +0 -0
  109. sknetwork/topology/triangles.cpp +5283 -1397
  110. sknetwork/topology/triangles.pyx +7 -4
  111. sknetwork/topology/weisfeiler_lehman_core.cp310-win_amd64.pyd +0 -0
  112. sknetwork/topology/weisfeiler_lehman_core.cpp +14781 -8915
  113. sknetwork/utils/__init__.py +1 -1
  114. sknetwork/utils/format.py +1 -1
  115. sknetwork/utils/membership.py +2 -2
  116. sknetwork/utils/values.py +5 -3
  117. sknetwork/visualization/__init__.py +2 -2
  118. sknetwork/visualization/dendrograms.py +55 -7
  119. sknetwork/visualization/graphs.py +261 -44
  120. sknetwork/visualization/tests/test_dendrograms.py +9 -9
  121. sknetwork/visualization/tests/test_graphs.py +63 -57
  122. scikit_network-0.31.0.dist-info/RECORD +0 -221
  123. sknetwork/embedding/louvain_hierarchy.py +0 -142
  124. sknetwork/embedding/tests/test_louvain_hierarchy.py +0 -19
  125. {scikit_network-0.31.0.dist-info → scikit_network-0.33.0.dist-info}/LICENSE +0 -0
  126. {scikit_network-0.31.0.dist-info → scikit_network-0.33.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created in February 2024
5
+ @author: Thomas Bonald <thomas.bonald@telecom-paris.fr>
6
+ @author: Yiwen Peng <yiwen.peng@telecom-paris.fr>
7
+ """
8
+ from typing import Tuple, Optional, Union, List
9
+
10
+ import numpy as np
11
+ from scipy import sparse
12
+
13
+ from sknetwork.utils.check import is_symmetric, check_format
14
+ from sknetwork.utils.format import get_adjacency
15
+ from sknetwork.path import get_distances
16
+
17
+
18
+ def is_acyclic(adjacency: sparse.csr_matrix, directed: Optional[bool] = None) -> bool:
19
+ """Check whether a graph has no cycle.
20
+
21
+ Parameters
22
+ ----------
23
+ adjacency:
24
+ Adjacency matrix of the graph.
25
+ directed:
26
+ Whether to consider the graph as directed (inferred if not specified).
27
+ Returns
28
+ -------
29
+ is_acyclic : bool
30
+ A boolean with value True if the graph has no cycle and False otherwise.
31
+
32
+ Example
33
+ -------
34
+ >>> from sknetwork.topology import is_acyclic
35
+ >>> from sknetwork.data import star, grid
36
+ >>> is_acyclic(star())
37
+ True
38
+ >>> is_acyclic(grid())
39
+ False
40
+ """
41
+ if directed is False:
42
+ # the graph must be undirected
43
+ if not is_symmetric(adjacency):
44
+ raise ValueError("The adjacency matrix is not symmetric. The parameter 'directed' must be True.")
45
+ elif directed is None:
46
+ # if not specified, infer from the graph
47
+ directed = not is_symmetric(adjacency)
48
+ has_loops = (adjacency.diagonal() > 0).any()
49
+ if has_loops:
50
+ return False
51
+ else:
52
+ n_cc = sparse.csgraph.connected_components(adjacency, directed, connection='strong', return_labels=False)
53
+ n_nodes = adjacency.shape[0]
54
+ if directed:
55
+ return n_cc == n_nodes
56
+ else:
57
+ n_edges = adjacency.nnz // 2
58
+ return n_cc == n_nodes - n_edges
59
+
60
+
61
+ def get_cycles(adjacency: sparse.csr_matrix, directed: Optional[bool] = None) -> List[List[int]]:
62
+ """Get all possible cycles of a graph.
63
+
64
+ Parameters
65
+ ----------
66
+ adjacency :
67
+ Adjacency matrix of the graph.
68
+ directed :
69
+ Whether to consider the graph as directed (inferred if not specified).
70
+
71
+ Returns
72
+ -------
73
+ cycles : list
74
+ List of cycles, each cycle represented as a list of nodes.
75
+
76
+ Example
77
+ -------
78
+ >>> from sknetwork.topology import get_cycles
79
+ >>> from sknetwork.data import cyclic_digraph
80
+ >>> graph = cyclic_digraph(4, metadata=True)
81
+ >>> get_cycles(graph.adjacency, directed=True)
82
+ [[0, 1, 2, 3]]
83
+ """
84
+
85
+ if directed is False:
86
+ # the graph must be undirected
87
+ if not is_symmetric(adjacency):
88
+ raise ValueError("The adjacency matrix is not symmetric. The parameter 'directed' must be True.")
89
+ elif directed is None:
90
+ # if not specified, infer from the graph
91
+ directed = not is_symmetric(adjacency)
92
+
93
+ cycles = []
94
+ n_nodes = adjacency.shape[0]
95
+ self_loops = np.argwhere(adjacency.diagonal() > 0).ravel()
96
+ if len(self_loops) > 0:
97
+ # add self-loops as cycles
98
+ for node in self_loops:
99
+ cycles.append([node])
100
+
101
+ # check if the graph is acyclic
102
+ n_cc, cc_labels = sparse.csgraph.connected_components(adjacency, directed, connection='strong', return_labels=True)
103
+ if directed and n_cc == n_nodes:
104
+ return cycles
105
+ elif not directed:
106
+ # acyclic undirected graph
107
+ n_edges = adjacency.nnz // 2
108
+ if n_cc == n_nodes - n_edges:
109
+ return cycles
110
+
111
+ # locate possible cycles
112
+ labels, counts = np.unique(cc_labels, return_counts=True)
113
+ if directed:
114
+ labels = labels[counts > 1]
115
+ cycle_starts = [np.argwhere(cc_labels == label).ravel()[0] for label in labels]
116
+
117
+ # find cycles for indicated nodes (depth-first traversal)
118
+ for start_node in cycle_starts:
119
+ stack = [(start_node, [start_node])]
120
+ while stack:
121
+ current_node, path = stack.pop()
122
+ for neighbor in adjacency.indices[adjacency.indptr[current_node]:adjacency.indptr[current_node+1]]:
123
+ if not directed and len(path) > 1 and neighbor == path[-2]:
124
+ # ignore the inverse edge (back move) in undirected graph
125
+ continue
126
+ if neighbor in path:
127
+ cycles.append(path[path.index(neighbor):])
128
+ else:
129
+ stack.append((neighbor, path + [neighbor]))
130
+
131
+ # remove duplicates
132
+ visited_cycles, unique_cycles = set(), []
133
+ for cycle in cycles:
134
+ candidate = np.roll(cycle, -cycle.index(min(cycle)))
135
+ current_cycle = tuple(candidate)
136
+ if not directed:
137
+ current_cycle = tuple(np.sort(candidate))
138
+ if current_cycle not in visited_cycles:
139
+ unique_cycles.append(list(candidate))
140
+ visited_cycles.add(current_cycle)
141
+
142
+ return unique_cycles
143
+
144
+
145
+ def break_cycles(adjacency: sparse.csr_matrix, root: Union[int, List[int]],
146
+ directed: Optional[bool] = None) -> sparse.csr_matrix:
147
+ """Break cycles of a graph from given roots.
148
+
149
+ Parameters
150
+ ----------
151
+ adjacency :
152
+ Adjacency matrix of the graph.
153
+ root :
154
+ The root node or list of root nodes to break cycles from.
155
+ directed :
156
+ Whether to consider the graph as directed (inferred if not specified).
157
+
158
+ Returns
159
+ -------
160
+ adjacency : sparse.csr_matrix
161
+ Adjacency matrix of the acyclic graph.
162
+
163
+ Example
164
+ -------
165
+ >>> from sknetwork.topology import break_cycles, is_acyclic
166
+ >>> from sknetwork.data import cyclic_digraph
167
+ >>> adjacency = cyclic_digraph(4)
168
+ >>> dag = break_cycles(adjacency, root=0, directed=True)
169
+ >>> is_acyclic(dag, directed=True)
170
+ True
171
+ """
172
+
173
+ if is_acyclic(adjacency, directed):
174
+ return adjacency
175
+
176
+ if root is None:
177
+ raise ValueError("The parameter root must be specified.")
178
+ else:
179
+ out_degree = len(adjacency[root].indices)
180
+ if out_degree == 0:
181
+ raise ValueError("Invalid root node. The root must have at least one outgoing edge.")
182
+ if isinstance(root, int):
183
+ root = [root]
184
+
185
+ if directed is False:
186
+ # the graph must be undirected
187
+ if not is_symmetric(adjacency):
188
+ raise ValueError("The adjacency matrix is not symmetric. The parameter 'directed' must be True.")
189
+ elif directed is None:
190
+ # if not specified, infer from the graph
191
+ directed = not is_symmetric(adjacency)
192
+
193
+ # break self-loops
194
+ adjacency = (adjacency - sparse.diags(adjacency.diagonal().astype(int), format='csr')).astype(int)
195
+
196
+ if directed:
197
+ # break cycles from the cycle node closest to the root
198
+ _, cc_labels = sparse.csgraph.connected_components(adjacency, directed, connection='strong', return_labels=True)
199
+ labels, counts = np.unique(cc_labels, return_counts=True)
200
+ cycle_labels = labels[counts > 1]
201
+ distances = get_distances(adjacency, source=root)
202
+
203
+ for label in cycle_labels:
204
+ cycle_nodes = np.argwhere(cc_labels == label).ravel()
205
+ roots_ix = np.argwhere(distances[cycle_nodes] == min(distances[cycle_nodes])).ravel()
206
+ subroots = set(cycle_nodes[roots_ix])
207
+ stack = [(subroot, [subroot]) for subroot in subroots]
208
+ # break cycles using depth-first traversal
209
+ while stack:
210
+ current_node, path = stack.pop()
211
+ # check if the edge still exists
212
+ if len(path) > 1 and adjacency[path[-2], path[-1]] <= 0:
213
+ continue
214
+ neighbors = adjacency.indices[adjacency.indptr[current_node]:adjacency.indptr[current_node+1]]
215
+ cycle_neighbors = set(neighbors) & set(cycle_nodes)
216
+ for neighbor in cycle_neighbors:
217
+ if neighbor in path:
218
+ adjacency[current_node, neighbor] = 0
219
+ adjacency.eliminate_zeros()
220
+ else:
221
+ stack.append((neighbor, path + [neighbor]))
222
+ else:
223
+ # break cycles from given roots for undirected graphs
224
+ for start_node in root:
225
+ stack = [(start_node, [start_node])]
226
+ # break cycles using depth-first traversal
227
+ while stack:
228
+ current_node, path = stack.pop()
229
+ # check if the edge still exists
230
+ if len(path) > 1 and adjacency[path[-2], path[-1]] <= 0:
231
+ continue
232
+ neighbors = list(adjacency.indices[adjacency.indptr[current_node]:adjacency.indptr[current_node+1]])
233
+ for neighbor in neighbors:
234
+ if len(path) > 1 and neighbor == path[-2]:
235
+ continue
236
+ if neighbor in path:
237
+ adjacency[current_node, neighbor] = 0
238
+ adjacency[neighbor, current_node] = 0
239
+ adjacency.eliminate_zeros()
240
+ else:
241
+ stack.append((neighbor, path + [neighbor]))
242
+
243
+ return adjacency