-
Notifications
You must be signed in to change notification settings - Fork 0
Pipeline Walkthrough
The pipeline transforms raw seismic waveforms into probabilistic eruption forecasts through six processing stages followed by a forecast stage. ForecastModel orchestrates all stages via method chaining — you invoke them in sequence on a single object. You can also instantiate and call each stage class independently when you need finer control over intermediate outputs or want to slot the package into a larger workflow.
Raw seismic waveforms are too high-dimensional and noisy to use directly as model input. CalculateTremor compresses each day of continuous waveform data into compact, interpretable metrics sampled at 10-minute intervals. Three metrics are calculated across multiple frequency bands in parallel:
- RSAM (Real Seismic Amplitude Measurement): The mean absolute amplitude within each frequency band. Rising RSAM in mid-to-high frequency bands is a classical precursor to volcanic unrest.
- DSAR (Displacement Seismic Amplitude Ratio): The ratio of RSAM between consecutive frequency bands. DSAR captures shifts in the spectral shape of tremor, which correlate with changes in the volcanic fluid system.
- Shannon Entropy: A single broadband measure of signal complexity. Low entropy indicates a repetitive, coherent signal; high entropy indicates random broadband noise.
The default frequency bands and their aliases are:
| Alias | Band (Hz) |
|---|---|
f0 |
0.01 – 0.1 |
f1 |
0.1 – 2 |
f2 |
2 – 5 |
f3 |
4.5 – 8 |
f4 |
8 – 16 |
RSAM columns are named rsam_f0 through rsam_f4. DSAR columns carry the names of both participating bands: dsar_f0-f1, dsar_f1-f2, dsar_f2-f3, dsar_f3-f4. Shannon Entropy produces a single column named entropy.
SDS (SeisComP Data Structure) is the primary on-disk waveform format. Pass the root of the archive to from_sds():
from eruption_forecast import CalculateTremor tremor = CalculateTremor( start_date="2025-01-01", end_date="2025-01-31", station="OJN", channel="EHZ", n_jobs=4, ).from_sds(sds_dir="/data/sds").run() # Access the resulting DataFrame print(tremor.df.head()) # Inspect where the merged CSV was saved print(f"Saved to: {tremor.csv}")
You can restrict which metrics are calculated:
tremor = CalculateTremor( start_date="2025-01-01", end_date="2025-01-31", station="OJN", channel="EHZ", methods=["rsam", "dsar", "entropy"], # default: all three ).from_sds(sds_dir="/data/sds").run()
When a local archive is unavailable, from_fdsn() downloads waveforms from any FDSN-compatible service and caches them locally as SDS miniSEED so subsequent runs skip the network:
tremor = CalculateTremor( start_date="2025-01-01", end_date="2025-01-31", station="OJN", channel="EHZ", ).from_fdsn(client_url="https://service.iris.edu").run()
Call change_freq_bands() before from_sds() or from_fdsn() to override the defaults:
tremor = CalculateTremor( start_date="2025-01-01", end_date="2025-01-31", station="OJN", channel="EHZ", ).change_freq_bands([ (0.1, 1.0), # Low frequency (1.0, 5.0), # Mid frequency (5.0, 10.0), # High frequency ]).from_sds(sds_dir="/data/sds").run()
- DateTime index at 10-minute intervals
- RSAM columns:
rsam_f0,rsam_f1,rsam_f2,rsam_f3,rsam_f4 - DSAR columns:
dsar_f0-f1,dsar_f1-f2,dsar_f2-f3,dsar_f3-f4 - Shannon Entropy column:
entropy
Before a model can learn, every time window of tremor data must be assigned a binary label: 1 if the window precedes an eruption, 0 otherwise. LabelBuilder generates this labeling by sliding a window across the full date range and assigning labels based on known eruption dates.
A window gets is_erupted = 1 when its end time falls within the interval [eruption_date − day_to_forecast, eruption_date]. All other windows get is_erupted = 0. This design captures the pre-eruptive signal window you want the model to recognise — for example, setting day_to_forecast=2 tells the labeler to mark the 48 hours leading into each eruption as positive.
from eruption_forecast import LabelBuilder labels = LabelBuilder( start_date="2020-01-01", end_date="2020-12-31", window_step=12, # Slide by 12 hours window_step_unit="hours", day_to_forecast=2, # Label 2 days before eruption as positive eruption_dates=[ "2020-03-15", "2020-06-20", "2020-09-10", ], volcano_id="VOLCANO_001", ).build() # Access the DataFrame print(labels.df.head()) # Columns: id, is_erupted # Check label distribution print(f"Positive labels: {(labels.df['is_erupted'] == 1).sum()}") print(f"Negative labels: {(labels.df['is_erupted'] == 0).sum()}")
Example with window_step=12h, day_to_forecast=2d, eruption on January 15.
Timeline (each tick = 12 hours):
── Jan10 ───┬─── Jan11 ───┬─── Jan12 ───┬─── Jan13 ───┬─── Jan14 ───┬─── Jan15 ───┐ ☄
00 │ 12 │ 00 │ 12 │ 00 │ 12 │ 00 │ 12 │ 00 │ 12 │ 00 │ 12 │
│ │ │ │ │ │ │ │ │ │ │ │
← window_step: 12h → │ │ │ │ │ │ │
│◄──────── day_to_forecast=2d ───────────►│
│ label = 1 zone │
ID Window data span End time (index) Label
── ──────────────────────────────────── ────────────────── ──────
1 Jan09·12:00 ══════════ Jan10·12:00 Jan10 12:00 0
2 Jan10·00:00 ══════════ Jan11·00:00 Jan11 00:00 0
3 Jan10·12:00 ══════════ Jan11·12:00 Jan11 12:00 0
4 Jan11·00:00 ══════════ Jan12·00:00 Jan12 00:00 0
5 Jan11·12:00 ══════════ Jan12·12:00 Jan12 12:00 0
6 Jan12·00:00 ══════════ Jan13·00:00 Jan13 00:00 1 ← label zone starts
7 Jan12·12:00 ══════════ Jan13·12:00 Jan13 12:00 1
8 Jan13·00:00 ══════════ Jan14·00:00 Jan14 00:00 1
9 Jan13·12:00 ══════════ Jan14·12:00 Jan14 12:00 1
10 Jan14·00:00 ══════════ Jan15·00:00 Jan15 00:00 1 ← eruption day
The window's datetime index is its end time. A window gets
label=1when its end time falls within[eruption_date − day_to_forecast, eruption_date].
| Parameter | Type | Description | Example |
|---|---|---|---|
window_step |
int |
How far to shift the window between consecutive labels | 12 |
window_step_unit |
"minutes" | "hours"
|
Unit for window_step
|
"hours" |
day_to_forecast |
int (days) |
How many days before the eruption to start labeling as positive (is_erupted=1) |
2 |
eruption_dates |
list[str] |
Known eruption dates in YYYY-MM-DD format |
["2025-03-20"] |
start_date / end_date
|
str |
Date range for generating all label windows | "2025-01-01" |
volcano_id |
str |
Identifier used in output filenames | "LEWOTOBI" |
Labels define which time windows are positive and which are negative, but the model needs the actual tremor values inside each window. TremorMatrixBuilder slices the tremor time series into windows that align exactly with the label DataFrame, then concatenates them into a single unified matrix.
Every row in the output matrix corresponds to one 10-minute sample inside one window. The matrix carries an id column (window identifier matching the label), a datetime column, and the selected tremor columns. This format is what tsfresh expects: all windows stacked vertically, identified by id.
from eruption_forecast.features.tremor_matrix_builder import TremorMatrixBuilder builder = TremorMatrixBuilder( tremor_df=tremor.df, label_df=labels.df, output_dir="output/features", window_size=1, ).build( select_tremor_columns=["rsam_f0", "rsam_f1", "dsar_f0-f1"], save_tremor_matrix_per_method=True, ) print(builder.df.shape) # (n_windows ×ばつ n_samples_per_window, n_columns)
The shape is (n_windows ×ばつ n_samples_per_window, n_columns). For a one-day window at 10-minute sampling, each window contains 144 samples. With 500 windows and three tremor columns selected, builder.df will have shape (72 000, 5) — the five columns being id, datetime, and the three tremor columns.
Use select_tremor_columns to limit which tremor columns are carried into feature extraction. Including only the columns relevant to your classification task reduces computation time in Stage 4 significantly.
Raw tremor amplitude values at each time step are unlikely to be the most discriminating input for a classifier. FeaturesBuilder applies tsfresh to extract over 700 statistical and temporal features per tremor column per window. These features describe properties of the tremor signal that a classifier can act on directly.
Feature categories extracted per column per window:
- Statistical: mean, median, standard deviation, variance, min, max, quantiles, skewness, kurtosis
- Time-domain: autocorrelation, partial autocorrelation, number of peaks, peak positions, linear trend coefficients
- Frequency-domain: FFT coefficients, spectral entropy, spectral centroid
- Complexity: approximate entropy, sample entropy, Lempel-Ziv complexity
- Peaks: number of peaks above mean, number of peaks above a threshold
FeaturesBuilder operates in two modes depending on whether labels are provided:
- Training mode (labels provided): Filters windows to match the label DataFrame, saves an aligned label CSV alongside the features CSV, and optionally applies tsfresh's relevance filter to discard statistically insignificant features.
- Prediction mode (no labels): Extracts features for all windows without filtering. Used when running inference on future data that has no ground-truth labels.
from eruption_forecast import FeaturesBuilder features_builder = FeaturesBuilder( tremor_matrix_df=builder.df, label_df=labels.df, output_dir="output/features", n_jobs=4, ) # Extract features; optionally exclude specific feature calculators features = features_builder.extract_features( select_tremor_columns=["rsam_f0", "rsam_f1"], exclude_features=["length", "has_duplicate"], ) print(f"Features shape: {features.shape}") # Example: (5000 windows, 1500 features)
With 700+ features per tremor column, the raw feature matrix is wide. Many features will be noise or redundant. FeatureSelector narrows the matrix down to the most informative features before they are passed to the classifier.
Three selection methods are available, each with different trade-offs:
| Method | Reduces | Captures Interactions | Speed |
|---|---|---|---|
tsfresh |
1000s → 100s | No | Fast |
random_forest |
Direct → N | Yes | Slow |
combined |
1000s → 100s → N | Yes | Fast |
-
tsfresh: Applies a statistical hypothesis test (Benjamini-Hochberg FDR correction) to retain only features with a significant relationship to the label. Fast and principled, but treats each feature independently. -
random_forest: Trains a Random Forest and ranks features by permutation importance. Captures feature interactions but is slower at wide feature matrices. -
combined(recommended): Runstsfreshfirst to coarsely prune the feature set, then appliesrandom_foreston the reduced matrix. Achieves the interaction-capturing benefit of Random Forest at reasonable speed.
from eruption_forecast.features import FeatureSelector # Two-stage combined selection (recommended) selector = FeatureSelector(method="combined", n_jobs=4, verbose=True) X_selected = selector.fit_transform( X_train, y_train, fdr_level=0.05, # Stage 1: tsfresh FDR threshold top_n=30, # Stage 2: final feature count ) print(f"Reduced: {X_train.shape[1]} → {X_selected.shape[1]} features") # tsfresh statistical selection only selector = FeatureSelector(method="tsfresh", n_jobs=4) X_selected = selector.fit_transform(X_train, y_train, fdr_level=0.05) # RandomForest permutation importance only selector = FeatureSelector(method="random_forest", n_jobs=4) X_selected = selector.fit_transform(X_train, y_train, top_n=30) # Retrieve feature scores after fitting scores = selector.get_feature_scores() print(scores.head(10))
Feature selection runs inside ModelTrainer automatically — you do not need to call FeatureSelector directly unless you are working outside the standard training pipeline.
ModelTrainer trains a classifier across multiple random seeds to produce a robust ensemble of models. Because volcanic datasets are heavily imbalanced (eruption windows are rare), each seed independently resamples the training data with RandomUnderSampler before fitting.
Two workflows are available depending on your evaluation strategy:
evaluate() train()
───────────────────── ────────────────────
Full Dataset Full Dataset
│ │
▼ ▼
80/20 Split RandomUnderSampler
(stratified) (full dataset)
┌────┴────┐ │
Train Test Feature Selection
│ │ (full dataset)
RandomUnder │ │
Sampler │ GridSearchCV
│ │ + CV folds
Feature │ │
Selection │ ┌──────┴──────┐
│ │ model.pkl registry.csv
GridSearchCV│
+ CV folds │
│ │
Evaluate ◄──┘
│
Save model + metrics
-
evaluate()— splits the full dataset 80/20 internally. Trains on the 80% split and evaluates each seed on the held-out 20%. Use this when you want per-seed metrics and an in-sample accuracy estimate to compare classifiers or tune hyperparameters. -
train()— treats the entire dataset as the training set with no internal split. Evaluation is deferred toModelPredictorusing a separate future dataset. Use this for final production models where no data should be withheld.
For full detail on both workflows, see the Training Workflows wiki page.
from eruption_forecast.model.model_trainer import ModelTrainer trainer = ModelTrainer( extracted_features_csv="output/features/all_features.csv", label_features_csv="output/features/label_features.csv", output_dir="output/trainings", classifier="xgb", cv_strategy="stratified", number_of_significant_features=20, feature_selection_method="combined", n_jobs=4, ) trainer.evaluate( random_state=0, total_seed=500, sampling_strategy=0.75, )
trainer = ModelTrainer( extracted_features_csv="output/features/all_features.csv", label_features_csv="output/features/label_features.csv", output_dir="output/trainings", classifier="rf", n_jobs=4, ) trainer.train( random_state=0, total_seed=500, sampling_strategy=0.75, )
fit() is a unified entry point that dispatches to either workflow based on the with_evaluation flag:
# Equivalent to evaluate() trainer.fit(with_evaluation=True, random_state=0, total_seed=500, sampling_strategy=0.75) # Equivalent to train() trainer.fit(with_evaluation=False, random_state=0, total_seed=500, sampling_strategy=0.75)
ForecastModel.train() calls fit() internally and exposes with_evaluation as a direct parameter, so you can control the workflow from the high-level API without dropping down to ModelTrainer.
ModelPredictor applies trained models to new data and produces per-window eruption probability estimates. It supports two modes:
-
Evaluation mode (
predict()/predict_best()): Requires ground-truth labels. Evaluates each seed model against known eruption labels and aggregates metrics across seeds. Used when you have a labelled future dataset and want to measure out-of-sample performance. -
Forecast mode (
predict_proba()): No labels required. Produces a time series of eruption probabilities. Used for operational forecasting when ground truth is unavailable.
Both modes support single-model and multi-model consensus inference.
from eruption_forecast.model.model_predictor import ModelPredictor predictor = ModelPredictor( start_date="2025-03-16", end_date="2025-03-22", trained_models=trainer.csv, # trained_model_*.csv or merged .pkl from merge_models() output_dir="output/predictions", ) # Metrics for every (classifier, seed) combination df_metrics = predictor.predict( future_features_csv="output/features/future_all_features.csv", future_labels_csv="output/features/future_label_features.csv", ) print(df_metrics[["balanced_accuracy", "f1_score"]].describe()) # Best single seed by a chosen criterion evaluator = predictor.predict_best( future_features_csv="output/features/future_all_features.csv", future_labels_csv="output/features/future_label_features.csv", criterion="balanced_accuracy", ) print(evaluator.summary()) evaluator.plot_all()
predict_best() accepts any metric column as criterion: "accuracy", "balanced_accuracy", "f1_score", "precision", "recall", "roc_auc", "pr_auc".
predictor = ModelPredictor( start_date="2025-03-16", end_date="2025-03-22", trained_models=trainer.csv, # trained_model_*.csv or merged .pkl from merge_models() output_dir="output/predictions", ) df_forecast = predictor.predict_proba( tremor_data="path/to/tremor.csv", # or pd.DataFrame window_size=2, window_step=12, window_step_unit="hours", plot=True, )
Pass a dict of model registry paths to aggregate across classifiers. predict_proba() first aggregates within each classifier across seeds, then averages across classifiers to produce a consensus probability:
predictor = ModelPredictor( start_date="2025-03-16", end_date="2025-03-22", trained_models={ "rf": "output/VG.OJN.00.EHZ/trainings/predictions/random-forest-classifier/stratified-shuffle-split/trained_model_RandomForestClassifier-StratifiedShuffleSplit_rs-0_ts-500_top-20.csv", "xgb": "output/VG.OJN.00.EHZ/trainings/predictions/xgb-classifier/stratified-shuffle-split/trained_model_XGBClassifier-StratifiedShuffleSplit_rs-0_ts-500_top-20.csv", }, output_dir="output/predictions", ) df_forecast = predictor.predict_proba( tremor_data="path/to/tremor.csv", window_size=2, window_step=12, window_step_unit="hours", plot=True, )
Output columns (multi-model):
| Column | Description |
|---|---|
{name}_eruption_probability |
Mean P(eruption) across seeds of that classifier |
{name}_uncertainty |
Std across seeds of that classifier |
{name}_confidence |
Seed-level agreement fraction (0.5–1.0) |
{name}_prediction |
Hard label for that classifier |
consensus_eruption_probability |
Mean P(eruption) averaged across all classifiers |
consensus_uncertainty |
Std of per-classifier means (inter-model disagreement) |
consensus_confidence |
Fraction of classifiers voting with consensus majority |
consensus_prediction |
Hard label — 1 if consensus_eruption_probability >= 0.5
|
Results are saved to predictions.csv. The plot shows each classifier as a dashed line and the consensus as a solid black line with a shaded uncertainty band (eruption_forecast.png in figures/).
ForecastModel wires all stages together with a fluent method-chaining API. The root_dir parameter anchors all output paths.
from eruption_forecast import ForecastModel # Initialize with station metadata and global parameters fm = ForecastModel( root_dir="output", station="OJN", channel="EHZ", start_date="2025-01-01", end_date="2025-12-31", window_size=2, volcano_id="Lewotobi Laki-laki", n_jobs=4, verbose=True, ) # Run the complete pipeline with method chaining fm.calculate( source="sds", sds_dir="/path/to/sds/data", methods=["rsam", "dsar", "entropy"], plot_daily=True, save_plot=True, remove_outlier_method="maximum", ).build_label( start_date="2025-01-01", end_date="2025-07-24", day_to_forecast=2, window_step=6, window_step_unit="hours", eruption_dates=[ "2025-03-20", "2025-04-22", "2025-05-18", "2025-06-17", "2025-07-07", ], ).extract_features( select_tremor_columns=["rsam_f2", "rsam_f3", "rsam_f4", "dsar_f3-f4", "entropy"], save_tremor_matrix_per_method=True, exclude_features=["agg_linear_trend", "linear_trend_timewise", "length"], use_relevant_features=True, ).train( classifier="rf", cv_strategy="stratified", random_state=0, total_seed=500, with_evaluation=False, number_of_significant_features=20, sampling_strategy=0.75, save_all_features=True, plot_significant_features=True, ).forecast( start_date="2025-07-28", end_date="2025-08-04", window_step=10, window_step_unit="minutes", )
What this pipeline does:
- Calculate tremor — computes RSAM, DSAR, and Shannon Entropy from raw SDS waveforms with maximum-outlier removal and daily plots saved
- Build labels — creates binary labels for January 1 through July 24, marking 2-day windows before each known eruption as positive
- Extract features — builds the tremor matrix for selected columns, runs tsfresh, and retains statistically relevant features
-
Train models — trains a Random Forest across 500 random seeds on the full dataset (
with_evaluation=False), saving models and significant feature lists - Forecast — runs the trained ensemble on July 28 through August 4 and writes a probability time series with a consensus plot
If pre-computed tremor data already exists, skip the calculate() call:
fm.load_tremor_data( tremor_csv="output/VG.OJN.00.EHZ/tremor/tremor_2025年01月01日_2025年12月31日.csv" ).build_label(...).extract_features(...).train(...).forecast(...)
See main.py in the repository root for a complete working example.
After training 500 seeds, each estimator and its feature list live in separate files on disk. Calling merge_models() collapses them all into a single SeedEnsemble pkl, removing the per-seed I/O overhead at prediction time.
500 ×ばつ models/00000.pkl
500 ×ばつ features/significant_features/00000.csv
│
▼ trainer.merge_models()
│
merged_model_RandomForestClassifier-StratifiedKFold_rs-0_ts-500_top-20.pkl
(SeedEnsemble — one object, one file)
from eruption_forecast.model.seed_ensemble import SeedEnsemble # Merge right after training merged_path = trainer.merge_models() # Load and predict ensemble = SeedEnsemble.load(merged_path) mean_p, std, confidence, prediction = ensemble.predict_with_uncertainty( features_df, threshold=0.7, ) print(f"Mean P(eruption): {mean_p.mean():.4f}") print(f"Mean confidence: {confidence.mean():.4f}") print(f"Eruption windows: {prediction.sum()}") # sklearn-compatible interface proba = ensemble.predict_proba(features_df) # (n_windows, 2)
Multiple classifiers can be bundled into one file:
bundle_path = trainer.merge_classifier_models( {"rf": rf_trainer.csv, "xgb": xgb_trainer.csv} )
Pass a merged pkl directly to ModelPredictor — .pkl vs .csv is detected automatically:
from eruption_forecast.model.model_predictor import ModelPredictor predictor = ModelPredictor( start_date="2025-07-28", end_date="2025-08-04", trained_models=merged_path, # or bundle_path for multi-classifier ) df_forecast = predictor.predict_proba( tremor_data="output/VG.OJN.00.EHZ/tremor/tremor_*.csv", window_size=2, window_step=12, window_step_unit="hours", plot=True, )
For full details see Training Workflows and Evaluation and Forecasting.