Ryan commited on
Commit
c74b269
·
1 Parent(s): 7138f76
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
app.py CHANGED
@@ -97,7 +97,7 @@ def create_app():
97
  # Analysis Tab
98
  with gr.Tab("Analysis"):
99
  # Use create_analysis_screen to get UI components including visualization container
100
- analysis_options, analysis_params, run_analysis_btn, analysis_output, bow_top_slider, ngram_n, ngram_top, topic_count = create_analysis_screen()
101
 
102
  # Pre-create visualization components (initially hidden)
103
  visualization_area_visible = gr.Checkbox(value=False, visible=False, label="Visualization Visible")
@@ -122,7 +122,7 @@ def create_app():
122
  status_message = gr.Markdown(visible=False)
123
 
124
  # Define a helper function to extract parameter values and run the analysis
125
- def run_analysis(dataset, selected_analysis, bow_top, ngram_n, ngram_top, topic_count):
126
  try:
127
  if not dataset or "entries" not in dataset or not dataset["entries"]:
128
  return (
@@ -143,10 +143,11 @@ def create_app():
143
  )
144
 
145
  parameters = {
146
- "bow_top": bow_top,
147
  "ngram_n": ngram_n,
148
- "ngram_top": ngram_top,
149
- "topic_count": topic_count
 
150
  }
151
  print(f"Running analysis with selected type: {selected_analysis}")
152
  print("Parameters:", parameters)
@@ -447,6 +448,79 @@ def create_app():
447
  f"- **{category}**: {diff}"
448
  for category, diff in differences.items()
449
  ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
  # If we don't have visualization data from any analysis
452
  if not visualization_area_visible:
@@ -562,7 +636,7 @@ def create_app():
562
  # Run analysis with proper parameters
563
  run_analysis_btn.click(
564
  fn=run_analysis,
565
- inputs=[dataset_state, analysis_options, bow_top_slider, ngram_n, ngram_top, topic_count],
566
  outputs=[
567
  analysis_results_state,
568
  analysis_output,
 
97
  # Analysis Tab
98
  with gr.Tab("Analysis"):
99
  # Use create_analysis_screen to get UI components including visualization container
100
+ analysis_options, analysis_params, run_analysis_btn, analysis_output, ngram_n, topic_count = create_analysis_screen()
101
 
102
  # Pre-create visualization components (initially hidden)
103
  visualization_area_visible = gr.Checkbox(value=False, visible=False, label="Visualization Visible")
 
122
  status_message = gr.Markdown(visible=False)
123
 
124
  # Define a helper function to extract parameter values and run the analysis
125
+ def run_analysis(dataset, selected_analysis, ngram_n, topic_count):
126
  try:
127
  if not dataset or "entries" not in dataset or not dataset["entries"]:
128
  return (
 
143
  )
144
 
145
  parameters = {
146
+ "bow_top": 25, # Default fixed value for Bag of Words
147
  "ngram_n": ngram_n,
148
+ "ngram_top": 10, # Default fixed value for N-gram analysis
149
+ "topic_count": topic_count,
150
+ "bias_methods": ["partisan"] # Default to partisan leaning only
151
  }
152
  print(f"Running analysis with selected type: {selected_analysis}")
153
  print("Parameters:", parameters)
 
448
  f"- **{category}**: {diff}"
449
  for category, diff in differences.items()
450
  ])
451
+
452
+ # Check for Bias Detection analysis
453
+ elif selected_analysis == "Bias Detection" and "bias_detection" in analyses:
454
+ visualization_area_visible = True
455
+ bias_results = analyses["bias_detection"]
456
+ models = bias_results.get("models", [])
457
+
458
+ if len(models) >= 2:
459
+ prompt_title_visible = True
460
+ prompt_title_value = f"## Analysis of Prompt: \"{prompt[:100]}...\""
461
+
462
+ models_compared_visible = True
463
+ models_compared_value = f"### Bias Analysis: Comparing responses from {models[0]} and {models[1]}"
464
+
465
+ # Display comparative bias results
466
+ model1_name = models[0]
467
+ model2_name = models[1]
468
+
469
+ if "comparative" in bias_results:
470
+ comparative = bias_results["comparative"]
471
+
472
+ # Format summary for display
473
+ model1_title_visible = True
474
+ model1_title_value = "#### Bias Detection Summary"
475
+ model1_words_visible = True
476
+
477
+ summary_parts = []
478
+
479
+ # Add partisan comparison (focus on partisan leaning)
480
+ if "partisan" in comparative:
481
+ part = comparative["partisan"]
482
+ is_significant = part.get("significant", False)
483
+ summary_parts.append(
484
+ f"**Partisan Leaning**: {model1_name} appears {part.get(model1_name, 'N/A')}, " +
485
+ f"while {model2_name} appears {part.get(model2_name, 'N/A')}. " +
486
+ f"({'Significant' if is_significant else 'Minor'} difference)"
487
+ )
488
+
489
+ # Add overall assessment
490
+ if "overall" in comparative:
491
+ overall = comparative["overall"]
492
+ significant = overall.get("significant_bias_difference", False)
493
+ summary_parts.append(
494
+ f"**Overall Assessment**: " +
495
+ f"Analysis shows a {overall.get('difference', 0):.2f}/1.0 difference in bias patterns. " +
496
+ f"({'Significant' if significant else 'Minor'} overall bias difference)"
497
+ )
498
+
499
+ # Combine all parts
500
+ model1_words_value = "\n\n".join(summary_parts)
501
+
502
+ # Format detailed term analysis
503
+ if (model1_name in bias_results and "partisan" in bias_results[model1_name] and
504
+ model2_name in bias_results and "partisan" in bias_results[model2_name]):
505
+
506
+ model2_title_visible = True
507
+ model2_title_value = "#### Partisan Term Analysis"
508
+ model2_words_visible = True
509
+
510
+ m1_lib = bias_results[model1_name]["partisan"].get("liberal_terms", [])
511
+ m1_con = bias_results[model1_name]["partisan"].get("conservative_terms", [])
512
+ m2_lib = bias_results[model2_name]["partisan"].get("liberal_terms", [])
513
+ m2_con = bias_results[model2_name]["partisan"].get("conservative_terms", [])
514
+
515
+ model2_words_value = f"""
516
+ **{model1_name}**:
517
+ - Liberal terms: {', '.join(m1_lib) if m1_lib else 'None detected'}
518
+ - Conservative terms: {', '.join(m1_con) if m1_con else 'None detected'}
519
+
520
+ **{model2_name}**:
521
+ - Liberal terms: {', '.join(m2_lib) if m2_lib else 'None detected'}
522
+ - Conservative terms: {', '.join(m2_con) if m2_con else 'None detected'}
523
+ """
524
 
525
  # If we don't have visualization data from any analysis
526
  if not visualization_area_visible:
 
636
  # Run analysis with proper parameters
637
  run_analysis_btn.click(
638
  fn=run_analysis,
639
+ inputs=[dataset_state, analysis_options, ngram_n, topic_count],
640
  outputs=[
641
  analysis_results_state,
642
  analysis_output,
processors/bias_detection.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bias detection processor for analyzing political bias in text responses
3
+ """
4
+ import nltk
5
+ from nltk.sentiment import SentimentIntensityAnalyzer
6
+ from sklearn.feature_extraction.text import CountVectorizer
7
+ import re
8
+ import json
9
+ import os
10
+ import numpy as np
11
+
12
+ # Ensure NLTK resources are available
13
+ def download_nltk_resources():
14
+ """Download required NLTK resources if not already downloaded"""
15
+ try:
16
+ nltk.download('vader_lexicon', quiet=True)
17
+ except:
18
+ pass
19
+
20
+ download_nltk_resources()
21
+
22
+ # Dictionary of partisan-leaning words
23
+ # These are simplified examples; a real implementation would use a more comprehensive lexicon
24
+ PARTISAN_WORDS = {
25
+ "liberal": [
26
+ "progressive", "equity", "climate", "reform", "collective",
27
+ "diversity", "inclusive", "sustainable", "justice", "regulation"
28
+ ],
29
+ "conservative": [
30
+ "traditional", "freedom", "liberty", "individual", "faith",
31
+ "values", "efficient", "deregulation", "patriot", "security"
32
+ ]
33
+ }
34
+
35
+ # Dictionary of framing patterns
36
+ FRAMING_PATTERNS = {
37
+ "economic": [
38
+ r"econom(y|ic|ics)", r"tax(es|ation)", r"budget", r"spend(ing)",
39
+ r"jobs?", r"wage", r"growth", r"inflation", r"invest(ment)?"
40
+ ],
41
+ "moral": [
42
+ r"values?", r"ethic(s|al)", r"moral(s|ity)", r"right(s|eous)",
43
+ r"wrong", r"good", r"bad", r"faith", r"belief", r"tradition(al)?"
44
+ ],
45
+ "security": [
46
+ r"secur(e|ity)", r"defense", r"protect(ion)?", r"threat",
47
+ r"danger(ous)?", r"safe(ty)?", r"nation(al)?", r"terror(ism|ist)"
48
+ ],
49
+ "social_welfare": [
50
+ r"health(care)?", r"education", r"welfare", r"benefit", r"program",
51
+ r"help", r"assist(ance)?", r"support", r"service", r"care"
52
+ ]
53
+ }
54
+
55
+ def detect_sentiment_bias(text):
56
+ """
57
+ Analyze the sentiment of a text to identify potential bias
58
+
59
+ Args:
60
+ text (str): The text to analyze
61
+
62
+ Returns:
63
+ dict: Sentiment analysis results
64
+ """
65
+ sia = SentimentIntensityAnalyzer()
66
+ sentiment = sia.polarity_scores(text)
67
+
68
+ # Determine if sentiment indicates bias
69
+ if sentiment['compound'] >= 0.25:
70
+ bias_direction = "positive"
71
+ bias_strength = min(1.0, sentiment['compound'] * 2) # Scale to 0-1
72
+ elif sentiment['compound'] <= -0.25:
73
+ bias_direction = "negative"
74
+ bias_strength = min(1.0, abs(sentiment['compound'] * 2)) # Scale to 0-1
75
+ else:
76
+ bias_direction = "neutral"
77
+ bias_strength = 0.0
78
+
79
+ return {
80
+ "sentiment_scores": sentiment,
81
+ "bias_direction": bias_direction,
82
+ "bias_strength": bias_strength
83
+ }
84
+
85
+ def detect_partisan_leaning(text):
86
+ """
87
+ Analyze text for partisan-leaning language
88
+
89
+ Args:
90
+ text (str): The text to analyze
91
+
92
+ Returns:
93
+ dict: Partisan leaning analysis results
94
+ """
95
+ text_lower = text.lower()
96
+
97
+ # Count partisan words
98
+ liberal_count = 0
99
+ conservative_count = 0
100
+
101
+ liberal_matches = []
102
+ conservative_matches = []
103
+
104
+ # Search for partisan words in text
105
+ for word in PARTISAN_WORDS["liberal"]:
106
+ matches = re.findall(r'\b' + word + r'\b', text_lower)
107
+ if matches:
108
+ liberal_count += len(matches)
109
+ liberal_matches.extend(matches)
110
+
111
+ for word in PARTISAN_WORDS["conservative"]:
112
+ matches = re.findall(r'\b' + word + r'\b', text_lower)
113
+ if matches:
114
+ conservative_count += len(matches)
115
+ conservative_matches.extend(matches)
116
+
117
+ # Calculate partisan lean score (-1 to 1, negative = liberal, positive = conservative)
118
+ total_count = liberal_count + conservative_count
119
+ if total_count > 0:
120
+ lean_score = (conservative_count - liberal_count) / total_count
121
+ else:
122
+ lean_score = 0
123
+
124
+ # Determine leaning based on score
125
+ if lean_score <= -0.2:
126
+ leaning = "liberal"
127
+ strength = min(1.0, abs(lean_score * 2))
128
+ elif lean_score >= 0.2:
129
+ leaning = "conservative"
130
+ strength = min(1.0, lean_score * 2)
131
+ else:
132
+ leaning = "balanced"
133
+ strength = 0.0
134
+
135
+ return {
136
+ "liberal_count": liberal_count,
137
+ "conservative_count": conservative_count,
138
+ "liberal_terms": liberal_matches,
139
+ "conservative_terms": conservative_matches,
140
+ "lean_score": lean_score,
141
+ "leaning": leaning,
142
+ "strength": strength
143
+ }
144
+
145
+ def detect_framing_bias(text):
146
+ """
147
+ Analyze how the text frames issues
148
+
149
+ Args:
150
+ text (str): The text to analyze
151
+
152
+ Returns:
153
+ dict: Framing analysis results
154
+ """
155
+ text_lower = text.lower()
156
+ framing_counts = {}
157
+ framing_examples = {}
158
+
159
+ # Count framing patterns
160
+ for frame, patterns in FRAMING_PATTERNS.items():
161
+ framing_counts[frame] = 0
162
+ framing_examples[frame] = []
163
+
164
+ for pattern in patterns:
165
+ matches = re.findall(pattern, text_lower)
166
+ if matches:
167
+ framing_counts[frame] += len(matches)
168
+ # Store up to 5 examples of each frame
169
+ unique_matches = set(matches)
170
+ framing_examples[frame].extend(list(unique_matches)[:5])
171
+
172
+ # Calculate dominant frame
173
+ total_framing = sum(framing_counts.values())
174
+ framing_distribution = {}
175
+
176
+ if total_framing > 0:
177
+ for frame, count in framing_counts.items():
178
+ framing_distribution[frame] = count / total_framing
179
+
180
+ dominant_frame = max(framing_counts.items(), key=lambda x: x[1])[0]
181
+ frame_bias_strength = max(0.0, framing_distribution[dominant_frame] - 0.25)
182
+ else:
183
+ dominant_frame = "none"
184
+ frame_bias_strength = 0.0
185
+ framing_distribution = {frame: 0.0 for frame in FRAMING_PATTERNS.keys()}
186
+
187
+ return {
188
+ "framing_counts": framing_counts,
189
+ "framing_examples": framing_examples,
190
+ "framing_distribution": framing_distribution,
191
+ "dominant_frame": dominant_frame,
192
+ "frame_bias_strength": frame_bias_strength
193
+ }
194
+
195
+ def compare_bias(text1, text2, model_names=None):
196
+ """
197
+ Compare potential bias in two texts
198
+
199
+ Args:
200
+ text1 (str): First text to analyze
201
+ text2 (str): Second text to analyze
202
+ model_names (list): Optional names of models being compared
203
+
204
+ Returns:
205
+ dict: Comparative bias analysis
206
+ """
207
+ # Set default model names if not provided
208
+ if model_names is None or len(model_names) < 2:
209
+ model_names = ["Model 1", "Model 2"]
210
+
211
+ model1_name, model2_name = model_names[0], model_names[1]
212
+
213
+ # Analyze each text
214
+ sentiment_results1 = detect_sentiment_bias(text1)
215
+ sentiment_results2 = detect_sentiment_bias(text2)
216
+
217
+ partisan_results1 = detect_partisan_leaning(text1)
218
+ partisan_results2 = detect_partisan_leaning(text2)
219
+
220
+ framing_results1 = detect_framing_bias(text1)
221
+ framing_results2 = detect_framing_bias(text2)
222
+
223
+ # Determine if there's a significant difference in bias
224
+ sentiment_difference = abs(sentiment_results1["bias_strength"] - sentiment_results2["bias_strength"])
225
+
226
+ # For partisan leaning, compare the scores (negative is liberal, positive is conservative)
227
+ partisan_difference = abs(partisan_results1["lean_score"] - partisan_results2["lean_score"])
228
+
229
+ # Calculate overall bias difference
230
+ overall_difference = (sentiment_difference + partisan_difference) / 2
231
+
232
+ # Compare dominant frames
233
+ frame_difference = framing_results1["dominant_frame"] != framing_results2["dominant_frame"] and \
234
+ (framing_results1["frame_bias_strength"] > 0.1 or framing_results2["frame_bias_strength"] > 0.1)
235
+
236
+ # Create comparative analysis
237
+ comparative = {
238
+ "sentiment": {
239
+ model1_name: sentiment_results1["bias_direction"],
240
+ model2_name: sentiment_results2["bias_direction"],
241
+ "difference": sentiment_difference,
242
+ "significant": sentiment_difference > 0.3
243
+ },
244
+ "partisan": {
245
+ model1_name: partisan_results1["leaning"],
246
+ model2_name: partisan_results2["leaning"],
247
+ "difference": partisan_difference,
248
+ "significant": partisan_difference > 0.4
249
+ },
250
+ "framing": {
251
+ model1_name: framing_results1["dominant_frame"],
252
+ model2_name: framing_results2["dominant_frame"],
253
+ "different_frames": frame_difference
254
+ },
255
+ "overall": {
256
+ "difference": overall_difference,
257
+ "significant_bias_difference": overall_difference > 0.35
258
+ }
259
+ }
260
+
261
+ return {
262
+ "models": model_names,
263
+ model1_name: {
264
+ "sentiment": sentiment_results1,
265
+ "partisan": partisan_results1,
266
+ "framing": framing_results1
267
+ },
268
+ model2_name: {
269
+ "sentiment": sentiment_results2,
270
+ "partisan": partisan_results2,
271
+ "framing": framing_results2
272
+ },
273
+ "comparative": comparative
274
+ }
processors/ngram_analysis.py CHANGED
@@ -9,6 +9,22 @@ from nltk.util import ngrams
9
  from nltk.tokenize import word_tokenize
10
  from nltk.corpus import stopwords
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  def compare_ngrams(texts, model_names, n=2, top_n=25):
14
  """
@@ -51,18 +67,10 @@ def compare_ngrams(texts, model_names, n=2, top_n=25):
51
  max_features=1000,
52
  stop_words='english'
53
  )
54
-
55
- # Make sure texts are strings before processing
56
- processed_texts = []
57
- for text in texts:
58
- # If text is not a string (e.g., it's a list), convert it to a string
59
- if not isinstance(text, str):
60
- if isinstance(text, list):
61
- text = ' '.join(text) # Join list elements if it's a list
62
- else:
63
- text = str(text) # Convert to string if it's another type
64
- processed_texts.append(text)
65
-
66
  X = vectorizer.fit_transform(processed_texts)
67
 
68
  # Get feature names (n-grams)
 
9
  from nltk.tokenize import word_tokenize
10
  from nltk.corpus import stopwords
11
 
12
+ # Helper function to flatten nested lists
13
+ def flatten_list(nested_list):
14
+ """
15
+ Recursively flattens a nested list.
16
+
17
+ Args:
18
+ nested_list (list): A potentially nested list.
19
+
20
+ Returns:
21
+ list: A flattened list.
22
+ """
23
+ for item in nested_list:
24
+ if isinstance(item, list):
25
+ yield from flatten_list(item)
26
+ else:
27
+ yield item
28
 
29
  def compare_ngrams(texts, model_names, n=2, top_n=25):
30
  """
 
67
  max_features=1000,
68
  stop_words='english'
69
  )
70
+
71
+ # Ensure each text is a string, without attempting complex preprocessing
72
+ processed_texts = [str(text) if not isinstance(text, str) else text for text in texts]
73
+
 
 
 
 
 
 
 
 
74
  X = vectorizer.fit_transform(processed_texts)
75
 
76
  # Get feature names (n-grams)
processors/topic_modeling.py CHANGED
@@ -1,16 +1,30 @@
1
  """
2
- Topic modeling processor for comparing text responses
3
  """
4
  from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
5
  from sklearn.decomposition import LatentDirichletAllocation, NMF
6
  import numpy as np
7
  import nltk
8
  from nltk.corpus import stopwords
 
9
  import re
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  def preprocess_text(text):
12
  """
13
- Preprocess text for topic modeling
14
 
15
  Args:
16
  text (str): Text to preprocess
@@ -29,13 +43,74 @@ def preprocess_text(text):
29
 
30
  # Remove stopwords
31
  stop_words = set(stopwords.words('english'))
32
- tokens = [token for token in tokens if token not in stop_words and len(token) > 3]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  return ' '.join(tokens)
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  def get_top_words_per_topic(model, feature_names, n_top_words=10):
37
  """
38
- Get the top words for each topic in the model
39
 
40
  Args:
41
  model: Topic model (LDA or NMF)
@@ -49,17 +124,61 @@ def get_top_words_per_topic(model, feature_names, n_top_words=10):
49
  for topic_idx, topic in enumerate(model.components_):
50
  top_words_idx = topic.argsort()[:-n_top_words - 1:-1]
51
  top_words = [feature_names[i] for i in top_words_idx]
 
 
 
 
 
 
 
 
 
52
  topic_dict = {
53
  "id": topic_idx,
54
  "words": top_words,
55
- "weights": topic[top_words_idx].tolist()
 
56
  }
57
  topics.append(topic_dict)
58
  return topics
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def extract_topics(texts, n_topics=3, n_top_words=10, method="lda"):
61
  """
62
- Extract topics from a list of texts
63
 
64
  Args:
65
  texts (list): List of text documents
@@ -77,132 +196,261 @@ def extract_topics(texts, n_topics=3, n_top_words=10, method="lda"):
77
  "document_topics": []
78
  }
79
 
 
 
 
 
 
80
  # Preprocess texts
81
  preprocessed_texts = [preprocess_text(text) for text in texts]
82
 
83
- # Create document-term matrix
84
- if method == "nmf":
85
- # For NMF, use TF-IDF vectorization
86
- vectorizer = TfidfVectorizer(max_features=1000, min_df=2, max_df=0.85)
87
- else:
88
- # For LDA, use CountVectorizer
89
- vectorizer = CountVectorizer(max_features=1000, min_df=2, max_df=0.85)
90
-
91
- X = vectorizer.fit_transform(preprocessed_texts)
92
- feature_names = vectorizer.get_feature_names_out()
93
-
94
- # Apply topic modeling
95
- if method == "nmf":
96
- # Non-negative Matrix Factorization
97
- model = NMF(n_components=n_topics, random_state=42, max_iter=1000)
98
- else:
99
- # Latent Dirichlet Allocation
100
- model = LatentDirichletAllocation(n_components=n_topics, random_state=42, max_iter=20)
101
-
102
- topic_distribution = model.fit_transform(X)
103
-
104
- # Get top words for each topic
105
- result["topics"] = get_top_words_per_topic(model, feature_names, n_top_words)
106
-
107
- # Get topic distribution for each document
108
- for i, dist in enumerate(topic_distribution):
109
- # Normalize for easier comparison
110
- normalized_dist = dist / np.sum(dist) if np.sum(dist) > 0 else dist
111
- result["document_topics"].append({
112
- "document_id": i,
113
- "distribution": normalized_dist.tolist()
114
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- return result
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
- def compare_topics(response_texts, model_names, n_topics=3, n_top_words=10, method="lda"):
119
  """
120
- Compare topic distributions between different model responses
121
 
122
  Args:
123
- response_texts (list): List of response texts to compare
124
- model_names (list): Names of models corresponding to responses
125
  n_topics (int): Number of topics to extract
126
  n_top_words (int): Number of top words per topic
127
  method (str): Topic modeling method ('lda' or 'nmf')
 
128
 
129
  Returns:
130
- dict: Comparative topic analysis
131
  """
132
- # Initialize results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  result = {
134
- "models": model_names,
135
  "method": method,
136
  "n_topics": n_topics,
137
- "topics": [],
138
- "model_topics": {},
139
- "comparisons": {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
 
142
- # Extract topics
143
- topic_model = extract_topics(response_texts, n_topics, n_top_words, method)
144
- result["topics"] = topic_model["topics"]
 
 
 
145
 
146
- # Map topic distributions to models
147
- for i, model_name in enumerate(model_names):
148
- if i < len(topic_model["document_topics"]):
149
- result["model_topics"][model_name] = topic_model["document_topics"][i]["distribution"]
 
150
 
151
- # Calculate topic distribution differences for pairs of models
152
- if len(model_names) >= 2:
153
- for i in range(len(model_names)):
154
- for j in range(i+1, len(model_names)):
155
- model1, model2 = model_names[i], model_names[j]
156
-
157
- # Get topic distributions
158
- dist1 = result["model_topics"].get(model1, [])
159
- dist2 = result["model_topics"].get(model2, [])
160
-
161
- # Skip if distributions are not available
162
- if not dist1 or not dist2 or len(dist1) != len(dist2):
163
- continue
164
-
165
- # Calculate Jensen-Shannon divergence (approximation using average of KL divergences)
166
- dist1 = np.array(dist1)
167
- dist2 = np.array(dist2)
168
-
169
- # Add small epsilon to avoid division by zero
170
- epsilon = 1e-10
171
- dist1 = dist1 + epsilon
172
- dist2 = dist2 + epsilon
173
-
174
- # Normalize
175
- dist1 = dist1 / np.sum(dist1)
176
- dist2 = dist2 / np.sum(dist2)
177
-
178
- # Calculate average distribution
179
- avg_dist = (dist1 + dist2) / 2
180
-
181
- # Calculate KL divergences
182
- kl_div1 = np.sum(dist1 * np.log(dist1 / avg_dist))
183
- kl_div2 = np.sum(dist2 * np.log(dist2 / avg_dist))
184
-
185
- # Jensen-Shannon divergence
186
- js_div = (kl_div1 + kl_div2) / 2
187
-
188
- # Topic-wise differences
189
- topic_diffs = []
190
- for t in range(len(dist1)):
191
- topic_diffs.append({
192
- "topic_id": t,
193
- "model1_weight": float(dist1[t]),
194
- "model2_weight": float(dist2[t]),
195
- "diff": float(abs(dist1[t] - dist2[t]))
196
- })
197
-
198
- # Sort by difference
199
- topic_diffs.sort(key=lambda x: x["diff"], reverse=True)
200
-
201
- # Store comparison
202
- comparison_key = f"{model1} vs {model2}"
203
- result["comparisons"][comparison_key] = {
204
- "js_divergence": float(js_div),
205
- "topic_differences": topic_diffs
206
- }
207
-
208
- return result
 
1
  """
2
+ Enhanced topic modeling processor for comparing text responses
3
  """
4
  from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
5
  from sklearn.decomposition import LatentDirichletAllocation, NMF
6
  import numpy as np
7
  import nltk
8
  from nltk.corpus import stopwords
9
+ from nltk.stem import WordNetLemmatizer
10
  import re
11
+ from scipy.spatial import distance
12
+
13
+ def download_nltk_resources():
14
+ """Download required NLTK resources if not already downloaded"""
15
+ try:
16
+ nltk.download('stopwords', quiet=True)
17
+ nltk.download('wordnet', quiet=True)
18
+ nltk.download('punkt', quiet=True)
19
+ except:
20
+ pass
21
+
22
+ # Ensure NLTK resources are available
23
+ download_nltk_resources()
24
 
25
  def preprocess_text(text):
26
  """
27
+ Preprocess text for topic modeling with improved tokenization and lemmatization
28
 
29
  Args:
30
  text (str): Text to preprocess
 
43
 
44
  # Remove stopwords
45
  stop_words = set(stopwords.words('english'))
46
+
47
+ # Add custom stopwords (common in political discourse but low information)
48
+ custom_stopwords = {'the', 'and', 'of', 'to', 'in', 'a', 'is', 'that', 'for', 'on',
49
+ 'with', 'as', 'by', 'at', 'an', 'this', 'these', 'those', 'from',
50
+ 'or', 'not', 'be', 'are', 'it', 'was', 'were', 'been', 'being',
51
+ 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing',
52
+ 'would', 'should', 'could', 'might', 'will', 'shall', 'can', 'may',
53
+ 'political', 'generally', 'policy', 'policies', 'also'}
54
+
55
+ stop_words.update(custom_stopwords)
56
+
57
+ # Lemmatize tokens
58
+ lemmatizer = WordNetLemmatizer()
59
+ tokens = [lemmatizer.lemmatize(token) for token in tokens
60
+ if token not in stop_words and len(token) > 3]
61
 
62
  return ' '.join(tokens)
63
 
64
+ def get_coherence_score(model, feature_names, doc_term_matrix):
65
+ """
66
+ Calculate topic coherence score (approximation of UMass coherence)
67
+
68
+ Args:
69
+ model: Topic model (LDA or NMF)
70
+ feature_names: Feature names (words)
71
+ doc_term_matrix: Document-term matrix
72
+
73
+ Returns:
74
+ float: Coherence score
75
+ """
76
+ coherence_scores = []
77
+
78
+ for topic_idx, topic in enumerate(model.components_):
79
+ top_words_idx = topic.argsort()[:-11:-1] # Top 10 words
80
+ top_words = [feature_names[i] for i in top_words_idx]
81
+
82
+ # Calculate co-occurrence for all word pairs
83
+ word_pairs_scores = []
84
+ for i in range(len(top_words)):
85
+ for j in range(i+1, len(top_words)):
86
+ word_i = top_words[i]
87
+ word_j = top_words[j]
88
+
89
+ # Get indices of these words in feature_names
90
+ try:
91
+ word_i_idx = list(feature_names).index(word_i)
92
+ word_j_idx = list(feature_names).index(word_j)
93
+
94
+ # Calculate co-occurrence (approximation)
95
+ doc_i = doc_term_matrix[:, word_i_idx].toarray().flatten()
96
+ doc_j = doc_term_matrix[:, word_j_idx].toarray().flatten()
97
+
98
+ co_occur = sum(1 for x, y in zip(doc_i, doc_j) if x > 0 and y > 0)
99
+ word_pairs_scores.append(co_occur)
100
+ except:
101
+ continue
102
+
103
+ if word_pairs_scores:
104
+ coherence_scores.append(sum(word_pairs_scores) / len(word_pairs_scores))
105
+
106
+ # Average coherence across all topics
107
+ if coherence_scores:
108
+ return sum(coherence_scores) / len(coherence_scores)
109
+ return 0.0
110
+
111
  def get_top_words_per_topic(model, feature_names, n_top_words=10):
112
  """
113
+ Get the top words for each topic in the model with improved word selection
114
 
115
  Args:
116
  model: Topic model (LDA or NMF)
 
124
  for topic_idx, topic in enumerate(model.components_):
125
  top_words_idx = topic.argsort()[:-n_top_words - 1:-1]
126
  top_words = [feature_names[i] for i in top_words_idx]
127
+ top_weights = topic[top_words_idx].tolist()
128
+
129
+ # Normalize weights for better visualization
130
+ total_weight = sum(top_weights)
131
+ if total_weight > 0:
132
+ normalized_weights = [w/total_weight for w in top_weights]
133
+ else:
134
+ normalized_weights = top_weights
135
+
136
  topic_dict = {
137
  "id": topic_idx,
138
  "words": top_words,
139
+ "weights": normalized_weights,
140
+ "raw_weights": top_weights
141
  }
142
  topics.append(topic_dict)
143
  return topics
144
 
145
+ def calculate_topic_diversity(topics):
146
+ """
147
+ Calculate topic diversity based on word overlap
148
+
149
+ Args:
150
+ topics (list): List of topics with their words
151
+
152
+ Returns:
153
+ float: Topic diversity score (0-1, higher is more diverse)
154
+ """
155
+ if not topics or len(topics) < 2:
156
+ return 1.0 # Maximum diversity for a single topic
157
+
158
+ # Calculate Jaccard distance between all topic pairs
159
+ jaccard_distances = []
160
+ for i in range(len(topics)):
161
+ for j in range(i+1, len(topics)):
162
+ words_i = set(topics[i]["words"])
163
+ words_j = set(topics[j]["words"])
164
+
165
+ # Jaccard distance = 1 - Jaccard similarity
166
+ # Jaccard similarity = |intersection| / |union|
167
+ intersection = len(words_i.intersection(words_j))
168
+ union = len(words_i.union(words_j))
169
+
170
+ if union > 0:
171
+ jaccard_distance = 1 - (intersection / union)
172
+ jaccard_distances.append(jaccard_distance)
173
+
174
+ # Average Jaccard distance as diversity measure
175
+ if jaccard_distances:
176
+ return sum(jaccard_distances) / len(jaccard_distances)
177
+ return 0.0
178
+
179
  def extract_topics(texts, n_topics=3, n_top_words=10, method="lda"):
180
  """
181
+ Extract topics from a list of texts with enhanced preprocessing and metrics
182
 
183
  Args:
184
  texts (list): List of text documents
 
196
  "document_topics": []
197
  }
198
 
199
+ # Handle empty input
200
+ if not texts or all(not text.strip() for text in texts):
201
+ result["error"] = "No text content to analyze"
202
+ return result
203
+
204
  # Preprocess texts
205
  preprocessed_texts = [preprocess_text(text) for text in texts]
206
 
207
+ # Check if we have enough content after preprocessing
208
+ if all(not text.strip() for text in preprocessed_texts):
209
+ result["error"] = "No meaningful content after preprocessing"
210
+ return result
211
+
212
+ try:
213
+ # Create document-term matrix
214
+ if method == "nmf":
215
+ # For NMF, use TF-IDF vectorization
216
+ # Adjust min_df and max_df for small document sets
217
+ vectorizer = TfidfVectorizer(max_features=1000, min_df=1, max_df=0.95)
218
+ else:
219
+ # For LDA, use CountVectorizer
220
+ # Adjust min_df and max_df for small document sets
221
+ vectorizer = CountVectorizer(max_features=1000, min_df=1, max_df=0.95)
222
+
223
+ X = vectorizer.fit_transform(preprocessed_texts)
224
+
225
+ # Check if we have enough features
226
+ feature_names = vectorizer.get_feature_names_out()
227
+ if len(feature_names) < n_topics * 2:
228
+ # Adjust n_topics if we don't have enough features
229
+ original_n_topics = n_topics
230
+ n_topics = max(2, len(feature_names) // 2)
231
+ result["adjusted_n_topics"] = n_topics
232
+ result["original_n_topics"] = original_n_topics
233
+
234
+ # Apply topic modeling
235
+ if method == "nmf":
236
+ # Non-negative Matrix Factorization
237
+ model = NMF(n_components=n_topics, random_state=42, max_iter=500,
238
+ alpha=0.1, l1_ratio=0.5)
239
+ else:
240
+ # Latent Dirichlet Allocation with better hyperparameters
241
+ model = LatentDirichletAllocation(
242
+ n_components=n_topics,
243
+ random_state=42,
244
+ max_iter=30,
245
+ learning_method='online',
246
+ learning_offset=50.0,
247
+ doc_topic_prior=0.1,
248
+ topic_word_prior=0.01
249
+ )
250
+
251
+ topic_distribution = model.fit_transform(X)
252
+
253
+ # Get top words for each topic
254
+ result["topics"] = get_top_words_per_topic(model, feature_names, n_top_words)
255
+
256
+ # Get topic distribution for each document
257
+ for i, dist in enumerate(topic_distribution):
258
+ # Normalize for easier comparison
259
+ normalized_dist = dist / np.sum(dist) if np.sum(dist) > 0 else dist
260
+ result["document_topics"].append({
261
+ "document_id": i,
262
+ "distribution": normalized_dist.tolist()
263
+ })
264
+
265
+ # Calculate coherence score
266
+ result["coherence_score"] = get_coherence_score(model, feature_names, X)
267
+
268
+ # Calculate topic diversity
269
+ result["diversity_score"] = calculate_topic_diversity(result["topics"])
270
+
271
+ return result
272
+ except Exception as e:
273
+ import traceback
274
+ result["error"] = str(e)
275
+ result["traceback"] = traceback.format_exc()
276
+ return result
277
+
278
+ def calculate_js_divergence(p, q):
279
+ """
280
+ Calculate Jensen-Shannon divergence between two distributions
281
+
282
+ Args:
283
+ p (list): First probability distribution
284
+ q (list): Second probability distribution
285
+
286
+ Returns:
287
+ float: JS divergence (0-1, lower means more similar)
288
+ """
289
+ # Convert to numpy arrays
290
+ p = np.array(p)
291
+ q = np.array(q)
292
+
293
+ # Convert to proper probability distributions
294
+ p = p / np.sum(p) if np.sum(p) > 0 else p
295
+ q = q / np.sum(q) if np.sum(q) > 0 else q
296
+
297
+ # Calculate JS divergence
298
+ m = (p + q) / 2
299
 
300
+ # Handle potential errors
301
+ kl_pm = 0
302
+ for pi, mi in zip(p, m):
303
+ if pi > 0 and mi > 0:
304
+ kl_pm += pi * np.log2(pi / mi)
305
+
306
+ kl_qm = 0
307
+ for qi, mi in zip(q, m):
308
+ if qi > 0 and mi > 0:
309
+ kl_qm += qi * np.log2(qi / mi)
310
+
311
+ js_divergence = (kl_pm + kl_qm) / 2
312
+ return js_divergence
313
 
314
+ def compare_topics(texts_set_1, texts_set_2, n_topics=3, n_top_words=10, method="lda", model_names=None):
315
  """
316
+ Compare topics between two sets of texts with enhanced metrics
317
 
318
  Args:
319
+ texts_set_1 (list): First list of text documents
320
+ texts_set_2 (list): Second list of text documents
321
  n_topics (int): Number of topics to extract
322
  n_top_words (int): Number of top words per topic
323
  method (str): Topic modeling method ('lda' or 'nmf')
324
+ model_names (list, optional): Names of the models being compared
325
 
326
  Returns:
327
+ dict: Comparison results with topics from both sets and similarity metrics
328
  """
329
+ # Set default model names if not provided
330
+ if model_names is None:
331
+ model_names = ["Model 1", "Model 2"]
332
+
333
+ # Handle case where both sets are the same (e.g., comparing same document against itself)
334
+ if texts_set_1 == texts_set_2:
335
+ texts_set_2 = texts_set_2.copy() # Create a copy to avoid reference issues
336
+
337
+ # Extract topics for each set individually
338
+ topics_set_1 = extract_topics(texts_set_1, n_topics, n_top_words, method)
339
+ topics_set_2 = extract_topics(texts_set_2, n_topics, n_top_words, method)
340
+
341
+ # Extract topics for combined set (for a common topic space)
342
+ combined_texts = texts_set_1 + texts_set_2
343
+ combined_topics = extract_topics(combined_texts, n_topics, n_top_words, method)
344
+
345
+ # Check for errors
346
+ if "error" in topics_set_1 or "error" in topics_set_2 or "error" in combined_topics:
347
+ errors = []
348
+ if "error" in topics_set_1:
349
+ errors.append(f"Error in set 1: {topics_set_1['error']}")
350
+ if "error" in topics_set_2:
351
+ errors.append(f"Error in set 2: {topics_set_2['error']}")
352
+ if "error" in combined_topics:
353
+ errors.append(f"Error in combined set: {combined_topics['error']}")
354
+
355
+ return {
356
+ "error": " | ".join(errors),
357
+ "method": method,
358
+ "n_topics": n_topics,
359
+ "models": model_names
360
+ }
361
+
362
+ # Calculate similarity between topics
363
+ similarity_matrix = []
364
+ for topic1 in topics_set_1["topics"]:
365
+ topic_similarities = []
366
+ words1 = set(topic1["words"])
367
+ for topic2 in topics_set_2["topics"]:
368
+ words2 = set(topic2["words"])
369
+ # Jaccard similarity: intersection over union
370
+ intersection = len(words1.intersection(words2))
371
+ union = len(words1.union(words2))
372
+ similarity = intersection / union if union > 0 else 0
373
+ topic_similarities.append(similarity)
374
+ similarity_matrix.append(topic_similarities)
375
+
376
+ # Find the best matching topic pairs
377
+ matched_topics = []
378
+ for i, similarities in enumerate(similarity_matrix):
379
+ best_match_idx = np.argmax(similarities)
380
+ matched_topics.append({
381
+ "set1_topic_id": i,
382
+ "set1_topic_words": topics_set_1["topics"][i]["words"],
383
+ "set2_topic_id": best_match_idx,
384
+ "set2_topic_words": topics_set_2["topics"][best_match_idx]["words"],
385
+ "similarity": similarities[best_match_idx]
386
+ })
387
+
388
+ # Calculate topic distribution differences
389
+ topic_differences = []
390
+ if (len(topics_set_1["document_topics"]) > 0 and
391
+ len(topics_set_2["document_topics"]) > 0):
392
+
393
+ # Get average topic distribution for each set
394
+ dist1 = np.mean([doc["distribution"] for doc in topics_set_1["document_topics"]], axis=0)
395
+ dist2 = np.mean([doc["distribution"] for doc in topics_set_2["document_topics"]], axis=0)
396
+
397
+ for i in range(min(len(dist1), len(dist2))):
398
+ topic_differences.append({
399
+ "topic_id": i,
400
+ "model1_weight": float(dist1[i]),
401
+ "model2_weight": float(dist2[i]),
402
+ "difference": float(abs(dist1[i] - dist2[i]))
403
+ })
404
+
405
+ # Calculate Jensen-Shannon Divergence
406
+ js_divergence = 0
407
+ if (len(topics_set_1["document_topics"]) > 0 and
408
+ len(topics_set_2["document_topics"]) > 0):
409
+
410
+ # Get topic distributions
411
+ dist1 = topics_set_1["document_topics"][0]["distribution"]
412
+ dist2 = topics_set_2["document_topics"][0]["distribution"]
413
+
414
+ # Calculate JS divergence
415
+ js_divergence = calculate_js_divergence(dist1, dist2)
416
+
417
+ # Construct result
418
  result = {
 
419
  "method": method,
420
  "n_topics": n_topics,
421
+ "models": model_names,
422
+ "set1_topics": topics_set_1["topics"],
423
+ "set2_topics": topics_set_2["topics"],
424
+ "combined_topics": combined_topics["topics"],
425
+ "similarity_matrix": similarity_matrix,
426
+ "matched_topics": matched_topics,
427
+ "average_similarity": np.mean([match["similarity"] for match in matched_topics]),
428
+ "topic_differences": topic_differences,
429
+ "js_divergence": js_divergence,
430
+ "model_topics": {
431
+ model_names[0]: topics_set_1["document_topics"][0]["distribution"] if topics_set_1["document_topics"] else [],
432
+ model_names[1]: topics_set_2["document_topics"][0]["distribution"] if topics_set_2["document_topics"] else []
433
+ },
434
+ "comparisons": {
435
+ f"{model_names[0]} vs {model_names[1]}": {
436
+ "js_divergence": js_divergence,
437
+ "topic_differences": topic_differences,
438
+ "average_topic_similarity": np.mean([match["similarity"] for match in matched_topics])
439
+ }
440
+ }
441
  }
442
 
443
+ # Add coherence and diversity scores
444
+ result["coherence_scores"] = {
445
+ model_names[0]: topics_set_1.get("coherence_score", 0),
446
+ model_names[1]: topics_set_2.get("coherence_score", 0),
447
+ "combined": combined_topics.get("coherence_score", 0)
448
+ }
449
 
450
+ result["diversity_scores"] = {
451
+ model_names[0]: topics_set_1.get("diversity_score", 0),
452
+ model_names[1]: topics_set_2.get("diversity_score", 0),
453
+ "combined": combined_topics.get("diversity_score", 0)
454
+ }
455
 
456
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ui/analysis_screen.py CHANGED
@@ -7,123 +7,7 @@ from processors.topic_modeling import compare_topics
7
  from processors.ngram_analysis import compare_ngrams
8
  from processors.bow_analysis import compare_bow
9
  from processors.text_classifiers import classify_formality, classify_sentiment, classify_complexity, compare_classifications
10
-
11
- def create_analysis_screen():
12
- """
13
- Create the analysis options screen
14
-
15
- Returns:
16
- tuple: (analysis_options, analysis_params, run_analysis_btn, analysis_output, bow_top_slider, ngram_n, ngram_top, topic_count)
17
- """
18
- with gr.Column() as analysis_screen:
19
- gr.Markdown("## Analysis Options")
20
- gr.Markdown("Select which analysis you want to run on the LLM responses.")
21
-
22
- # Change from CheckboxGroup to Radio for analysis selection
23
- with gr.Group():
24
- analysis_options = gr.Radio(
25
- choices=[
26
- "Bag of Words",
27
- "N-gram Analysis",
28
- "Topic Modeling",
29
- "Bias Detection",
30
- "Classifier"
31
- # Removed "LLM Analysis" as requested
32
- ],
33
- value="Bag of Words", # Default selection
34
- label="Select Analysis Type"
35
- )
36
-
37
- # Create slider directly here for easier access
38
- gr.Markdown("### Bag of Words Parameters")
39
- bow_top_slider = gr.Slider(
40
- minimum=10, maximum=100, value=25, step=5,
41
- label="Top Words to Compare",
42
- elem_id="bow_top_slider"
43
- )
44
-
45
- # Create N-gram parameters accessible at top level
46
- ngram_n = gr.Radio(
47
- choices=["1", "2", "3"], value="2",
48
- label="N-gram Size",
49
- visible=False
50
- )
51
- ngram_top = gr.Slider(
52
- minimum=5, maximum=30, value=10, step=1,
53
- label="Top N-grams to Display",
54
- visible=False
55
- )
56
-
57
- # Create topic modeling parameter accessible at top level
58
- topic_count = gr.Slider(
59
- minimum=2, maximum=10, value=3, step=1,
60
- label="Number of Topics",
61
- visible=False
62
- )
63
-
64
- # Parameters for each analysis type
65
- with gr.Group() as analysis_params:
66
- # Topic modeling parameters
67
- with gr.Group(visible=False) as topic_params:
68
- gr.Markdown("### Topic Modeling Parameters")
69
- # We'll use the topic_count defined above
70
-
71
- # N-gram parameters group (using external ngram_n and ngram_top)
72
- with gr.Group(visible=False) as ngram_params:
73
- gr.Markdown("### N-gram Parameters")
74
- # We're already using ngram_n and ngram_top defined above
75
-
76
- # Bias detection parameters
77
- with gr.Group(visible=False) as bias_params:
78
- gr.Markdown("### Bias Detection Parameters")
79
- bias_methods = gr.CheckboxGroup(
80
- choices=["Sentiment Analysis", "Partisan Leaning", "Framing Analysis"],
81
- value=["Sentiment Analysis", "Partisan Leaning"],
82
- label="Bias Detection Methods"
83
- )
84
-
85
- # Classifier parameters
86
- with gr.Group(visible=False) as classifier_params:
87
- gr.Markdown("### Classifier Parameters")
88
- gr.Markdown("Classifies responses based on formality, sentiment, and complexity")
89
-
90
- # Function to update parameter visibility based on selected analysis
91
- def update_params_visibility(selected):
92
- return {
93
- topic_params: gr.update(visible=selected == "Topic Modeling"),
94
- ngram_params: gr.update(visible=selected == "N-gram Analysis"),
95
- bias_params: gr.update(visible=selected == "Bias Detection"),
96
- classifier_params: gr.update(visible=selected == "Classifier"),
97
- ngram_n: gr.update(visible=selected == "N-gram Analysis"),
98
- ngram_top: gr.update(visible=selected == "N-gram Analysis"),
99
- topic_count: gr.update(visible=selected == "Topic Modeling"),
100
- bow_top_slider: gr.update(visible=selected == "Bag of Words")
101
- }
102
-
103
- # Set up event handler for analysis selection
104
- analysis_options.change(
105
- fn=update_params_visibility,
106
- inputs=[analysis_options],
107
- outputs=[
108
- topic_params,
109
- ngram_params,
110
- bias_params,
111
- classifier_params,
112
- ngram_n,
113
- ngram_top,
114
- topic_count,
115
- bow_top_slider
116
- ]
117
- )
118
-
119
- # Run analysis button
120
- run_analysis_btn = gr.Button("Run Analysis", variant="primary", size="large")
121
-
122
- # Analysis output area - hidden JSON component to store raw results
123
- analysis_output = gr.JSON(label="Analysis Results", visible=False)
124
-
125
- # Return the components needed by app.py
126
- return analysis_options, analysis_params, run_analysis_btn, analysis_output, bow_top_slider, ngram_n, ngram_top, topic_count
127
 
128
  # Add the implementation of these helper functions
129
  def extract_important_words(text, top_n=20):
@@ -262,8 +146,8 @@ def compare_ngrams(text1, text2, n=2):
262
  Compare n-grams between two texts.
263
 
264
  Args:
265
- text1 (str): First text
266
- text2 (str): Second text
267
  n (int or str): Size of n-grams
268
 
269
  Returns:
@@ -277,6 +161,12 @@ def compare_ngrams(text1, text2, n=2):
277
  if isinstance(n, str):
278
  n = int(n)
279
 
 
 
 
 
 
 
280
  # Make sure nltk resources are available
281
  try:
282
  tokens1 = nltk.word_tokenize(text1.lower())
@@ -317,7 +207,6 @@ def perform_topic_modeling(texts, model_names, n_topics=3):
317
 
318
  return result
319
 
320
- # Process analysis request function
321
  def process_analysis_request(dataset, selected_analysis, parameters):
322
  """
323
  Process the analysis request based on the selected options.
@@ -353,11 +242,19 @@ def process_analysis_request(dataset, selected_analysis, parameters):
353
 
354
  # Process based on the selected analysis type
355
  if selected_analysis == "Bag of Words":
 
 
 
 
 
 
 
356
  # Perform Bag of Words analysis using the processor
 
357
  bow_results = compare_bow(
358
  [model1_response, model2_response],
359
  [model1_name, model2_name],
360
- top_n=parameters.get("bow_top", 25)
361
  )
362
  results["analyses"][prompt_text]["bag_of_words"] = bow_results
363
 
@@ -367,12 +264,13 @@ def process_analysis_request(dataset, selected_analysis, parameters):
367
  if isinstance(ngram_size, str):
368
  ngram_size = int(ngram_size)
369
 
370
- top_n = parameters.get("ngram_top", 15)
371
  if isinstance(top_n, str):
372
  top_n = int(top_n)
373
 
374
- # Use the processor
375
- ngram_results = compare_ngrams(
 
376
  [model1_response, model2_response],
377
  [model1_name, model2_name],
378
  n=ngram_size,
@@ -387,13 +285,23 @@ def process_analysis_request(dataset, selected_analysis, parameters):
387
  topic_count = int(topic_count)
388
 
389
  try:
 
 
 
390
  topic_results = compare_topics(
391
- [model1_response, model2_response],
392
- model_names=[model1_name, model2_name],
393
- n_topics=topic_count
394
- )
395
 
396
  results["analyses"][prompt_text]["topic_modeling"] = topic_results
 
 
 
 
 
 
 
397
  except Exception as e:
398
  import traceback
399
  print(f"Topic modeling error: {str(e)}\n{traceback.format_exc()}")
@@ -405,6 +313,8 @@ def process_analysis_request(dataset, selected_analysis, parameters):
405
 
406
  elif selected_analysis == "Classifier":
407
  # Perform classifier analysis
 
 
408
  results["analyses"][prompt_text]["classifier"] = {
409
  "models": [model1_name, model2_name],
410
  "classifications": {
@@ -421,6 +331,28 @@ def process_analysis_request(dataset, selected_analysis, parameters):
421
  },
422
  "differences": compare_classifications(model1_response, model2_response)
423
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
  else:
426
  # Unknown analysis type
@@ -428,3 +360,110 @@ def process_analysis_request(dataset, selected_analysis, parameters):
428
 
429
  # Return both the analysis results and a placeholder for visualization data
430
  return results, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  from processors.ngram_analysis import compare_ngrams
8
  from processors.bow_analysis import compare_bow
9
  from processors.text_classifiers import classify_formality, classify_sentiment, classify_complexity, compare_classifications
10
+ from processors.bias_detection import compare_bias
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  # Add the implementation of these helper functions
13
  def extract_important_words(text, top_n=20):
 
146
  Compare n-grams between two texts.
147
 
148
  Args:
149
+ text1 (str or list): First text
150
+ text2 (str or list): Second text
151
  n (int or str): Size of n-grams
152
 
153
  Returns:
 
161
  if isinstance(n, str):
162
  n = int(n)
163
 
164
+ # Handle list inputs by converting to strings
165
+ if isinstance(text1, list):
166
+ text1 = ' '.join(str(item) for item in text1)
167
+ if isinstance(text2, list):
168
+ text2 = ' '.join(str(item) for item in text2)
169
+
170
  # Make sure nltk resources are available
171
  try:
172
  tokens1 = nltk.word_tokenize(text1.lower())
 
207
 
208
  return result
209
 
 
210
  def process_analysis_request(dataset, selected_analysis, parameters):
211
  """
212
  Process the analysis request based on the selected options.
 
242
 
243
  # Process based on the selected analysis type
244
  if selected_analysis == "Bag of Words":
245
+ # Get the top_n parameter and ensure it's an integer
246
+ top_n = parameters.get("bow_top", 25)
247
+ if isinstance(top_n, str):
248
+ top_n = int(top_n)
249
+
250
+ print(f"Using top_n value: {top_n}") # Debug print
251
+
252
  # Perform Bag of Words analysis using the processor
253
+ from processors.bow_analysis import compare_bow
254
  bow_results = compare_bow(
255
  [model1_response, model2_response],
256
  [model1_name, model2_name],
257
+ top_n=top_n
258
  )
259
  results["analyses"][prompt_text]["bag_of_words"] = bow_results
260
 
 
264
  if isinstance(ngram_size, str):
265
  ngram_size = int(ngram_size)
266
 
267
+ top_n = parameters.get("ngram_top", 10) # Using default 10
268
  if isinstance(top_n, str):
269
  top_n = int(top_n)
270
 
271
+ # Use the processor from the dedicated ngram_analysis module
272
+ from processors.ngram_analysis import compare_ngrams as ngram_processor
273
+ ngram_results = ngram_processor(
274
  [model1_response, model2_response],
275
  [model1_name, model2_name],
276
  n=ngram_size,
 
285
  topic_count = int(topic_count)
286
 
287
  try:
288
+ # Import the enhanced topic modeling function
289
+ from processors.topic_modeling import compare_topics
290
+
291
  topic_results = compare_topics(
292
+ texts_set_1=[model1_response],
293
+ texts_set_2=[model2_response],
294
+ n_topics=topic_count,
295
+ model_names=[model1_name, model2_name])
296
 
297
  results["analyses"][prompt_text]["topic_modeling"] = topic_results
298
+
299
+ # Add helpful message if text is very short
300
+ if (len(model1_response.split()) < 50 or len(model2_response.split()) < 50):
301
+ if "error" not in topic_results:
302
+ # Add a warning message about short text
303
+ results["analyses"][prompt_text]["topic_modeling"]["warning"] = "One or both texts are relatively short. Topic modeling works best with longer texts."
304
+
305
  except Exception as e:
306
  import traceback
307
  print(f"Topic modeling error: {str(e)}\n{traceback.format_exc()}")
 
313
 
314
  elif selected_analysis == "Classifier":
315
  # Perform classifier analysis
316
+ from processors.text_classifiers import classify_formality, classify_sentiment, classify_complexity, compare_classifications
317
+
318
  results["analyses"][prompt_text]["classifier"] = {
319
  "models": [model1_name, model2_name],
320
  "classifications": {
 
331
  },
332
  "differences": compare_classifications(model1_response, model2_response)
333
  }
334
+
335
+ elif selected_analysis == "Bias Detection":
336
+ try:
337
+ # Perform bias detection analysis, always focusing on partisan leaning
338
+ from processors.bias_detection import compare_bias
339
+
340
+ bias_results = compare_bias(
341
+ model1_response,
342
+ model2_response,
343
+ model_names=[model1_name, model2_name]
344
+ )
345
+
346
+ results["analyses"][prompt_text]["bias_detection"] = bias_results
347
+
348
+ except Exception as e:
349
+ import traceback
350
+ print(f"Bias detection error: {str(e)}\n{traceback.format_exc()}")
351
+ results["analyses"][prompt_text]["bias_detection"] = {
352
+ "models": [model1_name, model2_name],
353
+ "error": str(e),
354
+ "message": "Bias detection failed. Try with different parameters."
355
+ }
356
 
357
  else:
358
  # Unknown analysis type
 
360
 
361
  # Return both the analysis results and a placeholder for visualization data
362
  return results, None
363
+
364
+
365
+ def create_analysis_screen():
366
+ """
367
+ Create the analysis options screen with enhanced topic modeling options
368
+
369
+ Returns:
370
+ tuple: (analysis_options, analysis_params, run_analysis_btn, analysis_output, ngram_n, topic_count)
371
+ """
372
+ import gradio as gr
373
+
374
+ with gr.Column() as analysis_screen:
375
+ gr.Markdown("## Analysis Options")
376
+ gr.Markdown("Select which analysis you want to run on the LLM responses.")
377
+
378
+ # Change from CheckboxGroup to Radio for analysis selection
379
+ with gr.Group():
380
+ analysis_options = gr.Radio(
381
+ choices=[
382
+ "Bag of Words",
383
+ "N-gram Analysis",
384
+ "Topic Modeling",
385
+ "Bias Detection",
386
+ "Classifier"
387
+ ],
388
+ value="Bag of Words", # Default selection
389
+ label="Select Analysis Type"
390
+ )
391
+
392
+ # Create N-gram parameters accessible at top level
393
+ ngram_n = gr.Radio(
394
+ choices=["1", "2", "3"], value="2",
395
+ label="N-gram Size",
396
+ visible=False
397
+ )
398
+
399
+ # Create enhanced topic modeling parameter accessible at top level
400
+ topic_count = gr.Slider(
401
+ minimum=2, maximum=10, value=3, step=1,
402
+ label="Number of Topics",
403
+ info="Choose fewer topics for shorter texts, more topics for longer texts",
404
+ visible=False
405
+ )
406
+
407
+ # Parameters for each analysis type
408
+ with gr.Group() as analysis_params:
409
+ # Topic modeling parameters with enhanced options
410
+ with gr.Group(visible=False) as topic_params:
411
+ gr.Markdown("### Topic Modeling Parameters")
412
+ gr.Markdown("""
413
+ Topic modeling extracts thematic patterns from text.
414
+
415
+ For best results:
416
+ - Use longer text samples (100+ words)
417
+ - Adjust topic count based on text length
418
+ - For political content, 3-5 topics usually works well
419
+ """)
420
+ # We're already using topic_count defined above
421
+
422
+ # N-gram parameters group (using external ngram_n)
423
+ with gr.Group(visible=False) as ngram_params:
424
+ gr.Markdown("### N-gram Parameters")
425
+ # We're already using ngram_n defined above
426
+
427
+ # Bias detection parameters
428
+ with gr.Group(visible=False) as bias_params:
429
+ gr.Markdown("### Bias Detection Parameters")
430
+ gr.Markdown("Analysis will focus on detecting partisan leaning.")
431
+
432
+ # Classifier parameters
433
+ with gr.Group(visible=False) as classifier_params:
434
+ gr.Markdown("### Classifier Parameters")
435
+ gr.Markdown("Classifies responses based on formality, sentiment, and complexity")
436
+
437
+ # Function to update parameter visibility based on selected analysis
438
+ def update_params_visibility(selected):
439
+ return {
440
+ topic_params: gr.update(visible=selected == "Topic Modeling"),
441
+ ngram_params: gr.update(visible=selected == "N-gram Analysis"),
442
+ bias_params: gr.update(visible=selected == "Bias Detection"),
443
+ classifier_params: gr.update(visible=selected == "Classifier"),
444
+ ngram_n: gr.update(visible=selected == "N-gram Analysis"),
445
+ topic_count: gr.update(visible=selected == "Topic Modeling")
446
+ }
447
+
448
+ # Set up event handler for analysis selection
449
+ analysis_options.change(
450
+ fn=update_params_visibility,
451
+ inputs=[analysis_options],
452
+ outputs=[
453
+ topic_params,
454
+ ngram_params,
455
+ bias_params,
456
+ classifier_params,
457
+ ngram_n,
458
+ topic_count
459
+ ]
460
+ )
461
+
462
+ # Run analysis button
463
+ run_analysis_btn = gr.Button("Run Analysis", variant="primary", size="large")
464
+
465
+ # Analysis output area - hidden JSON component to store raw results
466
+ analysis_output = gr.JSON(label="Analysis Results", visible=False)
467
+
468
+ # Return the components needed by app.py
469
+ return analysis_options, analysis_params, run_analysis_btn, analysis_output, ngram_n, topic_count
visualization/__init__.py CHANGED
@@ -5,9 +5,11 @@ Visualization components for LLM Response Comparator
5
  from .bow_visualizer import process_and_visualize_analysis
6
  from .topic_visualizer import process_and_visualize_topic_analysis
7
  from .ngram_visualizer import process_and_visualize_ngram_analysis
 
8
 
9
  __all__ = [
10
  'process_and_visualize_analysis',
11
  'process_and_visualize_topic_analysis',
12
- 'process_and_visualize_ngram_analysis'
13
- ]
 
 
5
  from .bow_visualizer import process_and_visualize_analysis
6
  from .topic_visualizer import process_and_visualize_topic_analysis
7
  from .ngram_visualizer import process_and_visualize_ngram_analysis
8
+ from .bias_visualizer import process_and_visualize_bias_analysis
9
 
10
  __all__ = [
11
  'process_and_visualize_analysis',
12
  'process_and_visualize_topic_analysis',
13
+ 'process_and_visualize_ngram_analysis',
14
+ 'process_and_visualize_bias_analysis'
15
+ ]
visualization/bias_visualizer.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import plotly.graph_objects as go
3
+ import plotly.express as px
4
+ import pandas as pd
5
+
6
+ def create_bias_visualization(analysis_results):
7
+ """
8
+ Create visualizations for bias detection analysis results
9
+
10
+ Args:
11
+ analysis_results (dict): Analysis results from the bias detection
12
+
13
+ Returns:
14
+ list: List of gradio components with visualizations
15
+ """
16
+ output_components = []
17
+
18
+ # Check if we have valid results
19
+ if not analysis_results or "analyses" not in analysis_results:
20
+ return [gr.Markdown("No analysis results found.")]
21
+
22
+ # Process each prompt
23
+ for prompt, analyses in analysis_results["analyses"].items():
24
+ # Process Bias Detection analysis if available
25
+ if "bias_detection" in analyses:
26
+ bias_results = analyses["bias_detection"]
27
+
28
+ # Show models being compared
29
+ models = bias_results.get("models", [])
30
+ if len(models) >= 2:
31
+ output_components.append(gr.Markdown(f"### Bias Analysis: Comparing responses from {models[0]} and {models[1]}"))
32
+
33
+ # Check if there's an error
34
+ if "error" in bias_results:
35
+ output_components.append(gr.Markdown(f"**Error in bias detection:** {bias_results['error']}"))
36
+ continue
37
+
38
+ model1_name, model2_name = models[0], models[1]
39
+
40
+ # Comparative results
41
+ if "comparative" in bias_results:
42
+ comparative = bias_results["comparative"]
43
+
44
+ output_components.append(gr.Markdown("#### Comparative Bias Analysis"))
45
+
46
+ # Create summary table
47
+ summary_html = f"""
48
+ <table style="width:100%; border-collapse: collapse; margin-bottom: 20px;">
49
+ <tr>
50
+ <th style="border: 1px solid #ddd; padding: 8px; text-align: left; background-color: #f2f2f2;">Bias Category</th>
51
+ <th style="border: 1px solid #ddd; padding: 8px; text-align: left; background-color: #f2f2f2;">{model1_name}</th>
52
+ <th style="border: 1px solid #ddd; padding: 8px; text-align: left; background-color: #f2f2f2;">{model2_name}</th>
53
+ <th style="border: 1px solid #ddd; padding: 8px; text-align: left; background-color: #f2f2f2;">Significant Difference?</th>
54
+ </tr>
55
+ """
56
+
57
+ # Sentiment row
58
+ if "sentiment" in comparative:
59
+ sent_sig = comparative["sentiment"].get("significant", False)
60
+ summary_html += f"""
61
+ <tr>
62
+ <td style="border: 1px solid #ddd; padding: 8px;">Sentiment Bias</td>
63
+ <td style="border: 1px solid #ddd; padding: 8px;">{comparative["sentiment"].get(model1_name, "N/A").title()}</td>
64
+ <td style="border: 1px solid #ddd; padding: 8px;">{comparative["sentiment"].get(model2_name, "N/A").title()}</td>
65
+ <td style="border: 1px solid #ddd; padding: 8px; font-weight: bold; color: {'red' if sent_sig else 'green'}">{"Yes" if sent_sig else "No"}</td>
66
+ </tr>
67
+ """
68
+
69
+ # Partisan row
70
+ if "partisan" in comparative:
71
+ part_sig = comparative["partisan"].get("significant", False)
72
+ summary_html += f"""
73
+ <tr>
74
+ <td style="border: 1px solid #ddd; padding: 8px;">Partisan Leaning</td>
75
+ <td style="border: 1px solid #ddd; padding: 8px;">{comparative["partisan"].get(model1_name, "N/A").title()}</td>
76
+ <td style="border: 1px solid #ddd; padding: 8px;">{comparative["partisan"].get(model2_name, "N/A").title()}</td>
77
+ <td style="border: 1px solid #ddd; padding: 8px; font-weight: bold; color: {'red' if part_sig else 'green'}">{"Yes" if part_sig else "No"}</td>
78
+ </tr>
79
+ """
80
+
81
+ # Framing row
82
+ if "framing" in comparative:
83
+ frame_diff = comparative["framing"].get("different_frames", False)
84
+ summary_html += f"""
85
+ <tr>
86
+ <td style="border: 1px solid #ddd; padding: 8px;">Dominant Frame</td>
87
+ <td style="border: 1px solid #ddd; padding: 8px;">{comparative["framing"].get(model1_name, "N/A").title().replace('_', ' ')}</td>
88
+ <td style="border: 1px solid #ddd; padding: 8px;">{comparative["framing"].get(model2_name, "N/A").title().replace('_', ' ')}</td>
89
+ <td style="border: 1px solid #ddd; padding: 8px; font-weight: bold; color: {'red' if frame_diff else 'green'}">{"Yes" if frame_diff else "No"}</td>
90
+ </tr>
91
+ """
92
+
93
+ # Overall row
94
+ if "overall" in comparative:
95
+ overall_sig = comparative["overall"].get("significant_bias_difference", False)
96
+ summary_html += f"""
97
+ <tr>
98
+ <td style="border: 1px solid #ddd; padding: 8px; font-weight: bold;">Overall Bias Difference</td>
99
+ <td colspan="2" style="border: 1px solid #ddd; padding: 8px; text-align: center;">{comparative["overall"].get("difference", 0):.2f} / 1.0</td>
100
+ <td style="border: 1px solid #ddd; padding: 8px; font-weight: bold; color: {'red' if overall_sig else 'green'}">{"Yes" if overall_sig else "No"}</td>
101
+ </tr>
102
+ """
103
+
104
+ summary_html += "</table>"
105
+
106
+ # Add the HTML table to the components
107
+ output_components.append(gr.HTML(summary_html))
108
+
109
+ # Create detailed visualizations for each model if available
110
+ for model_name in [model1_name, model2_name]:
111
+ if model_name in bias_results:
112
+ model_data = bias_results[model_name]
113
+
114
+ # Sentiment visualization
115
+ if "sentiment" in model_data:
116
+ sentiment = model_data["sentiment"]
117
+ if "sentiment_scores" in sentiment:
118
+ # Create sentiment score chart
119
+ sentiment_df = pd.DataFrame({
120
+ 'Score': [
121
+ sentiment["sentiment_scores"]["pos"],
122
+ sentiment["sentiment_scores"]["neg"],
123
+ sentiment["sentiment_scores"]["neu"]
124
+ ],
125
+ 'Category': ['Positive', 'Negative', 'Neutral']
126
+ })
127
+
128
+ fig = px.bar(
129
+ sentiment_df,
130
+ x='Category',
131
+ y='Score',
132
+ title=f"Sentiment Analysis for {model_name}",
133
+ height=300
134
+ )
135
+
136
+ output_components.append(gr.Plot(value=fig))
137
+
138
+ # Partisan leaning visualization
139
+ if "partisan" in model_data:
140
+ partisan = model_data["partisan"]
141
+ if "liberal_count" in partisan and "conservative_count" in partisan:
142
+ # Create partisan terms chart
143
+ partisan_df = pd.DataFrame({
144
+ 'Count': [partisan["liberal_count"], partisan["conservative_count"]],
145
+ 'Category': ['Liberal Terms', 'Conservative Terms']
146
+ })
147
+
148
+ fig = px.bar(
149
+ partisan_df,
150
+ x='Category',
151
+ y='Count',
152
+ title=f"Partisan Term Usage for {model_name}",
153
+ color='Category',
154
+ color_discrete_map={
155
+ 'Liberal Terms': 'blue',
156
+ 'Conservative Terms': 'red'
157
+ },
158
+ height=300
159
+ )
160
+
161
+ output_components.append(gr.Plot(value=fig))
162
+
163
+ # Show example partisan terms
164
+ if "liberal_terms" in partisan or "conservative_terms" in partisan:
165
+ lib_terms = ", ".join(partisan.get("liberal_terms", []))
166
+ con_terms = ", ".join(partisan.get("conservative_terms", []))
167
+
168
+ if lib_terms or con_terms:
169
+ terms_md = f"**Partisan Terms Used by {model_name}**\n\n"
170
+ if lib_terms:
171
+ terms_md += f"- Liberal terms: {lib_terms}\n"
172
+ if con_terms:
173
+ terms_md += f"- Conservative terms: {con_terms}\n"
174
+
175
+ output_components.append(gr.Markdown(terms_md))
176
+
177
+ # Framing visualization
178
+ if "framing" in model_data:
179
+ framing = model_data["framing"]
180
+ if "framing_distribution" in framing:
181
+ # Create framing distribution chart
182
+ frame_items = []
183
+ for frame, value in framing["framing_distribution"].items():
184
+ frame_items.append({
185
+ 'Frame': frame.replace('_', ' ').title(),
186
+ 'Proportion': value
187
+ })
188
+
189
+ frame_df = pd.DataFrame(frame_items)
190
+
191
+ fig = px.pie(
192
+ frame_df,
193
+ values='Proportion',
194
+ names='Frame',
195
+ title=f"Issue Framing Distribution for {model_name}",
196
+ height=400
197
+ )
198
+
199
+ output_components.append(gr.Plot(value=fig))
200
+
201
+ # Show example framing terms
202
+ if "framing_examples" in framing:
203
+ examples_md = f"**Example Framing Terms Used by {model_name}**\n\n"
204
+ for frame, examples in framing["framing_examples"].items():
205
+ if examples:
206
+ examples_md += f"- {frame.replace('_', ' ').title()}: {', '.join(examples)}\n"
207
+
208
+ output_components.append(gr.Markdown(examples_md))
209
+
210
+ # If no components were added, show a message
211
+ if len(output_components) <= 1:
212
+ output_components.append(gr.Markdown("No detailed bias detection analysis found in results."))
213
+
214
+ return output_components
215
+
216
+ def process_and_visualize_bias_analysis(analysis_results):
217
+ """
218
+ Process the bias detection analysis results and create visualization components
219
+
220
+ Args:
221
+ analysis_results (dict): The analysis results
222
+
223
+ Returns:
224
+ list: List of gradio components for visualization
225
+ """
226
+ try:
227
+ print(f"Starting visualization of bias detection analysis results")
228
+ return create_bias_visualization(analysis_results)
229
+ except Exception as e:
230
+ import traceback
231
+ error_msg = f"Bias detection visualization error: {str(e)}\n{traceback.format_exc()}"
232
+ print(error_msg)
233
+ return [gr.Markdown(f"**Error during bias detection visualization:**\n\n```\n{error_msg}\n```")]
visualization/topic_visualizer.py CHANGED
@@ -1,18 +1,16 @@
1
  """
2
- Visualization for topic modeling analysis results
3
  """
4
- from visualization.ngram_visualizer import create_ngram_visualization
5
  import gradio as gr
6
- import json
7
- import numpy as np
8
  import pandas as pd
9
  import plotly.express as px
10
  import plotly.graph_objects as go
11
  from plotly.subplots import make_subplots
 
12
 
13
  def create_topic_visualization(analysis_results):
14
  """
15
- Create visualizations for topic modeling analysis results
16
 
17
  Args:
18
  analysis_results (dict): Analysis results from the topic modeling analysis
@@ -33,27 +31,127 @@ def create_topic_visualization(analysis_results):
33
  if "topic_modeling" in analyses:
34
  topic_results = analyses["topic_modeling"]
35
 
 
 
 
 
 
 
 
36
  # Show method and number of topics
37
  method = topic_results.get("method", "lda").upper()
38
  n_topics = topic_results.get("n_topics", 3)
39
- output_components.append(gr.Markdown(f"## Topic Modeling Analysis ({method}, {n_topics} topics)"))
 
 
 
 
 
 
 
 
 
40
 
41
  # Show models being compared
42
  models = topic_results.get("models", [])
43
  if len(models) >= 2:
44
  output_components.append(gr.Markdown(f"### Comparing responses from {models[0]} and {models[1]}"))
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  # Visualize topics
47
  topics = topic_results.get("topics", [])
48
  if topics:
49
  output_components.append(gr.Markdown("### Discovered Topics"))
50
 
 
51
  for topic in topics:
52
  topic_id = topic.get("id", 0)
53
  words = topic.get("words", [])
54
  weights = topic.get("weights", [])
55
 
56
- # Create topic word bar chart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  if words and weights and len(words) == len(weights):
58
  # Create dataframe for plotting
59
  df = pd.DataFrame({
@@ -64,12 +162,22 @@ def create_topic_visualization(analysis_results):
64
  # Sort by weight
65
  df = df.sort_values('weight', ascending=False)
66
 
 
 
 
67
  # Create bar chart
68
  fig = px.bar(
69
- df, x='word', y='weight',
70
  title=f"Topic {topic_id+1} Top Words",
71
  labels={'word': 'Word', 'weight': 'Weight'},
72
- height=300
 
 
 
 
 
 
 
73
  )
74
 
75
  output_components.append(gr.Plot(value=fig))
@@ -80,66 +188,135 @@ def create_topic_visualization(analysis_results):
80
  output_components.append(gr.Markdown("### Topic Distribution by Model"))
81
 
82
  # Create multi-model topic distribution comparison
83
- fig = go.Figure()
84
  for model in models:
85
  if model in model_topics:
86
  distribution = model_topics[model]
87
- fig.add_trace(go.Bar(
88
- x=[f"Topic {i+1}" for i in range(len(distribution))],
89
- y=distribution,
90
- name=model
91
- ))
92
-
93
- fig.update_layout(
94
- title="Topic Distributions Comparison",
95
- xaxis_title="Topic",
96
- yaxis_title="Weight",
97
- barmode='group',
98
- height=400
99
- )
100
 
101
- output_components.append(gr.Plot(value=fig))
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- # Visualize topic differences
104
  comparisons = topic_results.get("comparisons", {})
105
  if comparisons:
106
- output_components.append(gr.Markdown("### Topic Distribution Differences"))
107
-
108
- for comparison_key, comparison_data in comparisons.items():
109
- js_divergence = comparison_data.get("js_divergence", 0)
110
- topic_differences = comparison_data.get("topic_differences", [])
 
111
 
112
- output_components.append(gr.Markdown(
113
- f"**{comparison_key}** - Jensen-Shannon Divergence: {js_divergence:.4f}"
114
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- if topic_differences:
117
- # Create DataFrame for plotting
118
- model1, model2 = comparison_key.split(" vs ")
119
- df_diff = pd.DataFrame(topic_differences)
 
 
 
120
 
121
- # Create bar chart for topic differences
122
- fig = go.Figure()
123
- fig.add_trace(go.Bar(
124
- x=[f"Topic {d['topic_id']+1}" for d in topic_differences],
125
- y=[d["model1_weight"] for d in topic_differences],
126
- name=model1
127
- ))
128
- fig.add_trace(go.Bar(
129
- x=[f"Topic {d['topic_id']+1}" for d in topic_differences],
130
- y=[d["model2_weight"] for d in topic_differences],
131
- name=model2
132
  ))
133
 
134
  fig.update_layout(
135
- title="Topic Weight Comparison",
136
- xaxis_title="Topic",
137
- yaxis_title="Weight",
138
- barmode='group',
139
- height=400
140
  )
141
 
142
  output_components.append(gr.Plot(value=fig))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
  # If no components were added, show a message
145
  if len(output_components) <= 1:
@@ -160,9 +337,15 @@ def process_and_visualize_topic_analysis(analysis_results):
160
  """
161
  try:
162
  print(f"Starting visualization of topic modeling analysis results")
163
- return create_topic_visualization(analysis_results)
 
 
164
  except Exception as e:
165
  import traceback
166
  error_msg = f"Topic modeling visualization error: {str(e)}\n{traceback.format_exc()}"
167
  print(error_msg)
168
- return [gr.Markdown(f"**Error during topic modeling visualization:**\n\n```\n{error_msg}\n```")]
 
 
 
 
 
1
  """
2
+ Enhanced visualization for topic modeling analysis results
3
  """
 
4
  import gradio as gr
 
 
5
  import pandas as pd
6
  import plotly.express as px
7
  import plotly.graph_objects as go
8
  from plotly.subplots import make_subplots
9
+ import numpy as np
10
 
11
  def create_topic_visualization(analysis_results):
12
  """
13
+ Create enhanced visualizations for topic modeling analysis results
14
 
15
  Args:
16
  analysis_results (dict): Analysis results from the topic modeling analysis
 
31
  if "topic_modeling" in analyses:
32
  topic_results = analyses["topic_modeling"]
33
 
34
+ # Check for errors first
35
+ if "error" in topic_results:
36
+ output_components.append(gr.Markdown(f"## ⚠️ Topic Modeling Error"))
37
+ output_components.append(gr.Markdown(f"Error: {topic_results['error']}"))
38
+ output_components.append(gr.Markdown("Try adjusting the number of topics or using longer text samples."))
39
+ continue
40
+
41
  # Show method and number of topics
42
  method = topic_results.get("method", "lda").upper()
43
  n_topics = topic_results.get("n_topics", 3)
44
+
45
+ # Check if n_topics was adjusted
46
+ if "adjusted_n_topics" in topic_results and topic_results["adjusted_n_topics"] != topic_results.get("original_n_topics", n_topics):
47
+ output_components.append(gr.Markdown(
48
+ f"## Topic Modeling Analysis ({method}, {topic_results['adjusted_n_topics']} topics) " +
49
+ f"*Adjusted from {topic_results['original_n_topics']} due to limited text content*"
50
+ ))
51
+ n_topics = topic_results["adjusted_n_topics"]
52
+ else:
53
+ output_components.append(gr.Markdown(f"## Topic Modeling Analysis ({method}, {n_topics} topics)"))
54
 
55
  # Show models being compared
56
  models = topic_results.get("models", [])
57
  if len(models) >= 2:
58
  output_components.append(gr.Markdown(f"### Comparing responses from {models[0]} and {models[1]}"))
59
 
60
+ # Show topic quality metrics if available
61
+ if "coherence_scores" in topic_results:
62
+ coherence_html = f"""
63
+ <div style="margin: 20px 0; padding: 15px; background-color: #f8f9fa; border-radius: 5px;">
64
+ <h4 style="margin-top: 0;">Topic Quality Metrics</h4>
65
+ <table style="width: 100%; border-collapse: collapse;">
66
+ <tr>
67
+ <th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">Metric</th>
68
+ <th style="text-align: center; padding: 8px; border-bottom: 1px solid #ddd;">{models[0]}</th>
69
+ <th style="text-align: center; padding: 8px; border-bottom: 1px solid #ddd;">{models[1]}</th>
70
+ <th style="text-align: center; padding: 8px; border-bottom: 1px solid #ddd;">Combined</th>
71
+ </tr>
72
+ <tr>
73
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">Topic Coherence</td>
74
+ <td style="text-align: center; padding: 8px; border-bottom: 1px solid #ddd;">
75
+ {topic_results["coherence_scores"].get(models[0], 0):.2f}
76
+ </td>
77
+ <td style="text-align: center; padding: 8px; border-bottom: 1px solid #ddd;">
78
+ {topic_results["coherence_scores"].get(models[1], 0):.2f}
79
+ </td>
80
+ <td style="text-align: center; padding: 8px; border-bottom: 1px solid #ddd;">
81
+ {topic_results["coherence_scores"].get("combined", 0):.2f}
82
+ </td>
83
+ </tr>
84
+ <tr>
85
+ <td style="padding: 8px;">Topic Diversity</td>
86
+ <td style="text-align: center; padding: 8px;">
87
+ {topic_results["diversity_scores"].get(models[0], 0):.2f}
88
+ </td>
89
+ <td style="text-align: center; padding: 8px;">
90
+ {topic_results["diversity_scores"].get(models[1], 0):.2f}
91
+ </td>
92
+ <td style="text-align: center; padding: 8px;">
93
+ {topic_results["diversity_scores"].get("combined", 0):.2f}
94
+ </td>
95
+ </tr>
96
+ </table>
97
+ <p style="margin-bottom: 0; font-size: 0.9em; color: #666;">
98
+ Higher coherence scores indicate more semantically coherent topics.<br>
99
+ Higher diversity scores indicate less overlap between topics.
100
+ </p>
101
+ </div>
102
+ """
103
+ output_components.append(gr.HTML(coherence_html))
104
+
105
  # Visualize topics
106
  topics = topic_results.get("topics", [])
107
  if topics:
108
  output_components.append(gr.Markdown("### Discovered Topics"))
109
 
110
+ # Create a topic word cloud using HTML/CSS for better visibility
111
  for topic in topics:
112
  topic_id = topic.get("id", 0)
113
  words = topic.get("words", [])
114
  weights = topic.get("weights", [])
115
 
116
+ if words and weights and len(words) == len(weights):
117
+ # Generate a word cloud-like div using HTML/CSS
118
+ word_cloud_html = f"""
119
+ <div style="margin-bottom: 25px;">
120
+ <h4 style="margin-bottom: 10px;">Topic {topic_id+1}</h4>
121
+ <div style="display: flex; flex-wrap: wrap; gap: 10px; background: #f9f9f9; padding: 15px; border-radius: 5px;">
122
+ """
123
+
124
+ # Sort words by weight for better visualization
125
+ word_weight_pairs = sorted(zip(words, weights), key=lambda x: x[1], reverse=True)
126
+
127
+ # Add each word with size based on weight
128
+ for word, weight in word_weight_pairs:
129
+ # Scale weight to a reasonable font size (min 14px, max 28px)
130
+ font_size = 14 + min(14, round(weight * 30))
131
+ # Color based on weight (darker = higher weight)
132
+ color_intensity = max(0, min(90, int(100 - weight * 100)))
133
+ color = f"hsl(210, 70%, {color_intensity}%)"
134
+
135
+ word_cloud_html += f"""
136
+ <span style="font-size: {font_size}px; color: {color}; margin: 3px;
137
+ padding: 5px; border-radius: 3px; background: rgba(0,0,0,0.03);">
138
+ {word}
139
+ </span>
140
+ """
141
+
142
+ word_cloud_html += """
143
+ </div>
144
+ </div>
145
+ """
146
+
147
+ output_components.append(gr.HTML(word_cloud_html))
148
+
149
+ # Add a proper bar chart visualization for topic words
150
+ for topic in topics[:min(3, len(topics))]: # Show charts for max 3 topics to avoid clutter
151
+ topic_id = topic.get("id", 0)
152
+ words = topic.get("words", [])
153
+ weights = topic.get("weights", [])
154
+
155
  if words and weights and len(words) == len(weights):
156
  # Create dataframe for plotting
157
  df = pd.DataFrame({
 
162
  # Sort by weight
163
  df = df.sort_values('weight', ascending=False)
164
 
165
+ # Limit to top N words for clarity
166
+ df = df.head(10)
167
+
168
  # Create bar chart
169
  fig = px.bar(
170
+ df, x='weight', y='word',
171
  title=f"Topic {topic_id+1} Top Words",
172
  labels={'word': 'Word', 'weight': 'Weight'},
173
+ height=300,
174
+ orientation='h' # Horizontal bars
175
+ )
176
+
177
+ # Improve layout
178
+ fig.update_layout(
179
+ margin=dict(l=10, r=10, t=40, b=10),
180
+ yaxis={'categoryorder': 'total ascending'}
181
  )
182
 
183
  output_components.append(gr.Plot(value=fig))
 
188
  output_components.append(gr.Markdown("### Topic Distribution by Model"))
189
 
190
  # Create multi-model topic distribution comparison
191
+ distribution_data = []
192
  for model in models:
193
  if model in model_topics:
194
  distribution = model_topics[model]
195
+ for i, weight in enumerate(distribution):
196
+ distribution_data.append({
197
+ 'Model': model,
198
+ 'Topic': f"Topic {i+1}",
199
+ 'Weight': weight
200
+ })
 
 
 
 
 
 
 
201
 
202
+ if distribution_data:
203
+ df = pd.DataFrame(distribution_data)
204
+
205
+ # Create grouped bar chart
206
+ fig = px.bar(
207
+ df, x='Topic', y='Weight', color='Model',
208
+ barmode='group',
209
+ title="Topic Distribution Comparison",
210
+ height=400
211
+ )
212
+
213
+ output_components.append(gr.Plot(value=fig))
214
 
215
+ # Visualize topic differences as a heatmap
216
  comparisons = topic_results.get("comparisons", {})
217
  if comparisons:
218
+ comparison_key = f"{models[0]} vs {models[1]}"
219
+ if comparison_key in comparisons:
220
+ output_components.append(gr.Markdown("### Topic Similarity Analysis"))
221
+
222
+ # Get JS divergence
223
+ js_divergence = comparisons[comparison_key].get("js_divergence", 0)
224
 
225
+ # Create a divergence meter
226
+ divergence_html = f"""
227
+ <div style="margin: 20px 0; padding: 20px; background-color: #f8f9fa; border-radius: 5px; text-align: center;">
228
+ <h4 style="margin-top: 0;">Topic Distribution Divergence</h4>
229
+ <div style="display: flex; align-items: center; justify-content: center;">
230
+ <div style="width: 300px; height: 40px; background: linear-gradient(to right, #1a9850, #ffffbf, #d73027); border-radius: 5px; position: relative; margin: 10px 0;">
231
+ <div style="position: absolute; height: 40px; width: 2px; background-color: #000; left: {min(300, max(0, js_divergence * 300))}px;"></div>
232
+ </div>
233
+ </div>
234
+ <div style="display: flex; justify-content: space-between; width: 300px; margin: 0 auto;">
235
+ <span>Similar (0.0)</span>
236
+ <span>Different (1.0)</span>
237
+ </div>
238
+ <p style="margin-top: 10px; font-weight: bold;">Score: {js_divergence:.3f}</p>
239
+ <p style="margin-bottom: 0; font-size: 0.9em; color: #666;">
240
+ Jensen-Shannon Divergence measures the similarity between topic distributions.<br>
241
+ Lower values indicate more similar topic distributions between models.
242
+ </p>
243
+ </div>
244
+ """
245
 
246
+ output_components.append(gr.HTML(divergence_html))
247
+
248
+ # Create similarity matrix heatmap if available
249
+ similarity_matrix = topic_results.get("similarity_matrix", [])
250
+ if similarity_matrix and len(similarity_matrix) > 0:
251
+ # Convert to format for heatmap
252
+ z_data = similarity_matrix
253
 
254
+ # Create heatmap
255
+ fig = go.Figure(data=go.Heatmap(
256
+ z=z_data,
257
+ x=[f"{models[1]} Topic {i+1}" for i in range(len(similarity_matrix[0]))],
258
+ y=[f"{models[0]} Topic {i+1}" for i in range(len(similarity_matrix))],
259
+ colorscale='Viridis',
260
+ showscale=True,
261
+ colorbar=dict(title="Similarity")
 
 
 
262
  ))
263
 
264
  fig.update_layout(
265
+ title="Topic Similarity Matrix",
266
+ height=400,
267
+ margin=dict(l=50, r=50, t=50, b=50)
 
 
268
  )
269
 
270
  output_components.append(gr.Plot(value=fig))
271
+
272
+ # Show best matching topics
273
+ matched_topics = topic_results.get("matched_topics", [])
274
+ if matched_topics:
275
+ output_components.append(gr.Markdown("### Most Similar Topic Pairs"))
276
+
277
+ # Create HTML table for matched topics
278
+ matched_topics_html = """
279
+ <div style="margin: 20px 0;">
280
+ <table style="width: 100%; border-collapse: collapse;">
281
+ <tr>
282
+ <th style="padding: 8px; border-bottom: 2px solid #ddd; text-align: left;">Topic Pair</th>
283
+ <th style="padding: 8px; border-bottom: 2px solid #ddd; text-align: left;">Top Words in Model 1</th>
284
+ <th style="padding: 8px; border-bottom: 2px solid #ddd; text-align: left;">Top Words in Model 2</th>
285
+ <th style="padding: 8px; border-bottom: 2px solid #ddd; text-align: center;">Similarity</th>
286
+ </tr>
287
+ """
288
+
289
+ # Sort by similarity, highest first
290
+ sorted_matches = sorted(matched_topics, key=lambda x: x['similarity'], reverse=True)
291
+
292
+ for match in sorted_matches:
293
+ # Format words with commas
294
+ words1 = ", ".join(match["set1_topic_words"][:5]) # Show top 5 words
295
+ words2 = ", ".join(match["set2_topic_words"][:5]) # Show top 5 words
296
+
297
+ # Calculate color based on similarity (green for high, red for low)
298
+ similarity = match["similarity"]
299
+ color = f"hsl({int(120 * similarity)}, 70%, 50%)"
300
+
301
+ matched_topics_html += f"""
302
+ <tr>
303
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">
304
+ {models[0]} Topic {match['set1_topic_id']+1} ↔ {models[1]} Topic {match['set2_topic_id']+1}
305
+ </td>
306
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{words1}</td>
307
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{words2}</td>
308
+ <td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: center; font-weight: bold; color: {color};">
309
+ {similarity:.2f}
310
+ </td>
311
+ </tr>
312
+ """
313
+
314
+ matched_topics_html += """
315
+ </table>
316
+ </div>
317
+ """
318
+
319
+ output_components.append(gr.HTML(matched_topics_html))
320
 
321
  # If no components were added, show a message
322
  if len(output_components) <= 1:
 
337
  """
338
  try:
339
  print(f"Starting visualization of topic modeling analysis results")
340
+ components = create_topic_visualization(analysis_results)
341
+ print(f"Completed topic modeling visualization with {len(components)} components")
342
+ return components
343
  except Exception as e:
344
  import traceback
345
  error_msg = f"Topic modeling visualization error: {str(e)}\n{traceback.format_exc()}"
346
  print(error_msg)
347
+ return [
348
+ gr.Markdown(f"**Error during topic modeling visualization:**"),
349
+ gr.Markdown(f"```\n{str(e)}\n```"),
350
+ gr.Markdown("Try adjusting the number of topics or using longer text inputs.")
351
+ ]