gtech13 commited on
Commit
a0e5e8f
·
verified ·
1 Parent(s): 39054e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -115
app.py CHANGED
@@ -1,5 +1,4 @@
1
  import os
2
- # --- ANTI-CRASH ENVIRONMENT VARIABLES ---
3
  os.environ["OMP_NUM_THREADS"] = "1"
4
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
5
 
@@ -27,17 +26,7 @@ warnings.filterwarnings("ignore")
27
  # ==========================================
28
  # 1. PAGE CONFIGURATION & MAPPINGS
29
  # ==========================================
30
- st.set_page_config(page_title="Topic Modeling Pipeline", layout="wide", initial_sidebar_state="expanded")
31
-
32
- # Add this right below set_page_config to stop the screen from jumping left/right
33
- st.markdown("""
34
- <style>
35
- /* Force the vertical scrollbar to always show so the page width never changes */
36
- html { overflow-y: scroll; }
37
- /* Prevent horizontal scrolling */
38
- .block-container { max-width: 100%; overflow-x: hidden; }
39
- </style>
40
- """, unsafe_allow_html=True)
41
 
42
  EMBEDDING_MAP = {
43
  "MiniLM (Fast & Lightweight)": "sentence-transformers/all-MiniLM-L6-v2",
@@ -52,7 +41,6 @@ POOLING_MAP = {
52
  "Mean-Max (Combined)": "mean-max"
53
  }
54
 
55
- # --- CACHE THE NEURAL NETWORK ---
56
  @st.cache_resource
57
  def load_embedder(model_name, pool_strat):
58
  word_emb = models.Transformer(model_name)
@@ -62,18 +50,12 @@ def load_embedder(model_name, pool_strat):
62
  pooling_mode_max_tokens=("max" in pool_strat),
63
  pooling_mode_cls_token=("cls" in pool_strat)
64
  )
65
-
66
- modules = [word_emb, pool_model]
67
-
68
- # NEW MATH STEP: If user checked the box, append the Normalization layer!
69
- if apply_normalization:
70
- modules.append(models.Normalize())
71
  return SentenceTransformer(modules=[word_emb, pool_model])
72
 
73
  # ==========================================
74
- # 2. THE GUIDED UI (MAIN PAGE)
75
  # ==========================================
76
- st.title("🧠 BERTopic - Topic Modeling Pipeline Math Visualization")
77
 
78
  try:
79
  st.image("pipeline.png", use_container_width=True)
@@ -82,8 +64,9 @@ except FileNotFoundError:
82
 
83
  st.divider()
84
 
85
- # --- STEP 0: DATA SETTINGS ---
86
  st.header("📥 Step 0: Input Data & Core Settings")
 
 
87
  data_source = st.radio("Choose Data Source:", ["Use Sample ACM Abstract", "Paste Text"], horizontal=True)
88
 
89
  sample_abstract = """
@@ -105,60 +88,43 @@ with col_a:
105
  with col_b:
106
  words_per_theme = st.slider("Words to Output per Theme", 3, 10, 5)
107
 
108
- # --- THE VERTICAL CONFIGURATION WIZARD ---
109
  st.header("⚙️ Model Configuration")
110
 
111
- with st.expander("1️⃣ Semantic Layer (Embeddings & Pooling)", expanded=True):
112
- ui_embedding = st.selectbox("Embedding Model", list(EMBEDDING_MAP.keys()))
113
- ui_pooling = st.selectbox("Pooling Strategy", list(POOLING_MAP.keys()))
114
- ui_normalize = st.checkbox("Apply L2 Normalization (Recommended)", value=True, help="Forces all vectors to have a length of 1.0 so length doesn't bias the clustering.")
115
-
116
- with st.expander("2️⃣ Geometry Layer (Dimensionality Reduction)", expanded=True):
117
- ui_algo = st.selectbox("Algorithm", ["UMAP (Complex geometry)", "PCA (Fast/Deterministic)"])
118
- if "UMAP" in ui_algo:
119
- ui_metric = st.selectbox("Distance Metric", ["cosine", "euclidean", "manhattan"])
120
- else:
121
- ui_metric = "euclidean"
122
- st.info("PCA inherently uses Variance (Euclidean math), so distance metrics are bypassed.")
123
-
124
- with st.expander("3️⃣ Clustering Layer (Grouping)", expanded=True):
125
- st.markdown("""
126
- *Clustering mathematically draws boundaries around similar sentences.*
127
- * **Primary Engine (HDBSCAN):** Runs on datasets $\ge$ 15 sentences. Automatically filters outliers and finds dense semantic clouds.
128
- *(Defaults: min_cluster_size=10, cluster_selection_method='eom', metric='euclidean')*
129
- * **Fallback Engine (K-Means):** Runs on datasets $<$ 15 sentences. Forces all sentences into buckets to prevent math crashes on tiny text samples.
130
- *(Defaults: n_clusters = Target Themes, random_state=42)*
131
- """)
132
-
133
- with st.expander("4️⃣ Vocabulary Layer (Vectorization)", expanded=True):
134
- ngram_range = st.slider("N-Gram Range", 1, 3, (1, 2), help="1=Unigrams, 2=Bigrams (e.g., 'machine learning')")
135
- # Added the explanation here!
136
- auto_noise = st.checkbox(
137
- "Auto-Remove Redundant Noise (max_df)",
138
- value=True,
139
- help="Mathematically deletes words that appear in more than 85% of the documents."
140
- )
141
- st.caption("Deletes overly common words (like 'paper' or 'study') that appear everywhere, preventing generic filler from dominating your themes.")
142
-
143
- with st.expander("4️⃣ Extraction Layer (Representation)", expanded=True):
144
- st.markdown("*BERTopic ALWAYS extracts baseline words using **c-TF-IDF** (Word Frequency). Do you want to apply an advanced mathematical filter on top of it?*")
145
-
146
- ui_extraction = st.selectbox(
147
- "Apply Advanced Filter:",
148
- ["None (Base c-TF-IDF only)", "KeyBERTInspired (Semantic alignment)", "MMR (Reduce redundancy)"]
149
- )
150
-
151
- if "MMR" in ui_extraction:
152
- mmr_diversity = st.slider("MMR Diversity Penalty", 0.0, 1.0, 0.3)
153
- else:
154
- mmr_diversity = None
155
 
156
- # --- EVALUATION METRICS ---
157
  st.header("📊 Evaluation Metrics")
158
  eval_metrics = st.multiselect(
159
  "Select KPIs to generate a final report card:",
160
- ["Topic Diversity", "NPMI Coherence", "Silhouette Score"],
161
- default=["Topic Diversity", "NPMI Coherence", "Silhouette Score"]
162
  )
163
 
164
  st.divider()
@@ -172,7 +138,6 @@ if st.button("🚀 Run Topic Modeling Pipeline", type="primary", use_container_w
172
  st.error("Please provide more text data!")
173
  st.stop()
174
 
175
- # --- MATH EXECUTION (Inside Spinner) ---
176
  with st.spinner("Processing Semantic Pipeline... (Models are cached to prevent crashes)"):
177
 
178
  sentences = [s.strip() for s in raw_data.split('.') if len(s.strip()) > 10]
@@ -184,12 +149,9 @@ if st.button("🚀 Run Topic Modeling Pipeline", type="primary", use_container_w
184
 
185
  vectorizer_model = CountVectorizer(stop_words=stop_w, ngram_range=ngram_range, max_df=0.85 if auto_noise and dataset_size > 10 else 1.0)
186
 
187
- #custom_embedder = load_embedder(EMBEDDING_MAP[ui_embedding], POOLING_MAP[ui_pooling])
188
- # Ensure you pass the new variable into the function!
189
- custom_embedder = load_embedder(EMBEDDING_MAP[ui_embedding], POOLING_MAP[ui_pooling], ui_normalize)
190
  embeddings = custom_embedder.encode(sentences)
191
 
192
- # Fallback Logic (Step 3 representation in code)
193
  is_fallback = False
194
  if dataset_size < 15 or "PCA" in ui_algo:
195
  safe_n_themes = min(n_themes, dataset_size)
@@ -206,7 +168,6 @@ if st.button("🚀 Run Topic Modeling Pipeline", type="primary", use_container_w
206
  algo_used = "UMAP"
207
  cluster_algo = "HDBSCAN"
208
 
209
- # Representation
210
  if "MMR" in ui_extraction:
211
  rep_model = MaximalMarginalRelevance(diversity=mmr_diversity, top_n_words=words_per_theme)
212
  elif "KeyBERT" in ui_extraction:
@@ -227,7 +188,7 @@ if st.button("🚀 Run Topic Modeling Pipeline", type="primary", use_container_w
227
  topics, _ = topic_model.fit_transform(sentences)
228
 
229
  # ==========================================
230
- # 4. UI DISPLAY & METRICS (Outside Spinner)
231
  # ==========================================
232
  st.success("Analysis Complete!")
233
 
@@ -254,49 +215,54 @@ if st.button("🚀 Run Topic Modeling Pipeline", type="primary", use_container_w
254
  st.info(f"**Theme {t_id + 1}**\n\n" + "\n".join([f"��� {w}" for w in theme_w]))
255
  col_idx += 1
256
 
257
- # --- METRICS CALCULATIONS (Math First, UI Second) ---
258
- div_val, npmi_val, sil_val = 0.0, 0.0, 0.0
259
- div_status, npmi_status, sil_status = "Skipped", "Skipped", "Skipped"
260
 
261
  if len(eval_metrics) > 0:
262
- # 1. DO ALL THE MATH INVISIBLY FIRST
263
  with st.spinner("Calculating mathematical metrics... (NPMI requires building a dictionary and takes a moment)"):
264
 
265
- if "Topic Diversity" in eval_metrics:
266
- if len(all_words) > 0:
267
- u_words = set([w for t in all_words for w in t])
268
- t_words = sum([len(t) for t in all_words])
269
- div_val = float(len(u_words) / t_words) if t_words > 0 else 0.0
270
- div_status = f"{div_val:.2f}"
271
-
272
- if "NPMI Coherence" in eval_metrics:
 
273
  try:
274
  tokenized = [vectorizer_model.build_analyzer()(s) for s in sentences]
275
  dictionary = corpora.Dictionary(tokenized)
276
- cm = CoherenceModel(topics=all_words, texts=tokenized, dictionary=dictionary, coherence='c_npmi')
277
- temp_npmi = cm.get_coherence()
278
- if np.isnan(temp_npmi):
279
- npmi_status = "N/A (Too few words)"
280
- else:
281
- npmi_val = float(temp_npmi)
282
- npmi_status = f"{npmi_val:.2f}"
 
 
 
283
  except Exception:
284
  npmi_status = "Skipped (Data too small)"
 
285
 
 
286
  if "Silhouette Score" in eval_metrics:
287
  valid_idx = [i for i, t in enumerate(topics) if t != -1]
288
  unique_topics = set([topics[i] for i in valid_idx])
289
  if 1 < len(unique_topics) < len(valid_idx):
290
  sil_val = float(silhouette_score(
291
  np.array([embeddings[i] for i in valid_idx]),
292
- [topics[i] for i in valid_idx],
293
- metric='cosine'
294
- ))
295
- sil_status = f"{sil_val:.2f}"
296
- else:
297
- sil_status = "Skipped (Themes need ≥2 sentences)"
298
-
299
- # 2. DRAW THE UI INSTANTLY (No more jumping!)
300
  st.markdown("### 📊 Key Performance Indicators (KPI)")
301
  kpi_cols = st.columns(len(eval_metrics))
302
 
@@ -306,10 +272,11 @@ if st.button("🚀 Run Topic Modeling Pipeline", type="primary", use_container_w
306
  st.metric("Topic Diversity (Target: 1.0)", div_status)
307
  elif metric == "NPMI Coherence":
308
  st.metric("NPMI Coherence (Target: >0.1)", npmi_status)
 
 
309
  elif metric == "Silhouette Score":
310
  st.metric("Silhouette Score (Target: >0.0)", sil_status)
311
 
312
-
313
  # ==========================================
314
  # 5. XAI VISUALIZATION GRAPH
315
  # ==========================================
@@ -327,7 +294,7 @@ if st.button("🚀 Run Topic Modeling Pipeline", type="primary", use_container_w
327
  ax1.set_title("STEP 1: Embeddings & Pooling", fontsize=13, fontweight='bold')
328
  ax1.set_ylabel("Sentences")
329
  ax1.set_xlabel("Vector Dimensions (First 50 shown)")
330
- ax1.text(0.5, -0.25, f"Math: {ui_embedding.split()[0]} encodes text into 384D.\nPooling '{ui_pooling.split()[0]}' squashes word vectors into 1 sentence vector.",
331
  fontsize=10, ha='center', va='top', transform=ax1.transAxes, bbox=box_style)
332
 
333
  # 2. Geometry
@@ -358,21 +325,18 @@ if st.button("🚀 Run Topic Modeling Pipeline", type="primary", use_container_w
358
  else:
359
  ax4.text(0.5, 0.5, "Theme not found", ha='center', transform=ax4.transAxes)
360
 
361
- # 5. KPI Dashboard
362
  ax5 = plt.subplot(3, 2, 5)
363
  ax5.axis('off')
364
  ax5.set_title("STEP 5: Key Performance Indicators (KPI)", fontsize=13, fontweight='bold', y=0.9)
365
 
366
- div_str = f"{div_val:.2f}" if div_val > 0 else "Skipped"
367
- npmi_str = f"{npmi_val:.2f}" if npmi_val != 0.0 else "Skipped"
368
- sil_str = f"{sil_val:.2f}" if sil_val != 0.0 else "Skipped"
369
-
370
  kpi_text = (
371
- f"📊 Topic Diversity: {div_str} (Target: 1.0)\n\n"
372
- f"🧠 NPMI Coherence: {npmi_str} (Target: >0.1)\n\n"
373
- f"📐 Silhouette Score: {sil_str} (Target: >0.0)"
 
374
  )
375
- ax5.text(0.5, 0.4, kpi_text, fontsize=12, va='center', ha='center',
376
  bbox=dict(boxstyle="square,pad=1.5", facecolor='#e6f2ff', edgecolor='#377eb8', lw=2))
377
  ax5.text(0.5, -0.15, "Math: Since pipeline algorithms don't use 'Training Loss',\nthese KPIs provide the absolute mathematical grade of the topics.",
378
  fontsize=10, ha='center', va='top', transform=ax5.transAxes, bbox=box_style)
 
1
  import os
 
2
  os.environ["OMP_NUM_THREADS"] = "1"
3
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
4
 
 
26
  # ==========================================
27
  # 1. PAGE CONFIGURATION & MAPPINGS
28
  # ==========================================
29
+ st.set_page_config(page_title="Topic Modeling Pipeline", layout="wide", initial_sidebar_state="collapsed")
 
 
 
 
 
 
 
 
 
 
30
 
31
  EMBEDDING_MAP = {
32
  "MiniLM (Fast & Lightweight)": "sentence-transformers/all-MiniLM-L6-v2",
 
41
  "Mean-Max (Combined)": "mean-max"
42
  }
43
 
 
44
  @st.cache_resource
45
  def load_embedder(model_name, pool_strat):
46
  word_emb = models.Transformer(model_name)
 
50
  pooling_mode_max_tokens=("max" in pool_strat),
51
  pooling_mode_cls_token=("cls" in pool_strat)
52
  )
 
 
 
 
 
 
53
  return SentenceTransformer(modules=[word_emb, pool_model])
54
 
55
  # ==========================================
56
+ # 2. THE GUIDED UI
57
  # ==========================================
58
+ st.title("🧠 Advanced Topic Modeling Pipeline")
59
 
60
  try:
61
  st.image("pipeline.png", use_container_width=True)
 
64
 
65
  st.divider()
66
 
 
67
  st.header("📥 Step 0: Input Data & Core Settings")
68
+ st.info("💡 **Preprocessing Note:** You do not need to manually lowercase or strip punctuation. The `CountVectorizer` algorithm and the `Uncased` BERT Neural Networks handle casing and token normalization autonomously at the mathematical level.")
69
+
70
  data_source = st.radio("Choose Data Source:", ["Use Sample ACM Abstract", "Paste Text"], horizontal=True)
71
 
72
  sample_abstract = """
 
88
  with col_b:
89
  words_per_theme = st.slider("Words to Output per Theme", 3, 10, 5)
90
 
 
91
  st.header("⚙️ Model Configuration")
92
 
93
+ col1, col2 = st.columns(2)
94
+ with col1:
95
+ with st.expander("1️⃣ Semantic Layer (Embeddings)", expanded=True):
96
+ ui_embedding = st.selectbox("Embedding Model", list(EMBEDDING_MAP.keys()))
97
+ ui_pooling = st.selectbox("Pooling Strategy", list(POOLING_MAP.keys()))
98
+
99
+ with col2:
100
+ with st.expander("2️⃣ Geometry Layer (Dimensionality Reduction)", expanded=True):
101
+ ui_algo = st.selectbox("Algorithm", ["UMAP (Complex geometry)", "PCA (Fast/Deterministic)"])
102
+ ui_metric = st.selectbox("Distance Metric", ["cosine", "euclidean", "manhattan"]) if "UMAP" in ui_algo else "euclidean"
103
+
104
+ col3, col4 = st.columns(2)
105
+ with col3:
106
+ with st.expander("3️⃣ Clustering Layer (Grouping)", expanded=True):
107
+ st.markdown("""
108
+ *Clustering mathematically draws boundaries around similar sentences.*
109
+ * **Primary (HDBSCAN):** $\ge$ 15 sentences. Filters outliers. *(Defaults: min_cluster_size=10)*
110
+ * **Fallback (K-Means):** $<$ 15 sentences. Forces buckets to prevent math crashes on tiny text.
111
+ """)
112
+
113
+ with col4:
114
+ with st.expander("4️⃣ Vocabulary Layer (Vectorization)", expanded=True):
115
+ ngram_range = st.slider("N-Gram Range", 1, 3, (1, 2))
116
+ auto_noise = st.checkbox("Auto-Remove Redundant Noise", value=True, help="Mathematically deletes words appearing in >85% of documents.")
117
+
118
+ with st.expander("5️⃣ Extraction Layer (Representation)", expanded=True):
119
+ st.markdown("*BERTopic ALWAYS extracts baseline words using **c-TF-IDF**. Do you want to apply an advanced mathematical filter on top of it?*")
120
+ ui_extraction = st.selectbox("Apply Advanced Filter:", ["None (Base c-TF-IDF only)", "KeyBERTInspired (Semantic alignment)", "MMR (Reduce redundancy)"])
121
+ mmr_diversity = st.slider("MMR Diversity Penalty", 0.0, 1.0, 0.3) if "MMR" in ui_extraction else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
 
123
  st.header("📊 Evaluation Metrics")
124
  eval_metrics = st.multiselect(
125
  "Select KPIs to generate a final report card:",
126
+ ["Topic Diversity", "NPMI Coherence", "UMass Coherence", "Silhouette Score"],
127
+ default=["Topic Diversity", "NPMI Coherence", "UMass Coherence", "Silhouette Score"]
128
  )
129
 
130
  st.divider()
 
138
  st.error("Please provide more text data!")
139
  st.stop()
140
 
 
141
  with st.spinner("Processing Semantic Pipeline... (Models are cached to prevent crashes)"):
142
 
143
  sentences = [s.strip() for s in raw_data.split('.') if len(s.strip()) > 10]
 
149
 
150
  vectorizer_model = CountVectorizer(stop_words=stop_w, ngram_range=ngram_range, max_df=0.85 if auto_noise and dataset_size > 10 else 1.0)
151
 
152
+ custom_embedder = load_embedder(EMBEDDING_MAP[ui_embedding], POOLING_MAP[ui_pooling])
 
 
153
  embeddings = custom_embedder.encode(sentences)
154
 
 
155
  is_fallback = False
156
  if dataset_size < 15 or "PCA" in ui_algo:
157
  safe_n_themes = min(n_themes, dataset_size)
 
168
  algo_used = "UMAP"
169
  cluster_algo = "HDBSCAN"
170
 
 
171
  if "MMR" in ui_extraction:
172
  rep_model = MaximalMarginalRelevance(diversity=mmr_diversity, top_n_words=words_per_theme)
173
  elif "KeyBERT" in ui_extraction:
 
188
  topics, _ = topic_model.fit_transform(sentences)
189
 
190
  # ==========================================
191
+ # 4. UI DISPLAY & METRICS
192
  # ==========================================
193
  st.success("Analysis Complete!")
194
 
 
215
  st.info(f"**Theme {t_id + 1}**\n\n" + "\n".join([f"��� {w}" for w in theme_w]))
216
  col_idx += 1
217
 
218
+ # --- METRICS CALCULATIONS ---
219
+ div_val, npmi_val, umass_val, sil_val = 0.0, 0.0, 0.0, 0.0
220
+ div_status, npmi_status, umass_status, sil_status = "Skipped", "Skipped", "Skipped", "Skipped"
221
 
222
  if len(eval_metrics) > 0:
 
223
  with st.spinner("Calculating mathematical metrics... (NPMI requires building a dictionary and takes a moment)"):
224
 
225
+ # Diversity
226
+ if "Topic Diversity" in eval_metrics and len(all_words) > 0:
227
+ u_words = set([w for t in all_words for w in t])
228
+ t_words = sum([len(t) for t in all_words])
229
+ div_val = float(len(u_words) / t_words) if t_words > 0 else 0.0
230
+ div_status = f"{div_val:.2f}"
231
+
232
+ # Coherence Models (NPMI and UMass)
233
+ if "NPMI Coherence" in eval_metrics or "UMass Coherence" in eval_metrics:
234
  try:
235
  tokenized = [vectorizer_model.build_analyzer()(s) for s in sentences]
236
  dictionary = corpora.Dictionary(tokenized)
237
+
238
+ if "NPMI Coherence" in eval_metrics:
239
+ cm_npmi = CoherenceModel(topics=all_words, texts=tokenized, dictionary=dictionary, coherence='c_npmi')
240
+ temp_npmi = cm_npmi.get_coherence()
241
+ npmi_status = "N/A (Too few words)" if np.isnan(temp_npmi) else f"{float(temp_npmi):.2f}"
242
+
243
+ if "UMass Coherence" in eval_metrics:
244
+ cm_umass = CoherenceModel(topics=all_words, texts=tokenized, dictionary=dictionary, coherence='u_mass')
245
+ temp_umass = cm_umass.get_coherence()
246
+ umass_status = "N/A (Too few words)" if np.isnan(temp_umass) else f"{float(temp_umass):.2f}"
247
  except Exception:
248
  npmi_status = "Skipped (Data too small)"
249
+ umass_status = "Skipped (Data too small)"
250
 
251
+ # Silhouette
252
  if "Silhouette Score" in eval_metrics:
253
  valid_idx = [i for i, t in enumerate(topics) if t != -1]
254
  unique_topics = set([topics[i] for i in valid_idx])
255
  if 1 < len(unique_topics) < len(valid_idx):
256
  sil_val = float(silhouette_score(
257
  np.array([embeddings[i] for i in valid_idx]),
258
+ [topics[i] for i in valid_idx],
259
+ metric='cosine'
260
+ ))
261
+ sil_status = f"{sil_val:.2f}"
262
+ else:
263
+ sil_status = "Skipped (Themes need ≥2 sentences)"
264
+
265
+ # --- RENDER KPI DASHBOARD (UI draws instantly here!) ---
266
  st.markdown("### 📊 Key Performance Indicators (KPI)")
267
  kpi_cols = st.columns(len(eval_metrics))
268
 
 
272
  st.metric("Topic Diversity (Target: 1.0)", div_status)
273
  elif metric == "NPMI Coherence":
274
  st.metric("NPMI Coherence (Target: >0.1)", npmi_status)
275
+ elif metric == "UMass Coherence":
276
+ st.metric("UMass Coherence (Target: closer to 0)", umass_status)
277
  elif metric == "Silhouette Score":
278
  st.metric("Silhouette Score (Target: >0.0)", sil_status)
279
 
 
280
  # ==========================================
281
  # 5. XAI VISUALIZATION GRAPH
282
  # ==========================================
 
294
  ax1.set_title("STEP 1: Embeddings & Pooling", fontsize=13, fontweight='bold')
295
  ax1.set_ylabel("Sentences")
296
  ax1.set_xlabel("Vector Dimensions (First 50 shown)")
297
+ ax1.text(0.5, -0.25, f"Math: {ui_embedding.split('/')[-1]} encodes text into 384D.\nPooling '{ui_pooling.split()[0]}' squashes word vectors into 1 sentence vector.",
298
  fontsize=10, ha='center', va='top', transform=ax1.transAxes, bbox=box_style)
299
 
300
  # 2. Geometry
 
325
  else:
326
  ax4.text(0.5, 0.5, "Theme not found", ha='center', transform=ax4.transAxes)
327
 
328
+ # 5. KPI Dashboard (Updated with UMass)
329
  ax5 = plt.subplot(3, 2, 5)
330
  ax5.axis('off')
331
  ax5.set_title("STEP 5: Key Performance Indicators (KPI)", fontsize=13, fontweight='bold', y=0.9)
332
 
 
 
 
 
333
  kpi_text = (
334
+ f"📊 Topic Diversity: {div_status} (Target: 1.0)\n\n"
335
+ f"🧠 NPMI Coherence: {npmi_status} (Target: >0.1)\n\n"
336
+ f"📚 UMass Coherence: {umass_status} (Target: Closer to 0)\n\n"
337
+ f"📐 Silhouette Score: {sil_status} (Target: >0.0)"
338
  )
339
+ ax5.text(0.5, 0.4, kpi_text, fontsize=11, va='center', ha='center',
340
  bbox=dict(boxstyle="square,pad=1.5", facecolor='#e6f2ff', edgecolor='#377eb8', lw=2))
341
  ax5.text(0.5, -0.15, "Math: Since pipeline algorithms don't use 'Training Loss',\nthese KPIs provide the absolute mathematical grade of the topics.",
342
  fontsize=10, ha='center', va='top', transform=ax5.transAxes, bbox=box_style)