ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用ECharts和Flask搭建学生成绩可视化分析系统

用ECharts和Flask搭建学生成绩可视化分析系统 期末考试成绩一出来班主任和任课老师最头疼的事就来了几百行Excel摆在那里单看表格能看出哪个班均分高、哪个题失分严重但要跟家长汇报、跟年级组做对比、找教学薄弱点就完全不够用了。我之前帮学校做过一个小工具把期末成绩做成可视化图表——班级对比、分数段分布、各科均衡度、及格率一眼看完。用到的核心工具就是数据可视化里最常用的ECharts后面顺手套了个Flask接口实现了从静态页面到后端动态数据的完整流程。这个案例不复杂适合刚接触数据可视化的同学、想给学校做分析工具的老师也适合想快速上手ECharts的开发者我把整个思路和代码完整写出来。1. 项目概述与整体思路1.1 学生成绩分析到底要解决什么问题先说说实际痛点。一个年级十几个班每班四五十人每个学生五六科成绩原始表格至少上千行。直接用Excel看最多能排排序、算个平均分但要做对比、找规律就很吃力。领导要的是一张图就能看出“哪个班语文弱”“哪段分数集中”“及格率多少”这些诉求用表格表达效率太低了。所以这个项目的核心不是“画图好看”而是把成绩数据变成能辅助决策的信息。我用真实场景来拆解需求一般要回答这几个问题哪个班级整体强、哪个班级偏科学生成绩集中在哪个分数段是正态分布还是有明显断层各科平均分、及格率横向对比哪科是短板个体层面哪些学生总分靠前、哪些需要重点关注这些问题听起来简单但用传统表格逐个回答要花不少时间。可视化之后鼠标扫一眼就出结论这也是数据可视化最常见的价值——不是炫技是缩短从数据到结论的距离。1.2 技术选型为什么是ECharts加轻量后端我见过有人一上来就用PowerBI、Tableau不是说不行但这类产品重、授权贵而且数据更新还要手动导。还有人用D3.js功能确实强但入门成本高写一个柱状图要折腾半天不适合“快速交付”的校内小工具。我最后选的是ECharts理由很直接配置式开发官方文档和示例非常全中文支持好遇到问题好搜。图表类型丰富柱状图、饼图、雷达图、仪表盘这类分析场景常用的全都有。底层是Canvas渲染几千条数据完全不卡成绩表这个数据量毫无压力。支持按需引入打包后体积可控。至于后端这个项目其实数据量不大开始时完全可以做成纯静态页面数据用JSON文件写死打开浏览器就能看。但为了演示真实工作流我加了Flask。用Flask不是因为它功能强而是因为它足够轻——起一个服务、写几个接口前后端分离的标准姿势就出来了。这也方便后续扩展成绩表一更新接口重新读Excel算一遍页面不用改。1.3 项目结构设计整个项目我拆成三条线也是这类可视化项目的通用套路数据层原始成绩表Excel或CSV用Python做聚合统计输出标准JSON。接口层Flask提供API把JSON数据返回给前端。展示层HTML页面通过ECharts把JSON渲染成图表。这三层做好分离好处很明显后端只负责算数据前端只负责画图。以后换图表类型改前端就行换数据源改后端就行。我见过很多新手把数据和图表逻辑全写在一个页面里明面上看着方便后面改一个字段就得翻半天得不偿失。还有一点要注意这个项目起步阶段我没直接上Flask而是先把统计结果存成静态JSON用前端直接读取确认图表效果没问题之后再加接口层。先小步跑通再逐步完善这个节奏对新手非常友好。2. 数据准备与核心统计口径2.1 原始成绩表长什么样数据准备阶段最枯燥但也最容易出问题。我手里的原始表大概长这样的结构学号姓名班级语文数学英语总分1001张同学1班9285782551002李同学1班7588902531003王同学2班657260197真实表格远没有这么干净。我拿到过的表里班级有写成“一(1)班”的也有写成“1-1”的成绩有缺考的也有填了0但实际不应计入统计的。所以数据处理的第一步就是把字段统一。这里我建议用pandas来做Python里处理表格最方便的工具。Excel、CSV都能直接读分组、聚合、分箱一套下来很顺手。如果不会pandas用纯Python读CSV也行但代码会啰嗦不少。2.2 用Python把成绩算成图表需要的JSON结构前面说了后端只负责输出前端想要的数据结构。那前端到底要什么我先列个清单班级平均分对比每个班、每科的平均分分数段分布总分落在各分数段的人数各科均衡度平均分最高最低之间的差距及格率总分及各科及格人数占比针对这些需求我用pandas逐个算。下面是核心代码import pandas as pd import json # 1. 读取原始成绩 df pd.read_excel(scores.xlsx) # 如果列名不规范先重命名 df.columns [学号, 姓名, 班级, 语文, 数学, 英语, 总分] # 2. 班级各科平均分 class_avg df.groupby(班级)[[语文, 数学, 英语, 总分]].mean().round(1) class_avg_json [ {班级: cls, **{col: val for col, val in row.items()}} for cls, row in class_avg.iterrows() ] # 3. 分数段分布 bins [0, 60, 70, 80, 90, 101] labels [0-59, 60-69, 70-79, 80-89, 90-100] df[分数段] pd.cut(df[总分], binsbins, labelslabels, rightFalse) score_dist df[分数段].value_counts().reindex(labels) score_dist_json [{分数段: k, 人数: int(v)} for k, v in score_dist.items()] # 4. 各科及格率 pass_rate {} for subject in [语文, 数学, 英语, 总分]: if subject 总分: threshold 180 # 假设三科满分300及格线180 else: threshold 60 pass_rate[subject] round((df[subject] threshold).mean() * 100, 1) # 5. 汇总输出 result { classAvg: class_avg_json, scoreDist: score_dist_json, passRate: pass_rate, } with open(stats.json, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2)这段代码跑完stats.json里就是前端直接能用的数据。我特别强调ensure_asciiFalse不写这个中文全变成\uXXXX虽然也能解析但你要调试的时候看着非常难受。2.3 统计口径的坑及格线、缺考、满分不一致数据可视化项目里“算错”往往不是代码错而是口径没统一。我踩过的坑有三个写出来提醒一下。第一个是及格线的定义。语文、数学、英语满分如果是100及格线60没问题但如果满分为150及格线就不是60了。总分也一样三科总分300和450的及格线完全不同。所以我在代码里对总分单独设置阈值不要想当然写死。第二个是缺考和0分的处理。缺考在Excel里通常显示为空读进pandas就成了NaN如果不处理计算平均分时该科会被跳过但算总分时又会出现偏差。我建议对缺考统一填充为0但单独标记一个“缺考人数”字段方便排查。第三个是百分制和非百分制的混合对比。有些学校英语有听力口语额外加分最高可能超过100这种数据跟语文数学放在同一张雷达图里对比量纲不一致会导致图形失真。处理方式要么做归一化要么在图表标题里明确标注。数据口径对了后面画图才有意义。我在这个环节花的时间比写前端图表还多但值得。数据可视化有个铁律图是数据的翻译器翻译错了不是图的错是数据源头没管好。3. ECharts核心图表实现3.1 图表选型的逻辑什么场景配什么图ECharts里图表类型几十种但成绩分析用得上的就那几类关键是搞清楚每种图适合回答什么问题。我自己的选择逻辑是这样的班级之间比平均分用柱状图最直观几根柱子高矮立判。看分数段人数分布用饼图或柱状图都行但我更推荐柱状图能更清楚地看到从低分到高分的走势。分析各科是否均衡用雷达图六边形形状一眼能看出偏科。展示及格率这种单个指标用仪表盘最形象数值和颜色配合非常直观。选图别贪多。我做这个项目的时候一开始想着把所有图都堆上去结果页面又长又乱自己都懒得看。后来砍到四张图每张图回答一个问题效果反而好。3.2 班级平均分对比柱状图先看最常用的柱状图。它的配置在ECharts里属于最基础的一档但细节也不少。我先写一个完整版本div idchart1 stylewidth: 100%; height: 360px;/divconst chart1 echarts.init(document.getElementById(chart1)); const classAvgData { classes: [1班, 2班, 3班, 4班], chinese: [82.5, 80.1, 84.3, 79.8], math: [78.2, 85.6, 80.4, 76.9], english: [81.4, 79.8, 82.1, 80.5], }; chart1.setOption({ title: { text: 各班级平均分对比, left: center, }, tooltip: { trigger: axis, }, legend: { top: 30, }, grid: { left: 10%, right: 10%, bottom: 8%, top: 20%, containLabel: true, }, xAxis: { type: category, data: classAvgData.classes, }, yAxis: { type: value, min: 60, max: 100, }, series: [ { name: 语文, type: bar, data: classAvgData.chinese, itemStyle: { color: #5470c6 }, }, { name: 数学, type: bar, data: classAvgData.math, itemStyle: { color: #91cc75 }, }, { name: 英语, type: bar, data: classAvgData.english, itemStyle: { color: #fac858 }, }, ], });写完这段代码我先讲两个新手容易忽略的配置。第一个是grid里的containLabel: true。如果不设置坐标轴标签可能被截断尤其是“班级”这个类目名比较长的时候。设置之后ECharts会自动把图表区域让出一部分给标签。第二个是yAxis的min和max。如果平均分都在75到85之间纵轴默认从0开始柱子之间差距就不明显。我手动设成60到100相当于把图放大了四倍高低差异看得更清楚。但这里要注意min和max不能设置太极端否则会误导看图的人以为1班数学只考了40分其实跟别人只差5分。所以我建议在图表标题或注释里明确说明“纵轴从60分起”让读者知道这是局部放大图。3.3 分数段分布用柱状图代替饼图更实用分数段分布最初我用的饼图但后来发现一个问题饼图只能看占比看不出从低分到高分的连续变化。换成柱状图之后不及格、低分、中分段、高分段的人数是递增还是递减一眼就有答案。const scoreDistData [ { scoreRange: 0-59, count: 12 }, { scoreRange: 60-69, count: 28 }, { scoreRange: 70-79, count: 54 }, { scoreRange: 80-89, count: 48 }, { scoreRange: 90-100, count: 22 }, ]; const chart2 echarts.init(document.getElementById(chart2)); chart2.setOption({ title: { text: 总分分数段人数分布, left: center, }, tooltip: { trigger: axis, }, xAxis: { type: category, data: scoreDistData.map((item) item.scoreRange), }, yAxis: { type: value, }, series: [ { type: bar, data: scoreDistData.map((item) item.count), itemStyle: { color: function (params) { const colors [#c23531, #e06f3c, #f3a74f, #5ab1ef, #2f7ed8]; return colors[params.dataIndex]; }, }, label: { show: true, position: top, }, }, ], });这段代码里的itemStyle.color用了回调函数根据数据的位置返回不同颜色。低分段用深红高分段用深蓝既好看又暗示了颜色的语义——看到红色就知道这个分数段有问题看到蓝色就知道是优秀区。分数段的分法也有讲究。我用的是小学科满分为100的常见分法不同学校可能分得更细比如每5分一档。但我的经验是档位太多图表会显得碎看不清趋势档位太少又丢失信息。70分到90分之间是学生最集中的区域建议在这个范围加密档位两端放宽。3.4 各科均衡度雷达图雷达图是我个人很喜欢的图表类型适合看“形状”。各科平均分如果量纲一致画出来越接近正五边形说明各科发展越均衡哪个角凹进去就说明哪科是短板。const radarData [ { name: 1班, value: [82.5, 78.2, 81.4, 90], }, { name: 2班, value: [80.1, 85.6, 79.8, 85], }, ]; const chart3 echarts.init(document.getElementById(chart3)); chart3.setOption({ title: { text: 各班级学科均衡度对比, left: center, }, tooltip: {}, radar: { indicator: [ { name: 语文, max: 100 }, { name: 数学, max: 100 }, { name: 英语, max: 100 }, { name: 总分均分, max: 100 }, ], radius: 65%, }, series: [ { type: radar, data: radarData, areaStyle: { opacity: 0.15, }, }, ], });雷达图的indicator里每个维度都要定义max值否则ECharts会按数据最大值自动缩放导致两个维度之间不可比。比如语文最高85、数学最高95如果不统一max语文的85会顶到满格数学的95反而只到一半图形就骗人了。所以雷达图数据规范化的第一步就是统一量纲。另外我加了一个areaStyle给多边形区域加半透明填充。多个班级的数据叠加在一起时区域重叠的部分能看出来配色虽然融合在一起但轮廓还是清晰的。如果班级太多超过五个雷达图就没法看了这种情况建议分成多张图每张图放三个班级以内。3.5 及格率仪表盘仪表盘在ECharts里属于“常用于单个指标展示”的图表我用来展示总分及格率效果非常直观。绿色区域大就是整体及格率高红色区域大就要警惕了。const passRate 85.4; const chart4 echarts.init(document.getElementById(chart4)); chart4.setOption({ title: { text: 总分及格率, left: center, }, series: [ { type: gauge, min: 0, max: 100, progress: { show: true, width: 18, }, axisLine: { lineStyle: { width: 18, color: [ [0.6, #c23531], [0.8, #e06f3c], [1, #2f7ed8], ], }, }, label: { formatter: {value}%, }, data: [ { value: passRate, name: 及格率, }, ], }, ], });仪表盘配置里最容易调的是axisLine.lineStyle.color。我用数组定义了渐变色段低于60%显示红色60%到80%显示橙色80%以上显示蓝色。这样不用读数字第一眼看到指针颜色就能判断整体情况。这里的及格率来自Python端已经算好的数值(df[总分] 180).mean() * 100。我把阈值180写死在Python里其实更好的方法是把阈值也作为变量传到前端这样如果调成150或者200前端不用改。4. 完整实战从静态页面到Flask动态接口4.1 先跑通静态版本用JSON文件模拟后端数据新手做可视化项目我强烈建议先不要碰后端。把Python算好的stats.json放在项目目录里前端用fetch或者直接引入先把图表全部画出来。这一步能让你迅速聚焦在“图表配置对不对”上不会被接口报错干扰。我当时的静态页面目录结构是这样的grade-visualization/ ├── index.html ├── stats.json ├── js/ │ └── charts.js └── lib/ └── echarts.min.jsindex.html里放四个div容器然后引入ECharts和自定义图表脚本。charts.js里四个echarts.init加上四个setOption从stats.json读取数据。这里有个前端加载的小技巧如果数据文件比较简单可以直接用script标签全局引入JSON数据比如在charts.js之前加一句script srcstats.json/script这样stats.json里的内容会被当作一个JS对象暴露出来charts.js里直接就能用。比fetch再解析Promise要省事得多。但要注意JSON文件里不能写注释末尾也不能有分号。4.2 Flask版本写API接口返回数据静态版本跑通之后再上Flask就非常顺手了。我新建一个app.py把之前统计逻辑封装成接口from flask import Flask, jsonify, render_template import pandas as pd app Flask(__name__) def load_data(): df pd.read_excel(scores.xlsx) # 这里直接复用之前的统计逻辑省略重复代码 result compute_stats(df) return result app.route(/) def index(): return render_template(index.html) app.route(/api/stats) def stats(): result load_data() return jsonify(result) if __name__ __main__: app.run(debugTrue)这里我有一个习惯把数据读取和统计逻辑写进独立的load_data函数根路由只负责返回页面/api/stats只负责返回JSON。这样接口职责单一以后要加缓存、要改成读数据库只要改load_data内部实现前端和路由都不用动。前端这时候要从stats.json改成fetch接口改法如下fetch(/api/stats) .then((res) res.json()) .then((data) { renderChart1(data.classAvg); renderChart2(data.scoreDist); renderChart3(data.subjectRadar); renderChart4(data.passRate); });4.3 运行与调试在项目根目录执行python app.py然后浏览器访问http://127.0.0.1:5000就能看到页面。这时候如果页面空白优先打开浏览器开发者工具看Network面板里/api/stats接口是不是返回了200返回数据结构和前端代码是否匹配。我用Flask做这类项目的心得是debugTrue模式开着后端改了代码会自动重载非常方便。但上线部署给其他人用的时候一定要把debug关掉否则端口暴露会有安全隐患。还有一个容易踩的坑pandas读Excel需要openpyxl库没装的话会报ImportError。别问我是怎么知道的。先执行pip install pandas openpyxl flask把依赖装齐了再运行。5. 常见问题与排查技巧实录5.1 高频排查清单我整理了做这个项目过程中遇到最多的问题做成一个速查表方便你直接对照排查现象可能原因排查方法页面空白控制台报ECharts未定义echarts.min.js没引入成功检查脚本引入路径是否正确CDN是否可用图表区域黑框/无内容容器div高度为0给div设置固定高度如height: 360px数据不显示接口返回的字段名和前端不一致打开Network看接口返回对照字段名中文显示乱码后端JSON没加ensure_asciiFalse在json.dump或jsonify时设置UTF-8柱状图顺序错乱分数段类目是文本默认按字母/拼音排序手动指定categories和data的顺序图表加载慢echarts.min.js体积大改用按需引入或使用CDN加速第一条“页面空白”其实是最常见的。很多人第一步就直接在浏览器里打开本地HTML文件然后发现ECharts可以正常显示但一旦加了Flask反而打不开了。这时候要检查是不是用127.0.0.1:5000访问的而不是直接双击HTML文件——跨域、路径问题都可能搅在一起。5.2 性能优化别小看ECharts的体积ECharts完整版大概1MB虽然对成绩数据来说不算什么但在弱网环境下加载还是会有白屏期。我建议正式部署时用按需引入的方式只打包用到的柱状图、饼图、雷达图、仪表盘四种图表类型体积能缩到原来的一半以下。用npm做按需引入的话核心代码是这样的import * as echarts from echarts/core; import { BarChart, PieChart, RadarChart, GaugeChart } from echarts/charts; import { GridComponent, TooltipComponent, LegendComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers; echarts.use([ BarChart, PieChart, RadarChart, GaugeChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer, ]);如果不方便用打包工具用CDN方式引入完整版也能接受毕竟这个项目只在校园网内用带宽压力不大。我的建议是自己学习时可以怎么方便怎么来正式布署前再考虑优化体积。5.3 用户体验小技巧最后分享几个提升体验的小细节都是我在实际使用中慢慢加上的。第一个是窗口缩放自适应。浏览器窗口一旦变化图表不会自动跟着调整所以要在页面里加一段监听resize事件的代码window.addEventListener(resize, () { chart1.resize(); chart2.resize(); chart3.resize(); chart4.resize(); });第二个是给图表加右上角的“下载图片”按钮。ECharts的toolbox配置里自带这个功能加上它老师要把图表放进汇报PPT一键就能存图toolbox: { feature: { saveAsImage: { show: true }, }, },第三个是异步加载的loading提示。数据接口如果比较慢最好在fetch之前调用timeout或loading效果不然用户盯着空白页还以为电脑卡了。ECharts自带showLoading方法调用chart.showLoading()再chart.hideLoading()就能实现。这些细节看起来小但体验差别很大。做工具类项目用户的耐心是很有限的页面能不能第一时间给出反馈往往决定了这工具是天天用还是用一次就扔。套用我自己总结的一个经验做学生成绩可视化这类项目最忌讳一上来就搞数据库、搞权限系统、搞得像大厂中台。一个期末成绩分析工具最快路径就是“读Excel - 算统计 - JSON给前端 - ECharts画图”。先把这条链路跑通再考虑要不要加登录、要不要存历史成绩做趋势对比。后面扩展方向倒是很多比如把每一次考试成绩存进数据库画一个学期成绩走势折线图或者按老师维度统计不同班级的教学效果对比。这个项目我是从一次期末十几张Excel表做起的做完之后同事都会来问一句“这图怎么弄的”其实核心就是那一套统计逻辑加四个图表配置耐心调一调谁都能做出来。
返回列表