nettracer3d 0.2.5__tar.gz → 0.2.7__tar.gz
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-0.2.5/src/nettracer3d.egg-info → nettracer3d-0.2.7}/PKG-INFO +1 -1
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/pyproject.toml +1 -1
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/community_extractor.py +153 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/hub_getter.py +1 -1
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/modularity.py +263 -19
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/morphology.py +144 -7
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/nettracer.py +542 -48
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/nettracer_gui.py +1685 -255
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/network_analysis.py +96 -26
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/node_draw.py +3 -1
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/proximity.py +66 -6
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/simple_network.py +82 -72
- {nettracer3d-0.2.5 → nettracer3d-0.2.7/src/nettracer3d.egg-info}/PKG-INFO +1 -1
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/LICENSE +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/README.md +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/setup.cfg +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d.egg-info/requires.txt +0 -0
- {nettracer3d-0.2.5 → nettracer3d-0.2.7}/src/nettracer3d.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <boom2449@gmail.com>
|
|
6
6
|
Project-URL: User_Manual, https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link
|
|
@@ -2,6 +2,8 @@ import pandas as pd
|
|
|
2
2
|
import networkx as nx
|
|
3
3
|
import tifffile
|
|
4
4
|
import numpy as np
|
|
5
|
+
from typing import List, Dict, Tuple
|
|
6
|
+
from collections import defaultdict, Counter
|
|
5
7
|
from networkx.algorithms import community
|
|
6
8
|
from scipy import ndimage
|
|
7
9
|
from scipy.ndimage import zoom
|
|
@@ -601,8 +603,159 @@ def extract_mothers(nodes, excel_file_path, centroid_dic = None, directory = Non
|
|
|
601
603
|
|
|
602
604
|
|
|
603
605
|
|
|
606
|
+
def find_hub_nodes(G: nx.Graph, proportion: float = 0.1) -> List:
|
|
607
|
+
"""
|
|
608
|
+
Identifies hub nodes in a network based on average shortest path length,
|
|
609
|
+
handling multiple connected components.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
G (nx.Graph): NetworkX graph (can have multiple components)
|
|
613
|
+
proportion (float): Proportion of top nodes to return (0.0 to 1.0)
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
List of nodes identified as hubs across all components
|
|
617
|
+
"""
|
|
618
|
+
if not 0 < proportion <= 1:
|
|
619
|
+
raise ValueError("Proportion must be between 0 and 1")
|
|
620
|
+
|
|
621
|
+
# Get connected components
|
|
622
|
+
components = list(nx.connected_components(G))
|
|
623
|
+
|
|
624
|
+
# Dictionary to store average path lengths for all nodes
|
|
625
|
+
avg_path_lengths: Dict[int, float] = {}
|
|
626
|
+
|
|
627
|
+
# Process each component separately
|
|
628
|
+
for component in components:
|
|
629
|
+
# Create subgraph for this component
|
|
630
|
+
subgraph = G.subgraph(component)
|
|
631
|
+
|
|
632
|
+
# Calculate average shortest path length for each node in this component
|
|
633
|
+
for node in subgraph.nodes():
|
|
634
|
+
# Get shortest paths from this node to all others in the component
|
|
635
|
+
path_lengths = nx.single_source_shortest_path_length(subgraph, node)
|
|
636
|
+
# Calculate average path length within this component
|
|
637
|
+
avg_length = sum(path_lengths.values()) / (len(subgraph.nodes()) - 1)
|
|
638
|
+
avg_path_lengths[node] = avg_length
|
|
639
|
+
|
|
640
|
+
# Sort nodes by average path length (ascending)
|
|
641
|
+
sorted_nodes = sorted(avg_path_lengths.items(), key=lambda x: x[1])
|
|
642
|
+
|
|
643
|
+
# Calculate number of nodes to return
|
|
644
|
+
num_nodes = int(np.ceil(len(G.nodes()) * proportion))
|
|
645
|
+
|
|
646
|
+
# Return the top nodes (those with lowest average path lengths)
|
|
647
|
+
hub_nodes = [node for node, _ in sorted_nodes[:num_nodes]]
|
|
648
|
+
|
|
649
|
+
return hub_nodes
|
|
650
|
+
|
|
651
|
+
|
|
604
652
|
|
|
653
|
+
def generate_distinct_colors(n_colors: int) -> List[Tuple[int, int, int]]:
|
|
654
|
+
"""
|
|
655
|
+
Generate visually distinct RGB colors using HSV color space.
|
|
656
|
+
Colors are generated with maximum saturation and value, varying only in hue.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
n_colors: Number of distinct colors needed
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
List of RGB tuples
|
|
663
|
+
"""
|
|
664
|
+
colors = []
|
|
665
|
+
for i in range(n_colors):
|
|
666
|
+
hue = i / n_colors
|
|
667
|
+
# Convert HSV to RGB (assuming S=V=1)
|
|
668
|
+
h = hue * 6
|
|
669
|
+
c = int(255)
|
|
670
|
+
x = int(255 * (1 - abs(h % 2 - 1)))
|
|
671
|
+
|
|
672
|
+
if h < 1:
|
|
673
|
+
rgb = (c, x, 0)
|
|
674
|
+
elif h < 2:
|
|
675
|
+
rgb = (x, c, 0)
|
|
676
|
+
elif h < 3:
|
|
677
|
+
rgb = (0, c, x)
|
|
678
|
+
elif h < 4:
|
|
679
|
+
rgb = (0, x, c)
|
|
680
|
+
elif h < 5:
|
|
681
|
+
rgb = (x, 0, c)
|
|
682
|
+
else:
|
|
683
|
+
rgb = (c, 0, x)
|
|
684
|
+
|
|
685
|
+
colors.append(rgb)
|
|
686
|
+
return colors
|
|
605
687
|
|
|
688
|
+
def assign_community_colors(community_dict: Dict[int, int], labeled_array: np.ndarray) -> np.ndarray:
|
|
689
|
+
"""
|
|
690
|
+
Assign distinct colors to communities and create an RGB image.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
community_dict: Dictionary mapping node IDs to community numbers
|
|
694
|
+
labeled_array: 3D numpy array with labels corresponding to node IDs
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
RGB-coded numpy array (H, W, D, 3)
|
|
698
|
+
"""
|
|
699
|
+
# Get unique communities and their sizes
|
|
700
|
+
communities = set(community_dict.values())
|
|
701
|
+
community_sizes = Counter(community_dict.values())
|
|
702
|
+
|
|
703
|
+
# Sort communities by size (descending)
|
|
704
|
+
sorted_communities = sorted(communities, key=lambda x: community_sizes[x], reverse=True)
|
|
705
|
+
|
|
706
|
+
# Generate distinct colors
|
|
707
|
+
colors = generate_distinct_colors(len(communities))
|
|
708
|
+
|
|
709
|
+
# Create mapping from community to color
|
|
710
|
+
community_to_color = {comm: colors[i] for i, comm in enumerate(sorted_communities)}
|
|
711
|
+
|
|
712
|
+
# Create mapping from node ID to color
|
|
713
|
+
node_to_color = {node: community_to_color[comm] for node, comm in community_dict.items()}
|
|
714
|
+
|
|
715
|
+
# Create RGB array
|
|
716
|
+
rgb_array = np.zeros((*labeled_array.shape, 3), dtype=np.uint8)
|
|
717
|
+
|
|
718
|
+
# Assign colors to each voxel based on its label
|
|
719
|
+
for label in np.unique(labeled_array):
|
|
720
|
+
if label in node_to_color: # Skip background (usually label 0)
|
|
721
|
+
mask = labeled_array == label
|
|
722
|
+
for i in range(3): # RGB channels
|
|
723
|
+
rgb_array[mask, i] = node_to_color[label][i]
|
|
724
|
+
|
|
725
|
+
return rgb_array
|
|
726
|
+
|
|
727
|
+
def assign_community_grays(community_dict: Dict[int, int], labeled_array: np.ndarray) -> np.ndarray:
|
|
728
|
+
"""
|
|
729
|
+
Assign distinct grayscale values to communities.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
community_dict: Dictionary mapping node IDs to community numbers
|
|
733
|
+
labeled_array: 3D numpy array with labels corresponding to node IDs
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
grayscale numpy array
|
|
737
|
+
"""
|
|
738
|
+
# Get unique communities
|
|
739
|
+
communities = set(community_dict.values())
|
|
740
|
+
n_communities = len(communities)
|
|
741
|
+
|
|
742
|
+
# Generate evenly spaced grayscale values (excluding pure black for background)
|
|
743
|
+
gray_values = np.linspace(1, 255, n_communities, dtype=np.uint8)
|
|
744
|
+
|
|
745
|
+
# Create direct mapping from node ID to grayscale value
|
|
746
|
+
node_to_gray = {node: gray_values[list(communities).index(comm)]
|
|
747
|
+
for node, comm in community_dict.items()}
|
|
748
|
+
|
|
749
|
+
# Create output array
|
|
750
|
+
gray_array = np.zeros_like(labeled_array, dtype=np.uint8)
|
|
751
|
+
|
|
752
|
+
# Use numpy's vectorized operations for faster assignment
|
|
753
|
+
unique_labels = np.unique(labeled_array)
|
|
754
|
+
for label in unique_labels:
|
|
755
|
+
if label in node_to_gray:
|
|
756
|
+
gray_array[labeled_array == label] = node_to_gray[label]
|
|
757
|
+
|
|
758
|
+
return gray_array
|
|
606
759
|
|
|
607
760
|
|
|
608
761
|
if __name__ == "__main__":
|
|
@@ -115,7 +115,7 @@ def labels_to_boolean(label_array, labels_list):
|
|
|
115
115
|
|
|
116
116
|
return boolean_array
|
|
117
117
|
|
|
118
|
-
def get_hubs(nodepath, network, proportion = None, directory = None, centroids = None):
|
|
118
|
+
def get_hubs(nodepath, network, proportion = None, directory = None, centroids = None, gen_more_images = False):
|
|
119
119
|
|
|
120
120
|
if type(nodepath) == str:
|
|
121
121
|
nodepath = tifffile.imread(nodepath)
|
|
@@ -7,6 +7,8 @@ import matplotlib.colors as mcolors
|
|
|
7
7
|
import os
|
|
8
8
|
from . import network_analysis
|
|
9
9
|
from . import simple_network
|
|
10
|
+
import numpy as np
|
|
11
|
+
import itertools
|
|
10
12
|
|
|
11
13
|
def open_network(excel_file_path):
|
|
12
14
|
|
|
@@ -159,16 +161,7 @@ def louvain_mod(G, edge_weights=None, identifier=None, geometric = False, geo_in
|
|
|
159
161
|
for i, component in enumerate(connected_components):
|
|
160
162
|
# Apply the Louvain community detection on the subgraph
|
|
161
163
|
partition = community_louvain.best_partition(G.subgraph(component))
|
|
162
|
-
|
|
163
|
-
# Invert the partition dictionary to get communities
|
|
164
|
-
#communities = {}
|
|
165
|
-
#for node, comm_id in partition.items():
|
|
166
|
-
#communities.setdefault(comm_id, []).append(node)
|
|
167
|
-
#communities = list(communities.values())
|
|
168
|
-
|
|
169
|
-
# Assign a different color to each community within the component for visualization
|
|
170
|
-
#colors = [mcolors.to_hex(plt.cm.tab10(i / len(connected_components))[:3]) for _ in range(len(communities))]
|
|
171
|
-
|
|
164
|
+
|
|
172
165
|
# Calculate modularity
|
|
173
166
|
modularity = community_louvain.modularity(partition, G.subgraph(component))
|
|
174
167
|
num_nodes = len(component)
|
|
@@ -180,7 +173,7 @@ def louvain_mod(G, edge_weights=None, identifier=None, geometric = False, geo_in
|
|
|
180
173
|
|
|
181
174
|
def show_communities_flex(G, master_list, normalized_weights, geo_info = None, geometric=False, directory=None, weighted=True, partition=None, style=0):
|
|
182
175
|
if partition is None:
|
|
183
|
-
partition, normalized_weights = community_partition(master_list, weighted=weighted, style=style)
|
|
176
|
+
partition, normalized_weights, _ = community_partition(master_list, weighted=weighted, style=style)
|
|
184
177
|
print(partition)
|
|
185
178
|
if normalized_weights is None:
|
|
186
179
|
G, edge_weights = network_analysis.weighted_network(master_list)
|
|
@@ -289,7 +282,242 @@ def show_communities_flex(G, master_list, normalized_weights, geo_info = None, g
|
|
|
289
282
|
|
|
290
283
|
|
|
291
284
|
|
|
292
|
-
def community_partition(master_list, weighted = False, style = 0):
|
|
285
|
+
def community_partition(master_list, weighted = False, style = 0, dostats = True):
|
|
286
|
+
|
|
287
|
+
def calculate_network_stats(G, communities):
|
|
288
|
+
"""
|
|
289
|
+
Calculate comprehensive network statistics for the graph and its communities.
|
|
290
|
+
|
|
291
|
+
Parameters:
|
|
292
|
+
-----------
|
|
293
|
+
G : networkx.Graph
|
|
294
|
+
The input graph
|
|
295
|
+
communities : list
|
|
296
|
+
List of sets/lists containing node ids for each community
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
--------
|
|
300
|
+
dict
|
|
301
|
+
Dictionary containing various network statistics
|
|
302
|
+
"""
|
|
303
|
+
stats = {}
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
|
|
307
|
+
# Overall network modularity
|
|
308
|
+
stats['Modularity Entire Network'] = community.modularity(G, communities)
|
|
309
|
+
except:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
# Component-level modularity
|
|
314
|
+
connected_components = list(nx.connected_components(G))
|
|
315
|
+
if len(connected_components) > 1:
|
|
316
|
+
for i, component in enumerate(connected_components):
|
|
317
|
+
subgraph = G.subgraph(component)
|
|
318
|
+
component_communities = list(community.label_propagation_communities(subgraph))
|
|
319
|
+
modularity = community.modularity(subgraph, component_communities)
|
|
320
|
+
num_nodes = len(component)
|
|
321
|
+
stats[f'Modularity of component with {num_nodes} nodes'] = modularity
|
|
322
|
+
except:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Community size statistics
|
|
327
|
+
stats['Number of Communities'] = len(communities)
|
|
328
|
+
community_sizes = [len(com) for com in communities]
|
|
329
|
+
stats['Community Sizes'] = community_sizes
|
|
330
|
+
stats['Average Community Size'] = np.mean(community_sizes)
|
|
331
|
+
except:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
|
|
336
|
+
# Per-community statistics
|
|
337
|
+
for i, com in enumerate(communities):
|
|
338
|
+
subgraph = G.subgraph(com)
|
|
339
|
+
|
|
340
|
+
# Basic community metrics
|
|
341
|
+
stats[f'Community {i+1} Density'] = nx.density(subgraph)
|
|
342
|
+
stats[f'Community {i+1} Conductance'] = nx.conductance(G, com)
|
|
343
|
+
stats[f'Community {i+1} Avg Clustering'] = nx.average_clustering(subgraph)
|
|
344
|
+
|
|
345
|
+
# Degree centrality
|
|
346
|
+
degree_cent = nx.degree_centrality(subgraph)
|
|
347
|
+
stats[f'Community {i+1} Avg Degree Centrality'] = np.mean(list(degree_cent.values()))
|
|
348
|
+
|
|
349
|
+
# Average path length (only for connected subgraphs)
|
|
350
|
+
if nx.is_connected(subgraph):
|
|
351
|
+
stats[f'Community {i+1} Avg Path Length'] = nx.average_shortest_path_length(subgraph)
|
|
352
|
+
except:
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
# Global network metrics
|
|
357
|
+
stats['Global Clustering Coefficient'] = nx.average_clustering(G)
|
|
358
|
+
except:
|
|
359
|
+
pass
|
|
360
|
+
try:
|
|
361
|
+
stats['Assortativity'] = nx.degree_assortativity_coefficient(G)
|
|
362
|
+
except:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
def count_inter_community_edges(G, communities):
|
|
366
|
+
inter_edges = 0
|
|
367
|
+
for com1, com2 in itertools.combinations(communities, 2):
|
|
368
|
+
inter_edges += len(list(nx.edge_boundary(G, com1, com2)))
|
|
369
|
+
return inter_edges
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
stats['Inter-community Edges'] = count_inter_community_edges(G, communities)
|
|
373
|
+
except:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
# Calculate mixing parameter (ratio of external to total edges for nodes)
|
|
377
|
+
def mixing_parameter(G, communities):
|
|
378
|
+
external_edges = 0
|
|
379
|
+
total_edges = 0
|
|
380
|
+
for com in communities:
|
|
381
|
+
subgraph = G.subgraph(com)
|
|
382
|
+
internal_edges = subgraph.number_of_edges()
|
|
383
|
+
total_com_edges = sum(G.degree(node) for node in com)
|
|
384
|
+
external_edges += total_com_edges - (2 * internal_edges)
|
|
385
|
+
total_edges += total_com_edges
|
|
386
|
+
return external_edges / total_edges
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
stats['Mixing Parameter'] = mixing_parameter(G, communities)
|
|
390
|
+
except:
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
return stats
|
|
394
|
+
|
|
395
|
+
def calculate_louvain_network_stats(G, partition):
|
|
396
|
+
"""
|
|
397
|
+
Calculate comprehensive network statistics for the graph using Louvain community detection.
|
|
398
|
+
|
|
399
|
+
Parameters:
|
|
400
|
+
-----------
|
|
401
|
+
G : networkx.Graph
|
|
402
|
+
The input graph
|
|
403
|
+
partition : dict
|
|
404
|
+
Dictionary mapping node -> community id from Louvain detection
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
--------
|
|
408
|
+
dict
|
|
409
|
+
Dictionary containing various network statistics
|
|
410
|
+
"""
|
|
411
|
+
stats = {}
|
|
412
|
+
|
|
413
|
+
# Convert partition dict to communities list format
|
|
414
|
+
communities = []
|
|
415
|
+
max_community = max(partition.values())
|
|
416
|
+
for com_id in range(max_community + 1):
|
|
417
|
+
community_nodes = {node for node, com in partition.items() if com == com_id}
|
|
418
|
+
if community_nodes: # Only add non-empty communities
|
|
419
|
+
communities.append(community_nodes)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
# Overall network modularity using Louvain
|
|
423
|
+
stats['Modularity Entire Network'] = community_louvain.modularity(partition, G)
|
|
424
|
+
except:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
# Component-level modularity
|
|
429
|
+
connected_components = list(nx.connected_components(G))
|
|
430
|
+
if len(connected_components) > 1:
|
|
431
|
+
for i, component in enumerate(connected_components):
|
|
432
|
+
subgraph = G.subgraph(component)
|
|
433
|
+
subgraph_partition = community_louvain.best_partition(subgraph)
|
|
434
|
+
modularity = community_louvain.modularity(subgraph_partition, subgraph)
|
|
435
|
+
num_nodes = len(component)
|
|
436
|
+
stats[f'Modularity of component with {num_nodes} nodes'] = modularity
|
|
437
|
+
except:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
# Community size statistics
|
|
442
|
+
stats['Number of Communities'] = len(communities)
|
|
443
|
+
community_sizes = [len(com) for com in communities]
|
|
444
|
+
stats['Community Sizes'] = community_sizes
|
|
445
|
+
stats['Average Community Size'] = np.mean(community_sizes)
|
|
446
|
+
except:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
# Per-community statistics
|
|
451
|
+
for i, com in enumerate(communities):
|
|
452
|
+
subgraph = G.subgraph(com)
|
|
453
|
+
|
|
454
|
+
# Basic community metrics
|
|
455
|
+
stats[f'Community {i+1} Density'] = nx.density(subgraph)
|
|
456
|
+
stats[f'Community {i+1} Conductance'] = nx.conductance(G, com)
|
|
457
|
+
stats[f'Community {i+1} Avg Clustering'] = nx.average_clustering(subgraph)
|
|
458
|
+
|
|
459
|
+
# Degree centrality
|
|
460
|
+
degree_cent = nx.degree_centrality(subgraph)
|
|
461
|
+
stats[f'Community {i+1} Avg Degree Centrality'] = np.mean(list(degree_cent.values()))
|
|
462
|
+
|
|
463
|
+
# Average path length (only for connected subgraphs)
|
|
464
|
+
if nx.is_connected(subgraph):
|
|
465
|
+
stats[f'Community {i+1} Avg Path Length'] = nx.average_shortest_path_length(subgraph)
|
|
466
|
+
except:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
# Add some Louvain-specific statistics
|
|
471
|
+
stats['Partition Resolution'] = 1.0 # Default resolution parameter
|
|
472
|
+
except:
|
|
473
|
+
pass
|
|
474
|
+
try:
|
|
475
|
+
stats['Number of Iterations'] = len(set(partition.values()))
|
|
476
|
+
except:
|
|
477
|
+
pass
|
|
478
|
+
|
|
479
|
+
# Global network metrics
|
|
480
|
+
try:
|
|
481
|
+
stats['Global Clustering Coefficient'] = nx.average_clustering(G)
|
|
482
|
+
except:
|
|
483
|
+
pass
|
|
484
|
+
try:
|
|
485
|
+
stats['Assortativity'] = nx.degree_assortativity_coefficient(G)
|
|
486
|
+
except:
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
def count_inter_community_edges(G, communities):
|
|
490
|
+
inter_edges = 0
|
|
491
|
+
for com1, com2 in itertools.combinations(communities, 2):
|
|
492
|
+
inter_edges += len(list(nx.edge_boundary(G, com1, com2)))
|
|
493
|
+
return inter_edges
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
stats['Inter-community Edges'] = count_inter_community_edges(G, communities)
|
|
497
|
+
except:
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
# Calculate mixing parameter (ratio of external to total edges for nodes)
|
|
501
|
+
def mixing_parameter(G, communities):
|
|
502
|
+
external_edges = 0
|
|
503
|
+
total_edges = 0
|
|
504
|
+
for com in communities:
|
|
505
|
+
subgraph = G.subgraph(com)
|
|
506
|
+
internal_edges = subgraph.number_of_edges()
|
|
507
|
+
total_com_edges = sum(G.degree(node) for node in com)
|
|
508
|
+
external_edges += total_com_edges - (2 * internal_edges)
|
|
509
|
+
total_edges += total_com_edges
|
|
510
|
+
return external_edges / total_edges
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
stats['Mixing Parameter'] = mixing_parameter(G, communities)
|
|
514
|
+
except:
|
|
515
|
+
pass
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
return stats
|
|
519
|
+
|
|
520
|
+
stats = {}
|
|
293
521
|
|
|
294
522
|
if weighted:
|
|
295
523
|
G, edge_weights = network_analysis.weighted_network(master_list)
|
|
@@ -316,7 +544,12 @@ def community_partition(master_list, weighted = False, style = 0):
|
|
|
316
544
|
# Perform Louvain community detection
|
|
317
545
|
partition = community_louvain.best_partition(G)
|
|
318
546
|
|
|
319
|
-
|
|
547
|
+
if dostats:
|
|
548
|
+
|
|
549
|
+
stats = calculate_louvain_network_stats(G, partition)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
return partition, normalized_weights, stats
|
|
320
553
|
|
|
321
554
|
elif style == 1:
|
|
322
555
|
|
|
@@ -330,7 +563,11 @@ def community_partition(master_list, weighted = False, style = 0):
|
|
|
330
563
|
# Perform Louvain community detection
|
|
331
564
|
partition = community_louvain.best_partition(G)
|
|
332
565
|
|
|
333
|
-
|
|
566
|
+
if dostats:
|
|
567
|
+
|
|
568
|
+
stats = calculate_louvain_network_stats(G, partition)
|
|
569
|
+
|
|
570
|
+
return partition, None, stats
|
|
334
571
|
|
|
335
572
|
elif style == 0 and weighted:
|
|
336
573
|
|
|
@@ -357,7 +594,13 @@ def community_partition(master_list, weighted = False, style = 0):
|
|
|
357
594
|
for node in com:
|
|
358
595
|
output[node] = i + 1
|
|
359
596
|
|
|
360
|
-
|
|
597
|
+
if dostats:
|
|
598
|
+
|
|
599
|
+
stats = calculate_network_stats(G, communities)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
return output, normalized_weights, stats
|
|
361
604
|
|
|
362
605
|
elif style == 0:
|
|
363
606
|
|
|
@@ -370,9 +613,6 @@ def community_partition(master_list, weighted = False, style = 0):
|
|
|
370
613
|
# Add edges from the DataFrame
|
|
371
614
|
G.add_edges_from(edges)
|
|
372
615
|
|
|
373
|
-
# Detect communities using label propagation
|
|
374
|
-
communities = list(community.label_propagation_communities(G))
|
|
375
|
-
output = {}
|
|
376
616
|
|
|
377
617
|
# Detect communities using label propagation
|
|
378
618
|
communities = list(community.label_propagation_communities(G))
|
|
@@ -381,7 +621,11 @@ def community_partition(master_list, weighted = False, style = 0):
|
|
|
381
621
|
for node in com:
|
|
382
622
|
output[node] = i + 1
|
|
383
623
|
|
|
384
|
-
|
|
624
|
+
if dostats:
|
|
625
|
+
|
|
626
|
+
stats = calculate_network_stats(G, communities)
|
|
627
|
+
|
|
628
|
+
return output, None, stats
|
|
385
629
|
|
|
386
630
|
|
|
387
631
|
|