Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion dice_ml/explainer_interfaces/dice_genetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,14 @@ def mate(self, k1, k2, features_to_vary, query_instance):
# otherwise insert random gene(mutate) for maintaining diversity
if feat_name in features_to_vary:
if feat_name in self.data_interface.continuous_feature_names:
# Mutate uniformly across the feature's [low, high] range.
# The historical line passed feature_range[feat_name][0]
# twice, which collapsed every continuous mutation to the
# lower bound and starved the search of diversity — see
# the do_random_init pattern at the top of this file
# (`np.random.uniform(low, high)`) for the correct shape.
one_init[j] = np.random.uniform(self.feature_range[feat_name][0],
self.feature_range[feat_name][0])
self.feature_range[feat_name][1])
else:
one_init[j] = np.random.choice(self.feature_range[feat_name])
else:
Expand Down
63 changes: 63 additions & 0 deletions tests/test_dice_interface/test_dice_genetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,66 @@ def test_maxiter(self, desired_range, sample_custom_query_2, total_CFs, initiali
for cfs_example in ans.cf_examples_list:
for i in cfs_example.final_cfs_df[self.exp.data_interface.outcome_name].values:
assert desired_range[0] <= i <= desired_range[1]


class TestMateContinuousMutation:
"""Regression for the typo behind a class of #441 / #260 reports.

`DiceGenetic.mate` mutates a continuous feature with
`np.random.uniform(low, high)`. The historical code passed
`feature_range[feat_name][0]` for both arguments, which collapses
every continuous mutation to the lower bound. That starves the
genetic search of diversity along continuous axes and is consistent
with users reporting that the genetic explainer ignores their
feature_range / features_to_vary intent on continuous features.
"""

def test_mate_mutation_can_reach_upper_half_of_range(self):
import random
import types

import numpy as np

from dice_ml.explainer_interfaces.dice_genetic import DiceGenetic

# We don't need the full DiceGenetic init — only the `mate` method's
# dependencies. Build a minimal stand-in to keep this test fast and
# decoupled from data fixtures.
exp = DiceGenetic.__new__(DiceGenetic)
exp.data_interface = types.SimpleNamespace(
number_of_features=1,
feature_names=["x"],
continuous_feature_names=["x"],
)
exp.feature_range = {"x": [0.0, 100.0]}

# Force `mate` into the mutation branch (prob >= 0.80) deterministically
# by monkeypatching random.random within this test scope.
original_random = random.random
random.random = lambda: 0.99
try:
np.random.seed(0)
samples = []
for _ in range(200):
child = exp.mate(
k1=np.array([0.0]),
k2=np.array([0.0]),
features_to_vary=["x"],
query_instance=[0.0],
)
samples.append(child[0])
finally:
random.random = original_random

samples = np.array(samples)
# Every value must lie inside the declared feature range.
assert samples.min() >= 0.0
assert samples.max() <= 100.0
# The fix produces values across the range; the historical typo
# pinned every mutation to feature_range[name][0] (~0.0). We assert
# at least one mutation lands in the upper half of the range —
# essentially impossible on the broken code, near-certain after the fix.
assert (samples > 50.0).any(), (
"Continuous mutations are still pinned to the lower bound — "
"regression of the np.random.uniform(low, low) typo."
)