Source code for pyfru.random_forest

import secrets
import sys
from abc import ABC

from pyfru.data_structures import ImportanceResultTable, ResultArray, ResultTable

from . import _rust


class _RandomForestBase(ABC):
    def __init__(
        self,
        trees=500,
        tries=None,
        save_forest=True,
        calculate_importance=False,
        oob=False,
        seed=None,
        threads=None,
    ):
        self.trees = trees
        self.tries = tries
        self.save_forest = save_forest
        self.calculate_importance = calculate_importance
        self.oob = oob
        self.seed = seed
        self.threads = threads
        self._forest = None

    def __getstate__(self):
        state = self.__dict__.copy()
        if self._forest is not None:
            state["forest_bytes"] = self._forest.to_bytes()
            del state["_forest"]
        return state

    def __setstate__(self, state):
        if "forest_bytes" in state:
            forest = _rust.RandomForest.from_bytes(state["forest_bytes"])
            self._forest = forest
            del state["forest_bytes"]
        self.__dict__.update(state)

    def importance(self, scale=False, to_pycapsule=False):
        """
        Extract permutation (mean decrease in accuracy) importance from the model.

        Parameters
        ----------
        scale : bool
            If ``True``, values are scaled by their standard deviation over the ensemble.
            Defaults to ``False``.
        to_pycapsule : bool
            If ``True``, results are returned as an Arrow PyCapsule. If ``False``, results
            are returned as NumPy arrays, similar to scikit-learn. Defaults to ``False``.

        Returns
        -------
        numpy.ndarray or Arrow PyCapsule
            By default, a 1D NumPy array is returned. It contains the importance
            value for each column. If ``to_pycapsule=True``, an object implementing
            the Arrow PyCapsule interface is returned. This object can be loaded as
            a DataFrame in libraries supporting the Arrow PyCapsule interface. It
            has two columns: ``column`` and ``importance``.

        Notes
        -----
        Other packages often scale importance by its standard error estimate,
        producing importance values larger by a factor of the square root of
        the number of trees compared to fru.
        """
        importance = self._forest.importance(scale)
        return ImportanceResultTable(importance, to_pycapsule=to_pycapsule).get_array()

    def _get_seed(self):
        return self.seed if self.seed is not None else secrets.randbits(64)

    @staticmethod
    def _remove_pandas_rownames(obj):
        pd = sys.modules.get("pandas")

        # Remove pandas rownames, otherwise would be passed as __index_level_0__ to arrow
        if pd and isinstance(obj, pd.DataFrame):
            return obj.reset_index(drop=True)
        return obj

    @classmethod
    def _validate_x(cls, X):
        if not hasattr(X, "__arrow_c_stream__"):
            raise AttributeError("X must implement PyCapsule")
        X = cls._remove_pandas_rownames(X)
        return X

    @staticmethod
    def _validate_y(y):
        if not hasattr(y, "__arrow_c_stream__"):
            raise AttributeError("y must implement PyCapsule")
        return y


[docs] class RandomForestClassifier(_RandomForestBase): """ Random Forest Classifier implementing a fast multi-thread implementation of the original Random Forest model [1]_. Thanks to the Arrow PyCapsule interface, Pyfru supports a wide range of DataFrame libraries. In addition to prediction, it supports out-of-bag (OOB) predictions and feature importance. The importance measure is computed in a novel, efficient way. Parameters ---------- trees : int Number of trees to grow in the forest (often called ``ntree`` in other software). Must be greater than zero. The value should be large enough to provide stable results (prediction accuracy or importance). Larger datasets typically require more trees. Computation time grows linearly with the number of trees. Defaults to 500. tries : int | None Number of features to try at each split (often called ``mtry``). Must be greater than zero and less than or equal to the number of features. By default, it is set to the rounded square root of the number of features. Higher values increase correlation between trees. In most cases, the default setting is recommended. save_forest : bool If ``True``, the fitted forest is stored and can be used for prediction or serialization. Set to ``False`` when only importance or OOB results are needed. Defaults to ``True``. calculate_importance : bool If ``True``, feature importance is calculated. Defaults to ``False``. oob : bool If ``True``, out-of-bag predictions are calculated. Defaults to ``True``. seed : int | None Seed used by the algorithm. Set to ``None`` to use a random seed. threads : int | None Number of threads to use. Must be greater than zero. If ``None``, all available CPU cores are used. Defaults to ``None``. Notes ----- The Random Forest Classifier selects the best feature at each split using Gini impurity. For numerical features, the threshold is optimized by an exhaustive scan. The threshold is the midpoint between adjacent values. In case of ties, the smaller threshold is chosen. Ordered categorical variables are currently treated as categorical. Variables with six or more levels are treated as integers. Variables with five or fewer levels are split by exhaustively evaluating all possible partitions into two subsets using the Gini criterion. The maximum tree depth is fixed at 512. A leaf is created when the sample size reaches one. Leaves may also be formed earlier if no valid split is found; in such cases, ties are broken randomly. Pyfru uses its own PRNG, the pcg32 method by Melissa E. O'Neill [2]_, to provide reproducible results in parallel settings. For a given input and seed, the same trees are built regardless of the number of threads, although their order may differ. OOB predictions and importance scores are therefore consistent up to numerical precision. References ---------- .. [1] `L. Breiman, Random Forests, Machine Learning, 45(1), 5-32, 2001. <https://doi.org/10.1023/A:1010933404324>`_ .. [2] `O'Neil Melissa E. (2014). PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation. <https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf>`_ """
[docs] def fit(self, X, y): """ Build a forest of trees from the training data. Parameters ---------- X : Arrow PyCapsule A DataFrame-like object supporting the Arrow PyCapsule interface. Any library supporting this interface can be used (e.g., pandas, polars). Columns can be boolean, numerical, or categorical. Mixed column types are allowed. Other data types are not supported and will raise an exception. ``NaN`` values are not allowed. y : Arrow PyCapsule A Series-like object supporting the Arrow PyCapsule interface. Any library supporting this interface can be used. For classification, ``y`` must be categorical, otherwise an exception is raised. ``NaN`` values are not allowed. The length of ``y`` must match the number of rows in ``X``. """ y = self._validate_y(y) X = self._validate_x(X) self._forest = _rust.RandomForest( X, y, self.trees, self.tries, self.save_forest, self.calculate_importance, self.oob, self._get_seed(), True, self.threads, )
[docs] def predict(self, X=None, votes=False, validate_input=True, to_pycapsule=False): """ Predict for X. The predicted class is the one with the highest number of votes across the trees. If no X is given, out-of-bag predictions are returned. If ``votes=True``, vote counts are returned instead of predictions. Parameters ---------- X : Arrow PyCapsule | None A DataFrame-like object supporting the Arrow PyCapsule interface. Cannot contain ``NaN`` values. If ``None`` (default), out-of-bag predictions are returned. votes: bool If ``True``, the number of votes from each tree is returned instead of predictions. Defaults to ``False``. validate_input : bool If ``True``, column data types and names in ``X`` are checked against the data provided to ``fit``. For categorical columns, category names and their order must match. to_pycapsule : bool, optional If ``True``, results are returned as an Arrow PyCapsule. Otherwise, NumPy arrays are returned. Defaults to ``False``. Returns ------- numpy.ndarray or Arrow PyCapsule For predictions, a 1D NumPy array is returned with one prediction per observation. Categorical predictions are represented as strings. For votes, a 2D NumPy array is returned. If ``to_pycapsule=True``, results are returned as an Arrow PyCapsule that can be loaded as a Series in libraries supporting this interface. Notes ----- Voting in classification may result in ties. In such cases, a PRNG is used to break ties. If ``seed=None``, different seeds are used on each execution, so ties may be resolved differently. For deterministic behavior, set ``seed`` or use ``votes=True`` to inspect vote counts. This method checks that the input structure matches the training data structure stored in the object. This may take time for large data or low-latency use cases. Set ``validate_input=False`` to skip validation and assume the input is correct. """ if not votes: if X is None: preds = self._forest.oob(self._get_seed()) else: X = self._validate_x(X) preds = self._forest.predict( X, self._get_seed(), validate_input, self.threads ) return ResultArray(preds, to_pycapsule=to_pycapsule).get_array() if X is None: votes = self._forest.oob_votes() else: X = self._validate_x(X) votes = self._forest.predict_votes(X, validate_input, self.threads) return ResultTable(votes, to_pycapsule=to_pycapsule).get_df()
[docs] class RandomForestRegressor(_RandomForestBase): """ Random Forest Regressor implementing a fast multi-threaded version of the original Random Forest model [3]_. Thanks to the Arrow PyCapsule interface, Pyfru supports a wide range of DataFrame libraries. In addition to prediction, it supports out-of-bag (OOB) predictions and feature importance. The importance measure is computed in a novel, efficient way. Parameters ---------- trees : int Number of trees to grow in the forest (often called ``ntree`` in other software). Must be greater than zero. The value should be large enough to provide stable results (prediction accuracy or importance). Larger datasets typically require more trees. Computation time grows linearly with the number of trees. Defaults to 500. tries : int | None Number of features to try at each split (often called ``mtry``). Must be greater than zero and less than or equal to the number of features. By default, it is set to the rounded square root of the number of features. Higher values increase correlation between trees. In most cases, the default setting is recommended. save_forest : bool If ``True``, the fitted forest is stored and can be used for prediction or serialization. Set to ``False`` when only importance or OOB results are needed. Defaults to ``True``. calculate_importance : bool If ``True``, feature importance is calculated. Defaults to ``False``. oob : bool If ``True``, out-of-bag predictions are calculated. Defaults to ``True``. seed : int | None Seed used by the algorithm. Set to ``None`` to use a random seed. threads : int | None Number of threads to use. Must be greater than zero. If ``None``, all available CPU cores are used. Defaults to ``None``. Notes ----- The Random Forest Regressor selects the best feature at each split using variance reduction. For numerical features, the threshold is optimized by an exhaustive scan. For float features, the threshold is the midpoint between adjacent values; for integer features, it is the smaller of the two values. In case of ties, the smaller threshold is chosen. Ordered categorical variables are currently treated as categorical. Variables with six or more levels are treated as integers. Variables with five or fewer levels are split by exhaustively evaluating all possible partitions into two subsets using the variance reduction criterion. The maximum tree depth is fixed at 512. A leaf is created when the sample size reaches four. This means regression typically requires at least ten samples to be practical. Pyfru uses its own PRNG, the pcg32 method by Melissa E. O'Neill [4]_, to provide reproducible results in parallel settings. For a given input and seed, the same trees are built regardless of the number of threads, although their order may differ. OOB predictions and importance scores are therefore consistent up to numerical precision. References ---------- .. [3] `L. Breiman, Random Forests, Machine Learning, 45(1), 5-32, 2001. <https://doi.org/10.1023/A:1010933404324>`_ .. [4] `O'Neil Melissa E. (2014). PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation. <https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf>`_ """
[docs] def fit(self, X, y): """ Build a forest of trees from the training set. Parameters ---------- X : Arrow PyCapsule A DataFrame-like object supporting the Arrow PyCapsule interface. Any library supporting this interface can be used (e.g., pandas, polars). Columns can be boolean, numerical, or categorical. Mixed column types are allowed. Other data types are not supported and will raise an exception. ``NaN`` values are not allowed. y : Arrow PyCapsule A Series-like object supporting the Arrow PyCapsule interface. Any library supporting this interface can be used. For regression, ``y`` must be of type float or int; otherwise an exception is raised. ``NaN`` values are not allowed. The length of ``y`` must match the number of rows in ``X``. """ y = self._validate_y(y) X = self._validate_x(X) self._forest = _rust.RandomForest( X, y, self.trees, self.tries, self.save_forest, self.calculate_importance, self.oob, self._get_seed(), False, self.threads, )
[docs] def predict(self, X=None, validate_input=True, to_pycapsule=False): """ Predict for X using the mean over the ensemble. If no X is given, out-of-bag predictions are returned. Parameters ---------- X : Arrow PyCapsule | None A DataFrame-like object supporting the Arrow PyCapsule interface. Cannot contain ``NaN`` values. If ``None`` (default), out-of-bag predictions are returned. validate_input : bool If ``True``, column data types and names in ``X`` are checked against the data provided to ``fit``. For categorical columns, category names and their order must match. to_pycapsule : bool, optional If ``True``, results are returned as an Arrow PyCapsule. Otherwise, NumPy arrays are returned. Defaults to ``False``. Returns ------- numpy.ndarray or Arrow PyCapsule For predictions, a 1D NumPy array is returned containing one prediction per observation. If ``to_pycapsule=True``, results are returned as an Arrow PyCapsule, which can be loaded as a Series in libraries supporting this interface. Notes ----- Regression is performed using leaf averages, which is deterministic, aside from potential small numerical differences due to multithreading and tree construction order. This method checks that the input structure matches the training data structure stored in the object. This may take time for large data or low-latency use cases. Set ``validate_input=False`` to skip validation and assume the input is correct. """ if X is None: preds = self._forest.oob(self._get_seed()) else: X = self._validate_x(X) preds = self._forest.predict( X, self._get_seed(), validate_input, self.threads ) return ResultArray(preds, to_pycapsule=to_pycapsule).get_array()