2
\$\begingroup\$

For the purpose of defining a lot of Maximum Likelihood estimators, I think I need a metaclass.

At the moment I have to copy/paste a lot of code for every new class definition and just substitute the corresponding scipy.stats functions.

from scipy.stats import fisk, t
from statsmodels.base.model import GenericLikelihoodModel
from inspect import signature


class Fisk(GenericLikelihoodModel):
    """A maximum likelihood estimator for the fisk distribution.
    """

    nparams = 3

    def loglike(self, params):
        return fisk.logpdf(self.endog, *params).sum()

    def fit(self, **kwargs):
        if 'start_params' not in kwargs:
            # This is for performance and! convergence stability.
            # The scipy function provides better starting params.
            kwargs['start_params'] = fisk.fit(self.endog)
        res = super().fit(**kwargs)
        res.df_model = self.nparams
        res.df_resid = len(self.endog) - self.nparams
        return res

class T(GenericLikelihoodModel):
    """A maximum likelihood estimator for the Student-T distribution.
    """

    nparams = 3

    def loglike(self, params):
        return t.logpdf(self.endog, *params).sum()

    def fit(self, **kwargs):
        if 'start_params' not in kwargs:
            # This is for performance and! convergence stability.
            # The scipy function provides better starting params.
            kwargs['start_params'] = t.fit(self.endog)
        res = super().fit(**kwargs)
        res.df_model = self.nparams
        res.df_resid = len(self.endog) - self.nparams
        return res

With a metaclass I automated this code definition

class ML_estimator(type):
    def __new__(cls, clsname, bases, dct, f):
        return super().__new__(cls, clsname, (GenericLikelihoodModel, ), dct)

    def __init__(cls, clsname, bases, dct, f):
        cls.nparams = len(signature(f.pdf).parameters)
        def loglike(self, params):
            return f.logpdf(self.endog, *params).sum()
        cls.loglike = loglike

        def fit(self, **kwargs):
            if 'start_params' not in kwargs:
                # This is for performance and! convergence stability.
                # The scipy function provides better starting params.
                kwargs['start_params'] = f.fit(self.endog)
            res = super(cls, self).fit(**kwargs)
            res.df_model = self.nparams
            res.df_resid = len(self.endog) - self.nparams
            return res
        cls.fit = fit

class Fisk(metaclass=ML_estimator, f=fisk):
    pass

class T(metaclass=ML_estimator, f=t):
    pass

Does this seem like a reasonable application of metaclasses? What improvements do you have?

The ML-estimator can be tested with:

sample = fisk.rvs(c=1, size=1000)
res = Fisk(sample).fit()
res.summary()
\$\endgroup\$
3
\$\begingroup\$

Instead of constructing the object and then modifying its methods and attributes, you can directly add them in the __new__ method of the metaclass:

class LikelihoodEstimator(type):
    def __new__(cls, name, bases, dct, f):
        def loglike(self, params):
            return f.logpdf(self.endog, *params).sum()

        def fit(self, **kwargs):
            if 'start_params' not in kwargs:
                # This is for performance and! convergence stability.
                # The scipy function provides better starting params.
                kwargs['start_params'] = f.fit(self.endog)
            res = super(cls, self).fit(**kwargs)
            res.df_model = self.nparams
            res.df_resid = len(self.endog) - self.nparams
            return res

        dct.update(loglike=loglike, fit=fit, nparams=len(signature(f.pdf).parameters))
        return super().__new__(cls, name, bases + (GenericLikelihoodModel,), dct)

I also changed the name to more nicely fit within PEP8 guidelines. I also included back bases to avoid undesirable surprises when using this metaclass.


The other option that I think is more easily understandable is to use inheritance instead of metaclasses. There will still be a bit of boilerplate left but the understanding at a glance should overweight:

from scipy.stats import fisk, t
from statsmodels.base.model import GenericLikelihoodModel as _LikelihoodModel
from inspect import signature


class GenericLikelihoodModel(_LikelihoodModel):
    """A maximum likelihood estimator for any distribution"""

    def __init__(self, distribution, endog, exog=None, loglike=None, score=None, hessian=None, missing='none', extra_params_names=None, **kwds)
        self.nparams = len(signature(distribution.pdf).parameters)
        self.distribution = distribution
        super().__init__(endog, exog, loglike, score, hessian, missing, extra_params_names, **kwds)

    def loglike(self, params):
        return self.distribution.logpdf(self.endog, *params).sum()

    def fit(self, **kwargs):
        if 'start_params' not in kwargs:
            # This is for performance and! convergence stability.
            # The scipy function provides better starting params.
            kwargs['start_params'] = self.distribution.fit(self.endog)
        res = super().fit(**kwargs)
        res.df_model = self.nparams
        res.df_resid = len(self.endog) - self.nparams
        return res


class Fisk(GenericLikelihoodModel):
    """A maximum likelihood estimator for the fisk distribution"""
    def __init__(endog, exog=None, loglike=None, score=None, hessian=None, missing='none', extra_params_names=None, **kwds)
        super().__init__(fisk, endog, exog, loglike, score, hessian, missing, extra_params_names, **kwds)


class StudentT(GenericLikelihoodModel):
    """A maximum likelihood estimator for the Student-T distribution"""
    def __init__(endog, exog=None, loglike=None, score=None, hessian=None, missing='none', extra_params_names=None, **kwds)
        super().__init__(t, endog, exog, loglike, score, hessian, missing, extra_params_names, **kwds)
\$\endgroup\$
  • \$\begingroup\$ Thank you very much for your feedback. I just wanted to wait a bit more before accepting. \$\endgroup\$ – mcocdawc Jul 21 '18 at 23:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.