1. 选题定调:这份“作业”到底要做什么
先说结论:把一份看起来普普通通的课程作业,做成一个能写进简历的完整数据分析小项目,这完全可行。我这次拿到的题目就是“shuyibin的作业”,一份信息科技课的期末大作业,表面要求是“使用Python对一组真实数据进行统计分析并撰写报告”,但老师没有限定数据来源、没有限定分析维度、也没有限定报告形式。这种自由度极高的作业,最容易做崩,也最容易做出彩。
很多同学遇到这种开放式作业,第一反应是去网上抄一份现成的“学生成绩分析”或者“电商销售数据分析”,交差了事。但我的经验是:这种通用模板恰恰是评分最低的。为什么?因为全班三四十号人,三分之一都交同一份“销量分析”,老师审美疲劳不说,答辩时一问数据从哪来的、为什么选这个字段、异常值怎么处理的,答不上来就直接露馅。所以我在拿到题目后,没有急着写代码,而是先花了两天时间想清楚三件事:作业的受众是谁、要证明什么能力、用什么数据最能体现这种能力。
这份作业的受众是信息课老师,评分维度大概率包括:数据处理的规范程度、统计方法的正确性、可视化的表达效果、报告的逻辑完整性。也就是说,光跑出一个结果远远不够,过程的可解释性才是高分关键。我最终选定的方向是“校园图书馆借阅行为分析”,用自己模拟生成的一个月借阅流水数据,从时间规律、类别偏好、借阅周期三个角度切入,完整走了一遍“数据采集—清洗—聚合—可视化—结论输出”的流程。
为什么选图书馆数据而不是电商数据?因为校园场景离我最近,字段语义我能讲清楚,比如“还书日期减去借书日期就是借阅天数”,这类业务解释在答辩时非常加分。而且图书馆数据的分析结论是有实际意义的——可以反推热门书目的备货策略和开放时间优化,这种“分析结果能落回业务场景”的感觉,是纯模板作业给不了的。
2. 数据准备与预处理:八成的功夫都花在这
2.1 造一份“真实感”十足的数据集
既然是作业,我手上并没有现成的图书馆数据库,直接抓公开数据又不一定贴合校园场景。所以我的方案是自己写脚本模拟生成一份高仿真借阅流水。这里有个关键认知需要纠正:模拟数据不代表随便造,造数据的严谨程度直接决定后续分析的可靠程度。
我的数据字段设计如下:
| 字段名 | 含义 | 示例 | 模拟规则 |
|---|---|---|---|
| borrow_id | 借阅流水号 | B202505001 | 日期+当日序号 |
| student_id | 学号 | S2021001 | 五位编号 |
| book_id | 图书编号 | B001 | 共120本 |
| category | 图书类别 | 计算机/文学/历史 | 九个大类 |
| borrow_date | 借出日期 | 2025-05-04 | 集中在学期中段 |
| return_date | 归还日期 | 2025-05-18 | 借期3~30天 |
模拟的时候我给自己定了三个原则:第一,时间范围固定为2025年4月1日到5月31日,共61天,这样就有一个完整的学期中段周期可以观察规律;第二,借阅量与日期强关联,比如每周一和考试周前一周借阅量明显上升,这就人为制造了“业务规律”,方便后面分析出有意义的结论;第三,数据里必须埋入脏数据,包括缺失值、重复记录、异常日期,否则清洗环节没东西可写。
生成数据我用的是Python的random模块加pandas批量拼接,总共生成600条左右的流水。造数据不是目标,造出能支撑多维度分析的“信号”才是目标。
2.2 缺失值、重复值和异常值的处理逻辑
数据生成之后,我特意在脚本里埋了这些脏数据:三条records缺失return_date,两条records完全重复,一条records的borrow_date晚于return_date,还有一条category写成了“计算机类”而不是统一的“计算机”。
这里分享一个处理脏数据的顺序方法论:先看结构再看值,先补缺失再剔异常。我是这样操作的:
import pandas as pd df = pd.read_csv('library_borrow.csv', parse_dates=['borrow_date', 'return_date']) print(df.info()) print(df.isnull().sum())df.info()能快速暴露每列的非空数量和数据类型。缺失的return_date我采用“同类书籍平均借阅天数补齐”的策略,而不是直接删行。比如历史类图书的平均借期是12.6天,缺失的那条就用借用日期加上13天作为估计归还日。这样处理比直接drop更有技术含量,也更能体现对业务的理解。
遇到borrow_date晚于return_date的记录,我的判断是原始录入把日期填反了,处理方式是交换两个字段的值。重复行直接drop_duplicates()删掉就好。全部清洗完之后,我还会用df.describe()看一遍数值分布的合理性,比如借阅天数最小值为1、最大值为30,没有负数、没有超长周期,确认数据质量过关再进入下一步。
2.3 派生字段和聚合表构建
原始流水表只能回答“谁借了什么”,但分析需要的是“某天借了多少”“某类书被借了几次”这样的汇总视角。所以清洗完数据后,我紧接着做了三张聚合表:
- 每日借阅量表:
df.groupby('borrow_date').size(),用于观察时间趋势 - 类别借阅排行表:
df['category'].value_counts(),用于分析藏书偏好 - 借阅周期分布表:计算
(return_date - borrow_date).dt.days后生成新列,再分组统计
这里要强调:分析不是拿到数据就开始画图,而是先想清楚“要通过哪几张表回答哪些问题”,然后再用代码去实现。我把分析框架分成了三个问题:
- 图书馆的借阅高峰出现在什么时候?
- 哪类图书最受欢迎?男生女生的偏好差异大吗?
- 平均借阅时长是多少?不同类别的借阅周期有没有显著区别?
框架一旦定下来,后面的代码和图表就都是为这三个问题服务的,报告自然也不会散。
3. 核心分析与可视化实现
3.1 借阅时间趋势:找到隐藏的“节奏感”
第一个分析目标是观察61天内的借阅量变化。直接画df.groupby('borrow_date').size().plot(kind='line')确实能出一条线,但这种最朴实的方式信息量很低。我做了两个优化维度:
第一个优化是按星期几分组。把所有借阅记录映射到“周一”到“周日”,然后求每天的平均借阅量,这样就能回答“一周里哪天最忙”的问题。我得到的结果是:周一和周五显著偏高,周六周日断崖式下降。道理其实很朴素——周一大家刚回学校,习惯性要去图书馆借一周的书;周五则是因为周末前有阅读计划;周末反而是借阅低谷。
第二个优化是周粒度趋势。把61天按自然周拆分,观察第1周到第9周的借阅量走势,可以看到明显的“两头高中间平稳”形态。第1周是开学初借阅高峰,大家新学期信心满满要读书;第8周骤然上升,因为期末考试临近。
这两个维度的代码逻辑都不复杂,但分析角度一分开,报告的层次立刻就不一样了。导师看到的不再是“一条线”,而是“两条不同尺度的规律线”。
3.2 热门类别与读者画像
类别偏好的分析我用的是柱状图和饼图结合的方式。堆叠柱状图能同时展示“类别的总借阅量”和“男女比例构成”,饼图则适合展示占比。运行出来的结果是:计算机类借阅占比28.6%排名第一,文学类24.2%紧随其后,历史类和经管类分列三四位。
这个结果其实能延伸出好几个有价值的结论。计算机类高居榜首,和我们学校理工科背景强、编程课程学分比重大有直接关系。文学类能排到第二,说明“课外轻阅读”的需求始终存在,图书馆的新书采购不能只盯着专业书。对比男女比例还能发现,借阅计算机类的以男同学为主,文学类的女同学比例更高,但两者的重叠用户也不少。
这里我没有做过于复杂的推荐算法,而是用了一个很直观的pd.crosstab交叉表。交叉表在处理“类别×性别”这种双维计数场景时,比groupby更清爽:
cross = pd.crosstab(df['category'], df['gender']) cross['合计'] = cross.sum(axis=1) cross = cross.sort_values('合计', ascending=False)3.3 借阅周期分析:用箱线图替代均值
借阅周期就是我前面算出来的(return_date - borrow_date).dt.days。很多人在这一步会直接报一个“平均借阅天数14.2天”了事,但这样做有个很明显的坑:如果数据里有极端的31天长期借阅记录,均值会被拉高,掩盖掉大多数人的真实行为。
我的做法是画分组箱线图,每个类别一张图,同时观察中位数、四分位数和离群点。结果很有看点:计算机类的中位借阅周期是9天,明显短于文学类的16天。这说明工具书大家借回去是当手册查的,办完事就还;小说则被当枕头书,慢慢翻慢慢看。这种差异如果只报一个均值,是完全没有办法发现的。
再进一步,我还计算了逾期归还率,即借阅天数超过30天的记录占比。逾期率最高的是文学类,达到了6.8%,计算机类只有2.1%。这个数字对图书馆管理有直接价值:逾期率高的类别,应该适当延长借期或者引入临近到期提醒功能。
3.4 可视化细节:字体、配色与图表的“可读性”
可视化是这次作业里我踩坑最多的环节,集中说一下三个高频雷区。
第一个是Matplotlib中文乱码问题。默认字体不支持中文,图上一片方块。解决方式是在绘图前声明中文字体:
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False第二个是图表配色问题。Matplotlib默认的颜色虽然不难看,但同质性很强,多图并列时容易视觉疲劳。我选了一个低饱和度的配色方案,主色用#4C72B0和#DD8452,辅助色用#55A868,整套图表放在报告里视觉风格统一,加分不少。
第三个是坐标轴信息密度。很多人画柱状图时横坐标不旋转、不加数值标签,导致柱子挤成一团。我的习惯是:类别超过五个的柱状图,横坐标标签必须旋转30度;关键数值必须直接标在柱顶上方,用plt.text()手动添加,而不是让读者自己拿尺子去对坐标轴读数。
4. 报告撰写与常见问题排查实录
4.1 作业报告的结构化表达框架
代码和分析做完了,作业只完成了一半,另一半是报告。我见过太多技术能力不差但因为报告混乱被扣分的案例。我自己用的报告框架是这样:
- 项目背景与目标(1页):说明为什么选图书馆数据、想回答什么业务问题
- 数据来源与处理说明(1.5页):贴上字段字典、清洗前后对比,强调数据质量
- 分析过程与可视化(3页):三个分析模块各一页,每个模块按“图表+结论”的结构写
- 结论与改进建议(0.5页):把分析结果反推回图书馆管理的具体动作
报告里有一条铁律我每次都会强调:每一张图表下面必须紧跟一段话,写清楚“这张图说明了什么、为什么会出现这种情况、下一步应该怎么做”。图表要是孤零零摆在那,没有解读,那和分析不做的效果差不多。
4.2 高频报错与排查思路
把我在写这份作业过程中实际遇到、以及帮同学排查过的高频问题整理成一个速查表,全部都是真金白银换来的经验:
| 报错/问题现象 | 根因 | 解决方案 |
|---|---|---|
KeyError: 'gender' | 列名拼写错误或字段被空格包裹 | 先跑df.columns.tolist()核对列名 |
| 中文字体显示为方框 | Matplotlib缺少中文字体配置 | 设置font.sans-serif后重启kernel |
TypeError: unsupported operand type(s) | 日期列还是字符串类型 | 用pd.to_datetime()显式转换 |
| 数据透视表出现大量NaN | 分组键之间存在空组合 | 在pd.crosstab中加dropna=False |
| 图表全挤在左下角 | x轴或y轴scale问题 | 检查是否误用了plt.xlim(0, 10)硬限制 |
其中“日期列没转类型”这个问题在入门阶段特别常见。pd.read_csv读进来的日期默认是字符串,如果不转成datetime64类型,减法操作直接报错。排查思路也很简单:df.dtypes一查便知。如果这一列显示object,就说明该做类型转换了。
4.3 仿真答辩:把自己的作业当面试项目准备
既然这是一份能写进简历的作业,那么制作过程中就要养成分阶段记录的肌肉记忆。我每完成一个分析模块,就把代码、图表、结论三样东西归档到同一个文件夹,标注当天的修改时间。最后写报告的时候,直接调档拼接就行,不需要临时回想“我当时怎么处理的”。
更重要的一个习惯是:我做完之后会把自己当成面试官,对着报告逐段拷问一遍。数据哪里来的?模拟生成的,那模拟规则和真实场景的贴合依据是什么?缺失值为什么用均值补而不是删除?补完之后对后面的分析有没有引入偏差?这些问题的答案我都提前写在报告的脚注或附录里。老师答辩时一旦问到,我不至于慌。
我个人有个经验:老师提问的重点往往不在你的分析结论多惊艳,而在于你做每一步时有没有想过“为什么”。只要把“为什么”这个环节做扎实了,这份作业的分数下限就不会低。
5. 作业之外的延伸价值
做完这份“shuyibin的作业”之后,我最大的感受是:一份课程作业的价值,不取决于题目的新颖程度,而取决于你愿不愿意把每个环节往深处做一步。
改数据源就能变成一篇完整的数据分析项目;加一个协同过滤算法就能升级成图书推荐原型;把模拟数据替换成豆瓣的真实书目数据,结合爬虫技术,这份作业的含金量又会再上一个台阶。这些都是已经跑通的拓展方向,尤其是把模拟数据换成真实API数据的路径,非常建议感兴趣的同学接着试。
最后分享一个我在传输文件时踩过的小坑:交作业的时候,记得把数据和代码用zipfile压缩成一个压缩包再提交,千万别拿pd.to_csv直接存的裸表在微信里传来传去,格式全乱是小事,工程文件残缺被老师判定为抄袭才是大事。用压缩包保证“数据+代码+报告”三位一体,每份文件命名带上日期,这套习惯我从这份作业一直用到现在,省过不少麻烦。