FACTOR_MODEL
Factor analysis models observed features as linear combinations of a smaller set of latent factors plus feature-specific noise. It is useful when the goal is to explain shared covariance structure rather than simply maximize retained variance.
The model takes the form:
X = F W + \mu + \epsilon
where X is the observed data, F are the latent factors, W is the loading matrix, \mu is the feature mean, and \epsilon is the feature-specific noise.
This wrapper accepts rows as samples and columns as features. It returns transformed factor scores together with factor loadings, estimated noise variances, per-feature means, and the fitted log-likelihood path.
Excel Usage
=FACTOR_MODEL(data, n_components, factor_rotation, fa_svd_method, max_iter, random_state)
data(list[list], required): 2D array of numeric input data with rows as samples and columns as features.n_components(int, optional, default: null): Number of latent factors to estimate. Leave blank to keep the estimator default.factor_rotation(str, optional, default: “none”): Optional rotation applied to the fitted loading matrix.fa_svd_method(str, optional, default: “randomized”): Singular value decomposition method used during fitting.max_iter(int, optional, default: 1000): Maximum number of fitting iterations.random_state(int, optional, default: null): Integer seed for randomized fitting paths. Leave blank for the estimator default.
Returns (dict): Excel data type containing latent scores, loadings, noise variances, and log-likelihood summaries.
Example 1: Fit an unrotated two-factor model with randomized SVD
Inputs:
| data | n_components | factor_rotation | fa_svd_method | max_iter | random_state | |||
|---|---|---|---|---|---|---|---|---|
| 2 | 1 | 1 | 0.8 | 2 | none | randomized | 600 | 0 |
| 2.2 | 1.1 | 1.2 | 0.9 | |||||
| 1.8 | 0.9 | 0.8 | 0.7 | |||||
| 5 | 4.2 | 4.1 | 3.9 | |||||
| 5.1 | 4.1 | 4.2 | 4 | |||||
| 4.8 | 4 | 3.9 | 3.8 | |||||
| 3 | 2.4 | 2.2 | 2 | |||||
| 3.1 | 2.5 | 2.3 | 2.1 |
Excel formula:
=FACTOR_MODEL({2,1,1,0.8;2.2,1.1,1.2,0.9;1.8,0.9,0.8,0.7;5,4.2,4.1,3.9;5.1,4.1,4.2,4;4.8,4,3.9,3.8;3,2.4,2.2,2;3.1,2.5,2.3,2.1}, 2, "none", "randomized", 600, 0)
Expected output:
{"type":"Double","basicValue":19.9171,"properties":{"final_loglike":{"type":"Double","basicValue":19.9171},"component_count":{"type":"Double","basicValue":2},"sample_count":{"type":"Double","basicValue":8},"feature_count":{"type":"Double","basicValue":4},"scores":{"type":"Array","elements":[[{"type":"Double","basicValue":-1.08516},{"type":"Double","basicValue":-0.640792}],[{"type":"Double","basicValue":-0.959455},{"type":"Double","basicValue":-1.26449}],[{"type":"Double","basicValue":-1.21086},{"type":"Double","basicValue":-0.0170943}],[{"type":"Double","basicValue":1.24179},{"type":"Double","basicValue":0.00352144}],[{"type":"Double","basicValue":1.27562},{"type":"Double","basicValue":-1.14684}],[{"type":"Double","basicValue":1.09515},{"type":"Double","basicValue":0.060673}],[{"type":"Double","basicValue":-0.216393},{"type":"Double","basicValue":1.51114}],[{"type":"Double","basicValue":-0.14069},{"type":"Double","basicValue":1.49388}]]},"components":{"type":"Array","elements":[[{"type":"Double","basicValue":1.30317},{"type":"Double","basicValue":1.34059},{"type":"Double","basicValue":1.3394},{"type":"Double","basicValue":1.34994}],[{"type":"Double","basicValue":-0.0587273},{"type":"Double","basicValue":0.108721},{"type":"Double","basicValue":0.0028651},{"type":"Double","basicValue":0.019216}]]},"noise_variance":{"type":"Array","elements":[[{"type":"Double","basicValue":0.000186822}],[{"type":"Double","basicValue":0.000374229}],[{"type":"Double","basicValue":0.000848299}],[{"type":"Double","basicValue":0.0016567}]]},"feature_means":{"type":"Array","elements":[[{"type":"Double","basicValue":3.375}],[{"type":"Double","basicValue":2.525}],[{"type":"Double","basicValue":2.4625}],[{"type":"Double","basicValue":2.275}]]},"loglike":{"type":"Array","elements":[[{"type":"Double","basicValue":-28.3465}],[{"type":"Double","basicValue":-17.406}],[{"type":"Double","basicValue":-6.77301}],[{"type":"Double","basicValue":2.72231}],[{"type":"Double","basicValue":9.52349}],[{"type":"Double","basicValue":13.2404}],[{"type":"Double","basicValue":15.4178}],[{"type":"Double","basicValue":16.7325}],[{"type":"Double","basicValue":17.5815}],[{"type":"Double","basicValue":18.1711}],[{"type":"Double","basicValue":18.5907}],[{"type":"Double","basicValue":18.8887}],[{"type":"Double","basicValue":19.1004}],[{"type":"Double","basicValue":19.2534}],[{"type":"Double","basicValue":19.3678}],[{"type":"Double","basicValue":19.4565}],[{"type":"Double","basicValue":19.5275}],[{"type":"Double","basicValue":19.5856}],[{"type":"Double","basicValue":19.6341}],[{"type":"Double","basicValue":19.6751}],[{"type":"Double","basicValue":19.7101}],[{"type":"Double","basicValue":19.7404}],[{"type":"Double","basicValue":19.7668}],[{"type":"Double","basicValue":19.79}],[{"type":"Double","basicValue":19.8106}],[{"type":"Double","basicValue":19.8289}],[{"type":"Double","basicValue":19.8454}],[{"type":"Double","basicValue":19.8603}],[{"type":"Double","basicValue":19.8737}],[{"type":"Double","basicValue":19.886}],[{"type":"Double","basicValue":19.8972}],[{"type":"Double","basicValue":19.9076}],[{"type":"Double","basicValue":19.9171}]]},"n_iter":{"type":"Double","basicValue":33}}}
Example 2: Apply varimax rotation with the LAPACK SVD path
Inputs:
| data | n_components | factor_rotation | fa_svd_method | max_iter | random_state | |||
|---|---|---|---|---|---|---|---|---|
| 2 | 1 | 1 | 0.8 | 2 | varimax | lapack | 600 | 0 |
| 2.2 | 1.1 | 1.2 | 0.9 | |||||
| 1.8 | 0.9 | 0.8 | 0.7 | |||||
| 5 | 4.2 | 4.1 | 3.9 | |||||
| 5.1 | 4.1 | 4.2 | 4 | |||||
| 4.8 | 4 | 3.9 | 3.8 | |||||
| 3 | 2.4 | 2.2 | 2 | |||||
| 3.1 | 2.5 | 2.3 | 2.1 |
Excel formula:
=FACTOR_MODEL({2,1,1,0.8;2.2,1.1,1.2,0.9;1.8,0.9,0.8,0.7;5,4.2,4.1,3.9;5.1,4.1,4.2,4;4.8,4,3.9,3.8;3,2.4,2.2,2;3.1,2.5,2.3,2.1}, 2, "varimax", "lapack", 600, 0)
Expected output:
{"type":"Double","basicValue":19.9171,"properties":{"final_loglike":{"type":"Double","basicValue":19.9171},"component_count":{"type":"Double","basicValue":2},"sample_count":{"type":"Double","basicValue":8},"feature_count":{"type":"Double","basicValue":4},"scores":{"type":"Array","elements":[[{"type":"Double","basicValue":-1.24431},{"type":"Double","basicValue":-0.199699}],[{"type":"Double","basicValue":-1.54579},{"type":"Double","basicValue":0.360577}],[{"type":"Double","basicValue":-0.942826},{"type":"Double","basicValue":-0.759974}],[{"type":"Double","basicValue":0.957964},{"type":"Double","basicValue":0.790168}],[{"type":"Double","basicValue":0.249498},{"type":"Double","basicValue":1.69711}],[{"type":"Double","basicValue":0.881595},{"type":"Double","basicValue":0.652552}],[{"type":"Double","basicValue":0.798316},{"type":"Double","basicValue":-1.30118}],[{"type":"Double","basicValue":0.845554},{"type":"Double","basicValue":-1.23956}]]},"components":{"type":"Array","elements":[[{"type":"Double","basicValue":0.965454},{"type":"Double","basicValue":1.10117},{"type":"Double","basicValue":1.03266},{"type":"Double","basicValue":1.05122}],[{"type":"Double","basicValue":0.877264},{"type":"Double","basicValue":0.772289},{"type":"Double","basicValue":0.852994},{"type":"Double","basicValue":0.847144}]]},"noise_variance":{"type":"Array","elements":[[{"type":"Double","basicValue":0.000186822}],[{"type":"Double","basicValue":0.000374229}],[{"type":"Double","basicValue":0.000848299}],[{"type":"Double","basicValue":0.0016567}]]},"feature_means":{"type":"Array","elements":[[{"type":"Double","basicValue":3.375}],[{"type":"Double","basicValue":2.525}],[{"type":"Double","basicValue":2.4625}],[{"type":"Double","basicValue":2.275}]]},"loglike":{"type":"Array","elements":[[{"type":"Double","basicValue":-28.3465}],[{"type":"Double","basicValue":-17.406}],[{"type":"Double","basicValue":-6.77301}],[{"type":"Double","basicValue":2.72231}],[{"type":"Double","basicValue":9.52349}],[{"type":"Double","basicValue":13.2404}],[{"type":"Double","basicValue":15.4178}],[{"type":"Double","basicValue":16.7325}],[{"type":"Double","basicValue":17.5815}],[{"type":"Double","basicValue":18.1711}],[{"type":"Double","basicValue":18.5907}],[{"type":"Double","basicValue":18.8887}],[{"type":"Double","basicValue":19.1004}],[{"type":"Double","basicValue":19.2534}],[{"type":"Double","basicValue":19.3678}],[{"type":"Double","basicValue":19.4565}],[{"type":"Double","basicValue":19.5275}],[{"type":"Double","basicValue":19.5856}],[{"type":"Double","basicValue":19.6341}],[{"type":"Double","basicValue":19.6751}],[{"type":"Double","basicValue":19.7101}],[{"type":"Double","basicValue":19.7404}],[{"type":"Double","basicValue":19.7668}],[{"type":"Double","basicValue":19.79}],[{"type":"Double","basicValue":19.8106}],[{"type":"Double","basicValue":19.8289}],[{"type":"Double","basicValue":19.8454}],[{"type":"Double","basicValue":19.8603}],[{"type":"Double","basicValue":19.8737}],[{"type":"Double","basicValue":19.886}],[{"type":"Double","basicValue":19.8972}],[{"type":"Double","basicValue":19.9076}],[{"type":"Double","basicValue":19.9171}]]},"n_iter":{"type":"Double","basicValue":33}}}
Example 3: Fit a single latent factor across three observed features
Inputs:
| data | n_components | factor_rotation | fa_svd_method | max_iter | random_state | ||
|---|---|---|---|---|---|---|---|
| 1 | 1.1 | 0.9 | 1 | none | lapack | 600 | 0 |
| 2 | 2.1 | 1.8 | |||||
| 3 | 3.2 | 2.9 | |||||
| 4 | 4.1 | 4 | |||||
| 5 | 5.2 | 5.1 | |||||
| 6 | 6.1 | 5.9 |
Excel formula:
=FACTOR_MODEL({1,1.1,0.9;2,2.1,1.8;3,3.2,2.9;4,4.1,4;5,5.2,5.1;6,6.1,5.9}, 1, "none", "lapack", 600, 0)
Expected output:
{"type":"Double","basicValue":5.52431,"properties":{"final_loglike":{"type":"Double","basicValue":5.52431},"component_count":{"type":"Double","basicValue":1},"sample_count":{"type":"Double","basicValue":6},"feature_count":{"type":"Double","basicValue":3},"scores":{"type":"Array","elements":[[{"type":"Double","basicValue":-1.46771}],[{"type":"Double","basicValue":-0.89173}],[{"type":"Double","basicValue":-0.270316}],[{"type":"Double","basicValue":0.28352}],[{"type":"Double","basicValue":0.904934}],[{"type":"Double","basicValue":1.44131}]]},"components":{"type":"Array","elements":[[{"type":"Double","basicValue":1.70741},{"type":"Double","basicValue":1.71799},{"type":"Double","basicValue":1.75716}]]},"noise_variance":{"type":"Array","elements":[[{"type":"Double","basicValue":0.00139972}],[{"type":"Double","basicValue":0.000774029}],[{"type":"Double","basicValue":0.00459521}]]},"feature_means":{"type":"Array","elements":[[{"type":"Double","basicValue":3.5}],[{"type":"Double","basicValue":3.63333}],[{"type":"Double","basicValue":3.43333}]]},"loglike":{"type":"Array","elements":[[{"type":"Double","basicValue":-26.1316}],[{"type":"Double","basicValue":-19.5905}],[{"type":"Double","basicValue":-13.1569}],[{"type":"Double","basicValue":-7.0199}],[{"type":"Double","basicValue":-1.64193}],[{"type":"Double","basicValue":2.2234}],[{"type":"Double","basicValue":4.21805}],[{"type":"Double","basicValue":5.00012}],[{"type":"Double","basicValue":5.30885}],[{"type":"Double","basicValue":5.43552}],[{"type":"Double","basicValue":5.48545}],[{"type":"Double","basicValue":5.50637}],[{"type":"Double","basicValue":5.51722}],[{"type":"Double","basicValue":5.52431}]]},"n_iter":{"type":"Double","basicValue":14}}}
Example 4: Apply quartimax rotation on a four-feature matrix
Inputs:
| data | n_components | factor_rotation | fa_svd_method | max_iter | random_state | |||
|---|---|---|---|---|---|---|---|---|
| 1 | 2 | 0.9 | 1.1 | 2 | quartimax | randomized | 800 | 5 |
| 1.2 | 2.1 | 1 | 1.2 | |||||
| 4.8 | 5 | 4.9 | 5.1 | |||||
| 5 | 5.2 | 5.1 | 5.2 | |||||
| 2.5 | 3 | 2.4 | 2.8 | |||||
| 2.7 | 3.2 | 2.6 | 3 | |||||
| 6 | 1 | 5.9 | 1.1 | |||||
| 6.2 | 1.1 | 6 | 1.2 |
Excel formula:
=FACTOR_MODEL({1,2,0.9,1.1;1.2,2.1,1,1.2;4.8,5,4.9,5.1;5,5.2,5.1,5.2;2.5,3,2.4,2.8;2.7,3.2,2.6,3;6,1,5.9,1.1;6.2,1.1,6,1.2}, 2, "quartimax", "randomized", 800, 5)
Expected output:
{"type":"Double","basicValue":-13.288,"properties":{"final_loglike":{"type":"Double","basicValue":-13.288},"component_count":{"type":"Double","basicValue":2},"sample_count":{"type":"Double","basicValue":8},"feature_count":{"type":"Double","basicValue":4},"scores":{"type":"Array","elements":[[{"type":"Double","basicValue":-1.34344},{"type":"Double","basicValue":-0.576798}],[{"type":"Double","basicValue":-1.26736},{"type":"Double","basicValue":-0.529243}],[{"type":"Double","basicValue":0.550676},{"type":"Double","basicValue":1.44898}],[{"type":"Double","basicValue":0.648064},{"type":"Double","basicValue":1.5441}],[{"type":"Double","basicValue":-0.611582},{"type":"Double","basicValue":0.173514}],[{"type":"Double","basicValue":-0.515077},{"type":"Double","basicValue":0.293415}],[{"type":"Double","basicValue":1.23132},{"type":"Double","basicValue":-1.20076}],[{"type":"Double","basicValue":1.3074},{"type":"Double","basicValue":-1.1532}]]},"components":{"type":"Array","elements":[[{"type":"Double","basicValue":1.94917},{"type":"Double","basicValue":-0.00481887},{"type":"Double","basicValue":1.97864},{"type":"Double","basicValue":0.355679}],[{"type":"Double","basicValue":0.0419101},{"type":"Double","basicValue":1.49992},{"type":"Double","basicValue":0.137768},{"type":"Double","basicValue":1.60137}]]},"noise_variance":{"type":"Array","elements":[[{"type":"Double","basicValue":0.000849877}],[{"type":"Double","basicValue":0.00733668}],[{"type":"Double","basicValue":0.000992214}],[{"type":"Double","basicValue":0.0124078}]]},"feature_means":{"type":"Array","elements":[[{"type":"Double","basicValue":3.675}],[{"type":"Double","basicValue":2.825}],[{"type":"Double","basicValue":3.6}],[{"type":"Double","basicValue":2.5875}]]},"loglike":{"type":"Array","elements":[[{"type":"Double","basicValue":-51.9082}],[{"type":"Double","basicValue":-46.4325}],[{"type":"Double","basicValue":-41.0478}],[{"type":"Double","basicValue":-35.8109}],[{"type":"Double","basicValue":-30.8392}],[{"type":"Double","basicValue":-26.3029}],[{"type":"Double","basicValue":-22.3914}],[{"type":"Double","basicValue":-19.231}],[{"type":"Double","basicValue":-16.8385}],[{"type":"Double","basicValue":-15.1717}],[{"type":"Double","basicValue":-14.1574}],[{"type":"Double","basicValue":-13.6451}],[{"type":"Double","basicValue":-13.4311}],[{"type":"Double","basicValue":-13.3518}],[{"type":"Double","basicValue":-13.3215}],[{"type":"Double","basicValue":-13.3067}],[{"type":"Double","basicValue":-13.2966}],[{"type":"Double","basicValue":-13.288}]]},"n_iter":{"type":"Double","basicValue":18}}}
Python Code
import numpy as np
from sklearn.decomposition import FactorAnalysis as SklearnFactorAnalysis
def factor_model(data, n_components=None, factor_rotation='none', fa_svd_method='randomized', max_iter=1000, random_state=None):
"""
Fit factor analysis and return latent scores with loadings and likelihood summaries.
See: https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.FactorAnalysis.html
This example function is provided as-is without any representation of accuracy.
Args:
data (list[list]): 2D array of numeric input data with rows as samples and columns as features.
n_components (int, optional): Number of latent factors to estimate. Leave blank to keep the estimator default. Default is None.
factor_rotation (str, optional): Optional rotation applied to the fitted loading matrix. Valid options: None, Varimax, Quartimax. Default is 'none'.
fa_svd_method (str, optional): Singular value decomposition method used during fitting. Valid options: Randomized, Lapack. Default is 'randomized'.
max_iter (int, optional): Maximum number of fitting iterations. Default is 1000.
random_state (int, optional): Integer seed for randomized fitting paths. Leave blank for the estimator default. Default is None.
Returns:
dict: Excel data type containing latent scores, loadings, noise variances, and log-likelihood summaries.
"""
def py(value):
return value.item() if isinstance(value, np.generic) else value
def cell(value):
value = py(value)
if isinstance(value, bool):
return {"type": "Boolean", "basicValue": bool(value)}
if isinstance(value, (int, float)) and not isinstance(value, bool):
return {"type": "Double", "basicValue": float(value)}
return {"type": "String", "basicValue": str(value)}
def col(values):
return [[cell(value)] for value in values]
def mat(values):
return [[cell(value) for value in row] for row in values]
def parse_data(value):
value = [[value]] if not isinstance(value, list) else value
if not isinstance(value, list) or not value or not all(isinstance(row, list) and row for row in value):
return None, "Error: data must be a non-empty 2D list"
if len({len(row) for row in value}) != 1:
return None, "Error: data must be a rectangular 2D list"
data_np = np.array(value, dtype=float)
if data_np.ndim != 2 or data_np.size == 0:
return None, "Error: data must be a non-empty 2D list"
if not np.isfinite(data_np).all():
return None, "Error: data must contain only finite numeric values"
if data_np.shape[0] < 2:
return None, "Error: data must contain at least 2 samples"
return data_np, None
def orient_projection(scores, components):
score_np = np.array(scores, dtype=float, copy=True)
component_np = np.array(components, dtype=float, copy=True)
limit = min(score_np.shape[1], component_np.shape[0])
for index in range(limit):
component_row = component_np[index, :]
pivot = int(np.argmax(np.abs(component_row)))
pivot_value = component_row[pivot]
if pivot_value == 0 and score_np.shape[0] > 0:
score_column = score_np[:, index]
pivot_value = score_column[int(np.argmax(np.abs(score_column)))]
if pivot_value < 0:
component_np[index, :] *= -1.0
score_np[:, index] *= -1.0
return score_np, component_np
try:
data_np, error = parse_data(data)
if error:
return error
component_total = None if n_components in (None, "") else int(n_components)
if component_total is not None and (component_total < 1 or component_total > data_np.shape[1]):
return f"Error: n_components must be between 1 and {data_np.shape[1]}"
rotation_value = str(factor_rotation).strip().lower()
if rotation_value not in {"none", "varimax", "quartimax"}:
return "Error: factor_rotation must be 'none', 'varimax', or 'quartimax'"
svd_method_value = str(fa_svd_method).strip().lower()
if svd_method_value not in {"randomized", "lapack"}:
return "Error: fa_svd_method must be 'randomized' or 'lapack'"
if int(max_iter) < 1:
return "Error: max_iter must be at least 1"
fitted = SklearnFactorAnalysis(
n_components=component_total,
rotation=None if rotation_value == "none" else rotation_value,
svd_method=svd_method_value,
max_iter=int(max_iter),
random_state=None if random_state in (None, "") else int(random_state)
)
scores_np = np.asarray(fitted.fit_transform(data_np), dtype=float)
components_np = np.asarray(fitted.components_, dtype=float)
scores_np, components_np = orient_projection(scores_np, components_np)
noise_variance = np.atleast_1d(np.asarray(fitted.noise_variance_, dtype=float))
feature_means = np.atleast_1d(np.asarray(fitted.mean_, dtype=float))
loglike = np.atleast_1d(np.asarray(fitted.loglike_, dtype=float))
final_loglike = float(loglike[-1])
return {
"type": "Double",
"basicValue": final_loglike,
"properties": {
"final_loglike": {"type": "Double", "basicValue": final_loglike},
"component_count": {"type": "Double", "basicValue": float(components_np.shape[0])},
"sample_count": {"type": "Double", "basicValue": float(data_np.shape[0])},
"feature_count": {"type": "Double", "basicValue": float(data_np.shape[1])},
"scores": {"type": "Array", "elements": mat(scores_np.tolist())},
"components": {"type": "Array", "elements": mat(components_np.tolist())},
"noise_variance": {"type": "Array", "elements": col(noise_variance.tolist())},
"feature_means": {"type": "Array", "elements": col(feature_means.tolist())},
"loglike": {"type": "Array", "elements": col(loglike.tolist())},
"n_iter": {"type": "Double", "basicValue": float(fitted.n_iter_)}
}
}
except Exception as e:
return f"Error: {str(e)}"