Python實戰:抓肺炎疫情實時資料,畫2019-nCoV疫情地圖

天元浪子發表於2020-02-05

1. 前言

今天,群裡白堊老師問如何用python畫武漢肺炎疫情地圖。白堊老師是研究海洋生態與地球生物的學者,國家重點實驗室成員,於不惑之年學習python,實為我等學習楷模。先前我並沒有關注武漢肺炎的具體資料,也沒有畫過類似的資料分佈圖。於是就拿了兩個小時,專門研究了一下,遂成此文。

2月6日追記:本文釋出後,騰訊的資料來源多次變更url和資料格式,導致程式碼無法執行。有很多熱心朋友已經留言,幫助修正了程式碼,現將這些修正補充到正文中。所有資料抓取的截圖均未變更,或有不符,請各位朋友明鑑。另有朋友諮詢如何在分省地圖上顯示各省名字,這次一併補充到程式碼中。

2. 資料下載

網上一搜,首先搜到的是騰訊的疫情實時追蹤,那就用這個資料來源吧。
在這裡插入圖片描述
有了網址怎麼抓資料呢?這裡,我送大家一雙火眼金睛,可以從紛亂中找到最靠譜的下載方式。我習慣用FireFox瀏覽器,下面的講解就以FireFox為例(其他瀏覽器基本類似)。

  • 開啟選單,點選“Web開發者”,在遞進選單中選擇"網路":

在這裡插入圖片描述
在這裡插入圖片描述

  • 重新整理頁面,我們很快就能發現,應答型別為json格式的這個請求,最有可能包含我們需要的資料了:

在這裡插入圖片描述
在這裡插入圖片描述

在這裡插入圖片描述

在這裡插入圖片描述

  • 深入分析,我們就得到了url地址、請求方法、引數、應答格式等資訊。查詢引數中,callback是回撥函式名,我們可以嘗試置空,_應該是以毫秒為單位的當前時間戳。有了這些資訊,分分鐘就可以抓到資料了。我們先在IDLE中以互動方式抓一下看看效果:
>>> import time, json, requests
>>> url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
>>> data = json.loads(requests.get(url=url).json()['data'])

只要兩行程式碼,就可以抓到資料了。怎麼樣,是不是超級簡單?我們在來看看資料結構:

>>> data.keys()
dict_keys(['chinaTotal', 'chinaAdd', 'lastUpdateTime', 'areaTree', 'chinaDayList', 'chinaDayAddList', 'isShowAdd'])
>>> d = data['areaTree'][0]['children']
>>> len(d)
34
>>> [item['name'] for item in d]
['湖北', '浙江', '廣東', '河南', '湖南', '江西', '安徽', '重慶', '山東', '江蘇', '四川', '上海', '北京', '福建', '黑龍江', '廣西', '陝西', '河北', '雲南', '海南', '山西', '遼寧', '天津', '貴州', '甘肅', '吉林', '內蒙古', '寧夏', '新疆', '香港', '青海', '臺灣', '澳門', '西藏']
>>> d[0]['children']
[{'name': '武漢', 'total': {'confirm': 10117, 'suspect': 0, 'dead': 414, 'heal': 431}, 'today': {'confirm': 1766, 'suspect': 0, 'dead': 52, 'heal': 58}}, {'name': '孝感', 'total': {'confirm': 1886, 'suspect': 0, 'dead': 25, 'heal': 9}, 'today': {'confirm': 424, 'suspect': 0, 'dead': 7, 'heal': 3}}, {'name': '黃岡', 'total': {'confirm': 1807, 'suspect': 0, 'dead': 29, 'heal': 60}, 'today': {'confirm': 162, 'suspect': 0, 'dead': 4, 'heal': 8}}, {'name': '隨州', 'total': {'confirm': 834, 'suspect': 0, 'dead': 9, 'heal': 9}, 'today': {'confirm': 128, 'suspect': 0, 'dead': 1, 'heal': 0}}, {'name': '荊州', 'total': {'confirm': 801, 'suspect': 0, 'dead': 10, 'heal': 18}, 'today': {'confirm': 88, 'suspect': 0, 'dead': 1, 'heal': 6}}, {'name': '襄陽', 'total': {'confirm': 787, 'suspect': 0, 'dead': 2, 'heal': 10}, 'today': {'confirm': 52, 'suspect': 0, 'dead': 0, 'heal': 3}}, {'name': '黃石', 'total': {'confirm': 566, 'suspect': 0, 'dead': 2, 'heal': 25}, 'today': {'confirm': 57, 'suspect': 0, 'dead': 0, 'heal': 7}}, {'name': '宜昌', 'total': {'confirm': 563, 'suspect': 0, 'dead': 6, 'heal': 9}, 'today': {'confirm': 67, 'suspect': 0, 'dead': 2, 'heal': 0}}, {'name': '荊門', 'total': {'confirm': 508, 'suspect': 0, 'dead': 17, 'heal': 21}, 'today': {'confirm': 86, 'suspect': 0, 'dead': 1, 'heal': 5}}, {'name': '鄂州', 'total': {'confirm': 423, 'suspect': 0, 'dead': 18, 'heal': 8}, 'today': {'confirm': 41, 'suspect': 0, 'dead': 0, 'heal': 2}}, {'name': '咸寧', 'total': {'confirm': 399, 'suspect': 0, 'dead': 1, 'heal': 3}, 'today': {'confirm': 15, 'suspect': 0, 'dead': 1, 'heal': 1}}, {'name': '十堰', 'total': {'confirm': 353, 'suspect': 0, 'dead': 0, 'heal': 14}, 'today': {'confirm': 35, 'suspect': 0, 'dead': 0, 'heal': 5}}, {'name': '仙桃', 'total': {'confirm': 265, 'suspect': 0, 'dead': 5, 'heal': 0}, 'today': {'confirm': 40, 'suspect': 0, 'dead': 1, 'heal': 0}}, {'name': '恩施州', 'total': {'confirm': 144, 'suspect': 0, 'dead': 0, 'heal': 10}, 'today': {'confirm': 6, 'suspect': 0, 'dead': 0, 'heal': 4}}, {'name': '天門', 'total': {'confirm': 138, 'suspect': 0, 'dead': 10, 'heal': 1}, 'today': {'confirm': 10, 'suspect': 0, 'dead': 0, 'heal': 1}}, {'name': '潛江', 'total': {'confirm': 64, 'suspect': 0, 'dead': 1, 'heal': 0}, 'today': {'confirm': 10, 'suspect': 0, 'dead': 0, 'heal': 0}}, {'name': '神農架', 'total': {'confirm': 10, 'suspect': 0, 'dead': 0, 'heal': 2}, 'today': {'confirm': 0, 'suspect': 0, 'dead': 0, 'heal': 0}}, {'name': '地區待確認', 'total': {'confirm': 0, 'suspect': 0, 'dead': 0, 'heal': 3}, 'today': {'confirm': 0, 'suspect': 0, 'dead': 0, 'heal': 0}}]

3. 資料處理

以省為單位畫疫情圖,我們只需要統計同屬一個省的所有地市的確診資料即可。最終的資料抓取程式碼如下:

import time, json, requests

def catch_distribution():
    """抓取行政區域確診分佈資料"""
    
    data = {}
    url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
    for item in json.loads(requests.get(url=url).json()['data'])['areaTree'][0]['children']:
        if item['name'] not in data:
            data.update({item['name']:0})
        for city_data in item['children']:
            data[item['name']] += int(city_data['total']['confirm'])
    
    return data

4. 資料視覺化

資料視覺化,我習慣使用matplotlib模組。matplotlib有很多擴充套件工具包(toolkits),比如,畫3D需要mplot3d工具包,畫地圖的話,則需要basemap工具包,以及處理地圖投影的pyproj模組。另外畫海陸分界線、國界線、行政分界線等還需要shape資料。所需模組請自行安裝,shape檔案可以從這裡下載,繪圖用到的向量字型檔可以從自己的電腦上隨便找一個(我用的是simsun.ttf)。我的主程式是2019nCoV.py,shape檔案下載下來之後,是這樣儲存的:
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
以下為全部程式碼,除了疫情地圖,還包括了全國每日武漢肺炎確診資料的下載和視覺化。

# -*- coding: utf-8 -*-

import time
import json
import requests
from datetime import datetime
import numpy as np
import matplotlib
import matplotlib.figure
from matplotlib.font_manager import FontProperties
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.patches import Polygon
from matplotlib.collections import PatchCollection
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

plt.rcParams['font.sans-serif'] = ['FangSong']  # 設定預設字型
plt.rcParams['axes.unicode_minus'] = False  # 解決儲存影象時'-'顯示為方塊的問題

def catch_daily():
    """抓取每日確診和死亡資料"""
    
    url = 'https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_cn_day_counts&callback=&_=%d'%int(time.time()*1000)
    data = json.loads(requests.get(url=url).json()['data'])
    data.sort(key=lambda x:x['date'])
    
    date_list = list() # 日期
    confirm_list = list() # 確診
    suspect_list = list() # 疑似
    dead_list = list() # 死亡
    heal_list = list() # 治癒
    for item in data:
        month, day = item['date'].split('/')
        date_list.append(datetime.strptime('2020-%s-%s'%(month, day), '%Y-%m-%d'))
        confirm_list.append(int(item['confirm']))
        suspect_list.append(int(item['suspect']))
        dead_list.append(int(item['dead']))
        heal_list.append(int(item['heal']))
    
    return date_list, confirm_list, suspect_list, dead_list, heal_list

def catch_distribution():
    """抓取行政區域確診分佈資料"""
    
    data = {}
    url = 'https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5&callback=&_=%d'%int(time.time()*1000)
    for item in json.loads(requests.get(url=url).json()['data'])['areaTree'][0]['children']:
        if item['name'] not in data:
            data.update({item['name']:0})
        for city_data in item['children']:
            data[item['name']] += int(city_data['total']['confirm'])
    
    return data

def plot_daily():
    """繪製每日確診和死亡資料"""
    
    date_list, confirm_list, suspect_list, dead_list, heal_list = catch_daily() # 獲取資料
    
    plt.figure('2019-nCoV疫情統計圖表', facecolor='#f4f4f4', figsize=(10, 8))
    plt.title('2019-nCoV疫情曲線', fontsize=20)
    
    plt.plot(date_list, confirm_list, label='確診')
    plt.plot(date_list, suspect_list, label='疑似')
    plt.plot(date_list, dead_list, label='死亡')
    plt.plot(date_list, heal_list, label='治癒')
    
    plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%m-%d')) # 格式化時間軸標註
    plt.gcf().autofmt_xdate() # 優化標註(自動傾斜)
    plt.grid(linestyle=':') # 顯示網格
    plt.legend(loc='best') # 顯示圖例
    plt.savefig('2019-nCoV疫情曲線.png') # 儲存為檔案
    #plt.show()

def plot_distribution():
    """繪製行政區域確診分佈資料"""
    
    data = catch_distribution()
    
    font_14 = FontProperties(fname='res/simsun.ttf', size=14)
    font_11 = FontProperties(fname='res/simsun.ttf', size=11)
    
    width = 1600
    height = 800
    rect = [0.1, 0.12, 0.8, 0.8]
    lat_min = 0
    lat_max = 60
    lon_min = 77
    lon_max = 140
    
    '''全球等經緯投影模式使用以下設定,否則使用上面的對應設定
    width = 3000
    height = 1500
    rect = [0, 0, 1, 1]
    lat_min = -90
    lat_max = 90
    lon_min = 0
    lon_max = 360
    '''
    
    handles = [
            matplotlib.patches.Patch(color='#ffaa85', alpha=1, linewidth=0),
            matplotlib.patches.Patch(color='#ff7b69', alpha=1, linewidth=0),
            matplotlib.patches.Patch(color='#bf2121', alpha=1, linewidth=0),
            matplotlib.patches.Patch(color='#7f1818', alpha=1, linewidth=0),
]
    labels = [ '1-9人', '10-99人', '100-999人', '>1000人']
    
    provincePos = {
        "遼寧省":[121.7,40.9],
        "吉林省":[124.5,43.5],
        "黑龍江省":[125.6,46.5],
        "北京市":[116.0,39.9],
        "天津市":[117.0,38.7],
        "內蒙古自治區":[110.0,41.5],
        "寧夏回族自治區":[105.2,37.0],
        "山西省":[111.0,37.0],
        "河北省":[114.0,37.8],
        "山東省":[116.5,36.0],
        "河南省":[111.8,33.5],
        "陝西省":[107.5,33.5],
        "湖北省":[111.0,30.5],
        "江蘇省":[119.2,32.5],
        "安徽省":[115.5,31.8],
        "上海市":[121.0,31.0],
        "湖南省":[110.3,27.0],
        "江西省":[114.0,27.0],
        "浙江省":[118.8,28.5],
        "福建省":[116.2,25.5],
        "廣東省":[113.2,23.1],
        "臺灣省":[120.5,23.5],
        "海南省":[108.0,19.0],
        "廣西壯族自治區":[107.3,23.0],
        "重慶市":[106.5,29.5],
        "雲南省":[101.0,24.0],
        "貴州省":[106.0,26.5],
        "四川省":[102.0,30.5],
        "甘肅省":[103.0,35.0],
        "青海省":[95.0,35.0],
        "新疆維吾爾自治區":[85.5,42.5],
        "西藏自治區":[85.0,31.5],
        "香港特別行政區":[115.1,21.2],
        "澳門特別行政區":[112.5,21.2]
    }
    
    fig = matplotlib.figure.Figure()
    fig.set_size_inches(width/100, height/100) # 設定繪圖板尺寸
    axes = fig.add_axes(rect)
    
    # 蘭博託投影模式,區域性
    m = Basemap(projection='lcc', llcrnrlon=77, llcrnrlat=14, urcrnrlon=140, urcrnrlat=51, lat_1=33, lat_2=45, lon_0=100, ax=axes)
    
    # 蘭博託投影模式,全圖
    #m = Basemap(projection='lcc', llcrnrlon=80, llcrnrlat=0, urcrnrlon=140, urcrnrlat=51, lat_1=33, lat_2=45, lon_0=100, ax=axes)
    
    # 圓柱投影模式,區域性
    #m = Basemap(llcrnrlon=lon_min, urcrnrlon=lon_max, llcrnrlat=lat_min, urcrnrlat=lat_max, resolution='l', ax=axes)
    
    # 正射投影模式
    #m = Basemap(projection='ortho', lat_0=36, lon_0=102, resolution='l', ax=axes)
	
	# 全球等經緯投影模式,
    #m = Basemap(llcrnrlon=lon_min, urcrnrlon=lon_max, llcrnrlat=lat_min, urcrnrlat=lat_max, resolution='l', ax=axes)
    #m.etopo()
    
    m.readshapefile('res/china-shapefiles-master/china', 'province', drawbounds=True)
    m.readshapefile('res/china-shapefiles-master/china_nine_dotted_line', 'section', drawbounds=True)
    m.drawcoastlines(color='black') # 洲際線
    m.drawcountries(color='black')  # 國界線
    m.drawparallels(np.arange(lat_min,lat_max,10), labels=[1,0,0,0]) #畫經度線
    m.drawmeridians(np.arange(lon_min,lon_max,10), labels=[0,0,0,1]) #畫緯度線
    
    pset = set()
    for info, shape in zip(m.province_info, m.province):
        pname = info['OWNER'].strip('\x00')
        fcname = info['FCNAME'].strip('\x00')
        if pname != fcname: # 不繪製海島
            continue
        
        for key in data.keys():
            if key in pname:
                if data[key] == 0:
                    color = '#f0f0f0'
                elif data[key] < 10:
                    color = '#ffaa85'
                elif data[key] <100:
                    color = '#ff7b69'
                elif  data[key] < 1000:
                    color = '#bf2121'
                else:
                    color = '#7f1818'
                break
        
        poly = Polygon(shape, facecolor=color, edgecolor=color)
        axes.add_patch(poly)
        
        pos = provincePos[pname]
        text = pname.replace("自治區", "").replace("特別行政區", "").replace("壯族", "").replace("維吾爾", "").replace("回族", "").replace("省", "").replace("市", "")
        if text not in pset:
            x,  y = m(pos[0], pos[1])
            axes.text(x,  y, text, fontproperties=font_11, color='#00FFFF')
            pset.add(text)
    
    axes.legend(handles, labels, bbox_to_anchor=(0.5, -0.11), loc='lower center', ncol=4, prop=font_14)
    axes.set_title("2019-nCoV疫情地圖", fontproperties=font_14)
    FigureCanvasAgg(fig)
    fig.savefig('2019-nCoV疫情地圖.png')

if __name__ == '__main__':
    plot_daily()
    plot_distribution()
    

2019-nCoV疫情曲線:
在這裡插入圖片描述

2019-nCoV疫情地圖(蘭勃託投影):
在這裡插入圖片描述
2019-nCoV疫情地圖(圓柱投影):
在這裡插入圖片描述

2019-nCoV疫情地圖(正射投影):
在這裡插入圖片描述
2019-nCoV疫情地圖(全球等經緯投影模式):
在這裡插入圖片描述

既然都讀到這裡了,就給我——168號博主天元浪子投上5票吧,謝謝!

在這裡插入圖片描述

相關文章