Source code for mojolearn._x_sequence_croston

# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Andrew Hendel. Part of mojolearn, https://doi.org/10.5281/zenodo.22068632
"""Croston forecasters for intermittent demand: CrostonClassic,
CrostonOptimized and CrostonSBA, following statsforecast's models of the same
names. Each series is fitted on the GPU."""
from ._optional_numpy import require_numpy
np = require_numpy('_x_sequence_croston')

from . import _backend


class _Croston:
    """Shared fit/predict for the Croston variants (one series per GPU thread,
    `sequence/croston.mojo`)."""
    _VARIANT = 0

    def __init__(self, alias=None, numeric_mode=None):
        self.alias = alias
        self.numeric_mode = numeric_mode

    def fit(self, y, X=None):
        """Fit one series or a batch of series.

        Parameters
        ----------
        y : array-like of shape (n_obs,) or (batch_size, n_obs)
            The demand series, float32. One row per series for a batch.
        X : None
            Exogenous regressors are not supported; anything other than
            None raises NotImplementedError.

        Returns
        -------
        self
            The fitted model, with ``mean_`` set.

        Raises
        ------
        TypeError
            If ``y`` is float64.
        ValueError
            If a series has no observations.
        """
        if X is not None:
            raise NotImplementedError("Croston: exogenous regressors are not supported (nor in the reference)")
        y = np.asarray(y)
        if y.dtype == np.float64:
            raise TypeError("Croston: float64 is refused; pass float32")
        self._one = y.ndim == 1
        self._y = np.ascontiguousarray(y, dtype=np.float32).reshape(1, -1) if self._one else \
            np.ascontiguousarray(y, dtype=np.float32)
        if self._y.ndim != 2 or self._y.shape[1] < 1:
            raise ValueError("Croston: y must hold at least one observation per series")
        B, n = self._y.shape
        self.mean_ = np.zeros(B, dtype=np.float32)
        _backend.binding("_mojolearn_x_sequence", self.numeric_mode).croston(
            [self._y.ctypes.data, self.mean_.ctypes.data], [B, n, self._VARIANT])
        return self

    def predict(self, h, X=None, level=None):
        """Forecast h steps ahead.

        The Croston forecast is flat: each series' fitted demand rate,
        repeated ``h`` times.

        Parameters
        ----------
        h : int
            Forecast horizon, ``h >= 0``.
        X : None
            Unused.
        level : None
            Prediction intervals are not supported; anything other than None
            raises NotImplementedError.

        Returns
        -------
        dict
            ``{"mean": forecasts}``, shaped ``(h,)`` for a single series or
            ``(batch_size, h)`` for a batch.
        """
        if level is not None:
            raise NotImplementedError("Croston: prediction intervals are not implemented")
        h = int(h)
        if h < 0:
            raise ValueError("Croston: h must be >= 0")
        B = self.mean_.shape[0]
        f = np.empty((B, h), dtype=np.float32)
        # every row its series' mean, in Mojo (`croston_forecast`)
        if h:
            _backend.binding("_mojolearn_x_sequence", self.numeric_mode).croston_forecast(
                [self.mean_.ctypes.data, f.ctypes.data], [B, h])
        return {"mean": f[0] if self._one else f}

    def forecast(self, y, h, X=None, X_future=None, level=None, fitted=False):
        """Fit ``y`` and forecast ``h`` steps in one call.

        Equivalent to ``fit(y).predict(h)``. ``fitted=True`` (in-sample
        fitted values) raises NotImplementedError.

        Returns
        -------
        dict
            ``{"mean": forecasts}``, as returned by :meth:`predict`.
        """
        if fitted:
            raise NotImplementedError("Croston: fitted values are not implemented")
        return self.fit(y).predict(h, level=level)


[docs] class CrostonClassic(_Croston): """Croston's method for intermittent demand. Simple exponential smoothing (alpha 0.1) of the nonzero demands and of the intervals between them; the forecast is their ratio. Follows statsforecast's ``CrostonClassic``. ``fit`` takes one series or a batch of shape ``(batch_size, n_obs)``. Parameters ---------- alias : str or None, default None A display name for the model, stored as ``alias``. numeric_mode : str or None, default None Numeric mode for this model (``'identical'``, ``'deterministic'`` or ``'fast'``); None uses the process default (see :func:`mojolearn.set_numeric_mode`). Attributes ---------- mean_ : ndarray of shape (batch_size,), float32 The fitted demand rate of each series; the point forecast. Notes ----- Input must be float32; float64 raises TypeError. Prediction intervals, fitted values and exogenous regressors are not supported and raise NotImplementedError. Examples -------- >>> import numpy as np, mojolearn >>> rng = np.random.default_rng(0) >>> y = (rng.random(120) < 0.3).astype(np.float32) * rng.integers(1, 10, 120).astype(np.float32) >>> mojolearn.CrostonClassic().fit(y).predict(12)["mean"].shape (12,) """ _VARIANT = 0
[docs] class CrostonOptimized(_Croston): """Croston's method with optimized smoothing parameters. As :class:`CrostonClassic`, but each smoothing parameter (for demands and for intervals) is chosen by golden-section search on [0.1, 0.3]. Follows statsforecast's ``CrostonOptimized``. ``fit`` takes one series or a batch of shape ``(batch_size, n_obs)``. Parameters ---------- alias : str or None, default None A display name for the model, stored as ``alias``. numeric_mode : str or None, default None Numeric mode for this model (``'identical'``, ``'deterministic'`` or ``'fast'``); None uses the process default (see :func:`mojolearn.set_numeric_mode`). Attributes ---------- mean_ : ndarray of shape (batch_size,), float32 The fitted demand rate of each series; the point forecast. Notes ----- Input must be float32; float64 raises TypeError. Prediction intervals, fitted values and exogenous regressors are not supported and raise NotImplementedError. Examples -------- >>> import numpy as np, mojolearn >>> rng = np.random.default_rng(0) >>> y = (rng.random(120) < 0.3).astype(np.float32) * rng.integers(1, 10, 120).astype(np.float32) >>> mojolearn.CrostonOptimized().fit(y).predict(12)["mean"].shape (12,) """ _VARIANT = 1
[docs] class CrostonSBA(_Croston): """Syntetos-Boylan approximation (SBA) of Croston's method. The :class:`CrostonClassic` forecast multiplied by 0.95, which corrects Croston's bias. Follows statsforecast's ``CrostonSBA``. ``fit`` takes one series or a batch of shape ``(batch_size, n_obs)``. Parameters ---------- alias : str or None, default None A display name for the model, stored as ``alias``. numeric_mode : str or None, default None Numeric mode for this model (``'identical'``, ``'deterministic'`` or ``'fast'``); None uses the process default (see :func:`mojolearn.set_numeric_mode`). Attributes ---------- mean_ : ndarray of shape (batch_size,), float32 The fitted demand rate of each series; the point forecast. Notes ----- Input must be float32; float64 raises TypeError. Prediction intervals, fitted values and exogenous regressors are not supported and raise NotImplementedError. Examples -------- >>> import numpy as np, mojolearn >>> rng = np.random.default_rng(0) >>> y = (rng.random(120) < 0.3).astype(np.float32) * rng.integers(1, 10, 120).astype(np.float32) >>> mojolearn.CrostonSBA().fit(y).predict(12)["mean"].shape (12,) """ _VARIANT = 2