Python time series work comes down to three moves: decompose a series into trend and seasonality, test for stationarity, and forecast with ARIMA (AutoRegressive Integrated Moving Average). We predict sales, stock prices, and server metrics using statsmodels, with every number coming from real local runs.
“All models are wrong, but some are useful.” In time series, the useful ones are the ones that respect that the future leans on the past.
George Box (statistician), with a time series twist
Last Updated: July 2026 | Tested on: Python 3.14.6, statsmodels 0.14.6, pandas 2.3.3 | Difficulty: Advanced | Reading Time: 13 minutes
Time series data is everywhere: stock prices, server Central Processing Unit (CPU) usage, monthly sales, daily temperatures, website traffic. What makes it special is that the rows are ordered in time, and what happened recently tells you something about what happens next. A normal machine learning model treats every row as if it stands alone. A time series model does the opposite: it leans on the order, because in time, yesterday explains today.
Here is the quick mental picture. Think of an ice cream shop’s monthly sales. Sales climb a little every year as the shop gets popular (that slow climb is the trend). Every summer they jump and every winter they dip (that repeating wave is the seasonality). And on any given month a heat wave or a rainy week nudges sales up or down for no lasting reason (that leftover wobble is the residual, just noise). Decomposition splits the line into those three pieces. ARIMA then studies how each value relates to the values just before it (that is autocorrelation) and uses that to forecast the next few months, with a confidence band around the guess.
Table of Contents
Prerequisites
Python Time Series Decomposition
The diagram above shows a time series being pulled apart into its three components: trend (the long-term direction), seasonality (the pattern that repeats at fixed intervals), and residual (the random noise left over once trend and seasonality are gone). Splitting a series this way is the first real step in Python time series analysis, because each piece needs its own treatment. Trend gets flattened by differencing, seasonality gets handled by seasonal decomposition, and the residual is the part a forecasting model actually tries to nail. The code below walks through both the decomposition and a simple ARIMA forecast.
📄 decomposition.py: breaking a time series into components
import numpy as np
import pandas as pd
from statsmodels.tsa.seasonal import seasonal_decompose
# Simulate monthly sales with trend + seasonality + noise
rng = np.random.default_rng(42)
months = 48 # 4 years
trend = np.linspace(100, 200, months)
seasonal = 20 * np.sin(2 * np.pi * np.arange(months) / 12)
noise = rng.normal(0, 5, months)
sales = trend + seasonal + noise
dates = pd.date_range("2022-01", periods=months, freq="MS")
ts = pd.Series(sales, index=dates)
# Decompose into trend, seasonal, and residual parts
result = seasonal_decompose(ts, model="additive", period=12)
print("Monthly Sales Decomposition:")
print(f" Trend range: {result.trend.dropna().min():.0f} to {result.trend.dropna().max():.0f}")
print(f" Seasonal range: {result.seasonal.min():.1f} to {result.seasonal.max():.1f}")
print(f" Residual std: {result.resid.dropna().std():.1f}")
# Pick a mid-series month. The first and last 6 months are NaN
# because the trend uses a centered 12-month moving average.
i = 18 # July 2023
print(f"\nOriginal = Trend + Seasonal + Residual")
print(f"{ts.index[i].strftime('%Y-%m')}: {ts.iloc[i]:.1f} = {result.trend.iloc[i]:.1f} + {result.seasonal.iloc[i]:.1f} + {result.resid.iloc[i]:.1f}")
▶ Output
Monthly Sales Decomposition: Trend range: 112 to 188 Seasonal range: -24.2 to 19.3 Residual std: 2.5 Original = Trend + Seasonal + Residual 2023-07: 142.7 = 138.7 + 4.7 + -0.7
What happened here: The decomposition pulled the line apart into three pieces. The trend runs from about 112 to 188 (the steady climb), the seasonal swing covers roughly -24 to +19 around that trend (summer up, winter down), and the residual is small, a standard deviation of only 2.5, because we kept the noise tame on purpose. Look at the last line: July 2023 sales of 142.7 is exactly trend (138.7) plus seasonal boost (4.7) plus a tiny random dip (-0.7).
One small catch worth knowing: seasonal_decompose estimates the trend with a centered 12-month moving average, so the first 6 and last 6 months come back as NaN (there is no full window to average there). That is why we read off a mid-series month instead of month 1. This split is what tells you whether real growth is happening (the trend) or you are just looking at a good season.
ARIMA Forecasting
Think about guessing tomorrow’s temperature. You do not start from scratch. You glance at the last few days, assume tomorrow lands somewhere near them, and nudge the guess in whatever direction things have been drifting. ARIMA does the same thing with numbers: it leans on recent values (the AR part), the recent errors it made (the MA part), and the overall drift (the I part), then projects the next few steps forward with a confidence band around each one. Below we train it on the first 3 years of our sales series and ask it to forecast the final year we held back.
📄 arima_forecast.py: predicting future values
import numpy as np
import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
# Use same data, split into train/test
rng = np.random.default_rng(42)
months = 48
trend = np.linspace(100, 200, months)
seasonal = 20 * np.sin(2 * np.pi * np.arange(months) / 12)
noise = rng.normal(0, 5, months)
sales = trend + seasonal + noise
dates = pd.date_range("2022-01", periods=months, freq="MS")
ts = pd.Series(sales, index=dates)
train = ts[:36] # 3 years
test = ts[36:] # 1 year
# Fit ARIMA(1,1,1) - simple first-order model
model = ARIMA(train, order=(1, 1, 1))
fit = model.fit()
# Forecast next 12 months
forecast = fit.forecast(steps=12)
print("ARIMA(1,1,1) Forecast vs Actual:")
print(f"{"Month":<12} | {"Actual":>8} | {"Forecast":>8} | {"Error":>8}")
print("-" * 45)
errors = []
for date, actual, pred in zip(test.index, test.values, forecast.values):
err = actual - pred
errors.append(abs(err))
print(f"{date.strftime("%Y-%m"):<12} | {actual:>8.1f} | {pred:>8.1f} | {err:>+8.1f}")
mae = np.mean(errors)
print(f"\nMAE: {mae:.1f}")
print(f"Mean actual: {test.mean():.1f}")
print(f"MAPE: {mae/test.mean()*100:.1f}%")
▶ Output
ARIMA(1,1,1) Forecast vs Actual: Month | Actual | Forecast | Error --------------------------------------------- 2025-01 | 176.0 | 174.9 | +1.2 2025-02 | 184.5 | 177.1 | +7.4 2025-03 | 194.0 | 178.1 | +15.9 2025-04 | 206.2 | 178.6 | +27.6 2025-05 | 206.1 | 178.8 | +27.3 2025-06 | 199.9 | 178.9 | +21.0 2025-07 | 186.0 | 179.0 | +7.1 2025-08 | 182.7 | 179.0 | +3.7 2025-09 | 176.9 | 179.0 | -2.1 2025-10 | 176.8 | 179.0 | -2.2 2025-11 | 184.9 | 179.0 | +5.9 2025-12 | 191.1 | 179.0 | +12.1 MAE: 11.1 Mean actual: 188.8 MAPE: 5.9%
What happened here: A plain ARIMA(1,1,1) landed at 5.9% MAPE (mean absolute percentage error) across the 12-month forecast. Look closely at the Forecast column though: after the first couple of steps it basically flatlines around 179. That is ARIMA being honest about its limits. It knows there is a trend and a recent level, but it has no idea that summer comes around every year, so it cannot bend up and down with the seasons.
The big errors land in spring and early summer (+27 in April) exactly because the real sales peaked there and our flat forecast did not follow. The fix is SARIMA (Seasonal ARIMA), which adds seasonal autoregressive (AR) and moving average (MA) terms tied to a 12-month period so the forecast can ride the wave. ARIMA is the foundation here; SARIMA, Prophet, and neural models all build on it.
Common Mistakes
The number one mistake in Python time series forecasting is feeding ARIMA a series that still has a trend. ARIMA needs the data to be stationary, which just means the average level and the spread stay roughly constant over time. Picture a calm lake versus a river that keeps flowing downhill. The lake’s water level holds steady (stationary); the river’s level depends on where you measure along the slope (not stationary). A series with an upward trend is the river. The “I” in ARIMA stands for “Integrated”, and it is the cure: differencing the data (subtracting each value from the one before it) flattens the slope and turns the river back into a lake.
You do not have to eyeball it. The Augmented Dickey-Fuller (ADF) test gives you a p-value. If the p-value is below 0.05, the series is stationary and you are good to go. If not, difference it once (set d=1) and test again.
❌ Mistake: running ARIMA on data with a trend, without differencing
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import adfuller
# Same sales series: it has a clear upward trend, so it is NOT stationary.
rng = np.random.default_rng(42)
months = 48
trend = np.linspace(100, 200, months)
seasonal = 20 * np.sin(2 * np.pi * np.arange(months) / 12)
noise = rng.normal(0, 5, months)
sales = trend + seasonal + noise
dates = pd.date_range("2022-01", periods=months, freq="MS")
ts = pd.Series(sales, index=dates)
# Augmented Dickey-Fuller test on the raw series
p_raw = adfuller(ts)[1]
# Difference once (the "I" in ARIMA) to remove the trend, then test again
p_diff = adfuller(ts.diff().dropna())[1]
print("Augmented Dickey-Fuller stationarity test:")
print(f" Raw series p-value: {p_raw:.3f} -> {'stationary' if p_raw < 0.05 else 'NOT stationary'}")
print(f" Differenced (d=1) p-value: {p_diff:.3f} -> {'stationary' if p_diff < 0.05 else 'NOT stationary'}")
print("\nRule: p-value < 0.05 means stationary. Otherwise difference and retest.")
▶ Output
Augmented Dickey-Fuller stationarity test: Raw series p-value: 0.910 -> NOT stationary Differenced (d=1) p-value: 0.000 -> stationary Rule: p-value < 0.05 means stationary. Otherwise difference and retest.
What happened here: The raw series scored a p-value of 0.910, far above 0.05, so the test confirms it is not stationary (the trend gives it away). After one round of differencing the p-value drops to 0.000, which says the differenced series is stationary. That is exactly why we used d=1 in the ARIMA(1,1,1) model earlier: d is the number of times ARIMA differences the data for you before fitting. Run the test first, let it pick d, then model.
Practice Exercises
- Exercise 1: Take the same sales series and decompose it with
model="multiplicative"instead of"additive". Print the seasonal factors and explain in one sentence when a multiplicative model fits better (hint: when the seasonal swing grows as the trend grows). - Exercise 2: Fit a SARIMA model with
SARIMAX(train, order=(1,1,1), seasonal_order=(1,1,1,12))from statsmodels, forecast the same 12 months, and compare its MAPE against the plain ARIMA result of 5.9%. Did the seasonal terms help? - Exercise 3: Write a small function
check_stationarity(series)that runs the ADF test, prints the p-value, and keeps differencing (up to 2 times) until the series is stationary, returning the number of differences needed. Test it on the raw sales series.
Conclusion
You now have the three core moves of Python time series analysis: decompose a series into trend, seasonality, and residual so you can see whether real growth is happening or you are just riding a good season; test for stationarity with the Augmented Dickey-Fuller test and difference until the p-value drops below 0.05; and forecast with ARIMA, reading MAE and MAPE to judge how close you landed. You also saw exactly where plain ARIMA runs out of road: it cannot follow a repeating seasonal wave on its own, which is why its forecast flatlined after a couple of steps.
Next up is SARIMA and Prophet, which add the seasonal terms ARIMA is missing, and then the series moves into Natural Language Processing (NLP) basics in the following post. Before you move on, work through the practice exercises above. Fitting a SARIMA model yourself is the fastest way to feel the difference the seasonal terms make. For the full roadmap and every other tutorial in order, visit the Python + AI/ML tutorial series home.
Frequently Asked Questions
What do the p, d, q parameters in ARIMA mean?
p is the autoregressive order (how many past values to use), d is the differencing order (how many times to difference for stationarity), q is the moving average order (how many past errors to use). Use ACF and PACF plots or auto_arima from pmdarima to find optimal values.
When should I use SARIMA instead of ARIMA?
When your data has clear seasonal patterns (monthly sales peaks in December, daily traffic peaks at noon). SARIMA adds seasonal AR, I, and MA terms with a specified seasonal period.
Can I use scikit-learn for time series?
Not directly. Standard train_test_split shuffles data, destroying temporal order. Use TimeSeriesSplit for cross-validation. For Python time series modeling, use statsmodels, Prophet, or sktime (scikit-learn compatible time series library).
How far ahead can I reliably forecast?
It depends on the signal-to-noise ratio and pattern stability. Forecasts degrade rapidly beyond 1-2 seasonal cycles. For monthly data, 3-12 months is typical. Beyond that, confidence intervals become so wide that the forecast is not actionable.
Interview Questions on Time Series and ARIMA
If you can walk through these without peeking, you are ready for this topic in an interview.
Q: Why must a series be stationary before you fit the AR and MA parts of ARIMA?
The AR and MA terms assume the statistical properties of the series (mean, variance, autocorrelation) stay constant over time. If a trend is present the mean keeps shifting, so coefficients estimated on the early part of the series do not hold later and the forecast drifts. Differencing (the “I” term, controlled by d) removes the trend so the remaining series has a stable mean, which is why you test with the ADF test and pick d before fitting.
Q: How do ACF and PACF plots help you choose p and q?
The PACF (partial autocorrelation) plot points to the AR order p: for a pure AR process it cuts off sharply after lag p. The ACF (autocorrelation) plot points to the MA order q: for a pure MA process it cuts off after lag q. In practice you read where each drops back inside the confidence band, or you let auto_arima from pmdarima search the grid for you.
Q: What is the difference between MAE and MAPE, and when would MAPE mislead you?
MAE is the mean absolute error in the original units (units of sales, degrees, and so on), while MAPE expresses that error as a percentage of the actual values, which makes it easy to compare across different series. MAPE breaks down when actual values are near or equal to zero, because dividing by a tiny number blows the percentage up or makes it undefined. For series that cross zero, prefer MAE or RMSE.
Q: Your ARIMA forecast for monthly sales flatlines after two steps even though history clearly rises and dips every summer. What is wrong and how do you fix it?
Plain ARIMA has no seasonal component, so once its forecast reaches the recent level it cannot ride the yearly wave and it flattens out, which is exactly the behavior shown in the ARIMA output earlier in this post. Switch to SARIMA (via SARIMAX with a seasonal_order like (1,1,1,12) for monthly data) so seasonal AR, I, and MA terms tied to the 12-month period can bend the forecast up and down. The big spring and early-summer errors should shrink once the model can follow the season.
Q: The ADF test returns a p-value of 0.42 on your raw series, but you have already set d=1 in ARIMA. Should you trust the model as is?
A p-value of 0.42 is well above 0.05, so the raw series is not stationary and d=1 is a reasonable starting point, but you must re-run the ADF test on the differenced series to confirm it actually became stationary. If the differenced p-value drops below 0.05, d=1 is enough; if it does not, try d=2. Setting d without verifying can leave residual trend behind, and that inflates forecast error.
Q: You train on 3 years and forecast 12 months, but by month 10 the confidence intervals are so wide the forecast looks useless. Is that a bug?
No, that is expected. Forecast uncertainty compounds with each step because every prediction feeds into the next, so the intervals fan out the further ahead you go, especially beyond one or two seasonal cycles. If you need reliable long-horizon numbers, shorten the horizon, add more history, or use a model with stronger seasonal structure. Do not read a point estimate 12 months out as gospel.
Series: Python + AI/ML Cookbook, Part 5: Machine Learning
Reference: the complete, always-current details live in the official Python documentation.
Related Posts
Previous: ML: Recommender Systems (Collaborative and Content)
Next: ML: NLP Basics, Tokenization and TF-IDF
Series Home: Python + AI/ML Tutorial Series

No comment