Repository: optuna/optuna
Issue category: Bug Report

Axis tick scale in `plot_contour` with Matplotlib backend
### Expected behavior

When I plot with `optuna.visualization.matplotlib.plot_contour`, the horizontal and vertical ticks are set for each subfigure.

The subfigures on the diagonal are empty (as it should be), but the ticks are always scaled linearly. This is inconsistent with the off-diagonal subfigures in the corresponding columns/rows, where the axis ticks are logarithmic for logarithmic parameters.

This is only a problem (annoyance) in the first row and the last column, since the tick labels (which are rendered on the left and the bottom of the figure) do not correspond to the actual subfigures they should be describing.

### Environment

- Optuna version: 4.0.0
- Python version: 3.12.7
- OS: Linux-6.11.7-arch1-1-x86_64-with-glibc2.40
- (Optional) Other libraries and their versions:
    - Matplotlib: 3.9.2


### Error messages, stack traces, or logs

```shell
-
```


### Steps to reproduce

1. Create a study with at least 3 parameters that has logarithmic scale
2. Load and plot:

```python
import optuna
study = optuna.load_study(...)
import optuna.visualization.matplotlib as vis
vis.plot_contour(study)
import matplotlib.pyplot as plt
plt.show()
```


### Additional context (optional)

This bug does not appear with the Plotly backend.


Functions to edit:
optuna/visualization/matplotlib/_contour.py:_calculate_axis_data
optuna/visualization/matplotlib/_contour.py:_calculate_griddata
optuna/visualization/matplotlib/_contour.py:_generate_contour_subplot
Functions added: optuna/visualization/matplotlib/_contour.py:_filter_missing_values

Patch:
diff --git a/optuna/visualization/matplotlib/_contour.py b/optuna/visualization/matplotlib/_contour.py
index df2f38efd0..f729ffde9c 100644
--- a/optuna/visualization/matplotlib/_contour.py
+++ b/optuna/visualization/matplotlib/_contour.py
@@ -153,6 +153,43 @@ def get_indices(self) -> list[int]:
         return list(range(len(self.labels)))
 
 
+def _filter_missing_values(
+    xaxis: _AxisInfo, yaxis: _AxisInfo
+) -> tuple[list[str | float], list[str | float]]:
+    x_values = []
+    y_values = []
+    for x_value, y_value in zip(xaxis.values, yaxis.values):
+        if x_value is not None and y_value is not None:
+            x_values.append(x_value)
+            y_values.append(y_value)
+    return x_values, y_values
+
+
+def _calculate_axis_data(
+    axis: _AxisInfo,
+    values: Sequence[str | float],
+) -> tuple[np.ndarray, list[str], list[int], list[int | float]]:
+    # Convert categorical values to int.
+    cat_param_labels: list[str] = []
+    cat_param_pos: list[int] = []
+    returned_values: Sequence[int | float]
+    if axis.is_cat:
+        enc = _LabelEncoder()
+        returned_values = enc.fit_transform(list(map(str, values)))
+        cat_param_labels = enc.get_labels()
+        cat_param_pos = enc.get_indices()
+    else:
+        returned_values = list(map(lambda x: float(x), values))
+
+    # For x and y, create 1-D array of evenly spaced coordinates on linear or log scale.
+    if axis.is_log:
+        ci = np.logspace(np.log10(axis.range[0]), np.log10(axis.range[1]), CONTOUR_POINT_NUM)
+    else:
+        ci = np.linspace(axis.range[0], axis.range[1], CONTOUR_POINT_NUM)
+
+    return ci, cat_param_labels, cat_param_pos, list(returned_values)
+
+
 def _calculate_griddata(
     info: _SubContourInfo,
 ) -> tuple[
@@ -195,30 +232,6 @@ def _calculate_griddata(
             _PlotValues([], []),
         )
 
-    def _calculate_axis_data(
-        axis: _AxisInfo,
-        values: Sequence[str | float],
-    ) -> tuple[np.ndarray, list[str], list[int], list[int | float]]:
-        # Convert categorical values to int.
-        cat_param_labels: list[str] = []
-        cat_param_pos: list[int] = []
-        returned_values: Sequence[int | float]
-        if axis.is_cat:
-            enc = _LabelEncoder()
-            returned_values = enc.fit_transform(list(map(str, values)))
-            cat_param_labels = enc.get_labels()
-            cat_param_pos = enc.get_indices()
-        else:
-            returned_values = list(map(lambda x: float(x), values))
-
-        # For x and y, create 1-D array of evenly spaced coordinates on linear or log scale.
-        if axis.is_log:
-            ci = np.logspace(np.log10(axis.range[0]), np.log10(axis.range[1]), CONTOUR_POINT_NUM)
-        else:
-            ci = np.linspace(axis.range[0], axis.range[1], CONTOUR_POINT_NUM)
-
-        return ci, cat_param_labels, cat_param_pos, list(returned_values)
-
     xi, cat_param_labels_x, cat_param_pos_x, transformed_x_values = _calculate_axis_data(
         xaxis,
         x_values,
@@ -271,6 +284,19 @@ def _generate_contour_subplot(
     ax.set(xlabel=info.xaxis.name, ylabel=info.yaxis.name)
     ax.set_xlim(info.xaxis.range[0], info.xaxis.range[1])
     ax.set_ylim(info.yaxis.range[0], info.yaxis.range[1])
+    x_values, y_values = _filter_missing_values(info.xaxis, info.yaxis)
+    if info.xaxis.is_cat:
+        _, x_cat_param_label, x_cat_param_pos, _ = _calculate_axis_data(info.xaxis, x_values)
+        ax.set_xticks(x_cat_param_pos)
+        ax.set_xticklabels(x_cat_param_label)
+    else:
+        ax.set_xscale("log" if info.xaxis.is_log else "linear")
+    if info.yaxis.is_cat:
+        _, y_cat_param_label, y_cat_param_pos, _ = _calculate_axis_data(info.yaxis, y_values)
+        ax.set_yticks(y_cat_param_pos)
+        ax.set_yticklabels(y_cat_param_label)
+    else:
+        ax.set_yscale("log" if info.yaxis.is_log else "linear")
 
     if info.xaxis.name == info.yaxis.name:
         ax.label_outer()
@@ -318,12 +344,6 @@ def _generate_contour_subplot(
                 linewidth=2.0,
             )
 
-    if info.xaxis.is_cat:
-        ax.set_xticks(x_cat_param_pos)
-        ax.set_xticklabels(x_cat_param_label)
-    if info.yaxis.is_cat:
-        ax.set_yticks(y_cat_param_pos)
-        ax.set_yticklabels(y_cat_param_label)
     ax.label_outer()
     return cs