shiue2000 commited on
Commit
58a8eb1
·
verified ·
1 Parent(s): b70e41c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +94 -94
app.py CHANGED
@@ -11,9 +11,9 @@ import gradio as gr
11
  import logging
12
  from jinja2 import Template
13
  from matplotlib import font_manager
14
- # ===== 字型與樣式 =====
15
  # Load local SimHei font if available
16
- simhei_path = 'SimHei.ttf' # Assuming it's .ttf; change to .tiff if needed (though .ttf is standard)
17
  if os.path.exists(simhei_path):
18
  font_prop = font_manager.FontProperties(fname=simhei_path)
19
  plt.rcParams['font.family'] = 'sans-serif'
@@ -22,15 +22,15 @@ else:
22
  plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
23
  plt.rcParams['axes.unicode_minus'] = False
24
  plt.style.use("seaborn-v0_8")
25
- # ===== 日誌 =====
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
27
- # ===== 參數 =====
28
  candidates = ["許智傑", "邱議瑩", "賴瑞隆", "林岱樺", "柯志恩"]
29
  days_back = 7
30
  max_tweets_per_candidate = 20
31
  news_file = "news_sample.csv"
32
  history_file = "history_sentiment.csv"
33
- # ===== 情緒分析 =====
34
  try:
35
  from transformers import pipeline
36
  sentiment_pipeline = pipeline(
@@ -45,7 +45,7 @@ except:
45
  "label": random.choice(["positive", "negative", "neutral"]),
46
  "score": random.uniform(0.3, 0.9)
47
  }
48
- # ===== 模擬貼文抓取 =====
49
  def fetch_tweets(candidate):
50
  sample_texts = {
51
  "許智傑": ["許智傑積極參與地方活動", "許智傑被指政策空洞", "支持許智傑,打造高雄新未來!"],
@@ -63,7 +63,7 @@ def fetch_tweets(candidate):
63
  }
64
  for i in range(random.randint(5, max_tweets_per_candidate))
65
  ])
66
- # ===== 工具: Matplotlib base64 =====
67
  def fig_to_base64():
68
  buf = io.BytesIO()
69
  plt.savefig(buf, format="png", dpi=120, bbox_inches="tight")
@@ -72,76 +72,76 @@ def fig_to_base64():
72
  buf.close()
73
  plt.close()
74
  return img_b64
75
- # ===== 多圖產生器 =====
76
  def generate_charts(all_df, summary, df_hist):
77
  results = {}
78
- # 1. 每日情緒比例
79
  fig = plt.figure(figsize=(8, 5))
80
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
81
  kind='bar', stacked=True, colormap='coolwarm', ax=fig.gca()
82
  )
83
- plt.title("候選人每日社群情緒比例")
84
- plt.ylabel("比例")
85
- plt.xlabel("候選人")
86
- plt.legend(["正面", "負面", "中性"])
87
  results["img_b64_today"] = fig_to_base64()
88
- # 2. 歷史情緒趨勢
89
  fig = plt.figure(figsize=(10, 5))
90
  for c in candidates:
91
  temp = df_hist[df_hist['Candidate'] == c]
92
  if not temp.empty:
93
- plt.plot(temp['Date'], temp['Positive Ratio'], marker='o', label=f"{c} 正面")
94
- plt.plot(temp['Date'], temp['Negative Ratio'], marker='x', label=f"{c} 負面")
95
- plt.plot(temp['Date'], temp['Neutral Ratio'], marker='s', label=f"{c} 中性")
96
- plt.title("候選人歷史情緒趨勢")
97
  plt.xticks(rotation=45)
98
- plt.ylabel("比例")
99
- plt.xlabel("日期")
100
  plt.legend()
101
  results["img_b64_trend"] = fig_to_base64()
102
- # 3. 社群情緒趨勢
103
  sentiment_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Sentiment']).size().unstack(fill_value=0)
104
  sentiment_trend = sentiment_trend.div(sentiment_trend.sum(axis=1), axis=0).fillna(0)
105
  fig = plt.figure(figsize=(8, 5))
106
  for s in ['positive', 'negative', 'neutral']:
107
  if s in sentiment_trend.columns:
108
- plt.plot(sentiment_trend.index, sentiment_trend[s], marker='o', label={'positive':'正面', 'negative':'負面', 'neutral':'中性'}[s])
109
- plt.title("社群情緒趨勢")
110
- plt.xlabel("日期")
111
- plt.ylabel("比例")
112
  plt.legend()
113
  results["img_social_sentiment"] = fig_to_base64()
114
- # 4. 平台表現
115
  platforms = ["X", "Facebook", "Instagram", "PTT", "Line"]
116
  platform_counts = pd.Series({p: random.randint(10, 100) for p in platforms})
117
  fig = plt.figure(figsize=(8, 5))
118
  plt.bar(platforms, platform_counts, color='skyblue')
119
- plt.title("平台貼文聲量")
120
- plt.xlabel("平台")
121
- plt.ylabel("貼文數量")
122
  results["img_platform_performance"] = fig_to_base64()
123
- # 5. 候選人聲量趨勢
124
  candidate_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Candidate']).size().unstack(fill_value=0)
125
  fig = plt.figure(figsize=(8, 5))
126
  for c in candidates:
127
  if c in candidate_trend.columns:
128
  plt.plot(candidate_trend.index, candidate_trend[c], marker='o', label=c)
129
- plt.title("候選人貼文聲量趨勢")
130
- plt.xlabel("日期")
131
- plt.ylabel("貼文數量")
132
  plt.legend()
133
  results["img_candidate_volume"] = fig_to_base64()
134
- # 6. 候選人情緒分析
135
  fig = plt.figure(figsize=(8, 5))
136
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
137
  kind='bar', stacked=True, colormap='coolwarm', ax=fig.gca()
138
  )
139
- plt.title("候選人貼文情緒分析(正//中性)")
140
- plt.ylabel("比例")
141
- plt.xlabel("候選人")
142
- plt.legend(["正面", "負面", "中性"])
143
  results["img_candidate_sentiment"] = fig_to_base64()
144
- # 7. 知識圖譜
145
  fig, ax = plt.subplots(figsize=(8, 6))
146
  G = nx.Graph()
147
  for c in candidates:
@@ -149,75 +149,75 @@ def generate_charts(all_df, summary, df_hist):
149
  for i in range(len(candidates) - 1):
150
  G.add_edge(candidates[i], candidates[i + 1])
151
  nx.draw(G, nx.spring_layout(G), with_labels=True, node_color='lightgreen', font_size=12, ax=ax)
152
- plt.title("候選人知識圖譜")
153
  results["img_knowledge_graph"] = fig_to_base64()
154
  return results
155
- # ===== 主分析函數 =====
156
  def run_analysis():
157
  try:
158
  # Embed the template as a string to avoid file dependency and ensure syntax is correct
159
  html_template = """
160
  <!DOCTYPE html>
161
- <html lang="zh-TW">
162
  <head>
163
  <meta charset="UTF-8">
164
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
165
- <title>2026 高雄市長選舉輿情分析報告</title>
166
  <script src="https://cdn.tailwindcss.com"></script>
167
  </head>
168
  <body class="bg-gray-100 font-sans leading-normal tracking-normal">
169
  <div class="container mx-auto p-4">
170
- <h1 class="text-3xl font-bold mb-4">2026 高雄市長選舉輿情分析報告</h1>
171
- <p class="mb-4">報告日期: {{ report_date }}</p>
172
-
173
- <h2 class="text-2xl font-bold mb-2">參與度摘要</h2>
174
  {{ engagement_table | safe }}
175
-
176
- <h2 class="text-2xl font-bold mb-2">新聞摘要</h2>
177
  <ul class="list-disc pl-5 mb-4">
178
  {% for key, value in news_summary %}
179
  <li><strong>{{ key }}</strong>: {{ value }}</li>
180
  {% endfor %}
181
  </ul>
182
-
183
- <h2 class="text-2xl font-bold mb-2">新聞詳情</h2>
184
  {{ news_table | safe }}
185
-
186
- <h2 class="text-2xl font-bold mb-2">今日情緒比例</h2>
187
- <img src="data:image/png;base64,{{ img_b64_today }}" alt="今日情緒比例" class="mb-4">
188
-
189
- <h2 class="text-2xl font-bold mb-2">歷史情緒趨勢</h2>
190
- <img src="data:image/png;base64,{{ img_b64_trend }}" alt="歷史情緒趨勢" class="mb-4">
191
-
192
- <h2 class="text-2xl font-bold mb-2">社群情緒趨勢</h2>
193
- <img src="data:image/png;base64,{{ img_social_sentiment }}" alt="社群情緒趨勢" class="mb-4">
194
-
195
- <h2 class="text-2xl font-bold mb-2">平台表現</h2>
196
- <img src="data:image/png;base64,{{ img_platform_performance }}" alt="平台表現" class="mb-4">
197
-
198
- <h2 class="text-2xl font-bold mb-2">候選人聲量趨勢</h2>
199
- <img src="data:image/png;base64,{{ img_candidate_volume }}" alt="候選人聲量趨勢" class="mb-4">
200
-
201
- <h2 class="text-2xl font-bold mb-2">候選人情緒分析</h2>
202
- <img src="data:image/png;base64,{{ img_candidate_sentiment }}" alt="候選人情緒分析" class="mb-4">
203
-
204
- <h2 class="text-2xl font-bold mb-2">知識圖譜</h2>
205
- <img src="data:image/png;base64,{{ img_knowledge_graph }}" alt="知識圖譜" class="mb-4">
206
  </div>
207
  </body>
208
  </html>
209
  """
210
- # --- 貼文 & 情緒分析 ---
211
  all_df = pd.concat([fetch_tweets(c) for c in candidates], ignore_index=True)
212
  all_df['Sentiment'] = all_df['Content'].apply(lambda x: sentiment(x)['label'])
213
  all_df['Confidence'] = all_df['Content'].apply(lambda x: sentiment(x)['score'])
214
- # --- 統計 ---
215
  summary = all_df.groupby(['Candidate', 'Sentiment']).size().unstack(fill_value=0)
216
  summary['Total Posts'] = summary.sum(axis=1)
217
  summary['Positive Ratio'] = summary.get('positive', 0) / summary['Total Posts'].replace(0, 1)
218
  summary['Negative Ratio'] = summary.get('negative', 0) / summary['Total Posts'].replace(0, 1)
219
  summary['Neutral Ratio'] = summary.get('neutral', 0) / summary['Total Posts'].replace(0, 1)
220
- # --- 歷史資料 ---
221
  today_str = datetime.now().strftime('%Y-%m-%d')
222
  hist_row = summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].copy()
223
  hist_row['Date'] = today_str
@@ -227,56 +227,56 @@ def run_analysis():
227
  ignore_index=True
228
  ) if os.path.exists(history_file) else hist_row
229
  df_hist.to_csv(history_file, index=False)
230
- # --- 圖表 ---
231
  charts = generate_charts(all_df, summary, df_hist)
232
- # --- 新聞 ---
233
  if os.path.exists(news_file):
234
  df_news = pd.read_csv(news_file)
235
  news_summary = df_news.groupby('Category').size().to_dict()
236
  news_table = df_news.to_html(index=False, classes="min-w-full border border-gray-200")
237
  else:
238
  news_summary = {
239
- "民調": "柯志恩在多份民調中領先綠營候選人。",
240
- "黨內競爭": "民進黨初選競爭激烈。",
241
- "爭議": "林岱樺涉助理費爭議。"
242
  }
243
- news_table = "<p>無新聞資料</p>"
244
  # Convert news_summary to list of tuples to support iteration in template
245
  news_summary = list(news_summary.items())
246
- # --- 參與表 ---
247
  engagement_table = f"""
248
  <table class="min-w-full bg-white border border-gray-200">
249
  <tr class="bg-gray-100 border-b">
250
- <th class="py-2 px-4 border-r">總參與度</th>
251
  <td class="py-2 px-4 border-r">{len(all_df)}</td>
252
- <th class="py-2 px-4 border-r">正面情緒比例</th>
253
  <td class="py-2 px-4 border-r">{all_df['Sentiment'].value_counts(normalize=True).get('positive', 0):.1%}</td>
254
- <th class="py-2 px-4 border-r">平均互動率</th>
255
  <td class="py-2 px-4 border-r">3.9%</td>
256
- <th class="py-2 px-4 border-r">活躍平台數</th>
257
  <td class="py-2 px-4">{5}</td>
258
  </tr>
259
  </table>
260
  """
261
- # --- HTML 渲染 ---
262
  template = Template(html_template)
263
  html_content = template.render(
264
  report_date=datetime.now().strftime('%Y-%m-%d %H:%M'),
265
- engagement_table=engagement_table if engagement_table else "<p>未提供互動數據</p>",
266
- news_summary=news_summary if news_summary else "<p>未提供新聞摘要</p>",
267
- news_table=news_table if news_table else "<p>未提供新聞資料</p>",
268
  **charts
269
  )
270
-
271
  return html_content
272
  except Exception:
273
- return f"<pre>❌ 分析失敗:\n{traceback.format_exc()}</pre>"
274
- # ===== Gradio 前端 =====
275
  if __name__ == "__main__":
276
  iface = gr.Interface(
277
  fn=run_analysis,
278
  inputs=[],
279
  outputs=gr.HTML(),
280
- title="2026 高雄市長選舉輿情分析"
281
  )
282
  iface.launch(server_name="0.0.0.0", server_port=7860)
 
11
  import logging
12
  from jinja2 import Template
13
  from matplotlib import font_manager
14
+ # ===== Fonts and Styles =====
15
  # Load local SimHei font if available
16
+ simhei_path = 'SimHei.ttf' # Assuming it's .ttf; change to .tiff if needed (though .ttf is standard)
17
  if os.path.exists(simhei_path):
18
  font_prop = font_manager.FontProperties(fname=simhei_path)
19
  plt.rcParams['font.family'] = 'sans-serif'
 
22
  plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'Noto Sans TC', 'SimHei', 'Arial Unicode MS']
23
  plt.rcParams['axes.unicode_minus'] = False
24
  plt.style.use("seaborn-v0_8")
25
+ # ===== Logging =====
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
27
+ # ===== Parameters =====
28
  candidates = ["許智傑", "邱議瑩", "賴瑞隆", "林岱樺", "柯志恩"]
29
  days_back = 7
30
  max_tweets_per_candidate = 20
31
  news_file = "news_sample.csv"
32
  history_file = "history_sentiment.csv"
33
+ # ===== Sentiment Analysis =====
34
  try:
35
  from transformers import pipeline
36
  sentiment_pipeline = pipeline(
 
45
  "label": random.choice(["positive", "negative", "neutral"]),
46
  "score": random.uniform(0.3, 0.9)
47
  }
48
+ # ===== Simulate Post Fetching =====
49
  def fetch_tweets(candidate):
50
  sample_texts = {
51
  "許智傑": ["許智傑積極參與地方活動", "許智傑被指政策空洞", "支持許智傑,打造高雄新未來!"],
 
63
  }
64
  for i in range(random.randint(5, max_tweets_per_candidate))
65
  ])
66
+ # ===== Tool: Matplotlib to base64 =====
67
  def fig_to_base64():
68
  buf = io.BytesIO()
69
  plt.savefig(buf, format="png", dpi=120, bbox_inches="tight")
 
72
  buf.close()
73
  plt.close()
74
  return img_b64
75
+ # ===== Multi-Chart Generator =====
76
  def generate_charts(all_df, summary, df_hist):
77
  results = {}
78
+ # 1. Daily Sentiment Ratios
79
  fig = plt.figure(figsize=(8, 5))
80
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
81
  kind='bar', stacked=True, colormap='coolwarm', ax=fig.gca()
82
  )
83
+ plt.title("Candidates' Daily Social Sentiment Ratios")
84
+ plt.ylabel("Ratio")
85
+ plt.xlabel("Candidate")
86
+ plt.legend(["Positive", "Negative", "Neutral"])
87
  results["img_b64_today"] = fig_to_base64()
88
+ # 2. Historical Sentiment Trends
89
  fig = plt.figure(figsize=(10, 5))
90
  for c in candidates:
91
  temp = df_hist[df_hist['Candidate'] == c]
92
  if not temp.empty:
93
+ plt.plot(temp['Date'], temp['Positive Ratio'], marker='o', label=f"{c} Positive")
94
+ plt.plot(temp['Date'], temp['Negative Ratio'], marker='x', label=f"{c} Negative")
95
+ plt.plot(temp['Date'], temp['Neutral Ratio'], marker='s', label=f"{c} Neutral")
96
+ plt.title("Candidates' Historical Sentiment Trends")
97
  plt.xticks(rotation=45)
98
+ plt.ylabel("Ratio")
99
+ plt.xlabel("Date")
100
  plt.legend()
101
  results["img_b64_trend"] = fig_to_base64()
102
+ # 3. Social Sentiment Trends
103
  sentiment_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Sentiment']).size().unstack(fill_value=0)
104
  sentiment_trend = sentiment_trend.div(sentiment_trend.sum(axis=1), axis=0).fillna(0)
105
  fig = plt.figure(figsize=(8, 5))
106
  for s in ['positive', 'negative', 'neutral']:
107
  if s in sentiment_trend.columns:
108
+ plt.plot(sentiment_trend.index, sentiment_trend[s], marker='o', label={'positive':'Positive', 'negative':'Negative', 'neutral':'Neutral'}[s])
109
+ plt.title("Social Sentiment Trends")
110
+ plt.xlabel("Date")
111
+ plt.ylabel("Ratio")
112
  plt.legend()
113
  results["img_social_sentiment"] = fig_to_base64()
114
+ # 4. Platform Performance
115
  platforms = ["X", "Facebook", "Instagram", "PTT", "Line"]
116
  platform_counts = pd.Series({p: random.randint(10, 100) for p in platforms})
117
  fig = plt.figure(figsize=(8, 5))
118
  plt.bar(platforms, platform_counts, color='skyblue')
119
+ plt.title("Platform Post Volumes")
120
+ plt.xlabel("Platform")
121
+ plt.ylabel("Post Count")
122
  results["img_platform_performance"] = fig_to_base64()
123
+ # 5. Candidates' Volume Trends
124
  candidate_trend = all_df.groupby([pd.Grouper(key='Date', freq='D'), 'Candidate']).size().unstack(fill_value=0)
125
  fig = plt.figure(figsize=(8, 5))
126
  for c in candidates:
127
  if c in candidate_trend.columns:
128
  plt.plot(candidate_trend.index, candidate_trend[c], marker='o', label=c)
129
+ plt.title("Candidates' Post Volume Trends")
130
+ plt.xlabel("Date")
131
+ plt.ylabel("Post Count")
132
  plt.legend()
133
  results["img_candidate_volume"] = fig_to_base64()
134
+ # 6. Candidates' Sentiment Analysis
135
  fig = plt.figure(figsize=(8, 5))
136
  summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].plot(
137
  kind='bar', stacked=True, colormap='coolwarm', ax=fig.gca()
138
  )
139
+ plt.title("Candidates' Post Sentiment Analysis (Positive/Negative/Neutral)")
140
+ plt.ylabel("Ratio")
141
+ plt.xlabel("Candidate")
142
+ plt.legend(["Positive", "Negative", "Neutral"])
143
  results["img_candidate_sentiment"] = fig_to_base64()
144
+ # 7. Knowledge Graph
145
  fig, ax = plt.subplots(figsize=(8, 6))
146
  G = nx.Graph()
147
  for c in candidates:
 
149
  for i in range(len(candidates) - 1):
150
  G.add_edge(candidates[i], candidates[i + 1])
151
  nx.draw(G, nx.spring_layout(G), with_labels=True, node_color='lightgreen', font_size=12, ax=ax)
152
+ plt.title("Candidates' Knowledge Graph")
153
  results["img_knowledge_graph"] = fig_to_base64()
154
  return results
155
+ # ===== Main Analysis Function =====
156
  def run_analysis():
157
  try:
158
  # Embed the template as a string to avoid file dependency and ensure syntax is correct
159
  html_template = """
160
  <!DOCTYPE html>
161
+ <html lang="en">
162
  <head>
163
  <meta charset="UTF-8">
164
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
165
+ <title>2026 Kaohsiung Mayoral Election Public Opinion Analysis Report</title>
166
  <script src="https://cdn.tailwindcss.com"></script>
167
  </head>
168
  <body class="bg-gray-100 font-sans leading-normal tracking-normal">
169
  <div class="container mx-auto p-4">
170
+ <h1 class="text-3xl font-bold mb-4">2026 Kaohsiung Mayoral Election Public Opinion Analysis Report</h1>
171
+ <p class="mb-4">Report Date: {{ report_date }}</p>
172
+
173
+ <h2 class="text-2xl font-bold mb-2">Engagement Summary</h2>
174
  {{ engagement_table | safe }}
175
+
176
+ <h2 class="text-2xl font-bold mb-2">News Summary</h2>
177
  <ul class="list-disc pl-5 mb-4">
178
  {% for key, value in news_summary %}
179
  <li><strong>{{ key }}</strong>: {{ value }}</li>
180
  {% endfor %}
181
  </ul>
182
+
183
+ <h2 class="text-2xl font-bold mb-2">News Details</h2>
184
  {{ news_table | safe }}
185
+
186
+ <h2 class="text-2xl font-bold mb-2">Today's Sentiment Ratios</h2>
187
+ <img src="data:image/png;base64,{{ img_b64_today }}" alt="Today's Sentiment Ratios" class="mb-4">
188
+
189
+ <h2 class="text-2xl font-bold mb-2">Historical Sentiment Trends</h2>
190
+ <img src="data:image/png;base64,{{ img_b64_trend }}" alt="Historical Sentiment Trends" class="mb-4">
191
+
192
+ <h2 class="text-2xl font-bold mb-2">Social Sentiment Trends</h2>
193
+ <img src="data:image/png;base64,{{ img_social_sentiment }}" alt="Social Sentiment Trends" class="mb-4">
194
+
195
+ <h2 class="text-2xl font-bold mb-2">Platform Performance</h2>
196
+ <img src="data:image/png;base64,{{ img_platform_performance }}" alt="Platform Performance" class="mb-4">
197
+
198
+ <h2 class="text-2xl font-bold mb-2">Candidates' Volume Trends</h2>
199
+ <img src="data:image/png;base64,{{ img_candidate_volume }}" alt="Candidates' Volume Trends" class="mb-4">
200
+
201
+ <h2 class="text-2xl font-bold mb-2">Candidates' Sentiment Analysis</h2>
202
+ <img src="data:image/png;base64,{{ img_candidate_sentiment }}" alt="Candidates' Sentiment Analysis" class="mb-4">
203
+
204
+ <h2 class="text-2xl font-bold mb-2">Knowledge Graph</h2>
205
+ <img src="data:image/png;base64,{{ img_knowledge_graph }}" alt="Knowledge Graph" class="mb-4">
206
  </div>
207
  </body>
208
  </html>
209
  """
210
+ # --- Posts & Sentiment Analysis ---
211
  all_df = pd.concat([fetch_tweets(c) for c in candidates], ignore_index=True)
212
  all_df['Sentiment'] = all_df['Content'].apply(lambda x: sentiment(x)['label'])
213
  all_df['Confidence'] = all_df['Content'].apply(lambda x: sentiment(x)['score'])
214
+ # --- Statistics ---
215
  summary = all_df.groupby(['Candidate', 'Sentiment']).size().unstack(fill_value=0)
216
  summary['Total Posts'] = summary.sum(axis=1)
217
  summary['Positive Ratio'] = summary.get('positive', 0) / summary['Total Posts'].replace(0, 1)
218
  summary['Negative Ratio'] = summary.get('negative', 0) / summary['Total Posts'].replace(0, 1)
219
  summary['Neutral Ratio'] = summary.get('neutral', 0) / summary['Total Posts'].replace(0, 1)
220
+ # --- Historical Data ---
221
  today_str = datetime.now().strftime('%Y-%m-%d')
222
  hist_row = summary[['Positive Ratio', 'Negative Ratio', 'Neutral Ratio']].copy()
223
  hist_row['Date'] = today_str
 
227
  ignore_index=True
228
  ) if os.path.exists(history_file) else hist_row
229
  df_hist.to_csv(history_file, index=False)
230
+ # --- Charts ---
231
  charts = generate_charts(all_df, summary, df_hist)
232
+ # --- News ---
233
  if os.path.exists(news_file):
234
  df_news = pd.read_csv(news_file)
235
  news_summary = df_news.groupby('Category').size().to_dict()
236
  news_table = df_news.to_html(index=False, classes="min-w-full border border-gray-200")
237
  else:
238
  news_summary = {
239
+ "Polls": "Ko Chih-en leads Green Camp candidates in multiple polls.",
240
+ "Intra-party Competition": "Intense competition in the DPP primary.",
241
+ "Controversy": "Lin Dai-hua involved in assistant fee controversy."
242
  }
243
+ news_table = "<p>No news data available</p>"
244
  # Convert news_summary to list of tuples to support iteration in template
245
  news_summary = list(news_summary.items())
246
+ # --- Engagement Table ---
247
  engagement_table = f"""
248
  <table class="min-w-full bg-white border border-gray-200">
249
  <tr class="bg-gray-100 border-b">
250
+ <th class="py-2 px-4 border-r">Total Engagement</th>
251
  <td class="py-2 px-4 border-r">{len(all_df)}</td>
252
+ <th class="py-2 px-4 border-r">Positive Sentiment Ratio</th>
253
  <td class="py-2 px-4 border-r">{all_df['Sentiment'].value_counts(normalize=True).get('positive', 0):.1%}</td>
254
+ <th class="py-2 px-4 border-r">Average Interaction Rate</th>
255
  <td class="py-2 px-4 border-r">3.9%</td>
256
+ <th class="py-2 px-4 border-r">Active Platforms</th>
257
  <td class="py-2 px-4">{5}</td>
258
  </tr>
259
  </table>
260
  """
261
+ # --- HTML Rendering ---
262
  template = Template(html_template)
263
  html_content = template.render(
264
  report_date=datetime.now().strftime('%Y-%m-%d %H:%M'),
265
+ engagement_table=engagement_table if engagement_table else "<p>No engagement data provided</p>",
266
+ news_summary=news_summary if news_summary else "<p>No news summary provided</p>",
267
+ news_table=news_table if news_table else "<p>No news data provided</p>",
268
  **charts
269
  )
270
+
271
  return html_content
272
  except Exception:
273
+ return f"<pre>❌ Analysis failed:\n{traceback.format_exc()}</pre>"
274
+ # ===== Gradio Frontend =====
275
  if __name__ == "__main__":
276
  iface = gr.Interface(
277
  fn=run_analysis,
278
  inputs=[],
279
  outputs=gr.HTML(),
280
+ title="2026 Kaohsiung Mayoral Election Public Opinion Analysis"
281
  )
282
  iface.launch(server_name="0.0.0.0", server_port=7860)