nettracer3d 1.2.7__py3-none-any.whl → 1.3.6__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.
nettracer3d/histos.py ADDED
@@ -0,0 +1,1182 @@
1
+ from PyQt6.QtWidgets import (
2
+ QWidget,
3
+ QVBoxLayout,
4
+ QLabel,
5
+ QPushButton,
6
+ QMessageBox,
7
+ QFileDialog
8
+ )
9
+ from PyQt6.QtCore import Qt
10
+ import numpy as np
11
+ import pandas as pd
12
+ import networkx as nx
13
+ import matplotlib.pyplot as plt
14
+ import os
15
+
16
+
17
+ def convert_to_multigraph(G, weight_attr='weight'):
18
+ """
19
+ Convert weighted graph to MultiGraph by creating parallel edges.
20
+
21
+ Args:
22
+ G: NetworkX Graph with edge weights representing multiplicity
23
+ weight_attr: Name of the weight attribute (default: 'weight')
24
+
25
+ Returns:
26
+ MultiGraph with parallel edges instead of weights
27
+
28
+ Note:
29
+ - Weights are rounded to integers
30
+ - Original node/edge attributes are preserved on first edge
31
+ - Directed graphs become MultiDiGraphs
32
+ """
33
+
34
+ MG = nx.MultiGraph()
35
+
36
+ # Copy nodes with all their attributes
37
+ MG.add_nodes_from(G.nodes(data=True))
38
+
39
+ # Convert weighted edges to multiple parallel edges
40
+ for u, v, data in G.edges(data=True):
41
+ # Get weight (default to 1 if missing)
42
+ weight = data.get(weight_attr, 1)
43
+
44
+ # Round to integer for number of parallel edges
45
+ num_edges = int(round(weight))
46
+
47
+ if num_edges < 1:
48
+ num_edges = 1 # At least one edge
49
+
50
+ # Create parallel edges
51
+ for i in range(num_edges):
52
+ # First edge gets all the original attributes (except weight)
53
+ if i == 0:
54
+ edge_data = {k: v for k, v in data.items() if k != weight_attr}
55
+ MG.add_edge(u, v, **edge_data)
56
+ else:
57
+ # Subsequent parallel edges are simple
58
+ MG.add_edge(u, v)
59
+
60
+ return MG
61
+
62
+ class HistogramSelector(QWidget):
63
+ def __init__(self, network_analysis_instance, stats_dict, G):
64
+ super().__init__()
65
+ self.network_analysis = network_analysis_instance
66
+ self.stats_dict = stats_dict
67
+ self.G_unweighted = G
68
+ self.G = convert_to_multigraph(G)
69
+ self.init_ui()
70
+
71
+ def init_ui(self):
72
+ self.setWindowTitle('Network Analysis - Histogram Selector')
73
+ self.setGeometry(300, 300, 400, 700) # Increased height for more buttons
74
+
75
+ layout = QVBoxLayout()
76
+
77
+ # Title label
78
+ title_label = QLabel('Select Histogram to Generate:')
79
+ title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
80
+ title_label.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
81
+ layout.addWidget(title_label)
82
+
83
+ # Create buttons for each histogram type
84
+ self.create_button(layout, "Shortest Path Length Distribution", self.shortest_path_histogram)
85
+ self.create_button(layout, "Degree Centrality", self.degree_centrality_histogram)
86
+ self.create_button(layout, "Betweenness Centrality", self.betweenness_centrality_histogram)
87
+ self.create_button(layout, "Closeness Centrality", self.closeness_centrality_histogram)
88
+ self.create_button(layout, "Eigenvector Centrality", self.eigenvector_centrality_histogram)
89
+ self.create_button(layout, "Clustering Coefficient", self.clustering_coefficient_histogram)
90
+ self.create_button(layout, "Degree Distribution", self.degree_distribution_histogram)
91
+ self.create_button(layout, "Node Connectivity", self.node_connectivity_histogram)
92
+ self.create_button(layout, "Eccentricity", self.eccentricity_histogram)
93
+ self.create_button(layout, "K-Core Decomposition", self.kcore_histogram)
94
+ self.create_button(layout, "Triangle Count", self.triangle_count_histogram)
95
+ self.create_button(layout, "Load Centrality", self.load_centrality_histogram)
96
+ self.create_button(layout, "Communicability Betweenness Centrality", self.communicability_centrality_histogram)
97
+ self.create_button(layout, "Harmonic Centrality", self.harmonic_centrality_histogram)
98
+ self.create_button(layout, "Current Flow Betweenness", self.current_flow_betweenness_histogram)
99
+ self.create_button(layout, "Dispersion", self.dispersion_histogram)
100
+ self.create_button(layout, "Network Bridges", self.bridges_analysis)
101
+
102
+ # Compute All button - visually distinct
103
+ compute_all_button = QPushButton('Compute All Analyses and Export to CSV')
104
+ compute_all_button.clicked.connect(self.compute_all)
105
+ compute_all_button.setMinimumHeight(50)
106
+ compute_all_button.setStyleSheet("""
107
+ QPushButton {
108
+ background-color: #FF9800;
109
+ color: white;
110
+ border: 3px solid #F57C00;
111
+ padding: 10px;
112
+ font-size: 16px;
113
+ font-weight: bold;
114
+ border-radius: 8px;
115
+ }
116
+ QPushButton:hover {
117
+ background-color: #FB8C00;
118
+ border-color: #E65100;
119
+ }
120
+ QPushButton:pressed {
121
+ background-color: #F57C00;
122
+ }
123
+ """)
124
+ layout.addWidget(compute_all_button)
125
+
126
+ # Close button
127
+ close_button = QPushButton('Close')
128
+ close_button.clicked.connect(self.close)
129
+ close_button.setStyleSheet("QPushButton { background-color: #f44336; color: white; font-weight: bold; }")
130
+ #layout.addWidget(close_button)
131
+
132
+ self.setLayout(layout)
133
+
134
+ def create_button(self, layout, text, callback):
135
+ button = QPushButton(text)
136
+ button.clicked.connect(callback)
137
+ button.setMinimumHeight(40)
138
+ button.setStyleSheet("""
139
+ QPushButton {
140
+ background-color: #4CAF50;
141
+ color: white;
142
+ border: none;
143
+ padding: 10px;
144
+ font-size: 14px;
145
+ font-weight: bold;
146
+ border-radius: 5px;
147
+ }
148
+ QPushButton:hover {
149
+ background-color: #45a049;
150
+ }
151
+ QPushButton:pressed {
152
+ background-color: #3d8b40;
153
+ }
154
+ """)
155
+ layout.addWidget(button)
156
+
157
+ def compute_all(self):
158
+ """Compute all available analyses and export to CSV files and histogram images"""
159
+ from PyQt6.QtWidgets import QMessageBox, QFileDialog
160
+ import os
161
+ import pandas as pd
162
+
163
+ # Show confirmation dialog
164
+ reply = QMessageBox.question(
165
+ self,
166
+ 'Compute All Analyses',
167
+ 'This will compute all available analyses and may take a while for large networks.\n\n'
168
+ 'Do you want to continue?',
169
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
170
+ QMessageBox.StandardButton.No
171
+ )
172
+
173
+ if reply == QMessageBox.StandardButton.No:
174
+ return
175
+
176
+ # Get save location and folder name
177
+ folder_path, _ = QFileDialog.getSaveFileName(
178
+ self,
179
+ 'Select Location and Name for Output Folder',
180
+ 'network_analysis_results',
181
+ 'Folder (*)'
182
+ )
183
+
184
+ if not folder_path:
185
+ return
186
+
187
+ # Create main directory and subdirectories
188
+ try:
189
+ os.makedirs(folder_path, exist_ok=True)
190
+ csvs_path = os.path.join(folder_path, 'csvs')
191
+ graphs_path = os.path.join(folder_path, 'graph_images')
192
+ os.makedirs(csvs_path, exist_ok=True)
193
+ os.makedirs(graphs_path, exist_ok=True)
194
+ except Exception as e:
195
+ QMessageBox.critical(self, 'Error', f'Could not create directory: {str(e)}')
196
+ return
197
+
198
+ print(f"Computing all analyses and saving to: {folder_path}")
199
+
200
+ try:
201
+ # 1. Shortest Path Length Distribution
202
+ print("Computing shortest path distribution...")
203
+ components = list(nx.connected_components(self.G))
204
+ if len(components) > 1:
205
+ all_path_lengths = []
206
+ max_diameter = 0
207
+ for component in components:
208
+ subgraph = self.G.subgraph(component)
209
+ if len(component) < 2:
210
+ continue
211
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(subgraph))
212
+ component_diameter = max(nx.eccentricity(subgraph, sp=shortest_path_lengths).values())
213
+ max_diameter = max(max_diameter, component_diameter)
214
+ for pls in shortest_path_lengths.values():
215
+ all_path_lengths.extend(list(pls.values()))
216
+ all_path_lengths = [pl for pl in all_path_lengths if pl > 0]
217
+ if all_path_lengths:
218
+ path_lengths = np.zeros(max_diameter + 1, dtype=int)
219
+ pl, cnts = np.unique(all_path_lengths, return_counts=True)
220
+ path_lengths[pl] += cnts
221
+ freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
222
+ df = pd.DataFrame({
223
+ 'Path_Length': np.arange(1, max_diameter + 1),
224
+ 'Frequency_Percent': freq_percent
225
+ })
226
+ df.to_csv(os.path.join(csvs_path, 'shortest_path_distribution.csv'), index=False)
227
+
228
+ # Generate and save plot
229
+ fig, ax = plt.subplots(figsize=(15, 8))
230
+ ax.bar(np.arange(1, max_diameter + 1), height=freq_percent)
231
+ ax.set_title(f"Distribution of shortest path length in G (across {len(components)} components)",
232
+ fontdict={"size": 35}, loc="center")
233
+ ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
234
+ ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
235
+ plt.tight_layout()
236
+ plt.savefig(os.path.join(graphs_path, 'shortest_path_distribution.png'), dpi=150, bbox_inches='tight')
237
+ plt.close(fig)
238
+ else:
239
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
240
+ diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
241
+ path_lengths = np.zeros(diameter + 1, dtype=int)
242
+ for pls in shortest_path_lengths.values():
243
+ pl, cnts = np.unique(list(pls.values()), return_counts=True)
244
+ path_lengths[pl] += cnts
245
+ freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
246
+ df = pd.DataFrame({
247
+ 'Path_Length': np.arange(1, diameter + 1),
248
+ 'Frequency_Percent': freq_percent
249
+ })
250
+ df.to_csv(os.path.join(csvs_path, 'shortest_path_distribution.csv'), index=False)
251
+
252
+ # Generate and save plot
253
+ fig, ax = plt.subplots(figsize=(15, 8))
254
+ ax.bar(np.arange(1, diameter + 1), height=freq_percent)
255
+ ax.set_title("Distribution of shortest path length in G", fontdict={"size": 35}, loc="center")
256
+ ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
257
+ ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
258
+ plt.tight_layout()
259
+ plt.savefig(os.path.join(graphs_path, 'shortest_path_distribution.png'), dpi=150, bbox_inches='tight')
260
+ plt.close(fig)
261
+
262
+ # 2. Degree Centrality
263
+ print("Computing degree centrality...")
264
+ degree_centrality = nx.centrality.degree_centrality(self.G)
265
+ df = pd.DataFrame(list(degree_centrality.items()), columns=['Node', 'Degree_Centrality'])
266
+ df.to_csv(os.path.join(csvs_path, 'degree_centrality.csv'), index=False)
267
+
268
+ # Generate and save plot
269
+ fig = plt.figure(figsize=(15, 8))
270
+ plt.hist(degree_centrality.values(), bins=25)
271
+ plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2])
272
+ plt.title("Degree Centrality Histogram", fontdict={"size": 35}, loc="center")
273
+ plt.xlabel("Degree Centrality", fontdict={"size": 20})
274
+ plt.ylabel("Counts", fontdict={"size": 20})
275
+ plt.tight_layout()
276
+ plt.savefig(os.path.join(graphs_path, 'degree_centrality.png'), dpi=150, bbox_inches='tight')
277
+ plt.close(fig)
278
+
279
+ # 3. Betweenness Centrality
280
+ print("Computing betweenness centrality...")
281
+ components = list(nx.connected_components(self.G))
282
+ if len(components) > 1:
283
+ combined_betweenness_centrality = {}
284
+ for component in components:
285
+ if len(component) < 2:
286
+ for node in component:
287
+ combined_betweenness_centrality[node] = 0.0
288
+ continue
289
+ subgraph = self.G.subgraph(component)
290
+ component_betweenness = nx.centrality.betweenness_centrality(subgraph)
291
+ combined_betweenness_centrality.update(component_betweenness)
292
+ betweenness_centrality = combined_betweenness_centrality
293
+ title_suffix = f" (across {len(components)} components)"
294
+ else:
295
+ betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
296
+ title_suffix = ""
297
+ df = pd.DataFrame(list(betweenness_centrality.items()), columns=['Node', 'Betweenness_Centrality'])
298
+ df.to_csv(os.path.join(csvs_path, 'betweenness_centrality.csv'), index=False)
299
+
300
+ # Generate and save plot
301
+ fig = plt.figure(figsize=(15, 8))
302
+ plt.hist(betweenness_centrality.values(), bins=100)
303
+ plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
304
+ plt.title(f"Betweenness Centrality Histogram{title_suffix}", fontdict={"size": 35}, loc="center")
305
+ plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
306
+ plt.ylabel("Counts", fontdict={"size": 20})
307
+ plt.tight_layout()
308
+ plt.savefig(os.path.join(graphs_path, 'betweenness_centrality.png'), dpi=150, bbox_inches='tight')
309
+ plt.close(fig)
310
+
311
+ # 4. Closeness Centrality
312
+ print("Computing closeness centrality...")
313
+ closeness_centrality = nx.centrality.closeness_centrality(self.G)
314
+ df = pd.DataFrame(list(closeness_centrality.items()), columns=['Node', 'Closeness_Centrality'])
315
+ df.to_csv(os.path.join(csvs_path, 'closeness_centrality.csv'), index=False)
316
+
317
+ # Generate and save plot
318
+ fig = plt.figure(figsize=(15, 8))
319
+ plt.hist(closeness_centrality.values(), bins=60)
320
+ plt.title("Closeness Centrality Histogram", fontdict={"size": 35}, loc="center")
321
+ plt.xlabel("Closeness Centrality", fontdict={"size": 20})
322
+ plt.ylabel("Counts", fontdict={"size": 20})
323
+ plt.tight_layout()
324
+ plt.savefig(os.path.join(graphs_path, 'closeness_centrality.png'), dpi=150, bbox_inches='tight')
325
+ plt.close(fig)
326
+
327
+ # 5. Eigenvector Centrality
328
+ print("Computing eigenvector centrality...")
329
+ try:
330
+ eigenvector_centrality = nx.centrality.eigenvector_centrality(self.G_unweighted)
331
+ df = pd.DataFrame(list(eigenvector_centrality.items()), columns=['Node', 'Eigenvector_Centrality'])
332
+ df.to_csv(os.path.join(csvs_path, 'eigenvector_centrality.csv'), index=False)
333
+
334
+ # Generate and save plot
335
+ fig = plt.figure(figsize=(15, 8))
336
+ plt.hist(eigenvector_centrality.values(), bins=60)
337
+ plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08])
338
+ plt.title("Eigenvector Centrality Histogram", fontdict={"size": 35}, loc="center")
339
+ plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
340
+ plt.ylabel("Counts", fontdict={"size": 20})
341
+ plt.tight_layout()
342
+ plt.savefig(os.path.join(graphs_path, 'eigenvector_centrality.png'), dpi=150, bbox_inches='tight')
343
+ plt.close(fig)
344
+ except Exception as e:
345
+ print(f"Could not compute eigenvector centrality: {e}")
346
+
347
+ # 6. Clustering Coefficient
348
+ print("Computing clustering coefficient...")
349
+ clusters = nx.clustering(self.G_unweighted)
350
+ df = pd.DataFrame(list(clusters.items()), columns=['Node', 'Clustering_Coefficient'])
351
+ df.to_csv(os.path.join(csvs_path, 'clustering_coefficient.csv'), index=False)
352
+
353
+ # Generate and save plot
354
+ fig = plt.figure(figsize=(15, 8))
355
+ plt.hist(clusters.values(), bins=50)
356
+ plt.title("Clustering Coefficient Histogram", fontdict={"size": 35}, loc="center")
357
+ plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
358
+ plt.ylabel("Counts", fontdict={"size": 20})
359
+ plt.tight_layout()
360
+ plt.savefig(os.path.join(graphs_path, 'clustering_coefficient.png'), dpi=150, bbox_inches='tight')
361
+ plt.close(fig)
362
+
363
+ # 7. Degree Distribution
364
+ print("Computing degree distribution...")
365
+ degrees = {node: deg for node, deg in self.G.degree()}
366
+ df = pd.DataFrame(list(degrees.items()), columns=['Node', 'Degree'])
367
+ df.to_csv(os.path.join(csvs_path, 'degree_distribution.csv'), index=False)
368
+
369
+ # Generate and save plot
370
+ degree_values = [deg for node, deg in self.G.degree()]
371
+ fig = plt.figure(figsize=(15, 8))
372
+ plt.hist(degree_values, bins=max(30, int(np.sqrt(len(degree_values)))), alpha=0.7)
373
+ plt.title("Degree Distribution", fontdict={"size": 35}, loc="center")
374
+ plt.xlabel("Degree", fontdict={"size": 20})
375
+ plt.ylabel("Frequency", fontdict={"size": 20})
376
+ plt.yscale('log')
377
+ plt.tight_layout()
378
+ plt.savefig(os.path.join(graphs_path, 'degree_distribution.png'), dpi=150, bbox_inches='tight')
379
+ plt.close(fig)
380
+
381
+ # 8. Node Connectivity
382
+ print("Computing node connectivity...")
383
+ connectivity = {}
384
+ for node in self.G.nodes():
385
+ neighbors = list(self.G.neighbors(node))
386
+ if len(neighbors) > 1:
387
+ connectivity[node] = nx.node_connectivity(self.G, neighbors[0], neighbors[1])
388
+ else:
389
+ connectivity[node] = 0
390
+ df = pd.DataFrame(list(connectivity.items()), columns=['Node', 'Node_Connectivity'])
391
+ df.to_csv(os.path.join(csvs_path, 'node_connectivity.csv'), index=False)
392
+
393
+ # Generate and save plot
394
+ fig = plt.figure(figsize=(15, 8))
395
+ plt.hist(connectivity.values(), bins=20, alpha=0.7)
396
+ plt.title("Node Connectivity Distribution", fontdict={"size": 35}, loc="center")
397
+ plt.xlabel("Node Connectivity", fontdict={"size": 20})
398
+ plt.ylabel("Frequency", fontdict={"size": 20})
399
+ plt.tight_layout()
400
+ plt.savefig(os.path.join(graphs_path, 'node_connectivity.png'), dpi=150, bbox_inches='tight')
401
+ plt.close(fig)
402
+
403
+ # 9. Eccentricity
404
+ print("Computing eccentricity...")
405
+ if not nx.is_connected(self.G):
406
+ largest_cc = max(nx.connected_components(self.G), key=len)
407
+ G_cc = self.G.subgraph(largest_cc)
408
+ eccentricity = nx.eccentricity(G_cc)
409
+ else:
410
+ eccentricity = nx.eccentricity(self.G)
411
+ df = pd.DataFrame(list(eccentricity.items()), columns=['Node', 'Eccentricity'])
412
+ df.to_csv(os.path.join(csvs_path, 'eccentricity.csv'), index=False)
413
+
414
+ # Generate and save plot
415
+ fig = plt.figure(figsize=(15, 8))
416
+ plt.hist(eccentricity.values(), bins=20, alpha=0.7)
417
+ plt.title("Eccentricity Distribution", fontdict={"size": 35}, loc="center")
418
+ plt.xlabel("Eccentricity", fontdict={"size": 20})
419
+ plt.ylabel("Frequency", fontdict={"size": 20})
420
+ plt.tight_layout()
421
+ plt.savefig(os.path.join(graphs_path, 'eccentricity.png'), dpi=150, bbox_inches='tight')
422
+ plt.close(fig)
423
+
424
+ # 10. K-Core
425
+ print("Computing k-core decomposition...")
426
+ kcore = nx.core_number(self.G_unweighted)
427
+ df = pd.DataFrame(list(kcore.items()), columns=['Node', 'K_Core'])
428
+ df.to_csv(os.path.join(csvs_path, 'kcore.csv'), index=False)
429
+
430
+ # Generate and save plot
431
+ fig = plt.figure(figsize=(15, 8))
432
+ plt.hist(kcore.values(), bins=max(5, max(kcore.values())), alpha=0.7)
433
+ plt.title("K-Core Distribution", fontdict={"size": 35}, loc="center")
434
+ plt.xlabel("K-Core Number", fontdict={"size": 20})
435
+ plt.ylabel("Frequency", fontdict={"size": 20})
436
+ plt.tight_layout()
437
+ plt.savefig(os.path.join(graphs_path, 'kcore.png'), dpi=150, bbox_inches='tight')
438
+ plt.close(fig)
439
+
440
+ # 11. Triangle Count
441
+ print("Computing triangle count...")
442
+ triangles = nx.triangles(self.G)
443
+ df = pd.DataFrame(list(triangles.items()), columns=['Node', 'Triangle_Count'])
444
+ df.to_csv(os.path.join(csvs_path, 'triangle_count.csv'), index=False)
445
+
446
+ # Generate and save plot
447
+ fig = plt.figure(figsize=(15, 8))
448
+ plt.hist(triangles.values(), bins=30, alpha=0.7)
449
+ plt.title("Triangle Count Distribution", fontdict={"size": 35}, loc="center")
450
+ plt.xlabel("Number of Triangles", fontdict={"size": 20})
451
+ plt.ylabel("Frequency", fontdict={"size": 20})
452
+ plt.tight_layout()
453
+ plt.savefig(os.path.join(graphs_path, 'triangle_count.png'), dpi=150, bbox_inches='tight')
454
+ plt.close(fig)
455
+
456
+ # 12. Load Centrality
457
+ print("Computing load centrality...")
458
+ load_centrality = nx.load_centrality(self.G)
459
+ df = pd.DataFrame(list(load_centrality.items()), columns=['Node', 'Load_Centrality'])
460
+ df.to_csv(os.path.join(csvs_path, 'load_centrality.csv'), index=False)
461
+
462
+ # Generate and save plot
463
+ fig = plt.figure(figsize=(15, 8))
464
+ plt.hist(load_centrality.values(), bins=50, alpha=0.7)
465
+ plt.title("Load Centrality Distribution", fontdict={"size": 35}, loc="center")
466
+ plt.xlabel("Load Centrality", fontdict={"size": 20})
467
+ plt.ylabel("Frequency", fontdict={"size": 20})
468
+ plt.tight_layout()
469
+ plt.savefig(os.path.join(graphs_path, 'load_centrality.png'), dpi=150, bbox_inches='tight')
470
+ plt.close(fig)
471
+
472
+ # 13. Communicability Betweenness Centrality
473
+ print("Computing communicability betweenness centrality...")
474
+ components = list(nx.connected_components(self.G_unweighted))
475
+ if len(components) > 1:
476
+ combined_comm_centrality = {}
477
+ for component in components:
478
+ if len(component) < 2:
479
+ for node in component:
480
+ combined_comm_centrality[node] = 0.0
481
+ continue
482
+ subgraph = self.G_unweighted.subgraph(component)
483
+ try:
484
+ component_comm_centrality = nx.communicability_betweenness_centrality(subgraph)
485
+ combined_comm_centrality.update(component_comm_centrality)
486
+ except Exception as comp_e:
487
+ print(f"Error computing communicability centrality for component: {comp_e}")
488
+ for node in component:
489
+ combined_comm_centrality[node] = 0.0
490
+ comm_centrality = combined_comm_centrality
491
+ title_suffix = f" (across {len(components)} components)"
492
+ else:
493
+ comm_centrality = nx.communicability_betweenness_centrality(self.G_unweighted)
494
+ title_suffix = ""
495
+ df = pd.DataFrame(list(comm_centrality.items()), columns=['Node', 'Communicability_Betweenness_Centrality'])
496
+ df.to_csv(os.path.join(csvs_path, 'communicability_betweenness_centrality.csv'), index=False)
497
+
498
+ # Generate and save plot
499
+ fig = plt.figure(figsize=(15, 8))
500
+ plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
501
+ plt.title(f"Communicability Betweenness Centrality Distribution{title_suffix}",
502
+ fontdict={"size": 35}, loc="center")
503
+ plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
504
+ plt.ylabel("Frequency", fontdict={"size": 20})
505
+ plt.tight_layout()
506
+ plt.savefig(os.path.join(graphs_path, 'communicability_betweenness_centrality.png'), dpi=150, bbox_inches='tight')
507
+ plt.close(fig)
508
+
509
+ # 14. Harmonic Centrality
510
+ print("Computing harmonic centrality...")
511
+ harmonic_centrality = nx.harmonic_centrality(self.G)
512
+ df = pd.DataFrame(list(harmonic_centrality.items()), columns=['Node', 'Harmonic_Centrality'])
513
+ df.to_csv(os.path.join(csvs_path, 'harmonic_centrality.csv'), index=False)
514
+
515
+ # Generate and save plot
516
+ fig = plt.figure(figsize=(15, 8))
517
+ plt.hist(harmonic_centrality.values(), bins=50, alpha=0.7)
518
+ plt.title("Harmonic Centrality Distribution", fontdict={"size": 35}, loc="center")
519
+ plt.xlabel("Harmonic Centrality", fontdict={"size": 20})
520
+ plt.ylabel("Frequency", fontdict={"size": 20})
521
+ plt.tight_layout()
522
+ plt.savefig(os.path.join(graphs_path, 'harmonic_centrality.png'), dpi=150, bbox_inches='tight')
523
+ plt.close(fig)
524
+
525
+ # 15. Current Flow Betweenness
526
+ print("Computing current flow betweenness...")
527
+ components = list(nx.connected_components(self.G))
528
+ if len(components) > 1:
529
+ combined_current_flow = {}
530
+ for component in components:
531
+ if len(component) < 2:
532
+ for node in component:
533
+ combined_current_flow[node] = 0.0
534
+ continue
535
+ subgraph = self.G.subgraph(component)
536
+ try:
537
+ component_current_flow = nx.current_flow_betweenness_centrality(subgraph)
538
+ combined_current_flow.update(component_current_flow)
539
+ except Exception as comp_e:
540
+ print(f"Error computing current flow betweenness for component: {comp_e}")
541
+ for node in component:
542
+ combined_current_flow[node] = 0.0
543
+ current_flow = combined_current_flow
544
+ title_suffix = f" (across {len(components)} components)"
545
+ else:
546
+ current_flow = nx.current_flow_betweenness_centrality(self.G)
547
+ title_suffix = ""
548
+ df = pd.DataFrame(list(current_flow.items()), columns=['Node', 'Current_Flow_Betweenness'])
549
+ df.to_csv(os.path.join(csvs_path, 'current_flow_betweenness.csv'), index=False)
550
+
551
+ # Generate and save plot
552
+ fig = plt.figure(figsize=(15, 8))
553
+ plt.hist(current_flow.values(), bins=50, alpha=0.7)
554
+ plt.title(f"Current Flow Betweenness Centrality Distribution{title_suffix}",
555
+ fontdict={"size": 35}, loc="center")
556
+ plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
557
+ plt.ylabel("Frequency", fontdict={"size": 20})
558
+ plt.tight_layout()
559
+ plt.savefig(os.path.join(graphs_path, 'current_flow_betweenness.png'), dpi=150, bbox_inches='tight')
560
+ plt.close(fig)
561
+
562
+ # 16. Dispersion
563
+ print("Computing dispersion...")
564
+ dispersion_values = {}
565
+ nodes = list(self.G.nodes())
566
+ for u in nodes:
567
+ if self.G.degree(u) < 2:
568
+ dispersion_values[u] = 0
569
+ continue
570
+ neighbors = list(self.G.neighbors(u))
571
+ if len(neighbors) < 2:
572
+ dispersion_values[u] = 0
573
+ continue
574
+ disp_scores = []
575
+ for v in neighbors:
576
+ try:
577
+ disp_score = nx.dispersion(self.G, u, v)
578
+ disp_scores.append(disp_score)
579
+ except:
580
+ continue
581
+ dispersion_values[u] = sum(disp_scores) / len(disp_scores) if disp_scores else 0
582
+ df = pd.DataFrame(list(dispersion_values.items()), columns=['Node', 'Average_Dispersion'])
583
+ df.to_csv(os.path.join(csvs_path, 'dispersion.csv'), index=False)
584
+
585
+ # Generate and save plot
586
+ fig = plt.figure(figsize=(15, 8))
587
+ plt.hist(dispersion_values.values(), bins=30, alpha=0.7)
588
+ plt.title("Average Dispersion Distribution", fontdict={"size": 35}, loc="center")
589
+ plt.xlabel("Average Dispersion", fontdict={"size": 20})
590
+ plt.ylabel("Frequency", fontdict={"size": 20})
591
+ plt.tight_layout()
592
+ plt.savefig(os.path.join(graphs_path, 'dispersion.png'), dpi=150, bbox_inches='tight')
593
+ plt.close(fig)
594
+
595
+ # 17. Bridges (CSV only, no plot)
596
+ print("Computing bridges...")
597
+ bridges = list(nx.bridges(self.G))
598
+ try:
599
+ # Get the existing DataFrame from the model
600
+ original_df = self.network_analysis.network_table.model()._data
601
+
602
+ # Create boolean mask
603
+ mask = pd.Series([False] * len(original_df))
604
+
605
+ for u, v in bridges:
606
+ # Check for both (u,v) and (v,u) orientations
607
+ bridge_mask = (
608
+ ((original_df.iloc[:, 0] == u) & (original_df.iloc[:, 1] == v)) |
609
+ ((original_df.iloc[:, 0] == v) & (original_df.iloc[:, 1] == u))
610
+ )
611
+ mask |= bridge_mask
612
+ # Filter the DataFrame to only include bridge connections
613
+ df = original_df[mask].copy()
614
+ except:
615
+ df = pd.DataFrame(bridges, columns=['Node_A', 'Node_B'])
616
+
617
+ df.to_csv(os.path.join(csvs_path, 'bridges.csv'), index=False)
618
+
619
+ print(f"\nAll analyses complete! Results saved to: {folder_path}")
620
+ QMessageBox.information(
621
+ self,
622
+ 'Complete',
623
+ f'All analyses have been computed and saved to:\n\n{folder_path}\n\n'
624
+ f'CSVs: {csvs_path}\nGraphs: {graphs_path}\n\n'
625
+ f'Total files created: 17 CSVs + 16 histogram images'
626
+ )
627
+
628
+ except Exception as e:
629
+ print(f"Error during compute all: {e}")
630
+ import traceback
631
+ traceback.print_exc()
632
+ QMessageBox.critical(self, 'Error', f'An error occurred during computation:\n\n{str(e)}')
633
+
634
+ def shortest_path_histogram(self):
635
+ try:
636
+ # Check if graph has multiple disconnected components
637
+ components = list(nx.connected_components(self.G))
638
+
639
+ if len(components) > 1:
640
+ print(f"Warning: Graph has {len(components)} disconnected components. Computing shortest paths within each component separately.")
641
+
642
+ # Initialize variables to collect data from all components
643
+ all_path_lengths = []
644
+ max_diameter = 0
645
+
646
+ # Process each component separately
647
+ for i, component in enumerate(components):
648
+ subgraph = self.G.subgraph(component)
649
+
650
+ if len(component) < 2:
651
+ # Skip single-node components (no paths to compute)
652
+ continue
653
+
654
+ # Compute shortest paths for this component
655
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(subgraph))
656
+ component_diameter = max(nx.eccentricity(subgraph, sp=shortest_path_lengths).values())
657
+ max_diameter = max(max_diameter, component_diameter)
658
+
659
+ # Collect path lengths from this component
660
+ for pls in shortest_path_lengths.values():
661
+ all_path_lengths.extend(list(pls.values()))
662
+
663
+ # Remove self-paths (length 0) and create histogram
664
+ all_path_lengths = [pl for pl in all_path_lengths if pl > 0]
665
+
666
+ if not all_path_lengths:
667
+ print("No paths found across components (only single-node components)")
668
+ return
669
+
670
+ # Create combined histogram
671
+ path_lengths = np.zeros(max_diameter + 1, dtype=int)
672
+ pl, cnts = np.unique(all_path_lengths, return_counts=True)
673
+ path_lengths[pl] += cnts
674
+
675
+ title_suffix = f" (across {len(components)} components)"
676
+
677
+ else:
678
+ # Single component
679
+ shortest_path_lengths = dict(nx.all_pairs_shortest_path_length(self.G))
680
+ diameter = max(nx.eccentricity(self.G, sp=shortest_path_lengths).values())
681
+ path_lengths = np.zeros(diameter + 1, dtype=int)
682
+ for pls in shortest_path_lengths.values():
683
+ pl, cnts = np.unique(list(pls.values()), return_counts=True)
684
+ path_lengths[pl] += cnts
685
+ max_diameter = diameter
686
+ title_suffix = ""
687
+
688
+ # Generate visualization and results (same for both cases)
689
+ freq_percent = 100 * path_lengths[1:] / path_lengths[1:].sum()
690
+ fig, ax = plt.subplots(figsize=(15, 8))
691
+ ax.bar(np.arange(1, max_diameter + 1), height=freq_percent)
692
+ ax.set_title(
693
+ f"Distribution of shortest path length in G{title_suffix}",
694
+ fontdict={"size": 35}, loc="center"
695
+ )
696
+ ax.set_xlabel("Shortest Path Length", fontdict={"size": 22})
697
+ ax.set_ylabel("Frequency (%)", fontdict={"size": 22})
698
+ plt.show()
699
+
700
+ freq_dict = {freq: length for length, freq in enumerate(freq_percent, start=1)}
701
+ self.network_analysis.format_for_upperright_table(
702
+ freq_dict,
703
+ metric='Frequency (%)',
704
+ value='Shortest Path Length',
705
+ title=f"Distribution of shortest path length in G{title_suffix}"
706
+ )
707
+
708
+ except Exception as e:
709
+ print(f"Error generating shortest path histogram: {e}")
710
+
711
+ def degree_centrality_histogram(self):
712
+ try:
713
+ degree_centrality = nx.centrality.degree_centrality(self.G)
714
+ plt.figure(figsize=(15, 8))
715
+ plt.hist(degree_centrality.values(), bins=25)
716
+ plt.xticks(ticks=[0, 0.025, 0.05, 0.1, 0.15, 0.2])
717
+ plt.title("Degree Centrality Histogram ", fontdict={"size": 35}, loc="center")
718
+ plt.xlabel("Degree Centrality", fontdict={"size": 20})
719
+ plt.ylabel("Counts", fontdict={"size": 20})
720
+ plt.show()
721
+ self.stats_dict['Degree Centrality'] = degree_centrality
722
+ self.network_analysis.format_for_upperright_table(degree_centrality, metric='Node',
723
+ value='Degree Centrality',
724
+ title="Degree Centrality Table")
725
+ except Exception as e:
726
+ print(f"Error generating degree centrality histogram: {e}")
727
+
728
+ def betweenness_centrality_histogram(self):
729
+ try:
730
+ # Check if graph has multiple disconnected components
731
+ components = list(nx.connected_components(self.G))
732
+
733
+ if len(components) > 1:
734
+ print(f"Warning: Graph has {len(components)} disconnected components. Computing betweenness centrality within each component separately.")
735
+
736
+ # Initialize dictionary to collect betweenness centrality from all components
737
+ combined_betweenness_centrality = {}
738
+
739
+ # Process each component separately
740
+ for i, component in enumerate(components):
741
+ if len(component) < 2:
742
+ # For single-node components, betweenness centrality is 0
743
+ for node in component:
744
+ combined_betweenness_centrality[node] = 0.0
745
+ continue
746
+
747
+ # Create subgraph for this component
748
+ subgraph = self.G.subgraph(component)
749
+
750
+ # Compute betweenness centrality for this component
751
+ component_betweenness = nx.centrality.betweenness_centrality(subgraph)
752
+
753
+ # Add to combined results
754
+ combined_betweenness_centrality.update(component_betweenness)
755
+
756
+ betweenness_centrality = combined_betweenness_centrality
757
+ title_suffix = f" (across {len(components)} components)"
758
+
759
+ else:
760
+ # Single component
761
+ betweenness_centrality = nx.centrality.betweenness_centrality(self.G)
762
+ title_suffix = ""
763
+
764
+ # Generate visualization and results (same for both cases)
765
+ plt.figure(figsize=(15, 8))
766
+ plt.hist(betweenness_centrality.values(), bins=100)
767
+ plt.xticks(ticks=[0, 0.02, 0.1, 0.2, 0.3, 0.4, 0.5])
768
+ plt.title(
769
+ f"Betweenness Centrality Histogram{title_suffix}",
770
+ fontdict={"size": 35}, loc="center"
771
+ )
772
+ plt.xlabel("Betweenness Centrality", fontdict={"size": 20})
773
+ plt.ylabel("Counts", fontdict={"size": 20})
774
+ plt.show()
775
+ self.stats_dict['Betweenness Centrality'] = betweenness_centrality
776
+
777
+ self.network_analysis.format_for_upperright_table(
778
+ betweenness_centrality,
779
+ metric='Node',
780
+ value='Betweenness Centrality',
781
+ title=f"Betweenness Centrality Table{title_suffix}"
782
+ )
783
+
784
+ except Exception as e:
785
+ print(f"Error generating betweenness centrality histogram: {e}")
786
+
787
+ def closeness_centrality_histogram(self):
788
+ try:
789
+ closeness_centrality = nx.centrality.closeness_centrality(self.G)
790
+ plt.figure(figsize=(15, 8))
791
+ plt.hist(closeness_centrality.values(), bins=60)
792
+ plt.title("Closeness Centrality Histogram ", fontdict={"size": 35}, loc="center")
793
+ plt.xlabel("Closeness Centrality", fontdict={"size": 20})
794
+ plt.ylabel("Counts", fontdict={"size": 20})
795
+ plt.show()
796
+ self.stats_dict['Closeness Centrality'] = closeness_centrality
797
+ self.network_analysis.format_for_upperright_table(closeness_centrality, metric='Node',
798
+ value='Closeness Centrality',
799
+ title="Closeness Centrality Table")
800
+ except Exception as e:
801
+ print(f"Error generating closeness centrality histogram: {e}")
802
+
803
+ def eigenvector_centrality_histogram(self):
804
+ try:
805
+ eigenvector_centrality = nx.centrality.eigenvector_centrality(self.G_unweighted)
806
+ plt.figure(figsize=(15, 8))
807
+ plt.hist(eigenvector_centrality.values(), bins=60)
808
+ plt.xticks(ticks=[0, 0.01, 0.02, 0.04, 0.06, 0.08])
809
+ plt.title("Eigenvector Centrality Histogram ", fontdict={"size": 35}, loc="center")
810
+ plt.xlabel("Eigenvector Centrality", fontdict={"size": 20})
811
+ plt.ylabel("Counts", fontdict={"size": 20})
812
+ plt.show()
813
+ self.stats_dict['Eigenvector Centrality'] = eigenvector_centrality
814
+ self.network_analysis.format_for_upperright_table(eigenvector_centrality, metric='Node',
815
+ value='Eigenvector Centrality',
816
+ title="Eigenvector Centrality Table")
817
+ except Exception as e:
818
+ print(f"Error generating eigenvector centrality histogram: {e}")
819
+
820
+ def clustering_coefficient_histogram(self):
821
+ try:
822
+ clusters = nx.clustering(self.G_unweighted)
823
+ plt.figure(figsize=(15, 8))
824
+ plt.hist(clusters.values(), bins=50)
825
+ plt.title("Clustering Coefficient Histogram ", fontdict={"size": 35}, loc="center")
826
+ plt.xlabel("Clustering Coefficient", fontdict={"size": 20})
827
+ plt.ylabel("Counts", fontdict={"size": 20})
828
+ plt.show()
829
+ self.stats_dict['Clustering Coefficient'] = clusters
830
+ self.network_analysis.format_for_upperright_table(clusters, metric='Node',
831
+ value='Clustering Coefficient',
832
+ title="Clustering Coefficient Table")
833
+ except Exception as e:
834
+ print(f"Error generating clustering coefficient histogram: {e}")
835
+
836
+ def bridges_analysis(self):
837
+ try:
838
+ bridges = list(nx.bridges(self.G))
839
+ try:
840
+ # Get the existing DataFrame from the model
841
+ original_df = self.network_analysis.network_table.model()._data
842
+
843
+ # Create boolean mask
844
+ mask = pd.Series([False] * len(original_df))
845
+
846
+ for u, v in bridges:
847
+ # Check for both (u,v) and (v,u) orientations
848
+ bridge_mask = (
849
+ ((original_df.iloc[:, 0] == u) & (original_df.iloc[:, 1] == v)) |
850
+ ((original_df.iloc[:, 0] == v) & (original_df.iloc[:, 1] == u))
851
+ )
852
+ mask |= bridge_mask
853
+ # Filter the DataFrame to only include bridge connections
854
+ filtered_df = original_df[mask].copy()
855
+ df_dict = {i: row.tolist() for i, row in enumerate(filtered_df.values)}
856
+ self.network_analysis.format_for_upperright_table(df_dict, metric='Bridge ID', value = ['NodeA', 'NodeB', 'EdgeC'],
857
+ title="Bridges")
858
+ except:
859
+ self.network_analysis.format_for_upperright_table(bridges, metric='Node Pair',
860
+ title="Bridges")
861
+ except Exception as e:
862
+ print(f"Error generating bridges analysis: {e}")
863
+
864
+ def degree_distribution_histogram(self):
865
+ """Raw degree distribution - very useful for understanding network topology"""
866
+ try:
867
+ degrees = [self.G.degree(n) for n in self.G.nodes()]
868
+ plt.figure(figsize=(15, 8))
869
+ plt.hist(degrees, bins=max(30, int(np.sqrt(len(degrees)))), alpha=0.7)
870
+ plt.title("Degree Distribution", fontdict={"size": 35}, loc="center")
871
+ plt.xlabel("Degree", fontdict={"size": 20})
872
+ plt.ylabel("Frequency", fontdict={"size": 20})
873
+ plt.yscale('log') # Often useful for degree distributions
874
+ plt.show()
875
+
876
+ degree_dict = {node: deg for node, deg in self.G.degree()}
877
+ self.network_analysis.format_for_upperright_table(degree_dict, metric='Node',
878
+ value='Degree', title="Degree Distribution Table")
879
+ except Exception as e:
880
+ print(f"Error generating degree distribution histogram: {e}")
881
+
882
+
883
+ def node_connectivity_histogram(self):
884
+ """Local node connectivity - minimum number of nodes that must be removed to disconnect neighbors"""
885
+ try:
886
+ if self.G.number_of_nodes() > 500:
887
+ print("Note this analysis may be slow for large network (>500 nodes)")
888
+ #return
889
+
890
+ connectivity = {}
891
+ for node in self.G.nodes():
892
+ neighbors = list(self.G.neighbors(node))
893
+ if len(neighbors) > 1:
894
+ connectivity[node] = nx.node_connectivity(self.G, neighbors[0], neighbors[1])
895
+ else:
896
+ connectivity[node] = 0
897
+
898
+ plt.figure(figsize=(15, 8))
899
+ plt.hist(connectivity.values(), bins=20, alpha=0.7)
900
+ plt.title("Node Connectivity Distribution", fontdict={"size": 35}, loc="center")
901
+ plt.xlabel("Node Connectivity", fontdict={"size": 20})
902
+ plt.ylabel("Frequency", fontdict={"size": 20})
903
+ plt.show()
904
+ self.stats_dict['Node Connectivity'] = connectivity
905
+ self.network_analysis.format_for_upperright_table(connectivity, metric='Node',
906
+ value='Connectivity', title="Node Connectivity Table")
907
+ except Exception as e:
908
+ print(f"Error generating node connectivity histogram: {e}")
909
+
910
+ def eccentricity_histogram(self):
911
+ """Eccentricity - maximum distance from a node to any other node"""
912
+ try:
913
+ if not nx.is_connected(self.G):
914
+ print("Graph is not connected. Using largest connected component.")
915
+ largest_cc = max(nx.connected_components(self.G), key=len)
916
+ G_cc = self.G.subgraph(largest_cc)
917
+ eccentricity = nx.eccentricity(G_cc)
918
+ else:
919
+ eccentricity = nx.eccentricity(self.G)
920
+
921
+ plt.figure(figsize=(15, 8))
922
+ plt.hist(eccentricity.values(), bins=20, alpha=0.7)
923
+ plt.title("Eccentricity Distribution", fontdict={"size": 35}, loc="center")
924
+ plt.xlabel("Eccentricity", fontdict={"size": 20})
925
+ plt.ylabel("Frequency", fontdict={"size": 20})
926
+ plt.show()
927
+ self.stats_dict['Eccentricity'] = eccentricity
928
+ self.network_analysis.format_for_upperright_table(eccentricity, metric='Node',
929
+ value='Eccentricity', title="Eccentricity Table")
930
+ except Exception as e:
931
+ print(f"Error generating eccentricity histogram: {e}")
932
+
933
+ def kcore_histogram(self):
934
+ """K-core decomposition - identifies cohesive subgroups"""
935
+ try:
936
+ kcore = nx.core_number(self.G_unweighted)
937
+ plt.figure(figsize=(15, 8))
938
+ plt.hist(kcore.values(), bins=max(5, max(kcore.values())), alpha=0.7)
939
+ plt.title("K-Core Distribution", fontdict={"size": 35}, loc="center")
940
+ plt.xlabel("K-Core Number", fontdict={"size": 20})
941
+ plt.ylabel("Frequency", fontdict={"size": 20})
942
+ plt.show()
943
+ self.stats_dict['K-Core'] = kcore
944
+ self.network_analysis.format_for_upperright_table(kcore, metric='Node',
945
+ value='K-Core', title="K-Core Table")
946
+ except Exception as e:
947
+ print(f"Error generating k-core histogram: {e}")
948
+
949
+ def triangle_count_histogram(self):
950
+ """Number of triangles each node participates in"""
951
+ try:
952
+ triangles = nx.triangles(self.G)
953
+ plt.figure(figsize=(15, 8))
954
+ plt.hist(triangles.values(), bins=30, alpha=0.7)
955
+ plt.title("Triangle Count Distribution", fontdict={"size": 35}, loc="center")
956
+ plt.xlabel("Number of Triangles", fontdict={"size": 20})
957
+ plt.ylabel("Frequency", fontdict={"size": 20})
958
+ plt.show()
959
+ self.stats_dict['Triangle Count'] = triangles
960
+ self.network_analysis.format_for_upperright_table(triangles, metric='Node',
961
+ value='Triangle Count', title="Triangle Count Table")
962
+ except Exception as e:
963
+ print(f"Error generating triangle count histogram: {e}")
964
+
965
+ def load_centrality_histogram(self):
966
+ """Load centrality - fraction of shortest paths passing through each node"""
967
+ try:
968
+ if self.G.number_of_nodes() > 1000:
969
+ print("Note this analysis may be slow for large network (>1000 nodes)")
970
+ #return
971
+
972
+ load_centrality = nx.load_centrality(self.G)
973
+ plt.figure(figsize=(15, 8))
974
+ plt.hist(load_centrality.values(), bins=50, alpha=0.7)
975
+ plt.title("Load Centrality Distribution", fontdict={"size": 35}, loc="center")
976
+ plt.xlabel("Load Centrality", fontdict={"size": 20})
977
+ plt.ylabel("Frequency", fontdict={"size": 20})
978
+ plt.show()
979
+ self.stats_dict['Load Centrality'] = load_centrality
980
+ self.network_analysis.format_for_upperright_table(load_centrality, metric='Node',
981
+ value='Load Centrality', title="Load Centrality Table")
982
+ except Exception as e:
983
+ print(f"Error generating load centrality histogram: {e}")
984
+
985
+ def communicability_centrality_histogram(self):
986
+ """Communicability centrality - based on communicability between nodes"""
987
+ try:
988
+ if self.G.number_of_nodes() > 500:
989
+ print("Note this analysis may be slow for large network (>500 nodes)")
990
+ #return
991
+
992
+ # Check if graph has multiple disconnected components
993
+ components = list(nx.connected_components(self.G_unweighted))
994
+
995
+ if len(components) > 1:
996
+ print(f"Warning: Graph has {len(components)} disconnected components. Computing communicability centrality within each component separately.")
997
+
998
+ # Initialize dictionary to collect communicability centrality from all components
999
+ combined_comm_centrality = {}
1000
+
1001
+ # Process each component separately
1002
+ for i, component in enumerate(components):
1003
+ if len(component) < 2:
1004
+ # For single-node components, communicability betweenness centrality is 0
1005
+ for node in component:
1006
+ combined_comm_centrality[node] = 0.0
1007
+ continue
1008
+
1009
+ # Create subgraph for this component
1010
+ subgraph = self.G_unweighted.subgraph(component)
1011
+
1012
+ # Compute communicability betweenness centrality for this component
1013
+ try:
1014
+ component_comm_centrality = nx.communicability_betweenness_centrality(subgraph)
1015
+ # Add to combined results
1016
+ combined_comm_centrality.update(component_comm_centrality)
1017
+ except Exception as comp_e:
1018
+ print(f"Error computing communicability centrality for component {i+1}: {comp_e}")
1019
+ # Set centrality to 0 for nodes in this component if computation fails
1020
+ for node in component:
1021
+ combined_comm_centrality[node] = 0.0
1022
+
1023
+ comm_centrality = combined_comm_centrality
1024
+ title_suffix = f" (across {len(components)} components)"
1025
+
1026
+ else:
1027
+ # Single component
1028
+ comm_centrality = nx.communicability_betweenness_centrality(self.G_unweighted)
1029
+ title_suffix = ""
1030
+
1031
+ # Generate visualization and results (same for both cases)
1032
+ plt.figure(figsize=(15, 8))
1033
+ plt.hist(comm_centrality.values(), bins=50, alpha=0.7)
1034
+ plt.title(
1035
+ f"Communicability Betweenness Centrality Distribution{title_suffix}",
1036
+ fontdict={"size": 35}, loc="center"
1037
+ )
1038
+ plt.xlabel("Communicability Betweenness Centrality", fontdict={"size": 20})
1039
+ plt.ylabel("Frequency", fontdict={"size": 20})
1040
+ self.stats_dict['Communicability Betweenness Centrality'] = comm_centrality
1041
+ plt.show()
1042
+
1043
+ self.network_analysis.format_for_upperright_table(
1044
+ comm_centrality,
1045
+ metric='Node',
1046
+ value='Communicability Betweenness Centrality',
1047
+ title=f"Communicability Betweenness Centrality Table{title_suffix}"
1048
+ )
1049
+
1050
+ except Exception as e:
1051
+ print(f"Error generating communicability betweenness centrality histogram: {e}")
1052
+
1053
+ def harmonic_centrality_histogram(self):
1054
+ """Harmonic centrality - better than closeness for disconnected networks"""
1055
+ try:
1056
+ harmonic_centrality = nx.harmonic_centrality(self.G)
1057
+ plt.figure(figsize=(15, 8))
1058
+ plt.hist(harmonic_centrality.values(), bins=50, alpha=0.7)
1059
+ plt.title("Harmonic Centrality Distribution", fontdict={"size": 35}, loc="center")
1060
+ plt.xlabel("Harmonic Centrality", fontdict={"size": 20})
1061
+ plt.ylabel("Frequency", fontdict={"size": 20})
1062
+ plt.show()
1063
+ self.stats_dict['Harmonic Centrality Distribution'] = harmonic_centrality
1064
+ self.network_analysis.format_for_upperright_table(harmonic_centrality, metric='Node',
1065
+ value='Harmonic Centrality',
1066
+ title="Harmonic Centrality Table")
1067
+ except Exception as e:
1068
+ print(f"Error generating harmonic centrality histogram: {e}")
1069
+
1070
+ def current_flow_betweenness_histogram(self):
1071
+ """Current flow betweenness - models network as electrical circuit"""
1072
+ try:
1073
+ if self.G.number_of_nodes() > 500:
1074
+ print("Note this analysis may be slow for large network (>500 nodes)")
1075
+ #return
1076
+
1077
+ # Check if graph has multiple disconnected components
1078
+ components = list(nx.connected_components(self.G))
1079
+
1080
+ if len(components) > 1:
1081
+ print(f"Warning: Graph has {len(components)} disconnected components. Computing current flow betweenness centrality within each component separately.")
1082
+
1083
+ # Initialize dictionary to collect current flow betweenness from all components
1084
+ combined_current_flow = {}
1085
+
1086
+ # Process each component separately
1087
+ for i, component in enumerate(components):
1088
+ if len(component) < 2:
1089
+ # For single-node components, current flow betweenness centrality is 0
1090
+ for node in component:
1091
+ combined_current_flow[node] = 0.0
1092
+ continue
1093
+
1094
+ # Create subgraph for this component
1095
+ subgraph = self.G.subgraph(component)
1096
+
1097
+ # Compute current flow betweenness centrality for this component
1098
+ try:
1099
+ component_current_flow = nx.current_flow_betweenness_centrality(subgraph)
1100
+ # Add to combined results
1101
+ combined_current_flow.update(component_current_flow)
1102
+ except Exception as comp_e:
1103
+ print(f"Error computing current flow betweenness for component {i+1}: {comp_e}")
1104
+ # Set centrality to 0 for nodes in this component if computation fails
1105
+ for node in component:
1106
+ combined_current_flow[node] = 0.0
1107
+
1108
+ current_flow = combined_current_flow
1109
+ title_suffix = f" (across {len(components)} components)"
1110
+
1111
+ else:
1112
+ # Single component
1113
+ current_flow = nx.current_flow_betweenness_centrality(self.G)
1114
+ title_suffix = ""
1115
+
1116
+ # Generate visualization and results (same for both cases)
1117
+ plt.figure(figsize=(15, 8))
1118
+ plt.hist(current_flow.values(), bins=50, alpha=0.7)
1119
+ plt.title(
1120
+ f"Current Flow Betweenness Centrality Distribution{title_suffix}",
1121
+ fontdict={"size": 35}, loc="center"
1122
+ )
1123
+ plt.xlabel("Current Flow Betweenness Centrality", fontdict={"size": 20})
1124
+ plt.ylabel("Frequency", fontdict={"size": 20})
1125
+ plt.show()
1126
+ self.stats_dict['Current Flow Betweenness Centrality'] = current_flow
1127
+ self.network_analysis.format_for_upperright_table(
1128
+ current_flow,
1129
+ metric='Node',
1130
+ value='Current Flow Betweenness',
1131
+ title=f"Current Flow Betweenness Table{title_suffix}"
1132
+ )
1133
+
1134
+ except Exception as e:
1135
+ print(f"Error generating current flow betweenness histogram: {e}")
1136
+
1137
+ def dispersion_histogram(self):
1138
+ """Dispersion - measures how scattered a node's neighbors are"""
1139
+ try:
1140
+ if self.G.number_of_nodes() > 300: # Skip for large networks (very computationally expensive)
1141
+ print("Note this analysis may be slow for large network (>300 nodes)")
1142
+ #return
1143
+
1144
+ # Calculate average dispersion for each node
1145
+ dispersion_values = {}
1146
+ nodes = list(self.G.nodes())
1147
+
1148
+ for u in nodes:
1149
+ if self.G.degree(u) < 2: # Need at least 2 neighbors for dispersion
1150
+ dispersion_values[u] = 0
1151
+ continue
1152
+
1153
+ # Calculate dispersion for node u with all its neighbors
1154
+ neighbors = list(self.G.neighbors(u))
1155
+ if len(neighbors) < 2:
1156
+ dispersion_values[u] = 0
1157
+ continue
1158
+
1159
+ # Get dispersion scores for this node with all neighbors
1160
+ disp_scores = []
1161
+ for v in neighbors:
1162
+ try:
1163
+ disp_score = nx.dispersion(self.G, u, v)
1164
+ disp_scores.append(disp_score)
1165
+ except:
1166
+ continue
1167
+
1168
+ # Average dispersion for this node
1169
+ dispersion_values[u] = sum(disp_scores) / len(disp_scores) if disp_scores else 0
1170
+
1171
+ plt.figure(figsize=(15, 8))
1172
+ plt.hist(dispersion_values.values(), bins=30, alpha=0.7)
1173
+ plt.title("Average Dispersion Distribution", fontdict={"size": 35}, loc="center")
1174
+ plt.xlabel("Average Dispersion", fontdict={"size": 20})
1175
+ plt.ylabel("Frequency", fontdict={"size": 20})
1176
+ plt.show()
1177
+ self.stats_dict['Dispersion'] = dispersion_values
1178
+ self.network_analysis.format_for_upperright_table(dispersion_values, metric='Node',
1179
+ value='Average Dispersion',
1180
+ title="Average Dispersion Table")
1181
+ except Exception as e:
1182
+ print(f"Error generating dispersion histogram: {e}")