From d775c62ab13408b32c4d3e0b1d3b59f5f943ad1a Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Thu, 8 Jan 2026 09:45:28 -0600 Subject: [PATCH 01/22] Add ArcSinhTransformer for inverse hyperbolic sine transformation - Add ArcSinhTransformer class with loc and scale parameters - Support for positive and negative values (unlike LogTransformer) - Includes inverse_transform method - Add comprehensive tests with pytest parametrize - Add user guide documentation --- .../transformation/ArcSinhTransformer.rst | 121 +++++++++ docs/user_guide/transformation/index.rst | 1 + feature_engine/transformation/__init__.py | 4 +- feature_engine/transformation/arcsinh.py | 229 ++++++++++++++++++ tests/test_transformation/test_arcsinh.py | 202 +++++++++++++++ 5 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 docs/user_guide/transformation/ArcSinhTransformer.rst create mode 100644 feature_engine/transformation/arcsinh.py create mode 100644 tests/test_transformation/test_arcsinh.py diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst new file mode 100644 index 000000000..8edc182f4 --- /dev/null +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -0,0 +1,121 @@ +.. _arcsinh_transformer: + +.. currentmodule:: feature_engine.transformation + +ArcSinhTransformer +================== + +The :class:`ArcSinhTransformer()` applies the inverse hyperbolic sine transformation +(arcsinh) to numerical variables. Also known as the pseudo-logarithm, this +transformation is useful for data that contains both positive and negative values. + +The transformation is: x → arcsinh((x - loc) / scale) + +For large values of x, arcsinh(x) behaves like ln(x) + ln(2), providing similar +variance-stabilizing properties as the log transformation. For small values of x, +it behaves approximately linearly (x tends to x). This makes it ideal for variables +like net worth, profit/loss, or any metric that can be positive or negative. + +Unlike the :class:`LogTransformer()`, the :class:`ArcSinhTransformer()` can handle +zero and negative values without requiring any preprocessing. + +Example +~~~~~~~ + +Let's create a dataframe with positive and negative values and apply the arcsinh +transformation: + +.. code:: python + + import numpy as np + import pandas as pd + import matplotlib.pyplot as plt + from sklearn.model_selection import train_test_split + + from feature_engine.transformation import ArcSinhTransformer + + # Create sample data with positive and negative values + np.random.seed(42) + X = pd.DataFrame({ + 'profit': np.random.randn(1000) * 10000, # Values from -30000 to 30000 + 'net_worth': np.random.randn(1000) * 50000, + }) + + # Separate into train and test + X_train, X_test = train_test_split(X, test_size=0.3, random_state=0) + +Now let's set up the ArcSinhTransformer: + +.. code:: python + + # Set up the arcsinh transformer + tf = ArcSinhTransformer(variables=['profit', 'net_worth']) + + # Fit the transformer + tf.fit(X_train) + +The transformer does not learn any parameters when applying the fit method. It does +check however that the variables are numerical. + +We can now transform the variables: + +.. code:: python + + # Transform the data + train_t = tf.transform(X_train) + test_t = tf.transform(X_test) + +The arcsinh transformation compresses extreme values while preserving the sign: + +.. code:: python + + # Compare original and transformed distributions + fig, axes = plt.subplots(1, 2, figsize=(12, 4)) + + X_train['profit'].hist(ax=axes[0], bins=50) + axes[0].set_title('Original profit') + + train_t['profit'].hist(ax=axes[1], bins=50) + axes[1].set_title('Transformed profit') + + plt.tight_layout() + +Using loc and scale parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`ArcSinhTransformer()` supports location and scale parameters to +center and normalize data before transformation: + +.. code:: python + + # Center around mean and scale by std + tf = ArcSinhTransformer( + variables=['profit'], + loc=X_train['profit'].mean(), + scale=X_train['profit'].std() + ) + + tf.fit(X_train) + train_t = tf.transform(X_train) + +Inverse transformation +~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`ArcSinhTransformer()` supports inverse transformation to recover +the original values: + +.. code:: python + + # Transform and then inverse transform + train_t = tf.transform(X_train) + train_recovered = tf.inverse_transform(train_t) + + # Values should match original + np.allclose(X_train['profit'], train_recovered['profit']) + +API Reference +------------- + +.. autoclass:: ArcSinhTransformer + :members: + :inherited-members: diff --git a/docs/user_guide/transformation/index.rst b/docs/user_guide/transformation/index.rst index 85422c9f6..00ce20bfb 100644 --- a/docs/user_guide/transformation/index.rst +++ b/docs/user_guide/transformation/index.rst @@ -33,6 +33,7 @@ on the nature of the variable. LogCpTransformer ReciprocalTransformer ArcsinTransformer + ArcSinhTransformer PowerTransformer BoxCoxTransformer YeoJohnsonTransformer diff --git a/feature_engine/transformation/__init__.py b/feature_engine/transformation/__init__.py index 15011ac4b..9bbb62a59 100644 --- a/feature_engine/transformation/__init__.py +++ b/feature_engine/transformation/__init__.py @@ -4,6 +4,7 @@ """ from .arcsin import ArcsinTransformer +from .arcsinh import ArcSinhTransformer from .boxcox import BoxCoxTransformer from .log import LogCpTransformer, LogTransformer from .power import PowerTransformer @@ -11,11 +12,12 @@ from .yeojohnson import YeoJohnsonTransformer __all__ = [ + "ArcsinTransformer", + "ArcSinhTransformer", "BoxCoxTransformer", "LogTransformer", "LogCpTransformer", "PowerTransformer", "ReciprocalTransformer", "YeoJohnsonTransformer", - "ArcsinTransformer", ] diff --git a/feature_engine/transformation/arcsinh.py b/feature_engine/transformation/arcsinh.py new file mode 100644 index 000000000..0e51e68e0 --- /dev/null +++ b/feature_engine/transformation/arcsinh.py @@ -0,0 +1,229 @@ +# Authors: Ankit Hemant Lade (contributor) +# License: BSD 3 clause + +from typing import List, Optional, Union + +import numpy as np +import pandas as pd + +from feature_engine._base_transformers.base_numerical import BaseNumericalTransformer +from feature_engine._check_init_parameters.check_variables import ( + _check_variables_input_value, +) +from feature_engine._docstrings.fit_attributes import ( + _feature_names_in_docstring, + _n_features_in_docstring, + _variables_attribute_docstring, +) +from feature_engine._docstrings.init_parameters.all_trasnformers import ( + _variables_numerical_docstring, +) +from feature_engine._docstrings.methods import ( + _fit_not_learn_docstring, + _fit_transform_docstring, + _inverse_transform_docstring, +) +from feature_engine._docstrings.substitute import Substitution +from feature_engine.tags import _return_tags + + +@Substitution( + variables=_variables_numerical_docstring, + variables_=_variables_attribute_docstring, + feature_names_in_=_feature_names_in_docstring, + n_features_in_=_n_features_in_docstring, + fit=_fit_not_learn_docstring, + fit_transform=_fit_transform_docstring, + inverse_transform=_inverse_transform_docstring, +) +class ArcSinhTransformer(BaseNumericalTransformer): + """ + The ArcSinhTransformer() applies the inverse hyperbolic sine transformation + (arcsinh) to numerical variables. Also known as the pseudo-logarithm, this + transformation is useful for data that contains both positive and negative values. + + The transformation is: x → arcsinh((x - loc) / scale) + + For large values of x, arcsinh(x) behaves like ln(x) + ln(2), providing similar + variance-stabilizing properties as the log transformation. For small values of x, + it behaves approximately linearly. This makes it ideal for variables + like net worth, profit/loss, or any metric that can be positive or negative. + + A list of variables can be passed as an argument. Alternatively, the transformer + will automatically select and transform all variables of type numeric. + + More details in the :ref:`User Guide `. + + Parameters + ---------- + {variables} + + loc: float, default=0.0 + Location parameter for shifting the data before transformation. + The transformation becomes: arcsinh((x - loc) / scale) + + scale: float, default=1.0 + Scale parameter for normalizing the data before transformation. + Must be greater than 0. The transformation becomes: arcsinh((x - loc) / scale) + + Attributes + ---------- + {variables_} + + {feature_names_in_} + + {n_features_in_} + + Methods + ------- + {fit} + + {fit_transform} + + {inverse_transform} + + transform: + Transform the variables using the arcsinh function. + + See Also + -------- + feature_engine.transformation.LogTransformer : + Applies log transformation (only for positive values). + feature_engine.transformation.YeoJohnsonTransformer : + Applies Yeo-Johnson transformation. + + References + ---------- + .. [1] Burbidge, J. B., Magee, L., & Robb, A. L. (1988). Alternative + transformations to handle extreme values of the dependent variable. + Journal of the American Statistical Association, 83(401), 123-127. + + Examples + -------- + + >>> import numpy as np + >>> import pandas as pd + >>> from feature_engine.transformation import ArcSinhTransformer + >>> np.random.seed(42) + >>> X = pd.DataFrame(dict(x = np.random.randn(100) * 1000)) + >>> ast = ArcSinhTransformer() + >>> ast.fit(X) + >>> X = ast.transform(X) + >>> X.head() + x + 0 7.516076 + 1 -6.330816 + 2 7.780254 + 3 8.825252 + 4 -6.995893 + """ + + def __init__( + self, + variables: Union[None, int, str, List[Union[str, int]]] = None, + loc: float = 0.0, + scale: float = 1.0, + ) -> None: + + if not isinstance(loc, (int, float)): + raise ValueError( + f"loc must be a number (int or float). " + f"Got {type(loc).__name__} instead." + ) + + if not isinstance(scale, (int, float)) or scale <= 0: + raise ValueError( + f"scale must be a positive number (> 0). Got {scale} instead." + ) + + self.variables = _check_variables_input_value(variables) + self.loc = float(loc) + self.scale = float(scale) + + def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): + """ + This transformer does not learn parameters. + + Selects the numerical variables and stores feature names. + + Parameters + ---------- + X: Pandas DataFrame of shape = [n_samples, n_features]. + The training input samples. Can be the entire dataframe, not just the + variables to transform. + + y: pandas Series, default=None + It is not needed in this transformer. You can pass y or None. + + Returns + ------- + self: ArcSinhTransformer + The fitted transformer. + """ + + # check input dataframe and find/check numerical variables + X = super().fit(X) + + return self + + def transform(self, X: pd.DataFrame) -> pd.DataFrame: + """ + Transform the variables using the arcsinh function. + + Parameters + ---------- + X: Pandas DataFrame of shape = [n_samples, n_features] + The data to be transformed. + + Returns + ------- + X_new: pandas dataframe + The dataframe with the transformed variables. + """ + + # check input dataframe and if class was fitted + X = self._check_transform_input_and_state(X) + + # Ensure float dtype for the transformation + X[self.variables_] = X[self.variables_].astype(float) + + # Apply arcsinh transformation: arcsinh((x - loc) / scale) + X.loc[:, self.variables_] = np.arcsinh( + (X.loc[:, self.variables_] - self.loc) / self.scale + ) + + return X + + def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame: + """ + Convert the data back to the original representation. + + Parameters + ---------- + X: Pandas DataFrame of shape = [n_samples, n_features] + The data to be inverse transformed. + + Returns + ------- + X_tr: pandas dataframe + The dataframe with the inverse transformed variables. + """ + + # check input dataframe and if class was fitted + X = self._check_transform_input_and_state(X) + + # Inverse transform: x = sinh(y) * scale + loc + X.loc[:, self.variables_] = ( + np.sinh(X.loc[:, self.variables_]) * self.scale + self.loc + ) + + return X + + def _more_tags(self): + tags_dict = _return_tags() + tags_dict["variables"] = "numerical" + return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/tests/test_transformation/test_arcsinh.py b/tests/test_transformation/test_arcsinh.py new file mode 100644 index 000000000..621ad0664 --- /dev/null +++ b/tests/test_transformation/test_arcsinh.py @@ -0,0 +1,202 @@ +import numpy as np +import pandas as pd +import pytest + +from feature_engine.transformation import ArcSinhTransformer + + +@pytest.fixture +def df_numerical(): + """Fixture providing sample numerical data with positive and negative values.""" + return pd.DataFrame({ + "a": [-100, -10, 0, 10, 100], + "b": [1, 2, 3, 4, 5], + }) + + +@pytest.fixture +def df_multi_column(): + """Fixture providing DataFrame with multiple columns.""" + return pd.DataFrame({ + "a": [1, 2, 3], + "b": [4, 5, 6], + "c": [7, 8, 9], + }) + + +def test_default_parameters(df_numerical): + """Test transformer with default parameters applies arcsinh to all columns.""" + transformer = ArcSinhTransformer() + X_tr = transformer.fit_transform(df_numerical.copy()) + + expected_a = np.arcsinh(df_numerical["a"]) + expected_b = np.arcsinh(df_numerical["b"]) + np.testing.assert_array_almost_equal(X_tr["a"], expected_a) + np.testing.assert_array_almost_equal(X_tr["b"], expected_b) + + +def test_specific_variables(df_multi_column): + """Test transformer with specific variables selected.""" + transformer = ArcSinhTransformer(variables=["a", "b"]) + X_tr = transformer.fit_transform(df_multi_column.copy()) + + np.testing.assert_array_almost_equal( + X_tr["a"], np.arcsinh(df_multi_column["a"]) + ) + np.testing.assert_array_almost_equal( + X_tr["b"], np.arcsinh(df_multi_column["b"]) + ) + np.testing.assert_array_equal(X_tr["c"], df_multi_column["c"]) + + +def test_with_loc_and_scale(): + """Test transformer with loc and scale parameters.""" + X = pd.DataFrame({"a": [10, 20, 30, 40, 50]}) + loc = 30.0 + scale = 10.0 + transformer = ArcSinhTransformer(loc=loc, scale=scale) + X_tr = transformer.fit_transform(X.copy()) + + expected = np.arcsinh((X["a"] - loc) / scale) + np.testing.assert_array_almost_equal(X_tr["a"], expected) + np.testing.assert_almost_equal(X_tr["a"].iloc[2], 0.0, decimal=10) + + +@pytest.mark.parametrize("loc", [0.0, 10.0, -10.0, 100.5]) +def test_various_loc_values(loc): + """Test that various loc values work correctly.""" + X = pd.DataFrame({"a": [1, 2, 3, 4, 5]}) + transformer = ArcSinhTransformer(loc=loc) + X_tr = transformer.fit_transform(X.copy()) + + expected = np.arcsinh((X["a"] - loc) / 1.0) + np.testing.assert_array_almost_equal(X_tr["a"], expected) + + +@pytest.mark.parametrize("scale", [0.5, 1.0, 2.0, 10.0, 100.0]) +def test_various_scale_values(scale): + """Test that various scale values work correctly.""" + X = pd.DataFrame({"a": [1, 2, 3, 4, 5]}) + transformer = ArcSinhTransformer(scale=scale) + X_tr = transformer.fit_transform(X.copy()) + + expected = np.arcsinh((X["a"] - 0.0) / scale) + np.testing.assert_array_almost_equal(X_tr["a"], expected) + + +def test_inverse_transform(df_numerical): + """Test inverse_transform returns original values.""" + X_original = df_numerical.copy() + transformer = ArcSinhTransformer() + X_tr = transformer.fit_transform(df_numerical.copy()) + X_inv = transformer.inverse_transform(X_tr) + + np.testing.assert_array_almost_equal(X_inv["a"], X_original["a"], decimal=10) + np.testing.assert_array_almost_equal(X_inv["b"], X_original["b"], decimal=10) + + +def test_inverse_transform_with_loc_scale(): + """Test inverse_transform with loc and scale parameters.""" + X = pd.DataFrame({"a": [10, 20, 30, 40, 50]}) + X_original = X.copy() + transformer = ArcSinhTransformer(loc=25.0, scale=5.0) + X_tr = transformer.fit_transform(X.copy()) + X_inv = transformer.inverse_transform(X_tr) + + np.testing.assert_array_almost_equal(X_inv["a"], X_original["a"], decimal=10) + + +def test_negative_values(): + """Test that transformer handles negative values correctly.""" + X = pd.DataFrame({"a": [-1000, -500, 0, 500, 1000]}) + transformer = ArcSinhTransformer() + X_tr = transformer.fit_transform(X.copy()) + + assert X_tr["a"].iloc[0] < 0 + assert X_tr["a"].iloc[1] < 0 + assert X_tr["a"].iloc[2] == 0 + assert X_tr["a"].iloc[3] > 0 + assert X_tr["a"].iloc[4] > 0 + + np.testing.assert_almost_equal( + X_tr["a"].iloc[0], -X_tr["a"].iloc[4], decimal=10 + ) + np.testing.assert_almost_equal( + X_tr["a"].iloc[1], -X_tr["a"].iloc[3], decimal=10 + ) + + +@pytest.mark.parametrize("invalid_scale", [0, -1, -0.5, -100]) +def test_invalid_scale_raises_error(invalid_scale): + """Test that non-positive scale values raise ValueError.""" + with pytest.raises(ValueError, match="scale must be a positive number"): + ArcSinhTransformer(scale=invalid_scale) + + +@pytest.mark.parametrize("invalid_loc", ["invalid", [1, 2], {"a": 1}, None]) +def test_invalid_loc_raises_error(invalid_loc): + """Test that non-numeric loc values raise ValueError.""" + with pytest.raises(ValueError, match="loc must be a number"): + ArcSinhTransformer(loc=invalid_loc) + + +def test_fit_stores_attributes(): + """Test that fit stores expected attributes with correct values.""" + X = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + transformer = ArcSinhTransformer() + transformer.fit(X) + + assert hasattr(transformer, "variables_") + assert hasattr(transformer, "feature_names_in_") + assert hasattr(transformer, "n_features_in_") + assert transformer.n_features_in_ == 2 + assert set(transformer.variables_) == {"a", "b"} + assert transformer.feature_names_in_ == ["a", "b"] + + +def test_get_feature_names_out(): + """Test get_feature_names_out returns correct feature names.""" + X = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + transformer = ArcSinhTransformer() + transformer.fit(X) + + feature_names = transformer.get_feature_names_out() + assert feature_names == ["a", "b"] + + +def test_get_feature_names_out_with_subset(): + """Test get_feature_names_out with subset of variables.""" + X = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) + transformer = ArcSinhTransformer(variables=["a"]) + transformer.fit(X) + + feature_names = transformer.get_feature_names_out() + assert feature_names == ["a", "b", "c"] + + +def test_behavior_like_log_for_large_values(): + """Test that arcsinh behaves like log for large positive values.""" + X = pd.DataFrame({"a": [1000, 10000, 100000]}) + transformer = ArcSinhTransformer() + X_tr = transformer.fit_transform(X.copy()) + + log_approx = np.log(2 * X["a"]) + np.testing.assert_array_almost_equal(X_tr["a"], log_approx, decimal=1) + + +def test_behavior_like_identity_for_small_values(): + """Test that arcsinh behaves like identity for small values.""" + X = pd.DataFrame({"a": [0.001, 0.01, 0.1]}) + transformer = ArcSinhTransformer() + X_tr = transformer.fit_transform(X.copy()) + + np.testing.assert_array_almost_equal(X_tr["a"], X["a"], decimal=2) + + +def test_zero_input_returns_zero(): + """Test that arcsinh(0) = 0.""" + X = pd.DataFrame({"a": [0.0]}) + transformer = ArcSinhTransformer() + X_tr = transformer.fit_transform(X.copy()) + + assert X_tr["a"].iloc[0] == 0.0 From 841e30cca2c1415489ce67d53e0c5f09bfd86966 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:38:33 -0500 Subject: [PATCH 02/22] Enhance ArcSinhTransformer docs: add to index/README, improve user guide with comparison and references --- README.md | 1 + .../transformation/ArcSinhTransformer.rst | 5 ++ docs/index.rst | 1 + .../transformation/ArcSinhTransformer.rst | 50 +++++++++++++++++-- 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 docs/api_doc/transformation/ArcSinhTransformer.rst diff --git a/README.md b/README.md index 19ccf3416..6b9cdb24d 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Please share your story by answering 1 quick question * PowerTransformer * BoxCoxTransformer * YeoJohnsonTransformer +* ArcSinhTransformer ### Variable Scaling methods * MeanNormalizationScaler diff --git a/docs/api_doc/transformation/ArcSinhTransformer.rst b/docs/api_doc/transformation/ArcSinhTransformer.rst new file mode 100644 index 000000000..69f315cbb --- /dev/null +++ b/docs/api_doc/transformation/ArcSinhTransformer.rst @@ -0,0 +1,5 @@ +ArcSinhTransformer +================== + +.. autoclass:: feature_engine.transformation.ArcSinhTransformer + :members: diff --git a/docs/index.rst b/docs/index.rst index d1bf049a8..d73005b8f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -237,6 +237,7 @@ like anova, and machine learning models, like linear regression. Feature-engine - :doc:`api_doc/transformation/BoxCoxTransformer`: performs Box-Cox transformation of numerical variables - :doc:`api_doc/transformation/YeoJohnsonTransformer`: performs Yeo-Johnson transformation of numerical variables - :doc:`api_doc/transformation/ArcsinTransformer`: performs arcsin transformation of numerical variables +- :doc:`api_doc/transformation/ArcSinhTransformer`: applies arcsinh (pseudo-logarithm) transformation for data with positive and negative values Feature Creation: ~~~~~~~~~~~~~~~~~ diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index 8edc182f4..0592582bc 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -11,10 +11,52 @@ transformation is useful for data that contains both positive and negative value The transformation is: x → arcsinh((x - loc) / scale) -For large values of x, arcsinh(x) behaves like ln(x) + ln(2), providing similar -variance-stabilizing properties as the log transformation. For small values of x, -it behaves approximately linearly (x tends to x). This makes it ideal for variables -like net worth, profit/loss, or any metric that can be positive or negative. +Comparison to LogTransformer and ArcsinTransformer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **LogTransformer**: `log(x)` requires `x > 0`. If your data contains zeros or negative values, you cannot use the standard LogTransformer directly. You would need to shift the data (e.g. `LogCpTransformer`) or remove non-positive values. +- **ArcsinTransformer**: `arcsin(sqrt(x))` is typically used for proportions/ratios bounded between 0 and 1. It is not suitable for general unbounded numerical data. +- **ArcSinhTransformer**: `arcsinh(x)` works for **all real numbers** (positive, negative, and zero). It handles zero gracefully (arcsinh(0) = 0) and is symmetric around zero. + +When to use ArcSinhTransformer: +- Your data contains zeros or negative values (e.g., profit/loss, debt, temperature). +- You want a log-like transformation to stabilize variance or compress extreme values. +- You don't want to add an arbitrary constant (shift) to make values positive. + +Intuitive Explanation of Parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The transformation includes optional `loc` (location) and `scale` parameters: + +.. math:: + y = \text{arcsinh}\left(\frac{x - \text{loc}}{\text{scale}}\right) + +- **Why scale?** + The `arcsinh(x)` function is linear near zero (for small x) and logarithmic for large x. + The "linear region" is roughly between -1 and 1. + By adjusting the `scale`, you control which part of your data falls into this linear region versus the logarithmic region. + - If `scale` is large, more of your data falls in the linear region (behavior close to original data). + - If `scale` is small, more of your data falls in the logarithmic region (stronger compression of values). + Common practice is to set `scale` to 1 or usage the standard deviation of the variable. + +- **Why loc?** + The `loc` parameter centers the data. The transition from negative logarithmic behavior to positive logarithmic behavior happens around `x = loc`. + Common practice is to set `loc` to 0 or usage the mean of the variable. + +References +~~~~~~~~~~ + +For more details on the inverse hyperbolic sine transformation: + +1. `How should I transform non-negative data including zeros? `_ (StackExchange) +2. `Interpreting Treatment Effects: Inverse Hyperbolic Sine Outcome Variable `_ (World Bank Blog) +3. `Burbidge, J. B., Magee, L., & Robb, A. L. (1988). Alternative transformations to handle extreme values of the dependent variable. Journal of the American Statistical Association. `_ + +Example +~~~~~~~ + +Let's create a dataframe with positive and negative values and apply the arcsinh +transformation: Unlike the :class:`LogTransformer()`, the :class:`ArcSinhTransformer()` can handle zero and negative values without requiring any preprocessing. From 6022a24d6facd17f31a6d68dcba08157e12e6375 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:38:57 -0500 Subject: [PATCH 03/22] Add ArcSinhTransformer to standard estimator checks --- tests/test_transformation/test_check_estimator_transformers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_transformation/test_check_estimator_transformers.py b/tests/test_transformation/test_check_estimator_transformers.py index 7db0088f8..3de596c3c 100644 --- a/tests/test_transformation/test_check_estimator_transformers.py +++ b/tests/test_transformation/test_check_estimator_transformers.py @@ -7,6 +7,7 @@ from feature_engine.transformation import ( ArcsinTransformer, + ArcSinhTransformer, BoxCoxTransformer, LogCpTransformer, LogTransformer, @@ -20,7 +21,9 @@ BoxCoxTransformer(), LogTransformer(), LogCpTransformer(), + LogCpTransformer(), ArcsinTransformer(), + ArcSinhTransformer(), PowerTransformer(), ReciprocalTransformer(), YeoJohnsonTransformer(), From 201b0323415cc61a943e981a5b7bfd3cf66af452 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:39:56 -0500 Subject: [PATCH 04/22] Fix duplicate LogCpTransformer in estimator checks --- tests/test_transformation/test_check_estimator_transformers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_transformation/test_check_estimator_transformers.py b/tests/test_transformation/test_check_estimator_transformers.py index 3de596c3c..812cbbbaf 100644 --- a/tests/test_transformation/test_check_estimator_transformers.py +++ b/tests/test_transformation/test_check_estimator_transformers.py @@ -21,7 +21,6 @@ BoxCoxTransformer(), LogTransformer(), LogCpTransformer(), - LogCpTransformer(), ArcsinTransformer(), ArcSinhTransformer(), PowerTransformer(), From 0cb0023c2c4be83c40dcfd226d1373625b0cd96d Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:41:17 -0500 Subject: [PATCH 05/22] Docs: Remove leading 'The' from ArcSinhTransformer references --- docs/user_guide/transformation/ArcSinhTransformer.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index 0592582bc..a5970b384 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -5,7 +5,7 @@ ArcSinhTransformer ================== -The :class:`ArcSinhTransformer()` applies the inverse hyperbolic sine transformation +:class:`ArcSinhTransformer()` applies the inverse hyperbolic sine transformation (arcsinh) to numerical variables. Also known as the pseudo-logarithm, this transformation is useful for data that contains both positive and negative values. @@ -58,7 +58,7 @@ Example Let's create a dataframe with positive and negative values and apply the arcsinh transformation: -Unlike the :class:`LogTransformer()`, the :class:`ArcSinhTransformer()` can handle +Unlike the :class:`LogTransformer()`, :class:`ArcSinhTransformer()` can handle zero and negative values without requiring any preprocessing. Example @@ -125,7 +125,7 @@ The arcsinh transformation compresses extreme values while preserving the sign: Using loc and scale parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :class:`ArcSinhTransformer()` supports location and scale parameters to +:class:`ArcSinhTransformer()` supports location and scale parameters to center and normalize data before transformation: .. code:: python @@ -143,7 +143,7 @@ center and normalize data before transformation: Inverse transformation ~~~~~~~~~~~~~~~~~~~~~~ -The :class:`ArcSinhTransformer()` supports inverse transformation to recover +:class:`ArcSinhTransformer()` supports inverse transformation to recover the original values: .. code:: python From bf442669dbf6956040eba09d4f425246656d6902 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:41:34 -0500 Subject: [PATCH 06/22] Docs: Remove 'the' before LogTransformer reference --- docs/user_guide/transformation/ArcSinhTransformer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index a5970b384..d48e20aca 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -58,7 +58,7 @@ Example Let's create a dataframe with positive and negative values and apply the arcsinh transformation: -Unlike the :class:`LogTransformer()`, :class:`ArcSinhTransformer()` can handle +Unlike :class:`LogTransformer()`, :class:`ArcSinhTransformer()` can handle zero and negative values without requiring any preprocessing. Example From b9d92d5ad008d6c5d0060ab304b719bad74a9dbf Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:42:18 -0500 Subject: [PATCH 07/22] Docs: Rename Example section to Python demo --- docs/user_guide/transformation/ArcSinhTransformer.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index d48e20aca..933b16dbb 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -52,8 +52,8 @@ For more details on the inverse hyperbolic sine transformation: 2. `Interpreting Treatment Effects: Inverse Hyperbolic Sine Outcome Variable `_ (World Bank Blog) 3. `Burbidge, J. B., Magee, L., & Robb, A. L. (1988). Alternative transformations to handle extreme values of the dependent variable. Journal of the American Statistical Association. `_ -Example -~~~~~~~ +Python demo +----------- Let's create a dataframe with positive and negative values and apply the arcsinh transformation: From 246cd2c850123b56d24ab325322a5ba33d2a3cf1 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:43:14 -0500 Subject: [PATCH 08/22] Docs: Standardize section underlines to '---' --- docs/user_guide/transformation/ArcSinhTransformer.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index 933b16dbb..c7a104670 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -12,7 +12,7 @@ transformation is useful for data that contains both positive and negative value The transformation is: x → arcsinh((x - loc) / scale) Comparison to LogTransformer and ArcsinTransformer -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------------- - **LogTransformer**: `log(x)` requires `x > 0`. If your data contains zeros or negative values, you cannot use the standard LogTransformer directly. You would need to shift the data (e.g. `LogCpTransformer`) or remove non-positive values. - **ArcsinTransformer**: `arcsin(sqrt(x))` is typically used for proportions/ratios bounded between 0 and 1. It is not suitable for general unbounded numerical data. @@ -24,7 +24,7 @@ When to use ArcSinhTransformer: - You don't want to add an arbitrary constant (shift) to make values positive. Intuitive Explanation of Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------------------- The transformation includes optional `loc` (location) and `scale` parameters: @@ -44,7 +44,7 @@ The transformation includes optional `loc` (location) and `scale` parameters: Common practice is to set `loc` to 0 or usage the mean of the variable. References -~~~~~~~~~~ +---------- For more details on the inverse hyperbolic sine transformation: @@ -123,7 +123,7 @@ The arcsinh transformation compresses extreme values while preserving the sign: plt.tight_layout() Using loc and scale parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------ :class:`ArcSinhTransformer()` supports location and scale parameters to center and normalize data before transformation: @@ -141,7 +141,7 @@ center and normalize data before transformation: train_t = tf.transform(X_train) Inverse transformation -~~~~~~~~~~~~~~~~~~~~~~ +---------------------- :class:`ArcSinhTransformer()` supports inverse transformation to recover the original values: From 0c44f40bb51baf8ad42500b54db8e038ba3938d5 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:45:08 -0500 Subject: [PATCH 09/22] Docs: Add dataframe output to ArcSinhTransformer python demo --- .../transformation/ArcSinhTransformer.rst | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index c7a104670..0a3156e63 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -55,15 +55,9 @@ For more details on the inverse hyperbolic sine transformation: Python demo ----------- -Let's create a dataframe with positive and negative values and apply the arcsinh -transformation: - Unlike :class:`LogTransformer()`, :class:`ArcSinhTransformer()` can handle zero and negative values without requiring any preprocessing. -Example -~~~~~~~ - Let's create a dataframe with positive and negative values and apply the arcsinh transformation: @@ -86,6 +80,19 @@ transformation: # Separate into train and test X_train, X_test = train_test_split(X, test_size=0.3, random_state=0) + print(X.head()) + +The dataframe contains positive and negative values: + +.. code:: python + + profit net_worth + 0 4967.141530 69967.771829 + 1 -1382.643012 46231.684146 + 2 6476.885381 2981.518496 + 3 15230.298564 -32346.838885 + 4 -2341.533747 34911.165681 + Now let's set up the ArcSinhTransformer: .. code:: python From cc95d3a627603a3ffa069f6e610c43e1c15fff22 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:46:01 -0500 Subject: [PATCH 10/22] Docs: Update transformer setup text in ArcSinhTransformer demo --- docs/user_guide/transformation/ArcSinhTransformer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index 0a3156e63..847fe098d 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -93,7 +93,7 @@ The dataframe contains positive and negative values: 3 15230.298564 -32346.838885 4 -2341.533747 34911.165681 -Now let's set up the ArcSinhTransformer: +Now let's set up the ArcSinhTransformer and fit it to the training set: .. code:: python From aeeddad70ca8fc10cb3cc74f05bf032e08dd7ba8 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:46:55 -0500 Subject: [PATCH 11/22] Docs: Add commas around 'however' for grammar --- docs/user_guide/transformation/ArcSinhTransformer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index 847fe098d..3e688bfd0 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -104,7 +104,7 @@ Now let's set up the ArcSinhTransformer and fit it to the training set: tf.fit(X_train) The transformer does not learn any parameters when applying the fit method. It does -check however that the variables are numerical. +check, however, that the variables are numerical. We can now transform the variables: From e3e644002277f46df4643b65e37fd4b03b21878c Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:47:50 -0500 Subject: [PATCH 12/22] Docs: Add transformed dataframe output to ArcSinhTransformer demo --- .../transformation/ArcSinhTransformer.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index 3e688bfd0..84f03c914 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -114,6 +114,19 @@ We can now transform the variables: train_t = tf.transform(X_train) test_t = tf.transform(X_test) + print(train_t.head()) + +The dataframe with the transformed variables: + +.. code:: python + + profit net_worth + 105 8.997273 -11.552056 + 68 8.886371 -10.753000 + 479 10.016437 -10.686152 + 399 10.116836 -11.092693 + 434 10.310523 -9.723893 + The arcsinh transformation compresses extreme values while preserving the sign: .. code:: python From a8d880a5ad9748f1679c311351c66d55fe55d7d4 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:48:40 -0500 Subject: [PATCH 13/22] Docs: Clarify intro text for plotting code --- docs/user_guide/transformation/ArcSinhTransformer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index 84f03c914..5c53a7bd8 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -127,7 +127,7 @@ The dataframe with the transformed variables: 399 10.116836 -11.092693 434 10.310523 -9.723893 -The arcsinh transformation compresses extreme values while preserving the sign: +The arcsinh transformation compresses extreme values while preserving the sign. We can inspect the distribution of the original and transformed variables with histograms: .. code:: python From 8fad3b889ea2020b952bd986a657cfec31a92ac8 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:49:57 -0500 Subject: [PATCH 14/22] Docs: Add histogram plot image to ArcSinhTransformer guide --- docs/images/arcsinh_profit_histogram.png | Bin 0 -> 20116 bytes .../transformation/ArcSinhTransformer.rst | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 docs/images/arcsinh_profit_histogram.png diff --git a/docs/images/arcsinh_profit_histogram.png b/docs/images/arcsinh_profit_histogram.png new file mode 100644 index 0000000000000000000000000000000000000000..e776e30d4bc28e593ad54bb5d9515b4213300267 GIT binary patch literal 20116 zcmcJ11yogQ*Y3u^KmHAh@My6Iq1`kfz=vY`9n3{01U%JT7$9hWN z%F4`=pM%5r&);A-wb0|JQnhY|y&N*TtzwBnk=#fA5+sTw7@$xxDDmr8P-~U?kB~wZu@sX zr5vl=ga72#bx?}{|EbatSvzFY;@+dY`ze#^pYM*;%5tLFKPV_jacg#Qtf6UWZm2?2 z>_$P_eDu7IZ+rW_HJ_ayB$I<>C3Qo?)t)*X@zUcv>u?R9vz#|yqg#HVLs zaOp3Y!}1e}2vbV;CN1sJzXGRII_Nkrw3e~6&as9qC}&()Sa=%{z~sv$eR=wr1E*fI zpY=3W+{7e(b4sb=ZAwOYahny3V(P^zHWZ4Kk683aPPY|jQgJalz3e-BZDVMM-IIdt z^2)OgSrMWB9uH-<^YgWIj#7$g=&o`5eebTm8sU>EAJI89eJj?!aMc6H5xP9vr{uD|6xpz|;kqMx=4F0m z@!EK(&QQ5?dybxLR#7Q+%971oe@|{=L0X&$>7m}|0((*K=SO@*0;o+(?>6&|9O=o{ z@eH%5KR<|@E8j3Fn#pcxoOa!s)|3v3rxCDe=%{6z$UR|}ogqt9BeR?`2G4b`sj~_EnYtLHe~pc6Dt7(xfxaG=-E_`{k&&@3=#utmxb2`p zm&W(e8}1GTLoOwhp%Ml59n!pYBHIfjk+~6qh9@hpx)TOh`UixrkNV0=9U^08_s5XjsW#S-eGGNmk~MBUnIv~ zv1wr`9ICl1Cknz_{V(V!o zI$5C=zcAZDdnPYzR<23(wS&yYLN3+hNZ;G&xviOPKPAgC^@`?{9UT7>hwGZ8sZdiqcH6D*7(3yq6r~IvfATUr-kU6j>kb1^ zp3gNtIj+tbuW#_;&Z=+>a+LpgXHXYM5Y`mR8(JSxA-w+etlY8oIWOOHvH_Z#!zXn^ z(dO-kBiawY^1c#f-_HJ+pShUuO>TR%|B1BD+!}S;0i6q6Xz`vLJs(!};?8}q1*;fI zoTW?d8)i;pRZaaZo3GT4UOlkciHaut#&s~>pNjbN)|_nx=fLGWr@YX|0bVw;ttb^a zm*p7xI(V|imRdx3wJ$QKmBRDf+J2gRxu$ysr)i4Tvo9u_@?tKdm>3-mx<&j#aJlu` zJq-;LljGPp-b$-i4KK;g)vuPX3dx-;+}Ylc7QJ1NmKz#^rOX|SVf&JIqagqBP`+{K zAsz#vYkM`(tL;`NHJ!3}Ib7E}A{%X%Z$@HOmkHkyeqb+KzGpw3RzTIiY?w@!u)^Nr zIH}k5xuwv2WHeYW;?koMf%(#o*oKA%&d6)iq7l(YxtQbT6YRsdF#(5;7|@!%e{yIM z65F5v!wqrqmxo3TFwC<2Wx>5~vC~uxor|5tafEwt>FO+&$W4PlvI&lOHkKeF;HrQE08jneH@+oep@^^EN7%V}wRkgH622Dou*+U@>< zfuR$~ep*ZmzdR!}5X!KJ=e$EO@{;y5+2ZqZ@j)vggE3K2m8pFNrjfP-lL?U~SmU1T z>XNzE=xZ`gPNkMDLaTN-1w}=EZfDu$7%}SciV{-G7 z5)zmDOr`C)6$V$POOv5YGMS;O|^%JldePQ2e;@q@Qnr=%ry}rF#;rf2Hwd8)i zWkIv{`tqd9#8SUy%ZkD2cy-#mr@r~ci>)OF5hbn;w`Au1ro!(?C)l>3CFi56tE=rJ zhsYb7d&BVnSEVG9tsFb%0PV(f5mn5w5qfnWlSWYXmh4rcYKiVC- zcDCntLij!N%dzQJ)&{gubKPiuLSHP|6jm>{s~|sju-w_vb8!`JVST!Ki4>8wvHECR zg{y1F`x2M3xFUxzY{@fX>Tze`ho4B75(l;}U%pJ0x;m=)+_IHIzxP6e97VByxYYD* zNFUtSZZkLe_U(+@{_9s{ zwd%<}85%@My7QcSH!#bUDK9dsq8C19D?F_jAZS(ktXTW*WYD4BG?~!%7)H6;zIG1} zo)VmiZCYGkissr(Tg)wY6%;iK(bf!=Xo{>}_Co1V6lx}6;1d<~vT8}AK|$f@=tEA$ zc&w1%)#UXR655HRZNBK0Y0lobJJVED-EI7zat%60A(4pO-xjp?h@YaRy4}7IyqQ$n zm0>%nCpS4c>6H!_TS8|Tvk1v7)FQD&A6H(y_Z`M%8kZGP?<$w;qT>2E8VX~dU_@dB zHe_)Rsm7~EaCK>R`WJ7y3pTak@`mHdbBfWIWv-qI2?@@evm@r`d88O*QZz=vX_q`G z-Qm&nijs4aj+BekSOHLB)GW76I0p+0r?#=-*6mug_62XNRtw{`v`2|>5w265RDx}n z;xz2X$U8jLYrW9f4_{I$VFpX>Pwz-IjV)bYTXoV1mrrm>S;qEP9$}g?aN7Vpsihmz zMsUqKprTlPKyZ68up+2Lng4|jCd_4hGF~jlbVxW-z}O*>J;>^mJna!H03gH{0(+@a z@^XtxOmHJ#+hXE-)(R@Nj4Omo6vd{Otj@RMRFk*ERRO}$@ke{w_;duXS>2-`Nn6?{ zc7Us5u6Vlf-M|;C*410|&hi$PmeOvimm>IY$gLbrBbv0iR@Xr#0cEGL(TFE)TxnW) zg*#v;UVrOe35Nq(J&8nQvsExcKVbZh|H&1uQ&wNa4vNl|t!h{*N$izJA4-v|z1+HR z%!*1+8b=i&VCJxz$sRt&7_^+OwG~XNl&&^Oi;2#&x*0Y#WjJpc{`8Js!9pWNCk?Sh z86dHHYHBm;2~BO*q3EpkOH6197P&`OFZZEPRbLF=$QNu!6C|u#M+78lp6oF3L~FLL zCaTgW$*Oje{Bg0e(I%C5UeIQKkZl^5u{vF&8YLsNB44yv&(g%&>X4IBo>9JDzv0@{ zf=yGE7D}3%uGn75P+g9Y71_#N-9g`6SKF(vq;9Q{<9afAvE2bZFiUWd4y{FSG40** zaGJaB6c+#l8SasN`Y9~NO|jaj&2ioegB7ky$hlm5_1HB^yXEVxDx0A)JI=q}KP}^* z&(JZx!CyHAY3s~<`9^xuzAk#|=(yZ7$^NHWxZ2R?y8;XWhczAAQA~J>&F;_&lqpLc zpvp^KX->5{^z8`$Q2XNMtf?y|ej1w;W*v4Uyq5}v%4JJ4{rZO@6nq|>eOd-4Sa024E(%}h6$O!3l>5uZ^0=SM5kSBgx_*J}DNcAh>v z-W<+id|E_gsM9xB1}UK-4$1}wNkzd4?j03N%>qmV&Z`4@^;1(R8385p<$e2HmI)*C zU-7rKQqc8MNtdV?u1&iw!m1{xs^r|Hv-?aCNzm7qU78yazxR{K!bdXbG<2srBdX>I z_C159YNQvF;V*z*%+a>PT84#SYkh?%F%~H$a+Dof&5|5t4@3Mhq&E*V z8zrB2k~9#`v+nR9XKBpmDE@jz?q%Dld#7^S6CYiqbrhR-8Z`Srya*_Bo!qX&V*)Vx z{hF%Q!*_Lb;(`ZsSEjn+&-7C9hsHFA$lbZ4MVN;3phFt7-UzI}Rcf;1T+uB$r*Buu z8ruo_S89~xv_->TfS^9L)~4wc#EuU-Jj6Ps?sX0Kwpx9HfFk3WIIcf zY<{16K1E-a_H)V%%-Z7E_;_4?WAV55MA8vc`EI?1<}tzk4poo$$Ft^jRS-IJ1&x{# z@okmzU(5Y|q7@Z6k+X63#8}*hv|cE7D~{ljtQoQB=Lcb;CL}0P6xYER|KN#8*YavP zrh@%xRt6aM@EV8KsH3CzfoP{J)W6T2;kkA+9^6BHp>vC-# zezbo~?TuJ`m&%vVM>6nao~^zCtIg8>?BX{kIyMsC{h5ilLb8qKfo@l1KN+_NZ7-#z zD&1(-LRKHP7q|cCj^B;Z(9rPV_nDsd6eW(y?yQ(F`|;q#Zy!iJNB9Q0)_0!MIox(w z{(8Utp{{QIV435G5K|P&?GB!3(0Ylq7}-{n_w3gv6UN+0gfF(7WLINGS{q(3VOMDQ z#sJ#BG49J3Pml>`U}2HGz{2t`3uA$@xqtZ${MY3ahVSM(wpHQuzCh}D@MIhUo- zp--Wv<&(zFd3FmA>>)u&^REENLHgrVbe=?d#kPY4H41f|0n#1{r9AvrlKl-aHP5H# z^e*}lNJ?0Q5JsB7#wF*C@8STLCwacU2@7Mr_Uy=;;o;%TG5GeWPW?x~mT0|H$~_^7)>-N9-D9moWXQvv z`LS$u5QDyNUz5$hv$-?|_>-G}nOG|>K0ah6E-p@&LX5;&SgX>FsUS2ol;z5mE5dA< z?HwID%U|2x`uQ=K6k$^Qe;_o2nwnZ+jyr|cP!aV;3-a`kzuZXQ+(Ek)P~cDBb55$J zX8q{hWc5QB45n$vab-&9u`<6*z&Wir|Eu9%-@FCp^esw&qtQ*0W%+25a-!r?%a4=a z!%MPukaO2~lRH1WXOjm1^s!zt>F47dSAdY66#D_Fyn#$s@N_}Hg{Qp`6H|@Vc_-*f zV{PXLnWDHIzZ-Q_BaP0NW?_K#pGYhBu(n!aL&)NLDn4tFl`&jdKDG zztyLG@g@~pHuhhmpO+fUmCOrYv6%_#x9Ky!r>MyE;WpaWKcfO+?H{?yOp;imJ?c(dy^D9l`ckuFbIv1FvM zb7_W>qGCN@!dQT_?M0S)&}{^?x3`N9{Uqz|o?&LoY!;XoTw845C~LrEl#M$Zk2eNq zJFFP06j{U>4VJ1Lr4wW(J129brVq{(wo`|5N$PL*76bzCL^w@CFU;Byz~)QNqWJ04 zo3l;Dz&=g(6=WC<6e~PCN=x;s-~j$xqWg#>h*HZOS0y03g_+k-UN&fFxMKF%O{Kze zLMACaJ)p5s$`n1T5%lKGX~<($b{PEkP}d9HRJDjHFJBMf&}5$ov-F-6<})1>L@1<( zt8)YQ?%g{>%Ww4>!2bHCG=!vm+Ia;91>FT^NOL|^M^ZLkjNjFmJD>8yruk4uw^jf1 zlG6Ss+3!mE%KM$!7lq*LNKX@_Ued0y+ZlpBfvlB(Yq?{r`lP0xH?2UvVW_>mJrk+o zP>mNg5*I*^?WwoSjV_(xNO|fm8y26LdB^BO)(sVNqn<~Wm#lOY?%r)1-aZMW#_67R;0{#Y1+XJNeKu%zB;Bcn!EpZRG1s#i`f;a>rLvm@3~4QbYHT)SJyBRQ@N`_qd6yhQYftw4n=yO zAT{QePkk-DD=Y7yUCa?0zE=6;yhh!pdU|@sSA-nD2VKgBt_M5HGdwcVbT2aDeUi_n z{&)H@9aj~Fu8(0Cy}W7#YbkW9vvdwLSGW56dkrDQu8sLK3QOYp;~FKeN!TQh-M7zI zgE{rQb1^s>lfo~A;xaM;z)^-^q<`ZYw~7g=%#n++{+T zg~t(kVYPgd0mJPK3N|!DR#sO0{(z&65&r&+k^!uTmW`2RL+aq3qgrJE3Yy9zUcWxI zwzj6b`yVLuh5TyQpPviuNg;EiPMzTV&Q7zLam$rS1pepEQ;Pq9OvWGfKjo$?&%grv z0TWHw0Fl->?eFhDzB5o_8`vB!AZcTh?-vm8OrQ@Aary?rj8~okmLd>{x%QG1-pP@u?bRXvK83^P#+^0qzG#>Oa0D`w#{};MQtLKRgS?DQdsWm9Z0%6GRlO zuQ^);<4_|Tmk;$r>FNI`wfDT+4Cr_NX4aBU@G&b$B)z>@q0DZ4B;0g8sewWQc zLCDo_eSNiyk3pJ{OHN5)=Tw$EQ!9UhO%$IWc*Z$o9bwQUc1(7z`=M%kNuW-Obo> z1N&=VQJ#fbMPPi6mlu~ioZPpEMMy{;f$mUEhY9yiWN=rd^awnNOXgwvJUbf^O77=W zRUt69zSpQT@$T`HCvUweI`X&0g~%DO!eWBRmN-Ja+u@M%W%9K5LW&6sE3{df7zv<{ zRHix(XQ^JOj)?LXR_xkGS835O;mu&eKiEq8bEo1P?Cg~-Rjg0oHdC}$^#JCOrxdVB zwH>H@v{!n}^usgaL2JkFGjE{l`ILG8!+SpfoNlc#`ymgz=@?=#(kRU0XkB~xRz(U1 z62fKcDftfCOrUq-H--QTNPqL7JV<~NW!ObJl(YPaY01!V2M}hkp>uCP(AH)IHlmR= z=w~QN3PUXP9>&+thOeKeyhpg!(PCncM<*si0cD&*UnKnhB-dbt`zc`&M3<+oV7xRZ zhx?uivH&Qu9O&El_Vx8dwJ{#%AOV-swuea6bDs2Lf#t(5T5nt|apDTnuT%Gf@iG24r+?_{aizs!leKXw}PxpQ5zO$mNK-Qt(HlulwC)wlG-{lTa z4tnKVR4|(oCykr#DJ9qXf zBjK+X>8!ZEIzOM|^4}EOaa9O-*iMPNV+uy!?$TsS^ zY(M<++{4jNZ!VjC7Cq0*{2H?VHAt{jslTf3lf%8I=6Y|K0YCXRIQYDe;NlD0El5mp z#l^j;t6w`3d>%f0c-!1O>j*7>ZdLwop)X)f-zHe!Af!V+)O_VWRp=rG{qzsPd0-oa`f3Kww0@M%VxpTD%`xPA=ijx|lU`yZh zqGB{RH@|A1aXa4D%en$NGlhzxX_+A1)qd7P&ubRr*)q87xE*h$%6HY|GXKpQ3Z_-| z)%&<^YRgTuQ7F+-#V)pDl{`;!1RS7ux>MsrZ;BOK?YA7S`JAkfGP*vUVcM|?u~Eq6 z7N@?C1SGPP>Q--T)upAqjC!(fM7XRQr~oxgBjVD}WuDQ8zzr#BX=AHUJW6hn`_g&K z<>0yx0xgmax5@Vz$`pF9jW5Pmv9wHYkGDj&^uC=f+COo6ta@Rr*)l7w!gKpatq4?l zq*?|47*`!y59kNV+5+J}aI3R@dgx3XZOG{>S5%%Ip?vU6SO0=rSO2#oU*?8qOmT}f z#Tf_A^hXMWvQThYY#xRR#vhzjR}+M0nbyd`*|L+24C8%Jx{^ zV!{0ngn(IjGUj`7ZXKoJI|Yf9Xn7WH%b6Oua}R2A+5^6l2!3;;jQDauvGi_yHQ4CODDNU#HPtoLoXMFgdW>)i~~3DpvZMjZulZ#<`ECa;iPf zb9oxqqNffWA_AHRR}l7!Jj-ODSp43-w^EV91N$EeeT5kgD&bzaEq$29t zC4Ka-E~^i}+}X3gt`nAiG}dIevviORIpLeUNbo|ZkfhEZ$8;wTI#;=(P~W~-Z< zPB{>VQRe)6qmSm@!9m-~oDk_!Y%6L@{9v;6NQ9gJU{j~K$z0*cQ4gRTuBY-CAg!t! zSty_uHH$jNc%maFr-lzwnS?6nO^!-m@rV#y#caCH>0@uUH#wLCI`0MvG+itDFkaILHJcutY{ylT^XJdM0XxEN&^4X6 znHU+XOR*=2-6n0OR4ZJ{7?_wO_}FOrvzJLol8Ng#C@3fhbp*a?0%cM?_5e@I^wfo}Wk96CP*gas)E6R~A*PZPxY0>HB4%t9ZZ13^|1D$Lg(&B(Dx@3z6H}0GczhTA(sXi&i|OB{6&(sOxW}HfLJso6+ds4?+hU&Md(mwk zqwden&Y+xHe6A$mRD^XFjm5{2+YJ}QD*>{zR6!Ne^*|>S3w95S55&B;EiAI7!uh+b z4$NTFLMOL*o!70%N*@ryF-Yt1(WN|!=;&3+85P@Tq&K1zb}9top=(p~Z&9&rKJp-X z?5?_o-Iu4jnzFZVzy6s3uM_>n+Wr;0|2~VT9DJns6%Ta3bNnH>1~~}MZH_`&GLYSW zduu}yN`?wF-b@JHzq(RiFA>aR9AY&;sLpRY#|q8)Jv5r?>n~nK0tyg6_#z$x0ggP{ zJwTH~WZZ-5&oN)N8e#AJ-+;xNgS$zObpM_?F+L!;A}4%=*Aor=^Q@;8$@)E{n~_%HA_3E(Xjwz^rM zV}h$=N=y3%4KJq7oejBV|1>>yxbF&EugS%HJ-NYQZQ97%+s8cSDFC#w`0lLr$@-OT7y8eBlGmi48PNAVdHEAs-Z4aD@-$EtqC%>31FQRtTp+X3PjGrD^k(H|Gci?fqKlwa6}L%78Od9GuPpw%4`aINH< z4zY`DP0EfHPpA7Lekm1|U|FOUyvWU+S2qiS#cu%#c+%)E^jG!C;d)0&4f<|E>0?br z=r5Kq^fM8mV@mw41t+B*jY_L(7<@DQ?ya1Z>;>GrX>jg709fP_G@8*IEV3+Sa5mRl z|4^m_$v&PaiZCblZQ(EUGrNm^w2EnX7=?ugSy${YgstC3HMsgIeQQWguBnLfs;Et> z=p%~P8kKfe`tr#!@yVj&)1}vohZ(&yTK&mah~ZKX9?~+vBmvVE^r**&=E?(In*Lk^ z-&EBC8DIo-raF`2xfKdb1{ltt7Y9PX>}_-**Xm&&O)b8Uj_S86FxvdjFUI=-1+%g z|JuI~v7e9K?}ZveKX(M>#^%tB9#JaZBC2^N$N`H7#;`y8vj3r~?@ zoyTBP-+*UB0+Lm~HBSAq`S5l7trgt)>yQbub~*iEgq({N_Z^)O*(nOMY8M||c}_wf zfVlb)_8E8^zejryNI-CwF4RzGgSRkq9Gd7;e!J&+v*aL9k&0mDbH>i^vNq1x)jtE1 z@5mKECi>J?r$O<}{6xf2ddjqHC16raSreoN2=uGPHnUSX!uWdShu01Kz)^FH_}RZJ zjurVOGW$bCaAk&2HTE=d>?#RPaOoDS1GbVNenE${&$#5zNc_p{9G4+FP{6wN!QIe( zvHT~wlTgdu?}=1g{7zLZab;q#(+d|a>{)l*<&Rp~A4A@F>&agHsU$r<{${>W&lyBv zMjlQu=CJGN=m@7}T2Z_Q&iTI)6uxwt&hb~YPtxkAeQX&##-fmSiJ)SBu1JNzbno_% zIgY+1=BC%%!a_ceCLKH^#*|FUXRZM;F`JDEPS|gU0jTlr1Si8DH)q5^V-J_PIlYw1)!ne_Jat0KqNywDntaH0ul{{el~eh^-J*mn9Wk zlH57OuWGy1nItETNrsjq7%SJ2Aj?~o0ye;B=W+0iN8iG8h&TR@Kef$=U-((xs(Fli zN0-zN*tWE^@B-^7n)`zI7y02fju#z}i1}Z$r8f;e*-hB}~!c>nJqYu{>3H%0F;&`{Y3ah=Y zLhkf>3_AtWGVp4j;sP};K#XusgGD12E~z~aqZFA7aC|&3zNRaL*HpuO1)O+(AXIlK z9NYFBT|_{D6{H`RI7hoX##)pfm*LRcbh2$0bkFvT-LUVmFanPGHs3Dn;pcL{kePtoty|-VqPF{Es$$D;e67 zVUmq7VtQ|o7LP8-(5x8DHL<;hKQCkH6jd&7Y~HSGK(kLh;c;5gv_^D8`rSwQMkIW0 zaqgZnz?bw>A~0ils;gmQ1_tpk9Q1hFgt{VNpDenYnAVR=Q?126<#~_W zpMJXzZ7l;hioj!|iKNDq+ekOjMsW8t_*X3umpELE!5&3O3%peyF z(AW^qgT+kuCDRa)PwS_;Qgp`Z{h79)-_;rZ`0{=eFVtt|Jd#N)v~jIxfcajrFLzqA z=s1pnBUr4B1vWtcqXQ1c><86{DX&<>ii0of6nIIvuVw%;xHk+pe7JQFr1{DRRGIfc zjpg*A`UHSHI5>Ea%XxkI&8t^O&a<;0RTYLKi0u+b0skPm*2w?Wx}A~=H1jZ>_z}pC zU9iuUE^KJijmVCE)sLrADk?KXz8r4^CWR4~D7thN4^r;&Q($44#`16)M0Q#~b}4+9 zEeC|cH$(Fk>P*tp$+PfWU}ZgGOOcwOT?`&1#5+W^xlYDjRX9hEw(x>R?cmq>Ls|r) ze!GGaLuBwN;p!=AgrQ`lRuBDOK=DB+ zq-Y@cFq5>lTML4Qoy>R(l})P{p10ZWvUMm>OJmhW)xv8r7iG$}OFx>^+=4!douh2| ztx<2TxWmfS*wQ+p_mOG=O${A{G4^NUhS1iSpX9r20og80yfB8RRC1sGmsInq1|y!B z?r0w1v`QF4vWt(csXHkNHC@(pCV`-~RZ9UMFyfA~597B^Hnws$v)&>@8e!jt02*u0Vl0%@G8F?U22yl_~WxXGRTqxyn2MMpWlg& zd()t+klfs*IZ&k0V!|&rfRF?ZCwb4ET}%KP{4GsE<@%S~flOp6G{ z{)N*Z+N%tf*72;|+|EX^uhA_Bj{MR3b!hazau<~QTz;aMsnn8)L2j&vgQ}kr$W|Yl za;;wx{-Hbn!aun;$V{)+!B5G`zYBV)t6OyXCXU8yW#osLGg<@v6Uh0@&frVdYouh| z--FW9M!__W2Gff?Xq8MWC0N|4UKEdb3s7x^{`g^+(>al3Hg#6SXcI>ARTJ&8_Wkt4nJ+Lgf>x&lV4>tK1!ewRsVR!h$(|EXGH8k+RSkKvH{} zDsSDVPp=Vhc-BYo4ETz82-3jOE(z`;si2Fe|1c#%Zaa`36n-cyw9itj!X&b*6?MMn z8em*-tbAk6SG(4`4hB+~pjcma`eCM0Vv~mSw7)$v$197$2wiO+60vfDiRmT?W2Qf! zjwr?ls($zy39YyGF9w5jAcu69*4X!MB9+@EuCGFibw6$NUz+#d6*aKe!+7Fo1Al|cBP>b4OCZw*h(GM2p8CwPH-uTfj z?Fmc4bW+Xi5jX*O*Vo@aB^s+c7wM-LVSf`)EmJtZ_1n}`)}+)_qoq=~l3a3KU=_GL zgVo&fxpf==$lV#HCaPP%GXEX^>@9 z6L&hq(*P3}P=rh_yFrxbQ1~n^S}ZB6s~ZR00qC*MR5i=HB}R2kb#OaCoq1I=({AzW zv(k5`fx*7fzD+dFT}}Mhejp|P;m(1dNs&FeiY1OMeX4yT0!QTwiU71*m`oSw6W#^| zF-K>cnSCo*1Mbf}2Vx~EAiSb4=jz?NcMEgeP?PIRx#O7`(v6-OVeS)p0$!P zv)?F&LNmPsY#+f(%R5t{rNMGW9JJ07wjxElKRu0sx22`Ieg4HJxxL@%2+DVhxZ8uSWb)vT}`%SOo^aKR;y&s?`=AOf9?ve zgNQ9fih*h)Y|ClQ^Zx#aXHU6J;qFIYCt~ID?5Tx^RTZ8Nf1rmy>3#l=@U;lzclPWx z#14wM17J|7Z(pu%O9UHY4!!?=pN+za#f0>H*Q#!wIzSlF*LO`!0yZQi0 z7>v5Y^35a+=2gu`fcv{*UwVv6%Vv| z{T&!)e?oB(H}kmB@L*;Be8dkMZ&n(bFykJ#@fFyasM#)Ob4EGSeQVzYLAMLpu{STl zn}XkdADsUAv+G()wXkKP9}!H$SV*bH#Z#Ac;B?)_!GyEB4@FD|g)(^U z7GdZ?5{N@Jv#R}zopQ2upR24sL4u0XczWCT=@g}V!k-JDcIQzmW@>Y9e{2(4YwLSK z1rjxgL$GuOtIFgCMmR=b^$b&x8S(4;rW63C_U5XfreB_mc&%z$rhsVuNZX6IWkc`2 zR?XqkrifTyUmWaAK}ahDU-l=pKJjtP0%5DCxXYU$F<4%uu8*5$xENdin^RVByvEE0*Iy%dCC` zwWvS;xDnS@f(dB6`(511g&PAGHfu?VbSf|X`g4k)E2Y<8)9@p&rI?7ud@BQD!^UWmR;)}l2jkCy~OFzuEKqZDrh6l7ZO|cUr7#PgfTJd3Qx6qeV_mjyl!{p$ya^J#Y)$nQZp- zqwS_(=BW43X4&K;InUwEu^QnC8U2B805&BgMYZVIP{LewIWjwdc&=I6zpGoIVBeG< z%Ydvwyh%No_uX5f)w{c))~7plr;#~a7=MOm&0Pm<&`7hy z$#NPVghVB%Cr^ajtbq!sgYiU01R6q3`Q%2h_u*`B-rKithgW%WCpSPhq~dyXV2qsE zo2{Njo-dy{T$|K2!4Sffd8*P3+GWSyA8jSMqVg;=wZ# z6Te08Umf)QLt5&VP27lE^3b6}?J>7Vih#2pgA*4v*j~p~)awMQ>$byPX@J^FZKMaC zUVDNpz3({8V+JCVjyP*4hw$)QLoon95K9)^pB3??)ruW;@I5eqpAjLqf&tglfQo9A z<+|hIo1jtV5Zq!0RMU?BZM47Mod4wKD{^|YPw#k@wa_# z^{mWt(b=j1Hr0pyh33dR5H4N1lx^6R{MxBt-$*HO!EgNNxKqf*`;TXOauTX1#8krr zzvROx#^g{%#T9ldN*<%2()Fp7LEQP2csAwC$}k^LT>$b)Wv0(&y1U$(E>g2?TcWnf z-YWK}X$lu8sIre=5x+iX>XZ28NfoFhUw~#5*jSyn)o|Ui)&mcK?gA3}zHtAgLb&RB z${Y(|P-C+OX^WP?ujSD3bqMVC%_@av(M#JK^8zHoKcdJ^o}55zMuS}7p)~{5D_kK( z@ydMZk~iE^9ABBkibeU_IJIdAxY&A&K0hW3OH(c2cLjS`SWYv4L(!LK)HJHdbu@#~ zH4Jo`EA+0LSxZ2{3LvYFA2aT_qp|b7^%_a=2n)ExW;WKh*IHcp0hL&SfD;aEYu|ZR zRx26dwTFnAHFVrpR@fX6Tocc7w*xTDjam6NvuM+hlNxqah>Lc~Q*-mZJ;=XdkTJU^ zd-GD{(C8`YrLB3_A#qt*TSQq0bKeI_eyc>wI>taglkt`b?|lwxn_q8udHdv zc-ZKY!^Lvd$JT?-RG~LQ2D^kmfN4I=)ODjz2J(s}m<%ggp5_a3e3%6vT1<{^6V=2A z5)msf#f=DltK6#_X}EU2KG_^5;o>{sy$=Z>fi;5)JgY8if8)89THk#i7y=mx1|QE7 zjNdv07UPa|e;~#waG5lRU1c| zg(2p~q~zqq_#zl?4@Vpr8=HR}c&6H3WXvx1c6K;OSPG8t2uWYAIdf`1p*TyiTt!2O)s=2d{ zi#M=SQwvj3Qd46EBCrS~y{b@l7M2Dp$u*d&`yw49Ju%hJRje`FS0D=vxl=pCxpSfM z84+CTiPCKch^a=UVX|V z&6XE|`5= z`16UE-fYBDo%AU&CR7Cr6PCB!wVaVOmM3j&oJJY>74fD*|FGuFp4(+_;AuPLjC-{| zUfbSzXJaTZP{TpIS!h+QKKU5cXGicoHY#IQ%#EVwvdrGW)r}zb!z%^gN6??j@e%qS z)DK_((o8XB-eFBnY+!b+_Ar#wG)h-F_f!0HQ!wYpoPKa~_L%fzib{CFhobwV;VBLX z#i7zkYqKUql)-{k9x9w~s7Z0%5g8Et&>BPbReBM@y( zBxGE(VV2Er;6TAxJKKKq=irx68Fbxo_DGFr5#EqO-o4Txp?ZMi=EE<&bY&Zy=g!?i z-X+2r{giM4mT0H1KP)Xd`S5Bj_$!ftHPf3yEx9+6W%dAi(EN?m|=H`}K1=JufBuNht+30C;YQN!NpO>2&60?>n;^l$rXO%F7jHE-whrysZ*`#EDm&&jlM9NE9#R4wnsG3wzuqylvOqR z?$2*`2Apy5>I^R6t|Sq-Gw;cYKF~2V&>MQ-&1&D-kbPCR(pU99t&PlX zUu}23%j*^JNO(224QDM?P;6jjbas|~T6~0O+i6apG}V&vkBKzm+_6-`j9lt4mz!*n z#hX`7_D+@N`j5+Hxcd2Lq@)+u)12%@H|2aSwBYxCKCwfiI@g88*>snPtTB|(MH}AAOkdc z(~fnUO6rtMo)GlwdO#61!}MghoXnMKz2sS0;kA!6rmKUF8CUXwG7Ptw?G4VQbC8;9 zBmt)%FK6=r4C#$4rwHtQD>EPQ=FOK)sM$7P!TSOVY$NN>l3Hr#O}iD+IoY?e57P$u-wk&_W}yuBMlu1{Oj4@N7ek z0hNR2Grk^sg%OzfRzqzXM0wC!#Cda}wi&E!rr*1yc?V9+rA8k!DNKK74!x02QmBFR zwP|0VL7Rc8G1B$C`EVPmr!ie+`yP65Aawv0w)C_c3bNdN+-E6u1kG>)o#l%+Y4;ec zl!1QT3{JmUq;5!MltWzy5L#b{y#ImzWic)B9$=|!&o#l2ma&Vx;nUj*ssMsQ%gE2D z#=pY47iPt=*H3GL3fQl&V%%TYrm9HAN6V@Esskvdr5ov*-w>*EENK#0MlJ6&&St(3 zGtbw#NK-`WxxHjKQ>%K4mx;zeVYZUtpJZ72umPH4y6oq+Gz3{bn%k-IJ}XrIcx?dU zm#05u-6h$v`G+NvfGJIovCLpGjim&U-sSFveHkZoy0TFeW8}1tt&MN}n1y%1P+orc zqG;jG>(`$VTbun%R&6P7)_wZEj1+yuj57fw@`oATH0`M+U_4$7dwhn%dgkWNAzPM< z2*QVkbW~ewmmx+)iz3Vhvi&wP@^^gQ|1zoY&vy4wcZZ(%Qtrt! Date: Mon, 12 Jan 2026 06:51:50 -0500 Subject: [PATCH 15/22] Docs: Replace np.allclose with dataframe output in inverse transform demo --- .../transformation/ArcSinhTransformer.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index d1e729d86..94e9bda87 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -174,8 +174,18 @@ the original values: train_t = tf.transform(X_train) train_recovered = tf.inverse_transform(train_t) - # Values should match original - np.allclose(X_train['profit'], train_recovered['profit']) + print(train_recovered.head()) + +The recovered data: + +.. code:: python + + profit net_worth + 105 4040.508568 -51995.296356 + 68 3616.360250 -23385.060066 + 479 11195.749114 -21872.915016 + 399 12378.163120 -32844.713949 + 434 15023.570521 -8356.085689 API Reference ------------- From 6a8fc6491c92e760cfba568d833d7af8a16477c8 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:52:45 -0500 Subject: [PATCH 16/22] Docs: Remove API Reference from User Guide (exists in api_doc) --- docs/user_guide/transformation/ArcSinhTransformer.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index 94e9bda87..bc12df73a 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -187,9 +187,4 @@ The recovered data: 399 12378.163120 -32844.713949 434 15023.570521 -8356.085689 -API Reference -------------- -.. autoclass:: ArcSinhTransformer - :members: - :inherited-members: From bdcb2713a360d761bc526da6bfac99e2c51b4825 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:53:40 -0500 Subject: [PATCH 17/22] Docstring: Clarify linear behavior of arcsinh for small x --- feature_engine/transformation/arcsinh.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/feature_engine/transformation/arcsinh.py b/feature_engine/transformation/arcsinh.py index 0e51e68e0..3f6f47bdc 100644 --- a/feature_engine/transformation/arcsinh.py +++ b/feature_engine/transformation/arcsinh.py @@ -46,8 +46,9 @@ class ArcSinhTransformer(BaseNumericalTransformer): For large values of x, arcsinh(x) behaves like ln(x) + ln(2), providing similar variance-stabilizing properties as the log transformation. For small values of x, - it behaves approximately linearly. This makes it ideal for variables - like net worth, profit/loss, or any metric that can be positive or negative. + it behaves approximately linearly (i.e., arcsinh(x) ≈ x). This makes it ideal for + variables like net worth, profit/loss, or any metric that can be positive or + negative. A list of variables can be passed as an argument. Alternatively, the transformer will automatically select and transform all variables of type numeric. From 73b9ef140cee38c322d2b7a6aad7acda627f34c0 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:54:26 -0500 Subject: [PATCH 18/22] Docstring: Remove redundant 'does not learn parameters' sentence from fit --- feature_engine/transformation/arcsinh.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/feature_engine/transformation/arcsinh.py b/feature_engine/transformation/arcsinh.py index 3f6f47bdc..e0020ff86 100644 --- a/feature_engine/transformation/arcsinh.py +++ b/feature_engine/transformation/arcsinh.py @@ -143,8 +143,6 @@ def __init__( def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): """ - This transformer does not learn parameters. - Selects the numerical variables and stores feature names. Parameters From f055b0525851e3165fb23cb2bc70aa044b4e8e7c Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:55:13 -0500 Subject: [PATCH 19/22] Tests: Add explicit value assertions for negative values in ArcSinh test --- tests/test_transformation/test_arcsinh.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_transformation/test_arcsinh.py b/tests/test_transformation/test_arcsinh.py index 621ad0664..081f0b408 100644 --- a/tests/test_transformation/test_arcsinh.py +++ b/tests/test_transformation/test_arcsinh.py @@ -112,12 +112,11 @@ def test_negative_values(): transformer = ArcSinhTransformer() X_tr = transformer.fit_transform(X.copy()) - assert X_tr["a"].iloc[0] < 0 - assert X_tr["a"].iloc[1] < 0 - assert X_tr["a"].iloc[2] == 0 - assert X_tr["a"].iloc[3] > 0 - assert X_tr["a"].iloc[4] > 0 + # Expected values: arcsinh([ -1000, -500, 0, 500, 1000 ]) + expected = [-7.600902, -6.907755, 0.0, 6.907755, 7.600902] + np.testing.assert_array_almost_equal(X_tr["a"], expected, decimal=5) + # Verify symmetry property: arcsinh(-x) = -arcsinh(x) np.testing.assert_almost_equal( X_tr["a"].iloc[0], -X_tr["a"].iloc[4], decimal=10 ) From 3f17f04cb2369ba2e3b64d51a22f7cd8215bdbb4 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:55:47 -0500 Subject: [PATCH 20/22] Tests: Add string and boolean to invalid_scale parameterization --- tests/test_transformation/test_arcsinh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transformation/test_arcsinh.py b/tests/test_transformation/test_arcsinh.py index 081f0b408..a3d8b8d4d 100644 --- a/tests/test_transformation/test_arcsinh.py +++ b/tests/test_transformation/test_arcsinh.py @@ -125,7 +125,7 @@ def test_negative_values(): ) -@pytest.mark.parametrize("invalid_scale", [0, -1, -0.5, -100]) +@pytest.mark.parametrize("invalid_scale", [0, -1, -0.5, -100, "string", False]) def test_invalid_scale_raises_error(invalid_scale): """Test that non-positive scale values raise ValueError.""" with pytest.raises(ValueError, match="scale must be a positive number"): From 3be70047f226497b8d4e18673660c6957ee86651 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Mon, 12 Jan 2026 06:56:15 -0500 Subject: [PATCH 21/22] Docs: Add practical explanation for using loc and scale parameters --- docs/user_guide/transformation/ArcSinhTransformer.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/user_guide/transformation/ArcSinhTransformer.rst b/docs/user_guide/transformation/ArcSinhTransformer.rst index bc12df73a..e5fe01053 100644 --- a/docs/user_guide/transformation/ArcSinhTransformer.rst +++ b/docs/user_guide/transformation/ArcSinhTransformer.rst @@ -148,7 +148,12 @@ Using loc and scale parameters ------------------------------ :class:`ArcSinhTransformer()` supports location and scale parameters to -center and normalize data before transformation: +center and normalize data before transformation. + +In practice, it is common to standardize the variable (zero mean, unit variance) +so that the center of the distribution falls in the linear region of the arcsinh +function, while the tails are compressed logarithmically. We can achieve this +by setting ``loc`` to the mean and ``scale`` to the standard deviation: .. code:: python From f990fde7b89d4a51378461052a0226492103f602 Mon Sep 17 00:00:00 2001 From: ankitlade12 Date: Wed, 14 Jan 2026 00:34:29 -0500 Subject: [PATCH 22/22] Docs: Add ArcSinhTransformer to api_doc index and update description --- docs/api_doc/transformation/index.rst | 1 + docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api_doc/transformation/index.rst b/docs/api_doc/transformation/index.rst index 0705f4d0a..32866842d 100644 --- a/docs/api_doc/transformation/index.rst +++ b/docs/api_doc/transformation/index.rst @@ -13,6 +13,7 @@ mathematical transformations. LogCpTransformer ReciprocalTransformer ArcsinTransformer + ArcSinhTransformer PowerTransformer BoxCoxTransformer YeoJohnsonTransformer diff --git a/docs/index.rst b/docs/index.rst index d73005b8f..a7266bdc1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -237,7 +237,7 @@ like anova, and machine learning models, like linear regression. Feature-engine - :doc:`api_doc/transformation/BoxCoxTransformer`: performs Box-Cox transformation of numerical variables - :doc:`api_doc/transformation/YeoJohnsonTransformer`: performs Yeo-Johnson transformation of numerical variables - :doc:`api_doc/transformation/ArcsinTransformer`: performs arcsin transformation of numerical variables -- :doc:`api_doc/transformation/ArcSinhTransformer`: applies arcsinh (pseudo-logarithm) transformation for data with positive and negative values +- :doc:`api_doc/transformation/ArcSinhTransformer`: applies arcsinh (pseudo-logarithm) transformation of numerical variables Feature Creation: ~~~~~~~~~~~~~~~~~