0%

COVID-19 疫情可视化

2020年有个很魔幻的开局,从一只蝙蝠开始,迎来了不弱于03年非典的新冠状病毒。这一次疫情期间,全国上下减少外出,团结一心,共同抗疫,取得了奇迹般的成就。

疫情期间,信息的及时公布显得尤为重要。正是政府不断公布无死角的发布信息,分析解读,才使得公众对疫情的了解不断深入,对疫情状况有充分了解,并将恐惧与不安降到最低。

信息的及时公布很重要,但是如何把信息直观的展现出来也很重要,大量的数据涌来时,可视化无疑是最好的选择。

本文将记录我如何借助Python来完成一张疫情期间数据可视化的地图。

契机

停课不停学期间,作为计算机专业的大学生,自然是通过互联网与老师联系并进行新知识的学习。而在我们的GIS课程上,老师结合实际提出了制作疫情地图的作业要求。

为了完成作业,我开始了此次的制作。

过程

准备

制作疫情地图的话,很容易想到的就是一张静态地图,不同的区域因感染人数的多少而配以不同的颜色,制作起来就非常简单。但实际上,这样的工作一次还好,不适合大量重复实践,而且做出的图交互性比较差。

那么与之相对的,需要有一个可以帮我省去这个大量重复的工具,就自然而然的想到了程序设计,尤其的,python程序设计。

python语言作为一门热门语言,有着大量优秀的第三方库,且语法简单,开发迅速,因此自然成为了我此次不二的选择。

那么,接下来是选择一个合适的第三方库以提供数据的载体地图,在经过众多比较后,我选择了pyecharts。pyecharts是基于echart的,封装成python的函数,并支持渲染成网页,比较利于制作完成后的展示。由于是国产的作品,所以在地图的使用上便可以放轻松。

有了载体后,数据从何而来呢?有个不错的选择,github上有一个更新及时的数据库CSSEGISandData,维护者是疫情期间知名可视化网站COVID-19 Dashboard by the Center for Systems Science and Engineering (CSSE) at Johns Hopkins University (JHU)的作者,通过爬虫来及时获取数据,格式良好,只需要clone下来即可,后续通过pull进行更新,可以免去爬虫的编写,是非常理想的数据源。

于是我选择了FlyAI的数据源。其实相较而言,FlyAI的数据并没有非常大的优势,使用起来也不如github上的数据库方便。之所以选择FlyAI纯粹是因为我想起github时疫情地图已经制作完成了emm。

我选取了CSSEGISandData的数据作为数据来源(鬼知道交作业前一晚上发现FlyAI突然不提供数据后我心里是何等的卧槽),这样后续的更新可以通过命令行随时pull获取,比较方便。

准备阶段还差一样工具,获取到的数据需要经过处理才能使用,需要一款数据处理的工具,我的选择是numpypandas,pandas是python著名的数据处理库,经常与numpy联合使用,用途广泛,它也将在本次的疫情地图制作中发挥出重要的作用。

开始

通过venv创建好虚拟环境,再使用pip安装pyecharts和pandas。

1
2
3
python3 -m venv GIS_venv
source GIS_venv/bin/activate
pip3 install pyecharts pydata

pyecharts的API封装十分友好,把所有的图表封装成了类,并且支持链式调用,调用起来几乎只是填写各个配置项而已。

比如说对于一个地图图表,只要在生成一个实例时填写各个配置项(参数)即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
map_chart = (
Map()
.add(
series_name="",
data_pair=map_data,
zoom=0.85,
center=[119.5, 34.5],
is_map_symbol_show=False,
itemstyle_opts={
"normal": {"areaColor": "#323c48", "borderColor": "#404a59"},
"emphasis": {
"label": {"show": Timeline},
"areaColor": "rgba(255,255,255, 0.5)",
},
},
)
)

最后再通过map_chart.render('index.html')进行渲染,输出一个html文件,再通过浏览器就可以进行查看。

而在本次的实际制作过程中,我没有从零开始一点点构建整个的图表,一是初次接触pyecharts,使用并不熟练,另外是希望尽快完成,于是我参考了官方的画廊,选取了一个示例china_gdp_from_1993_to_2018进行修改。

示例中是将数据写死在了代码中,这自然是为了展示的方便,但是实际上,数据必定是单独分离开来的。

首次获取数据的话需要把git库给clone下来

1
git clone https://github.com/CSSEGISandData/COVID-19.git

后续想要更新的话就pull一下

1
2
cd COVID-19/
git pull https://github.com/CSSEGISandData/COVID-19.git

从CSSEGISandData中获取到的数据格式是每日各市级的数量

需要做的基本处理是筛选出中国的数据,并按照省进行分组求和,求出每天各省的三项数据,只保留日期,省份,确诊,治愈,死亡这些表项,删除其它无用数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def __get_provinces_base_data():
# 列名为['报道时间', '省份', '治愈', '新增', '死亡']
global BASE_DATA
if (not isinstance(BASE_DATA, pd.DataFrame)):
Country = 'Country/Region'
Province = 'Province/State'
China = 'Mainland China'
Hong_Kong = 'Hong Kong'
Taiwan = 'Taiwan'
Macau = 'Macau'
Date = 'Last Update'
Recovered = 'Recovered'
Confirmed = 'Confirmed'
Deaths = 'Deaths'

Province_Map = {'Hubei': '湖北',
'Zhejiang': '浙江',
'Guangdong': '广东',
'Henan': '河南',
...}

Data_dir = '/COVID-19/csse_covid_19_data/csse_covid_19_daily_reports/'

date = datetime.date(2020, 1, 22)

df = pd.DataFrame(
{DATE: {}, PROVINCE: {}, CURED: {}, CONFIRMED: {}, DEAD: {}})
df = pd.DataFrame()
while date < datetime.date.today()+datetime.timedelta(days=1):
Data_path = os.getcwd() + Data_dir + date.strftime('%m-%d-%Y') + '.csv'
print('cope with data comes from '+Data_path)
if not os.path.exists(Data_path):
print(
'请通过\n \'cd COVID-19/\'\n \'git pull https://github.com/CSSEGISandData/COVID-19.git\'\n更新数据!')
break
if (date == datetime.date(2020, 3, 22)):
Country = 'Country_Region'
Province = 'Province_State'
Date = 'Last_Update'
if (date == datetime.date(2020, 3, 11)):
China = 'China'
d = pd.read_csv(Data_path)
d = d[d[Country].isin(
[China, Hong_Kong, Taiwan, Macau])]
d = d.loc[:, [Date, Province, Recovered, Confirmed, Deaths]]
d = d.fillna(value=0.0)
for key in Province_Map.keys():
d.loc[d[Province] == key, Province] = Province_Map[key]
d = d.rename(columns={Date: DATE, Province: PROVINCE,
Recovered: CURED, Confirmed: CONFIRMED, Deaths: DEAD})
d[DATE] = date.strftime('%Y-%m-%d')
d = d.set_index(PROVINCE)
df = pd.concat([df, d])
date = date + datetime.timedelta(days=1)
df[[CURED, CONFIRMED, DEAD]] = df[[
CURED, CONFIRMED, DEAD]].astype(float)
BASE_DATA = df
else:
df = BASE_DATA
return df

需要吐槽的是,因为数据来自国外整理,所以有时会对中国的一些省份做地区性质的描述。而且数据中中国的名称,以及表格的表项发生过变化,最终就导致需要在意的细节比较多。为了可以在pyecharts中使用,我还需要把英文的城市名称翻译成对应的中文。

二次加工成适合与图表的数据格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def get_provinces_sum_data(column: str):
# 获取各省累计治愈/确诊/死亡人数
df = __get_provinces_base_data()

dates = get_date_list()
provinces = get_provinces_list()

df = [df.groupby(DATE).get_group(date) for date in dates]

data = [{i: item.at[i, column]
for i in item.index} for item in df]

for item in data:
for province in provinces:
if province not in item:
item[province] = 0.0

return data

使用数据完成地图的构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
provinces_daily_sick_data = getData.get_provinces_sick_daily_data()

date_data = provinces_daily_sick_data[date_index]

map_data = [[key, date_data[key]] for key in date_data.keys()]

map_chart = (
Map()
.add(
series_name="",
data_pair=map_data,
zoom=0.85,
center=[119.5, 34.5],
is_map_symbol_show=False,
itemstyle_opts={
"normal": {"areaColor": "#323c48", "borderColor": "#404a59"},
"emphasis": {
"label": {"show": Timeline},
"areaColor": "rgba(255,255,255, 0.5)",
},
},
)
)

所有代码我都上传到了Github中:COVID-map

效果

类似的思路完成其它图表的数据填充,得到的最终效果:

因为渲染结果是一个HTML文件,于是我将它发布在了我的博客中:新冠肺炎疫情地图

思考

在最开始的时候,我有想过把疫情地图做成实时的,但是实施起来后发现会有些难度。如果基于pyecharts进行实时修改,那么就需要将修改程序放置在服务器上,而且博客本身又是基于hexo的,需要在服务器端与本地同步,这点倒是可以通过git实现;除此之外也许可以通过github的action进行自动更新,但是都过于麻烦,考虑到自身时间并不富裕,我最终决定只完成一定时间之前的部分,但为实时更新打下基础。

事实上,借由发布到博客上来展示最终成果的契机,我得以重新完成博客环境的搭建。在此之前,因为一些缘由,自闭了相当长时间,而且博客环境也在那个时候收到了损坏。因此,算是重新开始写博客吧,如果时间充分的话,会慢慢写些这段时间的成就。

写在最后的,是和新冠病毒疫情有关。这点我特别赞同这节课老师的一个看法,我所制作的可视化作品,炫酷本身并不值得炫耀(况且也说不上效果特别好),每个展示出来的数字,都有着生命般沉甸甸的重量,这不是一个可以拿来开玩笑的事情。正因如此,本篇文章进行了灰白处理,用以缅怀在这场战疫中逝去的人们。

愿逝者安息,生者坚强。