Skip to content
Open
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
8 changes: 8 additions & 0 deletions doc/whats_new/v0.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ Changelog
Bug fixes
.........

- Fix a bug in :func:`~imblearn.metrics.macro_averaged_mean_absolute_error`
where a ``ValueError`` was raised when a class was present in ``y_pred``
but absent from ``y_true``. Such classes are now skipped (per-class MAE
is undefined when there are no ground-truth samples), matching the
graceful handling of missing labels in :func:`sklearn.metrics.f1_score`
with ``average='macro'``.
:pr:`1172` by :user:`jbbqqf`.

Enhancements
............

Expand Down
7 changes: 6 additions & 1 deletion imblearn/metrics/_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -1127,7 +1127,12 @@ def macro_averaged_mean_absolute_error(y_true, y_pred, *, sample_weight=None):
else:
sample_weight = np.ones(y_true.shape)
check_consistent_length(y_true, y_pred, sample_weight)
labels = unique_labels(y_true, y_pred)
# Iterate only over classes present in y_true: per-class MAE is undefined
# for a class that has zero ground-truth samples (mean_absolute_error
# rejects empty arrays). This matches the metric's documented intent
# ("computes each MAE for each class") and aligns the macro behaviour
# with sklearn's f1_score(average='macro') on labels missing from y_true.
labels = unique_labels(y_true)
mae = []
for possible_class in labels:
indices = np.flatnonzero(y_true == possible_class)
Expand Down
19 changes: 19 additions & 0 deletions imblearn/metrics/tests/test_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,3 +548,22 @@ def test_macro_averaged_mean_absolute_error_sample_weight():
)

assert ma_mae_unit_weights == pytest.approx(ma_mae_no_weights)


def test_macro_averaged_mean_absolute_error_class_only_in_y_pred():
# Non-regression test for #1094: when a label appears only in y_pred
# (predicted but absent from ground truth), the metric must not raise.
# The per-class MAE for a class with zero ground-truth samples is
# undefined and is therefore excluded from the macro average — this
# matches the docstring intent ("computes each MAE for each class") and
# mirrors the graceful handling of absent labels by sklearn's
# f1_score(average='macro').
# y_true = [0, 0], y_pred = [0, 1]: only class 0 is in y_true, with
# samples [0, 0] vs predictions [0, 1] -> MAE = (0+1)/2 = 0.5; macro
# over the single ground-truth class = 0.5. Crucially, no exception.
assert macro_averaged_mean_absolute_error([0, 0], [0, 1]) == pytest.approx(0.5)

# Symmetric sanity check: when a label is missing from y_pred but
# present in y_true, we still compute MAE on each ground-truth slice.
# class 0: |0-0|=0; class 1: |1-0|=1; macro = (0+1)/2 = 0.5.
assert macro_averaged_mean_absolute_error([0, 1], [0, 0]) == pytest.approx(0.5)