Skip to content

API: preprocessing.encoding

skyulf.preprocessing.encoding

Encoding nodes package.

Split from a single 832-LOC module into per-encoder files

_common.py — shared helpers (detect_categorical_columns, _exclude_target_column, …) one_hot.py — OneHotEncoder ordinal.py — OrdinalEncoder label.py — LabelEncoder target.py — TargetEncoder hash.py — HashEncoder dummy.py — DummyEncoder woe.py — WOEEncoder (Weight-of-Evidence / Information Value)

All public names are re-exported here so existing imports such as from skyulf.preprocessing.encoding import OneHotEncoderCalculator continue to work unchanged.

DummyEncoderApplier

Bases: BaseApplier

Replace each categorical column with one <col>_<category> indicator column.

The originals are always dropped. Parity depends on both engines rendering a value to the same string before comparing it with the learned categories — one rule, implemented per engine by _pandas_col_to_str and _polars_col_to_str_expr, which is a function of the value alone so a category cannot stop matching because of what else shares its batch. A value unseen at fit time yields an all-zero row rather than raising. Generated names that duplicate another indicator or a retained input column raise ValueError before output construction.

Version 1 artifacts render booleans and datetimes consistently across engines, including nanoseconds and timezone-aware instants. Midnight naive datetimes share date keys. Literal strings are unchanged. Unversioned artifacts retain their original rendering; refit the encoder and downstream model to obtain the portable category contract.

Source code in skyulf-core/skyulf/preprocessing/encoding/dummy.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
class DummyEncoderApplier(BaseApplier):
    """Replace each categorical column with one ``<col>_<category>`` indicator column.

    The originals are always dropped. Parity depends on both engines rendering
    a value to the *same* string before comparing it with the learned
    categories — one rule, implemented per engine by ``_pandas_col_to_str``
    and ``_polars_col_to_str_expr``, which is a function of the value alone so
    a category cannot stop matching because of what else shares its batch. A
    value unseen at fit time yields an all-zero row rather than raising.
    Generated names that duplicate another indicator or a retained input
    column raise ``ValueError`` before output construction.

    Version 1 artifacts render booleans and datetimes consistently across engines,
    including nanoseconds and timezone-aware instants. Midnight naive datetimes
    share date keys. Literal strings are unchanged. Unversioned artifacts retain
    their original rendering; refit the encoder and downstream model to obtain
    the portable category contract.
    """

    @staticmethod
    def validate_inference_state(raw: dict) -> dict:
        """Inspect saved categories without deriving a vocabulary from inference rows."""
        fields = {"type", "columns", "categories", "drop_first"}
        if isinstance(raw, dict) and "category_key_version" in raw:
            fields.add("category_key_version")
        if not local_state_fields(raw, "dummy_encoder", fields, allow_empty=True):
            return raw
        local_boolean(raw["drop_first"], "drop_first")
        uses_category_keys(raw)
        _validate_dummy_categories(raw)
        return raw

    @staticmethod
    def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
        """Keep legacy pandas batch rendering visible in the local context declaration."""
        if engine not in ("pandas", "polars"):
            return None
        DummyEncoderApplier.validate_inference_state(state)
        legacy = state.get("columns") and not uses_category_keys(state)
        context = "global" if engine == "pandas" and legacy else "row"
        return ExecutionCapability(engine, "apply", "local", "preserve", context)

    @apply_method
    def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
        """Dispatch to the engine-specific dummy encoding, forwarding ``(X, y)`` when present."""
        return apply_dual_engine(
            (X, y) if y is not None else X,
            params,
            {"polars": _dummy_apply_polars, "pandas": _dummy_apply_pandas},
        )

apply(X, y, params)

Dispatch to the engine-specific dummy encoding, forwarding (X, y) when present.

Source code in skyulf-core/skyulf/preprocessing/encoding/dummy.py
253
254
255
256
257
258
259
260
@apply_method
def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
    """Dispatch to the engine-specific dummy encoding, forwarding ``(X, y)`` when present."""
    return apply_dual_engine(
        (X, y) if y is not None else X,
        params,
        {"polars": _dummy_apply_polars, "pandas": _dummy_apply_pandas},
    )

inference_capability(state, *, engine) staticmethod

Keep legacy pandas batch rendering visible in the local context declaration.

Source code in skyulf-core/skyulf/preprocessing/encoding/dummy.py
243
244
245
246
247
248
249
250
251
@staticmethod
def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
    """Keep legacy pandas batch rendering visible in the local context declaration."""
    if engine not in ("pandas", "polars"):
        return None
    DummyEncoderApplier.validate_inference_state(state)
    legacy = state.get("columns") and not uses_category_keys(state)
    context = "global" if engine == "pandas" and legacy else "row"
    return ExecutionCapability(engine, "apply", "local", "preserve", context)

validate_inference_state(raw) staticmethod

Inspect saved categories without deriving a vocabulary from inference rows.

Source code in skyulf-core/skyulf/preprocessing/encoding/dummy.py
230
231
232
233
234
235
236
237
238
239
240
241
@staticmethod
def validate_inference_state(raw: dict) -> dict:
    """Inspect saved categories without deriving a vocabulary from inference rows."""
    fields = {"type", "columns", "categories", "drop_first"}
    if isinstance(raw, dict) and "category_key_version" in raw:
        fields.add("category_key_version")
    if not local_state_fields(raw, "dummy_encoder", fields, allow_empty=True):
        return raw
    local_boolean(raw["drop_first"], "drop_first")
    uses_category_keys(raw)
    _validate_dummy_categories(raw)
    return raw

DummyEncoderCalculator

Bases: BaseCalculator

Learn the sorted category list for each resolved categorical column.

The target column is excluded first: dummy encoding replaces the column it encodes with several derived ones, which would break a downstream Feature/Target Split. Nulls and float NaN are dropped before the list is built, so they never become a category. An explicit columns: [] yields an empty artifact so the applier no-ops.

Source code in skyulf-core/skyulf/preprocessing/encoding/dummy.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
@NodeRegistry.register("DummyEncoder", DummyEncoderApplier)
@node_meta(
    id="DummyEncoder",
    name="Dummy Encoder",
    category="Preprocessing",
    description="Convert categorical variables into dummy/indicator variables (pandas.get_dummies).",
    params={"columns": [], "drop_first": False},
    learns_from_data=True,
)
class DummyEncoderCalculator(BaseCalculator):
    """Learn the sorted category list for each resolved categorical column.

    The target column is excluded first: dummy encoding replaces the column it
    encodes with several derived ones, which would break a downstream
    Feature/Target Split. Nulls and float NaN are dropped before the list is
    built, so they never become a category. An explicit ``columns: []`` yields
    an empty artifact so the applier no-ops.
    """

    @fit_method
    def fit(self, X: Any, y: Any, config: dict[str, Any]) -> DummyEncoderArtifact:  # pylint: disable=arguments-differ
        """Short-circuit an explicit empty column selection, else learn the categories."""
        if user_picked_no_columns(config):
            return {}
        return cast(
            DummyEncoderArtifact,
            fit_dual_engine(
                (X, y) if y is not None else X,
                config,
                {"polars": _dummy_fit_polars, "pandas": _dummy_fit_pandas},
            ),
        )

fit(X, y, config)

Short-circuit an explicit empty column selection, else learn the categories.

Source code in skyulf-core/skyulf/preprocessing/encoding/dummy.py
332
333
334
335
336
337
338
339
340
341
342
343
344
@fit_method
def fit(self, X: Any, y: Any, config: dict[str, Any]) -> DummyEncoderArtifact:  # pylint: disable=arguments-differ
    """Short-circuit an explicit empty column selection, else learn the categories."""
    if user_picked_no_columns(config):
        return {}
    return cast(
        DummyEncoderArtifact,
        fit_dual_engine(
            (X, y) if y is not None else X,
            config,
            {"polars": _dummy_fit_polars, "pandas": _dummy_fit_pandas},
        ),
    )

FrequencyEncoderApplier

Bases: BaseApplier

Replay fixed category statistics without recomputing inference-batch frequencies.

Source code in skyulf-core/skyulf/preprocessing/encoding/frequency.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
class FrequencyEncoderApplier(BaseApplier):
    """Replay fixed category statistics without recomputing inference-batch frequencies."""

    @staticmethod
    def validate_inference_state(raw: dict) -> dict:
        """Check exact learned counts and dimensions before granting local replay."""
        fields = {"type", "columns", "mode", "category_key_version", "n_samples", "mappings"}
        if not local_state_fields(raw, "frequency_encoder", fields, allow_empty=True):
            return raw
        uses_category_keys(raw)
        _mode(raw["mode"])
        columns = raw["columns"]
        if type(columns) is not list or not columns or any(type(c) is not str for c in columns):
            raise ValueError("Fitted frequency columns must be a nonempty ordered string list.")
        if len(columns) != len(set(columns)):
            raise ValueError("Fitted frequency columns must be unique.")
        FrequencyEncoderApplier._validate_counts(raw)
        return raw

    @staticmethod
    def _validate_counts(raw: dict) -> None:
        """Each column counts the same positive training population exactly once."""
        n_samples = raw["n_samples"]
        if type(n_samples) is not int or n_samples < 1:
            raise ValueError("Fitted frequency n_samples must be a positive integer.")
        mappings = raw["mappings"]
        if type(mappings) is not dict or set(mappings) != set(raw["columns"]):
            raise ValueError("Fitted frequency mappings must match selected columns.")
        for mapping in mappings.values():
            FrequencyEncoderApplier._validate_column_counts(mapping, n_samples)

    @staticmethod
    def _validate_column_counts(mapping: Any, n_samples: int) -> None:
        """Inspect one categorical histogram without casting or correcting its entries."""
        if type(mapping) is not dict or any(type(key) is not str for key in mapping):
            raise ValueError("Fitted frequency categories must be string keys.")
        if any(type(count) is not int or count < 1 for count in mapping.values()):
            raise ValueError("Fitted frequency counts must be positive integers.")
        if sum(mapping.values()) != n_samples:
            raise ValueError("Fitted frequency counts disagree with n_samples.")

    @staticmethod
    def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
        """Describe validated row-local lookups without granting worker execution."""
        if engine not in ("pandas", "polars"):
            return None
        FrequencyEncoderApplier.validate_inference_state(state)
        return ExecutionCapability(engine, "apply", "local", "preserve", "row")

    @apply_method
    def apply(self, X: Any, y: Any, params: dict) -> Any:
        """Replace selected values while retaining row order, index, and target."""
        return apply_dual_engine(
            (X, y) if y is not None else X, params, {"pandas": _apply, "polars": _apply}
        )

apply(X, y, params)

Replace selected values while retaining row order, index, and target.

Source code in skyulf-core/skyulf/preprocessing/encoding/frequency.py
122
123
124
125
126
127
@apply_method
def apply(self, X: Any, y: Any, params: dict) -> Any:
    """Replace selected values while retaining row order, index, and target."""
    return apply_dual_engine(
        (X, y) if y is not None else X, params, {"pandas": _apply, "polars": _apply}
    )

inference_capability(state, *, engine) staticmethod

Describe validated row-local lookups without granting worker execution.

Source code in skyulf-core/skyulf/preprocessing/encoding/frequency.py
114
115
116
117
118
119
120
@staticmethod
def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
    """Describe validated row-local lookups without granting worker execution."""
    if engine not in ("pandas", "polars"):
        return None
    FrequencyEncoderApplier.validate_inference_state(state)
    return ExecutionCapability(engine, "apply", "local", "preserve", "row")

validate_inference_state(raw) staticmethod

Check exact learned counts and dimensions before granting local replay.

Source code in skyulf-core/skyulf/preprocessing/encoding/frequency.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@staticmethod
def validate_inference_state(raw: dict) -> dict:
    """Check exact learned counts and dimensions before granting local replay."""
    fields = {"type", "columns", "mode", "category_key_version", "n_samples", "mappings"}
    if not local_state_fields(raw, "frequency_encoder", fields, allow_empty=True):
        return raw
    uses_category_keys(raw)
    _mode(raw["mode"])
    columns = raw["columns"]
    if type(columns) is not list or not columns or any(type(c) is not str for c in columns):
        raise ValueError("Fitted frequency columns must be a nonempty ordered string list.")
    if len(columns) != len(set(columns)):
        raise ValueError("Fitted frequency columns must be unique.")
    FrequencyEncoderApplier._validate_counts(raw)
    return raw

FrequencyEncoderCalculator

Bases: BaseCalculator

Learn counts including missing values; frequency divides by all training rows.

mode='frequency' emits float64 and mode='count' emits int64. Omitted columns select categorical features, excluding the named target. An explicit empty selection is a no-op. Selected empty training data fails.

Source code in skyulf-core/skyulf/preprocessing/encoding/frequency.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
@NodeRegistry.register("FrequencyEncoder", FrequencyEncoderApplier)
@node_meta(
    id="FrequencyEncoder",
    name="Frequency Encoder",
    category="Preprocessing",
    description="Replace categories with training frequencies or counts; unseen values become zero.",
    params={"columns": [], "mode": "frequency"},
    learns_from_data=True,
)
class FrequencyEncoderCalculator(BaseCalculator):
    """Learn counts including missing values; frequency divides by all training rows.

    ``mode='frequency'`` emits float64 and ``mode='count'`` emits int64.
    Omitted columns select categorical features, excluding the named target.
    An explicit empty selection is a no-op. Selected empty training data fails.
    """

    @fit_method
    def fit(self, X: Any, y: Any, config: dict) -> dict:
        """Persist category counts for the selected native dataframe columns."""
        if user_picked_no_columns(config):
            return {}
        return fit_dual_engine(
            (X, y) if y is not None else X, config, {"pandas": _fit, "polars": _fit}
        )

fit(X, y, config)

Persist category counts for the selected native dataframe columns.

Source code in skyulf-core/skyulf/preprocessing/encoding/frequency.py
147
148
149
150
151
152
153
154
@fit_method
def fit(self, X: Any, y: Any, config: dict) -> dict:
    """Persist category counts for the selected native dataframe columns."""
    if user_picked_no_columns(config):
        return {}
    return fit_dual_engine(
        (X, y) if y is not None else X, config, {"pandas": _fit, "polars": _fit}
    )

HashEncoderApplier

Bases: BaseApplier

Replace each categorical column's values in place with a bucket index.

Buckets run over [0, n_features). No vocabulary is learned, so the same artifact handles values never seen at fit time — at the cost of collisions, which the hashing trick accepts by design. Both engines must agree on the bucket, which is why they share _stable_hash (blake2b) instead of using polars' native hash(): deployment always crosses engines, so a divergence would corrupt every production encoding. Version 2 normalizes missing and numeric scalars per value, keeping literal strings distinct. Older normalization versions retain their original bucket assignments.

Source code in skyulf-core/skyulf/preprocessing/encoding/hash.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class HashEncoderApplier(BaseApplier):
    """Replace each categorical column's values in place with a bucket index.

    Buckets run over ``[0, n_features)``. No vocabulary is learned, so the same
    artifact handles values never seen at fit time — at the cost of collisions,
    which the hashing trick accepts by design. Both engines must agree on the
    bucket, which is why they share ``_stable_hash`` (blake2b) instead of using
    polars' native ``hash()``: deployment always crosses engines, so a
    divergence would corrupt every production encoding. Version 2 normalizes
    missing and numeric scalars per value, keeping literal strings distinct.
    Older normalization versions retain their original bucket assignments.
    """

    @staticmethod
    def validate_inference_state(raw: dict) -> dict:
        """Inspect saved hashing choices without computing buckets or coercing scalars."""
        fields = {"type", "columns", "n_features"}
        if isinstance(raw, dict) and "numeric_normalization_version" in raw:
            fields.add("numeric_normalization_version")
        if not local_state_fields(raw, "hash_encoder", fields, allow_empty=True):
            return raw
        columns = raw["columns"]
        if type(columns) not in (list, tuple) or any(not isinstance(col, str) for col in columns):
            raise ValueError("Fitted hash columns must be a list or tuple of strings.")
        count = raw["n_features"]
        if not isinstance(count, (Real, Decimal, np.bool_)) or not count > 0:
            raise ValueError("Fitted hash bucket count must be positive and numeric.")
        _uses_numeric_normalization(raw)
        return raw

    @staticmethod
    def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
        """Describe fixed hashing while preserving legacy pandas rendering dependencies."""
        if engine not in ("pandas", "polars"):
            return None
        HashEncoderApplier.validate_inference_state(state)
        legacy = state.get("columns") and state.get("numeric_normalization_version") != 2
        context = "global" if engine == "pandas" and legacy else "row"
        return ExecutionCapability(engine, "apply", "local", "preserve", context)

    @apply_method
    def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
        """Dispatch to the engine-specific hashing, forwarding ``(X, y)`` only when ``y`` exists."""
        return apply_dual_engine(
            (X, y) if y is not None else X,
            params,
            {"polars": _hash_apply_polars, "pandas": _hash_apply_pandas},
        )

apply(X, y, params)

Dispatch to the engine-specific hashing, forwarding (X, y) only when y exists.

Source code in skyulf-core/skyulf/preprocessing/encoding/hash.py
170
171
172
173
174
175
176
177
@apply_method
def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
    """Dispatch to the engine-specific hashing, forwarding ``(X, y)`` only when ``y`` exists."""
    return apply_dual_engine(
        (X, y) if y is not None else X,
        params,
        {"polars": _hash_apply_polars, "pandas": _hash_apply_pandas},
    )

inference_capability(state, *, engine) staticmethod

Describe fixed hashing while preserving legacy pandas rendering dependencies.

Source code in skyulf-core/skyulf/preprocessing/encoding/hash.py
160
161
162
163
164
165
166
167
168
@staticmethod
def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
    """Describe fixed hashing while preserving legacy pandas rendering dependencies."""
    if engine not in ("pandas", "polars"):
        return None
    HashEncoderApplier.validate_inference_state(state)
    legacy = state.get("columns") and state.get("numeric_normalization_version") != 2
    context = "global" if engine == "pandas" and legacy else "row"
    return ExecutionCapability(engine, "apply", "local", "preserve", context)

validate_inference_state(raw) staticmethod

Inspect saved hashing choices without computing buckets or coercing scalars.

Source code in skyulf-core/skyulf/preprocessing/encoding/hash.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
@staticmethod
def validate_inference_state(raw: dict) -> dict:
    """Inspect saved hashing choices without computing buckets or coercing scalars."""
    fields = {"type", "columns", "n_features"}
    if isinstance(raw, dict) and "numeric_normalization_version" in raw:
        fields.add("numeric_normalization_version")
    if not local_state_fields(raw, "hash_encoder", fields, allow_empty=True):
        return raw
    columns = raw["columns"]
    if type(columns) not in (list, tuple) or any(not isinstance(col, str) for col in columns):
        raise ValueError("Fitted hash columns must be a list or tuple of strings.")
    count = raw["n_features"]
    if not isinstance(count, (Real, Decimal, np.bool_)) or not count > 0:
        raise ValueError("Fitted hash bucket count must be positive and numeric.")
    _uses_numeric_normalization(raw)
    return raw

HashEncoderCalculator

Bases: BaseCalculator

Resolve which columns to hash and how many buckets to use.

No vocabulary is learned — hashing is stateless, so the artifact is just the column set plus n_features. The target column is excluded because hash encoding overwrites the column it encodes. An explicit columns: [], or auto-detection finding nothing, yields an empty artifact so the applier no-ops.

Source code in skyulf-core/skyulf/preprocessing/encoding/hash.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
@NodeRegistry.register("HashEncoder", HashEncoderApplier)
@node_meta(
    id="HashEncoder",
    name="Hash Encoder",
    category="Preprocessing",
    description="Encode categorical features using hashing.",
    params={"n_features": 8, "columns": []},
    learns_from_data=True,
)
class HashEncoderCalculator(BaseCalculator):
    """Resolve which columns to hash and how many buckets to use.

    No vocabulary is learned — hashing is stateless, so the artifact is just
    the column set plus ``n_features``. The target column is excluded because
    hash encoding overwrites the column it encodes. An explicit
    ``columns: []``, or auto-detection finding nothing, yields an empty
    artifact so the applier no-ops.
    """

    @fit_method
    def fit(self, X: Any, y: Any, config: dict[str, Any]) -> HashEncoderArtifact:  # pylint: disable=arguments-differ
        """Build the artifact, auto-detecting categorical columns when none are named."""
        if user_picked_no_columns(config):
            return {}

        cols = resolve_columns(X, config, detect_categorical_columns)
        cols = _exclude_target_column(cols, config, "HashEncoder", y)
        if not cols:
            return {}

        n_features = config.get("n_features", 8)
        if isinstance(n_features, bool) or not isinstance(n_features, Integral) or n_features <= 0:
            raise ValueError("HashEncoder n_features must be a positive integer.")
        return {
            "type": "hash_encoder",
            "columns": cols,
            "n_features": int(n_features),
            "numeric_normalization_version": 2,
        }

    def infer_output_schema(
        self,
        input_schema: SkyulfSchema,
        config: dict[str, Any],
    ) -> SkyulfSchema | None:
        """Describe explicit signed integer buckets; abstain for automatic or unbounded output."""
        columns = config.get("columns")
        if columns is None:
            return None
        columns = [col for col in columns if col != config.get("target_column")]
        width = config.get("n_features", 8)
        if columns and not (
            isinstance(width, Integral) and not isinstance(width, bool) and 0 < width <= 2**63
        ):
            return None
        return encoded_columns_schema(input_schema, columns, "int64")

fit(X, y, config)

Build the artifact, auto-detecting categorical columns when none are named.

Source code in skyulf-core/skyulf/preprocessing/encoding/hash.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
@fit_method
def fit(self, X: Any, y: Any, config: dict[str, Any]) -> HashEncoderArtifact:  # pylint: disable=arguments-differ
    """Build the artifact, auto-detecting categorical columns when none are named."""
    if user_picked_no_columns(config):
        return {}

    cols = resolve_columns(X, config, detect_categorical_columns)
    cols = _exclude_target_column(cols, config, "HashEncoder", y)
    if not cols:
        return {}

    n_features = config.get("n_features", 8)
    if isinstance(n_features, bool) or not isinstance(n_features, Integral) or n_features <= 0:
        raise ValueError("HashEncoder n_features must be a positive integer.")
    return {
        "type": "hash_encoder",
        "columns": cols,
        "n_features": int(n_features),
        "numeric_normalization_version": 2,
    }

infer_output_schema(input_schema, config)

Describe explicit signed integer buckets; abstain for automatic or unbounded output.

Source code in skyulf-core/skyulf/preprocessing/encoding/hash.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def infer_output_schema(
    self,
    input_schema: SkyulfSchema,
    config: dict[str, Any],
) -> SkyulfSchema | None:
    """Describe explicit signed integer buckets; abstain for automatic or unbounded output."""
    columns = config.get("columns")
    if columns is None:
        return None
    columns = [col for col in columns if col != config.get("target_column")]
    width = config.get("n_features", 8)
    if columns and not (
        isinstance(width, Integral) and not isinstance(width, bool) and 0 < width <= 2**63
    ):
        return None
    return encoded_columns_schema(input_schema, columns, "int64")

LabelEncoderApplier

Bases: BaseApplier

Replace categorical values in place with the integer ids their fitted encoder assigned.

New feature artifacts use typed scalar keys: integral floats match their integer category without matching literal numeric strings. Legacy artifacts retain their original string lookup. Values unseen at fit time map to missing_code. Target encoders retain their existing string-label behavior. Encoded targets are native Series for the feature engine; pandas Series keep their index and all native Series keep their name. Unencoded targets pass through unchanged, including lists and NumPy arrays.

Source code in skyulf-core/skyulf/preprocessing/encoding/label.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
class LabelEncoderApplier(BaseApplier):
    """Replace categorical values in place with the integer ids their fitted encoder assigned.

    New feature artifacts use typed scalar keys: integral floats match their
    integer category without matching literal numeric strings. Legacy artifacts
    retain their original string lookup. Values unseen at fit time map to
    ``missing_code``. Target encoders retain their existing string-label behavior.
    Encoded targets are native Series for the feature engine; pandas Series keep
    their index and all native Series keep their name. Unencoded targets pass
    through unchanged, including lists and NumPy arrays.
    """

    @staticmethod
    def validate_inference_state(raw: dict) -> dict:
        """Inspect native learned classes without normalizing the fitted artifact."""
        fields = {"type", "encoders", "columns", "classes_count", "missing_code"}
        if isinstance(raw, dict):
            fields.update(set(raw) & {"category_key_version", "target_column"})
        local_state_fields(raw, "label_encoder", fields)
        uses_category_keys(raw)
        if not isinstance(raw["missing_code"], Decimal):
            local_scalar(raw["missing_code"], "LabelEncoder missing code")
        _validate_label_encoders(raw)
        return raw

    @staticmethod
    def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
        """Declare fixed lookups while retaining legacy feature rendering dependencies."""
        if engine not in ("pandas", "polars"):
            return None
        LabelEncoderApplier.validate_inference_state(state)
        legacy = state["columns"] and not uses_category_keys(state)
        context = "global" if engine == "pandas" and legacy else "row"
        return ExecutionCapability(engine, "apply", "local", "preserve", context)

    @apply_method
    def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
        """Dispatch to the engine-specific encode, forwarding ``(X, y)`` only when ``y`` exists."""
        return apply_target_encoder(
            X,
            y,
            params,
            {"polars": _label_apply_polars, "pandas": _label_apply_pandas},
        )

apply(X, y, params)

Dispatch to the engine-specific encode, forwarding (X, y) only when y exists.

Source code in skyulf-core/skyulf/preprocessing/encoding/label.py
213
214
215
216
217
218
219
220
221
@apply_method
def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
    """Dispatch to the engine-specific encode, forwarding ``(X, y)`` only when ``y`` exists."""
    return apply_target_encoder(
        X,
        y,
        params,
        {"polars": _label_apply_polars, "pandas": _label_apply_pandas},
    )

inference_capability(state, *, engine) staticmethod

Declare fixed lookups while retaining legacy feature rendering dependencies.

Source code in skyulf-core/skyulf/preprocessing/encoding/label.py
203
204
205
206
207
208
209
210
211
@staticmethod
def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
    """Declare fixed lookups while retaining legacy feature rendering dependencies."""
    if engine not in ("pandas", "polars"):
        return None
    LabelEncoderApplier.validate_inference_state(state)
    legacy = state["columns"] and not uses_category_keys(state)
    context = "global" if engine == "pandas" and legacy else "row"
    return ExecutionCapability(engine, "apply", "local", "preserve", context)

validate_inference_state(raw) staticmethod

Inspect native learned classes without normalizing the fitted artifact.

Source code in skyulf-core/skyulf/preprocessing/encoding/label.py
190
191
192
193
194
195
196
197
198
199
200
201
@staticmethod
def validate_inference_state(raw: dict) -> dict:
    """Inspect native learned classes without normalizing the fitted artifact."""
    fields = {"type", "encoders", "columns", "classes_count", "missing_code"}
    if isinstance(raw, dict):
        fields.update(set(raw) & {"category_key_version", "target_column"})
    local_state_fields(raw, "label_encoder", fields)
    uses_category_keys(raw)
    if not isinstance(raw["missing_code"], Decimal):
        local_scalar(raw["missing_code"], "LabelEncoder missing code")
    _validate_label_encoders(raw)
    return raw

LabelEncoderCalculator

Bases: BaseCalculator

Fit one LabelEncoder per named column, plus one for the target when it qualifies.

Unlike its sibling encoders this node deliberately does not auto-detect feature columns: an empty columns means "encode the target only", mirroring sklearn's convention that LabelEncoder is a 1-D label transformer and OrdinalEncoder is the tool for feature columns. Fitting zero encoders logs a warning rather than passing a silent no-op downstream.

Source code in skyulf-core/skyulf/preprocessing/encoding/label.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
@NodeRegistry.register("LabelEncoder", LabelEncoderApplier)
@node_meta(
    id="LabelEncoder",
    name="Label Encoder",
    category="Preprocessing",
    description="Encode target labels with value between 0 and n_classes-1.",
    params={"columns": [], "missing_code": -1},
    learns_from_data=True,
)
class LabelEncoderCalculator(BaseCalculator):
    """Fit one LabelEncoder per named column, plus one for the target when it qualifies.

    Unlike its sibling encoders this node deliberately does *not* auto-detect
    feature columns: an empty ``columns`` means "encode the target only",
    mirroring sklearn's convention that ``LabelEncoder`` is a 1-D label
    transformer and ``OrdinalEncoder`` is the tool for feature columns. Fitting
    zero encoders logs a warning rather than passing a silent no-op downstream.
    """

    def infer_output_schema(
        self, input_schema: SkyulfSchema, config: dict[str, Any]
    ) -> SkyulfSchema | None:
        """Describe selected integer labels, retaining the target-only empty-selection default."""
        target = config.get("target_column")
        columns = config.get("columns") or ([target] if isinstance(target, str) else [])
        return encoded_columns_schema(input_schema, columns, "int64")

    @fit_method
    def fit(self, X: Any, y: Any, config: dict[str, Any]) -> LabelEncoderArtifact:  # pylint: disable=arguments-differ
        """Fit the encoders on the frame's own engine, forwarding ``y`` when present."""
        return cast(
            LabelEncoderArtifact,
            fit_target_encoder(
                X,
                y,
                config,
                {"polars": _label_fit_polars, "pandas": _label_fit_pandas},
            ),
        )

fit(X, y, config)

Fit the encoders on the frame's own engine, forwarding y when present.

Source code in skyulf-core/skyulf/preprocessing/encoding/label.py
414
415
416
417
418
419
420
421
422
423
424
425
@fit_method
def fit(self, X: Any, y: Any, config: dict[str, Any]) -> LabelEncoderArtifact:  # pylint: disable=arguments-differ
    """Fit the encoders on the frame's own engine, forwarding ``y`` when present."""
    return cast(
        LabelEncoderArtifact,
        fit_target_encoder(
            X,
            y,
            config,
            {"polars": _label_fit_polars, "pandas": _label_fit_pandas},
        ),
    )

infer_output_schema(input_schema, config)

Describe selected integer labels, retaining the target-only empty-selection default.

Source code in skyulf-core/skyulf/preprocessing/encoding/label.py
406
407
408
409
410
411
412
def infer_output_schema(
    self, input_schema: SkyulfSchema, config: dict[str, Any]
) -> SkyulfSchema | None:
    """Describe selected integer labels, retaining the target-only empty-selection default."""
    target = config.get("target_column")
    columns = config.get("columns") or ([target] if isinstance(target, str) else [])
    return encoded_columns_schema(input_schema, columns, "int64")

OneHotEncoderApplier

Bases: BaseApplier

Expand each categorical column into its one-hot indicator columns.

Parity here comes from both engines transforming through the same fitted sklearn encoder carried in the artifact, not from two parallel implementations. Nulls are filled with the sentinel token before transforming, which only lines up when include_missing was set at fit time too — the encoder knows that level solely if it saw it. Indicator columns are concatenated on and the originals dropped unless drop_original is false. Generated names must be unique and must not collide with retained input columns; conflicts raise ValueError. New artifacts escape literal missing tokens and escape-prefixed strings; unversioned artifacts retain their original missing-value transformation. An empty artifact declares identity replay only for an explicit empty column selection with a validated recipe.

Source code in skyulf-core/skyulf/preprocessing/encoding/one_hot.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
class OneHotEncoderApplier(BaseApplier):
    """Expand each categorical column into its one-hot indicator columns.

    Parity here comes from both engines transforming through the *same* fitted
    sklearn encoder carried in the artifact, not from two parallel
    implementations. Nulls are filled with the sentinel token before
    transforming, which only lines up when ``include_missing`` was set at fit
    time too — the encoder knows that level solely if it saw it. Indicator
    columns are concatenated on and the originals dropped unless
    ``drop_original`` is false. Generated names must be unique and must not
    collide with retained input columns; conflicts raise ``ValueError``.
    New artifacts escape literal missing tokens and escape-prefixed strings;
    unversioned artifacts retain their original missing-value transformation.
    An empty artifact declares identity replay only for an explicit empty
    column selection with a validated recipe.
    """

    @staticmethod
    def validate_fitted_state(raw: dict) -> dict:
        """Inspect this node's supported saved state without fitting or applying data."""
        return _onehot_state(raw)

    @staticmethod
    def resolve_fitted_config(raw: dict, state: dict) -> dict:
        """Bind inference configuration to this node's inspected fitted artifact."""
        if not state and (not user_picked_no_columns(raw) or raw.get("_auto_columns")):
            raise ValueError("An empty encoder artifact requires explicit empty columns.")
        return _onehot_config(fitted_columns(raw, state or {"columns": []}), state)

    @apply_method
    def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
        """Dispatch to the engine-specific transform, forwarding ``(X, y)`` only when present."""
        return apply_dual_engine(
            (X, y) if y is not None else X,
            params,
            {"polars": _onehot_apply_polars, "pandas": _onehot_apply_pandas},
        )

apply(X, y, params)

Dispatch to the engine-specific transform, forwarding (X, y) only when present.

Source code in skyulf-core/skyulf/preprocessing/encoding/one_hot.py
293
294
295
296
297
298
299
300
@apply_method
def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
    """Dispatch to the engine-specific transform, forwarding ``(X, y)`` only when present."""
    return apply_dual_engine(
        (X, y) if y is not None else X,
        params,
        {"polars": _onehot_apply_polars, "pandas": _onehot_apply_pandas},
    )

resolve_fitted_config(raw, state) staticmethod

Bind inference configuration to this node's inspected fitted artifact.

Source code in skyulf-core/skyulf/preprocessing/encoding/one_hot.py
286
287
288
289
290
291
@staticmethod
def resolve_fitted_config(raw: dict, state: dict) -> dict:
    """Bind inference configuration to this node's inspected fitted artifact."""
    if not state and (not user_picked_no_columns(raw) or raw.get("_auto_columns")):
        raise ValueError("An empty encoder artifact requires explicit empty columns.")
    return _onehot_config(fitted_columns(raw, state or {"columns": []}), state)

validate_fitted_state(raw) staticmethod

Inspect this node's supported saved state without fitting or applying data.

Source code in skyulf-core/skyulf/preprocessing/encoding/one_hot.py
281
282
283
284
@staticmethod
def validate_fitted_state(raw: dict) -> dict:
    """Inspect this node's supported saved state without fitting or applying data."""
    return _onehot_state(raw)

OneHotEncoderCalculator

Bases: BaseCalculator

Fit a sklearn OneHotEncoder on the resolved categorical columns.

The target column is excluded before fitting: one-hot replaces the column it encodes with several derived ones, which would break a downstream Feature/Target Split. An explicit columns: [], or auto-detection finding no categorical columns, yields an empty artifact so the applier no-ops. Degenerate columns (zero categories, or one category with drop_first) are warned about rather than silently producing nothing.

Optional categories maps every explicitly selected column to an ordered list of unique strings. Unobserved levels retain outputs. Fixed categories require max_categories=None and include_missing=False; unknowns and nulls produce zeros with handle_unknown='ignore' or raise with 'error'. drop_first removes the first configured level. column_names='slug' folds category labels to ASCII; the default 'sklearn' preserves names.

Source code in skyulf-core/skyulf/preprocessing/encoding/one_hot.py
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
@NodeRegistry.register(
    "OneHotEncoder",
    OneHotEncoderApplier,
    execution_capabilities=(
        ExecutionCapability(
            "pandas",
            "apply",
            "python_batch",
            "preserve",
            "row",
            config_match=(("max_categories", None), ("include_missing", False)),
        ),
        ExecutionCapability("pandas", "apply", "local", "preserve", "row"),
        ExecutionCapability("polars", "apply", "local", "preserve", "row"),
    ),
)
@node_meta(
    id="OneHotEncoder",
    name="One-Hot Encoder",
    category="Preprocessing",
    description="Encodes categorical features as a one-hot numeric array.",
    params={
        "handle_unknown": "ignore",
        "drop_first": False,
        "max_categories": 20,
        "columns": [],
        "include_missing": False,
        "categories": None,
        "column_names": "sklearn",
    },
    learns_from_data=True,
)
class OneHotEncoderCalculator(BaseCalculator):
    """Fit a sklearn ``OneHotEncoder`` on the resolved categorical columns.

    The target column is excluded before fitting: one-hot replaces the column
    it encodes with several derived ones, which would break a downstream
    Feature/Target Split. An explicit ``columns: []``, or auto-detection
    finding no categorical columns, yields an empty artifact so the applier
    no-ops. Degenerate columns (zero categories, or one category with
    ``drop_first``) are warned about rather than silently producing nothing.

    Optional ``categories`` maps every explicitly selected column to an ordered
    list of unique strings. Unobserved levels retain outputs. Fixed categories
    require ``max_categories=None`` and ``include_missing=False``; unknowns and
    nulls produce zeros with ``handle_unknown='ignore'`` or raise with ``'error'``.
    ``drop_first`` removes the first configured level. ``column_names='slug'``
    folds category labels to ASCII; the default ``'sklearn'`` preserves names.
    """

    @fit_method
    def fit(self, X: Any, y: Any, config: dict[str, Any]) -> OneHotArtifact:  # pylint: disable=arguments-differ
        """Short-circuit an explicit empty column selection, else fit on the frame's own engine."""
        _fixed_category_options(config)
        if user_picked_no_columns(config):
            return {}
        return cast(
            OneHotArtifact,
            fit_dual_engine(
                (X, y) if y is not None else X,
                config,
                {"polars": _onehot_fit_polars, "pandas": _onehot_fit_pandas},
            ),
        )

fit(X, y, config)

Short-circuit an explicit empty column selection, else fit on the frame's own engine.

Source code in skyulf-core/skyulf/preprocessing/encoding/one_hot.py
488
489
490
491
492
493
494
495
496
497
498
499
500
501
@fit_method
def fit(self, X: Any, y: Any, config: dict[str, Any]) -> OneHotArtifact:  # pylint: disable=arguments-differ
    """Short-circuit an explicit empty column selection, else fit on the frame's own engine."""
    _fixed_category_options(config)
    if user_picked_no_columns(config):
        return {}
    return cast(
        OneHotArtifact,
        fit_dual_engine(
            (X, y) if y is not None else X,
            config,
            {"polars": _onehot_fit_polars, "pandas": _onehot_fit_pandas},
        ),
    )

OrdinalEncoderApplier

Bases: BaseApplier

Replace categorical values in place with fitted ordinal indices, target included.

Features use the shared encoder_object and versioned scalar keys; y uses a separate __target__ encoder with its existing string-label rules. Older feature artifacts retain their original string lookup. An unseen value becomes unknown_value unless handle_unknown is "error". Encoded output is float32, not integer.

Source code in skyulf-core/skyulf/preprocessing/encoding/ordinal.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
class OrdinalEncoderApplier(BaseApplier):
    """Replace categorical values in place with fitted ordinal indices, target included.

    Features use the shared ``encoder_object`` and versioned scalar keys; ``y``
    uses a separate ``__target__`` encoder with its existing string-label rules.
    Older feature artifacts retain their original string lookup. An unseen
    value becomes ``unknown_value`` unless ``handle_unknown`` is ``"error"``.
    Encoded output is ``float32``, not integer.
    """

    @staticmethod
    def validate_inference_state(raw: dict) -> dict:
        """Inspect feature and optional target encoders without changing lookup provenance."""
        fields = {"type", "columns", "encoder_object", "encoders", "categories_count"}
        if isinstance(raw, dict):
            fields.update({"category_key_version", "target_column"}.intersection(raw))
        if not local_state_fields(raw, "ordinal", fields, allow_empty=True):
            return raw
        uses_category_keys(raw)
        _validate_ordinal_features(raw)
        OrdinalEncoderApplier._validate_targets(raw)
        return raw

    @staticmethod
    def _validate_targets(raw: dict) -> None:
        """Keep embedded-target routing separate from selected feature encoders."""
        encoders = raw["encoders"]
        if type(encoders) is not dict or set(encoders) - {"__target__"}:
            raise ValueError("Unexpected fitted ordinal target encoders.")
        if "__target__" in encoders:
            _validate_ordinal_encoder(encoders["__target__"], 1)
        if "target_column" in raw and (
            not isinstance(raw["target_column"], str)
            or raw["target_column"] in raw["columns"]
            or "__target__" not in encoders
        ):
            raise ValueError("Fitted ordinal target routing disagrees with its encoders.")

    @staticmethod
    def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
        """Describe saved ordinal lookups without enabling worker execution."""
        if engine not in ("pandas", "polars"):
            return None
        OrdinalEncoderApplier.validate_inference_state(state)
        # Legacy pandas datetime string formatting can depend on neighboring rows.
        legacy_pandas = (
            engine == "pandas" and state.get("columns") and not uses_category_keys(state)
        )
        context = "global" if legacy_pandas else "row"
        return ExecutionCapability(engine, "apply", "local", "preserve", context)

    @apply_method
    def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
        """Dispatch to the engine-specific encode, forwarding ``(X, y)`` only when ``y`` exists."""
        return apply_target_encoder(
            X,
            y,
            params,
            {"polars": _ordinal_apply_polars, "pandas": _ordinal_apply_pandas},
        )

apply(X, y, params)

Dispatch to the engine-specific encode, forwarding (X, y) only when y exists.

Source code in skyulf-core/skyulf/preprocessing/encoding/ordinal.py
288
289
290
291
292
293
294
295
296
@apply_method
def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
    """Dispatch to the engine-specific encode, forwarding ``(X, y)`` only when ``y`` exists."""
    return apply_target_encoder(
        X,
        y,
        params,
        {"polars": _ordinal_apply_polars, "pandas": _ordinal_apply_pandas},
    )

inference_capability(state, *, engine) staticmethod

Describe saved ordinal lookups without enabling worker execution.

Source code in skyulf-core/skyulf/preprocessing/encoding/ordinal.py
275
276
277
278
279
280
281
282
283
284
285
286
@staticmethod
def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
    """Describe saved ordinal lookups without enabling worker execution."""
    if engine not in ("pandas", "polars"):
        return None
    OrdinalEncoderApplier.validate_inference_state(state)
    # Legacy pandas datetime string formatting can depend on neighboring rows.
    legacy_pandas = (
        engine == "pandas" and state.get("columns") and not uses_category_keys(state)
    )
    context = "global" if legacy_pandas else "row"
    return ExecutionCapability(engine, "apply", "local", "preserve", context)

validate_inference_state(raw) staticmethod

Inspect feature and optional target encoders without changing lookup provenance.

Source code in skyulf-core/skyulf/preprocessing/encoding/ordinal.py
247
248
249
250
251
252
253
254
255
256
257
258
@staticmethod
def validate_inference_state(raw: dict) -> dict:
    """Inspect feature and optional target encoders without changing lookup provenance."""
    fields = {"type", "columns", "encoder_object", "encoders", "categories_count"}
    if isinstance(raw, dict):
        fields.update({"category_key_version", "target_column"}.intersection(raw))
    if not local_state_fields(raw, "ordinal", fields, allow_empty=True):
        return raw
    uses_category_keys(raw)
    _validate_ordinal_features(raw)
    OrdinalEncoderApplier._validate_targets(raw)
    return raw

OrdinalEncoderCalculator

Bases: BaseCalculator

Fit an ordinal encoder for the features and, when split out, for the target.

Picking no feature columns is not a no-op here — unlike its sibling encoders, this node still fits a target encoder when y is available. The target is encoded only when y exists and the configured target name is listed in columns but absent from X, i.e. it has already been split away. categories_order supplies an explicit ordering, and the target reads the last row of an n_features + 1 row table.

Source code in skyulf-core/skyulf/preprocessing/encoding/ordinal.py
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
@NodeRegistry.register("OrdinalEncoder", OrdinalEncoderApplier)
@node_meta(
    id="OrdinalEncoder",
    name="Ordinal Encoder",
    category="Preprocessing",
    description="Encodes categorical features as an integer array.",
    params={
        "columns": [],
        "handle_unknown": "use_encoded_value",
        "unknown_value": -1,
        "categories_order": "",
    },
    learns_from_data=True,
)
class OrdinalEncoderCalculator(BaseCalculator):
    """Fit an ordinal encoder for the features and, when split out, for the target.

    Picking no feature columns is *not* a no-op here — unlike its sibling
    encoders, this node still fits a target encoder when ``y`` is available.
    The target is encoded only when ``y`` exists and the configured target name
    is listed in ``columns`` but absent from ``X``, i.e. it has already been
    split away. ``categories_order`` supplies an explicit ordering, and the
    target reads the last row of an ``n_features + 1`` row table.
    """

    def infer_output_schema(
        self, input_schema: SkyulfSchema, config: dict[str, Any]
    ) -> SkyulfSchema | None:
        """Describe explicit float32 codes, including the embedded target-only empty selection."""
        columns = config.get("columns")
        if columns == []:
            target = config.get("target_column")
            columns = [target] if isinstance(target, str) else []
        return encoded_columns_schema(input_schema, columns, "float32")

    @fit_method
    def fit(self, X: Any, y: Any, config: dict[str, Any]) -> OrdinalArtifact:  # pylint: disable=arguments-differ
        """Fit the encoders on the frame's own engine, forwarding ``y`` when present."""
        return cast(
            OrdinalArtifact,
            fit_target_encoder(
                X,
                y,
                config,
                {"polars": _ordinal_fit_polars, "pandas": _ordinal_fit_pandas},
            ),
        )

fit(X, y, config)

Fit the encoders on the frame's own engine, forwarding y when present.

Source code in skyulf-core/skyulf/preprocessing/encoding/ordinal.py
528
529
530
531
532
533
534
535
536
537
538
539
@fit_method
def fit(self, X: Any, y: Any, config: dict[str, Any]) -> OrdinalArtifact:  # pylint: disable=arguments-differ
    """Fit the encoders on the frame's own engine, forwarding ``y`` when present."""
    return cast(
        OrdinalArtifact,
        fit_target_encoder(
            X,
            y,
            config,
            {"polars": _ordinal_fit_polars, "pandas": _ordinal_fit_pandas},
        ),
    )

infer_output_schema(input_schema, config)

Describe explicit float32 codes, including the embedded target-only empty selection.

Source code in skyulf-core/skyulf/preprocessing/encoding/ordinal.py
518
519
520
521
522
523
524
525
526
def infer_output_schema(
    self, input_schema: SkyulfSchema, config: dict[str, Any]
) -> SkyulfSchema | None:
    """Describe explicit float32 codes, including the embedded target-only empty selection."""
    columns = config.get("columns")
    if columns == []:
        target = config.get("target_column")
        columns = [target] if isinstance(target, str) else []
    return encoded_columns_schema(input_schema, columns, "float32")

RareCategoryGroupingApplier

Bases: BaseApplier

Replay learned text-category membership without discovering new frequent values.

Source code in skyulf-core/skyulf/preprocessing/encoding/rare.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
class RareCategoryGroupingApplier(BaseApplier):
    """Replay learned text-category membership without discovering new frequent values."""

    @staticmethod
    def validate_inference_state(raw: dict) -> dict:
        """Reject malformed or ambiguous saved membership and fallback values."""
        fields = {"type", "columns", "category_key_version", "kept_categories", "replacement"}
        if not local_state_fields(raw, "rare_category_grouping", fields, allow_empty=True):
            return raw
        uses_category_keys(raw)
        _replacement(raw["replacement"])
        columns = raw["columns"]
        if type(columns) is not list or not columns or any(type(c) is not str for c in columns):
            raise ValueError("Fitted rare-grouping columns must be a nonempty ordered string list.")
        if len(columns) != len(set(columns)):
            raise ValueError("Fitted rare-grouping columns must be unique.")
        RareCategoryGroupingApplier._validate_membership(raw)
        return raw

    @staticmethod
    def _validate_membership(raw: dict) -> None:
        """Keep each learned category set attached to its exact source column."""
        kept = raw["kept_categories"]
        if type(kept) is not dict or set(kept) != set(raw["columns"]):
            raise ValueError("Fitted rare-grouping membership must match selected columns.")
        for keys in kept.values():
            if type(keys) is not list or any(type(key) is not str for key in keys):
                raise ValueError("Fitted rare-grouping keys must be ordered strings.")
            if len(keys) != len(set(keys)):
                raise ValueError("Fitted rare-grouping keys must be unique.")

    @staticmethod
    def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
        """Describe validated row-local membership without granting worker execution."""
        if engine not in ("pandas", "polars"):
            return None
        RareCategoryGroupingApplier.validate_inference_state(state)
        return ExecutionCapability(engine, "apply", "local", "preserve", "row")

    @apply_method
    def apply(self, X: Any, y: Any, params: dict) -> Any:
        """Replace selected columns with nullable strings, retaining index and target."""
        return apply_dual_engine(
            (X, y) if y is not None else X, params, {"pandas": _apply, "polars": _apply}
        )

apply(X, y, params)

Replace selected columns with nullable strings, retaining index and target.

Source code in skyulf-core/skyulf/preprocessing/encoding/rare.py
141
142
143
144
145
146
@apply_method
def apply(self, X: Any, y: Any, params: dict) -> Any:
    """Replace selected columns with nullable strings, retaining index and target."""
    return apply_dual_engine(
        (X, y) if y is not None else X, params, {"pandas": _apply, "polars": _apply}
    )

inference_capability(state, *, engine) staticmethod

Describe validated row-local membership without granting worker execution.

Source code in skyulf-core/skyulf/preprocessing/encoding/rare.py
133
134
135
136
137
138
139
@staticmethod
def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
    """Describe validated row-local membership without granting worker execution."""
    if engine not in ("pandas", "polars"):
        return None
    RareCategoryGroupingApplier.validate_inference_state(state)
    return ExecutionCapability(engine, "apply", "local", "preserve", "row")

validate_inference_state(raw) staticmethod

Reject malformed or ambiguous saved membership and fallback values.

Source code in skyulf-core/skyulf/preprocessing/encoding/rare.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@staticmethod
def validate_inference_state(raw: dict) -> dict:
    """Reject malformed or ambiguous saved membership and fallback values."""
    fields = {"type", "columns", "category_key_version", "kept_categories", "replacement"}
    if not local_state_fields(raw, "rare_category_grouping", fields, allow_empty=True):
        return raw
    uses_category_keys(raw)
    _replacement(raw["replacement"])
    columns = raw["columns"]
    if type(columns) is not list or not columns or any(type(c) is not str for c in columns):
        raise ValueError("Fitted rare-grouping columns must be a nonempty ordered string list.")
    if len(columns) != len(set(columns)):
        raise ValueError("Fitted rare-grouping columns must be unique.")
    RareCategoryGroupingApplier._validate_membership(raw)
    return raw

RareCategoryGroupingCalculator

Bases: BaseCalculator

Learn text categories meeting both count and share thresholds, including null.

Frequent missing values stay null; rare and unseen categories become the replacement string. A replacement already present in the data merges with grouped values. Numeric categories require explicit Casting before this node.

Source code in skyulf-core/skyulf/preprocessing/encoding/rare.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
@NodeRegistry.register("RareCategoryGrouping", RareCategoryGroupingApplier)
@node_meta(
    id="RareCategoryGrouping",
    name="Rare Category Grouping",
    category="Preprocessing",
    description="Keep frequent text categories and group rare or unseen values under a label.",
    params={"columns": [], "min_count": 2, "min_share": 0.0, "replacement": "Other"},
    learns_from_data=True,
)
class RareCategoryGroupingCalculator(BaseCalculator):
    """Learn text categories meeting both count and share thresholds, including null.

    Frequent missing values stay null; rare and unseen categories become the
    replacement string. A replacement already present in the data merges with
    grouped values. Numeric categories require explicit Casting before this node.
    """

    @fit_method
    def fit(self, X: Any, y: Any, config: dict) -> dict:
        """Persist fixed membership using training rows only."""
        if user_picked_no_columns(config):
            return {}
        return fit_dual_engine(
            (X, y) if y is not None else X, config, {"pandas": _fit, "polars": _fit}
        )

fit(X, y, config)

Persist fixed membership using training rows only.

Source code in skyulf-core/skyulf/preprocessing/encoding/rare.py
166
167
168
169
170
171
172
173
@fit_method
def fit(self, X: Any, y: Any, config: dict) -> dict:
    """Persist fixed membership using training rows only."""
    if user_picked_no_columns(config):
        return {}
    return fit_dual_engine(
        (X, y) if y is not None else X, config, {"pandas": _fit, "polars": _fit}
    )

TargetEncoderApplier

Bases: BaseApplier

Replace categorical columns with the target statistics the fitted encoder learned.

Both engines transform through the same sklearn TargetEncoder carried in the artifact, so parity comes from sharing it. The output shape follows the target type: binary and regression targets are encoded in place, while a multiclass target drops the originals and emits one {col}_cls{i} column per class. Multiclass output names must not collide with retained columns; conflicts raise ValueError before any input values are replaced. New artifacts use canonical missing and numeric feature keys on both engines; artifacts without a key version retain raw sklearn replay.

Source code in skyulf-core/skyulf/preprocessing/encoding/target.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
class TargetEncoderApplier(BaseApplier):
    """Replace categorical columns with the target statistics the fitted encoder learned.

    Both engines transform through the same sklearn ``TargetEncoder`` carried in
    the artifact, so parity comes from sharing it. The output shape follows the
    target type: binary and regression targets are encoded in place, while a
    multiclass target drops the originals and emits one ``{col}_cls{i}`` column
    per class. Multiclass output names must not collide with retained columns;
    conflicts raise ``ValueError`` before any input values are replaced.
    New artifacts use canonical missing and numeric feature keys on both
    engines; artifacts without a key version retain raw sklearn replay.
    """

    @staticmethod
    def validate_inference_state(raw: dict) -> dict:
        """Inspect the saved full-data encoder while leaving cross-fitted training values alone."""
        fields = {"type", "columns", "encoder_object"}
        if isinstance(raw, dict):
            fields.update({"category_key_version"}.intersection(raw))
        if not local_state_fields(raw, "target_encoder", fields, allow_empty=True):
            return raw
        uses_category_keys(raw)
        columns = raw["columns"]
        if not isinstance(columns, (list, tuple)) or any(not isinstance(c, str) for c in columns):
            raise ValueError("Fitted target encoder columns must be ordered strings.")
        encoder = raw["encoder_object"]
        required = {
            "n_features_in_",
            "categories_",
            "target_type_",
            "classes_",
            "target_mean_",
            "encodings_",
        }
        if type(encoder) is not TargetEncoder or not required.issubset(vars(encoder)):
            raise ValueError("Expected a fitted TargetEncoder.")
        if any(callable(value) for value in vars(encoder).values()):
            raise ValueError("Overridden target estimator methods are unsupported.")
        _validate_target_categories(encoder, len(columns))
        _validate_target_statistics(encoder)
        return raw

    @staticmethod
    def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
        """Describe saved target-statistic lookups without refitting or admitting workers."""
        if engine not in ("pandas", "polars"):
            return None
        TargetEncoderApplier.validate_inference_state(state)
        return ExecutionCapability(engine, "apply", "local", "preserve", "row")

    @apply_method
    def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
        """Dispatch to the engine-specific transform, forwarding ``(X, y)`` only when present."""
        return apply_dual_engine(
            (X, y) if y is not None else X,
            params,
            {"polars": _target_apply_polars, "pandas": _target_apply_pandas},
        )

apply(X, y, params)

Dispatch to the engine-specific transform, forwarding (X, y) only when present.

Source code in skyulf-core/skyulf/preprocessing/encoding/target.py
234
235
236
237
238
239
240
241
@apply_method
def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
    """Dispatch to the engine-specific transform, forwarding ``(X, y)`` only when present."""
    return apply_dual_engine(
        (X, y) if y is not None else X,
        params,
        {"polars": _target_apply_polars, "pandas": _target_apply_pandas},
    )

inference_capability(state, *, engine) staticmethod

Describe saved target-statistic lookups without refitting or admitting workers.

Source code in skyulf-core/skyulf/preprocessing/encoding/target.py
226
227
228
229
230
231
232
@staticmethod
def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
    """Describe saved target-statistic lookups without refitting or admitting workers."""
    if engine not in ("pandas", "polars"):
        return None
    TargetEncoderApplier.validate_inference_state(state)
    return ExecutionCapability(engine, "apply", "local", "preserve", "row")

validate_inference_state(raw) staticmethod

Inspect the saved full-data encoder while leaving cross-fitted training values alone.

Source code in skyulf-core/skyulf/preprocessing/encoding/target.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
@staticmethod
def validate_inference_state(raw: dict) -> dict:
    """Inspect the saved full-data encoder while leaving cross-fitted training values alone."""
    fields = {"type", "columns", "encoder_object"}
    if isinstance(raw, dict):
        fields.update({"category_key_version"}.intersection(raw))
    if not local_state_fields(raw, "target_encoder", fields, allow_empty=True):
        return raw
    uses_category_keys(raw)
    columns = raw["columns"]
    if not isinstance(columns, (list, tuple)) or any(not isinstance(c, str) for c in columns):
        raise ValueError("Fitted target encoder columns must be ordered strings.")
    encoder = raw["encoder_object"]
    required = {
        "n_features_in_",
        "categories_",
        "target_type_",
        "classes_",
        "target_mean_",
        "encodings_",
    }
    if type(encoder) is not TargetEncoder or not required.issubset(vars(encoder)):
        raise ValueError("Expected a fitted TargetEncoder.")
    if any(callable(value) for value in vars(encoder).values()):
        raise ValueError("Overridden target estimator methods are unsupported.")
    _validate_target_categories(encoder, len(columns))
    _validate_target_statistics(encoder)
    return raw

TargetEncoderCalculator

Bases: BaseCalculator

Fit a sklearn TargetEncoder against y, cross-fitting the training rows.

A target is mandatory: with no y the node logs a warning and returns an empty artifact rather than failing. The target column is excluded from the encoded set, since encoding it against itself is degenerate. This calculator also satisfies TrainTransformCalculatorProtocol, so the training split receives out-of-fold encodings — a row's own target never leaks into its feature — with the fold count capped by the smallest class count, while held-out splits go through the plain applier.

Source code in skyulf-core/skyulf/preprocessing/encoding/target.py
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
@NodeRegistry.register("TargetEncoder", TargetEncoderApplier)
@node_meta(
    id="TargetEncoder",
    name="Target Encoder",
    category="Preprocessing",
    description="Encode categorical features using target statistics.",
    params={
        "smooth": "auto",
        "target_type": "auto",
        "columns": [],
        "cv": 5,
        "shuffle": True,
        "random_state": DEFAULT_RANDOM_STATE,
    },
    learns_from_data=True,
)
class TargetEncoderCalculator(BaseCalculator):
    """Fit a sklearn ``TargetEncoder`` against ``y``, cross-fitting the training rows.

    A target is mandatory: with no ``y`` the node logs a warning and returns an
    empty artifact rather than failing. The target column is excluded from the
    encoded set, since encoding it against itself is degenerate. This calculator
    also satisfies ``TrainTransformCalculatorProtocol``, so the training split
    receives out-of-fold encodings — a row's own target never leaks into its
    feature — with the fold count capped by the smallest class count, while
    held-out splits go through the plain applier.
    """

    @fit_method
    def fit(self, X: Any, y: Any, config: dict[str, Any]) -> TargetEncoderArtifact:  # pylint: disable=arguments-differ
        """Short-circuit an explicit empty column selection, else fit on the frame's own engine."""
        if user_picked_no_columns(config):
            return {}
        return cast(
            TargetEncoderArtifact,
            fit_dual_engine(
                (X, y) if y is not None else X,
                config,
                {"polars": _target_fit_polars, "pandas": _target_fit_pandas},
            ),
        )

    def fit_transform_train(
        self, df: pd.DataFrame | SkyulfDataFrame | tuple, config: dict[str, Any]
    ) -> tuple[TargetEncoderArtifact, Any]:
        """Fit sklearn TargetEncoder and cross-fit the pipeline training rows."""
        if user_picked_no_columns(config):
            return {}, df

        artifact, transformed = fit_transform_train_dual_engine(
            df,
            config,
            {
                "polars": _target_fit_transform_train_polars,
                "pandas": _target_fit_transform_train_pandas,
            },
        )
        return cast(
            TargetEncoderArtifact,
            artifact,
        ), transformed

    def infer_output_schema(
        self,
        input_schema: SkyulfSchema,
        config: dict[str, Any],
    ) -> SkyulfSchema | None:
        """Describe explicit floating features for binary or continuous targets.

        Automatic category selection and multiclass feature expansion depend on
        training values, so they return ``None`` for runtime introspection.
        An explicit empty selection is always a no-op.
        """
        columns = config.get("columns")
        if columns == []:
            return input_schema
        if config.get("target_type", "auto") not in ("binary", "continuous"):
            return None
        if columns is not None:
            columns = [col for col in columns if col != config.get("target_column")]
        return encoded_columns_schema(input_schema, columns, "float64")

fit(X, y, config)

Short-circuit an explicit empty column selection, else fit on the frame's own engine.

Source code in skyulf-core/skyulf/preprocessing/encoding/target.py
510
511
512
513
514
515
516
517
518
519
520
521
522
@fit_method
def fit(self, X: Any, y: Any, config: dict[str, Any]) -> TargetEncoderArtifact:  # pylint: disable=arguments-differ
    """Short-circuit an explicit empty column selection, else fit on the frame's own engine."""
    if user_picked_no_columns(config):
        return {}
    return cast(
        TargetEncoderArtifact,
        fit_dual_engine(
            (X, y) if y is not None else X,
            config,
            {"polars": _target_fit_polars, "pandas": _target_fit_pandas},
        ),
    )

fit_transform_train(df, config)

Fit sklearn TargetEncoder and cross-fit the pipeline training rows.

Source code in skyulf-core/skyulf/preprocessing/encoding/target.py
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
def fit_transform_train(
    self, df: pd.DataFrame | SkyulfDataFrame | tuple, config: dict[str, Any]
) -> tuple[TargetEncoderArtifact, Any]:
    """Fit sklearn TargetEncoder and cross-fit the pipeline training rows."""
    if user_picked_no_columns(config):
        return {}, df

    artifact, transformed = fit_transform_train_dual_engine(
        df,
        config,
        {
            "polars": _target_fit_transform_train_polars,
            "pandas": _target_fit_transform_train_pandas,
        },
    )
    return cast(
        TargetEncoderArtifact,
        artifact,
    ), transformed

infer_output_schema(input_schema, config)

Describe explicit floating features for binary or continuous targets.

Automatic category selection and multiclass feature expansion depend on training values, so they return None for runtime introspection. An explicit empty selection is always a no-op.

Source code in skyulf-core/skyulf/preprocessing/encoding/target.py
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
def infer_output_schema(
    self,
    input_schema: SkyulfSchema,
    config: dict[str, Any],
) -> SkyulfSchema | None:
    """Describe explicit floating features for binary or continuous targets.

    Automatic category selection and multiclass feature expansion depend on
    training values, so they return ``None`` for runtime introspection.
    An explicit empty selection is always a no-op.
    """
    columns = config.get("columns")
    if columns == []:
        return input_schema
    if config.get("target_type", "auto") not in ("binary", "continuous"):
        return None
    if columns is not None:
        columns = [col for col in columns if col != config.get("target_column")]
    return encoded_columns_schema(input_schema, columns, "float64")

WOEEncoderApplier

Bases: BaseApplier

Replace each category with the Weight-of-Evidence value learned for it.

Both engines look the category up in the same fitted mapping, keyed by the scalar key of the value, and fall back to default for categories never seen at fit time. Nulls are keyed as the literal "nan" on both sides, so a missing category gets its own learned WOE instead of always taking the default (F-28). New artifacts distinguish numeric values from literal strings; older artifacts preserve their original string keys. Encoded columns become float.

Source code in skyulf-core/skyulf/preprocessing/encoding/woe.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
class WOEEncoderApplier(BaseApplier):
    """Replace each category with the Weight-of-Evidence value learned for it.

    Both engines look the category up in the same fitted mapping, keyed by the
    scalar key of the value, and fall back to ``default`` for categories
    never seen at fit time. Nulls are keyed as the literal ``"nan"`` on both
    sides, so a missing category gets its own learned WOE instead of always
    taking the default (F-28). New artifacts distinguish numeric values from
    literal strings; older artifacts preserve their original string keys.
    Encoded columns become float.
    """

    @staticmethod
    def validate_inference_state(raw: dict) -> dict:
        """Inspect learned mappings and their metadata without relearning target statistics."""
        fields = {"type", "columns", "mappings", "information_value", "default"}
        if isinstance(raw, dict) and "category_key_version" in raw:
            fields.add("category_key_version")
        if not local_state_fields(raw, "woe_encoder", fields, allow_empty=True):
            return raw
        columns = raw["columns"]
        if not isinstance(columns, (list, tuple)) or any(not isinstance(c, str) for c in columns):
            raise ValueError("Fitted WOE columns must be ordered strings.")
        uses_category_keys(raw)
        WOEEncoderApplier._validate_mappings(raw, columns)
        _validate_woe_values([raw["default"], *raw["information_value"].values()])
        return raw

    @staticmethod
    def _validate_mappings(raw: dict, columns: list[str]) -> None:
        """Keep every map and information-value entry attached to its fitted source column."""
        for field in ("mappings", "information_value"):
            if type(raw[field]) is not dict or set(raw[field]) != set(columns):
                raise ValueError("Fitted WOE maps and metadata must match selected columns.")
        for mapping in raw["mappings"].values():
            if type(mapping) is not dict or any(not isinstance(key, str) for key in mapping):
                raise ValueError("Fitted WOE maps must use string category keys.")
            _validate_woe_values(mapping.values())

    @staticmethod
    def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
        """Describe fixed WOE category lookups without granting partition execution."""
        if engine not in ("pandas", "polars"):
            return None
        WOEEncoderApplier.validate_inference_state(state)
        # Legacy pandas datetime string formatting can depend on neighboring rows.
        legacy_pandas = (
            engine == "pandas" and state.get("columns") and not uses_category_keys(state)
        )
        context = "global" if legacy_pandas else "row"
        return ExecutionCapability(engine, "apply", "local", "preserve", context)

    @apply_method
    def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
        """Dispatch to the engine-specific WOE lookup, forwarding ``(X, y)`` only when present."""
        return apply_dual_engine(
            (X, y) if y is not None else X,
            params,
            {"polars": _woe_apply_polars, "pandas": _woe_apply_pandas},
        )

apply(X, y, params)

Dispatch to the engine-specific WOE lookup, forwarding (X, y) only when present.

Source code in skyulf-core/skyulf/preprocessing/encoding/woe.py
167
168
169
170
171
172
173
174
@apply_method
def apply(self, X: Any, y: Any, params: dict[str, Any]) -> Any:  # pylint: disable=arguments-differ
    """Dispatch to the engine-specific WOE lookup, forwarding ``(X, y)`` only when present."""
    return apply_dual_engine(
        (X, y) if y is not None else X,
        params,
        {"polars": _woe_apply_polars, "pandas": _woe_apply_pandas},
    )

inference_capability(state, *, engine) staticmethod

Describe fixed WOE category lookups without granting partition execution.

Source code in skyulf-core/skyulf/preprocessing/encoding/woe.py
154
155
156
157
158
159
160
161
162
163
164
165
@staticmethod
def inference_capability(state: dict, *, engine: str) -> ExecutionCapability | None:
    """Describe fixed WOE category lookups without granting partition execution."""
    if engine not in ("pandas", "polars"):
        return None
    WOEEncoderApplier.validate_inference_state(state)
    # Legacy pandas datetime string formatting can depend on neighboring rows.
    legacy_pandas = (
        engine == "pandas" and state.get("columns") and not uses_category_keys(state)
    )
    context = "global" if legacy_pandas else "row"
    return ExecutionCapability(engine, "apply", "local", "preserve", context)

validate_inference_state(raw) staticmethod

Inspect learned mappings and their metadata without relearning target statistics.

Source code in skyulf-core/skyulf/preprocessing/encoding/woe.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
@staticmethod
def validate_inference_state(raw: dict) -> dict:
    """Inspect learned mappings and their metadata without relearning target statistics."""
    fields = {"type", "columns", "mappings", "information_value", "default"}
    if isinstance(raw, dict) and "category_key_version" in raw:
        fields.add("category_key_version")
    if not local_state_fields(raw, "woe_encoder", fields, allow_empty=True):
        return raw
    columns = raw["columns"]
    if not isinstance(columns, (list, tuple)) or any(not isinstance(c, str) for c in columns):
        raise ValueError("Fitted WOE columns must be ordered strings.")
    uses_category_keys(raw)
    WOEEncoderApplier._validate_mappings(raw, columns)
    _validate_woe_values([raw["default"], *raw["information_value"].values()])
    return raw

WOEEncoderCalculator

Bases: BaseCalculator

Fit the WOE mapping and per-column Information Value from a binary target.

Supervised and binary-only: a target without exactly two classes raises a descriptive error before fitting. The target column is excluded from the encoded set, since WOE computed against itself is near-perfect separation. The math is engine-agnostic, so fit funnels both engines through one narrow pandas boundary while apply stays in the caller's engine. fit_transform_train cross-fits the training rows so a row's own target never leaks into its encoding.

regularization adds a pseudocount to each observed category in each target class. Class totals include regularization * n_categories so both smoothed distributions sum to one. Missing feature values count as a category; each cross-fitting complement uses its own observed categories.

Source code in skyulf-core/skyulf/preprocessing/encoding/woe.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
@NodeRegistry.register("WOEEncoder", WOEEncoderApplier)
@node_meta(
    id="WOEEncoder",
    name="WOE / IV Encoder",
    category="Preprocessing",
    description=(
        "Weight-of-Evidence encoder for binary classification. Replaces each "
        "category with its log-odds and records Information Value per column."
    ),
    params={"regularization": 0.5, "columns": []},
    learns_from_data=True,
)
class WOEEncoderCalculator(BaseCalculator):
    """Fit the WOE mapping and per-column Information Value from a binary target.

    Supervised and binary-only: a target without exactly two classes raises a
    descriptive error before fitting. The target column
    is excluded from the encoded set, since WOE computed against itself is
    near-perfect separation. The math is engine-agnostic, so fit funnels both
    engines through one narrow pandas boundary while ``apply`` stays in the
    caller's engine. ``fit_transform_train`` cross-fits the training rows so a
    row's own target never leaks into its encoding.

    ``regularization`` adds a pseudocount to each observed category in each
    target class. Class totals include ``regularization * n_categories`` so
    both smoothed distributions sum to one. Missing feature values count as
    a category; each cross-fitting complement uses its own observed categories.
    """

    @fit_method
    def fit(self, X: Any, y: Any, config: dict[str, Any]) -> Mapping[str, Any]:  # pylint: disable=arguments-differ
        """Short-circuit an explicit empty column selection, else fit via the shared boundary.

        Both engine keys dispatch to the same ``_woe_fit``: it converts to
        pandas internally, so there is no engine-specific fit to choose between.
        """
        if user_picked_no_columns(config):
            return {}
        return cast(
            Mapping[str, Any],
            fit_dual_engine(
                (X, y) if y is not None else X, config, {"polars": _woe_fit, "pandas": _woe_fit}
            ),
        )

    def fit_transform_train(
        self, df: pd.DataFrame | SkyulfDataFrame | tuple, config: dict[str, Any]
    ) -> tuple[Mapping[str, Any], Any]:
        """Fit the full-data WOE artifact and cross-fit the training rows."""
        if user_picked_no_columns(config):
            return {}, df

        artifact, transformed = fit_transform_train_dual_engine(
            df,
            config,
            {"polars": _woe_fit_transform_train_polars, "pandas": _woe_fit_transform_train_pandas},
        )
        return artifact, transformed

    def infer_output_schema(
        self,
        input_schema: SkyulfSchema,
        config: dict[str, Any],
    ) -> SkyulfSchema | None:
        """Describe explicit floating WOE values while preserving the excluded target."""
        columns = config.get("columns")
        if columns is not None:
            columns = [col for col in columns if col != config.get("target_column")]
        return encoded_columns_schema(input_schema, columns, "float64")

fit(X, y, config)

Short-circuit an explicit empty column selection, else fit via the shared boundary.

Both engine keys dispatch to the same _woe_fit: it converts to pandas internally, so there is no engine-specific fit to choose between.

Source code in skyulf-core/skyulf/preprocessing/encoding/woe.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
@fit_method
def fit(self, X: Any, y: Any, config: dict[str, Any]) -> Mapping[str, Any]:  # pylint: disable=arguments-differ
    """Short-circuit an explicit empty column selection, else fit via the shared boundary.

    Both engine keys dispatch to the same ``_woe_fit``: it converts to
    pandas internally, so there is no engine-specific fit to choose between.
    """
    if user_picked_no_columns(config):
        return {}
    return cast(
        Mapping[str, Any],
        fit_dual_engine(
            (X, y) if y is not None else X, config, {"polars": _woe_fit, "pandas": _woe_fit}
        ),
    )

fit_transform_train(df, config)

Fit the full-data WOE artifact and cross-fit the training rows.

Source code in skyulf-core/skyulf/preprocessing/encoding/woe.py
416
417
418
419
420
421
422
423
424
425
426
427
428
def fit_transform_train(
    self, df: pd.DataFrame | SkyulfDataFrame | tuple, config: dict[str, Any]
) -> tuple[Mapping[str, Any], Any]:
    """Fit the full-data WOE artifact and cross-fit the training rows."""
    if user_picked_no_columns(config):
        return {}, df

    artifact, transformed = fit_transform_train_dual_engine(
        df,
        config,
        {"polars": _woe_fit_transform_train_polars, "pandas": _woe_fit_transform_train_pandas},
    )
    return artifact, transformed

infer_output_schema(input_schema, config)

Describe explicit floating WOE values while preserving the excluded target.

Source code in skyulf-core/skyulf/preprocessing/encoding/woe.py
430
431
432
433
434
435
436
437
438
439
def infer_output_schema(
    self,
    input_schema: SkyulfSchema,
    config: dict[str, Any],
) -> SkyulfSchema | None:
    """Describe explicit floating WOE values while preserving the excluded target."""
    columns = config.get("columns")
    if columns is not None:
        columns = [col for col in columns if col != config.get("target_column")]
    return encoded_columns_schema(input_schema, columns, "float64")

detect_categorical_columns(df)

Auto-detect the string/categorical columns an encoder should target.

The fallback every encoder passes to resolve_columns when the user configured no explicit column list: polars frames are matched on Utf8/Categorical/Enum/Object dtypes, pandas frames via select_dtypes on object/category/string — so "encode categoricals" means the same thing on either engine.

Source code in skyulf-core/skyulf/preprocessing/encoding/_common.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def detect_categorical_columns(df: Any) -> list[str]:
    """Auto-detect the string/categorical columns an encoder should target.

    The fallback every encoder passes to ``resolve_columns`` when the user
    configured no explicit column list: polars frames are matched on
    ``Utf8``/``Categorical``/``Enum``/``Object`` dtypes, pandas frames via
    ``select_dtypes`` on ``object``/``category``/``string`` — so "encode categoricals"
    means the same thing on either engine.
    """
    engine = get_engine(df)
    if engine.name == EngineName.POLARS:
        df_pl: Any = df
        return [
            c
            for c, t in zip(df_pl.columns, df_pl.dtypes, strict=True)
            if t in [pl.Utf8, pl.Categorical, pl.Object] or isinstance(t, pl.Enum)
        ]
    return [
        col
        for col in df.select_dtypes(include=["object", "category", "string"]).columns
        if not is_decimal_series(df[col])
    ]