머신러닝/머신러닝: 실전 프로젝트 학습
신용카드 사용자 연체 예측 프로젝트 2 : 범주형 변수 EDA 및 결측값 대체
qordnswnd123
2025. 1. 4. 16:33
1. 데이터 로드
import pandas as pd
import numpy as np
# 시각화 라이브러리
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
fe = fm.FontEntry(fname = 'MaruBuri-Regular.otf', name = 'MaruBuri')
fm.fontManager.ttflist.insert(0, fe)
plt.rc('font', family='MaruBuri')
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
train.head(5)
2. credit별 범주형 변수의 분포 확인
# 각 credit별 데이터프레임을 정의합니다.
train_0 = train[train['credit']==0.0]
train_1 = train[train['credit']==1.0]
train_2 = train[train['credit']==2.0]
# 각 credit별 데이터프레임 리스트를 정의합니다.
train_dfs = [train_0, train_1, train_2]
title = ['credit 0', 'credit 1', 'credit 2']
# credit별 범주형 변수의 분포를 확인하기 위해 함수를 정의합니다.
def category_countplot(column):
"""
category_countplot 함수에 입력할 수 있는 범주형 변수는 다음과 같습니다.
'gender', 'car', 'reality', 'income_type',
'edu_type', 'family_type', 'house_type',
'flag_mobil', 'work_phone', 'phone', 'email'
"""
fig, ax = plt.subplots(1, 3, figsize=(16, 6))
for i in range(len(title)):
sns.countplot(x=column, data=train_dfs[i], ax=ax[i], order=train_dfs[i][column].value_counts().index)
ax[i].tick_params(labelsize=12, rotation=50)
ax[i].set_title(title[i])
ax[i].set_ylabel('count')
plt.tight_layout()
plt.show()
return ax
# category_countplot 함수에 확인하고자 하는 범주형 변수를 입력하여 credit별 데이터 분포를 확인해보세요.
col_name = 'gender'
result_countplot = category_countplot(col_name)
3. 적은 등장 빈도를 가진 값 분석
# income_type과 edu_type의 각 범주에 대한 빈도를 계산하고, 각 범주명과 해당 빈도를 반환
income_type_counts = train['income_type'].value_counts()
edu_type_counts = train['edu_type'].value_counts()
# income_type과 edu_type에서 가장 빈도가 낮은 범주를 가져옴
last_income_type = income_type_counts.index[-1]
last_edu_type = edu_type_counts.index[-1]
# 가장 빈도가 낮은 income_type 범주에 대한 income_total의 평균과 중앙값 계산
last_income_type_mean = train[train['income_type'] == last_income_type]['income_total'].mean()
last_income_type_median = train[train['income_type'] == last_income_type]['income_total'].median()
# 가장 빈도가 낮은 edu_type 범주에 대한 income_total의 평균과 중앙값 계산
last_edu_type_mean = train[train['edu_type'] == last_edu_type]['income_total'].mean()
last_edu_type_median = train[train['edu_type'] == last_edu_type]['income_total'].median()
print(f"income_total의 평균은 {train['income_total'].mean()}, 중앙값은 {train['income_total'].median()}입니다.")
print('-'*60)
print(f'{last_income_type}의 income_total 평균값은 {last_income_type_mean}, income_total 중앙값은 {last_income_type_median}입니다.')
print(f'{last_edu_type}의 income_total 평균값은 {last_edu_type_mean}, income_total 중앙값은 {last_edu_type_median}입니다.')
income_total의 평균은 187306.52449257285, 중앙값은 157500.0입니다.
------------------------------------------------------------
Student의 income_total 평균값은 149142.85714285713, income_total 중앙값은 171000.0입니다.
Academic degree의 income_total 평균값은 241630.4347826087, income_total 중앙값은 270000.0입니다.
4. credit 별 job_type 분포 시각화
# job_type 칼럼의 결측값을 문자열 nan으로 채움
train['job_type'] = train['job_type'].fillna('nan')
test['job_type'] = test['job_type'].fillna('nan')
# 데이터셋 내에서 어떤 고유한 신용 등급들이 있는지 확인
credits = train['credit'].unique()
# 각 credit 값별로 job_type의 분포를 시각화
fig, axes = plt.subplots(len(credits), 1, figsize=(15, 5*len(credits)))
for i, credit in enumerate(credits):
job_type_credit = train[train['credit'] == credit]['job_type'].value_counts()
# 각 신용 등급별로 job_type의 분포를 막대그래프로 시각화
job_type_credit.plot(kind='bar', ax=axes[i])
axes[i].set_title(f'job_type 분포 (credit: {credit})', fontsize=13)
axes[i].set_xlabel('job_type', fontsize=11)
axes[i].set_ylabel('number', fontsize=11)
axes[i].set_xticklabels(labels=job_type_credit.index, rotation=45)
plt.tight_layout()
plt.show()
5. job_type의 결측값 및 income_type에 따른 분포 분석
# 'job_type' 칼럼의 값이 'nan'인 경우에 해당하는 'income_type'의 분포를 확인합니다.
print('job_type가 nan인 income_type 빈도 확인')
print(train[train['job_type'] == 'nan']['income_type'].value_counts())
print('-'*40)
# job_type에서 등장빈도가 낮은 고유값의 비율을 확인하기 위해 num_freq_low 값을 조절합니다.
# num_freq_low는 등장 빈도가 낮은 job_type 고유값의 개수를 의미합니다.
num_freq_low = 3
low_freq_sum = train['job_type'].value_counts()[-num_freq_low:].sum()
print(f'하위 {num_freq_low} 개의 고유값의 등장빈도는 전체 데이터의 {low_freq_sum / len(train) * 100}% 입니다.')
job_type가 nan인 income_type 빈도 확인
Pensioner 4440
Working 2312
Commercial associate 1026
State servant 392
Student 1
Name: income_type, dtype: int64
----------------------------------------
하위 3 개의 고유값의 등장빈도는 전체 데이터의 0.6274331934837661% 입니다.
6. 연금 수령자의 나이 분포 시각화 및 결측값 일부 대체
# 음수로 되어 있는 일자 관련 피처들을 양수로 변환
minus_col = ['days_birth', 'days_employed', 'begin_month']
for i in minus_col:
train[i] = -train[i]
test[i] = -test[i]
# 나이 피처 생성: 출생 일자를 이용하여 나이 계산
train['age'] = train['days_birth'] // 365
test['age'] = test['days_birth'] // 365
# 연금 수령자의 나이 분포를 시각화
pension_age = train[train['income_type'] == 'Pensioner']['age'].value_counts().sort_index()
plt.figure(figsize = (12,8))
sns.barplot(x = pension_age.index, y = pension_age.values)
# 연금 수령 빈도가 높은 연령 구간 표시
plt.axvspan(25, 37, color='red', alpha=0.2)
plt.show()
# job_type의 결측값 개수 출력
print('결측값 대체 전 job_type의 결측값의 수는 ', len(train[train['job_type'] == 'nan']), '개 입니다.')
# 연금 수령자(Pensioner)의 job_type을 'No job'으로 대체
train.loc[(train['job_type'] == 'nan') & (train['income_type'] == 'Pensioner'), 'job_type'] = 'No job'
test.loc[(test['job_type'] == 'nan') & (test['income_type'] == 'Pensioner'), 'job_type'] = 'No job'
# 결측치 대체 후 job_type의 결측값 개수 출력
print('결측값 대체 후 job_type의 결측값의 수는 ',len(train[train['job_type'] == 'nan']), '개 입니다.')

결측값 대체 전 job_type의 결측값의 수는 8171 개 입니다.
결측값 대체 후 job_type의 결측값의 수는 3731 개 입니다.
7. 등장 빈도가 낮은 job_type 배제 후 평균 income_total 값을 활용한 결측치 대체
# 'job_type' 결측값의 수를 확인합니다.
print('결측값 대체 전 job_type의 결측값의 수는 ',len(train[train['job_type'] == 'nan']), '개 입니다.')
# 등장 빈도가 낮은 job_type을 배제하기 위한 기준 값을 n으로 설정합니다.
n = 3
# 'job_type'의 각 범주별 등장 빈도를 확인합니다.
job_type_counts = train['job_type'].value_counts()
# 등장 빈도가 낮은 하위 n개의 'job_type' 범주를 선택합니다.
exclude_job_types = job_type_counts.tail(n).index.tolist()
# 결측치와 등장빈도가 낮은 job_type을 제외하고 각 job_type에 대해 'income_total'의 평균을 계산합니다.
mean_values = train[(train['job_type'] != 'nan') & (~train['job_type'].isin(exclude_job_types))].groupby('job_type')['income_total'].mean()
# 'job_type' 값이 'nan'인 각 행에 대해 반복합니다.
for idx, row in train[train['job_type'] == 'nan'].iterrows():
# 각 job_type의 평균값과의 차이 계산
differences = abs(mean_values - row['income_total'])
# 차이가 가장 작은 job_type을 선택합니다.
closest_job_type = differences.idxmin()
# 현재 행의 'job_type' 값을 가장 가까운 job_type으로 대체합니다.
train.at[idx, 'job_type'] = closest_job_type
# 대체 후 'job_type'의 결측값 수를 다시 확인합니다.
print('결측값 대체 후 job_type의 결측값의 수는 ',len(train[train['job_type'] == 'nan']), '개 입니다.')
결측값 대체 전 job_type의 결측값의 수는 3731 개 입니다.
결측값 대체 후 job_type의 결측값의 수는 0 개 입니다.
8. test 데이터에 등장 빈도가 낮은 job_type 배제 후 평균 income_total 값을 활용한 결측치 대체
# train 데이터에서 얻은 mean_values를 사용하여 test 데이터의 'job_type' 결측값을 대체합니다.
for idx, row in test[test['job_type'] == 'nan'].iterrows():
# 각 job_type의 평균값과의 차이 계산
# 데이터 누수(Data Leakage) 방지를 위해 train 데이터의 통계값을 사용합니다.
differences = abs(mean_values - row['income_total'])
# 차이가 가장 작은 job_type을 선택합니다.
closest_job_type = differences.idxmin()
# 현재 행의 'job_type' 값을 가장 가까운 job_type으로 대체합니다.
test.at[idx, 'job_type'] = closest_job_type
# 대체 후 test 데이터의 'job_type' 결측값 수를 확인합니다.
print('test 데이터의 결측값 대체 후 job_type의 결측값의 수는 ',len(test[test['job_type'] == 'nan']), '개 입니다.')
test 데이터의 결측값 대체 후 job_type의 결측값의 수는 0 개 입니다.