Quick Reference · model debugging & explainability

ELI5 cheat sheet

The entire library is two questions and two functions. What did the model learn overall?explain_weights. Why did it predict THIS?explain_prediction. Everything else is: which estimator you point them at, and which format you render into. Its unmatched feature is highlighting the actual words that drove a text prediction.

foundations & formatters global — explain_weights local — explain_prediction black-box — permutation & LIME supported libraries trap most common

Grounded against eli5.readthedocs.io (official docs) & github.com/eli5-org/eli5 — and every claim on this sheet was executed against eli5 0.16.0 / scikit-learn 1.8.0. All numbers in the diagrams are real output, not illustrations.

Two questions · two functions · one Explanation object · five formats
YOUR MODEL THE TWO QUESTIONS ONE OBJECT FIVE FORMATS WHITE BOX linear → coefficients tree → decision path read directly · exact BLACK BOX SVM · MLP · any pipeline wrap it first: PermutationImportance TextExplainer (LIME) TEXT / IMAGE pass vec= → word highlighting Keras → Grad-CAM heatmap explain_weights(clf) GLOBAL — "what did the model learn?" coefficients · feature_importances_ · permutation scores explain_prediction(clf, doc) LOCAL — "why THIS row?" <BIAS> + per-feature contributions = the prediction, exactly for text: highlights the words that moved the score Explanation .targets .feature_importances data, not pixels eli5.formatters format_as_html() format_as_text() format_as_dataframe() format_as_dict() format_as_image() show_weights / show_prediction = explain + format_as_html show_* only exists if IPython is installed. Plain .py script → AttributeError. Use explain_* + format_as_*. Explanation is separated from formatting. That's the whole design — one computation, five renderings. …and it's why you can pipe explanations into a DataFrame or a dashboard.

Foundations

Learn the explain → format split once and the rest of the API is mechanical. Card 02 is the trap nobody warns you about — the function every tutorial uses doesn't exist outside a notebook.

01Install & the Whole APIit really is this small
  • pip install eli5
    import eli5
    Maintained at eli5-org/eli5 (a live fork of the original TeamHG-Memex repo, which stalled at 0.13). Current: 0.16.0 — and it works fine on scikit-learn 1.8. The "eli5 is dead" advice you'll find online is out of date.
  • eli5.explain_weights(clf, …) # GLOBAL
    eli5.explain_prediction(clf, doc, …) # LOCAL
    The two functions. Both return an Explanation object. Both dispatch on estimator type — so the same call works for a linear model, a random forest, XGBoost or a pipeline.
  • eli5.show_weights(clf) · eli5.show_prediction(clf, doc)
    explain + format_as_html in one step. Notebook only — see card 02.
  • eli5.format_as_html · format_as_text · format_as_dict
    eli5.format_as_dataframe · format_as_image
    Five renderings of the same Explanation. HTML for notebooks, text for logs, dict for JSON APIs, DataFrame for analysis, image for Grad-CAM.
  • eli5.explain_weights_df(clf, feature_names=cols)
    eli5.explain_prediction_df(clf, row)
    The shortcut that skips HTML entirely. Returns a plain DataFrame — explain_weights_df gives [feature, weight, std]; explain_prediction_df gives [target, feature, weight, value]. Use these in scripts.
  • eli5.sklearn · eli5.xgboost · eli5.lightgbm · eli5.catboost
    eli5.keras · eli5.lightning · eli5.sklearn_crfsuite · eli5.lime · eli5.llm
    The dispatch targets. You rarely import these directly — the top-level functions find them.
02show_* Needs IPythonverified, and it surprises people
Every tutorial opens with eli5.show_weights(...). In a plain Python script that raises AttributeError — the function is only attached when IPython is importable.
  • import eli5
    eli5.show_weights(clf) # in a .py file
    # AttributeError: module 'eli5' has no attribute 'show_weights'
    script
    Not a bug — show_* returns an IPython.display.HTML object, so it's gated behind the IPython import.
  • expl = eli5.explain_weights(clf, feature_names=cols)
    html = eli5.format_as_html(expl)
    open('expl.html', 'w').write(html)
    The portable pattern. Works everywhere — scripts, web apps, CI. This is what show_weights does internally anyway.
  • print(eli5.format_as_text(expl))
    For terminals and logs. No IPython, no HTML, no browser.
03The Shared Argumentssame knobs everywhere
Both explain functions take these. feature_names and vec are the difference between a readable answer and x12, x87, x103.
ArgWhat it does
feature_namesNames for tabular columns. Without it you get x0, x1, … — pass list(X.columns).
vecThe vectorizer. Unlocks word/char highlighting for text — the killer feature. Card 08.
target_namesHuman class names instead of y=0, y=1.
targetsExplain only these classes. top_targets=2 → only the 2 most probable.
toptop=10 → 10 features. top=(5, 5)5 positive and 5 negative.
feature_re
feature_filter
Regex / callable to keep only matching features. Invaluable on wide one-hot matrices.
show_feature_valuesOn explain_prediction: also print the actual input value next to each contribution.

Global & local explanations

Global tells you what the model learned in aggregate. Local tells you why one row got its answer — and for trees and linear models, the decomposition is exact, not approximate.

04explain_weightswhat did it learn?
  • eli5.show_weights(logreg, feature_names=cols, top=15)
    Linear models → the coefficients, sorted, signed, colour-coded green/red. Plus a <BIAS> row for the intercept.
  • eli5.show_weights(rf, feature_names=cols)
    Trees / forestsfeature_importances_, with a ± standard deviation across trees. A large std means the trees disagree — treat that ranking with suspicion.
  • eli5.explain_weights_df(rf, feature_names=cols)
    # → columns: feature, weight, std
    Same numbers, as a DataFrame. Sort it, plot it, ship it.
  • # tree feature_importances_ = mean impurity decreasebiased
    It is systematically biased toward high-cardinality and continuous features — a random ID column can outrank a real signal. It's also computed on training data. Prefer PermutationImportance on held-out data (card 09) whenever the ranking actually matters.
  • eli5.show_weights(clf, vec=vectorizer, top=(10, 10))
    For text: the top 10 words pushing toward each class and the top 10 pushing away. The fastest sanity check on any text classifier.
05explain_prediction & <BIAS>why THIS row?
The key insight: <BIAS> is not noise. It's the model's starting point — the training-set mean (trees) or the intercept (linear). Contributions are the deltas from there, and they sum to the prediction exactly.
  • eli5.show_prediction(model, X_test.iloc[0],
      feature_names=cols, show_feature_values=True)
    Per-feature contributions for one row, plus the input values that produced them.
  • eli5.explain_prediction_df(tree, row)
    # target feature weight value
    Trees use the decision-path decomposition (Saabas): walk the path, attribute each node's change in the running mean to the feature that split there.
  • # verified on a real DecisionTreeRegressor:
    <BIAS> 223.80 # = mean(y_train), exactly
    rooms   +82.73
    dist_km +17.49
    # sum = 324.02 == model.predict(row)
    exact
    Not an approximation. Unlike LIME or SHAP sampling, this is an exact algebraic decomposition — see the diagram below.
  • top_targets=2 · targets=['spam']
    On multiclass, explain only the most probable classes instead of all 20.
  • # features not on the decision path get weight 0
    A tree only "uses" the features it actually split on for that row. A zero contribution means "not consulted for this row"not "irrelevant to the model".
06Text Highlightingthe killer feature
Pass vec= and eli5 maps coefficients back onto the original string, colouring each word by its contribution. Nothing else does this as cleanly.
  • vec = TfidfVectorizer().fit(docs)
    clf = LogisticRegression().fit(vec.transform(docs), y)

    eli5.show_prediction(clf, "great but awful", vec=vec,
      target_names=['neg', 'pos'])
    Verified real output: great +0.208 (green), awful −0.160 (red), score 0.049, proba 0.512. The near-tie is visible — you can see the model was torn.
  • eli5.show_prediction(pipe, doc) # Pipeline directly
    Pass the whole Pipeline — eli5 finds the vectorizer itself. FeatureUnion works too.
  • expl.targets[0].weighted_spans
    The character offsets behind the highlighting, if you want to render it yourself.
  • from eli5.sklearn import InvertableHashingVectorizer
    Undoes the hashing trick. HashingVectorizer destroys feature names by design — this recovers likely candidates so your explanation isn't a list of integers.
  • eli5.show_prediction(clf, doc) # no vec=
    You get x8231, x40122. Always pass vec= (or the whole pipeline) for text.

What the numbers actually mean

Every figure below is real output from eli5 0.16.0, not an illustration. The <BIAS> panel is the one worth internalising — it's why local explanations here are exact rather than sampled.

<BIAS> + contributions = the prediction, exactly

A depth-3 DecisionTreeRegressor on house prices. <BIAS> is literally mean(y_train) — the answer the tree would give with no information. Each split then nudges it. age contributes 0 because it wasn't on this row's decision path.

row: rooms = 6 · age = 5 · dist_km = 2.0 223.80 <BIAS> = mean(y_train) start here +82.73 rooms = 6 big house → push up +17.49 dist_km = 2.0 close in → push up age = 5 0.00 not on the path 223.80 + 82.73 + 17.49 = 324.02 == model.predict(row) EXACT — not sampled unlike LIME / SHAP estimates

✗ permutation importance on TRAIN

Same fitted forest, importances computed on the training set. The two useless sepal features pick up fake importance — the model memorised them.

perm.fit(X_train, y_train) petal lengthpetal width sepal lengthsepal width 0.298 0.116 0.020 0.009 non-zero = memorisation, not signal

✓ permutation importance on TEST

Identical model, held-out data. The sepal features collapse to exactly 0.000 — the truth. This is why you permute on data the model has never seen.

perm.fit(X_test, y_test) petal lengthpetal width sepal lengthsepal width 0.216 0.153 0.000 0.000 zero = genuinely irrelevant. Real numbers, verified.

Black-box models & supported libraries

When the model has no coefficients and no decision path, you can't read it — you have to probe it. Two probes: permute the inputs (global) or fit a local surrogate (LIME, local).

07PermutationImportanceworks on anything
Shuffle one column, see how much the score drops. Big drop → the model relied on it. Model-agnostic, and it measures what the model actually uses — not what impurity suggests.
  • from eli5.sklearn import PermutationImportance

    perm = PermutationImportance(model, scoring='accuracy',
      n_iter=10, random_state=0).fit(X_test, y_test)

    eli5.show_weights(perm, feature_names=cols)
    Note what's fitted where: model is already trained; perm.fit() is called on the test set.
  • cv='prefit' # ← the DEFAULTread this
    The default assumes you pass an already-fitted model and does not refit. So .fit(X, y) here means "score against this data", not "train on it". Pass cv=5 instead and it clones and refits per fold — a different, slower, more robust estimate.
  • perm.fit(X_train, y_train) # ✗inflates
    Verified above: irrelevant features scored 0.020 / 0.009 on train and 0.000 on test. Permuting on training data measures memorisation. Always use held-out data.
  • # correlated features hide each othercare
    If A and B are duplicates, shuffling A does nothing (the model reads B instead) and vice-versa. Both look unimportant. This is a limitation of the method, not of eli5 — cluster correlated features and permute them as a group.
  • from sklearn.inspection import permutation_importancesklearn has this
    For plain tabular models, sklearn's built-in is the maintained choice. eli5's edge is the rendering — and that it drops straight into show_weights alongside everything else.
08TextExplainer (LIME)explain any text pipeline
Your text model is a black box (SVM, MLP, a whole pipeline). LIME generates thousands of perturbed copies of the document, sees how predictions change, and fits a local white-box model to that neighbourhood.
  • from eli5.lime import TextExplainer

    te = TextExplainer(random_state=42)
    te.fit(doc, pipe.predict_proba)
    te.show_prediction(target_names=names)
    You hand it one document and a predict_proba callable. That's the whole contract — it never touches your model's internals.
  • te.metrics_ # {'score': …, 'mean_KL_divergence': …}check this
    Always inspect it before believing the output. score is how well the local surrogate fits (want high, near 1.0); mean_KL_divergence is how far its probabilities drift (want low). A bad fit means the explanation is fiction — LIME approximated a neighbourhood it couldn't model.
  • te.show_weights()
    The local surrogate's coefficients — i.e. what the black box looks like near this one document, not globally.
  • # LIME is stochastic
    Different random_state → different explanation. Set the seed, and re-run to check stability. If the story changes between seeds, don't trust either.
09Beyond scikit-learnsame two functions
The dispatch is automatic — explain_weights and explain_prediction just work on these too.
  • eli5.show_weights(xgb_clf) # XGBoost
    eli5.show_prediction(xgb_clf, row)
    XGBClassifier, XGBRegressor and raw xgboost.Booster. Same for LightGBM and CatBoost.
  • eli5.show_prediction(keras_model, img,
      image=pil_img) # Grad-CAM heatmap
    Keras image classifiers → a Grad-CAM overlay showing which pixels drove the class. Render it with format_as_image.
  • eli5.show_weights(crf) # sklearn-crfsuite
    Transition and state weights for CRF sequence models. Also supports lightning.
  • from eli5.llm import explain_prediction # needs `pip install openai`new
    The newest addition: explain an LLM classification via its token log-probabilities — highlighting how confident the model was in the label it emitted. Requires the openai client; it is not installed with eli5.
  • eli5.transform_feature_names(transformer, in_names)
    Propagates feature names through a pipeline's transformers — so names survive StandardScaler, MinMaxScaler and friends.

Decision map

Two forks decide everything: global or local, and can I read the model or must I probe it.

flowchart LR
  A["I need to explain
a model"] --> B{"Global or
local?"} B -->|"GLOBAL —
what did it learn?"| C{"Model type?"} C -->|"linear"| C1["explain_weights
→ coefficients"] C -->|"tree / forest / GBM"| C2["explain_weights
→ importances ± std"] C2 --> C3["…but impurity is BIASED.
Prefer PermutationImportance
on HELD-OUT data"] C -->|"black box"| C4["PermutationImportance
fit on X_test!"] B -->|"LOCAL —
why this row?"| D{"Data kind?"} D -->|"tabular,
white box"| D1["explain_prediction
<BIAS> + contribs
= prediction EXACTLY"] D -->|"text,
white box"| D2["explain_prediction
vec=vectorizer
→ word highlighting"] D -->|"text,
black box"| D3["TextExplainer (LIME)
CHECK te.metrics_"] D -->|"image (Keras)"| D4["explain_prediction
→ Grad-CAM"] C1 --> F["Explanation object"] C3 --> F C4 --> F D1 --> F D2 --> F D3 --> F D4 --> F F --> G{"Where does it
need to go?"} G -->|notebook| G1["show_weights /
show_prediction"] G -->|"script / API"| G2["format_as_html
format_as_text
format_as_dataframe"] G1 --> Z(["Ship it"]) G2 --> Z classDef glob fill:#ecf7f2,stroke:#059669,color:#065f46,stroke-width:2px; classDef loc fill:#fdf6ec,stroke:#d97706,color:#92400e,stroke-width:2px; classDef bb 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,C,D,G gate; class C1,C2 glob; class D1,D2,D4 loc; class C4,D3,F bb; class C3 warn; class G1,G2 glob; class Z ship;

Traps

The first three produce confidently wrong explanations — the most dangerous failure mode in explainability, because the output still looks authoritative.

10Confidently Wrongthe output still looks fine
  • PermutationImportance(m).fit(X_train, y_train)inflates
    Measures memorisation. Irrelevant features get non-zero scores. Fit on held-out data.
  • trusting tree feature_importances_
    Mean impurity decrease is biased toward high-cardinality / continuous features and computed on train. A random ID column can top the chart.
  • TextExplainer without checking te.metrics_
    If the local surrogate fits badly, the explanation is fiction — and it renders just as prettily as a good one. Check score and mean_KL_divergence.
  • permutation importance on correlated features
    Duplicated signal → both features look unimportant, because either can cover for the other.
  • reading a 0 contribution as "irrelevant"
    In a tree it means "not on this row's decision path". The feature may matter enormously for other rows.
11Frictionwhat the traceback means
  • AttributeError: module 'eli5' has no attribute 'show_weights'
    You're in a .py script without IPython. Use explain_weights + format_as_html. Card 02.
  • # explanation shows x0, x1, x2…
    You forgot feature_names=list(X.columns) — or, for text, vec=.
  • ModuleNotFoundError: No module named 'openai'
    eli5.llm needs the OpenAI client installed separately.
  • # HashingVectorizer → unreadable feature names
    Hashing is one-way by design. Wrap it in InvertableHashingVectorizer to recover candidates.
  • # "eli5 is unmaintained / broken on new sklearn"
    Out of date. The original repo stalled at 0.13, but eli5-org picked it up — 0.16.0 runs on scikit-learn 1.8. Verified.
12eli5 vs SHAPwhen to use which
They answer the same question with different guarantees. Neither is strictly better — but people reach for SHAP reflexively when eli5 is often the right call.
Reach for eli5Reach for SHAP
Text models — the word-level highlighting is unmatched. You need additive, axiomatic attributions (Shapley values) for audit or compliance.
Fast debugging — one line, no sampling, exact for linear/tree. You want beeswarm / dependence / interaction plots across the whole dataset.
Linear models — you just want the coefficients rendered nicely. Correlated features — SHAP handles them more principledly.
You want the explanation as a DataFrame or JSON for a dashboard. Deep nets — DeepExplainer / GradientExplainer.
The honest summary: eli5 is the debugger — fast, exact where it can be, and unbeatable on text. SHAP is the audit tool — slower, theoretically grounded, better plots. Use eli5 while building; reach for SHAP when someone needs to sign off.

Worth memorizing

two functions, that's itexplain_weights = global · explain_prediction = local
explain ≠ formatexplain_* → Explanation object → format_as_html/text/dict/dataframe/image
show_* needs IPythonplain script → AttributeError. Use explain_* + format_as_*
<BIAS> = the starting pointtraining mean (trees) / intercept (linear) — not noise
BIAS + contribs == predict()exactly — an algebraic decomposition, not a sampled estimate
pass vec= for textunlocks word highlighting · without it you get x8231
Pipelines work directlyhand explain_prediction the whole pipeline — it finds the vectorizer
cv='prefit' is the DEFAULTPermutationImportance won't refit — pass an already-fitted model
perm.fit(X_test, y_test)never on train — verified: fake importances on train, 0.000 on test
impurity importance is biasedfavours high-cardinality features · prefer permutation on held-out data
check te.metrics_bad LIME surrogate fit → the explanation is fiction, rendered beautifully
correlated features hidepermutation makes duplicated signal look unimportant on both features
weight 0 ≠ irrelevantin a tree it means "not on this row's decision path"
explain_weights_dfskip HTML entirely — straight to a pandas DataFrame
eli5 is NOT deadeli5-org/eli5 0.16.0 runs on sklearn 1.8 · old TeamHG-Memex stalled at 0.13
eli5 debugs, SHAP auditseli5 for text + fast exact checks · SHAP for axiomatic attributions