Python疫情数据分析与可视化:从数据清洗到报告打包的完整流程
简介一份面向新冠肺炎疫情数据分析与可视化的Python完整课设项目适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业或毕业设计参考。项目经导师指导并获98分涵盖从数据采集、舆情爬取、时序预测到可视化展示的完整流程可直接学习或二次开发。资源共57个文件压缩包大小3.94MB包含17个Python脚本、2个ipynb笔记、5个CSV数据集、5个HTML交互页面及4个JavaScript脚本同时附有算法设计说明文档和项目说明。其中代码涉及微博评论情感分析、NLP处理、疫情时序数据预测算法、中国及世界疫情地图、词云与日历热力图等模块图文输出包括16张PNG图和若干SVG图便于对照验证结果。目前已有424人学习下载尤其适合希望快速搭建疫情数据分析可视化项目并补齐报告环节的同学。1. 拿到疫情数据分析课设的 zip先别急着解压它在解决什么问题“新冠肺炎疫情数据分析与可视化”是 python 课程设计里出现频率极高的题目但市面上流传的zip质量参差不齐。常见情况是解压后里面有.py、有写好的报告 PDF运行却报错——数据路径写死、图例中文变方块、日期解析错位。这个标题真正要交付的不是“跑出一张图”而是一个完整闭环从公开数据源拿到疫情数据用 python 做完清洗、聚合、可视化最后把结论写成报告连同代码一起打包成规范的 zip 文件。它适合三类人准备交课设的学生、想拿真实数据练手数据分析与可视化入门的新手以及需要一套可复用模板来处理任意时间序列数据的开发者。本文按实际交付流程写代码可以直接复现参数是我调过的坑是我踩过的。2. 数据获取与清洗公开数据集到干净 DataFrame 的三个步骤2.1 数据源怎么选公开数据集的格式与字段做疫情数据分析第一步不是写代码而是选数据源。常见做法是用公开的疫情时间序列数据集课程设计里用得最多的是约翰霍普金斯大学维护的那份 COVID-19 数据它在网络上有多个镜像分为time_series和daily_reports两种格式。前者是一个文件里按国家/地区展开所有日期适合做长期趋势分析后者是每天一个文件适合做事件分析。课设场景我一般选time_series格式因为一行一个国家直接读进来就能干不用批量读几百个文件。这份数据源的特点是“宽表”结构Province/State、Country/Region是维度列后面全是日期列列名就是日期。字段看起来是这样字段含义原始类型Province/State省/州部分国家为空strCountry/Region国家/地区strLat / Long坐标float1/22/20 等日期列当天累计确诊数int注意这里的日期列名是月/日/年的美式格式后续to_datetime如果不指定format很容易解析出两种结果这是我后面要重点讲的坑。多国数据里有的国家带省/州明细行比如中国每个省一行读进来后必须按国家聚合否则画出来的是省级碎片线没法看。2.2 清洗脚本合并、去重、缺失值处理与日期对齐清洗的思路是把“宽表”转成“长表”这是 pandas 处理时间序列的标准姿势。宽表适合人看长表适合机器算每一行是一个国家在某一天的观测值后续groupby、diff、rolling全都要靠这种结构。下面这段代码完成了读取、聚合、变形三步import pandas as pd # 1. 读取原始时间序列累计确诊 raw pd.read_csv(data/raw/time_series_covid19_confirmed_global.csv) # 2. 去掉坐标列和省级列只保留国家和日期 meta_cols [Province/State, Country/Region, Lat, Long] date_cols [c for c in raw.columns if c not in meta_cols] # 3. 按国家聚合同一个国家多省行求和 by_country raw.groupby(Country/Region, as_indexFalse)[date_cols].sum() # 4. 宽表转长表日期列熔化成两列 long_df by_country.melt( id_varsCountry/Region, value_varsdate_cols, var_namedate, value_nameconfirmed, )这段脚本的核心逻辑有三处。第一groupby(Country/Region, as_indexFalse)里as_indexFalse很关键它让分组列不变成索引否则后面melt和merge容易碰到索引对不上的问题。第二melt的value_vars直接传整个日期列列表省去逐列拼接的写法顺便把var_namedate指定清楚。第三原始 CSV 里如果缺了最后一列date_cols里不会出现这也是为什么用[c for c in raw.columns]而不是手写日期范围的原因——数据源加列减列都不影响脚本。2.3 聚合与特征工程累计、新增、七日移动平均累计确诊数只是原始值真正画趋势图要看“新增”和“移动平均”。新增确诊是累计值的一阶差分移动平均则用来抹平周末报告延迟带来的锯齿。实现上注意分国家计算别把 A 国的差值算到 B 国头上# 5. 日期列转成 datetime 类型并固定美式格式 long_df[date] pd.to_datetime(long_df[date], format%m/%d/%y) # 6. 按国家时间排序确保 diff 方向正确 long_df long_df.sort_values([Country/Region, date]) # 7. 计算每日新增 long_df[new_confirmed] ( long_df.groupby(Country/Region)[confirmed].diff() ) # 8. 缺失的 NaN 补 0并转成整数 long_df[new_confirmed] long_df[new_confirmed].fillna(0).astype(int) # 9. 7 日移动平均min_periods1 让前 6 天也有值 long_df[avg7] ( long_df.groupby(Country/Region)[new_confirmed] .rolling(7, min_periods1) .mean() .reset_index(level0, dropTrue) ) # 10. 保存处理结果后续分析和绘图都不碰原始数据 long_df.to_csv(data/processed/covid19_daily.csv, indexFalse)diff()第一个值一定是 NaN因为没有任何前一天的数据fillna(0)是安全做法但你要意识到这个 0 是“不存在”而不是“当天确实为 0”写报告时不必解释心里清楚就行。rolling(7, min_periods1)里的 7 不是拍脑袋的它对应一周的报告周期很多国家周末不更新数据周一数字偏低7 天窗口能把这些周期性波动抹掉。min_periods1保证序列头 6 天不全是 NaN否则画图时前一周直接断线。最后一步reset_index(level0, dropTrue)是 groupby rolling 组合的经典操作不写它你会得到一个带多余索引级别的 Series和long_df拼不到一起。3. 可视化选型与核心绘图代码matplotlib 和 pyecharts 的取舍与关键参数3.1 用 matplotlib 画趋势曲线参数与配色细节做数据分析课设matplotlib 是第一选择因为它出的 PNG 图可以直接贴进 Word 报告不需要评审打开 HTML。画多条国家趋势对比时最忌讳的就是折线密集到分不清谁是谁。我会控制国家数量不超过 5 个并且打开图例和横向网格线import matplotlib.pyplot as plt import matplotlib.dates as mdates # 中文字体要放在第一次绘图之前设置 plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei] plt.rcParams[axes.unicode_minus] False # 从清洗结果读取长表 df pd.read_csv(data/processed/covid19_daily.csv, parse_dates[date]) fig, ax plt.subplots(figsize(12, 6), dpi150) for country in [China, US, India]: sub df[df[Country/Region] country] ax.plot( sub[date], sub[avg7], labelcountry, linewidth2, ) ax.xaxis.set_major_locator(mdates.MonthLocator()) ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) ax.set_title(Top 3 国家新增确诊 7 日移动平均对比) ax.set_xlabel(日期) ax.set_ylabel(新增确诊数7 日均值) ax.legend() ax.grid(axisy, alpha0.3) plt.tight_layout() plt.savefig(figures/top3_trend.png, dpi150) plt.show()figsize(12, 6)和dpi150是配套关系。dpi150下输出图片像素是 12×1501800 宽插入 Word 或 PDF 后依然清晰如果figsize调小到 8还想保持清晰就得把dpi提到 200 以上。mdates.MonthLocator()只让横轴每月显示一个刻度否则 2020 到 2023 的日刻度会把标签挤成一团。grid(axisy, alpha0.3)只画横向网格比默认的框式网格干净。注意plt.rcParams两句必须放在plt.subplots之前位置放错就有概率改不生效。3.2 用 pyecharts 做交互图图表类型与配置项课设答辩现场交互图比静态图更能撑场面。pyecharts 是 echarts 数据可视化能力的 python 封装输出的是独立 HTML 文件浏览器打开就能缩放、悬浮、看数据点。做时间序列交互图我的标配是 Line DataZoom Tooltipfrom pyecharts.charts import Line from pyecharts import options as opts # 准备中国新增确诊的 7 日均值数据 chn df[df[Country/Region] China] x_data chn[date].astype(str).tolist() y_data chn[avg7].round(2).tolist() line ( Line(init_optsopts.InitOpts(width1200px, height600px)) .add_xaxis(x_data) .add_yaxis( 7 日均值, y_data, is_smoothTrue, label_optsopts.LabelOpts(is_showFalse), ) .set_global_opts( title_optsopts.TitleOpts(title中国新增确诊趋势), tooltip_optsopts.TooltipOpts(triggeraxis), datazoom_opts[ opts.DataZoomOpts(range_start0, range_end100), opts.DataZoomOpts(type_inside, range_start0, range_end100), ], xaxis_optsopts.AxisOpts(name日期), yaxis_optsopts.AxisOpts(name新增确诊), ) ) line.render(figures/chn_trend.html)init_opts里的width和height控制整个图表容器大小不是像素分辨率别和 matplotlib 的figsize混淆。is_smoothTrue让折线变成曲线适合展示平滑后的移动平均如果数据本身波动剧烈设成平滑会显得“预测味”太重。DataZoomOpts是交互图的核心价值——底部多了一个滑块读者可以拖拽看 2020 年春季暴发段也可以拉远看三年全貌。第一个是内置滑块第二个是鼠标滚轮缩放两个叠着用是 echarts 的标准配置。3.3 热力图与排名图多维度对比的可视化手法趋势线适合看“头部玩家”但疫情数据还有另一个重要维度国家之间的横向对比。我常用热力图看多个国家在时间轴上的颜色深浅变化用 seaborn 的 heatmap 几条命令就能出图import seaborn as sns # 选出确诊总量 Top 10 国家做行列是日期值是 7 日均值 top_countries ( df.groupby(Country/Region)[confirmed].max() .nlargest(10).index.tolist() ) pivot df[df[Country/Region].isin(top_countries)].pivot_table( indexdate, columnsCountry/Region, valuesavg7, aggfuncmean, ) # 转置让国家在行、日期在列宽度更符合阅读习惯 fig, ax plt.subplots(figsize(14, 8), dpi150) sns.heatmap( pivot.T, cmapReds, cbar_kws{label: 新增确诊 7 日均值}, ) plt.title(Top 10 国家新增确诊热力对比) plt.tight_layout() plt.savefig(figures/top10_heatmap.png, dpi150)pivot_table的aggfuncmean在数据无重复时不会触发聚合但如果清洗阶段有重复日期这里平均值会掩盖错误所以热力图之前必须先确认长表每天每国只有一行。cmapReds语义直观——越红越严重。课设里不建议用默认的viridis蓝紫色块在非技术评委眼里没有“严重程度”直觉。4. 避坑排查疫情数据分析课设最常见的 5 个翻车现场与修复4.1 读 CSV 中文乱码或直接报错现象pd.read_csv抛出UnicodeDecodeError或者读出 DataFrame 后国家名全变成乱码。原因数据源文件不是 UTF-8 编码。很多聚合数据经过 Excel 二次编辑保存成 GBK/GB18030pandas 默认按 UTF-8 读就翻车。解决先看文件编码用二进制方式打开读前两行或者在read_csv里指定encodinggbk再试。拿不准就写成encodinggb18030并用enginepython兜底gb18030 能覆盖 GBK 全部字符容错率高。项目里统一把原始 CSV 转成 UTF-8 存一份后续脚本就不会被编码问题反复折腾。4.2 日期列被读成字符串排序和画图全乱现象long_df[date]的类型是object按日期排序后 10 月排到了 4 月前面折线图横轴乱成一团。原因CSV 里日期列本来就是文本pandas 不会自动猜成时间类型。更隐蔽的是to_datetime不指定format时04/02/20可能被解析成 4 月 2 日也可能被解析成 2 月 4 日取决于系统区域设置。解决读取后就强制转换并固定格式代码写成pd.to_datetime(df[date], format%m/%d/%y)。转换后立刻检查df[date].isna().sum()如果出现大量NaT说明源文件里混入了别的日期格式这时用errorscoerce先跑一遍定位是哪几行不合法。4.3 groupby 之后索引没重置后续合并报 duplicate现象后面执行df.merge(avg7, ondate)时抛出cannot reindex from a duplicate axis排查半天不知道重复从哪来的。原因groupby(Country/Region)[new_confirmed].rolling(7).mean()返回的 Series 索引是两层——外层国家、内层原索引。reset_index()不带参数时把两层索引全变成列和原 DataFrame 合并时就会撞车。解决统一用as_indexFalse处理 groupby或者在 rolling 之后必须写reset_index(level0, dropTrue)。我把这行注释写进代码里提醒自己也提醒看代码的人。4.4 图例中文显示成方块现象图和标题都正常唯独图例和坐标轴标签全是空心方块。原因matplotlib 默认字体 DejaVu Sans 不含中文字形遇到中文直接画方框。解决在import matplotlib.pyplot as plt之后、第一次绘图之前设置plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei]同时设置plt.rcParams[axes.unicode_minus] False。后者专门解决负号显示成方块的问题。如果换成 Linux 环境字体名改成WenQuanYi Micro Hei或Noto Sans CJK SC。这个方法有玄学成分字体名写错不会报错只是静默失效所以设置完立刻画一张带中文的小图验证。4.5 移动平均“偷看未来”曲线比实际更早下降现象图上的新增曲线在某个节点前就开始下降但对照原始数据那几天数值明明还在高位。原因rolling默认窗口是“右对齐”第 t 天用的是 t、t-1、…、t-6 的数据这没问题。但如果你嫌曲线不够平滑改成centerTrue第 t 天的值就混入了 t1、t2、t3 的数据——放在报告里就是“曲线比现实早三天见顶”答辩时会被一句话问穿。解决一律使用默认的centerFalse。检查方法也简单把avg7和new_confirmed最后 3 行打出来看移动平均不应该高于最后一天的新增确诊值太多否则就是窗口向后看了。5. 把代码和报告打包成可交付的 zip目录规范与报告结构5.1 交付 zip 的目录规范代码、数据、报告各归其位课设提交的是压缩包评审可能直接解压运行。目录规范决定了别人能不能看出来门道。我通常按以下结构组织covid19_analysis/ ├── data/ │ ├── raw/ # 原始下载数据只读不改 │ └── processed/ # 清洗后的 csv ├── scripts/ │ ├── 01_clean.py # 清洗与特征工程 │ ├── 02_plot.py # 静态图与交互图 │ └── 03_report.py # 统计口径输出 ├── figures/ # 图片与 html ├── report/ │ └── 疫情数据分析报告.md ├── requirements.txt └── README.md这个结构有两条硬规矩原始数据放data/raw之后不要直接改清洗后的结果放processed脚本里所有路径都写相对路径从项目根目录运行。很多课设解压后跑不通唯一原因就是路径写成了C:/Users/xxx/Desktop/...。另外不要在压缩包里带上__pycache__、.ipynb_checkpoints和虚拟环境几百 MB 的 zip 既臃肿又显得不专业。用命令行打包时可以顺手排除zip -r covid19_analysis.zip covid19_analysis \ -x */__pycache__/* *.pyc */.DS_Store-x后面跟排除模式*/__pycache__/*会匹配所有层级的缓存目录。Windows 用户不用装 zip 命令直接右键压缩但压缩前手动删掉缓存目录也行。README 里要写清楚运行顺序先pip install -r requirements.txt再按01_clean.py、02_plot.py、03_report.py依次执行。5.2 报告结构从问题定义到结论的七段式报告决定课设分数的上限代码只决定能不能交。我用七段式结构写报告每一段聚焦一个问题段落写什么篇幅1. 问题定义用 2-3 句话说明分析目标追踪疫情传播趋势、识别高发地区、对比响应速度半页2. 数据来源说明数据集名称、字段含义、时间范围和清洗前的形态半页3. 清洗过程写清楚宽表转长表、按国家聚合、缺失值处理对应01_clean.py1 页4. 分析维度列出 3 个分析角度时间趋势、国家对比、速度指标峰值/倍增时间1 页5. 图表与结论每张图配 2-3 句解读图说明什么、异常点在哪、可能的原因2 页6. 局限与误差承认数据更新延迟、部分国家漏报、移动平均掩盖突变半页7. 运行方法写清依赖、命令、输出文件对照表半页常见错误是把报告写成代码注释翻译版——“我读了 CSV然后 groupby然后画图”。评审想看的是“为什么选这个图、这个数字说明什么”。比如你画了 Top 10 国家热力图结论应当写“头部国家在 2021 年下半年进入平台期但颜色梯度显示区域差异显著说明全球不同步入同一阶段”而不是“热力图颜色有深有浅”。5.3 把关清单提交前跑一遍的最小验证打包提交之前我习惯做一次“干净环境演练”避免出现“在我电脑上能跑”的尴尬。具体做法是新建一个空目录把将要压缩的内容复制进去用python -m venv test_env建一个全新的虚拟环境然后按 README 的步骤从零跑一遍。检查清单如下requirements.txt存在且版本号没有用pandas这种裸库名。写成pandas1.3.0而不是pandas避免评估环境装到最新不兼容版本。requirements.txt 内容放在这一节作为参考pandas1.3.0 matplotlib3.4.0 seaborn0.11.0 pyecharts2.0.0运行时工作目录在项目根目录所有脚本用pathlib或相对路径不出现os.chdir和绝对路径。输出文件全部落在figures/和data/processed/不往项目根目录丢临时文件。打包后的 zip 重新解压到另一个目录重复跑一遍。这一步很花时间但能拦住 90% 的交付事故。6. 从“能跑”到“能答辩”用 Timeline 把疫情数据做成动态大屏6.1 用 pyecharts 的 Timeline 做动态大屏静态图和单页交互图做完如果想在答辩现场多讲两分钟可以加一个动态时间线。pyecharts 的Timeline组件能在同一个图表容器里按时间轮播数据。比如“每月累计确诊 Top 10 国家排名”这种需求用 Bar Timeline 就能做一个类似新闻里看到的大屏效果from pyecharts.charts import Timeline, Bar from pyecharts import options as opts # 按月份准备 top10 排名数据 tl Timeline(init_optsopts.InitOpts(width1400px, height700px)) for month in sorted(df[date].dt.to_period(M).unique()): sub df[df[date].dt.to_period(M) month] top10 ( sub.groupby(Country/Region)[confirmed] .max().nlargest(10).sort_values() ) bar ( Bar() .add_xaxis(top10.index.tolist()) .add_yaxis(累计确诊, top10.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(titlef{month} 累计确诊 Top 10), ) ) tl.add(bar, str(month)) tl.add_schema(play_interval800, is_auto_playTrue, is_loop_playFalse) tl.render(figures/timeline_top10.html)play_interval800表示每 800 毫秒切换到下一个月太快会看不清排名变化太慢让人想快进。is_auto_playTrue打开自动播放is_loop_playFalse播完就停方便结尾停在一个具体月份上做解释。这里我用每个月最后一天的累计值做max()而不是每天累加避免月中数据波动造成排名来回跳。6.2 导出 HTML 报告把图表嵌进交付物line.render()和tl.render()输出的是自包含 HTML 文件双击就能打开不依赖本地服务器。如果想把这个动态图嵌进 Markdown 报告用标准 HTMLiframe标签就能引用iframe srcfigures/timeline_top10.html width100% height600/iframeGitHub 直接预览 Markdown 不会渲染 iframe所以本地写报告、导出 PDF 时要么单独打开 HTML 截图关键帧要么把链接写进去让评审自己开。我的习惯是 figures 目录里同时保存 PNG 静态图和 HTML 交互图PDF 报告引用 PNG答辩现场用浏览器开 HTML两不耽误。做这类课设我最大的教训是“先跑通再清理”的顺序不能反。第一版代码可能很乱但先保证输出正确再动手整理目录结构。整理完成后立刻在干净环境里重跑一遍验证这个习惯帮我省掉了无数次交稿前发现图和数据对不上的返工。这个流程不只适用于疫情数据换成股票、气象、流量数据清洗、聚合、可视化的骨架完全一样希望帮到你。本文还有配套的精品资源点击获取