Predicting Apple Stock Prices with Neural Networks | by Soner Yıldırım | Towards Data Science

Stock price prediction is definitely not an easy task as there are many factors that need to be taken into consideration. Overall market conditions, competitors’ performance, new product releases, temper of global relations are just some key factors that have potential to increase or decrease stock prices. In addition to these, unexpected events may occur such as the corona virus situation we are currently experiencing.
The factors we have listed up to now are hard to predict. Thus, we will put them aside throughout this post. One key factor that can be used to make prediction on stock prices is the historical prices. For instance, if a stock has been increasing steadily for two years, we might assume that it will keep going up for another month. However, stocks are not usually follow a simple continuous upward or downward trend. Thus, we need more complicated tools to make observations that are hard for us to catch.
LSTM (Long Short Term Memory), which is a type of RNN (Recurrent Neural Network), can be used to predict stock prices using historical data. LSTM is suitable to model sequence data because it maintains an internal state to keep track of the data it has already seen. Common applications of LSTMs include time series analysis and natural language processing.
Let’s first obtain the data using pandas datareader module. It makes it very simple to get stock price data with a few lines of code.
import numpy as npimport pandas as pdfrom pandas_datareader import dataaapl = data.DataReader("AAPL",                         start='2015-1-1',                        end='2019-12-31',                        data_source='yahoo')
That’s all! We now have apple stock price data between 2015 and 2020 saved in a pandas dataframe.
We will use the adjusted close (Adj Close) price. Let’s take a look at the general trend from 2015 to 2020.
#import dataviz librariesimport matplotlib.pyplot as pltimport seaborn as snssns.set(style='darkgrid')%matplotlib inline#Plot "Adj Close"plt.figure(figsize=(12,8))plt.title("APPLE Stock Prices", fontsize=15)sns.lineplot(x=aapl.index, y='Adj Close', data=aapl)
2015 does not seem to be a profitable year which followed by a stable upward trend for almost 3 years. We observe a sharp decrease at the end of 2019 which followed by a steady increase for about a year.
What we will try to do with LSTM layers is to predict the price at time t using the historical prices of past 90 days (i.e. from t-90 to t-1). Please keep in mind that we will try to capture the trend, not exact prices.
LSTM requires the input to be a 3D tensor with shape (batch_size, timesteps, input_dim).
Since we are using historical data of 90 days (t-90 to t-1) to make prediction at time t, the number of timesteps is 90. We only use “Adj Close” price to make the prediction so the input_dim is 1. Input_dim can be increase by adding additional features. For example, the value of a competitors stock price may have an effect on Apple stock price. If we also use a second variable to make the prediction, than the input_dim will be 2. The batch_size is the number of observations to be feed into LSTM layers before updating the weights.
We need to format the data in a way that each input contains stock prices of a 90-day period (t-90 to t-1) and the target is the price at time t. We can use a basic for loop as follows:
hist = []target = []length = 90adj_close = aapl['Adj Close']for i in range(len(adj_close) - length):   x = adj_close[i:i+length]   y = adj_close[i+length]   hist.append(x)   target.append(y)
Each element of “hist” is a list of 90 items. Since we increment by one, the last item of the second element of “hist” must be equal to the first element of “target”. Let’s confirm:
hist[1][89] == target[0]True
“Hist” and “target” are lists. We need to convert them to numpy arrays and reshape the target variable to a 2-dimensional array.
hist = np.array(hist)target = np.array(target)target = target.reshape(-1,1)print(hist.shape)print(target.shape)(1168, 90) (1168, 1)
It is also better to normalize the values due to excessive computations done in a neural network. Normalization can be done with a few simple mathematical operations or pre-defined functions of libraries can be used. I will use MinMaxScaler of scikit-learn. By default, it normalizes values into a range of [0,1].
We are working on a supervised learning task. We will train the model with some data and test its performance using previously unseen data. Thus, we need to split the data into training and test sets. To get accurate performance results, the model should not have any clue about the data in test set. We should keep that in mind when normalizing the data as well. We need to split the data first. Then apply normalization separately.
#train/test splitX_train = hist[:1138]X_test = hist[1138:]y_train = target[:1138]y_test = target[1138:]
We split the data in a way that the model is trained with the data of 1138 days and tested on the data of the following 30 days.
We can now do the normalization. We will first create a MinMaxScaler object and apply fit_transform method on training set. Then, we will only apply transform method on test set. It is very important to make this separation. Otherwise, we would be leaking information to the model from test set.
from sklearn.preprocessing import MinMaxScalersc = MinMaxScaler()#train set, fit_transformX_train_scaled = sc.fit_transform(X_train)y_train_scaled = sc.fit_transform(y_train)#test set, only transformX_test_scaled = sc.transform(X_test)y_test_scaled = sc.transform(y_test)
Last step of data preprocessing is to convert inputs to 3-D arrays because LSTM accepts 3-D arrays as input.
X_train_scaled = X_train_scaled.reshape((len(X_train_scaled), length, 1))X_test_scaled = X_test_scaled.reshape((len(X_test_scaled), length, 1))
We will implement the neural network using LSTM layes of Keras which is a high-level API of TensorFlow.
import tensorflow as tffrom tensorflow.keras import layers
We will create a sequential model with 4 LSTM layers and a dense layer.
model = tf.keras.Sequential()model.add(layers.LSTM(units=64, return_sequences=True, input_shape=(90,1), dropout=0.2))model.add(layers.LSTM(units=32, return_sequences=True, dropout=0.2))model.add(layers.LSTM(units=32, return_sequences=True, dropout=0.2))model.add(layers.LSTM(units=16, dropout=0.2))model.add(layers.Dense(units=1))model.summary()
There are some important points to keep in mind:
If an LSTM layer is followed by another LSTM layer, return_sequences parameter must be set as True.
Input_shape parameter needs to specified only for first LSTM layer. For other layers, model figure out the input from the output of the previous layer.
Input_shape parameter is a tuple that contains timesteps and input_dim. Batch_size is specified during training.
Units parameter is the number of nodes used in a layer. There is not a strict rule to define optimum number of nodes.
Dropout is used to prevent the model from overfitting.
Units of dense layer should be 1 since it is used as the output layer.
We can now compile the model by specifying an optimizer and a loss function.
model.compile(optimizer='adam', loss='mean_squared_error')
Then, we train the model.
history = model.fit(X_train_scaled, y_train_scaled,                     epochs=30, batch_size=16)
We already discussed the batch_size. Epochs indicates how many times the entire dataset is fed into the neural network.
You see the number 72 at each epoch. It comes from number of training points divided by the batch size. X_train_scaled has 1138 data points. The batch size is 16. 1138/15=71.125 so it is completed in 72 cycles.
The model achieved a MSE of 0.0017. Let’s see how it has changed through 30 epochs.
loss = history.history['loss']epoch_count = range(1, len(loss) + 1)plt.figure(figsize=(12,8))plt.plot(epoch_count, loss, 'r--')plt.legend(['Training Loss'])plt.xlabel('Epoch')plt.ylabel('Loss')plt.show();
The loss has followed a downward trend and reached below 0.002 after epoch 25. However, the model has not converged yet. It seems like loss will keep decreasing if we do more epochs. Let’s try to 50 epochs.
Now the loss is below 0.001. Please note that it is not always best practice to keep reducing the loss. After some point, we may end up having an overfit model which focus on training set too much rather than generalizing well.
It is time to make predictions. We will try to predict the trend of stock price on the following 30 day period. Remember our goal is to predict the trend, not the actual prices.
pred = model.predict(X_test_scaled)pred_transformed = sc.inverse_transform(pred)y_test_transformed = sc.inverse_transform(y_test_scaled)plt.figure(figsize=(12,8))plt.plot(y_test_transformed, color='blue', label='Real')plt.plot(pred_transformed, color='red', label='Prediction')plt.title('Apple Stock Price Prediction')plt.legend()plt.show()
Although the values do not precisely match, we are able to predict the general trend in a 30-day period. The model responded to decrease between days 7 and 10 a few days late. Then it catched up the upward trend.
There are many ways to try to improve the performance of this model. You can achieve a better result by adjusting:
Number of layers
Number of nodes in a layer
Number of timesteps
Dropout ratio
Number of epochs
Batch size
Thank you for reading. Please let me know if you have any feedback.