[ENH]: Implement a NaN policy for onthefly.read_transform #429
4 changed files with 156 additions and 4 deletions
1
docs/changes/newsfragments/429.change
Normal file
1
docs/changes/newsfragments/429.change
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
:func:`.read_transform` has a new parameter ``nan_policy`` to handle NaN values when transforming feature data by `Fede Raimondo`_
|
||||||
1
docs/changes/newsfragments/429.enh
Normal file
1
docs/changes/newsfragments/429.enh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Implement a NaN policy for :func:`.read_transform` by `Fede Raimondo`_
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from ..typing import StorageLike
|
from ..typing import StorageLike
|
||||||
|
|
@ -19,6 +20,7 @@ def read_transform(
|
||||||
transform: str,
|
transform: str,
|
||||||
feature_name: Optional[str] = None,
|
feature_name: Optional[str] = None,
|
||||||
feature_md5: Optional[str] = None,
|
feature_md5: Optional[str] = None,
|
||||||
|
nan_policy: Optional[str] = "bypass",
|
||||||
transform_args: Optional[tuple] = None,
|
transform_args: Optional[tuple] = None,
|
||||||
transform_kw_args: Optional[dict] = None,
|
transform_kw_args: Optional[dict] = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
|
|
@ -35,6 +37,16 @@ def read_transform(
|
||||||
Name of the feature to read (default None).
|
Name of the feature to read (default None).
|
||||||
feature_md5 : str, optional
|
feature_md5 : str, optional
|
||||||
MD5 hash of the feature to read (default None).
|
MD5 hash of the feature to read (default None).
|
||||||
|
nan_policy : str, optional
|
||||||
|
The policy to handle NaN values (default "ignore").
|
||||||
|
Options are:
|
||||||
|
|
||||||
|
* "bypass": Do nothing and pass NaN values to the transform function.
|
||||||
|
* "drop_element": Drop (skip) elements with NaN values.
|
||||||
|
* "drop_rows": Drop (skip) rows with NaN values.
|
||||||
|
* "drop_columns": Drop (skip) columns with NaN values.
|
||||||
|
* "drop_symmetric": Drop (skip) symmetric pairs with NaN values.
|
||||||
|
|
||||||
transform_args : tuple, optional
|
transform_args : tuple, optional
|
||||||
The positional arguments for the callable of ``transform``
|
The positional arguments for the callable of ``transform``
|
||||||
(default None).
|
(default None).
|
||||||
|
|
@ -47,6 +59,18 @@ def read_transform(
|
||||||
pandas.DataFrame
|
pandas.DataFrame
|
||||||
The transformed feature as a dataframe.
|
The transformed feature as a dataframe.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If ``nan_policy`` is invalid or
|
||||||
|
if *package* is invalid.
|
||||||
|
RuntimeError
|
||||||
|
If *package* is ``bctpy`` and stored data kind is not ``matrix``.
|
||||||
|
ImportError
|
||||||
|
If ``bctpy`` cannot be imported.
|
||||||
|
AttributeError
|
||||||
|
If *function* to be invoked in invalid.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
This function has been only tested for:
|
This function has been only tested for:
|
||||||
|
|
@ -63,6 +87,18 @@ def read_transform(
|
||||||
transform_args = transform_args or ()
|
transform_args = transform_args or ()
|
||||||
transform_kw_args = transform_kw_args or {}
|
transform_kw_args = transform_kw_args or {}
|
||||||
|
|
||||||
|
if nan_policy not in [
|
||||||
|
"bypass",
|
||||||
|
"drop_element",
|
||||||
|
"drop_rows",
|
||||||
|
"drop_columns",
|
||||||
|
"drop_symmetric",
|
||||||
|
]:
|
||||||
|
raise_error(
|
||||||
|
f"Unknown nan_policy: {nan_policy}",
|
||||||
|
klass=ValueError,
|
||||||
|
)
|
||||||
|
|
||||||
# Read storage
|
# Read storage
|
||||||
stored_data = storage.read(
|
stored_data = storage.read(
|
||||||
feature_name=feature_name, feature_md5=feature_md5
|
feature_name=feature_name, feature_md5=feature_md5
|
||||||
|
|
@ -106,22 +142,52 @@ def read_transform(
|
||||||
except AttributeError as err:
|
except AttributeError as err:
|
||||||
raise_error(msg=str(err), klass=AttributeError)
|
raise_error(msg=str(err), klass=AttributeError)
|
||||||
|
|
||||||
# Apply function and store subject-wise
|
# Apply function and store element-wise
|
||||||
output_list = []
|
output_list = []
|
||||||
|
element_list = []
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Computing '{package}.{func_str}' for feature "
|
f"Computing '{package}.{func_str}' for feature "
|
||||||
f"{feature_name or feature_md5} ..."
|
f"{feature_name or feature_md5} ..."
|
||||||
)
|
)
|
||||||
for subject in range(stored_data["data"].shape[2]):
|
for i_element, element in enumerate(stored_data["element"]):
|
||||||
|
t_data = stored_data["data"][:, :, i_element]
|
||||||
|
has_nan = np.isnan(np.min(t_data))
|
||||||
|
if nan_policy == "drop_element" and has_nan:
|
||||||
|
logger.debug(
|
||||||
|
f"Skipping element {element} due to NaN values ..."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
elif nan_policy == "drop_rows" and has_nan:
|
||||||
|
logger.debug(
|
||||||
|
f"Skipping rows with NaN values in element {element} ..."
|
||||||
|
)
|
||||||
|
t_data = t_data[~np.isnan(t_data).any(axis=1)]
|
||||||
|
elif nan_policy == "drop_columns" and has_nan:
|
||||||
|
logger.debug(
|
||||||
|
f"Skipping columns with NaN values in element {element} "
|
||||||
|
"..."
|
||||||
|
)
|
||||||
|
t_data = t_data[:, ~np.isnan(t_data).any(axis=0)]
|
||||||
|
elif nan_policy == "drop_symmetric":
|
||||||
|
logger.debug(
|
||||||
|
f"Skipping pairs of rows/columns with NaN values in "
|
||||||
|
f"element {element}..."
|
||||||
|
)
|
||||||
|
good_rows = ~np.isnan(t_data).any(axis=1)
|
||||||
|
good_columns = ~np.isnan(t_data).any(axis=0)
|
||||||
|
good_idx = np.logical_and(good_rows, good_columns)
|
||||||
|
t_data = t_data[good_idx][:, good_idx]
|
||||||
|
|
||||||
output = func(
|
output = func(
|
||||||
stored_data["data"][:, :, subject],
|
t_data,
|
||||||
*transform_args,
|
*transform_args,
|
||||||
**transform_kw_args,
|
**transform_kw_args,
|
||||||
)
|
)
|
||||||
output_list.append(output)
|
output_list.append(output)
|
||||||
|
element_list.append(element)
|
||||||
|
|
||||||
# Create dataframe for index
|
# Create dataframe for index
|
||||||
idx_df = pd.DataFrame(data=stored_data["element"])
|
idx_df = pd.DataFrame(data=element_list)
|
||||||
# Create multiindex from dataframe
|
# Create multiindex from dataframe
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Generating pandas.MultiIndex for feature "
|
"Generating pandas.MultiIndex for feature "
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,36 @@ def matrix_storage(tmp_path: Path) -> HDF5FeatureStorage:
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def matrix_storage_with_nan(tmp_path: Path) -> HDF5FeatureStorage:
|
||||||
|
"""Return a HDF5FeatureStorage with matrix data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tmp_path : pathlib.Path
|
||||||
|
The path to the test directory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
storage = HDF5FeatureStorage(tmp_path / "matrix_store_nan.hdf5")
|
||||||
|
data = np.arange(36).reshape(3, 3, 4).astype(float)
|
||||||
|
data[1, 1, 2] = np.nan
|
||||||
|
data[1, 2, 2] = np.nan
|
||||||
|
for i in range(4):
|
||||||
|
storage.store(
|
||||||
|
kind="matrix",
|
||||||
|
meta={
|
||||||
|
"element": {"subject": f"test{i + 1}"},
|
||||||
|
"dependencies": [],
|
||||||
|
"marker": {"name": "matrix"},
|
||||||
|
"type": "BOLD",
|
||||||
|
},
|
||||||
|
data=data[:, :, i],
|
||||||
|
col_names=["f1", "f2", "f3"],
|
||||||
|
row_names=["g1", "g2", "g3"],
|
||||||
|
)
|
||||||
|
return storage
|
||||||
|
|
||||||
|
|
||||||
def test_incorrect_package(matrix_storage: HDF5FeatureStorage) -> None:
|
def test_incorrect_package(matrix_storage: HDF5FeatureStorage) -> None:
|
||||||
"""Test error check for incorrect package name.
|
"""Test error check for incorrect package name.
|
||||||
|
|
||||||
|
|
@ -176,3 +206,57 @@ def test_bctpy_function(
|
||||||
)
|
)
|
||||||
assert "Computing" in caplog.text
|
assert "Computing" in caplog.text
|
||||||
assert "Generating" in caplog.text
|
assert "Generating" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"nan_policy, error_msg",
|
||||||
|
[
|
||||||
|
("drop_element", None),
|
||||||
|
("drop_rows", "square"),
|
||||||
|
("drop_columns", "square"),
|
||||||
|
("drop_symmetric", None),
|
||||||
|
("bypass", "NaNs"),
|
||||||
|
("wrong", "Unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_bctpy_nans(
|
||||||
|
matrix_storage_with_nan: HDF5FeatureStorage,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
nan_policy: str,
|
||||||
|
error_msg: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test working function of bctpy.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
matrix_storage_with_nan : HDF5FeatureStorage
|
||||||
|
The HDF5FeatureStorage with matrix data, as fixture.
|
||||||
|
caplog : pytest.LogCaptureFixture
|
||||||
|
The pytest.LogCaptureFixture object.
|
||||||
|
nan_policy : str
|
||||||
|
The NAN policy to test.
|
||||||
|
error_msg : str
|
||||||
|
The expected error message snippet. If None, no error should be raised.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Skip test if import fails
|
||||||
|
pytest.importorskip("bct")
|
||||||
|
|
||||||
|
with caplog.at_level(logging.DEBUG):
|
||||||
|
if error_msg is None:
|
||||||
|
read_transform(
|
||||||
|
storage=matrix_storage_with_nan, # type: ignore
|
||||||
|
feature_name="BOLD_matrix",
|
||||||
|
transform="bctpy_eigenvector_centrality_und",
|
||||||
|
nan_policy=nan_policy,
|
||||||
|
)
|
||||||
|
assert "Computing" in caplog.text
|
||||||
|
assert "Generating" in caplog.text
|
||||||
|
else:
|
||||||
|
with pytest.raises(ValueError, match=error_msg):
|
||||||
|
read_transform(
|
||||||
|
storage=matrix_storage_with_nan, # type: ignore
|
||||||
|
feature_name="BOLD_matrix",
|
||||||
|
transform="bctpy_eigenvector_centrality_und",
|
||||||
|
nan_policy=nan_policy,
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue