Skip to content

Other examples#

Banking dataset#

In this section, we detail the pytorch code for designing an explainable deep model for processing the Banking dataset.

Banking is a dataset for binary classification from tabular inputs.

Please download the dataset here and update the tutorial data path accordingly.

The data is related to direct marketing campaigns (phone calls) of a Portuguese banking institution. The classification goal is to predict if the client will subscribe to a term deposit (variable y).

👀 Full file preview
"""Banking dataset workflow, binary classification with tabular data."""

import os
from functools import partial

import boto3
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import torch
from botocore.client import Config
from sklearn.model_selection import train_test_split
from torch import nn
from torch.nn import Sequential
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torchmetrics.classification import MulticlassAccuracy, MulticlassConfusionMatrix, MulticlassF1Score

from xpdeep import init, set_project
from xpdeep.dataset.parquet_dataset import FittedParquetDataset, ParquetDataset
from xpdeep.explain.explainer import Explainer
from xpdeep.explain.quality_metrics import Infidelity, Sensitivity
from xpdeep.explain.statistic import DictStats, DistributionStat
from xpdeep.metric import DictMetrics, TorchGlobalMetric, TorchLeafMetric
from xpdeep.model.feature_extraction_output_type import FeatureExtractionOutputType
from xpdeep.model.model_parameters import ModelDecisionGraphParameters
from xpdeep.model.xpdeep_model import XpdeepModel
from xpdeep.model.zoo.cross_entropy_loss_from_proba import CrossEntropyLossFromProbabilities
from xpdeep.project import Project, get_project
from xpdeep.trainer.callbacks import EarlyStopping, Scheduler
from xpdeep.trainer.trainer import Trainer

STORAGE_OPTIONS = {
    "key": os.getenv("S3_DATASET_ACCESS_KEY_ID"),
    "secret": os.getenv("S3_DATASET_SECRET_ACCESS_KEY"),
    "client_kwargs": {
        "endpoint_url": os.getenv("S3_DATASET_ENDPOINT_URL"),
    },
    "s3_additional_kwargs": {"addressing_style": "path"},
}


def main():
    """Process the dataset, train, and explain the model."""
    torch.random.manual_seed(42)

    # ##### Prepare the Dataset #######

    # 1. Load and preprocess data
    data = pd.read_csv("banking_dataset/train.csv", sep=";")
    filtered_data = data[data["y"] == "no"].sample(n=10000, random_state=42)
    data_train = pd.concat([data[data["y"] != "no"], filtered_data])

    test_data = pd.read_csv("banking_dataset/test.csv", sep=";")

    # 2. Split training set into training and validation
    train_data, val_data = train_test_split(data_train, test_size=0.15, random_state=42)

    # 4. Convert to pyarrow Table format and save as parquet files
    pq.write_table(pa.Table.from_pandas(train_data, preserve_index=False), "train.parquet")
    pq.write_table(pa.Table.from_pandas(val_data, preserve_index=False), "val.parquet")
    pq.write_table(pa.Table.from_pandas(test_data, preserve_index=False), "test.parquet")

    # 5. Upload dataset and analyze
    client = boto3.client(
        service_name="s3",
        endpoint_url=os.getenv("S3_DATASET_ENDPOINT_URL"),
        aws_access_key_id=os.getenv("S3_DATASET_ACCESS_KEY_ID"),
        aws_secret_access_key=os.getenv("S3_DATASET_SECRET_ACCESS_KEY"),
        config=Config(signature_version="s3v4"),
    )

    client.upload_file("train.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "banking_dataset/train.parquet")
    client.upload_file("val.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "banking_dataset/val.parquet")
    client.upload_file("test.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "banking_dataset/test.parquet")

    train_dataset = ParquetDataset(
        name="banking_train_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/banking_dataset/train.parquet",
        storage_options=STORAGE_OPTIONS,
    )
    analyzed_train_dataset = train_dataset.analyze(target_names=["y"])
    fit_train_dataset = analyzed_train_dataset.fit()

    # Create test and validation datasets based on the fitted schema
    fit_test_dataset = FittedParquetDataset(
        name="banking_test_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/banking_dataset/test.parquet",
        storage_options=STORAGE_OPTIONS,
        fitted_schema=fit_train_dataset.fitted_schema,
    )

    fit_val_dataset = FittedParquetDataset(
        name="banking_validation_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/banking_dataset/val.parquet",
        storage_options=STORAGE_OPTIONS,
        fitted_schema=fit_train_dataset.fitted_schema,
    )

    # ##### Prepare the Model #######

    # 1. Set input and target sizes based on the fitted schema
    input_size = fit_train_dataset.fitted_schema.input_size[1]
    target_size = fit_train_dataset.fitted_schema.target_size[1]

    # 2. Initialize models for feature extraction and task learning
    feature_extractor = Sequential(nn.Linear(input_size, 128), nn.ReLU(), nn.Linear(128, 50), nn.ReLU())
    task_learner = Sequential(nn.Linear(50, target_size), nn.Softmax(dim=1))

    # 3. Specify the model's structure and constraints
    model_specifications = ModelDecisionGraphParameters(
        graph_depth=3,
        target_homogeneity_pruning_threshold=0.99,
        population_pruning_threshold=0.01,
        prune_step=25,
        target_homogeneity_weight=1.0,
        discrimination_weight=0.2,
        balancing_weight=0.25,
        internal_model_complexity=1,
        feature_extraction_output_type=FeatureExtractionOutputType.VECTOR,
    )

    # 4. Create the explainable model using the xpdeep API
    xpdeep_model = XpdeepModel.from_torch(
        fitted_schema=fit_train_dataset.fitted_schema,
        feature_extraction=feature_extractor,
        task_learner=task_learner,
        backbone=None,
        decision_graph_parameters=model_specifications,
    )

    # ##### Train #######

    # 1. Define metrics and callbacks for training
    metrics = DictMetrics(
        global_multi_class_accuracy=TorchGlobalMetric(
            partial(MulticlassAccuracy, num_classes=target_size, average="micro"), target_as_indexes=True
        ),
        leaf_multi_class_accuracy=TorchLeafMetric(
            partial(MulticlassAccuracy, num_classes=target_size, average="micro"), target_as_indexes=True
        ),
        global_multi_class_F1_score=TorchGlobalMetric(
            partial(MulticlassF1Score, num_classes=target_size, average="macro"), target_as_indexes=True
        ),
        leaf_multi_class_F1_score=TorchLeafMetric(
            partial(MulticlassF1Score, num_classes=target_size, average="macro"), target_as_indexes=True
        ),
        global_confusion_matrix=TorchGlobalMetric(
            partial(MulticlassConfusionMatrix, normalize="all", num_classes=target_size), target_as_indexes=True
        ),
        leaf_confusion_matrix=TorchLeafMetric(
            partial(MulticlassConfusionMatrix, normalize="all", num_classes=target_size), target_as_indexes=True
        ),
    )

    callbacks = [
        EarlyStopping(monitoring_metric="Total loss", mode="minimize", patience=7),
        Scheduler(
            pre_scheduler=partial(ReduceLROnPlateau, mode="min", patience=5),
            step_method="epoch",
            monitoring_metric="Total loss",
        ),
    ]

    # 2. Define optimizer
    optimizer = partial(torch.optim.AdamW, lr=0.01, foreach=False, fused=False)

    # 3. Create and run the trainer
    trainer = Trainer(
        loss=CrossEntropyLossFromProbabilities(reduction="none"),
        optimizer=optimizer,
        callbacks=callbacks,
        start_epoch=0,
        max_epochs=25,
        metrics=metrics,
    )

    trained_model = trainer.train(
        model=xpdeep_model,
        train_set=fit_train_dataset,
        validation_set=fit_val_dataset,
        batch_size=2048,
    )

    # ##### Explain #######

    # 1. Build the Explainer
    statistics = DictStats(
        distribution_target=DistributionStat(on="target"), distribution_prediction=DistributionStat(on="prediction")
    )
    quality_metrics = [Sensitivity(), Infidelity()]

    explainer = Explainer(
        description_representativeness=1000, quality_metrics=quality_metrics, metrics=metrics, statistics=statistics
    )

    # 2. Generate global model explanations
    model_explanations = explainer.global_explain(
        trained_model,
        train_set=fit_train_dataset,
        test_set=fit_test_dataset,
        validation_set=fit_val_dataset,
    )
    print(model_explanations.visualisation_link)


if __name__ == "__main__":
    init(api_key=os.getenv("API_KEY"), api_url=os.getenv("API_URL"))
    set_project(Project.create_or_get(name="Banking Dataset Tutorial"))

    try:
        main()
    finally:
        get_project().delete()

Insurance dataset#

In this section, we detail the pytorch code for designing an explainable deep model for processing the Insurance dataset.

Insurance is a dataset for regression from tabular inputs.

Please download the dataset here and update the tutorial data path accordingly.

The "Insurance Dataset for Predicting Health Insurance Premiums in the US" is a collection of data on various factors that can influence medical costs and premiums for health insurance in the United States. The dataset includes information on 10 variables, including age, gender, body mass index (BMI), number of children, smoking status, region, income, education, occupation, and type of insurance plan.

👀 Full file preview
"""Insurance dataset workflow, regression with tabular data."""

import os
from functools import partial

import boto3
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import torch
from botocore.client import Config
from sklearn.model_selection import train_test_split
from torch import nn
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torchmetrics import MeanSquaredError

from xpdeep import init, set_project
from xpdeep.dataset.parquet_dataset import FittedParquetDataset, ParquetDataset
from xpdeep.explain.explainer import Explainer
from xpdeep.explain.quality_metrics import Infidelity, Sensitivity
from xpdeep.explain.statistic import DictStats, HistogramStat, VarianceStat
from xpdeep.metric import DictMetrics, TorchGlobalMetric, TorchLeafMetric
from xpdeep.model.feature_extraction_output_type import FeatureExtractionOutputType
from xpdeep.model.model_parameters import ModelDecisionGraphParameters
from xpdeep.model.xpdeep_model import XpdeepModel
from xpdeep.project import Project, get_project
from xpdeep.trainer.callbacks import EarlyStopping, ModelCheckpoint, Scheduler
from xpdeep.trainer.trainer import Trainer

STORAGE_OPTIONS = {
    "key": os.getenv("S3_DATASET_ACCESS_KEY_ID"),
    "secret": os.getenv("S3_DATASET_SECRET_ACCESS_KEY"),
    "client_kwargs": {
        "endpoint_url": os.getenv("S3_DATASET_ENDPOINT_URL"),
    },
    "s3_additional_kwargs": {"addressing_style": "path"},
}


def main():
    """Process the dataset, train, and explain the model."""
    torch.random.manual_seed(42)

    # ##### Prepare the Dataset #######

    # 1. Load and preprocess data
    data = pd.read_csv("insurance/insurance_dataset.csv")
    data["medical_history"] = data["medical_history"].fillna("No Record")
    data["family_medical_history"] = data["family_medical_history"].fillna("No Record")
    data.rename(columns={"medical_history": "indv_medical_history"}, inplace=True)  # noqa: PD002

    # Split dataset into training and test sets
    train_data, test_data = train_test_split(data, test_size=0.25, random_state=42)

    # Convert to pyarrow Table format and save as parquet files
    pq.write_table(pa.Table.from_pandas(train_data, preserve_index=False), "train.parquet")
    pq.write_table(pa.Table.from_pandas(test_data, preserve_index=False), "test.parquet")

    # Upload dataset and analyze
    client = boto3.client(
        service_name="s3",
        endpoint_url=os.getenv("S3_DATASET_ENDPOINT_URL"),
        aws_access_key_id=os.getenv("S3_DATASET_ACCESS_KEY_ID"),
        aws_secret_access_key=os.getenv("S3_DATASET_SECRET_ACCESS_KEY"),
        config=Config(signature_version="s3v4"),
    )

    client.upload_file("train.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "insurance/train.parquet")
    client.upload_file("test.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "insurance/test.parquet")

    # Create and fit training dataset
    train_dataset = ParquetDataset(
        name="insurance_train_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/insurance/train.parquet",
        storage_options=STORAGE_OPTIONS,
    )
    analyzed_train_dataset = train_dataset.analyze(target_names=["charges"])
    fit_train_dataset = analyzed_train_dataset.fit()

    # Create test dataset based on the fitted schema
    fit_test_dataset = FittedParquetDataset(
        name="insurance_test_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/insurance/test.parquet",
        storage_options=STORAGE_OPTIONS,
        fitted_schema=fit_train_dataset.fitted_schema,
    )

    # ##### Prepare the Model #######

    # 1. Set input and target sizes based on the fitted schema
    input_size = fit_train_dataset.fitted_schema.input_size[1]
    target_size = fit_train_dataset.fitted_schema.target_size[1]  # Should be 1 for regression

    # 2. Initialize models for feature extraction and task learning
    feature_extractor = nn.Sequential(
        nn.Linear(input_size, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU()
    )

    task_learner = nn.Sequential(nn.Linear(64, target_size))

    # 3. Specify the model's structure and constraints
    model_specifications = ModelDecisionGraphParameters(
        graph_depth=3,
        discrimination_weight=0.1,
        target_homogeneity_weight=0.1,
        target_homogeneity_pruning_threshold=0.9,
        population_pruning_threshold=0.15,
        balancing_weight=0.4,
        prune_step=7,
        internal_model_complexity=1,
        feature_extraction_output_type=FeatureExtractionOutputType.VECTOR,
    )

    # 4. Create the explainable model using the xpdeep API
    xpdeep_model = XpdeepModel.from_torch(
        fitted_schema=fit_train_dataset.fitted_schema,
        feature_extraction=feature_extractor,
        task_learner=task_learner,
        backbone=None,
        decision_graph_parameters=model_specifications,
    )

    # ##### Train #######

    # 1. Define metrics and callbacks for training
    metrics = DictMetrics(
        mse=TorchGlobalMetric(metric=partial(MeanSquaredError), on_raw_data=True),
        leaf_metric_mse=TorchLeafMetric(metric=partial(MeanSquaredError), on_raw_data=True),
        rmse=TorchGlobalMetric(partial(MeanSquaredError, squared=False), on_raw_data=True),
        leaf_metricrmse=TorchLeafMetric(partial(MeanSquaredError, squared=False), on_raw_data=True),
    )

    # Here as there is no validation set the only available metric is "Total loss" which is the total train loss
    # per epoch.
    callbacks = [
        EarlyStopping(monitoring_metric="Total loss", mode="minimize", patience=10),
        Scheduler(
            pre_scheduler=partial(ReduceLROnPlateau, mode="min", patience=3),
            step_method="epoch",
            monitoring_metric="Total loss",
        ),
        ModelCheckpoint(monitoring_metric="Total loss", mode="minimize"),
    ]

    # 2. Define optimizer
    optimizer = partial(torch.optim.AdamW, lr=0.001, foreach=False, fused=False)

    # 3. Create and run the trainer
    trainer = Trainer(
        loss=torch.nn.MSELoss(reduction="none"),
        optimizer=optimizer,
        callbacks=callbacks,
        start_epoch=0,
        max_epochs=10,
        metrics=metrics,
    )

    trained_model = trainer.train(
        model=xpdeep_model,
        train_set=fit_train_dataset,
        validation_set=None,
        batch_size=4096,
    )

    # ##### Explain #######

    # 1. Build the Explainer
    statistics = DictStats(
        histogram_target=HistogramStat(on="target", num_bins=20, num_items=1000, on_raw_data=True),
        histogram_prediction=HistogramStat(on="prediction", num_bins=20, num_items=1000, on_raw_data=True),
        histogram_error=HistogramStat(on="prediction_error", num_bins=20, num_items=1000, on_raw_data=True),
        variance_target=VarianceStat(on="target", on_raw_data=True),
        variance_prediction=VarianceStat(on="prediction", on_raw_data=True),
    )
    quality_metrics = [Sensitivity(), Infidelity()]

    explainer = Explainer(
        description_representativeness=1000, quality_metrics=quality_metrics, metrics=metrics, statistics=statistics
    )

    # 2. Generate global model explanations
    model_explanations = explainer.global_explain(
        trained_model,
        train_set=fit_train_dataset,
        test_set=fit_test_dataset,
    )
    print(model_explanations.visualisation_link)


if __name__ == "__main__":
    init(api_key=os.getenv("API_KEY"), api_url=os.getenv("API_URL"))
    set_project(Project.create_or_get(name="Insurance Dataset Tutorial"))

    try:
        main()
    finally:
        get_project().delete()

ECG dataset#

In this section, we detail the pytorch code for designing an explainable deep model for processing the ECG dataset.

ECG is a dataset for classification with time-series inputs.

Please download the dataset here and update the tutorial data path accordingly. The MIT-BIH Arrhythmia Database contains 48 half-hour excerpts of two-channel ambulatory ECG recordings, obtained from 47 subjects studied by the BIH Arrhythmia Laboratory between 1975 and 1979. Twenty-three recordings were chosen at random from a set of 4,000 24-hour ambulatory ECG recordings collected from a mixed population of inpatients (about 60%) and outpatients (about 40%) at Boston's Beth Israel Hospital. The remaining 25 recordings were selected from the same set to include less common but clinically significant arrhythmias that would not be well-represented in a small random sample.

👀 Full file preview
"""ECG dataset workflow, regression with time series data."""

import csv
import os
from functools import partial
from pathlib import Path

import boto3
import datasets
import numpy as np
import pandas as pd
import pywt
import torch
from botocore.client import Config
from datasets import Features
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torchmetrics.classification import MulticlassAccuracy, MulticlassConfusionMatrix, MulticlassF1Score

from xpdeep import init, set_project
from xpdeep.dataset.feature.feature import CategoricalFeature, ExplainableFeature, UnivariateTimeSeriesFeature
from xpdeep.dataset.parquet_dataset import AnalyzedParquetDataset, FittedParquetDataset
from xpdeep.dataset.preprocessor.preprocessor import SklearnPreprocessor
from xpdeep.dataset.preprocessor.zoo.doc import Scaler
from xpdeep.dataset.schema import AnalyzedSchema
from xpdeep.explain.explainer import Explainer
from xpdeep.explain.quality_metrics import Infidelity, Sensitivity
from xpdeep.explain.statistic import DictStats, DistributionStat
from xpdeep.metric import DictMetrics, TorchGlobalMetric, TorchLeafMetric
from xpdeep.model.feature_extraction_output_type import FeatureExtractionOutputType
from xpdeep.model.model_parameters import ModelDecisionGraphParameters
from xpdeep.model.xpdeep_model import XpdeepModel
from xpdeep.model.zoo.cross_entropy_loss_from_proba import CrossEntropyLossFromProbabilities
from xpdeep.model.zoo.doc import EcgCNN
from xpdeep.model.zoo.mlp import MLP
from xpdeep.project import Project, get_project
from xpdeep.trainer.callbacks import EarlyStopping, ModelCheckpoint, Scheduler
from xpdeep.trainer.trainer import Trainer

STORAGE_OPTIONS = {
    "key": os.getenv("S3_DATASET_ACCESS_KEY_ID"),
    "secret": os.getenv("S3_DATASET_SECRET_ACCESS_KEY"),
    "client_kwargs": {
        "endpoint_url": os.getenv("S3_DATASET_ENDPOINT_URL"),
    },
    "s3_additional_kwargs": {"addressing_style": "path"},
}


def main():
    """Process the dataset, train, and explain the model."""
    torch.random.manual_seed(42)

    # Prepare the Dataset #######

    path = "mitbih_database/"
    window_size = 180

    def denoise(data: list[float] | np.ndarray) -> np.ndarray:
        """Denoise the ECG signal using wavelet decomposition."""
        w = pywt.Wavelet("sym4")
        maxlev = pywt.dwt_max_level(len(data), w.dec_len)
        threshold = 0.04  # Threshold for filtering

        coeffs = pywt.wavedec(data, "sym4", level=maxlev)
        for i in range(1, len(coeffs)):
            coeffs[i] = pywt.threshold(coeffs[i], threshold * max(coeffs[i]))

        return pywt.waverec(coeffs, "sym4")

    classes = ["N", "L", "R", "A", "V"]

    x = []
    y = []

    # Read files
    filenames = next(os.walk(path))[2]
    filenames.sort()

    # Segregate filenames and annotations
    records = []
    annotations = []
    # segregating filenames and annotations
    for f in filenames:
        file_path = Path(path) / f
        if file_path.suffix == ".csv":
            records.append(file_path)
        else:
            annotations.append(file_path)

        # Records
    for r in range(len(records)):
        signals = []

        with records[r].open(mode="r", encoding="utf-8") as csvfile:
            spamreader = csv.reader(csvfile, delimiter=",", quotechar="|")
            row_index = -1
            for row in spamreader:
                if row_index >= 0:
                    signals.insert(row_index, int(row[1]))
                row_index += 1

        signals = denoise(signals)

        # signals = stats.zscore(signals)

        # Read annotations: R position and Arrhythmia class
        with annotations[r].open(mode="r", encoding="utf-8") as file_id:
            data = file_id.readlines()
            for d in range(1, len(data)):  # 0 index is Chart Head
                splitted = filter(None, data[d].split(" "))
                next(splitted)  # Skip time
                pos = int(next(splitted))  # R-peak sample ID
                arrhythmia_type = next(splitted)

                # Combine nested `if` statements into a single condition
                if arrhythmia_type in classes and window_size <= pos < (len(signals) - window_size):
                    beat = signals[pos - window_size : pos + window_size]
                    x.append(beat)
                    y.append(arrhythmia_type)

    # Append label to each beat
    for i in range(len(x)):
        x[i] = np.append(x[i], y[i])

    x_train_df = pd.DataFrame(x)  # Lowercase variable name

    # Convert input features and targets to NumPy arrays using `.to_numpy()`
    inputs = x_train_df.iloc[:, :-1].to_numpy()
    inputs = inputs.reshape(inputs.shape[0], inputs.shape[1], 1)
    targets = x_train_df.iloc[:, -1].to_numpy()

    train_combined = pd.DataFrame.from_dict({"ecg_arrhythmia": inputs.tolist(), "target": targets})

    # Split dataset
    train_val_data, test_data = train_test_split(train_combined, test_size=0.20)
    train_data, val_data = train_test_split(train_val_data, test_size=0.20)

    # Compute mean and variance for scaling
    ecg_arrhythmia_array = np.array([item for sublist in train_data["ecg_arrhythmia"] for item in sublist], dtype=float)
    data_mean = torch.tensor(np.mean(ecg_arrhythmia_array), dtype=torch.float32)
    data_scale = torch.tensor(np.var(ecg_arrhythmia_array), dtype=torch.float32)

    # Define the schema for the dataset
    features_schema = Features({
        "ecg_arrhythmia": datasets.Array2D(dtype="float32", shape=(360, 1)),
        "target": datasets.Value(dtype="string"),
    })

    # Convert the dataframes directly into the desired Parquet format using the `datasets` library
    # Train dataset
    train_parquet = datasets.Dataset.from_pandas(train_data, preserve_index=False)
    train_parquet = train_parquet.cast(features_schema)
    train_parquet.to_parquet("train.parquet")

    # Validation dataset
    val_parquet = datasets.Dataset.from_pandas(val_data, preserve_index=False)
    val_parquet = val_parquet.cast(features_schema)
    val_parquet.to_parquet("val.parquet")

    # Test dataset
    test_parquet = datasets.Dataset.from_pandas(test_data, preserve_index=False)
    test_parquet = test_parquet.cast(features_schema)
    test_parquet.to_parquet("test.parquet")

    # 1. Upload dataset and analyze
    client = boto3.client(
        service_name="s3",
        endpoint_url=os.getenv("S3_DATASET_ENDPOINT_URL"),
        aws_access_key_id=os.getenv("S3_DATASET_ACCESS_KEY_ID"),
        aws_secret_access_key=os.getenv("S3_DATASET_SECRET_ACCESS_KEY"),
        config=Config(signature_version="s3v4"),
    )

    client.upload_file("train.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "ecg/train.parquet")
    client.upload_file("val.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "ecg/val.parquet")
    client.upload_file("test.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "ecg/test.parquet")

    # 2. Define and analyze schema
    analyzed_schema = AnalyzedSchema(
        ExplainableFeature(
            name="ecg_arrhythmia",
            is_target=False,
            preprocessor=Scaler((360, 1), mean=data_mean, scale=data_scale),
            feature_type=UnivariateTimeSeriesFeature(
                asynchronous=False,
            ),
        ),
        ExplainableFeature(
            is_target=True,
            name="target",
            preprocessor=SklearnPreprocessor(preprocess_function=OneHotEncoder(sparse_output=False)),
            feature_type=CategoricalFeature(),
        ),
    )

    # 3. Create a train dataset from the analyzed schema and fit it
    analyzed_train_dataset = AnalyzedParquetDataset(
        name="ecg_train_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/ecg/train.parquet",
        storage_options=STORAGE_OPTIONS,
        analyzed_schema=analyzed_schema,
    )
    fit_train_dataset = analyzed_train_dataset.fit()
    fitted_schema = fit_train_dataset.fitted_schema
    print(fitted_schema)

    # 4. Create test and validation datasets based on the fitted schema
    fit_val_dataset = FittedParquetDataset(
        name="ecg_validation_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/ecg/test.parquet",
        storage_options=STORAGE_OPTIONS,
        fitted_schema=fitted_schema,
    )

    fit_test_dataset = FittedParquetDataset(
        name="ecg_test_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/ecg/val.parquet",
        storage_options=STORAGE_OPTIONS,
        fitted_schema=fitted_schema,
    )

    # ##### Prepare the Model #######

    # 1. Set input and target sizes based on the fitted schema
    target_size = fitted_schema.target_size[1]  # The number of classes

    # Initialize models
    # Not as backbone as EcgCNN is not pretrained and the backbone is not trainable with xpdeep.
    feature_extractor = EcgCNN(
        with_softmax=False,
        output_size=128,
    )

    task_learner = MLP(
        norm_layer=None,
        input_size=64,
        hidden_channels=[target_size],
        last_activation=partial(torch.nn.Softmax, dim=-1),
    )

    # 3. Specify the model's structure and constraints
    model_specifications = ModelDecisionGraphParameters(
        graph_depth=3,
        target_homogeneity_pruning_threshold=0.999,
        population_pruning_threshold=0.005,
        prune_step=8,
        target_homogeneity_weight=1.0,
        discrimination_weight=0.1,
        balancing_weight=0.1,
        internal_model_complexity=3,
        feature_extraction_output_type=FeatureExtractionOutputType.VECTOR,
    )

    # 4. Create the explainable model using the xpdeep API
    xpdeep_model = XpdeepModel.from_torch(
        fitted_schema=fit_train_dataset.fitted_schema,
        feature_extraction=feature_extractor,
        task_learner=task_learner,
        backbone=None,
        decision_graph_parameters=model_specifications,
    )

    # ##### Train #######

    # 1. Define metrics and callbacks for training
    metrics = DictMetrics(
        global_multi_class_accuracy=TorchGlobalMetric(
            partial(MulticlassAccuracy, num_classes=target_size, average="micro"), target_as_indexes=True
        ),
        leaf_multi_class_accuracy=TorchLeafMetric(
            partial(MulticlassAccuracy, num_classes=target_size, average="micro"), target_as_indexes=True
        ),
        global_multi_class_F1_score=TorchGlobalMetric(
            partial(MulticlassF1Score, num_classes=target_size, average="macro"), target_as_indexes=True
        ),
        leaf_multi_class_F1_score=TorchLeafMetric(
            partial(MulticlassF1Score, num_classes=target_size, average="macro"), target_as_indexes=True
        ),
        global_confusion_matrix=TorchGlobalMetric(
            partial(MulticlassConfusionMatrix, normalize="all", num_classes=target_size), target_as_indexes=True
        ),
        leaf_confusion_matrix=TorchLeafMetric(
            partial(MulticlassConfusionMatrix, normalize="all", num_classes=target_size), target_as_indexes=True
        ),
    )

    callbacks = [
        EarlyStopping(monitoring_metric="Total loss", mode="minimize", patience=5),
        Scheduler(
            pre_scheduler=partial(ReduceLROnPlateau, mode="min", patience=3),
            step_method="epoch",
            monitoring_metric="Total loss",
        ),
        ModelCheckpoint(monitoring_metric="global_multi_class_F1_score", mode="minimize"),
    ]

    # 2. Define optimizer
    optimizer = partial(torch.optim.AdamW, lr=0.001, foreach=False, fused=False)

    # 3. Create and run the trainer
    trainer = Trainer(
        loss=CrossEntropyLossFromProbabilities(reduction="none"),
        optimizer=optimizer,
        callbacks=callbacks,
        start_epoch=0,
        max_epochs=10,
        metrics=metrics,
    )

    trained_model = trainer.train(
        model=xpdeep_model,
        train_set=fit_train_dataset,
        validation_set=fit_val_dataset,
        batch_size=128,
    )

    # ##### Explain #######

    # 1. Build the Explainer
    statistics = DictStats(
        distribution_target=DistributionStat(on="target"), distribution_prediction=DistributionStat(on="prediction")
    )
    quality_metrics = [Sensitivity(), Infidelity()]

    explainer = Explainer(
        description_representativeness=1000, quality_metrics=quality_metrics, metrics=metrics, statistics=statistics
    )

    # 2. Generate global model explanations
    model_explanations = explainer.global_explain(
        trained_model,
        train_set=fit_train_dataset,
        validation_set=fit_val_dataset,
        test_set=fit_test_dataset,
    )
    print(model_explanations.visualisation_link)


if __name__ == "__main__":
    init(api_key=os.getenv("API_KEY"), api_url=os.getenv("API_URL"))
    set_project(Project.create_or_get(name="ECG Arrhythmia Tutorial"))

    try:
        main()
    finally:
        get_project().delete()

Gas Price dataset#

In this section, we detail the pytorch code for designing an explainable deep model for processing the Gas Price dataset.

Gas Price is a dataset for forecasting with time-series inputs.

Please download the dataset here and update the tutorial data path accordingly. Natural gas account for 1/4 of the global demand and roughly 1/3 of the US energy demand. After oil, Natural gas is the most dominate sort of energy. So, being about to improve natural gas demand prediction is extremely valuable. The dataset is designed for the task of forecasting natural gas prices.

👀 Full file preview
"""Gas Price dataset workflow, forecasting with time series data."""

import os
from functools import partial

import boto3
import datasets
import numpy as np
import pandas as pd
import torch
from botocore.client import Config
from datasets import Features
from sklearn.model_selection import train_test_split
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torchmetrics import MeanAbsoluteError, MeanSquaredError

from xpdeep import Project, get_project, init, set_project
from xpdeep.dataset.feature import ExplainableFeature
from xpdeep.dataset.feature.feature_types import UnivariateTimeSeriesFeature
from xpdeep.dataset.parquet_dataset import FittedParquetDataset
from xpdeep.dataset.preprocessor.zoo.doc import Scaler
from xpdeep.dataset.schema import FittedSchema
from xpdeep.explain.explainer import Explainer
from xpdeep.explain.quality_metrics import Infidelity, Sensitivity
from xpdeep.explain.statistic import DictStats, HistogramStat, VarianceStat
from xpdeep.metric import DictMetrics, TorchGlobalMetric, TorchLeafMetric
from xpdeep.model.feature_extraction_output_type import FeatureExtractionOutputType
from xpdeep.model.model_parameters import ModelDecisionGraphParameters
from xpdeep.model.xpdeep_model import XpdeepModel
from xpdeep.model.zoo.doc import GazPriceMLP
from xpdeep.model.zoo.mlp import MLP
from xpdeep.trainer.callbacks import EarlyStopping, Scheduler
from xpdeep.trainer.trainer import Trainer

STORAGE_OPTIONS = {
    "key": os.getenv("S3_DATASET_ACCESS_KEY_ID"),
    "secret": os.getenv("S3_DATASET_SECRET_ACCESS_KEY"),
    "client_kwargs": {
        "endpoint_url": os.getenv("S3_DATASET_ENDPOINT_URL"),
    },
    "s3_additional_kwargs": {"addressing_style": "path"},
}


def main():
    """Process the dataset, train, and explain the model."""
    torch.random.manual_seed(42)

    # ##### Prepare the Dataset #######

    # 1. Load and preprocess data
    data = pd.read_csv("gas_price/daily_csv.csv")
    data = data.set_index("Date")
    data["Price"].fillna(data["Price"].mean(), inplace=True)  # noqa: PD002

    # Create the samples for time series prediction
    lookback = 24
    horizon = 5
    num_samples = len(data) - lookback - horizon + 1

    data_input_numpy = data.to_numpy()
    data_target_numpy = data[["Price"]].to_numpy()

    # Generate tensor slices for inputs and targets
    tensor_slices = torch.arange(lookback + horizon).unsqueeze(0) + torch.arange(num_samples).unsqueeze(1)
    input_slices = tensor_slices[:, :lookback]
    target_slices = tensor_slices[:, lookback:]

    transformed_inputs = torch.gather(
        torch.from_numpy(data_input_numpy).unsqueeze(0).repeat(num_samples, 1, 1), 1, input_slices.unsqueeze(-1)
    ).numpy()
    transformed_targets = torch.gather(
        torch.from_numpy(data_target_numpy).unsqueeze(0).repeat(num_samples, 1, 1), 1, target_slices.unsqueeze(-1)
    ).numpy()

    # Store the processed data in a DataFrame
    data = pd.DataFrame({"Price": transformed_inputs.tolist(), "target Price": transformed_targets.tolist()})

    # Split dataset into training and test sets and add indices for xpdeep
    train_data, test_data = train_test_split(data, test_size=0.2, random_state=42)
    # Calculate mean and scale for the target (Price)
    price_array = np.array([item for sublist in train_data["Price"] for item in sublist], dtype=float)
    # Now you can compute the mean and variance
    data_mean = torch.tensor(np.mean(price_array), dtype=torch.float32)
    data_scale = torch.tensor(np.var(price_array), dtype=torch.float32)

    target_price_array = np.array([item for sublist in train_data["target Price"] for item in sublist], dtype=float)
    # Now you can compute the mean and variance
    target_mean = torch.tensor(np.mean(target_price_array), dtype=torch.float32)
    target_scale = torch.tensor(np.var(target_price_array), dtype=torch.float32)

    # Save directly to Parquet format with schema using `datasets`
    features_schema = Features({
        "Price": datasets.Array2D(dtype="float32", shape=(24, 1)),
        "target Price": datasets.Array2D(dtype="float32", shape=(5, 1)),
    })

    # Create datasets and save as Parquet
    train_parquet = datasets.Dataset.from_pandas(train_data, preserve_index=False).cast(features_schema)
    train_parquet.to_parquet("train.parquet")

    test_parquet = datasets.Dataset.from_pandas(test_data, preserve_index=False).cast(features_schema)
    test_parquet.to_parquet("test.parquet")

    # Use the test dataset as validation set
    val_parquet = test_parquet
    val_parquet.to_parquet("val.parquet")

    # 2. Upload datasets and create the schema
    client = boto3.client(
        service_name="s3",
        endpoint_url=os.getenv("S3_DATASET_ENDPOINT_URL"),
        aws_access_key_id=os.getenv("S3_DATASET_ACCESS_KEY_ID"),
        aws_secret_access_key=os.getenv("S3_DATASET_SECRET_ACCESS_KEY"),
        config=Config(signature_version="s3v4"),
    )

    client.upload_file("train.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "gas_price/train.parquet")
    client.upload_file("val.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "gas_price/val.parquet")
    client.upload_file("test.parquet", os.getenv("S3_DATASET_BUCKET_NAME"), "gas_price/test.parquet")

    fitted_schema = FittedSchema(
        ExplainableFeature(
            name="Price",
            preprocessor=Scaler((24, 1), mean=data_mean, scale=data_scale),
            feature_type=UnivariateTimeSeriesFeature(
                asynchronous=True,
            ),
        ),
        ExplainableFeature(
            name="target Price",
            is_target=True,
            preprocessor=Scaler((5, 1), mean=target_mean, scale=target_scale),
            feature_type=UnivariateTimeSeriesFeature(
                asynchronous=True,
                channel="Price",
            ),
        ),
        input_size=(1, 24, 1),
        target_size=(1, 5, 1),
    )

    # Create train, test, and validation datasets using the fitted schema
    fit_train_dataset = FittedParquetDataset(
        name="gas_price_train_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/gas_price/train.parquet",
        storage_options=STORAGE_OPTIONS,
        fitted_schema=fitted_schema,
    )

    fit_test_dataset = FittedParquetDataset(
        name="gas_price_test_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/gas_price/test.parquet",
        storage_options=STORAGE_OPTIONS,
        fitted_schema=fitted_schema,
    )

    fit_val_dataset = FittedParquetDataset(
        name="gas_price_validation_set",
        path=f"s3://{os.getenv('S3_DATASET_BUCKET_NAME')}/gas_price/val.parquet",
        storage_options=STORAGE_OPTIONS,
        fitted_schema=fitted_schema,
    )

    # ##### Prepare the Model #######

    # 1. Set input and target sizes based on the fitted schema

    # 2. Initialize models for feature extraction and task learning
    # Initialize models
    feature_extractor = MLP(
        norm_layer=partial(torch.nn.BatchNorm1d, track_running_stats=False),
        flatten_input=True,
        dropout=0.2,
        input_size=24 * 1,
        hidden_channels=[128, 64, 32],
    )
    task_learner = GazPriceMLP(input_size=32, hidden_channels=[5])

    # 3. Specify the model's structure and constraints
    model_specifications = ModelDecisionGraphParameters(
        graph_depth=3,
        discrimination_weight=0.1,
        target_homogeneity_weight=0.8,
        prune_step=15,
        target_homogeneity_pruning_threshold=0.99,
        population_pruning_threshold=0.05,
        balancing_weight=0.5,
        internal_model_complexity=4,
        feature_extraction_output_type=FeatureExtractionOutputType.VECTOR,
    )

    # 4. Create the explainable model using the xpdeep API
    xpdeep_model = XpdeepModel.from_torch(
        fitted_schema=fit_train_dataset.fitted_schema,
        feature_extraction=feature_extractor,
        task_learner=task_learner,
        backbone=None,
        decision_graph_parameters=model_specifications,
    )

    # ##### Train #######

    # 1. Define metrics and callbacks for training
    metrics = DictMetrics(
        mse=TorchGlobalMetric(metric=partial(MeanSquaredError), on_raw_data=True),
        mae=TorchGlobalMetric(metric=partial(MeanAbsoluteError), on_raw_data=True),
        leaf_metric_mse=TorchLeafMetric(metric=partial(MeanSquaredError), on_raw_data=True),
    )

    callbacks = [
        EarlyStopping(monitoring_metric="mse", mode="minimize", patience=10),
        Scheduler(
            pre_scheduler=partial(ReduceLROnPlateau, patience=5, mode="min"),
            step_method="epoch",
            monitoring_metric="Total loss",
        ),
    ]

    # 2. Define optimizer
    optimizer = partial(torch.optim.AdamW, lr=0.01, foreach=False, fused=False)

    # 3. Create and run the trainer
    trainer = Trainer(
        loss=torch.nn.MSELoss(reduction="none"),
        optimizer=optimizer,
        callbacks=callbacks,
        start_epoch=0,
        max_epochs=60,
        metrics=metrics,
    )

    trained_model = trainer.train(
        model=xpdeep_model,
        train_set=fit_train_dataset,
        validation_set=fit_val_dataset,
        batch_size=2048,
    )

    # ##### Explain #######

    # 1. Build the Explainer
    statistics = DictStats(
        histogram_target=HistogramStat(on="target", num_bins=20, num_items=1000, on_raw_data=True),
        histogram_prediction=HistogramStat(on="prediction", num_bins=20, num_items=1000, on_raw_data=True),
        histogram_error=HistogramStat(on="prediction_error", num_bins=20, num_items=1000, on_raw_data=True),
        variance_target=VarianceStat(on="target", on_raw_data=True),
        variance_prediction=VarianceStat(on="prediction", on_raw_data=True),
        distribution_input=HistogramStat(
            on="input", num_bins=20, num_items=1000, feature_name="Price", on_raw_data=True
        ),
    )
    # Here we add a per-time stamp mse (num_outputs is the number of timestamps)
    leaf_metric_per_timestamp_mse = TorchLeafMetric(
        metric=partial(MeanSquaredError, num_outputs=5), on_raw_data=True, reduced_dimensions=[0, 2]
    )
    metrics.update({"leaf_metric_per_timestamp_mse": leaf_metric_per_timestamp_mse})

    quality_metrics = [Sensitivity(), Infidelity()]

    explainer = Explainer(
        description_representativeness=1000, quality_metrics=quality_metrics, metrics=metrics, statistics=statistics
    )

    # 2. Generate global model explanations
    model_explanations = explainer.global_explain(
        trained_model,
        train_set=fit_train_dataset,
        test_set=fit_test_dataset,
        validation_set=fit_val_dataset,
    )
    print(model_explanations.visualisation_link)


if __name__ == "__main__":
    init(api_key=os.getenv("API_KEY"), api_url=os.getenv("API_URL"))
    set_project(Project.create_or_get(name="Gas Price Tutorial"))

    try:
        main()
    finally:
        get_project().delete()