본문 바로가기
ML | DL | Big data/Projects

kaggle 주택 가격 예측(1) - 포괄적인 데이터 탐색 분석 / EDA

by 썽하 2020. 8. 10.

 

xgboost를 활용한 실전 실습을 무엇으로 해볼까 kaggle을 구경하다가 많은 사람들의 튜토리얼 compete으로 이용되고 있는 주택 가격 예측으로 진행하기로 결정했다.

 

 

House Prices: Advanced Regression Techniques

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

www.kaggle.com

 

우선 머신러닝이나 딥러닝을 시작하기 전에는 학습과 예측할 데이터 분석부터 시작해야 한다.

처음부터 모든 데이터 분석을 내가 하면 좋겠지만, 정석으로 불려도 좋을 만큼 좋은 예시가 kaggle에 있기에 몇몇 노트북을 따라 하는 것으로 대체한다.

 

이번 글에서는 번역한 수준으로 해당 노트북을 따라 해 볼 예정이다.

 

눈으로 보고 어떤 흐름으로 데이터를 가공하고 정제했는지 봐도 된다. 하지만 따라 해 봄으로써 더 익숙해지고자 한다.

 

사전 준비

kaggle 주택 가격 예측 문제 이해와 data set(여기)

kaggle 이미지(설치하기) 혹은 본인의 ML 환경

 


 

 

COMPREHENSIVE DATA EXPLORATION WITH PYTHON

파이썬을 이용한 포괄적인 데이터 탐색

Pedro Marcelino - February 2017
번역 ) 밥먹는 개발자 - August 2020

Other Kernels: Data analysis and feature extraction with Python


 

데이터를 아는 것이 데이터 과학에서 가장 어려운 일이라고는 말할 수 없지만, 시간이 많이 걸린다.
그래서 대부분 이 중요한 초기 단계를 건너뛰고 물에 뛰어들곤 한다.

하지만 나는 물에 뛰어들기 전에 수영을 배워보고자 했다. Hair et al. (2013)의 'Examining your data' 챕터를 바탕으로 종합적이지만 완전하진 않은 분석을 따르도록 최선을 다했다. 나는 이 노트북에서 엄격한 연구를 보고하는 것과는 거리가 멀지만, 이게 커뮤니티에서 유용할 수 있기를 바라며, 데이터 분석 원칙의 일부를 어떻게 적용했는지 공유한다.

이 노트북에서 진행된 데이터 분석은 다음 챕터 순으로 진행될 예정이다.(챕터에 지어준 이름은 조금 이상하긴 하다.)

  1. Understand the problem. 우리는 각각의 변수를 살펴보고 이 문제에 대한 그것들의 의미와 중요성에 대해 철학적인(?) 분석을 할것이다.
  2. Univariable study. 종속변수(SalePrice)에만 초점을 맞추고. 조금 더 알아보고자 한다.
  3. Multivariate study. 종속변수와 독립변수가 어떤 관련이 있는지 이해하도록 노력하고자 한다.
  4. Basic cleaning. 데이터셋을 정리하고, 누락(missing) 데이터, 특이(outlier) 데이터, 범주형(categorical) 변수를 처리할 것이다
  5. Test assumptions. 우리의 데이터가 대부분의 mulivariate technique에서 요구되는 가정이 충족되는지 확인할 것이다.

이제 시작해보자!

In [1]:
#모듈들을 임포트한다.
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from scipy.stats import norm
from sklearn.preprocessing import StandardScaler
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
In [2]:
#데이터를 읽어온다.
df_train = pd.read_csv('../data/train.csv')
In [3]:
#컬럼들을 확인해보자.
df_train.columns
Out[3]:
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')
 

1. 그래서.. 이제 뭘 하지??¶

데이터를 이해하기 위해서는, 우리는 각각의 변수에 대해 그것들의 의미를 이해하고 문제와 어떤 관련이 있는지 살펴볼 필요가 있다. 시간이 많이 걸리겠지만, 이 과정을 통해서 데이터가 내포하는 의미를 제대로 느낄 수 있다.

분석에서 약간의 규율을 갖추기 위해 다음 컬럼을 가진 엑셀 파일을 생성해보자.

  • Variable - 변수이름.
  • Type - 데이터 유형에 대한 식별자. 'numerical', 'categorical' 두가지가 가능하다. 'numerical' 은 변수가 숫자 형태라는 의미이고, 'categorical'은 카테고리 형태의 값라는 뜻이다.
  • Segment - 데이터 세그먼트에 대한 식별자. 'building', 'space', 'location' 세가지 값이 있다. 'building'은 건물의 물리적 특성과 관련된 변수(예: 'OverallQuality')를 의미한다. 'space'는 집의 공간 특성을 뜻하는 변수(예: 'TotalBSMTSF')를 의미한다. 마지막으로, 'location'은 집이 있는 장소(예: '이웃집')에 대한 정보를 주는 변수를 의미한다.
  • Expectation - 'SalePrice'의 변수 영향에 대한 우리의 기대. 'High', 'Medium', 'Low'로 범주형 척도를 사용할 수 있다.
  • Conclusion - 데이터를 간단히 살펴본 후, 변수의 중요성에 대한 결론. 'Expectation'과 같은 범주적 척도로 유지할수 있다.
  • Comments - 일반적인 코멘트.

'Type'과 'Segment'는 단지 미래의 참고 자료일 뿐이지만, 'Expectation' 컬럼은 '개발자의 육감'에 도움이 될 것이기 때문에 중요하다. 이 컬럼들을 채우기 위해서는 모든 변수에 대한 설명을 읽고, 스스로 다음 질문에 대답해 보아야 한다.

  • 집을 살 때 이 변수를 생각하는가? (예: 우리가 꿈의 집을 생각할 때, 우리는 'Masonry veneer type'을 신경 쓰는가?)
  • 그렇다면 이 변수는 얼마나 중요할까?(예: 'Poor' 대신 'Excellent' 소재를 외관에 적용하면 어떤 영향이 있을까?)
  • 이 정보가 다른 변수에 이미 설명되어 있는가? (예: 'LandContourer'가 재산의 평탄성을 제공한다면, 'LandSlope'를 꼭 알아야 하는가?)

이 벅찬 연습이 끝나면, 'High' 'Expectation'으로 칼럼을 필터링하고 변수를 주의 깊게 살펴볼 필요가 있다. 그런 다음, 기대치를 수정하는 'Conclusion' 칼럼을 채우면서, 그 변수들과 'SalePrice' 사이의 몇 가지 scatter plot을 그려보자.

나는 이 과정을 통해 다음의 변수가 주택 가격 예측에 중요한 역할을 할 것이라는 결론을 내렸다.

  • OverallQual (이것은 내가 그것이 어떻게 계산되었는지 모르기 때문에 내가 좋아하지 않는 변수다; 가능한 다른 모든 변수를 사용하여 'OverallQuality'를 예측 해볼수도 있겠다.)
  • YearBuilt.
  • TotalBsmtSF.
  • GrLivArea.

두 개의 '건물' 변수('OverallQual' 및 'YearBuilt')와 두 개의 '공간' 변수('TotalBsmt')로 끝냈다. SF'와 'GrlivArea'). '위치, 위치, 위치'뿐이라는 부동산의 법칙에 어긋나기 때문에 약간 의외일 수도 있다. 이러한 빠른 데이터 검사 과정이 범주형 변수에 다소 가혹했을 가능성이 있다. 예를 들어, 나는 '이웃집' 변수가 더 관련이 있을 것으로 기대했지만, 데이터 검사 후에 나는 결국 배제하고 말았다. 아마도 이것은 범주형 변수 시각화에 더 적합한 상자 그림 대신 scatter plot을 사용하는 것과 관련이 있을 것이다. 우리가 데이터를 시각화하는 방법은 종종 우리의 결론에 영향을 미친다.

어쨌든 이번 exercise의 요지는 우리가 데이터의 기대 작용을 조금 생각해보는 것이었으니 목표는 달성했다고 생각한다. 이제 '대화는 조금 줄이고 행동은 조금 더'할 시간이 됐다. shake it!

 

2. 가장 먼저 해야 할 것 : 'SalePrice' 분석

'SalePrice'는 우리가 예측해야 할 결과이다. SalePrice의 대략적인 통계를 살펴보자.

In [4]:
#descriptive statistics summary
df_train['SalePrice'].describe()
Out[4]:
count      1460.000000
mean     180921.195890
std       79442.502883
min       34900.000000
25%      129975.000000
50%      163000.000000
75%      214000.000000
max      755000.000000
Name: SalePrice, dtype: float64
 

'음 좋다... 최저 가격이 0보다 크다.. 내 모델을 망가뜨릴 특성은 없다! 이제 시각화 자료를 살펴보자.'

In [5]:
#histogram
sns.distplot(df_train['SalePrice']);
 
 

꽤나 괜찮은 데이터 분포가 보인다. 이 그래프를 통해 다음과 같이 생각할 수 있다.

  • 정규분포와는 다르다..
  • 수용할만한 positive skewness가 있다..
  • 정점이 있다..

흥미로운데? 더 자세히 살펴보자.

In [6]:
#skewness and kurtosis
print("Skewness: %f" % df_train['SalePrice'].skew())
print("Kurtosis: %f" % df_train['SalePrice'].kurt())
 
Skewness: 1.882876
Kurtosis: 6.536282
 

관련 변수와 함께 'SalePrice' 살펴보기

 

이제 집중적으로 살펴보자.

데이터에 따르면, 'GrLivArea', 'TotalBsmtSF', 'OverallQuality', 'YearBuilt' 같은 변수가 관련이 높아 보인다.

최대한 효율적으로 탐색하기 위해 관련이 높을 것 같은 카테고리를 먼저 주의 깊게 살펴본 후, 전체적인 데이터에 집중할 계획이다.

 

numerical variables 분석

In [7]:
#scatter plot grlivarea/saleprice
var = 'GrLivArea'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));
 
 

흠... 'SalePrice'과 'GrLivArea'은 선형 관계이다.

'TotalBsmtSF'은 어떻까?

In [8]:
#scatter plot totalbsmtsf/saleprice
var = 'TotalBsmtSF'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));
 
 

'TotalBsmtSF' 역시 'SalePrice'와 강한 관계를 갖고 있다. 강한 선형 (혹은 지수 관계?) 인듯하다. 또한, 몇몇 'TotalBsmtSF'은 '0에 가까운 값을 가지고 있다.

 

categorical features 분석

In [9]:
#box plot overallqual/saleprice
var = 'OverallQual'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
f, ax = plt.subplots(figsize=(8, 6))
fig = sns.boxplot(x=var, y="SalePrice", data=data)
fig.axis(ymin=0, ymax=800000);
 
 

확실히 전체적인 퀄리티(EverallQual)가 SalePrice에 큰 영향을 미친다.

In [10]:
var = 'YearBuilt'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
f, ax = plt.subplots(figsize=(16, 8))
fig = sns.boxplot(x=var, y="SalePrice", data=data)
fig.axis(ymin=0, ymax=800000);
plt.xticks(rotation=90);
 
 

강력한 관계는 없지만 오래된 연식보다는 새로운 연식이 다소 가격이 높다.

Note: 우리는 'SalePrice'가 일정한 가격인지 알 수 없다. 일정한 가격은 인플레이션을 제거한다. 만약 가격이 일정하지 않다면, 몇 년 동안 가격이 비교될 수 있는 가격보다 더 비싸야 한다.

 

요약하자면

다음과 같이 결론을 내릴 수 있다.

  • 'GrLivArea'와 'TotalBsmtSF'가 'SalePrice'와 선형관계가 있는것 같다. 즉 한 변수가 증가하면 다른 변수도 증가한다는 뜻이다. 'TotalBsmtSF'의 경우 선형관계의 기울기가 특히 높다는 것을 알 수 있다.
  • 'OverallQual'과 'YearBuilt' 또한 'SalePrice'와 관련이 있다. 'OverallQual'의 boxplot을 보면 품질에 따라 가격이 어떻게 상승하는지 보여준다.

우리는 방금 네 가지 변수를 분석했지만, 우리가 분석해야 할 다른 많은 변수들이 있다. 여기서의 방법은 올바른 특징(feature selection)의 선택인 것 같고, 그것들 사이의 복잡한 관계에 대한 정의(feature engineering)가 아니다.

그 말은 즉슨, 필요 없는 feature와 필요한 feature를 분리해야 한다는 말이다.

 

3. Keep calm and work smart

 

지금까지 우리는 단지 직관을 따르고 중요하다고 생각하는 변수들을 분석했을 뿐이다. 객관적으로 분석하려고 노력했음에도 불구하고, 출발점이 주관적이었다는 뜻이다.

엔지니어로서, 나는 이런 접근방식이 불편한다. 나는 주관성을 배제하려고 한다.. 그럴만한 이유가 있다. 구조 공학에서는 사람의 주관이 많이 들어가면 물리적으로 구조물들이 무너지게 된다.

그러니까, 이제 주관을 배제하고 객관적인 분석을 해보자.

 

실용적인 탐색방법

데이터를 탐색하기 위해, 몇 가지 실용적인 방법으로 시작할 예정이다.

  • Correlation matrix (heatmap style).
  • 'SalePrice' Correlation matrix (zoomed heatmap style).
  • 가장 관련 높은 변수들 사이의 Scatter plots (move like Jagger style).
 

Correlation matrix (heatmap style)

In [11]:
#correlation matrix
corrmat = df_train.corr()
f, ax = plt.subplots(figsize=(12, 9))
sns.heatmap(corrmat, vmax=.8, square=True);
 
 

내 생각에는 이 히트맵이 우리가 사용할 Feature들의 관계를 한눈에 파악할 수 있는 가장 좋은 방법이다. (땡큐 @seaborn!)

첫눈에 나의 관심을 끄는 두 개의 빨간색 사각형이 있다. 첫 번째는 'TotalBsmt'와 '1 stFlrSF'이다. 그리고 두 번째는 'GarageX' 변수들이다. 두 경우 모두 이러한 변수 사이의 상관관계가 크다는 것을 보여준다. 이 상관관계가 너무 강해서 다공성(multicollinearity) 상태를 나타낼 수 있다. 이 변수들에 대해 생각해보면, 이것들이 거의 동일한 정보를 제공하므로 실제로 다공성이 발생한다는 결론을 내릴 수 있다. 히트맵은 이러한 상황을 감지하는 데 매우 좋고, 우리와 같이 피쳐 선택이 필수적인 문제에서는 필수적인 도구다.

관심을 끈 또 다른 것은 'SalePrice' 상관관계이다. 잘 알려진 'GrlivArea', 'TotalBsmtSF'를 볼 수 있다. 'OverallQuality'도 중요하지만, 우리는 또한 고려해야 할 많은 다른 변수들을 볼 수 있다. 그것들이 우리가 다음에 할 일이다.

 

'SalePrice' correlation matrix (zoomed heatmap style)

In [12]:
#saleprice correlation matrix
k = 10 #number of variables for heatmap
cols = corrmat.nlargest(k, 'SalePrice')['SalePrice'].index
cm = np.corrcoef(df_train[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()
 
 

위 hitmap에 표시된 feature들이 'SalePrice'에 가장 상관관계가 높은 변수들이다. 이것들에 대한 내 생각은 다음과 같다.

  • 'OverallQual', 'GrLivArea' 와 'TotalBsmtSF'는 'SalePrice'와 강한 상관 관계가 있다. Check!
  • 'GarageCars'와 'GarageArea'는 가장 상관 관계가 높은 변수들이다. 하지만 마지막에 말했던것과 같이, 두개는 거의 똑같은 의미를 가진다. (구분할수 있는 사람이 있을까?) 이 둘은 쌍둥이 형제나 마찬가지다. 따라서 우리는 분석에서 둘중 하나만 필요하다.('GarageCars'가 'SalePrice'보다 관계가 높으므로 'GarageCars'만 유지할 수 있다)
  • 'TotalBsmtSF'와 '1stFloor'도 쌍둥이 처럼 보인다. 우리는 'TotalBsmtSF' 만 사용하면 될 듯 하다.
  • 'FullBath'?? 레알?
  • 'TotRmsAbvGrd'와 'GrLivArea', 또 다시 쌍둥이다. 체르노빌에서 생성된 데이터인가?
  • 아... 'YearBuilt'... 'YearBuilt'는 'SalePrice'와 약한 상관 관계가 있는듯하다. 솔직히 'YearBuilt' 에 대해 생각하는건 좀 무섭다... 왜냐면 이걸 분석하기 위해서는 시계열 분석을 좀 해야할것 같다는 생각이 들거든.. 이건 숙제로 남겨둘게..

이제 scatter plot을 진행해보자!

 

가장 관련 높은 변수들 사이의 Scatter plots (move like Jagger style).

 

곧 볼 것에 대해서 마음에 준비를 하자. 내가 이 scatter plot을 처음 봤을 때, 완전 뿅 가버렸다. 짧은 공간에 엄청난 정보들이... 그저 놀라울 따름이다. 다시 한번, 땡큐 @seaborn!

In [13]:
#scatterplot
sns.set()
cols = ['SalePrice', 'OverallQual', 'GrLivArea', 'GarageCars', 'TotalBsmtSF', 'FullBath', 'YearBuilt']
sns.pairplot(df_train[cols], size = 2.5)
plt.show();
 
 

이미 우리는 중요 피쳐들이 무언지 알고 있지만, 이 거대한 scatter plot은 우리에게 변수 관계에 대한 합리적인 생각을 할 수 있게 도와준다.

흥미롭게 볼만한 수치 중 하나는 'TotalBsmtSF'와 'GrLiveArea' 사이의 숫자이다. 이 그림에서 우리는 점들이 거의 경계선처럼 작용하는 선을 그리는 것을 볼 수 있다. 대부분의 점들이 선 아래에 있다는 것은 완전히 말이 된다. 지하공간은 지상 생활권과 동일할 수 있지만, 지상 생활권보다 큰 지하공간은 기대할 수 없다(벙커를 사려하지 않는 한).

'SalePrice'와 'YearBuilt'에 관한 결과도 몇 가지 인사이트를 준다. 'dots cloud'의 밑쪽 경계선에서 거의 지수함수로 보일만한 결과가 보인다(창의적으로 보자). 'dots cloud'의 위쪽 경계선 또한 이런 경향을 볼 수 있다(더 창의적으로 보자..). 또한, 지난해에 관한 접 집합이 어떻게 이 한도를 상회하는 경향이 있는지 주목하자(물가가 더 빠르게 오르고 있다고 말하고 싶다).

Ok, 이제 실험은 그만해도 될 것 같다. 누락 데이터 분석으로 넘어가자.

 

4. Missing data(누락 데이터)

missing data에 대해 생각할 때 중요한 질문:

  • missing data가 얼마나 보편적인가?
  • missing data가 랜덤인가? 패턴이 있는가?

질문에 대한 답은 실용적인 이유로 중요하다. 왜냐하면 누락된 데이터는 표본 크기의 축소를 의미할 수 있기 때문이다. 분석을 진행하는 것을 방해할 수 있다. 더구나 실체적 관점에서 보면 누락된 데이터 과정이 편파적이지 않고 불편한 진실을 숨기지 않도록 해야 한다.

In [14]:
#missing data
total = df_train.isnull().sum().sort_values(ascending=False)
percent = (df_train.isnull().sum()/df_train.isnull().count()).sort_values(ascending=False)
missing_data = pd.concat([total, percent], axis=1, keys=['Total', 'Percent'])
missing_data.head(20)
Out[14]:
  Total Percent
PoolQC 1453 0.995205
MiscFeature 1406 0.963014
Alley 1369 0.937671
Fence 1179 0.807534
FireplaceQu 690 0.472603
LotFrontage 259 0.177397
GarageCond 81 0.055479
GarageType 81 0.055479
GarageYrBlt 81 0.055479
GarageFinish 81 0.055479
GarageQual 81 0.055479
BsmtExposure 38 0.026027
BsmtFinType2 38 0.026027
BsmtFinType1 37 0.025342
BsmtCond 37 0.025342
BsmtQual 37 0.025342
MasVnrArea 8 0.005479
MasVnrType 8 0.005479
Electrical 1 0.000685
Utilities 0 0.000000
 

위 표를 분석하여 missing data를 처리하는 방법을 알아보자.

우리는 데이터의 15% 이상이 누락되었을 때 해당 변수를 삭제하고 그것이 존재하지 않았던 것처럼 진행해야 한다. 이런 경우 누락된 자료를 메우기 위해 어떤 꼼수도 시도하지 않겠다는 뜻이다. 이 원칙에 따라 삭제해야 할 변수들이 몇 개 보인다(e.g. 'PoolQC', 'MiscFeature', 'Alley', 등). 중요 정보를 놓치게 될까? 그렇게 생각하지 않는다. 이러한 변수들은 대부분 우리가 집을 살 때 생각하는 측면이 아니기 때문에 그다지 중요한 것은 아닌 것 같다(아마 그렇기 때문에 데이터가 누락되지 않았을까?)

'GarageX' 변수들은 동일한 수치의 missing data를 가지고 있다는 것을 알 수 있다. missing data 가 같은 관측치 일 것이라 확신한다(확인하지 않을 것이지만 단지 5% 일뿐이며 우리는 5달러 문제에 20달러를 지출해서는 안된다). 차고와 관련된 가장 중요한 정보는 'GarageCars'이기 때문에 언급된 'GarageX' 변수들을 삭제하겠다. 동일한 논리가.'BsmtX' 변수에 적용된다.

'MasVnrArea'와 'MasVnrType'에 관해서, 이러한 변수는 반드시 필요한 것은 아니라고 생각할 수 있다. 이미 고려되고 있는 'YearBuilt'와 'OverallQual'와 강한 상관관계를 가지고 있다. 따라서 'MasVnrArea'와 'MasVnrType'를 삭제해도 정보를 잃지 않을 것이다.

마지막으로 'Electrical'에서 단 한 개의 missing data가 있다. 해당 데이터만 삭제하고 변수를 유지하겠다.

요약하면, missing data를 처리하기 위해 '전기' 변수를 제외한 결측 데이터가 있는 변수를 모두 삭제한다. 'Electrical'에서는 missing data가 있는 데이터를 삭제한다.

In [15]:
#dealing with missing data
df_train = df_train.drop((missing_data[missing_data['Total'] > 1]).index,1)
df_train = df_train.drop(df_train.loc[df_train['Electrical'].isnull()].index)
df_train.isnull().sum().max() #just checking that there's no missing data missing...
Out[15]:
0
 

Outliars!

특이치(Outliers) 또한 우리가 알아야 한다. 왜냐? 모델에 현저한 영향을 미칠 수 있고, 우리에게 특정한 행동에 대한 통찰력을 제공하는 귀중한 정보원이 될 수도 있기 때문이다.

Outliers는 복잡한 주제여서 더 많은 관심을 받을 만하다. 여기서는 'SalePrice'의 표준편차와 일련의 scatter plot을 통해 간단히 분석해 보겠다.

 

Univariate analysis(일변량 분석)

 

여기서 주요 관심사는 관찰을 outlier로 정의하는 임계값을 설정하는 것이다. 이를 위해 데이터를 표준화한다. 이 맥락에서 데이터 표준화는 데이터 값을 평균 0과 표준편차 1로 변환하는 것을 의미한다.

In [16]:
#standardizing data
saleprice_scaled = StandardScaler().fit_transform(df_train['SalePrice'][:,np.newaxis]);
low_range = saleprice_scaled[saleprice_scaled[:,0].argsort()][:10]
high_range= saleprice_scaled[saleprice_scaled[:,0].argsort()][-10:]
print('outer range (low) of the distribution:')
print(low_range)
print('\nouter range (high) of the distribution:')
print(high_range)
 
outer range (low) of the distribution:
[[-1.83820775]
 [-1.83303414]
 [-1.80044422]
 [-1.78282123]
 [-1.77400974]
 [-1.62295562]
 [-1.6166617 ]
 [-1.58519209]
 [-1.58519209]
 [-1.57269236]]

outer range (high) of the distribution:
[[3.82758058]
 [4.0395221 ]
 [4.49473628]
 [4.70872962]
 [4.728631  ]
 [5.06034585]
 [5.42191907]
 [5.58987866]
 [7.10041987]
 [7.22629831]]
 

'SalePrice'이 어떤 특성을 갖고 있는지 다시 한번 보면 :

  • Low range 값은 유사하며 0에서 그리 멀지 않음.
  • High range 값은 0에서 멀리 떨어져있고, 7.x 로 값이 범위를 많이 벗어났다.

현재로서는 이 값들 중 어떤 것도 outlier라고 생각하지 않겠지만, 우리는 이 두 가지 7.xx 값에 주의해야 한다.

 

Bivariate analysis(다변량 분석)

 

우리는 이미 다음과 같은 scatter plot을 이미 알고 있다. 하지만, 우리가 새울 관점에서 새로운 사물을 바라볼 때, 항상 발견할 수 있는 것들이 생긴다.

In [17]:
#bivariate analysis saleprice/grlivarea
var = 'GrLivArea'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));
 
'c' argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with 'x' & 'y'.  Please use a 2-D array with a single row if you really want to specify the same RGB or RGBA value for all points.
 
 

밝혀진 내용:

  • 'GrLivArea' 의 매우 큰 두개의 값이 이상해 보이고 클러스터에서 많이 벗어나있다. 추측해보건데 아마 농경지이지 않을까 싶다. 그렇기에 가격이 낮은것 같다. 확신할 수는 없지만 이 두가지 점이 전형적인 경우를 대표하지 않는다고 확신하기에, 이 두개의 값을 outlier라고 정의하고 삭제하겠다.
  • 그림의 맨 위에 있는 두개의 관측치는 우리가 주의해야 한다고 말한 7.xx 관찰이다. 두가지 특수한 경우처럼 보이지만, 추세를 따르고 있는것으로 보인다. 따라서 저 값들은 유지하겠다.
In [18]:
#deleting points
df_train.sort_values(by = 'GrLivArea', ascending = False)[:2]
df_train = df_train.drop(df_train[df_train['Id'] == 1299].index)
df_train = df_train.drop(df_train[df_train['Id'] == 524].index)
In [19]:
#bivariate analysis saleprice/grlivarea
var = 'TotalBsmtSF'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));
 
'c' argument looks like a single numeric RGB or RGBA sequence, which should be avoided as value-mapping will have precedence in case its length matches with 'x' & 'y'.  Please use a 2-D array with a single row if you really want to specify the same RGB or RGBA value for all points.
 
 

일부 데이터를 (e.g. TotalBsmtSF > 3000) 삭제하고 싶은 유혹이 생길 수도 있다. 하지만 그럴 가치가 없다. 그 값들을 감수하고도 좋은 모델을 만들 수 있으니 아무것도 하지 않겠다.

 

5. Getting hard core

 

그래서 'SalePrice'는 무엇인가?

이 질문에 대한 답은 다변량 분석(multivariate analysis)의 통계적 기초에 기초하는 가정을 검정함으로써 알 수 있다. 이미 데이터 정리를 좀 했고 'SalePrice'에 대해 많은 것을 발견했다. 이제 'SalePrice'가 multivariate analysis을 적용할 수 있는 통계적 가정을 어떻게 준수하는지 깊이 있게 이해할 때가 되었다.

Hair et al. (2013)에 따르면, 다음 4가지 가정을 시험해야 한다.

  • Normality - 정규성이 의미하는 것은 데이터가 정규 분포처럼 보여야 한다는 것이다. 이는 여러 통계 테스트가 이에 의존하기 때문에 중요하다(예: t-통계). 이 연습에서는 'SalePrice'에 대한 일변량(univariate) 정규성(제한된 접근 방식)을 점검할 것이다. 일변량 정규성은 다변량 정규성(이것이 우리가 얻고 싶어 하는 것이다)을 보장하지 않지만, 도움이 된다는 것을 기억하라. 고려해야 할 또 다른 세부사항은 큰 표본 (>200 관측치)에서 정규성은 그런 문제가 아니라는 것이다. 그러나 정규성을 해결하면 다른 문제(예: 이단성/heteroscedacity)는 많이 피하게 되기 때문에 이것이 우리가 이 분석을 하는 주된 이유다.

  • Homoscedasticity - 철자가 쓰기 매우 복잡하다. 동질성(homoscedasticity)은 '종속변수가 예측 변수의 범위에 걸쳐 동일한 수준의 분산을 보이는 경우'(Hair et al., 2013))를 가리킨다. 오차항이 독립 변수의 모든 값에서 동일하기를 원하기 때문에 동질성이 바람직하다.

  • Linearity - 선형성을 평가하는 가장 일반적인 방법은 scatter plot을 선형 패턴을 검색하는 것이다. 패턴이 선형적이지 않다면 데이터 변환을 탐구하는 것이 가치가 있을 것이다. 하지만, 우리가 본 대부분의 산점도는 선형 관계를 가지고 있는 것처럼 보이기 때문에, 우리는 이 문제에 관여하지 않을 것이다.

  • Absence of correlated errors - 정의에서 알 수 있듯이, 하나의 오류가 다른 오류와 상관될 때 상관된 오류가 발생한다. 예를 들어, 하나의 양의 오류가 체계적으로 음의 오차를 만든다면, 이 변수들 사이에 관계가 있다는 것을 의미한다. 이것은 종종 시계열에서 일어나는데, 어떤 패턴은 시간과 관련이 있다. 우리 또한 이 일에 관여하지 않을 것이다. 하지만 무언가를 발견한다면, 여러분이 얻고 있는 효과를 설명할 수 있는 변수를 추가하도록 노력하라. 그것은 상관된 오류에 대한 가장 일반적인 해결책이다.

 

In the search for normality

 

여기서의 포인트는 'SalePrice'를 매우 희박하게 테스트하는 것이다. 다음 사항에 주목하여 진행해보자.

  • Histogram - Kurtosis(첨도) 와 skewness(왜도)
  • Normal probability plot - 데이터 분포는 정규 분포를 나타내는 사선을 근접하게 따라야한다.
In [20]:
#histogram and normal probability plot
sns.distplot(df_train['SalePrice'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['SalePrice'], plot=plt)
 
 
 

오케이, 'SalePrice'는 정규 분포가 아니다. 'peakedness'를 보여주며 이는 양의 왜도를 나타내며 사선(빨간 선)을 따르지 않는다.

하지만 모든 게 다 사라진 게 아니다. 간단한 데이터 변환으로 문제를 해결할 수 있다. 이것은 통계학 서적에서 배울 수 있는 놀라운 것들 중 하나이다. positive skewness의 경우, log 변환이 대게 잘 작동한다. 내가 이걸 발견했을 때, 나는 호그와트의 학생이 새로운 쿨 마법을 발견하는 것 같은 기분이 들었다.

아바다 케다브라!

In [21]:
#applying log transformation
df_train['SalePrice'] = np.log(df_train['SalePrice'])
In [22]:
#transformed histogram and normal probability plot
sns.distplot(df_train['SalePrice'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['SalePrice'], plot=plt)
 
 
 

지렸다! 이제 'GrLivArea'을 살펴보자.

In [23]:
#histogram and normal probability plot
sns.distplot(df_train['GrLivArea'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['GrLivArea'], plot=plt)
 
 
 

같은 맛.. skewness... 아브다 카다브라!

In [24]:
#data transformation
df_train['GrLivArea'] = np.log(df_train['GrLivArea'])
In [25]:
#transformed histogram and normal probability plot
sns.distplot(df_train['GrLivArea'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['GrLivArea'], plot=plt)
 
 
 

다음 분, 오세요...

In [26]:
#histogram and normal probability plot
sns.distplot(df_train['TotalBsmtSF'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['TotalBsmtSF'], plot=plt)
 
 
 

오케이, 이제 보스 몬스터를 상대할 차례이다. 여기선 어떻게 해야 할까?

  • 일반적인 왜도를 나타내고 있음.
  • 상당한 양의 0값(지하실이 없는 주택).
  • 0값은 로그변환을 허용하지 않기 때문에 큰 문제임.

여기서 로그 변환을 적용하기 위해 지하실(binary variable)을 갖거나 갖지 않는 효과를 얻을 수 있는 변수를 만들겠다. 그런 다음 값이 0인 관측치를 무시한 채 0이 아닌 관측치를 모두 로그 변환할 예정이다. 이렇게 하면 지하실을 갖거나 갖지 않고 데이터를 변환할 수 있다.

이 방법이 정확한지 잘 모르겠다. 단지 이 케이스에 딱 맞는 것 같았다. 이게 바로 내가 '고위험 엔지니어링'이라고 부르는 것이다.

In [27]:
#새로운 변수를 위한 컬럼을 생성한다.(이진변수 하나면 충분하다)
#if area>0 it gets 1, for area==0 it gets 0
df_train['HasBsmt'] = pd.Series(len(df_train['TotalBsmtSF']), index=df_train.index)
df_train['HasBsmt'] = 0 
df_train.loc[df_train['TotalBsmtSF']>0,'HasBsmt'] = 1
In [28]:
#transform data
df_train.loc[df_train['HasBsmt']==1,'TotalBsmtSF'] = np.log(df_train['TotalBsmtSF'])
In [29]:
#histogram and normal probability plot
sns.distplot(df_train[df_train['TotalBsmtSF']>0]['TotalBsmtSF'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train[df_train['TotalBsmtSF']>0]['TotalBsmtSF'], plot=plt)
 
 
 

'homoscedasticity' 어떻게 읽고 쓰는 건지..¶

 

두 변수에 대한 homoscedasticity를 테스트하는 최선의 접근방식은 그래픽이다. equal dispersion(등분산)으로부터의 이탈은 원뿔(그래프의 한쪽에 작은 분산, 반대쪽에 큰 분산)이나 다이아몬드(분포 중심에 있는 많은 점)와 같은 모양으로 나타난다.

'SalePrice'와 'GrLivArea'로 시작해보자...

In [30]:
#scatter plot
plt.scatter(df_train['GrLivArea'], df_train['SalePrice']);
 
 

이 scatter plot의 이전 버전(로그 변환 이전)은 원뿔형이었다. 그런데 보다시피 더 이상 원뿔 모양이 없다. 이게 normality의 힘이다. 단수 일부 변수에서 정규성을 보장함으로써 동종성 문제를 해결했다.

이제 'SalePrice'와 'TotalBsmtSF'를 확인해보자.

In [31]:
#scatter plot
plt.scatter(df_train[df_train['TotalBsmtSF']>0]['TotalBsmtSF'], df_train[df_train['TotalBsmtSF']>0]['SalePrice']);
 
 

우리는 일반적으로 'SalePrice'가 'TotalBsmtSF'의 범위에 걸쳐 동일한 수준의 분산을 나타낸다고 말할 수 있다. Cool!

 

마지막이지만, 가장 작은 더미 변수

 

이지 모드

In [32]:
#categorical 변수를 더미로 변경한다.
df_train = pd.get_dummies(df_train)
 

결론

 

드디어! 끝에 도달했다.

이 커널을 통해 Hair et al. (2013)에서 제안한 많은 전략을 실행했다. 변수에 대해 철학적으로 생각하고 'SalePrice'만을 분석했으며, 가장 상관성이 높은 변수와 결측 데이터(missing data) 및 특이치(outlier)를 다루었으며, 몇 가지 기초적인 통계적 가정을 시험했고, 심지어 범주형 변수를 더미 변수로 변환했다. python이 이 모든 일을 쉽게 도와주었다.

그러나 탐구는 끝나지 않았다. 이 분석은 단순히 데이터 분석에서 끝났다는 것을 기억하자. 이제 본격적으로 모델을 만들어볼 시간이다. 주택 가격을 예측해보자. regularized linear regression이 좋을까? 아니면 ensemble 메서드가 좋을까? 아니면 다른 거라도?

이제, 다음은 너에게 달렸다!

 

Acknowledgements

이 글을 읽어줘서 고맙다. João Rico

In [35]:
from IPython.core.display import display, HTML
display(HTML("<style>.container {width:105% !important;}</style>"))
 
 
In [ ]:
 

 


Reference

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

https://www.kaggle.com/pmarcelino/comprehensive-data-exploration-with-python

 

댓글