티스토리 뷰
https://dacon.io/competitions/official/236439/overview/description에서 진행된 부동산 허위매물 분류 해커톤에 참가해 EDA, 전처리, 모델링을 통해 데이터의 숨은 특성을 찾고 정확한 분류 모델을 구현하고자 하였습니다.
부동산 허위매물 분류 해커톤: 가짜를 색출하라! - DACON
분석시각화 대회 코드 공유 게시물은 내용 확인 후 좋아요(투표) 가능합니다.
dacon.io
최근 법을 위반한 부동산 허위 매물이 증가하는 추세지만, 소비자는 허위 매물을 객관적으로 판단할 기준이 모호하여 경제적인 피해를 입는 사태가 증가하고 있습니다. 이를 해결하기 위해 위 데이터와 머신러닝을 이용해 신뢰성 있는 부동산 거래 시장을 만들고자 하였습니다.
변수명 | 자료형 | 변수명 | 자료형 |
허위매물여부 (Target) | int (0,1) | 방향 | object |
ID : 샘플 별 고유 id | object | 방수 | float |
매물확인방식 | object(현장, 전화, 서류) | 욕실 수 | float |
보증금 | float | 주차가능여부 | object |
월세 | int | 총 주차대수 | float |
전용면적 | float | 중개사무소 | object |
해당층 | float | 제공플랫폼 | object |
총층 | float | 게재일 | object |
관리비 | int |
EDA & Preprocessing
1. 종속변수(허위매물여부)의 빈도 수와 결측치 수 확인
분류 예측의 Target이 되는 '허위매물여부' 변수의 빈도수를 확인해 본 결과 허위 매물이 아님 (0)의 빈도 수가 압도적으로 많은 것을 확인할 수 있다. 분류 예측 모델 개발의 경우 종속변수의 클래스 수가 불균형한 경우 성능이 저하되는 것을 방지해야 한다. 아울러 데이터셋에 대한 결측치를 확인해 본 결과, 총 6개의 칼럼에 결측치가 존재한다.

변수명 | 결측치 수 |
전용면적 | 787 |
해당층 | 229 |
총 층 | 16 |
방수 | 16 |
욕실수 | 18 |
총 주차대수 | 696 |
2. 제공플랫폼을 빈도 수에 따라 그룹화
13개의 제공 플랫폼을 4개의 제공 플랫폼으로 묶어 범주형 변수의 카디널리티 수를 줄였습니다. (변수가 가질 수 있는 고유한 값의 개수, 카디널리티가 클수록 차원이 커져 성능이 저하될 수 있다.) 빈도수를 기준으로 상위 3개와 나머지 플랫폼 (기타 플랫폼)으로 그룹화하였습니다.
platform_counts = train_df['제공플랫폼'].value_counts()
train_df['제공플랫폼'] = train_df['제공플랫폼'].apply(lambda x: x if platform_counts[x] >= 186 else '기타플랫폼')

3. 게재일을 '연도_분기'로 변환
object 형 칼럼인 게재일을 연도_분기로 변환 (2024-01-01 => 2024_01)
train_df['게재일'] = pd.to_datetime(train_df['게재일'], errors='coerce')
train_df['분기별'] = train_df['게재일'].dt.year.astype(str) + '_Q' + train_df['게재일'].dt.quarter.astype(str)

4. 매물 유형에 대한 파생변수 생성
한국 감정원의 분류에 따라 부동산 매물을 보증금과 월세를 활용해 준월세, 준전세, 월세로 분류하기
- 12 X 월세 <= 보증금 < 240 X 월세 : 준월세
- 보증금 > 240 X 월세 : 준전세
- 보증금 < 12 X 월세 : 월세
train_df.loc[(train_df['보증금'] >= train_df['월세']*12) & (train_df['보증금'] < train_df['월세']*240), '매물유형'] = '준월세'
train_df.loc[train_df['보증금'] > train_df['월세']*240, '매물유형'] = '준전세'
train_df.loc[train_df['보증금'] < train_df['월세']*12, '매물유형'] = '월세'

5. 권역별 중개사무소에 대한 파생변수 생성
중개사무소를 알파벳으로 변환 후 빈도 수로 인코딩 (T93NT61210 -> T -> T의 빈도수 32)
#대문자로 통일 후 알파벳 별 빈도수를 train_df['권역별중개사무소']에 저장
train_df['중개사무소'] = train_df['중개사무소'].str.upper()
train_df['중개사무소'] = train_df['중개사무소'].str[0]
first_letter_frequency = train_df['중개사무소'].str[0].value_counts().to_dict()
# 권역별중개사무소 칼럼에 첫 글자 빈도수 할당
train_df['권역별중개사무소빈도수'] = train_df['중개사무소'].map(first_letter_frequency)

6. 평수, 세대 당 주차대수에 대한 파생변수 생성
- 전용면적을 평수로 변환 (평수 = 전용면적 * 0.3025)
- 주택 건설 기준에 따라, 전용면적이 30 미만이면 세대당 주차대수는 0.5 / 전용면적이 30 이상이면 세대당 주차대수는 0.6
- 세대수 = 총 주차대수 / 세대당 주차대수
#전용면적을 활용한 평수 계산
train_df['평수'] = train_df['전용면적']*0.3025
#세대당 주차대수와 세대수에 대한 파생변수 생성
train_df.loc[train_df['전용면적']<30, '세대당주차대수'] = 0.5
train_df.loc[train_df['전용면적']>=30, '세대당주차대수'] = 0.6
train_df['세대수'] = train_df['총주차대수'] / train_df['세대당주차대수']
7. 결측치 처리
- 방수와 욕실수가 모두 결측치인 데이터는 삭제, 해당층과 층층이 모두 결측치인 데이터는 삭제, 전용면적과 총 주차대수가 모두 결측치인 데이터는 삭제
- 해당층, 총층의 결측치인 층층의 평균으로 대체 -> 해당층의 결측치를 해당층의 평균으로 대체 시, 총층보다 큰 값이 존재하므로 층층의 평균으로 대체하는 방법 선택
- 욕실 수가 1이면, 방 수의 결측치를 1로 대체 / 욕실 수가 2 이상이면, 방 수의 결측치를 2로 대체 -> 욕실 수가 2개 이상일 때, 방 수가 1개인 경우는 없으므로 방 수의 결측치를 2로 대체
- 주차가능여부가 '불가능'인 경우 총 주차대수의 결측치를 평균으로 대체 / 주차가능여부가 '가능'인 경우 총 주차대수의 결측치를 중앙값으로 대체 -> 주차가능여부가 불가능하다는 것은 남아있는 주차공간이 없다는 것, 즉 총 주차대수가 많다는 것을 의미하므로 큰 값이 총 주차대수의 평균으로 대체
8. 범주형 변수 인코딩
- 중개사무소, 방향, 주차가능여부 등의 칼럼은 0부터 1씩 증가시키는 방법으로 인코딩하는 LabelEncoding 활용
- 카테고리 수가 많은 제공플랫폼 칼럼은 Target에 더 큰 영향을 미치는 독립변수 값을 더 큰 숫자로 인코딩하는 TargetEncoding 활용
#방수와 욕실수가 모두 결측치인 데이터는 삭제
idx = train_df[(train_df['방수'].isnull()) & (train_df['욕실수'].isnull())].index
#해당층과 총층이 모두 결측치인 데이터는 삭제
idx = train_df[(train_df['해당층'].isnull()) & (train_df['총층'].isnull())].index
#전용면적과 총주차대수가 모두 결측치인 데이터는 삭제
idx = train_df[(train_df['전용면적'].isnull()) & (train_df['총주차대수'].isnull())].index
#해당층과 총층에 대한 결측치 채우기
train_df['해당층'].fillna(train_df['총층'].mean(), inplace=True)
#결측치 채우기(반올림하지 않는 칼럼)
train_df['욕실수'] = train_df['욕실수'].fillna(1)
train_df['전용면적'].fillna(train_df['전용면적'].mean(), inplace=True)
train_df['평수'].fillna(train_df['평수'].mean(), inplace=True)
#총주차대수에 대한 결측치 처리
value1 = train_df['총주차대수'].median()
train_df.loc[(train_df['주차가능여부'] == '가능') & (train_df['총주차대수'].isnull()), '총주차대수'] = value1
value2 = train_df['총주차대수'].mean()
train_df.loc[(train_df['주차가능여부'] == '불가능') & (train_df['총주차대수'].isnull()), '총주차대수'] = value2
#방수에 대한 결측치 처리
condition1 = (train_df['욕실수']==1) & (train_df['방수'].isnull()) #욕실 수가 1개이면 1로 대체
train_df.loc[condition1, '방수'] = 1
condition3 = (train_df['욕실수']>1) & (train_df['방수'].isnull()) #욕실 수가 2개 이상이면 2로 대체
train_df.loc[condition3, '방수'] = 2
#범주형 칼럼에 대한 라벨인코딩
label_encode_cols = ['중개사무소','방향', '주차가능여부', '분기별', '매물유형', '매물확인방식']
label_encoders = {}
for col in label_encode_cols:
le = LabelEncoder()
train_df[col] = le.fit_transform(train_df[col].astype(str))
test_df[col] = test_df[col].astype(str).apply(lambda x: le.transform([x])[0] if x in le.classes_ else 0)
label_encoders[col] = le
encoder = TargetEncoder()
categorical_list = ['제공플랫폼']
train_df[categorical_list] = encoder.fit_transform(train_df[categorical_list], train_df['허위매물여부'])
test_df[categorical_list] = encoder.transform(test_df[categorical_list])
Modeling
1. 데이터를 여러 기준으로 단계를 나누어 학습이 거듭될수록 성능이 개선되는 Lightgbm을 활용
-> 학습 속도가 빠르고 leaf - wise 방식으로 data의 가장 중요한 부분까지 깊게 학습
-> leaf - wise 방식은 성능을 가장 낮추는 분할 기준을 선택해 이를 개선하는 방식
2. Optuna 프레임워크를 활용한 하이퍼파라미터 튜닝
파라미터 | 설명 | 튜닝 값 |
num_leavs | 생성할 트리의 최대 리프노드 개수 | 5 |
max_depth | 생성할 트리의 최대 깊이 | 9 |
learning_rate | 새로 추가되는 트리가 기존 트리를 얼마나 수정할지의 값 | 0.05 |
n_estimators | boosint을 반복할 횟수 | 350 |
scale_pos_weight | 종속변수의 클래스 수가 불균형한 경우, 적은 데이터가 있는 클래스를 더 중요하게 다루게 하는 조정값 | 2 |
3. 최종 평가 지표
- accuracy : 0.98
- precision : 0.864
- recall : 0.95
- f1 score : 0.905
(개인적으로 만족할만한 성능, 파생변수를 생성하는 것에 집중해서 부동산 관련 도메인 지식도 쌓고 어느 프로젝트보다 EDA에 시간을 투자한 보람이 있었다~)
'프로젝트' 카테고리의 다른 글
[Regression] Used Car Price Prediction (0) | 2025.03.01 |
---|---|
[Data Visualization] Titanic 데이터 시각화 (0) | 2025.01.22 |
[Time Series Forecasting] FinaceDataReader을 이용한 LSTM 시계열분석 (1) | 2024.06.21 |
[Computer Vision] Deepfake classification (0) | 2024.06.19 |
- python #프로그래머스 #겹치는선분의길이
- nlp #토큰화 #nltk #konply
- #opencv #이미지 읽기 #이미지 제작 #관심영역 지정 #스레시홀딩
- #프로그래머스 #안전지대 #시뮬레이션
- python #deque #프로그래머스 #공 던지기 #문자열 계산하기 #코딩테스트
- #seq2seq #encoder #decoder #teacher forcing
- #polars #대용량 데이터셋 처리
- 로지스틱 회귀 #오즈비 #최대우도추정법 #머신러닝
- #attention #deeplearning
- 파이썬 #코딩테스트 #프로그래머스 #스택 #큐
- # 프로그래머스 #연속된 부분수열의 합 #이중 포인터 #누적합
- subquery #sql 코딩테스트
- #tf idf
- 자연어 처리 #정제 #정규표현식 #어간 추출 #표제어 추출
- 파이썬 #시각화 #matplotlib
- seaborn #데이터시각화 #타이타닉
- # 프로그래머스 # 카펫 # 완전탐색
- 머신러닝 #xgboost #
- randomforest #bagging #머신러닝 #하이퍼파라미터 튜닝
- #python #프로그래머스 #외계어사전 #itertools #순열과조합
- #opencv #이미지 연산 #합성
- python #프로그래머스 #리스트 #인덱싱
- 잘라서 배열로 저장하기 #2차원으로 만들기
- #자연어 처리 #정수 인코딩 #빈도 수 기반
- 프로젝트 #머신러닝 #regression #eda #preprocessing #modeling
- # 할인행사 #counter #딕셔너리 #프로그래머스
- 머신러닝 #project #classification #dacon
- 머신러닝 #lightgbm #goss #ebf
- pca #주성분분석 #특이값분해 #고유값분해 #공분산행렬 #차원의 저주
- #물고기 종류별 대어 찾기 #즐겨찾기가 가장 많은 식당 정보 출력하기 #mysql #programmers