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()

• Is GenericLikelihoodModel of your own doing or is it from a third-party library? – Mathias Ettinger Jul 20 '18 at 10:44
• – mcocdawc Jul 20 '18 at 11:00

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)

• Thank you very much for your feedback. I just wanted to wait a bit more before accepting. – mcocdawc Jul 21 '18 at 23:16