ergodicDocs
Products: switch from ergodic to another product
Get started

GuidesDiscover from time series

Discover from time series

Learn which variables drive which, and at what lag, from a time series. You'll run PCMCI on a seeded simulation, read the lagged links and compare them with Granger tests.

You'll need

  • ergodic, installed with pip install "ergodic".
  • One series per variable on a regular time index, or a panel of several units.

Run the program#

The simulation is 500 weeks of three series. Ad spend raises site visits a week later, and visits raise sales two weeks later. Each series also carries its own past.

time_series.pyDownload
import numpy as np
import pandas as pd

from ergodic import DomainKnowledge, discover, granger
from ergodic.data import timeseries

# 1. Simulate 500 weeks: ads -> visits at lag 1, visits -> sales at lag 2.
rng = np.random.default_rng(0)
weeks = 500
ads, visits, sales = np.zeros(weeks), np.zeros(weeks), np.zeros(weeks)
noise = rng.normal(size=(weeks, 3))
for t in range(2, weeks):
    ads[t] = 0.6 * ads[t - 1] + noise[t, 0]
    visits[t] = 0.5 * visits[t - 1] + 0.5 * ads[t - 1] + noise[t, 1]
    sales[t] = 0.5 * sales[t - 1] + 0.4 * visits[t - 2] + noise[t, 2]
frame = pd.DataFrame({"week": np.arange(weeks), "ads": ads, "visits": visits, "sales": sales})
data = timeseries(frame, time="week")

# 2. Run PCMCI with links up to two weeks back.
result = discover(data, method="pcmci", max_lag=2)
print(result)

# 3. Each link is (source, target, lag), with the p-value that kept it.
for (source, target, lag), p in sorted(result.links.items()):
    print(f"{source}[t-{lag}] -> {target}[t]  p={p:.2g}")

# 4. Granger tests answer a narrower, predictive question.
print("granger:", sorted(granger(data, max_lag=2).significant))

# 5. Add the order of the funnel: ads, then visits, then sales.
knowledge = DomainKnowledge().with_tiers([["ads"], ["visits"], ["sales"]])
informed = discover(data, method="pcmci", max_lag=2, knowledge=knowledge)
print(sorted(informed.links) == sorted(result.links))
pruned = int(informed.diagnostics["n_knowledge_pruned"])
tests = int(informed.diagnostics["n_ci_tests"]), int(result.diagnostics["n_ci_tests"])
print(f"{pruned} links pruned; {tests[0]} tests instead of {tests[1]}")
Recorded output
TimeSeriesDiscoveryResult(pcmci/linear: dag on 9 window nodes, max_lag=2, 5 lagged links, 0 contemporaneous, alpha=0.05, n=496)
ads[t-1] -> ads[t]  p=0
ads[t-1] -> visits[t]  p=0
sales[t-1] -> sales[t]  p=0
visits[t-2] -> sales[t]  p=4.4e-15
visits[t-1] -> visits[t]  p=0
granger: [('ads', 'visits'), ('visits', 'sales')]
True
9 links pruned; 37 tests instead of 49

Read the window graph#

A TimeSeriesDiscoveryResult is a DiscoveryResult with two more fields: max_lag, and links, which maps (source, target, lag) to a p-value. A lag of 0 is a same-week edge. An oriented one is keyed (source, target, 0); an undirected one once, with the pair in sorted order.

result.graph lives on window nodes such as visits[t-2]. Its kind is dag when every edge is oriented and mixed otherwise. result.summary_edges() flattens it to variable pairs, with (x, x) for a series' own memory. There is no single variable-level graph on purpose: x can drive y at one lag while y drives x at another.

Knowledge reads variables as processes#

A constraint on (x, y) applies at every lag:

n_knowledge_pruned counts what the knowledge removed.

A same-week effect can add lagged links

PCMCI here tests each lagged link given lagged parents only. If y[t] causes z[t] in the same week, the past of y and of y's causes can also show up as lagged links into z. Read lagged links into a variable with a same-week parent with care.

Granger is a predictive test

granger asks whether the past of x improves the forecast of y, given every variable's past. A common cause you didn't measure makes x look causal, and it ignores same-week effects. That's why it returns a table of pairs (GrangerResult), not a graph. It needs continuous columns.

Can I use a panel of several units?

Yes. Build it with ergodic.data.panel(frame, entity=..., time=...) and pass it to discover(..., method="pcmci"). Windows are built within each unit and pooled, so no window crosses two units. granger takes a single TimeSeriesData only.

Can I bootstrap a time series?

Yes: bootstrap(data, method="pcmci", max_lag=2) resamples blocks of consecutive weeks. See Check stability with the bootstrap.

Next steps#

Documentation

Search ergodic 0.2.0

Search ergodic by task, function or error.