Rooobert commited on
Commit
5955d42
·
verified ·
1 Parent(s): 71c73af

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -414
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import streamlit as st
2
  import pandas as pd
3
  import plotly.express as px
 
4
  import numpy as np
5
  from datetime import datetime
6
  from dataclasses import dataclass, field
@@ -18,8 +19,8 @@ def read_google_sheet(sheet_id, sheet_number=0):
18
  return None
19
 
20
  # 📊 Google Sheets ID
21
- sheet_id = "1Wc15DZWq48MxL7nXAsROJ6sRvH5njSa1ea0aaOGUOVk"
22
- gid = "1168424766"
23
 
24
  @dataclass
25
  class SurveyMappings:
@@ -54,319 +55,123 @@ class SurveyAnalyzer:
54
 
55
  def calculate_age(self, birth_year_column):
56
  """🔢 計算年齡(從民國年到實際年齡)"""
57
- # 獲取當前年份(西元年)
58
  current_year = datetime.now().year
59
-
60
- # 將 NaN 或無效值處理為 NaN
61
  birth_years = pd.to_numeric(birth_year_column, errors='coerce')
62
-
63
- # 民國年份轉西元年份 (民國年+1911=西元年)
64
  western_years = birth_years + 1911
65
-
66
- # 計算年齡
67
  ages = current_year - western_years
68
-
69
  return ages
70
 
71
- def generate_report(self, df: pd.DataFrame) -> Dict[str, Any]:
72
- """📝 生成問卷調查報告"""
 
 
 
 
 
 
 
 
 
 
73
  # 計算年齡
74
  ages = self.calculate_age(df['2.出生年(民國__年)'])
75
-
76
- # 取得教育程度分布(帶計數單位)
77
- education_counts = df['3.教育程度'].value_counts().to_dict()
78
- education_with_counts = {k: f"{v}人" for k, v in education_counts.items()}
79
-
80
- # 性別分布(帶計數單位)
81
- gender_counts = df['1. 性別'].value_counts().to_dict()
82
- gender_with_counts = {k: f"{v}人" for k, v in gender_counts.items()}
83
-
84
- # 計算每個滿意度項目的平均分數和標準差
85
- satisfaction_stats = {}
86
- for i, col in enumerate(self.satisfaction_columns):
87
- mean_score = df[col].mean()
88
- std_dev = df[col].std()
89
- satisfaction_stats[self.satisfaction_short_names[i]] = {
90
- '平均分數': f"{mean_score:.2f}",
91
- '標準差': f"{std_dev:.2f}"
92
- }
93
-
94
- return {
95
- '基本統計': {
96
- '總受訪人數': len(df),
97
- '性別分布': gender_with_counts,
98
- '教育程度分布': education_with_counts,
99
- '平均年齡': f"{ages.mean():.1f}歲"
100
  },
101
- '滿意度統計': {
102
- '整體平均滿意度': f"{df[self.satisfaction_columns].mean().mean():.2f}",
103
- '各項滿意度': satisfaction_stats
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,
208
- xref='paper',
209
- yref='paper',
210
- text=f'受訪人數: {num_respondents}人',
211
- showarrow=False,
212
- font=dict(size=16),
213
- bgcolor='rgba(255,255,255,0.8)',
214
- bordercolor='rgba(0,0,0,0.2)',
215
- borderwidth=1,
216
- borderpad=4,
217
- y=-0.2
218
  )
219
 
220
  st.plotly_chart(fig, use_container_width=True)
221
 
222
- def plot_gender_distribution(self, df: pd.DataFrame, venues=None, month=None, age_range=None):
223
- """🟠 性別分佈圓餅圖 - 增強精緻版"""
224
  # 過濾數據
225
  filtered_df = df.copy()
226
  if venues and '全部' not in venues:
227
- filtered_df = filtered_df[filtered_df['場域名稱'].isin(venues)]
228
- if month and month != '全部':
229
- # 假設有一個月份欄位,如果沒有請調整
230
- filtered_df = filtered_df[filtered_df['月份'] == month]
231
-
232
- # 年齡篩選
233
- if age_range:
234
- ages = self.calculate_age(filtered_df['2.出生年(民國__年)'])
235
- age_mask = (ages >= age_range[0]) & (ages <= age_range[1])
236
- filtered_df = filtered_df[age_mask]
237
-
238
- # 取得性別資料
239
- gender_counts = filtered_df['1. 性別'].value_counts().reset_index()
240
- gender_counts.columns = ['性別', '人數']
241
-
242
- # 計算百分比
243
- total = gender_counts['人數'].sum()
244
- gender_counts['百分比'] = (gender_counts['人數'] / total * 100).round(1)
245
- gender_counts['標籤'] = gender_counts.apply(lambda x: f"{x['性別']}: {x['人數']}人 ({x['百分比']}%)", axis=1)
246
-
247
- # 獲取篩選條件說明
248
- filter_description = []
249
- if venues and '全部' not in venues:
250
- filter_description.append(f"場域: {', '.join(venues)}")
251
- if month and month != '全部':
252
- filter_description.append(f"月份: {month}")
253
- if age_range and (age_range[0] != min(self.calculate_age(df['2.出生年(民國__年)'])) or
254
- age_range[1] != max(self.calculate_age(df['2.出生年(民國__年)']))):
255
- filter_description.append(f"年齡: {age_range[0]}-{age_range[1]}歲")
256
-
257
- filter_text = "(" + ", ".join(filter_description) + ")" if filter_description else ""
258
-
259
- # 設定顏色映射 - 男性藍色,女性紅色 - 使用更精緻的顏色
260
- color_map = {'男性': '#1976D2', '女性': '#D32F2F'}
261
-
262
- # 建立子圖佈局以添加更多自定義元素
263
- fig = px.pie(
264
- gender_counts,
265
- names='性別',
266
- values='人數',
267
- title=f'👥 受訪者性別分布{filter_text}',
268
- color='性別',
269
- color_discrete_map=color_map,
270
- hover_data=['人數', '百分比'],
271
- labels={'人數': '人數', '百分比': '百分比'},
272
- custom_data=['標籤']
273
  )
274
 
275
- # 更新圖表佈局
276
- fig.update_layout(
277
- font=dict(family="Arial", size=16),
278
- title_font=dict(family="Arial Black", size=24),
279
- title_x=0.5, # 標題置中
280
- legend_title_text="性別",
281
- legend=dict(
282
- orientation="h",
283
- yanchor="bottom",
284
- y=-0.2,
285
- xanchor="center",
286
- x=0.5,
287
- font=dict(size=16),
288
- bordercolor="#E0E0E0",
289
- borderwidth=2
290
- ),
291
- margin=dict(l=20, r=20, t=80, b=100),
292
- paper_bgcolor='white',
293
- annotations=[
294
- dict(
295
- text=f"總受訪人數: {total}人",
296
- x=0.5, y=-0.3,
297
- xref="paper",
298
- yref="paper",
299
- showarrow=False,
300
- font=dict(size=16, color="#616161")
301
- )
302
- ]
303
- )
304
-
305
- # 添加男女比例標籤
306
- male_count = gender_counts.loc[gender_counts['性別'] == '男性', '人數'].values[0] if '男性' in gender_counts['性別'].values else 0
307
- female_count = gender_counts.loc[gender_counts['性別'] == '女性', '人數'].values[0] if '女性' in gender_counts['性別'].values else 0
308
-
309
- # 計算男女比例
310
- if male_count > 0 and female_count > 0:
311
- ratio = round(male_count / female_count, 2)
312
- ratio_text = f"男女比例 = {ratio}:1"
313
- elif male_count > 0 and female_count == 0:
314
- ratio_text = "僅有男性"
315
- elif female_count > 0 and male_count == 0:
316
- ratio_text = "僅有女性"
317
- else:
318
- ratio_text = "無性別數據"
319
-
320
- fig.add_annotation(
321
- text=ratio_text,
322
- x=0.5, y=-0.15,
323
- xref="paper",
324
- yref="paper",
325
- showarrow=False,
326
- font=dict(size=16, color="#424242", family="Arial Bold")
327
  )
328
 
329
- # 更新懸停資訊
330
- fig.update_traces(
331
- textinfo='percent+label',
332
- hovertemplate='%{customdata[0]}',
333
- textfont_size=16,
334
- marker=dict(line=dict(color='#FFFFFF', width=2)),
335
- pull=[0.03, 0.03], # 稍微分離餅圖片段
336
- rotation=45 # 旋轉角度
337
  )
338
 
339
  st.plotly_chart(fig, use_container_width=True)
340
-
341
- # 在圓餅圖下方添加簡單分析
342
- st.markdown("""
343
- <div style="background-color:#F5F5F5; padding:15px; border-radius:10px; margin-top:10px; border-left:5px solid #1976D2;">
344
- <h4 style="color:#1976D2;">📊 性別分佈簡易分析</h4>
345
- """, unsafe_allow_html=True)
346
-
347
- # 生成簡單分析文字
348
- if total > 0:
349
- majority_gender = '男性' if male_count > female_count else '女性' if female_count > male_count else '男女相等'
350
- majority_pct = max(male_count, female_count) / total * 100 if male_count != female_count else 50
351
-
352
- if male_count != female_count:
353
- st.markdown(f"""
354
- <p>本次調查中,<strong>{majority_gender}</strong>佔多數,約佔總體的<strong>{majority_pct:.1f}%</strong>。</p>
355
- """, unsafe_allow_html=True)
356
- else:
357
- st.markdown("<p>本次調查中,男女比例相等,各佔50%。</p>", unsafe_allow_html=True)
358
- else:
359
- st.markdown("<p>目前沒有足夠的性別數據進行分析。</p>", unsafe_allow_html=True)
360
-
361
- st.markdown("</div>", unsafe_allow_html=True)
362
 
363
- # 🎨 Streamlit UI
364
  def main():
365
  st.set_page_config(
366
- page_title="數位示範場域問卷調查分析",
367
- layout="wide",
368
- initial_sidebar_state="expanded"
369
- )
370
 
371
  # 自定義CSS樣式
372
  st.markdown("""
@@ -386,24 +191,6 @@ def main():
386
  text-align: center;
387
  margin-bottom: 30px;
388
  }
389
- .unit-name {
390
- font-size: 28px;
391
- font-weight: bold;
392
- color: #1565C0;
393
- text-align: center;
394
- padding: 10px;
395
- background-color: #E3F2FD;
396
- border-radius: 8px;
397
- margin: 20px 0;
398
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
399
- }
400
- .card {
401
- padding: 20px;
402
- border-radius: 10px;
403
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
404
- margin-bottom: 20px;
405
- background-color: white;
406
- }
407
  </style>
408
  """, unsafe_allow_html=True)
409
 
@@ -418,137 +205,79 @@ def main():
418
  if df is not None:
419
  analyzer = SurveyAnalyzer()
420
 
421
- # 設置單位名稱(假設有「單位名稱」欄位,若無則顯示預設值)
422
- unit_name = "數位示範場域滿意度調查中心"
423
- if '單位名稱' in df.columns and not df['單位名稱'].isnull().all():
424
- unit_names = df['單位名稱'].unique()
425
- if len(unit_names) == 1:
426
- unit_name = unit_names[0]
427
-
428
- # 顯示單位名稱
429
- st.markdown(f'<div class="unit-name">{unit_name}</div>', unsafe_allow_html=True)
430
 
431
- # 新增場域和月份篩選器(使用更美觀的設計)
432
- st.sidebar.markdown("### 🔍 **數據篩選**")
433
- st.sidebar.markdown("---")
434
 
435
  # 場域選擇
436
- if '場域名稱' in df.columns:
437
- venues = ['全部'] + sorted(df['場域名稱'].unique().tolist())
438
- else:
439
- # 如果沒有場域欄位,創建10個虛擬場域供選擇
440
- venue_names = [
441
- "臺北數位樂學園", "新北創新學院", "桃園智慧中心",
442
- "臺中數位學苑", "臺南創客基地", "高雄創新園區",
443
- "宜蘭數位中心", "花蓮創新基地", "臺東學習中心", "金門數位樂園"
444
- ]
445
- venues = ['全部'] + venue_names
446
-
447
  selected_venues = st.sidebar.multiselect(
448
- "📍 **選擇場域**",
449
  venues,
450
  default=['全部'],
451
  help="可選擇多個場域進行數據分析比較"
452
  )
453
-
454
- # 月份選擇
455
- if '月份' in df.columns:
456
- months = ['全部'] + sorted(df['月份'].unique().tolist())
457
- else:
458
- # 如果沒有月份欄位,可以創建虛擬月份選項
459
- current_year = datetime.now().year
460
- months = ['全部'] + [f'{current_year}年{i+1}月' for i in range(12)]
461
-
462
- selected_month = st.sidebar.selectbox(
463
- "📅 **��擇月份**",
464
- months,
465
- help="選擇特定月份查看數據趨勢"
466
- )
467
 
468
- # 年齡區間篩選
469
- st.sidebar.markdown("### 📊 **年齡區間篩選**")
470
- ages = analyzer.calculate_age(df['2.出生年(民國__年)'])
471
- min_age, max_age = int(ages.min()), int(ages.max())
472
- age_range = st.sidebar.slider(
473
- "選擇年齡範圍",
474
- min_age,
475
- max_age,
476
- (min_age, max_age),
477
- help="拖曳調整以篩選特定年齡區間的受訪者"
478
  )
479
 
480
- # 📌 基本統計數據
481
- st.sidebar.header("📌 選擇數據分析")
482
- selected_analysis = st.sidebar.radio("選擇要查看的分析",
483
- ["📋 問卷統計報告", "📊 滿意度統計", "🟠 性別分佈"])
484
-
485
- # 應用所有篩選條件
486
  filtered_df = df.copy()
487
  if selected_venues and '全部' not in selected_venues:
488
- if '場域名稱' in filtered_df.columns:
489
- filtered_df = filtered_df[filtered_df['場域名稱'].isin(selected_venues)]
490
- if selected_month and selected_month != '全部':
491
- if '月份' in filtered_df.columns:
492
- filtered_df = filtered_df[filtered_df['月份'] == selected_month]
493
-
494
- # 年齡篩選
495
- if age_range:
496
- ages = analyzer.calculate_age(filtered_df['2.出生年(民國__年)'])
497
- age_mask = (ages >= age_range[0]) & (ages <= age_range[1])
498
- filtered_df = filtered_df[age_mask]
499
-
500
- # 顯示當前選擇的篩選器
501
- filter_status = []
502
- if selected_venues and '全部' not in selected_venues:
503
- filter_status.append(f"📍 場域: {', '.join(selected_venues)}")
504
- if selected_month and selected_month != '全部':
505
- filter_status.append(f"📅 月份: {selected_month}")
506
- if age_range and (age_range[0] != min(analyzer.calculate_age(df['2.出生年(民國__年)'])) or
507
- age_range[1] != max(analyzer.calculate_age(df['2.出生年(民國__年)']))):
508
- filter_status.append(f"👥 年齡: {age_range[0]}-{age_range[1]}歲")
509
-
510
- if filter_status:
511
- st.markdown("""
512
- <div style="background-color:#E3F2FD; padding:10px; border-radius:8px; margin-bottom:20px; border-left:4px solid #1976D2;">
513
- <h4 style="margin-bottom:10px; color:#1565C0;">🔍 當前篩選條件</h4>
514
- """, unsafe_allow_html=True)
515
-
516
- for status in filter_status:
517
- st.markdown(f"<p style='margin:5px 0;'>{status}</p>", unsafe_allow_html=True)
518
-
519
- # 顯示篩選後的樣本數
520
- st.markdown(f"""
521
- <p style='margin-top:10px; font-weight:bold;'>📊 篩選後樣本數: {len(filtered_df)}人</p>
522
- </div>
523
- """, unsafe_allow_html=True)
524
-
525
- # 數據分析區塊
526
- if selected_analysis == "📋 問卷統計報告":
527
- st.markdown('<h2 style="color:#1976D2;">📋 問卷統計報告</h2>', unsafe_allow_html=True)
528
-
529
- # 生成目前篩選條件下的報告
530
- report = analyzer.generate_report(filtered_df)
531
 
532
- # 使用卡片樣式顯示統計信息
533
- col1, col2 = st.columns(2)
534
 
535
- with col1:
536
- st.markdown('<div class="card">', unsafe_allow_html=True)
537
- st.markdown('<h3 style="color:#1976D2; border-bottom:1px solid #e0e0e0; padding-bottom:10px;">📊 基本統計數據</h3>', unsafe_allow_html=True)
538
-
539
- for key, value in report['基本統計'].items():
540
- if isinstance(value, dict):
541
- st.markdown(f"<p><strong>{key}:</strong></p>", unsafe_allow_html=True)
542
- for k, v in value.items():
543
- st.markdown(f"<p style='margin-left:20px;'>- {k}: {v}</p>", unsafe_allow_html=True)
544
- else:
545
- st.markdown(f"<p><strong>{key}:</strong> {value}</p>", unsafe_allow_html=True)
546
-
547
- st.markdown('</div>', unsafe_allow_html=True)
548
 
549
- with col2:
550
- st.markdown('<div class="card">', unsafe_allow_html=True)
551
- # Add the rest of the code here
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
 
553
  if __name__ == "__main__":
554
  main()
 
1
  import streamlit as st
2
  import pandas as pd
3
  import plotly.express as px
4
+ import plotly.graph_objs as go
5
  import numpy as np
6
  from datetime import datetime
7
  from dataclasses import dataclass, field
 
19
  return None
20
 
21
  # 📊 Google Sheets ID
22
+ sheet_id = "18wlbzQ-ZmFDeBONHnK7YNwfuvwUhnAA9_64ERHPkwzk"
23
+ gid = "1564149687"
24
 
25
  @dataclass
26
  class SurveyMappings:
 
55
 
56
  def calculate_age(self, birth_year_column):
57
  """🔢 計算年齡(從民國年到實際年齡)"""
 
58
  current_year = datetime.now().year
 
 
59
  birth_years = pd.to_numeric(birth_year_column, errors='coerce')
 
 
60
  western_years = birth_years + 1911
 
 
61
  ages = current_year - western_years
 
62
  return ages
63
 
64
+ def generate_dynamic_basic_stats(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
65
+ """📊 生成動態基本統計數據"""
66
+ # 性別統計
67
+ gender_stats = df['1. 性別'].value_counts().reset_index()
68
+ gender_stats.columns = ['性別', '人數']
69
+ gender_stats['百分比'] = (gender_stats['人數'] / gender_stats['人數'].sum() * 100).round(1)
70
+
71
+ # 教育程度統計
72
+ education_stats = df['3.教育程度'].value_counts().reset_index()
73
+ education_stats.columns = ['教育程度', '人數']
74
+ education_stats['百分比'] = (education_stats['人數'] / education_stats['人數'].sum() * 100).round(1)
75
+
76
  # 計算年齡
77
  ages = self.calculate_age(df['2.出生年(民國__年)'])
78
+ age_groups = pd.cut(ages, bins=[0, 55, 65, 75, 100], labels=['55歲以下', '56-65歲', '66-75歲', '75歲以上'])
79
+ age_stats = age_groups.value_counts().reset_index()
80
+ age_stats.columns = ['年齡組', '人數']
81
+ age_stats['百分比'] = (age_stats['人數'] / age_stats['人數'].sum() * 100).round(1)
82
+
83
+ return [
84
+ {
85
+ 'category': '性別分佈',
86
+ 'data': gender_stats.to_dict('records')
87
+ },
88
+ {
89
+ 'category': '教育程度分佈',
90
+ 'data': education_stats.to_dict('records')
 
 
 
 
 
 
 
 
 
 
 
 
91
  },
92
+ {
93
+ 'category': '年齡分佈',
94
+ 'data': age_stats.to_dict('records')
95
  }
96
+ ]
97
 
98
+ def plot_sunburst_gender_distribution(self, df: pd.DataFrame, venues=None):
99
+ """🌞 性別分佈旭日圖"""
100
  # 過濾數據
101
  filtered_df = df.copy()
102
  if venues and '全部' not in venues:
103
+ filtered_df = filtered_df[filtered_df['單位名稱'].isin(venues)]
104
+
105
+ # 準備旭日圖數據
106
+ gender_edu_counts = filtered_df.groupby(['1. 性別', '3.教育程度']).size().reset_index(name='count')
107
+
108
+ # 創建旭日圖
109
+ fig = px.sunburst(
110
+ gender_edu_counts,
111
+ path=['1. 性別', '3.教育程度'],
112
+ values='count',
113
+ title='👥 性別與教育程度分佈旭日圖',
114
+ color='1. 性別',
115
+ color_discrete_map={'男性': '#2196F3', '女性': '#F50057'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  )
117
 
118
+ # 更新佈局
119
  fig.update_layout(
120
+ title_font_size=24,
121
+ title_x=0.5,
122
+ width=700,
123
+ height=700
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  )
125
 
126
  st.plotly_chart(fig, use_container_width=True)
127
 
128
+ def plot_satisfaction_treemap(self, df: pd.DataFrame, venues=None):
129
+ """🌈 滿意度指標樹狀圖"""
130
  # 過濾數據
131
  filtered_df = df.copy()
132
  if venues and '全部' not in venues:
133
+ filtered_df = filtered_df[filtered_df['單位名稱'].isin(venues)]
134
+
135
+ # 計算各滿意度項目的平均分數
136
+ satisfaction_means = {}
137
+ for col, short_name in zip(self.satisfaction_columns, self.satisfaction_short_names):
138
+ satisfaction_means[short_name] = filtered_df[col].mean()
139
+
140
+ # 準備樹狀圖數據
141
+ treemap_data = pd.DataFrame.from_dict(satisfaction_means, orient='index', columns=['score']).reset_index()
142
+ treemap_data.columns = ['滿意度項目', '平均分數']
143
+ treemap_data['分數等級'] = pd.cut(
144
+ treemap_data['平均分數'],
145
+ bins=[0, 3, 4, 5],
146
+ labels=['一般', '良好', '優秀']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  )
148
 
149
+ # 創建樹狀圖
150
+ fig = px.treemap(
151
+ treemap_data,
152
+ path=['分數等級', '滿意度項目'],
153
+ values='平均分數',
154
+ color='平均分數',
155
+ color_continuous_scale='RdYlBu',
156
+ title='📊 滿意度指標樹狀圖'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  )
158
 
159
+ # 更新佈局
160
+ fig.update_layout(
161
+ title_font_size=24,
162
+ title_x=0.5,
163
+ width=800,
164
+ height=600
 
 
165
  )
166
 
167
  st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
 
169
  def main():
170
  st.set_page_config(
171
+ page_title="數位示範場域問卷調查分析",
172
+ layout="wide",
173
+ initial_sidebar_state="expanded"
174
+ )
175
 
176
  # 自定義CSS樣式
177
  st.markdown("""
 
191
  text-align: center;
192
  margin-bottom: 30px;
193
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  </style>
195
  """, unsafe_allow_html=True)
196
 
 
205
  if df is not None:
206
  analyzer = SurveyAnalyzer()
207
 
208
+ # 場域名稱選擇
209
+ if '單位名稱' in df.columns:
210
+ venues = ['全部'] + sorted(df['單位名稱'].unique().tolist())
211
+ else:
212
+ venues = ['全部']
 
 
 
 
213
 
214
+ # 側邊欄篩選
215
+ st.sidebar.header("🔍 數據篩選")
 
216
 
217
  # 場域選擇
 
 
 
 
 
 
 
 
 
 
 
218
  selected_venues = st.sidebar.multiselect(
219
+ "📍 選擇場域",
220
  venues,
221
  default=['全部'],
222
  help="可選擇多個場域進行數據分析比較"
223
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
+ # 選擇分析類型
226
+ analysis_type = st.sidebar.radio(
227
+ "選擇分析類型",
228
+ ["📊 基本統計", "👥 性別分佈", "📈 滿意度分析"]
 
 
 
 
 
 
229
  )
230
 
231
+ # 過濾數據
 
 
 
 
 
232
  filtered_df = df.copy()
233
  if selected_venues and '全部' not in selected_venues:
234
+ filtered_df = filtered_df[filtered_df['單位名稱'].isin(selected_venues)]
235
+
236
+ # 根據選擇的分析類型顯示不同的可視化
237
+ if analysis_type == "📊 基本統計":
238
+ st.header("📊 基本統計")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
+ # 生成動態統計數據
241
+ dynamic_stats = analyzer.generate_dynamic_basic_stats(filtered_df)
242
 
243
+ # 創建三個列來顯示不同的統計數據
244
+ cols = st.columns(3)
 
 
 
 
 
 
 
 
 
 
 
245
 
246
+ for i, stat_group in enumerate(dynamic_stats):
247
+ with cols[i]:
248
+ # 創建 Plotly 圖表
249
+ df_stat = pd.DataFrame(stat_group['data'])
250
+
251
+ # 創建長條圖
252
+ fig = px.bar(
253
+ df_stat,
254
+ x=df_stat.columns[0],
255
+ y='人數',
256
+ text='百分比',
257
+ title=f"{stat_group['category']}",
258
+ color=df_stat.columns[0]
259
+ )
260
+
261
+ # 更新佈局
262
+ fig.update_traces(
263
+ texttemplate='%{text}%',
264
+ textposition='outside'
265
+ )
266
+ fig.update_layout(
267
+ height=400,
268
+ yaxis_title='人數',
269
+ xaxis_title=df_stat.columns[0]
270
+ )
271
+
272
+ st.plotly_chart(fig, use_container_width=True)
273
+
274
+ elif analysis_type == "👥 性別分佈":
275
+ st.header("👥 性別分佈")
276
+ analyzer.plot_sunburst_gender_distribution(df, selected_venues)
277
+
278
+ elif analysis_type == "📈 滿意度分析":
279
+ st.header("📈 滿意度分析")
280
+ analyzer.plot_satisfaction_treemap(df, selected_venues)
281
 
282
  if __name__ == "__main__":
283
  main()