본문 바로가기
Minding's Programming/Kaggle and Dacon

[Kaggle] House Prices Prediction : 보스턴 주택가격 예측 - 1. EDA & Feature Engineering

by Minding 2021. 4. 14.
728x90
반응형

[파이썬 라이브러리를 활용한 머신러닝] 책을 공부하던 때 처음 접해보았던

머신러닝 회귀 문제의 대표문제, Kaggle의 House Prices 예측 데이터셋을 다시 한번 살펴보는 시간을 가졌다.

 

 

파이썬 라이브러리를 활용한 머신러닝

데이터셋은 Kaggle에서 다운로드 가능하다.

www.kaggle.com/c/house-prices-advanced-regression-techniques

 

House Prices - Advanced Regression Techniques

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

www.kaggle.com

 

 

처음 공부 할 당시에는 tensorflow를 활용한 선형회귀만을 다루었는데, 이번에는 앙상블 기법을 통해 접근해보았다.

 

앙상블 기법을 활용한 모델링 및 예측 모델은 Kaggle의 munmun2004님의 커널을 참고했다.

www.kaggle.com/munmun2004/house-prices-for-begginers

 

[한글커널][House Prices]보스턴 집값 예측 for Begginers

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

www.kaggle.com

 


1. 데이터 확인

데이터는 주택가격을 설명하는 총 49개의 변수를 가지고 있으며 1460개의 훈련데이터, 1459개의 테스트 데이터가 제공된다.

 

각 변수에 대한 설명은 Dataset에 함께 있는 data_description.txt를 보면 알 수 있다.

주요 변수를 몇 개만 설명하자면,

변수이름 내용
MSSubClass 주거유형
LotFrontage 주택에 연결된 거리의 직선 피트
Neighborhood 미국 물리적 주소위치
OverallQual 집의 전체적인 재료 및 마감재 평가
OveallCond 집의 전반적인 상태평가
ExterQual 외부 소재 품질 평가
BsmtQual 지하실 높이 평가
Heating 난방의 종류
1stFlrSF 1층의 평방 피트
SalePrice 주택 가격

 

1-1. 데이터 불러오기

데이터를 불러오기 전 필요한 라이브러리들을 import하고, pandas를 통해 데이터를 불러온다.

import numpy as np
import pandas as pd
import tensorflow as tf # 선형회귀 때 사용
import matplotlib
from matplotlib import pyplot as plt

import seaborn as sns
sns.set()

%matplotlib inline

# 데이터 불러오기
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

 

1-2. 데이터 확인

데이터를 불러왔다면, 데이터가 어떻게 생겼는지 알아본다.

train.head()

train.shape
>>> (1460, 81)

test.shape
>>> (1459, 80)

train.columns
>>> Index(['Id', 'MSSubClass', 'MSZoning', 'LotFrontage', 'LotArea', 'Street',
       'Alley', 'LotShape', 'LandContour', 'Utilities', 'LotConfig',
       'LandSlope', 'Neighborhood', 'Condition1', 'Condition2', 'BldgType',
       'HouseStyle', 'OverallQual', 'OverallCond', 'YearBuilt', 'YearRemodAdd',
       'RoofStyle', 'RoofMatl', 'Exterior1st', 'Exterior2nd', 'MasVnrType',
       'MasVnrArea', 'ExterQual', 'ExterCond', 'Foundation', 'BsmtQual',
       'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinSF1',
       'BsmtFinType2', 'BsmtFinSF2', 'BsmtUnfSF', 'TotalBsmtSF', 'Heating',
       'HeatingQC', 'CentralAir', 'Electrical', '1stFlrSF', '2ndFlrSF',
       'LowQualFinSF', 'GrLivArea', 'BsmtFullBath', 'BsmtHalfBath', 'FullBath',
       'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'KitchenQual',
       'TotRmsAbvGrd', 'Functional', 'Fireplaces', 'FireplaceQu', 'GarageType',
       'GarageYrBlt', 'GarageFinish', 'GarageCars', 'GarageArea', 'GarageQual',
       'GarageCond', 'PavedDrive', 'WoodDeckSF', 'OpenPorchSF',
       'EnclosedPorch', '3SsnPorch', 'ScreenPorch', 'PoolArea', 'PoolQC',
       'Fence', 'MiscFeature', 'MiscVal', 'MoSold', 'YrSold', 'SaleType',
       'SaleCondition', 'SalePrice'],
      dtype='object')

데이터가 전체적으로 어떻게 이루어져 있는지를 먼저 파악해야, EDA와 Feature handling을 할 때 어떻게 해야 할 지 알 수 있다.

 

 

2. EDA & Feature Engineering

EDA (Exploratory Data Analysis)는 데이터를 다양한 각도에서 관찰하고 이해하는 과정으로,

데이터의 문제점이나 패턴을 발견할 수 있다.

 

문제점과 패턴을 발견하는 것은 머신러닝 모델학습에 큰 영향을 미치므로,

머신러닝 학습에 있어 가장 중요한 부분이라고 할 수 있다.

 

2-1. 이상치 제거 (Outlier remove)

fig, ax = plt.subplots()
ax.scatter(x = train['GrLivArea'], y = train['SalePrice'])
plt.ylabel('SalePrice')
plt.xlabel('GrLivArea')
plt.show()

matplotlib을 통해 데이터를 시각화하였다.

데이터의 범위에서 벗어난 것들이 보이는데, 이를 제거해 주어야 한다.

이상치를 제거해줄 때는 학습대상인 train data에만 적용해 주어야 한다!

 

train = train[train['GrLivArea']<4000]
train = train[train['GarageArea']<1200]
train = train[train['1stFlrSF']<2700]
train = train[train['2ndFlrSF']<1700]
train = train[train['TotalBsmtSF']<3000]

이상치의 기준은 사람마다 다를 수 있다.

너무 많이 제거해도 좋지않고, 너무 제거하지 않아도 좋지 않으므로 적절하게 지워주는 것이 좋다.

 

2-2. 상관관계 분석 (히트맵)

target값인 SalePrice와 각 특징들 간 상관계수를 뽑아내어 히트맵을 그린다.

이를 통해 각 특징들이 주택가격과 얼마나 강한 관련이 있는지 알아낼 수 있다.

또한, SalePrice가 아닌 각 변수들 간의 상관관계가 높으면, 다중공산성 문제가 발생할 수 있기에 유심히 봐준다.

# 상관계수
corrmat = train.corr()
corr_columns = corrmat.index[abs(corrmat["SalePrice"])>=0.4] # 상관계수 0.4 이상만 포함
corr_columns

>>> Index(['OverallQual', 'YearBuilt', 'YearRemodAdd', 'MasVnrArea', 'TotalBsmtSF',
       '1stFlrSF', 'GrLivArea', 'FullBath', 'TotRmsAbvGrd', 'Fireplaces',
       'GarageYrBlt', 'GarageCars', 'GarageArea', 'SalePrice'],
      dtype='object')
      
# 히트맵
plt.figure(figsize=(13,10))
heatmap = sns.heatmap(train[corr_columns].corr(),annot=True,cmap="RdYlGn")

 

2-3. 데이터 합쳐주기 (concat)

이상치 제거를 제외한 분포 정규화나 결측치 처리는 한번에 해주어야 하기 때문에

train data와 test data를 concat을 통해 합쳐준다.

 

train data에는 test data에 없는 SalePrice 칼럼이 있기 때문에 이를 지워주고 concat 시켜준다.

df_train = train.drop(['SalePrice'])
df = pd.concat((df_train,test))

 

2-4. target의 쏠림 현상 파악

target값인 SalePrice의 데이터 분포를 살펴보자

sns.distplot(train['SalePrice'])

한 쪽으로 쏠린모양이 보인다. 이런 경우 학습에 영향을 줄 수 있기 때문에

로그 변환을 통해 정규성을 띄게 바꾸어준다.

train['SalePrice'] = np.log1p(train["SalePrice"])
sns.distplot(train['SalePrice'])

또한, 이렇게 변환한 값을 price라는 변수에 저장해준다.

price = train['SalePrice']

 

2-5. 결측치(null) 확인 및 처리

결측치는 함수적용도 안될 뿐더러 분석결과를 왜곡시키는 값이므로 새로운 값으로 대체해주어야 한다.

null = (df.isna().sum() / len(df) * 100) #백분율로 계산

null = null.drop(null[null == 0].index).sort_values(ascending=False)
null

>>
PoolQC          99.724613
MiscFeature     96.419966
Alley           93.184165
Fence           80.378657
FireplaceQu     48.846816
LotFrontage     16.626506
GarageFinish     5.473322
GarageYrBlt      5.473322
GarageQual       5.473322
GarageCond       5.473322
GarageType       5.404475
BsmtExposure     2.822719
BsmtCond         2.822719
BsmtQual         2.788296
BsmtFinType1     2.719449
BsmtFinType2     2.719449
MasVnrType       0.826162
MasVnrArea       0.791738
MSZoning         0.137694
BsmtFullBath     0.068847
BsmtHalfBath     0.068847
Utilities        0.068847
Functional       0.068847
Exterior2nd      0.034423
Exterior1st      0.034423
SaleType         0.034423
BsmtFinSF1       0.034423
BsmtFinSF2       0.034423
BsmtUnfSF        0.034423
Electrical       0.034423
KitchenQual      0.034423
GarageCars       0.034423
GarageArea       0.034423
TotalBsmtSF      0.034423
dtype: float64

총 34개의 컬럼에 결측치가 발견되었다.

 

결측치 처리 또한 데이터를 다루는 사람의 마음대로이다.

결측치 전체를 0으로 만들어 줄 수 있으나, 세세하게 다루어주는 것이 학습에는 더 좋다.

 

- 수치형데이터의 경우에는 0, 범주형데이터의 경우에는 'None'으로 대체했다.

- 유형을 말해주는 특징 (ex. 지붕 재료, 사용가능한 자원의 종류 등)의 경우에는 제일 빈번한 값으로 대체했다.

 

개수가 많으므로 시각화는 생략했다.

# PoolQC : 수영장 품질, nan = 존재X (99%)
df['PoolQC'] = df['PoolQC'].fillna('None')

# MiscFeature : 기타기능, nan = 존재X (96%)
df['MiscFeature'] = df['MiscFeature'].fillna('None')

# Alley : 골목 접근 유형, nan = 골목 접근 금지
df['Alley'] = df['Alley'].fillna('None')

# Fence : 울타리 여부, nan = 울타리 없음
df['Fence'] = df['Fence'].fillna('None')

# FireplaceQu : 벽난로 품질, nan = 벽난로 없음
df['FireplaceQu'] = df['FireplaceQu'].fillna('None')

# LotFrontage : 부동산과 연결된 거리의 직선 피트, nan = 연결된 거리 없음
df['LotFrontage'] = df['LotFrontage'].fillna(0)

# GarageFinish : 차고 마감재 품질, nan = 차고 없음
df['GarageFinish'] = df['GarageFinish'].fillna('None')

# GarageYrBlt : 차고 제작연도, nan = 차고 없음
df['GarageYrBlt'] = df['GarageYrBlt'].fillna(0)

# GarageQual : 차고 품질, nan = 차고 없음
df['GarageQual'] = df['GarageQual'].fillna('None')

# GarageCond : 차고 상태, nan = 차고 없음
df['GarageCond'] = df['GarageCond'].fillna('None')

# GarageType : 차고 유형, nan = 차고 없음
df['GarageType'] = df['GarageType'].fillna('None')

# 지하실 관련 카테고리형 데이터, nan = 지하실 없음
# BsmtExposure, BsmtCond, BsmtQual, BsmtFinType1, BsmtFinType2
for data in ['BsmtExposure', 'BsmtCond', 'BsmtQual', 'BsmtFinType1', 'BsmtFinType2']:
    df[data] = df[data].fillna('None')
    
# 지하실 관련 수치형 데이터, nan = 지하실 없음
# BsmtFullBath, BsmtHalfBath, BsmtFinSF1, BsmtFinSF2, BsmtUnfSF, TotalBsmtSF
for data in ['BsmtFullBath', 'BsmtHalfBath', 'BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF', 'TotalBsmtSF']:
    df[data] = df[data].fillna(0)
    
# MasVnrType : 석조베니어 형태, nan = 베니어 없음
df['MasVnrType'] = df['MasVnrType'].fillna('None')

# MasVnrArea : 석조베니어 공간, nan = 베니어 없음
df['MasVnrArea'] = df['MasVnrArea'].fillna(0)

# MSZoning : RL이 제일 흔한 값이므로 결측치 RL로 변경
df['MSZoning'] = df['MSZoning'].fillna('RL')

# Utilities : AllPub이 가장 흔한 값이므로 결측치 AllPub으로 변경
df['Utilities'] = df['Utilities'].fillna('AllPub')

# Functional : 홈 기능, 가장 일반적인 Typ로 변경
df["Functional"] = df["Functional"].fillna("Typ")

# Exterior2nd :집 외부 덮개 (소재가 2개 이상인 경우), nan = 소재 1개만 사용
df['Exterior2nd'] = df['Exterior2nd'].fillna('None')
df['Exterior1st'] = df['Exterior1st'].fillna('VinylSd')

# Electrical : 전기시스템, 'SBrkr'이 제일 흔한 값이므로 변경
df['Electrical'] = df['Electrical'].fillna('SBrkr')

# KitchenQual : 주방 품질, 'TA'가 가장 흔한 값이므로 변경
df['KitchenQual'] = df['KitchenQual'].fillna('TA')

# GarageCars, GarageArea : 차고의 차 개수와 차고넓이, nan = 차고없음
df['GarageCars'] = df['GarageCars'].fillna(0)
df['GarageArea'] = df['GarageArea'].fillna(0)

# SaleType : 판매 유형, 가장 흔한 값인 'WD'로 변경
df['SaleType'] = df['SaleType'].fillna('WD')

34개 칼럼의 모든 결측치를 제거했다.

 

2-6. 수치형데이터와 범주형데이터 분리하여 처리

기본적으로 컴퓨터는 숫자만 인식할 수 있기때문에, 범주형의 수치화가 필요하다.

또한, 수치형데이터도 한쪽으로 쏠려있는 경우가 있을 수 있기 때문에, 조정이 필요하다.

이를 위해 수치형데이터와 범주형데이터를 나누어 따로 처리해준다.

 

그에 앞서 수치형 특징이 아닌데 수치형으로 표시되어 있거나, 범주형 특징이 아닌데 범주형데이터로 표시되어 있는 칼럼들을

astype()을 통해 형 변환 시켜준다.

# 판매월과 판매연도가 수치형으로 되어있어 카테고리형(str)로 타입 변경
df['YrSold'] = df['YrSold'].astype(str)
df['MoSold'] = df['MoSold'].astype(str)

# 주거유형이 수치형으로 되어있어 카테고리형으로 타입변경
df['MSSubClass'] = df['MSSubClass'].astype(str)
# 수치형데이터와 범주형데이터 분리
obj_df = df.select_dtypes(include='object')
num_df = df.select_dtypes(exclude='object')

 

- 범주형 데이터 처리 (라벨인코딩)

범주형데이터를 수치화시켜주는 인코딩 기법에는 크게 두 가지가 있다.

1. LabelEncoding

2. One-hot Encoding

 

라벨인코딩의 경우, 해당 특징을 나타내는 데이터에 등급 또는 순서가 없을 경우에는 모델에 잘못 반영될 수 있다.

그렇기 때문에 라벨인코딩에는 등급, 시설의 유무 여부를 나타내는 칼럼에만 적용시켜주었다.

 

# 등급이 나누어지거나, 순서가 없는 경우 모델에 잘못 반영될 수 있기 때문에 등급, 여부 칼럼만 포함

label_obj_list = ['Street', 'Alley','ExterQual', 'ExterCond','BsmtCond','HeatingQC', 'CentralAir',
       'KitchenQual', 'FireplaceQu','GarageFinish', 'GarageQual', 'GarageCond', 'PavedDrive', 'PoolQC',
       'Fence', 'MoSold', 'YrSold','SaleCondition']
       
# 카테고리형 칼럼을 라벨인코딩 (수치화, 문자를 0부터 시작하는 정수형 숫자로 바꾸어줌)
from sklearn.preprocessing import LabelEncoder

# encoder = LabelEncoder()

for obj in label_obj_list:
    encoder = LabelEncoder()
    encoder.fit(list(df[obj].values))
    df[obj] = encoder.transform(list(df[obj].values))

 

- 수치형 데이터 처리 (왜도)

위에서 처리해주었던 SalePrice와 같이 한쪽으로 쏠림이 있는 경우 왜도(비대칭도)값이 높게 나타난다.

이를 통해 한쪽으로 쏠려있는 데이터들을 로그변환을 취하여 준다.

왜도(skewness)값이 크면 클수록 데이터가 한 쪽에 많이 쏠려 있으며,

왼 쪽에 많이 쏠려있을 경우 양수값을, 오른쪽으로 많이 쏠려있으면 음수값을 가진다.

 

num_features = df.dtypes[df.dtypes != "object"].index

from scipy.stats import skew 
skewness = df[num_features].apply(lambda x: skew(x.dropna())).sort_values(ascending=False)

high_skewness = skewness[abs(skewness) > 1] # 왜도 값이 1 이상인 칼럼만 채택
skew_feats = high_skewness.index
print(high_skewness)

>>>
MiscVal          21.904223
PoolArea         18.669449
LotArea          13.245250
LowQualFinSF     12.059175
3SsnPorch        11.348142
KitchenAbvGr      4.290310
BsmtFinSF2        4.166631
EnclosedPorch     4.017907
ScreenPorch       3.946659
BsmtHalfBath      3.934869
MasVnrArea        2.551011
OpenPorchSF       2.528255
WoodDeckSF        1.856938
1stFlrSF          1.212917
KitchenQual      -1.454760
ExterQual        -1.802948
Fence            -1.988541
ExterCond        -2.499174
SaleCondition    -2.797852
BsmtCond         -2.853376
PavedDrive       -2.969670
GarageQual       -3.064592
CentralAir       -3.448736
GarageCond       -3.585278
GarageYrBlt      -3.895128
Street          -16.158425
PoolQC          -22.944518
dtype: float64
# 로그 변환
df[skew_feats] = np.log1p(df[skew_feats])

 

2-7. 파생 변수

위의 칼럼들의 내용과 특징들을 이용하여 주택가격에 대한 설명을 보충해줄 수 있는 변수들을 추가로 생성해준다.

df['TotalFlrSF'] = (df['1stFlrSF'] + df['2ndFlrSF'] + df['TotalBsmtSF']) # 부동산의 총 제곱피트

df['TotalBath'] = (df['FullBath'] + df['HalfBath'] + df['BsmtFullBath'] + df['BsmtHalfBath']) # 화장실

df['RemodorNot'] = np.where(df['YearBuilt'] == df['YearRemodAdd'], 0, 1) # 리모델링 여부

df['PoolorNot'] = np.where(df['PoolArea'] > 0, 1, 0) # 수영장 여부

df['GarageorNot'] = np.where(df['GarageArea'] > 0, 1, 0) # 차고 여부

df['BsmtorNot'] = np.where(df['TotalBsmtSF'] > 0, 1, 0) # 지하실 여부

df['FireplaceorNot'] = np.where(df['Fireplaces'] > 0, 1, 0) # 벽난로 여부

 

2-8. One-hot Encoding

위에서 인코딩 처리를 해주지 않은 칼럼들에 대해 변수 더미처리를 해준다.

# 원 - 핫 인코딩 (라벨인코딩 하지 않은 칼럼들)
df = pd.get_dummies(df)

df.shape
>>> (2905, 271)

원핫인코딩을 통해 칼럼개수가 늘어난 것을 볼 수 있다.

728x90
반응형

댓글