Oh-Seung-Rok

안녕하세요. 정량적인 분석을 통해 제품-서비스를 개선하고 소비자들의 욕구를 먼저 파악하는 분석가가 되고 싶은 오승록입니다. 포트폴리오 [https://seungrok0317.com]

Kaggle - Heart Failure Prediction

19 Mar 2021 » Kaggle

Heart Failure Prediction

  • 데이터 셋: https://www.kaggle.com/andrewmvd/heart-failure-clinical-data

  • 환자들에게 주어지는 다양한 변수를 통해 죽음의 가능성을 밝혀내는 데이터 셋이다.

라이브러리 설정 및 데이터 불러오기

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
df = pd.read_csv('C:/Users/dissi/Kaggle Practice/heart_failure_clinical_records_dataset.csv')
df.head()
ageanaemiacreatinine_phosphokinasediabetesejection_fractionhigh_blood_pressureplateletsserum_creatinineserum_sodiumsexsmokingtimeDEATH_EVENT
075.005820201265000.001.91301041
155.0078610380263358.031.11361061
265.001460200162000.001.31291171
350.011110200210000.001.91371071
465.011601200327000.002.71160081
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 299 entries, 0 to 298
Data columns (total 13 columns):
 #   Column                    Non-Null Count  Dtype  
---  ------                    --------------  -----  
 0   age                       299 non-null    float64
 1   anaemia                   299 non-null    int64  
 2   creatinine_phosphokinase  299 non-null    int64  
 3   diabetes                  299 non-null    int64  
 4   ejection_fraction         299 non-null    int64  
 5   high_blood_pressure       299 non-null    int64  
 6   platelets                 299 non-null    float64
 7   serum_creatinine          299 non-null    float64
 8   serum_sodium              299 non-null    int64  
 9   sex                       299 non-null    int64  
 10  smoking                   299 non-null    int64  
 11  time                      299 non-null    int64  
 12  DEATH_EVENT               299 non-null    int64  
dtypes: float64(3), int64(10)
memory usage: 30.5 KB
  • age : 나이
  • anaemia : 빈혈증 여부 (0 = 무, 1 = 유)
  • creatinine_phosphokinase : 크레아틴키나제 검사 결과
  • diabetes : 당뇨병 여부
  • ejection_fraction : 박출계수
  • high_blood_pressure : 고혈압 여부
  • platelets : 혈소판 수
  • serum_creatinine : 혈중 크레아틴 레벨
  • serum_sodium : 혈중 나트륨 레벨
  • sex : 성별 (0 = 여성, 1 = 남성)
  • smoking : 흡연여부 (0 = 비흡연, 1 = 흡연)
  • time : 관찰기간(일)
  • DEATH_EVENT: 사망여부 ( 0 = 생존, 1 = 사망)

EDA 및 기초 통계 분석

수치형 데이터

df.describe()
ageanaemiacreatinine_phosphokinasediabetesejection_fractionhigh_blood_pressureplateletsserum_creatinineserum_sodiumsexsmokingtimeDEATH_EVENT
count299.000000299.000000299.000000299.000000299.000000299.000000299.000000299.00000299.000000299.000000299.00000299.000000299.00000
mean60.8338930.431438581.8394650.41806038.0836120.351171263358.0292641.39388136.6254180.6488290.32107130.2608700.32107
std11.8948090.496107970.2878810.49406711.8348410.47813697804.2368691.034514.4124770.4781360.4676777.6142080.46767
min40.0000000.00000023.0000000.00000014.0000000.00000025100.0000000.50000113.0000000.0000000.000004.0000000.00000
25%51.0000000.000000116.5000000.00000030.0000000.000000212500.0000000.90000134.0000000.0000000.0000073.0000000.00000
50%60.0000000.000000250.0000000.00000038.0000000.000000262000.0000001.10000137.0000001.0000000.00000115.0000000.00000
75%70.0000001.000000582.0000001.00000045.0000001.000000303500.0000001.40000140.0000001.0000001.00000203.0000001.00000
max95.0000001.0000007861.0000001.00000080.0000001.000000850000.0000009.40000148.0000001.0000001.00000285.0000001.00000
  • 범주형 데이터 경우 0과 1로 분류되며 대부분의 평균값이 0.5 인것으로 미루어 보아 균형잡힌 데이터라 할 수 있다.
  • 크레아틴 키나제 경우 최소와 최대 차이가 큰데 최대값이 평균값과 과도하게 차이가 나는 것으로보아 최대값은 아웃라이어 값이라 할 수 있다.
  • 혈소판 수 역시 중위값 75% 값에 비해 최대값이 차이가 있음. 아웃라이어로 판단하기는 부족하지만 주목할만한 값임.
  • 크레아틴 레벨 역시 최대값이 아웃라이어
sns.histplot(data=df, x='age')
<matplotlib.axes._subplots.AxesSubplot at 0x1c0cda0c908>

output_13_1

sns.histplot(data=df, x='age', hue='DEATH_EVENT')

# 80세 이후로 사망횟수가 생존보다 높다.
<matplotlib.axes._subplots.AxesSubplot at 0x1c0ce184308>

output_14_1

sns.histplot(data=df, x='ejection_fraction', hue='DEATH_EVENT', kde=True)
<matplotlib.axes._subplots.AxesSubplot at 0x1c0ce28de08>

output_15_1

sns.histplot(data=df, x='platelets', hue='DEATH_EVENT')

# 정규분포의 형태 사망역시 마찬가지로 혈소판수는 사망여부에 큰 영향을 주진 않음.
<matplotlib.axes._subplots.AxesSubplot at 0x1c0ce2c5508>

output_16_1

범주형 데이터

sns.boxplot(data=df, x='DEATH_EVENT', y='ejection_fraction')

# 생존한 사람들이 박출계수가 상대적으로 높음.
<matplotlib.axes._subplots.AxesSubplot at 0x1c0ce4b5e08>

output_18_1

sns.violinplot(data=df, x='DEATH_EVENT', y='ejection_fraction', hue='smoking')
<matplotlib.axes._subplots.AxesSubplot at 0x1c0ce5e2dc8>

output_19_1

모델 학습을 위한 전처리

df.columns
Index(['age', 'anaemia', 'creatinine_phosphokinase', 'diabetes',
       'ejection_fraction', 'high_blood_pressure', 'platelets',
       'serum_creatinine', 'serum_sodium', 'sex', 'smoking', 'time',
       'DEATH_EVENT'],
      dtype='object')
from sklearn.preprocessing import StandardScaler

X_num = df[['age', 'creatinine_phosphokinase',
       'ejection_fraction', 'platelets',
       'serum_creatinine', 'serum_sodium', 'time']]  
        # 수치형 데이터

X_cat = df[['anaemia','diabetes','high_blood_pressure','sex', 'smoking']]
        # 범주형 데이터

y = df['DEATH_EVENT']
# 표준화 작업 (평균빼주고 편차로 나누어준다.)
scaler = StandardScaler()
scaler.fit(X_num)
X_scaled = scaler.transform(X_num)
X_scaled = pd.DataFrame(data=X_scaled, index=X_num.index, columns=X_num.columns)
X = pd.concat([X_scaled, X_cat], axis=1)
X
agecreatinine_phosphokinaseejection_fractionplateletsserum_creatinineserum_sodiumtimeanaemiadiabeteshigh_blood_pressuresexsmoking
01.1929450.000166-1.5305601.681648e-020.490057-1.504036-1.62950200110
1-0.4912797.514640-0.0070777.535660e-09-0.284552-0.141976-1.60369100010
20.350833-0.449939-1.530560-1.038073e+00-0.090900-1.731046-1.59078500011
3-0.912335-0.486071-1.530560-5.464741e-010.4900570.085034-1.59078510010
40.350833-0.435486-1.5305606.517986e-011.264666-4.682176-1.57787911000
.......................................
2940.098199-0.537688-0.007077-1.109765e+00-0.2845521.4470941.80345101111
295-0.4912791.278215-0.0070776.802472e-02-0.1877260.5390541.81635700000
296-1.3333921.5259791.8549584.902082e+00-0.5750310.3120441.90669701000
297-1.3333921.890398-0.007077-1.263389e+000.0059260.7660641.93250900011
298-0.912335-0.3983210.5853891.348231e+000.199578-0.1419761.99703800011

299 rows × 12 columns

학습, 테스트 데이터 분리

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1)

분류하기

선형회귀모델

from sklearn.linear_model import LogisticRegression

model_lr = LogisticRegression()
model_lr.fit(X_train, y_train)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                   intercept_scaling=1, l1_ratio=None, max_iter=100,
                   multi_class='auto', n_jobs=None, penalty='l2',
                   random_state=None, solver='lbfgs', tol=0.0001, verbose=0,
                   warm_start=False)
from sklearn.metrics import classification_report

pred = model_lr.predict(X_test)
print(classification_report(y_test, pred))
              precision    recall  f1-score   support

           0       0.86      0.92      0.89        64
           1       0.76      0.62      0.68        26

    accuracy                           0.83        90
   macro avg       0.81      0.77      0.78        90
weighted avg       0.83      0.83      0.83        90

앙상블 모델 (XGBoost)

from xgboost import XGBClassifier

model_xgb = XGBClassifier()
model_xgb.fit(X_train, y_train)
[21:28:13] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.3.0/src/learner.cc:1061: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.


C:\Users\dissi\anaconda31\lib\site-packages\xgboost\sklearn.py:888: UserWarning: The use of label encoder in XGBClassifier is deprecated and will be removed in a future release. To remove this warning, do the following: 1) Pass option use_label_encoder=False when constructing XGBClassifier object; and 2) Encode your labels (y) as integers starting with 0, i.e. 0, 1, 2, ..., [num_class - 1].
  warnings.warn(label_encoder_deprecation_msg, UserWarning)





XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,
              colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1,
              importance_type='gain', interaction_constraints='',
              learning_rate=0.300000012, max_delta_step=0, max_depth=6,
              min_child_weight=1, missing=nan, monotone_constraints='()',
              n_estimators=100, n_jobs=4, num_parallel_tree=1,
              objective='binary:logistic', random_state=0, reg_alpha=0,
              reg_lambda=1, scale_pos_weight=1, subsample=1,
              tree_method='exact', use_label_encoder=True,
              validate_parameters=1, verbosity=None)
pred = model_xgb.predict(X_test)
print(classification_report(y_test, pred))
              precision    recall  f1-score   support

           0       0.95      0.91      0.93        64
           1       0.79      0.88      0.84        26

    accuracy                           0.90        90
   macro avg       0.87      0.90      0.88        90
weighted avg       0.91      0.90      0.90        90

앙상블 모델 분석 (어떤 feature가 가장 많은 영향을 미치는지)

plt.bar(X.columns, model_xgb.feature_importances_)
plt.xticks(rotation=90)
plt.show()

output_34_0

  • time 이 가장 중요요소로 나타나지만 time과 death_event경우 밀접한 상관관계를 가진다. 즉 correlation이 높음.
  • 관찰 도중 death_event가 발생하면 time도 끝나기 때문. time 배제해야함.

재학습

X_num = df[['age', 'creatinine_phosphokinase','ejection_fraction', 'platelets',
            'serum_creatinine', 'serum_sodium']]  
        # 수치형 데이터

X_cat = df[['anaemia','diabetes','high_blood_pressure','sex', 'smoking']]
        # 범주형 데이터
    
y = df['DEATH_EVENT']

scaler = StandardScaler()
scaler.fit(X_num)
X_scaled = scaler.transform(X_num)
X_scaled = pd.DataFrame(data=X_scaled, index=X_num.index, columns=X_num.columns)
X = pd.concat([X_scaled, X_cat], axis=1)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1)
model_lr = LogisticRegression()
model_lr.fit(X_train, y_train)

pred = model_lr.predict(X_test)
print(classification_report(y_test, pred))
              precision    recall  f1-score   support

           0       0.78      0.92      0.84        64
           1       0.64      0.35      0.45        26

    accuracy                           0.76        90
   macro avg       0.71      0.63      0.65        90
weighted avg       0.74      0.76      0.73        90
model_xgb = XGBClassifier()
model_xgb.fit(X_train, y_train)

pred = model_xgb.predict(X_test)
print(classification_report(y_test, pred))
C:\Users\dissi\anaconda31\lib\site-packages\xgboost\sklearn.py:888: UserWarning: The use of label encoder in XGBClassifier is deprecated and will be removed in a future release. To remove this warning, do the following: 1) Pass option use_label_encoder=False when constructing XGBClassifier object; and 2) Encode your labels (y) as integers starting with 0, i.e. 0, 1, 2, ..., [num_class - 1].
  warnings.warn(label_encoder_deprecation_msg, UserWarning)


[21:37:11] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.3.0/src/learner.cc:1061: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
              precision    recall  f1-score   support

           0       0.81      0.88      0.84        64
           1       0.62      0.50      0.55        26

    accuracy                           0.77        90
   macro avg       0.72      0.69      0.70        90
weighted avg       0.76      0.77      0.76        90
plt.bar(X.columns, model_xgb.feature_importances_)
plt.xticks(rotation=90)
plt.show()

output_40_0

  • time이 빠져서 정확도가 줄었들었음.
  • serum_creatinine, ejection_fraction, age 순으로 변수 중요도
sns.jointplot(data=df, x='ejection_fraction', y='serum_creatinine', hue='DEATH_EVENT')
<seaborn.axisgrid.JointGrid at 0x1c0d0263208>

output_42_1

  • 혈중 크레아틴 레벨과 박출계수를 같이 사용시 더 잘 구분됨을 알 수 있다.
  • 각 변수로 사망을 따졌을시 구분하기 어렵지만 두 가지의 변수 사용시, 사망자가 2차원 평면 좌측 하단과 우측 상단에 분포해있음을 확인할 수 있다.

모델 평가

from sklearn.metrics import plot_precision_recall_curve
fig = plt.figure()
ax = fig.gca()
plot_precision_recall_curve(model_lr, X_test, y_test, ax=ax)
plot_precision_recall_curve(model_xgb, X_test, y_test, ax=ax)
<sklearn.metrics._plot.precision_recall_curve.PrecisionRecallDisplay at 0x1c0ce56e908>

output_45_1

from sklearn.metrics import plot_roc_curve

fig = plt.figure()
ax = fig.gca()
plot_roc_curve(model_lr, X_test, y_test, ax=ax)
plot_roc_curve(model_xgb, X_test, y_test, ax=ax)
<sklearn.metrics._plot.roc_curve.RocCurveDisplay at 0x1c0cf0407c8>

output_46_1

  • 정확도 76-77 auc의 경우 77-79의 모델이 나왔다.