본문 바로가기
Personal Projects/Dacon

[Dacon] 채무 불이행 여부 예측 해커톤 (2) - EDA

by muns91 2025. 3. 31.
채무 불이행 여부 예측 해커톤 - EDA

 

 이번 글은 채무 불이행 여부 예측 해커톤에서 수행했던 탐색적 데이터 분석(EDA)에 대한 글입니다. 대회를 수행하면서 EDA는 데이터는 어떤 데이터이며, 컬럼은 무엇이고 성능을 끌어올리기 위해 어떤 것들을 해야될지 고민하고 분석하는 과정이라고 할 수 있을 것 같습니다.  그럼 제가 대회를 통해서 어떤 것을 고민했는 지 살펴보도록 하겠습니다. 

 

내용 요약

1. 데이터 확인

2. 데이터 정보 확인

- 정보

- 결측치

3. 그래프 & 이상치

- 기본 그래프 확인

- 기준에 따른 그래프 확인

- '왜도' 보정

- 상관 그래프 확인

4. 컬럼 Drop

5. 파생 변수 추출하기

 

 

Dacon 채무 불이행 여부 대회 링크

: https://dacon.io/competitions/official/236450/overview/description

 

채무 불이행 여부 예측 해커톤: 불이행의 징후를 찾아라! - DACON

분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.

dacon.io


탐색적 데이터 분석 (Exploratory Data Anaysis, EDA)

 

1. 데이터 확인

# 학습/평가 데이터 로드
train_df = pd.read_csv('./train.csv').drop(columns=['UID'])
test_df = pd.read_csv('./test.csv').drop(columns=['UID'])

train_df.head(5)

 

print(train_df.columns.tolist())

 

 먼저, Data Load 이후에는 head()를 통해서 대략적인 데이터를 확인을 해봅니다. 여기서 다른 분야의 데이터도 마찬가지겠지만, 컬럼을 따로 뽑아서 살펴볼 때 모르는 용어 혹은 낯선 용어가 있으면 별도의 검색을 통해서 공부를 해보시는 것을 추천드립니다. 이렇게 컬럼에 있는 용어에 대한 공부를 해두시면 나중에 파생 변수를 생성하거나 나중에 면접 질문에 대비를 할 수 있으니, 개인적으로 모델 결과에 대한 '인사이트'를 발견하시는 것도 좋지만 컬럼 요소 혹은 컬럼 명에 대해서도 공부해두시는 것을 추천드립니다. 

 

2. 데이터 정보확인

train_df.describe()

train_df.info()

 

 대략적으로 데이터를 확인했으면 다음으로는 describe()와 info()를 통해서 데이터의 정보를 확인합니다. describe()를 통해서는 기본적으로 평균, 편차, 최대, 최소값 등을 확인해보실 수 있지만, 약간 더 깊게 들어가면 평균과 중앙값 비교를 통해서 대략적으로 이 컬럼의 데이터가 왜도가 있을 것이다 없을 것이다를 예측해보실 수 있습니다. 그리고 info()를 통해서 그래프를 그리기 전에 object 데이터가 무엇인지 숫자 혹은 연속형 데이터가 무엇인지를 확인할 수 있습니다.

더보기

train_df.isnull().sum()

 

 이후에는 그래프를 그리기 전에 isnull().sum()을 통해서 데이터의 결측치가 있는가를 확인해봅니다. 다행히도 이번 데이터에서는 train과 test 모두 결측치가 없으니, 여기서는 넘어가도록 하겠습니다. 

 

3. 그래프 & 이상치

# 바 그래프 그리기
plt.figure(figsize=(9, 7))
sns.barplot(x=default_percentages.index, y=default_percentages.values, palette='viridis')

# 그래프 제목과 레이블 추가
plt.title('채무 불이행 여부 비율 (%)')
plt.xlabel('채무 불이행 여부')
plt.ylabel('비율 (%)')
plt.xticks(ticks=[0, 1], labels=['0', '1'], rotation=0)
# y축 눈금을 퍼센트로 설정
plt.yticks(np.arange(0, 101, 10))  # 0부터 100까지 10 단위로 설정
plt.gca().set_ylim(0, 100)  # y축 범위 설정
# 퍼센트 값 텍스트 추가
for i, v in enumerate(default_percentages.values):
    plt.text(i, v + 1, f"{v:.1f}%", ha='center', va='bottom')

# 그래프 표시
plt.show()

 이제 그래프를 살펴보도록 하겠습니다. 일단 저 같은 경우, 분류해야되는 데이터의 불균형을 확인하기 위해 라벨 데이터의 비율을 살펴보는 편입니다. 대략 0.65:0.34 정도 되니, 이 정도는 애교로 불균형처리는 없다고 결정하고 넘어가는 편입니다.  

 

# 컬럼별 시각화
for col in train_df.columns:
    plt.figure(figsize=(8, 4))
    
    if train_df[col].dtype == 'object':  # 범주형 데이터
        train_df[col].value_counts().plot(kind='bar', color='skyblue', edgecolor='black')
        plt.ylabel('Count')
    else:  # 수치형 데이터
        train_df[col].hist(bins=30, color='cornflowerblue', edgecolor='black')
        plt.ylabel('Frequency')
    
    plt.title(col)
    plt.xlabel(col)
    plt.grid(axis='y', linestyle='--', alpha=0.7)
    plt.show()

 

 

 

 일단 기본 그래프를 확인할 때에는 히스토그램도 있고 여러 가지 방법이 있지만 저는 대략적으로 bar 그래프를 통해서 전체 데이터를 살펴보는 편입니다. 위와 같은 데이터를 확인함으로서 저는 데이터의 왜도를 살펴보는 편입니다. 일단 보면 연간 소득, 신용 거래 연수, 현재 미상환 신용액 등이 왼쪽 혹은 오른쪽으로 치우쳐져 있으니 왜도가 있는 값이라고 체크하고 넘어갑니다. 물론 이 과정에서 기준을 '채무 불이행 여부'에 따른 Bar 그래프를 컬럼 별로 각각 비교하는 방법도 있습니다. (저는 이미 나눠서 봐도 큰 특징을 발견하지 못했기 때문에 일단은 넘어가도록 하겠습니다.)

 

# '채무 불이행 여부' 컬럼을 제외한 데이터프레임 생성
filtered_columns = train_df.drop(columns=['채무 불이행 여부'])

# '신용 점수' 컬럼과 나머지 컬럼 간의 산점도 그리기
for column in filtered_columns.columns:
    if column != '신용 점수':  # '신용 점수' 자신과 비교하지 않도록 제외
        plt.figure(figsize=(6, 4))
        sns.scatterplot(x=train_df['신용 점수'], y=train_df[column], alpha=0.5)
        plt.title(f'신용 점수 vs {column}')
        plt.xlabel('신용 점수')
        plt.ylabel(column)
        plt.grid()
        plt.show()
        print()

 

 다음은 컬럼 간의 관계를 확인하기 위해서 산점도(Scatter)를 사용해봅니다. 이 과정에서 정말 많은 고민을 하고 이상치를 발견할 수 있는 단계라고 생각합니다. 저같은 경우는 신용 점수를 기준으로 다른 그래프를 확인해보는 과정을 살펴보았습니다. 아래 그림들을 보시면 데이터 구간이 뭉쳐있는 것을 확인할 수 있는 것과 혼자 튀는 것도 확인하실 수 있을 것입니다. 이런 것들을 이상치라고 결정을 내리면 이것을 없애던지 아니면 평균 혹은 최빈값으로 대체를 할 것인가 등을 결정하고 넘어가봅니다. (참고로 이번 데이터에서는 이것 저것 다 해보았지만 큰 영향을 미치지 않고 갯수도 적어도 그냥 넘어갔습니다.)

 

# 변환 전 데이터 복사
df_original = train_df.copy()

# 새로운 변환 적용
df_transformed = train_df.copy()
df_transformed["월 소득"] = np.sqrt(df_transformed["월 소득"])
df_transformed["잔여 월 소득"] = np.sqrt(df_transformed["잔여 월 소득"])
df_transformed["월 상환 부채액"] = np.sqrt(df_transformed["월 상환 부채액"])
df_transformed["신용 점수"], _ = boxcox(df_transformed["신용 점수"] + 1)

# 컬럼별 히스토그램 비교
fig, axes = plt.subplots(nrows=2, ncols=4, figsize=(16, 8))
columns = ["월 소득", "잔여 월 소득", "월 상환 부채액", "신용 점수"]

for i, col in enumerate(columns):
    # 변환 전
    sns.histplot(df_original[col], bins=30, kde=True, ax=axes[0, i], color="blue")
    axes[0, i].set_title(f"변환 전: {col}")

    # 변환 후
    sns.histplot(df_transformed[col], bins=30, kde=True, ax=axes[1, i], color="red")
    axes[1, i].set_title(f"변환 후: {col}")
plt.tight_layout()
plt.show()

 

 

 다음은 '왜도'에 대한 보정입니다. 왜도 같은 경우는 데이터의 분포가 한쪽으로 치우친 정도를 나타내는 데, 일반적으로 정규 분포를 가정하는 ML 모델과 통계 분석에서는 높은 왜도가 있는 데이터가 문제를 일으킬 수 있습니다. 그래서 왜도에 대해서 보정이 필요한 데, 거기에는 제곱근 변환(Square Root Transformation), Box-Cox 변환이 있습니다. 각 기법은 분포의 방향에 따른 완화 그리고 정규성을 높이는 데, 사용되는 기법입니다. 그래서 저는 대표적으로 몇 가지 컬럼에 대해서 왜도를 보정하는 작업을 수행하였습니다. (이걸로 대회에서는 큰 변화는 없었으나, 이 또한 알아가는 과정이었기 때문에 공부해보았습니다.)

 

4. 상관 그래프 확인

# 상관관계 행렬 계산
correlation_matrix = train_df.corr()

# 타겟 변수와의 상관관계 선택
target_correlation = correlation_matrix['채무 불이행 여부']

# 정렬
sorted_target_correlation = target_correlation.sort_values(ascending=False)

# 시각화
plt.figure(figsize=(8, 6))
sns.heatmap(sorted_target_correlation.values.reshape(-1, 1), annot=True, fmt=".2f", cmap='coolwarm',
            yticklabels=sorted_target_correlation.index, cbar=True)
plt.title('Sorted Correlation Heatmap with Target Variable: 채무 불이행 여부')
plt.xlabel('Correlation Coefficient')
plt.ylabel('Features')
plt.show()

 

 

 다음은 상관 분석을 통해 '채무 불이행 여부'와 나머지 컬럼 간의 관계를 살펴봅니다. 여기서 음수(-), 양수(+) 그리고 0 을 보실 수 있는 데, 음수 쪽으로 간다거나 완전 0 이 된다고 해서 컬럼을 Drop하시면 안됩니다. 음수 또한 엄연히 음의 상관관계를 가지고 있고 0 이라고해서 아예 상관이 없다고 할 수는 없기 때문입니다. 그래서 저는 컬럼 Drop을 위해 이 상관 관계 표는 참고 용으로만 주로 쓰는 편입니다. (참고로 그래프는 '채무 불이행 여부'와 나머지 컬럼간의 관계 부분만 보여드린 것입니다.)

 

5. 컬럼 Drop

# 훈련 데이터를 X와 y로 나누기
X = train_df.drop(columns=['채무 불이행 여부', '현재 미상환 신용액','마지막 연체 이후 경과 개월 수', '체납 세금 압류 횟수', '개인 파산 횟수', '대출 목적', '최대 신용한도', '신용 거래 연수', '개설된 신용계좌 수', '연간 소득'])
y = train_df['채무 불이행 여부']

# 테스트 데이터에서 '현재 미상환 신용액' 제외
test_df = test_df.drop(columns=['현재 미상환 신용액','마지막 연체 이후 경과 개월 수', '체납 세금 압류 횟수', '개인 파산 횟수','대출 목적', '최대 신용한도', '신용 거래 연수', '개설된 신용계좌 수', '연간 소득'], errors='ignore')

 

 다음은 컬럼 Drop 단계입니다. 저같은 경우는 기본적으로 모든 컬럼은 안고 한 번 데이터의 결과를 살펴봅니다. 이 과정에서 PCA(주성분 분석)을 통해서 몇 개의 컬럼이 적절한지를 확인할 수도 있고, 저 같이 한번 돌려보고 Feature Importance를 통해서 학습에 영향이 적은 데이터 컬럼을 확인하고 컬럼 Drop을 결정하는 방법도 있습니다. 컬럼 Drop 필수는 아니지만, 불필요한 컬럼은 데이터 학습 중 복잡성만 증가시키기 때문에 다양한 방법을 선택하셔서 컬럼을 Drop 하시는 것을 추천드립니다. (처음 하시는 분들은 감으로 Drop 하시는 데... 모든 것에는 합당한 이유가 있어야 합니다.)

 

6. 파생 변수 추출하기

train_df['월 소득'] = train_df['연간 소득'] / 12
train_df['잔여 월 소득'] = train_df['월 소득'] - train_df['월 상환 부채액']

test_df['월 소득'] = test_df['연간 소득'] / 12
test_df['잔여 월 소득'] = test_df['월 소득'] - test_df['월 상환 부채액']

 

 마지막으로는 기존 컬럼으로부터 파생 변수를 추출하는 방법입니다. 해당 기법은 기존 컬럼을 변형하거나 결합 혹은 새로운 변수를 만들어내는 과정입니다. 이 과정에서 모델 성능을 향상시키고 더 좋은 예측 결과를 얻을 수 있습니다. 저 같은 경우는 연간 소득을 12로 나누어서 '월 소득 변수', 그리고 월 소득에서 월 상환 부채액을 빼서 '잔여 월 소득'이라는 파생 변수를 만들어냈고, 신용 점수를 등급으로 바꾸어 보는 등, 다양한 시도를 하였습니다. 


■ 마무리

 여기까지 채무 불이행 여부에 대한 EDA 과정이었습니다. 이번 경진 대회는 금융과 관련이 있어서 컬럼명을 먼저 공부하는 과정에서 많은 인사이트를 얻을 수 있었습니다. 덕분에 약간의 금융 지식(?)도 생기고 최근에 언어 모델 관련 대회에서 보인 처참한(?) 성적 때문에 받은 상처로부터 힐링할 수 있었던 시간이었습니다. 언제나 늘 새로운 데이터를 접하고 이를 분석하고 좋은 결과를 만들어내는 과정에서 참 많은 것을 배우고 성장하는 것 같습니다. 그럼, 다음에는 최종 Code를 살펴보는 시간을 갖도록 하겠습니다. 

 

 

 

 

반응형