Implement metaclass for Maximumlikelihood estimator 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()
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)