Open In Colab

데이터 불러오기

from google.colab import drive
drive.mount('/content/drive')
Mounted at /content/drive
import pandas as pd
data = pd.read_csv("/content/drive/MyDrive/heart.csv")

Verson 1. 심플한 로지스틱 회귀 모형

데이터 이해

df = data.copy()
df.head()
age sex cp trtbps chol fbs restecg thalachh exng oldpeak slp caa thall output
0 63 1 3 145 233 1 0 150 0 2.3 0 0 1 1
1 37 1 2 130 250 0 1 187 0 3.5 0 0 2 1
2 41 0 1 130 204 0 0 172 0 1.4 2 0 2 1
3 56 1 1 120 236 0 1 178 0 0.8 2 0 2 1
4 57 0 0 120 354 0 1 163 1 0.6 2 0 2 1

디폴트 값은 5입니다.

df.columns
Index(['age', 'sex', 'cp', 'trtbps', 'chol', 'fbs', 'restecg', 'thalachh',
       'exng', 'oldpeak', 'slp', 'caa', 'thall', 'output'],
      dtype='object')
df.columns.values.tolist()
['age',
 'sex',
 'cp',
 'trtbps',
 'chol',
 'fbs',
 'restecg',
 'thalachh',
 'exng',
 'oldpeak',
 'slp',
 'caa',
 'thall',
 'output']

컬럼은 이런 방식으로 확인할 수 있습니다.

밑에 DataFrame.columns.values.tolist() 함수는 컬럼 추출 중 가장 런타임이 빠르다고 합니다.

print('Shape is',df.shape)
Shape is (303, 14)

303개 데이터, 14개 특성값이 있습니다.

df.isnull().sum()
age         0
sex         0
cp          0
trtbps      0
chol        0
fbs         0
restecg     0
thalachh    0
exng        0
oldpeak     0
slp         0
caa         0
thall       0
output      0
dtype: int64
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 303 entries, 0 to 302
Data columns (total 14 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   age       303 non-null    int64  
 1   sex       303 non-null    int64  
 2   cp        303 non-null    int64  
 3   trtbps    303 non-null    int64  
 4   chol      303 non-null    int64  
 5   fbs       303 non-null    int64  
 6   restecg   303 non-null    int64  
 7   thalachh  303 non-null    int64  
 8   exng      303 non-null    int64  
 9   oldpeak   303 non-null    float64
 10  slp       303 non-null    int64  
 11  caa       303 non-null    int64  
 12  thall     303 non-null    int64  
 13  output    303 non-null    int64  
dtypes: float64(1), int64(13)
memory usage: 33.3 KB

글쓴이는 윗방식으로 null값 유무를 체크했습니다.

그러나 df.info() 방식이 여러가지 정보를 같이 줘 더 효율적입니다.

df.describe()
age sex cp trtbps chol fbs restecg thalachh exng oldpeak slp caa thall output
count 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000
mean 54.366337 0.683168 0.966997 131.623762 246.264026 0.148515 0.528053 149.646865 0.326733 1.039604 1.399340 0.729373 2.313531 0.544554
std 9.082101 0.466011 1.032052 17.538143 51.830751 0.356198 0.525860 22.905161 0.469794 1.161075 0.616226 1.022606 0.612277 0.498835
min 29.000000 0.000000 0.000000 94.000000 126.000000 0.000000 0.000000 71.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 47.500000 0.000000 0.000000 120.000000 211.000000 0.000000 0.000000 133.500000 0.000000 0.000000 1.000000 0.000000 2.000000 0.000000
50% 55.000000 1.000000 1.000000 130.000000 240.000000 0.000000 1.000000 153.000000 0.000000 0.800000 1.000000 0.000000 2.000000 1.000000
75% 61.000000 1.000000 2.000000 140.000000 274.500000 0.000000 1.000000 166.000000 1.000000 1.600000 2.000000 1.000000 3.000000 1.000000
max 77.000000 1.000000 3.000000 200.000000 564.000000 1.000000 2.000000 202.000000 1.000000 6.200000 2.000000 4.000000 3.000000 1.000000

데이터를 보면 어느정도 스케일링이 필요하다는 것을 알 수 있습니다.

특성 스케일링

df['age'] = df['age']/max(df['age'])
df['cp'] = df['cp']/max(df['cp'])
df['trtbps'] = df['trtbps']/max(df['trtbps'])
df['chol'] = df['chol']/max(df['chol'])
df['thalachh'] = df['thalachh']/max(df['thalachh'])
df.describe()
age sex cp trtbps chol fbs restecg thalachh exng oldpeak slp caa thall output
count 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000 303.000000
mean 0.706056 0.683168 0.322332 0.658119 0.436638 0.148515 0.528053 0.740826 0.326733 1.039604 1.399340 0.729373 2.313531 0.544554
std 0.117949 0.466011 0.344017 0.087691 0.091898 0.356198 0.525860 0.113392 0.469794 1.161075 0.616226 1.022606 0.612277 0.498835
min 0.376623 0.000000 0.000000 0.470000 0.223404 0.000000 0.000000 0.351485 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 0.616883 0.000000 0.000000 0.600000 0.374113 0.000000 0.000000 0.660891 0.000000 0.000000 1.000000 0.000000 2.000000 0.000000
50% 0.714286 1.000000 0.333333 0.650000 0.425532 0.000000 1.000000 0.757426 0.000000 0.800000 1.000000 0.000000 2.000000 1.000000
75% 0.792208 1.000000 0.666667 0.700000 0.486702 0.000000 1.000000 0.821782 1.000000 1.600000 2.000000 1.000000 3.000000 1.000000
max 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 2.000000 1.000000 1.000000 6.200000 2.000000 4.000000 3.000000 1.000000

이전과 달리 특성 스케일이 확실히 비슷해졌습니다.

데이터 모델링

from sklearn.model_selection import train_test_split

#splitting data into training data and testing data
X_train, X_test, y_train, y_test = train_test_split(
    df.drop(['output'], axis=1),
    df.output,
    test_size= 0.2,  # 20% test data & 80% train data
    random_state=0,
    stratify=df.output
)

stratify 속성 => y값의 공평한 분배를 위해 사용하는 속성입니다.

from sklearn.linear_model import LogisticRegression
clf = LogisticRegression()
clf.fit(X_train, y_train)

from sklearn.metrics import accuracy_score

Y_pred = clf.predict(X_test)
acc=accuracy_score(y_test, Y_pred)
print('Accuracy is',round(acc,2)*100,'%')
Accuracy is 89.0 %

로지스틱 회귀 모형을 별다른 튜닝 없이 사용했습니다.

정확도 측면에서만 보면 캐글에 있는 다른 코드와 별반 다르지 않습니다.

Verson 2. 심플한 딥러닝 모형

데이터 이해2

df = data.copy()
df.output.value_counts()
1    165
0    138
Name: output, dtype: int64

이전 모델에서 생략(?)된 부분인거 같은데 1과 0 값의 비율이 조금 차이가 있습니다.

df.corr().abs()['output'].sort_values(ascending = False)
output      1.000000
exng        0.436757
cp          0.433798
oldpeak     0.430696
thalachh    0.421741
caa         0.391724
slp         0.345877
thall       0.344029
sex         0.280937
age         0.225439
trtbps      0.144931
restecg     0.137230
chol        0.085239
fbs         0.028046
Name: output, dtype: float64

Y값과의 상관계수가 어느정도 되는지 확인해보았습니다.

데이터 모델링2

from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

X = df.drop('output', axis = 1)
y = df['output']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 42)
X_train.shape
(242, 13)
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)

여기서는 StandardScaler를 사용해 스케일링을 했습니다.

평균 0, 분산 1로 조정합니다. 이 스케일링은 이상치가 있을때 잘 작용하지 않을 수 있습니다.

from tensorflow import keras

model = keras.Sequential(
    [
        keras.layers.Dense(
            256, activation="relu", input_shape=[13]
        ),
        keras.layers.Dense(515, activation="relu"),
        keras.layers.Dropout(0.3),
        keras.layers.Dense(50, activation="relu"),
        keras.layers.Dropout(0.3),
        keras.layers.Dense(1, activation="sigmoid"),
    ]
)
model.summary()
Model: "sequential_5"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_20 (Dense)             (None, 256)               3584      
_________________________________________________________________
dense_21 (Dense)             (None, 515)               132355    
_________________________________________________________________
dropout_10 (Dropout)         (None, 515)               0         
_________________________________________________________________
dense_22 (Dense)             (None, 50)                25800     
_________________________________________________________________
dropout_11 (Dropout)         (None, 50)                0         
_________________________________________________________________
dense_23 (Dense)             (None, 1)                 51        
=================================================================
Total params: 161,790
Trainable params: 161,790
Non-trainable params: 0
_________________________________________________________________

활성화 함수로 제일 많이 사용하는 relu와 sigmoid함수를 사용했습니다.

relu함수 : 입력이 양수일 경우 그대로 반환, 음수일경우 0으로 만듭니다.

sigmoid함수 : 1 / (1 + e^z) 함수. 값을 0에서 1 사이로 변환합니다.

첫번째 구간에 아웃풋 값을 256개 주었는데, 변수값이 13개임으로 모수가 14개입니다.

그래서 256*14 = 3584개 파라미터가 나오게 된 것입니다.

중간에 있는 드롭아웃은 일정 비율만큼 뉴런을 랜덤하게 꺼서 과대적합을 막는 역할을 합니다.

model.compile(optimizer = 'Adam', loss = 'binary_crossentropy', metrics = ['binary_accuracy'])

early_stopping = keras.callbacks.EarlyStopping( patience = 20, min_delta = 0.001,
                                               restore_best_weights =True )
history = model.fit(
    X_train, y_train,
    validation_data=(X_test, y_test),
    batch_size=15,
    epochs=50,
    callbacks = [early_stopping],
    verbose=1, 
)
Epoch 1/50
17/17 [==============================] - 1s 15ms/step - loss: 0.5500 - binary_accuracy: 0.7190 - val_loss: 0.3814 - val_binary_accuracy: 0.8852
Epoch 2/50
17/17 [==============================] - 0s 6ms/step - loss: 0.3823 - binary_accuracy: 0.8347 - val_loss: 0.3797 - val_binary_accuracy: 0.8852
Epoch 3/50
17/17 [==============================] - 0s 6ms/step - loss: 0.3354 - binary_accuracy: 0.8719 - val_loss: 0.4391 - val_binary_accuracy: 0.8197
Epoch 4/50
17/17 [==============================] - 0s 6ms/step - loss: 0.3017 - binary_accuracy: 0.8802 - val_loss: 0.4147 - val_binary_accuracy: 0.8689
Epoch 5/50
17/17 [==============================] - 0s 6ms/step - loss: 0.2589 - binary_accuracy: 0.9091 - val_loss: 0.4388 - val_binary_accuracy: 0.8689
Epoch 6/50
17/17 [==============================] - 0s 6ms/step - loss: 0.2579 - binary_accuracy: 0.9256 - val_loss: 0.4795 - val_binary_accuracy: 0.8525
Epoch 7/50
17/17 [==============================] - 0s 7ms/step - loss: 0.2019 - binary_accuracy: 0.9256 - val_loss: 0.4895 - val_binary_accuracy: 0.8689
Epoch 8/50
17/17 [==============================] - 0s 6ms/step - loss: 0.1889 - binary_accuracy: 0.9298 - val_loss: 0.5359 - val_binary_accuracy: 0.8361
Epoch 9/50
17/17 [==============================] - 0s 6ms/step - loss: 0.1887 - binary_accuracy: 0.9215 - val_loss: 0.5324 - val_binary_accuracy: 0.8525
Epoch 10/50
17/17 [==============================] - 0s 6ms/step - loss: 0.1578 - binary_accuracy: 0.9545 - val_loss: 0.5441 - val_binary_accuracy: 0.8689
Epoch 11/50
17/17 [==============================] - 0s 6ms/step - loss: 0.1686 - binary_accuracy: 0.9215 - val_loss: 0.6338 - val_binary_accuracy: 0.8689
Epoch 12/50
17/17 [==============================] - 0s 6ms/step - loss: 0.1448 - binary_accuracy: 0.9504 - val_loss: 0.6872 - val_binary_accuracy: 0.8197
Epoch 13/50
17/17 [==============================] - 0s 6ms/step - loss: 0.1065 - binary_accuracy: 0.9628 - val_loss: 0.7682 - val_binary_accuracy: 0.8197
Epoch 14/50
17/17 [==============================] - 0s 7ms/step - loss: 0.0879 - binary_accuracy: 0.9835 - val_loss: 0.8583 - val_binary_accuracy: 0.8197
Epoch 15/50
17/17 [==============================] - 0s 6ms/step - loss: 0.0877 - binary_accuracy: 0.9711 - val_loss: 0.9300 - val_binary_accuracy: 0.8361
Epoch 16/50
17/17 [==============================] - 0s 6ms/step - loss: 0.0688 - binary_accuracy: 0.9835 - val_loss: 0.9281 - val_binary_accuracy: 0.8361
Epoch 17/50
17/17 [==============================] - 0s 8ms/step - loss: 0.0615 - binary_accuracy: 0.9835 - val_loss: 0.9688 - val_binary_accuracy: 0.8361
Epoch 18/50
17/17 [==============================] - 0s 6ms/step - loss: 0.0496 - binary_accuracy: 0.9835 - val_loss: 1.0818 - val_binary_accuracy: 0.8197
Epoch 19/50
17/17 [==============================] - 0s 6ms/step - loss: 0.0915 - binary_accuracy: 0.9628 - val_loss: 1.3326 - val_binary_accuracy: 0.8525
Epoch 20/50
17/17 [==============================] - 0s 6ms/step - loss: 0.0953 - binary_accuracy: 0.9669 - val_loss: 1.1602 - val_binary_accuracy: 0.8525
Epoch 21/50
17/17 [==============================] - 0s 7ms/step - loss: 0.0366 - binary_accuracy: 0.9959 - val_loss: 1.1617 - val_binary_accuracy: 0.8525
Epoch 22/50
17/17 [==============================] - 0s 6ms/step - loss: 0.0407 - binary_accuracy: 0.9876 - val_loss: 1.2300 - val_binary_accuracy: 0.8361
model.evaluate(X_test, y_test)
2/2 [==============================] - 0s 7ms/step - loss: 0.3797 - binary_accuracy: 0.8852
[0.3796648383140564, 0.8852459192276001]
predictions =(model.predict(X_test)>0.5).astype("int32")
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
accuracy_score(y_test, predictions)
0.8852459016393442

아까 결과와 비슷한 수치를 보입니다.

print(classification_report(y_test, predictions))
              precision    recall  f1-score   support

           0       0.00      0.00      0.00        29
           1       0.52      1.00      0.69        32

    accuracy                           0.52        61
   macro avg       0.26      0.50      0.34        61
weighted avg       0.28      0.52      0.36        61

classification_report 함수가 상당히 유용한 걸 알 수있습니다.

한번에 정밀도, 재현율, f1-score 값 까지 보여줍니다.

느낀점

분류에 기본적인 로지스틱 회귀모형과 단순한 딥러닝 코드를 따라해봤습니다.

특히 딥러닝 부분에 경우 정말 기본적인 것밖에 몰라 코드 해석에 시간이 많이 걸렸네요.

여러가지로 코드를 만져가며 느낀점은 이번 데이터에 경우 스케일링이 많이 중요한 것 같습니다.

스케일링 종류에 따라서 정확도 값이 크게 변하는 것을 관찰했습니다.

특히 트리기반 부스팅 모델이 아니라 더 그런 것 같습니다.

너무 복잡한 모델을 급하게 이해하기 보다, 이해할 수 있는 모델을 관찰하며 데이터 분석은 어떤 과정으로 하는가를 살펴봤습니다.