Source code for pretab.transformers.binning.binning

from typing import ClassVar

import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin

from ...core.exceptions import InsufficientSamplesError, InvalidParamError, PretabDataError
from ...core.params import UNSET, AliasResolverMixin


[docs] class CustomBinTransformer(AliasResolverMixin, TransformerMixin, BaseEstimator): """ Custom binning transformer for one-dimensional numerical features. This transformer bins continuous values into discrete intervals, using either a fixed number of equal-width bins or a user-provided array of bin edges. It is compatible with scikit-learn pipelines. Parameters ---------- output_dim : int or array-like If int, defines the number of equal-width bins. If array-like, defines the bin edges to use directly. Note that ``output_dim`` here is the number of *bins*, not the number of output columns: this transformer always emits a single ordinal column of integer bin indices. The bin count only becomes an output width after a subsequent one-hot encoding. Attributes ---------- n_features_in_ : int The number of input features seen during ``fit`` (expected to be 1). total_output_dim_ : int Total number of output columns (fitted). Always ``1`` because the output is a single ordinal column. Notes ----- This transformer operates on a single feature of shape ``(n_samples, 1)``. When ``output_dim`` is an integer, equal-width bin edges are computed from the data range; when it is an array-like, the provided edges are used directly. The output contains integer bin indices in a single column, so its width is ``1`` regardless of ``output_dim`` -- this is a documented exception to the exact-width contract that the fixed-basis families follow. The input must be numeric: binning is performed with :func:`pandas.cut`, so string / categorical data cannot be processed and raises a :class:`~pretab.core.exceptions.PretabDataError`. Encode such columns with a categorical method (e.g. ``"int"`` or ``"one-hot"``) before binning. Examples -------- >>> import numpy as np >>> from pretab.transformers import CustomBinTransformer >>> X = np.linspace(0, 1, 10).reshape(-1, 1) >>> transformer = CustomBinTransformer(output_dim=4) >>> transformer.fit_transform(X).shape (10, 1) """ _param_aliases: ClassVar[dict[str, str]] = {}
[docs] def __init__(self, output_dim=UNSET): # An int yields equal-width bins; an array-like is used as bin edges. self.output_dim = output_dim
[docs] def fit(self, X, y=None): """ Fit the transformer on the data. Parameters ---------- X : array-like of shape (n_samples, 1) Input data. y : Ignored Not used, present here for API consistency by convention. Returns ------- self : object Fitted transformer. """ # Fit doesn't need to do anything as we are directly using provided bins X = np.asarray(X) self.n_features_in_ = X.shape[1] if X.ndim > 1 else 1 self.total_output_dim_ = 1 return self
[docs] def transform(self, X): """ Transform the data using the specified binning strategy. Parameters ---------- X : array-like of shape (n_samples, 1) Input data to transform. Returns ------- X_binned : ndarray of shape (n_samples, 1) Binned data with integer bin indices. """ X = np.asarray(X) # Ensures squeeze works and consistent input if X.ndim != 2 or X.shape[1] != 1: raise PretabDataError("Input must be a 2D array with shape (n_samples, 1).") if X.shape[0] <= 2: raise InsufficientSamplesError("Input must have more than 2 observations.") if not np.issubdtype(X.dtype, np.number): try: X = X.astype(np.float64) except (ValueError, TypeError) as exc: raise PretabDataError( "CustomBinTransformer requires numeric input: it bins continuous " "values with pandas.cut and cannot process string/categorical " "data. Encode string columns with a categorical method (e.g. " "'int' or 'one-hot') before binning." ) from exc bins_spec = self._resolve_param("output_dim", default=UNSET) if bins_spec is UNSET: raise InvalidParamError("CustomBinTransformer requires 'output_dim'.") if isinstance(bins_spec, int): # Calculate equal width bins based on the range of the data and number of bins _, bins = pd.cut(X.squeeze(), bins=bins_spec, retbins=True) else: # Use predefined bins bins = bins_spec # Apply the bins to the data binned_data = pd.cut( # type: ignore X.squeeze(), bins=np.sort(np.unique(bins)), # type: ignore labels=False, include_lowest=True, ) return np.expand_dims(np.array(binned_data), 1)
[docs] def get_feature_names_out(self, input_features=None): """Return the names of the transformed features. Parameters ---------- input_features : list of str The names of the input features. Returns ------- input_features : ndarray of shape (n_features,) The names of the output features after transformation. """ if input_features is None: raise InvalidParamError("input_features must be specified") return input_features