☰
新冠病毒数据集实战:从地理位置到城市坐标的完整分析链路
2026/10/14 15:56:02 网站建设 项目流程

简介:这份资源面向数据分析初学者、地理信息可视化爱好者及疫情数据研究者,提供一套可直接上手的新冠疫情时空分析基础数据。包内共3个文件,以csv表格与json地理数据为主,压缩包约360KB,体积轻便、便于快速加载。其中地理边界数据可用于绘制中国地图轮廓,全国城市位置数据用于将各城市坐标映射到地图上,而2020年2月9日的疫情数据则记录了当日各地确诊情况,三者配合即可完成疫情分布地图、城市点位标注与区域对比等可视化练习。借助这套数据,读者能够熟悉Python中pandas读取表格、json解析地理边界以及matplotlib或pyecharts绘图的基本流程,并理解真实疫情数据的字段结构与空间关联方式,为后续更复杂的时空分析打下基础。目前已有4703人学习下载,适合作为课程作业、数据可视化入门项目或疫情趋势复现的素材。

1. 新冠病毒数据集:从地理位置到城市坐标,一份数据能撑起多少分析场景

2020 年 2 月 9 日前后,很多做数据分析和可视化的人都在找同一类东西:一份能同时给出病例数量、地理位置和全国城市坐标的新冠病毒数据集。原因很直接——单有确诊数字,你只能画折线图;单有城市经纬度,你只能画散点图;只有把「时间、空间、数量」三个维度拼在一起,才能做出真正有解释力的分析。这份数据集的核心价值就在这里:它把疫情统计数据和地理信息做了关联,让「哪个城市、什么时间、多少病例」变成一张可以查询、可以聚合、可以可视化的表。

它适合三类人:做数据可视化练手的前端或数据分析新手,需要快速拿到带地理维度的真实数据;做空间分析的研究者,想验证聚集性、传播距离、城市间差异;以及做数据工程项目的人,想拿一份结构清晰但又有真实脏数据特征的数据集来练 ETL。这篇文章不讲空泛的背景,只讲怎么把这份数据真正跑起来——从字段理解、坐标匹配、数据清洗,到可视化落地和常见翻车点。

2. 先搞清楚数据里有什么:字段结构、坐标系统和关联逻辑

2.1 三类数据的分工:病例表、城市坐标表、地理区域表

拿到一份「包含地理位置、全国城市位置、2020-2-9 新冠病毒数据集」的资源,第一件事不是急着画图,而是把里面的表拆开看。常见做法是把它理解成三张逻辑表:

第一张是病例统计表,通常包含日期、省份、城市、确诊数、疑似数、死亡数、治愈数等字段。这张表是事实表,记录的是「某个时间点某个地方的数字」。

第二张是城市坐标表,包含城市名、省份、经度、纬度,有的还会带行政区划代码。这张表是维度表,作用是给病例表补上空间信息。

第三张是地理区域表,可能是省份边界、城市边界或者区域分组,用来做地图渲染时的区域匹配。

三张表能不能关联起来,取决于一个关键字段:城市名或行政区划代码。这里就是第一个坑——病例表里的城市名和坐标表里的城市名,往往不是一套写法。

2.2 城市名匹配:为什么「北京」和「北京市」会让你的 join 失败

我一般会先做一次字段探查,用 Python 把两张表的城市名各自去重,然后做差集。下面这段代码是我常用的探查模板:

import pandas as pd # 读取病例表和坐标表,注意编码,中文数据常见 utf-8 或 gbk cases = pd.read_csv("cases_20200209.csv", encoding="utf-8") coords = pd.read_csv("city_coords.csv", encoding="utf-8") # 统一去掉首尾空格,避免看不见的空格导致匹配失败 cases["city"] = cases["city"].astype(str).str.strip() coords["city"] = coords["city"].astype(str).str.strip() # 查看病例表里有、但坐标表里没有的城市 case_cities = set(cases["city"].unique()) coord_cities = set(coords["city"].unique()) missing = case_cities - coord_cities print("病例表中无法匹配坐标的城市数量:", len(missing)) print("示例:", list(missing)[:20])

这段代码的逻辑很直接:先把城市名做标准化去空格,再用集合差集找出匹配不上的城市。参数上唯一需要注意的是编码,中文 CSV 经常是 GBK,如果读出来乱码就换encoding="gbk"再试。输出结果里如果出现「北京」和「北京市」同时存在,就说明需要做名称归一化。

归一化的常见做法是建一张映射表,把「市」「省」「自治区」这类后缀统一处理,同时处理「恩施土家族苗族自治州」这种长名称的简写问题。不要用正则一刀切,因为有些城市名本身带「市」字但不是后缀,硬切会出错。

2.3 经纬度坐标系:WGS84 和 GCJ02 别混用

坐标表里的经纬度,必须确认坐标系。常见的有 WGS84(GPS 原始坐标)和 GCJ02(国内地图常用的加密坐标)。如果你拿 WGS84 的坐标直接往 GCJ02 的地图上打点,会出现几百米的偏移,在城市级别看不出来,但做街道级或聚集性分析时就是灾难。

判断方法很简单:拿几个已知地标的坐标去对应地图上验证。如果偏差在几十米内,基本是同一套;如果偏差几百米,就是混用了。处理方式是要么统一转成一套,要么在可视化时用对应坐标系的地图底图。这一步没有捷径,必须验证。

3. 把数据跑起来:从清洗到可视化的完整链路

3.1 数据清洗:处理缺失值、重复记录和异常数字

真实疫情数据几乎没有干净的。2020 年 2 月 9 日这个时间点,很多城市的统计口径还在变化,常见问题有三类:缺失值、重复记录、异常数字。

缺失值方面,疑似数、死亡数、治愈数经常为空。处理策略取决于分析目标:如果只做确诊数可视化,可以直接忽略;如果要做死亡率分析,就必须把空值当 0 还是当缺失,需要明确标注。

重复记录方面,同一城市同一天可能出现多条,原因是数据合并了不同来源。去重逻辑要按「日期 + 城市」做主键,保留最新或最完整的一条。

异常数字方面,偶尔会出现确诊数突然为负或者大得离谱,这通常是录入错误。下面是我常用的清洗代码:

import pandas as pd import numpy as np df = pd.read_csv("cases_20200209.csv", encoding="utf-8") # 统一城市名和省份名 df["city"] = df["city"].astype(str).str.strip() df["province"] = df["province"].astype(str).str.strip() # 数值字段转数字,无法转换的变成 NaN for col in ["confirmed", "suspected", "dead", "cured"]: df[col] = pd.to_numeric(df[col], errors="coerce") # 缺失值填 0,但要记录哪些是原本就缺失的 df["suspected"] = df["suspected"].fillna(0) df["dead"] = df["dead"].fillna(0) df["cured"] = df["cured"].fillna(0) # 去掉确诊数为负的异常记录 df = df[df["confirmed"] >= 0] # 按日期和城市去重,保留确诊数最大的一条 df = df.sort_values("confirmed", ascending=False) df = df.drop_duplicates(subset=["date", "city"], keep="first") print("清洗后记录数:", len(df)) print(df[["date", "province", "city", "confirmed"]].head())

逻辑说明:先做类型转换,把可能的字符串数字变成数值,无法转换的用coerce变成 NaN,避免后续计算出错。缺失值填 0 是一种简化处理,适合可视化场景,但如果做统计推断,建议保留缺失标记。去重时按确诊数降序再保留第一条,相当于保留信息量最大的记录。参数上,drop_duplicates的subset决定了去重维度,这里用日期加城市,是因为分析粒度是「城市日」。

3.2 坐标关联:把病例数字落到地图上

清洗完病例表,下一步是关联坐标。用 pandas 的 merge 就能完成,但要注意关联后的行数变化——如果坐标表里有重复城市,会导致病例记录被放大。

coords = pd.read_csv("city_coords.csv", encoding="utf-8") coords["city"] = coords["city"].astype(str).str.strip() # 坐标表去重,避免一对多导致数据膨胀 coords = coords.drop_duplicates(subset=["city"], keep="first") # 左连接,保留所有病例记录 merged = pd.merge(df, coords[["city", "lng", "lat"]], on="city", how="left") # 检查有多少记录没匹配上坐标 unmatched = merged[merged["lng"].isna()] print("未匹配坐标的记录数:", len(unmatched)) print(unmatched["city"].unique()[:20])

这里的关键参数是how="left",保证病例表的所有记录都保留,匹配不上的坐标为空,方便后续单独处理。如果匹配率低于 90%,说明城市名标准化没做好,需要回到上一步补映射规则。

3.3 可视化落地:用 pyecharts 做城市级疫情地图

数据准备好之后,可视化是最直观的验证方式。我一般用 pyecharts 做快速验证,因为它对中文城市名和地图支持比较好。

from pyecharts.charts import Geo from pyecharts import options as opts from pyecharts.globals import ChartType # 只取确诊数大于 0 的城市 plot_data = merged[merged["confirmed"] > 0][["city", "confirmed"]] plot_data = plot_data.groupby("city", as_index=False)["confirmed"].sum() geo = ( Geo() .add_schema(maptype="china") .add( "确诊数", [list(z) for z in zip(plot_data["city"], plot_data["confirmed"])], type_=ChartType.EFFECT_SCATTER, ) .set_series_opts(label_opts=opts.LabelOpts(is_show=False)) .set_global_opts( title_opts=opts.TitleOpts(title="2020-02-09 全国城市确诊分布"), visualmap_opts=opts.VisualMapOpts(max_=plot_data["confirmed"].max()), ) ) geo.render("covid_map.html")

这段代码的逻辑是:先按城市聚合确诊数,再用 Geo 图打点,EFFECT_SCATTER会生成带涟漪效果的点,适合表现数量差异。visualmap_opts的max_参数控制颜色映射的上限,如果某个城市数值特别大,会导致其他城市颜色都很浅,这时候可以用对数或者分位数来调整。

渲染出来的 HTML 可以直接在浏览器打开,不需要额外服务。如果点打不上,优先检查城市名是否和 pyecharts 内置的地图城市名一致,这是最常见的翻车点。

4. 避坑与排查:这份数据集最容易翻车的 5 个地方

4.1 城市名对不上,地图上少了一大片

现象:可视化时发现某些省份的城市完全没有点,但数据里明明有记录。

原因:病例表的城市名和坐标表或地图组件的城市名不一致,比如「恩施」和「恩施土家族苗族自治州」、「乌鲁木齐」和「乌鲁木齐市」。

解决:建一张标准化映射表,把所有变体映射到统一名称。映射表要人工核对,不要用自动替换,因为有些简称会和其他城市冲突。

4.2 经纬度偏移,点飘到隔壁城市

现象:地图上的点位置明显不对,比如某个城市的点落在邻市。

原因:坐标系混用,WGS84 和 GCJ02 没有统一。

解决:确认坐标表来源,统一转换成同一套坐标系。如果无法转换,至少在可视化时用对应底图,并标注坐标系。

4.3 数据重复导致数字翻倍

现象:某个城市的确诊数比官方公布的高很多。

原因:病例表里同一城市同一天有多条记录,merge 坐标后没有去重。

解决:在 merge 之前先按「日期 + 城市」去重,merge 之后再检查一次行数是否和预期一致。

4.4 缺失值填 0 导致统计失真

现象:计算死亡率或治愈率时结果异常低。

原因:把缺失的死亡数、治愈数直接填 0,相当于假设这些城市没有死亡或治愈,但实际是数据没统计。

解决:区分「真实为 0」和「缺失」,缺失值单独标记,统计时排除或做敏感性分析。

4.5 时间字段格式不统一,排序出错

现象:按日期排序时顺序混乱,或者时间序列图断裂。

原因:日期字段有的是「2020-02-09」,有的是「2020/2/9」,格式不统一。

解决:统一用pd.to_datetime转换,指定format参数或让 pandas 自动推断,转换后再排序。

5. 进阶用法:把这份数据集变成可复用的分析模板

5.1 用空间自相关验证聚集性

如果你不满足于画地图,想验证「疫情是否存在空间聚集」,可以用空间自相关指标。常见做法是计算 Moran's I,需要用到城市间的空间权重矩阵。权重可以用距离倒数或者邻接关系构建。这一步对坐标精度要求高,如果坐标系混用,结果会完全失真。

import numpy as np from scipy.spatial.distance import cdist # 取有坐标的城市 geo_data = merged.dropna(subset=["lng", "lat"]).copy() coords_array = geo_data[["lng", "lat"]].values # 用欧氏距离构建权重矩阵,距离越近权重越大 dist_matrix = cdist(coords_array, coords_array, metric="euclidean") weights = 1 / (dist_matrix + 1e-6) # 加小量避免除零 np.fill_diagonal(weights, 0) # 标准化权重 weights = weights / weights.sum(axis=1, keepdims=True) # 计算 Moran's I x = geo_data["confirmed"].values x_mean = x.mean() x_centered = x - x_mean numerator = np.sum(weights * np.outer(x_centered, x_centered)) denominator = np.sum(x_centered ** 2) moran_i = (len(x) / weights.sum()) * (numerator / denominator) print("Moran's I:", moran_i)

这段代码的核心是构建空间权重矩阵,然后用 Moran's I 公式计算。参数上,距离度量的选择会影响结果,欧氏距离适合小范围,大范围建议用球面距离。1e-6是为了避免对角线除零。Moran's I 大于 0 表示正相关,说明高值城市倾向于聚集;小于 0 表示离散。这个指标只能说明是否存在聚集,不能说明原因。

5.2 用时间序列做城市间对比

把数据按城市和日期透视,可以得到每个城市的时间序列。用这个可以做城市间增长曲线对比,找出增长模式不同的城市。

# 透视成日期 × 城市的确诊数矩阵 pivot = merged.pivot_table( index="date", columns="city", values="confirmed", aggfunc="sum" ).fillna(0) # 计算每个城市的日增长率 growth = pivot.pct_change().replace([np.inf, -np.inf], 0).fillna(0) # 找出增长率波动最大的 5 个城市 volatility = growth.std().sort_values(ascending=False) print(volatility.head())

逻辑说明:pivot_table把长表变成宽表,aggfunc="sum"处理可能的重复。pct_change计算日增长率,无穷值替换为 0 是因为除零会产生 inf。波动性用标准差衡量,波动大的城市可能是数据质量问题,也可能是真实疫情变化剧烈,需要结合原始数据判断。

5.3 一个我踩过的坑:别用这份数据做预测

最后说一个血泪经验。2020 年初的数据统计口径变化非常频繁,今天确诊标准变了,明天疑似转确诊规则变了,这些都会导致时间序列出现跳变。我见过有人直接拿这份数据跑时间序列预测,结果模型学到的全是口径变化,不是真实传播规律。如果你要做预测,至少要先做变点检测,把口径变化的时间点标出来,或者只用口径稳定的一段数据。数据本身没问题,问题是用数据的人忽略了它的生成过程。

希望帮到你。

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

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

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

立即咨询