Getting time series average of Mean absolute forecast errors from a feedforward neural network

Viewed 18

I am replicating a paper that uses financial data to predict corporate earnings. In the paper, the authors claim that

We generate out-of-sample forecasts of one-year-forward earnings +1 for the above testing sample using the 6 machine learning algorithms and the 56 predictors. For each year t between 1975 and 2019, we use all observations from the previous 10 years (i.e., year t - 10, t - 9, …, t - 1) as the training sample to estimate the models, then apply the models to the predictors of year t to generate earnings forecasts for year t+1.

Suppose that I have data from 2010-2020, then based on the paper, I will loop over all the years starting from 2010 to 2019. For instance, If I am looking at 2010's data, then I should use all firms' 2000 to 2009's predictors and 2010's earnings as my training sample, then use the trained model to fit 2010's predictors for 2011's earnings predictions. Am I understanding this right?

I then notice that the authors calculate the time series average of Mean absolute forecast errors (see this picture MAE). At first, I thought that I can calculate 2010's MAE by simply taking the difference between model-based forecasts and the actual 2011 earnings. But then, I am confused by the "time-series average". How is that different from the cross-sectional average if I already obtain all those predicted earnings forecasts?

I hope I made myself clear. I attached my code for the years 1969 to 2017 in case I made mistakes interpreting the paper.

import matplotlib.pyplot as plt
from tqdm import tqdm
import numpy as np

import torch
from torch import nn
from torch import tensor
from torch import optim


torch.manual_seed(0)
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")

#random functions 
def mae(x, y):
    return np.mean(abs(x - y))
#DataLoader class accepts data in the form of a Dataset object
#Create a custom class which is going to inherit from the Dataset class. 
#Other than the making of an __init__ method, inheriting the Dataset class requires us to also 
#override the __getitem__ and __len__ methods. The __len__ method returns the length of our Dataset object and 
#the __getitem__ method returns the xy pair at a given index.

class ICC(Dataset):
  def __init__(self, X, y):
    self.X = X
    self.y = y
    self.len = len(self.X)
  def __getitem__(self, index):
    return self.X[index], self.y[index]
  def __len__(self):
    return self.len

# hyperparameters
#input_size = 784
#hidden_size = 100
#num_classes = 10
num_epochs = 50
learning_rate = 0.001
batch_size = 100
# Initialize the model.
# Set the hidden dimension size.
hidden_dim = 100

# inputs and outputs.
df = pd.read_pickle("predictorsv2.pkl")
predictors = ['ACT', 'AP','AT', 'CEQ', 'CHE', 'DLC', 'DLTT', 'INTAN', 'INVT', 'IVAO', 'LCT','LT', 'PPENT', 'RECT', 'TXP', 'COGS', 'DP', 'DVC',  'NOPIO','SALE',  'TXT', 'XAD', 'XIDO', 'XINT', 'XRD', 'XSGA',  'E', 'CFO', 'delta_ACT', 'delta_AP','delta_AT', 'delta_CEQ', 'delta_CHE', 'delta_DLC', 'delta_DLTT','delta_INTAN', 'delta_INVT', 'delta_IVAO', 'delta_LCT', 
'delta_LT','delta_PPENT', 'delta_RECT', 'delta_TXP', 'delta_COGS', 'delta_DP','delta_DVC', 'delta_NOPIO', 'delta_SALE', 'delta_TXT', 'delta_XAD','delta_XIDO', 'delta_XINT', 'delta_XRD', 'delta_XSGA', 'delta_E','delta_CFO']

for t in range(1969,2017,1):
    beg = t-10
    df = df[(df.year_t>beg)&(df.year_t<t)]
    df = df.dropna(subset = predictors)
    df = df.dropna(subset = ['E_t+1'])
    X = df[predictors]
    y = df['E_t+1']
    X = X.values
    y = torch.tensor(y.values)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    scaler = StandardScaler()
    scaler.fit(X_train)
    X_train = torch.tensor(scaler.transform(X_train)).to(device)
    X_test = torch.tensor(scaler.transform(X_test)).to(device)
    train_data = ICC(X_train, y_train)
    test_data = ICC(X_test, y_test)
#Batch_size — the DataLoader creates batches for us to be able to iterate through them. 
#Shuffle — this allows our data to be shuffled, but more importantly, it shuffles our data every epoch. This trick allows our batches to be a random set of 64 records each time. This trick helps with generalization.
#Drop_last — set as True only when shuffle is set to True. This drops the last non-full batch. Our training set has a size of 514 (X_train.shape). When we divide that by 64, you’ll see that our last batch only contains 1 item. That 1 item is an insufficient sample size to be able to fit the model.
    train_loader = DataLoader(dataset=train_data, batch_size=batch_size, shuffle=True, drop_last=True)
    test_loader = DataLoader(dataset=test_data, batch_size=batch_size, shuffle=True, drop_last=True)

    # Loading data; to tensor; normalize (only covariates)
    num_data, input_dim = X_train.shape
    num_data, output_dim = y_train.reshape(-1,1).shape
    # Step 1: Initialization. 
    class FeedforwardNN(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim):
        super(FeedforwardNN, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.bn1 = nn.BatchNorm1d(hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, output_dim)
        self.relu = nn.ReLU()
        self.drop_layer = nn.Dropout(p=0.5)
    def forward(self, x):
        x = self.relu(self.bn1(self.fc1(x)))
        x = self.drop_layer(x)
        return self.fc2(x)
    #set up the model 
    model = FeedforwardNN(input_dim, hidden_dim, output_dim)
    model.to(device)
    # Initialize the optimizer
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
    # Initialize the loss function.
    criterion = nn.L1Loss()
    #the actual model
    train_batches = int(np.ceil(len(X_train)/batch_size))-1
    test_batches = int(np.ceil(len(X_test)/batch_size))-1
    for epoch in tqdm(range(epochs)):
        train_loss = 0
        validation_loss = 0
        model.train()
        for i in range(train_batches):
            optimizer.zero_grad()
            beg = i*batch_size
            end = (i+1)*batch_size
            predictions = model((X_train[beg:end].float()))
            loss = criterion(predictions, y_train[beg:end].reshape(-1,1).float())
            loss.backward()
            optimizer.step()
            train_loss+= loss.data.item()
    #turn to the evaluation mode 
        model.eval()
        for i in range(test_batches):
            beg = i*batch_size
            end = (i+1)*batch_size
            predictions = model(X_test[beg:end].float())
            test_loss = criterion(predictions, y_test[beg:end].reshape(-1,1).float())
            validation_loss +=test_loss.data.item()
        if(epoch % print_epoch == 0):
            print('Train: epoch: {0} - val_loss: {1:.3f}; train_loss:{2:.3f}'.format(epoch, validation_loss/(i+1), train_loss/(i+1)))

#prediction 
target = df[df.year_t==t]
X_target = torch.tensor(target[predictors].values).to(device)
target_loader = DataLoader(dataset=X_target, batch_size=batch_size)

pred = []
for data in tqdm(target_loader):
    model.eval()
    pred_earnings = model(data.float())
    pred.extend(pred_earnings.cpu().detach().numpy())
0 Answers
Related