Rooobert commited on
Commit
dd9f8a4
·
verified ·
1 Parent(s): da1dc34

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +175 -169
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,228 +17,235 @@ def read_google_sheet(sheet_id, sheet_number=0):
18
  st.error(f"❌ 讀取失敗:{str(e)}")
19
  return None
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  class SurveyAnalyzer:
22
  """📊 問卷分析類"""
23
 
24
  def __init__(self):
25
- # 滿意度欄位名稱
26
  self.satisfaction_columns = [
27
- '1.示範場域提供多元的數位課程與活動',
28
  '2.示範場域的數位課程與活動對我的生活應用有幫助',
29
- '3.示範場域的服務人員親切有禮貌',
30
  '4.示範場域的服務空間與數位設備友善方便',
31
  '5.在示範場域可以獲得需要的協助',
32
  '6.對於示範場域的服務感到滿意'
33
  ]
34
-
35
- # 對應的簡短名稱
36
  self.satisfaction_short_names = [
37
  '多元課程與活動',
38
- '生活應用幫助',
39
  '服務人員親切',
40
  '空間設備友善',
41
  '獲得需要協助',
42
  '整體服務滿意'
43
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  def plot_satisfaction_scores(self, df: pd.DataFrame):
46
- """📊 示範場域滿意度平均分數圖表"""
47
- # 確保所有滿意度欄位都存在
48
- existing_columns = [col for col in self.satisfaction_columns if col in df.columns]
49
-
50
- # 計算平均分數和標準差
51
- satisfaction_means = [df[col].mean() for col in existing_columns]
52
- satisfaction_stds = [df[col].std() for col in existing_columns]
53
 
54
  # 創建數據框
55
  satisfaction_df = pd.DataFrame({
56
- '滿意度項目': [self.satisfaction_short_names[self.satisfaction_columns.index(col)] for col in existing_columns],
57
  '平均分數': satisfaction_means,
58
  '標準差': satisfaction_stds
59
  })
60
 
61
- # 排序結果(由高到低)
62
- satisfaction_df = satisfaction_df.sort_values(by='平均分數', ascending=False)
63
-
64
- # 建立顏色漸變映射
65
- color_scale = [
66
- [0, '#90CAF9'], # 淺藍色
67
- [0.5, '#2196F3'], # 中藍色
68
- [1, '#1565C0'] # 深藍色
69
- ]
70
-
71
  # 繪製條形圖
72
  fig = px.bar(
73
  satisfaction_df,
74
  x='滿意度項目',
75
  y='平均分數',
76
  error_y='標準差',
77
- title='📊 示範場域各項滿意度分析',
78
  color='平均分數',
79
- color_continuous_scale=color_scale,
80
- text='平均分數',
81
- hover_data={
82
- '滿意度項目': True,
83
- '平均分數': ':.2f',
84
- '標準差': ':.2f'
85
- }
86
  )
87
 
88
  # 調整圖表佈局
89
  fig.update_layout(
90
- font=dict(family="Arial", size=16),
91
- title_font=dict(family="Arial Black", size=24),
92
- title_x=0.5, # 標題置中
93
  xaxis_title="滿意度項目",
94
  yaxis_title="平均分數",
95
- yaxis_range=[0, 5], # 評分範圍從0開始,視覺上更明顯
96
- plot_bgcolor='rgba(240,240,240,0.8)', # 淺灰色背景
97
- paper_bgcolor='white',
98
- xaxis_tickangle=-25, # 斜角標籤,避免重疊
99
- margin=dict(l=40, r=40, t=80, b=60),
100
- legend_title_text="平均分數",
101
- shapes=[
102
- # 添加參考線 - 4分線
103
- dict(
104
- type='line',
105
- yref='y', y0=4, y1=4,
106
- xref='paper', x0=0, x1=1,
107
- line=dict(color='rgba(220,20,60,0.5)', width=2, dash='dash')
108
- )
109
- ],
110
- annotations=[
111
- # 參考線標籤
112
- dict(
113
- x=0.02, y=4.1,
114
- xref='paper', yref='y',
115
- text='優良標準 (4分)',
116
- showarrow=False,
117
- font=dict(size=14, color='rgba(220,20,60,0.8)')
118
- )
119
- ]
120
  )
121
 
122
  # 調整文字格式
123
  fig.update_traces(
124
  texttemplate='%{y:.2f}',
125
- textposition='outside',
126
- marker_line_color='rgb(8,48,107)',
127
- marker_line_width=1.5,
128
- opacity=0.85
129
  )
130
 
131
- # 計算整體平均滿意度(只計算存在的欄位)
132
- overall_satisfaction = df[existing_columns].mean().mean()
133
-
134
- # 返回圖表和整體滿意度
135
- return fig, overall_satisfaction, len(df)
136
-
137
- def analyze_demographic_data(self, df: pd.DataFrame):
138
- """分析性別和教育程度"""
139
- # 性別分佈
140
- if '性別' in df.columns:
141
- gender_counts = df['性別'].value_counts()
142
- gender_pie = go.Figure(data=[go.Pie(
143
- labels=gender_counts.index,
144
- values=gender_counts.values,
145
- hole=.3,
146
- title='性別分佈'
147
- )])
148
- gender_pie.update_layout(title='📊 性別分佈')
149
- else:
150
- gender_pie = None
151
- st.warning("資料中缺少性別欄位")
152
-
153
- # 教育程度分佈
154
- if '教育程度' in df.columns:
155
- education_counts = df['教育程度'].value_counts()
156
- education_bar = go.Figure(data=[go.Bar(
157
- x=education_counts.index,
158
- y=education_counts.values,
159
- text=education_counts.values,
160
- textposition='auto'
161
- )])
162
- education_bar.update_layout(
163
- title='📊 教育程度分佈',
164
- xaxis_title='教育程度',
165
- yaxis_title='人數'
166
- )
167
- else:
168
- education_bar = None
169
- st.warning("資料中缺少教育程度欄位")
170
 
171
- return gender_pie, education_bar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
 
173
  def main():
174
- st.set_page_config(page_title="示範場域滿意度調查", layout="wide")
175
-
176
- # 讀取 Google Sheet 數據
177
- sheet_id = "1Wc15DZWq48MxL7nXAsROJ6sRvH5njSa1ea0aaOGUOVk"
178
- gid = "1168424766"
 
179
  df = read_google_sheet(sheet_id, gid)
180
-
181
  if df is not None:
182
- # 創建分析器
183
  analyzer = SurveyAnalyzer()
 
 
 
184
 
185
- # 顯示標題
186
- st.title("📊 示範場域滿意度調查分析")
187
-
188
- # 提示缺少的滿意度欄位
189
- missing_columns = [col for col in analyzer.satisfaction_columns if col not in df.columns]
190
- if missing_columns:
191
- st.warning(f"⚠️ 缺少以下滿意度欄位: {missing_columns}")
192
-
193
- # 繪製滿意度圖表
194
- satisfaction_fig, overall_satisfaction, num_respondents = analyzer.plot_satisfaction_scores(df)
195
-
196
- # 顯示滿意度圖表
197
- st.plotly_chart(satisfaction_fig, use_container_width=True)
198
-
199
- # 顯示整體滿意度
200
- st.markdown(f"""
201
- ### 📈 整體滿意度分析
202
- - **受訪人數**: {num_respondents} 人
203
- - **整體平均滿意度**: {overall_satisfaction:.2f} 分
204
-
205
- #### 🔍 滿意度解讀
206
- - 0-1分: 非常不滿意
207
- - 1-2分: 不滿意
208
- - 2-3分: 普通
209
- - 3-4分: 滿意
210
- - 4-5分: 非常滿意
211
-
212
- 根據調查結果,整體滿意度為 {overall_satisfaction:.2f} 分,
213
- """, unsafe_allow_html=True)
214
-
215
- # 根據整體滿意度提供文字解讀
216
- if overall_satisfaction < 2:
217
- st.warning("⚠️ 整體滿意度較低,建議深入檢討服務品質")
218
- elif overall_satisfaction < 3:
219
- st.info("ℹ️ 整體滿意度處於普通水平,可以進一步改善服務")
220
- elif overall_satisfaction < 4:
221
- st.success("✅ 整體滿意度良好,但仍有提升空間")
222
  else:
223
- st.balloons()
224
- st.success("🎉 整體滿意度非常高,表現優異!")
225
-
226
- # 人口統計分析
227
- st.header("👥 人口統計分析")
228
-
229
- # 創建兩列顯示
230
- col1, col2 = st.columns(2)
231
-
232
- # 性別分佈
233
- with col1:
234
- gender_pie, _ = analyzer.analyze_demographic_data(df)
235
- if gender_pie:
236
- st.plotly_chart(gender_pie, use_container_width=True)
237
-
238
- # 教育程度分佈
239
- with col2:
240
- _, education_bar = analyzer.analyze_demographic_data(df)
241
- if education_bar:
242
- st.plotly_chart(education_bar, use_container_width=True)
 
 
 
 
 
 
 
 
 
243
 
244
  if __name__ == "__main__":
245
  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
+ # 📊 Google Sheets ID
21
+ sheet_id = "1Wc15DZWq48MxL7nXAsROJ6sRvH5njSa1ea0aaOGUOVk"
22
+ gid = "1168424766"
23
+
24
+ @dataclass
25
+ class SurveyMappings:
26
+ """📋 問卷數據對應"""
27
+ gender: Dict[str, int] = field(default_factory=lambda: {'男性': 1, '女性': 2})
28
+ education: Dict[str, int] = field(default_factory=lambda: {
29
+ '國小(含)以下': 1, '國/初中': 2, '高中/職': 3, '專科': 4, '大學': 5, '研究所(含)以上': 6})
30
+ frequency: Dict[str, int] = field(default_factory=lambda: {
31
+ '第1次': 1, '2-3次': 2, '4-6次': 3, '6次以上': 4, '經常來學習,忘記次數了': 5})
32
+
33
  class SurveyAnalyzer:
34
  """📊 問卷分析類"""
35
 
36
  def __init__(self):
37
+ self.mappings = SurveyMappings()
38
  self.satisfaction_columns = [
39
+ '1. 示範場域提供多元的數位課程與活動',
40
  '2.示範場域的數位課程與活動對我的生活應用有幫助',
41
+ '3. 示範場域的服務人員親切有禮貌',
42
  '4.示範場域的服務空間與數位設備友善方便',
43
  '5.在示範場域可以獲得需要的協助',
44
  '6.對於示範場域的服務感到滿意'
45
  ]
 
 
46
  self.satisfaction_short_names = [
47
  '多元課程與活動',
48
+ '生活應用有幫助',
49
  '服務人員親切',
50
  '空間設備友善',
51
  '獲得需要協助',
52
  '整體服務滿意'
53
  ]
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):
108
+ """📊 各項滿意度平均分數圖表"""
109
+ # 準備數據
110
+ satisfaction_means = [df[col].mean() for col in self.satisfaction_columns]
111
+ satisfaction_stds = [df[col].std() for col in self.satisfaction_columns]
 
 
 
112
 
113
  # 創建數據框
114
  satisfaction_df = pd.DataFrame({
115
+ '滿意度項目': self.satisfaction_short_names,
116
  '平均分數': satisfaction_means,
117
  '標準差': satisfaction_stds
118
  })
119
 
 
 
 
 
 
 
 
 
 
 
120
  # 繪製條形圖
121
  fig = px.bar(
122
  satisfaction_df,
123
  x='滿意度項目',
124
  y='平均分數',
125
  error_y='標準差',
126
+ title='📊 各項滿意度平均分數與標準差',
127
  color='平均分數',
128
+ color_continuous_scale='Viridis',
129
+ text='平均分數'
 
 
 
 
 
130
  )
131
 
132
  # 調整圖表佈局
133
  fig.update_layout(
134
+ font=dict(size=16),
135
+ title_font=dict(size=24),
 
136
  xaxis_title="滿意度項目",
137
  yaxis_title="平均分數",
138
+ yaxis_range=[1, 5], # 假設評分範圍是 1-5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  )
140
 
141
  # 調整文字格式
142
  fig.update_traces(
143
  texttemplate='%{y:.2f}',
144
+ textposition='outside'
 
 
 
145
  )
146
 
147
+ st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ def plot_gender_distribution(self, df: pd.DataFrame, venues=None, month=None):
150
+ """🟠 性別分佈圓餅圖(使用藍色和紅色)"""
151
+ # 過濾數據
152
+ filtered_df = df.copy()
153
+ if venues and '全部' not in venues:
154
+ filtered_df = filtered_df[filtered_df['場域名稱'].isin(venues)]
155
+ if month and month != '全部':
156
+ # 假設有一個月份欄位,如果沒有請調整
157
+ filtered_df = filtered_df[filtered_df['月份'] == month]
158
+
159
+ gender_counts = filtered_df['1. 性別'].value_counts().reset_index()
160
+ gender_counts.columns = ['性別', '人數']
161
+
162
+ # 計算百分比
163
+ total = gender_counts['人數'].sum()
164
+ gender_counts['百分比'] = (gender_counts['人數'] / total * 100).round(1)
165
+ gender_counts['標籤'] = gender_counts.apply(lambda x: f"{x['性別']}: {x['人數']}人 ({x['百分比']}%)", axis=1)
166
+
167
+ # 設定顏色映射 - 男性藍色,女性紅色
168
+ color_map = {'男性': '#2171b5', '女性': '#cb181d'}
169
+
170
+ fig = px.pie(
171
+ gender_counts,
172
+ names='性別',
173
+ values='人數',
174
+ title='🟠 受訪者性別分布',
175
+ color='性別',
176
+ color_discrete_map=color_map,
177
+ hover_data=['人數', '百分比'],
178
+ labels={'人數': '人數', '百分比': '百分比'},
179
+ custom_data=['標籤']
180
+ )
181
+
182
+ # 更新悬停信息
183
+ fig.update_traces(
184
+ textinfo='percent+label',
185
+ hovertemplate='%{customdata[0]}'
186
+ )
187
+
188
+ st.plotly_chart(fig, use_container_width=True)
189
 
190
+ # 🎨 Streamlit UI
191
  def main():
192
+ st.set_page_config(page_title="問卷調查分析", layout="wide")
193
+
194
+ st.title("📊 問卷調查分析報告")
195
+ st.write("本頁面展示問卷調查數據的分析結果,包括統計信息與視覺化圖表。")
196
+
197
+ # 讀取數據
198
  df = read_google_sheet(sheet_id, gid)
199
+
200
  if df is not None:
 
201
  analyzer = SurveyAnalyzer()
202
+
203
+ # 新增場域和月份篩選器
204
+ st.sidebar.header("🔍 數據篩選")
205
 
206
+ # 假設數據有「場域名稱」欄位,如果名稱不同請調整
207
+ if '場域名稱' in df.columns:
208
+ venues = ['全部'] + sorted(df['場域名稱'].unique().tolist())
209
+ selected_venues = st.sidebar.multiselect("選擇場域", venues, default=['全部'])
210
+ else:
211
+ # 如果沒有場域欄位,創建10個虛擬場域供選擇
212
+ venues = ['全部'] + [f'場域{i+1}' for i in range(10)]
213
+ selected_venues = st.sidebar.multiselect("選擇場域", venues, default=['全部'])
214
+
215
+ # 假設數據有「月份」欄位,如果沒有請調整
216
+ if '月份' in df.columns:
217
+ months = ['全部'] + sorted(df['月份'].unique().tolist())
218
+ selected_month = st.sidebar.selectbox("選擇月份", months)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  else:
220
+ # 如果沒有月份欄位,可以創建虛擬月份選項
221
+ months = ['全部'] + [f'{i+1}月' for i in range(12)]
222
+ selected_month = st.sidebar.selectbox("選擇月份", months)
223
+
224
+ # 📌 基本統計數據
225
+ st.sidebar.header("📌 選擇數據分析")
226
+ selected_analysis = st.sidebar.radio("選擇要查看的分析",
227
+ ["📋 問卷統計報告", "📊 滿意度統計", "🟠 性別分佈"])
228
+
229
+ if selected_analysis == "📋 問卷統計報告":
230
+ st.header("📋 問卷統計報告")
231
+ report = analyzer.generate_report(df)
232
+ for category, stats in report.items():
233
+ with st.expander(f"🔍 {category}", expanded=True):
234
+ for key, value in stats.items():
235
+ if key == '各項滿意度':
236
+ st.write(f"**{key}:**")
237
+ for item, item_stats in value.items():
238
+ st.write(f" - **{item}**: {', '.join([f'{k}: {v}' for k, v in item_stats.items()])}")
239
+ else:
240
+ st.write(f"**{key}**: {value}")
241
+
242
+ elif selected_analysis == "📊 滿意度統計":
243
+ st.header("📊 滿意度統計")
244
+ analyzer.plot_satisfaction_scores(df)
245
+
246
+ elif selected_analysis == "🟠 性別分佈":
247
+ st.header("🟠 性別分佈")
248
+ analyzer.plot_gender_distribution(df, selected_venues, selected_month)
249
 
250
  if __name__ == "__main__":
251
  main()