Autoregressive Integrated Moving Average (ARIMA) Models in Python

Many real-world decisions depend on how a metric evolves over time: demand, temperature, sales, traffic, or usage. **ARIMA** models are a foundational forecasting method because they combine trend handling, autoregressive behavior, and moving-average error structure in one interpretable framework.

In this tutorial, you will build an ARIMA forecasting workflow in Python using `statsmodels`.

## What ARIMA means

ARIMA stands for:

- **AR (Autoregressive)**: use past values of the series
- **I (Integrated)**: difference the series to make it more stationary
- **MA (Moving Average)**: use past forecast errors

An ARIMA model is written as `ARIMA(p, d, q)`:

- `p`: number of autoregressive lags
- `d`: number of differences
- `q`: number of moving-average lags

## Creating the dataset

import requests
import pandas as pd

# Download source file once so later blocks can focus on modeling steps
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)

# Parse dates and set a daily frequency-aware index
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)
Date
1981-01-01    20.7
1981-01-02    17.9
1981-01-03    18.8
1981-01-04    14.6
1981-01-05    15.8
Freq: D, Name: Temp, dtype: float64
<Day>
This block downloads the dataset once, stores it locally, parses the date index, and creates a daily time series used by the remaining examples. Keeping a local copy avoids repeated network calls and makes the tutorial easier to rerun.

## Visualizing the time series

import pandas as pd
import matplotlib.pyplot as plt

# Load prepared series for a quick visual inspection
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()
Plotting first helps you quickly inspect trend, variability, and potential seasonality before selecting ARIMA parameters. This initial check reduces guesswork when deciding whether differencing is needed.

## Differencing to reduce non-stationarity

import pandas as pd
import matplotlib.pyplot as plt

# Create first-differenced series (x_t - x_{t-1})
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()
First differencing (`d=1`) is a common way to stabilize the mean level so ARIMA assumptions are more reasonable. A more stable series generally leads to better-behaved parameter estimates.

## Fitting an ARIMA model

import pandas as pd
from statsmodels.tsa.arima.model import ARIMA

# Fit ARIMA with chosen (p, d, q) values
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())
                               SARIMAX Results                                
==============================================================================
Dep. Variable:                   Temp   No. Observations:                 3652
Model:                 ARIMA(2, 1, 2)   Log Likelihood               -8386.458
Date:                Wed, 11 Mar 2026   AIC                          16782.915
Time:                        16:47:59   BIC                          16813.929
Sample:                    01-01-1981   HQIC                         16793.960
                         - 12-31-1990                                         
Covariance Type:                  opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
ar.L1          0.4852      0.134      3.620      0.000       0.222       0.748
ar.L2         -0.1243      0.067     -1.865      0.062      -0.255       0.006
ma.L1         -0.8895      0.136     -6.548      0.000      -1.156      -0.623
ma.L2         -0.0105      0.126     -0.083      0.934      -0.258       0.237
sigma2         5.8022      0.128     45.191      0.000       5.551       6.054
===================================================================================
Ljung-Box (L1) (Q):                   0.00   Jarque-Bera (JB):                14.49
Prob(Q):                              0.99   Prob(JB):                         0.00
Heteroskedasticity (H):               0.86   Skew:                             0.08
Prob(H) (two-sided):                  0.01   Kurtosis:                         3.27
===================================================================================

Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
This fits an `ARIMA(2, 1, 2)` model and prints parameter estimates and diagnostic statistics to assess model behavior. The summary is where you inspect coefficient significance and residual diagnostics before trusting forecasts.

## Train-test forecasting

import pandas as pd
from statsmodels.tsa.arima.model import ARIMA

# Hold out the last year to evaluate forecasting performance
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())
Temp             mean  mean_ci_lower  mean_ci_upper  actual
Date                                                       
1990-01-01  12.887473       8.135428      17.639518    14.8
1990-01-02  13.212491       7.685577      18.739405    13.3
1990-01-03  13.341927       7.671745      19.012109    15.6
1990-01-04  13.366339       7.645590      19.087088    14.5
1990-01-05  13.363559       7.600908      19.126209    14.3
This example trains on historical data, forecasts the holdout period, and returns both point forecasts and confidence intervals. Using a holdout period gives a more honest estimate of real-world forecast behavior than evaluating on training data.

## Visualizing forecast vs actual

import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.arima.model import ARIMA

# Refit on train, forecast test, then compare visually
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()
Overlaying forecast and actual values helps you evaluate practical forecast quality and whether uncertainty intervals are realistic. The chart also makes bias and under/over-shoot patterns easier to spot than tables alone.