Skip to content

[MRG] Fix label-aware cost correction in ot.da (closes #664)#833

Merged
rflamary merged 6 commits into
PythonOT:masterfrom
CodingSelim:fix-da-cost-correction
Jul 7, 2026
Merged

[MRG] Fix label-aware cost correction in ot.da (closes #664)#833
rflamary merged 6 commits into
PythonOT:masterfrom
CodingSelim:fix-da-cost-correction

Conversation

@CodingSelim

Copy link
Copy Markdown
Contributor

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Motivation and context / Related issue

Closes #664.

The cost correction in BaseTransport.fit that is supposed to push apart source and target samples with different labels was computed backwards. It built missing_ys = (ys == -1) / missing_yt = (yt == -1) and used their outer product missing_labels, so the large limit_max cost was applied only where both labels are missing, and label_match was actually a mismatch mask. The net effect:

  • when all labels are known (missing_labels is all zeros) the correction is a complete no-op, so ys/yt had no influence on the transport at all
  • it also contradicted the code`s own comment ("0 iff either masked").

This is why a supervised fit gave the same cost matrix as an unsupervised one.

Fix: use present masks (ys != -1) / (yt != -1), so the correction applies exactly to labeled source/target pairs whose labels differ. This matches the original pre-vectorized loop (the for c in classes: cost_[idx_s, idx_t] = limit_max version) that the vectorization in #587 replaced.

How has this been tested

  • The semisupervised checks in test_da.py had been flipped when the bug was introduced to assert n_unsup == n_semisup (i.e. that supervised mode does nothing), while the comment right above still said the norms should be different. Restored them to assert the cost actually changes.
  • Added test_semisupervised_cost_correction: with all labels known, different-label pairs get pushed to limit_max and same-label pairs keep their smaller cost; in the semisupervised case, columns with an unlabeled (-1) target are never forbidden. It fails on current master and passes with this change.
  • Full test_da.py passes locally.

PR checklist

  • I have read the CONTRIBUTING document.
  • All tests passed, and additional code has been covered with new tests.
  • I have added the PR and Issue fix to the RELEASES.md file.

the cost correction that pushes apart differently-labeled samples was
computed backwards. it used missing_ys = (ys == -1) and the product of
the two missing masks, so the large cost was applied only where both
labels are missing, and never where two labeled samples have different
labels. with all labels known the correction was a no-op, so ys/yt had
no effect on the transport (see PythonOT#664).

switched to present masks (ys != -1) so the correction applies exactly
to labeled source/target pairs whose labels differ, matching the
original pre-vectorized loop. restored the semisupervised tests that had
been flipped to assert the buggy no-op (n_unsup == n_semisup) back to
asserting the cost actually changes, and added a regression test.

closes PythonOT#664
@github-actions github-actions Bot added the Tests label Jul 6, 2026
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.85%. Comparing base (e52c2a3) to head (fbd765f).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #833   +/-   ##
=======================================
  Coverage   96.85%   96.85%           
=======================================
  Files         128      128           
  Lines       25659    25683   +24     
=======================================
+ Hits        24852    24876   +24     
  Misses        807      807           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rflamary

rflamary commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

hello @CodingSelim the PR is looking good, what you coded makes sens to me. You need to skip the tf backend for the DA class.

@kachayev i'm open to your opinion on this too. I would like to do a release shortly

Comment thread test/test_da.py
Comment thread ot/da.py Outdated
Comment thread ot/da.py
@kachayev

kachayev commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@rflamary Looks good to me! Left a couple of minor comments/suggestions.

@CodingSelim

Copy link
Copy Markdown
Contributor Author

thanks for the review! pushed the MISSING_LABEL constant.

on the cost_correction refactor, i think the item assignment (cost_correction[mask] = self.limit_max) won't work across backends, jax arrays are immutable and the tf tensor path doesn't support that kind of in-place scatter either, which is why the original avoids indexing and does it with arithmetic. i kept the multiply approach but it should be fine, the only reason for the catch_warnings is the 0*inf case when limit_max is left at the default inf, and we already scrub those with nan_to_num right after. happy to try a where-based version if you'd prefer that over the warnings filter, that one stays backend safe.

@rflamary

rflamary commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Yep jax is a pain for generic backends... I think the PR looks great, let us wait for tetss to pass and merge it

@kachayev

kachayev commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@CodingSelim you are right! don't worry, it's a chunk of old code anyways

@rflamary rflamary changed the title Fix label-aware cost correction in ot.da (closes #664) [MRG] Fix label-aware cost correction in ot.da (closes #664) Jul 7, 2026
@rflamary rflamary merged commit c980bda into PythonOT:master Jul 7, 2026
21 checks passed
Flastre pushed a commit to Flastre/POT that referenced this pull request Jul 8, 2026
…ythonOT#833)

* fix label-aware cost correction in ot.da

the cost correction that pushes apart differently-labeled samples was
computed backwards. it used missing_ys = (ys == -1) and the product of
the two missing masks, so the large cost was applied only where both
labels are missing, and never where two labeled samples have different
labels. with all labels known the correction was a no-op, so ys/yt had
no effect on the transport (see PythonOT#664).

switched to present masks (ys != -1) so the correction applies exactly
to labeled source/target pairs whose labels differ, matching the
original pre-vectorized loop. restored the semisupervised tests that had
been flipped to assert the buggy no-op (n_unsup == n_semisup) back to
asserting the cost actually changes, and added a regression test.

closes PythonOT#664

* add PR number to releases entry

* Apply suggestion from @rflamary

* use MISSING_LABEL constant instead of bare -1

---------

Co-authored-by: Rémi Flamary <remi.flamary@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect computation of cost_correction matrix in ot.da.EMDTransport

3 participants