risk-network 0.0.8b18__py3-none-any.whl → 0.0.9b26__py3-none-any.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.
Files changed (50) hide show
  1. risk/__init__.py +2 -2
  2. risk/annotations/__init__.py +2 -2
  3. risk/annotations/annotations.py +133 -72
  4. risk/annotations/io.py +50 -34
  5. risk/log/__init__.py +4 -2
  6. risk/log/{config.py → console.py} +5 -3
  7. risk/log/{params.py → parameters.py} +21 -46
  8. risk/neighborhoods/__init__.py +3 -5
  9. risk/neighborhoods/api.py +446 -0
  10. risk/neighborhoods/community.py +281 -96
  11. risk/neighborhoods/domains.py +92 -38
  12. risk/neighborhoods/neighborhoods.py +210 -149
  13. risk/network/__init__.py +1 -3
  14. risk/network/geometry.py +69 -58
  15. risk/network/graph/__init__.py +6 -0
  16. risk/network/graph/api.py +194 -0
  17. risk/network/graph/network.py +269 -0
  18. risk/network/graph/summary.py +254 -0
  19. risk/network/io.py +58 -48
  20. risk/network/plotter/__init__.py +6 -0
  21. risk/network/plotter/api.py +54 -0
  22. risk/network/{plot → plotter}/canvas.py +80 -26
  23. risk/network/{plot → plotter}/contour.py +43 -34
  24. risk/network/{plot → plotter}/labels.py +123 -113
  25. risk/network/plotter/network.py +424 -0
  26. risk/network/plotter/utils/colors.py +416 -0
  27. risk/network/plotter/utils/layout.py +94 -0
  28. risk/risk.py +11 -469
  29. risk/stats/__init__.py +8 -4
  30. risk/stats/binom.py +51 -0
  31. risk/stats/chi2.py +69 -0
  32. risk/stats/hypergeom.py +28 -18
  33. risk/stats/permutation/__init__.py +1 -1
  34. risk/stats/permutation/permutation.py +45 -39
  35. risk/stats/permutation/test_functions.py +25 -17
  36. risk/stats/poisson.py +17 -11
  37. risk/stats/stats.py +20 -16
  38. risk/stats/zscore.py +68 -0
  39. {risk_network-0.0.8b18.dist-info → risk_network-0.0.9b26.dist-info}/METADATA +9 -5
  40. risk_network-0.0.9b26.dist-info/RECORD +44 -0
  41. {risk_network-0.0.8b18.dist-info → risk_network-0.0.9b26.dist-info}/WHEEL +1 -1
  42. risk/network/graph.py +0 -159
  43. risk/network/plot/__init__.py +0 -6
  44. risk/network/plot/network.py +0 -282
  45. risk/network/plot/plotter.py +0 -137
  46. risk/network/plot/utils/color.py +0 -353
  47. risk/network/plot/utils/layout.py +0 -53
  48. risk_network-0.0.8b18.dist-info/RECORD +0 -37
  49. {risk_network-0.0.8b18.dist-info → risk_network-0.0.9b26.dist-info}/LICENSE +0 -0
  50. {risk_network-0.0.8b18.dist-info → risk_network-0.0.9b26.dist-info}/top_level.txt +0 -0
@@ -4,179 +4,364 @@ risk/neighborhoods/community
4
4
  """
5
5
 
6
6
  import community as community_louvain
7
+ import igraph as ig
8
+ import markov_clustering as mc
7
9
  import networkx as nx
8
10
  import numpy as np
9
- import markov_clustering as mc
10
- from networkx.algorithms.community import asyn_lpa_communities, greedy_modularity_communities
11
+ from leidenalg import find_partition, RBConfigurationVertexPartition
12
+ from networkx.algorithms.community import greedy_modularity_communities
13
+
14
+ from risk.log import logger
11
15
 
12
16
 
13
- def calculate_greedy_modularity_neighborhoods(network: nx.Graph) -> np.ndarray:
17
+ def calculate_greedy_modularity_neighborhoods(
18
+ network: nx.Graph, fraction_shortest_edges: float = 1.0
19
+ ) -> np.ndarray:
14
20
  """Calculate neighborhoods using the Greedy Modularity method.
15
21
 
16
22
  Args:
17
- network (nx.Graph): The network graph to analyze for community structure.
23
+ network (nx.Graph): The network graph.
24
+ fraction_shortest_edges (float, optional): Shortest edge rank fraction threshold for creating
25
+ subgraphs before clustering.
18
26
 
19
27
  Returns:
20
28
  np.ndarray: A binary neighborhood matrix where nodes in the same community have 1, and others have 0.
21
29
  """
30
+ # Create a subgraph with the shortest edges based on the rank fraction
31
+ subnetwork = _create_percentile_limited_subgraph(
32
+ network, fraction_shortest_edges=fraction_shortest_edges
33
+ )
22
34
  # Detect communities using the Greedy Modularity method
23
- communities = greedy_modularity_communities(network)
24
- # Create a mapping from node to community
25
- community_dict = {node: idx for idx, community in enumerate(communities) for node in community}
35
+ communities = greedy_modularity_communities(subnetwork)
36
+ # Get the list of nodes in the original NetworkX graph
37
+ nodes = list(network.nodes())
38
+ node_index_map = {node: idx for idx, node in enumerate(nodes)}
26
39
  # Create a binary neighborhood matrix
27
- neighborhoods = np.zeros((network.number_of_nodes(), network.number_of_nodes()), dtype=int)
28
- node_index = {node: i for i, node in enumerate(network.nodes())}
29
- for node_i, community_i in community_dict.items():
30
- for node_j, community_j in community_dict.items():
31
- if community_i == community_j:
32
- neighborhoods[node_index[node_i], node_index[node_j]] = 1
40
+ num_nodes = len(nodes)
41
+ # Initialize neighborhoods with zeros and set self-self entries to 1
42
+ neighborhoods = np.eye(num_nodes, dtype=int)
43
+ # Fill in the neighborhood matrix for nodes in the same community
44
+ for community in communities:
45
+ # Iterate through all pairs of nodes in the same community
46
+ for node_i in community:
47
+ for node_j in community:
48
+ idx_i = node_index_map[node_i]
49
+ idx_j = node_index_map[node_j]
50
+ # Set them as neighbors (1) in the binary matrix
51
+ neighborhoods[idx_i, idx_j] = 1
33
52
 
34
53
  return neighborhoods
35
54
 
36
55
 
37
- def calculate_label_propagation_neighborhoods(network: nx.Graph) -> np.ndarray:
56
+ def calculate_label_propagation_neighborhoods(
57
+ network: nx.Graph, fraction_shortest_edges: float = 1.0
58
+ ) -> np.ndarray:
38
59
  """Apply Label Propagation to the network to detect communities.
39
60
 
40
61
  Args:
41
62
  network (nx.Graph): The network graph.
63
+ fraction_shortest_edges (float, optional): Shortest edge rank fraction threshold for creating
64
+ subgraphs before clustering.
42
65
 
43
66
  Returns:
44
- np.ndarray: Binary neighborhood matrix on Label Propagation.
67
+ np.ndarray: A binary neighborhood matrix on Label Propagation.
45
68
  """
46
- # Apply Label Propagation
47
- communities = nx.algorithms.community.label_propagation.label_propagation_communities(network)
48
- # Create a mapping from node to community
49
- community_dict = {}
50
- for community_id, community in enumerate(communities):
51
- for node in community:
52
- community_dict[node] = community_id
69
+ # Create a subgraph with the shortest edges based on the rank fraction
70
+ subnetwork = _create_percentile_limited_subgraph(
71
+ network, fraction_shortest_edges=fraction_shortest_edges
72
+ )
73
+ # Apply Label Propagation for community detection
74
+ communities = nx.algorithms.community.label_propagation.label_propagation_communities(
75
+ subnetwork
76
+ )
77
+ # Get the list of nodes in the network
78
+ nodes = list(network.nodes())
79
+ node_index_map = {node: idx for idx, node in enumerate(nodes)}
80
+ # Create a binary neighborhood matrix
81
+ num_nodes = len(nodes)
82
+ # Initialize neighborhoods with zeros and set self-self entries to 1
83
+ neighborhoods = np.eye(num_nodes, dtype=int)
84
+ # Assign neighborhoods based on community labels using the mapped indices
85
+ for community in communities:
86
+ for node_i in community:
87
+ for node_j in community:
88
+ idx_i = node_index_map[node_i]
89
+ idx_j = node_index_map[node_j]
90
+ neighborhoods[idx_i, idx_j] = 1
91
+
92
+ return neighborhoods
53
93
 
94
+
95
+ def calculate_leiden_neighborhoods(
96
+ network: nx.Graph,
97
+ resolution: float = 1.0,
98
+ fraction_shortest_edges: float = 1.0,
99
+ random_seed: int = 888,
100
+ ) -> np.ndarray:
101
+ """Calculate neighborhoods using the Leiden method.
102
+
103
+ Args:
104
+ network (nx.Graph): The network graph.
105
+ resolution (float, optional): Resolution parameter for the Leiden method. Defaults to 1.0.
106
+ fraction_shortest_edges (float, optional): Shortest edge rank fraction threshold for creating
107
+ subgraphs before clustering.
108
+ random_seed (int, optional): Random seed for reproducibility. Defaults to 888.
109
+
110
+ Returns:
111
+ np.ndarray: A binary neighborhood matrix where nodes in the same community have 1, and others have 0.
112
+ """
113
+ # Create a subgraph with the shortest edges based on the rank fraction
114
+ subnetwork = _create_percentile_limited_subgraph(
115
+ network, fraction_shortest_edges=fraction_shortest_edges
116
+ )
117
+ # Convert NetworkX graph to iGraph
118
+ igraph_network = ig.Graph.from_networkx(subnetwork)
119
+ # Apply Leiden algorithm using RBConfigurationVertexPartition, which supports resolution
120
+ partition = find_partition(
121
+ igraph_network,
122
+ partition_type=RBConfigurationVertexPartition,
123
+ resolution_parameter=resolution,
124
+ seed=random_seed,
125
+ )
126
+ # Get the list of nodes in the original NetworkX graph
127
+ nodes = list(network.nodes())
128
+ node_index_map = {node: idx for idx, node in enumerate(nodes)}
54
129
  # Create a binary neighborhood matrix
55
- num_nodes = network.number_of_nodes()
56
- neighborhoods = np.zeros((num_nodes, num_nodes), dtype=int)
57
- # Assign neighborhoods based on community labels
58
- for node_i, community_i in community_dict.items():
59
- for node_j, community_j in community_dict.items():
60
- if community_i == community_j:
61
- neighborhoods[node_i, node_j] = 1
130
+ num_nodes = len(nodes)
131
+ # Initialize neighborhoods with zeros and set self-self entries to 1
132
+ neighborhoods = np.eye(num_nodes, dtype=int)
133
+ # Assign neighborhoods based on community partitions using the mapped indices
134
+ for community in partition:
135
+ for node_i in community:
136
+ for node_j in community:
137
+ idx_i = node_index_map[igraph_network.vs[node_i]["_nx_name"]]
138
+ idx_j = node_index_map[igraph_network.vs[node_j]["_nx_name"]]
139
+ neighborhoods[idx_i, idx_j] = 1
62
140
 
63
141
  return neighborhoods
64
142
 
65
143
 
66
144
  def calculate_louvain_neighborhoods(
67
- network: nx.Graph, resolution: float, random_seed: int = 888
145
+ network: nx.Graph,
146
+ resolution: float = 0.1,
147
+ fraction_shortest_edges: float = 1.0,
148
+ random_seed: int = 888,
68
149
  ) -> np.ndarray:
69
150
  """Calculate neighborhoods using the Louvain method.
70
151
 
71
152
  Args:
72
153
  network (nx.Graph): The network graph.
73
- resolution (float): Resolution parameter for the Louvain method.
154
+ resolution (float, optional): Resolution parameter for the Louvain method. Defaults to 0.1.
155
+ fraction_shortest_edges (float, optional): Shortest edge rank fraction threshold for creating
156
+ subgraphs before clustering.
74
157
  random_seed (int, optional): Random seed for reproducibility. Defaults to 888.
75
158
 
76
159
  Returns:
77
- np.ndarray: Binary neighborhood matrix on the Louvain method.
160
+ np.ndarray: A binary neighborhood matrix on the Louvain method.
78
161
  """
162
+ # Create a subgraph with the shortest edges based on the rank fraction
163
+ subnetwork = _create_percentile_limited_subgraph(
164
+ network, fraction_shortest_edges=fraction_shortest_edges
165
+ )
79
166
  # Apply Louvain method to partition the network
80
167
  partition = community_louvain.best_partition(
81
- network, resolution=resolution, random_state=random_seed
168
+ subnetwork, resolution=resolution, random_state=random_seed
82
169
  )
170
+ # Get the list of nodes in the network and create a mapping to indices
171
+ nodes = list(network.nodes())
172
+ node_index_map = {node: idx for idx, node in enumerate(nodes)}
83
173
  # Create a binary neighborhood matrix
84
- neighborhoods = np.zeros((network.number_of_nodes(), network.number_of_nodes()), dtype=int)
85
- # Assign neighborhoods based on community partitions
86
- for node_i, community_i in partition.items():
87
- for node_j, community_j in partition.items():
88
- if community_i == community_j:
89
- neighborhoods[node_i, node_j] = 1
174
+ num_nodes = len(nodes)
175
+ # Initialize neighborhoods with zeros and set self-self entries to 1
176
+ neighborhoods = np.eye(num_nodes, dtype=int)
177
+ # Group nodes by community
178
+ community_groups = {}
179
+ for node, community in partition.items():
180
+ community_groups.setdefault(community, []).append(node)
181
+
182
+ # Assign neighborhoods based on community partitions using the mapped indices
183
+ for community, nodes in community_groups.items():
184
+ for node_i in nodes:
185
+ for node_j in nodes:
186
+ idx_i = node_index_map[node_i]
187
+ idx_j = node_index_map[node_j]
188
+ neighborhoods[idx_i, idx_j] = 1
90
189
 
91
190
  return neighborhoods
92
191
 
93
192
 
94
- def calculate_markov_clustering_neighborhoods(network: nx.Graph) -> np.ndarray:
95
- """Apply Markov Clustering (MCL) to the network.
193
+ def calculate_markov_clustering_neighborhoods(
194
+ network: nx.Graph, fraction_shortest_edges: float = 1.0
195
+ ) -> np.ndarray:
196
+ """Apply Markov Clustering (MCL) to the network and return a binary neighborhood matrix.
96
197
 
97
198
  Args:
98
199
  network (nx.Graph): The network graph.
200
+ fraction_shortest_edges (float, optional): Shortest edge rank fraction threshold for creating
201
+ subgraphs before clustering.
99
202
 
100
203
  Returns:
101
- np.ndarray: Binary neighborhood matrix on Markov Clustering.
204
+ np.ndarray: A binary neighborhood matrix on Markov Clustering.
102
205
  """
103
- # Convert the graph to an adjacency matrix
104
- adjacency_matrix = nx.to_numpy_array(network)
105
- # Run Markov Clustering
106
- result = mc.run_mcl(adjacency_matrix) # Run MCL with default parameters
107
- # Get clusters
206
+ # Create a subgraph with the shortest edges based on the rank fraction
207
+ subnetwork = _create_percentile_limited_subgraph(
208
+ network, fraction_shortest_edges=fraction_shortest_edges
209
+ )
210
+ # Step 1: Convert the subnetwork to an adjacency matrix
211
+ subnetwork_nodes = list(subnetwork.nodes())
212
+ adjacency_matrix = nx.to_numpy_array(subnetwork, nodelist=subnetwork_nodes)
213
+ # Step 2: Run Markov Clustering (MCL) on the subnetwork's adjacency matrix
214
+ result = mc.run_mcl(adjacency_matrix)
108
215
  clusters = mc.get_clusters(result)
109
- # Create a community label for each node
110
- community_dict = {}
111
- for community_id, community in enumerate(clusters):
112
- for node in community:
113
- community_dict[node] = community_id
114
-
115
- # Create a binary neighborhood matrix
116
- num_nodes = network.number_of_nodes()
117
- neighborhoods = np.zeros((num_nodes, num_nodes), dtype=int)
118
- # Assign neighborhoods based on community labels
119
- for node_i, community_i in community_dict.items():
120
- for node_j, community_j in community_dict.items():
121
- if community_i == community_j:
122
- neighborhoods[node_i, node_j] = 1
216
+ # Step 3: Prepare the original network nodes and indices
217
+ nodes = list(network.nodes())
218
+ node_index_map = {node: idx for idx, node in enumerate(nodes)}
219
+ num_nodes = len(nodes)
220
+ # Step 4: Initialize the neighborhood matrix for the original network
221
+ neighborhoods = np.eye(num_nodes, dtype=int)
222
+ # Step 5: Fill the neighborhoods matrix using the clusters from the subnetwork
223
+ for cluster in clusters:
224
+ for node_i in cluster:
225
+ for node_j in cluster:
226
+ # Map the indices back to the original network's node indices
227
+ original_node_i = subnetwork_nodes[node_i]
228
+ original_node_j = subnetwork_nodes[node_j]
229
+
230
+ if original_node_i in node_index_map and original_node_j in node_index_map:
231
+ idx_i = node_index_map[original_node_i]
232
+ idx_j = node_index_map[original_node_j]
233
+ neighborhoods[idx_i, idx_j] = 1
123
234
 
124
235
  return neighborhoods
125
236
 
126
237
 
127
- def calculate_spinglass_neighborhoods(network: nx.Graph) -> np.ndarray:
128
- """Apply Spin Glass Community Detection to the network.
238
+ def calculate_spinglass_neighborhoods(
239
+ network: nx.Graph, fraction_shortest_edges: float = 1.0
240
+ ) -> np.ndarray:
241
+ """Apply Spinglass Community Detection to the network, handling disconnected components.
129
242
 
130
243
  Args:
131
244
  network (nx.Graph): The network graph.
245
+ fraction_shortest_edges (float, optional): Shortest edge rank fraction threshold for creating
246
+ subgraphs before clustering.
132
247
 
133
248
  Returns:
134
- np.ndarray: Binary neighborhood matrix on Spin Glass communities.
249
+ np.ndarray: A binary neighborhood matrix based on Spinglass communities.
135
250
  """
136
- # Use the asynchronous label propagation algorithm as a proxy for Spin Glass
137
- communities = asyn_lpa_communities(network)
138
- # Create a community label for each node
139
- community_dict = {}
140
- for community_id, community in enumerate(communities):
141
- for node in community:
142
- community_dict[node] = community_id
143
-
144
- # Create a binary neighborhood matrix
145
- num_nodes = network.number_of_nodes()
146
- neighborhoods = np.zeros((num_nodes, num_nodes), dtype=int)
147
- # Assign neighborhoods based on community labels
148
- for node_i, community_i in community_dict.items():
149
- for node_j, community_j in community_dict.items():
150
- if community_i == community_j:
151
- neighborhoods[node_i, node_j] = 1
251
+ # Create a subgraph with the shortest edges based on the rank fraction
252
+ subnetwork = _create_percentile_limited_subgraph(
253
+ network, fraction_shortest_edges=fraction_shortest_edges
254
+ )
255
+ # Step 1: Find connected components in the graph
256
+ components = list(nx.connected_components(subnetwork))
257
+ # Prepare to store community results
258
+ nodes = list(network.nodes())
259
+ node_index_map = {node: idx for idx, node in enumerate(nodes)}
260
+ num_nodes = len(nodes)
261
+ # Initialize neighborhoods with zeros and set self-self entries to 1
262
+ neighborhoods = np.eye(num_nodes, dtype=int)
263
+ # Step 2: Run Spinglass on each connected component
264
+ for component in components:
265
+ # Extract the subgraph corresponding to the current component
266
+ subgraph = network.subgraph(component)
267
+ # Convert the subgraph to an iGraph object
268
+ igraph_subgraph = ig.Graph.from_networkx(subgraph)
269
+ # Ensure the subgraph is connected before running Spinglass
270
+ if not igraph_subgraph.is_connected():
271
+ logger.error("Warning: Subgraph is not connected. Skipping...")
272
+ continue
273
+
274
+ # Apply Spinglass community detection
275
+ try:
276
+ communities = igraph_subgraph.community_spinglass()
277
+ except Exception as e:
278
+ logger.error(f"Error running Spinglass on component: {e}")
279
+ continue
280
+
281
+ # Step 3: Assign neighborhoods based on community labels
282
+ for community in communities:
283
+ for node_i in community:
284
+ for node_j in community:
285
+ idx_i = node_index_map[igraph_subgraph.vs[node_i]["_nx_name"]]
286
+ idx_j = node_index_map[igraph_subgraph.vs[node_j]["_nx_name"]]
287
+ neighborhoods[idx_i, idx_j] = 1
152
288
 
153
289
  return neighborhoods
154
290
 
155
291
 
156
- def calculate_walktrap_neighborhoods(network: nx.Graph) -> np.ndarray:
292
+ def calculate_walktrap_neighborhoods(
293
+ network: nx.Graph, fraction_shortest_edges: float = 1.0
294
+ ) -> np.ndarray:
157
295
  """Apply Walktrap Community Detection to the network.
158
296
 
159
297
  Args:
160
298
  network (nx.Graph): The network graph.
299
+ fraction_shortest_edges (float, optional): Shortest edge rank fraction threshold for creating
300
+ subgraphs before clustering.
161
301
 
162
302
  Returns:
163
- np.ndarray: Binary neighborhood matrix on Walktrap communities.
303
+ np.ndarray: A binary neighborhood matrix on Walktrap communities.
164
304
  """
165
- # Use the asynchronous label propagation algorithm as a proxy for Walktrap
166
- communities = asyn_lpa_communities(network)
167
- # Create a community label for each node
168
- community_dict = {}
169
- for community_id, community in enumerate(communities):
170
- for node in community:
171
- community_dict[node] = community_id
172
-
305
+ # Create a subgraph with the shortest edges based on the rank fraction
306
+ subnetwork = _create_percentile_limited_subgraph(
307
+ network, fraction_shortest_edges=fraction_shortest_edges
308
+ )
309
+ # Convert NetworkX graph to iGraph
310
+ igraph_network = ig.Graph.from_networkx(subnetwork)
311
+ # Apply Walktrap community detection
312
+ communities = igraph_network.community_walktrap().as_clustering()
313
+ # Get the list of nodes in the original NetworkX graph
314
+ nodes = list(network.nodes())
315
+ node_index_map = {node: idx for idx, node in enumerate(nodes)}
173
316
  # Create a binary neighborhood matrix
174
- num_nodes = network.number_of_nodes()
175
- neighborhoods = np.zeros((num_nodes, num_nodes), dtype=int)
317
+ num_nodes = len(nodes)
318
+ # Initialize neighborhoods with zeros and set self-self entries to 1
319
+ neighborhoods = np.eye(num_nodes, dtype=int)
176
320
  # Assign neighborhoods based on community labels
177
- for node_i, community_i in community_dict.items():
178
- for node_j, community_j in community_dict.items():
179
- if community_i == community_j:
180
- neighborhoods[node_i, node_j] = 1
321
+ for community in communities:
322
+ for node_i in community:
323
+ for node_j in community:
324
+ idx_i = node_index_map[igraph_network.vs[node_i]["_nx_name"]]
325
+ idx_j = node_index_map[igraph_network.vs[node_j]["_nx_name"]]
326
+ neighborhoods[idx_i, idx_j] = 1
181
327
 
182
328
  return neighborhoods
329
+
330
+
331
+ def _create_percentile_limited_subgraph(G: nx.Graph, fraction_shortest_edges: float) -> nx.Graph:
332
+ """Create a subgraph containing the shortest edges based on the specified rank fraction
333
+ of all edge lengths in the input graph.
334
+
335
+ Args:
336
+ G (nx.Graph): The input graph with 'length' attributes on edges.
337
+ fraction_shortest_edges (float): The rank fraction (between 0 and 1) to filter edges.
338
+
339
+ Returns:
340
+ nx.Graph: A subgraph with nodes and edges where the edges are within the shortest
341
+ specified rank fraction.
342
+ """
343
+ # Step 1: Extract edges with their lengths
344
+ edges_with_length = [(u, v, d) for u, v, d in G.edges(data=True) if "length" in d]
345
+ if not edges_with_length:
346
+ raise ValueError(
347
+ "No edge lengths found in the graph. Ensure edges have 'length' attributes."
348
+ )
349
+
350
+ # Step 2: Sort edges by length in ascending order
351
+ edges_with_length.sort(key=lambda x: x[2]["length"])
352
+ # Step 3: Calculate the cutoff index for the given rank fraction
353
+ cutoff_index = int(fraction_shortest_edges * len(edges_with_length))
354
+ if cutoff_index == 0:
355
+ raise ValueError("The rank fraction is too low, resulting in no edges being included.")
356
+
357
+ # Step 4: Create the subgraph by selecting only the shortest edges within the rank fraction
358
+ subgraph = nx.Graph()
359
+ subgraph.add_nodes_from(G.nodes(data=True)) # Retain all nodes from the original graph
360
+ subgraph.add_edges_from(edges_with_length[:cutoff_index])
361
+ # Step 5: Remove nodes with no edges
362
+ subgraph.remove_nodes_from(list(nx.isolates(subgraph)))
363
+ # Step 6: Check if the resulting subgraph has no edges and issue a warning
364
+ if subgraph.number_of_edges() == 0:
365
+ raise Warning("The resulting subgraph has no edges. Consider adjusting the rank fraction.")
366
+
367
+ return subgraph