diff --git a/src/safeds/data/tabular/transformation/__init__.py b/src/safeds/data/tabular/transformation/__init__.py index a837d3165..bf705e10d 100644 --- a/src/safeds/data/tabular/transformation/__init__.py +++ b/src/safeds/data/tabular/transformation/__init__.py @@ -6,6 +6,7 @@ if TYPE_CHECKING: from ._discretizer import Discretizer + from ._functional_table_transformer import FunctionalTableTransformer from ._invertible_table_transformer import InvertibleTableTransformer from ._k_nearest_neighbors_imputer import KNearestNeighborsImputer from ._label_encoder import LabelEncoder @@ -16,10 +17,12 @@ from ._standard_scaler import StandardScaler from ._table_transformer import TableTransformer + apipkg.initpkg( __name__, { "Discretizer": "._discretizer:Discretizer", + "FunctionalTableTransformer": "._functional_table_transformer:FunctionalTableTransformer", "InvertibleTableTransformer": "._invertible_table_transformer:InvertibleTableTransformer", "LabelEncoder": "._label_encoder:LabelEncoder", "OneHotEncoder": "._one_hot_encoder:OneHotEncoder", @@ -34,6 +37,7 @@ __all__ = [ "Discretizer", + "FunctionalTableTransformer", "InvertibleTableTransformer", "LabelEncoder", "OneHotEncoder", diff --git a/src/safeds/data/tabular/transformation/_functional_table_transformer.py b/src/safeds/data/tabular/transformation/_functional_table_transformer.py new file mode 100644 index 000000000..7c4add23c --- /dev/null +++ b/src/safeds/data/tabular/transformation/_functional_table_transformer.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from safeds._utils import _structural_hash + +if TYPE_CHECKING: + from collections.abc import Callable + + from safeds.data.tabular.containers import Table + +from ._table_transformer import TableTransformer + + +class FunctionalTableTransformer(TableTransformer): + """ + Wraps a callable so that it conforms to the TableTransformer interface. + + Parameters + ---------- + transformer: + A callable that receives a table and returns a table. + """ + + # ------------------------------------------------------------------------------------------------------------------ + # Dunder methods + # ------------------------------------------------------------------------------------------------------------------ + + def __init__( + self, + transformer: Callable[[Table], Table], + ) -> None: + super().__init__(None) + self._transformer = transformer + + def __hash__(self) -> int: + return _structural_hash( + super().__hash__(), + self._transformer, + ) + + # ------------------------------------------------------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------------------------------------------------------ + + @property + def is_fitted(self) -> bool: + """FunctionalTableTransformer is always considered to be fitted.""" + return True + + # ------------------------------------------------------------------------------------------------------------------ + # Learning and transformation + # ------------------------------------------------------------------------------------------------------------------ + + def fit(self, table: Table) -> FunctionalTableTransformer: # noqa: ARG002 + """ + **Note:** For FunctionalTableTransformer this is a no-OP. + + Parameters + ---------- + table: + Required only to be consistent with other transformers. + + Returns + ------- + fitted_transformer: + Returns self, because this transformer is always fitted. + """ + return self + + def transform(self, table: Table) -> Table: + """ + Apply the callable to a table. + + **Note:** The given table is not modified. + + Parameters + ---------- + table: + The table on which on which the callable is executed. + + Returns + ------- + transformed_table: + The transformed table. + + Raises + ------ + Exception: + Raised when the wrapped callable encounters an error. + """ + return self._transformer(table) + + def fit_and_transform(self, table: Table) -> tuple[FunctionalTableTransformer, Table]: + """ + **Note:** For the FunctionalTableTransformer this is the same as transform(). + + Parameters + ---------- + table: + The table on which the callable is to be executed. + + Returns + ------- + fitted_transformer: + Return self because the transformer is always fitted. + transformed_table: + The transformed table. + """ + fitted_transformer = self + transformed_table = self.transform(table) + return fitted_transformer, transformed_table diff --git a/tests/safeds/data/tabular/transformation/test_functional_table_transformer.py b/tests/safeds/data/tabular/transformation/test_functional_table_transformer.py new file mode 100644 index 000000000..44f510438 --- /dev/null +++ b/tests/safeds/data/tabular/transformation/test_functional_table_transformer.py @@ -0,0 +1,102 @@ +import pytest +from safeds.data.tabular.containers import Table +from safeds.data.tabular.transformation import FunctionalTableTransformer +from safeds.exceptions import ColumnNotFoundError + + +def valid_callable(table: Table) -> Table: + return table.remove_columns(["col1"]) + + +class TestInit: + def test_should_not_raise_type_error(self) -> None: + FunctionalTableTransformer(valid_callable) + + +class TestFit: + def test_should_return_self(self) -> None: + table = Table( + { + "col1": [1, 2, 3], + "col2": [1, 2, 3], + }, + ) + transformer = FunctionalTableTransformer(valid_callable) + assert transformer.fit(table) is transformer + + +class TestIsFitted: + def test_should_always_be_fitted(self) -> None: + transformer = FunctionalTableTransformer(valid_callable) + assert transformer.is_fitted + + +class TestTransform: + def test_should_raise_specific_error_when_error_in_method(self) -> None: + table = Table( + { + "col2": [1, 2, 3], + }, + ) + transformer = FunctionalTableTransformer(valid_callable) + with pytest.raises(ColumnNotFoundError): + transformer.transform(table) + + def test_should_not_modify_original_table(self) -> None: + table = Table( + { + "col1": [1, 2, 3], + "col2": [1, 2, 3], + }, + ) + transformer = FunctionalTableTransformer(valid_callable) + transformer.transform(table) + assert table == Table( + { + "col1": [1, 2, 3], + "col2": [1, 2, 3], + }, + ) + + def test_should_return_modified_table(self) -> None: + table = Table( + { + "col1": [1, 2, 3], + "col2": [1, 2, 3], + }, + ) + transformer = FunctionalTableTransformer(valid_callable) + transformed_table = transformer.transform(table) + assert transformed_table == Table( + { + "col2": [1, 2, 3], + }, + ) + + +class TestFitAndTransform: + def test_should_return_self(self) -> None: + table = Table( + { + "col1": [1, 2, 3], + "col2": [1, 2, 3], + }, + ) + transformer = FunctionalTableTransformer(valid_callable) + assert transformer.fit_and_transform(table)[0] is transformer + + def test_should_not_modify_original_table(self) -> None: + table = Table( + { + "col1": [1, 2, 3], + "col2": [1, 2, 3], + }, + ) + transformer = FunctionalTableTransformer(valid_callable) + transformer.fit_and_transform(table) + assert table == Table( + { + "col1": [1, 2, 3], + "col2": [1, 2, 3], + }, + ) diff --git a/tests/safeds/data/tabular/transformation/test_table_transformer.py b/tests/safeds/data/tabular/transformation/test_table_transformer.py index d03b157e1..0f741092d 100644 --- a/tests/safeds/data/tabular/transformation/test_table_transformer.py +++ b/tests/safeds/data/tabular/transformation/test_table_transformer.py @@ -4,6 +4,7 @@ from safeds.data.tabular.containers import Table from safeds.data.tabular.transformation import ( Discretizer, + FunctionalTableTransformer, KNearestNeighborsImputer, LabelEncoder, OneHotEncoder, @@ -51,6 +52,10 @@ def transformers_non_numeric() -> list[TableTransformer]: ] +def valid_callable_for_functional_table_transformer(table: Table) -> Table: + return table.remove_columns(["col1"]) + + def transformers() -> list[TableTransformer]: """ Return the list of all transformers to test. @@ -69,6 +74,7 @@ def transformers() -> list[TableTransformer]: + [ SimpleImputer(strategy=SimpleImputer.Strategy.mode()), KNearestNeighborsImputer(neighbor_count=3, value_to_replace=None), + FunctionalTableTransformer(valid_callable_for_functional_table_transformer), ] )