Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Nov 7, 2025

📄 13% (0.13x) speedup for _filter_missing_values in optuna/visualization/matplotlib/_contour.py

⏱️ Runtime : 447 microseconds 394 microseconds (best of 250 runs)

📝 Explanation and details

The optimization achieves a 13% speedup by eliminating repeated attribute lookups in the inner loop through local variable binding.

Key optimization: The code caches x_values.append and y_values.append as local variables append_x and append_y before the loop. This avoids Python's costly attribute resolution on every iteration - instead of looking up the append method 6,329 times each (as shown in the profiler), the lookup happens only once per list.

Performance impact: The line profiler shows the append operations dropped from 20.2% + 20.8% = 41% of total time to 18.6% + 19.9% = 38.4% of total time. This 2.6 percentage point reduction in the most expensive operations drives the overall speedup.

Test case performance: The optimization shows strongest gains on large datasets (18-20% faster on 1000+ element tests) where the loop dominates execution time. Smaller datasets see modest slowdowns (2-17%) due to the overhead of creating local variables, but this is negligible in real-world usage where contour plots typically process hundreds or thousands of data points.

Why this works: Python's attribute lookup involves dictionary searches and method binding. By caching these expensive operations outside the loop, we convert O(n) attribute lookups to O(1), making the inner loop more CPU-efficient while preserving identical behavior and output.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 38 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
🌀 Generated Regression Tests and Runtime
from typing import Any, List

# imports
import pytest  # used for our unit tests
from optuna.visualization.matplotlib._contour import _filter_missing_values


# minimal _AxisInfo class for testing
class _AxisInfo:
    def __init__(self, values: List[Any]):
        self.values = values
from optuna.visualization.matplotlib._contour import _filter_missing_values

# unit tests

# --- Basic Test Cases ---

def test_basic_all_values_present():
    # Both axes contain only valid values (no None)
    x = _AxisInfo([1, 2, 3])
    y = _AxisInfo([4, 5, 6])
    x_out, y_out = _filter_missing_values(x, y) # 1.43μs -> 1.46μs (2.19% slower)

def test_basic_some_missing_values():
    # Some values are None in both axes
    x = _AxisInfo([1, None, 3])
    y = _AxisInfo([4, 5, None])
    x_out, y_out = _filter_missing_values(x, y) # 1.26μs -> 1.31μs (4.18% slower)

def test_basic_strings_and_floats():
    # Mix of string and float values
    x = _AxisInfo(['a', 2.5, None, 'b'])
    y = _AxisInfo([None, 3.7, 4.1, 'c'])
    x_out, y_out = _filter_missing_values(x, y) # 1.33μs -> 1.38μs (2.91% slower)

def test_basic_empty_lists():
    # Both axes are empty
    x = _AxisInfo([])
    y = _AxisInfo([])
    x_out, y_out = _filter_missing_values(x, y) # 820ns -> 976ns (16.0% slower)

def test_basic_no_valid_pairs():
    # All pairs contain at least one None
    x = _AxisInfo([None, None])
    y = _AxisInfo([None, 5])
    x_out, y_out = _filter_missing_values(x, y) # 982ns -> 1.11μs (11.9% slower)

# --- Edge Test Cases ---

def test_edge_all_none():
    # Both axes are all None
    x = _AxisInfo([None, None, None])
    y = _AxisInfo([None, None, None])
    x_out, y_out = _filter_missing_values(x, y) # 1.05μs -> 1.11μs (5.23% slower)

def test_edge_one_axis_all_none():
    # One axis is all None, other is valid
    x = _AxisInfo([None, None, None])
    y = _AxisInfo([1, 2, 3])
    x_out, y_out = _filter_missing_values(x, y) # 988ns -> 1.06μs (6.70% slower)

def test_edge_unequal_lengths_x_longer():
    # xaxis longer than yaxis
    x = _AxisInfo([1, 2, 3, 4])
    y = _AxisInfo([5, None])
    x_out, y_out = _filter_missing_values(x, y) # 1.15μs -> 1.28μs (10.3% slower)

def test_edge_unequal_lengths_y_longer():
    # yaxis longer than xaxis
    x = _AxisInfo([None, 2])
    y = _AxisInfo([3, 4, 5])
    x_out, y_out = _filter_missing_values(x, y) # 1.10μs -> 1.17μs (5.74% slower)

def test_edge_all_values_none_in_one_position():
    # All pairs have None in the same position
    x = _AxisInfo([None, 1, None, 2])
    y = _AxisInfo([3, None, 4, None])
    x_out, y_out = _filter_missing_values(x, y) # 1.09μs -> 1.13μs (3.44% slower)

def test_edge_types_preserved():
    # Check that types are preserved (float, str)
    x = _AxisInfo([1.1, 'x', None])
    y = _AxisInfo(['y', 2.2, None])
    x_out, y_out = _filter_missing_values(x, y) # 1.18μs -> 1.29μs (9.03% slower)

def test_edge_empty_and_nonempty():
    # One axis is empty, other is nonempty
    x = _AxisInfo([])
    y = _AxisInfo([1, 2, 3])
    x_out, y_out = _filter_missing_values(x, y) # 794ns -> 903ns (12.1% slower)

def test_edge_single_element_valid():
    # Single valid pair
    x = _AxisInfo([42])
    y = _AxisInfo(['answer'])
    x_out, y_out = _filter_missing_values(x, y) # 956ns -> 1.16μs (17.7% slower)

def test_edge_single_element_none():
    # Single pair with None
    x = _AxisInfo([None])
    y = _AxisInfo([1])
    x_out, y_out = _filter_missing_values(x, y) # 959ns -> 985ns (2.64% slower)

def test_edge_mixed_types():
    # Mixed types in axes
    x = _AxisInfo([1, 'a', None, 3.5])
    y = _AxisInfo(['b', 2, 3, None])
    x_out, y_out = _filter_missing_values(x, y) # 1.31μs -> 1.31μs (0.383% faster)

# --- Large Scale Test Cases ---

def test_large_scale_all_valid():
    # Large input, all valid
    N = 1000
    x = _AxisInfo(list(range(N)))
    y = _AxisInfo(list(range(N, 2*N)))
    x_out, y_out = _filter_missing_values(x, y) # 53.0μs -> 44.7μs (18.7% faster)

def test_large_scale_some_missing():
    # Large input, some None values interleaved
    N = 1000
    x_vals = [i if i % 10 != 0 else None for i in range(N)]
    y_vals = [j if j % 15 != 0 else None for j in range(N)]
    x = _AxisInfo(x_vals)
    y = _AxisInfo(y_vals)
    x_out, y_out = _filter_missing_values(x, y) # 50.7μs -> 43.0μs (17.7% faster)
    # Only pairs where both are not None
    for i, (xv, yv) in enumerate(zip(x_out, y_out)):
        pass
    # Check length is correct
    expected_count = sum(
        1 for i in range(N) if (i % 10 != 0) and (i % 15 != 0)
    )

def test_large_scale_all_none():
    # Large input, all None
    N = 1000
    x = _AxisInfo([None] * N)
    y = _AxisInfo([None] * N)
    x_out, y_out = _filter_missing_values(x, y) # 17.1μs -> 17.0μs (0.884% faster)

def test_large_scale_one_axis_none():
    # Large input, one axis all None
    N = 1000
    x = _AxisInfo([None] * N)
    y = _AxisInfo(list(range(N)))
    x_out, y_out = _filter_missing_values(x, y) # 17.9μs -> 18.0μs (0.755% slower)

def test_large_scale_unequal_lengths():
    # Large input, unequal lengths
    x = _AxisInfo(list(range(900)))
    y = _AxisInfo(list(range(1000)))
    x_out, y_out = _filter_missing_values(x, y) # 48.2μs -> 40.7μs (18.2% faster)

def test_large_scale_mixed_types():
    # Large input, alternating types and None
    N = 1000
    x_vals = [i if i % 2 == 0 else None for i in range(N)]
    y_vals = [str(i) if i % 3 == 0 else None for i in range(N)]
    x = _AxisInfo(x_vals)
    y = _AxisInfo(y_vals)
    x_out, y_out = _filter_missing_values(x, y) # 28.0μs -> 26.5μs (5.54% faster)
    # Only indices where both are not None
    for xv, yv in zip(x_out, y_out):
        pass
    # Length is number of i in 0..N-1 where i % 2 == 0 and i % 3 == 0
    expected_count = sum(1 for i in range(N) if i % 2 == 0 and i % 3 == 0)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
from typing import Any

# imports
import pytest  # used for our unit tests
from optuna.visualization.matplotlib._contour import _filter_missing_values


# Minimal _AxisInfo stub for testing
class _AxisInfo:
    def __init__(self, values):
        self.values = values
from optuna.visualization.matplotlib._contour import _filter_missing_values

# unit tests

# -------------------- Basic Test Cases --------------------

def test_all_values_present():
    # All values present, no missing values
    x = _AxisInfo([1, 2, 3])
    y = _AxisInfo([4, 5, 6])
    x_out, y_out = _filter_missing_values(x, y) # 1.21μs -> 1.34μs (9.81% slower)

def test_some_missing_values():
    # Some values are None
    x = _AxisInfo([1, None, 3])
    y = _AxisInfo([4, 5, None])
    x_out, y_out = _filter_missing_values(x, y) # 1.11μs -> 1.24μs (10.5% slower)

def test_all_missing_values():
    # All values are None
    x = _AxisInfo([None, None])
    y = _AxisInfo([None, None])
    x_out, y_out = _filter_missing_values(x, y) # 977ns -> 1.09μs (10.4% slower)

def test_mixed_types():
    # Values are mix of float and str
    x = _AxisInfo([1.0, "a", None])
    y = _AxisInfo(["b", 2.0, 3.0])
    x_out, y_out = _filter_missing_values(x, y) # 1.20μs -> 1.28μs (6.34% slower)

# -------------------- Edge Test Cases --------------------

def test_empty_lists():
    # Both lists empty
    x = _AxisInfo([])
    y = _AxisInfo([])
    x_out, y_out = _filter_missing_values(x, y) # 845ns -> 917ns (7.85% slower)

def test_unequal_length_x_longer():
    # x longer than y
    x = _AxisInfo([1, 2, 3, 4])
    y = _AxisInfo([10, None])
    x_out, y_out = _filter_missing_values(x, y) # 1.21μs -> 1.26μs (3.98% slower)

def test_unequal_length_y_longer():
    # y longer than x
    x = _AxisInfo([None, 2])
    y = _AxisInfo([10, None, 30])
    x_out, y_out = _filter_missing_values(x, y) # 969ns -> 1.08μs (10.4% slower)

def test_none_and_false_and_zero():
    # None, False, and 0: Only None is considered missing
    x = _AxisInfo([None, False, 0, ""])
    y = _AxisInfo([1, 2, 3, 4])
    x_out, y_out = _filter_missing_values(x, y) # 1.33μs -> 1.37μs (2.27% slower)

def test_none_and_empty_string():
    # None and empty string: Only None is missing
    x = _AxisInfo(["", None, "foo"])
    y = _AxisInfo([None, "", "bar"])
    x_out, y_out = _filter_missing_values(x, y) # 1.20μs -> 1.22μs (1.88% slower)

def test_all_non_none_falsey():
    # All values are falsey but not None
    x = _AxisInfo([0, "", False])
    y = _AxisInfo([0.0, "", False])
    x_out, y_out = _filter_missing_values(x, y) # 1.25μs -> 1.29μs (3.25% slower)

def test_single_element_none():
    # Single element, one is None
    x = _AxisInfo([None])
    y = _AxisInfo([1])
    x_out, y_out = _filter_missing_values(x, y) # 957ns -> 971ns (1.44% slower)

def test_single_element_both_present():
    # Single element, both present
    x = _AxisInfo([2])
    y = _AxisInfo([3])
    x_out, y_out = _filter_missing_values(x, y) # 1.06μs -> 1.08μs (2.31% slower)

# -------------------- Large Scale Test Cases --------------------

def test_large_all_present():
    # Large lists, all present
    size = 1000
    x = _AxisInfo(list(range(size)))
    y = _AxisInfo(list(range(size, 2*size)))
    x_out, y_out = _filter_missing_values(x, y) # 53.0μs -> 44.2μs (19.8% faster)

def test_large_some_missing():
    # Large lists, every 10th value is None
    size = 1000
    x_values = [i if i % 10 != 0 else None for i in range(size)]
    y_values = [i if i % 15 != 0 else None for i in range(size)]
    x = _AxisInfo(x_values)
    y = _AxisInfo(y_values)
    x_out, y_out = _filter_missing_values(x, y) # 50.7μs -> 42.9μs (18.2% faster)
    # Only indices where neither x nor y is None
    expected = [(xv, yv) for xv, yv in zip(x_values, y_values) if xv is not None and yv is not None]

def test_large_all_missing():
    # Large lists, all None
    size = 1000
    x = _AxisInfo([None]*size)
    y = _AxisInfo([None]*size)
    x_out, y_out = _filter_missing_values(x, y) # 17.2μs -> 17.2μs (0.487% slower)

def test_large_unequal_length():
    # Large lists, unequal length
    x = _AxisInfo(list(range(1000)))
    y = _AxisInfo(list(range(500)))
    x_out, y_out = _filter_missing_values(x, y) # 28.7μs -> 24.1μs (18.9% faster)

def test_large_sparse_missing():
    # Large lists, random sparse missing values
    import random
    random.seed(42)
    size = 1000
    x_values = [i if random.random() > 0.01 else None for i in range(size)]
    y_values = [i if random.random() > 0.01 else None for i in range(size)]
    x = _AxisInfo(x_values)
    y = _AxisInfo(y_values)
    x_out, y_out = _filter_missing_values(x, y) # 53.2μs -> 44.3μs (20.2% faster)
    expected = [(xv, yv) for xv, yv in zip(x_values, y_values) if xv is not None and yv is not None]
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-_filter_missing_values-mhoba3yb and push.

Codeflash Static Badge

The optimization achieves a **13% speedup** by eliminating repeated attribute lookups in the inner loop through **local variable binding**. 

**Key optimization**: The code caches `x_values.append` and `y_values.append` as local variables `append_x` and `append_y` before the loop. This avoids Python's costly attribute resolution on every iteration - instead of looking up the `append` method 6,329 times each (as shown in the profiler), the lookup happens only once per list.

**Performance impact**: The line profiler shows the append operations dropped from 20.2% + 20.8% = 41% of total time to 18.6% + 19.9% = 38.4% of total time. This 2.6 percentage point reduction in the most expensive operations drives the overall speedup.

**Test case performance**: The optimization shows **strongest gains on large datasets** (18-20% faster on 1000+ element tests) where the loop dominates execution time. Smaller datasets see modest slowdowns (2-17%) due to the overhead of creating local variables, but this is negligible in real-world usage where contour plots typically process hundreds or thousands of data points.

**Why this works**: Python's attribute lookup involves dictionary searches and method binding. By caching these expensive operations outside the loop, we convert O(n) attribute lookups to O(1), making the inner loop more CPU-efficient while preserving identical behavior and output.
@codeflash-ai codeflash-ai bot requested a review from mashraf-222 November 7, 2025 03:44
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Nov 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant