weibull
Weibull
Bases: OneToOneInversableDerivableTransformer
Transform series by applying a Weibull saturation function.
This transformation is typically applied after an adstock transformation to model saturation effects in response curves.
The Weibull formula is adapted to account for scaling and shift parameters, allowing additional flexibility in the response curve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
volume_1 | float | Baseline level of the normalized curve. | required |
volume_2 | float | Amplitude of the exponential component controlling the saturation range. Defaults to -volume_1. | None |
driver_1 | float | Scaling coefficient applied to the input series before the Weibull exponent. Controls how quickly the response saturates with respect to the input. Must be non-zero for invertibility. | 1.0 |
driver_2 | float | Horizontal shift applied to the input before exponentiation. Defaults to 0. | None |
symmetry | float | Weibull shape parameter controlling the curvature of the response curve. Must be strictly positive. | 1.0 |
Source code in eki_mmo_equations/one_to_one_transformations/diminishing_return/weibull.py
class Weibull(OneToOneInversableDerivableTransformer):
"""Transform series by applying a Weibull saturation function.
This transformation is typically applied after an adstock transformation
to model saturation effects in response curves.
The Weibull formula is adapted to account for scaling and shift parameters,
allowing additional flexibility in the response curve.
```math
volume_1 + volume_2 \\times \\exp\\left(-\\left(driver_1 \\times serie + driver_2\\right)^{symmetry}\\right)
```
Args:
volume_1 (float):
Baseline level of the normalized curve.
volume_2 (float, optional):
Amplitude of the exponential component controlling the saturation range.
Defaults to -volume_1.
driver_1 (float):
Scaling coefficient applied to the input series before the Weibull
exponent. Controls how quickly the response saturates with respect
to the input. Must be non-zero for invertibility.
driver_2 (float, optional):
Horizontal shift applied to the input before exponentiation.
Defaults to 0.
symmetry (float):
Weibull shape parameter controlling the curvature of the response
curve. Must be strictly positive.
"""
def __init__(
self,
volume_1: float,
volume_2: Optional[float] = None,
driver_1: float = 1.0,
driver_2: Optional[float] = None,
symmetry: float = 1.0,
) -> None:
self.volume_1 = volume_1
self.volume_2 = -volume_1 if volume_2 is None else volume_2
self.driver_1 = driver_1
self.driver_2 = 0.0 if driver_2 is None else driver_2
self.symmetry = symmetry
@property
def parameters(self) -> Dict[str, float]:
return self.__dict__
# ------- METHODS -------
def fit(self, serie: np.ndarray, y: Optional[np.ndarray] = None):
return super().fit(serie, y)
def transform(self, serie: np.ndarray, copy: bool = False) -> np.ndarray:
serie = super().transform(serie, copy)
return self._transformer(serie, self.volume_1, self.volume_2, self.driver_1, self.driver_2, self.symmetry)
def inverse_transform(self, serie: np.ndarray, copy: bool = False) -> np.ndarray:
serie = super().inverse_transform(serie, copy)
return self._inverse_transformer(
serie, self.volume_1, self.volume_2, self.driver_1, self.driver_2, self.symmetry
)
def derivative_transform(self, serie: np.ndarray, copy: bool = False) -> np.ndarray:
serie = super().derivative_transform(serie, copy)
return self._derivative_transformer(serie, self.volume_2, self.driver_1, self.driver_2, self.symmetry)
# ------- TRANSFORMERS -------
@staticmethod
def _transformer(
serie: np.ndarray, volume_1: float, volume_2: float, driver_1: float, driver_2: float, symmetry: float
) -> np.ndarray:
base = driver_1 * serie + driver_2
with np.errstate(over="ignore", invalid="ignore"):
transformed_serie = volume_1 + volume_2 * np.exp(-(base**symmetry))
return np.nan_to_num(transformed_serie)
@staticmethod
def _inverse_transformer(
serie: np.ndarray, volume_1: float, volume_2: float, driver_1: float, driver_2: float, symmetry: float
) -> np.ndarray:
ratio = (serie - volume_1) / volume_2
if np.any(~np.isfinite(ratio)) or np.any((ratio <= 0) | (ratio > 1)):
raise ParameterScopeException(
"Invalid value for inverse transform: (serie - volume_1) / volume_2 must be within the interval (0, 1]."
)
with np.errstate(divide="raise", invalid="raise"):
inverse_serie = ((-np.log(ratio)) ** (1 / symmetry) - driver_2) / driver_1
return inverse_serie
@staticmethod
def _derivative_transformer(
serie: np.ndarray, volume_2: float, driver_1: float, driver_2: float, symmetry: float
) -> np.ndarray:
base = driver_1 * serie + driver_2
with np.errstate(over="ignore", invalid="ignore"):
transformed_serie = -volume_2 * np.exp(-(base**symmetry)) * symmetry * driver_1 * (base ** (symmetry - 1))
return np.nan_to_num(transformed_serie)
# ------- CHECKERS -------
def check_params(self, serie: np.ndarray) -> None:
"""Check if parameters respect their application scope."""
if self.driver_1 == 0:
raise ParameterScopeException("Parameter driver_1 must be non-zero for invertibility reasons.")
if self.symmetry <= 0:
raise ParameterScopeException(f"Parameter symmetry must be strictly positive, not {self.symmetry}.")
if self.volume_2 == 0:
raise ParameterScopeException("Parameter volume_2 must be non-zero for invertibility reasons.")
if np.any(self.driver_1 * serie + self.driver_2 < 0):
raise ParameterScopeException("Parameter combination must ensure driver_1 * serie + driver_2 >= 0.")
check_params(serie)
Check if parameters respect their application scope.
Source code in eki_mmo_equations/one_to_one_transformations/diminishing_return/weibull.py
def check_params(self, serie: np.ndarray) -> None:
"""Check if parameters respect their application scope."""
if self.driver_1 == 0:
raise ParameterScopeException("Parameter driver_1 must be non-zero for invertibility reasons.")
if self.symmetry <= 0:
raise ParameterScopeException(f"Parameter symmetry must be strictly positive, not {self.symmetry}.")
if self.volume_2 == 0:
raise ParameterScopeException("Parameter volume_2 must be non-zero for invertibility reasons.")
if np.any(self.driver_1 * serie + self.driver_2 < 0):
raise ParameterScopeException("Parameter combination must ensure driver_1 * serie + driver_2 >= 0.")
WeibullUnscale
Bases: Weibull
Transform series by applying the Weibull function multiplied by the max parameter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
volume_1 | float | Baseline level of the normalized curve. | required |
volume_2 | float | Amplitude of the exponential component controlling the saturation range. Defaults to -volume_1. | None |
driver_1 | float | Scaling coefficient applied to the input series before the Weibull exponent. Controls how quickly the response saturates with respect to the input. Must be non-zero for invertibility. | 1.0 |
driver_2 | float | Horizontal shift applied to the input before exponentiation. Defaults to 0. | None |
symmetry | float | Weibull shape parameter controlling the curvature of the response curve. Must be strictly positive. | 1.0 |
max_training | float | Symbolic maximum applied to the output of the transformation. Defaults to None and is automatically set during fitting to the maximum value of the input training series. | None |
Source code in eki_mmo_equations/one_to_one_transformations/diminishing_return/weibull.py
class WeibullUnscale(Weibull):
"""Transform series by applying the Weibull function multiplied by the max parameter.
```math
max \\times \\left(
volume_1 + volume_2 \\times \\exp\\left(-\\left(driver_1 \\times serie + driver_2\\right)^{symmetry}\\right)
\\right)
```
Args:
volume_1 (float):
Baseline level of the normalized curve.
volume_2 (float, optional):
Amplitude of the exponential component controlling the saturation range.
Defaults to -volume_1.
driver_1 (float):
Scaling coefficient applied to the input series before the Weibull
exponent. Controls how quickly the response saturates with respect
to the input. Must be non-zero for invertibility.
driver_2 (float, optional):
Horizontal shift applied to the input before exponentiation.
Defaults to 0.
symmetry (float):
Weibull shape parameter controlling the curvature of the response
curve. Must be strictly positive.
max_training (float, optional):
Symbolic maximum applied to the output of the transformation.
Defaults to None and is automatically set during fitting to the maximum
value of the input training series.
"""
def __init__(
self,
volume_1: float,
volume_2: Optional[float] = None,
driver_1: float = 1.0,
driver_2: Optional[float] = None,
symmetry: float = 1.0,
max_training: Optional[float] = None,
) -> None:
super().__init__(volume_1, volume_2, driver_1, driver_2, symmetry)
self.max_training = max_training
def fit(self, serie: np.ndarray, y: Optional[np.ndarray] = None) -> None:
if self.max_training is None:
if np.any(serie[serie > 0]):
self.max_training = serie.max()
else:
self.max_training = serie.min() or 1
return super().fit(serie, y)
@staticmethod
def _transformer_unscale(
serie: np.ndarray,
volume_1: float,
volume_2: float,
driver_1: float,
driver_2: float,
symmetry: float,
max_training: float,
) -> np.ndarray:
return max_training * Weibull._transformer(
serie=serie,
volume_1=volume_1,
volume_2=volume_2,
driver_1=driver_1,
driver_2=driver_2,
symmetry=symmetry,
)
@staticmethod
def _inverse_transformer_unscale(
serie: np.ndarray,
volume_1: float,
volume_2: float,
driver_1: float,
driver_2: float,
symmetry: float,
max_training: float,
) -> np.ndarray:
scaled_serie = serie / max_training
return Weibull._inverse_transformer(
serie=scaled_serie,
volume_1=volume_1,
volume_2=volume_2,
driver_1=driver_1,
driver_2=driver_2,
symmetry=symmetry,
)
@staticmethod
def _derivative_transformer_unscale(
serie: np.ndarray, volume_2: float, driver_1: float, driver_2: float, symmetry: float, max_training: float
) -> np.ndarray:
return max_training * Weibull._derivative_transformer(
serie=serie,
volume_2=volume_2,
driver_1=driver_1,
driver_2=driver_2,
symmetry=symmetry,
)
# ------- METHODS -------
def transform(self, serie: np.ndarray, copy: bool = False) -> np.ndarray:
if self.max_training is None:
raise ValueError("max_training is not set. Call fit() before transform().")
serie = super(Weibull, self).transform(serie, copy)
return self._transformer_unscale(
serie, self.volume_1, self.volume_2, self.driver_1, self.driver_2, self.symmetry, self.max_training
)
def inverse_transform(self, serie: np.ndarray, copy: bool = False) -> np.ndarray:
if self.max_training is None:
raise ValueError("max_training is not set. Call fit() before inverse_transform().")
serie = super(Weibull, self).inverse_transform(serie, copy)
return self._inverse_transformer_unscale(
serie, self.volume_1, self.volume_2, self.driver_1, self.driver_2, self.symmetry, self.max_training
)
def derivative_transform(self, serie: np.ndarray, copy: bool = False) -> np.ndarray:
if self.max_training is None:
raise ValueError("max_training is not set. Call fit() before derivative_transform().")
serie = super(Weibull, self).derivative_transform(serie, copy)
return self._derivative_transformer_unscale(
serie, self.volume_2, self.driver_1, self.driver_2, self.symmetry, self.max_training
)
# ------- CHECKERS -------
def check_params(self, serie: np.ndarray) -> None:
"""Check if parameters respect their application scope."""
super().check_params(serie)
if self.max_training is not None and self.max_training <= 0:
raise ParameterScopeException(f"Parameter max must be strictly positive, not {self.max_training}.")
check_params(serie)
Check if parameters respect their application scope.
Source code in eki_mmo_equations/one_to_one_transformations/diminishing_return/weibull.py
def check_params(self, serie: np.ndarray) -> None:
"""Check if parameters respect their application scope."""
super().check_params(serie)
if self.max_training is not None and self.max_training <= 0:
raise ParameterScopeException(f"Parameter max must be strictly positive, not {self.max_training}.")