Repository: blei-lab/treeffuser (language: python)

Issue:
Support for pandas dataframe
Support pandas Dataframe as input to Treeffuser. In particular, users won't have to manually specific the categorical columns (with `cat_idx`).

Proposed in: #86 
Support for pandas dataframe
Support pandas Dataframe as input to Treeffuser. In particular, users won't have to manually specific the categorical columns (with `cat_idx`).

Proposed in: #86 


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 6340d65c..e40f04d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@ docs/_build
 # Data files
 **/raw/**
 **/data.npy
+examples/m5/*
 
 # Debug files
 
diff --git a/examples/README_example.ipynb b/examples/README_example.ipynb
new file mode 100644
index 00000000..05fcf404
--- /dev/null
+++ b/examples/README_example.ipynb
@@ -0,0 +1,209 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Quick start: Forecasting with synthetic data\n",
+    "\n",
+    "In this notebook, we train Treeffuser on synthethic data and then visualize both the original and model-generated samples to explore how well Treeffuser captures the underlying distribution of the data."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Getting started"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We first install `treeffuser` and import the relevant libraries."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "!pip install treeffuser\n",
+    "\n",
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "\n",
+    "from treeffuser import Treeffuser"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We simulate a non-linear, bimodal response of $y$ given $x$, where the two modes follow two different response functions: one is a sine function and the other is a cosine function over $x$."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "seed = 0  # fixing the random seed for reproducibility\n",
+    "n = 5000  # number of data points\n",
+    "\n",
+    "rng = np.random.default_rng(seed=seed)\n",
+    "x = rng.uniform(0, 2 * np.pi, size=n)  # x values in the range [0, 2π)\n",
+    "z = rng.integers(0, 2, size=n)  # response function assignments\n",
+    "\n",
+    "y = z * np.sin(x - np.pi / 2) + (1 - z) * np.cos(x)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We also introduce heteroscedastic, fat-tailed noise from a Laplace distribution, meaning the variability of $y$ increases with $x$ and may result in large outliers."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "y += rng.laplace(scale=x / 30, size=n)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Fitting Treffuser and producing samples"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Fitting Treeffuser and generating samples is very simple, as Treeffuser adheres to the `sklearn.base.BaseEstimator` class. Fitting amounts to initializing the model and calling the `fit` method, just like any `scikit-learn` estimator. Samples are then generated using the `sample` method."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model = Treeffuser(sde_initialize_from_data=True, seed=seed)\n",
+    "model.fit(x, y)\n",
+    "\n",
+    "y_samples = model.sample(x, n_samples=1, seed=seed, verbose=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Plotting the samples"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We create a scatter plot to visualize both the original data and the samples produced by Treeffuser. The samples closely reflect the underlying response distributions that generated the data."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "plt.scatter(x, y, s=1, label=\"observed data\")\n",
+    "plt.scatter(x, y_samples[0, :], s=1, alpha=0.7, label=\"Treeffuser samples\")\n",
+    "\n",
+    "plt.xlabel(\"$x$\")\n",
+    "plt.ylabel(\"$y$\")\n",
+    "\n",
+    "legend = plt.legend(loc=\"upper center\", scatterpoints=1, bbox_to_anchor=(0.5, -0.125), ncol=2)\n",
+    "for legend_handle in legend.legend_handles:\n",
+    "    legend_handle.set_sizes([32])  # change marker size for legend\n",
+    "\n",
+    "plt.tight_layout()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The samples generated by Treeffuser can be used to compute any downstream estimates of interest."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "x = np.array(np.pi).reshape((1, 1))\n",
+    "y_samples = model.sample(x, n_samples=100, verbose=True)  # y_samples.shape[0] is 100\n",
+    "\n",
+    "# Estimate downstream quantities of interest\n",
+    "y_mean = y_samples.mean(axis=0)  # conditional mean for each x\n",
+    "y_std = y_samples.std(axis=0)  # conditional std for each x\n",
+    "\n",
+    "print(f\"Mean of the samples: {y_mean}\")\n",
+    "print(f\"Standard deviation of the samples: {y_std} \")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "For convenience, we also provide a class Samples that can estimate standard quantities."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from treeffuser.samples import Samples\n",
+    "\n",
+    "y_samples = Samples(y_samples)\n",
+    "y_mean = y_samples.sample_mean()  # same as before\n",
+    "y_std = y_samples.sample_std()  # same as before\n",
+    "y_quantiles = y_samples.sample_quantile(q=[0.05, 0.95])  # conditional quantiles for each x\n",
+    "\n",
+    "print(f\"Mean of the samples: {y_mean}\")\n",
+    "print(f\"Standard deviation of the samples: {y_std} \")\n",
+    "print(f\"5th and 95th quantiles of the samples: {y_quantiles.reshape(-1)}\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": ".venv",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/examples/m5-news-vendor-notebook.ipynb b/examples/m5-news-vendor-notebook.ipynb
new file mode 100644
index 00000000..e1f8fecc
--- /dev/null
+++ b/examples/m5-news-vendor-notebook.ipynb
@@ -0,0 +1,1754 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Forecasting Walmart sales with Treeffuser\n",
+    "\n",
+    "In this tutorial we show how to use Treeffuser to model and forecast Walmart sales using the M5 forecasting dataset from Kaggle."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Getting started\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "To get started, we first install `treeffuser` and import the relevant libraries (if needed)."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "ExecuteTime": {
+     "end_time": "2024-10-10T19:30:25.237555Z",
+     "start_time": "2024-10-10T19:30:23.907554Z"
+    }
+   },
+   "source": [
+    "!pip install treeffuser"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "import pandas as pd\n",
+    "\n",
+    "from pathlib import Path\n",
+    "\n",
+    "from tqdm import tqdm\n",
+    "from treeffuser import Treeffuser\n",
+    "\n",
+    "# load autoreload extension\n",
+    "%load_ext autoreload\n",
+    "%autoreload 2"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Next, create a Kaggle account and download the data from https://www.kaggle.com/competitions/m5-forecasting-accuracy/data.\n",
+    "\n",
+    "If you're running this notebook in Colab, manually upload the necessary files (`calendar.csv`, `sales_train_validation.csv`, `sell_prices.csv`) to Colab by clicking the `Files` tab on the left sidebar and selecting `Upload`. Move the files into a new folder named `m5`. Once uploaded, the notebook will be able to read and process the data.\n",
+    "\n",
+    "If you're running this on your local machine, you can also use Kaggle's [command-line tool](https://www.kaggle.com/docs/api) and run the following from the command line:\n",
+    "\n",
+    "```bash\n",
+    "cd ./m5 # path to folder where you want to save the data\n",
+    "kaggle competitions download -c m5-forecasting-accuracy\n",
+    "```\n",
+    "\n",
+    "Use your favorite tool to unzip the archive. In Linux/macOS,\n",
+    "\n",
+    "```bash\n",
+    "unzip m5-forecasting-accuracy.zip\n",
+    "```\n",
+    "\n",
+    "We'll be using the following files: `calendar.csv`, `sales_train_validation.csv`, and `sell_prices.csv`.\n",
+    "\n",
+    "\n",
+    "<!-- - `calendar.csv` - Contains information about the dates on which the products are sold.\n",
+    "- `sales_train_validation.csv` - Contains the historical daily unit sales data per product and store `[d_1 - d_1913]`.\n",
+    "- `sell_prices.csv` - Contains information about the price of the products sold per store and date. -->\n",
+    "<!-- - `sales_train_evaluation.csv`- Includes sales [`d_1 - d_1941]` (labels used for the Public leaderboard). -->\n",
+    "<!-- - `sample_submission.csv` - The correct format for submissions. Reference the Evaluation tab for more info. -->"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Load the data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "data_path = Path(\"./m5\")  # change with path where you extracted the data archive\n",
+    "\n",
+    "calendar_df = pd.read_csv(data_path / \"calendar.csv\")\n",
+    "sales_train_df = pd.read_csv(data_path / \"sales_train_validation.csv\")\n",
+    "sell_prices_df = pd.read_csv(data_path / \"sell_prices.csv\")\n",
+    "\n",
+    "# add explicit columns for the day, month, year for ease of processing\n",
+    "calendar_df[\"date\"] = pd.to_datetime(calendar_df[\"date\"])\n",
+    "calendar_df[\"day\"] = calendar_df[\"date\"].dt.day\n",
+    "calendar_df[\"month\"] = calendar_df[\"date\"].dt.month\n",
+    "calendar_df[\"year\"] = calendar_df[\"date\"].dt.year"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## The data\n",
+    "\n",
+    "### Preprocessing\n",
+    "`sell_prices_df` contains the prices of each item in each store at a given time. The `wm_yr_wk` is a unique identifier for the time."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>store_id</th>\n",
+       "      <th>item_id</th>\n",
+       "      <th>wm_yr_wk</th>\n",
+       "      <th>sell_price</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>11325</td>\n",
+       "      <td>9.58</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>11326</td>\n",
+       "      <td>9.58</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>11327</td>\n",
+       "      <td>8.26</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>11328</td>\n",
+       "      <td>8.26</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>11329</td>\n",
+       "      <td>8.26</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  store_id        item_id  wm_yr_wk  sell_price\n",
+       "0     CA_1  HOBBIES_1_001     11325        9.58\n",
+       "1     CA_1  HOBBIES_1_001     11326        9.58\n",
+       "2     CA_1  HOBBIES_1_001     11327        8.26\n",
+       "3     CA_1  HOBBIES_1_001     11328        8.26\n",
+       "4     CA_1  HOBBIES_1_001     11329        8.26"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sell_prices_df.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "`calendar_df` contains information about the dates on which the products were sold."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>date</th>\n",
+       "      <th>wm_yr_wk</th>\n",
+       "      <th>weekday</th>\n",
+       "      <th>wday</th>\n",
+       "      <th>month</th>\n",
+       "      <th>year</th>\n",
+       "      <th>d</th>\n",
+       "      <th>event_name_1</th>\n",
+       "      <th>event_type_1</th>\n",
+       "      <th>event_name_2</th>\n",
+       "      <th>event_type_2</th>\n",
+       "      <th>snap_CA</th>\n",
+       "      <th>snap_TX</th>\n",
+       "      <th>snap_WI</th>\n",
+       "      <th>day</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>2011-01-29</td>\n",
+       "      <td>11101</td>\n",
+       "      <td>Saturday</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>d_1</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>29</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>2011-01-30</td>\n",
+       "      <td>11101</td>\n",
+       "      <td>Sunday</td>\n",
+       "      <td>2</td>\n",
+       "      <td>1</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>d_2</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>30</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>2011-01-31</td>\n",
+       "      <td>11101</td>\n",
+       "      <td>Monday</td>\n",
+       "      <td>3</td>\n",
+       "      <td>1</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>d_3</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>31</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>2011-02-01</td>\n",
+       "      <td>11101</td>\n",
+       "      <td>Tuesday</td>\n",
+       "      <td>4</td>\n",
+       "      <td>2</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>d_4</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>2011-02-02</td>\n",
+       "      <td>11101</td>\n",
+       "      <td>Wednesday</td>\n",
+       "      <td>5</td>\n",
+       "      <td>2</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>d_5</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>2</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "        date  wm_yr_wk    weekday  wday  month  year    d event_name_1  \\\n",
+       "0 2011-01-29     11101   Saturday     1      1  2011  d_1          NaN   \n",
+       "1 2011-01-30     11101     Sunday     2      1  2011  d_2          NaN   \n",
+       "2 2011-01-31     11101     Monday     3      1  2011  d_3          NaN   \n",
+       "3 2011-02-01     11101    Tuesday     4      2  2011  d_4          NaN   \n",
+       "4 2011-02-02     11101  Wednesday     5      2  2011  d_5          NaN   \n",
+       "\n",
+       "  event_type_1 event_name_2 event_type_2  snap_CA  snap_TX  snap_WI  day  \n",
+       "0          NaN          NaN          NaN        0        0        0   29  \n",
+       "1          NaN          NaN          NaN        0        0        0   30  \n",
+       "2          NaN          NaN          NaN        0        0        0   31  \n",
+       "3          NaN          NaN          NaN        1        1        0    1  \n",
+       "4          NaN          NaN          NaN        1        0        1    2  "
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "calendar_df.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "`sales_train_df` contains the number of units sold for an item in each department and store. The sales are grouped by day: for example, the `d_1907` column has the number of units sold on the 1907-th day."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>id</th>\n",
+       "      <th>item_id</th>\n",
+       "      <th>dept_id</th>\n",
+       "      <th>cat_id</th>\n",
+       "      <th>store_id</th>\n",
+       "      <th>state_id</th>\n",
+       "      <th>d_1</th>\n",
+       "      <th>d_2</th>\n",
+       "      <th>d_3</th>\n",
+       "      <th>d_4</th>\n",
+       "      <th>...</th>\n",
+       "      <th>d_1904</th>\n",
+       "      <th>d_1905</th>\n",
+       "      <th>d_1906</th>\n",
+       "      <th>d_1907</th>\n",
+       "      <th>d_1908</th>\n",
+       "      <th>d_1909</th>\n",
+       "      <th>d_1910</th>\n",
+       "      <th>d_1911</th>\n",
+       "      <th>d_1912</th>\n",
+       "      <th>d_1913</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>HOBBIES_1_001_CA_1_validation</td>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>1</td>\n",
+       "      <td>3</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>3</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>HOBBIES_1_002_CA_1_validation</td>\n",
+       "      <td>HOBBIES_1_002</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>HOBBIES_1_003_CA_1_validation</td>\n",
+       "      <td>HOBBIES_1_003</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>2</td>\n",
+       "      <td>1</td>\n",
+       "      <td>2</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>HOBBIES_1_004_CA_1_validation</td>\n",
+       "      <td>HOBBIES_1_004</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>5</td>\n",
+       "      <td>4</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>3</td>\n",
+       "      <td>7</td>\n",
+       "      <td>2</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>HOBBIES_1_005_CA_1_validation</td>\n",
+       "      <td>HOBBIES_1_005</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>2</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>2</td>\n",
+       "      <td>2</td>\n",
+       "      <td>2</td>\n",
+       "      <td>4</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>5 rows × 1919 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                              id        item_id    dept_id   cat_id store_id  \\\n",
+       "0  HOBBIES_1_001_CA_1_validation  HOBBIES_1_001  HOBBIES_1  HOBBIES     CA_1   \n",
+       "1  HOBBIES_1_002_CA_1_validation  HOBBIES_1_002  HOBBIES_1  HOBBIES     CA_1   \n",
+       "2  HOBBIES_1_003_CA_1_validation  HOBBIES_1_003  HOBBIES_1  HOBBIES     CA_1   \n",
+       "3  HOBBIES_1_004_CA_1_validation  HOBBIES_1_004  HOBBIES_1  HOBBIES     CA_1   \n",
+       "4  HOBBIES_1_005_CA_1_validation  HOBBIES_1_005  HOBBIES_1  HOBBIES     CA_1   \n",
+       "\n",
+       "  state_id  d_1  d_2  d_3  d_4  ...  d_1904  d_1905  d_1906  d_1907  d_1908  \\\n",
+       "0       CA    0    0    0    0  ...       1       3       0       1       1   \n",
+       "1       CA    0    0    0    0  ...       0       0       0       0       0   \n",
+       "2       CA    0    0    0    0  ...       2       1       2       1       1   \n",
+       "3       CA    0    0    0    0  ...       1       0       5       4       1   \n",
+       "4       CA    0    0    0    0  ...       2       1       1       0       1   \n",
+       "\n",
+       "   d_1909  d_1910  d_1911  d_1912  d_1913  \n",
+       "0       1       3       0       1       1  \n",
+       "1       1       0       0       0       0  \n",
+       "2       1       0       1       1       1  \n",
+       "3       0       1       3       7       2  \n",
+       "4       1       2       2       2       4  \n",
+       "\n",
+       "[5 rows x 1919 columns]"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "sales_train_df.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "To align the sales data with the other DataFrames, we convert `sales_train_df` to a long format. We collapse the daily sales columns `d_{i}` into a single `sales` column, with an  additional `day` column indicating the day corresponding to each sales entry."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>item_id</th>\n",
+       "      <th>dept_id</th>\n",
+       "      <th>cat_id</th>\n",
+       "      <th>store_id</th>\n",
+       "      <th>state_id</th>\n",
+       "      <th>d</th>\n",
+       "      <th>sales</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_1</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_2</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_3</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_4</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>HOBBIES_1_001</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_5</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "         item_id    dept_id   cat_id store_id state_id    d  sales\n",
+       "0  HOBBIES_1_001  HOBBIES_1  HOBBIES     CA_1       CA  d_1      0\n",
+       "1  HOBBIES_1_001  HOBBIES_1  HOBBIES     CA_1       CA  d_2      0\n",
+       "2  HOBBIES_1_001  HOBBIES_1  HOBBIES     CA_1       CA  d_3      0\n",
+       "3  HOBBIES_1_001  HOBBIES_1  HOBBIES     CA_1       CA  d_4      0\n",
+       "4  HOBBIES_1_001  HOBBIES_1  HOBBIES     CA_1       CA  d_5      0"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "def convert_sales_data_from_wide_to_long(sales_df_wide):\n",
+    "    index_vars = [\"item_id\", \"dept_id\", \"cat_id\", \"store_id\", \"state_id\"]\n",
+    "    sales_df_long = pd.wide_to_long(\n",
+    "        sales_df_wide.iloc[:100, 1:],\n",
+    "        i=index_vars,\n",
+    "        j=\"day\",\n",
+    "        stubnames=[\"d\"],\n",
+    "        sep=\"_\",\n",
+    "    ).reset_index()\n",
+    "\n",
+    "    sales_df_long = sales_df_long.rename(columns={\"d\": \"sales\", \"day\": \"d\"})\n",
+    "\n",
+    "    sales_df_long[\"d\"] = \"d_\" + sales_df_long[\"d\"].astype(\n",
+    "        \"str\"\n",
+    "    )  # restore \"d_{i}\" format for day\n",
+    "    return sales_df_long\n",
+    "\n",
+    "\n",
+    "sales_train_df_long = convert_sales_data_from_wide_to_long(sales_train_df)\n",
+    "sales_train_df_long.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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RX3yyScaM2aM6tSpo+7duxc67dixY5WYmGi/n5OToxo1ahQ5LwAAKNmKdJ2ba26m1FSqVEk+Pj5OW2mysrKctub8XFRUlCSpSZMmOn36tCZOnHjdchMQEKCAgIAi5wQAALcWywcUu4u/v7+io6Odro2Tnp6uuLg4l5djjHE4pgYAAPy23dSWm5uVmJioXr16KSYmRrGxsUpJSVFGRoYGDRok6eoupRMnTmjx4sWSpFdffVU1a9ZU/fr1JV297s1LL72kp556ymPrAAAAShaPlptu3brp7Nmzmjx5sjIzM9W4cWOlpaUpIiJCkpSZmelwzZuCggKNHTtWR44cka+vr2rXrq1p06bp8ccf99QqAACAEsZmjDFWZjhy5Ij9mJdbUU5OjoKDg5Wdna3y5cu7ffmRY953+zLd7ei0zp6OAACAJVb+fls+5qZOnTpq27atlixZosuXLxc5JAAAQHGwXG727t2rFi1aaMSIEapSpYoef/xxffrpp8WRDQAAwDLL5aZx48aaMWOGTpw4oUWLFunUqVO6++671ahRI82YMUPffvttceQEAABwSZFPBff19dXDDz+st99+Wy+88IK+/vprjRw5UtWrV1fv3r2VmZnpzpwAAAAuKXK52bFjh5588kmFh4drxowZGjlypL7++mutW7dOJ06c0IMPPujOnAAAAC6xfCr4jBkztGjRIh08eFCdOnXS4sWL1alTJ5UqdbUnRUVFad68efZr0QAAAPyaLJebOXPmqH///urXr5+qVKlS6DQ1a9bUggULbjocAACAVZbLzZdffvmL0/j7+6tPnz5FCgQAAHAzLB9zs2jRIi1btsxpfNmyZXr99dfdEgoAAKCoLJebadOmqVKlSk7jlStX1tSpU90SCgAAoKgsl5tjx44V+vULERERDt8DBQAA4AmWy03lypW1b98+p/G9e/cqJCTELaEAAACKynK5eeyxx/T0009r/fr1ys/PV35+vtatW6ehQ4fqscceK46MAAAALrN8ttSUKVN07NgxtWvXTr6+V2cvKChQ7969OeYGAAB4nOVy4+/vr9TUVD333HPau3evgoKC1KRJE0VERBRHPgAAAEssl5trbr/9dt1+++3uzAIAAHDTLJeb/Px8vfbaa1q7dq2ysrJUUFDg8Pi6devcFg4AAMAqy+Vm6NCheu2119S5c2c1btxYNputOHIBAAAUieVy89Zbb+ntt99Wp06diiMPAADATbF8Kri/v7/q1KlTHFkAAABumuVyM2LECM2aNUvGmOLIAwAAcFMs75bavHmz1q9frw8++ECNGjWSn5+fw+MrV650WzgAAACrLJebChUq6OGHHy6OLAAAADfNcrlZtGhRceQAAABwC8vH3EhSXl6ePvzwQ82bN08XLlyQJJ08eVIXL150azgAAACrLG+5OXbsmO6//35lZGQoNzdX7du3V7ly5TR9+nRdvnxZc+fOLY6cAAAALrG85Wbo0KGKiYnRd999p6CgIPv4ww8/rLVr17o1HAAAgFVFOlvq448/lr+/v8N4RESETpw44bZgAAAARWF5y01BQYHy8/Odxr/55huVK1fOLaEAAACKynK5ad++vWbOnGm/b7PZdPHiRU2YMIGvZAAAAB5nebfUK6+8orZt26phw4a6fPmyevTooS+//FKVKlXS0qVLiyMjAACAyyyXm6pVq2rPnj1aunSpdu3apYKCAg0YMEB/+tOfHA4wBgAA8ATL5UaSgoKC1L9/f/Xv39/deQAAAG6K5XKzePHiGz7eu3fvIocBAAC4WZbLzdChQx3u//jjj/r+++/l7++v0qVLU24AAIBHWT5b6rvvvnO4Xbx4UQcPHtTdd9/NAcUAAMDjivTdUj9Xt25dTZs2zWmrDgAAwK/NLeVGknx8fHTy5El3LQ4AAKBILB9z88477zjcN8YoMzNTs2fPVnx8vNuCAQAAFIXlcvPQQw853LfZbAoNDdW9996rl19+2V25AAAAisRyuSkoKCiOHAAAAG7htmNuAAAASgLLW24SExNdnnbGjBm/OE1ycrJefPFFZWZmqlGjRpo5c6ZatWpV6LQrV67UnDlztGfPHuXm5qpRo0aaOHGiOnTo4HImAADg3SyXm927d2vXrl3Ky8tTvXr1JEmHDh2Sj4+PWrZsaZ/OZrP94rJSU1M1bNgwJScnKz4+XvPmzVPHjh31xRdfqGbNmk7Tb9y4Ue3bt9fUqVNVoUIFLVq0SF26dNG2bdvUokULq6sCAAC8kM0YY6zMMGPGDH300Ud6/fXXddttt0m6emG/fv36qVWrVhoxYoTLy7rzzjvVsmVLzZkzxz7WoEEDPfTQQ0pKSnJpGY0aNVK3bt307LPPujR9Tk6OgoODlZ2drfLly7uc1VWRY953+zLd7ei0zp6OAACAJVb+fls+5ubll19WUlKSvdhI0m233aYpU6ZYOlvqypUr2rlzpxISEhzGExIStGXLFpeWUVBQoAsXLqhixYrXnSY3N1c5OTkONwAA4L0sl5ucnBydPn3aaTwrK0sXLlxweTlnzpxRfn6+wsLCHMbDwsJ06tQpl5bx8ssv69KlS3r00UevO01SUpKCg4Pttxo1aricEQAA3Hosl5uHH35Y/fr10/Lly/XNN9/om2++0fLlyzVgwAB17drVcoCfH5tjjHHpeJ2lS5dq4sSJSk1NVeXKla873dixY5WdnW2/HT9+3HJGAABw67B8QPHcuXM1cuRI9ezZUz/++OPVhfj6asCAAXrxxRddXk6lSpXk4+PjtJUmKyvLaWvOz6WmpmrAgAFatmyZ7rvvvhtOGxAQoICAAJdzAQCAW5vlLTelS5dWcnKyzp49az9z6ty5c0pOTlaZMmVcXo6/v7+io6OVnp7uMJ6enq64uLjrzrd06VL17dtXb775pjp35sBYAADgyPKWm2syMzOVmZmp1q1bKygoyOXdST+VmJioXr16KSYmRrGxsUpJSVFGRoYGDRok6eoupRMnTmjx4sWSrhab3r17a9asWbrrrrvsW32CgoIUHBxc1FUBAABexPKWm7Nnz6pdu3a6/fbb1alTJ2VmZkqSBg4caOk0cEnq1q2bZs6cqcmTJ6t58+bauHGj0tLSFBERIelqgcrIyLBPP2/ePOXl5Wnw4MEKDw+334YOHWp1NQAAgJeyfJ2b3r17KysrS/Pnz1eDBg20d+9e1apVS2vWrNHw4cO1f//+4srqFlznhuvcAABuPVb+flveLbVmzRqtXr1a1atXdxivW7eujh07ZnVxAAAAbmV5t9SlS5dUunRpp/EzZ85wVhIAAPA4y+WmdevW9gN8pavXqSkoKNCLL76otm3bujUcAACAVZZ3S7344ou65557tGPHDl25ckWjRo3S/v37de7cOX388cfFkREAAMBllrfcNGzYUPv27dMdd9yh9u3b69KlS+ratat2796t2rVrF0dGAAAAl1nacvPjjz8qISFB8+bN06RJk4orEwAAQJFZ2nLj5+enzz//3PLF+gAAAH4tlndL9e7dWwsWLCiOLAAAADfN8gHFV65c0fz585Wenq6YmBin75OaMWOG28IBAABYZbncfP7552rZsqUk6dChQw6PsbsK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qnHjxkpLS1NERIQkKTMz0+GaN1FRUUpLS9Pw4cP16quvqmrVqvr73/+uP/zhD55ahWIVEBCgCRMmOO1WK0nI6B5kdA8yugcZ3eNWyOitPHqdGwAAAHfju6UAAIBXodwAAACvQrkBAABehXIDAAC8CuUGAAB4FcpNCZacnKyoqCgFBgYqOjpamzZt8nQkBxs3blSXLl1UtWpV2Ww2/ec///F0JAdJSUn63e9+p3Llyqly5cp66KGHdPDgQU/HcjBnzhw1bdrUfgXT2NhYffDBB56OdUNJSUmy2WwaNmyYp6PYTZw4UTabzeFWpUoVT8dycuLECfXs2VMhISEqXbq0mjdvrp07d3o6ll1kZKTT62iz2TR48GBPR7PLy8vT+PHjFRUVpaCgINWqVUuTJ09WQUGBp6M5uHDhgoYNG6aIiAgFBQUpLi5O27dv93Ss3wzKTQmVmpqqYcOGady4cdq9e7datWqljh07Olz3x9MuXbqkZs2aafbs2Z6OUqgNGzZo8ODB+uSTT5Senq68vDwlJCTo0qVLno5mV716dU2bNk07duzQjh07dO+99+rBBx/U/v37PR2tUNu3b1dKSoqaNm3q6ShOGjVqpMzMTPvts88+83QkB999953i4+Pl5+enDz74QF988YVefvnlEvV1MNu3b3d4Da9dEf6Pf/yjh5P9vxdeeEFz587V7NmzdeDAAU2fPl0vvvii/vGPf3g6moOBAwcqPT1db7zxhj777DMlJCTovvvu04kTJzwd7bfBoES64447zKBBgxzG6tevb8aMGeOhRDcmyfz73//2dIwbysrKMpLMhg0bPB3lhm677TYzf/58T8dwcuHCBVO3bl2Tnp5u2rRpY4YOHerpSHYTJkwwzZo183SMGxo9erS5++67PR3DkqFDh5ratWubgoICT0ex69y5s+nfv7/DWNeuXU3Pnj09lMjZ999/b3x8fMx7773nMN6sWTMzbtw4D6X6bWHLTQl05coV7dy5UwkJCQ7jCQkJ2rJli4dS3fqys7MlSRUrVvRwksLl5+frrbfe0qVLlxQbG+vpOE4GDx6szp0767777vN0lEJ9+eWXqlq1qqKiovTYY4/p8OHDno7k4J133lFMTIz++Mc/qnLlymrRooX++c9/ejrWdV25ckVLlixR//79ZbPZPB3H7u6779batWt16NAhSdLevXu1efNmderUycPJ/l9eXp7y8/MVGBjoMB4UFKTNmzd7KNVvi0e/fgGFO3PmjPLz852+HT0sLMzpW9HhGmOMEhMTdffdd6tx48aejuPgs88+U2xsrC5fvqyyZcvq3//+txo2bOjpWA7eeust7dq1q8QeM3DnnXdq8eLFuv3223X69GlNmTJFcXFx2r9/v0JCQjwdT5J0+PBhzZkzR4mJifrrX/+qTz/9VE8//bQCAgIcvkOvpPjPf/6j8+fPq2/fvp6O4mD06NHKzs5W/fr15ePjo/z8fD3//PPq3r27p6PZlStXTrGxsXruuefUoEEDhYWFaenSpdq2bZvq1q3r6Xi/CZSbEuzn/1syxpSo/0HdSoYMGaJ9+/aVyP811atXT3v27NH58+e1YsUK9enTRxs2bCgxBef48eMaOnSo1qxZ4/Q/0ZKiY8eO9n83adJEsbGxql27tl5//XUlJiZ6MNn/KygoUExMjKZOnSpJatGihfbv3685c+aUyHKzYMECdezYUVWrVvV0FAepqalasmSJ3nzzTTVq1Eh79uzRsGHDVLVqVfXp08fT8ezeeOMN9e/fX9WqVZOPj49atmypHj16aNeuXZ6O9ptAuSmBKlWqJB8fH6etNFlZWU5bc/DLnnrqKb3zzjvauHGjqlev7uk4Tvz9/VWnTh1JUkxMjLZv365Zs2Zp3rx5Hk521c6dO5WVlaXo6Gj7WH5+vjZu3KjZs2crNzdXPj4+HkzorEyZMmrSpIm+/PJLT0exCw8PdyqsDRo00IoVKzyU6PqOHTumDz/8UCtXrvR0FCfPPPOMxowZo8cee0zS1TJ77NgxJSUllahyU7t2bW3YsEGXLl1STk6OwsPD1a1bN0VFRXk62m8Cx9yUQP7+/oqOjrafqXBNenq64uLiPJTq1mOM0ZAhQ7Ry5UqtW7fulvlQMcYoNzfX0zHs2rVrp88++0x79uyx32JiYvSnP/1Je/bsKXHFRpJyc3N14MABhYeHezqKXXx8vNOlCA4dOqSIiAgPJbq+RYsWqXLlyurcubOnozj5/vvvVaqU458uHx+fEncq+DVlypRReHi4vvvuO61evVoPPvigpyP9JrDlpoRKTExUr169FBMTo9jYWKWkpCgjI0ODBg3ydDS7ixcv6quvvrLfP3LkiPbs2aOKFSuqZs2aHkx21eDBg/Xmm2/qv//9r8qVK2ffEhYcHKygoCAPp7vqr3/9qzp27KgaNWrowoULeuutt/TRRx9p1apVno5mV65cOafjlMqUKaOQkJASc/zSyJEj1aVLF9WsWVNZWVmaMmWKcnJyStT/5IcPH664uDhNnTpVjz76qD799FOlpKQoJSXF09EcFBQUaNGiRerTp498fUven4guXbro+eefV82aNdWoUSPt3r1bM2bMUP/+/T0dzcHq1atljFG9evX01Vdf6ZlnnlG9evXUr18/T0f7bfDouVq4oVdffdVEREQYf39/07JlyxJ3CvP69euNJKdbnz59PB3NGGMKzSbJLFq0yNPR7Pr372//GYeGhpp27dqZNWvWeDrWLyppp4J369bNhIeHGz8/P1O1alXTtWtXs3//fk/HcvLuu++axo0bm4CAAFO/fn2TkpLi6UhOVq9ebSSZgwcPejpKoXJycszQoUNNzZo1TWBgoKlVq5YZN26cyc3N9XQ0B6mpqaZWrVrG39/fVKlSxQwePNicP3/e07F+M2zGGOOZWgUAAOB+HHMDAAC8CuUGAAB4FcoNAADwKpQbAADgVSg3AADAq1BuAACAV6HcAAAAr0K5AQAAXoVyAwAAvArlBgAAeBXKDQAA8Cr/B3gUTWNt77b4AAAAAElFTkSuQmCC",
+      "text/plain": [
+       "<Figure size 640x480 with 1 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "plt.hist(\n",
+    "    sales_train_df_long[\"sales\"],\n",
+    "    bins=np.arange(0, 10 + 1.5) - 0.5,\n",
+    "    range=[0, 10],\n",
+    "    density=True,\n",
+    ")\n",
+    "plt.xticks(range(10))\n",
+    "plt.ylabel(\"Frequency of number of sales\")\n",
+    "plt.title(\"Number of sales over the entire timespan\");"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Train and test sets"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The dataset comprises sales data of 100 items over 1,913 days. For simplicity, we select the data from the first 365 days and discard the rest."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "n_items = 100\n",
+      "n_days = 1913\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(f\"n_items = {len(sales_train_df_long['item_id'].unique())}\")\n",
+    "print(f\"n_days = {len(sales_train_df_long['d'].unique())}\")\n",
+    "\n",
+    "sales_train_df_long[\"day_number\"] = sales_train_df_long[\"d\"].str.extract(\"(\\d+)\").astype(int)\n",
+    "data = sales_train_df_long[sales_train_df_long[\"day_number\"] <= 365].copy()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We compute the lags of the previous 30 days and merge the sales, calendar, and price data together."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>item_id</th>\n",
+       "      <th>dept_id</th>\n",
+       "      <th>cat_id</th>\n",
+       "      <th>store_id</th>\n",
+       "      <th>state_id</th>\n",
+       "      <th>d</th>\n",
+       "      <th>sales</th>\n",
+       "      <th>day_number</th>\n",
+       "      <th>sales_lag_1</th>\n",
+       "      <th>sales_lag_2</th>\n",
+       "      <th>...</th>\n",
+       "      <th>year</th>\n",
+       "      <th>event_name_1</th>\n",
+       "      <th>event_type_1</th>\n",
+       "      <th>event_name_2</th>\n",
+       "      <th>event_type_2</th>\n",
+       "      <th>snap_CA</th>\n",
+       "      <th>snap_TX</th>\n",
+       "      <th>snap_WI</th>\n",
+       "      <th>day</th>\n",
+       "      <th>sell_price</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>HOBBIES_1_002</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_141</td>\n",
+       "      <td>0</td>\n",
+       "      <td>141</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>18</td>\n",
+       "      <td>3.97</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>HOBBIES_1_002</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_142</td>\n",
+       "      <td>0</td>\n",
+       "      <td>142</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>Father's day</td>\n",
+       "      <td>Cultural</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>19</td>\n",
+       "      <td>3.97</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>HOBBIES_1_002</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_143</td>\n",
+       "      <td>0</td>\n",
+       "      <td>143</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>20</td>\n",
+       "      <td>3.97</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>HOBBIES_1_002</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_144</td>\n",
+       "      <td>1</td>\n",
+       "      <td>144</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>21</td>\n",
+       "      <td>3.97</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>HOBBIES_1_002</td>\n",
+       "      <td>HOBBIES_1</td>\n",
+       "      <td>HOBBIES</td>\n",
+       "      <td>CA_1</td>\n",
+       "      <td>CA</td>\n",
+       "      <td>d_145</td>\n",
+       "      <td>0</td>\n",
+       "      <td>145</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>...</td>\n",
+       "      <td>2011</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>NaN</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>22</td>\n",
+       "      <td>3.97</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>5 rows × 53 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "         item_id    dept_id   cat_id store_id state_id      d  sales  \\\n",
+       "0  HOBBIES_1_002  HOBBIES_1  HOBBIES     CA_1       CA  d_141      0   \n",
+       "1  HOBBIES_1_002  HOBBIES_1  HOBBIES     CA_1       CA  d_142      0   \n",
+       "2  HOBBIES_1_002  HOBBIES_1  HOBBIES     CA_1       CA  d_143      0   \n",
+       "3  HOBBIES_1_002  HOBBIES_1  HOBBIES     CA_1       CA  d_144      1   \n",
+       "4  HOBBIES_1_002  HOBBIES_1  HOBBIES     CA_1       CA  d_145      0   \n",
+       "\n",
+       "   day_number  sales_lag_1  sales_lag_2  ...  year  event_name_1  \\\n",
+       "0         141          0.0          0.0  ...  2011           NaN   \n",
+       "1         142          0.0          0.0  ...  2011  Father's day   \n",
+       "2         143          0.0          0.0  ...  2011           NaN   \n",
+       "3         144          0.0          0.0  ...  2011           NaN   \n",
+       "4         145          1.0          0.0  ...  2011           NaN   \n",
+       "\n",
+       "   event_type_1  event_name_2  event_type_2  snap_CA  snap_TX  snap_WI  day  \\\n",
+       "0           NaN           NaN           NaN        0        0        0   18   \n",
+       "1      Cultural           NaN           NaN        0        0        0   19   \n",
+       "2           NaN           NaN           NaN        0        0        0   20   \n",
+       "3           NaN           NaN           NaN        0        0        0   21   \n",
+       "4           NaN           NaN           NaN        0        0        0   22   \n",
+       "\n",
+       "   sell_price  \n",
+       "0        3.97  \n",
+       "1        3.97  \n",
+       "2        3.97  \n",
+       "3        3.97  \n",
+       "4        3.97  \n",
+       "\n",
+       "[5 rows x 53 columns]"
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "n_lags = 30\n",
+    "\n",
+    "# sort data before computing lags\n",
+    "data_index_vars = [\"item_id\", \"dept_id\", \"cat_id\", \"store_id\", \"state_id\"]\n",
+    "data.sort_values(data_index_vars + [\"day_number\"], inplace=True)\n",
+    "\n",
+    "for lag in range(1, n_lags + 1):\n",
+    "    data[f\"sales_lag_{lag}\"] = data.groupby(by=data_index_vars)[\"sales\"].shift(lag)\n",
+    "\n",
+    "data = data.merge(calendar_df).merge(sell_prices_df)\n",
+    "\n",
+    "data.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Treeffuser can handle **categorical columns**, but the dtype of those columns must be set to `category` in the DataFrame."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "categorical_columns = [\n",
+    "    \"item_id\",\n",
+    "    \"dept_id\",\n",
+    "    \"cat_id\",\n",
+    "    \"store_id\",\n",
+    "    \"state_id\",\n",
+    "    \"d\",\n",
+    "    \"wm_yr_wk\",\n",
+    "    \"weekday\",\n",
+    "    \"event_name_1\",\n",
+    "    \"event_type_1\",\n",
+    "    \"event_name_2\",\n",
+    "    \"event_type_2\",\n",
+    "    \"snap_CA\",\n",
+    "    \"snap_TX\",\n",
+    "    \"snap_WI\",\n",
+    "]\n",
+    "data[categorical_columns] = data[categorical_columns].astype(\"category\")\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Finally, for each item, we take the first 300 days as train data and use the remaining 65 data as test data for evaluation."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(15216, 50)\n",
+      "(3891, 50)\n"
+     ]
+    }
+   ],
+   "source": [
+    "is_train = data[\"day_number\"] <= 300\n",
+    "\n",
+    "y_name = \"sales\"\n",
+    "x_names = [name for name in data.columns if name != y_name and name not in [\"day_number\", \"date\"]]\n",
+    "\n",
+    "X_train, y_train, dates_train = data[is_train][x_names], data[is_train][y_name], data[is_train][\"date\"]\n",
+    "X_test, y_test, dates_test = data[~is_train][x_names], data[~is_train][y_name], data[~is_train][\"date\"]\n",
+    "\n",
+    "print(X_train.shape)\n",
+    "print(X_test.shape)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Probabilistic predictions with Treeffuser\n",
+    "\n",
+    "We regress the sales on the following covariates."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "item_id, dept_id, cat_id, store_id, state_id, d, sales_lag_1, sales_lag_2, sales_lag_3, sales_lag_4, sales_lag_5, sales_lag_6, sales_lag_7, sales_lag_8, sales_lag_9, sales_lag_10, sales_lag_11, sales_lag_12, sales_lag_13, sales_lag_14, sales_lag_15, sales_lag_16, sales_lag_17, sales_lag_18, sales_lag_19, sales_lag_20, sales_lag_21, sales_lag_22, sales_lag_23, sales_lag_24, sales_lag_25, sales_lag_26, sales_lag_27, sales_lag_28, sales_lag_29, sales_lag_30, wm_yr_wk, weekday, wday, month, year, event_name_1, event_type_1, event_name_2, event_type_2, snap_CA, snap_TX, snap_WI, day, sell_price\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(\", \".join(map(str, X_train.columns)))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[LightGBM] [Warning] Met negative value in categorical features, will convert it to NaN\n",
+      "[LightGBM] [Warning] Met negative value in categorical features, will convert it to NaN\n",
+      "[LightGBM] [Warning] Met negative value in categorical features, will convert it to NaN\n",
+      "[LightGBM] [Warning] Met negative value in categorical features, will convert it to NaN\n",
+      "[LightGBM] [Warning] Categorical features with more bins than the configured maximum bin number found.\n",
+      "[LightGBM] [Warning] For categorical features, max_bin and max_bin_by_feature may be ignored with a large number of categories.\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "/Users/achille/Documents/treeffuser-ter/src/treeffuser/_base_tabular_diffusion.py:205: CastFloat32Warning: Input array is not float; it has been recast to float32.\n",
+      "  y = _check_array(y)\n"
+     ]
+    },
+    {
+     "data": {
+      "text/html": [
+       "<style>#sk-container-id-1 {\n",
+       "  /* Definition of color scheme common for light and dark mode */\n",
+       "  --sklearn-color-text: black;\n",
+       "  --sklearn-color-line: gray;\n",
+       "  /* Definition of color scheme for unfitted estimators */\n",
+       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
+       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
+       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
+       "  --sklearn-color-unfitted-level-3: chocolate;\n",
+       "  /* Definition of color scheme for fitted estimators */\n",
+       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
+       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
+       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
+       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
+       "\n",
+       "  /* Specific color for light theme */\n",
+       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
+       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
+       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
+       "  --sklearn-color-icon: #696969;\n",
+       "\n",
+       "  @media (prefers-color-scheme: dark) {\n",
+       "    /* Redefinition of color scheme for dark theme */\n",
+       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
+       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
+       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
+       "    --sklearn-color-icon: #878787;\n",
+       "  }\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 {\n",
+       "  color: var(--sklearn-color-text);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 pre {\n",
+       "  padding: 0;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 input.sk-hidden--visually {\n",
+       "  border: 0;\n",
+       "  clip: rect(1px 1px 1px 1px);\n",
+       "  clip: rect(1px, 1px, 1px, 1px);\n",
+       "  height: 1px;\n",
+       "  margin: -1px;\n",
+       "  overflow: hidden;\n",
+       "  padding: 0;\n",
+       "  position: absolute;\n",
+       "  width: 1px;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-dashed-wrapped {\n",
+       "  border: 1px dashed var(--sklearn-color-line);\n",
+       "  margin: 0 0.4em 0.5em 0.4em;\n",
+       "  box-sizing: border-box;\n",
+       "  padding-bottom: 0.4em;\n",
+       "  background-color: var(--sklearn-color-background);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-container {\n",
+       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
+       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
+       "     so we also need the `!important` here to be able to override the\n",
+       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
+       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
+       "  display: inline-block !important;\n",
+       "  position: relative;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-text-repr-fallback {\n",
+       "  display: none;\n",
+       "}\n",
+       "\n",
+       "div.sk-parallel-item,\n",
+       "div.sk-serial,\n",
+       "div.sk-item {\n",
+       "  /* draw centered vertical line to link estimators */\n",
+       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
+       "  background-size: 2px 100%;\n",
+       "  background-repeat: no-repeat;\n",
+       "  background-position: center center;\n",
+       "}\n",
+       "\n",
+       "/* Parallel-specific style estimator block */\n",
+       "\n",
+       "#sk-container-id-1 div.sk-parallel-item::after {\n",
+       "  content: \"\";\n",
+       "  width: 100%;\n",
+       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
+       "  flex-grow: 1;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-parallel {\n",
+       "  display: flex;\n",
+       "  align-items: stretch;\n",
+       "  justify-content: center;\n",
+       "  background-color: var(--sklearn-color-background);\n",
+       "  position: relative;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-parallel-item {\n",
+       "  display: flex;\n",
+       "  flex-direction: column;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-parallel-item:first-child::after {\n",
+       "  align-self: flex-end;\n",
+       "  width: 50%;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-parallel-item:last-child::after {\n",
+       "  align-self: flex-start;\n",
+       "  width: 50%;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-parallel-item:only-child::after {\n",
+       "  width: 0;\n",
+       "}\n",
+       "\n",
+       "/* Serial-specific style estimator block */\n",
+       "\n",
+       "#sk-container-id-1 div.sk-serial {\n",
+       "  display: flex;\n",
+       "  flex-direction: column;\n",
+       "  align-items: center;\n",
+       "  background-color: var(--sklearn-color-background);\n",
+       "  padding-right: 1em;\n",
+       "  padding-left: 1em;\n",
+       "}\n",
+       "\n",
+       "\n",
+       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
+       "clickable and can be expanded/collapsed.\n",
+       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
+       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
+       "*/\n",
+       "\n",
+       "/* Pipeline and ColumnTransformer style (default) */\n",
+       "\n",
+       "#sk-container-id-1 div.sk-toggleable {\n",
+       "  /* Default theme specific background. It is overwritten whether we have a\n",
+       "  specific estimator or a Pipeline/ColumnTransformer */\n",
+       "  background-color: var(--sklearn-color-background);\n",
+       "}\n",
+       "\n",
+       "/* Toggleable label */\n",
+       "#sk-container-id-1 label.sk-toggleable__label {\n",
+       "  cursor: pointer;\n",
+       "  display: block;\n",
+       "  width: 100%;\n",
+       "  margin-bottom: 0;\n",
+       "  padding: 0.5em;\n",
+       "  box-sizing: border-box;\n",
+       "  text-align: center;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n",
+       "  /* Arrow on the left of the label */\n",
+       "  content: \"▸\";\n",
+       "  float: left;\n",
+       "  margin-right: 0.25em;\n",
+       "  color: var(--sklearn-color-icon);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n",
+       "  color: var(--sklearn-color-text);\n",
+       "}\n",
+       "\n",
+       "/* Toggleable content - dropdown */\n",
+       "\n",
+       "#sk-container-id-1 div.sk-toggleable__content {\n",
+       "  max-height: 0;\n",
+       "  max-width: 0;\n",
+       "  overflow: hidden;\n",
+       "  text-align: left;\n",
+       "  /* unfitted */\n",
+       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-toggleable__content.fitted {\n",
+       "  /* fitted */\n",
+       "  background-color: var(--sklearn-color-fitted-level-0);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-toggleable__content pre {\n",
+       "  margin: 0.2em;\n",
+       "  border-radius: 0.25em;\n",
+       "  color: var(--sklearn-color-text);\n",
+       "  /* unfitted */\n",
+       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n",
+       "  /* unfitted */\n",
+       "  background-color: var(--sklearn-color-fitted-level-0);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
+       "  /* Expand drop-down */\n",
+       "  max-height: 200px;\n",
+       "  max-width: 100%;\n",
+       "  overflow: auto;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
+       "  content: \"▾\";\n",
+       "}\n",
+       "\n",
+       "/* Pipeline/ColumnTransformer-specific style */\n",
+       "\n",
+       "#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
+       "  color: var(--sklearn-color-text);\n",
+       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
+       "  background-color: var(--sklearn-color-fitted-level-2);\n",
+       "}\n",
+       "\n",
+       "/* Estimator-specific style */\n",
+       "\n",
+       "/* Colorize estimator box */\n",
+       "#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
+       "  /* unfitted */\n",
+       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
+       "  /* fitted */\n",
+       "  background-color: var(--sklearn-color-fitted-level-2);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n",
+       "#sk-container-id-1 div.sk-label label {\n",
+       "  /* The background is the default theme color */\n",
+       "  color: var(--sklearn-color-text-on-default-background);\n",
+       "}\n",
+       "\n",
+       "/* On hover, darken the color of the background */\n",
+       "#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n",
+       "  color: var(--sklearn-color-text);\n",
+       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
+       "}\n",
+       "\n",
+       "/* Label box, darken color on hover, fitted */\n",
+       "#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
+       "  color: var(--sklearn-color-text);\n",
+       "  background-color: var(--sklearn-color-fitted-level-2);\n",
+       "}\n",
+       "\n",
+       "/* Estimator label */\n",
+       "\n",
+       "#sk-container-id-1 div.sk-label label {\n",
+       "  font-family: monospace;\n",
+       "  font-weight: bold;\n",
+       "  display: inline-block;\n",
+       "  line-height: 1.2em;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-label-container {\n",
+       "  text-align: center;\n",
+       "}\n",
+       "\n",
+       "/* Estimator-specific */\n",
+       "#sk-container-id-1 div.sk-estimator {\n",
+       "  font-family: monospace;\n",
+       "  border: 1px dotted var(--sklearn-color-border-box);\n",
+       "  border-radius: 0.25em;\n",
+       "  box-sizing: border-box;\n",
+       "  margin-bottom: 0.5em;\n",
+       "  /* unfitted */\n",
+       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-estimator.fitted {\n",
+       "  /* fitted */\n",
+       "  background-color: var(--sklearn-color-fitted-level-0);\n",
+       "}\n",
+       "\n",
+       "/* on hover */\n",
+       "#sk-container-id-1 div.sk-estimator:hover {\n",
+       "  /* unfitted */\n",
+       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 div.sk-estimator.fitted:hover {\n",
+       "  /* fitted */\n",
+       "  background-color: var(--sklearn-color-fitted-level-2);\n",
+       "}\n",
+       "\n",
+       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
+       "\n",
+       "/* Common style for \"i\" and \"?\" */\n",
+       "\n",
+       ".sk-estimator-doc-link,\n",
+       "a:link.sk-estimator-doc-link,\n",
+       "a:visited.sk-estimator-doc-link {\n",
+       "  float: right;\n",
+       "  font-size: smaller;\n",
+       "  line-height: 1em;\n",
+       "  font-family: monospace;\n",
+       "  background-color: var(--sklearn-color-background);\n",
+       "  border-radius: 1em;\n",
+       "  height: 1em;\n",
+       "  width: 1em;\n",
+       "  text-decoration: none !important;\n",
+       "  margin-left: 1ex;\n",
+       "  /* unfitted */\n",
+       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
+       "  color: var(--sklearn-color-unfitted-level-1);\n",
+       "}\n",
+       "\n",
+       ".sk-estimator-doc-link.fitted,\n",
+       "a:link.sk-estimator-doc-link.fitted,\n",
+       "a:visited.sk-estimator-doc-link.fitted {\n",
+       "  /* fitted */\n",
+       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
+       "  color: var(--sklearn-color-fitted-level-1);\n",
+       "}\n",
+       "\n",
+       "/* On hover */\n",
+       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
+       ".sk-estimator-doc-link:hover,\n",
+       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
+       ".sk-estimator-doc-link:hover {\n",
+       "  /* unfitted */\n",
+       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
+       "  color: var(--sklearn-color-background);\n",
+       "  text-decoration: none;\n",
+       "}\n",
+       "\n",
+       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
+       ".sk-estimator-doc-link.fitted:hover,\n",
+       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
+       ".sk-estimator-doc-link.fitted:hover {\n",
+       "  /* fitted */\n",
+       "  background-color: var(--sklearn-color-fitted-level-3);\n",
+       "  color: var(--sklearn-color-background);\n",
+       "  text-decoration: none;\n",
+       "}\n",
+       "\n",
+       "/* Span, style for the box shown on hovering the info icon */\n",
+       ".sk-estimator-doc-link span {\n",
+       "  display: none;\n",
+       "  z-index: 9999;\n",
+       "  position: relative;\n",
+       "  font-weight: normal;\n",
+       "  right: .2ex;\n",
+       "  padding: .5ex;\n",
+       "  margin: .5ex;\n",
+       "  width: min-content;\n",
+       "  min-width: 20ex;\n",
+       "  max-width: 50ex;\n",
+       "  color: var(--sklearn-color-text);\n",
+       "  box-shadow: 2pt 2pt 4pt #999;\n",
+       "  /* unfitted */\n",
+       "  background: var(--sklearn-color-unfitted-level-0);\n",
+       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
+       "}\n",
+       "\n",
+       ".sk-estimator-doc-link.fitted span {\n",
+       "  /* fitted */\n",
+       "  background: var(--sklearn-color-fitted-level-0);\n",
+       "  border: var(--sklearn-color-fitted-level-3);\n",
+       "}\n",
+       "\n",
+       ".sk-estimator-doc-link:hover span {\n",
+       "  display: block;\n",
+       "}\n",
+       "\n",
+       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
+       "\n",
+       "#sk-container-id-1 a.estimator_doc_link {\n",
+       "  float: right;\n",
+       "  font-size: 1rem;\n",
+       "  line-height: 1em;\n",
+       "  font-family: monospace;\n",
+       "  background-color: var(--sklearn-color-background);\n",
+       "  border-radius: 1rem;\n",
+       "  height: 1rem;\n",
+       "  width: 1rem;\n",
+       "  text-decoration: none;\n",
+       "  /* unfitted */\n",
+       "  color: var(--sklearn-color-unfitted-level-1);\n",
+       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 a.estimator_doc_link.fitted {\n",
+       "  /* fitted */\n",
+       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
+       "  color: var(--sklearn-color-fitted-level-1);\n",
+       "}\n",
+       "\n",
+       "/* On hover */\n",
+       "#sk-container-id-1 a.estimator_doc_link:hover {\n",
+       "  /* unfitted */\n",
+       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
+       "  color: var(--sklearn-color-background);\n",
+       "  text-decoration: none;\n",
+       "}\n",
+       "\n",
+       "#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n",
+       "  /* fitted */\n",
+       "  background-color: var(--sklearn-color-fitted-level-3);\n",
+       "}\n",
+       "</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>Treeffuser(extra_lightgbm_params={}, seed=0)</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator  sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label  sk-toggleable__label-arrow \">&nbsp;Treeffuser<span class=\"sk-estimator-doc-link \">i<span>Not fitted</span></span></label><div class=\"sk-toggleable__content \"><pre>Treeffuser(extra_lightgbm_params={}, seed=0)</pre></div> </div></div></div></div>"
+      ],
+      "text/plain": [
+       "Treeffuser(extra_lightgbm_params={}, seed=0)"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "model = Treeffuser(seed=0)\n",
+    "model.fit(X_train, y_train)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "100%|██████████| 100/100 [12:39<00:00,  7.59s/it] \n"
+     ]
+    }
+   ],
+   "source": [
+    "y_test_samples = model.sample(X_test, n_samples=100, seed=0, n_steps=50, verbose=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Newsvendor model"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We illustrate the practical relevance of accurate probabilistic predictions with an application to inventory management, using the newsvendor model \\citep{arrow1951optimal}. \n",
+    "\n",
+    "Assume that every day we decide how many units $q$ of an item to buy. \n",
+    "We buy at a cost $c$ and sell at a price $p$. \n",
+    "However, the demand $y$ is random, introducing uncertainty in our decision. \n",
+    "The goal is to maximize the expected profit:\n",
+    "$$\\max_{q} p~\\mathbb{E}\\left[\\min(q, y)\\right] - c q.$$\n",
+    "The optimal solution to the newsvendor problem is to buy $q = F^{-1}\\left( \\frac{p-c}{p} \\right)$ units, where $F^{-1}$ is the quantile function of the distribution of $y$. \n",
+    "\n",
+    "Using Treeffuser, we can compute the quantiles from the samples and forecast the optimal quantity of units to buy.\n",
+    "\n",
+    "To compute profits, we use the observed prices, assume a margin of $50\\%$ over all products, and assume the actual number of sales of an item correspond to the demand of this item. We let Treeffuser, learn the conditional distribution of the demand of each item, estimate their quantiles, and thus determine the optimal quantity to buy. \n",
+    "\n",
+    "We use the held-out data to compute the profit made if Treeffuser was used to forecast the demand of each item and to manage the inventory of each item."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "\n",
+    "def newsvendor_utility(y_true, quantity_ordered, prices, stocking_cost):\n",
+    "    \"\"\"\n",
+    "    The newsvendor utility function with stock q, demand y, selling price p, stocking cost c is given by\n",
+    "    $$ U(y, q, p, c) = p * min(y, q) - c * q $$\n",
+    "    \"\"\"\n",
+    "    utility = prices * np.minimum(y_true, quantity_ordered) - stocking_cost * quantity_ordered\n",
+    "    return utility\n",
+    "\n",
+    "\n",
+    "def newsvendor_optimal_quantity(y_samples, prices, stocking_cost):\n",
+    "    \"\"\"\n",
+    "    Returns the optimal quantity to order for the newsvendor problem.\n",
+    "    \n",
+    "    It is given theoeretically by:\n",
+    "        $$ q* = argmax_{q} E[U(y, q, p, c)] $$ \n",
+    "    which has a closed form solution,\n",
+    "        $$ q* = F^{-1}( (p - c) / p) $$\n",
+    "    where F is the CDF of the demand distribution\n",
+    "    \"\"\"\n",
+    "    # compute the target quantiles (p - c) / p\n",
+    "    target_quantiles = (prices - stocking_cost) / prices\n",
+    "    target_quantiles = np.maximum(target_quantiles, 0.0)\n",
+    "\n",
+    "    # compute the empirical quantities corresponding to the target quantiles\n",
+    "    res = []\n",
+    "    for i in range(y_samples.shape[1]):\n",
+    "        optimal_quantities = np.quantile(y_samples[:, i], target_quantiles[i])\n",
+    "        res.append(optimal_quantities)\n",
+    "    optimal_quantities = np.array(res)\n",
+    "    return optimal_quantities\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "185.07666666666668"
+      ]
+     },
+     "execution_count": 16,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# we don't know the profit margin of each item, so we assume it is 50%.\n",
+    "profit_margin = 0.5\n",
+    "\n",
+    "prices = X_test[\"sell_price\"].values\n",
+    "stocking_cost = prices / (1 + profit_margin)\n",
+    "\n",
+    "# Compute optimal quantities. \n",
+    "optimal_quantities = newsvendor_optimal_quantity(y_test_samples, prices, stocking_cost)\n",
+    "\n",
+    "# Treeffuser models continuous responses, hence we cast the predicted quantities into int\n",
+    "optimal_quantities = optimal_quantities.astype(int)\n",
+    "\n",
+    "profit = newsvendor_utility(y_test, optimal_quantities, prices, stocking_cost)\n",
+    "profit.sum()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "X_test_results = pd.DataFrame({\n",
+    "    \"date\": dates_test,\n",
+    "    \"profit\": profit\n",
+    "})\n",
+    "daily_profit = X_test_results.groupby(\"date\").sum().sort_values(\"date\").reset_index()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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",
+      "text/plain": [
+       "<Figure size 640x480 with 1 Axes>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import matplotlib.dates as mdates\n",
+    "\n",
+    "\n",
+    "plt.plot(pd.to_datetime(daily_profit[\"date\"]), daily_profit[\"profit\"].cumsum())\n",
+    "plt.ylabel(\"Cumulated profit\")\n",
+    "plt.xlabel(\"Date\")\n",
+    "plt.gca().xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MONDAY))\n",
+    "plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))\n",
+    "plt.gcf().autofmt_xdate()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.7"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/pyproject.toml b/pyproject.toml
index c55dda0f..1557a56d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,7 @@ tqdm = "^4.66.4"
 lightgbm = "^4.3.0"
 ml-collections = "^0.1.1"
 scikit-learn = "^1.5.0"
+pandas = "^2.0.0"
 
 [tool.poetry.dev-dependencies]
 pytest = "^8.2.2"
diff --git a/src/treeffuser/_base_tabular_diffusion.py b/src/treeffuser/_base_tabular_diffusion.py
index bca3327c..cd5fa9c2 100644
--- a/src/treeffuser/_base_tabular_diffusion.py
+++ b/src/treeffuser/_base_tabular_diffusion.py
@@ -6,6 +6,7 @@
 
 import einops
 import numpy as np
+import pandas as pd
 from jaxtyping import Float
 from numpy import ndarray
 from sklearn.base import BaseEstimator
@@ -35,19 +36,20 @@ def _check_array(array: ndarray[float]):
         array = array.reshape(-1, 1)
 
     # Cast floats
-    try:
-        if not np.issubdtype(array.dtype, np.floating):
+    if not np.issubdtype(array.dtype, np.floating):
+        try:
             array = np.asarray(array, dtype=np.float32)
             warnings.warn(
                 "Input array is not float; it has been recast to float32.",
                 CastFloat32Warning,
                 stacklevel=2,
             )
-    except ValueError as e:
-        # Raise the ValueError preserving the original exception context, see B904 from flake8-bugbear
-        raise ValueError(
-            "Input array is not float and cannot be converted to float32."
-        ) from e  #
+        except ValueError as e:
+            # raise the ValueError preserving the original exception context, see B904 from flake8-bugbear
+            raise ValueError(
+                "Input array is not float and cannot be converted to float32."
+                "Please check if you have encoded the categorical variables as numerical values."
+            ) from e
 
     return array
 
@@ -57,6 +59,32 @@ def _check_array(array: ndarray[float]):
 ###################################################
 
 
+def _ensure_numerical_columns(X: pd.DataFrame):
+    """
+    Convert the categorical columns of a DataFrame to numerical columns.
+    Raise an error if the columns are not numerical or categorical.
+    Copy the DataFrame to avoid modifying the original, but only if necessary.
+    """
+    X_has_been_copied = False
+    for c in X.columns:
+        if not isinstance(X[c].dtype, pd.CategoricalDtype) and not np.issubdtype(
+            X[c].dtype, np.number
+        ):
+            raise ValueError(
+                f"Dtypes of columns in `X` must be int, float, bool or category. "
+                f"Column {c} has dtype {X[c].dtype}."
+                f"Convert the column to a numerical dtype or a category, e.g.,"
+                f"X['{c}'] = X['{c}'].astype('category')."
+            )
+        if isinstance(X[c].dtype, pd.CategoricalDtype):
+            if X_has_been_copied is False:
+                # copy the DataFrame to avoid modifying the original
+                X = X.copy()
+                X_has_been_copied = True
+            X[c] = X[c].cat.codes
+    return X
+
+
 class BaseTabularDiffusion(BaseEstimator, abc.ABC):
     """
     Abstract class for the tabular diffusions. Every particular
@@ -72,6 +100,7 @@ def __init__(
         self.sde_initialize_from_data = sde_initialize_from_data
         self.score_model = None
         self._is_fitted = False
+        self._x_dataframe_columns = None
         self._x_scaler = None
         self._x_dim = None
         self._x_cat_idx = None
@@ -90,26 +119,126 @@ def get_new_score_model(self) -> ScoreModel:
         Return the score model.
         """
 
-    def _validate_data(
+    def _preprocess_and_validate_data(
         self,
-        X: ndarray | None = None,
-        y: ndarray | None = None,
+        X: ndarray | pd.DataFrame | None = None,
+        y: ndarray | pd.Series | pd.DataFrame | None = None,
+        cat_idx: list[int] | None = None,
         validate_X: bool = True,
         validate_y: bool = True,
-    ) -> tuple[Float[ndarray, "batch x_dim"] | None, Float[ndarray, "batch y_dim"] | None]:
-        """Reshape X and/or y to adhere to the (batch, n_dim) convention, and cast them as flows."""
-        if validate_X and X is not None:
+        reset_data_schema: bool = False,
+    ) -> tuple[
+        Float[ndarray, "batch x_dim"] | None, Float[ndarray, "batch y_dim"] | None, list[int]
+    ]:
+        """
+        Preprocess and validate the input data by methods like `fit`, `predict`, `sample`, ...
+        (1) It transforms the different possible inputs (pandas, numpy) into numpy arrays.
+        (2) It checks and convert the shapes to be 2d arrays. (3) It detects categorical columns
+        in DataFrames. (4) It ensure that the input in `predict` has the same shape/columns as the
+        input in `fit` (see parameter `reset` below).
+
+        Parameters
+        ----------
+        X : np.ndarray or pd.DataFrame, optional
+            Input data with shape (batch, x_dim).
+        y : np.ndarray or pd.Series or pd.DataFrame, optional
+            Target data with shape (batch, y_dim) or (batch).
+        cat_idx : List[int], optional
+            If X is a np.ndarray, list of indices of categorical features in X.
+            If X is a DataFrame, setting `cat_idx` will raise an error. Instead, ensure that the
+            categorical columns have dtype `category`, and they will be automatically detected as
+            categorical features. E.g., X['column_name'] = X['column_name'].astype('category').
+        validate_X : bool, optional
+            If True, validate the input data `X` and raise an error if no `X` is provided.
+            Default is True.
+        validate_y : bool, optional
+            If True, validate the target data `y` and raise an error if no `y` is provided.
+            Default is True.
+        reset_data_schema : bool, optional
+            If True, reset the cached schema of X and y (their shape, the name of the columns,
+            the categorical indices).
+            If False, check that X and y have the same shape and columns as the cached properties.
+            It should be set to True when `_preprocess_and_validate_data` is called from the `fit`
+            method and False when called from `predict` or `sample`, to ensure that the prediction
+            data is consistent with the training data. Default is False.
+        """
+        if validate_X:
+            if X is None:
+                raise ValueError("Input data `X` cannot be None.")
+            if isinstance(X, pd.DataFrame):
+                if cat_idx is not None and cat_idx != []:
+                    raise ValueError(
+                        "Categorical indices `cat_idx` should not be provided when `X` is a DataFrame."
+                        "Instead, ensure that the categorical columns have dtype `category`."
+                        "E.g., X['column_name'] = X['column_name'].astype('category')."
+                    )
+                cat_idx = [
+                    X.columns.get_loc(col) for col in X.select_dtypes("category").columns
+                ]
+                # check dtype of columns and convert categorical columns to numerical
+                X = _ensure_numerical_columns(X)
+                # at that point, X contains only numerical columns
+                if reset_data_schema:
+                    # store the columns of the DataFrame
+                    self._x_dataframe_columns = X.columns
+                    self._x_cat_idx = cat_idx
+                    self._x_dim = X.shape[1]
+                else:
+                    # check coherence of the input data with the training data
+                    # ensure the columns of the DataFrame are the same as the ones used during training
+                    if not all(col in X.columns for col in self._x_dataframe_columns):
+                        raise ValueError(
+                            "Column names in `X` are not present in the DataFrame but were present during training."
+                            f"{set(self._x_dataframe_columns) - set(X.columns)}"
+                        )
+                    # reorder the columns of the DataFrame to match the order of the training data
+                    X = X[self._x_dataframe_columns]
+                X = X.values
+
+            elif isinstance(X, np.ndarray):
+                if cat_idx is not None:
+                    for idx in cat_idx:
+                        if idx < 0 or idx >= X.shape[1]:
+                            raise ValueError(
+                                f"Invalid indices in `cat_idx`: {idx} is not in "
+                                f"[0, {X.shape[1]}-1] (the shape of X)."
+                            )
+                else:
+                    cat_idx = []
+
+                if reset_data_schema:
+                    self._x_cat_idx = cat_idx
+            else:
+                raise TypeError(
+                    "Input data `X` must be a numpy array or a pandas DataFrame."
+                    f"Reveived {type(X).__name}."
+                )
             X = _check_array(X)
 
-        if validate_y and y is not None:
+        if validate_y:
+            if y is None:
+                raise ValueError("Target data `y` cannot be None.")
+            if isinstance(y, pd.Series) or isinstance(y, pd.DataFrame):
+                y = y.values
+            elif not isinstance(y, np.ndarray):
+                raise TypeError(
+                    "Target data `y` must be a numpy array, a pandas Series or a pandas DataFrame."
+                    f"Received {type(y).__name}."
+                )
+
+            if reset_data_schema:
+                # store the original number of dimensions of the response
+                self._y_original_ndim = y.ndim
+                self._y_dim = y.shape[1] if y.ndim > 1 else 1
+
             y = _check_array(y)
 
-        return X, y
+        return X, y, self._x_cat_idx
 
     def fit(
         self,
-        X: Float[ndarray, "batch x_dim"],
-        y: Float[ndarray, "batch y_dim"],
+        X: Float[ndarray, "batch x_dim"] | pd.DataFrame,
+        y: Float[ndarray, "batch y_dim"] | pd.Series | pd.DataFrame,
         cat_idx: list[int] | None = None,
     ):
         """
@@ -117,12 +246,16 @@ def fit(
 
         Parameters
         ----------
-        X : np.ndarray
+        X : np.ndarray or pd.DataFrame
             Input data with shape (batch, x_dim).
-        y : np.ndarray
+        y : np.ndarray or pd.Series or pd.DataFrame
             Target data with shape (batch, y_dim).
         cat_idx : List[int], optional
-            List of indices of categorical features in X. Default is None.
+            If X is a np.ndarray, list of indices of categorical features in X.
+            If X is a DataFrame, setting `cat_idx` will raise an error. Instead, ensure that the
+            categorical columns have dtype `category`, and they will be automatically detected as
+            categorical features. E.g., X['column_name'] = X['column_name'].astype('category').
+            Default is None.
 
         Returns
         -------
@@ -134,13 +267,6 @@ def fit(
         The method handles 2D inputs (["batch x_dim"], ["batch y_dim"]) by convention,
         but also works with 1D inputs (["batch"]) for single-dimensional data.
         """
-        if cat_idx is not None:
-            for idx in cat_idx:
-                if idx < 0 or idx >= X.shape[1]:
-                    raise ValueError(
-                        f"Invalid indices in `cat_idx`: {idx} is not in "
-                        f"[0, {X.shape[1]}-1] (the shape of X)."
-                    )
         self.sde = self.get_new_sde()
         self.score_model = self.get_new_score_model()
         self._x_scaler = ScalerMixedTypes()
@@ -149,14 +275,11 @@ def fit(
         # store the original number of dimensions of the response
         # before reshaping the data so as to ensure that predictions
         # have the same shape as the user-inputted response
-        self._y_original_ndim = y.ndim
-        X, y = self._validate_data(X=X, y=y)
-
-        self._y_dim = y.shape[1]
-        x_transformed = self._x_scaler.fit_transform(
-            X,
-            cat_idx=cat_idx,
+        X, y, cat_idx = self._preprocess_and_validate_data(
+            X=X, y=y, cat_idx=cat_idx, reset_data_schema=True
         )
+
+        x_transformed = self._x_scaler.fit_transform(X, cat_idx=cat_idx)
         y_transformed = self._y_scaler.fit_transform(y)
 
         if self.sde_initialize_from_data:
@@ -168,10 +291,10 @@ def fit(
 
     def sample(
         self,
-        X: Float[ndarray, "batch x_dim"],
+        X: Float[ndarray, "batch x_dim"] | pd.DataFrame,
         n_samples: int,
         n_parallel: int = 10,
-        n_steps: int = 100,
+        n_steps: int = 50,
         seed=None,
         verbose: bool = False,
     ) -> Float[ndarray, "n_samples batch y_dim"]:
@@ -180,7 +303,7 @@ def sample(
 
         Parameters
         ----------
-        X : np.ndarray
+        X : np.ndarray or pd.DataFrame
             Input data with shape (batch, x_dim).
         n_samples : int
             Number of samples to draw for each input.
@@ -211,10 +334,10 @@ def sample(
         if not self._is_fitted:
             raise ValueError("The model has not been fitted yet.")
 
-        X, _ = self._validate_data(X=X, validate_y=False)
+        X, _, _ = self._preprocess_and_validate_data(X=X, validate_y=False)
 
         y_samples = self._sample_without_validation(
-            X, n_samples, n_parallel, n_steps, seed, verbose
+            X, n_samples, n_parallel=n_parallel, n_steps=n_steps, seed=seed, verbose=verbose
         )
 
         # Ensure output aligns with original shape provided by user
@@ -243,7 +366,7 @@ def _sample_without_validation(
         y_samples = []
         x_batched = None
 
-        pbar = tqdm(total=n_samples, disable=~verbose)
+        pbar = tqdm(total=n_samples, disable=not verbose)
         while n_samples_sampled < n_samples:
             batch_size_samples = min(n_parallel, n_samples - n_samples_sampled)
             y_batch = self.sde.sample_from_theoretical_prior(
@@ -286,7 +409,7 @@ def _score_fn(y, t):
 
     def predict(
         self,
-        X: Float[ndarray, "batch x_dim"],
+        X: Float[ndarray, "batch x_dim"] | pd.DataFrame,
         tol: float = 1e-3,
         max_samples: int = 100,
         verbose: bool = False,
@@ -299,7 +422,7 @@ def predict(
 
         Parameters
         ----------
-        X : Float[ndarray, "batch x_dim"]
+        X : Float[ndarray, "batch x_dim"] or pd.DataFrame
             Input data with shape (batch, x_dim).
         tol : float, optional
             Tolerance for the stopping criterion based on the relative change in the mean estimate. Default is 1e-3.
@@ -324,11 +447,10 @@ def predict(
         The method handles 2D inputs (["batch x_dim"], ["batch y_dim"]) by convention,
         but also works with 1D inputs (["batch"]) for single-dimensional data.
         """
-        X, _ = self._validate_data(X=X, validate_y=False)
-
         if not self._is_fitted:
             raise ValueError("The model has not been fitted yet.")
 
+        X, _, _ = self._preprocess_and_validate_data(X=X, validate_y=False)
         y_preds = self._predict_from_sample(X, tol, max_samples, verbose)
 
         if self._y_original_ndim == 1:
@@ -420,7 +542,7 @@ def predict_distribution(
         if not self._is_fitted:
             raise ValueError("The model has not been fitted yet.")
 
-        X, _ = self._validate_data(X=X, validate_y=False)
+        X, _, _ = self._preprocess_and_validate_data(X=X, validate_y=False)
 
         y_samples = self._sample_without_validation(X=X, n_samples=n_samples, verbose=verbose)
 
@@ -473,7 +595,7 @@ def compute_nll(
         if not self._is_fitted:
             raise ValueError("The model has not been fitted yet.")
 
-        X, y = self._validate_data(X=X, y=y)
+        X, y, _ = self._preprocess_and_validate_data(X=X, y=y)
 
         return self._compute_nll_from_sample(X, y, n_samples, bandwidth, verbose)
 
diff --git a/src/treeffuser/samples.py b/src/treeffuser/samples.py
index 39d6dbed..216f0c48 100644
--- a/src/treeffuser/samples.py
+++ b/src/treeffuser/samples.py
@@ -320,6 +320,9 @@ def to_numpy(self) -> Float[np.ndarray, "n_samples batch y_dim"]:
         """
         return self._samples
 
+    def __str__(self) -> str:
+        return str(self._samples)
+
     def __getitem__(self, key):
         """
         Prevent the user from removing the first or second dimension of the samples.


Test patch:
diff --git a/tests/test_treeffuser.py b/tests/test_treeffuser.py
index 54c0ebfb..991ebb65 100644
--- a/tests/test_treeffuser.py
+++ b/tests/test_treeffuser.py
@@ -112,6 +112,41 @@ def test_categorical():
         model.fit(X=X, y=y, cat_idx=cat_idx)
 
 
+def test_dataframe_input():
+    """Basic test for DataFrame input support."""
+    n = 10**3
+    rng = np.random.default_rng(seed=0)
+
+    X_noncat = rng.uniform(low=1, high=2, size=(n, 1))
+    X_cat = rng.choice(1, size=(n, 1))
+    y = rng.normal(loc=X_noncat + 2 * X_cat, scale=1, size=(n, 1))
+
+    import pandas as pd
+
+    df = pd.DataFrame({"X_noncat": X_noncat.flatten(), "X_cat": X_cat.flatten()})
+    model = Treeffuser()
+
+    # not setting X_cat as categorical
+    model.fit(X=df, y=y)
+    assert model._x_cat_idx == []
+    assert model._x_dim == 2
+
+    # setting X_cat as categorical
+    df["X_cat"] = df["X_cat"].astype("category")
+    model.fit(X=df, y=y)
+    assert model._x_cat_idx == [1]
+    assert model._x_dim == 2
+
+    # check error is raised if cat_idx is given when X is a DataFrame
+    with pytest.raises(ValueError, match="`cat_idx` should not be provided when"):
+        model.fit(X=df, y=y, cat_idx=[1])
+
+    # giving a dataframe for y
+    y = pd.DataFrame({"a:": y.flatten(), "b": y.flatten()})
+    model.fit(X=df, y=y)
+    assert model._y_dim == 2
+
+
 def fit_and_validate_model(
     x_train, y_train, x_test, y_test, n_samples=10**2, flatten_x=False, flatten_y=False
 ):