Documentation
Everything needed to load SHAD, understand its labels and annotations, and reproduce the paper's evaluation.
Installation
The data (≈ 550 MB of CSV and 31 MB of XML) lives in the Git repository. Clone it, then install the small Python helper package
(it only depends on numpy and pandas):
git clone https://github.com/scality/shad.git cd shad pip install -e . # core loader pip install -e ".[plot]" # + matplotlib, for Series.plot() python -m shad # sanity check: prints the dataset summary
The loader locates the data automatically when you run from inside the repository. Otherwise pass SHAD(root="/path/to/shad")
or set the SHAD_ROOT environment variable.
Quick start
from shad import SHAD
ds = SHAD()
ds.summary() # series per scenario, with type / category / criticality
ds.catalog.head() # one row per series (id, group, explanation, paths, ...)
s = ds.load("1773343796") # -> shad.Series
s.data # (n, 171) DataFrame, UTC DatetimeIndex, 1-minute sampling
s.labels # (n, 171) per-dimension binary labels (same columns as data)
s.label # (n,) top-level binary label
s.X, s.Y, s.y # the same as numpy arrays
s.segments() # [(201, 291)] anomalous intervals as row indices (end exclusive)
s.anomalous_dimensions # the 4 labelled dimensions of store1/g2disk01
s.events # [AnomalyEvent(offset_minutes=210, duration_minutes=90, store='1', disk='g2disk01', ...)]
s.annotation.descriptions["store1-g2disk01-disk_read_bytes"]
s.plot() # matplotlib figure of the labelled dimensions
ds.load("1774026259", fill="ffill") # impute missing values ("ffill", "interpolate", "zero")
ds.filter(type="Disk", criticality="High") # ids matching metadata
train_ids, test_ids = ds.split() # 83 nominal / 132 anomalous (paper protocol)
A complete walk-through is available in the
getting-started notebook, and a
60-line semi-supervised baseline (detection + explanation) in
examples/baseline_detector.py.
Without the package
import pandas as pd
import xml.etree.ElementTree as ET
df = pd.read_csv("Dataset/Single Disk/1773343796.csv", parse_dates=["metric_timestamp"])
metrics = [c for c in df.columns if c != "metric_timestamp" and not c.startswith("label")]
X = df[metrics].to_numpy() # (n, 171)
Y = df[["label_" + m for m in metrics]].to_numpy() # (n, 171) per-dimension labels
y = df["label"].to_numpy() # (n,) top-level label
root = ET.parse("Annotations/1773343796.xml").getroot()
print(root.findtext("type"), root.findtext("criticality"), root.findtext("textual_explanation"))
Repository layout
shad/ ├── Dataset/ 215 CSV files, one folder per scenario │ ├── Nominal/ 83 series without anomaly │ ├── Single Disk/ ... Asynchronous SNode/ ├── Annotations/ 215 XML files, one per series (<id>.xml) ├── Representations/ Catch22 features and multi-panel images (LLM experiment inputs) ├── Scripts/ representation, LLM interpretability and website-building scripts ├── shad/ Python loader package (pip install -e .) ├── notebooks/ getting_started.ipynb ├── examples/ quickstart.py, baseline_detector.py ├── tests/ pytest sanity checks of the files and the loader └── docs/ this website (GitHub Pages)
A series is identified by its id: the Unix timestamp at which the experiment was launched (e.g. 1773343796).
The same id names its CSV file (Dataset/<scenario>/<id>.csv), its annotation (Annotations/<id>.xml),
and its representations.
Data format
CSV files
Each CSV has 344 columns and ~1,000–1,070 rows (one per minute over ~18 hours):
| Column(s) | Count | Content |
|---|---|---|
metric_timestamp | 1 | UTC timestamp, e.g. 2026-03-12 19:40:00.053000+00:00. Nominal sampling period: 60 s. |
<metric> | 171 | Telemetry values, see the metric dictionary. Order: 17 platform, 13 S3, 57 server, 84 device metrics. |
label_<metric> | 171 | Per-dimension binary label of the corresponding metric (same order). |
label | 1 | Top-level binary label. |
Metric names follow <metric> (platform and S3 levels), <store>-<metric> (server level, e.g.
store2-latency_sent_milliseconds) and <store>-<device>-<metric> (device level, e.g.
store1-g2disk01-disk_read_bytes), with stores store1–store3 and devices
g1disk01, g1disk02, g2disk01, g2disk02 (NVMe data disks), ssd01, ssd02 (metadata SSDs) and root (OS disk).
Labels
- Top-level label (
label): 1 while at least one anomaly is ongoing, from injection to recovery. It is exactly the logical OR of the 171 per-dimension labels. This is the label used for anomaly detection, in the same format as TSB-AD. - Per-dimension labels (
label_<metric>): 1 only on the dimensions of the root-cause component directly targeted by an ongoing anomaly (e.g. the 4 metrics of a failed disk, or the 47 server and device metrics of an isolated server). Other dimensions may react to the anomaly (cluster-wide latency, rebuild tasks on other servers, ...) without being labelled. These labels are the ground truth for explanation. - Failures that were never recovered (some disks in the simultaneous / asynchronous disk scenarios) stay labelled until the end of the series.
XML annotations
<dataset>
<id>…</id> experiment id
<anomaly_presence>true|false</anomaly_presence>
<category>None|Single|Simultaneous|Asynchronous|Degradation</category>
<type>None|Disk|Server|SNode</type>
<criticality>None|Low|Medium|High</criticality>
<textual_explanation>…</textual_explanation> free-text description written by the operators
<dimensions>
<dimension> one per column (timestamp + 171 metrics)
<name>…</name>
<description>…</description> unit and meaning of the metric
<anomalies> empty unless the dimension is a root-cause target
<anomaly>
<offset_minutes>…</offset_minutes> onset (minutes after the workload start)
<duration_minutes>…</duration_minutes> duration, "None" if never recovered
<store>1|2|3</store> targeted storage server
<disk>…</disk> targeted disk (disk failures only)
<degradation>…</degradation> max delay (server degradations only)
<related_dimensions> all dimensions affected by the same event
<dimension>…</dimension> …
With the package, Annotation.events returns the list of unique events (deduplicated across dimensions), and
Annotation.related_dimensions(dim) the dimensions sharing an event with dim.
Time reference
starting at … time in the textual explanation is the launch of the experiment; the workload and the telemetry
start ~11 minutes later. offset_minutes is counted from the first CSV timestamp and matches the start of the labelled periods within a
few minutes. Rows are not always exactly one minute apart (monitoring gaps of a few minutes occur), so always use the timestamps
or the label columns — not row indices — to locate anomalies precisely.Scenarios
Criticality is determined by the scenario. Onsets span 30 min to 16.5 h after the start; durations range from 1 minute to more than 9 hours.
- Disk failures: the NVMe disk is detached from the OS (lazy unmount + driver unbind); recovery rebinds the driver and triggers a RING disk replacement and rebuild. Asynchronous events are cascading (overlapping), rolling (partially overlapping) or independent.
- Server failures: Linux
tc/netemon the target server — 100% packet loss (single, asynchronous), or delay and loss ramped linearly or in stages (degradation, 0.1 ms–1 s delay, 0–30% loss). - Storage node failures: 1–3 of the 6 storage-node processes of a server are killed (SIGABRT) and later restarted.
Metric dictionary
The 171 dimensions, identical in every series. Also available as shad.metrics_table() and in
docs/METRICS.md.
Experiment catalog
All 215 series. Click a column header to sort, an id to open it in the explorer. Also available as
SHAD().catalog and in docs/EXPERIMENTS.md.
Evaluation protocol
Detection
- Split: semi-supervised methods are trained on the 83 nominal series; all methods are evaluated on the 132 anomalous series (
SHAD().split()). - Metric: VUS-PR with a 25-point buffer, computed on the top-level label. Detector implementations and tuned hyper-parameters come from TSB-AD.
- Methods: 10 semi-supervised (AE, OmniAnomaly, USAD, CNN, LSTM, OCSVM, TimesNet, FITS, MCD, Anomaly Transformer) and 8 unsupervised (KMeansAD, PCA, CBLOF, KNN, HBOS, LOF, Isolation Forest, Robust PCA).
Explanation
- Per-dimension contributions to the anomaly score are ranked and compared with the per-dimension labels using NDCG.
Interpretation
- Each series is given to a frozen LLM as (i) Catch22 features per dimension, (ii) a multi-panel image, or (iii) an LLM-written description of that image.
- The LLM answers a questionnaire: anomaly presence, one affected dimension, time range, type (disk / server / storage node), criticality. Scored with F1 (presence, type, criticality) and accuracy (dimension, time), averaged over 5 runs.
- Two supervision levels simulate an end-to-end pipeline: level 1 gives the anomaly presence, level 2 also gives the affected components.
Reproducing the paper
| Script | Purpose |
|---|---|
Scripts/query_XML.py | Examples of XML queries (global info, dimensions, anomalies by store, degradation, related dimensions). |
Scripts/generate_images.py | Multi-panel image of each series → Representations/Images/. |
Scripts/compute_features.py | Catch22 features of every dimension → Representations/Catch22/ (needs pycatch22). |
Scripts/interpretabiliy_main.py | LLM interpretability experiment for the three representations (needs mistralai, python-dotenv and a MISTRAL_API_KEY in .env). |
Scripts/check_consistency.py | Cross-checks CSV labels against XML annotations. |
Scripts/build_site_data.py | Regenerates the JSON files of this website and the markdown catalogs. |
pip install -e ".[interpretability]" echo "MISTRAL_API_KEY=..." > .env python Scripts/interpretabiliy_main.py # run from the repository root
Known caveats
- Missing values. Less than 1% of the values are missing. This occurs as the anomalies happen in a real world system. The loader keeps them as
NaN; usefill=to impute. - Unplanned crashes, Permanent disk losses, telemetry issues. Few experiments show anomalies that were not fixed as planned and lasted much longer, or problems with some telemetries not working as expected. All these experiments were re-run, and both versions are included (the unplanned versions are annotated).
This website
The site is static (HTML + vanilla JS, no build step) and lives in docs/. The explorer reads the CSV and XML files straight from the repository
(raw.githubusercontent.com), so nothing is duplicated.
python Scripts/build_site_data.py # after any change in Dataset/ or Annotations/ python -m http.server 8000 # from the repository root, then open http://localhost:8000/docs/
Locally, the explorer reads ../Dataset directly. To publish: Settings → Pages → Deploy from a branch → main / docs.
Citation & license
@inproceedings{stanzione2026shad,
title = {Detect, Explain, Interpret: An End-to-End Benchmark for Time Series
Anomaly Detection, Explainability and Interpretability},
author = {Stanzione, Roberto and Barbe, Jules and Parrino, Magali and
Fourmann, J{\'e}r{\'e}mie and Boniol, Paul},
booktitle = {Advances in Neural Information Processing Systems (NeurIPS),
Evaluations and Datasets Track},
year = {2026}
}
Data and code are released under the AGPL-3.0-only license.