Search

당뇨병 위험 분류 예측 프로젝트

이상치 처리와 피처 엔지니어링 → 모델 성능 개선

데이터 읽기

import pandas as pd import numpy as np train = pd.read_csv('train.csv') test = pd.read_csv('test.csv') submission = pd.read_csv('sample_submission.csv')
Python
복사
ID
Pregnancies
Glucose
BloodPressure
SkinThickness
Insulin
BMI
DiabetesPedigreeFunction
Age
Outcome
TRAIN_000
4
103
60
33
192
24.0
0.966
33
0
TRAIN_001
10
133
68
0
0
27.0
0.245
36
0
TRAIN_002
4
112
78
40
0
39.4
0.236
38
0
TRAIN_003
1
119
88
41
170
45.3
0.507
26
0
TRAIN_004
1
114
66
36
200
38.1
0.289
21
0

기본 교차 검증 성능

데이터 전처리, 피처 엔지니어링을 하지 않은 상태에서 교차 검증을 수행하는 주된 목적은 ‘기준 성능’을 설정하기 위함이다.
이 기준점 설정을 통해 이후 다양한 기법의 효과를 명확하게 측정하고 평가할 수 있다.
Stratified K-Fold 교차 분석은 K-Fold 로 나눌 때, 클래스 분포가 전체 데이터셋의 클래스 분포와 유사하도록 데이터를 분할한다.
이진 분류 문제를 해결할 때, 클래스 불균형이 심하다면 효과적이다.
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score, cross_validate, KFold, StratifiedKFold features_org = train.columns[1:-1] train_x = train[features_org] train_y = train['Outcome'] kf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42) display("####### 기본 교차 검증 성능 #########") RF_model = RandomForestClassifier(random_state = 42) cv_result = cross_validate(RF_model, train_x, train_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1']) df_cv_result = pd.DataFrame(cv_result, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1']) display(df_cv_result) display(df_cv_result.describe().loc['mean',:].to_frame().T)
Python
복사
'####### 기본 교차 검증 성능 #########'
test_accuracy
test_precision
test_recall
test_f1
0
0.736196
0.652174
0.526316
0.582524
1
0.736196
0.640000
0.561404
0.598131
2
0.730061
0.666667
0.456140
0.541667
3
0.742331
0.631579
0.631579
0.631579
test_accuracy
test_precision
test_recall
test_f1
mean
0.736196
0.647605
0.54386
0.588475
4개의 fold에서 평균 정확도는 73.62%, 평균 정밀도는 64.76%가 나왔습니다.

IQR 기반 이상치 (Outlier) 검출

# 이상치를 탐지하는 함수 정의 def detect_outliers(dataframe, column): Q1 = dataframe[column].quantile(0.25) Q3 = dataframe[column].quantile(0.75) IQR = Q3 - Q1 # IQR 기반으로 이상치 범위를 정의 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 이상치의 인덱스를 반환 return dataframe[(dataframe[column] < lower_bound) | (dataframe[column] > upper_bound)].index # 숫자형 데이터를 대상으로 이상치 탐지 numeric_columns = train.select_dtypes(include=[np.number]).columns.tolist() outliers_dict = {} for column in features_org: outliers = detect_outliers(train, column) if len(outliers) > 0: outliers_dict[column] = outliers display(f"num of outlier of {column} : {len(outliers)}") display(outliers_dict)
Python
복사
'num of outlier of Pregnancies : 3'
'num of outlier of Glucose : 4'
'num of outlier of BloodPressure : 41'
'num of outlier of SkinThickness : 1'
'num of outlier of Insulin : 32'
'num of outlier of BMI : 13'
'num of outlier of DiabetesPedigreeFunction : 30'
'num of outlier of Age : 8'
{'Pregnancies': Int64Index([85, 367, 531], dtype='int64'), 'Glucose': Int64Index([61, 399, 497, 601], dtype='int64'), 'BloodPressure': Int64Index([ 15, 26, 51, 102, 122, 124, 135, 151, 161, 191, 235, 239, 241, 248, 255, 275, 287, 291, 304, 312, 317, 333, 355, 361, 362, 363, 369, 374, 386, 391, 398, 405, 430, 452, 456, 508, 543, 560, 616, 629, 636], dtype='int64'), 'SkinThickness': Int64Index([213], dtype='int64'), 'Insulin': Int64Index([ 11, 50, 70, 98, 117, 129, 140, 193, 212, 247, 257, 258, 261, 302, 326, 341, 342, 360, 381, 417, 444, 484, 512, 525, 576, 587, 600, 618, 620, 625, 635, 638], dtype='int64'), 'BMI': Int64Index([26, 135, 151, 275, 312, 359, 361, 430, 474, 533, 579, 592, 625], dtype='int64'), 'DiabetesPedigreeFunction': Int64Index([ 50, 79, 92, 96, 113, 114, 130, 163, 250, 327, 336, 338, 382, 402, 407, 426, 427, 439, 481, 491, 498, 548, 554, 566, 573, 592, 597, 618, 623, 633], dtype='int64'), 'Age': Int64Index([10, 14, 255, 256, 533, 544, 603, 604], dtype='int64')}

IQR 기반 이상치 (Outlier) 중위값 대체 및 교차 검증 변화 확인

train_dout_median = train.copy() #이상치를 중앙값으로 대체 for column, outlier_indices in outliers_dict.items(): median_value = train_dout_median[column].median() train_dout_median.loc[outlier_indices, column] = median_value train_dout_median_x = train_dout_median[features_org] train_dout_median_y = train_dout_median['Outcome'] RF_model = RandomForestClassifier(random_state = 42) # 이상치 처리 후 RandomForest를 사용한 교차 검증 성능 확인 cv_result_dout_median = cross_validate(RF_model, train_dout_median_x, train_dout_median_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1']) df_cv_result_dout_median = pd.DataFrame(cv_result_dout_median, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1']) display(df_cv_result_dout_median) display(df_cv_result_dout_median.describe().loc['mean',:].to_frame().T)
Python
복사
test_accuracy
test_precision
test_recall
test_f1
0
0.760736
0.687500
0.578947
1
0.742331
0.636364
0.614035
2
0.699387
0.595238
0.438596
3
0.760736
0.687500
0.578947
test_accuracy
test_precision
test_recall
test_f1
mean
0.740798
0.65165
0.552632
0.596798
IQR 방식을 이용해 이상치를 처리한 뒤, 모델의 성능을 확인해보면 소폭 상승한 것을 확인했다. 하지만, 이 상승폭이 그리 크지 않다는 점을 주목할 필요가 있다. 이는 IQR 기반의 이상치 처리 방법이 모델의 성능을 향상시키는 데 역할을 하지 않았다고 볼 수 있다.

IQR 기반의 이상치 (Outlier) 제거 및 교차 검증 성능 변화 확인

train_delete_outlier = train.copy() # 원본 train 데이터에서 이상치 제거 rows_to_drop = set() for column in features_org: outliers_indices = detect_outliers(train_delete_outlier, column) rows_to_drop.update(outliers_indices) train_delete_outlier = train_delete_outlier.drop(rows_to_drop) train_delete_outlier_x = train_delete_outlier[features_org] train_delete_outlier_y = train_delete_outlier['Outcome'] # 이상치를 제거한 후 RandomForest를 사용한 교차 검증 성능 확인 cv_result_delete_outlier = cross_validate(RF_model, train_delete_outlier_x, train_delete_outlier_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1']) df_cv_result_delete_outlier = pd.DataFrame(cv_result_delete_outlier, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1']) display(f"IQR 기반의 이상치 제거한 갯수 : {len(train) - len(train_delete_outlier)} 개, 비율 : {(len(train) - len(train_delete_outlier))/len(train) * 100.0}") display(df_cv_result_delete_outlier) display(df_cv_result_delete_outlier.describe().loc['mean',:].to_frame().T)
Python
복사
'IQR 기반의 이상치 제거한 갯수 : 119 개, 비율 : 18.25153374233129'
test_accuracy
test_precision
test_recall
test_f1
0
0.753731
0.642857
0.439024
0.521739
1
0.744361
0.615385
0.400000
0.484848
2
0.781955
0.677419
0.525000
0.591549
3
0.789474
0.724138
0.512195
0.600000
test_accuracy
test_precision
test_recall
test_f1
mean
0.76738
0.66495
0.469055
0.549534

z_score 기반의 이상치 제거 및 교차 검증 성능 변화 확인

from scipy import stats # Z-score 기반 이상치 제거 z_scores = np.abs(stats.zscore(train_x)) threshold = 3 # 이 값을 조절하여 이상치로 간주되는 임계점을 설정합니다. train_zscore = train.copy()[(z_scores < threshold).all(axis=1)] display(f"z_score 기반의 이상치 제거한 갯수 : {len(train) - len(train_zscore)} 개, 비율 : {(len(train) - len(train_zscore))/len(train) * 100.0}") # 데이터 업데이트 train_zscore_x = train_zscore[features_org] train_zscore_y = train_zscore['Outcome'] display("####### Z-score 기반 이상치 제거 후 교차 검증 성능 #########") RF_model = RandomForestClassifier(random_state=42) cv_result_zscore = cross_validate(RF_model, train_zscore_x, train_zscore_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1']) df_cv_result_zscore = pd.DataFrame(cv_result_zscore, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1']) display(df_cv_result_zscore) display(df_cv_result_zscore.describe().loc['mean',:].to_frame().T)
Python
복사
'z_score 기반의 이상치 제거한 갯수 : 69 개, 비율 : 10.582822085889571'
'####### Z-score 기반 이상치 제거 후 교차 검증 성능 #########'
test_accuracy
test_precision
test_recall
test_f1
0
0.767123
0.684211
0.541667
0.604651
1
0.739726
0.656250
0.437500
0.525000
2
0.739726
0.647059
0.458333
0.536585
3
0.827586
0.789474
0.638298
0.705882
test_accuracy
test_precision
test_recall
test_f1
mean
0.76854
0.694248
0.518949
0.59303
Z-score 를 활용한 이상치 제거 방법이 이 데이터셋에서는 가장 효과적이었다는 것을 수치로 확인할 수 있다. 또한, 결과를 통해 이상치를 제거 하는 방법이 데이터셋마다 다르게 적용되어야 한다는 점도 확인할 수 있다. 따라서, 여러 방법을 실험해보는 것이 가장 중요하다는 것을 알 수 있다.

SMOTE 통한 데이터 불균형

오버샘플링을 통해서 소수 클래스의 데이터를 증가시켜보고 얼마나 모델의 성능이 높아지는지 검증해보자.
SMOTE의 다양한 종류들을 적용해보자.
from imblearn.over_sampling import SMOTE, BorderlineSMOTE,KMeansSMOTE,SVMSMOTE from sklearn.metrics import accuracy_score rf_model_resampled_ = RandomForestClassifier(random_state = 42) # 교차 검증을 수동으로 구현하기 위한 리스트 초기화 accuracies = [] kf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42) smote_kmeans = KMeansSMOTE(random_state=42) for train_idx, val_idx in kf.split(train_zscore_x, train_zscore_y): # 학습 데이터와 검증 데이터 분할 X_train, X_val = train_zscore_x.iloc[train_idx], train_zscore_x.iloc[val_idx] y_train, y_val = train_zscore_y.iloc[train_idx], train_zscore_y.iloc[val_idx] # 학습 데이터에만 SMOTE 오버샘플링 적용 X_train_resampled, y_train_resampled = smote_kmeans.fit_resample(X_train, y_train) # 오버샘플링된 학습 데이터로 모델 학습 rf_model_resampled_.fit(X_train_resampled, y_train_resampled) # 검증 데이터에 대한 예측 val_predictions = rf_model_resampled_.predict(X_val) # 성능 지표 계산 accuracies.append(accuracy_score(y_val, val_predictions)) # 성능 지표의 평균 계산 mean_accuracy = np.mean(accuracies) display(f"accuracy of each Fold : {accuracies}") display(f"accuracy of mean_accuracy : {mean_accuracy}")
Python
복사
'accuracy of each Fold : [0.821917808219178, 0.7534246575342466, 0.7602739726027398, 0.8]'
'accuracy of mean_accuracy : 0.7839041095890411'
오버샘플링을 진행한 후의 교차 검증 평균값이 0.02 정도 증가한 것으로 확인되었다.
전체 학습 데이터에 대해서 오버샘플링을 진행한 후 교차 검증을 수행하면 안된다.
오버샘플링에 사용된 정보가 검증 데이터에도 포함될 수 있기 때문이다. 즉, 오버샘플링의 과정에서 학습 데이터의 정보가 검증 데이터에 누출될 수 있다.
따라서 교차 검증에서 오버샘플링을 사용할 때는 각 폴드의 학습 데이터에만 오버샘플링을 적용하는 것이 중요하다.

feature pair의 조합 연산 통한 새로운 feature 생성

머신러닝 모델을 만드는 과정에서 가장 중요한 부분 중 하나는 피처 엔지니어링이다. 간단한 데이터 정규화부터 복잡한 피처의 조합 및 생성에 이르기까지 다양한 작업을 포함한다.
여기서는 기존 피처들의 조합을 통해 새로운 피처들을 생성할 것이다. 이는 피처 간의 복잡한 상호작용이나 비선형 관계를 포착하는 데 도움이 될 수 있다.
특정 피처의 쌍을 선택하고, 이들의 차이(Diff), 합(Sum), 비율(Ratio)을 계산하여 새로운 피처를 생성하고, 이렇게 생성된 각 피처에 대해 교차 검증을 수행하고 이 결과가 모델 성능에 어떤 영향을 미치는지 확인해볼 것이다.
itertools의 combinations 메소드를 이용해 주어진 특성 목록에서 크기가 2인 모든 조합을 생성한다.
import itertools # 기존 피처의 조합을 생성하고, 새로운 피처를 추가한 후 각 조합에 대한 교차 검증 점수를 계산합니다. # 그 중에서 가장 높은 정확도를 가진 피처만 선택합니다. # RandomForest 모델과 KFold 객체를 생성합니다. rf_model = RandomForestClassifier(random_state = 42) # 주어진 피처의 조합을 생성합니다. features_subset = ['Insulin', 'Age', 'BMI'] feature_combinations = list(itertools.combinations(features_subset, 2)) cv_scores = {} for feature_pair in feature_combinations: feature1, feature2 = feature_pair train_try = train_zscore_x.copy() # 새로운 피처를 추가합니다. train_try[f'{feature1}_{feature2}_Diff'] = train_try[feature1] - train_try[feature2] train_try[f'{feature1}_{feature2}_Sum'] = train_try[feature1] + train_try[feature2] train_try[f'{feature1}_{feature2}_Ratio'] = train_try[feature1] / (train_try[feature2].replace(0, train_try[feature2].median())) features_to_evaluate = [f'{feature1}_{feature2}_Diff', f'{feature1}_{feature2}_Sum', f'{feature1}_{feature2}_Ratio'] # 각 피처에 대한 교차 검증 점수를 계산합니다. feature_scores = {} for feature in features_to_evaluate: scores = cross_validate(rf_model, pd.concat([train_zscore_x, train_try[feature]], axis=1), train_zscore_y, cv=kf, scoring='accuracy') feature_scores[feature] = scores['test_score'].mean() # 가장 높은 점수를 가진 피처만 선택합니다. best_feature = max(feature_scores, key=feature_scores.get) cv_scores[best_feature] = feature_scores[best_feature] display(f"cv_scores[{best_feature}] : {cv_scores[best_feature]}") # 점수를 기준으로 정렬합니다. # 먼저, cv_scores 딕셔너리의 각 항목을 리스트로 변환합니다. items = list(cv_scores.items()) # 그 다음, 이 리스트를 정렬합니다. 각 항목은 (key, value) 쌍이므로, item[1]을 사용하여 값을 기준으로 정렬합니다. sorted_items = sorted(items, key=lambda item: item[1], reverse=True) # 이제 이 정렬된 리스트를 다시 딕셔너리로 변환합니다. cv_scores = dict(sorted_items) display(cv_scores)
Python
복사
'cv_scores[Insulin_Age_Sum] : 0.7839631554085971'
'cv_scores[Insulin_BMI_Ratio] : 0.7822626358053849'
'cv_scores[Age_BMI_Ratio] : 0.7668162494095419'
{'Insulin_Age_Sum': 0.7839631554085971, 'Insulin_BMI_Ratio': 0.7822626358053849, 'Age_BMI_Ratio': 0.7668162494095419}
인슐린, 나이 조합에서는 Sum 이라는 새로운 피처를 추가했을 때 모델의 정확도가 78.4% 로 이상치를 제거한 교차 검증에서의 76.8% 이라는 정확도보다 높아진 것을 확인할 수 있다. 인슐린, BMI 조합에서는 Ratio 라는 새로운 피처를 추가했을 때 모델의 정확도가 78.2%로 마찬가지로 정확도가 더 높아졌다.
나이와 BMI 조합에서는 Ratio라는 새로운 피처를 추가했을 때 모델의 정확도가 76.7%로 이상치를 제거한 교차 검증의 기준 모델보다 오히려 성능이 저하된 것을 확인했다. 이는 해당 정보가 타겟 클래스를 예측하는 데 도움이 되지 않음을 의미한다.

feature pair의 조합 연산 통한 feature 추가하여 교차 검증 성능 확인

앞서 유용하다고 판단된 Insulin_Age_SumInsulin_BMI_Ratio 를 train_zscore에 추가하고, 교차 검증 성능을 확인해본다.
from sklearn.ensemble import RandomForestClassifier # 피쳐 생성 train_try = train_zscore.copy() train_try['Insulin_Age_Sum'] = train_try['Insulin'] + train_try['Age'] train_try['BMI'] = train_try['BMI'].replace(0, train_try['BMI'].median()) train_try['Insulin_BMI_Ratio'] = train_try['Insulin'] / train_try['BMI'] # 피처 추가 train_prep = train_zscore.copy() train_prep['Insulin_Age_Sum'] = train_try['Insulin_Age_Sum'] train_prep['Insulin_BMI_Ratio'] = train_try['Insulin_BMI_Ratio'] train_prep_y= train_prep['Outcome'] train_prep_x = train_prep.drop(['ID', 'Outcome'], axis=1) RF_model_prep = RandomForestClassifier(random_state=42) cv_result_prep = cross_validate(RF_model_prep, train_prep_x, train_zscore_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1']) df_cv_result_prep = pd.DataFrame(cv_result_prep, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1']) display(df_cv_result_prep) display(df_cv_result_prep.describe().loc['mean',:].to_frame().T) display(train_prep_x.head(5))
Python
복사
test_accuracy
test_precision
test_recall
test_f1
0
0.773973
0.702703
0.541667
1
0.773973
0.741935
0.479167
2
0.739726
0.625000
0.520833
3
0.841379
0.800000
0.680851
test_accuracy
test_precision
test_recall
test_f1
mean
0.782263
0.71741
0.555629
0.624464

test 데이터에 동일한 피쳐 생성, 추가 하기

위에서 만든 두 피처들을 test 데이터셋에도 추가하고 추론 결과를 csv에 저장한다.
test['Insulin_Age_Sum'] = test['Insulin'] + test['Age'] test['BMI'] = test['BMI'].replace(0, test['BMI'].median()) test['Insulin_BMI_Ratio'] = test['Insulin'] / test['BMI']
Python
복사

모델 학습, 추론하여 제출 파일 생성하기

rf_model = RandomForestClassifier(random_state = 42) rf_model.fit(train_prep_x, train_prep_y) test_x = test.drop('ID', axis=1) pred = rf_model.predict(test_x) submission['Outcome'] = pred submission.to_csv("submission.csv", index = False)
Python
복사

결측치 데이터 분석 통한 향후 모델링 전략 수립 근거 마련

피처 값이 0인 데이터 관찰과 해석

import matplotlib.pyplot as plt import seaborn as sns features = ['Glucose', 'BloodPressure', 'SkinThickness','Insulin','BMI' ] plt.figure(figsize=(14,4)) for idx, feature in enumerate(features): ax1 = plt.subplot(1,5,idx+1) plt.title(feature) plt.tight_layout() sns.histplot(x=feature, data=train, color='blue', kde=True) plt.show()
Python
복사

train data 결측치 빈도수 및 비율 확인

0값이 포함된 5개의 feature에 대해서 각각 몇개의 0값이 있고 비율이 어떻게 되는지 살펴 본다.
features_to_check = ['SkinThickness', 'Insulin', 'Glucose', 'BMI', 'BloodPressure'] for feature in features_to_check: n_missing = len(train[train[feature] == 0]) ratio_missing = round(n_missing / len(train) * 100, 1) print(f'{feature} 결측치 갯수 : {n_missing}/{len(train)} ({ratio_missing})%')
Python
복사
SkinThickness 결측치 갯수 : 195/652 (29.9)% Insulin 결측치 갯수 : 318/652 (48.8)% Glucose 결측치 갯수 : 4/652 (0.6)% BMI 결측치 갯수 : 7/652 (1.1)% BloodPressure 결측치 갯수 : 30/652 (4.6)%
Plain Text
복사

train, test data의 결측치 분포 비교

결측치로 판단한 피처들에 대해 Train 데이터와 Test 데이터에서 각각 결측치의 비율을 계산하고, 이를 막대 그래프로 시각화해보자.
# seaborn의 색상 팔레트를 변경합니다. sns.set_palette('pastel') # Train 데이터의 결측치 비율을 계산합니다. missing_ratio_train = [] for feature in features_to_check: n_missing = len(train[train[feature] == 0]) ratio_missing = round(n_missing / len(train) * 100.0, 1) missing_ratio_train.append(ratio_missing) # Test 데이터의 결측치 비율을 계산합니다. missing_ratio_test = [] for feature in features_to_check: n_missing = len(test[test[feature] == 0]) ratio_missing = round(n_missing / len(test) * 100.0, 1) missing_ratio_test.append(ratio_missing) # 각각의 데이터프레임으로 변환합니다. missing_df_train = pd.DataFrame({'Feature': features_to_check, 'Missing Ratio': missing_ratio_train, 'Data': 'Train'}) missing_df_test = pd.DataFrame({'Feature': features_to_check, 'Missing Ratio': missing_ratio_test, 'Data': 'Test'}) # 두 데이터프레임을 합쳐서 새로운 데이터프레임을 만듭니다. missing_df = pd.concat([missing_df_train, missing_df_test]) # 시각화를 합니다. plt.figure(figsize=(10, 4)) sns.barplot(x='Feature', y='Missing Ratio', hue='Data', data=missing_df) plt.title('Missing Ratio of Each Feature in Train/Test Data') plt.show()
Python
복사
train, test 데이터에 있는 결측치의 비율은 크게 다르지 않았다. → 결측치 처리 방법을 고려할 때, 훈련 데이터와 테스트 데이터 모두 동일하게 적용할 수 있다.

Insulin 결측치 그룹과 정상 그룹 간 다른 피처들의 결측치 비율 비교

train 데이터를 ‘Insulin’ 피처가 결측치인 데이터와 결측치가 아닌 데이터들로 분리하고 이 각 경우에 대해 결측치 비율을 계산하여 막대 그래프로 시각화를 진행해보자.
→ 즉, Insulin 피처가 결측치인 데이터셋과 아닌 데이터셋의 다른 피처에서의 결측치 분포가 다른지 같은지를 비교하고자 하는 것이다.
# 'Insulin'이 결측치인 데이터와 결측치가 아닌 데이터를 분리 train_missing_insulin = train[train['Insulin'] == 0] train_normal_insulin = train[train['Insulin'] != 0] # 'Insulin'을 제외한 피처 리스트 features_to_check = ['SkinThickness', 'Glucose', 'BMI', 'BloodPressure'] # 각 데이터 프레임에 대한 결측치 비율 계산 missing_insulin_ratios = [len(train_missing_insulin[train_missing_insulin[feature] == 0]) / len(train_missing_insulin) * 100 for feature in features_to_check] normal_insulin_ratios = [len(train_normal_insulin[train_normal_insulin[feature] == 0]) / len(train_normal_insulin) * 100 for feature in features_to_check] # 데이터 프레임 생성 df_missing_ratios = pd.DataFrame({ 'Feature':features_to_check, 'Missing Insulin':missing_insulin_ratios, 'Normal Insulin':normal_insulin_ratios }) display(df_missing_ratios) # 데이터 프레임을 긴 형식(Long format)으로 변경 df_missing_ratios_melted = df_missing_ratios.melt(id_vars='Feature', value_vars=['Missing Insulin','Normal Insulin']) display(df_missing_ratios_melted) # 시각화 plt.figure(figsize=(10,4)) sns.barplot(x='Feature', y='value', hue='variable', data=df_missing_ratios_melted) plt.ylabel('Missing Ratio(%)') plt.title('Missing Ratio Comparison between Missing Insulin and Normal Insulin Groups') plt.show()
Python
복사
Feature
Missing Insulin
Normal Insulin
0
SkinThickness
61.320755
1
Glucose
0.943396
2
BMI
1.886792
3
BloodPressure
9.433962
Feature
variable
value
0
SkinThickness
Missing Insulin
61.320755
1
Glucose
Missing Insulin
0.943396
2
BMI
Missing Insulin
1.886792
3
BloodPressure
Missing Insulin
9.433962
4
SkinThickness
Normal Insulin
0.000000
5
Glucose
Normal Insulin
0.299401
6
BMI
Normal Insulin
0.299401
7
BloodPressure
Normal Insulin
0.000000
- Insulin값이 결측치인 데이터셋들은 SkinThickness에서도 결측치의 개수가 약 60%인 것을 확인할 수 있고, - Insulin값이 결측치인 데이터셋에서 BloodPressure 의 결측치 비율도 약 10%인 것을 확인했다. ⇒ 전반적으로 Insulin이 결측치인 데이터셋에서 다른 피처에서도 결측치가 많이 나오는 것을 확인했다. ⇒ 즉, 두 그룹(인슐린이 결측치인 집단과 아닌 집단)이 서로 다른 특성을 가지고 있다는 것을 의미한다. ⇒ 결국, 두 집단의 차이를 고려하지 않고 결측치를 대체하면 정보의 손실이나 왜곡이 발생할 수 있다는 것을 의미한다. ⇒ 이를 고려해서 결측치 대체 전략을 수립하는 것이 필요해 보인다.

Insulin 결측치 데이터셋과 정상 데이터셋의 각 feature의 분포 비교

두 데이터셋에서의 다른 피처들의 분포를 비교해본다.
features_org = train.columns[1:-1] # 특성들의 분포를 시각화하기 위한 함수 def plot_feature_distribution(df1, df2, label1, label2, features): i = 0 sns.set_style('whitegrid') plt.figure(figsize=(14,8)) for feature in features: i += 1 plt.subplot(3,3,i) sns.kdeplot(df1[feature], bw_adjust=0.5, label=label1) sns.kdeplot(df2[feature], bw_adjust=0.5, label=label2) plt.title(feature, fontsize=14) plt.legend() plt.tight_layout() plt.show() # train_normal과 train_abnormal 데이터셋의 특성 분포 시각화 plot_feature_distribution(train_normal_insuline, train_missing_insuline, 'train_normal', 'train_abnormal', features_org)
Python
복사
큰 차이가 없는 변수 : Glucose, BMI, BloodPressure, DiabetesPedigreeFunction 차이가 있고, 분석의 여지가 있는 변수 : Pregnancies, SkinThickness, Age 인슐린 결측치 데이터셋에서 ‘SkinThickness’, ‘BloodPressure’ 에서의 결측치가 많이 관측되고 있다.

Insulin 결측치 데이터셋과 정상 데이터셋 간의 통계적 차이 검증(T - 검정, Mann-Whitney U 검정)

T - 검정
전제 조건
비교하고자 하는 두 데이터셋이 정규 분포를 따라야 한다. → ‘Shapiro-Wilk’ 정규성 검정 필요
1.
정규성 검증
scipy.stats 의 shapiro 메소드 활용
from scipy.stats import shapiro # Function to test normality using Shapiro-Wilk test def test_normality(data): p_value = shapiro(data)[1] return p_value > 0.05 features = train.columns[1:-1] # Testing normality for each feature in both datasets normality_results_normal = {feature : test_normality(train_normal_insulin[feature]) for feature in features} normality_results_missing = {feature : test_normality(train_missing_insulin[feature]) for feature in features} display(normality_results_normal) display(normality_results_missing)
Python
복사
{'Pregnancies': False, 'Glucose': False, 'BloodPressure': False, 'SkinThickness': False, 'Insulin': False, 'BMI': False, 'DiabetesPedigreeFunction': False, 'Age': False}
{'Pregnancies': False, 'Glucose': False, 'BloodPressure': False, 'SkinThickness': False, 'Insulin': True, 'BMI': False, 'DiabetesPedigreeFunction': False, 'Age': False}
shapiro Wilk 정규성 검정의 귀무 가설은 ‘데이터셋이 정규 분포를 따른다’ 입니다. 두 데이터셋에 대한 정규성 검정 결과, 모든 피처에서 p - value 값이 0.05 이하이므로 정규성 분포를 따르지 않는 것으로 확인된다.
Mann-Whitney U 검정 (비모수 검정 방법)
두 독립적인 그룹 간의 중앙값 차이를 비교하는 방법
데이터가 정규 분포를 따르지 않거나 표본 크기가 작을 때 유용하며 데이터 순위에 기반하여 수행된다.
< 맨 휘트니 검정 주의사항 >
두 그룹의 분포 형태가 비슷해야 한다
두 그룹의 표본 크기가 비슷해야 한다.
scipy.statsmannwhitneyu 메소드 사용
비모수적 검정 방법. 데이터가 어떤 분포인지 상관 없음. 순위 데이터를 기반으로 두 집단 간의 중앙값 차이를 비교합니다.
데이터가 정규분포를 따르지 않거나 이상치가 많을 때 유용하다. T - 검정
이상치에 민감하지 않습니다. 순위를 바탕으로 수치를 계산하기 때문. T - 검정
작은 샘플 크기(30 미만)에서 사용 가능 T - 검정
from scipy.stats import shapiro, levene, ttest_ind, manwhitneyu # Dictionary to store p-values for the Mann-Whitney U test mwu_pvalues = {} column_lst = train.columns[1:] # Apply Mann-Whitney U test for each feature for column in column_lst: if column != 'ID': _, p_value = mannwhitneyu(train_missing_insulin[column], train_normal_insulin[column]) mwu_pvalues[column] = p_value display(mwu_pvalues)
Python
복사
{'Pregnancies': 3.9128231274464e-06, -- 다른 분포 'Glucose': 0.4918211064207565, -- 같은 분포 'BloodPressure': 0.3571499089544796, -- 같은 분포 'SkinThickness': 3.8449530612401654e-41, -- 다른 분포 'Insulin': 4.620801078882003e-122, -- 다른 분포 'BMI': 0.021561267194010858, -- 다른 분포 'DiabetesPedigreeFunction': 3.6452537161343067e-06, -- 다른 분포 'Age': 4.752779400653781e-08, -- 다른 분포
Plain Text
복사

향후 데이터 분석 및 모델링 전략 설정 : Insulin 결측치 데이터와 정상 데이터 교차 검증

앞서 인슐린 피처가 결측치인 데이터와 그렇지 않은 데이터 그룹 간 다른 피처들의 분포에 차이가 있는 것을 검증했다.
이를 바탕으로 두 그룹의 데이터를 각각 다른 모델로 학습시키는 전략을 취하려 한다. 각 그룹의 특성에 더 잘 맞는 모델을 학습시킬 수 있을 것이다.
StratifiedKFold를 사용하여 교차 검증을 수행해 보겠습니다. StratifiedKFold는 클래스 비율을 유지하면서 데이터를 나누는 방법으로, 불균형한 분포를 가진 데이터셋에 유용합니다.
먼저, 'Insulin' 결측치가 없는 데이터에 대해 랜덤 포레스트 분류기를 학습시킵니다. 그리고 StratifiedKFold를 사용하여 교차 검증을 통해 모델의 성능을 평가해 봅시다.
이 때, 성능 지표로는 정확도, 정밀도, 재현율, F1 점수
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score, cross_validate, StratifiedKFold from sklearn.metrics import accuracy_score, recall_score, f1_score, precision_score, roc_auc_score, confusion_matrix features = train.columns[1:-1] train_normal_insulin_x = train_normal_insulin[features] train_noraml_insulin_y = train_normal_insulin['Outcome'] train_missing_insulin_x = train_missing_insulin[features].drop('Insulin', axis=1) train_missing_insulin_y = train_missing_insulin['Outcome'] kf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42) display("####### Insulin 결측치 없는 데이터셋 #########") RF_model_normal = RandomForestClassifier(random_state = 42) cv_result_normal = cross_validate(RF_model_normal, train_normal_insulin_x, train_noraml_insulin_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1']) df_cv_result_normal = pd.DataFrame(cv_result_normal, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1']) display(df_cv_result_normal) display(df_cv_result_normal.describe().loc['mean',:].to_frame().T) display("####### Insulin 결측치 데이터셋 #########") RF_model_abnormal = RandomForestClassifier(random_state = 42) cv_result_abnormal = cross_validate(RF_model_abnormal, train_missing_insulin_x, train_missing_insulin_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(df_cv_result_abnormal) display(df_cv_result_abnormal.describe().loc['mean',:].to_frame().T)
Bash
복사
'####### Insulin 결측치 없는 데이터셋 #########'
test_accuracy
test_precision
test_recall
test_f1
0
0.821429
0.772727
0.629630
0.693878
1
0.773810
0.666667
0.592593
0.627451
2
0.807229
0.777778
0.538462
0.636364
3
0.759036
0.652174
0.555556
0.600000
test_accuracy
test_precision
test_recall
test_f1
mean
0.790376
0.717336
0.57906
0.639423
'####### Insulin 결측치 데이터셋 #########'
test_accuracy
test_precision
test_recall
test_f1
0
0.762500
0.761905
0.533333
0.627451
1
0.712500
0.642857
0.580645
0.610169
2
0.683544
0.600000
0.500000
0.545455
3
0.734177
0.655172
0.633333
0.644068
test_accuracy
test_precision
test_recall
test_f1
mean
0.72318
0.664984
0.561828
0.606786
두 그룹에 대해 서로 다른 성능을 보이고 있다.
이는 두 그룹의 데이터가 각기 다른 특성을 가지고 있음을 나타내고, 따라서 두 그룹의 데이터를 각각 다른 모델로 학습시키는 것이 더 좋은 성능을 얻는 데 도움이 될 수 있을 것이다.

비결측치 그룹에 대한 데이터 전처리 및 피처 엔지니어링

인슐린 결측치가 없는 정상 train 데이터 추출

from sklearn.preprocessing import StandardScaler from sklearn.ensemble import RandomForestClassifier # feature set 정의 : 첫번째 열부터 Age 열까지를 선택 features_org = list(train.columns)[:-2] # train 데이터에서 Insulin이 0이 아닌 데이터 추출 train_normal = train.copy() train_normal = train_normal.loc[train_normal['Insulin'] != 0] train_normal_x = train_normal[features_org] train_normal_y = train_normal['Outcome'] display(train_normal.head(5))
Python
복사
ID
Pregnancies
Glucose
BloodPressure
SkinThickness
Insulin
BMI
DiabetesPedigreeFunction
Age
Outcome
0
TRAIN_000
4
103
60
33
192
24.0
0.966
33
0
3
TRAIN_003
1
119
88
41
170
45.3
0.507
26
0
4
TRAIN_004
1
114
66
36
200
38.1
0.289
21
0
5
TRAIN_005
3
78
50
32
88
31.0
0.248
26
1
6
TRAIN_006
1
91
54
25
100
25.2
0.234
23
0

Z - score 기반의 이상치 제거

인슐린 결측치가 없는 정상 train 데이터셋에서 z-score 기반의 이상치 제거 방법을 실습하자.
from scipy import stats features_org = train.columns[1:-1] train_normal_x = train_normal[features_org] # z-score 기반 이상치 제거 z_scores = np.abs(stats.zscore(train_normal_x)) threshold = 3 train_zscore = train_normal.copy()[(z_scores < threshold).all(axis=1)] display(f'z_score 기반의 이상치 제거한 갯수 : {len(train_normal) - len(train_zscore)} 개, 비율 : {(len(train_normal) - len(train_zscore))/len(train_normal) * 100.0}')
Python
복사
'z_score 기반의 이상치 제거한 갯수 : 26 개, 비율 : 7.784431137724551'

train_normal 에 대한 기본 교차 검증 성능 확인

from sklearn.model_selection import cross_val_score, cross_validate, KFold, StratifiedKFold kf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42) train_normal_x = train_normal[features_org] train_normal_y = train_normal['Outcome'] RF_model_normal = RandomForestClassifier(random_state = 42) cv_result_normal = cross_validate(RF_model_normal, train_normal_x, train_normal_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1']) df_cv_result_normal = pd.DataFrame(cv_result_normal, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1']) display(df_cv_result_normal) display(df_cv_result_normal.describe().loc['mean',:].to_frame().T)
Python
복사
test_accuracy
test_precision
test_recall
test_f1
0
0.821429
0.772727
0.629630
0.693878
1
0.773810
0.666667
0.592593
0.627451
2
0.807229
0.777778
0.538462
0.636364
3
0.759036
0.652174
0.555556
0.600000
test_accuracy
test_precision
test_recall
test_f1
mean
0.790376
0.717336
0.57906
0.639423

상관관계 통해 새로운 변수 생성에 대한 아이디어 얻기

정상 데이터에 대해서 각 피처와 타겟 간의 상관관계를 분석한다.
pointbiserialr() : 점 이연 상관관계와 p-value 를 통해 통계적 유의성을 확인한다
import seaborn as sns import matplotlib.pyplot as plt import scipy.stats import pointbiserialr correlation_org_lst, correlation_dealout_lst = [], [] p_value_org_list, p_value_dealout_lst = [], [] # 점 이연 상관계수 계산 및 출력 for feature in features_org: correlation_org, p_value_org = pointbiserialr(train_normal[feature], train_normal['Outcome']) correlation_org_lst.append(correlation_org) p_value_org_list.append(p_value_org) # 데이터프레임 생성 correlation_dict = { 'Feature' : features_org, 'correlation_org' : correlation_org_lst, 'p_value_org' : p_value_org_list} correlation_df = pd.DataFrame(correlation_dict) display(correlation_df) plt.figure(figsize=(18,6)) plt.subplot(1,3,1) sns.barplot(x='Feature', y='correlation_org', data=correlation_df) plt.gca().set_title('Point Biserial Correlation [original train]') plt.gca().set_xticklabels(features_org, rotation=30) plt.subplot(1,3,2) sns.barplot(x='Feature', y='p_value_org', data=correlation_df) plt.gca().set_xticklabels(features_org, rotation=30) plt.gca().set_title('p_value [original train']) plt.subplot(1,3,3) sns.heatmap(train_normal[features_org].corr(), cmap='coolwarm', annot=True) plt.tight_layout() plt.show()
Python
복사
BloodPressure 와 DiabetesPedigreeFunction 이 p-value가 상대적으로 높게 나왔으므로 다른 피처 대비 타겟 변수와의 상관 관계 유의성이 떨어진다고 해석할 수 있다. 특히 BloodPressure의 경우에 다른 피처와의 조합을 고려해 새로운 피처를 생성해보면 모델 성능에 기여할 수도 있다.

train_normal 데이터에 feature 조합 통한 새로운 feature 생성하기(1)

‘BloodPressure’ , ‘BMI’ 조합
두 피처를 더하거나 빼서 새로운 피처를 생성
BloodPressure 를 BMI 로 나눈 새로운 피처를 생성(BMI가 0인 경우, 중위수로 대체)
from sklearn.ensemble import RandomForestClassifier # 피처 후보 생성 train_normal_try1 = train_normal.copy() train_normal_try1['BloodPressure_BMI_Diff'] = train_normal_try1['BloodPressure'] - train_normal_try1['BMI'] train_normal_try1['BloodPressure_BMI_Sum'] = train_normal_try1['BloodPressure'] + train_normal_try1['BMI'] train_normal_try1['BMI'] = train_normal_try1['BMI'].replace(0, train_normal_try1['BMI'].median()) train_normal_try1['BloodPressure_BMI_Ratio'] = train_normal_try1['BloodPressure'] / train_normal_try1['BMI'] train_normal_try1_x = train_normal_try1.drop('Outcome', axis=1) features_to_evaluate = ['BloodPressure_BMI_Diff', 'BloodPressure_BMI_Sum', 'BloodPressure_BMI_Ratio'] rf_model = RandomForestClassifier(random_state=42) # 교차 검증 성능 비교 cv_scores = {} for feature in features_to_evaluate: train_normal_add_x = train_normal[features_org].copy() train_normal_add_x[feature] = train_normal_try1[feature] scores = cross_val_score(rf_model, train_normal_add_x, train_normal_y, cv=kf, scoring='accuracy') cv_scores[feature] = scores.mean() display(f'accuracy : {cv_scores}')
Python
복사
"accuracy : {'BloodPressure_BMI_Diff': 0.769399024670109, 'BloodPressure_BMI_Sum': 0.8023164084911073, 'BloodPressure_BMI_Ratio': 0.7903399311531842}" 생성된 세개의 피처가 기존의 피처에 추가될 경우 교차 검증 정확도는 위와 같다. 두 피처를 더한 값이 baseline 교차 검증 성능(79.04%)보다 높다. ⇒ 체질량지수와 혈압이 동시에 높을 때 당뇨 발병 영향이 커서 그런 것으로 판단된다.

train_normal 데이터에 feature 그룹화 통한 새로운 feature 생성하기

이번에는 BloodPressure와 DiabetesPedigreeFunction 두 특성을 조합해 타겟 변수와의 의미 있는 연관성을 찾아보자.
1.
BloodPressure 피처의 사분위수를 확인한다.
2.
BloodPressure 피처를 2개의 범주로 나누고 나눈 범주를 LabelEncoder를 사용해 숫자로 인코딩한다.
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score, cross_validate, KFold from sklearn.preprocessing import LabelEncoder train_normal_try2 = train_normal.copy() # BloodPressure 사분위수 계산 q1 = np.percentile(train_normal_try2['BloodPressure'],25) q2 = np.percentile(train_normal_try2['BloodPressure'],50) q3 = np.percentile(train_normal_try2['BloodPressure'],75) q4 = np.percentile(train_normal_try2['BloodPressure'],100) # BloodPressure 2개 범주로 나누기 q_BloodPressure_lst = [0, q1, q4] BloodPressure_labels_train = pd.cut(train_normal_try2['BloodPressure'], bins=q_BloodPressure_lst, labels=['q1','q2']) # LabelEncoder로 범주형 데이터 인코딩 le_train_ = LabelEncoder() train_normal_try2['BloodPressure_cat'] = le_train_.fit_transform(BloodPressure_labels_train) # BloodPressure_cat 범주에 따른 DiabetesPedigreeFunction의 빈도수 계산 train_normal_try2['DiabetesPedigreeFunction_by_BloodPressure_cat'] = train_normal_try2.groupby('BloodPressure_cat')['DiabetesPedigreeFunction'].transform('count') # BloodPressure_cat 범주에 따른 DiabetesPedigreeFunction의 빈도수 dict 형태로 backup DPF_counts_by_BloodPressure_cat_train = train_normal_try2.groupby('BloodPressure_cat')['DiabetesPedigreeFunction'].count().to_dict() display(DPF_counts_by_BloodPressure_cat_train) # 피처가 추가된 데이터로 교차 검증 수행 train_normal_try2_x = train_normal_try2.drop(['ID','Outcome','BloodPressure_cat'], axis=1) rf_model = RandomForestClassifier(random_state=42) # 결과 출력 cv_result_try = cross_val_score(rf_model, train_normal_try2_x, train_normal_y, cv=kf, scoring='accuracy') display(f'accuacy : {cv_result_try.mean()}') display(train_normal_try2.head(5))
Python
복사
{0: 85, 1: 249}
'accuracy : 0.7993043602983362'
ID
Pregnancies
Glucose
BloodPressure
SkinThickness
Insulin
BMI
DiabetesPedigreeFunction
Age
Outcome
BloodPressure_cat
DiabetesPedigreeFunction_by_BloodPressure_cat
0
TRAIN_000
4
103
60
33
192
24.0
0.966
33
0
0
85
3
TRAIN_003
1
119
88
41
170
45.3
0.507
26
0
1
249
4
TRAIN_004
1
114
66
36
200
38.1
0.289
21
0
1
249
5
TRAIN_005
3
78
50
32
88
31.0
0.248
26
1
0
85
6
TRAIN_006
1
91
54
25
100
25.2
0.234
23
0
0
85
새롭게 추가된 피처를 포함한 데이터로 랜덤포레스트 분류기를 사용하여 교차 검증을 수행한 결과, 정확도는 약 79.93%이다.
baseline 교차 검증 성능(0.79)보다 향상된 결과로 생성한 변수가 설명력이 높은 의미 있는 피처일 가능성이 높다.

새로 생성한 feature 추가하여 교차 검증 성능 확인(2)

기존에 설명력이 낮았던 피처를 삭제하고, 새롭게 생성한 피처들을 추가하여 모델 성능 비교를 진행해본다.
train_normal_prep = train_normal.copy() train_normal_prep['BloodPressure_BMI_Sum'] = train_normal_try1['BloodPressure_BMI_Sum'] train_normal_prep['DiabetesPedigreeFunction_by_BloodPressure_cat'] = train_normal_try2['DiabetesPedigreeFunction_by_BloodPressure_cat'] # 점이연 상관 관계 유의성 없는 'BloodPressure', 'DiabetesPedigreeFunction' 제거 train_normal_prep = train_normal_prep.drop('BloodPressure', axis=1) train_normal_prep = train_normal_prep.drop('DiabetesPedigreeFunction', axis=1) train_normal_prep_x = train_normal_prep.drop(['ID','Outcome'], axis=1) # RandomForestClassifier로 오버샘플링된 데이터에 대한 교차 검증 RF_model_prep = RandomForestClassifier(random_state=42) cv_result_normal_prep = cross_validate(RF_model_prep, train_normal_prep_x, train_normal_y, cv=kf, scoring=['accuracy', 'precision', 'recall', 'f1']) df_cv_result_normal_prep = pd.DataFrame(cv_result_normal_prep, columns=['test_accuracy', 'test_precision', 'test_recall', 'test_f1']) display(df_cv_result_normal_prep) display(df_cv_result_normal_prep.describe().loc['mean',:].to_frame().T) display(train_normal_prep_x.head(5))
Python
복사
test_accuracy
test_precision
test_recall
test_f1
mean
0.799197
0.721968
0.615741
0.660939
Pregnancies
Glucose
SkinThickness
Insulin
BMI
Age
BloodPressure_BMI_Sum
DiabetesPedigreeFunction_by_BloodPressure_cat
0
4
103
33
192
24.0
33
84.0
85
3
1
119
41
170
45.3
26
133.3
249
4
1
114
36
200
38.1
21
104.1
249
5
3
78
32
88
31.0
26
81.0
85
6
1
91
25
100
25.2
23
79.2
85

test_normal 데이터에 새로운 feature 생성하기

test 데이터에서도 인슐린 결측치가 없는 정상 데이터(test_normal)를 추출하고, 여기에 train_normal에 대해 실시하였던 피처를 생성 및 불필요한 피처를 제거하는 과정을 진행해본다.
test_normal = test.copy() test_normal = test_normal.loc[test_normal['Insulin'] != 0] # 'BloodPressure_BMI_Sum' 생성 test_normal_prep = test_normal.copy() test_normal_prep.loc[:,'BloodPressure_BMI_Sum'] = test_normal_prep['BloodPressure'] + test_normal_prep['BMI'] # 'DiabetesPedigreeFunction_by_BloodPressure_cat' 생성 BloodPressure_labels_test = pd.cut(test_normal_prep['BloodPressure'], bins = q_BloodPressure_lst, labels = ['q1', 'q2']) test_normal_prep['BloodPressure_cat'] = le_train_.fit_transform(BloodPressure_labels_test) # "BloodPressure_cat" 범주 (train 데이터, train_normal_try1 기준)에 따른 "DiabetesPedigreeFunction"의 빈도수 (train 데이터, train_normal_try2의 통계치) 로 대입 test_normal_prep['DiabetesPedigreeFunction_by_BloodPressure_cat'] = test_normal_prep['BloodPressure_cat'].apply(lambda x: DPF_counts_by_BloodPressure_cat_train.get(x)) # 불필요한 feature 제거 test_normal_prep = test_normal_prep.drop('BloodPressure_cat', axis=1) test_normal_prep = test_normal_prep.drop('BloodPressure', axis=1) test_normal_prep = test_normal_prep.drop('DiabetesPedigreeFunction', axis=1) display(test_normal_prep.head(7))
Python
복사

전처리 및 피처 엔지니어링 처리된 train, test 데이터 저장하기

train_normal_prep.to_csv("train_normal_prep.csv", index = False) test_normal_prep.to_csv("test_normal_prep.csv", index = False) display(train_normal_prep.head(7))
Python
복사

결측치 그룹에 대한 데이터 전처리 및 피처 엔지니어링

결측치와 이상치를 처리하고 그 영향을 이해한다.
회귀 모델을 활용하여 결측치를 예측하고 대체하는 방법을 학습한다.
기존 피처를 조합하거나 그룹화하여 새로운 피처를 생성하는 방법을 학습한다.
선형 판별 분석(LDA)와 같은 방법으로 새로운 피처를 생성한다.
SMOTE 등을 사용하여 데이터 불균형 문제를 해결하는 방법을 실습한다.
전처리와 피처 엔지니어링이 모델의 성능에 미치는 영향을 이해한다.
전처리 및 피처 엔지니어링이 완료된 데이터를 저장하고 재사용하는 방법을 학습한다.

인슐린 결측치 데이터 추출

데이터셋에서 Insuline 특성값이 0인 데이터를 결측치로 간주하고 해당 행을 분리한다.
train_abnormal
test_abnormal
로 따로 분리해 결측치 처리 전략을 수립하고 분석을 진행한다.
features_org = list(train.columns)[1:-1] # ID열 제외 # 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))
Python
복사

train_abnormal 에 대한 기본 교차검증 성능 확인

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_validate, cross_val_score, 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)
Python
복사
{'fit_time': array([0.11429811, 0.11109877, 0.11164045, 0.11078095]), 'score_time': array([0.00942183, 0.00894594, 0.00889683, 0.00876164]), 'test_accuracy': array([0.725 , 0.7375 , 0.74683544, 0.72151899]), 'test_precision': array([0.7826087 , 0.5625 , 0.80769231, 0.55555556]), 'test_recall': array([0.51428571, 0.72 , 0.58333333, 0.6 ]), 'test_f1': array([0.62068966, 0.63157895, 0.67741935, 0.57692308])}
test_accuracy
test_precision
test_recall
test_f1
0
0.725000
0.782609
0.514286
0.620690
1
0.737500
0.562500
0.720000
0.631579
2
0.746835
0.807692
0.583333
0.677419
3
0.721519
0.555556
0.600000
0.576923
test_accuracy
test_precision
test_recall
test_f1
mean
0.732714
0.677089
0.604405
0.626653

인슐린 결측치 데이터(train_abnormal)에 ‘SkinThickness’ 결측치 처리

데이터의 SkinThickness 열에는 결측치로 간주되는 0값이 포함되어 있다.
이번에는 이 SkinThickness 결측치를 처리해보자. 그러기 위해선 결측치가 아닌 행을 추출해서
train_applied = train.copy() from sklearn.preprocessing import StandardScaler # SkinThickness 결측치 대체에 사용할 피처 선택 features_for_skin = ['Pregnancies','Glucose','BloodPressure','BMI','DiabetesPedigreeFunction','Age'] # SkinThickness 값이 0인 데이터와 그렇지 않은 데이터로 분리 train_normal_skin = train_applied[train_applied['SkinThickness']!=0] 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()) display(pd.DataFrame(train_normal_skin_x_scaled, columns=features_for_skin).head())
Python
복사
Pregnancies
Glucose
BloodPressure
BMI
DiabetesPedigreeFunction
Age
0
4
103
60
24.0
0.966
33
1
10
133
68
27.0
0.245
36
2
4
112
78
39.4
0.236
38
3
1
119
88
45.3
0.507
26
4
1
114
66
38.1
0.289
21
Pregnancies
Glucose
BloodPressure
BMI
DiabetesPedigreeFunction
Age
0
0.131861
-0.519744
-0.852777
-1.282968
1.340931
0.110916
1
0.131861
-0.246380
0.526760
1.009190
-0.754366
0.570050
2
-0.785812
-0.033763
1.293169
1.887355
0.023477
-0.531872
3
-0.785812
-0.185632
-0.392932
0.815697
-0.602242
-0.991006
4
-0.174030
-1.279088
-1.619187
-0.241078
-0.719923
-0.531872
결측치가 아닌 데이터를 활용해서 SVR(Support Vector Regression) 모델을 학습시킨다.
스케일링된 SkinThickness가 결측치가 아닌 훈련 데이터를 사용해서 학습시켜야 한다.
학습된 모델을 바탕으로 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])}")
Python
복사
'train 결측치 개수 : 195'
'train_dealed_missing_skin 결측치 개수 : 0'

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)}")
Python
복사
test_accuracy
test_precision
test_recall
test_f1
0
0.712500
0.750000
0.514286
0.610169
1
0.737500
0.562500
0.720000
0.631579
2
0.784810
0.880000
0.611111
0.721311
3
0.746835
0.592593
0.640000
0.615385
test_accuracy
test_precision
test_recall
test_f1
mean
0.745411
0.696273
0.621349
0.644611
"train_abnormal_prep : ['ID', 'Pregnancies', 'Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI', 'DiabetesPedigreeFunction', 'Age', 'Outcome']"

SkinThickness 결측치 처리 후 교차 검증 수행 성능 향상 근거 확인

'Outcome'과 각 특성 간의 상관관계를 파악하기 위해 점 이연 상관계수를 계산한다.
두 데이터셋(train_abnormal과 train_abnormal_prep)에 대해 각각 상관계수를 계산하고 통계적 유의성을 검증하기 위해 p-value도 함께 계산
각 특성에 대하여 pointbiserialr 함수를 사용해 점 이연 상관계수와 p-value를 계산
계산된 상관계수와 p-value 값을 데이터프레임에 저장
저장된 값을 바탕으로 시각화를 통해 상관관계의 크기와 통계적 유의성을 확인
from scipy.stats import pointbiserialr correlation_abnormal_lst, correlation_abnormal_prep_lst = [], [] p_value_abnormal_lst, p_value_abnormal_prep_lst = [], [] feature_lst = train.colmns[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()
Python
복사

train_abnormal 데이터에 feature조합 통한 새로운 feature 생성하기

'Age', 'Pregnancies' 조합으로 피쳐 연산 방법을 이용하여 새로운 feature 생성해보자.
두 개 이상의 피처를 조합하여 새로운 정보나 패턴을 찾아내기 위한 목적으로 사용이 많이 된다. 'Age'와 'Pregnancies'는 상관 관계가 상대적으로 높은 피처쌍으로, 두 변수의 조합을 통해 새로운 피처를 생성하여 통해 타겟 변수와의 관계나 새로운 패턴을 발견할 수도 있다.
'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] 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}")
Python
복사
"accuracy : {'Pregnancies_Age_Diff': 0.7264636075949367, 'Pregnancies_Age_Sum': 0.7327531645569619, 'Pregnancies_Age_Ratio': 0.7265822784810126}"
새로운 피처를 추가했을 때, 되려 성능이 감소했기 때문에 이 조합의 피처들은 추가하지 않기로 결정한다.

train_abnormal 데이터에 feature 그룹화 조합 통한 새로운 feature 생성

'SkinThickness'와 'BMI'에 대한 새로운 피처들을 생성하고 랜덤 포레스트 분류기를 사용하여 각 피처가 모델의 정확도에 어떤 영향을 미치는지 평가해보자.
이번에는 'SkinThickness'와 'BMI'의 차이, 합계, 비율을 나타내는 새로운 피처들을 생성하여 분석해보자.
또한, 'BMI'의 결측치를 평균 값으로 대체하는 처리도 수행해야 한다.
'BMI' 결측치 대체 및 'Skin_BMI_Ratio' 피처 생성
'BMI'의 결측치를 평균 값으로 대체하고, 'SkinThickness'와 'BMI'의 비율을 나타내는 'Skin_BMI_Ratio' 피처를 생성합니다.
피처 평가를 위한 반복문
'features_to_evaluate'에 있는 각 피처를 반복하면서 그 피처를 추가하여 교차 검증을 진행합니다.
생성된 피처를 포함한 데이터로 랜덤 포레스트 분류기를 사용하여 교차 검증을 수행하고, 정확도를 저장합니다.
# 피쳐 후보 생성 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] 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)}") display(train_abnormal_prep.values.shape)
Python
복사
"accuracy : {'Skin_BMI_Diff': 0.7328322784810126, 'Skin_BMI_Sum': 0.7359177215189873, 'Skin_BMI_Ratio': 0.739121835443038}"
"train_abnormal_prep : ['ID', 'Pregnancies', 'Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI', 'DiabetesPedigreeFunction', 'Age', 'Outcome', 'Skin_BMI_Ratio']"
(318, 11)
생성한 세가지 피처 중 Skin_BMI_Ratio 의 교차 검증 성능이 가장 높았다. 이 피처를 추가했다.

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 객체 생성 및 적용
변환된 값 데이터 프레임에 추가: 변환된 값을 데이터 프레임에 추가하기 위해 pd.DataFrame을 사용합니다
Python
복사