COVID19 line list 數據集分析 (1) 數據清理

前言

2020 年全球的關鍵詞非COVID19 莫屬。雖然如今關於病毒的起源衆說紛紜,也引發了不小的外交衝突。做爲數據愛好者,仍是用數聽說話比較靠譜。python

COVID19數據來源有不少,這裏僅僅選kaggle上的數據,連接以下:www.kaggle.com/sudalairajk… 這裏面的數據會持續更新,因此你拿到的數據可能會和我不一樣。git

該連接共包含如下數據集:github

  • COVID19_line_list_data.csv(358.85 KB)--> 關於一些每次確診個例的報告
  • COVID19_open_line_list.csv(2.93 MB)--> 更詳細的確診個例報告
  • covid_19_data.csv(1.53 MB)--> 各國確診數據,時間線爲行
  • time_series_covid_19_confirmed.csv(100.3 KB)--> 時間線爲列的各國確診數據
  • time_series_covid_19_confirmed_US.csv(1.11 MB)--> 美國確診相關的數據
  • time_series_covid_19_deaths_US.csv(1.04 MB)--> 美國死亡數據
  • time_series_covid_19_deaths.csv(76.09 KB)--> 時間線爲列的各國死亡數據
  • time_series_covid_19_recovered.csv(84.62 KB)-->時間線爲列的治癒人數數據

各個數據集的側重點不一樣,今天咱們分析一下第一組數據,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軸已經變成頗有條理的時間線。

  • 咱們觀察該曲線,能夠看到report_date曲線在最上端,也就是最晚的時間,這很符合邏輯。
  • hospitalize_date 住院時間若是缺失的話,咱們能夠直接用報告時間代替。
  • 根據邏輯,通常病人在有症狀後,會隔一段時間或者立馬去醫院。所以hospitalize_date 一定會晚於symptom_onset 時間。這裏咱們能夠作出統計看看病人有症狀後多久會去醫院,並以此爲依據倒推symptom_onset時間。
  • 與此相似,咱們能夠統計有暴露史的起始時間與病人發病的時間差,所以填充exposure_start。
  • 至於exposure_end的缺失值,咱們有理由相信,病人入院就結束暴露史。

以上就是咱們的填充思路,具體的代碼(技巧)以下:

直接賦值填充

# 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天左右去醫院,症狀最多的是發燒,等等。

相關文章
相關標籤/搜索