#!/usr/bin/env python # # file: xai_trio_demo.py # # description: # This script provides an educational demonstration of three fundamental # Explainable AI (XAI) techniques: Feature Ablation, SHAP (Global), and # LIME (Local). It generates an intuitive synthetic dataset, fits a # Random Forest model, and visualizes the interpretability results side-by-side. # # revision history: # 20260429 (AM): initial version #------------------------------------------------------------------------------ # import system modules # import os import sys import numpy as np import matplotlib.pyplot as plt from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score from sklearn.model_selection import train_test_split # import XAI modules # (requires: pip install shap lime) # import shap import lime import lime.lime_tabular #------------------------------------------------------------------------------ # # global variables are listed here # #------------------------------------------------------------------------------ # set the filename using basename # __FILE__ = os.path.basename(__file__) # define default values for the dataset, model, and plotting # DEF_N_SAMPLES = 500 DEF_RANDOM_SEED = 42 DEF_OUT_FILE_NAME = "xai_trio_3panel.png" DEF_PLOT_TITLE = "Explainable AI (XAI) Methods Demonstration" DEF_FEATURE_NAMES = ["Study_Hours", "Sleep_Hours", "Screen_Time", "Random_Noise"] #------------------------------------------------------------------------------ # # functions are listed here # #------------------------------------------------------------------------------ # generate intuitive synthetic data # def generate_student_data(n_samples=DEF_N_SAMPLES, seed=DEF_RANDOM_SEED): """ method: generate_student_data arguments: n_samples: total number of synthetic students to generate seed: random seed for reproducibility return: X: feature matrix of shape (n_samples, 4) y: target array of shape (n_samples,) representing test scores description: Generates a dataset with known relationships to help validate XAI tools: - Study Hours: Strong positive effect - Sleep Hours: Moderate positive effect - Screen Time: Moderate negative effect - Random Noise: No real effect """ np.random.seed(seed) study_hours = np.random.uniform(0, 10, n_samples) sleep_hours = np.random.uniform(4, 10, n_samples) screen_time = np.random.uniform(0, 8, n_samples) random_noise = np.random.uniform(0, 100, n_samples) X = np.column_stack([study_hours, sleep_hours, screen_time, random_noise]) # Base score of 40 + Feature Contributions + Gaussian Noise # y = 40 + (5.0 * study_hours) + (2.5 * sleep_hours) - (3.0 * screen_time) + np.random.normal(0, 2.0, n_samples) # exit gracefully # return X, y # perform an ablation study # def perform_ablation_study(X_train, X_test, y_train, y_test): """ method: perform_ablation_study arguments: X_train, X_test, y_train, y_test: split dataset return: ablation_drops: list of performance drops for each feature baseline_r2: the R^2 score of the model with all features description: Trains a baseline model, then systematically retrains the model by removing one feature at a time to see how much the R^2 score drops. A larger drop indicates a more important feature. """ # 1. train baseline model # baseline_model = RandomForestRegressor(random_state=DEF_RANDOM_SEED) baseline_model.fit(X_train, y_train) baseline_preds = baseline_model.predict(X_test) baseline_r2 = r2_score(y_test, baseline_preds) ablation_drops = [] # 2. systematically ablate (remove) each feature # for i in range(X_train.shape[1]): # create new datasets without feature i # X_train_ablated = np.delete(X_train, i, axis=1) X_test_ablated = np.delete(X_test, i, axis=1) # retrain and evaluate # ablated_model = RandomForestRegressor(random_state=DEF_RANDOM_SEED) ablated_model.fit(X_train_ablated, y_train) ablated_preds = ablated_model.predict(X_test_ablated) ablated_r2 = r2_score(y_test, ablated_preds) # record the drop in performance (baseline - ablated) # performance_drop = baseline_r2 - ablated_r2 ablation_drops.append(max(0, performance_drop)) # floor at 0 for clean plotting # exit gracefully # return ablation_drops, baseline_r2 # evaluate XAI models and plot the 3-panel visualization # def evaluate_and_plot(outfile=DEF_OUT_FILE_NAME): """ method: evaluate_and_plot arguments: outfile: path to save the resulting image return: status: boolean indicating success description: Generates dataset, trains a Random Forest, applies Ablation, SHAP, and LIME, and produces a side-by-side 3-panel plot of the insights. """ print("Preparing data and baseline model...") X, y = generate_student_data() X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=DEF_RANDOM_SEED) # Train the primary model for SHAP and LIME to interpret # main_model = RandomForestRegressor(random_state=DEF_RANDOM_SEED) main_model.fit(X_train, y_train) # setup a 3-panel figure layout # fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 6)) features = DEF_FEATURE_NAMES y_pos = np.arange(len(features)) # --- PANEL 1: ABLATION STUDY (GLOBAL) --- # print("Performing feature ablation study...") ablation_drops, base_r2 = perform_ablation_study(X_train, X_test, y_train, y_test) ax1.barh(y_pos, ablation_drops, color='skyblue', edgecolor='k', alpha=0.8) ax1.set_yticks(y_pos) ax1.set_yticklabels(features, fontsize=11) ax1.invert_yaxis() # highest impact at the top ax1.set_xlabel("Drop in $R^2$ Score (Higher = More Important)") ax1.set_title("Ablation Study (Global)\nBaseline $R^2$: %.2f" % base_r2, fontsize=14) ax1.grid(True, linestyle='--', alpha=0.5, axis='x') # --- PANEL 2: SHAP FEATURE IMPORTANCE (GLOBAL) --- # print("Calculating SHAP values...") explainer_shap = shap.TreeExplainer(main_model) shap_values = explainer_shap.shap_values(X_test) # Calculate mean absolute SHAP value for each feature # mean_abs_shap = np.mean(np.abs(shap_values), axis=0) ax2.barh(y_pos, mean_abs_shap, color='lightcoral', edgecolor='k', alpha=0.8) ax2.set_yticks(y_pos) ax2.set_yticklabels(features, fontsize=11) ax2.invert_yaxis() ax2.set_xlabel("Mean |SHAP Value| (Impact on Model Output)") ax2.set_title("SHAP Feature Importance (Global)\nAverage contribution magnitude", fontsize=14) ax2.grid(True, linestyle='--', alpha=0.5, axis='x') # --- PANEL 3: LIME (LOCAL INSTANCE EXPLANATION) --- # print("Generating LIME explanation for a single instance...") explainer_lime = lime.lime_tabular.LimeTabularExplainer( training_data=X_train, feature_names=features, mode='regression', random_state=DEF_RANDOM_SEED ) # Pick a specific student (instance) from the test set to explain # instance_idx = 0 student_instance = X_test[instance_idx] lime_exp = explainer_lime.explain_instance( data_row=student_instance, predict_fn=main_model.predict, num_features=4 ) # Extract LIME weights and names # lime_list = lime_exp.as_list() lime_features = [x[0] for x in lime_list] lime_weights = [x[1] for x in lime_list] # Map colors based on positive/negative impact for this specific student # colors = ['lightgreen' if w > 0 else 'salmon' for w in lime_weights] y_pos_lime = np.arange(len(lime_features)) ax3.barh(y_pos_lime, lime_weights, color=colors, edgecolor='k', alpha=0.8) ax3.set_yticks(y_pos_lime) ax3.set_yticklabels(lime_features, fontsize=10) ax3.invert_yaxis() ax3.set_xlabel("Local Weight (Impact on this specific prediction)") predicted_score = main_model.predict(student_instance.reshape(1, -1))[0] ax3.set_title("LIME Explanation (Local)\nWhy did Student %d score %.1f?" % (instance_idx, predicted_score), fontsize=14) ax3.axvline(0, color='gray', linestyle='-', linewidth=1.5) ax3.grid(True, linestyle='--', alpha=0.5, axis='x') # add a main title to the figure # fig.suptitle(DEF_PLOT_TITLE, fontsize=18, y=1.02, fontweight='bold') # adjust layout # plt.tight_layout() # save the plot to disk # try: plt.savefig(outfile, dpi=150, bbox_inches='tight') print("\nSaved visualization to: %s" % outfile) except Exception as e: print("**> Error saving plot: %s" % str(e)) return False # display the plot to the user # plt.show() # exit gracefully # return True # function: main # def main(argv): print("--- Starting Explainable AI (XAI) Demonstration ---") status = evaluate_and_plot() if not status: print("**> Process failed during plotting.") return False print("--- Demonstration Complete ---") # exit gracefully # return True # begin gracefully # if __name__ == '__main__': main(sys.argv[0:]) # # end of file