Changelog¶
All notable changes to alsgls will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
[2.2.0] - 2026-09-03¶
Added¶
Kackar–Harville corrected standard errors, on by default.
cov_params,bse,conf_int,pvaluesand the prediction intervals now reportPhi_c + Lambda: the df-rescaled plug-in plus the first-order term forSigmabeing estimated (Kackar and Harville 1984).covariance("plugin")returns the uncorrected plug-in that linearmodels, systemfit and Stata report. Every reported standard error grows, by 4% atn = 20, 1% atn = 200, on the test fixture.Validated four ways before shipping: derivatives and information against finite differences; the structured implementation against the
r^2double loop to 1e-16;Lambdainvariant to rotatingFto 1e-16 (which is what justifies the pseudo-inverse of the singular information); and, at the true(F, D),Phi + Lambdareproduces the actual spread of the feasible estimator to 0.98 wherePhialone gives 0.94. An independent closed form: two equations with orthogonal regressors giveLambda/Phi = 1/Texactly.What it is not: calibration at
n = 20. Evaluated at(F_hat, D_hat)the ratio is 0.885 against 0.851 for the plug-in, becausePhi_hatis itself biased by Jensen’s inequality on a noisySigma_hat, and that is not whatLambdacorrects. Kenward and Roger’s (1997) second-order term is meant to and, measured here, makes it worse (0.774) – the failure Kenward and Roger (2009) document for covariance structures nonlinear in their parameters. It is not used.bootstrap()remains the calibrated object at smalln.Cost: 5 ms at K = 20, 89 ms at K = 60, 0.5 s at K = 100, 5 s at K = 200.
alsgls.kackar_harvillemodule;GramBlocks/assemble_blocksinalsgls.ops, the block assembly factored out ofcompute_XtSigmaInvX.
[2.1.0] - 2026-09-03¶
Added¶
results.bootstrap(B, method, seed)on bothALSGLSSystemResultsandALSGLS, returning aBootstrapResultswith percentile-tconf_int(), bootstrap-tpvalues, bootstrapbse, and the raw replicate arrays. Each replicate refitsF,Dandbeta, which is what captures the part of the sampling variance the plug-in cannot: thatSigmais estimated. The studentised interval is the calibrated object, per Rilstone and Veall (1996), Fiebig and Kim (2000) and Horowitz (2019); the plug-in is biased the same way inside each replicate as in the sample, so the quantiles absorb the bias. Schemes:"parametric","wild","residual". Pairs resampling is deliberately absent — atn = 20it leaves ~63% distinct rows.Validated on the Monte Carlo fixture at
n = 20(100 replicates, B = 199): the plug-in covers 0.875 and rejects a true null 0.105 of the time; the parametric bootstrap-t covers 0.930 and rejects 0.050, with an interval width 1.00 times what a correctly calibrated normal interval would have. Cost:B = 999takes about 20 s at K = 20, 40 s at K = 60 and 1.7 min at K = 100.init_Dkeyword onals_glsto warm-start the Sigma step.max_identified_rank(K)inalsgls._validation.
Changed¶
The Sigma step is now L-BFGS-B on the profile likelihood over
log D, withFconcentrated out in closed form, on residuals standardised to unit variance — exactly what R’sfactanaldoes, and for the reason it gives: alternating the two closed forms is a fixed-point iteration that crawls when a diagonal variance is small. On bootstrap replicates atn = 20the alternation’s per-fit cost had a median of 24 ms but a mean of 142 ms and a maximum of 598 ms (11,619 inner iterations); the quasi-Newton step’s mean is 1.7 ms and its maximum 7 ms, and where the alternation hit its cap it also found a likelihood 4.6e-4 nats/row better. The gradient is the envelope theorem, computed with the existing Woodbury kernels and verified against finite differences to 3.7e-9. Every guard from 2.0.0 still holds: agreement with sklearn’sFactorAnalysis, BIC rank recovery, Zellner, monotonicity ink.Standard errors now apply the residual degrees-of-freedom rescale
Sigma_ij * n / sqrt((n - p_i)(n - p_j))thatlinearmodels(debiased=True), Rsystemfit("geomean") and Statasureg(dfk) all apply. Applied asF -> diag(sqrt c) F,D -> c * D, which preserves the low-rank structure exactly. Standard errors grow bysqrt(n / (n - p)).The OLS ridge initialisation uses the nominal
lam_B, not the GLS-scaledlam_B_eff. An OLS ridge objective scales uniformly underY -> sY, so a fixed penalty is what givesB -> sB; the GLS objective has a dimensionless first term and needslam / s^2. Using the GLS scaling in the OLS init made the starting point scale-dependent, which the forgiving fixed-point Sigma step hid and the quasi-Newton one exposed.
Fixed¶
Unidentified factor ranks were accepted. The
k-factor model spendsK*k + K - k(k-1)/2parameters on a covariance withK(K+1)/2free entries; past Ledermann’s bound(K - k)^2 >= K + kthe loadings are not identified and the likelihood has a ridge of maxima. R’sfactanalrefuses with “degrees of freedom < 0”;als_glsnow does too, with the largest identified rank in the message._auto_rankand_default_k_candidatesrespect it. Both Monte Carlo test suites had been running atK = 4, k = 2— 11 parameters for 10 free entries, df = -1 — and one helper atK = 3, k = 2. Every calibration number they recorded was measured on aSigmathe data could not pin down. They now run atk = 1.
Root cause of the standard-error shortfall, measured¶
The plug-in (X' Sigma_hat^-1 X)^-1 understated the sampling spread — se
ratio 0.69 at n = 20. An oracle experiment splits it exactly:
0.69 = 0.77 x 0.88. The first factor is the plug-in’s bias at Sigma_hat
(Jensen: the formula is concave in Sigma), the second is the extra spread
feasible GLS carries over GLS at the true Sigma, which no formula at a fixed
Sigma_hat can see. Both are Freedman and Peters (1984, JASA, Theorem 1). The
oracle at the true Sigma is calibrated (1.03), so the linear algebra was
never wrong; its inputs were. The df rescale closes about a fifth of the gap.
The bootstrap closes the rest.
[2.0.0] - 2026-09-02¶
Note on version history: PyPI has only ever carried 0.1.0. The 1.0.0 and 1.1.0 entries below describe work that landed on the default branch but was never tagged or published, so for anyone installing from PyPI this release also carries everything in those two sections.
Changed¶
The Σ-step is now the closed-form factor-analysis update, and fitted values change for every user. The factor loadings were previously fitted by steepest descent with a backtracking line search, which stopped improving after about two sweeps and left the fit 2 to 20 nats/row short of the likelihood the same objective reaches from the same starting point. Running more sweeps did not help: 500 sweeps produced bit-identical output to 4.
Given
D, the maximisingFhas a closed form going back to Lawley, obtained from the topkright singular vectors of theD-standardised residuals. This is whatsklearn.decomposition.FactorAnalysiscomputes and what R’sstats::factanaloptimises over; the new implementation agrees with sklearn’s to 1e-7 on the implied covariance and to 1e-9 on the likelihood.The visible consequence is rank selection.
select_rank_bicchose 4, 8, 6, 7 and 10 on five fixtures whose true ranks are 2, 4, 5, 3 and 6, because the optimiser’s shortfall shrank askgrew and so the likelihood kept improving for a reason unrelated to the data. It now recovers the true rank on all five. The fit is also faster in wall clock, 1.4x at K=20 rising to 2.1x at K=200, because it converges and stops instead of running its sweep budget.lam_Bis now relative to the residual variance scale. An absolute ridge made the fit depend on the units ofY: at the default1e-3, scalingYby1e4moved every coefficient by 100% and the estimated correlation matrix by 1.16. The fit is now equivariant underY -> sY.
Removed¶
lam_F, fromals_gls,ALSGLSandALSGLSSystem. The Σ-step is the exact conditional solution, so there is no search direction for a penalty onFto bias. It never described a coherent estimator: the direction was penalised while acceptance was tested on the unpenalised likelihood, so the iteration could stop at a point stationary for neither, and it was the sole cause of the scale-dependence above.scale_correctandscale_floor. The guarded rescaling of Σ existed to patch the gradient step. Measured after a closed-form step, the optimal scale factor is 0.9999995 — a no-op.grad_F_nllfromalsgls.ops, now unused.infono longer carriesaccept_t,scale_used,obj_traceor thenll_sigma_tracealias, none of which have meaning without a line search. It gainssigma_iters,var_refandlam_B_eff.
Fixed¶
The
(F, D)line search froze after two sweeps. The backtracking ladder proposed(F + t*dF, D_mle(F + t*dF)), whoset -> 0limit is(F, D_mle(F))rather than the incumbent(F, D). The guarded scale correction movedDoff that manifold, so every candidate started nats behind the incumbent, all 40 halvings were rejected, andFnever moved again. Superseded by the closed-form step above, but fixed first so the two changes could be reviewed apart.betawas stale relative to the returnedSigma. The sweep ends on aSigmastep, sobetasolved the GLS normal equations at the previous sweep’sSigma. This matters becausecov_paramsreports(X' Sigma^-1 X)^-1asbeta’s variance, which is only its variance when the two agree.als_glsnow refreshesbetaat the finalSigma.select_rank_bicreported half the textbook BIC (N*nll + p/2*log Nwhere-2*loglik + p*log Nis2*N*nll + p*log N), and counted parameters that are not free while skipping ones that are.n_paramsis nowK*k + K - k(k-1)/2 + sum(p_j): the loadings less thek(k-1)/2rotationsF -> FQthat leaveF F^Tfixed, the diagonal variances, and the regression coefficients. Checked against R’sfactanal, which reports the complementarydf = ((K-k)^2 - K - k)/2.Out-of-domain arguments were accepted silently.
lam_B = nanpassed the non-negativity guard, sincenan < 0is False, and returnedFat its initialisation with no error.alphaoutside(0, 1)returned intervals whose lower bound exceeded their upper for every parameter.d_floor <= 0letDreach zero or go negative while the internals clipped at1e-12, so the returned(F, D)described a different and not positive definiteSigmafrom the one every reported number used.sweeps=Truepassed the positive-integer check and ran one sweep. Non-finite data surfaced asSVD did not converge. All are now rejected at the public boundary.ALSGLS.scoredocumented the negative mean squared error and returned the negative log-likelihood per row.d_floordocumented as an absolute variance while being applied as a fraction of the mean residual variance. The relative form is correct and deliberate; only the docstring was wrong.Documentation taught
em_gls(), removed in 1.0, and used four argument namesals_glshas never accepted (max_iter,tol,verbose, andsimulate_surkwargs that do not exist), so every example on three pages failed on the import.cg_solve’s positive-definiteness guard tested the previous iteration’s value while its message quoted the current one.
Changed (infrastructure)¶
Adopted the py-canon fleet standard: src/ layout, shared CI/docs/release workflows, ruff + pyright + pydoclint linting (mypy retired), and tag-driven trusted publishing.
scikit-learnadded as a test-only dependency, used as an independent implementation to check theSigmastep against.
[1.1.0] - 2025-03-31¶
New Features¶
Rank selection methods:
rank="bic"andrank="cv"for automatic rank selectionReal data example: Fama-French 49 industry portfolios demonstration
Formal methods documentation: Rigorous mathematical foundations
Improvements¶
Replaced heuristic ALS F-update with gradient-based descent
Added
select_rank_bic()andselect_rank_cv()functionsNew parameters:
rank_candidates,cv_folds,cv_random_state
Documentation¶
New
formal_methods.mdwith convergence proofs and complexity analysisNew
real_world_applications.mdwith finance example
[1.0.0] - 2024-12-21¶
🚨 BREAKING CHANGES¶
This is a major release with significant API changes that improve type safety, performance, and maintainability.
Removed Functions¶
em_gls()- Dense EM baseline algorithm removed entirelywoodbury_pieces()- Deprecated function that computed explicit inverse removed
API Changes¶
apply_siginv_to_matrix()-C_invparameter removed,C_cholnow requiredBefore:
apply_siginv_to_matrix(M, F, D)orapply_siginv_to_matrix(M, F, D, C_inv=C_inv)After:
apply_siginv_to_matrix(M, F, D, C_chol=C_chol)(Cholesky factor required)
Migration Guide¶
Replace
em_gls()calls withals_gls()- they provide equivalent statistical resultsUpdate
apply_siginv_to_matrix()calls to usewoodbury_chol()for the Cholesky factor:# Old approach Dinv, C_inv = woodbury_pieces(F, D) result = apply_siginv_to_matrix(M, F, D, C_inv=C_inv) # New approach Dinv, C_chol = woodbury_chol(F, D) result = apply_siginv_to_matrix(M, F, D, C_chol=C_chol)
Added¶
Full type safety - Comprehensive type hints throughout with mypy compliance
Enhanced error messages - More informative validation with actionable suggestions
Input validation helpers - Centralized validation with better error reporting
Changed¶
Mandatory numerical stability - All operations now use Cholesky factorization
Cleaner API - Single computational path eliminates confusion
Improved documentation - Focus on ALS benefits without legacy comparisons
Fixed¶
Type consistency - All return types properly specified and validated
Error message quality - Include context and suggestions for common issues
[0.3.0] - 2024-01-XX¶
Added¶
High-level
ALSGLSestimator with scikit-learn APIALSGLSSystemfor statsmodels-style system estimationAutomatic rank selection with
rank="auto"Comprehensive documentation with Sphinx
Changed¶
Improved conjugate gradient solver stability
Better memory usage tracking
Enhanced convergence diagnostics in info dict
Fixed¶
Numerical stability for near-singular matrices
Edge cases in diagonal floor handling
[0.2.0] - 2024-01-XX¶
Added¶
EM baseline implementation (
em_gls) for comparisonMatrix-free conjugate gradient solver
Woodbury matrix identity optimization
Performance benchmarking scripts
Changed¶
Refactored core operations into
ops.pyImproved simulation functions
Better default parameters
[0.1.0] - 2024-01-XX¶
Added¶
Initial release
Core
als_glsfunctionBasic simulation utilities
MSE and NLL metrics
Example scripts