Time series forecasting in Python means using past observations, recorded at regular intervals, to predict future values with a quantified range of uncertainty. The two most widely used approaches are ARIMA/SARIMA from the statsmodels library and Prophet from Meta’s open-source prophet package. A reliable workflow has five steps: prepare the series with a proper date index, explore its trend and seasonality, build a simple baseline, fit ARIMA and Prophet, and compare them with time-based backtesting. Keep a model only if it beats the baseline, and always report prediction intervals, not just a single line.
This guide walks through each step with working code on one public dataset. It explains how to judge the results, and what it takes to turn a notebook forecast into something a sales, finance or operations team can rely on.
In this guide
- What is time series forecasting?
- ARIMA vs Prophet: when to use each
- Set up your Python environment
- Prepare your time series data
- Explore trend, seasonality and stationarity
- Start with a baseline forecast
- How to build an ARIMA forecast in Python
- How to build a Prophet forecast in Python
- How to evaluate time series forecasts
- Comparing ARIMA and Prophet on the same data
- Which approach fits your business?
- From notebook to production
- Common mistakes
- When to bring in a forecasting expert
- Frequently asked questions
What Is Time Series Forecasting?
A time series is a sequence of values recorded at regular intervals: daily orders, weekly website sessions, monthly revenue, quarterly demand. Forecasting uses the patterns in that history to estimate what comes next.
Most business series combine three components:
- Trend: a long-run rise or fall, such as steady revenue growth.
- Seasonality: a pattern that repeats over a fixed period, such as higher retail sales every December or lower B2B activity every August.
- Noise: irregular variation that no model can explain.
A good forecast captures the trend and seasonality, gives an honest range for the noise, and is checked against what actually happened. Forecasting is one part of predictive analytics; for the wider picture of how predictive models fit into business decisions, see our introduction to machine learning for business.
ARIMA vs Prophet: When to Use Each
ARIMA and Prophet take different routes to the same goal. ARIMA (AutoRegressive Integrated Moving Average) models the series using its own past values and past errors, after differencing to remove trend; SARIMA adds seasonal terms. Prophet fits a regression with a flexible trend, Fourier-based seasonal components and optional holiday effects (Taylor & Letham, 2018).
Table 1. ARIMA vs Prophet at a Glance
| Criterion | ARIMA / SARIMA | Prophet |
|---|---|---|
| Model type | Statistical model of autocorrelation, after differencing | Additive (or multiplicative) regression: trend + seasonality + holidays |
| Data needs | Regular, gap-free series; works on shorter histories | Benefits from several seasonal cycles; tolerates gaps |
| Seasonality | One seasonal period per model (e.g., 12 for monthly) | Several at once (yearly, weekly, daily) |
| Holidays and events | Only through exogenous variables (SARIMAX) | Built-in holiday and event effects |
| Missing data and outliers | Must be handled before fitting | Handles missing dates; outliers still distort fits |
| Interpretability | Clear coefficients and residual diagnostics | Clear component plots (trend, seasonality, holidays) |
| Tuning effort | Choosing p, d, q, P, D, Q needs care or automation | Sensible defaults; tune changepoints and seasonality |
| Speed | Fast for a single series | Fast; scales well to many series |
| Best for | Stable series with clear autocorrelation and one seasonal pattern | Business series with strong seasonality, holidays and trend changes |
| Watch out for | Mis-specified orders, over-differencing, non-constant variance | Over-flexible trends that extrapolate badly |
Neither method is universally better. On a given dataset, the only fair answer comes from testing both against a simple baseline, which this guide shows you how to do.
Set Up Your Python Environment
Install the libraries used in this guide:
pip install pandas numpy matplotlib statsmodels prophet pmdarima scikit-learnThe code uses current APIs: from prophet import Prophet (the package was renamed from fbprophet) and statsmodels.tsa.arima.model.ARIMA or statsmodels.tsa.statespace.sarimax.SARIMAX (the older statsmodels.tsa.arima_model.ARIMA class has been removed). If pmdarima fails to import, it is usually a version mismatch with NumPy; install it in a fresh virtual environment or skip the automatic order-selection step.
If you are new to pandas and plotting, start with our guide to Python for data analysis.
Prepare Your Time Series Data
Every example in this guide uses the same public dataset, so you can reproduce each step: the Mauna Loa atmospheric CO₂ series bundled with statsmodels. It contains weekly measurements from March 1958 to December 2001 from the Scripps flask network, and the statsmodels documentation describes it as public domain. It has a clear upward trend, strong yearly seasonality and some missing weeks, which makes it a good stand-in for a business series such as monthly revenue or demand. Swap in your own data by replacing the loading step.
Load and resample the data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
# Weekly CO2 readings with a DatetimeIndex
co2 = sm.datasets.co2.load_pandas().data
# Resample to monthly averages (month-start frequency)
y = co2["co2"].resample("MS").mean()
print(y.isna().sum(), "missing months")
y = y.interpolate() # fill any gaps after resampling
y = y.asfreq("MS") # make the frequency explicit for the modelsFor your own data, the same three rules apply:
- Use a proper
DatetimeIndexwithpd.to_datetime(), sorted in time order. - Fix the frequency. Resample to the level you will forecast (daily, weekly, monthly) and make sure every period exists. Missing periods silently break ARIMA.
- Fill gaps deliberately. Interpolate short gaps, but investigate long ones: a missing month of sales is a data problem, not a modelling problem.
Also check for one-off spikes (a bulk order, a system outage, a promotion) and decide whether the model should learn from them. Poor input data is the most common reason forecasts fail; our guide to data quality management covers how to catch these problems early.
Real company data is rarely this tidy. If your sales, CRM or finance exports need cleaning before they can be forecast, our data science consulting team offers a free data review to tell you what is usable and what needs fixing.
Split by time, never at random
Hold out the most recent periods as a test set. Here, the last 24 months are kept aside to judge every model:
horizon = 24
train, test = y.iloc[:-horizon], y.iloc[-horizon:]
print(train.index.min(), train.index.max(), test.index.min(), test.index.max())Shuffling or random splitting leaks future information into training and makes any model look better than it is.
Explore the Series: Trend, Seasonality and Stationarity
Plot and decompose
from statsmodels.tsa.seasonal import STL
train.plot(figsize=(10, 4), title="Monthly CO2 (training data)")
plt.show()
stl = STL(train, period=12).fit()
stl.plot()
plt.show()STL splits the series into trend, seasonal and remainder components. What to look for: a smooth trend, a seasonal pattern that repeats every 12 months, and a remainder with no obvious structure. If the seasonal swings grow as the level grows, consider a log transform or a multiplicative model.
Check stationarity
ARIMA assumes the series (after differencing) is stationary: its mean and variance do not drift over time. Two complementary tests help:
from statsmodels.tsa.stattools import adfuller, kpss
adf_p = adfuller(train)[1]
kpss_p = kpss(train, regression="c", nlags="auto")[1]
print(f"ADF p = {adf_p:.3f} KPSS p = {kpss_p:.3f}")
# After one regular and one seasonal difference
diffed = train.diff().diff(12).dropna()
print(f"ADF p (differenced) = {adfuller(diffed)[1]:.3f}")The tests have opposite null hypotheses. The ADF test’s null is that the series has a unit root (non-stationary), so a small p value supports stationarity. The KPSS test’s null is that the series is stationary, so a small p value suggests it is not. What to look for: on a trending, seasonal series like this one, the raw data should look non-stationary on both tests, and the differenced series should look stationary. The number of differences you needed becomes d (regular) and D (seasonal) in the model. KPSS may print a warning when the p value falls outside its lookup table; the direction of the result is still usable.
Read the ACF and PACF
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
plot_acf(diffed, lags=36, ax=axes[0])
plot_pacf(diffed, lags=36, ax=axes[1], method="ywm")
plt.show()The autocorrelation (ACF) and partial autocorrelation (PACF) plots of the differenced series suggest starting values for the model orders. Significant spikes at lags 1–2 point to non-seasonal AR or MA terms; a spike at lag 12 points to a seasonal term. Treat these as starting points, not answers.
Start With a Baseline Forecast
A forecasting model is only useful if it beats a simple rule. Two baselines should be in every project:
- Naive: every future value equals the last observed value.
- Seasonal naive: every future value equals the value from the same month last year.
naive = pd.Series(train.iloc[-1], index=test.index)
seasonal_naive = pd.Series(np.resize(train.iloc[-12:].values, len(test)), index=test.index)
def evaluate(actual, pred, train=train, m=12):
err = actual - pred
mae = np.mean(np.abs(err))
rmse = np.sqrt(np.mean(err ** 2))
mape = np.mean(np.abs(err / actual)) * 100
scale = np.mean(np.abs(train.diff(m).dropna())) # in-sample seasonal naive error
mase = mae / scale
return pd.Series({"MAE": mae, "RMSE": rmse, "MAPE %": mape, "MASE": mase})
results = {"Naive": evaluate(test, naive),
"Seasonal naive": evaluate(test, seasonal_naive)}
pd.DataFrame(results).T.round(3)The seasonal naive forecast is the benchmark to beat for any seasonal series. If ARIMA or Prophet cannot outperform it on your data, the simpler rule is the better choice, and that is a useful finding in itself.
How to Build an ARIMA Forecast in Python
Choose p, d, q (and seasonal P, D, Q, m)
A seasonal ARIMA model is written SARIMA(p, d, q)(P, D, Q)m:
- p: number of autoregressive terms (how many past values the model uses).
- d: number of regular differences needed to remove the trend.
- q: number of moving-average terms (how many past forecast errors the model uses).
- P, D, Q: the seasonal equivalents, applied at the seasonal lag.
- m: the length of the season (12 for monthly data with a yearly pattern, 7 for daily data with a weekly pattern).
The stationarity tests suggested d = 1 and D = 1. A common, well-tested starting point for monthly seasonal data is SARIMA(1, 1, 1)(0, 1, 1)12. Compare a few candidates by AIC (lower is better) and by out-of-sample error, rather than trusting any single rule.
Fit SARIMA With statsmodels
from statsmodels.tsa.statespace.sarimax import SARIMAX
sarima = SARIMAX(train, order=(1, 1, 1), seasonal_order=(0, 1, 1, 12))
sarima_fit = sarima.fit(disp=False)
print(sarima_fit.summary())The summary shows each coefficient with its standard error and p value, plus the AIC. What to look for: coefficients that are clearly different from zero, and no warnings about non-convergence. If a term is far from significant, try a simpler order and compare the AIC.
Check the Residuals
A good model leaves residuals that look like random noise.
from statsmodels.stats.diagnostic import acorr_ljungbox
sarima_fit.plot_diagnostics(figsize=(10, 8))
plt.show()
# Skip the first 13 residuals, which are affected by differencing
print(acorr_ljungbox(sarima_fit.resid.iloc[13:], lags=[12, 24]))What to look for: residuals centred on zero with no pattern over time, a roughly normal histogram, and no significant autocorrelation. In the Ljung-Box output, large p values (above .05) mean no remaining autocorrelation was detected. Small p values mean the model has missed some structure, so revisit the orders.
Forecast With Prediction Intervals
fc = sarima_fit.get_forecast(steps=len(test))
sarima_pred = fc.predicted_mean
sarima_ci = fc.conf_int(alpha=0.05) # 95% prediction interval
ax = train.iloc[-60:].plot(figsize=(10, 4), label="Train")
test.plot(ax=ax, label="Actual")
sarima_pred.plot(ax=ax, label="SARIMA forecast")
ax.fill_between(sarima_ci.index, sarima_ci.iloc[:, 0], sarima_ci.iloc[:, 1], alpha=0.2)
ax.legend(); plt.show()
results["SARIMA(1,1,1)(0,1,1)12"] = evaluate(test, sarima_pred)A 95% prediction interval is the range the model expects the actual value to fall within 95% of the time, if the model’s assumptions hold. Intervals widen as the horizon grows, because uncertainty compounds. For planning, the interval is often more useful than the single line: it tells a finance or operations team how much buffer to hold.
Automatic Order Selection With auto_arima
Searching orders by hand is slow when you have many series. pmdarima.auto_arima tests combinations and returns the one with the best information criterion:
import pmdarima as pm
auto = pm.auto_arima(train, seasonal=True, m=12,
stepwise=True, information_criterion="aic",
suppress_warnings=True, error_action="ignore")
print(auto.order, auto.seasonal_order)
auto_pred = pd.Series(np.asarray(auto.predict(n_periods=len(test))), index=test.index)
results["auto_arima"] = evaluate(test, auto_pred)Treat the selected order as a strong suggestion, not a final answer: still check the residuals and the out-of-sample error. If pmdarima will not install alongside your NumPy version, a short loop over a handful of SARIMAX orders, compared by AIC, achieves the same thing.
To include known drivers such as price, promotions or marketing spend, pass them as exog to SARIMAX (for both fitting and forecasting). You will need future values of those drivers to forecast.
How to Build a Prophet Forecast in Python
Format the Data (ds and y)
Prophet expects a DataFrame with two columns: ds (the date) and y (the value).
from prophet import Prophet
df_train = train.reset_index()
df_train.columns = ["ds", "y"]Fit the Model
prophet_model = Prophet(yearly_seasonality=True,
weekly_seasonality=False,
daily_seasonality=False,
interval_width=0.95)
prophet_model.fit(df_train)
future = prophet_model.make_future_dataframe(periods=len(test), freq="MS")
forecast = prophet_model.predict(future)
prophet_pred = forecast.set_index("ds")["yhat"].iloc[-len(test):]
prophet_pred.index = test.index
results["Prophet"] = evaluate(test, prophet_pred)
prophet_model.plot(forecast); plt.show()
prophet_model.plot_components(forecast); plt.show()Weekly and daily seasonality are switched off because the data are monthly. yhat_lower and yhat_upper in the forecast DataFrame hold the 95% interval. What to look for in the component plot: a smooth trend and a stable yearly pattern. A trend that bends sharply near the end of the history is a warning sign that it may extrapolate badly.
Seasonality, Holidays and Changepoints
The settings that matter most for business data:
seasonality_mode="multiplicative"when seasonal swings grow with the level of the series (common in sales).changepoint_prior_scalecontrols how flexible the trend is. The default is 0.05; raise it if the trend misses real shifts, lower it if the trend chases noise.- Holidays: for daily or weekly data,
prophet_model.add_country_holidays(country_name="US")adds a country’s public holidays, and you can pass your own events (product launches, promotions, Black Friday) as aholidaysDataFrame.
Add External Regressors
If a known driver affects the series, add it as a regressor before fitting:
# Example only: requires a 'promo' column in both df_train and future
m = Prophet()
m.add_regressor("promo")
# m.fit(df_train); forecast = m.predict(future)As with SARIMAX, you must supply future values of every regressor for the forecast period.
How to Evaluate Time Series Forecasts
One train/test split can be misleading: a model may get lucky on one particular period. Rolling-origin backtesting repeats the test several times, each time training on data up to a cut-off and forecasting the next block.
Table 2. Forecast Accuracy Metrics Explained
| Metric | What it measures | Formula (plain text) | When to use | Pitfall |
|---|---|---|---|---|
| MAE | Average size of errors, in the data’s units | mean of the absolute errors | Default choice; easy to explain (“off by 120 units a month on average”) | Not comparable across series of different scales |
| RMSE | Typical error size, penalising large misses more | square root of the mean squared error | When large errors are especially costly | Sensitive to a few big misses |
| MAPE | Average error as a percentage of actual values | mean of absolute error divided by actual, × 100 | Comparing series of different scales; reporting to non-technical teams | Undefined or explosive when actuals are zero or near zero |
| sMAPE | Percentage error using the average of actual and forecast | mean of absolute error divided by the mean of absolute actual and forecast, × 100 | Data with small values where MAPE explodes | Harder to interpret; not symmetric in practice |
| MASE | Error relative to a seasonal naive forecast | MAE divided by the in-sample seasonal naive MAE | Judging whether a model beats the baseline (below 1 = better) | Needs enough history to compute the scale |
Backtesting ARIMA
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5, test_size=12)
fold_mae = []
for train_idx, val_idx in tscv.split(train):
tr, val = train.iloc[train_idx], train.iloc[val_idx]
fit = SARIMAX(tr, order=(1, 1, 1), seasonal_order=(0, 1, 1, 12)).fit(disp=False)
pred = fit.forecast(steps=len(val))
fold_mae.append(np.mean(np.abs(val.values - pred.values)))
print("SARIMA backtest MAE by fold:", np.round(fold_mae, 3))
print("Mean:", round(np.mean(fold_mae), 3))Each fold trains on an expanding window and forecasts the next 12 months, all within the training data, so the final test set stays untouched.
Cross-Validating Prophet
Prophet has built-in tools for the same job:
from prophet.diagnostics import cross_validation, performance_metrics
cv = cross_validation(prophet_model, initial="7300 days",
period="365 days", horizon="730 days")
print(performance_metrics(cv)[["horizon", "mae", "rmse", "mape", "coverage"]].tail())initial is the first training window, period is how far the cut-off moves each time, and horizon is how far ahead each forecast reaches. The coverage column shows how often actual values fell inside the prediction interval; for a 95% interval, coverage well below 0.95 means the model is overconfident.
ARIMA vs Prophet Results on the Same Data
With every model evaluated on the same 24-month test set, one table answers the question that matters:
comparison = pd.DataFrame(results).T.round(3).sort_values("MASE")
print(comparison)
ax = test.plot(figsize=(10, 4), label="Actual", color="black")
sarima_pred.plot(ax=ax, label="SARIMA")
prophet_pred.plot(ax=ax, label="Prophet")
seasonal_naive.plot(ax=ax, label="Seasonal naive", linestyle="--")
ax.fill_between(sarima_ci.index, sarima_ci.iloc[:, 0], sarima_ci.iloc[:, 1], alpha=0.15)
ax.legend(); plt.show()How to read the comparison:
- Any model with a MASE above 1 is losing to the seasonal naive baseline on this test period. Do not use it without a good reason.
- Look at the backtest averages, not just the single test set. A model that wins one period but loses most folds is not reliable.
- Check the intervals. A model with a slightly higher error but well-calibrated intervals is often more useful for planning than one with tight but overconfident intervals.
- Prefer the simpler model when the difference is small. It is easier to explain, maintain and retrain.
On a smooth, strongly seasonal series like this one, both SARIMA and Prophet typically capture the trend and seasonality well, and the ranking can change with the test period. That is exactly why backtesting matters. On messy business data with promotions, holidays and structural breaks, the differences between methods are usually larger.
Not sure which model fits your data, or whether your forecast actually beats a simple baseline? Send us your data for a free data review, and we will tell you what is forecastable and which approach is worth building.
Which Forecasting Approach Fits Your Business?
Table 3. Which Forecasting Approach Fits Your Business Problem?
| Business situation | Data you have | Suggested starting method | Why |
|---|---|---|---|
| Monthly revenue for one business line | 3+ years of monthly totals | Seasonal naive, then SARIMA | Short, stable series with one yearly pattern suit SARIMA |
| Daily e-commerce orders | 2+ years of daily data with promotions and holidays | Prophet with holidays and promo regressors | Handles weekly and yearly seasonality plus events |
| Weekly demand for many products | Hundreds of SKUs, some sparse | Seasonal naive per SKU, then Prophet or ARIMA on high-volume SKUs | Automation matters; sparse items need simpler methods |
| Call-centre or staffing volume | Hourly or daily history | Prophet (multiple seasonalities) | Daily and weekly cycles at the same time |
| Sales driven by price and marketing spend | History plus future plans for those drivers | SARIMAX or Prophet with regressors | Lets you test “what if” scenarios |
| Volatile series with frequent structural breaks | Short or unstable history | Simple baselines plus expert judgement | Complex models overfit unstable data |
If your goal is to understand past performance before forecasting it, our guide on how to analyze sales data is a good first step. When the patterns become more complex (many products, external drivers, several seasonal cycles), machine-learning models such as gradient boosting can also be tested, but they still need to beat the same baselines.
From Notebook to Production
A forecast that lives in a notebook helps no one. To make it useful:
- Automate the data feed. Pull the latest actuals from your database, CRM or accounting system on a schedule, and run the same cleaning steps every time.
- Retrain on a fixed cadence. Monthly forecasts are usually refitted monthly; daily forecasts weekly. Re-check model orders and settings quarterly.
- Monitor forecast error. Track MAE or MAPE on each new period and set an alert when error drifts above its backtest level. Rising error usually means the business has changed.
- Show forecasts where decisions are made. Publish the forecast and its interval to the dashboard your team already uses; see how to create a dashboard in Power BI.
- Document assumptions. Record the data range, model settings, known events and what the interval means, so the forecast can be explained in a budget meeting.
- Keep humans in the loop. Planned launches, price changes or supply problems that the history cannot know about should adjust the forecast, with the adjustment recorded.
Common Time Series Forecasting Mistakes
- Splitting data randomly instead of by time, which leaks the future into training.
- Skipping the baseline, so there is no proof the model adds value.
- Evaluating on one holdout period instead of backtesting across several.
- Ignoring missing periods or an irregular frequency, which silently breaks ARIMA.
- Over-differencing, which adds noise and distorts the forecast.
- Reporting a single line without prediction intervals.
- Using MAPE on data with zeros or very small values.
- Letting Prophet’s trend chase recent noise by setting
changepoint_prior_scaletoo high. - Adding regressors without future values for the forecast period.
- Never retraining, so the model drifts as the business changes.
When to Bring In a Forecasting Expert
The workflow above works well for a single, clean series. Consider expert help when:
- You need forecasts for many products, regions or customer segments
- Promotions, holidays or price changes drive large swings
- External factors (marketing spend, weather, economic data) matter
- Forecasts feed inventory, staffing or budget decisions where errors are costly
- You need automated retraining, monitoring and dashboards, not a one-off notebook
- Your data is spread across several systems and needs cleaning first
Our team reviews your data, builds and backtests candidate models against baselines, and delivers forecasts with prediction intervals, a written forecast report, a documented Jupyter notebook and dashboard-ready outputs. Our predictive analytics services cover demand, sales, revenue and financial forecasting, and the fixed-scope Revenue Forecasting Package is typically delivered in 2–3 weeks.
Pricing is quote-based. It depends on the service, scope and deliverables, the size and quality of your data, the number of data sources, the complexity of the modelling, your tech stack and your timeline, and projects can be fixed-price or hourly-range. Forecasts are estimates, not guarantees: we report how accurate a model has been in backtesting and how uncertain its predictions are, so you can plan with that range.
Request a Proposal · Book a consultation · See how pricing works
Frequently Asked Questions
Neither is better in general. Prophet is quicker to set up for business data with several seasonal patterns, holidays and trend changes. SARIMA often performs well on stable series with one seasonal pattern and offers clearer statistical diagnostics. Test both against a seasonal naive baseline on your own data.
Enough to see the pattern repeat. For seasonal data, aim for at least two to three full seasonal cycles (for example, two to three years of monthly data), and more if you want to backtest properly. Very short histories favour simple methods.
SARIMA is ARIMA with additional seasonal terms. It models patterns that repeat at a fixed seasonal lag, such as every 12 months, using the seasonal orders P, D and Q and the season length m.
Use stationarity tests to choose d (and D), read the ACF and PACF of the differenced series for starting values of p and q, then compare a few candidates by AIC and out-of-sample error. pmdarima.auto_arima automates this search.
Evaluate on data the model has not seen, using rolling-origin backtesting. Report MAE or RMSE in the data’s own units, MASE to compare against a seasonal naive baseline, and MAPE only when the series has no zeros or near-zero values.
Yes. Pass them as exog to SARIMAX, or use add_regressor() in Prophet. In both cases you need values for those variables over the forecast period.
When you have many related series, many external drivers or complex non-linear effects. Gradient-boosting and deep-learning models can then help, but they need careful feature engineering and must still beat the same baselines.
Conclusion
Time series forecasting in Python is straightforward to start and easy to get wrong. Prepare a clean series with a fixed frequency, explore its trend and seasonality, set a seasonal naive baseline, then fit SARIMA and Prophet and compare them with backtesting. Keep the model that reliably beats the baseline, report its prediction intervals, and plan to monitor and retrain it. That discipline matters more than the choice of library.
