Notes

Granger Causality

When two things move together over time—like ad spend and sales—it’s tempting to say one “causes” the other. Granger causality is a practical way to test a narrower, time-based idea: does one series help predict another?

What it means (in plain terms)
A time series X “Granger-causes” Y if past values of X contain useful information for forecasting Y, beyond what past values of Y already provide. It’s about predictive causality, not philosophical cause-and-effect. If adding lagged (past) values of X significantly improves a forecasting model for Y, we say X Granger-causes Y.

How it’s tested
Typically you fit two regression-style time series models (often a VAR, vector autoregression):

  • Restricted model: predict Y using only past Y.
  • Unrestricted model: predict Y using past Y and past X.

An F-test (or similar) checks whether the lagged X terms jointly improve fit. Choosing the number of lags matters, and the series is often assumed to be roughly stationary.

Concrete examples

  • Do last month’s mortgage rates improve predictions of this month’s house sales, beyond house sales history?
  • Do yesterday’s flu-related searches help forecast next week’s clinic visits?
  • Do past machine sensor vibrations help predict future failure rates?

Why it matters in AI/ML
Granger causality is used for feature selection in forecasting, building causal graphs as a starting point, and diagnosing lead–lag relationships in multivariate time series. It can fail when there are hidden confounders, nonlinear effects, or when both series are driven by a third process—so results should be read as “predicts” rather than “proves causes.”

Granger Causality

Granger Causality tests whether past values of one time series improve prediction of another series beyond what the target’s own past explains, typically via VAR or regression models. It indicates predictive directionality, not true causal mechanism, and depends on model specification and stationarity. In AI/ML, it helps select lagged features and infer temporal influence, e.g., whether search trends help forecast product demand.

Granger Causality

Imagine you’re trying to predict tomorrow’s ice cream sales. If knowing today’s temperature helps you make a better prediction than using past sales alone, you might say temperature “drives” sales. Granger causality is a similar idea for data that changes over time: one time series “Granger-causes” another if its past values improve predictions of the other series. It doesn’t prove true cause-and-effect like a controlled experiment; it’s about better forecasting using history. In AI and machine learning, it’s often used to test whether one signal contains useful predictive information about another.

Python
import numpy as np  # numerical arrays and random number generation
import pandas as pd  # time-indexed tabular data
from statsmodels.tsa.stattools import grangercausalitytests  # Granger causality test routine

# --- Create a realistic "marketing spend -> sales" monthly dataset ---
rng = np.random.default_rng(7)  # reproducible randomness
n = 120  # 10 years of monthly observations
idx = pd.date_range("2015-01-01", periods=n, freq="MS")  # monthly start dates

marketing = np.zeros(n)  # marketing spend time series
sales = np.zeros(n)      # sales time series

for t in range(1, n):
    marketing[t] = 0.7 * marketing[t - 1] + rng.normal(0, 1.0)  # persistent spend with noise
    sales[t] = 0.6 * sales[t - 1] + 0.5 * marketing[t - 1] + rng.normal(0, 1.0)  # lagged marketing impacts sales

df = pd.DataFrame({"sales": sales, "marketing": marketing}, index=idx)  # align both series on the same time axis

# --- Test whether past marketing helps predict sales (beyond past sales itself) ---
max_lag = 6  # test up to 6 months of lagged effects
# *** KEY LINE ***
res = grangercausalitytests(df[["sales", "marketing"]], maxlag=max_lag, verbose=False)  # tests "marketing -> sales"

# Extract p-values (ssr F-test) across lags into a compact table for inspection
pvals = {lag: out[0]["ssr_ftest"][1] for lag, out in res.items()}  # p-value is element [1]
pvals = pd.Series(pvals, name="p_value").to_frame().assign(lag=lambda x: x.index)
print(pvals.sort_index())  # smaller p-values at some lags indicate evidence of Granger-causality

# Optional: reverse direction to check "sales -> marketing" as a sanity check
# *** KEY LINE ***
res_rev = grangercausalitytests(df[["marketing", "sales"]], maxlag=max_lag, verbose=False)  # tests "sales -> marketing"
pvals_rev = {lag: out[0]["ssr_ftest"][1] for lag, out in res_rev.items()}
print(pd.Series(pvals_rev, name="p_value").sort_index())
Code Notes
  • The key output is the lag-by-lag p-value table extracted from statsmodels.tsa.stattools.grangercausalitytests; small p-values indicate that adding lagged values of the second series improves prediction of the first.
  • maxlag controls how many past periods are tested; in ML workflows it’s often chosen via domain knowledge (e.g., business cycles) or validated with a holdout period.
  • Common mistake: passing columns in the wrong order—grangercausalitytests expects [target, candidate_cause] in the DataFrame slice.
  • Typical use in AI/ML: feature engineering for forecasting (include lagged drivers) and directional screening before fitting multivariate models (e.g., VAR or dynamic regression).
R
library(lmtest)  # provides grangertest() for Granger causality testing

# --- Create a realistic "marketing spend -> sales" monthly dataset ---
set.seed(7)  # reproducibility
n <- 120  # 10 years of monthly observations
dates <- seq.Date(from = as.Date("2015-01-01"), by = "month", length.out = n)  # monthly index

marketing <- numeric(n)  # marketing spend time series
sales <- numeric(n)      # sales time series

for (t in 2:n) {
  marketing[t] <- 0.7 * marketing[t - 1] + rnorm(1, 0, 1.0)  # persistent spend with noise
  sales[t] <- 0.6 * sales[t - 1] + 0.5 * marketing[t - 1] + rnorm(1, 0, 1.0)  # lagged marketing impacts sales
}

df <- data.frame(date = dates, sales = sales, marketing = marketing)  # keep series aligned by date

# --- Test whether past marketing helps predict sales (beyond past sales itself) ---
lag_k <- 6  # test a 6-month lag structure
# *** KEY LINE ***
gc_ms <- grangertest(sales ~ marketing, order = lag_k, data = df)  # tests "marketing -> sales" at lag_k
print(gc_ms)  # inspect the F-test and p-value for the chosen lag order

# Optional: reverse direction to check "sales -> marketing" as a sanity check
# *** KEY LINE ***
gc_sm <- grangertest(marketing ~ sales, order = lag_k, data = df)  # tests "sales -> marketing" at lag_k
print(gc_sm)
Code Notes
  • The printed result from lmtest::grangertest includes an F-test and p-value for the specified lag order; it summarizes whether lagged predictors add explanatory power.
  • order sets the number of lags included; unlike the Python example that reports multiple lags, this call tests a single chosen lag length.
  • Common mistake: interpreting significance as true causation; in practice, you still need to check stationarity, lag selection, and potential confounders before using the result operationally.
  • Typical use in AI/ML: decide whether to include lagged exogenous drivers in forecasting pipelines (e.g., dynamic regression / ARIMAX-style features).