[FEAT] Add ALE (old fork)#689
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
bthirion
left a comment
There was a problem hiding this comment.
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.
| to gather local samples in each bin. | ||
| feature_idx : int | ||
| Column index of the feature of interest. | ||
| grid_resolution : int, default=50 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Is there a reference to justify the behavior ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
could also have an auto mode
| \frac{\partial f(X)}{\partial X_j} | ||
| \;\middle|\; X_j = z | ||
| \right] dz - c | ||
|
|
There was a problem hiding this comment.
We should include a bilbiographic reference to the definition
There was a problem hiding this comment.
I added the Apley and Zhu reference. Should we also add Molnar's one even though I haven't specifically used it ?
| 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." | ||
| ) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Yep, I agree with your second point, it's not really useful here.
| 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,) |
There was a problem hiding this comment.
bin_idx could be a 1D array which could refactor this code with a loop, and reduce the number of copies of X
|
Kept as draft to have a history of discussions |
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: