Python可视化:新冠疫情发展趋势绘制【动画】
最近这几个月,新冠疫情牵动了全国乃至全世界人民的心。股市崩盘、经济发展开倒车都已经是小事情了,最令人担忧的是每天都有许多家庭在面对令人难以承受的别离。非常感谢我们伟大的政府,感谢我们领导人的强大魄力,感谢我们国家对于生命的尊重,让我们在经历了阵痛之后将局面掌控了下来。然而在这个全球经济趋向于一体化的时代,谁又能独善其身呢?
病毒从哪里来我们不清楚,就不多说了,各国如何应对疫情我也不想置评,毕竟就算我们操心操到着急上火也于事无补,每个国家都有自己的判断和想法。但是对于整个新冠疫情的发展趋势,我们不得不关心。
之前的几个月,我每天早上起床就是打开丁香园、头条等平台的疫情地图,看一下是否情况有好转;到了最近,除了国内的情况,又开始关注海外疫情的发展。这些平台做了很好的工具,能帮助我们迅速了解各种信息。但是作为一个数据人,我们怎么能停留在知其然而不知其所以然的层次呢?
今天我就教大家如何使用Python来将新冠疫情的发展趋势可视化出来。
一、数据收集
关于国内疫情的数据,最权威的来源当然是卫健委。中国卫健委以及各省市的卫健委每天早上都会发布详细的疫情通告,我们可以从这里获取信息;至于国外,各国的CDC(疾控中心)都会发布类似的信息。我们可以将这些信息抓取并解析出来。
下图就是中国卫健委在4月12日发布的疫情通报,这里边有着相对固定的模板,我们可以使用正则表达式来将我们需要的数字解析出来。
image-20200412130043608但是问题来了,先不说全世界这么多国家,单单是中国三十多个省市自治区,想要把数据都解析出来所需的时间成本就不是我们可以承受的。好在有一些令人尊敬的私人团体替我们完成了这些事情,并且将数据免费开源给了大家,开源万岁。
image-20200412130622637那现在我们就可以节省下大量的时间了,我们只需直接访问这一接口获取数据并将数据整理一下即可。
首先,我们最关注的自然是每天的确诊及治愈信息。全国数据我们需要关注下边这一个接口,我们需要在请求中附加国家、起始日期和是否包含港澳台的信息。
image-20200412131150382另外,我们需要申请一个API Key,并且附加在请求的Header之中。
image-20200412134355270各省市的数据接口也是类似,多说无益,那我就直接上代码了。
import requests
import datetime
import json
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
if __name__ == "__main__":
# 该接口需要我们在header中加一个token信息
header = {
'Token': 'xxxxx' # 输入你申请的API Key
}
# 全国及各省份明细数据接口
url_total_base = 'https://covid-19.adapay.tech/api/v1/infection/region?region=China&include_hmt=true&start_date={0}&end_date={1}'
url_detail_base = 'https://covid-19.adapay.tech/api/v1/infection/region/detail?region=China&include_hmt=true&start_date={0}&end_date={1}'
# 该接口提供的数据从1月22日开始,每次请求最多查询10天的数据
# 因此我们写一个函数,基于我们关注的时间区间生成每次查询的起始日期
def get_date_lists(start_date, end_date=None):
if end_date is None:
end_date = datetime.datetime.today().date() - datetime.timedelta(days=1)
date_list = []
if type(start_date) == str:
start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
while start_date <= end_date:
end_date_tmp = start_date + datetime.timedelta(days=9)
date_list.append([start_date.strftime('%Y-%m-%d'), end_date_tmp.strftime('%Y-%m-%d')])
start_date += datetime.timedelta(days=10)
return date_list
# 获取1月22日以来每次查询的起始日期
date_list = get_date_lists(start_date='2020-01-22')
# 获取数据
result_total = []
result_detail = []
for start_date, end_date in date_list:
# 获取全国数据
# 生成本次查询的真实url
url_total = url_total_base.format(start_date, end_date)
# 请求接口,并用json模块加载结果数据
res_total = json.loads(requests.get(url_total, headers=header).text)
# 判断请求返回结果是否正常
if res_total['code'] == '90000':
# 判断结果是否为空
if len(res_total['data']['region']['China']) == 0:
print(start_date + '~' + end_date + ' total data not ready')
else:
# 解析数据,这里因为有多层嵌套,直接生硬地把多层key解析成一个字符串,后续再做处理
df_total_tmp = pd.json_normalize(res_total['data']['region']['China'], max_level=1).stack()
result_total.append(df_total_tmp)
else:
print(start_date + '~' + end_date + ' total bad request')
# 获取各省份数据
# 与上边基本相同
url_detail = url_detail_base.format(start_date, end_date)
res_detail = json.loads(requests.get(url_detail, headers=header).text)
if res_detail['code'] == '90000':
if len(res_detail['data']['area']) == 0:
print(start_date + '~' + end_date + ' detail data not ready')
else:
df_detail_tmp = pd.json_normalize(res_detail['data']['area'], max_level=2).stack()
result_detail.append(df_detail_tmp)
else:
print(start_date + '~' + end_date + ' detail bad request')
# 合并多次请求返回的结果
total_data = pd.concat(result_total, axis=0).reset_index()
detail_data = pd.concat(result_detail, axis=0).reset_index()
好,到这里数据就获取到了。
二、数据清洗
我们先看下数据长什么样。
image-20200412140325655可以看到,日期和指标名称是放在一个字段之中的,并且用'.'分隔,各省市的明细数据也类似,我们需要将不同字段剥离出来。但是这样的话指标仍然是以行的形式存储,我们需要将不同的指标放到不同的列里边去。
# 将日期和指标解析出来,并将指标分别放到不同的列
df_total = total_data.copy()
df_total['date'] = df_total['level_1'].str.split('.').map(lambda x: x[0])
df_total['metrics'] = df_total['level_1'].str.split('.').map(lambda x: x[1])
df_total_stats = pd.pivot_table(df_total, index='date', columns='metrics', values=0).reset_index()
# 将省份、日期和指标解析出来,并将指标分别放到不同的列
df_detail = detail_data.copy()
df_detail['province'] = df_detail['level_1'].str.split('.').map(lambda x: x[0])
df_detail['date'] = df_detail['level_1'].str.split('.').map(lambda x: x[1])
df_detail['metrics'] = df_detail['level_1'].str.split('.').map(lambda x: x[2])
df_detail_stats = pd.pivot_table(df_detail, index=['date', 'province'], columns='metrics', values=0).reset_index()
全国和各省市的数据一样,都包含六个指标:每日新增确诊、累计确诊、新增治愈、累计治愈、新增死亡和累计死亡。我们还需要一个现有确诊的字段,这一指标由累计确诊减去累计治愈和累计死亡得来。
df_total_stats['current_confirmed'] = df_total_stats['confirmed'] - df_total_stats['deaths'] - df_total_stats['recovered']
df_total_stats.head()
image-20200412135601003
df_detail_stats['current_confirmed'] = df_detail_stats['confirmed'] - df_detail_stats['deaths'] - df_detail_stats['recovered']
df_detail_stats
image-20200412135629546
三、数据可视化
plotly
是Python
中一个非常强大的可视化库,这次我们就采用它来完成本次的可视化任务。
全国疫情趋势图
首先,我们想看到一个全国疫情的趋势图,而趋势又可以分为新增趋势和累计趋势。
config = {
'displaylogo': False,
'editable': True,
'responsive': False,
'displayModeBar': False
}
layout = {
'xaxis': {
'tickformat': '%m-%d',
'showspikes': True,
'spikemode': 'across',
'spikesnap': 'cursor',
'title': ''
},
'yaxis': {
# 'type': 'log',
'title': '',
'showspikes': True,
'spikemode': 'across',
'spikesnap': 'cursor'
},
'hoverdistance': 100,
'spikedistance': 1000,
'hovermode': 'x'
}
trace_confirmed_add = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['confirmed_add'],
name = '新增确诊'
)
trace_recovered_add = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['recovered_add'],
name = '新增治愈'
)
trace_deaths_add = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['deaths_add'],
name = '新增死亡'
)
data_add = [trace_confirmed_add, trace_recovered_add, trace_deaths_add]
fig = go.Figure(data=data_add, layout=layout)
fig.update_layout(title=dict(text='全国新冠疫情新增趋势图', x=0.5, xanchor='center'))
fig.update_traces(line_shape = 'spline')
fig.show(config=config)
trace_confirmed = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['confirmed'],
name = '累计确诊'
)
trace_recovered = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['recovered'],
name = '累计治愈'
)
trace_deaths = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['deaths'],
name = '累计死亡'
)
trace_cur_confirmed = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['current_confirmed'],
name = '现有确诊'
)
data_cum = [trace_confirmed, trace_recovered, trace_deaths, trace_cur_confirmed]
fig = go.Figure(data=data_cum, layout=layout)
fig.update_layout(title=dict(text='全国新冠疫情累计趋势图', x=0.5, xanchor='center'))
fig.update_traces(line_shape = 'spline')
fig.show(config=config)
可以看到,由于不同指标的量级不同,所以有些指标的趋势看不大清楚。一个处理办法是将y
坐标轴转换为对数坐标轴。
我们将上边的layout
配置中的yaxis
调整一下,去掉'type': 'log'
之前的注释。这样,所有指标的趋势我们就都可以看得一清二楚了。不过对数轴的图在理解时一定要和线性轴区分开,这里同样长度的间隔在不同的数值区间代表的量级是不一样的,线条变动的幅度和真正数据量级的变化也不一样。我们可以这样来理解:正常的线性坐标轴看的是,但是对数坐标轴看的是。还有一个问题是当数据等于或小于0时,在图中是体现不出来的,因为当且仅当时有解。具体选用哪种坐标轴,需要结合实际情况来看。
疫情地图
接下来我们想要看一下全国不同省市的疫情趋势,由于全国有几十个省份,如果每个省份都画一个趋势图的话,未免也太过繁琐。因此我们考虑以地图热点的形式来展示这些信息。
目前``plotly`并没有提供对于中国各省市地图的原生支持,但是它可以支持使用GeoJSON来配置我们自己的地图。因此我们只需要将中国各省份的GeoJSON作为一个参数传递进去即可。阿里云有提供导出GeoJSON的免费工具:http://datav.aliyun.com/tools/atlas。
我们发现在这个数据中,有一个properties.name
字段是省份的名称,这和我们获取到的全拼的省份名称不一样,因此我们需要做一个映射。
province_maper = {
'Anhui' : '安徽省',
'Beijing': '北京市',
'Chongqing': '重庆市',
'Fujian': '福建省',
'Gansu': '甘肃省',
'Guangdong': '广东省',
'Guangxi': '广西壮族自治区',
'Guizhou': '贵州省',
'Hainan': '海南省',
'Hebei': '河北省',
'Heilongjiang': '黑龙江省',
'Henan': '河南省',
'Hong Kong': '香港特别行政区',
'Hubei': '湖北省',
'Hunan': '湖南省',
'Jiangsu': '江苏省',
'Jiangxi': '江西省',
'Jilin': '吉林省',
'Liaoning': '辽宁省',
'Macao': '澳门特别行政区',
'Neimenggu': '内蒙古自治区',
'Ningxia': '宁夏省',
'Qinghai': '青海省',
'Shaanxi': '陕西省',
'Shandong': '山东省',
'Shanghai': '上海市',
'Shanxi': '山西省',
'Sichuan': '四川省',
'Taiwan': '台湾省',
'Tianjin': '天津市',
'Xinjiang': '新疆维吾尔自治区',
'Xizang': '西藏自治区',
'Yunnan': '云南省',
'Zhejiang': '浙江省'
}
df_detail_stats['province_name'] = df_detail_stats['province'].map(lambda x: province_maper[x])
然后我们分别绘制现有确诊地图和累计确诊地图,并且增加动画。
import plotly.express as px
geojson_str = open('全国.json', 'r').read()
geojson = json.loads(geojson_str)
colors = [
[0, 'white'],
[0.002, 'rgb(255,247,236)'],
[0.02, 'rgb(253,212,158)'],
[0.1, 'rgb(252,141,89)'],
[0.2, 'rgb(215,48,31)'],
[1, 'rgb(127,0,0)']
]
# 绘制现有确诊地图
fig = px.choropleth_mapbox(
df_detail_stats.rename(
{'date': '日期', 'province_name': '地区', 'current_confirmed': '现有确诊'},
axis=1
),
geojson=geojson,
locations="地区",
featureidkey="properties.name",
mapbox_style='white-bg',
zoom=3,
center={'lat':37, 'lon':102},
color='现有确诊',
color_continuous_scale=colors,
range_color=[0, 5000],
animation_frame='日期',
width=1000,
height=800
)
fig.update_geos(fitbounds="locations", visible=False)
fig.update_layout(title='中国COVID-19现有确诊地图', title_x=0.5)
fig.write_html('现有确诊.html', config=config)
# 绘制累计确诊地图
fig = px.choropleth_mapbox(
df_detail_stats.rename(
{'date': '日期', 'province_name': '地区', 'confirmed': '累计确诊'},
axis=1
),
geojson=geojson,
locations="地区",
featureidkey="properties.name",
mapbox_style='white-bg',
zoom=3,
center={'lat':37, 'lon':102},
color='累计确诊',
color_continuous_scale=colors,
range_color=[0, 5000],
animation_frame='日期',
width=1000,
height=800
)
fig.update_geos(fitbounds="locations", visible=False)
fig.update_layout(title='中国COVID-19累计确诊地图', title_x=0.5)
fig.write_html('累计确诊.html', config=config)
然后我们看一下效果。
现有确诊地图 累计确诊地图 image-20200412183509996当然,我们还可以使用plotly来绘制全球的疫情变化趋势,这个其实比绘制中国的地图更加简单,因为plotly可以直接支持全球国家级的地图,在此就不重复劳动了。大家可以自己尝试一下,作为一个练习。看一百遍不如自己亲自实践一遍。
大家有任何问题,都可以在下方留言,或者关注后私信沟通。