회귀에서 목표 에상 변수(forecast variable)의 과거 값을 이용하는 대신에, 이동 평균 모델은 회귀처럼 보이는 모델에서 과거 예측 오차(forecast error)을 이용한다.

여기서 E(t)는 백색잡음(White noise)이다. 이것을 q차 이동 평균 모델인 MA(q) 모델이라고 부르자.

물론, E(t)의 값을 관찰하지 않기 때문에, 실제로는 일반적인 회귀가 아니다.

Y(t)의 각 값을 과거 몇 개의 예측 오차(forecast error)의 가중 이동 평균으로 생각할 수 있다는 것에 주목하자. 하지만 이동 평균 평활과 헷갈리면 안된다. 이동 평균 모델은 미래 값을 예측할 때 사용한다. 이동 평균 평활은 과거 값의 추세-주기를 측정할 때 사용한다.

 

매개변수를 다르게 설정한 이동 평균 모델로부터 얻은 데이터의 두가지 예

MA(1): Y(t)=20+E(t)+0.8E(t-1)

MA(2): Y(t)=E(t)-E(t-1)+0.8E(t-2)

*두 가지 경우 모두, E(t)는 평균이 0이고 분산이 1인 정규 분포를 따르는 백색 잡음(White noise)이다.

 

위 그림은 MA(1) 모델과 MA(2) 모델로 얻은 몇몇 데이터를 나타낸다. 매개변수 θ1,…,θq 을 바꾸면 다른 시계열 패턴이 나타난다. 자기회귀 모델을 이용하는 경우처럼, 오차항 E(t)의 분산은 시계열의 패턴이 아니라 눈금만 맞출 것이다.

 

정상성을 나타내는 어떤 AR(p) 모델을 MA(∞) 모델로 쓸 수 있다. 예를 들어, 반복하여 대입하면, AR(1) 모델에 대해 다음과 같이 나타낼 수 있다.

 

-1<ϕ1<1 에 대해, k 가 커질 수록   

이는 MA(∞)의 과정이다.

 

MA 매개변수에 대한 몇몇 제한조건을 도입하면 반대 결과도 성립한다. 그러면 MA 모델을 가역적(invertible)이라고 부른다. 즉, 어떤 가역적인 MA(q) 과정을 AR(∞) 과정으로 쓸 수 있다. 가역적 모델은 단순하게 MA 모델을 AR 모델로 바꿀 수 있도록 하는 것만은 아니다. 몇 가지 수학적 특징도 갖고 있다.

 

예를 들어보자.

MA(1) 과정

다음과 같은 식을 생각해보자. 이것은 AR(∞)로 표현하면, 가장 최근의 오차는 현재와 과거 관측값의 선형 함수로 쓸 수 있다.

|θ|>1 이면, 가중치의 시작(lag) 값이 증가함에 따라 증가하고, 따라서 더 멀리 떨어진 관측값일수록 현재 오차에 미치는 영향이 커진다.

|θ|=1 이면, 가중치가 크기에 대해서는 상수, 멀리 떨어진 관측값과 가까운 관측값 모두 동일하게 영향을 준다.

위 예 AR(1), AR(2) 경우 모두 그럴듯하지 않기 때문에, |θ|<1 가 필요하며, 결국 가장 최근 관측값이 멀리 떨어진 관측값  보다 더 큰 가중치를 갖게 된다. 따라서, |θ|<1일때 과정은 가역적(invertible)이다.

 

다른 모델에 대한 가역성(invertibility) 제한조건은 정상성(stationarity) 제한조건과 비슷하다.

  • MA(1) 모델의 경우: −1<θ1<1.
  • MA(2) 모델의 경우:   θ1−θ2<1.

q>=3에 대해서는 더 복잡한 조건이 성립한다. 여기서, 파이썬, R에서 모델을 다룰 때, 이러한 제한조건을 처리해준다.

 

 

[출처: otexts.com/fppkr/MA.html]

다중 회귀 모델에서, 목표 예상 변수(forecast variable)의 선형 조합을 이용해, 관심 있는 변수를 예측한다. 

자기 회귀(autoregressive) 모델에서는, 변수의 과거 값의 선형 조합을 이용하여 관심 있는 변수를 예측한다.

autoregressive라는 단어에는 자기 자신에 대한 변수의 회귀라는 의미가 있다.

 

따라서, 차수 p의 자기회귀 모델(autoregressive models)은 다음과 같은 수학적 공식을 따른다.

y(t)의 시차 값을 예측변수(predictor)로 다루는 것만 제외하면 다중 회귀와 비슷하다.

 

다중회귀 모델

위 자기회귀 모델을 p 자기회귀 모델인 AR(p) 모델이라 부르자.

 

자기회귀 모델(autoregressive model)은 다양한 종류의 서로 다른 시계열 패턴을 매우 유연하게 다루는 장점이 있다.

매개변수를 다르게 설정한 자기회귀 모델로부터 얻은 데이터의 두가지 예.

AR(1): Yt=18-0.8Y(t-1)+E(t)

AR(2): Yt=8+1.3Y(t-1)-0.7Y(t-2)+E(t)

*두 모델 모두, E(t)는 평균이 0, 분산이 1인 정규 분포를 따르는 백색잡음(White noise)이다.

두 시계열은 AR(1) 모델과 AR(2)모델로 얻은 시계열이다. 매개변수 ϕ1,…,ϕp 을 바꾸면 다른 시계열 패턴이 나온다.

오차항 εt의 분산은 시계열의 패턴이 아니라 눈금만 바꿀 것이다.

 

AR(1) 모델은:

  • ϕ1=0일 때, yt는 백색잡음과 같다.
  • ϕ1=1이고 c=0일 때, yt는 확률보행 모델과 같다;
  • ϕ1=1이고 c≠0일 때, yt는 표류가 있는 확률보행 모델과 같다.
  • ϕ1<0일 때, yt는 평균값을 중심으로 진동하는 경향을 나타낸다.

보통은 자기회귀 모델을 정상성을 나타내는 데이터에만 사용한다. 이 경우, 매개변수 값에 대한 몇몇 제한조건이 필요하다.

  • AR(1) 모델의 경우: −1<ϕ1<1
  • AR(2) 모델의 경우: −1<ϕ2<1, ϕ1+ϕ2<1, ϕ2−ϕ1<1

p>=3일 때는, 제한조건이 훨씬 복잡하다. 

모델을 다룰 때 R이나 파이썬에서 이러한 제한조건을 처리해준다.

 

[출처:otexts.com/fppkr/MA.html]

1. View의 개요

1. View와 View Group

안드로이드 화면에서 실제로 사용되는 것들은 모두 View라는 클래스의 상속을 받는다.

뷰 클래스는 '위젯'이라고도 하는데, 쉽게 말해 화면에서는 버튼을 버튼 위젯, 시제 코드에서는 버튼 클래스라고 부른다.

또한, 다른 위젯을 담을 수 있는 위젯을 특별히 레이아웃이라 하며, 레이아웃은 ViewGroup이라는 클래스 아래에 존재한다.

위젯과 레이아웃

그러나 위젯은 단독으로 존재하지 않으며, 위젯을 담아 배치하는 틀이 바로 레이아웃이다.

레이아웃은 위젯을 포함하는 컨테이너 역할을 하므로 눈에 보이는 개념이 아니다.

위젯은

-넓은 의미로 View 클래스 하위의 모든 클래스를 지칭. 

-좁은 의미로 레이아웃 이외의 클래스를 지칭.

 

View 클래스 계층도

안드로이드에서 View 클래스의 상속을 받은 클래스(위젯) 계층도의 일부를 살펴보자.

안드로이드의 View 클래스 계층도

위 그림을 보면 최상단에 Object 클래스가 있고, 이를 상속받은 View 클래스가 있다.

안드로이드 화면에 나타나는 모든 위젯은 View 하위에 존재한다. 

레이아웃은 ViewGroup을 상속받은 LinearLayout, RelativeLayout, FrameLayout,GridLayout,TableLayout을 지칭한다.

뷰 컨테이너는 ListView,GridView,TabHost,Gallery 등이 있다. 뷰 컨테이너도 ViewGroup 클래스에서 상속받는다.

 

2. View 클래스의 XML 속성

View 클래스의 XML 속성은 수십 개가 넘으니 자주 사용할 것만 살펴보자. 다음은 Button의 기본 형태이며 View 클래스로부터 상속받았다.

버튼의 id,layout_width 등 몇 가지 기본적인 XML 속성이 표현되었다. 이러한 속성을 하나씩 살펴보자.

id 속성

id 속성은 모든 위젯의 아이디를 나타낸다. java 코드에서 버튼 등의 위젯에 접근할 때 id 속성에 지정한 아이디를 사용한다. 일반적으로 id 속성은 위젯에 아이디를 새로 부여하는 개념이므로 '@+id/' 형식으로 지정한다. 그 다음 새로 지정할 아이디를 넣는다. "@+id/btn1"

 

위젯에 접근하기 위해 Java 코드에서 다음과 같은 형식을 사용한다.

 

위 버튼 예제는 Java 코드에서 다음과 같은 접근 방식을 사용할 수 있다.

Button, RadioButton,CheckBox 등의 위젯은 일반적으로 클릭 또는 터치했을 때, 어떤 동작을 하기 위한 것이므로 id 속성을 지정한다. 

하지만, 클릭이나 터치를 해도 아무 동작이 필요없는 글자(텍스트뷰)나 배경 이미지(이미지뷰)등은 굳이 id 속성을 지정하지 않아도 된다.

 

예제 4-1 id 속성의 XML 코드

layout_width, layout_height 속성

layout_width,layout_height 속성은 모든 위젯에 필수로 들어간다. 매우 중요하다.

이 둘은 각각 위젯의 너비와 높이를 나타내며 match_parent 와 wrap_content 값으로 설정할 수 있다.

match_parent: 이름 그대로 자신의 부모(대개는 레이아웃)에 너비나 높이를 맞춘다는 의미.

wrap_content: 글자가 꼭 들어갈 정도로 자신의 너비나 높이를 설정한다는 의미.

 

버튼의 경우 그 안에 들어갈 것이 글자이기 때문에 글자가 곡 들어갈 정도라고 표현했다. 하지만 다른 위젯에는 글자 외의 것이 들어간다. 예를 들면 이미지뷰에는 이미지가 들어가고 wrap_content는 이미지가 꼭 들어갈 정도로 너비나 높이가 설정된다.

 

예제를 살펴보자.

위 예제들에서 볼 수 있듯이 layout_width 속성에 wrap_content 값을 사용하면 버튼의 너비가 그 안의 글자인 '버튼입니다'에 꼭 맞는 크기가 되고, match_parent(또는 fill_parent)값을 사용하면 '버튼입니다' 글자와 관계없이 버튼을 싸고 있는 부모(linear_layout)의 너비에 꽉 차는 크기가 된다. 

 

값을 숫자로 직접 지정할수는 있지만 단위에 주의해야 한다. 가장 단순한 방법이 px(PiXel)단위이다. 

 

background 속성

background 속성은 위젯의 색상을 주로 #RRGGBB 값으로 지정한다. 각 값은 Red, Green, Blue를 의미하며 RR,GG,BB의 위치는 16진수 00~FF로 표현할 수 있다. 예를 들면 빨간색은 #FF0000, 파란색은 #0000FF로 지정한다. 값을 적절히 조합하면 필요한 색을 만들 수도 있다.

예제를 살펴보자.

위 예제는 레이아웃을 빨간색(#ff0000), 버튼을 초록색(#00ff00)으로 표현했다.

 

색상을 지정하기 위해 #AARRGGBB 방식을 사용할 수도 있다. AA는 알파 값으로 투명도이다. 00~FF로 지정할 수 있는데 00은 완전 투명을, FF는 완전 불투명을 나타낸다. 

즉, #FF0000FF는 불투명파랑, #000000FF는 완전 투명이므로 색상이 나타나지 않는다. #770000FF는 반투명 파란색이다.

 

padding, layout_margin 속성

padding 속성을 사용하여 위젯의 경계선으로부터 위젯 안의 요소가 떨어지게 설정할 수 있다. 기본적으로는 레이아웃 내에 버튼, 텍스트 뷰 등을 여러 개 두었을 때 레이아웃의 경계선에 딱 붙어서 표현되는데, padding 속성을 사용하면 레이아웃의 경계선과 위젯 사이에 여백을 둘 수 있다.

우선 텍스트뷰, 에디트텍스트, 버튼이 있는 XML 파일을 살펴보자.

 

위 코드를 이용해 결과를 살펴보면 경계선에 붙어 있어 답답해 보인다. 레이아웃에 padding 속성을 사용해보자.

*padding 은 상하좌우 모두에 지정하는 속성이다. 각각 따로 지정하고 싶으면 paddingTop, paddingBottom, paddingLeft,paddingRight를 이용한다.

LinearLayout에 지정된 padding속성 때문에 그 안의 요소들의 경계선에서 30dp만큼 떨어져 출력되었다.

위 에제는 LinearLayout에 padding을 설정했지만, Button에 padding을 설정하면 버튼 안의 글자가 버튼의 경계선에서 일정 간격 떨어져서 표현된다. 이와는 반대로 위젯과 위젯 사이에 여유를 두고 싶다면 layout_margin 속성을 사용하자.

*layout_margin은 상하좌우 모두에 지정하는 속성이다. 각각 따로 지정하고 싶으면 layout_marginTop, layout_marginBottom, layout_marginLeft,layout_marginRight를 이용한다.

 

예제 4-12 layout_margin 속성의 XML 코드

정리하면 padding 속성에는 자신의 내부에 들어 있는 위젯과 자신의 경계선 사이 간격을 지정하고, layout_margin 속성에는 자신과 부모 레이아웃이나 위젯 사이의 간격, 주위 다른 위젯과 간격을 지정한다. 따라서, layout_margin은 각 위젯의 속성으로 지정해야 한다.

 

Visibility 속성

visibility 속성으로 위젯을 보일 것인지 여부를 설정할 수 있다. 세 가지 값을 지정할 수 있는데 디폴트인 visible은 보이는 상태, invisible과 gone은 안 보이는 상태이다. 

invisible과 gone의 차이는, invisible은 보이지 않을 뿐 원래의 자리를 계속 유지하지만 gone은 그 자리까지 아예 내놓는다는 것이다.

 

visibility 속성은 XML 보다 Java 코드에서 상황에 따라 동적으로 필요한 버튼 등을 보이거나 안보이게 하는 경우에 주로 활용된다.

Enabled, Clickable 속성

위젯의 동작 여부는 enabled 속성으로 설정한다.

클릭이나 터치가 가능하게 하는 것은 clickable 속성으로 설정한다.

값은 true와 false로 지정하며 디폴트 값은 true 이다. enabled, clickable 속성은 XML 보다 Java 코드에서 주로 사용한다.

Rotation 속성

rotation 속성은 위젯을 회전시켜 출력하며 값은 각도로 지정한다. 특별 화면을 만들 때 사용한다.

지금까지는 많이 사용되는 XML 속성을 살펴보았다. 이 외에도 상당히 많지만 필요할 때마다 설명하겠다.

 

 

[자료 출처: Android Studio를 활용한 안드로이드 프로그래밍(6판)] 

이번 프로젝트는 아파트 실거래가를 예측하는 것이다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import timeit
import sklearn
import warnings
warnings.filterwarnings('ignore')
import sys

train_data=pd.read_csv('d:/data/real_estate/train.csv')
test_data=pd.read_csv('d:/data/real_estate/test.csv')

print('train.csv. Shape: ',train_data.shape)
print('test.csv. Shape: ', test_data.shape)

train.csv. Shape:  (1216553, 13)
test.csv. Shape:  (5463, 12)

훈련데이터는 총 120만개정도이고 , 테스트 데이터는 5400여개가 존재한다.

각 데이터의 정보를 간략하게 살펴보자.

 

train_data.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1216553 entries, 0 to 1216552
Data columns (total 13 columns):
 #   Column                  Non-Null Count    Dtype  
---  ------                  --------------    -----  
 0   transaction_id          1216553 non-null  int64  
 1   apartment_id            1216553 non-null  int64  
 2   city                    1216553 non-null  object 
 3   dong                    1216553 non-null  object 
 4   jibun                   1216553 non-null  object 
 5   apt                     1216553 non-null  object 
 6   addr_kr                 1216553 non-null  object 
 7   exclusive_use_area      1216553 non-null  float64
 8   year_of_completion      1216553 non-null  int64  
 9   transaction_year_month  1216553 non-null  int64  
 10  transaction_date        1216553 non-null  object 
 11  floor                   1216553 non-null  int64  
 12  transaction_real_price  1216553 non-null  int64  
dtypes: float64(1), int64(6), object(6)
memory usage: 120.7+ MB
test_data.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5463 entries, 0 to 5462
Data columns (total 12 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   transaction_id          5463 non-null   int64  
 1   apartment_id            5463 non-null   int64  
 2   city                    5463 non-null   object 
 3   dong                    5463 non-null   object 
 4   jibun                   5463 non-null   object 
 5   apt                     5463 non-null   object 
 6   addr_kr                 5463 non-null   object 
 7   exclusive_use_area      5463 non-null   float64
 8   year_of_completion      5463 non-null   int64  
 9   transaction_year_month  5463 non-null   int64  
 10  transaction_date        5463 non-null   object 
 11  floor                   5463 non-null   int64  
dtypes: float64(1), int64(5), object(6)
memory usage: 512.3+ KB

훈련데이터와 테스트데이터의 레이블을 살펴보면 훈련데이터에만 transaction_real_price가 있는것을 확인할 수 있다. 이는 결국 훈련 데이터를 통해 가격을 예측해보기 위한점이다.

 

[Train Column name Description]

  • transaction_id : 아파트 거래에 대한 유니크한 아이디
  • apartment_id : 아파트 아이디
  • city : 도시
  • dong : 동
  • jibun : 지번
  • apt : 아파트단지 이름
  • addr_kr : 주소
  • exclusive_use_area : 전용면적
  • year_of_completion : 설립일자
  • transaction_year_month : 거래년월
  • transaction_date : 거래날짜
  • floor : 층
  • transaction_real_price : 실거래가 (target variable)

이제 결측값이 있는지 확인해보자. 

train_null=train_data.drop('transaction_real_price',axis=1).isnull().sum()/len(train_data)*100
test_null=test_data.isnull().sum()/len(test_data)*100
pd.DataFrame({'train_null_count': train_null,'test_null_count':test_null})

결측치 확인

딱히, 결측치는 존재하지 않는다.

 

간단하게 훈련 데이터를 살펴보자.

train_data.head()


transaction_id	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price
0	0	7622	서울특별시	신교동	6-13	신현(101동)	신교동 6-13 신현(101동)	84.82	2002	200801	21~31	2	37500
1	1	5399	서울특별시	필운동	142	사직파크맨션	필운동 142 사직파크맨션	99.17	1973	200801	1~10	6	20000
2	2	3578	서울특별시	필운동	174-1	두레엘리시안	필운동 174-1 두레엘리시안	84.74	2007	200801	1~10	6	38500
3	3	10957	서울특별시	내수동	95	파크팰리스	내수동 95 파크팰리스	146.39	2003	200801	11~20	15	118000
4	4	10639	서울특별시	내수동	110-15	킹스매너	내수동 110-15 킹스매너	194.43	2004	200801	21~31	3	120000

탐색적 데이터 분석 및 처리

훈련데이터 ['transaction_real_price']레이블의 통계적 요약을 살펴보면 다음과 같다.

train_data['transaction_real_price'].describe()

count    1.216553e+06
mean     3.822769e+04
std      3.104898e+04
min      1.000000e+02
25%      1.900000e+04
50%      3.090000e+04
75%      4.700000e+04
max      8.200000e+05
Name: transaction_real_price, dtype: float64

평균 38827 Min: 100 MAX: 820000 표준편차:31048

 

이를 그래프로 막대그래프와 왜도 첨도를 통해 그래프를 살펴보자. 

왜도(Skewness): 왼쪽으로 치우쳐져 있을수록 값이 크다.

첨도(Kurtosis): 첨도 값이 3에 가까울 경우 정규분포에 근사한다. 첨도 값이 클수록 뾰족하고 값이 작을수록 완만해진다.

f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train_data['transaction_real_price'])
print("%s->Skewness: %f, Kurtosis: %f" % ('transaction_real_price',train_data['transaction_real_price'].skew(),train_data['transaction_real_price'].kurt()))

transaction_real_price->Skewness: 3.407169, Kurtosis: 24.839821

일반 분포

이 그래프를 정규화에 근사시키기 위해 log1p 함수를 이용해 로그를씌워 다시 한번확인해보자.

train_data['transaction_real_price'] = np.log1p(train_data['transaction_real_price'])
print("%s -> Skewness: %f, Kurtosis: %f" %  ('transaction_real_price',train_data['transaction_real_price'].skew(),train_data['transaction_real_price'].kurt()))

transaction_real_price -> Skewness: -0.094932, Kurtosis: 0.248866

f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train_data['transaction_real_price'])

log를 씌운 그래프 분포

칼럼들의 상관관계

각 칼럼들간의 상관관계를 확인해보자. 

k=train_data.shape[1]
corrmat=train_data.corr()
cols=corrmat.nlargest(k,'transaction_real_price')['transaction_real_price'].index
cm=np.corrcoef(train_data[cols].values.T)
f,ax=plt.subplots(figsize=(8,6))
sns.heatmap(data=cm,annot=True,square=True,fmt='.2f',linewidths=5,cmap='Reds',yticklabels=cols.values,xticklabels=cols.values)

각 칼럼들간의 상관관계

훈련데이터의 'apartment_id' 칼럼의 unique_value의 갯수를 확인해보자.

 

len(train_data['apartment_id'].unique())

12533

 

이후 'transaction_id'를 따로 변수로 지정한 후, 본래의 데이터프레임에서는 없애주자.

train_id=train_data['transaction_id']
train=train_data.drop('transaction_id',axis=1)
test_id=test_data['transaction_id']
test=test_data.drop('transaction_id',axis=1)

Floor(층)

가설) 층수가 높아질수록 집값도 높아진다.

 

'floor' 과 'transaction_real_price'의 연관성을 확인하기 위해 산점도표에 케스팅해보자. 

 

 

f,ax=plt.subplots(figsize=(8,6))
plt.scatter(train['floor'],train['transaction_real_price'])
plt.xlabel('floor')
plt.ylabel('transaction_real_price')
plt.show()

집값과 층수의 연관성

위 그래프를 확인해보면 층수가 높아질수록 집값도 높아진다라는 본인의 생각은 틀린것을 알 수 있다.

 

그렇다면,

floor(층수)칼럼에 대해서 막대그래프를 통한 분포표도 확인해보자.

 

f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train['floor'])
print('%s->Skewness: %f, Kurtosis: %f'% ('floor',train['floor'].skew(),train['floor'].kurt()))

floor->Skewness: 1.324710, Kurtosis: 3.796603

exclusive_use_area(전용면적)

가설) 전용면적이 넓어지면 넓어질수록 집값은 비싸다

우선 훈련데이터의 'exclusive_use_area'를 간단하게 통계적 요약으로 살펴보자.

train['exclusive_use_area'].describe()

count    1.216553e+06
mean     7.816549e+01
std      2.915113e+01
min      9.260000e+00
25%      5.976000e+01
50%      8.241000e+01
75%      8.497000e+01
max      4.243200e+02
Name: exclusive_use_area, dtype: float64

Mean: 78.1 Min: 9.2 Max: 424.3

이를 산점도표에 케스팅하여 전용면적과 집값의 연관성을 확인해보자.

 

f,ax=plt.subplots(figsize=(8,6))
plt.scatter(train['exclusive_use_area'],train['transaction_real_price'])
plt.xlabel('exclusive_use_area')
plt.ylabel('transaction_real_price')
plt.show()

집값과 전용면적의 연관성

대략 위 산점도표를 살펴보면, 전용면적이 넓어질수록 집값도 올라가는 경향을 보인다. 그러므로 가설은 맞다고 가정하자.

 

이상치들을 확인해보자.

 

train[train['exclusive_use_area']>400]

	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price
563870	12633	서울특별시	도곡동	193-1	힐데스하임빌라	도곡동 193-1 힐데스하임빌라	424.32	1998	201604	11~20	10	498000
train[(train['exclusive_use_area']<150)&(train['transaction_real_price']<200)]

	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	100

크게 이상치라고 판단할수 있는 건 없는것 같다.

이를 한번더 막대 그래프로 살펴보자.

 

f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train['exclusive_use_area'])
print("%s->Skewness: %f, Kurtosis= %f"% ('exclusive_use_area',train['exclusive_use_area'].skew(),train['exclusive_use_area'].kurt()))

transaction_year_month(거래 년,월)

훈련데이터와 테스트데이터에 있는 년월 칼럼을 따로 뽑아와 새로운 칼럼으로 만들어보자.

train_test_data=[train,test]
for dataset in train_test_data:
    dataset['transaction_year_month']=dataset['transaction_year_month'].astype(str)
    dataset['year']=dataset['transaction_year_month'].str[:4].astype(int)
    dataset['month']=dataset['transaction_year_month'].str[4:6].astype(int)
    dataset['transaction_year_month']=dataset['transaction_year_month'].astype(int)

년도 별로 그래프를 확인해보자.

f,ax=plt.subplots(figsize=(8,6))
sns.boxplot(train['year'],train['transaction_real_price'])
plt.show()

보면 2016년도에 이상치같이 보이는 것이 하나 생겨있다. 이를 좀더 확인해보자.

train[(train['year']==2016)&(train['transaction_real_price']<150)]

apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	100	2016	6

서면 아파트가 값이 낮다. 

그렇다면 서면 전체에 대해서 한번 확인해보자.

 

train[train['apt']=='서면']
	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
695725	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201105	21~31	3	5900	2011	5
696834	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201108	21~31	1	7500	2011	8
700884	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	88.93	1974	201206	1~10	1	25000	2012	6
702589	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201212	21~31	5	6000	2012	12
702876	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201301	11~20	4	7400	2013	1
703979	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201304	1~10	1	7000	2013	4
711024	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201407	21~31	5	7000	2014	7
712441	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201410	21~31	3	8800	2014	10
713864	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201501	1~10	4	7750	2015	1
721684	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201604	1~10	1	8600	2016	4
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	100	2016	6
723957	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	55.04	1974	201608	21~31	2	23200	2016	8
726597	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201612	11~20	4	6650	2016	12
1186983	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	34.08	1974	201702	11~20	1	16400	2017	2
1189112	6238	부산광역시	전포동	산99-46	서면	전포동 산99-46 서면	57.59	1976	201706	21~30	3	9300	2017	6
1189212	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	54.15	1974	201706	1~10	5	26200	2017	6

서면 아파트 값이 전체적으로 싼것은아니다.. 그럼 뭐때문에 이렇게 값이 떨어진걸까? 확인해보니 floor=-1 인것을 볼수있다.

그렇다면 다른 -1층은 어떨까?

train[train['floor']==-1].sort_values('transaction_real_price')
	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.250	1974	201606	21~30	-1	100	2016	6
651052	1514	부산광역시	대청동4가	75-176	근영빌라2동	대청동4가 75-176 근영빌라2동	47.170	1996	200809	21~30	-1	1300	2008	9
651087	1513	부산광역시	대청동4가	75-181	근영빌라1동	대청동4가 75-181 근영빌라1동	59.890	1996	200810	11~20	-1	2500	2008	10
651641	1514	부산광역시	대청동4가	75-176	근영빌라2동	대청동4가 75-176 근영빌라2동	47.170	1996	201101	21~31	-1	3200	2011	1
674249	9764	부산광역시	동삼동	213-19	조은아크로빌	동삼동 213-19 조은아크로빌	46.800	2001	201506	11~20	-1	3500	2015	6
...	...	...	...	...	...	...	...	...	...	...	...	...	...	...
537384	10310	서울특별시	청담동	102-13	청담파라곤Ⅱ 2단지	청담동 102-13 청담파라곤Ⅱ 2단지	241.880	2010	201012	11~20	-1	290000	2010	12
1095417	11320	서울특별시	한남동	810	한남더힐	한남동 810 한남더힐	212.524	2011	201705	21~31	-1	480000	2017	5
1095416	11320	서울특별시	한남동	810	한남더힐	한남동 810 한남더힐	212.524	2011	201705	21~31	-1	490000	2017	5
23585	11320	서울특별시	한남동	810	한남더힐	한남동 810 한남더힐	240.230	2011	201612	1~10	-1	543000	2016	12
23376	11320	서울특별시	한남동	810	한남더힐	한남동 810 한남더힐	240.230	2011	201610	11~20	-1	575000	2016	10

지하 1층 치고 비싼곳도 있는것이 확인된다. 그러므로 지하1층은 값이 싸다라는 가정도 패스.

 

그러면 transaction_real_price가 100인 위치를 한번 싹다 확인해보자.

train[train['jibun']=='263-5'].sort_values('floor')

 

	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	100	2016	6
700884	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	88.93	1974	201206	1~10	1	25000	2012	6
1186983	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	34.08	1974	201702	11~20	1	16400	2017	2
723957	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	55.04	1974	201608	21~31	2	23200	2016	8
1189212	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	54.15	1974	201706	1~10	5	26200	2017	6

확인해보니 같은 위치의 가격에 비해 price가 100인 값만 현저하게 작다..

이것을 이상치라고 판단하고 넘어가야하는가... 고민된다 통계적 계산을 통해 이상치 판단 근거를 만들 수 있지만 이는 다음에 해보는 것으로하고 우선 넘어가자.

 

그 다음은 월별과 가격의 연관성을 확인해보자.

위 그림을 보면 6월에 이상치같은 것이 있는데 이는 위에서 얘기한 price:100인 집이다.

 

year_of_completion(설립일자)

f,ax=plt.subplots(figsize=(22,6))
sns.boxplot(train['year_of_completion'],train['transaction_real_price'])
plt.show()

다음 설립일자에서... 너무많지만 가장 이상치같이 보이는 점을 확대해서 확인해보자..

1974년도 설립일자

1974년도에 가장 낮은 값이 있었는데 이는 무엇인지 확인해보자.

train[train['year_of_completion']==1974].sort_values('transaction_real_price',ascending=True)
	apartment_id	city	dong	jibun	apt	addr_kr	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month
722888	6225	부산광역시	범전동	263-5	서면	범전동 263-5 서면	138.25	1974	201606	21~30	-1	4.615121	2016	6
350513	12414	서울특별시	화곡동	354-54	화곡동복지	화곡동 354-54 화곡동복지	53.94	1974	201206	21~30	1	7.601402	2012	6
689192	7162	부산광역시	가야동	385-2	시영	가야동 385-2 시영	39.67	1974	201004	21~30	6	7.696667	2010	4
679153	7162	부산광역시	가야동	385-2	시영	가야동 385-2 시영	39.67	1974	200807	11~20	4	7.824446	2008	7
678316	7162	부산광역시	가야동	385-2	시영	가야동 385-2 시영	39.67	1974	200805	11~20	5	7.863651	2008	5
...	...	...	...	...	...	...	...	...	...	...	...	...	...	...
1163472	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201706	11~20	2	12.706851	2017	6
1164677	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201709	11~20	3	12.736704	2017	9
1164022	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201707	21~31	5	12.765691	2017	7
1164377	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201708	1~10	3	12.765691	2017	8
1164370	4695	서울특별시	반포동	757	반포 주공1단지	반포동 757 반포 주공1단지	140.33	1974	201708	1~10	4	12.779876	2017	8

이또한 똑같은 데이터였다.. 

 

변수 카테고리화

city(도시)

우선 서울특별시와 부산광역시를 레이블 0,1로 구분시켜 다시 데이터프레임을 구성하자.

 

replace_name={'서울특별시':0,'부산광역시':1}
train=train.replace({'city':replace_name})
test=test.replace({'city':replace_name})
f,ax=plt.subplots(figsize=(8,6))
sns.boxplot(train['city'],train['transaction_real_price'])
plt.show()

지역별 집값 추세

보니 서울이 부산보다 집값이 높은것을 확인할 수 있다. 거래량도 확인해보자

f,ax=plt.subplots(figsize=(8,6))
sns.countplot(train['city'])
plt.show()

지역별 거래량

이를 통해 집값은 서울이 더 높고, 거래량도 서울이 더 높은것을 확인할 수 있다.

 

addr_kr(주소)

동, 지번, 아파트단지이름을 합친 것을 addr_kr로 하였는데 이는 그냥 삭제해버리자.

train=train.drop('addr_kr',axis=1)
test=test.drop('addr_kr',axis=1)

dong(동)

데이콘 튜토리얼 샘플코드에서는 한강의 유무에 따른 feature를 하나 생성해준다. 그러므로 본인도 만들어놓아본다.

train['hangang']=train['dong'].isin(['성수동1가','삼성동','이촌동','공덕동','서교동','한강로3가','목동']).astype(int)
test['hangang']=train['dong'].isin(['성수동1가','삼성동','이촌동','공덕동','서교동','한강로3가','목동']).astype(int)
len(train['dong'].unique())

473

dong은 그 지역을 그룹핑한 것이다. 그러므로 지역에 따라 가격차이를 확인해 줄 수 있다.

아파트 거래가격의 평균순으로 데이터를 레이블링해보자.

 

train_dong=train[['transaction_real_price','dong']].groupby('dong').mean().sort_values('transaction_real_price').reset_index()
train_dong.head()
dong	transaction_real_price
0	신선동3가	5500.000000
1	장안읍 명례리	5508.333333
2	신창동2가	5531.250000
3	봉래동5가	5732.546012
4	중앙동4가	6312.500000
dong_num={}
for i in range(len(train_dong)):
    dong=train_dong['dong'].iloc[i]
    dong_num[dong]=i
dong_num

{'신선동3가': 0,
 '장안읍 명례리': 1,
 '신창동2가': 2,
 '봉래동5가': 3,
 '중앙동4가': 4,
 '장충동2가': 5,
 '신선동2가': 6,
 '부평동2가': 7,
 '필동1가': 8,
 '아미동2가': 9,
 '초장동': 10,
 '수정동': 11,
 '기장읍 대변리': 12,
 '동광동5가': 13,
 '보수동1가': 14,
 '남항동3가': 15,
 '대청동1가': 16,
 '누상동': 17,
 '반송동': 18,
 '기장읍 서부리': 19,
 '동삼동': 20,
 '영등포동3가': 21,
 '일광면 이천리': 22,
 '봉래동4가': 23,
 '서동': 24,
 '일광면 삼성리': 25,
 '대청동4가': 26,
 '영등포동2가': 27,
 '덕천동': 28,
 '모라동': 29,
 '감만동': 30,
 '미근동': 31,
 '장림동': 32,
 '학장동': 33,
 '동대신동3가': 34,
 '구평동': 35,
 '보수동2가': 36,
 ...
 '대치동': 464,
 '서빙고동': 465,
 '한남동': 466,
 '반포동': 467,
 '회현동2가': 468,
 '용산동5가': 469,
 '청암동': 470,
 '압구정동': 471,
 '장충동1가': 472}
train=train.replace({'dong': dong_num})
test=test.replace({'dong':dong_num})
train.head()
apartment_id	city	dong	jibun	apt	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month	hangang
0	7622	0	335	6-13	신현(101동)	84.82	2002	200801	21~31	2	37500	2008	1	0
1	5399	0	408	142	사직파크맨션	99.17	1973	200801	1~10	6	20000	2008	1	0
2	3578	0	408	174-1	두레엘리시안	84.74	2007	200801	1~10	6	38500	2008	1	0
3	10957	0	459	95	파크팰리스	146.39	2003	200801	11~20	15	118000	2008	1	0
4	10639	0	459	110-15	킹스매너	194.43	2004	200801	21~31	3	120000	2008	1	0

Jibun(지번)

지번의 unique_value를 확인해보자.

len(train['jibun'].unique())

8961
train_jibun=train[['transaction_real_price','jibun']].groupby('jibun').mean().sort_values('transaction_real_price').reset_index()
train_jibun.head()
	jibun	transaction_real_price
0	산3-148	1428.263889
1	737-1	1852.413793
2	741-1	1855.584416
3	11-174	2000.000000
4	1181-4	2043.055556

apt(아파트)

아파트의 unique_value를 확인해보자

len(train['apt'].unique())

10440
train_apt=train[['transaction_real_price','apt']].groupby('apt').mean().sort_values('transaction_real_price').reset_index()
train_apt.head()
	apt	transaction_real_price
0	좌천시민(737-1)	1852.413793
1	좌천시민(741-1)	1855.584416
2	수정(1181-4)	2043.055556
3	수정(1175-1)	2111.785714
4	수정(1186-1)	2292.333333

확인해보니 지번과 아파트 데이터는 unique_value가 너무 많다.. 따로 레이블링은 하지말자..

 

transaction_date(거래 기간)

위 데이터는 처음 거래시작한 day와 거래가 끝난 day의 차이를 칼럼으로 변경시켜주자.

train['day_diff']=train['transaction_date'].str.extract('(~\d+)')[0].str[1:].astype(int)-train['transaction_date'].str.extract('(\d+~)')[0].str[:-1].astype(int)
test['day_diff']=test['transaction_date'].str.extract('(~\d+)')[0].str[1:].astype(int)-test['transaction_date'].str.extract('(\d+~)')[0].str[:-1].astype(int)

이것에 대해 unique_value를 확인해보자.

len(train['transaction_date'].unique())

6

거래기간은 unique_value가 6개이므로 이는 레이블링 처리를 해주자.

 

train_date=train[['transaction_real_price','transaction_date']].groupby('transaction_date').mean().sort_values('transaction_real_price').reset_index()
train_date.head()
transaction_date	transaction_real_price
0	21~29	33055.585346
1	21~28	37182.344295
2	1~10	37732.015054
3	11~20	38217.678678
4	21~31	38879.439698
date_num={}
for i in range(len(train_date)):
    date=train_date['transaction_date'].iloc[i]
    date_num[date]=i
date_num

{'21~29': 0, '21~28': 1, '1~10': 2, '11~20': 3, '21~31': 4, '21~30': 5}
train=train.replace({'transaction_date':date_num})
test=test.replace({'transaction_date':date_num})
train.head()
apartment_id	city	dong	jibun	apt	exclusive_use_area	year_of_completion	transaction_year_month	transaction_date	floor	transaction_real_price	year	month	hangang	day_diff
0	7622	0	335	6-13	신현(101동)	84.82	2002	200801	4	2	37500	2008	1	0	10
1	5399	0	408	142	사직파크맨션	99.17	1973	200801	2	6	20000	2008	1	0	9
2	3578	0	408	174-1	두레엘리시안	84.74	2007	200801	2	6	38500	2008	1	0	9
3	10957	0	459	95	파크팰리스	146.39	2003	200801	3	15	118000	2008	1	0	9
4	10639	0	459	110-15	킹스매너	194.43	2004	200801	4	3	120000	2008	1	0	10

전처리

floor 같은 경우 음수값이 존재하기 때문에 log를 취하기 전에 각 값에 +5를 해주어 모든 값을 양수로 만들어주자.

train['floor']=np.log(train['floor']+5)
test['floor']=np.log(test['floor']+5)
f,ax=plt.subplots(figsize=(8,6))
sns.distplot(train['floor'])
print("%s->Skewness: %f, Kurtosis: %f"%('floor',train['floor'].skew(),train['floor'].kurt()))

floor->Skewness: 0.089636, Kurtosis: -0.640885

이제 아파트와 지번 그리고 거래년월을 없애버리자.

drop_columns=['apt','jibun','transaction_year_month']

train=train.drop(drop_columns,axis=1)
test=test.drop(drop_columns,axis=1)
train.head()
	apartment_id	city	dong	exclusive_use_area	year_of_completion	transaction_date	floor	transaction_real_price	year	month	hangang	day_diff
0	7622	0	335	84.82	2002	4	1.945910	37500	2008	1	0	10
1	5399	0	408	99.17	1973	2	2.397895	20000	2008	1	0	9
2	3578	0	408	84.74	2007	2	2.397895	38500	2008	1	0	9
3	10957	0	459	146.39	2003	3	2.995732	118000	2008	1	0	9
4	10639	0	459	194.43	2004	4	2.079442	120000	2008	1	0	10

Feature Engineering

train_test_data=[train,test]

for dataset in train_test_data:
    dataset['age']=dataset['year']-dataset['year_of_completion']
    dataset['is_rebuild']=(dataset['age']>=30).astype(int)
    

거래하는 기간까지의 아파트 나이를 특징으로 생성해주고, 샘플코드에 있는 아파트의 재건축 유무를 판단하는 특징을 만들어준다.

train_columns=[]
for column in train.columns[:]:
    if train[column].skew()>=1:
        print('%s -> Skewness: %f, Kurtosis: %f'%(column,train[column].skew(),train[column].kurt()))
        train_columns.append(column)
    elif train[column].kurt()>=3:
        print('%s -->Skewness: %f, Kurtosis: %f'%(column,train[column].skew(),train[column].kurt()))
        train_columns.append(column)
        
exclusive_use_area -> Skewness: 1.227509, Kurtosis: 3.100517
transaction_real_price -> Skewness: 3.407169, Kurtosis: 24.839821
hangang -> Skewness: 6.358349, Kurtosis: 38.428662
day_diff -->Skewness: -0.589751, Kurtosis: 4.573129
is_rebuild -> Skewness: 3.324832, Kurtosis: 9.054522

정규분포 근사화를 위해 첨도와 왜도를 조정해준다.

for column in train_columns :
    train[column] = np.log1p(train[column])
    test[column] = np.log1p(test[column])
    print("%s -> Skewness: %f, Kurtosis: %f" %  (column,train[column].skew(), 
                                                 train[column].kurt()))
                                                 
                                                 

exclusive_use_area -> Skewness: -0.438156, Kurtosis: 1.744119
hangang -> Skewness: 6.358349, Kurtosis: 38.428662
day_diff -> Skewness: -1.128137, Kurtosis: 6.788569
is_rebuild -> Skewness: 3.324832, Kurtosis: 9.054522

이제 상관관계를 다시 확인해보자.

#상관관계 확인
k=train.shape[1] #히트맵 변수 갯수
corrmat = train.corr() #변수간의 상관관계
cols = corrmat.nlargest(k, 'transaction_real_price')['transaction_real_price'].index #price기준으로 제일 큰순서대로 20개를 뽑아냄
cm = np.corrcoef(train[cols].values.T)
f, ax = plt.subplots(figsize=(20, 6))
sns.heatmap(data = cm, annot=True, square=True, fmt = '.2f', linewidths=.5, cmap='Reds', 
            yticklabels = cols.values, xticklabels = cols.values)

 

모델링

from sklearn.linear_model import ElasticNet, Lasso
from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
from sklearn.kernel_ridge import KernelRidge
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import RobustScaler
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.metrics import mean_squared_error
import xgboost as xgb
import lightgbm as lgb
target = train['transaction_real_price']
del train['transaction_real_price']
#cross validation score
n_folds = 2

def cv_score(models):
    kfold = KFold(n_splits=n_folds, shuffle=True ,random_state=42).get_n_splits(train.values)
    for m in models:
        cvs = np.mean(cross_val_score(m['model'], train.values, target, cv=kfold))
        rmse = np.mean(np.sqrt(-cross_val_score(m['model'], train.values, np.expm1(target), scoring = "neg_mean_squared_error", cv = kfold)))
        print("Model {} CV score : {:.4f}".format(m['name'], cvs))
        print("RMSE : {:.4f}".format(rmse))
lasso = make_pipeline(RobustScaler(), Lasso(alpha = 0.0005, random_state=42))
ENet = make_pipeline(RobustScaler(), ElasticNet(alpha=0.0005, l1_ratio=.9, random_state=42))
gboost = GradientBoostingRegressor(random_state=42)
forest = RandomForestRegressor(n_estimators = 100, n_jobs = -1, random_state=42)
xgboost = xgb.XGBRegressor(random_state=42)
lightgbm = lgb.LGBMRegressor(random_state=42, num_leaves = 100, min_data_in_leaf = 15, max_depth=6,
                            learning_rate = 0.1, min_child_samples = 30, feature_fraction=0.9, bagging_freq= 1,
                            bagging_fraction = 0.9, bagging_seed = 11, lambda_l1 = 0.1, verbosity = -1 )

models = [{'model': gboost, 'name':'GradientBoosting'}, {'model': xgboost, 'name':'XGBoost'},
          {'model': lightgbm, 'name':'LightGBM'}, {'model' : lasso, 'name' : 'LASSO Regression'}, 
          {'model' : ENet, 'name' : 'Elastic Net Regression'}, {'model' : forest, 'name' : 'RandomForset'}]
start = timeit.default_timer()
cv_score(models)
stop = timeit.default_timer()
print('불러오는데 걸린 시간 : {}초'.format(stop - start))
Model GradientBoosting CV score : 0.5706
RMSE : 0.0108
Model XGBoost CV score : 0.5788
RMSE : 0.0108
Model LightGBM CV score : 0.5681
RMSE : 0.0108
Model LASSO Regression CV score : 0.5526
RMSE : 0.0107
Model Elastic Net Regression CV score : 0.5602
RMSE : 0.0106
Model RandomForset CV score : 0.4911
RMSE : 0.0118
불러오는데 걸린 시간 : 910.3417148000003초
#여러개의 모델로 만들어진 predict 데이터들의 평균을 구한다.

models = [{'model':xgboost, 'name':'XGBoost'},
          {'model':lightgbm, 'name':'LightGBM'},
         {'model':forest, 'name' : 'RandomForest'}]

def AveragingBlending(models, x, y, sub_x):
    for m in models : 
        m['model'].fit(x.values, y)
    
    predictions = np.column_stack([m['model'].predict(sub_x.values) for m in models])
    return predictions
start = timeit.default_timer()

y_test_pred = AveragingBlending(models, train, target, test)
y_test_pred = (y_test_pred[:, 0]*0.05 + y_test_pred[:, 1]*0.1 + y_test_pred[:, 2]*0.85)
predictions = y_test_pred

stop = timeit.default_timer()
print('불러오는데 걸린 시간 : {}초'.format(stop - start))
[LightGBM] [Warning] feature_fraction is set=0.9, colsample_bytree=1.0 will be ignored. Current value: feature_fraction=0.9
[LightGBM] [Warning] min_data_in_leaf is set=15, min_child_samples=30 will be ignored. Current value: min_data_in_leaf=15
[LightGBM] [Warning] lambda_l1 is set=0.1, reg_alpha=0.0 will be ignored. Current value: lambda_l1=0.1
[LightGBM] [Warning] bagging_fraction is set=0.9, subsample=1.0 will be ignored. Current value: bagging_fraction=0.9
[LightGBM] [Warning] bagging_freq is set=1, subsample_freq=0 will be ignored. Current value: bagging_freq=1
불러오는데 걸린 시간 : 260.0541379999995초
sub = pd.read_csv('d:/data/real_estate/submission.csv')
sub['transaction_real_price'] = np.expm1(predictions)
sub.to_csv('d:/data/real_estate/submission.csv', index=False)

 

 

후방이동(backshift)

후방이동(backshift) 연산자 B는 시계열 시차를 다룰 때 유용한 표기법 장치이다.

(참고 문헌에서는 '후방이동(backshift)'을 나타내는 B 대신에 '시차(lag)'을 나타내는 L을 사용한다.)

다르게 말하면, y(t)에 작용하는 B는 데이터를 한 시점 뒤로 옮기는 효과를 낸다. B를 y(t)에 두 번 적용하면 데이터를 두 시점 뒤로 옮긴다.

월별 데이터에서, '지난해와 같은 달' 을 다루고 싶다면, 다음과 같이 표기한다.

후방이동(backshift)연산자는 차분을 구하는 과정을 설명할 때 편리하다. 1차 차분을 다음과 같이 쓸 수 있다.

1차 차분을 (1-B)로 나타냈다는 것에 주목해보자. 비슷하게, 2차 차분을 계산해야하면, 이 때는 아래와 같이 주어진다.

일반적으로, d차 차분은 다음과 같이 쓸 수 있다.

차분을 연산자로 결합하면 보통의 대수 법칙을 사용하여 다룰 수 있게 되기 때문에, 후방이동(backshift) 기호는 특별히 유용하다. B를 포함하는 항은 서로 곱할 수 있다.

 

예를 들면, 1차 차분 뒤에 이어서 나오는 계절성 차분은 다음과 같이 쓸 수 있다.

 

이는 이전에 얻은 결과와 같다.

ARIMA 모형은 대표적인 통계적 시계열 예측 모형으로, 현재 값을 과거 값과 과거 예측 오차를 통해 설명한다. 이 모형을 적합하려면 시계열 Yt가 정상성 조건을 만족하는 정상 시계열(Stationary series)이어야 한다. (정확하게는 약정상성, weak stationary). 또한, 주어진 시계열 자료에 적합한 ARIMA(p,d,q)를 결정하는 절차는 Box-Jenkins method를 따르며, ACF(Autocorrelation function: 자기상관함수)로 시계열의 특성을 파악하고 적절한 차수의 ARIMA 모형을 선택한다.

 

Box-Jenkins method

 

 

정상성(Stationary)

우리는 시간의 순서에 따라 기록되지 않은 일반적 자료들을 분석할 때, Random sample들에 iid가정을 한다. 

*iid: All samples are independent and identically distributed

시간에 종속되어있는 시게열은 상식적으로 iid 가정을 할 수 없다. 그래서 이러한 시계열 자료에 대해 예측 모형을 적합하고 통계적 검정을 하기 위해서는 분석을 단순화 시킬 수 있는 새로운 가정이 필요하다. 

이중 가장 중요한 것이 시계열 모형의 확률적 성질이 시간에 따라 변하지 않는다고 가정하는 정상성 가정이다.

 

ARIMA모형은 해당 시계열이 약정상성(Weak Stationary)를 만족한다고 가정하며, 약정상성을 만족해야 좋은 fitting과 predict 성능을 보여줄 수 있다. 시계열 Yt가 다음의 세 조건을 만족할 때 약정상성을 가진다고 표현한다. 시계열 분서겡서 말하는 정상 시계열(Stationary series)은 약 정상성을 가지는 시계열을 말한다.

-평균이 모든 시점 t에서 동일하다 : 추세, 계절성, 순환성 등의 패턴이 보이지 않게 된다.

-분산이 모든 시점 t에서 동일하다: 자료 변화의 폭이 일정하게 된다.

-Yt와 Y(t-h) 간의 Covariation(즉, Yt의 자기 공분산 함수)이 모든 시점 t에 대해서 동일하다: 시간에 따라 상이한 자기상관적 패턴을 보이지 않게 된다.

 

즉, 풀어설명하자면 추세와 계절성이 있는 시계열은 정상성을 나타내는 시계열이 아니다. 일반적으로 정상성을 나타내는 시계열은 장기적으로 볼 때 예측할 수 있는 패턴을 나타내지 않을 것이다. (어떤 주기적인 행동이 있을 수 있더라도) 시간 그래프는 시계열이 일정한 분산을 갖고 대략적으로 평평하게 될 것을 나타낼 것이다.

그렇다면 다음 시계열 그래프중 어떤 것이 정상성을 나타낼까? 

a) 거래일 200일 동안의 구글 주식 가격

b) 거래일 200일 동안의 구글 주식 가격의 일일 변동

c) 미국의 연간 파업 수

d) 미국에서 판매되는 새로운 단독 주택의 월별 판매액

e) 미국에서 계란 12개의 연간 가격(고정 달러)

f) 호주 빅토리아 주에서 매월 도살한 돼지의 전체 수

g) 캐나다 북서부 맥킨지 강 지역에서 연간 포획된 시라소니의 전체 수

h) 호주 월별 맥주 생산량

i) 호주 월별 전기 생산량

 

분명하게 계절성이 보이는 d),h),i)는 후보가 되지 못한다. 추세가 있고 수준이 변하는 a),c),e),f),i)도 후보가 되지 못한다. 분산이 증가하는 i)도 후보가 되지 못한다. 그렇다면 b)와 g)만 정상성을 나타내는 시계열 후보로 남았다.

얼핏 보면 시계열 g)에서 나타나는 뚜렷한 주기(cycle)때문에 정상성을 나타내는 시계열이 아닌 것처럼 보일 수 있다. 하지만 이러한 주기는 불규칙적(aperiodic)이다. 

즉, 먹이를 구하기 힘들만큼 시라소니 개체수가 너무 많이 늘어나 번식을 멈춰서, 개체수가 작은 숫자로 줄어들고, 그 다음 먹이를 구할 수 있게 되어 개체수가 다시 늘어나는 식이다. 

장기적으로 볼 때 , 이러한 주기의 시작이나 끝은 예측할 수 없다. 따라서 이 시계열은 정상성을 나타내느 시계열이다.

 

차분(differencing)

구글 주식 가격의 ACF(왼쪽), 일별 변동(오른쪽)

처음 그림에서 패널(a) 의 구글 주식 가격이 정상성을 나타내는 시계열이 아니었지만 (b)의 일별 변화는 정상성을 나타냈다느 것에 주목해보자. 이 그림은 정상성을 나타내지 않는 시계열을 정상성을 나타내도록 만드는 한 가지 방법을 나타낸다.

연이은 관측값들의 차이를 계산하는 것이다. 이를 차분(differencing)이라 부른다.

 

로그변환은 시계열의 분산 변화를 일정하게 만드는데 도움이 될 수 있다. 차분(differencing)은 시계열의 수준에서 나타내느 변화를 제거하여 시계열의 평균 변화를 일정하게 만드는데 도움이 될 수 있다. 결과적으로 추세나 계절성이 제거(또는 감소) 된다.

 

정상성을 나타내지 않는 시계열을 찾아낼 때 데이터의 시간 그래프를 살펴보는 것만큼, ACF 그래프도 유용하다. 정상성을 나타내지 않는 데이터에서는 ACF가 느리게 감소하지만, 정상성을 나타내는 시계열에서는, ACF가 비교적 빠르게 0으로 떨어질 것이다. 그리고 정상성을 나타내지 않는 데이터에서 r1은 종종 큰 양수 값을 갖는다.

 

차분을 구한 구글 주식 가격의 ACF는 단순히 white noise 시계열처럼 생겼다. 95% limitation 바깥에 자기상관(autocorrelation)값이 없고, 융-박스(Ljung-Box) Q* 통계는 h=10 에 대해 0.355라는 p-value를 갖는다. 이 결과는 구글 주식 가격의 일별

Box.test(diff(goog200), lag=10, type="LJung-Box")

#Box-Ljung test

#data: diff(goog200)
#X-Squared=11, df=10, p-value=0.4

변동이 기본적으로는 이전 거래일의 데이터와 상관이 없는 무작위적인 양이라는 것을 말해준다.

 

확률보행 모델

차분(difference)을 구한 시게열은 원래의 시계열에서 연이은 관측값의 차이이고, 다음과 같이 쓸 수 있다.

첫 번째 관측값에 대한 차분 y'1을 계산할 수 없기 때문에 차분을 구한 시계열은 T-1개의 값만 가질 것이다.

차분을 구한 시계열이 white noise이면, 원래 시계열에 대한 모델은 다음과 같이 쓸 수 있다.

여기서 Error(t)는 white noise를 의미한다. 이것을 정리하면 '확률보행(random walk)' 모델을 얻는다.

확률보행(random walk) 모델은 정상성을 나타내지 않은 데이터, 특별히 금융이나 경제 데이터를 다룰 때 널리 사용되고 있다. 확률보행에는 보통 다음과 같은 특징을 가진다.

 

-누가 봐도 알 수 있는 긴 주기를 갖는 상향 또는 하향 추세가 있다.

-갑작스럽고 예측할 수 없는 방향 변화가 있다.

 

미래 이동을 예측할 수 없고 위로 갈 확률이나 아래로 갈 확률이 정확하게 같기 때문에 확률보행 모델에서 얻어낸 예측값은 마지막 관측값과 같다. 따라서, 확률보행 모델은 단순(naive) 예측값을 뒷받침한다.

 

밀접하게 연관된 모델은 차분값이 0이 아닌 평균값을 갖게 한다. 그러면,

c값은 연이은 관측값의 차이의 평균이다. c가 양수이면, 평균 변화는 y(t)값에 따라 증가한다. 따라서 y(t)는 위쪽 방향으로 이동하는 경향을 나타낼 것이다. 하지만 c가 음수이면, y(t)는 아래쪽 방향으로 이동하는 경향을 나타낼 것이다.

 

2차 차분

가끔 차분(difference)을 구한 데이터가 정상성(stationarity)이 없다고 보일 수도 있다. 정상성을 나타내는 시계열을 얻기 위해 데이터에서 다음과 같이 한 번 더 차분을 구하는 작업이 필요할 수 있다.

이 경우에는, y(t)''는 T-2개의 값을 가질 것이다. 그러면, 원본 데이터의 '변화에서 나타나는 변화'를 모델링하게 되는 셈이다. 실제 상황에서는, 2차 차분 이상으로 구해야 하는 경우는 거의 일어나지 않는다.

 

계절성 차분

계절성 차분(seasonal differencing)은 관측치와, 같은 계절의 이전 관측값과의 차이를 말한다. 따라서

여기에서 m은 계절의 개수이다. m 주기 시차 뒤의 관측을 빼기 때문에 시차 m 차분이라고 부르기도 한다.

계절성으로 차분을 구한 데이터가 white nois로 보이면, 원본 데이터에 대해 적절한 모델은 다음과 같다.

이 모델에서 낸 예측값은 관련 있느 계절의 마지막 관측값과 같다. 즉, 이 모델은 계절성 단순(seasonal naive) 예측값을 나타낸다.

 

위 그림의 아래쪽 패널은 호주에서 팔린 A10 약물(당뇨병 약)의 월별 처방전의 수에 로그를 취하여 계절성 차분을 구한 결과를 나타낸다. 변환과 차분을 통해 시계열이 정상성을 나타내는 것처럼 보인다.

cbind("판매량 (백만 달러)"= a10, 
	  "월별 로그 판매량"= log(a10), 
      "로그 눈금에서 연간 변동"=diff(log(a10),12))%>%
  autoplot(facets=TRUE)+
  	xlab("연도")+ylab("")+
    ggtitle("당뇨병 약 판매량")

보통의 차분과 계절성 차분을 구분하기 위해, 때때로 보통의 차분을 시차 1에서 차분을 구한다는 의미로 "1차 차분(first difference)"라고 부른다. 

위 그림에서 나타낸 것처럼, 계절성을 나타내는 데이터를 얻기 위해 계절성 차분과 1차 차분 둘 다 구하는 것이 필요하기도 한다. 여기서는 데이터를 먼저 로그로 변환하고(두번째 패널), 그 후 계절성 차분을 계산했다(세번째 패널). 데이터에서 여전히 정상성이 보이지 않는 것 같아서, 1차 차분을 더 많이 계산했다.

cbind("10억 kWh" = usmelec,
      "로그" = log(usmelec),
      "계절성\n 차분 로그값" =
        diff(log(usmelec),12),
      "두 번\n 차분을 구한 로그값" =
        diff(diff(log(usmelec),12),1)) %>%
  autoplot(facets=TRUE) +
    xlab("연도") + ylab("") +
    ggtitle("미국 월별 순 전기 생산량")

 

어떤 차분(difference)을 구할지 정할 때는 주관적인 요소가 어느정도 들어간다. 위 약물 그림의 계절성 차분(seasonal difference)을 구한 데이터는 월별 순 전기 생산량의 계절성 차분을 구한 데이터와는 큰 차이를 나타내지 않는다. 후자의 경우, 계절성 차분을 구한 데이터로 결정했어야 했고, 차분을 더 구하지 않아도 되었다. 이에 반면해 전자의 경우에는 데이터의 정상성(stationarity)이 충분히 나타나지 않아서 차분을 더 구해야 했다. 차분을 구하는 것에 대한 몇가지 형식적인 검정을 아래에서 다루지만, 모델링 과정에서 항상 몇 가지 선택이 존재하고, 분석하는 사람마다 다른 선택을 할 수 있다.

y'(t)=y(t)-y(t-m)가 계절성 차분(seasonal difference)을 구한 시계열을 나타낸다면, 두 번 차분한 시계열은 다음과 같다.

계절성 차분과 1차 차분을 둘 다 적용할 때, 어떤 것을 먼저 적용하더라도 차이는 없다. 결과는 같을 것이다. 하지만, 데이터에 계절성 패턴이 강하게 나타나면, 계절성 차분을 먼저 계산하는 것을 추천한다. 왜냐면, 때때로 결과 시계열에서 정상성이 나타나기도 해서 이런 경우 1차 차분을 구할 필요가 없게 되기 때문이다. 1차 차분을 먼저 계산했다면, 여전히 남아있는 계절성이 나타날 것이다.

 

차분을 구했다면, 차분 값이 해석 가능할 것이라는 것은 중요하다. 첫 번째 차분값은 한 관측값과 그 다음 관측값 사이의 변화이다. 계절성 차분값은 한 해와 그 다음 해 사이의 변화이다. 다른 시차값(lagged value)은 직관적으로 해석하기가 쉽지 않기 때문에 사용하지 않는 것이 좋다.

 

단위근검정

단위근검정(unit root tests)은 더 객관적으로 차분을 구하는 것이 필요할 지 결정하기 위해 사용하는 한 가지 방법이다. 차분을 구하는 것이 필요한지 결정하는 상황을 위해 설계된 통계적 가설 검정들이 존재한다. 사용할 수 있는 단위근검정은 다양하고, 서로 다른 가정에 기초하고 있으며, 상반되는 답을 낼 수도 있다. 분석 과정에서 KPSS(Kwiatkowski-Phillips-Schmidt-Shin)검정을 사용한다. 이 검정은 데이터에 정상성이 나타난다는 것이 귀무가설이고, 귀무 가설이 거짓이라는 증거를 찾으려고 한다. 결과적으로 ,작은 p-value는 차이를 구하는 것이 필요하다는 것을 나타낸다. 검정은 다음과 같다.

예로는 구글 주식 가격 데이터에 적용해보자.

 

library(urca)
goog %>% ur.kpss() %>% summary()
#> 
#> ####################### 
#> # KPSS Unit Root Test # 
#> ####################### 
#> 
#> Test is of type: mu with 7 lags. 
#> 
#> Value of test-statistic is: 10.72 
#> 
#> Critical value for a significance level of: 
#>                 10pct  5pct 2.5pct  1pct
#> critical values 0.347 0.463  0.574 0.739

검정 통계량은 1% 임계값보다 훨씬 크다. 이것은 귀무가설이 기각 된다는 것을 의미한다. 즉, 데이터가 정상성을 가지고 있지 않다. 데이터에 차분을 수행할 수 있고 검정을 다시 적용할 수 있다.

 

goog %>% diff() %>% ur.kpss() %>% summary()
#> 
#> ####################### 
#> # KPSS Unit Root Test # 
#> ####################### 
#> 
#> Test is of type: mu with 7 lags. 
#> 
#> Value of test-statistic is: 0.0324 
#> 
#> Critical value for a significance level of: 
#>                 10pct  5pct 2.5pct  1pct
#> critical values 0.347 0.463  0.574 0.739

이번에는 검정 통계가 작고(0.0324), 정상성이 나타나는 데이터에서 볼 수 있는 것처럼 범위 안에 잘 들어간다. 따라서 차분을 구한 데이터가 정상성을 나타낸다고 결론내릴 수 있다.

 

1차 차분의 적당한 횟수를 결정하기 위해 여러번의 KPSS 검정을 사용하는 이 과정을 ndiffs()로 수행할 수 있다.

 

ndiffs(goog)
#> [1] 1

위 검정들에서 본 것처럼, google데이터가 정상성을 나타내도록 하려면 한번의 차분(difference)이 필요하다.

계절성 차분(seasonal difference)이 필요한지 결정하기 위한 비슷한 함수는 nsdiffs()이다. 이 함수는 필요한 계절성 차분의 적당한 횟수를 결정하기 위해 계절성 강도 측정량을 사용한다. Fs<0.64이면, 계절성 차분이 필요 없다고 알려주고, 이외의 경우에는 하나의 계절성 차분이 필요하다 알려준다.

 

 

+ Recent posts