2020 年全球的關鍵詞非COVID19 莫屬。雖然如今關於病毒的起源衆說紛紜,也引發了不小的外交衝突。做爲數據愛好者,仍是用數聽說話比較靠譜。python
COVID19數據來源有不少,這裏僅僅選kaggle上的數據,連接以下:www.kaggle.com/sudalairajk… 這裏面的數據會持續更新,因此你拿到的數據可能會和我不一樣。git
該連接共包含如下數據集:github
各個數據集的側重點不一樣,今天咱們分析一下第一組數據,COVID19_line_list_data。bash
首先仍是加載一些包,我首先預計會用到這幾個包,後面用的包會在後面導入。app
import plotly.graph_objects as go
from collections import Counter
import missingno as msno
import pandas as pd
複製代碼
數據源我已經提早下好,而且放到代碼所在路徑的data 文件中,你能夠根據你的狀況調整路徑。ide
line_list_data_file = 'data/COVID19_line_list_data.csv'
複製代碼
一如既往,首先查看數據統計信息。函數
line_list_data_raw_df = pd.read_csv(line_list_data_file)
print(line_list_data_raw_df.info())
print(line_list_data_raw_df.describe())
複製代碼
結果以下,系統識別出了27列的數據,可是仔細看,有多列數據Non-Null Count 爲0,意味着爲空列,樣本量爲1085行。ui
Backend TkAgg is interactive backend. Turning interactive mode on.
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1085 entries, 0 to 1084
Data columns (total 27 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 1085 non-null int64
1 case_in_country 888 non-null float64
2 reporting date 1084 non-null object
3 Unnamed: 3 0 non-null float64
4 summary 1080 non-null object
5 location 1085 non-null object
6 country 1085 non-null object
7 gender 902 non-null object
8 age 843 non-null float64
9 symptom_onset 563 non-null object
10 If_onset_approximated 560 non-null float64
11 hosp_visit_date 507 non-null object
12 exposure_start 128 non-null object
13 exposure_end 341 non-null object
14 visiting Wuhan 1085 non-null int64
15 from Wuhan 1081 non-null float64
16 death 1085 non-null object
17 recovered 1085 non-null object
18 symptom 270 non-null object
19 source 1085 non-null object
20 link 1085 non-null object
21 Unnamed: 21 0 non-null float64
22 Unnamed: 22 0 non-null float64
23 Unnamed: 23 0 non-null float64
24 Unnamed: 24 0 non-null float64
25 Unnamed: 25 0 non-null float64
26 Unnamed: 26 0 non-null float64
dtypes: float64(11), int64(2), object(14)
memory usage: 229.0+ KB
None
id case_in_country ... Unnamed: 25 Unnamed: 26
count 1085.000000 888.000000 ... 0.0 0.0
mean 543.000000 48.841216 ... NaN NaN
std 313.356825 78.853528 ... NaN NaN
min 1.000000 1.000000 ... NaN NaN
25% 272.000000 11.000000 ... NaN NaN
50% 543.000000 28.000000 ... NaN NaN
75% 814.000000 67.250000 ... NaN NaN
max 1085.000000 1443.000000 ... NaN NaN
[8 rows x 13 columns]
複製代碼
pandas 提供了方便的dropna 函數,能夠識別出全部的nan 數據,而且標識爲True,Dataframe 能夠對每列(axis=1)的全部布爾標識進行邏輯運算(any 或者是all),至關於or 或者and 運算,以後獲得1維的標識,進行刪除。 我的習慣於對一個dataframe 直接操做,能夠節省變量內存,所以後續不少操做都會設置inplace=True。spa
line_list_data_raw_df.dropna(axis=1, how='all', inplace=True)
print(f'df shape is {line_list_data_raw_df.shape}')
複製代碼
df shape is (1085, 20)
複製代碼
缺失值查詢很簡單,用info函數很容易獲得統計數據,可是這裏咱們能夠用圖畫來更直觀的展現數據的缺失狀況。3d
missingno 是專門進行缺失數據可視化的python 庫,它自帶多個可視化類型,好比matrix,bar chart,dendrogram等。對於小樣本量,matrix會是不錯的選擇,更大的數據量能夠選用dendrogram。 關於該庫更多的詳情,請參考github:github.com/ResidentMar…
msno.matrix(df=line_list_data_raw_df, fontsize=16)
複製代碼
結果以下:左側欄(Y軸)是樣本量,咱們最多的樣本量爲1085個。橫座標是特徵名稱,由於咱們的特徵比較少,因此能夠清晰的展現。黑色表示該特徵樣本齊全,白色間隙表示該特徵缺失部分樣本。能夠看到case_in_country 有樣本缺失,並且集中在開始。畫面的右側有一條曲線(sparkline),用於展現每一個樣本特徵個數。好比有個數字10,表示該行只有10個特徵,數字20表示最多的一個樣本有20個特徵。
數據清理的很關鍵的一種就是數據填充,下面咱們就要針對不一樣的列進行填充,文中用的填充思路可能不是最佳的,可是目的是爲了展現不一樣的填充方法的實現形式。咱們不會簡單的一根筋,只會填充爲常數,均值或者其餘統計指標。
咱們注意到有幾列是時間相關的特徵,咱們首先要將其轉成時間格式,python的時間格式不少,因爲咱們後續操做都用pandas,所以我這裏將其轉爲pandas中的時間格式(Timestamp)。 咱們能夠先看一下不轉時間格式,曲線圖效果如何。咱們採用plotly 畫圖,具體看代碼。爲何用plotly? 由於能夠交互!!
fig = go.Figure()
for col in date_cols:
fig.add_trace(go.Scatter(y=line_list_data_raw_df[col], name=col))
fig.show()
複製代碼
能夠看到Y座標(紅色框內所示)亂成一團。
咱們查看一下這幾列的數據格式有哪些。date_cols = [
'reporting date',
'symptom_onset',
'hosp_visit_date',
'exposure_start',
'exposure_end']
print(line_list_data_raw_df[date_cols].head(5))
print(line_list_data_raw_df[date_cols].tail(5))
複製代碼
能夠看到結果中時間格式有多種,有的是1/20/2020, 有的是01/03/20,還有不少是NaN缺失。
reporting date symptom_onset hosp_visit_date exposure_start exposure_end
0 1/20/2020 01/03/20 01/11/20 12/29/2019 01/04/20
1 1/20/2020 1/15/2020 1/15/2020 NaN 01/12/20
2 1/21/2020 01/04/20 1/17/2020 NaN 01/03/20
3 1/21/2020 NaN 1/19/2020 NaN NaN
4 1/21/2020 NaN 1/14/2020 NaN NaN
reporting date symptom_onset hosp_visit_date exposure_start exposure_end
1080 2/25/2020 NaN NaN NaN NaN
1081 2/24/2020 NaN NaN NaN NaN
1082 2/26/2020 NaN NaN NaN 2/17/2020
1083 2/25/2020 NaN NaN 2/19/2020 2/21/2020
1084 2/25/2020 2/17/2020 NaN 2/15/2020 2/15/2020
複製代碼
咱們能夠寫一個小的函數來看一下時間數據的長度分佈:
# check the length of date
for col in date_cols:
date_len = line_list_data_raw_df[col].astype(str).apply(len)
date_len_ct = Counter(date_len)
print(f'{col} datetiem length distributes as {date_len_ct}')
複製代碼
能夠看到時間字符串的長度不一樣,其中hosp_visit_date的長度有4種(除去長度爲3的NaN)。
reporting date datetiem length distributes as Counter({9: 894, 8: 190, 3: 1})
symptom_onset datetiem length distributes as Counter({3: 522, 9: 379, 8: 167, 10: 17})
hosp_visit_date datetiem length distributes as Counter({3: 578, 9: 375, 8: 128, 10: 2, 7: 2})
exposure_start datetiem length distributes as Counter({3: 957, 9: 91, 8: 30, 10: 7})
exposure_end datetiem length distributes as Counter({3: 744, 9: 292, 8: 46, 10: 3})
複製代碼
對於通常的字符串轉成時間格式,pandas中to_datetime 函數能夠解決問題,可是本案例中出現了mix的時間格式,所以咱們須要一點小技巧來完成格式轉換。
def mixed_dt_format_to_datetime(series, format_list):
temp_series_list = []
for format in format_list:
temp_series = pd.to_datetime(series, format=format, errors='coerce')
temp_series_list.append(temp_series)
out = pd.concat([temp_series.dropna(how='any')
for temp_series in temp_series_list])
return out
複製代碼
代碼核心思想:to_datetime 每次只能轉一個時間格式,咱們須要將格式不匹配的數據設置爲NaT(沒有筆誤,不是NaN)。對於同一列,咱們用不一樣的時間格式屢次轉換,最後求交集。或者你能夠對每一行的數據進行分別判斷,可是這個循環次數可能會比較多,我預測效率不是很高。
調用函數,轉換時間格式,而後咱們再次print info。能夠看到數據的格式已經變成了datetime64[ns],代表轉換成功。
for col in date_cols:
line_list_data_raw_df[col] = mixed_dt_format_to_datetime(
line_list_data_raw_df[col], ['%m/%d/%Y', '%m/%d/%y'])
print(line_list_data_raw_df[date_cols].info())
複製代碼
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 reporting date 1084 non-null datetime64[ns]
1 symptom_onset 563 non-null datetime64[ns]
2 hosp_visit_date 506 non-null datetime64[ns]
3 exposure_start 128 non-null datetime64[ns]
4 exposure_end 341 non-null datetime64[ns]
複製代碼
此時咱們能夠再次plot 這幾個曲線,Y軸已經變成頗有條理的時間線。
# fill missing report_date
print(line_list_data_raw_df[pd.isnull(
line_list_data_raw_df['reporting date'])].index)
print(line_list_data_raw_df['reporting date'].iloc[260:263])
line_list_data_raw_df.loc[261, 'reporting date'] = pd.Timestamp('2020-02-11')
print(line_list_data_raw_df.info())
複製代碼
time_delta = line_list_data_raw_df['reporting date'] - \
line_list_data_raw_df['hosp_visit_date']
time_delta.dt.days.hist(bins=20)
line_list_data_raw_df['hosp_visit_date'].fillna(
line_list_data_raw_df['reporting date'], inplace=True)
複製代碼
咱們能夠看到病人住院和報道的時間差(天數)分佈,大部分仍是在一天左右。因此咱們能夠近似的用reporting date的數據填充hosp_visit_date。
#fill missing symptom_onset
time_delta = line_list_data_raw_df['hosp_visit_date'] - \
line_list_data_raw_df['symptom_onset']
time_delta.dt.days.hist(bins=20)
average_time_delta = pd.Timedelta(days=round(time_delta.dt.days.mean()))
symptom_onset_calc = line_list_data_raw_df['hosp_visit_date'] - \
average_time_delta
line_list_data_raw_df['symptom_onset'].fillna(symptom_onset_calc, inplace=True)
print(line_list_data_raw_df.info())
複製代碼
一樣的,咱們能夠看看住院和病人有症狀的時間差分佈。此次分佈最高點再也不是1天附近,而是3天。也就是說大部分人在有症狀以後3天左右的時間去醫院,也有人接近25天才去。因此咱們這裏採用求均值的方法,而後根據入院時間倒推發病時間。
#fill missing exposure_start
time_delta = line_list_data_raw_df['symptom_onset'] - \
line_list_data_raw_df['exposure_start']
time_delta.dt.days.hist(bins=20)
average_time_delta = pd.Timedelta(days=round(time_delta.dt.days.mean()))
symptom_onset_calc = line_list_data_raw_df['symptom_onset'] - \
average_time_delta
line_list_data_raw_df['exposure_start'].fillna(symptom_onset_calc, inplace=True)
print(line_list_data_raw_df.info())
複製代碼
大部分人有暴露史後,4天到10天內出現症狀的機率較高,這也就是所謂的潛伏期。同理,咱們能夠以此倒推出暴露(感染)日期。
#fill missing exposure_end
line_list_data_raw_df['exposure_end'].fillna(line_list_data_raw_df['hosp_visit_date'], inplace=True)
print(line_list_data_raw_df.info())
複製代碼
咱們再次plot 這幾個時間特徵,能夠看到他們已經沒有缺失值。
其餘的填充方法,思路見代碼註釋。
# case_in_country 在其餘數據集中比較齊全,對於該數據集不重要,因此用-1 填充
line_list_data_raw_df['case_in_country'].fillna(-1, inplace=True)
print(line_list_data_raw_df.info())
# summary 每一個case 都不相同,沒法推斷,所以替換爲空字符串
print(line_list_data_raw_df['summary'].head(5))
line_list_data_raw_df['summary'].fillna('', inplace=True)
# 雖然性別能夠統計,可是這裏咱們直接用unknown 代替
print(line_list_data_raw_df.info())
print(line_list_data_raw_df['gender'].head(5))
line_list_data_raw_df['gender'].fillna('unknown', inplace=True)
# 年齡採用均值代替
line_list_data_raw_df['age'].hist(bins=10)
line_list_data_raw_df['age'].fillna(
line_list_data_raw_df['age'].mean(), inplace=True)
line_list_data_raw_df['age'].hist(bins=10)
# If_onset_approximated 設爲1表示都是咱們猜想的
print(line_list_data_raw_df['If_onset_approximated'].head(5))
line_list_data_raw_df['If_onset_approximated'].fillna(1, inplace=True)
print(line_list_data_raw_df.info())
# from Wuhan 丟失的數據在index 166和175 之間,能夠看到location 是北京,並且屬於早期,所以咱們能夠設爲1,表示來自武漢。
print(line_list_data_raw_df[pd.isnull(
line_list_data_raw_df['from Wuhan'])].index)
print(line_list_data_raw_df[['from Wuhan','country','location']].iloc[166:175])
line_list_data_raw_df['from Wuhan'].fillna(1.0,inplace=True)
# 咱們經過統計詞頻,選取出現最高的symptom 來代替缺失值。能夠看到最多見的symtom 是發燒。
symptom = Counter(line_list_data_raw_df['symptom'])
print(symptom.most_common(2)[1][0])
line_list_data_raw_df['symptom'].fillna(symptom.most_common(2)[1][0],inplace=True)
複製代碼
再次查看缺失matrix,bingo!雖然matrix再也不花哨(黑白相間),可是這是最完美的黑。
# missing data visualization
msno.matrix(df=line_list_data_raw_df, fontsize=16)
複製代碼
本文中主要介紹了數據清理尤爲是填充相關的技巧。你能夠填充一個具體的值,空值,統計值或者是根據其餘的列進行推斷。其中也涉及到一些小技巧,好比混合的時間格式如何轉成datetime,如何對數據缺失狀況進行可視化。 咱們沒有對該數據進行EDA處理,可是在數據清理的過程當中,咱們仍是對該病程有了一點更多的瞭解: 好比病人潛伏期在4天到10天比較多,病人出現症狀後通常3天左右去醫院,症狀最多的是發燒,等等。