Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/Python-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest] # windows-latest
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']

steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 6 additions & 2 deletions python/dalex/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
## Changelog

### development
### v1.8.0 (2026-01-20)

...
* substitute the deprecated `pkg_resources` dependency that breaks `dalex` ([#579](https://github.com/ModelOriented/DALEX/issues/579))
* increase the `plotly` dependency to `>=6.0.0` and fix compatibility issues with the new version, e.g. `titlefont` is now `title_font` ([#573](https://github.com/ModelOriented/DALEX/issues/573))
* restrict the `pandas` dependency to `<3.0.0` to counteract future api changes, e.g. `pd.stack(..., future_stack=False)`.
* remove the `ppscore` optional dependency used by the `aspect` module from `dalex[full]` as it imposes `pandas<2.0.0`
* increase the dependency to `python>=3.9` and add `python==3.13` to CI

### v1.7.2 (2025-02-12)

Expand Down
2 changes: 1 addition & 1 deletion python/dalex/dalex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .aspect import Aspect


__version__ = '1.7.2.9000'
__version__ = '1.8.0'

__all__ = [
"Arena",
Expand Down
2 changes: 1 addition & 1 deletion python/dalex/dalex/_explainer/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ def is_y_in_data(data, y):


def get_model_info(model):
model_package = re.search("(?<=<class ').*?(?=\.)", str(type(model)))[0]
model_package = re.search(r"(?<=<class ').*?(?=\.)", str(type(model)))[0]
return {'model_package': model_package}
11 changes: 6 additions & 5 deletions python/dalex/dalex/_global_checks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pkg_resources
from importlib import import_module
from importlib.metadata import version, PackageNotFoundError
from packaging.version import parse
from re import search

# WARNING: below code is parsed by setup.py
Expand All @@ -25,9 +26,9 @@
'flask_cors': '3.0.8',
'requests': '2.24.0',
'kaleido': '0.2.1',
'ppscore': '1.2.0',
'ipywidgets': '7.6.3'
}
# 'ppscore': '1.3.0',
# WARNING
# WARNING

Expand All @@ -41,13 +42,13 @@ def global_check_import(name=None, functionality=None):
else:
import_module(name)

installed_version = pkg_resources.parse_version(pkg_resources.get_distribution(name).version)
needed_version = pkg_resources.parse_version(OPTIONAL_DEPENDENCIES[name])
installed_version = parse(version(name))
needed_version = parse(OPTIONAL_DEPENDENCIES[name])
if installed_version < needed_version:
raise ImportWarning("Outdated version of optional dependency '" + name + "'. " +
("Update '" + name + "' for " + functionality + ". ") if functionality else "" +
"Use pip or conda to update '" + name + "' to avoid potential errors.")
except ImportError:
except (ImportError, PackageNotFoundError):
raise ImportError("Missing optional dependency '" + name + "'. " +
("Install '" + name + "' for " + functionality + ". ") if functionality else "" +
"Use pip or conda to install '" + name + "'.")
Expand Down
12 changes: 7 additions & 5 deletions python/dalex/dalex/fairness/_group_fairness/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,20 +328,20 @@ def plot_metric_scores(fobject,
for metric in data.metric.unique():
for label in data.label.unique():
x = float(privileged_data.loc[(privileged_data.metric == metric) &
(privileged_data.label == label), :].score)
(privileged_data.label == label), :].score.iloc[0])
if np.isnan(x):
lines_nan = True
continue
# lines
for subgroup in data.subgroup.unique():
y = float(data.loc[(data.metric == metric) &
(data.label == label) &
(data.subgroup == subgroup)].subgroup_numeric)
(data.subgroup == subgroup)].subgroup_numeric.iloc[0])
# horizontal

x0 = float(data.loc[(data.metric == metric) &
(data.label == label) &
(data.subgroup == subgroup)].score)
(data.subgroup == subgroup)].score.iloc[0])

if np.isnan(x0):
lines_nan = True
Expand Down Expand Up @@ -691,7 +691,7 @@ def plot_ceteris_paribus_cutoff(fobject,
def plot_density(fobject,
other_objects,
title):
data = pd.DataFrame(columns=['y', 'y_hat', 'subgroup', 'model'])
data_list = []
objects = [fobject]
if other_objects is not None:
for other_obj in other_objects:
Expand All @@ -703,7 +703,9 @@ def plot_density(fobject,
'y_hat': y_hat,
'subgroup': np.repeat(subgroup, len(y)),
'model': np.repeat(obj.label, len(y))})
data = pd.concat([data, data_to_append])
data_list.append(data_to_append)

data = pd.concat(data_list)

fig = go.Figure()

Expand Down
17 changes: 10 additions & 7 deletions python/dalex/dalex/fairness/_group_fairness/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ def __init__(self, sub_confusion_matrix):

def to_vertical_DataFrame(self) -> pd.DataFrame:

columns = ['metric', 'subgroup', 'score']
data = pd.DataFrame(columns=columns)
df_list = []
metrics = self.subgroup_confusion_matrix_metrics
for subgroup in metrics.keys():
metric = metrics.get(subgroup)
subgroup_vec = np.repeat(subgroup, len(metric))
sub_df = pd.DataFrame({'metric': metric.keys(), 'subgroup': subgroup_vec, 'score': metric.values()})
data = pd.concat([data, sub_df])
df_list.append(sub_df)
data = pd.concat(df_list, ignore_index=True)
return data

def to_horizontal_DataFrame(self) -> pd.DataFrame:
Expand Down Expand Up @@ -194,7 +194,7 @@ def _fairness_theme(title):
'template': 'plotly_white',
'title_x': 0.5,
'title_y': 0.99,
'titlefont': {'size': 25},
'title_font': {'size': 25},
'font': {'color': "#371ea3"},
'margin': {'t': 78, 'b': 71, 'r': 30}}

Expand Down Expand Up @@ -286,8 +286,7 @@ def calculate_regression_measures(y, y_hat, protected, privileged):
unique_protected = np.unique(protected)
unique_unprivileged = unique_protected[unique_protected != privileged]

data = pd.DataFrame(columns=['subgroup', 'independence', 'separation', 'sufficiency'])

data_list = []
for unprivileged in unique_unprivileged:
# filter elements
array_elements = np.isin(protected, [privileged, unprivileged])
Expand Down Expand Up @@ -319,8 +318,12 @@ def calculate_regression_measures(y, y_hat, protected, privileged):
'independence': [r_ind],
'separation': [r_sep],
'sufficiency': [r_suf]})
data_list.append(to_append)

data = pd.concat([data, to_append])
if data_list:
data = pd.concat(data_list, ignore_index=True)
else:
data = pd.DataFrame(columns=['subgroup', 'independence', 'separation', 'sufficiency'])

# append the scale
to_append = pd.DataFrame({'subgroup': [privileged],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def aggregate_profiles(all_profiles, mean_prediction, type, groups, center, span
aggregated_profiles = \
all_profiles. \
loc[:, ["_vname_", "_label_", "_x_", "_yhat_", "_ids_", "_original_"] + groups]. \
groupby(['_vname_', '_label_']). \
groupby(['_vname_', '_label_'])[["_x_", "_yhat_", "_ids_", "_original_"] + groups]. \
progress_apply(lambda split_profile: split_over_variables_and_labels(split_profile.copy(deep=True),
type, groups, span)). \
reset_index(level=[0, 1]) # remove level_2
Expand Down Expand Up @@ -83,7 +83,7 @@ def split_over_variables_and_labels(split_profile, type, groups, span):

par_profile = split_profile.groupby(['_x_'] + groups, sort=False). \
apply(lambda point: (point['_yhat_'] * point['_w_']).sum() / point['_w_'].sum() \
if point['_w_'].sum() != 0 else 0)
if point['_w_'].sum() != 0 else 0, include_groups=False)

par_profile.name = '_yhat_'
par_profile = par_profile.reset_index()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def check_variable_groups(variable_groups, explainer):
if not isinstance(variable_groups[key][0], str):
raise TypeError("variable_groups' is a dict of lists of variables")

wrong_names[i] = np.in1d(variable_groups[key], explainer.data.columns).all()
wrong_names[i] = np.isin(variable_groups[key], explainer.data.columns).all()

wrong_names = not wrong_names.all()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def calculate_2d_changes(explainer,

yhats = explainer.predict(current_data)
average_yhats[i] = yhats.mean()
average_yhats_norm[i] = average_yhats[i] - diffs_1d[inds.iloc[i, 0]] - diffs_1d.iloc[inds.iloc[i, 1]]
average_yhats_norm[i] = average_yhats[i] - diffs_1d.iloc[inds.iloc[i, 0]] - diffs_1d.iloc[inds.iloc[i, 1]]

columns = explainer.data.columns
average_yhats = pd.Series(average_yhats)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def single_variable_profile(predict,
ids = np.repeat(data.index.values, split_points.shape[0])
new_data = data.loc[ids, :]
original = new_data.loc[:, variable].copy()
if pd.api.types.is_numeric_dtype(new_data[variable]):
new_data[variable] = new_data[variable].astype('float')
new_data.loc[:, variable] = np.tile(split_points, data.shape[0])

yhat = predict(model, new_data)
Expand Down
46 changes: 23 additions & 23 deletions python/dalex/setup.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,52 @@
import codecs
import os
import ast


this_directory = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f:
readme = f.read()

with open(os.path.join(this_directory, 'NEWS.md'), encoding='utf-8')as f:
news = f.read()


# https://packaging.python.org/guides/single-sourcing-package-version/
def read(rel_path):
with codecs.open(os.path.join(this_directory, rel_path), 'r') as fp:
"""Read a file relative to the setup.py location."""
with open(os.path.join(this_directory, rel_path), encoding='utf-8') as fp:
return fp.read()

readme = read('README.md')
news = read('NEWS.md')

def get_version(rel_path):
"""Extract __version__ from a file without importing it."""
for line in read(rel_path).splitlines():
if line.startswith('__version__'):
delimiter = '"' if '"' in line else "'"
return line.split(delimiter)[1]


def get_optional_dependencies(rel_path):
"""Parse OPTIONAL_DEPENDENCIES dict from a python file securely."""
# read _global_checks.py and construct a list of optional dependencies
flag = False
to_parse = "{"

for line in read(rel_path).splitlines():
if flag:
if line == "}": # end
if line == "}": # end of dict
to_parse += line
break
to_parse += line.strip()
if line.startswith('OPTIONAL_DEPENDENCIES'): # start
if line.startswith('OPTIONAL_DEPENDENCIES'): # start of dict
flag = True

od_dict = eval(to_parse)
od_list = [k + ">=" + v for k, v in od_dict.items()]
del od_list[0] # remove artificial dependency used in test_global.py
# Use ast.literal_eval instead of eval for safety
od_dict = ast.literal_eval(to_parse)
od_list = [f"{k}>={v}" for k, v in od_dict.items()]
# remove artificial dependency used in test_global.py
del od_list[0]
return od_list


def run_setup():
# fixes warning https://github.com/pypa/setuptools/issues/2230
from setuptools import setup, find_packages

extras_require = get_optional_dependencies("dalex/_global_checks.py")
full_dependencies = get_optional_dependencies("dalex/_global_checks.py")

setup(
name="dalex",
Expand All @@ -57,7 +56,7 @@ def run_setup():
author_email="przemyslaw.biecek@gmail.com",
version=get_version("dalex/__init__.py"),
description="Responsible Machine Learning in Python",
long_description=u"\n\n".join([readme, news]),
long_description="\n\n".join([readme, news]),
long_description_content_type="text/markdown",
url="https://dalex.drwhy.ai/",
project_urls={
Expand All @@ -70,26 +69,27 @@ def run_setup():
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"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",
"License :: OSI Approved",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
],
install_requires=[
'setuptools',
'pandas>=1.5.3',
'numpy>=1.23.3',
'packaging',
'pandas>=1.5.3,<3.0.0',
'numpy>=1.23.5',
'scipy>=1.6.3',
'plotly>=5.1.0,<6.0.0',
'plotly>=6.0.0',
'tqdm>=4.61.2',
],
extras_require={'full': extras_require},
extras_require={'full': full_dependencies},
packages=find_packages(include=["dalex", "dalex.*"]),
python_requires='>=3.8',
python_requires='>=3.9',
include_package_data=True
)

Expand Down
4 changes: 2 additions & 2 deletions python/dalex/test/test_aggregated_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def test_accumulated(self):
self.assertIsInstance(fig1, Figure)
self.assertIsInstance(fig2, Figure)

test1 = case1.result.groupby('_vname_').apply(lambda x: x['_yhat_'].abs().min()).tolist()
test2 = case2.result.groupby('_vname_').apply(lambda x: x['_yhat_'].abs().min()).tolist()
test1 = case1.result.groupby('_vname_')['_yhat_'].apply(lambda x: x.abs().min()).tolist()
test2 = case2.result.groupby('_vname_')['_yhat_'].apply(lambda x: x.abs().min()).tolist()

self.assertListEqual(test1, np.zeros(len(test1)).tolist())
self.assertListEqual(test2, np.zeros(len(test2)).tolist())
Expand Down
12 changes: 6 additions & 6 deletions python/dalex/test/test_arena_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def setUp(self):
FairnessCheckContainer, ShapleyValuesDependenceContainer, ShapleyValuesVariableImportanceContainer,
VariableAgainstAnotherContainer, VariableDistributionContainer]

@unittest.skipIf(sys.platform.startswith("win"), "requires Windows")

def test_supported_plots(self):
arena = dx.Arena()
arena.push_model(self.exp)
Expand All @@ -74,15 +74,15 @@ def test_supported_plots(self):
except Exception:
pass

@unittest.skipIf(sys.platform.startswith("win"), "requires Windows")
@unittest.skipUnless(sys.platform.startswith("ubuntu"), "requires Ubuntu")
def test_server(self):
arena = dx.Arena()
arena.push_model(self.exp)
arena.push_model(self.exp2)
port = get_free_port()
try:
arena.run_server(port=port)
time.sleep(2)
time.sleep(10)
self.assertFalse(try_port(port))
arena.stop_server()
except AssertionError as e:
Expand All @@ -93,7 +93,7 @@ def test_server(self):
except Exception:
pass

@unittest.skipIf(sys.platform.startswith("win"), "requires Windows")
@unittest.skipUnless(sys.platform.startswith("ubuntu"), "requires Ubuntu")
def test_plots(self):
arena = dx.Arena()
arena.push_model(self.exp)
Expand All @@ -110,7 +110,7 @@ def test_plots(self):
except Exception:
pass

@unittest.skipIf(sys.platform.startswith("win"), "requires Windows")
@unittest.skipUnless(sys.platform.startswith("ubuntu"), "requires Ubuntu")
def test_observation_attributes(self):
arena = dx.Arena()
arena.push_model(self.exp)
Expand All @@ -128,7 +128,7 @@ def test_observation_attributes(self):
except Exception:
pass

@unittest.skipIf(sys.platform.startswith("win"), "requires Windows")
@unittest.skipUnless(sys.platform.startswith("ubuntu"), "requires Ubuntu")
def test_variable_attributes(self):
arena = dx.Arena()
arena.push_model(self.exp)
Expand Down
Loading