티스토리 뷰

반응형

https://www.kaggle.com/competitions/hms-harmful-brain-activity-classification/overview

 

2024년 2월 12일 현재, 캐글에서 진행하고 있는 컴피티션이다.

필자는 본 경진대회를 진행하면서, 실제로 배운 데이터 분석 요소들을 적용해보려고 한다.

 

글로 써질 것들은 모델을 학습하고, 만들기 까지의 과정이다.

 

오늘 시간에는 EDA의 두 번째 시간이다.

 

EDA(탐색적 데이터 분석)

https://www.kaggle.com/competitions/hms-harmful-brain-activity-classification/discussion/467021

 

지난시간의 결론은 label은 빼고, sub_id와 seconds의 값들을 합쳐주는 것.

조금 더 명확하게 하기 위해서 이번에는 다른 사람의 EDA 분석을 탐구해보려고 한다.

 

현재 가장 많은 투표수를 받은 EDA이다.

초보의 EDA 실력 향상에 대해서 도움이 될 것 같다.

 

VOTE 변수에 대한 탐구

train.describe()
	eeg_id		eeg_sub_id	eeg_seconds	spectrogram_id	spectrogram_sub_id	spect_seconds	label_id	patient_id	seizure_vote	lpd_vote	gpd_vote	lrda_vote	grda_vote	other_vote
count	1.068000e+05	106800.000000	106800.000000	1.068000e+05	106800.000000		106800.000000	1.068000e+05	106800.000000	106800.000000	106800.000000	106800.000000	106800.000000	106800.000000	106800.000000
mean	2.104387e+09	26.286189	118.817228	1.067262e+09	43.733596		520.431404	2.141415e+09	32304.428493	0.878024	1.138783	1.264925	0.948296	1.059185	1.966283
std	1.233371e+09	69.757658	314.557803	6.291475e+08	104.292116		1449.759868	1.241670e+09	18538.196252	1.538873	2.818845	3.131889	2.136799	2.228492	3.621180
min	5.686570e+05	0.000000	0.000000	3.537330e+05	0.000000		0.000000	3.380000e+02	56.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000
25%	1.026896e+09	1.000000	6.000000	5.238626e+08	2.000000		12.000000	1.067419e+09	16707.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000
50%	2.071326e+09	5.000000	26.000000	1.057904e+09	8.000000		62.000000	2.138332e+09	32068.000000	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000
75%	3.172787e+09	16.000000	82.000000	1.623195e+09	29.000000		394.000000	3.217816e+09	48036.000000	1.000000	1.000000	0.000000	1.000000	1.000000	2.000000
max	4.294958e+09	742.000000	3372.000000	2.147388e+09	1021.000000		17632.000000	4.294934e+09	65494.000000	19.000000	18.000000	16.000000	15.000000	15.000000	25.000000

내가 했던 eda와 다른 점은 describe()를 썻다는 것.

 

이제 와서 전체적인 분포를 보니, 확실히 이상하다는 게 느껴진다.

중앙값 수치에 비해, max값이 너무 큰 값들이 많다. 반드시 정규화를 해주어야 할 듯 싶다.

 

여기서 이제 카테고리(범주형) 타입을 구하기 위해 범주형을 구하는 코드를 따로 썼다.

categorical_columns = train.select_dtypes(include=['object', 'category']).columns
categorical_summary = train[categorical_columns].describe()
categorical_summary
list(set(train['expert_consensus'].unique()))
['Other', 'LPD', 'Seizure', 'GRDA', 'GPD', 'LRDA']

고윳값이 어떻게 있는지 보고, 그 다음에는 내가 그린 타깃별 퍼센테이지 그래프를 똑같이 그렸다.

 

여기서 또 특이한 점은, patient_id에 대한 histplot을 그렸다는 것.

 

plt.figure(figsize=(10, 6))
sns.histplot(train['patient_id'], bins=30, kde=False)
plt.title('Distribution of Patient ID')
plt.xlabel('Patient ID')
plt.ylabel('Count')
plt.show()

bins를 지정한걸로 봐선, 추이를 보고 싶었나 보다.

그리고 나서는 각 vote에 대한 히스토그램 그래프를 그렸다.

어찌보면 당연한 게, vote값이 값별로 차이가 있으므로 그 개수가 몇개나 0이 되는지를 일단 살펴보는 것이다. 

targets = ['seizure_vote', 'lpd_vote', 'gpd_vote', 'lrda_vote', 'grda_vote', 'other_vote']

plt.figure(figsize=(15, 10))
for i, column in enumerate(targets, 1):
    plt.subplot(2, 4, i)
    sns.histplot(train[column], kde=False, bins=30)
    plt.title(column)
plt.tight_layout()

0이 무려 8만개 가까이 된다..

그 후에는 vote에서의 상관계수를 구했다.

correlation_targets = train[targets].corr()
plt.figure(figsize=(12, 8))
sns.heatmap(correlation_targets, annot=True, cmap='coolwarm', fmt=".2f")
plt.title('Correlation Matrix of Vote Columns')
plt.show()

plt.figure(figsize=(12, 10))
for i, column in enumerate(targets, 1):
    plt.subplot(3, 2, i)
    sns.violinplot(data=train, x='expert_consensus', y=column)
    plt.title(f'Distribution of {column} by Expert Consensus')

plt.tight_layout()
plt.show()

추가로 바이올린 플롯도 그렸는데, 이는 필자가 구했던 barplot과 양상이 똑같다.

 

이걸 보면서, EDA의 작성자는 seizure_vote와 grda_vote, other_vote 사이에 관계가 있다고 결론지었는데, 아무래도 0.2값을 기준으로 본 게 아닐까 싶다.

 

추가적으로 모든 vote 간의 pair 플롯도 그렸다.

sns.pairplot(train[targets])
plt.suptitle('Pairwise Relationships of Target Votes', y=1.02)
plt.show()

pair 플롯을 보니, 몇몇개는 확실히 선형성도 띄고 있다는 걸 알 수 있다.

하지만 몇몇개는 선형성이 부족해보인다. 이상치도 몇개 있다.

 

저런 이상치에 대한건 아마 갭을 기준으로 제거해야할 듯 싶다.

 

offset seconds에 대한 탐구

그러고 보면, offset second에 대해서는 생각해 본적이 없었다.

시계열 데이터인 만큼, 개수가 몇개나 되는지를 확인하는 절차였다.

 

확실히 이상한 게, discribe를 보면 offset_seond의 50%는 26인데, max는 무려 3372다,,,

이럴 때는 반드시 정규화를 통해 정규분포를 만들어야 한다. 왜도가 너무 지나치다.

offset_stats = train[['eeg_label_offset_seconds', 'spectrogram_label_offset_seconds']].describe()

plt.figure(figsize=(12, 6))
sns.histplot(train['eeg_label_offset_seconds'], bins=30, kde=True)
plt.title('Distribution of EEG Label Offset Seconds')
plt.xlabel('EEG Label Offset Seconds')
plt.ylabel('Count')
plt.show()

plt.figure(figsize=(12, 6))
sns.histplot(train['spectrogram_label_offset_seconds'], bins=30, kde=True)
plt.title('Distribution of Spectrogram Label Offset Seconds')
plt.xlabel('Spectrogram Label Offset Seconds')
plt.ylabel('Count')
plt.show()

offset_stats

 

	eeg_label_offset_seconds	spectrogram_label_offset_seconds
count	106800.000000	106800.000000
mean	118.817228	520.431404
std	314.557803	1449.759868
min	0.000000	0.000000
25%	6.000000	12.000000
50%	26.000000	62.000000
75%	82.000000	394.000000
max	3372.000000	17632.000000

 

뭔가 이상한 걸 보셨는지 결국엔 sort를 통해 몇개인지와 값을 나열했다.

 

all_eeg_label_offset_seconds = sorted(list(train['eeg_label_offset_seconds'].unique()))
len(all_eeg_label_offset_seconds), str(all_eeg_label_offset_seconds[0:5]), str(all_eeg_label_offset_seconds[-5:])

>(1502, '[0.0, 2.0, 4.0, 6.0, 8.0]', '[3352.0, 3362.0, 3364.0, 3366.0, 3372.0]')

all_spectrogram_label_offset_seconds = sorted(list(train['spectrogram_label_offset_seconds'].unique()))
len(all_spectrogram_label_offset_seconds), str(all_spectrogram_label_offset_seconds[0:5]), str(all_spectrogram_label_offset_seconds[-5:])

>(4686,
 '[0.0, 2.0, 4.0, 6.0, 8.0]',
 '[17622.0, 17624.0, 17626.0, 17630.0, 17632.0]')

 

 

ID에 대한 고찰

 

이번엔 groupby를 사용해서 eeg_id 별 'eeg_sub_id'의 고유값 개수를 나타냈다.

 

이런 방식으로도 groupby를 사용할 수 있다는 건 처음 알았다.

 

이 방법은 반드시 메모해놓자.

eeg_sub_id_count_per_eeg_id = train.groupby('eeg_id')['eeg_sub_id'].nunique()
print(eeg_sub_id_count_per_eeg_id)
spectrogram_sub_id_count_per_spectrogram_id = train.groupby('spectrogram_id')['spectrogram_sub_id'].nunique()
eeg_id
568657         4
582999        11
642382         2
751790         1
778705         1
              ..
4293354003     1
4293843368     1
4294455489     1
4294858825     5
4294958358     1
Name: eeg_sub_id, Length: 17089, dtype: int64

그리고 나서 그에 대한 그래프를 그렸다.

 

plt.figure(figsize=(12, 6))
sns.histplot(eeg_sub_id_count_per_eeg_id, bins=50, kde=True)
plt.title('EEG Sub-ID Count per EEG ID')
plt.xlabel('Count of EEG Sub-ID per EEG ID')
plt.ylabel('Frequency')
plt.show()

plt.figure(figsize=(12, 6))
sns.histplot(spectrogram_sub_id_count_per_spectrogram_id, bins=50, kde=True)
plt.title('Spectrogram Sub-ID Count per Spectrogram ID')
plt.xlabel('Count of Spectrogram Sub-ID per Spectrogram ID')
plt.ylabel('Frequency')
plt.show()
 

 

의견 별 vote count

이건 필자가 찾던 코드.

consensus 별 vote count를 만들고 싶은데 어떻게 해야할 지 몰라서 헤맸다.

 

여기서는 groupby로 한방에 해결하는 걸 볼 수 있었다. 일단 범주형으로 그룹화하고, vote 컬럼(연속형)에 대해서 sum한다.

확실히 여기까지 보니, groupby를 활용하는 방식을 알 수 있을 것 같다.

vote_counts_by_consensus = train.groupby('expert_consensus')[targets].sum()

plt.figure(figsize=(12, 8))
vote_counts_by_consensus.plot(kind='bar', stacked=True)
plt.title('Overall Vote Counts by Expert Consensus')
plt.xlabel('Expert Consensus')
plt.ylabel('Total Votes')
plt.xticks(rotation=45)
plt.legend(title='Vote Types')
plt.show()

그리고 나서는 eeg_labe_offset_seconds 별로도 vote 수를 sum 해주었다.

 

cumulative_votes = train.groupby('eeg_label_offset_seconds')[targets].sum().cumsum().reset_index()

plt.figure(figsize=(12, 8))
for column in targets:
    plt.plot(cumulative_votes['eeg_label_offset_seconds'], cumulative_votes[column], label=column)

plt.title('Vote Counts Over EEG Label Offset Seconds')
plt.xlabel('EEG Label Offset Seconds')
plt.ylabel('Total Votes')
plt.legend()
plt.show()

여기서 중요한 점은, second가 500이상에서 gpd가 침범한다는 것.

그에 반해 

cumulative_votes = train.groupby('spectrogram_label_offset_seconds')[targets].sum().cumsum().reset_index()

plt.figure(figsize=(12, 8))
for column in targets:
    plt.plot(cumulative_votes['spectrogram_label_offset_seconds'], cumulative_votes[column], label=column)

plt.title('Vote Counts Over Spectrogram Offset Seconds')
plt.xlabel('EEG Label Offset Seconds')
plt.ylabel('Total Votes')
plt.legend()
plt.show()

spectrogram을 기준으로 했을 때는 침범하지 않는다.

그리고 무엇보다, 각각 1000, 5000이상으로 하면 count 수치의 변화가 거의 없다.

 

따라서, 정규화를 할 때도 그 이상을 이상치로 규정하고 모델링에 포함시키면 될 것 같다.

 

그래서인지 작성자도 이런 질문을 던졌다.

is < 1000 offset is the major information of the targets except for gpd target ?

 

1000이하의 오프셋 변수가 major information, 즉, 모델링에 있어서 가장 예측적인 수치인가?

이에 대해서는 test 셋에 따라 달라질 것이므로, 작성자는 직접 LB에 모델을 던져보면서 확인했다.

 from collections import Counter
target_votes = Counter(list(train['expert_consensus']))
target_votes = {f"{k.lower()}_vote":v for k,v in target_votes.items()}
total_votes = sum([v for _,v in target_votes.items()])
mean_vote_ratio = {k:(target_votes[k]/total_votes) for k,target in target_votes.items()}

 

with Equal probability 1/6: LB 1.09

With Overlapping LB: 1.1

seizure_vote  0.196002,
gpd_vote  0.156386,
lrda_vote 0.155805,
other_vote 0.17610,
grda_vote 0.17660,
lpd_vote 0.139101

 

이에 관해서 보면 test set이 train set과 가까운 means(평균)을 가졌을 때, LB(리더보드) 수치가 비슷한걸 봐서는 그렇다고 결론을 내릴 수 있다.

Conclusion: Test set have means closer to Train Set

 

with non-overlapping based on spectrogram id
LB: 1.0

seizure_vote    0.174031
lpd_vote        0.112700
gpd_vote        0.090854
lrda_vote       0.071484
grda_vote       0.136408
other_vote      0.414523

with non-overlapping based on eeg id
LB: 0.97

seizure_vote    0.152810
lpd_vote        0.142456
gpd_vote        0.104062
lrda_vote       0.065407
grda_vote       0.114851
other_vote      0.420414

Conclusion: Test set have non-overlapping egg based sequences

마찬가지로, non-overlapping eeg(중첩이 아닌 eeg)다.

즉, eeg는 중복을 피해야 한다.

 

test.csv Metadata for the test set. As there are no overlapping samples in the test set, many columns in the train metadata don't apply.

이는 이미 overview에서 명시했지만, 다시 한번 체크

 

환자별 vote의 합

total_votes_per_pat = train.groupby('patient_id')[targets].sum().sum(axis=1) 
#각 환자의 id 별로 targets을 sum 하고, 그 vote의 총 수를 구한다
normalized_votes = train.groupby('patient_id')[targets].sum().div(total_votes_per_pat, axis=0)
#각 환자의 traget을 나눈 걸 id별 총 수로 나눈다. -> 퍼센테이지로 변환하여 정규화
mean_vote_ratio = normalized_votes.mean() #각 열에 대해서 평균으로 만든다
print( mean_vote_ratio )

with non-overlapping based on p id
LB: 1.28

seizure_vote 0.310718
lpd_vote 0.046279
gpd_vote 0.051885
lrda_vote 0.081796
grda_vote 0.231471
other_vote 0.277851

Conclusion: Test set have multiple patient_id sequences

patient_id로 모델을 쓰면 LB가 오히려 성능이 떨어졌으므로,test set에서 patient_id는 여러 시퀀스(순서)를 가지고 있다. 즉, 순차적이지 않은 모델링은 악영향을 미칠 수도 있을 것 같다.

Overall : Test set have repeat patients with "non overlapping eeg".

 

분석 결과 : 일부 피처 값 정규화 필요. eeg에 대해서 중복을 피해야 한다.  patient_id에 대해서는 순차화 필요.

 

확실히 EDA에 대해서 많은 인사이트를 알아갈 수 있는 탐구였다.

특히나 groupby의 사용법과, 이번 프로젝트를 어떻게 해야할 지 방향성을 잡은 것 같다.

이 정도의 분석을 할 수 있을 때까지 정진해야겠다.

반응형