본문 바로가기

AI

시계열 데이터로 MLP, CNN, LSTM 돌려보기 - 한국부동산원 아파트 월간 매매가격지수 활용

 

시계열 데이터를 활용하여 MLP, CNN, LSTM을 돌리는 것을 실습한 내용이다.

해당 내용의 접근법은 Kaggle의 상품 판매량 예측한 내용의 블로그를 참고하여 다른 데이터에 적용한 case이다.

 

참고 글 : https://leedakyeong.tistory.com/entry/Deep-Learning-for-Time-Series-Forecasting-kaggle

 

Deep Learning for Time Series Forecasting (kaggle 코드 리뷰)

2021.05.24 - [통계 지식/시계열자료 분석] - 시계열 분해란?(Time Series Decomposition) :: 시계열 분석이란? 시계열 데이터란? 추세(Trend), 순환(Cycle), 계절성(Seasonal), 불규칙 요소(Random, Residual) 시계열 분해

leedakyeong.tistory.com

 

 

 


- 데이터

한국부동산원(https://www.reb.or.kr/r-one/main.do)에서

아파트 월간 매매가격지수를 활용하였다.

 

 

기간 2003년11월~2022년11월 까지의 데이터를 다운받았으며,

지역별로 있으나 우선 1차로

'전국' 대상의 매매가격지수만을 input으로 활용하였다.

 


- 개발환경

어디서나 접속 가능하도록 클라우드 환경인, colab환경을 사용하였다.

 

 

 


- 구현

 

1. Read Data

필요한 모듈을 임포트 하고, 데이터를 읽어들인다.

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

from google.colab import drive
drive.mount('/content/drive')

filename = '/content/drive/My Drive/Colab Notebooks/월간_매매가격지수_아파트_0311-2211.xlsx'
data = pd.read_excel(filename,index_col = 0)

 

 

 

2. 전처리 (데이터 정제)

read한 값을 보니, 빈 열들이 있으며 column이름을 맞춰줘야 한다. 

9번째 행의 '지역'의 value를 column으로 바꿔주어야 한다.

 

 

또한 전국과 서울 지수를 뽑는다.

전국만 뽑아도 되지만 일단 추후 확장성을 위해 같이 뽑아놓았다.

# column 이름 변경하기
new_column_names = data.iloc[9].values

for i, col in enumerate(data.columns):
    data = data.rename(columns={col: new_column_names[i]})
    
    
data_All = data.loc[['전국','서울']] #전국, 서울 지수만 뽑기

 

 

잘 뽑힌 것을 확인 할 수 있다.

 

다만 column 1~3번째가 NaN값이므로 삭제해주고, column이름을 datetime형태로 변경한다.

그후 보기 편하려고 transpose하였다.

data_All.dropna(axis=1,inplace = True) #NaN인 해당 열 삭제

# column이름을 datetime형태로 변경
data_All.columns = pd.to_datetime(data_All.columns, format='%Y년 %m월')

# transpose함.
data_All = data_All.T

 

그 후, 데이터 확인.

 

 

데이터 (매매가격지수) 중간에 float타입이 아니라 문자열로 되어 있는 값이 있었다. 따라서 수정.

# read한 데이터들이 문자열, float타입이 섞여있는 경우가 있다.
# 추후에 float타입이 아닐 시 model을 돌리는데 에러가 나므로 float으로 변경해준다. 
data_All = data_All[:].astype(float)

 

 

 

처음에 서울까지 같이 뽑긴했지만,  

전국만 다시 뽑아서 해보자.

data_All = pd.DataFrame(data_All['전국'])

data_All['날짜'] = data_All.index #날짜 기록용으로 날짜 컬럼을 추가함.

 

 

 

 

매매지수 예측을 위해 과거 window size만큼의 데이터를 활용하여 예측하려 한다.

window size는 6으로 과거 6개월 데이터를 활용할 것이며

lag를 6으로 설정하여 해당 6개월치의 데이터를 가지고 6개월 후의 데이터를 예측한다.

 

예를들면 2023년 1월 매매가격지수를 예측하기 위해 2022년 1월~6월의 6개월치 데이터를 활용한다.

즉 이 데이터들이 설명변수 x가 된다. 그리고 6개월 뒤의 데이터가 반응변수 y가 된다.

 

 

shift를 1~6개월치 시킨 것들을 나중에 NaN이 있는 것을 기준으로 잘라서 사용하는 아래 코드를 이용하여 해당 구조를 얻을 수 있다.

cols, names = [], []
data_All

lag = 6
lag_size = lag
window = 6
dropnan=True

for i in range(window,0,-1):
    # Input sequence
    cols.append(data_All.shift(i))
    names += [('%s(t-%d)' % (col,i)) for col in data_All.columns]#전국(t-6), 전국(t-5), ...

# Current timestep (t=0)
cols.append(data_All)
names += [('%s(t)' % (col)) for col in data_All.columns]#전국(t)

# Target timestep (t=lag)
cols.append(data_All.shift(-lag))
names += [('%s(t+%d)' % (col, lag)) for col in data_All.columns]#전국(t+6)

# Put it all together
agg = pd.concat(cols, axis=1)
agg.columns = names

# Drop rows with NaN values
if dropnan:
    agg.dropna(inplace=True)

 

 

이렇게 column 이름에 '전국' 들어간 것과 '날짜' 들어간 것을 분리하였다.

series = agg

series_date = series[series.columns[series.columns.str.contains('날짜')]]
series = series[series.columns[series.columns.str.contains('전국')]]

 

데이터 확인.

 

 

 

 

3. Train/Validation Split

 

0.4로 training dataset과 validation dataset을 split하였기에

6:4로 분리한 것이며,

최종적으로 개수는 123 rows와 82 rows로 분리되었다.

 

위에는 앞으로 쓰게 될 모델 관련 module들을 import해놓았다. 

import warnings

from tensorflow.keras import optimizers
from tensorflow.keras.utils import plot_model
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Conv1D, MaxPooling1D
from tensorflow.keras.layers import Dense, LSTM, RepeatVector, TimeDistributed, Flatten
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
import plotly.graph_objs as go
from plotly.offline import init_notebook_mode, iplot

%matplotlib inline
warnings.filterwarnings("ignore")
init_notebook_mode(connected=True)

# Set seeds to make the experiment more reproducibles.
# from tensorflow import set_random_seed
from numpy.random import seed

# Label
labels_col = '전국(t+%d)' % lag_size # 전국(t+6)
labels = series[labels_col]
series = series.drop(labels_col, axis=1)

X_train, X_valid, Y_train, Y_valid = train_test_split(series, labels.values, test_size=0.4, random_state=0)

 

 

 

4. Model 구현

 

 

4.1 MLP

 

Multi-Layer Perceptron을 활용한다.

add를 통해 layer를 추가하였다.

epochs = 40
batch = 100
lr = 0.0003 # Learning Rate
adam = optimizers.Adam(lr)

model_mlp = Sequential()
model_mlp.add(Dense(100, activation='relu', input_dim=X_train.shape[1]))
model_mlp.add(Dense(1))
model_mlp.compile(loss='mse', optimizer=adam)

 

 

summary를 통해 확인.

 

훈련시키기.

 

 

 

 

Predict

훈련된 모델로 예측한다.

mlp_train_pred = model_mlp.predict(X_train.values)
mlp_valid_pred = model_mlp.predict(X_valid.values)

 

 

 

실제 값과 예측 값을 잘 보여주기 위해 새로운 dataFrame을 만든다.

 

Y_train_ = pd.DataFrame({'y_true': Y_train, 'y_pred':
mlp_train_pred.reshape(mlp_train_pred.shape[0],)}).set_index(X_train.index).sort_index()


Y_valid_ = pd.DataFrame({'y_true': Y_valid, 'y_pred':
mlp_valid_pred.reshape(mlp_valid_pred.shape[0],)}).set_index(X_valid.index).sort_index()


Y_train_ = pd.merge(Y_train_, agg[['날짜(t+6)']], left_index=True, right_index=True, how='left')
Y_train_.set_index('날짜(t+6)', inplace=True)

Y_valid_ = pd.merge(Y_valid_, agg[['날짜(t+6)']], left_index=True, right_index=True, how='left')
Y_valid_.set_index('날짜(t+6)', inplace=True)

 

Y_train_.set_index를 해서 '날짜(t+6)'의 날짜로 인덱스를 바꾼 이유는

이게 알아보기가 더 편해보였다.

해당 y_true, y_pred가 언제 날짜의 실제값,예측값인지를 바로 보기 위함이다.

걸 안했더라면 6개월 lag한걸 고려해서 2004-05-01로 index가 보여졌을 것이다.

실제 대상 날짜가 2004-11-01이므로 이렇게 바꾸었다.

 

 

 

 

 

Train 성능 / Validation 성능

 

from sklearn import metrics

def scoring(y_true, y_pred):
    r2 = round(metrics.r2_score(y_true, y_pred) * 100, 3)
    # mae = round(metrics.mean_absolute_error(y_true, y_pred), 3)
    corr = round(np.corrcoef(y_true, y_pred)[0,1],3)
    mape = round(metrics.mean_absolute_percentage_error(y_true, y_pred) * 100, 3)
    rmse = round(metrics.mean_squared_error(y_true, y_pred, squared=False), 3)
    
    df= pd.DataFrame({
        'R2':r2,
        'Corr':corr,
        'RMSE':rmse,
        'MAPE':mape
        },
        index=[0])
    
    return df

 

 

 

 

그래프로 표현

def abline(slope, intercept):
    """ Plot a line from slope and intercept"""
    axes = plt.gca() # Get the Current Axes
    x_vals = np.array(axes.get_xlim())
    y_vals = intercept + slope * x_vals
    plt.plot(x_vals, y_vals, '--')
    
   
def MinMax(y_true, y_pred, m="min"):
    if(m == "min") :
        return min(min(y_true),min(y_pred)) - 2
    else :
        return max(max(y_true),max(y_pred)) + 2
        

def myGraph(temp, title) :
    fig, axs = plt.subplots(1,2,figsize=(20,5), gridspec_kw={'width_ratios':[2.5, 1]})
    axs[0].plot(temp.index, temp.y_true, label = "Original")
    axs[0].plot(temp.index, temp.y_pred, label = "Predicted")
    axs[0].legend(loc='upper right')
    axs[0].title.set_text(title)
    axs[0].set_xlabel("date")
    axs[0].set_ylabel("price index")

    axs[1].plot(temp.y_true, temp.y_pred,'.')
    plt.xlim(MinMax(temp.y_true, temp.y_pred), MinMax(temp.y_true, temp.y_pred,"max"))
    plt.ylim(MinMax(temp.y_true, temp.y_pred), MinMax(temp.y_true, temp.y_pred,"max"))
    abline(1,0)
    plt.title(title)
    plt.xlabel("Original")
    plt.ylabel("Predicted")

 

temp = Y_valid_

 

실제 값과 예측값이 비슷한 편이긴 하나, 그렇다고 매우 비슷하다고 보기는 어려워보인다.

사실 모델 자체의 문제라기 보다는, 해당 데이터는 input data가 너무 적고 제약적이다.

따라서 추후에 다른 설명변수들이 추가로 필요해보인다. 

 

 

 

 

이 외에 MLP로 했던걸 CNN이나 LSTM등의 모델로 표현하는 것은 어렵지 않다.

input data의 형태만 조금 바꿔주면 된다.

 

 

 

4.2 CNN

 

CNN 아키텍처로 시계열 분석을 위해선, Input Data shape를 3차원으로 변형해준다

X_train_series = X_train.values.reshape((X_train.shape[0], X_train.shape[1], 1))
print('Traing set shape :', X_train_series.shape)
# print(X_train_series[:5])

X_valid_series = X_valid.values.reshape((X_valid.shape[0], X_valid.shape[1], 1))
print('Validation set shape :', X_valid_series.shape)
# print(X_valid_series[:5])

출력 결과:

 

Traing set shape : (123, 7, 1)

Validation set shape : (82, 7, 1)

 

 

model_cnn = Sequential()
model_cnn.add(Conv1D(filters=4, kernel_size=2, activation='relu',
                     input_shape=(X_train_series.shape[1], X_train_series.shape[2])))
model_cnn.add(MaxPooling1D(pool_size=2))
model_cnn.add(Flatten())
model_cnn.add(Dense(50, activation='relu'))
model_cnn.add(Dense(1))
model_cnn.compile(loss='mse', optimizer=adam)

만든 모델 summary :

 

training :

 

training된 모델로 예측 및 출력

cnn_train_pred = model_cnn.predict(X_train_series)
cnn_valid_pred = model_cnn.predict(X_valid_series)



Y_train_ = pd.DataFrame({'y_true': Y_train, 'y_pred':
cnn_train_pred.reshape(cnn_train_pred.shape[0],)}).set_index(X_train.index).sort_index()

Y_train_ = pd.merge(Y_train_, agg[['날짜(t+6)']], left_index=True, right_index=True, how='left')
Y_train_.set_index('날짜(t+6)', inplace=True)


Y_valid_ = pd.DataFrame({'y_true': Y_valid, 'y_pred':
cnn_valid_pred.reshape(cnn_valid_pred.shape[0],)}).set_index(X_valid.index).sort_index()

Y_valid_ = pd.merge(Y_valid_, agg[['날짜(t+6)']], left_index=True, right_index=True, how='left')
Y_valid_.set_index('날짜(t+6)', inplace=True)



scoring(Y_train_.y_true, Y_train_.y_pred)
scoring(Y_valid_.y_true, Y_valid_.y_pred)

 

 

 

 

 

4.3 LSTM

이하 동일

model_lstm = Sequential()
model_lstm.add(LSTM(50, activation='relu', input_shape=(X_train_series.shape[1],
                                                       X_train_series.shape[2])))
model_lstm.add(Dense(1))
model_lstm.compile(loss='mse', optimizer=adam)

lstm_train_pred = model_lstm.predict(X_train.values)
lstm_valid_pred = model_lstm.predict(X_valid.values)

Y_train_ = pd.DataFrame({'y_true': Y_train, 'y_pred':
lstm_train_pred.reshape(lstm_train_pred.shape[0],)}).set_index(X_train.index).sort_index()
Y_train_ = pd.merge(Y_train_, agg[['날짜(t+6)']], left_index=True, right_index=True, how='left')
Y_train_.set_index('날짜(t+6)', inplace=True)


Y_valid_ = pd.DataFrame({'y_true': Y_valid, 'y_pred':
lstm_valid_pred.reshape(lstm_valid_pred.shape[0],)}).set_index(X_valid.index).sort_index()
Y_valid_ = pd.merge(Y_valid_, agg[['날짜(t+6)']], left_index=True, right_index=True, how='left')
Y_valid_.set_index('날짜(t+6)', inplace=True)

 

 

 

 

기본적인 MLP, CNN, LSTM만 적용해 보았다.

이번 예제는 input 자체가 굉장히 제한적이기에 추가로 input을 더 넣어볼 생각이다. 

 

이상.