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

三、数据可视化

plotlyPython中一个非常强大的可视化库,这次我们就采用它来完成本次的可视化任务。

全国疫情趋势图

首先,我们想看到一个全国疫情的趋势图,而趋势又可以分为新增趋势和累计趋势。

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坐标轴转换为对数坐标轴。

image-20200412142312494

我们将上边的layout配置中的yaxis调整一下,去掉'type': 'log'之前的注释。这样,所有指标的趋势我们就都可以看得一清二楚了。不过对数轴的图在理解时一定要和线性轴区分开,这里同样长度的间隔在不同的数值区间代表的量级是不一样的,线条变动的幅度和真正数据量级的变化也不一样。我们可以这样来理解:正常的线性坐标轴看的是1,2,3,4, \cdots,​但是对数坐标轴看的是1,10,100,1000,\cdots。还有一个问题是当数据等于或小于0时,在图中是体现不出来的,因为log_{10}x当且仅当x>0时有解。具体选用哪种坐标轴,需要结合实际情况来看。

image-20200412143603816

疫情地图

接下来我们想要看一下全国不同省市的疫情趋势,由于全国有几十个省份,如果每个省份都画一个趋势图的话,未免也太过繁琐。因此我们考虑以地图热点的形式来展示这些信息。

目前``plotly`并没有提供对于中国各省市地图的原生支持,但是它可以支持使用GeoJSON来配置我们自己的地图。因此我们只需要将中国各省份的GeoJSON作为一个参数传递进去即可。阿里云有提供导出GeoJSON的免费工具:http://datav.aliyun.com/tools/atlas

我们发现在这个数据中,有一个properties.name字段是省份的名称,这和我们获取到的全拼的省份名称不一样,因此我们需要做一个映射。

image-20200412180901487
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可以直接支持全球国家级的地图,在此就不重复劳动了。大家可以自己尝试一下,作为一个练习。看一百遍不如自己亲自实践一遍。

大家有任何问题,都可以在下方留言,或者关注后私信沟通。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容