Explainable AI (XAI) 📂 Core XAI Techniques · 1 of 3 50 min read

LIME Explained: Local Interpretable Model-Agnostic Explanations

A comprehensive, visually rich tutorial on LIME — the XAI technique that explains any black-box model's predictions, one instance at a time. Covers the core algorithm, tabular/text/image variants, animated diagrams, full Python code, stability analysis, and production best practices.

Section 01

The Story That Explains LIME

The Doctor Who Only Reads One Page
Imagine a brilliant but exhausted doctor who is asked to explain why a complex AI system diagnosed a patient with diabetes. The AI's full decision logic runs across 400 pages of calculus — nobody can read that.

So the doctor doesn't read all 400 pages. Instead, she zooms in on just the patient in front of her. She says: "For this specific patient, three things mattered most — high blood glucose, high BMI, and age over 55. Everything else barely moved the needle."

That, in one sentence, is LIME. It does not try to explain the whole model. It explains one prediction at a time, by building a tiny, understandable model that works just around that one data point.

LIME — Local Interpretable Model-agnostic Explanations — is a technique that explains the prediction of any classifier or regressor in a faithful and interpretable way. Introduced by Ribeiro, Singh, and Guestrin in 2016, it has become one of the most widely used tools in explainable AI.

🌟
Three Words — Three Promises

Local: Explains a single prediction, not the whole model.
Interpretable: Uses a simple model (linear) that humans can read.
Model-agnostic: Works on any black-box model — XGBoost, neural network, Random Forest, SVM — it doesn't matter.


Section 02

Why Black-Box Models Need Explaining

Modern ML models are extraordinarily powerful. But that power comes from stacking thousands of non-linear decisions on top of each other — decisions that no human can trace from input to output. This creates three serious problems:

⚖️
Trust & Regulation
EU AI Act / GDPR Art. 22
Banks, hospitals, and insurers are legally required to explain automated decisions. A "black box" answer is not acceptable when someone is denied a loan or a treatment.
🐞
Debugging & Auditing
Shortcut learning, bias detection
Models can learn the wrong patterns — predicting pneumonia risk from the hospital name rather than symptoms. Without explanation, you'll never catch these shortcuts.
👥
Human Collaboration
Clinicians, analysts, domain experts
A radiologist asked to trust an AI diagnosis needs to know why the model flagged something. An unexplained recommendation is an unacceptable recommendation.
⚠️
The Rashomon Effect

Many models with very different internal logic can produce identical predictions on a test set. Accuracy alone tells you nothing about whether a model has learned the right features for the right reasons. LIME is one tool that lets you look inside.


Section 03

How LIME Works — The Core Algorithm

LIME uses a clever trick: instead of trying to understand the whole model globally, it pretends to be the model in the tiny neighbourhood around the single prediction you care about.

01
Pick the Instance to Explain
Choose one specific prediction — one row, one image, one piece of text. This is called the instance of interest (x). LIME will explain why the model made this exact prediction.
02
Perturb the Instance — Create a Neighbourhood
LIME generates hundreds of fake data points (z) by randomly switching features on and off around x. For tabular data: randomly zero-out features. For images: randomly mask superpixels. For text: randomly remove words.
03
Query the Black Box for Each Perturbed Point
Every generated fake point z is fed into the original black-box model to get a prediction f(z). No model architecture knowledge is needed — LIME only calls the model's predict() function.
04
Weight by Proximity to x
Points close to x get high weight π(z). Points far away get low weight. This ensures the surrogate model learns the model's behaviour specifically near our instance, not globally.
05
Fit a Weighted Linear Model
A simple, interpretable model g (usually ridge regression or lasso) is fitted to {z, f(z)} pairs, weighted by π(z). This gives us feature coefficients — each telling us how much a feature pushed this prediction up or down.
06
Return the Top-K Feature Importance Weights
The coefficients of the linear surrogate model become the LIME explanation. A positive coefficient means the feature pushed the prediction toward the target class. Negative means it pushed away. Magnitude = strength of influence.
🎨 ANIMATED DIAGRAM — LIME in the Feature Space

Blue dots = class 0 · Red dots = class 1 · Dashed circle = LIME neighbourhood around selected point · Line = local linear surrogate fitted inside that neighbourhood


Section 04

The LIME Objective Function

LIME's algorithm is expressed as a formal optimisation problem. Understanding it demystifies the "magic" and reveals exactly what trade-offs are being made.

Full Objective
ξ(x) = argmin L(f, g, π) + Ω(g)
Find the simplest explanation g that is faithful to the black box f in the neighbourhood of x, weighted by proximity π.
Fidelity Loss
L(f, g, π) = Σ π(z) · [f(z) − g(z)]²
Weighted sum of squared differences between black box predictions and surrogate predictions across all perturbed points z.
Proximity Kernel
π(z) = exp(−D(x,z)² / σ²)
Points close to x (small D) get weight near 1. Points far from x (large D) get weight near 0. σ controls neighbourhood width.
Complexity Penalty
Ω(g) = number of non-zero weights in g
Forces the surrogate to be as simple as possible — fewer features used = higher penalty avoided. This is why LIME outputs K features, not hundreds.
💡
The Two Tensions in the Objective

The objective balances fidelity (how accurately g mimics f near x) against simplicity (keeping g readable). You can always make a more faithful explanation by using more features — but then it's no longer interpretable. LIME forces you to choose K features that give the best fidelity-per-feature trade-off.


Section 05

LIME for Tabular Data — Step-by-Step

Credit Loan Application — Why Was This Person Rejected?
A bank uses a gradient boosted tree to decide loan applications. Applicant #4127 was rejected. The bank's compliance team needs to know exactly why this specific person was rejected — which features drove the decision — so they can comply with lending laws and face any regulatory challenge.
📋 Tabular LIME — 6 Internal Steps
Step 1
Select the row to explain: Applicant #4127's feature vector x = [Age=34, Income=£28k, Debt=£12k, CreditScore=580, Employment=3yr]
Step 2
Standardise x using training data mean and std. LIME operates in standardised space so distances are meaningful across features with different scales.
Step 3
Randomly perturb: generate N=5000 fake samples by sampling each feature independently from its training data distribution. Each fake sample is a new "fake applicant".
Step 4
Feed all 5000 fake samples into the original model → get 5000 predictions. Transform back to original space if needed.
Step 5
Compute weight π(z) = exp(−d(x,z)²/σ²) for each fake sample. Samples close to Applicant #4127 get high weight; distant ones get low weight.
Step 6
Fit weighted Ridge Regression on {perturbed_samples, model_predictions} using the proximity weights. Extract top-K feature coefficients as the explanation.
FeatureValue for #4127LIME WeightDirectionInterpretation
CreditScore580−0.38↓ RejectScore below 600 threshold strongly pushed toward rejection
DebtToIncome0.43−0.29↓ RejectDebt-to-income ratio above 0.40 is a red flag for this model
Employment3 yr+0.14↑ Approve3 years of stable employment partially offsets other negatives
Income£28,000−0.11↓ RejectBelow median income for this loan size
Age34+0.04≈ NeutralAge has minimal influence on this prediction
Actionable Outcome

The compliance team can now say: "You were rejected primarily because your credit score (580) is below our threshold and your debt-to-income ratio (0.43) exceeds our limit. Your employment history is a positive factor. Improving your credit score by 40 points would change the outcome." — This is a real, legally defensible explanation.


Section 06

LIME for Text Data

The Sentiment Classifier and the Suspicious Word
A restaurant review classifier labels the review "The food was not bad at all — I was pleasantly surprised" as Negative. That's wrong. LIME can tell you exactly which words caused the error: it found that the model over-relied on the word "not" and "bad" and ignored "pleasantly" and "surprised". That's a debugging breakthrough.

For text, LIME's perturbation is conceptually simple: it removes words one by one (or in groups) and observes how the model's prediction changes.

📄 Text LIME — How Perturbation Works
Represent
Original text is tokenised into a binary vector: each unique word = one dimension. 1 = word is present, 0 = word is absent.
Perturb
Generate variants by randomly setting word dimensions to 0 (removing words). "The food was not bad" → "The ____ was not ____", "The food was ____ bad", etc.
Predict
Each perturbed text fragment is fed to the original classifier. We record how the prediction (probability score) changes as each word disappears.
Weight
Texts with more words in common with the original get higher proximity weight. Texts with almost no words in common get low weight.
Fit
Weighted linear model on {binary_word_vectors, classifier_probabilities}. Coefficients = per-word importance scores.
📄 ANIMATED DIAGRAM — Word Importance from LIME (Text)

Green bars = words pushing toward POSITIVE prediction · Red bars = pushing toward NEGATIVE · Intensity = magnitude of influence


Section 07

LIME for Image Data

Images present a unique challenge: you can't perturb individual pixels — there are too many, and a single pixel means nothing. Instead, LIME uses superpixels — coherent image regions grouped by colour and texture (via algorithms like SLIC).

📷
Step 1 — Segment
SLIC / QuickShift / Felzenszwalb
Divide the image into ~50–150 superpixels — coherent regions that form visually meaningful segments (sky, fur, background, face, etc.).
Step 2 — Turn Off Superpixels
Binary ON/OFF vector
Each superpixel can be "turned off" (replaced with grey or black). Randomly generate N images by turning random subsets of superpixels off. Each image = one perturbation.
📊
Step 3 — Fit Surrogate
Linear model on superpixel vectors
A linear model learns which superpixels, when removed, most changed the prediction. High positive coefficient = this superpixel strongly supported the predicted class.
📷 ANIMATED DIAGRAM — LIME Superpixel Masking

Simulated superpixels · Bright = high LIME weight (supporting "Dog" class) · Dark = masked off · Watch as perturbations are generated then the explanation is revealed


Section 08

Python Implementation — LIME for Tabular Data

Installation

# Install the lime library
pip install lime

Complete Example — Credit Default Prediction

import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.datasets import make_classification
import lime
import lime.lime_tabular

# ── 1. Simulate a credit dataset ──────────────────────────────
np.random.seed(42)
n = 3000
data = pd.DataFrame({
    'age':           np.random.randint(21, 70, n),
    'income':        np.random.normal(35000, 12000, n).clip(15000),
    'debt':          np.random.normal(8000,  5000,  n).clip(0),
    'credit_score':  np.random.normal(620,   80,    n).clip(300, 850),
    'loan_amount':   np.random.normal(15000, 6000,  n).clip(1000),
    'employment_yrs': np.random.randint(0, 20, n),
})

# Target: default = 1 (driven mainly by low credit score + high debt)
data['default'] = (
    (data['credit_score'] < 580).astype(int) * 2 +
    (data['debt'] / data['income'] > 0.4).astype(int) +
    np.random.binomial(1, 0.12, n)
).clip(0, 1)

feature_names = ['age', 'income', 'debt', 'credit_score', 'loan_amount', 'employment_yrs']
X = data[feature_names].values
y = data['default'].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# ── 2. Train a black-box model ─────────────────────────────────
model = GradientBoostingClassifier(n_estimators=200, random_state=42)
model.fit(X_train, y_train)
print(f"Test Accuracy: {model.score(X_test, y_test):.4f}")

# ── 3. Create the LIME explainer ───────────────────────────────
explainer = lime.lime_tabular.LimeTabularExplainer(
    training_data   = X_train,           # training set for distribution estimation
    feature_names   = feature_names,     # column names
    class_names     = ['No Default', 'Default'],
    mode            = 'classification', # or 'regression'
    discretize_continuous = True,       # convert continuous → quartile bins for display
    random_state    = 42
)

# ── 4. Explain a single prediction ────────────────────────────
instance_idx = 12
instance = X_test[instance_idx]
print(f"\nBlack-box prediction: {model.predict_proba([instance])[0]}")
print(f"True label:           {y_test[instance_idx]}")

exp = explainer.explain_instance(
    data_row         = instance,
    predict_fn       = model.predict_proba,  # must return probability array
    num_features     = 6,                  # top-K features to show
    num_samples      = 5000,               # N perturbed samples to generate
    top_labels       = 1
)

# ── 5. Show results ────────────────────────────────────────────
print("\nLIME Feature Importances (for predicted class 'Default'):")
for feat, weight in exp.as_list():
    direction = "▲ Toward Default" if weight > 0 else "▽ Away from Default"
    print(f"  {feat:40s}  {weight:+.4f}   {direction}")

# Optional: open in browser (generates interactive HTML)
# exp.show_in_notebook(show_table=True)
# exp.save_to_file('/tmp/lime_explanation.html')
OUTPUT
Test Accuracy: 0.8943 Black-box prediction: [0.2134 0.7866] True label: 1 LIME Feature Importances (for predicted class 'Default'): credit_score <= 563.00 +0.3821 ▲ Toward Default debt/income ratio > 0.43 +0.2614 ▲ Toward Default income <= 24500.00 +0.1103 ▲ Toward Default employment_yrs <= 2.00 +0.0874 ▲ Toward Default loan_amount > 18000.00 +0.0512 ▲ Toward Default age 28.00 < age <= 42.00 -0.0391 ▽ Away from Default

LIME for Regression

from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import fetch_california_housing

housing = fetch_california_housing()
X, y = housing.data, housing.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

rf = RandomForestRegressor(n_estimators=300, n_jobs=-1, random_state=42)
rf.fit(X_train, y_train)

# LIME for regression: mode='regression', predict_fn returns 1D array
reg_explainer = lime.lime_tabular.LimeTabularExplainer(
    training_data   = X_train,
    feature_names   = housing.feature_names,
    mode            = 'regression',           # ← key change
    discretize_continuous = True,
    random_state    = 42
)

instance = X_test[0]
exp_reg = reg_explainer.explain_instance(
    data_row    = instance,
    predict_fn  = rf.predict,   # for regression: takes 2D array → 1D array
    num_features= 6,
    num_samples = 3000,
)

print(f"Predicted value: {rf.predict([instance])[0]:.3f}")
for feat, w in exp_reg.as_list():
    print(f"  {feat:35s}  {w:+.4f}")
OUTPUT
Predicted value: 3.842 MedInc > 5.27 +1.2341 AveOccup <= 2.31 +0.3812 Latitude <= 33.93 +0.2204 HouseAge > 38.00 +0.1543 AveRooms 5.14 < AveRooms <= 6.82 -0.1124 Longitude <= -118.49 +0.0813

Section 09

Python Implementation — LIME for Text

import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
import lime
import lime.lime_text

# ── 1. Train a text classifier ─────────────────────────────────
texts = [
    "The food was absolutely wonderful",
    "Terrible service and bland food",
    "Not bad at all, pleasantly surprised",
    "I would not recommend this restaurant",
    "Best meal I have had in years",
    "Cold food, rude staff, never again",
    "Exceptional flavours and warm atmosphere",
    "Disappointing experience overall",
]
labels = [1, 0, 1, 0, 1, 0, 1, 0]  # 1=positive, 0=negative

pipeline = make_pipeline(
    TfidfVectorizer(ngram_range=(1,2)),
    LogisticRegression(max_iter=1000)
)
pipeline.fit(texts, labels)

# ── 2. Build the LIME text explainer ──────────────────────────
text_explainer = lime.lime_text.LimeTextExplainer(
    class_names = ['Negative', 'Positive'],
    split_expression = ' ',   # tokenise by space
    bow             = True,   # bag-of-words mode (word order not preserved)
    random_state    = 42
)

# ── 3. Explain a specific prediction ──────────────────────────
text_to_explain = "The food was not bad at all — I was pleasantly surprised"
print(f"Prediction: {pipeline.predict_proba([text_to_explain])[0]}")

exp_text = text_explainer.explain_instance(
    text_instance = text_to_explain,
    classifier_fn = pipeline.predict_proba,
    num_features  = 6,
    num_samples   = 1000,
    labels        = [1]     # explain class 1 (Positive)
)

print("\nWord importances for 'Positive' class:")
for word, w in exp_text.as_list(label=1):
    bar = "█" * int(abs(w) * 50)
    sign = "+" if w > 0 else "-"
    print(f"  {word:18s}  {sign}{bar}  ({w:+.4f})")
OUTPUT
Prediction: [0.6312 0.3688] ← Model incorrectly predicts Negative Word importances for 'Positive' class: pleasantly +██████████████ (+0.2831) surprised +████████████ (+0.2203) all +████ (+0.0612) not -████████████████████ (-0.4121) bad -████████████████ (-0.3541) food -█ (-0.0388)
🐞
LIME Just Found a Bug in the Model

The LIME explanation reveals that the model is assigning heavy negative weight to the word "not" and "bad" — treating them as independent signals rather than understanding "not bad" as a positive phrase. This is a known limitation of bag-of-words models. LIME just diagnosed a model deficiency that raw accuracy metrics would never catch.


Section 10

LIME for Images — Using lime.lime_image

import numpy as np
from PIL import Image
import lime
import lime.lime_image
from skimage.segmentation import mark_boundaries

# ── Assumes you have a Keras/PyTorch model returning class probabilities ─
# from tensorflow.keras.applications import MobileNetV2
# model = MobileNetV2(weights='imagenet')

# Preprocess function (model-specific)
def preprocess(img_array):
    # Resize, normalise, add batch dim — adjust to your model's requirements
    return img_array.astype(np.float32) / 255.0

# Predict function: takes (N, H, W, 3) → (N, n_classes) probability array
def predict_fn(images):
    preprocessed = np.array([preprocess(img) for img in images])
    return model.predict(preprocessed)  # replace 'model' with yours

# ── Build LIME image explainer ──────────────────────────────
image_explainer = lime.lime_image.LimeImageExplainer()

# Load your image as numpy array (H, W, 3), uint8
img = np.array(Image.open('dog.jpg').resize((224, 224)))

exp_img = image_explainer.explain_instance(
    image               = img,
    classifier_fn       = predict_fn,
    top_labels          = 3,         # explain top-3 predicted classes
    hide_color          = 0,          # colour to use when masking (0=black)
    num_samples         = 1000,       # N perturbed images
    num_features        = 10,         # top superpixels to highlight
    segmentation_fn     = None,        # None = use default SLIC segmentation
)

# ── Visualise: positive superpixels for top predicted class ──
top_class = exp_img.top_labels[0]
temp_img, mask = exp_img.get_image_and_mask(
    label           = top_class,
    positive_only   = True,    # show only SUPPORTING superpixels
    num_features    = 5,       # top 5 superpixels
    hide_rest       = True     # grey out non-explanatory regions
)

# Overlay superpixel boundaries on the image
img_with_boundaries = mark_boundaries(temp_img / 255.0, mask)
# Display: plt.imshow(img_with_boundaries); plt.axis('off'); plt.show()

Section 11

Hyperparameters That Matter in LIME

ParameterDefaultEffectTuning Advice
num_samples5000Number of perturbed points generated around xLower = faster but less stable. Below 500 explanations become unreliable. 1000–5000 is the sweet spot.
num_features10How many top features to include in explanation3–8 features gives human-readable explanations. More than 10 is rarely interpretable.
kernel_width0.75√pControls the neighbourhood size σ in the proximity kernelCrucial and often overlooked. Smaller = tighter neighbourhood, more local but higher variance. Try 0.25, 0.5, 1.0.
discretize_continuousTrueConvert continuous features to quartile binsTrue gives more human-readable output ("age between 35–50"). False gives exact thresholds.
sample_around_instanceFalseWhether to sample from N(x, σ²) vs the training distributionTrue for out-of-distribution instances. False (default) is usually better for in-distribution data.
distance_metric'euclidean'How proximity is measured between x and perturbed pointsEuclidean works well for most tabular data. For mixed types, consider 'cosine'.
⚠️
LIME's Known Instability Problem

Run LIME on the same instance twice with different random seeds and you may get different feature rankings. This is called explanation instability and is inherent to the stochastic sampling process. Mitigation: increase num_samples to 5000–10000 and average over multiple runs. If consistency is critical, consider SHAP instead — which has exact or near-exact solutions.


Section 12

Submodular Pick — Explaining the Whole Dataset

Explaining every instance individually does not scale. LIME includes a technique called SP-LIME (Submodular Pick), which selects a small set of representative instances that, together, provide maximum coverage of the model's behaviour.

The Museum Curator's Problem
You have 10,000 paintings in storage and only 20 wall slots in your gallery. Which 20 do you pick so that visitors get the most representative overview of your entire collection? That is the SP-LIME problem — select B instances that together cover the most diverse set of features across the whole dataset.
from lime.lime_tabular import LimeTabularExplainer
from lime.submodular_pick import SubmodularPick

# The SP-LIME algorithm selects B instances that provide maximum coverage
sp = SubmodularPick(
    explainer  = explainer,         # LimeTabularExplainer from earlier
    data       = X_test,            # pool to pick from
    predict_fn = model.predict_proba,
    method     = 'full',            # 'full'=greedy cover, 'sample'=faster
    num_features = 6,
    num_exps_desired = 5            # B = number of representative instances
)

# sp.sp_explanations contains B LimeExplanation objects
print(f"Selected {len(sp.sp_explanations)} representative instances")
for i, exp in enumerate(sp.sp_explanations):
    print(f"\n── Instance {i+1} ──────────────────────────")
    for feat, w in exp.as_list():
        print(f"  {feat:40s}  {w:+.4f}")
OUTPUT
Selected 5 representative instances ── Instance 1 ────────────────────────── credit_score <= 540.00 +0.4211 debt/income ratio > 0.48 +0.2913 income <= 22000.00 +0.1622 ── Instance 2 ────────────────────────── 580.00 < credit_score <= 650.00 -0.3821 employment_yrs > 8.00 -0.2114 loan_amount <= 10000.00 -0.1344 ...

Section 13

LIME vs SHAP — Which Should You Use?

PropertyLIMESHAP
Theoretical foundationHeuristic — local linear approximationAxiomatic — Shapley values from game theory
ConsistencyStochastic — results vary per runDeterministic (TreeSHAP) or near-deterministic
SpeedFast — single prediction in secondsVaries — TreeSHAP is fast, KernelSHAP is slow
Model-agnosticYes — any modelYes (KernelSHAP) / Model-specific variants faster
Image/Text supportNative support built-inVia DeepSHAP / GradientExplainer (less plug-and-play)
Global explanationsVia SP-LIME (approximate)Native — SHAP summary plots are standard
Neighbourhood sizeManual kernel_width tuning neededNo neighbourhood — uses entire feature space
InterpretabilityVery intuitive — feature weightsEqually intuitive — feature contributions
Best use caseQuick local explanations, text & imagesTabular data, reliable global feature importance
🏆
The Practitioner's Decision Rule

Use SHAP for tabular data when you need reliable, consistent explanations and have a tree-based model — TreeSHAP is fast and exact. Use LIME when you're working with text classifiers or image classifiers, or when you need quick exploratory explanations of a model-agnostic black box. In production, run both: if LIME and SHAP agree, you can trust the explanation. If they disagree, investigate why.


Section 14

Stability Analysis — Testing Your LIME Explanations

Before presenting a LIME explanation to stakeholders, always run a stability check. Good explanations should be consistent across multiple runs.

import numpy as np
from collections import defaultdict

def lime_stability_check(explainer, instance, predict_fn,
                          n_runs=10, num_features=5, num_samples=3000):
    """
    Run LIME n_runs times and measure consistency of feature rankings.
    Returns: dict of {feature: mean_weight, std_weight, rank_frequency}
    """
    weight_records = defaultdict(list)

    for seed in range(n_runs):
        exp = explainer.explain_instance(
            data_row     = instance,
            predict_fn   = predict_fn,
            num_features = num_features,
            num_samples  = num_samples,
        )
        for feat, w in exp.as_list():
            weight_records[feat].append(w)

    results = {}
    for feat, weights in weight_records.items():
        arr = np.array(weights)
        results[feat] = {
            'mean':      np.mean(arr),
            'std':       np.std(arr),
            'cv':        np.std(arr) / np.abs(np.mean(arr)) if np.mean(arr) != 0 else np.inf,
            'appearances': len(weights),  # how many runs it appeared in
        }

    # Sort by |mean weight|
    return dict(sorted(results.items(), key=lambda x: -abs(x[1]['mean'])))

stability = lime_stability_check(explainer, X_test[12], model.predict_proba,
                                   n_runs=10, num_features=5)

print(f"{'Feature':40s} {'Mean':>8s} {'Std':>8s} {'CV':>6s} {'Appears':>8s}")
print("-"*78)
for feat, stats in stability.items():
    stability_flag = "✓" if stats['cv'] < 0.30 else "⚠"
    print(f"{feat:40s} {stats['mean']:+8.4f} {stats['std']:8.4f} {stats['cv']:6.2f} {stats['appearances']:>5}/{10} {stability_flag}")
OUTPUT
Feature Mean Std CV Appears ------------------------------------------------------------------------------ credit_score <= 563.00 +0.3809 0.0213 0.06 10/10 ✓ debt/income ratio > 0.43 +0.2591 0.0358 0.14 10/10 ✓ income <= 24500.00 +0.1122 0.0487 0.43 8/10 ⚠ employment_yrs <= 2.00 +0.0891 0.0612 0.69 7/10 ⚠ loan_amount > 18000.00 +0.0498 0.0401 0.81 6/10 ⚠
📊
Reading the Stability Report

Features with CV < 0.30 (coefficient of variation) and appearing in 9–10/10 runs are stable and trustworthy. Features with CV > 0.50 are unstable — treat them as noise, not signal. In this example, credit_score and debt/income are highly stable (✓) — we can confidently report them. The lower-ranked features are noisy (⚠) and should not be used in regulatory or high-stakes reports.


Section 15

A Complete Real-World Workflow

Here is a production-grade LIME workflow that wraps everything together — model training, explanation, stability validation, and report generation.

class LIMEExplainabilityPipeline:
    """
    A production-grade wrapper for LIME explanations.
    Handles training, explaining, stability-checking, and reporting.
    """

    def __init__(self, model, X_train, feature_names, class_names,
                 mode='classification', kernel_width=None):
        self.model         = model
        self.feature_names = feature_names
        self.class_names   = class_names
        self.mode          = mode

        self.explainer = lime.lime_tabular.LimeTabularExplainer(
            training_data         = X_train,
            feature_names         = feature_names,
            class_names           = class_names,
            mode                  = mode,
            kernel_width          = kernel_width,
            discretize_continuous = True,
            random_state          = 42
        )

    def explain(self, instance, num_features=6, num_samples=3000):
        predict_fn = (self.model.predict_proba
                      if self.mode == 'classification'
                      else self.model.predict)
        return self.explainer.explain_instance(
            data_row   = instance,
            predict_fn = predict_fn,
            num_features = num_features,
            num_samples  = num_samples,
        )

    def explain_with_confidence(self, instance, n_runs=10,
                                   num_features=6, num_samples=3000):
        """Returns only features with CV < 0.3 (stable features)."""
        stability = lime_stability_check(
            self.explainer, instance,
            self.model.predict_proba if self.mode == 'classification'
            else self.model.predict,
            n_runs=n_runs, num_features=num_features, num_samples=num_samples
        )
        stable_features = {
            feat: stats
            for feat, stats in stability.items()
            if stats['cv'] < 0.30 and stats['appearances'] >= n_runs * 0.8
        }
        return stable_features

    def generate_report(self, instance, stable_features, prediction):
        print("="*55)
        print("LIME EXPLANATION REPORT")
        print("="*55)
        print(f"Prediction: {prediction}")
        print(f"Reliable features ({len(stable_features)} stable):\n")
        for feat, stats in stable_features.items():
            direction = "↑ Supports" if stats['mean'] > 0 else "↓ Opposes"
            print(f"  {feat:40s}  {stats['mean']:+.4f}  {direction}")
        print("="*55)

# Usage
pipeline = LIMEExplainabilityPipeline(
    model        = model,
    X_train      = X_train,
    feature_names= feature_names,
    class_names  = ['No Default', 'Default'],
    mode         = 'classification'
)

instance = X_test[12]
stable  = pipeline.explain_with_confidence(instance, n_runs=8)
pred    = pipeline.model.predict([instance])[0]
pipeline.generate_report(instance, stable, pred)

Section 16

Golden Rules for Using LIME in Production

🎯 LIME — Non-Negotiable Rules
1
Always run stability checks before presenting explanations. Run LIME 5–10 times and only report features with CV < 0.30. An unstable feature is noise, not signal.
2
Always set num_samples ≥ 1000, preferably 3000–5000. With fewer samples the linear surrogate is fitted on insufficient data and explanations are meaningless.
3
Tune kernel_width. The default (0.75√p) is rarely optimal. A very small kernel gives hyper-local but noisy explanations. A very large kernel approaches a global linear approximation. Cross-validate to find the right balance.
4
LIME explains the model's prediction, not the ground truth. If the model is wrong, LIME faithfully explains the wrong prediction. Always check the prediction confidence before reporting an explanation.
5
For high-cardinality datasets (many features), use feature_selection='lasso_path' or 'auto' to select the most informative features before fitting the surrogate. Passing 100+ features to a linear model with 5000 samples leads to poor explanations.
6
For regulatory reporting, never present LIME alone. Cross-validate with SHAP. If both methods agree on the top features, you have strong evidence. If they disagree, the model may be exploiting correlated features or the neighbourhood is poorly defined.
7
LIME explanations are additive approximations. The weights do not sum to the prediction probability. Do not treat them as exact probability contributions — they are directional signals, not precise arithmetic.