☰
Python疫情数据分析与可视化:从数据清洗到报告打包的完整流程
2026/10/10 11:28:34 网站建设 项目流程

简介:一份面向新冠肺炎疫情数据分析与可视化的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省/州,部分国家为空str
Country/Region国家/地区str
Lat / Long坐标float
1/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_index=False)[date_cols].sum() # 4. 宽表转长表:日期列熔化成两列 long_df = by_country.melt( id_vars="Country/Region", value_vars=date_cols, var_name="date", value_name="confirmed", )

这段脚本的核心逻辑有三处。第一,groupby("Country/Region", as_index=False)里as_index=False很关键,它让分组列不变成索引,否则后面melt和merge容易碰到索引对不上的问题。第二,melt的value_vars直接传整个日期列列表,省去逐列拼接的写法,顺便把var_name="date"指定清楚。第三,原始 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_periods=1 让前 6 天也有值 long_df["avg7"] = ( long_df.groupby("Country/Region")["new_confirmed"] .rolling(7, min_periods=1) .mean() .reset_index(level=0, drop=True) ) # 10. 保存处理结果,后续分析和绘图都不碰原始数据 long_df.to_csv("data/processed/covid19_daily.csv", index=False)

diff()第一个值一定是 NaN,因为没有任何前一天的数据,fillna(0)是安全做法,但你要意识到这个 0 是“不存在”而不是“当天确实为 0”,写报告时不必解释,心里清楚就行。rolling(7, min_periods=1)里的 7 不是拍脑袋的,它对应一周的报告周期:很多国家周末不更新数据,周一数字偏低,7 天窗口能把这些周期性波动抹掉。min_periods=1保证序列头 6 天不全是 NaN,否则画图时前一周直接断线。最后一步reset_index(level=0, drop=True)是 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), dpi=150) for country in ["China", "US", "India"]: sub = df[df["Country/Region"] == country] ax.plot( sub["date"], sub["avg7"], label=country, linewidth=2, ) 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(axis="y", alpha=0.3) plt.tight_layout() plt.savefig("figures/top3_trend.png", dpi=150) plt.show()

figsize=(12, 6)和dpi=150是配套关系。dpi=150下输出图片像素是 12×150=1800 宽,插入 Word 或 PDF 后依然清晰;如果figsize调小到 8,还想保持清晰,就得把dpi提到 200 以上。mdates.MonthLocator()只让横轴每月显示一个刻度,否则 2020 到 2023 的日刻度会把标签挤成一团。grid(axis="y", alpha=0.3)只画横向网格,比默认的框式网格干净。注意plt.rcParams两句必须放在plt.subplots之前,位置放错就有概率改不生效。

3.2 用 pyecharts 做交互图:图表类型与配置项

课设答辩现场,交互图比静态图更能撑场面。pyecharts 是 echarts 数据可视化能力的 python 封装,输出的是独立 HTML 文件,浏览器打开就能缩放、悬浮、看数据点。做时间序列交互图,我的标配是 Line + DataZoom + Tooltip:

from 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_opts=opts.InitOpts(width="1200px", height="600px")) .add_xaxis(x_data) .add_yaxis( "7 日均值", y_data, is_smooth=True, label_opts=opts.LabelOpts(is_show=False), ) .set_global_opts( title_opts=opts.TitleOpts(title="中国新增确诊趋势"), tooltip_opts=opts.TooltipOpts(trigger="axis"), datazoom_opts=[ opts.DataZoomOpts(range_start=0, range_end=100), opts.DataZoomOpts(type_="inside", range_start=0, range_end=100), ], xaxis_opts=opts.AxisOpts(name="日期"), yaxis_opts=opts.AxisOpts(name="新增确诊"), ) ) line.render("figures/chn_trend.html")

init_opts里的width和height控制整个图表容器大小,不是像素分辨率,别和 matplotlib 的figsize混淆。is_smooth=True让折线变成曲线,适合展示平滑后的移动平均;如果数据本身波动剧烈,设成平滑会显得“预测味”太重。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( index="date", columns="Country/Region", values="avg7", aggfunc="mean", ) # 转置让国家在行、日期在列,宽度更符合阅读习惯 fig, ax = plt.subplots(figsize=(14, 8), dpi=150) sns.heatmap( pivot.T, cmap="Reds", cbar_kws={"label": "新增确诊 7 日均值"}, ) plt.title("Top 10 国家新增确诊热力对比") plt.tight_layout() plt.savefig("figures/top10_heatmap.png", dpi=150)

pivot_table的aggfunc="mean"在数据无重复时不会触发聚合;但如果清洗阶段有重复日期,这里平均值会掩盖错误,所以热力图之前必须先确认长表每天每国只有一行。cmap="Reds"语义直观——越红越严重。课设里不建议用默认的viridis,蓝紫色块在非技术评委眼里没有“严重程度”直觉。

4. 避坑排查:疫情数据分析课设最常见的 5 个翻车现场与修复

4.1 读 CSV 中文乱码或直接报错

  • 现象:pd.read_csv抛出UnicodeDecodeError,或者读出 DataFrame 后国家名全变成乱码。
  • 原因:数据源文件不是 UTF-8 编码。很多聚合数据经过 Excel 二次编辑,保存成 GBK/GB18030,pandas 默认按 UTF-8 读就翻车。
  • 解决:先看文件编码,用二进制方式打开读前两行,或者在read_csv里指定encoding="gbk"再试。拿不准就写成encoding="gb18030"并用engine="python"兜底,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,说明源文件里混入了别的日期格式,这时用errors="coerce"先跑一遍,定位是哪几行不合法。

4.3 groupby 之后索引没重置,后续合并报 duplicate

  • 现象:后面执行df.merge(avg7, on="date")时抛出cannot reindex from a duplicate axis,排查半天不知道重复从哪来的。
  • 原因:groupby("Country/Region")["new_confirmed"].rolling(7).mean()返回的 Series 索引是两层——外层国家、内层原索引。reset_index()不带参数时把两层索引全变成列,和原 DataFrame 合并时就会撞车。
  • 解决:统一用as_index=False处理 groupby,或者在 rolling 之后必须写reset_index(level=0, drop=True)。我把这行注释写进代码里,提醒自己也提醒看代码的人。

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 的数据,这没问题。但如果你嫌曲线不够平滑,改成center=True,第 t 天的值就混入了 t+1、t+2、t+3 的数据——放在报告里就是“曲线比现实早三天见顶”,答辩时会被一句话问穿。
  • 解决:一律使用默认的center=False。检查方法也简单:把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这种裸库名。写成pandas>=1.3.0而不是pandas,避免评估环境装到最新不兼容版本。requirements.txt 内容放在这一节作为参考:
pandas>=1.3.0 matplotlib>=3.4.0 seaborn>=0.11.0 pyecharts>=2.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_opts=opts.InitOpts(width="1400px", height="700px")) 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_opts=opts.TitleOpts(title=f"{month} 累计确诊 Top 10"), ) ) tl.add(bar, str(month)) tl.add_schema(play_interval=800, is_auto_play=True, is_loop_play=False) tl.render("figures/timeline_top10.html")

play_interval=800表示每 800 毫秒切换到下一个月,太快会看不清排名变化,太慢让人想快进。is_auto_play=True打开自动播放,is_loop_play=False播完就停,方便结尾停在一个具体月份上做解释。这里我用每个月最后一天的累计值做max(),而不是每天累加,避免月中数据波动造成排名来回跳。

6.2 导出 HTML 报告,把图表嵌进交付物

line.render()和tl.render()输出的是自包含 HTML 文件,双击就能打开,不依赖本地服务器。如果想把这个动态图嵌进 Markdown 报告,用标准 HTMLiframe标签就能引用:

<iframe src="figures/timeline_top10.html" width="100%" height="600"></iframe>

GitHub 直接预览 Markdown 不会渲染 iframe,所以本地写报告、导出 PDF 时要么单独打开 HTML 截图关键帧,要么把链接写进去让评审自己开。我的习惯是 figures 目录里同时保存 PNG 静态图和 HTML 交互图:PDF 报告引用 PNG,答辩现场用浏览器开 HTML,两不耽误。

做这类课设,我最大的教训是“先跑通再清理”的顺序不能反。第一版代码可能很乱,但先保证输出正确,再动手整理目录结构。整理完成后立刻在干净环境里重跑一遍验证,这个习惯帮我省掉了无数次交稿前发现图和数据对不上的返工。这个流程不只适用于疫情数据,换成股票、气象、流量数据,清洗、聚合、可视化的骨架完全一样,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询