Many real-world decisions depend on how a metric evolves over time: demand, temperature, web traffic, or energy usage. ARIMA is one of the most widely used forecasting methods because it handles two key features of time series that ordinary regression cannot: **autocorrelation** (today's value is correlated with yesterday's) and **non-stationarity** (the mean or variance shifts over time). ARIMA stands for Autoregressive Integrated Moving Average — three components that work together to model the temporal structure. Once fit, the model produces point forecasts with confidence intervals that widen appropriately as you forecast further into the future. For data with strong seasonal patterns (weekly, monthly), [SARIMAX](/tutorials/statsmodels-sarimax-time-series-forecasting) adds a seasonal layer on top.
## What ARIMA means
ARIMA is parameterized by three integers `(p, d, q)`, each controlling a different part of the model.
- **AR (Autoregressive)** — `p`: the number of past values used to predict the next one. AR(2) means the model uses the two most recent values.
- **I (Integrated)** — `d`: the number of times the series is differenced to make it stationary. `d=1` computes `x_t - x_{t-1}`; `d=0` uses the raw series.
- **MA (Moving Average)** — `q`: the number of past forecast errors included in the prediction. This helps the model correct for recent mispredictions.
## Creating the dataset
The daily minimum temperatures dataset covers Melbourne from 1981 to 1990 — a long enough series to show autocorrelation and seasonality clearly.
import requests
import pandas as pd
url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/daily-min-temperatures.csv"
response = requests.get(url, timeout=30)
response.raise_for_status()
with open("daily-min-temperatures.csv", "w", encoding="utf-8") as f:
f.write(response.text)
df = pd.read_csv("daily-min-temperatures.csv")
df["Date"] = pd.to_datetime(df["Date"])
df = df.set_index("Date")
series = df["Temp"].asfreq("D")
print(series.head())
print(series.index.freq)
- `pd.to_datetime(df["Date"])` converts string dates to proper datetime objects — required for time-aware indexing and frequency setting.
- `.asfreq("D")` sets the frequency to daily so statsmodels can correctly interpret forecast steps and handle any gaps.
- Saving locally once means all later blocks can load from disk without repeating the network request.
## Visualizing the time series
Before choosing ARIMA parameters, always plot the series. The shape reveals whether differencing is needed, whether seasonality is present, and whether the variance is growing.
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("daily-min-temperatures.csv")
df["Date"] = pd.to_datetime(df["Date"])
df = df.set_index("Date")
series = df["Temp"].asfreq("D")
plt.figure(figsize=(10, 4))
plt.plot(series.index, series.values, linewidth=1)
plt.title("Daily Minimum Temperatures")
plt.xlabel("Date")
plt.ylabel("Temperature")
plt.tight_layout()
plt.show()- A roughly stable mean level over time (no persistent upward or downward drift) suggests `d=0` may be sufficient — differencing would be needed if the mean drifts. - A repeating annual cycle in the plot indicates seasonality — ARIMA alone won't capture it, and [SARIMAX](/tutorials/statsmodels-sarimax-time-series-forecasting) would be a better choice. - A visual inspection takes seconds and can save you from fitting a model on fundamentally misspecified data. ## Differencing to reduce non-stationarity ARIMA assumes the differenced series is stationary — meaning its statistical properties don't change over time. First differencing removes linear trends and is the most common transformation before fitting.
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("daily-min-temperatures.csv")
df["Date"] = pd.to_datetime(df["Date"])
df = df.set_index("Date")
series = df["Temp"].asfreq("D")
diff_series = series.diff().dropna()
plt.figure(figsize=(10, 4))
plt.plot(diff_series.index, diff_series.values, linewidth=1, color="tab:orange")
plt.title("First-Differenced Temperature Series")
plt.xlabel("Date")
plt.ylabel("Differenced Temperature")
plt.tight_layout()
plt.show()
- `series.diff()` computes `x_t - x_{t-1}` for each row — this removes a constant trend, making the mean roughly zero and more stable over time.
- `.dropna()` removes the first row, which has no prior value to difference against.
- If the differenced series still shows a trend, a second difference (`d=2`) may be needed — though `d>2` is rarely used in practice.
## Fitting an ARIMA model
With the data prepared and a rough sense of the series structure, fitting ARIMA is a two-line call.
import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
df = pd.read_csv("daily-min-temperatures.csv")
df["Date"] = pd.to_datetime(df["Date"])
df = df.set_index("Date")
series = df["Temp"].asfreq("D")
model = ARIMA(series, order=(2, 1, 2))
results = model.fit()
print(results.summary())- `order=(2, 1, 2)` means: use the 2 most recent values for AR, apply one difference for stationarity, and use the 2 most recent forecast errors for MA. - The summary reports AIC and BIC for model comparison (lower is better), coefficient estimates, and a Ljung-Box test for whether residuals are white noise. - Significant AR or MA coefficients confirm those terms are capturing real autocorrelation structure in the series. ## Train-test forecasting Evaluating on a held-out test set gives an honest picture of how the model performs on unseen data.
import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
df = pd.read_csv("daily-min-temperatures.csv")
df["Date"] = pd.to_datetime(df["Date"])
df = df.set_index("Date")
series = df["Temp"].asfreq("D")
train = series.iloc[:-365]
test = series.iloc[-365:]
model = ARIMA(train, order=(2, 1, 2))
results = model.fit()
forecast_res = results.get_forecast(steps=len(test))
forecast_df = forecast_res.summary_frame(alpha=0.05)
forecast_df.index = test.index
forecast_df["actual"] = test.values
print(forecast_df[["mean", "mean_ci_lower", "mean_ci_upper", "actual"]].head())- `series.iloc[:-365]` holds out the final year — the model is fit on all earlier data and then asked to forecast blind. - `get_forecast(steps=365)` projects 365 steps ahead; `.summary_frame(alpha=0.05)` returns the point forecast and 95% confidence interval bounds. - Adding `actual` as a column lets you compute forecast errors directly from the DataFrame for quantitative evaluation. ## Visualizing forecast vs actual Overlaying the forecast on the actual test period shows immediately whether the model captures the level and trend, and whether the confidence intervals are appropriately sized.
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.arima.model import ARIMA
df = pd.read_csv("daily-min-temperatures.csv")
df["Date"] = pd.to_datetime(df["Date"])
df = df.set_index("Date")
series = df["Temp"].asfreq("D")
train = series.iloc[:-365]
test = series.iloc[-365:]
model = ARIMA(train, order=(2, 1, 2))
results = model.fit()
forecast_res = results.get_forecast(steps=len(test))
forecast_df = forecast_res.summary_frame(alpha=0.05)
forecast_df.index = test.index
plt.figure(figsize=(11, 5))
plt.plot(train.index[-500:], train.values[-500:], label="Train (recent)", alpha=0.7)
plt.plot(test.index, test.values, label="Actual", color="black")
plt.plot(forecast_df.index, forecast_df["mean"], label="Forecast", color="tab:blue")
plt.fill_between(
forecast_df.index,
forecast_df["mean_ci_lower"],
forecast_df["mean_ci_upper"],
color="tab:blue",
alpha=0.2,
label="95% CI",
)
plt.title("ARIMA Forecast vs Actual")
plt.xlabel("Date")
plt.ylabel("Temperature")
plt.legend()
plt.tight_layout()
plt.show()- `train.index[-500:]` shows only the most recent 500 training points — displaying the full 3,000+ day training history would compress the forecast period visually. - Confidence bands that widen sharply over the forecast horizon indicate the model loses confidence quickly — expected for long-range forecasts of a highly variable daily series. - If the forecast diverges from the actual series in a consistent direction (always above or always below), the model may be missing a trend or seasonal component. ### Conclusion ARIMA is a strong baseline for time series with autocorrelation and trend. The `(p, d, q)` parameters control how much of the past the model uses — start with `(1, 1, 1)` as a baseline, inspect the summary, and adjust based on residual diagnostics. For monthly or quarterly data with clear seasonal patterns, upgrade to [SARIMAX](/tutorials/statsmodels-sarimax-time-series-forecasting).