티스토리 뷰

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에 시간을 투자한 보람이 있었다~)

TAG more
글 보관함
최근에 올라온 글