티스토리 뷰
https://www.kaggle.com/competitions/hms-harmful-brain-activity-classification/overview
2024년 2월 12일 현재, 캐글에서 진행하고 있는 컴피티션이다.
필자는 본 경진대회를 진행하면서, 실제로 배운 데이터 분석 요소들을 적용해보려고 한다.
글로 써질 것들은 모델을 학습하고, 만들기 까지의 과정이다.
오늘 시간에는 EDA를 진행한다
EDA(탐색적 데이터 분석)
지난 시간의 train 데이터에 대한 분석 결과는 다음과 같다.
분석 결과 : id의 종속관계에 대한 가설 수립 및 일치화
따라서 id를 통한 그룹화로 타깃값 분포 보기
시간에 따라 나열되어 있으므로, 시계열 데이터.
그리고 vote에 대한 인사이트 수립
결측치 없음
우선 종속변수간의 증명으로, id에 대한 피어슨 상관계수 분석
from scipy import stats
import seaborn as sns
import matplotlib.pyplot as plt #시각화 툴
stats.pearsonr(train['eeg_id'],train['spectrogram_id'] )
PearsonRResult(statistic=-0.00771522617580455, pvalue=0.011690062195768723)
배운 대로에 따르면, p-value가 낮으면 낮을수록 해당 플롯이 영향이 많다.
그런데 이건 종속관계에 있으므로, 어차피 피어슨은 의미가 없다.
추가로 id의 분포를 살펴보았다.
sns.pairplot(train[['eeg_id','spectrogram_id']])
하나의 id가 올라감에 따라서 다른 id들이 올라가는 구조도 아니다.
즉, 뒤죽박죽 섞여있다.
이는 연계되어 있는 patient_id도 똑같을 것임이 분명하므로, sort_value()를 통해 정렬해줘서 더블 체크를 해준다.
t_copy = train.copy()
t_copy =t_copy.sort_values(['eeg_id','eeg_label_offset_seconds'])
t_copy
eeg_id eeg_sub_id eeg_label_offset_seconds spectrogram_id spectrogram_sub_id spectrogram_label_offset_seconds label_id patient_id expert_consensus seizure_vote lpd_vote gpd_vote lrda_vote grda_vote other_vote
40927 568657 0 0.0 789577333 0 0.0 1825637311 20654 Other 0 0 3 0 2 7
40928 568657 1 6.0 789577333 1 6.0 3640441665 20654 Other 0 0 3 0 2 7
40929 568657 2 12.0 789577333 2 12.0 1364530340 20654 Other 0 0 3 0 2 7
40930 568657 3 16.0 789577333 3 16.0 1874525225 20654 Other 0 0 3 0 2 7
77116 582999 0 0.0 1552638400 0 0.0 1722186807 20230 LPD 0 12 0 1 0 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
33556 4294858825 1 2.0 657299228 1 2.0 2581506921 4312 Other 0 0 0 0 1 14
33557 4294858825 2 6.0 657299228 2 6.0 3251917981 4312 Other 0 0 0 0 1 14
33558 4294858825 3 8.0 657299228 3 8.0 3646714202 4312 Other 0 0 0 0 1 14
33559 4294858825 4 12.0 657299228 4 12.0 3219273381 4312 Other 0 0 0 0 1 14
13213 4294958358 0 0.0 260520016 9 2508.0 2788887007 25986 Other 0 0 0 0 0 2
최소의 egg_id는 568657 4294957358의 범위.
그렇다면 나머지 범위 또한 확인해보자.
print([t_copy['eeg_id'].min(),t_copy['eeg_id'].max()])
print([t_copy['spectrogram_id'].min(),t_copy['spectrogram_id'].max()])
print([t_copy['patient_id'].min(),t_copy['patient_id'].max()])
[568657, 4294958358]
[353733, 2147388374]
[56, 65494]
각자 범위가 다르고, 나열되어 있는 순서도 다르다.
이번에도 pairplot을 그려보면?
역시나 엉망진창
아직은 초보이므로, 배울 겸 duplicates를 활용해 아예 drop해버리고 깔끔하게 egg_id 별로 하나만 남긴 후에 상관 있는 열들만 나열해보기로 했다.
t_copy = t_copy.drop_duplicates('eeg_id')
t_copy
eeg_id eeg_sub_id eeg_label_offset_seconds spectrogram_id spectrogram_sub_id spectrogram_label_offset_seconds label_id patient_id expert_consensus seizure_vote lpd_vote gpd_vote lrda_vote grda_vote other_vote
40927 568657 0 0.0 789577333 0 0.0 1825637311 20654 Other 0 0 3 0 2 7
77124 582999 8 24.0 1552638400 8 24.0 129939089 20230 LPD 0 12 0 1 0 1
1150 642382 1 24.0 14960202 13 1032.0 2552357208 5955 Other 0 0 0 0 0 1
31929 751790 0 0.0 618728447 4 908.0 2898467035 38549 GPD 0 0 1 0 0 0
3319 778705 0 0.0 52296320 0 0.0 3255875127 40955 Other 0 0 0 0 0 2
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
59218 4293354003 0 0.0 1188113564 0 0.0 447244163 16610 GRDA 0 0 0 0 1 1
77025 4293843368 0 0.0 1549502620 0 0.0 1618953053 15065 GRDA 0 0 0 0 1 1
105275 4294455489 0 0.0 2105480289 0 0.0 469526364 56 Other 0 0 0 0 0 1
33557 4294858825 2 6.0 657299228 2 6.0 3251917981 4312 Other 0 0 0 0 1 14
13213 4294958358 0 0.0 260520016 9 2508.0 2788887007 25986 Other 0 0 0 0 0 2
17089 rows × 15 columns
106800이던 데이터가 17089개로 줄어들었다.
이걸로 scatterplot을 그려봐도 크게 변한 건 못 봤다.
일단, 점이 너무 많다.
그렇기에 다른 시각화 방안을 찾아야 할 것 같다.
어찌되었든, 하나의 id가 순서대로여도 다른 id는 전혀 순서대로가 아니라는 인사이트를 얻었다.
특징값들이 너무 많은 범주로 나뉘어져 있다.
이게 모델에 긍정적인 영향을 끼칠지, 아니면 이것 자체로 분류력이 생길지는 해봐야 할 것 같다.
피처 요약표
여기서 이제 피처 요약표가 중요하다.
피처 값이 몇개나 있는지 피처 요약표를 통해 알아본다.
def resumetable(df):
print(f'데이터 세트 형상: {df.shape}')
summary = pd.DataFrame(df.dtypes, columns=['데이터 타입'])
summary['결측값 개수'] = df.isnull().sum().values #결측값 개수 열 추가
summary['고윳값 개수'] = df.nunique().values
summary['데이터 종류'] = None
for col in df.columns: #데이터 종류 추가
if 'id' in col:
summary.loc[col, '데이터 종류'] = 'id형'
elif df[col].dtype == object:
summary.loc[col, '데이터 종류'] = '명목형'
elif df[col].dtype == float:
summary.loc[col, '데이터 종류'] = '연속형'
elif 'vote' in col:
summary.loc[col, '데이터 종류'] = '투표형'
summary['첫 번째 값'] = df.loc[0].values #첫째값
summary['두 번째 값'] = df.loc[1].values #둘째값
return summary
summary=resumetable(train)
summary = summary.reset_index() #그렇게 구한 서버리를 index 리셋
summary = summary.rename(columns={'index': '피처'}) #피처를
summary
피처 데이터 타입 결측값 개수 고윳값 개수 데이터 종류 첫 번째 값 두 번째 값
0 eeg_id int64 0 17089 id형 1628180742 1628180742
1 eeg_sub_id int64 0 743 id형 0 1
2 eeg_label_offset_seconds float64 0 1502 연속형 0.0 6.0
3 spectrogram_id int64 0 11138 id형 353733 353733
4 spectrogram_sub_id int64 0 1022 id형 0 1
5 spectrogram_label_offset_secondsfloat64 0 4686 연속형 0.0 6.0
6 label_id int64 0 106800 id형 127492639 3887563113
7 patient_id int64 0 1950 id형 42516 42516
8 expert_consensus object 0 6 명목형 Seizure Seizure
9 seizure_vote int64 0 18 투표형 3 3
10 lpd_vote int64 0 19 투표형 0 0
11 gpd_vote int64 0 17 투표형 0 0
12 lrda_vote int64 0 16 투표형 0 0
13 grda_vote int64 0 16 투표형 0 0
14 other_vote int64 0 26 투표형 0 0
타깃값인 expert_consensus는 6개,
그리고 vote는 16개~26개의 값.
id 형까지 포함하면 대놓고 다 연속형(투표형 포함)이기에, 고윳값이 꽤나 많다는 걸 알 수 있다. 아예 label_id의 경우에는 인덱스와 같은 값이므로 index로 빼는 것도 고려.
이제 타깃값의 비율을 구해본다.
타깃값 비율
def write_percent(ax, total_size):
'''도형 객체를 순회하며 막대 그래프 상단에 타깃값 비율 표시'''
for patch in ax.patches:
height = patch.get_height() # 도형 높이(데이터 개수)
width = patch.get_width() # 도형 너비
left_coord = patch.get_x() # 도형 왼쪽 테두리의 x축 위치
percent = height/total_size*100 # 타깃값 비율
# (x, y) 좌표에 텍스트 입력
ax.text(left_coord + width/2.0, # x축 위치
height + total_size*0.001, # y축 위치
'{:1.3f}%'.format(percent), # 입력 텍스트
ha='center') # 가운데 정렬
mpl.rc('font', size=15)
plt.figure(figsize=(7, 6))
ax = sns.countplot(x='expert_consensus', data=train)
write_percent(ax, len(train)) # 비율 표시
ax.set_title('Target Distribution');
타깃값 비율이 거의 같은 녀석들이 있다.
GPD와 LRDA가 그러하고, Other과 GRDA가 그러하다.
어찌되었건, 고윳값별 타깃값 비율이 달라야 모델링에 도움이 된다.
그렇기에 피처별 고윳값 그래프를 더 추가해서 그려본다.
참고로 그래프 그리는 거는 코드를 가져다 쓰더라도 다 이해하고 그리는 게 도움이 될 것이다.
id의 경우, 연속형 범주이므로 값을 분할하며 범주화한다.
import matplotlib.gridspec as gridspec
def plot_target_ratio_by_features(df, features, num_rows, num_cols,
size=(12, 18)):
mpl.rc('font', size=9)
plt.figure(figsize=size) # 전체 Figure 크기 설정
grid = gridspec.GridSpec(num_rows, num_cols) # 서브플롯 배치
plt.subplots_adjust(wspace=0.3, hspace=0.3) # 서브플롯 좌우/상하 여백 설정
for idx, feature in enumerate(features):
ax = plt.subplot(grid[idx])
# ax축에 고윳값별 타깃값 비율을 막대 그래프로 그리기
sns.barplot(x=feature, y='expert_consensus', data=df, palette='Set2', ax=ax)
t_copy3 = train.drop('expert_consensus',axis=1)
bin_features = t_copy3.columns
# 이진 피처 고윳값별 타깃값 1 비율을 막대 그래프로 그리기
plot_target_ratio_by_features(train, bin_features, 6, 3) # 6행 3열 배치
여기까지 보니, 어느정도 윤곽이 나오기 시작했다.
위 그래프는 x를 연속형으로 두고, y를 범주형으로 둔 박스플롯이므로, 범주 별 기술통계량 및 경향성을 계략적으로 파악한 것이다.
책에서 배운 노하우는 타깃값 비율이 다른 피처를 선택하고(예측력), 신뢰구간의 범위가 넓지 않은 피처이다.
우선, label_id의 경우, 결정력이 없기 떄문에(전체가 다 똑같다) 모델링에서는 무조건 제거해야 한다.
그리고 신뢰구간의 경우 다 일정하다. 어찌보면 당연한 것이 타깃값이 전부 획일화되어 있고, vote는 범위가 넓지 않은 값이기 때문.
나머지 id의 값들은, id와 sub id의 경우 어느정도 비율이 달라 타깃값 예측에 대한 예측력이 존재할 것이다.
따라서, id와 sub_id를 합치는 방향으로 쓰거나, 아니면 sub_id만 쓰는 방향으로 모델링을 잡아야 할 것 같다.
분석 결과: label_id는 제거. 나머지 피처는 일단 전부 모델링 고려
피어슨 상관관계 분석
원래는 독립성, 정규성, 등분산성이 전부 충족된 상태에서 써야 하지만, 일단 타깃값을 제외한 모든 카데고리가 연속형이므로 참고용으로 피어슨 상관관계 분석을 시행한다.
import matplotlib.gridspec as gridspec
def plot_target_ratio_by_features(df, features, num_rows, num_cols,
size=(12, 18)):
mpl.rc('font', size=9)
plt.figure(figsize=size) # 전체 Figure 크기 설정
grid = gridspec.GridSpec(num_rows, num_cols) # 서브플롯 배치
plt.subplots_adjust(wspace=0.3, hspace=0.3) # 서브플롯 좌우/상하 여백 설정
for idx, feature in enumerate(features):
ax = plt.subplot(grid[idx])
# ax축에 고윳값별 타깃값 1 비율을 막대 그래프로 그리기
sns.barplot(x=feature, y='expert_consensus', data=df, palette='Set2', ax=ax)
t_copy3 = train.drop('expert_consensus',axis=1)
bin_features = t_copy3.columns
# 이진 피처 고윳값별 타깃값 1 비율을 막대 그래프로 그리기
plot_target_ratio_by_features(train, bin_features, 6, 3) # 6행 3열 배치
plt.figure(figsize=(10, 8))
cont_corr = t_copy3[bin_features].corr() # 연속형 피처 간 상관관계
sns.heatmap(cont_corr, annot=True, cmap='OrRd'); # 히트맵 그리기
딱 봐도 특징이 연관되어 있는 게 보인다.
특히나 eeg_sub_id, eeg_second와 spectogram_sub_id, spectogram _second.
이 둘은 당연히도 같은 시계열 형태이고, 증가하는 단조함수 형태이기에 높게 나왔다.
그래서 이 둘은 종속변수라고도 할 수 있기에, 같은 변수로 묶어주는 걸 고려해보는 게 좋겠다.
여기서는 타겟값이 object, 즉, 범주형으로 되어 있기에 뺏지만, 타깃값까지 포함해서 만든 히트맵은 다음과 같다.
feature = ['Seizure', 'GPD', 'LRDA', 'Other', 'GRDA', 'LPD']
t_copy4 = train.copy()
# Create a dictionary to map feature names to their index numbers
feature_index = {feat: idx for idx, feat in enumerate(feature)}
# Apply the mapping to create a new column 'transform'
t_copy4['transform'] = t_copy4['expert_consensus'].map(feature_index)
t_copy4
plt.figure(figsize=(10, 8))
t_copy4 = t_copy4.drop('expert_consensus',axis=1)
col = t_copy4.columns
cont_corr = t_copy4[col].corr() # 연속형 피처 간 상관관계
sns.heatmap(cont_corr, annot=True, cmap='OrRd'); # 히트맵 그리기
plt.savefig('상관계수2.jpg', format='jpeg')
맨 끝에 있는 transform 함수가 타깃값의 고윳값을 0~6으로 바꾼 형태이다.
보게 되면, 눈에 띄게 종속이라고 할만한 값은 없어보이지만, 확실히 2개의 값인 seizure_vote와 pd_vote에 영향을 많이 받는 걸 볼 수 있다.
내 판단으로는, 당연히 종속값이라 높은 값을 가지는 게 맞고, 그래야 하는 것 같다.
오히려 반대로, 종속값이 별로 나오지 않는 lrda_vote는 오히려 모델링에 도움이 안될 수도 있겠다.
따라서 스피어맨 계수도 보자.
plt.figure(figsize=(10, 8))
col = t_copy4.columns
cont_corr = t_copy4[col].corr(method='spearman') # 연속형 피처 간 상관관계
sns.heatmap(cont_corr, annot=True, cmap='OrRd'); # 히트맵 그리기
plt.savefig('상관계수2.jpg', format='jpeg')
스피어맨 계수도 비슷하게 나온다. 게다가 값이 더 올라간 값들이 대부분이다.
즉, 분포가 선형 모델은 아니라는 것.
그렇다면 Lrda_vote를 떨어뜨려볼지 말지 여부도 모델의 성능이 잘 안나온다면 고려해봄직 하다.
분석 결과 : eeg_sub_id, eeg_second와 spectogram_sub_id, spectogram _second는 합쳐준다.
lrda_vote는 모델의 성능이 안나온다면 떨어뜨림을 고려.