1. feature 값이 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()
※ 결과 해석
'Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI' 이 4개의 feature의 분포를 보면 0값이 분포가 튀어 있음을 관찰할 수 있습니다.
그리고 이 4개 feature는 물리적으로 0 값이라는 것이 존재할 수 없는 수치임을 곧 알 수 있습니다.
이러한 도메인 관점의 관찰없이, train.info() 메소드를 통해 Non-Null Count 값만 보고 결측치가 없는 데이터라고 넘어가거나, IRQ 혹은 Z-Score와 같은 통계적 분포만을 가지고 이상치(Outlier) 관점으로 접근하면, 데이터 분석과 모델링 과정에서 왜곡된 분석을 할 수 있으므로 주의하여야 합니다.
0 데이터는 실제 데이터 수집 환경에서 자주 발생할 수 있는 현상입니다.
실제로는 데이터가 없는 결측치인데 0으로 처리되는 경우가 많기 때문에, 평균으로부터 비정상적으로 분포가 떨어진 데이터인, 노이즈와 같이 잘못 측정된 데이터인지, 데이터가 없는 결측치인지 잘 판단해야 합니다.
'Glucose', 'BloodPressure', 'SkinThickness', 'Insulin', 'BMI' feature의 0값은 결측치라고 판단하여야 합니다.
2. 결측치 빈도수 및 비율 확인
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.0, 1)
print(f"{feature} 결측치 갯수 : {n_missing}/{len(train)} ({ratio_missing})%")
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)%
※ 결과 해석
'SkinThickness': 약 30%의 결측치가 있습니다.
이는 상당히 높은 비율로, 이 피처의 결측치를 어떻게 처리할지가 중요한 문제입니다.
결측치가 많기 때문에 이를 제거하면 데이터의 많은 부분이 손실될 수 있습니다. 이를 대체하려면 적절한 방법을 사용해야 합니다.
'Insulin': 결측치의 비율이 가장 높고, 약 49%에 달합니다.
이는 이 피처가 목표 변수에 미치는 영향을 정확하게 평가하기 어렵게 만듭니다.
결측치를 적절히 대처하지 않으면 모델의 성능에 부정적인 영향을 미칠 수 있습니다.
'Glucose', 'BMI', 'BloodPressure': 이들 피처의 결측치 비율은 각각 약 0.6%, 1.1%, 4.6%로 상대적으로 낮습니다.
그러나 이들 피처는 대개 건강 상태와 밀접하게 관련되어 있으므로, 이들의 결측치는 여전히 중요하게 다루져야 합니다.
3. train, test data의 결측치 분포 비교
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# Assuming features_to_check is a list of feature names you're interested in checking
# 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()
※ 결과 해석
'Insulin' 피처는 훈련 데이터와 테스트 데이터 모두에서 가장 높은 결측치 비율을 보여주고 있습니다.
이는 'Insulin' 피처가 데이터에서 중요한 정보를 포함하고 있지만, 그 정보를 완전히 활용하는 것이 어려울 수 있음을 나타냅니다.
'SkinThickness', 'Glucose', 'BMI', 'BloodPressure' 등의 피처들도 결측치가 있지만, 이들의 결측치 비율은 'Insulin'에 비해 상대적으로 낮습니다.
또한, 모든 피처들에서 훈련 데이터와 테스트 데이터의 결측치 비율이 대체로 비슷함을 볼 수 있습니다.
이는 결측치 처리 방법을 선택할 때 훈련 데이터와 테스트 데이터에서 동일하게 적용할 수 있음을 나타냅니다.
4. 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.0 for feature in features_to_check]
normal_insulin_ratios = [len(train_normal_insulin[train_normal_insulin[feature] == 0]) / len(train_normal_insulin) * 100.0 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'])
# 시각화
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()
※ 결과 해석
이 시각화 결과는 'Insulin' 결측치 그룹과 결측치가 아닌 그룹 간에 피처들의 결측치 비율에서 상당한 차이가 있음을 보여줍니다.
특히, 'Insulin'이 결측치인 그룹에서 'SkinThickness'와 'BMI' 피처의 결측치 비율은 훨씬 높은 것으로 보입니다.
이는 'Insulin' 결측치와 이외의 피처들의 결측치 사이에 상관성이 있을 수 있음을 시사합니다.
반면, 'Insulin' 결측치가 없는 그룹에서는 이러한 피처들의 결측치 비율이 상대적으로 낮습니다.
이는 두 그룹이 서로 다른 특성을 가지고 있음을 의미합니다.
결과적으로, 이 두 그룹은 매우 다른 특성을 가지고 있으며, 이 점을 고려하지 않고 결측치를 대체하면 정보의 손실이나 왜곡이 발생할 수 있습니다.
따라서 이를 고려하여 결측치 대체 전략을 수립하는 것이 중요합니다.
5. 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_insulin, train_missing_insulin, "train_normal", "train_abnormal", features_org)
※ 결과 해석
a. 전반적으로 'Pregnancies', 'Age',
'SkinThickness'에서는 분포의 차이가 있어 보이고, 'Glucose', 'BloodPressure', 'BMI'는 유사한 분포 특성을 가집니다.
b. 인슐린 결측치가 있는 데이터에서 'SkinThickness', 'BMI' 등의 결측치가 많이 관찰됩니다.
분포의 차이가 있는 feature
- Pregnancies: 인슐린이 결측치인 그룹에서 임신 횟수는 주로 0-10회 사이에 분포하고 있습니다.
반면, 인슐린 결측치가 아닌 그룹에서는 임신 횟수가 더 넓게 분포하고 있습니다. - Age: 인슐린이 결측치인 그룹에서는 나이가 대체로 젊은 편이며, 인슐린 결측치가 아닌 그룹에서는 나이 분포가 더 넓게 나타나고 있습니다.
- SkinThickness: 인슐린이 결측치인 그룹에서는 피부 두께가 대체로 얇은 편입니다. 반면 인슐린 결측치가 아닌 그룹에서는 피부 두께가 더 넓게 분포하고 있습니다.
- DiabetesPedigreeFunction: 두 그룹 모두 넓게 분포하고 있지만, 인슐린이 결측치인 그룹에서는 이 값이 대체로 낮은 편입니다.
분포가 유사한 feature
- Glucose: 두 그룹 모두 유사한 분포를 보이고 있습니다. 이는 두 그룹 간에 혈당 수치에 대한 큰 차이가 없음을 나타냅니다.
- BloodPressure: 두 그룹 모두 유사한 분포를 보이며, 특별한 차이점은 없어 보입니다.
- BMI: 두 그룹 모두 유사한 분포를 보이고 있습니다.
6. Insulin 결측치 데이터셋과 정상데이터셋 간의 통계적차이 검정, T-검정 고려
두 그룹 간의 차이를 통계적으로 분석하기 위해, 먼저 T-검정을 고려합니다.
T-검정을 적용하기 위한 전제 조건 중 하나는 데이터가 정규 분포를 따르는지 여부입니다.
이를 확인하기 위해 Shapiro-Wilk 정규성 검정을 수행합니다.
- 우선, 정규성을 확인하기 위한 함수를 만들어 보겠습니다. 이 함수는 scipy.stats의 shapiro 메소드를 사용하여 입력된 데이터가 정규 분포를 따르는지 검정해 보는 것이 목적입니다.
shapiro 메소드는 Shapiro-Wilk 정규성 검정을 수행하고, 검정 통계량과 p-value를 반환합니다.
만약 p-value가 0.05보다 크다면, 우리는 해당 데이터가 정규 분포를 따른다는 귀무 가설을 기각하지 않습니다. 이 함수는 이러한 로직을 바탕으로 작성될 것입니다. - 그 다음, 만든 함수를 사용하여 'Insulin' 값이 결측치인 데이터와 결측치가 아닌 데이터의 각 특성에 대해 정규성을 검정해 보겠습니다.
각 특성에 대한 검정 결과는 딕셔너리에 저장될 것이며, 이 딕셔너리는 특성 이름을 키로, 정규성 검정 결과를 값으로 가집니다.
이를 통해 각 특성의 데이터가 정규 분포를 따르는지 여부를 확인해 볼 수 있습니다.
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)
{'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}
※ 결과 해석
정상적인 Insulin 값을 가진 데이터셋과 결측치를 가진 Insulin 데이터셋의 모든 특성들이 정규 분포를 따르지 않음을 확인할 수 있습니다.
따라서 두 그룹이 정규성을 따른다는 전제하에서 사용할 수 있는 T-검정은 할 수 없고, 비 모수적 검정 방법을 사용해야 합니다.
7. Mann-Whitney U 테스트 통한 두 데이터셋 간의 유의미한 차이 검정
※ Mann Whitney U 검정
Mann Whitney U 검정 은 두 독립적인 그룹 간의 중앙값 차이를 비교하는 비모수적 방법입니다.
이 검정은 특히 데이터가 정규 분포를 따르지 않거나 표본 크기가 작을 때 유용하며, 이 검정은 데이터의 순위에 기반하여 수행됩니다.
그러나 Mann Whitney U 검정에는 몇 가지 주의 사항이 있습니다:
두 그룹의 표본 크기가 매우 다르면 (예: 하나는 매우 크고 다른 하나는 작은 경우), 결과가 왜곡될 수 있습니다.
검정의 가정 중 하나는 두 그룹의 분포 형태가 비슷해야 한다는 것입니다.
즉, 한 그룹이 편향되어 있고 다른 그룹이 그렇지 않으면 검정 결과가 왜곡될 수 있습니다.
현재의 경우, 두 그룹의 표본 크기는 비교적 비슷하며 (정상: 343, 결측: 318), 대부분의 특성에서 두 그룹 모두 정규 분포를 따르지 않기 때문에 Mann Whitney U 검정의 사용은 타당해 보입니다.
##############################################################################
# Mann-Whitney U 테스트 통한 두 집단간 차이가 있는지를 통계적으로 검정
#############################################################################
from scipy.stats import shapiro, levene, ttest_ind, mannwhitneyu
# 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': # Exclude the 'ID' column
_, p_value = mannwhitneyu(train_missing_insulin[column], train_normal_insulin[column])
mwu_pvalues[column] = p_value
display(mwu_pvalues)
{'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,
'Outcome': 0.10778310764824817}
※ 결과 해석
Mann Whitney U 검정의 결과는 다음과 같습니다:
Mann-Whitney U 테스트의 결과를 해석하면, 'Pregnancies', 'SkinThickness', 'Insulin', 'BMI', 'DiabetesPedigreeFunction', 'Age' feature에서는 'Insulin' 값이 결측치인 데이터와 결측치가 아닌 데이터 간에 통계적으로 유의미한 차이가 있음을 알 수 있습니다.
이는 이들 특성의 p-value가 0.05보다 작기 때문입니다.
반면, 'Glucose', 'BloodPressure', feature에서는 두 그룹 간에 통계적으로 유의미한 차이를 찾지 못했습니다.
이는 이들 특성의 p-value가 0.05보다 크기 때문입니다.
따라서, 이 결과를 바탕으로 'Insulin' 값이 결측치인 데이터와 결측치가 아닌 데이터는 특정 feature들에서 통계적으로 유의미한 차이를 보임을 확인할 수 있습니다.
이는 데이터 처리 전략을 수립하거나, 모델을 학습시키는 데 중요한 정보가 될 수 있습니다.
8. 향후 데이터 분석 및 모델링 전략 설정
: Insulin 결측치 데이터와 정상 데이터 교차검증
앞서 'Insulin' 결측치가 있는 데이터와 없는 데이터 간의 분포 차이를 관찰하고, 통계적으로 그 차이가 유의미한지를 검정했습니다.
이를 통해, 이 두 그룹은 특정 특성들에서 통계적으로 유의미한 차이를 보임을 확인했습니다.
이를 바탕으로, 우리는 두 그룹의 데이터를 각각 다른 모델로 학습시키는 전략을 취하려 합니다.
이를 통해 각 그룹의 특성에 더 잘 맞는 모델을 학습시킬 수 있을 것으로 기대합니다.
이번에는 StratifiedKFold를 사용하여 교차 검증을 수행해 보겠습니다.
StratifiedKFold는 클래스 비율을 유지하면서 데이터를 나누는 방법으로, 불균형한 분포를 가진 데이터셋에 유용합니다.
- 먼저, 'Insulin' 결측치가 없는 데이터에 대해 랜덤 포레스트 분류기를 학습시킵니다. 그리고 StratifiedKFold를 사용하여 교차 검증을 통해 모델의 성능을 평가해 봅시다.
이 때, 성능 지표로는 정확도, 정밀도, 재현율, F1 점수를 사용합니다. - 그 다음, 'Insulin' 결측치가 있는 데이터에 대해 같은 과정을 수행합니다.
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)
※ 결과 해석
'Insulin' 결측치가 없는 데이터에 대해 학습시킨 모델의 평균 정확도는 약 0.79, 정밀도는 약 0.72, 재현율은 약 0.58, F1 점수는 약 0.64로 나타났습니다.
반면, 'Insulin' 결측치가 있는 데이터에 대해 학습시킨 모델의 평균 정확도는 약 0.72, 정밀도는 약 0.66, 재현율은 약 0.56, F1 점수는 약 0.61로 나타났습니다.
이를 통해, 'Insulin' 결측치가 없는 데이터에 대해 학습시킨 모델이 'Insulin' 결측치가 있는 데이터에 대해 학습시킨 모델보다 전반적으로 더 높은 성능을 보였습니다.
이는 두 그룹의 데이터가 각기 다른 특성을 가지고 있음을 나타냅니다.
따라서, 이 두 그룹의 데이터를 각각 다른 모델로 학습시키는 것이 더 좋은 성능을 얻는 데 도움이 될 수 있습니다.
'머신러닝 > 머신러닝: 실전 프로젝트 학습' 카테고리의 다른 글
당뇨병 위험 분류 예측 프로젝트(이진분류) 6 : 그룹별 모델 최적화 및 결합 (0) | 2024.12.29 |
---|---|
당뇨병 위험 분류 예측 프로젝트(이진분류) 5 : 결측치 그룹에 대한 데이터 전처리 및 피처 엔지니어링 (0) | 2024.12.29 |
당뇨병 위험 분류 예측 프로젝트(이진분류) 4 : 비결측치 그룹에 대한 데이터 전처리 및 피처 엔지니어링 (0) | 2024.12.29 |
당뇨병 위험 분류 예측 프로젝트(이진분류) 2 : 이상치 처리 및 피처 엔지니어링 (0) | 2024.12.28 |
당뇨병 위험 분류 예측 프로젝트(이진분류) 1 : 데이터 분석 (0) | 2024.12.28 |