당뇨병 위험 분류 예측 프로젝트(이진분류) 5 : 결측치 그룹에 대한 데이터 전처리 및 피처 엔지니어링
1. 인슐린 결측치 데이터(train_abnormal) 추출
features_org = list(train.columns)[1:-1]
# train 데이터에서 Insulin이 0(결측치)인 데이터 추출
train_abnormal = train.copy()
train_abnormal = train_abnormal.loc[train_abnormal['Insulin'] == 0]
test_abnormal = test.copy()
test_abnormal = test_abnormal.loc[test_abnormal['Insulin'] == 0]
display(train_abnormal.head(5))
2. train_abnormal에 대한 기본 교차 검증 성능 확인
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score, cross_validate, KFold
train_abnormal_x = train_abnormal[features_org]
train_abnormal_y = train_abnormal['Outcome']
kf = KFold(n_splits=4, shuffle=True, random_state=42)
RF_model_abnormal = RandomForestClassifier(random_state = 42)
cv_result_abnormal = cross_validate(RF_model_abnormal, train_abnormal_x, train_abnormal_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1'])
df_cv_result_abnormal = pd.DataFrame(cv_result_abnormal, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1'])
display(cv_result_abnormal)
display(df_cv_result_abnormal)
display(df_cv_result_abnormal.describe().loc['mean',:].to_frame().T)
3.인슐린 결측 데이터 (train_abnormal)에 'SkinThickness' 결측치 처리
데이터의 'SkinThickness' 열에는 결측치로 간주되는 0 값이 포함되어 있습니다.
결측치가 아닌 데이터를 사용하여 'SkinThickness'를 예측하는 모델을 학습하기 위해, 먼저 결측치가 아닌 행을 추출합니다
from sklearn.preprocessing import StandardScaler
train_applied = train.copy()
# 'SkinThickness' 결측치 대체에 사용할 feature 선택
features_for_skin = ['Pregnancies', 'Glucose', 'BloodPressure', 'BMI', 'DiabetesPedigreeFunction', 'Age']
# 'SkinThickness' 값이 0인 데이터와 그렇지 않은 데이터로 분리
train_normal_skin = train_applied[train_applied['SkinThickness'] != 0]
####################################################################
# 인슐린 결측치가 없는 train_normal 데이터에서 SVC 모델에 의한 학습
####################################################################
train_normal_skin_x = train_normal_skin[features_for_skin]
train_normal_skin_y = train_normal_skin['SkinThickness']
scaler_reg_train = StandardScaler()
train_normal_skin_x_scaled = scaler_reg_train.fit_transform(train_normal_skin_x)
display(train[features_for_skin].head(5))
display(pd.DataFrame(train_normal_skin_x_scaled, columns = features_for_skin).head(5))
4. 인슐린 결측 데이터 (train_abnormal)에 'SkinThickness' 결측치 예측
from sklearn.svm import SVR
svm_model = SVR()
svm_model.fit(train_normal_skin_x_scaled, train_normal_skin_y)
####################################################################
# 학습된 SVC 모델로 'SkinThickness' 결측치 데이터를 예측 대체
####################################################################
train_missing_skin = train_applied[train_applied['SkinThickness'] == 0]
train_missing_skin_x = train_missing_skin[features_for_skin]
train_missing_skin_x_scaled = scaler_reg_train.transform(train_missing_skin_x)
# 결측치가 있는 데이터에 대한 'SkinThickness' 값을 예측
predicted_skin = svm_model.predict(train_missing_skin_x_scaled)
# 예측된 값으로 'SkinThickness' 결측치 대체
train_missing_skin = train_missing_skin.copy()
train_missing_skin.loc[:,'SkinThickness'] = predicted_skin
####################################################################
# 처리된 데이터를 병합
####################################################################
train_dealed_missing_skin = pd.concat([train_normal_skin, train_missing_skin]).sort_index()
display(f"train 결측치 개수 : {len(train[train['SkinThickness'] == 0])}")
display(f"train_dealed_missing_skin 결측치 개수 : {len(train_dealed_missing_skin[train_dealed_missing_skin['SkinThickness'] == 0])}")
'train 결측치 개수 : 195'
'train_dealed_missing_skin 결측치 개수 : 0'
5. 'SkinThickness' 결측치 처리후 교차 검증 성능 확인
# 인슐인 결측치 데이터셋 추출
train_abnormal_prep = train_dealed_missing_skin[train_dealed_missing_skin['Insulin'] == 0]
train_abnormal_prep_x = train_abnormal_prep[features_org]
kf = KFold(n_splits=4, shuffle=True, random_state=42)
# RandomForestClassifier로 교차 검증 진행
RF_model_abnormal_prep = RandomForestClassifier(random_state = 42)
cv_result_abnormal_prep = cross_validate(RF_model_abnormal_prep, train_abnormal_prep_x, train_abnormal_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1'])
df_cv_result_abnormal_prep = pd.DataFrame(cv_result_abnormal_prep, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1'])
display(df_cv_result_abnormal_prep)
display(df_cv_result_abnormal_prep.describe().loc['mean',:].to_frame().T)
display(f"train_abnormal_prep : {list(train_abnormal_prep.columns)}")
※ 결과 해석
train_abnormal에 대한 교차 검증 성능 보다 향상되었음을 알 수 있습니다
평균 정확도(Accuracy): 73.27% -> 74.5%
평균 정밀도(precision): 67.71% -> 69.6%
평균 재현율(recall)은 60.44% -> 62.1%
평균 F1 점수 62.67% -> 64.5%
6. 'SkinThickness' 결측치 처리후 교차 검증 성능 향상 근거 확인
from scipy.stats import pointbiserialr
correlation_abnormal_lst, correlation_abnormal_prep_lst = [], []
p_value_abnormal_lst, p_value_abnormal_prep_lst = [], []
feature_lst = train.columns[1:-1].to_list()
# 점 이연 상관계수 계산 및 출력
for feature in feature_lst:
correlation_abnormal, p_value_abnormal = pointbiserialr(train_abnormal[feature], train_abnormal['Outcome'])
correlation_abnormal_lst.append(correlation_abnormal)
p_value_abnormal_lst.append(p_value_abnormal)
# 점 이연 상관계수 계산 및 출력
for feature in feature_lst:
correlation_abnormal_prep, p_value_abnormal_prep = pointbiserialr(train_abnormal_prep[feature], train_abnormal_prep['Outcome'])
correlation_abnormal_prep_lst.append(correlation_abnormal_prep)
p_value_abnormal_prep_lst.append(p_value_abnormal_prep)
# 데이터프레임 생성
correlation_dict = {'Feature': feature_lst,
'correlation_abnormal': correlation_abnormal_lst,
'p_value_abnormal' : p_value_abnormal_lst,
'correlation_abnormal_prep': correlation_abnormal_prep_lst,
'p_value_abnormal_prep' : p_value_abnormal_prep_lst}
correlation_df = pd.DataFrame(correlation_dict)
display(correlation_df)
import matplotlib.pyplot as plt
import seaborn as sns
# Seaborn barplot
plt.figure(figsize=(12, 6))
plt.subplot(2,2,1)
sns.barplot(x='Feature', y='correlation_abnormal', data=correlation_df)
plt.gca().set_title("Point Biserial Correlation [train_abnorma]")
plt.gca().set_xticklabels(feature_lst, rotation=30)
plt.subplot(2,2,2)
sns.barplot(x='Feature', y='p_value_abnormal', data=correlation_df)
plt.gca().set_xticklabels(feature_lst, rotation=30)
plt.gca().set_title("p_value [train_abnorma]")
plt.subplot(2,2,3)
sns.barplot(x='Feature', y='correlation_abnormal_prep', data=correlation_df)
plt.gca().set_title("Point Biserial Correlation [train_abnormal_prep]")
plt.gca().set_xticklabels(feature_lst, rotation=30)
plt.subplot(2,2,4)
sns.barplot(x='Feature', y='p_value_abnormal_prep', data=correlation_df)
plt.gca().set_xticklabels(feature_lst, rotation=30)
plt.gca().set_title("p_value [train_abnormal_prep]")
plt.tight_layout()
plt.show()
※ 결과 해석
p-value가 0.05보다 작은 경우, 상관계수가 통계적으로 유의하다고 간주됩니다.
'SkinThickness' 결측치가 SVR (Support Vector Regression) 모델에 의해 예측 대체되고 난 이후, 타겟변수인 'Outcome'과의 점 이연상관관계가 크게 높아졌고, p-value가 낮아졌음을 알 수 있습니다.
이는 수행한 결측치 처리가 매우 유용한 처리였음을 알 수 있고, 교차 검증 성능이 향상된 것에 대한 납득 가능한 근거입니다.
7. train_abnormal 데이터에 feature 조합 통한 새로운 feature 생성 하기
'Age', 'Pregnancies' 조합으로 피쳐 연산 방법을 이용하여 새로운 feature 생성 : 'Age'와 'Pregnancies'는 상관 관계가 상대적으로 높은 피처쌍으로, 두 변수의 조합을 통해 새로운 피처를 생성하여 통해 타겟 변수와의 관계나 새로운 패턴을 발견할 수도 있습니다.
from sklearn.ensemble import RandomForestClassifier
# 피쳐 후보 생성
train_abnormal_try = train_abnormal_prep.copy()
train_abnormal_try['Pregnancies_Age_Diff'] = train_abnormal_try['Pregnancies'] - train_abnormal_try['Age']
train_abnormal_try['Pregnancies_Age_Sum'] = train_abnormal_try['Pregnancies'] + train_abnormal_try['Age']
train_abnormal_try['Pregnancies_Age_Ratio'] = train_abnormal_try['Pregnancies'] / train_abnormal_try['Age']
train_abnormal_try_x = train_abnormal_try.drop('Outcome', axis=1)
features_to_evaluate = ['Pregnancies_Age_Diff', 'Pregnancies_Age_Sum', 'Pregnancies_Age_Ratio']
rf_model = RandomForestClassifier(random_state = 42)
kf = KFold(n_splits=4, shuffle=True, random_state=42)
cv_scores = {}
for feature in features_to_evaluate:
train_abnormal_add_x = train_abnormal_prep[features_org].copy()
train_abnormal_add_x[feature] = train_abnormal_try[feature]
#display(train_abnormal_add_x[feature].isnull().sum())
scores = cross_val_score(rf_model, train_abnormal_add_x, train_abnormal_y, cv=kf, scoring='accuracy')
cv_scores[feature] = scores.mean()
display(f"accuracy : {cv_scores}")
"accuracy : {'Pregnancies_Age_Diff': 0.7264636075949367, 'Pregnancies_Age_Sum': 0.7327531645569619, 'Pregnancies_Age_Ratio': 0.7265822784810126}"
※ 결과 해석
교차 검증 결과, 세 가지 새로운 피처 중에서 Pregnancies_Age_Ratio 피처가 가장 높은 정확도를 보였습니다.
이는 해당 피처가 다른 피처들에 비해 모델의 성능 향상에 더 큰 기여를 할 수 있는 가능성이 있다는 것을 의미합니다.
그러나 'SkinThickness' 결측치를 처리한 후의 교차 검증 성능(정확도 74.54%)에 비해 모든 생성된 feature 추가시의 성능이 오히려 떨어졌기 때문에, train_abnormal_prep에는 이 피처를 추가하지 않기로 합니다.
8. train_abnormal 데이터에 feature 그룹화 조합 통한 새로운 feature 생성
'SkinThickness'와 'BMI'에 대한 새로운 피처들을 생성하고 랜덤 포레스트 분류기를 사용하여 각 피처가 모델의 정확도에 어떤 영향을 미치는지 평가합니다.
'SkinThickness'와 'BMI'의 차이, 합계, 비율을 나타내는 새로운 피처들을 생성하여 분석합니다. 또한 'BMI'의 결측치를 평균 값으로 대체하는 처리도 수행합니다.
# 피쳐 후보 생성
train_abnormal_try = train_abnormal_prep.copy()
train_abnormal_try['Skin_BMI_Diff'] = train_abnormal_try['SkinThickness'] - train_abnormal_try['BMI']
train_abnormal_try['Skin_BMI_Sum'] = train_abnormal_try['SkinThickness'] + train_abnormal_try['BMI']
bmi_mean = train_abnormal_try[train_abnormal_try['BMI'] !=0]['BMI'].mean()
train_abnormal_try['BMI'] = train_abnormal_try['BMI'].replace(0, bmi_mean)
train_abnormal_try['Skin_BMI_Ratio'] = train_abnormal_try['SkinThickness'] / train_abnormal_try['BMI']
train_abnormal_try_x = train_abnormal_try.drop('Outcome', axis=1)
features_to_evaluate = ['Skin_BMI_Diff', 'Skin_BMI_Sum', 'Skin_BMI_Ratio']
rf_model = RandomForestClassifier(random_state = 42)
# a) 교차 검증 성능 비교
kf = KFold(n_splits=4, shuffle=True, random_state=42)
#display(train_abnormal_y)
cv_scores = {}
for feature in features_to_evaluate:
train_abnormal_add_x = train_abnormal_prep[features_org].copy()
train_abnormal_add_x[feature] = train_abnormal_try[feature]
#display(train_abnormal_add_x.values.shape)
scores = cross_val_score(rf_model, train_abnormal_add_x, train_abnormal_y, cv=kf, scoring='accuracy')
cv_scores[feature] = scores.mean()
display(f"accuracy : {cv_scores}")
train_abnormal_prep.loc[:, 'Skin_BMI_Ratio'] = train_abnormal_try['Skin_BMI_Ratio'].copy()
display(f"train_abnormal_prep : {list(train_abnormal_prep.columns)}")
"accuracy : {'Skin_BMI_Diff': 0.7328322784810126, 'Skin_BMI_Sum': 0.7359177215189873, 'Skin_BMI_Ratio': 0.742246835443038}"
"train_abnormal_prep : ['ID', 'Pregnancies', 'Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI', 'DiabetesPedigreeFunction', 'Age', 'Outcome', 'Skin_BMI_Ratio']"
※ 결과 해석
생성한 세 가지 새로운 피처 중 Skin_BMI_Ratio가 가장 높은 정확도(약 0.7422)로 성능이 개선되었습니다.
Skin_BMI_Diff와 Skin_BMI_Sum도 원래 피처만 사용했을 때보다 약간의 성능 향상이 있었지만, Skin_BMI_Ratio에 비해서는 미미한 차이였습니다.
따라서 Skin_BMI_Ratio 피처가 데이터셋에 추가되었습니다.
9. train_abnormal 데이터에 선형 판별법 (LDA) 통한 데이터 생성
이 코드 셀에서는 선형 판별 분석(Linear Discriminant Analysis, LDA)를 사용하여 피처의 차원을 축소하고 변환된 값을 원래 데이터 프레임에 추가하는 과정을 수행합니다.
LDA는 클래스 간의 차이를 최대화하고 클래스 내의 차이를 최소화하여 새로운 피처를 생성하는 기법입니다.
이를 통해 기존 피처들을 조합하여 새로운 차원에서의 패턴을 발견하고 랜덤 포레스트 분류기를 사용하여 교차 검증을 수행합니다.
여기서는 'Pregnancies', 'Glucose', 'BloodPressure', 'BMI', 'Insulin', 'DiabetesPedigreeFunction', 'Age' 피처들을 LDA에 사용합니다.
- 데이터 스케일링: 데이터를 스케일링하기 위해 StandardScaler 객체 scaler_for_lda로 fit_transform 메소드를 이용하여 스케일링된 데이터 train_abnormal_try_scaled를 생성합니다.
- LDA 객체 생성: LDA 객체를 생성하기 위해 LDA 클래스를 이용합니다.
- LDA 적용: train_abnormal_try_scaled에 LDA를 적용하기 위해 fit_transform 메소드를 사용합니다.
- 변환된 값 데이터 프레임에 추가: 변환된 값을 데이터 프레임에 추가하기 위해 pd.DataFrame을 사용합니다
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
# LDA에 사용하는 데이터셋 준비
#features_for_lda = [ 'Glucose', 'BloodPressure', 'BMI', 'DiabetesPedigreeFunction', 'Age' ]
features_for_lda = [ 'Pregnancies', 'Glucose', 'BloodPressure', 'BMI', 'Insulin', 'DiabetesPedigreeFunction', 'Age' ]
train_abnormal_try = train_abnormal_prep.copy()[features_for_lda]
scaler_for_lda = StandardScaler()
train_abnormal_try_scaled = scaler_for_lda.fit_transform(train_abnormal_try)
lda_abnormal = LDA()
train_lda_arr = lda_abnormal.fit_transform(train_abnormal_try_scaled, train_abnormal_y)
train_lda = pd.DataFrame(train_lda_arr, columns =['lda'])
# LDA를 통해 변환된 값을 원래의 데이터프레임에 추가
train_abnormal_prep = train_abnormal_prep.reset_index(drop=True) # 인덱스 리셋
train_abnormal_prep['lda'] = train_lda['lda']
features_prep = features_org + ['Skin_BMI_Ratio', 'lda']
train_abnormal_prep_x = train_abnormal_prep[features_prep].copy()
# 교차 검증
RF_model_lda = RandomForestClassifier(random_state=42)
cv_result_abnormal_prep = cross_validate(RF_model_lda, train_abnormal_prep_x, train_abnormal_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1'])
df_cv_result_abnormal_prep = pd.DataFrame(cv_result_abnormal_prep, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1'])
display(df_cv_result_abnormal_prep)
display(df_cv_result_abnormal_prep.describe().loc['mean',:].to_frame().T)
display(train_abnormal_prep_x.head(5))
display(train_abnormal_prep.values.shape)
※ 결과 해석
LDA를 통해 생성된 새로운 피처 lda를 포함하여 RandomForest 모델의 교차 검증을 수행한 결과, 정확도는 약 0.7553, 정밀도는 약 0.6728, 재현율은 약 0.6494, F1 스코어는 약 0.6584로 나타났습니다.
이전에 사용한 피처들만으로의 성능과 비교하여 교차 검증 성능이 향상되었음을 알 수 있습니다.
lda 피처를 포함한 데이터셋의 처음 5개 행을 확인할 수 있으며, 전체 데이터셋의 크기는 (383, 11)로 나타납니다.
LDA를 통해 생성된 새로운 피처를 포함하여 모델을 학습하는 것은 다양한 차원 축소 기법 중 하나로, 모델의 성능을 향상시키기 위한 시도입니다.
10. test_abnormal 데이터에 'SkinThickness' 결측치 처리
이제부터는 지금까지 train 데이터를 결측치가 있는 데이터만을 추출한후, 전처리와 피쳐엔지니어링을 수행했던 과정을, 그대로 test 데이터에서도 수행합니다.
테스트 데이터에 대한 'SkinThickness'의 결측치를 처리해 보겠습니다.
다만, test 데이터에서는 학습 데이터에서 학습된 scaler와 Support Vector Regression (SVR) 모델을 사용하여 테스트 데이터의 'SkinThickness' 결측치를 예측하고 대체합니다.
test_applied = test.copy()
# 'SkinThickness' 값이 0인 데이터와 그렇지 않은 데이터로 분리
test_normal_skin = test_applied[test_applied['SkinThickness'] != 0]
# 학습
test_normal_skin_x = test_normal_skin[features_for_skin]
test_normal_skin_y = test_normal_skin['SkinThickness']
# 학습 데이터에서 학습된 scaler와 모델 사용
test_normal_skin_x_scaled = scaler_reg_train.transform(test_normal_skin_x) # 학습 데이터의 scaler 사용
# 예측
test_missing_skin = test_applied[test_applied['SkinThickness'] == 0]
test_missing_skin_x = test_missing_skin[features_for_skin]
test_missing_skin_x_scaled = scaler_reg_train.transform(test_missing_skin_x)
# 결측치가 있는 데이터에 대한 'SkinThickness' 값을 예측
predicted_skin = svm_model.predict(test_missing_skin_x_scaled)
# 예측된 값으로 'SkinThickness' 결측치 대체
test_missing_skin = test_missing_skin.copy()
test_missing_skin.loc[:, 'SkinThickness'] = predicted_skin
# 처리된 데이터를 병합
test_dealed_missing_skin = pd.concat([test_normal_skin, test_missing_skin]).sort_index()
# 인슐인 결측치 데이터셋 추출
test_abnormal_prep = test_dealed_missing_skin[test_dealed_missing_skin['Insulin'] == 0]
test_abnormal_prep_x = test_abnormal_prep[features_org]
display(test_dealed_missing_skin.head())
display(f"test 결측치 개수 : {len(test[test['SkinThickness'] == 0])}")
display(f"test_dealed_missing_skin 결측치 개수 : {len(test_dealed_missing_skin[test_dealed_missing_skin['SkinThickness'] == 0])}")
11. test_abnormal 데이터에 'Skin_BMI_Ratio' 피쳐 생성
train_abnormal 데이터에서 새롭게 생성한 피쳐를 test_abnormal 데이터에 대하여도 동일하게 생성하도록 하겠습니다.
테스트 데이터에 대한 'BMI'와 'SkinThickness' 피쳐를 기반으로 새로운 피쳐인 'Skin_BMI_Ratio'를 생성하는 과정입니다.
test_abnormal_prep = test_abnormal_prep.copy()
mean_bmi_train = train_abnormal_prep[train_abnormal_prep['BMI'] != 0]['BMI'].mean()
test_abnormal_prep.loc[:, 'BMI'] = test_abnormal_prep['BMI'].replace(0, mean_bmi_train)
test_abnormal_prep.loc[:, 'Skin_BMI_Ratio'] = test_abnormal_prep['SkinThickness'] / test_abnormal_prep['BMI']
display(test_abnormal_prep.head(5))
12. test_abnormal 데이터에 선형 판별법 (LDA) 통한 새로운 피쳐 생성
train_abnormal에서 사용하였던 선형 판별분석(Linear Discriminant Analysis, LDA)을 적용하여, 동일하게 test_abnormal에서도 동일한 피쳐를 생성해보겠습니다.
# 데이터 복사
test_abnormal_try = test_abnormal_prep.copy()[features_for_lda]
# 데이터 정규화
test_abnormal_try_scaled = scaler_for_lda.transform(test_abnormal_try)
# LDA 적용
test_lda_arr = lda_abnormal.transform(test_abnormal_try_scaled)
test_lda = pd.DataFrame(test_lda_arr, columns =['lda'])
test_abnormal_prep = test_abnormal_prep.reset_index(drop=True) # 인덱스 리셋
test_abnormal_prep['lda'] = test_lda['lda']
display(test_abnormal_prep.head())