머신러닝/머신러닝: 실전 프로젝트 학습

아파트 경매 가격 예측 프로젝트 4 : 데이터 결합

qordnswnd123 2025. 1. 8. 14:44

1. 데이터 로드

import pandas as pd

train = pd.read_csv('train.csv')
result = pd.read_csv('Auction_result.csv')

2. 데이터 전처리

# year 피처 생성 및 날짜 피처 제거
train['Final_auction_date'] = pd.to_datetime(train['Final_auction_date'], errors = 'ignore')
train['year'] = train['Final_auction_date'].dt.year
date_col = ['Appraisal_date', 'First_auction_date', 'Final_auction_date', 'Preserve_regist_date', 'Close_date']
train = train.drop(date_col, axis= 1)

# 최빈값으로 결측값 보완
addr_freq = train['addr_bunji1'].value_counts().index[0]
road_freq = train['road_bunji1'].value_counts().index[0]
train['addr_bunji1'] = train['addr_bunji1'].fillna(addr_freq)
train['road_bunji1'] = train['road_bunji1'].fillna(road_freq)

# 결측값 많은 피처 제거
much_null = ['addr_li', 'addr_bunji2', 'Specific', 'road_bunji2']
train = train.drop(much_null, axis = 1)

# Hammer_price를 제외하고 상관계수가 높았던 피처 제거
highcorr_col = ['Total_land_real_area', 'Total_land_auction_area', 'Total_building_area', 'Total_building_auction_area']
train = train.drop(highcorr_col, axis =1)

 


3. datetime 자료형으로 변환 및 연도 추출

result['result_year'] = pd.to_datetime(result['Auction_date'], errors = 'ignore').dt.year

result = result[result['result_year'] >= 2014]

4. Auction_key 열의 중복된 값 제거

train데이터와 result 데이터의 결합을 위해 result 데이터프레임에서 피처 엔지니어링을 수행하였습니다.
우선, 데이터셋을 결합하기 위해 Auction_key의 중복된 값을 제거하는 작업을 수행하겠습니다.

작업 순서는 다음과 같습니다.

  1. result 데이터프레임에서 Auction_keyAuction_results 피처만을 가져옵니다.
  2. drop_duplicates() 메서드를 사용해서 Auction_key 값을 기준으로 중복 제거를 수행하여, 마지막으로 등장하는 행을 유지합니다.
  3. 2번의 결과를 need_merge 변수에 지정합니다.
  4. reset_index() 메서드를 사용해서 need_merge 데이터프레임의 인덱스를 재설정합니다.

마지막에 존재하는 값이 가장 최근의 정보를 보유하고 있을 것이라 판단할 수 있기 때문에,
중복 제거를 수행할 때 마지막으로 등장하는 행을 유지하였습니다.

reset_index() 메서드는 데이터프레임의 인덱스를 재설정하는 기능을 수행합니다.
해당 메서드를 사용했을 때 0부터 시작하는 정수 인덱스로 재설정됩니다.
기존의 인덱스 정보를 제거하여 drop 매개변수를 True로 설정할 수 있습니다.
reset_index() 메서드를 사용함으로써 올바른 인덱스를 새로 부여하여,
행의 순서를 명확하게 파악할 수 있습니다.

need_merge = result[['Auction_key', 'Auction_results']].drop_duplicates(subset = 'Auction_key', keep = 'last')
need_merge = need_merge.reset_index(drop = True)
need_merge.head(5)

 


5. 데이터 결합 방향 결정

result.head(43)

 

※ 결과 해석

감정가(Appraisal_price): 감정가는 시장성, 수익성, 비용성을 고려하여 결정되는 금액을 의미합니다.
Auction_key가 1,2,3인 값은 감소하나 4,5,6,7,8,9는 값이 동일한 것을 볼 수 있습니다.
감정가는 여러 가지 요소를 고려해서 결정하며 학습 목표도 적은 편입니다.
따라서 감정가의 최소값을 새로운 피처로 생성했습니다.

 

최저매각가격(Minimum_sales_price): 최저매각가격은 경매를 진행할 때 경매 마감으로 건물을 구입할 수 없는 기본값을 의미합니다.
이 최저매각가격은 최저제시가격보다 한 금액을 낮춘 범위에 따라 다릅니다.
따라서 Auction_key 별로 최저매각가격의 최소값을 사용해서 새 피처를 생성했습니다.

 

경매회수(Auction_seq): 경매 일정이 한 회씩 종료될 때마다 회차를 의미합니다.
다시 경매를 시작하기 위해 각 Auction_key 별로 경매횟수의 최댓값을 사용해서 새 피처를 생성했습니다.

 

경매결과(Auction_result): 데이터에서 경매결과를 보았을 때 Auction_key 5,8,9 값에서만 비산 가격의 확인이 불가능한 경우를 제외하고 수요 대비 비산적인 가격에서 높은 낙찰이 있었음을 확인했습니다.

따라서 이를 반영하기 위해 유찰 횟수를 피처로 사용하였습니다.


6. Appraisal_price(감정가) 최솟값 결합

Appraisal_price: 이전에 보았듯이 그대로이거나 소폭 하락한 것을 볼 수 있습니다.
가장 최근의 감정가를 가져올 때 가장 좋은 결과를 낼 것으로 예상되기 때문에 감정가의 최소값을 가져와주겠습니다.

 

groupby() 메서드는 데이터를 그룹화하고 그룹 단위로 작업을 수행하기 위해 사용되는 기능입니다.
데이터를 카테고리별로 분류하여 해당 카테고리에 대한 통계를 계산하거나, 데이터를 특정 기준으로 집계하고자 할 때,
그룹 단위로 연산을 수행하고자 할 때 주로 사용합니다.

 

groupby() 메서드는 다음과 같은 단계를 통해 사용할 수 있습니다.

  1. 그룹화할 기준 열을 선택(코드에서는 Auction_key 열 입니다).
  2. 확인하고자 하는 열 선택(코드에서는 Appraisal_price 열을 선택했습니다).
  3. 통계 함수를 적용하거나 집계 함수를 사용(코드에서는 min 함수를 사용했습니다).

이렇게 그룹화된 데이터는 그룹별로 분석이 가능하며, 각 그룹에 대한 통계적 분석이나 집계 연산을 수행할 수 있습니다.

values 속성을 사용해 appraisal_min 열에 할당했습니다.
해당 속성을 사용하지 않는다면 인덱스가 0인 Appraisal_price 열의 값이 결측치가 됩니다.
왜냐하면 appraisal_min 변수에 있는 Series는 인덱스가 1부터 시작하기 때문에 인덱스가 0인 값은 결측치가 되기 때문입니다.

 

appraisal_min = result.groupby('Auction_key')['Appraisal_price'].min()
need_merge['appraisal_min'] = appraisal_min.values
need_merge.head()

 


7. Minimum_sales_price(최저매각가격)의 최솟값 결합

sales_min = result.groupby('Auction_key')['Minimum_sales_price'].min()
need_merge['sales_min'] = sales_min.values
need_merge.head()

 

※ 결과 해석

result 데이터프레임에는 중복된 auction_key 가 존재합니다.
그룹화를 함으로써 각 auction_key 의 값 중 원하는 값을 가져올 수 있습니다.

이번 코드에서도 이전과 마찬가지로 최소값을 가져왔습니다.
이는, 앞의 시각화 단계에서 보듯이 경매 횟수가 많아질 때 감정가가 떨어지는 것을 반영하였습니다.

우리가 궁금한 것은 낙찰된 경매가격이므로, 낮은 감정가가 필요합니다.

 


8. Auction_seq(경매횟수)의 최댓값 결합

max_seq = result.groupby('Auction_key')['Auction_seq'].max()
need_merge['max_seq'] = max_seq.values
need_merge.head(2)

 

※ 결과 해석

경매횟수의 값이 클수록 유찰 혹은 변경이 많이 일어났다는 의미로 해석할 수 있는데,
이러한 특성을 반영하기 위해 각 Auction_key 별 경매횟수의 최댓값을 사용해서 새 피처를 생성하겠습니다.


9. Auction_results(경매결과)가 유찰인 횟수 결합

아파트 경매에서 유찰 횟수가 높다는 것은 해당 아파트에 대한 경매 참여자들의 관심이 낮거나,
일정가의 기준 가격과의 불일치로 인해 거래가 이루어지지 않았음을 의미합니다.
이를 반영하기 위해 유찰 횟수를 피처로 추가하겠습니다.

 

join() 메서드는 두 개의 데이터프레임을 결합하는 기능을 제공하는 메서드입니다.
이를 통해 두 개의 데이터프레임을 공통 열이나 인덱스를 기준으로 연결하여 더 큰 데이터프레임을 생성할 수 있습니다.

  • on: 결합할 때 기준이 되는 열이나 인덱스를 지정하며, 설정하지 않은 경우 인덱스를 기준으로 결합합니다.
  • how: 결합 방법을 지정합니다. left, right, outer, inner 등의 값이 있습니다.
  • inner: 공통된 키를 기준으로 두 데이터프레임을 내부 조인합니다. 공통된 키가 없는 경우 공통되지 않은 행이 포함되지 않습니다.
  • outer: 공통된 키를 기준으로 두 데이터프레임을 외부 조인합니다. 공통된 키가 없는 경우 공통되지 않은 행도 결과에 포함되며,
    해당 열의 값은 NaN으로 채워집니다.
  • left: 왼쪽 데이터프레임을 기준으로 결합합니다. 왼쪽 데이터프레임의 열은 유지되고, 오른쪽 데이터프레임의 행이 없을 경우 해당 열의 값은 NaN으로 채워집니다.
  • right: 오른쪽 데이터프레임을 기준으로 결합합니다. 오른쪽 데이터프레임의 열은 유지되고, 왼쪽 데이터프레임의 행이 없을 경우 NaN으로 채워집니다.

join() 메서드의 on 매개변수의 기본값은 None인데, 이를 지정하지 않을 경우 인덱스를 기준으로 병합을 수행합니다.
하지만, on 매개변수를 사용함으로써 merge() 메서드와 동일한 동작을 수행할 수 있습니다.

 

생성된 피처명이 Auction_results_right인 이유는 auction_count 변수에 할당된 시리즈(Series)의 이름이 Auction_result이기 때문입니다.

need_merge 데이터프레임에서 Auction_result 피처가 존재하였기 때문에 각 데이터프레임에서 Auction_result 피처에 Isuffix, rsuffix를 할당한 문자열이 추가되었습니다.

 

4번 인덱스의 Auction_results_right 피처의 값이 결측치로 표기되었습니다.
5번 인덱스에 데이터를 보았을 때 유찰횟수가 한 건도 없는 건물(Auction_key)이 존재했었습니다.
유찰이 한 건도 발생하지 않은 경우 수치를 쓸 수 없으므로 결측치가 되었습니다.
이 값은 아래 스텝에서 0으로 치환하겠습니다.

failed_auction = result[result['Auction_results'] == '유찰']
auction_count = failed_auction.groupby('Auction_key')['Auction_results'].count()
auction_result = need_merge.join(auction_count, on = 'Auction_key', how = 'left', lsuffix = '_left', rsuffix = '_right')
auction_result.head(5)

 


10. Auction_results_right 결측치 채우기

auction_result = auction_result.fillna(0)
auction_result['Auction_results_right'].isnull().sum()
0

 


11. 증감률 피처 만들기

# 첫 경매시 최저매각가격
first_price = result.groupby('Auction_key')['Minimum_sales_price'].max().values # 첫
# 마지막 경매시 최저매각가격
last_price = result.groupby('Auction_key')['Minimum_sales_price'].min().values # 최종

change_rate = (last_price - first_price) / first_price * 100  
auction_result['change_rate'] = change_rate  
auction_result.head(2)

 


12. 데이터 결합

train_result = train.merge(auction_result, on = 'Auction_key', how = 'left')

 


13. Auction_results_right(유찰횟수)별 낙찰가 추이 확인

import matplotlib.pyplot as plt

pivot = train_result.pivot_table(index = 'year', columns = 'Auction_results_right', values = 'Hammer_price',  aggfunc = 'sum')
pivot.plot(figsize = (8,6))
plt.show()

※ 결과 해석

연도별로 유찰횟수에 따른 낙찰가의 추이를 확인했을 때, 해가 지날수록 낙찰가가 감소하는 모습을 보이고 있습니다.
따라서 year 피처를 모델에 반영할 경우, 경매가 진행되는 연도에 따라 낙찰가가 감소하는 경향을
모델이 학습하여 예측에 반영할 수 있습니다.
이를 통해 모델은 시간의 흐름에 따른 낙찰가 변동성을 파악하고, 더 정확한 낙찰가 예측을 수행할 수 있을 것입니다.


14. 데이터 처리 방향 정하기

objects = ['Auction_class', 'Bid_class', 'Appraisal_company', 'Final_result', 'Creditor', 'addr_do', 'addr_si', 'addr_dong', 'addr_san', 'addr_etc', 'Apartment_usage', 'Share_auction_YorN', 'road_name', 'Close_result', 'Auction_results_left']

for col in objects:
    print(train_result[col].value_counts()[:3])
    print(len(train_result[col].value_counts()))
    print('--------------------------')
임의    1439
강제     494
Name: Auction_class, dtype: int64
2
--------------------------
일반    1796
개별     125
일괄      12
Name: Bid_class, dtype: int64
3
--------------------------
부경감정    33
자연감정    33
서초감정    31
Name: Appraisal_company, dtype: int64
238
--------------------------
낙찰    1933
Name: Final_result, dtype: int64
1
--------------------------
Private    483
국민은행       119
우리은행        96
Name: Creditor, dtype: int64
448
--------------------------
서울    1242
부산     691
Name: addr_do, dtype: int64
2
--------------------------
노원구    129
강남구    102
사하구     96
Name: addr_si, dtype: int64
39
--------------------------
상계동    52
서초동    32
아현동    32
Name: addr_dong, dtype: int64
285
--------------------------
N    1912
Y      21
Name: addr_san, dtype: int64
2
--------------------------
1층 101호                 3
,-18 청담삼익 1동 9층 905호    2
2층 201호                 2
Name: addr_etc, dtype: int64
1929
--------------------------
아파트     1656
주상복합     277
Name: Apartment_usage, dtype: int64
2
--------------------------
N    1854
Y      79
Name: Share_auction_YorN, dtype: int64
2
--------------------------
마포대로    33
0       24
마들로     15
Name: road_name, dtype: int64
1038
--------------------------
배당      1921
          12
Name: Close_result, dtype: int64
2
--------------------------
배당    1921
낙찰      12
Name: Auction_results_left, dtype: int64
2
--------------------------

 

※ 결과 해석

1) 원 핫 인코딩

  • Bid_class(일반, 개별, 일괄) -> 순서가 없는 범주형 데이터이므로 원 핫 인코딩 사용

2) 라벨 인코딩

  • 공통: 순서가 없는 범주이지만, 라벨 인코딩을 사용하여 0 혹은 1 두 가지의 값으로 인코딩한다면 원 핫 인코딩과 동일한 결과를 얻을 수 있음
  • Auction_class(일반, 강제)
  • addr_do(서울, 부산)
  • addr_san(N, Y)
  • Share_auction_YorN(N, Y)
  • Auction_results_left(배당, 낙찰)
  • Apartment_usage(아파트, 주상복합)

3) 제거

  • Close_result(배당, 빈값 두 개인 값) -> 배당 1921개, 빈값이 17개로 모든 값이 배당에 귀속
  • Final_result(낙찰) -> 단일 값
  • addr_dong(석촌동, 아현동 등 고유값 285개) -> 고 단위까지 확인하는 것으로 믿음성 확보 가능
  • addr_etc(xx 초등 등 데이터 1929개) -> 데이터가 세분화되어 있어서 특성을 추출하기가 어려움
  • road_name(마포로길, 마들로 등 고유값 1038개) -> 분류할 수 있는 기준이 존재하지 않음
  • Creditor(은행 등의 고유 데이터 448개) -> 고유값 개수가 많고 분류 기준이 모호함
  • Appraisal_company(xx감정으로 끝나는 데이터 고유값 238개) -> '감정'으로 끝나는 데이터 동일
  • addr_si(성남시, xx 남구 등 고유값 39개) -> 서울, 부산에서 컴파일 비싼 곳 or 아닌 곳 분리할 필요 없음

15. Appraisal_company(감정사) 피처 확인

check_company = train_result['Appraisal_company'].str[:-2][:50]
print(check_company)
0       정명
1        희
2       혜림
3       신라
4       나라
5      한마음
6     미래새한
7       부일
8       금정
9       연산
10      명장
11      명장
12      문일
13      미르
14      국제
15      드림
16      금정
17      대일
18    미래새한
19     한마음
20      혜림
21      드림
22      문일
23      대한
24       희
25      내외
26      대일
27      대한
28      대한
29      대한
30     오상호
31      대화
32      태화
33      대일
34      국제
35       희
36      하나
37      나라
38      써브
39      부경
40      통일
41      대화
42      나라
43     한마음
44      대화
45      삼보
46     한마음
47      대한
48      한라
49      대화
Name: Appraisal_company, dtype: object

※ 결과 해석

이름은 일반적으로 연속형이나 범주형 변수와 달리 숫자적인 의미를 갖지 않습니다.
그리고 출력된 값을 확인해보았을 때 특별한 의미를 가지는 항목을 찾을 수 없으며, 글자수도 각각 다른 것을 확인할 수 있습니다.
따라서 Appraisal_company 열은 사용이 불가능한 것으로 판단하고, 이를 제거하였습니다.


16. 시군구(addr_si)별 평균 낙찰가 확인

import matplotlib.pyplot as plt

plt.figure(figsize=(4, 8), dpi=100, constrained_layout=True)
addr_index = train_result.groupby('addr_si')['Hammer_price'].mean().sort_values().index
addr_value = train_result.groupby('addr_si')['Hammer_price'].mean().sort_values().values

plt.xlabel('Hammer_price')
plt.ylabel('addr_si')
ax = plt.barh(y=addr_index, width=addr_value)
plt.show()

※ 결과 해석

출력된 결과를 확인해보았을 때 강남구, 서초구, 용산구, 송파구, 관악구, 영등포구 여섯 개 지역은 다른 지역에 비해 낙찰가가 타 지역에 비해 높습니다.
이를 바탕으로, 지가가 높은 지역과 낮은 지역을 구분하는 피처를 생성할 수 있을 것입니다.


17. 시군구(addr_si) 피처 엔지니어링

high_landprice = train_result.groupby('addr_si')['Hammer_price'].mean().sort_values(ascending = False).index[:6]

train_result['low_high'] = train_result['addr_si'].apply(lambda x : 1 if x in high_landprice else 0)
train_result['low_high'].value_counts()
0    1581
1     352
Name: low_high, dtype: int64

 


18. 원 핫 인코딩 사용

from sklearn.preprocessing import OneHotEncoder

ohe = OneHotEncoder(sparse_output=False)
onehot_bid = ohe.fit_transform(train_result[['Bid_class']])
onehot_frame = pd.DataFrame(onehot_bid, columns = ohe.categories_[0])
train_result = pd.concat([train_result, onehot_frame], axis = 1)

train_result = train_result.drop(['Bid_class', '일괄'], axis = 1)
train_result.head(2)

 


19. 두 가지 이상 항목에 대해 라벨 인코딩 사용

from sklearn.preprocessing import LabelEncoder

label_col = ['Auction_class', 'addr_do', 'addr_san', 'Share_auction_YorN', 'Auction_results_left', 'Apartment_usage']

for i in label_col:
    le = LabelEncoder()
    train_result[i] = le.fit_transform(train_result[i])

 


20. 피처 제거

drop_list = ['Close_result', 'Final_result', 'addr_dong', 'addr_etc', 'road_name', 'Appraisal_company', 'Creditor', 'addr_si']

train_result = train_result.drop(drop_list, axis = 1)