数据清洗到可视化:Python第二次作业完整流程与避坑指南
“第二次作业”这四个字乍一听平平无奇可过来人都知道第一次作业大家还在熟悉环境彼此差距拉不开第三次往后逐渐摸到门道也知道该问谁了唯独第二次最容易稀里糊涂地翻车——它要求你把整套流程完整走通又默认你已经具备了基本工具链。我这学期就接了一份典型的编程类第二次作业拿到一份几百行的成绩表做数据清洗、统计计算、绘制图表最后输出一份结论。看起来都是基础操作真正做下来才发现坑都在细节里。这篇文章就把我这次的完整做法、踩过的坑、调通之后才明白的道理原原本本分享出来。正在做课程作业的同学或者刚开始接触 Python 数据分析的自学者都可以直接照着这套思路走。1. 先别急着写代码把“第二次作业”拆成验收清单1.1 为什么第二次作业最容易翻车很多人拿到作业的第一反应是打开编辑器直接开写这个习惯在第一次作业还能侥幸过关第二次就悬了。因为第二次作业考察的往往不是“某个功能能不能实现”而是“整个流程你能不能完整走下来”。我这次的任务表面上是“统计成绩并画图”实际上包含了很多没写在明面上的要求数据读取要稳定不能换个环境就报编码错误清洗逻辑要合理不是简单dropna()一把梭统计指标要能解释清楚不是只输出数字图表要让人看得懂标题、轴标签、图例缺一不可最终结论必须能从数据里找到依据。我拿到题目后没有急着写代码而是先花二十分钟把要求拆成了五个验收点每条对应一个技术动作。比如“去除重复记录”对应drop_duplicates()“处理缺失值”要提前决定策略“计算及格率”需要写布尔筛选。这样拆完心里基本有数了写起来也不会东一榔头西一棒子。这个习惯强烈建议养成。因为作业检查时最怕被问“你这里为什么要用fillna()而不是直接删掉”如果答不上来代码再漂亮也白搭。拆解任务的过程其实就是逼自己想清楚每个操作背后的业务含义。1.2 把题目要求翻译成技术动作我当时的题目是给定一份学生成绩 CSV字段包括学号、班级、姓名、实验成绩满分 40、期末成绩满分 60、总评成绩共 580 行数据。要求完成数据清洗统计各班的平均分、标准差、及格率、优秀率并绘制三张图总评分布直方图、实验成绩与期末成绩散点图、各班总评箱线图。拆完之后我的任务清单长这样题目要求技术动作预期产物读取数据pd.read_csv()DataFrame清洗数据drop_duplicates()、缺失值处理、异常值过滤干净数据集统计指标mean()、std()、布尔筛选统计表三张图表hist()、scatter()、boxplot()PNG 图片结论根据统计和图表输出文字分析这样一列事情立刻清晰了数据读取和数据清洗是地基地基本来就不稳后面统计和画图全是空中楼阁。所以我决定把主要精力放在前两步。1.3 环境准备与数据摸底环境方面我用了 Anaconda 自带的 Python 3.9再加 pandas、numpy、matplotlib 三个库。如果你用的是纯净 Python可以一条命令装齐pip install pandas numpy matplotlib装好后先别急着做操作把数据读进来摸个底。这步我没用任何花哨写法就是最朴素的四件套import pandas as pd import numpy as np import matplotlib.pyplot as plt raw pd.read_csv(scores.csv, encodingutf-8) print(raw.shape) print(raw.head()) print(raw.info())shape看行列数head()看前几行内容info()看每列类型和缺失情况。这一步能帮你快速判断数据干不干净比如info()里某列非空数量小于总行数说明有缺失列类型显示object而应该是数值说明有脏数据。我第一次跑info()就发现期末成绩是object类型里面混进了个别无法转成数字的文本这就是后面必须处理的点。2. 数据读取与清洗最容易踩坑的一公里2.1 读取CSV的两个经典坑第一个坑是编码。read_csv默认按utf-8读但很多从 Excel 另存出来的文件是gbk编码直接读会报UnicodeDecodeError。我第一次读的时候就撞上了换成encodinggbk立刻通了。建议的做法是先试utf-8报错了就换gbk再不行就用encodinggbk, errorsignore应急但事后一定要查一下哪行数据有问题。第二个坑是路径和工作目录。在 Jupyter Notebook 里跑代码相对路径是相对于.ipynb文件所在目录而不是你当前在终端里看到的路径。如果你把数据放在data/scores.csv读取时就要写data/scores.csv不要只写文件名。如果实在不确定可以用os.getcwd()打印当前工作目录再用os.path.join()拼路径。这个小动作能省掉很多无意义的报错。读入之后我还做了两件事一是用raw.head()确认列名有没有隐藏空格。很多 CSV 的表头会带上空格比如期末成绩 直接写df[期末成绩]会报KeyError。我那次就遇到一列叫总评成绩 肉眼完全看不出来差点被带偏。解决办法是用raw.columns.tolist()打印出来逐字看或者用raw.columns raw.columns.str.strip()去掉首尾空格。2.2 清洗顺序和边界规则数据清洗最忌讳一上来就dropna()然后自认为“干净了”。我的习惯是先备份原始数据再按固定顺序清洗。df raw.copy() print(清洗前行数:, df.shape[0]) # 第一步去重学号唯一 df df.drop_duplicates(subset[学号]) print(去重后行数:, df.shape[0]) # 第二步删除成绩为空的行 df df.dropna(subset[实验成绩, 期末成绩, 总评成绩]) print(删缺失后行数:, df.shape[0]) # 第三步数值类型转换 df[实验成绩] pd.to_numeric(df[实验成绩], errorscoerce) df[期末成绩] pd.to_numeric(df[期末成绩], errorscoerce) df[总评成绩] pd.to_numeric(df[总评成绩], errorscoerce)errorscoerce这个参数很关键它能把无法转换的值变成NaN而不会让整列读取失败。转换完再dropna()那些混进去的脏文本就被清掉了。清洗顺序为什么这么重要因为去重和删缺失在前类型转换在后后面还会做异常值过滤。一旦顺序颠倒比如先过滤异常值再删缺失可能会出现明明分数越界却因为缺失被保留的情况逻辑上就乱了。至于异常值规则要结合业务实验成绩满分 40期末满分 60那么分数小于 0 或超过满分的就是异常。我用的是区间过滤df df[(df[实验成绩] 0) (df[实验成绩] 40)] df df[(df[期末成绩] 0) (df[期末成绩] 60)] print(过滤异常后行数:, df.shape[0])全部跑完我注意到清洗前是 580 行清洗后剩 552 行说明有 28 行是重复、缺失或异常的。这个数字记得保留写分析结论的时候用得上——它能直接证明你的清洗步骤是必要的而不是走过场。2.3 清洗完成后的自检方法清洗完别急着统计先自检三件套看行列数、看数据类型、看描述性统计。我当时的做法是这样的print(df.shape) print(df.dtypes) print(df.describe())df.describe()会输出每列计数、均值、标准差、最小值、四分位数和最大值。如果最大值超出了满分说明异常值没删干净如果count小于行数说明还有缺失没处理。这三样检查完数据才算真正“能用”了。我后来还养成了一个习惯清洗过程中每做一步就打印一次行数或者用df.shape[0]记下来。这样每一步删掉多少行、为什么删全都有记录。写报告的时候直接引用这些数字答辩时老师问起来也说得清楚。3. 统计指标计算别只会调用mean()和std()3.1 哪些指标必须算指标背后说明了什么作业要求算平均分、标准差、及格率、优秀率。这几个指标不是随便选的它们分别回答不同的问题平均分回答“整体水平怎么样”标准差回答“大家分数是聚在一起还是两极分化”及格率回答“有多少人没迈过 60 分门槛”优秀率回答“拔尖的人多不多”。光看平均分很容易被误导。比如两个班平均分都是 68但一个班标准差是 5另一个班是 15说明前者分数很集中后者有人考 90 多分也有人考 50 分左右。标准差大不是好事它意味着教学效果不稳定。这些解读才是作业里真正值钱的部分。计算本身不复杂我的代码是这样的for col in [实验成绩, 期末成绩, 总评成绩]: mean_val df[col].mean() std_val df[col].std() median_val df[col].median() print(f{col}: 平均值{mean_val:.2f}, 标准差{std_val:.2f}, 中位数{median_val:.2f})注意这里我给的是median()也一起算因为中位数和平均数的差异能反映数据是否偏斜。如果中位数明显低于平均数说明高分段拉高了均值如果中位数明显高于平均数说明低分段拖了后腿。3.2 分组统计与比率计算的写法统计不能只算整体还要分班比较。这一步用groupby()做分组聚合grouped df.groupby(班级)[总评成绩].agg([mean, std, median]) print(grouped)agg()一次可以算多个指标比挨个groupby().mean()高效很多。算出来的结果放到 Excel 或直接打印成表班级之间的差异一目了然。及格率和优秀率的写法更绕一些核心是布尔筛选加求平均pass_rate (df[总评成绩] 60).mean() * 100 excellent_rate (df[总评成绩] 85).mean() * 100 print(f及格率: {pass_rate:.1f}%, 优秀率: {excellent_rate:.1f}%)这里有个小技巧布尔序列的mean()就是“True 占全部数据的比例”不必先sum()再除以len()。这样写既简洁也不容易出现分母写错的问题。分组后的及格率可以按班级分别算for cls in df[班级].unique(): sub df[df[班级] cls] rate (sub[总评成绩] 60).mean() * 100 print(f{cls} 及格率: {rate:.1f}%)3.3 统计结果的解读与报告落点统计数字本身不产生价值解读才有。我当时观察到整体标准差明显偏大就回头看了分数分布发现部分学生成绩集中在低分段于是写了一句话“该课程整体掌握情况存在明显分层低分群体拉低了及格率”。这句话不是拍脑袋而是从标准差和直方图里读出来的。写报告时每个统计指标都要对应一句解释不能只摆数字。比如“平均分 71.2说明整体水平中等偏上但标准差 14.8 说明高分和低分差距很大”这种写法才算把数据用起来了。我在统计阶段还做了一张汇总表把每个班的总评平均分、标准差、及格率、优秀率放在一起方便最后写结论。表格比文字直观得多也算是在交作业时的一个加分项。4. 可视化部分图表的选型、参数和排版4.1 什么场景选什么图这次的作业要求三张图每张图的选型其实都有讲究。总评分布用直方图hist因为它能直观反映分数集中在哪个区间、是否呈正态分布实验成绩和期末成绩的关系用散点图scatter用来观察两个变量是否存在相关趋势各班总评对比用箱线图boxplot因为箱线图最能表现中位数、四分位数和离群点多个班并排放置时对比效果非常强。选图的原则就一句话你想表达什么观点就选最能支撑这个观点的图。比如想说明“大多数同学分数集中在 70-80”用直方图最合适想说明“某班中位数比另一班高但离散程度也大”箱线图是这个问题的标准答案。别为了好看去画饼图饼图只适合展示占比结构用在这里毫无意义。4.2 中文字体和绘图参数设置matplotlib 默认不支持中文不设置的话标题会变成方块。我的做法是在绘图前做全局设置plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] FalseSimHei是 Windows 自带的黑体macOS 上可以换成Arial Unicode MSLinux 则需要系统安装中文字体。axes.unicode_minus设为False是为了让负号正常显示不然坐标轴上的负号会显示成乱码。直方图的参数我建议多调几个才能出效果bins np.arange(0, 101, 10) plt.figure(figsize(8, 5)) plt.hist(df[总评成绩], binsbins, color#4C72B0, alpha0.7, edgecolorwhite) plt.title(总评成绩分布直方图) plt.xlabel(分数段) plt.ylabel(人数) plt.xticks(bins) plt.grid(axisy, linestyle--, alpha0.4) plt.show()bins这里我用了np.arange(0, 101, 10)也就是 0-10、10-20……每 10 分一组共 10 组。如果你不指定 binsmatplotlib 会根据数据自动分但效果不稳定。手动指定能让别人一眼看懂分段的含义而且不同分段的对比也更公平。散点图的关键参数是alpha透明度。数据点多时重合严重透明度调低到 0.5 左右才能看出密度差异plt.figure(figsize(8, 5)) plt.scatter(df[实验成绩], df[期末成绩], alpha0.5, color#C44E52) plt.title(实验成绩与期末成绩散点图) plt.xlabel(实验成绩) plt.ylabel(期末成绩) plt.show()箱线图画多个班对比时可以直接传一个列表data_by_class [df[df[班级] cls][总评成绩] for cls in df[班级].unique()] plt.figure(figsize(8, 5)) plt.boxplot(data_by_class, labelsdf[班级].unique()) plt.title(各班总评成绩箱线图) plt.ylabel(总评成绩) plt.show()4.3 保存图表与配套结论图表画完记得保存不要用截图。截图一方面分辨率不可控另一方面图片边缘容易截掉。正确的保存方式plt.savefig(hist_total_score.png, dpi150, bbox_inchestight)dpi150基本满足打印需求bbox_inchestight会自动裁剪多余留白让图更紧凑。我一般先plt.show()看一眼效果满意了再savefig最后加一句plt.close()释放内存。在 Notebook 里连续画图不close()短时间内看不出问题画多了可能卡顿养成习惯比较好。图表放进报告时每张图下面必须跟着一段结论。比如直方图配“总评成绩集中在 60-80 分段人数分布接近正态”散点图配“实验成绩与期末成绩呈弱正相关部分低分学生两项都偏低”。图是证据结论是观点两者缺一不可。5. 运行报错和调试经验速查5.1 读报错信息的正确姿势刚开始写代码的人看到报错就慌其实报错是最好的调试线索。我的方法是只看最后两行先搞清楚错误类型和出错位置再决定下一步。比如KeyError告诉我列名取错了那多半是列名有空格或大小写不对TypeError告诉我类型不匹配比如字符串和数字混在一起了ValueError通常和数据内容有关比如NaN进入计算。我在这次作业里踩过最深的一个坑是清洗完成后忘记把成绩列转成数值类型结果算均值时报了TypeError。后来我打印了df.dtypes才发现列类型是object于是补了一步pd.to_numeric()。这类问题其实都可以通过dtypes提前发现根本不用等报错。报错信息里还会给出文件名和行号用那个行号定位代码比从头到尾看一遍效率高得多。如果报错出现在df[某列]这类语句建议立刻print(df.columns)看看实际列名。5.2 常见报错场景与解决思路我把这次作业以及反复调试过程中遇到的高频报错整理成了速查表希望对正在做同类作业的同学有帮助报错类型常见场景解决思路FileNotFoundError文件路径不对文件不在当前工作目录打印当前目录检查相对路径拼写确认文件名扩展名UnicodeDecodeErrorCSV 编码不是 utf-8换成encodinggbk或其他编码KeyError列名写错列名带空格或特殊字符打印df.columns逐一核对用str.strip()清理TypeError字符串与数值混算类型未转换用pd.to_numeric()转类型用dtypes检查ValueError数据含NaN或传参长度不匹配清洗阶段删缺失检查isnull().sum()IndexError索引超出范围先print(df.shape)确认行数再检查切片逻辑这些报错绝大多数不是代码“写错了”而是“假设不成立”。你以为列名叫期末成绩实际列名带了一个空格你以为数据全是数字结果混进了文本。清洗和自检的目的就是提前戳破这些不成立的假设。5.3 我调试时惯用的三个小方法第一个方法清洗前后各打印一次shape确认删掉了多少行。这不仅是自检还是排查 bug 的锚点。如果清洗后行数没变说明drop_duplicates或dropna没生效那就得检查条件写没写对。第二个方法用小规模数据跑通全流程。我会在最初调试时只取前 50 行sample df.head(50)用 sample 跑完统计和画图确认所有代码能正常工作再切回全量数据。这样可以避免每次调试都等所有数据跑完定位问题也更快。这个方法我后来一直用不只做作业处理任何新数据集都先小批量验证。第三个方法在 Notebook 里把每一步写成一个独立 cell。这样哪一步报错我只需要修那一个 cell前面已经算好的结果不会因为重新运行而丢失。如果单个 cell 里塞了一大段逻辑报错之后从头跑到尾很浪费时间。6. 提交前复查与这次作业留给我的体会6.1 一个能救命的自查清单代码全部写完、图也出来了别急着提交。我习惯在交作业前按这个清单过一遍数据是不是从原始 CSV 读取的有没有在代码里硬编码数据清洗步骤能不能解释清楚每步删掉的行数有没有记录统计指标是否都有文字解读数值能不能和图表对应上每张图是否有标题、坐标轴标签、保存文件报告里的每个结论是否都能从某个统计表或某张图里找到依据代码换个目录重新跑一遍能不能顺利通过最后一条特别重要。很多同学代码在自己的 Notebook 里能跑交到老师那里就报错多半是路径问题或依赖缺失。我提交前会全程重启内核跑一遍确认从读取到出图没有一步需要手动干预。6.2 做完这次作业我最大的感触这次作业真正教会我的不是怎么调hist()的参数也不是groupby的用法而是一种做事的顺序先拆需求再摸数据再定清洗规则再算指标最后画图写结论。每一步都在为下一步铺路急不得也跳不得。后来我把这十几行代码改成了一个给学生会用的成绩汇总小脚本原以为学生会的真实数据会干净一些结果比作业里的数据脏多了学号有重复成绩有文本不同来源的表格字段名还不一致。清洗步骤一条不落地全用上了那 28 行被清洗掉的记录给我省下的时间远比当初写它们的成本大得多。如果你也在为第二次作业头疼我的建议很简单别把精力花在找什么“更新更酷”的写法上先用最笨的办法把流程走通再把每一步都做扎实这比任何花哨的技巧都管用。