Skip to content
bitanathPublic

About

πŸ”’ A js library for Principal Factor Analysis

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ”’ factor-js

Factor analysis (principal factor analysis) and SVD in pure JavaScript and WebAssembly. Written in pure AssemblyScript a superset of Typescript.

Given a table of observations, factor() reduces correlated variables to a handful of latent factors, tells you how much variance each one explains, and gives you per-row factor scores you can feed into downstream models. svd() exposes the underlying singular value decomposition if you want it directly.

  • Runs in Node and the browser, with ESM, CommonJS and a UMD global build.
  • Ships bundled TypeScript types.
  • Optional WASM backend (AssemblyScript) for browser workloads.
  • Results match the Python references (factor-analyzer, NumPy) and the SVD from pca-js.

Install

npm install factor-js

Quickstart

Node / Bun (ESM)

import { factor, svd } from "factor-js";

Node (CommonJS)

const { factor, svd } = require("factor-js");

Browser (ESM, via jsDelivr)

<script type="module">
  import { factor } from "https://cdn.jsdelivr.net/npm/factor-js@1.0.4/+esm";
</script>

Browser (global)

<script src="https://cdn.jsdelivr.net/npm/factor-js@1.0.4/dist/factor.js"></script>
<script>
  // window.Factor.factor(...), window.Factor.svd(...)
</script>

WASM (Assembly script source)

import { factor } from "factor-js/wasm";

A worked example, with actual results

The repo bundles a real dataset: tests/data.csv, 405 responses Γ— 40 self-report variables (mood, body, senses, cognition, social feelings), each rated on a βˆ’3…3 scale. Rows are observations, columns are variables.

import { readFileSync } from "node:fs";
import { factor } from "factor-js";

const rows = readFileSync("tests/data.csv", "utf8").trim().split("\n");
const names = rows[0].split(",");
const data = rows.slice(1).map((r) => r.split(",").map(Number));

const { loadings, variance, scores } = factor(data);
const nVars = names.length;

// Eigenvalue = variance * number of variables (Kaiser criterion: keep eigenvalue > 1)
console.log("Factor  Explained  Eigenvalue  Cumulative");
let cumulative = 0;
variance.slice(0, 4).forEach((v, i) => {
  cumulative += v;
  console.log(
    `F${i + 1}`.padEnd(7),
    `${(v * 100).toFixed(2)}%`.padStart(9),
    (v * nVars).toFixed(2).padStart(10),
    `${(cumulative * 100).toFixed(2)}%`.padStart(11)
  );
});

// The variables with the strongest loading on each of the first three factors
for (let f = 0; f < 3; f++) {
  const top = names
    .map((name, i) => ({ name, loading: loadings[i][f] }))
    .sort((a, b) => Math.abs(b.loading) - Math.abs(a.loading))
    .slice(0, 6);
  console.log(`\nFactor ${f + 1}:`);
  top.forEach((t) => console.log(`  ${t.name.padEnd(14)} ${t.loading >= 0 ? "+" : ""}${t.loading.toFixed(3)}`));
}

That prints:

Factor  Explained  Eigenvalue  Cumulative
F1         41.99%      16.80      41.99%
F2         13.85%       5.54      55.84%
F3          9.32%       3.73      65.16%
F4          2.72%       1.09      67.88%

Factor 1:
  joy            +0.856
  happy          +0.851
  pleasure       +0.827
  love           +0.822
  angry          +0.811
  nauseated      +0.808

Factor 2:
  computations   +0.829
  recognizing    +0.798
  remembering    +0.722
  reasoning      +0.630
  hungry         -0.610
  communicating  +0.589

Factor 3:
  seeing         +0.638
  temperature    +0.607
  odors          +0.542
  sounds         +0.502
  embarrassed    -0.491
  guilt          -0.410

So you can probably name the factors as feelings or emotions for the first, logic or thinking for the second and sensory or tactile for the last. These factors basically group a hierarchy of needs (Study from Mind Perception Framework/Weisman et al.), commonly studied for organizational behavior/psychology tasks.

Interpreting the return values

Field Shape Meaning
loadings nVars Γ— nFactors Correlation of each variable with each factor
variance nFactors Share of total variance explained (sums to 1)
scores nObs Γ— nFactors Per-observation factor scores

Eigenvalues are variance.map(v => v * nVars). Factor directions are sign-corrected so each factor's loadings sum to a positive value.

JavaScript or WASM?

svd()/factor() do the same math in both builds, and produce bit-identical results (the test suite cross-checks them). Which is faster depends on the engine:

  • JavaScriptCore / browsers: WASM is the better default, especially for the larger, more complex cases.
  • Node / V8: V8's JIT is extremely good at warm numeric loops, and WASM starts to lose on large workloads. Both are fast; pick JS for simplicity.

Median time per call (lower is better; WASM/JS < 1Γ— means WASM wins):

Benchmark - Directional non scientific On Toy data the WASM slowdown can actually be avoided through rolled loops in svd.ts However this leads to a massive slowdown in the non toy samples for WASM, check pca.js if you really want this

Test Size JS WASM WASM/JS
SVD 100Γ—10 1.76ms 0.28ms 0.16Γ—
SVD 1000Γ—20 2.17ms 4.80ms 2.21Γ—
SVD 1000Γ—100 54.43ms 104.44ms 1.92Γ—
Factor (real data) 405Γ—40 4.95ms 8.84ms 1.79Γ—
Factor (random) 1000Γ—40 13.23ms 22.78ms 1.72Γ—

Reproduce with npm run bench. The WASM backend uses the AssemblyScript minimal runtime and flat row-major buffers for the optimization.

API

factor(data: number[][]): FactorResult

Principal factor analysis on a row-major matrix. Columns are standardised internally.

  • data β€” observations Γ— variables, rows >= columns.
  • Returns { loadings, scores, variance } as above.
  • Throws Need more rows than columns if rows < columns.

svd(A: number[][]): { U, S, V }

Singular value decomposition via A β‰ˆ U Β· diag(S) Β· Vα΅€.

  • A β€” rows Γ— columns, rows >= columns.
  • U is rows Γ— columns, S has length columns, V is columns Γ— columns.
  • Throws Need more rows than columns if rows < columns.

factor() standardises its input first, so you do not need to center or scale data yourself. svd() operates on exactly the matrix you give it.

Building and testing

NOTE: TSConfig errors -> You may get tsconfig errors when initially cloning the repo. Post build you may get TSConfig deprecation errors, it is recommended to ignore them. This is because we cross-build from AssemblyScript (not Typescript) and thus have to use portable code. Build should only produce warnings no errors unless something has breaking changes in dev dependencies (unlikely).

npm install
npm run build:all   # asc -> core/*.wasm, tsc + rollup -> dist/*
npm test            # JS-vs-WASM parity across random, edge and real-data cases
npm run bench       # JS-vs-WASM benchmark (run under node or bun)

The tests are generated with AI assistance and cross-check against NumPy and factor-analyzer. The Python side lives in python/:

python -m venv venv && source venv/bin/activate
pip install -r python/requirements.txt
python python/test_sample.py   # random sample
python python/test_data.py     # tests/data.csv

License

GPL-3.0-only. This work derives from pca-js.

About

πŸ”’ A js library for Principal Factor Analysis

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages