부동산 허위매물 분류 해커톤 - 최종 코드
안녕하세요! 이번에는 EDA 과정 이후의 대회에서 최종 제출한 코드를 살펴보도록 하겠습니다. 이번 최종 코드 과정에서는 지난 EDA를 통해 어떻게 성능을 올릴지 그래프와 수치를 보면서 전략을 세웠다면 이번 시간에는 최종 코드를 통해서는 여러 전략의 시도 끝에 대회에 최종적으로 어떤 전략들을 세웠는 지를 설명하도록 하겠습니다. 전반적인 과정을 아래와 같습니다.
* 최종 코드 주요 내용
1. 결측치 처리
2. 파생 변수 생성
3. 정규화 시도
4. 데이터 변형
5. Feature Drop
6. StandardScaler
7. LGBM
8. Feature Importance
데이콘 링크 : https://dacon.io/
데이터사이언티스트 AI 컴피티션
10만 AI 팀이 협업하는 데이터 사이언스 플랫폼. AI 경진대회와 대상 맞춤 온/오프라인 교육, 문제 기반 학습 서비스를 제공합니다.
dacon.io
Code Review & Process Check
전반적인 코드를 살펴보고 리뷰해보도록 하겠습니다.
개발 환경 : Google Colab
사용 언어 : Python
1. Data Load & Check
import pandas as pd
import numpy as np
import math
import seaborn as sns
import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
from lightgbm import LGBMClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import accuracy_score
from collections import Counter
from sklearn.preprocessing import LabelEncoder
# 데이터 불러오기
train = pd.read_csv('./train.csv')
test = pd.read_csv('./test.csv')
# train 데이터프레임에서 ID 컬럼 제거
train = train.drop(columns=['ID'])
# test 데이터프레임에서 ID 컬럼 제거
test = test.drop(columns=['ID'])
train.head(10)
해당 코드에서는 라이브러리 import와 함께 데이터를 불러오고 ID 컬럼을 우선적으로 제거하였습니다. 그리고 head를 통해 대략적인 데이터를 체크해봅니다.
2. 허위 매물 여부 비율 확인
# 허위매물여부 비율 계산
counts = train['허위매물여부'].value_counts()
percentages = counts / counts.sum() * 100 # 비율 계산
# 바차트 생성
plt.figure(figsize=(8, 6))
bar_plot = sns.barplot(x=counts.index, y=counts.values, palette='pastel')
plt.title('Distribution of 허위매물여부')
plt.xlabel('허위매물여부 (0: False, 1: True)')
plt.ylabel('Count')
plt.xticks(ticks=[0, 1], labels=['0 (허위매물 아님)', '1 (허위매물)'])
# 개수와 비율을 바차트 위에 추가
for p in bar_plot.patches:
bar_plot.annotate(f'{int(p.get_height())} ({p.get_height() / counts.sum() * 100:.1f}%)',
(p.get_x() + p.get_width() / 2., p.get_height()),
ha='center', va='bottom', fontsize=10)
plt.tight_layout()
plt.show()
코드를 통해 허위매물여부 정답 비율을 살펴보면 거의 9대 1의 가까운 비율이라고 할 수 있습니다. 이를 통해서 데이터 불균형을 처리해야되나를 고민할 수 도 있지만 보통 10% 이하의 정말로 소수의 데이터가 아니라면 저 정도면 준수한 편이다 라고 생각하시면 됩니다. 물론 그래도 찝찝하다면 train_test_split를 이용하여 0의 비율과 1의 비율을 맞추시던가 SMOTE 기법을 사용해서 오버샘플링을 시도하는 것도 좋은 방법이 될 것 같습니다. 일단 저는 이정도면 준수하다는 판단을 했기 때문에 비중을 늘리거나 줄이지는 않았습니다.
3. 결측치 확인 및 제거
train.isnull().sum()
# train 데이터프레임의 결측치가 있는 열 제거
train = train.dropna(axis=1)
# test 데이터프레임의 결측치가 있는 열 제거
test = test.dropna(axis=1)
일단 이 과정에서 각 결측치를 채우기 위해 그룹화 할 수 있는 컬럼을 찾으려고 시도했으나, 결측치들를 포함하고 있는 컬럼도 하나에만 집중되어 있는게 아니라, 다양하게 분포되어 있었기 때문에 일단 기준이 되는 것은 스킵하고 0과 결측치를 포함하고 있는 열 제거를 통해서 성능을 올릴 수 있었습니다.
4. 데이터 변환 및 파생 변수 생성
# 날짜 컬럼을 datetime 형식으로 변환
train['게재일'] = pd.to_datetime(train['게재일'])
test['게재일'] = pd.to_datetime(test['게재일'])
# 연도, 월, 일, 요일 추출
for df in [train, test]:
df['연도'] = df['게재일'].dt.year % 100 # 연도를 2자리로 변환
df['월'] = df['게재일'].dt.month
df['일'] = df['게재일'].dt.day
# '게재일' 컬럼 삭제
train.drop(columns=['게재일'], inplace=True)
test.drop(columns=['게재일'], inplace=True)
위 코드를 통해서 저는 yyyy-mm-dd 형태로 되어 있는 게제일 컬럼의 요소들을 연도, 월, 일로 각각 변환한 다음에 새로운 컬럼으로 생성하였습니다.
5. 데이터 분포 확인
# 서브플롯 생성
fig, axes = plt.subplots(1, 2, figsize=(14, 6), sharey=True)
# 허위매물여부 0의 밀도
sns.kdeplot(data=train[train['허위매물여부'] == 0], x='월', ax=axes[0], fill=True, color='lightblue', alpha=0.6)
axes[0].set_title('KDE of 허위매물여부 0 (허위매물 아님)')
axes[0].set_xlabel('월')
axes[0].set_ylabel('밀도')
axes[0].grid(True) # 그리드 추가
# 허위매물여부 1의 밀도
sns.kdeplot(data=train[train['허위매물여부'] == 1], x='월', ax=axes[1], fill=True, color='salmon', alpha=0.6)
axes[1].set_title('KDE of 허위매물여부 1 (허위매물)')
axes[1].set_xlabel('월')
axes[1].set_ylabel('밀도')
axes[1].grid(True) # 그리드 추가
plt.tight_layout()
plt.show()
다양한 컬럼 중에서 저는 '월' 컬럼에서 위 그래프와 같은 특징을 발견하였습니다. 왼쪽 그래프의 경우는 허위매물여부가 0일 때이고, 오른쪽은 허위매물여부 1의 그래프입니다. 1의 경우에는 월별로 분포가 정규분포의 형태를 보이는 반면, 0은 그렇지 않은 모습을 보입니다. 그래서 이러한 것을 보면서 1, 2, 8, 9, 10, 11, 12월에서 부분적으로 혹은 전체적으로 데이터를 줄여보자라는 아이디어를 수행하였고 성능이 오르는 것을 확인할 수 있었습니다.
# 허위매물여부 0과 1로 나누기
fake_data = train[train['허위매물여부'] == 0] # 허위매물여부가 0인 데이터
real_data = train[train['허위매물여부'] == 1] # 허위매물여부가 1인 데이터
print(fake_data['월'].value_counts())
remeve_num = 100
# 허위매물여부가 0인 데이터에서 랜덤으로 행 제거
desired_counts = {
8: remeve_num, # 월 8에 대해 제거할 개수
9: remeve_num, # 월 9에 대해 제거할 개수
10: remeve_num, # 월 10에 대해 제거할 개수
11: remeve_num, # 월 11에 대해 제거할 개수
12: remeve_num # 월 12에 대해 제거할 개수
}
# 각 월에 대해 랜덤으로 제거할 행 선택
for month, count in desired_counts.items():
# 해당 월의 행 추출
month_data = fake_data[fake_data['월'] == month]
# 랜덤으로 제거할 행 선택
if len(month_data) >= count:
rows_to_remove = month_data.sample(n=count, random_state=42) # 랜덤으로 제거할 행 샘플링
else:
rows_to_remove = month_data # 원하는 개수보다 적으면 모두 선택
# fake_data에서 해당 행 제거
fake_data = fake_data.drop(rows_to_remove.index)
# 허위매물여부가 0인 데이터와 1인 데이터 합치기
train = pd.concat([fake_data, real_data], ignore_index=True)
# 결과 출력
print("Updated train DataFrame after removing specified rows:")
print(train['허위매물여부'].value_counts())
6. Label Encoding
# LabelEncoder 인스턴스 생성
label_encoders = {}
label_encode_cols = train.select_dtypes(include=['object']).columns.difference(['ID']).tolist()
# train 데이터프레임의 각 object 타입 컬럼에 대해 LabelEncoder 적용 (ID 제외)
for col in label_encode_cols:
le = LabelEncoder()
train[col] = le.fit_transform(train[col])
label_encoders[col] = le # LabelEncoder 저장
# test 데이터프레임의 각 object 타입 컬럼에 대해 LabelEncoder 적용 (ID 제외)
for col in label_encode_cols:
if col in test.columns:
le = label_encoders[col]
test[col] = test[col].astype(str) # 데이터 타입을 문자열로 변환
unseen = set(test[col].unique()) - set(le.classes_) # 보지 못한 값 확인
# 보지 못한 클래스 추가
if unseen:
le.classes_ = np.append(le.classes_, list(unseen)) # 새로운 클래스 추가
# 변환
test[col] = le.transform(test[col])
이후에는 범주형 데이터에 대한 Label Encoding을 수행하였습니다. get_dummies를 사용하기에는 중개사무소의 범주가 너무 많다고 판단되었기 때문에 한 컬럼에서 범주화를 수행할 수 있는 Label Encoding을 사용하였습니다. 그리고 test와 train에서 서로 존재하지 않는 범주가 있기 때문에 이를 고려하여 범주화를 수행하였습니다.
7. 데이터 변형
# 보증금과 월세 컬럼 나누기
train['보증금'] = train['보증금'] / 100000
train['월세'] = train['월세'] / 10000
test['보증금'] = test['보증금'] / 100000
test['월세'] = test['월세'] / 10000
보증금과 월세의 경우에는 단위가 너무 크기 때문에 각각 알맞은 적당한 숫자로 나누어주는 작업을 수행하였습니다.
8. 컬럼 Drop
x_train = train.drop(['허위매물여부', '주차가능여부', '매물확인방식', '방향'], axis=1) #'주차가능여부', '매물확인방식'
y_train = train['허위매물여부']
x_test = test.drop(['주차가능여부', '매물확인방식', '방향'], axis=1)
컬럼의 경우 Drop을 하는 기준은 저같은 경우는 Correlation 그래프를 확인하고 상관 정도가 적은 컬럼을 제거하기도 합니다. 하지만 뒤에 나올 Feature Importance를 통해서 학습 중 영향력이 적은 컬럼을 Drop 하는 것도 기준이 될 수 있는 방법이기 때문에 이와 같은 방법으로 컬럼을 줄여나가는 방식을 사용하였습니다. 그 결과, 주차가능여부, 매물확인방식, 방향을 Drop 했을 때, 가장 좋은 성능이 나왔습니다.
9. 스케일러
from sklearn.preprocessing import StandardScaler
# StandardScaler 인스턴스 생성
scaler = StandardScaler()
# StandardScaler를 사용하여 x_train과 x_test 정규화
x_train_scaled = scaler.fit_transform(x_train)
x_test_scaled = scaler.transform(x_test)
다음은 스케일러입니다. 저는 MinMax 말고도 Robust 등의 다양한 스케일러로 학습 한 후에 결과를 제출하였지만 결과적으로 Standard Scaler를 사용했을 때, 결과가 가장 좋아서 해당 스케일러를 선택하게 되었습니다.
10. 모델 학습
# LightGBM Classifier 초기화
model_lgb = LGBMClassifier(
objective='binary',
metric='binary_logloss',
learning_rate=0.1,
num_leaves=31,
n_estimators=300,
random_state=42
)
# 모델 훈련
model_lgb.fit(x_train_scaled, y_train)
# 예측
predictions_lgb = model_lgb.predict(x_test_scaled)
학습에 사용된 모델은 LGBM입니다. LGBM 같은 경우 대규모 데이터셋을 빠르게 학습할 수 있는 장점이 있고 트리 기반의 모델의 성능을 극대화하면서도 학습 속도를 높이는 것에 중점을 두었다는 특징을 가지고 있기 때문에 해당 모델이 적합하다는 이유와 역시 Catboost, XGB, Deep Learning을 사용해본 결과, LGBM이 가장 좋은 성능을 보였습니다.
11. Feature Importance
# Feature importance 추출
feature_importances = model_lgb.feature_importances_
# 특성 이름 가져오기
feature_names = x_train.columns
# 중요도와 특성 이름을 데이터프레임으로 변환
importance_df = pd.DataFrame({
'Feature': feature_names,
'Importance': feature_importances
})
# 중요도에 따라 정렬
importance_df = importance_df.sort_values(by='Importance', ascending=False)
# 중요도 시각화
plt.figure(figsize=(10, 6))
plt.barh(importance_df['Feature'], importance_df['Importance'], color='skyblue')
plt.xlabel('Importance')
plt.title('Feature Importance')
plt.gca().invert_yaxis() # 내림차순으로 표시
plt.show()
마지막으로 Feature Importance는 ML 모델에서 각 특성이 목표 변수에 미치는 중요도를 나타내는 속성입니다. 이를 통해서 각 컬럼이 예측 결과에 얼마나 기여하는 지를 수치적으로 확인할 수 있습니다. 저는 이것을 통해서 컬럼을 Drop 하는 기준 중 하나로 사용합니다. 개인적으로 상관 값을 보는 것보다 더 좋을 때가 많아서, 최근에 상관 값은 거의 형식적으로 보는 용도로 사용하고 있습니다.
마무리
여기까지 전체 코드에 대한 내용이었습니다. 일단 이번에도 어떻게든 데이터로부터 인사이트를 찾아 성능을 올릴 수 있었던 귀중한 경험이 되었습니다. 역시나... 이번에도 다양한 시도(?) 덕분에 어마무시한 제출 수를 기록하게 되었지만, 일단 결과가 괜찮으니 다음에는 더 줄여보면서 앞으로 남은 대회를 박차고 이겨낼 수 있도록 해야겠습니다. 아직도 제게는 진행 중인 대회가 남아있고, 차후에는 새로운 대회가 또 열릴 터이니 열심히 전투력을 상승할 수 있는 기회로 만들어야겠습니다. 그럼 2025년 2번째 대회에 대한 후기는 여기서 마무리하도록 하겠습니다.

데이터 EDA :
2025.02.22 - [Personal Projects/Dacon] - [Dacon] 부동산 허위매물 분류 해커톤 (2) - EDA
진행 중 대회
악성 URL 분류 AI 경진 대회 :
https://dacon.io/competitions/official/236451/overview/description
악성 URL 분류 AI 경진대회 - DACON
분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.
dacon.io
채무 불이행 여부 예측 해커톤 :
https://dacon.io/competitions/official/236450/overview/description
채무 불이행 여부 예측 해커톤: 불이행의 징후를 찾아라! - DACON
분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.
dacon.io
건설공사 사고 예방 및 대응책 생성 :
https://dacon.io/competitions/official/236455/overview/description
건설공사 사고 예방 및 대응책 생성 : 한솔데코 시즌3 AI 경진대회 - DACON
분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.
dacon.io
'Personal Projects > Dacon' 카테고리의 다른 글
[Dacon] 부동산 허위매물 분류 해커톤 (2) - EDA (0) | 2025.02.28 |
---|---|
[Dacon] 부동산 허위매물 분류 해커톤 (1) - 후기 (Private 43, 상위 10%) (0) | 2025.02.28 |
[Dacon] 전기차 가격 예측 해커톤 (3) - Prediction Process (0) | 2025.01.31 |
[Dacon] 전기차 가격 예측 해커톤 (2) - EDA (0) | 2025.01.31 |
[Dacon] 전기차 가격 예측 해커톤 (1) - 후기 (최종 3위) (0) | 2025.01.31 |