Skip to content

Commit

Permalink
feat: normalized option in inverse_transform (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
slevang authored Nov 20, 2023
2 parents 8d06153 + a9d6bdf commit 4ad2791
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 48 deletions.
4 changes: 1 addition & 3 deletions tests/models/test_eof.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,13 +510,11 @@ def test_save_load(dim, mock_data_array, tmp_path, engine):

# Test that the recreated model can be used to transform new data
assert np.allclose(
original.scores(), loaded.transform(mock_data_array), rtol=1e-3, atol=1e-3
original.transform(mock_data_array), loaded.transform(mock_data_array)
)

# The loaded model should also be able to inverse_transform new data
assert np.allclose(
original.inverse_transform(original.scores()),
loaded.inverse_transform(loaded.scores()),
rtol=1e-3,
atol=1e-3,
)
3 changes: 1 addition & 2 deletions tests/models/test_eof_rotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,11 @@ def test_save_load(dim, mock_data_array, tmp_path, engine):

# Test that the recreated model can be used to transform new data
assert np.allclose(
original.scores(), loaded.transform(mock_data_array), rtol=1e-3, atol=1e-3
original.transform(mock_data_array), loaded.transform(mock_data_array)
)

# The loaded model should also be able to inverse_transform new data
assert np.allclose(
original.inverse_transform(original.scores()),
loaded.inverse_transform(loaded.scores()),
rtol=1e-2,
)
6 changes: 1 addition & 5 deletions tests/models/test_mca.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,16 +423,12 @@ def test_save_load(dim, mock_data_array, tmp_path, engine):

# Test that the recreated model can be used to transform new data
assert np.allclose(
original.scores(),
original.transform(mock_data_array, mock_data_array),
loaded.transform(mock_data_array, mock_data_array),
rtol=1e-3,
atol=1e-3,
)

# The loaded model should also be able to inverse_transform new data
assert np.allclose(
original.inverse_transform(*original.scores()),
loaded.inverse_transform(*loaded.scores()),
rtol=1e-3,
atol=1e-3,
)
8 changes: 2 additions & 6 deletions tests/models/test_mca_rotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,16 +295,12 @@ def test_save_load(dim, mock_data_array, tmp_path, engine):

# Test that the recreated model can be used to transform new data
assert np.allclose(
original.scores(),
loaded.transform(data1=mock_data_array, data2=mock_data_array),
rtol=1e-3,
atol=1e-3,
original.transform(mock_data_array, mock_data_array),
loaded.transform(mock_data_array, mock_data_array),
)

# The loaded model should also be able to inverse_transform new data
assert np.allclose(
original.inverse_transform(*original.scores()),
loaded.inverse_transform(*loaded.scores()),
rtol=1e-3,
atol=1e-3,
)
56 changes: 32 additions & 24 deletions tests/models/test_orthogonality.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,8 @@ def test_crmca_scores(dim, use_coslat, power, squared_loadings, mock_data_array)
(("lon", "lat"), False),
],
)
def test_eof_transform(dim, use_coslat, mock_data_array):
@pytest.mark.parametrize("normalized", [True, False])
def test_eof_transform(dim, use_coslat, mock_data_array, normalized):
"""Transforming the original data results in the model scores"""
model = EOF(
n_modes=5,
Expand All @@ -464,8 +465,8 @@ def test_eof_transform(dim, use_coslat, mock_data_array):
random_state=5,
)
model.fit(mock_data_array, dim=dim)
scores = model.scores()
pseudo_scores = model.transform(mock_data_array)
scores = model.scores(normalized=normalized)
pseudo_scores = model.transform(mock_data_array, normalized=normalized)
assert np.allclose(
scores, pseudo_scores, atol=1e-4
), "Transformed data does not match the scores"
Expand All @@ -480,13 +481,14 @@ def test_eof_transform(dim, use_coslat, mock_data_array):
(("lon", "lat"), False),
],
)
def test_ceof_transform(dim, use_coslat, mock_data_array):
@pytest.mark.parametrize("normalized", [True, False])
def test_ceof_transform(dim, use_coslat, mock_data_array, normalized):
"""Not implemented yet"""
model = ComplexEOF(n_modes=5, standardize=True, use_coslat=use_coslat)
model.fit(mock_data_array, dim=dim)
scores = model.scores()
scores = model.scores(normalized=normalized)
with pytest.raises(NotImplementedError):
pseudo_scores = model.transform(mock_data_array)
pseudo_scores = model.transform(mock_data_array, normalized=normalized)


# Rotated EOF
Expand All @@ -501,14 +503,15 @@ def test_ceof_transform(dim, use_coslat, mock_data_array):
(("lon", "lat"), False, 2),
],
)
def test_reof_transform(dim, use_coslat, power, mock_data_array):
@pytest.mark.parametrize("normalized", [True, False])
def test_reof_transform(dim, use_coslat, power, mock_data_array, normalized):
"""Transforming the original data results in the model scores"""
model = EOF(n_modes=5, standardize=True, use_coslat=use_coslat, random_state=5)
model.fit(mock_data_array, dim=dim)
rot = EOFRotator(n_modes=5, power=power)
rot.fit(model)
scores = rot.scores()
pseudo_scores = rot.transform(mock_data_array)
scores = rot.scores(normalized=normalized)
pseudo_scores = rot.transform(mock_data_array, normalized=normalized)
np.testing.assert_allclose(
scores,
pseudo_scores,
Expand All @@ -529,15 +532,16 @@ def test_reof_transform(dim, use_coslat, power, mock_data_array):
(("lon", "lat"), False, 2),
],
)
def test_creof_transform(dim, use_coslat, power, mock_data_array):
@pytest.mark.parametrize("normalized", [True, False])
def test_creof_transform(dim, use_coslat, power, mock_data_array, normalized):
"""not implemented yet"""
model = ComplexEOF(n_modes=5, standardize=True, use_coslat=use_coslat)
model.fit(mock_data_array, dim=dim)
rot = ComplexEOFRotator(n_modes=5, power=power)
rot.fit(model)
scores = rot.scores()
scores = rot.scores(normalized=normalized)
with pytest.raises(NotImplementedError):
pseudo_scores = rot.transform(mock_data_array)
pseudo_scores = rot.transform(mock_data_array, normalized=normalized)


# MCA
Expand Down Expand Up @@ -687,13 +691,14 @@ def r2_score(x, y, dim=None):
(("lon", "lat"), False),
],
)
def test_eof_inverse_transform(dim, use_coslat, mock_data_array):
@pytest.mark.parametrize("normalized", [True, False])
def test_eof_inverse_transform(dim, use_coslat, mock_data_array, normalized):
"""Inverse transform produces an approximate reconstruction of the original data"""
data = mock_data_array
model = EOF(n_modes=19, standardize=True, use_coslat=use_coslat)
model.fit(data, dim=dim)
scores = model.data["scores"]
data_rec = model.inverse_transform(scores)
scores = model.scores(normalized=normalized)
data_rec = model.inverse_transform(scores, normalized=normalized)
r2 = r2_score(data, data_rec, dim=dim)
r2 = r2.mean()
# Choose a threshold of 0.95; a bit arbitrary
Expand All @@ -709,13 +714,14 @@ def test_eof_inverse_transform(dim, use_coslat, mock_data_array):
(("lon", "lat"), False),
],
)
def test_ceof_inverse_transform(dim, use_coslat, mock_data_array):
@pytest.mark.parametrize("normalized", [True, False])
def test_ceof_inverse_transform(dim, use_coslat, mock_data_array, normalized):
"""Inverse transform produces an approximate reconstruction of the original data"""
data = mock_data_array
model = ComplexEOF(n_modes=19, standardize=True, use_coslat=use_coslat)
model.fit(data, dim=dim)
scores = model.data["scores"]
data_rec = model.inverse_transform(scores).real
scores = model.scores(normalized=normalized)
data_rec = model.inverse_transform(scores, normalized=normalized).real
r2 = r2_score(data, data_rec, dim=dim)
r2 = r2.mean()
# Choose a threshold of 0.95; a bit arbitrary
Expand All @@ -734,15 +740,16 @@ def test_ceof_inverse_transform(dim, use_coslat, mock_data_array):
(("lon", "lat"), False, 2),
],
)
def test_reof_inverse_transform(dim, use_coslat, power, mock_data_array):
@pytest.mark.parametrize("normalized", [True, False])
def test_reof_inverse_transform(dim, use_coslat, power, mock_data_array, normalized):
"""Inverse transform produces an approximate reconstruction of the original data"""
data = mock_data_array
model = EOF(n_modes=19, standardize=True, use_coslat=use_coslat)
model.fit(data, dim=dim)
rot = EOFRotator(n_modes=19, power=power)
rot.fit(model)
scores = rot.data["scores"]
data_rec = rot.inverse_transform(scores).real
scores = rot.scores(normalized=normalized)
data_rec = rot.inverse_transform(scores, normalized=normalized).real
r2 = r2_score(data, data_rec, dim=dim)
r2 = r2.mean()
# Choose a threshold of 0.95; a bit arbitrary
Expand All @@ -763,15 +770,16 @@ def test_reof_inverse_transform(dim, use_coslat, power, mock_data_array):
(("lon", "lat"), False, 2),
],
)
def test_creof_inverse_transform(dim, use_coslat, power, mock_data_array):
@pytest.mark.parametrize("normalized", [True, False])
def test_creof_inverse_transform(dim, use_coslat, power, mock_data_array, normalized):
"""Inverse transform produces an approximate reconstruction of the original data"""
data = mock_data_array
model = ComplexEOF(n_modes=19, standardize=True, use_coslat=use_coslat)
model.fit(data, dim=dim)
rot = ComplexEOFRotator(n_modes=19, power=power)
rot.fit(model)
scores = rot.data["scores"]
data_rec = rot.inverse_transform(scores).real
scores = rot.scores(normalized=normalized)
data_rec = rot.inverse_transform(scores, normalized=normalized).real
r2 = r2_score(data, data_rec, dim=dim)
r2 = r2.mean()
# Choose a threshold of 0.95; a bit arbitrary
Expand Down
8 changes: 7 additions & 1 deletion xeofs/models/_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ def fit_transform(
"""
return self.fit(data, dim, weights).transform(data, **kwargs)

def inverse_transform(self, scores: DataObject) -> DataObject:
def inverse_transform(
self, scores: DataObject, normalized: bool = True
) -> DataObject:
"""Reconstruct the original data from transformed data.
Parameters
Expand All @@ -291,13 +293,17 @@ def inverse_transform(self, scores: DataObject) -> DataObject:
Transformed data to be reconstructed. This could be a subset
of the `scores` data of a fitted model, or unseen data. Must
have a 'mode' dimension.
normalized: bool, default=True
Whether the scores data have been normalized by the L2 norm.
Returns
-------
data: DataArray | Dataset | List[DataArray]
Reconstructed data.
"""
if normalized:
scores = scores * self.data["norms"]
data_reconstructed = self._inverse_transform_algorithm(scores)
return self.preprocessor.inverse_transform_data(data_reconstructed)

Expand Down
14 changes: 7 additions & 7 deletions xeofs/models/mca_rotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .mca import MCA, ComplexMCA
from ..preprocessing.preprocessor import Preprocessor
from ..utils.rotation import promax
from ..utils.data_types import DataArray
from ..utils.data_types import DataArray, DataObject
from ..utils.xarray_utils import argsort_dask, get_deterministic_sign_multiplier
from ..data_container import DataContainer
from .._version import __version__
Expand Down Expand Up @@ -319,7 +319,9 @@ def _sort_by_variance(self):
)
self.sorted = True

def transform(self, **kwargs) -> DataArray | List[DataArray]:
def transform(
self, data1: DataObject | None = None, data2: DataObject | None = None
) -> DataArray | List[DataArray]:
"""Project new "unseen" data onto the rotated singular vectors.
Parameters
Expand All @@ -336,7 +338,7 @@ def transform(self, **kwargs) -> DataArray | List[DataArray]:
"""
# raise error if no data is provided
if not kwargs:
if data1 is None and data2 is None:
raise ValueError("No data provided. Please provide data1 and/or data2.")

n_modes = self._params["n_modes"]
Expand All @@ -348,8 +350,7 @@ def transform(self, **kwargs) -> DataArray | List[DataArray]:

results = []

if "data1" in kwargs.keys():
data1 = kwargs["data1"]
if data1 is not None:
# Select the (non-rotated) singular vectors of the first dataset
comps1 = self.model.data["components1"].sel(mode=slice(1, n_modes))
norm1 = self.model.data["norm1"].sel(mode=slice(1, n_modes))
Expand All @@ -375,8 +376,7 @@ def transform(self, **kwargs) -> DataArray | List[DataArray]:

results.append(projections1)

if "data2" in kwargs.keys():
data2 = kwargs["data2"]
if data2 is not None:
# Select the (non-rotated) singular vectors of the second dataset
comps2 = self.model.data["components2"].sel(mode=slice(1, n_modes))
norm2 = self.model.data["norm2"].sel(mode=slice(1, n_modes))
Expand Down

0 comments on commit 4ad2791

Please sign in to comment.