Code from: SECT: A spatiotemporal explicit causal transformer for path-faithful spatiotemporal attribution
Data files
May 04, 2026 version files 39.11 MB
-
__init__.py
4 B
-
baselines.py
21.16 KB
-
check_relevance.py
26.41 KB
-
config.py
2.32 KB
-
dataset.py
5.41 KB
-
environment.yml
7.84 KB
-
inference_model_0.7089.pt
32.83 MB
-
inspect_shapefile.py
2.32 KB
-
knockout.py
18.24 KB
-
model.py
16.48 KB
-
plot_relevance.py
24.36 KB
-
preprocess_sect.py
9.97 KB
-
README.md
46.04 KB
-
summarize_layerwise.py
6.71 KB
-
sweep.py
4.31 KB
-
train.py
31.72 KB
-
tx_county_3h_with_ssi_labels.csv
6.06 MB
Abstract
This package gives the implementation of the Spatiotemporal Explicit Causal Transformer (SECT), a dual-stream architecture that encodes self and neighbor sequences with causal convolutions and temporal transformers, integrates spatial self-information (SSI) as a localized anomaly metric, and fuses the streams with attention-based pooling. Interpretability is achieved through layer-wise relevance propagation with conservation checks, ensuring attribution is preserved and comparable at the neighbor–variable–lag (NVL) level. In a winter storm outage case study, SECT achieves competitive predictive performance relative to strong temporal and spatial baselines while preserving explicit neighbor identity and lag structure. Multi-seed ablation, knockout, and falsification experiments demonstrate that the model's performance degrades systematically when temporal order, neighbor alignment, or anomaly structure are perturbed, supporting the structural validity of its learned pathways. These results position SECT as a framework that integrates predictive modeling with experimentally testable, pathway-level spatiotemporal attribution.
Description of the data and file structure
Spatiotemporal prediction remains a central challenge in geographical information science, particularly when both accurate forecasts and causally grounded explanations are required. Existing sequence and graph models capture dependencies across time and space but typically aggregate spatial context into latent representations and provide coarse or non-conserved attributions, limiting traceability to specific neighbors, variables, and lags. This package gives the implementation of the Spatiotemporal Explicit Causal Transformer (SECT), a dual-stream architecture that encodes self and neighbor sequences with causal convolutions and temporal transformers, integrates spatial self-information (SSI) as a localized anomaly metric, and fuses the streams with attention-based pooling. Interpretability is achieved through layer-wise relevance propagation with conservation checks, ensuring attribution is preserved and comparable at the neighbor-variable-lag (NVL) level. In a winter storm outage case study, SECT achieves competitive predictive performance relative to strong temporal and spatial baselines while preserving explicit neighbor identity and lag structure. Multi-seed ablation, knockout, and falsification experiments demonstrate that the model's performance degrades systematically when temporal order, neighbor alignment, or anomaly structure are perturbed, supporting the structural validity of its learned pathways. These results position SECT as a framework that integrates predictive modeling with experimentally testable, pathway-level spatiotemporal attribution.
The repository contains Python source code, a pre-trained model checkpoint, and a Conda environment specification. The input dataset (meteorological and outage records for 254 Texas counties at 3-hour intervals) is not included in this repository; it must be obtained from the original data sources listed in the Sharing/Access Information section below. All scripts read from and write to paths relative to the repository root.
Sharing/Access Information
- Related publication: Liu, J., Zhang, Z., Hu, N., & Niu, Y. (2026). SECT: a spatiotemporal explicit causal transformer for path-faithful spatiotemporal attribution. International Journal of Geographical Information Science, 1-26. https://doi.org/10.1080/13658816.2026.2656266
- Meteorological data source: Weather API and Geolocation Developer API - WeatherAPI provided by https://www.weatherapi.com/ . If aiming to replicate the dataset used by the study, it would entail using the API to fetch the study scope's weather data, provided in JSON, and extract desired variables from the hourly meteorological metrics. There is no expected file name since metrics are collected from API-fetched JSON. Placing the aggregated file in the root directory would be sufficient for downstream processing using
preprocess_sect.py. - Outage data source: EAGLE-I outage dataset provided by US Department of Energy: https://doi.ccs.ornl.gov/dataset/ccec86f0-e144-5de8-aee0-fb26028b26e1. If aiming to replicate the dataset used by the study, it would entail downloading the dataset provided in the interface. The expected file name is
eaglei_outages.zip. The outage history is organized by the year, county state, FIPS code, outage case sum, start time. Placing the aggregated file in the root directory would be sufficient for downstream processing usingpreprocess_sect.py. - Texas county shapefile (required for
plot_relevance.py): Texas County Boundaries provided by Texas Department of Transportation: https://gis-txdot.opendata.arcgis.com/datasets/TXDOT::texas-county-boundaries-detailed. This will be a direct download in the format of Shapefile. The expected file name isTexas_County_Boundaries_Detailed_-3993510293615756158.zip. Placing the shapefile in the root directory would be sufficient for downstream processing usingpreprocess_sect.py. - County adjacency data source: Inferred from Texas county shapefile.
Variable and abbreviation definitions
The preprocessed input CSV (tx_county_3h_with_ssi_labels.csv) contains the following columns. All meteorological features are measured at 3-hour intervals per county.
| Column | Full Name | Units | Description |
|---|---|---|---|
time |
Timestamp | - | Observation datetime (3-hour cadence) |
county |
County name | - | Texas county identifier (254 counties total) |
temp |
Temperature | °F | Near-surface air temperature |
wind_kph |
Wind speed | km/h | Sustained wind speed |
humidity |
Relative humidity | % | Relative humidity |
dewpoint |
Dew point temperature | °F | Dew point temperature |
gust_kph |
Wind gust speed | km/h | Peak wind gust speed |
outage_count |
Power outage count | count | Number of reported power outage events |
ssi_spatial_temp |
SSI - temperature | dimensionless | Spatial Self-Information for temperature: (value − neighbor median) / neighbor σ |
ssi_spatial_wind_kph |
SSI - wind speed | dimensionless | Spatial Self-Information for wind speed |
ssi_spatial_humidity |
SSI - humidity | dimensionless | Spatial Self-Information for humidity |
ssi_spatial_dewpoint |
SSI - dew point | dimensionless | Spatial Self-Information for dew point |
ssi_spatial_gust_kph |
SSI - gust speed | dimensionless | Spatial Self-Information for gust speed |
ssi_spatial_outage_count |
SSI - outage count | dimensionless | Spatial Self-Information for outage count |
outage_class |
Outage severity class | - | Ordinal classification label for outage severity (see classification_meta.json for class definitions) |
SSI (Spatial Self-Information) is a localized anomaly metric computed per timestamp as (value − median of spatial neighbors) / σ, where σ is estimated using median absolute deviation (MAD) with an IQR fallback for low-variance cases. Neighbor sets are defined by county adjacency (all adjacent counties; median degree = 5, range 1-8 for the 254 Texas counties).
NVL - Neighbor-Variable-Lag: the three-dimensional attribution index used by SECT's LRP to identify which neighbor county, which input feature, and which time lag contributed most to a prediction.
LRP - Layer-wise Relevance Propagation: a backpropagation-based attribution method that decomposes a model's output score into contributions from each input, with a conservation constraint ensuring relevance sums are preserved across layers.
AMP - Automatic Mixed Precision: PyTorch training technique using 16-bit floating point where possible, to reduce memory usage and accelerate training.
macro-F1 - Macro-averaged F1 score across all outage severity classes; the primary evaluation metric reported in all tables.
Usage notes
The following software is required to run the code in this repository:
- Python 3.10 and Conda (to create the environment from
environment.yml) - PyTorch 2.8 with CUDA 12.6 (GPU recommended; CPU-only is possible with reduced speed)
- All other dependencies are specified in
environment.ymland installed automatically
To create the environment:
conda env create -f environment.yml
conda activate sect
All scripts are run from the repository root directory as Python modules (e.g., python -m sect.train). See the Reproduction Instructions section for the exact commands used to produce each figure and table in the manuscript.
The Texas county shapefile required by plot_relevance.py is not included in this repository and must be obtained separately (see Sharing/Access Information above).
File descriptions
The repository contains the following files, grouped by their role in the analysis pipeline.
Core pipeline
| File | Description |
|---|---|
preprocess_sect.py |
Data preprocessing |
train.py |
Model training |
check_relevance.py |
LRP attribution analysis |
plot_relevance.py |
Geospatial attribution visualization |
preprocess_sect.py
Reads the raw meteorological and outage CSV together with a county adjacency CSV. For each timestamp, computes Spatial Self-Information (SSI) for all six meteorological variables and outage count using a robust median/MAD estimator with IQR fallback. Builds four neighbor dictionary JSON files (indexed by county name and county code, covering all neighbors and the top-K neighbors by adjacency). The SSI-enriched CSV and neighbor JSONs are the primary inputs to all downstream scripts.
train.py
Main training script for the SECT model. Reads the preprocessed CSV, neighbor JSONs, and metadata JSONs. Splits data chronologically (default 80:20) or spatially (held-out counties). Fits a feature scaler on the training split only and writes it to scaler.json. Trains SECTDualPath using automatic mixed precision (AMP), a cosine learning rate schedule, and gradient clipping, with early stopping monitored on macro-F1. Saves the best checkpoint to checkpoints/best.pt and class metadata to classification_meta.json. Supports --ablate nei and --features flags for the ablation experiments reported in Table 2.
check_relevance.py
Attribution analysis script. Loads a trained checkpoint, detects model configuration from checkpoint metadata, and runs layer-wise relevance propagation (LRP) with full conservation checks (fusion, classifier, and projection ratios). For each sample, records the per-(neighbor, variable, lag) relevance tensor, neighbor attention weights, prediction, and county/time identifiers. Writes all outputs to a timestamped folder attributions/run_YYYYmmdd_HHMMSS/: always produces relevance_summary.json; with --layerwise, also produces layerwise.npz containing per-layer token and lag@feature relevance arrays. These outputs are the data source for Figure 3.
plot_relevance.py
Geospatial visualization script. Reads relevance_summary.json (from check_relevance.py) and a Texas county shapefile (obtained separately; not included due to licensing). For a given sample index, renders per-county attribution panels overlaid on a Texas county map, showing the top-3 contributing features, lags, and feature@lag pairs as either pie charts or horizontal bar charts (--bars flag). Outputs a static PNG file. Used to produce Figure 3 and Figure 4 of the manuscript.
Analysis & validation
| File | Description |
|---|---|
baselines.py |
GRU, GRU+GCN, and GCN baseline models (Table 1) |
knockout.py |
NVL knockout and falsification tests (Tables 3-4) |
summarize_layerwise.py |
Layer-wise attribution summary printer |
sweep.py |
Hyperparameter grid search |
baselines.py
Implements three reference models - GRU (temporal only), GCN (spatial graph convolution), and GRU+GCN (hybrid with neighbor attention) - using the same preprocessed data as SECT. Each model is trained independently and reports macro-F1 and per-class F1 scores on the validation split. The reported scores form Table 1 of the manuscript.
knockout.py
Causal validation script. Loads the trained checkpoint and runs inference under a range of structured perturbations without retraining: (i) top-K NVL knockout (zeroing the highest-attribution neighbor-variable-lag paths), (ii) lag falsification (shuffling or replacing with future lags), (iii) neighbor permutation, (iv) SSI corruption (additive noise or zeroing), and (v) spatial neighbor masking. Prints macro-F1, per-class F1, and Δ values for each condition, which map directly to Tables 3 and 4 of the manuscript.
summarize_layerwise.py
Attribution summarization utility. Reads layerwise.npz and relevance_summary.json produced by check_relevance.py --layerwise. Prints concise per-layer summaries: which lags carry the most relevance, the top contributing features, and the dominant lag@feature pairs. Supports filtering by sample index, county name, or timestamp. Useful for interpreting the layer-by-layer attribution patterns underlying Figure 3.
sweep.py
Hyperparameter grid search script. Trains SECT across combinations of D_MODEL, N_HEADS, and ENCODER_LAYERS, logging macro-F1 for each configuration to checkpoints/sweep_results.json. The best-performing configuration (D=128, heads=2, layers=4) was used for all reported results.
Supporting modules
| File | Description |
|---|---|
model.py |
SECTDualPath model architecture and LRP methods |
dataset.py |
SECTWindowDataset - windowed spatiotemporal data loader |
inspect_shapefile.py |
Utility to inspect shapefile field names |
tx_county_3h_with_ssi_labels.csv |
Sample Model-ready input format |
model.py
Defines the SECTDualPath neural network. The architecture consists of: (1) depthwise causal convolutions that encode per-feature temporal lags independently for the self-path and each neighbor path; (2) masked Transformer encoders (causal upper-triangular attention mask) for temporal sequence modeling; (3) attention-based neighbor pooling using the self-path last token as the query; and (4) a late-fusion classifier head. Also implements attribute() (gradient × input), attribute_lag_feature() (epsilon-LRP to convolution outputs), and attribute_layerwise() (per-layer token-to-lag@feature relevance) for explainability. Imported by all training and inference scripts.
dataset.py
Defines SECTWindowDataset, a PyTorch Dataset that reads the preprocessed CSV and precomputes per-county time/feature arrays for fast window sampling. Returns (X_self [L, V], X_nei [K, L, V], mask_nei [K], y) tuples. Supports configurable feature selection, mean/std normalization, and disallowed-neighbor masking. Used internally by train.py, check_relevance.py, knockout.py, and the baselines.
inspect_shapefile.py
Standalone utility that prints all field names and sample values from a given shapefile. Used once before running plot_relevance.py to identify the correct county name column (e.g., CNTY_NM) for the --county_name_col argument.
tx_county_3h_with_ssi_labels.csv
Sample processed file that organizes the raw inputs in the model-ready format. This file, on exporting to other dataset, could be generated by using preprocess_sect.py.
Configuration & environment
| File | Description |
|---|---|
config.py |
Centralized hyperparameter store |
environment.yml |
Conda environment specification |
__init__.py |
Python package initializer |
config.py
Centralized configuration module imported by all scripts. Defines all key hyperparameters: LOOKBACK_L=8 (24-hour lookback at 3-hour cadence), HORIZON_H=1, TOP_K_NEIGHBORS=3, D_MODEL=128, N_HEADS=2, ENCODER_LAYERS=4, FF_DIM=1024, BATCH_SIZE=4096, LR=1e-3, EPOCHS=200, PATIENCE=20, and SPLIT_STRATEGY (temporal or spatial). Modify this file to change any global setting.
environment.yml
Conda environment specification file. Pins Python 3.10, PyTorch 2.8 with CUDA 12.6, and all required packages: geopandas, shapely, pyproj, folium, matplotlib, pandas, numpy, scipy, scikit-learn, and tqdm. Create the environment with conda env create -f environment.yml to ensure full reproducibility of all results.
__init__.py
Empty file that marks the directory as a Python package, enabling module-style imports (e.g., python -m sect.train).
Included artifacts
| File | Description |
|---|---|
inference_model_0.7089.pt |
Pre-trained SECT checkpoint (macro-F1 = 0.7089, ~32 MB) |
inference_model_0.7089.pt
A pre-trained SECT model checkpoint saved at the epoch achieving macro-F1 = 0.7089 on the held-out validation set. Stored in PyTorch binary format (~32 MB). This file is provided so that attribution, knockout, and falsification experiments (Figures 3-4, Tables 3-4) can be reproduced exactly without retraining from scratch. It is loaded by check_relevance.py and knockout.py via the --ckpt argument.
Reproduction instructions for figures and tables:
This section covers how to reproduce Figure 3 and Tables 1–4 from the manuscript. Figure 3 visualizes Neighbor–Variable–Lag attributions; Table 1 compares baselines; Table 2 reports ablation results; Tables 3 and 4 report knockout and falsification results.
Excluded figures: Figure 1, 2, 4
These figures are not included because they are manually generated. Figure 1 and 2 provide conceptualization, and figure 4 is a re-arranged version of Figure 3 to demonstrate path attribution.
Step 0: Setup and inference model
To begin with, set up the environment based on the specification given by environment.yml. To set up a dedicated conda environment:
conda env create -f environment.yml
To train the SECT model and generate the inference checkpoint used in the paper:
python -c "from sect.train import main; main()"
This will provide a checkpoint file named best.pt. The inference checkpoint included in this repository is named as inference_model_0.7089.pt for stable reproduction.
Figure 3 generation
Use the trained checkpoint (inference_model_0.7089.pt) to compute layer-wise relevance scores:
python -m sect.check_relevance \
--ckpt checkpoints/inference_model_0.7089.pt \
--county_name "Anderson" \
--batch_size 4 \
--layerwise
And it would save both relevance_summary.json and layerwise.npz (if --layerwise flag is enabled) under the folder attributions/run_YYYYmmdd_HHMMSS/.
Then, the bar-style map could be rendered using:
python -m sect.plot_relevance \
--shapefile path/to/texas_counties.shp \
--county_name_col CNTY_NM \
--sample_index 0 \
--output outputs/figures/figure3.png \
--bars
where sample_index could be further modified to render results from different temporal period. The shapefile (e.g. Texas counties) must be obtained separately and specified via --shapefile; this repository does not include shapefile data due to licensing.
Table 1 - Baselines
Table 1 results can be generated by using the following commands:
GRU Baseline:
python -m sect.baselines --model gru --preset medium
GRU+GCN Baseline:
python -m sect.baselines --model gru_gcn --preset medium
GCN Baseline:
python -m sect.baselines --model gcn --preset medium
And SECT performances are recorded in step 0. The commands would report the macro-F1 and per-class F1.
Table 2 - Ablation study replication
Run these exactly to reproduce the three ablation rows reported in Table 2. Each command trains SECT under the stated input restriction and prints macro-F1 and per-class F1 at the end.
No neighbors
python -m sect.train --ablate nei --per_epoch_logs
(Zeroes neighbor inputs during train/val; matches “No neighbors” row.)
Outage + SSI only
python -m sect.train --features outage_count,ssi_spatial_outage_count --per_epoch_logs
No outage
python -m sect.train --features temp,wind_kph,humidity,dewpoint,gust_kph,ssi_spatial_temp,ssi_spatial_wind_kph,ssi_spatial_humidity,ssi_spatial_dewpoint,ssi_spatial_gust_kph --per_epoch_logs
These settings correspond to the configurations and scores summarized in Table 2 of the manuscript.
Table 3 - NVL Knockout Sweep
Table 3 reports macro-F1, per-class F1, and Δ values under different knockout scopes and K values (top-K relevance-ranked paths).
The results are produced by sect.knockout with the trained checkpoint.
Commands:
# Both self and neighbor paths
python -m sect.knockout --mode conv --top_m 5 --limit 4096 --batch_size 256
python -m sect.knockout --mode conv --top_m 10 --limit 4096 --batch_size 256
python -m sect.knockout --mode conv --top_m 15 --limit 4096 --batch_size 256
# Self-only paths
python -m sect.knockout --mode conv --scope self --top_m 10 --limit 4096 --batch_size 256
# Neighbor-only paths
python -m sect.knockout --mode conv --scope nei --top_m 10 --limit 4096 --batch_size 256
Each run evaluates a subset of 4096 samples (matching the K-path knockout protocol in the manuscript) and prints macro-F1, per-class F1, and Δ values in the console output, which directly correspond to Table 3 entries.
Table 4 - Falsification tests
Each run evaluates with no knockout (--top_m 0) and applies one falsification at a time (or a combined one). Metrics printed to stdout map directly to Table 4.
# Clean baseline (evaluation only)
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256
Lag falsification:
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256 --lag_falsify shuffle
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256 --lag_falsify future
Neighbor falsification:
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256 --neighbor_falsify permute
SSI falsification:
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256 --ssi_falsify noise
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256 --ssi_falsify zero
Spatial mask falsification:
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256 --mask_neighbors first_k:2
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256 --mask_neighbors random_p:0.5
Combined falsification (lag shuffle + neighbor permute):
python -m sect.knockout --mode conv --top_m 0 --limit 10000000 --batch_size 256 --lag_falsify shuffle --neighbor_falsify permute
These flags and settings correspond to the manuscript’s Table 4 configurations and reported metrics.
Key design choices
- Temporal window L=8 (3h cadence → 24h lookback). Horizon h=1.
- Spatial SSI is per-timestamp using adjacency neighbors (all neighbors), robust median/MAD with IQR fallback; also computed for
outage_count. - No temporal SSI in inputs; temporal SSI implementation has been removed; causal encoders learn temporal dynamics.
- Neighbor aggregation with attention (K=3), stored weights for attribution.
- Neighbor degree distribution (adjacency; 254 counties): min=1, q1=4, median=5, q3=6, p90=7, max=8; counts={1:2, 2:10, 3:28, 4:51, 5:65, 6:61, 7:35, 8:2}.
- Strict causality: inputs strictly before the target timestamp; features at ≤ t-1 predict class at t.
LISA vs SSI (row-wise standardized comparison)
- LISA uses Local Moran's I with binary adjacency weights (row-normalized) and outputs
lisa_<var>columns for all SSI variables, includinglisa_outage_count. - Row-wise comparison is done by z-scoring each SSI/LISA column across the full dataset, then comparing per-row standardized values (saved as
ssi_vs_lisa_zscore_rowwise.csv). - Correlations between standardized SSI and LISA are low and mostly negative, indicating they capture different spatial signals:
- temp: corr=−0.1878, diff_std=1.5413
- wind_kph: corr=−0.2271, diff_std=1.5666
- humidity: corr=−0.2568, diff_std=1.5855
- dewpoint: corr=−0.1880, diff_std=1.5414
- gust_kph: corr=−0.2387, diff_std=1.5740
- outage_count: corr=0.1768, diff_std=1.2831
- diff_mean is ~0 by construction (z-scored columns), so diff_std is the key separation metric.
LISA multi-seed K sweep (spatial split; 10 seeds)
Across matched K=3..8, SSI remains higher macro-F1 than LISA for this model setup.
| K | mean | std | min | max |
|---|---|---|---|---|
| 3 | 0.6603 | 0.0197 | 0.6352 | 0.6978 |
| 4 | 0.6643 | 0.0204 | 0.6407 | 0.7012 |
| 5 | 0.6671 | 0.0172 | 0.6449 | 0.7018 |
| 6 | 0.6713 | 0.0191 | 0.6450 | 0.7107 |
| 7 | 0.6706 | 0.0162 | 0.6485 | 0.6993 |
| 8 | 0.6698 | 0.0192 | 0.6467 | 0.7030 |
Splits and evaluation
- Default split: contiguous chronological 80:20 (train:val). Scaler is fit on train-only, then applied to val.
- Extremes concentrate mid-period (Feb 15-18, 2021). For robustness, consider an additional “stress” split where training covers part of the peak and validation covers a later part of the peak (disjoint blocks).
- Spatial holdout option: set
SPLIT_STRATEGY="spatial"to validate on held-out counties; scaler fit on train counties only; optional neighbor masking.
How to run
- Quick training (uses config in
sect/config.py):
python -c "from sect.train import main; main()"
- Relevance sanity checks (conservation & attention):
python -c "from sect.check_relevance import main; main()"
- Relevance with layer-wise outputs (adds layerwise.npz):
python -m sect.check_relevance --batch_size 4 --layerwise
- Use meta-aware inference checkpoint directly (aligns shapes automatically):
ln -sf checkpoints/inference_model_0.7089.pt checkpoints/best.pt && python -m sect.check_relevance --batch_size 4 --layerwise
- Relevance for a specific county:
python -m sect.check_relevance --county_name "Example County" --batch_size 16
- Plot a detailed per-sample map (requires shapefile):
python -m sect.plot_relevance \
--shapefile path/to/Your_County_Boundaries.shp \
--county_name_col CNTY_NM \
--sample_index 0 \
--output sample_map.png
Bar-based visualization (alternative to pies)
- Use
--barsto draw three stacked horizontal bars in each on-map panel instead of pies:- Each bar has a fixed total length representing total attribution.
- The top-3 contributors are colored as contiguous segments; the remainder is a single “other” segment.
- Only the top-3 pairs are listed as text under the bars; features and lags get inline labels on their bar segments.
- Label normalization (applied to on-bar tags and pairs text):
ssi_spatial_xxx→ssi_xxxtemperature→t(andssi_temp→ssi_t)dewpoints/dewpoint→dewhumidity→humgust_kph→gustwind_kph→wind
- Optional layout knobs:
--bar_label_offset(default 0.68): vertical position of on-bar labels as a fraction of bar height (0..1)--pairs_text_offset(default 0.40): downward offset for the pairs text in units of panel scaler
- Example (full command used in this project):
python sect/plot_relevance.py \
--shapefile path/to/Your_County_Boundaries.shp \
--county_name_col CNTY_NM \
--sample_index 0 \
--output attributions/sample_map_bars.png \
--bars --bar_label_offset 0.5 --pairs_text_offset 0.7
Defaults: --bar_label_offset 0.5, --pairs_text_offset 0.7.
- Summarize layer-wise attribution arrays:
python -m sect.summarize_layerwise \
--npz attributions/run_YYYYMMDD_HHMMSS/layerwise.npz \
--summary attributions/run_YYYYMMDD_HHMMSS/relevance_summary.json
- Baselines (examples):
python -m sect.baselines --model gru --preset medium
python -m sect.baselines --model gru_gcn --preset medium
python -m sect.baselines --model gcn --preset medium
- Hyperparameter sweep:
python -m sect.sweep
Where to change settings
sect/config.py:LOOKBACK_L = 8,HORIZON_H = 1TOP_K_NEIGHBORS = 3EPOCHS,LR,PATIENCE,TRAIN_VAL_SPLIT = 0.8USE_FOCAL,FOCAL_GAMMA
- Preprocessing parameters (
preprocess_sect.py): spatial SSI parameters (min_neighbors, eps).
Files & modules
See the File Descriptions section at the top of this README for full descriptions of every file, including their inputs, outputs, and correspondence to manuscript figures and tables.
Generated artifacts (produced at runtime, not included in the repository except where noted):
checkpoints/best.pt- best checkpoint saved during trainingcheckpoints/inference_model_0.7089.pt- included pre-trained checkpoint (macro-F1 = 0.7089)scaler.json- feature mean/std saved after training, used for inference normalizationclassification_meta.json- class label mapping and class weightsneighbors_top3_by_name.json,neighbors_all_by_name.json, etc. - neighbor dictionaries built bypreprocess_sect.pycheckpoints/sweep_results.json- hyperparameter sweep logattributions/run_*/relevance_summary.json- per-sample NVL attribution outputattributions/run_*/layerwise.npz- per-layer token and lag@feature relevance arrays
Typical workflow
- Generate SSI and labels:
python preprocess_sect.py(temporal SSI has been removed; only spatial SSI is computed) - Train:
python -c "from sect.train import main; main()" - Check relevance (optionally add
--layerwise):python -c "from sect.check_relevance import main; main()" - Visualize per-sample maps:
python -m sect.plot_relevance --shapefile path/to/Your_County_Boundaries.shp --county_name_col <NAME_FIELD> --sample_index 0 --output sample_map.png - (Optional) Summarize layer-wise attribution:
python -m sect.summarize_layerwise --npz attributions/run_*/layerwise.npz --summary attributions/run_*/relevance_summary.json - (Optional) Run sensitivity sweeps:
python -m sect.run_multi_seedwith--kor--Lflags to reproduce K/L sweep results.
LRP sanity check (example)
- Checkpoint:
checkpoints/inference_model_0.7089.pt(symlinked tobest.pt) - Meta (from ckpt):
ENCODER_LAYERS=4,D_MODEL=128,FF_DIM=1024,N_HEADS=2 - Command:
python -m sect.check_relevance --batch_size 4 - Results (batch_size=4):
- Fusion conservation ratio (mean): 1.0008
- Classifier LRP conservation ratio (mean): 1.0000
- Projection conservation ratios: self=0.9993, neighbor=0.9989
- Neighbor attention sums (per sample): ~[1.0000, 1.0000, 1.0000, 1.0000]
- Attention split check (sum neighbors vs nei_emb): 1.0000
- Total input relevance: 65.0760; target score mean: 2.6221
- corr(R_in_per_sample, target): 0.6806
- Non-negativity: self=True, neighbor=True
Sensitivity tests
Multi-seed K sweep results (spatial split)
Best macro-F1 per seed across K values (spatial split, 10 seeds):
| seed | K=1 | K=2 | K=3 | K=4 | K=5 | K=6 | K=7 | K=8 |
|---|---|---|---|---|---|---|---|---|
| 0 | 0.6577 | 0.6684 | 0.6871 | 0.7101 | 0.7137 | 0.7093 | 0.7102 | 0.7097 |
| 1 | 0.6879 | 0.6908 | 0.7012 | 0.7126 | 0.7082 | 0.7179 | 0.7197 | 0.7174 |
| 2 | 0.6851 | 0.6988 | 0.7052 | 0.6958 | 0.6960 | 0.7019 | 0.7019 | 0.7009 |
| 3 | 0.6777 | 0.6856 | 0.6953 | 0.7044 | 0.7050 | 0.7090 | 0.7057 | 0.7084 |
| 4 | 0.6831 | 0.6846 | 0.6995 | 0.7060 | 0.7136 | 0.7162 | 0.7243 | 0.7269 |
| 5 | 0.7101 | 0.7171 | 0.7218 | 0.7204 | 0.7316 | 0.7246 | 0.7253 | 0.7281 |
| 6 | 0.6888 | 0.6930 | 0.7260 | 0.7182 | 0.7238 | 0.7213 | 0.7187 | 0.7198 |
| 7 | 0.6657 | 0.6739 | 0.6781 | 0.6883 | 0.6868 | 0.6862 | 0.6883 | 0.6873 |
| 8 | 0.6862 | 0.6974 | 0.7051 | 0.7050 | 0.7158 | 0.7238 | 0.7157 | 0.7226 |
| 9 | 0.6447 | 0.6572 | 0.6824 | 0.6789 | 0.6985 | 0.6951 | 0.6999 | 0.6931 |
| mean | 0.6787 | 0.6867 | 0.7002 | 0.7040 | 0.7093 | 0.7105 | 0.7110 | 0.7114 |
| std | 0.0175 | 0.0162 | 0.0148 | 0.0124 | 0.0127 | 0.0122 | 0.0114 | 0.0134 |
Performance rises meaningfully from K=3 to K=5 (Wilcoxon signed-rank, p=0.019). Past K=5, improvements are modest and not statistically reliable (K=5 vs K=8, p=0.28). K=5 is adopted as the default.
Lookback L multi-seed (fixed K=5)
Avg epoch time (first 10 epochs):
| L | Time (s) |
|---|---|
| 4 | 1.086 |
| 6 | 1.134 |
| 8 | 1.178 |
| 10 | 1.190 |
| 12 | 1.198 |
Best macro-F1 per seed across L values (K=5, spatial split, 10 seeds):
| seed | L=4 | L=6 | L=8 | L=10 | L=12 |
|---|---|---|---|---|---|
| 0 | 0.7173 | 0.7242 | 0.7137 | 0.7228 | 0.6882 |
| 1 | 0.7259 | 0.7262 | 0.7082 | 0.7040 | 0.6947 |
| 2 | 0.7041 | 0.6960 | 0.6960 | 0.7064 | 0.6838 |
| 3 | 0.7109 | 0.6945 | 0.7050 | 0.6877 | 0.6975 |
| 4 | 0.7226 | 0.7298 | 0.7136 | 0.7401 | 0.7329 |
| 5 | 0.7008 | 0.7178 | 0.7316 | 0.7101 | 0.6923 |
| 6 | 0.7306 | 0.7182 | 0.7238 | 0.7177 | 0.7149 |
| 7 | 0.7002 | 0.7169 | 0.6868 | 0.6949 | 0.6908 |
| 8 | 0.7020 | 0.6901 | 0.7158 | 0.7200 | 0.6673 |
| 9 | 0.6951 | 0.6804 | 0.6985 | 0.6887 | 0.6807 |
| mean | 0.7109 | 0.7094 | 0.7093 | 0.7092 | 0.6943 |
Performance saturates for L in [4, 10] with no statistically significant differences (Wilcoxon p > 0.7). At L=12 it drops significantly (p ≈ 0.01). L=8 is adopted as the default to capture diurnal structure while avoiding longer-range noise.
Commands used
- Note: Code defaults are now
NEIGHBORS_JSON=neighbors_all_by_name.jsonandTOP_K_NEIGHBORS=5(seesect/config.py). You can omit--neighbors_jsonand--top_k_neighborsto use these defaults; explicit flags here override them. - Neighbor K sweep (multi-seed; K=1..8; spatial split):
python -m sect.run_multi_seed \
--k 1 2 3 4 5 6 7 8 \
--num_seeds 10 \
--split_strategy spatial \
--save_csv runs/k_sweep_seeds.csv \
--save_summary runs/k_sweep_summary.csv
- Lookback L sweep (multi-seed; K=5; spatial split):
python -m sect.run_multi_seed \
--k 5 \
--L 4 6 8 10 12 \
--num_seeds 10 \
--split_strategy spatial \
--save_csv runs/lsweep_k5_seeds.csv \
--save_summary runs/lsweep_k5_summary.csv
- LISA K sweep (multi-seed; K=3..8; spatial split):
python -m sect.run_multi_seed \
--k 3 4 5 6 7 8 \
--num_seeds 10 \
--split_strategy spatial \
--neighbors_json neighbors_all_by_name.json \
--data_csv tx_county_3h_with_lisa_labels.csv \
--meta_json classification_meta_lisa.json \
--features temp,wind_kph,humidity,dewpoint,gust_kph,outage_count,lisa_temp,lisa_wind_kph,lisa_humidity,lisa_dewpoint,lisa_gust_kph,lisa_outage_count \
--save_csv runs/lisa_k_sweep_seeds.csv \
--save_summary runs/lisa_k_sweep_summary.csv
- ShapTime baseline (Bi-RNN; early stopping):
python -m sect.shaptime_baseline --epochs 100 --patience 10 --interp_first_blocks 8 --interp_blocks 8
Ablation results
Best macro-F1 per seed across ablation conditions (K=5, L=8, spatial split, 10 seeds):
| seed | No neighbors | Outage+SSI only | No outage |
|---|---|---|---|
| 0 | 0.6447 | 0.7651 | 0.4268 |
| 1 | 0.6699 | 0.7517 | 0.4561 |
| 2 | 0.6909 | 0.7497 | 0.4226 |
| 3 | 0.6537 | 0.7031 | 0.4268 |
| 4 | 0.6705 | 0.7320 | 0.4476 |
| 5 | 0.6891 | 0.7650 | 0.4561 |
| 6 | 0.6837 | 0.7395 | 0.4263 |
| 7 | 0.6421 | 0.7317 | 0.4395 |
| 8 | 0.6899 | 0.6951 | 0.4979 |
| 9 | 0.6599 | 0.7127 | 0.4282 |
| mean | 0.6694 | 0.7346 | 0.4428 |
| std | 0.0178 | 0.0233 | 0.0220 |
Per-class means: No neighbors=[0.6699, 0.5193, 0.6588, 0.8298]; Outage+SSI only=[0.7282, 0.5838, 0.7610, 0.8651]; No outage=[0.5685, 0.4193, 0.4331, 0.3502].
- Climate ablation command:
python -m sect.run_multi_seed \
--k 5 --lookback_l 8 \
--num_seeds 10 \
--split_strategy spatial \
--features outage_count,ssi_spatial_outage_count \
--save_csv runs/ablate_climate_seeds.csv \
--save_summary runs/ablate_climate_summary.csv
- Outage ablation command:
python -m sect.run_multi_seed \
--k 5 --lookback_l 8 \
--num_seeds 10 \
--split_strategy spatial \
--features temp,wind_kph,humidity,dewpoint,gust_kph,ssi_spatial_temp,ssi_spatial_wind_kph,ssi_spatial_humidity,ssi_spatial_dewpoint,ssi_spatial_gust_kph \
--save_csv runs/ablate_outage_seeds.csv \
--save_summary runs/ablate_outage_summary.csv
Knockout results
- Modes (knockout.py)
--mode raw: zero the selected (neighbor, lag, feature) entries at the standardized raw input tensors (Xs,Xn).--mode conv: zero the selected entries in the depthwise causal convolution outputs (conv features) before the Linear projection/Transformer.- Falsification (evaluation-only perturbations):
--lag_falsify {none,shuffle,future},--neighbor_falsify {none,permute},--ssi_falsify {none,noise,zero},--mask_neighbors {none,first_k:N,random_p:P}
- Conv-level knockout (zero conv outputs at selected (neighbor, feature, lag); subset evaluation
--limit 4096):- command:
python -m sect.knockout --mode conv --top_m 3 --limit 4096 --batch_size 256
- Conv-level knockout sweep (baseline macro-F1=0.7317):
- Scope=both
- m=5: macro-F1=0.6770; Δ=−0.0548; per-class=[0.5954, 0.5366, 0.6839, 0.8920]
- m=10: macro-F1=0.6586; Δ=−0.0731; per-class=[0.5209, 0.5426, 0.6764, 0.8947]
- m=15: macro-F1=0.6576; Δ=−0.0742; per-class=[0.4906, 0.5486, 0.6939, 0.8971]
- Scope=self
- m=5: macro-F1=0.6770; Δ=−0.0548; per-class=[0.5954, 0.5366, 0.6839, 0.8920]
- m=10: macro-F1=0.6586; Δ=−0.0731; per-class=[0.5209, 0.5426, 0.6764, 0.8947]
- m=15: macro-F1=0.6576; Δ=−0.0742; per-class=[0.4906, 0.5486, 0.6939, 0.8971]
- Scope=nei
- m=5: macro-F1=0.7292; Δ=−0.0026; per-class=[0.7598, 0.5444, 0.7228, 0.8895]
- m=10: macro-F1=0.7294; Δ=−0.0023; per-class=[0.7581, 0.5406, 0.7269, 0.8920]
- m=15: macro-F1=0.7295; Δ=−0.0022; per-class=[0.7588, 0.5424, 0.7260, 0.8908]
- example commands:
- Scope=both
python -m sect.knockout --mode conv --scope both --top_m 5 --batch_size 256 --val_only
python -m sect.knockout --mode conv --scope both --top_m 10 --batch_size 256 --val_only
python -m sect.knockout --mode conv --scope both --top_m 15 --batch_size 256 --val_only
python -m sect.knockout --mode conv --scope self --top_m 10 --batch_size 256 --val_only
python -m sect.knockout --mode conv --scope nei --top_m 10 --batch_size 256 --val_only
- Conv-level knockout sweep (new model; seed=42; checkpoint
checkpoints/knockout_model.pt; baseline macro-F1=0.7048):- Scope=both
- m=5: macro-F1=0.6878; Δ=−0.0170; per-class=[0.6547, 0.5411, 0.7154, 0.8400]
- m=10: macro-F1=0.6812; Δ=−0.0236; per-class=[0.6465, 0.5482, 0.7161, 0.8140]
- m=15: macro-F1=0.6682; Δ=−0.0366; per-class=[0.5862, 0.5716, 0.7134, 0.8017]
- Scope=self
- m=5: macro-F1=0.6878; Δ=−0.0170; per-class=[0.6547, 0.5411, 0.7154, 0.8400]
- m=10: macro-F1=0.6812; Δ=−0.0236; per-class=[0.6465, 0.5482, 0.7161, 0.8140]
- m=15: macro-F1=0.6682; Δ=−0.0366; per-class=[0.5862, 0.5716, 0.7134, 0.8017]
- Scope=nei
- m=5: macro-F1=0.7025; Δ=−0.0023; per-class=[0.6832, 0.5405, 0.7364, 0.8501]
- m=10: macro-F1=0.6921; Δ=−0.0127; per-class=[0.6736, 0.5325, 0.7294, 0.8330]
- m=15: macro-F1=0.6928; Δ=−0.0120; per-class=[0.6770, 0.5348, 0.7249, 0.8344]
- Scope=both
Falsification results (evaluation-only; use --top_m 0)
- Clean baseline (conv;
--top_m 0; checkpoint=checkpoints/knockout_model.pt;--val_only): macro-F1=0.7048; per-class=[0.6865, 0.5451, 0.7406, 0.8471] - Lag falsification
- shuffle: macro-F1=0.6488; Δ=−0.0560; per-class=[0.6351, 0.5129, 0.6848, 0.7623]
- future (+1 roll): macro-F1=0.6680; Δ=−0.0368; per-class=[0.6515, 0.5096, 0.7018, 0.8091]
- Neighbor falsification
- permute: macro-F1=0.5387; Δ=−0.1661; per-class=[0.4964, 0.4114, 0.5089, 0.7383]
- SSI falsification
- noise: macro-F1=0.5932; Δ=−0.1116; per-class=[0.4736, 0.4980, 0.6070, 0.7942]
- zero: macro-F1=0.5354; Δ=−0.1694; per-class=[0.0749, 0.5467, 0.6691, 0.8509]
- Spatial mask falsification
- first_k:2: macro-F1=0.6563; Δ=−0.0485; per-class=[0.6397, 0.4880, 0.6852, 0.8125]
- random_p:0.5: macro-F1=0.6750; Δ=−0.0298; per-class=[0.6597, 0.5028, 0.6923, 0.8451]
- Combined falsification
- lag_falsify=shuffle + neighbor_falsify=permute: macro-F1=0.5275; Δ=−0.1773; per-class=[0.5022, 0.4183, 0.5080, 0.6815]
Example commands (isolated falsification):
python -m sect.knockout --checkpoint checkpoints/knockout_model.pt --mode conv --scope both --top_m 0 --batch_size 256 --val_only --lag_falsify shuffle
python -m sect.knockout --checkpoint checkpoints/knockout_model.pt --mode conv --scope both --top_m 0 --batch_size 256 --val_only --lag_falsify future
python -m sect.knockout --checkpoint checkpoints/knockout_model.pt --mode conv --scope both --top_m 0 --batch_size 256 --val_only --neighbor_falsify permute
python -m sect.knockout --checkpoint checkpoints/knockout_model.pt --mode conv --scope both --top_m 0 --batch_size 256 --val_only --ssi_falsify noise
python -m sect.knockout --checkpoint checkpoints/knockout_model.pt --mode conv --scope both --top_m 0 --batch_size 256 --val_only --ssi_falsify zero
python -m sect.knockout --checkpoint checkpoints/knockout_model.pt --mode conv --scope both --top_m 0 --batch_size 256 --val_only --mask_neighbors first_k:2
python -m sect.knockout --checkpoint checkpoints/knockout_model.pt --mode conv --scope both --top_m 0 --batch_size 256 --val_only --mask_neighbors random_p:0.5
Baselines
- Settings (presets used by
python -m sect.baselines --model <gru|gru_gcn> --preset <small|medium|large>):- small: hidden_dim=128, layers=1, lr=3e-4, batch_size=2048, epochs=60, patience=12, weight_decay=0.01
- medium: hidden_dim=256, layers=2, lr=3e-4, batch_size=4096, epochs=100, patience=16, weight_decay=0.05
- large: hidden_dim=512, layers=2, lr=2e-4, batch_size=8192, epochs=120, patience=20, weight_decay=0.05
- GRU (self-only GRU → last hidden → classifier)
- 10-seed (spatial): mean macro-F1=0.6870; per-class mean=[0.7068, 0.5050, 0.6953, 0.8408]
- per-seed macro-F1: [0.7004, 0.6770, 0.6889, 0.6719, 0.7141, 0.6893, 0.6955, 0.6704, 0.6877, 0.6744]
- p-value = 0.003906
- 10-seed (spatial): mean macro-F1=0.6870; per-class mean=[0.7068, 0.5050, 0.6953, 0.8408]
- GRU_GCN (GRU self; GRU per-neighbor → mean over active neighbors → classifier)
- 10-seed (spatial): mean macro-F1=0.7316; per-class mean=[0.7420, 0.5805, 0.7424, 0.8615]
- per-seed macro-F1: [0.7442, 0.7346, 0.7239, 0.7304, 0.7500, 0.7380, 0.7380, 0.7192, 0.7483, 0.6897]
- p-value = 0.005859
- 10-seed (spatial): mean macro-F1=0.7316; per-class mean=[0.7420, 0.5805, 0.7424, 0.8615]
- GCN (TemporalGCNClassifier: per-node GRUs → 2-layer GCN with \hat{A}=D{-1/2}(A+I)D{-1/2} → self-node readout)
- 10-seed (spatial): mean macro-F1=0.7331; per-class mean=[0.7402, 0.5892, 0.7460, 0.8572]
- per-seed macro-F1: [0.7401, 0.7359, 0.7249, 0.7221, 0.7624, 0.7415, 0.7372, 0.7232, 0.7421, 0.7019]
- p-value = 0.001953
- 10-seed (spatial): mean macro-F1=0.7331; per-class mean=[0.7402, 0.5892, 0.7460, 0.8572]
- ShapTime baseline (self-only Bi-RNN; ShapTime on 8 blocks; val split)
- val_acc=0.6581; val_macro-F1=0.7032
