본문 바로가기
AI 빅데이터/후려치는 데이터분석과 AI 알고리즘

[데이터분석] 주택가격 예측 Kaggle 도전기

by 마고커 2020. 9. 25.


오랜만 아니 거의 처음으로 진지하게 캐글 문제를 풀어보기로.. 쉬어보이는 주택 가격 예측을 선택하였다.

 

 

House Prices: Advanced Regression Techniques

Predict sales prices and practice feature engineering, RFs, and gradient boosting

www.kaggle.com

집에 관한 여러 약 80가지의 항목이 있고, 그 내용들을 학습하여 test set에 있는 주택 가격을 예측하는 것이다. 

 

1) 간단한 전처리만으로 도전

 

데이터를 살펴보면, 대체로 항목별로 구분되지만 일부는 continuous 형태의 데이터로 되어 있다. 회귀 분류에는 이런 특성이 도움되지 않을 것 같아 항목별로 데이터 종류가 10~20개 정도로 줄어들 수 있도록 반올림처리한 정도만 우선 진행했다.

 

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
import seaborn as sns

df = pd.read_csv("train.csv")

df['LotArea_Adjusted'] = pd.DataFrame(round(df['LotArea']/5000)*5000)
df['LotFrontage_Adjusted'] = pd.DataFrame(round(df['LotFrontage']/10)*10)
df['YearBuild_Adjusted'] = pd.DataFrame(round(df['YearBuilt']/10)*10)
df['YearRemdAdd_Adjusted'] = pd.DataFrame(round(df['YearRemodAdd']/10)*10)
df['GarageYrBlt_Adjusted'] = pd.DataFrame(round(df['GarageYrBlt']/10)*10)
df['YrSold_Adjusted'] = pd.DataFrame(round(df['YrSold']/10)*10)
df['MasVnrArea_Adjusted'] = pd.DataFrame(round(df['MasVnrArea']/100)*100)
df['BsmtFinSF1_Adjusted'] = pd.DataFrame(round(df['BsmtFinSF1']/100)*100)
df['BsmtUnfSF_Adjusted'] = pd.DataFrame(round(df['BsmtUnfSF']/100)*100)
df['TotalBsmtSF_Adjusted'] = pd.DataFrame(round(df['TotalBsmtSF']/100)*100)
df['1stFlrSF_Adjusted'] = pd.DataFrame(round(df['1stFlrSF']/100)*100)
df['2ndFlrSF_Adjusted'] = pd.DataFrame(round(df['2ndFlrSF']/100)*100)
df['GrLivArea_Adjusted'] = pd.DataFrame(round(df['GrLivArea']/100)*100)
df['GarageArea_Adjusted'] = pd.DataFrame(round(df['GarageArea']/100)*100)
df['WoodDeckSF_Adjusted'] = pd.DataFrame(round(df['WoodDeckSF']/100)*100)
df['OpenPorchSF_Adjusted'] = pd.DataFrame(round(df['OpenPorchSF']/20)*20)
df['EnclosedPorch_Adjusted'] = pd.DataFrame(round(df['EnclosedPorch']/20)*20)

# 조정된 column들은 삭제
columns = ['LotArea', 'LotFrontage', 'YearBuilt', 'YearRemodAdd', 'GarageYrBlt', 'YrSold','MasVnrArea', \
           'BsmtFinSF1', 'BsmtUnfSF', 'TotalBsmtSF', '1stFlrSF', '2ndFlrSF', 'GrLivArea', 'GarageArea', 'WoodDeckSF', 'OpenPorchSF', 'EnclosedPorch' ]
df_adjusted = df.drop(columns=columns, axis=1)

# Y값은 SalePrice
df_train_y = df_adjusted.loc[:, 'SalePrice']
df_train_x = df_adjusted.drop('SalePrice', axis=1)

# XGBoost는 수치형 데이터만 입력 받을 수 있음
df_train_x_cat = df_train_x.copy()
cat_col = ['MSZoning', 'Street', 'Alley', 'LotShape', 'LandContour', 'Utilities', 'LotConfig', 'LandSlope', 'Neighborhood', \
           'Condition1', 'Condition2', 'BldgType', 'HouseStyle', \
           'RoofStyle', 'RoofMatl', 'Exterior1st', 'Exterior2nd', 'MasVnrType', 'ExterQual', 'ExterCond', 'Foundation', \
           'BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2', 'Heating', 'HeatingQC', 'CentralAir', \
           'Electrical', 'KitchenQual', 'Functional', 'FireplaceQu', 'GarageType', 'GarageFinish', 'GarageQual', \
           'GarageCond', 'PavedDrive', 'PoolQC', 'Fence', 'MiscFeature', 'SaleType', 'SaleCondition']

# 항목형 데이터를 수치형으로 변환
for col in cat_col:
    df_train_x_cat[col] = df_train_x_cat[col].astype('category').cat.codes           

from sklearn.model_selection import train_test_split
train_x, test_x, train_y, test_y = train_test_split(df_train_x_cat, df_train_y, test_size=0.2,shuffle=True, random_state=1004)

# XGBoost 학습
import xgboost as xgb
from xgboost import plot_importance, plot_tree
from xgboost import XGBClassifier

xgb_model = xgb.XGBRegressor(n_estimators=1000)
xgb_model.fit(train_x, train_y, eval_set=[(test_x, test_y)], early_stopping_rounds=50,verbose=False)

// 테스트 데이터 변환
df_test = pd.read_csv("test.csv")

df_test['LotArea_Adjusted'] = pd.DataFrame(round(df_test['LotArea']/5000)*5000)
df_test['LotFrontage_Adjusted'] = pd.DataFrame(round(df_test['LotFrontage']/10)*10)
df_test['YearBuild_Adjusted'] = pd.DataFrame(round(df_test['YearBuilt']/10)*10)
df_test['YearRemdAdd_Adjusted'] = pd.DataFrame(round(df_test['YearRemodAdd']/10)*10)
df_test['GarageYrBlt_Adjusted'] = pd.DataFrame(round(df_test['GarageYrBlt']/10)*10)
df_test['YrSold_Adjusted'] = pd.DataFrame(round(df_test['YrSold']/10)*10)
df_test['MasVnrArea_Adjusted'] = pd.DataFrame(round(df_test['MasVnrArea']/100)*100)
df_test['BsmtFinSF1_Adjusted'] = pd.DataFrame(round(df_test['BsmtFinSF1']/100)*100)
df_test['BsmtUnfSF_Adjusted'] = pd.DataFrame(round(df_test['BsmtUnfSF']/100)*100)
df_test['TotalBsmtSF_Adjusted'] = pd.DataFrame(round(df_test['TotalBsmtSF']/100)*100)
df_test['1stFlrSF_Adjusted'] = pd.DataFrame(round(df_test['1stFlrSF']/100)*100)
df_test['2ndFlrSF_Adjusted'] = pd.DataFrame(round(df_test['2ndFlrSF']/100)*100)
df_test['GrLivArea_Adjusted'] = pd.DataFrame(round(df_test['GrLivArea']/100)*100)
df_test['GarageArea_Adjusted'] = pd.DataFrame(round(df_test['GarageArea']/100)*100)
df_test['WoodDeckSF_Adjusted'] = pd.DataFrame(round(df_test['WoodDeckSF']/100)*100)
df_test['OpenPorchSF_Adjusted'] = pd.DataFrame(round(df_test['OpenPorchSF']/20)*20)
df_test['EnclosedPorch_Adjusted'] = pd.DataFrame(round(df_test['EnclosedPorch']/20)*20)

# 항목형 데이터를 수치형으로 변환
df_test_adjusted = df_test.drop(columns=columns, axis=1)
for col in cat_col:
    df_test_adjusted[col] = df_test_adjusted[col].astype('category').cat.codes
    
# train 항목과 column의 위치가 같아야 함
df_test_adjusted.loc[:, train_x.columns]

# 예측하고 submission
xgb_predict = pd.DataFrame(xgb_model.predict(df_test_adjusted), columns=["SalePrice"])
pd_submission = pd.concat([df_test_adjusted['Id'], xgb_predict], axis=1, ignore_index=False)
pd_submission.reset_index(drop='True').to_csv("submission.csv", index=False)

 

XGBoost를 사용했고 한 것이라곤 일부 데이터를 반올림하고, 카테고리형 데이터를 수치형으로 바꾼 것 밖에 없다. 결과는..

 

 

4,700 팀중에 3,689등.. 약 78% 안에 들었다. 별로 마음에 들지 않아서 다른 팀의 노트북을 몇 개 찾아서 적용해 보기로..

 

2) 결측치 제거와 정규화

 

주로 아래의 노트북을 참고하였다. 거의 종합적인 테크닉들이 들어가 있지만, 결측치 제거와 데이터 정규화, 일부 아웃라이어 제거 정도만 진행했다.

 

 

Comprehensive data exploration with Python

Explore and run machine learning code with Kaggle Notebooks | Using data from House Prices: Advanced Regression Techniques

www.kaggle.com

우선 주요한 데이터를 correlation matrix로 알아본다.

 

k=10
cols = corrmat.nlargest(k, 'SalePrice')['SalePrice'].index
cm = np.corrcoef(df[cols].values.T)
sns.set(font_scale=1.25)
hm = sns.heatmap(cm, cbar=True, annot=True, square=True, fmt='.2f', annot_kws={'size': 10}, yticklabels=cols.values, xticklabels=cols.values)
plt.show()

 

차고 크기와 차고의 수용 능력은 상식적으로 강하게 연관되어 있고, 데이터도 그렇게 표현(0.88)되어 있다. 1층의 크기와 전체 집의 크기도 마찬가지(0.82). 제일 상관도가 높은 항목은 전체적인 질(OverallQual)인 것만 확인한다. 일단 결측치가 많은 칼럼들을 모두 제거한다.

 

total = df.isnull().sum().sort_values(ascending=False)
percent = (df.isnull().sum()/df.isnull().count()).sort_values(ascending=False)
missing_data = pd.concat([total, percent], axis=1, keys=['Total', 'Percent'])

# 결측치가 하나보다 많은 해당 칼럼을 날리고..
df = df.drop(missing_data[missing_data['Total'] > 1].index, 1)
df = df.drop(df[df['Electrical'].isnull()].index)

 

상관도 높은 일부 데이터와 타겟인 SalePrice의 상관도를 보면 아래와 같이 일부 튀는 데이터(GrLivArea가 4,500을 넘는 두 데이터)가 있다. 성능에 영향을 줄 수 있으므로 제거.

 

var = 'GrLivArea'
data = pd.concat([df['SalePrice'], df[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));

df = df.drop(df[df['GrLivArea'] > 4500].index)

 

 

마지막으로, 아래와 같이 정규성을 갖지 못한 데이터를 정규성을 갖도록 수정해 준다.

 

from scipy.stats import norm
from scipy import stats 

sns.distplot(df['SalePrice'], fit=norm)
fig = plt.figure()
stats.probplot(df['SalePrice'], plot=plt)

 

 

# 데이터에 로그값을 취해 주어 정규성 확보
df['SalePrice'] = np.log(df['SalePrice'])

sns.distplot(df['SalePrice'], fit=norm)
fig = plt.figure()
stats.probplot(df['SalePrice'], plot=plt)

 

 

비슷한 성격의 GrLivArea 칼럼도 동일하게 적용해 준다. 이런 내용을 적용해 주었더니, 3,200 등 정도로 순위가 올랐다. 상위 68%(!)로 올랐지만, 여전히 선두권과는 격차가 크다. 

 

3) 결측치 채워주기

 

결측이 있다고 무작정 해당 칼럼을 없애기보다는 일부 결측치를 채워서 학습하는 것이 모델 성능 개선에 도움된다는 글이 있었다. 다행히 scikitlearn에는 다른 데이터들을 참조하여 결측치를 예측해 주는 라이브러리가 있어 적용해 보았다.

 

# 결측을 채워주는 라이브러리 임포트
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer

# 결측 채워주는 라이브러리는 수치형만 적용 가능하므로 변환
# 기존의 astype('category').cat.codes를 그대로 쓰지 않는 이유는
# 해당 함수는 결측치를 -1 이나 0으로 채우기 때문. 결측은 일단 결측으로 남겨두고 변환
non_numerical_features = ['MSZoning', 'Street', 'Alley', 'LotShape', 'LandContour', 'Utilities',
                          'LotConfig', 'LandSlope', 'Neighborhood', 'Condition1', 'Condition2', 'BldgType',
                          'HouseStyle', 'RoofStyle', 'RoofMatl', 'Exterior1st', 'Exterior2nd',
                          'MasVnrType', 'ExterQual', 'ExterCond', 'Foundation', 'BsmtQual', 'BsmtCond',
                          'BsmtExposure', 'BsmtFinType1', 'Heating', 'BsmtFinType2', 'HeatingQC',
                          'CentralAir', 'Electrical', 'KitchenQual', 'Functional', 'FireplaceQu',
                          'GarageType', 'GarageFinish', 'GarageQual', 'GarageCond', 'PavedDrive',
                          'PoolQC', 'Fence', 'SaleType', 'SaleCondition', 'MiscFeature']

# 변환
df.loc[:, 'MSSubClass':'SaleCondition'] = IterativeImputer().fit_transform(df.loc[:, 'MSSubClass':'SaleCondition'])

 

거의 Null값이었던 Alley column이 채워져 있다.

 

 

테스트 데이터에도 동일하고 적용하고 결측 칼럼을 제거하지 않고 학습한 뒤 예측해 본다.

 

 

아직 선두권과는 많은 차이가 있지만, 2,982 등, 상위 63% 수준까지 올랐다. 선두권 팀들의 노트북을 봐도 그다지 크게 다른 부분은 못 찾았는데, 아무튼 일부 테크닉만 적용하는 것으로 성능이 개선될 수 있음을 확인하는 것으로 이번 도전은 만족하기로..



댓글