LiCE: Likely Counterfactual Explanations
—
LiCE
These are the main entry points to using the LiCE framework.
- class humancompatible.explain.lice.lice.LiCE.LiCE(spn: SPN, nn_path: str, data_handler: DataHandler)[source]
Bases:
objectLiCE (Likely Counterfactual Explanations) class for generating counterfactual explanations of a neural network using a Sum-Product Network (SPN) to ensure plausibility.
This class formulates and solves a Mixed-Integer Optimization (MIO) problem to find counterfactuals that minimize the distance to a factual instance while satisfying desired prediction and likelihood constraints.
Initializes the LiCE explainer.
Parameters:
- spnSPN
The Sum-Product Network (SPN) used for likelihood estimation and integration into the optimization problem.
- nn_pathstr
The file path to the ONNX-formatted neural network model.
- data_handlerDataHandler
An instance of DataHandler for preprocessing and postprocessing of input and output data.
- MIO_EPS = 1e-06
A small epsilon value used in Mixed-Integer Optimization (MIO) problems, particularly for handling strict inequalities.
- generate_counterfactual(factual: ndarray | DataFrame, desired_class: bool, ll_threshold: float = -inf, ll_opt_coefficient: float = 0, n_counterfactuals: int = 1, solver_name: str = 'gurobi', verbose: bool = False, time_limit: int = 600, leaf_encoding: str = 'histogram', spn_variant: str = 'lower', ce_relative_distance: float = inf, ce_max_distance: float = inf) list[ndarray | DataFrame][source]
Generates one or more counterfactual explanations for a given factual instance.
This is the main method for finding counterfactuals. It builds and solves the Pyomo optimization model.
Parameters:
- factualDataLike
The original instance for which to find a counterfactual.
- desired_classbool
The target class for the counterfactual (True for class 1, False for class 0).
- ll_thresholdfloat, optional
The minimum log-likelihood for the generated counterfactuals. If set to a finite value, a constraint ensures the counterfactual’s log-likelihood from the SPN meets this value. Defaults to -np.inf (no constraint).
- ll_opt_coefficientfloat, optional
If non-zero, the log-likelihood of the SPN is incorporated into the objective function. A positive coefficient encourages higher log-likelihood. Defaults to 0 (no optimization of log-likelihood).
- n_counterfactualsint, optional
The number of counterfactuals to generate. Currently, multiple counterfactuals are only supported with the ‘gurobi’ solver. Defaults to 1.
- solver_namestr, optional
The name of the Pyomo-compatible solver to use (e.g., “appsi_highs”, “gurobi”, “cplex”). Defaults to “gurobi”.
- verbosebool, optional
If True, the solver’s output will be printed. Defaults to False.
- time_limitint, optional
The maximum time (in seconds) allowed for the solver to run. Defaults to 600.
- leaf_encodingstr, optional
The type of encoding used for leaf nodes in the SPN. Options include “histogram” (leading to histogram-specifc formlation) or values of pw_repn parameter of Pyomo’s Piecewise component. Defaults to “histogram”.
- spn_variantstr, optional
The variant of SPN encoding to use, “lower” for a lower bound approximation or “upper” for approximation from above. Defaults to “lower”.
- ce_relative_distancefloat, optional
For multiple counterfactuals (Gurobi only), this sets the relative gap from the optimal solution to consider other solutions in the pool. E.g., 0.1 means solutions within 10% of the optimal objective value. Defaults to np.inf (no relative distance constraint).
- ce_max_distancefloat, optional
A hard upper bound on the total cost (distance) of the counterfactual. Defaults to np.inf (no maximum distance constraint).
Returns:
- list[DataLike]
A list of generated counterfactuals (DataLike objects). The list might be empty if no counterfactuals are found or if the solver terminates unexpectedly.
Raises:
- NotImplementedError
If n_counterfactuals > 1 is requested with a solver other than ‘gurobi’.
- ValueError
If the solver terminates with an unexpected condition.
- property model: Model
Returns the Pyomo model from the most recent generate_counterfactual call.
This allows inspection of the optimization problem after it has been built and potentially solved.
Returns:
- pyo.Model
The Pyomo concrete model instance.
- property stats: dict[str, object]
Returns a dictionary containing performance statistics and results from the last counterfactual generation attempt.
Returns:
- dict[str, object]
A dictionary with the following keys: - “time_total”: Total time taken for the generate_counterfactual call (including CE recovery). - “time_solving”: Time spent by the solver. - “time_building”: Time spent building the Pyomo model. - “optimal”: Boolean indicating if the solver found an optimal solution. - “ll_computed”: A list of log-likelihoods for each generated counterfactual (if applicable). - “dist_computed”: A list of distances for each generated counterfactual.
—
Sum-Product Network wrapper
- class humancompatible.explain.lice.spn.SPN.Node(node: Node, feature_list: list[Feature], normalize: bool, min_density: float)[source]
A representation of a node in an SPN
Initializes a custom Node object from an SPFlow_Node.
This constructor wraps an SPFlow library’s internal node representation to provide a more convenient and type-aware interface for SPN nodes. It extracts relevant information such as node type, scope, densities, and breakpoints (for continuous leaves) or weights (for sum nodes).
Parameters:
- nodeSPFlow_Node
The raw node object from the SPFlow library (e.g., spn.structure.Base.Leaf, spn.structure.Base.Product, spn.structure.Base.Sum).
- feature_listlist[Feature]
A list of Feature objects (e.g., Contiguous, Categorical, Binary, Mixed) that define the characteristics of the input features. This list is used to determine the specific type of leaf node and its properties.
- normalizebool
A boolean indicating whether the data used to learn the SPN was normalized to a [0, 1] range. This affects how breakpoints are handled for continuous leaves.
- min_densityfloat
A minimum density value to use, especially for handling edge cases or padding in histograms to ensure non-zero probabilities (log-likelihoods).
Raises:
- NotImplementedError
If a multivariate leaf node (a leaf node spanning multiple features) is encountered, as it’s not currently supported.
- ValueError
If an unknown or unsupported SPFlow node type is provided.
- get_breaks_densities(span_all=True) tuple[ndarray[float], ndarray[float]][source]
Returns the breakpoints and corresponding density values for a continuous leaf node.
This method ensures that the breakpoints cover the entire feature range (if span_all is True) and normalizes them if the SPN was learned on normalized data. It’s crucial for constructing piecewise linear functions for log-likelihood estimation.
Parameters:
- span_allbool, optional
If True, the returned breakpoints will span the entire defined range of the input feature (either [0, 1] if normalized, or the feature’s original bounds). If the node’s internal breaks are narrower, min_density is used to pad the outer regions. Defaults to True.
Returns:
- tuple[np.ndarray[float], np.ndarray[float]]
A tuple containing two NumPy arrays: - The first array contains the breakpoints (x-values) for the
piecewise function, scaled to the appropriate range (0-1 if normalized, or original bounds if not).
The second array contains the corresponding density values (y-values) for each segment defined by the breakpoints.
Raises:
- ValueError
If this method is called on a node that is not a leaf node over a Contiguous feature.
- AssertionError
If the feature bounds are not available for scaling when span_all is True.
- class humancompatible.explain.lice.spn.SPN.NodeType(value)[source]
An enumeration.
- LEAF = 2
- LEAF_BINARY = 4
- LEAF_CATEGORICAL = 3
- PRODUCT = 1
- SUM = 0
- class humancompatible.explain.lice.spn.SPN.SPN(data: ndarray | DataFrame, data_handler: DataHandler, normalize_data: bool = False, learn_mspn_kwargs: dict[str, Any] = {})[source]
A wrapper class for Sum-Product Networks (SPNs).
This class facilitates learning an SPN from data using SPFlow, representing its structure in a custom Node format, and performing log-likelihood inference. It integrates with a DataHandler for preprocessing input data.
Initializes the SPN wrapper, learns an SPN from the provided data, and constructs its internal node representation.
Parameters:
- dataDataLike
The input data used to learn the SPN. This can be a NumPy array or a pandas DataFrame, as accepted by DataHandler.
- data_handlerDataHandler
An instance of a DataHandler class responsible for preprocessing the input data (e.g., encoding, scaling, handling feature types).
- normalize_databool, optional
If True, the data will be normalized to a [0, 1] range before learning the SPN. This setting affects how continuous leaf node breakpoints are interpreted. Defaults to False.
- learn_mspn_kwargsdict[str, Any], optional
A dictionary of keyword arguments to be passed directly to the SPFlow’s learn_mspn function. This allows customizing the SPN learning process (e.g., min_instances_slice). Defaults to an empty dict.
- compute_ll(data: ndarray | DataFrame) ndarray[float][source]
Computes the exact log-likelihood of the given data using the learned SPN.
Parameters:
- dataDataLike
The input data for which to compute the log-likelihood. Can be a single sample (1D array) or multiple samples (2D array).
Returns:
- np.ndarray[float]
The log-likelihood values for the input data. Returns a scalar if a single sample is provided, otherwise a NumPy array of log-likelihoods.
- compute_max_approx(data: ndarray | DataFrame, return_all: bool = False) float | dict[int, float][source]
Computes an approximate log-likelihood for a single data sample by replacing sum operations with a max operation in the log-domain, just as the MIO approximation would.
This method is useful for quickly evaluating log-likelihoods without the full log-sum-exp computation, which is often approximated in MIP contexts. It traverses the SPN in topological order.
Parameters:
- dataDataLike
A single data sample (1D NumPy array or similar) for which to compute the approximate log-likelihood.
- return_allbool, optional
If True, returns a dictionary where keys are node IDs and values are their computed approximate log-likelihoods. If False, returns only the approximate log-likelihood of the root node (the final output). Defaults to False.
Returns:
- float | dict[int, float]
The approximate log-likelihood of the root node (if return_all is False), or a dictionary of approximate log-likelihoods for all nodes (if return_all is True).
Raises:
- ValueError
If more than one sample is provided in data.
- input_scale(feature_i) float[source]
Returns the scaling factor for a specific input feature.
This is relevant if the data was not normalized, in which case the original scale of the feature might be needed for certain computations (e.g., when relating changes in normalized space back to original space).
Parameters:
- feature_iint
The index of the feature for which to retrieve the input scale.
Returns:
- float
The input scale (1 if data was normalized, otherwise the feature’s internal scale factor).
- property min_density: float
Returns the minimum density value used for SPN calculations.
This value typically comes from SPFLow’s EPSILON, which is a small constant to prevent log(0) issues.
Returns:
- float
The minimum density value (epsilon).
- property nodes: list[Node]
Returns a list of custom Node objects representing the SPN’s structure, ordered topologically (parents appear before children).
This property ensures that the Node objects are created and cached upon first access.
Returns:
- list[Node]
A list of Node objects, ordered such that dependencies are met (i.e., children nodes appear after their parents in the list).
—
Data handling
- class humancompatible.explain.lice.data.DataHandler.DataHandler(X: ndarray | DataFrame, y: ndarray | Series | None = None, categ_map: dict[int | str, list[int | str]] = {}, ordered: list[int | str] = [], bounds_map: dict[int | str, tuple[int, int]] = {}, discrete: list[int | str] = [], immutable: list[int | str] = [], monotonicity: dict[int | str, Monotonicity] = {}, causal_inc: list[tuple[int | str, int | str]] = [], greater_than: list[tuple[int | str, int | str]] = [], regression: bool = False, feature_names: list[str] | None = None, target_name: str | None = None)[source]
Bases:
objectHandles all data processing, transforming raw pandas DataFrames or NumPy arrays into a normalized and encoded format.
This class is designed to be initialized with training data and then used to consistently encode all subsequent data. It supports mixed data types, where some values are categorical, and normalizes contiguous data to a [0, 1] range. The output can be either one-hot encoded or direct data with mapped categorical values to negative integers.
Initializes a DataHandler instance for data processing and encoding.
Parameters:
- XDataLike
Input features. Can be a pandas DataFrame or a NumPy array. Expected shape: (num_samples, num_features).
- yOneDimData | None, optional
Target feature (e.g., labels for classification or regression targets). Expected shape: (num_samples,). Defaults to None.
- categ_mapdict[FeatureID, list[CategValue]], optional
A dictionary where keys are feature identifiers (indices or names) and values are lists of unique categorical values for that feature. If a list is empty, all unique values of the feature are considered categorical. If a list is non-empty but doesn’t cover all values, the feature is treated as mixed. Defaults to an empty dictionary.
- orderedlist[FeatureID], optional
A list of feature identifiers that should be treated as ordered categorical. Defaults to an empty list.
- bounds_mapdict[FeatureID, tuple[int, int]], optional
A dictionary where keys are feature identifiers and values are tuples (min, max) defining the real bounds for contiguous features. Defaults to an empty dictionary.
- discretelist[FeatureID], optional
A list of feature identifiers that should be treated as discrete contiguous. Defaults to an empty list.
- immutablelist[FeatureID], optional
A list of feature identifiers that represent immutable features (cannot be changed). Defaults to an empty list.
- monotonicitydict[FeatureID, Monotonicity], optional
A dictionary where keys are feature identifiers and values specify the monotonicity constraint for that feature (can only decrease or only increase). Defaults to an empty dictionary.
- causal_inclist[tuple[FeatureID, FeatureID]], optional
A list of tuples, where each tuple (cause, effect) indicates that an increase in ‘cause’ must lead to an increase in ‘effect’. Defaults to an empty list.
- greater_thanlist[tuple[FeatureID, FeatureID]], optional
A list of tuples, where each tuple (greater, smaller) indicates that ‘greater’ must be greater than ‘smaller’. Defaults to an empty list.
- regressionbool, optional
If True, the task is treated as regression; otherwise, it’s classification. Defaults to False.
- feature_namesOptional[list[str]], optional
A list of names for the input features. If None and X is a DataFrame, column names from X will be used. Defaults to None.
- target_nameOptional[str], optional
The name of the target feature. If None and y is a pandas Series, its name will be used. If X is a DataFrame and target_name is provided, the target column will be extracted from X. Defaults to None.
Raises:
- ValueError
If the length of feature_names does not match the number of features in X.
- allowed_changes(pre_vals, post_vals)[source]
Checks if a proposed change from pre_vals to post_vals is allowed based on feature constraints (immutability, monotonicity) and defined causal/greater-than relationships.
Parameters:
- pre_valsnp.ndarray
The original feature values for a single instance. Expected shape: (num_features,).
- post_valsnp.ndarray
The proposed new feature values for the same instance. Expected shape: (num_features,).
Returns:
- bool
True if all changes are allowed according to the defined constraints, False otherwise.
Raises:
- ValueError
If an invalid feature type is encountered during the check of causal or greater-than relationships.
- property causal_inc: list[tuple[Feature, Feature]]
- decode(X: ndarray[float64], denormalize: bool = True, encoded_one_hot: bool = True, as_dataframe: bool = True) ndarray[float64][source]
Decodes the encoded input features back to their original format.
This method reverses the encoding process, denormalizing contiguous features and converting one-hot encoded categorical features back to their original values.
Parameters:
- Xnp.ndarray[np.float64]
The encoded input data matrix. Expected shape: (num_samples, num_encoded_features), where num_encoded_features can be higher than the original number of features due to one-hot encoding.
- denormalizebool, optional
If True, the denormalization process will be applied to contiguous features. Defaults to True.
- encoded_one_hotbool, optional
If True, it is assumed that the input X is one-hot encoded. Defaults to True.
- as_dataframebool, optional
If True, the decoded features will be returned as a pandas DataFrame. If False, a NumPy array will be returned. Defaults to True.
Returns:
- np.ndarray[np.float64] | pd.DataFrame
The decoded features in their original format. - If as_dataframe is True: a pandas DataFrame with original feature names. - If as_dataframe is False: a NumPy array. Expected shape: (num_samples, num_original_features).
- decode_y(y: ndarray[float64], denormalize: bool = True, as_series: bool = True) ndarray[float64][source]
Decodes the encoded target feature (y) back to its original format.
This method reverses the encoding process for the target variable, denormalizing if applicable and converting one-hot encoded forms back to their original values.
Parameters:
- ynp.ndarray[np.float64]
The encoded target feature data. Expected shape: (num_samples,) for non-one-hot encoded targets, or (num_samples, num_categorical_values) for one-hot encoded categorical targets.
- denormalizebool, optional
If True, denormalization will be applied to the target feature (if it’s contiguous). Defaults to True.
- as_seriesbool, optional
If True, the decoded target feature will be returned as a pandas Series. If False, a NumPy array will be returned. Defaults to True.
Returns:
- np.ndarray[np.float64] | pd.Series
The decoded target feature data in its original format. - If as_series is True: a pandas Series with the original target name. - If as_series is False: a NumPy array. Expected shape: (num_samples,).
- encode(X: ndarray | DataFrame, normalize: bool = True, one_hot: bool = True) ndarray[float64][source]
Encodes the input features according to the DataHandler’s configuration.
This method transforms raw input data into a format suitable for model training or inference, handling normalization and one-hot encoding as specified.
Parameters:
- XDataLike
Input features, which can be a pandas DataFrame, pandas Series, or a NumPy array. Expected shape: (num_samples, num_features) for DataFrame/2D array, or (num_features,) for a single sample Series/1D array.
- normalizebool, optional
If True, contiguous features will be normalized to the [0, 1] range. Defaults to True.
- one_hotbool, optional
If True, categorical features will be one-hot encoded. If False, categorical values will be mapped to negative integers. Defaults to True.
Returns:
- np.ndarray[np.float64]
The encoded input features. The shape depends on one_hot: - If one_hot is True: (num_samples, total_one_hot_features) - If one_hot is False: (num_samples, num_features)
Raises:
- ValueError
If the input X has an unexpected shape or type that cannot be processed.
- encode_all(X_all: ndarray, normalize: bool, one_hot: bool)[source]
Encodes both input features and the target feature when they are concatenated into a single NumPy array.
Assumes the last column of X_all is the target feature.
Parameters:
- X_allnp.ndarray
A NumPy array where input features are in all columns except the last one, and the target feature is in the last column. Expected shape: (num_samples, num_features + 1).
- normalizebool
Whether to normalize contiguous features (both input and target).
- one_hotbool
Whether to perform one-hot encoding for categorical values (both input and target).
Returns:
- np.ndarray[np.float64]
The combined encoded features and target.
- encode_y(y: ndarray | Series, normalize: bool = True, one_hot: bool = True) ndarray[float64][source]
Encodes the target feature (y) according to the DataHandler’s configuration.
This method transforms the raw target variable into a format suitable for model training or inference, handling normalization and one-hot encoding as specified.
Parameters:
- yOneDimData
The target feature data. Can be a pandas Series or a NumPy array. Expected shape: (num_samples,).
- normalizebool, optional
If True, the target feature will be normalized (if it’s contiguous). Defaults to True.
- one_hotbool, optional
If True, categorical target feature will be one-hot encoded. If False, categorical values will be mapped to negative integers. Defaults to True.
Returns:
- np.ndarray[np.float64]
The encoded target feature. The shape depends on one_hot and the target type: - If one_hot is True and target is categorical: (num_samples, num_unique_target_values) - Otherwise: (num_samples,)
- encoding_width(one_hot: bool) int[source]
Calculates the total width of the encoded input features.
This method determines the number of columns that the encoded data matrix will have, considering whether one-hot encoding is applied.
Parameters:
- one_hotbool
If True, the width for one-hot encoding will be considered. If False, the width for direct mapping (e.g., negative integers for categories) will be used.
Returns:
- int
The total number of columns in the encoded input feature matrix.
- property feature_names: list[str]
A list of names for all input features.
Returns:
- list[str]
A list of strings, where each string is the name of an input feature.
- property features: list[Feature]
A list of Feature objects representing the input features.
Returns:
- list[Feature]
A list containing instances of Feature (e.g., Contiguous, Categorical, etc.).
- property greater_than: list[tuple[Feature, Feature]]
—