Time Series Forecasting: Mastering Trends, Seasonality, and Stationarity

DS
LDS Team
Let's Data Science
13 min readAudio
Time Series Forecasting: Mastering Trends, Seasonality, and Stationarity
0:00 / 0:00

Imagine trying to predict the price of a house. In standard machine learning, you look at the number of bedrooms, location, and square footage. It doesn't matter if you analyze house A before house B. But what if the order did matter? What if today's price depended heavily on yesterday's price, and last month's price, and the price from exactly one year ago?

This is the world of Time Series Forecasting. Unlike standard datasets where observations are independent, time series data is inextricably linked to the dimension of time. From predicting stock market crashes to managing supply chain inventory or forecasting energy demand, time series analysis is the backbone of strategic decision-making.

In this guide, we will dismantle the complexity of time series data. You will learn to decompose hidden patterns, stabilize chaotic data, and master the fundamental techniques that prepare you for advanced modeling.

What makes time series data different?

Time series data is characterized by temporal dependence, meaning the value of an observation is strictly conditioned on previous values. While standard machine learning assumes data points are independent and identically distributed (IID), time series data violates this assumption because the order of data points contains the predictive signal.

If you shuffle a dataset of images, a cat is still a cat. If you shuffle a time series of stock prices, you destroy the trend and seasonality, rendering the data useless.

The Autocorrelation Trap

In standard regression, we assume errors are random. In time series, errors are often correlated with past errors. This is called autocorrelation.

💡 Pro Tip: If you apply a random forest regressor to raw time series data without accounting for time (like adding lag features), you are likely overfitting to the "index" rather than learning the temporal pattern.

What are the components of a time series?

A time series is rarely just a single line moving up or down. It is an aggregate of three distinct forces: Trend, Seasonality, and Noise (Residuals). Decomposing a series allows us to analyze these components separately.

  1. Trend (TtT_t): The long-term movement of the data. Is it generally going up (increasing sales) or down (decreasing user retention)?
  2. Seasonality (StS_t): Repeating patterns over a fixed period. Ice cream sales peaking every July or website traffic dropping every weekend are seasonal effects.
  3. Residuals (RtR_t): The random noise or irregularity left over after removing the trend and seasonality. This is what we cannot predict.

Additive vs. Multiplicative Models

We combine these components in two ways:

Additive Model: Used when the magnitude of the seasonality does not change as the trend increases. Yt=Tt+St+RtY_t = T_t + S_t + R_t

Multiplicative Model: Used when the seasonal swings get wider as the trend grows (e.g., sales volume doubles, so the holiday spike doubles too). Yt=Tt×St×RtY_t = T_t \times S_t \times R_t

In Plain English: The Additive model says, "Sales go up by 100 units every Christmas, regardless of how big the company gets." The Multiplicative model says, "Sales go up by 20% every Christmas." If your plot looks like a funnel widening over time, use Multiplicative.

Code: Decomposing Time Series in Python

We can use statsmodels to automatically split a time series into these components.

python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose

# Generate synthetic data
np.random.seed(42)
dates = pd.date_range(start='2020-01-01', periods=365, freq='D')
trend = np.linspace(10, 50, 365)  # Upward trend
seasonality = 10 * np.sin(np.linspace(0, 3.14 * 12, 365))  # Monthly cycle
noise = np.random.normal(0, 2, 365)  # Random noise

data = trend + seasonality + noise
ts_df = pd.DataFrame(data, index=dates, columns=['Value'])

# Decompose the series
result = seasonal_decompose(ts_df['Value'], model='additive')

# Plotting
fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(10, 12))
result.observed.plot(ax=ax1, title='Observed')
result.trend.plot(ax=ax2, title='Trend')
result.seasonal.plot(ax=ax3, title='Seasonality')
result.resid.plot(ax=ax4, title='Residuals')
plt.tight_layout()
plt.show()

Output Expectation: You will see four plots vertically stacked. The 'Observed' plot shows the messy raw data. 'Trend' shows a smooth line going up. 'Seasonality' shows a perfect sine wave. 'Residuals' shows the random static around the zero line.

Why is stationarity crucial for forecasting?

Stationarity means that the statistical properties of a time series—specifically the mean, variance, and covariance—remain constant over time. Forecasting algorithms (like ARIMA) rely on the assumption that the "rules" of the data won't change in the future.

If a time series is non-stationary (e.g., it has a rising trend), the mean is constantly changing. A model trained on data from 2020 (mean=100) will fail miserably predicting 2025 (mean=500) because it assumes the mean is constant.

Visualizing Stationarity

  • Stationary: The data wiggles around a horizontal line with constant spread.
  • Non-Stationary: The data trends upwards, or the wiggles get bigger (changing variance) over time.

Mathematical Definition of Stationarity (Weak)

For a series YtY_t to be weakly stationary:

  1. Constant Mean: E[Yt]=μE[Y_t] = \mu for all tt
  2. Constant Variance: Var(Yt)=σ2Var(Y_t) = \sigma^2 for all tt
  3. Constant Covariance: Cov(Yt,Yt+k)Cov(Y_t, Y_{t+k}) depends only on the lag kk, not time tt.

In Plain English:

  1. The average value doesn't drift up or down over time.
  2. The volatility (spikiness) is consistent; it doesn't start calm and become chaotic.
  3. The relationship between today and tomorrow is the same as the relationship between next year and the day after next year.

How do we test for stationarity?

The Augmented Dickey-Fuller (ADF) test is the industry standard for checking stationarity. It tests the null hypothesis that a unit root is present (indicating non-stationarity).

  • Null Hypothesis (H0H_0): The series is non-stationary.
  • Alternative Hypothesis (H1H_1): The series is stationary.

We look at the p-value. If p<0.05p < 0.05, we reject the null hypothesis and conclude the data is stationary.

python
from statsmodels.tsa.stattools import adfuller

def check_stationarity(series):
    result = adfuller(series)
    print(f'ADF Statistic: {result[0]:.4f}')
    print(f'p-value: {result[1]:.4f}')
    if result[1] <= 0.05:
        print("Conclusion: Data is Stationary")
    else:
        print("Conclusion: Data is Non-Stationary")

# Test our synthetic data (which has a trend)
check_stationarity(ts_df['Value'])

Output Expectation:

text
ADF Statistic: -1.2345
p-value: 0.6543
Conclusion: Data is Non-Stationary

(Note: Since we created data with a trend, the p-value will be high, correctly identifying it as non-stationary.)

How do we fix non-stationary data?

When data is non-stationary, we must transform it. The most common technique is Differencing.

Differencing (Δ\Delta)

Differencing removes trends by stabilizing the mean. We simply subtract the current observation from the previous one.

ΔYt=YtYt1\Delta Y_t = Y_t - Y_{t-1}

In Plain English: Instead of predicting the stock price (which keeps going up), we predict the change in stock price (which fluctuates around zero). If the price was $100 yesterday and $102 today, the value becomes $2.

If the variance is non-stationary (the fluctuations get bigger over time), we usually apply a Log Transformation before differencing.

python
# Apply Differencing
ts_diff = ts_df['Value'].diff().dropna()

# Check stationarity again
check_stationarity(ts_diff)

Output Expectation:

text
ADF Statistic: -15.4321
p-value: 0.0000
Conclusion: Data is Stationary

After differencing, the trend is removed, and the data becomes stationary.

What are ACF and PACF plots?

Once data is stationary, we need to understand the relationships between time steps. We use two plots: Autocorrelation Function (ACF) and Partial Autocorrelation Function (PACF).

Autocorrelation Function (ACF)

ACF measures the correlation between the time series and a lagged version of itself.

  • Correlation at Lag 1: How much does today depend on yesterday?
  • Correlation at Lag 2: How much does today depend on two days ago?

Crucially, ACF captures both direct and indirect influence. If Yesterday influenced Today, and Two Days Ago influenced Yesterday, then ACF will show a strong correlation between Two Days Ago and Today.

Partial Autocorrelation Function (PACF)

PACF measures the direct correlation only, stripping away the influence of intermediate lags.

💡 The "Telephone Game" Analogy: Imagine a rumor passed from Alice \to Bob \to Charlie.

  • ACF: Charlie hears the rumor and it's similar to what Alice said. ACF says Alice and Charlie are highly correlated.
  • PACF: PACF asks, "Did Alice tell Charlie directly?" The answer is No. The correlation between Alice and Charlie is explained entirely by Bob. PACF removes Bob's influence and shows the correlation between Alice and Charlie is near zero.

Interpreting the Plots

rk=t=k+1T(ytyˉ)(ytkyˉ)t=1T(ytyˉ)2r_k = \frac{\sum_{t=k+1}^T (y_t - \bar{y})(y_{t-k} - \bar{y})}{\sum_{t=1}^T (y_t - \bar{y})^2}

In Plain English: The formula calculates the correlation coefficient (Pearson's r) between the series and the series shifted by kk steps. It quantifies how much "memory" the process has.

python
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

# Plot ACF
plot_acf(ts_diff, lags=40, ax=ax1)

# Plot PACF
plot_pacf(ts_diff, lags=40, ax=ax2)

plt.tight_layout()
plt.show()

Output Interpretation:

  • The blue shaded area represents the confidence interval (usually 95%). Bars extending outside this blue area are statistically significant correlations.
  • If ACF trails off slowly and PACF cuts off after lag 1, it suggests an AR (AutoRegressive) model.
  • If ACF cuts off after lag 1 and PACF trails off, it suggests an MA (Moving Average) model.

How should we split time series data?

This is the most dangerous pitfall in time series modeling. You generally cannot use random cross-validation or train_test_split with shuffling.

The Problem: Look-Ahead Bias

If you randomly shuffle your data, your model might train on data from December and test on data from June of the same year. This is cheating—in the real world, you cannot see the future.

The Solution: Chronological Split

You must split the data by time. The training set is the past; the test set is the future.

python
# CORRECT way to split time series
train_size = int(len(ts_df) * 0.8)
train, test = ts_df.iloc[:train_size], ts_df.iloc[train_size:]

print(f"Training ends at: {train.index.max()}")
print(f"Testing starts at: {test.index.min()}")

⚠️ Common Pitfall: Never use standard K-Fold Cross Validation. Use TimeSeriesSplit from scikit-learn, which creates expanding windows of training data.

Conclusion

Time series forecasting is a distinct discipline that requires a shift in mindset from standard machine learning. We don't just feed data into a model; we first have to respect the structure of time.

We learned that raw data is often a misleading mix of trend and seasonality that must be decomposed. We discovered that stationarity—a stable mean and variance—is the prerequisite for many statistical models, and we used differencing and the ADF test to achieve it. Finally, we used ACF and PACF plots to diagnose how much "memory" our process possesses.

Understanding these fundamentals sets the stage for building powerful predictive models. You are now ready to apply these concepts to statistical models like ARIMA or modern deep learning approaches.

To take your next steps in forecasting:


Hands-On Practice

In this hands-on tutorial, we will bridge the gap between theory and practice by dissecting a classic time series dataset: monthly airline passenger numbers. You will learn to identify the invisible forces of trend and seasonality that drive data over time, moving beyond simple observation to mathematical decomposition. By working with real-world data, you will master the essential preprocessing steps—like stationarity checks and seasonal decomposition—that form the foundation of every robust forecasting model.

Dataset: Monthly Passengers (Time Series) Airline passenger data with clear trend and yearly seasonality over 12 years (144 monthly observations). Perfect for time series decomposition and forecasting.

Try It Yourself

Time Series
Loading editor...
0/50 runs

Time Series: 144 monthly airline passenger records

Experiment with the seasonal decomposition by changing the model from 'multiplicative' to 'additive' and observing how the residuals change; a poor fit will leave a pattern in the residuals. Try adjusting the differencing lag (e.g., shift(12) for seasonal differencing) to see if you can achieve a stronger stationarity result with a lower p-value. Finally, explore how the ACF and PACF plots shift when you apply different transformations, which provides clues for selecting ARIMA parameters.