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())