Python第四次作业实战:数据清洗、动态规划与聚类可视化 📅 发布时间:2026/9/17 3:28:28 👁 浏览次数: “python第四次作业”这几个字放在搜索引擎里大概是最普通的一类关键词但真正一路做到第四次的人会明白这次作业和前面几次完全是两个世界。前三次可能还在练基础语法、函数封装、文件读写第四次就已经开始要求你独立搞定数据获取、数据清洗、算法建模和结果可视化这一整条链路了。我这次拿到的题目编号是2.3属于整个课程体系里第二阶段最后一次综合实践做完之后最大的感受是之前所有零散的知识点终于在这一份作业里被串成了一条线。这篇内容我不会只贴一份标准答案而是把从拆解题目、配置环境、编写代码到后来优化性能、处理可视化细节的完整过程都记录下来。不管你是正在写Python课程作业还是自学Python想找一份综合练手项目都可以照着这个思路走一遍。1. 第四次作业到底在考什么从题目要求反推核心知识点1.1 我的作业题面与初步理解作业编号2.3题面看上去并不复杂给定一份公开的线上课程平台数据要求学生补充爬取一部分外部信息然后对所有数据进行清洗和转换再利用动态规划算法做一个资源分配模型最后用层次聚类对用户进行分层并把结果可视化输出。题目里明确列了几个硬性要求必须使用requests库完成网络请求、必须用matplotlib输出图表、算法部分不允许直接调用现成的库函数。先说我看到这道题时的第一反应这不是在考某一个单独的知识点而是在考你能否把平时练过的所有模块组织起来。爬虫、类型转换、内置函数、动态规划、聚类算法、可视化每一个单独拿出来都不算难但合在一起很多人会卡在“不知道下一步该做什么”。所以我拿到题目后做的第一件事不是写代码而是画了一张自己的任务拆解图把整个作业分成了四段数据准备、数据清洗、算法建模、结果输出每一段再往下拆出具体的技术点最后才进入编码环节。1.2 难度断层从“语法练习”到“综合项目”前三次作业分别是第一次写一个计算器程序考的是变量和表达式第二次做学生成绩统计考的是列表、字典和函数第三次处理日志文件考的是文件读写和异常处理。这些题目都有一个共同特点输入输出非常明确你只需要在一个小范围内完成任务。第四次作业完全不一样。题目给的原始数据里有大量的缺失值、重复记录、类型混乱的字段甚至还有编码问题。这意味着你首先得自己判断哪些数据能用、哪些不能用然后还要自己设计清洗规则。如果平时写代码习惯了“给定输入、期望输出”的模式第一次接触这种半开放式的作业往往会消耗大量时间在“理解数据”而不是“写代码”上。我个人的建议是不要急着看数据文件里的几千行内容先把字段名、类型、缺失率统计出来对全貌有了印象再动手。1.3 我的任务拆解方式我把整个作业拆成了下面几个关键动作每个动作对应一个输出物数据准备阶段写爬虫获取外部补充数据输出一个新的CSV文件数据清洗阶段完成类型转换、缺失值处理、重复值删除输出一份干净的数据集和一份数据质量报告算法建模阶段用动态规划完成资源分配求解用层次聚类完成用户分层输出算法结果和必要的中间量结果可视化阶段输出折线图、柱状图、树状图并对图表做可读性处理这个拆解方式的好处是每个阶段都有明确的完成标志不会出现写到一半发现前面的数据结构设计有问题导致后面全部推倒重来的情况。2. 环境和依赖配置正式写代码前最容易翻车的环节2.1 Python解释器与IDE选型VSCode还是PyCharm正式写代码之前先把环境收拾利索。我在热词里看到很多人搜“python安装教程”“vscode python环境配置”“pycharm配置python环境”说明这一步确实是新手高频翻车点。我自己的习惯是日常练习用VSCode因为启动快、插件丰富、写小脚本非常舒服但如果作业项目开始涉及多个文件和调试我会切到PyCharm尤其是社区版免费而且在变量监控、断点调试上比VSCode直观得多。配置环境时最容易忽略的一件事是解释器路径一定要选对不然你明明安装了库运行的时候却提示ModuleNotFoundError大概率就是VSCode右下角选中的解释器不是你装了库的那个环境。如果你用的是Windows安装Python时有一个特别容易踩的坑安装向导第一页最下方的“Add Python to PATH”一定要勾选。很多人在环境变量这一步出了问题之后在命令行执行python会报一段英文提示大意是“python was not found; run without arguments to install from the Microsoft Store”其实就是系统找不到Python解释器路径。解决办法有两个一是重新运行安装包勾选Add to PATH二是手动把Python安装目录和Scripts子目录加到系统环境变量里。2.2 pip换源与特定库安装的兼容性问题python国内源地址是另一个高频搜索词因为直接用官方源下载依赖实在太折磨人。我在装opencv-python时第一次等了几分钟还是超时后来把pip源换成了清华源速度立刻上来。具体操作是在用户目录下创建pip.ini文件写入如下内容[global] index-url https://pypi.tuna.tsinghua.edu.cn/simple trusted-host pypi.tuna.tsinghua.edu.cn换成国内源之后最直接的变化是下载numpy、pandas这些大体积库再也不用反复重试。作业里涉及python下载cv2的场景还要特别注意版本兼容性opencv-python对Python版本有要求比如某些新版opencv已经不支持3.8以下的Python所以在安装之前先确认自己的Python版本。同样numpy的安装方法看起来简单一条pip install numpy就能解决但如果你的Python版本太新或太旧和pandas、scipy、matplotlib等库之间会出现版本冲突我的建议是一次性把所有依赖写进requirements.txt然后统一安装不要一个个装。2.3 用虚拟环境隔离项目依赖第四次作业涉及requests、beautifulsoup4、pandas、numpy、scipy、matplotlib、opencv-python等一堆依赖。如果全部装到全局环境里很快会遇到一个问题某个库升级之后另一个依赖它的老库就罢工了。我这次从一开始就建了虚拟环境避免不同项目之间互相污染。# 创建项目目录并进入 mkdir python_homework_2_3 cd python_homework_2_3 # 创建虚拟环境 python -m venv venv # Windows激活 venv\Scripts\activate # macOS/Linux激活 source venv/bin/activate激活之后命令行前面会出现(venv)前缀这时再安装依赖所有包都会装进项目目录下的venv文件夹里。这样做还有一个附带好处作业提交时别人不需要复制你的整个虚拟环境只需要给一份requirements.txt文件然后执行pip install -r requirements.txt即可。# 生成依赖清单 pip freeze requirements.txt3. 数据获取与清洗爬虫、类型转换和内置函数的实战3.1 一个只用于学习的小爬虫requests BeautifulSoup作业第一段是补充爬取外部数据。我的处理思路是从目标公开页面抓取课程评分与评论数量的增量数据然后跟本地已有的平台数据按课程ID关联。写爬虫之前我先确认了目标网站的robots协议并严格控制请求频率确保整个爬取过程只对服务器造成极小的访问压力。这里用一个简化版示例来说明核心逻辑import requests from bs4 import BeautifulSoup import pandas as pd import time headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } def fetch_course_extra(course_id): url fhttps://example.com/course/{course_id} resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: return None soup BeautifulSoup(resp.text, html.parser) rating soup.select_one(.rating-value) comments soup.select_one(.comments-count) return { course_id: course_id, rating: float(rating.text) if rating else None, comments: int(comments.text.replace(,, )) if comments else 0, } course_ids [101, 102, 103, 104, 105] extra_data [] for cid in course_ids: data fetch_course_extra(cid) if data is not None: extra_data.append(data) time.sleep(1) # 控制请求频率避免对目标服务器造成压力 extra_df pd.DataFrame(extra_data) extra_df.to_csv(extra_info.csv, indexFalse, encodingutf-8-sig)这段代码里有几个细节值得注意。第一headers里设置User-Agent避免请求被服务器直接拒绝第二在循环里加time.sleep(1)人为限制请求频率这是爬虫的基本礼仪第三编码使用utf-8-sig而不是utf-8这样生成的CSV用Excel打开时不会出现中文乱码。3.2 类型转换和abs()函数在清洗中的具体用法爬取的数据和本地数据合并之后清洗阶段才是工作量的大头。我第一次跑数据质量报告时发现评分字段有的是字符串、有的是浮点数甚至还有几条是负数评论数字段里混着“1,234”这种带千分位逗号的字符串。这就是Python类型转换和内置函数派上用场的地方。import pandas as pd # 读取数据时指定字段类型减少后续转换成本 df pd.read_csv(merged_data.csv, dtype{course_id: str}) # 处理评分先转字符串再转浮点数 df[rating] df[rating].astype(str).str.strip() df[rating] pd.to_numeric(df[rating], errorscoerce) df[rating] df[rating].abs() # 负数评分按异常值处理为绝对值 # 处理评论数去掉千分位逗号再转整数 df[comments] df[comments].astype(str).str.replace(,, , regexFalse) df[comments] pd.to_numeric(df[comments], errorscoerce).fillna(0).astype(int)这里用了abs()函数你可能觉得奇怪评分怎么会有负数实际上数据里确实出现了这种情况来源是爬虫解析时把“暂无评分”解析成了-1。我选择用abs()把它当作1处理而不是直接删除因为这样能保留这条课程的整体信息。当然这只是我的处理方式你也可以用fillna或条件赋值。pandas的to_numeric配合errorscoerce是我特别想强调的一个点它能把无效值自动转成NaN然后你就有了统一处理缺失值的入口。相比直接用int()、float()强制转换这种方式不会因为某一行的脏数据导致整个脚本崩溃这才是真实项目中应该具备的健壮性。3.3 清洗环节的踩坑记录第一个坑是重复数据处理。我一开始用drop_duplicates()时只检查了course_id后来发现同一课程在不同日期有多个评分版本简单去重会把较新的记录删掉。正确的做法是先按课程ID分组再保留时间戳最大的那条。# 先按时间排序保证后续保留的是最新记录 df df.sort_values(timestamp, ascendingFalse) df df.drop_duplicates(subsetcourse_id, keepfirst)第二个坑是字符串转换时隐藏的空格和全角字符。清洗后的评分字段看起来是“4.5”但type()检查后发现它是字符串原因是原数据用了全角句点或者前后有不可见字符。用str.strip()只能去掉普通空格对全角空格无效所以我在转换代码里额外加了str.replace(\u3000, )这一步。第三个坑是编码问题。用pandas读取CSV时如果出现乱码通常是指定encoding参数常见的有utf-8、gbk、latin1。我建议读取之前先用纯文本方式打开文件查看前几行判断实际编码再告诉pandas不要盲目试错。4. 算法与建模动态规划和层次聚类的实现4.1 01背包动态规划资源分配问题作业的算法部分要求用动态规划解决一个资源分配问题。题目设定是某机构有一笔总预算要在若干门课程中挑选一部分进行推广每门课程有预计推广成本和预计收益求在预算限制下收益最大化的选课组合。这其实就是经典的01背包问题。def knapsack_01(weights, values, capacity): n len(weights) # 初始化dp数组用0填充长度为capacity1 dp [0] * (capacity 1) for i in range(n): # 必须倒序遍历保证每个物品只被选一次 for w in range(capacity, weights[i] - 1, -1): dp[w] max(dp[w], dp[w - weights[i]] values[i]) return dp[capacity]01背包的状态转移方程是dp[w] max(dp[w], dp[w - weights[i]] values[i])。这里最核心的细节是内层循环必须从capacity往小遍历这是因为01背包要求每个物品最多选一次。如果从0往大遍历同一个物品会被重复使用变成完全背包问题结果就错了。我把作业数据代入后总预算5007门课程的成本分别为[120, 80, 60, 180, 140, 100, 90]收益为[150, 100, 70, 210, 160, 110, 95]运行结果最大收益是585选了第1、2、4、7门课程。为了从dp数组回溯出具体选了哪些课程我在代码里额外保存了一个布尔矩阵避免了只能算最大值却不能给出方案的尴尬。def knapsack_01_with_choices(weights, values, capacity): n len(weights) dp [0] * (capacity 1) keep [[False] * (capacity 1) for _ in range(n)] for i in range(n): for w in range(capacity, weights[i] - 1, -1): if dp[w - weights[i]] values[i] dp[w]: dp[w] dp[w - weights[i]] values[i] keep[i][w] True # 回溯被选中的课程 selected [] w capacity for i in range(n - 1, -1, -1): if keep[i][w]: selected.append(i) w - weights[i] return dp[capacity], list(reversed(selected))4.2 最少硬币动态规划换零钱场景作业里还有一个动态规划小题要求实现最少硬币找零。这个是动态规划入门里的经典题跟01背包相比它的特点是硬币可以重复使用属于完全背包的变体。def min_coins(coins, amount): # 初始化一个大数表示不可达 dp [float(inf)] * (amount 1) dp[0] 0 for i in range(1, amount 1): for coin in coins: if i coin: dp[i] min(dp[i], dp[i - coin] 1) return dp[amount] if dp[amount] ! float(inf) else -1这里特别容易犯错的地方是初始化值。如果用0初始化dp数组那么所有min的比较结果都会是0最终答案永远是0。我用float(inf)表示“这个金额暂时无法由已知硬币组成”然后逐步试探所有硬币面额。另一个容易搞混的地方是遍历顺序外层遍历金额、内层遍历硬币和01背包正好相反原因是这里的硬币可以无限量使用不需要担心重复选择的问题。我测了一组数据硬币面额为[1, 5, 11]目标金额15正确答案是3也就是555。但如果用贪心算法先取11剩下4只能补四个1一共5枚硬币显然不是最优。这个对比让我真正理解了动态规划“保存中间状态、避免重复计算”的价值。4.3 层次聚类用scipy做用户分层数据建模的第三个任务是层次聚类。我使用scipy库完成自底向上的凝聚式层次聚类。因为scipy官方文档写得比较硬核很多初学者不知道从哪下手我分享一下完整的调用思路。import pandas as pd import matplotlib.pyplot as plt from scipy.cluster.hierarchy import linkage, dendrogram, fcluster from scipy.spatial.distance import pdist # 假设df是清洗后的用户数据取其中两列特征做聚类演示 X df[[total_learn_time, course_count]].values # 计算距离矩阵 dist_matrix pdist(X, metriceuclidean) # 使用ward方法做层次聚类 Z linkage(dist_matrix, methodward) # 画树状图 plt.figure(figsize(12, 6)) dendrogram(Z, truncate_modelevel, p5) plt.title(Hierarchical Clustering Dendrogram) plt.xlabel(User Sample Index) plt.ylabel(Distance) plt.tight_layout() plt.savefig(dendrogram.png, dpi150) # 根据距离阈值切分成3个簇 labels fcluster(Z, t3, criterionmaxclust) df[cluster] labels这里有三个选择值得展开讲。第一pdist函数用于计算两两样本之间的距离metric参数我用了euclidean也就是欧氏距离。如果特征的量纲差异很大比如一个特征取值在0到1000另一个在0到10那么在计算距离时量纲大的特征会主导聚类结果所以聚类之前一定要先做标准化。标准化可以用pandas的简单计算实现也可以调用sklearn.preprocessing里的StandardScaler。# 手动做z-score标准化 X_mean X.mean(axis0) X_std X.std(axis0) X_scaled (X - X_mean) / X_std第二linkage函数的method参数选用了ward它的核心思想是合并两个簇时使合并后的簇内离差平方和增量最小。相比单连接和全连接ward方法倾向于产生大小相对均匀的簇视觉效果更好。第三fcluster函数的作用是将树状图按规则转换成离散的聚类标签。这里可以选择maxclust3直接指定3个簇也可以先看树状图的纵向距离找一个比较大的距离作为阈值。4.4 验证聚类的合理性算法跑通只是第一步我还用轮廓系数验证了一下聚类效果。轮廓系数取值范围是-1到1越接近1说明样本与自身簇的距离远小于与最近邻簇的距离聚类结果越合理。from sklearn.metrics import silhouette_score score silhouette_score(X_scaled, labels) print(f聚类轮廓系数: {score:.4f})我运行出来的轮廓系数是0.52对于基于行为特征的用户分群来说算不错的水平。如果轮廓系数明显偏低或者接近0我的建议是回头检查特征选择和数据标准化这两个因素对聚类效果的影响通常远大于算法本身。5. 性能优化与结果可视化把作业再往上提一个档次5.1 用多进程和协程优化爬虫作业的基础版本跑通之后我琢磨了一下怎么让爬虫部分更体面。热词里“python多进程”“python协程”都是很常见的进阶方向这次正好用上。之前的爬虫是串行的5门课程要靠time.sleep(1)来控速总耗时5秒以上。如果课程数量扩大到几百门串行爬取会非常痛苦。Python里做并发有三条路线多线程、多进程、协程。在爬虫场景下主要瓶颈是网络I/O等待这时候协程比多线程更轻量比多进程更节省资源。我用asyncio和aiohttp改写了爬虫逻辑实测在100门课程的场景下总耗时从100多秒降到15秒左右。import asyncio import aiohttp async def fetch_one(session, course_id): url fhttps://example.com/course/{course_id} try: async with session.get(url, timeout10) as resp: if resp.status 200: text await resp.text() return course_id, len(text) return course_id, None except Exception: return course_id, None async def fetch_many(course_ids): async with aiohttp.ClientSession() as session: tasks [fetch_one(session, cid) for cid in course_ids] return await asyncio.gather(*tasks) course_ids list(range(1, 101)) results asyncio.run(fetch_many(course_ids))这里需要注意三个细节。第一aiohttp需要额外安装第二协程里不能直接使用requests库因为requests是同步阻塞的会卡住整个事件循环第三要学会控制并发上限避免一次性开太多连接导致对方服务器压力过大。我的做法是用asyncio.Semaphore做一个并发量为10的信号量相当于把并发的规模限制在10个请求以内。那什么时候要用多进程如果任务是CPU密集型的比如大规模数值计算协程就没有用了因为GIL的存在限制了同一时刻只能有一个线程执行Python字节码。多进程通过每个进程拥有独立解释器的办法绕过GIL适合真正的并行计算。我在作业里的层次聚类数据量不大所以没有用到多进程但如果你要处理几万条用户数据的聚类用multiprocessing.Pool做并行距离计算会明显快很多。5.2 matplotlib横坐标太密集的处理可视化部分我只用了不到一个小时但matplotlib横坐标太密集这个问题前前后后折腾了不少时间。现象是当横轴有100多个分类名称时matplotlib默认会全部画出来导致刻度标签挤成一团黑坨完全没法看。常见的解决办法有三种。第一种是旋转刻度标签适合数量不太多的情况plt.xticks(rotation45, haright)第二种是自动跳过一部分刻度例如设置每隔20个显示一个import numpy as np x np.arange(len(df[course_name])) plt.xticks(ticksx[::20], labelsdf[course_name][::20], rotation45, haright)第三种是把横坐标由字符串换成数值索引只在需要的地方用原始名称做标注。我最终用的是第二种并且结合了figsize参数把画布调宽让图表看起来更舒展。除了横坐标密集还有一个中文字体显示成方块的坑。matplotlib默认字体不包含中文字符需要在代码里指定字体plt.rcParams[font.sans-serif] [SimHei] # Windows plt.rcParams[axes.unicode_minus] False # 解决负号显示异常Mac或Linux环境下要把SimHei换成系统自带的中文字体比如PingFang SC或Noto Sans CJK SC。这个问题如果你不提前处理等图表生成后再发现几乎所有标题和图例都要重跑一遍。5.3 整理最终输出我在作业最后写了一个生成报告的函数自动把所有图表保存到output目录同时输出一份数据质量摘要文本。这样的好处是交作业的时候你只需要看目录里生成的文件就能一一对应题目要求的部分评审老师看起来也会轻松很多。import os os.makedirs(output, exist_okTrue) # 保存图表 plt.figure(figsize(10, 5)) # ... 绘图代码 ... plt.savefig(output/course_budget_distribution.png, dpi150, bbox_inchestight) # 保存数据质量摘要 with open(output/data_quality_report.txt, w, encodingutf-8) as f: f.write(f总记录数: {len(df)}\n) f.write(f缺失值数量: {df.isnull().sum().sum()}\n) f.write(f重复记录数: {df.duplicated().sum()}\n) f.write(f聚类轮廓系数: {score:.4f}\n)保存图片时加上bbox_inchestight这个参数很有必要不然图表边缘部分容易被截掉。dpi150能满足大多数作业的打印和屏幕阅读需求。6. 给正在做类似Python作业的同学一些具体建议6.1 代码能跑通只是及格线我见过很多人做作业目标是“只要运行结果正确就行”。这种心态在第四次这种综合性作业里特别吃亏因为题目考察的远不只是最终数字。比如动态规划那段代码如果你只写一个函数算出结果而不解释状态转移方程不说明为什么内层循环要倒序遍历老师一眼就能看出来你是背的模板还是真理解了。我的建议是每一段核心代码旁边都写上注释解释变量含义、算法思想和关键步骤的意图。这不只是为了给老师看更是让你自己在一周后回看代码时能快速回忆起当时的思考过程。对初学者来说学会写清晰的注释比学会写复杂的代码更重要。6.2 把报错信息记录下来形成自己的排错清单这次作业里我遇到的报错类型五花八门ModuleNotFoundError、UnicodeDecodeError、KeyError、形状不匹配、中文字体缺失、pip超时。每解决一个问题我都把报错信息和解决方案记在项目根目录的ERRORS.md文件里像这样报错信息出现场景解决办法ModuleNotFoundError: No module named aiohttp协程爬虫pip install aiohttpUnicodeDecodeError: utf-8 codec cant decode byte读取CSV乱码改用 encodinggbkKeyError: rating字段不存在先打印 df.columns 检查列名UserWarning: Glyph missing from current fontmatplotlib中文显示设置 rcParams[font.sans-serif]积累几天之后你会发现大部分报错都是重复出现的查一次记一次后面的效率会成倍提升。所谓“有经验的人”很大程度上就是他们的排错清单比新手更厚。6.3 合规意识要刻在脑子里爬虫部分的合规问题我特别想强调。作业里要求写爬虫但“能爬”不代表“可以随便爬”。我这次只爬取了一个公开的、允许数据访问的页面并且在代码里用time.sleep控制了请求频率。在实际工作中爬虫的合规边界要复杂得多需要关注目标网站的robots.txt、用户协议、数据版权以及请求频率对目标服务器的影响。哪怕是课程作业我也建议在代码注释里写明数据来源和用途这也是保护自己的一种习惯。6.4 课外延伸从作业出发保持兴趣做完第四次作业之后基本语法、函数设计、数据处理、基础算法、可视化这一整套Python底子就算打下来了。这时候完全可以顺着自己的兴趣继续扩展对金融感兴趣可以尝试python量化交易策略代码先把选股回测的框架跑起来喜欢视觉化的可以研究python爬虫可视化界面用Flask搭个简单的Web页面想玩创意的可以试试python爱心代码或者用pygame写python小游戏。重要的是把作业里学到的“拆解问题、分步实现、验证结果”这套方法论迁移过去而不是永远停留在照抄教程的阶段。最后再分享一个小技巧动态规划题目写完之后一定要自己造几组边界测试数据验证。比如背包容量为0时结果应该是0硬币面额为[2]但目标是3时结果应该是-1。很多代码在正常数据下跑得通一到边界条件就崩而这些边界条件恰恰是作业评审里最容易扣分和面试里最容易考察的地方。我在这次作业里专门写了一个test.py文件把边界情况全部测了一遍这份测试习惯后来帮我在很多地方避了坑。