Source code for bayesflow.adapters.transforms.nan_to_num
import keras.ops as ops
from bayesflow.utils.serialization import serializable, serialize
from .transform import Transform
[docs]
@serializable("bayesflow.adapters")
class NanToNum(Transform):
"""
Replace NaNs with a default value, and optionally encode a missing-data mask as a separate output key.
This is based on "Missing data in amortized simulation-based neural posterior estimation" by Wang et al. (2024).
Parameters
----------
key : str
The variable key to look for in the simulation data dict.
default_value : float, optional
Value to substitute wherever data is NaN. Default is 0.0.
return_mask : bool, optional
If True, a mask array will be returned under a new key, and the inverse transform uses it to restore
the NaNs. If False, the transform is not invertible: the inverse leaves the filled values untouched,
since genuine occurrences of `default_value` cannot be distinguished from filled-in NaNs. Default is False.
mask_prefix : str, optional
Prefix for the mask key in the output dictionary. Default is 'mask_'.
"""
def __init__(self, key: str, default_value: float = 0.0, return_mask: bool = False, mask_prefix: str = "mask"):
super().__init__()
self.key = key
self.default_value = default_value
self.return_mask = return_mask
self.mask_prefix = mask_prefix
[docs]
def get_config(self) -> dict:
return serialize(
{
"key": self.key,
"default_value": self.default_value,
"return_mask": self.return_mask,
"mask_prefix": self.mask_prefix,
}
)
@property
def mask_key(self) -> str:
"""
Key under which the mask will be stored in the output dictionary.
"""
return f"{self.mask_prefix}_{self.key}"
[docs]
def forward(self, data: dict[str, any], **kwargs) -> dict[str, any]:
"""
Forward transform: fill NaNs and optionally output mask under 'mask_<key>'.
"""
data = data.copy()
# Check if the mask key already exists in the data
if self.mask_key in data.keys():
raise ValueError(
f"Mask key '{self.mask_key}' already exists in the data. Please choose a different mask_prefix."
)
# Identify NaNs and fill with default value
mask = ops.isnan(data[self.key])
data[self.key] = ops.where(
mask, ops.convert_to_tensor(self.default_value, dtype=data[self.key].dtype), data[self.key]
)
if not self.return_mask:
return data
# Prepare mask array (1 for valid, 0 for NaN)
mask_array = ops.cast(~mask, "int8")
# Return both the filled data and the mask under separate keys
data[self.mask_key] = mask_array
return data
[docs]
def inverse(self, data: dict[str, any], **kwargs) -> dict[str, any]:
"""
Inverse transform: restore NaNs using the mask under 'mask_<key>'.
Without a mask (`return_mask=False`), the data is returned unchanged.
"""
if not self.return_mask:
return data
data = data.copy()
# Retrieve mask and values to reconstruct NaNs
if self.key not in data.keys():
return data
values = data[self.key]
nan = ops.convert_to_tensor(float("nan"), dtype=values.dtype)
mask_array = ops.cast(data[self.mask_key], "bool")
data[self.key] = ops.where(mask_array, values, nan)
return data