Multivariate Analysis¶
Principal component analysis and maximum likelihood factor analysis with varimax and promax rotations.
Multivariate analysis module.
Provides principal component analysis and factor analysis, matching R’s implementations.
- Public API:
pca(X) - Principal Component Analysis (matches prcomp) factor_analysis(X) - Maximum Likelihood Factor Analysis (matches factanal)
- pystatistics.multivariate.pca(X, *, center=True, scale=False, n_components=None, names=None, backend=None, solver=None, force=False, device_resident=False, oversample=10, n_iter=4, seed=0)[source]¶
Principal Component Analysis via SVD.
Matches R’s
stats::prcomp().- Algorithm:
Center X (subtract column means).
Optionally scale (divide by column standard deviations).
Thin SVD: X_centered = U @ diag(S) @ V’.
sdev = S / sqrt(n - 1).
rotation = V (right singular vectors = loadings).
x = U @ diag(S) (scores = X_centered @ V).
- Parameters:
X (_Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str]) – Data matrix (n x p), n observations, p variables.
center (bool) – Whether to center columns (subtract mean). Default True.
scale (bool) – Whether to scale columns (divide by SD). Default False. Equivalent to R’s
prcomp(scale. = TRUE).n_components (int | None) – Number of components to retain. Default: min(n, p).
names (list[str] | None) – Variable names for the p columns.
backend (str | None) –
Compute backend. The default depends on the input type:
numpy array / CPU DataSource →
'cpu'(R-reference path; matches Rprcomp()to rtol = 1e-10). This is the regulated-industry default: unspecified means the validated-against-R path.torch.Tensor on GPU (e.g.
ds.to('cuda')['X']) →'gpu'. Creating a GPU DataSource is already an opt-in to the GPU path, so we don’t demand the user repeat the choice with an explicitbackend='gpu'.
- Explicit values:
'cpu': force the numpy reference path (float64). Raises if given a GPU tensor (no silent device migration — Rule 1).'gpu': force GPU, float32. Raises if no GPU is available. Results match the CPU path at theGPU_FP32tolerance tier (rtol ≈ 1e-4) — statistically equivalent, not bitwise-equivalent.'gpu_fp64': force GPU, float64. CUDA only (MPS lacks float64; raises on Apple Silicon). CPU-matching precision at a performance cost — consumer NVIDIA parts run FP64 at ~1/64× FP32, so this is usually slower than CPU LAPACK.'auto': prefer GPU (float32) when CUDA is available, else CPU.
solver (str | None) –
Numerical algorithm to use. Only matters when actually running on GPU (the CPU path always uses SVD). The default (
None) resolves by device:'svd'on CUDA, and'randomized'on Apple Silicon (MPS), where it is the only genuinely on-device path.'svd': SVD of X. Always safe. Moderate (~3–4×) speedupover CPU LAPACK. CUDA only — on MPS
torch.linalg.svdsilently falls back to the CPU, so an explicitsolver='svd'on Apple Silicon raises (no silent CPU substitution).'gram': Eigendecomposition of X’X — turns PCA into abig GEMM + small symmetric eigendecomp, both GPU sweet spots. For tall-skinny well-conditioned data (n ≫ p) this is typically 30–100×+ faster than the SVD path. Squares the condition number, so raises
NumericalErroron ill-conditioned data unlessforce=True. Precision gates: cond(X) ≲ 1e6 for FP64, cond(X) ≲ 1e3 for FP32. CUDA only — on MPStorch.linalg.eighhas no Metal kernel, so an explicitsolver='gram'on Apple Silicon raises.'randomized': Halko–Martinsson–Tropp randomized truncatedSVD with CholeskyQR2 orthonormalization. Every X-sized operation runs on-device via
matmul+cholesky(only a tiny l×l eigendecomposition touches the host), so this is the path that makes PCA run — and beat the CPU 3–8× — on Apple Silicon. Device-agnostic, also selectable on CUDA. Approximate (top-k only) and seeded; seeoversample/n_iter/seed. Squares the condition number inside CholeskyQR, so it gates on cond(X) ≲ 1e3 (FP32) just like the Gram path;force=Truebypasses.'auto': On CUDA, uses'gram'when n > 2p AND thecondition check passes; falls back to
'svd'otherwise. On MPS, routes to'randomized'.
force (bool) – Bypass the condition-number gate (Gram and randomized paths). Numerical results will be unreliable on truly ill-conditioned inputs — use only when you understand the data is well-conditioned despite the automated estimator disagreeing.
oversample (int) – Randomized solver only — extra sketch dimensions drawn beyond
n_componentsfor accuracy (default 10).n_iter (int) – Randomized solver only — number of power iterations (default 4). Higher is more accurate on slowly-decaying spectra at proportional cost.
seed (int) – Randomized solver only — RNG seed for the Gaussian sketch. The default is fixed, so a given input is reproducible; pass a different value to draw an independent sketch. GPU RNG is not bit-identical to the CPU RNG (statistically equivalent, not bitwise-equal).
device_resident (bool) – When
Trueand the GPU backend runs, the returnedPCASolutionholds its numeric fields (sdev,rotation,center,scale,x) astorch.Tensorinstances on the fit’s device rather than materialising them as numpy arrays. This saves the ~150 ms D2H copy of the scores matrix on 1M × 100 FP32 data, which otherwise dominates any multi-step GPU pipeline that consumes PCA output. Callresult.to_numpy()orresult.to('cpu')to materialise a numpy-backed copy. Ignored on the CPU path (result is always numpy-backed there). DefaultFalsepreserves 1.8.0 behavior.
- Returns:
PCASolution with sdev, rotation (loadings), scores, etc.
- Raises:
ValidationError – If inputs are invalid.
- Return type:
Validates against: R
stats::prcomp().
- pystatistics.multivariate.factor_analysis(X, *, n_factors, rotation='varimax', method='ml', names=None, lower=0.005, tol=1e-08, max_iter=1000)[source]¶
Maximum likelihood factor analysis.
Matches R’s
stats::factanal().The model is: Sigma = Lambda Lambda’ + Psi, where Lambda is the loadings matrix (p x m) and Psi = diag(psi_1, …, psi_p) is the uniqueness matrix.
- Algorithm:
Minimise the ML objective over uniquenesses psi using L-BFGS-B, parameterised in log(psi) to ensure psi > 0.
Extract loadings from the eigendecomposition of Psi^{-1/2} S Psi^{-1/2}.
Apply rotation (varimax, promax, or none).
Compute chi-squared goodness-of-fit test.
- Parameters:
X (_Buffer | _SupportsArray[dtype[Any]] | _NestedSequence[_SupportsArray[dtype[Any]]] | complex | bytes | str | _NestedSequence[complex | bytes | str]) – Data matrix (n x p).
n_factors (int) – Number of factors to extract.
rotation (str) – ‘varimax’ (orthogonal), ‘promax’ (oblique), or ‘none’.
method (str) – ‘ml’ (only ML supported).
lower (float) – Lower bound on the uniquenesses, enforced during the optimisation. Matches R’s
factanal(lower=)(default 0.005). Guards against degenerate Heywood solutions where a uniqueness collapses to ~0. Must satisfy0 < lower < 1.tol (float) – Convergence tolerance.
max_iter (int) – Maximum iterations.
- Returns:
FactorSolution with loadings, uniquenesses, test statistics, etc.
- Raises:
ValidationError – If inputs are invalid.
ConvergenceError – If the optimisation does not converge.
- Return type:
Validates against: R
stats::factanal().
- class pystatistics.multivariate.PCASolution(_result)[source]¶
Bases:
SolutionReprMixinResult from principal component analysis.
Matches the structure of R’s
prcomp()return value. Wraps aResult[PCAParams]envelope; every datum is exposed via a read-only@propertyso the public attribute surface is unchanged from the previous flat dataclass.Device residency¶
The numeric fields (
sdev,rotation,center,scale,x) may beNDArrayortorch.Tensor. When the GPU backend is invoked with a device-resident input tensor anddevice_resident=Trueis passed topca(), the result’s fields stay on the original device – thexscores matrix especially, which is typically the largest payload. Downstream computations can chain straight offresult.xwithout paying the host<->device round-trip that otherwise dominates multi-step GPU pipelines. Callto_numpy()(orto()with a CPU device) to materialise a numpy-backed copy.- property device: str¶
'cpu','cuda', or'mps'.A numpy-backed result reports
'cpu'. A tensor-backed result reports the underlying tensor’s device type.- Type:
Device hosting the numeric fields
- to_numpy()[source]¶
Return a new PCASolution with numpy-backed numeric fields.
Idempotent on numpy-backed results (returns
self). On a tensor-backed result this is the explicit “I want CPU numpy” escape hatch, matching the rest of the library’s no-silent-migration contract.- Return type:
- to(device)[source]¶
Move the result to
device('cpu','cuda','mps').device='cpu'returns a numpy-backed PCASolution (same asto_numpy()). Any other device requires torch and materialises the fields astorch.Tensorinstances on that device.- Parameters:
device (str)
- Return type:
- property explained_variance_ratio¶
Proportion of variance explained by each component.
Returned as the same array type as
sdev– numpy for a numpy-backed result, torch.Tensor on the same device for a tensor-backed result. Both numpy ndarrays and torch tensors support.sum()and elementwise arithmetic so the impl is type-polymorphic.
- property cumulative_variance_ratio¶
Cumulative proportion of variance explained.
- class pystatistics.multivariate.FactorSolution(_result)[source]¶
Bases:
SolutionReprMixinResult from factor analysis.
Matches the structure of R’s
factanal()return value. Wraps aResult[FactorParams]envelope; every datum is exposed via a read-only@propertyso the public attribute surface is unchanged from the previous flat dataclass.- Parameters:
_result (Result[FactorParams])
- class pystatistics.multivariate.PCAParams(sdev, rotation, center, scale, x, n_obs, n_vars, var_names)[source]¶
Bases:
objectImmutable parameter payload for principal component analysis.
The numeric fields may be numpy arrays (CPU path) or
torch.Tensorinstances (device-resident GPU path); seePCASolutionfor the device-residency contract.- sdev¶
Standard deviations of principal components (length min(n, p)).
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]]
- rotation¶
Loadings matrix (p x n_components) – columns are eigenvectors.
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]]
- center¶
Column means used for centering (length p).
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]]
- scale¶
Column SDs used for scaling (None if scale=False).
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]] | None
- x¶
Scores matrix (n x n_components).
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]]
- class pystatistics.multivariate.FactorParams(loadings, uniquenesses, communalities, rotation_matrix, chi_sq, p_value, dof, n_factors, n_obs, n_vars, var_names, method, rotation_method, converged, n_iter, objective)[source]¶
Bases:
objectImmutable parameter payload for factor analysis.
- Parameters:
- loadings¶
Rotated loadings matrix (p x n_factors).
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]]
- uniquenesses¶
Uniqueness for each variable (length p).
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]]
- communalities¶
1 - uniquenesses.
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]]
- rotation_matrix¶
Rotation matrix, or None if no rotation.
- Type:
numpy.ndarray[tuple[Any, …], numpy.dtype[numpy._typing._array_like._ScalarT]] | None