Repository: Epistimio/orion

Issue:
Add partial dependency plots 
See https://scikit-optimize.github.io/stable/modules/plots.html#plot-objective

Discussion:

Patch:
diff --git a/docs/src/user/web_api.rst b/docs/src/user/web_api.rst
index 3511bfdb..2e8c7367 100644
--- a/docs/src/user/web_api.rst
+++ b/docs/src/user/web_api.rst
@@ -274,6 +274,21 @@ visualize your experiments and their results.
 
    :statuscode 404: When the specified experiment doesn't exist in the database.
 
+.. http:get:: /plots/partial_dependencies/:experiment
+
+   Return a partial dependency plot for the specified experiment.
+
+   **Example response**
+
+   .. sourcecode:: http
+
+      HTTP/1.1 200 OK
+      Content-Type: text/javascript
+
+   The JSON output is generated automatically according to the `Plotly.js schema reference <https://plotly.com/python/reference/index/>`_.
+
+   :statuscode 404: When the specified experiment doesn't exist in the database.
+
 .. http:get:: /plots/regret/:experiment
 
    Return a regret plot for the specified experiment.
diff --git a/src/orion/analysis/__init__.py b/src/orion/analysis/__init__.py
index 33ceb8f3..8e31a5b7 100644
--- a/src/orion/analysis/__init__.py
+++ b/src/orion/analysis/__init__.py
@@ -8,7 +8,8 @@
    :synopsis: Provides agnostic HPO analysis tools
 """
 
-from orion.analysis.lpi import lpi
-from orion.analysis.regret import regret
+from orion.analysis.lpi_utils import lpi
+from orion.analysis.partial_dependency_utils import partial_dependency
+from orion.analysis.regret_utils import regret
 
-__all__ = ["lpi", "regret"]
+__all__ = ["lpi", "partial_dependency", "regret"]
diff --git a/src/orion/analysis/base.py b/src/orion/analysis/base.py
new file mode 100644
index 00000000..29f2200e
--- /dev/null
+++ b/src/orion/analysis/base.py
@@ -0,0 +1,138 @@
+"""
+:mod:`orion.analysis.base` -- Base tools to compute diverse analysis
+====================================================================
+
+.. module:: orion.analysis.base
+   :platform: Unix
+   :synopsis: Base tools to compute diverse analysis
+"""
+import itertools
+
+import numpy
+from sklearn.ensemble import (
+    AdaBoostRegressor,
+    BaggingRegressor,
+    ExtraTreesRegressor,
+    GradientBoostingRegressor,
+    RandomForestRegressor,
+)
+
+from orion.core.worker.transformer import build_required_space
+
+_regressors_ = {
+    "AdaBoostRegressor": AdaBoostRegressor,
+    "BaggingRegressor": BaggingRegressor,
+    "ExtraTreesRegressor": ExtraTreesRegressor,
+    "GradientBoostingRegressor": GradientBoostingRegressor,
+    "RandomForestRegressor": RandomForestRegressor,
+}
+
+
+def flatten_params(space, params=None):
+    """Return the params of the corresponding flat space
+
+    If no params are passed, returns all flattened params.
+    If params are passed, returns the corresponding flattened params.
+
+    Parameters
+    ----------
+    space: Space object
+        A space object from an experiment.
+    params: list of str, optional
+        The parameters to select from the search space. If the flattened search space
+        contains flattened params such as ('y' -> 'y[0]', 'y[1]'), passing 'y' in the list of
+        params will returned the flattened version ['y[0]', 'y[1]']
+
+    Examples
+    --------
+    If space has x~uniform(0, 1) and y~uniform(0, 1, shape=(1, 2)).
+    >>> flatten_params(space)
+    ['x', 'y[0,0]', 'y[0,1]']
+    >>> flatten_params(space, params=['x'])
+    ['x']
+    >>> flatten_params(space, params=['x', 'y'])
+    ['x', 'y[0,0]', 'y[0,1]']
+    >>> flatten_params(space, params=['x', 'y[0,1]'])
+    ['x', 'y[0,1]']
+    >>> flatten_params(space, params=['y[0,1]', 'x'])
+    ['x', 'y[0,1]']
+
+    Raises
+    ------
+    ValueError
+        If one of the parameter names passed is not in the flattened space.
+
+    """
+    keys = set(space.keys())
+    flattened_keys = set(
+        build_required_space(
+            space,
+            dist_requirement="linear",
+            type_requirement="numerical",
+            shape_requirement="flattened",
+        ).keys()
+    )
+
+    if params is None:
+        return sorted(flattened_keys)
+
+    flattened_params = []
+    for param in params:
+        if param not in flattened_keys and param not in keys:
+            raise ValueError(
+                f"Parameter {param} not contained in space: {flattened_keys}"
+            )
+        elif param not in flattened_keys and param in keys:
+            dim = space[param]
+            flattened_params += [
+                f'{dim.name}[{",".join(map(str, index))}]'
+                for index in itertools.product(*map(range, dim.shape))
+            ]
+        else:
+            flattened_params.append(param)
+
+    return flattened_params
+
+
+def to_numpy(trials, space):
+    """Convert trials in DataFrame to Numpy array of (params + objective)"""
+    return trials[list(space.keys()) + ["objective"]].to_numpy()
+
+
+def flatten_numpy(trials_array, flattened_space):
+    """Flatten dimensions"""
+    flattened_points = numpy.array(
+        [flattened_space.transform(point[:-1]) for point in trials_array]
+    )
+
+    return numpy.concatenate((flattened_points, trials_array[:, -1:]), axis=1)
+
+
+def train_regressor(regressor_name, data, **kwargs):
+    """Train regressor model
+
+    Parameters
+    ----------
+    model: str
+        Name of the regression model to use. Can be one of
+        - AdaBoostRegressor
+        - BaggingRegressor
+        - ExtraTreesRegressor
+        - GradientBoostingRegressor
+        - RandomForestRegressor (Default)
+    trials: DataFrame or dict
+        A dataframe of trials containing, at least, the columns 'objective' and 'id'. Or a dict
+        equivalent.
+
+    **kwargs
+        Arguments for the regressor model.
+
+    """
+    if regressor_name not in _regressors_:
+        raise ValueError(
+            f"{regressor_name} is not a supported regressor. "
+            f"Did you mean any of theses: list(_regressors_.keys())"
+        )
+
+    regressor = _regressors_[regressor_name](**kwargs)
+    return regressor.fit(data[:, :-1], data[:, -1])
diff --git a/src/orion/analysis/lpi.py b/src/orion/analysis/lpi_utils.py
similarity index 50%
rename from src/orion/analysis/lpi.py
rename to src/orion/analysis/lpi_utils.py
index 89b786c7..0564ccf4 100644
--- a/src/orion/analysis/lpi.py
+++ b/src/orion/analysis/lpi_utils.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 """
-:mod:`orion.analysis.lpi` -- Provide tools to calculate Local Parameter Importance
-==================================================================================
+:mod:`orion.analysis.lpi_utils` -- Provide tools to calculate Local Parameter Importance
+========================================================================================
 
 .. module:: orion.analysis.lpi
    :platform: Unix
@@ -9,75 +9,17 @@
 """
 import numpy
 import pandas as pd
-from sklearn.ensemble import (
-    AdaBoostRegressor,
-    BaggingRegressor,
-    ExtraTreesRegressor,
-    GradientBoostingRegressor,
-    RandomForestRegressor,
-)
 
+from orion.analysis.base import flatten_numpy, to_numpy, train_regressor
 from orion.core.worker.transformer import build_required_space
 
-_regressors_ = {
-    "AdaBoostRegressor": AdaBoostRegressor,
-    "BaggingRegressor": BaggingRegressor,
-    "ExtraTreesRegressor": ExtraTreesRegressor,
-    "GradientBoostingRegressor": GradientBoostingRegressor,
-    "RandomForestRegressor": RandomForestRegressor,
-}
 
-
-def train_regressor(regressor_name, data, **kwargs):
-    """Train regressor model
-
-    Parameters
-    ----------
-    model: str
-        Name of the regression model to use. Can be one of
-        - AdaBoostRegressor
-        - BaggingRegressor
-        - ExtraTreesRegressor
-        - GradientBoostingRegressor
-        - RandomForestRegressor (Default)
-    trials: DataFrame or dict
-        A dataframe of trials containing, at least, the columns 'objective' and 'id'. Or a dict
-        equivalent.
-
-    **kwargs
-        Arguments for the regressor model.
-
-    """
-    if regressor_name not in _regressors_:
-        raise ValueError(
-            f"{regressor_name} is not a supported regressor. "
-            f"Did you mean any of theses: list(_regressors_.keys())"
-        )
-
-    regressor = _regressors_[regressor_name](**kwargs)
-    return regressor.fit(data[:, :-1], data[:, -1])
-
-
-def to_numpy(trials, space):
-    """Convert trials in DataFrame to Numpy array of (params + objective)"""
-    return trials[list(space.keys()) + ["objective"]].to_numpy()
-
-
-def flatten(trials_array, flattened_space):
-    """Flatten dimensions"""
-    flattened_points = numpy.array(
-        [flattened_space.transform(point[:-1]) for point in trials_array]
-    )
-
-    return numpy.concatenate((flattened_points, trials_array[:, -1:]), axis=1)
-
-
-def make_grid(point, space, model, n):
+def make_grid(point, space, model, n_points):
     """Build a grid based on point.
 
     The shape of the grid will be
         (number of hyperparameters,
-         number of points ``n``,
+         number of points ``n_points``,
          number of hyperparameters + 1)
 
     Last column is the objective predicted by the model for a given point.
@@ -86,25 +28,18 @@ def make_grid(point, space, model, n):
     ----------
     point: numpy.ndarray
         A tuple representation of the best trials, (hyperparameters + objective)
-    model: str
-        Name of the regression model to use. Can be one of
-        - AdaBoostRegressor
-        - BaggingRegressor
-        - ExtraTreesRegressor
-        - GradientBoostingRegressor
-        - RandomForestRegressor (Default)
-    trials: DataFrame or dict
-        A dataframe of trials containing, at least, the columns 'objective' and 'id'. Or a dict
-        equivalent.
-
-    **kwargs
-        Arguments for the regressor model.
+    space: Space object
+        A space object from an experiment. It must be flattened and linearized.
+    model: `sklearn.base.RegressorMixin`
+        Trained regressor used to compute predictions on the grid
+    n_points: int
+        Number of points for each dimension on the grid.
 
     """
-    grid = numpy.zeros((len(space), n, len(space) + 1))
+    grid = numpy.zeros((len(space), n_points, len(space) + 1))
     for i, dim in enumerate(space.values()):
         grid[i, :, :] = point
-        grid[i, :, i] = numpy.linspace(*dim.interval(), num=n)
+        grid[i, :, i] = numpy.linspace(*dim.interval(), num=n_points)
         grid[i, :, -1] = model.predict(grid[i, :, :-1])
     return grid
 
@@ -114,23 +49,25 @@ def compute_variances(grid):
     return grid[:, :, -1].var(axis=1)
 
 
-def _lpi(point, space, model, n):
+def _lpi(point, space, model, n_points):
     """Local parameter importance for each hyperparameters"""
-    grid = make_grid(point, space, model, n)
+    grid = make_grid(point, space, model, n_points)
     variances = compute_variances(grid)
     ratios = variances / variances.sum()
     return pd.DataFrame(data=ratios, index=space.keys(), columns=["LPI"])
 
 
-def _linear_lpi(point, space, model, n):
-    # TODO
-    return
+# def _linear_lpi(point, space, model, n):
+#     # TODO
+#     return
 
 
-modes = dict(best=_lpi, linear=_linear_lpi)
+modes = dict(best=_lpi)  # , linear=_linear_lpi)
 
 
-def lpi(trials, space, mode="best", model="RandomForestRegressor", n=20, **kwargs):
+def lpi(
+    trials, space, mode="best", model="RandomForestRegressor", n_points=20, **kwargs
+):
     """
     Calculates the Local Parameter Importance for a collection of :class:`Trial`.
 
@@ -162,7 +99,7 @@ def lpi(trials, space, mode="best", model="RandomForestRegressor", n=20, **kwarg
         - GradientBoostingRegressor
         - RandomForestRegressor (Default)
 
-    n: int
+    n_points: int
         Number of points to compute the variances. Default is 20.
 
     **kwargs
@@ -175,7 +112,10 @@ def lpi(trials, space, mode="best", model="RandomForestRegressor", n=20, **kwarg
         param values and LPI metrics are returned in a DataFrame format.
     """
     flattened_space = build_required_space(
-        space, type_requirement="numerical", shape_requirement="flattened"
+        space,
+        dist_requirement="linear",
+        type_requirement="numerical",
+        shape_requirement="flattened",
     )
     if trials.empty or trials.shape[0] == 0:
         return pd.DataFrame(
@@ -185,8 +125,8 @@ def lpi(trials, space, mode="best", model="RandomForestRegressor", n=20, **kwarg
         )
 
     data = to_numpy(trials, space)
-    data = flatten(data, flattened_space)
+    data = flatten_numpy(data, flattened_space)
     model = train_regressor(model, data, **kwargs)
     best_point = data[numpy.argmin(data[:, -1])]
-    results = modes[mode](best_point, flattened_space, model, n)
+    results = modes[mode](best_point, flattened_space, model, n_points)
     return results
diff --git a/src/orion/analysis/partial_dependency_utils.py b/src/orion/analysis/partial_dependency_utils.py
new file mode 100644
index 00000000..1745e25e
--- /dev/null
+++ b/src/orion/analysis/partial_dependency_utils.py
@@ -0,0 +1,151 @@
+"""
+:mod:`orion.analysis.partial_dependency_utils` -- Tools to compute Partial Dependency
+=====================================================================================
+
+.. module:: orion.analysis.lpi
+   :platform: Unix
+   :synopsis: Provide tools to calculate Partial Dependency
+"""
+import copy
+import itertools
+
+import numpy
+import pandas
+
+from orion.analysis.base import flatten_numpy, flatten_params, to_numpy, train_regressor
+from orion.core.worker.transformer import build_required_space
+
+
+def partial_dependency(
+    trials,
+    space,
+    params=None,
+    model="RandomForestRegressor",
+    n_grid_points=10,
+    n_samples=50,
+    **kwargs
+):
+    """
+    Calculates the partial dependency of parameters in a collection of :class:`Trial`.
+
+    Parameters
+    ----------
+    trials: DataFrame or dict
+        A dataframe of trials containing, at least, the columns 'objective' and 'id'. Or a dict
+        equivalent.
+
+    space: Space object
+        A space object from an experiment.
+
+    params: list of str, optional
+        The parameters to include in the computation. All parameters are included by default.
+
+    model: str
+        Name of the regression model to use. Can be one of
+        - AdaBoostRegressor
+        - BaggingRegressor
+        - ExtraTreesRegressor
+        - GradientBoostingRegressor
+        - RandomForestRegressor (Default)
+
+    n_grid_points: int
+        Number of points in the grid to compute partial dependency. Default is 10.
+
+    n_samples: int
+        Number of samples to randomly generate the grid used to compute the partial dependency.
+        Default is 50.
+
+    **kwargs
+        Arguments for the regressor model.
+
+    Returns
+    -------
+    dict
+        Dictionary of DataFrames. Each combination of parameters as keys (dim1.name, dim2.name)
+        and for each parameters individually (dim1.name). Columns are
+        (dim1.name, dim2.name, objective) or (dim1.name, objective).
+
+    """
+    params = flatten_params(space, params)
+
+    flattened_space = build_required_space(
+        space,
+        dist_requirement="linear",
+        type_requirement="numerical",
+        shape_requirement="flattened",
+    )
+
+    if trials.empty or trials.shape[0] == 0:
+        return {}
+
+    data = to_numpy(trials, space)
+    data = flatten_numpy(data, flattened_space)
+    model = train_regressor(model, data, **kwargs)
+
+    data = flattened_space.sample(n_samples)
+    data = pandas.DataFrame(data, columns=flattened_space.keys())
+
+    partial_dependencies = dict()
+    for x_i, x_name in enumerate(params):
+        grid, averages, stds = partial_dependency_grid(
+            flattened_space, model, [x_name], data, n_grid_points
+        )
+        grid = reverse(flattened_space, grid)
+        partial_dependencies[x_name] = (grid, averages, stds)
+        for y_i in range(x_i + 1, len(params)):
+            y_name = params[y_i]
+            grid, averages, stds = partial_dependency_grid(
+                flattened_space, model, [x_name, y_name], data, n_grid_points
+            )
+            grid = reverse(flattened_space, grid)
+            partial_dependencies[(x_name, y_name)] = (grid, averages, stds)
+
+    return partial_dependencies
+
+
+def reverse(transformed_space, grid):
+    """Reverse transformations on the grid to bring back to original space"""
+    for param in grid.keys():
+        transformed_dim = transformed_space[param].original_dimension
+        param_grid = []
+        for value in grid[param]:
+            param_grid.append(transformed_dim.reverse(value))
+        grid[param] = param_grid
+    return grid
+
+
+def make_grid(dim, n_points):
+    """Build a grid of n_points for a dim"""
+    if dim.prior_name == "choices":
+        low, high = dim.interval()
+        return numpy.arange(low, high + 1)
+
+    return numpy.linspace(*dim.interval(), num=n_points)
+
+
+def partial_dependency_grid(space, model, params, samples, n_points=40):
+    """Compute the dependency grid for a given set of params (1 or 2)"""
+
+    samples = copy.deepcopy(samples)
+
+    grids = {}
+    for name in params:
+        grids[name] = make_grid(space[name], n_points)
+
+    lengths = [len(grids[name]) for name in params]
+    averages = numpy.zeros(lengths)
+    stds = numpy.zeros(lengths)
+
+    indexed_combinations = zip(
+        itertools.product(*(list(range(length)) for length in lengths)),
+        itertools.product(*grids.values()),
+    )
+    for z_idx, combination in indexed_combinations:
+        for i, name in enumerate(params):
+            samples[name] = combination[i]
+
+        predictions = model.predict(samples.to_numpy())
+        averages[z_idx] = numpy.mean(predictions)
+        stds[z_idx] = numpy.std(predictions)
+
+    return grids, averages.T, stds.T
diff --git a/src/orion/analysis/regret.py b/src/orion/analysis/regret_utils.py
similarity index 92%
rename from src/orion/analysis/regret.py
rename to src/orion/analysis/regret_utils.py
index 102ca856..36651223 100644
--- a/src/orion/analysis/regret.py
+++ b/src/orion/analysis/regret_utils.py
@@ -45,11 +45,11 @@ def regret(trials, names=("best", "best_id")):
     return df
 
 
-def get_regrets_idx(x):
+def get_regrets_idx(objectives):
     """Return the indices corresponding to the cumulative minimum"""
-    minima = numpy.minimum.accumulate(x)
+    minima = numpy.minimum.accumulate(objectives)
     diff = numpy.diff(minima)
-    jumps = numpy.arange(len(x))
+    jumps = numpy.arange(len(objectives))
     jumps[1:] *= diff != 0
     jumps = numpy.maximum.accumulate(jumps)
     return jumps
diff --git a/src/orion/plotting/backend_plotly.py b/src/orion/plotting/backend_plotly.py
index c75c647c..8a84fd87 100644
--- a/src/orion/plotting/backend_plotly.py
+++ b/src/orion/plotting/backend_plotly.py
@@ -10,13 +10,19 @@ import functools
 
 import numpy
 import pandas as pd
+import plotly.express as px
 import plotly.graph_objects as go
+from plotly.subplots import make_subplots
 
-import orion.analysis.regret
+import orion.analysis
+import orion.analysis.base
 from orion.algo.space import Categorical, Fidelity
+from orion.core.worker.transformer import build_required_space
 
 
-def lpi(experiment, model="RandomForestRegressor", model_kwargs=None, n=20, **kwargs):
+def lpi(
+    experiment, model="RandomForestRegressor", model_kwargs=None, n_points=20, **kwargs
+):
     """Plotly implementation of `orion.plotting.lpi`"""
     if not experiment:
         raise ValueError("Parameter 'experiment' is None")
@@ -26,7 +32,9 @@ def lpi(experiment, model="RandomForestRegressor", model_kwargs=None, n=20, **kw
 
     df = experiment.to_pandas()
     df = df.loc[df["status"] == "completed"]
-    df = orion.analysis.lpi(df, experiment.space, model=model, n=n, **model_kwargs)
+    df = orion.analysis.lpi(
+        df, experiment.space, model=model, n_points=n_points, **model_kwargs
+    )
 
     fig = go.Figure(data=[go.Bar(x=df.index.tolist(), y=df["LPI"].tolist())])
 
@@ -61,33 +69,26 @@ def parallel_coordinates(experiment, order=None, colorscale="YlOrRd", **kwargs):
 
         return df
 
-    def infer_order(order):
-        """Create order if not passed, otherwised verify it"""
+    def infer_order(space, order):
+        """Create order if not passed, otherwise verify it"""
+        params = orion.analysis.base.flatten_params(space, order)
         if order is None:
-            order = list(experiment.space.keys())
             fidelity_dims = [
                 dim for dim in experiment.space.values() if isinstance(dim, Fidelity)
             ]
-            if fidelity_dims:
-                del order[order.index(fidelity_dims[0].name)]
-                order.insert(0, fidelity_dims[0].name)
-        else:
-            names = set(experiment.space.keys())
-            any_invalid = set(order) - names
-            if any_invalid:
-                raise ValueError(
-                    f"Some names are invalid: {any_invalid} not in {names}"
-                )
+            fidelity = fidelity_dims[0].name if fidelity_dims else None
+            if fidelity in params:
+                del params[params.index(fidelity)]
+                params.insert(0, fidelity)
 
-        return order
+        return params
 
     def get_dimension(data, name, dim):
         dim_data = dict(label=name, values=data[name])
-        if isinstance(dim, Categorical):
-            dim_data["tickvals"] = list(range(len(dim.categories)))
-            dim_data["ticktext"] = dim.categories
-        elif isinstance(dim, Fidelity):
-            dim_data["range"] = (dim.low, dim.high)
+        if dim.type == "categorical":
+            categories = dim.interval()
+            dim_data["tickvals"] = list(range(len(categories)))
+            dim_data["ticktext"] = categories
         else:
             dim_data["range"] = dim.interval()
         return dim_data
@@ -102,17 +103,14 @@ def parallel_coordinates(experiment, order=None, colorscale="YlOrRd", **kwargs):
 
     trial = experiment.fetch_trials_by_status("completed")[0]
 
-    dimensions = []
-    for name in infer_order(order):
-        dim = experiment.space[name]
-        shape = dim.shape
-        if shape:
-            assert len(shape) == 1
-            for i in range(shape[0]):
-                name_i = f"{name}[{i}]"
-                dimensions.append(get_dimension(df, name_i, dim))
-        else:
-            dimensions.append(get_dimension(df, name, dim))
+    flattened_space = build_required_space(
+        experiment.space, shape_requirement="flattened"
+    )
+
+    dimensions = [
+        get_dimension(df, name, flattened_space[name])
+        for name in infer_order(experiment.space, order)
+    ]
 
     objective_name = trial.objective.name
 
@@ -143,6 +141,192 @@ def parallel_coordinates(experiment, order=None, colorscale="YlOrRd", **kwargs):
     return fig
 
 
+def partial_dependencies(
+    experiment,
+    params=None,
+    smoothing=0.85,
+    n_grid_points=10,
+    n_samples=50,
+    colorscale="Blues",
+    model="RandomForestRegressor",
+    model_kwargs=None,
+):
+    """Plotly implementation of `orion.plotting.partial_dependencies`"""
+
+    def build_data():
+        """Builds the dataframe for the plot"""
+        df = experiment.to_pandas()
+
+        names = list(experiment.space.keys())
+        df["params"] = df[names].apply(_format_hyperparameters, args=(names,), axis=1)
+
+        df = df.loc[df["status"] == "completed"]
+        data = orion.analysis.partial_dependency(
+            df,
+            experiment.space,
+            params=params,
+            model=model,
+            n_grid_points=n_grid_points,
+            n_samples=n_samples,
+            **model_kwargs,
+        )
+        df = _flatten_dims(df, experiment.space)
+        return (df, data)
+
+    def _set_scale(figure, dims, x, y):
+        for axis, dim in zip("xy", dims):
+            if "reciprocal" in dim.prior_name:
+                getattr(figure, f"update_{axis}axes")(type="log", row=y, col=x)
+
+    def _plot_marginalized_avg(data, x_name):
+        return go.Scatter(
+            x=data[0][x_name],
+            y=data[1],
+            mode="lines",
+            name=None,
+            showlegend=False,
+            line=dict(
+                color=px.colors.qualitative.D3[0],
+            ),
+        )
+
+    def _plot_marginalized_std(data, x_name):
+        return go.Scatter(
+            x=list(data[0][x_name]) + list(data[0][x_name])[::-1],
+            y=list(data[1] - data[2]) + list((data[1] + data[2]))[::-1],
+            mode="lines",
+            name=None,
+            fill="toself",
+            showlegend=False,
+            line=dict(
+                color=px.colors.qualitative.D3[0],
+                width=0,
+            ),
+        )
+
+    def _plot_contour(data, x_name, y_name):
+        return go.Contour(
+            x=data[0][x_name],
+            y=data[0][y_name],
+            z=data[1],
+            connectgaps=True,
+            # Share the same color range across contour plots
+            coloraxis="coloraxis",
+            line_smoothing=smoothing,
+            # To show labels
+            contours=dict(
+                coloring="heatmap",
+                showlabels=True,  # show labels on contours
+                labelfont=dict(  # label font properties
+                    size=12,
+                    color="white",
+                ),
+            ),
+        )
+
+    def _plot_scatter(x, y):
+        return go.Scatter(
+            x=x,
+            y=y,
+            marker={"line": {"width": 0.5, "color": "Grey"}, "color": "black"},
+            mode="markers",
+            showlegend=False,
+        )
+
+    if model_kwargs is None:
+        model_kwargs = {}
+
+    df, data = build_data()
+
+    if not data:
+        return go.Figure()
+
+    params = [
+        param_names for param_names in data.keys() if isinstance(param_names, str)
+    ]
+
+    flattened_space = build_required_space(
+        experiment.space,
+        shape_requirement="flattened",
+    )
+
+    fig = make_subplots(
+        rows=len(params),
+        cols=len(params),
+        shared_xaxes=True,
+        shared_yaxes=False,
+    )
+
+    fig.update_layout(paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)")
+    cmin = float("inf")
+    cmax = -float("inf")
+
+    for x_i in range(len(params)):
+        x_name = params[x_i]
+        fig.add_trace(
+            _plot_marginalized_avg(data[x_name], x_name),
+            row=x_i + 1,
+            col=x_i + 1,
+        )
+        fig.add_trace(
+            _plot_marginalized_std(data[x_name], x_name),
+            row=x_i + 1,
+            col=x_i + 1,
+        )
+
+        _set_scale(fig, [flattened_space[x_name]], x_i + 1, x_i + 1)
+
+        fig.update_xaxes(title_text=x_name, row=len(params), col=x_i + 1)
+        if x_i > 0:
+            fig.update_yaxes(title_text=x_name, row=x_i + 1, col=1)
+        else:
+            fig.update_yaxes(title_text="Objective", row=x_i + 1, col=x_i + 1)
+
+        for y_i in range(x_i + 1, len(params)):
+            y_name = params[y_i]
+            fig.add_trace(
+                _plot_contour(
+                    data[(x_name, y_name)],
+                    x_name,
+                    y_name,
+                ),
+                row=y_i + 1,
+                col=x_i + 1,
+            )
+            fig.add_trace(
+                _plot_scatter(df[x_name], df[y_name]),
+                row=y_i + 1,
+                col=x_i + 1,
+            )
+
+            cmin = min(cmin, data[(x_name, y_name)][1].min())
+            cmax = max(cmax, data[(x_name, y_name)][1].max())
+
+            _set_scale(
+                fig,
+                [flattened_space[name] for name in [x_name, y_name]],
+                x_i + 1,
+                y_i + 1,
+            )
+
+    for x_i in range(len(params)):
+        plot_id = len(params) * x_i + x_i + 1
+        if plot_id > 1:
+            key = f"yaxis{plot_id}_range"
+        else:
+            key = "yaxis_range"
+        fig.update_layout(**{key: [cmin, cmax]})
+
+    fig.update_layout(
+        title=f"Partial dependencies for experiment '{experiment.name}'",
+    )
+    fig.layout.coloraxis.colorbar.title = "Objective"
+
+    fig.update_layout(coloraxis=dict(colorscale=colorscale), showlegend=False)
+
+    return fig
+
+
 def regret(experiment, order_by, verbose_hover, **kwargs):
     """Plotly implementation of `orion.plotting.regret`"""
 
diff --git a/src/orion/plotting/base.py b/src/orion/plotting/base.py
index 29ad9637..9d7857c4 100644
--- a/src/orion/plotting/base.py
+++ b/src/orion/plotting/base.py
@@ -10,7 +10,9 @@
 import orion.plotting.backend_plotly as backend
 
 
-def lpi(experiment, model="RandomForestRegressor", model_kwargs=None, n=20, **kwargs):
+def lpi(
+    experiment, model="RandomForestRegressor", model_kwargs=None, n_points=20, **kwargs
+):
     """
     Make a bar plot to visualize the local parameter importance metric.
 
@@ -53,7 +55,7 @@ def lpi(experiment, model="RandomForestRegressor", model_kwargs=None, n=20, **kw
 
     """
     return backend.lpi(
-        experiment, model=model, model_kwargs=model_kwargs, n=n, **kwargs
+        experiment, model=model, model_kwargs=model_kwargs, n_points=n_points, **kwargs
     )
 
 
@@ -89,6 +91,75 @@ def parallel_coordinates(experiment, order=None, **kwargs):
     return backend.parallel_coordinates(experiment, order=order, **kwargs)
 
 
+def partial_dependencies(
+    experiment,
+    params=None,
+    smoothing=0.85,
+    n_grid_points=10,
+    n_samples=50,
+    colorscale="Blues",
+    model="RandomForestRegressor",
+    model_kwargs=None,
+):
+    """
+    Make countour plots to visualize the search space of each combination of params.
+
+    Parameters
+    ----------
+    experiment: ExperimentClient or Experiment
+        The orion object containing the experiment data
+
+    params: list of str, optional
+        Indicates the parameters to include in the plots. All parameters are included by default.
+
+    smoothing: float, optional
+        Smoothing applied to the countor plot. 0 corresponds to no smoothing. Default is 0.85.
+
+    colorscale: str, optional
+        The colorscale used for the contour plots. Supported values depends on the backend.
+        Default is 'Blues'.
+
+    n_grid_points: int, optional
+        Number of points in the grid to compute partial dependency. Default is 10.
+
+    n_samples: int, optinal
+        Number of samples to randomly generate the grid used to compute the partial dependency.
+        Default is 50.
+
+    model: str
+        Name of the regression model to use. Can be one of
+        - AdaBoostRegressor
+        - BaggingRegressor
+        - ExtraTreesRegressor
+        - GradientBoostingRegressor
+        - RandomForestRegressor (Default)
+
+    model_kwargs: dict, optional
+        Arguments for the regressor model.
+
+    Returns
+    -------
+    plotly.graph_objects.Figure
+
+    Raises
+    ------
+    ValueError
+        If no experiment is provided.
+
+    """
+
+    return backend.partial_dependencies(
+        experiment,
+        params=params,
+        smoothing=smoothing,
+        n_grid_points=n_grid_points,
+        n_samples=n_samples,
+        colorscale=colorscale,
+        model=model,
+        model_kwargs=model_kwargs,
+    )
+
+
 def regret(experiment, order_by="suggested", verbose_hover=True, **kwargs):
     """
     Make a plot to visualize the performance of the hyper-optimization process.
@@ -131,6 +202,7 @@ def regret(experiment, order_by="suggested", verbose_hover=True, **kwargs):
 PLOT_METHODS = {
     "lpi": lpi,
     "parallel_coordinates": parallel_coordinates,
+    "partial_dependencies": partial_dependencies,
     "regret": regret,
 }
 
@@ -187,6 +259,11 @@ class PlotAccessor:
         __doc__ = parallel_coordinates.__doc__
         return self(kind="parallel_coordinates", **kwargs)
 
+    def partial_dependencies(self, **kwargs):
+        """Make countour plots to visualize the search space of each combination of params."""
+        __doc__ = partial_dependencies.__doc__
+        return self(kind="partial_dependencies", **kwargs)
+
     def regret(self, **kwargs):
         """Make a plot to visualize the performance of the hyper-optimization process."""
         __doc__ = regret.__doc__
diff --git a/src/orion/serving/plots_resources.py b/src/orion/serving/plots_resources.py
index 589737fb..7b9009f0 100644
--- a/src/orion/serving/plots_resources.py
+++ b/src/orion/serving/plots_resources.py
@@ -40,6 +40,17 @@ class PlotsResource(object):
         experiment = ExperimentClient(retrieve_experiment(experiment_name), None)
         resp.body = experiment.plot.parallel_coordinates().to_json()
 
+    def on_get_partial_dependencies(
+        self, req: Request, resp: Response, experiment_name: str
+    ):
+        """
+        Handle GET requests for plotting partial dependencies on
+        plots/partial_dependencies/:experiment where ``experiment`` is the user-defined name of the
+        experiment.
+        """
+        experiment = ExperimentClient(retrieve_experiment(experiment_name), None)
+        resp.body = experiment.plot.partial_dependencies().to_json()
+
     def on_get_regret(self, req: Request, resp: Response, experiment_name: str):
         """
         Handle GET requests for plotting regret plots on plots/regret/:experiment
diff --git a/src/orion/serving/webapi.py b/src/orion/serving/webapi.py
index 10c58f75..110e9c11 100644
--- a/src/orion/serving/webapi.py
+++ b/src/orion/serving/webapi.py
@@ -49,6 +49,11 @@ class WebApi(falcon.API):
             suffix="trial_in_experiment",
         )
         self.add_route("/plots/lpi/{experiment_name}", plots_resource, suffix="lpi")
+        self.add_route(
+            "/plots/partial_dependencies/{experiment_name}",
+            plots_resource,
+            suffix="partial_dependencies",
+        )
         self.add_route(
             "/plots/parallel_coordinates/{experiment_name}",
             plots_resource,
diff --git a/tox.ini b/tox.ini
index dd1d7493..fc3f4394 100644
--- a/tox.ini
+++ b/tox.ini
@@ -146,7 +146,7 @@ skip_install = false
 deps =
     pylint == 2.5.*
 commands =
-    pylint src/orion/core src/orion/client src/orion/algo
+    pylint src/orion/core src/orion/client src/orion/algo src/orion/analysis src/orion/plotting
 
 
 [testenv:doc8]


Test patch:
diff --git a/tests/functional/serving/test_plots_resource.py b/tests/functional/serving/test_plots_resource.py
index 79053149..3088f313 100644
--- a/tests/functional/serving/test_plots_resource.py
+++ b/tests/functional/serving/test_plots_resource.py
@@ -128,6 +128,50 @@ class TestParallelCoordinatesPlots:
         assert list(response.json.keys()) == ["data", "layout"]
 
 
+@pytest.mark.usefixtures("version_XYZ")
+class TestPartialDependenciesPlots:
+    """Tests parallel coordinates plots"""
+
+    def test_unknown_experiment(self, client):
+        """Tests that the API returns a 404 Not Found when an unknown experiment is queried."""
+        response = client.simulate_get("/plots/partial_dependencies/unknown-experiment")
+
+        assert response.status == "404 Not Found"
+        assert response.json == {
+            "title": "Experiment not found",
+            "description": 'Experiment "unknown-experiment" does not exist',
+        }
+
+    def test_plot(self, client):
+        """Tests that the API returns the plot in json format."""
+        with create_experiment(config, trial_config, ["completed"]) as (
+            _,
+            _,
+            experiment,
+        ):
+            response = client.simulate_get(
+                "/plots/partial_dependencies/experiment-name"
+            )
+
+        assert response.status == "200 OK"
+        assert response.json
+        assert list(response.json.keys()) == ["data", "layout"]
+
+    def test_no_trials(self, client):
+        """Tests that the API returns an empty figure when no trials are found."""
+        with create_experiment(config, trial_config, []) as (
+            _,
+            _,
+            experiment,
+        ):
+            response = client.simulate_get(
+                "/plots/partial_dependencies/experiment-name"
+            )
+
+        assert response.status == "200 OK"
+        assert list(response.json.keys()) == ["data", "layout"]
+
+
 @pytest.mark.usefixtures("version_XYZ")
 class TestLPIPlots:
     """Tests lpi plots"""
diff --git a/tests/unittests/analysis/test_base.py b/tests/unittests/analysis/test_base.py
new file mode 100644
index 00000000..505c6f32
--- /dev/null
+++ b/tests/unittests/analysis/test_base.py
@@ -0,0 +1,155 @@
+"""Tests for :mod:`orion.analysis.base`"""
+import numpy
+import pandas as pd
+import pytest
+from sklearn.ensemble import (
+    AdaBoostRegressor,
+    BaggingRegressor,
+    ExtraTreesRegressor,
+    GradientBoostingRegressor,
+    RandomForestRegressor,
+)
+
+from orion.analysis.base import flatten_numpy, flatten_params, to_numpy, train_regressor
+from orion.core.io.space_builder import SpaceBuilder
+from orion.core.worker.transformer import build_required_space
+
+data = pd.DataFrame(
+    data={
+        "id": ["a", "b", "c"],
+        "x": [0, 1, 2],
+        "y": [1, 2, 0],
+        "z": ["a", "b", "c"],
+        "objective": [0.1, 0.2, 0.3],
+    }
+)
+
+
+@pytest.fixture
+def space():
+    space = SpaceBuilder().build(
+        {
+            "x": "uniform(0, 6)",
+            "y": "uniform(0, 3)",
+            "z": "choices(['a', 'b', 'c'])",
+        }
+    )
+    return space
+
+
+@pytest.fixture
+def fspace(space):
+    return build_required_space(
+        space,
+        dist_requirement="linear",
+        type_requirement="numerical",
+        shape_requirement="flattened",
+    )
+
+
+@pytest.fixture
+def params():
+    return list("xyz")
+
+
+@pytest.fixture
+def hspace():
+    space = SpaceBuilder().build(
+        {
+            "x": "uniform(0, 6, shape=3)",
+            "y": "uniform(0, 3, shape=(2, 3))",
+            "z": "choices(['a', 'b', 'c'])",
+        }
+    )
+    return space
+
+
+@pytest.fixture
+def flat_params():
+    return (
+        [f"x[{i}]" for i in range(3)]
+        + [f"y[{i},{j}]" for i in range(2) for j in range(3)]
+        + ["z"]
+    )
+
+
+class TestFlattenParams:
+    def test_no_params(self, space, params):
+        """Test that all params are returned if None"""
+        assert flatten_params(space) == params
+
+    def test_flat_no_params(self, hspace, flat_params):
+        """Test that all flattened params are returned if None"""
+        assert flatten_params(hspace) == flat_params
+
+    def test_params_unchanged(self, space):
+        """Test that params list passed is not modified"""
+        params = ["x", "y"]
+        flatten_params(space, params)
+        assert params == ["x", "y"]
+
+    def test_unexisting_params(self, space):
+        """Test that ValueError is raised if passing unexisting params"""
+        with pytest.raises(ValueError) as exc:
+            flatten_params(space, ["idoexistbelieveme!!!"])
+        assert exc.match(f"Parameter idoexistbelieveme!!! not contained in space: ")
+
+    def test_no_flatten(self, space, hspace):
+        """Test selection of params not involving flattening"""
+        assert flatten_params(space, ["x", "y"]) == ["x", "y"]
+        assert flatten_params(hspace, ["z"]) == ["z"]
+
+    def test_flattened_params(self, hspace):
+        """Test selecting specific flattened params"""
+        params = ["x[0]", "x[2]", "y[0,2]", "y[1,1]", "z"]
+        assert flatten_params(hspace, params) == params
+
+    def test_top_params(self, hspace):
+        """Test selecting all flattened keys of a parameter"""
+        params = ["x", "y[0,2]", "y[1,1]", "z"]
+        assert (
+            flatten_params(hspace, params) == [f"x[{i}]" for i in range(3)] + params[1:]
+        )
+
+
+def test_to_numpy(space):
+    """Test that trials are correctly converted to numpy array"""
+    array = to_numpy(data, space)
+
+    assert array.shape == (3, 4)
+    numpy.testing.assert_equal(array[:, 0], data["x"])
+    numpy.testing.assert_equal(array[:, 1], data["y"])
+    numpy.testing.assert_equal(array[:, 2], data["z"])
+    numpy.testing.assert_equal(array[:, 3], data["objective"])
+
+
+class TestTrainRegressor:
+    def test_train_regressor(self, space, fspace):
+        """Test training different models"""
+        array = flatten_numpy(to_numpy(data, space), fspace)
+        model = train_regressor("AdaBoostRegressor", array)
+        assert isinstance(model, AdaBoostRegressor)
+        model = train_regressor("BaggingRegressor", array)
+        assert isinstance(model, BaggingRegressor)
+        model = train_regressor("ExtraTreesRegressor", array)
+        assert isinstance(model, ExtraTreesRegressor)
+        model = train_regressor("GradientBoostingRegressor", array)
+        assert isinstance(model, GradientBoostingRegressor)
+        model = train_regressor("RandomForestRegressor", array)
+        assert isinstance(model, RandomForestRegressor)
+
+    def test_train_regressor_kwargs(self, space, fspace):
+        """Test training models with kwargs"""
+        array = flatten_numpy(to_numpy(data, space), fspace)
+        model = train_regressor(
+            "RandomForestRegressor", array, max_depth=2, max_features="sqrt"
+        )
+        assert model.max_depth == 2
+        assert model.max_features == "sqrt"
+
+    def test_train_regressor_invalid(self, space, fspace):
+        """Test error message for invalid model names"""
+        array = flatten_numpy(to_numpy(data, space), fspace)
+        with pytest.raises(ValueError) as exc:
+            train_regressor("IDontExist", array)
+        assert exc.match("IDontExist is not a supported regressor")
diff --git a/tests/unittests/analysis/test_lpi.py b/tests/unittests/analysis/test_lpi.py
index 15b03796..4af3de3c 100644
--- a/tests/unittests/analysis/test_lpi.py
+++ b/tests/unittests/analysis/test_lpi.py
@@ -5,21 +5,9 @@ import copy
 import numpy
 import pandas as pd
 import pytest
-from sklearn.ensemble import (
-    AdaBoostRegressor,
-    BaggingRegressor,
-    ExtraTreesRegressor,
-    GradientBoostingRegressor,
-    RandomForestRegressor,
-)
 
-from orion.analysis.lpi import (
-    compute_variances,
-    lpi,
-    make_grid,
-    to_numpy,
-    train_regressor,
-)
+from orion.analysis.base import to_numpy, train_regressor
+from orion.analysis.lpi_utils import compute_variances, lpi, make_grid
 from orion.core.io.space_builder import SpaceBuilder
 
 data = pd.DataFrame(
@@ -60,49 +48,6 @@ def test_parameter_not_modified():
     pd.testing.assert_frame_equal(data, original)
 
 
-def test_to_numpy():
-    """Test that trials are correctly converted to numpy array"""
-    array = to_numpy(data, space)
-
-    assert array.shape == (4, 3)
-    numpy.testing.assert_equal(array[:, 0], data["x"])
-    numpy.testing.assert_equal(array[:, 1], data["y"])
-    numpy.testing.assert_equal(array[:, 2], data["objective"])
-
-
-def test_train_regressor():
-    """Test training different models"""
-    array = to_numpy(data, space)
-    model = train_regressor("AdaBoostRegressor", array)
-    assert isinstance(model, AdaBoostRegressor)
-    model = train_regressor("BaggingRegressor", array)
-    assert isinstance(model, BaggingRegressor)
-    model = train_regressor("ExtraTreesRegressor", array)
-    assert isinstance(model, ExtraTreesRegressor)
-    model = train_regressor("GradientBoostingRegressor", array)
-    assert isinstance(model, GradientBoostingRegressor)
-    model = train_regressor("RandomForestRegressor", array)
-    assert isinstance(model, RandomForestRegressor)
-
-
-def test_train_regressor_kwargs():
-    """Test training models with kwargs"""
-    array = to_numpy(data, space)
-    model = train_regressor(
-        "RandomForestRegressor", array, max_depth=2, max_features="sqrt"
-    )
-    assert model.max_depth == 2
-    assert model.max_features == "sqrt"
-
-
-def test_train_regressor_invalid():
-    """Test error message for invalid model names"""
-    array = to_numpy(data, space)
-    with pytest.raises(ValueError) as exc:
-        train_regressor("IDontExist", array)
-    assert exc.match("IDontExist is not a supported regressor")
-
-
 def test_make_grid():
     """Test grid has correct format"""
     trials = to_numpy(data, space)
diff --git a/tests/unittests/analysis/test_partial_dependency.py b/tests/unittests/analysis/test_partial_dependency.py
new file mode 100644
index 00000000..4c8f9a6a
--- /dev/null
+++ b/tests/unittests/analysis/test_partial_dependency.py
@@ -0,0 +1,376 @@
+# -*- coding: utf-8 -*-
+"""Tests :func:`orion.analysis.partial_dependency`"""
+import copy
+
+import numpy
+import pandas as pd
+import pytest
+from sklearn.ensemble import (
+    AdaBoostRegressor,
+    BaggingRegressor,
+    ExtraTreesRegressor,
+    GradientBoostingRegressor,
+    RandomForestRegressor,
+)
+
+from orion.analysis.base import flatten_numpy, to_numpy, train_regressor
+from orion.analysis.partial_dependency_utils import (
+    make_grid,
+    partial_dependency,
+    partial_dependency_grid,
+    reverse,
+)
+from orion.core.io.space_builder import SpaceBuilder
+from orion.core.worker.transformer import build_required_space
+
+data = pd.DataFrame(
+    data={
+        "id": ["a", "b", "c", "d"],
+        "x": [0, 1, 2, 3],
+        "y": list(numpy.e ** numpy.array([2, 3, 1, 4])),
+        "z": ["a", "a", "b", "c"],
+        "objective": [0.1, 0.2, 0.3, 0.5],
+    }
+)
+
+
+hdata = pd.DataFrame(
+    data={
+        "id": ["a", "b", "c", "d"],
+        "x": [0, 1, 2, 3],
+        "y": list(numpy.e ** numpy.array([[2, 1, 2], [3, 0, 1], [1, 3, 0], [4, 4, 0]])),
+        "z": ["a", "a", "b", "c"],
+        "objective": [0.1, 0.2, 0.3, 0.5],
+    }
+)
+
+
+@pytest.fixture
+def space():
+    """Base flat space"""
+    return SpaceBuilder().build(
+        {
+            "x": "uniform(0, 6)",
+            "y": "loguniform(0.001, 1)",
+            "z": "choices(['a', 'b', 'c'])",
+        }
+    )
+
+
+@pytest.fixture
+def hspace():
+    """Space with multidim dims"""
+    return SpaceBuilder().build(
+        {
+            "x": "uniform(0, 6)",
+            "y": f"loguniform({numpy.e}, {numpy.e}**4, shape=3)",
+            "z": "choices(['a', 'b', 'c'])",
+        }
+    )
+
+
+def flatten_space(some_space):
+    """Flatten a space"""
+    return build_required_space(
+        some_space,
+        dist_requirement="linear",
+        type_requirement="numerical",
+        shape_requirement="flattened",
+    )
+
+
+def mock_model():
+    """Return a mocked regressor which just predict iterated integers"""
+
+    class Model:
+        """Mocked Regressor"""
+
+        def __init__(self):
+            self.i = 0
+
+        def predict(self, data):
+            """Returns counting of predictions requested."""
+            data = numpy.arange(data.shape[0]) + self.i
+            self.i += data.shape[0]
+            return data  #  + numpy.random.normal(0, self.i, size=data.shape[0])
+
+    return Model()
+
+
+def mock_train_regressor(monkeypatch, assert_model=None):
+    """Mock the train_regressor to return the mocked regressor instead"""
+
+    def train_regressor(model, data, **kwargs):
+        """Return the mocked model, and then model argument if requested"""
+        if assert_model:
+            assert model == assert_model
+        return mock_model()
+
+    monkeypatch.setattr(
+        "orion.analysis.partial_dependency_utils.train_regressor", train_regressor
+    )
+
+
+def test_reverse_no_reshape_needed(space):
+    """Test that transformed grids can be reversed to original type, when there was no reshape"""
+    flattened_space = flatten_space(space)
+
+    grid = {
+        "x": [0, 1, 2, 4],
+        "y": numpy.log([0.001, 0.01, 0.1, 1]),
+        "z": [0, 0, 1, 2],
+    }
+
+    reversed_grid = reverse(flattened_space, grid)
+    assert reversed_grid["x"] == [0, 1, 2, 4]
+    assert reversed_grid["y"] == [0.001, 0.01, 0.1, 1]
+    assert reversed_grid["z"] == ["a", "a", "b", "c"]
+
+
+def test_reverse_with_reshape(hspace):
+    """Test that transformed grids can be reversed to original type, when reshape was applied"""
+    flattened_space = flatten_space(hspace)
+
+    grid = {
+        "x": [0, 1, 2, 4],
+        "y[0]": numpy.log([0.001, 0.01, 0.1, 1]),
+        "y[1]": numpy.log([1, 0.01, 0.1, 0.001]),
+        "z": [0, 0, 1, 2],
+    }
+
+    reversed_grid = reverse(flattened_space, grid)
+    assert reversed_grid["x"] == [0, 1, 2, 4]
+    assert reversed_grid["y[0]"] == [0.001, 0.01, 0.1, 1]
+    assert reversed_grid["y[1]"] == [1, 0.01, 0.1, 0.001]
+    assert reversed_grid["z"] == ["a", "a", "b", "c"]
+
+
+def test_make_grid(hspace):
+    """Test that different dimension types are supported"""
+    flattened_space = flatten_space(hspace)
+    assert list(make_grid(flattened_space["x"], 7)) == [0, 1, 2, 3, 4, 5, 6]
+    numpy.testing.assert_almost_equal(
+        make_grid(flattened_space["y[0]"], 4), [1, 2, 3, 4], decimal=4
+    )
+    numpy.testing.assert_almost_equal(
+        make_grid(flattened_space["y[2]"], 4), [1, 2, 3, 4], decimal=4
+    )
+    assert list(make_grid(flattened_space["z"], 10)) == [0, 1, 2]
+
+
+def test_partial_dependency_grid(hspace):
+    """Test the computation of the averages and stds"""
+
+    flattened_space = flatten_space(hspace)
+
+    n_points = 5
+    n_samples = 20
+    samples = flattened_space.sample(n_samples)
+    samples = pd.DataFrame(samples, columns=flattened_space.keys())
+
+    params = ["x", "y[0]", "y[2]", "z"]
+
+    # Test for 1 param
+    grid, averages, stds = partial_dependency_grid(
+        flattened_space, mock_model(), ["x"], samples, n_points=n_points
+    )
+
+    assert list(grid.keys()) == ["x"]
+    assert list(grid["x"]) == [0, 1.5, 3, 4.5, 6]
+    assert averages.shape == (n_points,)
+    assert stds.shape == (n_points,)
+    assert averages[0] == numpy.arange(n_samples).mean()
+    assert (
+        averages[4]
+        == numpy.arange(n_samples * (n_points - 1), n_samples * n_points).mean()
+    )
+    assert stds[0] == numpy.arange(n_samples).std()
+
+    # Test for 2 param
+    grid, averages, stds = partial_dependency_grid(
+        flattened_space, mock_model(), ["x", "y[0]"], samples, n_points=n_points
+    )
+
+    assert list(grid.keys()) == ["x", "y[0]"]
+    assert list(grid["x"]) == [0, 1.5, 3, 4.5, 6]
+    # assert list(grid["y[0]"]) == [0, 0.75, 1.5, 2.25, 3]
+    numpy.testing.assert_almost_equal(
+        grid["y[0]"],
+        numpy.linspace(
+            numpy.log(float(f"{numpy.e}")),
+            numpy.log(float(f"{numpy.e}") ** 4),
+            num=n_points,
+        ),
+        decimal=4,
+    )
+
+    assert averages.shape == (n_points, n_points)
+    assert stds.shape == (n_points, n_points)
+    assert averages[0, 0] == numpy.arange(n_samples).mean()
+    assert (
+        averages[4, 4]
+        == numpy.arange(
+            n_samples * n_points * n_points - n_samples, n_samples * n_points * n_points
+        ).mean()
+    )
+    assert stds[0, 0] == numpy.arange(n_samples).std()
+    assert stds[4, 4] == numpy.arange(n_samples).std()
+
+    # Test for 2 param with one categorical, with less categories then n_points
+    grid, averages, stds = partial_dependency_grid(
+        flattened_space, mock_model(), ["x", "z"], samples, n_points=n_points
+    )
+
+    assert list(grid.keys()) == ["x", "z"]
+    assert list(grid["x"]) == [0, 1.5, 3, 4.5, 6]
+    assert list(grid["z"]) == [0, 1, 2]
+    assert averages.shape == (3, n_points)
+    assert stds.shape == (3, n_points)
+
+    assert averages[0, 0] == numpy.arange(n_samples).mean()
+    assert (
+        averages[2, 4]
+        == numpy.arange(
+            n_samples * 3 * n_points - n_samples, n_samples * 3 * n_points
+        ).mean()
+    )
+    assert stds[0, 0] == numpy.arange(n_samples).std()
+    assert stds[2, 4] == numpy.arange(n_samples).std()
+
+
+def test_accept_empty(space):
+    """Tests an empty dataframe is returned if you give an empty dataframe"""
+    empty_frame = pd.DataFrame()
+    results = partial_dependency(empty_frame, space, n_grid_points=3, n_samples=5)
+
+    assert results == {}
+
+    empty_frame = pd.DataFrame(columns=["x", "y", "z", "objective"])
+    results = partial_dependency(empty_frame, space, n_grid_points=3, n_samples=5)
+
+    assert results == {}
+
+
+def test_parameter_not_modified(monkeypatch, space):
+    """Tests the original dataframe is not modified"""
+    original = copy.deepcopy(data)
+
+    mock_train_regressor(monkeypatch)
+    partial_dependency(data, space, n_samples=10)
+
+    pd.testing.assert_frame_equal(data, original)
+
+
+def test_single_param(monkeypatch, space):
+    """Test computing for a single param"""
+    mock_train_regressor(monkeypatch)
+
+    partial_dependencies = partial_dependency(data, space, params=["x"], n_samples=10)
+    assert list(partial_dependencies.keys()) == ["x"]
+    assert len(partial_dependencies["x"][0]["x"]) == 10
+    assert partial_dependencies["x"][1].shape == (10,)
+    assert partial_dependencies["x"][2].shape == (10,)
+
+
+def test_multiple_params(monkeypatch, space):
+    """Test computing for multiple param"""
+    mock_train_regressor(monkeypatch)
+
+    partial_dependencies = partial_dependency(
+        data, space, params=["x", "y"], n_samples=10
+    )
+    assert set(partial_dependencies.keys()) == {"x", ("x", "y"), "y"}
+    assert len(partial_dependencies["x"][0]["x"]) == 10
+    assert len(partial_dependencies["y"][0]["y"]) == 10
+    assert len(partial_dependencies[("x", "y")][0]["x"]) == 10
+    assert len(partial_dependencies[("x", "y")][0]["y"]) == 10
+    assert partial_dependencies["x"][1].shape == (10,)
+    assert partial_dependencies["x"][2].shape == (10,)
+    assert partial_dependencies["y"][1].shape == (10,)
+    assert partial_dependencies["y"][2].shape == (10,)
+    assert partial_dependencies[("x", "y")][1].shape == (10, 10)
+    assert partial_dependencies[("x", "y")][2].shape == (10, 10)
+
+
+def test_multidim(monkeypatch, hspace):
+    """Test computing for multiple params in multidim space"""
+    mock_train_regressor(monkeypatch)
+
+    partial_dependencies = partial_dependency(
+        hdata, hspace, params=["x", "y[0]", "y[1]"], n_samples=10
+    )
+    assert set(partial_dependencies.keys()) == {
+        "x",
+        "y[0]",
+        "y[1]",
+        ("x", "y[0]"),
+        ("x", "y[1]"),
+        ("y[0]", "y[1]"),
+    }
+    assert len(partial_dependencies["x"][0]["x"]) == 10
+    assert len(partial_dependencies["y[0]"][0]["y[0]"]) == 10
+    assert len(partial_dependencies["y[1]"][0]["y[1]"]) == 10
+    assert len(partial_dependencies[("x", "y[0]")][0]["x"]) == 10
+    assert len(partial_dependencies[("x", "y[0]")][0]["y[0]"]) == 10
+    assert partial_dependencies["x"][1].shape == (10,)
+    assert partial_dependencies["x"][2].shape == (10,)
+    assert partial_dependencies["y[0]"][1].shape == (10,)
+    assert partial_dependencies["y[0]"][2].shape == (10,)
+    assert partial_dependencies[("x", "y[0]")][1].shape == (10, 10)
+    assert partial_dependencies[("x", "y[0]")][2].shape == (10, 10)
+
+
+def test_categorical(monkeypatch, space):
+    """Test computing for categorical"""
+    mock_train_regressor(monkeypatch)
+
+    partial_dependencies = partial_dependency(data, space, params=["z"], n_samples=10)
+
+    assert set(partial_dependencies.keys()) == {"z"}
+    assert partial_dependencies["z"][0]["z"] == ["a", "b", "c"]
+    assert partial_dependencies["z"][1].shape == (3,)
+    assert partial_dependencies["z"][2].shape == (3,)
+
+
+def test_n_grid_points(monkeypatch, space):
+    """Test that number of points on grid is correct, including adjustment for categorical dims"""
+    mock_train_regressor(monkeypatch)
+
+    n_grid_points = 5
+    partial_dependencies = partial_dependency(
+        data, space, params=["x", "z"], n_grid_points=n_grid_points, n_samples=10
+    )
+
+    assert set(partial_dependencies.keys()) == {"x", ("x", "z"), "z"}
+    assert partial_dependencies["z"][1].shape == (3,)
+    assert partial_dependencies["z"][2].shape == (3,)
+
+    assert len(partial_dependencies["x"][0]["x"]) == n_grid_points
+    assert len(partial_dependencies[("x", "z")][0]["x"]) == n_grid_points
+    assert partial_dependencies[("x", "z")][0]["z"] == ["a", "b", "c"]
+    assert partial_dependencies["z"][0]["z"] == ["a", "b", "c"]
+    assert partial_dependencies["x"][1].shape == (n_grid_points,)
+    assert partial_dependencies["x"][2].shape == (n_grid_points,)
+    assert partial_dependencies["z"][1].shape == (3,)
+    assert partial_dependencies["z"][2].shape == (3,)
+    assert partial_dependencies[("x", "z")][1].shape == (3, n_grid_points)
+    assert partial_dependencies[("x", "z")][2].shape == (3, n_grid_points)
+
+
+def test_n_samples(monkeypatch, space):
+    """Test n_samples is used properly when sampling random points"""
+    mock_train_regressor(monkeypatch)
+
+    PARAMS = ["x", "z"]
+    N_SAMPLES = numpy.random.randint(20, 50)
+
+    def mock_partial_dependency_grid(space, model, params, samples, n_points):
+        assert samples.shape == (N_SAMPLES, len(space))
+        return partial_dependency_grid(space, model, params, samples, n_points)
+
+    monkeypatch.setattr(
+        "orion.analysis.partial_dependency_utils.partial_dependency_grid",
+        mock_partial_dependency_grid,
+    )
+
+    partial_dependency(data, space, params=PARAMS, n_grid_points=5, n_samples=N_SAMPLES)
diff --git a/tests/unittests/analysis/test_regret.py b/tests/unittests/analysis/test_regret.py
index 63c0986b..7dbc0d9e 100644
--- a/tests/unittests/analysis/test_regret.py
+++ b/tests/unittests/analysis/test_regret.py
@@ -3,7 +3,7 @@
 import pandas as pd
 import pytest
 
-from orion.analysis.regret import regret
+from orion.analysis import regret
 
 
 def test_accept_dict():
diff --git a/tests/unittests/plotting/test_plotly_backend.py b/tests/unittests/plotting/test_plotly_backend.py
index 7759382d..fbc2a999 100644
--- a/tests/unittests/plotting/test_plotly_backend.py
+++ b/tests/unittests/plotting/test_plotly_backend.py
@@ -1,12 +1,14 @@
 """Collection of tests for :mod:`orion.plotting.backend_plotly`."""
 import copy
 
+import numpy
 import pandas
 import plotly
 import pytest
 
+from orion.analysis.partial_dependency_utils import partial_dependency_grid
 from orion.core.worker.experiment import Experiment
-from orion.plotting.base import lpi, parallel_coordinates, regret
+from orion.plotting.base import lpi, parallel_coordinates, partial_dependencies, regret
 from orion.testing import create_experiment
 
 config = dict(
@@ -67,13 +69,15 @@ def assert_regret_plot(plot):
 def mock_space(x="uniform(0, 6)", y="uniform(0, 3)", **kwargs):
     """Build a mocked space"""
     mocked_config = copy.deepcopy(config)
-    mocked_config["space"] = {"x": x, "y": y}
+    mocked_config["space"] = {"x": x}
+    if y is not None:
+        mocked_config["space"]["y"] = y
     mocked_config["space"].update(kwargs)
     return mocked_config
 
 
 def mock_experiment(
-    monkeypatch, ids=None, x=None, y=None, objectives=None, status=None
+    monkeypatch, ids=None, x=None, y=None, z=None, objectives=None, status=None
 ):
     """Mock experiment to_pandas to return given data (or default one)"""
     if ids is None:
@@ -87,22 +91,59 @@ def mock_experiment(
     if status is None:
         status = ["completed", "completed", "completed", "completed"]
 
+    data = {
+        "id": ids,
+        "x": x,
+        "objective": objectives,
+        "status": status,
+    }
+
+    if not isinstance(y, str):
+        data["y"] = y
+    if z is not None:
+        data["z"] = z
+
     def to_pandas(self, with_evc_tree=False):
-        data = pandas.DataFrame(
-            data={
-                "id": ids,
-                "x": x,
-                "y": y,
-                "objective": objectives,
-                "status": status,
-            }
-        )
 
-        return data
+        return pandas.DataFrame(data=data)
 
     monkeypatch.setattr(Experiment, "to_pandas", to_pandas)
 
 
+def mock_model():
+    """Return a mocked regressor which just predict iterated integers"""
+
+    class Model:
+        """Mocked Regressor"""
+
+        def __init__(self):
+            self.i = 0
+
+        def predict(self, data):
+            """Returns counting of predictions requested."""
+            data = numpy.arange(data.shape[0]) + self.i
+            self.i += data.shape[0]
+            return data  #  + numpy.random.normal(0, self.i, size=data.shape[0])
+
+    return Model()
+
+
+def mock_train_regressor(monkeypatch, assert_model=None, assert_model_kwargs=None):
+    """Mock the train_regressor to return the mocked regressor instead"""
+
+    def train_regressor(model, data, **kwargs):
+        """Return the mocked model, and then model argument if requested"""
+        if assert_model:
+            assert model == assert_model
+        if assert_model_kwargs:
+            assert kwargs == assert_model_kwargs
+        return mock_model()
+
+    monkeypatch.setattr(
+        "orion.analysis.partial_dependency_utils.train_regressor", train_regressor
+    )
+
+
 def assert_lpi_plot(plot, dims):
     """Checks the layout of a LPI plot"""
     assert plot.layout.title.text == "LPI for experiment 'experiment-name'"
@@ -114,6 +155,181 @@ def assert_lpi_plot(plot, dims):
     assert trace["y"][0] > trace["y"][1]
 
 
+def assert_partial_dependencies_plot(
+    plot,
+    dims,
+    custom_colorscale=False,
+    smoothing=0.85,
+    n_grid_points=5,
+    n_samples=4,
+    log_dims=None,
+):
+    """Checks the layout of a partial dependencies plot"""
+    if not isinstance(n_grid_points, dict):
+        n_grid_points = {dim: n_grid_points for dim in dims}
+    if log_dims is None:
+        log_dims = {}
+
+    def _ax_label(axis, index):
+        if index == 0:
+            return f"{axis}axis"
+
+        return f"{axis}axis{index + 1}"
+
+    def _ax_layout(axis, index):
+        return plot.layout[_ax_label(axis, index)]
+
+    assert (
+        plot.layout.title.text
+        == "Partial dependencies for experiment 'experiment-name'"
+    )
+
+    assert plot.layout.coloraxis.colorbar.title.text == "Objective"
+    assert plot.layout.yaxis.title.text == "Objective"
+
+    yrange = _ax_layout("y", 0).range
+
+    def all_indices():
+        return {
+            j * len(dims) + i + 1 for i in range(len(dims)) for j in range(i, len(dims))
+        }
+
+    def first_column():
+        return {i * len(dims) + 1 for i in range(len(dims))}
+
+    def last_row():
+        return {len(dims) * (len(dims) - 1) + i + 1 for i in range(len(dims))}
+
+    def diagonal():
+        return {i * len(dims) + i + 1 for i in range(len(dims))}
+
+    def assert_axis_log(axis, index, name):
+        axis_type = _ax_layout(axis, index).type
+        if name in log_dims:
+            assert axis_type == "log"
+        else:
+            assert axis_type != "log"
+
+    def assert_log_x():
+        x_tested = set()
+        for dim_i, dim_name in enumerate(dims):
+            x_index = dim_i * len(dims) + dim_i
+            for row in range(dim_i, len(dims)):
+                assert_axis_log("x", x_index, dim_name)
+                x_tested.add(x_index + 1)
+                x_index += len(dims)
+
+        assert x_tested == all_indices()
+
+    assert_log_x()
+
+    def assert_shared_y_on_diagonal():
+        y_tested = set()
+        for dim_i, dim_name in enumerate(dims):
+            # Test shared y axis across the diagonal
+            y_index = dim_i * len(dims) + dim_i
+            assert _ax_layout("y", y_index).range == yrange
+            y_tested.add(y_index + 1)
+
+        assert y_tested == diagonal()
+
+    assert_shared_y_on_diagonal()
+
+    def assert_log_y():
+        y_tested = set()
+        for dim_i, dim_name in enumerate(dims):
+            # Test shared y axis across the diagonal
+            y_index = dim_i * len(dims) + dim_i
+            # Should not be log
+            assert_axis_log("y", y_index, None)
+            y_tested.add(y_index + 1)
+
+            y_index = dim_i * len(dims)
+            for column in range(max(dim_i, 0)):
+                assert_axis_log("y", y_index, dim_name)
+                y_tested.add(y_index + 1)
+                y_index += 1
+
+        assert y_tested == all_indices()
+
+    assert_log_y()
+
+    def assert_x_labels():
+        x_tested = set()
+        for dim_i, dim_name in enumerate(dims):
+            x_index = len(dims) * (len(dims) - 1) + dim_i
+            assert _ax_layout("x", x_index).title.text == dim_name
+
+        assert x_tested == last_row()
+
+    def assert_y_labels():
+        y_tested = set()
+        for dim_i, dim_name in enumerate(dims):
+            if dim_i > 0:
+                # Test lable at left of row
+                y_index = dim_i * len(dims)
+                assert _ax_layout("y", y_index).title.text == dim_name
+                y_tested.add(y_index + 1)
+            else:
+                assert _ax_layout("y", 0).title.text == "Objective"
+                y_tested.add(1)
+
+        assert y_tested == first_column()
+
+    assert_y_labels()
+
+    # assert x_tested == {1, 4, 5, 7, 8, 9}
+    # assert y_tested == {1, 4, 5, 7, 8, 9}
+
+    if custom_colorscale:
+        assert plot.layout.coloraxis.colorscale[0][1] != "rgb(247,251,255)"
+    else:
+        assert plot.layout.coloraxis.colorscale[0][1] == "rgb(247,251,255)"
+
+    data = plot.data
+    data_index = 0
+    for x_i, x_name in enumerate(dims):
+
+        # Test scatter mean
+        assert data[data_index].mode == "lines"
+        assert data[data_index].showlegend is False
+        assert len(data[data_index].x) == n_grid_points[x_name]
+        assert len(data[data_index].y) == n_grid_points[x_name]
+        data_index += 1
+        # Test scatter var
+        assert data[data_index].mode == "lines"
+        assert data[data_index].fill == "toself"
+        assert data[data_index].showlegend is False
+        assert len(data[data_index].x) == 2 * n_grid_points[x_name]
+        assert len(data[data_index].y) == 2 * n_grid_points[x_name]
+        data_index += 1
+
+        for y_i in range(x_i + 1, len(dims)):
+            y_name = dims[y_i]
+
+            # Test contour
+            assert data[data_index].line.smoothing == smoothing
+            # To share colorscale across subplots
+            assert data[data_index].coloraxis == "coloraxis"
+            assert len(data[data_index].x) == n_grid_points[x_name]
+            assert len(data[data_index].y) == n_grid_points[y_name]
+            assert data[data_index].z.shape == (
+                n_grid_points[y_name],
+                n_grid_points[x_name],
+            )
+            data_index += 1
+
+            # Test scatter
+            assert data[data_index].mode == "markers"
+            assert data[data_index].showlegend is False
+            assert len(data[data_index].x) == n_samples
+            assert len(data[data_index].y) == n_samples
+            data_index += 1
+
+    # Make sure we covered all data
+    assert len(data) == data_index
+
+
 @pytest.mark.usefixtures("version_XYZ")
 class TestLPI:
     """Tests the ``lpi()`` method provided by the plotly backend"""
@@ -130,7 +346,7 @@ class TestLPI:
             _,
             experiment,
         ):
-            plot = lpi(experiment, random_state=1)
+            plot = lpi(experiment, model_kwargs=dict(random_state=1))
 
         assert type(plot) is plotly.graph_objects.Figure
 
@@ -143,8 +359,7 @@ class TestLPI:
             _,
             experiment,
         ):
-            plot = lpi(experiment, random_state=1)
-            # Why is this test failing?
+            plot = lpi(experiment, model_kwargs=dict(random_state=1))
             df = experiment.to_pandas()
             assert df["x"].tolist() == [0, 1, 2, 4]
             assert df["y"].tolist() == [3, 2, 0, 1]
@@ -161,7 +376,7 @@ class TestLPI:
             experiment,
             _,
         ):
-            plot = lpi(experiment, random_state=1)
+            plot = lpi(experiment, model_kwargs=dict(random_state=1))
 
         assert_lpi_plot(plot, dims=["x", "y"])
 
@@ -231,6 +446,324 @@ class TestLPI:
         assert_lpi_plot(plot, dims=["x", "y[0]", "y[1]", "y[2]"])
 
 
+@pytest.mark.usefixtures("version_XYZ")
+class TestPartialDependencies:
+    """Tests the ``partial_dependencies()`` method provided by the plotly backend"""
+
+    def test_returns_plotly_object(self, monkeypatch):
+        """Tests that the plotly backend returns a plotly object"""
+        mock_train_regressor(monkeypatch)
+        with create_experiment(config, trial_config, ["completed"]) as (
+            _,
+            _,
+            experiment,
+        ):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        assert type(plot) is plotly.graph_objects.Figure
+
+    def test_graph_layout(self, monkeypatch):
+        """Tests the layout of the plot"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space()
+        mock_experiment(monkeypatch)
+        with create_experiment(config, trial_config, ["completed"]) as (
+            _,
+            _,
+            experiment,
+        ):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+            df = experiment.to_pandas()
+            assert df["x"].tolist() == [0, 1, 2, 4]
+            assert df["y"].tolist() == [3, 2, 0, 1]
+            assert df["objective"].tolist() == [0.1, 0.2, 0.3, 0.5]
+
+        assert_partial_dependencies_plot(plot, dims=["x", "y"])
+
+    def test_ignore_uncompleted_statuses(self, monkeypatch):
+        """Tests that uncompleted statuses are ignored"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space()
+        mock_experiment(
+            monkeypatch,
+            ids="abcdefgh",
+            x=[0, 0, 0, 1, 0, 2, 0, 3],
+            y=[1, 0, 0, 2, 0, 0, 0, 3],
+            objectives=[0.1, None, None, 0.2, None, 0.3, None, 0.5],
+            status=[
+                "completed",
+                "new",
+                "reserved",
+                "completed",
+                "broken",
+                "completed",
+                "interrupted",
+                "completed",
+            ],
+        )
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(experiment, n_grid_points=5)
+
+        assert_partial_dependencies_plot(plot, dims=["x", "y"])
+
+    def test_multidim(self, monkeypatch):
+        """Tests that dimensions with shape > 1 are flattened properly"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space(y="uniform(0, 3, shape=2)")
+        mock_experiment(monkeypatch, y=[[3, 3], [2, 3], [1, 2], [0, 3]])
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        assert_partial_dependencies_plot(plot, dims=["x", "y[0]", "y[1]"])
+
+    def test_fidelity(self, monkeypatch):
+        """Tests that fidelity is supported"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space(y="fidelity(1, 200, base=3)")
+        mock_experiment(monkeypatch, y=[1, 3 ** 2, 1, 3 ** 4])
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        assert_partial_dependencies_plot(plot, dims=["x", "y"])
+
+    def test_categorical(self, monkeypatch):
+        """Tests that categorical is supported"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space(y='choices(["a", "b", "c"])')
+        mock_experiment(monkeypatch, y=["c", "c", "a", "b"])
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        # There is only 3 categories, so test must be adjusted accordingly.
+        assert_partial_dependencies_plot(
+            plot, dims=["x", "y"], n_grid_points={"x": 5, "y": 3}
+        )
+
+    def test_categorical_multidim(self, monkeypatch):
+        """Tests that multidim categorical is supported"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space(y='choices(["a", "b", "c"], shape=3)')
+        mock_experiment(
+            monkeypatch,
+            y=[["c", "b", "a"], ["c", "a", "c"], ["a", "b", "a"], ["c", "b", "b"]],
+        )
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        assert_partial_dependencies_plot(
+            plot,
+            dims=["x", "y[0]", "y[1]", "y[2]"],
+            n_grid_points={"x": 5, "y[0]": 3, "y[1]": 3, "y[2]": 3},
+        )
+
+    def test_logarithmic_scales_first(self, monkeypatch):
+        """Test that log dims are turn to log scale
+
+        Test first dim specifically because special xaxis name for first dim.
+        """
+        mock_train_regressor(monkeypatch)
+        config = mock_space(x="loguniform(0.001, 1)", z="uniform(0, 1)")
+        mock_experiment(monkeypatch, x=[0.001, 0.1, 0.01, 1], z=[0, 0.1, 0.2, 0.5])
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        assert_partial_dependencies_plot(
+            plot, dims=["x", "y", "z"], n_grid_points=5, log_dims=["x"]
+        )
+
+    def test_logarithmic_scales_any_dim(self, monkeypatch):
+        """Test that log dims are turn to log scale"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space(y="loguniform(0.001, 1)", z="uniform(0, 1)")
+        mock_experiment(monkeypatch, y=[0.001, 0.1, 0.01, 1], z=[0, 0.1, 0.2, 0.5])
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        assert_partial_dependencies_plot(
+            plot, dims=["x", "y", "z"], n_grid_points=5, log_dims=["y"]
+        )
+
+    def test_int_logarithmic_scales(self, monkeypatch):
+        """Test that int log dims are turn to log scale"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space(y="loguniform(1, 1000, discrete=True)", z="uniform(0, 1)")
+        mock_experiment(monkeypatch, y=[1, 10, 100, 1000], z=[0, 0.1, 0.2, 0.5])
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        assert_partial_dependencies_plot(
+            plot, dims=["x", "y", "z"], n_grid_points=5, log_dims=["y"]
+        )
+
+    def test_one_param(self, monkeypatch):
+        """Test ploting a space with only 1 dim"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space(y=None)
+        mock_experiment(monkeypatch, y="drop")
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment, n_grid_points=5, model_kwargs=dict(random_state=1)
+            )
+
+        assert_partial_dependencies_plot(plot, dims=["x"], n_grid_points=5)
+
+    def test_select_params(self, monkeypatch):
+        """Test selecting subset"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space(z="uniform(0, 1)")
+        mock_experiment(monkeypatch, z=[0, 0.1, 0.2, 0.5])
+
+        for params in [["x"], ["x", "y"], ["y", "z"]]:
+            with create_experiment(config, trial_config) as (_, _, experiment):
+                plot = partial_dependencies(
+                    experiment,
+                    params=params,
+                    n_grid_points=5,
+                    model_kwargs=dict(random_state=1),
+                )
+
+            assert_partial_dependencies_plot(plot, dims=params, n_grid_points=5)
+
+    def test_custom_smoothing(self, monkeypatch):
+        """Test changing smoothing value"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space()
+        mock_experiment(monkeypatch)
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment,
+                n_grid_points=5,
+                model_kwargs=dict(random_state=1),
+                smoothing=1.2,
+            )
+
+        with pytest.raises(AssertionError):
+            assert_partial_dependencies_plot(plot, dims=["x", "y"], n_grid_points=5)
+
+        assert_partial_dependencies_plot(
+            plot, dims=["x", "y"], n_grid_points=5, smoothing=1.2
+        )
+
+    def test_custom_n_grid_points(self, monkeypatch):
+        """Test changing n_grid_points value"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space()
+        mock_experiment(monkeypatch)
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment,
+                n_grid_points=10,
+                model_kwargs=dict(random_state=1),
+            )
+
+        with pytest.raises(AssertionError):
+            assert_partial_dependencies_plot(plot, dims=["x", "y"], n_grid_points=5)
+
+        assert_partial_dependencies_plot(plot, dims=["x", "y"], n_grid_points=10)
+
+    def test_custom_n_samples(self, monkeypatch):
+        """Test changing n_samples value"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space()
+        mock_experiment(monkeypatch)
+
+        PARAMS = ["x", "y"]
+        N_SAMPLES = numpy.random.randint(20, 50)
+
+        def mock_partial_dependency_grid(space, model, params, samples, n_points):
+            print(samples)
+            assert samples.shape == (N_SAMPLES, len(PARAMS))
+            return partial_dependency_grid(space, model, params, samples, n_points)
+
+        monkeypatch.setattr(
+            "orion.analysis.partial_dependency_utils.partial_dependency_grid",
+            mock_partial_dependency_grid,
+        )
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment,
+                n_grid_points=10,
+                model_kwargs=dict(random_state=1),
+                n_samples=N_SAMPLES,
+            )
+
+    def test_custom_colorscale(self, monkeypatch):
+        """Test changing colorscale"""
+        mock_train_regressor(monkeypatch)
+        config = mock_space()
+        mock_experiment(monkeypatch)
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment,
+                n_grid_points=5,
+                colorscale="Viridis",
+                model_kwargs=dict(random_state=1),
+            )
+
+        with pytest.raises(AssertionError):
+            assert_partial_dependencies_plot(
+                plot, dims=["x", "y"], n_grid_points=5, custom_colorscale=False
+            )
+
+        assert_partial_dependencies_plot(
+            plot, dims=["x", "y"], n_grid_points=5, custom_colorscale=True
+        )
+
+    def test_custom_model(self, monkeypatch):
+        """Test changing type of regression model"""
+        mock_train_regressor(monkeypatch, assert_model="BaggingRegressor")
+        config = mock_space()
+        mock_experiment(monkeypatch)
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment,
+                n_grid_points=5,
+                model="BaggingRegressor",
+                model_kwargs=dict(random_state=1),
+            )
+
+    def test_custom_model_kwargs(self, monkeypatch):
+        """Test changing arguments of regression model"""
+        mock_train_regressor(monkeypatch, assert_model_kwargs=dict(random_state=1))
+        config = mock_space()
+        mock_experiment(monkeypatch)
+
+        with create_experiment(config, trial_config) as (_, _, experiment):
+            plot = partial_dependencies(
+                experiment,
+                n_grid_points=5,
+                model_kwargs=dict(random_state=1),
+            )
+
+
 @pytest.mark.usefixtures("version_XYZ")
 class TestRegret:
     """Tests the ``regret()`` method provided by the plotly backend"""