-
Notifications
You must be signed in to change notification settings - Fork 0
Pipeline Walkthrough
This page is the annotated tour of the two bundled entry-point scripts:
-
Research Workflow -
main.py- one-shot calculate → train → predict → evaluate → explain on a fixed date split. -
Scenarios Workflow -
scenarios.py- same pipeline executed once per scenario in a nine-scenario sweep.
Both scripts share the same ForecastModel instance and the same calculate() output; they differ in how the train → predict → evaluate → explain legs are sequenced.
main.py runs a single, linear pipeline: it computes tremor over the whole year,
trains on the first seven months, forecasts the next four weeks, evaluates against the held-out eruptions, and finishes with per-seed SHAP explanations over the tree classifiers.
┌──────────────────────────────────────────────────────────────────────┐
│ main.py - Stage Flow │
└──────────────────────────────────────────────────────────────────────┘
fm = ForecastModel(network="VG", station="OJN", location="00",
channel="EHZ", day_to_forecast=2,
n_jobs=8, verbose=True)
│
▼
┌─────────────────────┐
│ fm.calculate() │ CalculateTremor → tremor_*.csv
│ │ source = SDS (D:\Data\OJN)
│ │ methods = rsam, dsar, entropy
│ │ dates: 2025-01-01 → 2025-12-31
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ fm.train() │ TrainingModel → ClassifierEnsemble
│ │ build_label (window_step = 6 h, dtf = 2)
│ │ extract_features (tsfresh, 5 tremor columns)
│ │ fit (4 classifiers × 25 seeds)
│ │ cv = shuffle-stratified, scoring = recall
│ │ resample = under, n_jobs = 4, n_grids = 4
└──────────┬──────────┘
│ cached → {station_dir}/training/{hash}.TrainingModel.pkl
│
▼
┌─────────────────────┐
│ fm.predict() │ PredictionModel → results
│ │ build_label (window_step = 10 min)
│ │ extract_features (same tremor columns)
│ │ forecast (4 clf × 25 seeds → consensus)
│ │ plot_threshold = 0.7, save_seed_result = True
└──────────┬──────────┘
│ cached → {station_dir}/prediction/{hash}.PredictionModel.pkl
│
▼
┌─────────────────────┐
│ fm.evaluate( │ EvaluationModel(model="prediction")
│ model= │ MetricsEnsemble.compute()
│ "prediction") │ aggregate per-classifier metrics
│ │ plot_per_seed = True
└──────────┬──────────┘
│ cached → {station_dir}/evaluation/prediction/{hash}.EvaluationModel.pkl
│
▼
┌─────────────────────┐
│ fm.EvaluationModel │ ClassifierComparator
│ .compare() │ ranking CSV + comparison plots
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ fm.explain( │ ExplanationModel(model="prediction")
│ model= │ ExplainerEnsemble.explain() (tree classifiers only)
│ "prediction") │ per-seed bar/beeswarm + per-eruption waterfalls
│ │ save_per_seed = True, plot_per_seed = False
└─────────────────────┘
│ cached → {station_dir}/explanation/prediction/{hash}.ExplanationModel.pkl
- Runs once across the full year so the same tremor frame is reused by every downstream stage.
-
start_dateis internally shifted back byday_to_forecastdays (forecast_model.py:154) so the first label window has enough lead-in. -
interpolate=Truefills miniSEED gaps so tsfresh receives a continuous signal. -
plot_daily=Trueemits per-day band plots undertremor/figures/- useful for spotting station outages before training.
-
classifiers=["lite-rf", "rf", "gb", "xgb"]- four classifiers fit independently; their fittedSeedEnsembles are bundled into a singleClassifierEnsemble. -
eruption_dateslists every known eruption - both the training-window eruptions and the held-out ones the forecast will be evaluated against. -
scoring="recall"instructsGridSearchCVto maximise positive recall - false negatives are worse than false positives for eruption forecasting. -
seeds=25inmain.pyis a quick run; the default inscenarios.pyisseeds=10per scenario. -
n_jobs=4, n_grids=4- 4 outer seed workers × 4 inner CV workers. Clamped byBaseModel.validate()if the box has fewer cores.
-
window_step=10, window_step_unit="minutes"- 144 forecasts/day. The dense grid is what makes the forecast plot smooth. -
save_seed_result=Truewrites one CSV per seed underprediction/results/{clf-slug}/for downstream uncertainty analysis. -
use_cache=Falseforces a fresh forecast - flip toTrueto short-circuit when nothing upstream changed. - The cache identity threads in
_training_cache_hash, so re-training automatically invalidates downstream predictions.
- Reuses the in-memory
PredictionModel- no re-extraction of features, no re-fit. - Falls back to
train()'seruption_dateswhen called without an explicit list. - Writes
(n_samples, n_seeds)y_proba.csv/y_pred.csvmatrices underevaluation/prediction/classifiers/{Clf}/predictions/(no per-seed JSON; per-seed metric tables live in memory onself.metrics). -
plot_per_seed=Trueis expensive - flip off for fast iteration. -
use_cache=True(default) consults{evaluation_dir}/{hash}.EvaluationModel.pklbefore the per-classifierpredict_probapass; passuse_cache=Falseto force a fresh evaluation.
- Reuses the cached
MetricsEnsemblefromevaluate()and hands it toClassifierComparator. -
comparator.get_ranking()writescomparison/metrics/ranking_recall.csv(defaults to recall ranking - matches the trainingscoring). -
comparator.plot_all()writes ROC overlay, metric bars, seed stability violins, and a comparison grid underevaluation/prediction/comparison/figures/.
- Called at the end of
main.pyafterevaluate(...); produces per-seed SHAP bar + beeswarm plots and per-eruption waterfall plots. - Restricted to tree classifiers (RF /
lite-rf/ GB / XGB). Non-tree classifiers in the ensemble are skipped with a warning. -
eruption_datesis passed explicitly inmain.py(the same eight-eruption list used bytrain(...)); when omitted it falls back totrain()'s dates just likeevaluate(...). -
save_per_seed=Truewrites per-seedshap.Explanationpickles alongside the bundledClassifierExplanation_*.pkl;plot_per_seed=Falseskips per-seed PNG rendering (aggregate bar + beeswarm still run). -
use_cache=True(default) consults{explanation_dir}/{hash}.ExplanationModel.pklbefore re-running SHAP; passuse_cache=Falseto force a fresh explanation. - Output lands under
explanation/prediction/- see Explanation Workflow for the full tree.
scenarios.py keeps the same ForecastModel instance and the same tremor frame, but loops the train → predict → evaluate legs over nine scenarios that vary the training and prediction date splits. Each scenario's outputs land in its own directory so they can be compared side-by-side.
| # | Train window | Forecast window | Goal |
|---|---|---|---|
| 1 | 2025-01-01 → 2025-03-31 | 2025-04-01 → 2025-04-30 | Train on 1 eruption, forecast eruption 2 |
| 2 | 2025-01-01 → 2025-03-31 | 2025-05-01 → 2025-05-31 | Train on 1 eruption, forecast eruption 3 |
| 3 | 2025-01-01 → 2025-03-31 | 2025-06-01 → 2025-06-30 | Train on 1 eruption, forecast eruption 4 |
| 5 | 2025-01-01 → 2025-04-30 | 2025-05-01 → 2025-05-31 | Train on 1+2, forecast eruption 3 |
| 6 | 2025-01-01 → 2025-05-31 | 2025-06-01 → 2025-06-30 | Train on 1+2+3, forecast eruption 4 |
| 7 | 2025-01-01 → 2025-06-30 | 2025-07-01 → 2025-07-13 | Train on 1–4, forecast eruption 5 |
| 8 | 2025-01-01 → 2025-07-26 | 2025-07-27 → 2025-08-22 | Train on 1–5, forecast eruptions 6+7 |
| 9 | 2025-01-01 → 2025-08-22 | 2025-01-01 → 2025-08-22 | Sanity check - train and predict over the full record |
(Scenario 4 is intentionally absent in scenarios.py.)
┌──────────────────────────────────────────────────────────────────────────┐
│ scenarios.py - Outer-Loop Flow │
└──────────────────────────────────────────────────────────────────────────┘
fm = ForecastModel(...) # one ForecastModel reused
│
▼
┌─────────────────────┐
│ fm.calculate() │ one full-year tremor CSV (shared across scenarios)
└──────────┬──────────┘
│
▼
for scenario in scenarios: # nine scenarios in scenarios.py
│
│ output_dir = os.path.join(
│ root_dir, "output", fm.nslc,
│ "scenarios", slugify(name))
│
▼
┌───────────────────────────────────────────────────────────────────┐
│ (scoped to scenario.output_dir) │
│ │
│ fm.train(start, end, eruption_dates, …, output_dir=output_dir) │
│ │ │
│ ▼ │
│ fm.predict(start, end, …, output_dir=output_dir, │
│ eruption_dates=plot_kwargs["eruption_dates"]) │
│ │ │
│ ▼ │
│ tn = TelegramNotification(verbose=False) │
│ tn.send_message(message=f"{name}: {description}") \ │
│ .send_document( │
│ file=fm.PredictionModel.forecast_plot_path, │
│ caption=f"{name}: {description}") │
│ │ │
│ ▼ │
│ fm.evaluate(model="prediction", plot_per_seed=True, │
│ output_dir=output_dir) │
│ │ │
│ ▼ │
│ fm.explain(model="prediction", │
│ eruption_dates=eruption_dates, │
│ save_per_seed=True, plot_per_seed=False, │
│ max_display=20) │
└───────────────────────────────────────────────────────────────────┘
│
▼
next scenario → loop back
-
output_diris built once per scenario fromslugify(name)("Scenario 1"→"scenario-1"). Every stage call inside the loop forwards thatoutput_dir, so artefacts land atoutput/{nslc}/scenarios/{slug}/. - The outer
fm.calculate(...)runs once before the loop. Its tremor frame is captured onfm.tremor_dfand reused on every iteration -fm.train()reads fromfm.tremor_df, not from disk. -
eruption_datesis the full list of eight known eruptions on every scenario. The training window simply excludes the eruptions that haven't happened yet, and the prediction window picks them up later. -
plot_kwargs["eruption_dates"]is forwarded intofm.predict(...)as**plot_kwargs, which routes them toforecast_plotsso eruption-day markers are drawn on the per-scenario forecast plot. - The Telegram hook (
TelegramNotification.send_message(...).send_document(...)) ships the per-scenario forecast PNG to the configured chat the momentpredict()returns. Usingsend_document(...)(rather thansend_photo(...)) forces the file to be uploaded as a document — Telegram never re-encodes it, so the full DPI plot is preserved. -
fm.evaluate(...)runs after the notification, so by the time you see the plot in Telegram, the per-seedy_proba/y_predmatrices and aggregate metric plots are already being written in the background. -
fm.explain(...)runs last in each scenario - per-seed SHAP explanations are bundled intoClassifierExplanation_*.pklunderexplanation/prediction/classifiers/and per-eruption waterfall plots land underexplanation/prediction/eruptions/. See Explanation Workflow for the full output tree.
output/
└── VG.OJN.00.EHZ/
├── tremor/ # shared across all scenarios
│ └── VG.OJN.00.EHZ_2025-01-01_2025-12-31.csv
└── scenarios/
├── scenario-1/
│ ├── training/
│ ├── prediction/
│ ├── evaluation/prediction/
│ ├── explanation/prediction/
│ └── cache/
├── scenario-2/
...
└── scenario-9/
See Output Structure for the full per-scenario tree.
Research (main.py) |
Scenarios (scenarios.py) |
|
|---|---|---|
| Number of forecasts | 1 | N (9 in the bundled script) |
| Tremor computation | once | once (reused) |
| Training | once | once per scenario |
| Output isolation | flat | one directory per scenario |
| Telegram notifications | per-stage via @notify decorator |
per-scenario forecast plot via TelegramNotification().send_message(...).send_document(...)
|
| Use case | a single research run on a fixed split | leave-one-out style sweeps, ablation over training windows |
When the goal is to publish a forecast plot, use main.py. When the goal is to compare how forecast quality degrades as the training-window shifts, use scenarios.py.