Building models
pyreml relies on the MixedModel.from_dataframe() method to build a model from a pandas DataFrame. This high-leve constructor reads the design from patsy formulas, assembles the incidence and covariance matrices, and returns a model ready to fit.
The from_dataframe constructor
model = MixedModel.from_dataframe(
data = df,
response = "height",
fixed = "1 + C(BLOC)",
random = Random(
formula = "1",
unit = "ID",
right_hand = "str",
covariance = K,
matrix_index = ped["id"].tolist(),
),
residual = Residual(
right_hand = "het",
het_formula = "1 + C(BLOC)",
)
)
model.fit()| Argument | Meaning |
|---|---|
data |
One row per observation, wide format (one column per response variable). |
response |
One response variable, or a list for a multivariate model. |
fixed |
A patsy formula; the intercept is "1". Applied per response. |
random |
A Random object or a list of them. See variance structures and random regression. |
residual |
A Residual object; defaults to iid. See variance structures. |
dtype |
The PyTorch floating-point dtype for every tensor of the model. Defaults to torch.double for maximal precision. Downgrade to torch.float for computational speed. |
device |
The device the model lives and trains on. Defaults to "cpu", use "cuda" for GPU acceleration. |
Each argument maps onto a term of the model: fixed builds \(\mathbf{X}\), each random effect contributes columns to \(\mathbf{Z}\) and a block of \(\mathbf{G}\), and residual defines \(\mathbf{R}\) (see the model).
Data layout and missing values
data is in wide format: one row per observation, one column per response variable. Missing values are handled per response: a row missing a given response is dropped for that response only, while rows missing a fixed covariate or a grouping variable are dropped throughout. Each response therefore keeps its own set of observed rows, and unbalanced designs are handled out of the box.
The fitted model
After fit() (see training), the estimates are read off the model and its effects:
| Accessor | Content |
|---|---|
model.estimates |
Fixed-effect estimates (BLUE) as a long table response · term · estimate · SE · t. |
model.random[i].table |
Random-effect predictions (BLUP) for effect i, as a long table unit · prediction · SE · CD, with SE the prediction error standard deviation and CD the coefficient of determination (see the BLUP intuition). |
model.random[i].variance |
Its estimated variance structure. For homogeneity across methods, sigma is always a variance (\(\Sigma\) or \(\sigma^2\)) and never a standard deviation \(\sigma\). |
model.residual.table |
Model residuals, as a long table observation · response · residual · SD, with SD the residual standard deviation, i.e. the square root of the corresponding diagonal element of \(\mathbf{R}\). |
model.residual.variance |
The estimated residual variance. |
model.AIC |
Akaike information criterion AIC. |
model.AIC_meth |
Method with which the AIC was obtained. |
pyreml does not compute p-values for the fixed effects. However, as an approximation, the t column can be compared to a \(\mathcal{N}(0,1)\) distribution. This approximation asymptotically improves with the sample size.
Low-level constructor
pyreml provides a low-level constructor for application cases that are not implemented. The low-level constructor builds directly from the response vector y, the design matrices X and Z, and a “residual incidence” matrix W that masks useless elements of R. It must also be provided a list of PyTorch variance parameters varparams and at least one differentiable variance method: varmeth, varmeth_inv, or both.
Like from_dataframe, it accepts dtype and device. Every tensor inside varmeth / varmeth_inv must match them.
import torch
from pyreml import MixedModel
dtype = torch.double
device = "cuda"
log_su = torch.zeros((), dtype=dtype, device=device, requires_grad=True)
log_se = torch.zeros((), dtype=dtype, device=device, requires_grad=True)
varparams = [{"tensor": log_su}, {"tensor": log_se}]
def varmeth(self):
G = torch.exp(log_su) * torch.eye(self.Z.shape[1], dtype=self.dtype, device=self.device)
R = torch.exp(log_se) * torch.eye(self.n, dtype=self.dtype, device=self.device)
return G, R
model = MixedModel(
y, X, Z, W=None,
varmeth=varmeth,
varparams=varparams,
dtype=dtype,
device=device,
)
model.fit()The varmeth contract
varmeth(self) -> (G, R) is bound as a method on the model, so it can read self.Z, self.W, self.n, self.dtype, self.device, … and returns the pair \((\mathbf{G}, \mathbf{R})\) that forms \(\mathbf{V} = \mathbf{Z}\mathbf{G}
\mathbf{Z}^\top + \mathbf{R}\). It must be differentiable in the varparams tensors so autograd can propagate the REML gradient. G may be None when the model has no random effect.
The varmeth_inv contract
varmeth_inv(self) -> (Ginv, Rinv, logdet_G, logdet_R) is the inverse-side alternative. Rather than forming \(\mathbf{V}\) and factoring it, it supplies the structured inverses \(\mathbf{G}^{-1}\) and \(\mathbf{R}^{-1}\) together with the log-determinants \(\log|\mathbf{G}|\) and \(\log|\mathbf{R}|\), from which the likelihood is evaluated without ever assembling \(\mathbf{V}\) (see SMW). Like varmeth, it is bound as a method and must be differentiable in the varparams tensors. Ginv and logdet_G may both be None when the model has no random effect.
def varmeth_inv(self):
Ginv = torch.exp(-log_su) * torch.eye(self.Z.shape[1], dtype=self.dtype, device=self.device)
Rinv = torch.exp(-log_se) * torch.eye(self.n, dtype=self.dtype, device=self.device)
logdet_G = self.Z.shape[1] * log_su
logdet_R = self.n * log_se
return Ginv, Rinv, logdet_G, logdet_R
model = MixedModel(
y, X, Z, W=None,
varmeth_inv=varmeth_inv,
varparams=varparams,
dtype=dtype,
device=device,
)
model.fit()Either method alone is sufficient and fixes the solving path; supplying both lets the model choose (see SMW).
The varparams format
A list of dicts, one per trainable variance parameter, each carrying at least a tensor key — a leaf PyTorch tensor with requires_grad, created on the model’s dtype and device. These must be the very tensors read inside varmeth (or varmeth_inv). The optimizer trains \(\boldsymbol{\beta}\) jointly with them; \(\boldsymbol{\beta}\) is initialized from \(\mathbf{X}\) and is not part of varparams.