Skip to main content

narrashap

Turn SHAP explanations into plain-language, fidelity-checked narratives in one line of code.

PyPI Python License: MIT

If you have ever built a machine learning model and been asked "okay, but why did it say that?", this library is for that moment. narrashap takes the output of SHAP and turns it into a few sentences that a non-technical reader can follow.

  • Works offline. The built-in template mode needs no API key and costs nothing.
  • Optional LLM mode. Plug in Groq (free tier available) or Anthropic for more natural phrasing.
  • Fidelity scoring. In LLM mode, the generated text is scored against the underlying SHAP values.
  • Domain-aware wording. Neutral by default, with healthcare and fraud wording built in.
  • Safety guardrails. In LLM mode, causal wording such as "causes", "proven" or "guaranteed" is blocked, with one automatic rewrite attempt.

Table of contents


What problem does this solve?

Machine learning models, such as one that predicts a health risk or flags a fraudulent transaction, do not explain themselves. SHAP can tell you which factors influenced a prediction and by how much, but its raw output looks like this:

Age: +0.40
Smoker: +0.90

That is useful to a data scientist. It means little to a patient, a manager, or anyone without a machine learning background.

narrashap turns it into this (real output from the Quickstart below):

"Based on the information provided, the estimate is 72.0%. This falls in the high risk range. Here is what influenced that result the most. The biggest factor was smoker, which raised the estimate. Another important factor was age, which raised the estimate. This is higher than about 67 out of 100 cases in our data. This is only a computer estimate based on patterns in data. It does not mean any one factor by itself causes the outcome. Please treat it as one input to a decision, not as a final answer."


Install

You need Python 3.10 or newer. Check with python --version.

pip install narrashap

For LLM-generated narratives, install the extra for your provider:

pip install "narrashap[groq]"        # Groq
pip install "narrashap[anthropic]"   # Anthropic
pip install "narrashap[all]"         # both

Using a virtual environment is recommended:

python -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install narrashap

Check the install:

python -c "from narrashap import narrate; print('It works!')"

Quickstart

This example uses tiny fake data, so you can see the library work before connecting it to a real model. Save it as quick_test.py (or paste it into a notebook cell):

import pandas as pd
from narrashap import narrate

# Pretend this is the data your model was trained on
training_data = pd.DataFrame({
    "Age": [25, 30, 45, 60, 35, 50],
    "Smoker": [0, 0, 1, 1, 0, 1],
})

# SHAP values for one person: Age contributed +0.4, Smoker contributed +0.9
shap_values = [0.4, 0.9]

# The actual feature values for that person
instance = pd.Series({"Age": 45, "Smoker": 1})

result = narrate(
    shap_values=shap_values,
    instance=instance,
    training_data=training_data,
    feature_names=["Age", "Smoker"],
    base_value=0.1,          # the model's starting point before considering this person
    risk_percentage=72.0,    # optional: the real percentage you would show a user
    risk_level="HIGH RISK",  # optional: a label to go with it
)

print(result)

Run it:

python quick_test.py

You get the paragraph shown at the top of this page. This is the template mode: fixed sentence templates, no AI service, no API key, no cost. The wording is domain-neutral unless you choose a domain, as described next.


Domains and audiences

By default the wording is neutral ("the estimate", "this case"). Two optional arguments change it:

narrate(..., domain="healthcare")   # "generic" (default), "healthcare" or "fraud"
narrate(..., audience="clinician")  # "general", "patient", "clinician" or "executive"

domain applies to both template mode and LLM mode. audience sets the reading level and decides which closing note is used: "general" and "patient" get a plain-language note, while "clinician" and "executive" get a shorter, more technical one. If you leave audience out, healthcare uses "patient" and every other domain uses "general".

In template mode you can also fine-tune two words, used in the closing note and the percentile sentence:

narrate(
    ...,
    domain="healthcare",
    audience="patient",
    condition="fibroids",   # "...does not mean any one factor by itself causes fibroids"
    population="women",     # "...higher than about 67 out of 100 women in our data"
)

Without them, healthcare falls back to "the condition" and "people". An unknown domain or audience raises a ValueError straight away.


Optional: richer explanations with an LLM

Template sentences follow a fairly repetitive pattern. If you want explanations that read more naturally, you can pass an LLM client. narrashap supports Groq and Anthropic. The steps below use Groq, which has a free tier and does not need a credit card to start.

1. Get a Groq API key

  1. Go to console.groq.com and sign up.
  2. Open the API Keys section and create a key.
  3. Copy it right away (it looks like gsk_abc123...); it is only shown once.

2. Install the extra

pip install "narrashap[groq]"

3. Make the key available to your code

Never type your key into a file that might be shared or pushed to GitHub. Set it as an environment variable instead.

Mac/Linux:

export GROQ_API_KEY="gsk_your-actual-key-here"

Windows (PowerShell):

$env:GROQ_API_KEY = "gsk_your-actual-key-here"

This lasts only for the current terminal window. To make it permanent on Windows, run setx GROQ_API_KEY "gsk_your-actual-key-here" and then open a brand new terminal.

Check that it is set:

echo $GROQ_API_KEY          # Mac/Linux
echo $env:GROQ_API_KEY      # Windows PowerShell

4. Pass the client to narrate()

from narrashap import narrate
from narrashap.core.llm_client import GroqClient

result = narrate(
    shap_values=shap_values,
    instance=instance,
    training_data=training_data,
    feature_names=["Age", "Smoker"],
    base_value=0.1,
    risk_percentage=72.0,
    risk_level="HIGH RISK",
    llm_client=GroqClient(),   # switches on the LLM-generated version
)

print(result)

To use Anthropic instead, install narrashap[anthropic], set ANTHROPIC_API_KEY, and pass AnthropicClient() from the same module.

If you see a "model not found" error, the default model has probably been retired. See Troubleshooting.


Using it with a real model

narrashap works on top of a trained model; it does not build one for you. The general shape is:

import shap
import pandas as pd
from narrashap import narrate

# 1. Your trained model, the data it learned from, and the one row to explain
model = ...          # your trained model
X_train = ...        # the training data (a pandas DataFrame)
X_instance = ...     # a one-row DataFrame: the person or case to explain

# 2. Compute SHAP values for that one instance
explainer = shap.Explainer(model, X_train)
shap_values = explainer(X_instance)[0]

# 3. Get the narrative
result = narrate(
    shap_values=shap_values,
    instance=X_instance.iloc[0],
    training_data=X_train,
    risk_percentage=65.6,        # your model's predicted probability, as a percentage
    risk_level="MODERATE RISK",
)

print(result)

A complete working example, including a scikit-learn Pipeline with a preprocessing step and a logistic regression classifier, lives in the uf-risk-model repository, in fibroids_hospital_app/app.py.


Jupyter notebook cell

This cell assumes your notebook already has a fitted scikit-learn Pipeline called model (with steps named preprocessor and classifier, the classifier being a logistic regression) and the training DataFrame X_train. It does not train or fit anything.

import pandas as pd
from narrashap import narrate

# Choose which row to explain
patient_idx = 0
X_instance = X_train.iloc[[patient_idx]]

# Reuse the already-fitted pipeline steps
preprocessor = model.named_steps["preprocessor"]
classifier = model.named_steps["classifier"]

X_trans = preprocessor.transform(X_instance)
feature_names = list(preprocessor.get_feature_names_out())

# Clean names like "num__Age" into "Age" so the narrative reads naturally
def clean_name(name):
    return name.replace("num__", "").replace("cat__", "").replace("_", " ").title()

clean_feature_names = [clean_name(fn) for fn in feature_names]

# SHAP values for a logistic regression: coefficient * feature value
# (replace this line if your model is not logistic regression)
shap_vals = classifier.coef_[0] * X_trans[0]

# Training data transformed the same way, used for percentile context
X_train_trans = preprocessor.transform(X_train)
training_df = pd.DataFrame(X_train_trans, columns=clean_feature_names)

# The model's real predicted probability, as a percentage
proba = classifier.predict_proba(X_trans)[0][1]
risk_pct = round(proba * 100, 1)
risk_level = (
    "HIGH RISK" if risk_pct >= 70
    else "MODERATE RISK" if risk_pct >= 40
    else "LOW RISK"
)

narrated_shap = narrate(
    shap_values=shap_vals,
    instance=pd.Series(X_trans[0], index=clean_feature_names),
    training_data=training_df,
    feature_names=clean_feature_names,
    base_value=classifier.intercept_[0],
    risk_percentage=risk_pct,
    risk_level=risk_level,
    domain="healthcare",   # this cell is a health-risk example
    audience="patient",
)

print(narrated_shap)

For the LLM version, add llm_client=GroqClient() to the narrate() call, with from narrashap.core.llm_client import GroqClient and GROQ_API_KEY set.


What you get back

By default, narrate() returns a plain string: the explanation text, ready to print or display.

For more detail, such as a score for how well the text matches the real numbers, use return_details=True:

result = narrate(
    shap_values=shap_values,
    instance=instance,
    training_data=training_data,
    llm_client=GroqClient(),
    return_details=True,
)

print(result.text)             # the explanation
print(result.fidelity_score)   # 0 to 1: how well the text matches the SHAP values

fidelity_score is only calculated when an LLM backend is used. In template mode it is None.


Safety guardrails

narrashap is deliberately careful not to imply that a factor causes an outcome. It describes factors as "associated with" or "among the strongest factors in" a prediction. SHAP shows what influenced a model's prediction; it cannot prove that anything causes an outcome in the real world, and that distinction matters a great deal in areas like healthcare.

Template mode uses fixed sentences that were written with this in mind, so nothing needs checking at run time.

LLM mode checks every generated explanation for risky phrasing (words like "causes", "proven", "guaranteed", plus extra phrases for each domain). If a banned phrase appears, the library asks the model to rewrite the text once. If the retry still contains banned wording, it raises a RuntimeError instead of showing something misleading. Treat that error as the system doing its job.

The check ignores a banned word when a negation such as "not" or "never" appears earlier in the same sentence, so safe disclaimers like "this does not mean smoking causes the condition" pass. The trade-off is that an unrelated negation earlier in a long sentence can occasionally let a real violation through, so keep reading the output in sensitive settings. LLM prompts also ask the model to avoid statistical jargon, but that part is a request, not a hard check.


Troubleshooting

ModuleNotFoundError: No module named 'narrashap' You probably installed it into a different Python environment than the one running your code. Activate your virtual or conda environment first, then install, and activate it again whenever you return to the project.

"narrashap is not installed in this environment" inside a Jupyter notebook Notebooks sometimes use a different Python than your terminal. Check which one with:

import sys
print(sys.executable)

Then install with that exact Python, for example in a notebook cell: !pip install narrashap.

error: externally-managed-environment when installing This appears on some Linux and Homebrew Python setups. Use a virtual environment (see Install) rather than forcing the install.

ValueError: Unknown domain or Unknown audience Use one of the values listed in the message. Domains are "generic", "healthcare" and "fraud"; audiences are "general", "patient", "clinician" and "executive".

"Groq API key required" even though I set it Environment variables reset when you open a new terminal window (unless you used setx on Windows and reopened the terminal). Set the key again in the same window you run your code from.

"The model ... does not exist or you do not have access to it" (from Groq) The default model name is out of date. Open narrashap/core/llm_client.py, find the GroqClient class, and update the default model= value to a current name from console.groq.com/docs/models.

The LLM explanation sounds too technical or mentions raw numbers Pass risk_percentage and risk_level to narrate(). Without them, the explanation falls back to the model's raw internal numbers, which are not meant for a general audience.

"Generated narrative still contains banned phrases after retry" The LLM used risky causal language twice in a row and the safety check correctly blocked it. This is rare and usually goes away if you try again, since LLM responses vary. If it happens constantly, please open an issue.


Development

To work on narrashap itself:

git clone https://github.com/daviewisdm/narrashap.git
cd narrashap
pip install -e ".[dev]"
python -m pytest -q

The -e flag installs in editable mode, so code changes take effect without reinstalling. If any test fails, check your Python version and dependencies before assuming the library is at fault.


Project structure

narrashap/
├── narrashap/
│   ├── convenience.py   # the narrate() one-liner; start here
│   ├── core/
│   │   ├── extractor.py   # turns raw SHAP output into a clean structure
│   │   ├── llm_client.py  # the backends: Groq, Anthropic, or none
│   │   ├── narrator.py    # builds the explanation and checks it for safety
│   │   └── scorer.py      # scores how accurate a generated explanation is
│   ├── domains/           # per-industry terminology and tone (healthcare, fraud)
│   └── templates/         # template-mode wording, audience levels, causal-language checks
└── tests/                 # automated tests

Current limitations

  • Thoroughly tested against a logistic regression healthcare model. Tree-based models and neural networks should work in principle but have not been confirmed.
  • Only the healthcare and fraud domains have terminology and tone configured. Finance and insurance are planned.
  • It explains one prediction at a time, not a batch.

Contributing

Issues and pull requests are welcome. If you plan a larger change, please open an issue first to discuss it. Run the test suite (python -m pytest -q) before submitting.


License

Released under the MIT License.

Metadata

Release files for narrashap 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for narrashap 0.1.0
File Size Uploaded Provenance
narrashap-0.1.0.tar.gz 34.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for narrashap 0.1.0
File Interpreter ABI Platform
narrashap-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 63.2 kB

Release files / narrashap-0.1.0.tar.gz

Download URL narrashap-0.1.0.tar.gz
Size 34.1 kB
Tags Source
SHA-256 checksum
How to use checksums
933999057d6a9e54c5ded51865c09ff87598909106174c71ebe544acb7c586ff
BLAKE2b-256 checksum
How to use checksums
935efaeb057dcb2a6fdfdad7c21dff0ed1efa32bc850a463f43ad40ef629cf77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.8

Release files / narrashap-0.1.0-py3-none-any.whl

Download URL narrashap-0.1.0-py3-none-any.whl
Size 29.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
54a48a6c46d1b42c41b8ed640fb4f3c75fb1e01e0cdc9dbfd78f1d8a42adc95d
BLAKE2b-256 checksum
How to use checksums
c3152467864759aafc5e56309163dbbb92a96e2fec265ca87400d697b017b447
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.8

Security for narrashap 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page