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天左右去医院,症状最多的是发烧,等等。