最近這幾個(gè)月,新冠疫情牽動(dòng)了全國(guó)乃至全世界人民的心欣除。股市崩盤、經(jīng)濟(jì)發(fā)展開(kāi)倒車都已經(jīng)是小事情了挪略,最令人擔(dān)憂的是每天都有許多家庭在面對(duì)令人難以承受的別離历帚。非常感謝我們偉大的政府,感謝我們領(lǐng)導(dǎo)人的強(qiáng)大魄力杠娱,感謝我們國(guó)家對(duì)于生命的尊重挽牢,讓我們?cè)诮?jīng)歷了陣痛之后將局面掌控了下來(lái)。然而在這個(gè)全球經(jīng)濟(jì)趨向于一體化的時(shí)代摊求,誰(shuí)又能獨(dú)善其身呢禽拔?
病毒從哪里來(lái)我們不清楚,就不多說(shuō)了室叉,各國(guó)如何應(yīng)對(duì)疫情我也不想置評(píng)睹栖,畢竟就算我們操心操到著急上火也于事無(wú)補(bǔ),每個(gè)國(guó)家都有自己的判斷和想法茧痕。但是對(duì)于整個(gè)新冠疫情的發(fā)展趨勢(shì)野来,我們不得不關(guān)心。
之前的幾個(gè)月踪旷,我每天早上起床就是打開(kāi)丁香園曼氛、頭條等平臺(tái)的疫情地圖,看一下是否情況有好轉(zhuǎn)埃脏;到了最近搪锣,除了國(guó)內(nèi)的情況秋忙,又開(kāi)始關(guān)注海外疫情的發(fā)展彩掐。這些平臺(tái)做了很好的工具,能幫助我們迅速了解各種信息灰追。但是作為一個(gè)數(shù)據(jù)人堵幽,我們?cè)趺茨芡A粼谥淙欢恢渌匀坏膶哟文兀?/p>
今天我就教大家如何使用Python來(lái)將新冠疫情的發(fā)展趨勢(shì)可視化出來(lái)狗超。
一、數(shù)據(jù)收集
關(guān)于國(guó)內(nèi)疫情的數(shù)據(jù)朴下,最權(quán)威的來(lái)源當(dāng)然是衛(wèi)健委努咐。中國(guó)衛(wèi)健委以及各省市的衛(wèi)健委每天早上都會(huì)發(fā)布詳細(xì)的疫情通告,我們可以從這里獲取信息殴胧;至于國(guó)外渗稍,各國(guó)的CDC(疾控中心)都會(huì)發(fā)布類似的信息。我們可以將這些信息抓取并解析出來(lái)团滥。
下圖就是中國(guó)衛(wèi)健委在4月12日發(fā)布的疫情通報(bào)竿屹,這里邊有著相對(duì)固定的模板,我們可以使用正則表達(dá)式來(lái)將我們需要的數(shù)字解析出來(lái)灸姊。
但是問(wèn)題來(lái)了拱燃,先不說(shuō)全世界這么多國(guó)家,單單是中國(guó)三十多個(gè)省市自治區(qū)力惯,想要把數(shù)據(jù)都解析出來(lái)所需的時(shí)間成本就不是我們可以承受的碗誉。好在有一些令人尊敬的私人團(tuán)體替我們完成了這些事情,并且將數(shù)據(jù)免費(fèi)開(kāi)源給了大家父晶,開(kāi)源萬(wàn)歲哮缺。
那現(xiàn)在我們就可以節(jié)省下大量的時(shí)間了,我們只需直接訪問(wèn)這一接口獲取數(shù)據(jù)并將數(shù)據(jù)整理一下即可甲喝。
首先蝴蜓,我們最關(guān)注的自然是每天的確診及治愈信息。全國(guó)數(shù)據(jù)我們需要關(guān)注下邊這一個(gè)接口俺猿,我們需要在請(qǐng)求中附加國(guó)家茎匠、起始日期和是否包含港澳臺(tái)的信息。
另外押袍,我們需要申請(qǐng)一個(gè)API Key诵冒,并且附加在請(qǐng)求的Header之中。
各省市的數(shù)據(jù)接口也是類似谊惭,多說(shuō)無(wú)益汽馋,那我就直接上代碼了。
import requests
import datetime
import json
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
if __name__ == "__main__":
# 該接口需要我們?cè)趆eader中加一個(gè)token信息
header = {
'Token': 'xxxxx' # 輸入你申請(qǐng)的API Key
}
# 全國(guó)及各省份明細(xì)數(shù)據(jù)接口
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}'
# 該接口提供的數(shù)據(jù)從1月22日開(kāi)始圈盔,每次請(qǐng)求最多查詢10天的數(shù)據(jù)
# 因此我們寫(xiě)一個(gè)函數(shù)豹芯,基于我們關(guān)注的時(shí)間區(qū)間生成每次查詢的起始日期
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日以來(lái)每次查詢的起始日期
date_list = get_date_lists(start_date='2020-01-22')
# 獲取數(shù)據(jù)
result_total = []
result_detail = []
for start_date, end_date in date_list:
# 獲取全國(guó)數(shù)據(jù)
# 生成本次查詢的真實(shí)url
url_total = url_total_base.format(start_date, end_date)
# 請(qǐng)求接口,并用json模塊加載結(jié)果數(shù)據(jù)
res_total = json.loads(requests.get(url_total, headers=header).text)
# 判斷請(qǐng)求返回結(jié)果是否正常
if res_total['code'] == '90000':
# 判斷結(jié)果是否為空
if len(res_total['data']['region']['China']) == 0:
print(start_date + '~' + end_date + ' total data not ready')
else:
# 解析數(shù)據(jù)驱敲,這里因?yàn)橛卸鄬忧短滋福苯由驳匕讯鄬觡ey解析成一個(gè)字符串,后續(xù)再做處理
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')
# 獲取各省份數(shù)據(jù)
# 與上邊基本相同
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')
# 合并多次請(qǐng)求返回的結(jié)果
total_data = pd.concat(result_total, axis=0).reset_index()
detail_data = pd.concat(result_detail, axis=0).reset_index()
好众眨,到這里數(shù)據(jù)就獲取到了握牧。
二容诬、數(shù)據(jù)清洗
我們先看下數(shù)據(jù)長(zhǎng)什么樣。
可以看到沿腰,日期和指標(biāo)名稱是放在一個(gè)字段之中的览徒,并且用'.'分隔,各省市的明細(xì)數(shù)據(jù)也類似颂龙,我們需要將不同字段剝離出來(lái)习蓬。但是這樣的話指標(biāo)仍然是以行的形式存儲(chǔ),我們需要將不同的指標(biāo)放到不同的列里邊去措嵌。
# 將日期和指標(biāo)解析出來(lái)友雳,并將指標(biāo)分別放到不同的列
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()
# 將省份、日期和指標(biāo)解析出來(lái)铅匹,并將指標(biāo)分別放到不同的列
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()
全國(guó)和各省市的數(shù)據(jù)一樣押赊,都包含六個(gè)指標(biāo):每日新增確診、累計(jì)確診包斑、新增治愈流礁、累計(jì)治愈、新增死亡和累計(jì)死亡罗丰。我們還需要一個(gè)現(xiàn)有確診的字段神帅,這一指標(biāo)由累計(jì)確診減去累計(jì)治愈和累計(jì)死亡得來(lái)。
df_total_stats['current_confirmed'] = df_total_stats['confirmed'] - df_total_stats['deaths'] - df_total_stats['recovered']
df_total_stats.head()
df_detail_stats['current_confirmed'] = df_detail_stats['confirmed'] - df_detail_stats['deaths'] - df_detail_stats['recovered']
df_detail_stats
三萌抵、數(shù)據(jù)可視化
plotly
是Python
中一個(gè)非常強(qiáng)大的可視化庫(kù)找御,這次我們就采用它來(lái)完成本次的可視化任務(wù)。
全國(guó)疫情趨勢(shì)圖
首先绍填,我們想看到一個(gè)全國(guó)疫情的趨勢(shì)圖霎桅,而趨勢(shì)又可以分為新增趨勢(shì)和累計(jì)趨勢(shì)。
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='全國(guó)新冠疫情新增趨勢(shì)圖', 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 = '累計(jì)確診'
)
trace_recovered = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['recovered'],
name = '累計(jì)治愈'
)
trace_deaths = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['deaths'],
name = '累計(jì)死亡'
)
trace_cur_confirmed = go.Scatter(
x = df_total_stats['date'],
y = df_total_stats['current_confirmed'],
name = '現(xiàn)有確診'
)
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='全國(guó)新冠疫情累計(jì)趨勢(shì)圖', x=0.5, xanchor='center'))
fig.update_traces(line_shape = 'spline')
fig.show(config=config)
可以看到讨永,由于不同指標(biāo)的量級(jí)不同滔驶,所以有些指標(biāo)的趨勢(shì)看不大清楚。一個(gè)處理辦法是將y
坐標(biāo)軸轉(zhuǎn)換為對(duì)數(shù)坐標(biāo)軸卿闹。
我們將上邊的layout
配置中的yaxis
調(diào)整一下揭糕,去掉'type': 'log'
之前的注釋。這樣锻霎,所有指標(biāo)的趨勢(shì)我們就都可以看得一清二楚了著角。不過(guò)對(duì)數(shù)軸的圖在理解時(shí)一定要和線性軸區(qū)分開(kāi),這里同樣長(zhǎng)度的間隔在不同的數(shù)值區(qū)間代表的量級(jí)是不一樣的旋恼,線條變動(dòng)的幅度和真正數(shù)據(jù)量級(jí)的變化也不一樣吏口。我們可以這樣來(lái)理解:正常的線性坐標(biāo)軸看的是,?但是對(duì)數(shù)坐標(biāo)軸看的是。還有一個(gè)問(wèn)題是當(dāng)數(shù)據(jù)等于或小于0時(shí)锨侯,在圖中是體現(xiàn)不出來(lái)的,因?yàn)?img class="math-inline" src="https://math.jianshu.com/math?formula=log_%7B10%7Dx" alt="log_{10}x" mathimg="1">當(dāng)且僅當(dāng)時(shí)有解冬殃。具體選用哪種坐標(biāo)軸囚痴,需要結(jié)合實(shí)際情況來(lái)看。
疫情地圖
接下來(lái)我們想要看一下全國(guó)不同省市的疫情趨勢(shì)审葬,由于全國(guó)有幾十個(gè)省份深滚,如果每個(gè)省份都畫(huà)一個(gè)趨勢(shì)圖的話,未免也太過(guò)繁瑣涣觉。因此我們考慮以地圖熱點(diǎn)的形式來(lái)展示這些信息痴荐。
目前``plotly`并沒(méi)有提供對(duì)于中國(guó)各省市地圖的原生支持,但是它可以支持使用GeoJSON來(lái)配置我們自己的地圖官册。因此我們只需要將中國(guó)各省份的GeoJSON作為一個(gè)參數(shù)傳遞進(jìn)去即可生兆。阿里云有提供導(dǎo)出GeoJSON的免費(fèi)工具:http://datav.aliyun.com/tools/atlas。
我們發(fā)現(xiàn)在這個(gè)數(shù)據(jù)中膝宁,有一個(gè)properties.name
字段是省份的名稱鸦难,這和我們獲取到的全拼的省份名稱不一樣,因此我們需要做一個(gè)映射员淫。
province_maper = {
'Anhui' : '安徽省',
'Beijing': '北京市',
'Chongqing': '重慶市',
'Fujian': '福建省',
'Gansu': '甘肅省',
'Guangdong': '廣東省',
'Guangxi': '廣西壯族自治區(qū)',
'Guizhou': '貴州省',
'Hainan': '海南省',
'Hebei': '河北省',
'Heilongjiang': '黑龍江省',
'Henan': '河南省',
'Hong Kong': '香港特別行政區(qū)',
'Hubei': '湖北省',
'Hunan': '湖南省',
'Jiangsu': '江蘇省',
'Jiangxi': '江西省',
'Jilin': '吉林省',
'Liaoning': '遼寧省',
'Macao': '澳門特別行政區(qū)',
'Neimenggu': '內(nèi)蒙古自治區(qū)',
'Ningxia': '寧夏省',
'Qinghai': '青海省',
'Shaanxi': '陜西省',
'Shandong': '山東省',
'Shanghai': '上海市',
'Shanxi': '山西省',
'Sichuan': '四川省',
'Taiwan': '臺(tái)灣省',
'Tianjin': '天津市',
'Xinjiang': '新疆維吾爾自治區(qū)',
'Xizang': '西藏自治區(qū)',
'Yunnan': '云南省',
'Zhejiang': '浙江省'
}
df_detail_stats['province_name'] = df_detail_stats['province'].map(lambda x: province_maper[x])
然后我們分別繪制現(xiàn)有確診地圖和累計(jì)確診地圖合蔽,并且增加動(dòng)畫(huà)。
import plotly.express as px
geojson_str = open('全國(guó).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)']
]
# 繪制現(xiàn)有確診地圖
fig = px.choropleth_mapbox(
df_detail_stats.rename(
{'date': '日期', 'province_name': '地區(qū)', 'current_confirmed': '現(xiàn)有確診'},
axis=1
),
geojson=geojson,
locations="地區(qū)",
featureidkey="properties.name",
mapbox_style='white-bg',
zoom=3,
center={'lat':37, 'lon':102},
color='現(xiàn)有確診',
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='中國(guó)COVID-19現(xiàn)有確診地圖', title_x=0.5)
fig.write_html('現(xiàn)有確診.html', config=config)
# 繪制累計(jì)確診地圖
fig = px.choropleth_mapbox(
df_detail_stats.rename(
{'date': '日期', 'province_name': '地區(qū)', 'confirmed': '累計(jì)確診'},
axis=1
),
geojson=geojson,
locations="地區(qū)",
featureidkey="properties.name",
mapbox_style='white-bg',
zoom=3,
center={'lat':37, 'lon':102},
color='累計(jì)確診',
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='中國(guó)COVID-19累計(jì)確診地圖', title_x=0.5)
fig.write_html('累計(jì)確診.html', config=config)
然后我們看一下效果介返。
當(dāng)然拴事,我們還可以使用plotly來(lái)繪制全球的疫情變化趨勢(shì),這個(gè)其實(shí)比繪制中國(guó)的地圖更加簡(jiǎn)單圣蝎,因?yàn)閜lotly可以直接支持全球國(guó)家級(jí)的地圖刃宵,在此就不重復(fù)勞動(dòng)了。大家可以自己嘗試一下徘公,作為一個(gè)練習(xí)组去。看一百遍不如自己親自實(shí)踐一遍步淹。
大家有任何問(wèn)題从隆,都可以在下方留言,或者關(guān)注后私信溝通缭裆。