The simulation algorithms presented in earlier chapters are correct but not always efficient. This chapter covers advanced techniques: the branching representation for Hawkes processes, vectorized O(n) computation, strategies for large-scale simulation, and a validation framework that ensures your code is correct.
An alternative to Ogata’s thinning is the cluster (branching) simulation, which directly exploits the Galton-Watson branching structure of the Hawkes process:
Algorithm:
t_bg ~ HPP(μ) on [0, T]K ~ Poisson(n*)d ~ φ(·)/n* (normalized kernel as a density)For the exponential kernel φ(t) = α·exp(−βt):
n* = α/βd ~ Exponential(β) (since φ(t)/n* = β·exp(−βt))def simulate_hawkes_branching(mu, alpha, beta, T):
n_star = alpha / beta
all_events = []
def generate_offspring(t_parent):
k = np.random.poisson(n_star)
delays = np.random.exponential(1.0 / beta, size=k)
for d in delays:
t_child = t_parent + d
if t_child < T:
all_events.append(t_child)
generate_offspring(t_child)
# Background events
n_bg = np.random.poisson(mu * T)
bg_times = np.random.uniform(0, T, n_bg)
for t in bg_times:
all_events.append(t)
generate_offspring(t)
return np.sort(all_events)
Comparison with Ogata thinning:
The recursive computation of Rᵢ = exp(−β·Δtᵢ) · (1 + Rᵢ₋₁) is inherently sequential — each step depends on the previous. However, the compensator computation and MLE gradient can be parallelized.
For large n (millions of events), avoid Python loops by using numba JIT compilation:
from numba import njit
@njit
def hawkes_loglik_fast(events, T, mu, alpha, beta):
n = len(events)
R = 0.0
loglik = 0.0
comp = 0.0
for i in range(n):
if i > 0:
dt = events[i] - events[i-1]
R = np.exp(-beta * dt) * (1.0 + R)
intensity = mu + alpha * R
loglik += np.log(intensity)
comp += 1.0 - np.exp(-beta * (T - events[i]))
loglik -= mu * T + (alpha / beta) * comp
return loglik
With numba, this runs ~100x faster than a pure Python loop for n = 100,000 events.
| Operation | Naive | With recursion |
|---|---|---|
Evaluate λ*(t) at all n event times |
O(n²) | O(n) |
Compute compensator Λ*(T) |
O(n²) | O(n) |
Log-likelihood ℓ(θ) |
O(n²) | O(n) |
Gradient ∇ℓ(θ) |
O(n²) | O(n) |
| Ogata thinning simulation | O(n) | O(n) |
| Branching simulation | O(n) | O(n) |
The O(n²) vs O(n) difference is critical: for n = 10,000, naive computation takes ~100× longer.
Truncation for long histories: If the event sequence is very long, the exponential kernel decays rapidly. For events more than τ_cutoff = 5/β time units in the past, exp(−β·τ_cutoff) < 0.007 — negligible. We can safely truncate the sum:
# Only include events within a window of 5/beta
def hawkes_intensity_truncated(t, past_events, mu, alpha, beta):
window = 5.0 / beta
recent = past_events[past_events > t - window]
return mu + alpha * np.sum(np.exp(-beta * (t - recent)))
For very long T (e.g., simulating years of seismicity):
Stationary initialization: Don’t start from empty history (there is a transient). Run for a burn-in period and discard the first T_burn events.
Memory management: Store only the last 5/β seconds of history at any time — older events don’t contribute.
Parallel independent runs: For uncertainty estimation, simulate K independent realizations in parallel using multiprocessing or joblib.
from joblib import Parallel, delayed
results = Parallel(n_jobs=4)(
delayed(simulate_hawkes_branching)(mu, alpha, beta, T)
for _ in range(K)
)
Every simulation function should be validated by checking that estimates from simulated data recover the known parameters:
def validate_simulator(true_params, n_sim=100, T=200):
mu, alpha, beta = true_params
estimates = []
for _ in range(n_sim):
events = simulate_hawkes_branching(mu, alpha, beta, T)
est = fit_hawkes(events, T).x
estimates.append(est)
estimates = np.array(estimates)
print("True: ", true_params)
print("Mean est:", estimates.mean(axis=0))
print("Bias: ", estimates.mean(axis=0) - true_params)
print("Std err: ", estimates.std(axis=0))
# Check ~95% coverage
z = (estimates - true_params) / estimates.std(axis=0)
print("Coverage:", (np.abs(z) < 1.96).mean(axis=0))
This detects bugs in the simulation or estimation code and measures finite-sample bias.
See code/13_advanced_simulation.py for benchmarks comparing Ogata vs. branching simulation, vectorized implementations, and a full validation harness.
Rᵢ reduces log-likelihood computation from O(n²) to O(n).numba for large-scale simulation; truncate distant history for efficiency.| ← Chapter 12 | Table of Contents | Chapter 14: Applications → |