Dusit-P commited on
Commit
4096fb9
·
verified ·
1 Parent(s): f6f7109

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +568 -126
app.py CHANGED
@@ -1,14 +1,10 @@
1
- # app.py — Thai Sentiment (WangchanBERTa Variants)
2
- # - Focus on POS/NEG only
3
- # - Batch + CSV tabs
4
- # - CSV: auto-detect text/date cols, hide date widgets if no date col
5
- # - DatePicker fallback to Textbox if component missing
6
-
7
  import os, json, importlib.util, traceback, re, math, tempfile, datetime
8
  import gradio as gr
9
  import torch, pandas as pd
10
  import torch.nn.functional as F
11
  import plotly.graph_objects as go
 
12
  from huggingface_hub import hf_hub_download
13
  from safetensors.torch import load_file
14
  from transformers import AutoTokenizer
@@ -24,28 +20,42 @@ if DEFAULT_MODEL not in AVAILABLE_CHOICES:
24
 
25
  NEG_COLOR = "#F87171"
26
  POS_COLOR = "#34D399"
 
27
  TEMPLATE = "plotly_white"
28
  CACHE = {}
29
 
30
- # ================= Date Component Fallback =================
31
- try:
32
- DateInput = getattr(gr, "Date", None) or getattr(gr, "DatePicker", None)
33
- except Exception:
34
- DateInput = None
35
- DATE_FALLBACK_TO_TEXT = False
36
- if DateInput is None:
37
- DateInput = gr.Textbox
38
- DATE_FALLBACK_TO_TEXT = True
39
 
40
- def _normalize_date_input(v):
41
- if v is None: return None
42
- if isinstance(v, float) and math.isnan(v): return None
43
- if isinstance(v, datetime.date): return pd.Timestamp(v)
44
- try:
45
- ts = pd.to_datetime(v, errors="coerce")
46
- return ts if pd.notna(ts) else None
47
- except Exception:
48
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  # ================= Loader =================
51
  def _import_models():
@@ -99,65 +109,226 @@ def _to_datetime_safe(s): return pd.to_datetime(s, errors="coerce", infer_dateti
99
 
100
  LIKELY_TEXT_COLS = ["text","review","message","comment","content","sentence","body","ข้อความ","รีวิว"]
101
  LIKELY_DATE_COLS = ["date","created_at","time","timestamp","datetime","วันที่","วันเวลา","เวลา"]
 
102
 
103
- def detect_text_and_date_cols(df):
 
104
  cols = list(df.columns)
105
  low = {c.lower(): c for c in cols}
 
 
106
  text_col = None
107
  for k in LIKELY_TEXT_COLS:
108
  if k in low: text_col = low[k]; break
109
  if text_col is None:
110
  cand = [c for c in cols if df[c].dtype == object]
111
  text_col = cand[0] if cand else cols[0]
 
 
112
  date_candidates = []
113
  for c in cols:
114
- if c.lower() in LIKELY_DATE_COLS: date_candidates.append(c); continue
 
 
115
  sample = df[c].head(50)
116
  if _to_datetime_safe(sample).notna().sum() >= max(3, int(len(sample)*0.2)):
117
  date_candidates.append(c)
118
  date_candidates = list(dict.fromkeys(date_candidates))
119
- date_col = date_candidates[0] if len(date_candidates)>0 else None
120
- return text_col, date_candidates, date_col
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  # ================= Charts =================
123
- def make_basic_charts(df):
 
124
  total = len(df)
125
- neg_df = df[df["label"]=="negative"]; pos_df = df[df["label"]=="positive"]
126
- fig_bar = go.Figure()
127
- fig_bar.add_bar(name="negative", x=["negative"], y=[len(neg_df)], marker_color=NEG_COLOR)
128
- fig_bar.add_bar(name="positive", x=["positive"], y=[len(pos_df)], marker_color=POS_COLOR)
129
- fig_bar.update_layout(barmode="group", title="Label counts", template=TEMPLATE)
130
- labels=["negative","positive"]; values=[len(neg_df), len(pos_df)]
131
- fig_pie = go.Figure(go.Pie(labels=labels, values=values, hole=0.35,
132
- marker=dict(colors=[NEG_COLOR, POS_COLOR])))
133
- fig_pie.update_layout(title="Positive vs Negative", template=TEMPLATE)
134
  neg_avg = pd.to_numeric(df["negative(%)"].str.rstrip("%"), errors="coerce").mean()
135
  pos_avg = pd.to_numeric(df["positive(%)"].str.rstrip("%"), errors="coerce").mean()
136
- info=(f"**Summary**\n- Total: {total}\n- Negative: {len(neg_df)}\n- Positive: {len(pos_df)}\n"
137
- f"- Avg negative: {neg_avg:.2f}%\n- Avg positive: {pos_avg:.2f}%")
138
- return fig_bar, fig_pie, info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  def _resample_counts(df, date_col, freq):
 
141
  g = df.groupby([pd.Grouper(key=date_col, freq=freq),"label"]).size().unstack(fill_value=0)
142
  for c in ["negative","positive"]:
143
  if c not in g.columns: g[c]=0
144
  return g[["negative","positive"]].sort_index()
145
 
146
- def _rolling_window(freq): return 7 if freq=="D" else (4 if freq=="W" else 3)
147
-
148
- def make_time_chart(df, date_col, freq, use_ma):
149
- ts=_resample_counts(df,date_col,freq)
150
- if use_ma: ts=ts.rolling(_rolling_window(freq), min_periods=1).mean()
151
- fig=go.Figure()
152
- fig.add_scatter(x=ts.index,y=ts["negative"],mode="lines",name="negative",line=dict(color=NEG_COLOR))
153
- fig.add_scatter(x=ts.index,y=ts["positive"],mode="lines",name="positive",line=dict(color=POS_COLOR))
154
- fig.update_layout(title="Reviews over time (POS/NEG)",template=TEMPLATE,
155
- xaxis_title="Date",yaxis_title="Count")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  return fig
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  # ================= Core Predict =================
159
  def _predict_batch(texts, model_name, batch_size=32):
160
- model,tok,cfg=load_model(model_name); results=[]
 
161
  for i in range(0,len(texts),batch_size):
162
  chunk=texts[i:i+batch_size]
163
  enc=tok(chunk,padding=True,truncation=True,
@@ -168,94 +339,365 @@ def _predict_batch(texts, model_name, batch_size=32):
168
  for txt,p in zip(chunk,probs):
169
  neg,pos=float(p[0]),float(p[1])
170
  label="positive" if pos>=neg else "negative"
171
- results.append({"review":txt,"negative(%)":_format_pct(neg),
172
- "positive(%)":_format_pct(pos),"label":label})
 
 
 
 
173
  return results
174
 
175
- # ================= Batch =================
176
- def predict_many(text_block, model_choice):
177
  try:
178
- raw=(text_block or "").splitlines()
179
- norm=[_norm_text(t) for t in raw]; clean=[t for t in norm if _is_substantive_text(t)]
180
- if not clean: return pd.DataFrame(),go.Figure(),go.Figure(),"No valid text"
181
- results=_predict_batch(clean,model_choice); df=pd.DataFrame(results)
182
- bar,pie,info=make_basic_charts(df)
183
- return df,bar,pie,info
184
- except: return pd.DataFrame(),go.Figure(),go.Figure(),traceback.format_exc()
 
 
 
 
 
 
 
 
 
185
 
186
- # ================= CSV Inspect =================
187
  def on_file_change(file_obj):
 
188
  if file_obj is None:
189
- return gr.update(choices=[],value=None),gr.update(choices=[],value=None),\
190
- gr.update(visible=False),gr.update(visible=False),\
191
- gr.update(visible=False),gr.update(visible=False),\
192
- gr.update(visible=False),"Please upload a CSV"
 
 
 
 
 
193
  try:
194
- df=pd.read_csv(file_obj.name)
195
- text_col,date_candidates,date_col=detect_text_and_date_cols(df)
196
- has_date=date_col is not None
197
- note=f"Detected text col: **{text_col}**; "+("date col: **{}**".format(date_col) if has_date else "_no date col_")
198
- return gr.update(choices=list(df.columns),value=text_col),\
199
- gr.update(choices=date_candidates,value=date_col),\
200
- gr.update(visible=has_date),gr.update(visible=has_date),\
201
- gr.update(visible=has_date),gr.update(visible=has_date),\
202
- gr.update(visible=has_date),note
203
- except: return gr.update(choices=[],value=None),gr.update(choices=[],value=None),\
204
- gr.update(visible=False),gr.update(visible=False),\
205
- gr.update(visible=False),gr.update(visible=False),\
206
- gr.update(visible=False),"Error reading CSV"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
- # ================= CSV Predict =================
209
- def predict_csv(file_obj,model_choice,text_col,date_col,date_from,date_to,freq,use_ma):
210
- if file_obj is None: return pd.DataFrame(),go.Figure(),go.Figure(),gr.update(visible=False), "No file",None
 
 
 
 
 
 
211
  try:
212
- df_raw=pd.read_csv(file_obj.name); cols=list(df_raw.columns)
213
- if text_col not in cols: text_col,_d,_dc=detect_text_and_date_cols(df_raw);
214
- texts=[_norm_text(v) for v in df_raw[text_col].tolist()]
215
- texts=[t for t in texts if _is_substantive_text(t)]
216
- if not texts: return pd.DataFrame(),go.Figure(),go.Figure(),gr.update(visible=False),"No valid texts",None
217
- results=_predict_batch(texts,model_choice); out=pd.DataFrame(results)
218
- bar,pie,info=make_basic_charts(out)
219
- fig_line=go.Figure(); show_time=False
220
- if date_col and date_col in cols:
221
- dts=_to_datetime_safe(df_raw[date_col])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  if dts.notna().any():
223
- df_time=out.copy(); df_time["__dt__"]=dts; df_time=df_time.dropna(subset=["__dt__"])
224
- start_ts=_normalize_date_input(date_from); end_ts=_normalize_date_input(date_to)
225
- if start_ts is not None: df_time=df_time[df_time["__dt__"]>=start_ts]
226
- if end_ts is not None: df_time=df_time[df_time["__dt__"]<=end_ts]
227
- if len(df_time)>0: fig_line=make_time_chart(df_time,"__dt__",freq,use_ma); show_time=True
228
- fd,path=tempfile.mkstemp(suffix=".csv"); os.close(fd)
229
- out.to_csv(path,index=False,encoding="utf-8-sig")
230
- return out,bar,pie,gr.update(visible=show_time,value=fig_line),info,path
231
- except: return pd.DataFrame(),go.Figure(),go.Figure(),gr.update(visible=False),"Error\n"+traceback.format_exc(),None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
  # ================= Gradio UI =================
234
- with gr.Blocks(title="Thai Sentiment") as demo:
235
- gr.Markdown("### Thai Sentiment — WangchanBERTa Variants")
236
- model_radio=gr.Radio(choices=AVAILABLE_CHOICES,value=DEFAULT_MODEL,label="เลือกโมเดล")
 
 
 
 
 
 
 
 
 
237
 
238
- with gr.Tab("Batch"):
239
- t2=gr.Textbox(lines=8,label="รีวิว (บรรทัดละ 1)")
240
- btn2=gr.Button("Predict",variant="primary")
241
- df2=gr.Dataframe(); bar2=gr.Plot(); pie2=gr.Plot(); sum2=gr.Markdown()
242
- btn2.click(predict_many,[t2,model_radio],[df2,bar2,pie2,sum2])
243
-
244
- with gr.Tab("CSV Upload"):
 
 
 
 
 
 
 
 
 
 
 
 
245
  with gr.Row():
246
- file_in=gr.File(file_types=[".csv"]); text_dd=gr.Dropdown(label="Text col")
247
- date_dd=gr.Dropdown(label="Date col (opt)")
 
 
 
 
 
 
 
 
248
  with gr.Row():
249
- date_from=DateInput(label="เริ่มวันที่"+(" (YYYY-MM-DD)" if DATE_FALLBACK_TO_TEXT else ""),visible=False)
250
- date_to=DateInput(label="ถึงวันที่"+(" (YYYY-MM-DD)" if DATE_FALLBACK_TO_TEXT else ""),visible=False)
251
- freq=gr.Radio(choices=["D","W","M"],value="D",label="Freq",visible=False)
252
- use_ma=gr.Checkbox(value=True,label="MA",visible=False)
253
- btn3=gr.Button("Predict CSV",variant="primary")
254
- note=gr.Markdown()
255
- df3=gr.Dataframe(); bar3=gr.Plot(); pie3=gr.Plot()
256
- line=gr.Plot(visible=False); sum3=gr.Markdown(); dl=gr.File()
 
 
257
 
258
- file_in.change(on_file_change,[file_in],[text_dd,date_dd,date_from,date_to,freq,use_ma,line,note])
259
- btn3.click(predict_csv,[file_in,model_radio,text_dd,date_dd,date_from,date_to,freq,use_ma],[df3,bar3,pie3,line,sum3,dl])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- if __name__=="__main__": demo.launch()
 
 
1
+ # app.py — Thai Sentiment (WangchanBERTa Variants) - ปรับปรุง UI และเพิ่ม Shop Analysis
 
 
 
 
 
2
  import os, json, importlib.util, traceback, re, math, tempfile, datetime
3
  import gradio as gr
4
  import torch, pandas as pd
5
  import torch.nn.functional as F
6
  import plotly.graph_objects as go
7
+ from plotly.subplots import make_subplots
8
  from huggingface_hub import hf_hub_download
9
  from safetensors.torch import load_file
10
  from transformers import AutoTokenizer
 
20
 
21
  NEG_COLOR = "#F87171"
22
  POS_COLOR = "#34D399"
23
+ NEUTRAL_COLOR = "#94A3B8"
24
  TEMPLATE = "plotly_white"
25
  CACHE = {}
26
 
27
+ # ================= Date Presets =================
28
+ DATE_PRESETS = {
29
+ "ทั้งหมด": None,
30
+ "7 วันล่าสุด": 7,
31
+ "30 วันล่าสุด": 30,
32
+ "90 วันล่าสุด": 90,
33
+ "เดือนนี้": "current_month",
34
+ "เดือนที่แล้ว": "last_month"
35
+ }
36
 
37
+ def apply_date_preset(df, date_col, preset_key):
38
+ """กรองข้อมูลตาม preset ที่เลือก"""
39
+ if preset_key == "ทั้งหมด":
40
+ return df
41
+
42
+ now = pd.Timestamp.now()
43
+
44
+ if isinstance(DATE_PRESETS[preset_key], int):
45
+ days = DATE_PRESETS[preset_key]
46
+ cutoff = now - pd.Timedelta(days=days)
47
+ return df[df[date_col] >= cutoff]
48
+
49
+ elif DATE_PRESETS[preset_key] == "current_month":
50
+ start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
51
+ return df[df[date_col] >= start]
52
+
53
+ elif DATE_PRESETS[preset_key] == "last_month":
54
+ end_last = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
55
+ start_last = (end_last - pd.Timedelta(days=1)).replace(day=1)
56
+ return df[(df[date_col] >= start_last) & (df[date_col] < end_last)]
57
+
58
+ return df
59
 
60
  # ================= Loader =================
61
  def _import_models():
 
109
 
110
  LIKELY_TEXT_COLS = ["text","review","message","comment","content","sentence","body","ข้อความ","รีวิว"]
111
  LIKELY_DATE_COLS = ["date","created_at","time","timestamp","datetime","วันที่","วันเวลา","เวลา"]
112
+ LIKELY_SHOP_COLS = ["shop","store","branch","ร้าน","สาขา","ชื่อร้าน"]
113
 
114
+ def detect_columns(df):
115
+ """ตรวจหา text, date, shop columns อัตโนมัติ"""
116
  cols = list(df.columns)
117
  low = {c.lower(): c for c in cols}
118
+
119
+ # Text column
120
  text_col = None
121
  for k in LIKELY_TEXT_COLS:
122
  if k in low: text_col = low[k]; break
123
  if text_col is None:
124
  cand = [c for c in cols if df[c].dtype == object]
125
  text_col = cand[0] if cand else cols[0]
126
+
127
+ # Date candidates
128
  date_candidates = []
129
  for c in cols:
130
+ if c.lower() in LIKELY_DATE_COLS:
131
+ date_candidates.append(c)
132
+ continue
133
  sample = df[c].head(50)
134
  if _to_datetime_safe(sample).notna().sum() >= max(3, int(len(sample)*0.2)):
135
  date_candidates.append(c)
136
  date_candidates = list(dict.fromkeys(date_candidates))
137
+ date_col = date_candidates[0] if len(date_candidates) > 0 else None
138
+
139
+ # Shop candidates
140
+ shop_candidates = []
141
+ for c in cols:
142
+ if c.lower() in LIKELY_SHOP_COLS:
143
+ shop_candidates.append(c)
144
+ continue
145
+ # ตรวจว่ามีค่าซ้ำพอสมควร (เหมือนเป็น categorical)
146
+ if df[c].dtype == object:
147
+ unique_ratio = df[c].nunique() / len(df)
148
+ if 0.01 <= unique_ratio <= 0.5: # 1-50% ของข้อมูลเป็นค่าซ้ำ
149
+ shop_candidates.append(c)
150
+ shop_candidates = list(dict.fromkeys(shop_candidates))
151
+ shop_col = shop_candidates[0] if len(shop_candidates) > 0 else None
152
+
153
+ return text_col, date_candidates, date_col, shop_candidates, shop_col
154
 
155
  # ================= Charts =================
156
+ def make_summary_chart(df, chart_type="pie"):
157
+ """สร้างกราฟสรุปแบบเดียว (ไม่ซ้ำซ้อน)"""
158
  total = len(df)
159
+ neg_count = len(df[df["label"]=="negative"])
160
+ pos_count = len(df[df["label"]=="positive"])
161
+
 
 
 
 
 
 
162
  neg_avg = pd.to_numeric(df["negative(%)"].str.rstrip("%"), errors="coerce").mean()
163
  pos_avg = pd.to_numeric(df["positive(%)"].str.rstrip("%"), errors="coerce").mean()
164
+
165
+ info = (f"**📊 สรุปผลการวิเคราะห์**\n\n"
166
+ f"- 📝 ทั้งหมด: **{total:,}** รีวิว\n"
167
+ f"- 😞 เชิงลบ: **{neg_count:,}** ({neg_count/total*100:.1f}%)\n"
168
+ f"- 😊 เชิงบวก: **{pos_count:,}** ({pos_count/total*100:.1f}%)\n"
169
+ f"- 📈 ค่าเฉลี่ยความมั่นใจ:\n"
170
+ f" - เชิงลบ: {neg_avg:.2f}%\n"
171
+ f" - เชิงบวก: {pos_avg:.2f}%")
172
+
173
+ if chart_type == "pie":
174
+ fig = go.Figure(go.Pie(
175
+ labels=["😞 เชิงลบ","😊 เชิงบวก"],
176
+ values=[neg_count, pos_count],
177
+ hole=0.4,
178
+ marker=dict(colors=[NEG_COLOR, POS_COLOR]),
179
+ textinfo='label+percent',
180
+ textfont_size=14
181
+ ))
182
+ fig.update_layout(
183
+ title="สัดส่วนรีวิวเชิงบวก vs เชิงลบ",
184
+ template=TEMPLATE,
185
+ height=400
186
+ )
187
+ else: # bar
188
+ fig = go.Figure()
189
+ fig.add_bar(
190
+ x=["เชิงลบ","เชิงบวก"],
191
+ y=[neg_count, pos_count],
192
+ marker_color=[NEG_COLOR, POS_COLOR],
193
+ text=[neg_count, pos_count],
194
+ textposition='auto'
195
+ )
196
+ fig.update_layout(
197
+ title="จำนวนรีวิวแยกตามความรู้สึก",
198
+ template=TEMPLATE,
199
+ yaxis_title="จำนวน (รีวิว)",
200
+ height=400
201
+ )
202
+
203
+ return fig, info
204
 
205
  def _resample_counts(df, date_col, freq):
206
+ """รวมข้อมูลตามช่วงเวลา"""
207
  g = df.groupby([pd.Grouper(key=date_col, freq=freq),"label"]).size().unstack(fill_value=0)
208
  for c in ["negative","positive"]:
209
  if c not in g.columns: g[c]=0
210
  return g[["negative","positive"]].sort_index()
211
 
212
+ def make_time_chart(df, date_col, freq, use_smooth):
213
+ """กราฟแนวโน้มตามเวลา"""
214
+ ts = _resample_counts(df, date_col, freq)
215
+
216
+ if use_smooth:
217
+ window = 7 if freq=="D" else (4 if freq=="W" else 3)
218
+ ts = ts.rolling(window, min_periods=1).mean()
219
+
220
+ fig = go.Figure()
221
+ fig.add_scatter(
222
+ x=ts.index, y=ts["negative"],
223
+ mode="lines+markers",
224
+ name="😞 เชิงลบ",
225
+ line=dict(color=NEG_COLOR, width=2),
226
+ marker=dict(size=6)
227
+ )
228
+ fig.add_scatter(
229
+ x=ts.index, y=ts["positive"],
230
+ mode="lines+markers",
231
+ name="😊 เชิงบวก",
232
+ line=dict(color=POS_COLOR, width=2),
233
+ marker=dict(size=6)
234
+ )
235
+
236
+ freq_map = {"D": "รายวัน", "W": "รายสัปดาห์", "M": "รายเดือน"}
237
+ smooth_text = " (ปรับให้เรียบแล้ว)" if use_smooth else ""
238
+
239
+ fig.update_layout(
240
+ title=f"📈 แนวโน้มรีวิวตามเวลา ({freq_map[freq]}){smooth_text}",
241
+ template=TEMPLATE,
242
+ xaxis_title="วันที่",
243
+ yaxis_title="จำนวนรีวิว",
244
+ hovermode='x unified',
245
+ height=450
246
+ )
247
+
248
  return fig
249
 
250
+ def make_shop_analysis(df, shop_col, date_col=None, freq="D"):
251
+ """วิเคราะห์แยกตามร้าน/สาขา"""
252
+
253
+ # 1. สรุปภาพรวมแต่ละร้าน
254
+ shop_summary = []
255
+ for shop in df[shop_col].unique():
256
+ if pd.isna(shop):
257
+ continue
258
+ shop_df = df[df[shop_col] == shop]
259
+ neg = len(shop_df[shop_df["label"]=="negative"])
260
+ pos = len(shop_df[shop_df["label"]=="positive"])
261
+ total = len(shop_df)
262
+ pos_ratio = pos / total * 100 if total > 0 else 0
263
+
264
+ shop_summary.append({
265
+ "ร้าน/สาขา": shop,
266
+ "รีวิวทั้งหมด": total,
267
+ "😞 เชิงลบ": neg,
268
+ "😊 เชิงบวก": pos,
269
+ "% เชิงบวก": f"{pos_ratio:.1f}%"
270
+ })
271
+
272
+ summary_df = pd.DataFrame(shop_summary).sort_values("รีวิวทั้งหมด", ascending=False)
273
+
274
+ # 2. กราฟเปรียบเทียบร้าน
275
+ fig_compare = go.Figure()
276
+
277
+ shops = summary_df["ร้าน/สาขา"].tolist()
278
+ negs = summary_df["😞 เชิงลบ"].tolist()
279
+ poss = summary_df["😊 เชิงบวก"].tolist()
280
+
281
+ fig_compare.add_bar(name="😞 เชิงลบ", x=shops, y=negs, marker_color=NEG_COLOR)
282
+ fig_compare.add_bar(name="😊 เชิงบวก", x=shops, y=poss, marker_color=POS_COLOR)
283
+
284
+ fig_compare.update_layout(
285
+ title="🏪 เปรียบเทียบรีวิวแต่ละร้าน/สาขา",
286
+ barmode='stack',
287
+ template=TEMPLATE,
288
+ xaxis_title="ร้าน/สาขา",
289
+ yaxis_title="จำนวนรีวิว",
290
+ height=450
291
+ )
292
+
293
+ # 3. กราฟแนวโน้มตามเวลาแยกร้าน (ถ้ามี date_col)
294
+ fig_trend = None
295
+ if date_col and date_col in df.columns:
296
+ fig_trend = go.Figure()
297
+
298
+ for shop in shops[:5]: # แสดงแค่ 5 ร้านแรก
299
+ shop_df = df[df[shop_col] == shop].copy()
300
+ if len(shop_df) == 0:
301
+ continue
302
+
303
+ # คำนวณ positive ratio ตามเวลา
304
+ shop_df['pos_score'] = (shop_df['label'] == 'positive').astype(int)
305
+ ts = shop_df.groupby(pd.Grouper(key=date_col, freq=freq))['pos_score'].mean() * 100
306
+
307
+ fig_trend.add_scatter(
308
+ x=ts.index,
309
+ y=ts.values,
310
+ mode='lines+markers',
311
+ name=shop,
312
+ line=dict(width=2),
313
+ marker=dict(size=5)
314
+ )
315
+
316
+ freq_map = {"D": "รายวัน", "W": "รายสัปดาห์", "M": "รายเดือน"}
317
+ fig_trend.update_layout(
318
+ title=f"📊 แนวโน้ม % รีวิวเชิงบวกแยกตามร้าน ({freq_map[freq]})",
319
+ template=TEMPLATE,
320
+ xaxis_title="วันที่",
321
+ yaxis_title="% รีวิวเชิงบวก",
322
+ hovermode='x unified',
323
+ height=450
324
+ )
325
+
326
+ return summary_df, fig_compare, fig_trend
327
+
328
  # ================= Core Predict =================
329
  def _predict_batch(texts, model_name, batch_size=32):
330
+ model,tok,cfg=load_model(model_name)
331
+ results=[]
332
  for i in range(0,len(texts),batch_size):
333
  chunk=texts[i:i+batch_size]
334
  enc=tok(chunk,padding=True,truncation=True,
 
339
  for txt,p in zip(chunk,probs):
340
  neg,pos=float(p[0]),float(p[1])
341
  label="positive" if pos>=neg else "negative"
342
+ results.append({
343
+ "review":txt,
344
+ "negative(%)":_format_pct(neg),
345
+ "positive(%)":_format_pct(pos),
346
+ "label":label
347
+ })
348
  return results
349
 
350
+ # ================= Tab 1: วิเคราะห์หลายรีวิว =================
351
+ def predict_many(text_block, model_choice, chart_type):
352
  try:
353
+ raw = (text_block or "").splitlines()
354
+ norm = [_norm_text(t) for t in raw]
355
+ clean = [t for t in norm if _is_substantive_text(t)]
356
+
357
+ if not clean:
358
+ return pd.DataFrame(), go.Figure(), "❌ ไม่พบข้อความที่สามารถวิเคราะห์ได้\n\nกรุณาป้อนข้อความที่มีความยาวอย่างน้อย 2 ตัวอักษร"
359
+
360
+ results = _predict_batch(clean, model_choice)
361
+ df = pd.DataFrame(results)
362
+
363
+ fig, info = make_summary_chart(df, chart_type)
364
+
365
+ return df, fig, info
366
+
367
+ except Exception as e:
368
+ return pd.DataFrame(), go.Figure(), f"❌ เกิดข้อผิดพลาด:\n\n{traceback.format_exc()}"
369
 
370
+ # ================= Tab 2: อัปโหลด CSV =================
371
  def on_file_change(file_obj):
372
+ """เมื่ออัปโหลดไฟล์ - ตรวจหา columns อัตโนมัติ"""
373
  if file_obj is None:
374
+ return (gr.update(choices=[],value=None),
375
+ gr.update(choices=[],value=None),
376
+ gr.update(choices=[],value=None),
377
+ gr.update(visible=False),
378
+ gr.update(visible=False),
379
+ gr.update(visible=False),
380
+ gr.update(visible=False),
381
+ "⚠️ กรุณาอัปโหลดไฟล์ CSV")
382
+
383
  try:
384
+ df = pd.read_csv(file_obj.name)
385
+ text_col, date_candidates, date_col, shop_candidates, shop_col = detect_columns(df)
386
+
387
+ has_date = date_col is not None
388
+ has_shop = shop_col is not None
389
+
390
+ note = f"✅ **ตรวจพบคอลัมน์:**\n"
391
+ note += f"- 📝 ข้อความ: **{text_col}**\n"
392
+
393
+ if has_date:
394
+ note += f"- 📅 วันที่: **{date_col}**\n"
395
+ else:
396
+ note += f"- 📅 วันที่: _ไม่พบ_\n"
397
+
398
+ if has_shop:
399
+ note += f"- 🏪 ร้าน/สาขา: **{shop_col}** (พบ {df[shop_col].nunique()} ร้าน)\n"
400
+ else:
401
+ note += f"- 🏪 ร้าน/สาขา: _ไม่พบ_\n"
402
+
403
+ note += f"\n_หากไม่ถูกต้อง สามารถเลือกใหม่ได้จากเมนูด้านบน_"
404
+
405
+ return (gr.update(choices=list(df.columns), value=text_col),
406
+ gr.update(choices=date_candidates if date_candidates else ["ไม่มี"], value=date_col),
407
+ gr.update(choices=shop_candidates if shop_candidates else ["ไม่มี"], value=shop_col),
408
+ gr.update(visible=has_date),
409
+ gr.update(visible=has_date),
410
+ gr.update(visible=has_shop),
411
+ gr.update(visible=has_shop),
412
+ note)
413
+
414
+ except Exception as e:
415
+ return (gr.update(choices=[],value=None),
416
+ gr.update(choices=[],value=None),
417
+ gr.update(choices=[],value=None),
418
+ gr.update(visible=False),
419
+ gr.update(visible=False),
420
+ gr.update(visible=False),
421
+ gr.update(visible=False),
422
+ f"❌ ไม่สามารถอ่านไฟล์ได้:\n{str(e)}")
423
 
424
+ def predict_csv(file_obj, model_choice, text_col, date_col, shop_col,
425
+ date_preset, freq, use_smooth, chart_type):
426
+ """วิเคราะห์รีวิวจากไฟล์ CSV"""
427
+ if file_obj is None:
428
+ return (pd.DataFrame(), go.Figure(), go.Figure(),
429
+ gr.update(visible=False), gr.update(visible=False),
430
+ pd.DataFrame(), gr.update(visible=False),
431
+ "❌ กรุณาอัปโหลดไฟล์ CSV", None)
432
+
433
  try:
434
+ df_raw = pd.read_csv(file_obj.name)
435
+ cols = list(df_raw.columns)
436
+
437
+ # ตรวจสอบ text column
438
+ if text_col not in cols:
439
+ text_col, _, _, _, _ = detect_columns(df_raw)
440
+
441
+ # ดึงข้อความและทำนาย
442
+ texts = [_norm_text(v) for v in df_raw[text_col].tolist()]
443
+ texts = [t for t in texts if _is_substantive_text(t)]
444
+
445
+ if not texts:
446
+ return (pd.DataFrame(), go.Figure(), go.Figure(),
447
+ gr.update(visible=False), gr.update(visible=False),
448
+ pd.DataFrame(), gr.update(visible=False),
449
+ "❌ ไม่พบข้อความที่สามารถวิเคราะห์ได้ในไฟล์", None)
450
+
451
+ results = _predict_batch(texts, model_choice)
452
+ df_out = pd.DataFrame(results)
453
+
454
+ # กราฟสรุปหลัก
455
+ fig_main, info = make_summary_chart(df_out, chart_type)
456
+
457
+ # กราฟตามเวลา
458
+ fig_time = go.Figure()
459
+ show_time = False
460
+
461
+ if date_col and date_col in cols and date_col != "ไม่มี":
462
+ dts = _to_datetime_safe(df_raw[date_col])
463
  if dts.notna().any():
464
+ df_time = df_out.copy()
465
+ df_time["__dt__"] = dts
466
+ df_time = df_time.dropna(subset=["__dt__"])
467
+
468
+ # ใช้ date preset
469
+ df_time = apply_date_preset(df_time, "__dt__", date_preset)
470
+
471
+ if len(df_time) > 0:
472
+ fig_time = make_time_chart(df_time, "__dt__", freq, use_smooth)
473
+ show_time = True
474
+
475
+ # วิเคราะห์ตาม Shop
476
+ shop_summary_df = pd.DataFrame()
477
+ fig_shop = go.Figure()
478
+ fig_shop_trend = None
479
+ show_shop = False
480
+
481
+ if shop_col and shop_col in cols and shop_col != "ไม่มี":
482
+ df_with_shop = df_out.copy()
483
+ df_with_shop[shop_col] = df_raw[shop_col]
484
+
485
+ # ถ้ามี date ด้วย ให้ใส่เข้าไป
486
+ if date_col and date_col in cols and date_col != "ไม่มี":
487
+ dts = _to_datetime_safe(df_raw[date_col])
488
+ if dts.notna().any():
489
+ df_with_shop["__dt__"] = dts
490
+ df_with_shop = df_with_shop.dropna(subset=["__dt__"])
491
+ df_with_shop = apply_date_preset(df_with_shop, "__dt__", date_preset)
492
+
493
+ shop_summary_df, fig_shop, fig_shop_trend = make_shop_analysis(
494
+ df_with_shop, shop_col, "__dt__", freq
495
+ )
496
+ else:
497
+ shop_summary_df, fig_shop, _ = make_shop_analysis(df_with_shop, shop_col)
498
+ else:
499
+ shop_summary_df, fig_shop, _ = make_shop_analysis(df_with_shop, shop_col)
500
+
501
+ show_shop = True
502
+
503
+ # บันทึกไฟล์
504
+ fd, path = tempfile.mkstemp(suffix=".csv")
505
+ os.close(fd)
506
+ df_out.to_csv(path, index=False, encoding="utf-8-sig")
507
+
508
+ return (df_out, fig_main, fig_time,
509
+ gr.update(visible=show_time, value=fig_time),
510
+ gr.update(visible=show_shop, value=fig_shop),
511
+ shop_summary_df,
512
+ gr.update(visible=show_shop and fig_shop_trend is not None, value=fig_shop_trend),
513
+ info, path)
514
+
515
+ except Exception as e:
516
+ return (pd.DataFrame(), go.Figure(), go.Figure(),
517
+ gr.update(visible=False), gr.update(visible=False),
518
+ pd.DataFrame(), gr.update(visible=False),
519
+ f"❌ เกิดข้อผิดพลาด:\n\n{traceback.format_exc()}", None)
520
 
521
  # ================= Gradio UI =================
522
+ with gr.Blocks(title="Thai Sentiment Analysis", theme=gr.themes.Soft()) as demo:
523
+ gr.Markdown("""
524
+ # 🇹🇭 Thai Sentiment Analysis
525
+ ### วิเคราะห์ความรู้สึกรีวิวภาษาไทย (เชิงบวก/เชิงลบ)
526
+ """)
527
+
528
+ model_radio = gr.Radio(
529
+ choices=AVAILABLE_CHOICES,
530
+ value=DEFAULT_MODEL,
531
+ label="🤖 เลือกโมเดล",
532
+ info="แนะนำ: WCB สำหรับความเร็ว, WCB_4Layer_BiLSTM สำหรับความแม่นยำ"
533
+ )
534
 
535
+ # =================== Tab 1: วิเคราะห์หลายรีวิว ===================
536
+ with gr.Tab("📝 วิเคราะห์หลายรีวิว"):
537
+ gr.Markdown("""
538
+ **วิธีใช้:** ป้อนรีวิวหลายรายการ (แต่ละบรรทัด = 1 รีวิว) แล้วกด "เริ่มวิเคราะห์"
539
+
540
+ **ตัวอย่าง:**
541
+ ```
542
+ อาหารอร่อยมาก บริการดีค่ะ
543
+ ของแพงไป รสชาติก็ธรรมดา
544
+ บรรยากาศดี แต่รอนาน
545
+ ```
546
+ """)
547
+
548
+ text_input = gr.Textbox(
549
+ lines=10,
550
+ label="📄 ข้อความรีวิว (บรรทัดละ 1 รีวิว)",
551
+ placeholder="ป้อนรีวิวที่ต้องการวิเคราะห์...\nแต่ละบรรทัด = 1 รีวิว"
552
+ )
553
+
554
  with gr.Row():
555
+ chart_type_1 = gr.Radio(
556
+ choices=["pie", "bar"],
557
+ value="pie",
558
+ label="📊 รูปแบบกราฟ",
559
+ info="Pie = วงกลม, Bar = แท่ง"
560
+ )
561
+ predict_btn_1 = gr.Button("🚀 เริ่มวิเคราะห์", variant="primary", size="lg")
562
+
563
+ result_df_1 = gr.Dataframe(label="📋 ผลการวิเคราะห์ทั้งหมด")
564
+
565
  with gr.Row():
566
+ with gr.Column(scale=1):
567
+ result_chart_1 = gr.Plot(label="📊 กราฟสรุป")
568
+ with gr.Column(scale=1):
569
+ result_info_1 = gr.Markdown()
570
+
571
+ predict_btn_1.click(
572
+ predict_many,
573
+ [text_input, model_radio, chart_type_1],
574
+ [result_df_1, result_chart_1, result_info_1]
575
+ )
576
 
577
+ # =================== Tab 2: อัปโหลด CSV ===================
578
+ with gr.Tab("📤 อัปโหลด CSV"):
579
+ gr.Markdown("""
580
+ **วิธีใช้:** อัปโหลดไฟล์ CSV ที่มีคอลัมน์รีวิว (และอาจมีวันที่/ร้านด้วย)
581
+
582
+ **คอลัมน์ที่ต้องมี:**
583
+ - ✅ คอลัมน์ข้อความรีวิว (เช่น "text", "review", "รีวิว")
584
+ - ⭐ คอลัมน์วันที่ (optional - สำหรับวิเคราะห์แนวโน้ม)
585
+ - ⭐ คอลัมน์ร้าน/สาขา (optional - สำหรับเปรียบเทียบร้าน)
586
+ """)
587
+
588
+ with gr.Row():
589
+ file_input = gr.File(
590
+ file_types=[".csv"],
591
+ label="📁 อัปโหลดไฟล์ CSV"
592
+ )
593
+
594
+ detect_note = gr.Markdown("⬆️ อัปโหลดไฟล์เพื่อเริ่มต้น")
595
+
596
+ with gr.Row():
597
+ text_col_dd = gr.Dropdown(
598
+ label="📝 คอลัมน์ข้อความรีวิว",
599
+ info="เลือกคอลัมน์ที่มีเนื้อหารีวิว"
600
+ )
601
+ date_col_dd = gr.Dropdown(
602
+ label="📅 คอลัมน์วันที่ (ถ้าไม่มีเว้นว่าง)",
603
+ info="สำหรับวิเคราะห์แนวโน้มตามเวลา"
604
+ )
605
+ shop_col_dd = gr.Dropdown(
606
+ label="🏪 คอลัมน์ร้าน/สาขา (ถ้าไม่มีเว้นว่าง)",
607
+ info="สำหรับเปรียบเทียบแต่ละร้าน"
608
+ )
609
+
610
+ gr.Markdown("### ⚙️ ตั้งค่าการวิเคราะห์")
611
+
612
+ with gr.Row():
613
+ date_preset = gr.Radio(
614
+ choices=list(DATE_PRESETS.keys()),
615
+ value="ทั้งหมด",
616
+ label="📆 ช่วงเวลาที่ต้องการวิเคราะห์",
617
+ visible=False
618
+ )
619
+
620
+ freq = gr.Radio(
621
+ choices=[("รายวัน", "D"), ("รายสัปดาห์", "W"), ("รายเดือน", "M")],
622
+ value="D",
623
+ label="📊 ความละเอียดของกราฟ",
624
+ visible=False
625
+ )
626
+
627
+ with gr.Row():
628
+ use_smooth = gr.Checkbox(
629
+ value=True,
630
+ label="✨ ปรับกราฟให้เรียบ (Moving Average)",
631
+ info="ช่วยให้เห็นแนวโน้มชัดเจนขึ้น",
632
+ visible=False
633
+ )
634
+
635
+ chart_type_2 = gr.Radio(
636
+ choices=[("วงกลม", "pie"), ("แท่ง", "bar")],
637
+ value="pie",
638
+ label="📊 รูปแบบกราฟสรุป"
639
+ )
640
+
641
+ shop_analysis_row = gr.Row(visible=False)
642
+ shop_trend_row = gr.Row(visible=False)
643
+
644
+ predict_btn_2 = gr.Button("🚀 เริ่มวิเคราะห์ CSV", variant="primary", size="lg")
645
+
646
+ gr.Markdown("### 📊 ผลการวิเคราะห์")
647
+
648
+ result_df_2 = gr.Dataframe(label="📋 ผลการวิเคราะห์ทั้งหมด")
649
+
650
+ with gr.Row():
651
+ with gr.Column(scale=1):
652
+ result_chart_2 = gr.Plot(label="📊 กราฟสรุปภาพรวม")
653
+ with gr.Column(scale=1):
654
+ result_info_2 = gr.Markdown()
655
+
656
+ result_time = gr.Plot(label="📈 กราฟแนวโน้มตามเวลา", visible=False)
657
+
658
+ with shop_analysis_row:
659
+ gr.Markdown("### 🏪 วิเคราะห์แยกตามร้าน/สาขา")
660
+
661
+ shop_summary = gr.Dataframe(label="📊 สรุปแต่ละร้าน")
662
+ result_shop = gr.Plot(label="🏪 เปรียบเทียบรีวิวแต่ละร้าน", visible=False)
663
+
664
+ with shop_trend_row:
665
+ result_shop_trend = gr.Plot(label="📈 แนวโน้ม % เชิงบวกแยกตามร้าน", visible=False)
666
+
667
+ download_file = gr.File(label="💾 ดาวน์โหลดผลลัพธ์ (CSV)")
668
+
669
+ # Event handlers
670
+ file_input.change(
671
+ on_file_change,
672
+ [file_input],
673
+ [text_col_dd, date_col_dd, shop_col_dd,
674
+ date_preset, freq, use_smooth,
675
+ shop_analysis_row, detect_note]
676
+ )
677
+
678
+ predict_btn_2.click(
679
+ predict_csv,
680
+ [file_input, model_radio, text_col_dd, date_col_dd, shop_col_dd,
681
+ date_preset, freq, use_smooth, chart_type_2],
682
+ [result_df_2, result_chart_2, result_time,
683
+ result_time, result_shop,
684
+ shop_summary, result_shop_trend,
685
+ result_info_2, download_file]
686
+ )
687
+
688
+ gr.Markdown("""
689
+ ---
690
+ ### 💡 เกี่ยวกับโมเดล
691
+
692
+ **WangchanBERTa Variants** - โมเดล BERT ภาษาไทยที่ได้รับการฝึกสำหรับงานวิเคราะห์ความรู้สึก
693
+
694
+ - **WCB**: เร็ว เหมาะกับงานทั่วไป
695
+ - **WCB_BiLSTM**: เพิ่มความแม่นยำด้วย BiLSTM
696
+ - **WCB_CNN_BiLSTM**: ใช้ CNN + BiLSTM เพิ่มประสิทธิภาพ
697
+ - **WCB_4Layer_BiLSTM**: แม่นยำสูงสุด แต่ช้ากว่า
698
+
699
+ 📌 **หมายเหตุ:** โมเดลวิเคราะห์เฉพาะ **เชิงบวก/เชิงลบ** เท่านั้น (ไม่มี neutral)
700
+ """)
701
 
702
+ if __name__ == "__main__":
703
+ demo.launch()