Skip to content

[FEAT] Add ALE (old fork)#689

Draft
abaziree wants to merge 13 commits into
mind-inria:mainfrom
abaziree:297-ale
Draft

[FEAT] Add ALE (old fork)#689
abaziree wants to merge 13 commits into
mind-inria:mainfrom
abaziree:297-ale

Conversation

@abaziree

@abaziree abaziree commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Issue #297

I added 1D ALE for continuous and discrete numeric features, and 2D ALE.
There are currently no tests.

Reference paper: https://arxiv.org/pdf/1612.08468

Use of AI tools:

  • LLM tools were used to generate at least part of the content of this pull request.

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.16%. Comparing base (60c4194) to head (c025899).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #689   +/-   ##
=======================================
  Coverage   98.16%   98.16%           
=======================================
  Files          23       23           
  Lines        1633     1633           
=======================================
  Hits         1603     1603           
  Misses         30       30           

☔ View full report in Codecov by Harness.
📢 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.

@bthirion bthirion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm still wondering what we should do for discrete features. Isn't PDP a more natural solution for such feature ? I'm worried about the arbitrary ordering, that may lead to a biased representation.

To be discussed again.

Comment thread src/hidimstat/visualization/_compute_accumulated_local_effects.py Outdated
Comment thread src/hidimstat/visualization/_compute_accumulated_local_effects.py Outdated
Comment thread src/hidimstat/visualization/_compute_accumulated_local_effects.py Outdated
to gather local samples in each bin.
feature_idx : int
Column index of the feature of interest.
grid_resolution : int, default=50

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

could have an auto value, where the number of bins is set in order to minimize the histogram error.

difference in model output when the feature moves from the lower to the upper
neighbour values across all samples that have that value. The cumulative sum
of these average effects gives the (uncentered) ALE curve, which is then
centered so its weighted mean is zero.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there a reference to justify the behavior ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The docstring is maybe not precise enough. In an interval ]x_{k-1}, x_k] of the feature of interest , we calculate the local effect by considering on the one hand samples with value x_{k-1} subsequently shifted to the value x_k, and on the other hand samples with value x_k subsequently shifted to the value x_{k-1}. It's exactly the same formula as the one in the Apley & Zhu paper.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please include a ref to that paper.

X : array-like of shape (n_samples, n_features)
feature_indices : tuple or list of two ints
Column indices ``[i, j]`` of the two features of interest.
grid_resolution : int, default=20

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

could also have an auto mode

\frac{\partial f(X)}{\partial X_j}
\;\middle|\; X_j = z
\right] dz - c

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should include a bilbiographic reference to the definition

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added the Apley and Zhu reference. Should we also add Molnar's one even though I haven't specifically used it ?

Comment on lines +6 to +59
def _check_estimator_predict(estimator):
"""Return a scalar-output prediction callable for *estimator*.

For classifiers we use ``predict_proba`` (returns the probability of the
positive class for binary problems) when available, otherwise
``decision_function``. For regressors we use ``predict``.

Parameters
----------
estimator : fitted sklearn-compatible estimator

Returns
-------
predict_fn : callable
A function that accepts an array of shape ``(n_samples, n_features)``
and returns a 1D array of shape ``(n_samples,)``.
"""
feature_names = getattr(estimator, "feature_names_in_", None)

if hasattr(estimator, "predict_proba"):

def predict_fn(X):
if feature_names is not None:
X = pd.DataFrame(X, columns=feature_names)
proba = estimator.predict_proba(X)
# Binary: keep only the positive-class column
if proba.ndim == 2 and proba.shape[1] == 2:
return proba[:, 1]
return proba

return predict_fn

if hasattr(estimator, "decision_function"):

def predict_fn(X):
if feature_names is not None:
X = pd.DataFrame(X, columns=feature_names)
return estimator.decision_function(X).ravel()

return predict_fn

if hasattr(estimator, "predict"):

def predict_fn(X):
if feature_names is not None:
X = pd.DataFrame(X, columns=feature_names)
return estimator.predict(X).ravel()

return predict_fn

raise ValueError(
"'estimator' must expose at least one of predict_proba, "
"decision_function, or predict."
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This function has a few issues :

  • L38-54 can be refactored with getattr(estimator, prediction_func)(X).ravel()
  • imho, nested functions reduce code readability. Here, the function could directly return the prediction for instance

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yep, I agree with your second point, it's not really useful here.

Comment on lines +328 to +366
quantiles_i = _build_quantile_grid(x_i, grid_resolution)
quantiles_j = _build_quantile_grid(x_j, grid_resolution)

n_bins_i = len(quantiles_i) - 1
n_bins_j = len(quantiles_j) - 1

if n_bins_i < 1:
raise ValueError(
f"Feature {idx_i} has fewer than 2 unique quantile edges. Increase grid_resolution or check your data."
)
if n_bins_j < 1:
raise ValueError(
f"Feature {idx_j} has fewer than 2 unique quantile edges. Increase grid_resolution or check your data."
)

bin_idx_i = _bin_indices(x_i, quantiles_i)
bin_idx_j = _bin_indices(x_j, quantiles_j)

# Second-order finite differences: evaluate at the four corners of each 2D bin
X_ll = X.copy()
X_ll[:, idx_i] = quantiles_i[bin_idx_i]
X_ll[:, idx_j] = quantiles_j[bin_idx_j]

X_lh = X.copy()
X_lh[:, idx_i] = quantiles_i[bin_idx_i]
X_lh[:, idx_j] = quantiles_j[bin_idx_j + 1]

X_hl = X.copy()
X_hl[:, idx_i] = quantiles_i[bin_idx_i + 1]
X_hl[:, idx_j] = quantiles_j[bin_idx_j]

X_hh = X.copy()
X_hh[:, idx_i] = quantiles_i[bin_idx_i + 1]
X_hh[:, idx_j] = quantiles_j[bin_idx_j + 1]

# Second-order local effects
local_effects = (predict_fn(X_hh) - predict_fn(X_hl)) - (
predict_fn(X_lh) - predict_fn(X_ll)
) # shape (n_samples,)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

bin_idx could be a 1D array which could refactor this code with a loop, and reduce the number of copies of X

@abaziree abaziree marked this pull request as draft June 24, 2026 17:43
@abaziree abaziree changed the title [FEAT] Add ALE [FEAT] Add ALE (old fork) Jun 24, 2026
@abaziree

Copy link
Copy Markdown
Collaborator Author

Kept as draft to have a history of discussions

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants