
简介这是一份基于Python爬虫获取中国多城市PM2.5监测数据并进行可视化分析的完整期末大作业资源面向高校数据可视化、大数据分析相关课程设计或期末项目场景也适合希望快速上手爬虫与可视化全流程的初学者。压缩包共21个文件包括1个Python主程序、覆盖北京、上海、广州、成都、沈阳等城市多年记录的5个CSV数据文件、13个图表HTML展示页面以及2份实验报告与数据说明文档整体大小约6.13MB。源码含详细注释实验报告涵盖分析思路、实现步骤与结果展示按模块组织清晰方便对照学习与二次修改。目前已有352人学习下载。下载后按说明部署即可运行适合用来理解数据采集、清洗、分析与可视化交互的完整流程也可作为课程设计或期末大作业的实用参考。1. 基于Python爬虫的中国城市PM2.5值数据可视化分析期末课设到底在考什么基于Python爬虫的中国城市PM2.5值数据可视化分析这份资源我带学生改过好几版自己也拆过完整流程。它本质上是“数据获取数据清洗可视化呈现”一条线的综合课设五个城市北京、上海、成都、广州、沈阳2010—2015年逐小时PM2.5观测数据已经以CSV形式躺在data目录里main.py负责把数据读入、清洗、聚合最终输出一批带交互的HTML图表再配上两到三份Word实验报告文档。适用人群很明确正在做数据可视化课程设计或期末大作业的学生以及想补一份完整Python数据分析实操的从业者。需要先说透的是这个项目里爬虫只占一小段背景真正的拿分点在pandas数据处理和Pyecharts图表产出拿到手先别急着跑main.py把数据字段和output目录里的HTML文件对应关系看清楚后面能省一大半调试时间。2. 数据侧实战pandas合并五个城市的CSV先把脏数据收拾干净2.1 资源包里的数据长什么样data目录下是五个城市各自独立的CSV文件命名规律是“城市名PM20100101_20151231.csv”比如BeijingPM20100101_20151231.csv表示北京从2010年1月1日到2015年12月31日的逐小时记录。这类数据集的字段结构基本同构核心列包括时间标记year、month、day、hour和污染物浓度PM同时带DEWP、TEMP、HUM、PRES这类气象辅助列以及cbwd、Iws、precipitation、Iprec等风力和降水信息。这里我习惯把字段按用途拆成三组。分组字段列用途时间标记year, month, day, hour拼接时间索引做趋势分析和重采样目标指标PMPM2.5浓度值全项目分析的核心气象辅助DEWP, TEMP, HUM, PRES, cbwd, Iws, precipitation, Iprec相关性分析和实验报告背景描述很多同学上来就盯着PM列把气象列当垃圾丢掉这其实有点浪费。实验报告里“气象条件对PM2.5浓度的影响”这种章节靠的就是cbwd风向和DEWP露点这几列。哪怕你最后不做相关性分析保留这些列也能在报告里多写一段内容。关于“爬虫”这个词这里要讲清楚定位。这批数据原始来源是环境监测站点的逐小时记录技术上确实需要脚本去批量抓取和汇总所以才叫“基于Python爬虫”。但资源包里给的是成品CSV爬虫环节在实际操作中被前置了。我的建议是不要在爬虫上硬加戏把爬虫作为数据来源背景写进报告“数据获取”一节注明数据时间范围和字段含义即可课程设计的评分重点在清洗和分析。2.2 读取、时间解析与五表拼接读文件的姿势直接决定后面踩不踩坑。最常见的错误是直接pd.read_csv完事然后发现year列是float类型、PM列混着-999这种异常值后面构建时间索引时直接报错。我一般先做一个快速摸底把每个文件的shape和缺失量打印出来再决定用哪种清洗策略。import pandas as pd import os DATA_DIR data CITIES [Beijing, Shanghai, Chengdu, Guangzhou, Shenyang] dfs [] for name in CITIES: path os.path.join(DATA_DIR, f{name}PM20100101_20151231.csv) df pd.read_csv(path, encodingutf-8) df[city] name # 先看每个文件的基本情况和缺失规模 print(name, df.shape, PM缺失:, df[PM].isna().sum()) dfs.append(df) df_all pd.concat(dfs, ignore_indexTrue) df_all[dt] pd.to_datetime( df_all[[year, month, day, hour]].astype(int) ) df_all df_all.sort_values([city, dt]).reset_index(dropTrue) print(df_all[city].value_counts())这段代码有几个关键点。pd.read_csv里encoding指定utf-8如果读取报UnicodeDecodeError改成gbk重试这是Windows环境下最常见的中文编码问题。df_all[[“year”, “month”, “day”, “hour”]].astype(int)这一步很关键因为部分CSV中间混了空行或异常分隔符pandas会把这些列推断成float不转int直接丢给to_datetime会报错。dt列建成之后按城市和时间排序保证后续插值和重采样的数据是连续且有序的。concat时用了ignore_indexTrue避免五个DataFrame各自的索引叠加造成错位。这里还隐含一个参数选择的考虑五个文件共约26万行逐小时数据这个体量用pandas串行处理完全够用不需要上Dask或Spark。如果在答辩时被问到“数据量大怎么办”你可以直接说当前规模下pandas内存操作性能足够26万行级别的groupby和resample都是毫秒级响应这也是课程设计的合理技术边界。2.3 缺失值处理和重采样处理缺失值是这份资源里最容易拉开分差的地方。直接dropna是最省事的做法但代价是时间轴断裂。逐小时数据里PM列经常出现连续几小时甚至一整天缺失的情况硬删除会让后续日均值计算产生系统性偏差。我的做法是先替换异常值标记再做线性插值最后按天重采样。# 部分文件用-999标记异常值先统一替换成NaN df_all[PM] df_all[PM].replace(-999, pd.NA) # 按城市分组后线性插值缺失段用前后值衔接 df_all[PM] ( df_all.groupby(city)[PM] .transform(lambda s: s.interpolate(methodlinear, limit_directionboth)) ) # 重采样到日粒度日均值更稳定也方便后续绘图 df_hour df_all.set_index(dt) df_daily ( df_hour.groupby(city)[PM] .resample(D) .mean() .reset_index() ) print(df_daily.groupby(city)[PM].describe().round(1))插值为什么用transform而不是apply因为transform会保留原始索引并返回与原DataFrame等长的序列方便直接赋值回df_all[“PM”]列不会因为分组后索引重置而错位。limit_direction“both”的意思是序列开头和结尾的缺失值也允许被插值否则首尾缺失段会原样保留为NaN。重采样到“D”粒度之后逐小时的噪声被平均掉画出来的趋势线更平滑也更适合放在实验报告里展示季节规律。这一步做完建议顺手把df_daily保存成一份中间结果CSV。原因很实际你后面调整图表参数时会反复跑脚本每次都重新读原始数据、重新插值重采样浪费时间不说万一中间改了清洗逻辑前后图表数据不一致就麻烦了。保存中间结果算是给自己留一颗后悔药。3. 可视化侧选型Matplotlib打底Pyecharts出交互式HTML3.1 为什么课设里要用交互式HTML这个问题几乎每次答辩都会被问到为什么不用Matplotlib画静态图我给的回答很直接交互式HTML能在答辩现场直接演示——鼠标悬浮显示具体数值、拖拽缩放时间轴、图例开关切换城市这些是静态PNG给不了的体验。而且output目录里全是.html文件说明这套资源的设计思路就是浏览器直开不需要额外装查看器。课程设计评分里“展示效果”通常占10到15分交互式图表在这块有明显优势。技术选型上Pyecharts是ECharts的Python封装底层渲染是JavaScript生成的是独立HTML文件不依赖Jupyter Notebook环境。版本方面要特别注意Pyecharts 1.x和2.x的API差异很大1.x里用Page、Overlap的写法2.x改成链式调用add_yaxis。这份资源里的代码是按2.x写的如果你本地装的是老版本建议直接pip install pyecharts升级。用Matplotlib打底的意思是先用静态图快速看数据分布确认哪些城市、哪些时间段有异常再决定正式图表怎么画。这不算多写代码而是减少来回调试HTML的次数毕竟HTML渲染一次比PNG慢不少。数据可视化分析的核心是“让数据自己说话”你先用草稿图把话听明白再用正式图把话说出去。3.2 按小时粒度画全年趋势对于单个城市的长时间趋势我一般用Line图加DataZoom组件。DataZoom是Pyecharts里最好用的交互组件之一可以在图表底部拉出一个滑块自由缩放时间范围。这里给出一个标准模板你可以直接套用到任意城市。from pyecharts.charts import Line from pyecharts import options as opts def city_line_chart(df_city, city_name, output_path): 生成单城市PM2.5日均值趋势折线图 line ( Line(init_optsopts.InitOpts(width1200px, height600px)) .add_xaxis(df_city[dt].dt.strftime(%Y-%m-%d).tolist()) .add_yaxis( PM2.5, df_city[PM].round(1).tolist(), is_smoothTrue, is_symbol_showFalse, # 隐藏数据点符号6年数据点太多 label_optsopts.LabelOpts(is_showFalse), ) .set_global_opts( title_optsopts.TitleOpts(titlef{city_name} PM2.5 日均值趋势), tooltip_optsopts.TooltipOpts(triggeraxis), datazoom_opts[ opts.DataZoomOpts(range_start0, range_end100), opts.DataZoomOpts(type_inside), # 支持鼠标滚轮缩放 ], yaxis_optsopts.AxisOpts(namePM2.5浓度), ) ) line.render(output_path) print(f已生成: {output_path})is_smoothTrue把折线变成平滑曲线视觉上更干净因为日均值序列本身波动很密锯齿感太强会显得像噪声。is_symbol_showFalse隐藏数据点六年日均值有近2200个点全部画点会糊成一片。datazoom同时配置滑块和inside两种模式答辩现场既能拖底部滑块也能直接滚轮缩放这个交互细节很容易被老师留意到。render输出的是相对路径建议调用时传入绝对路径避免运行目录不一致导致文件写到别处去。3.3 按城市对比的聚合图单城市趋势图只能看个体实验报告第二大部分通常是横向对比。对比图我推荐用年度均值柱状图它信息密度高一眼能看出哪个城市污染最重、年度变化趋势是什么。把5个城市、6年的数据压缩成一张图是期末大作业里性价比最高的图表类型。from pyecharts.charts import Bar # 基于日粒度数据计算城市年均值 yearly_mean ( df_daily.assign(yeardf_daily[dt].dt.year) .groupby([city, year])[PM] .mean() .unstack() ) # 各城市6年总均值用于横向对比 city_mean yearly_mean.mean(axis1).sort_values(ascendingFalse) cities city_mean.index.tolist() values city_mean.round(1).tolist() bar ( Bar(init_optsopts.InitOpts(width1000px, height500px)) .add_xaxis(cities) .add_yaxis( PM2.5年均值, values, category_gap50%, label_optsopts.LabelOpts(positiontop, formatter{c} μg/m³), ) .set_global_opts( title_optsopts.TitleOpts(title各城市PM2.5年均值对比2010-2015), yaxis_optsopts.AxisOpts(name浓度μg/m³), ) ) bar.render(os.path.join(OUTPUT_DIR, city_mean_compare.html))yearly_mean这个中间变量是宽表结构行是城市列是年份方便同时算城市均值和年度变化。city_mean排序后传入柱状图排序这步很重要默认按城市名排序会打乱对比的直观性降序排列能让柱状图从左到右形成“从重污染到相对干净”的阅读顺序。category_gap“50%”控制柱宽比例6个城市用50%看起来比较匀称。label格式化加上单位μg/m³报告截图时不需要额外标注单位。数据可视化分析做到这里其实已经覆盖了两个核心输出时间趋势和空间对比。剩下的事情是确定output目录里哪些HTML对应报告的哪个章节。我的经验是图表按“第几章-第几张”编号比如1-1.html对应报告第一章第一节的图2-3.html对应第二章第三张图3-Beijing.html这种带城市名的对应单城市专题分析所有文件打开看一眼标题就能确认比靠文件名猜靠谱。4. 避坑指南PM2.5课设里最容易翻车的五个细节4.1 数据读取与清洗的坑坑一读取后year列变成浮点型构建时间索引直接报错现象pd.read_csv读入后df[“year”].dtype显示float64执行pd.to_datetime时报“invalid string”或整数转换失败。原因CSV中间存在空行或字段错位pandas在推断数据类型时放弃整型退回到float64以容纳NaN。这在五个城市文件里不是必现的但只要有其中一个文件出问题concat之后整个df_all的year列都会被带偏。解决读文件后不急着用原始列先统一执行df_all[[“year”, “month”, “day”, “hour”]].astype(int)。前提是确认空行已经被read_csv默认跳过如果还有非数值脏数据astype会抛异常那就先用pd.to_numeric配合errors“coerce”把非法值转NaN再dropna。坑二PM列存在-999之类的异常标记直接算均值把结论拉偏现象统计各城市PM均值发现某个城市数值高得离谱或者最值出现在明显不合理的时段。原因部分监测站在设备故障时段用-999占位pandas不知道这是异常标记把它当真实数值参与聚合。如果缺失率在2%以下还好但个别月份可能集中出现算出来的年均值偏差能到十几μg/m³。解决读取后立即执行df_all[“PM”] df_all[“PM”].replace(-999, pd.NA)把异常值纳入缺失值体系再统一走插值或删除逻辑。这里不建议直接用where做条件替换因为部分文件可能混用“-999”和“NA”字符串两种标记replace一次处理更干净。坑三插值在分组后错位PM列顺序全乱现象执行df_all.groupby(“city”)[“PM”].apply(lambda s: s.interpolate())后城市数据出现串行北京的值跑到上海的行里。原因groupby.apply在部分pandas版本里会压缩索引并尝试“对齐”赋值回去的时候如果没按原始索引对齐就会出现整段漂移。这是pandas操作里非常隐蔽的错位问题不对比原始数据很难发现。解决用groupby.transform替代applytransform保证返回结果的索引与原始DataFrame一致。代码写完后做个验证df_all.groupby(“city”)[“PM”].count()和原始文件的行数做对比数量对不上就是错位了。4.2 可视化输出与文件管理的坑坑四main.py运行后没有任何HTML文件生成Terminal只显示exit code 0现象脚本正常跑完无任何报错但output目录里找不到新文件。首次遇到这种情况的人容易怀疑脚本有问题退出码却明明白白是0。原因脚本里用了相对路径写render比如render(“output/xxx.html”)。运行时工作目录不在项目根目录而是Python解释器所在目录文件被写到了一个你可能压根没注意到的路径下。Pyecharts不会因为目录不存在而报错但如果你的脚本没提前创建output目录它会直接把文件写到当前目录。解决在脚本开头固定项目根目录用os.path.join拼接所有输出路径确保不依赖运行时工作目录。import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) DATA_DIR os.path.join(BASE_DIR, data) OUTPUT_DIR os.path.join(BASE_DIR, output) os.makedirs(OUTPUT_DIR, exist_okTrue)处理完跑一次ls输出目录确认文件时间戳是最新的再开始做图表标题和报告章节的映射。坑五爬虫代码过段时间就跑不通拿不到最新数据现象照着实验报告里的爬虫代码复现发现请求报403或者返回空数据以为是自己的环境问题折腾半天无果。原因项目数据采集时间为2010至2015年原爬虫代码针对的是当年的站点接口现在接口结构、请求头校验、Cookie策略大概率已变更。这类代码的有效期本质上是阶段性的跟数据可视化分析本身无关属于数据获取侧的时效性问题。解决课程设计直接用仓库里给的成品CSV爬虫部分在报告里作为数据来源说明来写描述“通过Python脚本采集五个城市监测站逐小时记录”。不要在答辩现场演示爬虫除非你确认目标数据源当前仍可用且已获取授权。把精力放在清洗和可视化上这才是期末大作业真正的评分区域。5. 最后的加分项图号映射与实验报告结论素材output目录里那一堆HTML文件改成项目结构时确实容易让人摸不着头脑。我的做法是维护一张图号映射表每生成一个图表就登记“文件编号—图表标题—报告章节”三列信息。这个方法看起来笨但答辩前检查材料时非常省心不用一个个打开HTML确认内容。输出文件标题内容特征报告建议位置1-1.html 至 1-5.html第一篇章核心图表含数据概览与趋势报告第一章“数据来源与总体分析”2-1.html 至 2-3.html对比分析与聚合图表报告第二章“多城市对比分析”3-城市名.html单城市专题深度分析报告第三章“城市专题研究”映射表建好后再跑一段统计代码把报告“实验结论”章节需要的数字一次性打印出来。不要手抄数据手抄必出错。# 生成实验报告结论素材 summary ( df_daily.groupby(city)[PM] .agg([mean, max, median]) .round(1) .sort_values(mean, ascendingFalse) ) print(summary) # 冬季vs夏季均值差异 df_daily[month] df_daily[dt].dt.month season df_daily.groupby([city, month])[PM].mean().unstack() print(season[[1, 2, 12]].mean(axis1).round(1)) # 冬半年 print(season[[6, 7, 8]].mean(axis1).round(1)) # 夏半年这段代码输出的是城市排名和季节差异基本覆盖了实验报告结论部分百分之七十的数据论据。剩下的百分之三十留给气象列相关性分析选一个城市做TEMP和PM的散点图结论写“温度较高时段PM2.5浓度整体偏低”这类可验证描述即可。从那以后我每次经手可视化课设都会强制自己走一遍三件事打开每个HTML核对标题、确认图号与报告章节映射、把关键统计数字打印出来贴进实验总结。这套流程帮我躲过不少答辩前夜的尴尬希望帮到你。本文还有配套的精品资源点击获取