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.
closesPythonOT#664
@codecov

codecovBot 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

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 threadtest/test_da.py
Comment threadot/da.py Outdated
Comment threadot/da.py
@kachayev

Copy link
Copy Markdown
Collaborator

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

@CodingSelim

Copy link
Copy Markdown
ContributorAuthor

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

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

Copy link
Copy Markdown
Collaborator

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

@rflamaryrflamary 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:masterJul 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.
closesPythonOT#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 freeto 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

@CodingSelim@rflamary@kachayev