Rooobert commited on
Commit
1935af6
·
verified ·
1 Parent(s): 936edd6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +271 -34
app.py CHANGED
@@ -104,32 +104,122 @@ class SurveyAnalyzer:
104
  }
105
  }
106
 
107
- 標準差',
108
- title='📊 各項滿意度平均分數與標準差',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  color='平均分數',
110
- color_continuous_scale='Viridis',
111
- text='平均分數'
 
 
 
 
 
112
  )
113
 
114
  # 調整圖表佈局
115
  fig.update_layout(
116
- font=dict(size=16),
117
- title_font=dict(size=24),
 
118
  xaxis_title="滿意度項目",
119
  yaxis_title="平均分數",
120
- yaxis_range=[1, 5], # 假設評分範圍是 1-5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  )
122
 
123
  # 調整文字格式
124
  fig.update_traces(
125
  texttemplate='%{y:.2f}',
126
- textposition='outside'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  )
128
 
129
  st.plotly_chart(fig, use_container_width=True)
130
 
131
- def plot_gender_distribution(self, df: pd.DataFrame, venues=None, month=None):
132
- """🟠 性別分佈圓餅圖(使用藍色和紅色)"""
133
  # 過濾數據
134
  filtered_df = df.copy()
135
  if venues and '全部' not in venues:
@@ -138,6 +228,13 @@ class SurveyAnalyzer:
138
  # 假設有一個月份欄位,如果沒有請調整
139
  filtered_df = filtered_df[filtered_df['月份'] == month]
140
 
 
 
 
 
 
 
 
141
  gender_counts = filtered_df['1. 性別'].value_counts().reset_index()
142
  gender_counts.columns = ['性別', '人數']
143
 
@@ -146,14 +243,27 @@ class SurveyAnalyzer:
146
  gender_counts['百分比'] = (gender_counts['人數'] / total * 100).round(1)
147
  gender_counts['標籤'] = gender_counts.apply(lambda x: f"{x['性別']}: {x['人數']}人 ({x['百分比']}%)", axis=1)
148
 
149
- # 設定顏色映射 - 男性藍色,女性紅色
150
- color_map = {'男性': '#2171b5', '女性': '#cb181d'}
 
 
 
 
 
 
 
 
 
 
 
 
151
 
 
152
  fig = px.pie(
153
  gender_counts,
154
  names='性別',
155
  values='人數',
156
- title='🟠 受訪者性別分布',
157
  color='性別',
158
  color_discrete_map=color_map,
159
  hover_data=['人數', '百分比'],
@@ -161,13 +271,93 @@ class SurveyAnalyzer:
161
  custom_data=['標籤']
162
  )
163
 
164
- # 更新悬停信息
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  fig.update_traces(
166
  textinfo='percent+label',
167
- hovertemplate='%{customdata[0]}'
 
 
 
 
168
  )
169
 
170
  st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
  # 🎨 Streamlit UI
173
  def main():
@@ -290,26 +480,73 @@ def main():
290
  selected_analysis = st.sidebar.radio("選擇要查看的分析",
291
  ["📋 問卷統計報告", "📊 滿意度統計", "🟠 性別分佈"])
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  if selected_analysis == "📋 問卷統計報告":
294
- st.header("📋 問卷統計報告")
295
- report = analyzer.generate_report(df)
296
- for category, stats in report.items():
297
- with st.expander(f"🔍 {category}", expanded=True):
298
- for key, value in stats.items():
299
- if key == '各項滿意度':
300
- st.write(f"**{key}:**")
301
- for item, item_stats in value.items():
302
- st.write(f" - **{item}**: {', '.join([f'{k}: {v}' for k, v in item_stats.items()])}")
303
- else:
304
- st.write(f"**{key}**: {value}")
305
-
306
- elif selected_analysis == "📊 滿意度統計":
307
- st.header("📊 滿意度統計")
308
- analyzer.plot_satisfaction_scores(df)
309
-
310
- elif selected_analysis == "🟠 性別分佈":
311
- st.header("🟠 性別分佈")
312
- analyzer.plot_gender_distribution(df, selected_venues, selected_month)
 
 
 
 
 
 
313
 
314
  if __name__ == "__main__":
315
  main()
 
104
  }
105
  }
106
 
107
+ def plot_satisfaction_scores(self, df: pd.DataFrame, venues=None, month=None, age_range=None):
108
+ """📊 各項滿意度平均分數圖表 - 美化版"""
109
+ # 過濾數據
110
+ filtered_df = df.copy()
111
+ if venues and '全部' not in venues:
112
+ filtered_df = filtered_df[filtered_df['場域名稱'].isin(venues)]
113
+ if month and month != '全部':
114
+ # 假設有一個月份欄位,如果沒有請調整
115
+ filtered_df = filtered_df[filtered_df['月份'] == month]
116
+
117
+ # 年齡篩選
118
+ if age_range:
119
+ ages = self.calculate_age(filtered_df['2.出生年(民國__年)'])
120
+ age_mask = (ages >= age_range[0]) & (ages <= age_range[1])
121
+ filtered_df = filtered_df[age_mask]
122
+
123
+ # 計算過濾後數據的平均和標準差
124
+ satisfaction_means = [filtered_df[col].mean() for col in self.satisfaction_columns]
125
+ satisfaction_stds = [filtered_df[col].std() for col in self.satisfaction_columns]
126
+
127
+ # 創建數據框
128
+ satisfaction_df = pd.DataFrame({
129
+ '滿意度項目': self.satisfaction_short_names,
130
+ '平均分數': satisfaction_means,
131
+ '標準差': satisfaction_stds
132
+ })
133
+
134
+ # 排序結果(可選)
135
+ satisfaction_df = satisfaction_df.sort_values(by='平均分數', ascending=False)
136
+
137
+ # 建立顏色漸變映射
138
+ color_scale = [
139
+ [0, '#90CAF9'], # 淺藍色
140
+ [0.5, '#2196F3'], # 中藍色
141
+ [1, '#1565C0'] # 深藍色
142
+ ]
143
+
144
+ # 繪製條形圖
145
+ fig = px.bar(
146
+ satisfaction_df,
147
+ x='滿意度項目',
148
+ y='平均分數',
149
+ error_y='標準差',
150
+ title='📊 各項滿意度平均分數與標準差分析',
151
  color='平均分數',
152
+ color_continuous_scale=color_scale,
153
+ text='平均分數',
154
+ hover_data={
155
+ '滿意度項目': True,
156
+ '平均分數': ':.2f',
157
+ '標準差': ':.2f'
158
+ }
159
  )
160
 
161
  # 調整圖表佈局
162
  fig.update_layout(
163
+ font=dict(family="Arial", size=16),
164
+ title_font=dict(family="Arial Black", size=24),
165
+ title_x=0.5, # 標題置中
166
  xaxis_title="滿意度項目",
167
  yaxis_title="平均分數",
168
+ yaxis_range=[0, 5], # 評分範圍從0開始,視覺上更明顯
169
+ plot_bgcolor='rgba(240,240,240,0.8)', # 淺灰色背景
170
+ paper_bgcolor='white',
171
+ xaxis_tickangle=-25, # 斜角標籤,避免重疊
172
+ margin=dict(l=40, r=40, t=80, b=60),
173
+ legend_title_text="平均分數",
174
+ shapes=[
175
+ # 添加參考線 - 例如4分
176
+ dict(
177
+ type='line',
178
+ yref='y', y0=4, y1=4,
179
+ xref='paper', x0=0, x1=1,
180
+ line=dict(color='rgba(220,20,60,0.5)', width=2, dash='dash')
181
+ )
182
+ ],
183
+ annotations=[
184
+ # 參考線標籤
185
+ dict(
186
+ x=0.02, y=4.1,
187
+ xref='paper', yref='y',
188
+ text='優良標準 (4分)',
189
+ showarrow=False,
190
+ font=dict(size=14, color='rgba(220,20,60,0.8)')
191
+ )
192
+ ]
193
  )
194
 
195
  # 調整文字格式
196
  fig.update_traces(
197
  texttemplate='%{y:.2f}',
198
+ textposition='outside',
199
+ marker_line_color='rgb(8,48,107)',
200
+ marker_line_width=1.5,
201
+ opacity=0.85
202
+ )
203
+
204
+ # 添加受訪人數標註
205
+ num_respondents = len(filtered_df)
206
+ fig.add_annotation(
207
+ x=0.5, y=0,
208
+ xref='paper', yref='paper',
209
+ text=f'受訪人數: {num_respondents}人',
210
+ showarrow=False,
211
+ font=dict(size=16),
212
+ bgcolor='rgba(255,255,255,0.8)',
213
+ bordercolor='rgba(0,0,0,0.2)',
214
+ borderwidth=1,
215
+ borderpad=4,
216
+ y=-0.2
217
  )
218
 
219
  st.plotly_chart(fig, use_container_width=True)
220
 
221
+ def plot_gender_distribution(self, df: pd.DataFrame, venues=None, month=None, age_range=None):
222
+ """🟠 性別分佈圓餅圖 - 增強精緻版"""
223
  # 過濾數據
224
  filtered_df = df.copy()
225
  if venues and '全部' not in venues:
 
228
  # 假設有一個月份欄位,如果沒有請調整
229
  filtered_df = filtered_df[filtered_df['月份'] == month]
230
 
231
+ # 年齡篩選
232
+ if age_range:
233
+ ages = self.calculate_age(filtered_df['2.出生年(民國__年)'])
234
+ age_mask = (ages >= age_range[0]) & (ages <= age_range[1])
235
+ filtered_df = filtered_df[age_mask]
236
+
237
+ # 取得性別資料
238
  gender_counts = filtered_df['1. 性別'].value_counts().reset_index()
239
  gender_counts.columns = ['性別', '人數']
240
 
 
243
  gender_counts['百分比'] = (gender_counts['人數'] / total * 100).round(1)
244
  gender_counts['標籤'] = gender_counts.apply(lambda x: f"{x['性別']}: {x['人數']}人 ({x['百分比']}%)", axis=1)
245
 
246
+ # 獲取篩選條件說明
247
+ filter_description = []
248
+ if venues and '全部' not in venues:
249
+ filter_description.append(f"場域: {', '.join(venues)}")
250
+ if month and month != '全部':
251
+ filter_description.append(f"月份: {month}")
252
+ if age_range and (age_range[0] != min(self.calculate_age(df['2.出生年(民國__年)'])) or
253
+ age_range[1] != max(self.calculate_age(df['2.出生年(民國__年)']))):
254
+ filter_description.append(f"年齡: {age_range[0]}-{age_range[1]}歲")
255
+
256
+ filter_text = "(" + ", ".join(filter_description) + ")" if filter_description else ""
257
+
258
+ # 設定顏色映射 - 男性藍色,女性紅色 - 使用更精緻的顏色
259
+ color_map = {'男性': '#1976D2', '女性': '#D32F2F'}
260
 
261
+ # 建立子圖佈局以添加更多自定義元素
262
  fig = px.pie(
263
  gender_counts,
264
  names='性別',
265
  values='人數',
266
+ title=f'👥 受訪者性別分布{filter_text}',
267
  color='性別',
268
  color_discrete_map=color_map,
269
  hover_data=['人數', '百分比'],
 
271
  custom_data=['標籤']
272
  )
273
 
274
+ # 更新圖表佈局
275
+ fig.update_layout(
276
+ font=dict(family="Arial", size=16),
277
+ title_font=dict(family="Arial Black", size=24),
278
+ title_x=0.5, # 標題置中
279
+ legend_title_text="性別",
280
+ legend=dict(
281
+ orientation="h",
282
+ yanchor="bottom",
283
+ y=-0.2,
284
+ xanchor="center",
285
+ x=0.5,
286
+ font=dict(size=16),
287
+ bordercolor="#E0E0E0",
288
+ borderwidth=2
289
+ ),
290
+ margin=dict(l=20, r=20, t=80, b=100),
291
+ paper_bgcolor='white',
292
+ annotations=[
293
+ dict(
294
+ text=f"總受訪人數: {total}人",
295
+ x=0.5, y=-0.3,
296
+ xref="paper",
297
+ yref="paper",
298
+ showarrow=False,
299
+ font=dict(size=16, color="#616161")
300
+ )
301
+ ]
302
+ )
303
+
304
+ # 添加男女比例標籤
305
+ male_count = gender_counts.loc[gender_counts['性別'] == '男性', '人數'].values[0] if '男性' in gender_counts['性別'].values else 0
306
+ female_count = gender_counts.loc[gender_counts['性別'] == '女性', '人數'].values[0] if '女性' in gender_counts['性別'].values else 0
307
+
308
+ # 計算男女比例
309
+ if male_count > 0 and female_count > 0:
310
+ ratio = round(male_count / female_count, 2)
311
+ ratio_text = f"男女比例 = {ratio}:1"
312
+ elif male_count > 0 and female_count == 0:
313
+ ratio_text = "僅有男性"
314
+ elif female_count > 0 and male_count == 0:
315
+ ratio_text = "僅有女性"
316
+ else:
317
+ ratio_text = "無性別數據"
318
+
319
+ fig.add_annotation(
320
+ text=ratio_text,
321
+ x=0.5, y=-0.15,
322
+ xref="paper",
323
+ yref="paper",
324
+ showarrow=False,
325
+ font=dict(size=16, color="#424242", family="Arial Bold")
326
+ )
327
+
328
+ # 更新懸停資訊
329
  fig.update_traces(
330
  textinfo='percent+label',
331
+ hovertemplate='%{customdata[0]}',
332
+ textfont_size=16,
333
+ marker=dict(line=dict(color='#FFFFFF', width=2)),
334
+ pull=[0.03, 0.03], # 稍微分離餅圖片段
335
+ rotation=45 # 旋轉角度
336
  )
337
 
338
  st.plotly_chart(fig, use_container_width=True)
339
+
340
+ # 在圓餅圖下方添加簡單分析
341
+ st.markdown("""
342
+ <div style="background-color:#F5F5F5; padding:15px; border-radius:10px; margin-top:10px; border-left:5px solid #1976D2;">
343
+ <h4 style="color:#1976D2;">📊 性別分佈簡易分析</h4>
344
+ """, unsafe_allow_html=True)
345
+
346
+ # 生成簡單分析文字
347
+ if total > 0:
348
+ majority_gender = '男性' if male_count > female_count else '女性' if female_count > male_count else '男女相等'
349
+ majority_pct = max(male_count, female_count) / total * 100 if male_count != female_count else 50
350
+
351
+ if male_count != female_count:
352
+ st.markdown(f"""
353
+ <p>本次調查中,<strong>{majority_gender}</strong>佔多數,約佔總體的<strong>{majority_pct:.1f}%</strong>。</p>
354
+ """, unsafe_allow_html=True)
355
+ else:
356
+ st.markdown("<p>本次調查中,男女比例相等,各佔50%。</p>", unsafe_allow_html=True)
357
+ else:
358
+ st.markdown("<p>目前沒有足夠的性別數據進行分析。</p>", unsafe_allow_html=True)
359
+
360
+ st.markdown("</div>", unsafe_allow_html=True)
361
 
362
  # 🎨 Streamlit UI
363
  def main():
 
480
  selected_analysis = st.sidebar.radio("選擇要查看的分析",
481
  ["📋 問卷統計報告", "📊 滿意度統計", "🟠 性別分佈"])
482
 
483
+ # 應用所有篩選條件
484
+ filtered_df = df.copy()
485
+ if selected_venues and '全部' not in selected_venues:
486
+ if '場域名稱' in filtered_df.columns:
487
+ filtered_df = filtered_df[filtered_df['場域名稱'].isin(selected_venues)]
488
+ if selected_month and selected_month != '全部':
489
+ if '月份' in filtered_df.columns:
490
+ filtered_df = filtered_df[filtered_df['月份'] == selected_month]
491
+
492
+ # 年齡篩選
493
+ if age_range:
494
+ ages = analyzer.calculate_age(filtered_df['2.出生年(民國__年)'])
495
+ age_mask = (ages >= age_range[0]) & (ages <= age_range[1])
496
+ filtered_df = filtered_df[age_mask]
497
+
498
+ # 顯示當前選擇的篩選器
499
+ filter_status = []
500
+ if selected_venues and '全部' not in selected_venues:
501
+ filter_status.append(f"📍 場域: {', '.join(selected_venues)}")
502
+ if selected_month and selected_month != '全部':
503
+ filter_status.append(f"📅 月份: {selected_month}")
504
+ if age_range and (age_range[0] != min(analyzer.calculate_age(df['2.出生年(民國__年)'])) or
505
+ age_range[1] != max(analyzer.calculate_age(df['2.出生年(民國__年)']))):
506
+ filter_status.append(f"👥 年齡: {age_range[0]}-{age_range[1]}歲")
507
+
508
+ if filter_status:
509
+ st.markdown("""
510
+ <div style="background-color:#E3F2FD; padding:10px; border-radius:8px; margin-bottom:20px; border-left:4px solid #1976D2;">
511
+ <h4 style="margin-bottom:10px; color:#1565C0;">🔍 當前篩選條件</h4>
512
+ """, unsafe_allow_html=True)
513
+
514
+ for status in filter_status:
515
+ st.markdown(f"<p style='margin:5px 0;'>{status}</p>", unsafe_allow_html=True)
516
+
517
+ # 顯示篩選後的樣本數
518
+ st.markdown(f"""
519
+ <p style='margin-top:10px; font-weight:bold;'>📊 篩選後樣本數: {len(filtered_df)}人</p>
520
+ </div>
521
+ """, unsafe_allow_html=True)
522
+
523
+ # 數據分析區塊
524
  if selected_analysis == "📋 問卷統計報告":
525
+ st.markdown('<h2 style="color:#1976D2;">📋 問卷統計報告</h2>', unsafe_allow_html=True)
526
+
527
+ # 生成目前篩選條件下的報告
528
+ report = analyzer.generate_report(filtered_df)
529
+
530
+ # 使用卡片樣式顯示統計信息
531
+ col1, col2 = st.columns(2)
532
+
533
+ with col1:
534
+ st.markdown('<div class="card">', unsafe_allow_html=True)
535
+ st.markdown('<h3 style="color:#1976D2; border-bottom:1px solid #e0e0e0; padding-bottom:10px;">📊 基本統計數據</h3>', unsafe_allow_html=True)
536
+
537
+ for key, value in report['基本統計'].items():
538
+ if isinstance(value, dict):
539
+ st.markdown(f"<p><strong>{key}:</strong></p>", unsafe_allow_html=True)
540
+ for k, v in value.items():
541
+ st.markdown(f"<p style='margin-left:20px;'>- {k}: {v}</p>", unsafe_allow_html=True)
542
+ else:
543
+ st.markdown(f"<p><strong>{key}:</strong> {value}</p>", unsafe_allow_html=True)
544
+
545
+ st.markdown('</div>', unsafe_allow_html=True)
546
+
547
+ with col2:
548
+ st.markdown('<div class="card">', unsafe_allow_html=True)
549
+
550
 
551
  if __name__ == "__main__":
552
  main()