diff --git a/CHANGELOG.md b/CHANGELOG.md index 709c6f6..1a186ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added - Default values for NParEGO scalarization_weights - SHAP PDP ICE plots now work with categorical values +- Added scikit-learn to dependencies, for MDS +- Added MDS plot ### Modified - SHAP PDP ICE plots must now have color and x-axis indices that are distinct diff --git a/obsidian/campaign/campaign.py b/obsidian/campaign/campaign.py index b99e190..e3c05f2 100644 --- a/obsidian/campaign/campaign.py +++ b/obsidian/campaign/campaign.py @@ -99,6 +99,7 @@ def add_data(self, df: pd.DataFrame): def clear_data(self): """Clears campaign data""" self.data = pd.DataFrame() + self.iter = 0 @property def optimizer(self) -> Optimizer: diff --git a/obsidian/plotting/plotly.py b/obsidian/plotting/plotly.py index 5dfccef..f789730 100644 --- a/obsidian/plotting/plotly.py +++ b/obsidian/plotting/plotly.py @@ -8,11 +8,61 @@ import plotly.graph_objects as go from plotly.graph_objects import Figure +from sklearn.manifold import MDS import pandas as pd import numpy as np +def MDS_plot(campaign: Campaign) -> Figure: + """ + Creates a Multi-Dimensional Scaling (MDS) plot of the campaign data, + colored by iteration. + + This plot is helpful to visualize the convergence of the optimizer on a 2D plane. + + Args: + campaign (Campaign): The campaign object containing the data. + + Returns: + fig (Figure): The MDS plot + """ + mds = MDS(n_components=2) + X_mds = mds.fit_transform(campaign.X_space.encode(campaign.X)) + + iter_max = campaign.data['Iteration'].max() + iter_vals = campaign.data['Iteration'].values + + if campaign.data['Iteration'].nunique() == 1: + iter_vals = np.zeros_like(iter_vals) + iter_max = 0 + cbar = None + else: + cbar = dict(title=dict(text='Iteration', font=dict(size=10))) + + fig = go.Figure() + + fig.add_trace(go.Scatter(x=X_mds[:, 0], y=X_mds[:, 1], + mode='markers', + name='Observations', + marker={'color': iter_vals, 'size': 10, + 'cmax': iter_max, 'cmin': 0, + 'colorscale': [[0, obsidian_colors.rich_blue], + [0.5, obsidian_colors.teal], + [1, obsidian_colors.lemon]], + 'colorbar': cbar + }, + showlegend=False + )) + + fig.update_xaxes(title_text='Component 1') + fig.update_yaxes(title_text='Component 2') + fig.update_layout(template='ggplot2', title='Multi-Dimensional Scaling (MDS) Plot', + autosize=False, height=400, width=500) + + return fig + + def parity_plot(optimizer: Optimizer, f_transform: bool = False, response_id: int = 0) -> Figure: diff --git a/obsidian/tests/test_plotting.py b/obsidian/tests/test_plotting.py index bc2cab5..b8a1eb0 100644 --- a/obsidian/tests/test_plotting.py +++ b/obsidian/tests/test_plotting.py @@ -6,7 +6,8 @@ factor_plot, surface_plot, visualize_inputs, - optim_progress + optim_progress, + MDS_plot ) from obsidian.objectives import Scalar_WeightedSum @@ -75,6 +76,19 @@ def test_optim_progress_plot(): obj = Scalar_WeightedSum(weights=[1, 1]) campaign.set_objective(obj) fig = optim_progress(campaign, X_suggest=X_suggest) + + +@pytest.mark.fast +def test_MDS_plot(): + # Test with default data + fig = MDS_plot(campaign) + + # Test with no iteration data + data = campaign.data + data = data.drop(columns='Iteration') + campaign.clear_data() + campaign.add_data(data) + fig = MDS_plot(campaign) if __name__ == '__main__': diff --git a/pyproject.toml b/pyproject.toml index 8002663..3a487b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ sphinx = { version = "^7.3.7", optional = true} myst-parser = { version = "^3.0.1", optional = true} pydata-sphinx-theme = { version = "^0.15.4", optional = true} linkify-it-py = { version = "^2.0.3", optional = true} +scikit-learn = "^1.5.1" [tool.poetry.extras]