1. 项目缘起与整体设计思路
1.1 为什么选择“数据采集+分析+可视化”这条链路
这个项目的核心目标很明确:把公开的疫情统计数据,从采集、清洗、分析到最终在网页上呈现,跑通一条完整的数据流水线。它不是一个单纯的爬虫练习,也不是一个孤立的图表Demo,而是一个覆盖“数据获取—数据处理—数据表达”全流程的小型数据工程项目。
我选择这条链路,是因为它恰好对应了数据从业者日常工作中最高频的三个环节。采集解决“数据从哪来”的问题,分析解决“数据说明什么”的问题,网页展示解决“数据给谁看”的问题。三者缺一不可,单独拆开任何一个,项目的价值都会打折扣。比如只做爬虫,数据躺在CSV里没人看;只做分析,没有真实数据支撑就是空谈;只做前端,没有数据管道就是花架子。
从技术选型上看,这个项目对新手非常友好。采集用Python的requests配合解析库就能完成,分析用pandas加matplotlib足够覆盖大部分需求,网页展示用Flask或Streamlit可以快速搭起来。整条链路不依赖复杂的分布式框架,一台普通笔记本就能跑通,学习成本可控,但涉及的知识点又足够全面。
注意:项目涉及的数据必须来自公开、合规的统计渠道,采集频率要克制,避免对数据源造成压力。这是做任何数据采集项目的前提。
1.2 整体架构拆解与模块划分
我把整个项目拆成四个相对独立的模块,每个模块有明确的输入和输出,模块之间通过文件或数据库解耦。这样做的好处是,任何一个环节出问题,都不会导致整个项目瘫痪,排查起来也方便。
第一个模块是数据采集层。它的任务是定时从公开数据源拉取原始数据,保存为结构化文件。输入是数据源的接口地址或页面URL,输出是原始CSV或JSON文件。这一层要处理网络请求、反爬应对、数据去重和增量更新。
第二个模块是数据清洗与存储层。原始数据往往存在缺失值、格式不统一、字段冗余等问题,需要在这一层做标准化处理。输入是原始文件,输出是干净的、可直接分析的数据表。我通常会把清洗后的数据存入SQLite,方便后续查询和复用。
第三个模块是数据分析层。基于清洗后的数据,计算各类统计指标,比如累计确诊、新增趋势、治愈率、死亡率、区域分布等。输入是干净数据,输出是分析结果表和图表。这一层是项目的“大脑”,决定了最终呈现什么内容。
第四个模块是网页展示层。把分析结果以图表、表格、地图等形式呈现在网页上,让非技术用户也能直观理解数据。输入是分析结果,输出是可访问的网页。这一层要考虑响应式布局、加载速度和交互体验。
四个模块串起来,就是一条完整的数据流水线。我建议在项目初期就把每个模块的接口约定好,比如文件命名规则、字段名称、数据格式,这样后续开发和调试会顺畅很多。
1.3 技术栈选型与理由
技术栈的选择没有绝对的对错,关键看是否匹配项目规模和自身熟悉度。我下面列的是经过实际验证、对新手最友好的组合。
| 环节 | 技术选型 | 选择理由 |
|---|---|---|
| 数据采集 | Python + requests + BeautifulSoup | 语法简单,生态成熟,解析HTML和JSON都很方便 |
| 数据清洗 | pandas | 表格处理能力极强,缺失值、重复值、类型转换一行代码搞定 |
| 数据存储 | SQLite | 零配置,单文件,适合中小规模数据,方便迁移 |
| 数据分析 | pandas + numpy | 统计计算和分组聚合效率高,与清洗环节无缝衔接 |
| 可视化 | matplotlib + pyecharts | 静态图用matplotlib,交互图用pyecharts,覆盖大部分场景 |
| 网页框架 | Flask | 轻量灵活,适合快速搭建数据展示页面 |
| 前端图表 | ECharts | 图表类型丰富,交互流畅,中文文档完善 |
这套组合的特点是“轻”。不需要配置数据库服务器,不需要搭建前端工程化环境,一个Python进程加一个HTML文件就能跑起来。对于个人项目和学习用途来说,过度设计反而是负担。
提示:如果你更倾向于零前端代码,可以把Flask换成Streamlit,用纯Python写页面,开发速度更快,但定制化能力会弱一些。两者按需选择即可。
2. 数据采集环节的核心细节与实操要点
2.1 数据源的选择与合规边界
数据采集的第一步不是写代码,而是确定数据源。这一步决定了后续所有工作的合法性和可持续性。我的原则是:只采集公开发布的、允许合理使用的统计数据,不碰任何需要授权或涉及个人隐私的数据。
具体来说,优先选择官方统计渠道定期发布的汇总数据,这类数据通常以表格或接口形式提供,结构规整,更新频率稳定。采集前要确认该渠道的使用条款,看是否允许自动化获取。如果条款不明确,宁可手动下载历史数据,也不要做高频自动采集。
采集频率也要克制。疫情数据通常每天更新一次,那就每天采集一次,没必要每小时跑一遍。高频采集不仅增加数据源压力,也容易触发访问限制,得不偿失。我一般把采集任务放在凌晨执行,避开访问高峰。
注意:采集到的数据要保留原始来源标识和采集时间戳,方便后续追溯和核对。这是数据工作者的基本素养。
2.2 请求构造与反爬应对思路
写采集代码时,最常遇到的问题不是解析,而是请求被拒绝。要解决这个问题,核心思路是“让请求看起来像正常用户访问”。
首先,请求头要完整。User-Agent、Accept、Accept-Language这些字段都要带上,不要用默认的Python请求头。其次,请求间隔要合理,每次请求之间加0.5到1秒的延时,避免短时间内大量请求。再次,如果数据源有分页或接口参数,要按正常逻辑逐页获取,不要试图一次性拉全量。
下面是一个请求构造的示例,展示了基本的请求头设置和延时控制:
import requests import time import random headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_data(url, params=None): try: resp = requests.get(url, headers=headers, params=params, timeout=10) resp.raise_for_status() time.sleep(random.uniform(0.5, 1.0)) return resp.json() except requests.RequestException as e: print(f"请求失败: {e}") return None这段代码的关键点有三个:一是请求头模拟真实浏览器,二是每次请求后随机延时,三是异常捕获避免程序中断。实测下来,这套组合能应对大部分常规场景。
如果数据源返回的是HTML页面而非JSON接口,就需要用BeautifulSoup解析。解析时要先定位到数据所在的表格或列表容器,再逐行提取字段。这里有个经验:优先用稳定的class或id定位,不要依赖可能变化的层级结构。
2.3 数据去重与增量更新策略
采集不是一次性的工作,而是持续的过程。如果每次采集都全量保存,数据会迅速膨胀,分析时还要额外去重。我的做法是:以“日期+区域”作为唯一键,每次采集后与已有数据比对,只追加新记录。
具体实现上,可以用pandas读取已有CSV,把新采集的数据拼接上去,然后按唯一键去重,保留最新记录。代码大致如下:
import pandas as pd def merge_data(old_file, new_df, key_cols): try: old_df = pd.read_csv(old_file) combined = pd.concat([old_df, new_df], ignore_index=True) combined = combined.drop_duplicates(subset=key_cols, keep="last") combined.to_csv(old_file, index=False) return combined except FileNotFoundError: new_df.to_csv(old_file, index=False) return new_df这段逻辑看起来简单,但有两个细节容易踩坑。第一,keep="last"要确保新数据在后面,否则会保留旧值。第二,如果数据源会修正历史数据,那去重时应该保留最新采集的版本,而不是最新日期的版本。这两种策略要根据实际情况选择。
提示:建议每次采集都单独保存一份原始快照,命名带上日期,比如
raw_20240101.csv。这样即使后续清洗逻辑出错,也能从原始数据重新处理。
3. 数据清洗与分析的关键环节
3.1 缺失值与异常值的处理原则
原始数据几乎没有“干净”的,缺失值和异常值是常态。处理这两类问题,核心原则是:先理解数据含义,再决定处理方式,不要无脑填充或删除。
缺失值分三种情况。第一种是“真的没有数据”,比如某个区域当天未上报,这种情况可以保留空值,分析时跳过。第二种是“数据源格式问题”,比如数字被存成了空字符串,这种情况要转换成NaN再处理。第三种是“字段本身不适用”,比如某些区域没有境外输入病例,这个字段为空是合理的。
异常值同样要区分。有些异常值是真实的极端情况,比如某天突然暴增,可能是统计口径调整导致的,这种要保留。有些异常值是录入错误,比如数值明显超出合理范围,这种要修正或剔除。判断标准是:结合上下文和常识,看这个值是否说得通。
import pandas as pd import numpy as np def clean_data(df): df = df.replace("", np.nan) numeric_cols = ["confirmed", "cured", "dead", "new_confirmed"] for col in numeric_cols: if col in df.columns: df[col] = pd.to_numeric(df[col], errors="coerce") df[numeric_cols] = df[numeric_cols].fillna(0) df = df[df["confirmed"] >= 0] return df这段代码做了三件事:空字符串转NaN、数值列类型转换、负值过滤。每一步都有明确目的,不是机械操作。
3.2 核心统计指标的计算逻辑
数据分析环节要算哪些指标,取决于你想回答什么问题。对于疫情数据,我通常关注以下几类指标:
- 累计指标:累计确诊、累计治愈、累计死亡。这类指标反映总体规模,适合看趋势。
- 新增指标:每日新增确诊、新增治愈、新增死亡。这类指标反映变化速度,适合看拐点。
- 比率指标:治愈率、死亡率、现存确诊占比。这类指标反映应对能力,适合做区域对比。
- 派生指标:七日移动平均、环比增长率、现存重症率。这类指标用于平滑波动,看清趋势。
计算这些指标时,有几个细节要注意。第一,累计指标要确保单调不减,如果出现下降,说明数据有问题。第二,新增指标可能为负,通常是数据修正导致的,分析时要标注说明。第三,比率指标的分母要排除零值,避免除零错误。
def calc_metrics(df): df = df.sort_values(["region", "date"]) df["cured_rate"] = df["cured"] / df["confirmed"].replace(0, np.nan) df["dead_rate"] = df["dead"] / df["confirmed"].replace(0, np.nan) df["existing"] = df["confirmed"] - df["cured"] - df["dead"] df["new_7d_avg"] = df.groupby("region")["new_confirmed"].transform( lambda x: x.rolling(7, min_periods=1).mean() ) return df七日移动平均是我最常用的平滑手段。它的逻辑是取最近七天的平均值,把单日波动抹平,让趋势更清晰。min_periods=1保证数据开头几天也能算出值,不会出现大片空值。
3.3 分组聚合与区域对比分析
疫情数据最有价值的分析维度是区域对比。通过分组聚合,可以快速看出哪些区域防控效果好,哪些区域压力大。
分组聚合的核心是groupby加聚合函数。比如按区域分组,计算每个区域的累计确诊、治愈率、死亡率,然后排序对比。这里有个技巧:聚合后的列名要重命名,否则多级索引处理起来很麻烦。
def region_summary(df): latest = df.sort_values("date").groupby("region").tail(1) summary = latest.groupby("region").agg( total_confirmed=("confirmed", "sum"), total_cured=("cured", "sum"), total_dead=("dead", "sum"), ).reset_index() summary["cured_rate"] = summary["total_cured"] / summary["total_confirmed"] summary["dead_rate"] = summary["total_dead"] / summary["total_confirmed"] summary = summary.sort_values("total_confirmed", ascending=False) return summary这段代码先取每个区域的最新记录,再按区域聚合。注意tail(1)要在groupby之后、agg之前执行,顺序错了结果就不对。这是新手最容易犯的错误之一。
提示:区域对比时要注意人口基数的影响。确诊数高的区域不一定是防控差,可能只是人口多。有条件的话,可以引入人口数据做人均指标,对比会更公平。
4. 网页展示的设计与实现
4.1 页面结构规划与信息层级
网页展示的目标是让用户在最短时间内获取最关键的信息。所以页面结构要遵循“总—分—详”的层级:顶部放核心指标卡片,中间放趋势图和区域对比图,底部放明细表格。
核心指标卡片放什么?我一般放四个数:累计确诊、现存确诊、累计治愈、累计死亡。这四个数一眼就能看出整体态势。卡片要大、要醒目,数字用大字号,标签用小字号。
趋势图放什么?放每日新增和七日移动平均的折线图。新增用柱状图或细折线,移动平均用粗折线,两条线叠在一起,既能看波动又能看趋势。
区域对比图放什么?放横向柱状图,按累计确诊排序,每个区域一根柱子。如果区域太多,只显示前十五名,其余归入“其他”。
明细表格放什么?放每个区域的最新数据,包含确诊、治愈、死亡、治愈率、死亡率等字段,支持排序和搜索。
4.2 图表选型与交互设计
图表选型要匹配数据特征。趋势数据用折线图,对比数据用柱状图,占比数据用饼图或环形图,地理分布用地图。不要为了好看选不合适的图表类型,比如用饼图展示时间趋势,那就是灾难。
交互设计上,我建议至少做三件事:鼠标悬停显示数值、图例点击切换显示、时间范围筛选。这三件事用ECharts都能轻松实现,配置项里加几行代码就行。
option = { tooltip: { trigger: "axis" }, legend: { data: ["新增确诊", "七日平均"] }, xAxis: { type: "category", data: dates }, yAxis: { type: "value" }, series: [ { name: "新增确诊", type: "bar", data: newCases }, { name: "七日平均", type: "line", smooth: true, data: avgCases } ] };这段配置的关键是tooltip和legend。trigger: "axis"让悬停时显示该时间点所有系列的值,legend让用户能点击图例隐藏或显示某个系列。这两个功能对数据探索非常重要。
注意:图表颜色不要超过五种,否则用户分不清。同一含义的数据在不同图表中要用同一颜色,保持一致性。
4.3 响应式布局与加载优化
网页要在不同设备上都能正常显示,所以布局要用响应式设计。核心思路是用百分比宽度代替固定像素,用flex或grid做弹性布局,在小屏幕上自动堆叠。
加载优化方面,主要做两件事。第一,数据文件要压缩,JSON比CSV加载快,gzip压缩后体积能减少七成以上。第二,图表按需渲染,首屏只渲染可见区域的图表,滚动到下方再加载其余图表。
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; padding: 16px; } .card { background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }这段CSS用auto-fit和minmax实现了自适应网格。屏幕宽时一行显示多个卡片,屏幕窄时自动变成一列。gap控制间距,box-shadow增加层次感。整套样式不到二十行,效果却很稳。
5. 常见问题与排查技巧实录
5.1 采集环节的高频问题
采集环节最常见的问题是请求失败和解析出错。请求失败的原因可能是网络波动、请求头不完整、访问频率过高。排查时先看状态码,403通常是请求头问题,429是频率问题,500以上是数据源问题。
解析出错的原因通常是页面结构变化或字段缺失。排查时先把响应内容保存下来,用文本编辑器打开看实际结构,再对照解析代码找差异。不要凭猜测改代码,要看实际数据。
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 返回403 | 请求头不完整 | 检查User-Agent等字段 | 补全请求头,模拟浏览器 |
| 返回429 | 请求频率过高 | 查看请求间隔 | 增加延时,降低频率 |
| 解析结果为空 | 页面结构变化 | 保存响应内容对比 | 更新解析规则 |
| 数据重复 | 去重逻辑缺失 | 检查唯一键设置 | 按日期+区域去重 |
| 字段缺失 | 数据源格式调整 | 对比新旧字段列表 | 补充默认值或跳过 |
这张表是我踩坑后整理的,基本覆盖了采集环节九成以上的问题。遇到新问题时,先对照这张表排查,能省不少时间。
5.2 分析环节的数据陷阱
分析环节最容易踩的坑是“数据看起来对,其实错了”。比如累计确诊出现下降,说明数据有修正;新增确诊出现负值,说明统计口径变了;治愈率超过百分之百,说明分子分母口径不一致。
这些问题的根源是数据源本身在调整,而不是代码写错了。处理方式是在分析结果中标注异常点,说明可能的原因,而不是强行修正数据。数据工作者要尊重数据的真实性,不能为了图表好看就篡改数据。
另一个陷阱是“幸存者偏差”。比如只看确诊数高的区域,会忽略那些确诊数低但死亡率高的区域。分析时要多维度交叉,不要只盯一个指标。
提示:每次分析前先做数据质量检查,看缺失率、异常值比例、字段一致性。这一步花五分钟,能省后面五小时的排查时间。
5.3 展示环节的性能瓶颈
展示环节的性能问题通常出现在数据量大的时候。如果一次性加载几十万条记录,浏览器会卡死。解决办法是服务端聚合,只把汇总结果传给前端,明细数据按需加载。
另一个瓶颈是图表数量过多。一个页面放十几个图表,渲染时间会很长。我的做法是首屏只放三到四个核心图表,其余图表折叠或分页,用户点击后再渲染。
如果数据更新频繁,还要考虑缓存策略。静态数据可以缓存几小时,动态数据可以缓存几分钟。缓存能大幅减少重复计算和请求,提升响应速度。
6. 项目扩展与个人经验分享
6.1 可以继续深挖的方向
这个项目跑通之后,有几个方向可以继续扩展。第一个方向是引入预测模型,用时间序列方法预测未来几天的趋势。第二个方向是做区域聚类,把疫情走势相似的区域归为一类,分析共性特征。第三个方向是接入实时数据流,用消息队列做增量更新,实现准实时展示。
预测模型方面,ARIMA和Prophet都是不错的选择,前者适合平稳序列,后者适合有季节性的序列。聚类方面,K-Means和层次聚类都可以用,关键是把时间序列转换成特征向量。实时流方面,可以用轻量级的消息队列加定时任务,不必上重型框架。
这些扩展不是必须的,但能让项目从“练习”升级为“作品”。我的建议是先把基础链路跑稳,再考虑扩展,不要一开始就贪多。
6.2 我在实操中总结的几条经验
第一条经验是:先跑通,再优化。很多新手一开始就纠结代码结构、性能优化,结果项目迟迟跑不起来。正确的做法是先写最简版本,能采集、能分析、能展示就行,然后再逐步重构。
第二条经验是:数据质量比算法重要。我见过太多人花大量时间调模型参数,却忽略了数据本身的缺失和错误。实际上,干净的数据加简单的方法,效果往往好过脏数据加复杂模型。
第三条经验是:展示要克制。不要把所有能算的指标都堆到页面上,用户看不过来。只放最关键的几个指标,其余放到详情页或下载文件里。少即是多,这个道理在数据展示上尤其成立。
第四条经验是:做好日志和版本管理。采集和分析过程要记录日志,方便排查问题。代码和数据要用版本管理工具管理,方便回溯和对比。这两件事看起来麻烦,实际能省大量时间。
6.3 给不同基础读者的上手建议
如果你是完全新手,建议从最小闭环开始:先写一个脚本采集一天的数据,保存成CSV;再用pandas读出来算几个简单指标;最后用matplotlib画一张图。这三步跑通,你就理解了整条链路。
如果你有一定基础,可以直接上Flask加ECharts,做一个带交互的网页。重点放在数据清洗和指标计算上,展示部分用现成的图表库,不要自己造轮子。
如果你是想做作品集,建议在项目中加入一些差异化设计,比如区域对比的雷达图、趋势预测的置信区间、数据质量的评分卡。这些细节能让项目脱颖而出。
最后分享一个小技巧:把整个项目拆成多个独立的Python脚本,每个脚本负责一个环节,用命令行参数控制输入输出。这样调试时只跑单个脚本,不用每次跑全流程,效率能提升好几倍。