shap-svg 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,23 +26,107 @@ affiliated with the SHAP authors.
26
26
 
27
27
  | Component | SHAP counterpart | Shows |
28
28
  | --- | --- | --- |
29
- | `ShapBar` | `shap.plots.bar` | mean(\|SHAP value\|) per feature across samples |
30
- | `ShapBeeswarm` | `shap.plots.beeswarm` | one dot per sample per feature, coloured by feature value |
31
- | `ShapHeatmap` | `shap.plots.heatmap` | samples × features coloured by SHAP value, with the f(x) line above |
32
- | `ShapWaterfall` | `shap.plots.waterfall` | how one sample's prediction is built from E[f(X)] to f(x) |
29
+ | `Plots.bar` | `shap.plots.bar` | mean(\|SHAP value\|) per feature across samples |
30
+ | `Plots.beeswarm` | `shap.plots.beeswarm` | one dot per sample per feature, coloured by feature value |
31
+ | `Plots.heatmap` | `shap.plots.heatmap` | samples × features coloured by SHAP value, with the f(x) line above |
32
+ | `Plots.waterfall` | `shap.plots.waterfall` | how one sample's prediction is built from E[f(X)] to f(x) |
33
33
 
34
34
  Every chart is a pure component: all state that changes what is drawn arrives through props, so the
35
35
  host application owns its own controls. The only internal state is hover highlighting.
36
36
 
37
37
  ## Usage
38
38
 
39
- ```tsx
40
- import { ShapBeeswarm, ShapWaterfall } from "shap-svg/react";
39
+ `shap-svg` draws SHAP values; it does not compute them. The values come from
40
+ [`shap`](https://github.com/shap/shap) in Python, travel through your server as JSON, and are handed to
41
+ a component in the browser:
42
+
43
+ ```text
44
+ Python: shap computes the values → server returns them as JSON → browser fetches → <Plots.waterfall explanation={…} />
45
+ ```
46
+
47
+ ### 1. Compute SHAP values in Python and serialise them
48
+
49
+ ```python
50
+ import numpy as np
51
+ import shap
52
+
53
+ explainer = shap.TreeExplainer(model) # any shap explainer works
54
+
55
+
56
+ def to_payload(explanation: shap.Explanation, sample_ids=None) -> dict:
57
+ """Serialise a shap.Explanation into the JSON shap-svg reads."""
58
+ payload = {
59
+ "contract_version": 1,
60
+ "values": np.asarray(explanation.values, dtype=float).tolist(),
61
+ "base_values": np.asarray(explanation.base_values, dtype=float).tolist(),
62
+ "data": np.asarray(explanation.data, dtype=float).tolist(),
63
+ "feature_names": [str(name) for name in explanation.feature_names],
64
+ }
65
+ if sample_ids is not None:
66
+ payload["sample_ids"] = [str(sample_id) for sample_id in sample_ids]
67
+ return payload
68
+ ```
69
+
70
+ - **Pass the explanation as it comes.** A scikit-learn binary classifier gives `values` shaped
71
+ `(samples, features, 2)` and `base_values` shaped `(samples, 2)`; `shap-svg` reads class 1 by default
72
+ (`classIndex` on every component). There is no need to pick a class in Python.
73
+ - **Send only finite numbers.** Fill or drop missing feature values first. Flask's `jsonify` writes
74
+ `NaN` unquoted, which is not valid JSON, and `parseExplanation` rejects it; serialising with
75
+ `json.dumps(payload, allow_nan=False)` makes the mistake fail on the server instead.
76
+ - **`sample_ids` are optional** but give each Sample a stable key: the heatmap hands it back from
77
+ `onSampleClick`. Add `sample_labels` for the names a person should read.
78
+
79
+ ### 2. Return the payload from your server
41
80
 
42
- <ShapBeeswarm explanation={explanation} maxDisplay={15} groupByGenus rowSort="name" />
43
- <ShapWaterfall explanation={explanation} sampleIndex={0} decimals="percent" />
81
+ ```python
82
+ from flask import Flask, jsonify
83
+
84
+ app = Flask(__name__)
85
+
86
+
87
+ @app.post("/api/explain")
88
+ def explain():
89
+ X = rows_to_explain() # a pandas DataFrame, e.g. built from the request body
90
+ explanation = explainer(X)
91
+ return jsonify(to_payload(explanation, sample_ids=X.index))
44
92
  ```
45
93
 
94
+ Any framework works — the contract is only the JSON above. Serve it gzipped if you can: the payload is
95
+ mostly repeated digits and compresses well.
96
+
97
+ ### 3. Fetch it in the browser and hand it to a component
98
+
99
+ ```tsx
100
+ import { useEffect, useState } from "react";
101
+ import type { Explanation } from "shap-svg";
102
+ import { Plots } from "shap-svg/react";
103
+
104
+ export function ExplanationView() {
105
+ const [explanation, setExplanation] = useState<Explanation>();
106
+
107
+ useEffect(() => {
108
+ fetch("/api/explain", { method: "POST" })
109
+ .then((response) => response.json())
110
+ .then(setExplanation);
111
+ }, []);
112
+
113
+ if (!explanation) return <p>Loading…</p>;
114
+
115
+ return (
116
+ <>
117
+ <Plots.beeswarm explanation={explanation} maxDisplay={15} groupByGenus rowSort="name" />
118
+ <Plots.waterfall explanation={explanation} sampleIndex={0} decimals="percent" />
119
+ </>
120
+ );
121
+ }
122
+ ```
123
+
124
+ The charts are named the way `shap` names them in Python: `shap.plots.bar` becomes `<Plots.bar />`.
125
+ `Plots` brings all four charts into your bundle, even if a page draws one — about 30 KB minified.
126
+
127
+ From here every control — how many features, grouping, sorting, precision — is a prop. Changing one
128
+ redraws from the payload already in memory; nothing goes back to the server.
129
+
46
130
  The framework-free core — parsing, ordering, collapsing, layout and colour — has no React import and
47
131
  can drive any renderer:
48
132
 
@@ -94,11 +178,11 @@ Per chart:
94
178
 
95
179
  | Chart | Prop | Default | |
96
180
  | --- | --- | --- | --- |
97
- | `ShapBeeswarm`, `ShapHeatmap` | `rowSort` | `"importance"` | `"importance"`, `"name"` or `"featureValue"`; reorders the rows shown, never which rows are shown |
98
- | `ShapBeeswarm` | `seed`, `dotRadius` | `0`, `3` | jitter is seeded, so a chart is identical on every render |
99
- | `ShapHeatmap` | `onSampleClick` | — | called with the column's `sample_ids` entry |
100
- | `ShapWaterfall` | `sampleIndex` | `0` | which sample to explain |
101
- | `ShapWaterfall` | `decimals` | `2` | `2`, `3`, `4` or `"percent"`; display only |
181
+ | `Plots.beeswarm`, `Plots.heatmap` | `rowSort` | `"importance"` | `"importance"`, `"name"` or `"featureValue"`; reorders the rows shown, never which rows are shown |
182
+ | `Plots.beeswarm` | `seed`, `dotRadius` | `0`, `3` | jitter is seeded, so a chart is identical on every render |
183
+ | `Plots.heatmap` | `onSampleClick` | — | called with the column's `sample_ids` entry |
184
+ | `Plots.waterfall` | `sampleIndex` | `0` | which sample to explain |
185
+ | `Plots.waterfall` | `decimals` | `2` | `2`, `3`, `4` or `"percent"`; display only |
102
186
 
103
187
  ## Faithful to SHAP where it matters
104
188