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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +151 -222
app.py CHANGED
@@ -1,7 +1,6 @@
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
@@ -18,266 +17,196 @@ def read_google_sheet(sheet_id, sheet_number=0):
18
  st.error(f"❌ 讀取失敗:{str(e)}")
19
  return None
20
 
21
- # 📊 Google Sheets ID
22
- sheet_id = "18wlbzQ-ZmFDeBONHnK7YNwfuvwUhnAA9_64ERHPkwzk"
23
- gid = "1564149687"
24
-
25
- @dataclass
26
- class SurveyMappings:
27
- """📋 問卷數據對應"""
28
- gender: Dict[str, int] = field(default_factory=lambda: {'男性': 1, '女性': 2})
29
- education: Dict[str, int] = field(default_factory=lambda: {
30
- '國小(含)以下': 1, '國/初中': 2, '高中/職': 3, '專科': 4, '大學': 5, '研究所(含)以上': 6})
31
- frequency: Dict[str, int] = field(default_factory=lambda: {
32
- '第1次': 1, '2-3次': 2, '4-6次': 3, '6次以上': 4, '經常來學習,忘記次數了': 5})
33
-
34
  class SurveyAnalyzer:
35
  """📊 問卷分析類"""
36
 
37
  def __init__(self):
38
- self.mappings = SurveyMappings()
39
  self.satisfaction_columns = [
40
- '1. 示範場域提供多元的數位課程與活動',
41
  '2.示範場域的數位課程與活動對我的生活應用有幫助',
42
- '3. 示範場域的服務人員親切有禮貌',
43
  '4.示範場域的服務空間與數位設備友善方便',
44
  '5.在示範場域可以獲得需要的協助',
45
  '6.對於示範場域的服務感到滿意'
46
  ]
 
 
47
  self.satisfaction_short_names = [
48
  '多元課程與活動',
49
- '生活應用有幫助',
50
  '服務人員親切',
51
  '空間設備友善',
52
  '獲得需要協助',
53
  '整體服務滿意'
54
  ]
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("""
178
- <style>
179
- .main-header {
180
- font-size: 42px;
181
- font-weight: bold;
182
- color: #1E88E5;
183
- text-align: center;
184
- margin-bottom: 10px;
185
- padding-bottom: 15px;
186
- border-bottom: 2px solid #e0e0e0;
187
- }
188
- .sub-header {
189
- font-size: 24px;
190
- color: #424242;
191
- text-align: center;
192
- margin-bottom: 30px;
193
- }
194
- </style>
195
- """, unsafe_allow_html=True)
196
 
197
- # 主標題與副標題
198
- st.markdown('<div class="main-header">📊 114年度樂齡學習數位示範體驗場域 服務滿意度調查分析報告</div>', unsafe_allow_html=True)
199
- st.markdown('<div class="sub-header">本報告提供全面的問卷調查分析與視覺化圖表,瞭解樂齡學習者參與數位示範場域服務滿意情形</div>', unsafe_allow_html=True)
200
- st.markdown('<div class="sub-header">本國立中正大學高齡教育研究中心專案管理團隊製作</div>', unsafe_allow_html=True)
201
-
202
- # 讀取數據
203
  df = read_google_sheet(sheet_id, gid)
204
-
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()
 
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
 
17
  st.error(f"❌ 讀取失敗:{str(e)}")
18
  return None
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  class SurveyAnalyzer:
21
  """📊 問卷分析類"""
22
 
23
  def __init__(self):
24
+ # 更新滿意度欄位名稱
25
  self.satisfaction_columns = [
26
+ '1.示範場域提供多元的數位課程與活動',
27
  '2.示範場域的數位課程與活動對我的生活應用有幫助',
28
+ '3.示範場域的服務人員親切有禮貌',
29
  '4.示範場域的服務空間與數位設備友善方便',
30
  '5.在示範場域可以獲得需要的協助',
31
  '6.對於示範場域的服務感到滿意'
32
  ]
33
+
34
+ # 對應的簡短名稱
35
  self.satisfaction_short_names = [
36
  '多元課程與活動',
37
+ '生活應用幫助',
38
  '服務人員親切',
39
  '空間設備友善',
40
  '獲得需要協助',
41
  '整體服務滿意'
42
  ]
 
 
 
 
 
 
 
 
43
 
44
+ def plot_satisfaction_scores(self, df: pd.DataFrame):
45
+ """📊 示範場域滿意度平均分數圖表"""
46
+ # 計算平均分數和標準差
47
+ satisfaction_means = [df[col].mean() for col in self.satisfaction_columns]
48
+ satisfaction_stds = [df[col].std() for col in self.satisfaction_columns]
49
+
50
+ # 創建數據框
51
+ satisfaction_df = pd.DataFrame({
52
+ '滿意度項目': self.satisfaction_short_names,
53
+ '平均分數': satisfaction_means,
54
+ '標準差': satisfaction_stds
55
+ })
56
+
57
+ # 排序結果(由高到低)
58
+ satisfaction_df = satisfaction_df.sort_values(by='平均分數', ascending=False)
59
+
60
+ # 建立顏色漸變映射
61
+ color_scale = [
62
+ [0, '#90CAF9'], # 淺藍色
63
+ [0.5, '#2196F3'], # 中藍色
64
+ [1, '#1565C0'] # 深藍色
 
 
 
 
 
 
 
 
 
 
 
65
  ]
 
 
 
 
 
 
 
66
 
67
+ # 繪製條形圖
68
+ fig = px.bar(
69
+ satisfaction_df,
70
+ x='滿意度項目',
71
+ y='平均分數',
72
+ error_y='標準差',
73
+ title='📊 示範場域各項滿意度分析',
74
+ color='平均分數',
75
+ color_continuous_scale=color_scale,
76
+ text='平均分數',
77
+ hover_data={
78
+ '滿意度項目': True,
79
+ '平均分數': ':.2f',
80
+ '標準差': ':.2f'
81
+ }
82
  )
83
 
84
+ # 調整圖表佈局
85
  fig.update_layout(
86
+ font=dict(family="Arial", size=16),
87
+ title_font=dict(family="Arial Black", size=24),
88
+ title_x=0.5, # 標題置中
89
+ xaxis_title="滿意度項目",
90
+ yaxis_title="平均分數",
91
+ yaxis_range=[0, 5], # 評分範圍從0開始,視覺上更明顯
92
+ plot_bgcolor='rgba(240,240,240,0.8)', # 淺灰色背景
93
+ paper_bgcolor='white',
94
+ xaxis_tickangle=-25, # 斜角標籤,避免重疊
95
+ margin=dict(l=40, r=40, t=80, b=60),
96
+ legend_title_text="平均分數",
97
+ shapes=[
98
+ # 添加參考線 - 4分線
99
+ dict(
100
+ type='line',
101
+ yref='y', y0=4, y1=4,
102
+ xref='paper', x0=0, x1=1,
103
+ line=dict(color='rgba(220,20,60,0.5)', width=2, dash='dash')
104
+ )
105
+ ],
106
+ annotations=[
107
+ # 參考線標籤
108
+ dict(
109
+ x=0.02, y=4.1,
110
+ xref='paper', yref='y',
111
+ text='優良標準 (4分)',
112
+ showarrow=False,
113
+ font=dict(size=14, color='rgba(220,20,60,0.8)')
114
+ )
115
+ ]
116
  )
117
 
118
+ # 調整文字格式
119
+ fig.update_traces(
120
+ texttemplate='%{y:.2f}',
121
+ textposition='outside',
122
+ marker_line_color='rgb(8,48,107)',
123
+ marker_line_width=1.5,
124
+ opacity=0.85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  )
126
 
127
+ # 添加受訪人數標註
128
+ num_respondents = len(df)
129
+ fig.add_annotation(
130
+ x=0.5,
131
+ xref='paper',
132
+ yref='paper',
133
+ text=f'受訪人數: {num_respondents}人',
134
+ showarrow=False,
135
+ font=dict(size=16),
136
+ bgcolor='rgba(255,255,255,0.8)',
137
+ bordercolor='rgba(0,0,0,0.2)',
138
+ borderwidth=1,
139
+ borderpad=4,
140
+ y=-0.2
141
  )
142
 
143
+ # 計算整體平均滿意度
144
+ overall_satisfaction = df[self.satisfaction_columns].mean().mean()
 
 
 
 
 
145
 
146
+ # 返回圖表和整體滿意度
147
+ return fig, overall_satisfaction
148
 
149
  def main():
150
+ st.set_page_config(page_title="示範場域滿意度調查", layout="wide")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ # 讀取 Google Sheet 數據
153
+ sheet_id = "1Wc15DZWq48MxL7nXAsROJ6sRvH5njSa1ea0aaOGUOVk"
154
+ gid = "1168424766"
 
 
 
155
  df = read_google_sheet(sheet_id, gid)
156
+
157
  if df is not None:
158
+ # 檢查必要的欄位是否存在
159
+ required_columns = [
160
+ '1.示範場域提供多元的數位課程與活動',
161
+ '2.示範場域的數位課程與活動對我的生活應用有幫助',
162
+ '3.示範場域的服務人員親切有禮貌',
163
+ '4.示範場域的服務空間與數位設備友善方便',
164
+ '5.在示範場域可以獲得需要的協助',
165
+ '6.對於示範場域的服務感到滿意'
166
+ ]
 
 
 
 
 
 
 
 
 
167
 
168
+ # 確認所有必要欄位都存在
169
+ missing_columns = [col for col in required_columns if col not in df.columns]
170
+ if missing_columns:
171
+ st.error(f"缺少以下必要欄位: {missing_columns}")
172
+ else:
173
+ # 創建分析器
174
+ analyzer = SurveyAnalyzer()
 
 
 
 
 
 
 
175
 
176
+ # 顯示標題
177
+ st.title("📊 示範場域滿意度調查分析")
178
 
179
+ # 繪製滿意度圖表
180
+ satisfaction_fig, overall_satisfaction = analyzer.plot_satisfaction_scores(df)
181
 
182
+ # 顯示圖表
183
+ st.plotly_chart(satisfaction_fig, use_container_width=True)
184
+
185
+ # 顯示整體滿意度
186
+ st.markdown(f"""
187
+ ### 📈 整體滿意度分析
188
+ - **整體平均滿意度**: {overall_satisfaction:.2f} 分
189
+
190
+ #### 🔍 滿意度解讀
191
+ - 0-1分: 非常不滿意
192
+ - 1-2分: 不滿意
193
+ - 2-3分: 普通
194
+ - 3-4分: 滿意
195
+ - 4-5分: 非常滿意
196
+
197
+ 根據調查結果,整體滿意度為 {overall_satisfaction:.2f} 分,
198
+ """, unsafe_allow_html=True)
199
+
200
+ # 根據整體滿意度提供文字解讀
201
+ if overall_satisfaction < 2:
202
+ st.warning("⚠️ 整體滿意度較低,建議深入檢討服務品質")
203
+ elif overall_satisfaction < 3:
204
+ st.info("ℹ️ 整體滿意度處於普通水平,可以進一步改善服務")
205
+ elif overall_satisfaction < 4:
206
+ st.success("✅ 整體滿意度良好,但仍有提升空間")
207
+ else:
208
+ st.balloons()
209
+ st.success("🎉 整體滿意度非常高,表現優異!")
 
 
 
 
 
 
 
210
 
211
  if __name__ == "__main__":
212
  main()