AI/Kaggle

[Kaggle]Google Brain - Ventilator Pressure Prediction 02 (EDA) - 의료 데이터 분석

찌리남 2021. 12. 30. 23:20
728x90

저번 글에서 제가 참여한 캐글 대회의 전반적인 내용에 대해 다뤘습니다. Google Brain이 발전된 형태의 기계식 산소 호흡기를 개발하기 위해 의료 데이터 예측 모델을 평가하는 대회라고 소개하고 마지막에 학습 데이터의 Feature들을 간략하게 이야기했었습니다. 

 

피쳐들

  • id - globally-unique time step identifier across an entire file
  • breath_id - globally-unique time step for breaths
  • R - lung attribute indicating how restricted the airway is (in cmH2O/L/S). Physically, this is the change in pressure per change in flow (air volume per time). Intuitively, one can imagine blowing up a balloon through a straw. We can change R by changing the diameter of the straw, with higher R being harder to blow.
  • C - lung attribute indicating how compliant the lung is (in mL/cmH2O). Physically, this is the change in volume per change in pressure. Intuitively, one can imagine the same balloon example. We can change C by changing the thickness of the balloon’s latex, with higher C having thinner latex and easier to blow.
  • time_step - the actual time stamp.
  • u_in - the control input for the inspiratory solenoid valve. Ranges from 0 to 100.
  • u_out - the control input for the exploratory solenoid valve. Either 0 or 1.
  • pressure - the airway pressure measured in the respiratory circuit, measured in cmH2

 

불러오기

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
#import cudf, cupy
train_df = pd.read_csv("train.csv", index_col=0)
test_df  = pd.read_csv("test.csv", index_col=0)
sample   = pd.read_csv("sample_submission.csv")

우선 대회에서 제공하는 데이터를 분석하기 위해 필요한 라이브러리와 파일들을 불러옵니다.

 

Train dataset 확인

train_df.head()

head() 함수로 train 데이터를 불러왔을 때 볼 수 있는 데이터프레임입니다. 이 데이터 프레임만 봐서는 각 피쳐들의 특징에 대해 파악하기 어려우니 EDA를 하면서 각 피쳐들의 특징 및 피쳐 간의 관계에 대해 탐구해보려고 합니다. Train datset은 총 6,036,000 rows * 8 columns로 이루어진 데이터 프레임이라는 사실을 염두에 두고 시작해보면 좋을 것 같습니다. 

id breath_id R C time_step u_in u_out pressure
1 1 20 50 0.000000 0.083334 0 5.837492
2 1 20 50 0.033652 18.383041 0 5.907794
3 1 20 50 0.067514 22.509278 0 7.876254
4 1 20 50 0.101542 22.808822 0 11.742872
5 1 20 50 0.135756 25.355850 0 12.234987

 

columns 분석

train_df.nunique().to_frame()
train_df.isnull().sum(axis=0).to_frame()

nunique() 함수로 각 피쳐들의 고유값의 수를 구합니다. 명령어의 결과들을 살펴보면 눈에 띄는 것들이 몇 개 있습니다. 우선 'breath_id'의 결과가 75450인데 이 수는 전체 행에서 80을 나눈 값과 같습니다.(6,036,000 / 75450 = 80) 그러므로 각 'breath_id'의 데이터가 각 80행씩 있는 데이터라고 해석할 수 있습니다. 그리고 각 'breath_id'들은 각자의 'R', 'C'를 갖고 있고 약 3초간의 'time_step'을 갖습니다.

그리고 isnull().sum(axis=0) 함수로 결측치를 확인했을 때 하나도 찾을 수 없었습니다.

breath_id 75450
R 3
C 3
time_step 3767571
u_in 4020248
u_out 2
pressure 950
breath_id 0
R 0
C 0
time_step 0
u_in 0
u_out 0
pressure 0

 

'u_in', 'u_out' 분석

breath_one = train_df[train_df["breath_id"] == 1].reset_index(drop=True)
breath_one.nunique().to_frame()

위의 코드로 예측한 사실을 확인하기 위해 'breath_id'가 1인 row들을 마스킹한 breath_one이라는 데이터 프레임을 생성합니다. nunique() 함수로 이를 확인합니다. 결과를 보면 예측한 것처럼 'breath_id', 'R', 'C' 모두 하나의 값을 가집니다. 그리고 'time_step' 예상한 것처럼 80개를 갖습니다.

plot() 함수로  
   
   
   
   
   
   

 

breath_one.plot(x="time_step", y="u_in", kind="line", figsize=(12,3), lw=2, title="u_in")
breath_one.plot(x="time_step", y="u_out", kind="line", figsize=(12,3), lw=2, title="u_out")
breath_one.plot(x="time_step", y="pressure", kind="line", figsize=(12,3), lw=2, title="pressure")

'time_step'을 가로축으로 하는 'u_in', 'u_out', 'pressure'의 선 그래프를 그려봅니다. 그래프를 보면 'u_in'과 'u_out'의 값이 반비례하는 것처럼 보입니다. 숨을 들이쉴 때는 숨을 내쉬지 않고, 내쉴 때는 들이쉬지 않는다는 것을 생각해보면 당연한 일입니다. 그리고 'u_in'의 값이 높은 상태에서(숨을 들이쉬고 있는 상태) 서서히 'pressure'가 높아지다가 'u_out'이 1이 되면(숨을 내쉬면) 급격히 'pressure'가 낮아지는 것을 확인할 수 있습니다.

 

'R', 'C' 분석

RAPIDS Nearest Neighbors (KNN) 과 RAPIDS kMeans를 사용해 'breath_id=87'과 'u_in'이 같고 'R'과 'C'는 다른 데이터들을 선 그래프로 표현합니다. 'R'은 높을수록(50, 빨간색) 숨을 들이쉴 때 압력이 급격히 높아지고 숨을 내쉬기 전부터 서서히 낮아집니다. 그리고 낮을수록(5, 노란색) 숨을 들이쉴 때도 압력의 증가가 크지 않고, 숨을 내쉬면서 압력이 급격히 감소합니다. 이를 통해 유추할 수 있는 것은 'R'은 폐의 저항을 나타낸다는 것입니다. 그래서 저항이 높을수록 공기를 잘 가두기 때문에 압력도 높아지고 낮으면 공기를 잘 못 가둬서 압력이 높아지지 않는 것입니다.

'C'는 높을수록(50, 노란색) 숨을 들이쉴 때 압력이 아주 낮게 상승하고, 낮을수록(10, 빨간색) 숨을 들이쉴 때 아주 높게 상승하고 내쉴 때 급격히 낮아지는 것을 볼 수 있습니다. 이를 통해 알 수 있는 것은  'C'는 얼마나 폐가 잘 늘어나는지에 대한 데이터를 말한다는 것입니다. 결과적으로 'C'가 50이어서 숨을 들이쉴 때 폐가 아주 잘 늘어난다면 폐 안에 공기의 양이 많아져도 가두는 공간이 같이 늘어나기 때문에 압력이 많이 늘어나지 않습니다. 반대로 폐가 쉽게 늘어나지 않는다면 한정된 공간에 많은 공기를 가두기 때문에 압력이 급격히 증가하는 것입니다.

 

'R', 'C' 조합별 'pressure' 분석

breath_2 = train_df.query('breath_id == 2').reset_index(drop = True)
breath_3 = train_df.query('breath_id == 3').reset_index(drop = True)
breath_4 = train_df.query('breath_id == 4').reset_index(drop = True)
breath_5 = train_df.query('breath_id == 5').reset_index(drop = True)
breath_17 = train_df.query('breath_id == 17').reset_index(drop = True)
breath_18 = train_df.query('breath_id == 18').reset_index(drop = True)
breath_21 = train_df.query('breath_id == 21').reset_index(drop = True)
breath_39 = train_df.query('breath_id == 39').reset_index(drop = True)

fig, axes = plt.subplots(3,3,figsize=(15,15))
sns.lineplot(data=breath_39, x="time_step", y="pressure", lw=2, ax=axes[0,0])
axes[0,0].set_title ("R=5, C=10", fontsize=18)
axes[0,0].set(xlabel='')
#axes[0,0].set(ylim=(0, None))
sns.lineplot(data=breath_21, x="time_step", y="pressure",  lw=2, ax=axes[0,1])
axes[0,1].set_title ("R=20, C=10", fontsize=18)
axes[0,1].set(xlabel='')
axes[0,1].set(ylabel='')
#axes[0,1].set(ylim=(0, None))
sns.lineplot(data=breath_18, x="time_step", y="pressure",  lw=2,ax=axes[0,2])
axes[0,2].set_title ("R=50, C=10", fontsize=18)
axes[0,2].set(xlabel='')
axes[0,2].set(ylabel='')
#axes[0,2].set(ylim=(0, None))
sns.lineplot(data=breath_17, x="time_step", y="pressure",  lw=2,ax=axes[1,0])
axes[1,0].set_title ("R=5, C=20", fontsize=18)
axes[1,0].set(xlabel='')
#axes[1,0].set(ylim=(0, None))
sns.lineplot(data=breath_2, x="time_step", y="pressure",  lw=2,ax=axes[1,1])
axes[1,1].set_title ("R=20, C=20", fontsize=18)
axes[1,1].set(xlabel='')
axes[1,1].set(ylabel='')
#axes[1,1].set(ylim=(0, None))
sns.lineplot(data=breath_3, x="time_step", y="pressure",  lw=2,ax=axes[1,2])
axes[1,2].set_title ("R=50, C=20", fontsize=18)
axes[1,2].set(xlabel='')
axes[1,2].set(ylabel='')
#axes[1,2].set(ylim=(0, None))
sns.lineplot(data=breath_5, x="time_step", y="pressure",  lw=2,ax=axes[2,0])
axes[2,0].set_title ("R=5, C=50", fontsize=18)
#axes[2,0].set(ylim=(0, None))
sns.lineplot(data=breath_one, x="time_step", y="pressure",  lw=2,ax=axes[2,1])
axes[2,1].set_title ("R=20, C=50", fontsize=18)
axes[2,1].set(ylabel='')
#axes[2,1].set(ylim=(0, None))
sns.lineplot(data=breath_4, x="time_step", y="pressure",  lw=2,ax=axes[2,2])
axes[2,2].set_title ("R=50, C=50", fontsize=18)
axes[2,2].set(ylabel='')
#axes[2,2].set(ylim=(0, None))

plt.show();

위의 'R', 'C' 분석에서 알 수 있듯 이 column의 값은 'pressure'의 그래프의 형태를 결정하는 특성을 갖고 있습니다. 그러므로 아마 이 값들의 조합마다 아주 다른 형태의 'pressure' 그래프를 보여줄 것으로 보입니다. 위의 코드로 출력된 그래프들을 확인하면 9개 조합의 그래프 모두 확연히 다른 성격을 보여줍니다. 'R'은 낮을 때, 'C'는 높을 때 압력이 낮기 때문에 일곱 번째 그래프(R=5, C=50)가 가장 낮은 압력을 나타냅니다.

 

zero time 분석

zero_time = train_df.query("time_step < 0.000001 & u_in < 0.000001").reset_index(drop = True)
zero_time_5_10  = zero_time.query("R ==  5 & C == 10").reset_index(drop = True)
zero_time_5_20  = zero_time.query("R ==  5 & C == 20").reset_index(drop = True)
zero_time_5_50  = zero_time.query("R ==  5 & C == 50").reset_index(drop = True)
zero_time_20_10 = zero_time.query("R == 20 & C == 10").reset_index(drop = True)
zero_time_20_20 = zero_time.query("R == 20 & C == 20").reset_index(drop = True)
zero_time_20_50 = zero_time.query("R == 20 & C == 50").reset_index(drop = True)
zero_time_50_10 = zero_time.query("R == 50 & C == 10").reset_index(drop = True)
zero_time_50_20 = zero_time.query("R == 50 & C == 20").reset_index(drop = True)
zero_time_50_50 = zero_time.query("R == 50 & C == 50").reset_index(drop = True)

fig, axes = plt.subplots(9,1,figsize=(12,15))
sns.violinplot(x=zero_time_5_10["pressure"], linewidth=2, ax=axes[0], color="indianred")
axes[0].set_title ("R=5, C=10", fontsize=14)
axes[0].set(xlim=(3, 8))
sns.violinplot(x=zero_time_5_20["pressure"], linewidth=2, ax=axes[1], color="firebrick")
axes[1].set_title ("R=5, C=20", fontsize=14)
axes[1].set(xlim=(3, 8))
sns.violinplot(x=zero_time_5_50["pressure"], linewidth=2, ax=axes[2], color="darkred" )
axes[2].set_title ("R=5, C=50", fontsize=14)
axes[2].set(xlim=(3, 8))
sns.violinplot(x=zero_time_20_10["pressure"], linewidth=2, ax=axes[3], color="greenyellow")
axes[3].set_title ("R=20, C=10", fontsize=14)
axes[3].set(xlim=(3, 8))
sns.violinplot(x=zero_time_20_20["pressure"], linewidth=2, ax=axes[4], color="olivedrab")
axes[4].set_title ("R=20, C=20", fontsize=14)
axes[4].set(xlim=(3, 8))
sns.violinplot(x=zero_time_20_50["pressure"], linewidth=2, ax=axes[5], color="olive" )
axes[5].set_title ("R=20, C=50", fontsize=14)
axes[5].set(xlim=(3, 8))
sns.violinplot(x=zero_time_50_10["pressure"], linewidth=2, ax=axes[6], color="steelblue")
axes[6].set_title ("R=50, C=10", fontsize=14)
axes[6].set(xlim=(3, 8))
sns.violinplot(x=zero_time_50_20["pressure"], linewidth=2, ax=axes[7], color="cornflowerblue")
axes[7].set_title ("R=50, C=20", fontsize=14)
axes[7].set(xlim=(3, 8))
sns.violinplot(x=zero_time_50_50["pressure"], linewidth=2, ax=axes[8], color="midnightblue" )
axes[8].set_title ("R=50, C=50", fontsize=14)
axes[8].set(xlim=(3, 8));

'time_step'이 0인 경우에도 'pressure'가 있는 경우가 있습니다. 공기를 들이쉬기 전의 압력을 알려주는 데이터이기 때문에 'R', 'C'의 값이 더 많은 영향을 줄 수도 있을 것 같습니다. 그래프로 결과를 보면 'pressure'의 차이가 그렇게 크지 않은 것을 확인할 수 있습니다. 그러나 이 대회의 문제는 회귀 예측 문제이기 때문에 이 정도 차이도 의미있는 정보로 볼 수 있습니다. 

 

결론

이상 EDA를 통해 각 피처 별로 어떤 특징이 있고 피처 간에 어떤 연관이 있는지 알아봤습니다. 결론적으로는 각 피처들이 'pressure'와 어떤 관계에 있는지 파악하여 얻은 인사이트를 피처 엔지니어링에 활용할 수 있고, 이번 대회의 데이터가 시계열 데이터이고 문제가 회귀 예측이기 때문에 어떤 모델을 써야할지 계획할 수 있게 됐습니다. 다음 글에서는 예측 모델을 실험한 내용에 대해 다루겠습니다.

728x90
반응형

'AI > Kaggle' 카테고리의 다른 글

[Kaggle] Google Brain - Ventilator Pressure Prediction 01(소개)  (0) 2021.12.23