# Path: formulaic/materializers/types/factor_values.py
# class FactorValues(Generic[T], wrapt.ObjectProxy):
#     """
#     A convenience wrapper that surfaces a `FactorValuesMetadata` instance at
#     `<object>.__formulaic_metadata__`. This wrapper can otherwise wrap any
#     object and behaves just like that object.
#     """
# 
#     def __init__(
#         self,
#         values,
#         *,
#         metadata: FactorValuesMetadata = MISSING,
#         kind: Union[str, Factor.Kind] = MISSING,
#         column_names: Tuple[str] = MISSING,
#         spans_intercept: bool = MISSING,
#         drop_field: Optional[str] = MISSING,
#         format: str = MISSING,
#         encoded: bool = MISSING,
#     ):
#         metadata_constructor = FactorValuesMetadata
#         metadata_kwargs = dict(
#             kind=Factor.Kind(kind) if kind is not MISSING else kind,
#             column_names=column_names,
#             spans_intercept=spans_intercept,
#             drop_field=drop_field,
#             format=format,
#             encoded=encoded,
#         )
#         for key in set(metadata_kwargs):
#             if metadata_kwargs[key] is MISSING:
#                 metadata_kwargs.pop(key)
# 
#         if hasattr(values, "__formulaic_metadata__"):
#             metadata_constructor = values.__formulaic_metadata__.replace
#             if isinstance(values, FactorValues):
#                 values = values.__wrapped__
# 
#         if metadata:
#             metadata_constructor = metadata.replace
# 
#         wrapt.ObjectProxy.__init__(self, values)
#         self._self_metadata = metadata_constructor(**metadata_kwargs)
# 
#     @property
#     def __formulaic_metadata__(self) -> FactorValuesMetadata:
#         return self._self_metadata
# 
#     def __repr__(self) -> str:
#         return self.__wrapped__.__repr__()  # pragma: no cover
# 
# Path: formulaic/utils/stateful_transforms.py
# def stateful_transform(func: Callable) -> Callable:
#     """
#     Transform a callable object into a stateful transform.
# 
#     This is done by adding special arguments to the callable's signature:
#     - _state: The existing state or an empty dictionary.
#     - _metadata: Any extra metadata passed about the factor being evaluated.
#     - _spec: The `ModelSpec` instance being evaluated (or an empty `ModelSpec`).
#     If the callable has any of these in its signature, these will be passed onto
#     it; otherwise, they will be swallowed by the stateful transform wrapper.
# 
#     Stateful transforms are also transformed into single dispatches, allowing
#     different implementations for incoming data types.
# 
#     Args:
#         func: The function (or other callable) to be made into a stateful
#             transform.
# 
#     Returns:
#         The stateful transform callable.
#     """
#     func = functools.singledispatch(func)
#     params = inspect.signature(func).parameters.keys()
# 
#     @functools.wraps(func)
#     def wrapper(data, *args, _metadata=None, _state=None, _spec=None, **kwargs):
#         from formulaic.model_spec import ModelSpec
# 
#         _state = {} if _state is None else _state
#         extra_params = {}
#         if "_metadata" in params:
#             extra_params["_metadata"] = _metadata
#         if "_spec" in params:
#             extra_params["_spec"] = _spec or ModelSpec([])
# 
#         if isinstance(data, dict):
#             results = {}
#             for key, datum in data.items():
#                 if isinstance(key, str) and key.startswith("__"):
#                     results[key] = datum
#                 else:
#                     statum = _state.get(key, {})
#                     results[key] = wrapper(
#                         datum, *args, _state=statum, **extra_params, **kwargs
#                     )
#                     if statum:
#                         _state[key] = statum
#             return results
#         return func(data, *args, _state=_state, **extra_params, **kwargs)
# 
#     wrapper.__is_stateful_transform__ = True
#     return wrapper


from collections import defaultdict
from enum import Enum
from typing import Iterable, Optional, Union
from formulaic.materializers.types import FactorValues
from formulaic.utils.stateful_transforms import stateful_transform
import numpy
import pandas
        lambda i, j: (x - knots[i]) / (knots[i + j] - knots[i])
        if knots[i + j] != knots[i]
        else 0
    )
    for i in range(len(knots) - 1):
        if extrapolation is SplineExtrapolation.EXTEND:
            cache[0][i] = (
                (x >= (knots[i] if i != degree else -numpy.inf))
                & (
                    x
                    < (knots[i + 1] if i + 1 != len(knots) - degree - 1 else numpy.inf)
                )
            ).astype(float)
        else:
            cache[0][i] = (
                (x >= knots[i])
                & (
                    (x < knots[i + 1])
                    if i + 1 != len(knots) - degree - 1
                    else (x <= knots[i + 1])  # Properly handle boundary
                )
            ).astype(float)
    for d in range(1, degree + 1):
        cache[d % 2].clear()
        for i in range(len(knots) - d - 1):
            cache[d % 2][i] = (
                alpha(i, d) * cache[(d - 1) % 2][i]
                + (1 - alpha(i + 1, d)) * cache[(d - 1) % 2][i + 1]
            )


# Path: formulaic/materializers/types/factor_values.py
# class FactorValues(Generic[T], wrapt.ObjectProxy):
#     """
#     A convenience wrapper that surfaces a `FactorValuesMetadata` instance at
#     `<object>.__formulaic_metadata__`. This wrapper can otherwise wrap any
#     object and behaves just like that object.
#     """
# 
#     def __init__(
#         self,
#         values,
#         *,
#         metadata: FactorValuesMetadata = MISSING,
#         kind: Union[str, Factor.Kind] = MISSING,
#         column_names: Tuple[str] = MISSING,
#         spans_intercept: bool = MISSING,
#         drop_field: Optional[str] = MISSING,
#         format: str = MISSING,
#         encoded: bool = MISSING,
#     ):
#         metadata_constructor = FactorValuesMetadata
#         metadata_kwargs = dict(
#             kind=Factor.Kind(kind) if kind is not MISSING else kind,
#             column_names=column_names,
#             spans_intercept=spans_intercept,
#             drop_field=drop_field,
#             format=format,
#             encoded=encoded,
#         )
#         for key in set(metadata_kwargs):
#             if metadata_kwargs[key] is MISSING:
#                 metadata_kwargs.pop(key)
# 
#         if hasattr(values, "__formulaic_metadata__"):
#             metadata_constructor = values.__formulaic_metadata__.replace
#             if isinstance(values, FactorValues):
#                 values = values.__wrapped__
# 
#         if metadata:
#             metadata_constructor = metadata.replace
# 
#         wrapt.ObjectProxy.__init__(self, values)
#         self._self_metadata = metadata_constructor(**metadata_kwargs)
# 
#     @property
#     def __formulaic_metadata__(self) -> FactorValuesMetadata:
#         return self._self_metadata
# 
#     def __repr__(self) -> str:
#         return self.__wrapped__.__repr__()  # pragma: no cover
# 
# Path: formulaic/utils/stateful_transforms.py
# def stateful_transform(func: Callable) -> Callable:
#     """
#     Transform a callable object into a stateful transform.
# 
#     This is done by adding special arguments to the callable's signature:
#     - _state: The existing state or an empty dictionary.
#     - _metadata: Any extra metadata passed about the factor being evaluated.
#     - _spec: The `ModelSpec` instance being evaluated (or an empty `ModelSpec`).
#     If the callable has any of these in its signature, these will be passed onto
#     it; otherwise, they will be swallowed by the stateful transform wrapper.
# 
#     Stateful transforms are also transformed into single dispatches, allowing
#     different implementations for incoming data types.
# 
#     Args:
#         func: The function (or other callable) to be made into a stateful
#             transform.
# 
#     Returns:
#         The stateful transform callable.
#     """
#     func = functools.singledispatch(func)
#     params = inspect.signature(func).parameters.keys()
# 
#     @functools.wraps(func)
#     def wrapper(data, *args, _metadata=None, _state=None, _spec=None, **kwargs):
#         from formulaic.model_spec import ModelSpec
# 
#         _state = {} if _state is None else _state
#         extra_params = {}
#         if "_metadata" in params:
#             extra_params["_metadata"] = _metadata
#         if "_spec" in params:
#             extra_params["_spec"] = _spec or ModelSpec([])
# 
#         if isinstance(data, dict):
#             results = {}
#             for key, datum in data.items():
#                 if isinstance(key, str) and key.startswith("__"):
#                     results[key] = datum
#                 else:
#                     statum = _state.get(key, {})
#                     results[key] = wrapper(
#                         datum, *args, _state=statum, **extra_params, **kwargs
#                     )
#                     if statum:
#                         _state[key] = statum
#             return results
#         return func(data, *args, _state=_state, **extra_params, **kwargs)
# 
#     wrapper.__is_stateful_transform__ = True
#     return wrapper


# Path: formulaic/transforms/basis_spline.py
from collections import defaultdict
from enum import Enum
from typing import Iterable, Optional, Union
from formulaic.materializers.types import FactorValues
from formulaic.utils.stateful_transforms import stateful_transform
import numpy
import pandas

        lambda i, j: (x - knots[i]) / (knots[i + j] - knots[i])
        if knots[i + j] != knots[i]
        else 0
    )
    for i in range(len(knots) - 1):
        if extrapolation is SplineExtrapolation.EXTEND:
            cache[0][i] = (
                (x >= (knots[i] if i != degree else -numpy.inf))
                & (
                    x
                    < (knots[i + 1] if i + 1 != len(knots) - degree - 1 else numpy.inf)
                )
            ).astype(float)
        else:
            cache[0][i] = (
                (x >= knots[i])
                & (
                    (x < knots[i + 1])
                    if i + 1 != len(knots) - degree - 1
                    else (x <= knots[i + 1])  # Properly handle boundary
                )
            ).astype(float)
    for d in range(1, degree + 1):
        cache[d % 2].clear()
        for i in range(len(knots) - d - 1):
            cache[d % 2][i] = (
                alpha(i, d) * cache[(d - 1) % 2][i]
                + (1 - alpha(i + 1, d)) * cache[(d - 1) % 2][i + 1]
            )


    return FactorValues(