MATRIX_NORMAL
Overview
The MATRIX_NORMAL function computes the probability density function (PDF), log-PDF, or draws random samples from a matrix normal distribution (also known as the matrix Gaussian distribution). This distribution is a generalization of the multivariate normal distribution to matrix-valued random variables, making it useful for modeling data that naturally takes the form of matrices, such as repeated multivariate measurements or spatiotemporal data.
The function uses SciPy’s scipy.stats.matrix_normal implementation. A random matrix X of dimensions n \times p follows the matrix normal distribution \mathcal{MN}_{n,p}(M, U, V) with mean matrix M, among-row covariance matrix U (of size n \times n), and among-column covariance matrix V (of size p \times p).
The probability density function is defined as:
p(X \mid M, U, V) = \frac{\exp\left(-\frac{1}{2}\text{tr}\left[V^{-1}(X - M)^T U^{-1}(X - M)\right]\right)}{(2\pi)^{np/2} |V|^{n/2} |U|^{p/2}}
where \text{tr} denotes the matrix trace. The matrix normal distribution is equivalent to the multivariate normal distribution with the relationship \text{vec}(X) \sim \mathcal{N}_{np}(\text{vec}(M), V \otimes U), where \otimes denotes the Kronecker product and \text{vec} is the vectorization operator.
The separate row and column covariance matrices capture dependencies in two dimensions: U models correlations among the rows of X, while V models correlations among the columns. This separable covariance structure reduces the number of parameters compared to a full multivariate normal covariance matrix from (np)^2 to n^2 + p^2.
For further theoretical background, see the Matrix normal distribution article on Wikipedia. The SciPy implementation documentation is available in the SciPy reference.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=MATRIX_NORMAL(x, mean, rowcov, colcov, mn_method, size)
x(list[list], optional, default: null): Matrix at which to evaluate the function or template for sample shape.mean(list[list], optional, default: null): Mean matrix of the distribution. Must match shape of x. Default is zero matrix.rowcov(list[list], optional, default: null): Among-row covariance matrix. Must be square with size equal to number of rows in x. Default is identity matrix.colcov(list[list], optional, default: null): Among-column covariance matrix. Must be square with size equal to number of columns in x. Default is identity matrix.mn_method(str, optional, default: “pdf”): Method to compute. Valid options are pdf, logpdf, or rvs.size(int, optional, default: 1): Number of samples to draw if method is rvs.
Returns (list[list]): 2D list of results, or error message string.
Examples
Example 1: PDF with identity covariances
Inputs:
| x | mean | rowcov | colcov | mn_method | ||||
|---|---|---|---|---|---|---|---|---|
| 1 | 2 | 0 | 0 | 1 | 0 | 1 | 0 | |
| 3 | 4 | 0 | 0 | 0 | 1 | 0 | 1 |
Excel formula:
=MATRIX_NORMAL({1,2;3,4}, {0,0;0,0}, {1,0;0,1}, {1,0;0,1}, "pdf")
Expected output:
| Result |
|---|
| 7.749e-9 |
Example 2: LogPDF with identity covariances
Inputs:
| x | mean | rowcov | colcov | mn_method | ||||
|---|---|---|---|---|---|---|---|---|
| 1 | 2 | 0 | 0 | 1 | 0 | 1 | 0 | logpdf |
| 3 | 4 | 0 | 0 | 0 | 1 | 0 | 1 |
Excel formula:
=MATRIX_NORMAL({1,2;3,4}, {0,0;0,0}, {1,0;0,1}, {1,0;0,1}, "logpdf")
Expected output:
| Result |
|---|
| -18.68 |
Example 3: PDF with default mean (zero matrix)
Inputs:
| x | rowcov | colcov | mn_method | |||
|---|---|---|---|---|---|---|
| 0 | 0 | 1 | 0 | 1 | 0 | |
| 0 | 0 | 0 | 1 | 0 | 1 |
Excel formula:
=MATRIX_NORMAL({0,0;0,0}, {1,0;0,1}, {1,0;0,1}, "pdf")
Expected output:
| Result |
|---|
| 0.02533 |
Example 4: LogPDF with scaled covariance matrices
Inputs:
| x | mean | rowcov | colcov | mn_method | ||||
|---|---|---|---|---|---|---|---|---|
| 0.5 | 0.5 | 0 | 0 | 2 | 0 | 2 | 0 | logpdf |
| 0.5 | 0.5 | 0 | 0 | 0 | 2 | 0 | 2 |
Excel formula:
=MATRIX_NORMAL({0.5,0.5;0.5,0.5}, {0,0;0,0}, {2,0;0,2}, {2,0;0,2}, "logpdf")
Expected output:
| Result |
|---|
| -6.573 |
Python Code
from scipy.stats import matrix_normal as scipy_matrix_normal
def matrix_normal(x=None, mean=None, rowcov=None, colcov=None, mn_method='pdf', size=1):
"""
Computes the PDF, log-PDF, or draws random samples from a matrix normal distribution.
See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.matrix_normal.html
This example function is provided as-is without any representation of accuracy.
Args:
x (list[list], optional): Matrix at which to evaluate the function or template for sample shape. Default is None.
mean (list[list], optional): Mean matrix of the distribution. Must match shape of x. Default is zero matrix. Default is None.
rowcov (list[list], optional): Among-row covariance matrix. Must be square with size equal to number of rows in x. Default is identity matrix. Default is None.
colcov (list[list], optional): Among-column covariance matrix. Must be square with size equal to number of columns in x. Default is identity matrix. Default is None.
mn_method (str, optional): Method to compute. Valid options are pdf, logpdf, or rvs. Valid options: PDF, LogPDF, RVS. Default is 'pdf'.
size (int, optional): Number of samples to draw if method is rvs. Default is 1.
Returns:
list[list]: 2D list of results, or error message string.
"""
def to2d(val):
return [[val]] if not isinstance(val, list) else val
x = to2d(x)
mean = to2d(mean) if mean is not None else None
rowcov = to2d(rowcov) if rowcov is not None else None
colcov = to2d(colcov) if colcov is not None else None
# Validate x
if not isinstance(x, list) or len(x) < 1 or not all(isinstance(row, list) and len(row) > 0 for row in x):
return "Invalid input: x must be a 2D list with at least one row."
try:
x_mat = [[float(val) for val in row] for row in x]
except Exception:
return "Invalid input: x must contain numeric values."
n_rows = len(x_mat)
n_cols = len(x_mat[0])
# Validate mean
if mean is not None:
if not isinstance(mean, list) or len(mean) != n_rows or not all(isinstance(row, list) and len(row) == n_cols for row in mean):
return "Invalid input: mean must be a 2D list with same shape as x."
try:
mean_mat = [[float(val) for val in row] for row in mean]
except Exception:
return "Invalid input: mean must contain numeric values."
else:
mean_mat = [[0.0 for _ in range(n_cols)] for _ in range(n_rows)]
# Validate rowcov
if rowcov is not None:
if not isinstance(rowcov, list) or len(rowcov) != n_rows or not all(isinstance(row, list) and len(row) == n_rows for row in rowcov):
return "Invalid input: rowcov must be a square 2D list with shape (n_rows, n_rows)."
try:
rowcov_mat = [[float(val) for val in row] for row in rowcov]
except Exception:
return "Invalid input: rowcov must contain numeric values."
else:
rowcov_mat = [[float(i == j) for j in range(n_rows)] for i in range(n_rows)]
# Validate colcov
if colcov is not None:
if not isinstance(colcov, list) or len(colcov) != n_cols or not all(isinstance(row, list) and len(row) == n_cols for row in colcov):
return "Invalid input: colcov must be a square 2D list with shape (n_cols, n_cols)."
try:
colcov_mat = [[float(val) for val in row] for row in colcov]
except Exception:
return "Invalid input: colcov must contain numeric values."
else:
colcov_mat = [[float(i == j) for j in range(n_cols)] for i in range(n_cols)]
# Validate mn_method
valid_methods = {'pdf', 'logpdf', 'rvs'}
if mn_method not in valid_methods:
return f"Invalid input: mn_method must be one of {sorted(valid_methods)}."
try:
dist = scipy_matrix_normal(mean=mean_mat, rowcov=rowcov_mat, colcov=colcov_mat)
except Exception as e:
return f"scipy.stats.matrix_normal error: {e}"
if mn_method in ('pdf', 'logpdf'):
try:
if mn_method == 'pdf':
val = dist.pdf(x_mat)
else:
val = dist.logpdf(x_mat)
# Convert numpy types to native float for Excel compatibility
return [[float(val)]]
except Exception as e:
return f"scipy.stats.matrix_normal {mn_method} error: {e}"
elif mn_method == 'rvs':
if size is not None:
try:
size_int = int(size)
if size_int < 1:
return "Invalid input: size must be >= 1."
except Exception:
return "Invalid input: size must be an integer."
else:
size_int = 1
try:
samples = dist.rvs(size=size_int)
# If size == 1, samples is a matrix; if size > 1, samples is a 3D array
if size_int == 1:
# Return as 2D list
return [[float(val) for val in row] for row in samples]
else:
# Return as 2D list of flattened samples
return [ [float(val) for row in sample for val in row] for sample in samples ]
except Exception as e:
return f"scipy.stats.matrix_normal rvs error: {e}"
return "Unknown error."