Skip to content

[MRG] Fix mean centering in ot.dr.fda and ot.dr.wda - #840

Merged
rflamary merged 3 commits into
PythonOT:masterfrom
deeb01:fix-dr-mean-centering
Aug 26, 2026
Merged

[MRG] Fix mean centering in ot.dr.fda and ot.dr.wda#840
rflamary merged 3 commits into
PythonOT:masterfrom
deeb01:fix-dr-mean-centering

Conversation

@deeb01

@deeb01 deeb01 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Types of changes

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

Motivation and context / Related issue

np.mean(X) without axis returns a scalar; .reshape((1, -1)) on it gives (1, 1) and broadcasts silently.

  1. X -= mx.reshape((1, -1)) subtracts one scalar from every feature, so proj does not center as documented. wda's learned P is unaffected — the cost sees only pairwise differences.
  2. In fda, mxc[:, i] = np.mean(xc[i]) makes every class mean a constant vector, so Cb = [Σ_i (m_i − m̄)²]·11ᵀ: rank one, no information about which features separate the classes.
  3. Both mutated the caller's X in place.

On 3 classes separated along feature 0 with 4 noise features, fda gave [0.68, 0.99, 0.63, 1.00, 0.97]; now [1.00, 0.022, 0.043, 0.034, 0.124], matching sklearn's LDA to |cos| = 1.0.

ot.dr.fda is the baseline in examples/others/plot_WDA.py.

How has this been tested (if it applies)

3 new tests, each verified failing on master. test_dr.py 9 passed, pre-commit clean.

PR checklist

  • I have read the CONTRIBUTING document.
  • The documentation is up-to-date with the changes I made (check build artifacts).
  • 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.

np.mean(X) returns the mean over all entries rather than the per-feature
mean, so `X -= mx.reshape((1, -1))` subtracted a single scalar from every
feature. The `proj` callables therefore did not center the data, contrary
to their documented behaviour.

In `fda` the same pattern appears in the class means, where
`mxc[:, i] = np.mean(xc[i])` fills the column of class i with a scalar.
Every class mean becomes a constant vector, so the between-class scatter
matrix reduces to a multiple of the all-ones outer product and carries no
information about which features separate the classes. The generalized
eigenproblem eig(Cb, Cw + reg*I) then returns a direction driven only by
the within-class scatter. On data separated along a single axis, `fda`
returned a direction essentially orthogonal to it; it now agrees with
sklearn's LinearDiscriminantAnalysis to within numerical precision.

Both functions also mutated the caller's array through the in-place `-=`,
which is now avoided.

Adds non-regression tests for the recovered direction, for the centering
of `proj`, and for the absence of input mutation.
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.86%. Comparing base (2de0938) to head (7b4dc62).

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

@rflamary
rflamary merged commit 62706ab into PythonOT:master Aug 26, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants