Spaces:
Paused
Paused
| 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() | |