The Problem
SHAP is powerful but can be slow for non-tree models (KernelExplainer) and sometimes produces explanations that are hard to explain to non-technical stakeholders. LIME offers an alternative approach: instead of computing exact Shapley values, it fits a simple, interpretable model (like a linear regression) locally around the prediction being explained.
How LIME works
- Take the instance you want to explain
- Generate perturbed samples around it (slightly modified versions)
- Get the model's predictions for all perturbed samples
- Weight the samples by proximity to the original instance
- Fit a simple linear model on the weighted samples
- The linear model's coefficients are the explanation
The result is a set of feature weights that show how each feature contributed to this specific prediction — similar to SHAP's local explanation but using a different mathematical approach.
SHAP vs. LIME: when they differ
| Criterion | SHAP | LIME |
|---|
| Theoretical basis | Shapley values (game theory) | Local surrogate models |
| Consistency | Guaranteed (same input = same output) | Can vary between runs (sampling-based) |
| Speed (tree models) | Fast (TreeExplainer) | Moderate |
| Speed (non-tree) | Slow (KernelExplainer) | Moderate |
| Global explanations | Yes (summary plots) | No (local only) |
| Interpretability | Waterfall/force plots | Feature weight bars |
When LIME is preferable: When you need a quick local explanation for a non-tree model and don't need global explanations. When you want a simpler narrative ("this feature had weight +0.3") vs. SHAP's game-theoretic framing.
When SHAP is preferable: When you need both global and local explanations. When consistency matters (regulatory contexts). When you're using tree-based models (TreeExplainer is fast and exact).
Building the LIME module
"""
explain_lime.py
LIME-based local explanations and SHAP comparison.
"""
import json
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def generate_lime_explanations(model, X_train, X_explain, feature_names=None,
num_features=10, num_samples=3, output_dir="."):
"""
Generate LIME local explanations for sample predictions.
Args:
model: Trained model with predict_proba
X_train: Training data (for LIME's training distribution)
X_explain: Data to explain (test set)
feature_names: Optional feature names
num_features: Number of features to include in explanation
num_samples: Number of samples to explain
output_dir: Directory to save outputs
Returns:
Dict with LIME explanations
"""
try:
import lime
import lime.lime_tabular
except ImportError:
return {"error": "LIME not installed. Install with: pip install lime"}
if feature_names is None:
feature_names = list(X_explain.columns)
# Create LIME explainer
explainer = lime.lime_tabular.LimeTabularExplainer(
training_data=X_train.values,
feature_names=feature_names,
class_names=["denied", "approved"],
mode="classification",
)
results = {"explanations": []}
# Explain selected samples
indices = list(range(min(num_samples, len(X_explain))))
for idx in indices:
exp = explainer.explain_instance(
X_explain.iloc[idx].values,
model.predict_proba,
num_features=num_features,
)
# Save the explanation plot
fig = exp.as_pyplot_figure()
fig.set_size_inches(10, 6)
plot_path = f"{output_dir}/lime_explanation_{idx}.png"
fig.savefig(plot_path, dpi=150, bbox_inches='tight')
plt.close()
# Extract explanation data
explanation_list = exp.as_list()
results["explanations"].append({
"sample_index": idx,
"prediction_features": X_explain.iloc[idx].to_dict(),
"lime_explanation": [
{"condition": cond, "weight": round(weight, 4)}
for cond, weight in explanation_list
],
"plot": plot_path,
})
return results
def compare_shap_lime(shap_results, lime_results, output_file="shap_vs_lime_comparison.md"):
"""Generate a comparison report between SHAP and LIME explanations."""
lines = []
lines.append("# SHAP vs. LIME Explanation Comparison\n")
lines.append("## Methodology\n")
lines.append("- **SHAP**: Based on Shapley values from cooperative game theory. Exact for tree models.")
lines.append("- **LIME**: Based on local surrogate models (linear approximation around the prediction).\n")
lines.append("## Per-Sample Comparison\n")
for shap_exp in shap_results.get("local_explanations", []):
idx = shap_exp["sample_index"]
lime_exp = next((e for e in lime_results.get("explanations", []) if e["sample_index"] == idx), None)
if not lime_exp:
continue
lines.append(f"### Sample {idx}\n")
lines.append("**SHAP values:**")
for feat, val in shap_exp["shap_values"].items():
lines.append(f"- {feat}: {val}")
lines.append(f"\n**LIME explanation:**")
for item in lime_exp["lime_explanation"]:
lines.append(f"- {item['condition']}: {item['weight']}")
lines.append("")
lines.append("## Key Differences\n")
lines.append("- SHAP values are additive and sum to the prediction difference from baseline")
lines.append("- LIME weights represent a local linear approximation, not exact contributions")
lines.append("- SHAP is consistent (same input always gives same output); LIME can vary between runs")
lines.append("- Both should be used together for regulatory compliance — cross-validation of explanations")
with open(output_file, "w") as f:
f.write("\n".join(lines))
return output_file
if __name__ == "__main__":
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
np.random.seed(42)
n = 3000
income = np.random.normal(55000, 22000, n).clip(15000, 200000).astype(int)
credit_score = np.random.normal(700, 85, n).clip(300, 850).astype(int)
debt_ratio = np.random.uniform(0.1, 0.6, n).round(3)
employment_years = np.random.uniform(0, 20, n).round(1)
repaid = np.where(
(income > 50000) & (credit_score > 680) & (debt_ratio < 0.4),
np.random.binomial(1, 0.9, n),
np.random.binomial(1, 0.5, n),
)
df = pd.DataFrame({
"income": income, "credit_score": credit_score,
"debt_ratio": debt_ratio, "employment_years": employment_years,
"repaid": repaid,
})
X = df.drop(columns=["repaid"])
y = df["repaid"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
model = GradientBoostingClassifier(random_state=42)
model.fit(X_train, y_train)
print(f"Model accuracy: {model.score(X_test, y_test):.2%}")
print("\nGenerating LIME explanations...")
lime_results = generate_lime_explanations(model, X_train, X_test, num_samples=3)
if "error" in lime_results:
print(lime_results["error"])
else:
with open("lime_explanations.json", "w") as f:
json.dump(lime_results, f, indent=2, default=str)
print(f"Generated {len(lime_results['explanations'])} LIME explanations")
print("Saved to lime_explanations.json")
# Try comparison with SHAP if available
try:
with open("shap_explanations.json") as f:
shap_results = json.load(f)
compare_shap_lime(shap_results, lime_results)
print("Comparison report saved to shap_vs_lime_comparison.md")
except FileNotFoundError:
print("SHAP results not found — run explain_shap.py first for comparison")
You run LIME on the same prediction 3 times and get slightly different explanations each time. SHAP gives you the same result every time. Why?
1
A stakeholder asks you to explain a loan denial and wants "one number" they can act on. Your LIME explanation gives feature weights like credit_score <= 650: -0.32. Your SHAP explanation gives a waterfall showing the same feature contributing -0.30 to the prediction. Which one do you hand to a non-technical business stakeholder, and why?
Both SHAP and LIME are approximations of "why" a model made a decision — neither is a literal, exact readout of the model's internal reasoning, even though SHAP's TreeExplainer values are mathematically exact Shapley values for the game-theoretic decomposition it defines. For a single conversational explanation to a business stakeholder, LIME's condition-based output ("credit_score <= 650") often reads more naturally. For anything that will be filed as a compliance record or shown to a regulator, prefer SHAP because it is deterministic — the same input always produces the same explanation, which matters when someone might ask you to reproduce it later. The professional move either way is to say which tool generated the number and to avoid presenting an approximation as if it were exact ground truth.