def test_unsupported_versions_to_json(self, unsupported_version): counterfactual_explanations = CounterfactualExplanations( cf_examples_list=[], local_importance=None, summary_importance=None, version=unsupported_version) with pytest.raises(UserConfigValidationException) as ucve: counterfactual_explanations.to_json() assert "Unsupported serialization version {}".format(unsupported_version) in str(ucve)
def test_unsupported_versions_from_json(self, unsupported_version): json_str = json.dumps({'metadata': {'version': unsupported_version}}) with pytest.raises(UserConfigValidationException) as ucve: CounterfactualExplanations.from_json(json_str) assert "Incompatible version {} found in json input".format(unsupported_version) in str(ucve) json_str = json.dumps({'metadata': {'versio': unsupported_version}}) with pytest.raises(UserConfigValidationException) as ucve: CounterfactualExplanations.from_json(json_str) assert "No version field in the json input" in str(ucve)
def test_no_counterfactuals_found_summary_importance( self, desired_class, sample_custom_query_10, total_CFs, version): counterfactual_explanations = self.exp.global_feature_importance( query_instances=sample_custom_query_10, desired_class=desired_class, total_CFs=total_CFs) counterfactual_explanations.cf_examples_list[0].final_cfs_df = None counterfactual_explanations.cf_examples_list[ 0].final_cfs_df_sparse = None counterfactual_explanations.cf_examples_list[9].final_cfs_df = None counterfactual_explanations.cf_examples_list[ 9].final_cfs_df_sparse = None self.verify_counterfactual_explanations( counterfactual_explanations, None, sample_custom_query_10.shape[0], version, local_importance_available=True, summary_importance_available=True) counterfactual_explanations_as_json = counterfactual_explanations.to_json( ) recovered_counterfactual_explanations = CounterfactualExplanations.from_json( counterfactual_explanations_as_json) self.verify_counterfactual_explanations( recovered_counterfactual_explanations, None, sample_custom_query_10.shape[0], version, local_importance_available=True, summary_importance_available=True) assert counterfactual_explanations == recovered_counterfactual_explanations
def test_summary_importance_output(self, desired_class, sample_custom_query_10, total_CFs, version): counterfactual_explanations = self.exp.global_feature_importance( query_instances=sample_custom_query_10, desired_class=desired_class, total_CFs=total_CFs) self.verify_counterfactual_explanations( counterfactual_explanations, total_CFs, sample_custom_query_10.shape[0], version, local_importance_available=True, summary_importance_available=True) json_output = counterfactual_explanations.to_json() assert json_output is not None assert json.loads(json_output).get('metadata').get( 'version') == version recovered_counterfactual_explanations = CounterfactualExplanations.from_json( json_output) self.verify_counterfactual_explanations( counterfactual_explanations, total_CFs, sample_custom_query_10.shape[0], version, local_importance_available=True, summary_importance_available=True) assert recovered_counterfactual_explanations == counterfactual_explanations
def test_counterfactual_explanations_output(self, desired_class, sample_custom_query_1, total_CFs, version): counterfactual_explanations = self.exp.generate_counterfactuals( query_instances=sample_custom_query_1, desired_class=desired_class, total_CFs=total_CFs) self.verify_counterfactual_explanations(counterfactual_explanations, total_CFs, sample_custom_query_1.shape[0], version) json_output = counterfactual_explanations.to_json() assert json_output is not None assert json.loads(json_output).get('metadata').get( 'version') == version recovered_counterfactual_explanations = CounterfactualExplanations.from_json( json_output) self.verify_counterfactual_explanations(counterfactual_explanations, total_CFs, sample_custom_query_1.shape[0], version) assert recovered_counterfactual_explanations == counterfactual_explanations
def test_sorted_summary_importance_counterfactual_explanations(self): unsorted_summary_importance = { "age": 0.985, "workclass": 0.65, "education": 0.915, "occupation": 0.95, "hours_per_week": 0.985, "gender": 0.67, "marital_status": 0.655, "race": 0.41 } sorted_summary_importance = { "age": 0.985, "hours_per_week": 0.985, "occupation": 0.95, "education": 0.915, "gender": 0.67, "marital_status": 0.655, "workclass": 0.65, "race": 0.41 } counterfactual_explanations = CounterfactualExplanations( cf_examples_list=[], local_importance=None, summary_importance=unsorted_summary_importance) assert unsorted_summary_importance == counterfactual_explanations.summary_importance assert sorted_summary_importance == counterfactual_explanations.summary_importance assert list(unsorted_summary_importance.keys()) != list(counterfactual_explanations.summary_importance.keys()) assert list(sorted_summary_importance.keys()) == list(counterfactual_explanations.summary_importance.keys())
def test_sorted_local_importance_counterfactual_explanations(self): unsorted_local_importance = [{ "age": 0.985, "workclass": 0.65, "education": 0.915, "occupation": 0.95, "hours_per_week": 0.985, "gender": 0.67, "marital_status": 0.655, "race": 0.41 }, { "age": 0.985, "workclass": 0.65, "education": 0.915, "occupation": 0.95, "hours_per_week": 0.985, "gender": 0.67, "marital_status": 0.655, "race": 0.41 }] sorted_local_importance = [{ "age": 0.985, "hours_per_week": 0.985, "occupation": 0.95, "education": 0.915, "gender": 0.67, "marital_status": 0.655, "workclass": 0.65, "race": 0.41 }, { "age": 0.985, "hours_per_week": 0.985, "occupation": 0.95, "education": 0.915, "gender": 0.67, "marital_status": 0.655, "workclass": 0.65, "race": 0.41 }] counterfactual_explanations = CounterfactualExplanations( cf_examples_list=[], local_importance=unsorted_local_importance, summary_importance=None) for index in range(0, len(unsorted_local_importance)): assert unsorted_local_importance[ index] == counterfactual_explanations.local_importance[index] assert sorted_local_importance[ index] == counterfactual_explanations.local_importance[index] for index in range(0, len(unsorted_local_importance)): assert list(unsorted_local_importance[index].keys()) != \ list(counterfactual_explanations.local_importance[index].keys()) assert list(sorted_local_importance[index].keys()) == \ list(counterfactual_explanations.local_importance[index].keys())
def generate_counterfactuals(self, query_instances, total_CFs, desired_class="opposite", desired_range=None, permitted_range=None, features_to_vary="all", stopping_threshold=0.5, posthoc_sparsity_param=0.1, posthoc_sparsity_algorithm="linear", verbose=False, **kwargs): """General method for generating counterfactuals. :param query_instances: Input point(s) for which counterfactuals are to be generated. This can be a dataframe with one or more rows. :param total_CFs: Total number of counterfactuals required. :param desired_class: Desired counterfactual class - can take 0 or 1. Default value is "opposite" to the outcome class of query_instance for binary classification. :param desired_range: For regression problems. Contains the outcome range to generate counterfactuals in. :param permitted_range: Dictionary with feature names as keys and permitted range in list as values. Defaults to the range inferred from training data. If None, uses the parameters initialized in data_interface. :param features_to_vary: Either a string "all" or a list of feature names to vary. :param stopping_threshold: Minimum threshold for counterfactuals target class probability. :param posthoc_sparsity_param: Parameter for the post-hoc operation on continuous features to enhance sparsity. :param posthoc_sparsity_algorithm: Perform either linear or binary search. Takes "linear" or "binary". Prefer binary search when a feature range is large (for instance, income varying from 10k to 1000k) and only if the features share a monotonic relationship with predicted outcome in the model. :param verbose: Whether to output detailed messages. :param sample_size: Sampling size :param random_seed: Random seed for reproducibility :param kwargs: Other parameters accepted by specific explanation method :returns: A CounterfactualExplanations object that contains the list of counterfactual examples per query_instance as one of its attributes. """ if total_CFs <= 0: raise UserConfigValidationException( "The number of counterfactuals generated per query instance (total_CFs) should be a positive integer." ) cf_examples_arr = [] query_instances_list = [] if isinstance(query_instances, pd.DataFrame): for ix in range(query_instances.shape[0]): query_instances_list.append(query_instances[ix:(ix + 1)]) elif isinstance(query_instances, Iterable): query_instances_list = query_instances for query_instance in tqdm(query_instances_list): res = self._generate_counterfactuals( query_instance, total_CFs, desired_class=desired_class, desired_range=desired_range, permitted_range=permitted_range, features_to_vary=features_to_vary, stopping_threshold=stopping_threshold, posthoc_sparsity_param=posthoc_sparsity_param, posthoc_sparsity_algorithm=posthoc_sparsity_algorithm, verbose=verbose, **kwargs) cf_examples_arr.append(res) return CounterfactualExplanations(cf_examples_list=cf_examples_arr)
def test_serialization_deserialization_counterfactual_explanations_class( self): counterfactual_explanations = CounterfactualExplanations( cf_examples_list=[], local_importance=None, summary_importance=None) assert counterfactual_explanations.cf_examples_list is not None assert len(counterfactual_explanations.cf_examples_list) == 0 assert counterfactual_explanations.summary_importance is None assert counterfactual_explanations.local_importance is None assert counterfactual_explanations.metadata is not None assert counterfactual_explanations.metadata['version'] is not None assert counterfactual_explanations.metadata['version'] == '1.0' counterfactual_explanations_as_json = counterfactual_explanations.to_json( ) recovered_counterfactual_explanations = CounterfactualExplanations.from_json( counterfactual_explanations_as_json) assert counterfactual_explanations == recovered_counterfactual_explanations
def test_empty_counterfactual_explanations_object(self, version): counterfactual_explanations = CounterfactualExplanations( cf_examples_list=[], local_importance=None, summary_importance=None, version=version) self.verify_counterfactual_explanations(counterfactual_explanations, None, 0, version) counterfactual_explanations_as_json = counterfactual_explanations.to_json() assert counterfactual_explanations_as_json is not None recovered_counterfactual_explanations = CounterfactualExplanations.from_json( counterfactual_explanations_as_json) self.verify_counterfactual_explanations(recovered_counterfactual_explanations, None, 0, version) assert counterfactual_explanations == recovered_counterfactual_explanations
def test_KD_tree_counterfactual_explanations_output( self, desired_range, sample_custom_query_2, total_CFs): counterfactual_explanations = self.exp_regr.generate_counterfactuals( query_instances=sample_custom_query_2, total_CFs=total_CFs, desired_range=desired_range) assert counterfactual_explanations is not None json_str = counterfactual_explanations.to_json() assert json_str is not None recovered_counterfactual_explanations = CounterfactualExplanations.from_json( json_str) assert recovered_counterfactual_explanations is not None assert counterfactual_explanations == recovered_counterfactual_explanations
def test_no_counterfactuals_found(self, desired_class, sample_custom_query_1, total_CFs, version): counterfactual_explanations = self.exp.generate_counterfactuals( query_instances=sample_custom_query_1, desired_class=desired_class, total_CFs=total_CFs) counterfactual_explanations.cf_examples_list[0].final_cfs_df = None counterfactual_explanations.cf_examples_list[0].final_cfs_df_sparse = None self.verify_counterfactual_explanations(counterfactual_explanations, None, sample_custom_query_1.shape[0], version) counterfactual_explanations_as_json = counterfactual_explanations.to_json() recovered_counterfactual_explanations = CounterfactualExplanations.from_json( counterfactual_explanations_as_json) self.verify_counterfactual_explanations(recovered_counterfactual_explanations, None, sample_custom_query_1.shape[0], version) assert counterfactual_explanations == recovered_counterfactual_explanations
def load_result(self, data_directory_path): metadata_file_path = (data_directory_path / (_CommonSchemaConstants.METADATA + '.json')) if metadata_file_path.exists(): with open(metadata_file_path, 'r') as result_file: metadata = json.load(result_file) if metadata['version'] == _SchemaVersions.V1: cf_schema_keys = _V1SchemaConstants.ALL else: cf_schema_keys = _V2SchemaConstants.ALL counterfactual_examples_dict = {} for counterfactual_examples_key in cf_schema_keys: result_path = (data_directory_path / (counterfactual_examples_key + '.json')) with open(result_path, 'r') as result_file: counterfactual_examples_dict[ counterfactual_examples_key] = json.load(result_file) counterfactuals_json_str = json.dumps(counterfactual_examples_dict) self.counterfactual_obj = \ CounterfactualExplanations.from_json(counterfactuals_json_str) else: self.counterfactual_obj = None result_path = (data_directory_path / (CounterfactualConfig.HAS_COMPUTATION_FAILED + '.json')) with open(result_path, 'r') as result_file: self.has_computation_failed = json.load(result_file) result_path = (data_directory_path / (CounterfactualConfig.FAILURE_REASON + '.json')) with open(result_path, 'r') as result_file: self.failure_reason = json.load(result_file) result_path = (data_directory_path / (CounterfactualConfig.IS_COMPUTED + '.json')) with open(result_path, 'r') as result_file: self.is_computed = json.load(result_file)
def feature_importance(self, query_instances, cf_examples_list=None, total_CFs=10, local_importance=True, global_importance=True, desired_class="opposite", desired_range=None, permitted_range=None, features_to_vary="all", stopping_threshold=0.5, posthoc_sparsity_param=0.1, posthoc_sparsity_algorithm="linear", **kwargs): """ Estimate feature importance scores for the given inputs. :param query_instances: A list of inputs for which to compute the feature importances. These can be provided as a dataframe. :param cf_examples_list: If precomputed, a list of counterfactual examples for every input point. If cf_examples_list is provided, then all the following parameters are ignored. :param total_CFs: The number of counterfactuals to generate per input (default is 10) :param other_parameters: These are the same as the generate_counterfactuals method. :returns: An object of class CounterfactualExplanations that includes the list of counterfactuals per input, local feature importances per input, and the global feature importance summarized over all inputs. """ if cf_examples_list is None: cf_examples_list = self.generate_counterfactuals( query_instances, total_CFs, desired_class=desired_class, desired_range=desired_range, permitted_range=permitted_range, features_to_vary=features_to_vary, stopping_threshold=stopping_threshold, posthoc_sparsity_param=posthoc_sparsity_param, posthoc_sparsity_algorithm=posthoc_sparsity_algorithm, **kwargs).cf_examples_list allcols = self.data_interface.categorical_feature_names + self.data_interface.continuous_feature_names summary_importance = None local_importances = None if global_importance: summary_importance = {} # Initializing importance vector for col in allcols: summary_importance[col] = 0 if local_importance: local_importances = [{} for _ in range(len(cf_examples_list))] # Initializing local importance for the ith query instance for i in range(len(cf_examples_list)): for col in allcols: local_importances[i][col] = 0 # Summarizing the found counterfactuals for i in range(len(cf_examples_list)): cf_examples = cf_examples_list[i] org_instance = cf_examples.test_instance_df if cf_examples.final_cfs_df_sparse is not None: df = cf_examples.final_cfs_df_sparse else: df = cf_examples.final_cfs_df for index, row in df.iterrows(): for col in self.data_interface.continuous_feature_names: if not np.isclose(org_instance[col].iat[0], row[col]): if summary_importance is not None: summary_importance[col] += 1 if local_importances is not None: local_importances[i][col] += 1 for col in self.data_interface.categorical_feature_names: if org_instance[col].iat[0] != row[col]: if summary_importance is not None: summary_importance[col] += 1 if local_importances is not None: local_importances[i][col] += 1 if local_importances is not None: for col in allcols: local_importances[i][col] /= ( cf_examples_list[0].final_cfs_df.shape[0]) if summary_importance is not None: for col in allcols: summary_importance[col] /= ( cf_examples_list[0].final_cfs_df.shape[0] * len(cf_examples_list)) return CounterfactualExplanations( cf_examples_list, local_importance=local_importances, summary_importance=summary_importance)
def generate_counterfactuals(self, query_instance, total_CFs, desired_class="opposite", proximity_weight=0.5, diversity_weight=1.0, categorical_penalty=0.1, algorithm="DiverseCF", features_to_vary="all", permitted_range=None, yloss_type="hinge_loss", diversity_loss_type="dpp_style:inverse_dist", feature_weights="inverse_mad", optimizer="tensorflow:adam", learning_rate=0.05, min_iter=500, max_iter=5000, project_iter=0, loss_diff_thres=1e-5, loss_converge_maxiter=1, verbose=False, init_near_query_instance=True, tie_random=False, stopping_threshold=0.5, posthoc_sparsity_param=0.1, posthoc_sparsity_algorithm="linear", limit_steps_ls=10000): """Generates diverse counterfactual explanations :param query_instance: Test point of interest. A dictionary of feature names and values or a single row dataframe. :param total_CFs: Total number of counterfactuals required. :param desired_class: Desired counterfactual class - can take 0 or 1. Default value is "opposite" to the outcome class of query_instance for binary classification. :param proximity_weight: A positive float. Larger this weight, more close the counterfactuals are to the query_instance. :param diversity_weight: A positive float. Larger this weight, more diverse the counterfactuals are. :param categorical_penalty: A positive float. A weight to ensure that all levels of a categorical variable sums to 1. :param algorithm: Counterfactual generation algorithm. Either "DiverseCF" or "RandomInitCF". :param features_to_vary: Either a string "all" or a list of feature names to vary. :param permitted_range: Dictionary with continuous feature names as keys and permitted min-max range in list as values. Defaults to the range inferred from training data. If None, uses the parameters initialized in data_interface. :param yloss_type: Metric for y-loss of the optimization function. Takes "l2_loss" or "log_loss" or "hinge_loss". :param diversity_loss_type: Metric for diversity loss of the optimization function. Takes "avg_dist" or "dpp_style:inverse_dist". :param feature_weights: Either "inverse_mad" or a dictionary with feature names as keys and corresponding weights as values. Default option is "inverse_mad" where the weight for a continuous feature is the inverse of the Median Absolute Devidation (MAD) of the feature's values in the training set; the weight for a categorical feature is equal to 1 by default. :param optimizer: Tensorflow optimization algorithm. Currently tested only with "tensorflow:adam". :param learning_rate: Learning rate for optimizer. :param min_iter: Min iterations to run gradient descent for. :param max_iter: Max iterations to run gradient descent for. :param project_iter: Project the gradients at an interval of these many iterations. :param loss_diff_thres: Minimum difference between successive loss values to check convergence. :param loss_converge_maxiter: Maximum number of iterations for loss_diff_thres to hold to declare convergence. Defaults to 1, but we assigned a more conservative value of 2 in the paper. :param verbose: Print intermediate loss value. :param init_near_query_instance: Boolean to indicate if counterfactuals are to be initialized near query_instance. :param tie_random: Used in rounding off CFs and intermediate projection. :param stopping_threshold: Minimum threshold for counterfactuals target class probability. :param posthoc_sparsity_param: Parameter for the post-hoc operation on continuous features to enhance sparsity. :param posthoc_sparsity_algorithm: Perform either linear or binary search. Takes "linear" or "binary". Prefer binary search when a feature range is large (for instance, income varying from 10k to 1000k) and only if the features share a monotonic relationship with predicted outcome in the model. :param limit_steps_ls: Defines an upper limit for the linear search step in the posthoc_sparsity_enhancement :return: A CounterfactualExamples object to store and visualize the resulting counterfactual explanations (see diverse_counterfactuals.py). """ # check feature MAD validity and throw warnings if feature_weights == "inverse_mad": self.data_interface.get_valid_mads(display_warnings=True, return_mads=False) # check permitted range for continuous features if permitted_range is not None: # if not self.data_interface.check_features_range(permitted_range): # raise ValueError( # "permitted range of features should be within their original range") # else: self.data_interface.permitted_range = permitted_range self.minx, self.maxx = self.data_interface.get_minx_maxx( normalized=True) self.cont_minx = [] self.cont_maxx = [] for feature in self.data_interface.continuous_feature_names: self.cont_minx.append( self.data_interface.permitted_range[feature][0]) self.cont_maxx.append( self.data_interface.permitted_range[feature][1]) if ([ total_CFs, algorithm, features_to_vary, yloss_type, diversity_loss_type, feature_weights, optimizer ] != (self.cf_init_weights + self.loss_weights + self.optimizer_weights)): self.do_cf_initializations(total_CFs, algorithm, features_to_vary) self.do_loss_initializations(yloss_type, diversity_loss_type, feature_weights) self.do_optimizer_initializations(optimizer) """ Future Support: We have three main components in our tensorflow graph: (1) initialization of tf.variables (2) defining ops for loss function initializations, and (3) defining ops for optimizer initializations. Need to define methods to delete some nodes from a tensorflow graphs or update variables/ops in a tensorflow graph dynamically, so that only those components corresponding to the variables that are updated change. """ # check if hyperparameters are to be updated if not collections.Counter([proximity_weight, diversity_weight, categorical_penalty]) == \ collections.Counter(self.hyperparameters): self.update_hyperparameters(proximity_weight, diversity_weight, categorical_penalty) final_cfs_df, test_instance_df, final_cfs_df_sparse = self.find_counterfactuals( query_instance, limit_steps_ls, desired_class, learning_rate, min_iter, max_iter, project_iter, loss_diff_thres, loss_converge_maxiter, verbose, init_near_query_instance, tie_random, stopping_threshold, posthoc_sparsity_param, posthoc_sparsity_algorithm) counterfactual_explanations = exp.CounterfactualExamples( data_interface=self.data_interface, final_cfs_df=final_cfs_df, test_instance_df=test_instance_df, final_cfs_df_sparse=final_cfs_df_sparse, posthoc_sparsity_param=posthoc_sparsity_param, desired_class=desired_class) return CounterfactualExplanations( cf_examples_list=[counterfactual_explanations])
def generate_counterfactuals(self, query_instances, total_CFs, desired_class="opposite", desired_range=None, permitted_range=None, features_to_vary="all", stopping_threshold=0.5, posthoc_sparsity_param=0.1, proximity_weight=0.2, sparsity_weight=0.2, diversity_weight=5.0, categorical_penalty=0.1, posthoc_sparsity_algorithm="linear", verbose=False, **kwargs): """General method for generating counterfactuals. :param query_instances: Input point(s) for which counterfactuals are to be generated. This can be a dataframe with one or more rows. :param total_CFs: Total number of counterfactuals required. :param desired_class: Desired counterfactual class - can take 0 or 1. Default value is "opposite" to the outcome class of query_instance for binary classification. :param desired_range: For regression problems. Contains the outcome range to generate counterfactuals in. This should be a list of two numbers in ascending order. :param permitted_range: Dictionary with feature names as keys and permitted range in list as values. Defaults to the range inferred from training data. If None, uses the parameters initialized in data_interface. :param features_to_vary: Either a string "all" or a list of feature names to vary. :param stopping_threshold: Minimum threshold for counterfactuals target class probability. :param proximity_weight: A positive float. Larger this weight, more close the counterfactuals are to the query_instance. Used by ['genetic', 'gradientdescent'], ignored by ['random', 'kdtree'] methods. :param sparsity_weight: A positive float. Larger this weight, less features are changed from the query_instance. Used by ['genetic', 'kdtree'], ignored by ['random', 'gradientdescent'] methods. :param diversity_weight: A positive float. Larger this weight, more diverse the counterfactuals are. Used by ['genetic', 'gradientdescent'], ignored by ['random', 'kdtree'] methods. :param categorical_penalty: A positive float. A weight to ensure that all levels of a categorical variable sums to 1. Used by ['genetic', 'gradientdescent'], ignored by ['random', 'kdtree'] methods. :param posthoc_sparsity_param: Parameter for the post-hoc operation on continuous features to enhance sparsity. :param posthoc_sparsity_algorithm: Perform either linear or binary search. Takes "linear" or "binary". Prefer binary search when a feature range is large (for instance, income varying from 10k to 1000k) and only if the features share a monotonic relationship with predicted outcome in the model. :param verbose: Whether to output detailed messages. :param sample_size: Sampling size :param random_seed: Random seed for reproducibility :param kwargs: Other parameters accepted by specific explanation method :returns: A CounterfactualExplanations object that contains the list of counterfactual examples per query_instance as one of its attributes. """ self._validate_counterfactual_configuration( query_instances=query_instances, total_CFs=total_CFs, desired_class=desired_class, desired_range=desired_range, permitted_range=permitted_range, features_to_vary=features_to_vary, stopping_threshold=stopping_threshold, posthoc_sparsity_param=posthoc_sparsity_param, posthoc_sparsity_algorithm=posthoc_sparsity_algorithm, verbose=verbose, kwargs=kwargs ) cf_examples_arr = [] query_instances_list = [] if isinstance(query_instances, pd.DataFrame): for ix in range(query_instances.shape[0]): query_instances_list.append(query_instances[ix:(ix+1)]) elif isinstance(query_instances, Iterable): query_instances_list = query_instances for query_instance in tqdm(query_instances_list): self.data_interface.set_continuous_feature_indexes(query_instance) res = self._generate_counterfactuals( query_instance, total_CFs, desired_class=desired_class, desired_range=desired_range, permitted_range=permitted_range, features_to_vary=features_to_vary, stopping_threshold=stopping_threshold, posthoc_sparsity_param=posthoc_sparsity_param, posthoc_sparsity_algorithm=posthoc_sparsity_algorithm, verbose=verbose, **kwargs) cf_examples_arr.append(res) self._check_any_counterfactuals_computed(cf_examples_arr=cf_examples_arr) return CounterfactualExplanations(cf_examples_list=cf_examples_arr)