简介:这份压缩包汇集北京邮电大学Python课程中数据处理方向的学习笔记、复习巩固题与大作业材料,面向正在学习Python基础语法、数据结构与数据分析实战的在校学生及自学者。内容从控制流、函数、模块与异常处理等基础梳理,延伸到列表、字典、字符串、文件操作和正则表达式的练习,并通过综合性项目训练数据清洗、预处理、分析与可视化,适合用来对照课程进度查漏补缺。包内共103个文件,以24个py脚本、16个ipynb笔记本、12个csv数据集和32个png结果图为主,另有配置文件、说明文档与少量缓存文件,压缩包约84.46MB;笔记本与脚本便于复现实验,csv和图片则对应数据处理与可视化产出。已有96人学习下载。借助其中的笔记、练习解答、大作业代码与配套数据集,读者可以理解完整的数据处理流程,积累pandas、matplotlib等工具的实操经验,并参考目录组织方式独立完成类似课程项目。
1. 一份 Python 数据处理作业包,值不值得花时间拆
期末前一周,A同学发来一个压缩包,说里面是某高校课程的 Python 数据处理作业,让我帮忙看看为什么一跑就报错。解压之后我发现,这东西比想象中正经:原始 CSV、清洗脚本、统计脚本、出图脚本、一份 requirements,外加一个 notebook,几乎把数据处理从读取数据到出汇总图表的整条主干链路都走了一遍。
它不算什么大项目,但对两类人很实在。刚学完语法、想找个能跑通的练手场景的新手,照着一份能跑通的作业走一遍,比看十篇教程更有用;手上要做课程设计、要给业务出一份小报表的开发者,把它的读写、清洗、聚合、出图四段拆开,稍作改动就能当模板用。下面按拆包顺序讲:先看骨架,再拆核心代码,然后跑起来,最后讲怎么改成自己的东西。
2. 拆包先看骨架:目录结构与技术栈
2.1 目录树里每个文件干什么
压缩包解开第一件事别急着双击脚本,先看目录。这类作业包的布局相当典型,数据、源码、笔记、依赖各占一块:
python-homework-data/ ├── data/ │ ├── raw/ # 原始数据,只读不改 │ │ ├── sales_2023.csv # 销售流水,含缺失与重复 │ │ └── student_score.csv # 成绩表,含异常分数 │ └── output/ # 脚本产物统一落这里 ├── src/ │ ├── utils.py # 公共函数:路径、编码、字体 │ ├── task1_clean.py # 清洗:缺失值、重复值、类型转换 │ ├── task2_stats.py # 统计:分组聚合、透视表 │ └── task3_plot.py # 可视化:柱状图、折线图、导出 ├── notebooks/ │ └── explore.ipynb # 探索过程,非必需 ├── requirements.txt └── README.md我一般会先盯data/raw和src/两块。raw目录里的文件名决定了后面所有列名和类型处理,src里的文件是按题目编号切的,一个文件对应一类任务,这种「一题一文件」的切法虽然不够优雅,但排查问题时非常省事——哪一步结果不对,单独跑那一个文件就行。
utils.py是最值得先读的文件。作业包里的公共逻辑基本都堆在这,路径拼接、编码兜底、matplotlib 中文字体设置通常都在里面。很多人跳过它直接跑 task 脚本,结果遇到路径错误或者图上中文变方块,回头才发现 utils 里早就写好了处理方式,只是自己没调用。
| 文件 | 职责 | 运行方式 |
|---|---|---|
utils.py | 路径、编码、字体、日志等公共能力 | 被其他脚本 import |
task1_clean.py | 读原始数据、清洗、写中间结果 | 独立运行 |
task2_stats.py | 读清洗后的数据、聚合出指标 | 依赖 task1 产物 |
task3_plot.py | 读统计结果、出图并保存 | 依赖 task2 产物 |
这张表说明了一个容易被忽略的事实:三个 task 之间有执行顺序,不是随便点哪个都能跑。task2 读的是 task1 写出来的中间文件,task3 读的是 task2 的汇总结果。跳过前一步直接跑后面,报的往往是 FileNotFoundError,而不是看起来更「合理」的数据错误。
2.2 技术栈:为什么是 pandas 加 matplotlib 这一套
依赖清单不长,核心就四五个:pandas负责表格读写与聚合,numpy做数值运算和条件筛选,matplotlib出图,openpyxl作为 Excel 读写引擎,再算上jupyter用于探索。这套组合是数据处理类作业的默认答案,原因很直接——CSV 和 Excel 是课程数据最常见的两种载体,pandas 两行代码就能吃进来,不需要建库建表,也不需要写 SQL。
这里有个新手常问的问题:为什么不用数据库?因为作业数据量通常在几千到几万行,pandas 全量装进内存毫无压力,用 SQLite 反而多出一层 schema 定义和连接管理。什么时候该换?等单文件超过几十万行、或者需要多进程并发读写的时候再考虑,那个阶段 pandas 的read_csv(chunksize=...)也能顶一阵。
numpy的存在感看起来不高,但 pandas 底层就是它,做条件筛选、分箱、相关系数计算时直接用 numpy 函数比绕 pandas 更清爽。openpyxl是隐式依赖——你不写to_excel就感觉不到它,一旦要导出 xlsx 又没装,报错信息还不太直白,属于典型的「不装能跑一半」的包。
2.3 数据格式约定:编码、列名、缺失值
数据文件是这类项目翻车最集中的地方,拆包时值得单独确认三件事。
第一是编码。国内课程数据里 CSV 用 GBK 保存的比例不低,用pd.read_csv默认的 utf-8 去读,直接抛UnicodeDecodeError。稳妥的写法是先探测再读,或者干脆在 utils 里封装一个带兜底的读取函数,后面所有脚本都走它。
第二是列名。原始表的表头经常带空格、全角括号、单位后缀,比如销售额(元)。这类列名在代码里手写容易出错,清洗第一步就该统一成下划线命名的英文或拼音短名。
第三是缺失值的表示方式。同一份表里可能同时出现空字符串、NULL、-、0、NaN五种形态,pandas 默认只认识其中一部分。读的时候用na_values显式声明,比读进来之后再replace干净得多,也避免了把0误当缺失值删掉的惨案。
# utils.py 片段:一个带编码兜底的读取函数 import pandas as pd def read_table(path, **kwargs): """优先 utf-8-sig,失败回退 gbk;两种编码在国内数据里覆盖绝大多数场景""" for enc in ("utf-8-sig", "gbk"): try: return pd.read_csv(path, encoding=enc, **kwargs) except UnicodeDecodeError: continue raise RuntimeError(f"两种编码都读不了:{path}")utf-8-sig而不是utf-8,是为了顺手吃掉 Excel 导出时带的 BOM 头,否则第一列列名会莫名其妙变成\ufeffid。**kwargs透传是有意留的口子,na_values、dtype、usecols这些参数在具体任务里各不相同,封装层不该把它们写死。异常最后统一抛RuntimeError并带上路径,比让UnicodeDecodeError直接冒出来更好定位——尤其当你一次要读五六个文件的时候。
2.4 执行入口与运行顺序
三个 task 脚本的入口结构基本一致:if __name__ == "__main__":下拼路径、调函数、打印产物位置。路径一般用pathlib.Path(__file__).resolve().parent.parent反推项目根目录,这样在哪个目录下执行都不会错。
顺序上,先 task1 再 task2 再 task3,中间产物落在data/output。如果 README 里没写清楚,我的习惯是看每个脚本read_csv的入参路径——读raw的是第一步,读output的是后续步骤,依赖关系一目了然。
3. 核心模块拆解:一条从原始 CSV 到汇总图表的链路
3.1 数据读取与清洗:把脏数据挡在入口
清洗脚本是整条链路的地基,写得好后面两步都轻松。这个作业包里的 task1 大致做了四件事:读文件、统一列名、处理缺失与重复、转换类型并落盘。
# task1_clean.py import pandas as pd from pathlib import Path from utils import read_table ROOT = Path(__file__).resolve().parent.parent RAW = ROOT / "data" / "raw" / "sales_2023.csv" OUT = ROOT / "data" / "output" / "sales_clean.csv" # 列名映射:原始表头带中文和单位,统一成短名 COL_MAP = { "订单编号": "order_id", "下单日期": "order_date", "销售额(元)": "amount", "所属区域": "region", "客户等级": "level", } def clean(): df = read_table( RAW, na_values=["", "NULL", "-", "null"], # 显式声明缺失值形态 dtype={"order_id": "string"}, # 订单号按字符串读,避免前导零丢失 ) df = df.rename(columns=COL_MAP) # 金额转数值,转不了的置为 NaN 而不是直接抛错 df["amount"] = pd.to_numeric(df["amount"], errors="coerce") df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce") # 去重:按订单号判断,保留最后一条(视为修正后的记录) df = df.drop_duplicates(subset=["order_id"], keep="last") # 金额缺失用区域中位数补,比全局均值更贴近业务 df["amount"] = df.groupby("region")["amount"].transform( lambda s: s.fillna(s.median()) ) df = df.dropna(subset=["order_date", "region"]) df.to_csv(OUT, index=False, encoding="utf-8-sig") print(f"清洗完成:{len(df)} 行 -> {OUT}") if __name__ == "__main__": clean()这段代码里几个参数值得单独说。dtype={"order_id": "string"}是关键——订单号如果全数字,pandas 默认推断成 int64,前导零直接没了,后续和别的表关联就对不上。errors="coerce"让非法值变成 NaN 而不是中断整个脚本,代价是脏数据被静默吞掉,所以后面必须配一步缺失率统计,否则你根本不知道吞了多少。
drop_duplicates的keep="last"是一种业务假设:同一订单号出现多次,后面的记录是修正版。如果数据源没有这个语义,就得改成keep=False全部剔除再人工核对。groupby(...).transform(...)而不是fillna(df["amount"].median()),是因为不同区域的消费水平差异大,用全局中位数填会让华东的单子被西北的均值拉低,属于看起来能跑、实际上指标全歪的那种坑。
写回用index=False,不然下一次读进来会多出一列Unnamed: 0;用utf-8-sig是为了 Excel 直接双击打开不乱码。
3.2 分组聚合与指标计算:把明细变成能看的数
清洗完的数据是明细行,统计脚本要做的是把它压成维度加指标的形式。常见需求是「按区域和月份看销售额、订单数、客单价」。
# task2_stats.py import pandas as pd from pathlib import Path ROOT = Path(__file__).resolve().parent.parent CLEAN = ROOT / "data" / "output" / "sales_clean.csv" OUT = ROOT / "data" / "output" / "summary.xlsx" def summarize(): df = pd.read_csv(CLEAN, parse_dates=["order_date"]) df["month"] = df["order_date"].dt.to_period("M").astype(str) # 2023-01 形式 summary = ( df.groupby(["region", "month"]) .agg( order_cnt=("order_id", "nunique"), # 去重计数,防重复下单 amount_sum=("amount", "sum"), amount_avg=("amount", "mean"), level_top=("level", lambda s: s.mode().iat[0] if not s.mode().empty else None), ) .reset_index() ) summary["avg_order_value"] = (summary["amount_sum"] / summary["order_cnt"]).round(2) # 透视成「区域 × 月份」的宽表,方便直接看趋势 pivot = summary.pivot(index="region", columns="month", values="amount_sum") with pd.ExcelWriter(OUT, engine="openpyxl") as writer: summary.to_excel(writer, sheet_name="长表", index=False) pivot.to_excel(writer, sheet_name="透视表") print(f"统计完成:{len(summary)} 行 -> {OUT}") if __name__ == "__main__": summarize()agg里每个字段指定聚合函数,比先groupby再逐列算要快也更好读。order_cnt用nunique而不是count,是因为同一订单可能有多条明细行,count会把行数当订单数,指标虚高一大截——这个错误在课程作业里出现频率极高。lambda s: s.mode().iat[0]取众数,是「每组取一个代表值」的常见写法,mode()返回可能为空,所以加了判空。
to_period("M")转成月度周期再转字符串,比dt.strftime("%Y-%m")稳,因为它不会因为时区或者跨年边界出问题。透视那一步pivot要求 index 和 columns 组合唯一,如果上游聚合粒度不对,这里会直接抛ValueError: Index contains duplicate entries,反倒是个免费的数据校验。
ExcelWriter用上下文管理,多 sheet 写在一个文件里,比导出多个 CSV 更适合交付。
3.3 可视化与结果导出:图上中文别再变方块
出图脚本的代码量最小,坑却最多,核心就两件事:字体和保存。
# task3_plot.py import matplotlib matplotlib.use("Agg") # 无图形界面环境必须,服务器上跑不报错 import matplotlib.pyplot as plt import pandas as pd from pathlib import Path # 中文字体:优先 Windows 的黑体,回退 Linux 常见字体 plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "WenQuanYi Zen Hei"] plt.rcParams["axes.unicode_minus"] = False # 负号显示成方块的老问题 ROOT = Path(__file__).resolve().parent.parent SRC = ROOT / "data" / "output" / "summary.xlsx" IMG = ROOT / "data" / "output" / "trend.png" def plot(): df = pd.read_excel(SRC, sheet_name="长表") total = df.groupby("month")["amount_sum"].sum().sort_index() fig, ax = plt.subplots(figsize=(9, 4.5), dpi=150) ax.plot(total.index, total.values, marker="o", linewidth=1.8, label="月度销售额") ax.set_title("月度销售额趋势") ax.set_xlabel("月份") ax.set_ylabel("销售额(元)") ax.tick_params(axis="x", rotation=45) ax.grid(alpha=0.3, linestyle="--") ax.legend() fig.tight_layout() # 防止 x 轴标签被裁掉 fig.savefig(IMG, bbox_inches="tight") print(f"图表已保存:{IMG}") if __name__ == "__main__": plot()matplotlib.use("Agg")必须在import pyplot之前调用,顺序反了不生效。在没装桌面环境的机器上,默认后端会尝试连接显示服务,直接报no display name,很多人以为是代码问题,其实是后端选错。
字体列表是回退机制,matplotlib 会按顺序找第一个装了的。Windows 上SimHei基本都有,Linux 容器里通常得装fonts-wqy-zenhei才有中文。axes.unicode_minus = False解决的是负号变方块的玄学问题——中文字体往往不带 Unicode 减号字形。
bbox_inches="tight"配合fig.tight_layout()是双重保险,前者裁掉多余白边,后者调整子图间距。dpi=150是屏幕查看和插入文档之间的折中值,要印刷再往上调。
4. 跑起来:环境、命令与输出核对
4.1 虚拟环境与依赖安装
别在全局环境里装,尤其当你机器上已经有一堆项目的时候。虚拟环境三步走,进项目根目录执行:
# 创建并激活虚拟环境(Windows 用 .venv\Scripts\activate) python -m venv .venv source .venv/bin/activate # 依赖一次装齐;网络慢的时候换国内镜像源 python -m pip install --upgrade pip pip install -r requirements.txt # 确认关键包版本,作业代码对 pandas 大版本比较敏感 python -c "import pandas, matplotlib; print(pandas.__version__, matplotlib.__version__)"python -m venv而不是virtualenv,因为前者是标准库自带,不额外装东西。python -m pip而不是直接pip,能保证用的是当前解释器对应的 pip,避免装到别的环境里去——这个坑我踩过不止一次,明明装了包,运行还是 ModuleNotFoundError。
版本确认那一步别省。pandas 2.x 之后applymap改名成map,df.append直接移除,老作业代码在新版本上会连环报错。如果requirements.txt里没锁版本,建议手动加上pandas>=1.5,<2.0之类的约束,或者反过来把代码里的过时 API 改掉。
要是遇到没写requirements.txt的情况,从 import 语句反推即可:import pandas对应 pandas,read_excel需要 openpyxl,to_period是 pandas 自带。别一上来就pip install -r一个不存在的文件然后卡住。
4.2 分题运行与批量执行
按顺序跑,每一步都看输出:
# 逐题执行,注意工作目录要在项目根目录 python src/task1_clean.py python src/task2_stats.py python src/task3_plot.py # 想一次跑完就用 && 串起来,前一步失败会中断 python src/task1_clean.py && python src/task2_stats.py && python src/task3_plot.py用&&而不是;,是因为后一步依赖前一步的产物,前面失败了后面跑出来的多半是旧数据或者空文件,容易误判成「跑通了」。
如果用python src/task1_clean.py报ModuleNotFoundError: No module named 'utils',是因为 Python 默认只把脚本所在目录加进sys.path,而utils.py和 task 脚本同级、按理说应该能找到。真出问题多半是你在别的目录下用了绝对路径执行,或者在 IDE 里配错了工作目录。改成python -m src.task1_clean并在src下加个空的__init__.py是更规范的做法。
想给脚本加参数,最省事的是argparse:
import argparse p = argparse.ArgumentParser() p.add_argument("--input", default="data/raw/sales_2023.csv") p.add_argument("--out-dir", default="data/output") args = p.parse_args()这样换数据集不用改代码,--input指到新文件就行。默认值给相对路径,配合前面Path(__file__)反推根目录的写法,两个机制要统一,别一半相对一半绝对。
4.3 输出结果核对:三个校验点
跑完不算完,得确认结果对。我一般看三处。
第一是行数。清洗前后行数差多少,心里要有数。掉了 5% 以内通常是缺失值和重复值,掉了 40% 就得回头查dropna的条件是不是写宽了——dropna()不传参数是「任一列缺失就删行」,很容易误伤。
第二是合计值。清洗前后amount总和应该基本持平(除非你补了缺失值),如果差得离谱,八成是to_numeric把一堆字符串转成了 NaN 又被填了中位数,属于数据被悄悄改写。
第三是产物本身。Excel 打开看两个 sheet 在不在、透视表的行列对不对;图片打开看中文是不是正常显示、x 轴标签有没有被裁。这三处都过了,才算真的跑通。
5. 踩坑记录:五个让作业跑不起来的问题
5.1 数据读取阶段的两个坑
现象:UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc4 in position 0。原因:CSV 是 Excel 另存为的 GBK 编码,而read_csv默认 utf-8。解决:用前面read_table那种带兜底的读法,或者直接encoding="gbk"。判断方法很简单,用记事本打开看中文是否正常,正常就是 GBK 系。别用errors="ignore"糊过去,那样中文列名会被吞掉,后面按列名取数全失败。
现象:读进来的表多了一列Unnamed: 0,或者第一行数据变成了列名。原因:前者是上游to_csv时没写index=False,把行索引也存进去了;后者是文件里本来就有多层表头或者前面有几行说明文字。解决:前者读的时候index_col=0,但更该改上游;后者用skiprows=n跳过说明行,或者header=1指定表头行号。先head -5 data.csv看一眼原始内容,比在代码里瞎试快得多。
5.2 运行环境与输出阶段的三个坑
现象:图出来了,中文全是方块,标题、轴标签、图例无一幸免。原因:matplotlib 默认字体 DejaVu Sans 不含中文字形。解决:设font.sans-serif并带上回退列表,同时关掉axes.unicode_minus。Linux 上还得确认系统真装了中文字体,fc-list :lang=zh能查;查不到就装fonts-wqy-zenhei,光改代码没用。
现象:ModuleNotFoundError: No module named 'openpyxl',但明明装了 pandas。原因:pandas 的 Excel 读写是可选依赖,read_excel/to_excel需要额外引擎,xlsx 走 openpyxl,xls 走 xlrd。解决:pip install openpyxl。顺便说一下,写 xlsx 用engine="openpyxl"显式指定更保险,尤其在同时装了多个引擎的机器上。
现象:同一份数据跑两遍,图表叠在一起、Excel 里数据翻倍。原因:脚本以追加模式写文件,或者 savefig 覆盖前没关 figure,多次调用导致状态累积。解决:写文件一律覆盖式,to_csv不加mode="a";画图用fig, ax = plt.subplots()显式管理,结束时plt.close(fig)。涉及随机抽样的地方记得random_state=42,不然每次结果都不一样,根本没法核对。
6. 二次开发:把作业包改成自己的数据处理模板
作业包的代码能跑,但复用性一般:路径写死、参数写死、三步之间靠文件传递。想拿它当日常模板,我做了三处改造,投入不大,收益明显。
第一处是把配置抽出来。原始脚本里路径、列名映射、聚合口径散在各文件,换一份数据要改五六个地方。抽成一个config.py或者 YAML,主流程只读配置:
# config.py CONFIG = { "input": "data/raw/sales_2023.csv", "out_dir": "data/output", "col_map": {"订单编号": "order_id", "销售额(元)": "amount", "所属区域": "region"}, "na_values": ["", "NULL", "-"], "group_keys": ["region", "month"], }第二处是把三段脚本串成一个 pipeline,用一个main.py调度,中间产物可选落盘。这样调试时可以只跑某一步,交付时一条命令走完。第三处是加校验,清洗后立刻算缺失率、去重率、总额偏差,超过阈值就打印警告,别让脏数据一路流到图上。
| 维度 | 作业版 | 模板版 |
|---|---|---|
| 路径 | 脚本内硬编码 | 配置文件 + CLI 参数 |
| 执行 | 三个脚本手动按序 | 一个入口,支持--step |
| 校验 | 无 | 缺失率、去重率、总额偏差 |
| 复用 | 换数据要改代码 | 换配置即可 |
改造后怎么验证没改坏?我的做法是留一份原始产物当基线,改完跑一遍,用 pandas 对比新旧结果:pd.testing.assert_frame_equal(new, old, check_dtype=False),数值一致才算过。这一行比肉眼看 Excel 靠谱得多。
血泪经验是:别一上来就重构。先把作业包原样跑通、产物存一份,再动手改,否则出了问题你连「本来是什么样」都不知道,后悔药没处买。从那以后我每次接这种半成品项目,第一步都是git init加一次 commit,把原始状态钉死,再谈改造。希望帮到你。
本文还有配套的精品资源,点击获取