Python疫情数据可视化:构建真实世界数据流水线
简介本资源是一份高质量的Python课程设计成果面向计算机、电子信息工程、数学等专业的本科生聚焦COVID-19疫情数据的采集、清洗、统计分析与多维可视化实践切实提升数据处理与工程实现能力。压缩包共66个文件含17个核心Python脚本如spider-yqkx.py疫情爬虫、mapworld.py全球疫情地图绘制、logistic.py时序预测、5个CSV原始与中间数据集、16张PNG/JPG图表输出、5个HTML交互式报告页以及Dockerfile、requirements.txt、README.md等完整工程配置总大小4.12MB。已有62人学习下载。读者可直接复用全套代码流程从微博舆情爬取weibo_战疫情爬虫_spider.py到中文分词与TF-IDF文本分析jiebafenci.py/tfidf.py再到中国/全球疫情地图渲染mapchina.py/mapworld.py及治愈率、增长率等关键指标计算配套Word设计报告与Jupyter Notebook实验记录结构清晰、注释充分兼具教学参考性与项目落地性。1. 为什么用 Python 做 COVID-19 疫情数据可视化不是在练手而是在建「实时感知力」2020 年初当第一份 WHO 全球疫情通报 PDF 还没被人工转成 Excel 时有人已用 37 行 Python 脚本自动抓取 Johns Hopkins 大学 GitHub 仓库的 CSV每 15 分钟更新一次热力图——这不是炫技而是课程设计里最硬核的真实需求把散落、异构、带噪声的疫情原始数据时间戳错位、国家名缩写不统一、累计值与日增值混存变成可交互、可回溯、可解释的时空分析视图。这个 ZIP 包里的“课程设计”本质是高校数据科学类课程对「真实世界数据流闭环能力」的一次压力测试它不考你能不能画出漂亮的折线图而考你能不能在pandas.read_csv()报错ParserError: Expected 14 fields in line 123, saw 15时三分钟内定位到某国新增字段含逗号分隔的备注文本并用quotingcsv.QUOTE_NONEerror_bad_linesFalse旧版或on_bad_linesskippandas 1.4救活整条 pipeline。适合两类人一是刚学完 NumPy 基础、正卡在「数据清洗到底要洗几次」的本科生二是需要快速交付教学案例、但不想被学生问倒“为什么中国数据和 WHO 不一致”的授课教师。它不承诺“零基础秒懂”但保证每一步命令都有明确输入输出、每个报错都有对应解法——因为疫情数据从不等你重装 Python。2. 从原始 CSV 到时空热力图四步数据流水线搭建课程设计的核心不是画图而是构建一条能扛住数据源变更的稳定流水线。我通常把整个流程拆成四个原子环节源获取 → 结构清洗 → 时空对齐 → 可视化封装。每个环节都必须有输入校验和失败兜底否则一个国家改名如“Czechia”→“Czech Republic”就能让全图崩掉。2.1 下载并校验原始数据源别信 URL只信 SHA256JHU CSSE 的疫情数据托管在 GitHub但直接requests.get()会触发反爬返回 403。课程设计中更可靠的做法是用git clone拉取整个仓库快照再提取csse_covid_19_data/csse_covid_19_time_series/下的 CSV 文件。关键在于每次运行前必须校验文件哈希值避免因网络中断导致下载不完整。# 克隆仓库仅需首次 git clone https://github.com/CSSEGISandData/COVID-19.git # 进入数据目录获取最新 time_series_covid19_confirmed_global.csv cd COVID-19/csse_covid_19_data/csse_covid_19_time_series/ sha256sum time_series_covid19_confirmed_global.csv提示课程设计报告里必须记录该文件的 SHA256 值例如a1f8b3c...并在代码中做断言校验。若哈希不匹配立即终止后续流程——这是防止“用错版本数据却得出错误结论”的第一道防线。2.2 清洗结构混乱的宽表把“国家-省份-日期”三维数据压成标准长表JHU 的原始 CSV 是典型的宽表wide format列名是Province_State,Country_Region,Lat,Long_,1/22/20,1/23/20, ...。这种结构无法直接用于groupby或时间序列分析。必须用melt()压成“长表”long format但要注意三个坑日期列名格式不统一部分列名含空格如1/22/20 需先strip()地理字段缺失值处理Province_State为空时应填充为Country_Region值避免聚合时丢失数值列类型强制转换原始 CSV 中数字列被读为字符串因含空单元格需pd.to_numeric(..., errorscoerce)转换并设NaN。import pandas as pd import numpy as np # 读取原始 CSV跳过前 1 行JHU 数据常含说明行 df_raw pd.read_csv(time_series_covid19_confirmed_global.csv, skiprows1) # 清洗列名去除空格、标准化日期格式转为 datetime cols_clean [col.strip() for col in df_raw.columns] df_raw.columns cols_clean # 找出所有日期列排除前 4 列Province_State, Country_Region, Lat, Long_ date_cols df_raw.columns[4:].tolist() # melt将日期列压成一行一日期 df_long df_raw.melt( id_vars[Province_State, Country_Region, Lat, Long_], value_varsdate_cols, var_nameDate, value_nameConfirmed ) # 修复 Province_State 空值用 Country_Region 填充 df_long[Province_State] df_long[Province_State].fillna(df_long[Country_Region]) # 强制转换 Confirmed 为数值无效值设为 NaN df_long[Confirmed] pd.to_numeric(df_long[Confirmed], errorscoerce) # 将 Date 字符串转为 datetime并提取年月日 df_long[Date] pd.to_datetime(df_long[Date], errorscoerce) df_long df_long.dropna(subset[Date, Confirmed]) # 删除日期或确诊数为空的行逻辑说明这段代码不是为了“跑通”而是建立数据契约——确保后续所有分析都基于Date是datetime64、Confirmed是float64、Province_State无空值的干净长表。参数errorscoerce是关键它让to_numeric遇到非数字时返回NaN而非报错比errorsraise更健壮。2.3 时空对齐解决“同一国家多个记录”与“日期不连续”问题原始数据中一个国家如美国可能有 50 条记录各州而中国只有 1 条无省级细分。若直接按Country_Region汇总会丢失空间粒度若保留省级则全球地图渲染性能爆炸。课程设计中采用双粒度策略国家级视图对Country_Region汇总用于全球热力图省级/州级视图仅对特定国家如 US、CN启用用于局部放大。同时原始数据存在日期跳跃如某国某日数据缺失需用resample补全# 按国家汇总确诊数忽略省份 df_country df_long.groupby([Country_Region, Date])[Confirmed].sum().reset_index() # 补全日期对每个国家生成连续日期序列 all_dates pd.date_range(startdf_country[Date].min(), enddf_country[Date].max(), freqD) countries df_country[Country_Region].unique() # 构建完整网格国家 × 所有日期 grid pd.MultiIndex.from_product([countries, all_dates], names[Country_Region, Date]) df_full df_country.set_index([Country_Region, Date]).reindex(grid).reset_index() # 前向填充确诊数疫情数据天然适合 FFILL今日确诊 ≥ 昨日确诊 df_full[Confirmed] df_full.groupby(Country_Region)[Confirmed].ffill() # 计算日增用 diff()再用 clip(0) 防止负值数据修正导致 df_full[New_Cases] df_full.groupby(Country_Region)[Confirmed].diff().clip(0)参数说明ffill()是疫情数据清洗的“后悔药”——它假设确诊数不会减少除非数据修正用前一日值填充当日空缺。clip(0)则是安全阀diff()可能产生负值如某国修正历史数据将昨日确诊从 100 改为 90clip(0)强制日增 ≥ 0避免图表出现诡异负峰。3. 用 Plotly GeoPandas 绘制可交互全球热力图不只是颜色深浅课程设计的可视化不能止步于matplotlib.pyplot.plot()必须体现“可交互、可下钻、可导出”。Plotly 是首选它原生支持 hover 显示详情、zoom 缩放、点击国家筛选。但直接用px.choropleth()会翻车——因为 JHU 的国家名如US和 GeoPandas 自带世界地图的admin字段如United States of America不匹配。3.1 国家名映射表一份必须维护的“外交辞令字典”我维护一个country_mapping.csv包含三列jhu_name,geopandas_name,iso_alpha3。例如jhu_namegeopandas_nameiso_alpha3USUnited States of AmericaUSAUKUnited KingdomGBRKorea, SouthSouth KoreaKORTaiwan*TaiwanTWN注意Taiwan*是 JHU 原始命名需在代码中显式替换为Taiwan否则 GeoPandas 无法匹配。课程设计报告中必须声明此映射依据如参考 ISO 3166-1 标准而非简单写“手动修正”。3.2 构建带地理坐标的热力图用choropleth_mapbox替代choroplethpx.choropleth()依赖内置 GeoJSON对中文标签支持差且无法自定义投影。更稳的方案是用plotly.express.choropleth_mapbox()配合 Mapbox 免费 token 和自定义 GeoJSONimport plotly.express as px import geopandas as gpd # 加载世界地图 GeoJSON推荐 Natural Earth 数据集比自带更准 world gpd.read_file(ne_110m_admin_0_countries.geojson) # 合并映射表修正国家名 mapping_df pd.read_csv(country_mapping.csv) df_geo df_full.merge(mapping_df, left_onCountry_Region, right_onjhu_name, howleft) # 按日期聚合取最新一天的全球确诊数 latest_date df_geo[Date].max() df_latest df_geo[df_geo[Date] latest_date].copy() # 关键用 geopandas 的 geometry 生成 GeoJSON 特征 world_geo world[[ADMIN, geometry]].rename(columns{ADMIN: geopandas_name}) df_merged df_latest.merge(world_geo, ongeopandas_name, howinner) # 创建 Mapbox 热力图 fig px.choropleth_mapbox( df_merged, geojsondf_merged.__geo_interface__, # 直接传 geometry locationsdf_merged.index, colorConfirmed, color_continuous_scaleYlOrRd, range_color(0, df_merged[Confirmed].quantile(0.98)), mapbox_stylecarto-positron, zoom1, center{lat: 20, lon: 0}, opacity0.8, labels{Confirmed: 累计确诊}, hover_data[Country_Region, Confirmed, New_Cases] ) fig.update_layout(margin{r:0,t:0,l:0,b:0}) fig.show()逻辑说明df_merged.__geo_interface__是 GeoPandas 的魔法属性它把geometry列自动转为 Plotly 可识别的 GeoJSON 特征集合比手动json.dumps()更可靠。range_color设为 0.98 分位数是为了压制极端值如美国、印度对色阶的干扰让中小国家颜色差异更明显——这是疫情数据可视化的玄学不是所有数字都值得同等权重。4. 避坑课程设计中最常踩的 5 个“数据陷阱”学生交上来的代码80% 的报错不出这五类。它们不是语法错误而是对疫情数据特性的误判。4.1 现象pandas.read_csv()读出Confirmed列全是NaN原因原始 CSV 中Confirmed数值列含空单元格pandas 默认将整列推断为object类型to_numeric()无法转换字符串 。解决在read_csv()中显式指定dtype或用converters参数预处理df_raw pd.read_csv(file.csv, converters{col: lambda x: float(x) if x.strip() else np.nan for col in date_cols})4.2 现象全球热力图显示大片空白仅少数国家着色原因GeoPandas 地图中的ADMIN字段与 JHU 国家名未完全匹配如RussiavsRussian Federationmerge后geometry为空。解决打印df_merged.isnull().sum()定位geometry列空值然后检查country_mapping.csv是否遗漏该国。切忌用fillna()补 geometry——地理坐标不能瞎填。4.3 现象日增曲线出现尖锐负峰如某日 -5000原因JHU 数据会回溯修正历史值如某国将昨日确诊从 10000 改为 15000diff()计算出负值。解决必须用clip(0)截断或改用shift(-1)计算“修正后日增”df_full[New_Cases_Adjusted] df_full.groupby(Country_Region)[Confirmed].apply( lambda x: x - x.shift(1).fillna(0) ).clip(0)4.4 现象px.choropleth_mapbox()报错ValueError: Invalid value of type builtins.dict received for the geojson property原因传入的geojson是dict而非str或pathlib.Path常见于gpd.GeoDataFrame.__geo_interface__直接传入Plotly 期望的是 JSON 字符串。解决用json.dumps()序列化import json geojson_str json.dumps(df_merged.__geo_interface__) fig px.choropleth_mapbox(..., geojsongeojson_str, ...)4.5 现象导出 HTML 后交互失效hover 不显示、zoom 不响应原因Plotly 默认导出为离线模式但若本地没安装plotly-orca或kaleidofig.write_html()会生成不带 JS 的静态 HTML。解决强制使用在线 CDNfig.write_html(map.html, include_plotlyjscdn, full_htmlTrue)或安装kaleidopip install kaleido再用fig.write_image(map.png)导出高清图。5. 进阶技巧用 Dash 构建可筛选的疫情看板让课程设计变作品集课程设计交作业只是起点真正加分的是把它变成可演示的 Web 应用。Dash 是最轻量的选择——无需前端知识纯 Python 就能搭出带下拉筛选、日期滑块、多图联动的看板。核心就三步布局定义 → 回调绑定 → 状态管理。5.1 布局用dbc组件库快速搭出专业 UI不要手写 HTML用dash-bootstrap-componentsdbc提供栅格系统和预设样式import dash from dash import dcc, html, callback, Input, Output, State import dash_bootstrap_components as dbc app dash.Dash(__name__, external_stylesheets[dbc.themes.BOOTSTRAP]) app.layout dbc.Container([ dbc.Row([ dbc.Col(html.H1(COVID-19 全球疫情看板, classNametext-center my-4), width12) ]), dbc.Row([ dbc.Col([ html.Label(选择国家), dcc.Dropdown( idcountry-dropdown, options[{label: c, value: c} for c in df_latest[Country_Region].unique()], valueChina, clearableFalse ) ], width4), dbc.Col([ html.Label(日期范围), dcc.DatePickerRange( iddate-range, min_date_alloweddf_latest[Date].min(), max_date_alloweddf_latest[Date].max(), start_datedf_latest[Date].min(), end_datedf_latest[Date].max() ) ], width4), dbc.Col([ html.Label(指标), dbc.RadioItems( idmetric-radio, options[ {label: 累计确诊, value: Confirmed}, {label: 日增确诊, value: New_Cases} ], valueConfirmed, inlineTrue ) ], width4) ], classNamemb-4), dbc.Row([ dbc.Col(dcc.Graph(idtrend-chart), width8), dbc.Col(dcc.Graph(idmap-chart), width4) ]) ])5.2 回调用callback实现“选国家 → 更新折线图 热力图”Dash 的灵魂是回调函数。这里定义两个输出折线图趋势和热力图当前日全球分布callback( [Output(trend-chart, figure), Output(map-chart, figure)], [Input(country-dropdown, value), Input(date-range, start_date), Input(date-range, end_date), Input(metric-radio, value)] ) def update_charts(selected_country, start_date, end_date, metric): # 过滤数据 mask (df_full[Country_Region] selected_country) \ (df_full[Date] start_date) \ (df_full[Date] end_date) df_filtered df_full[mask].copy() # 折线图 trend_fig px.line(df_filtered, xDate, ymetric, titlef{selected_country} {metric} 趋势) # 热力图取 end_date 当日全球数据 df_map df_full[df_full[Date] end_date].copy() # 此处插入 3.2 节的 choropleth_mapbox 代码略 return trend_fig, map_fig提示callback的输入顺序必须与Input()列表严格一致否则参数错位。这是 Dash 最容易翻车的细节——建议用print(fInputs: {selected_country}, {start_date})在回调开头调试。5.3 部署用gunicornnginx在 Linux 服务器跑起来课程设计若只在本地app.run_server()价值减半。真正在服务器部署只需三步安装gunicornpip install gunicorn启动服务gunicorn --bind 0.0.0.0:8050 --workers 2 app:app.serverNginx 反向代理/etc/nginx/sites-available/covid-dashboardlocation / { proxy_pass http://127.0.0.1:8050; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }然后sudo systemctl restart nginx。学生交作业时附上服务器 IP 和截图远比 ZIP 包更有说服力。我带过 7 届课程设计最深的教训是别花 3 天调matplotlib颜色花 30 分钟配好 Dash 回调你的项目就从“作业”变成了“作品”。疫情数据会过时但这条流水线思维——源校验、结构清洗、时空对齐、交互封装——会跟着你进任何一家公司的数据分析岗。希望帮到你。本文还有配套的精品资源点击获取