import warnings
from .._data import conform_dataset, normalize_likelihood
from .._display import session_block
[docs]class VarDec(object):
"""
Variance decompositon through GLMMs.
Example
-------
.. doctest::
>>> from limix.vardec import VarDec
>>> from limix.stats import multivariate_normal as mvn
>>> from numpy import ones, eye, concatenate, zeros, exp
>>> from numpy.random import RandomState
>>>
>>> random = RandomState(0)
>>> nsamples = 20
>>>
>>> M = random.randn(nsamples, 2)
>>> M = (M - M.mean(0)) / M.std(0)
>>> M = concatenate((ones((nsamples, 1)), M), axis=1)
>>>
>>> K0 = random.randn(nsamples, 10)
>>> K0 = K0 @ K0.T
>>> K0 /= K0.diagonal().mean()
>>> K0 += eye(nsamples) * 1e-4
>>>
>>> K1 = random.randn(nsamples, 10)
>>> K1 = K1 @ K1.T
>>> K1 /= K1.diagonal().mean()
>>> K1 += eye(nsamples) * 1e-4
>>>
>>> y = M @ random.randn(3) + mvn(random, zeros(nsamples), K0)
>>> y += mvn(random, zeros(nsamples), K1)
>>>
>>> vardec = VarDec(y, "normal", M)
>>> vardec.append(K0)
>>> vardec.append(K1)
>>> vardec.append_iid()
>>>
>>> vardec.fit(verbose=False)
>>> print(vardec) # doctest: +FLOAT_CMP
Variance decomposition
----------------------
<BLANKLINE>
π² ~ π(πΌπΆ, 0.385β
πΊ + 1.184β
πΊ + 0.000β
πΈ)
>>> y = exp((y - y.mean()) / y.std())
>>> vardec = VarDec(y, "poisson", M)
>>> vardec.append(K0)
>>> vardec.append(K1)
>>> vardec.append_iid()
>>>
>>> vardec.fit(verbose=False)
>>> print(vardec) # doctest: +FLOAT_CMP
Variance decomposition
----------------------
<BLANKLINE>
π³ ~ π(πΌπΆ, 0.000β
πΊ + 0.350β
πΊ + 0.000β
πΈ) for yα΅’ ~ Poisson(Ξ»α΅’=g(zα΅’)) and g(x)=eΛ£
"""
[docs] def __init__(self, y, lik="normal", M=None):
"""
Constructor.
Parameters
----------
y : array_like
Phenotype.
lik : tuple, "normal", "bernoulli", "probit", "binomial", "poisson"
Sample likelihood describing the residual distribution.
Either a tuple or a string specifying the likelihood is required. The
Normal, Bernoulli, Probit, and Poisson likelihoods can be selected by
providing a string. Binomial likelihood on the other hand requires a tuple
because of the number of trials: ``("binomial", array_like)``. Defaults to
``"normal"``.
M : nΓc array_like
Covariates matrix.
"""
from numpy import asarray, eye
from glimix_core.mean import LinearMean, KronMean
y = asarray(y, float)
data = conform_dataset(y, M)
y = data["y"]
M = data["M"]
self._y = y
self._M = M
self._lik = normalize_likelihood(lik)
if self._multi_trait():
A = eye(self._y.shape[1])
self._mean = KronMean(A, asarray(M, float))
else:
self._mean = LinearMean(asarray(M, float))
self._covariance = []
self._glmm = None
self._fit = False
self._unnamed = 0
@property
def effsizes(self):
"""
Covariace effect sizes.
Returns
-------
effsizes : ndarray
Effect sizes.
"""
if not self._fit:
self.fit()
if hasattr(self._mean, "effsizes"):
return self._mean.effsizes
return self._mean.B
@property
def covariance(self):
"""
Get the covariance matrices.
Returns
-------
covariances : list
Covariance matrices.
"""
return self._covariance
def fit(self, verbose=True):
"""
Fit the model.
Parameters
----------
verbose : bool, optional
Set ``False`` to silence it. Defaults to ``True``.
"""
with session_block("Variance decomposition", disable=not verbose):
if self._lik[0] == "normal":
if self._multi_trait():
self._fit_lmm_multi_trait(verbose)
elif self._simple_model():
self._fit_lmm_simple_model(verbose)
else:
self._fit_lmm(verbose)
else:
if self._simple_model():
self._fit_glmm_simple_model(verbose)
else:
self._fit_glmm(verbose)
if verbose:
print(self)
self._fit = True
def lml(self):
"""
Get the log of the marginal likelihood.
Returns
-------
float
Log of the marginal likelihood.
"""
if not self._fit:
self._glmm.fit()
return self._glmm.lml()
def append_iid(self, name="residual"):
from glimix_core.cov import EyeCov
if self._multi_trait():
cov = MTEyeCov(self._y.shape[1])
else:
cov = EyeCov(self._y.shape[0])
cov.name = name
self._covariance.append(cov)
def append(self, K, name=None):
from numpy_sugar import is_all_finite
from numpy import asarray
from glimix_core.cov import GivenCov
data = conform_dataset(self._y, K=K)
K = asarray(data["K"], float)
if not is_all_finite(K):
raise ValueError("Covariance-matrix values must be finite.")
K = K / K.diagonal().mean()
if self._multi_trait():
cov = MTGivenCov(self._y.shape[1], K)
else:
cov = GivenCov(K)
if name is None:
name = "unnamed-{}".format(self._unnamed)
self._unnamed += 1
cov.name = name
self._covariance.append(cov)
def plot(self):
import limix
import seaborn as sns
from matplotlib.ticker import FormatStrFormatter
variances = [c.scale for c in self._covariance]
variances = [(v / sum(variances)) * 100 for v in variances]
names = [c.name for c in self._covariance]
ax = sns.barplot(x=names, y=variances)
ax.yaxis.set_major_formatter(FormatStrFormatter("%.0f%%"))
ax.set_xlabel("random effects")
ax.set_ylabel("explained variance")
ax.set_title("Variance decomposition")
with warnings.catch_warnings():
warnings.simplefilter("ignore")
limix.plot.get_pyplot().tight_layout()
limix.plot.show()
def _fit_lmm(self, verbose):
from glimix_core.cov import SumCov
from glimix_core.gp import GP
from numpy import asarray
y = asarray(self._y, float).ravel()
gp = GP(y, self._mean, SumCov(self._covariance))
gp.fit(verbose=verbose)
self._glmm = gp
def _fit_glmm(self, verbose):
from glimix_core.cov import SumCov
from glimix_core.ggp import ExpFamGP
from numpy import asarray
y = asarray(self._y, float).ravel()
gp = ExpFamGP(y, self._lik, self._mean, SumCov(self._covariance))
gp.fit(verbose=verbose)
self._glmm = gp
def _fit_lmm_multi_trait(self, verbose):
from numpy import sqrt, asarray
from glimix_core.lmm import Kron2Sum
from numpy_sugar.linalg import economic_qs, ddot
X = asarray(self._M, float)
QS = economic_qs(self._covariance[0]._K)
G = ddot(QS[0][0], sqrt(QS[1]))
lmm = Kron2Sum(self._y, self._mean.A, X, G, rank=1, restricted=True)
lmm.fit(verbose=verbose)
self._glmm = lmm
self._covariance[0]._set_kron2sum(lmm)
self._covariance[1]._set_kron2sum(lmm)
self._mean.B = lmm.B
def _fit_lmm_simple_model(self, verbose):
from numpy_sugar.linalg import economic_qs
from glimix_core.lmm import LMM
from numpy import asarray
K = self._get_matrix_simple_model()
y = asarray(self._y, float).ravel()
QS = None
if K is not None:
QS = economic_qs(K)
lmm = LMM(y, self._M, QS)
lmm.fit(verbose=verbose)
self._set_simple_model_variances(lmm.v0, lmm.v1)
self._glmm = lmm
def _fit_glmm_simple_model(self, verbose):
from numpy_sugar.linalg import economic_qs
from glimix_core.glmm import GLMMExpFam
from numpy import asarray
K = self._get_matrix_simple_model()
y = asarray(self._y, float).ravel()
QS = None
if K is not None:
QS = economic_qs(K)
glmm = GLMMExpFam(y, self._lik, self._M, QS)
glmm.fit(verbose=verbose)
self._set_simple_model_variances(glmm.v0, glmm.v1)
self._glmm = glmm
def _set_simple_model_variances(self, v0, v1):
from glimix_core.cov import GivenCov, EyeCov
for c in self._covariance:
if isinstance(c, GivenCov):
c.scale = v0
elif isinstance(c, EyeCov):
c.scale = v1
def _get_matrix_simple_model(self):
from glimix_core.cov import GivenCov
K = None
for i in range(len(self._covariance)):
if isinstance(self._covariance[i], GivenCov):
self._covariance[i].scale = 1.0
K = self._covariance[i].value()
break
return K
def _multi_trait(self):
return self._y.ndim == 2 and self._y.shape[1] > 1
def _simple_model(self):
from glimix_core.cov import GivenCov, EyeCov
if len(self._covariance) > 2:
return False
c = self._covariance
if len(c) == 1 and isinstance(c[0], EyeCov):
return True
if isinstance(c[0], GivenCov) and isinstance(c[1], EyeCov):
return True
if isinstance(c[1], GivenCov) and isinstance(c[0], EyeCov):
return True
return False
def __repr__(self):
from glimix_core.cov import GivenCov
from limix.qtl._result._draw import draw_model
from limix._display import draw_title
covariance = ""
for c in self._covariance:
s = c.scale
if isinstance(c, GivenCov):
covariance += f"{s:.3f}β
πΊ + "
else:
covariance += f"{s:.3f}β
πΈ + "
if len(covariance) > 2:
covariance = covariance[:-3]
msg = draw_title("Variance decomposition")
msg += draw_model(self._lik[0], "πΌπΆ", covariance)
msg = msg.rstrip()
return msg
class MTGivenCov:
def __init__(self, ntraits, K):
self._ntraits = ntraits
self._K = K
self._kron2sum = None
self._name = "unnamed"
def _set_kron2sum(self, kron2sum):
self._kron2sum = kron2sum
@property
def scale(self):
"""
Scale parameter, s.
"""
from numpy import eye
if self._kron2sum is None:
return eye(self._ntraits)
return self._kron2sum.C0
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
class MTEyeCov:
def __init__(self, ntraits):
self._ntraits = ntraits
self._kron2sum = None
self._name = "unnamed"
def _set_kron2sum(self, kron2sum):
self._kron2sum = kron2sum
@property
def scale(self):
"""
Scale parameter, s.
"""
from numpy import eye
if self._kron2sum is None:
return eye(self._ntraits)
return self._kron2sum.C1
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name