前言
其實這篇博客一開始在CSDN發表,奈何我修改一下以后再發表,CSDN就說我違反國家法定法規。。。不允許我發表了,經查詢,CSDN的客服說疫情期間所有跟疫情有關的博客都不允許發布。。
作為一個剛開始寫博客的小萌新,實在是收到了一點打擊,轉換陣地繼續發表,一下正文開始:
差不多兩個月前,剛好學校有一個社會實踐的任務,又是疫情期間,又看到網上已經有各路大神位COVID-19的疫情數據進行統計分析和建模。自己又是學習計算機的一名學生,不想應付了事,就趁着這個機會,決定也做一個類似的數據可視化,效果如下圖
由於我想要實現的是可交互的圖表,之前接觸的matplotlib並不能滿足我的這個需求,而pyechart這個庫雖然比較簡單方便,但是可定制化程度相對不高,風格也不是我喜歡的風格,所以尋尋覓覓,看上了Altair這個用於python可視化的庫,但是網上關於這個庫的使用的相關文章少之又少,所以自己去Altair
官網查看了相關文檔和案例,終於完成了自己的目標,考慮到目前關於Altair的快速上手入門的教程基本沒有,所以完成了這個項目后決定分享一下,方便后來者,僅僅是分享一下自己的心得,一點經驗。如有不足,歡迎指出。
這個博客並不是一個給從零開始詳細講述關於python、numpy、pandas等的教程,而是為了達成目的的一個實踐性心得,但是會盡量將一切講得盡量詳細,讓初學者也可以跟着這個教程做出想要的美觀的圖表
COVID-19 疫情數據
數據從國家衛健委國家衛健委進行爬取,由於本次只專注於數據可視化,並且爬蟲代碼的有效性較短,如果國家衛健委修改了接口,那么就得重新分析,重構代碼(本人項目期間國家衛健委的接口就修改了兩次),所以此次不會進行爬蟲的相關的講解,我會直接給出爬取后序列化的csv文件。
Altair數據可視化
本次總共繪制以下幾個幾個圖表:
- COVID-19 中國 累計確診人數和累計疑似人數折線圖
- COVID-19 中國 每日新增確診人數和每日新增疑似確診人數折線圖
- COVID-19 中國 每日新增確診增長率折線圖
- COVID-19 中國 死亡率變化曲線圖
- COVID-19 國內最嚴重省份(除湖北)確診人數柱狀圖
- COVID-19 中國 前十嚴重省份累計確診人數折線圖
- COVID-19 世界累計確診變化曲線
- COVID-19 海外各國(地區) 確診人數柱狀圖
- COVID-19 全球 前五嚴重國家累計確診人數折線圖
- COVID-19 世界各國(地區) 確診人數熱度圖
繪制圖表的過程中我們用到的庫有:
import os
import re
import json,requests
import geopandas
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import altair as alt
import datetime
import js2py
使用 jupyter lab(jupyter notebook)開發
預備知識
我們用pandas的DataFrame結構讀取並保存我們csv文件中的數據,關於pandas不會過多講解,但是有涉及到的都會力求讓大家理解。十個圖表大致可以分為折線圖、柱狀圖、地理地圖三大類,
在Altair中,altair.Chart (注:上面我們import altair 時, 是 import altair as alt,所以我們可以使用alt來指代altair,下文不再贅述)是基本的圖表對象,我們可以使用mark_line()函數來繪制折線圖,使用mark_point來繪制散點圖,mark_bar來繪制柱狀圖,mark_geoshape()繪制地理地圖。
繪制圖表時需要有數據我們才能繪制圖表,我們得需要把數據傳遞給alt.Char,根據Altair官方文檔,所有最高層級的chart對象(包括:Chart, LayerChart, and VConcatChart, HConcatChart, RepeatChart, FacetChart,但是我們此次只需要用到Chart),都接受一個數據集作為其第一個參數,數據集可以是以下幾種類型的:
- pandas 的 DataFrame 類型。
- Data 或者跟 Data 相關的對象。 (例如 UrlData, InlineData, NamedData)
- 一個指向 json 或者 csv 格式的url字符串,或者支持 geo_interface 接口的對象(例如 geopandas 的 GeoDataFrame,shapely 的 Geometries,GeoJson)
我們本次只會使用到pandas 的DataFrame,如果你此前對DataFrame並不熟悉甚至不了解也沒關系,在這篇博客中我會講下我們用到的地方。 本人對pandas並不是十分熟悉,如果想要深入了解,請自行查找相關資料。
如果你已經熟悉DataFrame下面可以跳過
DataFrame是一種形如:
時間 | 確診人數 | 疑似人數 | 死亡人數 | |
---|---|---|---|---|
1 | 2020-02-19 | 200 | 400 | 10 |
2 | 2020-02-20 | 300 | 600 | 20 |
的表格型數據結構,跟我們的excel表格類似,每一行都有單獨的索引。
DataFrame有兩種格式,一種是寬型:
時間 | 確診人數 | 疑似人數 | 死亡人數 | |
---|---|---|---|---|
0 | 2020-02-19 | 200 | 400 | 10 |
1 | 2020-02-20 | 300 | 600 | 20 |
這種就是寬型DataFrame,寬型DaraFrame每有一個獨立變量值就會產生一行。元數據被記錄在行和列標簽中。
在這里每一行對應一個時間,每一個觀察值(確診人數、疑似人數、死亡人數)存儲在這一行的一個列標簽中。
還有一種是長型:
時間 | 類型 | 人數 | |
---|---|---|---|
0 | 2020-02-19 | 確診 | 200 |
1 | 2020-02-19 | 疑似 | 400 |
2 | 2020-02-19 | 死亡 | 10 |
3 | 2020-02-20 | 確診 | 300 |
4 | 2020-02-20 | 疑似 | 600 |
5 | 2020-02-20 | 死亡 | 20 |
長型DataFrame每有一個觀測值就有一行,元數據被作為值記錄在表中。
在這里,每一行只包含一個觀測值,還有伴隨這個觀察值的元數據。(在這里觀察值是人數,時間和類型是伴隨着這個觀察值的元數據),最重要的是,列和索引標簽,不再包含任何有用的索引信息。(簡單來說就是寬型數據知道行列索引就可以知道對應元數據的還有確切意義,例如 行索引 0 列索引 確診人數,我們就可以知道 2020-02-19 確診人數是200,但是觀察值。行索引 0 列索引 無論是時間還是類型,得到的都是無用的元數據,例如 行索引 0 列索引時間 得到的值是 2020-02-19,行索引 1 列索引時間 得到的值是 2020-02-19,並不唯一,並沒有確定的意義,僅僅是一個值)
Altair在使用長型結構的DataFrame時可以有更好地表現,推薦使用長型,所以后面我們構建傳遞給Chart的DataFrame時都是構建長型的。
然后繪制圖時,我們還需要告訴Chart哪些數據是作為x軸,哪些是作為y軸,在Altair中我們可以通過調用Chart對象的encode方法並往里面傳遞參數來對我們的x軸、y軸等進行設置。例如如下一個DataFrame:
時間 | 確診人數 | |
---|---|---|
0 | 2020-02-19 | 200 |
3 | 2020-02-20 | 300 |
由於Chart還支持鏈式調用,所以我們可以使用
alt.Chart(data).mark_line().encode(x='時間',y='確診人數')
來畫出以時間為x軸,確診人數為y軸的折線圖,展現出確診人數隨時間變化的折線圖。
COVID-19 中國 累計確診人數和累計疑似人數折線圖
為了繪制出COVID-19 中國 累計確診人數和累計疑似人數折線圖,我們需要中國疫情發生以來每日的疫情數據,我們所要用到的數據都在上面下載的資源里的 中國每日歷史數據(截止到 2020-XX-XX).csv (2020-XX-XX可能會根據你下載的時間而有所不同)文件中,現假設你下載了數據集並且將其解壓在了D:\ 盤下,所以你D:\ 下應該會有 :
- 2020-1-1-2020-1-23湖北遷出前十城市.csv
- 中國每日歷史數據(截止到 2020-04-23).csv
- 中國省份總數據.csv
- 修復后的世界地圖.csv
- 全球國家每日歷史數據(截止至2020-04-23).json
file_name = 'D:\\中國每日歷史數據(截止到 2020-XX-XX).csv'
chinaDayHisDf = pd.read_csv(file_name)
# 可以使用 chinaDayHisDf .head(number) number 是你想查看的前幾行行數,若置空則默認五行
# 可以使用 chinaDayHisDf.info() 查看這個DataFrame的相關信息
可以看到我們這個chinaDayHisDf的結構是:
新增死亡 | 新增疑似 | 新增確診 | 日期 | 累計死亡 | 累計疑似 | 累計確診 |
---|---|---|---|---|---|---|
1 | 0 | 4 | 1月16日 | 2 | 0 | 45 |
0 | 0 | 17 | 1月17日 | 2 | 0 | 62 |
1 | 0 | 59 | 1月18日 | 3 | 0 | 121 |
1 | 0 | 77 | 1月19日 | 4 | 0 | 198 |
2 | 27 | 77 | 1月20日 | 6 | 54 | 291 |
3 | 26 | 149 | 1月21日 | 9 | 37 | 440 |
8 | 257 | 131 | 1月22日 | 17 | 393 | 571 |
8 | 680 | 259 | 1月23日 | 25 | 1072 | 830 |
16 | 1118 | 444 | 1月24日 | 41 | 1965 | 1287 |
15 | 1309 | 688 | 1月25日 | 56 | 2684 | 1975 |
24 | 3806 | 769 | 1月26日 | 80 | 5794 | 2744 |
26 | 2077 | 1769 | 1月27日 | 106 | 6973 | 4535 |
26 | 3248 | 1459 | 1月28日 | 132 | 9239 | 5597 |
38 | 4148 | 1741 | 1月29日 | 170 | 12167 | 7736 |
43 | 4812 | 1985 | 1月30日 | 213 | 15238 | 9720 |
46 | 5019 | 2104 | 1月31日 | 259 | 17988 | 11821 |
... | ... | ... | ... | ... | ... | ... |
我們首先簡單地繪制出一天累計確診人數的增長曲線,我們用到的數據有日期,累計確診,以日期為x軸,累計確診人數為y軸,我們先來繪制最簡單的累計確診人數隨日期變化的曲線:
# 若使用的是jupyter lab 或類似的環境並且已經設定是,直接在代碼模式下 輸入chart運行便可查看結果
# 若是其他環境,則需要 chart.save('chart.html') 名字可隨意, 生成html文件查看
chart = alt.Chart(chinaDayHisDF).mark_line().encode(
x='日期',
y='累計確診'
)
結果如下面所示:
觀察x軸可以看出日期的排序並不正確,這是由於我們日期所存儲的數據是字符串類型的,所以自動按照了字符串的排序方法進行了排序,所以導致了顯示的不正確,為了顯示正確,我們需要將日期轉換成便於操作的TimeStamp類型。觀察日期的格式可以發現,我們可以用月和日提取出對應的月份,和日期,這需要用到正則表達式,此時不細講正則表達式,所以僅貼出代碼供參考:
def changeDate(dateStr):
# 定義匹配規則,將'月'前面的數字提取並划分到group[0]
# 將'日'前面的數字提取並划分到group[1]
rex = r'([\d]+)月([\d]+)日'
# 對dateStr進行匹配,並獲取對應的月 日
m = re.match(rex,dateStr)
mon = m.groups()[0]
day = m.groups()[1]
# 構建初始化TimrStamp所需要的字符串
str = '2020-%s-%s' % (mon,day)
date = pd.to_datetime(str)
return date
有了這個函數,我們需要將這個函數應用到chinaDayHisDF每一行中的['日期']列,如果不熟悉DataFrame的朋友可能會直接用循環來迭代,對每一個值進行修改。
但是DataFrame為我們提供了簡單的apply函數,可以將一個函數一你個用到每一行,或者每一列中。具體可查閱相應文檔。
chinaDayHisDF['日期'] = chinaDayHisDF['日期'].apply(changeDate)
此時再繪制一次我們的圖表,可以發現此時已經正常顯示了,並且由於我們的['日期']是TImeStamp類型,所以Altair自動幫我們將x軸以日期的形式顯示。
接下來我們需要對x軸,y軸進行自定義修改,例如修改x軸標題,標題顏色,字體大小,要達到這個目的,我們可以調用Chart.encode()函數對這些進行設置。(Chart對象可以鏈式調用,Chart().mark_line() 返回的還是Chart對象, 所以可以Chart(chinaDayHisDF).mark_line().encode()來進行調用)
alt.Chart(chinaDayHisDF).mark_line().encode(
x=alt.X('日期'),
# 使用axis參數對軸進行定義,此處我們傳遞一個我們自定義的alt.Axis對象進去
y=alt.X('累計確診',axis=alt.Axis(title='累計確診人數',titleFontSize=15,titleColor='red'))
)
但我們現在的DataFrame格式是寬型的,同時繪制多條折線(例如同時繪制累計確診人數,累計疑似確診人數,累計死亡人數)十分不便,根據官方文檔的建議,我們應該使用長型的數據格式,效率更高並且更方便。
所以我們需要一個['symbol']列來區分不同的折線,通過['count']列存儲元數據,即存儲對應的類型的人數。
構建對應的長型DataFrame代碼如下:
# 創建可視化確診和疑似確診 Altair庫需要的數據格式
data = {
'date':chinaDayHisDF['日期'],
'symbol':'累計確診',
'count':chinaDayHisDF['累計確診']
}
chinaConfAndSusDf = pd.DataFrame(data)
data = {
'date':chinaDayHisDF['日期'],
'symbol':'累計疑似',
'count':chinaDayHisDF['累計疑似']
}
chinaConfAndSusDf = chinaConfAndSusDf.append(pd.DataFrame(data))
data = {
'date':chinaDayHisDF['日期'],
'symbol':'累計死亡',
'count':chinaDayHisDF['累計死亡']
}
chinaConfAndSusDf = chinaConfAndSusDf.append(pd.DataFrame(data))
chinaConfAndSusDf 格式如下:
date | symbol | count |
---|---|---|
2020-01-16 | 累計確診 | 45 |
2020-01-17 | 累計確診 | 62 |
2020-01-18 | 累計確診 | 121 |
2020-01-19 | 累計確診 | 198 |
2020-01-20 | 累計確診 | 291 |
2020-01-21 | 累計確診 | 440 |
2020-01-22 | 累計確診 | 571 |
2020-01-23 | 累計確診 | 830 |
2020-01-24 | 累計確診 | 1287 |
2020-01-25 | 累計確診 | 1975 |
2020-01-26 | 累計確診 | 2744 |
... | ... | ... |
此時若是直接繪制折線圖可以發現並不是我們所預期的效果,這是因為只告訴了Chart要顯示的x軸數據和y軸數據,但是此時有三個類型的需要同時繪制,即三組y軸的數據,並不能區分出來,所以需要我們告訴Chart對象到底用哪一列進行區分。我們可以往Chart.encode()傳入color參數,用不同的顏色區分不同的折線。
alt.Chart(chinaConfAndSusDf).mark_line().encode(
x=alt.X('date',axis=alt.Axis(title='日期')),
y=alt.Y('count:Q',axis=alt.Axis(title='累計確診人數',titleFontSize=15,titleColor='red')),
color='symbol'
)
此時我們對累計死亡人數的折線顏色不滿意,希望用黑色來繪制累計死亡人數的折線,並且自動生成的圖例標題是默認的’symbol‘,我們需要對其進行設置,並且默認生成的圖表大小過小,我們可以通過Chart.properties對圖表的長寬進行設置
color = alt.Color('symbol:N',legend=alt.Legend(title='圖例'),
scale=alt.Scale(domain=['累計確診','累計疑似','累計死亡'], range=['#FF4433','#FF9933','#112233']))
alt.Chart(chinaConfAndSusDf).mark_line().encode(
x=alt.X('date',axis=alt.Axis(title='日期')),
y=alt.Y('count:Q',axis=alt.Axis(title='累計確診人數',titleFontSize=15,titleColor='red')),
color=color
).properties(
width=1200, height=350
)
但此時我們的圖表還僅僅是靜態的圖表,並沒有體現Altair庫的優勢,我們所需要的是一個動態的可交互的可視化圖表。我們需要實現的功能是有一條豎線指示我們當前所在的x軸日期點,並且這個豎線會隨我們的鼠標移動而在x軸上移動,並且在豎線於折線的交點上顯示對應的人數。並且折線太多時會影響我們查看數據,所以我們還需要一個篩選功能,當我們點擊對應圖例時,只會顯示當前圖例對應的折線,其它折線灰顯。
為了實現以上功能,我們需要介紹Altair的一個概念,selection和condition,selection是一種選擇器,可以理解為根據不同的情況做出不同的反應,selection必須被添加在圖上才可以使用,其中做出的反應由condition確定,某些屬性的值可以根據不同的condition做出不同的響應或許我說得不清楚,有意向的朋友可以移步Altair官網查看文檔我這里僅僅以解決上述問題入手說明一下
由於我們需要根據我們的鼠標做出反應,所以需要一個selection來跟我們鼠標移動這個事件綁定在一起,同時我們的rule是根據鼠標移動時而在x軸上移動,所以我們這個selection select的是date,根據不同的date,rule有不同的反應。所以首先我們得先把這個selection添加到我們的圖上,但是我們的圖是折現圖,selection 好像只能綁定點圖(官網沒查閱到相關信息,但是自己有測試),所以我們需要一個點圖來給綁定我們我的selection,或者用我們的線圖中的mark_point方法來畫出點圖,在這里直接用我們的line線圖的mark_point()方法產生一個point點圖,selection通過這份點圖 就可以找到對應的X值,但是point和line互相重疊,由於我們的主圖是線圖line,所以point圖的點都需要設置透明,不可見。rule直線顯示我們的當前位置和跟着我們的鼠標在x軸方向上左右移動,我們的rule通過chart的transform_filter來達到這樣的效果,transform_filter可以理解為一個“過濾器”,根據selection,只在當前select的值做出反應。同時,我們的點圖point應當在rule所在的交點顯示出點,但是在其他應該都透明不可見,所以我們的point圖應該根據condition來決定是否透明,此處是,當前select不透明,其他地方透明。所以point的opacity (透明度)應該設置為opacity=alt.condition(nearest, alt.value(1), alt.value(0)),1 不透明,0透明。同時我們需要用chart.mark_text()來顯示人數值,這個同理,設置文本的text為count的值,同時其他未被select的地方都顯示 ' '空白字符就行了
同時由於現在我們有了一個text的chat,一個line的chart,一個point的chart,還有一個rule的chart 總共有多張圖要一起畫在一張圖上,所以我們需要使用alt.layer(line,point,text)來一起顯示
這里可能有點難理解,如果本人講得不夠清楚,可以前往官網查閱
nearest = alt.selection_multi(name='nearest', nearest=True, on='mouseover',fields=['date'],empty='none')
line = alt.Chart(chinaConfAndSusDf,width=800,height=300).mark_line().encode(
x=alt.X('date', axis=alt.Axis(title='日期',titleFontSize=16,titlePadding=20,grid=True,labelFontSize=14)),
y=alt.Y('count', axis=alt.Axis( title='人數',titleFontSize=16,titleColor='#FF9988',titlePadding=20)),
color=alt.Color('symbol',legend=alt.Legend(title=None,titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=16,rowPadding=10,labelOpacity=0.5,offset=60),
scale=alt.Scale(domain=['累計確診','累計疑似'], range=['#FF4433','#FF9933']))
)
points = line.mark_point(strokeWidth=5).encode(
x=alt.X('date', axis=alt.Axis(title='日期',titleFontSize=16,titlePadding=20,grid=True,labelFontSize=14)),
y=alt.Y('count', axis=alt.Axis( title='人數',titleFontSize=16,titleColor='#FF9988',titlePadding=20)),
opacity=alt.condition(nearest, alt.value(1), alt.value(0)),
).add_selection(
nearest
)
text = line.mark_text(align='left', dx=5, dy=-15,fontSize=15, fontWeight='bold').encode(
text=alt.condition(nearest, 'count', alt.value(' ')),
)
rules = alt.Chart(chinaConfAndSusDf).mark_rule(strokeWidth=2,color='gray',opacity=0.2).encode(
x=alt.X('date')
).transform_filter(
nearest
)
alt.layer(line,rules,points,text)
細心的讀者可能發現,移動鼠標的時候,rule線很不跟手,有一種很卡頓的感覺,這其實是為什么呢?其實這個原因一開始我也沒有發現,以為官方示例不夠好,官方的示例是即使有了線圖,也不會有現有的線圖畫點圖,而是新畫一個點圖,這個點圖只有x軸,y軸不設置,即默認都是0,這是為什么呢?看下我們的selection,它的nearest=True,,表明它會選取離我們最近的點,這樣問題就來了,如果我們用line畫出點圖,由於我們需要顯示點在曲線上,即這個點必須有x坐標,y坐標,並且在這個點圖上添加selection,那么這個最近就會同時以x,y計算,得到的不是離我們最近的x軸的值,所以可能會出現跳躍現象,所以為了平滑,我們需要修改一下,不用line直接畫點圖,而且重新用數據構建一份點圖,這份點圖只有x軸,y的值系統選取,即是一條直線,這樣就避免了跳躍,圖也會流暢。最后我們只需要一個綁定legend(圖例)的selection就行了,當select時,曲線顯示,未select的曲線灰顯,並且文本透明。不一一細說,好好理解selection condition之后都大同小異
# Create a selection that chooses the nearest point & selects based on x-value
# nearest 是一個selection,字面意思就是選擇器,可以給它一個名字方便后續使用,也可以指定類型type,是單選擇器還是多選擇器
# 選擇器可以看作一種選擇判斷、一種根據情況做出不同顯示的類
# nearst=True表明選擇最近的點,on是指定綁定什么事件,在這里是mouseover即鼠標移動,fields指定綁定的字段,在這里是指定X軸,即根據X軸取值
# 在這里,即是根據不同的date值有不同的情況
nearest = alt.selection(name='nearest',type='single', nearest=True, on='mouseover',
fields=['date'],empty='none')
# selCur 是一個selection,字面意思就是選擇器,可以給它一個名字方便后續使用,
# fields指定綁定的字段,bind只當這個選擇器綁定在那里
# 在這里就是這個選擇器綁定在legend(圖例)上,同時根據字段‘symbol’進行區分
# 在這里,即是根據不同的symbol值有不同的情況
selCur = alt.selection_multi(name='selCur',fields=['symbol'],bind='legend')
# color類可以為我們的曲線,圖例設置不同的顏色,區分曲線,
# 可以用一個legend類構造color的legend,各種屬性名字很好地表現出了它的作用
# scale屬性可以用一個Scale類來實例化,domain指定了顏色作用的“域”,可以根據domain來區分不同的曲線,range可以分別設置顏色
# 在這里就是累計確診用#FF4433顏色顯示,累計疑似用#FF9933顯示
color = alt.Color('symbol:N',legend=alt.Legend(title=None,titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=16,rowPadding=10,labelOpacity=0.5,offset=60),
scale=alt.Scale(domain=['累計確診','累計疑似','累計死亡'], range=['#FF4433','#FF9933','#112233']))
# alt.Chart(data)畫出圖,這里data是我們需要繪制的數據,mark_line方法表明這是一個線圖
# encode方法里面面的x屬性是設置x軸alt.X是X軸的類,可以指定用那些列作為我們的x軸,axis是對坐標軸進行設計,同理,X中的date是列明,指定date做X軸
# 由於我們的是TimeStamp類型,是一個日期格式,所以可以用monthdata來只顯示月日
# The basic line
line = alt.Chart(chinaConfAndSusDf).mark_line().encode(
x=alt.X('monthdate(date):T', axis=alt.Axis(title='日期',titleFontSize=16,titlePadding=20,grid=True,labelFontSize=14)),
y=alt.Y('count', axis=alt.Axis( title='人數',titleFontSize=16,titleColor='#FF9988',titlePadding=20)),
# condition 跟selection配合使用,第一個值為selection,第二個值是True時的值,即被選中時的值,第三個Fasle值
color=alt.condition(selCur,color,alt.value('lightgray')),
strokeWidth=alt.condition(selCur,alt.value(5),alt.value(3))
)
# Transparent selectors across the chart. This is what tells us
# the x-value of the cursor
# 選擇器還需要掛載到我們的圖上才行,不然是不起作用的
selectors = alt.Chart(chinaConfAndSusDf).mark_point(strokeWidth=3).encode(
x=alt.X('monthdate(date):T'),
opacity=alt.value(0),
).add_selection(
nearest
)
# Draw points on the line, and highlight based on selection
points = line.mark_point(strokeWidth=5).encode(
opacity=alt.condition(nearest, alt.value(1), alt.value(0)),
size=alt.condition(selCur,alt.value(3),alt.value(0)),
).add_selection(
selCur
)
# Draw text labels near the points, and highlight based on selection
text = line.mark_text(align='left', dx=5, dy=-15,fontSize=15, fontWeight='bold').encode(
text=alt.condition(nearest, 'count', alt.value(' ')),
opacity=alt.condition(selCur, alt.value(1), alt.value(0)),
)
# Draw a rule at the location of the selection
rules = alt.Chart(chinaConfAndSusDf).mark_rule(strokeWidth=2,color='gray',opacity=0.2).encode(
x=alt.X('monthdate(date):T')
).transform_filter(
nearest
)
# Put the five layers into a chart and bind the data
alt.layer(
line, selectors, points, rules, text
).properties(
width=1200, height=350,
title= {
"text": "COVID-19 中國 累計確診人數和累計疑似人數折線圖",
"fontSize": 20,
'offset':20
}
).configure_view(stroke=None)
最終結果圖像如下:
COVID-19 中國 每日新增確診人數和每日新增疑似確診人數折線圖
與COVID-19 中國 累計確診人數和累計疑似人數折線圖類似,我們只需要構建對應的每日新增確診人數,每日新增確診疑似人數,死亡人數即可,在此不再贅述。
最終結果如圖所示:
COVID-19 中國 每日新增確診增長率折線圖
構建COVID-19 中國 每日新增確診增長率折線圖與上述類似,不過我們並沒有直接的每日新增確診增長率的數據,所以我們需要從我們現有的數據構建出我們需要的數據。
我們先看一下我們的數據:
新增死亡 | 新增疑似 | 新增確診 | 日期 | 累計死亡 | 累計疑似 | 累計確診 |
---|---|---|---|---|---|---|
1 | 0 | 4 | 1月16日 | 2 | 0 | 45 |
0 | 0 | 17 | 1月17日 | 2 | 0 | 62 |
1 | 0 | 59 | 1月18日 | 3 | 0 | 121 |
1 | 0 | 77 | 1月19日 | 4 | 0 | 198 |
2 | 27 | 77 | 1月20日 | 6 | 54 | 291 |
3 | 26 | 149 | 1月21日 | 9 | 37 | 440 |
8 | 257 | 131 | 1月22日 | 17 | 393 | 571 |
8 | 680 | 259 | 1月23日 | 25 | 1072 | 830 |
16 | 1118 | 444 | 1月24日 | 41 | 1965 | 1287 |
15 | 1309 | 688 | 1月25日 | 56 | 2684 | 1975 |
24 | 3806 | 769 | 1月26日 | 80 | 5794 | 2744 |
26 | 2077 | 1769 | 1月27日 | 106 | 6973 | 4535 |
26 | 3248 | 1459 | 1月28日 | 132 | 9239 | 5597 |
38 | 4148 | 1741 | 1月29日 | 170 | 12167 | 7736 |
43 | 4812 | 1985 | 1月30日 | 213 | 15238 | 9720 |
46 | 5019 | 2104 | 1月31日 | 259 | 17988 | 11821 |
... | ... | ... | ... | ... | ... | ... |
為了利於操作,直接在DataFrame上新增一列昨天的累計確認人數,然后我們今日累計確診列減去昨天累計確診列再除昨日確診人數就可以計算出COVID-19每日確診ren's的增長率。DateFrame.shift()函數可以講某一列往后偏移一定量,DateFrame.shift()就是偏移一位例如:
數據本來是
那么偏移后的數據是
其中NaN是not a number 的意思,因為本來一前面就沒有其他數據了,所以偏移后到底是什么呢?numpy用了NaN來指明NaN會在后續我們對數據的操作中引發很多異常,所以我們要進行處理,很自然地,我們用0來替代NaN。
yesDayConfirm = chinaDayListDF.confirm.shift()
yesDayConfirm[0] = 0
chinaDayListDF['yesDayConfirm'] = yesDayConfirm
# 使用round保留兩位小數
chinaDayListDF['confirmAddRate'] = round((chinaDayListDF['confirm'] - chinaDayListDF['yesDayConfirm'])/ chinaDayListDF['yesDayConfirm']*100,2)
# 由於上一步存在 0 除上其他數 或者 其他數除0地可能,
# 所以可能會產生np.inf,-np.inf等,所以我們需要講這些都替換為0
chinaDayListDF = chinaDayListDF.replace([np.inf,-np.inf,np.NaN],0)
操作過后的DataFrame應該是以下格式:
新增死亡 | 新增疑似 | 新增確診 | 日期 | 累計死亡 | 累計疑似 | 累計確診 | yesDayConfirm | confirmAddRate | |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 4 | 2020-01-16 | 2 | 0 | 45 | 0.0 | inf |
1 | 0 | 0 | 17 | 2020-01-17 | 2 | 0 | 62 | 45.0 | 37.78 |
2 | 1 | 0 | 59 | 2020-01-18 | 3 | 0 | 121 | 62.0 | 95.16 |
3 | 1 | 0 | 77 | 2020-01-19 | 4 | 0 | 198 | 121.0 | 63.64 |
4 | 2 | 27 | 77 | 2020-01-20 | 6 | 54 | 291 | 198.0 | 38.89 |
5 | 3 | 26 | 149 | 2020-01-21 | 9 | 37 | 440 | 291.0 | 51.2 |
6 | 8 | 257 | 131 | 2020-01-22 | 17 | 393 | 571 | 440.0 | 29.77 |
7 | 8 | 680 | 259 | 2020-01-23 | 25 | 1072 | 830 | 571.0 | 45.36 |
8 | 16 | 1118 | 444 | 2020-01-24 | 41 | 1965 | 1287 | 830.0 | 53.49 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
剩下的操作與之前類似,不再贅述。
最終結果如下圖所示:
求增長人數可以用時間序列的一次差分來繪制出,求增長率可以對時間序列進行對數化處理再進行一次差分操作繪制出,這涉及到時間序列的處理,僅提一句
COVID-19 中國 死亡率變化曲線圖
與上述操作類似,不再贅述
最終結果如下圖所示:
COVID-19 國內最嚴重省份(除湖北)確診人數柱狀圖
對於我們要繪制的省內的數據,我們需要讀取我上傳的DataSet里面‘中國省份總數據.csv’
# 路徑根據自己的路徑進行修改
file_name = 'D:\\中國省份總數據.csv'
chinaProviceTotalDataDf = pd.read_csv(file_name)
由於這個DataFrame太大,我們無法在這里顯示出來,各位讀者有興趣的可以自己去看看
我們目前有的是中國全部省份的每天的歷史數據,而我們繪制的COVID-19 國內最嚴重省份(除湖北)確診人數柱狀圖僅僅需要每個省份最新的數據,所以我們得先把每個省份最新得數據單獨提取出來,構建一個全國所有省份最新數據的DataFrame
chinaProviceDataDf = pd.DataFrame()
chinaDayDf = pd.DataFrame()
# columns 中是每個省份名稱,通過這個提取特定的省份數據
for name in list(chinaProviceTotalDataDf.columns):
df = pd.DataFrame(list(chinaProviceTotalDataDf[name]))
df['日期'] = df['日期'].apply(changeDate)
# 最近日期的索引
maxDayIdx = df['日期'] == np.max(df['日期'])
d = {
'name': name,
'date': df[maxDayIdx]['日期'],
'confirm': df[maxDayIdx]['累計確診'],
'suspect': df[maxDayIdx]['累計疑似']
}
chinaProviceDataDf = chinaProviceDataDf.append(pd.DataFrame(d))
chinaProviceDataDf = chinaProviceDataDf.reset_index()
chinaProviceDataDf = chinaProviceDataDf.drop(columns='index')
生成的DataFrame : chinaProviceDataDf格式如下
name | date | confirm | suspect | |
---|---|---|---|---|
0 | 上海市 | 2020-04-24 | 641 | 12 |
1 | 雲南省 | 2020-04-24 | 185 | 0 |
2 | 內蒙古自治區 | 2020-04-24 | 197 | 4 |
3 | 北京市 | 2020-04-24 | 593 | 1 |
... | ... | ... | ... | ... |
DataFrame.sort_valules()函數可以根據一個列的值大小進行升降序排序,然后我們通過head()函數可以獲取出前幾行,所以我們可以如下獲取前五嚴重和前十嚴重的省份數據
most5ProDF = chinaProviceDataDf.drop(index=(chinaProviceDataDf[chinaProviceDataDf.name == '湖北省'].index)).sort_values('confirm',ascending=False).head(5)
most10ProDF = chinaProviceDataDf.drop(index=(chinaProviceDataDf[chinaProviceDataDf.name == '湖北省'].index)).sort_values('confirm',ascending=False).head(10)
selBarCur = alt.selection_multi(name='selBarLegend',fields=['name'],bind='legend')
hoverSel = alt.selection(name='hoverSel',type='single', nearest=True, on='mouseover',
fields=['name'],empty='none')
color = alt.Color('name:N',scale=alt.Scale(
domain=list(most10ProDF.name),
range=['#d62728','#9467bd','#e377c2','#ff7f0e','#1f77b4',
'#8c564b','#17becf','#FF6666','#c49c94','#9edae5']),
legend=alt.Legend(title='確診人數', titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=17,rowPadding=5,labelOpacity=0.8,orient='left'))
# color = alt.Color('name:N',scale=alt.Scale(scheme='tableau20' ),
# legend=alt.Legend(title='確診人數', titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=15,rowPadding=5,labelOpacity=0.8))
bar = alt.Chart(most10ProDF).mark_bar().encode(
x=alt.X('confirm',axis=alt.Axis(title='人數',titleFontSize=16,titlePadding=20,grid=True,labelFontSize=15)),
y=alt.Y('name:N',sort=list(most10ProDF.name),axis=alt.Axis(title='省份',titleFontSize=18,titlePadding=20,grid=True,tickSize=15,labelFontSize=15,labelPadding=2,labelAngle=-25)),
# color = alt.Color('name:N',legend=alt.Legend(title='確診人數', titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=15,rowPadding=5,labelOpacity=0.5))
color=alt.condition(selBarCur,color,alt.value('lightgray')),
size=alt.condition(hoverSel,alt.value(28),alt.value(20)),
)
point = alt.Chart(most10ProDF).mark_point().encode(
y=alt.Y('name',sort=list(most10ProDF.name)),
opacity=alt.value(0)
).add_selection(
hoverSel
)
text = bar.mark_text(align='left', dx=10, dy=0,fontSize=15, fontWeight='bold').encode(
text='confirm',
opacity=alt.condition(selBarCur,alt.value(1),alt.value(0))
).add_selection (
selBarCur
)
alt.layer(
point,bar,text,
).properties(
width=1200,height=500,
title= {
"text": "COVID-19 國內最嚴重省份(除湖北)確診人數柱狀圖",
"fontSize": 20,
'offset':20
}
)
繪制出的圖標如下:
COVID-19 中國 前十嚴重省份累計確診人數折線圖
我們已經獲取了前十嚴重的省份,所以我們可以取出省份名,再進行一個for循環獲取出每個省份的歷史數據然后構建我們需要的DataFrame。此處不再贅述。
most10ProList = list(most10ProDF.name)
chinaMost10ProDayHisDf = pd.DataFrame()
for name in most10ProList:
df = pd.DataFrame(list(chinaProviceTotalDataDf[name]))
df['日期'] = df['日期'].apply(changeDate)
d = {
'name': name,
'日期': df['日期'],
'累計確診': df['累計確診'],
}
chinaMost10ProDayHisDf = chinaMost10ProDayHisDf.append(pd.DataFrame(d))
import altair as alt
import pandas as pd
import numpy as np
# Create a selection that chooses the nearest point & selects based on x-value
nearest = alt.selection(name='nearest',type='single', nearest=True, on='mouseover',
fields=['日期'],empty='none')
selCur = alt.selection_multi(name='selCur',fields=['name'],bind='legend')
color = alt.Color('name:N',legend=alt.Legend(title=None, titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=16,rowPadding=10,labelOpacity=0.5,offset=60),
scale=alt.Scale(domain=list(chinaMost10ProDayHisDf.name),
range=['#d62728','#9467bd','#e377c2','#ff7f0e','#1f77b4',
'#8c564b','#17becf','#FF6666','#c49c94','#9edae5']))
# The basic line
line = alt.Chart(chinaMost10ProDayHisDf).mark_line().encode(
x=alt.X('monthdate(日期):T', axis=alt.Axis(title='日期',titleFontSize=16,titlePadding=20,grid=True,labelFontSize=14)),
y=alt.Y('累計確診', axis=alt.Axis( title='累計確診人數',titleFontSize=16,titleColor='#FF9988',titlePadding=20)),
color=alt.condition(selCur,color,alt.value('lightgray')),
strokeWidth=alt.condition(selCur,alt.value(5),alt.value(3)),
)
# Transparent selectors across the chart. This is what tells us
# the x-value of the cursor
selectors = alt.Chart(chinaMost10ProDayHisDf).mark_point(strokeWidth=3).encode(
x=alt.X('monthdate(日期):T'),
opacity=alt.value(0),
).add_selection(
nearest
)
# Draw points on the line, and highlight based on selection
points = line.mark_point(strokeWidth=5).encode(
opacity=alt.condition(nearest, alt.value(1), alt.value(0)),
size=alt.condition(selCur,alt.value(3),alt.value(0)),
).add_selection(
selCur
)
# Draw text labels near the points, and highlight based on selection
text = line.mark_text(align='left', dx=5, dy=-15,fontSize=15, fontWeight='bold').encode(
text=alt.condition(nearest, '累計確診', alt.value(' ')),
opacity=alt.condition(selCur, alt.value(1), alt.value(0)),
)
# Draw a rule at the location of the selection
rules = alt.Chart(chinaMost10ProDayHisDf).mark_rule(strokeWidth=2,color='gray',opacity=0.2).encode(
x=alt.X('monthdate(日期):T')
).transform_filter(
nearest
)
# Put the five layers into a chart and bind the data
alt.layer(
line, selectors, points, rules, text
).properties(
width=1200, height=400,
title= {
"text": "COVID-19 中國 前十嚴重省份累計確診人數折線圖",
"fontSize": 20,
'offset':20
}
).configure_view(stroke=None)
最終效果如下所示:
COVID-19 世界累計確診變化曲線
世界整體疫情數據存放在 ‘全球每日歷史數據.csv’中,按照之前的操作,即可簡單完成,不再贅述。
最終效果如下所示:
COVID-19 海外各國(地區) 確診人數柱狀圖
由於每個國家數據開始統計的時間不一致,所以體現在數據格式上就是長度不一致,所以沒辦法直接整個存入在csv中,為了方便,DataSet中提供了全球國家每日歷史數據的json文件,我們只需要讀取json文件,將其存在dict中。
# file_name為全球國家每日歷史數據(截止至2020-04-24).json在你電腦上的對應的路徑
with open('%s' % file_name,'r') as f:
globalDict = json.loads(f.read())
然后我們讀取每個國家中最近日期的累計確診人數,將其保存下來
# file_name為全球國家每日歷史數據(截止至2020-04-24).json在你電腦上的對應的路徑
with open('%s' % file_name,'r') as f:
globalDict = json.loads(f.read())
globalCountryDf = pd.DataFrame()
for name in globalDict.keys():
tempDf = pd.DataFrame(globalDict[name])
tempDf['日期'] = tempDf['日期'].apply(changeDate)
df = tempDf[tempDf['日期'] == np.max(tempDf['日期'])]
d = {
'date' : df['日期'],
'name': name,
'confirm': df['累計確診']
}
globalCountryDf = globalCountryDf.append(pd.DataFrame(d))
globalPCountryDf = globalCountryDf.sort_values('confirm',ascending=False).head(20)
selBarCur = alt.selection_multi(name='selBarLegend',fields=['name'],bind='legend')
hoverSel = alt.selection(name='hoverSel',type='single', nearest=True, on='mouseover',
fields=['name'],empty='none')
color = alt.Color('name:N',scale=alt.Scale(
domain=list(globalPCountryDf['name']),
range=['#d62728','#9467bd','#e377c2','#ff7f0e','#1f77b4',
'#8c564b','#17becf','#FF6666','#c49c94','#9edae5',
'#c5b0d5','#dbbb88','#ff9896','#7f7f7f','#98df8a',
'#2ca02c','#ffbb78','#bcbd22','#aec7e8','#f7b6d2']),
legend=alt.Legend(title='國家', titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=15,rowPadding=5,labelOpacity=0.8,offset=60,orient='left'))
# color = alt.Color('name:N',scale=alt.Scale(scheme='tableau20' ),
# legend=alt.Legend(title='確診人數', titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=15,rowPadding=5,labelOpacity=0.8))
bar = alt.Chart(globalPCountryDf).mark_bar(size=23).encode(
# ,scale=alt.Scale(domain=(0,max))
x=alt.X('confirm',axis=alt.Axis(title='人數',titleFontSize=16,titlePadding=20,grid=True,labelFontSize=15)),
y=alt.Y('name:N', sort=list(globalPCountryDf),axis=alt.Axis(title='國家',titleFontSize=18,titlePadding=20,grid=True,tickSize=15,labelFontSize=15,labelPadding=2,labelAngle=-25)),
# color = alt.Color('name:N',legend=alt.Legend(title='確診人數', titleFontSize=16,titleOpacity=0.5,titlePadding=20,labelFontSize=15,rowPadding=5,labelOpacity=0.5))
color=alt.condition(selBarCur,color,alt.value('lightgray')),
size=alt.condition(hoverSel,alt.value(28),alt.value(20)),
)
point = alt.Chart(globalPCountryDf).mark_point().encode(
y=alt.Y('name',sort=list(globalPCountryDf)),
opacity=alt.value(0)
).add_selection(
hoverSel
)
text = bar.mark_text(align='left', dx=10, dy=0,fontSize=15, fontWeight='bold').encode(
text='confirm',
opacity=alt.condition(selBarCur,alt.value(1),alt.value(0))
).add_selection (
selBarCur
)
alt.layer(
bar,text,point
).properties(
width=1200,height=600,
title= {
"text": "COVID-19 海外各國(地區) 確診人數柱狀圖",
"fontSize": 20,
'offset':20
}
)
最終效果如下所示:
COVID-19 全球 前五嚴重國家累計確診人數折線圖
我們已經有了前十嚴重國家的名單,所以我們可以提取出前五嚴重的國家,所以我們只需要拿前五的國家名單去構建我們需要的這五個國家的每日歷史數據的DataFrame就行了,就此不再贅述。
最終結果如下圖所示:
COVID-19 世界各國(地區) 確診人數熱度圖
COVID-19 世界各國(地區) 確診人數熱度圖所使用的數據在:修復后的世界地圖.csv 中,這份地圖是我提取修復過的。我們只需要用一個DataFrame讀取里面的數據,然后添加確診人數列到里面就可以了。
繪制地圖其實有很多流程,還有技術方面可以講,還有繪圖的圖層思想,但是由於太繁瑣,所以此時不講,如果比較多人有興趣,有時間會考慮寫詳細點,畢竟現在已經寫得很長了
color = alt.Color('confirm:N',legend=alt.Legend(title=None,titlePadding=20,rowPadding=-1,symbolStrokeWidth=0,symbolType='square',symbolSize=300,
labelPadding=20,labelOpacity=0,clipHeight=12,
titleFontSize=14,titleOpacity=0.5),scale=alt.Scale(scheme='yelloworangebrown'))
shape = alt.Chart().mark_geoshape(fill='lightblue')
geo = alt.Chart(world_gdf).mark_geoshape().encode(
color = color
).project(
'naturalEarth1'
).properties(
width=1200,height=600,
title= {
"text": "COVID-19 世界各國(地區) 確診人數熱度圖",
"fontSize": 20,
'offset':20
}
)
hover = alt.selection(type='single', on='mouseover', nearest=True,fields=['lat','lon'])
base = alt.Chart(world_gdf).encode(
longitude='lon:Q',
latitude='lat:Q',
)
point = base.mark_point().encode(
color=alt.value('lightblue'),
size=alt.condition(~hover, alt.value(30), alt.value(100)),
tooltip=['cname', 'confirm'],
opacity=alt.value(1)
).add_selection(hover)
layer = alt.layer(
shape,geo,point
)
layer.configure_view(stroke=None)
最終結果如下圖所示:
總結
后續還可以開展數據建模,疫情預測分析等。本人目前也沒整理,所以沒寫,其實這個小小的項目可以做的有很多,例如過程中我就用到了爬蟲,還有數據清洗,數據可視化,函數擬合,數據建模,在繪制世界地圖時還自己處理修復了地圖。希望以后有機會再分享。這次是我第一次寫博客,盡量寫得通俗易懂,但是肯定存在不完善的地方,不能盡善盡美,只希望可以幫助到有需要的人,如果有人跟着教程一步步做有問題,歡迎聯系讓我修改。