CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
test_pls.py678 linesDownload Raw Back to tests
1import warnings
2
3import numpy as np
4import pytest
5from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_equal
6
7from sklearn.cross_decomposition import CCA, PLSSVD, PLSCanonical, PLSRegression
8from sklearn.cross_decomposition._pls import (
9    _center_scale_xy,
10    _get_first_singular_vectors_power_method,
11    _get_first_singular_vectors_svd,
12    _svd_flip_1d,
13)
14from sklearn.datasets import load_linnerud, make_regression
15from sklearn.ensemble import VotingRegressor
16from sklearn.exceptions import ConvergenceWarning
17from sklearn.linear_model import LinearRegression
18from sklearn.utils import check_random_state
19from sklearn.utils.extmath import svd_flip
20
21
22def assert_matrix_orthogonal(M):
23    K = np.dot(M.T, M)
24    assert_array_almost_equal(K, np.diag(np.diag(K)))
25
26
27def test_pls_canonical_basics():
28    # Basic checks for PLSCanonical
29    d = load_linnerud()
30    X = d.data
31    y = d.target
32
33    pls = PLSCanonical(n_components=X.shape[1])
34    pls.fit(X, y)
35
36    assert_matrix_orthogonal(pls.x_weights_)
37    assert_matrix_orthogonal(pls.y_weights_)
38    assert_matrix_orthogonal(pls._x_scores)
39    assert_matrix_orthogonal(pls._y_scores)
40
41    # Check X = TP' and y = UQ'
42    T = pls._x_scores
43    P = pls.x_loadings_
44    U = pls._y_scores
45    Q = pls.y_loadings_
46    # Need to scale first
47    Xc, yc, x_mean, y_mean, x_std, y_std = _center_scale_xy(
48        X.copy(), y.copy(), scale=True
49    )
50    assert_array_almost_equal(Xc, np.dot(T, P.T))
51    assert_array_almost_equal(yc, np.dot(U, Q.T))
52
53    # Check that rotations on training data lead to scores
54    Xt = pls.transform(X)
55    assert_array_almost_equal(Xt, pls._x_scores)
56    Xt, yt = pls.transform(X, y)
57    assert_array_almost_equal(Xt, pls._x_scores)
58    assert_array_almost_equal(yt, pls._y_scores)
59
60    # Check that inverse_transform works
61    X_back = pls.inverse_transform(Xt)
62    assert_array_almost_equal(X_back, X)
63    _, y_back = pls.inverse_transform(Xt, yt)
64    assert_array_almost_equal(y_back, y)
65
66
67def test_sanity_check_pls_regression():
68    # Sanity check for PLSRegression
69    # The results were checked against the R-packages plspm, misOmics and pls
70
71    d = load_linnerud()
72    X = d.data
73    y = d.target
74
75    pls = PLSRegression(n_components=X.shape[1])
76    X_trans, _ = pls.fit_transform(X, y)
77
78    # FIXME: one would expect y_trans == pls.y_scores_ but this is not
79    # the case.
80    # xref: https://github.com/scikit-learn/scikit-learn/issues/22420
81    assert_allclose(X_trans, pls.x_scores_)
82
83    expected_x_weights = np.array(
84        [
85            [-0.61330704, -0.00443647, 0.78983213],
86            [-0.74697144, -0.32172099, -0.58183269],
87            [-0.25668686, 0.94682413, -0.19399983],
88        ]
89    )
90
91    expected_x_loadings = np.array(
92        [
93            [-0.61470416, -0.24574278, 0.78983213],
94            [-0.65625755, -0.14396183, -0.58183269],
95            [-0.51733059, 1.00609417, -0.19399983],
96        ]
97    )
98
99    expected_y_weights = np.array(
100        [
101            [+0.32456184, 0.29892183, 0.20316322],
102            [+0.42439636, 0.61970543, 0.19320542],
103            [-0.13143144, -0.26348971, -0.17092916],
104        ]
105    )
106
107    expected_y_loadings = np.array(
108        [
109            [+0.32456184, 0.29892183, 0.20316322],
110            [+0.42439636, 0.61970543, 0.19320542],
111            [-0.13143144, -0.26348971, -0.17092916],
112        ]
113    )
114
115    assert_array_almost_equal(np.abs(pls.x_loadings_), np.abs(expected_x_loadings))
116    assert_array_almost_equal(np.abs(pls.x_weights_), np.abs(expected_x_weights))
117    assert_array_almost_equal(np.abs(pls.y_loadings_), np.abs(expected_y_loadings))
118    assert_array_almost_equal(np.abs(pls.y_weights_), np.abs(expected_y_weights))
119
120    # The R / Python difference in the signs should be consistent across
121    # loadings, weights, etc.
122    x_loadings_sign_flip = np.sign(pls.x_loadings_ / expected_x_loadings)
123    x_weights_sign_flip = np.sign(pls.x_weights_ / expected_x_weights)
124    y_weights_sign_flip = np.sign(pls.y_weights_ / expected_y_weights)
125    y_loadings_sign_flip = np.sign(pls.y_loadings_ / expected_y_loadings)
126    assert_array_almost_equal(x_loadings_sign_flip, x_weights_sign_flip)
127    assert_array_almost_equal(y_loadings_sign_flip, y_weights_sign_flip)
128
129
130def test_sanity_check_pls_regression_constant_column_y():
131    # Check behavior when the first column of y is constant
132    # The results are checked against a modified version of plsreg2
133    # from the R-package plsdepot
134    d = load_linnerud()
135    X = d.data
136    y = d.target
137    y[:, 0] = 1
138    pls = PLSRegression(n_components=X.shape[1])
139    pls.fit(X, y)
140
141    expected_x_weights = np.array(
142        [
143            [-0.6273573, 0.007081799, 0.7786994],
144            [-0.7493417, -0.277612681, -0.6011807],
145            [-0.2119194, 0.960666981, -0.1794690],
146        ]
147    )
148
149    expected_x_loadings = np.array(
150        [
151            [-0.6273512, -0.22464538, 0.7786994],
152            [-0.6643156, -0.09871193, -0.6011807],
153            [-0.5125877, 1.01407380, -0.1794690],
154        ]
155    )
156
157    expected_y_loadings = np.array(
158        [
159            [0.0000000, 0.0000000, 0.0000000],
160            [0.4357300, 0.5828479, 0.2174802],
161            [-0.1353739, -0.2486423, -0.1810386],
162        ]
163    )
164
165    assert_array_almost_equal(np.abs(expected_x_weights), np.abs(pls.x_weights_))
166    assert_array_almost_equal(np.abs(expected_x_loadings), np.abs(pls.x_loadings_))
167    # For the PLSRegression with default parameters, y_loadings == y_weights
168    assert_array_almost_equal(np.abs(pls.y_loadings_), np.abs(expected_y_loadings))
169    assert_array_almost_equal(np.abs(pls.y_weights_), np.abs(expected_y_loadings))
170
171    x_loadings_sign_flip = np.sign(expected_x_loadings / pls.x_loadings_)
172    x_weights_sign_flip = np.sign(expected_x_weights / pls.x_weights_)
173    # we ignore the first full-zeros row for y
174    y_loadings_sign_flip = np.sign(expected_y_loadings[1:] / pls.y_loadings_[1:])
175
176    assert_array_equal(x_loadings_sign_flip, x_weights_sign_flip)
177    assert_array_equal(x_loadings_sign_flip[1:], y_loadings_sign_flip)
178
179
180def test_sanity_check_pls_canonical():
181    # Sanity check for PLSCanonical
182    # The results were checked against the R-package plspm
183
184    d = load_linnerud()
185    X = d.data
186    y = d.target
187
188    pls = PLSCanonical(n_components=X.shape[1])
189    pls.fit(X, y)
190
191    expected_x_weights = np.array(
192        [
193            [-0.61330704, 0.25616119, -0.74715187],
194            [-0.74697144, 0.11930791, 0.65406368],
195            [-0.25668686, -0.95924297, -0.11817271],
196        ]
197    )
198
199    expected_x_rotations = np.array(
200        [
201            [-0.61330704, 0.41591889, -0.62297525],
202            [-0.74697144, 0.31388326, 0.77368233],
203            [-0.25668686, -0.89237972, -0.24121788],
204        ]
205    )
206
207    expected_y_weights = np.array(
208        [
209            [+0.58989127, 0.7890047, 0.1717553],
210            [+0.77134053, -0.61351791, 0.16920272],
211            [-0.23887670, -0.03267062, 0.97050016],
212        ]
213    )
214
215    expected_y_rotations = np.array(
216        [
217            [+0.58989127, 0.7168115, 0.30665872],
218            [+0.77134053, -0.70791757, 0.19786539],
219            [-0.23887670, -0.00343595, 0.94162826],
220        ]
221    )
222
223    assert_array_almost_equal(np.abs(pls.x_rotations_), np.abs(expected_x_rotations))
224    assert_array_almost_equal(np.abs(pls.x_weights_), np.abs(expected_x_weights))
225    assert_array_almost_equal(np.abs(pls.y_rotations_), np.abs(expected_y_rotations))
226    assert_array_almost_equal(np.abs(pls.y_weights_), np.abs(expected_y_weights))
227
228    x_rotations_sign_flip = np.sign(pls.x_rotations_ / expected_x_rotations)
229    x_weights_sign_flip = np.sign(pls.x_weights_ / expected_x_weights)
230    y_rotations_sign_flip = np.sign(pls.y_rotations_ / expected_y_rotations)
231    y_weights_sign_flip = np.sign(pls.y_weights_ / expected_y_weights)
232    assert_array_almost_equal(x_rotations_sign_flip, x_weights_sign_flip)
233    assert_array_almost_equal(y_rotations_sign_flip, y_weights_sign_flip)
234
235    assert_matrix_orthogonal(pls.x_weights_)
236    assert_matrix_orthogonal(pls.y_weights_)
237
238    assert_matrix_orthogonal(pls._x_scores)
239    assert_matrix_orthogonal(pls._y_scores)
240
241
242def test_sanity_check_pls_canonical_random():
243    # Sanity check for PLSCanonical on random data
244    # The results were checked against the R-package plspm
245    n = 500
246    p_noise = 10
247    q_noise = 5
248    # 2 latents vars:
249    rng = check_random_state(11)
250    l1 = rng.normal(size=n)
251    l2 = rng.normal(size=n)
252    latents = np.array([l1, l1, l2, l2]).T
253    X = latents + rng.normal(size=4 * n).reshape((n, 4))
254    y = latents + rng.normal(size=4 * n).reshape((n, 4))
255    X = np.concatenate((X, rng.normal(size=p_noise * n).reshape(n, p_noise)), axis=1)
256    y = np.concatenate((y, rng.normal(size=q_noise * n).reshape(n, q_noise)), axis=1)
257
258    pls = PLSCanonical(n_components=3)
259    pls.fit(X, y)
260
261    expected_x_weights = np.array(
262        [
263            [0.65803719, 0.19197924, 0.21769083],
264            [0.7009113, 0.13303969, -0.15376699],
265            [0.13528197, -0.68636408, 0.13856546],
266            [0.16854574, -0.66788088, -0.12485304],
267            [-0.03232333, -0.04189855, 0.40690153],
268            [0.1148816, -0.09643158, 0.1613305],
269            [0.04792138, -0.02384992, 0.17175319],
270            [-0.06781, -0.01666137, -0.18556747],
271            [-0.00266945, -0.00160224, 0.11893098],
272            [-0.00849528, -0.07706095, 0.1570547],
273            [-0.00949471, -0.02964127, 0.34657036],
274            [-0.03572177, 0.0945091, 0.3414855],
275            [0.05584937, -0.02028961, -0.57682568],
276            [0.05744254, -0.01482333, -0.17431274],
277        ]
278    )
279
280    expected_x_loadings = np.array(
281        [
282            [0.65649254, 0.1847647, 0.15270699],
283            [0.67554234, 0.15237508, -0.09182247],
284            [0.19219925, -0.67750975, 0.08673128],
285            [0.2133631, -0.67034809, -0.08835483],
286            [-0.03178912, -0.06668336, 0.43395268],
287            [0.15684588, -0.13350241, 0.20578984],
288            [0.03337736, -0.03807306, 0.09871553],
289            [-0.06199844, 0.01559854, -0.1881785],
290            [0.00406146, -0.00587025, 0.16413253],
291            [-0.00374239, -0.05848466, 0.19140336],
292            [0.00139214, -0.01033161, 0.32239136],
293            [-0.05292828, 0.0953533, 0.31916881],
294            [0.04031924, -0.01961045, -0.65174036],
295            [0.06172484, -0.06597366, -0.1244497],
296        ]
297    )
298
299    expected_y_weights = np.array(
300        [
301            [0.66101097, 0.18672553, 0.22826092],
302            [0.69347861, 0.18463471, -0.23995597],
303            [0.14462724, -0.66504085, 0.17082434],
304            [0.22247955, -0.6932605, -0.09832993],
305            [0.07035859, 0.00714283, 0.67810124],
306            [0.07765351, -0.0105204, -0.44108074],
307            [-0.00917056, 0.04322147, 0.10062478],
308            [-0.01909512, 0.06182718, 0.28830475],
309            [0.01756709, 0.04797666, 0.32225745],
310        ]
311    )
312
313    expected_y_loadings = np.array(
314        [
315            [0.68568625, 0.1674376, 0.0969508],
316            [0.68782064, 0.20375837, -0.1164448],
317            [0.11712173, -0.68046903, 0.12001505],
318            [0.17860457, -0.6798319, -0.05089681],
319            [0.06265739, -0.0277703, 0.74729584],
320            [0.0914178, 0.00403751, -0.5135078],
321            [-0.02196918, -0.01377169, 0.09564505],
322            [-0.03288952, 0.09039729, 0.31858973],
323            [0.04287624, 0.05254676, 0.27836841],
324        ]
325    )
326
327    assert_array_almost_equal(np.abs(pls.x_loadings_), np.abs(expected_x_loadings))
328    assert_array_almost_equal(np.abs(pls.x_weights_), np.abs(expected_x_weights))
329    assert_array_almost_equal(np.abs(pls.y_loadings_), np.abs(expected_y_loadings))
330    assert_array_almost_equal(np.abs(pls.y_weights_), np.abs(expected_y_weights))
331
332    x_loadings_sign_flip = np.sign(pls.x_loadings_ / expected_x_loadings)
333    x_weights_sign_flip = np.sign(pls.x_weights_ / expected_x_weights)
334    y_weights_sign_flip = np.sign(pls.y_weights_ / expected_y_weights)
335    y_loadings_sign_flip = np.sign(pls.y_loadings_ / expected_y_loadings)
336    assert_array_almost_equal(x_loadings_sign_flip, x_weights_sign_flip)
337    assert_array_almost_equal(y_loadings_sign_flip, y_weights_sign_flip)
338
339    assert_matrix_orthogonal(pls.x_weights_)
340    assert_matrix_orthogonal(pls.y_weights_)
341
342    assert_matrix_orthogonal(pls._x_scores)
343    assert_matrix_orthogonal(pls._y_scores)
344
345
346def test_convergence_fail():
347    # Make sure ConvergenceWarning is raised if max_iter is too small
348    d = load_linnerud()
349    X = d.data
350    y = d.target
351    pls_nipals = PLSCanonical(n_components=X.shape[1], max_iter=2)
352    with pytest.warns(ConvergenceWarning):
353        pls_nipals.fit(X, y)
354
355
356@pytest.mark.parametrize("Est", (PLSSVD, PLSRegression, PLSCanonical))
357def test_attibutes_shapes(Est):
358    # Make sure attributes are of the correct shape depending on n_components
359    d = load_linnerud()
360    X = d.data
361    y = d.target
362    n_components = 2
363    pls = Est(n_components=n_components)
364    pls.fit(X, y)
365    assert all(
366        attr.shape[1] == n_components for attr in (pls.x_weights_, pls.y_weights_)
367    )
368
369
370@pytest.mark.parametrize("Est", (PLSRegression, PLSCanonical, CCA))
371def test_univariate_equivalence(Est):
372    # Ensure 2D y with 1 column is equivalent to 1D y
373    d = load_linnerud()
374    X = d.data
375    y = d.target
376
377    est = Est(n_components=1)
378    one_d_coeff = est.fit(X, y[:, 0]).coef_
379    two_d_coeff = est.fit(X, y[:, :1]).coef_
380
381    assert one_d_coeff.shape == two_d_coeff.shape
382    assert_array_almost_equal(one_d_coeff, two_d_coeff)
383
384
385@pytest.mark.parametrize("Est", (PLSRegression, PLSCanonical, CCA, PLSSVD))
386def test_copy(Est):
387    # check that the "copy" keyword works
388    d = load_linnerud()
389    X = d.data
390    y = d.target
391    X_orig = X.copy()
392
393    # copy=True won't modify inplace
394    pls = Est(copy=True).fit(X, y)
395    assert_array_equal(X, X_orig)
396
397    # copy=False will modify inplace
398    with pytest.raises(AssertionError):
399        Est(copy=False).fit(X, y)
400        assert_array_almost_equal(X, X_orig)
401
402    if Est is PLSSVD:
403        return  # PLSSVD does not support copy param in predict or transform
404
405    X_orig = X.copy()
406    with pytest.raises(AssertionError):
407        pls.transform(X, y, copy=False)
408        assert_array_almost_equal(X, X_orig)
409
410    X_orig = X.copy()
411    with pytest.raises(AssertionError):
412        pls.predict(X, copy=False)
413        assert_array_almost_equal(X, X_orig)
414
415    # Make sure copy=True gives same transform and predictions as predict=False
416    assert_array_almost_equal(
417        pls.transform(X, y, copy=True), pls.transform(X.copy(), y.copy(), copy=False)
418    )
419    assert_array_almost_equal(
420        pls.predict(X, copy=True), pls.predict(X.copy(), copy=False)
421    )
422
423
424def _generate_test_scale_and_stability_datasets():
425    """Generate dataset for test_scale_and_stability"""
426    # dataset for non-regression 7818
427    rng = np.random.RandomState(0)
428    n_samples = 1000
429    n_targets = 5
430    n_features = 10
431    Q = rng.randn(n_targets, n_features)
432    y = rng.randn(n_samples, n_targets)
433    X = np.dot(y, Q) + 2 * rng.randn(n_samples, n_features) + 1
434    X *= 1000
435    yield X, y
436
437    # Data set where one of the features is constraint
438    X, y = load_linnerud(return_X_y=True)
439    # causes X[:, -1].std() to be zero
440    X[:, -1] = 1.0
441    yield X, y
442
443    X = np.array([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [2.0, 2.0, 2.0], [3.0, 5.0, 4.0]])
444    y = np.array([[0.1, -0.2], [0.9, 1.1], [6.2, 5.9], [11.9, 12.3]])
445    yield X, y
446
447    # Seeds that provide a non-regression test for #18746, where CCA fails
448    seeds = [530, 741]
449    for seed in seeds:
450        rng = np.random.RandomState(seed)
451        X = rng.randn(4, 3)
452        y = rng.randn(4, 2)
453        yield X, y
454
455
456@pytest.mark.parametrize("Est", (CCA, PLSCanonical, PLSRegression, PLSSVD))
457@pytest.mark.parametrize("X, y", _generate_test_scale_and_stability_datasets())
458def test_scale_and_stability(Est, X, y):
459    """scale=True is equivalent to scale=False on centered/scaled data
460    This allows to check numerical stability over platforms as well"""
461
462    X_s, y_s, *_ = _center_scale_xy(X, y)
463
464    X_score, y_score = Est(scale=True).fit_transform(X, y)
465    X_s_score, y_s_score = Est(scale=False).fit_transform(X_s, y_s)
466
467    assert_allclose(X_s_score, X_score, atol=1e-4)
468    assert_allclose(y_s_score, y_score, atol=1e-4)
469
470
471@pytest.mark.parametrize("Estimator", (PLSSVD, PLSRegression, PLSCanonical, CCA))
472def test_n_components_upper_bounds(Estimator):
473    """Check the validation of `n_components` upper bounds for `PLS` regressors."""
474    rng = np.random.RandomState(0)
475    X = rng.randn(10, 5)
476    y = rng.randn(10, 3)
477    est = Estimator(n_components=10)
478    err_msg = "`n_components` upper bound is .*. Got 10 instead. Reduce `n_components`."
479    with pytest.raises(ValueError, match=err_msg):
480        est.fit(X, y)
481
482
483def test_n_components_upper_PLSRegression():
484    """Check the validation of `n_components` upper bounds for PLSRegression."""
485    rng = np.random.RandomState(0)
486    X = rng.randn(20, 64)
487    y = rng.randn(20, 3)
488    est = PLSRegression(n_components=30)
489    err_msg = "`n_components` upper bound is 20. Got 30 instead. Reduce `n_components`."
490    with pytest.raises(ValueError, match=err_msg):
491        est.fit(X, y)
492
493
494@pytest.mark.parametrize("n_samples, n_features", [(100, 10), (100, 200)])
495def test_singular_value_helpers(n_samples, n_features, global_random_seed):
496    # Make sure SVD and power method give approximately the same results
497    X, y = make_regression(
498        n_samples, n_features, n_targets=5, random_state=global_random_seed
499    )
500    u1, v1, _ = _get_first_singular_vectors_power_method(X, y, norm_y_weights=True)
501    u2, v2 = _get_first_singular_vectors_svd(X, y)
502
503    _svd_flip_1d(u1, v1)
504    _svd_flip_1d(u2, v2)
505
506    rtol = 1e-3
507    # Setting atol because some coordinates are very close to zero
508    assert_allclose(u1, u2, atol=u2.max() * rtol)
509    assert_allclose(v1, v2, atol=v2.max() * rtol)
510
511
512def test_one_component_equivalence(global_random_seed):
513    # PLSSVD, PLSRegression and PLSCanonical should all be equivalent when
514    # n_components is 1
515    X, y = make_regression(100, 10, n_targets=5, random_state=global_random_seed)
516    svd = PLSSVD(n_components=1).fit(X, y).transform(X)
517    reg = PLSRegression(n_components=1).fit(X, y).transform(X)
518    canonical = PLSCanonical(n_components=1).fit(X, y).transform(X)
519
520    rtol = 1e-3
521    # Setting atol because some entries are very close to zero
522    assert_allclose(svd, reg, atol=reg.max() * rtol)
523    assert_allclose(svd, canonical, atol=canonical.max() * rtol)
524
525
526def test_svd_flip_1d():
527    # Make sure svd_flip_1d is equivalent to svd_flip
528    u = np.array([1, -4, 2])
529    v = np.array([1, 2, 3])
530
531    u_expected, v_expected = svd_flip(u.reshape(-1, 1), v.reshape(1, -1))
532    _svd_flip_1d(u, v)  # inplace
533
534    assert_allclose(u, u_expected.ravel())
535    assert_allclose(u, [-1, 4, -2])
536
537    assert_allclose(v, v_expected.ravel())
538    assert_allclose(v, [-1, -2, -3])
539
540
541def test_loadings_converges(global_random_seed):
542    """Test that CCA converges. Non-regression test for #19549."""
543    X, y = make_regression(
544        n_samples=200, n_features=20, n_targets=20, random_state=global_random_seed
545    )
546
547    cca = CCA(n_components=10, max_iter=500)
548
549    with warnings.catch_warnings():
550        warnings.simplefilter("error", ConvergenceWarning)
551
552        cca.fit(X, y)
553
554    # Loadings converges to reasonable values
555    assert np.all(np.abs(cca.x_loadings_) < 1)
556
557
558def test_pls_constant_y():
559    """Checks warning when y is constant. Non-regression test for #19831"""
560    rng = np.random.RandomState(42)
561    x = rng.rand(100, 3)
562    y = np.zeros(100)
563
564    pls = PLSRegression()
565
566    msg = "y residual is constant at iteration"
567    with pytest.warns(UserWarning, match=msg):
568        pls.fit(x, y)
569
570    assert_allclose(pls.x_rotations_, 0)
571
572
573@pytest.mark.parametrize("PLSEstimator", [PLSRegression, PLSCanonical, CCA])
574def test_pls_coef_shape(PLSEstimator):
575    """Check the shape of `coef_` attribute.
576
577    Non-regression test for:
578    https://github.com/scikit-learn/scikit-learn/issues/12410
579    """
580    d = load_linnerud()
581    X = d.data
582    y = d.target
583
584    pls = PLSEstimator(copy=True).fit(X, y)
585
586    n_targets, n_features = y.shape[1], X.shape[1]
587    assert pls.coef_.shape == (n_targets, n_features)
588
589
590@pytest.mark.parametrize("scale", [True, False])
591@pytest.mark.parametrize("PLSEstimator", [PLSRegression, PLSCanonical, CCA])
592def test_pls_prediction(PLSEstimator, scale):
593    """Check the behaviour of the prediction function."""
594    d = load_linnerud()
595    X = d.data
596    y = d.target
597
598    pls = PLSEstimator(copy=True, scale=scale).fit(X, y)
599    y_pred = pls.predict(X, copy=True)
600
601    y_mean = y.mean(axis=0)
602    X_trans = X - X.mean(axis=0)
603
604    assert_allclose(pls.intercept_, y_mean)
605    assert_allclose(y_pred, X_trans @ pls.coef_.T + pls.intercept_)
606
607
608@pytest.mark.parametrize("Klass", [CCA, PLSSVD, PLSRegression, PLSCanonical])
609def test_pls_feature_names_out(Klass):
610    """Check `get_feature_names_out` cross_decomposition module."""
611    X, y = load_linnerud(return_X_y=True)
612
613    est = Klass().fit(X, y)
614    names_out = est.get_feature_names_out()
615
616    class_name_lower = Klass.__name__.lower()
617    expected_names_out = np.array(
618        [f"{class_name_lower}{i}" for i in range(est.x_weights_.shape[1])],
619        dtype=object,
620    )
621    assert_array_equal(names_out, expected_names_out)
622
623
624@pytest.mark.parametrize("Klass", [CCA, PLSSVD, PLSRegression, PLSCanonical])
625def test_pls_set_output(Klass):
626    """Check `set_output` in cross_decomposition module."""
627    pd = pytest.importorskip("pandas")
628    X, y = load_linnerud(return_X_y=True, as_frame=True)
629
630    est = Klass().set_output(transform="pandas").fit(X, y)
631    X_trans, y_trans = est.transform(X, y)
632    assert isinstance(y_trans, np.ndarray)
633    assert isinstance(X_trans, pd.DataFrame)
634    assert_array_equal(X_trans.columns, est.get_feature_names_out())
635
636
637def test_pls_regression_fit_1d_y():
638    """Check that when fitting with 1d `y`, prediction should also be 1d.
639
640    Non-regression test for Issue #26549.
641    """
642    X = np.array([[1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36]])
643    y = np.array([2, 6, 12, 20, 30, 42])
644    expected = y.copy()
645
646    plsr = PLSRegression().fit(X, y)
647    y_pred = plsr.predict(X)
648    assert y_pred.shape == expected.shape
649
650    # Check that it works in VotingRegressor
651    lr = LinearRegression().fit(X, y)
652    vr = VotingRegressor([("lr", lr), ("plsr", plsr)])
653    y_pred = vr.fit(X, y).predict(X)
654    assert y_pred.shape == expected.shape
655    assert_allclose(y_pred, expected)
656
657
658def test_pls_regression_scaling_coef():
659    """Check that when using `scale=True`, the coefficients are using the std. dev. from
660    both `X` and `y`.
661
662    Non-regression test for:
663    https://github.com/scikit-learn/scikit-learn/issues/27964
664    """
665    # handcrafted data where we can predict y from X with an additional scaling factor
666    rng = np.random.RandomState(0)
667    coef = rng.uniform(size=(3, 5))
668    X = rng.normal(scale=10, size=(30, 5))  # add a std of 10
669    y = X @ coef.T
670
671    # we need to make sure that the dimension of the latent space is large enough to
672    # perfectly predict `y` from `X` (no information loss)
673    pls = PLSRegression(n_components=5, scale=True).fit(X, y)
674    assert_allclose(pls.coef_, coef)
675
676    # we therefore should be able to predict `y` from `X`
677    assert_allclose(pls.predict(X), y)
678