尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python成绩数据分析与可视化:从Excel清洗到ECharts看板

Python成绩数据分析与可视化:从Excel清洗到ECharts看板 简介这是一份面向Python数据分析初学者与教学场景的学生成绩数据分析与可视化项目源码包围绕真实成绩数据集展开完整的数据处理流程。项目读取包含性别、种族、父母教育水平、午餐情况、备考课程及数学、阅读、写作成绩等字段的CSV文件先备份原始数据再用中位数填充成绩缺失值并汉化列名随后按种族、性别、父母教育水平、午餐类型、备考课程等维度计算平均成绩最后借助pandas、matplotlib、seaborn与mpld3绘制箱型图、饼图、直方图、散点图并将图表导出为HTML便于查看分享。资源共31个文件以13个txt说明、7个html可视化结果、3个md文档、3个csv数据及yml环境配置为主压缩包约76KB目录涵盖源码、数据备份与输出模块。已有101人学习适合想掌握数据清洗、分组统计与图表落地的读者参考复用。1. 从一份成绩单到一套分析系统Python 成绩数据分析与可视化到底在做什么期末考完教务老师甩过来一个文件夹里面是十几个 Excel每个班一份字段还不完全一样——有的叫「学号」有的叫「学生编号」有的成绩列里混着「缺考」「缓考」这种文本。你要在两天内交出一份能看的分析报告班级均分排名、分数段分布、单科与总分的相关性、临界生名单。手工透视表不是不能做但下周换一批数据你还得重来一遍。这就是「基于 Python 的学生成绩数据分析与可视化」要解决的真实场景把一次性的手工劳动变成一套可复用的脚本流程。它适合三类人一是学校里负责教务或质量分析的老师想摆脱重复的 Excel 操作二是刚学完 Python 基础、想找一个完整项目练手的学生成绩数据字段规整、业务含义直观是极好的练手素材三是需要给教学管理做数据支撑的开发者这套流程稍加改造就能接到 Flask 或 ECharts 前端上做成校园数据看板。核心链路其实就四步读取与清洗、统计与建模、可视化出图、导出报告。下面按这条链路拆开讲每一步都给能直接抄的代码和参数说明。2. 数据读取与清洗把脏 Excel 变成规整 DataFrame2.1 先看清成绩表的三种典型脏法拿到手的成绩表脏法基本逃不出三类。第一类是列名不统一同一个「语文」可能写成「语文成绩」「语文(150分)」「语文得分」。第二类是数值列混入文本缺考的写「缺考」免修的写「免修」还有的用「—」占位pandas 读进来整列就是 object 类型一算均值就报错。第三类是学号被当成数字前导零丢失或者学号里带字母导致整列类型混乱。处理顺序很关键先统一列名再处理数值列最后处理学号这类标识列。顺序反了会出现「改完列名发现数值列还是字符串」的返工。我一般会先写一个列名映射字典把各种变体收敛到标准名这一步做完后面所有代码都省心。2.2 用 pandas 读表并统一列名的完整脚本import pandas as pd import numpy as np # 列名映射左边是原始可能出现的写法右边是标准名 COLUMN_MAP { 学生编号: 学号, 学籍号: 学号, student_id: 学号, 姓名: 姓名, 学生姓名: 姓名, 语文成绩: 语文, 语文(150分): 语文, 语文得分: 语文, 数学成绩: 数学, 数学(150分): 数学, 英语成绩: 英语, 英语(150分): 英语, 班级: 班级, 所在班级: 班级, } def load_score_sheet(path: str, sheet_name0) - pd.DataFrame: # dtypestr 先全部按字符串读避免学号前导零丢失 df pd.read_excel(path, sheet_namesheet_name, dtypestr) df df.rename(columnslambda c: COLUMN_MAP.get(str(c).strip(), str(c).strip())) # 去掉全空行 df df.dropna(howall) return df df load_score_sheet(class_01.xlsx) print(df.dtypes) print(df.head())这段代码有三个关键点。dtypestr是血泪经验如果让 pandas 自动推断学号「0012」会变成整数 12后面按学号关联其他表时对不上。rename里用 lambda 配合字典没匹配到的列名原样保留不会因为映射表不全就丢列。dropna(howall)清掉 Excel 里常见的空行否则后面 groupby 会多出一个 NaN 分组。2.3 数值列清洗把「缺考」变成可控的 NaNSUBJECTS [语文, 数学, 英语] def clean_scores(df: pd.DataFrame) - pd.DataFrame: df df.copy() for col in SUBJECTS: if col not in df.columns: continue # 非数字字符统一转 NaN再转 float df[col] pd.to_numeric( df[col].astype(str).str.strip().replace( {缺考: np.nan, 免修: np.nan, —: np.nan, -: np.nan, : np.nan} ), errorscoerce ) # 总分只在三科都有成绩时才算避免缺考被当 0 分拉低均分 df[总分] df[SUBJECTS].sum(axis1, min_countlen(SUBJECTS)) return df df clean_scores(df) print(df[SUBJECTS [总分]].describe())errorscoerce是核心参数它把无法转换的值变成 NaN 而不是抛异常这样一份表里混着几种脏值也能一次跑完。min_countlen(SUBJECTS)这个参数容易被忽略默认sum会把 NaN 当 0缺考的学生总分就变成另外两科之和排名直接失真。设成 3 表示三科齐全才算总分缺一科总分就是 NaN后续统计时自动排除。提示清洗完一定要用describe()看一眼各科 min/max。如果语文最高分出现 1500 这种值多半是某行把「150」和「0」粘一起了属于录入错误得回头找原始表核对别在脚本里硬改。3. 统计分析与指标计算均分、排名、分数段和临界生3.1 班级维度的聚合统计清洗完的数据是一行一个学生要出班级报告就得按班级聚合。这里有个细节均分应该用mean()自动跳过 NaN而不是先fillna(0)再算否则缺考学生的 0 分会把班级均分拉低。排名用rank(methodmin, ascendingFalse)同分并列名次不跳号。def class_summary(df: pd.DataFrame) - pd.DataFrame: agg df.groupby(班级).agg( 人数(学号, count), 语文均分(语文, mean), 数学均分(数学, mean), 英语均分(英语, mean), 总分均分(总分, mean), ).round(2) # 按总分均分排名 agg[均分排名] agg[总分均分].rank(methodmin, ascendingFalse).astype(int) return agg.sort_values(均分排名) summary class_summary(df) print(summary)agg里每个字段指定聚合函数比循环写groupby().mean()更清晰也更快。round(2)放在聚合后避免中间过程反复舍入累积误差。如果班级数量多sort_values(均分排名)让输出直接就是排名顺序省得再排一次。3.2 分数段划分与临界生识别分数段是教学分析里最常被问的优秀、良好、及格、不及格各多少人。用pd.cut按总分切段注意rightFalse表示左闭右开避免 60 分到底算及格还是不及格的边界争议。临界生是另一个高频需求——总分在及格线上下 10 分以内的学生是最值得投入辅导资源的群体。def add_bands(df: pd.DataFrame, total_max450) - pd.DataFrame: df df.copy() # 按总分比例切段90% 优秀80% 良好60% 及格 bins [0, total_max * 0.6, total_max * 0.8, total_max * 0.9, total_max 1] labels [不及格, 及格, 良好, 优秀] df[分数段] pd.cut(df[总分], binsbins, labelslabels, rightFalse) # 临界生总分在及格线 ±10 分 line total_max * 0.6 df[是否临界生] df[总分].between(line - 10, line 10) return df df add_bands(df) band_count df.groupby([班级, 分数段], observedTrue).size().unstack(fill_value0) print(band_count) print(df[df[是否临界生]][[学号, 姓名, 班级, 总分]])observedTrue在 pandas 较新版本里能避免为未出现的分段生成空列输出更干净。between默认闭区间正好覆盖 ±10 的边界。临界生名单建议单独导出成一张表交给班主任比塞在总报告里更有用。3.3 单科相关性哪两科最该一起补教学上常想知道「数学好的学生语文是不是也好」用相关系数矩阵就能回答。df[SUBJECTS].corr()默认算 Pearson 相关系数值在 -1 到 1 之间越接近 1 说明两科成绩同向变化越明显。corr df[SUBJECTS].corr(methodpearson).round(3) print(corr) # 找出相关性最高的一对科目 import itertools pairs list(itertools.combinations(SUBJECTS, 2)) best max(pairs, keylambda p: abs(corr.loc[p[0], p[1]])) print(f相关性最强的一对{best}系数 {corr.loc[best[0], best[1]]})method可选pearson、spearman、kendall。成绩数据一般用 Pearson 就够如果分数段划分很粗、存在大量并列Spearman 更稳健。相关系数高不代表因果报告里措辞要谨慎说「存在同向变化趋势」而不是「一科好导致另一科好」。4. 可视化落地matplotlib 静态图与 ECharts 交互看板4.1 用 matplotlib 出四张标准分析图静态报告用 matplotlib 最省事四张图基本能覆盖需求班级均分柱状图、分数段堆叠柱状图、单科箱线图、相关性热力图。中文字体是第一个坑不设置就是一堆方框。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] # Windows 用黑体 matplotlib.rcParams[axes.unicode_minus] False # 负号正常显示 fig, axes plt.subplots(2, 2, figsize(14, 10)) # 图1班级总分均分 summary[总分均分].plot(kindbar, axaxes[0, 0], color#4C72B0) axes[0, 0].set_title(各班总分均分) axes[0, 0].set_ylabel(分数) # 图2分数段堆叠 band_count.plot(kindbar, stackedTrue, axaxes[0, 1], colormapRdYlGn) axes[0, 1].set_title(各班分数段分布) # 图3单科箱线图 df[SUBJECTS].plot(kindbox, axaxes[1, 0]) axes[1, 0].set_title(单科成绩分布) # 图4相关性热力图 im axes[1, 1].imshow(corr.values, cmapcoolwarm, vmin-1, vmax1) axes[1, 1].set_xticks(range(len(SUBJECTS)), SUBJECTS) axes[1, 1].set_yticks(range(len(SUBJECTS)), SUBJECTS) for i in range(len(SUBJECTS)): for j in range(len(SUBJECTS)): axes[1, 1].text(j, i, corr.values[i, j], hacenter, vacenter) fig.colorbar(im, axaxes[1, 1]) plt.tight_layout() plt.savefig(score_report.png, dpi150)font.sans-serif在 Linux 上要换成系统里实际存在的中文字体名比如WenQuanYi Micro Hei否则还是方框。dpi150是打印和屏幕都够用的折中值再高文件就大了。tight_layout()解决子图标题重叠比手动调subplots_adjust省事。4.2 用 pyecharts 生成可交互的 ECharts 看板如果报告要在浏览器里给多人看静态图不够ECharts 的交互悬浮看数值、点击筛选体验好很多。Python 侧用 pyecharts 生成 HTML不需要写前端代码。from pyecharts.charts import Bar, Pie, Grid from pyecharts import options as opts def build_class_bar(summary: pd.DataFrame) - Bar: bar ( Bar() .add_xaxis(summary.index.tolist()) .add_yaxis(总分均分, summary[总分均分].tolist()) .set_global_opts( title_optsopts.TitleOpts(title各班总分均分对比), yaxis_optsopts.AxisOpts(name分数), xaxis_optsopts.AxisOpts(name班级), ) ) return bar def build_band_pie(df: pd.DataFrame) - Pie: counts df[分数段].value_counts() pie ( Pie() .add(, [list(z) for z in zip(counts.index.tolist(), counts.values.tolist())]) .set_global_opts(title_optsopts.TitleOpts(title全校分数段占比)) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {c}人 ({d}%))) ) return pie bar build_class_bar(summary) pie build_band_pie(df) grid Grid().add(bar, grid_optsopts.GridOpts(pos_left5%, pos_right5%)) grid.render(score_dashboard.html)add_yaxis的第二个参数是数据列表顺序必须和 x 轴一致否则图和表对不上。formatter{b}: {c}人 ({d}%)里{b}是类目名、{c}是数值、{d}是百分比这是 ECharts 的模板语法改标签格式就改这里。render出来的 HTML 是自包含的直接发给别人双击就能看不依赖服务器。注意pyecharts 版本之间 API 有差异Grid的叠加用法在 1.x 和 2.x 上略有不同。如果add报错先确认版本别硬套网上的老代码。5. 避坑与排查成绩分析脚本最容易翻车的五个地方5.1 缺考被当成 0 分均分集体偏低现象班级数学均分算出来比教务手工算的低好几分排名也对不上。原因清洗时用了fillna(0)或者sum没设min_count缺考学生的 0 分参与了均值计算。解决数值列统一转 NaN聚合用mean()自动跳过总分用min_count控制。改完拿一个已知班级手工核对一遍均分确认一致再往下走。5.2 学号前导零丢失多表关联全错现象按学号把成绩表和学籍表 merge匹配上的行数少了一大截。原因读 Excel 时没指定dtypestr学号「0012」被读成整数 12和学籍表里的字符串「0012」对不上。解决所有含学号的表都用dtypestr读merge 前用astype(str).str.zfill(6)统一补零到固定长度。这个坑在跨表分析时几乎必踩。5.3 中文字体缺失图上全是方框现象脚本跑通没报错打开图片标题和轴标签全是「口口口」。原因matplotlib 默认字体不含中文且没配置 fallback。解决rcParams[font.sans-serif]设成系统里确实存在的中文字体。Windows 用SimHeimacOS 用Arial Unicode MSLinux 先fc-list :langzh查一下有哪些再填。设完记得axes.unicode_minusFalse否则负号也变方框。5.4 分数段边界把 60 分划错档现象明明 60 分应该及格结果被分到不及格。原因pd.cut默认rightTrue区间是左开右闭60 落在 (0,60] 里。解决显式设rightFalse让区间变成左闭右开 [0,60)60 就归到及格段。切完用value_counts()数一下各段人数和手工数对一遍。5.5 相关性当因果写进报告现象报告里写「数学成绩好导致英语成绩好」被教研组长打回来。原因相关系数只反映同向变化不说明因果可能两科都受学习习惯这个第三因素影响。解决措辞改成「数学与英语成绩存在较强同向变化趋势相关系数 0.72」把因果判断留给教学讨论。数据报告里措辞严谨比数字漂亮更重要。6. 从脚本到可复用工具参数化配置与结果校验写到这一步脚本能跑但每次换数据还得改代码里的文件名和科目列表这不算落地。真正能反复用的是把可变部分抽成配置再加一层结果校验防止数据换了、逻辑没跟上导致静默出错。先做配置化。把文件路径、科目列表、分数段比例、及格线这些全放进一个字典或 YAML脚本只读配置不写死。import yaml CONFIG { input_files: [class_01.xlsx, class_02.xlsx], subjects: [语文, 数学, 英语], total_max: 450, band_ratios: {及格: 0.6, 良好: 0.8, 优秀: 0.9}, critical_margin: 10, output: {image: report.png, html: dashboard.html}, } def load_config(pathNone): if path: with open(path, encodingutf-8) as f: return yaml.safe_load(f) return CONFIGband_ratios用比例而不是绝对分数换一套满分不同的考试比如单科 100 分制时只改total_max分段自动跟着变。critical_margin单独拎出来不同学校对临界生的定义不一样有的 ±5 分有的 ±15 分配置化之后不用动代码。再做结果校验。数据换了一批最怕的是脚本不报错但结果明显不对。加几个断言把常识性检查固化进去。def validate(df: pd.DataFrame, cfg: dict): subjects cfg[subjects] # 1. 各科分数不能超过满分 for col in subjects: assert df[col].max() cfg[total_max] / len(subjects) 1, f{col} 出现超满分值 # 2. 总分非空比例不能太低 valid_ratio df[总分].notna().mean() assert valid_ratio 0.8, f总分有效比例仅 {valid_ratio:.1%}检查缺考处理 # 3. 班级数不能为 0 assert df[班级].nunique() 0, 班级字段为空 print(f校验通过{len(df)} 条记录{df[班级].nunique()} 个班级总分有效 {valid_ratio:.1%})这三个断言覆盖了最常见的静默错误分数超范围说明清洗漏了脏值总分有效比例过低说明缺考处理有问题班级为空说明列名映射没匹配上。断言失败直接抛异常比生成一份错报告再被人发现要好得多。最后一个技巧是留一份基准数据做回归。我会把第一次人工核对无误的结果存成baseline.csv之后每次改脚本都跑一遍对比均分差异超过 0.01 就停下来查。这套流程我用了两年最大的教训是成绩数据的坑几乎都不在算法上而在「缺考怎么算」「边界怎么划」「学号怎么对齐」这些业务细节里。代码写得再漂亮一个fillna(0)就能让整份报告失去可信度。所以每次交付前我都会挑一个班手工算一遍均分和及格人数和脚本结果对上了才敢发出去。希望帮到你。本文还有配套的精品资源点击获取
返回列表