第五周作业大概是我带过的数据分析课程里,最容易被低估的一次。题目文字看着很长很绕,什么"对数据进行多维度分析与可视化展示""异常值需通过脚本处理并说明依据""最终输出需保证可复现",但本质上它只考一件事:你能不能从一堆乱糟糟的原始数据里,走完"清洗—计算—表达—交付"的完整链路。我见过太多人把大量时间耗在反复修改图表颜色上,最后却没时间写运行说明,或者因为脏数据没处理干净导致结论站不住脚。这篇文章就拿一份典型的共享单车骑行记录作业当例子,把从读题到提交的整条路线拆开讲清楚。不管你是正在赶这门课的学生,还是想找个真实数据集练手的自学者,照着这个思路走,基本不会跑偏。
1. 拿到"第五周作业"后的第一件事:把题目翻译成评分点
1.1 题目不会直接告诉你它在考什么
课程作业的文字描述通常挺抽象的,比如"对数据进行探索性分析"“挖掘骑行规律并完成可视化展示"。如果你只按字面意思理解,很容易做成一个"把所有能想到的图表都画一遍"的大杂烩。但你把题目拆开看,就会发现每个词背后都对应一个明确的交付物。
- 探索性分析,意味着你至少要回答数据长什么样、有哪些字段、缺失情况如何、有没有异常值。
- 清洗数据并说明依据,意味着你不能在Excel里偷偷删行,必须用脚本留下处理痕迹,并且每个处理动作都要有理由。
- 多维度分析,意味着不能只有一个维度上的图表,至少要覆盖时间、空间、行为特征中的两类以上。
- 可视化展示,如果题目写了"可交互",那你交一堆静态图片是会扣分的。
- 可复现,意味着运行环境、依赖版本、启动方式都要交代清楚。
我带过的人里,失分最惨的往往不是代码能力差,而是压根没读懂题目想要什么。有人交了一份非常漂亮的HTML页面,但数据清洗过程一个字没提,老师想跑都跑不起来,最后只能给个及格分。所以我拿到任何作业的第一反应,永远是先做一个需求到交付物的映射表。
1.2 把题目关键词翻译成作业产物
你可以自己做一个类似下面的对照表,读题时一项项打勾:
| 题目关键词 | 实际考点 | 作业里对应的产物 |
|---|---|---|
| 数据清洗 | 异常值判断、缺失值处理策略 | 清洗脚本 + 异常值处理说明 |
| 分析规律 | 聚合统计、分组对比 | 聚合结果表 + 每个结论对应的文字解释 |
| 可视化展示 | 图表类型选择、交互实现 | 可交互图表页面 |
| 运行说明 | 依赖管理、路径规范 | requirements.txt + README |
这份表格做完之后,你的作业框架就出来了,后面所有工作都是在填这些格子。
1.3 时间分配参考
以一份20万行左右的骑行数据为例,我建议按这个比例分配时间:
- 读题 + 数据探索:1.5小时。先搞清楚数据里到底有什么,别急着写清洗逻辑。
- 数据清洗与校验:2小时。这是最容易低估的部分,脏数据往往比你想象的更脏。
- 指标计算:2小时。把多维度的聚合结果算出来。
- 可视化与页面布局:4小时。如果你不熟ECharts,这个时间可能还要翻倍。
- 运行说明与部署验证:1.5小时。确保换一台机器也能跑起来。
这个计划的核心原则是:不要把时间全部砸在"锦上添花"的图表美化上,先把"骨架"跑通。骨架没跑通,再好看的皮囊也白搭。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 清洗环节的三个坑:别让脏数据毁掉后面的可视化
2.1 时间字段的格式混乱
我拿到的那份CSV,start_time这一列长得极其任性,我贴几个真实例子:
code复制2023/04/01 08:05
2023-04-01 08:05:00
2023-04-01T08:05:12
1682921100
同一列里出现了斜杠、横线、T分隔符,甚至有两个值直接是Unix时间戳。如果不去管它,后面的按小时聚合、按工作日/周末分组全都算不准。
这种情况的处理方式很简单,用pandas统一解析:
python复制import pandas as pd
df = pd.read_csv("bike_trip.csv", encoding="utf-8")
df["start_time"] = pd.to_datetime(df["start_time"], errors="coerce")
df["end_time"] = pd.to_datetime(df["end_time"], errors="coerce")
df = df.dropna(subset=["start_time", "end_time"])
这里的errors="coerce"作用很关键:遇到解析不了的字符串,不会让程序报错退出,而是变成NaN,等下一步统一删除。我见过有人为了一个格式错误来回改代码,最后发现用coerce + dropna两步就解决了。
2.2 骑行时长出现负数和零值
第二个高频坑是duration_sec字段。20万条数据里有800多条是负数,还有200多条等于0。负数的来源有可能是系统时钟回拨、数据上传时序错乱、甚至是站点设备异常造成的,但不管来源是什么,业务上"骑行时长为负数"一定不对。
我的处理逻辑是:
python复制df["duration_sec"] = pd.to_numeric(df["duration_sec"], errors="coerce")
df = df.dropna(subset=["duration_sec"])
df = df[(df["duration_sec"] > 0) & (df["duration_sec"] < 7200)]
注意我同时加了一个上限7200秒,也就是2小时。共享单车单次骑行超过2小时的也有,但数量极少,而且很多是"忘记还车"导致的异常记录。如果你不做上限处理,后面计算平均骑行时长时,几个超长异常值会把均值拉得没法看。
处理完之后,我建议你顺手写一句说明,比如"删除duration_sec为负和大于7200秒的记录,共占全量数据的0.8%,不影响整体分析"。这种一句话,在老师眼里就是你"有依据地处理异常值"的证据。
2.3 缺失值比例决定你的处理方式
清洗时还可能遇到bike_id有11%的缺失,station_id缺失2.3%。很多新手的直觉是"有缺失就删行",但如果bike_id缺失了11%,直接删掉会损失大量样本,后面的站点热度分析也会失真。
更合理的做法是先判断缺失比例和缺失模式:
python复制missing_rate = df.isnull().mean().sort_values(ascending=False)
print(missing_rate)
如果某个字段缺失率低于5%,通常可以直接删除对应行。如果缺失率在10%-20%之间,你要考虑这个字段是不是本身就允许为空。比如bike_id缺失,可能是车辆GPS没有上报设备编号,但骑行记录仍然有效。这时如果后续分析不需要按单车维度做聚合,完全可以保留这些行。
判断"能不能保留"有一个简单标准:这个字段会在你的核心分析里作为分组键吗?不会,就保留;会,再考虑填充或删除。我一般把这条规则写成一个小表,方便自己交代:
| 字段 | 缺失率 | 处理方式 | 理由 |
|---|---|---|---|
| station_id | 2.3% | 删除该行 | 影响范围小,且站点是核心维度 |
| bike_id | 11.4% | 保留,不参与单车站点分析 | 缺失率较高,删除损失大 |
| duration_sec | 0.2% | 删除该行 | 少量缺失,不影响整体 |
清洗这件事,看起来不产生任何"炫酷"的图表,但它决定了你后面所有结论是不是可信。我后来带实习生时发现,能在清洗脚本里把每个操作都写出理由的人,做生产环境数据治理时也更容易上手,因为那是同一种判断力。
3. 指标设计与聚合计算:让图表能回答业务问题
3.1 从业务问题倒推指标,而不是从数据强行找结论
拿到清洗后的数据,下一步不是立刻画图,而是先问自己:我想回答哪几个问题?共享单车运营方最关心的几类问题通常是:
- 一天之中什么时候骑行量最高?这对应早晚高峰通勤需求。
- 哪些站点是热门站点?这对应城市核心区域和交通枢纽。
- 单次骑行时长分布是什么形状?这能判断用户是通勤还是休闲。
- 工作日和周末的骑行模式差多少?这能区分通勤刚需和休闲娱乐。
带着问题去做聚合,你的代码会非常有方向感。反过来,如果一上来就groupby所有字段、把所有组合都跑一遍,最后只会得到一堆不知道往哪放的图。
3.2 核心聚合代码
基于这几个问题,我用pandas做了四个核心计算。第一步先提取小时和周末标记:
python复制df["hour"] = df["start_time"].dt.hour
df["is_weekend"] = df["start_time"].dt.dayofweek >= 5
然后按小时统计骑行量:
python复制hourly = df.groupby("hour").size().reset_index(name="trip_count")
再对比工作日与周末:
python复制weekday_weekend = df.groupby(["is_weekend", "hour"]).size().unstack(0)
热门站点按出发站统计:
python复制station_stats = (
df.groupby("start_station_id")
.agg(
trip_count=("trip_id", "count"),
avg_duration=("duration_sec", "mean"),
)
.sort_values("trip_count", ascending=False)
.head(10)
)
骑行时长分布可以直接用pandas的分位数概括,比如df["duration_sec"].quantile([0.25, 0.5, 0.75, 0.9]),也可以画直方图。我个人更推荐先看分位数,再看直方图,因为分位数能直接告诉你"60%的骑行在15分钟以内"这种可写进报告的结论。
3.3 每个指标后面必须跟一句结论
这是作业里最容易被忽略的加分项。很多人把图表一贴就结束了,但没有解释"这说明了什么"。我的习惯是每一个图表下面配一句业务结论,比如:
- 早高峰8点到9点的骑行量是凌晨时段的40倍,说明通勤是绝对主力场景。
- 骑行时长中位数是12分钟,75%的订单在20分钟以内,用户以短途接驳为主。
- 工作日图表呈现明显的双峰结构,周末变成单峰结构,且午后的骑行量明显更高。
你不需要每个结论都长篇大论,一两句话说清楚就好。老师看作业的时候,最希望看到的就是"这个学生能从数据里读出业务含义",而不是只会跑函数。
4. 可视化选型:为什么我建议Flask + ECharts而不是静态图
4.1 三种方案的对比
很多人纠结作业里到底用什么做可视化。我直接给结论:如果作业要求"可交互",不要用Matplotlib硬撑;如果不想写太多前端,可以用Streamlit;但如果想拿一个比较高的分,同时还能练习一点真实项目里的技术栈,Flask + ECharts是最合适的组合。
| 方案 | 学习成本 | 展示效果 | 调试速度 | 作业适配度 |
|---|---|---|---|---|
| Matplotlib静态图 | 低 | 一般 | 快 | 基础分 |
| Streamlit | 低 | 较好 | 快 | 良好 |
| Flask + ECharts | 中 | 强 | 中 | 高分 |
Matplotlib不是不能用,它适合做探索性阶段的内部分析,把数据分布看清楚之后,再决定最终呈现用哪种图表。直接拿Matplotlib当最终交付,你会发现图例、配色、交互全都差点意思。
Streamlit确实是省力方案,几十行代码就能出一个带下拉框的页面。但它的缺点是样式高度模板化,而且一旦你为了改样式去写HTML/CSS,成本反而比Flask还高。作业场景下,我建议用它作为保底方案,而不是首选。
4.2 用Flask写一个轻量数据接口
我的做法是:把聚合好的结果存成JSON文件,然后用Flask起一个非常轻的接口,前端用ECharts请求这些JSON。这样前后端分离,代码结构清晰,也方便你后面换了数据重新跑一遍。
python复制from flask import Flask, jsonify
import json
app = Flask(__name__)
@app.route("/api/hourly")
def hourly():
with open("output/hourly.json", encoding="utf-8") as f:
data = json.load(f)
return jsonify(data)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
实际写的时候,你可以把多个聚合结果都放进去,比如/api/station、/api/weekday_weekend,前端按需请求。这个阶段不用搞得太复杂,Flask自带的路由已经够用。
4.3 ECharts配置里最容易出效果的几个细节
数据接口有了,剩下的就是前端展示。ECharts的基础折线图大家都会写,但作业要拿高分,我建议多关注几个细节。
第一个是交互提示。默认的tooltip是跟随鼠标显示,有数据项就触发,但我更推荐配置成坐标轴触发,再带上数据单位:
javascript复制tooltip: {
trigger: "axis",
valueFormatter: (value) => value + " 次"
}
第二个是数据缩放。小时维度只有24个点,不缩放问题不大,但如果你要展示30天的趋势,dataZoom能在同一张图里兼顾整体趋势和局部细节。
javascript复制dataZoom: [
{ type: "inside", start: 0, end: 100 }
]
第三个是自适应窗口大小。很多人的图表在浏览器窗口拉大或缩小时会变形,加一行监听就行:
javascript复制window.addEventListener("resize", () => chart.resize());
第四个是配色。ECharts默认主题的颜色太常见了,一眼就能看出来你没动过。最简单的改进是在setOption里指定color数组,选一组低饱和度的色板,比如:
javascript复制color: ["#4E79A7", "#F28E2B", "#59A14F", "#E15759"]
这组颜色来自Tableau经典配色,柔和不刺眼,适合作业报告的场景。
还有一个经常翻车的地方是中文字体乱码。ECharts本身对中文支持没问题,但如果你在页面里引用了某个没有中文字形的字体,图表里的中文就会变成方框。稳妥的做法是不要太依赖本地字体,直接使用系统默认字体栈。
5. 提交前最容易翻车的四个环境问题
5.1 路径分隔符与编码问题
作业提交之后,老师会换一台机器跑你的代码。只要你用了Windows下的反斜杠路径,比如C:\Users\xxx\data\raw.csv,换到macOS或Linux上就必然报错。解决方法是统一用相对路径,或者直接用Pathlib:
python复制from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
df = pd.read_csv(DATA_DIR / "bike_trip.csv", encoding="utf-8")
另外,CSV文件的编码也是一个隐藏雷点。Windows下很多表格工具导出的CSV是gbk编码,而Linux和macOS默认按utf-8读,一读就乱码。你可以在read_csv里显式指定encoding="utf-8",如果数据源本身是gbk,就改成encoding="gbk"。最稳妥的做法是统一把原始CSV转成UTF-8后再提交。
5.2 Pandas版本差异
pandas的API在不同版本间有细微变化,最常见的是groupby的observed参数。在pandas 2.x里,groupby一个category类型的列时会默认observed=False,导致一些空组合也被统计进去,结果跟旧版本不一样。
解决办法不在代码里,而在依赖管理。你在项目根目录放一个requirements.txt,锁定关键版本:
text复制pandas==2.0.3
flask==3.0.0
然后README里写清楚安装命令。这样不管对方是什么环境,至少有一个确定的版本可以参考。
5.3 端口占用与启动脚本
Flask默认跑在5000端口,但很多机器上这个端口已经被别的服务占用了。你可以在启动命令里显式指定端口,比如app.run(port=8000)。同时提供一个启动脚本会更显专业。
Windows下可以放一个run.bat:
bat复制@echo off
pip install -r requirements.txt
python app.py
macOS/Linux下放一个run.sh:
bash复制#!/bin/bash
pip install -r requirements.txt
python app.py
不要小看这个小步骤。老师检查作业时,最怕遇到那种"代码写得不错但要我花20分钟研究怎么启动"的项目。你的目标很简单,让对方在极短时间内看到成果。
5.4 中文字体乱码
这个问题在Matplotlib里最典型。如果你在图表里用了中文标题,但系统没有SimHei字体,就会出现一排小方框。解决方法是在代码前面设置:
python复制import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei", "Arial Unicode MS", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False
如果你用的是ECharts,一般不太会遇到这个问题,但要注意页面HTML的meta标签里加上<meta charset="utf-8">,否则浏览器可能按默认编码解析导致中文乱码。
5.5 提交前的完整检查表
我把这些常见问题整理成一个表格,提交前一格一格核对:
| 检查项 | 常见问题 | 解决办法 |
|---|---|---|
| 路径 | 代码中写死Windows绝对路径 | 使用Pathlib和相对路径 |
| 编码 | CSV编码不一致导致乱码 | read_csv显式指定encoding |
| 依赖 | 对方环境缺少第三方库 | requirements.txt锁定版本 |
| 端口 | 5000端口被占用 | 启动时指定8000端口 |
| 字体 | 中文显示为方框 | 配置中文字体路径 |
| 数据 | 清洗脚本与结果数据不一致 | 清洗前保存原始文件,清洗后另存结果文件 |
这一套检查跑完,作业基本就稳了。
6. 从第五周作业里带走的通用经验
就说一个我反复遇到的场景。每次收作业都能看到两类人:一类人把代码提交到平台就没下文了,另一类人会额外附一个"数据说明.txt",里面写清楚每个字段代表什么、每一步清洗删了多少条数据、聚合结果有什么含义、运行需要什么环境。我几乎不需要看代码,就知道后一类人以后做项目时能省多少沟通成本。
所以这个作业最值钱的不是图表的视觉效果,也不是你用了多高级的机器学习模型。它真正训练的是你拿到一份陌生数据之后,能不能自己建立一套判断体系:这个字段能干什么、异常值该怎么处理、指标用均值还是中位数、图表怎么选才不误导人。这些东西,工作里天天都要用。
还有一个很实际的建议:如果你时间来不及了,宁可砍掉一个分析维度,也要保证清洗脚本、README和最终页面三者是自洽的。我见过太多人最后三小时还在加新图表,结果README没写、依赖没锁、甚至启动就会报错。稳定的完整交付,永远比花哨的半成品拿到更多分数。
其实把作业做完之后,你会发现自己已经默默摸了一遍一个最小数据产品的流程:原始数据进来,经过清洗和聚合,通过接口提供出去,最后在页面上呈现给用户。这个链路想通了,第五周作业就不只是"交差",而是你简历项目里的第一块真实拼图。以后再遇到类似数据,你会有底气地说一句:这事我做过。
