What Is Zigzag Persistence?
Standard persistent homology watches a single expanding wave: features are born as the scale parameter grows and die when they merge. Zigzag persistence generalises this to a two-way tide: the wave can both advance and retreat, and features are tracked through both the incoming and outgoing water. This makes zigzag persistence appropriate for data that changes through time — populations that grow, shrink, merge, and split across sequential samples.
The name comes from the zigzag shape of the diagram connecting the sequence of spaces: arrows pointing left and right alternate, like the lines of a zigzag seam.
Mathematical Formulation
A zigzag persistence module is a sequence of vector spaces connected by linear maps in alternating directions:
where each denotes a map in either direction (forward or backward). This arises in practice from a sequence of spaces connected by inclusion into unions:
The Structure Theorem for zigzag modules (Carlsson–de Silva 2010) guarantees decomposition into interval modules , yielding a barcode exactly as in ordinary persistence. Feature intervals now represent periods during which a topological feature (component, loop, void) is present in the sequence.
Python Code Stub
import gudhi
def compute_zigzag_persistence(
rips_complex_sequence: list,
) -> list[tuple[int, tuple[float, float]]]:
"""
Compute zigzag persistence from a time-ordered list of precomputed Rips complexes.
Each element is a Gudhi SimplexTree for a time window.
Returns a list of (dimension, (birth, death)) pairs.
The zigzag sequence encodes the alternating inclusions:
X_0 -> X_0 u X_1 <- X_1 -> X_1 u X_2 <- X_2 -> ...
This is built as an arrow filtration — a sequence of (simplex, is_addition)
pairs — where is_addition=True adds a simplex (forward arrow) and
is_addition=False removes one (backward arrow).
Gudhi 3.9 API: compute_zigzag_persistence(arrow_filtration) returns a list
of [dimension, birth, death] triples directly (not a ZigzagPersistence object).
"""
# Step 1: seed with all simplices from the first complex (all forward arrows)
prev_simplices: set[tuple[int, ...]] = set()
first_simplices = {tuple(s) for s, _ in rips_complex_sequence[0].get_filtration()}
arrow_filtration: list[tuple[list[int], bool]] = [
(list(s), True) for s in first_simplices
]
prev_simplices = first_simplices
# Step 2: for each consecutive pair, forward-add new simplices then
# backward-remove departed simplices to traverse X_i -> X_i u X_{i+1} <- X_{i+1}
for st in rips_complex_sequence[1:]:
next_simplices = {tuple(s) for s, _ in st.get_filtration()}
# Forward arrows: add simplices present in next but not prev (X_i -> X_i u X_{i+1})
for s in next_simplices - prev_simplices:
arrow_filtration.append((list(s), True))
# Backward arrows: remove simplices present in prev but not next (X_i u X_{i+1} <- X_{i+1})
for s in prev_simplices - next_simplices:
arrow_filtration.append((list(s), False))
prev_simplices = next_simplices
# compute_zigzag_persistence returns list of [dim, birth, death] triples
raw = gudhi.zigzag_persistence.compute_zigzag_persistence(arrow_filtration)
return [(dim, (birth, death)) for dim, birth, death in raw]
Application to the Research Programme
Zigzag persistence is the primary method in Paper 3, where it is applied to five-year rolling windows of the BHPS/Understanding Society linked panel to track how employment trajectory topology evolves across macroeconomic cycles. The topological complexity index — a summary statistic derived from the sum of H₁ barcode lengths per window — is shown to lead official unemployment statistics by approximately two quarters.
In Paper 7, zigzag-derived complexity indices serve as time-series features in the geometric forecasting model, contributing approximately 15% of combined SHAP feature importance.
The primary computational challenge is that zigzag persistence cannot be trivially parallelised across windows (the zigzag structure requires sequential computation), leading to the ~14-hour runtime on HPC hardware documented in the Computational Log.