Source code for statsmodels.discrete.truncated_model

__all__ = [
    "HurdleCountModel",
    "TruncatedLFNegativeBinomialP",
    "TruncatedLFPoisson",
]

from copy import deepcopy
import warnings

import numpy as np
from scipy.linalg import block_diag

import statsmodels.base.model as base
import statsmodels.base.wrapper as wrap
from statsmodels.discrete.discrete_model import (
    CountModel,
    CountResults,
    DiscreteModel,
    GeneralizedPoisson,
    L1CountResults,
    NegativeBinomialP,
    Poisson,
    _discrete_results_docs,
    _l1_results_attr,
    _validate_l1_method,
)
from statsmodels.distributions.discrete import (
    truncatednegbin,
    truncatedpoisson,
)
import statsmodels.regression.linear_model as lm
from statsmodels.tools._decorators import cache_readonly
from statsmodels.tools.numdiff import approx_hess
from statsmodels.tools.sm_exceptions import ConvergenceWarning
from statsmodels.tools.validation import (
    array_like,
    bool_like,
    float_like,
    int_like,
    string_like,
)


class TruncatedLFGeneric(CountModel):
    __doc__ = """
    Generic Truncated model for count data

    .. versionadded:: 0.14.0

    {params}
    {extra_params}

    Attributes
    ----------
    endog : ndarray
        A reference to the endogenous response variable
    exog : ndarray
        A reference to the exogenous design.
    truncation : int
        Truncation parameter that specifies the truncation point out of
        the support of the distribution. pmf(k) = 0 for k <= truncation
    """.format(
           params=base._model_params_doc,
           extra_params="""offset : array_like, optional
        Offset is added to the linear prediction with coefficient equal to 1.
    exposure : array_like, optional
        Log(exposure) is added to the linear prediction with coefficient
        equal to 1.

    """ + base._missing_param_doc,
       )

    def __init__(self, endog, exog, truncation=0, offset=None,
                 exposure=None, missing="none", **kwargs):
        super().__init__(
            endog,
            exog,
            offset=offset,
            exposure=exposure,
            missing=missing,
            **kwargs
            )
        mask = self.endog > truncation
        self.exog = self.exog[mask]
        self.endog = self.endog[mask]
        if offset is not None:
            self.offset = self.offset[mask]
        if exposure is not None:
            self.exposure = self.exposure[mask]

        self.trunc = truncation
        self.truncation = truncation  # needed for recreating model
        # We cannot set the correct df_resid here, not enough information
        self._init_keys.extend(["truncation"])
        self._null_drop_keys = []

    def loglike(self, params):
        """
        Log-likelihood of Generic Truncated model.

        Parameters
        ----------
        params : array_like
            The parameters of the model.

        Returns
        -------
        loglike : float
            The log-likelihood function of the model evaluated at `params`.
        """
        return np.sum(self.loglikeobs(params))

    def loglikeobs(self, params):
        """
        Log-likelihood for observations of Generic Truncated model.

        Parameters
        ----------
        params : array_like
            The parameters of the model.

        Returns
        -------
        loglike : ndarray (nobs,)
            The log likelihood for each observation of the model evaluated
            at `params`.
        """
        llf_main = self.model_main.loglikeobs(params)

        yt = self.trunc + 1

        # equivalent ways to compute truncation probability
        # pmf0 = np.zeros_like(self.endog, dtype=np.float64)
        # for i in range(self.trunc + 1):
        #     model = self.model_main.__class__(np.ones_like(self.endog) * i,
        #                                       self.exog)
        #     pmf0 += np.exp(model.loglikeobs(params))
        #
        # pmf1 = self.model_main.predict(
        #     params, which="prob", y_values=np.arange(yt)).sum(-1)

        pmf = self.predict(
            params, which="prob-base", y_values=np.arange(yt)).sum(-1)

        # Skip pmf = 1 to avoid warnings
        log_1_m_pmf = np.full_like(pmf, -np.inf)
        loc = pmf > 1
        log_1_m_pmf[loc] = np.nan
        loc = pmf < 1
        log_1_m_pmf[loc] = np.log(1 - pmf[loc])
        llf = llf_main - log_1_m_pmf

        return llf

    def score_obs(self, params):
        """
        Generic Truncated model score (gradient) vector of the log-likelihood.

        Parameters
        ----------
        params : array_like
            The parameters of the model

        Returns
        -------
        score : ndarray, 1-D
            The score vector of the model, i.e., the first derivative of the
            log-likelihood function, evaluated at `params`
        """
        score_main = self.model_main.score_obs(params)

        pmf = np.zeros_like(self.endog, dtype=np.float64)
        # TODO: can we rewrite to following without creating new models
        score_trunc = np.zeros_like(score_main, dtype=np.float64)
        for i in range(self.trunc + 1):
            model = self.model_main.__class__(
                np.ones_like(self.endog) * i,
                self.exog,
                offset=getattr(self, "offset", None),
                exposure=getattr(self, "exposure", None),
                )
            pmf_i = np.exp(model.loglikeobs(params))
            score_trunc += (model.score_obs(params).T * pmf_i).T
            pmf += pmf_i

        dparams = score_main + (score_trunc.T / (1 - pmf)).T

        return dparams

    def score(self, params):
        """
        Generic Truncated model score (gradient) vector of the log-likelihood.

        Parameters
        ----------
        params : array_like
            The parameters of the model

        Returns
        -------
        score : ndarray, 1-D
            The score vector of the model, i.e., the first derivative of the
            log-likelihood function, evaluated at `params`
        """
        return self.score_obs(params).sum(0)

    def fit(self, start_params=None, method="bfgs", maxiter=35,
            full_output=1, disp=1, callback=None,
            cov_type="nonrobust", cov_kwds=None, use_t=None, **kwargs):
        if start_params is None:
            offset = getattr(self, "offset", 0) + getattr(self, "exposure", 0)
            if np.size(offset) == 1 and offset == 0:
                offset = None
            model = self.model_main.__class__(self.endog, self.exog,
                                              offset=offset)
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", category=ConvergenceWarning)
                start_params = model.fit(disp=0).params

        # Todo: check how we can to this in __init__
        k_params = self.df_model + 1 + self.k_extra
        self.df_resid = self.endog.shape[0] - k_params

        mlefit = super().fit(
            start_params=start_params,
            method=method,
            maxiter=maxiter,
            disp=disp,
            full_output=full_output,
            callback=lambda x: x,
            **kwargs
            )

        zipfit = self.result_class(self, mlefit._results)
        result = self.result_class_wrapper(zipfit)

        if cov_kwds is None:
            cov_kwds = {}

        result._get_robustcov_results(cov_type=cov_type,
                                      use_self=True, use_t=use_t, **cov_kwds)
        return result

    fit.__doc__ = DiscreteModel.fit.__doc__

    def fit_regularized(
            self, start_params=None, method="l1",
            maxiter="defined_by_method", full_output=1, disp=1, callback=None,
            alpha=0, trim_mode="auto", auto_trim_tol=0.01, size_trim_tol=1e-4,
            qc_tol=0.03, **kwargs):

        if np.size(alpha) == 1 and alpha != 0:
            k_params = self.exog.shape[1]
            alpha = alpha * np.ones(k_params)

        alpha_p = alpha
        if start_params is None:
            offset = getattr(self, "offset", 0) + getattr(self, "exposure", 0)
            if np.size(offset) == 1 and offset == 0:
                offset = None
            model = self.model_main.__class__(self.endog, self.exog,
                                              offset=offset)
            start_params = model.fit_regularized(
                start_params=start_params, method=method, maxiter=maxiter,
                full_output=full_output, disp=0, callback=callback,
                alpha=alpha_p, trim_mode=trim_mode,
                auto_trim_tol=auto_trim_tol,
                size_trim_tol=size_trim_tol, qc_tol=qc_tol, **kwargs).params
        cntfit = super(CountModel, self).fit_regularized(
                start_params=start_params, method=method, maxiter=maxiter,
                full_output=full_output, disp=disp, callback=callback,
                alpha=alpha, trim_mode=trim_mode, auto_trim_tol=auto_trim_tol,
                size_trim_tol=size_trim_tol, qc_tol=qc_tol, **kwargs)

        if method in ["l1", "l1_cvxopt_cp"]:
            discretefit = self.result_class_reg(self, cntfit)
        else:
            raise TypeError(
                    f"argument method == {method}, which is not handled")

        return self.result_class_reg_wrapper(discretefit)

    fit_regularized.__doc__ = DiscreteModel.fit_regularized.__doc__

    def hessian(self, params):
        """
        Generic Truncated model Hessian matrix of the log-likelihood.

        Parameters
        ----------
        params : array_like
            The parameters of the model

        Returns
        -------
        hess : ndarray, (k_vars, k_vars)
            The Hessian, second derivative of the log-likelihood function,
            evaluated at `params`
        """
        return approx_hess(params, self.loglike)

    def predict(self, params, exog=None, exposure=None, offset=None,
                which="mean", y_values=None):
        """
        Predict response variable or other statistic given exogenous variables.

        Parameters
        ----------
        params : array_like
            The parameters of the model.
        exog : array_like, optional
            Explanatory variables for the main count model.
            If ``exog`` is None, then the data from the model will be used.
        offset : array_like, optional
            Offset is added to the linear predictor of the mean function with
            coefficient equal to 1.
            Default is zero if exog is not None, and the model offset if exog
            is None.
        exposure : array_like, optional
            Log(exposure) is added to the linear predictor with coefficient
            equal to 1. If exposure is specified, then it will be logged by
            the method. The user does not need to log it first.
            Default is one if exog is not None, and it is the model exposure
            if exog is None.
        which : str, optional
            Statistic to predict. Default is 'mean'.

            - 'mean' : the conditional expectation of endog E(y | x)
            - 'mean-main' : mean parameter of truncated count model.
              Note, this is not the mean of the truncated distribution.
            - 'linear' : the linear predictor of the truncated count model.
            - 'var' : returns the estimated variance of endog implied by the
              model.
            - 'prob' : probabilities of each count from 0 to max(endog), or
              for y_values if those are provided. This is a multivariate
              return (2-dim when predicting for several observations).
              The probabilities in the truncated region are zero.
            - 'prob-base' : probabilities for untruncated base distribution.
              The probabilities are for each count from 0 to max(endog), or
              for y_values if those are provided. This is a multivariate
              return (2-dim when predicting for several observations).


        y_values : array_like, optional
            Values of the random variable endog at which pmf is evaluated.
            Only used if ``which="prob"``

        Returns
        -------
        ndarray
            The predicted values, whose interpretation depends on `which`.

        Notes
        -----
        If exposure is specified, then it will be logged by the method.
        The user does not need to log it first.
        """
        exog, offset, exposure = self._get_predict_arrays(
            exog=exog,
            offset=offset,
            exposure=exposure
            )

        fitted = np.dot(exog, params[:exog.shape[1]])
        linpred = fitted + exposure + offset

        if which == "mean":
            mu = np.exp(linpred)
            if self.truncation == 0:
                prob_main = self.model_main._prob_nonzero(mu, params)
                return mu / prob_main
            elif self.truncation == -1:
                return mu
            elif self.truncation > 0:
                counts = np.atleast_2d(np.arange(0, self.truncation + 1))
                # next is same as in prob-main below
                probs = self.model_main.predict(
                    params, exog=exog, exposure=np.exp(exposure),
                    offset=offset, which="prob", y_values=counts)
                prob_tregion = probs.sum(1)
                mean_tregion = (np.arange(self.truncation + 1) * probs).sum(1)
                mean = (mu - mean_tregion) / (1 - prob_tregion)
                return mean
            else:
                raise ValueError("unsupported self.truncation")
        elif which == "linear":
            return linpred
        elif which == "mean-main":
            return np.exp(linpred)
        elif which == "prob":
            if y_values is not None:
                counts = np.atleast_2d(y_values)
            else:
                counts = np.atleast_2d(np.arange(0, np.max(self.endog)+1))
            mu = np.exp(linpred)[:, None]
            if self.k_extra == 0:
                # poisson, no extra params
                probs = self.model_dist.pmf(counts, mu, self.trunc)
            elif self.k_extra == 1:
                p = self.model_main.parameterization
                probs = self.model_dist.pmf(counts, mu, params[-1],
                                            p, self.trunc)
            else:
                raise ValueError("k_extra is not 0 or 1")
            return probs
        elif which == "prob-base":
            if y_values is not None:
                counts = np.asarray(y_values)
            else:
                counts = np.arange(0, np.max(self.endog)+1)

            probs = self.model_main.predict(
                params, exog=exog, exposure=np.exp(exposure),
                offset=offset, which="prob", y_values=counts)
            return probs
        elif which == "var":
            mu = np.exp(linpred)
            counts = np.atleast_2d(np.arange(0, self.truncation + 1))
            # next is same as in prob-main below
            probs = self.model_main.predict(
                params, exog=exog, exposure=np.exp(exposure),
                offset=offset, which="prob", y_values=counts)
            prob_tregion = probs.sum(1)
            mean_tregion = (np.arange(self.truncation + 1) * probs).sum(1)
            mean = (mu - mean_tregion) / (1 - prob_tregion)
            mnc2_tregion = (np.arange(self.truncation + 1)**2 *
                            probs).sum(1)
            vm = self.model_main._var(mu, params)
            # uncentered 2nd moment
            mnc2 = (mu**2 + vm - mnc2_tregion) / (1 - prob_tregion)
            v = mnc2 - mean**2
            return v
        else:
            raise ValueError(
                f"argument which == {which} not handled")


[docs] class TruncatedLFPoisson(TruncatedLFGeneric): __doc__ = """ Truncated Poisson model for count data .. versionadded:: 0.14.0 {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. truncation : int Truncation parameter that specifies the truncation point out of the support of the distribution. pmf(k) = 0 for k <= truncation """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. """ + base._missing_param_doc, ) def __init__(self, endog, exog, offset=None, exposure=None, truncation=0, missing="none", **kwargs): super().__init__( endog, exog, offset=offset, exposure=exposure, truncation=truncation, missing=missing, **kwargs ) self.model_main = Poisson(self.endog, self.exog, exposure=getattr(self, "exposure", None), offset=getattr(self, "offset", None), ) self.model_dist = truncatedpoisson self.result_class = TruncatedLFPoissonResults self.result_class_wrapper = TruncatedLFGenericResultsWrapper self.result_class_reg = L1TruncatedLFGenericResults self.result_class_reg_wrapper = L1TruncatedLFGenericResultsWrapper def _predict_mom_trunc0(self, params, mu): """ Predict mean and variance of zero-truncated distribution experimental api, will likely be replaced by other methods Parameters ---------- params : array_like The model parameters. This is only used to extract extra params like dispersion parameter. mu : array_like Array of mean predictions for main model. Returns ------- m : ndarray Predicted mean of the zero-truncated distribution. var_ : ndarray Predicted variance of the zero-truncated distribution. """ w = (1 - np.exp(-mu)) # prob of no truncation, 1 - P(y=0) m = mu / w var_ = m - (1 - w) * m**2 return m, var_
[docs] class TruncatedLFNegativeBinomialP(TruncatedLFGeneric): __doc__ = """ Truncated Generalized Negative Binomial model for count data .. versionadded:: 0.14.0 {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. truncation : int Truncation parameter that specifies the truncation point out of the support of the distribution. pmf(k) = 0 for k <= truncation """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. p : int, optional P denotes parameterizations for NB regression. p=1 for NB-1 and p=2 for NB-2. Default is p=2. """ + base._missing_param_doc, ) def __init__(self, endog, exog, offset=None, exposure=None, truncation=0, p=2, missing="none", **kwargs): super().__init__( endog, exog, offset=offset, exposure=exposure, truncation=truncation, missing=missing, **kwargs ) self.model_main = NegativeBinomialP( self.endog, self.exog, exposure=getattr(self, "exposure", None), offset=getattr(self, "offset", None), p=p ) self.k_extra = self.model_main.k_extra self.exog_names.extend(self.model_main.exog_names[-self.k_extra:]) self.model_dist = truncatednegbin self.result_class = TruncatedNegativeBinomialResults self.result_class_wrapper = TruncatedLFGenericResultsWrapper self.result_class_reg = L1TruncatedLFGenericResults self.result_class_reg_wrapper = L1TruncatedLFGenericResultsWrapper def _predict_mom_trunc0(self, params, mu): """ Predict mean and variance of zero-truncated distribution experimental api, will likely be replaced by other methods Parameters ---------- params : array_like The model parameters. This is only used to extract extra params like dispersion parameter. mu : array_like Array of mean predictions for main model. Returns ------- m : ndarray Predicted mean of the zero-truncated distribution. var_ : ndarray Predicted variance of the zero-truncated distribution. """ # note: prob_zero and vm are distribution specific, rest is generic # when mean of base model is mu alpha = params[-1] p = self.model_main.parameterization prob_zero = (1 + alpha * mu**(p-1))**(- 1 / alpha) w = 1 - prob_zero # prob of no truncation, 1 - P(y=0) m = mu / w vm = mu * (1 + alpha * mu**(p-1)) # variance of NBP # uncentered 2nd moment is vm + mu**2 mnc2 = (mu**2 + vm) / w # uses mnc2_tregion = 0 var_ = mnc2 - m**2 return m, var_
class TruncatedLFGeneralizedPoisson(TruncatedLFGeneric): __doc__ = """ Truncated Generalized Poisson model for count data .. versionadded:: 0.14.0 {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. truncation : int Truncation parameter that specifies the truncation point out of the support of the distribution. pmf(k) = 0 for k <= truncation """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. p : int, optional Dispersion power parameter for the GeneralizedPoisson model. p=1 for GP-1 and p=2 for GP-2. Default is p=2. """ + base._missing_param_doc, ) def __init__(self, endog, exog, offset=None, exposure=None, truncation=0, p=2, missing="none", **kwargs): super().__init__( endog, exog, offset=offset, exposure=exposure, truncation=truncation, missing=missing, **kwargs ) self.model_main = GeneralizedPoisson( self.endog, self.exog, exposure=getattr(self, "exposure", None), offset=getattr(self, "offset", None), p=p ) self.k_extra = self.model_main.k_extra self.exog_names.extend(self.model_main.exog_names[-self.k_extra:]) self.model_dist = None self.result_class = TruncatedNegativeBinomialResults self.result_class_wrapper = TruncatedLFGenericResultsWrapper self.result_class_reg = L1TruncatedLFGenericResults self.result_class_reg_wrapper = L1TruncatedLFGenericResultsWrapper class _RCensoredGeneric(CountModel): __doc__ = """ Generic right Censored model for count data {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. """ + base._missing_param_doc, ) def __init__(self, endog, exog, offset=None, exposure=None, missing="none", **kwargs): self.zero_idx = np.nonzero(endog == 0)[0] self.nonzero_idx = np.nonzero(endog)[0] super().__init__( endog, exog, offset=offset, exposure=exposure, missing=missing, **kwargs ) def loglike(self, params): """ Log-likelihood of Generic Censored model. Parameters ---------- params : array_like The parameters of the model. Returns ------- loglike : float The log-likelihood function of the model evaluated at `params`. """ return np.sum(self.loglikeobs(params)) def loglikeobs(self, params): """ Log-likelihood for observations of Generic Censored model. Parameters ---------- params : array_like The parameters of the model. Returns ------- loglike : ndarray (nobs,) The log likelihood for each observation of the model evaluated at `params`. """ llf_main = self.model_main.loglikeobs(params) llf = np.concatenate( (llf_main[self.zero_idx], np.log(1 - np.exp(llf_main[self.nonzero_idx]))) ) return llf def score_obs(self, params): """ Generic Censored model score (gradient) vector of the log-likelihood. Parameters ---------- params : array_like The parameters of the model Returns ------- score : ndarray, 1-D The score vector of the model, i.e., the first derivative of the log-likelihood function, evaluated at `params` """ score_main = self.model_main.score_obs(params) llf_main = self.model_main.loglikeobs(params) score = np.concatenate(( score_main[self.zero_idx], (score_main[self.nonzero_idx].T * -np.exp(llf_main[self.nonzero_idx]) / (1 - np.exp(llf_main[self.nonzero_idx]))).T )) return score def score(self, params): """ Generic Censored model score (gradient) vector of the log-likelihood. Parameters ---------- params : array_like The parameters of the model Returns ------- score : ndarray, 1-D The score vector of the model, i.e., the first derivative of the log-likelihood function, evaluated at `params` """ return self.score_obs(params).sum(0) def fit(self, start_params=None, method="bfgs", maxiter=35, full_output=1, disp=1, callback=None, cov_type="nonrobust", cov_kwds=None, use_t=None, **kwargs): if start_params is None: offset = getattr(self, "offset", 0) + getattr(self, "exposure", 0) if np.size(offset) == 1 and offset == 0: offset = None model = self.model_main.__class__(self.endog, self.exog, offset=offset) with warnings.catch_warnings(): warnings.simplefilter("ignore", category=ConvergenceWarning) start_params = model.fit(disp=0).params mlefit = super().fit( start_params=start_params, method=method, maxiter=maxiter, disp=disp, full_output=full_output, callback=lambda x: x, **kwargs ) zipfit = self.result_class(self, mlefit._results) result = self.result_class_wrapper(zipfit) if cov_kwds is None: cov_kwds = {} result._get_robustcov_results(cov_type=cov_type, use_self=True, use_t=use_t, **cov_kwds) return result fit.__doc__ = DiscreteModel.fit.__doc__ def fit_regularized( self, start_params=None, method="l1", maxiter="defined_by_method", full_output=1, disp=1, callback=None, alpha=0, trim_mode="auto", auto_trim_tol=0.01, size_trim_tol=1e-4, qc_tol=0.03, **kwargs): if np.size(alpha) == 1 and alpha != 0: k_params = self.exog.shape[1] alpha = alpha * np.ones(k_params) alpha_p = alpha if start_params is None: offset = getattr(self, "offset", 0) + getattr(self, "exposure", 0) if np.size(offset) == 1 and offset == 0: offset = None model = self.model_main.__class__(self.endog, self.exog, offset=offset) start_params = model.fit_regularized( start_params=start_params, method=method, maxiter=maxiter, full_output=full_output, disp=0, callback=callback, alpha=alpha_p, trim_mode=trim_mode, auto_trim_tol=auto_trim_tol, size_trim_tol=size_trim_tol, qc_tol=qc_tol, **kwargs).params cntfit = super(CountModel, self).fit_regularized( start_params=start_params, method=method, maxiter=maxiter, full_output=full_output, disp=disp, callback=callback, alpha=alpha, trim_mode=trim_mode, auto_trim_tol=auto_trim_tol, size_trim_tol=size_trim_tol, qc_tol=qc_tol, **kwargs) if method in ["l1", "l1_cvxopt_cp"]: discretefit = self.result_class_reg(self, cntfit) else: raise TypeError( f"argument method == {method}, which is not handled") return self.result_class_reg_wrapper(discretefit) fit_regularized.__doc__ = DiscreteModel.fit_regularized.__doc__ def hessian(self, params): """ Generic Censored model Hessian matrix of the log-likelihood. Parameters ---------- params : array_like The parameters of the model Returns ------- hess : ndarray, (k_vars, k_vars) The Hessian, second derivative of the log-likelihood function, evaluated at `params` """ return approx_hess(params, self.loglike) class _RCensoredPoisson(_RCensoredGeneric): __doc__ = """ Censored Poisson model for count data {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. """ + base._missing_param_doc, ) def __init__(self, endog, exog, offset=None, exposure=None, missing="none", **kwargs): super().__init__(endog, exog, offset=offset, exposure=exposure, missing=missing, **kwargs) self.model_main = Poisson(np.zeros_like(self.endog), self.exog) self.model_dist = None self.result_class = TruncatedLFGenericResults self.result_class_wrapper = TruncatedLFGenericResultsWrapper self.result_class_reg = L1TruncatedLFGenericResults self.result_class_reg_wrapper = L1TruncatedLFGenericResultsWrapper class _RCensoredGeneralizedPoisson(_RCensoredGeneric): __doc__ = """ Censored Generalized Poisson model for count data {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. """ + base._missing_param_doc, ) def __init__(self, endog, exog, offset=None, p=2, exposure=None, missing="none", **kwargs): super().__init__( endog, exog, offset=offset, exposure=exposure, missing=missing, **kwargs) self.model_main = GeneralizedPoisson( np.zeros_like(self.endog), self.exog) self.model_dist = None self.result_class = TruncatedLFGenericResults self.result_class_wrapper = TruncatedLFGenericResultsWrapper self.result_class_reg = L1TruncatedLFGenericResults self.result_class_reg_wrapper = L1TruncatedLFGenericResultsWrapper class _RCensoredNegativeBinomialP(_RCensoredGeneric): __doc__ = """ Censored Negative Binomial model for count data {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. """ + base._missing_param_doc, ) def __init__(self, endog, exog, offset=None, p=2, exposure=None, missing="none", **kwargs): super().__init__( endog, exog, offset=offset, exposure=exposure, missing=missing, **kwargs ) self.model_main = NegativeBinomialP(np.zeros_like(self.endog), self.exog, p=p ) self.model_dist = None self.result_class = TruncatedLFGenericResults self.result_class_wrapper = TruncatedLFGenericResultsWrapper self.result_class_reg = L1TruncatedLFGenericResults self.result_class_reg_wrapper = L1TruncatedLFGenericResultsWrapper class _RCensored(_RCensoredGeneric): __doc__ = """ Censored model for count data {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. """ + base._missing_param_doc, ) def __init__(self, endog, exog, model=Poisson, distribution=truncatedpoisson, offset=None, exposure=None, missing="none", **kwargs): super().__init__( endog, exog, offset=offset, exposure=exposure, missing=missing, **kwargs ) self.model_main = model(np.zeros_like(self.endog), self.exog) self.model_dist = distribution # fix k_extra and exog_names self.k_extra = k_extra = self.model_main.k_extra if k_extra > 0: self.exog_names.extend(self.model_main.exog_names[-k_extra:]) self.result_class = TruncatedLFGenericResults self.result_class_wrapper = TruncatedLFGenericResultsWrapper self.result_class_reg = L1TruncatedLFGenericResults self.result_class_reg_wrapper = L1TruncatedLFGenericResultsWrapper def _prob_nonzero(self, mu, params): """ Probability that count is not zero internal use in Censored model, will be refactored or removed """ prob_nz = self.model_main._prob_nonzero(mu, params) return prob_nz
[docs] class HurdleCountModel(CountModel): __doc__ = """ Hurdle model for count data .. versionadded:: 0.14.0 {params} {extra_params} Attributes ---------- endog : ndarray A reference to the endogenous response variable exog : ndarray A reference to the exogenous design. Notes ----- The parameters in the NegativeBinomial zero model are not identified if the predicted mean is constant. If there is no or only little variation in the predicted mean, then convergence might fail, hessian might not be invertible or parameter estimates will have large standard errors. References ---------- not yet """.format( params=base._model_params_doc, extra_params="""offset : array_like, optional Offset is added to the linear prediction with coefficient equal to 1. dist : {'poisson', 'negbin'}, optional Log-likelihood type of count model family. Default is 'poisson'. zerodist : {'poisson', 'negbin'}, optional Log-likelihood type of zero hurdle model family. Default is 'poisson'. p : int, optional Dispersion power parameter for the NegativeBinomialP count model. Used when dist='negbin'. Default is 2. pzero : int, optional Dispersion power parameter for the NegativeBinomialP zero hurdle model. Used when zerodist='negbin'. Default is 2. exposure : array_like, optional Log(exposure) is added to the linear prediction with coefficient equal to 1. """ + base._missing_param_doc, ) def __init__(self, endog, exog, offset=None, dist="poisson", zerodist="poisson", p=2, pzero=2, exposure=None, missing="none", **kwargs): if (offset is not None) or (exposure is not None): msg = "Offset and exposure are not yet implemented" raise NotImplementedError(msg) super().__init__( endog, exog, offset=offset, exposure=exposure, missing=missing, **kwargs ) # Use self.exog rather than the exog argument: the base class has # already converted it to a 2-dimensional ndarray, so this also # accepts the array_like inputs (lists, Series, DataFrames) that # every other count model accepts. self.k_exog = self.exog.shape[1] self.k_extra1 = 0 self.k_extra2 = 0 self._initialize(dist, zerodist, p, pzero) self.result_class = HurdleCountResults self.result_class_wrapper = HurdleCountResultsWrapper self.result_class_reg = L1HurdleCountResults self.result_class_reg_wrapper = L1HurdleCountResultsWrapper def _initialize(self, dist, zerodist, p, pzero): if (dist not in ["poisson", "negbin"] or zerodist not in ["poisson", "negbin"]): raise NotImplementedError('dist and zerodist must be "poisson",' '"negbin"') if zerodist == "poisson": self.model1 = _RCensored(self.endog, self.exog, model=Poisson) elif zerodist == "negbin": self.model1 = _RCensored(self.endog, self.exog, model=NegativeBinomialP) self.k_extra1 += 1 if dist == "poisson": self.model2 = TruncatedLFPoisson(self.endog, self.exog) elif dist == "negbin": self.model2 = TruncatedLFNegativeBinomialP(self.endog, self.exog, p=p) self.k_extra2 += 1 @property def _k_zero(self): """ Index that splits the parameter vector into its two components. Parameters are ordered as the zero model's exog parameters, the zero model's extra parameters, the main model's exog parameters and then the main model's extra parameters. ``params[:_k_zero]`` therefore belongs to the zero model and ``params[_k_zero:]`` to the main model. """ return self.k_exog + self.k_extra1
[docs] def loglike(self, params): """ Log-likelihood of Generic Hurdle model. Parameters ---------- params : array_like The parameters of the model. Returns ------- loglike : float The log-likelihood function of the model evaluated at `params`. """ k = self._k_zero return (self.model1.loglike(params[:k]) + self.model2.loglike(params[k:]))
[docs] def fit(self, start_params=None, method="bfgs", maxiter=35, full_output=1, disp=1, callback=None, cov_type="nonrobust", cov_kwds=None, use_t=None, **kwargs): if cov_type != "nonrobust": raise ValueError("robust cov_type currently not supported") k_zero = self._k_zero if start_params is None: start_params1 = None start_params2 = None else: start_params = array_like(start_params, "start_params", ndim=1) k_params = k_zero + self.k_exog + self.k_extra2 if start_params.size != k_params: raise ValueError( "start_params must have one entry per parameter. The " f"model has {k_params} parameters, {k_zero} in the zero " f"model and {k_params - k_zero} in the main model, but " f"start_params has {start_params.size}." ) start_params1 = start_params[:k_zero] start_params2 = start_params[k_zero:] results1 = self.model1.fit( start_params=start_params1, method=method, maxiter=maxiter, disp=disp, full_output=full_output, callback=lambda x: x, **kwargs ) results2 = self.model2.fit( start_params=start_params2, method=method, maxiter=maxiter, disp=disp, full_output=full_output, callback=lambda x: x, **kwargs ) result = deepcopy(results1) result._results.model = self result.mle_retvals["converged"] = [results1.mle_retvals["converged"], results2.mle_retvals["converged"]] result._results.params = np.append(results1._results.params, results2._results.params) # TODO: the following should be in __init__ or initialize result._results.df_model += results2._results.df_model # this looks wrong attr does not exist, always 0 self.k_extra1 += getattr(results1._results, "k_extra", 0) self.k_extra2 += getattr(results2._results, "k_extra", 0) self.k_extra = (self.k_extra1 + self.k_extra2 + 1) xnames1 = ["zm_" + name for name in self.model1.exog_names] self.exog_names[:] = xnames1 + self.model2.exog_names # fix up cov_params, # we could use normalized cov_params directly, unless it's not used result._results.normalized_cov_params = None try: cov1 = results1._results.cov_params() cov2 = results2._results.cov_params() result._results.normalized_cov_params = block_diag(cov1, cov2) except ValueError as e: if "need covariance" not in str(e): # could be some other problem raise modelfit = self.result_class(self, result._results, results1, results2) result = self.result_class_wrapper(modelfit) return result
fit.__doc__ = DiscreteModel.fit.__doc__
[docs] def fit_regularized( self, start_params=None, method="l1", maxiter="defined_by_method", full_output=1, disp=1, callback=None, alpha=0, trim_mode="auto", auto_trim_tol=0.01, size_trim_tol=1e-4, qc_tol=0.03, **kwargs, ): r""" Fit the model using a regularized maximum likelihood. The zero model and the main model are penalized and fit separately, and the combined parameter vector is then refit jointly. Parameters ---------- start_params : array_like, optional Initial guess of the solution for the log-likelihood maximization. The default is an array of zeros. Ordered as described under `alpha`. method : {'l1', 'l1_cvxopt_cp'}, optional See notes for details. maxiter : int or 'defined_by_method', optional Maximum number of iterations to perform. If 'defined_by_method', then use method defaults (see notes). full_output : bool, optional Set to True to have all available output in the Results object's mle_retvals attribute. The output is dependent on the solver. See LikelihoodModelResults notes section for more information. disp : bool, optional Set to True to print convergence messages. callback : callable, optional Called after each iteration, as callback(xk), where xk is the current parameter vector. alpha : float or array_like, optional Non-negative. The weight multiplying the l1 penalty term. If a scalar, every exog parameter of both components is penalized by this value and the extra parameters are left unpenalized, for example the shape parameter of a NegativeBinomialP component. If an array, it must have one entry per parameter, ordered as the zero model's exog parameters, the zero model's extra parameters, the main model's exog parameters and then the main model's extra parameters. For example, a hurdle model with 3 exog variables, ``zerodist="poisson"`` and ``dist="negbin"`` takes an `alpha` of length 7, whose last entry is the weight on the main model's shape parameter. trim_mode : {'auto', 'size', 'off'}, optional If not 'off', trim (set to zero) parameters that would have been zero if the solver reached the theoretical minimum. If 'auto', trim params using the theory in the notes below. If 'size', trim params if they have very small absolute value. auto_trim_tol : float, optional Tolerance used when trim_mode == 'auto'. size_trim_tol : float, optional Tolerance used when trim_mode == 'size'. qc_tol : float, optional Print warning and do not allow auto trim when condition (ii) in the notes below is violated by this much. **kwargs Additional keyword arguments used when fitting the model. Returns ------- L1HurdleCountResultsWrapper A results instance. Notes ----- Using 'l1_cvxopt_cp' requires the cvxopt module. Optional arguments for the solvers (available in Results.mle_settings):: 'l1' acc : float (default 1e-6) Requested accuracy as used by slsqp 'l1_cvxopt_cp' abstol : float absolute accuracy (default: 1e-7). reltol : float relative accuracy (default: 1e-6). feastol : float tolerance for feasibility conditions (default: 1e-7). refinement : int number of iterative refinement steps when solving KKT equations (default: 1). Optimization methodology With :math:`L` the negative log likelihood, we solve the convex but non-smooth problem .. math:: \min_\beta L(\beta) + \sum_k\alpha_k |\beta_k| via the transformation to the smooth, convex, constrained problem in twice as many variables (adding the "added variables" :math:`u_k`) .. math:: \min_{\beta,u} L(\beta) + \sum_k\alpha_k u_k, subject to .. math:: -u_k \leq \beta_k \leq u_k. With :math:`\partial_k L` the derivative of :math:`L` in the :math:`k^{th}` parameter direction, theory dictates that, at the minimum, exactly one of two conditions holds: (i) :math:`|\partial_k L| = \alpha_k` and :math:`\beta_k \neq 0` (ii) :math:`|\partial_k L| \leq \alpha_k` and :math:`\beta_k = 0` """ _validate_l1_method(method) if maxiter != "defined_by_method": maxiter = int_like(maxiter, "maxiter") if maxiter < 0: raise ValueError("maxiter must be non-negative") full_output = bool_like(full_output, "full_output") disp = bool_like(disp, "disp") if callback is not None and not callable(callback): raise TypeError("callback must be callable or None") trim_mode = string_like( trim_mode, "trim_mode", options=("auto", "size", "off"), lower=False ) auto_trim_tol = float_like(auto_trim_tol, "auto_trim_tol") size_trim_tol = float_like(size_trim_tol, "size_trim_tol") qc_tol = float_like(qc_tol, "qc_tol") k_zero = self._k_zero k_params = k_zero + self.k_exog + self.k_extra2 if np.size(alpha) == 1: # Do not penalize extra parameters if alpha is a scalar alpha = float_like(alpha, "alpha") * np.concatenate([ np.ones(self.k_exog), np.zeros(self.k_extra1), np.ones(self.k_exog), np.zeros(self.k_extra2), ]) else: alpha = array_like(alpha, "alpha", ndim=1) if alpha.size != k_params: raise ValueError( "alpha must be a scalar or a 1-dimensional array with " f"one entry per parameter. The model has {k_params} " f"parameters, {k_zero} in the zero model and " f"{k_params - k_zero} in the main model, but alpha has " f"{alpha.size}." ) if not np.all(alpha >= 0): raise ValueError("alpha must be non-negative") alpha1 = alpha[:k_zero] alpha2 = alpha[k_zero:] if start_params is None: start_params1 = None start_params2 = None else: start_params = array_like(start_params, "start_params", ndim=1) if start_params.size != k_params: raise ValueError( "start_params must have one entry per parameter. The " f"model has {k_params} parameters, {k_zero} in the zero " f"model and {k_params - k_zero} in the main model, but " f"start_params has {start_params.size}." ) start_params1 = start_params[:k_zero] start_params2 = start_params[k_zero:] results1 = self.model1.fit_regularized( start_params=start_params1, method=method, maxiter=maxiter, full_output=full_output, disp=False, callback=callback, alpha=alpha1, trim_mode=trim_mode, auto_trim_tol=auto_trim_tol, size_trim_tol=size_trim_tol, qc_tol=qc_tol, **kwargs, ) start_params1 = results1.params results2 = self.model2.fit_regularized( start_params=start_params2, method=method, maxiter=maxiter, full_output=full_output, disp=False, callback=callback, alpha=alpha2, trim_mode=trim_mode, auto_trim_tol=auto_trim_tol, size_trim_tol=size_trim_tol, qc_tol=qc_tol, **kwargs, ) start_params2 = results2.params start_params = np.append(start_params1, start_params2) cntfit = super(CountModel, self).fit_regularized( start_params=start_params, method=method, maxiter=maxiter, full_output=full_output, disp=disp, callback=callback, alpha=alpha, trim_mode=trim_mode, auto_trim_tol=auto_trim_tol, size_trim_tol=size_trim_tol, qc_tol=qc_tol, **kwargs, ) # params comes from this joint refit, not from either component fit, # so its own converged flag (already set by the call above) has to # be kept, not replaced by the two component flags. cntfit.mle_retvals["converged"] = [ results1.mle_retvals["converged"], results2.mle_retvals["converged"], cntfit.mle_retvals["converged"], ] self.k_extra1 += getattr(results1._results, "k_extra", 0) self.k_extra2 += getattr(results2._results, "k_extra", 0) self.k_extra = (self.k_extra1 + self.k_extra2 + 1) xnames1 = ["zm_" + name for name in self.model1.exog_names] self.exog_names[:] = xnames1 + self.model2.exog_names # Deliberately keep the covariance that came back from the joint # refit. fit has no joint optimization step, so it has to assemble a # covariance out of the two component fits, but here # DiscreteModel.cov_params_func_l1 has already built one from the # joint hessian and the joint set of trimmed parameters. Replacing it # with a block diagonal of the component covariances would pair the # params of one optimization with the standard errors of another, # which shows up as a non-zero coefficient reported with a nan # standard error whenever the two disagree about trimming. hurdlefit = self.result_class_reg( self, cntfit, results_zero=results1, results_count=results2 ) return self.result_class_reg_wrapper(hurdlefit)
[docs] def score_obs(self, params): """ Hurdle model score (gradient) vector of the log-likelihood. Parameters ---------- params : array_like The parameters of the model Returns ------- score_obs : ndarray, 2-D The derivative of the log-likelihood for each observation, evaluated at `params`, with shape (nobs, k_params). """ k_zero = self._k_zero params_zero = params[:k_zero] params_main = params[k_zero:] score_zero = self.model1.score_obs(params_zero) # The score of the main model is only defined for non-zero elements of # endog due to under-the-hood masking in left-truncated models. Since # those entries contribute nothing to the gradient, we can just fill # them with 0. score_main = np.zeros((self.exog.shape[0], self.k_exog + self.k_extra2)) truncated_score_main = self.model2.score_obs(params_main) nonzero_idx = np.nonzero(self.endog)[0] score_main[nonzero_idx, :] = truncated_score_main return np.hstack((score_zero, score_main))
[docs] def score(self, params): return self.score_obs(params).sum(0)
[docs] def hessian(self, params): """ Hurdle model Hessian matrix of the log-likelihood. When the zero and main models are separately estimated, this is a block diagonal matrix of the two models' Hessians. Parameters ---------- params : array_like The parameters of the model Returns ------- hess : ndarray, (k_vars, k_vars) The Hessian, second derivative of loglikelihood function, evaluated at `params` """ k_zero = self._k_zero params_zero = params[:k_zero] params_main = params[k_zero:] hessian_zero = self.model1.hessian(params_zero) hessian_main = self.model2.hessian(params_main) hessian = block_diag(hessian_zero, hessian_main) return hessian
[docs] def predict(self, params, exog=None, exposure=None, offset=None, which="mean", y_values=None): """ Predict response variable or other statistic given exogenous variables. Parameters ---------- params : array_like The parameters of the model. exog : array_like, optional Explanatory variables for the main count model. If ``exog`` is None, then the data from the model will be used. offset : array_like, optional Offset is added to the linear predictor of the mean function with coefficient equal to 1. Default is zero if exog is not None, and the model offset if exog is None. exposure : array_like, optional Log(exposure) is added to the linear predictor with coefficient equal to 1. If exposure is specified, then it will be logged by the method. The user does not need to log it first. Default is one if exog is not None, and it is the model exposure if exog is None. which : str, optional Statistic to predict. Default is 'mean'. - 'mean' : the conditional expectation of endog E(y | x) - 'mean-main' : mean parameter of truncated count model. Note, this is not the mean of the truncated distribution. - 'linear' : the linear predictor of the truncated count model. - 'var' : returns the estimated variance of endog implied by the model. - 'prob-main' : probability of selecting the main model which is the probability of observing a nonzero count P(y > 0 | x). - 'prob-zero' : probability of observing a zero count. P(y=0 | x). This is equal to ``1 - prob-main``. - 'prob-trunc' : probability of truncation of the truncated count model. This is the probability of observing a zero count implied by the truncation model. - 'mean-nonzero' : expected value conditional on having observation larger than zero, E(y | X, y>0) - 'prob' : probabilities of each count from 0 to max(endog), or for y_values if those are provided. This is a multivariate return (2-dim when predicting for several observations). y_values : array_like, optional Values of the random variable endog at which pmf is evaluated. Only used if ``which="prob"`` Returns ------- ndarray The predicted values, whose interpretation depends on `which`. Notes ----- 'prob-zero' / 'prob-trunc' is the ratio of probabilities of observing a zero count between hurdle model and the truncated count model. If this ratio is larger than one, then the hurdle model has an inflated number of zeros compared to the count model. If it is smaller than one, then the number of zeros is deflated. """ which = which.lower() # make it case insensitive no_exog = True if exog is None else False exog, offset, exposure = self._get_predict_arrays( exog=exog, offset=offset, exposure=exposure ) exog_zero = None # not yet if exog_zero is None: if no_exog: exog_zero = self.exog else: exog_zero = exog k_zeros = int((len(params) - self.k_extra1 - self.k_extra2) / 2 ) + self.k_extra1 params_zero = params[:k_zeros] params_main = params[k_zeros:] lin_pred = (np.dot(exog, params_main[:self.exog.shape[1]]) + exposure + offset) # this currently is mean_main, offset, exposure for zero part ? mu1 = self.model1.predict(params_zero, exog=exog) # prob that count model applies y>0 from zero model predict prob_main = self.model1.model_main._prob_nonzero(mu1, params_zero) prob_zero = (1 - prob_main) mu2 = np.exp(lin_pred) prob_ntrunc = self.model2.model_main._prob_nonzero(mu2, params_main) if which == "mean": return prob_main * np.exp(lin_pred) / prob_ntrunc elif which == "mean-main": return np.exp(lin_pred) elif which == "linear": return lin_pred elif which == "mean-nonzero": return np.exp(lin_pred) / prob_ntrunc elif which == "prob-zero": return prob_zero elif which == "prob-main": return prob_main elif which == "prob-trunc": return 1 - prob_ntrunc # not yet supported elif which == "var": # generic computation using results from submodels mu = np.exp(lin_pred) mt, vt = self.model2._predict_mom_trunc0(params_main, mu) var_ = prob_main * vt + prob_main * (1 - prob_main) * mt**2 return var_ elif which == "prob": probs_main = self.model2.predict( params_main, exog, np.exp(exposure), offset, which="prob", y_values=y_values) probs_main *= prob_main[:, None] probs_main[:, 0] = prob_zero return probs_main else: raise ValueError(f"which = {which} is not available")
class TruncatedLFGenericResults(CountResults): __doc__ = _discrete_results_docs % { "one_line_description": "A results class for Generic Truncated", "extra_attr": ""}
[docs] class TruncatedLFPoissonResults(TruncatedLFGenericResults): __doc__ = _discrete_results_docs % { "one_line_description": "A results class for Truncated Poisson", "extra_attr": ""} @cache_readonly def _dispersion_factor(self): if self.model.trunc != 0: msg = "dispersion is only available for zero-truncation" raise NotImplementedError(msg) mu = np.exp(self.predict(which="linear")) return (1 - mu / (np.exp(mu) - 1))
[docs] class TruncatedNegativeBinomialResults(TruncatedLFGenericResults): __doc__ = _discrete_results_docs % { "one_line_description": "A results class for Truncated Negative Binomial", "extra_attr": ""} @cache_readonly def _dispersion_factor(self): if self.model.trunc != 0: msg = "dispersion is only available for zero-truncation" raise NotImplementedError(msg) alpha = self.params[-1] p = self.model.model_main.parameterization mu = np.exp(self.predict(which="linear")) return (1 - alpha * mu**(p-1) / (np.exp(mu**(p-1)) - 1))
class L1TruncatedLFGenericResults(L1CountResults, TruncatedLFGenericResults): pass class TruncatedLFGenericResultsWrapper(lm.RegressionResultsWrapper): pass wrap.populate_wrapper(TruncatedLFGenericResultsWrapper, TruncatedLFGenericResults) class L1TruncatedLFGenericResultsWrapper(lm.RegressionResultsWrapper): pass wrap.populate_wrapper(L1TruncatedLFGenericResultsWrapper, L1TruncatedLFGenericResults)
[docs] class HurdleCountResults(CountResults): __doc__ = _discrete_results_docs % { "one_line_description": "A results class for Hurdle model", "extra_attr": ""} def __init__(self, model, mlefit, results_zero, results_count, cov_type="nonrobust", cov_kwds=None, use_t=None): super().__init__( model, mlefit, cov_type=cov_type, cov_kwds=cov_kwds, use_t=use_t, ) self.results_zero = results_zero self.results_count = results_count # TODO: this is to fix df_resid, should be automatic but is not self.df_resid = self.model.endog.shape[0] - len(self.params)
[docs] @cache_readonly def llnull(self): return (self.results_zero._results.llnull + self.results_count._results.llnull)
[docs] @cache_readonly def bse(self): return np.append(self.results_zero.bse, self.results_count.bse)
class L1HurdleCountResults(HurdleCountResults): __doc__ = _discrete_results_docs % { "one_line_description": "A results class for Hurdle model fit by l1 regularization", "extra_attr": _l1_results_attr } def __init__( self, model, mlefit, results_zero, results_count, cov_type="nonrobust", cov_kwds=None, use_t=None, ): super().__init__( model=model, mlefit=mlefit, results_zero=results_zero, results_count=results_count, cov_type=cov_type, cov_kwds=cov_kwds, use_t=use_t ) # TODO: mixins might eliminate the need for the below duplicated code # (cf. .discrete_model.L1CountResults) # self.trimmed is a boolean array with T/F telling whether or not that # entry in params has been zeroed out. self.trimmed = mlefit.mle_retvals["trimmed"] self.nnz_params = (~self.trimmed).sum() # Set degrees of freedom. Adjust for extra parameters not included in # df_model. Unlike L1CountResults, df_resid does not add k_extra back, # so that it agrees with HurdleCountResults on an untrimmed fit. k_extra = getattr(self.model, "k_extra", 0) self.df_model = self.nnz_params - 1 - k_extra self.df_resid = self.model.endog.shape[0] - self.nnz_params @cache_readonly def bse(self): # Not HurdleCountResults.bse, which concatenates the standard errors # of the two component fits. params come from the joint refit, so the # standard errors have to come from the joint covariance as well, or # the two disagree about which parameters were trimmed. This is the # standard LikelihoodModelResults.bse. with warnings.catch_warnings(): warnings.simplefilter("ignore", RuntimeWarning) return np.sqrt(np.diag(self.cov_params())) class HurdleCountResultsWrapper(lm.RegressionResultsWrapper): pass wrap.populate_wrapper(HurdleCountResultsWrapper, HurdleCountResults) class L1HurdleCountResultsWrapper(lm.RegressionResultsWrapper): pass wrap.populate_wrapper(L1HurdleCountResultsWrapper, L1HurdleCountResults)