주식 가격 예측하기

elice에서 진행한 데이터사이언티스트 에듀챌린지에서 참여했던 프로젝트입니다. 경진대회 이후 학습을 목적으로 다시 한번 정리해 보았습니다.


목적

  • 2016년 12월 29일까지의 가격 변동을 통해 2016년 12월 30일의 각 회사의 종가의 변화와 종가를 예측
  • 종가 변화의 방향(올랐는지 내렸는지), 그리고 2016년 12월 30일의 종가를 맞춰야함

데이터셋

  • 501개 회사의 주식 가격 데이터
  • 2010년 1월 4일부터 2016년 12월 30일까지의 주식 가격을 포함하고 있음
  • 850,763 개의 트레이닝 데이터
  • 501 개의 채점 데이터
  • 데이터 변수 종류
    • 시작가 (opening price)
    • 종가 (closing price)
    • 최저가 (low)
    • 최고가 (high)
    • 거래량 (volume)
  • 테스트 데이터 포함되지 않음
  • 데이터 출처 : S&P 500 data

데이터 포맷

1. 학습 데이터

  • json 형식으로 list의 list로 이루어짐
[
  '2016-01-05',  // 2016년 1월 5일의 가격
  'launchers',  // launchers 회사의 가격
  123.348,  // 일중 시작가
  125.602,  // 일중 종가
  122.565,  // 일중 최저가
  126.159,  // 일중 최고가
  2164708  // 거래량
]

2. 채점 데이터

  • Grading.input.txt에는 한 줄에 하나씩 회사명이 들어 있음
teller
winch
bay
admissions
mules
firearms
...

평가 기준

  • MSE (Mean squared error) 기준이며, 종가 방향과 가격을 맞춰야함(세부 채점 방법은 elice 페이지 참조)

과정

python import pandas as pd import numpy as np import json import matplotlib.pyplot as plt %matplotlib inline from sklearn.preprocessing import MinMaxScaler import math import os import tensorflow as tf from tqdm import tqdm

  • 이번 주식 가격 예측은 딥러닝 모델인 RNN의 LSTM 모델을 사용하였습니다.

1. 데이터 불러오기

 # 학습 데이터
with open("train_data.json") as fp:
    training_data = json.load(fp)

 # 채점 데이터
with open("grading.input.txt", 'r') as fp:
    grading_data = [ line.rstrip('\n') for line in fp]
  • 데이터 확인
training_data[:5]

결과

[['2016-01-05', 'teller', 123.677, 125.591, 122.459, 126.181, 2163918],
 ['2016-01-06', 'teller', 125.134, 120.089, 120.067, 125.768, 2382872],
 ['2016-01-07', 'teller', 116.413, 114.925, 114.67, 119.469, 2488061],
 ['2016-01-08', 'teller', 115.226, 116.64, 113.26, 117.412, 2009124],
 ['2016-01-11', 'teller', 116.741, 115.231, 113.889, 117.504, 1408088]]
  • 데이터 프레임으로 만들기
df_train_total = pd.DataFrame(training_data, columns=['date', 'symbol', 'open', 'close', 'low', 'high', 'volume'])

2. 데이터 살펴보기

  • 회사(symbol)마다 데이터 갯수 확인합니다.
    • 회사별 최대 데이터 갯수는 1,761개이며, 중간 시점부터 거래가 발생하여 데이터 갯수가 적은 회사도 존재하는 것을 확인 할 수 있었습니다.
grouped_symbol = df_train_total.groupby('symbol')
grouped_symbol.count()

결과(생략)

  • 그래프 그려보기
    list_symbols = list(set(df_train_total['symbol']))
    df_one = df_train_total[df_train_total['symbol'] == list_symbols[0]]
    plt.plot(df_one['close'])
    

    결과(생략)

3. 데이터 생성 및 학습

  • 각 기업 별로 2016년 12월 30일의 종가를 예측해야하기 때문에 전체 데이터 셋에서 해당 해당 기업의 데이터만 뽑아서 학습 후 채점 데이터를 만들고 그 다음 기업의 데이터만으로 다시 학습하여 채점 데이터를 만드는 작업을 반복해야 합니다.
  • 따라서, For문으로 반복해야 하므로 전체 코드를 한번에 담았습니다.
# Hyper Parameter
seq_length = 21

params = {
    'hidden_dim': 10,
    'learning_rate': 0.01,
    'iterations': 1000,
}

list_data = []

for index in tqdm(grading_data):

    # Choose One Comapnay
    df_train = df_train_total[df_train_total['symbol']==index]

    # Preprocessing
    scalerX = MinMaxScaler(feature_range=(0, 1))
    scalerY = MinMaxScaler(feature_range=(0, 1))

    x = scalerX.fit_transform(df_train[['open', 'low', 'high', 'volume', 'close']].values)
    y = scalerY.fit_transform(df_train['close'].values)

    # Divide Dataset to x and y
    dataX = []
    dataY = []

    for i in range(0, len(y) - seq_length):
        _x = x[i:i+seq_length]
        _y = y[i + seq_length]
        dataX.append(_x)
        dataY.append(_y)

    # train / test data set
    train_size = int(len(dataY))

    X_train = np.array(dataX[0:train_size])
    y_train = np.array(dataY[0:train_size]).reshape(-1, 1)

    X_test = np.array([x[len(y) - seq_length: len(y)]])

    # Model Training and Predicting
    test_predict = stock_price_predict(X_train, X_test, y_train, params)

    # inverse scaled stock price
    test_result = scalerY.inverse_transform(test_predict)

    # Creating Grading data
    diff_last_day = test_result[-1][0] - df_train['close'].iloc[-1]
    last_day = test_result[-1][0]

    if diff_last_day > 0:
        data = "+ {:.3f}\n".format(last_day)
    else:
        data = "- {:.3f}\n".format(last_day)

    list_data.append(data)

Preparing Dataset

  • 데이터 셋은 전체 데이터를 사용하기 위해 주어진 모든 데이터를 학습에 사용하도록 하였습니다.
  • 최종적으로 2016년 12월 30일의 주식 종가를 확인하기 위해 seq_length 기간만큼의 데이터를 채점 데이터를 출력하기 위한 입력 데이터로 사용하였습니다.

Data preprocessing

  • 데이터 값의 스케일 조정을 위해 Normalization을 한다.
  • MinMaxScaler을 사용하여 전처리를 한 후에 입력(X)과 타겟(Y) 데이터로 나눕니다.

4. 모델 만들기

  • 위에서 함수로 사용한 LSTM 모델은 아래와 같습니다.
def stock_price_predict(X_train, X_test, y_train, params):

    X_train = X_train
    X_test = X_test
    y_train = y_train

    hidden_dim = params['hidden_dim']
    learning_rate = params['learning_rate']
    iterations = params['iterations']

    data_dim = 5
    output_dim = 1

    tf.set_random_seed(777)  # reproducibility
    tf.reset_default_graph()

    # input place holders
    X = tf.placeholder(tf.float32, [None, seq_length, data_dim])
    Y = tf.placeholder(tf.float32, [None, 1])

    #build a LSTM network
    cell = tf.contrib.rnn.BasicLSTMCell(num_units=hidden_dim, state_is_tuple=True, activation=tf.tanh)

    with tf.variable_scope('rnn'):
        outputs, _states = tf.nn.dynamic_rnn(cell, X, dtype=tf.float32, scope='rnn')

    Y_pred = tf.contrib.layers.fully_connected(outputs[:, -1], output_dim, activation_fn=None)

    # cost/loss
    loss = tf.reduce_sum(tf.square(Y_pred - Y)) # sum of the squares

    # optimaizer
    optimizer = tf.train.AdamOptimizer(learning_rate)
    train = optimizer.minimize(loss)

    with tf.Session() as sess:
        init = tf.global_variables_initializer()
        sess.run(init)

        # Training Step
        for i in range(iterations):
            _, step_loss = sess.run([train, loss], feed_dict={
                X: X_train, Y: y_train
            })

        # Test step
        test_predict = sess.run(Y_pred, feed_dict={X: X_test})

    return test_predict
  • hyper parameter를 조정하여 과거 며칠 동안의 데이터를 학습에 사용할 것인지 seq_length변수로 지정할 수 있습니다.

5. 채점 데이터 만들기

  • list 형태로 저장된 채점 데이터를 텍스트 파일로 저장
with open("submission.txt", 'w') as f_write:
    for word in list_data:
        f_write.write(word)

결론 및 고찰

  • baseline을 2016년 12월 29일 종가로 하였을 때 RMSE = 0.4829
  • seq_length가 7일로 iterations이 500번일 때는 0.500으로 baseline 보다 낮았지만, 21일 간격으로 1500번을 hyper Parameter를 조정하여 RMSE = 0.316 으로 줄일 수 있었음
  • seq_length가 21일 정도로 길게 잡았을 때 정확도가 올라감
  • 시계열 데이터 예측에서 RNN의 우수함을 확인함
  • 향후 Feature Engineeringhyper parameter tuning을 통해 성능을 향상시키는 작업 필요
  • 엘리스 에듀챌린지 시 실수로 학습 데이터를 적게 잡아서 성능이 좋지 못하였음.
  • 엘리스 에듀챌린지 종료 후 별도로 2016년 12월 29일을 채점 데이터로 잡고 RMSE 기준으로 측정함

References

  1. 모두의 딥러닝 - RNN