<- Previous: Rating Curve | Back to Index | Next: Spatial Extremes ->
Time series analysis is the statistical study of data points collected sequentially over time. Unlike cross-sectional data where observations are assumed independent, time series data exhibit temporal dependence — today's observation is related to yesterday's, and tomorrow's will be related to today's. Understanding and modeling this dependence is essential for forecasting future values and understanding the dynamics of the system being studied.
This chapter introduces the theory and practice of time series modeling in RMC-BestFit, covering autoregressive (AR), moving average (MA), and integrated (ARIMA/ARIMAX) models. We emphasize both the mathematical foundations and practical implementation for readers new to time series methods.
Consider a water resources engineer tasked with forecasting next month's streamflow for reservoir operations. Simply using the long-term average ignores valuable information: if this month's flow is unusually high, next month's flow is likely to also be above average (rivers don't jump randomly between extremes). Time series models capture this persistence, producing better forecasts than naive methods.
Time series analysis is fundamental to:
- Streamflow forecasting: Predict future flows for reservoir operations, water supply planning
- Climate projections: Model temperature, precipitation, and drought indices
- Demand forecasting: Predict water demand, electricity load, or economic variables
- Quality control: Detect anomalies in sensor data through residual monitoring
- Understanding dynamics: Quantify how quickly systems respond to perturbations
The RMC.BestFit library provides a comprehensive suite of time series models with both Maximum Likelihood (MLE) and Bayesian MCMC estimation, enabling full uncertainty quantification for forecasts.
Before diving into specific models, let's establish key concepts that underpin all time series analysis.
A time series is stationary if its statistical properties (mean, variance, autocorrelation structure) don't change over time. Most classical time series models assume stationarity because:
- Non-stationary series have time-varying parameters, making estimation ill-defined
- Forecasts from non-stationary models can diverge to infinity
- Standard statistical tests assume stationarity
Weak stationarity (the practical definition) requires:
- Constant mean:
$E[Y_t] = \mu$ for all$t$ - Constant variance:
$\text{Var}(Y_t) = \sigma^2$ for all$t$ - Autocovariance depends only on lag:
$\text{Cov}(Y_t, Y_{t+h}) = \gamma(h)$ for all$t$
Many real-world series are non-stationary due to trends or seasonality. The ARIMA framework handles this by differencing the series to achieve stationarity before modeling.
Autocorrelation measures the correlation between a time series and lagged versions of itself. The autocorrelation function (ACF) at lag
The ACF reveals the memory structure of the series:
- Fast decay: Short-memory process (AR models with small coefficients)
- Slow decay: Long-memory or near unit-root process
-
Sharp cutoff: MA structure (ACF drops to zero after lag
$q$ ) - Sinusoidal pattern: Seasonal or cyclical component
The partial autocorrelation function (PACF) measures the correlation between
- AR(p) process: PACF cuts off after lag
$p$ - MA(q) process: PACF decays gradually
White noise is a sequence of uncorrelated random variables with constant mean and variance:
White noise has:
-
$\rho(0) = 1$ (correlation with itself) -
$\rho(h) = 0$ for$h \neq 0$ (no correlation at any lag)
The goal of time series modeling is to transform the observed series into white noise residuals. If residuals still show autocorrelation, the model is missing structure.
The backshift (lag) operator
The difference operator is:
Higher-order differences:
Using these operators, model equations become compact polynomial expressions that reveal the mathematical structure.
The RMC.BestFit library implements four primary time series model classes:
| Model | Class | Parameters | When to Use |
|---|---|---|---|
| AR(p) | AutoRegressive |
Gradual ACF decay, sharp PACF cutoff | |
| MA(q) | MovingAverage |
Sharp ACF cutoff, gradual PACF decay | |
| ARIMA(p,d,q) | ARIMA |
Non-stationary series needing differencing | |
| ARIMAX(p,d,q,b) | ARIMAX |
External predictors available |
Each model class has a corresponding analysis class for Bayesian MCMC estimation:
ARAnalysisMAAnalysisARIMAAnalysisARIMAXAnalysis
The autoregressive model is the workhorse of time series analysis. It expresses the current value as a linear combination of past values plus random noise — essentially a regression of the series on its own lagged values.
Imagine predicting tomorrow's river flow. If today's flow is high, tomorrow's is likely high too (persistence). If yesterday's was also high, that provides additional information. An AR model formalizes this: predict today using a weighted combination of recent past values.
The "order"
The AR(p) model is defined as [1]:
where:
-
$Y_t$ is the observation at time$t$ -
$\mu$ is the process mean (intercept) -
$\phi_1, \phi_2, \ldots, \phi_p$ are the AR coefficients -
$\varepsilon_t \stackrel{iid}{\sim} N(0, \sigma^2)$ is white noise
An equivalent formulation that's often more intuitive:
where
Using the backshift operator:
where
For an AR(p) model with intercept:
-
$\mu$ : Process mean (intercept) -
$\phi_1, \phi_2, \ldots, \phi_p$ : AR coefficients -
$\sigma$ : Error standard deviation
Total parameters:
For a stationary AR process, all roots of the characteristic polynomial must lie outside the unit circle [1]:
This ensures the process doesn't explode to infinity.
Simplified conditions for low orders:
-
AR(1):
$|\phi_1| < 1$ -
AR(2):
$\phi_1 + \phi_2 < 1$ ,$\phi_2 - \phi_1 < 1$ ,$|\phi_2| < 1$
A sufficient (but not necessary) general condition:
Interpretation of coefficients:
-
$\phi_1 > 0$ : Positive persistence (high values followed by high values) -
$\phi_1 < 0$ : Oscillating behavior (high values followed by low values) -
$\phi_1 \approx 1$ : Near unit root, very persistent (slow mean reversion) -
$\phi_1 \approx 0$ : Little dependence on immediate past
The AR(p) process has distinctive correlation patterns:
| Statistic | Pattern |
|---|---|
| ACF | Exponential decay (or damped sinusoid for complex roots) |
| PACF | Cuts off sharply after lag |
This PACF cutoff is the key diagnostic for identifying AR order.
The conditional log-likelihood, conditioning on the first
where the residuals are:
Let's fit an AR(2) model to monthly streamflow data:
using Numerics.Data;
using RMC.BestFit.Models;
using RMC.BestFit.Analyses;
using RMC.BestFit.Estimation;
// Create time series from monthly streamflow data
var startDate = new DateTime(1990, 1, 1);
var ts = new TimeSeries(TimeInterval.OneMonth, startDate, monthlyFlows);
// Create AR(2) model
var model = new AutoRegressive(ts, order: 2, includeIntercept: true)
{
UseDefaultTrainingSteps = false, // Use all data for training
UseJeffreysRuleForScale = true // Non-informative prior on sigma
};
model.TrainingTimeSteps = ts.Count;
// Perform MLE estimation using MaximumLikelihood class
var mle = new MaximumLikelihood(model);
mle.Estimate();
// Display estimated parameters (access via Parameters list)
if (mle.IsEstimated)
{
model.SetParameterValues(mle.BestParameterSet.Values);
// Parameter indices for AR(2) with intercept: [0]=μ, [1]=φ₁, [2]=φ₂, [3]=σ
Console.WriteLine($"Mean (μ): {model.Parameters[0].Value:F2}");
Console.WriteLine($"AR(1) (φ₁): {model.Parameters[1].Value:F4}");
Console.WriteLine($"AR(2) (φ₂): {model.Parameters[2].Value:F4}");
Console.WriteLine($"Sigma (σ): {model.Parameters[3].Value:F4}");
}For Bayesian estimation with uncertainty quantification and forecasting:
// Create analysis object
var analysis = new ARAnalysis(model);
analysis.BayesianAnalysis.Iterations = 10000;
analysis.BayesianAnalysis.WarmupIterations = 5000;
analysis.ForecastingTimeSteps = 12; // Forecast 12 months ahead
// Run MCMC
await analysis.RunAsync();
// Access posterior summaries
var results = analysis.BayesianAnalysis.Results;
Console.WriteLine("\nPosterior Summary:");
Console.WriteLine($"{"Parameter",-12} {"Mean",10} {"Std Dev",10} {"2.5%",10} {"97.5%",10}");
for (int i = 0; i < model.Parameters.Count; i++)
{
var stats = results.ParameterResults[i].SummaryStatistics;
Console.WriteLine($"{model.Parameters[i].Name,-12} {stats.Mean,10:F4} {stats.StandardDeviation,10:F4} " +
$"{stats.LowerCI,10:F4} {stats.UpperCI,10:F4}");
}
// Access forecasts with uncertainty
var forecasts = analysis.AnalysisResults;
Console.WriteLine("\nForecasts:");
Console.WriteLine($"{"Month",-8} {"Forecast",12} {"Lower 95%",12} {"Upper 95%",12}");
for (int h = 0; h < 12; h++)
{
var mode = forecasts.ModeCurve[h];
var lower = forecasts.LowerCurve[h];
var upper = forecasts.UpperCurve[h];
Console.WriteLine($"{h + 1,-8} {mode.Value,12:F1} {lower.Value,12:F1} {upper.Value,12:F1}");
}Choosing the order
- Plot the PACF — it should cut off after lag
$p$ - Start with low orders (1-3) for most applications
- Use information criteria (AIC, BIC) to compare models
- Prefer parsimony — simpler models often forecast better
Common issues:
-
Near unit root (
$\phi_1 \approx 1$ ): Consider differencing instead - Seasonal patterns: PACF may show spikes at seasonal lags; consider seasonal terms or Fourier basis
- Outliers: Can inflate variance estimates; consider robust methods or data cleaning
The moving average model expresses the current value as a linear combination of current and past random shocks (error terms). While AR models capture persistence through lagged observations, MA models capture persistence through the lingering effects of past innovations.
Imagine a river where upstream rainfall events cause flow perturbations that take several days to pass the gauge. Today's flow depends on today's rainfall plus echoes of the past few days' rainfall. An MA model captures this: the current value depends on the current shock plus weighted past shocks.
The "order"
The MA(q) model is defined as [1]:
where:
-
$Y_t$ is the observation at time$t$ -
$\mu$ is the process mean -
$\theta_1, \theta_2, \ldots, \theta_q$ are the MA coefficients -
$\varepsilon_t \stackrel{iid}{\sim} N(0, \sigma^2)$ is white noise
Using the backshift operator:
where
For an MA(q) model with intercept:
-
$\mu$ : Process mean -
$\theta_1, \theta_2, \ldots, \theta_q$ : MA coefficients -
$\sigma$ : Error standard deviation
Total parameters:
For an invertible MA process, all roots of the MA polynomial must lie outside the unit circle [1]:
Simplified conditions:
-
MA(1):
$|\theta_1| < 1$
Invertibility ensures:
- A unique model representation (non-invertible models have equivalent invertible forms)
- The MA process can be written as an infinite-order AR process
- Past shocks can be recovered from observed data
The MA(q) process has distinctive correlation patterns:
| Statistic | Pattern |
|---|---|
| ACF | Cuts off sharply after lag |
| PACF | Exponential decay (or damped sinusoid) |
This ACF cutoff is the key diagnostic for identifying MA order — it's the opposite pattern from AR.
The exact likelihood for MA models requires iterative computation of residuals. Given parameters, residuals are computed recursively:
with initialization
The conditional log-likelihood is:
MA(q) forecasts converge to the mean
-
Short horizon (
$h \leq q$ ): Forecast uses recent shocks
-
Long horizon (
$h > q$ ): Forecast equals the mean
This makes MA models suitable for transient shocks — perturbations that affect the system for exactly
using Numerics.Data;
using RMC.BestFit.Models;
using RMC.BestFit.Analyses;
using RMC.BestFit.Estimation;
// Create MA(2) model
var model = new MovingAverage(ts, order: 2, includeIntercept: true)
{
UseDefaultTrainingSteps = false,
UseJeffreysRuleForScale = true
};
model.TrainingTimeSteps = ts.Count;
// MLE estimation using MaximumLikelihood class
var mle = new MaximumLikelihood(model);
mle.Estimate();
if (mle.IsEstimated)
{
model.SetParameterValues(mle.BestParameterSet.Values);
// Parameter indices for MA(2) with intercept: [0]=μ, [1]=θ₁, [2]=θ₂, [3]=σ
Console.WriteLine($"Mean (μ): {model.Parameters[0].Value:F2}");
Console.WriteLine($"MA(1) (θ₁): {model.Parameters[1].Value:F4}");
Console.WriteLine($"MA(2) (θ₂): {model.Parameters[2].Value:F4}");
Console.WriteLine($"Sigma (σ): {model.Parameters[3].Value:F4}");
}
// Bayesian estimation
var analysis = new MAAnalysis(model);
analysis.BayesianAnalysis.Iterations = 10000;
analysis.BayesianAnalysis.WarmupIterations = 5000;
await analysis.RunAsync();ARIMA (AutoRegressive Integrated Moving Average) combines differencing with ARMA modeling to handle non-stationary series. Many real-world series exhibit trends or stochastic wandering that violates stationarity — ARIMA handles this by differencing the series before applying AR and MA components [1].
Consider a stock price that wanders randomly over time (a "random walk"). The price level is non-stationary, but the daily changes (differences) might be stationary noise. ARIMA works by:
- Differencing the series
$d$ times to achieve stationarity - Fitting an ARMA(p,q) model to the differenced series
- Integrating forecasts back to the original scale
The "I" in ARIMA stands for "Integrated" — the opposite of differencing.
The ARIMA(p,d,q) model is defined as:
where:
-
$(1-L)^d$ is the differencing operator applied$d$ times -
$\Phi(L)$ is the AR polynomial -
$\Theta(L)$ is the MA polynomial
Let
The differencing operation transforms the series:
-
First difference (
$d=1$ ):$\Delta Y_t = Y_t - Y_{t-1}$ — removes linear trend -
Second difference (
$d=2$ ):$\Delta^2 Y_t = \Delta(\Delta Y_t) = Y_t - 2Y_{t-1} + Y_{t-2}$ — removes quadratic trend
Effect on series length: After
Differencing is appropriate when the series shows:
- Trending behavior: Systematic upward or downward movement
- Unit root: ACF decays very slowly (series is very persistent)
- Non-constant mean: Mean appears to change over time
Warning: Over-differencing can introduce artificial patterns. If the differenced series shows negative autocorrelation at lag 1, you may have over-differenced.
For an ARIMA(p,d,q) model:
-
$\mu$ : Mean of the differenced series (drift term if$d > 0$ ) -
$\phi_1, \ldots, \phi_p$ : AR coefficients -
$\theta_1, \ldots, \theta_q$ : MA coefficients -
$\sigma$ : Error standard deviation
Total parameters:
Computed on the differenced series
where
Fitting an ARIMA(1,1,1) model (differenced AR(1) with MA(1) errors):
using Numerics.Data;
using RMC.BestFit.Models;
using RMC.BestFit.Analyses;
using RMC.BestFit.Estimation;
// Create ARIMA(1,1,1) model
var model = new ARIMA(ts)
{
IncludeIntercept = true,
AROrderP = 1,
DiffOrderD = 1,
MAOrderQ = 1,
UseDefaultTrainingSteps = false
};
model.TrainingTimeSteps = ts.Count;
// MLE estimation using MaximumLikelihood class
var mle = new MaximumLikelihood(model);
mle.Estimate();
if (mle.IsEstimated)
{
model.SetParameterValues(mle.BestParameterSet.Values);
// Parameter indices for ARIMA(1,1,1) with intercept: [0]=μ, [1]=φ₁, [2]=θ₁, [3]=σ
Console.WriteLine($"Drift (μ): {model.Parameters[0].Value:F4}");
Console.WriteLine($"AR(1) (φ₁): {model.Parameters[1].Value:F4}");
Console.WriteLine($"MA(1) (θ₁): {model.Parameters[2].Value:F4}");
Console.WriteLine($"Sigma (σ): {model.Parameters[3].Value:F4}");
}
// Bayesian estimation with forecasting
var analysis = new ARIMAAnalysis(model);
analysis.BayesianAnalysis.Iterations = 10000;
analysis.BayesianAnalysis.WarmupIterations = 5000;
analysis.ForecastingTimeSteps = 24; // 24-month forecast
await analysis.RunAsync();| Model | ARIMA Notation | Description |
|---|---|---|
| White noise | ARIMA(0,0,0) | No structure, just noise |
| Random walk | ARIMA(0,1,0) | |
| Random walk with drift | ARIMA(0,1,0) + intercept | |
| AR(p) | ARIMA(p,0,0) | Autoregressive only |
| MA(q) | ARIMA(0,0,q) | Moving average only |
| ARMA(p,q) | ARIMA(p,0,q) | Mixed, no differencing |
ARIMAX extends ARIMA by incorporating exogenous (external) predictor variables. When external factors influence the response — climate indices affecting streamflow, economic indicators affecting demand, upstream flows affecting downstream gauges — ARIMAX captures both the internal dynamics and external effects [3].
Predicting reservoir inflow using only past inflows ignores valuable information: upstream precipitation, snowpack, temperature. ARIMAX includes these external predictors while still modeling the temporal dynamics of inflow itself.
The model separates two sources of predictability:
- Internal dynamics: How the series depends on its own past (AR/MA terms)
- External effects: How external predictors influence the series (regression terms)
The ARIMAX model is:
where:
-
$\gamma(t)$ is an optional deterministic trend -
$\psi(t)$ is an optional seasonal component (Fourier series) -
$X_{k,t-b}$ is the$k$ -th exogenous variable at lag$b$ -
$\beta_k$ is the regression coefficient for covariate$k$
RMC-BestFit supports polynomial trend functions:
| Trend Type | Formula | Parameters |
|---|---|---|
| None | 0 | |
| Linear | 1 | |
| Quadratic | 2 | |
| Cubic | 3 |
Warning: Including both differencing (
When IncludeSeasonality = true, a Fourier basis captures periodic patterns:
where
- Monthly data:
$S = 12$ - Quarterly data:
$S = 4$ - Daily data:
$S = 365$
This captures seasonal patterns without requiring seasonal differencing or multiplicative seasonal ARIMA.
Covariates enter with an optional lag
-
$b = 0$ : Contemporaneous effect —$X_t$ affects$Y_t$ -
$b > 0$ : Lagged effect —$X_{t-b}$ affects$Y_t$
Covariate extension for forecasting:
When forecasting beyond available covariate data, RMC-BestFit offers:
None: No extension (throws exception if insufficient data)BlockBootstrap: Resamples blocks of covariate data, preserving temporal autocorrelationKNN: K-nearest neighbors imputation, preserving local covariate structure
Modeling streamflow with precipitation as a covariate:
using Numerics.Data;
using RMC.BestFit.Models;
using RMC.BestFit.Analyses;
using RMC.BestFit.Estimation;
// Time series of streamflow (response) and precipitation (covariate)
var flowTS = new TimeSeries(TimeInterval.OneMonth, startDate, monthlyFlows);
var precipTS = new TimeSeries(TimeInterval.OneMonth, startDate, monthlyPrecip);
// Create ARIMAX model
var model = new ARIMAX(flowTS)
{
IncludeIntercept = true,
AROrderP = 1,
DiffOrderD = 0,
MAOrderQ = 0,
XOrderB = 0, // Contemporaneous effect
IncludeSeasonality = true, // Capture seasonal patterns
UseDefaultTrainingSteps = false
};
model.TrainingTimeSteps = flowTS.Count;
// Add covariate(s)
model.SetCovariates(new List<TimeSeries> { precipTS });
// MLE estimation using MaximumLikelihood class
var mle = new MaximumLikelihood(model);
mle.Estimate();
if (mle.IsEstimated)
{
model.SetParameterValues(mle.BestParameterSet.Values);
// Parameter order depends on model configuration
// For ARIMAX with intercept, AR(1), seasonality, and 1 covariate:
// Parameters are ordered: intercept, AR coefficients, seasonal terms, covariate betas, sigma
Console.WriteLine("Estimated parameters:");
for (int i = 0; i < model.Parameters.Count; i++)
{
Console.WriteLine($" {model.Parameters[i].Name}: {model.Parameters[i].Value:F4}");
}
}
// Bayesian estimation
var analysis = new ARIMAXAnalysis(model);
analysis.BayesianAnalysis.Iterations = 10000;
analysis.BayesianAnalysis.WarmupIterations = 5000;
analysis.ForecastingTimeSteps = 12;
analysis.ARIMAX.CovariateExtension = ARIMAX.CovariateExtensionMethod.BlockBootstrap;
await analysis.RunAsync();All time series models in RMC-BestFit support Bayesian MCMC estimation through their corresponding analysis classes. Bayesian estimation provides complete uncertainty quantification — not just point estimates, but full probability distributions for parameters and forecasts [4].
MLE gives you the "best" parameter values, but doesn't tell you how uncertain those estimates are. Bayesian estimation provides:
- Parameter uncertainty: Full posterior distributions, not just point estimates
- Forecast uncertainty: Prediction intervals that account for parameter uncertainty
- Probabilistic inference: Answer questions like "What's the probability AR(1) > 0.5?"
- Prior incorporation: Include expert knowledge when appropriate
The default sampler is DEMCzs (Differential Evolution MCMC with snooker update) [5], which is:
- Self-tuning: No manual proposal distribution tuning required
- Robust: Handles multimodal posteriors and correlated parameters
- Parallelizable: Multiple chains run simultaneously
The posterior distribution combines the likelihood with prior information:
where:
-
$\mathcal{L}(Y | \theta)$ is the likelihood function -
$\pi(\theta)$ is the prior (product of individual parameter priors)
RMC-BestFit uses weakly informative default priors:
| Parameter | Default Prior | Rationale |
|---|---|---|
| Bounds from data range | ||
| Allows non-stationary exploration | ||
| Allows non-invertible exploration | ||
| Positive scale parameter |
Jeffreys' prior for scale:
When UseJeffreysRuleForScale = true, the scale parameter receives the non-informative Jeffreys' prior:
This adds
Always check MCMC convergence before using results [4]:
var results = analysis.BayesianAnalysis.Results;
Console.WriteLine($"{"Parameter",-12} {"R-hat",8} {"ESS",8} {"Converged?",12}");
for (int i = 0; i < model.Parameters.Count; i++)
{
var stats = results.ParameterResults[i].SummaryStatistics;
bool converged = stats.Rhat < 1.1 && stats.ESS > 100;
Console.WriteLine($"{model.Parameters[i].Name,-12} {stats.Rhat,8:F3} {stats.ESS,8:F0} {(converged ? "Yes" : "NO"),12}");
}Diagnostic thresholds:
- R-hat (Gelman-Rubin): Should be < 1.1 (preferably < 1.05)
- ESS (Effective Sample Size): Should be > 100 for reliable inference
If convergence fails:
- Increase
IterationsandWarmupIterations - Check for model misspecification
- Consider reparameterization
- Examine trace plots for pathological behavior
Fitting a model is only the beginning. Proper diagnostics verify that the model adequately captures the data structure.
If the model is correctly specified, residuals should be white noise:
var mapParams = analysis.BayesianAnalysis.Results.MAP.Values;
var residuals = model.Residuals(mapParams);
// Check residual statistics
double mean = residuals.Average();
double variance = residuals.Select(r => r * r).Average() - mean * mean;
// Get sigma from the last parameter (always the scale parameter)
double sigma = mapParams.Last();
Console.WriteLine($"Residual mean: {mean:F4} (should be ≈ 0)");
Console.WriteLine($"Residual std: {Math.Sqrt(variance):F4} (should be ≈ σ = {sigma:F4})");Residuals should show no significant autocorrelation:
// Compute residual autocorrelations
var residualAcf = Statistics.AutoCorrelation(residuals, maxLag: 20);
Console.WriteLine("Lag ACF");
for (int lag = 1; lag <= 20; lag++)
{
double bound = 1.96 / Math.Sqrt(residuals.Length); // 95% confidence bound
string sig = Math.Abs(residualAcf[lag]) > bound ? " *" : "";
Console.WriteLine($"{lag,3} {residualAcf[lag],7:F3}{sig}");
}Significant autocorrelation (marked with *) suggests the model is missing structure. Consider:
- Increasing AR or MA order
- Adding seasonal terms
- Checking for outliers or structural breaks
The Ljung-Box test formally tests for residual autocorrelation:
Under the null hypothesis of no autocorrelation,
// Ljung-Box test (using Numerics.Data.Statistics)
int H = 20; // Number of lags to test
double Q = Statistics.LjungBoxStatistic(residuals, H);
int df = H - model.Order; // Degrees of freedom
double pValue = 1 - new ChiSquared(df).CDF(Q);
Console.WriteLine($"Ljung-Box Q({H}): {Q:F2}");
Console.WriteLine($"p-value: {pValue:F4}");
Console.WriteLine(pValue < 0.05 ? "Warning: Significant residual autocorrelation" : "OK: No significant autocorrelation");Residuals should be approximately normally distributed:
// Jarque-Bera test for normality
double skewness = Statistics.Skewness(residuals);
double kurtosis = Statistics.Kurtosis(residuals); // Excess kurtosis
double jb = (residuals.Length / 6.0) * (skewness * skewness + kurtosis * kurtosis / 4.0);
Console.WriteLine($"Skewness: {skewness:F3} (should be ≈ 0)");
Console.WriteLine($"Excess kurtosis: {kurtosis:F3} (should be ≈ 0)");
Console.WriteLine($"Jarque-Bera: {jb:F2}");Non-normality may indicate:
- Outliers (high kurtosis)
- Asymmetric shocks (non-zero skewness)
- Need for transformation (log, Box-Cox)
Many hydrologic variables (streamflow, precipitation, concentrations) are positively skewed with variance that increases with the mean. Transformations can stabilize variance and improve normality.
| Transform | Formula | Inverse | Use Case |
|---|---|---|---|
| None | Data already Gaussian | ||
| Logarithmic | Positive, right-skewed data | ||
| Box-Cox | General power transform |
The most common transformation for hydrologic data:
var model = new AutoRegressive(ts, order: 2)
{
TransformType = RMC.BestFit.Models.Transform.Logarithmic
};Interpretation: The model is fit in log-space, so AR coefficients describe multiplicative dynamics. A forecast of $\hat{Y}^_{t+1}$ in log-space corresponds to $\exp(\hat{Y}^_{t+1})$ in original units.
Warning: Requires all values to be positive. Add a small constant if zeros are present.
The Box-Cox family includes log (
var model = new AutoRegressive(ts, order: 2)
{
TransformType = RMC.BestFit.Models.Transform.BoxCox
};
model.SetTransformParameters(lambda1: 0.5); // Square root transformWhen using transformations, the likelihood must include the log-Jacobian for proper inference. For Box-Cox:
RMC-BestFit handles this automatically.
Choosing the right model orders (p, d, q) is both art and science. Here's a systematic approach.
The classic approach [1]:
- Identification: Use ACF/PACF patterns to guess orders
- Estimation: Fit the model
- Diagnostics: Check residuals
- Refinement: Adjust orders if diagnostics fail
| Pattern | Suggested Model |
|---|---|
| ACF: exponential decay; PACF: cuts off at |
AR(p) |
| ACF: cuts off at |
MA(q) |
| ACF: exponential decay; PACF: exponential decay | ARMA(p,q) |
| ACF: very slow decay | Differencing needed (ARIMA) |
Compare models using penalized likelihood:
-
AIC:
$-2\ell(\hat{\theta}) + 2k$ — favors predictive accuracy -
BIC:
$-2\ell(\hat{\theta}) + k\log(n)$ — stronger penalty, favors parsimony
Lower values are better. BIC typically selects simpler models than AIC.
// Compare AR(1), AR(2), AR(3)
var results = new List<(int p, double AIC, double BIC)>();
foreach (int p in new[] { 1, 2, 3 })
{
var model = new AutoRegressive(ts, order: p, includeIntercept: true)
{
UseDefaultTrainingSteps = false
};
model.TrainingTimeSteps = ts.Count;
model.Estimate();
int k = p + 2; // Number of parameters
int n = ts.Count - p; // Effective sample size
double logLik = model.DataLogLikelihood(model.Parameters.Select(x => x.Value).ToArray());
double aic = -2 * logLik + 2 * k;
double bic = -2 * logLik + k * Math.Log(n);
results.Add((p, aic, bic));
Console.WriteLine($"AR({p}): AIC = {aic:F1}, BIC = {bic:F1}");
}
var bestAIC = results.OrderBy(r => r.AIC).First();
var bestBIC = results.OrderBy(r => r.BIC).First();
Console.WriteLine($"\nBest by AIC: AR({bestAIC.p})");
Console.WriteLine($"Best by BIC: AR({bestBIC.p})");| Scenario | Recommendation |
|---|---|
| Stationary series, gradual ACF decay | Start with AR(1) or AR(2) |
| Stationary series, sharp ACF cutoff | Try MA(1) or MA(2) |
| Trending series | Difference first (ARIMA with d=1) |
| Seasonal patterns | Use ARIMAX with IncludeSeasonality = true |
| External predictors available | Use ARIMAX |
| Uncertain about order | Prefer parsimony — simpler models often forecast better |
Forecast monthly streamflow using historical data:
using Numerics.Data;
using RMC.BestFit.Models;
using RMC.BestFit.Analyses;
using RMC.BestFit.Estimation;
// Load historical monthly streamflow (log-transformed for variance stabilization)
var ts = new TimeSeries(TimeInterval.OneMonth, new DateTime(1980, 1, 1), logMonthlyFlows);
// Fit AR(2) model — streamflow often shows strong lag-1 persistence
var model = new AutoRegressive(ts, order: 2, includeIntercept: true)
{
UseDefaultTrainingSteps = false,
UseJeffreysRuleForScale = true
};
model.TrainingTimeSteps = ts.Count;
// Bayesian estimation with 12-month forecast
var analysis = new ARAnalysis(model);
analysis.BayesianAnalysis.Iterations = 15000;
analysis.BayesianAnalysis.WarmupIterations = 7500;
analysis.ForecastingTimeSteps = 12;
await analysis.RunAsync();
// Check convergence
var results = analysis.BayesianAnalysis.Results;
bool converged = results.ParameterResults.All(p => p.SummaryStatistics.Rhat < 1.1);
Console.WriteLine($"Converged: {converged}");
// Display forecasts (transform back from log scale)
Console.WriteLine("\n12-Month Forecast (original units):");
Console.WriteLine($"{"Month",-8} {"Median",10} {"5th %ile",10} {"95th %ile",10}");
var forecasts = analysis.AnalysisResults;
for (int h = 0; h < 12; h++)
{
double median = Math.Exp(forecasts.ModeCurve[h].Value);
double lower = Math.Exp(forecasts.LowerCurve[h].Value);
double upper = Math.Exp(forecasts.UpperCurve[h].Value);
Console.WriteLine($"{h + 1,-8} {median,10:F0} {lower,10:F0} {upper,10:F0}");
}Predict streamflow using ENSO (El Niño Southern Oscillation) as a covariate:
// Monthly streamflow and ENSO index (Niño 3.4)
var flowTS = new TimeSeries(TimeInterval.OneMonth, startDate, monthlyFlows);
var ensoTS = new TimeSeries(TimeInterval.OneMonth, startDate, nino34Index);
// ARIMAX: AR(1) + ENSO effect + seasonality
var model = new ARIMAX(flowTS)
{
IncludeIntercept = true,
AROrderP = 1,
MAOrderQ = 0,
DiffOrderD = 0,
XOrderB = 1, // ENSO affects flow with 1-month lag
IncludeSeasonality = true,
UseDefaultTrainingSteps = false
};
model.TrainingTimeSteps = flowTS.Count;
model.SetCovariates(new List<TimeSeries> { ensoTS });
// MLE estimation
var mle = new MaximumLikelihood(model);
mle.Estimate();
if (mle.IsEstimated)
{
model.SetParameterValues(mle.BestParameterSet.Values);
// Find the beta coefficient for ENSO in the parameters list
// Parameter order: intercept, AR coeffs, seasonal terms, beta coeffs, sigma
Console.WriteLine("Estimated parameters:");
for (int i = 0; i < model.Parameters.Count; i++)
{
if (model.Parameters[i].Name.Contains("Beta"))
{
Console.WriteLine($"ENSO effect ({model.Parameters[i].Name}): {model.Parameters[i].Value:F3}");
Console.WriteLine($"Interpretation: A 1-unit increase in Niño 3.4 is associated with");
Console.WriteLine($" a {model.Parameters[i].Value:F1} unit change in streamflow");
}
}
}
// Bayesian estimation for uncertainty
var analysis = new ARIMAXAnalysis(model);
analysis.BayesianAnalysis.Iterations = 15000;
analysis.BayesianAnalysis.WarmupIterations = 7500;
await analysis.RunAsync();
// ENSO effect uncertainty
var ensoStats = analysis.BayesianAnalysis.Results.ParameterResults
.First(p => p.Name?.Contains("Beta") ?? false).SummaryStatistics;
Console.WriteLine($"ENSO effect: {ensoStats.Mean:F3} (95% CI: [{ensoStats.LowerCI:F3}, {ensoStats.UpperCI:F3}])");Test whether a series needs differencing:
// Plot ACF to check for slow decay
var acf = Statistics.AutoCorrelation(data, maxLag: 20);
Console.WriteLine("Lag ACF (slow decay suggests non-stationarity)");
for (int lag = 1; lag <= 10; lag++)
{
Console.WriteLine($"{lag,3} {acf[lag],7:F3}");
}
// If ACF decays very slowly, try differencing
var diffData = new double[data.Length - 1];
for (int t = 0; t < diffData.Length; t++)
{
diffData[t] = data[t + 1] - data[t];
}
var diffAcf = Statistics.AutoCorrelation(diffData, maxLag: 20);
Console.WriteLine("\nAfter differencing:");
Console.WriteLine("Lag ACF");
for (int lag = 1; lag <= 10; lag++)
{
Console.WriteLine($"{lag,3} {diffAcf[lag],7:F3}");
}
// Fit ARIMA(1,1,0) if differencing helped
var model = new ARIMA(ts)
{
AROrderP = 1,
DiffOrderD = 1,
MAOrderQ = 0
};By default, RMC-BestFit uses 80% of data for training, reserving 20% for out-of-sample validation. To use all data (matching R's arima() behavior):
model.UseDefaultTrainingSteps = false;
model.TrainingTimeSteps = timeSeries.Count;When to use each approach:
- Default (80/20 split): Good for model validation, prevents overfitting
- All data: Use when validating against other software or when sample size is limited
All time series models have been validated against R's arima() function using the Box-Jenkins airline passenger dataset [1].
The airline passenger dataset contains 144 monthly observations (1949-1960) of international airline passengers. It's log-transformed for variance stabilization and exhibits both trend and seasonality.
| Model | Parameter | R Value | RMC-BestFit | Within Tolerance? |
|---|---|---|---|---|
| AR(1) | φ₁ | 0.9646 | ≈0.96 | Yes (10%) |
| AR(1) | μ | 5.5392 | ≈5.54 | Yes (10%) |
| MA(1) | θ₁ | 0.4018 | ≈0.40 | Yes (10%) |
| ARIMA(1,1,1) | φ₁ | 0.8822 | ≈0.88 | Yes (10%) |
Bayesian estimates use 15% tolerance to account for MCMC variability.
-
Gaussian errors:
$\varepsilon_t \sim N(0, \sigma^2)$ - Constant variance: Homoscedasticity over time
- Linear dynamics: Current value is a linear function of past values/shocks
- No structural breaks: Parameters are constant over time
| Violation | Symptom | Remedy |
|---|---|---|
| Non-normality | Heavy tails, skewness | Transformation (log, Box-Cox) |
| Heteroscedasticity | Variance changes with level | Transformation or GARCH models |
| Nonlinearity | Residual patterns | Threshold models, neural nets |
| Structural break | Sudden parameter change | Split sample, regime-switching |
-
Maximum order constraints:
$p, q \leq 10$ ;$d \leq 3$ - Minimum data requirements: At least 10 observations
- No seasonal ARIMA: Use Fourier basis via ARIMAX instead
- No automatic model selection: User specifies orders (use AIC/BIC for guidance)
- Linear models only: Nonlinear dynamics require other approaches
| Class | Key Properties | Key Methods |
|---|---|---|
AutoRegressive |
Order, TimeSeries, TransformType, TrainingTimeSteps, Parameters |
Residuals(), DataLogLikelihood(), SetParameterValues() |
MovingAverage |
Order, TimeSeries, TransformType, TrainingTimeSteps, Parameters |
Residuals(), DataLogLikelihood(), SetParameterValues() |
ARIMA |
AROrderP, DiffOrderD, MAOrderQ, IncludeIntercept, Parameters |
Residuals(), DataLogLikelihood(), SetParameterValues() |
ARIMAX |
AROrderP, DiffOrderD, MAOrderQ, XOrderB, IncludeSeasonality, CovariateExtension |
SetCovariates(), Residuals(), DataLogLikelihood() |
Note: MLE estimation is performed using the MaximumLikelihood class:
var mle = new MaximumLikelihood(model);
mle.Estimate();
if (mle.IsEstimated)
model.SetParameterValues(mle.BestParameterSet.Values);| Class | Key Properties | Key Methods |
|---|---|---|
ARAnalysis |
AutoRegressive, BayesianAnalysis, AnalysisResults, ForecastingTimeSteps |
RunAsync(), CancelAnalysis(), Validate() |
MAAnalysis |
MovingAverage, BayesianAnalysis, AnalysisResults, ForecastingTimeSteps |
RunAsync(), CancelAnalysis(), Validate() |
ARIMAAnalysis |
ARIMA, BayesianAnalysis, AnalysisResults, ForecastingTimeSteps |
RunAsync(), CancelAnalysis(), Validate() |
ARIMAXAnalysis |
ARIMAX, BayesianAnalysis, AnalysisResults, ForecastingTimeSteps, CovariateExtensionMethod |
RunAsync(), CancelAnalysis(), Validate() |
Primary source paths: src/RMC.BestFit/Models/TimeSeries, src/RMC.BestFit/Analyses/TimeSeries, src/RMC.BestFit/Estimation/BayesianAnalysis.cs, and src/RMC.BestFit/Estimation/MaximumLikelihood.cs.
[1] G. E. P. Box, G. M. Jenkins, G. C. Reinsel, and G. M. Ljung, Time Series Analysis: Forecasting and Control, 5th ed., Hoboken, NJ: Wiley, 2015.
[2] P. J. Brockwell and R. A. Davis, Introduction to Time Series and Forecasting, 3rd ed., New York: Springer, 2016.
[3] J. D. Hamilton, Time Series Analysis, Princeton, NJ: Princeton University Press, 1994.
[4] A. Gelman, J. B. Carlin, H. S. Stern, D. B. Dunson, A. Vehtari, and D. B. Rubin, Bayesian Data Analysis, 3rd ed., Boca Raton, FL: Chapman and Hall/CRC, 2013.
[5] C. J. F. ter Braak and J. A. Vrugt, "Differential Evolution Markov Chain with snooker updater and fewer chains," Statistics and Computing, vol. 18, no. 4, pp. 435-446, 2008.
[6] R. Prado and M. West, Time Series: Modeling, Computation, and Inference, Boca Raton, FL: Chapman and Hall/CRC, 2010.
[7] R. J. Hyndman and Y. Khandakar, "Automatic time series forecasting: The forecast package for R," Journal of Statistical Software, vol. 27, no. 3, pp. 1-22, 2008.
[8] C. Chatfield, The Analysis of Time Series: An Introduction, 6th ed., Boca Raton, FL: Chapman and Hall/CRC, 2004.
<- Previous: Rating Curve | Back to Index | Next: Spatial Extremes ->
Last updated: 2026-02-01 RMC-BestFit v2.0