피마 인디언 당뇨병 예측
- Pima Indian Diabetes 데이터셋을 이용해 당뇨병 여부를 판단하는 머신러닝 예측 모델 만들고 평가 지표 적용하기
- 데이터셋은 캐글에서 다운로드: https://www.kaggle.com/datasets/uciml/pima-indians-diabetes-database
- Pregnancies: 임신 횟수
- Glucose: 포도당 부하 검사 수치
- BloodPressure: 혈압
- SkinThickness: 팔 삼두근 뒤쪽 피하지방 측정값
- Insulin: 혈청 인슐린
- BMI: 체질량지수
- DiabetesPedigreeFunction: 당뇨 내력 가중치값
- Age: 나이
- Outcome: 클래스 결정값(0 또는 1)
라이브러리 정의, Outcome 클래스 결정값의 분포와 데이터 확인
In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score
from sklearn.metrics import f1_score, confusion_matrix, precision_recall_curve, roc_curve
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
diabetes_data = pd.read_csv('./diabetes.csv')
print(diabetes_data['Outcome'].value_counts())
diabetes_data.head(3)
0 500
1 268
Name: Outcome, dtype: int64
Out[1]:
Pregnancies | Glucose | BloodPressure | SkinThickness | Insulin | BMI | DiabetesPedigreeFunction | Age | Outcome | |
---|---|---|---|---|---|---|---|---|---|
0 | 6 | 148 | 72 | 35 | 0 | 33.6 | 0.627 | 50 | 1 |
1 | 1 | 85 | 66 | 29 | 0 | 26.6 | 0.351 | 31 | 0 |
2 | 8 | 183 | 64 | 0 | 0 | 23.3 | 0.672 | 32 | 1 |
In [2]:
diabetes_data.tail(3)
Out[2]:
Pregnancies | Glucose | BloodPressure | SkinThickness | Insulin | BMI | DiabetesPedigreeFunction | Age | Outcome | |
---|---|---|---|---|---|---|---|---|---|
765 | 5 | 121 | 72 | 23 | 112 | 26.2 | 0.245 | 30 | 0 |
766 | 1 | 126 | 60 | 0 | 0 | 30.1 | 0.349 | 47 | 1 |
767 | 1 | 93 | 70 | 31 | 0 | 30.4 | 0.315 | 23 | 0 |
In [3]:
diabetes_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 768 entries, 0 to 767
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Pregnancies 768 non-null int64
1 Glucose 768 non-null int64
2 BloodPressure 768 non-null int64
3 SkinThickness 768 non-null int64
4 Insulin 768 non-null int64
5 BMI 768 non-null float64
6 DiabetesPedigreeFunction 768 non-null float64
7 Age 768 non-null int64
8 Outcome 768 non-null int64
dtypes: float64(2), int64(7)
memory usage: 54.1 KB
- 유틸리티 함수인 get_clf_eval(), get_eval_by_threshold(), precision_recall_curve_plot() 이용하기 위해 정의해놓기
In [4]:
from sklearn.preprocessing import Binarizer
def get_clf_eval(y_test, pred, pred_proba):
confusion = confusion_matrix(y_test, pred)
accuracy = accuracy_score(y_test, pred)
precision = precision_score(y_test, pred)
recall = recall_score(y_test, pred)
f1 = f1_score(y_test, pred)
roc_auc = roc_auc_score(y_test, pred_proba)
print('오차 행렬')
print(confusion)
print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, F1:{3:.4f}, AUC: {4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))
def get_eval_by_threshold(y_test, pred_proba_c1, thresholds):
for custom_threshold in thresholds:
binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_c1)
custom_predict = binarizer.transform(pred_proba_c1)
print('임계값:', custom_threshold)
get_clf_eval(y_test, custom_predict)
def precision_recall_curve_plot(y_test, pred_proba_c1):
precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_c1)
plt.figure(figsize=(8,6))
threshold_boundary = thresholds.shape[0]
plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision')
plt.plot(thresholds, recalls[0:threshold_boundary], label='recall')
start, end = plt.xlim()
plt.xticks(np.round(np.arange(start, end, 0.1), 2))
plt.xlabel('Threshold value')
plt.ylabel('Precision and Recall value')
plt.legend()
plt.grid
plt.show()
로지스틱 회귀를 이용해 예측 모델 생성하기¶
In [5]:
#피처 데이터셋 X, 레이블 데이터셋 y 추출
#맨 끝이 Outcome 칼럼으로 레이블 값임. 칼럼위치 -1을 이용해 추출
X = diabetes_data.iloc[:, :-1]
y = diabetes_data.iloc[:, -1]
#데이터 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=156)
#모델 학습, 예측 및 평가 수행
lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train, y_train)
pred = lr_clf.predict(X_test)
pred_proba = lr_clf.predict_proba(X_test)[:, 1]
In [6]:
print(pred)
print(pred_proba)
[1 0 0 0 0 1 1 0 1 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0 1 1 1 0 0 1 1 0 0
0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0
1 0 0 1 1 0 0 1 0 0 0 0 1 0 0 1 0 1 1 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 1 1 1
0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0
1 0 1 0 0 0]
[0.66161545 0.35906789 0.26430486 0.4831761 0.05359471 0.72985332
0.71822556 0.25970607 0.57696165 0.13345736 0.49344637 0.1317572
0.17991477 0.36361204 0.53193516 0.70948673 0.15475004 0.21825156
0.05396137 0.27209314 0.1199557 0.52457922 0.16838136 0.23695204
0.13896197 0.1786304 0.09744204 0.16620221 0.85218295 0.72160175
0.61038863 0.14652302 0.11496817 0.7121391 0.73844305 0.39479094
0.25011011 0.29492893 0.23775066 0.06710913 0.22501017 0.35114593
0.44144232 0.12999205 0.61137784 0.19234862 0.26894362 0.13557014
0.31982305 0.14240034 0.2725478 0.31475503 0.20004802 0.80590787
0.83647868 0.73481657 0.48609845 0.38827433 0.2126252 0.27471867
0.65131067 0.43741855 0.69456443 0.05014045 0.28765753 0.50105802
0.15509111 0.42164105 0.19648963 0.12268663 0.07764339 0.47144932
0.22122335 0.33418318 0.77873243 0.25500032 0.14896907 0.54072946
0.80849492 0.21800817 0.19277318 0.72137011 0.36150112 0.17397468
0.16303694 0.2284312 0.60206371 0.30453727 0.30894484 0.51978728
0.18315886 0.79238499 0.76239924 0.26237589 0.06229849 0.14255476
0.20095728 0.34116145 0.31508377 0.13963845 0.95149276 0.22386202
0.16514606 0.2430681 0.75753368 0.40254244 0.13376945 0.14532763
0.94133841 0.60948592 0.66767609 0.1281338 0.15425852 0.39744724
0.22116264 0.06751979 0.14602773 0.14512257 0.30472082 0.89509316
0.17210175 0.75301699 0.41427206 0.23229809 0.16541407 0.18663864
0.12759509 0.33323124 0.31496644 0.39793867 0.75901186 0.41300834
0.62423273 0.05484324 0.26461527 0.49968527 0.14480081 0.53708566
0.07716849 0.44236771 0.11605912 0.33100698 0.16827676 0.71674815
0.12205671 0.72484388 0.47633891 0.32047145 0.80186174 0.15230448
0.60453253 0.14435682 0.49790914 0.16782522]
In [7]:
get_clf_eval(y_test, pred, pred_proba)
오차 행렬
[[87 10]
[26 31]]
정확도: 0.7662, 정밀도: 0.7561, 재현율: 0.5439, F1:0.6327, AUC: 0.8343
- 전체 데이터의 65%가 Negative이므로 정확도보다는 재현율 성능에 초점을 맞춰보자
- 정밀도 재현율 곡선을 보고 임곗값별 정밀도와 재현율 값의 변화 확인하기
In [8]:
pred_proba_c1 = lr_clf.predict_proba(X_test)[:, 1]
precision_recall_curve_plot(y_test, pred_proba_c1)
- 임계값을 0.42정도로 낮추면 정밀도와 재현율의 균형이 맞음, 그러나 둘 모두 0.7도 안되는 수치를 보임
- 임계값을 인위적으로 조적하기 전에 다시 데이터값 점검해보자
In [9]:
diabetes_data.describe()
Out[9]:
Pregnancies | Glucose | BloodPressure | SkinThickness | Insulin | BMI | DiabetesPedigreeFunction | Age | Outcome | |
---|---|---|---|---|---|---|---|---|---|
count | 768.000000 | 768.000000 | 768.000000 | 768.000000 | 768.000000 | 768.000000 | 768.000000 | 768.000000 | 768.000000 |
mean | 3.845052 | 120.894531 | 69.105469 | 20.536458 | 79.799479 | 31.992578 | 0.471876 | 33.240885 | 0.348958 |
std | 3.369578 | 31.972618 | 19.355807 | 15.952218 | 115.244002 | 7.884160 | 0.331329 | 11.760232 | 0.476951 |
min | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.078000 | 21.000000 | 0.000000 |
25% | 1.000000 | 99.000000 | 62.000000 | 0.000000 | 0.000000 | 27.300000 | 0.243750 | 24.000000 | 0.000000 |
50% | 3.000000 | 117.000000 | 72.000000 | 23.000000 | 30.500000 | 32.000000 | 0.372500 | 29.000000 | 0.000000 |
75% | 6.000000 | 140.250000 | 80.000000 | 32.000000 | 127.250000 | 36.600000 | 0.626250 | 41.000000 | 1.000000 |
max | 17.000000 | 199.000000 | 122.000000 | 99.000000 | 846.000000 | 67.100000 | 2.420000 | 81.000000 | 1.000000 |
In [10]:
plt.hist(diabetes_data['Glucose'], bins=100)
plt.show()
- describe를 보면 포도당 수치의 min이 0인 것이 말이 안되는데, 히스토그램을 그려보니 0이 상당히 존재함을 알 수 있음
- min값이 0으로 되어있는 피처에 대해 0값의 건수 및 전체 데이터 건수 대비 몇퍼센트 비율로 존재하는지 확인해보자 >> 확인할 피처는 Glucose, BloodPressure, SkinThickness, Insulin, BMI
In [11]:
#검사할 피처들 리스트 만들기
zero_features = ['Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI']
#전체 데이터 건수
total_count = diabetes_data['Glucose'].count()
#피처별로 반복하면서 데이터값이 0인 건수 추출, 퍼센트 계산
for feature in zero_features:
zero_count = diabetes_data[diabetes_data[feature]==0][feature].count()
print('{0} 0 건수는 {1}, 퍼센트는 {2:.2f}%'.format(feature, zero_count, 100*zero_count/total_count))
Glucose 0 건수는 5, 퍼센트는 0.65%
BloodPressure 0 건수는 35, 퍼센트는 4.56%
SkinThickness 0 건수는 227, 퍼센트는 29.56%
Insulin 0 건수는 374, 퍼센트는 48.70%
BMI 0 건수는 11, 퍼센트는 1.43%
- SkinThickness와 Insulin의 0 건수가 많음, 전체 데이터 건수가 많지 않기 때문에 일괄적으로 삭제해버리면 학습효과가 떨어질 것 같음
- 따라서, 위 피처들의 0값을 평균값으로 대체하기
In [12]:
mean_zero_features = diabetes_data[zero_features].mean()
diabetes_data[zero_features] = diabetes_data[zero_features].replace(0, mean_zero_features)
- 0값을 평균값으로 대체한 데이터셋에 피처 스케일링을 적용해 변환
- 로지스틱 회귀의 경우, 일반적으로 숫자 데이터에 스케일링을 적용하는 것이 좋음
In [13]:
X = diabetes_data.iloc[:, :-1]
y = diabetes_data.iloc[:, -1]
#피처 스케일링
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
#데이터분리
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=156, stratify=y)
#모델 학습, 예측 및 평가 수행
lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train, y_train)
pred = lr_clf.predict(X_test)
pred_proba = lr_clf.predict_proba(X_test)[:, 1]
In [14]:
print(pred)
print(pred_proba)
[0 0 0 1 0 1 1 0 0 0 1 0 0 0 0 0 1 0 1 1 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 1 0
0 1 1 0 0 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 1 0 1 0 0 0 0
1 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 0 1 1 0 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1
1 0 0 0 1 1 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1 0 1 0 0 0
1 0 0 0 1 0]
[0.03609018 0.20217293 0.15928588 0.5681113 0.41651923 0.75549138
0.83987164 0.15198534 0.19444674 0.1077338 0.66955827 0.39625109
0.04715651 0.39291971 0.26530665 0.09318057 0.79415787 0.08938344
0.5860422 0.80457251 0.41814913 0.34954916 0.06452018 0.3194815
0.30000267 0.10537022 0.92130861 0.75027822 0.17419735 0.88879611
0.23061778 0.23754691 0.11313996 0.15306837 0.10329326 0.74313694
0.2224308 0.20825111 0.85342706 0.66495884 0.41219643 0.23686881
0.70419015 0.13055228 0.34605328 0.0150281 0.46644956 0.08887057
0.48910823 0.33082282 0.31213555 0.91820103 0.89749958 0.06165143
0.12671445 0.43108455 0.07021933 0.35552394 0.3317995 0.39565923
0.18076256 0.83760011 0.33534618 0.62537857 0.3051479 0.1038554
0.24510902 0.7193746 0.14750218 0.77086348 0.06017212 0.33727392
0.12026259 0.45894577 0.7908756 0.30480354 0.0406929 0.06609392
0.11013565 0.25182157 0.11057751 0.0819377 0.82968245 0.17504021
0.29181251 0.34757337 0.4848255 0.78567839 0.05909875 0.0231481
0.04200661 0.62137772 0.8685517 0.24247788 0.29107973 0.69140305
0.22277791 0.19870694 0.03038902 0.81698674 0.88022367 0.41008537
0.41661078 0.33979481 0.10699201 0.14959661 0.30090962 0.46406975
0.04126547 0.10615885 0.75239478 0.53873613 0.14352952 0.19978459
0.38615466 0.53060808 0.87530484 0.12625729 0.27083764 0.97637068
0.53803937 0.4885671 0.71363481 0.65712739 0.25320725 0.22063939
0.11258663 0.06409598 0.23635966 0.11860331 0.02286245 0.0902012
0.36486885 0.09065155 0.04348144 0.63074776 0.76843124 0.76360014
0.03804969 0.11618351 0.20958995 0.49398293 0.72820896 0.1663991
0.77616029 0.06096428 0.13623837 0.01885686 0.81453194 0.45290759
0.38122112 0.03768592 0.59327439 0.12830766]
In [15]:
get_clf_eval(y_test, pred, pred_proba)
오차 행렬
[[90 10]
[21 33]]
정확도: 0.7987, 정밀도: 0.7674, 재현율: 0.6111, F1:0.6804, AUC: 0.8433
- 데이터 변환과 스케일링을 통해 성능 수치가 일정 수준 개선됨. 그러나 재현율 수치는 여전히 개선이 필요해보임
- 분류 결정 임곗값을 변화시키면서 재현율값의 성능 수치가 어느 정도 개선되는지 확인하기
오류발생!!
In [16]:
thresholds = [0.3, 0.33, 0.36, 0.39, 0.41, 0.45, 0.48, 0.50]
pred_proba = lr_clf.predict_proba(X_test)
get_eval_by_threshold(y_test, pred_proba[:, 1].reshape(-1, 1), thresholds)
임계값: 0.3
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_21932\1779271336.py in <module>
1 thresholds = [0.3, 0.33, 0.36, 0.39, 0.41, 0.45, 0.48, 0.50]
2 pred_proba = lr_clf.predict_proba(X_test)
----> 3 get_eval_by_threshold(y_test, pred_proba[:, 1].reshape(-1, 1), thresholds)
~\AppData\Local\Temp\ipykernel_21932\646892070.py in get_eval_by_threshold(y_test, pred_proba_c1, thresholds)
17 custom_predict = binarizer.transform(pred_proba_c1)
18 print('임계값:', custom_threshold)
---> 19 get_clf_eval(y_test, custom_predict)
20
21 def precision_recall_curve_plot(y_test, pred_proba_c1):
TypeError: get_clf_eval() missing 1 required positional argument: 'pred_proba'
- 정확도와 정밀도를 희생하고 재현율을 높이는데 가장 좋은 임곗값은 0.33으로 재현율이 0.7963임, 그러나 정밀도가 0.5972로 매우 맞아짐
- 임곗값 0.48이 전체적인 성능 평가 지표를 유지하면서 재현율을 약간 향상시키는 좋음 임곗값으로 보임
- 앞서 학습된 로지스틱회귀모델을 이용해 임곗값을 0.48로 낮추고 다시 예측해보자 >> predict 메서드에서는 임곗값 변환이 안되므로 별도의 로직을 이용해야 함(Binarizer를 이용)
In [17]:
binarizer = Binarizer(threshold=0.48)
#위에서 구한 lr_clf의 predict_proba 예측 확률 array에서 1에 해당하는 칼럼값을 Binarizer 변환
pred_th_048 = binarizer.fit_transform(pred_proba[:, 1].reshape(-1, 1))
get_clf_eval(y_test, pred_th_048, pred_proba[:, 1])
오차 행렬
[[88 12]
[19 35]]
정확도: 0.7987, 정밀도: 0.7447, 재현율: 0.6481, F1:0.6931, AUC: 0.8433