Jomaric's picture
feat: initial release of network analyzer space
d4b1fe3
Raw
History Blame Contribute Delete
9.55 kB
import gradio as gr
import pandas as pd
import networkx as nx
import networkx.community as nx_comm
from pyvis.network import Network
import tempfile
import os
# Premium color palette for nodes grouped by community
COMMUNITY_COLORS = [
"#ff7043", # Hot Orange
"#4db6ac", # Teal Accent
"#9575cd", # Deep Purple
"#ffd54f", # Warm Yellow
"#64b5f6", # Ocean Blue
"#f06292", # Vivid Pink
"#81c784", # Forest Green
"#ffffff", # Pure White
"#a1887f", # Warm Clay
"#ba68c8" # Violet
]
def run_louvain_communities(df, is_directed, seed_val=42):
G = nx.from_pandas_edgelist(df, 'Source', 'Target', create_using=nx.DiGraph() if is_directed else nx.Graph())
# Louvain communities require undirected graph for computation in standard networkx
G_undirected = G.to_undirected() if is_directed else G
try:
communities = nx_comm.louvain_communities(G_undirected, seed=seed_val)
# Calculate modularity
modularity = nx_comm.modularity(G_undirected, communities)
except Exception as e:
# Fallback to label propagation if Louvain fails for any reason
communities = list(nx_comm.label_propagation_communities(G_undirected))
modularity = nx_comm.modularity(G_undirected, communities)
# Build results table
node_to_community = {}
community_records = []
for comm_idx, comm in enumerate(communities):
comm_color = COMMUNITY_COLORS[comm_idx % len(COMMUNITY_COLORS)]
members = sorted(list(comm))
members_str = ", ".join(members)
community_records.append({
"Community ID": comm_idx + 1,
"Node Count": len(comm),
"Members": members_str
})
for node in comm:
node_to_community[node] = {
"Community ID": comm_idx + 1,
"Color": comm_color
}
df_comm_summary = pd.DataFrame(community_records).sort_values("Node Count", ascending=False)
node_records = []
for node, info in node_to_community.items():
node_records.append({
"Node": node,
"Community ID": info["Community ID"]
})
df_nodes = pd.DataFrame(node_records).sort_values("Community ID")
return G, df_comm_summary, df_nodes, node_to_community, modularity
def generate_vis_html(G, node_to_community):
net = Network(
height="500px",
width="100%",
bgcolor="#16100c",
font_color="#f4eee6",
notebook=False
)
net.set_options("""
var options = {
"nodes": {
"borderWidth": 2,
"font": {
"color": "#f4eee6",
"size": 14,
"face": "Inter, sans-serif"
}
},
"edges": {
"color": {
"color": "rgba(255, 255, 255, 0.15)",
"highlight": "#ff7043"
},
"smooth": {
"type": "continuous"
}
},
"physics": {
"barnesHut": {
"gravitationalConstant": -12000,
"centralGravity": 0.3,
"springLength": 120,
"springConstant": 0.04
}
}
}
""")
degrees = dict(G.degree())
for node in G.nodes():
deg = degrees.get(node, 1)
size = 10 + (deg * 1.5)
info = node_to_community.get(node, {"Community ID": 0, "Color": "#ff7043"})
net.add_node(
node,
label=node,
size=size,
color=info["Color"],
title=f"Community ID: {info['Community ID']} | Connections: {deg}"
)
for edge in G.edges():
net.add_edge(edge[0], edge[1])
temp_dir = tempfile.gettempdir()
temp_path = os.path.join(temp_dir, next(tempfile._get_candidate_names()) + ".html")
net.save_graph(temp_path)
with open(temp_path, "r", encoding="utf-8") as f:
html_content = f.read()
try:
os.remove(temp_path)
except:
pass
escaped_html = html_content.replace('"', '"')
iframe_code = f'<iframe srcdoc="{escaped_html}" style="width: 100%; height: 530px; border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px;"></iframe>'
return iframe_code
def analyze_communities(file_obj, is_directed):
if file_obj is None:
return "Please upload a CSV or Excel network dataset.", "", None, None, None, None
try:
if file_obj.name.endswith('.csv'):
df = pd.read_csv(file_obj.name)
else:
df = pd.read_excel(file_obj.name)
except Exception as e:
return f"Error reading file: {str(e)}", "", None, None, None, None
# Standardize column headers
rename_map = {}
for col in df.columns:
if col.lower() in ['source', 'from', 'node1']:
rename_map[col] = 'Source'
elif col.lower() in ['target', 'to', 'node2']:
rename_map[col] = 'Target'
df = df.rename(columns=rename_map)
if 'Source' not in df.columns or 'Target' not in df.columns:
return "CSV/Excel must contain at least 'Source' and 'Target' columns representing network edges.", "", None, None, None, None
# Calculate
G, df_comm_summary, df_nodes, node_to_community, modularity = run_louvain_communities(df, is_directed)
# Stats
stats_html = f"""
<div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1rem;'>
<div style='background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 1rem; text-align: center;'>
<div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Communities Detected</div>
<div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{len(df_comm_summary)}</div>
</div>
<div style='background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 1rem; text-align: center;'>
<div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Modularity Score</div>
<div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{modularity:.4f}</div>
</div>
<div style='background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 1rem; text-align: center;'>
<div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Total Nodes</div>
<div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{G.number_of_nodes()}</div>
</div>
</div>
"""
# Generate PyVis HTML
vis_html = generate_vis_html(G, node_to_community)
# Download nodes CSV
out_csv = tempfile.mktemp(suffix=".csv")
df_nodes.to_csv(out_csv, index=False)
return "", stats_html, vis_html, df_comm_summary, df_nodes, gr.update(value=out_csv, visible=True)
theme = gr.themes.Default(
primary_hue="orange",
neutral_hue="stone"
).set(
body_background_fill="#0d0907",
body_text_color="#c4bbae",
block_background_fill="#16100c",
block_border_width="1px",
block_label_text_color="#f4eee6"
)
with gr.Blocks(theme=theme, title="Community Detection") as demo:
gr.Markdown(
"""
# 👥 Network Community & Subgroup Detector
### Run structural community partitioning (Louvain Modularity) on network datasets. Automatically group nodes into clusters and color-code subgroups!
"""
)
error_msg = gr.Markdown("", visible=False)
with gr.Row():
with gr.Column(scale=1):
file_obj = gr.File(label="Upload CSV or Excel Network File", file_types=[".csv", ".xlsx"])
is_directed = gr.Checkbox(label="Is Directed Network", value=False)
btn = gr.Button("Detect Network Communities", variant="primary")
gr.Markdown(
"""
💡 **How Modularity Works**: Modularity ranges between `-0.5` and `1.0`.
Scores above `0.3` suggest a very strong, well-separated community structure where nodes cluster in tight cliques.
"""
)
with gr.Column(scale=2):
stats_box = gr.HTML()
with gr.Tabs():
with gr.TabItem("Interactive Subgroups Map"):
vis_box = gr.HTML()
with gr.TabItem("Communities List"):
table_summary = gr.Dataframe(headers=["Community ID", "Node Count", "Members"])
with gr.TabItem("Node Grouping Table"):
table_nodes = gr.Dataframe(headers=["Node", "Community ID"])
download_btn = gr.File(label="Download Community Assignments CSV", visible=False)
def process(file_obj, is_directed):
err, stats, vis, summary, nodes, csv_path = analyze_communities(file_obj, is_directed)
if err:
return gr.update(value=err, visible=True), "", "", None, None, gr.update(visible=False)
return gr.update(visible=False), stats, vis, summary, nodes, csv_path
btn.click(
process,
inputs=[file_obj, is_directed],
outputs=[error_msg, stats_box, vis_box, table_summary, table_nodes, download_btn]
)
if __name__ == "__main__":
demo.launch()