Jomaric commited on
Commit
0ff0477
·
0 Parent(s):

feat: initial release of network analyzer space

Browse files
Files changed (3) hide show
  1. README.md +19 -0
  2. app.py +241 -0
  3. requirements.txt +3 -0
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Centrality Analysis
3
+ emoji: 👑
4
+ colorFrom: orange
5
+ colorTo: yellow
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
10
+
11
+ # Network Centrality Analysis Suite
12
+
13
+ An interactive educational web application designed to help digital humanities and social science students identify, visualize, and analyze node importance and structural power in networks.
14
+
15
+ ### Features
16
+ 1. **Interactive Centrality Scale**: Select from four classical centrality metrics (Degree, Betweenness, Eigenvector, Closeness) and dynamically adjust Vis.js node sizing and color gradients.
17
+ 2. **Directed or Undirected Networks**: Analyze paths and directions seamlessly.
18
+ 3. **Sortable Rankings Table**: View calculated metrics in a fully interactive, searchable dataframe.
19
+ 4. **Data Exports**: Download the full rankings as a clean CSV file.
app.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import networkx as nx
4
+ from pyvis.network import Network
5
+ import tempfile
6
+ import os
7
+
8
+ def calculate_centralities(df, is_directed):
9
+ # Build NetworkX graph
10
+ G = nx.from_pandas_edgelist(df, 'Source', 'Target', create_using=nx.DiGraph() if is_directed else nx.Graph())
11
+
12
+ # Calculate centralities
13
+ deg_cent = nx.degree_centrality(G)
14
+ bet_cent = nx.betweenness_centrality(G)
15
+
16
+ # Eigenvector has a fallback for non-convergence or directed graphs
17
+ try:
18
+ eig_cent = nx.eigenvector_centrality(G, max_iter=1000)
19
+ except:
20
+ try:
21
+ eig_cent = nx.eigenvector_centrality_numpy(G)
22
+ except:
23
+ eig_cent = {node: 0.0 for node in G.nodes()}
24
+
25
+ clo_cent = nx.closeness_centrality(G)
26
+
27
+ # Build table
28
+ records = []
29
+ for node in G.nodes():
30
+ records.append({
31
+ "Node": node,
32
+ "Degree Centrality": deg_cent.get(node, 0.0),
33
+ "Betweenness Centrality": bet_cent.get(node, 0.0),
34
+ "Eigenvector Centrality": eig_cent.get(node, 0.0),
35
+ "Closeness Centrality": clo_cent.get(node, 0.0)
36
+ })
37
+
38
+ df_cent = pd.DataFrame(records).sort_values("Degree Centrality", ascending=False)
39
+ return G, df_cent
40
+
41
+ def get_color_gradient(value, max_val):
42
+ # Maps centrality value to an aesthetic gradient: low = muted brown, high = hot orange/white
43
+ if max_val <= 0:
44
+ return "#ff7043"
45
+ ratio = min(value / max_val, 1.0)
46
+ # Interpolate colors between #3d281c (wash) and #ff7043 (accent) or #ffffff
47
+ r = int(61 + (255 - 61) * ratio)
48
+ g = int(40 + (112 - 40) * ratio)
49
+ b = int(28 + (67 - 28) * ratio)
50
+ return f"#{r:02x}{g:02x}{b:02x}"
51
+
52
+ def generate_vis_html(G, df_cent, active_metric):
53
+ net = Network(
54
+ height="500px",
55
+ width="100%",
56
+ bgcolor="#16100c",
57
+ font_color="#f4eee6",
58
+ notebook=False
59
+ )
60
+
61
+ net.set_options("""
62
+ var options = {
63
+ "nodes": {
64
+ "borderWidth": 2,
65
+ "font": {
66
+ "color": "#f4eee6",
67
+ "size": 14,
68
+ "face": "Inter, sans-serif"
69
+ }
70
+ },
71
+ "edges": {
72
+ "color": {
73
+ "color": "rgba(255, 112, 67, 0.25)",
74
+ "highlight": "#ff7043"
75
+ },
76
+ "smooth": {
77
+ "type": "continuous"
78
+ }
79
+ },
80
+ "physics": {
81
+ "barnesHut": {
82
+ "gravitationalConstant": -12000,
83
+ "centralGravity": 0.3,
84
+ "springLength": 120,
85
+ "springConstant": 0.04
86
+ }
87
+ }
88
+ }
89
+ """)
90
+
91
+ # Score dictionary
92
+ scores = dict(zip(df_cent['Node'], df_cent[active_metric]))
93
+ max_score = max(scores.values()) if scores else 1.0
94
+
95
+ for node in G.nodes():
96
+ score = scores.get(node, 0.0)
97
+ # Sizing logic: baseline = 10, scaled up to max 45
98
+ size = 10 + (35 * (score / max_score if max_score > 0 else 0))
99
+ color = get_color_gradient(score, max_score)
100
+
101
+ net.add_node(
102
+ node,
103
+ label=node,
104
+ size=size,
105
+ color=color,
106
+ title=f"Centrality Score: {score:.5f}"
107
+ )
108
+
109
+ # Add edges
110
+ for edge in G.edges():
111
+ net.add_edge(edge[0], edge[1])
112
+
113
+ temp_dir = tempfile.gettempdir()
114
+ temp_path = os.path.join(temp_dir, next(tempfile._get_candidate_names()) + ".html")
115
+ net.save_graph(temp_path)
116
+
117
+ with open(temp_path, "r", encoding="utf-8") as f:
118
+ html_content = f.read()
119
+
120
+ try:
121
+ os.remove(temp_path)
122
+ except:
123
+ pass
124
+
125
+ escaped_html = html_content.replace('"', '&quot;')
126
+ 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>'
127
+ return iframe_code
128
+
129
+ def analyze_centrality(file_obj, is_directed, active_metric):
130
+ if file_obj is None:
131
+ return "Please upload a CSV or Excel network dataset.", "", None, None, None
132
+
133
+ try:
134
+ if file_obj.name.endswith('.csv'):
135
+ df = pd.read_csv(file_obj.name)
136
+ else:
137
+ df = pd.read_excel(file_obj.name)
138
+ except Exception as e:
139
+ return f"Error reading file: {str(e)}", "", None, None, None
140
+
141
+ # Standardize column headers
142
+ rename_map = {}
143
+ for col in df.columns:
144
+ if col.lower() in ['source', 'from', 'node1']:
145
+ rename_map[col] = 'Source'
146
+ elif col.lower() in ['target', 'to', 'node2']:
147
+ rename_map[col] = 'Target'
148
+
149
+ df = df.rename(columns=rename_map)
150
+
151
+ if 'Source' not in df.columns or 'Target' not in df.columns:
152
+ return "CSV/Excel must contain at least 'Source' and 'Target' columns representing network edges.", "", None, None, None
153
+
154
+ # Calculate scores
155
+ G, df_cent = calculate_centralities(df, is_directed)
156
+
157
+ # General stats
158
+ stats_html = f"""
159
+ <div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 1rem;'>
160
+ <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;'>
161
+ <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Network Nodes</div>
162
+ <div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{G.number_of_nodes()}</div>
163
+ </div>
164
+ <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;'>
165
+ <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Network Edges</div>
166
+ <div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{G.number_of_edges()}</div>
167
+ </div>
168
+ </div>
169
+ """
170
+
171
+ # Generate PyVis HTML
172
+ vis_html = generate_vis_html(G, df_cent, active_metric)
173
+
174
+ # Sort for displaying
175
+ display_df = df_cent.sort_values(active_metric, ascending=False)
176
+
177
+ # Download scores CSV
178
+ out_csv = tempfile.mktemp(suffix=".csv")
179
+ df_cent.to_csv(out_csv, index=False)
180
+
181
+ return "", stats_html, vis_html, display_df, gr.update(value=out_csv, visible=True)
182
+
183
+ theme = gr.themes.Default(
184
+ primary_hue="orange",
185
+ neutral_hue="stone"
186
+ ).set(
187
+ body_background_fill="#0d0907",
188
+ body_text_color="#c4bbae",
189
+ block_background_fill="#16100c",
190
+ block_border_width="1px",
191
+ block_label_text_color="#f4eee6"
192
+ )
193
+
194
+ with gr.Blocks(theme=theme, title="Centrality Analysis") as demo:
195
+ gr.Markdown(
196
+ """
197
+ # 👑 Network Centrality Analysis Suite
198
+ ### Quantify node influence and structural power inside complex networks using four classical centrality algorithms. Drag, zoom, and visualize node importance dynamically!
199
+ """
200
+ )
201
+
202
+ error_msg = gr.Markdown("", visible=False)
203
+
204
+ with gr.Row():
205
+ with gr.Column(scale=1):
206
+ file_obj = gr.File(label="Upload CSV or Excel Network File", file_types=[".csv", ".xlsx"])
207
+ is_directed = gr.Checkbox(label="Is Directed Network", value=False)
208
+
209
+ active_metric = gr.Radio(
210
+ choices=["Degree Centrality", "Betweenness Centrality", "Eigenvector Centrality", "Closeness Centrality"],
211
+ value="Degree Centrality",
212
+ label="Centrality Measure",
213
+ info="Degree (total links), Betweenness (brokerage), Eigenvector (influence of connections), Closeness (distance)."
214
+ )
215
+
216
+ btn = gr.Button("Calculate Centrality Rankings", variant="primary")
217
+
218
+ with gr.Column(scale=2):
219
+ stats_box = gr.HTML()
220
+
221
+ with gr.Tabs():
222
+ with gr.TabItem("Interactive Graph Scaling"):
223
+ vis_box = gr.HTML()
224
+ with gr.TabItem("Rankings Table"):
225
+ table_box = gr.Dataframe(headers=["Node", "Degree Centrality", "Betweenness Centrality", "Eigenvector Centrality", "Closeness Centrality"])
226
+ download_btn = gr.File(label="Download Calculated Rankings CSV", visible=False)
227
+
228
+ def process(file_obj, is_directed, metric):
229
+ err, stats, vis, table, csv_path = analyze_centrality(file_obj, is_directed, metric)
230
+ if err:
231
+ return gr.update(value=err, visible=True), "", "", None, gr.update(visible=False)
232
+ return gr.update(visible=False), stats, vis, table, csv_path
233
+
234
+ btn.click(
235
+ process,
236
+ inputs=[file_obj, is_directed, active_metric],
237
+ outputs=[error_msg, stats_box, vis_box, table_box, download_btn]
238
+ )
239
+
240
+ if __name__ == "__main__":
241
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ pandas
2
+ networkx
3
+ pyvis