One SEMinR, Many Languages
SEMinR is no longer R-only: experimental ports now bring the same toolkit to other languages, starting with Python and JavaScript/TypeScript. Each port mirrors the R API and is validated for numerical parity against the R package using golden fixtures — so you can stay in the language your model analysis lives in.

SEMinR began life as an R package, and R remains its authoritative home. But not everyone lives in R. So we’re excited to share that SEMinR now has two experimental ports that bring the same PLS-SEM and covariance-based SEM toolkit to other ecosystems:
- seminr for Python — install with
pip install seminr. Requires Python 3.11+, with NumPy and SciPy as the only runtime dependencies (optional pandas support for DataFrame input). - seminr for JavaScript & TypeScript — install with
npm install @seminr/core. Zero-dependency and runtime-agnostic: it runs in Bun, Node, Deno, and the browser as plain JavaScript, with first-class TypeScript types included for TS projects.
Both ports cover the full non-plotting SEMinR surface: the specification DSL (composites, reflective/PLSc constructs, higher-order constructs, all three interaction methods, and quadratic terms), estimate_pls with bootstrapping, the assessment suite (reliability, HTMT, Fornell-Larcker, cross-loadings, VIFs, f², AIC/BIC), PLSpredict, PLS-MGA, and covariance-based CFA/CBSEM. The public API deliberately mirrors SEMinR’s familiar names, so moving over is largely mechanical — R’s snake_case becomes camelCase in the JavaScript/TypeScript port, and R’s named arguments become named-argument objects.
The same analysis, across languages
Talk is cheap, so here is a complete PLS-SEM analysis — measurement model, structural model, and estimation — written in each language. Click the tabs below to switch between R, Python, and TypeScript and see for yourself how little changes: the same constructs, the same paths, the same estimator. Only each language’s natural conventions differ — R’s snake_case becomes camelCase in TypeScript, and named arguments become keyword or object arguments.
# Measurement model — how each construct is measured
measurements <- constructs(
reflective("Image", multi_items("IMAG", 1:5)),
composite("Expectation", multi_items("CUEX", 1:3)),
composite("Loyalty", multi_items("CUSL", 1:3), weights = mode_B),
composite("Complaints", single_item("CUSCO"))
)
# Structural model — how the constructs relate
structure <- relationships(
paths(from = c("Image", "Expectation"), to = c("Complaints", "Loyalty"))
)
# Estimate with PLS, then inspect the results
model <- estimate_pls(data = mobi, measurements, structure)
summary(model)from seminr import (
constructs, reflective, composite, multi_items, single_item, mode_B,
relationships, paths, estimate_pls, summarize,
)
from seminr.datasets import load_mobi
mobi = load_mobi()
# Measurement model — how each construct is measured
measurements = constructs(
reflective("Image", multi_items("IMAG", [1, 2, 3, 4, 5])),
composite("Expectation", multi_items("CUEX", [1, 2, 3])),
composite("Loyalty", multi_items("CUSL", [1, 2, 3]), weights=mode_B),
composite("Complaints", single_item("CUSCO")),
)
# Structural model — how the constructs relate
structure = relationships(
paths(["Image", "Expectation"], ["Complaints", "Loyalty"]),
)
# Estimate with PLS, then inspect the results
model = estimate_pls(mobi, measurements, structure)
summarize(model)import {
parseCsv, constructs, reflective, composite, multiItems, singleItem, modeB,
relationships, paths, estimatePls, summarize,
} from "@seminr/core";
// Load your data — any source of CSV text works
const mobi = parseCsv(await Bun.file("mobi.csv").text());
// Measurement model — how each construct is measured
const measurements = constructs(
reflective("Image", multiItems("IMAG", [1, 2, 3, 4, 5])),
composite("Expectation", multiItems("CUEX", [1, 2, 3])),
composite("Loyalty", multiItems("CUSL", [1, 2, 3]), modeB),
composite("Complaints", singleItem("CUSCO")),
);
// Structural model — how the constructs relate
const structure = relationships(
paths(["Image", "Expectation"], ["Complaints", "Loyalty"]),
);
// Estimate with PLS, then inspect the results
const model = estimatePls(mobi, measurements, structure);
summarize(model);How we know the ports agree: golden fixtures
A port is only useful if you can trust its numbers. Rewriting an estimator in a new language is exactly the kind of work where subtle discrepancies creep in — a different matrix library, a different convergence criterion, a different way of handling an edge case. So rather than judge the ports by eye, we hold them to the R package as ground truth using golden fixtures.
The idea is simple. A script runs the R SEMinR package on the bundled mobi / ECSI dataset and serializes the results — path coefficients, weights, loadings, reliability tables, HTMT, f², predictions, and more — into JSON files. Those files are committed into each port’s repository as the test contract. The port’s own test suite then re-runs the same models and asserts, cell by cell, that its output matches the R-generated fixtures.
A few details that make this a strong guarantee:
- R is the single source of truth. Fixtures are generated from the R implementation (
Rscript scripts/generate-fixtures.R), not written by hand. When SEMinR’s behavior is the reference, there’s no room to “define correct” as whatever the port happens to produce. - Tight, quantity-appropriate tolerances. PLS quantities must match R to within
1e-5. Covariance-based estimation — which the ports reimplement from scratch to match lavaan’s ML/MLR estimator, rather than calling lavaan — uses tiered tolerances appropriate to each fit measure. - Bootstrapping is checked deterministically. Random resampling can’t be compared naively across languages, so the fixtures include R’s exact resample index matrix. The ports feed those same indices in, which lets us verify the bootstrap machinery itself rather than the RNG.
- Parity is the acceptance bar for every feature. A capability isn’t considered “done” in a port until it matches the fixtures — the same standard applies to the Python and TypeScript versions alike, and the two ports even share fixtures and algorithm digests with each other.
A word of caution
These ports are experimental. The R package remains the authoritative implementation, and we recommend validating results against it before relying on a port for published work. seminr-ts in particular is still pre-1.0, so its API may shift between minor versions.
That said, the fixtures give us — and you — a concrete, continuously-checked basis for trusting the numbers. If you already use SEMinR to estimate your models, you can now stay in Python or JavaScript/TypeScript for the full modern toolkit of estimation, assessment, prediction, and multi-group analysis.
Try them out, and please tell us how it goes:
- Python — https://github.com/sem-in-r/seminr-py (PyPI)
- JavaScript / TypeScript — https://github.com/sem-in-r/seminr-ts (npm)
Bug reports and feature requests on either repository are the fastest way to help us mature these ports.