美文网首页
Python可视化:新冠疫情发展趋势绘制【动画】

Python可视化:新冠疫情发展趋势绘制【动画】

作者: 老Q在折腾 | 来源:发表于2020-04-13 01:20 被阅读0次

    最近这几个月,新冠疫情牵动了全国乃至全世界人民的心。股市崩盘、经济发展开倒车都已经是小事情了,最令人担忧的是每天都有许多家庭在面对令人难以承受的别离。非常感谢我们伟大的政府,感谢我们领导人的强大魄力,感谢我们国家对于生命的尊重,让我们在经历了阵痛之后将局面掌控了下来。然而在这个全球经济趋向于一体化的时代,谁又能独善其身呢?

    病毒从哪里来我们不清楚,就不多说了,各国如何应对疫情我也不想置评,毕竟就算我们操心操到着急上火也于事无补,每个国家都有自己的判断和想法。但是对于整个新冠疫情的发展趋势,我们不得不关心。

    之前的几个月,我每天早上起床就是打开丁香园、头条等平台的疫情地图,看一下是否情况有好转;到了最近,除了国内的情况,又开始关注海外疫情的发展。这些平台做了很好的工具,能帮助我们迅速了解各种信息。但是作为一个数据人,我们怎么能停留在知其然而不知其所以然的层次呢?

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

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

    相关文章

      网友评论

          本文标题:Python可视化:新冠疫情发展趋势绘制【动画】

          本文链接:https://www.haomeiwen.com/subject/qokwmhtx.html