Source code for statsmodels.gam.gam_cross_validation.gam_cross_validation

"""
Cross-validation classes for GAM

Author: Luca Puggini

"""

from statsmodels.compat.python import with_metaclass

from abc import ABCMeta, abstractmethod
import itertools

import numpy as np

from statsmodels.gam.smooth_basis import GenericSmoothers, UnivariateGenericSmoother


class BaseCV(with_metaclass(ABCMeta)):
    """
    Base class for cross validation over a given model

    Computes the cross validation error of a given model. All the cross
    validation classes can be derived from this one (e.g. GamCV, LassoCV,...)

    Parameters
    ----------
    cv_iterator : instance of cross-validation iterator
        Iterator used to split the data into train and test indices.
    endog : ndarray
        dependent (response) variable of the model
    exog : ndarray
        design matrix, used only to determine the number of observations
        passed to ``cv_iterator.split``

    Attributes
    ----------
    cv_iterator : instance of cross-validation iterator
        Iterator used to split the data into train and test indices.
    endog : ndarray
        dependent (response) variable of the model
    exog : ndarray
        design matrix, used only to determine the number of observations
        passed to ``cv_iterator.split``
    train_test_cv_indices : generator
        generator of (train_index, test_index) pairs produced by
        ``cv_iterator.split``
    """

    def __init__(self, cv_iterator, endog, exog):
        self.cv_iterator = cv_iterator
        self.exog = exog
        self.endog = endog
        # TODO: cv_iterator.split only needs nobs from endog or exog
        self.train_test_cv_indices = self.cv_iterator.split(
            self.exog, self.endog, label=None
        )

    def fit(self, **kwargs):
        """
        Compute the cross validation error over all train/test splits

        Parameters
        ----------
        **kwargs
            Keyword arguments passed to the ``fit`` method of the
            cross-validated model.

        Returns
        -------
        cv_err : ndarray
            array of the errors computed on each test fold
        """
        cv_err = []

        for train_index, test_index in self.train_test_cv_indices:
            cv_err.append(self._error(train_index, test_index, **kwargs))

        return np.array(cv_err)

    @abstractmethod
    def _error(self, train_index, test_index, **kwargs):
        """
        Train the model on the train set and return the error on the test set

        Parameters
        ----------
        train_index : ndarray
            index or boolean mask selecting the training observations
        test_index : ndarray
            index or boolean mask selecting the test observations
        **kwargs
            Keyword arguments passed to the ``fit`` method of the
            cross-validated model.

        Returns
        -------
        error : float
            prediction error evaluated on the test set
        """


def _split_train_test_smoothers(x, smoothers, train_index, test_index):
    """
    Split smoothers in test and train sets and create GenericSmoothers

    Parameters
    ----------
    x : ndarray
        values of the independent variables for all the smoothers
    smoothers : instance
        additive smoother instance whose components are split
    train_index : ndarray
        index or boolean mask selecting the training observations
    test_index : ndarray
        index or boolean mask selecting the test observations

    Returns
    -------
    train_multivariate_smoothers : GenericSmoothers
        smoothers evaluated on the training observations
    test_multivariate_smoothers : GenericSmoothers
        smoothers evaluated on the test observations

    Notes
    -----
    This does not take exog_linear into account.
    """
    train_smoothers = []
    test_smoothers = []
    for smoother in smoothers.smoothers:
        train_basis = smoother.basis[train_index]
        train_der_basis = smoother.der_basis[train_index]
        train_der2_basis = smoother.der2_basis[train_index]
        train_cov_der2 = smoother.cov_der2
        # TODO: Double check this part. cov_der2 is calculated with all data
        train_x = smoother.x[train_index]

        train_smoothers.append(
            UnivariateGenericSmoother(
                train_x,
                train_basis,
                train_der_basis,
                train_der2_basis,
                train_cov_der2,
                smoother.variable_name + " train",
            )
        )

        test_basis = smoother.basis[test_index]
        test_der_basis = smoother.der_basis[test_index]
        test_cov_der2 = smoother.cov_der2
        # TODO: Double check this part. cov_der2 is calculated with all data
        test_x = smoother.x[test_index]

        test_smoothers.append(
            UnivariateGenericSmoother(
                test_x,
                test_basis,
                test_der_basis,
                train_der2_basis,
                test_cov_der2,
                smoother.variable_name + " test",
            )
        )

    train_multivariate_smoothers = GenericSmoothers(x[train_index], train_smoothers)
    test_multivariate_smoothers = GenericSmoothers(x[test_index], test_smoothers)

    return train_multivariate_smoothers, test_multivariate_smoothers


class MultivariateGAMCV(BaseCV):
    """
    Cross validation error of a multivariate additive model

    Parameters
    ----------
    smoother : additive smoother instance
        smoother providing the basis for the smooth terms of the model
    alphas : list of float
        penalty weights, one for each smooth term
    gam : model class
        model class for creating a model with the training data
    cost : function
        cost function used to compute the prediction error
    endog : ndarray
        dependent (response) variable of the model
    exog : ndarray or None
        design matrix of the linear (non-smooth) terms of the model
    cv_iterator : instance of cross-validation iterator
        iterator used to split the data into train and test indices

    Attributes
    ----------
    cost : function
        cost function used to compute the prediction error
    gam : model class
        model class for creating a model with the training data
    smoother : additive smoother instance
        smoother providing the basis for the smooth terms of the model
    exog_linear : ndarray or None
        design matrix of the linear (non-smooth) terms of the model
    alphas : list of float
        penalty weights, one for each smooth term
    cv_iterator : instance of cross-validation iterator
        iterator used to split the data into train and test indices
    """

    def __init__(self, smoother, alphas, gam, cost, endog, exog, cv_iterator):
        self.cost = cost
        self.gam = gam
        self.smoother = smoother
        self.exog_linear = exog
        self.alphas = alphas
        self.cv_iterator = cv_iterator
        # TODO: super does not do anything with endog, exog, except get nobs
        # refactor to clean up what where `exog` and `exog_linear` is attached
        # exog is not used in super
        super().__init__(cv_iterator, endog, self.smoother.basis)

    def _error(self, train_index, test_index, **kwargs):
        """
        Train the model on the train set and return the error on the test set

        Parameters
        ----------
        train_index : ndarray
            index or boolean mask selecting the training observations
        test_index : ndarray
            index or boolean mask selecting the test observations
        **kwargs
            Keyword arguments passed to the ``fit`` method of the gam model.

        Returns
        -------
        error : float
            prediction error, evaluated with ``cost``, on the test set
        """
        train_smoother, test_smoother = _split_train_test_smoothers(
            self.smoother.x, self.smoother, train_index, test_index
        )

        endog_train = self.endog[train_index]
        endog_test = self.endog[test_index]
        if self.exog_linear is not None:
            exog_linear_train = self.exog_linear[train_index]
            exog_linear_test = self.exog_linear[test_index]
        else:
            exog_linear_train = None
            exog_linear_test = None

        gam = self.gam(
            endog_train,
            exog=exog_linear_train,
            smoother=train_smoother,
            alpha=self.alphas,
        )
        gam_res = gam.fit(**kwargs)
        # exog_linear_test and test_smoother.basis will be column_stacked
        #     but not transformed in predict
        endog_est = gam_res.predict(
            exog_linear_test, test_smoother.basis, transform=False
        )

        return self.cost(endog_test, endog_est)


class BasePenaltiesPathCV(with_metaclass(ABCMeta)):
    """
    Base class for cross validation over a grid of parameters

    The best parameter is saved in ``alpha_cv``.

    Parameters
    ----------
    alphas : list of float
        grid of penalty weights to search over

    Attributes
    ----------
    alphas : list of float
        grid of penalty weights to search over
    alpha_cv : float or None
        best penalty weight found by cross-validation
    cv_error : ndarray or None
        cross-validation error for each value in ``alphas``
    cv_std : ndarray or None
        standard deviation of the cross-validation error for each value in
        ``alphas``

    Notes
    -----
    This class is currently not used.
    """

    def __init__(self, alphas):
        self.alphas = alphas
        self.alpha_cv = None
        self.cv_error = None
        self.cv_std = None

    def plot_path(self):
        """Plot the cross validation error and standard deviation over the alphas grid"""
        from statsmodels.graphics.utils import _import_mpl

        plt = _import_mpl()
        plt.plot(self.alphas, self.cv_error, c="black")
        plt.plot(self.alphas, self.cv_error + 1.96 * self.cv_std, c="blue")
        plt.plot(self.alphas, self.cv_error - 1.96 * self.cv_std, c="blue")

        plt.plot(self.alphas, self.cv_error, "o", c="black")
        plt.plot(self.alphas, self.cv_error + 1.96 * self.cv_std, "o", c="blue")
        plt.plot(self.alphas, self.cv_error - 1.96 * self.cv_std, "o", c="blue")

        # TODO add return


[docs] class MultivariateGAMCVPath: """ K-fold cross-validation for GAM Parameters ---------- smoother : additive smoother instance smoother providing the basis for the smooth terms of the model alphas : list of iterables list of alpha for smooths. The product space will be used as alpha grid for cross-validation gam : model class model class for creating a model with k-fold training data cost : function cost function for the prediction error endog : ndarray dependent (response) variable of the model exog : ndarray or None design matrix of the linear (non-smooth) terms of the model cv_iterator : instance of cross-validation iterator iterator used to split the data into train and test indices Attributes ---------- cost : function cost function for the prediction error smoother : additive smoother instance smoother providing the basis for the smooth terms of the model gam : model class model class for creating a model with k-fold training data alphas : list of iterables list of alpha for smooths alphas_grid : list of tuples product space of ``alphas`` searched during cross-validation endog : ndarray dependent (response) variable of the model exog : ndarray or None design matrix of the linear (non-smooth) terms of the model cv_iterator : instance of cross-validation iterator iterator used to split the data into train and test indices cv_error : ndarray mean cross-validation error for each element of ``alphas_grid`` cv_std : ndarray standard deviation of the cross-validation error for each element of ``alphas_grid`` alpha_cv : tuple or None element of ``alphas_grid`` with the smallest cross-validation error Warnings -------- The API of this class is preliminary and will change. """ def __init__(self, smoother, alphas, gam, cost, endog, exog, cv_iterator): self.cost = cost self.smoother = smoother self.gam = gam self.alphas = alphas self.alphas_grid = list(itertools.product(*self.alphas)) self.endog = endog self.exog = exog self.cv_iterator = cv_iterator self.cv_error = np.zeros( shape=( len( self.alphas_grid, ) ) ) self.cv_std = np.zeros( shape=( len( self.alphas_grid, ) ) ) self.alpha_cv = None
[docs] def fit(self, **kwargs): """ Perform the cross-validation search over the alphas grid Parameters ---------- **kwargs Keyword arguments passed to the ``fit`` method of the gam model. Returns ------- self : MultivariateGAMCVPath instance with ``cv_error``, ``cv_std`` and ``alpha_cv`` set """ for i, alphas_i in enumerate(self.alphas_grid): gam_cv = MultivariateGAMCV( smoother=self.smoother, alphas=alphas_i, gam=self.gam, cost=self.cost, endog=self.endog, exog=self.exog, cv_iterator=self.cv_iterator, ) cv_err = gam_cv.fit(**kwargs) self.cv_error[i] = cv_err.mean() self.cv_std[i] = cv_err.std() self.alpha_cv = self.alphas_grid[np.argmin(self.cv_error)] return self