Skip to content

data: fix Marginal.visualize crash on categorical features (#119)#666

Merged
paulbkoch merged 1 commit into
interpretml:mainfrom
jbbqqf:feat/119-fix-marginal-categorical-visualize
May 12, 2026
Merged

data: fix Marginal.visualize crash on categorical features (#119)#666
paulbkoch merged 1 commit into
interpretml:mainfrom
jbbqqf:feat/119-fix-marginal-categorical-visualize

Conversation

@jbbqqf

@jbbqqf jbbqqf commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Marginal(...).explain_data(...).visualize(key) crashed with
TypeError: unsupported operand type(s) for -: 'str' and 'str' whenever
feature_types[key] was "nominal" or "ordinal", making the marginal
data explainer unusable on every dataset that contained a categorical
feature.

Fixes #119visualize has a bug for categorical features

Context

MarginalExplanation.visualize builds a small two-panel plotly figure
(feature histogram + response histogram). It computed the histogram
bin width for both panels with a single line at the top of the function:

bin_size = density_dict["names"][1] - density_dict["names"][0]

For continuous features density_dict["names"] is the numeric output
of np.histogram(...) (bin edges), so the subtraction is fine. For
nominal/ordinal features the same field stores string category labels
returned by np.unique(...), and the subtraction blows up before any
branch on is_categorical ever executes — so neither path renders.

There is a second instance of the same pattern further down for the
response axis (resp_bin_size = density_dict["names"][...] — note that
it even pointed at the feature density rather than the response
density). Both are addressed in this PR.

The fix matches the long-standing report from issue #119, with one
small addition: classification targets (categorical response) now also
render, because the response side is now guarded the same way.

Changes

  • python/interpret-core/interpret/data/_response.py
    • move bin_size into the else branch so it is only computed when
      density_dict["names"] are numeric bin edges.
    • mirror the same guard on the response axis. When the response is
      categorical (e.g. classification target) build a plotly Histogram
      without ybins, otherwise compute resp_bin_size from
      resp_density_dict["names"] (was previously computed off the
      feature density — a latent bug the same line fixed).
    • add a tiny _is_numeric_bin_edges predicate so the categorical
      response check stays self-documenting; comment cites issue visualize has a bug for categorical features  #119
      so future readers don't have to re-derive the reasoning.
  • python/interpret-core/tests/data/test_marginal_visualize.py (new)
    • four pytest cases: nominal feature on continuous target, continuous
      feature (non-regression), nominal feature on classification target,
      and the key=None overall path.
    • the two nominal-feature cases fail on origin/main with the exact
      TypeError cited in visualize has a bug for categorical features  #119; all four pass on this branch.

Reproduce BEFORE/AFTER yourself (copy-paste)

# --- one-time setup ---
git clone https://github.com/interpretml/interpret.git /tmp/repro && cd /tmp/repro
python -m venv .venv && source .venv/bin/activate
pip install -e "python/interpret-core[testing]" plotly

cat > /tmp/repro_119.py <<'PY'
import numpy as np
import pandas as pd
from interpret.data import Marginal

df = pd.DataFrame({
    "cat":  ["a", "b", "a", "c", "b", "a", "c", "b"],
    "cont": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
})
y = np.array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5])
m = Marginal(feature_types=["nominal", "continuous"]).explain_data(df, y)
print("categorical viz:", type(m.visualize(0)).__name__)
print("continuous viz: ", type(m.visualize(1)).__name__)
PY

# --- BEFORE (origin/main) ---
git checkout origin/main
python /tmp/repro_119.py
# Expected: TypeError: unsupported operand type(s) for -: 'str' and 'str'

# --- AFTER (this PR) ---
git fetch https://github.com/jbbqqf/interpret.git feat/119-fix-marginal-categorical-visualize
git checkout FETCH_HEAD
python /tmp/repro_119.py
# Expected: both lines print "Figure"

# Optional: run the new regression suite, which fails on main and
# passes on this branch
git checkout origin/main
python -m pytest -vv python/interpret-core/tests/data/test_marginal_visualize.py
# Expected: 2 failed, 2 passed (the categorical cases hit the bug)
git checkout FETCH_HEAD
python -m pytest -vv python/interpret-core/tests/data/test_marginal_visualize.py
# Expected: 4 passed

What I ran locally

  • python -m pytest -vv tests/data/test_marginal_visualize.py4 passed
    on this branch.
  • The same suite on origin/main2 failed, 2 passed (the two
    failures are the exact TypeError cited in visualize has a bug for categorical features  #119; the passing ones
    cover the continuous and overall paths that were already correct).
  • ruff check interpret-core/interpret/data/_response.py → 6 errors
    reported, identical to origin/main (all pre-existing
    PLC0415/NPY002 flags in this file). My change introduces zero
    new lint findings.
  • ruff format --check on the touched files → already formatted.

The selenium / dashboard / full visual suites need extra optional
dependencies (requests, dash, etc.) that aren't part of the
testing extra; they were not run.

Edge cases tested

# Scenario Input Expected Verified by
1 Nominal feature, continuous target feature_types=["nominal","continuous"], key=0 plotly Figure (was TypeError) test_visualize_nominal_feature_continuous_target_succeeds
2 Continuous feature (regression guard) same dataset, key=1 plotly Figure (still works) test_visualize_continuous_feature_still_works
3 Nominal feature, classification target y = [0,1,0,1,0,1,0,1], key=0 plotly Figure (categorical response branch) test_visualize_nominal_feature_classification_target
4 Overall view (key=None) any plot_density figure (path unchanged) test_visualize_overall_unaffected

Risk / blast radius

The change is local to MarginalExplanation.visualize. It only adds
code paths that were previously unreachable (categorical features /
classification responses). Continuous-feature behaviour is byte-equal
to before — same plotly trace, same xbins dict. The new
_is_numeric_bin_edges helper is module-private. No public API
changes.

Release note

Fixed Marginal data explainer crashing with TypeError when visualising
nominal/ordinal features or classification targets (issue #119).

PR drafted with assistance from Claude Code. The change was reviewed
manually against interpretml/interpret's source and the cited issue.
The reproducer block above was used during development; it is the same
one a reviewer can paste verbatim.

…ml#119)

Marginal(...).explain_data(...).visualize(key) raised
TypeError: unsupported operand type(s) for -: 'str' and 'str' whenever
feature_types[key] was nominal/ordinal because the histogram path
unconditionally computed density_dict["names"][1] - density_dict["names"][0]
for the bin size, even though for categorical features the names list
holds string category labels rather than numeric bin edges.

Compute bin_size lazily only on the numeric branch, and apply the same
guard on the response side so that a categorical response (e.g.
classification target) also renders without crashing. A small helper
_is_numeric_bin_edges keeps the predicate explicit.

Add tests/data/test_marginal_visualize.py with four cases covering the
two regression branches plus the previously-working continuous and
overall paths to lock in the contract.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Signed-off-by: jbbqqf <jbaptiste.braun@gmail.com>
@codecov

codecov Bot commented May 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 67.18%. Comparing base (6c79a67) to head (e78b256).

Files with missing lines Patch % Lines
python/interpret-core/interpret/data/_response.py 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #666      +/-   ##
==========================================
+ Coverage   66.86%   67.18%   +0.32%     
==========================================
  Files          77       77              
  Lines       11724    11729       +5     
==========================================
+ Hits         7839     7880      +41     
+ Misses       3885     3849      -36     
Flag Coverage Δ
bdist_linux_311_python 66.91% <87.50%> (+0.32%) ⬆️
bdist_linux_312_python 66.93% <87.50%> (+0.33%) ⬆️
bdist_linux_313_python 66.93% <87.50%> (+0.33%) ⬆️
bdist_linux_314_python 66.82% <87.50%> (+0.35%) ⬆️
bdist_linuxarm_311_python 66.95% <87.50%> (+0.33%) ⬆️
bdist_linuxarm_312_python 66.95% <87.50%> (+0.32%) ⬆️
bdist_linuxarm_313_python 66.93% <87.50%> (+0.33%) ⬆️
bdist_linuxarm_314_python 66.86% <87.50%> (+0.33%) ⬆️
bdist_mac_311_python 67.08% <87.50%> (+0.32%) ⬆️
bdist_mac_312_python 67.09% <87.50%> (+0.33%) ⬆️
bdist_mac_313_python 67.09% <87.50%> (+0.33%) ⬆️
bdist_mac_314_python 66.98% <87.50%> (+0.32%) ⬆️
bdist_win_311_python 67.09% <87.50%> (+0.32%) ⬆️
bdist_win_312_python 67.09% <87.50%> (+0.32%) ⬆️
bdist_win_313_python 67.09% <87.50%> (+0.32%) ⬆️
bdist_win_314_python 66.98% <87.50%> (+0.30%) ⬆️
sdist_linux_311_python 66.86% <87.50%> (+0.32%) ⬆️
sdist_linux_312_python 66.88% <87.50%> (+0.33%) ⬆️
sdist_linux_313_python 66.86% <87.50%> (+0.32%) ⬆️
sdist_linux_314_python 66.79% <87.50%> (+0.33%) ⬆️
sdist_linuxarm_311_python 66.87% <87.50%> (+0.32%) ⬆️
sdist_linuxarm_312_python 66.87% <87.50%> (+0.32%) ⬆️
sdist_linuxarm_313_python 66.87% <87.50%> (+0.32%) ⬆️
sdist_linuxarm_314_python 66.78% <87.50%> (+0.32%) ⬆️
sdist_mac_311_python 66.99% <87.50%> (+0.32%) ⬆️
sdist_mac_312_python 66.99% <87.50%> (+0.32%) ⬆️
sdist_mac_313_python 67.01% <87.50%> (+0.33%) ⬆️
sdist_mac_314_python 66.90% <87.50%> (+0.32%) ⬆️
sdist_win_311_python 67.11% <87.50%> (+0.33%) ⬆️
sdist_win_312_python 67.11% <87.50%> (+0.33%) ⬆️
sdist_win_313_python 67.11% <87.50%> (+0.33%) ⬆️
sdist_win_314_python 67.02% <87.50%> (+0.33%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@paulbkoch
paulbkoch merged commit 7a43690 into interpretml:main May 12, 2026
67 checks passed
@paulbkoch

Copy link
Copy Markdown
Collaborator

Very nice PR. Thanks @jbbqqf!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

visualize has a bug for categorical features

2 participants