📌의료지표 데이터를 통해 심부전증 진단하기(Kaggle 데이터 활용)

왜 심부전증 데이터를 골랐는가? 에 대하여

  • 부트캠프에서 처음으로 빅데이터 분석을 배우며 마지막에 딥러닝에 대해 배우는 과정이 있었다.
  • 이때, 이미지를 통해 폐질환 등을 분류해는 모델링에 대한 사례를 배웠는데 이때 생긴 궁금증이 있었다.
  • “이미지로 분류하는 것이 아니라, 의료관련 다양한 지표 데이터들을 활용해 머신러닝으로 질병을 예측/분류 할수는 없는 걸까?”
  • 때마침 kaggle에서 심부전증환자를 예측하는데 활용할 수 있는 데이터를 발견해서 이를 통해 확인해보고자 한다.




주제 : 예측 모델로 심부전증 여부 예측하기


데이터 소개

Kaggle의 Heart Failure Prediction 데이터셋을 사용


컬럼설명

  1. 범주형 데이터
    • anaemia: 환자의 빈혈증 여부 (0: 정상, 1: 빈혈)
    • diabetes: 당뇨병 여부 (0: 정상, 1: 당뇨)
    • high_blood_pressure: 고혈압 여부 (0: 정상, 1: 고혈압)
    • sex: 성별 (0: 여성, 1: 남성)
    • smoking: 흡연 여부 (0: 비흡연, 1: 흡연)
    • DEATH_EVENT: 사망 여부 (0: 생존, 1: 사망)




  2. 수치형 데이터
    • age: 환자의 나이
      • 심부전 유병률은 60세 이상에서 급증하며 나이가 들수록 증가하는 것으로 알려졌다.
    • time: 관찰 기간 (일)
    • creatinine_phosphokinase: 크레아틴키나제 검사 결과
      • CK (크레아틴키나제)는 심장, 뇌, 골격근, 및 다른 조직에서 발견되는 효소
      • CK(크레아틴키나제) 농도는 근육 또는 심장 세포가 손상될 때에 올라간다.
    • ejection_fraction: 박출계수 (%)
      • 맥박을 통해 심장에서 박출 되거나 펌프하는 혈액의 %량
      • 범주는 0-100%
      • 의학적으로 박출계수가 50% 또는 더 높다면, 정상 %로 고려(치료 필요 없음)
      • 박출계수가 50%보다 낮다면, 심장은 대부분 충분한 산소가 없다거나, 관상동맥으로 흐르는 혈액이 부족하다는 의미
    • platelets: 혈소판 수 (kiloplatelets/mL)
      • 성인의 경우 혈액 1마이크로 리터 안에 약 15~40만 개의 혈소판을 보유하고 있다.
      • 혈소판의 수나 기능에 이상이 생기면 지혈 작용에 영향을 주어 출혈이 생길 수 있다.
    • serum_creatinine: 혈중 크레아틴 레벨 (mg/dL)
      • 크레아티닌은 근육에서 생성되는 노폐물 - 신장기능 평가의 주요 지표
      • 혈중 크레아티닌 농도의 정상범위는 0.50~1.4 mg/dL
    • serum_sodium: 혈중 나트륨 레벨 (mEq/L)
      • 고나트륨혈증은 보통 혈중농도가 145mEq/L 이상인 경우를 말함
      • 고나트륨혈증 발생기 심혈관계 이상 발생할 수 있음

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


Step 0. 의료 데이터셋에 대하여

✔️ 의료 데이터의 수집 관련 정책의 변화

  • 한국의 경우 한정
  • 2020.1월 데이터 3법 개정 이후 -> ‘가명정보’ 사용이 가능해짐
  • 개인 식별이 가능한 정보를 비식별화( de-identification)을 하여 일반적으로 사용가능한 데이터로 변화시킨 것

✔️ 의료 데이터 분석의 현재

  • 해외 (덴마크, 일본, 영국, 미국 등) 의 경우 과거부터 의료데이터 분석을 시행해옴.
  • 단순하게는 병원 공실률 개선을 위한 분석 부터 넓게는 질병발생 가능성 예측 까지 넓은 범주에서 의료 관련 데이터 분석을 시행
  • 하지만, 한국의 경우 데이터 3법 개정 이후부터 의료 관련 데이터 분석이 활발해지고 있는 상황.

✔️ 주요 판단지표 : Accuracy, Precision, Recall

  • Accuracy : 예측결과(pred)와 실제 값(label)이 같은 개수 / 전체 데이터
  • Precision(정밀도) : TP라고 예측했을때 실제로 TP일 확률 => [ TP/ (FP+TP) ] (FP 를 낮추는데 초점)
  • Recall (재현율) : 실제값이 Positive인 데이터 중에서 예측결과(Pred)와 실제 값(label)이 일치한 데이터의 비율 => [TP / (FN+TP) ] (FN을 낮추는데 초점)

💡 의료 데이터 분석에서는 **Recall이 가장 중요한 지표**

  • Why?
    • **Recall이 중요한 지표인 경우 : 실제로 Positive한 것을 Negative로 ‘잘못’ 판단했을때 그 영향력이 심각한 경우
    • 의료 데이터의 경우가 대표적인 예시
    • 실제 질병을 질병이 없다고 예측할 경우 그 잘못된 판단으로 환자의 생명 을 잃게 될 수 도 있기 때문.
  • 그래서 실제 의료 분석은 여러단계를 거치면서 분석한다.
    • 1) 비교적 간단하지만 정확성은 떨어지는 테스트
    • 2) ‘1)’보다는 좀 더 복잡하지만 정확성은 좀 더 높은 테스트
    • 3) ‘2)’보다는 좀 더 복잡하지만 정확성은 좀 더 높은 테스트
    • 와 같이 조금씩 더 복잡하면서 정확성이 더 높은 테스트를 순차적으로 시행
    • e.g) MRI -> 혈액 검사 -> 조직검사
  • 즉, 일반적으로 의료분석은 상대적으로 쉽게 얻을 수 있는 데이터들을 조합해서 차근 차근 분석 정확도를 높여가기 때문에 초반에는 Recall 이 높은 분석방법이 더 중요하다.

🤔 그래도 최종적으로는 Precision 과 Recall이 모두 높은 방법을 사용

  • Recall을 100%로 만드는 것을 추구하면 Precision은 0에 가까워 진다.
    • 가령, 무한대의 환자가 있다고 가정히할때
    • Recall을 100%로 만들려면 무한대의 모든 환자에게 병이 있다. (True) 라고 진단하면 된다.
    • 하지만 이 경우 Precision 을 측정하면 분모값이 환자수(무한대) 이기 때문에 0쪽으로 하락한다.
    • 그러므로, Recall은 95%이상을 추구하면서 Precision을 떨어지지 않도록 하는 것이 중요하다.



Step 1. 데이터셋 준비하기

1
2
3
4
5
6
7
8
9
10
11
12
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 컬럼 전체 확인 가능하도록 출력 범위 설정
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1000)

# 불필요한 경고 표시 생략
import warnings
warnings.filterwarnings(action = 'ignore')

Kaggle API 로 데이터 불러오기 및 셋팅

1
2
3
4
5
# 다운로드 패키지
import opendatasets as od

# os 명령 관련 패키지
import os

#username = gabesoon

#user key = f7dab1c856bc634fde741afaf75781ea

od.download(“https://www.kaggle.com/rashikrahmanpritom/heart-attack-analysis-prediction-dataset”)

1
os.listdir()
['.ipynb_checkpoints',
 'heart-attack-analysis-prediction-dataset',
 'heart_failure_clinical_records_dataset.csv',
 '[분류] 데이터 분석으로 심부전증을 예측하기 (Kaggle 데이터).ipynb']

Pandas 라이브러리로 csv파일 읽어들이기

1
2
# 데이터 로드
df = pd.read_csv('heart_failure_clinical_records_dataset.csv')
1
2
3
# 데이터 확인
print(df.info())
df.head()
<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
None
age anaemia creatinine_phosphokinase diabetes ejection_fraction high_blood_pressure platelets serum_creatinine serum_sodium sex smoking time DEATH_EVENT
0 75.0 0 582 0 20 1 265000.00 1.9 130 1 0 4 1
1 55.0 0 7861 0 38 0 263358.03 1.1 136 1 0 6 1
2 65.0 0 146 0 20 0 162000.00 1.3 129 1 1 7 1
3 50.0 1 111 0 20 0 210000.00 1.9 137 1 0 7 1
4 65.0 1 160 1 20 0 327000.00 2.7 116 0 0 8 1
  • 총 299명의 환자 데이터

  • Columns 는 13개

  • NaN값 있는 columns 없음


범주형 데이터 : count 확인

  • anaemia: 환자의 빈혈증 여부 (0: 정상, 1: 빈혈)

  • diabetes: 당뇨병 여부 (0: 정상, 1: 당뇨)

  • high_blood_pressure: 고혈압 여부 (0: 정상, 1: 고혈압)

  • sex: 성별 (0: 여성, 1: 남성)

  • smoking: 흡연 여부 (0: 비흡연, 1: 흡연)

  • DEATH_EVENT: 사망 여부 (0: 생존, 1: 사망)

수치형 데이터 : 전반적인 통계량 확인

  • age: 환자의 나이

  • creatinine_phosphokinase: 크레아틴키나제 검사 결과

  • ejection_fraction: 박출계수 (%)

  • platelets: 혈소판 수 (kiloplatelets/mL)

  • serum_creatinine: 혈중 크레아틴 레벨 (mg/dL)

  • serum_sodium: 혈중 나트륨 레벨 (mEq/L)

  • time: 관찰 기간 (일)

1
df.describe()
age anaemia creatinine_phosphokinase diabetes ejection_fraction high_blood_pressure platelets serum_creatinine serum_sodium sex smoking time DEATH_EVENT
count 299.000000 299.000000 299.000000 299.000000 299.000000 299.000000 299.000000 299.00000 299.000000 299.000000 299.00000 299.000000 299.00000
mean 60.833893 0.431438 581.839465 0.418060 38.083612 0.351171 263358.029264 1.39388 136.625418 0.648829 0.32107 130.260870 0.32107
std 11.894809 0.496107 970.287881 0.494067 11.834841 0.478136 97804.236869 1.03451 4.412477 0.478136 0.46767 77.614208 0.46767
min 40.000000 0.000000 23.000000 0.000000 14.000000 0.000000 25100.000000 0.50000 113.000000 0.000000 0.00000 4.000000 0.00000
25% 51.000000 0.000000 116.500000 0.000000 30.000000 0.000000 212500.000000 0.90000 134.000000 0.000000 0.00000 73.000000 0.00000
50% 60.000000 0.000000 250.000000 0.000000 38.000000 0.000000 262000.000000 1.10000 137.000000 1.000000 0.00000 115.000000 0.00000
75% 70.000000 1.000000 582.000000 1.000000 45.000000 1.000000 303500.000000 1.40000 140.000000 1.000000 1.00000 203.000000 1.00000
max 95.000000 1.000000 7861.000000 1.000000 80.000000 1.000000 850000.000000 9.40000 148.000000 1.000000 1.00000 285.000000 1.00000

1) 환자의 나이 분포는 40~95세 사이이고, 평균 연령은 60.83이다.

2) (0,1) 로된 범주형 데이터의 평균값을 보면 대부분 0.35~0.64 범주안에 있다 = 특별하게 불균형한 데이터는 없는 것으로 보인다.

(대부분의 데이터가 6:4 정도의 비율) - 용인 가능한 수준

단, 사망자 데이터는 0.32로 1/3이 생존이라 다소 불균형이다.

3) creatinine_phosphokinase 의 경우 min값은 23, 50%값은 250, 75%값은 582인데, max값은 7861로 이상치일 것으로 보인다. (제외 해야할 수도 있다.)

Step 2. EDA 및 데이터 기초 통계 분석


수치형 데이터별 Histogram - 데이터 빈도 with DEATH_EVENT

연령대별 사망수 분포

1
2
3
4
5
# seaborn의 histplot, jointplot, pairplot을 이용
# kde = 히스토그램 분포를 곡선으로 표현
# gray = 1 이지만 0과 겹친 데이터

sns.histplot(x='age', data=df, hue='DEATH_EVENT', kde=True)
<AxesSubplot:xlabel='age', ylabel='Count'>

  • age 데이터는 longtail 형태의 데이터(앞쪽 데이터의 빈도가 높고 뒤로 갈수록 빈도가 낮아지는 형태)

크레아틴키나제 검사 결과 (3000이하인 것만)

creatinine_phosphokinase: 크레아틴키나제 검사 결과

1) CK (크레아틴키나제)는 심장, 뇌, 골격근, 및 다른 조직에서 발견되는 효소

2) CK(크레아틴키나제) 농도는 근육 또는 심장 세포가 손상될 때에 올라간다.


  • creatinine_phosphokinase 는 describe()로 보았을때 outlier가 존재하는 데이터였다.

  • 실제로 값이 3000 이상인 경우 데이터의 count가 낮아서 전체 데이터를 시각화할 경우 데이터를 크게 보기가 어려웠다.

  • 빈도수가 많은 데이터를 살피기 위해 값이 3000이하 인 데이터들만 추려서 시각화 하였다.

1
2
3
a = df.loc[df['creatinine_phosphokinase'] < 3000]

sns.histplot(x= 'creatinine_phosphokinase', data=a, hue='DEATH_EVENT', kde=True)
<AxesSubplot:xlabel='creatinine_phosphokinase', ylabel='Count'>

ejection_fraction: 박출계수 (%) - 히스토그램

1
2
# bins 조절로 빈자리가 없도록 조절
sns.histplot(x='ejection_fraction', data=df, bins=13, hue='DEATH_EVENT', kde=True)
<AxesSubplot:xlabel='ejection_fraction', ylabel='Count'>

ejection_fraction: 박출계수 (%)

1) 맥박을 통해 심장에서 박출 되거나 펌프하는 혈액의 %량

2) 범주는 0-100%

3) 의학적으로 박출계수가 50% 또는 더 높다면, 정상 %로 고려(치료 필요 없음)

4) 박출계수가 50%보다 낮다면, 심장은 대부분 충분한 산소가 없다거나, 관상동맥으로 흐르는 혈액이 부족하다는 의미


  • 전반적으로 ejection_fraction이 낮은 사람의 사망수가 높고, 높은 사람은 사망수가 낮은 경향을 보인다.(도메인 지식과 동일)

  • 사망자수와 반비례 형태의 관계를 보이고, 각 빈도 별로 사망자수의 비율에 차이는 있지만 사망수는 전반적으로 높게 분포되어 있어 사망 여부 판별에 중요한 데이터로 사용할 수 있을 것으로 예상된다.

  • 특히 Precision을 높이기 위한 데이터로는 유용할 것이다.(사망한 경우자체가 많음-> TP증가 -> FP 낮아짐 -> Precision 낮아짐)

  • 다만 Recall을 높이는데는 크게 유용한 데이터는 아닐 것이다.

관찰일수(time) - 히스토그램

1
sns.histplot(x='time', data=df, hue='DEATH_EVENT', kde=True)
<AxesSubplot:xlabel='time', ylabel='Count'>

  • 관찰기간(time)에 대한 데이터가 초반에 많이 몰려있고, 관찰기간이 긴 데이터일 수록 분포가 떨어진다. (Gamma distribution)

  • 생존데이터의 경우 ‘쌍봉 형태’ 이다. 생존자에 대한 대부분의 데이터가 2곳에 집약되어 있는 것.

  • 사망자가 발생하면 관찰기간(time)의 값이 더 높아질수가 없는 구조이다.

  • 그러므로 time 데이터는 제외하고 학습을 진행하는 것이 바람직 하다.

ejection_fraction: 박출계수 (%) - Boxplot

1
2
3
4
# seaborn의 Boxplot 계열(boxplot(), violinplot(), swarmplot())을 사용

# ejection_fraction: 박출계수 (%)
sns.boxplot(x='DEATH_EVENT', y='ejection_fraction', data=df)
<AxesSubplot:xlabel='DEATH_EVENT', ylabel='ejection_fraction'>

[도메인 지식]

  • ejection_fraction이 50%보다 낮다면, 심장은 대부분 충분한 산소가 없다거나, 관상동맥으로 흐르는 혈액이 부족하다는 의미이다.

[Boxplot 분석]

  • 생존자의 ejection_fraction 평균은 약 40이다.

  • 사망자의 ejection_fraction 평균은 약 30 이다.


  • ejection_fraction은 도메인 지식 만큼은 아니겠지만, 사망자와 생존자간의 평균 차이가 상당하므로 사망자수 와의 상관성이 있는 주요 지표일 것으로 예상된다.

혈소판 수 (kiloplatelets/mL)

1
sns.histplot(x='platelets', data=df, hue='DEATH_EVENT')
<AxesSubplot:xlabel='platelets', ylabel='Count'>

platelets: 혈소판 수 (kiloplatelets/mL)

1) 성인의 경우 혈액 1마이크로 리터 안에 약 15~40만 개의 혈소판을 보유하고 있다.

2) 혈소판의 수나 기능에 이상이 생기면 지혈 작용에 영향을 주어 출혈이 생길 수 있다.

  • 혈소판 수의 데이터와 사망자수 간의 연관성이 보이지 않는다.

관찰 기간 (일)

1
sns.histplot(x='time', data=df, hue='DEATH_EVENT', kde=True)
<AxesSubplot:xlabel='time', ylabel='Count'>

  • 전반적으로 볼때, 관찰 기간이 짧을때 사망자수가 많지만 관찰 기간이 길어질 수록 사망자수가 적어지는 경향이 있다.

  • 다만, 관찰 기간이 중간치일때는 사망자수의 분포가 균일하다.

  • 그러므로 관찰 기간이 사망자수를 예측하는데 결정적인 지표라고 하기는 어렵다.

수치형 데이터별 Jointplot

[혈소판 수 (kiloplatelets/mL) x 크레아틴키나제 검사 결과] X 사망자 수

creatinine_phosphokinase: 크레아틴키나제 검사 결과

1) CK (크레아틴키나제)는 심장, 뇌, 골격근, 및 다른 조직에서 발견되는 효소

2) CK(크레아틴키나제) 농도는 근육 또는 심장 세포가 손상될 때에 올라간다.

1
2
#sns.jointplot(x='platelets', y='creatinine_phosphokinase', hue='DEATH_EVENT', data=df, alpha=0.4)
sns.jointplot(x='platelets', y='creatinine_phosphokinase',data=df, kind='kde', hue='DEATH_EVENT')
<seaborn.axisgrid.JointGrid at 0x1c97ad695b0>

[박출계수 (%) x 혈중 나트륨 레벨 (mEq/L)] X 사망자 수

1
sns.jointplot(x='ejection_fraction', y='serum_creatinine', data=df, hue='DEATH_EVENT')
<seaborn.axisgrid.JointGrid at 0x1c97be7b640>

범주형 데이터별 Boxplot

흡연자여부 별 박출계수

1
sns.boxplot(x='smoking', y='ejection_fraction', data=df)
<AxesSubplot:xlabel='smoking', ylabel='ejection_fraction'>

[도메인 지식]

  • ejection_fraction이 50%보다 낮다면, 심장은 대부분 충분한 산소가 없다거나, 관상동맥으로 흐르는 혈액이 부족하다는 의미이다.

[Boxplot 분석]

  • 흡연자와 비흡연자간 데이터의 평균은 큰 차이가 없다.

  • 흡연자의 경우 ejection_fraction 값의 범위가 좁다.

  • 흡연자의 ejection_fraction 최고치가 더 낮다.

  • 흡연자의 ejection_fraction 평균은 30 중반이다.

  • 비흡연자의 ejection_fraction 평균은 30 후반이다.


  • 흡연자의 ejection_fraction 평균은 정상치 아래이므로 심장에 문제가 있을 가능성이 있다고 예상할 수 있다.

  • 즉, 흡연여부는 심부전증에 간접적으로 유의미한 데이터일 가능성이 높다.

  • 다만 이 데이터에서는 비흡연자의 ejection_fraction도 평균이 40 아래이므로 도메인 지식만큼 심부전 발생여부와 강력한 연관성을 보이지 않을 가능성이 크다.

흡연여부 별 사망자수 x 혈소판 수 (kiloplatelets/mL)

1
sns.swarmplot(x='DEATH_EVENT', y='platelets', hue='smoking', data=df)
<AxesSubplot:xlabel='DEATH_EVENT', ylabel='platelets'>


Step 3. 데이터 전처리

데이터 표준화

1
from sklearn.preprocessing import StandardScaler
1
2
# 형태별 데이터 분류를 위해 columns 확인 - 복사해서 사용
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')
1
2
3
4
5
6
7
8
9
10
# 수치형 입력 데이터, 범주형 입력 데이터, 출력 데이터로 구분하기

#범주형
X_num = df[['age', 'creatinine_phosphokinase','ejection_fraction', 'platelets','serum_creatinine', 'serum_sodium']]

#수치형
X_cat = df[['anaemia', 'diabetes', 'high_blood_pressure', 'sex', 'smoking']]

#출력 (label)
y = df['DEATH_EVENT']
1
2
3
4
5
6
7
8
# 수치형 입력 데이터를 표준화 & 입력 데이터 통합
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)
1
X.head()
age creatinine_phosphokinase ejection_fraction platelets serum_creatinine serum_sodium anaemia diabetes high_blood_pressure sex smoking
0 1.192945 0.000166 -1.530560 1.681648e-02 0.490057 -1.504036 0 0 1 1 0
1 -0.491279 7.514640 -0.007077 7.535660e-09 -0.284552 -0.141976 0 0 0 1 0
2 0.350833 -0.449939 -1.530560 -1.038073e+00 -0.090900 -1.731046 0 0 0 1 1
3 -0.912335 -0.486071 -1.530560 -5.464741e-01 0.490057 0.085034 1 0 0 1 0
4 0.350833 -0.435486 -1.530560 6.517986e-01 1.264666 -4.682176 1 1 0 0 0

Train / Test Split

1
from sklearn.model_selection import train_test_split
1
2
3
4
# 학습 / 테스트용 데이터 분리 (7:3)
# 시간 순서 상관이 없으므로 shuffle=True 기본으로 사용

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1) 

Step 4. Classification 모델 학습

Logistic Regression 으로 기본적인 분류 분석 진행

Logistic Regression 모델 생성 & 학습

1
from sklearn.linear_model import LogisticRegression
1
2
3
4
5
6
# 모델 생성
# max_iter : Gradient Descent 수행 횟수
model_lr = LogisticRegression(max_iter=1000)

# 모델 학습
model_lr.fit(X_train, y_train)
LogisticRegression(max_iter=1000)

모델 결과 평가1

1
2
3
4
5
6
7
8
9
10
from sklearn.metrics import accuracy_score, precision_score , recall_score , confusion_matrix

def get_eval(y_test , pred):
    confusion = confusion_matrix( y_test, pred)
    accuracy = accuracy_score(y_test , pred)
    precision = precision_score(y_test , pred)
    recall = recall_score(y_test , pred)
    print('오차 행렬')
    print(confusion)
    print('정확도: {0:.2f}, 정밀도: {1:.2f}, 재현율: {2:.2f}'.format(accuracy , precision ,recall))
1
from sklearn.metrics import classification_report
1
2
3
4
5
# 예측값 pred에 할당
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

1
get_eval(y_test,pred)
오차 행렬
[[59  5]
 [17  9]]
정확도: 0.76, 정밀도: 0.64, 재현율: 0.35

Step 5. 앙상블로 머신러닝 결과 향상 시키기

추가모델 (XGBoost) 생성&학습

1
from xgboost import XGBClassifier
1
2
3
4
5
# 모델 생성
model_xgb = XGBClassifier()

# 모델 학습
model_xgb.fit(X_train, y_train)
[14:33:57] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: 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.
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=6, num_parallel_tree=1, random_state=0,
              reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=1,
              tree_method='exact', validate_parameters=1, verbosity=None)

추가 모델 학습 결과 평가

1
2
3
4
5
# 예측
pred = model_xgb.predict(X_test)

# 결과 출력
print(classification_report(y_test, pred))
              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

  • Precision과 Recall은 정확도를 측정하는 관점이 서로 다르다.

  • 때문에 어느 하나가 높다고 무조건 좋은 결과라 할 수 없다.

  • 이 둘을 모두 고려한 값이 f1-score 이다. (Precision & Recall의 조화평균)

1
get_eval(y_test,pred)
오차 행렬
[[56  8]
 [13 13]]
정확도: 0.77, 정밀도: 0.62, 재현율: 0.50

Step 6. 분석결과에서 Insight 찾기

Feature importance 확인하기

1
2
3
plt.barh(X.columns, model_xgb.feature_importances_) # 가로 출력 (barh)

plt.show()

  • serum_creatinine이 가장 중요한 요소이다.

  • ejection_fraction 이 두번째로 중요한 요소이다.

  • age가 세번째로 중요한 요소이다.

즉 심부전증 예측을 위해 위의 3 요소를 중점적으로 살펴 보는 것이 중요하다.

1
sns.jointplot(x='ejection_fraction', y='serum_creatinine', data=df, hue='DEATH_EVENT')
<seaborn.axisgrid.JointGrid at 0x25f9abd48b0>

  • 주요 변수로 판별된 2개의 feature에서 발생한 DEATH_EVENT가 서로 구분이 잘 된다.

  • 이 데이터들을 기반으로 심부전증 여부를 판별하게 된다면 분류를 잘 할 수 있을 것으로 예상 된다.


Step 7. 두개의 모델 학습 결과 심화 분석

Precision-Recall 커브 확인 - 주로 불균형한 데이터르 평가할때 사용

1
from sklearn.metrics import plot_precision_recall_curve
1
2
3
4
5
6
7
# 두 모델의 Precision-Recall 커브를 한번에 그리기
fig = plt.figure() #기본 figure 생성

ax = fig.gca() #여러개의 x를 사용해 하나의 캔버스에 그리기 위해 사용

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 0x25f9ad915e0>

  • 2개의 그래프를 비교하였을때 위쪽에 위치한 것이 더 정확도가 높은 모델이란 뜻이다.

  • 전반적으로 XGBClassifier이 더 높은 성능을 보여주고 있다.

ROC 커브 확인하기 - 주로 균형인 데이터를 평가할때 사용

1
from sklearn.metrics import plot_roc_curve
1
2
3
4
5
# 두 모델의 ROC 커브를 한번에 그리기
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 0x25f9adff4f0>

  • TP(True Positive) rate가 낮을때 얼마나 빨리 1에 가까워 지는지로 평가

  • AUC (Area Under Curve) : 그래프 아래의 영역의 넓이 값

  • 전반적으로 LogisticRegression이 더 높은 성능을 보여주고 있다.





결론

  • 주어진 데이터를 기반으로 보았을때 심부전증 예측에 중요한 요소는 serum_creatinine(혈중 크레아틴 레벨), ejection_fraction(박출계수), 그리고 age이다.

  • 실제로 도메인 지식과 비교봤을때 크레아틴 레벨이나, 박출계수는 심부전을 측정하는데 중요한 지표이다.

  • 그러므로 Classification을 적용한 데이터 분석으로 심부전증을 예측할 수 있음을 확인했다.

댓글남기기