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
You do not need the package to use SHAD: every series is a plain CSV file and every annotation a plain XML file (see Data format). The package only saves you the parsing and harmonises a few inconsistencies.

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)CountContent
metric_timestamp1UTC timestamp, e.g. 2026-03-12 19:40:00.053000+00:00. Nominal sampling period: 60 s.
<metric>171Telemetry values, see the metric dictionary. Order: 17 platform, 13 S3, 57 server, 84 device metrics.
label_<metric>171Per-dimension binary label of the corresponding metric (same order).
label1Top-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

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

The 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.

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

Explanation

Interpretation

Reproducing the paper

ScriptPurpose
Scripts/query_XML.pyExamples of XML queries (global info, dimensions, anomalies by store, degradation, related dimensions).
Scripts/generate_images.pyMulti-panel image of each series → Representations/Images/.
Scripts/compute_features.pyCatch22 features of every dimension → Representations/Catch22/ (needs pycatch22).
Scripts/interpretabiliy_main.pyLLM interpretability experiment for the three representations (needs mistralai, python-dotenv and a MISTRAL_API_KEY in .env).
Scripts/check_consistency.pyCross-checks CSV labels against XML annotations.
Scripts/build_site_data.pyRegenerates 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

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.