pvaluedotone commited on
Commit
c9b15b4
Β·
verified Β·
1 Parent(s): 69c0bd3

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +224 -0
app.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import re
3
+ import torch
4
+ import gradio as gr
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+ from transformers import pipeline
8
+
9
+ cached_df = None
10
+ cached_file_name = None
11
+
12
+ # Load sentiment pipeline
13
+ sentiment_pipeline = pipeline(
14
+ "text-classification",
15
+ model="pvaluedotone/bigbird-flight-2",
16
+ tokenizer="pvaluedotone/bigbird-flight-2",
17
+ device=0 if torch.cuda.is_available() else -1
18
+ )
19
+
20
+ # Contractions dictionary
21
+ contractions_dict = {
22
+ "don't": "do not", "can't": "cannot", "i'm": "i am", "it's": "it is",
23
+ "he's": "he is", "she's": "she is", "they're": "they are", "we're": "we are",
24
+ "you're": "you are", "that's": "that is", "there's": "there is", "what's": "what is",
25
+ "won't": "will not", "isn't": "is not", "aren't": "are not", "wasn't": "was not",
26
+ "weren't": "were not", "didn't": "did not", "doesn't": "does not", "haven't": "have not",
27
+ "hasn't": "has not", "hadn't": "had not", "wouldn't": "would not", "shouldn't": "should not",
28
+ "couldn't": "could not", "mustn't": "must not", "let's": "let us"
29
+ }
30
+ contractions_pattern = re.compile(r"\b(" + "|".join(re.escape(k) for k in contractions_dict.keys()) + r")\b")
31
+
32
+ def expand_contractions(text: str) -> str:
33
+ def replace(match):
34
+ return contractions_dict[match.group(0)]
35
+ return contractions_pattern.sub(replace, text)
36
+
37
+ # Emoticon mapping
38
+ emoticon_dict = {
39
+ ":)": "smile", ":-)": "smile", ":(": "sad", ":-(": "sad",
40
+ ";)": "wink", ";-)": "wink", ":d": "laugh", ":-d": "laugh",
41
+ ":p": "playful", ":-p": "playful", ":'(": "cry", ":/": "skeptical",
42
+ ":'-)": "tears_of_joy"
43
+ }
44
+
45
+ def clean_text(text: str) -> str:
46
+ if not isinstance(text, str):
47
+ return ""
48
+ text = re.sub(r"http\S+|@\w+", "", text)
49
+ text = expand_contractions(text)
50
+ try:
51
+ import emoji
52
+ text = emoji.demojize(text)
53
+ except ImportError:
54
+ pass
55
+ for emoticon, desc in emoticon_dict.items():
56
+ text = text.replace(emoticon, f" {desc} ")
57
+ text = re.sub(r"#(\w+)", r"\1", text)
58
+ text = re.sub(r"\s+", " ", text).strip()
59
+ return text
60
+
61
+ def predict_sentiment(texts):
62
+ results = sentiment_pipeline(texts, truncation=False, batch_size=32)
63
+ sentiments = []
64
+ confidences = []
65
+ for r in results:
66
+ label_num = int(r['label'].split('_')[-1])
67
+ sentiments.append(label_num)
68
+ confidences.append(r['score'])
69
+ return sentiments, confidences
70
+
71
+ def recategorize(labels, mode, pos_threshold, neg_threshold):
72
+ if mode == "Original (1–10)":
73
+ return labels
74
+ elif mode == "Binary (Positive vs Negative)":
75
+ return ["Positive" if lbl >= pos_threshold else "Negative" for lbl in labels]
76
+ elif mode == "Ternary (Pos/Neu/Neg)":
77
+ return [
78
+ "Positive" if lbl >= pos_threshold else
79
+ "Negative" if lbl <= neg_threshold else
80
+ "Neutral" for lbl in labels
81
+ ]
82
+
83
+ def analyze_sentiment(file, text_column, mode, pos_thresh, neg_thresh, auto_fix, apply_cleaning):
84
+ global cached_df, cached_file_name
85
+
86
+ try:
87
+ df = pd.read_csv(file.name)
88
+ except Exception as e:
89
+ return f"Error reading CSV file: {e}", None, None, None, None, None
90
+
91
+ if text_column not in df.columns:
92
+ return "Selected column not found.", None, None, None, None, None
93
+
94
+ if (
95
+ cached_df is not None and
96
+ cached_file_name == file.name and
97
+ "sentiment_1to10" in cached_df.columns and
98
+ "confidence" in cached_df.columns
99
+ ):
100
+ df = cached_df.copy()
101
+ else:
102
+ if apply_cleaning:
103
+ df["processed_text"] = df[text_column].apply(clean_text)
104
+ else:
105
+ df["processed_text"] = df[text_column].astype(str)
106
+
107
+ predictions, confidences = predict_sentiment(df["processed_text"].tolist())
108
+ df["sentiment_1to10"] = predictions
109
+ df["confidence"] = confidences
110
+
111
+ cached_df = df.copy()
112
+ cached_file_name = file.name
113
+
114
+ if mode == "Ternary (Pos/Neu/Neg)":
115
+ if pos_thresh <= neg_thresh:
116
+ if auto_fix:
117
+ neg_thresh = pos_thresh - 1
118
+ if neg_thresh < 1:
119
+ return "⚠️ Unable to auto-correct: thresholds out of valid range (1–10).", None, None, None, None, None
120
+ else:
121
+ return (
122
+ f"⚠️ Invalid thresholds: Positive min ({pos_thresh}) must be greater than Negative max ({neg_thresh}).",
123
+ None, None, None, None, None
124
+ )
125
+
126
+ df["sentiment_recategorised"] = recategorize(df["sentiment_1to10"], mode, pos_thresh, neg_thresh)
127
+
128
+ output_file = "bigbird_sentiment_results.csv"
129
+ df.to_csv(output_file, index=False)
130
+
131
+ if "plot1_path" not in globals():
132
+ plt.figure(figsize=(6, 4))
133
+ sns.countplot(x=df["sentiment_1to10"], palette="Blues")
134
+ plt.title("Original 10-Class Sentiment Distribution")
135
+ plt.tight_layout()
136
+ global plot1_path
137
+ plot1_path = "original_dist.png"
138
+ plt.savefig(plot1_path)
139
+ plt.close()
140
+
141
+ plt.figure(figsize=(6, 4))
142
+ sns.countplot(x=df["sentiment_recategorised"], palette="Set2")
143
+ plt.title(f"Recategorised Sentiment Distribution ({mode})")
144
+ plt.tight_layout()
145
+ plot2_path = "recategorised_dist.png"
146
+ plt.savefig(plot2_path)
147
+ plt.close()
148
+
149
+ if "plot3_path" not in globals():
150
+ plt.figure(figsize=(6, 4))
151
+ sns.histplot(df["confidence"], bins=20, color="skyblue", kde=True)
152
+ plt.title("Confidence Score Distribution")
153
+ plt.xlabel("Confidence")
154
+ plt.tight_layout()
155
+ global plot3_path
156
+ plot3_path = "confidence_dist.png"
157
+ plt.savefig(plot3_path)
158
+ plt.close()
159
+
160
+ preview = df[[text_column, "processed_text", "sentiment_1to10", "confidence", "sentiment_recategorised"]].head(10)
161
+ return f"βœ… Sentiment analysis complete. Used cache: {cached_file_name == file.name}", preview, output_file, plot1_path, plot2_path, plot3_path
162
+
163
+ def get_text_columns(file):
164
+ try:
165
+ df = pd.read_csv(file.name, nrows=1)
166
+ text_columns = df.select_dtypes(include='object').columns.tolist()
167
+ if not text_columns:
168
+ return gr.update(choices=[], value=None, label="⚠️ No text columns found!")
169
+ return gr.update(choices=text_columns, value=text_columns[0])
170
+ except Exception:
171
+ return gr.update(choices=[], value=None, label="⚠️ Error reading file")
172
+
173
+ with gr.Blocks() as app:
174
+ gr.Markdown("## ✈️ Sentiment analysis with `pvaluedotone/bigbird-flight`")
175
+ gr.Markdown("**Citation:** Mat Roni, S. (2025). *Sentiment analysis with Big Bird Flight on Gradio* (version 1.0) [software]. https://huggingface.co/spaces/pvaluedotone/bigbird-flight")
176
+
177
+ with gr.Row():
178
+ file_input = gr.File(label="Upload CSV", file_types=[".csv"])
179
+ column_dropdown = gr.Dropdown(label="Select Text Column", choices=[], interactive=True)
180
+
181
+ file_input.change(get_text_columns, inputs=file_input, outputs=column_dropdown)
182
+
183
+ output_mode = gr.Radio(
184
+ label="Sentiment Output Type",
185
+ choices=["Original (1–10)", "Binary (Positive vs Negative)", "Ternary (Pos/Neu/Neg)"],
186
+ value="Original (1–10)",
187
+ interactive=True
188
+ )
189
+
190
+ pos_thresh_slider = gr.Slider(3, 10, value=7, step=1, label="Positive min", visible=False)
191
+ neg_thresh_slider = gr.Slider(1, 7, value=4, step=1, label="Negative max", visible=False)
192
+ auto_fix_checkbox = gr.Checkbox(label="Auto-correct thresholds if overlapping?", value=True)
193
+ cleaning_checkbox = gr.Checkbox(label="Apply Text Cleaning", value=True) # βœ… New toggle
194
+
195
+ def toggle_thresholds(mode):
196
+ show_pos = mode != "Original (1–10)"
197
+ show_neg = mode == "Ternary (Pos/Neu/Neg)"
198
+ return (
199
+ gr.update(visible=show_pos),
200
+ gr.update(visible=show_neg)
201
+ )
202
+
203
+ output_mode.change(toggle_thresholds, inputs=output_mode, outputs=[pos_thresh_slider, neg_thresh_slider])
204
+
205
+ run_button = gr.Button("Process sentiment")
206
+
207
+ status = gr.Textbox(label="Status")
208
+ df_output = gr.Dataframe(label="Sample Output (Top 10)")
209
+ file_result = gr.File(label="Download Full Results")
210
+ plot_orig = gr.Image(label="Original Sentiment Distribution")
211
+ plot_recat = gr.Image(label="Recategorised Sentiment Distribution")
212
+ plot_conf = gr.Image(label="Confidence Score Distribution")
213
+
214
+ run_button.click(
215
+ analyze_sentiment,
216
+ inputs=[
217
+ file_input, column_dropdown, output_mode,
218
+ pos_thresh_slider, neg_thresh_slider, auto_fix_checkbox,
219
+ cleaning_checkbox # βœ… New input
220
+ ],
221
+ outputs=[status, df_output, file_result, plot_orig, plot_recat, plot_conf]
222
+ )
223
+
224
+ app.launch(share=True, debug=True)