Nonlinear Spatial Panel Models

This guide demonstrates SARPanelTobit and SEMPanelTobit on synthetic panel data.

Both models handle censored outcomes observed for \(N\) spatial units across \(T\) time periods.

  • SARPanelTobit — spatial lag in the latent outcome: \(y^*_t = \rho W y^*_t + X_t \beta + \varepsilon_t\)

  • SEMPanelTobit — spatial lag in the disturbance: \(y^*_t = X_t \beta + u_t\), \(u_t = \lambda W u_t + \varepsilon_t\)

Observed outcomes are censored at zero: \(y_t = \max(0, y^*_t)\).

import numpy as np

from bayespecon import SARPanelTobit, SEMPanelTobit, dgp

rng = np.random.default_rng(42)

# Panel dimensions: 10×10 rook grid, 5 time periods
N = 100
T = 5
side = 10

SAR Panel Tobit

Generate censored panel data from a spatial-lag process and fit SARPanelTobit.

rho_true = 0.35
beta_true = np.array([1.0, 1.4])
sigma_true = 0.8

sar_data = dgp.simulate_panel_sar_tobit_fe(
    N=N,
    T=T,
    n=side,
    contiguity="rook",
    rho=rho_true,
    beta=beta_true,
    sigma=sigma_true,
    censoring=0.0,
    rng=rng,
)
y_sar = sar_data["y"]
X_sar = sar_data["X"]
W_graph = sar_data["W_graph"]

print(f"Censored fraction: {(y_sar == 0).mean():.2f}")

sar_model = SARPanelTobit(y=y_sar, X=X_sar, W=W_graph, N=N, T=T, censoring=0.0)
sar_idata = sar_model.fit(
    tune=500, draws=500, chains=2, random_seed=42, progressbar=False
)

rho_hat = float(sar_idata.posterior["rho"].mean())
beta_hat = sar_idata.posterior["beta"].mean(("chain", "draw")).values
print(f"rho:   true={rho_true:.2f}  estimated={rho_hat:.3f}")
for k, (bt, bh) in enumerate(zip(beta_true, beta_hat)):
    print(f"beta[{k}]: true={bt:.2f}  estimated={bh:.3f}")
Censored fraction: 0.21
rho:   true=0.35  estimated=0.306
beta[0]: true=1.00  estimated=0.985
beta[1]: true=1.40  estimated=1.434
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [rho, beta, sigma, y_cens_gap]
Sampling 2 chains for 500 tune and 500 draw iterations (1_000 + 1_000 draws total) took 10 seconds.
We recommend running at least 4 chains for robust computation of convergence diagnostics
The rhat statistic is larger than 1.01 for some parameters. This indicates problems during sampling. See https://arxiv.org/abs/1903.08008 for details

SEM Panel Tobit

Generate censored panel data from a spatial-error process and fit SEMPanelTobit.

lam_true = 0.35

sem_data = dgp.simulate_panel_sem_tobit_fe(
    N=N,
    T=T,
    n=side,
    contiguity="rook",
    lam=lam_true,
    beta=beta_true,
    sigma=sigma_true,
    censoring=0.0,
    rng=rng,
)
y_sem = sem_data["y"]
X_sem = sem_data["X"]

print(f"Censored fraction: {(y_sem == 0).mean():.2f}")

sem_model = SEMPanelTobit(
    y=y_sem,
    X=X_sem,
    W=sem_data["W_graph"],
    N=N,
    T=T,
    censoring=0.0,
)
sem_idata = sem_model.fit(
    tune=500, draws=500, chains=2, random_seed=42, progressbar=False
)

lam_hat = float(sem_idata.posterior["lam"].mean())
beta_hat = sem_idata.posterior["beta"].mean(("chain", "draw")).values
print(f"lam:   true={lam_true:.2f}  estimated={lam_hat:.3f}")
for k, (bt, bh) in enumerate(zip(beta_true, beta_hat)):
    print(f"beta[{k}]: true={bt:.2f}  estimated={bh:.3f}")
Censored fraction: 0.32
lam:   true=0.35  estimated=0.324
beta[0]: true=1.00  estimated=0.850
beta[1]: true=1.40  estimated=1.353
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [lam, beta, sigma, y_cens_gap]
Sampling 2 chains for 500 tune and 500 draw iterations (1_000 + 1_000 draws total) took 10 seconds.
We recommend running at least 4 chains for robust computation of convergence diagnostics
The rhat statistic is larger than 1.01 for some parameters. This indicates problems during sampling. See https://arxiv.org/abs/1903.08008 for details