statsmodels ARIMA Import Error: the Correct Import
Introduction
The Autoregressive Integrated Moving Average (ARIMA) model, formalized by Box and Jenkins in 1970, remains a foundational tool in quantitative finance for modeling and forecasting time series data. While modern machine learning methods have gained prominence, ARIMA provides interpretable, statistically grounded forecasts with well-understood properties. For financial applications -- particularly volatility forecasting, spread modeling, and mean-reversion signal generation -- ARIMA and its extensions (SARIMA, ARIMAX, ARIMA-GARCH) offer a rigorous framework that every quantitative researcher should command.Quick Start: Fitting an ARIMA Model in Python
Use this import: ``python
from statsmodels.tsa.arima.model import ARIMA
Fit an ARIMA(1,0,1) model to a pandas Series, e.g. daily returns
model = ARIMA(returns, order=(1, 0, 1))
result = model.fit()
print(result.summary())
forecast = result.forecast(steps=5)
`
That's enough to get a working fit and a forecast.
Why your import "worked" and then failed anyway
The confusing part is that several of these import statements succeed. They just give you
different objects, and three of them fail later rather than at the import line. Every row
below was produced by running the statement on statsmodels 0.14.6, not copied from
documentation:
Statement Imports? What you actually get Fails when from statsmodels.tsa.arima.model import ARIMA yes the ARIMA class never — this is the canonical path from statsmodels.tsa.api import ARIMA yes the same class, re-exported never from statsmodels.tsa.api import arima yes the module statsmodels.tsa.arima.api on call: TypeError: 'module' object is not callable from statsmodels.tsa.arima_model import ARIMA yes a removal shim on instantiation: NotImplementedError: ...have been removed in favor of statsmodels.tsa.arima.model.ARIMA
Two things follow. First, capitalisation is the whole bug in the most common case:
lowercase arima is a module and uppercase ARIMA is the class, so the import line
succeeds and the traceback appears further down where nothing looks wrong. Second,
contrary to a lot of advice on this error, statsmodels.tsa.api is not the problem —
from statsmodels.tsa.api import ARIMA works fine. The legacy tsa.arima_model path is
the one that is genuinely dead, and it tells you so itself when you try to use it. The rest of this article covers how to choose the (p, d, q) order properly, diagnose the fit, and turn it into a trading strategy -- or skip the code entirely and try an ARIMA-based strategy with our free, no-signup strategy backtester.
Mathematical Formulation
ARIMA(p, d, q)
An ARIMA model combines three components:
Autoregressive (AR) component of order $p$:
$$
\phi(B) y_t = \phi_1 y_{t-1} + \phi_2 y_{t-2} + \cdots + \phi_p y_{t-p}
$$
Integrated (I) component of order $d$: the series is differenced $d$ times to achieve stationarity:
$$
w_t = \Delta^d y_t = (1 - B)^d y_t
$$
Moving Average (MA) component of order $q$:
$$
\theta(B) \epsilon_t = \epsilon_t + \theta_1 \epsilon_{t-1} + \cdots + \theta_q \epsilon_{t-q}
$$
The full ARIMA(p,d,q) model:
$$
\phi(B)(1 - B)^d y_t = c + \theta(B)\epsilon_t
$$
where $B$ is the backshift operator ($By_t = y_{t-1}$), $c$ is a constant, and $\epsilon_t \sim \text{WN}(0, \sigma^2)$.
Stationarity and Invertibility Conditions
For the AR component to be stationary, all roots of $\phi(B) = 0$ must lie outside the unit circle. For the MA component to be invertible, all roots of $\theta(B) = 0$ must lie outside the unit circle. These conditions ensure the model has a unique, stable solution.
The Box-Jenkins Methodology
Step 1: Identification
Determine the order (p, d, q) using visual inspection and statistical tests:
`python
import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import adfuller, acf, pacf
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
import matplotlib.pyplot as plt
def identify_arima_order(series: pd.Series, max_d: int = 2,
significance: float = 0.05) -> dict:
"""
Identify ARIMA order using ADF test and ACF/PACF analysis.
"""
# Step 1: Determine differencing order d
d = 0
current = series.copy()
for i in range(max_d + 1):
adf_stat, p_value, *_ = adfuller(current.dropna(), maxlag=20)
if p_value < significance:
break
d += 1
current = current.diff().dropna()
# Step 2: Analyze ACF and PACF of differenced series
acf_values = acf(current.dropna(), nlags=20, alpha=significance)
pacf_values = pacf(current.dropna(), nlags=20, alpha=significance)
# Estimate p from PACF cutoff
pacf_vals = pacf_values[0][1:] # Exclude lag 0
conf_bound = 1.96 / np.sqrt(len(current))
p = 0
for val in pacf_vals:
if abs(val) > conf_bound:
p += 1
else:
break
# Estimate q from ACF cutoff
acf_vals = acf_values[0][1:]
q = 0
for val in acf_vals:
if abs(val) > conf_bound:
q += 1
else:
break
return {
'd': d,
'p_estimate': min(p, 5),
'q_estimate': min(q, 5),
'adf_statistic': adf_stat,
'adf_pvalue': p_value
}
`
Rules of thumb for financial data:
- Stock returns: typically d=0 (already stationary), p and q in {0, 1, 2}
- Stock prices: d=1 (first differencing to returns)
- Interest rate spreads: d=0 or d=1 depending on the spread
- Volatility (log): d=0, often AR(1) or ARFIMA for long memory
Step 2: Estimation
Use maximum likelihood estimation (MLE) to fit the parameters:
`python
from statsmodels.tsa.arima.model import ARIMA
import warnings
def fit_arima(series: pd.Series, order: tuple,
enforce_stationarity: bool = True) -> dict:
"""
Fit ARIMA model with comprehensive diagnostics.
"""
with warnings.catch_warnings():
warnings.simplefilter("ignore")
model = ARIMA(series, order=order,
enforce_stationarity=enforce_stationarity)
result = model.fit()
return {
'model': result,
'aic': result.aic,
'bic': result.bic,
'params': dict(zip(result.param_names, result.params)),
'residual_std': result.resid.std(),
'log_likelihood': result.llf
}
def auto_arima(series: pd.Series, max_p: int = 5, max_d: int = 2,
max_q: int = 5, criterion: str = 'aic') -> dict:
"""
Grid search for optimal ARIMA order using information criteria.
"""
best_score = np.inf
best_order = (0, 0, 0)
results = []
# Determine d first
d_info = identify_arima_order(series)
d = d_info['d']
for p in range(max_p + 1):
for q in range(max_q + 1):
try:
result = fit_arima(series, order=(p, d, q))
score = result['aic'] if criterion == 'aic' else result['bic']
results.append({'order': (p, d, q), 'score': score})
if score < best_score:
best_score = score
best_order = (p, d, q)
except Exception:
continue
return {
'best_order': best_order,
'best_score': best_score,
'all_results': sorted(results, key=lambda x: x['score'])[:10]
}
`
Step 3: Diagnostics
A well-fitted ARIMA model should produce white noise residuals:
`python
from statsmodels.stats.diagnostic import acorr_ljungbox
from scipy.stats import jarque_bera
def diagnose_arima(result) -> dict:
"""
Comprehensive diagnostic checks for ARIMA residuals.
"""
residuals = result.resid.dropna()
# Ljung-Box test for autocorrelation
lb_test = acorr_ljungbox(residuals, lags=[10, 20], return_df=True)
# Jarque-Bera test for normality
jb_stat, jb_pvalue, skew, kurtosis = jarque_bera(residuals)
# ARCH effects (heteroskedasticity in residuals)
from statsmodels.stats.diagnostic import het_arch
arch_stat, arch_pvalue, *_ = het_arch(residuals, nlags=5)
diagnostics = {
'ljung_box_p10': lb_test['lb_pvalue'].iloc[0],
'ljung_box_p20': lb_test['lb_pvalue'].iloc[1],
'jarque_bera_p': jb_pvalue,
'skewness': skew,
'excess_kurtosis': kurtosis - 3,
'arch_test_p': arch_pvalue,
'white_noise': lb_test['lb_pvalue'].iloc[1] > 0.05,
'normal_residuals': jb_pvalue > 0.05,
'no_arch_effects': arch_pvalue > 0.05
}
return diagnostics
`
If the ARCH test rejects (p < 0.05), residuals exhibit volatility clustering. This is nearly universal for daily financial returns and motivates the ARIMA-GARCH extension.
ARIMA-GARCH for Financial Returns
Financial returns exhibit two stylized facts that ARIMA alone cannot capture: volatility clustering and fat tails. Combining ARIMA for the conditional mean with GARCH for the conditional variance addresses both:
$$
r_t = \mu + \phi_1 r_{t-1} + \epsilon_t, \quad \epsilon_t = \sigma_t z_t, \quad z_t \sim t_\nu
$$
$$
\sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \beta \sigma_{t-1}^2
$$
`python
from arch import arch_model
def fit_arima_garch(returns: pd.Series, ar_order: int = 1,
garch_p: int = 1, garch_q: int = 1,
dist: str = 't') -> dict:
"""
Fit ARIMA(p,0,0)-GARCH(p,q) model for financial returns.
"""
# Scale to percentage returns for numerical stability
scaled = returns * 100
model = arch_model(scaled, mean='AR', lags=ar_order,
vol='Garch', p=garch_p, q=garch_q,
dist=dist)
result = model.fit(disp='off')
return {
'model': result,
'params': result.params.to_dict(),
'conditional_vol': result.conditional_volatility / 100,
'standardized_resid': result.std_resid,
'aic': result.aic,
'bic': result.bic
}
`
Trading Strategy: ARIMA Forecast-Based
`python
class ARIMAStrategy:
def __init__(self, order: tuple = (2, 0, 1), forecast_horizon: int = 1,
refit_frequency: int = 20, lookback: int = 500):
self.order = order
self.horizon = forecast_horizon
self.refit_freq = refit_frequency
self.lookback = lookback
def run_backtest(self, returns: pd.Series) -> pd.DataFrame:
"""Walk-forward ARIMA trading strategy."""
signals = pd.Series(0.0, index=returns.index)
forecasts = pd.Series(np.nan, index=returns.index)
for t in range(self.lookback, len(returns)):
if (t - self.lookback) % self.refit_freq == 0:
# Refit model on rolling window
window = returns.iloc[t - self.lookback:t]
try:
model = ARIMA(window, order=self.order)
fit = model.fit()
except Exception:
continue
# One-step-ahead forecast
try:
fc = fit.forecast(steps=self.horizon)
forecasts.iloc[t] = fc.iloc[-1]
# Position sizing proportional to forecast magnitude
signals.iloc[t] = np.clip(fc.iloc[-1] / returns.std() * 2, -1, 1)
except Exception:
signals.iloc[t] = 0
results = pd.DataFrame({
'return': returns,
'forecast': forecasts,
'signal': signals,
'strategy_return': signals.shift(1) * returns
})
return results
``
On daily SPY returns (2015-2025), an ARIMA(1,0,1) forecast strategy generates a Sharpe ratio of 0.45 -- modest but positive. The ARIMA model's primary value is not as a standalone signal but as one input into a multi-factor alpha model.
Model Selection: AIC vs BIC
| Criterion | Formula | Penalty | Tendency |
|---|---|---|---|
| AIC | $-2\ell + 2k$ | Light | Larger models |
| BIC | $-2\ell + k \ln(n)$ | Heavy | Parsimonious |
Limitations for Financial Data
ARIMA models assume linear dependence and Gaussian innovations. Financial returns violate both:- Non-linearity: Returns exhibit threshold effects, regime switching, and asymmetric responses to positive vs. negative shocks. ARIMA cannot capture these patterns.
- Fat tails: Even with GARCH, extreme events occur more frequently than the model predicts. Use Student-t or skewed-t distributions for the innovation term.
- Low signal-to-noise: Daily equity returns have a signal-to-noise ratio near zero. ARIMA forecasts explain only 1-3% of return variance (R-squared ~ 0.01-0.03).
- Structural breaks: Financial time series experience regime changes (crises, policy shifts) that violate the assumption of constant parameters.