Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7dc6b4c
fix error
dalestee Apr 15, 2026
759969b
test
Apr 13, 2026
fd7700d
test: action should pass with 3.14 and without 3.9 and 3.10
dalestee Apr 13, 2026
9075f78
other version mods
dalestee Apr 13, 2026
c07499a
test normal
dalestee Apr 13, 2026
888fe2a
test as before
dalestee Apr 13, 2026
b30cbf1
test without 3.9 and 3.10. plus 3.14
dalestee Apr 13, 2026
60b2a60
fix error
dalestee Apr 13, 2026
f78e496
test after fixing ruff errors with ruff format
dalestee Apr 13, 2026
6f741f8
Fix remaining ruff issues
dalestee Apr 13, 2026
6993462
replacing isinstance(A, (X, Y)) by isinstance(A, X | Y)
dalestee Apr 13, 2026
93c917b
Format code with ruff
dalestee Apr 13, 2026
2c23de7
python tests passed
dalestee Apr 13, 2026
a2cc2eb
fix pyupgrade
dalestee Apr 13, 2026
38b6a4f
reducing tests
dalestee Apr 13, 2026
3cbb0f3
hook precommit
dalestee Apr 13, 2026
f4ef3df
added tests names
dalestee Apr 13, 2026
6abb1aa
test all versions at the same time
dalestee Apr 14, 2026
23cf492
Update shapash/explainer/multi_decorator.py
dalestee Apr 14, 2026
7175266
Update shapash/decomposition/contributions.py
dalestee Apr 14, 2026
39172ad
changing versions for pyupgrade
dalestee Apr 15, 2026
be9c5d8
ruff
dalestee Apr 15, 2026
10d191d
fix: possible bug where if it was a Series instead of a DataFrame it …
dalestee Apr 15, 2026
fc2856e
upgrade: more robust syntax
dalestee Apr 15, 2026
e379c50
update: readme
dalestee Apr 15, 2026
a48ac19
fix: correcting fallback
dalestee Apr 16, 2026
570e531
fix: fixing boolean mistake
dalestee Apr 16, 2026
14da385
Merge branch 'master' into Bug-incorrect-plot-displayed-after-sorting…
dalestee Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
max-parallel: 1
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ repos:
args: [--fix]
- id: ruff-format
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.2
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py37-plus]
args: [--py311-plus]
- repo: https://github.com/asottile/blacken-docs
rev: v1.8.0
hooks:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Shapash can use category-encoders object, sklearn ColumnTransformer or simply fe

## 🛠 Installation

Shapash is intended to work with Python versions 3.9 to 3.12. Installation can be done with pip:
Shapash is intended to work with Python versions 3.11 to 3.14. Installation can be done with pip:

```bash
pip install shapash
Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@ authors = [
]
description = "Shapash is a Python library which aims to make machine learning interpretable and understandable by everyone."
readme = "README.md"
requires-python = ">=3.9, <3.14"
requires-python = ">=3.11, <3.15"
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description focuses on a webapp table-sorting selection bug, but this change also bumps the supported Python range (drops 3.9/3.10 and adds 3.14) and updates CI/pre-commit accordingly. Please update the PR description/title to reflect this broader compatibility change, or split the Python version bump into a separate PR for easier review/release notes.

Copilot uses AI. Check for mistakes.
license = {text = "Apache Software License 2.0"}
keywords = ["shapash"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
]
Expand Down
25 changes: 12 additions & 13 deletions shapash/backend/base_backend.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, Optional, Union
from typing import Any

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -28,7 +28,7 @@ class BaseBackend(ABC):
support_groups = True
supported_cases = ["classification", "regression"]

def __init__(self, model: Any, preprocessing: Optional[Any] = None):
def __init__(self, model: Any, preprocessing: Any | None = None):
"""Create a backend instance using a given implementation.

Parameters
Expand Down Expand Up @@ -63,13 +63,12 @@ def run_explainer(self, x: pd.DataFrame) -> dict:
dict containing local contributions
"""
raise NotImplementedError(
f"`{self.__class__.__name__}` is a subclass of BaseBackend and "
f"must implement the `_run_explainer` method"
f"`{self.__class__.__name__}` is a subclass of BaseBackend and must implement the `_run_explainer` method"
)

def get_local_contributions(
self, x: pd.DataFrame, explain_data: Any, subset: Optional[list[int]] = None
) -> Union[pd.DataFrame, list[pd.DataFrame]]:
self, x: pd.DataFrame, explain_data: Any, subset: list[int] | None = None
) -> pd.DataFrame | list[pd.DataFrame]:
"""Get local contributions using the explainer data computed in the `run_explainer`
method.

Expand Down Expand Up @@ -109,10 +108,10 @@ def get_local_contributions(
def get_global_features_importance(
self,
contributions: pd.DataFrame,
explain_data: Optional[dict] = None,
subset: Optional[list[int]] = None,
explain_data: dict | None = None,
subset: list[int] | None = None,
norm: int = 1,
) -> Union[pd.Series, list[pd.Series]]:
) -> pd.Series | list[pd.Series]:
"""Get global contributions using the explainer data computed in the `run_explainer`
method.

Expand Down Expand Up @@ -141,8 +140,8 @@ def get_global_features_importance(
def format_and_aggregate_local_contributions(
self,
x: pd.DataFrame,
contributions: Union[pd.DataFrame, np.array, list[pd.DataFrame], list[np.array]],
) -> Union[pd.DataFrame, list[pd.DataFrame]]:
contributions: pd.DataFrame | np.ndarray | list[pd.DataFrame] | list[np.ndarray],
) -> pd.DataFrame | list[pd.DataFrame]:
"""
This function allows to format and aggregate contributions in the right format
(pd.DataFrame or list of pd.DataFrame).
Expand Down Expand Up @@ -175,8 +174,8 @@ def format_and_aggregate_local_contributions(
return contributions

def _apply_preprocessing(
self, contributions: Union[pd.DataFrame, list[pd.DataFrame]]
) -> Union[pd.DataFrame, list[pd.DataFrame]]:
self, contributions: pd.DataFrame | list[pd.DataFrame]
) -> pd.DataFrame | list[pd.DataFrame]:
"""
Reconstruct contributions for original features, taken into account a preprocessing.

Expand Down
2 changes: 1 addition & 1 deletion shapash/decomposition/contributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,6 @@ def assign_contributions(ranked):
"""
if len(ranked) != 3:
raise ValueError(
f"Expected lenght : 3, observed lenght : {len(ranked)}," "please check the outputs of rank_contributions."
f"Expected length : 3, observed length : {len(ranked)}, please check the outputs of rank_contributions."
)
return {"contrib_sorted": ranked[0], "x_sorted": ranked[1], "var_dict": ranked[2]}
9 changes: 7 additions & 2 deletions shapash/explainer/consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def tuning_colorscale(self, values):
desc_df = values.describe(percentiles=np.arange(0.1, 1, 0.1).tolist())
min_pred, max_init = list(desc_df.loc[["min", "max"]].values)
desc_pct_df = (desc_df.loc[~desc_df.index.isin(["count", "mean", "std"])] - min_pred) / (max_init - min_pred)
color_scale = list(map(list, (zip(desc_pct_df.values.flatten(), self._style_dict["init_contrib_colorscale"]))))
color_scale = list(
map(list, (zip(desc_pct_df.values.flatten(), self._style_dict["init_contrib_colorscale"], strict=False)))
)
return color_scale

def compile(self, contributions, x=None, preprocessing=None):
Expand Down Expand Up @@ -416,7 +418,9 @@ def plot_examples(self, method_1, method_2, l2, index, backend_name_1, backend_n
axes = np.array([axes])
fig.suptitle("Examples of explanations' comparisons for various distances (L2 norm)")

for n, (i, j, k, l, m, o) in enumerate(zip(method_1, method_2, l2, index, backend_name_1, backend_name_2)):
for n, (i, j, k, l, m, o) in enumerate(
zip(method_1, method_2, l2, index, backend_name_1, backend_name_2, strict=False)
):
# Only keep top features according to both methods
idx = np.flip(np.abs(np.concatenate([i, j])).argsort()) % len(i)
_, first_occurrence_idx = np.unique(idx, return_index=True)
Expand Down Expand Up @@ -601,6 +605,7 @@ def plot_pairwise_consistency(
weights[0][c].round(2),
weights[1][c].round(2),
(weights[0][c] - weights[1][c]).round(2),
strict=False,
)
]

Expand Down
8 changes: 4 additions & 4 deletions shapash/explainer/multi_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def check_args(self, args, name):
"""
if not args:
raise ValueError(
f"{name} is applied without arguments," "please check that you have specified contributions."
f"{name} is applied without arguments, please check that you have specified contributions."
)

def check_method(self, method, name):
Expand Down Expand Up @@ -177,7 +177,7 @@ def combine_masks(self, masks):
pd.Dataframe
Combination of all masks.
"""
transposed_masks = list(map(list, zip(*masks)))
transposed_masks = list(map(list, zip(*masks, strict=False)))
return self.delegate("combine_masks", transposed_masks)

def compute_masked_contributions(self, s_contrib, masks):
Expand All @@ -197,7 +197,7 @@ def compute_masked_contributions(self, s_contrib, masks):
list
List of masked contributions (pandas.Series).
"""
arg_tup = list(zip(s_contrib, masks))
arg_tup = list(zip(s_contrib, masks, strict=False))
return self.delegate("compute_masked_contributions", arg_tup)

def summarize(self, s_contribs, var_dicts, xs_sorted, masks, columns_dict, features_dict):
Expand All @@ -224,7 +224,7 @@ def summarize(self, s_contribs, var_dicts, xs_sorted, masks, columns_dict, featu
list of pd.DataFrame
Result of the summarize step
"""
arg_tup = list(zip(s_contribs, var_dicts, xs_sorted, masks))
arg_tup = list(zip(s_contribs, var_dicts, xs_sorted, masks, strict=False))
return self.delegate("summarize", arg_tup, columns_dict, features_dict)

def compute_features_import(self, contributions, norm=1):
Expand Down
23 changes: 14 additions & 9 deletions shapash/explainer/smart_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import math
import random
from typing import Optional

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -452,7 +451,7 @@ def contribution_plot(
if self._explainer._case == "classification":
label_num, _, label_value = self._explainer.check_label_name(label)

if not isinstance(col, (str, int)):
if not isinstance(col, str | int):
raise ValueError("parameter col must be string or int.")
if hasattr(self._explainer, "inv_features_dict"):
col = self._explainer.inv_features_dict.get(col, col)
Expand Down Expand Up @@ -977,7 +976,10 @@ def compare_plot(
f"Response: <b>{label_value}</b> - "
+ "Probas: "
+ " ; ".join(
[str(id) + ": <b>" + str(round(proba, 2)) + "</b>" for proba, id in zip(preds, line_reference)]
[
str(id) + ": <b>" + str(round(proba, 2)) + "</b>"
for proba, id in zip(preds, line_reference, strict=False)
]
)
)

Expand All @@ -988,7 +990,10 @@ def compare_plot(
if show_predict:
preds = [self._explainer._local_pred(line) for line in line_reference]
subtitle = "Predictions: " + " ; ".join(
[str(id) + ": <b>" + str(round(pred, 2)) + "</b>" for id, pred in zip(line_reference, preds)]
[
str(id) + ": <b>" + str(round(pred, 2)) + "</b>"
for id, pred in zip(line_reference, preds, strict=False)
]
)

new_contrib = list()
Expand All @@ -1006,11 +1011,11 @@ def compare_plot(
preds = [self._explainer.x_init.loc[id] for id in line_reference]
dict_features = self._explainer.inv_features_dict

iteration_list = list(zip(new_contrib, feature_values))
iteration_list = list(zip(new_contrib, feature_values, strict=False))
iteration_list.sort(key=lambda x: maximum_difference_sort_value(x), reverse=True)
iteration_list = iteration_list[:max_features]
iteration_list = iteration_list[::-1]
new_contrib, feature_values = list(zip(*iteration_list))
new_contrib, feature_values = list(zip(*iteration_list, strict=False))

fig = plot_line_comparison(
line_reference,
Expand Down Expand Up @@ -1122,7 +1127,7 @@ def interactions_plot(
>>> xpl.plot.interactions_plot(0, 1)
"""

if not (isinstance(col1, (str, int)) or isinstance(col2, (str, int))):
if not (isinstance(col1, str | int) or isinstance(col2, str | int)):
Comment thread
dalestee marked this conversation as resolved.
Outdated
raise ValueError("parameters col1 and col2 must be string or int.")

col_id1 = self._explainer.check_features_name([col1])[0]
Expand Down Expand Up @@ -1925,7 +1930,7 @@ def confusion_matrix_plot(
def distribution_plot(
self,
col: str,
hue: Optional[str] = None,
hue: str | None = None,
width: int = 700,
height: int = 500,
nb_cat_max: int = 7,
Expand Down Expand Up @@ -2106,7 +2111,7 @@ def clustering_by_explainability_plot(

if not hasattr(self._explainer, "model"):
raise AssertionError(
"Explainer object was not compiled. Please compile the explainer " "object using .compile(...) method."
"Explainer object was not compiled. Please compile the explainer object using .compile(...) method."
)

if not hasattr(self._explainer, "_case") or self._explainer._case not in {"classification", "regression"}:
Expand Down
2 changes: 1 addition & 1 deletion shapash/explainer/smart_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def validate_contributions(self, contributions, x_init):
pandas.DataFrame
Local contributions on the original feature space (no encoding).
"""
if not isinstance(contributions, (np.ndarray, pd.DataFrame)):
if not isinstance(contributions, np.ndarray | pd.DataFrame):
raise ValueError("Type of contributions must be pd.DataFrame or np.ndarray")
if isinstance(contributions, np.ndarray):
return pd.DataFrame(contributions, columns=x_init.columns, index=x_init.index)
Expand Down
6 changes: 3 additions & 3 deletions shapash/manipulation/select_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ def keep_right_contributions(y_pred, contributions, _case, _classes, label_dict,

"""
if _case == "classification":
complete_sum = [list(x) for x in list(zip(*[df.values.tolist() for df in contributions]))]
complete_sum = [list(x) for x in list(zip(*[df.values.tolist() for df in contributions], strict=False))]
indexclas = [_classes.index(x) for x in list(flatten(y_pred.values))]
summary = pd.DataFrame(
[summar[ind] for ind, summar in zip(indexclas, complete_sum)],
[summar[ind] for ind, summar in zip(indexclas, complete_sum, strict=False)],
columns=contributions[0].columns,
index=contributions[0].index,
dtype=object,
Expand All @@ -61,7 +61,7 @@ def keep_right_contributions(y_pred, contributions, _case, _classes, label_dict,
y_pred = y_pred.map(lambda x: label_dict[x])
if proba_values is not None:
y_proba = pd.DataFrame(
[proba[ind] for ind, proba in zip(indexclas, proba_values.values)],
[proba[ind] for ind, proba in zip(indexclas, proba_values.values, strict=False)],
columns=["proba"],
index=y_pred.index,
)
Expand Down
2 changes: 1 addition & 1 deletion shapash/manipulation/summarize.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def summarize(s_contrib, var_dict, x_sorted, mask, columns_dict, features_dict):
summary = pd.concat([contrib_sum, var_dict_sum, x_sorted_sum], axis=1)

# Ordering columns
ordered_columns = list(flatten(zip(var_dict_sum.columns, x_sorted_sum.columns, contrib_sum.columns)))
ordered_columns = list(flatten(zip(var_dict_sum.columns, x_sorted_sum.columns, contrib_sum.columns, strict=False)))
summary = summary[ordered_columns]
return summary

Expand Down
2 changes: 1 addition & 1 deletion shapash/plots/plot_bar_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def plot_bar_chart(
margin={"l": 150, "r": 20, "t": topmargin, "b": 70},
)
bars = []
for num, expl in enumerate(zip(var_dict, x_val, contrib)):
for num, expl in enumerate(zip(var_dict, x_val, contrib, strict=False)):
feat_name, x_val_el, contrib_value = expl
is_grouped = False
if x_val_el == "":
Expand Down
8 changes: 6 additions & 2 deletions shapash/plots/plot_contribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ def plot_scatter(
feature_values = pd.DataFrame({column_name: feature_values_str})

if pred is not None:
hv_text = [f"Id: {x}<br />Predict: {y}" for x, y in zip(feature_values.index, pred.values.flatten())]
hv_text = [
f"Id: {x}<br />Predict: {y}" for x, y in zip(feature_values.index, pred.values.flatten(), strict=False)
]
else:
hv_text = [f"Id: {x}" for x in feature_values.index]

Expand Down Expand Up @@ -679,7 +681,9 @@ def _prepare_hover_text(feature_values, pred, feature_name):
hv_text = [
f"Id: {id_val}{f'<br />Predict: {pred_val}' if pred is not None else ''}"
for id_val, pred_val in zip(
feature_values.index, pred.values.flatten() if pred is not None else [""] * len(feature_values)
feature_values.index,
pred.values.flatten() if pred is not None else [""] * len(feature_values),
strict=False,
)
]

Expand Down
8 changes: 3 additions & 5 deletions shapash/plots/plot_correlations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Optional

import numpy as np
import pandas as pd
import scipy.cluster.hierarchy as sch
Expand All @@ -14,7 +12,7 @@

def plot_correlations(
df,
style_dict: Optional[dict] = None,
style_dict: dict | None = None,
palette_name: str = "default",
features_dict=None,
optimized=False,
Expand Down Expand Up @@ -197,7 +195,7 @@ def prepare_corr_matrix(df_subset):
coloraxis="coloraxis",
text=[
[
f"Feature 1: {features_dict.get(y, y)} <br />" f"Feature 2: {features_dict.get(x, x)}"
f"Feature 1: {features_dict.get(y, y)} <br />Feature 2: {features_dict.get(x, x)}"
for x in list_features
]
for y in list_features
Expand All @@ -219,7 +217,7 @@ def prepare_corr_matrix(df_subset):
coloraxis="coloraxis",
text=[
[
f"Feature 1: {features_dict.get(y, y)} <br />" f"Feature 2: {features_dict.get(x, x)}"
f"Feature 1: {features_dict.get(y, y)} <br />Feature 2: {features_dict.get(x, x)}"
for x in list_features
]
for y in list_features
Expand Down
Loading