Source code for ordinal_xai.interpretation.ice_prob

"""
Individual Conditional Expectation (ICE) Plot implementation for probability visualization in ordinal regression.

This module implements ICE plots specifically designed for visualizing probability distributions
in ordinal regression models. It extends the standard ICE plot to show how class probabilities
change as feature values change, using stacked area plots to represent the probability
distribution across ordinal classes.

Key Features:
- Stacked area plots to visualize probability distributions
- Support for both individual instance analysis and average behavior (PDP)
- Automatic handling of categorical and numerical features
- Detailed probability annotations at original feature values
- Color-coded visualization of ordinal class probabilities

The implementation is particularly useful for:
- Understanding how features affect the probability distribution across ordinal classes
- Analyzing the relationship between feature values and class probabilities
- Comparing individual instance behavior with average behavior
- Visualizing the uncertainty in ordinal predictions
"""

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from .base_interpretation import BaseInterpretation
from ..utils import pdp_modified
import matplotlib.cm as cm
from matplotlib.patches import Patch
import textwrap

[docs] class ICEProb(BaseInterpretation): """ Individual Conditional Expectation (ICE) Plot interpretation method for probabilities. This class implements ICE plots specifically designed for visualizing probability distributions in ordinal regression models. It uses stacked area plots to show how the probability distribution across ordinal classes changes as feature values change. Parameters ---------- model : object The trained ordinal regression model. Must implement predict_proba method. X : pd.DataFrame Dataset used for interpretation. Should contain the same features used during model training. y : pd.Series, optional Target labels. Not required for interpretation but useful for reference. Attributes ---------- model : object The trained ordinal regression model X : pd.DataFrame Dataset used for interpretation y : pd.Series Target labels (if provided) """
[docs] def __init__(self, model, X, y=None): """ Initialize the ICE Plot interpretation method for probabilities. Parameters ---------- model : object The trained ordinal regression model X : pd.DataFrame Dataset used for interpretation y : pd.Series, optional Target labels """ super().__init__(model, X, y)
[docs] def explain(self, observation_idx=None, feature_subset=None, plot=False): """ Generate Individual Conditional Expectation Plots for probabilities. This method computes and optionally visualizes how the model's probability distribution changes as feature values change. It uses stacked area plots to show the probability distribution across ordinal classes. Parameters ---------- observation_idx : int, optional Index of specific instance to highlight in the plot. If provided, only this instance's probability distribution will be shown along with the average (PDP) distribution. feature_subset : list, optional List of feature names or indices to plot. If None, all features are used. plot : bool, default=False Whether to create visualizations of the ICE plots. Returns ------- dict Dictionary containing ICE results for each feature: - 'grid_values': Feature values used for prediction - 'average': Average probability predictions (PDP) for each class - 'individual': Individual probability predictions for each instance and class Notes ----- - Uses stacked area plots to visualize probability distributions - Shows both individual instance probabilities and average probabilities - Includes probability annotations at original feature values - Uses a viridis colormap for different ordinal classes - Automatically handles both categorical and numerical features """ if feature_subset is None: feature_subset = self.X.columns.tolist() else: feature_subset = [self.X.columns[i] if isinstance(i, int) else i for i in feature_subset] num_features = len(feature_subset) num_cols = min(num_features, 4) # Max 4 plots per row num_rows = int(np.ceil(num_features / num_cols)) # Compute required rows if not self.model.is_fitted_: self.model.fit(self.X, self.y) # Ensure model is fitted results = {} # Compute ICE curves for each feature for idx, feature in enumerate(feature_subset): feature_idx = [self.X.columns.get_loc(feature)] ice_result = pdp_modified.partial_dependence( self.model, self.X, features=feature_idx, response_method="predict_proba", kind="both" ) results[feature] = ice_result # Create visualizations if requested if plot: fig, axes = plt.subplots(nrows=num_rows, ncols=num_cols, figsize=(7 * num_cols, 5 * num_rows)) if num_features == 1: axes = np.array([[axes]]) elif num_features <= num_cols: axes = axes.reshape(1, -1) legend_elements = None # Initialize legend elements for idx, feature in enumerate(feature_subset): row, col = divmod(idx, num_cols) ax = axes[row, col] ice_result = results[feature] x_values = ice_result['grid_values'][0] averaged_predictions = ice_result['average'] # Shape: (n_classes, n_grid_points) individual_predictions = ice_result['individual'] # Shape: (n_classes, n_instances, n_grid_points) num_ranks = averaged_predictions.shape[0] # Create colormap for different ranks cmap = cm.get_cmap('viridis', num_ranks) colors = [cmap(i) for i in range(num_ranks)] # Plot curves based on whether observation_idx is specified if observation_idx is not None: # Create a stacked area plot for the specified instance instance_probs = individual_predictions[:, observation_idx, :] # Create legend elements if not already created if legend_elements is None: legend_elements = [ Patch(facecolor=colors[rank], alpha=0.7, label=f'R{rank}') for rank in range(num_ranks) ] + [ Patch(facecolor=colors[rank], alpha=0.15, label=f'R{rank} avg', hatch='//') for rank in range(num_ranks) ] # Plot stacked areas for instance ax.stackplot(x_values, instance_probs, colors=colors, alpha=0.7, zorder=2) # Plot stacked areas for average probabilities baseline = np.zeros(len(x_values), dtype=np.float64) for rank in range(num_ranks): # Create stacked area for this rank ax.fill_between(x_values, baseline, baseline + averaged_predictions[rank], color=colors[rank], alpha=0.15, zorder=1) # Add dashed line at the top edge of this rank's area ax.plot(x_values, baseline + averaged_predictions[rank], color=colors[rank], linestyle='--', linewidth=1.5, zorder=3) # Update baseline for next rank baseline = baseline + averaged_predictions[rank] # Add original value marker and vertical line original_value = self.X.iloc[observation_idx][feature] # Add vertical line at original feature value ymin, ymax = 0, 1 # Probability bounds ax.vlines(x=original_value, ymin=ymin, ymax=ymax, colors='black', linestyles='dashed', linewidth=1.5, zorder=5) # Find probabilities at original value if isinstance(original_value, (int, float)): closest_idx = np.argmin(np.abs(x_values - original_value)) else: closest_idx = np.where(x_values == original_value)[0][0] # Get and display probabilities at original value probs_at_orig = instance_probs[:, closest_idx] prob_str = ", ".join([f"R{i}: {p:.2f}" for i, p in enumerate(probs_at_orig)]) # Add probability annotation caption = f"Probabilities at {feature}={original_value}: {prob_str}" wrapped_caption = "\n".join(textwrap.wrap(caption, width=60)) ax.text( 0.5, 1.02, wrapped_caption, ha='center', va='bottom', transform=ax.transAxes, fontsize=8, bbox=dict(facecolor='white', alpha=0.8, edgecolor='none', pad=1) ) # Set y-axis limits ax.set_ylim(0, 1) else: # Plot all instances and average (standard line plot) for i in range(len(self.X)): for rank in range(num_ranks): ax.plot(x_values, individual_predictions[rank, i, :], color=colors[rank], alpha=0.1, linewidth=0.5) # Plot the average curves for rank in range(num_ranks): ax.plot(x_values, averaged_predictions[rank], color=colors[rank], linestyle='--', linewidth=2, label=f'R{rank}') # Create simple legend with one entry per rank color if legend_elements is None: legend_elements = [ Patch(facecolor=colors[rank], label=f'Rank {rank}') for rank in range(num_ranks) ] ax.set_xlabel(feature, fontsize=12, labelpad=6) ax.set_ylabel("Probability", fontsize=8, labelpad=4) ax.grid(alpha=0.3) # Add shared legend if legend_elements is not None: fig.legend( handles=legend_elements, loc='lower right', fontsize=12, ncol=min(num_ranks, 3), # Adjust number of columns based on number of ranks handletextpad=0.5, columnspacing=0.5, frameon=True, borderpad=0.5, labelspacing=0.5, handlelength=1.5, borderaxespad=0.5, fancybox=True ) # Hide empty subplots for idx in range(num_features, num_rows * num_cols): row, col = divmod(idx, num_cols) fig.delaxes(axes[row, col]) plt.tight_layout(pad=3.0, h_pad=8.0) plt.subplots_adjust(bottom=0.25) plt.show() print(f"Generated ICE Plots for features: {feature_subset}") return results