Repository: vanderschaarlab/temporai (language: python)

Issue:
[Docs] Add doscstrings for all the important classes



Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5a8bff44..aedcfb17 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,6 +6,7 @@ repos:
   hooks:
   - id: trailing-whitespace
     args: ['--markdown-linebreak-ext=md']
+    exclude: 'docs/user_guide'  # Auto-generated, leave as is.
   - id: check-added-large-files
     args: ['--maxkb=1000']
   - id: check-ast
@@ -18,6 +19,7 @@ repos:
   - id: check-executables-have-shebangs
   - id: debug-statements
   - id: end-of-file-fixer
+    exclude: 'docs/user_guide'  # Auto-generated, leave as is.
   - id: pretty-format-json
     args: ['--autofix', '--indent=4', '--no-sort-keys']
   - id: requirements-txt-fixer
@@ -45,6 +47,7 @@ repos:
   hooks:
   - id: blacken-docs
     additional_dependencies: [black]
+    exclude: 'docs/user_guide'  # Auto-generated, leave as is.
 
 - repo: https://github.com/PyCQA/flake8
   rev: 6.0.0
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 6940f655..17074a6c 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -15,8 +15,8 @@ sphinx:
   configuration: docs/conf.py
 
 # Optionally build your docs in additional formats such as PDF
-# formats:
-#   - pdf
+formats:
+  - pdf
 
 python:
   install:
diff --git a/AUTHORS.md b/AUTHORS.md
index bf3d0155..5e24d0e0 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,3 +1,4 @@
 # Contributors
 
 * Evgeny Saveliev [e.s.saveliev@gmail.com](mailto:e.s.saveliev@gmail.com)
+* Bogdan Cebere [bogdan.cebere@gmail.com](mailto:bogdan.cebere@gmail.com)
diff --git a/README.md b/README.md
index 4d11c031..f74bb012 100644
--- a/README.md
+++ b/README.md
@@ -25,27 +25,42 @@
 
 # <img src="docs/assets/TemporAI_Logo_Icon.png" height=25> TemporAI
 
+<!-- exclude_docs -->
 > **⚗️ Status:** This project is still in *alpha*, and the API may change without warning.  
+<!-- exclude_docs_end -->
+<!-- include_docs
+:::{important}
+**Status:** This project is still in *alpha*, and the API may change without warning.  
+:::
+include_docs_end -->
 
-*TemporAI* is a Machine Learning-centric time-series library for medicine.  The tasks that are currently of focus in TemporAI are: time-series prediction, time-to-event (a.k.a. survival) analysis with time-series data, and counterfactual inference (i.e. \[individualized\] treatment effects).
 
-*TemporAI* provides data preprocessing methods (including scaling and imputation methods for static and temporal covariates). AutoML tools for hyperparameter tuning and pipeline selection are available.
+## 📃 Overview
 
-In future versions, the library also aims to provide the user with understanding of their data, model, and problem, through e.g. integration with interpretability methods.
+*TemporAI* is a Machine Learning-centric time-series library for medicine.  The tasks that are currently of focus in TemporAI are: time-to-event (survival) analysis with time-series data, treatment effects (causal inference) over time, and time-series prediction. Data preprocessing methods, including missing value imputation for static and temporal covariates, are provided. AutoML tools for hyperparameter tuning and pipeline selection are also available.
 
-Key concepts:
+### How is TemporAI unique?
+
+* **🏥 Medicine-first:** Focused on use cases for medicine and healthcare, such as temporal treatment effects, survival analysis over time, imputation methods, models with built-in and post-hoc interpretability, ... See [methods](./#-methods).
+* **🏗️ Fast prototyping:** A plugin design allowing for on-the-fly integration of new methods by the users.
+* **🚀 From research to practice:** Relevant novel models from research community adapted for practical use.
+* **🌍 A healthcare ecosystem vision:** A range of interactive demonstration apps, new medical problem settings, interpretability tools, data-centric tools etc. are planned.
+
+### Key concepts
 
 <div align="center">
 
 <!-- exclude_docs -->
-<img src="docs/assets/Conceptual.png" width="650" alt="key concepts">
+<img src="docs/assets/Conceptual.png" alt="key concepts">
 <!-- exclude_docs_end -->
 <!-- include_docs
-<img src="docs/assets/Conceptual.png" width="750" alt="key concepts">
+<img src="docs/assets/Conceptual.png" alt="key concepts">
 include_docs_end -->
 
 </div>
 
+
+
 ## 🚀 Installation
 
 ```bash
@@ -56,6 +71,8 @@ or from source, using
 $ pip install .
 ```
 
+
+
 ## 💥 Sample Usage
 * List the available plugins
 ```python
@@ -64,7 +81,63 @@ from tempor.plugins import plugin_loader
 print(plugin_loader.list())
 ```
 
-* Use an imputer
+* Use a time-to-event (survival) analysis model
+```python
+from tempor.utils.dataloaders import PBCDataLoader
+from tempor.plugins import plugin_loader
+
+# Load a time-to-event dataset:
+dataset = PBCDataLoader().load()
+
+# Initialize the model:
+model = plugin_loader.get("time_to_event.dynamic_deephit")
+
+# Train:
+model.fit(dataset)
+
+# Make risk predictions:
+prediction = model.predict(dataset, horizons=[0.25, 0.50, 0.75])
+```
+
+* Use a temporal treatment effects model
+```python
+import numpy as np
+
+from tempor.utils.dataloaders import DummyTemporalTreatmentEffectsDataLoader
+from tempor.plugins import plugin_loader
+
+# Load a dataset with temporal treatments and outcomes:
+dataset = DummyTemporalTreatmentEffectsDataLoader(
+    temporal_covariates_missing_prob=0.0,
+    temporal_treatments_n_features=1,
+    temporal_treatments_n_categories=2,
+).load()
+
+# Initialize the model:
+model = plugin_loader.get("treatments.temporal.regression.crn_regressor", epochs=20)
+
+# Train:
+model.fit(dataset)
+
+# Define target variable horizons for each sample:
+horizons = [
+    tc.time_indexes()[0][len(tc.time_indexes()[0]) // 2 :] for tc in dataset.time_series
+]
+
+# Define treatment scenarios for each sample:
+treatment_scenarios = [
+    [np.asarray([1] * len(h)), np.asarray([0] * len(h))] for h in horizons
+]
+
+# Predict counterfactuals:
+counterfactuals = model.predict_counterfactuals(
+    dataset,
+    horizons=horizons,
+    treatment_scenarios=treatment_scenarios,
+)
+```
+
+* Use a missing data imputer
 ```python
 from tempor.utils.dataloaders import SineDataLoader
 from tempor.plugins import plugin_loader
@@ -77,7 +150,7 @@ print(static_data_n_missing, temporal_data_n_missing)
 assert static_data_n_missing > 0
 assert temporal_data_n_missing > 0
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("preprocessing.imputation.temporal.bfill")
 
 # Train:
@@ -91,14 +164,14 @@ print(static_data_n_missing, temporal_data_n_missing)
 assert temporal_data_n_missing == 0
 ```
 
-* Use a classifier
+* Use a one-off classifier (prediction)
 ```python
 from tempor.utils.dataloaders import SineDataLoader
 from tempor.plugins import plugin_loader
 
 dataset = SineDataLoader().load()
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
 
 # Train:
@@ -108,54 +181,59 @@ model.fit(dataset)
 prediction = model.predict(dataset)
 ```
 
-* Use a regressor
+* Use a temporal regressor (forecasting)
 ```python
-from tempor.utils.dataloaders import SineDataLoader
+from tempor.utils.dataloaders import DummyTemporalPredictionDataLoader
 from tempor.plugins import plugin_loader
 
-dataset = SineDataLoader().load()
+# Load a dataset with temporal targets.
+dataset = DummyTemporalPredictionDataLoader(temporal_covariates_missing_prob=0.0).load()
 
-# Load the model:
-model = plugin_loader.get("prediction.one_off.regression.nn_regressor", n_iter=50)
+# Initialize the model:
+model = plugin_loader.get("prediction.temporal.regression.seq2seq_regressor", epochs=10)
 
 # Train:
 model.fit(dataset)
 
 # Predict:
-prediction = model.predict(dataset)
+prediction = model.predict(dataset, n_future_steps=5)
 ```
 
-* Benchmark models
-Classification task
+* Benchmark models, time-to-event task
 ```python
 from tempor.benchmarks import benchmark_models
 from tempor.plugins import plugin_loader
 from tempor.plugins.pipeline import pipeline
-from tempor.utils.dataloaders import SineDataLoader
+from tempor.utils.dataloaders import PBCDataLoader
 
 testcases = [
     (
         "pipeline1",
         pipeline(
             [
-                "preprocessing.scaling.static.static_minmax_scaler",
-                "prediction.one_off.classification.nn_classifier",
+                "preprocessing.scaling.temporal.ts_minmax_scaler",
+                "time_to_event.dynamic_deephit",
             ]
-        )({"nn_classifier": {"n_iter": 10}}),
+        )({"ts_coxph": {"n_iter": 100}}),
     ),
     (
         "plugin1",
-        plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=10),
+        plugin_loader.get("time_to_event.dynamic_deephit", n_iter=100),
+    ),
+    (
+        "plugin2",
+        plugin_loader.get("time_to_event.ts_coxph", n_iter=100),
     ),
 ]
-dataset = SineDataLoader().load()
+dataset = PBCDataLoader().load()
 
 aggr_score, per_test_score = benchmark_models(
-    task_type="prediction.one_off.classification",
+    task_type="time_to_event",
     tests=testcases,
     data=dataset,
     n_splits=2,
     random_state=0,
+    horizons=[2.0, 4.0, 6.0],
 )
 
 print(aggr_score)
@@ -166,7 +244,7 @@ print(aggr_score)
 from tempor.utils.serialization import load, save
 from tempor.plugins import plugin_loader
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
 
 buff = save(model)  # Save model to bytes.
@@ -175,15 +253,15 @@ reloaded = load(buff)  # Reload model.
 # `save_to_file`, `load_from_file` also available in the serialization module.
 ```
 
-* AutoML
+* AutoML - search for the best pipeline for your task
 ```python
 from tempor.automl.seeker import PipelineSeeker
 from tempor.utils.dataloaders import SineDataLoader
 
 dataset = SineDataLoader().load()
 
-# Specify the AutoML pipeline seeker for the task of your choice, providing candidate methods, metric,
-# preprocessing steps etc.
+# Specify the AutoML pipeline seeker for the task of your choice, providing candidate methods,
+# metric, preprocessing steps etc.
 seeker = PipelineSeeker(
     study_name="my_automl_study",
     task_type="prediction.one_off.classification",
@@ -207,51 +285,14 @@ seeker = PipelineSeeker(
 best_pipelines, best_scores = seeker.search()  # doctest: +SKIP
 ```
 
-## 🔑 Methods
-
-
-
-### Prediction
-
-#### One-off
-Prediction where targets are static.
-
-* Classification (category: `prediction.one_off.classification`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `nn_classifier` | Neural-net based classifier. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
-| `ode_classifier` | Classifier based on ordinary differential equation (ODE) solvers.  | --- |
-| `cde_classifier` | Classifier based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926) |
-| `laplace_ode_classifier` | Classifier based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
-
-* Regression (category: `prediction.one_off.regression`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `nn_regressor` | Neural-net based regressor. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
-| `ode_regressor` | Regressor based on ordinary differential equation (ODE) solvers.  | --- |
-| `cde_regressor` | Regressor based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926)
-| `laplace_ode_regressor` | Regressor based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
-
-#### Temporal
-Prediction where targets are temporal (time series).
-
-* Classification (category: `prediction.temporal.classification`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `seq2seq_classifier` | Seq2Seq prediction, classification | --- |
-
-* Regression (category: `prediction.temporal.regression`)
 
-| Name | Description| Reference |
-| --- | --- | --- |
-| `seq2seq_regressor` | Seq2Seq prediction, regression | --- |
+<!-- include_docs
+{#methods}include_docs_end -->
+## 🔑 Methods
 
 
 
-### Time-to-Event
+### Time-to-Event (survival) analysis over time
 
 Risk estimation given event data (category: `time_to_event`)
 
@@ -295,6 +336,46 @@ Treatment effects estimation where treatments are temporal (time series).
 
 
 
+### Prediction
+
+#### One-off
+Prediction where targets are static.
+
+* Classification (category: `prediction.one_off.classification`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `nn_classifier` | Neural-net based classifier. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
+| `ode_classifier` | Classifier based on ordinary differential equation (ODE) solvers.  | --- |
+| `cde_classifier` | Classifier based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926) |
+| `laplace_ode_classifier` | Classifier based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
+
+* Regression (category: `prediction.one_off.regression`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `nn_regressor` | Neural-net based regressor. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
+| `ode_regressor` | Regressor based on ordinary differential equation (ODE) solvers.  | --- |
+| `cde_regressor` | Regressor based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926)
+| `laplace_ode_regressor` | Regressor based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
+
+#### Temporal
+Prediction where targets are temporal (time series).
+
+* Classification (category: `prediction.temporal.classification`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `seq2seq_classifier` | Seq2Seq prediction, classification | --- |
+
+* Regression (category: `prediction.temporal.regression`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `seq2seq_regressor` | Seq2Seq prediction, regression | --- |
+
+
+
 ### Preprocessing
 
 #### Imputation
@@ -332,7 +413,7 @@ Treatment effects estimation where treatments are temporal (time series).
 
 
 
-## Tutorials
+## 📖 Tutorials
 
 ### Data
 
@@ -387,8 +468,9 @@ pytest -vsx
 For development and contribution to TemporAI, see:
 * 📓 [Extending TemporAI tutorials](./tutorials/extending/)
 * 📃 [Contribution guide](./CONTRIBUTING.md)
+* 👩‍💻 [Developer's guide](./docs/dev_guide.md)
 
-## Citing
+## ✍️ Citing
 
 If you use this code, please cite the associated paper:
 ```
diff --git a/docs/assets/Conceptual.png b/docs/assets/Conceptual.png
index 30fd69e6..4050ce52 100644
Binary files a/docs/assets/Conceptual.png and b/docs/assets/Conceptual.png differ
diff --git a/docs/assets/TemporAI_Logo_Icon.ico b/docs/assets/TemporAI_Logo_Icon.ico
new file mode 100644
index 00000000..9a91bd7e
Binary files /dev/null and b/docs/assets/TemporAI_Logo_Icon.ico differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.core._base_estimator.BaseEstimator.png b/docs/assets/class_diagrams/tempor.plugins.core._base_estimator.BaseEstimator.png
new file mode 100644
index 00000000..1a13e5dd
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.core._base_estimator.BaseEstimator.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.core._base_predictor.BasePredictor.png b/docs/assets/class_diagrams/tempor.plugins.core._base_predictor.BasePredictor.png
new file mode 100644
index 00000000..1defd4b0
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.core._base_predictor.BasePredictor.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.core._base_transformer.BaseTransformer.png b/docs/assets/class_diagrams/tempor.plugins.core._base_transformer.BaseTransformer.png
new file mode 100644
index 00000000..6fbff98e
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.core._base_transformer.BaseTransformer.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier.png b/docs/assets/class_diagrams/tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier.png
new file mode 100644
index 00000000..7fc208bf
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.prediction.one_off.regression.BaseOneOffRegressor.png b/docs/assets/class_diagrams/tempor.plugins.prediction.one_off.regression.BaseOneOffRegressor.png
new file mode 100644
index 00000000..79fe0441
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.prediction.one_off.regression.BaseOneOffRegressor.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.prediction.temporal.classification.BaseTemporalClassifier.png b/docs/assets/class_diagrams/tempor.plugins.prediction.temporal.classification.BaseTemporalClassifier.png
new file mode 100644
index 00000000..0d2a2c02
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.prediction.temporal.classification.BaseTemporalClassifier.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.prediction.temporal.regression.BaseTemporalRegressor.png b/docs/assets/class_diagrams/tempor.plugins.prediction.temporal.regression.BaseTemporalRegressor.png
new file mode 100644
index 00000000..38abab7a
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.prediction.temporal.regression.BaseTemporalRegressor.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.preprocessing.imputation._base.BaseImputer.png b/docs/assets/class_diagrams/tempor.plugins.preprocessing.imputation._base.BaseImputer.png
new file mode 100644
index 00000000..517a3ed2
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.preprocessing.imputation._base.BaseImputer.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.preprocessing.scaling._base.BaseScaler.png b/docs/assets/class_diagrams/tempor.plugins.preprocessing.scaling._base.BaseScaler.png
new file mode 100644
index 00000000..44f040a2
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.preprocessing.scaling._base.BaseScaler.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.time_to_event.BaseTimeToEventAnalysis.png b/docs/assets/class_diagrams/tempor.plugins.time_to_event.BaseTimeToEventAnalysis.png
new file mode 100644
index 00000000..2ff0d686
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.time_to_event.BaseTimeToEventAnalysis.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.treatments.one_off._base.BaseOneOffTreatmentEffects.png b/docs/assets/class_diagrams/tempor.plugins.treatments.one_off._base.BaseOneOffTreatmentEffects.png
new file mode 100644
index 00000000..134bcb92
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.treatments.one_off._base.BaseOneOffTreatmentEffects.png differ
diff --git a/docs/assets/class_diagrams/tempor.plugins.treatments.temporal._base.BaseTemporalTreatmentEffects.png b/docs/assets/class_diagrams/tempor.plugins.treatments.temporal._base.BaseTemporalTreatmentEffects.png
new file mode 100644
index 00000000..449e3117
Binary files /dev/null and b/docs/assets/class_diagrams/tempor.plugins.treatments.temporal._base.BaseTemporalTreatmentEffects.png differ
diff --git a/docs/conf.py b/docs/conf.py
index c835650d..94bc4e60 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -10,6 +10,7 @@
 import os
 import shutil
 import sys
+import time
 
 # -- Path setup --------------------------------------------------------------
 
@@ -43,7 +44,7 @@
 try:
     import sphinx
 
-    cmd_line = f"sphinx-apidoc --implicit-namespaces -f -o {output_dir} {module_dir}"
+    cmd_line = f"sphinx-apidoc --implicit-namespaces -e -f -o {output_dir} {module_dir}"
 
     args = cmd_line.split(" ")
     if tuple(sphinx.__version__.split(".")) >= ("1", "7"):
@@ -93,8 +94,25 @@
     "smartquotes",
     "substitution",
     "tasklist",
+    "attrs_inline",
+    "attrs_block",
 ]
 
+# MyST URL schemes.
+myst_url_schemes = {
+    "http": None,
+    "https": None,
+    "ftp": None,
+    "mailto": None,
+    "repo-code": "https://github.com/vanderschaarlab/temporai/tree/main/{{path}}#{{fragment}}",
+    # "doi": "https://doi.org/{{path}}",
+    # "gh-issue": {
+    #     "url": "https://github.com/executablebooks/MyST-Parser/issue/{{path}}#{{fragment}}",
+    #     "title": "Issue #{{path}}",
+    #     "classes": ["github"],
+    # },
+}
+
 # The suffix of source filenames.
 source_suffix = [".rst", ".md"]
 
@@ -106,7 +124,7 @@
 
 # General information about the project.
 project = "TemporAI"
-copyright = "2022, van der Schaar Lab"  # pylint: disable=redefined-builtin
+copyright = f"{time.strftime('%Y')}, van der Schaar Lab"  # pylint: disable=redefined-builtin
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -155,7 +173,8 @@
 # show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = "sphinx"
+# https://pygments.org/styles/
+pygments_style = "tango"
 
 # A list of ignored prefixes for module index sorting.
 # modindex_common_prefix = []
@@ -190,6 +209,8 @@
 html_theme_options = {
     # Set the name of the project to appear in the navigation.
     "nav_title": "TemporAI",
+    # Icon,
+    "logo_icon": "timelapse",  # TODO: Remove once the actual logo is fixed.
     # Set you GA account ID to enable tracking
     # "google_analytics_account": "UA-XXXXX",
     #
@@ -205,7 +226,7 @@
     "repo_url": "https://github.com/vanderschaarlab/temporai",
     "repo_name": "temporai",
     # Visible levels of the global TOC; -1 means unlimited
-    "globaltoc_depth": 2,
+    "globaltoc_depth": 3,
     # If False, expand all TOC entries
     "globaltoc_collapse": True,
     # If True, show hidden TOC entries
@@ -224,12 +245,12 @@
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-html_logo = "assets/TemporAI_Logo.svg"
+# html_logo = "assets/TemporAI_Logo.svg"
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-# html_favicon = None
+html_favicon = "assets/TemporAI_Logo_Icon.ico"
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
diff --git a/docs/dev_guide.md b/docs/dev_guide.md
new file mode 100644
index 00000000..87d630ae
--- /dev/null
+++ b/docs/dev_guide.md
@@ -0,0 +1,252 @@
+# Developer's Guide
+
+If you are looking to contribute to *TemporAI*, this guide is a good starting point.
+
+
+
+## Contributing
+
+✅ Please read the [Contributing Guide](contributing.md) first.
+
+
+## Project layout
+
+### Project root
+<!--
+.github/: GitHub workflows (CI/CD)
+docs/: Sphinx documentation content
+src/: Source code
+tests/: Tests (pytest)
+tutorials/: Jupyter notebook tutorials
+<root files>: Configuration files, README, and LICENSE
+-->
+```
+.
+├── .github/: GitHub workflows (CI/CD)
+├── docs/: Sphinx documentation content
+├── src/: Source code
+├── tests/: Tests (pytest)
+├── tutorials/: Jupyter notebook tutorials
+└── <root files>: Configuration files, README, and LICENSE
+```
+
+### Source code
+The source code is located under [`src/tempor`](repo-code:src/tempor), the code organization is shown below.
+<!--
+Use: https://tree.nathanfriend.io/
+tempor
+  automl: AutoML tools
+  benchmarks: Benchmarking tools
+  config: Library config
+  core: Core code, such as global utils
+  data: format
+  exc: Exceptions
+  log: Logger
+  models: Model components
+  plugins: Plugins
+    core: The core plugin mechanism, base classes
+    pipeline: Pipeline mechanism
+    prediction: Prediction plugins
+    preprocessing: Preprocessing plugins
+    time_to_event: Time-to-event plugins
+    treatments: Treatment effects plugins
+  utils: Utilities
+    dataloaders: Data loaders for provided datasets
+    serialization: Serialization tools
+-->
+```
+.
+└── tempor/
+    ├── automl: AutoML tools
+    ├── benchmarks: Benchmarking tools
+    ├── config: Library config
+    ├── core: Core code, such as global utils
+    ├── data: format
+    ├── exc: Exceptions
+    ├── log: Logger
+    ├── models: Model components
+    ├── plugins: Plugins/
+    │   ├── core: The core plugin mechanism, base classes
+    │   ├── pipeline: Pipeline mechanism
+    │   ├── prediction: Prediction plugins
+    │   ├── preprocessing: Preprocessing plugins
+    │   ├── time_to_event: Time-to-event plugins
+    │   └── treatments: Treatment effects plugins
+    └── utils: Utilities/
+        ├── dataloaders: Data loaders for provided datasets
+        └── serialization: Serialization tools
+```
+
+### Tests
+
+Tests located in the [`tests`](repo-code:tests) directory follow the directory structure of the source code.
+
+### Config files
+The project contains the following config files:
+* [`.coveragerc`](repo-code:.coveragerc): [coverage](https://coverage.readthedocs.io) config.
+* [`.isort.cfg`](repo-code:.isort.cfg): [isort](https://pycqa.github.io/isort/) config.
+* [`.pre-commit-config.yaml`](repo-code:.pre-commit-config.yaml): [pre-commit](https://pre-commit.com/) config.
+* [`.readthedocs.yml`](repo-code:.readthedocs.yml): [ReadTheDocs](https://docs.readthedocs.io/) config.
+* [`codecov.yml`](repo-code:codecov.yml): [codecov](https://docs.codecov.com/docs) config.
+* [`mypy.ini`](repo-code:mypy.ini): [mypy](https://mypy.readthedocs.io/) config.
+* [`pyproject.toml`](repo-code:pyproject.toml): miscellaneous setup and tools configuration in the [pyproject.toml](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/) form.
+* [`pytest.ini`](repo-code:pytest.ini): [pytest](https://docs.pytest.org/) config.
+* [`setup.cfg`](repo-code:setup.cfg): miscellaneous setup and tools configuration in the setuptools [setup.cfg](https://setuptools.pypa.io) config.
+* [`tox.ini`](repo-code:tox.ini): [tox](https://tox.wiki/) config.
+
+
+
+## Data format
+
+Please familiarize yourself with the data format we use.
+
+1. Go through the [data tutorials](user_guide/data/index.md).
+2. See the following parts of the full API (module) reference:
+    1. [Data samples reference](api/tempor.data.samples),
+    2. [Dataset reference](api/tempor.data.dataset),
+    3. [Dataloader reference](api/tempor.data.dataloader).
+
+
+
+## User guide
+
+**Usage**
+
+It is recommended to go through the [usage tutorials](user_guide/usage/index.md) before contributing.
+
+
+**Extending**
+
+🔥 The [extending guide](user_guide/extending/index.md) provides a great starring point for developing new methods.
+
+
+
+## Base Classes
+<!--
+Generating class diagrams can be done with the below command
+(requires graphviz to by installed on the system, and pylint via pip):
+```sh
+pyreverse -o png \
+"tempor.plugins" \
+--class \
+"
+tempor.plugins.core._base_estimator.BaseEstimator,
+tempor.plugins.core._base_predictor.BasePredictor,
+tempor.plugins.core._base_transformer.BaseTransformer,
+tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier,
+tempor.plugins.prediction.one_off.regression.BaseOneOffRegressor,
+tempor.plugins.prediction.temporal.classification.BaseTemporalClassifier,
+tempor.plugins.prediction.temporal.regression.BaseTemporalRegressor,
+tempor.plugins.preprocessing.imputation._base.BaseImputer,
+tempor.plugins.preprocessing.scaling._base.BaseScaler,
+tempor.plugins.time_to_event.BaseTimeToEventAnalysis,
+tempor.plugins.treatments.one_off._base.BaseOneOffTreatmentEffects,
+tempor.plugins.treatments.temporal._base.BaseTemporalTreatmentEffects
+" \
+-ASmy
+```
+--->
+The [sklearn](https://scikit-learn.org/)-inspired `fit/transform/predict` API is used for the methods. This section briefly describes the base classes used to achieve this. Full details are available in the API documentation linked for each class.
+
+:::{caution}
+*TemporAI* is in alpha, and the details of the API may change.
+:::
+
+### Core base classes
+
+#### `BaseEstimator`
+All method plugins derive from [`BaseEstimator`](api/tempor.plugins.core):
+
+![BaseEstimator_class_diagram](assets/class_diagrams/tempor.plugins.core._base_estimator.BaseEstimator.png){w=500px}
+
+Methods worth noting are: `fit` (fits model), `hyperparameter_space` (abstract method, returns the default hyperparameter space), `sample_hyperparameters` (implements sampling of said hyperparameters). The `_fit` method is the abstract method that the concrete plugins should implement (it will be called by `fit`).
+
+Note also that the `BaseEstimator` inherits from the [`Plugin`](api/tempor.plugins.core) interface, which facilitates loading of the methods by the PluginLoader.
+
+`ParamsDefinition` attribute facilitates accessing the class parameters using `self.params` in the derived classes.
+
+#### `BasePredictor`
+All predictive method plugins derive from [`BasePredictor`](api/tempor.plugins.core):
+
+![BasePredictor_class_diagram](assets/class_diagrams/tempor.plugins.core._base_predictor.BasePredictor.png){w=550px}
+
+This base class adds prediction-specific methods (analogously to `_fit/fit`):
+* `predict` (public API) and `_predict` (for concrete implementation by each plugin): return predicted values.
+* `predict_proba` and `_predict_proba`: return predicted probabilities, classification setting only.
+* `predict_counterfactuals` and `_predict_counterfactuals`: return counterfactual predictions, treatment effects setting only.
+
+Some of these methods are defined further in task-specific base classes.
+
+#### `BaseTransformer`
+All preprocessing data transformation method plugins derive from [`BaseTransformer`](api/tempor.plugins.core):
+
+![BaseTransformer_class_diagram](assets/class_diagrams/tempor.plugins.core._base_transformer.BaseTransformer.png){w=550px}
+
+This base class provides the `transform` (public API) and `_transform` (for concrete implementation by each plugin) methods that take in, and return a transformed version of, a `Dataset`.
+
+### Task-specific base classes
+
+#### `BaseScaler`, `BaseImputer`
+
+These are the base classes for imputation (`"preprocessing.imputation"`) and scaling imputation (`"preprocessing.scaling"`). These currently do not differ from `BaseTransformer`.
+
+#### `BaseTimeToEventAnalysis`
+
+[`BaseTimeToEventAnalysis`](api/tempor.plugins.time_to_event) is the base class for time-to-event (survival) analysis. Note that the `predict` method requires a `horizons` argument to specify time points for risk estimation.
+
+![BaseTimeToEventAnalysis_class_diagram](assets/class_diagrams/tempor.plugins.time_to_event.BaseTimeToEventAnalysis.png){w=750px}
+
+This class expects [`TimeToEventAnalysisDataset`](tempor.data.dataset).
+
+#### `BaseOneOffTreatmentEffects`
+
+[`BaseOneOffTreatmentEffects`](api/tempor.plugins.treatments.one_off) is the base class for one-off treatment effects plugins. `predict_counterfactuals` returns `StaticSamples`.
+
+![BaseOneOffTreatmentEffects_class_diagram](assets/class_diagrams/tempor.plugins.treatments.one_off._base.BaseOneOffTreatmentEffects.png){w=600px}
+
+This class expects [`OneOffTreatmentEffectsDataset`](tempor.data.dataset).
+
+#### `BaseTemporalTreatmentEffects`
+
+[`BaseTemporalTreatmentEffects`](api/tempor.plugins.treatments.temporal) is the base class for temporal treatment effects plugins. `predict_counterfactuals` returns `TimeSeriesSamples`.
+
+![BaseTemporalTreatmentEffects_class_diagram](assets/class_diagrams/tempor.plugins.treatments.temporal._base.BaseTemporalTreatmentEffects.png){w=600px}
+
+This class expects [`BaseTemporalTreatmentEffectsDataset`](tempor.data.dataset).
+
+#### `BaseOneOff{Classifier,Regressor}`
+
+These are the base classes for the one-off prediction setting, where the targets are static values.
+
+* [`BaseOneOffClassifier`](api/tempor.plugins.prediction.one_off.classification), base class for one-off classification task plugins:
+![BaseOneOffClassifier_class_diagram](assets/class_diagrams/tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier.png){w=600px}
+
+* [`BaseOneOffRegressor`](api/tempor.plugins.prediction.one_off.classification), base class for one-off regression task plugins:
+![BaseOneOffRegressor_class_diagram](assets/class_diagrams/tempor.plugins.prediction.one_off.regression.BaseOneOffRegressor.png){w=600px}
+
+These classes expects [`OneOffPredictionDataset`](tempor.data.dataset).
+
+#### `BaseTemporal{Classifier,Regressor}`
+
+These are the base classes for the temporal prediction setting, where the targets are time series.
+
+* [`BaseTemporalClassifier`](api/tempor.plugins.prediction.temporal.classification), base class for one-off classification task plugins:
+![BaseTemporalClassifier_class_diagram](assets/class_diagrams/tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier.png){w=600px}
+
+* [`BaseTemporalRegressor`](api/tempor.plugins.prediction.temporal.classification), base class for one-off regression task plugins:
+![BaseTemporalRegressor_class_diagram](assets/class_diagrams/tempor.plugins.prediction.one_off.regression.BaseOneOffRegressor.png){w=600px}
+
+These classes expects [`TemporalPredictionDataset`](tempor.data.dataset).
+
+
+
+## Pipelines
+The `Pipeline` functionality is provided by the [`tempor.plugins.pipeline`](api/tempor.plugins.pipeline) module, see especially:
+* `PipelineBase` base class (defines pipeline interface),
+* `PipelineMeta` metaclass (dynamically generates pipelines),
+* `pipeline` function (creates pipeline from its definition).
+
+
+
+## Models
+The [`tempor.models`](api/tempor.models) namespace contains various underlying model components (currently mostly `torch` modules), such as [`MLP`](api/tempor.models.mlp), general-purpose [`TimeSeriesModel`](api/tempor.models.ts_model), [`NeuralODE`](api/tempor.models.ts_ode), etc. Feel free to use or build upon these in your methods.
diff --git a/docs/index.md b/docs/index.md
index 69471c4a..8e736bb1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,24 +3,20 @@
 TemporAI: ML-centric Toolkit for Medical Time Series
 
 
-## Note
-
-> **💡 Status**: Migrating from the [previous iteration of the project](https://github.com/vanderschaarlab/clairvoyance2).
-
-
 
 ## Contents
 
 ```{toctree}
 :glob:
-:maxdepth: 3
+:maxdepth: 1
 
 Overview <overview>
-User Guide <user_guide>
+User Guide <user_guide/index>
 Contributions & Help <contributing>
 License <license>
 Authors <authors>
 Changelog <changelog>
+Developer's Guide <dev_guide>
 Module Reference <api/modules>
 ```
 
diff --git a/docs/overview.md b/docs/overview.md
index d2af131b..8149fddc 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -8,24 +8,36 @@
 
 # <img src="assets/TemporAI_Logo_Icon.png" height=25> TemporAI
 
-> **⚗️ Status:** This project is still in *alpha*, and the API may change without warning.  
 
-*TemporAI* is a Machine Learning-centric time-series library for medicine.  The tasks that are currently of focus in TemporAI are: time-series prediction, time-to-event (a.k.a. survival) analysis with time-series data, and counterfactual inference (i.e. \[individualized\] treatment effects).
+:::{important}
+**Status:** This project is still in *alpha*, and the API may change without warning.  
+:::
 
-*TemporAI* provides data preprocessing methods (including scaling and imputation methods for static and temporal covariates). AutoML tools for hyperparameter tuning and pipeline selection are available.
 
-In future versions, the library also aims to provide the user with understanding of their data, model, and problem, through e.g. integration with interpretability methods.
 
-Key concepts:
+## 📃 Overview
+
+*TemporAI* is a Machine Learning-centric time-series library for medicine.  The tasks that are currently of focus in TemporAI are: time-to-event (survival) analysis with time-series data, treatment effects (causal inference) over time, and time-series prediction. Data preprocessing methods, including missing value imputation for static and temporal covariates, are provided. AutoML tools for hyperparameter tuning and pipeline selection are also available.
+
+### How is TemporAI unique?
+
+* **🏥 Medicine-first:** Focused on use cases for medicine and healthcare, such as temporal treatment effects, survival analysis over time, imputation methods, models with built-in and post-hoc interpretability, ... See [methods](#methods).
+* **🏗️ Fast prototyping:** A plugin design allowing for on-the-fly integration of new methods by the users.
+* **🚀 From research to practice:** Relevant novel models from research community adapted for practical use.
+* **🌍 A healthcare ecosystem vision:** A range of interactive demonstration apps, new medical problem settings, interpretability tools, data-centric tools etc. are planned.
+
+### Key concepts
 
 <div align="center">
 
 
-<img src="assets/Conceptual.png" width="750" alt="key concepts">
+<img src="assets/Conceptual.png" alt="key concepts">
 
 
 </div>
 
+
+
 ## 🚀 Installation
 
 ```bash
@@ -36,6 +48,8 @@ or from source, using
 $ pip install .
 ```
 
+
+
 ## 💥 Sample Usage
 * List the available plugins
 ```{testcode}
@@ -52,7 +66,121 @@ from tempor.plugins import plugin_loader
 print(plugin_loader.list())
 ```
 
-* Use an imputer
+* Use a time-to-event (survival) analysis model
+```{testcode}
+:hide:
+import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
+
+from tempor.utils.dataloaders import PBCDataLoader
+from tempor.plugins import plugin_loader
+
+# Load a time-to-event dataset:
+dataset = PBCDataLoader().load()
+
+# Initialize the model:
+model = plugin_loader.get("time_to_event.dynamic_deephit")
+
+# Train:
+model.fit(dataset)
+
+# Make risk predictions:
+prediction = model.predict(dataset, horizons=[0.25, 0.50, 0.75])
+```
+```python
+from tempor.utils.dataloaders import PBCDataLoader
+from tempor.plugins import plugin_loader
+
+# Load a time-to-event dataset:
+dataset = PBCDataLoader().load()
+
+# Initialize the model:
+model = plugin_loader.get("time_to_event.dynamic_deephit")
+
+# Train:
+model.fit(dataset)
+
+# Make risk predictions:
+prediction = model.predict(dataset, horizons=[0.25, 0.50, 0.75])
+```
+
+* Use a temporal treatment effects model
+```{testcode}
+:hide:
+import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
+
+import numpy as np
+
+from tempor.utils.dataloaders import DummyTemporalTreatmentEffectsDataLoader
+from tempor.plugins import plugin_loader
+
+# Load a dataset with temporal treatments and outcomes:
+dataset = DummyTemporalTreatmentEffectsDataLoader(
+    temporal_covariates_missing_prob=0.0,
+    temporal_treatments_n_features=1,
+    temporal_treatments_n_categories=2,
+).load()
+
+# Initialize the model:
+model = plugin_loader.get("treatments.temporal.regression.crn_regressor", epochs=20)
+
+# Train:
+model.fit(dataset)
+
+# Define target variable horizons for each sample:
+horizons = [
+    tc.time_indexes()[0][len(tc.time_indexes()[0]) // 2 :] for tc in dataset.time_series
+]
+
+# Define treatment scenarios for each sample:
+treatment_scenarios = [
+    [np.asarray([1] * len(h)), np.asarray([0] * len(h))] for h in horizons
+]
+
+# Predict counterfactuals:
+counterfactuals = model.predict_counterfactuals(
+    dataset,
+    horizons=horizons,
+    treatment_scenarios=treatment_scenarios,
+)
+```
+```python
+import numpy as np
+
+from tempor.utils.dataloaders import DummyTemporalTreatmentEffectsDataLoader
+from tempor.plugins import plugin_loader
+
+# Load a dataset with temporal treatments and outcomes:
+dataset = DummyTemporalTreatmentEffectsDataLoader(
+    temporal_covariates_missing_prob=0.0,
+    temporal_treatments_n_features=1,
+    temporal_treatments_n_categories=2,
+).load()
+
+# Initialize the model:
+model = plugin_loader.get("treatments.temporal.regression.crn_regressor", epochs=20)
+
+# Train:
+model.fit(dataset)
+
+# Define target variable horizons for each sample:
+horizons = [
+    tc.time_indexes()[0][len(tc.time_indexes()[0]) // 2 :] for tc in dataset.time_series
+]
+
+# Define treatment scenarios for each sample:
+treatment_scenarios = [
+    [np.asarray([1] * len(h)), np.asarray([0] * len(h))] for h in horizons
+]
+
+# Predict counterfactuals:
+counterfactuals = model.predict_counterfactuals(
+    dataset,
+    horizons=horizons,
+    treatment_scenarios=treatment_scenarios,
+)
+```
+
+* Use a missing data imputer
 ```{testcode}
 :hide:
 import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
@@ -68,7 +196,7 @@ print(static_data_n_missing, temporal_data_n_missing)
 assert static_data_n_missing > 0
 assert temporal_data_n_missing > 0
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("preprocessing.imputation.temporal.bfill")
 
 # Train:
@@ -93,7 +221,7 @@ print(static_data_n_missing, temporal_data_n_missing)
 assert static_data_n_missing > 0
 assert temporal_data_n_missing > 0
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("preprocessing.imputation.temporal.bfill")
 
 # Train:
@@ -107,7 +235,7 @@ print(static_data_n_missing, temporal_data_n_missing)
 assert temporal_data_n_missing == 0
 ```
 
-* Use a classifier
+* Use a one-off classifier (prediction)
 ```{testcode}
 :hide:
 import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
@@ -117,7 +245,7 @@ from tempor.plugins import plugin_loader
 
 dataset = SineDataLoader().load()
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
 
 # Train:
@@ -132,7 +260,7 @@ from tempor.plugins import plugin_loader
 
 dataset = SineDataLoader().load()
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
 
 # Train:
@@ -142,43 +270,44 @@ model.fit(dataset)
 prediction = model.predict(dataset)
 ```
 
-* Use a regressor
+* Use a temporal regressor (forecasting)
 ```{testcode}
 :hide:
 import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
 
-from tempor.utils.dataloaders import SineDataLoader
+from tempor.utils.dataloaders import DummyTemporalPredictionDataLoader
 from tempor.plugins import plugin_loader
 
-dataset = SineDataLoader().load()
+# Load a dataset with temporal targets.
+dataset = DummyTemporalPredictionDataLoader(temporal_covariates_missing_prob=0.0).load()
 
-# Load the model:
-model = plugin_loader.get("prediction.one_off.regression.nn_regressor", n_iter=50)
+# Initialize the model:
+model = plugin_loader.get("prediction.temporal.regression.seq2seq_regressor", epochs=10)
 
 # Train:
 model.fit(dataset)
 
 # Predict:
-prediction = model.predict(dataset)
+prediction = model.predict(dataset, n_future_steps=5)
 ```
 ```python
-from tempor.utils.dataloaders import SineDataLoader
+from tempor.utils.dataloaders import DummyTemporalPredictionDataLoader
 from tempor.plugins import plugin_loader
 
-dataset = SineDataLoader().load()
+# Load a dataset with temporal targets.
+dataset = DummyTemporalPredictionDataLoader(temporal_covariates_missing_prob=0.0).load()
 
-# Load the model:
-model = plugin_loader.get("prediction.one_off.regression.nn_regressor", n_iter=50)
+# Initialize the model:
+model = plugin_loader.get("prediction.temporal.regression.seq2seq_regressor", epochs=10)
 
 # Train:
 model.fit(dataset)
 
 # Predict:
-prediction = model.predict(dataset)
+prediction = model.predict(dataset, n_future_steps=5)
 ```
 
-* Benchmark models
-Classification task
+* Benchmark models, time-to-event task
 ```{testcode}
 :hide:
 import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
@@ -186,31 +315,36 @@ import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
 from tempor.benchmarks import benchmark_models
 from tempor.plugins import plugin_loader
 from tempor.plugins.pipeline import pipeline
-from tempor.utils.dataloaders import SineDataLoader
+from tempor.utils.dataloaders import PBCDataLoader
 
 testcases = [
     (
         "pipeline1",
         pipeline(
             [
-                "preprocessing.scaling.static.static_minmax_scaler",
-                "prediction.one_off.classification.nn_classifier",
+                "preprocessing.scaling.temporal.ts_minmax_scaler",
+                "time_to_event.dynamic_deephit",
             ]
-        )({"nn_classifier": {"n_iter": 10}}),
+        )({"ts_coxph": {"n_iter": 100}}),
     ),
     (
         "plugin1",
-        plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=10),
+        plugin_loader.get("time_to_event.dynamic_deephit", n_iter=100),
+    ),
+    (
+        "plugin2",
+        plugin_loader.get("time_to_event.ts_coxph", n_iter=100),
     ),
 ]
-dataset = SineDataLoader().load()
+dataset = PBCDataLoader().load()
 
 aggr_score, per_test_score = benchmark_models(
-    task_type="prediction.one_off.classification",
+    task_type="time_to_event",
     tests=testcases,
     data=dataset,
     n_splits=2,
     random_state=0,
+    horizons=[2.0, 4.0, 6.0],
 )
 
 print(aggr_score)
@@ -219,31 +353,36 @@ print(aggr_score)
 from tempor.benchmarks import benchmark_models
 from tempor.plugins import plugin_loader
 from tempor.plugins.pipeline import pipeline
-from tempor.utils.dataloaders import SineDataLoader
+from tempor.utils.dataloaders import PBCDataLoader
 
 testcases = [
     (
         "pipeline1",
         pipeline(
             [
-                "preprocessing.scaling.static.static_minmax_scaler",
-                "prediction.one_off.classification.nn_classifier",
+                "preprocessing.scaling.temporal.ts_minmax_scaler",
+                "time_to_event.dynamic_deephit",
             ]
-        )({"nn_classifier": {"n_iter": 10}}),
+        )({"ts_coxph": {"n_iter": 100}}),
     ),
     (
         "plugin1",
-        plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=10),
+        plugin_loader.get("time_to_event.dynamic_deephit", n_iter=100),
+    ),
+    (
+        "plugin2",
+        plugin_loader.get("time_to_event.ts_coxph", n_iter=100),
     ),
 ]
-dataset = SineDataLoader().load()
+dataset = PBCDataLoader().load()
 
 aggr_score, per_test_score = benchmark_models(
-    task_type="prediction.one_off.classification",
+    task_type="time_to_event",
     tests=testcases,
     data=dataset,
     n_splits=2,
     random_state=0,
+    horizons=[2.0, 4.0, 6.0],
 )
 
 print(aggr_score)
@@ -257,7 +396,7 @@ import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
 from tempor.utils.serialization import load, save
 from tempor.plugins import plugin_loader
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
 
 buff = save(model)  # Save model to bytes.
@@ -269,7 +408,7 @@ reloaded = load(buff)  # Reload model.
 from tempor.utils.serialization import load, save
 from tempor.plugins import plugin_loader
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
 
 buff = save(model)  # Save model to bytes.
@@ -278,7 +417,7 @@ reloaded = load(buff)  # Reload model.
 # `save_to_file`, `load_from_file` also available in the serialization module.
 ```
 
-* AutoML
+* AutoML - search for the best pipeline for your task
 ```{testcode}
 :hide:
 import os; import sys; f = open(os.devnull, 'w'); sys.stdout = f
@@ -343,51 +482,14 @@ seeker = PipelineSeeker(
 best_pipelines, best_scores = seeker.search()  # doctest: +SKIP
 ```
 
-## 🔑 Methods
-
-
-
-### Prediction
-
-#### One-off
-Prediction where targets are static.
-
-* Classification (category: `prediction.one_off.classification`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `nn_classifier` | Neural-net based classifier. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
-| `ode_classifier` | Classifier based on ordinary differential equation (ODE) solvers.  | --- |
-| `cde_classifier` | Classifier based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926) |
-| `laplace_ode_classifier` | Classifier based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
-
-* Regression (category: `prediction.one_off.regression`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `nn_regressor` | Neural-net based regressor. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
-| `ode_regressor` | Regressor based on ordinary differential equation (ODE) solvers.  | --- |
-| `cde_regressor` | Regressor based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926)
-| `laplace_ode_regressor` | Regressor based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
-
-#### Temporal
-Prediction where targets are temporal (time series).
-
-* Classification (category: `prediction.temporal.classification`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `seq2seq_classifier` | Seq2Seq prediction, classification | --- |
 
-* Regression (category: `prediction.temporal.regression`)
 
-| Name | Description| Reference |
-| --- | --- | --- |
-| `seq2seq_regressor` | Seq2Seq prediction, regression | --- |
+{#methods}
+## 🔑 Methods
 
 
 
-### Time-to-Event
+### Time-to-Event (survival) analysis over time
 
 Risk estimation given event data (category: `time_to_event`)
 
@@ -431,6 +533,46 @@ Treatment effects estimation where treatments are temporal (time series).
 
 
 
+### Prediction
+
+#### One-off
+Prediction where targets are static.
+
+* Classification (category: `prediction.one_off.classification`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `nn_classifier` | Neural-net based classifier. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
+| `ode_classifier` | Classifier based on ordinary differential equation (ODE) solvers.  | --- |
+| `cde_classifier` | Classifier based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926) |
+| `laplace_ode_classifier` | Classifier based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
+
+* Regression (category: `prediction.one_off.regression`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `nn_regressor` | Neural-net based regressor. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
+| `ode_regressor` | Regressor based on ordinary differential equation (ODE) solvers.  | --- |
+| `cde_regressor` | Regressor based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926)
+| `laplace_ode_regressor` | Regressor based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
+
+#### Temporal
+Prediction where targets are temporal (time series).
+
+* Classification (category: `prediction.temporal.classification`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `seq2seq_classifier` | Seq2Seq prediction, classification | --- |
+
+* Regression (category: `prediction.temporal.regression`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `seq2seq_regressor` | Seq2Seq prediction, regression | --- |
+
+
+
 ### Preprocessing
 
 #### Imputation
@@ -468,29 +610,29 @@ Treatment effects estimation where treatments are temporal (time series).
 
 
 
-## Tutorials
+## 📖 Tutorials
 
 ### Data
 
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial01_data_format.ipynb) - [Data Format](https://github.com/vanderschaarlab/temporai/tutorials/data/tutorial01_data_format.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial02_datasets.ipynb) - [Datasets](https://github.com/vanderschaarlab/temporai/tutorials/data/tutorial02_datasets.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial03_dataloaders.ipynb) - [Data Loaders](https://github.com/vanderschaarlab/temporai/tutorials/data/tutorial03_dataloaders.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial04_data_splitting.ipynb) - [Data Splitting](https://github.com/vanderschaarlab/temporai/tutorials/data/tutorial04_data_splitting.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial01_data_format.ipynb) - [Data Format](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/data/tutorial01_data_format.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial02_datasets.ipynb) - [Datasets](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/data/tutorial02_datasets.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial03_dataloaders.ipynb) - [Data Loaders](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/data/tutorial03_dataloaders.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial04_data_splitting.ipynb) - [Data Splitting](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/data/tutorial04_data_splitting.ipynb)
 
 ### User Guide
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial01_plugins.ipynb) - [Plugins](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial01_plugins.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial02_imputation.ipynb) - [Imputation](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial02_imputation.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial03_scaling.ipynb) - [Scaling](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial03_scaling.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial04_prediction.ipynb) - [Prediction](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial04_prediction.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial05_time_to_event.ipynb) - [Time-to-event Analysis](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial05_time_to_event.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial06_treatments.ipynb) - [Treatment Effects](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial06_treatments.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial07_pipeline.ipynb) - [Pipeline](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial07_pipeline.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial08_benchmarks.ipynb) - [Benchmarks](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial08_benchmarks.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial09_automl.ipynb) - [AutoML](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial09_automl.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial01_plugins.ipynb) - [Plugins](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial01_plugins.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial02_imputation.ipynb) - [Imputation](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial02_imputation.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial03_scaling.ipynb) - [Scaling](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial03_scaling.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial04_prediction.ipynb) - [Prediction](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial04_prediction.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial05_time_to_event.ipynb) - [Time-to-event Analysis](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial05_time_to_event.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial06_treatments.ipynb) - [Treatment Effects](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial06_treatments.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial07_pipeline.ipynb) - [Pipeline](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial07_pipeline.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial08_benchmarks.ipynb) - [Benchmarks](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial08_benchmarks.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial09_automl.ipynb) - [AutoML](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial09_automl.ipynb)
 
 ### Extending TemporAI
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial01_custom_plugin.ipynb) - [Writing a Custom Plugin](https://github.com/vanderschaarlab/temporai/tutorials/extending/tutorial01_custom_plugin.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial02_testing_custom_plugin.ipynb) - [Testing a Custom Plugin](https://github.com/vanderschaarlab/temporai/tutorials/extending/tutorial02_testing_custom_plugin.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial01_custom_plugin.ipynb) - [Writing a Custom Plugin](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/extending/tutorial01_custom_plugin.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial02_testing_custom_plugin.ipynb) - [Testing a Custom Plugin](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/extending/tutorial02_testing_custom_plugin.ipynb)
 
 
 
@@ -513,10 +655,11 @@ pytest -vsx
 ```
 
 For development and contribution to TemporAI, see:
-* 📓 [Extending TemporAI tutorials](https://github.com/vanderschaarlab/temporai/tutorials/extending/)
-* 📃 [Contribution guide](https://github.com/vanderschaarlab/temporai/CONTRIBUTING.md)
+* 📓 [Extending TemporAI tutorials](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/extending/)
+* 📃 [Contribution guide](https://github.com/vanderschaarlab/temporai/tree/main/CONTRIBUTING.md)
+* 👩‍💻 [Developer's guide](dev_guide.md)
 
-## Citing
+## ✍️ Citing
 
 If you use this code, please cite the associated paper:
 ```
diff --git a/docs/pre_build.py b/docs/pre_build.py
index 2ce217cb..c3009a23 100644
--- a/docs/pre_build.py
+++ b/docs/pre_build.py
@@ -9,11 +9,16 @@
 import os
 import re
 
-REPO_URL = "https://github.com/vanderschaarlab/temporai/"
+from pre_build_nbconvert import do_conversion
+
+REPO_URL_ROOT = "https://github.com/vanderschaarlab/temporai/"
+REPO_URL_TREE = f"{REPO_URL_ROOT}tree/main/"
 
 
 # -- Update `docs/requirements.txt` with the content of `install_requires` in `setup.cfg`.
 
+print("Updating `docs/requirements.txt`...")
+
 SETUP_CFG_PATH = os.path.join(os.path.dirname(__file__), "../setup.cfg")
 DOCS_REQS_FILE = os.path.join(os.path.dirname(__file__), "requirements.txt")
 with open(SETUP_CFG_PATH, "r", encoding="utf8") as file:
@@ -35,6 +40,7 @@
     file.write(docs_req_content)
 
 # -- Convert `README.md` into `overview.md`.
+print("Working on `docs/overview.md`...")
 
 README_PATH = os.path.join(os.path.dirname(__file__), "../README.md")
 OVERVIEW_PATH = os.path.join(os.path.dirname(__file__), "overview.md")
@@ -46,7 +52,8 @@
     "include_docs_end -->": "",
     "./docs/": "",
     "docs/": "",
-    "./": REPO_URL,
+    "./#-": "#",
+    "./": REPO_URL_TREE,
 }
 
 with open(README_PATH, "r", encoding="utf8") as file:
@@ -85,12 +92,13 @@
 
 
 # -- Convert `README.md` into `pypi.md`.
+print("Working on `pypi.md`...")
 
 PYPI_README_PATH = os.path.join(os.path.dirname(__file__), "../pypi.md")
 
 REPLACE = {
     # Add more as necessary.
-    "./": REPO_URL,
+    "./": REPO_URL_TREE,
 }
 
 with open(README_PATH, "r", encoding="utf8") as file:
@@ -110,3 +118,7 @@
 
 with open(PYPI_README_PATH, "w", encoding="utf8") as file:
     file.write(readme_content)
+
+
+# Convert tutorial notebooks into user guide pages.
+do_conversion()
diff --git a/docs/pre_build_nbconvert.py b/docs/pre_build_nbconvert.py
new file mode 100644
index 00000000..9eae6546
--- /dev/null
+++ b/docs/pre_build_nbconvert.py
@@ -0,0 +1,69 @@
+import glob
+import os
+import re
+
+import nbformat
+from nbconvert import MarkdownExporter
+
+_this_dir = os.path.join(os.path.dirname(__file__))
+
+README_PATH = os.path.join(os.path.dirname(__file__), "../README.md")
+with open(README_PATH, "r", encoding="utf8") as file:
+    readme_content = file.read()
+
+DIR_MAP = {
+    os.path.join(_this_dir, "../tutorials/data"): os.path.join(_this_dir, "user_guide/data"),
+    os.path.join(_this_dir, "../tutorials/usage"): os.path.join(_this_dir, "user_guide/usage"),
+    os.path.join(_this_dir, "../tutorials/extending"): os.path.join(_this_dir, "user_guide/extending"),
+}
+
+
+def remove_last_and_on(s: str, substring: str) -> str:
+    i = s.rfind(substring)
+    return s[:i] if i >= 0 else s
+
+
+def get_notebook_link(notebook_file: str) -> str:
+    find = r"- \[!\[Test In Colab\].*?" + notebook_file + r"\)"
+    found = re.findall(find, readme_content)
+    if len(found) == 0:
+        raise RuntimeError(
+            f"The section corresponding to notebook links for notebook {notebook_file} not found in README.md"
+        )
+    return found[0][2:]
+
+
+# NOTE: Images are not handled.
+def convert_nb_to_md(notebook_path: str, output_path: str, notebook_link: str) -> None:
+    # Load notebook
+    with open(notebook_path, encoding="utf8") as f:
+        nb = nbformat.read(f, as_version=4)
+
+    # Initialize markdown exporter
+    md_exporter = MarkdownExporter()
+
+    # Convert to markdown
+    body, resources = md_exporter.from_notebook_node(nb)  # pylint: disable=unused-variable
+
+    # Remove the install step.
+    body = re.sub(r"\*Skip.*?temporai.git\n```", "", body, flags=re.DOTALL)
+    # Remove the "congratulations!" note.
+    body = remove_last_and_on(body, "## 🎉 Congratulations!")
+
+    # Add the notebook links.
+    body = re.sub(r"#(.*?)\n", r"#\1\n" + notebook_link + "\n", body, flags=re.DOTALL, count=1)
+
+    # Write markdown to file
+    with open(output_path, "w", encoding="utf8") as f:
+        f.write(body)
+
+
+def do_conversion():
+    for nb_dir, doc_dir in DIR_MAP.items():
+        for nb_file in glob.glob(os.path.join(nb_dir, "*.ipynb")):
+            print(f"Converting {nb_file}...")
+            filename = os.path.basename(nb_file)
+            nb_links = get_notebook_link(filename)
+            filename_no_ext = filename.split(".")[0]
+            md_file = os.path.join(doc_dir, f"{filename_no_ext}.md")
+            convert_nb_to_md(nb_file, md_file, nb_links)
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 26433963..9d23df2d 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -18,9 +18,9 @@ pandas >=1
 pandera >=0.13
 pydantic >=1, <2
 rich
-scikit-learn
 scikit-learn >= 1.0
 scipy
+seaborn
 torch
 torchcde
 torchdiffeq
diff --git a/docs/user_guide.md b/docs/user_guide.md
deleted file mode 100644
index dd8b8215..00000000
--- a/docs/user_guide.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# User Guide
-
-We provide a user guide in the form of a series of notebooks.
-
-
-
-## Data Tutorials
-
-**Data Format:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial01_data_format.ipynb)
-
-**Datasets:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial02_datasets.ipynb)
-
-**Data Loaders:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial03_dataloaders.ipynb)
-
-
-
-## Main Tutorials
-
-**Plugins:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial01_plugins.ipynb)
-
-**Imputation:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial02_imputation.ipynb)
-
-**Scaling:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial03_scaling.ipynb)
-
-**Prediction:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial04_prediction.ipynb)
-
-**Time:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial05_time_to_event.ipynb)
-
-**Treatment:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial06_treatments.ipynb)
-
-**Pipeline:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial07_pipeline.ipynb)
-
-**Plugins:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial08_benchmarks.ipynb)
-
-
-
-## Extending the Library
-
-**Writing a Custom Plugin:**
-
-[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial01_custom_plugin.ipynb)
diff --git a/docs/user_guide/data/index.md b/docs/user_guide/data/index.md
new file mode 100644
index 00000000..5254da5f
--- /dev/null
+++ b/docs/user_guide/data/index.md
@@ -0,0 +1,11 @@
+# Tutorials: Data
+
+This section contains tutorials on the data format and data manipulation.
+
+
+```{toctree}
+:glob:
+:maxdepth: 2
+
+*
+```
diff --git a/docs/user_guide/data/tutorial01_data_format.md b/docs/user_guide/data/tutorial01_data_format.md
new file mode 100644
index 00000000..f8aeca5e
--- /dev/null
+++ b/docs/user_guide/data/tutorial01_data_format.md
@@ -0,0 +1,897 @@
+# Data Tutorial 01: Data Format
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial01_data_format.ipynb)
+
+This tutorial shows a minimal example of the data format for TemporAI.
+
+
+
+## Time series data
+
+Time series data contains data samples (e.g. patients), with features that unfold sequentially over some number of timesteps.
+
+Time series data should take form of a `pandas.DataFrame`, with the following specifics:
+* The index should be a 2-level multiindex, where level `0` index represents sampled IDs, and level `1` represents the timesteps for each sample.
+* The sample index can be comprised of either `int`s or `str`s (homogenous, not a mix of these).
+* The time index (timesteps) may be `int`, `float` or `pandas.Timestep`-compatible format (homogenous, not a mix of these).
+* The columns of the dataframe represent the features, column names must be `str`.
+* Column (feature) values currently supported are: `bool`, `int`, `float`, or `pandas.Categorical` (homogenous per column).
+
+Other points to note:
+* Sample IDs must be unique.
+* (Sample ID, timestep) combination must be unique (a sample cannot have more than one of the same timestep).
+* Null values such as `numpy.nan` are allowed and represent missing values.
+
+
+```python
+import pandas as pd
+import numpy as np
+
+from IPython.display import display
+```
+
+
+```python
+# Create a time series dataframe.
+
+time_series_df = pd.DataFrame(
+    {
+        "sample_idx": ["sample_0", "sample_0", "sample_0", "sample_0", "sample_1", "sample_1", "sample_2"],
+        "time_idx": [1, 2, 3, 4, 2, 4, 9],
+        "t_feat_0": [11, 12, 13, 14, 21, 22, 31],
+        "t_feat_1": [1.1, 1.2, 1.3, np.nan, 2.1, 2.2, 3.1],
+        "t_feat_2": ["a", "b", "b", "c", "a", "a", "c"],
+    }
+)
+
+# Set the 2-level index:
+time_series_df.set_index(keys=["sample_idx", "time_idx"], drop=True, inplace=True)
+
+# "feat_2" needs to be set to a categorical, as `str` format is not supported.
+time_series_df["t_feat_2"] = pd.Categorical(time_series_df["t_feat_2"])
+
+# Preview the dataframe:
+time_series_df.info()
+time_series_df
+```
+
+    <class 'pandas.core.frame.DataFrame'>
+    MultiIndex: 7 entries, ('sample_0', 1) to ('sample_2', 9)
+    Data columns (total 3 columns):
+     #   Column    Non-Null Count  Dtype   
+    ---  ------    --------------  -----   
+     0   t_feat_0  7 non-null      int64   
+     1   t_feat_1  6 non-null      float64 
+     2   t_feat_2  7 non-null      category
+    dtypes: category(1), float64(1), int64(1)
+    memory usage: 725.0+ bytes
+
+
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+      <td>b</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+      <td>b</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>NaN</td>
+      <td>c</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+      <td>c</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## Static data
+
+Static data contains data samples (e.g. patients), features that are not associated with a particular time.
+
+Static data should take form of a `pandas.DataFrame`, with the following specifics:
+* The index represents sample IDs and is a (single level) index that can be comprised of `int`s or `str`s (homogenous, not a mix of these).
+* The columns of the dataframe represent the features, column names must be `str`.
+* Column (feature) values currently supported are: `bool`, `int`, `float`, or `pandas.Categorical` (homogenous per column).
+
+Other points to note:
+* Sample IDs must be unique.
+* Null values such as `numpy.nan` are allowed and represent missing values.
+
+
+```python
+# Create a static data dataframe.
+
+static_df = pd.DataFrame(
+    {
+        "s_feat_0": [100, 200, 300],
+        "s_feat_1": [-1.1, np.nan, -1.3],
+    },
+    index=["sample_0", "sample_1", "sample_2"],
+)
+
+# Preview the dataframe:
+static_df.info()
+static_df
+```
+
+    <class 'pandas.core.frame.DataFrame'>
+    Index: 3 entries, sample_0 to sample_2
+    Data columns (total 2 columns):
+     #   Column    Non-Null Count  Dtype  
+    ---  ------    --------------  -----  
+     0   s_feat_0  3 non-null      int64  
+     1   s_feat_1  2 non-null      float64
+    dtypes: float64(1), int64(1)
+    memory usage: 72.0+ bytes
+
+
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>NaN</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## Event data
+
+Event data contains data samples (e.g. patients), with features that represent occurrence of an event at a certain time.
+If the event did not occur, it is "censored".
+
+Event data should take form of a `pandas.DataFrame`, with the following specifics:
+* The index represents sample IDs and is a (single level) index that can be comprised of `int`s or `str`s (homogenous, not a mix of these).
+* The columns of the dataframe represent the features, column names must be `str`.
+* Column (feature) values must be of the form: `Tuple[<timestep>, bool]`.
+    * The first element `<timestep>` may be `int`, `float` or `pandas.Timestep`-compatible format (homogenous per column).
+    * The second element `bool` indicates whether the event occurred at this time (`True`) or the event feature is censored (`False`).
+    * In case of censoring, the timestep should indicate the last time information about the sample was available. 
+
+Other points to note:
+* Sample IDs must be unique.
+* Null values such as `numpy.nan` are not allowed allowed - indicate an event as censored (did not occur) instead.
+
+
+```python
+# Create an event dataframe.
+
+event_df = pd.DataFrame(
+    {
+        "e_feat_0": [(10, True), (12, False), (13, True)],
+        "e_feat_1": [(10, False), (10, False), (11, True)],
+    },
+    index=["sample_0", "sample_1", "sample_2"],
+)
+
+# Preview the dataframe:
+event_df.info()
+event_df
+```
+
+    <class 'pandas.core.frame.DataFrame'>
+    Index: 3 entries, sample_0 to sample_2
+    Data columns (total 2 columns):
+     #   Column    Non-Null Count  Dtype 
+    ---  ------    --------------  ----- 
+     0   e_feat_0  3 non-null      object
+     1   e_feat_1  3 non-null      object
+    dtypes: object(2)
+    memory usage: 72.0+ bytes
+
+
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>e_feat_0</th>
+      <th>e_feat_1</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>(10, True)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>(12, False)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>(13, True)</td>
+      <td>(11, True)</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+The data can also be initialised from a 2D `numpy` array (static, event) or a 3D `numpy` array (time series).
+
+`TODO: more info`
+
+## Dataset
+
+The collection of data that represents the task at hand is a `Dataset`.
+
+A `Dataset` contains:
+* Time series data (covariates),
+* Static data (covariates), optional,
+* Predictive data, which depends on the *task*.
+    * This may contain *targets* and *treatments*.
+
+For example, for the *time-to-event analysis task* we create a dataset as follows.
+
+
+```python
+from tempor.data.dataset import TimeToEventAnalysisDataset
+
+# Create a dataset of time-to-event analysis task:
+data = TimeToEventAnalysisDataset(
+    time_series=time_series_df,
+    static=static_df,
+    targets=event_df,
+)
+
+# Preview dataset:
+data
+```
+
+
+
+
+    TimeToEventAnalysisDataset(
+        time_series=TimeSeriesSamples([3, *, 3]),
+        static=StaticSamples([3, 2]),
+        predictive=TimeToEventAnalysisTaskData(targets=EventSamples([3, 2]))
+    )
+
+
+
+
+```python
+data.time_series
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+      <td>b</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+      <td>b</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>NaN</td>
+      <td>c</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+      <td>c</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.static
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>NaN</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">EventSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>e_feat_0</th>
+      <th>e_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>(10, True)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>(12, False)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>(13, True)</td>
+      <td>(11, True)</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## Useful methods
+
+The data (`{TimeSeries,Event,Static}Samples` classes) provide a number of useful methods, some examples below.
+
+### Examples for `TimeSeriesSamples`
+
+
+```python
+time_series = data.time_series
+```
+
+
+```python
+# Return time series data as a dataframe:
+
+time_series.dataframe()
+```
+
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+      <td>b</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+      <td>b</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>NaN</td>
+      <td>c</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+      <td>a</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+      <td>c</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+# Return time series data as a numpy array:
+
+time_series.numpy(padding_indicator=-999.0)
+```
+
+
+
+
+    array([[[11, 1.1, 'a'],
+            [12, 1.2, 'b'],
+            [13, 1.3, 'b'],
+            [14, nan, 'c']],
+    
+           [[21, 2.1, 'a'],
+            [22, 2.2, 'a'],
+            [-999.0, -999.0, -999.0],
+            [-999.0, -999.0, -999.0]],
+    
+           [[31, 3.1, 'c'],
+            [-999.0, -999.0, -999.0],
+            [-999.0, -999.0, -999.0],
+            [-999.0, -999.0, -999.0]]], dtype=object)
+
+
+
+
+```python
+# Return the time series data as a list of dataframes:
+
+time_series.list_of_dataframes()
+```
+
+
+
+
+    [                     t_feat_0  t_feat_1 t_feat_2
+     sample_idx time_idx                             
+     sample_0   1               11       1.1        a
+                2               12       1.2        b
+                3               13       1.3        b
+                4               14       NaN        c,
+                          t_feat_0  t_feat_1 t_feat_2
+     sample_idx time_idx                             
+     sample_1   2               21       2.1        a
+                4               22       2.2        a,
+                          t_feat_0  t_feat_1 t_feat_2
+     sample_idx time_idx                             
+     sample_2   9               31       3.1        c]
+
+
+
+
+```python
+# Show number of features and samples:
+
+print("num_features:", time_series.num_features)
+print("num_samples:", time_series.num_samples)
+```
+
+    num_features: 3
+    num_samples: 3
+
+
+
+```python
+# Show number of samples for each sample:
+
+print("timesteps per sample:", time_series.num_timesteps())
+```
+
+    timesteps per sample: [4, 2, 1]
+
+
+### Examples for `{Static,Event}Samples`
+
+
+```python
+assert data.static is not None
+assert data.predictive.targets is not None
+
+static = data.static
+event = data.predictive.targets
+```
+
+
+```python
+# Return the static data as a dataframe, numpy array:
+
+display(static.dataframe())
+display(static.numpy())
+```
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>NaN</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+    array([[100. ,  -1.1],
+           [200. ,   nan],
+           [300. ,  -1.3]])
+
+
+
+```python
+# Return the event data as a dataframe, numpy array:
+
+display(event.dataframe())
+display(event.numpy())
+```
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>e_feat_0</th>
+      <th>e_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>(10, True)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>(12, False)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>(13, True)</td>
+      <td>(11, True)</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+    array([[(10, True), (10, False)],
+           [(12, False), (10, False)],
+           [(13, True), (11, True)]], dtype=object)
+
+
diff --git a/docs/user_guide/data/tutorial02_datasets.md b/docs/user_guide/data/tutorial02_datasets.md
new file mode 100644
index 00000000..56304ef5
--- /dev/null
+++ b/docs/user_guide/data/tutorial02_datasets.md
@@ -0,0 +1,1778 @@
+# Data Tutorial 02: Datasets
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial02_datasets.ipynb)
+
+This tutorial shows different TemporAI `Dataset`s.
+
+
+
+## Prepare some example data
+
+
+```python
+import pandas as pd
+import numpy as np
+
+# Some time series data:
+time_series_df = pd.DataFrame(
+    {
+        "sample_idx": ["sample_0", "sample_0", "sample_0", "sample_0", "sample_1", "sample_1", "sample_2"],
+        "time_idx": [1, 2, 3, 4, 2, 4, 9],
+        "t_feat_0": [11, 12, 13, 14, 21, 22, 31],
+        "t_feat_1": [1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 3.1],
+        "t_feat_2": [10, 20, 30, 40, 11, 21, 111],
+    }
+)
+time_series_df.set_index(keys=["sample_idx", "time_idx"], drop=True, inplace=True)
+
+# Some static data:
+static_df = pd.DataFrame(
+    {
+        "s_feat_0": [100, 200, 300],
+        "s_feat_1": [-1.1, -1.2, -1.3],
+        "s_feat_2": [0, 1, 0],
+    },
+    index=["sample_0", "sample_1", "sample_2"],
+)
+
+event_df = pd.DataFrame(
+    {
+        "e_feat_0": [(10, True), (12, False), (13, True)],
+        "e_feat_1": [(10, False), (10, False), (11, True)],
+    },
+    index=["sample_0", "sample_1", "sample_2"],
+)
+```
+
+Preview the dataframes below.
+
+
+```python
+time_series_df
+```
+
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+      <td>10</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+      <td>20</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+      <td>30</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>1.4</td>
+      <td>40</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+      <td>11</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+      <td>21</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+      <td>111</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+static_df
+```
+
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+      <th>s_feat_2</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>-1.2</td>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+event_df
+```
+
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>e_feat_0</th>
+      <th>e_feat_1</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>(10, True)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>(12, False)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>(13, True)</td>
+      <td>(11, True)</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## `CovariatesDataset`
+
+A `CovariatesDataset` contains time series and optionally static covariates only, without any predictive data
+(targets or treatments).
+
+It can be used with `preprocessing` transformations.
+
+
+```python
+from tempor.data import dataset
+```
+
+
+```python
+# Initialize a CovariatesDataset:
+data = dataset.CovariatesDataset(
+    time_series=time_series_df,
+    static=static_df,  # Optional, can be `None`.
+)
+
+data
+```
+
+
+
+
+    CovariatesDataset(
+        time_series=TimeSeriesSamples([3, *, 3]),
+        static=StaticSamples([3, 3])
+    )
+
+
+
+
+```python
+data.time_series
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+      <td>10</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+      <td>20</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+      <td>30</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>1.4</td>
+      <td>40</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+      <td>11</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+      <td>21</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+      <td>111</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.static
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+      <th>s_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>-1.2</td>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## `OneOffPredictionDataset`
+
+A `OneOffPredictionDataset` contains time series and optionally static covariates.
+
+It also needs `StaticSamples` prediction *targets* for estimators to be able to `fit` on this dataset.
+
+It can be used with `prediction.one_off` estimators. The task is to predict some one-off value for each sample.
+
+
+```python
+# Initialize a OneOffPredictionDataset:
+data = dataset.OneOffPredictionDataset(
+    time_series=time_series_df,
+    static=static_df.loc[:, :"s_feat_1"],  # Optional, can be `None`.
+    targets=static_df.loc[:, ["s_feat_2"]],  # Optional, can be `None` at inference time.
+)
+
+data
+```
+
+
+
+
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([3, *, 3]),
+        static=StaticSamples([3, 2]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([3, 1]))
+    )
+
+
+
+
+```python
+data.time_series
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+      <td>10</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+      <td>20</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+      <td>30</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>1.4</td>
+      <td>40</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+      <td>11</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+      <td>21</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+      <td>111</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.static
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>-1.2</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## `TemporalPredictionDataset`
+
+A `TemporalPredictionDataset` contains time series and optionally static covariates.
+
+It also needs `TimeSeriesSamples` prediction *targets* for estimators to be able to `fit` on this dataset.
+
+It can be used with `prediction.temporal` estimators. The task is to predict some time series for each sample.
+
+
+```python
+# Initialize a TemporalPredictionDataset:
+data = dataset.TemporalPredictionDataset(
+    time_series=time_series_df.loc[:, :"t_feat_1"],
+    static=static_df,  # Optional, can be `None`.
+    targets=time_series_df.loc[:, ["t_feat_2"]],  # Optional, can be `None` at inference time.
+)
+
+data
+```
+
+
+
+
+    TemporalPredictionDataset(
+        time_series=TimeSeriesSamples([3, *, 2]),
+        static=StaticSamples([3, 3]),
+        predictive=TemporalPredictionTaskData(
+            targets=TimeSeriesSamples([3, *, 1])
+        )
+    )
+
+
+
+
+```python
+data.time_series
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>1.4</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.static
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+      <th>s_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>-1.2</td>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>10</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>20</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>30</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>40</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>11</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>21</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>111</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## `TimeToEventAnalysisDataset`
+
+A `TimeToEventAnalysisDataset` contains time series and optionally static covariates.
+
+It also needs `EventSamples` prediction *targets* for estimators to be able to `fit` on this dataset.
+
+It can be used with `time_to_event` estimators. The task is to predict risk scores for each sample.
+
+
+```python
+# Initialize a TimeToEventAnalysisDataset:
+data = dataset.TimeToEventAnalysisDataset(
+    time_series=time_series_df,
+    static=static_df,  # Optional, can be `None`.
+    targets=event_df,  # Optional, can be `None` at inference time.
+)
+
+data
+```
+
+
+
+
+    TimeToEventAnalysisDataset(
+        time_series=TimeSeriesSamples([3, *, 3]),
+        static=StaticSamples([3, 3]),
+        predictive=TimeToEventAnalysisTaskData(targets=EventSamples([3, 2]))
+    )
+
+
+
+
+```python
+data.time_series
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+      <td>10</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+      <td>20</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+      <td>30</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>1.4</td>
+      <td>40</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+      <td>11</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+      <td>21</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+      <td>111</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.static
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+      <th>s_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>-1.2</td>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">EventSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>e_feat_0</th>
+      <th>e_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>(10, True)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>(12, False)</td>
+      <td>(10, False)</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>(13, True)</td>
+      <td>(11, True)</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## `OneOffTreatmentEffectsDataset`
+
+A `OneOffTreatmentEffectsDataset` contains time series and optionally static covariates.
+
+It also needs `TimeSeriesSamples` prediction *targets* and `EventSamples` treatments
+for estimators to be able to `fit` on this dataset.
+
+It can be used with `treatments.one_off` estimators.
+The task is to predict a time series counterfactual outcome based on a one-off treatment event.
+
+
+```python
+# Initialize a TimeToEventAnalysisDataset:
+data = dataset.OneOffTreatmentEffectsDataset(
+    time_series=time_series_df.loc[:, :"t_feat_1"],
+    static=static_df,  # Optional, can be `None`.
+    targets=time_series_df.loc[:, ["t_feat_2"]],  # Optional, can be `None` at inference time.
+    treatments=event_df.loc[:, ["e_feat_0"]],
+)
+
+data
+```
+
+
+
+
+    OneOffTreatmentEffectsDataset(
+        time_series=TimeSeriesSamples([3, *, 2]),
+        static=StaticSamples([3, 3]),
+        predictive=OneOffTreatmentEffectsTaskData(
+            targets=TimeSeriesSamples([3, *, 1]),
+            treatments=EventSamples([3, 1])
+        )
+    )
+
+
+
+
+```python
+data.time_series
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+      <th>t_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+      <td>1.1</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+      <td>1.2</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+      <td>1.3</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+      <td>1.4</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+      <td>2.1</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+      <td>2.2</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+      <td>3.1</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.static
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+      <th>s_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>-1.2</td>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>10</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>20</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>30</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>40</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>11</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>21</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>111</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.predictive.treatments
+```
+
+
+
+
+<p><span style="font-family: monospace;">EventSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>e_feat_0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>(10, True)</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>(12, False)</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>(13, True)</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+## `TemporalTreatmentEffectsDataset`
+
+A `TemporalTreatmentEffectsDataset` contains time series and optionally static covariates.
+
+It also needs `TimeSeriesSamples` prediction *targets* and `TimeSeriesSamples` treatments
+for estimators to be able to `fit` on this dataset.
+
+It can be used with `treatments.temporal` estimators.
+The task is to predict a time series counterfactual outcome based on a time series treatment.
+
+
+```python
+# Initialize a TimeToEventAnalysisDataset:
+data = dataset.TemporalTreatmentEffectsDataset(
+    time_series=time_series_df.loc[:, :"t_feat_0"],
+    static=static_df,  # Optional, can be `None`.
+    targets=time_series_df.loc[:, ["t_feat_1"]],  # Optional, can be `None` at inference time.
+    treatments=time_series_df.loc[:, ["t_feat_2"]],
+)
+
+data
+```
+
+
+
+
+    TemporalTreatmentEffectsDataset(
+        time_series=TimeSeriesSamples([3, *, 1]),
+        static=StaticSamples([3, 3]),
+        predictive=TemporalTreatmentEffectsTaskData(
+            targets=TimeSeriesSamples([3, *, 1]),
+            treatments=TimeSeriesSamples([3, *, 1])
+        )
+    )
+
+
+
+
+```python
+data.time_series
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>11</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>12</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>13</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>14</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>21</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>22</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>31</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.static
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>s_feat_0</th>
+      <th>s_feat_1</th>
+      <th>s_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>sample_0</th>
+      <td>100</td>
+      <td>-1.1</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>sample_1</th>
+      <td>200</td>
+      <td>-1.2</td>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <td>300</td>
+      <td>-1.3</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>1.1</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>1.2</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>1.3</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>1.4</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>2.1</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>2.2</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>3.1</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+```python
+data.predictive.treatments
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>t_feat_2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="4" valign="top">sample_0</th>
+      <th>1</th>
+      <td>10</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>20</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>30</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>40</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">sample_1</th>
+      <th>2</th>
+      <td>11</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>21</td>
+    </tr>
+    <tr>
+      <th>sample_2</th>
+      <th>9</th>
+      <td>111</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
diff --git a/docs/user_guide/data/tutorial03_dataloaders.md b/docs/user_guide/data/tutorial03_dataloaders.md
new file mode 100644
index 00000000..378e3969
--- /dev/null
+++ b/docs/user_guide/data/tutorial03_dataloaders.md
@@ -0,0 +1,2823 @@
+# Data Tutorial 03: Data loaders
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial03_dataloaders.ipynb)
+
+This tutorial shows TemporAI `Dataloader`s.
+
+
+
+## `Dataloader` class
+
+A TemporAI `Dataloader` implements a `load()` method which returns a TemporAI dataset.
+
+`Dataloader`s are useful to load in some custom datasets, having done the necessary preprocessing,
+perhaps user-configured.
+
+Below is an example of `SineDataLoader`.
+
+
+```python
+from tempor.utils.dataloaders import SineDataLoader
+
+# The DataLoader class:
+SineDataLoader
+```
+
+
+
+
+    tempor.utils.dataloaders.sine.SineDataLoader
+
+
+
+The constructor of the `Dataloader` can take various keyword arguments - this is where the user may customize the data
+preprocessing etc.
+
+
+```python
+# Initialize.
+
+sine_dataloader = SineDataLoader(
+    no=80,  # Here, number of samples.
+    seq_len=5,  # Here, time series sequence length.
+    # ...
+)
+
+sine_dataloader
+```
+
+
+
+
+    <tempor.utils.dataloaders.sine.SineDataLoader at 0x7f8155017fa0>
+
+
+
+
+```python
+# Load the Dataset:
+data = sine_dataloader.load()
+
+print(type(data))
+
+data
+```
+
+    <class 'tempor.data.dataset.OneOffPredictionDataset'>
+
+
+
+
+
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([80, *, 5]),
+        static=StaticSamples([80, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([80, 1]))
+    )
+
+
+
+
+```python
+data.time_series
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-0.151203</td>
+      <td>0.206110</td>
+      <td>0.783078</td>
+      <td>0.768667</td>
+      <td>0.957344</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.679518</td>
+      <td>0.785370</td>
+      <td>0.913243</td>
+      <td>0.999923</td>
+      <td>0.973799</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.997174</td>
+      <td>0.999603</td>
+      <td>0.985913</td>
+      <td>0.784278</td>
+      <td>0.730349</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.561921</td>
+      <td>0.749235</td>
+      <td>0.996514</td>
+      <td>0.218111</td>
+      <td>0.291970</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>-0.297606</td>
+      <td>0.150635</td>
+      <td>0.944377</td>
+      <td>-0.445537</td>
+      <td>-0.224335</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">79</th>
+      <th>0</th>
+      <td>0.999730</td>
+      <td>0.101680</td>
+      <td>-0.976039</td>
+      <td>-0.999547</td>
+      <td>-0.715265</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.803590</td>
+      <td>0.577241</td>
+      <td>-0.696389</td>
+      <td>-0.897416</td>
+      <td>-0.312411</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.269220</td>
+      <td>0.903914</td>
+      <td>-0.188132</td>
+      <td>-0.586595</td>
+      <td>0.160840</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>-0.378464</td>
+      <td>0.997443</td>
+      <td>0.381883</td>
+      <td>-0.139366</td>
+      <td>0.597849</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>-0.866853</td>
+      <td>0.833703</td>
+      <td>0.826536</td>
+      <td>0.340273</td>
+      <td>0.900138</td>
+    </tr>
+  </tbody>
+</table>
+<p>400 rows × 5 columns</p>
+</div>
+
+
+
+
+```python
+data.static
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.374540</td>
+      <td>0.950714</td>
+      <td>0.731994</td>
+      <td>0.598658</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.156019</td>
+      <td>0.155995</td>
+      <td>0.058084</td>
+      <td>0.866176</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.601115</td>
+      <td>0.708073</td>
+      <td>0.020584</td>
+      <td>0.969910</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.832443</td>
+      <td>0.212339</td>
+      <td>0.181825</td>
+      <td>0.183405</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.304242</td>
+      <td>0.524756</td>
+      <td>0.431945</td>
+      <td>0.291229</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>75</th>
+      <td>0.051682</td>
+      <td>0.531355</td>
+      <td>0.540635</td>
+      <td>0.637430</td>
+    </tr>
+    <tr>
+      <th>76</th>
+      <td>0.726091</td>
+      <td>0.975852</td>
+      <td>0.516300</td>
+      <td>0.322956</td>
+    </tr>
+    <tr>
+      <th>77</th>
+      <td>0.795186</td>
+      <td>0.270832</td>
+      <td>0.438971</td>
+      <td>0.078456</td>
+    </tr>
+    <tr>
+      <th>78</th>
+      <td>0.025351</td>
+      <td>0.962648</td>
+      <td>0.835980</td>
+      <td>0.695974</td>
+    </tr>
+    <tr>
+      <th>79</th>
+      <td>0.408953</td>
+      <td>0.173294</td>
+      <td>0.156437</td>
+      <td>0.250243</td>
+    </tr>
+  </tbody>
+</table>
+<p>80 rows × 4 columns</p>
+</div>
+
+
+
+
+```python
+data.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>75</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>76</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>77</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>78</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>79</th>
+      <td>1</td>
+    </tr>
+  </tbody>
+</table>
+<p>80 rows × 1 columns</p>
+</div>
+
+
+
+## Provided `Dataloader`s
+
+TemporAI comes with a number of dataloaders, see below.
+
+
+```python
+# Display information about each dataloader's default loaded dataset.
+
+from tempor.utils.dataloaders import all_dataloaders
+
+from IPython.display import display
+
+for dataloader_cls in all_dataloaders:
+    print(f"\n{'-' * 80}\n")
+
+    print(f"{dataloader_cls.__name__} loads the following dataset:\n")
+    data = dataloader_cls().load()
+    print(data)
+
+    print("This contains:", end="\n\n")
+
+    print("time_series:")
+    display(data.time_series)
+    if data.static is not None:
+        print("static:")
+        display(data.static)
+    if data.predictive.targets is not None:
+        print("predictive.targets:")
+        display(data.predictive.targets)
+    if data.predictive.treatments is not None:
+        print("predictive.treatments:")
+        display(data.predictive.treatments)
+```
+
+    
+    --------------------------------------------------------------------------------
+    
+    DummyTemporalPredictionDataLoader loads the following dataset:
+    
+    TemporalPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 3]),
+        predictive=TemporalPredictionTaskData(
+            targets=TimeSeriesSamples([100, *, 2])
+        )
+    )
+    This contains:
+    
+    time_series:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>NaN</td>
+      <td>0.893763</td>
+      <td>NaN</td>
+      <td>NaN</td>
+      <td>1.047522</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1.257931</td>
+      <td>2.172271</td>
+      <td>2.226089</td>
+      <td>2.360713</td>
+      <td>1.981578</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>2.247657</td>
+      <td>0.853397</td>
+      <td>2.525946</td>
+      <td>3.213647</td>
+      <td>2.897191</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>3.396456</td>
+      <td>5.386071</td>
+      <td>3.721545</td>
+      <td>2.503248</td>
+      <td>3.517212</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>4.387812</td>
+      <td>3.365264</td>
+      <td>5.612532</td>
+      <td>5.573375</td>
+      <td>4.767746</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>12</th>
+      <td>12.654769</td>
+      <td>14.810888</td>
+      <td>12.914859</td>
+      <td>NaN</td>
+      <td>12.818675</td>
+    </tr>
+    <tr>
+      <th>13</th>
+      <td>13.418815</td>
+      <td>12.135655</td>
+      <td>12.481295</td>
+      <td>13.336797</td>
+      <td>13.696168</td>
+    </tr>
+    <tr>
+      <th>14</th>
+      <td>13.785503</td>
+      <td>14.431228</td>
+      <td>15.193174</td>
+      <td>17.551818</td>
+      <td>14.464249</td>
+    </tr>
+    <tr>
+      <th>15</th>
+      <td>15.344934</td>
+      <td>15.916966</td>
+      <td>14.368132</td>
+      <td>15.965113</td>
+      <td>15.419334</td>
+    </tr>
+    <tr>
+      <th>16</th>
+      <td>16.033907</td>
+      <td>15.162631</td>
+      <td>17.338485</td>
+      <td>17.007235</td>
+      <td>17.034645</td>
+    </tr>
+  </tbody>
+</table>
+<p>1547 rows × 5 columns</p>
+</div>
+
+
+    static:
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.753423</td>
+      <td>3.239284</td>
+      <td>0.995587</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.829240</td>
+      <td>3.175298</td>
+      <td>0.770566</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.674581</td>
+      <td>3.229741</td>
+      <td>1.302317</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.584040</td>
+      <td>3.234011</td>
+      <td>1.594861</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.501552</td>
+      <td>3.211027</td>
+      <td>0.639503</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>0.680235</td>
+      <td>3.287749</td>
+      <td>0.705369</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>0.788814</td>
+      <td>3.313229</td>
+      <td>1.318394</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>0.589116</td>
+      <td>3.268607</td>
+      <td>1.646737</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0.551060</td>
+      <td>3.268599</td>
+      <td>0.998024</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>0.716501</td>
+      <td>3.254501</td>
+      <td>1.047537</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 3 columns</p>
+</div>
+
+
+    predictive.targets:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-1.433570</td>
+      <td>0.714861</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>-0.600733</td>
+      <td>2.744446</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.622874</td>
+      <td>1.816995</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>1.879785</td>
+      <td>4.981217</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>2.477957</td>
+      <td>5.932101</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>12</th>
+      <td>10.736462</td>
+      <td>13.415872</td>
+    </tr>
+    <tr>
+      <th>13</th>
+      <td>11.617465</td>
+      <td>15.103293</td>
+    </tr>
+    <tr>
+      <th>14</th>
+      <td>12.858327</td>
+      <td>16.105966</td>
+    </tr>
+    <tr>
+      <th>15</th>
+      <td>13.652358</td>
+      <td>16.148926</td>
+    </tr>
+    <tr>
+      <th>16</th>
+      <td>14.442286</td>
+      <td>17.567963</td>
+    </tr>
+  </tbody>
+</table>
+<p>1547 rows × 2 columns</p>
+</div>
+
+
+    
+    --------------------------------------------------------------------------------
+    
+    DummyTemporalTreatmentEffectsDataLoader loads the following dataset:
+    
+    TemporalTreatmentEffectsDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 3]),
+        predictive=TemporalTreatmentEffectsTaskData(
+            targets=TimeSeriesSamples([100, *, 2]),
+            treatments=TimeSeriesSamples([100, *, 2])
+        )
+    )
+    This contains:
+    
+    time_series:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>NaN</td>
+      <td>0.893763</td>
+      <td>NaN</td>
+      <td>NaN</td>
+      <td>1.047522</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1.257931</td>
+      <td>2.172271</td>
+      <td>2.226089</td>
+      <td>2.360713</td>
+      <td>1.981578</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>2.247657</td>
+      <td>0.853397</td>
+      <td>2.525946</td>
+      <td>3.213647</td>
+      <td>2.897191</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>3.396456</td>
+      <td>5.386071</td>
+      <td>3.721545</td>
+      <td>2.503248</td>
+      <td>3.517212</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>4.387812</td>
+      <td>3.365264</td>
+      <td>5.612532</td>
+      <td>5.573375</td>
+      <td>4.767746</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>12</th>
+      <td>12.654769</td>
+      <td>14.810888</td>
+      <td>12.914859</td>
+      <td>NaN</td>
+      <td>12.818675</td>
+    </tr>
+    <tr>
+      <th>13</th>
+      <td>13.418815</td>
+      <td>12.135655</td>
+      <td>12.481295</td>
+      <td>13.336797</td>
+      <td>13.696168</td>
+    </tr>
+    <tr>
+      <th>14</th>
+      <td>13.785503</td>
+      <td>14.431228</td>
+      <td>15.193174</td>
+      <td>17.551818</td>
+      <td>14.464249</td>
+    </tr>
+    <tr>
+      <th>15</th>
+      <td>15.344934</td>
+      <td>15.916966</td>
+      <td>14.368132</td>
+      <td>15.965113</td>
+      <td>15.419334</td>
+    </tr>
+    <tr>
+      <th>16</th>
+      <td>16.033907</td>
+      <td>15.162631</td>
+      <td>17.338485</td>
+      <td>17.007235</td>
+      <td>17.034645</td>
+    </tr>
+  </tbody>
+</table>
+<p>1547 rows × 5 columns</p>
+</div>
+
+
+    static:
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.753423</td>
+      <td>3.239284</td>
+      <td>0.995587</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.829240</td>
+      <td>3.175298</td>
+      <td>0.770566</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.674581</td>
+      <td>3.229741</td>
+      <td>1.302317</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.584040</td>
+      <td>3.234011</td>
+      <td>1.594861</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.501552</td>
+      <td>3.211027</td>
+      <td>0.639503</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>0.680235</td>
+      <td>3.287749</td>
+      <td>0.705369</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>0.788814</td>
+      <td>3.313229</td>
+      <td>1.318394</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>0.589116</td>
+      <td>3.268607</td>
+      <td>1.646737</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0.551060</td>
+      <td>3.268599</td>
+      <td>0.998024</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>0.716501</td>
+      <td>3.254501</td>
+      <td>1.047537</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 3 columns</p>
+</div>
+
+
+    predictive.targets:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-1.433570</td>
+      <td>0.714861</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>-0.600733</td>
+      <td>2.744446</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.622874</td>
+      <td>1.816995</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>1.879785</td>
+      <td>4.981217</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>2.477957</td>
+      <td>5.932101</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>12</th>
+      <td>10.736462</td>
+      <td>13.415872</td>
+    </tr>
+    <tr>
+      <th>13</th>
+      <td>11.617465</td>
+      <td>15.103293</td>
+    </tr>
+    <tr>
+      <th>14</th>
+      <td>12.858327</td>
+      <td>16.105966</td>
+    </tr>
+    <tr>
+      <th>15</th>
+      <td>13.652358</td>
+      <td>16.148926</td>
+    </tr>
+    <tr>
+      <th>16</th>
+      <td>14.442286</td>
+      <td>17.567963</td>
+    </tr>
+  </tbody>
+</table>
+<p>1547 rows × 2 columns</p>
+</div>
+
+
+    predictive.treatments:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-1.433570</td>
+      <td>0.714861</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>-0.600733</td>
+      <td>2.744446</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.622874</td>
+      <td>1.816995</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>1.879785</td>
+      <td>4.981217</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>2.477957</td>
+      <td>5.932101</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>12</th>
+      <td>10.736462</td>
+      <td>13.415872</td>
+    </tr>
+    <tr>
+      <th>13</th>
+      <td>11.617465</td>
+      <td>15.103293</td>
+    </tr>
+    <tr>
+      <th>14</th>
+      <td>12.858327</td>
+      <td>16.105966</td>
+    </tr>
+    <tr>
+      <th>15</th>
+      <td>13.652358</td>
+      <td>16.148926</td>
+    </tr>
+    <tr>
+      <th>16</th>
+      <td>14.442286</td>
+      <td>17.567963</td>
+    </tr>
+  </tbody>
+</table>
+<p>1547 rows × 2 columns</p>
+</div>
+
+
+    
+    --------------------------------------------------------------------------------
+    
+    GoogleStocksDataLoader loads the following dataset:
+    
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([50, *, 5]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([50, 1]))
+    )
+    This contains:
+    
+    time_series:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>Open</th>
+      <th>High</th>
+      <th>Low</th>
+      <th>Close</th>
+      <th>Volume</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0.875000</th>
+      <td>0.661264</td>
+      <td>0.652789</td>
+      <td>0.677836</td>
+      <td>0.696887</td>
+      <td>0.185147</td>
+    </tr>
+    <tr>
+      <th>0.886364</th>
+      <td>0.667446</td>
+      <td>0.716935</td>
+      <td>0.731552</td>
+      <td>0.748318</td>
+      <td>0.150912</td>
+    </tr>
+    <tr>
+      <th>0.897727</th>
+      <td>0.751374</td>
+      <td>0.784055</td>
+      <td>0.800261</td>
+      <td>0.791407</td>
+      <td>0.140203</td>
+    </tr>
+    <tr>
+      <th>0.909091</th>
+      <td>0.785577</td>
+      <td>0.838572</td>
+      <td>0.831813</td>
+      <td>0.832628</td>
+      <td>0.244291</td>
+    </tr>
+    <tr>
+      <th>0.920455</th>
+      <td>0.885578</td>
+      <td>0.879778</td>
+      <td>0.900782</td>
+      <td>0.889539</td>
+      <td>0.413625</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">9</th>
+      <th>0.806818</th>
+      <td>0.642857</td>
+      <td>0.647974</td>
+      <td>0.649153</td>
+      <td>0.639975</td>
+      <td>0.625178</td>
+    </tr>
+    <tr>
+      <th>0.818182</th>
+      <td>0.687362</td>
+      <td>0.757221</td>
+      <td>0.741200</td>
+      <td>0.789788</td>
+      <td>0.333141</td>
+    </tr>
+    <tr>
+      <th>0.829545</th>
+      <td>0.756044</td>
+      <td>0.732512</td>
+      <td>0.772230</td>
+      <td>0.732379</td>
+      <td>0.120629</td>
+    </tr>
+    <tr>
+      <th>0.840909</th>
+      <td>0.710852</td>
+      <td>0.687907</td>
+      <td>0.721525</td>
+      <td>0.713076</td>
+      <td>0.101900</td>
+    </tr>
+    <tr>
+      <th>0.875000</th>
+      <td>0.661264</td>
+      <td>0.652789</td>
+      <td>0.677836</td>
+      <td>0.696887</td>
+      <td>0.185147</td>
+    </tr>
+  </tbody>
+</table>
+<p>500 rows × 5 columns</p>
+</div>
+
+
+    predictive.targets:
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>out</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.710852</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.756044</td>
+    </tr>
+    <tr>
+      <th>10</th>
+      <td>0.564835</td>
+    </tr>
+    <tr>
+      <th>11</th>
+      <td>0.557005</td>
+    </tr>
+    <tr>
+      <th>12</th>
+      <td>0.552061</td>
+    </tr>
+    <tr>
+      <th>13</th>
+      <td>0.510852</td>
+    </tr>
+    <tr>
+      <th>14</th>
+      <td>0.451786</td>
+    </tr>
+    <tr>
+      <th>15</th>
+      <td>0.421704</td>
+    </tr>
+    <tr>
+      <th>16</th>
+      <td>0.387225</td>
+    </tr>
+    <tr>
+      <th>17</th>
+      <td>0.345879</td>
+    </tr>
+    <tr>
+      <th>18</th>
+      <td>0.286951</td>
+    </tr>
+    <tr>
+      <th>19</th>
+      <td>0.332143</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.687362</td>
+    </tr>
+    <tr>
+      <th>20</th>
+      <td>0.205906</td>
+    </tr>
+    <tr>
+      <th>21</th>
+      <td>0.286676</td>
+    </tr>
+    <tr>
+      <th>22</th>
+      <td>0.247939</td>
+    </tr>
+    <tr>
+      <th>23</th>
+      <td>0.492445</td>
+    </tr>
+    <tr>
+      <th>24</th>
+      <td>0.767858</td>
+    </tr>
+    <tr>
+      <th>25</th>
+      <td>0.810440</td>
+    </tr>
+    <tr>
+      <th>26</th>
+      <td>0.697940</td>
+    </tr>
+    <tr>
+      <th>27</th>
+      <td>0.597390</td>
+    </tr>
+    <tr>
+      <th>28</th>
+      <td>0.390659</td>
+    </tr>
+    <tr>
+      <th>29</th>
+      <td>0.385989</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.642857</td>
+    </tr>
+    <tr>
+      <th>30</th>
+      <td>0.361401</td>
+    </tr>
+    <tr>
+      <th>31</th>
+      <td>0.370879</td>
+    </tr>
+    <tr>
+      <th>32</th>
+      <td>0.388325</td>
+    </tr>
+    <tr>
+      <th>33</th>
+      <td>0.393819</td>
+    </tr>
+    <tr>
+      <th>34</th>
+      <td>0.389149</td>
+    </tr>
+    <tr>
+      <th>35</th>
+      <td>0.359753</td>
+    </tr>
+    <tr>
+      <th>36</th>
+      <td>0.399038</td>
+    </tr>
+    <tr>
+      <th>37</th>
+      <td>0.378984</td>
+    </tr>
+    <tr>
+      <th>38</th>
+      <td>0.225962</td>
+    </tr>
+    <tr>
+      <th>39</th>
+      <td>0.099863</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.628297</td>
+    </tr>
+    <tr>
+      <th>40</th>
+      <td>0.131181</td>
+    </tr>
+    <tr>
+      <th>41</th>
+      <td>0.000000</td>
+    </tr>
+    <tr>
+      <th>42</th>
+      <td>0.054121</td>
+    </tr>
+    <tr>
+      <th>43</th>
+      <td>0.062088</td>
+    </tr>
+    <tr>
+      <th>44</th>
+      <td>0.204533</td>
+    </tr>
+    <tr>
+      <th>45</th>
+      <td>0.163049</td>
+    </tr>
+    <tr>
+      <th>46</th>
+      <td>0.166072</td>
+    </tr>
+    <tr>
+      <th>47</th>
+      <td>0.186126</td>
+    </tr>
+    <tr>
+      <th>48</th>
+      <td>0.233929</td>
+    </tr>
+    <tr>
+      <th>49</th>
+      <td>0.246566</td>
+    </tr>
+    <tr>
+      <th>5</th>
+      <td>0.671978</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>0.704808</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>0.684753</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>0.684753</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>0.607281</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+    
+    --------------------------------------------------------------------------------
+    
+    PBCDataLoader loads the following dataset:
+    
+    TimeToEventAnalysisDataset(
+        time_series=TimeSeriesSamples([312, *, 14]),
+        static=StaticSamples([312, 1]),
+        predictive=TimeToEventAnalysisTaskData(targets=EventSamples([312, 1]))
+    )
+    This contains:
+    
+    time_series:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>drug</th>
+      <th>ascites</th>
+      <th>hepatomegaly</th>
+      <th>spiders</th>
+      <th>edema</th>
+      <th>histologic</th>
+      <th>serBilir</th>
+      <th>serChol</th>
+      <th>albumin</th>
+      <th>alkaline</th>
+      <th>SGOT</th>
+      <th>platelets</th>
+      <th>prothrombin</th>
+      <th>age</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="2" valign="top">1</th>
+      <th>0.569489</th>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>3.0</td>
+      <td>3.281890</td>
+      <td>0.000000</td>
+      <td>-0.894575</td>
+      <td>0.195532</td>
+      <td>-1.485263</td>
+      <td>-0.529101</td>
+      <td>0.136768</td>
+      <td>0.248058</td>
+    </tr>
+    <tr>
+      <th>1.095170</th>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>3.0</td>
+      <td>2.015877</td>
+      <td>-0.469461</td>
+      <td>-1.570646</td>
+      <td>0.285613</td>
+      <td>0.195488</td>
+      <td>-0.456022</td>
+      <td>0.813132</td>
+      <td>0.248058</td>
+    </tr>
+    <tr>
+      <th rowspan="3" valign="top">2</th>
+      <th>5.319790</th>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>2.0</td>
+      <td>0.172710</td>
+      <td>-0.658914</td>
+      <td>-1.431455</td>
+      <td>-0.605844</td>
+      <td>-0.442126</td>
+      <td>-1.395605</td>
+      <td>0.339677</td>
+      <td>1.292856</td>
+    </tr>
+    <tr>
+      <th>6.261636</th>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>2.0</td>
+      <td>-0.013468</td>
+      <td>-0.603657</td>
+      <td>-1.172958</td>
+      <td>-0.512364</td>
+      <td>-0.046806</td>
+      <td>-1.259888</td>
+      <td>0.339677</td>
+      <td>1.292856</td>
+    </tr>
+    <tr>
+      <th>7.266455</th>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>1.0</td>
+      <td>2.0</td>
+      <td>0.098239</td>
+      <td>0.000000</td>
+      <td>-1.312149</td>
+      <td>-0.443529</td>
+      <td>0.293680</td>
+      <td>-1.364286</td>
+      <td>0.339677</td>
+      <td>1.292856</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">312</th>
+      <th>1.045888</th>
+      <td>1.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>2.0</td>
+      <td>2.0</td>
+      <td>3.672865</td>
+      <td>3.319599</td>
+      <td>0.059878</td>
+      <td>1.385274</td>
+      <td>0.986129</td>
+      <td>-1.103291</td>
+      <td>1.624769</td>
+      <td>-1.962482</td>
+    </tr>
+    <tr>
+      <th>1.867265</th>
+      <td>1.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>2.0</td>
+      <td>1.0</td>
+      <td>2.350998</td>
+      <td>2.901224</td>
+      <td>-0.099197</td>
+      <td>0.916176</td>
+      <td>0.641817</td>
+      <td>-0.998892</td>
+      <td>1.354223</td>
+      <td>-1.962482</td>
+    </tr>
+    <tr>
+      <th>2.921367</th>
+      <td>1.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>0.694010</td>
+      <td>-0.066873</td>
+      <td>0.338261</td>
+      <td>0.327254</td>
+      <td>0.552551</td>
+      <td>-0.894494</td>
+      <td>0.474950</td>
+      <td>-1.962482</td>
+    </tr>
+    <tr>
+      <th>3.425145</th>
+      <td>1.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>0.340271</td>
+      <td>0.000000</td>
+      <td>-0.377580</td>
+      <td>0.251620</td>
+      <td>0.016956</td>
+      <td>-0.466462</td>
+      <td>-0.066141</td>
+      <td>-1.962482</td>
+    </tr>
+    <tr>
+      <th>3.989158</th>
+      <td>1.0</td>
+      <td>0.0</td>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>0.0</td>
+      <td>1.0</td>
+      <td>0.507832</td>
+      <td>2.017110</td>
+      <td>0.795603</td>
+      <td>0.622990</td>
+      <td>0.169983</td>
+      <td>-0.351624</td>
+      <td>-0.133778</td>
+      <td>-1.962482</td>
+    </tr>
+  </tbody>
+</table>
+<p>1945 rows × 14 columns</p>
+</div>
+
+
+    static:
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>sex</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>1</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>5</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>308</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>309</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>310</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>311</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>312</th>
+      <td>0.0</td>
+    </tr>
+  </tbody>
+</table>
+<p>312 rows × 1 columns</p>
+</div>
+
+
+    predictive.targets:
+
+
+
+<p><span style="font-family: monospace;">EventSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>status</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>1</th>
+      <td>(0.569488555470374, True)</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>(14.1523381885883, False)</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>(0.7365020260650499, True)</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>(0.27653050049282957, True)</td>
+    </tr>
+    <tr>
+      <th>5</th>
+      <td>(4.12057824991786, False)</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>308</th>
+      <td>(4.98850071186069, False)</td>
+    </tr>
+    <tr>
+      <th>309</th>
+      <td>(4.55317051801555, False)</td>
+    </tr>
+    <tr>
+      <th>310</th>
+      <td>(4.4025846019056, False)</td>
+    </tr>
+    <tr>
+      <th>311</th>
+      <td>(4.12879202716022, False)</td>
+    </tr>
+    <tr>
+      <th>312</th>
+      <td>(3.98915781404008, False)</td>
+    </tr>
+  </tbody>
+</table>
+<p>312 rows × 1 columns</p>
+</div>
+
+
+    
+    --------------------------------------------------------------------------------
+    
+    PKPDDataLoader loads the following dataset:
+    
+    Generating simple PKPD dataset with random seed 100...
+    OneOffTreatmentEffectsDataset(
+        time_series=TimeSeriesSamples([40, *, 2]),
+        predictive=OneOffTreatmentEffectsTaskData(
+            targets=TimeSeriesSamples([40, *, 1]),
+            treatments=EventSamples([40, 1])
+        )
+    )
+    This contains:
+    
+    time_series:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>k_in</th>
+      <th>p</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-0.781441</td>
+      <td>-0.245827</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>-1.001889</td>
+      <td>-0.541524</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>-1.070862</td>
+      <td>-0.589326</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>-1.425115</td>
+      <td>-1.065485</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>-1.841006</td>
+      <td>-1.542429</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">39</th>
+      <th>5</th>
+      <td>0.959902</td>
+      <td>-0.690057</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>1.683426</td>
+      <td>-0.128967</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>2.233045</td>
+      <td>0.637906</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>1.645018</td>
+      <td>1.056963</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>0.333051</td>
+      <td>1.048721</td>
+    </tr>
+  </tbody>
+</table>
+<p>400 rows × 2 columns</p>
+</div>
+
+
+    predictive.targets:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>y</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-0.197049</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.020346</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>-0.281120</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>-0.483934</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>-0.947253</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">39</th>
+      <th>5</th>
+      <td>-1.418583</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>-1.495843</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>-1.193632</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>-0.850845</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>-0.431988</td>
+    </tr>
+  </tbody>
+</table>
+<p>400 rows × 1 columns</p>
+</div>
+
+
+    predictive.treatments:
+
+
+
+<p><span style="font-family: monospace;">EventSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>a</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>5</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>10</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>11</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>12</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>13</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>14</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>15</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>16</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>17</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>18</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>19</th>
+      <td>(7, False)</td>
+    </tr>
+    <tr>
+      <th>20</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>21</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>22</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>23</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>24</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>25</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>26</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>27</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>28</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>29</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>30</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>31</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>32</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>33</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>34</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>35</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>36</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>37</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>38</th>
+      <td>(7, True)</td>
+    </tr>
+    <tr>
+      <th>39</th>
+      <td>(7, True)</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+    
+    --------------------------------------------------------------------------------
+    
+    SineDataLoader loads the following dataset:
+    
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([100, 1]))
+    )
+    This contains:
+    
+    time_series:
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-0.019015</td>
+      <td>-0.048177</td>
+      <td>-0.108546</td>
+      <td>0.441865</td>
+      <td>0.024508</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.300030</td>
+      <td>0.364550</td>
+      <td>0.576590</td>
+      <td>0.890053</td>
+      <td>0.534722</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.587904</td>
+      <td>0.713509</td>
+      <td>0.972993</td>
+      <td>0.986179</td>
+      <td>0.892946</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.814697</td>
+      <td>0.937661</td>
+      <td>0.882158</td>
+      <td>0.692221</td>
+      <td>0.997357</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.956846</td>
+      <td>0.997797</td>
+      <td>0.349572</td>
+      <td>0.124454</td>
+      <td>0.818278</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>5</th>
+      <td>0.967121</td>
+      <td>0.126890</td>
+      <td>0.926979</td>
+      <td>0.982022</td>
+      <td>0.963113</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>0.706533</td>
+      <td>0.413329</td>
+      <td>0.569034</td>
+      <td>0.656214</td>
+      <td>0.989224</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>0.252748</td>
+      <td>0.663121</td>
+      <td>0.024381</td>
+      <td>0.050668</td>
+      <td>0.999771</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>-0.270150</td>
+      <td>0.854119</td>
+      <td>-0.528273</td>
+      <td>-0.576478</td>
+      <td>0.994586</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>-0.719177</td>
+      <td>0.969389</td>
+      <td>-0.907592</td>
+      <td>-0.957876</td>
+      <td>0.973752</td>
+    </tr>
+  </tbody>
+</table>
+<p>1000 rows × 5 columns</p>
+</div>
+
+
+    static:
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.374540</td>
+      <td>0.950714</td>
+      <td>0.731994</td>
+      <td>0.598658</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.156019</td>
+      <td>0.155995</td>
+      <td>0.058084</td>
+      <td>0.866176</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.601115</td>
+      <td>0.708073</td>
+      <td>0.020584</td>
+      <td>0.969910</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.832443</td>
+      <td>0.212339</td>
+      <td>0.181825</td>
+      <td>0.183405</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.304242</td>
+      <td>0.524756</td>
+      <td>0.431945</td>
+      <td>0.291229</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>0.118165</td>
+      <td>0.696737</td>
+      <td>0.628943</td>
+      <td>0.877472</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>0.735071</td>
+      <td>0.803481</td>
+      <td>0.282035</td>
+      <td>0.177440</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>0.750615</td>
+      <td>0.806835</td>
+      <td>0.990505</td>
+      <td>0.412618</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0.372018</td>
+      <td>0.776413</td>
+      <td>0.340804</td>
+      <td>0.930757</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>0.858413</td>
+      <td>0.428994</td>
+      <td>0.750871</td>
+      <td>0.754543</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 4 columns</p>
+</div>
+
+
+    predictive.targets:
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 1 columns</p>
+</div>
+
+
diff --git a/docs/user_guide/data/tutorial04_data_splitting.md b/docs/user_guide/data/tutorial04_data_splitting.md
new file mode 100644
index 00000000..9276100a
--- /dev/null
+++ b/docs/user_guide/data/tutorial04_data_splitting.md
@@ -0,0 +1,101 @@
+# Data Tutorial 04: Data splitting
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial04_data_splitting.ipynb)
+
+This tutorial shows how a `Dataset` can be split or KFold-ed.
+
+
+
+## 1. Splitting data
+
+
+```python
+# Get a dataset.
+
+from tempor.utils.dataloaders import SineDataLoader
+
+sine_dataset = SineDataLoader().load()
+
+sine_dataset
+```
+
+
+
+
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([100, 1]))
+    )
+
+
+
+The method `train_test_split` (same API as [sklearn.model_selection.train_test_split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html)) can be used to split the dataset into train and test sets.
+
+
+```python
+sine_dataset_train, sine_dataset_test = sine_dataset.train_test_split(test_size=0.2)
+
+print("Training set:")
+print(sine_dataset_train)
+print("Test set:")
+print(sine_dataset_test)
+```
+
+    Training set:
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([80, *, 5]),
+        static=StaticSamples([80, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([80, 1]))
+    )
+    Test set:
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([20, *, 5]),
+        static=StaticSamples([20, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([20, 1]))
+    )
+
+
+## 2. Using `KFold` and other `sklearn` splitter classes
+
+Any of the [sklearn splitter classes](https://scikit-learn.org/stable/modules/classes.html#splitter-classes) can be used with `Dataset` `split` method.
+
+The following example illustrates this with the [`KFold` splitter](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.KFold.html#sklearn.model_selection.KFold):
+
+
+```python
+from sklearn.model_selection import KFold
+
+kfold = KFold(n_splits=5, random_state=123, shuffle=True)
+
+splits = list(sine_dataset.split(splitter=kfold))
+
+print(len(splits))
+```
+
+    5
+
+
+
+```python
+train_set_0, test_set_0 = splits[0]
+
+print("0th split training set:")
+print(sine_dataset_train)
+print("0th split test set:")
+print(sine_dataset_test)
+```
+
+    0th split training set:
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([80, *, 5]),
+        static=StaticSamples([80, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([80, 1]))
+    )
+    0th split test set:
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([20, *, 5]),
+        static=StaticSamples([20, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([20, 1]))
+    )
+
+
diff --git a/docs/user_guide/extending/index.md b/docs/user_guide/extending/index.md
new file mode 100644
index 00000000..669f2c81
--- /dev/null
+++ b/docs/user_guide/extending/index.md
@@ -0,0 +1,11 @@
+# Tutorials: Extending
+
+This section contains tutorials on extending TemporAI (e.g. adding your own models).
+
+
+```{toctree}
+:glob:
+:maxdepth: 2
+
+*
+```
diff --git a/docs/user_guide/extending/tutorial01_custom_plugin.md b/docs/user_guide/extending/tutorial01_custom_plugin.md
new file mode 100644
index 00000000..657422b7
--- /dev/null
+++ b/docs/user_guide/extending/tutorial01_custom_plugin.md
@@ -0,0 +1,453 @@
+# Extending TemporAI Tutorial 01: Writing a Custom Plugin
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial01_custom_plugin.ipynb)
+
+This tutorial shows how to extend TemporAI by wring a custom plugin.
+
+
+
+## Writing a Custom `Plugin` 101
+
+In order to write a custom plugin for TemporAI, you need to do the following:
+1. Inherit from the appropriate **base class** for the type of plugin you are writing.
+2. Implement the **methods** the plugin needs.
+3. **Register** the plugin with TemporAI.
+
+We will go through an example in this tutorial.
+
+### 1. Inherit from the appropriate **base class** for the type of plugin you are writing.
+
+You need to find which type (category) of plugin you are writing.
+
+A summary of different plugin categories is available in the 
+[README](https://github.com/vanderschaarlab/temporai/blob/main/README.md#-methods).
+
+You can also view all the different plugin categories as so:
+
+
+```python
+from tempor.plugins import plugin_loader
+
+plugin_categories = plugin_loader.list_categories()
+
+list(plugin_categories.keys())
+```
+
+
+
+
+    ['prediction.one_off.classification',
+     'prediction.one_off.regression',
+     'prediction.temporal.classification',
+     'prediction.temporal.regression',
+     'preprocessing.imputation.static',
+     'preprocessing.imputation.temporal',
+     'preprocessing.nop',
+     'preprocessing.scaling.static',
+     'preprocessing.scaling.temporal',
+     'time_to_event',
+     'treatments.one_off.regression',
+     'treatments.temporal.classification',
+     'treatments.temporal.regression']
+
+
+
+Remember you can also see the existing plugins and how they correspond to different categories, as follows:
+
+
+```python
+all_plugins = plugin_loader.list()
+
+from rich.pretty import pprint  # For prettifying the print output only.
+
+pprint(all_plugins, indent_guides=True)
+```
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'prediction'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'cde_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'laplace_ode_classifier'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'laplace_ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'cde_regressor'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_regressor'</span><span style="font-weight: bold">]}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'preprocessing'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'imputation'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'static'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'static_tabular_imputer'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ffill'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_tabular_imputer'</span>, <span style="color: #008000; text-decoration-color: #008000">'bfill'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'nop'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'nop_transformer'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'scaling'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'static'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'static_minmax_scaler'</span>, <span style="color: #008000; text-decoration-color: #008000">'static_standard_scaler'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ts_minmax_scaler'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_standard_scaler'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'time_to_event'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ts_coxph'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_xgb'</span>, <span style="color: #008000; text-decoration-color: #008000">'dynamic_deephit'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'treatments'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'synctwin_regressor'</span><span style="font-weight: bold">]}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_regressor'</span><span style="font-weight: bold">]}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>
+<span style="font-weight: bold">}</span>
+</pre>
+
+
+
+Let's say you would like to write a plugin of category `"prediction.one_off.classification"`.
+
+You can find which base class you need to inherit from as follows.
+
+
+```python
+plugin_categories = plugin_loader.list_categories()
+
+print("Base classes for all categories:")
+pprint(plugin_categories, indent_guides=False)
+
+print("Base class you need:")
+print(plugin_categories["prediction.one_off.classification"])
+```
+
+    Base classes for all categories:
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">{</span>
+    <span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>: <span style="font-weight: bold">&lt;</span><span style="color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold">class</span><span style="color: #000000; text-decoration-color: #000000"> </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.regression'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.prediction.one_off.regression.BaseOneOffRegressor'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'prediction.temporal.classification'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.prediction.temporal.classification.BaseTemporalClassifier'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'prediction.temporal.regression'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.prediction.temporal.regression.BaseTemporalRegressor'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'preprocessing.imputation.static'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.preprocessing.imputation._base.BaseImputer'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'preprocessing.imputation.temporal'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.preprocessing.imputation._base.BaseImputer'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'preprocessing.nop'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.core._base_transformer.BaseTransformer'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'preprocessing.scaling.static'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.preprocessing.scaling._base.BaseScaler'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'preprocessing.scaling.temporal'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.preprocessing.scaling._base.BaseScaler'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'time_to_event'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.time_to_event.BaseTimeToEventAnalysis'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'treatments.one_off.regression'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.treatments.one_off._base.BaseOneOffTreatmentEffects'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'treatments.temporal.classification'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.treatments.temporal._base.BaseTemporalTreatmentEffects'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #000000; text-decoration-color: #000000">    </span><span style="color: #008000; text-decoration-color: #008000">'treatments.temporal.regression'</span><span style="color: #000000; text-decoration-color: #000000">: &lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.treatments.temporal._base.BaseTemporalTreatmentEffects'</span><span style="font-weight: bold">&gt;</span>
+<span style="font-weight: bold">}</span>
+</pre>
+
+
+
+    Base class you need:
+    <class 'tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier'>
+
+
+You can then find the class in the TemporAI source code, to see its method signatures etc.
+
+### 2. Implement the **methods** the plugin needs.
+
+Different category plugins have different methods that need to be implemented, but the key methods are:
+* `_fit()` where you provide your implementation of the fitting (training).
+* `_predict()` where you provide your implementation of the prediction (inference).
+* `_transform()` where you provide your implementation of data transformation (for preprocessing plugins).
+
+Classification-related plugins also have `_predict_proba()` and treatment effects plugins have `_predict_counterfactuals()`.
+
+Note that these methods have a preceding underscore `_`, and are different from the corresponding "public" methods
+without the underscore (e.g `fit()`). When extending, you need to implement the `_<...>` method,
+and the corresponding "public" method in TemporAI is what the user of your plugin will call.
+The "public" methods also do various necessary validation and other checks behind the scenes.
+
+If you haven't implemented some required method for the plugin, Python will notify you by raising an exception when you
+attempt to instantiate your plugin (see [Python `abc`](https://docs.python.org/3/library/abc.html)).
+
+
+In our example case, you will need to implement the following methods for `BaseOneOffClassifier`:
+
+```python
+from tempor.plugins.prediction.one_off.classification import BaseOneOffClassifier
+
+class MyPlugin(BaseOneOffClassifier):
+    # The initializer:
+    def __init__(self, **params) -> None:
+        ...
+
+    # The _fit implementation.
+    def _fit(self, data: dataset.BaseDataset, *args, **kwargs):
+        ...
+
+    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+        ...
+
+    def _predict_proba(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+        ...
+    
+    @staticmethod
+    def hyperparameter_space(*args, **kwargs):
+        # This method is not currently used in TemporAI (it will be used once AutoML component is implemented).
+        # For now, you may just return an empty list.
+        ...
+``` 
+
+### 3. **Register** the plugin with TemporAI.
+
+Registering your plugin with TemporAI is very simple, you need to use the `register_plugin` decorator,
+as shown in the example below.
+
+You will need to specify the `name` of your plugin and its `category` in the decorator.
+
+```python
+from tempor.plugins.core import register_plugin
+
+@register_plugin(name="my_plugin", category="prediction.one_off.classification")
+class MyPlugin(BaseOneOffClassifier):
+    ...
+```
+
+### Note on `__init__` parameters (arguments)
+
+You will also need to define the input parameters (arguments) that will be passed into your plugin's `__init__` in the
+following way:
+
+```python
+import dataclasses
+
+# 1. Write dataclass with your __init__ parameters:
+@dataclasses.dataclass
+class MyPluginParams:
+    # Specify the parameter, data type and default value as below:
+    lr: float = 0.001
+    batch_size: int = 100
+
+class MyPlugin(BaseOneOffClassifier):
+    # 2. Set the `ParamsDefinition` class variable in your plugin to this dataclass.
+    ParamsDefinition = MyPluginParams
+    
+    def __init__(self, **params) -> None:
+        # 3. Call the parent __init__ as so.
+        super().__init__(**params)
+
+        # 4. You will now be able to access these in your class like so:
+        print(self.params.lr)
+        print(self.params.batch_size) 
+
+
+# 5. The user will then be able to specify the arguments as necessary when initializing your plugin:
+model = MyPlugin(batch_size=22)
+```
+
+
+### Putting it all together
+
+Now putting this together in an example of a one-off classifier plugin that always returns `1`s.
+
+
+```python
+import dataclasses
+
+import numpy as np
+
+from tempor.plugins.core import register_plugin
+from tempor.data import dataset, samples
+from tempor.plugins.prediction.one_off.classification import BaseOneOffClassifier
+
+
+@dataclasses.dataclass
+class MyClassifierParams:
+    some_parameter: int = 1
+    other_parameter: float = 0.5
+
+
+@register_plugin(name="my_classifier", category="prediction.one_off.classification")
+class MyClassifierClassifier(BaseOneOffClassifier):
+    ParamsDefinition = MyClassifierParams
+
+    def __init__(self, **param) -> None:
+        super().__init__(**param)
+
+    def _fit(self, data: dataset.BaseDataset, *args, **kwargs):
+        """Does nothing."""
+        return self  # Fit method needs to return `self`.
+
+    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+        """Always returns 1"""
+
+        assert data.predictive.targets is not None
+        preds = np.ones_like(data.predictive.targets.numpy())
+
+        return samples.StaticSamples.from_numpy(preds, dtype=int)
+
+    def _predict_proba(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+        """Always returns 1.0"""
+
+        assert data.predictive.targets is not None
+        preds = np.ones_like(data.predictive.targets.numpy())
+
+        return samples.StaticSamples.from_numpy(preds, dtype=float)
+
+    @staticmethod
+    def hyperparameter_space(*args, **kwargs):
+        return []
+```
+
+We now see our plugin in TemporAI:
+
+
+```python
+from tempor.plugins import plugin_loader
+
+all_plugins = plugin_loader.list()
+
+pprint(all_plugins, indent_guides=True)
+
+my_classifier_found = "my_classifier" in all_plugins["prediction"]["one_off"]["classification"]
+print(f"`my_classifier` plugin found in the category 'prediction.one_off.classification': {my_classifier_found}")
+assert my_classifier_found
+```
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'prediction'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'cde_classifier'</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'ode_classifier'</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'nn_classifier'</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'laplace_ode_classifier'</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'my_classifier'</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'laplace_ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'cde_regressor'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_regressor'</span><span style="font-weight: bold">]}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'preprocessing'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'imputation'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'static'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'static_tabular_imputer'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ffill'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_tabular_imputer'</span>, <span style="color: #008000; text-decoration-color: #008000">'bfill'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'nop'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'nop_transformer'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'scaling'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'static'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'static_minmax_scaler'</span>, <span style="color: #008000; text-decoration-color: #008000">'static_standard_scaler'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ts_minmax_scaler'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_standard_scaler'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'time_to_event'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ts_coxph'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_xgb'</span>, <span style="color: #008000; text-decoration-color: #008000">'dynamic_deephit'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'treatments'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'synctwin_regressor'</span><span style="font-weight: bold">]}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_regressor'</span><span style="font-weight: bold">]}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>
+<span style="font-weight: bold">}</span>
+</pre>
+
+
+
+    `my_classifier` plugin found in the category 'prediction.one_off.classification': True
+
+
+The plugin can be used as normal.
+
+
+```python
+# Get the plugin.
+
+my_classifier = plugin_loader.get("prediction.one_off.classification.my_classifier")
+
+print(my_classifier)
+```
+
+    MyClassifierClassifier(
+        name='my_classifier',
+        category='prediction.one_off.classification',
+        params={'some_parameter': 1, 'other_parameter': 0.5}
+    )
+
+
+
+```python
+# Fit and predict on some data.
+
+from tempor.utils.dataloaders import SineDataLoader
+
+dataset = SineDataLoader(random_state=42).load()
+
+my_classifier.fit(dataset)
+
+print("Prediction:")
+my_classifier.predict(dataset)
+```
+
+    Prediction:
+
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>feat_0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>1</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 1 columns</p>
+</div>
+
+
+
diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md
new file mode 100644
index 00000000..cc6d9edf
--- /dev/null
+++ b/docs/user_guide/index.md
@@ -0,0 +1,13 @@
+# User Guide
+
+We provide a user guide in the form of a series of tutorials which can also be run as notebooks.
+
+
+```{toctree}
+:glob:
+:maxdepth: 1
+
+Tutorials: Data <data/index>
+Tutorials: Usage <usage/index>
+Tutorials: Extending <extending/index>
+```
diff --git a/docs/user_guide/usage/index.md b/docs/user_guide/usage/index.md
new file mode 100644
index 00000000..49ac580c
--- /dev/null
+++ b/docs/user_guide/usage/index.md
@@ -0,0 +1,11 @@
+# Tutorials: Usage
+
+This section provides the main usage tutorials for TemporAI.
+
+
+```{toctree}
+:glob:
+:maxdepth: 2
+
+*
+```
diff --git a/docs/user_guide/usage/tutorial01_plugins.md b/docs/user_guide/usage/tutorial01_plugins.md
new file mode 100644
index 00000000..00f649ee
--- /dev/null
+++ b/docs/user_guide/usage/tutorial01_plugins.md
@@ -0,0 +1,204 @@
+# User Guide Tutorial 01: Plugins
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial01_plugins.ipynb)
+
+This tutorial shows how to load TemporAI estimators (a.k.a. models), which are `Plugin`s.
+
+
+
+## Loading a `Plugin`
+
+All estimators (a.k.a. models) in TemporAI are implemented as `Plugin`s, for ease of extending the library.
+
+Each `Plugin` has two plugin-specific attributes: its `name` and `category`.
+
+You can load a plugin in two ways:
+* From python module (file),
+* From API.
+
+From its python module (file):
+
+
+```python
+# Example of loading the prediction.one_off.classification.nn_classifier plugin from the module:
+
+from tempor.plugins.prediction.one_off.classification.plugin_nn_classifier import NeuralNetClassifier
+
+nn_classifier = NeuralNetClassifier(n_iter=100)
+
+print(f"Plugin class:\n{NeuralNetClassifier}\n")
+print(f"Plugin instance:\n{nn_classifier}")
+```
+
+    Plugin class:
+    <class 'tempor.plugins.prediction.one_off.classification.plugin_nn_classifier.NeuralNetClassifier'>
+    
+    Plugin instance:
+    NeuralNetClassifier(
+        name='nn_classifier',
+        category='prediction.one_off.classification',
+        params={
+            'n_static_units_hidden': 100,
+            'n_static_layers_hidden': 2,
+            'n_temporal_units_hidden': 102,
+            'n_temporal_layers_hidden': 2,
+            'n_iter': 100,
+            'mode': 'RNN',
+            'n_iter_print': 10,
+            'batch_size': 100,
+            'lr': 0.001,
+            'weight_decay': 0.001,
+            'window_size': 1,
+            'device': None,
+            'dataloader_sampler': None,
+            'dropout': 0.0,
+            'nonlin': 'relu',
+            'random_state': 0,
+            'clipping_value': 1,
+            'patience': 20,
+            'train_ratio': 0.8
+        }
+    )
+
+
+Or from the plugin API, as below.
+
+Note the `tempor.plugins.plugin_loader` object - this allows loading plugins by API.
+
+
+```python
+from tempor.plugins import plugin_loader
+
+# ^ Import the `plugin_loader`.
+
+nn_classifier_cls = plugin_loader.get_class("prediction.one_off.classification.nn_classifier")
+# ^ Get the plugin class from API by the fully-qualified plugin name.
+#   The fully-qualified plugin name is "<PLUGIN CATEGORY>.<PLUGIN NAME>".
+
+nn_classifier = nn_classifier_cls(n_iter=100)
+
+print(f"Plugin class:\n{nn_classifier_cls}\n")
+print(f"Plugin instance:\n{nn_classifier}")
+```
+
+    Plugin class:
+    <class 'tempor.plugins.prediction.one_off.classification.plugin_nn_classifier.NeuralNetClassifier'>
+    
+    Plugin instance:
+    NeuralNetClassifier(
+        name='nn_classifier',
+        category='prediction.one_off.classification',
+        params={
+            'n_static_units_hidden': 100,
+            'n_static_layers_hidden': 2,
+            'n_temporal_units_hidden': 102,
+            'n_temporal_layers_hidden': 2,
+            'n_iter': 100,
+            'mode': 'RNN',
+            'n_iter_print': 10,
+            'batch_size': 100,
+            'lr': 0.001,
+            'weight_decay': 0.001,
+            'window_size': 1,
+            'device': None,
+            'dataloader_sampler': None,
+            'dropout': 0.0,
+            'nonlin': 'relu',
+            'random_state': 0,
+            'clipping_value': 1,
+            'patience': 20,
+            'train_ratio': 0.8
+        }
+    )
+
+
+You can also get the plugin instance directly (rather than the class) from the API, as follows: 
+
+## Listing all available `Plugin`s
+
+You can list all `Plugin`s currently available in TemporAI as follows:
+
+
+```python
+from tempor.plugins import plugin_loader
+
+# Use plugin_loader.list():
+all_plugins = plugin_loader.list()
+
+# Displaying using pretty print here for clarity:
+from rich.pretty import pprint
+
+pprint(all_plugins, indent_guides=False)
+```
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">{</span>
+    <span style="color: #008000; text-decoration-color: #008000">'prediction'</span>: <span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span>
+            <span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'cde_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'laplace_ode_classifier'</span><span style="font-weight: bold">]</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'laplace_ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'cde_regressor'</span><span style="font-weight: bold">]</span>
+        <span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_regressor'</span><span style="font-weight: bold">]}</span>
+    <span style="font-weight: bold">}</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'preprocessing'</span>: <span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'imputation'</span>: <span style="font-weight: bold">{</span>
+            <span style="color: #008000; text-decoration-color: #008000">'static'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'static_tabular_imputer'</span><span style="font-weight: bold">]</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ffill'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_tabular_imputer'</span>, <span style="color: #008000; text-decoration-color: #008000">'bfill'</span><span style="font-weight: bold">]</span>
+        <span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'nop'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'nop_transformer'</span><span style="font-weight: bold">]</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'scaling'</span>: <span style="font-weight: bold">{</span>
+            <span style="color: #008000; text-decoration-color: #008000">'static'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'static_minmax_scaler'</span>, <span style="color: #008000; text-decoration-color: #008000">'static_standard_scaler'</span><span style="font-weight: bold">]</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ts_minmax_scaler'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_standard_scaler'</span><span style="font-weight: bold">]</span>
+        <span style="font-weight: bold">}</span>
+    <span style="font-weight: bold">}</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'time_to_event'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ts_coxph'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_xgb'</span>, <span style="color: #008000; text-decoration-color: #008000">'dynamic_deephit'</span><span style="font-weight: bold">]</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'treatments'</span>: <span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'synctwin_regressor'</span><span style="font-weight: bold">]}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_regressor'</span><span style="font-weight: bold">]}</span>
+    <span style="font-weight: bold">}</span>
+<span style="font-weight: bold">}</span>
+</pre>
+
+
+
+Note that plugin categories are hierarchical (nested).
+
+To quickly view the plugin fully qualified name (`fqn`) for all plugins, use `plugin_loader.list_fqns()`.
+
+These are the names to use in `plugin_loader.{get,get_class}` calls.
+
+
+```python
+plugin_loader.list_fqns()
+```
+
+
+
+
+    ['prediction.one_off.classification.cde_classifier',
+     'prediction.one_off.classification.ode_classifier',
+     'prediction.one_off.classification.nn_classifier',
+     'prediction.one_off.classification.laplace_ode_classifier',
+     'prediction.one_off.regression.laplace_ode_regressor',
+     'prediction.one_off.regression.nn_regressor',
+     'prediction.one_off.regression.ode_regressor',
+     'prediction.one_off.regression.cde_regressor',
+     'prediction.temporal.classification.seq2seq_classifier',
+     'prediction.temporal.regression.seq2seq_regressor',
+     'preprocessing.imputation.static.static_tabular_imputer',
+     'preprocessing.imputation.temporal.ffill',
+     'preprocessing.imputation.temporal.ts_tabular_imputer',
+     'preprocessing.imputation.temporal.bfill',
+     'preprocessing.nop.nop_transformer',
+     'preprocessing.scaling.static.static_minmax_scaler',
+     'preprocessing.scaling.static.static_standard_scaler',
+     'preprocessing.scaling.temporal.ts_minmax_scaler',
+     'preprocessing.scaling.temporal.ts_standard_scaler',
+     'time_to_event.ts_coxph',
+     'time_to_event.ts_xgb',
+     'time_to_event.dynamic_deephit',
+     'treatments.one_off.regression.synctwin_regressor',
+     'treatments.temporal.classification.crn_classifier',
+     'treatments.temporal.regression.crn_regressor']
+
+
+
diff --git a/docs/user_guide/usage/tutorial02_imputation.md b/docs/user_guide/usage/tutorial02_imputation.md
new file mode 100644
index 00000000..211c6b18
--- /dev/null
+++ b/docs/user_guide/usage/tutorial02_imputation.md
@@ -0,0 +1,646 @@
+# User Guide Tutorial 02: Preprocessing › Imputation
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial02_imputation.ipynb)
+
+This tutorial shows how to use TemporAI `preprocessing.imputation` plugins.
+
+
+
+## All `preprocessing.imputation` plugins
+
+To see all the relevant plugins:
+
+
+```python
+from tempor.plugins import plugin_loader
+
+plugin_loader.list()["preprocessing"]["imputation"]
+```
+
+
+
+
+    {'static': ['static_tabular_imputer'],
+     'temporal': ['ffill', 'ts_tabular_imputer', 'bfill']}
+
+
+
+## Using a static data imputation plugin
+
+
+```python
+from tempor.utils.dataloaders import SineDataLoader
+from tempor.plugins import plugin_loader
+
+dataset = SineDataLoader(with_missing=True, random_state=42).load()
+print(dataset)
+
+model = plugin_loader.get("preprocessing.imputation.static.static_tabular_imputer", static_imputer="mean")
+print(model)
+```
+
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([100, 1]))
+    )
+    StaticTabularImputer(
+        name='static_tabular_imputer',
+        category='preprocessing.imputation.static',
+        params={
+            'imputer': 'ice',
+            'random_state': 0,
+            'imputer_params': {'random_state': 0}
+        }
+    )
+
+
+
+```python
+# Note missingness in static data.
+
+print("Missing value count:", dataset.static.dataframe().isnull().sum().sum())  # type: ignore
+
+dataset.static
+```
+
+    Missing value count: 40
+
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.374540</td>
+      <td>0.950714</td>
+      <td>0.731994</td>
+      <td>0.598658</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.156019</td>
+      <td>0.155995</td>
+      <td>0.058084</td>
+      <td>0.866176</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.601115</td>
+      <td>0.708073</td>
+      <td>0.020584</td>
+      <td>0.969910</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.832443</td>
+      <td>NaN</td>
+      <td>0.181825</td>
+      <td>0.183405</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.304242</td>
+      <td>0.524756</td>
+      <td>0.431945</td>
+      <td>0.291229</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>NaN</td>
+      <td>0.696737</td>
+      <td>0.628943</td>
+      <td>NaN</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>0.735071</td>
+      <td>0.803481</td>
+      <td>0.282035</td>
+      <td>NaN</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>0.750615</td>
+      <td>0.806835</td>
+      <td>0.990505</td>
+      <td>0.412618</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0.372018</td>
+      <td>0.776413</td>
+      <td>0.340804</td>
+      <td>0.930757</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>0.858413</td>
+      <td>0.428994</td>
+      <td>0.750871</td>
+      <td>0.754543</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 4 columns</p>
+</div>
+
+
+
+
+```python
+# Note no more missingness in static data.
+
+dataset = model.fit_transform(dataset)  # Or call fit() then transform().
+
+print("Missing value count:", dataset.static.dataframe().isnull().sum().sum())  # type: ignore
+
+dataset.static
+```
+
+    Missing value count: 0
+
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.374540</td>
+      <td>0.950714</td>
+      <td>0.731994</td>
+      <td>0.598658</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.156019</td>
+      <td>0.155995</td>
+      <td>0.058084</td>
+      <td>0.866176</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.601115</td>
+      <td>0.708073</td>
+      <td>0.020584</td>
+      <td>0.969910</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.832443</td>
+      <td>0.450438</td>
+      <td>0.181825</td>
+      <td>0.183405</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.304242</td>
+      <td>0.524756</td>
+      <td>0.431945</td>
+      <td>0.291229</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>0.498806</td>
+      <td>0.696737</td>
+      <td>0.628943</td>
+      <td>0.509994</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>0.735071</td>
+      <td>0.803481</td>
+      <td>0.282035</td>
+      <td>0.503886</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>0.750615</td>
+      <td>0.806835</td>
+      <td>0.990505</td>
+      <td>0.412618</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0.372018</td>
+      <td>0.776413</td>
+      <td>0.340804</td>
+      <td>0.930757</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>0.858413</td>
+      <td>0.428994</td>
+      <td>0.750871</td>
+      <td>0.754543</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 4 columns</p>
+</div>
+
+
+
+## Using a temporal data imputation plugin
+
+
+```python
+from tempor.utils.dataloaders import SineDataLoader
+from tempor.plugins import plugin_loader
+
+dataset = SineDataLoader(with_missing=True, random_state=42).load()
+print(dataset)
+
+model = plugin_loader.get("preprocessing.imputation.temporal.bfill")
+print(model)
+```
+
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([100, 1]))
+    )
+    BFillImputer(
+        name='bfill',
+        category='preprocessing.imputation.temporal',
+        params={}
+    )
+
+
+
+```python
+# Note missingness in temporal data.
+
+print("Missing value count:", dataset.time_series.dataframe().isnull().sum().sum())
+
+dataset.time_series
+```
+
+    Missing value count: 500
+
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-0.955338</td>
+      <td>0.016053</td>
+      <td>-0.995752</td>
+      <td>0.948138</td>
+      <td>0.738158</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>-0.896718</td>
+      <td>0.717189</td>
+      <td>-0.497625</td>
+      <td>0.962001</td>
+      <td>0.968258</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>-0.346466</td>
+      <td>0.999920</td>
+      <td>0.423104</td>
+      <td>0.639780</td>
+      <td>0.972469</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.393737</td>
+      <td>0.699299</td>
+      <td>0.984517</td>
+      <td>0.094046</td>
+      <td>0.749807</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.918072</td>
+      <td>-0.009290</td>
+      <td>NaN</td>
+      <td>NaN</td>
+      <td>NaN</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>5</th>
+      <td>0.904284</td>
+      <td>-0.939985</td>
+      <td>0.994099</td>
+      <td>-0.984349</td>
+      <td>0.688521</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>0.990911</td>
+      <td>-0.518593</td>
+      <td>0.908681</td>
+      <td>-0.801263</td>
+      <td>0.813486</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>0.757745</td>
+      <td>0.131791</td>
+      <td>NaN</td>
+      <td>-0.110629</td>
+      <td>0.908965</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>NaN</td>
+      <td>0.723981</td>
+      <td>0.476023</td>
+      <td>0.650082</td>
+      <td>0.971498</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>-0.288052</td>
+      <td>0.996486</td>
+      <td>0.173255</td>
+      <td>0.999008</td>
+      <td>0.998817</td>
+    </tr>
+  </tbody>
+</table>
+<p>1000 rows × 5 columns</p>
+</div>
+
+
+
+
+```python
+# Note no more missingness in temporal data.
+
+dataset = model.fit_transform(dataset)  # Or call fit() then transform().
+
+print("Missing value count:", dataset.time_series.dataframe().isnull().sum().sum())
+
+dataset.time_series
+```
+
+    Missing value count: 0
+
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-0.955338</td>
+      <td>0.016053</td>
+      <td>-0.995752</td>
+      <td>0.948138</td>
+      <td>0.738158</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>-0.896718</td>
+      <td>0.717189</td>
+      <td>-0.497625</td>
+      <td>0.962001</td>
+      <td>0.968258</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>-0.346466</td>
+      <td>0.999920</td>
+      <td>0.423104</td>
+      <td>0.639780</td>
+      <td>0.972469</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.393737</td>
+      <td>0.699299</td>
+      <td>0.984517</td>
+      <td>0.094046</td>
+      <td>0.749807</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.918072</td>
+      <td>-0.009290</td>
+      <td>-0.167662</td>
+      <td>-0.893854</td>
+      <td>-0.127538</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>5</th>
+      <td>0.904284</td>
+      <td>-0.939985</td>
+      <td>0.994099</td>
+      <td>-0.984349</td>
+      <td>0.688521</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>0.990911</td>
+      <td>-0.518593</td>
+      <td>0.908681</td>
+      <td>-0.801263</td>
+      <td>0.813486</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>0.757745</td>
+      <td>0.131791</td>
+      <td>0.476023</td>
+      <td>-0.110629</td>
+      <td>0.908965</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>-0.288052</td>
+      <td>0.723981</td>
+      <td>0.476023</td>
+      <td>0.650082</td>
+      <td>0.971498</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>-0.288052</td>
+      <td>0.996486</td>
+      <td>0.173255</td>
+      <td>0.999008</td>
+      <td>0.998817</td>
+    </tr>
+  </tbody>
+</table>
+<p>1000 rows × 5 columns</p>
+</div>
+
+
+
diff --git a/docs/user_guide/usage/tutorial03_scaling.md b/docs/user_guide/usage/tutorial03_scaling.md
new file mode 100644
index 00000000..4c1972f3
--- /dev/null
+++ b/docs/user_guide/usage/tutorial03_scaling.md
@@ -0,0 +1,856 @@
+# User Guide Tutorial 03: Preprocessing › Scaling
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial03_scaling.ipynb)
+
+This tutorial shows how to use TemporAI `preprocessing.scaling` plugins.
+
+
+
+## All `preprocessing.scaling` plugins
+
+To see all the relevant plugins:
+
+
+```python
+from tempor.plugins import plugin_loader
+
+plugin_loader.list()["preprocessing"]["scaling"]
+```
+
+
+
+
+    {'static': ['static_minmax_scaler', 'static_standard_scaler'],
+     'temporal': ['ts_minmax_scaler', 'ts_standard_scaler']}
+
+
+
+## Using a static data scaling plugin
+
+
+```python
+from tempor.utils.dataloaders import SineDataLoader
+from tempor.plugins import plugin_loader
+
+dataset = SineDataLoader(static_scale=5.0, random_state=42).load()
+print(dataset)
+
+model = plugin_loader.get("preprocessing.scaling.static.static_minmax_scaler", static_imputer="mean")
+print(model)
+```
+
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([100, 1]))
+    )
+    StaticMinMaxScaler(
+        name='static_minmax_scaler',
+        category='preprocessing.scaling.static',
+        params={}
+    )
+
+
+
+```python
+# Note the scale of static features.
+
+from IPython.display import display
+
+print("Min, max values per feature:")
+display(dataset.static.dataframe().describe().T.loc[:, ["min", "max"]])  # type: ignore
+
+dataset.static
+```
+
+    Min, max values per feature:
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>min</th>
+      <th>max</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.025308</td>
+      <td>4.818100</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.045985</td>
+      <td>4.950269</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.102922</td>
+      <td>4.952526</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.082939</td>
+      <td>4.858910</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>1.872701</td>
+      <td>4.753572</td>
+      <td>3.659970</td>
+      <td>2.993292</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.780093</td>
+      <td>0.779973</td>
+      <td>0.290418</td>
+      <td>4.330881</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>3.005575</td>
+      <td>3.540363</td>
+      <td>0.102922</td>
+      <td>4.849549</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>4.162213</td>
+      <td>1.061696</td>
+      <td>0.909125</td>
+      <td>0.917023</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>1.521211</td>
+      <td>2.623782</td>
+      <td>2.159725</td>
+      <td>1.456146</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>0.590824</td>
+      <td>3.483686</td>
+      <td>3.144714</td>
+      <td>4.387360</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>3.675355</td>
+      <td>4.017405</td>
+      <td>1.410173</td>
+      <td>0.887198</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>3.753074</td>
+      <td>4.034174</td>
+      <td>4.952526</td>
+      <td>2.063088</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>1.860090</td>
+      <td>3.882065</td>
+      <td>1.704018</td>
+      <td>4.653787</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>4.292064</td>
+      <td>2.144970</td>
+      <td>3.754355</td>
+      <td>3.772714</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 4 columns</p>
+</div>
+
+
+
+
+```python
+# Note the new scale of static features.
+
+dataset = model.fit_transform(dataset)  # Or call fit() then transform().
+
+print("Min, max values per feature:")
+display(dataset.static.dataframe().describe().T.loc[:, ["min", "max"]])  # type: ignore
+
+dataset.static
+```
+
+    Min, max values per feature:
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>min</th>
+      <th>max</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.0</td>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.0</td>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.0</td>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.0</td>
+      <td>1.0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.385452</td>
+      <td>0.959893</td>
+      <td>0.733472</td>
+      <td>0.609374</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.157483</td>
+      <td>0.149662</td>
+      <td>0.038662</td>
+      <td>0.889440</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.621823</td>
+      <td>0.712515</td>
+      <td>0.000000</td>
+      <td>0.998040</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.863151</td>
+      <td>0.207107</td>
+      <td>0.166241</td>
+      <td>0.174642</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.312115</td>
+      <td>0.525621</td>
+      <td>0.424118</td>
+      <td>0.287524</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>0.117993</td>
+      <td>0.700959</td>
+      <td>0.627225</td>
+      <td>0.901266</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>0.761570</td>
+      <td>0.809786</td>
+      <td>0.269558</td>
+      <td>0.168397</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>0.777786</td>
+      <td>0.813205</td>
+      <td>1.000000</td>
+      <td>0.414607</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0.382821</td>
+      <td>0.782190</td>
+      <td>0.330150</td>
+      <td>0.957051</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>0.890244</td>
+      <td>0.427990</td>
+      <td>0.752934</td>
+      <td>0.772571</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 4 columns</p>
+</div>
+
+
+
+## Using a temporal data scaling plugin
+
+
+```python
+from tempor.utils.dataloaders import SineDataLoader
+from tempor.plugins import plugin_loader
+
+dataset = SineDataLoader(ts_scale=5.0, random_state=42).load()
+print(dataset)
+
+model = plugin_loader.get("preprocessing.scaling.temporal.ts_standard_scaler")
+print(model)
+```
+
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([100, 1]))
+    )
+    TimeSeriesStandardScaler(
+        name='ts_standard_scaler',
+        category='preprocessing.scaling.temporal',
+        params={}
+    )
+
+
+
+```python
+# Note the scale of time series features.
+
+from IPython.display import display
+
+print("Min, max values per feature:")
+display(dataset.time_series.dataframe().describe().T.loc[:, ["min", "max"]])
+
+dataset.time_series
+```
+
+    Min, max values per feature:
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>min</th>
+      <th>max</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>-4.999519</td>
+      <td>4.999999</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>-4.999982</td>
+      <td>4.999999</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>-4.999923</td>
+      <td>4.999992</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>-4.999979</td>
+      <td>5.000000</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>-4.999970</td>
+      <td>4.999928</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-0.095075</td>
+      <td>-0.240884</td>
+      <td>-0.542729</td>
+      <td>2.209324</td>
+      <td>0.122539</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1.500152</td>
+      <td>1.822750</td>
+      <td>2.882952</td>
+      <td>4.450264</td>
+      <td>2.673609</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>2.939520</td>
+      <td>3.567547</td>
+      <td>4.864963</td>
+      <td>4.930896</td>
+      <td>4.464728</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>4.073484</td>
+      <td>4.688307</td>
+      <td>4.410788</td>
+      <td>3.461105</td>
+      <td>4.986786</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>4.784229</td>
+      <td>4.988986</td>
+      <td>1.747861</td>
+      <td>0.622269</td>
+      <td>4.091392</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>5</th>
+      <td>4.835604</td>
+      <td>0.634449</td>
+      <td>4.634897</td>
+      <td>4.910111</td>
+      <td>4.815565</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>3.532665</td>
+      <td>2.066645</td>
+      <td>2.845170</td>
+      <td>3.281070</td>
+      <td>4.946122</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>1.263739</td>
+      <td>3.315606</td>
+      <td>0.121903</td>
+      <td>0.253339</td>
+      <td>4.998853</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>-1.350749</td>
+      <td>4.270597</td>
+      <td>-2.641363</td>
+      <td>-2.882388</td>
+      <td>4.972930</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>-3.595883</td>
+      <td>4.846944</td>
+      <td>-4.537960</td>
+      <td>-4.789380</td>
+      <td>4.868760</td>
+    </tr>
+  </tbody>
+</table>
+<p>1000 rows × 5 columns</p>
+</div>
+
+
+
+
+```python
+# Note the new scale of time series features.
+
+dataset = model.fit_transform(dataset)  # Or call fit() then transform().
+
+print("Min, max values per feature:")
+display(dataset.time_series.dataframe().describe().T.loc[:, ["min", "max"]])
+
+dataset.time_series
+```
+
+    Min, max values per feature:
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>min</th>
+      <th>max</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>-1.711349</td>
+      <td>1.200819</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>-1.724449</td>
+      <td>1.239101</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>-1.734762</td>
+      <td>1.230568</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>-1.592516</td>
+      <td>1.277314</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>-1.728804</td>
+      <td>1.177170</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+      <th>2</th>
+      <th>3</th>
+      <th>4</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-0.283024</td>
+      <td>-0.314064</td>
+      <td>-0.413046</td>
+      <td>0.476436</td>
+      <td>-0.240201</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0.181555</td>
+      <td>0.297505</td>
+      <td>0.602791</td>
+      <td>1.119549</td>
+      <td>0.501140</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0.600744</td>
+      <td>0.814586</td>
+      <td>1.190527</td>
+      <td>1.257482</td>
+      <td>1.021640</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.930989</td>
+      <td>1.146729</td>
+      <td>1.055848</td>
+      <td>0.835676</td>
+      <td>1.173350</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>1.137980</td>
+      <td>1.235837</td>
+      <td>0.266196</td>
+      <td>0.020977</td>
+      <td>0.913149</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>5</th>
+      <td>1.152942</td>
+      <td>-0.054654</td>
+      <td>1.122305</td>
+      <td>1.251517</td>
+      <td>1.123594</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>0.773486</td>
+      <td>0.369785</td>
+      <td>0.591587</td>
+      <td>0.784009</td>
+      <td>1.161533</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>0.112705</td>
+      <td>0.739922</td>
+      <td>-0.215959</td>
+      <td>-0.084900</td>
+      <td>1.176857</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>-0.648715</td>
+      <td>1.022938</td>
+      <td>-1.035365</td>
+      <td>-0.984802</td>
+      <td>1.169324</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>-1.302567</td>
+      <td>1.193742</td>
+      <td>-1.597773</td>
+      <td>-1.532077</td>
+      <td>1.139052</td>
+    </tr>
+  </tbody>
+</table>
+<p>1000 rows × 5 columns</p>
+</div>
+
+
+
diff --git a/docs/user_guide/usage/tutorial04_prediction.md b/docs/user_guide/usage/tutorial04_prediction.md
new file mode 100644
index 00000000..deadc005
--- /dev/null
+++ b/docs/user_guide/usage/tutorial04_prediction.md
@@ -0,0 +1,589 @@
+# User Guide Tutorial 04: Prediction
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial04_prediction.ipynb)
+
+This tutorial shows how to use TemporAI `prediction` plugins.
+
+
+
+## All `prediction` plugins
+
+To see all the relevant plugins:
+
+
+```python
+from tempor.plugins import plugin_loader
+from rich.pretty import pprint
+
+all_prediction_plugins = plugin_loader.list()["prediction"]
+
+pprint(all_prediction_plugins, indent_guides=False)
+```
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">{</span>
+    <span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'cde_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'laplace_ode_classifier'</span><span style="font-weight: bold">]</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'laplace_ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'cde_regressor'</span><span style="font-weight: bold">]</span>
+    <span style="font-weight: bold">}</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_regressor'</span><span style="font-weight: bold">]}</span>
+<span style="font-weight: bold">}</span>
+</pre>
+
+
+
+## Using a one-off prediction plugin
+
+One-off prediction is the task of predicting a single value for each sample (may be classification or regression).
+
+
+```python
+from tempor.utils.dataloaders import SineDataLoader
+from tempor.plugins import plugin_loader
+
+dataset = SineDataLoader(random_state=42).load()
+print(dataset)
+
+model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
+print(model)
+```
+
+    OneOffPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 4]),
+        predictive=OneOffPredictionTaskData(targets=StaticSamples([100, 1]))
+    )
+    NeuralNetClassifier(
+        name='nn_classifier',
+        category='prediction.one_off.classification',
+        params={
+            'n_static_units_hidden': 100,
+            'n_static_layers_hidden': 2,
+            'n_temporal_units_hidden': 102,
+            'n_temporal_layers_hidden': 2,
+            'n_iter': 50,
+            'mode': 'RNN',
+            'n_iter_print': 10,
+            'batch_size': 100,
+            'lr': 0.001,
+            'weight_decay': 0.001,
+            'window_size': 1,
+            'device': None,
+            'dataloader_sampler': None,
+            'dropout': 0.0,
+            'nonlin': 'relu',
+            'random_state': 0,
+            'clipping_value': 1,
+            'patience': 20,
+            'train_ratio': 0.8
+        }
+    )
+
+
+
+```python
+# Targets:
+dataset.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 1 columns</p>
+</div>
+
+
+
+
+```python
+# Train.
+model.fit(dataset)
+```
+
+
+
+
+    NeuralNetClassifier(
+        name='nn_classifier',
+        category='prediction.one_off.classification',
+        params={
+            'n_static_units_hidden': 100,
+            'n_static_layers_hidden': 2,
+            'n_temporal_units_hidden': 102,
+            'n_temporal_layers_hidden': 2,
+            'n_iter': 50,
+            'mode': 'RNN',
+            'n_iter_print': 10,
+            'batch_size': 100,
+            'lr': 0.001,
+            'weight_decay': 0.001,
+            'window_size': 1,
+            'device': None,
+            'dataloader_sampler': None,
+            'dropout': 0.0,
+            'nonlin': 'relu',
+            'random_state': 0,
+            'clipping_value': 1,
+            'patience': 20,
+            'train_ratio': 0.8
+        }
+    )
+
+
+
+
+```python
+# Predict:
+
+model.predict(dataset)
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>feat_0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>1.0</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 1 columns</p>
+</div>
+
+
+
+## Using a temporal prediction plugin
+
+Temporal prediction is the task of predicting a time series for each sample (may be classification or regression).
+
+
+```python
+from tempor.utils.dataloaders import DummyTemporalPredictionDataLoader
+from tempor.plugins import plugin_loader
+
+dataset = DummyTemporalPredictionDataLoader(random_state=42, temporal_covariates_missing_prob=0.0).load()
+print(dataset)
+
+model = plugin_loader.get("prediction.temporal.regression.seq2seq_regressor", epochs=10)
+print(model)
+```
+
+    TemporalPredictionDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 3]),
+        predictive=TemporalPredictionTaskData(
+            targets=TimeSeriesSamples([100, *, 2])
+        )
+    )
+    Seq2seqRegressor(
+        name='seq2seq_regressor',
+        category='prediction.temporal.regression',
+        params={
+            'encoder_rnn_type': 'LSTM',
+            'encoder_hidden_size': 100,
+            'encoder_num_layers': 1,
+            'encoder_bias': True,
+            'encoder_dropout': 0.0,
+            'encoder_bidirectional': False,
+            'encoder_nonlinearity': None,
+            'encoder_proj_size': None,
+            'decoder_rnn_type': 'LSTM',
+            'decoder_hidden_size': 100,
+            'decoder_num_layers': 1,
+            'decoder_bias': True,
+            'decoder_dropout': 0.0,
+            'decoder_bidirectional': False,
+            'decoder_nonlinearity': None,
+            'decoder_proj_size': None,
+            'adapter_hidden_dims': [50],
+            'adapter_out_activation': 'Tanh',
+            'predictor_hidden_dims': [],
+            'predictor_out_activation': None,
+            'max_len': None,
+            'optimizer_str': 'Adam',
+            'optimizer_kwargs': {'lr': 0.01, 'weight_decay': 1e-05},
+            'batch_size': 32,
+            'epochs': 10,
+            'padding_indicator': -999.0
+        }
+    )
+
+
+
+```python
+# Targets:
+dataset.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-3.110475</td>
+      <td>-3.566948</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1.528495</td>
+      <td>-0.653673</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>2.275307</td>
+      <td>-0.695371</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>4.844060</td>
+      <td>3.469371</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>4.420301</td>
+      <td>5.147500</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>7</th>
+      <td>5.994185</td>
+      <td>6.225290</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>10.913662</td>
+      <td>5.346697</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>9.558824</td>
+      <td>7.585175</td>
+    </tr>
+    <tr>
+      <th>10</th>
+      <td>10.194430</td>
+      <td>5.795619</td>
+    </tr>
+    <tr>
+      <th>11</th>
+      <td>13.774189</td>
+      <td>8.457336</td>
+    </tr>
+  </tbody>
+</table>
+<p>1573 rows × 2 columns</p>
+</div>
+
+
+
+
+```python
+# Train.
+model.fit(dataset);
+```
+
+    Preparing data for decoder training...
+    Preparing data for decoder training DONE.
+    === Training stage: 1. Train encoder ===
+    Epoch: 0, Loss: 68.64124755859375
+    Epoch: 1, Loss: 35.37634651184082
+    Epoch: 2, Loss: 21.269977149963378
+    Epoch: 3, Loss: 17.418054847717286
+    Epoch: 4, Loss: 13.851956520080567
+    Epoch: 5, Loss: 8.335217247009277
+    Epoch: 6, Loss: 6.385376853942871
+    Epoch: 7, Loss: 5.112505340576172
+    Epoch: 8, Loss: 4.690582332611084
+    Epoch: 9, Loss: 4.158801422119141
+    === Training stage: 2. Train decoder ===
+    Epoch: 0, Loss: 29.110631188485222
+    Epoch: 1, Loss: 4.417614046603097
+    Epoch: 2, Loss: 3.908697671333463
+    Epoch: 3, Loss: 3.8485680392058788
+    Epoch: 4, Loss: 3.8276885692068126
+    Epoch: 5, Loss: 3.793576312405029
+    Epoch: 6, Loss: 3.749186389072364
+    Epoch: 7, Loss: 3.787092407949102
+    Epoch: 8, Loss: 3.7378030509586164
+    Epoch: 9, Loss: 3.717736062595655
+
+
+
+```python
+# Predict:
+
+model.predict(dataset, n_future_steps=10)
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>11</th>
+      <td>9.828533</td>
+      <td>8.405809</td>
+    </tr>
+    <tr>
+      <th>12</th>
+      <td>9.831676</td>
+      <td>8.414574</td>
+    </tr>
+    <tr>
+      <th>13</th>
+      <td>9.813161</td>
+      <td>8.395016</td>
+    </tr>
+    <tr>
+      <th>14</th>
+      <td>9.798813</td>
+      <td>8.378744</td>
+    </tr>
+    <tr>
+      <th>15</th>
+      <td>9.772892</td>
+      <td>8.352385</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>17</th>
+      <td>11.214181</td>
+      <td>9.754721</td>
+    </tr>
+    <tr>
+      <th>18</th>
+      <td>11.275115</td>
+      <td>9.821613</td>
+    </tr>
+    <tr>
+      <th>19</th>
+      <td>11.298031</td>
+      <td>9.848743</td>
+    </tr>
+    <tr>
+      <th>20</th>
+      <td>11.280746</td>
+      <td>9.832634</td>
+    </tr>
+    <tr>
+      <th>21</th>
+      <td>11.256371</td>
+      <td>9.801851</td>
+    </tr>
+  </tbody>
+</table>
+<p>1000 rows × 2 columns</p>
+</div>
+
+
+
diff --git a/docs/user_guide/usage/tutorial05_time_to_event.md b/docs/user_guide/usage/tutorial05_time_to_event.md
new file mode 100644
index 00000000..a821a41d
--- /dev/null
+++ b/docs/user_guide/usage/tutorial05_time_to_event.md
@@ -0,0 +1,289 @@
+# User Guide Tutorial 05: Time-to-event Analysis
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial05_time_to_event.ipynb)
+
+This tutorial shows how to use TemporAI `time_to_event` plugins.
+
+
+
+## All `time_to_event` plugins
+
+Time-to-event analysis, in the context of TemporAI, refers to models that estimate risk over time for each sample.
+This may also be referred to as survival analysis.
+
+To see all the relevant plugins:
+
+
+```python
+from tempor.plugins import plugin_loader
+
+plugin_loader.list()["time_to_event"]
+```
+
+
+
+
+    ['ts_coxph', 'ts_xgb', 'dynamic_deephit']
+
+
+
+## Using a time-to-event analysis plugin.
+
+
+```python
+from tempor.utils.dataloaders import PBCDataLoader
+from tempor.plugins import plugin_loader
+
+dataset = PBCDataLoader(random_state=42).load()
+print(dataset)
+
+model = plugin_loader.get("time_to_event.dynamic_deephit", n_iter=50)
+print(model)
+```
+
+    TimeToEventAnalysisDataset(
+        time_series=TimeSeriesSamples([312, *, 14]),
+        static=StaticSamples([312, 1]),
+        predictive=TimeToEventAnalysisTaskData(targets=EventSamples([312, 1]))
+    )
+    DynamicDeepHitTimeToEventAnalysis(
+        name='dynamic_deephit',
+        category='time_to_event',
+        params={
+            'n_iter': 50,
+            'batch_size': 100,
+            'lr': 0.001,
+            'n_layers_hidden': 1,
+            'n_units_hidden': 40,
+            'split': 100,
+            'rnn_mode': 'GRU',
+            'alpha': 0.34,
+            'beta': 0.27,
+            'sigma': 0.21,
+            'dropout': 0.06,
+            'device': 'cpu',
+            'patience': 20,
+            'output_mode': 'MLP',
+            'random_state': 0
+        }
+    )
+
+
+
+```python
+# Targets:
+dataset.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">EventSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>status</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>1</th>
+      <td>(0.569488555470374, True)</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>(14.1523381885883, False)</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>(0.7365020260650499, True)</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>(0.27653050049282957, True)</td>
+    </tr>
+    <tr>
+      <th>5</th>
+      <td>(4.12057824991786, False)</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>308</th>
+      <td>(4.98850071186069, False)</td>
+    </tr>
+    <tr>
+      <th>309</th>
+      <td>(4.55317051801555, False)</td>
+    </tr>
+    <tr>
+      <th>310</th>
+      <td>(4.4025846019056, False)</td>
+    </tr>
+    <tr>
+      <th>311</th>
+      <td>(4.12879202716022, False)</td>
+    </tr>
+    <tr>
+      <th>312</th>
+      <td>(3.98915781404008, False)</td>
+    </tr>
+  </tbody>
+</table>
+<p>312 rows × 1 columns</p>
+</div>
+
+
+
+
+```python
+# Train.
+model.fit(dataset)
+```
+
+
+
+
+    DynamicDeepHitTimeToEventAnalysis(
+        name='dynamic_deephit',
+        category='time_to_event',
+        params={
+            'n_iter': 50,
+            'batch_size': 100,
+            'lr': 0.001,
+            'n_layers_hidden': 1,
+            'n_units_hidden': 40,
+            'split': 100,
+            'rnn_mode': 'GRU',
+            'alpha': 0.34,
+            'beta': 0.27,
+            'sigma': 0.21,
+            'dropout': 0.06,
+            'device': 'cpu',
+            'patience': 20,
+            'output_mode': 'MLP',
+            'random_state': 0
+        }
+    )
+
+
+
+
+```python
+# Predict:
+
+model.predict(dataset, horizons=[0.25, 0.50, 0.75])
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>risk_score</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="3" valign="top">1</th>
+      <th>0.25</th>
+      <td>0.422643</td>
+    </tr>
+    <tr>
+      <th>0.50</th>
+      <td>0.626620</td>
+    </tr>
+    <tr>
+      <th>0.75</th>
+      <td>0.717206</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">2</th>
+      <th>0.25</th>
+      <td>0.221314</td>
+    </tr>
+    <tr>
+      <th>0.50</th>
+      <td>0.288492</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">311</th>
+      <th>0.50</th>
+      <td>0.039302</td>
+    </tr>
+    <tr>
+      <th>0.75</th>
+      <td>0.048346</td>
+    </tr>
+    <tr>
+      <th rowspan="3" valign="top">312</th>
+      <th>0.25</th>
+      <td>0.248833</td>
+    </tr>
+    <tr>
+      <th>0.50</th>
+      <td>0.390042</td>
+    </tr>
+    <tr>
+      <th>0.75</th>
+      <td>0.444533</td>
+    </tr>
+  </tbody>
+</table>
+<p>936 rows × 1 columns</p>
+</div>
+
+
+
+### Note:
+
+The current Dynamic DeepHit implementation has the following limitations:
+- Only one output feature is supported (no competing risks).
+- Risk prediction for time points beyond the last event time in the dataset may throw errors.
+
diff --git a/docs/user_guide/usage/tutorial06_treatments.md b/docs/user_guide/usage/tutorial06_treatments.md
new file mode 100644
index 00000000..d4d9e92f
--- /dev/null
+++ b/docs/user_guide/usage/tutorial06_treatments.md
@@ -0,0 +1,400 @@
+# User Guide Tutorial 06: Treatment Effects
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial06_treatments.ipynb)
+
+This tutorial shows how to use TemporAI `treatments` plugins.
+
+
+
+## All `treatments` plugins
+
+> ⚠️ The `treatments` API is preliminary and likely to change.
+
+In the treatment effects estimation task, the goal is to predict a counterfactual outcome given an alternative treatment.
+
+To see all the relevant plugins:
+
+
+```python
+from tempor.plugins import plugin_loader
+from rich.pretty import pprint
+
+all_treatments_plugins = plugin_loader.list()["treatments"]
+
+pprint(all_treatments_plugins, indent_guides=False)
+```
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">{</span>
+    <span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'synctwin_regressor'</span><span style="font-weight: bold">]}</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_regressor'</span><span style="font-weight: bold">]}</span>
+<span style="font-weight: bold">}</span>
+</pre>
+
+
+
+## Using a temporal treatment effects plugin.
+
+In this setting, the treatments are time series, and the outcomes are also time series.
+
+
+```python
+from tempor.utils.dataloaders import DummyTemporalTreatmentEffectsDataLoader
+from tempor.plugins import plugin_loader
+
+dataset = DummyTemporalTreatmentEffectsDataLoader(
+    random_state=42,
+    temporal_covariates_missing_prob=0.0,
+    temporal_treatments_n_features=1,
+    temporal_treatments_n_categories=2,
+).load()
+print(dataset)
+
+model = plugin_loader.get("treatments.temporal.regression.crn_regressor", epochs=20)
+print(model)
+```
+
+    TemporalTreatmentEffectsDataset(
+        time_series=TimeSeriesSamples([100, *, 5]),
+        static=StaticSamples([100, 3]),
+        predictive=TemporalTreatmentEffectsTaskData(
+            targets=TimeSeriesSamples([100, *, 2]),
+            treatments=TimeSeriesSamples([100, *, 1])
+        )
+    )
+    CRNTreatmentsRegressor(
+        name='crn_regressor',
+        category='treatments.temporal.regression',
+        params={
+            'encoder_rnn_type': 'LSTM',
+            'encoder_hidden_size': 100,
+            'encoder_num_layers': 1,
+            'encoder_bias': True,
+            'encoder_dropout': 0.0,
+            'encoder_bidirectional': False,
+            'encoder_nonlinearity': None,
+            'encoder_proj_size': None,
+            'decoder_rnn_type': 'LSTM',
+            'decoder_hidden_size': 100,
+            'decoder_num_layers': 1,
+            'decoder_bias': True,
+            'decoder_dropout': 0.0,
+            'decoder_bidirectional': False,
+            'decoder_nonlinearity': None,
+            'decoder_proj_size': None,
+            'adapter_hidden_dims': [50],
+            'adapter_out_activation': 'Tanh',
+            'predictor_hidden_dims': [],
+            'predictor_out_activation': None,
+            'max_len': None,
+            'optimizer_str': 'Adam',
+            'optimizer_kwargs': {'lr': 0.01, 'weight_decay': 1e-05},
+            'batch_size': 32,
+            'epochs': 20,
+            'padding_indicator': -999.0
+        }
+    )
+
+
+
+```python
+# Targets:
+dataset.predictive.targets
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+      <th>1</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>-3.110475</td>
+      <td>-3.566948</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1.528495</td>
+      <td>-0.653673</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>2.275307</td>
+      <td>-0.695371</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>4.844060</td>
+      <td>3.469371</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>4.420301</td>
+      <td>5.147500</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>7</th>
+      <td>5.994185</td>
+      <td>6.225290</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>10.913662</td>
+      <td>5.346697</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>9.558824</td>
+      <td>7.585175</td>
+    </tr>
+    <tr>
+      <th>10</th>
+      <td>10.194430</td>
+      <td>5.795619</td>
+    </tr>
+    <tr>
+      <th>11</th>
+      <td>13.774189</td>
+      <td>8.457336</td>
+    </tr>
+  </tbody>
+</table>
+<p>1573 rows × 2 columns</p>
+</div>
+
+
+
+
+```python
+# Treatments:
+dataset.predictive.treatments
+```
+
+
+
+
+<p><span style="font-family: monospace;">TimeSeriesSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th></th>
+      <th>0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th>time_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="5" valign="top">0</th>
+      <th>0</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="5" valign="top">99</th>
+      <th>7</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>8</th>
+      <td>1</td>
+    </tr>
+    <tr>
+      <th>9</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>10</th>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>11</th>
+      <td>1</td>
+    </tr>
+  </tbody>
+</table>
+<p>1573 rows × 1 columns</p>
+</div>
+
+
+
+
+```python
+# Train.
+model.fit(dataset);
+```
+
+    Preparing data for decoder training...
+    Preparing data for decoder training DONE.
+    === Training stage: 1. Train encoder ===
+    Epoch: 0, Prediction Loss: 76.050, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 76.050
+    Epoch: 1, Prediction Loss: 37.508, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 37.508
+    Epoch: 2, Prediction Loss: 21.953, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 21.953
+    Epoch: 3, Prediction Loss: 18.895, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 18.895
+    Epoch: 4, Prediction Loss: 19.568, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 19.568
+    Epoch: 5, Prediction Loss: 16.534, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 16.534
+    Epoch: 6, Prediction Loss: 12.039, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 12.039
+    Epoch: 7, Prediction Loss: 10.009, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 10.009
+    Epoch: 8, Prediction Loss: 8.666, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 8.666
+    Epoch: 9, Prediction Loss: 6.788, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 6.788
+    Epoch: 10, Prediction Loss: 5.674, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 5.674
+    Epoch: 11, Prediction Loss: 5.016, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 5.016
+    Epoch: 12, Prediction Loss: 4.605, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 4.605
+    Epoch: 13, Prediction Loss: 4.349, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 4.349
+    Epoch: 14, Prediction Loss: 4.096, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 4.096
+    Epoch: 15, Prediction Loss: 4.008, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 4.008
+    Epoch: 16, Prediction Loss: 4.159, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 4.159
+    Epoch: 17, Prediction Loss: 3.980, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.980
+    Epoch: 18, Prediction Loss: 3.904, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.904
+    Epoch: 19, Prediction Loss: 3.995, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.995
+    === Training stage: 2. Train decoder ===
+    Epoch: 0, Prediction Loss: 34.805, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 34.805
+    Epoch: 1, Prediction Loss: 13.675, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 13.675
+    Epoch: 2, Prediction Loss: 7.021, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 7.021
+    Epoch: 3, Prediction Loss: 4.086, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 4.086
+    Epoch: 4, Prediction Loss: 3.847, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.847
+    Epoch: 5, Prediction Loss: 3.795, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.795
+    Epoch: 6, Prediction Loss: 3.762, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.762
+    Epoch: 7, Prediction Loss: 3.800, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.800
+    Epoch: 8, Prediction Loss: 3.792, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.792
+    Epoch: 9, Prediction Loss: 3.703, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.703
+    Epoch: 10, Prediction Loss: 3.683, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.683
+    Epoch: 11, Prediction Loss: 3.734, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.734
+    Epoch: 12, Prediction Loss: 3.690, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.690
+    Epoch: 13, Prediction Loss: 3.728, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.728
+    Epoch: 14, Prediction Loss: 3.694, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.694
+    Epoch: 15, Prediction Loss: 3.647, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.647
+    Epoch: 16, Prediction Loss: 3.639, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.639
+    Epoch: 17, Prediction Loss: 3.612, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.612
+    Epoch: 18, Prediction Loss: 3.633, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.633
+    Epoch: 19, Prediction Loss: 3.604, Lambda: 1.000, Treatment BR Loss: 0.000, Loss: 3.604
+
+
+
+```python
+# Predict counterfactuals:
+
+import numpy as np
+
+dataset = dataset[:5]
+
+# Define horizons for each sample.
+horizons = [tc.time_indexes()[0][len(tc.time_indexes()[0]) // 2 :] for tc in dataset.time_series]
+print("Horizons for sample 0:\n", horizons[0], end="\n\n")
+
+# Define treatment scenarios for each sample.
+treatment_scenarios = [[np.asarray([1] * len(h)), np.asarray([0] * len(h))] for h in horizons]
+print("Alternative treatment scenarios for sample 0:\n", treatment_scenarios[0], end="\n\n")
+
+# Call predict_counterfactuals.
+counterfactuals = model.predict_counterfactuals(dataset, horizons=horizons, treatment_scenarios=treatment_scenarios)
+print("Counterfactual outcomes for sample 0, given the alternative treatment scenarios:\n")
+for idx, c in enumerate(counterfactuals[0]):
+    print(f"Treatment scenario {idx}, {treatment_scenarios[0][idx]}")
+    print(c, end="\n\n")
+```
+
+    Horizons for sample 0:
+     [5, 6, 7, 8, 9, 10]
+    
+    Alternative treatment scenarios for sample 0:
+     [array([1, 1, 1, 1, 1, 1]), array([0, 0, 0, 0, 0, 0])]
+    
+    Counterfactual outcomes for sample 0, given the alternative treatment scenarios:
+    
+    Treatment scenario 0, [1 1 1 1 1 1]
+    TimeSeries() with data:
+                     0         1
+    time_idx                    
+    5         6.339921  5.118720
+    6         6.379971  5.159422
+    7         6.380514  5.159956
+    8         6.380521  5.159964
+    9         6.380521  5.159964
+    10        6.380521  5.159964
+    
+    Treatment scenario 1, [0 0 0 0 0 0]
+    TimeSeries() with data:
+                     0         1
+    time_idx                    
+    5         6.694273  5.142348
+    6         6.732183  5.177425
+    7         6.732780  5.178019
+    8         6.732789  5.178029
+    9         6.732790  5.178029
+    10        6.732790  5.178029
+    
+
+
diff --git a/docs/user_guide/usage/tutorial07_pipeline.md b/docs/user_guide/usage/tutorial07_pipeline.md
new file mode 100644
index 00000000..dea1c433
--- /dev/null
+++ b/docs/user_guide/usage/tutorial07_pipeline.md
@@ -0,0 +1,229 @@
+# User Guide Tutorial 07: Pipeline
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial07_pipeline.ipynb)
+
+This tutorial shows how to use TemporAI `Pipeline`s.
+
+
+
+## TemporAI `Pipeline`
+
+A TemporAI `Pipeline` allows you to combine multiple plugins into one;
+inspired by be [scikit-learn pipeline](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html).
+
+* All but the final plugin in the pipeline need to be data transformers (the `preprocessing` plugin category),
+* The final one must be a predictive plugin (any of the `prediction`, `time_to_event`, `treatments` plugin categories).
+
+When fitting, all the stages will be fitted, and the data will be sequentially transformed by all the preprocessing
+steps before fitting the final predictive method plugin.
+
+When predicting, the data will be again transformed by the preprocessing steps, and prediction carried out using the
+final predictive method plugin.
+
+**Note:**
+
+All pipelines follow `PipelineBase` interface, see API reference for details.
+
+## Example
+
+Below is an example of a pipeline ending with `prediction.one_off.nn_classifier`.
+
+Initializing the `Pipeline` follows the following steps.
+1. Use the `pipeline()` function to create a *pipeline class* from a list of strings denoting its steps.
+1. Instantiate the pipeline class. The initialization arguments to each component plugin can be passed as a dictionary at this step.
+1. Use the pipeline like any other TemporAI estimator (call `.fit(...)`, `.predict(...)` and so on).
+
+
+```python
+from rich.pretty import pprint  # For fancy printing only.
+```
+
+
+```python
+from tempor.plugins.pipeline import pipeline
+
+# 1. Create a pipeline class based on your desired definition of the pipeline.
+PipelineClass = pipeline(
+    # Provide plugin names for the pipeline, in order.
+    [
+        # Preprocessing (data transformer) plugins:
+        "preprocessing.imputation.temporal.bfill",
+        "preprocessing.imputation.static.static_tabular_imputer",
+        "preprocessing.imputation.temporal.ts_tabular_imputer",
+        "preprocessing.scaling.temporal.ts_minmax_scaler",
+        # Prediction plugin:
+        "prediction.one_off.classification.nn_classifier",
+    ],
+)
+print("Pipeline class:")
+print(PipelineClass)
+
+print("\nPipeline base classes (note `PipelineBase`):")
+pprint(PipelineClass.mro())
+
+pipe = PipelineClass(
+    # You can provide initialization arguments to each plugin comprising the pipeline as a dictionary, as follows:
+    {
+        "static_imputer": {"static_imputer": "ice", "random_state": 42},
+        "nn_classifier": {"n_iter": 100},
+    }
+)
+
+print("Pipeline instance:")
+pprint(pipe)
+```
+
+    Pipeline class:
+    <class 'tempor.plugins.pipeline.pipeline.<locals>.Pipeline'>
+    
+    Pipeline base classes (note `PipelineBase`):
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">[</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">&lt;</span><span style="color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold">class</span><span style="color: #000000; text-decoration-color: #000000"> </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.pipeline.pipeline.&lt;locals&gt;.Pipeline'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #000000; text-decoration-color: #000000">&lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.pipeline.PipelineBase'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #000000; text-decoration-color: #000000">&lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.prediction.one_off.classification.BaseOneOffClassifier'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #000000; text-decoration-color: #000000">&lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.core._base_predictor.BasePredictor'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #000000; text-decoration-color: #000000">&lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.core._base_estimator.BaseEstimator'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #000000; text-decoration-color: #000000">&lt;class </span><span style="color: #008000; text-decoration-color: #008000">'tempor.plugins.core._plugin.Plugin'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #000000; text-decoration-color: #000000">&lt;class </span><span style="color: #008000; text-decoration-color: #008000">'abc.ABC'</span><span style="color: #000000; text-decoration-color: #000000">&gt;,</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #000000; text-decoration-color: #000000">&lt;class </span><span style="color: #008000; text-decoration-color: #008000">'object'</span><span style="font-weight: bold">&gt;</span>
+<span style="font-weight: bold">]</span>
+</pre>
+
+
+
+    Pipeline instance:
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #800080; text-decoration-color: #800080; font-weight: bold">Pipeline</span><span style="font-weight: bold">(</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #808000; text-decoration-color: #808000">pipeline_seq</span>=<span style="color: #008000; text-decoration-color: #008000">'preprocessing.imputation.temporal.bfill-&gt;preprocessing.imputation.static.static_tabular_imputer-&gt;preprocessing.imputation.temporal.ts_tabular_imputer-&gt;preprocessing.scaling.temporal.ts_minmax_scaler-&gt;prediction.one_off.classification.nn_classifier'</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #808000; text-decoration-color: #808000">predictor_category</span>=<span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #808000; text-decoration-color: #808000">params</span>=<span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'bfill'</span>: <span style="font-weight: bold">{}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'static_tabular_imputer'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'imputer'</span>: <span style="color: #008000; text-decoration-color: #008000">'ice'</span>, <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>, <span style="color: #008000; text-decoration-color: #008000">'imputer_params'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span><span style="font-weight: bold">}}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'ts_tabular_imputer'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'imputer'</span>: <span style="color: #008000; text-decoration-color: #008000">'ice'</span>, <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>, <span style="color: #008000; text-decoration-color: #008000">'imputer_params'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span><span style="font-weight: bold">}}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'ts_minmax_scaler'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'feature_range'</span>: <span style="font-weight: bold">[</span><span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>, <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'clip'</span>: <span style="color: #ff0000; text-decoration-color: #ff0000; font-style: italic">False</span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'nn_classifier'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'n_static_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'n_static_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'n_temporal_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">102</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'n_temporal_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'n_iter'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'mode'</span>: <span style="color: #008000; text-decoration-color: #008000">'RNN'</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'n_iter_print'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">10</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'batch_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'lr'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'weight_decay'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'window_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'device'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'dataloader_sampler'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'dropout'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'nonlin'</span>: <span style="color: #008000; text-decoration-color: #008000">'relu'</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'clipping_value'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'patience'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">20</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'train_ratio'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.8</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>
+<span style="font-weight: bold">)</span>
+</pre>
+
+
+
+Using the `Pipeline`:
+
+
+```python
+from tempor.utils.dataloaders import SineDataLoader
+
+dataset = SineDataLoader(random_state=42).load()
+
+# Fit:
+pipe.fit(dataset)
+
+# Predict:
+pipe.predict(dataset)  # This will transform the data ant then predict.
+```
+
+
+
+
+<p><span style="font-family: monospace;">StaticSamples</span> with data:</p><div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>feat_0</th>
+    </tr>
+    <tr>
+      <th>sample_idx</th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>2</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>3</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>4</th>
+      <td>0.0</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>95</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>96</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>97</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>98</th>
+      <td>1.0</td>
+    </tr>
+    <tr>
+      <th>99</th>
+      <td>1.0</td>
+    </tr>
+  </tbody>
+</table>
+<p>100 rows × 1 columns</p>
+</div>
+
+
+
diff --git a/docs/user_guide/usage/tutorial08_benchmarks.md b/docs/user_guide/usage/tutorial08_benchmarks.md
new file mode 100644
index 00000000..98d17ac3
--- /dev/null
+++ b/docs/user_guide/usage/tutorial08_benchmarks.md
@@ -0,0 +1,389 @@
+# User Guide Tutorial 09: Benchmarks
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial08_benchmarks.ipynb)
+
+TemporAI provides some useful benchmarking tools in `tempor.benchmarks`, these are demonstrated here.
+
+
+
+## Using `tempor.benchmarks.benchmark_models`
+
+The `tempor.benchmarks.benchmark_models` function provides a quick way to benchmark a number of models (plugins) for
+a particular task.
+
+It takes a list of models (these may also be a `Pipeline`) and a dataset, and performs cross-validation to
+get the mean and standard deviation of the various metrics.
+
+It returns a tuple `(results_readable, results)` as below.
+
+
+```python
+from tempor.benchmarks import benchmark_models
+from tempor.utils.dataloaders import SineDataLoader
+from tempor.plugins import plugin_loader
+
+from IPython.display import display
+
+dataset = SineDataLoader(random_state=42, no=25).load()
+
+results_readable, results = benchmark_models(
+    task_type="prediction.one_off.classification",
+    tests=[
+        ("model_1", plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=10)),
+        ("model_2", plugin_loader.get("prediction.one_off.classification.ode_classifier", n_iter=100)),
+    ],
+    data=dataset,
+    n_splits=3,
+)
+
+print("Results in easily-readable format:")
+display(results_readable)
+
+print("Full results:\n")
+for model, value in results.items():
+    print(f"{model}:")
+    display(value)
+```
+
+    2023-05-14 19:18:40 | INFO     | tempor.benchmarks.benchmark:benchmark_models:91 | Test case: model_1
+    2023-05-14 19:18:42 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.68637615442276, validation loss: 0.6680578589439392
+    2023-05-14 19:18:43 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6887814402580261, validation loss: 0.6939960718154907
+    2023-05-14 19:18:43 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6906945109367371, validation loss: 0.6966598033905029
+    2023-05-14 19:18:43 | INFO     | tempor.benchmarks.benchmark:benchmark_models:91 | Test case: model_2
+    2023-05-14 19:18:48 | INFO     | tempor.models.ts_ode:_train:608 | Epoch:99| train loss: 0.6465951204299927, validation loss: 0.5632617473602295
+    2023-05-14 19:18:52 | INFO     | tempor.models.ts_ode:_train:608 | Epoch:99| train loss: 0.913261890411377, validation loss: 0.8132617473602295
+    2023-05-14 19:18:57 | INFO     | tempor.models.ts_ode:_train:608 | Epoch:99| train loss: 0.7132617831230164, validation loss: 0.8132617473602295
+
+
+    Results in easily-readable format:
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>model_1</th>
+      <th>model_2</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>aucroc</th>
+      <td>0.311 +/- 0.083</td>
+      <td>0.583 +/- 0.312</td>
+    </tr>
+    <tr>
+      <th>aucprc</th>
+      <td>0.385 +/- 0.028</td>
+      <td>0.588 +/- 0.292</td>
+    </tr>
+    <tr>
+      <th>accuracy</th>
+      <td>0.602 +/- 0.033</td>
+      <td>0.519 +/- 0.105</td>
+    </tr>
+    <tr>
+      <th>f1_score_micro</th>
+      <td>0.602 +/- 0.033</td>
+      <td>0.519 +/- 0.105</td>
+    </tr>
+    <tr>
+      <th>f1_score_macro</th>
+      <td>0.375 +/- 0.013</td>
+      <td>0.338 +/- 0.048</td>
+    </tr>
+    <tr>
+      <th>f1_score_weighted</th>
+      <td>0.453 +/- 0.04</td>
+      <td>0.361 +/- 0.116</td>
+    </tr>
+    <tr>
+      <th>kappa</th>
+      <td>0.0 +/- 0.0</td>
+      <td>0.0 +/- 0.0</td>
+    </tr>
+    <tr>
+      <th>kappa_quadratic</th>
+      <td>0.0 +/- 0.0</td>
+      <td>0.0 +/- 0.0</td>
+    </tr>
+    <tr>
+      <th>precision_micro</th>
+      <td>0.602 +/- 0.033</td>
+      <td>0.519 +/- 0.105</td>
+    </tr>
+    <tr>
+      <th>precision_macro</th>
+      <td>0.301 +/- 0.016</td>
+      <td>0.259 +/- 0.053</td>
+    </tr>
+    <tr>
+      <th>precision_weighted</th>
+      <td>0.363 +/- 0.039</td>
+      <td>0.28 +/- 0.104</td>
+    </tr>
+    <tr>
+      <th>recall_micro</th>
+      <td>0.602 +/- 0.033</td>
+      <td>0.519 +/- 0.105</td>
+    </tr>
+    <tr>
+      <th>recall_macro</th>
+      <td>0.5 +/- 0.0</td>
+      <td>0.5 +/- 0.0</td>
+    </tr>
+    <tr>
+      <th>recall_weighted</th>
+      <td>0.602 +/- 0.033</td>
+      <td>0.519 +/- 0.105</td>
+    </tr>
+    <tr>
+      <th>mcc</th>
+      <td>0.0 +/- 0.0</td>
+      <td>0.0 +/- 0.0</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+    Full results:
+    
+    model_1:
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>mean</th>
+      <th>stddev</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>aucroc</th>
+      <td>0.311111</td>
+      <td>0.083148</td>
+    </tr>
+    <tr>
+      <th>aucprc</th>
+      <td>0.385185</td>
+      <td>0.028154</td>
+    </tr>
+    <tr>
+      <th>accuracy</th>
+      <td>0.601852</td>
+      <td>0.032736</td>
+    </tr>
+    <tr>
+      <th>f1_score_micro</th>
+      <td>0.601852</td>
+      <td>0.032736</td>
+    </tr>
+    <tr>
+      <th>f1_score_macro</th>
+      <td>0.375458</td>
+      <td>0.012951</td>
+    </tr>
+    <tr>
+      <th>f1_score_weighted</th>
+      <td>0.452788</td>
+      <td>0.039572</td>
+    </tr>
+    <tr>
+      <th>kappa</th>
+      <td>0.000000</td>
+      <td>0.000000</td>
+    </tr>
+    <tr>
+      <th>kappa_quadratic</th>
+      <td>0.000000</td>
+      <td>0.000000</td>
+    </tr>
+    <tr>
+      <th>precision_micro</th>
+      <td>0.601852</td>
+      <td>0.032736</td>
+    </tr>
+    <tr>
+      <th>precision_macro</th>
+      <td>0.300926</td>
+      <td>0.016368</td>
+    </tr>
+    <tr>
+      <th>precision_weighted</th>
+      <td>0.363297</td>
+      <td>0.038647</td>
+    </tr>
+    <tr>
+      <th>recall_micro</th>
+      <td>0.601852</td>
+      <td>0.032736</td>
+    </tr>
+    <tr>
+      <th>recall_macro</th>
+      <td>0.500000</td>
+      <td>0.000000</td>
+    </tr>
+    <tr>
+      <th>recall_weighted</th>
+      <td>0.601852</td>
+      <td>0.032736</td>
+    </tr>
+    <tr>
+      <th>mcc</th>
+      <td>0.000000</td>
+      <td>0.000000</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+    model_2:
+
+
+
+<div>
+<style scoped>
+    .dataframe tbody tr th:only-of-type {
+        vertical-align: middle;
+    }
+
+    .dataframe tbody tr th {
+        vertical-align: top;
+    }
+
+    .dataframe thead th {
+        text-align: right;
+    }
+</style>
+<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+      <th>mean</th>
+      <th>stddev</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>aucroc</th>
+      <td>0.583333</td>
+      <td>0.311805</td>
+    </tr>
+    <tr>
+      <th>aucprc</th>
+      <td>0.587731</td>
+      <td>0.291568</td>
+    </tr>
+    <tr>
+      <th>accuracy</th>
+      <td>0.518519</td>
+      <td>0.105369</td>
+    </tr>
+    <tr>
+      <th>f1_score_micro</th>
+      <td>0.518519</td>
+      <td>0.105369</td>
+    </tr>
+    <tr>
+      <th>f1_score_macro</th>
+      <td>0.338162</td>
+      <td>0.047609</td>
+    </tr>
+    <tr>
+      <th>f1_score_weighted</th>
+      <td>0.360713</td>
+      <td>0.115623</td>
+    </tr>
+    <tr>
+      <th>kappa</th>
+      <td>0.000000</td>
+      <td>0.000000</td>
+    </tr>
+    <tr>
+      <th>kappa_quadratic</th>
+      <td>0.000000</td>
+      <td>0.000000</td>
+    </tr>
+    <tr>
+      <th>precision_micro</th>
+      <td>0.518519</td>
+      <td>0.105369</td>
+    </tr>
+    <tr>
+      <th>precision_macro</th>
+      <td>0.259259</td>
+      <td>0.052684</td>
+    </tr>
+    <tr>
+      <th>precision_weighted</th>
+      <td>0.279964</td>
+      <td>0.104057</td>
+    </tr>
+    <tr>
+      <th>recall_micro</th>
+      <td>0.518519</td>
+      <td>0.105369</td>
+    </tr>
+    <tr>
+      <th>recall_macro</th>
+      <td>0.500000</td>
+      <td>0.000000</td>
+    </tr>
+    <tr>
+      <th>recall_weighted</th>
+      <td>0.518519</td>
+      <td>0.105369</td>
+    </tr>
+    <tr>
+      <th>mcc</th>
+      <td>0.000000</td>
+      <td>0.000000</td>
+    </tr>
+  </tbody>
+</table>
+</div>
+
+
+## Supported tasks
+
+> ⚠️ Not all task types are supported by `benchmark_models` yet.
+
+Supported tasks (for each `task_type` argument):
+* `task_type="prediction.one_off.classification"`.
+* `task_type="prediction.one_off.regression"`.
+* `task_type="time_to_event"`.
+
+
diff --git a/docs/user_guide/usage/tutorial09_automl.md b/docs/user_guide/usage/tutorial09_automl.md
new file mode 100644
index 00000000..08726c08
--- /dev/null
+++ b/docs/user_guide/usage/tutorial09_automl.md
@@ -0,0 +1,689 @@
+# User Guide Tutorial 09: AutoML
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial09_automl.ipynb)
+
+TemporAI provides AutoML tools for finding the best model for your use case in `tempor.automl`, these are demonstrated here.
+
+
+
+## AutoML in TemporAI Overview
+
+TemporAI provides two AutoML approaches ("seekers") under the `tempor.automl.seekers` module.
+
+1. `MethodSeeker`: Search the hyperparameter space of a particular predictive method.
+2. `PipelineSeeker`: Search the hyperparameter space of a pipeline like `preprocessing steps -> predictive step`.
+
+The optimization strategies are facilitated by [`optuna`](https://optuna.readthedocs.io/) and the currently supported strategies are:
+* [Bayesian, specifically Tree-structured Parzen estimator](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) (`"bayesian"`),
+* [Random](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.RandomSampler.html) (`"random"`),
+* [CMA-ES](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.CmaEsSampler.html) (`"cmaes"`),
+* [QMC](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.QMCSampler.html) (`"qmc"`),
+* [Grid](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.GridSampler.html) (`"grid"`).
+
+## Using `MethodSeeker`
+
+Use a `MethodSeeker` to search for best algorithm and hyperparameters parameters for a particular task.
+No preprocessing (data transformation) steps are carried out in this approach, so preprocess the data using
+`tempor.plugins.preprocessing` first, as needed.
+
+A `MethodSeeker` can be initialized as follows.
+
+
+```python
+from tempor.automl.seeker import MethodSeeker
+from tempor.utils.dataloaders import SineDataLoader
+
+# Load your dataset.
+dataset = SineDataLoader().load()
+
+seeker = MethodSeeker(
+    # Name your AutoML study:
+    study_name="my_automl_study",
+    # Select the type of task:
+    task_type="prediction.one_off.classification",
+    # Choose which predictive methods to use in the search:
+    estimator_names=[
+        "cde_classifier",
+        "ode_classifier",
+        "nn_classifier",
+    ],
+    # Choose a metric. Metric maximization/minimization will be determined automatically.
+    metric="aucroc",
+    # Pass in your dataset.
+    dataset=dataset,
+    # How many best models to return:
+    return_top_k=3,
+    # Number of AutoML iterations:
+    num_iter=100,
+    # Type of AutoML tuner to use:
+    tuner_type="bayesian",
+    # You can also provide some other options like early stopping patience, number of cross-validation folds etc.
+)
+```
+
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:_set_up_tuners:354 | Setting up estimators and tuners for study my_automl_study.
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator cde_classifier.
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator ode_classifier.
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator nn_classifier.
+
+
+You can then run the AutoML search as below.
+
+The below example also shows how you can provide a custom hyperparameter space (*override* the default hyperparameter
+space for a model).
+
+
+```python
+from tempor.automl.params import IntegerParams, CategoricalParams
+
+# Provide a custom hyperparameter space to search for each type of model.
+# NOTE: For the sake of speed of this example, we limit epochs to 2.
+hp_space = {
+    "cde_classifier": [
+        IntegerParams(name="n_iter", low=2, high=2),
+        IntegerParams(name="n_temporal_units_hidden", low=5, high=20),
+        CategoricalParams(name="lr", choices=[1e-2, 1e-3, 1e-4]),
+    ],
+    "ode_classifier": [
+        IntegerParams(name="n_iter", low=2, high=2),
+        IntegerParams(name="n_units_hidden", low=5, high=20),
+        CategoricalParams(name="lr", choices=[1e-2, 1e-3, 1e-4]),
+    ],
+    "nn_classifier": [
+        IntegerParams(name="n_iter", low=2, high=2),
+        IntegerParams(name="n_units_hidden", low=5, high=20),
+        CategoricalParams(name="lr", choices=[1e-2, 1e-3, 1e-4]),
+    ],
+}
+
+# Initialize a `MethodSeeker` and provide `override_hp_space`.
+seeker = MethodSeeker(
+    study_name="my_automl_study",
+    task_type="prediction.one_off.classification",
+    estimator_names=[
+        "cde_classifier",
+        "ode_classifier",
+        "nn_classifier",
+    ],
+    metric="aucroc",
+    dataset=dataset,
+    return_top_k=3,
+    num_iter=3,  # For the sake of speed of this example, only 3 AutoML iterations.
+    tuner_type="bayesian",
+    # Override hyperparameter space:
+    override_hp_space=hp_space,
+)
+```
+
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:_set_up_tuners:354 | Setting up estimators and tuners for study my_automl_study.
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator cde_classifier.
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator ode_classifier.
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator nn_classifier.
+
+
+
+```python
+# Execute the search.
+
+best_methods, best_scores = seeker.search()
+```
+
+    2023-05-14 21:48:44 | INFO     | tempor.automl.seeker:search:413 | Running  search for estimator 'cde_classifier' 1/3.
+    2023-05-14 21:48:44 | INFO     | tempor.automl.tuner:tune:205 | Baseline score computation skipped
+    2023-05-14 21:48:44 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from CDEClassifier:
+    {'n_iter': 2, 'n_temporal_units_hidden': 13, 'lr': 0.01}
+    2023-05-14 21:48:49 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from CDEClassifier:
+    {'n_iter': 2, 'n_temporal_units_hidden': 11, 'lr': 0.0001}
+    2023-05-14 21:48:53 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from CDEClassifier:
+    {'n_iter': 2, 'n_temporal_units_hidden': 20, 'lr': 0.001}
+    2023-05-14 21:48:56 | INFO     | tempor.automl.seeker:search:413 | Running  search for estimator 'ode_classifier' 2/3.
+    2023-05-14 21:48:56 | INFO     | tempor.automl.tuner:tune:205 | Baseline score computation skipped
+    2023-05-14 21:48:56 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from ODEClassifier:
+    {'n_iter': 2, 'n_units_hidden': 13, 'lr': 0.01}
+    2023-05-14 21:48:59 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from ODEClassifier:
+    {'n_iter': 2, 'n_units_hidden': 11, 'lr': 0.0001}
+    2023-05-14 21:49:02 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from ODEClassifier:
+    {'n_iter': 2, 'n_units_hidden': 20, 'lr': 0.001}
+    2023-05-14 21:49:05 | INFO     | tempor.automl.seeker:search:413 | Running  search for estimator 'nn_classifier' 3/3.
+    2023-05-14 21:49:05 | INFO     | tempor.automl.tuner:tune:205 | Baseline score computation skipped
+    2023-05-14 21:49:05 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from NeuralNetClassifier:
+    {'n_iter': 2, 'n_units_hidden': 13, 'lr': 0.01}
+    2023-05-14 21:49:06 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6919505596160889, validation loss: 0.6882655620574951
+    2023-05-14 21:49:07 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.691156268119812, validation loss: 0.6865323185920715
+    2023-05-14 21:49:07 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.692247211933136, validation loss: 0.6861071586608887
+    2023-05-14 21:49:08 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6928325891494751, validation loss: 0.6825112104415894
+    2023-05-14 21:49:08 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6919053792953491, validation loss: 0.6819757223129272
+    2023-05-14 21:49:08 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from NeuralNetClassifier:
+    {'n_iter': 2, 'n_units_hidden': 11, 'lr': 0.0001}
+    2023-05-14 21:49:09 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6919505596160889, validation loss: 0.691270649433136
+    2023-05-14 21:49:09 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.691156268119812, validation loss: 0.6910805106163025
+    2023-05-14 21:49:10 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.692247211933136, validation loss: 0.6900344491004944
+    2023-05-14 21:49:10 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6928325891494751, validation loss: 0.6906693577766418
+    2023-05-14 21:49:11 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6919053792953491, validation loss: 0.6910874843597412
+    2023-05-14 21:49:11 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from NeuralNetClassifier:
+    {'n_iter': 2, 'n_units_hidden': 20, 'lr': 0.001}
+    2023-05-14 21:49:11 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6919505596160889, validation loss: 0.6896670460700989
+    2023-05-14 21:49:12 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.691156268119812, validation loss: 0.6886401772499084
+    2023-05-14 21:49:12 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.692247211933136, validation loss: 0.6884845495223999
+    2023-05-14 21:49:13 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6928325891494751, validation loss: 0.6887120604515076
+    2023-05-14 21:49:13 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6919053792953491, validation loss: 0.6893669962882996
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:search:447 | 
+    Evaluation for cde_classifier scores:
+    [0.47310606060606064, 0.48396464646464643, 0.4840277777777778].
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:search:447 | 
+    Evaluation for ode_classifier scores:
+    [0.47992424242424236, 0.48409090909090907, 0.484090909090909].
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:search:447 | 
+    Evaluation for nn_classifier scores:
+    [0.37064393939393936, 0.5370580808080809, 0.4579545454545454].
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:search:448 | 
+    All estimator definitions searched:
+    ['cde_classifier', 'ode_classifier', 'nn_classifier']
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:search:449 | 
+    Best scores for each estimator searched:
+    [0.4840277777777778, 0.48409090909090907, 0.5370580808080809]
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:search:450 | 
+    Best hyperparameters for each estimator searched:
+    [{'n_iter': 2, 'n_temporal_units_hidden': 20, 'lr': 0.001}, {'n_iter': 2, 'n_units_hidden': 11, 'lr': 0.0001}, {'n_iter': 2, 'n_units_hidden': 11, 'lr': 0.0001}]
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_create_estimator_with_hps:571 | 
+    Selected score 0.5370580808080809 for nn_classifier with hyperparameters:
+    {'n_iter': 2, 'n_units_hidden': 11, 'lr': 0.0001}
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_create_estimator_with_hps:571 | 
+    Selected score 0.48409090909090907 for ode_classifier with hyperparameters:
+    {'n_iter': 2, 'n_units_hidden': 11, 'lr': 0.0001}
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_create_estimator_with_hps:571 | 
+    Selected score 0.4840277777777778 for cde_classifier with hyperparameters:
+    {'n_iter': 2, 'n_temporal_units_hidden': 20, 'lr': 0.001}
+
+
+
+```python
+# The best methods are returned, and can be used by calling .predict() and so on.
+
+import rich.pretty  # For pretty printing only.
+
+for method in best_methods:
+    rich.pretty.pprint(method, indent_guides=False)
+```
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #800080; text-decoration-color: #800080; font-weight: bold">NeuralNetClassifier</span><span style="font-weight: bold">(</span>
+    <span style="color: #808000; text-decoration-color: #808000">name</span>=<span style="color: #008000; text-decoration-color: #008000">'nn_classifier'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">category</span>=<span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">params</span>=<span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'n_static_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_static_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_temporal_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">102</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_temporal_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_iter'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'mode'</span>: <span style="color: #008000; text-decoration-color: #008000">'RNN'</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_iter_print'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">10</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'batch_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'lr'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0001</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'weight_decay'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'window_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'device'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'dataloader_sampler'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'dropout'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'nonlin'</span>: <span style="color: #008000; text-decoration-color: #008000">'relu'</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'clipping_value'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'patience'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">20</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'train_ratio'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.8</span>
+    <span style="font-weight: bold">}</span>
+<span style="font-weight: bold">)</span>
+</pre>
+
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #800080; text-decoration-color: #800080; font-weight: bold">ODEClassifier</span><span style="font-weight: bold">(</span>
+    <span style="color: #808000; text-decoration-color: #808000">name</span>=<span style="color: #008000; text-decoration-color: #008000">'ode_classifier'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">category</span>=<span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">params</span>=<span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'n_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">11</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'nonlin'</span>: <span style="color: #008000; text-decoration-color: #008000">'relu'</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'dropout'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'atol'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.01</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'rtol'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.01</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'interpolation'</span>: <span style="color: #008000; text-decoration-color: #008000">'cubic'</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'lr'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0001</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'weight_decay'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_iter'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'batch_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">500</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_iter_print'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'patience'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">10</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'clipping_value'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'train_ratio'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.8</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'device'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'dataloader_sampler'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>
+    <span style="font-weight: bold">}</span>
+<span style="font-weight: bold">)</span>
+</pre>
+
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #800080; text-decoration-color: #800080; font-weight: bold">CDEClassifier</span><span style="font-weight: bold">(</span>
+    <span style="color: #808000; text-decoration-color: #808000">name</span>=<span style="color: #008000; text-decoration-color: #008000">'cde_classifier'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">category</span>=<span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">params</span>=<span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'n_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'nonlin'</span>: <span style="color: #008000; text-decoration-color: #008000">'relu'</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'dropout'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'atol'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.01</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'rtol'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.01</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'interpolation'</span>: <span style="color: #008000; text-decoration-color: #008000">'cubic'</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'lr'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'weight_decay'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_iter'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'batch_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">500</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'n_iter_print'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'patience'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">10</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'clipping_value'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'train_ratio'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.8</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'device'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'dataloader_sampler'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>
+    <span style="font-weight: bold">}</span>
+<span style="font-weight: bold">)</span>
+</pre>
+
+
+
+## Using `PipelineSeeker`
+
+Use a `PipelineSeeker` to search for best *pipeline* (`preprocessing steps -> prediction step`) for a particular task.
+
+This seeker will create pipelines comprised of:
+- A static imputer (if at lease one candidate in ``static_imputers`` provided),
+- A static scaler (if at lease one candidate in ``static_scalers`` provided),
+- A temporal imputer (if at lease one candidate in ``temporal_imputers`` provided),
+- A temporal scaler (if at lease one candidate in ``temporal_scalers`` provided),
+- The final predictor, from the ``estimator_names`` options.
+
+The imputer/scaler candidates will be sampled as a categorical hyperparameter. The hyperparameter spaces of these,
+and of the final predictor, will be sampled.
+
+A `PipelineSeeker` uses a very similar interface to `MethodSeeker`, and can be initialized as follows.
+
+
+```python
+from tempor.automl.seeker import PipelineSeeker
+
+seeker = PipelineSeeker(
+    study_name="my_automl_study",
+    task_type="prediction.one_off.classification",
+    # The estimators here will be the final step of the pipeline:
+    estimator_names=[
+        "cde_classifier",
+        "ode_classifier",
+        "nn_classifier",
+    ],
+    metric="aucroc",
+    dataset=dataset,
+    return_top_k=3,
+    num_iter=100,
+    tuner_type="bayesian",
+    # The following arguments specify the candidates of the different preprocessing steps, e.g.:
+    static_imputers=["static_tabular_imputer"],
+    static_scalers=[],
+    temporal_imputers=["ffill", "bfill"],
+    temporal_scalers=["ts_minmax_scaler"],
+)
+```
+
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_set_up_tuners:354 | Setting up estimators and tuners for study my_automl_study.
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_init_estimator:733 | Creating estimator <Pipeline with cde_classifier>.
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_init_estimator:733 | Creating estimator <Pipeline with ode_classifier>.
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_init_estimator:733 | Creating estimator <Pipeline with nn_classifier>.
+
+
+By default, the following preprocessing candidates will be used, if you do not specify the argument:
+
+
+```python
+from tempor.automl.seeker import (
+    DEFAULT_STATIC_IMPUTERS,
+    DEFAULT_STATIC_SCALERS,
+    DEFAULT_TEMPORAL_IMPUTERS,
+    DEFAULT_TEMPORAL_SCALERS,
+)
+
+print("Static imputer candidates:", DEFAULT_STATIC_IMPUTERS)
+print("Static scaler candidates:", DEFAULT_STATIC_SCALERS)
+print("Temporal imputer candidates:", DEFAULT_TEMPORAL_IMPUTERS)
+print("Temporal scaler candidates:", DEFAULT_TEMPORAL_SCALERS)
+```
+
+    Static imputer candidates: ['static_tabular_imputer']
+    Static scaler candidates: ['static_minmax_scaler', 'static_standard_scaler']
+    Temporal imputer candidates: ['ffill', 'ts_tabular_imputer', 'bfill']
+    Temporal scaler candidates: ['ts_minmax_scaler', 'ts_standard_scaler']
+
+
+You can execute the search as follows.
+
+
+```python
+from tempor.automl.params import IntegerParams, CategoricalParams
+
+# Provide a custom hyperparameter space to search for each type of model.
+# These can be provided for the final (predictive) step of the pipeline.
+# Default hyperparameter space will be sampled for the preprocessing steps.
+# NOTE: For the sake of speed of this example, we limit epochs to 2.
+hp_space = {
+    "cde_classifier": [
+        IntegerParams(name="n_iter", low=2, high=2),
+        IntegerParams(name="n_temporal_units_hidden", low=5, high=20),
+        CategoricalParams(name="lr", choices=[1e-2, 1e-3, 1e-4]),
+    ],
+    "ode_classifier": [
+        IntegerParams(name="n_iter", low=2, high=2),
+        IntegerParams(name="n_units_hidden", low=5, high=20),
+        CategoricalParams(name="lr", choices=[1e-2, 1e-3, 1e-4]),
+    ],
+    "nn_classifier": [
+        IntegerParams(name="n_iter", low=2, high=2),
+        IntegerParams(name="n_units_hidden", low=5, high=20),
+        CategoricalParams(name="lr", choices=[1e-2, 1e-3, 1e-4]),
+    ],
+}
+
+# Initialize a `PipelineSeeker` and provide `override_hp_space`.
+seeker = PipelineSeeker(
+    study_name="my_automl_study",
+    task_type="prediction.one_off.classification",
+    estimator_names=[
+        "cde_classifier",
+        "ode_classifier",
+        "nn_classifier",
+    ],
+    metric="aucroc",
+    dataset=dataset,
+    return_top_k=3,
+    num_iter=3,  # For the sake of speed of this example, only 3 AutoML iterations.
+    tuner_type="bayesian",
+    # Override hyperparameter space:
+    override_hp_space=hp_space,
+    # Specify preprocessing candidates:
+    static_imputers=["static_tabular_imputer"],
+    static_scalers=["static_minmax_scaler", "static_standard_scaler"],
+    temporal_imputers=[],
+    temporal_scalers=["ts_minmax_scaler", "ts_standard_scaler"],
+)
+```
+
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_set_up_tuners:354 | Setting up estimators and tuners for study my_automl_study.
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_init_estimator:733 | Creating estimator <Pipeline with cde_classifier>.
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_init_estimator:733 | Creating estimator <Pipeline with ode_classifier>.
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:_init_estimator:733 | Creating estimator <Pipeline with nn_classifier>.
+
+
+
+```python
+best_pipelines, best_scores = seeker.search()
+```
+
+    2023-05-14 21:49:14 | INFO     | tempor.automl.seeker:search:413 | Running  search for estimator '<Pipeline with cde_classifier>' 1/3.
+    2023-05-14 21:49:14 | INFO     | tempor.automl.tuner:tune:205 | Baseline score computation skipped
+    2023-05-14 21:49:14 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_minmax_scaler->preprocessing.scaling.temporal.ts_standard_scaler->prediction.one_off.classification.cde_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'softimpute'}, 'static_minmax_scaler': {'clip': True}, 'ts_standard_scaler': {}, 'cde_classifier': {'n_iter': 2, 'n_temporal_units_hidden': 17, 'lr': 0.001}}}
+    2023-05-14 21:49:30 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_minmax_scaler->preprocessing.scaling.temporal.ts_minmax_scaler->prediction.one_off.classification.cde_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'mean'}, 'static_minmax_scaler': {'clip': False}, 'ts_minmax_scaler': {'clip': False}, 'cde_classifier': {'n_iter': 2, 'n_temporal_units_hidden': 14, 'lr': 0.001}}}
+    2023-05-14 21:49:36 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_standard_scaler->preprocessing.scaling.temporal.ts_standard_scaler->prediction.one_off.classification.cde_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'most_frequent'}, 'static_standard_scaler': {}, 'ts_standard_scaler': {}, 'cde_classifier': {'n_iter': 2, 'n_temporal_units_hidden': 6, 'lr': 0.0001}}}
+    2023-05-14 21:49:42 | INFO     | tempor.automl.seeker:search:413 | Running  search for estimator '<Pipeline with ode_classifier>' 2/3.
+    2023-05-14 21:49:42 | INFO     | tempor.automl.tuner:tune:205 | Baseline score computation skipped
+    2023-05-14 21:49:42 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_minmax_scaler->preprocessing.scaling.temporal.ts_standard_scaler->prediction.one_off.classification.ode_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'softimpute'}, 'static_minmax_scaler': {'clip': True}, 'ts_standard_scaler': {}, 'ode_classifier': {'n_iter': 2, 'n_units_hidden': 17, 'lr': 0.001}}}
+    2023-05-14 21:50:00 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_minmax_scaler->preprocessing.scaling.temporal.ts_minmax_scaler->prediction.one_off.classification.ode_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'mean'}, 'static_minmax_scaler': {'clip': False}, 'ts_minmax_scaler': {'clip': False}, 'ode_classifier': {'n_iter': 2, 'n_units_hidden': 14, 'lr': 0.001}}}
+    2023-05-14 21:50:06 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_standard_scaler->preprocessing.scaling.temporal.ts_standard_scaler->prediction.one_off.classification.ode_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'most_frequent'}, 'static_standard_scaler': {}, 'ts_standard_scaler': {}, 'ode_classifier': {'n_iter': 2, 'n_units_hidden': 6, 'lr': 0.0001}}}
+    2023-05-14 21:50:11 | INFO     | tempor.automl.seeker:search:413 | Running  search for estimator '<Pipeline with nn_classifier>' 3/3.
+    2023-05-14 21:50:11 | INFO     | tempor.automl.tuner:tune:205 | Baseline score computation skipped
+    2023-05-14 21:50:11 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_minmax_scaler->preprocessing.scaling.temporal.ts_standard_scaler->prediction.one_off.classification.nn_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'softimpute'}, 'static_minmax_scaler': {'clip': True}, 'ts_standard_scaler': {}, 'nn_classifier': {'n_iter': 2, 'n_units_hidden': 17, 'lr': 0.001}}}
+    2023-05-14 21:50:14 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6918608546257019, validation loss: 0.6910383105278015
+    2023-05-14 21:50:17 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6909429430961609, validation loss: 0.6872987151145935
+    2023-05-14 21:50:21 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.692150354385376, validation loss: 0.6879752278327942
+    2023-05-14 21:50:24 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6926363706588745, validation loss: 0.6881052851676941
+    2023-05-14 21:50:27 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6915659308433533, validation loss: 0.6888220310211182
+    2023-05-14 21:50:28 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_minmax_scaler->preprocessing.scaling.temporal.ts_minmax_scaler->prediction.one_off.classification.nn_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'mean'}, 'static_minmax_scaler': {'clip': False}, 'ts_minmax_scaler': {'clip': False}, 'nn_classifier': {'n_iter': 2, 'n_units_hidden': 14, 'lr': 0.001}}}
+    2023-05-14 21:50:28 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6920319199562073, validation loss: 0.6892786622047424
+    2023-05-14 21:50:29 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6913070678710938, validation loss: 0.6893746852874756
+    2023-05-14 21:50:30 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6924894452095032, validation loss: 0.6886722445487976
+    2023-05-14 21:50:31 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6930248141288757, validation loss: 0.6894596815109253
+    2023-05-14 21:50:32 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6921922564506531, validation loss: 0.6893032789230347
+    2023-05-14 21:50:33 | INFO     | tempor.automl.tuner:objective:227 | 
+    Hyperparameters sampled from preprocessing.imputation.static.static_tabular_imputer->preprocessing.scaling.static.static_standard_scaler->preprocessing.scaling.temporal.ts_standard_scaler->prediction.one_off.classification.nn_classifier:
+    {'plugin_params': {'static_tabular_imputer': {'imputer': 'most_frequent'}, 'static_standard_scaler': {}, 'ts_standard_scaler': {}, 'nn_classifier': {'n_iter': 2, 'n_units_hidden': 6, 'lr': 0.0001}}}
+    2023-05-14 21:50:33 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6914154291152954, validation loss: 0.6911771297454834
+    2023-05-14 21:50:34 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.690178394317627, validation loss: 0.6934390664100647
+    2023-05-14 21:50:35 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6933540105819702, validation loss: 0.6896734833717346
+    2023-05-14 21:50:36 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.694333553314209, validation loss: 0.690070390701294
+    2023-05-14 21:50:37 | INFO     | tempor.models.ts_model:_train:379 | Epoch:0| train loss: 0.6921719312667847, validation loss: 0.6909489631652832
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:search:447 | 
+    Evaluation for <Pipeline with cde_classifier> scores:
+    [0.4716540404040404, 0.47575757575757577, 0.4657828282828283].
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:search:447 | 
+    Evaluation for <Pipeline with ode_classifier> scores:
+    [0.47575757575757577, 0.4779040404040404, 0.4779671717171716].
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:search:447 | 
+    Evaluation for <Pipeline with nn_classifier> scores:
+    [0.48478535353535346, 0.4361742424242424, 0.5071338383838383].
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:search:448 | 
+    All estimator definitions searched:
+    ['<Pipeline with cde_classifier>', '<Pipeline with ode_classifier>', '<Pipeline with nn_classifier>']
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:search:449 | 
+    Best scores for each estimator searched:
+    [0.47575757575757577, 0.4779671717171716, 0.5071338383838383]
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:search:450 | 
+    Best hyperparameters for each estimator searched:
+    [{'<candidates>(preprocessing.imputation.static)': 'static_tabular_imputer', '[static_tabular_imputer](imputer)': 'mean', '<candidates>(preprocessing.scaling.static)': 'static_minmax_scaler', '[static_minmax_scaler](clip)': False, '<candidates>(preprocessing.scaling.temporal)': 'ts_minmax_scaler', '[ts_minmax_scaler](clip)': False, '[cde_classifier](n_iter)': 2, '[cde_classifier](n_temporal_units_hidden)': 14, '[cde_classifier](lr)': 0.001}, {'<candidates>(preprocessing.imputation.static)': 'static_tabular_imputer', '[static_tabular_imputer](imputer)': 'most_frequent', '<candidates>(preprocessing.scaling.static)': 'static_standard_scaler', '[static_minmax_scaler](clip)': False, '<candidates>(preprocessing.scaling.temporal)': 'ts_standard_scaler', '[ts_minmax_scaler](clip)': False, '[ode_classifier](n_iter)': 2, '[ode_classifier](n_units_hidden)': 6, '[ode_classifier](lr)': 0.0001}, {'<candidates>(preprocessing.imputation.static)': 'static_tabular_imputer', '[static_tabular_imputer](imputer)': 'most_frequent', '<candidates>(preprocessing.scaling.static)': 'static_standard_scaler', '[static_minmax_scaler](clip)': False, '<candidates>(preprocessing.scaling.temporal)': 'ts_standard_scaler', '[ts_minmax_scaler](clip)': False, '[nn_classifier](n_iter)': 2, '[nn_classifier](n_units_hidden)': 6, '[nn_classifier](lr)': 0.0001}]
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:_create_estimator_with_hps:748 | 
+    Selected score 0.5071338383838383 for <Pipeline with nn_classifier> with hyperparameters:
+    {'<candidates>(preprocessing.imputation.static)': 'static_tabular_imputer', '[static_tabular_imputer](imputer)': 'most_frequent', '<candidates>(preprocessing.scaling.static)': 'static_standard_scaler', '[static_minmax_scaler](clip)': False, '<candidates>(preprocessing.scaling.temporal)': 'ts_standard_scaler', '[ts_minmax_scaler](clip)': False, '[nn_classifier](n_iter)': 2, '[nn_classifier](n_units_hidden)': 6, '[nn_classifier](lr)': 0.0001}
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:_create_estimator_with_hps:748 | 
+    Selected score 0.4779671717171716 for <Pipeline with ode_classifier> with hyperparameters:
+    {'<candidates>(preprocessing.imputation.static)': 'static_tabular_imputer', '[static_tabular_imputer](imputer)': 'most_frequent', '<candidates>(preprocessing.scaling.static)': 'static_standard_scaler', '[static_minmax_scaler](clip)': False, '<candidates>(preprocessing.scaling.temporal)': 'ts_standard_scaler', '[ts_minmax_scaler](clip)': False, '[ode_classifier](n_iter)': 2, '[ode_classifier](n_units_hidden)': 6, '[ode_classifier](lr)': 0.0001}
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:_create_estimator_with_hps:748 | 
+    Selected score 0.47575757575757577 for <Pipeline with cde_classifier> with hyperparameters:
+    {'<candidates>(preprocessing.imputation.static)': 'static_tabular_imputer', '[static_tabular_imputer](imputer)': 'mean', '<candidates>(preprocessing.scaling.static)': 'static_minmax_scaler', '[static_minmax_scaler](clip)': False, '<candidates>(preprocessing.scaling.temporal)': 'ts_minmax_scaler', '[ts_minmax_scaler](clip)': False, '[cde_classifier](n_iter)': 2, '[cde_classifier](n_temporal_units_hidden)': 14, '[cde_classifier](lr)': 0.001}
+
+
+
+```python
+# The best performing pipelines are returned, and can be used by calling .predict() and so on.
+
+for method in best_pipelines:
+    rich.pretty.pprint(method, indent_guides=False)
+```
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #800080; text-decoration-color: #800080; font-weight: bold">Pipeline</span><span style="font-weight: bold">(</span>
+    <span style="color: #808000; text-decoration-color: #808000">pipeline_seq</span>=<span style="color: #008000; text-decoration-color: #008000">'preprocessing.imputation.static.static_tabular_imputer-&gt;preprocessing.scaling.static.static_standard_scaler-&gt;preprocessing.scaling.temporal.ts_standard_scaler-&gt;prediction.one_off.classification.nn_classifier'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">predictor_category</span>=<span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">params</span>=<span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'static_tabular_imputer'</span>: <span style="font-weight: bold">{</span>
+            <span style="color: #008000; text-decoration-color: #008000">'imputer'</span>: <span style="color: #008000; text-decoration-color: #008000">'most_frequent'</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'imputer_params'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span><span style="font-weight: bold">}</span>
+        <span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'static_standard_scaler'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'with_mean'</span>: <span style="color: #00ff00; text-decoration-color: #00ff00; font-style: italic">True</span>, <span style="color: #008000; text-decoration-color: #008000">'with_std'</span>: <span style="color: #00ff00; text-decoration-color: #00ff00; font-style: italic">True</span><span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'ts_standard_scaler'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'with_mean'</span>: <span style="color: #00ff00; text-decoration-color: #00ff00; font-style: italic">True</span>, <span style="color: #008000; text-decoration-color: #008000">'with_std'</span>: <span style="color: #00ff00; text-decoration-color: #00ff00; font-style: italic">True</span><span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'nn_classifier'</span>: <span style="font-weight: bold">{</span>
+            <span style="color: #008000; text-decoration-color: #008000">'n_static_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_static_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_temporal_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">102</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_temporal_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_iter'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'mode'</span>: <span style="color: #008000; text-decoration-color: #008000">'RNN'</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_iter_print'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">10</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'batch_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'lr'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0001</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'weight_decay'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'window_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'device'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'dataloader_sampler'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'dropout'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'nonlin'</span>: <span style="color: #008000; text-decoration-color: #008000">'relu'</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'clipping_value'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'patience'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">20</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'train_ratio'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.8</span>
+        <span style="font-weight: bold">}</span>
+    <span style="font-weight: bold">}</span>
+<span style="font-weight: bold">)</span>
+</pre>
+
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #800080; text-decoration-color: #800080; font-weight: bold">Pipeline</span><span style="font-weight: bold">(</span>
+    <span style="color: #808000; text-decoration-color: #808000">pipeline_seq</span>=<span style="color: #008000; text-decoration-color: #008000">'preprocessing.imputation.static.static_tabular_imputer-&gt;preprocessing.scaling.static.static_standard_scaler-&gt;preprocessing.scaling.temporal.ts_standard_scaler-&gt;prediction.one_off.classification.ode_classifier'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">predictor_category</span>=<span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">params</span>=<span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'static_tabular_imputer'</span>: <span style="font-weight: bold">{</span>
+            <span style="color: #008000; text-decoration-color: #008000">'imputer'</span>: <span style="color: #008000; text-decoration-color: #008000">'most_frequent'</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'imputer_params'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span><span style="font-weight: bold">}</span>
+        <span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'static_standard_scaler'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'with_mean'</span>: <span style="color: #00ff00; text-decoration-color: #00ff00; font-style: italic">True</span>, <span style="color: #008000; text-decoration-color: #008000">'with_std'</span>: <span style="color: #00ff00; text-decoration-color: #00ff00; font-style: italic">True</span><span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'ts_standard_scaler'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'with_mean'</span>: <span style="color: #00ff00; text-decoration-color: #00ff00; font-style: italic">True</span>, <span style="color: #008000; text-decoration-color: #008000">'with_std'</span>: <span style="color: #00ff00; text-decoration-color: #00ff00; font-style: italic">True</span><span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'ode_classifier'</span>: <span style="font-weight: bold">{</span>
+            <span style="color: #008000; text-decoration-color: #008000">'n_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">6</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'nonlin'</span>: <span style="color: #008000; text-decoration-color: #008000">'relu'</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'dropout'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'atol'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.01</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'rtol'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.01</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'interpolation'</span>: <span style="color: #008000; text-decoration-color: #008000">'cubic'</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'lr'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0001</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'weight_decay'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_iter'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'batch_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">500</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_iter_print'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'patience'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">10</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'clipping_value'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'train_ratio'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.8</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'device'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'dataloader_sampler'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>
+        <span style="font-weight: bold">}</span>
+    <span style="font-weight: bold">}</span>
+<span style="font-weight: bold">)</span>
+</pre>
+
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #800080; text-decoration-color: #800080; font-weight: bold">Pipeline</span><span style="font-weight: bold">(</span>
+    <span style="color: #808000; text-decoration-color: #808000">pipeline_seq</span>=<span style="color: #008000; text-decoration-color: #008000">'preprocessing.imputation.static.static_tabular_imputer-&gt;preprocessing.scaling.static.static_minmax_scaler-&gt;preprocessing.scaling.temporal.ts_minmax_scaler-&gt;prediction.one_off.classification.cde_classifier'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">predictor_category</span>=<span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>,
+    <span style="color: #808000; text-decoration-color: #808000">params</span>=<span style="font-weight: bold">{</span>
+        <span style="color: #008000; text-decoration-color: #008000">'static_tabular_imputer'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'imputer'</span>: <span style="color: #008000; text-decoration-color: #008000">'mean'</span>, <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>, <span style="color: #008000; text-decoration-color: #008000">'imputer_params'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span><span style="font-weight: bold">}}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'static_minmax_scaler'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'feature_range'</span>: <span style="font-weight: bold">[</span><span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>, <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'clip'</span>: <span style="color: #ff0000; text-decoration-color: #ff0000; font-style: italic">False</span><span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'ts_minmax_scaler'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'feature_range'</span>: <span style="font-weight: bold">[</span><span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>, <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'clip'</span>: <span style="color: #ff0000; text-decoration-color: #ff0000; font-style: italic">False</span><span style="font-weight: bold">}</span>,
+        <span style="color: #008000; text-decoration-color: #008000">'cde_classifier'</span>: <span style="font-weight: bold">{</span>
+            <span style="color: #008000; text-decoration-color: #008000">'n_units_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_layers_hidden'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'nonlin'</span>: <span style="color: #008000; text-decoration-color: #008000">'relu'</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'dropout'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.0</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'atol'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.01</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'rtol'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.01</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'interpolation'</span>: <span style="color: #008000; text-decoration-color: #008000">'cubic'</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'lr'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'weight_decay'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.001</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_iter'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">2</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'batch_size'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">500</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'n_iter_print'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">100</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'random_state'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'patience'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">10</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'clipping_value'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">1</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'train_ratio'</span>: <span style="color: #008080; text-decoration-color: #008080; font-weight: bold">0.8</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'device'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>,
+            <span style="color: #008000; text-decoration-color: #008000">'dataloader_sampler'</span>: <span style="color: #800080; text-decoration-color: #800080; font-style: italic">None</span>
+        <span style="font-weight: bold">}</span>
+    <span style="font-weight: bold">}</span>
+<span style="font-weight: bold">)</span>
+</pre>
+
+
+
+## Advanced customization
+
+You may further customize the AutoML tuning behavior by specifying the sampler an pruner, if desired.
+
+See the below example.
+
+
+```python
+# 1. Import a Tuner:
+from tempor.automl.tuner import OptunaTuner
+
+# 2. Customize this as needed:
+import optuna
+
+custom_tuner = OptunaTuner(
+    study_name="my_automl_study",
+    direction="maximize",
+    # Customized sampler:
+    study_sampler=optuna.samplers.TPESampler(seed=12345, n_startup_trials=3),
+    # Customized pruner:
+    study_pruner=optuna.pruners.MedianPruner(interval_steps=2),
+    # Using default optuna storage object here, but may a provide custom one, e.g. redis.
+    study_storage=None,
+)
+
+# 3. Pass the Tuner to the {Method/Pipeline}Seeker:
+seeker = MethodSeeker(
+    study_name="my_automl_study",
+    task_type="prediction.one_off.classification",
+    estimator_names=[
+        "cde_classifier",
+        "ode_classifier",
+        "nn_classifier",
+    ],
+    metric="aucroc",
+    dataset=dataset,
+    # Like so:
+    custom_tuner=custom_tuner,
+)
+
+# 4. Execute search:
+# results = seeker.search() ...
+```
+
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:_set_up_tuners:354 | Setting up estimators and tuners for study my_automl_study.
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator cde_classifier.
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator ode_classifier.
+    2023-05-14 21:50:38 | INFO     | tempor.automl.seeker:_init_estimator:563 | Creating estimator nn_classifier.
+
+
+## Supported tasks
+
+> ⚠️ The tasks for which benchmarking is supported are supported by AutoML. See the benchmarking tutorial.
+
+
diff --git a/pypi.md b/pypi.md
index 975f8dd4..36889f72 100644
--- a/pypi.md
+++ b/pypi.md
@@ -12,7 +12,7 @@
 [![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/release/python-370/)
 [![PyPI-Server](https://img.shields.io/pypi/v/temporai?color=blue)](https://pypi.org/project/temporai/)
 [![](https://pepy.tech/badge/temporai)](https://pypi.org/project/temporai/)
-[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/vanderschaarlab/temporai/LICENSE.txt)
+[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/vanderschaarlab/temporai/tree/main/LICENSE.txt)
 
 [![Tests](https://github.com/vanderschaarlab/temporai/actions/workflows/test.yml/badge.svg)](https://github.com/vanderschaarlab/temporai/actions/workflows/test.yml)
 [![Tests](https://github.com/vanderschaarlab/temporai/actions/workflows/test_full.yml/badge.svg)](https://github.com/vanderschaarlab/temporai/actions/workflows/test.yml)
@@ -25,27 +25,42 @@
 
 # <img src='https://raw.githubusercontent.com/vanderschaarlab/temporai/main/docs/assets/TemporAI_Logo_Icon.png' height=25> TemporAI
 
+<!-- exclude_docs -->
 > **⚗️ Status:** This project is still in *alpha*, and the API may change without warning.  
+<!-- exclude_docs_end -->
+<!-- include_docs
+:::{important}
+**Status:** This project is still in *alpha*, and the API may change without warning.  
+:::
+include_docs_end -->
 
-*TemporAI* is a Machine Learning-centric time-series library for medicine.  The tasks that are currently of focus in TemporAI are: time-series prediction, time-to-event (a.k.a. survival) analysis with time-series data, and counterfactual inference (i.e. \[individualized\] treatment effects).
 
-*TemporAI* provides data preprocessing methods (including scaling and imputation methods for static and temporal covariates). AutoML tools for hyperparameter tuning and pipeline selection are available.
+## 📃 Overview
 
-In future versions, the library also aims to provide the user with understanding of their data, model, and problem, through e.g. integration with interpretability methods.
+*TemporAI* is a Machine Learning-centric time-series library for medicine.  The tasks that are currently of focus in TemporAI are: time-to-event (survival) analysis with time-series data, treatment effects (causal inference) over time, and time-series prediction. Data preprocessing methods, including missing value imputation for static and temporal covariates, are provided. AutoML tools for hyperparameter tuning and pipeline selection are also available.
 
-Key concepts:
+### How is TemporAI unique?
+
+* **🏥 Medicine-first:** Focused on use cases for medicine and healthcare, such as temporal treatment effects, survival analysis over time, imputation methods, models with built-in and post-hoc interpretability, ... See [methods](https://github.com/vanderschaarlab/temporai/tree/main/#-methods).
+* **🏗️ Fast prototyping:** A plugin design allowing for on-the-fly integration of new methods by the users.
+* **🚀 From research to practice:** Relevant novel models from research community adapted for practical use.
+* **🌍 A healthcare ecosystem vision:** A range of interactive demonstration apps, new medical problem settings, interpretability tools, data-centric tools etc. are planned.
+
+### Key concepts
 
 <div align="center">
 
 <!-- exclude_docs -->
-<img src='https://raw.githubusercontent.com/vanderschaarlab/temporai/main/docs/assets/Conceptual.png' width="650" alt="key concepts">
+<img src='https://raw.githubusercontent.com/vanderschaarlab/temporai/main/docs/assets/Conceptual.png' alt="key concepts">
 <!-- exclude_docs_end -->
 <!-- include_docs
-<img src='https://raw.githubusercontent.com/vanderschaarlab/temporai/main/docs/assets/Conceptual.png' width="750" alt="key concepts">
+<img src='https://raw.githubusercontent.com/vanderschaarlab/temporai/main/docs/assets/Conceptual.png' alt="key concepts">
 include_docs_end -->
 
 </div>
 
+
+
 ## 🚀 Installation
 
 ```bash
@@ -56,6 +71,8 @@ or from source, using
 $ pip install .
 ```
 
+
+
 ## 💥 Sample Usage
 * List the available plugins
 ```python
@@ -64,7 +81,63 @@ from tempor.plugins import plugin_loader
 print(plugin_loader.list())
 ```
 
-* Use an imputer
+* Use a time-to-event (survival) analysis model
+```python
+from tempor.utils.dataloaders import PBCDataLoader
+from tempor.plugins import plugin_loader
+
+# Load a time-to-event dataset:
+dataset = PBCDataLoader().load()
+
+# Initialize the model:
+model = plugin_loader.get("time_to_event.dynamic_deephit")
+
+# Train:
+model.fit(dataset)
+
+# Make risk predictions:
+prediction = model.predict(dataset, horizons=[0.25, 0.50, 0.75])
+```
+
+* Use a temporal treatment effects model
+```python
+import numpy as np
+
+from tempor.utils.dataloaders import DummyTemporalTreatmentEffectsDataLoader
+from tempor.plugins import plugin_loader
+
+# Load a dataset with temporal treatments and outcomes:
+dataset = DummyTemporalTreatmentEffectsDataLoader(
+    temporal_covariates_missing_prob=0.0,
+    temporal_treatments_n_features=1,
+    temporal_treatments_n_categories=2,
+).load()
+
+# Initialize the model:
+model = plugin_loader.get("treatments.temporal.regression.crn_regressor", epochs=20)
+
+# Train:
+model.fit(dataset)
+
+# Define target variable horizons for each sample:
+horizons = [
+    tc.time_indexes()[0][len(tc.time_indexes()[0]) // 2 :] for tc in dataset.time_series
+]
+
+# Define treatment scenarios for each sample:
+treatment_scenarios = [
+    [np.asarray([1] * len(h)), np.asarray([0] * len(h))] for h in horizons
+]
+
+# Predict counterfactuals:
+counterfactuals = model.predict_counterfactuals(
+    dataset,
+    horizons=horizons,
+    treatment_scenarios=treatment_scenarios,
+)
+```
+
+* Use a missing data imputer
 ```python
 from tempor.utils.dataloaders import SineDataLoader
 from tempor.plugins import plugin_loader
@@ -77,7 +150,7 @@ print(static_data_n_missing, temporal_data_n_missing)
 assert static_data_n_missing > 0
 assert temporal_data_n_missing > 0
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("preprocessing.imputation.temporal.bfill")
 
 # Train:
@@ -91,14 +164,14 @@ print(static_data_n_missing, temporal_data_n_missing)
 assert temporal_data_n_missing == 0
 ```
 
-* Use a classifier
+* Use a one-off classifier (prediction)
 ```python
 from tempor.utils.dataloaders import SineDataLoader
 from tempor.plugins import plugin_loader
 
 dataset = SineDataLoader().load()
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
 
 # Train:
@@ -108,54 +181,59 @@ model.fit(dataset)
 prediction = model.predict(dataset)
 ```
 
-* Use a regressor
+* Use a temporal regressor (forecasting)
 ```python
-from tempor.utils.dataloaders import SineDataLoader
+from tempor.utils.dataloaders import DummyTemporalPredictionDataLoader
 from tempor.plugins import plugin_loader
 
-dataset = SineDataLoader().load()
+# Load a dataset with temporal targets.
+dataset = DummyTemporalPredictionDataLoader(temporal_covariates_missing_prob=0.0).load()
 
-# Load the model:
-model = plugin_loader.get("prediction.one_off.regression.nn_regressor", n_iter=50)
+# Initialize the model:
+model = plugin_loader.get("prediction.temporal.regression.seq2seq_regressor", epochs=10)
 
 # Train:
 model.fit(dataset)
 
 # Predict:
-prediction = model.predict(dataset)
+prediction = model.predict(dataset, n_future_steps=5)
 ```
 
-* Benchmark models
-Classification task
+* Benchmark models, time-to-event task
 ```python
 from tempor.benchmarks import benchmark_models
 from tempor.plugins import plugin_loader
 from tempor.plugins.pipeline import pipeline
-from tempor.utils.dataloaders import SineDataLoader
+from tempor.utils.dataloaders import PBCDataLoader
 
 testcases = [
     (
         "pipeline1",
         pipeline(
             [
-                "preprocessing.scaling.static.static_minmax_scaler",
-                "prediction.one_off.classification.nn_classifier",
+                "preprocessing.scaling.temporal.ts_minmax_scaler",
+                "time_to_event.dynamic_deephit",
             ]
-        )({"nn_classifier": {"n_iter": 10}}),
+        )({"ts_coxph": {"n_iter": 100}}),
     ),
     (
         "plugin1",
-        plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=10),
+        plugin_loader.get("time_to_event.dynamic_deephit", n_iter=100),
+    ),
+    (
+        "plugin2",
+        plugin_loader.get("time_to_event.ts_coxph", n_iter=100),
     ),
 ]
-dataset = SineDataLoader().load()
+dataset = PBCDataLoader().load()
 
 aggr_score, per_test_score = benchmark_models(
-    task_type="prediction.one_off.classification",
+    task_type="time_to_event",
     tests=testcases,
     data=dataset,
     n_splits=2,
     random_state=0,
+    horizons=[2.0, 4.0, 6.0],
 )
 
 print(aggr_score)
@@ -166,7 +244,7 @@ print(aggr_score)
 from tempor.utils.serialization import load, save
 from tempor.plugins import plugin_loader
 
-# Load the model:
+# Initialize the model:
 model = plugin_loader.get("prediction.one_off.classification.nn_classifier", n_iter=50)
 
 buff = save(model)  # Save model to bytes.
@@ -175,7 +253,7 @@ reloaded = load(buff)  # Reload model.
 # `save_to_file`, `load_from_file` also available in the serialization module.
 ```
 
-* AutoML
+* AutoML - search for the best pipeline for your task
 ```python
 from tempor.automl.seeker import PipelineSeeker
 from tempor.utils.dataloaders import SineDataLoader
@@ -207,51 +285,14 @@ seeker = PipelineSeeker(
 best_pipelines, best_scores = seeker.search()  # doctest: +SKIP
 ```
 
-## 🔑 Methods
-
-
-
-### Prediction
-
-#### One-off
-Prediction where targets are static.
-
-* Classification (category: `prediction.one_off.classification`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `nn_classifier` | Neural-net based classifier. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
-| `ode_classifier` | Classifier based on ordinary differential equation (ODE) solvers.  | --- |
-| `cde_classifier` | Classifier based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926) |
-| `laplace_ode_classifier` | Classifier based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
-
-* Regression (category: `prediction.one_off.regression`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `nn_regressor` | Neural-net based regressor. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
-| `ode_regressor` | Regressor based on ordinary differential equation (ODE) solvers.  | --- |
-| `cde_regressor` | Regressor based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926)
-| `laplace_ode_regressor` | Regressor based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
-
-#### Temporal
-Prediction where targets are temporal (time series).
-
-* Classification (category: `prediction.temporal.classification`)
-
-| Name | Description| Reference |
-| --- | --- | --- |
-| `seq2seq_classifier` | Seq2Seq prediction, classification | --- |
-
-* Regression (category: `prediction.temporal.regression`)
 
-| Name | Description| Reference |
-| --- | --- | --- |
-| `seq2seq_regressor` | Seq2Seq prediction, regression | --- |
+<!-- include_docs
+{#methods}include_docs_end -->
+## 🔑 Methods
 
 
 
-### Time-to-Event
+### Time-to-Event (survival) analysis over time
 
 Risk estimation given event data (category: `time_to_event`)
 
@@ -295,6 +336,46 @@ Treatment effects estimation where treatments are temporal (time series).
 
 
 
+### Prediction
+
+#### One-off
+Prediction where targets are static.
+
+* Classification (category: `prediction.one_off.classification`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `nn_classifier` | Neural-net based classifier. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
+| `ode_classifier` | Classifier based on ordinary differential equation (ODE) solvers.  | --- |
+| `cde_classifier` | Classifier based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926) |
+| `laplace_ode_classifier` | Classifier based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
+
+* Regression (category: `prediction.one_off.regression`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `nn_regressor` | Neural-net based regressor. Supports multiple recurrent models, like RNN, LSTM, Transformer etc.  | --- |
+| `ode_regressor` | Regressor based on ordinary differential equation (ODE) solvers.  | --- |
+| `cde_regressor` | Regressor based Neural Controlled Differential Equations for Irregular Time Series.  | [Paper](https://arxiv.org/abs/2005.08926)
+| `laplace_ode_regressor` | Regressor based Inverse Laplace Transform (ILT) algorithms implemented in PyTorch.  | [Paper](https://arxiv.org/abs/2206.04843) |
+
+#### Temporal
+Prediction where targets are temporal (time series).
+
+* Classification (category: `prediction.temporal.classification`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `seq2seq_classifier` | Seq2Seq prediction, classification | --- |
+
+* Regression (category: `prediction.temporal.regression`)
+
+| Name | Description| Reference |
+| --- | --- | --- |
+| `seq2seq_regressor` | Seq2Seq prediction, regression | --- |
+
+
+
 ### Preprocessing
 
 #### Imputation
@@ -332,29 +413,29 @@ Treatment effects estimation where treatments are temporal (time series).
 
 
 
-## Tutorials
+## 📖 Tutorials
 
 ### Data
 
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial01_data_format.ipynb) - [Data Format](https://github.com/vanderschaarlab/temporai/tutorials/data/tutorial01_data_format.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial02_datasets.ipynb) - [Datasets](https://github.com/vanderschaarlab/temporai/tutorials/data/tutorial02_datasets.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial03_dataloaders.ipynb) - [Data Loaders](https://github.com/vanderschaarlab/temporai/tutorials/data/tutorial03_dataloaders.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial04_data_splitting.ipynb) - [Data Splitting](https://github.com/vanderschaarlab/temporai/tutorials/data/tutorial04_data_splitting.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial01_data_format.ipynb) - [Data Format](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/data/tutorial01_data_format.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial02_datasets.ipynb) - [Datasets](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/data/tutorial02_datasets.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial03_dataloaders.ipynb) - [Data Loaders](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/data/tutorial03_dataloaders.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/data/tutorial04_data_splitting.ipynb) - [Data Splitting](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/data/tutorial04_data_splitting.ipynb)
 
 ### User Guide
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial01_plugins.ipynb) - [Plugins](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial01_plugins.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial02_imputation.ipynb) - [Imputation](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial02_imputation.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial03_scaling.ipynb) - [Scaling](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial03_scaling.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial04_prediction.ipynb) - [Prediction](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial04_prediction.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial05_time_to_event.ipynb) - [Time-to-event Analysis](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial05_time_to_event.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial06_treatments.ipynb) - [Treatment Effects](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial06_treatments.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial07_pipeline.ipynb) - [Pipeline](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial07_pipeline.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial08_benchmarks.ipynb) - [Benchmarks](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial08_benchmarks.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial09_automl.ipynb) - [AutoML](https://github.com/vanderschaarlab/temporai/tutorials/user_guide/tutorial09_automl.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial01_plugins.ipynb) - [Plugins](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial01_plugins.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial02_imputation.ipynb) - [Imputation](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial02_imputation.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial03_scaling.ipynb) - [Scaling](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial03_scaling.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial04_prediction.ipynb) - [Prediction](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial04_prediction.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial05_time_to_event.ipynb) - [Time-to-event Analysis](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial05_time_to_event.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial06_treatments.ipynb) - [Treatment Effects](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial06_treatments.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial07_pipeline.ipynb) - [Pipeline](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial07_pipeline.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial08_benchmarks.ipynb) - [Benchmarks](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial08_benchmarks.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/user_guide/tutorial09_automl.ipynb) - [AutoML](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/user_guide/tutorial09_automl.ipynb)
 
 ### Extending TemporAI
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial01_custom_plugin.ipynb) - [Writing a Custom Plugin](https://github.com/vanderschaarlab/temporai/tutorials/extending/tutorial01_custom_plugin.ipynb)
-- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial02_testing_custom_plugin.ipynb) - [Testing a Custom Plugin](https://github.com/vanderschaarlab/temporai/tutorials/extending/tutorial02_testing_custom_plugin.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial01_custom_plugin.ipynb) - [Writing a Custom Plugin](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/extending/tutorial01_custom_plugin.ipynb)
+- [![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial02_testing_custom_plugin.ipynb) - [Testing a Custom Plugin](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/extending/tutorial02_testing_custom_plugin.ipynb)
 
 
 
@@ -385,10 +466,11 @@ pytest -vsx
 ```
 
 For development and contribution to TemporAI, see:
-* 📓 [Extending TemporAI tutorials](https://github.com/vanderschaarlab/temporai/tutorials/extending/)
-* 📃 [Contribution guide](https://github.com/vanderschaarlab/temporai/CONTRIBUTING.md)
+* 📓 [Extending TemporAI tutorials](https://github.com/vanderschaarlab/temporai/tree/main/tutorials/extending/)
+* 📃 [Contribution guide](https://github.com/vanderschaarlab/temporai/tree/main/CONTRIBUTING.md)
+* 👩‍💻 [Developer's guide](https://github.com/vanderschaarlab/temporai/tree/main/docs/dev_guide.md)
 
-## Citing
+## ✍️ Citing
 
 If you use this code, please cite the associated paper:
 ```
diff --git a/setup.cfg b/setup.cfg
index b098c68b..b58b2529 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -74,9 +74,9 @@ install_requires =
     pandera >=0.13
     pydantic >=1, <2
     rich
-    scikit-learn
     scikit-learn >= 1.0
     scipy
+    seaborn
     torch
     torchcde
     torchdiffeq
diff --git a/src/tempor/automl/params.py b/src/tempor/automl/params.py
index aeb12431..74f0a1f6 100644
--- a/src/tempor/automl/params.py
+++ b/src/tempor/automl/params.py
@@ -2,4 +2,4 @@
 
 # pylint: disable=wildcard-import,unused-wildcard-import
 
-from tempor.plugins.core._params import *  # noqa: F401,F403
+from tempor.plugins.core._params import *  # noqa: F401,F403  # pragma: no cover
diff --git a/src/tempor/automl/seeker.py b/src/tempor/automl/seeker.py
index 1ee129da..1c1272fd 100644
--- a/src/tempor/automl/seeker.py
+++ b/src/tempor/automl/seeker.py
@@ -102,6 +102,7 @@ def evaluation_callback_dispatch(
     random_state: int,
     horizon: Optional[data_typing.TimeIndex],
     raise_exceptions: bool,
+    silence_warnings: bool,
     *args,
     **kwargs,
 ) -> float:
@@ -126,6 +127,8 @@ def evaluation_callback_dispatch(
         raise_exceptions (bool):
             If set to `True`, if an exception is raised during evaluation, this will be raised and execution will be
             terminated. Otherwise the exception will be ignored and a dummy value returned.
+        silence_warnings (bool, optional):
+            Whether to silence warnings raised. Defaults to `False`.
 
     Returns:
         float: The mean evaluation metric across the cross-validation folds.
@@ -139,6 +142,7 @@ def evaluation_callback_dispatch(
             n_splits=n_cv_folds,
             random_state=random_state,
             raise_exceptions=raise_exceptions,
+            silence_warnings=silence_warnings,
         )
     elif task_type == "prediction.one_off.regression":
         metrics = evaluation.evaluate_prediction_oneoff_regressor(
@@ -147,6 +151,7 @@ def evaluation_callback_dispatch(
             n_splits=n_cv_folds,
             random_state=random_state,
             raise_exceptions=raise_exceptions,
+            silence_warnings=silence_warnings,
         )
     elif task_type == "prediction.temporal.classification":  # pragma: no cover
         raise NotImplementedError
@@ -166,6 +171,7 @@ def evaluation_callback_dispatch(
             n_splits=n_cv_folds,
             random_state=random_state,
             raise_exceptions=raise_exceptions,
+            silence_warnings=silence_warnings,
         )
     elif task_type == "treatments.one_off.classification":  # pragma: no cover
         raise NotImplementedError
@@ -204,6 +210,7 @@ def __init__(
         grid: Optional[Dict[str, Dict[str, Any]]] = None,
         custom_tuner: Optional[BaseTuner] = None,
         raise_exceptions: bool = True,
+        silence_warnings: bool = False,
         **kwargs,  # pylint: disable=unused-argument
     ) -> None:
         """The base class for an AutoML Seeker, to be derived from by concrete implementations. Provides an AutoML
@@ -254,6 +261,9 @@ def __init__(
             raise_exceptions (bool, optional):
                  If set to `True`, if an exception is raised during AutoML study run, this will be raised and
                  execution will be terminated. Otherwise the exception will be ignored. Defaults to `True`.
+            silence_warnings (bool, optional):
+                Whether to silence warnings raised. Some dependencies (e.g. `xgbse`) may circumvent this and raise
+                warnings regardless. Defaults to `False`.
 
         Raises:
             ValueError: If incompatible / invalid input arguments have been passed.
@@ -276,6 +286,7 @@ def __init__(
         self.compute_baseline_score = compute_baseline_score
         self.grid = grid
         self.raise_exceptions = raise_exceptions
+        self.silence_warnings = silence_warnings
 
         if len(estimator_defs) != len(estimator_names):
             raise ValueError("`estimator_defs` and `estimator_names` must be the same length.")
@@ -428,6 +439,7 @@ def search(self) -> Tuple[List[BasePredictor], List[float]]:
                     random_state=self.random_state,
                     horizon=self.horizon,
                     raise_exceptions=self.raise_exceptions,
+                    silence_warnings=self.silence_warnings,
                 ),
                 override_hp_space=override,
                 compute_baseline_score=self.compute_baseline_score,
@@ -490,6 +502,7 @@ def __init__(
         grid: Optional[Dict[str, Dict[str, Any]]] = None,
         custom_tuner: Optional[BaseTuner] = None,
         raise_exceptions: bool = True,
+        silence_warnings: bool = False,
         **kwargs,
     ) -> None:
         """An AutoML seeker which will search the hyperparameter space of each of the predictor estimators defined in
@@ -533,6 +546,8 @@ def __init__(
                 See `~tempor.automl.seeker.BaseSeeker`.
             raise_exceptions (bool, optional):
                 See `~tempor.automl.seeker.BaseSeeker`.
+            silence_warnings (bool, optional):
+                See `~tempor.automl.seeker.BaseSeeker`.
         """
         estimator_defs = estimator_names
 
@@ -556,6 +571,7 @@ def __init__(
             grid=grid,
             custom_tuner=custom_tuner,
             raise_exceptions=raise_exceptions,
+            silence_warnings=silence_warnings,
             **kwargs,
         )
 
@@ -599,6 +615,7 @@ def __init__(
         grid: Optional[Dict[str, Dict[str, Any]]] = None,
         custom_tuner: Optional[BaseTuner] = None,
         raise_exceptions: bool = True,
+        silence_warnings: bool = False,
         **kwargs,
     ) -> None:
         """An AutoML seeker which will sample pipelines comprised of:
@@ -664,6 +681,8 @@ def __init__(
                 See `~tempor.automl.seeker.BaseSeeker`.
             raise_exceptions (bool, optional):
                 See `~tempor.automl.seeker.BaseSeeker`.
+            silence_warnings (bool, optional):
+                See `~tempor.automl.seeker.BaseSeeker`.
         """
         # Define estimator definitions:
         estimator_defs = []
@@ -723,6 +742,7 @@ def __init__(
             grid=grid,
             custom_tuner=custom_tuner,
             raise_exceptions=raise_exceptions,
+            silence_warnings=silence_warnings,
             **kwargs,
         )
 
diff --git a/src/tempor/automl/tuner.py b/src/tempor/automl/tuner.py
index 7a2c529d..9158a8b9 100644
--- a/src/tempor/automl/tuner.py
+++ b/src/tempor/automl/tuner.py
@@ -68,7 +68,7 @@ def tune(
         override_hp_space: Optional[List[Params]] = None,
         compute_baseline_score: bool = True,
         **kwargs,
-    ) -> Tuple[List[float], List[Dict]]:
+    ) -> Tuple[List[float], List[Dict]]:  # pragma: no cover
         """Run the hyperparameter tuner and return scores and chosen hyperparameters.
 
         Args:
diff --git a/src/tempor/benchmarks/__init__.py b/src/tempor/benchmarks/__init__.py
index 45cadef7..a957a26b 100644
--- a/src/tempor/benchmarks/__init__.py
+++ b/src/tempor/benchmarks/__init__.py
@@ -1,4 +1,4 @@
-from .benchmark import benchmark_models  # noqa: F401
+from .benchmark import benchmark_models, visualize_benchmark  # noqa: F401
 from .evaluation import (  # noqa: F401
     ClassifierSupportedMetric,
     OutputMetric,
diff --git a/src/tempor/benchmarks/benchmark.py b/src/tempor/benchmarks/benchmark.py
index 5f2b2ba5..1405266c 100644
--- a/src/tempor/benchmarks/benchmark.py
+++ b/src/tempor/benchmarks/benchmark.py
@@ -1,7 +1,9 @@
 from typing import Any, Callable, Dict, List, Optional, Tuple
 
+import matplotlib.pyplot as plt
 import pandas as pd
 import pydantic
+import seaborn as sns
 
 from tempor.core.types import PredictiveTaskType
 from tempor.data import data_typing, dataset
@@ -11,7 +13,7 @@
 
 
 def print_score(mean: pd.Series, std: pd.Series) -> pd.Series:
-    with pd.option_context("mode.chained_assignment", None):
+    with pd.option_context("mode.chained_assignment", None):  # pyright: ignore
         mean.loc[(mean < 1e-3) & (mean != 0)] = 1e-3
         std.loc[(std < 1e-3) & (std != 0)] = 1e-3
 
@@ -29,6 +31,8 @@ def benchmark_models(
     n_splits: int = 3,
     random_state: int = 0,
     horizons: Optional[data_typing.TimeIndex] = None,
+    raise_exceptions: bool = False,
+    silence_warnings: bool = True,
 ) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
     """Benchmark the performance of several algorithms.
 
@@ -46,6 +50,13 @@ def benchmark_models(
             Random seed. Defaults to ``0``.
         horizons (data_typing.TimeIndex, optional):
             Time horizons for making predictions, if applicable to the task.
+        raise_exceptions (bool, optional):
+            Whether to raise exceptions during evaluation. If `False`, the exceptions will be swallowed and the
+            evaluation will continue - exception count will be reported in the `"errors"` column of the resultant
+            dataframe. Defaults to `False`.
+        silence_warnings (bool, optional):
+            Whether to silence warnings raised. Some dependencies (e.g. `xgbse`) may circumvent this and raise warnings
+            regardless. Defaults to `True`.
 
     Returns:
         Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
@@ -60,31 +71,27 @@ def benchmark_models(
 
     results = {}
 
+    # TODO: Handle missing cases.
     if task_type == "prediction.one_off.classification":
         evaluator: Callable = evaluation.evaluate_prediction_oneoff_classifier
     elif task_type == "prediction.one_off.regression":
         evaluator = evaluation.evaluate_prediction_oneoff_regressor
     elif task_type == "time_to_event":
         evaluator = evaluation.evaluate_time_to_event
-    elif task_type == "prediction.temporal.classification":
-        # TODO: This case needs to be handled in `tempor.benchmarks.evaluation`.
+    elif task_type == "prediction.temporal.classification":  # pragma: no cover
         raise NotImplementedError
-    elif task_type == "prediction.temporal.regression":
-        # TODO: This case needs to be handled in `tempor.benchmarks.evaluation`.
+    elif task_type == "prediction.temporal.regression":  # pragma: no cover
         raise NotImplementedError
-    elif task_type == "treatments.one_off.classification":
-        # TODO: This case needs to be handled in `tempor.benchmarks.evaluation`.
+    elif task_type == "treatments.one_off.classification":  # pragma: no cover
         raise NotImplementedError
-    elif task_type == "treatments.one_off.regression":
-        # TODO: This case needs to be handled in `tempor.benchmarks.evaluation`.
+    elif task_type == "treatments.one_off.regression":  # pragma: no cover
         raise NotImplementedError
-    elif task_type == "treatments.temporal.classification":
-        # TODO: This case needs to be handled in `tempor.benchmarks.evaluation`.
+    elif task_type == "treatments.temporal.classification":  # pragma: no cover
         raise NotImplementedError
-    elif task_type == "treatments.temporal.regression":
-        # TODO: This case needs to be handled in `tempor.benchmarks.evaluation`.
+    elif task_type == "treatments.temporal.regression":  # pragma: no cover
         raise NotImplementedError
-    else:
+    else:  # pragma: no cover
+        # Should not reach here, will be caught by Pydantic.
         raise ValueError(f"Unsupported task type: {task_type}")
 
     for testcase, plugin in tests:
@@ -96,6 +103,8 @@ def benchmark_models(
             n_splits=n_splits,
             random_state=random_state,
             horizons=horizons,  # type: ignore
+            raise_exceptions=raise_exceptions,
+            silence_warnings=silence_warnings,
         )
 
         mean_score = scores["mean"].to_dict()
@@ -119,3 +128,31 @@ def benchmark_models(
     aggr = aggr.set_axis(results.keys(), axis=1)
 
     return aggr, results
+
+
+@pydantic.validate_arguments(config=dict(arbitrary_types_allowed=True))
+def visualize_benchmark(results: Dict[str, pd.DataFrame], palette: str = "viridis") -> Any:
+
+    # Pre-format DF for plotting.
+    for k, v in results.items():
+        v["method"] = k
+    df_sns = pd.concat(list(results.values()))
+    df_sns["metric"] = df_sns.index
+
+    # Prepare "stddev" column for plotting as error bars.
+    err = df_sns.pivot(index="method", columns="metric", values="stddev")
+    key = {order: idx for idx, order in enumerate(df_sns["method"].unique())}
+    err = err.sort_index(key=lambda x: x.map(key)).T
+
+    axes = []
+    for metric in err.index:
+        set_options = dict(title=f"Benchmark results: {metric}", ylabel=f"{metric} (CV mean)", xlabel="Benchmark case")
+        out = sns.barplot(
+            df_sns[df_sns["metric"] == metric], x="method", y="mean", palette=palette, yerr=err.loc[metric, :]
+        )
+        out.set(**set_options)
+        axes.append(out)
+        print(f"Plotting bar plot for metric: {metric}")
+        plt.show()
+
+    return axes
diff --git a/src/tempor/benchmarks/evaluation.py b/src/tempor/benchmarks/evaluation.py
index 90132bce..a66bda21 100644
--- a/src/tempor/benchmarks/evaluation.py
+++ b/src/tempor/benchmarks/evaluation.py
@@ -1,4 +1,5 @@
 import copy
+import warnings
 from time import time
 from typing import TYPE_CHECKING, Any, Callable, Dict, List, Sequence, Union, cast
 
@@ -233,17 +234,15 @@ def __init__(
                 Defaults to :obj:`~tempor.benchmarks.evaluation.classifier_supported_metrics`.
         """
         self.metrics: Union[ClassifierSupportedMetric, Sequence[ClassifierSupportedMetric]]
-        if isinstance(metric, str):
+        if isinstance(metric, str):  # pragma: no cover
+            # Should be prevented by pydantic.
             self.metrics = [cast(ClassifierSupportedMetric, metric)]
         else:
             self.metrics = metric
 
-    def get_metric(self) -> Union[ClassifierSupportedMetric, Sequence[ClassifierSupportedMetric]]:
-        return self.metrics
-
     def score_proba(self, y_test: np.ndarray, y_pred_proba: np.ndarray) -> Dict[str, float]:
         if y_test is None or y_pred_proba is None:
-            raise RuntimeError("Invalid input for score_proba")
+            raise ValueError("Invalid input for score_proba")
 
         results = {}
         y_pred = np.argmax(np.asarray(y_pred_proba), axis=1)
@@ -323,7 +322,7 @@ def score_proba(self, y_test: np.ndarray, y_pred_proba: np.ndarray) -> Dict[str,
                 )
             elif metric == "mcc":
                 results[metric] = sklearn.metrics.matthews_corrcoef(y_test, y_pred)
-            else:
+            else:  # pragma: no cover
                 raise ValueError(f"invalid metric {metric}")
 
         logger.debug(f"evaluate_classifier: {results}")
@@ -344,6 +343,7 @@ def evaluate_prediction_oneoff_classifier(  # pylint: disable=unused-argument
     n_splits: int = 3,
     random_state: int = 0,
     raise_exceptions: bool = False,
+    silence_warnings: bool = False,
     **kwargs: Any,
 ) -> pd.DataFrame:
     """Helper for evaluating classifiers.
@@ -361,6 +361,8 @@ def evaluate_prediction_oneoff_classifier(  # pylint: disable=unused-argument
             Whether to raise exceptions during evaluation. If `False`, the exceptions will be swallowed and the
             evaluation will continue - exception count will be reported in the `"errors"` column of the resultant
             dataframe. Defaults to `False`.
+        silence_warnings (bool, optional):
+            Whether to silence warnings raised. Defaults to `False`.
 
     Returns:
         pd.DataFrame:
@@ -373,48 +375,52 @@ def evaluate_prediction_oneoff_classifier(  # pylint: disable=unused-argument
             :obj:`~tempor.benchmarks.evaluation.ClassifierSupportedMetric`.
     """
 
-    if n_splits < 2 or not isinstance(n_splits, int):
-        raise ValueError("n_splits must be an integer >= 2")
-    estimator_ = cast("BaseOneOffClassifier", estimator)
-    enable_reproducibility(random_state)
-
-    results = _InternalScores()
-    evaluator = ClassifierMetrics()
-    for metric in classifier_supported_metrics:
-        results.metrics[metric] = np.zeros(n_splits)
-
-    splitter = sklearn.model_selection.StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=random_state)
-
-    if data.predictive.targets is None:
-        raise ValueError("The dataset for evaluation needs to contain targets but did not")
-    labels = data.predictive.targets.numpy().squeeze()
-    if len(labels.shape) > 1:
-        raise ValueError("Classifier evaluation expects 1D output")
-
-    indx = 0
-    for train_data, test_data in data.split(splitter=splitter, y=labels):
-        model = copy.deepcopy(estimator_)
-        start = time()
-        try:
-            model.fit(train_data)
-
-            if TYPE_CHECKING:  # pragma: no cover
-                assert test_data.predictive.targets is not None  # nosec B101
-            test_labels = test_data.predictive.targets.numpy()
-            preds = model.predict_proba(test_data).numpy()
-
-            scores = evaluator.score_proba(test_labels, preds)
-            for metric in scores:
-                results.metrics[metric][indx] = scores[metric]
-            results.errors.append(0)
-        except BaseException as e:  # pylint: disable=broad-except
-            logger.error(f"Evaluation failed: {e}")
-            results.errors.append(1)
-            if raise_exceptions:
-                raise
-
-        results.durations.append(time() - start)
-        indx += 1
+    with warnings.catch_warnings():
+        if silence_warnings:
+            warnings.simplefilter("ignore")
+
+        if n_splits < 2 or not isinstance(n_splits, int):
+            raise ValueError("n_splits must be an integer >= 2")
+        estimator_ = cast("BaseOneOffClassifier", estimator)
+        enable_reproducibility(random_state)
+
+        results = _InternalScores()
+        evaluator = ClassifierMetrics()
+        for metric in classifier_supported_metrics:
+            results.metrics[metric] = np.zeros(n_splits)
+
+        splitter = sklearn.model_selection.StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=random_state)
+
+        if data.predictive.targets is None:
+            raise ValueError("The dataset for evaluation needs to contain targets but did not")
+        labels = data.predictive.targets.numpy().squeeze()
+        if len(labels.shape) > 1:
+            raise ValueError("Classifier evaluation expects 1D output")
+
+        indx = 0
+        for train_data, test_data in data.split(splitter=splitter, y=labels):
+            model = copy.deepcopy(estimator_)
+            start = time()
+            try:
+                model.fit(train_data)
+
+                if TYPE_CHECKING:  # pragma: no cover
+                    assert test_data.predictive.targets is not None  # nosec B101
+                test_labels = test_data.predictive.targets.numpy()
+                preds = model.predict_proba(test_data).numpy()
+
+                scores = evaluator.score_proba(test_labels, preds)
+                for metric in scores:
+                    results.metrics[metric][indx] = scores[metric]
+                results.errors.append(0)
+            except BaseException as e:  # pylint: disable=broad-except
+                logger.error(f"Evaluation failed: {e}")
+                results.errors.append(1)
+                if raise_exceptions:
+                    raise
+
+            results.durations.append(time() - start)
+            indx += 1
 
     return _postprocess_results(results)
 
@@ -426,6 +432,7 @@ def evaluate_prediction_oneoff_regressor(  # pylint: disable=unused-argument
     n_splits: int = 3,
     random_state: int = 0,
     raise_exceptions: bool = False,
+    silence_warnings: bool = False,
     **kwargs: Any,
 ) -> pd.DataFrame:
     """Helper for evaluating regression tasks.
@@ -443,6 +450,8 @@ def evaluate_prediction_oneoff_regressor(  # pylint: disable=unused-argument
             Whether to raise exceptions during evaluation. If `False`, the exceptions will be swallowed and the
             evaluation will continue - exception count will be reported in the `"errors"` column of the resultant
             dataframe. Defaults to `False`.
+        silence_warnings (bool, optional):
+            Whether to silence warnings raised. Defaults to `False`.
 
     Returns:
         pd.DataFrame:
@@ -454,42 +463,47 @@ def evaluate_prediction_oneoff_regressor(  # pylint: disable=unused-argument
             The index of the dataframe contains all the metrics evaluated: all of
             :obj:`~tempor.benchmarks.evaluation.RegressionSupportedMetric`.
     """
-    if n_splits < 2 or not isinstance(n_splits, int):
-        raise ValueError("n_splits must be an integer >= 2")
-    estimator_ = cast("BaseOneOffRegressor", estimator)
-    enable_reproducibility(random_state)
-    metrics = regression_supported_metrics
-
-    results = _InternalScores()
-    for metric in metrics:
-        results.metrics[metric] = np.zeros(n_splits)
-
-    splitter = sklearn.model_selection.KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
-
-    indx = 0
-    for train_data, test_data in data.split(splitter=splitter):
-        model = copy.deepcopy(estimator_)
-        start = time()
-        try:
-            model.fit(train_data)
-
-            if TYPE_CHECKING:  # pragma: no cover
-                assert test_data.predictive.targets is not None  # nosec B101
-            targets = test_data.predictive.targets.numpy().squeeze()
-            preds = model.predict(test_data).numpy().squeeze()
-
-            results.metrics["mse"][indx] = sklearn.metrics.mean_squared_error(targets, preds)
-            results.metrics["mae"][indx] = sklearn.metrics.mean_absolute_error(targets, preds)
-            results.metrics["r2"][indx] = sklearn.metrics.r2_score(targets, preds)
-            results.errors.append(0)
-        except BaseException as e:  # pylint: disable=broad-except
-            logger.error(f"Regression evaluation failed: {e}")
-            results.errors.append(1)
-            if raise_exceptions:
-                raise
-
-        results.durations.append(time() - start)
-        indx += 1
+
+    with warnings.catch_warnings():
+        if silence_warnings:
+            warnings.simplefilter("ignore")
+
+        if n_splits < 2 or not isinstance(n_splits, int):
+            raise ValueError("n_splits must be an integer >= 2")
+        estimator_ = cast("BaseOneOffRegressor", estimator)
+        enable_reproducibility(random_state)
+        metrics = regression_supported_metrics
+
+        results = _InternalScores()
+        for metric in metrics:
+            results.metrics[metric] = np.zeros(n_splits)
+
+        splitter = sklearn.model_selection.KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
+
+        indx = 0
+        for train_data, test_data in data.split(splitter=splitter):
+            model = copy.deepcopy(estimator_)
+            start = time()
+            try:
+                model.fit(train_data)
+
+                if TYPE_CHECKING:  # pragma: no cover
+                    assert test_data.predictive.targets is not None  # nosec B101
+                targets = test_data.predictive.targets.numpy().squeeze()
+                preds = model.predict(test_data).numpy().squeeze()
+
+                results.metrics["mse"][indx] = sklearn.metrics.mean_squared_error(targets, preds)
+                results.metrics["mae"][indx] = sklearn.metrics.mean_absolute_error(targets, preds)
+                results.metrics["r2"][indx] = sklearn.metrics.r2_score(targets, preds)
+                results.errors.append(0)
+            except BaseException as e:  # pylint: disable=broad-except
+                logger.error(f"Regression evaluation failed: {e}")
+                results.errors.append(1)
+                if raise_exceptions:
+                    raise
+
+            results.durations.append(time() - start)
+            indx += 1
 
     return _postprocess_results(results)
 
@@ -580,6 +594,7 @@ def evaluate_time_to_event(  # pylint: disable=unused-argument
     n_splits: int = 3,
     random_state: int = 0,
     raise_exceptions: bool = False,
+    silence_warnings: bool = False,
     **kwargs: Any,
 ) -> pd.DataFrame:
     """Helper for evaluating time-to-event tasks.
@@ -599,6 +614,8 @@ def evaluate_time_to_event(  # pylint: disable=unused-argument
             Whether to raise exceptions during evaluation. If `False`, the exceptions will be swallowed and the
             evaluation will continue - exception count will be reported in the `"errors"` column of the resultant
             dataframe. Defaults to `False`.
+        silence_warnings (bool, optional):
+            Whether to silence warnings raised. Defaults to `False`.
 
     Returns:
         pd.DataFrame:
@@ -610,51 +627,56 @@ def evaluate_time_to_event(  # pylint: disable=unused-argument
             The index of the dataframe contains all the metrics evaluated: all of
             :obj:`~tempor.benchmarks.evaluation.TimeToEventSupportedMetric`.
     """
-    if n_splits < 2 or not isinstance(n_splits, int):
-        raise ValueError("n_splits must be an integer >= 2")
-    estimator_ = cast("BaseTimeToEventAnalysis", estimator)
-    enable_reproducibility(random_state)
-    metrics = time_to_event_supported_metrics
-    metrics_map = {
-        "c_index": compute_c_index,
-        "brier_score": compute_brier_score,
-    }
-
-    results = _InternalScores()
-    for metric in metrics:
-        results.metrics[metric] = np.zeros(n_splits)
-
-    splitter = sklearn.model_selection.KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
-
-    indx = 0
-    for train_data, test_data in data.split(splitter=splitter):
-        model = copy.deepcopy(estimator_)
-        start = time()
-        try:
-            model.fit(train_data)
-
-            # targets = test_data.predictive.targets.numpy().squeeze()
-            preds = model.predict(test_data, horizons=horizons)
-
-            for metric_name in time_to_event_supported_metrics:
-                metric_func = metrics_map[metric_name]
-                results.metrics[metric_name][indx] = _compute_time_to_event_metric(
-                    metric_func,
-                    train_data=train_data,
-                    test_data=test_data,
-                    horizons=horizons,
-                    predictions=preds,
-                )
-
-            results.errors.append(0)
-
-        except BaseException as e:  # pylint: disable=broad-except
-            logger.error(f"Regression evaluation failed: {e}")
-            results.errors.append(1)
-            if raise_exceptions:
-                raise
-
-        results.durations.append(time() - start)
-        indx += 1
+    with warnings.catch_warnings():
+        if silence_warnings:
+            warnings.simplefilter("ignore")
+            # NOTE: xbgse is somehow able to circumvent warnings silencing, so will still raise warnings.
+
+        if n_splits < 2 or not isinstance(n_splits, int):
+            raise ValueError("n_splits must be an integer >= 2")
+        estimator_ = cast("BaseTimeToEventAnalysis", estimator)
+        enable_reproducibility(random_state)
+        metrics = time_to_event_supported_metrics
+        metrics_map = {
+            "c_index": compute_c_index,
+            "brier_score": compute_brier_score,
+        }
+
+        results = _InternalScores()
+        for metric in metrics:
+            results.metrics[metric] = np.zeros(n_splits)
+
+        splitter = sklearn.model_selection.KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
+
+        indx = 0
+        for train_data, test_data in data.split(splitter=splitter):
+            model = copy.deepcopy(estimator_)
+            start = time()
+            try:
+                model.fit(train_data)
+
+                # targets = test_data.predictive.targets.numpy().squeeze()
+                preds = model.predict(test_data, horizons=horizons)
+
+                for metric_name in time_to_event_supported_metrics:
+                    metric_func = metrics_map[metric_name]
+                    results.metrics[metric_name][indx] = _compute_time_to_event_metric(
+                        metric_func,
+                        train_data=train_data,
+                        test_data=test_data,
+                        horizons=horizons,
+                        predictions=preds,
+                    )
+
+                results.errors.append(0)
+
+            except BaseException as e:  # pylint: disable=broad-except
+                logger.error(f"Regression evaluation failed: {e}")
+                results.errors.append(1)
+                if raise_exceptions:
+                    raise
+
+            results.durations.append(time() - start)
+            indx += 1
 
     return _postprocess_results(results)
diff --git a/src/tempor/benchmarks/metrics.py b/src/tempor/benchmarks/metrics.py
index 66b7d7c1..7383a2af 100644
--- a/src/tempor/benchmarks/metrics.py
+++ b/src/tempor/benchmarks/metrics.py
@@ -12,7 +12,7 @@
 
 if Version(sklearn.__version__) >= Version("1.1"):
     _has_input_name = True
-else:
+else:  # pragma: no cover
     _has_input_name = False
 
 
@@ -68,7 +68,7 @@ def check_y_survival(y_or_event: np.ndarray, *args, allow_all_censored=False) ->
             continue
 
         yt = sklearn.utils.check_array(yt, ensure_2d=False)
-        if not np.issubdtype(yt.dtype, np.number):
+        if not np.issubdtype(yt.dtype, np.number):  # pragma: no cover
             raise ValueError("time must be numeric, but found {} for argument {}".format(yt.dtype, i + 2))
 
         return_val.append(yt)
@@ -88,36 +88,6 @@ def _check_estimate_1d(estimate, test_time):
     return estimate
 
 
-def _check_inputs(event_indicator, event_time, estimate):
-    sklearn.utils.check_consistent_length(event_indicator, event_time, estimate)
-    event_indicator = sklearn.utils.check_array(
-        event_indicator,
-        ensure_2d=False,
-        **dict(input_name="event_indicator") if _has_input_name else dict(),
-    )
-    event_time = sklearn.utils.check_array(
-        event_time,
-        ensure_2d=False,
-        **dict(input_name="event_time") if _has_input_name else dict(),
-    )
-    estimate = _check_estimate_1d(estimate, event_time)
-
-    if not np.issubdtype(event_indicator.dtype, np.bool_):
-        raise ValueError(
-            "only boolean arrays are supported as class labels for survival analysis, got {0}".format(
-                event_indicator.dtype
-            )
-        )
-
-    if len(event_time) < 2:
-        raise ValueError("Need a minimum of two samples")
-
-    if not event_indicator.any():
-        raise ValueError("All samples are censored")
-
-    return event_indicator, event_time, estimate
-
-
 def _check_times(test_time, times):
     times = sklearn.utils.check_array(
         np.atleast_1d(times),
@@ -526,7 +496,7 @@ def _estimate_concordance_index(
 
         est = estimate[order[mask]]
 
-        if not event_i:
+        if not event_i:  # pragma: no cover
             raise RuntimeError("got censored sample at index %d, but expected uncensored" % order[ind])
 
         ties = np.absolute(est - est_i) <= tied_tol
diff --git a/src/tempor/core/pydantic_utils.py b/src/tempor/core/pydantic_utils.py
index eb6760b5..e203e1fb 100644
--- a/src/tempor/core/pydantic_utils.py
+++ b/src/tempor/core/pydantic_utils.py
@@ -1,21 +1,21 @@
-from typing import Any, Dict, Optional, Type
+from typing import Any, Type
 
 import pydantic
 
-
-def exclusive_args(
-    values: Dict,
-    arg1: str,
-    arg2: str,
-    arg1_friendly_name: Optional[str] = None,
-    arg2_friendly_name: Optional[str] = None,
-) -> None:
-    arg1_value = values.get(arg1, None)
-    arg2_value = values.get(arg2, None)
-    arg1_name = arg1_friendly_name if arg1_friendly_name else f"`{arg1}`"
-    arg2_name = arg2_friendly_name if arg2_friendly_name else f"`{arg2}`"
-    if arg1_value is not None and arg2_value is not None:
-        raise ValueError(f"Must provide either {arg1_name} or {arg2_name} but not both")
+# Currently unused.
+# def exclusive_args(
+#     values: Dict,
+#     arg1: str,
+#     arg2: str,
+#     arg1_friendly_name: Optional[str] = None,
+#     arg2_friendly_name: Optional[str] = None,
+# ) -> None:
+#     arg1_value = values.get(arg1, None)
+#     arg2_value = values.get(arg2, None)
+#     arg1_name = arg1_friendly_name if arg1_friendly_name else f"`{arg1}`"
+#     arg2_name = arg2_friendly_name if arg2_friendly_name else f"`{arg2}`"
+#     if arg1_value is not None and arg2_value is not None:
+#         raise ValueError(f"Must provide either {arg1_name} or {arg2_name} but not both")
 
 
 def is_pydantic_dataclass(cls: Type) -> bool:
diff --git a/src/tempor/core/utils.py b/src/tempor/core/utils.py
index b6569473..961a5b11 100644
--- a/src/tempor/core/utils.py
+++ b/src/tempor/core/utils.py
@@ -1,4 +1,3 @@
-import enum
 from typing import Any, Dict, Tuple, Type
 
 from typing_extensions import get_args
@@ -13,8 +12,9 @@ def get_class_full_name(o: object):
     return module + "." + class_.__qualname__
 
 
-def get_enum_name(enum_: enum.Enum) -> str:
-    return enum_.name.lower()
+# Currently unused
+# def get_enum_name(enum_: enum.Enum) -> str:
+#     return enum_.name.lower()
 
 
 class RichReprStrPassthrough:
@@ -34,34 +34,6 @@ def is_iterable(o: object) -> bool:
     return is_iterable_
 
 
-def get_version(version: str) -> Tuple[int, ...]:
-    """Get the semantic ``version`` as a tuple of ``int`` s.
-
-    Note:
-        Assumes that the ``version`` string is specified as ``.``-separated ``ints``; will throw exceptions in
-        case of more complex version semantics.
-
-    Args:
-        module (ModuleType): The module to get the version of.
-
-    Returns:
-        Tuple[int, ...]: Tuple of integers representing ``(major, minor, patch[, ...])``.
-    """
-    return tuple(int(v) for v in version.split("."))
-
-
-def version_above_incl(version: Tuple[int, int], above_incl: Tuple[int, int]) -> bool:
-    v_major, v_minor = version
-    compare_major, compare_minor = above_incl
-    return (v_major == compare_major and v_minor >= compare_minor) or v_major > compare_major
-
-
-def version_below_excl(version: Tuple[int, int], below_excl: Tuple[int, int]) -> bool:
-    v_major, v_minor = version
-    compare_major, compare_minor = below_excl
-    return (v_major == compare_major and v_minor < compare_minor) or v_major < compare_major
-
-
 def ensure_literal_matches_dict_keys(
     literal: Any, d: Dict[str, Any], literal_name: str = "literal", dict_name: str = "dictionary"
 ):
diff --git a/src/tempor/data/clv2conv.py b/src/tempor/data/clv2conv.py
index 5f29ae60..0244b81c 100644
--- a/src/tempor/data/clv2conv.py
+++ b/src/tempor/data/clv2conv.py
@@ -62,7 +62,8 @@ def clairvoyance2_dataset_to_tempor_dataset(data: Clairvoyance2Dataset) -> datas
         targets_df = data.temporal_targets.to_multi_index_dataframe()
     elif data.event_targets is not None:
         targets_df = data.event_targets.df
-    else:
+    else:  # pragma: no cover
+        # Case caught by exceptions above.
         targets_df = None
 
     # Treatments.
@@ -81,6 +82,9 @@ def clairvoyance2_dataset_to_tempor_dataset(data: Clairvoyance2Dataset) -> datas
         targets_df = _from_clv2_time_series(targets_df)  # pyright: ignore
     elif data.event_targets is not None:
         targets_df = _from_clv2_event(targets_df)  # pyright: ignore
+    else:  # pragma: no cover
+        # Case caught by exceptions above.
+        targets_df = None
     if data.temporal_treatments is not None:
         treatments_df = _from_clv2_time_series(treatments_df)  # pyright: ignore
     elif data.event_treatments is not None:
@@ -95,7 +99,7 @@ def clairvoyance2_dataset_to_tempor_dataset(data: Clairvoyance2Dataset) -> datas
         TemporDatasetCls = dataset.OneOffTreatmentEffectsDataset
     elif data.temporal_targets is not None and data.temporal_treatments is not None:
         TemporDatasetCls = dataset.TemporalTreatmentEffectsDataset
-    else:
+    else:  # pragma: no cover
         raise ValueError(
             "Cannot convert a clairvoyance2 dataset to tempor dataset in this case, as it is "
             f"not supported, see clairvoyance2 dataset components:\n{data}"
diff --git a/src/tempor/data/dataset.py b/src/tempor/data/dataset.py
index 062a5951..0d13309d 100644
--- a/src/tempor/data/dataset.py
+++ b/src/tempor/data/dataset.py
@@ -246,7 +246,7 @@ def split(
 
 # `Dataset`s corresponding to different tasks follow. More can be added to handle new Tasks.
 
-# TODO: unit test CovariatesDataset.
+
 class CovariatesDataset(BaseDataset):
     def __init__(
         self,
diff --git a/src/tempor/data/pandera_utils.py b/src/tempor/data/pandera_utils.py
index 82880d31..24047b46 100644
--- a/src/tempor/data/pandera_utils.py
+++ b/src/tempor/data/pandera_utils.py
@@ -50,7 +50,7 @@
     "unique",
 ]
 
-if Version(pa.__version__) < Version("0.14"):
+if Version(pa.__version__) < Version("0.14"):  # pragma: no cover
     # Before v0.14, pandera API had an extra parameter `report_duplicates`.
     _PA_MULTI_INDEX_INIT_PARAMETERS.append("report_duplicates")
 
@@ -209,7 +209,7 @@ def check(
             if data_container is None:
                 # Only in case of direct type comparison, we also need to check via the union_dtype.check(pandera_dtype)
                 # method, making sure that pandera_dtype is an DataType instance not class.
-                if hasattr(pandera_dtype, "__mro__"):
+                if hasattr(pandera_dtype, "__mro__"):  # pragma: no cover
                     pandera_dtype = pandera_dtype()  # type: ignore
                 validated = validated or union_dtype.check(pandera_dtype)
             if tempor.core.utils.is_iterable(validated):
@@ -365,16 +365,17 @@ class checks:
         lambda df: df.index.nlevels == 2,
         error="Index must be a MultiIndex with 2 levels",
     )
-    require_2level_multiindex_one_to_one = pa.Check(
-        lambda df: (df.groupby(level=0).size() == 1).all(),
-        error="MultiIndex Index must one-to-one correspondence for between the two levels",
-    )
     require_element_len_2 = pa.Check(
         lambda x: len(x) == 2,
         element_wise=True,
         error="Each item must contain a sequence of length 2",
     )
 
+    # require_2level_multiindex_one_to_one = pa.Check(
+    #     lambda df: (df.groupby(level=0).size() == 1).all(),
+    #     error="MultiIndex Index must one-to-one correspondence for between the two levels",
+    # )
+
     class configurable:
         """Namespace containing functions to get configurable `pandera.Check` s."""
 
diff --git a/src/tempor/data/predictive.py b/src/tempor/data/predictive.py
index 73169d27..a0bfe031 100644
--- a/src/tempor/data/predictive.py
+++ b/src/tempor/data/predictive.py
@@ -11,9 +11,6 @@
     from .dataset import PredictiveDataset  # For typing only, no circular import.
 
 
-# TODO: Unit test.
-
-
 class PredictiveTaskData(abc.ABC):
     _targets: Optional[samples.DataSamples]
     _treatments: Optional[samples.DataSamples]
@@ -139,7 +136,7 @@ class OneOffTreatmentEffectsTaskData(PredictiveTaskData):
 
     @property
     def predictive_task(self) -> data_typing.PredictiveTask:
-        return data_typing.PredictiveTask.TEMPORAL_TREATMENT_EFFECTS
+        return data_typing.PredictiveTask.ONE_OFF_TREATMENT_EFFECTS
 
     def __init__(
         self,
diff --git a/src/tempor/data/samples.py b/src/tempor/data/samples.py
index afa0b582..42e896de 100644
--- a/src/tempor/data/samples.py
+++ b/src/tempor/data/samples.py
@@ -136,7 +136,7 @@ def short_repr(self) -> str:  # pragma: no cover
         ...
 
     @abc.abstractmethod
-    def __getitem__(self, key: data_typing.GetItemKey) -> Self:
+    def __getitem__(self, key: data_typing.GetItemKey) -> Self:  # pragma: no cover
         ...
 
 
diff --git a/src/tempor/data/utils.py b/src/tempor/data/utils.py
index f8032c44..dcaad74e 100644
--- a/src/tempor/data/utils.py
+++ b/src/tempor/data/utils.py
@@ -31,7 +31,7 @@ def value_in_df(df: pd.DataFrame, *, value: Any) -> bool:
 
 
 def set_df_column_names_inplace(df: pd.DataFrame, names: Sequence) -> pd.DataFrame:
-    if Version(pd.__version__) < Version("1.5"):
+    if Version(pd.__version__) < Version("1.5"):  # pragma: no cover
         df.set_axis(names, axis="columns", inplace=True)  # pyright: ignore
         return df
     else:
diff --git a/src/tempor/log/_custom_logger.py b/src/tempor/log/_custom_logger.py
index 3a37b2de..4c089c9b 100644
--- a/src/tempor/log/_custom_logger.py
+++ b/src/tempor/log/_custom_logger.py
@@ -118,7 +118,7 @@ def _configure_loggers(config: conf.TemporConfig):
 conf.updated_on_configure.add(_configure_loggers)
 
 
-if TYPE_CHECKING:
+if TYPE_CHECKING:  # pragma: no cover
     # This is to allow type checkers to recognize the custom added logger.print method.
 
     from loguru import Logger as _Logger
diff --git a/src/tempor/models/ddh.py b/src/tempor/models/ddh.py
index b4d4e465..666d5135 100644
--- a/src/tempor/models/ddh.py
+++ b/src/tempor/models/ddh.py
@@ -156,7 +156,7 @@ def fit(
                 tb = t_train[j * self.batch_size : (j + 1) * self.batch_size]
                 eb = e_train[j * self.batch_size : (j + 1) * self.batch_size]
 
-                if xb.shape[0] == 0:
+                if xb.shape[0] == 0:  # pragma: no cover
                     continue
 
                 optimizer.zero_grad()
@@ -178,12 +178,12 @@ def fit(
                 tb = t_val[j * self.batch_size : (j + 1) * self.batch_size]
                 eb = e_val[j * self.batch_size : (j + 1) * self.batch_size]
 
-                if xb.shape[0] == 0:
+                if xb.shape[0] == 0:  # pragma: no cover
                     continue
 
                 valid_loss += self.total_loss(xb, tb, eb)
 
-            if torch.isnan(valid_loss):
+            if torch.isnan(valid_loss):  # pragma: no cover
                 raise RuntimeError("NaNs detected in the total loss")
 
             valid_loss = valid_loss.item()
@@ -260,7 +260,7 @@ def predict_emb(
         x: np.ndarray,
     ) -> torch.Tensor:
         if self.model is None:
-            raise Exception(
+            raise RuntimeError(
                 "The model has not been fitted yet. Please fit the "
                 + "model using the `fit` method on some training data "
                 + "before calling `predict_survival`."
@@ -280,7 +280,7 @@ def predict_survival(
         bs: int = 100,
     ) -> np.ndarray:
         if self.model is None:
-            raise Exception(
+            raise RuntimeError(
                 "The model has not been fitted yet. Please fit the "
                 + "model using the `fit` method on some training data "
                 + "before calling `predict_survival`."
@@ -308,7 +308,7 @@ def predict_survival(
 
                 if isinstance(pred, list):
                     scores[t_].extend(pred)
-                else:
+                else:  # pragma: no cover
                     scores[t_].append(pred)
 
         output = []
@@ -556,7 +556,7 @@ def forward_emb(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
         inputmask = torch.isnan(x[:, :, 0])
         x[torch.isnan(x)] = -1
 
-        if torch.isnan(x).sum() != 0:
+        if torch.isnan(x).sum() != 0:  # pragma: no cover
             raise RuntimeError("NaNs detected in the input")
 
         if self.rnn_type in ["GRU", "LSTM", "RNN"]:
@@ -564,12 +564,12 @@ def forward_emb(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
         else:
             hidden = self.embedding(x)
 
-        if torch.isnan(hidden).sum() != 0:
+        if torch.isnan(hidden).sum() != 0:  # pragma: no cover
             raise RuntimeError("NaNs detected in the embeddings")
 
         # Longitudinal modelling
         longitudinal_prediction = self.longitudinal(hidden)
-        if torch.isnan(longitudinal_prediction).sum() != 0:
+        if torch.isnan(longitudinal_prediction).sum() != 0:  # pragma: no cover
             raise RuntimeError("NaNs detected in the longitudinal_prediction")
 
         hidden_attentive = self.forward_attention(x, inputmask, hidden)
@@ -590,7 +590,7 @@ def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, List]:
         # Soft max for probability distribution
         outcomes_t = torch.cat(outcomes, dim=1)
         outcomes_t = self.soft(outcomes_t)
-        if torch.isnan(outcomes_t).sum() != 0:
+        if torch.isnan(outcomes_t).sum() != 0:  # pragma: no cover
             raise RuntimeError("NaNs detected in the outcome")
 
         outcomes = [outcomes_t[:, i * self.output_dim : (i + 1) * self.output_dim] for i in range(self.risks)]
diff --git a/src/tempor/models/mlp.py b/src/tempor/models/mlp.py
index 591e85b6..01296ed5 100644
--- a/src/tempor/models/mlp.py
+++ b/src/tempor/models/mlp.py
@@ -342,7 +342,7 @@ def _train_epoch(self, loader: torch.utils.data.DataLoader) -> float:
             self.optimizer.zero_grad()
 
             X_next, y_next = sample
-            if len(X_next) < 2:
+            if len(X_next) < 2:  # pragma: no cover
                 continue
 
             preds = self.forward(X_next).squeeze()
diff --git a/src/tempor/models/samplers.py b/src/tempor/models/samplers.py
index 47c1a476..065b317b 100644
--- a/src/tempor/models/samplers.py
+++ b/src/tempor/models/samplers.py
@@ -11,28 +11,30 @@
 class BaseSampler(torch.utils.data.sampler.Sampler):
     """DataSampler samples the conditional vector and corresponding data."""
 
-    def get_dataset_conditionals(self) -> Optional[np.ndarray]:
+    def get_dataset_conditionals(self) -> Optional[np.ndarray]:  # pragma: no cover
         return None
 
     @validate_arguments(config=dict(arbitrary_types_allowed=True))
-    def sample_conditional(self, batch: int, **kwargs: Any) -> Optional[Tuple]:  # pylint: disable=unused-argument
+    def sample_conditional(
+        self, batch: int, **kwargs: Any  # pylint: disable=unused-argument
+    ) -> Optional[Tuple]:  # pragma: no cover
         return None
 
     @validate_arguments(config=dict(arbitrary_types_allowed=True))
     def sample_conditional_for_class(
         self, batch: int, c: int  # pylint: disable=unused-argument
-    ) -> Optional[np.ndarray]:
+    ) -> Optional[np.ndarray]:  # pragma: no cover
         return None
 
-    def conditional_dimension(self) -> int:
+    def conditional_dimension(self) -> int:  # pragma: no cover
         """Return the total number of categories."""
         return 0
 
-    def conditional_probs(self) -> Optional[np.ndarray]:
+    def conditional_probs(self) -> Optional[np.ndarray]:  # pragma: no cover
         """Return the total number of categories."""
         return None
 
-    def train_test(self) -> Tuple:
+    def train_test(self) -> Tuple:  # pragma: no cover
         raise NotImplementedError()
 
 
diff --git a/src/tempor/models/ts_model.py b/src/tempor/models/ts_model.py
index 12309f15..54d516f4 100644
--- a/src/tempor/models/ts_model.py
+++ b/src/tempor/models/ts_model.py
@@ -149,7 +149,7 @@ def __init__(
             self.loss = nn.MSELoss()
         elif task_type == "classification":
             self.loss = nn.CrossEntropyLoss()
-        else:
+        else:  # Prevented by pydantic.  # pragma: no cover
             raise ValueError(f"Invalid task type {task_type}")
 
         self.n_iter = n_iter
@@ -241,7 +241,7 @@ def forward(
         else:
             temporal_data_merged = temporal_data
 
-        if torch.isnan(temporal_data_merged).sum() != 0:
+        if torch.isnan(temporal_data_merged).sum() != 0:  # pragma: no cover
             raise ValueError("NaNs detected in the temporal merged data")
 
         pred = self.temporal_layer(static_data, temporal_data_merged)
diff --git a/src/tempor/models/ts_ode.py b/src/tempor/models/ts_ode.py
index f232a70b..47a00a65 100644
--- a/src/tempor/models/ts_ode.py
+++ b/src/tempor/models/ts_ode.py
@@ -321,7 +321,6 @@ def __init__(
                 n_units_latent=n_units_hidden,
                 device=device,
             )
-
         else:
             raise RuntimeError(f"Invalid ODE backend {self.backend}")
 
@@ -337,7 +336,7 @@ def __init__(
                 nonlin=nonlin,
                 device=device,
             )
-        elif self.backend == "laplace":
+        else:  # self.backend == "laplace":
             self.initial_temporal = ReverseGRUEncoder(
                 n_temporal_units_in + 1,
                 n_units_latent=n_units_hidden,
@@ -628,7 +627,7 @@ def _train_epoch(self, loaders: List[DataLoader]) -> float:
                 self.optimizer.zero_grad()  # clear gradients for this training step
 
                 pred = self(static_mb, temporal_mb, horizons_mb)  # rnn output
-                if torch.isnan(pred).sum() > 0:
+                if torch.isnan(pred).sum() > 0:  # pragma: no cover
                     raise RuntimeError("NaNs in the training prediction")
 
                 loss = self.loss(pred.squeeze(), y_mb.squeeze())
@@ -638,7 +637,7 @@ def _train_epoch(self, loaders: List[DataLoader]) -> float:
                     torch.nn.utils.clip_grad_norm_(self.parameters(), self.clipping_value)  # pyright: ignore
                 self.optimizer.step()  # apply gradients
 
-                if torch.isnan(loss):
+                if torch.isnan(loss):  # pragma: no cover
                     raise RuntimeError("NaNs in the loss")
 
                 losses.append(loss.detach().cpu())
@@ -654,7 +653,7 @@ def _test_epoch(self, loaders: List[DataLoader]) -> float:
                 loader
             ):
                 pred = self(static_mb, temporal_mb, horizons_mb)  # ODE output
-                if torch.isnan(pred).sum() > 0:
+                if torch.isnan(pred).sum() > 0:  # pragma: no cover
                     raise RuntimeError("NaNs in the test prediction")
                 loss = self.loss(pred.squeeze(), y_mb.squeeze())
 
diff --git a/src/tempor/plugins/core/_base_estimator.py b/src/tempor/plugins/core/_base_estimator.py
index dd6139ae..71252c4c 100644
--- a/src/tempor/plugins/core/_base_estimator.py
+++ b/src/tempor/plugins/core/_base_estimator.py
@@ -41,8 +41,7 @@ def root_checks(cls, values: Dict):  # pylint: disable=no-self-argument
                 assert ParamsDefinitionClass is not None  # nosec B101
 
             try:
-                if not pydantic_utils.is_pydantic_dataclass(ParamsDefinitionClass):
-                    ParamsDefinitionClass = pydantic_utils.make_pydantic_dataclass(ParamsDefinitionClass)
+                ParamsDefinitionClass = pydantic_utils.make_pydantic_dataclass(ParamsDefinitionClass)
                 defined_params = omegaconf.OmegaConf.structured(dataclasses.asdict(ParamsDefinitionClass(**params)))
             except Exception as ex:
                 name = utils.get_class_full_name(ex)
@@ -99,7 +98,7 @@ def fit(
         return fitted_model
 
     @abc.abstractmethod
-    def _fit(self, data: dataset.BaseDataset, *args, **kwargs) -> Self:
+    def _fit(self, data: dataset.BaseDataset, *args, **kwargs) -> Self:  # pragma: no cover
         ...
 
     @staticmethod
diff --git a/src/tempor/plugins/core/_base_predictor.py b/src/tempor/plugins/core/_base_predictor.py
index db558d13..0dc07014 100644
--- a/src/tempor/plugins/core/_base_predictor.py
+++ b/src/tempor/plugins/core/_base_predictor.py
@@ -19,6 +19,8 @@ def predict(
         *args,
         **kwargs,
     ) -> Any:  # TODO: Narrow down output formats later.
+        if not self.is_fitted:
+            raise ValueError("The model was not fitted, call `fit` first")
         if not data.predict_ready:
             raise ValueError(
                 f"The dataset was not predict-ready, check that all necessary data components are present:\n{data}"
@@ -35,6 +37,8 @@ def predict_proba(
         *args,
         **kwargs,
     ) -> Any:  # TODO: Narrow down output formats later.
+        if not self.is_fitted:
+            raise ValueError("The model was not fitted, call `fit` first")
         if not data.predict_ready:
             raise ValueError(
                 f"The dataset was not predict-ready, check that all necessary data components are present:\n{data}"
@@ -51,6 +55,8 @@ def predict_counterfactuals(
         *args,
         **kwargs,
     ) -> Any:  # TODO: Narrow down output formats later.
+        if not self.is_fitted:
+            raise ValueError("The model was not fitted, call `fit` first")
         if not data.predict_ready:
             raise ValueError(
                 f"The dataset was not predict-ready, check that all necessary data components are present:\n{data}"
@@ -61,6 +67,7 @@ def predict_counterfactuals(
 
         return prediction
 
+    # TODO: Add similar methods for predict_{proba,counterfactuals}.
     @pydantic.validate_arguments(config=dict(arbitrary_types_allowed=True))
     def fit_predict(
         self,
diff --git a/src/tempor/plugins/core/_plugin.py b/src/tempor/plugins/core/_plugin.py
index 0ba9117e..76f15e06 100644
--- a/src/tempor/plugins/core/_plugin.py
+++ b/src/tempor/plugins/core/_plugin.py
@@ -101,7 +101,13 @@ def class_decorator(cls: Callable[P, T]) -> Callable[P, T]:
         for existing_cls in PLUGIN_REGISTRY.values():
             # Cannot have the same plugin name (not just fqn), as this is not supported by Pipeline.
             if cls.name == existing_cls.name:
-                raise TypeError(f"Plugin with name {cls.name} already registered (as class {existing_cls})")
+                if not _check_same_class(cls, existing_cls):
+                    raise TypeError(f"Plugin with name {cls.name} already registered (as class {existing_cls})")
+                else:  # pragma: no cover
+                    # The same class is being reimported, do not raise error.
+                    # Some kind of coverage issues - this case *is* covered by test:
+                    # test_plugins.py::TestRegistration::test_category_registration_reimport_allowed
+                    pass
 
         PLUGIN_REGISTRY[cls.fqn()] = cls
 
@@ -172,7 +178,8 @@ def _glob_plugin_paths(package_dir: str) -> List[str]:
 def _module_name_from_path(path: str) -> str:
     path = os.path.normpath(path)
     split = path[path.rfind(f"{tempor.import_name}{os.sep}") :].split(os.sep)
-    if split[-1][-3:] != ".py":
+    if split[-1][-3:] != ".py":  # pragma: no cover
+        # Should be prevented by `_glob_plugin_paths`.
         raise ValueError(f"Path `{path}` is not a python file.")
     split[-1] = split[-1].replace(".py", "")
     return ".".join(split)
diff --git a/src/tempor/plugins/core/_types.py b/src/tempor/plugins/core/_types.py
index 83cb8f7b..8b745240 100644
--- a/src/tempor/plugins/core/_types.py
+++ b/src/tempor/plugins/core/_types.py
@@ -1,8 +1,9 @@
-import enum
+# NOTE: Currently unused.
 
-
-class EstimatorMethods(enum.Enum):
-    FIT = enum.auto()
-    TRANSFORM = enum.auto()
-    PREDICT = enum.auto()
-    PREDICT_COUNTERFACTUAL = enum.auto()
+# import enum
+#
+# class EstimatorMethods(enum.Enum):
+#     FIT = enum.auto()
+#     TRANSFORM = enum.auto()
+#     PREDICT = enum.auto()
+#     PREDICT_COUNTERFACTUAL = enum.auto()
diff --git a/src/tempor/plugins/pipeline/generators.py b/src/tempor/plugins/pipeline/generators.py
index ec5992a8..1c7c8d54 100644
--- a/src/tempor/plugins/pipeline/generators.py
+++ b/src/tempor/plugins/pipeline/generators.py
@@ -2,7 +2,7 @@
 
 from tempor.data import dataset
 
-if TYPE_CHECKING:
+if TYPE_CHECKING:  # pragma: no cover
     from tempor.plugins.core._params import Params
 
 
diff --git a/src/tempor/plugins/prediction/one_off/classification/__init__.py b/src/tempor/plugins/prediction/one_off/classification/__init__.py
index 7c5ef022..8e695876 100644
--- a/src/tempor/plugins/prediction/one_off/classification/__init__.py
+++ b/src/tempor/plugins/prediction/one_off/classification/__init__.py
@@ -47,11 +47,13 @@ def predict_proba(
         return super().predict_proba(data, *args, **kwargs)
 
     @abc.abstractmethod
-    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:  # pragma: no cover
         ...
 
     @abc.abstractmethod
-    def _predict_proba(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+    def _predict_proba(
+        self, data: dataset.PredictiveDataset, *args, **kwargs
+    ) -> samples.StaticSamples:  # pragma: no cover
         ...
 
     def _unpack_dataset(self, data: dataset.BaseDataset) -> Tuple:
diff --git a/src/tempor/plugins/prediction/one_off/regression/__init__.py b/src/tempor/plugins/prediction/one_off/regression/__init__.py
index 7997c7f7..060d5a83 100644
--- a/src/tempor/plugins/prediction/one_off/regression/__init__.py
+++ b/src/tempor/plugins/prediction/one_off/regression/__init__.py
@@ -37,7 +37,7 @@ def predict(
         return super().predict(data, *args, **kwargs)
 
     @abc.abstractmethod
-    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:  # pragma: no cover
         ...
 
     def _unpack_dataset(self, data: dataset.BaseDataset) -> Tuple:
diff --git a/src/tempor/plugins/prediction/one_off/regression/plugin_cde_regressor.py b/src/tempor/plugins/prediction/one_off/regression/plugin_cde_regressor.py
index 6580ea34..31a54538 100644
--- a/src/tempor/plugins/prediction/one_off/regression/plugin_cde_regressor.py
+++ b/src/tempor/plugins/prediction/one_off/regression/plugin_cde_regressor.py
@@ -153,21 +153,6 @@ def _predict(
         preds = preds.reshape(-1, 1)
         return samples.StaticSamples.from_numpy(preds)
 
-    def _predict_proba(
-        self,
-        data: dataset.PredictiveDataset,
-        *args,
-        **kwargs,
-    ) -> samples.StaticSamples:
-        if self.model is None:
-            raise RuntimeError("Fit the model first")
-        static, temporal, observation_times, _ = self._unpack_dataset(data)
-
-        preds = self.model.predict_proba(static, temporal, observation_times)
-        preds = preds.astype(float)
-
-        return samples.StaticSamples.from_numpy(preds)
-
     @staticmethod
     def hyperparameter_space(*args, **kwargs):
         return [
diff --git a/src/tempor/plugins/prediction/one_off/regression/plugin_laplace_regressor.py b/src/tempor/plugins/prediction/one_off/regression/plugin_laplace_regressor.py
index 3c458cee..a184a216 100644
--- a/src/tempor/plugins/prediction/one_off/regression/plugin_laplace_regressor.py
+++ b/src/tempor/plugins/prediction/one_off/regression/plugin_laplace_regressor.py
@@ -156,21 +156,6 @@ def _predict(
         preds = preds.reshape(-1, 1)
         return samples.StaticSamples.from_numpy(preds)
 
-    def _predict_proba(
-        self,
-        data: dataset.PredictiveDataset,
-        *args,
-        **kwargs,
-    ) -> samples.StaticSamples:
-        if self.model is None:
-            raise RuntimeError("Fit the model first")
-        static, temporal, observation_times, _ = self._unpack_dataset(data)
-
-        preds = self.model.predict_proba(static, temporal, observation_times)
-        preds = preds.astype(float)
-
-        return samples.StaticSamples.from_numpy(preds)
-
     @staticmethod
     def hyperparameter_space(*args, **kwargs):
         return [
diff --git a/src/tempor/plugins/prediction/one_off/regression/plugin_ode_regressor.py b/src/tempor/plugins/prediction/one_off/regression/plugin_ode_regressor.py
index 0eb0c1bc..d6d5e133 100644
--- a/src/tempor/plugins/prediction/one_off/regression/plugin_ode_regressor.py
+++ b/src/tempor/plugins/prediction/one_off/regression/plugin_ode_regressor.py
@@ -153,21 +153,6 @@ def _predict(
         preds = preds.reshape(-1, 1)
         return samples.StaticSamples.from_numpy(preds)
 
-    def _predict_proba(
-        self,
-        data: dataset.PredictiveDataset,
-        *args,
-        **kwargs,
-    ) -> samples.StaticSamples:
-        if self.model is None:
-            raise RuntimeError("Fit the model first")
-        static, temporal, observation_times, _ = self._unpack_dataset(data)
-
-        preds = self.model.predict_proba(static, temporal, observation_times)
-        preds = preds.astype(float)
-
-        return samples.StaticSamples.from_numpy(preds)
-
     @staticmethod
     def hyperparameter_space(*args, **kwargs):
         return [
diff --git a/src/tempor/plugins/prediction/temporal/classification/__init__.py b/src/tempor/plugins/prediction/temporal/classification/__init__.py
index 6ad45081..9665108c 100644
--- a/src/tempor/plugins/prediction/temporal/classification/__init__.py
+++ b/src/tempor/plugins/prediction/temporal/classification/__init__.py
@@ -51,13 +51,13 @@ def predict_proba(  # type: ignore[override]  # pylint: disable=arguments-differ
     @abc.abstractmethod
     def _predict(  # type: ignore[override]  # pylint: disable=arguments-differ
         self, data: dataset.PredictiveDataset, n_future_steps: int, *args, time_delta: int = 1, **kwargs
-    ) -> samples.TimeSeriesSamples:
+    ) -> samples.TimeSeriesSamples:  # pragma: no cover
         ...
 
     @abc.abstractmethod
     def _predict_proba(  # type: ignore[override]  # pylint: disable=arguments-differ
         self, data: dataset.PredictiveDataset, n_future_steps: int, *args, time_delta: int = 1, **kwargs
-    ) -> samples.TimeSeriesSamples:
+    ) -> samples.TimeSeriesSamples:  # pragma: no cover
         ...
 
 
diff --git a/src/tempor/plugins/prediction/temporal/classification/plugin_seq2seq_classifier.py b/src/tempor/plugins/prediction/temporal/classification/plugin_seq2seq_classifier.py
index f70bdce3..8e438598 100644
--- a/src/tempor/plugins/prediction/temporal/classification/plugin_seq2seq_classifier.py
+++ b/src/tempor/plugins/prediction/temporal/classification/plugin_seq2seq_classifier.py
@@ -133,7 +133,7 @@ def _predict_proba(
         *args,
         **kwargs,
     ) -> samples.TimeSeriesSamples:
-        raise NotImplementedError("not supported")
+        raise NotImplementedError("Not currently supported")
 
     @staticmethod
     def hyperparameter_space(*args, **kwargs):
diff --git a/src/tempor/plugins/prediction/temporal/regression/__init__.py b/src/tempor/plugins/prediction/temporal/regression/__init__.py
index c82acec6..a1698e2c 100644
--- a/src/tempor/plugins/prediction/temporal/regression/__init__.py
+++ b/src/tempor/plugins/prediction/temporal/regression/__init__.py
@@ -39,7 +39,7 @@ def predict(  # type: ignore[override]  # pylint: disable=arguments-differ
     @abc.abstractmethod
     def _predict(  # type: ignore[override]  # pylint: disable=arguments-differ
         self, data: dataset.PredictiveDataset, n_future_steps: int, *args, time_delta: int = 1, **kwargs
-    ) -> samples.TimeSeriesSamples:
+    ) -> samples.TimeSeriesSamples:  # pragma: no cover
         ...
 
 
diff --git a/src/tempor/plugins/time_to_event/helper_embedding.py b/src/tempor/plugins/time_to_event/helper_embedding.py
index 82c02e5e..5c171936 100644
--- a/src/tempor/plugins/time_to_event/helper_embedding.py
+++ b/src/tempor/plugins/time_to_event/helper_embedding.py
@@ -14,11 +14,11 @@
 
 class OutputTimeToEventAnalysis:
     @abc.abstractmethod
-    def fit(self, X: pd.DataFrame, T: pd.Series, Y: pd.Series) -> Self:
+    def fit(self, X: pd.DataFrame, T: pd.Series, Y: pd.Series) -> Self:  # pragma: no cover
         ...
 
     @abc.abstractmethod
-    def predict_risk(self, X: pd.DataFrame, time_horizons: List) -> pd.DataFrame:
+    def predict_risk(self, X: pd.DataFrame, time_horizons: List) -> pd.DataFrame:  # pragma: no cover
         ...
 
 
diff --git a/src/tempor/plugins/time_to_event/plugin_ts_coxph.py b/src/tempor/plugins/time_to_event/plugin_ts_coxph.py
index a0516ed9..9582579e 100644
--- a/src/tempor/plugins/time_to_event/plugin_ts_coxph.py
+++ b/src/tempor/plugins/time_to_event/plugin_ts_coxph.py
@@ -31,7 +31,7 @@ def problem_versions() -> bool:
         # so the workaround is needed for lifelines < 0.27.6.
         return Version(pd.__version__) >= Version("2.0.0rc0") and Version(lifelines.__version__) < Version("0.27.6")
 
-    if problem_versions():
+    if problem_versions():  # pragma: no cover
         # Monkeypatch `pandas.DataFrame.describe`.
 
         original_pd_df_describe = pd.DataFrame.describe
@@ -47,7 +47,7 @@ def monkeypatched_describe(*args, **kwargs):
         yield
 
     finally:
-        if problem_versions():
+        if problem_versions():  # pragma: no cover
             # Restore `pandas.DataFrame.describe`.
             pd.DataFrame.describe = original_pd_df_describe  # pyright: ignore
 
diff --git a/src/tempor/plugins/time_to_event/plugin_ts_xgb.py b/src/tempor/plugins/time_to_event/plugin_ts_xgb.py
index ab5b70ab..647c1f99 100644
--- a/src/tempor/plugins/time_to_event/plugin_ts_xgb.py
+++ b/src/tempor/plugins/time_to_event/plugin_ts_xgb.py
@@ -163,6 +163,8 @@ def __init__(
             self.model = XGBSEStackedWeibull(xgboost_params)
         elif strategy == "km":
             self.model = XGBSEKaplanNeighbors(xgboost_params)
+        else:
+            raise ValueError(f"Unknown strategy: {strategy}")
 
         self.time_points = time_points
 
@@ -173,7 +175,7 @@ def fit(self, X: pd.DataFrame, T: pd.Series, Y: pd.Series):
         obs_times = T[Y == 1]
 
         lower_bound = max(censored_times.min(), obs_times.min()) + 1
-        if pd.isna(lower_bound):
+        if pd.isna(lower_bound):  # pragma: no cover
             lower_bound = T.min()
         upper_bound = T.max()
 
diff --git a/src/tempor/plugins/treatments/one_off/_base.py b/src/tempor/plugins/treatments/one_off/_base.py
index 2feb5b0f..54f55bf3 100644
--- a/src/tempor/plugins/treatments/one_off/_base.py
+++ b/src/tempor/plugins/treatments/one_off/_base.py
@@ -36,7 +36,7 @@ def predict(
         return super().predict(data, *args, **kwargs)
 
     @abc.abstractmethod
-    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:  # pragma: no cover
         ...
 
     @pydantic.validate_arguments(config=dict(arbitrary_types_allowed=True))
@@ -50,5 +50,5 @@ def predict_counterfactuals(
         return super().predict_counterfactuals(data, *args, **kwargs)
 
     @abc.abstractmethod
-    def _predict_counterfactuals(self, data: dataset.PredictiveDataset, *args, **kwargs) -> List:
+    def _predict_counterfactuals(self, data: dataset.PredictiveDataset, *args, **kwargs) -> List:  # pragma: no cover
         ...
diff --git a/src/tempor/plugins/treatments/temporal/_base.py b/src/tempor/plugins/treatments/temporal/_base.py
index 105d024b..01c9583d 100644
--- a/src/tempor/plugins/treatments/temporal/_base.py
+++ b/src/tempor/plugins/treatments/temporal/_base.py
@@ -31,12 +31,14 @@ def predict(
         data: dataset.PredictiveDataset,
         *args,
         **kwargs,
-    ) -> samples.StaticSamples:
+    ) -> samples.TimeSeriesSamples:
         check_data_class(data)
         return super().predict(data, *args, **kwargs)
 
     @abc.abstractmethod
-    def _predict(self, data: dataset.PredictiveDataset, *args, **kwargs) -> samples.StaticSamples:
+    def _predict(
+        self, data: dataset.PredictiveDataset, *args, **kwargs
+    ) -> samples.TimeSeriesSamples:  # pragma: no cover
         ...
 
     @pydantic.validate_arguments(config=dict(arbitrary_types_allowed=True))
@@ -50,5 +52,5 @@ def predict_counterfactuals(
         return super().predict_counterfactuals(data, *args, **kwargs)
 
     @abc.abstractmethod
-    def _predict_counterfactuals(self, data: dataset.PredictiveDataset, *args, **kwargs) -> List:
+    def _predict_counterfactuals(self, data: dataset.PredictiveDataset, *args, **kwargs) -> List:  # pragma: no cover
         ...
diff --git a/src/tempor/utils/dataloaders/uci_diabetes.py b/src/tempor/utils/dataloaders/uci_diabetes.py
index 46190b68..85139426 100644
--- a/src/tempor/utils/dataloaders/uci_diabetes.py
+++ b/src/tempor/utils/dataloaders/uci_diabetes.py
@@ -50,7 +50,7 @@ def load(self, **kwargs) -> dataset.TemporalPredictionDataset:
                     make_regular=self.make_regular,
                     use_int_index=self.use_int_index,
                 )
-            except (requests.exceptions.RequestException, urllib.error.URLError) as ex:
+            except (requests.exceptions.RequestException, urllib.error.URLError) as ex:  # pragma: no cover
                 if retry + 1 == download_retries:
                     logger.error(f"Failed to download UCI diabetes dataset after {download_retries} retries.")
                     raise
diff --git a/tutorials/data/tutorial02_datasets.ipynb b/tutorials/data/tutorial02_datasets.ipynb
index fe9ec36c..f43dda9c 100644
--- a/tutorials/data/tutorial02_datasets.ipynb
+++ b/tutorials/data/tutorial02_datasets.ipynb
@@ -34,7 +34,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 0. Prepare some example data"
+    "## Prepare some example data"
    ]
   },
   {
@@ -336,7 +336,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 1. `CovariatesDataset`\n",
+    "## `CovariatesDataset`\n",
     "\n",
     "A `CovariatesDataset` contains time series and optionally static covariates only, without any predictive data\n",
     "(targets or treatments).\n",
@@ -576,7 +576,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 1. `OneOffPredictionDataset`\n",
+    "## `OneOffPredictionDataset`\n",
     "\n",
     "A `OneOffPredictionDataset` contains time series and optionally static covariates.\n",
     "\n",
@@ -873,7 +873,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 1. `TemporalPredictionDataset`\n",
+    "## `TemporalPredictionDataset`\n",
     "\n",
     "A `TemporalPredictionDataset` contains time series and optionally static covariates.\n",
     "\n",
@@ -1193,7 +1193,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 1. `TimeToEventAnalysisDataset`\n",
+    "## `TimeToEventAnalysisDataset`\n",
     "\n",
     "A `TimeToEventAnalysisDataset` contains time series and optionally static covariates.\n",
     "\n",
@@ -1500,7 +1500,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 1. `OneOffTreatmentEffectsDataset`\n",
+    "## `OneOffTreatmentEffectsDataset`\n",
     "\n",
     "A `OneOffTreatmentEffectsDataset` contains time series and optionally static covariates.\n",
     "\n",
@@ -1892,7 +1892,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 1. `TemporalTreatmentEffectsDataset`\n",
+    "## `TemporalTreatmentEffectsDataset`\n",
     "\n",
     "A `TemporalTreatmentEffectsDataset` contains time series and optionally static covariates.\n",
     "\n",
diff --git a/tutorials/data/tutorial04_data_splitting.ipynb b/tutorials/data/tutorial04_data_splitting.ipynb
index 57f4a902..f6ea5c50 100644
--- a/tutorials/data/tutorial04_data_splitting.ipynb
+++ b/tutorials/data/tutorial04_data_splitting.ipynb
@@ -34,7 +34,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 0. Splitting data"
+    "## 1. Splitting data"
    ]
   },
   {
@@ -113,7 +113,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "## 0. Using `KFold` and other `sklearn` splitter classes\n",
+    "## 2. Using `KFold` and other `sklearn` splitter classes\n",
     "\n",
     "Any of the [sklearn splitter classes](https://scikit-learn.org/stable/modules/classes.html#splitter-classes) can be used with `Dataset` `split` method.\n",
     "\n",
diff --git a/tutorials/user_guide/tutorial01_plugins.ipynb b/tutorials/usage/tutorial01_plugins.ipynb
similarity index 100%
rename from tutorials/user_guide/tutorial01_plugins.ipynb
rename to tutorials/usage/tutorial01_plugins.ipynb
diff --git a/tutorials/user_guide/tutorial02_imputation.ipynb b/tutorials/usage/tutorial02_imputation.ipynb
similarity index 100%
rename from tutorials/user_guide/tutorial02_imputation.ipynb
rename to tutorials/usage/tutorial02_imputation.ipynb
diff --git a/tutorials/user_guide/tutorial03_scaling.ipynb b/tutorials/usage/tutorial03_scaling.ipynb
similarity index 100%
rename from tutorials/user_guide/tutorial03_scaling.ipynb
rename to tutorials/usage/tutorial03_scaling.ipynb
diff --git a/tutorials/user_guide/tutorial04_prediction.ipynb b/tutorials/usage/tutorial04_prediction.ipynb
similarity index 100%
rename from tutorials/user_guide/tutorial04_prediction.ipynb
rename to tutorials/usage/tutorial04_prediction.ipynb
diff --git a/tutorials/user_guide/tutorial05_time_to_event.ipynb b/tutorials/usage/tutorial05_time_to_event.ipynb
similarity index 100%
rename from tutorials/user_guide/tutorial05_time_to_event.ipynb
rename to tutorials/usage/tutorial05_time_to_event.ipynb
diff --git a/tutorials/user_guide/tutorial06_treatments.ipynb b/tutorials/usage/tutorial06_treatments.ipynb
similarity index 100%
rename from tutorials/user_guide/tutorial06_treatments.ipynb
rename to tutorials/usage/tutorial06_treatments.ipynb
diff --git a/tutorials/user_guide/tutorial07_pipeline.ipynb b/tutorials/usage/tutorial07_pipeline.ipynb
similarity index 100%
rename from tutorials/user_guide/tutorial07_pipeline.ipynb
rename to tutorials/usage/tutorial07_pipeline.ipynb
diff --git a/tutorials/user_guide/tutorial08_benchmarks.ipynb b/tutorials/usage/tutorial08_benchmarks.ipynb
similarity index 100%
rename from tutorials/user_guide/tutorial08_benchmarks.ipynb
rename to tutorials/usage/tutorial08_benchmarks.ipynb
diff --git a/tutorials/user_guide/tutorial09_automl.ipynb b/tutorials/usage/tutorial09_automl.ipynb
similarity index 99%
rename from tutorials/user_guide/tutorial09_automl.ipynb
rename to tutorials/usage/tutorial09_automl.ipynb
index 9dc1758d..3e39802b 100644
--- a/tutorials/user_guide/tutorial09_automl.ipynb
+++ b/tutorials/usage/tutorial09_automl.ipynb
@@ -41,7 +41,7 @@
     "1. `MethodSeeker`: Search the hyperparameter space of a particular predictive method.\n",
     "2. `PipelineSeeker`: Search the hyperparameter space of a pipeline like `preprocessing steps -> predictive step`.\n",
     "\n",
-    "The optimization strategies are facilitated by [`optuna`]() and the currently supported strategies are:\n",
+    "The optimization strategies are facilitated by [`optuna`](https://optuna.readthedocs.io/) and the currently supported strategies are:\n",
     "* [Bayesian, specifically Tree-structured Parzen estimator](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) (`\"bayesian\"`),\n",
     "* [Random](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.RandomSampler.html) (`\"random\"`),\n",
     "* [CMA-ES](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.CmaEsSampler.html) (`\"cmaes\"`),\n",


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e4a3e2f2..36572483 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -47,9 +47,6 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: true
-      # Prevents scheduled run being disabled after no commit
-      # for some time (creates a dummy commit):
-      - uses: gautamkrishnar/keepalive-workflow@v1
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v4
         with:
diff --git a/.github/workflows/test_deps.yml b/.github/workflows/test_deps.yml
new file mode 100644
index 00000000..21e23e56
--- /dev/null
+++ b/.github/workflows/test_deps.yml
@@ -0,0 +1,70 @@
+name: "Tests: Dependency Versions"
+
+on:
+  schedule:
+    # This schedule is monthly.
+    - cron: "29 3 12 * *"
+  workflow_dispatch:
+
+jobs:
+  # Lint code with pre-commit.
+  Linter:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        python-version: [3.8]
+        os: [ubuntu-latest]
+    env:
+      CODECOV_OS: ${{ matrix.os }}
+      CODECOV_PYTHON: ${{ matrix.python-version }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          pip install --upgrade pip
+          pip install pre-commit
+      - name: Run pre-commit validation
+        run: pre-commit run --all-files
+
+  # Run tests with pytest.
+  # Test with the dependency versions specified in the matrix.
+  Tests:
+    needs: [Linter]
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        python-version: [3.8]
+        os: [ubuntu-latest]
+        dep:
+          - torch==1.10
+          - torch>=2
+          - pandas==1.3
+          - pandas>=2
+          - numpy==1.20
+          - numpy>=1.24
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+          cache: "pip" # Caching pip dependencies.
+      - name: Install dependencies
+        run: |
+          pip install --upgrade pip
+          pip install .[dev]
+          pip install "${{ matrix.python-version }}"
+      - name: Test with pytest (xdist)
+        if: matrix.os != 'ubuntu-latest'
+        run: pytest -vvvx -n auto -m "not slow and not extra and not skipci" --durations=50 --cov
+      - name: Test with pytest
+        if: matrix.os == 'ubuntu-latest'
+        run: pytest -vvvx -m "not slow and not extra and not skipci" --durations=50 --cov
diff --git a/.github/workflows/test_full.yml b/.github/workflows/test_full.yml
index 47703c5e..4d6d5cce 100644
--- a/.github/workflows/test_full.yml
+++ b/.github/workflows/test_full.yml
@@ -43,9 +43,6 @@ jobs:
       - uses: actions/checkout@v3
         with:
           submodules: true
-      # Prevents scheduled run being disabled after no commit
-      # for some time (creates a dummy commit):
-      - uses: gautamkrishnar/keepalive-workflow@v1
       - name: Set up Python ${{ matrix.python-version }}
         uses: actions/setup-python@v4
         with:
diff --git a/docs/user_guide/extending/tutorial02_testing_custom_plugin.md b/docs/user_guide/extending/tutorial02_testing_custom_plugin.md
new file mode 100644
index 00000000..d3ef080b
--- /dev/null
+++ b/docs/user_guide/extending/tutorial02_testing_custom_plugin.md
@@ -0,0 +1,250 @@
+# Extending TemporAI Tutorial 02: Testing a Custom Plugin
+[![Test In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/vanderschaarlab/temporai/blob/main/tutorials/extending/tutorial02_testing_custom_plugin.ipynb)
+
+This tutorial gives a brief overview of how to test your custom plugin.
+
+For the basics of writing a custom plugin, see the *"Writing Custom Plugin" tutorial* first. This tutorial assumes you have already written a custom plugin.
+
+*Skip the below cell if you are not on Google Colab / already have TemporAI installed:*
+
+
+```python
+%pip install temporai[dev]
+```
+
+**⚙️ Installation for testing**
+
+You should install the `[dev]` TemporAI to run tests:
+```bash
+pip install temporai[dev]
+```
+
+Alternatively, the best way to install TemporAI for development is to clone the repo, checkout a branch, and install in editable mode:
+```bash
+git clone https://github.com/vanderschaarlab/temporai.git
+cd temporai
+git checkout -b my-branch-name
+pip install -e .[dev]
+```
+
+📘 See also the [contribution guide](https://github.com/vanderschaarlab/temporai/blob/main/CONTRIBUTING.md).
+
+## 1. Find and adapt suitable tests
+
+The simplest way to run a set of tests on your custom plugin is to find and adapt existing tests for plugins of the same category.
+
+Plugin categories in TemporAI can be found as below.
+
+
+```python
+from tempor.plugins import plugin_loader
+from rich.pretty import pprint  # For prettifying the print output only.
+
+plugin_categories = plugin_loader.list_categories()
+
+print("Plugin categories:")
+pprint(list(plugin_categories.keys()), indent_guides=False)
+
+print("All plugins:")
+pprint(plugin_loader.list(), indent_guides=True)
+```
+
+    Plugin categories:
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">[</span>
+    <span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.classification'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'prediction.one_off.regression'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'prediction.temporal.classification'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'prediction.temporal.regression'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'preprocessing.imputation.static'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'preprocessing.imputation.temporal'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'preprocessing.nop'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'preprocessing.scaling.static'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'preprocessing.scaling.temporal'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'time_to_event'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'treatments.one_off.regression'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'treatments.temporal.classification'</span>,
+    <span style="color: #008000; text-decoration-color: #008000">'treatments.temporal.regression'</span>
+<span style="font-weight: bold">]</span>
+</pre>
+
+
+
+    All plugins:
+
+
+
+<pre style="white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'prediction'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'cde_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_classifier'</span>, <span style="color: #008000; text-decoration-color: #008000">'laplace_ode_classifier'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'laplace_ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'nn_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'ode_regressor'</span>, <span style="color: #008000; text-decoration-color: #008000">'cde_regressor'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'seq2seq_regressor'</span><span style="font-weight: bold">]}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'preprocessing'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'imputation'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'static'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'static_tabular_imputer'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ffill'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_tabular_imputer'</span>, <span style="color: #008000; text-decoration-color: #008000">'bfill'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'nop'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'nop_transformer'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'scaling'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'static'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'static_minmax_scaler'</span>, <span style="color: #008000; text-decoration-color: #008000">'static_standard_scaler'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ts_minmax_scaler'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_standard_scaler'</span><span style="font-weight: bold">]</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="font-weight: bold">}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'time_to_event'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'ts_coxph'</span>, <span style="color: #008000; text-decoration-color: #008000">'ts_xgb'</span>, <span style="color: #008000; text-decoration-color: #008000">'dynamic_deephit'</span><span style="font-weight: bold">]</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="color: #008000; text-decoration-color: #008000">'treatments'</span>: <span style="font-weight: bold">{</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'one_off'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'synctwin_regressor'</span><span style="font-weight: bold">]}</span>,
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   │   </span><span style="color: #008000; text-decoration-color: #008000">'temporal'</span>: <span style="font-weight: bold">{</span><span style="color: #008000; text-decoration-color: #008000">'classification'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_classifier'</span><span style="font-weight: bold">]</span>, <span style="color: #008000; text-decoration-color: #008000">'regression'</span>: <span style="font-weight: bold">[</span><span style="color: #008000; text-decoration-color: #008000">'crn_regressor'</span><span style="font-weight: bold">]}</span>
+<span style="color: #7fbf7f; text-decoration-color: #7fbf7f">│   </span><span style="font-weight: bold">}</span>
+<span style="font-weight: bold">}</span>
+</pre>
+
+
+
+Suitable tests can be found in TemporAI source code (`tests/plugins/...`), organized hierarchically by plugin category.
+
+For example, tests for `prediction/one_off/classification` will be located under:
+* [`tests/plugins/prediction/one_off/classification`](https://github.com/vanderschaarlab/temporai/tree/main/tests/plugins/prediction/one_off/classification).
+
+Find an example test file in this category, and adapt the tests from it to your custom plugin. For instance, we could look at:
+* [test file for `nn_classifier` plugin](https://github.com/vanderschaarlab/temporai/blob/main/tests/plugins/prediction/one_off/classification/test_nn_classifier.py).
+
+The test suite differs by plugin category, but in general, the following points are worth noting.
+
+* [`pytest`](https://docs.pytest.org/) is used for testing.
+* [Parametrization](https://docs.pytest.org/en/7.3.x/how-to/parametrize.html) is used to run similar tests with different parameters.
+* [Fixtures](https://docs.pytest.org/en/7.3.x/how-to/fixtures.html) are used for reusable test elements, e.g. datasets.
+* Plugins are tested with two methods of importing them (`PLUGIN_FROM_OPTIONS`): `"from_api"` and `"from_module"`, this is handled by the helper fixture `get_test_plugin`.
+* Typically, the set of test functions needed looks something like this:
+    * `test_sanity`: the very basics, check loading of your plugin works.
+    * `test_fit`: test your plugin's `fit` method.
+    * `test_predict, test_transform, ...`: test the additional methods relevant to your plugin's category.
+    * `test_serde`: test serialization and deserialization works.
+* The below constants are set and reused as parameters in the test suite. Adapt these as necessary. The various dataset fixtures can be found in [`conftest.py`](https://github.com/vanderschaarlab/temporai/blob/main/tests/conftest.py).
+    ```python
+    INIT_KWARGS = {"random_state": 123, "n_iter": 5}  # Input parameters for the plugin.
+    TEST_ON_DATASETS = ["sine_data_small"]  # A list of dataset fixtures to test the plugin on.
+    ```
+
+An example test file would looks something like below.
+
+In this example, it is assumed that the plugin is of the `prediction/one_off/classification` category, and is named `"my_classifier"`.
+
+```python
+# test_my_classifier.py
+# Example for illustration only - adapt to your own plugin as needed.
+
+from typing import Callable, Dict
+
+import pytest
+
+from tempor.plugins.prediction.one_off.classification import BaseOneOffClassifier
+from tempor.plugins.prediction.one_off.classification.plugin_my_classifier import MyClassifier
+from tempor.utils.serialization import load, save
+
+INIT_KWARGS = {"random_state": 123, "n_iter": 5}
+PLUGIN_FROM_OPTIONS = ["from_api", pytest.param("from_module", marks=pytest.mark.extra)]
+TEST_ON_DATASETS = ["sine_data_small"]
+
+
+@pytest.fixture
+def get_test_plugin(get_plugin: Callable):
+    def func(plugin_from: str, base_kwargs: Dict):
+        return get_plugin(
+            plugin_from,
+            fqn="prediction.one_off.classification.my_classifier",
+            cls=MyClassifier,
+            kwargs=base_kwargs,
+        )
+
+    return func
+
+
+@pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
+def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
+    test_plugin = get_test_plugin(plugin_from, INIT_KWARGS)
+    assert test_plugin is not None
+    assert test_plugin.name == "nn_classifier"
+    assert test_plugin.fqn() == "prediction.one_off.classification.my_classifier"
+    assert len(test_plugin.hyperparameter_space()) == 9
+
+
+@pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
+@pytest.mark.parametrize("data", TEST_ON_DATASETS)
+def test_fit(plugin_from: str, data: str, get_test_plugin: Callable, get_dataset: Callable) -> None:
+    test_plugin: BaseOneOffClassifier = get_test_plugin(plugin_from, INIT_KWARGS)
+    dataset = get_dataset(data)
+    test_plugin.fit(dataset)
+
+
+@pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
+@pytest.mark.parametrize("data", TEST_ON_DATASETS)
+def test_predict(
+    plugin_from: str, data: str, no_targets: bool, get_test_plugin: Callable, get_dataset: Callable
+) -> None:
+    test_plugin: BaseOneOffClassifier = get_test_plugin(plugin_from, INIT_KWARGS)
+    dataset = get_dataset(data)
+    test_plugin.fit(dataset)
+    output = test_plugin.predict(dataset)
+    assert output.numpy().shape == (len(dataset.time_series), 1)
+
+
+# Other categories of plugins would have more / different methods to test.
+
+
+@pytest.mark.parametrize("data", TEST_ON_DATASETS)
+def test_serde(data: str, get_test_plugin: Callable, get_dataset: Callable) -> None:
+    test_plugin: BaseOneOffClassifier = get_test_plugin("from_api", INIT_KWARGS)
+    dataset = get_dataset(data)
+
+    dump = save(test_plugin)
+    reloaded1 = load(dump)
+
+    reloaded1.fit(dataset)
+
+    dump = save(reloaded1)
+    reloaded2 = load(dump)
+
+    reloaded2.predict(dataset)
+
+```
+
+
+In order to run the tests, the test file (e.g. `test_my_classifier.py`) should be placed into the appropriate test directory, e.g. in this example under:
+
+[`tests/plugins/prediction/one_off/classification`](https://github.com/vanderschaarlab/temporai/tree/main/tests/plugins/prediction/one_off/classification).
+
+The tests can the be run like so:
+```bash
+pytest -x tests/plugins/prediction/one_off/classification/test_my_classifier.py
+```
+
+## 2. Plugin loader and all-plugin tests
+
+Some additional tests that apply to your plugin are found in:
+* [`tests/plugins/test_plugin_loader.py`](https://github.com/vanderschaarlab/temporai/blob/main/tests/plugins/test_plugin_loader.py): The plugin registry checks.
+* [`tests/plugins/test_all_plugins.py`](https://github.com/vanderschaarlab/temporai/blob/main/tests/plugins/test_all_plugins.py): The common automatic basic tests for all plugins.
+
+In `test_plugin_loader.py`, your custom plugin should be added to `test_tempor_plugin_loader_contents`, e.g.
+```python
+assert "my_classifier" in all_plugins["prediction"]["one_off"]["classification"]
+```
+
+`test_all_plugins.py` tests will run automatically, no changes here are needed.
+
+To check that these two test files pass:
+```bash
+pytest -x tests/plugins/test_plugin_loader.py
+pytest -x tests/plugins/test_all_plugins.py
+```
+
+## 3. Finally...
+
+Now is a perfect time to contribute your awesome plugin to the open sources eco-system by submitting a PR to TemporAI!
+
+Please follow the [contribution guide](https://github.com/vanderschaarlab/temporai/blob/main/CONTRIBUTING.md).
+
diff --git a/pytest.ini b/pytest.ini
index 0945830c..52b512e8 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -22,6 +22,8 @@ testpaths =
 filterwarnings =
     # pytest-xdist -related warning, ignore:
     ignore:.*rsyncdir.*:DeprecationWarning
+    # Jupyter platformdirs-related warning, ignore:
+    ignore:.*platformdirs.*:DeprecationWarning
 # Use pytest markers to select/deselect specific tests
 markers =
     internet: requires internet access
diff --git a/tests/automl/test_seeker.py b/tests/automl/test_seeker.py
index fba68969..a2e049a8 100644
--- a/tests/automl/test_seeker.py
+++ b/tests/automl/test_seeker.py
@@ -1,14 +1,16 @@
 # pylint: disable=redefined-outer-name,unused-argument
 
-from typing import Callable
+from typing import Any, Callable, Dict
 from unittest.mock import MagicMock
 
 import pytest
 
+from tempor.automl._types import AutoMLCompatibleEstimator
 from tempor.automl.seeker import (
     DEFAULT_STATIC_SCALERS,
     DEFAULT_TEMPORAL_SCALERS,
     METRIC_DIRECTION_MAP,
+    BaseSeeker,
     MethodSeeker,
     PipelineSeeker,
     TunerType,
@@ -17,6 +19,31 @@
 from tempor.plugins.core._params import CategoricalParams, IntegerParams
 
 
+def test_init_fails_estimator_name_def_length_mismatch():
+    class MySeeker(BaseSeeker):
+        def __init__(self, **kwargs) -> None:
+            super().__init__(
+                study_name="test",
+                task_type="prediction.one_off.classification",
+                estimator_names=["a", "b"],
+                estimator_defs=["a", "b", "c"],
+                metric="accuracy",
+                dataset=MagicMock(PredictiveDataset),
+                **kwargs,
+            )
+
+        def _init_estimator(self, estimator_name: str, estimator_def: Any):
+            pass
+
+        def _create_estimator_with_hps(
+            self, estimator_cls: AutoMLCompatibleEstimator, hps: Dict[str, Any], score: float
+        ):
+            pass
+
+    with pytest.raises(ValueError, match=".*length.*"):
+        MySeeker()
+
+
 @pytest.fixture
 def patch_slow(monkeypatch, request):
     # Monkeypatch all the slow things for ease of unit testing, specifically:
diff --git a/tests/automl/test_tuner.py b/tests/automl/test_tuner.py
index 93946dd2..d0d15c37 100644
--- a/tests/automl/test_tuner.py
+++ b/tests/automl/test_tuner.py
@@ -3,6 +3,7 @@
 import functools
 import warnings
 from typing import Any, Callable, Dict, Type
+from unittest.mock import Mock
 
 import optuna
 import pytest
@@ -422,6 +423,73 @@ def test_override_hp_space(
         # {'n_temporal_layers_hidden': 1, 'n_iter': 2}
         assert params[0] == params[1] == {"n_temporal_layers_hidden": 1, "n_iter": 2}
 
+    def test_tune_no_optimize_kwargs(
+        self,
+        helper_initialize: Callable,
+        tune_objective: Callable,
+        monkeypatch,
+    ):
+        dataset, p = helper_initialize(
+            data="sine_data_small",
+            plugin="prediction.one_off.classification.nn_classifier",
+        )
+
+        sampler = optuna.samplers.TPESampler(seed=SEED)
+
+        hp_tuner = tuner.OptunaTuner(
+            study_name="my_study",
+            direction="maximize",
+            study_sampler=sampler,
+            study_pruner=None,
+        )
+        monkeypatch.setattr(hp_tuner.study, "optimize", Mock(return_value=([], [])))
+
+        hp_tuner.tune(
+            estimator=p,
+            dataset=dataset,
+            evaluation_callback=functools.partial(
+                tune_objective,
+                evaluation_case="prediction.one_off.classification",
+                metric="accuracy",
+            ),
+            optimize_kwargs=None,  # This.
+        )
+
+    def test_tune_empty_hp_space(
+        self,
+        helper_initialize: Callable,
+        tune_objective: Callable,
+        monkeypatch,
+    ):
+        dataset, p = helper_initialize(
+            data="sine_data_small",
+            plugin="prediction.one_off.classification.nn_classifier",
+        )
+
+        sampler = optuna.samplers.TPESampler(seed=SEED)
+
+        hp_tuner = tuner.OptunaTuner(
+            study_name="my_study",
+            direction="maximize",
+            study_sampler=sampler,
+            study_pruner=None,
+        )
+        monkeypatch.setattr(p, "hyperparameter_space", Mock(return_value=[]))  # This.
+
+        scores, params = hp_tuner.tune(
+            estimator=p,
+            dataset=dataset,
+            evaluation_callback=functools.partial(
+                tune_objective,
+                evaluation_case="prediction.one_off.classification",
+                metric="accuracy",
+            ),
+            optimize_kwargs=None,
+            compute_baseline_score=False,
+        )
+
+        assert scores == params == []
+
     class TestPredictionOneOffClassification:
         @pytest.mark.parametrize("data", SETTINGS["prediction.one_off.classification"]["TEST_ON_DATASETS"])
         @pytest.mark.parametrize("plugin", SETTINGS["prediction.one_off.classification"]["TEST_WITH_PLUGINS"])
diff --git a/tests/benchmarks/test_benchmark.py b/tests/benchmarks/test_benchmark.py
index d2565c65..08ac5495 100644
--- a/tests/benchmarks/test_benchmark.py
+++ b/tests/benchmarks/test_benchmark.py
@@ -1,12 +1,14 @@
+import sys
 from typing import Callable
 
 import pytest
 
-from tempor.benchmarks import (
+from tempor.benchmarks import (  # visualize_benchmark,
     benchmark_models,
     classifier_supported_metrics,
     regression_supported_metrics,
     time_to_event_supported_metrics,
+    visualize_benchmark,
 )
 from tempor.plugins import plugin_loader
 from tempor.plugins.pipeline import pipeline
@@ -22,6 +24,7 @@
 PREDICTOR_TIME_TO_EVENT = "time_to_event.dynamic_deephit"
 
 
+@pytest.mark.filterwarnings("ignore:.*Matplotlib.*:UserWarning")
 @pytest.mark.parametrize("data", TEST_ON_DATASETS_CLASSIFIER)
 def test_classifier_benchmark(data: str, request: pytest.FixtureRequest) -> None:
     testcases = [
@@ -60,6 +63,12 @@ def test_classifier_benchmark(data: str, request: pytest.FixtureRequest) -> None
         for testcase, _ in testcases:
             assert metric in per_test_score[testcase].index
 
+    # Check also visualize_benchmarks executes without error.
+    if not sys.platform.startswith(("win", "darwin")):
+        # TODO: There appears to be a problem running this on GH runners for Windows and MaxOS, hence disabled.
+        # Investigate and resolve.
+        visualize_benchmark(per_test_score)
+
 
 @pytest.mark.parametrize("data", TEST_ON_DATASETS_REGRESSOR)
 def test_regressor_benchmark(data: str, request: pytest.FixtureRequest) -> None:
diff --git a/tests/benchmarks/test_benchmarks_utils.py b/tests/benchmarks/test_benchmarks_utils.py
new file mode 100644
index 00000000..a942ed15
--- /dev/null
+++ b/tests/benchmarks/test_benchmarks_utils.py
@@ -0,0 +1,41 @@
+from unittest.mock import Mock
+
+import numpy as np
+import pytest
+
+from tempor.benchmarks import utils
+
+
+def test_get_y_pred_proba_hlpr_edge():
+    mock = Mock()
+    out = utils.get_y_pred_proba_hlpr(
+        y_pred_proba=mock,
+        nclasses=3,
+    )
+    assert out == mock
+
+    mock = Mock(shape=[1])
+    out = utils.get_y_pred_proba_hlpr(
+        y_pred_proba=mock,
+        nclasses=2,
+    )
+    assert out == mock
+
+    mock = Mock(shape=[1, 4])
+    out = utils.get_y_pred_proba_hlpr(
+        y_pred_proba=mock,
+        nclasses=2,
+    )
+    assert out == mock
+
+
+def test_evaluate_auc_multiclass_nans():
+    with pytest.raises(ValueError, match=".*nan.*"):
+        utils.evaluate_auc_multiclass(y_test=np.asarray([1, 1, 1]), y_pred_proba=np.asarray([0.9, np.nan, 0.9]))
+
+
+def test_evaluate_auc_multiclass():
+    out = utils.evaluate_auc_multiclass(
+        y_test=np.asarray([0, 2, 1]), y_pred_proba=np.asarray([[0.8, 0.1, 0.1], [0.1, 0.1, 0.8], [0.1, 0.8, 0.1]])
+    )
+    assert len(out) == 2
diff --git a/tests/benchmarks/test_evaluation.py b/tests/benchmarks/test_evaluation.py
index b3248079..3f898c4e 100644
--- a/tests/benchmarks/test_evaluation.py
+++ b/tests/benchmarks/test_evaluation.py
@@ -1,4 +1,5 @@
 from typing import Any, Callable
+from unittest.mock import Mock
 
 import pytest
 
@@ -11,6 +12,7 @@
     regression_supported_metrics,
     time_to_event_supported_metrics,
 )
+from tempor.benchmarks.evaluation import ClassifierMetrics
 from tempor.plugins import plugin_loader
 from tempor.plugins.pipeline import pipeline
 
@@ -25,6 +27,12 @@
 PREDICTOR_TIME_TO_EVENT = "time_to_event.dynamic_deephit"
 
 
+def test_classifier_metrics_score_proba_input_validation():
+    clf_metrics = ClassifierMetrics()
+    with pytest.raises(ValueError, match=".*.input*"):
+        clf_metrics.score_proba(None, None)  # type: ignore
+
+
 @pytest.mark.parametrize("data", TEST_ON_DATASETS_CLASSIFIER)
 @pytest.mark.parametrize(
     "model_template",
@@ -44,7 +52,9 @@
     ],
 )
 @pytest.mark.parametrize("n_splits", [2])
-def test_classifier_evaluation(model_template: Any, n_splits: int, data: str, request: pytest.FixtureRequest) -> None:
+def test_evaluate_prediction_oneoff_classifier(
+    model_template: Any, n_splits: int, data: str, request: pytest.FixtureRequest
+) -> None:
     dataset = request.getfixturevalue(data)
 
     scores = evaluate_prediction_oneoff_classifier(model_template, dataset, n_splits=n_splits, seed=0)
@@ -58,7 +68,7 @@ def test_classifier_evaluation(model_template: Any, n_splits: int, data: str, re
 
 @pytest.mark.parametrize("data", TEST_ON_DATASETS_CLASSIFIER)
 @pytest.mark.parametrize("n_splits", [-1, 0, 1])
-def test_classifier_evaluation_fail(n_splits: int, data: str, request: pytest.FixtureRequest) -> None:
+def test_evaluate_prediction_oneoff_classifier_fail(n_splits: int, data: str, request: pytest.FixtureRequest) -> None:
     dataset = request.getfixturevalue(data)
 
     with pytest.raises(ValueError):
@@ -67,6 +77,61 @@ def test_classifier_evaluation_fail(n_splits: int, data: str, request: pytest.Fi
         )
 
 
+def test_evaluate_prediction_oneoff_classifier_fail_validation() -> None:
+    import numpy as np
+
+    from tempor.data.dataset import PredictiveDataset
+
+    mock_data = Mock(PredictiveDataset, predictive=Mock(targets=None), num_features=1)
+
+    with pytest.raises(ValueError, match=".*targets.*"):
+        evaluate_prediction_oneoff_classifier(
+            plugin_loader.get(PREDICTOR_CLASSIFICATION, n_iter=N_ITER), mock_data, n_splits=3, seed=0
+        )
+
+    mock_data = Mock(
+        PredictiveDataset, predictive=Mock(targets=Mock(numpy=Mock(return_value=np.ones(shape=(5, 5)))), num_features=1)
+    )
+
+    with pytest.raises(ValueError, match=".*1D.*"):
+        evaluate_prediction_oneoff_classifier(
+            plugin_loader.get(PREDICTOR_CLASSIFICATION, n_iter=N_ITER), mock_data, n_splits=3, seed=0
+        )
+
+
+@pytest.mark.parametrize("data", TEST_ON_DATASETS_CLASSIFIER)
+@pytest.mark.parametrize("do_raise", [True, False])
+def test_evaluate_prediction_oneoff_classifier_model_error(
+    data: str, request: pytest.FixtureRequest, do_raise: bool, monkeypatch
+):
+    dataset = request.getfixturevalue(data)
+    p = plugin_loader.get(PREDICTOR_CLASSIFICATION, n_iter=N_ITER)
+
+    def raise_(*args, **kwargs):
+        raise ValueError("test error")
+
+    monkeypatch.setattr(p, "fit", raise_)
+
+    if do_raise:
+        with pytest.raises(ValueError, match=".*test error.*"):
+            evaluate_prediction_oneoff_classifier(
+                p,
+                dataset,
+                n_splits=3,
+                seed=0,
+                raise_exceptions=do_raise,
+            )
+    else:
+        scores = evaluate_prediction_oneoff_classifier(
+            p,
+            dataset,
+            n_splits=3,
+            seed=0,
+            raise_exceptions=do_raise,
+        )
+        assert (scores["errors"] > 0).all()
+
+
 @pytest.mark.parametrize("data", TEST_ON_DATASETS_REGRESSOR)
 @pytest.mark.parametrize(
     "model_template",
@@ -86,7 +151,9 @@ def test_classifier_evaluation_fail(n_splits: int, data: str, request: pytest.Fi
     ],
 )
 @pytest.mark.parametrize("n_splits", [2])
-def test_regressor_evaluation(model_template: Any, n_splits: int, data: str, request: pytest.FixtureRequest) -> None:
+def test_evaluate_prediction_oneoff_regressor(
+    model_template: Any, n_splits: int, data: str, request: pytest.FixtureRequest
+) -> None:
     dataset = request.getfixturevalue(data)
 
     scores = evaluate_prediction_oneoff_regressor(model_template, dataset, n_splits=n_splits, seed=0)
@@ -100,7 +167,7 @@ def test_regressor_evaluation(model_template: Any, n_splits: int, data: str, req
 
 @pytest.mark.parametrize("data", TEST_ON_DATASETS_REGRESSOR)
 @pytest.mark.parametrize("n_splits", [-1, 0, 1])
-def test_regressor_evaluation_fail(n_splits: int, data: str, request: pytest.FixtureRequest) -> None:
+def test_evaluate_prediction_oneoff_regressor_fail(n_splits: int, data: str, request: pytest.FixtureRequest) -> None:
     dataset = request.getfixturevalue(data)
 
     with pytest.raises(ValueError):
@@ -109,6 +176,39 @@ def test_regressor_evaluation_fail(n_splits: int, data: str, request: pytest.Fix
         )
 
 
+@pytest.mark.parametrize("data", TEST_ON_DATASETS_REGRESSOR)
+@pytest.mark.parametrize("do_raise", [True, False])
+def test_evaluate_prediction_oneoff_regressor_error(
+    data: str, request: pytest.FixtureRequest, do_raise: bool, monkeypatch
+):
+    dataset = request.getfixturevalue(data)
+    p = plugin_loader.get(PREDICTOR_REGRESSION, n_iter=N_ITER)
+
+    def raise_(*args, **kwargs):
+        raise ValueError("test error")
+
+    monkeypatch.setattr(p, "fit", raise_)
+
+    if do_raise:
+        with pytest.raises(ValueError, match=".*test error.*"):
+            evaluate_prediction_oneoff_regressor(
+                p,
+                dataset,
+                n_splits=3,
+                seed=0,
+                raise_exceptions=do_raise,
+            )
+    else:
+        scores = evaluate_prediction_oneoff_regressor(
+            p,
+            dataset,
+            n_splits=3,
+            seed=0,
+            raise_exceptions=do_raise,
+        )
+        assert (scores["errors"] > 0).all()
+
+
 @pytest.mark.parametrize("data", TEST_ON_DATASETS_TIME_TO_EVENT)
 @pytest.mark.parametrize(
     "model_template",
@@ -155,9 +255,93 @@ def test_time_to_event_evaluation_fail(
 
     with pytest.raises(ValueError):
         evaluate_time_to_event(
-            plugin_loader.get("time_to_event.nn_regressor", n_iter=N_ITER),
+            plugin_loader.get("time_to_event.dynamic_deephit", n_iter=N_ITER),
             dataset,
             n_splits=n_splits,
             seed=0,
             horizons=horizons,
         )
+
+
+@pytest.mark.parametrize("data", TEST_ON_DATASETS_TIME_TO_EVENT)
+@pytest.mark.parametrize("do_raise", [True, False])
+def test_time_to_event_model_error(
+    get_event0_time_percentiles: Callable, data: str, request: pytest.FixtureRequest, do_raise: bool, monkeypatch
+):
+    dataset = request.getfixturevalue(data)
+    p = plugin_loader.get(PREDICTOR_TIME_TO_EVENT, n_iter=N_ITER)
+
+    def raise_(*args, **kwargs):
+        raise ValueError("test error")
+
+    monkeypatch.setattr(p, "fit", raise_)
+
+    horizons = get_event0_time_percentiles(dataset, [0.25, 0.5, 0.75])
+
+    if do_raise:
+        with pytest.raises(ValueError, match=".*test error.*"):
+            evaluate_time_to_event(
+                p,
+                dataset,
+                n_splits=3,
+                seed=0,
+                horizons=horizons,
+                raise_exceptions=do_raise,
+            )
+    else:
+        scores = evaluate_time_to_event(
+            p,
+            dataset,
+            n_splits=3,
+            seed=0,
+            horizons=horizons,
+            raise_exceptions=do_raise,
+        )
+        assert (scores["errors"] > 0).all()
+
+
+def test_compute_time_to_event_metric():
+    import pandas as pd
+
+    from tempor.benchmarks.evaluation import _compute_time_to_event_metric
+
+    metric_func = Mock()
+    mock_pred = Mock(num_timesteps_equal=Mock(return_value=True), num_features=1)
+    mock_test_data = Mock(predictive=Mock(targets=Mock(num_features=1)))
+    mock_train_data = Mock(predictive=Mock(targets=Mock(num_features=1)))
+
+    with pytest.raises(ValueError, match=".*horizon.*"):
+        _compute_time_to_event_metric(
+            metric_func=metric_func,
+            train_data=mock_train_data,
+            test_data=mock_test_data,
+            horizons=pd.to_datetime(["2000-01-01", "2000-01-02"]),  # type: ignore
+            predictions=mock_pred,
+        )
+
+    with pytest.raises(ValueError, match=".*one event.*"):
+        _compute_time_to_event_metric(
+            metric_func=metric_func,
+            train_data=Mock(predictive=Mock(targets=Mock(num_features=999))),
+            test_data=mock_test_data,
+            horizons=Mock(),
+            predictions=mock_pred,
+        )
+
+    with pytest.raises(ValueError, match=".*targets.*"):
+        _compute_time_to_event_metric(
+            metric_func=metric_func,
+            train_data=Mock(predictive=Mock(targets=None)),
+            test_data=mock_test_data,
+            horizons=Mock(),
+            predictions=mock_pred,
+        )
+
+    with pytest.raises(ValueError, match=".*equal number of time steps.*"):
+        _compute_time_to_event_metric(
+            metric_func=metric_func,
+            train_data=mock_train_data,
+            test_data=mock_test_data,
+            horizons=[1, 2, 3],
+            predictions=Mock(num_timesteps_equal=Mock(return_value=False)),
+        )
diff --git a/tests/benchmarks/test_metrics.py b/tests/benchmarks/test_metrics.py
new file mode 100644
index 00000000..2f74d3eb
--- /dev/null
+++ b/tests/benchmarks/test_metrics.py
@@ -0,0 +1,200 @@
+# pylint: disable=protected-access
+
+from unittest.mock import Mock
+
+import numpy as np
+import pytest
+
+from tempor.benchmarks import metrics
+
+
+def test_validation_check_y_survival():
+    with pytest.raises(ValueError, match=".*y must be.*"):
+        metrics.check_y_survival(y_or_event="wrong")  # type: ignore
+
+    with pytest.raises(ValueError, match=".*boolean.*"):
+        metrics.check_y_survival(
+            y_or_event=np.asarray([[1, 3, 7], [1, 1, 1]], dtype={"names": ("a", "b"), "formats": ("int", "f8")})
+        )
+
+    with pytest.raises(ValueError, match=".*all.*censored.*"):
+        metrics.check_y_survival(
+            y_or_event=np.asarray(
+                [[False, False, False], [0, 0, 0]], dtype={"names": ("a", "b"), "formats": ("bool", "f8")}
+            ),
+            allow_all_censored=False,
+        )
+
+
+def test_validation_check_estimate_1d():
+    with pytest.raises(ValueError, match=".*Expected 1D.*"):
+        metrics._check_estimate_1d([[1, 1], [2, 2]], test_time=100)
+
+
+def test_validation_check_times():
+    with pytest.raises(ValueError, match=".*within.*"):
+        metrics._check_times(np.asarray([1000]), [1, 4])
+
+
+def test_validation_check_estimate_2d():
+    with pytest.raises(ValueError, match=".*estimate with.*"):
+        metrics._check_estimate_2d(np.asarray([[1, 2], [1, 2]]), np.asarray([1, 3]), np.asarray([1]), Mock())
+
+
+def test_compute_counts():
+    out = metrics._compute_counts(np.asarray([True, True, False]), np.asarray([1, 8, 9]))
+    assert len(out) == 4
+
+
+def test_compute_counts_order_given():
+    out = metrics._compute_counts(np.asarray([True, True, False]), np.asarray([1, 8, 9]), order=np.asarray([2, 1, 0]))
+    assert len(out) == 4
+
+
+def test_compute_counts_truncated_validation():
+    with pytest.raises(ValueError, match=".*must be larger.*"):
+        metrics._compute_counts_truncated(
+            np.asarray([True, True, False]), np.asarray([11, 11, 11]), np.asarray([1, 1, 1])
+        )
+
+
+def test_kaplan_meier_estimator_reverse():
+    uniq_times, y = metrics.kaplan_meier_estimator(
+        event=np.asarray([True, True, False]),
+        time_exit=np.asarray([1, 8, 9]),
+        reverse=True,
+    )
+    assert len(uniq_times) == 3
+    assert len(y) == 3
+
+
+def test_kaplan_meier_estimator_time_enter():
+    uniq_times, y = metrics.kaplan_meier_estimator(
+        event=np.asarray([True, True, False]),
+        time_exit=np.asarray([1, 8, 9]),
+        time_enter=np.asarray([0, 1, 0]),
+    )
+    assert len(uniq_times) == 4
+    assert len(y) == 4
+
+    with pytest.raises(ValueError, match=".*left.*"):
+        metrics.kaplan_meier_estimator(
+            event=np.asarray([True, True, False]),
+            time_exit=np.asarray([1, 8, 9]),
+            time_enter=np.asarray([0, 1, 0]),
+            reverse=True,
+        )
+
+
+def test_kaplan_meier_estimator_time_min():
+    uniq_times, y = metrics.kaplan_meier_estimator(
+        event=np.asarray([True, True, False]),
+        time_exit=np.asarray([1, 8, 9]),
+        time_min=0,
+    )
+    assert len(uniq_times) == 3
+    assert len(y) == 3
+
+
+def test_survival_function_estimator_fit():
+    model = metrics.SurvivalFunctionEstimator()
+    model.fit(
+        y=np.asarray(
+            list(zip([False, False, True, True], [1, 2, 11, 4])), dtype={"names": ("e", "t"), "formats": ("bool", "f8")}
+        )
+    )
+
+
+def test_survival_function_estimator_predict_proba_invalid():
+    model = metrics.SurvivalFunctionEstimator()
+    model.fit(
+        y=np.asarray(
+            list(zip([False, False, True, True], [1, 2, 11, 4])), dtype={"names": ("e", "t"), "formats": ("bool", "f8")}
+        )
+    )
+    model.prob_[-1] = 1
+    with pytest.raises(ValueError, match=".*smaller than largest.*"):
+        model.predict_proba(np.asarray([12, 13]))
+
+
+def test_censoring_distribution_estimator_fit_all():
+    model = metrics.CensoringDistributionEstimator()
+    model.fit(
+        y=np.asarray(
+            list(zip([True, True, True, True], [1, 2, 11, 4])), dtype={"names": ("e", "t"), "formats": ("bool", "f8")}
+        )
+    )
+
+
+def test_censoring_distribution_estimator_predict_ipcw_ghat_0(monkeypatch):
+    model = metrics.CensoringDistributionEstimator()
+    monkeypatch.setattr(model, "predict_proba", Mock(return_value=np.asarray([0, 0, 0, 0])))
+
+    model.fit(
+        y=np.asarray(
+            list(zip([True, True, True, True], [1, 2, 11, 4])), dtype={"names": ("e", "t"), "formats": ("bool", "f8")}
+        )
+    )
+    with pytest.raises(ValueError, match=".*is zero.*"):
+        model.predict_ipcw(
+            y=np.asarray(
+                list(zip([True, True, True, True], [1, 2, 11, 4])),
+                dtype={"names": ("e", "t"), "formats": ("bool", "f8")},
+            )
+        )
+
+
+def test_estimate_concordance_index_invalid_comparable(monkeypatch):
+    monkeypatch.setattr(metrics, "_get_comparable", Mock(return_value=([], [])))
+
+    with pytest.raises(ValueError, match=".*pairs.*"):
+        metrics._estimate_concordance_index(Mock(), Mock(), Mock(), Mock())
+
+
+def test_create_structured_array_validation():
+    with pytest.raises(ValueError, match=".*different.*"):
+        metrics.create_structured_array([], [], name_event="a", name_time="a")
+
+
+def test_create_structured_array_non_bool_event():
+    y = metrics.create_structured_array([1, 1, 0, 1], [22, 34, 22, 134])
+    assert len(y) == 4
+
+
+def test_create_structured_array_non_bool_event_validation():
+    with pytest.raises(ValueError, match=".*binary.*"):
+        metrics.create_structured_array([1, 22, 1111, 44544], [22, 34, 22, 134])
+    with pytest.raises(ValueError, match=".*0 and 1.*"):
+        metrics.create_structured_array([3, 4, 3, 4], [22, 34, 22, 134])
+
+
+def test_brier_score_reshape():
+    out = metrics.brier_score(
+        survival_train=np.asarray(
+            list(zip([True, True, True, True], [1, 2, 11, 4])),
+            dtype={"names": ("e", "t"), "formats": ("bool", "f8")},
+        ),
+        survival_test=np.asarray(
+            list(zip([True, True], [1, 4])),
+            dtype={"names": ("e", "t"), "formats": ("bool", "f8")},
+        ),
+        estimate=np.asarray([0.7, 0.9]),
+        times=[3],
+    )
+    assert len(out) == 2
+
+
+def test_concordance_index_ipcw_tau_none():
+    out = metrics.concordance_index_ipcw(
+        survival_train=np.asarray(
+            list(zip([True, True, True, True], [1, 2, 11, 4])),
+            dtype={"names": ("e", "t"), "formats": ("bool", "f8")},
+        ),
+        survival_test=np.asarray(
+            list(zip([True, True], [1, 4])),
+            dtype={"names": ("e", "t"), "formats": ("bool", "f8")},
+        ),
+        estimate=np.asarray([0.7, 0.9]),
+        tau=None,
+    )
+    assert len(out) == 5
diff --git a/tests/config/test_config.py b/tests/config/test_config.py
index abb29c61..8c1888fd 100644
--- a/tests/config/test_config.py
+++ b/tests/config/test_config.py
@@ -30,6 +30,15 @@ def test_default_config(mock_updated_on_configure):
     assert config == tempor.config.get_config()
 
 
+def test_change_config_file_log(mock_updated_on_configure):
+    config = tempor.get_config()
+    config.logging.file_log = False
+    tempor.configure(config)
+    assert isinstance(config, TemporConfig)
+    assert config.logging.file_log is False
+    assert config == tempor.config.get_config()
+
+
 @pytest.mark.parametrize("str_path", [False, True])
 def test_change_config_yaml_file(tmp_path_factory, str_path, mock_updated_on_configure):
     yaml_file_path = tmp_path_factory.mktemp("my_config") / "config.yaml"
@@ -78,6 +87,18 @@ def test_change_config_via_hydra(tmp_path_factory, mock_updated_on_configure):
         assert lib_config == tempor.get_config()
 
 
+def test_change_config_fails_wrong_type():
+    with pytest.raises(TypeError, match=".*type.*not supported.*"):
+        tempor.configure([])  # type: ignore
+
+
+@pytest.mark.parametrize("wd_raw", ["~", "$PWD"])
+def test_get_working_dir(wd_raw):
+    c = TemporConfig(logging=Mock(), working_directory=wd_raw)
+    wd = c.get_working_dir()
+    assert wd_raw not in wd
+
+
 def test_observer(monkeypatch):
     mock_func = Mock()
     monkeypatch.setattr(tempor.config, "updated_on_configure", {mock_func})
diff --git a/tests/data/test_clv2conv.py b/tests/data/test_clv2conv.py
index df135ff9..0a8e60e8 100644
--- a/tests/data/test_clv2conv.py
+++ b/tests/data/test_clv2conv.py
@@ -174,7 +174,7 @@ def test_event_conversion_fails_times_differ_per_feature(self, load_pbc: dataset
         data_ = load_pbc
 
         # Add an event feature with different times.
-        df = data_.predictive.targets.dataframe()
+        df = data_.predictive.targets.dataframe()  # pyright: ignore
         df["feat_2"] = df["status"].apply(lambda x: (x[0] + 0.5, x[1]))
 
         data = dataset.TimeToEventAnalysisDataset(
@@ -214,7 +214,7 @@ def test_time_to_event_analysis_dataset(self, load_pbc: dataset.TimeToEventAnaly
         data_ = load_pbc
 
         # Make events multi-feature:
-        df = data_.predictive.targets.dataframe()
+        df = data_.predictive.targets.dataframe()  # pyright: ignore
         df["feat_2"] = df["status"].copy()
 
         data = dataset.TimeToEventAnalysisDataset(
@@ -241,7 +241,7 @@ def test_one_off_treatment_effects_dataset(self, load_pbc: dataset.TimeToEventAn
         data_ = load_pbc
 
         # Make events multi-feature:
-        df = data_.predictive.targets.dataframe()
+        df = data_.predictive.targets.dataframe()  # pyright: ignore
         df["feat_2"] = df["status"].copy()
 
         data = dataset.OneOffTreatmentEffectsDataset(
@@ -290,3 +290,20 @@ def test_temporal_off_treatment_effects_dataset(self, load_sine: dataset.OneOffT
         if static:
             assert data_converted.static_covariates is not None
             assert data.static.num_features == data_converted.static_covariates.n_features  # pyright: ignore
+
+    def test_unsupported(self, load_sine: dataset.OneOffTreatmentEffectsDataset):
+        data_ = load_sine
+        data = dataset.CovariatesDataset(
+            time_series=data_.time_series.dataframe(),
+            static=None,
+        )
+
+        data_converted = clv2conv.tempor_dataset_to_clairvoyance2_dataset(data)
+        assert isinstance(data_converted, Clairvoyance2Dataset)
+        assert len(data) == data_converted.n_samples
+        assert list(range(len(data))) == data_converted.sample_indices
+        assert data.time_series.num_features == data_converted.temporal_covariates.n_features
+        assert data_converted.temporal_targets is None
+        assert data_converted.event_targets is None
+        assert data_converted.temporal_treatments is None
+        assert data_converted.event_treatments is None
diff --git a/tests/data/test_dataloader.py b/tests/data/test_dataloader.py
new file mode 100644
index 00000000..e4bbd344
--- /dev/null
+++ b/tests/data/test_dataloader.py
@@ -0,0 +1,121 @@
+from tempor.data import data_typing, dataloader, dataset
+
+
+def test_dataloaders():
+    class MyOneOffPredictionDataLoader(dataloader.OneOffPredictionDataLoader):
+        def load(self, **kwargs) -> dataset.OneOffPredictionDataset:
+            raise NotImplementedError
+
+        @staticmethod
+        def dataset_dir() -> None:
+            return None
+
+        @staticmethod
+        def url() -> None:
+            return None
+
+    class MyTemporalPredictionDataLoader(dataloader.TemporalPredictionDataLoader):
+        def load(self, **kwargs) -> dataset.OneOffPredictionDataset:
+            raise NotImplementedError
+
+        @staticmethod
+        def dataset_dir() -> None:
+            return None
+
+        @staticmethod
+        def url() -> None:
+            return None
+
+    class MyTimeToEventAnalysisDataLoader(dataloader.TimeToEventAnalysisDataLoader):
+        def load(self, **kwargs) -> dataset.OneOffPredictionDataset:
+            raise NotImplementedError
+
+        @staticmethod
+        def dataset_dir() -> None:
+            return None
+
+        @staticmethod
+        def url() -> None:
+            return None
+
+    class MyOneOffTreatmentEffectsDataLoader(dataloader.OneOffTreatmentEffectsDataLoader):
+        def load(self, **kwargs) -> dataset.OneOffPredictionDataset:
+            raise NotImplementedError
+
+        @staticmethod
+        def dataset_dir() -> None:
+            return None
+
+        @staticmethod
+        def url() -> None:
+            return None
+
+    class MyTemporalTreatmentEffectsDataLoader(dataloader.TemporalTreatmentEffectsDataLoader):
+        def load(self, **kwargs) -> dataset.OneOffPredictionDataset:
+            raise NotImplementedError
+
+        @staticmethod
+        def dataset_dir() -> None:
+            return None
+
+        @staticmethod
+        def url() -> None:
+            return None
+
+    dl_oneoff_pred = MyOneOffPredictionDataLoader()
+    dl_temporal_pred = MyTemporalPredictionDataLoader()
+    dl_tte = MyTimeToEventAnalysisDataLoader()
+    dl_oneoff_tr = MyOneOffTreatmentEffectsDataLoader()
+    dl_temporal_tr = MyTemporalTreatmentEffectsDataLoader()
+
+    assert dl_oneoff_pred.dataset_dir() is None
+    assert dl_oneoff_pred.url() is None
+    assert dl_oneoff_pred.predictive_task == data_typing.PredictiveTask.ONE_OFF_PREDICTION
+
+    assert dl_temporal_pred.dataset_dir() is None
+    assert dl_temporal_pred.url() is None
+    assert dl_temporal_pred.predictive_task == data_typing.PredictiveTask.TEMPORAL_PREDICTION
+
+    assert dl_tte.dataset_dir() is None
+    assert dl_tte.url() is None
+    assert dl_tte.predictive_task == data_typing.PredictiveTask.TIME_TO_EVENT_ANALYSIS
+
+    assert dl_oneoff_tr.dataset_dir() is None
+    assert dl_oneoff_tr.url() is None
+    assert dl_oneoff_tr.predictive_task == data_typing.PredictiveTask.ONE_OFF_TREATMENT_EFFECTS
+
+    assert dl_temporal_tr.dataset_dir() is None
+    assert dl_temporal_tr.url() is None
+    assert dl_temporal_tr.predictive_task == data_typing.PredictiveTask.TEMPORAL_TREATMENT_EFFECTS
+
+
+def test_requires_internet():
+    class CaseUrl(dataloader.OneOffPredictionDataLoader):
+        def load(self, **kwargs) -> dataset.OneOffPredictionDataset:
+            raise NotImplementedError
+
+        @staticmethod
+        def dataset_dir() -> None:
+            return None
+
+        @staticmethod
+        def url():
+            return "some_url"
+
+    class CaseNoUrl(dataloader.OneOffPredictionDataLoader):
+        def load(self, **kwargs) -> dataset.OneOffPredictionDataset:
+            raise NotImplementedError
+
+        @staticmethod
+        def dataset_dir() -> None:
+            return None
+
+        @staticmethod
+        def url():
+            return None
+
+    case_url = CaseUrl()
+    case_no_url = CaseNoUrl()
+
+    assert case_url.requires_internet()
+    assert not case_no_url.requires_internet()
diff --git a/tests/data/test_dataset.py b/tests/data/test_dataset.py
index f6e28d0e..37fad95b 100644
--- a/tests/data/test_dataset.py
+++ b/tests/data/test_dataset.py
@@ -141,6 +141,44 @@ def test_init(self, mock_dataset_cls: Type[MockedBaseDataset], mocked_depends: M
         )
         dummy_dataset.mock_validate_call.assert_called_once()
 
+    def test_repr(self, mock_dataset_cls: Type[MockedBaseDataset], mocked_depends: MockedDepends):
+        data_time_series = Mock()
+        data_static = Mock()
+        data_targets = Mock()
+        data_treatments = Mock()
+
+        dummy_dataset = mock_dataset_cls(
+            time_series=data_time_series,
+            static=data_static,
+            targets=data_targets,
+            treatments=data_treatments,
+            **{"dummy": "kwarg"},
+        )
+
+        assert "time_series" in str(dummy_dataset)
+        assert "static" in str(dummy_dataset)
+        assert "predictive" in str(dummy_dataset)
+
+        dummy_dataset.static = None
+        dummy_dataset.predictive = None
+        assert "static" not in str(dummy_dataset)
+        assert "predictive" not in str(dummy_dataset)
+
+    def test_predictive_none(self, mock_dataset_cls: Type[MockedBaseDataset], mocked_depends: MockedDepends):
+        data_time_series = Mock()
+        data_static = Mock()
+
+        dummy_dataset = mock_dataset_cls(
+            time_series=data_time_series,
+            static=data_static,
+            targets=None,
+            treatments=None,
+            **{"dummy": "kwarg"},
+        )
+
+        dummy_dataset.predictive = None
+        assert dummy_dataset.predictive_task is None
+
     def test_init_no_targets_provided(self, mock_dataset_cls: Type[MockedBaseDataset], mocked_depends: MockedDepends):
         data_time_series = Mock()
         data_static = Mock()
@@ -258,6 +296,70 @@ def test_validate_fails_sample_index_mismatch(self, mock_dataset_cls: Type[Mocke
             )
 
 
+# --- CovariatesDataset. ---
+
+
+class MockedCovariatesDataset(dataset.CovariatesDataset):
+    mock_validate_call: Mock
+
+    def validate(self) -> None:
+        self._validate()
+
+    def _validate(self) -> None:
+        self.mock_validate_call()
+
+
+@pytest.fixture
+def mock_covariate_dataset_cls():
+    MockedCovariatesDataset.mock_validate_call = Mock()
+    return MockedCovariatesDataset
+
+
+class TestCovariatesDataset:
+    def test_init(self, mock_covariate_dataset_cls: Type[MockedCovariatesDataset], mocked_depends: MockedDepends):
+        data_time_series = Mock()
+        data_static = Mock()
+        data_targets = None
+        data_treatments = None
+
+        dummy_dataset = mock_covariate_dataset_cls(
+            time_series=data_time_series,
+            static=data_static,
+            targets=data_targets,
+            treatments=data_treatments,
+            **{"dummy": "kwarg"},
+        )
+
+        mocked_depends.TimeSeriesSamples.assert_called_once_with(data_time_series)
+        mocked_depends.StaticSamples.assert_called_once_with(data_static)
+        mocked_depends.OneOffPredictionTaskData.assert_not_called()
+        dummy_dataset.mock_validate_call.assert_called_once()
+
+        assert dummy_dataset.fit_ready
+
+    def test_init_fails(self, mock_covariate_dataset_cls: Type[MockedCovariatesDataset], mocked_depends: MockedDepends):
+        data_time_series = Mock()
+        data_static = Mock()
+
+        with pytest.raises(ValueError, match=".*targets.*not.*"):
+            mock_covariate_dataset_cls(
+                time_series=data_time_series,
+                static=data_static,
+                targets=Mock(),
+                treatments=None,
+                **{"dummy": "kwarg"},
+            )
+
+        with pytest.raises(ValueError, match="treatments.*not"):
+            mock_covariate_dataset_cls(
+                time_series=data_time_series,
+                static=data_static,
+                targets=None,
+                treatments=Mock(),
+                **{"dummy": "kwarg"},
+            )
+
+
 # --- OneOffPredictionDataset. ---
 
 
diff --git a/tests/data/test_pandera_utils.py b/tests/data/test_pandera_utils.py
index a3299528..90436b0a 100644
--- a/tests/data/test_pandera_utils.py
+++ b/tests/data/test_pandera_utils.py
@@ -1,11 +1,17 @@
+from unittest.mock import Mock
+
 import pandas as pd
 import pandera as pa
 import pytest
 
-from tempor.data.pandera_utils import UnionDtype
+from tempor.data.pandera_utils import UnionDtype, set_up_2level_multiindex, set_up_index
 
 
 class TestUnionDtype:
+    def test_unknown_mapping(self):
+        with pytest.raises(KeyError):
+            UnionDtype["unknown"]  # pylint: disable=pointless-statement
+
     @pytest.mark.parametrize(
         "union, df",
         [
@@ -18,6 +24,16 @@ class TestUnionDtype:
                     }
                 ),
             ),
+            (
+                UnionDtype[pa.Int(), pa.String],  # type: ignore
+                # ^ Check also instantiated case.
+                pd.DataFrame(
+                    {
+                        "a": [1, 2, 3, 4],
+                        "b": ["aa", "bb", "cc", "dd"],
+                    }
+                ),
+            ),
             (
                 UnionDtype[pa.Int, pa.Float],  # type: ignore
                 pd.DataFrame(
@@ -162,3 +178,51 @@ def test_validation_passes_dtype_directly(self, union, dtype):
     )
     def test_validation_fails_dtype_directly(self, union, dtype):
         assert union().check(pandera_dtype=dtype, data_container=None) is False
+
+
+class TestHelpers:
+    def test_set_up_index_validation(self):
+        with pytest.raises(ValueError, match=".*[Ii]ndex.*not.*None.*"):
+            set_up_index(
+                schema=Mock(index=None),
+                data=Mock(),
+                dtype=Mock(),
+                name="dummy",
+                nullable=True,
+                unique=False,
+                coerce=False,
+            )
+
+    def test_set_up_2level_multiindex_validation(self):
+        with pytest.raises(ValueError, match=".*[Ii]ndex.*not.*None.*"):
+            set_up_2level_multiindex(
+                schema=Mock(index=None),
+                data=Mock(),
+                dtypes=Mock(),
+                names=Mock(),
+                nullable=Mock(),
+                coerce=Mock(),
+                unique=Mock(),
+            )
+
+        with pytest.raises(ValueError, match=".*MultiIndex.*"):
+            set_up_2level_multiindex(
+                schema=Mock(index=Mock()),
+                data=Mock(),
+                dtypes=Mock(),
+                names=Mock(),
+                nullable=Mock(),
+                coerce=Mock(),
+                unique=Mock(),
+            )
+
+        with pytest.raises(ValueError, match=".*2 levels.*"):
+            set_up_2level_multiindex(
+                schema=Mock(index=Mock(pa.MultiIndex, indexes=[])),
+                data=Mock(),
+                dtypes=Mock(),
+                names=Mock(),
+                nullable=Mock(),
+                coerce=Mock(),
+                unique=Mock(),
+            )
diff --git a/tests/data/test_predictive.py b/tests/data/test_predictive.py
new file mode 100644
index 00000000..dfd1f71c
--- /dev/null
+++ b/tests/data/test_predictive.py
@@ -0,0 +1,65 @@
+# pylint: disable=redefined-outer-name,unused-argument
+
+from unittest.mock import Mock
+
+import pytest
+
+from tempor.data import data_typing, predictive
+
+
+@pytest.fixture
+def mock_samples(monkeypatch):
+    from tempor.data import samples
+
+    monkeypatch.setattr(samples, "StaticSamples", Mock())
+    monkeypatch.setattr(samples, "EventSamples", Mock())
+    monkeypatch.setattr(samples, "TimeSeriesSamples", Mock())
+
+
+def test_predictive(mock_samples):
+    class MyOneOffPredictionTaskData(predictive.OneOffPredictionTaskData):
+        pass
+
+    class MyTemporalPredictionTaskData(predictive.TemporalPredictionTaskData):
+        pass
+
+    class MyTimeToEventAnalysisTaskData(predictive.TimeToEventAnalysisTaskData):
+        pass
+
+    class MyOneOffTreatmentEffectsTaskData(predictive.OneOffTreatmentEffectsTaskData):
+        pass
+
+    class MyTemporalTreatmentEffectsTaskData(predictive.TemporalTreatmentEffectsTaskData):
+        pass
+
+    p1 = MyOneOffPredictionTaskData(parent_dataset=Mock(), targets=None)
+    p2 = MyTemporalPredictionTaskData(parent_dataset=Mock(), targets=None)
+    p3 = MyTimeToEventAnalysisTaskData(parent_dataset=Mock(), targets=None)
+    p4 = MyOneOffTreatmentEffectsTaskData(parent_dataset=Mock(), targets=None, treatments=None)  # type: ignore
+    p5 = MyTemporalTreatmentEffectsTaskData(parent_dataset=Mock(), targets=None, treatments=None)  # type: ignore
+
+    assert p1.predictive_task == data_typing.PredictiveTask.ONE_OFF_PREDICTION
+    assert p2.predictive_task == data_typing.PredictiveTask.TEMPORAL_PREDICTION
+    assert p3.predictive_task == data_typing.PredictiveTask.TIME_TO_EVENT_ANALYSIS
+    assert p4.predictive_task == data_typing.PredictiveTask.ONE_OFF_TREATMENT_EFFECTS
+    assert p5.predictive_task == data_typing.PredictiveTask.TEMPORAL_TREATMENT_EFFECTS
+
+
+def test_repr(mock_samples):
+    class MyOneOffPredictionTaskData(predictive.OneOffPredictionTaskData):
+        pass
+
+    p = MyOneOffPredictionTaskData(parent_dataset=Mock(), targets=None)
+    assert "targets=None" in str(p)
+    assert "treatments" not in str(p)
+
+    p = MyOneOffPredictionTaskData(parent_dataset=Mock(), targets=Mock())
+    assert "targets=" in str(p) and "targets=None" not in str(p)
+    assert "treatments" not in str(p)
+
+    class MyTemporalTreatmentEffectsTaskData(predictive.TemporalTreatmentEffectsTaskData):
+        pass
+
+    p = MyTemporalTreatmentEffectsTaskData(parent_dataset=Mock(), targets=Mock(), treatments=Mock())
+    assert "targets=" in str(p) and "targets=None" not in str(p)
+    assert "treatments=" in str(p) and "treatments=None" not in str(p)
diff --git a/tests/data/test_samples.py b/tests/data/test_samples.py
index 5b28dc83..211ddaa0 100644
--- a/tests/data/test_samples.py
+++ b/tests/data/test_samples.py
@@ -277,6 +277,12 @@ def test_repr(self, df_static: pd.DataFrame):
         assert "StaticSamples with data:" in str(s)
         assert str(s.dataframe()) in str(s)
 
+    def test_repr_html(self, df_static: pd.DataFrame):
+        s = samples.StaticSamples(data=df_static)
+        assert "span" in s._repr_html_()  # pylint: disable=protected-access
+        assert "StaticSamples" in s._repr_html_()  # pylint: disable=protected-access
+        assert s.dataframe()._repr_html_() in s._repr_html_()  # type: ignore  # pylint: disable=protected-access
+
     @pytest.mark.parametrize("df", dfs_test.df_static_success)
     def test_init_success(self, df: pd.DataFrame):
         samples.StaticSamples(data=df)
@@ -615,6 +621,12 @@ def test_repr(self, df_time_series: pd.DataFrame):
         assert "TimeSeriesSamples with data:" in str(s)
         assert str(s.dataframe()) in str(s)
 
+    def test_repr_html(self, df_time_series: pd.DataFrame):
+        s = samples.TimeSeriesSamples.from_dataframe(df_time_series)
+        assert "span" in s._repr_html_()  # pylint: disable=protected-access
+        assert "TimeSeriesSamples" in s._repr_html_()  # pylint: disable=protected-access
+        assert s.dataframe()._repr_html_() in s._repr_html_()  # type: ignore  # pylint: disable=protected-access
+
     @pytest.mark.parametrize("df", dfs_test.df_time_series_success)
     def test_init_success(self, df: pd.DataFrame):
         samples.TimeSeriesSamples(data=df)
@@ -708,6 +720,12 @@ def test_repr(self, df_event: pd.DataFrame):
         assert "EventSamples with data:" in str(s)
         assert str(s.dataframe()) in str(s)
 
+    def test_repr_html(self, df_event: pd.DataFrame):
+        s = samples.EventSamples.from_dataframe(df_event)
+        assert "span" in s._repr_html_()  # pylint: disable=protected-access
+        assert "EventSamples" in s._repr_html_()  # pylint: disable=protected-access
+        assert s.dataframe()._repr_html_() in s._repr_html_()  # type: ignore  # pylint: disable=protected-access
+
     @pytest.mark.parametrize("df", dfs_test.df_event_success)
     def test_init_success(self, df: pd.DataFrame):
         samples.EventSamples(data=df)
diff --git a/tests/data/test_utils.py b/tests/data/test_utils.py
index 91ee3765..2d991a60 100644
--- a/tests/data/test_utils.py
+++ b/tests/data/test_utils.py
@@ -1075,14 +1075,14 @@ class TestEventTimeValuePairsToEventDataframe:
             ],
         ],
     )
-    def test_success(self, sample_index, event_time_value_pairs):
-        feature_index = ["f1", "f2"]
+    @pytest.mark.parametrize("feature_index", [["f1", "f2"], None])
+    def test_success(self, sample_index, event_time_value_pairs, feature_index):
         df = utils.event_time_value_pairs_to_event_dataframe(
             event_time_value_pairs, sample_index=sample_index, feature_index=feature_index
         )
 
         assert list(df.index) == sample_index
-        assert list(df.columns) == feature_index
+        assert list(df.columns) == (feature_index if feature_index is not None else [0, 1])
         assert df.shape == (3, 2)
         assert isinstance(df.iloc[0, 0], tuple)
 
diff --git a/tests/log/test_custom_logger.py b/tests/log/test_custom_logger.py
index ad94ea91..af758d86 100644
--- a/tests/log/test_custom_logger.py
+++ b/tests/log/test_custom_logger.py
@@ -178,6 +178,25 @@ def test_file_log_enabled(tmp_path: pathlib.Path):
     assert re.match(console_logs_part0 + "ERROR" + console_logs_part1 + "This is error\n", log_content, re_flags)
 
 
+def test_file_log_disabled(tmp_path: pathlib.Path):
+    config = tempor.get_config()
+    config.working_directory = str(tmp_path)
+    config.logging.level = "ERROR"  # NOTE: The file logging will ignore this and always log at "DEBUG".
+    config.logging.file_log = False
+    tempor.configure(config)
+
+    logger.trace("This is trace")
+    logger.debug("This is debug")
+    logger.info("This is info")
+    logger.print("This one is printed")
+    logger.warning("This is warning")
+    logger.error("This is error")
+
+    log_dir = tmp_path / "logs"
+
+    assert not os.path.exists(log_dir)
+
+
 @pytest.mark.parametrize("config_diagnose", [False, True])
 def test_exception_catch(config_diagnose, caplog, tmp_path):  # noqa: F811
     def raise_exc():
diff --git a/tests/models/test_ddh.py b/tests/models/test_ddh.py
index de648dd3..9218a544 100644
--- a/tests/models/test_ddh.py
+++ b/tests/models/test_ddh.py
@@ -1,11 +1,12 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Any
+from unittest.mock import Mock
 
 import numpy as np
 import pytest
 
-from tempor.models.ddh import DynamicDeepHitModel
+from tempor.models.ddh import DynamicDeepHitLayers, DynamicDeepHitModel
 from tempor.utils.dataloaders import PBCDataLoader
 
 
@@ -14,6 +15,8 @@ def get_test_data():
     # This is module-scoped such that tests can run quicker.
 
     data = PBCDataLoader().load()
+    assert data.predictive.targets is not None
+
     x: Any = [df.to_numpy() for df in data.time_series.list_of_dataframes()]
     x = np.array(x, dtype=object)
     t, e = (df.to_numpy().reshape((-1,)) for df in data.predictive.targets.split_as_two_dataframes())
@@ -24,6 +27,9 @@ def get_test_data():
     return x, t, e, horizons
 
 
+# Test DynamicDeepHitModel:
+
+
 @pytest.mark.parametrize(
     "rnn_mode",
     [
@@ -84,3 +90,78 @@ def test_ddh_predict_output_modes(
     output = model.fit(x=x, t=t, e=e).predict_risk(x=x, t=horizons)
 
     assert output.shape == (len(x), len(horizons))
+
+
+def test_ddh_predict_more_cases(get_test_data):
+    x, t, e, horizons = get_test_data
+
+    # No clipping.
+    model = DynamicDeepHitModel(n_iter=10, clipping_value=0)
+    output = model.fit(x=x, t=t, e=e).predict_risk(x=x, t=horizons)
+    assert output.shape == (len(x), len(horizons))
+
+    # Force early stop.
+    model = DynamicDeepHitModel(n_iter=10, patience=0)
+    output = model.fit(x=x, t=t, e=e).predict_risk(x=x, t=horizons)
+    assert output.shape == (len(x), len(horizons))
+
+
+def test_ddh_not_fit(get_test_data):
+    x, t, *_ = get_test_data
+
+    model = DynamicDeepHitModel(n_iter=10, clipping_value=0)
+    with pytest.raises(RuntimeError, match=".*fit.*"):
+        model.predict_emb(x)
+    with pytest.raises(RuntimeError, match=".*fit.*"):
+        model.predict_survival(x, t)
+
+
+def test_loss_fails(get_test_data):
+    import torch
+
+    x, t, e, _ = get_test_data
+
+    model = DynamicDeepHitModel(n_iter=10, clipping_value=0)
+
+    with pytest.raises(RuntimeError, match=".*loss.*"):
+        model.total_loss(x, t, e)
+
+    model.model = Mock(return_value=torch.tensor([np.nan, np.nan]))
+    x = torch.ones(size=[10, 2])
+
+    with pytest.raises(RuntimeError, match=".*NaNs.*"):
+        model.total_loss(x, t, e)
+
+
+def test_ranking_loss_edge_case():
+    # if torch.sum(t > ti) > 0 : False
+    import torch
+
+    model = DynamicDeepHitModel(n_iter=10, clipping_value=0)
+
+    model.ranking_loss(
+        cif=[
+            torch.tensor([[1, 1, 1], [1, 1, 1], [1, 1, 1]]),
+            torch.tensor([[1, 1, 1], [1, 1, 1], [1, 1, 1]]),
+            torch.tensor([[1, 1, 1], [1, 1, 1], [1, 1, 1]]),
+        ],
+        t=torch.tensor([2, 1, 1]),
+        e=torch.tensor([1, 0, 1]),
+    )
+
+
+def test_ddh_predict_survival_all_step(get_test_data):
+    x, t, e, horizons = get_test_data
+
+    model = DynamicDeepHitModel(n_iter=10, clipping_value=0)
+    output = model.fit(x=x, t=t, e=e).predict_survival(x=x, t=horizons, all_step=True)
+    assert output.shape[1] == len(horizons)
+    assert output.shape[0] >= len(x)
+
+
+# Test DynamicDeepHitLayers:
+
+
+def test_ddh_layers_rnn_type():
+    with pytest.raises(RuntimeError, match=".*rnn.*type.*"):
+        DynamicDeepHitLayers(input_dim=10, seq_len=10, output_dim=10, layers_rnn=1, hidden_rnn=2, rnn_type="unknown")
diff --git a/tests/models/test_mlp.py b/tests/models/test_mlp.py
index 82cfbd30..e7d80484 100644
--- a/tests/models/test_mlp.py
+++ b/tests/models/test_mlp.py
@@ -1,4 +1,5 @@
 from typing import TYPE_CHECKING
+from unittest.mock import Mock
 
 import numpy as np
 import pytest
@@ -151,3 +152,106 @@ def test_mlp_regression(residual: bool) -> None:
     assert model.predict(X).shape == y.shape
     with pytest.raises(ValueError):
         model.predict_proba(X)
+
+
+def test_mlp_input_validation_fails():
+    with pytest.raises(ValueError, match=".*>=.*0.*"):
+        MLP(task_type="regression", n_units_in=-1, n_units_out=3)
+    with pytest.raises(ValueError, match=".*>=.*0.*"):
+        MLP(task_type="regression", n_units_in=3, n_units_out=-1)
+
+
+def test_mlp_hidden_0():
+    model = MLP(task_type="regression", n_units_in=3, n_units_out=3, n_layers_hidden=0)
+    assert len(model.model) == 1
+
+
+def test_nonlin_out_mismatch():
+    with pytest.raises(RuntimeError, match=".*mismatch.*"):
+        MLP(task_type="regression", n_units_in=3, n_units_out=3, nonlin_out=[("tanh", 1), ("softmax", 3)])
+
+
+def test_mlp_loss_provided():
+    mock_loss = Mock()
+    model = MLP(task_type="regression", n_units_in=3, n_units_out=3, n_layers_hidden=0, loss=mock_loss)
+    assert model.loss == mock_loss
+
+
+def test_residual_layer_x_last_dim_0():
+    rl = ResidualLayer(n_units_in=10, n_units_out=10)
+    out = rl.forward(torch.ones(size=(3, 3, 0)))
+    assert list(out.shape) == [3, 3, 10]
+
+
+def test_mlp_score():
+    model = MLP(task_type="regression", n_units_in=3, n_units_out=3, n_units_hidden=3, device=torch.device("cpu"))
+    out = model.score(X=np.ones(shape=(3, 3)), y=np.ones((3,)))
+    assert isinstance(out, float)
+
+    model = MLP(task_type="classification", n_units_in=3, n_units_out=3, n_units_hidden=3, device=torch.device("cpu"))
+    out = model.score(X=np.ones(shape=(3, 3)), y=np.ones((3,)))
+    assert isinstance(out, float)
+
+
+def test_mlp_clipping_0_case():
+    X, y = load_digits(return_X_y=True)
+
+    model = MLP(
+        task_type="classification",
+        n_units_in=X.shape[1],
+        n_units_out=len(np.unique(y)),
+        n_iter=10,
+        clipping_value=0,
+    )
+
+    model.fit(X, y)  # type: ignore
+
+
+def test_mlp_print_iter_no_early_stopping_case():
+    X, y = load_digits(return_X_y=True)
+
+    model = MLP(
+        task_type="classification",
+        n_units_in=X.shape[1],
+        n_units_out=len(np.unique(y)),
+        n_iter=10,
+        n_iter_print=3,
+        early_stopping=False,
+    )
+
+    model.fit(X, y)  # type: ignore
+
+
+def test_mlp_early_stop():
+    X, y = load_digits(return_X_y=True)
+
+    model = MLP(
+        task_type="classification",
+        n_units_in=X.shape[1],
+        n_units_out=len(np.unique(y)),
+        early_stopping=True,
+        n_iter=100,
+        n_iter_min=0,
+        patience=1,
+    )
+
+    model.fit(X, y)  # type: ignore
+
+
+def test_mlp_len():
+    X, y = load_digits(return_X_y=True)
+
+    model = MLP(
+        task_type="classification",
+        n_units_in=X.shape[1],
+        n_units_out=len(np.unique(y)),
+        early_stopping=True,
+        n_iter=10,
+    )
+
+    assert len(model) == len(model.model)
+
+
+def test_linear_layer_nonlin_none():
+    ll = LinearLayer(n_units_in=10, n_units_out=10, nonlin=None)
+    assert len(ll.model) == 1
diff --git a/tests/models/test_models_utils.py b/tests/models/test_models_utils.py
index 195b9cf7..b1017b36 100644
--- a/tests/models/test_models_utils.py
+++ b/tests/models/test_models_utils.py
@@ -85,3 +85,13 @@ def test_handle_rnn_cuda_randomness(
                 random_seed=42,
                 torch_use_deterministic_algorithms=True,
             )
+
+
+def test_get_nonlin_unknown():
+    with pytest.raises(ValueError):
+        utils.get_nonlin("unknown")  # type: ignore
+
+
+def test_get_sampler_unknown():
+    with pytest.raises(ValueError):
+        utils.get_sampler("unknown")  # type: ignore
diff --git a/tests/models/test_samplers.py b/tests/models/test_samplers.py
new file mode 100644
index 00000000..5d745205
--- /dev/null
+++ b/tests/models/test_samplers.py
@@ -0,0 +1,9 @@
+from tempor.models.samplers import ImbalancedDatasetSampler
+
+
+def test_basics():
+    s = ImbalancedDatasetSampler(labels=[0, 1, 2, 3, 4], train_size=0.8)
+    assert len(s) == 4
+    tr, ts = s.train_test()
+    assert len(tr) == 4
+    assert len(ts) == 1
diff --git a/tests/models/test_transformer.py b/tests/models/test_transformer.py
index 1e523a75..9887f440 100644
--- a/tests/models/test_transformer.py
+++ b/tests/models/test_transformer.py
@@ -3,7 +3,7 @@
 import torch
 
 from tempor.models.constants import DEVICE
-from tempor.models.transformer import TransformerModel
+from tempor.models.transformer import TransformerModel, Transpose
 from tempor.utils.dataloaders import GoogleStocksDataLoader, SineDataLoader
 
 
@@ -23,7 +23,7 @@ def unpack_dataset(source):
 
 @pytest.mark.parametrize("source", [GoogleStocksDataLoader, SineDataLoader])
 def test_sanity(source) -> None:
-    _, temporal, _, _ = unpack_dataset(source)  # pylint: disable=unused-variable
+    _, temporal, _, _ = unpack_dataset(source)
 
     n_hidden = 10
     model = TransformerModel(n_units_in=temporal[0].shape[-1], n_units_hidden=n_hidden)
@@ -31,3 +31,11 @@ def test_sanity(source) -> None:
     out = model.forward(temporal)
 
     assert out.shape == (len(temporal), temporal[0].shape[0], n_hidden)
+
+
+@pytest.mark.parametrize("contiguous", [True, False])
+def test_transpose(contiguous):
+    x = torch.ones(size=(2, 3))
+    tr = Transpose(1, 0, contiguous=contiguous)
+    out = tr(x)
+    assert list(out.shape) == [3, 2]
diff --git a/tests/models/test_ts_model.py b/tests/models/test_ts_model.py
index a343fa38..8fb0462e 100644
--- a/tests/models/test_ts_model.py
+++ b/tests/models/test_ts_model.py
@@ -1,10 +1,12 @@
 from typing import Any
+from unittest.mock import Mock
 
 import numpy as np
 import pytest
+import torch
 from typing_extensions import get_args
 
-from tempor.models.ts_model import ModelTaskType, TimeSeriesModel, TSModelMode
+from tempor.models.ts_model import ModelTaskType, TimeSeriesLayer, TimeSeriesModel, TSModelMode, WindowLinearLayer
 from tempor.utils.dataloaders import GoogleStocksDataLoader, SineDataLoader
 
 
@@ -104,6 +106,7 @@ def test_rnn_classification_fit_predict(mode: TSModelMode, source: Any) -> None:
         output_shape=[2],
         n_iter=10,
         mode=mode,
+        clipping_value=0,
     )
 
     static_data = np.concatenate([static, static_fake])
@@ -117,3 +120,140 @@ def test_rnn_classification_fit_predict(mode: TSModelMode, source: Any) -> None:
     assert y_pred.shape == y.shape
 
     assert model.score(static_data, temporal_data, observation_times, y) <= 1
+
+
+def test_init_inputs():
+    with pytest.raises(ValueError, match=".*shape.*"):
+        TimeSeriesModel(
+            task_type="regression",
+            n_static_units_in=3,
+            n_temporal_units_in=3,
+            n_temporal_window=10,
+            output_shape=[],
+        )
+
+    loss = Mock()
+    model = TimeSeriesModel(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        n_temporal_window=10,
+        output_shape=[2],
+        loss=loss,
+    )
+    assert model.loss == loss
+
+    with pytest.raises(RuntimeError, match=".*mismatch.*"):
+        TimeSeriesModel(
+            task_type="regression",
+            n_static_units_in=3,
+            n_temporal_units_in=3,
+            n_temporal_window=10,
+            output_shape=[1, 7],
+            loss=loss,
+            nonlin_out=[("tanh", 1), ("tanh", 2)],
+        )
+
+
+def test_forward_nans_found():
+    model = TimeSeriesModel(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        n_temporal_window=2,
+        output_shape=[2],
+    )
+
+    with pytest.raises(ValueError, match=".*NaNs.*static.*"):
+        model.forward(
+            static_data=torch.tensor([np.nan, np.nan]),
+            temporal_data=torch.ones(10, 3, 2),
+            observation_times=torch.ones(10, 3),
+        )
+
+    t = torch.ones(10, 3, 2)
+    t[0, 2, 1] = torch.nan
+    with pytest.raises(ValueError, match=".*NaNs.*temporal.*"):
+        model.forward(
+            static_data=torch.ones(10, 3),
+            temporal_data=t,
+            observation_times=torch.ones(10, 3),
+        )
+
+    o = torch.ones(10, 3)
+    o[0, 2] = torch.nan
+    with pytest.raises(ValueError, match=".*NaNs.*horizon.*"):
+        model.forward(
+            static_data=torch.ones(10, 3),
+            temporal_data=torch.ones(10, 3, 2),
+            observation_times=o,
+        )
+
+
+def test_predict_proba_validation_fail():
+    model = TimeSeriesModel(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        n_temporal_window=2,
+        output_shape=[2],
+    )
+
+    with pytest.raises(RuntimeError, match=".*classification.*"):
+        model.predict_proba([], [], [])
+
+
+def test_check_tensor():
+    t = torch.ones(size=(3, 2))
+    model = TimeSeriesModel(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        n_temporal_window=2,
+        output_shape=[2],
+    )
+    out = model._check_tensor(t)  # pylint: disable=protected-access
+    assert out.device == model.device
+
+
+def test_time_series_layer_unknown_mode():
+    with pytest.raises(RuntimeError, match=".*mode.*"):
+        TimeSeriesLayer(
+            n_static_units_in=3,
+            n_temporal_units_in=3,
+            n_temporal_window=2,
+            n_units_out=2,
+            mode="unknown",  # type: ignore
+        )
+
+
+@pytest.mark.parametrize("mode", ["RNN", "InceptionTime"])
+def test_time_series_layer_forward_nans(mode):
+    tsl = TimeSeriesLayer(
+        n_static_units_in=0,
+        n_temporal_units_in=3,
+        n_temporal_window=2,
+        n_units_out=2,
+        mode=mode,
+        device=torch.device("cpu"),
+    )
+    s = torch.ones(size=(0,))
+    t = torch.ones(size=(10, 2, 3))
+    t[0, 0, 0] = torch.nan
+    with pytest.raises(RuntimeError, match=".*NaNs.*"):
+        tsl.forward(s, t)
+
+
+def test_windowed_layer_forward_nans():
+    tsl = WindowLinearLayer(
+        n_static_units_in=2,
+        n_temporal_units_in=3,
+        window_size=2,
+        n_units_out=2,
+        device=torch.device("cpu"),
+    )
+    s = torch.ones(size=(5, 2))
+    t = torch.ones(size=(10, 2, 3))
+    t[0, 0, 0] = torch.nan
+    with pytest.raises(ValueError, match=".*mismatch.*"):
+        tsl.forward(s, t)
diff --git a/tests/models/test_ts_ode.py b/tests/models/test_ts_ode.py
index ccd72d05..18f6246a 100644
--- a/tests/models/test_ts_ode.py
+++ b/tests/models/test_ts_ode.py
@@ -2,6 +2,7 @@
 
 import numpy as np
 import pytest
+import torch
 
 from tempor.models.constants import ODEBackend
 from tempor.models.ts_ode import NeuralODE
@@ -62,6 +63,7 @@ def test_ode_regression_fit_predict(source: Any, backend: ODEBackend) -> None:
         n_iter=10,
         nonlin_out=[("tanh", outlen)],
         backend=backend,
+        clipping_value=0,
     )
 
     model.fit(static, temporal, observation_times, outcome)
@@ -106,3 +108,150 @@ def test_ode_classification_fit_predict(source: Any, backend: ODEBackend) -> Non
     assert y_pred.shape == y.shape
 
     assert model.score(static_data, temporal_data, observation_times, y) <= 1
+
+
+def test_init_inputs():
+    with pytest.raises(ValueError, match=".*output shape.*"):
+        NeuralODE(
+            task_type="regression",
+            n_static_units_in=3,
+            n_temporal_units_in=3,
+            output_shape=[],
+        )
+
+    with pytest.raises(RuntimeError, match=".*backend.*"):
+        NeuralODE(
+            task_type="regression",
+            n_static_units_in=3,
+            n_temporal_units_in=3,
+            output_shape=[2],
+            backend="unknown",  # type: ignore
+        )
+
+
+def test_forward_nans_found():
+    model = NeuralODE(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        output_shape=[2],
+    )
+
+    with pytest.raises(ValueError, match=".*NaNs.*static.*"):
+        model.forward(
+            static_data=torch.tensor([np.nan, np.nan]),
+            temporal_data=torch.ones(10, 3, 2),
+            observation_times=torch.ones(10, 3),
+        )
+
+    t = torch.ones(10, 3, 2)
+    t[0, 2, 1] = torch.nan
+    with pytest.raises(ValueError, match=".*NaNs.*temporal.*"):
+        model.forward(
+            static_data=torch.ones(10, 3),
+            temporal_data=t,
+            observation_times=torch.ones(10, 3),
+        )
+
+    o = torch.ones(10, 3)
+    o[0, 2] = torch.nan
+    with pytest.raises(ValueError, match=".*NaNs.*horizon.*"):
+        model.forward(
+            static_data=torch.ones(10, 3),
+            temporal_data=torch.ones(10, 3, 2),
+            observation_times=o,
+        )
+
+
+def test_forward_interpolation():
+    model = NeuralODE(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        n_layers_hidden=5,
+        output_shape=[2],
+        interpolation="linear",
+        backend="ode",
+        device=torch.device("cpu"),
+    )
+
+    s = torch.ones(10, 3)
+    t = torch.ones(10, 3, 3)
+    o = torch.ones(10, 3)
+    model.forward(s, t, o)
+
+    model = NeuralODE(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        output_shape=[2],
+        interpolation="unknown",  # type: ignore
+        device=torch.device("cpu"),
+    )
+    with pytest.raises(RuntimeError, match=".*interpolation.*"):
+        model.forward(s, t, o)
+
+
+def test_forward_invalid_solver():
+    s = torch.ones(10, 3)
+    t = torch.ones(10, 3, 3)
+    o = torch.ones(10, 3)
+
+    model = NeuralODE(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        output_shape=[2],
+        device=torch.device("cpu"),
+        backend="cde",
+    )
+    model.backend = "unknown"
+    with pytest.raises(RuntimeError, match=".*solver.*"):
+        model.forward(s, t, o)
+
+
+def test_predict_proba_validation_fail():
+    model = NeuralODE(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        output_shape=[2],
+    )
+
+    with pytest.raises(RuntimeError, match=".*classification.*"):
+        model.predict_proba([], [], [])
+
+
+def test_ode_early_stop() -> None:
+    static, temporal, observation_times, outcome = unpack_dataset(SineDataLoader)
+
+    outcome = outcome.reshape(-1, 1)
+
+    outlen = int(len(outcome.reshape(-1)) / len(outcome))
+
+    model = NeuralODE(
+        task_type="regression",
+        n_static_units_in=static.shape[-1],
+        n_temporal_units_in=temporal.shape[-1],
+        output_shape=outcome.shape[1:],  # type: ignore
+        n_iter=100,
+        nonlin_out=[("tanh", outlen)],
+        backend="cde",
+        clipping_value=0,
+        n_iter_print=1,
+        patience=1,
+    )
+
+    model.fit(static, temporal, observation_times, outcome)
+
+
+def test_check_tensor():
+    t = torch.ones(size=(3, 2))
+    model = NeuralODE(
+        task_type="regression",
+        n_static_units_in=3,
+        n_temporal_units_in=3,
+        output_shape=[2],
+    )
+    out = model._check_tensor(t)  # pylint: disable=protected-access
+    assert out.device == model.device
diff --git a/tests/plugins/core/test_base_estimator.py b/tests/plugins/core/test_base_estimator.py
index 72d599c4..4b3af6cb 100644
--- a/tests/plugins/core/test_base_estimator.py
+++ b/tests/plugins/core/test_base_estimator.py
@@ -1,5 +1,3 @@
-# pylint: disable=redefined-outer-name, unused-argument
-
 import dataclasses
 import re
 from typing import Any, Tuple
@@ -178,7 +176,9 @@ def _fit(self, data, *args, **kwargs):
         my_model = MyModel()
         data = Mock(spec=BaseDataset)
 
+        assert my_model.is_fitted is False
         my_model.fit(data)
+        assert my_model.is_fitted is True
 
     def test_fit_fails_data_not_fit_ready(self):
         from tempor.data.dataset import BaseDataset
diff --git a/tests/plugins/core/test_base_predictor.py b/tests/plugins/core/test_base_predictor.py
index ab4a15e4..06d2bfe5 100644
--- a/tests/plugins/core/test_base_predictor.py
+++ b/tests/plugins/core/test_base_predictor.py
@@ -31,11 +31,88 @@ def _fit(self, data, *args, **kwargs):
             def _predict(self, data: PredictiveDataset, *args, **kwargs) -> Any:
                 return super()._predict(data, *args, **kwargs)
 
+            def _predict_proba(self, data: PredictiveDataset, *args, **kwargs) -> Any:
+                return super()._predict(data, *args, **kwargs)
+
+            def _predict_counterfactuals(self, data: PredictiveDataset, *args, **kwargs) -> Any:
+                return super()._predict(data, *args, **kwargs)
+
+            @property
+            def is_fitted(self) -> bool:
+                return True
+
         my_model = MyModel()
         data = Mock(spec=PredictiveDataset)
         data.predict_ready = True
 
         my_model.predict(data)
+        my_model.predict_proba(data)
+        my_model.predict_counterfactuals(data)
+
+    def test_fit_predict(self):
+        class MyModel(tempor.plugins.core.BasePredictor):
+            name = "my_model"
+            category = "my_category"
+            ParamsDefinition = self.MyModelParams
+
+            @staticmethod
+            def hyperparameter_space(*args: Any, **kwargs: Any):
+                return MagicMock()
+
+            def _fit(self, data, *args, **kwargs):
+                pass
+
+            def _predict(self, data: PredictiveDataset, *args, **kwargs) -> Any:
+                return super()._predict(data, *args, **kwargs)
+
+            def _predict_proba(self, data: PredictiveDataset, *args, **kwargs) -> Any:
+                return super()._predict(data, *args, **kwargs)
+
+            def _predict_counterfactuals(self, data: PredictiveDataset, *args, **kwargs) -> Any:
+                return super()._predict(data, *args, **kwargs)
+
+        my_model = MyModel()
+        data = Mock(spec=PredictiveDataset)
+        data.predict_ready = True
+
+        my_model.fit_predict(data)
+        my_model.predict_proba(data)
+        my_model.predict_counterfactuals(data)
+
+    def test_predict_fails_data_not_fitted(self):
+        class MyModel(tempor.plugins.core.BasePredictor):
+            name = "my_model"
+            category = "my_category"
+            ParamsDefinition = self.MyModelParams
+
+            @staticmethod
+            def hyperparameter_space(*args: Any, **kwargs: Any):
+                return MagicMock()
+
+            def _fit(self, data, *args, **kwargs):
+                pass
+
+            def _predict(self, data: PredictiveDataset, *args, **kwargs) -> Any:
+                pass
+
+            def _predict_proba(self, data: PredictiveDataset, *args, **kwargs) -> Any:
+                pass
+
+            def _predict_counterfactuals(self, data: PredictiveDataset, *args, **kwargs) -> Any:
+                pass
+
+        my_model = MyModel()
+        data = Mock(spec=PredictiveDataset)
+        data.predict_ready = False
+
+        with pytest.raises(ValueError, match=".*not fitted.*"):
+            my_model.predict(data)
+
+        with pytest.raises(ValueError, match=".*not fitted.*"):
+            my_model.predict_proba(data)
+
+        with pytest.raises(ValueError, match=".*not fitted.*"):
+            my_model.predict_counterfactuals(data)
 
     def test_predict_fails_data_not_fit_ready(self):
         class MyModel(tempor.plugins.core.BasePredictor):
@@ -59,6 +136,10 @@ def _predict_proba(self, data: PredictiveDataset, *args, **kwargs) -> Any:
             def _predict_counterfactuals(self, data: PredictiveDataset, *args, **kwargs) -> Any:
                 pass
 
+            @property
+            def is_fitted(self) -> bool:
+                return True
+
         my_model = MyModel()
         data = Mock(spec=PredictiveDataset)
         data.predict_ready = False
diff --git a/tests/plugins/core/test_importing.py b/tests/plugins/core/test_importing.py
index 9e7ed17b..704ba189 100644
--- a/tests/plugins/core/test_importing.py
+++ b/tests/plugins/core/test_importing.py
@@ -2,6 +2,9 @@
 
 import os
 import pathlib
+from unittest.mock import Mock
+
+import pytest
 
 import tempor
 import tempor.plugins.core._plugin as plugin_core
@@ -74,3 +77,19 @@ def test_import_plugins(tmp_path: pathlib.Path, patch_plugins_core_module):
             "tempor.dummy_plugins.plugin_file_1",
         ]
     ) == sorted(names)
+
+
+def test_import_fail(tmp_path: pathlib.Path, monkeypatch, patch_plugins_core_module):
+    test_plugins_dir = tmp_path / tempor.import_name / "dummy_plugins"
+    test_plugins_dir.mkdir(parents=True, exist_ok=True)
+    with open(test_plugins_dir / "plugin_file_1.py", "w", encoding="utf8") as f:
+        f.write(plugin_file_1)
+
+    import importlib.util
+
+    monkeypatch.setattr(importlib.util, "spec_from_file_location", Mock(return_value=None))
+
+    plugin_core.register_plugin_category("dummy_category", expected_class=plugin_core.Plugin)
+    init_file = os.path.join(str(test_plugins_dir), "__init__.py")
+    with pytest.raises(RuntimeError, match=".*[Ii]mport failed.*"):
+        plugin_core.importing.import_plugins(init_file)
diff --git a/tests/plugins/core/test_params.py b/tests/plugins/core/test_params.py
new file mode 100644
index 00000000..b3d43110
--- /dev/null
+++ b/tests/plugins/core/test_params.py
@@ -0,0 +1,176 @@
+from typing import Any, List
+from unittest.mock import MagicMock, Mock
+
+import pytest
+from optuna.trial import Trial
+
+from tempor.plugins.core import _params as params
+
+
+class TestParams:
+    def test_basic_functionality(self):
+        get_mock = MagicMock()
+        optuna_sample = Mock()
+        default_sample = Mock()
+
+        class MyParams(params.Params):
+            def get(self) -> List[Any]:
+                return get_mock
+
+            def _sample_optuna_trial(self, trial: Trial) -> Any:
+                return optuna_sample
+
+            def _sample_default(self) -> Any:
+                return default_sample
+
+        my_params = MyParams(name="my_params", bounds=(0, 100))
+
+        assert my_params.name == "my_params"
+        assert my_params.bounds == (0, 100)
+
+        assert my_params.get() == get_mock
+        assert my_params.sample() == default_sample
+        assert my_params.sample(trial=Mock()) == optuna_sample
+
+        assert "MyParams(" in str(my_params)
+        assert "name=" in str(my_params)
+        assert "bounds=" in str(my_params)
+
+    def test_init_fails_reserved_name(self):
+        class MyParams(params.Params):
+            def get(self) -> List[Any]:
+                return Mock()
+
+            def _sample_optuna_trial(self, trial: Trial) -> Any:
+                return Mock()
+
+            def _sample_default(self) -> Any:
+                return Mock()
+
+        with pytest.raises(ValueError, match=".*special.*"):
+            MyParams(name="trail", bounds=(0, 100))
+        with pytest.raises(ValueError, match=".*special.*"):
+            MyParams(name="override", bounds=(0, 100))
+
+        for special in params.RESERVED_ARG_NAMES:
+            with pytest.raises(ValueError, match=".*special.*"):
+                MyParams(name=special, bounds=(0, 100))
+
+
+class TestCategoricalParams:
+    def test_basic_functionality(self):
+        cat_params = params.CategoricalParams(name="my_cat", choices=["a", "b", "c"])
+
+        # Init basics:
+
+        assert cat_params.name == "my_cat"
+        assert cat_params.choices == ["a", "b", "c"]
+        assert cat_params.bounds == ("a", "c")
+
+        assert cat_params.get() == ["my_cat", ["a", "b", "c"]]
+
+        # Sampling:
+
+        trial = Mock(Trial)
+        mock_suggest_categorical = Mock()
+
+        def suggest_categorical(name, choices):
+            mock_suggest_categorical(name=name, choices=choices)
+            return "optuna_suggestion"
+
+        trial.suggest_categorical = suggest_categorical
+
+        optuna_sample = cat_params.sample(trial=trial)
+
+        mock_suggest_categorical.assert_called_once_with(name="my_cat", choices=["a", "b", "c"])
+        assert optuna_sample == "optuna_suggestion"
+
+        for _ in range(100):
+            assert cat_params.sample() in ["a", "b", "c"]
+
+        # Repr:
+        assert "CategoricalParams(" in str(cat_params)
+        assert "name=" in str(cat_params)
+        assert "choices=" in str(cat_params)
+
+
+class TestFloatParams:
+    def test_basic_functionality(self):
+        float_params = params.FloatParams(name="my_float", low=1.5, high=2.5)
+
+        # Init basics:
+
+        assert float_params.name == "my_float"
+        assert float_params.bounds == (1.5, 2.5)
+        assert float_params.low == 1.5
+        assert float_params.high == 2.5
+
+        assert float_params.get() == ["my_float", 1.5, 2.5]
+
+        # Sampling:
+
+        trial = Mock(Trial)
+        mock_suggest_float = Mock()
+
+        def suggest_float(name, low, high):
+            mock_suggest_float(name=name, low=low, high=high)
+            return "optuna_suggestion"
+
+        trial.suggest_float = suggest_float
+
+        optuna_sample = float_params.sample(trial=trial)
+
+        mock_suggest_float.assert_called_once_with(name="my_float", low=1.5, high=2.5)
+        assert optuna_sample == "optuna_suggestion"
+
+        for _ in range(100):
+            assert 1.5 <= float_params.sample() <= 2.5
+
+        # Repr:
+        assert "FloatParams(" in str(float_params)
+        assert "name=" in str(float_params)
+        assert "low=1.5" in str(float_params)
+        assert "high=2.5" in str(float_params)
+
+
+class TestIntegerParams:
+    def test_basic_functionality(self):
+        int_params = params.IntegerParams(name="my_int", low=1, high=5, step=2)
+
+        # Init basics:
+
+        assert int_params.name == "my_int"
+        assert int_params.bounds == (1, 5)
+        assert int_params.low == 1
+        assert int_params.high == 5
+        assert int_params.step == 2
+
+        assert int_params.choices == [1, 3, 5]
+
+        assert int_params.get() == ["my_int", 1, 5, 2]
+
+        # Sampling:
+
+        trial = Mock(Trial)
+        mock_suggest_int = Mock()
+
+        def suggest_int(name, low, high, step):
+            mock_suggest_int(name=name, low=low, high=high, step=step)
+            return "optuna_suggestion"
+
+        trial.suggest_int = suggest_int
+
+        optuna_sample = int_params.sample(trial=trial)
+
+        mock_suggest_int.assert_called_once_with(name="my_int", low=1, high=5, step=2)
+        assert optuna_sample == "optuna_suggestion"
+
+        for _ in range(100):
+            assert int_params.sample() in [1, 3, 5]
+
+        # Repr:
+        assert "IntegerParams(" in str(int_params)
+        assert "name=" in str(int_params)
+        assert "low=1" in str(int_params)
+        assert "high=5" in str(int_params)
+        assert "step=2" in str(int_params)
diff --git a/tests/plugins/core/test_plugins.py b/tests/plugins/core/test_plugins.py
index 1a5b72cf..fce1f69d 100644
--- a/tests/plugins/core/test_plugins.py
+++ b/tests/plugins/core/test_plugins.py
@@ -79,6 +79,18 @@ class DummyPluginB1(plugin_core.Plugin):
         assert plugin_core.PLUGIN_REGISTRY[p_a2.fqn()] == DummyPluginA2
         assert plugin_core.PLUGIN_REGISTRY[p_b1.fqn()] == DummyPluginB1
 
+    def test_category_registration_reimport_allowed(self):
+        plugin_core.register_plugin_category("dummy_category", expected_class=plugin_core.Plugin)
+
+        @plugin_core.register_plugin(name="dummy_plugin_A", category="dummy_category")
+        class DummyPluginA(plugin_core.Plugin):  # type: ignore  # pylint: disable=unused-variable
+            pass
+
+        @plugin_core.register_plugin(name="dummy_plugin_A", category="dummy_category")
+        # pylint: disable-next=function-redefined
+        class DummyPluginA(plugin_core.Plugin):  # type: ignore  # pyright: ignore  # noqa: F811
+            pass
+
     def test_category_registration_fails_duplicate(self):
         plugin_core.register_plugin_category("same_category", expected_class=plugin_core.Plugin)
 
@@ -272,7 +284,7 @@ def test_plugin_added_live(self, loader: plugin_core.PluginLoader):
         plugin_core.register_plugin_category("category_c", expected_class=plugin_core.Plugin)
 
         @plugin_core.register_plugin(name="plugin_c1", category="category_c")
-        class PluginC1(plugin_core.Plugin):  # pylint: disable=unused-variable
+        class PluginC1(plugin_core.Plugin):
             pass
 
         listed = loader.list()
diff --git a/tests/plugins/prediction/one_off/classification/test_cde_classifier.py b/tests/plugins/prediction/one_off/classification/test_cde_classifier.py
index 2bacfe80..b4d57ecf 100644
--- a/tests/plugins/prediction/one_off/classification/test_cde_classifier.py
+++ b/tests/plugins/prediction/one_off/classification/test_cde_classifier.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -40,6 +41,22 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 9
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(OneOffPredictionDataset))
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict_proba(Mock(OneOffPredictionDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/one_off/classification/test_laplace_classifier.py b/tests/plugins/prediction/one_off/classification/test_laplace_classifier.py
index e38be9a1..065f5898 100644
--- a/tests/plugins/prediction/one_off/classification/test_laplace_classifier.py
+++ b/tests/plugins/prediction/one_off/classification/test_laplace_classifier.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -40,6 +41,22 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 7
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(OneOffPredictionDataset))
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict_proba(Mock(OneOffPredictionDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
@@ -71,6 +88,26 @@ def test_predict(
     assert output.numpy().shape == (len(dataset.time_series), 1)
 
 
+@pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
+@pytest.mark.parametrize("data", TEST_ON_DATASETS)
+@pytest.mark.parametrize("device", DEVICES)
+def test_predict_proba(
+    plugin_from: str, data: str, device: str, get_test_plugin: Callable, get_dataset: Callable
+) -> None:
+    test_plugin: BaseOneOffClassifier = get_test_plugin(plugin_from, INIT_KWARGS, device=device)
+    dataset = get_dataset(data)
+
+    output = test_plugin.fit(dataset).predict_proba(dataset)
+
+    if data == "google_stocks_data_small":
+        out_classes = 6
+    elif data == "google_stocks_data_full":
+        out_classes = 10
+    else:
+        raise NotImplementedError
+    assert output.numpy().shape == (len(dataset.time_series), out_classes)
+
+
 @pytest.mark.filterwarnings("ignore:RNN.*contiguous.*:UserWarning")  # Expected: problem with current serialization.
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/one_off/classification/test_nn_classifier.py b/tests/plugins/prediction/one_off/classification/test_nn_classifier.py
index c993dca3..79a9a08d 100644
--- a/tests/plugins/prediction/one_off/classification/test_nn_classifier.py
+++ b/tests/plugins/prediction/one_off/classification/test_nn_classifier.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -40,6 +41,22 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 9
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(OneOffPredictionDataset))
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict_proba(Mock(OneOffPredictionDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/one_off/classification/test_ode_classifier.py b/tests/plugins/prediction/one_off/classification/test_ode_classifier.py
index b9fb7328..747f48d0 100644
--- a/tests/plugins/prediction/one_off/classification/test_ode_classifier.py
+++ b/tests/plugins/prediction/one_off/classification/test_ode_classifier.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -40,6 +41,22 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 9
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(OneOffPredictionDataset))
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict_proba(Mock(OneOffPredictionDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/one_off/classification/test_prediction_oneoff_classification_base.py b/tests/plugins/prediction/one_off/classification/test_prediction_oneoff_classification_base.py
new file mode 100644
index 00000000..6bff56bb
--- /dev/null
+++ b/tests/plugins/prediction/one_off/classification/test_prediction_oneoff_classification_base.py
@@ -0,0 +1,76 @@
+from typing import TYPE_CHECKING, Any, List
+
+import numpy as np
+import pytest
+
+from tempor.data.dataset import PredictiveDataset
+from tempor.plugins.prediction.one_off.classification import BaseOneOffClassifier
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+    from tempor.plugins.core._params import Params
+    from tempor.data.dataset import BaseDataset
+    from tempor.data.samples import StaticSamples
+
+from unittest.mock import Mock
+
+
+class DummyOneOffClassifier(BaseOneOffClassifier):
+    name = "dummy"
+    category = "dummy_cat"
+
+    def _predict(self, data: "PredictiveDataset", *args, **kwargs) -> "StaticSamples":
+        raise NotImplementedError
+
+    def _predict_proba(self, data: "PredictiveDataset", *args, **kwargs) -> "StaticSamples":
+        raise NotImplementedError
+
+    def _fit(self, data: "BaseDataset", *args, **kwargs) -> "Self":
+        return super()._fit(data, *args, **kwargs)
+
+    @staticmethod
+    def hyperparameter_space(*args: Any, **kwargs: Any) -> List["Params"]:
+        raise NotImplementedError
+
+
+def test_wrong_dataset():
+    plugin = DummyOneOffClassifier()
+
+    with pytest.raises(TypeError, match="[Ee]xpected.*data.*one-off classification.*"):
+        plugin.fit(data=Mock())
+
+
+def test_unpack_dataset_empty():
+    plugin = DummyOneOffClassifier()
+
+    mock_data = Mock(
+        PredictiveDataset,
+        time_series=Mock(
+            numpy=Mock(return_value=[1, 2, 3]),
+            time_indexes=Mock(return_value="mock_time_indexes"),
+        ),
+        static=None,
+        predictive=Mock(
+            targets=None,
+        ),
+    )
+
+    _, _, _, outcome = plugin._unpack_dataset(data=mock_data)  # pylint: disable=protected-access
+
+    assert outcome.shape == np.zeros((3, 0)).shape
+
+    mock_data = Mock(
+        PredictiveDataset,
+        time_series=Mock(
+            numpy=Mock(return_value=[1, 2, 3]),
+            time_indexes=Mock(return_value="mock_time_indexes"),
+        ),
+        static=None,
+        predictive=Mock(
+            targets=Mock(numpy=Mock(return_value=np.asarray([1, 1, 1, 1]))),
+        ),
+    )
+
+    _, _, _, outcome = plugin._unpack_dataset(data=mock_data)  # pylint: disable=protected-access
+
+    assert outcome.shape == np.zeros((4, 1)).shape
diff --git a/tests/plugins/prediction/one_off/regression/test_cde_regressor.py b/tests/plugins/prediction/one_off/regression/test_cde_regressor.py
index 3d3ad412..19287ad5 100644
--- a/tests/plugins/prediction/one_off/regression/test_cde_regressor.py
+++ b/tests/plugins/prediction/one_off/regression/test_cde_regressor.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -39,6 +40,19 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 9
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(OneOffPredictionDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/one_off/regression/test_laplace_regressor.py b/tests/plugins/prediction/one_off/regression/test_laplace_regressor.py
index 6a018793..cf8bf019 100644
--- a/tests/plugins/prediction/one_off/regression/test_laplace_regressor.py
+++ b/tests/plugins/prediction/one_off/regression/test_laplace_regressor.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -39,6 +40,19 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 7
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(OneOffPredictionDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/one_off/regression/test_nn_regressor.py b/tests/plugins/prediction/one_off/regression/test_nn_regressor.py
index 4c7c19bd..1b9b29d2 100644
--- a/tests/plugins/prediction/one_off/regression/test_nn_regressor.py
+++ b/tests/plugins/prediction/one_off/regression/test_nn_regressor.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -39,6 +40,19 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 9
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(OneOffPredictionDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/one_off/regression/test_ode_regressor.py b/tests/plugins/prediction/one_off/regression/test_ode_regressor.py
index 6b507381..b573aedd 100644
--- a/tests/plugins/prediction/one_off/regression/test_ode_regressor.py
+++ b/tests/plugins/prediction/one_off/regression/test_ode_regressor.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -39,6 +40,19 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 9
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(OneOffPredictionDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/one_off/regression/test_prediction_oneoff_regression_base.py b/tests/plugins/prediction/one_off/regression/test_prediction_oneoff_regression_base.py
new file mode 100644
index 00000000..dee4fa1f
--- /dev/null
+++ b/tests/plugins/prediction/one_off/regression/test_prediction_oneoff_regression_base.py
@@ -0,0 +1,57 @@
+from typing import TYPE_CHECKING, Any, List
+
+import numpy as np
+import pytest
+
+from tempor.data.dataset import PredictiveDataset
+from tempor.plugins.prediction.one_off.regression import BaseOneOffRegressor
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+    from tempor.plugins.core._params import Params
+    from tempor.data.dataset import BaseDataset
+    from tempor.data.samples import StaticSamples
+
+from unittest.mock import Mock
+
+
+class DummyOneOffRegressor(BaseOneOffRegressor):
+    name = "dummy"
+    category = "dummy_cat"
+
+    def _predict(self, data: "PredictiveDataset", *args, **kwargs) -> "StaticSamples":
+        raise NotImplementedError
+
+    def _fit(self, data: "BaseDataset", *args, **kwargs) -> "Self":
+        return super()._fit(data, *args, **kwargs)
+
+    @staticmethod
+    def hyperparameter_space(*args: Any, **kwargs: Any) -> List["Params"]:
+        raise NotImplementedError
+
+
+def test_wrong_dataset():
+    plugin = DummyOneOffRegressor()
+
+    with pytest.raises(TypeError, match="[Ee]xpected.*data.*one-off regression.*"):
+        plugin.fit(data=Mock())
+
+
+def test_unpack_dataset_empty():
+    plugin = DummyOneOffRegressor()
+
+    mock_data = Mock(
+        PredictiveDataset,
+        time_series=Mock(
+            numpy=Mock(return_value=[1, 2, 3]),
+            time_indexes=Mock(return_value="mock_time_indexes"),
+        ),
+        static=None,
+        predictive=Mock(
+            targets=None,
+        ),
+    )
+
+    _, _, _, outcome = plugin._unpack_dataset(data=mock_data)  # pylint: disable=protected-access
+
+    assert outcome.shape == np.zeros((3, 0)).shape
diff --git a/tests/plugins/prediction/temporal/classification/test_prediction_temporal_classification_base.py b/tests/plugins/prediction/temporal/classification/test_prediction_temporal_classification_base.py
new file mode 100644
index 00000000..ada2da64
--- /dev/null
+++ b/tests/plugins/prediction/temporal/classification/test_prediction_temporal_classification_base.py
@@ -0,0 +1,64 @@
+from typing import TYPE_CHECKING, Any, List
+
+import pytest
+
+from tempor.data.dataset import TemporalPredictionDataset
+from tempor.plugins.prediction.temporal.classification import BaseTemporalClassifier
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+    from tempor.plugins.core._params import Params
+    from tempor.data.dataset import BaseDataset, PredictiveDataset
+    from tempor.data.samples import TimeSeriesSamples
+
+from unittest.mock import MagicMock, Mock
+
+mock_predict_proba = Mock()
+
+
+class DummyTemporalClassifier(BaseTemporalClassifier):
+    name = "dummy"
+    category = "dummy_cat"
+
+    def _predict(
+        self,
+        data: "PredictiveDataset",
+        n_future_steps: int,
+        *args,
+        time_delta: int = 1,
+        **kwargs,
+    ) -> "TimeSeriesSamples":
+        raise NotImplementedError
+
+    def _predict_proba(
+        self,
+        data: "PredictiveDataset",
+        n_future_steps: int,
+        *args,
+        time_delta: int = 1,
+        **kwargs,
+    ) -> "TimeSeriesSamples":
+        mock_predict_proba()
+        return Mock()
+
+    def _fit(self, data: "BaseDataset", *args, **kwargs) -> "Self":
+        return super()._fit(data, *args, **kwargs)
+
+    @staticmethod
+    def hyperparameter_space(*args: Any, **kwargs: Any) -> List["Params"]:
+        raise NotImplementedError
+
+
+def test_wrong_dataset():
+    plugin = DummyTemporalClassifier()
+
+    with pytest.raises(TypeError, match="[Ee]xpected.*data.*temporal classification.*"):
+        plugin.fit(data=Mock())
+
+
+def test_predict_proba():
+    plugin = DummyTemporalClassifier()
+    plugin._fitted = True  # pylint: disable=protected-access
+
+    plugin.predict_proba(MagicMock(TemporalPredictionDataset), n_future_steps=1)
+    mock_predict_proba.assert_called_once()
diff --git a/tests/plugins/prediction/temporal/classification/test_seq2seq_classifier.py b/tests/plugins/prediction/temporal/classification/test_seq2seq_classifier.py
index c9017d2b..8eb9f84f 100644
--- a/tests/plugins/prediction/temporal/classification/test_seq2seq_classifier.py
+++ b/tests/plugins/prediction/temporal/classification/test_seq2seq_classifier.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -39,6 +40,32 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 8
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import TemporalPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(TemporalPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(TemporalPredictionDataset), n_future_steps=1)
+
+
+def test_predict_proba_not_implemented(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import TemporalPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(TemporalPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(NotImplementedError):
+        test_plugin.predict_proba(Mock(TemporalPredictionDataset), n_future_steps=1)
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/prediction/temporal/regression/test_prediction_temporal_regression_base.py b/tests/plugins/prediction/temporal/regression/test_prediction_temporal_regression_base.py
new file mode 100644
index 00000000..efa81880
--- /dev/null
+++ b/tests/plugins/prediction/temporal/regression/test_prediction_temporal_regression_base.py
@@ -0,0 +1,42 @@
+from typing import TYPE_CHECKING, Any, List
+
+import pytest
+
+from tempor.plugins.prediction.temporal.regression import BaseTemporalRegressor
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+    from tempor.plugins.core._params import Params
+    from tempor.data.dataset import BaseDataset, PredictiveDataset
+    from tempor.data.samples import TimeSeriesSamples
+
+from unittest.mock import Mock
+
+
+class DummyTemporalRegressor(BaseTemporalRegressor):
+    name = "dummy"
+    category = "dummy_cat"
+
+    def _predict(
+        self,
+        data: "PredictiveDataset",
+        n_future_steps: int,
+        *args,
+        time_delta: int = 1,
+        **kwargs,
+    ) -> "TimeSeriesSamples":
+        raise NotImplementedError
+
+    def _fit(self, data: "BaseDataset", *args, **kwargs) -> "Self":
+        return super()._fit(data, *args, **kwargs)
+
+    @staticmethod
+    def hyperparameter_space(*args: Any, **kwargs: Any) -> List["Params"]:
+        raise NotImplementedError
+
+
+def test_wrong_dataset():
+    plugin = DummyTemporalRegressor()
+
+    with pytest.raises(TypeError, match="[Ee]xpected.*data.*temporal regression.*"):
+        plugin.fit(data=Mock())
diff --git a/tests/plugins/prediction/temporal/regression/test_seq2seq_regressor.py b/tests/plugins/prediction/temporal/regression/test_seq2seq_regressor.py
index abe860d4..05be6559 100644
--- a/tests/plugins/prediction/temporal/regression/test_seq2seq_regressor.py
+++ b/tests/plugins/prediction/temporal/regression/test_seq2seq_regressor.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -39,6 +40,19 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 8
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import TemporalPredictionDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(TemporalPredictionDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(TemporalPredictionDataset), n_future_steps=1)
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/preprocessing/scaling/static/test_static_standard_scaler.py b/tests/plugins/preprocessing/scaling/static/test_static_standard_scaler.py
index 6986de74..d5a69804 100644
--- a/tests/plugins/preprocessing/scaling/static/test_static_standard_scaler.py
+++ b/tests/plugins/preprocessing/scaling/static/test_static_standard_scaler.py
@@ -37,6 +37,16 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 0
 
 
+def test_no_static(get_test_plugin: Callable, get_dataset: Callable) -> None:
+    test_plugin: BaseScaler = get_test_plugin("from_api", INIT_KWARGS)
+    dataset = get_dataset("sine_data_scaled_small")
+    dataset.static = None
+
+    test_plugin.fit(dataset)
+    out = test_plugin.transform(dataset)
+    assert out.static is None
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 def test_fit(plugin_from: str, data: str, get_test_plugin: Callable, get_dataset: Callable) -> None:
diff --git a/tests/plugins/time_to_event/test_helper_embedding.py b/tests/plugins/time_to_event/test_helper_embedding.py
new file mode 100644
index 00000000..fadab7ff
--- /dev/null
+++ b/tests/plugins/time_to_event/test_helper_embedding.py
@@ -0,0 +1,36 @@
+from unittest.mock import Mock
+
+import pytest
+
+from tempor.exc import UnsupportedSetupException
+from tempor.plugins.time_to_event.helper_embedding import DDHEmbedding, DynamicDeepHitModel
+
+
+def test_merge_data_no_static(pbc_data_full):
+    emb = DDHEmbedding(emb_model=DynamicDeepHitModel())
+    data = pbc_data_full
+    (_, temporal, observation_times, *_) = emb._convert_data(data)  # pylint: disable=protected-access
+    emb._merge_data(None, temporal, observation_times)  # pylint: disable=protected-access
+
+
+def test_convert_data_no_static_no_targets(pbc_data_full):
+    emb = DDHEmbedding(emb_model=DynamicDeepHitModel())
+    data = pbc_data_full
+    data._validate = Mock()  # pylint: disable=protected-access
+    data.static = None
+    data.predictive.targets = None
+    (static, _, _, event_times, event_values) = emb._convert_data(data)  # pylint: disable=protected-access
+    assert event_times is None
+    assert event_values is None
+    assert static.shape[1] == 0  # type: ignore
+
+
+def test_validate_data(pbc_data_full):
+    emb = DDHEmbedding(emb_model=DynamicDeepHitModel())
+    data = pbc_data_full
+
+    data._validate = Mock()  # pylint: disable=protected-access
+    data.predictive.targets = Mock(return_value=100, num_features=100)
+
+    with pytest.raises(UnsupportedSetupException):
+        emb._validate_data(data)  # pylint: disable=protected-access
diff --git a/tests/plugins/time_to_event/test_time_to_event_base.py b/tests/plugins/time_to_event/test_time_to_event_base.py
new file mode 100644
index 00000000..69f099b7
--- /dev/null
+++ b/tests/plugins/time_to_event/test_time_to_event_base.py
@@ -0,0 +1,53 @@
+from typing import TYPE_CHECKING, Any, List
+
+import pytest
+
+from tempor.data.dataset import TimeToEventAnalysisDataset
+from tempor.exc import UnsupportedSetupException
+from tempor.plugins.time_to_event import BaseTimeToEventAnalysis
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+    from tempor.plugins.core._params import Params
+    from tempor.data.dataset import BaseDataset, PredictiveDataset
+    from tempor.data.samples import TimeSeriesSamples
+
+from unittest.mock import MagicMock, Mock
+
+mock_predict_proba = Mock()
+
+
+class DummyTimeToEventAnalysis(BaseTimeToEventAnalysis):
+    name = "dummy"
+    category = "dummy_cat"
+
+    def _predict(
+        self,
+        data: "PredictiveDataset",
+        horizons,
+        *args,
+        **kwargs,
+    ) -> "TimeSeriesSamples":
+        raise NotImplementedError
+
+    def _fit(self, data: "BaseDataset", *args, **kwargs) -> "Self":
+        return super()._fit(data, *args, **kwargs)
+
+    @staticmethod
+    def hyperparameter_space(*args: Any, **kwargs: Any) -> List["Params"]:
+        raise NotImplementedError
+
+
+def test_wrong_dataset():
+    plugin = DummyTimeToEventAnalysis()
+
+    with pytest.raises(TypeError, match="[Ee]xpected.*data.*survival analysis.*"):
+        plugin.fit(data=Mock())
+
+
+def test_predict_proba():
+    plugin = DummyTimeToEventAnalysis()
+    plugin._fitted = True  # pylint: disable=protected-access
+
+    with pytest.raises(UnsupportedSetupException):
+        plugin.predict_proba(MagicMock(TimeToEventAnalysisDataset))
diff --git a/tests/plugins/time_to_event/test_ts_coxph_plugin.py b/tests/plugins/time_to_event/test_ts_coxph_plugin.py
index e9eaa977..e9c5f6b8 100644
--- a/tests/plugins/time_to_event/test_ts_coxph_plugin.py
+++ b/tests/plugins/time_to_event/test_ts_coxph_plugin.py
@@ -2,10 +2,16 @@
 
 from typing import TYPE_CHECKING, Callable, Dict
 
+import numpy as np
+import pandas as pd
 import pytest
 
 from tempor.benchmarks.evaluation import evaluate_time_to_event
-from tempor.plugins.time_to_event.plugin_ts_coxph import CoxPHTimeToEventAnalysis
+from tempor.plugins.time_to_event.plugin_ts_coxph import (
+    CoxPHSurvivalAnalysis,
+    CoxPHTimeToEventAnalysis,
+    drop_constant_columns,
+)
 from tempor.utils.serialization import load, save
 
 if TYPE_CHECKING:  # pragma: no cover
@@ -102,3 +108,31 @@ def test_benchmark(
 
     assert (score.loc[:, "errors"] == 0).all()
     assert score.loc["c_index", "mean"] > 0.5  # pyright: ignore
+
+
+class TestCoxPHSurvivalAnalysis:
+    def test_drop_constant_columns(self):
+        df = pd.DataFrame(data=np.asarray([[1, 1, 1], [1, 2, 1], [2, 2, 2]]).T, columns=["a", "b", "c"])
+        print(df)
+        const_cols = drop_constant_columns(df)
+        assert const_cols == ["a", "c"]
+
+    def test_init_fit_options(self):
+        model = CoxPHSurvivalAnalysis(fit_options=None)
+        assert model.fit_options == {"step_size": 0.1}
+        model = CoxPHSurvivalAnalysis(fit_options={"step_size": 0.5})
+        assert model.fit_options == {"step_size": 0.5}
+
+    @pytest.mark.filterwarnings("ignore::lifelines.utils.ConvergenceWarning")  # Expected.
+    def test_predict_risk(self):
+        model = CoxPHSurvivalAnalysis()
+        X = pd.DataFrame({"f1": [11, 22, 11], "f2": [0.11, 0.91, 0.13]})
+        Y = pd.Series([True, False, True])
+        T = pd.Series([2, 5, 19])
+        model.fit(X, T, Y)
+
+        out1 = model.predict_risk(X, time_horizons=[3])
+        out2 = model.predict_risk(X, time_horizons=[100])  # Case: eval times all > survival times
+
+        assert out1.shape == (3, 1)
+        assert out2.shape == (3, 1)
diff --git a/tests/plugins/time_to_event/test_ts_xgb_plugin.py b/tests/plugins/time_to_event/test_ts_xgb_plugin.py
index f7a6305f..5aa36e94 100644
--- a/tests/plugins/time_to_event/test_ts_xgb_plugin.py
+++ b/tests/plugins/time_to_event/test_ts_xgb_plugin.py
@@ -2,10 +2,11 @@
 
 from typing import TYPE_CHECKING, Callable, Dict
 
+import pandas as pd
 import pytest
 
 from tempor.benchmarks.evaluation import evaluate_time_to_event
-from tempor.plugins.time_to_event.plugin_ts_xgb import XGBTimeToEventAnalysis
+from tempor.plugins.time_to_event.plugin_ts_xgb import XGBSurvivalAnalysis, XGBTimeToEventAnalysis
 from tempor.utils.serialization import load, save
 
 if TYPE_CHECKING:  # pragma: no cover
@@ -100,3 +101,16 @@ def test_benchmark(
         assert score.loc["c_index", "mean"] >= 0.5  # pyright: ignore
     else:
         assert score.loc["c_index", "mean"] > 0.5  # pyright: ignore
+
+
+class TestXGBSurvivalAnalysis:
+    def test_init_validation_failure(self):
+        with pytest.raises(ValueError, match=".*strategy.*"):
+            XGBSurvivalAnalysis(strategy="unknown")  # type: ignore
+
+    def test_fit_lower_bound_edge_case(self):
+        model = XGBSurvivalAnalysis()
+        X = pd.DataFrame({"f1": [11, 22, 11], "f2": [0.11, 0.91, 0.13]})
+        Y = pd.Series([True, False, True])
+        T = pd.Series([2, 5, 19])
+        model.fit(X, T, Y)
diff --git a/tests/plugins/treatments/one_off/regression/test_synctwin_regressor.py b/tests/plugins/treatments/one_off/regression/test_synctwin_regressor.py
index 283ec2fa..0a83dc7d 100644
--- a/tests/plugins/treatments/one_off/regression/test_synctwin_regressor.py
+++ b/tests/plugins/treatments/one_off/regression/test_synctwin_regressor.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import TYPE_CHECKING, Callable, Dict
+from unittest.mock import Mock
 
 import pandas as pd
 import pytest
@@ -45,6 +46,19 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 5
 
 
+def test_fit_first_runtime_error(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import OneOffTreatmentEffectsDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(OneOffTreatmentEffectsDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict_counterfactuals(Mock(OneOffTreatmentEffectsDataset))
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/treatments/one_off/test_treatments_oneoff_base.py b/tests/plugins/treatments/one_off/test_treatments_oneoff_base.py
new file mode 100644
index 00000000..8a5ef003
--- /dev/null
+++ b/tests/plugins/treatments/one_off/test_treatments_oneoff_base.py
@@ -0,0 +1,52 @@
+from typing import TYPE_CHECKING, Any, List
+
+import pytest
+
+from tempor.data.dataset import OneOffTreatmentEffectsDataset
+from tempor.plugins.treatments.one_off import BaseOneOffTreatmentEffects
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+    from tempor.plugins.core._params import Params
+    from tempor.data.dataset import BaseDataset, PredictiveDataset
+    from tempor.data.samples import TimeSeriesSamples
+
+from unittest.mock import MagicMock, Mock
+
+mock_predict_proba = Mock()
+
+
+class DummyOneOffTreatmentEffects(BaseOneOffTreatmentEffects):
+    name = "dummy"
+    category = "dummy_cat"
+
+    def _predict(
+        self,
+        data: "PredictiveDataset",
+        *args,
+        **kwargs,
+    ) -> "TimeSeriesSamples":
+        return Mock()
+
+    def _predict_counterfactuals(self, data: "PredictiveDataset", *args, **kwargs) -> List:
+        return []
+
+    def _fit(self, data: "BaseDataset", *args, **kwargs) -> "Self":
+        return self
+
+    @staticmethod
+    def hyperparameter_space(*args: Any, **kwargs: Any) -> List["Params"]:
+        raise NotImplementedError
+
+
+def test_wrong_dataset():
+    plugin = DummyOneOffTreatmentEffects()
+
+    with pytest.raises(TypeError, match="[Ee]xpected.*data.*one-off treatment effects.*"):
+        plugin.fit(data=Mock())
+
+
+def test_predict():
+    plugin = DummyOneOffTreatmentEffects()
+    plugin._fitted = True  # pylint: disable=protected-access
+    plugin.predict(MagicMock(OneOffTreatmentEffectsDataset))
diff --git a/tests/plugins/treatments/temporal/classification/test_crn_classifier.py b/tests/plugins/treatments/temporal/classification/test_crn_classifier.py
index 9ab05779..ed3405f3 100644
--- a/tests/plugins/treatments/temporal/classification/test_crn_classifier.py
+++ b/tests/plugins/treatments/temporal/classification/test_crn_classifier.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import TYPE_CHECKING, Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -41,6 +42,46 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 8
 
 
+def test_validations(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import TemporalTreatmentEffectsDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(TemporalTreatmentEffectsDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(TemporalTreatmentEffectsDataset), horizons=[[]])
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict_counterfactuals(
+            Mock(TemporalTreatmentEffectsDataset), horizons=[[]], treatment_scenarios=[[]]
+        )
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+    test_plugin._fitted = True  # pylint: disable=protected-access
+    test_plugin.model = Mock()
+
+    with pytest.raises(ValueError, match=".*horizons.*length.*"):
+        test_plugin.predict(Mock(TemporalTreatmentEffectsDataset, __len__=Mock(return_value=3)), horizons=[[1], [1]])
+
+    with pytest.raises(ValueError, match=".*horizons.*length.*"):
+        test_plugin.predict_counterfactuals(
+            Mock(TemporalTreatmentEffectsDataset, __len__=Mock(return_value=3)),
+            horizons=[[1], [1]],
+            treatment_scenarios=[[]],
+        )
+
+    with pytest.raises(ValueError, match=".*treatment.*scenarios.*length.*"):
+        test_plugin.predict_counterfactuals(
+            Mock(TemporalTreatmentEffectsDataset, __len__=Mock(return_value=3)),
+            horizons=[[1], [1], [1]],
+            treatment_scenarios=[[1], [1], [1], [2]],
+        )
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/treatments/temporal/regression/test_crn_regressor.py b/tests/plugins/treatments/temporal/regression/test_crn_regressor.py
index 85cc7715..06e77fe5 100644
--- a/tests/plugins/treatments/temporal/regression/test_crn_regressor.py
+++ b/tests/plugins/treatments/temporal/regression/test_crn_regressor.py
@@ -1,6 +1,7 @@
 # pylint: disable=redefined-outer-name
 
 from typing import TYPE_CHECKING, Callable, Dict
+from unittest.mock import Mock
 
 import pytest
 
@@ -41,6 +42,46 @@ def test_sanity(get_test_plugin: Callable, plugin_from: str) -> None:
     assert len(test_plugin.hyperparameter_space()) == 8
 
 
+def test_validations(get_test_plugin: Callable, monkeypatch):
+    from tempor.data.dataset import TemporalTreatmentEffectsDataset
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+
+    test_plugin.fit(Mock(TemporalTreatmentEffectsDataset))
+    test_plugin.model = None
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict(Mock(TemporalTreatmentEffectsDataset), horizons=[[]])
+
+    with pytest.raises(RuntimeError, match=".*[Ff]it.*first.*"):
+        test_plugin.predict_counterfactuals(
+            Mock(TemporalTreatmentEffectsDataset), horizons=[[]], treatment_scenarios=[[]]
+        )
+
+    test_plugin = get_test_plugin("from_api", INIT_KWARGS, device="cpu")
+    monkeypatch.setattr(test_plugin, "_fit", Mock())
+    test_plugin._fitted = True  # pylint: disable=protected-access
+    test_plugin.model = Mock()
+
+    with pytest.raises(ValueError, match=".*horizons.*length.*"):
+        test_plugin.predict(Mock(TemporalTreatmentEffectsDataset, __len__=Mock(return_value=3)), horizons=[[1], [1]])
+
+    with pytest.raises(ValueError, match=".*horizons.*length.*"):
+        test_plugin.predict_counterfactuals(
+            Mock(TemporalTreatmentEffectsDataset, __len__=Mock(return_value=3)),
+            horizons=[[1], [1]],
+            treatment_scenarios=[[]],
+        )
+
+    with pytest.raises(ValueError, match=".*treatment.*scenarios.*length.*"):
+        test_plugin.predict_counterfactuals(
+            Mock(TemporalTreatmentEffectsDataset, __len__=Mock(return_value=3)),
+            horizons=[[1], [1], [1]],
+            treatment_scenarios=[[1], [1], [1], [2]],
+        )
+
+
 @pytest.mark.parametrize("plugin_from", PLUGIN_FROM_OPTIONS)
 @pytest.mark.parametrize("data", TEST_ON_DATASETS)
 @pytest.mark.parametrize("device", DEVICES)
diff --git a/tests/plugins/treatments/temporal/test_treatments_temporal_base.py b/tests/plugins/treatments/temporal/test_treatments_temporal_base.py
new file mode 100644
index 00000000..aad87df5
--- /dev/null
+++ b/tests/plugins/treatments/temporal/test_treatments_temporal_base.py
@@ -0,0 +1,43 @@
+from typing import TYPE_CHECKING, Any, List
+
+import pytest
+
+from tempor.plugins.treatments.temporal import BaseTemporalTreatmentEffects
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+    from tempor.plugins.core._params import Params
+    from tempor.data.dataset import BaseDataset, PredictiveDataset
+    from tempor.data.samples import TimeSeriesSamples
+
+from unittest.mock import Mock
+
+
+class DummyTemporalTreatmentEffects(BaseTemporalTreatmentEffects):
+    name = "dummy"
+    category = "dummy_cat"
+
+    def _predict(
+        self,
+        data: "PredictiveDataset",
+        *args,
+        **kwargs,
+    ) -> "TimeSeriesSamples":
+        return Mock()
+
+    def _predict_counterfactuals(self, data: "PredictiveDataset", *args, **kwargs) -> List:
+        return []
+
+    def _fit(self, data: "BaseDataset", *args, **kwargs) -> "Self":
+        return self
+
+    @staticmethod
+    def hyperparameter_space(*args: Any, **kwargs: Any) -> List["Params"]:
+        raise NotImplementedError
+
+
+def test_wrong_dataset():
+    plugin = DummyTemporalTreatmentEffects()
+
+    with pytest.raises(TypeError, match="[Ee]xpected.*data.*temporal treatment effects.*"):
+        plugin.fit(data=Mock())
diff --git a/tests/utils/dataloaders/test_dataloaders.py b/tests/utils/dataloaders/test_dataloaders.py
index 184444a2..acf49e4e 100644
--- a/tests/utils/dataloaders/test_dataloaders.py
+++ b/tests/utils/dataloaders/test_dataloaders.py
@@ -12,8 +12,8 @@
     [
         dataloaders.SineDataLoader,
         dataloaders.GoogleStocksDataLoader,
-        dataloaders.GoogleStocksDataLoader,
         dataloaders.PKPDDataLoader,
+        dataloaders.PBCDataLoader,
         dataloaders.DummyTemporalPredictionDataLoader,
         dataloaders.DummyTemporalTreatmentEffectsDataLoader,
         dataloaders.UCIDiabetesDataLoader,
@@ -33,3 +33,22 @@ def test_init_load_and_basic_methods(dataloader_cls: Type[DataLoader]):
     # Assert the right type of dataset for the dataloader has been returned.
     return_type = dataloader_cls.load.__annotations__["return"]
     assert isinstance(dataset, return_type)
+
+
+@pytest.mark.parametrize(
+    "dataloader_cls",
+    [
+        dataloaders.GoogleStocksDataLoader,
+        dataloaders.PBCDataLoader,
+        dataloaders.UCIDiabetesDataLoader,
+    ],
+)
+def test_download_and_local(dataloader_cls, tmpdir, monkeypatch):
+    temp_data_root_dir = tmpdir.mkdir("data_root")
+    monkeypatch.setattr(DataLoader, "data_root_dir", str(temp_data_root_dir))
+
+    dl = dataloader_cls()
+    # First time - download.
+    dl.load()
+    # Second time - local.
+    dl.load()
diff --git a/tests/utils/test_serialization.py b/tests/utils/test_serialization.py
index 7b87d450..ed082d97 100644
--- a/tests/utils/test_serialization.py
+++ b/tests/utils/test_serialization.py
@@ -1,4 +1,4 @@
-from tempor.utils.serialization import load, save
+from tempor.utils.serialization import load, load_from_file, save, save_to_file
 
 
 def test_save_load() -> None:
@@ -11,3 +11,22 @@ def test_save_load() -> None:
     assert isinstance(reloaded, dict)
     assert reloaded["a"] == 1
     assert reloaded["b"] == "dssf"
+
+
+def test_save_load_file(tmpdir) -> None:
+    path = tmpdir.mkdir("serialization").join("obj.p")
+    obj = {"a": 1, "b": "dssf"}
+    save_to_file(path, obj)
+    reloaded = load_from_file(path)
+    assert isinstance(reloaded, dict)
+    assert reloaded["a"] == 1
+    assert reloaded["b"] == "dssf"
+
+    # Test the mkdir case also.
+    path = str(tmpdir / "nonexistent" / "obj.p")
+    obj = {"a": 1, "b": "dssf"}
+    save_to_file(path, obj)
+    reloaded = load_from_file(path)
+    assert isinstance(reloaded, dict)
+    assert reloaded["a"] == 1
+    assert reloaded["b"] == "dssf"
diff --git a/tutorials/extending/tutorial02_testing_custom_plugin.ipynb b/tutorials/extending/tutorial02_testing_custom_plugin.ipynb
index 3fe32922..c4a01248 100644
--- a/tutorials/extending/tutorial02_testing_custom_plugin.ipynb
+++ b/tutorials/extending/tutorial02_testing_custom_plugin.ipynb
@@ -33,7 +33,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### ⚙️ Installation for testing\n",
+    "**⚙️ Installation for testing**\n",
     "\n",
     "You should install the `[dev]` TemporAI to run tests:\n",
     "```bash\n",