Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add FunctionalTableTransformer #901

Merged
merged 19 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/safeds/data/tabular/transformation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -34,6 +37,7 @@

__all__ = [
"Discretizer",
"FunctionalTableTransformer",
"InvertibleTableTransformer",
"LabelEncoder",
"OneHotEncoder",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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):
"""
Learn a transformation for a set of columns in a `Table` and transform another `Table` with the same columns.
lars-reimann marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
funct:
The Callable that receives a table and returns a table that is to be wrapped.
"""

# ------------------------------------------------------------------------------------------------------------------
# Dunder methods
# ------------------------------------------------------------------------------------------------------------------

def __init__(
self,
funct: Callable[[Table], Table],
) -> None:
super().__init__(None)
self._func = funct
Tarmandan marked this conversation as resolved.
Show resolved Hide resolved

def __hash__(self) -> int:
return _structural_hash(

Check warning on line 37 in src/safeds/data/tabular/transformation/_functional_table_transformer.py

View check run for this annotation

Codecov / codecov/patch

src/safeds/data/tabular/transformation/_functional_table_transformer.py#L37

Added line #L37 was not covered by tests
super().__hash__(),
self._func,
)

# ------------------------------------------------------------------------------------------------------------------
# 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.

"""
try:
return self._func(table)
except Exception as e:
# TODO Evaluate if switch to non-generic exception is useful, as _func can be any callable
raise Exception("The underlying function encountered an error") from e # noqa: TRY002
Tarmandan marked this conversation as resolved.
Show resolved Hide resolved

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import pytest
from safeds.data.tabular.containers import Table
from safeds.data.tabular.transformation import FunctionalTableTransformer


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_generic_error_when_error_in_method(self) -> None:
table = Table(
{
"col2": [1, 2, 3],
},
)
transformer = FunctionalTableTransformer(valid_callable)
with pytest.raises(Exception, match=r"The underlying function encountered an error"):
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],
},
)