Multi-trait factor analysis of GWAS Z-scores is brittle.
Clorinn makes it convex.
Clorinn decomposes a trait variant matrix of GWAS Z-scores,
into a low-rank shared signal and a sparse component,
separating genetic structure from noise, outliers, and trait-specific effects.
The result is a denoised matrix suitable for stable factor analysis.
Convex optimization guarantees that
the solution is independent of initialization,
reproducible across runs, and robust to small changes in the input data.
Clorinn is an acronym for Convex Low-Rank Inference via Nuclear Norm constraint.
You run PCA on 2,000 GWAS traits.
The top factor is dominated by a single artifact in one trait.
You add a new cohort.
Every factor changes.
You fix a data error in one trait.
Every factor changes again.
This is not a bug -- this is because GWAS data is not Gaussian: outliers dominate, entries are missing, and errors are correlated across traits. In such cases, a single artifact can dominate the leading factors. Clorinn is designed to handle the noise that actually appears in the data.
Use Clorinn instead of PCA / SVD when:
- a few traits or SNPs dominate the top components
- results change drastically when adding/removing traits
- data has missing entries
- traits share samples (correlated errors)
If your data is clean, Gaussian, and well-behaved, PCA and/or truncated SVD is usually fine.
The observed Z-score matrix
where
Once the denoised
Clorinn separates what you optimize (the model) from how you optimize it (the solver). In practice, you pick one of each:
| Models | minimize |
subject to |
Use when |
|---|---|---|---|
| NNM | Independent traits, isotropic noise | ||
| NNM-Sparse |
|
Sparse outliers or trait-specific effects | |
| NNM-Corr1 | Correlated errors e.g. traits share samples |
1For NNM-Corr,
| Solvers | Algorithm | Models supported |
|---|---|---|
| FW | Frank-Wolfe | NNM, NNM-Sparse, NNM-Corr |
| AFW | Away-step Frank-Wolfe | NNM, NNM-Sparse, NNM-Corr |
| PGD | Projected Gradient Descent | NNM, NNM-Sparse, NNM-Corr |
Typical choices
- Start with NNM + FW (default)
- Use NNM-Sparse if a few entries dominate
- Use NNM-Corr if traits share samples
- Use PGD → AFW for faster convergence on large problems
- Correlated-error model (NNM-Corr) using sampling covariance
- Explicit handling of missing data (pattern-based computation)
- Projected Gradient Descent (PGD) solver
- Hybrid PGD → Frank–Wolfe workflow
- Cleaner API
# From GitHub
pip install git+https://github.com/daklab/clorinn.git
# Development install
git clone https://github.com/daklab/clorinn.git
cd clorinn
pip install -e .Requirements: Python ≥ 3.10, NumPy, SciPy, scikit-learn.
=======
from clorinn.optimize import FrankWolfe
from clorinn.utils import MatrixFactorization
fw = FrankWolfe(model='nnm')
fw.fit(Z, radius=r)
mf = MatrixFactorization(k=10)
mf.fit(fw.result.X)
L = mf.L # trait loadings (N, k)
F = mf.F # hidden factors (P, k)fw = FrankWolfe(model='nnm-sparse')
fw.fit(Z, radius=r, sparse_scale=0.5)
X = fw.result.X # shared low-rank component
M = fw.result.M # sparse trait-specific componentfrom clorinn.utils import SamplingCovariance
# Option 1: pass a plain ndarray
fw = FrankWolfe(model='nnm-corr')
fw.fit(Z, radius=r, noise_cov=A)
# Option 2: explicitly validate before fitting
cov = SamplingCovariance.from_matrix(A)
fw.fit(Z, radius=r, noise_cov=cov)
print(cov.repair_info)
# {'n_iter': 12, 'converged': True, 'min_eig_input': -0.003, ...}from clorinn.optimize import ProjectedGradientDescent, FrankWolfe
model = 'nnm' # or 'nnm-corr', 'nnm-sparse'
pgd = ProjectedGradientDescent(
model=model,
stop_criteria=('relative_loss', 'boundary_active'),
verbose=1,
)
afw = AwayStepFrankWolfe(model=model)
fit_kwargs = {}
if model == 'nnm-corr':
fit_kwargs['noise_cov'] = A
elif model == 'nnm-sparse':
fit_kwargs['sparse_scale'] = 0.5
pgd.fit(Z, radius=r, **fit_kwargs)
afw.fit(Z, radius=r, X0=pgd.result.X, **fit_kwargs)SamplingCovariance constructs and validates the
from clorinn.utils import SamplingCovariance
# From a pre-assembled matrix
cov = SamplingCovariance.from_matrix(A, repair=True, verbose=1)
# from_ldsc() — planned for v2.1
# cov = SamplingCovariance.from_ldsc(ldsc_intercepts, std_errors)
print(cov.A) # validated PD matrix
print(cov.is_repaired) # True if Higham repair was applied
print(cov.repair_info) # n_iter, min_eig_input, min_eig_output, reg_appliedBecause pairwise LDSC intercept estimates are noisy,
the assembled matrix is not guaranteed to be positive definite.
repair=True applies Higham's nearest-PD algorithm with Dykstra correction.
# All tests, silent
clorinn --test
# All tests, progress output
clorinn --test --verbose
# All tests, debug output
clorinn --test --vverbose
# Specific test class
clorinn --test --testmodule TestRegressionNNMCorrThe test suite is organised into four directories
under src/clorinn/tests/:
tests/
unit/ Tests for individual classes and methods in isolation
regression/ Numerical regression fixtures (freeze solver traces)
invariants/ Solver invariants (feasibility, monotonicity, gaps)
theory/ Mathematical correctness (exact missingness, gradients)
integration/ Robustness on degenerate inputs
To regenerate regression fixtures after an intentional solver change:
python -m clorinn.tests.regression.generate_current_behaviorClorinn is related to Sparse PCA, Robust PCA, nuclear norm minimisation for matrix completion, and latent factor models for GWAS. Key comparators evaluated in the paper: truncated SVD [Tanigawa et al. 2019], FactorGo [Zhang et al. 2023], flashier [Willwerscheid et al. 2024], GUIDE [Lazarev et al. 2025], and GLEANR [Omdahl et al. 2025].
[1] Banerjee S, O'Connell S, Colbert SMC, Mullins N, Knowles DA. Convex approaches to isolate the shared and distinct genetic components of complex traits. 2025. medRxiv 2025.04.15.25325870