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)}"

Online Calculator

2D array of numeric input data with rows as samples and columns as features.
Number of latent factors to estimate. Leave blank to keep the estimator default.
Optional rotation applied to the fitted loading matrix.
Singular value decomposition method used during fitting.
Maximum number of fitting iterations.
Integer seed for randomized fitting paths. Leave blank for the estimator default.