Overview

In this section, we demonstrate how to use MatrixLM.jl with a simple example using simulated data.

Matrix Linear Models (MLMs) provide a simple yet robust multivariate framework for encoding relationships and groupings in high-throughput data. MLM's flexibility allows it to encode both categorical and continuous relationships, thereby enhancing the detection of associations between responses and predictors.

Within the scope of the matrix linear model framework, the model is articulated as follows:

\[Y = XBZ^T+E\]

Where

  • $Y_{n \times m}$ is the response matrix
  • $X_{n \times p}$ is the matrix for main predictors,
  • $Z_{m \times q}$ denotes the response attributes matrix based on supervised knowledge,
  • $E_{n \times m}$ is the error term,
  • $B_{p \times q}$ is the matrix for main and interaction effects.

Data Generation

For simplicity, we assume that both the responses and the predictors are presented as dataframes.

Our dataset consists of a dataframe dfX, which includes 5 predictors (but will require p=7 coefficients, as we will see later). Among these predictors, two are categorical, and three are numerical, spread across n = 100 samples. We then consider a response dataframe Y composed of m = 250 responses. To simulate the Y data, we need to generate the matrices Z,B, and E.

The matrix Z provides information about the response population, which corresponds to the Y's columns $y_{i \in [1, 250]}$. The dimensions of this matrix are set at 250x10.

Given this setup, the coefficient matrix B is designed to have dimensions of 7×10. This matches the number of columns in X (the matrix coding the 5 predictors in the data frame) with the number of information categories in Z. Finally, we construct the noise matrix E containing the error terms. We generate this matrix as a normally distributed matrix (all values from N(0,σ=4)), adding a layer of randomness to our simulation.

using MatrixLM, DataFrames, Random, Plots, StatsModels, Statistics
using StableRNGs

rng = StableRNG(2026)

# Dimensions of matrices
n = 100
m = 250

# Number of groupings designed in the Z matrix
q = 10

# Generate data with two categorical variables and 3 numerical variables.
dfX = hcat(
    DataFrame(catvar1=string.(rand(rng, 0:1, n)), catvar2=rand(rng, ["A", "B", "C", "D"], n)),
    DataFrame(rand(rng, n,3), ["var3", "var4", "var5"])
    );

first(dfX, 5)
5×5 DataFrame
Rowcatvar1catvar2var3var4var5
StringStringFloat64Float64Float64
10A0.6546910.847950.753778
21A0.02193910.4387050.612088
30B0.7719630.3776010.77834
41D0.3828140.06036950.597588
51C0.3395130.2723420.812023

Let's use the function design_matrix() to get the predictor model matrix based on the formula expression, including all the variable terms.

# Convert dataframe to prediction matrix
X = design_matrix(@mlmformula(catvar1 + catvar2 + var3 + var4 + var5), dfX)

p = size(X)[2]; # 5 columns in dfX, but p=7 coefs bc catvar2 has 4 levels

X[1:5, :]
5×7 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.654691   0.84795    0.753778
 1.0  0.0  0.0  0.0  0.0219391  0.438705   0.612088
 0.0  1.0  0.0  0.0  0.771963   0.377601   0.77834
 1.0  0.0  0.0  1.0  0.382814   0.0603695  0.597588
 1.0  0.0  1.0  0.0  0.339513   0.272342   0.812023

We also have the option to specify contrast coding in our model. For a detailed understanding of how to implement contrast coding, please refer to the documentation for contrast coding with StatsModels.jl. This provides comprehensive instructions and examples.

# Convert dataframe to prediction matrix
my_ctrst = Dict(
             :catvar1 => DummyCoding(base = "0"),
             :catvar2 => DummyCoding(base = "A")
           )

X = design_matrix(@mlmformula(catvar1 + catvar2 + var3 + var4 + var5), dfX, my_ctrst);

X[1:5, :]
5×7 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.654691   0.84795    0.753778
 1.0  0.0  0.0  0.0  0.0219391  0.438705   0.612088
 0.0  1.0  0.0  0.0  0.771963   0.377601   0.77834
 1.0  0.0  0.0  1.0  0.382814   0.0603695  0.597588
 1.0  0.0  1.0  0.0  0.339513   0.272342   0.812023

Randomly generate some data for column covariates Z and the error matrix E:

Z = rand(rng, m,q);
E = randn(rng, n,m).*4;

Next, we will structure the coefficient matrix B following a specific pattern. This approach will facilitate a more effective visualization of the results in the subsequent steps:

# (p,q)
B = [
    2.0   3.0   4.0   5.0  6.0  7.0  8.0  0.0 0.5 -2.0;
    0.01  0.02  0.01  0.09 0.18 0.03 0.14 0.0 0.5 -2.0;
    -1.0  -0.5  0.02  0.49 1.1  2.0  5.0  0.0 0.5 -2.0;
    -0.01 0.02  -0.01 3.0  3.0  7.0  0.14 0.0 0.5 -2.0;
    0.0   0.0   0.0   0.0  3.0  3.0  3.0  3.0 0.5 -2.0;
    3.0   3.0   3.0   3.0  0.08 0.03 0.0  0.0 0.5 -2.0;
    0.01  0.0   3.0   3.0  3.0  3.0 0.04  0.0 0.5 -2.0;
];

Generate the response matrix Y:

Y = X*B*Z' + E;

Now, construct the RawData object consisting of the response variable Y and row/column predictors X and Z. All three matrices must be passed in as 2-dimensional arrays.

# Construct a RawData object
dat = RawData(Response(Y), Predictors(X, Z));

We can use the function show() to respectively display a readable summary of the matrices and dimensions stored in a RawData object.

show(dat)
RawData
  Response matrix Y: 100 × 250
  Design matrix X: 100 × 7
  Design matrix Z: 250 × 10
  X includes intercept: false
  Z includes intercept: false
  Preview of Y first row (first 3 columns): [14.4917, 11.2353, 16.684, …]
  Preview of X first row (first 3 columns): [0.0, 0.0, 0.0, …]
  Preview of Z first row (first 3 columns): [0.7882, 0.0619, 0.0065, …]

Model estimation

Least-squares estimates for matrix linear models can be obtained by running mlm. An object of type Mlm will be returned, with variables for the coefficient estimates (B), the coefficient variance estimates (varB), and the estimated variance of the errors (sigma). By default, mlm estimates both row and column main effects (X and Z intercepts), but this behavior can be suppressed by setting addXIntercept=false and/or addZIntercept=false. Column weights for Y and the target type for variance shrinkage[1] can be optionally supplied to weights and targetType, respectively.

est = mlm(dat; addXIntercept=false, addZIntercept=false); # Model estimation

We can use the function show() to respectively display a readable summary of the matrices and dimensions stored in a Mlm object.

show(est)
Matrix linear model fit
  Observations: 100
  Responses: 250
  Row predictors: 7
  Column predictors: 10
  Coefficient matrix B: 7 × 10
  Residual covariance matrix sigma(Σ): 250 × 250
  Weighted fit: false
  Preview of B first row (first 3 columns): [2.2743, 3.1679, 3.9293, …]
  Preview of sigma(Σ) first row (first 3 columns): [18.6124, 1.7587, -0.3541, …]
  Covariance shrinkage: none

Model predictions and residuals

The coefficient estimates can be accessed using coef(est). Predicted values and residuals can be obtained by calling predict() and residuals(). By default, both of these functions use the same data used to fit the model. However, a new Predictors object can be passed into predict() as the newPredictors argument, and a new RawData object can be passed into residuals() as the newData argument. For convenience, fitted(est) will return the fitted values by calling predict with the default arguments.

To compare the estimated coefficients with the original matrix B, we will visualize the matrices using heatmaps. This graphical representation allows us to readily see differences and similarities between the two.

esti_coef = coef(est); # Get the coefficients of the model

plot(
    heatmap(B[end:-1:1, :],
            size = (800, 300)),
    heatmap(esti_coef[end:-1:1, :],
            size = (800, 300),
            clims = (-2, 8)),
    title = ["\$ \\mathbf{B}\$" "\$ \\mathbf{\\hat{B}}\$"]
)
Example block output

Let's employ the same visualization method to compare the predicted values with the original Y response matrix. This allows us to gauge the accuracy of our model predictions.

preds = predict(est); # Prediction value

plot(
    heatmap(Y[end:-1:1, :],
            size = (800, 300)),
    heatmap(preds.Y[end:-1:1, :],
            size = (800, 300),
            # clims = (-2, 8)
            ),
    title = ["\$ \\mathbf{Y}\$" "\$ \\mathbf{\\hat{Y}}\$"]
)
Example block output

The residuals() function, available in MatrixLM.jl, computes residuals for each observation, helping us evaluate the discrepancy between the model's predictions and the observed data.

resids = residuals(est);

plot(
    heatmap(resids[end:-1:1, :],
            size = (800, 300)),
    histogram(
        (reshape(resids,250*100,1)),
            grid  = false,
            label = "",
            size = (800, 300)),
    title = ["Residuals" "Distribution of the residuals"]
)
Example block output

T-statistics and permutation test

The t-statistics for an Mlm object (defined as est.B ./ sqrt.(est.varB)) can be obtained by running t_stat. By default, t_stat does not return the corresponding t-statistics for any main effects that were estimated by mlm, but they will be returned if isMainEff=true.

tStats = t_stat(est);

Permutation p-values for the t-statistics can be computed by the mlm_perms function. mlm_perms calls the more general function perm_pvals and will run the permutations in parallel when possible. The illustrative example below runs only 500 permutations; a different number can be specified as the second argument (by modifying the nPerms value). By default, the function used to permute Y is shuffle_rows, which shuffles the rows for Y. Alternative functions for permuting Y, such as shuffle_cols, can be passed into the argument permFun. mlm_perms calls mlm and t_stat, so the user is free to specify keyword arguments for mlm or t_stat; by default, mlm_perms will call both functions using their default behavior.

nPerms = 500
tStats, pVals = mlm_perms(dat, nPerms, addXIntercept=false, addZIntercept=false);

plot(
    heatmap(tStats[end:-1:1, :],
            c = :bluesreds,
            clims = (-40, 40),
            size = (800, 300)),
    heatmap(-log.(pVals[end:-1:1, :]),
            grid = false,
            size = (800, 300)),
    title = ["T Statistics" "- Log(P-values)"]
)
Example block output

White cells represent p-values of 0, not missing. These p-values should be reported as < 1/500 when using 500 permutations here, and more permutations would give more precision about how small these p-values are.

julia> pVals7×10 Matrix{Float64}:
 0.0    0.0    0.0    0.0    0.0    0.0    0.0    0.532  0.018  0.0
 0.674  0.914  0.962  0.88   0.76   1.0    0.316  0.812  0.802  0.0
 0.0    0.038  0.998  0.468  0.0    0.0    0.0    0.9    0.682  0.0
 0.718  0.942  0.982  0.0    0.0    0.0    0.11   0.908  0.946  0.0
 0.866  0.944  0.982  0.848  0.0    0.0    0.0    0.0    0.912  0.0
 0.0    0.0    0.0    0.0    0.964  0.992  0.98   0.988  0.544  0.0
 0.94   0.934  0.0    0.0    0.0    0.0    0.934  0.722  0.072  0.0

Summary

Call the summary on a fitted MatrixLM object to inspect the estimates, standard errors, confidence intervals (CIs), and the associated row and column labels.

The method returns a dataframe with one row per coefficient and the following columns:

  • row_term, col_term: index labels for the row-side (X) and column-side (Z) associated with each coefficient entry (column-major order).
  • coef: coefficient estimate for each X–Z pair.
  • std_error, t_stat, p_value: respectively, standard error, T-statistics, and p-values for testing.
  • ci_lower, ci_upper: two-sided confidence interval bounds at the specified significance level.

The summary method has three keyword arguments:

  • alpha: two-sided significance level for CIs and margins of error (default 0.05, 95% CI).
  • permutation_test: when true, compute permutation-based t-stats/p-values; when false, use t-distribution.
  • nPerms: number of permutations to run when permutation_test is true.

This summary table can be used to identify which row/column covariate combinations are significant and to estimate their effect sizes along with their uncertainty.

MatrixLM.summary(est, alpha = 0.05, permutation_test = false)
70×8 DataFrame
Rowrow_termcol_termcoefstd_errort_statp_valueci_lowerci_upper
StringStringFloat64Float64Float64Float64Float64Float64
1row_1col_12.27430.16027314.19011.34407e-251.960172.58843
2row_2col_1-0.2682230.210208-1.275990.204945-0.6802220.143777
3row_3col_1-1.025310.231353-4.43182.41766e-5-1.47875-0.571867
4row_4col_10.2875630.2064761.392720.166825-0.1171220.692247
5row_5col_1-0.1801210.253377-0.7108830.478828-0.6767310.316488
6row_6col_12.651630.2284811.60553.59752e-202.203823.09945
7row_7col_10.2148280.2436170.8818250.380008-0.2626530.692309
8row_1col_23.167860.14505921.83831.22845e-392.883553.45217
9row_2col_2-0.09518130.190254-0.5002850.617984-0.4680720.27771
10row_3col_2-0.6643640.209392-3.172830.00201114-1.07477-0.253964
11row_4col_2-0.08273780.186876-0.4427410.658919-0.4490090.283533
12row_5col_2-0.09718890.229325-0.4238040.672628-0.5466590.352281
13row_6col_23.198980.20679215.46953.62713e-282.793673.60428
14row_7col_2-0.2134650.220492-0.968130.335338-0.6456220.218692
15row_1col_33.929340.15535725.29235.34961e-453.624854.23384
16row_2col_30.1408470.203760.6912410.491032-0.2585150.54021
17row_3col_3-0.007464340.224257-0.03328480.973514-0.4469990.432071
18row_4col_30.1141240.2001430.5702130.569825-0.2781490.506396
19row_5col_30.135180.2456050.5503940.583289-0.3461980.616557
20row_6col_33.056240.22147213.79968.49696e-252.622163.49032
21row_7col_32.841290.23614512.0324.38372e-212.378463.30413
22row_1col_44.567450.16378827.88631.10319e-484.246434.88847
23row_2col_40.3080170.2148181.433850.154766-0.1130180.729053
24row_3col_40.5226990.2364272.210830.02934950.05931110.986088
25row_4col_42.94540.21100413.9593.99576e-252.531843.35896
26row_5col_40.4484850.2589341.732040.0863804-0.05901630.955986
27row_6col_43.369130.23349114.42934.382e-262.911493.82676
28row_7col_42.592150.2489610.41191.38418e-172.10423.0801
29row_1col_56.070640.16197437.47922.70459e-605.753186.38811
30row_2col_50.3049280.2124381.435370.154333-0.1114430.721299
31row_3col_51.194270.2338085.107921.58355e-60.7360171.65253
32row_4col_53.373760.20866716.16821.56171e-292.964783.78274
33row_5col_52.637850.25606510.30152.40835e-172.135973.13973
34row_6col_5-0.172990.230905-0.7491850.455522-0.6255550.279574
35row_7col_53.097510.24620212.58122.97991e-222.614973.58006
36row_1col_66.908160.17628239.1884.31587e-626.562657.25367
37row_2col_6-0.008149530.231205-0.03524810.971953-0.4613030.445004
38row_3col_62.201570.2544628.651859.44562e-141.702832.70031
39row_4col_66.796230.227129.92612.11965e-516.351127.24133
40row_5col_63.24020.27868611.62673.23949e-202.693983.78641
41row_6col_60.06106150.2513030.242980.808524-0.4314830.553606
42row_7col_63.071830.26795211.46417.25191e-202.546653.597
43row_1col_77.99010.16781647.61224.81962e-707.661198.31901
44row_2col_70.4515470.2201012.051550.04285440.02015710.882937
45row_3col_75.42630.24224122.40041.50967e-404.951525.90108
46row_4col_70.6127930.2161932.834470.005564550.1890621.03652
47row_5col_72.975520.26530211.21562.49314e-192.455543.4955
48row_6col_7-0.05325360.239234-0.2226010.824305-0.5221430.415636
49row_7col_70.2071780.2550830.8121980.418626-0.2927750.707131
50row_1col_80.1387010.1586750.8741230.384167-0.1722960.449699
51row_2col_8-0.08863240.208112-0.4258890.671113-0.4965240.319259
52row_3col_80.05964740.2290460.2604170.795083-0.3892740.508569
53row_4col_80.05080290.2044170.2485260.804242-0.3498470.451453
54row_5col_82.857310.2508511.39051.04504e-192.365653.34897
55row_6col_8-0.01485260.226202-0.06566090.94778-0.4582010.428495
56row_7col_8-0.2741850.241188-1.136810.258363-0.7469050.198536
57row_1col_90.5405390.1781513.034160.003080940.191370.889708
58row_2col_90.1811920.2336550.7754690.439911-0.2767640.639149
59row_3col_90.2279570.2571590.8864420.377527-0.2760660.731979
60row_4col_90.05794440.2295070.2524730.801199-0.3918810.50777
61row_5col_90.09424410.281640.3346260.738615-0.457760.646248
62row_6col_90.4438010.2539661.747480.0836545-0.05396330.941566
63row_7col_90.9461060.2707923.493850.0007135370.4153641.47685
64row_1col_10-1.97130.140361-14.04452.66784e-25-2.2464-1.6962
65row_2col_10-2.118710.184091-11.5095.80325e-20-2.47952-1.7579
66row_3col_10-2.236880.202609-11.04045.9702e-19-2.63398-1.83977
67row_4col_10-2.341550.180823-12.94944.98966e-23-2.69596-1.98715
68row_5col_10-1.873760.221897-8.444272.65697e-13-2.30867-1.43885
69row_6col_10-2.005830.200094-10.02459.67541e-17-2.39801-1.61365
70row_7col_10-1.730580.21335-8.111471.38433e-12-2.14874-1.31242

References

  • 1Ledoit, O., & Wolf, M. (2003). Improved estimation of the covariance matrix of stock returns with an application to portfolio selection. Journal of empirical finance, 10(5), 603-621.