Jomaric commited on
Commit
55c35d7
·
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 +267 -0
  3. requirements.txt +3 -0
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Co-occurrence Networks
3
+ emoji: 📊
4
+ colorFrom: red
5
+ colorTo: orange
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
10
+
11
+ # Word Co-occurrence Networks
12
+
13
+ An interactive computational linguistics application that maps and visualizes word co-occurrences based on textual proximity. Useful for analyzing key themes, concept maps, and stylistic structures in literature, media, and digital archives.
14
+
15
+ ### Features
16
+ 1. **Interactive Visualizations**: Zoom, drag, and toggle physics-enabled word-word network visualizations using Vis.js.
17
+ 2. **Custom Stopwords**: Filter out noise words and focus on high-value conceptual connections.
18
+ 3. **Sliding Window Adjustment**: Control how far apart two words can be (window size) to construct a connection.
19
+ 4. **Data Exports**: Download a clean, sorted edge list CSV for further network analysis in tools like Gephi.
app.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import re
8
+ from collections import Counter
9
+
10
+ # A robust built-in stopword list to guarantee functionality even without downloading NLTK datasets
11
+ DEFAULT_STOPWORDS = set([
12
+ "i", "me", "my", "myself", "we", "our", "ours", "ourselves", "you", "your", "yours",
13
+ "yourself", "yourselves", "he", "him", "his", "himself", "she", "her", "hers",
14
+ "herself", "it", "its", "itself", "they", "them", "their", "theirs", "themselves",
15
+ "what", "which", "who", "whom", "this", "that", "these", "those", "am", "is", "are",
16
+ "was", "were", "be", "been", "being", "have", "has", "had", "having", "do", "does",
17
+ "did", "doing", "a", "an", "the", "and", "but", "if", "or", "because", "as", "until",
18
+ "while", "of", "at", "by", "for", "with", "about", "against", "between", "into",
19
+ "through", "during", "before", "after", "above", "below", "to", "from", "up", "down",
20
+ "in", "out", "on", "off", "over", "under", "again", "further", "then", "once", "here",
21
+ "there", "when", "where", "why", "how", "all", "any", "both", "each", "few", "more",
22
+ "most", "other", "some", "such", "no", "nor", "not", "only", "own", "same", "so",
23
+ "than", "too", "very", "s", "t", "can", "will", "just", "don", "should", "now", "d",
24
+ "ll", "m", "o", "re", "ve", "y", "ain", "aren", "couldn", "didn", "doesn", "hadn",
25
+ "hasn", "haven", "isn", "ma", "mightn", "mustn", "needn", "shan", "shouldn", "wasn",
26
+ "weren", "won", "wouldn"
27
+ ])
28
+
29
+ def tokenize_and_clean(text, custom_stopwords_str):
30
+ # Convert custom stopwords
31
+ custom_stops = set([w.strip().lower() for w in custom_stopwords_str.split(',') if w.strip()])
32
+ all_stopwords = DEFAULT_STOPWORDS.union(custom_stops)
33
+
34
+ # Simple regex tokenizer
35
+ words = re.findall(r'\b[a-zA-Z]{2,}\b', text.lower())
36
+ cleaned_words = [w for w in words if w not in all_stopwords]
37
+ return cleaned_words
38
+
39
+ def build_cooccurrence_matrix(words, window_size):
40
+ cooccurrences = Counter()
41
+
42
+ for i in range(len(words)):
43
+ current_word = words[i]
44
+ # Look ahead up to the window size
45
+ start = i + 1
46
+ end = min(i + 1 + window_size, len(words))
47
+
48
+ for j in range(start, end):
49
+ next_word = words[j]
50
+ if current_word != next_word:
51
+ # Sort alphabetically to keep edges undirected
52
+ pair = tuple(sorted([current_word, next_word]))
53
+ cooccurrences[pair] += 1
54
+
55
+ return cooccurrences
56
+
57
+ def generate_vis_html(cooccurrences, min_freq, word_counts):
58
+ # Filter by minimum frequency
59
+ filtered_pairs = {pair: count for pair, count in cooccurrences.items() if count >= min_freq}
60
+
61
+ if not filtered_pairs:
62
+ return None, None
63
+
64
+ # Extract unique nodes from filtered edges
65
+ nodes = set()
66
+ for pair in filtered_pairs.keys():
67
+ nodes.update(pair)
68
+
69
+ # Initialize PyVis Network
70
+ net = Network(
71
+ height="500px",
72
+ width="100%",
73
+ bgcolor="#16100c",
74
+ font_color="#f4eee6",
75
+ notebook=False
76
+ )
77
+
78
+ net.set_options("""
79
+ var options = {
80
+ "nodes": {
81
+ "borderWidth": 1,
82
+ "borderWidthSelected": 3,
83
+ "color": {
84
+ "border": "#2c1e16",
85
+ "background": "#ff7043",
86
+ "highlight": {
87
+ "border": "#ff7043",
88
+ "background": "#ffffff"
89
+ }
90
+ },
91
+ "font": {
92
+ "color": "#f4eee6",
93
+ "size": 14,
94
+ "face": "Inter, sans-serif"
95
+ }
96
+ },
97
+ "edges": {
98
+ "color": {
99
+ "color": "rgba(255, 112, 67, 0.3)",
100
+ "highlight": "#ff7043"
101
+ },
102
+ "smooth": {
103
+ "type": "continuous"
104
+ }
105
+ },
106
+ "physics": {
107
+ "barnesHut": {
108
+ "gravitationalConstant": -15000,
109
+ "centralGravity": 0.35,
110
+ "springLength": 100,
111
+ "springConstant": 0.05
112
+ },
113
+ "minVelocity": 0.75
114
+ }
115
+ }
116
+ """)
117
+
118
+ # Add nodes (scaled by absolute frequency in the text)
119
+ for node in nodes:
120
+ freq = word_counts.get(node, 1)
121
+ size = 10 + min(freq * 1.5, 40) # Caps size to prevent massive nodes
122
+ net.add_node(node, label=node, size=size, title=f"Word occurrences: {freq}")
123
+
124
+ # Add edges
125
+ for (source, target), weight in filtered_pairs.items():
126
+ net.add_edge(source, target, value=weight, title=f"Co-occurrences: {weight}")
127
+
128
+ # Save graph and read
129
+ temp_dir = tempfile.gettempdir()
130
+ temp_path = os.path.join(temp_dir, next(tempfile._get_candidate_names()) + ".html")
131
+ net.save_graph(temp_path)
132
+
133
+ with open(temp_path, "r", encoding="utf-8") as f:
134
+ html_content = f.read()
135
+
136
+ try:
137
+ os.remove(temp_path)
138
+ except:
139
+ pass
140
+
141
+ escaped_html = html_content.replace('"', '"')
142
+ 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>'
143
+
144
+ # Create DataFrame for display/download
145
+ edge_list = [{"Source": k[0], "Target": k[1], "Co-occurrences": v} for k, v in filtered_pairs.items()]
146
+ df = pd.DataFrame(edge_list).sort_values("Co-occurrences", ascending=False)
147
+
148
+ return iframe_code, df
149
+
150
+ def analyze_cooccurrence(text, window_size, min_freq, custom_stopwords):
151
+ if not text or len(text.strip()) < 10:
152
+ return "Please input a longer block of text.", None, None, None
153
+
154
+ words = tokenize_and_clean(text, custom_stopwords)
155
+ if len(words) < 5:
156
+ return "Not enough meaningful words found after filtering stopwords.", None, None, None
157
+
158
+ word_counts = Counter(words)
159
+ cooccurrences = build_cooccurrence_matrix(words, window_size)
160
+
161
+ vis_html, df = generate_vis_html(cooccurrences, min_freq, word_counts)
162
+
163
+ if vis_html is None:
164
+ return f"No word pairs met the minimum co-occurrence threshold of {min_freq}. Try lowering the slider.", None, None, None
165
+
166
+ # Stats
167
+ num_nodes = len(df['Source'].unique()) + len(df['Target'].unique())
168
+ num_edges = len(df)
169
+
170
+ stats_html = f"""
171
+ <div style='display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 1rem;'>
172
+ <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;'>
173
+ <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Words in Network</div>
174
+ <div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{num_nodes}</div>
175
+ </div>
176
+ <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;'>
177
+ <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Unique Word Pairs (Links)</div>
178
+ <div style='font-size: 2rem; font-weight: bold; margin-top: 0.5rem;'>{num_edges}</div>
179
+ </div>
180
+ </div>
181
+ """
182
+
183
+ # Download file path
184
+ out_csv = tempfile.mktemp(suffix=".csv")
185
+ df.to_csv(out_csv, index=False)
186
+
187
+ return "", stats_html, vis_html, df.head(50), out_csv
188
+
189
+ theme = gr.themes.Default(
190
+ primary_hue="orange",
191
+ neutral_hue="stone"
192
+ ).set(
193
+ body_background_fill="#0d0907",
194
+ body_text_color="#c4bbae",
195
+ block_background_fill="#16100c",
196
+ block_border_width="1px",
197
+ block_label_text_color="#f4eee6"
198
+ )
199
+
200
+ with gr.Blocks(theme=theme, title="Word Co-occurrence Networks") as demo:
201
+ gr.Markdown(
202
+ """
203
+ # 📊 Interactive Word Co-occurrence Networks
204
+ ### Map how words and concepts interconnect based on proximity. Perfect for structural linguistics, narrative themes, and semantic modeling.
205
+ """
206
+ )
207
+
208
+ error_msg = gr.Markdown("", visible=False)
209
+
210
+ with gr.Row():
211
+ with gr.Column(scale=1):
212
+ raw_text = gr.Textbox(
213
+ label="Input Text Document",
214
+ placeholder="Paste your textual content here (e.g. speeches, novel chapters, or code blocks)...",
215
+ lines=10
216
+ )
217
+
218
+ with gr.Row():
219
+ window_size = gr.Slider(
220
+ minimum=2,
221
+ maximum=10,
222
+ value=5,
223
+ step=1,
224
+ label="Sliding Window Size",
225
+ info="Maximum distance in words to capture a connection."
226
+ )
227
+ min_freq = gr.Slider(
228
+ minimum=1,
229
+ maximum=20,
230
+ value=3,
231
+ step=1,
232
+ label="Min Co-occurrence Frequency",
233
+ info="Filters out peripheral connections."
234
+ )
235
+
236
+ custom_stopwords = gr.Textbox(
237
+ label="Custom Stopwords (comma separated)",
238
+ placeholder="chapter, page, said, would",
239
+ info="Words to ignore during co-occurrence analysis."
240
+ )
241
+
242
+ btn = gr.Button("Build Co-occurrence Network", variant="primary")
243
+
244
+ with gr.Column(scale=2):
245
+ stats_box = gr.HTML()
246
+
247
+ with gr.Tabs():
248
+ with gr.TabItem("Interactive Graph"):
249
+ vis_box = gr.HTML()
250
+ with gr.TabItem("Data Table"):
251
+ table_box = gr.Dataframe(headers=["Source", "Target", "Co-occurrences"])
252
+ download_btn = gr.File(label="Download Full Dataset")
253
+
254
+ def process(text, window, freq, stops):
255
+ err, stats, vis, table, csv_path = analyze_cooccurrence(text, window, freq, stops)
256
+ if err:
257
+ return gr.update(value=err, visible=True), "", "", None, gr.update(visible=False)
258
+ return gr.update(visible=False), stats, vis, table, gr.update(value=csv_path, visible=True)
259
+
260
+ btn.click(
261
+ process,
262
+ inputs=[raw_text, window_size, min_freq, custom_stopwords],
263
+ outputs=[error_msg, stats_box, vis_box, table_box, download_btn]
264
+ )
265
+
266
+ if __name__ == "__main__":
267
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ pandas
2
+ networkx
3
+ pyvis