Skip to content

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

Open
deeb01 wants to merge 2 commits into
PythonOT:masterfrom
deeb01:fix-dr-mean-centering
Open

[MRG] Fix mean centering in ot.dr.fda and ot.dr.wda#840
deeb01 wants to merge 2 commits into
PythonOT:masterfrom
deeb01:fix-dr-mean-centering

Conversation

@deeb01

@deeb01 deeb01 commented Aug 25, 2026

Copy link
Copy Markdown

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.
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.

1 participant