Quick Reference · Local Interpretable Model-agnostic Explanations

LIME cheat sheet

One idea: your model is complicated globally, but nearly linear if you zoom in far enough. So perturb one row, watch the predictions move, and fit a tiny weighted linear model to that neighbourhood. Its coefficients are the explanation. The whole method lives or dies on one number — exp.score — and almost nobody checks it.

foundations & the algorithm tabular text image & submodular pick trust & tuning trap most common

Grounded against github.com/marcotcr/lime · Ribeiro, Singh & Guestrin, "Why Should I Trust You?" (KDD 2016). Every claim and figure here was executed against lime 0.2.0.1 / scikit-learn 1.8.0. The R² and stability numbers are measured, not quoted. Re-verified 2026-08-28: 0.2.0.1 (Jun 2020) is still the last release — LIME is feature-frozen; SHAP is the actively-developed alternative (see card 11).

Four steps · and the one number that decides whether the answer means anything
THE ALGORITHM — SAME FOUR STEPS FOR TABULAR, TEXT AND IMAGES 1 · PERTURB 5000 fake neighbours 2 · PREDICT BLACK BOX predict_proba only! label every neighbour 3 · WEIGHT near = heavy · far = ignored 4 · FIT A LINE sparse weighted linear model the coefficients ARE the explanation exp.as_list() exp.intercept exp.score ← R² THE GATE — exp.score is the surrogate's R² It answers: "did the straight line actually fit the neighbourhood?" MEASURED: R² ranged 0.006 → 0.777 on tabular (median 0.56) LIME renders a confident, colourful explanation at EVERY one of those values. The bet LIME makes "Any model, however twisted, is roughly a straight line if you zoom in far enough." Sometimes that bet pays: text surrogate R² = 0.9999 Sometimes it loses badly: tabular R² as low as 0.006 LIME does not know which case you're in. exp.score does. Read it, every time. Unlike SHAP, there is no additivity guarantee — the weights are just coefficients of a line that may or may not fit. This is the single most important thing on this sheet.

Foundations

Three explainers, one shape: construct with the training data, then explain_instance(row, predict_fn). The awkward part is that predict_fn has strict requirements — card 02.

01Install & the Three Explainersthat's the whole API
  • pip install lime
    Last release: 0.2.0.1 (2020). Effectively feature-frozen — but verified working on scikit-learn 1.8. It's finished, not broken.
  • from lime.lime_tabular import LimeTabularExplainer
    Rows of numbers. The most used and the most fragile — cards 04–05.
  • from lime.lime_text import LimeTextExplainer
    Documents. Where LIME genuinely shines — card 06.
  • from lime.lime_image import LimeImageExplainer
    Images, via superpixel segmentation — card 07.
  • from lime.submodular_pick import SubmodularPick
    Picks a handful of maximally non-redundant instances to explain — LIME's answer to "but that's only one row" (card 08).
  • # the universal shape
    explainer = Lime*Explainer(training_data, …)
    exp = explainer.explain_instance(row, predict_fn, num_features=10)
    exp.as_list()
    Memorise this. All three explainers follow it exactly.
02predict_fn Is Strictthe #1 error
LIME needs to see how the prediction moves across perturbations. A hard label can't show that — so classifiers must expose probabilities.
  • explainer.explain_instance(row, clf.predict)
    # NotImplementedError: LIME does not currently support
    # classifier models without probability scores.
    verified
    The single most common LIME error, and the message is at least honest about it.
  • explainer.explain_instance(row, clf.predict_proba)
    Classification: must return shape (n_samples, n_classes) — probabilities for every class, not just the positive one.
  • LimeTabularExplainer(X, mode='regression')
      .explain_instance(row, reg.predict)
    Regression: now it's plain predict, returning shape (n_samples,). The opposite of the classification rule — an easy thing to get backwards.
  • SVC() # has no predict_proba by default
    Use SVC(probability=True), or wrap the model in a function that returns probabilities.
  • predict_fn = lambda x: pipe.predict_proba(x)
    For text, pass a pipeline's predict_proba — LIME hands it raw strings, so the vectorizer must be inside.
03The Explanation Objectwhat comes back
  • exp.as_list(label=1) # [(feature, weight), …]
    The coefficients, sorted by |weight|. This is the explanation.
  • exp.score # surrogate R² ← READ THIS
    The trust gate. How well the local line fit the neighbourhood. Card 09 — and the whole reason this sheet exists.
  • exp.local_pred # the surrogate's prediction
    clf.predict_proba(row) # the real one
    Compare them. Measured on 12 rows: they differed by 0.146 on average, up to 0.424. When they diverge, the explanation describes a model you don't have.
  • exp.intercept · exp.local_exp · exp.available_labels()
    The surrogate's bias term; raw {label: [(idx, weight)]}; which labels were explained.
  • exp.show_in_notebook(show_table=True)
    exp.as_pyplot_figure()
    exp.save_to_file('exp.html')
    Notebook widget · matplotlib figure · standalone HTML. save_to_file is the one for reports and CI.

The number nobody reads

Both panels are real, measured output from the same library on the same afternoon. They are the argument for using LIME on text — and for being very careful with it on tabular data.

✓ text — R² = 0.9999

A TF-IDF + logistic pipeline on "great but awful". The neighbourhood really is linear (bag-of-words + linear model → LIME is recovering the truth), so the surrogate fits almost perfectly.

explain_instance("great but awful", pipe.predict_proba) awfulgreatbut −0.0938 +0.0856 −0.0005 (stop word, correctly ~0) exp.score = 0.9999 → trust it completely the straight line IS the model here

✗ tabular — R² down to 0.006

A RandomForest on 4 features. Across 12 rows the surrogate R² was median 0.56, minimum 0.006. At 0.006 the line explains nothing — yet LIME still returns a confident, ranked, colour-coded chart.

exp.score across 12 test rows 0.0 0.5 1.0 median 0.56 min 0.006 max 0.777 surrogate vs model: off by 0.146 avg, 0.424 worst The explanation describes a model you do not have.

Stability is a knob, not a curse

LIME's reputation for instability is real — but it's governed by num_samples, and the default (5000) is mostly fine. Measured: the same feature's weight across 8 random seeds. Turning num_samples down to speed things up is what actually makes LIME flaky.

same row · same model · 8 different random_state values · spread of the top feature's weight num_samples=100 spread 0.145 sd 0.042 — flaky, don't do this num_samples=500 spread 0.042 sd 0.014 — usable num_samples=5000 spread 0.019 sd 0.006 — the default. Stable. Verdict: set random_state, keep num_samples high, and re-run twice to confirm. Instability is manageable — a bad R² is not.

The three explainers

Same four steps, different notion of "perturb": resample a column · delete a word · switch off a superpixel. That choice is the entire difference between them.

04LimeTabularExplainerthe fragile one
  • ex = LimeTabularExplainer(
      X_train.values, # numpy, not DataFrame
      feature_names=list(X.columns),
      class_names=['cheap', 'expensive'],
      mode='classification',
      random_state=42)
    Pass the training data — it's used to learn each feature's distribution and to build the discretiser. Takes a numpy array, not a DataFrame.
  • exp = ex.explain_instance(
      row, clf.predict_proba,
      num_features=10, num_samples=5000,
      labels=(1,)) # or top_labels=1
    labels=(1,) is the default — it explains class 1 only. For multiclass, use top_labels=3.
  • categorical_features=[0, 3], categorical_names={0: [...]}
    Declare your categoricals, or LIME treats a one-hot column as continuous and perturbs it to 0.37.
  • sample_around_instance=False # the DEFAULTsurprising
    By default LIME samples from the global training distribution and then centres it — not tightly around your instance. Set True for a genuinely local neighbourhood. The default is a frequent source of poor R².
  • # perturbation ignores feature correlations
    Features are sampled independently, so LIME will happily ask your model about a 2-room mansion. Off-manifold points get real predictions, and those enter the fit.
05Discretisationwhy you see bins
By default LIME bins every continuous feature into quartiles and explains the bin, not the value. This is why the output reads like a rule, not a coefficient.
  • # VERIFIED output — note these are BINS:
    "4.00 < rooms <= 5.00"   +0.5741
    "dist_km > 15.40"       −0.1336
    "age <= 14.00"          +0.0952
    Read this correctly: "+0.57 for being in the 4–5 rooms bracket" — not "+0.57 per room". The weight belongs to the bin.
  • discretize_continuous=True # default
    discretizer='quartile' # | 'decile' | 'entropy'
    Discretisation makes the surrogate more faithful (it can capture thresholds a line can't) and the output more readable. 'entropy' picks bins using a decision tree — often the best of the three.
  • discretize_continuous=False
    Now weights are true per-unit coefficients. More familiar — but usually a worse fit, and it hides thresholds entirely.
  • # bins are computed from TRAINING data
    A test row outside the training range lands in an edge bin. The explanation is still rendered, confidently.
06LimeTextExplainerwhere LIME shines
Perturbation = randomly delete words. The surrogate learns which words, when removed, change the prediction. Intuitive, faithful, and it fits nearly perfectly (R² = 0.9999 measured).
  • te = LimeTextExplainer(class_names=['neg', 'pos'],
      random_state=0)

    exp = te.explain_instance(
      "great but awful", # a RAW STRING
      pipe.predict_proba, # vectorizer must be INSIDE the pipe
      num_features=6)
    No training_data argument — text needs none; the document is the neighbourhood.
  • exp.show_in_notebook(text=True)
    Highlights the words in the original document. This is LIME's best-loved output.
  • bow=True # default
    Bag-of-words: every occurrence of a word is removed together, and position is ignored. Set bow=False if your model is order-sensitive (a transformer), so each token is perturbed individually.
  • char_level=True · split_expression=r'\W+'
    For character-level models, and for custom tokenisation.
  • kernel_width=25 # text default
    Note this differs from tabular's default (√n_features × 0.75).
07LimeImageExplainersuperpixels
Perturbation = switch off superpixels. The image is segmented (quickshift by default), then random subsets of segments are greyed out.
  • exp = LimeImageExplainer().explain_instance(
      image, # (H, W, 3) numpy
      model.predict, # must return (n, n_classes)
      top_labels=3, hide_color=0, num_samples=1000)
    hide_color=0 blacks out a segment; None replaces it with the segment's mean colour (usually more sensible).
  • img, mask = exp.get_image_and_mask(
      exp.top_labels[0], positive_only=True,
      num_features=5, hide_rest=True)
    plt.imshow(mark_boundaries(img, mask))
    The standard two-liner. positive_only=False shows the regions arguing against the class too — often the more revealing view.
  • segmentation_fn=SegmentationAlgorithm('slic', n_segments=100)
    The segmentation IS the feature space. Bad segments → meaningless explanation. This is the knob that matters most for images.
  • # image LIME is slow
    num_samples forward passes through your CNN, per image. Budget accordingly.
08SubmodularPickfrom local to global
One explanation explains one row. SP-LIME picks a small set of instances that together cover the model's behaviour with as little redundancy as possible — the paper's answer to "so how do I trust the model?"
  • sp = SubmodularPick(explainer, X_train.values,
      clf.predict_proba, sample_size=50,
      num_exps_desired=5, num_features=8)

    [e.as_pyplot_figure() for e in sp.sp_explanations]
    Greedily maximises coverage of important features across the chosen rows. 5 well-chosen explanations beat 500 random ones.
  • sp.explanations # all of them
    sp.sp_explanations # the chosen few
    Underused, and the most defensible way to present LIME to a stakeholder.
  • # it runs LIME sample_size times
    Cost = sample_size × num_samples model calls. Start small.
09Trust & Tuningthe checks to actually run
Three lines separate a real explanation from a decorative one. Run all three, every time.
  • print(exp.score) # R² — is the line even fitting?
    < 0.3 → discard the explanation. The neighbourhood isn't linear and the coefficients are noise. Nothing else you do matters if this fails.
  • print(exp.local_pred, clf.predict_proba(row))
    Do the surrogate and the model agree on this row? If not, you're explaining a fiction. Measured gap: 0.146 average, 0.424 worst.
  • # re-run with a different seed
    If the top features change, raise num_samples until they don't.
  • kernel_width=None # → √n_features × 0.75
    The most arbitrary default in the library. It defines "nearby". Too wide → you're explaining the global model. Too narrow → almost no samples carry weight and the fit is noise. Results are sensitive to it; if R² is poor, this is the second knob to try.
  • feature_selection='auto' # | 'lasso_path' | 'forward_selection' | 'none'
    How the sparse subset of num_features is chosen before fitting.
  • model_regressor=Ridge(alpha=1)
    Swap the surrogate. Any sklearn regressor with coef_ works — a more regularised one can stabilise the weights.

Decision map

Data kind picks the explainer. Then everything routes through the same gate.

flowchart LR
  A["Explain one
prediction"] --> B{"Data kind?"} B -->|text| C["LimeTextExplainer
pass RAW strings +
pipeline.predict_proba"] B -->|tabular| D["LimeTabularExplainer
pass training data"] B -->|image| E["LimeImageExplainer
segmentation IS
the feature space"] D --> D1{"Classification
or regression?"} D1 -->|clf| D2["predict_proba
(n, n_classes)"] D1 -->|reg| D3["mode='regression'
plain predict"] C --> G D2 --> G D3 --> G E --> G G["THE GATE
print(exp.score)"] --> H{"R² good?"} H -->|"> 0.6 — trust it"| I["exp.as_list()
exp.show_in_notebook()"] H -->|"0.3 – 0.6 — shaky"| J["raise num_samples
tune kernel_width
try discretizer='entropy'"] H -->|"< 0.3 — DISCARD"| K["The neighbourhood
isn't linear.
Use SHAP instead."] J --> H I --> L{"Need the
whole model?"} L -->|yes| M["SubmodularPick
a few covering rows"] L -->|no| Z(["Ship it"]) M --> Z classDef txt fill:#ecf7f2,stroke:#059669,color:#065f46,stroke-width:2px; classDef tab fill:#fdf6ec,stroke:#d97706,color:#92400e,stroke-width:2px; classDef img fill:#eef0fd,stroke:#4f46e5,color:#3730a3,stroke-width:2px; classDef gate fill:#ecf6fa,stroke:#0891b2,color:#155e75,stroke-width:2px; classDef warn fill:#fdf0ef,stroke:#dc2626,color:#991b1b,stroke-width:2px; classDef ship fill:#1a1d24,stroke:#1a1d24,color:#fbbf24,stroke-width:2px; class B,D1,H,L gate; class C,I txt; class D,D2,D3,J tab; class E,M img; class G,K warn; class Z ship;

Traps & perspective

LIME's failure mode is the worst kind: it never errors, never warns, and always renders something beautiful. The quality signal exists — it's just not on by default.

10Silently Wrongit still looks great
  • # never checking exp.scorethe big one
    R² as low as 0.006 still yields a confident, ranked, colour-coded chart. If you take one thing from this sheet, take this.
  • # reading a bin weight as a per-unit coefficient
    "4.00 < rooms <= 5.00" → +0.57 means "+0.57 for being in that bracket", not "+0.57 per room".
  • num_samples=100 # to make it fast
    Weight spread across seeds jumped to 0.145. This is where LIME's instability reputation actually comes from — it's self-inflicted.
  • random_state=None # default
    Unreproducible explanations. Always set it. A stakeholder re-running your notebook and getting a different answer is not a good day.
  • # forgetting categorical_features=
    One-hot columns get perturbed to fractional values. The model gets nonsense; you get a chart.
  • # LIME is not additive
    Weights do not sum to the prediction. There's no local-accuracy guarantee — that's SHAP's property, not LIME's. Don't present them as contributions that add up.
11LIME vs SHAPthe honest comparison
LIMESHAP
Fast & model-agnostic. Any model, any data kind, one API. Exact for trees/linear (TreeSHAP). Slow for everything else.
No guarantees. Weights are coefficients of a line that might not fit. Axiomatic. Local accuracy, consistency, missingness. Weights sum to the prediction.
Intuitive output — rules and highlighted words. Non-specialists read it instantly. Log-odds, base values, 3-D arrays. Powerful, but needs explaining.
Ships a quality signal (exp.score) — if you read it. No comparable "did this work?" number. It's exact, so it doesn't need one.
Best for: text, images, quick sanity checks, explaining to humans. Best for: tabular models, audits, anything needing defensible numbers.
The verdict from the numbers on this page: on text, LIME fit at R² = 0.9999 — use it, it's excellent. On tabular, the median fit was 0.56 and the worst 0.006 — reach for SHAP, which is exact for exactly the tree models you'd be using there anyway.
12What LIME Cannot Tell Youthe limits
  • # it explains the SURROGATE, not the modelcore
    Every LIME weight is a coefficient of a little linear model that stands in for yours. When they agree (high R²), that's fine. When they don't, you are reading a description of a model that does not exist.
  • # not causal
    "The model used this" ≠ "changing this changes the outcome". Same caveat as SHAP.
  • # perturbations can be off-manifold
    Independent sampling produces impossible rows. The model answers anyway, and those answers shape your explanation.
  • # LIME can be gamed
    Published attacks (Slack et al., "Fooling LIME and SHAP", 2020) build models that behave one way on real data and another on LIME's perturbed samples — hiding blatant bias from the explainer. Explanations are evidence, not proof.
  • # a good explanation of a bad model is still bad
    LIME faithfully reports what the model learned — including its leakage and its bias. Validate the model first, explain it second.

Worth memorizing

PRINT exp.scorethe surrogate's R² · measured as low as 0.006 while still rendering a confident chart
< 0.3 → discard itthe neighbourhood isn't linear · the coefficients are noise
the 4 stepsperturb → predict → weight by proximity → fit a sparse linear model
universal shapeExplainer(train)explain_instance(row, predict_fn)as_list()
clf → predict_probaclf.predict raises NotImplementedError · regression → plain predict
LIME is NOT additiveweights don't sum to the prediction · that's SHAP's guarantee, not LIME's
weights are BINS"4 < rooms <= 5" → +0.57 means "for being in that bracket", not per room
num_samples is the knobspread 0.145 at 100 → 0.019 at 5000 · instability is self-inflicted
always set random_statedefault is None → unreproducible explanations
text: no training_datapass raw strings + a pipeline's predict_proba (vectorizer inside)
bow=True ignores orderset bow=False for transformers and order-sensitive models
declare categorical_featuresor one-hot columns get perturbed to 0.37
sample_around_instancedefaults to False — samples the global distribution, not your neighbourhood
images: segmentation IS the feature spacebad superpixels → meaningless explanation
SubmodularPicka few covering rows > hundreds of random ones · the defensible way to present LIME
text → LIME · tabular → SHAPmeasured: R² 0.9999 on text, median 0.56 on tabular