Source code for robustx.generators.CE_methods.MCE

import pandas as pd
from gurobipy import Model, GRB
from gurobipy.gurobipy import quicksum, abs_
from robustx.lib.OptSolver import OptSolver
from robustx.generators.CEGenerator import CEGenerator
from robustx.lib.tasks.Task import Task


[docs] class MCE(CEGenerator): """ A counterfactual explanation generator that uses Mixed-Integer Linear Programming (MILP) to find counterfactual explanations. Inherits from the CEGenerator class and implements the _generation_method to find counterfactual explanations using MILP with the Gurobi optimizer. Attributes: _task (Task): The task to solve, inherited from CEGenerator. __customFunc (callable, optional): A custom distance function, inherited from CEGenerator. opt (OptSolver): An optimizer instance for setting up and solving the MILP problem. """ def __init__(self, ct: Task): """ Initializes the MCE recourse generator with a given task. @param ct: The task to solve, provided as a Task instance. """ super().__init__(ct) self.opt = OptSolver(ct) def _generation_method(self, instance, column_name="target", neg_value=0, M=10000, epsilon=0.0001, minimum_distance=0.0, **kwargs) -> pd.DataFrame: """ Generates a counterfactual explanation for a provided instance using MILP. @param instance: The instance for which to generate a counterfactual. Can be a DataFrame or Series. @param column_name: The name of the target column. (Not used in this method) @param neg_value: The value considered negative in the target variable. @param M: A large constant used for modeling constraints. @param epsilon: A small constant used for modeling constraints. @param minimum_distance: The minimum distance constraint for the output node. @param kwargs: Additional keyword arguments. @return: A DataFrame containing the counterfactual explanation for the provided instance. """ # Convert instance to list if isinstance(instance, pd.DataFrame): ilist = instance.iloc[0].tolist() else: ilist = instance.tolist() # Reset model of OptSolver self.opt.gurobiModel = Model() # Set up without fixed inputs self.opt.setup(instance=instance, delta=0, M=M, fix_inputs=False) # Add final constrain after set up if not neg_value: self.opt.gurobiModel.addConstr(self.opt.outputNode - epsilon >= minimum_distance, name="output_node_lb_>=0") else: self.opt.gurobiModel.addConstr(self.opt.outputNode + epsilon <= minimum_distance, name="output_node_ub_<=0") # Set minimising objective # objective = self.opt.gurobiModel.addVar(name="objective") # self.opt.gurobiModel.addConstr(objective == quicksum( # (self.opt.inputNodes[f'v_0_{i}'] - ilist[i]) ** 2 for i in range(len(self.task.training_data.X.columns)))) # Set minimising objective with L1 obj_vars_l1 = [] for i in range(len(self.task.training_data.X.columns)): self.opt.gurobiModel.update() key = f"v_0_{i}" this_obj_var_l1 = self.opt.gurobiModel.addVar(vtype=GRB.SEMICONT, lb=-GRB.INFINITY, name=f"objl1_feat_{i}") self.opt.gurobiModel.addConstr(this_obj_var_l1 >= ilist[i] - self.opt.inputNodes[key]) self.opt.gurobiModel.addConstr(this_obj_var_l1 >= self.opt.inputNodes[key] - ilist[i]) obj_vars_l1.append(this_obj_var_l1) self.opt.gurobiModel.setObjective(quicksum(obj_vars_l1), GRB.MINIMIZE) self.opt.gurobiModel.Params.NonConvex = 2 self.opt.gurobiModel.update() self.opt.gurobiModel.optimize() status = self.opt.gurobiModel.status # If no solution was obtained that means the INN could not be modelled if status != GRB.status.OPTIMAL: print("No solution found using MCE!") return pd.DataFrame(instance).T ce = [] # Find input variables and final CE for v in self.opt.gurobiModel.getVars(): if 'v_0_' in v.varName: ce.append(v.getAttr(GRB.Attr.X)) res = pd.DataFrame(ce).T res.columns = instance.index return res