nettracer3d 1.3.1__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.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- nettracer3d/community_extractor.py +3 -2
- nettracer3d/endpoint_joiner.py +286 -0
- nettracer3d/filaments.py +348 -106
- nettracer3d/histos.py +1182 -0
- nettracer3d/modularity.py +14 -96
- nettracer3d/neighborhoods.py +3 -2
- nettracer3d/nettracer.py +91 -50
- nettracer3d/nettracer_gui.py +359 -803
- nettracer3d/network_analysis.py +12 -5
- nettracer3d/network_graph_widget.py +302 -101
- nettracer3d/segmenter.py +1 -1
- nettracer3d/segmenter_GPU.py +0 -1
- nettracer3d/tutorial.py +41 -25
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/METADATA +4 -6
- nettracer3d-1.3.6.dist-info/RECORD +32 -0
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/WHEEL +1 -1
- nettracer3d-1.3.1.dist-info/RECORD +0 -30
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/entry_points.txt +0 -0
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-1.3.1.dist-info → nettracer3d-1.3.6.dist-info}/top_level.txt +0 -0
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}")
|