Lashtw commited on
Commit
98c7701
·
verified ·
1 Parent(s): b5fac9d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +190 -147
app.py CHANGED
@@ -4,204 +4,247 @@ import plotly.graph_objects as go
4
  import plotly.io as pio
5
  from zipfile import ZipFile
6
  from io import BytesIO
 
 
7
 
8
  # ================= 全局配置 =================
9
- FONT_NAME = "Noto Sans CJK TC" # 精确匹配系统安装的字体名称
 
 
 
10
 
11
  # 配置Kaleido(图片导出引擎)
12
  pio.kaleido.scope.default_format = "png"
13
  pio.kaleido.scope.default_width = 1200
14
  pio.kaleido.scope.default_height = 900
15
  pio.kaleido.scope.default_scale = 2
16
- pio.kaleido.scope.default_font = FONT_NAME # 关键配置
17
 
18
  # ================= 核心函数 =================
19
  def load_data(uploaded_file):
20
- """优化数据加载与验证"""
21
  try:
22
- df = pd.read_csv(uploaded_file, encoding='utf-8').dropna(how='all')
 
 
 
 
 
 
 
23
 
24
- # 数据验证
25
- if '姓名' not in df.columns:
26
- raise ValueError("CSV文件中缺少必要欄位:姓名")
27
-
28
  # 自动识别数值列
29
- numeric_columns = []
30
- potential_numeric = ['平均', '總分', '國文', '英文', '數學', '自科',
31
- '社會', '地理', '歷史', '公民', '物理', '化學', '生物']
32
- for col in potential_numeric:
33
- if col in df.columns:
34
- try:
35
- df[col] = pd.to_numeric(df[col], errors='coerce')
36
- numeric_columns.append(col)
37
- except:
38
- pass
39
-
40
- if not numeric_columns:
41
- raise ValueError("CSV文件中未找到有效的數值欄位")
42
 
43
- return df, numeric_columns
44
  except Exception as e:
45
  st.error(f"數據加載錯誤:{str(e)}")
46
  return None, None
47
 
48
  def create_radar_chart(df, selected_rows, selected_columns, student_name=""):
49
- """生成强化中文支持的雷达图"""
50
  colors = ['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD']
51
- line_styles = ['solid', 'dot', 'dash', 'longdash', 'dashdot']
52
-
53
  fig = go.Figure()
54
-
55
- # 添加每个学的雷达轨迹
56
- for i, name in enumerate(selected_rows):
57
- student_data = df[df['姓名'] == name][selected_columns].iloc[0]
58
- fig.add_trace(go.Scatterpolar(
59
- r=student_data.values,
60
- theta=selected_columns,
61
- fill='toself',
62
- name=name,
63
- line=dict(
64
- color=colors[i % len(colors)],
65
- dash=line_styles[i % len(line_styles)],
66
- width=2
 
67
  )
68
- ))
69
-
70
- # 动态调整最大值
71
- max_value = df[selected_columns].max().max() * 1.1 if selected_columns else 100
72
-
73
- # 统一字体配置
 
74
  fig.update_layout(
75
  polar=dict(
76
  radialaxis=dict(
77
- visible=True,
78
  range=[0, max_value],
79
- tickfont=dict(size=14, family=FONT_NAME)
80
  ),
81
  angularaxis=dict(
82
- tickfont=dict(size=16, family=FONT_NAME),
83
- rotation=20 # 调整标签旋转角度
84
  )
85
  ),
86
  showlegend=True,
87
  legend=dict(
88
- font=dict(family=FONT_NAME, size=14),
89
- orientation="v",
90
  x=1.2,
91
  y=0.5
92
  ),
93
- title=dict(
94
- text=f'{student_name} 成績比較圖' if student_name else '學生成績雷達圖',
95
- font=dict(size=24, family=FONT_NAME),
96
- x=0.5
97
- ),
98
- margin=dict(t=80, b=120, r=200), # 增加右侧边距
99
- font=dict(family=FONT_NAME, size=14)
100
  )
 
 
 
 
101
  return fig
102
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  # ================= 主界面逻辑 =================
104
  def main():
 
 
 
 
 
 
105
  st.title('學生成績雷達圖產生器')
106
- st.markdown("""
107
- <style>
108
- .stDownloadButton>button { background-color: #4CAF50 !important; color: white !important; }
109
- .stProgress > div > div > div { background-color: #4CAF50; }
110
- </style>
111
- """, unsafe_allow_html=True)
112
-
113
  uploaded_file = st.file_uploader("上傳CSV檔案", type=['csv'])
114
-
115
- if uploaded_file is not None:
116
- df, numeric_columns = load_data(uploaded_file)
117
 
118
- if df is not None and numeric_columns:
119
- # ===== 科目选择区块 =====
120
- st.write("### 選擇要比較的欄位")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
- # 快速选择设科目
123
- preset_columns = ['平均', '國文', '英文', '數學', '自科', '社會']
124
- use_preset = st.checkbox("使用五科分數與平均比較")
125
- available_preset = [col for col in preset_columns if col in numeric_columns]
126
 
127
- selected_columns = st.multiselect(
128
- '選擇科目',
129
- options=numeric_columns,
130
- default=available_preset if use_preset else numeric_columns[:5]
131
- )
132
-
133
- # ===== 学生选择区块 =====
134
- st.write("### 選擇要比較的對象")
135
- all_students = df['姓名'].tolist()
136
- selected_students = st.multiselect('選擇學生', all_students)
137
-
138
- # 实时显示雷达图
139
- if selected_columns and selected_students:
140
- try:
141
- fig = create_radar_chart(df, selected_students, selected_columns)
142
- st.plotly_chart(fig, use_container_width=True)
143
- except Exception as e:
144
- st.error(f"生成雷達圖錯誤:{str(e)}")
145
-
146
- # ===== 批量下载区块 =====
147
- st.write("### 批次下載全班學生雷達圖")
148
- baseline_students = st.multiselect("選擇比較基準", all_students)
149
-
150
- if baseline_students and selected_columns:
151
- progress_bar = st.progress(0)
152
- image_data = {}
153
- total = len([s for s in all_students if s not in baseline_students])
154
-
155
- for idx, student in enumerate(all_students):
156
- if student in baseline_students:
157
- continue
158
-
159
  try:
160
- # 生成每个学生的对比图
161
- fig = create_radar_chart(
162
- df,
163
- baseline_students + [student],
164
- selected_columns,
165
- student_name=student
166
- )
167
-
168
- # 强制刷新字体配置
169
- fig.update_layout(font_family=FONT_NAME)
170
-
171
- # 生成图片字节数据
172
- img_bytes = pio.to_image(fig, format="png", engine="kaleido")
173
- image_data[student] = img_bytes
174
  except Exception as e:
175
- st.error(f"生成 {student} 圖表失敗:{str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
- progress_bar.progress((idx+1)/len(all_students))
178
-
179
- # 显示预览
180
- if image_data:
181
- st.write("### 圖表預覽")
182
- cols = st.columns(3)
183
- for i, (name, img) in enumerate(image_data.items()):
184
- with cols[i % 3]:
185
- st.image(img, use_container_width=True)
186
- st.caption(f"{name} 比較圖")
187
-
188
- # 打包下载
189
- zip_buffer = BytesIO()
190
- with ZipFile(zip_buffer, "w") as zip_file:
191
- for name, data in image_data.items():
192
- zip_file.writestr(
193
- f"{name}_成績比較圖.png",
194
- data
195
  )
196
- zip_buffer.seek(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
- st.download_button(
199
- label="⬇️ 下載全部圖表 (ZIP)",
200
- data=zip_buffer,
201
- file_name="學生成績比較圖.zip",
202
- mime="application/zip",
203
- use_container_width=True
204
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  if __name__ == "__main__":
207
  main()
 
4
  import plotly.io as pio
5
  from zipfile import ZipFile
6
  from io import BytesIO
7
+ import time
8
+ import psutil
9
 
10
  # ================= 全局配置 =================
11
+ FONT_NAME = "Noto Sans CJK TC" # 根据构建日志验证实际字体名称
12
+ MAX_STUDENTS = 50 # 最大处理学生数
13
+ BATCH_SIZE = 5 # 每批处理数量
14
+ DELAY_SECONDS = 5 # 批处理间隔时间
15
 
16
  # 配置Kaleido(图片导出引擎)
17
  pio.kaleido.scope.default_format = "png"
18
  pio.kaleido.scope.default_width = 1200
19
  pio.kaleido.scope.default_height = 900
20
  pio.kaleido.scope.default_scale = 2
21
+ pio.kaleido.scope.default_font = FONT_NAME
22
 
23
  # ================= 核心函数 =================
24
  def load_data(uploaded_file):
25
+ """优化内存使用的数据加载"""
26
  try:
27
+ # 使用分块读取优化大文件处理
28
+ df = pd.read_csv(uploaded_file,
29
+ encoding='utf-8',
30
+ dtype={'姓名': 'category'},
31
+ usecols=lambda col: col in ['姓名', '平均', '總分', '國文', '英文', '數學', '自科', '社會'])
32
+
33
+ # 清理无效数据
34
+ df = df.dropna(subset=['姓名']).head(MAX_STUDENTS)
35
 
 
 
 
 
36
  # 自动识别数值列
37
+ numeric_cols = df.select_dtypes(include='number').columns.tolist()
38
+ if not numeric_cols:
39
+ raise ValueError("未找到有效的數值欄位")
 
 
 
 
 
 
 
 
 
 
40
 
41
+ return df, numeric_cols
42
  except Exception as e:
43
  st.error(f"數據加載錯誤:{str(e)}")
44
  return None, None
45
 
46
  def create_radar_chart(df, selected_rows, selected_columns, student_name=""):
47
+ """高效生成雷达图"""
48
  colors = ['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD']
49
+
 
50
  fig = go.Figure()
51
+
52
+ # 使用成器减少内存占用
53
+ def generate_traces():
54
+ for i, name in enumerate(selected_rows):
55
+ student_data = df[df['姓名'] == name][selected_columns].iloc[0]
56
+ yield go.Scatterpolar(
57
+ r=student_data.values,
58
+ theta=selected_columns,
59
+ fill='toself',
60
+ name=name,
61
+ line=dict(
62
+ color=colors[i % len(colors)],
63
+ width=2
64
+ )
65
  )
66
+
67
+ fig.add_traces(list(generate_traces()))
68
+
69
+ # 动态计算最大值
70
+ max_value = df[selected_columns].max().max() * 1.1
71
+
72
+ # 优化布局配置
73
  fig.update_layout(
74
  polar=dict(
75
  radialaxis=dict(
 
76
  range=[0, max_value],
77
+ tickfont_size=14
78
  ),
79
  angularaxis=dict(
80
+ tickfont_size=16,
81
+ rotation=20
82
  )
83
  ),
84
  showlegend=True,
85
  legend=dict(
 
 
86
  x=1.2,
87
  y=0.5
88
  ),
89
+ margin=dict(t=50, b=100, r=200),
90
+ font_family=FONT_NAME,
91
+ title_font_size=24,
92
+ title_x=0.5
 
 
 
93
  )
94
+
95
+ if student_name:
96
+ fig.update_layout(title_text=f"{student_name} 成績比較圖")
97
+
98
  return fig
99
 
100
+ # ================= 资源监控 =================
101
+ def show_system_stats():
102
+ """显示系统资源状态"""
103
+ mem = psutil.virtual_memory()
104
+ stats = f"""
105
+ | 内存使用 | {mem.percent}% |
106
+ | CPU使用 | {psutil.cpu_percent()}% |
107
+ | 处理学生数 | {len(st.session_state.get('processed_students', []))} |
108
+ """
109
+ st.sidebar.markdown("### 系统状态")
110
+ st.sidebar.markdown(stats)
111
+
112
  # ================= 主界面逻辑 =================
113
  def main():
114
+ st.set_page_config(page_title="成績分析系統", layout="wide")
115
+
116
+ # 初始化会话状态
117
+ if 'processed_students' not in st.session_state:
118
+ st.session_state.processed_students = []
119
+
120
  st.title('學生成績雷達圖產生器')
121
+
122
+ # 文件上传区块
 
 
 
 
 
123
  uploaded_file = st.file_uploader("上傳CSV檔案", type=['csv'])
124
+
125
+ if uploaded_file:
126
+ df, numeric_cols = load_data(uploaded_file)
127
 
128
+ if df is not None and numeric_cols:
129
+ # ===== 配置区块 =====
130
+ with st.sidebar:
131
+ st.header("分析設定")
132
+
133
+ # 科目选择
134
+ use_preset = st.checkbox("使用預設科目", True)
135
+ preset_cols = ['平均', '國文', '英文', '數學', '自科', '社會']
136
+ available_preset = [c for c in preset_cols if c in numeric_cols]
137
+ selected_cols = st.multiselect(
138
+ "選擇科目",
139
+ options=numeric_cols,
140
+ default=available_preset if use_preset else numeric_cols[:3]
141
+ )
142
+
143
+ # 基准学生选择
144
+ baseline_students = st.multiselect(
145
+ "選擇比較基準",
146
+ options=df['姓名'].unique(),
147
+ help="選擇要作為比較基準的學生"
148
+ )
149
 
150
+ # ===== 实时览区块 =====
151
+ st.subheader("即時預覽")
152
+ preview_col1, preview_col2 = st.columns([3, 1])
 
153
 
154
+ with preview_col1:
155
+ selected_students = st.multiselect(
156
+ "選擇預覽學生",
157
+ options=df['姓名'].unique(),
158
+ max_selections=3
159
+ )
160
+
161
+ if selected_students and selected_cols:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  try:
163
+ fig = create_radar_chart(df, selected_students, selected_cols)
164
+ st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
165
  except Exception as e:
166
+ st.error(f"預覽生成失敗:{str(e)}")
167
+
168
+ # ===== 批量处理区块 =====
169
+ st.divider()
170
+ st.subheader("批次輸出")
171
+
172
+ if baseline_students and selected_cols:
173
+ # 初始化批处理
174
+ all_students = [s for s in df['姓名'] if s not in baseline_students]
175
+ total_batches = (len(all_students) + BATCH_SIZE - 1) // BATCH_SIZE
176
+
177
+ # 进度控制
178
+ progress_bar = st.progress(0)
179
+ status_text = st.empty()
180
+ download_placeholder = st.empty()
181
+
182
+ image_data = {}
183
+
184
+ # 分批处理
185
+ for batch_num in range(total_batches):
186
+ start_idx = batch_num * BATCH_SIZE
187
+ end_idx = start_idx + BATCH_SIZE
188
+ batch = all_students[start_idx:end_idx]
189
 
190
+ # 生成当前批次
191
+ for student in batch:
192
+ try:
193
+ start_time = time.time()
194
+
195
+ fig = create_radar_chart(
196
+ df,
197
+ baseline_students + [student],
198
+ selected_cols,
199
+ student_name=student
 
 
 
 
 
 
 
 
200
  )
201
+
202
+ # 优化图像生成
203
+ img_bytes = pio.to_image(fig, format="png", engine="kaleido")
204
+ image_data[student] = img_bytes
205
+ st.session_state.processed_students.append(student)
206
+
207
+ # 更新状态
208
+ elapsed = time.time() - start_time
209
+ status_text.info(f"""
210
+ 正在處理:{student}
211
+ 已用時間:{elapsed:.1f}秒
212
+ 剩餘批次:{total_batches - batch_num - 1}
213
+ """)
214
+
215
+ except Exception as e:
216
+ st.error(f"學生 {student} 處理失敗:{str(e)}")
217
+
218
+ # 更新进度
219
+ progress = (batch_num + 1) / total_batches
220
+ progress_bar.progress(progress)
221
 
222
+ # 添加延迟避免资源耗尽
223
+ if batch_num < total_batches - 1:
224
+ time.sleep(DELAY_SECONDS)
225
+
226
+ # 完成处理
227
+ progress_bar.empty()
228
+ status_text.success(f"處理完成!共生成 {len(image_data)} 張圖表")
229
+
230
+ # 打包下载
231
+ if image_data:
232
+ with BytesIO() as zip_buffer:
233
+ with ZipFile(zip_buffer, 'w') as zip_file:
234
+ for name, data in image_data.items():
235
+ zip_file.writestr(f"{name}.png", data)
236
+
237
+ zip_buffer.seek(0)
238
+ download_placeholder.download_button(
239
+ "⬇️ 下載全部圖表",
240
+ data=zip_buffer,
241
+ file_name="scores_analysis.zip",
242
+ mime="application/zip",
243
+ use_container_width=True
244
+ )
245
+
246
+ # 显示系统状态
247
+ show_system_stats()
248
 
249
  if __name__ == "__main__":
250
  main()