Lashtw commited on
Commit
c6eaf85
·
verified ·
1 Parent(s): 1e25590

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +260 -84
app.py CHANGED
@@ -1,103 +1,279 @@
1
  import streamlit as st
2
  import pandas as pd
3
- import plotly.express as px
4
- import os
 
5
  from io import BytesIO
6
- import zipfile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  def setup_chinese_font():
9
- import plotly.io as pio
10
- pio.templates["custom_radar"] = {
11
- "layout": {
12
- "font": {"family": "SimSun-ExtB", "size": 12},
13
- "title_x": 0.5,
14
- "title_y": 0.98,
15
- "title_font_size": 14,
16
- "polar": {
17
- "radialaxis": {"tickfont": {"family": "SimSun-ExtB"}},
18
- "angularaxis": {"tickfont": {"family": "SimSun-ExtB"}}
19
- }
20
- }
21
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- def create_radar_chart(student_data, avg_score, student_id):
24
- fig = px.line_polar(r=student_data, theta=student_data.index,
25
- name=f"學生 {student_id}", line_close=True)
26
- fig.add_scatterpolar(r=[avg_score]*len(student_data),
27
- theta=student_data.index,
28
- name="平均分數", fill=None)
29
- fig.update_traces(fill='toself')
30
- fig.update_layout(title_text=f"學生 {student_id} 的成績分析",
31
- title_x=0.5, showlegend=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  return fig
33
 
 
 
 
 
34
  def generate_radar_image(fig):
35
- img_bytes = fig.to_image(format="png", width=800, height=600)
36
- return img_bytes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  def main():
39
- st.title("學生成绩分析工具")
40
- st.write("上傳你的成績文件,我們將為你生成詳細的分析報告。")
41
-
42
- # 文件上传组件
43
- uploaded_file = st.file_uploader(
44
- "請選擇要上傳的文件",
45
- accept_multiple_files=False,
46
- key="fileUploader"
47
- )
 
 
 
48
 
 
 
 
49
  if uploaded_file is not None:
50
- try:
51
- # 判断文件类型
52
- file_ext = os.path.splitext(uploaded_file.name)[1].lower()
53
- if file_ext == ".csv":
54
- df = pd.read_csv(uploaded_file)
55
- elif file_ext in [".xlsx", ".xls"]:
56
- df = pd.read_excel(uploaded_file, engine='openpyxl')
57
- else:
58
- st.error("不支援的文件格式,請上傳 CSV 或 Excel 文件。")
59
- return
60
-
61
- # 检查數據格式
62
- if '學生ID' not in df.columns or '科目' not in df.columns:
63
- st.error("文件格式不符合要求,請確保包含 '學生ID' 和 '科目' 這兩列。")
64
- return
65
-
66
- # 獲取獨特的學生ID和科目列表
67
- student_ids = df['學生ID'].unique()
68
- subjects = df['科目'].unique()
69
-
70
- # 用戶選擇
71
- st.write("### 選擇分析條件")
72
- selected_student = st.selectbox(
73
- "請選擇要分析的学生",
74
- options=student_ids,
75
- key="selectedStudent"
76
  )
77
 
78
- # 確定���定學生的數據
79
- filtered_data = df[df['學生ID'] == selected_student]
80
- student_subjects = filtered_data['科目']
81
- scores = filtered_data['分數']
82
-
83
- # 計算平均分
84
- avg_score = scores.mean()
85
-
86
- # 生成雷達圖
87
- fig = create_radar_chart(scores, avg_score, selected_student)
88
- st.plotly_chart(fig)
89
-
90
- # 提供下載按鈕
91
- img_bytes = generate_radar_image(fig)
92
- st.download_button(
93
- "下載圖片",
94
- data=img_bytes,
95
- file_name=f"student_{selected_student}_analysis.png",
96
- mime="image/png"
97
- )
98
 
99
- except Exception as e:
100
- st.error(f"發生錯誤:{e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  if __name__ == "__main__":
103
  main()
 
1
  import streamlit as st
2
  import pandas as pd
3
+ import plotly.graph_objects as go
4
+ import plotly.io as pio
5
+ from zipfile import ZipFile
6
  from io import BytesIO
7
+ import matplotlib.pyplot as plt
8
+ import os
9
+ import matplotlib.font_manager as fm
10
+
11
+ # ============================
12
+ # 載入本地字型檔與設定
13
+ # ============================
14
+
15
+ font_path = "NotoSansTC-Regular.ttf" # 確認此字型檔路徑正確
16
+ if os.path.exists(font_path):
17
+ fm.fontManager.addfont(font_path)
18
+ font_prop = fm.FontProperties(fname=font_path)
19
+ CHINESE_FONT = font_prop.get_name() # 例如 "Noto Sans TC"
20
+ else:
21
+ st.error(f"找不到字型檔: {font_path}")
22
+ CHINESE_FONT = "Noto Sans TC" # fallback
23
+
24
+ # 配置 Kaleido 的預設參數與額外字型設定
25
+ pio.kaleido.scope.default_font = CHINESE_FONT
26
+ pio.kaleido.scope.default_format = "png"
27
+ pio.kaleido.scope.default_width = 1200
28
+ pio.kaleido.scope.default_height = 900
29
+ pio.kaleido.scope.default_scale = 2
30
+ pio.kaleido.scope.extra_fonts = {CHINESE_FONT: font_path}
31
+
32
+ # ============================
33
+ # 系統字型檢查與安裝
34
+ # ============================
35
 
36
  def setup_chinese_font():
37
+ """檢查系統中文字型是否可用,若無則嘗試安裝"""
38
+ try:
39
+ test_fig, ax = plt.subplots()
40
+ ax.text(0.5, 0.5, "中文測試", fontfamily=CHINESE_FONT)
41
+ test_fig.savefig("font_test.png")
42
+ plt.close(test_fig)
43
+ except Exception as e:
44
+ st.warning("正在安裝中文字型...")
45
+ os.system('apt-get update && apt-get install -y --force-yes fonts-noto-cjk')
46
+ st.experimental_rerun()
47
+
48
+ # ============================
49
+ # 資料讀取
50
+ # ============================
51
+
52
+ def load_data(uploaded_file):
53
+ """從 CSV 檔案中讀取資料"""
54
+ try:
55
+ df = pd.read_csv(uploaded_file, encoding='utf-8').dropna(how='all')
56
+ if '姓名' not in df.columns:
57
+ raise ValueError("CSV檔案中缺少必要欄位:姓名")
58
+ numeric_columns = []
59
+ potential_numeric = ['平均', '總分', '國文', '英文', '數學', '自科',
60
+ '社會', '地理', '歷史', '公民', '物理', '化學', '生物']
61
+ for col in potential_numeric:
62
+ if col in df.columns:
63
+ try:
64
+ df[col] = pd.to_numeric(df[col], errors='coerce')
65
+ numeric_columns.append(col)
66
+ except:
67
+ pass
68
+ if not numeric_columns:
69
+ raise ValueError("CSV檔案中未找到有效的數值欄位")
70
+ return df, numeric_columns
71
+ except Exception as e:
72
+ st.error(f"數據加載錯誤:{str(e)}")
73
+ return None, None
74
+
75
+ # ============================
76
+ # 生成雷達圖
77
+ # ============================
78
+
79
+ def create_radar_chart(df, selected_rows, selected_columns, student_name=""):
80
+ """生成雷達圖 (包含圖例、極座標軸等設定)"""
81
+ fig = go.Figure()
82
+ line_styles = ['solid', 'dot', 'dash', 'longdash', 'dashdot']
83
+ colors = ['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD']
84
+
85
+ for i, row_name in enumerate(selected_rows):
86
+ row_data = df[df['姓名'] == row_name][selected_columns].iloc[0]
87
+ fig.add_trace(go.Scatterpolar(
88
+ r=row_data.values,
89
+ theta=selected_columns,
90
+ fill='toself',
91
+ name=row_name,
92
+ line=dict(
93
+ color=colors[i % len(colors)],
94
+ dash=line_styles[i % len(line_styles)],
95
+ width=2
96
+ ),
97
+ marker=dict(opacity=0.5)
98
+ ))
99
+
100
+ max_value = df[selected_columns].max().max() * 1.1 if selected_columns else 100
101
 
102
+ fig.update_layout(
103
+ polar=dict(
104
+ radialaxis=dict(
105
+ visible=True,
106
+ range=[0, max_value],
107
+ tickfont=dict(size=14, color='black', family=CHINESE_FONT)
108
+ ),
109
+ angularaxis=dict(
110
+ tickfont=dict(size=16, color='black', family=CHINESE_FONT),
111
+ rotation=20
112
+ )
113
+ ),
114
+ showlegend=True,
115
+ legend=dict(
116
+ font=dict(size=14, color='black', family=CHINESE_FONT),
117
+ orientation="v",
118
+ yanchor="middle",
119
+ xanchor="left",
120
+ x=1.2,
121
+ y=0.5
122
+ ),
123
+ title=dict(
124
+ text=f'{student_name} 成績比較圖' if student_name else '學生成績雷達圖',
125
+ font=dict(size=24, family=CHINESE_FONT),
126
+ x=0.5,
127
+ xanchor='center'
128
+ ),
129
+ margin=dict(t=80, b=120, r=200),
130
+ plot_bgcolor='white',
131
+ paper_bgcolor='white',
132
+ font=dict(family=CHINESE_FONT, size=14)
133
+ )
134
  return fig
135
 
136
+ # ============================
137
+ # 生成圖片
138
+ # ============================
139
+
140
  def generate_radar_image(fig):
141
+ """生成圖片 (重設 Kaleido 配置與更新字型設定)"""
142
+ try:
143
+ # 重設 kaleido 配置(含 extra_fonts)
144
+ pio.kaleido.scope.default_font = CHINESE_FONT
145
+ pio.kaleido.scope.default_format = "png"
146
+ pio.kaleido.scope.default_width = 1200
147
+ pio.kaleido.scope.default_height = 900
148
+ pio.kaleido.scope.default_scale = 2
149
+ pio.kaleido.scope.extra_fonts = {CHINESE_FONT: font_path}
150
+
151
+ # 強制更新圖表內所有文字字型設定
152
+ fig.update_layout(
153
+ font=dict(
154
+ family=CHINESE_FONT,
155
+ size=14
156
+ ),
157
+ title=dict(
158
+ font=dict(
159
+ family=CHINESE_FONT,
160
+ size=24
161
+ )
162
+ ),
163
+ legend=dict(
164
+ font=dict(
165
+ family=CHINESE_FONT,
166
+ size=14
167
+ )
168
+ ),
169
+ polar=dict(
170
+ radialaxis=dict(
171
+ tickfont=dict(
172
+ family=CHINESE_FONT,
173
+ size=14
174
+ )
175
+ ),
176
+ angularaxis=dict(
177
+ tickfont=dict(
178
+ family=CHINESE_FONT,
179
+ size=16
180
+ )
181
+ )
182
+ )
183
+ )
184
+
185
+ return pio.to_image(fig, format="png", engine="kaleido")
186
+ except Exception as e:
187
+ st.error(f"圖片生成失敗: {str(e)}")
188
+ return None
189
+
190
+ # ============================
191
+ # 主程式
192
+ # ============================
193
 
194
  def main():
195
+ st.title('學生成績雷達圖產生器')
196
+ st.markdown("""
197
+ <style>
198
+ .stDownloadButton>button {
199
+ background-color: #4CAF50 !important;
200
+ color: white !important;
201
+ }
202
+ .stProgress > div > div > div {
203
+ background-color: #4CAF50;
204
+ }
205
+ </style>
206
+ """, unsafe_allow_html=True)
207
 
208
+ setup_chinese_font() # 檢查中文字型
209
+
210
+ uploaded_file = st.file_uploader("上傳CSV檔案", type=['csv'])
211
  if uploaded_file is not None:
212
+ df, numeric_columns = load_data(uploaded_file)
213
+ if df is not None and numeric_columns:
214
+ st.write("### 選擇要比較的欄位")
215
+ preset_columns = ['平均', '國文', '英文', '數學', '自科', '社會']
216
+ use_preset = st.checkbox("使用五科分數與平均比較")
217
+ available_preset = [col for col in preset_columns if col in numeric_columns]
218
+ selected_columns = st.multiselect(
219
+ '選擇科目',
220
+ numeric_columns,
221
+ default=available_preset if use_preset else numeric_columns[:5]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  )
223
 
224
+ st.write("### 選擇要比較的對象")
225
+ all_students = df['姓名'].tolist()
226
+ selected_rows = st.multiselect('選擇學生', all_students)
227
+
228
+ if selected_columns and selected_rows:
229
+ try:
230
+ fig = create_radar_chart(df, selected_rows, selected_columns)
231
+ st.plotly_chart(fig, use_container_width=True)
232
+ except Exception as e:
233
+ st.error(f"生成雷達圖錯誤:{str(e)}")
 
 
 
 
 
 
 
 
 
 
234
 
235
+ st.write("### 批次下載全班學生雷達圖")
236
+ comparison_items = st.multiselect("選擇比較基準", all_students)
237
+ if comparison_items and selected_columns:
238
+ progress_bar = st.progress(0)
239
+ image_bytes = {}
240
+ current_columns = selected_columns.copy()
241
+ current_comparison = comparison_items.copy()
242
+ target_students = [s for s in all_students if s not in current_comparison]
243
+ for idx, student in enumerate(target_students):
244
+ try:
245
+ # 動態組合比較組(比較基準加上當前學生)
246
+ fig = create_radar_chart(
247
+ df,
248
+ current_comparison + [student],
249
+ current_columns,
250
+ student_name=student
251
+ )
252
+ img = generate_radar_image(fig)
253
+ if img:
254
+ image_bytes[student] = img
255
+ except Exception as e:
256
+ st.error(f"生成 {student} 圖表失敗:{str(e)}")
257
+ progress_bar.progress((idx + 1) / len(target_students))
258
+ if image_bytes:
259
+ st.write("### 圖表預覽")
260
+ cols = st.columns(3)
261
+ for i, (student, img) in enumerate(image_bytes.items()):
262
+ with cols[i % 3]:
263
+ st.image(img, use_container_width=True)
264
+ st.caption(f"{student} 比較圖")
265
+ zip_buffer = BytesIO()
266
+ with ZipFile(zip_buffer, "w") as zip_file:
267
+ for student, img in image_bytes.items():
268
+ zip_file.writestr(f"{student}_成績比較圖.png", img)
269
+ zip_buffer.seek(0)
270
+ st.download_button(
271
+ label="⬇️ 下載全部圖表 (ZIP)",
272
+ data=zip_buffer,
273
+ file_name="學生成績比較圖.zip",
274
+ mime="application/zip",
275
+ use_container_width=True
276
+ )
277
 
278
  if __name__ == "__main__":
279
  main()