Time series forecasting is fundamentally different from cross-sectional prediction. The observations are ordered in time and autocorrelated — what happened last month is correlated with what happens this month. ARIMA (Autoregressive Integrated Moving Average) is the workhorse model for this setting.
This tutorial uses CreateARIMAModel to build a monthly revenue forecast from stationarity diagnostics through to a 6-month-ahead prediction.
Prerequisites
- Python 3.9+
analysistoolboxinstalled (pip install analysistoolbox)- Familiarity with the concept of autocorrelation
The three components of ARIMA
ARIMA(p, d, q) has three parameters:
| Parameter | Name | What it captures |
|---|---|---|
| p | AR order | Autocorrelation: how many past values predict the current value |
| d | Integration | Trend: how many times to difference the series to make it stationary |
| q | MA order | Moving average: how many past forecast errors predict the current value |
A stationary time series has constant mean and variance over time. ARIMA requires stationarity — that's why the "I" (integration / differencing) is there. If your series has a trend (e.g., revenue growing every year), you difference it once (d=1) to remove the trend before fitting the AR and MA components.
Step 1: Build a synthetic time series
import pandas as pd
import numpy as np
np.random.seed(42)
# 48 months of monthly revenue with trend + seasonality + noise
months = pd.date_range(start='2020-01', periods=48, freq='MS')
trend = np.linspace(100_000, 180_000, 48)
seasonal = 20_000 * np.sin(2 * np.pi * np.arange(48) / 12)
noise = np.random.normal(0, 8_000, 48)
df = pd.DataFrame({
'month': months,
'revenue': (trend + seasonal + noise).round(-2),
})
print(df.tail())
print(f"\nRevenue range: ${df['revenue'].min():,.0f} – ${df['revenue'].max():,.0f}")
Step 2: Diagnostic run — check for stationarity
Before fitting, let CreateARIMAModel test whether your series needs differencing:
from analysistoolbox.predictive_analytics import CreateARIMAModel
# Diagnostic run: show ACF/PACF plots and stationarity test
model_diag = CreateARIMAModel(
dataframe=df,
outcome_column_name='revenue',
time_column_name='month',
test_for_stationarity=True,
show_acf_pacf_plots=True,
show_model_results=True,
plot_training_and_test_performance=False,
plot_residuals=False,
)
The ADF (Augmented Dickey-Fuller) test has a null hypothesis that the series is non-stationary. If the p-value is above 0.05, you fail to reject non-stationarity — set differencing_periods=1 to difference the series. Look at the ACF plot: slow geometric decay in autocorrelations indicates a non-stationary series and confirms you need differencing.
Step 3: Read the ACF and PACF plots
These two plots tell you which ARIMA order to use:
- PACF (Partial Autocorrelation Function): spikes at lag
ksuggest AR orderp = k. If PACF has one significant spike at lag 1 and cuts off, usep=1. - ACF (Autocorrelation Function): spikes at lag
ksuggest MA orderq = k. If ACF decays gradually, the MA component is handling it; if it cuts off at lag 2, useq=2.
For our synthetic data with trend, you'd expect:
- Non-stationarity (ADF fails) → set
differencing_periods=1 - Seasonal pattern at lag 12 in the ACF
Step 4: Fit the model
# Fit ARIMA(1, 1, 1) — autoregressive, differenced once, moving average
model = CreateARIMAModel(
dataframe=df,
outcome_column_name='revenue',
time_column_name='month',
lookback_periods=1, # AR(1)
differencing_periods=1, # I(1) — difference once for stationarity
lag_periods=1, # MA(1)
test_size=0.20, # hold out last ~10 months for evaluation
test_for_stationarity=True,
show_acf_pacf_plots=False,
show_model_results=True,
plot_residuals=True,
plot_training_and_test_performance=True,
)
The plot_training_and_test_performance=True chart overlays actual vs. predicted values on the test set. Look for:
- Do predictions track the trend direction?
- Are errors random (good) or systematic (bad — indicates a missing component)?
The plot_residuals=True chart shows the residuals. They should look like white noise — no patterns, roughly normally distributed around zero.
Step 5: Forecast 6 months ahead
# Forecast the next 6 months
forecast = model.forecast(steps=6)
future_months = pd.date_range(
start=df['month'].max() + pd.offsets.MonthBegin(),
periods=6,
freq='MS'
)
forecast_df = pd.DataFrame({
'month': future_months,
'forecast': forecast.round(-2),
})
print("\n=== 6-Month Revenue Forecast ===")
print(forecast_df.to_string(index=False))
Step 6: Add confidence intervals
The ARIMA model also provides prediction intervals, which show the range of plausible outcomes:
import matplotlib.pyplot as plt
forecast_result = model.get_forecast(steps=6)
ci = forecast_result.conf_int(alpha=0.10) # 90% confidence interval
fig, ax = plt.subplots(figsize=(12, 5))
# Historical data
ax.plot(df['month'], df['revenue'], label='Actual', color='#2563EB')
# Forecast
ax.plot(future_months, forecast, label='Forecast', color='#D97706', linestyle='--')
ax.fill_between(
future_months,
ci.iloc[:, 0],
ci.iloc[:, 1],
alpha=0.2, color='#D97706', label='90% CI'
)
ax.set_title('Monthly Revenue: Actual and 6-Month Forecast')
ax.set_ylabel('Revenue ($)')
ax.legend()
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'${x/1000:.0f}K'))
plt.tight_layout()
plt.show()
Confidence intervals widen as you forecast further into the future — this is correct behavior. Anyone who shows you a time series forecast without widening intervals is either using a model that doesn't support them or hiding uncertainty from you. The 90% CI means: if the model's assumptions hold, the true value should fall within this band 90% of the time. It does NOT mean the model is 90% accurate — it means the model is 90% calibrated about its own uncertainty.
Common mistakes to avoid
Mistake 1: Forgetting seasonality. Our data has a 12-month seasonal cycle. A plain ARIMA model won't capture it. Use SARIMA (Seasonal ARIMA) by setting lookback_periods, differencing_periods, and lag_periods for the seasonal components as well.
Mistake 2: Using test data to tune hyperparameters. If you pick p, d, q by trying different values and checking which minimizes test-set error, you've effectively trained on the test set. Use the ACF/PACF plots or AIC/BIC from show_model_results=True to select the order, then evaluate once on the held-out test set.
Mistake 3: Forecasting too far out. ARIMA uncertainty compounds exponentially. A 6-month horizon for monthly data is reasonable; a 24-month horizon usually isn't, unless you have strong seasonality anchoring the long-run behavior.
Full code
import pandas as pd
import numpy as np
from analysistoolbox.predictive_analytics import CreateARIMAModel
np.random.seed(42)
months = pd.date_range(start='2020-01', periods=48, freq='MS')
trend = np.linspace(100_000, 180_000, 48)
seasonal = 20_000 * np.sin(2 * np.pi * np.arange(48) / 12)
noise = np.random.normal(0, 8_000, 48)
df = pd.DataFrame({
'month': months,
'revenue': (trend + seasonal + noise).round(-2),
})
# Fit ARIMA(1, 1, 1)
model = CreateARIMAModel(
dataframe=df,
outcome_column_name='revenue',
time_column_name='month',
lookback_periods=1,
differencing_periods=1,
lag_periods=1,
test_size=0.20,
test_for_stationarity=True,
show_acf_pacf_plots=True,
show_model_results=True,
plot_residuals=True,
plot_training_and_test_performance=True,
)
# 6-month forecast
forecast = model.forecast(steps=6)
future_months = pd.date_range(
start=df['month'].max() + pd.offsets.MonthBegin(),
periods=6, freq='MS'
)
print(pd.DataFrame({'month': future_months, 'forecast': forecast.round(-2)}).to_string(index=False))