Result

关于新型冠状病毒的可视化项目不少,但基本都是静态的,还没看到表现疫情随时间变化的地图。正好组里有需要,且有一部分数据,所以我就做了市级别的动态“疫情.GIF”,这里分享一下制作类似动态地图的一种方法。上图是最后的结果,另外两个分辨率的图:原图1000*1000px(可以直接在微信上分享),Jupyter代码文件在这里

数据

绘制这种专题地图需要两部分数据:感染病例数据和行政底图数据。

按照需求,感染病例数据需要细分至市级别,而且还要历史各天都有,多亏组里的同学非常细致的收集整理,截止到2月6号之前每一天的数据都齐全。

全国行政底图数据是用来画图的基础,本来以为很容易找到,自己做的时候才发现各坑:国家边界不正确、缺少九段线等等、只有形状没有属性等。最后和同学找了半天,然后用ArcGIS修修补补才完成一个能用的。这里提醒一下画全国的底图需要注意的地方:

  • 国家、各省的边界要正确。
  • 台湾、九段线不能缺失。
  • 底图投影要正确,我国全国范围的地图一般用兰伯特Lambert投影(正轴等角割圆锥投影)。如果直接用经纬度或者Web墨卡托投影,会出现明显的变形。

两份数据有了之后,需要把疫情数据绑定到底图数据上,根据省市的名字进行连接,这就需要两份数据的省市名规则是一样的。但是现实很少尽如人意,在连接的时候总会发现各种问题,如一个数据为“湖北”,另一个为“湖北省”;一个为“新疆”,另一个为“新疆维吾尔自治区”,这些情况需要自己单独手动处理。

画图

数据备好,只欠画图,我们的思路是每天都出一幅图,最后将多天的图片合成GIF来展现动态。考虑到我们要出数十幅图,这些图片除了数据以外其他的都一致,所以通过代码来画图方便一些,这样一天的图画好之后,其他天的图只要把数据换一下就可以。

Python的Geopandas包可以直接读取shp文件,并调用Matplotlib来绘制地图,十分方便,例如官方的例子两行读取数据并画图:

import geopandas as gpd
world_df = gpd.read_file("world.shp")
world_df.plot()

world

这里将shp文件读成Pandas的DataFrame,然后直接调用plot方法作图,也可以指定根据属性/列来设置颜色,这正是我们需要的。

我们先读取底图数据,并将WGS84坐标转换为兰伯特投影坐标:

basemap_file = "data/basemap/全国省市级地图.shp"
bound_file = "data/basemap/bou2_4l.shp"

basemap = gpd.read_file(basemap_file)
basemap.crs = 'epsg:4326'
basemap = basemap.to_crs('epsg:3415')

bound = gpd.read_file(bound_file)
bound.crs = 'epsg:4326'
bound = bound.to_crs('epsg:3415')

这里我们用了两份数据,一个是市级的行政区域数据,用来填充不同的颜色,另一个是边界数据,有省界、九段线、南沙群岛等,叠加基础行政区划图上更加完整,看看底图效果:

figsize=(6,6)
fig, ax = plt.subplots(figsize=figsize)
ax.set_aspect('equal')
ax.axis('off')
basemap.plot(ax=ax)
bound.plot(ax=ax, color='black')

basemap

已经具有雏形,剩下的就是根据数据,把不同的区域设置不同的颜色,用来表示疫情的严重程度。这里要用上疫情数据:

number_file = "data/numbers/confirmed/新冠肺炎各省市病例数据2020-01-30.csv"
numbers = pd.read_csv(number_file, encoding="GBK")
numbers.fillna(0)
print(numbers.head())

#     省    市  新增确诊  新增康复  新增死亡  累计确诊  累计康复  累计死亡
# 0  湖北  武汉市   356     7    25  2261    82   129
# 1  湖北  黄冈市   172     3     7   496     7    12
# 2  湖北  孝感市   125     0     3   399     0     6
# 3  湖北  荆门市    49     0     0   191     0     4
# 4  浙江  温州市    58     4     0   172     7     0

然后根据省市名称把疫情数据与底图数据连接,这里省去了全部处理省市名的代码:

df = basemap.merge(numbers, on=["省", "市"], how="left")

最后就开始制图啦:

df.plot(column="累计确诊")

结果就会出来一幅巨丑的图(太辣眼睛就不放了),还是得自己调整一下线宽、颜色、分级方法等:

show_column = "累计确诊"
df["show"] = df[show_column].fillna(0)

figsize=(15, 15)
fig, ax = plt.subplots(figsize=figsize)
ax.set_aspect('equal')

bins = [0, 10, 50, 100, 200, 500]
labels = ["None / Data Missing", "1-10", "11-50", "51-100", "101-200", "201-500", "501-"]
classification_kwdsdict = {"bins":bins}
legend_kwds = {"loc":"upper center", "fontsize":16,}# "labels":labels}
style_kwdsdict = {"edgecolor":"#ffffff", "linewidth": 0.8, "cmap":"Reds", }

ax = df.plot(column="show", ax=ax, legend=True, **style_kwdsdict, legend_kwds=legend_kwds, scheme="UserDefined", classification_kwds=classification_kwdsdict)
texts = ax.get_legend().get_texts()
for i in range(len(texts)):
    texts[i].set_text(labels[i])

bound.plot(ax=ax, edgecolor='#000000', linewidth=1)
ax.axis('off')
ax.set_title(title, fontsize=30)
fig.savefig(os.path.join(output_dit, title+'.jpg'), dpi=300)

ConfirmedCase2020-01-30

这里单幅地图就绘制好了,要绘制所有图,写个循环把numbers_file换成其他天的数据就可以,非常方便。

List

制作GIF

每天一幅图已经制作好,剩下的就是把多张图片压制成GIF图,展现动态变化。用Photoshop制作GIF很方便,还能调节每一幅图片停留的时常。用“文件–脚本–将文件载入堆栈”一次导入所有图片,再点“窗口–时间轴”调出时间轴工具,然后“创建帧动画”,“从涂层导入帧”,就可以看到连续的动画了,如果发现顺序反了,可以点“反向帧”调整过来。

在每个帧上可以调整滞留时间,例如对于这次的疫情图,最开始的数据每天基本没变化,信息较少,每帧的时间可以短一些;后面疫情变化较快,信息多,每帧停留的时间长一些;最后一帧设置的更久一些,表示结束。

PS

设置完成,预览没问题,就直接“文件–导出–存储为Web所用格式”,选择GIF格式,完成。