Basic models provide simple yet effective forecasting capabilities with minimal computational requirements. These models are ideal for establishing baselines, handling simple patterns, and situations where interpretability and speed are important.
DLinear performs linear forecasting through decomposition-based modeling, separating trend and seasonal components for better interpretability and performance.
DLinear decomposes time series into trend and seasonal components, then applies separate linear transformations:
- Decomposition: Series = Trend + Seasonal + Residual
- Linear Mapping: Each component is linearly mapped to future values
- Aggregation: Final forecast combines all component predictions
#[derive(Debug, Clone)]
pub struct DLinearConfig {
pub horizon: usize,
pub input_size: usize,
pub kernel_size: usize,
pub individual: bool,
pub use_decomposition: bool,
}use neuro_divergent::models::{DLinear, DLinearConfig};
let config = DLinearConfig::builder()
.horizon(7) // Forecast 7 steps ahead
.input_size(28) // Use 28 historical points
.kernel_size(25) // Moving average kernel size
.individual(false) // Shared parameters across series
.use_decomposition(true) // Enable trend/seasonal decomposition
.build()?;
let model = DLinear::new(config)?;let dlinear = DLinear::builder()
.horizon(12)
.input_size(36)
.kernel_size(25)
.individual(true) // Individual parameters per series
.build()?;let config = DLinearConfig {
horizon: 24,
input_size: 168, // Weekly data (24 * 7)
kernel_size: 25, // Smooth decomposition
individual: true, // Series-specific parameters
use_decomposition: true,
};
let model = DLinear::new(config)?;horizon(required): Number of future time steps to forecastinput_size(required): Number of historical time steps to use as inputkernel_size(default: 25): Size of moving average kernel for decompositionindividual(default: false): Whether to use individual linear layers for each seriesuse_decomposition(default: true): Whether to use trend/seasonal decomposition
- Training Time: O(n) - Very fast
- Inference Time: O(1) - Constant time
- Memory Usage: Low
- Interpretability: High
- Best For: Datasets with clear trend/seasonal patterns
- Extremely Fast: Linear operations only
- Interpretable: Clear decomposition into trend/seasonal components
- Robust: Minimal parameters, less prone to overfitting
- Efficient: Low memory and computational requirements
- Baseline: Excellent starting point for any forecasting task
- Linear Assumption: Cannot capture complex non-linear patterns
- Fixed Decomposition: Uses simple moving average decomposition
- Limited Capacity: May underfit complex datasets
NLinear applies normalization before linear transformation, improving performance on non-stationary time series.
- Normalization: Subtract last value and normalize by standard deviation
- Linear Mapping: Apply linear transformation to normalized data
- Denormalization: Add back last value to get final forecast
#[derive(Debug, Clone)]
pub struct NLinearConfig {
pub horizon: usize,
pub input_size: usize,
pub individual: bool,
pub use_normalization: bool,
}let nlinear = NLinear::builder()
.horizon(7)
.input_size(28)
.individual(false)
.use_normalization(true)
.build()?;// Configuration for non-stationary financial data
let config = NLinearConfig::builder()
.horizon(5)
.input_size(60) // 2-3 months of daily data
.individual(true) // Each stock has different characteristics
.use_normalization(true) // Handle different price levels
.build()?;
let model = NLinear::new(config)?;horizon(required): Number of future time steps to forecastinput_size(required): Number of historical time steps to useindividual(default: false): Series-specific linear layersuse_normalization(default: true): Apply normalization before modeling
- Training Time: O(n) - Very fast
- Inference Time: O(1) - Constant time
- Memory Usage: Low
- Best For: Non-stationary time series with varying scales
- Handles Non-Stationarity: Normalization helps with varying scales
- Simple and Fast: Minimal computational overhead
- Good Baseline: Often surprisingly competitive
- Stable Training: Normalization improves numerical stability
Standard multi-layer perceptron for univariate time series forecasting.
A feed-forward neural network with:
- Input layer (input_size)
- Hidden layers with ReLU activation
- Output layer (horizon)
- Optional dropout for regularization
#[derive(Debug, Clone)]
pub struct MLPConfig {
pub horizon: usize,
pub input_size: usize,
pub hidden_sizes: Vec<usize>,
pub dropout: f64,
pub activation: ActivationType,
}let mlp = MLP::builder()
.horizon(7)
.input_size(28)
.hidden_sizes(vec![64, 32]) // Two hidden layers
.dropout(0.1)
.activation(ActivationType::ReLU)
.build()?;let deep_mlp = MLP::builder()
.horizon(12)
.input_size(48)
.hidden_sizes(vec![256, 128, 64, 32]) // 4 hidden layers
.dropout(0.2) // More regularization for deeper network
.activation(ActivationType::ReLU)
.build()?;let shallow_mlp = MLP::builder()
.horizon(3)
.input_size(12)
.hidden_sizes(vec![32]) // Single hidden layer
.dropout(0.05) // Light regularization
.build()?;horizon(required): Forecast horizoninput_size(required): Input sequence lengthhidden_sizes(default: [64]): Sizes of hidden layersdropout(default: 0.1): Dropout rate for regularizationactivation(default: ReLU): Activation function
- Training Time: O(n·h) where h is hidden size
- Inference Time: O(h) - Proportional to network size
- Memory Usage: Medium
- Best For: Non-linear patterns in univariate series
- Non-Linear: Can capture complex patterns
- Flexible: Easily adjustable architecture
- Universal Approximator: Theoretically can model any function
- Fast Training: No recurrent connections
- No Temporal Structure: Treats input as fixed-size vector
- Limited Context: Cannot handle variable-length sequences
- Overfitting: May overfit with small datasets
Multi-layer perceptron specifically designed for multivariate time series forecasting.
Similar to MLP but with modifications for multivariate data:
- Input layer handles multiple variables
- Shared or separate processing for each variable
- Output layer produces forecasts for all variables
#[derive(Debug, Clone)]
pub struct MLPMultivariateConfig {
pub horizon: usize,
pub input_size: usize,
pub num_variables: usize,
pub hidden_sizes: Vec<usize>,
pub dropout: f64,
pub shared_weights: bool,
pub activation: ActivationType,
}let mv_mlp = MLPMultivariate::builder()
.horizon(12)
.input_size(36)
.num_variables(5) // GDP, inflation, unemployment, etc.
.hidden_sizes(vec![128, 64])
.shared_weights(false) // Different processing per variable
.dropout(0.15)
.build()?;// When variables have similar characteristics
let shared_mlp = MLPMultivariate::builder()
.horizon(7)
.input_size(28)
.num_variables(10) // Multiple similar sensors
.hidden_sizes(vec![64, 32])
.shared_weights(true) // Shared processing
.dropout(0.1)
.build()?;num_variables(required): Number of variables to forecastshared_weights(default: false): Share weights across variables- All other parameters same as MLP
- Training Time: O(n·h·v) where v is number of variables
- Memory Usage: Medium to High (depends on num_variables)
- Best For: Multivariate forecasting with cross-variable dependencies
use neuro_divergent::training::{TrainingConfig, OptimizerConfig, LossFunctionConfig};
let training_config = TrainingConfig::builder()
.max_epochs(100)
.batch_size(32)
.learning_rate(0.001)
.optimizer(OptimizerConfig::Adam {
beta1: 0.9,
beta2: 0.999
})
.loss_function(LossFunctionConfig::MSE)
.early_stopping_patience(10)
.build()?;// Different loss functions for different scenarios
let mse_config = LossFunctionConfig::MSE; // Standard squared loss
let mae_config = LossFunctionConfig::MAE; // Robust to outliers
let huber_config = LossFunctionConfig::Huber { // Combines MSE + MAE
delta: 1.0
};
let mape_config = LossFunctionConfig::MAPE; // Percentage error// L2 regularization
let config = MLPConfig::builder()
.hidden_sizes(vec![128, 64])
.dropout(0.2)
.l2_regularization(0.01) // Weight decay
.build()?;
// Early stopping
let training_config = TrainingConfig::builder()
.early_stopping_patience(15)
.validation_split(0.2)
.build()?;| Model | Parameters | Training Time | Inference Time | Memory |
|---|---|---|---|---|
| DLinear | O(I) | O(n) | O(1) | Low |
| NLinear | O(I) | O(n) | O(1) | Low |
| MLP | O(I×H + H²) | O(n×H) | O(H) | Medium |
| MLP-MV | O(V×I×H + H²) | O(n×V×H) | O(V×H) | High |
Where I=input_size, H=hidden_size, V=num_variables, n=dataset_size
Typical performance on standard datasets:
- DLinear: 13.2 sMAPE
- NLinear: 13.5 sMAPE
- MLP: 13.8 sMAPE
- Linear Baseline: 14.1 sMAPE
- NLinear: 0.084 MAE (best for non-stationary)
- DLinear: 0.087 MAE
- MLP: 0.089 MAE
- Random Walk: 0.095 MAE
// Choose based on data characteristics
fn select_basic_model(data_info: &DataInfo) -> Box<dyn BaseModel<f64>> {
match data_info {
// Stationary data with clear trend/seasonality
DataInfo { stationary: true, seasonal: true, .. } => {
Box::new(DLinear::builder()
.horizon(data_info.horizon)
.use_decomposition(true)
.build().unwrap())
},
// Non-stationary data
DataInfo { stationary: false, .. } => {
Box::new(NLinear::builder()
.horizon(data_info.horizon)
.use_normalization(true)
.build().unwrap())
},
// Complex non-linear patterns
DataInfo { complex_patterns: true, .. } => {
Box::new(MLP::builder()
.horizon(data_info.horizon)
.hidden_sizes(vec![64, 32])
.dropout(0.15)
.build().unwrap())
},
// Default to simple linear
_ => {
Box::new(DLinear::builder()
.horizon(data_info.horizon)
.build().unwrap())
}
}
}// Grid search for MLP
let hidden_sizes_options = vec![
vec![32],
vec![64],
vec![64, 32],
vec![128, 64],
vec![128, 64, 32],
];
let dropout_options = vec![0.0, 0.1, 0.2, 0.3];
let mut best_config = None;
let mut best_score = f64::INFINITY;
for hidden_sizes in hidden_sizes_options {
for &dropout in &dropout_options {
let config = MLPConfig::builder()
.horizon(7)
.input_size(28)
.hidden_sizes(hidden_sizes.clone())
.dropout(dropout)
.build()?;
let model = MLP::new(config.clone())?;
let cv_score = evaluate_cross_validation(&model, &data)?;
if cv_score < best_score {
best_score = cv_score;
best_config = Some(config);
}
}
}// Preprocessing recommendations for basic models
use neuro_divergent::preprocessing::{StandardScaler, RobustScaler};
// For DLinear - minimal preprocessing needed
let dlinear_data = data.clone(); // Raw data often works well
// For NLinear - built-in normalization, but scaling can help
let mut scaler = RobustScaler::new(); // Robust to outliers
let nlinear_data = scaler.fit_transform(&data)?;
// For MLP - standardization usually helps
let mut scaler = StandardScaler::new();
let mlp_data = scaler.fit_transform(&data)?;// Combine basic models for robustness
let ensemble_models = vec![
Box::new(DLinear::builder()
.horizon(7)
.use_decomposition(true)
.build()?) as Box<dyn BaseModel<f64>>,
Box::new(NLinear::builder()
.horizon(7)
.use_normalization(true)
.build()?) as Box<dyn BaseModel<f64>>,
Box::new(MLP::builder()
.horizon(7)
.hidden_sizes(vec![64, 32])
.dropout(0.1)
.build()?) as Box<dyn BaseModel<f64>>,
];
let ensemble = NeuralForecast::builder()
.with_models(ensemble_models)
.build()?;use neuro_divergent::prelude::*;
// Load data
let data = TimeSeriesDataFrame::from_csv("simple_data.csv")?;
// Create simple model
let model = DLinear::builder()
.horizon(7)
.input_size(28)
.build()?;
// Create forecaster
let mut nf = NeuralForecast::builder()
.with_model(Box::new(model))
.with_frequency(Frequency::Daily)
.build()?;
// Train and predict
nf.fit(data.clone())?;
let forecasts = nf.predict()?;// Quick baseline establishment
fn establish_baseline(data: &TimeSeriesDataFrame<f64>) -> NeuroDivergentResult<Vec<f64>> {
// Try multiple simple models quickly
let models = vec![
("DLinear", DLinear::builder().horizon(7).build()?),
("NLinear", NLinear::builder().horizon(7).build()?),
("MLP", MLP::builder().horizon(7).hidden_sizes(vec![32]).build()?),
];
let mut results = Vec::new();
for (name, mut model) in models {
let dataset = TimeSeriesDataset::from_dataframe(data)?;
model.fit(&dataset)?;
let forecast = model.predict(&dataset)?;
let mae = calculate_mae(&forecast.forecasts, &actual_values)?;
results.push(mae);
println!("{}: MAE = {:.4}", name, mae);
}
Ok(results)
}Basic models provide an excellent foundation for time series forecasting, offering simplicity, speed, and interpretability while often achieving competitive performance on many real-world datasets.