Remark

Please be aware that these lecture notes are accessible online in an ‘early access’ format. They are actively being developed, and certain sections will be further enriched to provide a comprehensive understanding of the subject matter.

3.5. Stationarity and Non-Stationarity in Time-Series Analysis#

3.5.1. What is Stationarity?#

3.5.1.1. A Conceptual Guide#

In time-series analysis, the concept of stationarity is the bedrock upon which many of our statistical models are built. But what does it actually mean?

Intuitively, a time series is stationary when the underlying system generating the data doesn’t change over time.

Think of it this way: Imagine you are measuring the temperature in a room where a perfectly functioning thermostat is set to 70°F.

Over the course of a day, your thermometer readings will fluctuate—sometimes it’s 69.8°F, sometimes 70.2°F—but the behavior of the data follows a strict set of rules:

  • The average temperature stays anchored at 70°F.

  • The variation (how far the temperature swings) remains consistent.

  • The patterns of how one reading relates to the next don’t shift.

This is stationarity: the fundamental ‘’rules of the game” remain constant, regardless of when you look at the data.

3.5.1.2. Real-World Analogies#

To spot the difference in the wild, compare these scenarios:

The Stationary World
  • Dice Rolls: Every time you roll a die, the probability of getting a 6 is exactly the same. The rules of the die don’t change from roll to roll.

  • Heart Rate at Rest: For a healthy person, their resting heart rate might fluctuate beat-to-beat, but it hovers around a consistent average (e.g., 60 bpm) without wandering off indefinitely.

  • Climate Normals: Daily temperature (after we remove seasonal effects) tends to bounce around a long-term average.

The Non-Stationary World
  • Stock Prices: These are classic non-stationary examples. They can drift upward or downward over decades; the average price in 1980 is very different from the average price today.

  • Company Revenue: Successful companies often see revenue that grows over time (a trend).

  • Website Traffic: This data often has complex structures, like daily peaks or weekly cycles (seasonality) combined with long-term growth.

3.5.2. Simulating the Difference#

To visualize this, let’s generate two synthetic datasets that mimic these real-world analogs.

  1. Stationary Example: We will simulate a “Heart Rate at Rest” using an AR(1) process. This model pulls values back toward a mean of zero, keeping the series bounded and consistent.

  2. Non-Stationary Example: We will simulate “Website Traffic” using a Random Walk with Drift. We’ll also add a weekly seasonal cycle to make it realistic. You’ll see this series wander away from its starting point and repeat a 7-day pattern.

Hide code cell content

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

np.random.seed(3030)

# --- 1. STATIONARY DATA: Heart Rate at Rest ---
n_hr = 1000
phi = 0.6
sigma = 1.0
mean_hr = 72  # Define the mean heart rate

noise_hr = np.random.normal(0, sigma, size=n_hr)
X = np.zeros(n_hr)
for t in range(1, n_hr):
    X[t] = phi * X[t-1] + noise_hr[t]

X = X + mean_hr  # Shift to realistic heart rate

time_index_hr = pd.date_range(start=pd.Timestamp.now(), periods=n_hr, freq='s')
df_hr = pd.DataFrame({'date': time_index_hr, 'hr_val': X})

# --- 2. NON-STATIONARY DATA: Website Traffic ---
n_tr = 400
mu = 0.05  # Daily drift
time_index_traffic = pd.date_range('2024-01-01', periods=n_tr, freq='D')

# Calculate seasonality using the day of week from the index
season = 2.0 * np.sin(2 * np.pi * (time_index_traffic.dayofweek) / 7)
innov = np.random.normal(0, 1.0, size=n_tr)

Z = np.zeros(n_tr)
for t in range(1, n_tr):
    Z[t] = Z[t-1] + mu + innov[t]

Z_final = Z + season
df_traffic = pd.DataFrame({'date': time_index_traffic, 'traffic_val': Z_final})
Heart Rate at Rest
Loading ITables v2.6.2 from the init_notebook_mode cell... (need help?)
Website Traffic
Loading ITables v2.6.2 from the init_notebook_mode cell... (need help?)
../_images/stationary_vs_nonstationary.png

Fig. 3.24 Comparison of Stationary vs. Non-Stationary Processes#

Fig. 3.24 contrasts these two distinct behaviors using our real-world examples:

  • Top Panel (Stationary Heart Rate): We can see the data “vibrating” around a constant center (our resting heart rate). Notice how the series never escapes its baseline; even when a random spike occurs, it is immediately pulled back toward the middle. Because the average and the spread of the wiggles stay the same throughout the entire timeline, we call this stationary. If we looked at any small slice of this graph, it would look statistically identical to any other slice.

  • Bottom Panel (Non-Stationary Website Traffic): In our traffic data, the “rules” are constantly shifting. We see a clear upward drift—the mean is higher at the end of the chart than at the beginning. We also see a repeating weekly “wave” (seasonality) that gives the data a specific structure depending on the day of the week. Because the average value and the patterns change as time passes, we call this non-stationary.

Decoding the Jargon: What is an AR(p) Process?

When we look at time series models, we’ll often see terms like AR(1), AR(2), or AR(p). We shouldn’t let the notation scare us—it’s just our way of counting “memory.”

“Autoregressive” (AR) simply means the data predicts itself (auto = self, regressive = prediction).

The number we put in the parentheses tells us how many steps back the memory goes:

  • AR(1) = “Short-Term Memory”

    (3.15)#\[\begin{equation} X_t = \phi X_{t-1} + \epsilon_t \end{equation}\]
    • Our Rule: Today’s value depends only on what happened one step ago (plus some random noise \(\epsilon_t\)).

    • The Vibe (Heart Rate): We can think of this like a rubber band or a pendulum swinging back to center. In our heart rate example, if the previous second was high, the next one likely stays near it, but the “pull” of the \(\phi\) coefficient ensures the connection fades fast and we return to our average.

  • AR(2) = “Richer Memory”

    (3.16)#\[\begin{equation} X_t = \phi_1 X_{t-1} + \phi_2 X_{t-2} + \epsilon_t \end{equation}\]
    • Our Rule: Today’s value depends on both Yesterday and the Day Before.

    • The Vibe: This gives our data more complex movements, like a wave that oscillates or bounces. We are remembering not just where we are, but the direction we were coming from.

  • AR(p) = “Deep Memory”

    (3.17)#\[\begin{equation} X_t = \phi_1 X_{t-1} + \phi_2 X_{t-2} + \cdots + \phi_p X_{t-p} + \epsilon_t \end{equation}\]
    • Our Rule: Today depends on the last \(p\) steps.

    • The Vibe: The higher we set \(p\), the more complex the historical patterns we can capture in our models.

Contrast with our Random Walk (Non-Stationary):

(3.18)#\[\begin{equation} Z_t = Z_{t-1} + \mu + \epsilon_t \end{equation}\]
  • Notice that we have no coefficient pulling us back (unlike the \(\phi\) in our AR model).

  • Our value just drifts forever based on wherever it was yesterday plus a trend \(\mu\).

  • In our website traffic example: This is why we see the data “wandering” upward—it lacks the “memory” that pulls it back to a constant mean.

3.5.3. Types of Stationarity#

In time-series analysis, we generally talk about two levels of stationarity: the “impossible ideal” (Strict) and the “practical compromise” (Weak).

  1. Strict Stationarity (The Ideal)

  2. Weak Stationarity (The Practical Standard)

3.5.4. Strict Stationarity#

Strict stationarity [Grenander and Rosenblatt, 2008] represents a very high bar to clear in time series analysis. It demands that the entire probability distribution of your data remains identical, regardless of where you look in time.

In formal terms, for any set of time points and any time shift (or lag) , the joint distributions must be equal:

(3.19)#\[\begin{equation} (X_{t_1}, \ldots, X_{t_n}) \stackrel{d}{=} (X_{t_1+h}, \ldots, X_{t_n+h}) \end{equation}\]

Practically, this means much more than simply keeping a stable average. It requires every single statistical property—the mean, variance, skewness, kurtosis, and even higher-order moments—to remain frozen in time. If you were to take a snippet of data from January and compare it to another from June, they must be statistically indistinguishable. Furthermore, any relationship you observe today must hold exactly true tomorrow, next week, and in the next century.

To understand this intuitively, consider the example of flipping a fair coin every day for a year. On January 1st, the probability of obtaining Heads is 50%; on December 31st, that probability remains exactly 50%. Even the chance of flipping three Heads in a row remains the same in winter as it does in summer. Because the underlying rules (the physics of the coin) never change and the flips are independent and identical (IID), this process is strictly stationary.

However, applying this in the real world is challenging. While strict stationarity is theoretically useful, it is practically impossible to verify because proving that every possible statistical moment is constant would require infinite data.

Hide code cell content

import numpy as np
import pandas as pd

np.random.seed(4242)

T = 400
# Simulate fair coin flips (Bernoulli trial, p=0.5)
coin = np.random.binomial(1, 0.5, size=T)

# Calculate a rolling mean just to visualize the "average" behavior over time
# We use a centered window of 25 periods
roll_mean = pd.Series(coin).rolling(25, min_periods=1, center=True).mean()

# Organize into a DataFrame
df_strict = pd.DataFrame({
    'flip_outcome': coin,
    'rolling_mean': roll_mean
})
Loading ITables v2.6.2 from the init_notebook_mode cell... (need help?)
../_images/strict_stationarity_coin_flips.png

Fig. 3.25 Strict Stationarity: Whether you look at Window A or Window B, the probability distribution (the randomness of the coin) is identical.#

3.5.4.1. Weak Stationarity#

Because strict stationarity [Grenander and Rosenblatt, 2008] is so practically difficult to prove, analysts usually settle for Weak Stationarity (often called Covariance Stationarity). This is the version actually tested for in most econometric models.

In this framework, the requirements are relaxed. We stop worrying about the entire distribution and focus only on the first two “moments.” To achieve weak stationarity, the data must satisfy three specific conditions:

  1. Constant Mean: The average value (\(E[X_t]\)) doesn’t change over time.

  2. Constant Variance: The spread of the data (\(Var(X_t)\)) is stable.

  3. Constant Autocovariance: The relationship between two points depends only on the time distance (lag) between them, not when they occurred.

To see this in action, we often look at an AR(1) process. This type of process is considered weakly stationary because it fluctuates around 0 with a stable variance, and its “memory” fades consistently over time.

To verify this behavior, we use the ACF (Autocorrelation Function). This is our primary tool for visualizing that “memory,” allowing us to confirm that correlations diminish as the time lag increases.

Hide code cell source

import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import acf

np.random.seed(4242)

T = 600
phi = 0.6
noise = np.random.normal(0, 1.0, size=T)
X = np.zeros(T)

# Generate AR(1) process
for t in range(1, T):
    X[t] = phi * X[t-1] + noise[t]

# Calculate Autocorrelation Function (ACF)
# This calculates how related X(t) is to X(t-lag)
max_lag = 30
acf_vals = acf(X, nlags=max_lag, fft=True)

# Organize into a DataFrame
df_weak = pd.DataFrame({'ar1_value': X})
df_acf = pd.DataFrame({'lag': range(max_lag+1), 'acf': acf_vals})

Weak stationary series:

Loading ITables v2.6.2 from the init_notebook_mode cell... (need help?)
../_images/weak_stationarity_acf.png

Fig. 3.26 Weak Stationarity: The top panel shows the series fluctuating around a constant mean. The bottom panel (ACF) shows how the “memory” of the process depends only on the time lag.#

3.5.5. The Three Pillars of Weak Stationarity#

To understand weak stationarity [Grenander and Rosenblatt, 2008] fully, we must break down its three essential pillars. Each addresses a different aspect of behavioral stability, starting with the most fundamental: the mean.

3.5.5.1. Condition 1: Constant Mean — The “Center” Never Drifts#

The first condition requires a Constant Mean, meaning the “center” of the data never drifts. Mathematically, this is expressed as:

(3.20)#\[\begin{equation} \mathbb{E}[X_t] = \mu \quad \text{for all } t \end{equation}\]

In practice, this ensures that the long-run average is stable. Whether you examine the first 100 observations or the last, the center of the distribution stays put.

Consider a real-world example of a factory producing widgets. On average, the factory might make 1000 widgets per day. While production naturally fluctuates—hitting 950 on one day and 1050 on another—the average across January, June, and December remains anchored at 1000 units. Crucially, there is no slow drift upward or downward over the year. To visualize this, we can simulate daily production data that varies randomly but adheres strictly to this fixed long-term average.

Hide code cell source

import numpy as np
import pandas as pd

np.random.seed(5151)

n = 365
idx = pd.date_range('2024-01-01', periods=n, freq='D')
mu = 1000                    # constant mean (e.g., widgets/day)
noise = np.random.normal(0, 60, size=n)

X = mu + noise

# Calculate segment means to verify stability
seg1 = slice(0, n//3)
seg2 = slice(n//3, 2*n//3)
seg3 = slice(2*n//3, n)

mean1 = X[seg1].mean()
mean2 = X[seg2].mean()
mean3 = X[seg3].mean()
overall = X.mean()

# Organize into a DataFrame
df_constant_mean = pd.DataFrame({
    'date': idx,
    'production': X
})

print(f"Segment 1 mean: {mean1:.1f}")
print(f"Segment 2 mean: {mean2:.1f}")
print(f"Segment 3 mean: {mean3:.1f}")
print(f"Overall mean: {overall:.1f}")
Segment 1 mean: 1000.8
Segment 2 mean: 995.6
Segment 3 mean: 997.8
Overall mean: 998.1
Loading ITables v2.6.2 from the init_notebook_mode cell... (need help?)
../_images/weak_stationarity_constant_mean.png

Fig. 3.27 Weak Stationarity – Condition 1: The mean remains constant across the beginning, middle, and end of the series. Notice how the segment means (green dashed lines for three equal segments) all hover around 1000 units.#

3.5.5.2. Condition 2: Finite, Constant Variance — The “Spread” Stays Stable#

Following the requirement for a constant mean, the second condition dictates a Finite, Constant Variance, ensuring the “spread” of the data remains stable. Mathematically, this is defined as [Grenander and Rosenblatt, 2008]:

(3.21)#\[\begin{equation} \text{Var}[X_t] = \sigma^2 < \infty \quad \text{for all } t \end{equation}\]

In practice, this means the amount of variability—or volatility—around the mean does not explode or shrink over time. Good days and bad days happen with roughly the same frequency and magnitude throughout the entire series.

Consider daily stock returns as a real-world example. If the average return is 0.05% per day, and the typical deviation is ±2%, a stationary series maintains this consistency. You do not suddenly see weeks where returns swing wildly by ±10% followed by weeks where they barely move. To illustrate this, we can generate simulated stock returns with stable parameters and visualize how the rolling standard deviation (volatility) stays roughly constant.

Hide code cell source

import numpy as np
import pandas as pd

np.random.seed(6161)

n = 600                         # ~2.5 years of trading days
idx = pd.date_range('2023-01-01', periods=n, freq='B')
mu = 0.0005                     # 0.05% daily mean
sigma = 0.02                    # 2% daily standard deviation

# Simulated returns with constant variance
r = np.random.normal(mu, sigma, size=n)

# Rolling volatility estimates
win_short = 20                  # ~1 month
win_long  = 60                  # ~3 months
roll_std_short = pd.Series(r, index=idx).rolling(win_short, min_periods=win_short).std(ddof=1)
roll_std_long  = pd.Series(r, index=idx).rolling(win_long,  min_periods=win_long).std(ddof=1)

# Organize into a DataFrame
df_constant_var = pd.DataFrame({
    'date': idx,
    'returns': r,
    'rolling_std_20d': roll_std_short,
    'rolling_std_60d': roll_std_long
})
Loading ITables v2.6.2 from the init_notebook_mode cell... (need help?)
../_images/weak_stationarity_constant_variance.png

Fig. 3.28 Weak Stationarity – Condition 2: The spread (variance) of the returns stays stable over time. The rolling standard deviation lines hover around the true σ value.#

3.5.5.3. Condition 3: Time-Invariant Covariance — Relationships Depend Only on Distance#

The final piece of the puzzle is Time-Invariant Covariance, which ensures that relationships depend only on distance, not on the calendar date. Mathematically, this condition is expressed as [Grenander and Rosenblatt, 2008]:

(3.22)#\[\begin{equation} \text{Cov}[X_t, X_{t+h}] = \gamma(h) \quad \text{(function of lag } h \text{ only)} \end{equation}\]

n practice, this means the strength of the relationship between observations depends only on how far apart they are in time (\(h\)), not on the specific moment \(t\) when they occurred.

Think about daily temperature as a real-world example. Today’s temperature is usually highly correlated with yesterday’s—perhaps a correlation of 0.8. In a stationary series, this 0.8 correlation holds whether you are observing winter months or summer months. The lag (1 day) determines the relationship, not the season.

A key insight here is that the “memory” of the process is consistent. If you pick two random weeks from your dataset—say, Week 5 and Week 30—and compute the autocorrelation structure within each, they should look nearly identical. To demonstrate this, we can generate a stationary AR(1) process and compare the autocorrelation function (ACF) from two different time windows; if the process is truly stationary, these ACFs will align.

Hide code cell source

import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import acf

np.random.seed(7272)

T = 800
phi = 0.7
noise = np.random.normal(0, 1.0, size=T)
X = np.zeros(T)
for t in range(1, T):
    X[t] = phi * X[t-1] + noise[t]

# Two disjoint windows to compare within-window ACFs
w = 150
w1 = slice(100, 100+w)
w2 = slice(500, 500+w)
X1 = X[w1]
X2 = X[w2]

# Global ACF and within-window ACFs
max_lag = 20
acf_global = acf(X, nlags=max_lag, fft=True)
acf_w1 = acf(X1, nlags=max_lag, fft=True)
acf_w2 = acf(X2, nlags=max_lag, fft=True)

# Organize into DataFrames
df_time_invariant = pd.DataFrame({'ar1_value': X})
df_acf_comparison = pd.DataFrame({
    'lag': range(max_lag+1),
    'acf_global': acf_global,
    'acf_window_a': acf_w1,
    'acf_window_b': acf_w2
})
Loading ITables v2.6.2 from the init_notebook_mode cell... (need help?)
../_images/weak_stationarity_time_invariant_covariance.png

Fig. 3.29 Weak Stationarity – Condition 3: The autocorrelation structure depends only on lag, not calendar time. Window A and Window B are from different periods, yet their ACF curves nearly overlap with the full-series ACF.#

3.5.6. Practical Stationarity Checklist#

When you’re working with real data, you won’t always have the luxury of mathematical proofs. Instead, you need practical heuristics to quickly assess whether your series is behaving in a stationary way. Here are four visual and conceptual checks you can perform.

  • Good Sign: The data oscillates around the same horizontal level from beginning to end.

  • Red Flag: You see systematic upward or downward drift over time—the “center” is moving.

  • Visual Test: Imagine drawing a horizontal line through your data. Does it capture the “average” equally well in January and December?

  • Good Sign: The “spread” or “width” of fluctuations remains similar across all time periods.

  • Red Flag: Some periods show wild swings while others are calm (think stock market volatility clustering).

  • Visual Test: Are the peaks and valleys roughly the same size throughout your dataset?

  • Good Sign: The pattern of how consecutive observations relate to each other stays consistent over time.

  • Red Flag: Sometimes observations are highly correlated, other times they appear independent.

  • Technical Test: The autocorrelation function (ACF) should depend only on the lag \(h\), not on which period you’re analyzing.

⚠️ Be Careful Here

  • Maybe OK: Regular, repeating patterns can still be stationary (see the special case below).

  • Red Flag: Seasonal patterns with changing amplitude (e.g., winter variance growing over the years) or shifting timing.

  • Key Question: Is the seasonal pattern itself evolving over time?

../_images/Practical_Stationarity_Checklist.png

Fig. 3.30 A visual guide to the four key heuristic checks—constant mean, constant variance, consistent autocorrelation, and stable seasonal components—used to assess whether a time series is stationary. (Image generated using Google Gemini).#

3.5.7. Common Stationary Time Series#

Now that we’ve got a feel for what it means for a process to be stationary, let’s take a closer look at two of the most common examples you’ll encounter: white noise and autoregressive (AR) models.

3.5.7.1. White Noise — The “Gold Standard” of Stationarity#

Mathematically, white noise is defined as:

(3.23)#\[\begin{equation} X_t \sim \text{i.i.d.}(\mu, \sigma^2) \end{equation}\]

This means that each observation in the series is drawn independently from the same distribution.

Some key features of white noise include:

  1. Independence: Every observation is completely unrelated to the others.

  2. Identical distribution: The underlying probability distribution doesn’t change over time.

  3. Constant mean and variance: \(\mathbb{E}[X_t] = \mu\) and \(\text{Var}[X_t] = \sigma^2\).

  4. Zero autocorrelation: \(\text{Corr}[X_t, X_{t+h}] = 0\) for all \(h \neq 0\).

The most common form of white noise is Gaussian white noise, written as:

(3.24)#\[\begin{equation} X_t \sim \mathcal{N}(0, \sigma^2) \quad \text{independently.} \end{equation}\]

You can think of white noise as representing completely random fluctuations without any predictable structure. Examples include measurement errors from a precise instrument, daily stock returns (roughly, under the efficient market hypothesis), regression residuals from a well-fitted model, or even a series of fair coin flips coded as 0 and 1.

White noise matters because it serves as the simplest example of a stationary process and provides a foundation for more complex models. It’s often used as a benchmark—many models can be described as transformations of white noise—and as a diagnostic tool—if your model’s residuals resemble white noise, that’s a sign you’ve captured the underlying patterns of your data well.

Next, we’ll see how white noise extends into something more structured: the autoregressive model.

Hide code cell source

import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import acf

np.random.seed(1010)

T = 400
mu = 0.0
sigma = 1.5

# Generate Gaussian white noise
wn = np.random.normal(mu, sigma, size=T)

# Compute ACF
max_lag = 30
acf_wn = acf(wn, nlags=max_lag, fft=True)

# Organize into DataFrames
df_white_noise = pd.DataFrame({
    'time': range(T),
    'white_noise': wn
})

df_acf_wn = pd.DataFrame({
    'lag': range(max_lag+1),
    'acf': acf_wn
})

print("White Noise Data (first 20 observations):")
display(df_white_noise.head(20).set_index('time'))
White Noise Data (first 20 observations):
white_noise
time
0 -1.763172
1 -0.574722
2 -2.207049
3 -2.700853
4 0.195151
5 2.393428
6 1.489741
7 -3.545561
8 -0.719388
9 -2.475573
10 -0.815234
11 1.169417
12 -0.753913
13 0.428834
14 4.054856
15 -0.111775
16 -2.055154
17 0.537881
18 0.897075
19 -0.460199
../_images/white_noise.png

Fig. 3.31 White Noise: The top panel shows completely random, uncorrelated observations. The bottom panel (ACF) shows zero correlation at all lags except lag 0, confirming independence.#

3.5.7.2. Stationary Autoregressive (AR) Processes#

White noise is completely random, but most real-world time series aren’t like that. In reality, what happens today often depends, at least a little, on what happened yesterday. That’s where autoregressive (AR) processes come into play.

The simplest version is the AR(1) model, defined as:

(3.25)#\[\begin{equation} X_t = \phi X_{t-1} + \epsilon_t \quad \text{where } |\phi| < 1 \text{ and } \epsilon_t \sim \text{WN}(0, \sigma^2) \end{equation}\]

Here, each new value \(X_t\) depends partly on its previous value \(X_{t-1}\) and partly on a random shock \(\epsilon_t\).

Some key characteristics of an AR(1) process are:

  1. Dependence on the past: The coefficient \(\phi\) controls how strongly each value depends on the previous one.

  2. Mean reversion: When the process drifts away from its long-term average, it gradually moves back toward it.

  3. Finite variance: Unlike a random walk, the variability doesn’t blow up over time.

  4. Autocorrelation decay: The correlation between observations decreases exponentially with lag, following \(\text{Corr}[X_t, X_{t+h}] = \phi^h\).

For the process to be stationary, the absolute value of \(\phi\) must be less than 1. When \(|\phi| < 1\), the series is stable and mean-reverting. If \(\phi = 1\), it behaves like a random walk—non-stationary and unpredictable. And if \(|\phi| > 1\), the process becomes explosive, with values that drift off to infinity.

A simple way to visualize this is with the ball and spring analogy. Imagine a ball attached to a spring:

  • The spring represents the pull of mean reversion.

  • The shocks (\(\epsilon_t\)) are random pushes that move the ball off its equilibrium position.

  • The balance between these pushes and the spring’s pull gives rise to the gentle ups and downs of a stationary process.

If \(\phi\) is near 1, the spring is loose—the ball takes a long time to return. If \(\phi\) is smaller (say, 0.3), the spring is tight—pulling the ball back quickly after each shock.

You can find AR(1)-type behavior in many real situations:

  • Interest rates, which tend to revert toward central bank targets.

  • Exchange rates, which drift but are pulled toward purchasing power parity.

  • Commodity prices, shaped by supply-demand adjustments.

  • Temperature anomalies, which oscillate around long-term climate averages.

Next, we’ll look at how to simulate an AR(1) process and visualize its dynamics in action.

Hide code cell source

import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import acf

np.random.seed(2020)

T = 500
phi = 0.7
sigma = 1.0

# Generate AR(1) process
epsilon = np.random.normal(0, sigma, size=T)
ar1 = np.zeros(T)
for t in range(1, T):
    ar1[t] = phi * ar1[t-1] + epsilon[t]

# Compute ACF
max_lag = 30
acf_ar1 = acf(ar1, nlags=max_lag, fft=True)

# Theoretical ACF (phi^h)
theoretical_acf = phi ** np.arange(max_lag+1)

# Organize into DataFrames
df_ar1 = pd.DataFrame({
    'time': range(T),
    'ar1_value': ar1
})

df_acf_ar1 = pd.DataFrame({
    'lag': range(max_lag+1),
    'acf_sample': acf_ar1,
    'acf_theoretical': theoretical_acf
})

print("AR(1) Data (first 20 observations):")
display(df_ar1.head(20).set_index('time'))
AR(1) Data (first 20 observations):
ar1_value
time
0 0.000000
1 0.075552
2 -1.077743
3 -1.405850
4 -1.877211
5 -2.588149
6 -1.872858
7 -1.246487
8 -0.462428
9 -0.896582
10 -1.428941
11 0.311776
12 1.492942
13 -0.169298
14 0.195211
15 -1.308174
16 -1.284683
17 -1.668505
18 -0.775337
19 -0.485442
../_images/ar1_process.png

Fig. 3.32 AR(1) Process with φ=0.7: The top panel shows mean-reverting behavior around zero. The bottom panel shows exponentially decaying autocorrelation, with sample ACF (orange) closely matching the theoretical curve (blue dashed line).#

3.5.7.3. Extended AR(p) Process#

So far, we’ve looked at the simplest autoregressive model, where each observation depends only on the previous one. But in many real-world scenarios, today’s value might also be influenced by data from two, three, or even more time steps back. To capture those longer memory effects, we extend the model to an AR(p) process.

The general form looks like this:

(3.26)#\[\begin{equation} X_t = \phi_1 X_{t-1} + \phi_2 X_{t-2} + \dots + \phi_p X_{t-p} + \epsilon_t \end{equation}\]

Here, \(p\) represents the number of past terms (or “lags”) that help determine the current value \(X_t\).

For the process to be stationary, all roots of the characteristic polynomial must lie outside the unit circle. This is a generalization of the simpler condition \(|\phi| < 1\) that we used for the AR(1) model.

What does adding more lags actually do?

  1. Adds memory: The model can look further back in time, not just one step.

  2. Creates richer patterns: With multiple coefficients, the series can display oscillations, cycles, or more subtle correlations over time.

  3. Remains mean-reverting: Even with more complexity, the process still gravitates back toward its long-term mean.

This added flexibility makes AR(p) models extremely useful in practice. They form the foundation of ARIMA models (the cornerstone of classical time series forecasting) and are widely used in economics, finance, and climate modeling. For instance, GDP growth, unemployment rates, and many macroeconomic indicators often follow AR-like structures.

By adjusting the number of lags and the coefficients, an AR(p) model can fit a wide variety of temporal behaviors—offering a balance between simplicity and the ability to capture real-world dynamics.

Next, we’ll explore how to simulate and visualize an AR(2) process to see how added memory shapes the data’s appearance over time.

Hide code cell source

import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import acf

np.random.seed(3030)

T = 500
phi1 = 0.6
phi2 = 0.3
sigma = 1.0

# Generate AR(2) process
epsilon = np.random.normal(0, sigma, size=T)
ar2 = np.zeros(T)
for t in range(2, T):
    ar2[t] = phi1 * ar2[t-1] + phi2 * ar2[t-2] + epsilon[t]

# Compute ACF
max_lag = 30
acf_ar2 = acf(ar2, nlags=max_lag, fft=True)

# Organize into DataFrames
df_ar2 = pd.DataFrame({
    'time': range(T),
    'ar2_value': ar2
})

df_acf_ar2 = pd.DataFrame({
    'lag': range(max_lag+1),
    'acf': acf_ar2
})

print("AR(2) Data (first 20 observations):")
display(df_ar2.head(20).set_index('time'))
AR(2) Data (first 20 observations):
ar2_value
time
0 0.000000
1 0.000000
2 0.420205
3 0.120762
4 0.962059
5 0.412693
6 1.712811
7 -0.567000
8 -0.443548
9 -0.625700
10 0.936077
11 -0.635702
12 -0.865037
13 1.196572
14 -1.266921
15 -0.797223
16 2.514767
17 2.712615
18 2.664726
19 1.591058
../_images/ar2_process.png

Fig. 3.33 AR(2) Process with φ₁=0.6, φ₂=0.3: The ACF shows a richer decay pattern compared to AR(1), reflecting the influence of two past values rather than one.#

Key Takeaways for This Section

Hierarchy of Stationarity

  1. Strict stationarity → Complete distributional invariance (theoretical ideal, rarely testable).

  2. Weak stationarity → Constant mean, variance, and time-invariant covariance (practical focus).

  3. Practical checklist → Observable characteristics you can spot in real data.

Counter-Intuitive Insights

  • Periodic ≠ Non-stationary: Regular, repeating patterns can be stationary if their amplitude, frequency, and phase distribution don’t change.

  • White noise ≠ “No information”: Even purely random data has clear statistical structure (constant mean, variance, zero autocorrelation).

  • AR processes balance memory and stability: Past dependence doesn’t prevent stationarity as long as the coefficients create mean reversion (\(|\phi| < 1\)).