OK. It's taken me longer than I expected, and I wish I had more time to put finishing touches on, and check for errors. Feel free to point out any errors :) I hope it is of some help :)
Code Review and Suggestions
Strengths
Areas for Improvement
The following suggestions are listed (roughly) in decreasing order of seriousness.
1. Data Splitting and Scaling
Issue: The MinMaxScaler
is applied to the entire dataset before splitting into training and test sets. This introduces data leakage because information from the test set (eg., its minimum and maximum values) is used to normalise the training data. This artificially inflates model performance, as the test set is no longer a truly unseen dataset.
Why This Is a Problem:
- Data leakage occurs when information from outside the training data influences the model during training. In this case, knowing the test set's range (minimum and maximum values) allows the model to "cheat" by benefiting from test set information during training.
- For example, if the test set contains extreme values absent from the training set, scaling the entire dataset compresses the range of the training data unnaturally, leading to overly optimistic evaluation.
- For time series data, preserving temporal order is critical. The test set must simulate future, unseen data, and any preprocessing must rely solely on past (training) data.
Fix:
scaler.fit(X_train) # Apply to training data only
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
- Why This Fix Works:
- By fitting the
MinMaxScaler
only on the training data, the test set remains unseen during scaling. This helps to ensure that model evaluation simulates predicting future data, preserving the test set's integrity as a truly unseen dataset.
2. Train-Test Split and Validation
train_size = int(0.8 * len(data_flattened_scaled))
X_train = X[:train_size]
y_train = y[:train_size]
X_test = X[train_size:]
y_test = y[train_size:]
- Why This Fix Works:
- Separating the data into training and validation sets ensures that model evaluation reflects performance on unseen data, providing an unbiased assessment.
3. Time Series Cross-Validation
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(X):
X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]
- Why This Fix Works:
- TimeSeriesSplit helps to ensure that training data always precedes validation data, preserving the temporal order. This prevents future data from influencing model training, leading to a more realistic evaluation.
4. LSTM Input and Output
Issue: This is a curious one! The LSTM target (y_lstm) is reshaped to (90, 1, 600), which looks to be inconsistent with the intended regression task.
Why This Is a Problem:
- LSTM output should match the dimensionality of the target variable ((90, 600) in this case) to ensure compatibility with regression.
- LSTM models expect 3D input tensors (samples, timesteps, features), so incorrect reshaping can misalign input-output dimensions, causing training or prediction errors.
Fix:
y_lstm = data_flattened_scaled[3:93] # Shape: (90, 600)
- Why This Fix Works:
- Ensuring that the target shape matches the output dimension allows the model to perform regression correctly, aligning predictions with the target. Although the tensor shape for y_lstm may be incorrect and we would typically expect a runtime error due to mismatched dimensions, the code likely runs (I am assuming that it does, I haven't actually run it myself) because Keras can internally reshape or broadcast the target tensor to fit the model's output during training. However, this silent handling of shape mismatches can lead to unintended behaviour: the model may not optimise correctly for the intended regression task, and its predictions might not align directly with the flattened day-wise targets. Adjusting the target shape ensures consistency with the regression, allowing the model to produce outputs that are directly comparable to the ground truth and easier to interpret. I won't rule out the possibility that I have got this one wrong!
5. Naive Prediction
residuals_naive = [np.mean((X_test[i] - X_test[i - 1]) ** 2) for i in range(1, len(X_test))]
- Why This Fix Works:
- Evaluating naive predictions on the validation set helps to ensure a fair comparison with other models, reflecting real-world forecasting challenges.
6. ARIMA Loop Efficiency
Issue: The ARIMA model is fit independently for each feature of each day, leading to inefficiency and excessive computation time.
Why This Is a Problem:
Fitting ARIMA independently for 600 features per day is computationally expensive and redundant, especially when many features may be correlated or irrelevant.
This approach will likely not scale well for larger datasets.
If there are no current performance issues and you don't intend to run it a bigger scale, then this is a non-issue.
Fix:
from sklearn.decomposition import PCA
pca = PCA(n_components=10) # Choose appropriate number of components
X_pca = pca.fit_transform(data_flattened_scaled)
- Why This Fix Works:
- Reducing dimensionality via PCA aggregates feature information, significantly decreasing computational overhead while retaining most of the variance.
7. Visualisation - Ambiguous Labels
plt.title('Residuals for Models (Validation Set)')
plt.xlabel('Days (Validation Set)')
- Why This Fix Works:
- Including a phrase like "Validation Set" explicitly ties the residuals to a specific data split (validation) rather than leaving it ambiguous. This tells the audience that these are errors from predictions on unseen data, not training data.
8. Visualisation - Overlapping Labels
Issue: The x-axis labels (eg., Day 1, Day 2, etc.) are printed too close together, causing them to overlap and making them illegible.
Why This Is a Problem:
- Overlapping labels make it difficult for the audience to identify individual days or interpret the residuals associated with each day.
- This diminishes the clarity of the visualisation and can lead to frustration or misinterpretation of the data.
Fix:
plt.xticks(ticks=range(0, len(days), 2), labels=[f'Day {i}' for i in range(0, len(days), 2)], rotation=45, ha='right')
- Why This Fix Works:
- Reducing the number of labels by skipping every 2 or more days, combined with rotating and aligning the remaining labels to the right, ensures clarity and readability while preserving the overall layout and scale of the plot. You may need to experiment with the number of skipped days to achieve an optimal plot for your results.
Are Residuals the Best Way to Visually Compare Models?
Plotting just the residuals is a useful starting point for comparing models, as they highlight the magnitude and distribution of errors. However, residual plots alone will likely not provide a comprehensive comparison:
Limitations: Residuals focus only on individual errors and do not summarise overall performance (eg., MSE or MAE). They also lack insight into how well models capture underlying patterns or trends in the data.
Alternatives: Consider complementing residual plots with the following visualisations:
1. Prediction vs. Actual plot:
Overlay model predictions with actual values to assess how well the model captures trends and patterns:
import matplotlib.pyplot as plt
plt.plot(y_test, label='Ground Truth')
plt.plot(y_pred, label='Predictions', alpha=0.7)
plt.title('Predictions vs. Ground Truth')
plt.legend()
plt.show()
2. Residual Histograms:
Examine the distribution of residuals to detect systematic biases or outliers:
import numpy as np
residuals = y_test - y_pred
plt.hist(residuals, bins=20, edgecolor='black')
plt.title('Residual Distribution')
plt.xlabel('Residuals')
plt.ylabel('Frequency')
plt.show()
3. Heatmaps:
Use heatmaps to visualise residuals across time and features, revealing patterns that might otherwise go unnoticed:
import seaborn as sns
import numpy as np
# Example residual matrix
residual_matrix = np.random.rand(10, 25) # Replace with actual residuals
sns.heatmap(residual_matrix, annot=True, fmt=".2f", cmap="coolwarm")
plt.title("Residuals Heatmap")
plt.xlabel("Features")
plt.ylabel("Days")
plt.show()
Statistical Methods for Model Comparison
While visualisations provide qualitative insights into model performance, statistical methods offer a formal framework to compare models quantitatively. These methods ensure that observed differences in performance are meaningful and not due to random variation. Below are some common statistical approaches for model comparison:
1. Aggregate Metrics
Aggregate metrics summarise model performance across all predictions, providing a clear quantitative measure of accuracy. Common metrics include:
- Mean Absolute Error (MAE): Measures the average magnitude of errors in predictions, without considering their direction.
- Mean Squared Error (MSE): Penalises larger errors more heavily, making it sensitive to outliers.
Example implementation:
from sklearn.metrics import mean_squared_error, mean_absolute_error
mse = mean_squared_error(y_test, y_pred)
mae = mean_absolute_error(y_test, y_pred)
print(f"MSE: {mse:.2f}, MAE: {mae:.2f}")
2. Likelihood-Based Criteria (AIC/BIC):
For models based on likelihood estimation (eg., ARIMA), metrics such as:
Akaike Information Criterion (AIC): Balances goodness-of-fit with model complexity by penalising the number of parameters.
Bayesian Information Criterion (BIC): Similar to AIC but imposes a stricter penalty on model complexity.
These criteria allow for the comparison of models by evaluating their relative likelihoods while discouraging overfitting.
3. Diebold-Mariano Test:
In time-series contexts, the Diebold-Mariano test is often used to compare the predictive accuracy of two models. It tests whether the forecast errors from two models differ significantly, accounting for potential autocorrelation in the residuals.
This test is especially valuable when comparing models that produce sequential predictions over time.
4. Residual-Based Tests:
For general regression tasks, residual-based tests can help assess performance differences:
Paired t-tests: Compare the absolute, or squared, residuals from two models to determine if one produces smaller errors than the other.
ANOVA-based tests: Analyse residual variance across multiple models to identify differences.
Summing up
This review highlights both the strengths of the current implementation and the areas where refinements can improve the robustness and interpretability of results. The code demonstrates a comprehensive approach by combining traditional statistical models like ARIMA with modern machine learning methods such as LSTM, showcasing versatility in addressing complex time-series problems. However, several improvements, such as addressing data leakage, adopting proper time-series cross-validation, and refining visualisations techniques can improve the reliability and clarity of the analysis.
In particular, ensuring that the data pipeline preserves temporal integrity (eg., through proper train-test splits and scaling) should avoid inflated performance metrics. Exploring alternative visualisation methods, like heatmaps and prediction vs. actual plots, can provide additional insights into model behaviour. Complementing these with statistical methods, such as aggregate metrics, AIC/BIC, and the Diebold-Mariano test, allows for rigorous quantitative comparisons between models.
The unusual results reported in the OP likely stem from some of the identified issues, such as improper data splitting or mismatched model evaluation techniques. Implementing the suggested fixes and enhancements will not only address these concerns but also establish a robust framework for fair and meaningful model comparison. This foundation can be extended to handle larger datasets, more sophisticated models, and deeper statistical insights, making the analysis both scalable and adaptable for future applications.
Residuals (MSE)
is something you claimed to show. I am assuming you take 1st 3-day info out of 93 files and start prediction and you come up with the remaining 90-day results and showResiduals (MSE)
$\endgroup$Residuals (MSE)
, be sure to do something like this then plot the heatmap of the matrix. I think your implementation to feed data for this type of matrix-based multi-variante time is not correct. you need to re-visit it. you might need to useMultiOutputRegressor
. I liked you started to ask questions from scratch but the implementation looks for me invalid $\endgroup$