NEWS


nmfkc 1.0.2

nmfkc.inference(method = "refit"): one multiplier per column by default

The refit bootstrap now defaults to wild.unit = "column": one multiplier per column of Y, shared by its P rows. It used to default to "element", an independent multiplier per cell, which treats the P rows of a column as independent -- for longitudinal data with P time points per individual, the repeated measurements of one individual. When they are correlated, as growth curves typically are, "element" understates the bootstrap SE. "column" allows any dependence within a column, as the sandwich SE (built from one score per column, CR1) and the "onestep" engine (one multiplier per column score) already did; the refit default was the only one of the three that did not. nmfkc.ar.latent.inference() has defaulted to "column" from the start.

This changes the result of an existing call of nmfkc.inference(method = "refit") with P > 1 that does not set wild.unit: its bootstrap SE, intervals and p-values come from different draws. wild.unit = "element" reproduces the earlier results exactly. With P = 1 nothing changes: both units draw the same N multipliers in the same order (checked identical). The point estimates, the sandwich SE and the "onestep" engine are not affected.

nmfkc 1.0.1

The identification diagnostic in nmf.ffb.inference(), nmf.rrr.inference() and nmfre.inference()

The three return identified.approx and identification as well, built on the same check as nmfkc.inference() (below), adapted to each model. Each is an algebraic check on the point estimate -- no re-fits -- and summary() prints one line. Nothing else changes: every other component is identical (checked on six calls).

Not covered: nmf.gmm.inference() and nmf.rrr.signed.inference() (signed coefficients) and nmfkc.net.inference() (the family (X T, T^-1 C T^-T)).

nmfkc.inference() reports whether C is identified

The coefficients and intervals of nmfkc.inference() are conditional on the estimated basis X. It now also returns identified.approx and identification, the diagnostic of nmfkc.ar.latent.inference() (below) applied to X and C: when identified.approx is TRUE the factorization is unique up to a permutation of the bases, so the intervals concern the same C whichever admissible representative (X T, T^-1 C) is taken; when FALSE the entries of C describe this representative. It also requires A of full row rank (identification$A.full.rank), since otherwise C is not unique even given X. summary() prints one line about it. sep.tol and pin.tol can be passed through ...; coefficients at or below boundary.tol count as zero. nmfkc.net.inference(), which calls nmfkc.inference() with A = t(X), does not carry these components: for Y ~ X C X' the family of equivalent fits is (X T, T^-1 C T^-T), which the diagnostic does not cover.

It is an algebraic check on the point estimate -- no re-fits, nothing random -- and takes about a millisecond for Q = 3 with 50 covariates. Nothing else changes: the estimates, intervals and other components are identical.

nmfkc.ar.latent.inference(): a weaker identification check, with two routes

identified.approx says whether the entries of the latent transition matrices G_d and the composition p.star can be inferred, i.e. whether the factorization is unique up to a permutation. It used to require an anchor row of X for every basis and a pure covariate for every basis (a column of Theta that is non-zero for that basis alone). The second half was more than the argument needs, and fits whose Theta is nearly one-basis-per-covariate were reported as not identified. Two sufficient conditions are now checked, with nothing for the caller to choose:

Every fit that passed the old check passes the new one. The Details of ?nmfkc.ar.latent.inference give the argument and two counterexamples: separability of X alone is not enough, and neither are zeros that merely separate every pair of bases (T = (J - I)/2 is then admissible).

A coefficient counts as zero when it is at most kkt.tol or at most the new pin.tol (default 0.01) times the coefficient of the same covariate for the basis being pinned -- that ratio is the mixing of bases the covariate still allows. A pass is no longer a warning: print() states it with the precision reached, e.g. "unique up to a permutation of the bases; the zeros it rests on hold to 0.0024, so an equivalent fit mixes the bases by at most about 0.24%". A failure still warns, naming the part that fails. A covariate whose coefficients are all numerical noise is ignored; ratios inside such a column once looked like a pin. For X the tolerance is 1 - sep.tol times the loading of the basis being pinned.

identification gains pinned, pin.order, X.pinned, X.pin.order, route and the margins pin.margin and X.pin.margin; pure and pure.margin use the new zero rule (the margin is now a ratio, not an absolute coefficient). On the US 49-industry monthly realized volatility (Q = 3, D = 1) the check said FALSE and now says TRUE, by the mirror route; a direct search over all admissible T on configurations of this kind found only permutations. The fields about X carry a capital X., like X.anchor and A.full.rank; development builds of 2026-10-18 spelled them x.pinned, x.pin.order and x.pin.margin.

Nothing else changes: estimates, intervals and the other components are identical (8 calls checked).

A block wild bootstrap for time points: wild.unit = "block"

nmfkc.ar.latent.inference() and nmfkc.inference(method = "refit") accept wild.unit = "block". One multiplier is drawn per block of L = max(1, round(n^(1/3))) consecutive columns (time points, n = ncol(Y)) and shared by all rows (series), so residuals within a block keep their serial dependence. The block grid starts at a random offset in 0, ..., L-1, drawn anew in each replicate. L is 3 for n = 27, 5 for 100 and 8 for 500; for n <= 3 it is 1 and the draws are those of "column". The length is fixed by n: it cannot be set and is not stored. Use it when the residuals are autocorrelated (for example with too few lags); for a correctly specified VAR the default of nmfkc.ar.latent.inference() -- one Rademacher multiplier per time point ("column"), wild.B = 500 -- suffices. The Details of ?nmfkc.ar.latent.inference describe the scheme.

nmfkc.inference() now also returns wild.unit, the multiplier unit of the "refit" bootstrap (NA otherwise).

Nothing else changes: the default bootstrap of nmfkc.ar.latent.inference(), its "element" and "exp" modes, and the onestep and refit (element, column) modes of nmfkc.inference() give bit-identical results (8 calls checked).

Anchor rows of X: X.anchor, X.init = "spa", and a separability diagnostic

Two options for nmfkc(), both passed through .... A fit that uses neither is unchanged: checked bit-identical on 167 existing calls.

On data separable by construction, the anchored fit recovered X to 1e-5 and the covariate effects to 1e-3, while the default start reached the same objective at a rotated solution.

summary() now reports the separability of the basis: for each basis the largest share it takes in any row of X (with the columns of X scaled to sum to one, so the value does not depend on X.restriction), minimized over the bases. 1 means every basis has a row of its own, 1/Q that every row mixes all bases equally; an X.anchor fit scores exactly 1, and on the data above the default fit scored 0.91. The per-basis values and the rows attaining them are anchor.purity and anchor.row. All three are stored on the fit, in fit$criterion; summary() reads them there (and recomputes them for objects saved by earlier versions). It looks at X only -- zeros in B or in C can also pin the solution -- so a value near 1 says the fit is nearly unique, while a low value does not say it is not.

These three criterion elements are the only change to a fitted object: every value it held before is unchanged (checked bit-identical on the same 167 calls, with the new elements set aside).

The anchor options reach the other fitters

X.anchor and X.init = "spa" now work in nmfre(), nmf.ffb(), nmf.rrr(), nmf.gmm() / nmf.gmm.twostage() and nmfkc.signed(). All but the last used to accept X.anchor and drop it without a word -- the fit came out identical with or without it -- so a caller could believe anchors were imposed when they were not. Each now imposes them and returns the rows used as $X.anchor; what differs between them is where the basis is estimated and what limits the rank.

| function | anchors apply to | rank limit | |:--|:--|:--| | nmfre() | X; kept through its floor on X | none (the random effects give B full rank) | | nmf.ffb() | the stage-1 basis, then held fixed | rank <= nrow(Y2) | | nmf.rrr() | the response basis X1 | rank1 <= min(rank2, nrow(Y2)) | | nmf.gmm(), nmf.gmm.twostage() | X (found on Y, not on the residuals) | none (the mixture gives B full rank) | | nmfkc.signed() | X | rank <= nrow(A) |

"spa" compares rows of Y by their shares, so it needs a non-negative Y. nmfkc.signed() documents a signed Y: there the anchor rows can be given but not searched for, and the start uses the semi-NMF basis step (Ding, Li & Jordan 2010). nmfkc.net() and nmf.ffb(method = "mu") cannot use the options and now refuse them rather than ignoring them.

Nothing changes for a call that uses neither option: checked bit-identical on 22 default calls across the seven fitters.

nmfkc.net.ecv(): a diagonal to leave out, folds to share, repeated splits, held-out predictions

Asked for by the covariate-network study, which compares models on edge-wise CV. Each option is passed through ... and changes nothing when absent: checked bit-identical on 16 existing calls of nmfkc.net(), nmfkc.net.ecv() and nmfkc.net.rank().

Two inputs went wrong before and now work:

The element-wise CVs keep a weights argument

nmfkc.ecv(), nmfkc.signed.ecv(), nmf.rrr.ecv() and nmf.rrr.signed.ecv() dropped a Y.weights (Y1.weights for the RRR pair) without a word: the result was identical with or without it. Three of them spliced the fold mask in ahead of ...; nmfkc.signed.ecv() removed the weights from the fit arguments. Now, in all four:

Calls without weights are unchanged: checked bit-identical on 10 + 16 calls covering the four functions, nmfkc.rank(), nmfkc.signed.rank(), nmf.rrr.rank(), nmf.ffb.cv(method = "fiml") and cores = 2.

nmfkc() no longer stops on data with all-zero columns

With X.init = "kmeans" (the default) or "kmeans++", two or more all-zero observation columns could make k-means return the zero vector as a cluster centre. Normalizing that basis column gave 0/0, and the fit stopped with "missing value where TRUE/FALSE needed" -- or, with maxit < 10, returned an all-NaN object as if it were a fit. The initialization is now rerun on the non-zero columns, with a message saying so; the fit itself still uses every column, since with covariates a zero column is informative about C.

A fit whose objective becomes NaN now stops with the cause rather than R's generic message, and a Y with no positive entries is named as such. A user-supplied X.init with a zero column is reported, not altered.

No fit that succeeded before changes. The repair acts only on a start that normalizes to a non-finite value, and such a start could not produce a result. Checked bit-identical (identical() and all.equal(tolerance = 0)) on 143 previously successful calls, among them data with zero columns that k-means happened not to isolate, and X.restriction = "none", where a zero basis column never failed and still stays zero.

Documentation that answers questions raised in use

None of these change a result.

nmf.gmm(): covariate effects that differ by class (class.effects)

nmf.gmm() gains class.effects, which names the covariates whose coefficients may differ between classes. The class-k mean of the scores becomes C a_n + Delta_k a_n[S] + mu_k with sum_k xi_k Delta_k = 0, so the columns S of C keep their meaning as the (xi-weighted) average effect and the class-specific coefficients are returned in C.class. Before, every covariate had one coefficient shared by all classes, so a covariate whose effect differs between classes could only be absorbed by extra components. Covariates are named by row of A or, with a formula, by term, so class.effects = "diet" frees every indicator row of a factor. The default (NULL) fits the common-effect model exactly as before. The M-step solves the class-specific coefficients, the common ones and the class means jointly by weighted least squares; BIC and ICL count the (K - 1) Q |S| extra parameters. With a categorical covariate, freeing all of its levels gives each level its own class means, and the likelihood then no longer ties the classes of one level to those of another; the help page says so. nmf.gmm.inference() and nmf.gmm.twostage() refuse such fits for now.

nmf.gmm(): an ordinary-NMF starting basis, and the basis it started from

nmf.gmm() and nmf.gmm.twostage() accept X.init = "nmf": the basis of an ordinary NMF, nmfkc(Y, rank = rank, seed = seed) without covariates. Before, the only named choices were the one-step initializers ("nndsvd", the default, "kmeans++", "kmeans", ...), so reproducing a fit started from a fitted NMF basis meant building that basis by hand. The default is unchanged.

The fit now returns X0, the column-normalized basis the EM started from. All nstart restarts share this one basis and differ only in the seeding of the mixture, so the partition can depend on the basis; returning it makes a fit reproducible from the object alone. Both functions now build the basis through one internal helper, so the two-stage baseline cannot drift from the joint fit.

nmf.gmm.twostage(): the help no longer says "only the order differs"

The help page, the fixX entry of nmf.gmm() and the vignette said that the two-stage baseline differs from the joint fit "only in the order of adjustment and clustering". It does not: unless fixX = TRUE, the two-stage route refits the basis to the reconstituted, shifted residuals, and even with fixX = TRUE it fits different data. The text now says what is held equal (the initial basis, the mixture family, the settings) and what is not. It also no longer promises that the two routes "agree" when the covariate is mean-independent of the class: the class-mean displacement vanishes in the population, but the fitted partitions need not coincide in a finite sample. The reference to the accompanying paper names its oracle proposition instead of a proposition number, which has changed between drafts.

The package description names nmf.ffb(), not the removed nmf.sem()

The Description field of DESCRIPTION still told readers to reach the feed-forward + feedback model through nmf.sem() -- the very name 0.9.8 removed -- and called the model "NMF-based Structural Equation Modeling (NMF-SEM)", a label the package retired when nmf.sem() was renamed in 0.8.8. It now names nmf.ffb() and NMF-FFB. The 0.9.8 release on CRAN still carries the old sentence; nothing else in it refers to the removed functions.

nmfkc 0.9.8 (2026-09-22)

The deprecated nmfae* names are removed

The fifteen nmfae* / nmfae.signed* forwarders are gone: nmfae(), nmfae.inference(), nmfae.ecv(), nmfae.cv(), nmfae.rank(), nmfae.DOT(), nmfae.heatmap(), nmfae.kernel.beta.cv(), nmfae.rename() and their six nmfae.signed* counterparts. Use the nmf.rrr* / nmf.rrr.signed* names, which they forwarded to. Deprecated in 0.8.8, carried through two CRAN releases with a .Deprecated() note.

The nmfae S3 classes stay. They are where this family's methods are defined, nmf.rrr() still returns c("nmf.rrr", "nmfae", "nmf"), and summary() still returns "summary.nmfae". Nothing about dispatch changes, so code that inspects the class of a fit, and objects saved by an earlier version, keep working. Renaming that layer to nmf.rrr is a separate change, for a release that is not three days from a submission.

The deprecated nmf.sem* names are removed

nmf.sem(), nmf.sem.inference(), nmf.sem.cv(), nmf.sem.split() and nmf.sem.DOT() are gone, together with the six S3 methods registered on the nmf.sem classes. Use nmf.ffb() and its family; the names map one to one and nothing else changes, because the removed functions were pure forwarders (nmf.sem <- function(...) { .Deprecated("nmf.ffb"); nmf.ffb(...) }).

They were deprecated in 0.8.8 (2026-07-14) and have emitted a .Deprecated() note through two CRAN releases, 0.8.8 and 0.9.6. No package on CRAN depends on nmfkc, so nothing else is affected.

Fitted objects no longer carry the legacy class: nmf.ffb() now returns c("nmf.ffb", "nmf") rather than c("nmf.ffb", "nmf.sem", "nmf"), inference results drop "nmf.sem.inference", nmf.ffb.DOT() drops "nmf.sem.DOT", and summary() returns "summary.nmf.ffb" alone. Code that tests for those class strings, and objects saved by an earlier version, will no longer dispatch to the nmf.sem methods -- which is the point of removing them, but it is worth knowing before loading an old .rds.

NMF-FFB: the help pages no longer describe options that were removed

C1.restriction accepted "block" and "cross" while the exclusion restriction was being settled; both were dropped in 0.9.8 in favour of "union", but the help pages and the vignette still described them and still listed them as admissible values. They now describe only what the code accepts, "union" and "none" (or a user matrix).

The wording of the restriction itself is also corrected. It was "an outcome may not feed back into a factor on which it loads", which reads as if any non-zero loading blocked the entry; the rule is "a factor on which it has more than a negligible loading" -- the dominant factor of the outcome, plus every factor whose loading reaches C1.restriction.threshold. Neither half alone is the rule, and the help now says why. The vignette adds the reference for reading a BIC difference as evidence (Kass and Raftery 1995, p. 777).

The NMF-GMM family is marked experimental again

nmf.gmm(), nmf.gmm.inference(), nmf.gmm.select(), nmf.gmm.twostage() and the S3 methods on their classes say on their help pages that they are experimental and still under development: argument names, defaults and the contents of the returned objects are not yet stable. The vignette says so too.

The notice was removed in 0.9.7 on the reasoning that publishing on CRAN is a commitment to a fixed interface. That reasoning was premature — the family is still being developed alongside the paper — so the notice is back, and now covers nmf.gmm.twostage() and the S3 methods, which it had missed. Nothing else in the package carries the caveat.

Every restriction on X that is not a gauge fix is removed

X.rowSums.min (both fitters) and X.restriction = "rowSums" (nmfkc.signed()) are gone. Both acted on the rows of X, which changes X %*% C %*% A, so neither was a gauge fix: the scale they removed could not be handed to C, they sat outside the multiplicative form, and the objective was no longer monotone.

Both were added after the last CRAN release, so nothing on CRAN is affected. "rowSums" is refused with a message rather than silently re-mapped to "colSums", which would change results without saying so. The remaining restrictions -- "colSums" (default), "colSqSums", "totalSum", "none", "fixed" -- are all gauge fixes and leave the updates monotone.

Both fitters now return epsilon.iter (relative change at the last step) and objfunc.increases (steps at which the objective rose), and print() / summary() show them, so a fit that is oscillating rather than descending is visible. nmfkc.signed()'s converged is now the stopping rule's own test rather than iter < maxit, so a run that stopped early on a non-finite objective is no longer reported as converged.

The two epsilon.iter are not the same quantity and should not be compared across the two fitters: nmfkc() divides by max(|f_i|, 1) and nmfkc.signed() by |f_{i-1}|. Each matches the stopping rule of its own fitter, and they agree while the objective exceeds 1; below 1 nmfkc()'s test is effectively an absolute one. Both are documented on the respective help pages.

NMF-FFB: names brought into line with the rest of the package

nmf.ffb() grew its own vocabulary while the likelihood estimator was being built, and of the 55 fields on a fit only nine shared a name with an nmfkc() fit -- two of those meaning something different. The names are now the package's. This is a breaking change, confined to the likelihood branch and its inference, all of which was added after the last CRAN release.

| was | is | why | |:--|:--|:--| | fit$Q | fit$rank | rank is the field on every other fitter | | fit$MAE | fit$mae | likewise (the path / candidates columns stay MAE, beside BIC) | | fit$objfunc (NULL) | fit$objfunc = the minimized negative log-likelihood | it was the one house field the fiml path left empty | | fit$objfunc.full | fit$objfunc.penalized | it is the penalized objective, unrelated to the full fit | | mask = (argument) | C1.restriction = | mirrors X.restriction; the argument and the field of the same name held different things | | fit$mask (matrix) | fit$C1.free | it marks the entries left free, and the argument mask was a rule string | | fit$mask.rule | fit$C1.restriction | the field now means what the argument means | | cross.threshold | C1.restriction.threshold | it parameterizes the restriction, so it shares its prefix | | phi =, fit$phi | Phi.restriction | phi and Phi differed only in case | | lambda1 | C1.L1.path | the L1 penalty on C1, as a path; fit$path$C1.L1 is one value of it | | lambda1.selected | C1.L1.selected | | | ci.level | boot.level | the confidence level of the bootstrap, as wild.level elsewhere | | bootstrap.B, .threshold, .ci.level, .n.valid, .n.invalid, .type | boot.B, boot.threshold, boot.level, boot.n.valid, boot.n.invalid, boot.method | boot.method is the house name | | C1.array, C2.array | C1.boot.draws, C2.boot.draws | as C.boot.draws in nmfkc.inference() | | rho.boot | rho.boot.draws | | | mask.change.rate | C1.restriction.change.rate | | | calibration = "full" | calibration = "procedure" | "full" already named the unrestricted feedback fit | | plot(fit, which = "full") | which = "penalized" | same reason | | coefficients$p_value | (removed; use prob.unsupported) | it held 1 - support_rate, which is not a p-value | | nmf.ffb.diagnostics()$top$rank | $top$order | rank now means the number of factors |

Three defects surfaced while doing this, all of them silent until now.

Options withdrawn in 0.9.8 (starts, nsplit, calibration as an argument of the fit) reached ... and were dropped without a word; they now warn. A renamed argument is worse than a withdrawn one -- mask = "none" would be dropped and the fit would silently use the default restriction, the opposite of the request -- so the four renamed arguments stop with the new name instead.

NMF-FFB is the canonical name everywhere, NMF-SEM the alias

The nmf.sem* functions have been deprecated aliases of nmf.ffb* for some time, but the rest of the package had not followed:

The fitted object still carries c("nmf.ffb", "nmf.sem", "nmf"), and the deprecated functions still work and still say so. The tutorial now also shows the exclusion restriction (fit$C1.restriction, fit$C1.free), the BIC path that plot() draws, and the two-stage convergence line.

NMF-FFB: one function per step of the procedure

The feedback model is now driven by four functions, one for each step, instead of two that each did several things:

ecv <- nmf.ffb.ecv(Y1, Y2, rank = 1:5)   # 1. choose Q by element-wise CV
fit <- nmf.ffb(Y1, Y2, rank = Q)         # 2. estimate; BIC selects the support
tst <- nmf.ffb.test(fit, Y1, Y2)         # 3. test the feed-forward null
dgn <- nmf.ffb.diagnostics(fit)          # 4. cycles, spectral radius, best supports
inf <- nmf.ffb.inference(fit, Y1, Y2)    # 5. intervals for the retained entries

Options removed

Measurement, not taste, decided each of these; keeping them invited the reader to compare procedures as if they were equally valid.

The feedback test: what the bootstrap conditions on, and two fixes

The parametric bootstrap that calibrates the feedback LR statistics used to hold the estimated basis X and the exclusion restriction fixed at their observed values while regenerating Y1*. Both are functions of Y1, so the null distribution omitted the adaptivity of that selection and the p-values were anti-conservative: on the two positive examples of the NMF-FFB paper the fixed-basis bootstrap gives p < 0.001, re-running stage 1 on every replicate gives p = 0.05 and 0.35, and the exclusion restriction turns out to move in 40% and 95% of the null replicates. nmf.ffb.test() therefore has a calibration argument with two levels:

Two fixes in the same code. LR.p.boot is now (1 + #)/(1 + B_ok) instead of the raw proportion, which was exactly 0 whenever no replicate reached the observed statistic (the normal case for a strongly significant fit, and not a valid p-value); the floor 1/(1 + B_ok) is printed as < floor. And the L-BFGS-B convergence codes of the null replicates are no longer discarded: LR.boot.n.nonconv reports how many missed the tolerance and a warning is raised above 10%, because on a flat null likelihood (small N, full Phi) the share can reach 40% and must be visible to the user.

nmf.ffb(): exclusion restriction "union" (new default)

C1.restriction = "block" excluded only the dominant factor of each outcome, and "cross" only the factors with loading at or above C1.restriction.threshold. Neither is a superset of the other: an outcome with a substantial second loading could still feed that factor under "block", and an outcome whose largest loading is below the threshold kept its own factor free under "cross". The new default "union" excludes both, which is the rule "an outcome may not feed back into a factor on which it loads". On the NHANES data of the paper this removes four selected paths (including BMI -> physical factor, coefficient 0.58) that were items feeding a factor on which they load. The fit records C1.restriction and stage1.args so that nmf.ffb.test() can re-derive the restriction and re-run stage 1.

nmf.ffb(): likelihood-based estimator (method = "fiml", new default)

The joint multiplicative-update estimator that nmf.ffb() used until now minimizes the structural-form squared error \eqn{\lVert Y_1 - X(\Theta_1 Y_1 + \Theta_2 Y_2)\rVert_F^2}. Once \eqn{X} is free that objective cannot separate \eqn{\Theta_1} from \eqn{\Theta_2}: the structural and reduced forms fit equally well, so the recovered feedback is an artefact of the initialization and the penalties. nmf.ffb() therefore gains a two-stage likelihood-based estimator, now the default:

  1. the basis \eqn{\hat X} is estimated by the feed-forward fit nmfkc(Y1, A = Y2) (or supplied through the new X argument);
  2. conditional on \eqn{\hat X}, the Gaussian working model Y1 = X B + E, B = Theta1 Y1 + Theta2 Y2 + U, U ~ N(0, Phi), E ~ N(0, diag(psi)) is fitted by FIML (L-BFGS-B, analytic gradient) under the non-negativity of Theta1, Theta2 and an exclusion restriction on Theta1 (C1.restriction, default "union", see above: no outcome may feed back into a factor on which it loads). The feed-forward null (Theta1 = 0, a non-negative MIMIC factor model with correlated factors), the unpenalized feedback fit and an L1 path on Theta1 with re-estimation on each support are fitted; the support with the smallest BIC is the reported model.

The penalized problem of the L1 path is non-convex, and a single starting point can miss the support with the smallest BIC: on the Holzinger-Swineford data the start from the unpenalized fit alone proposes a one-path model (BIC -1968.9) while the six-path model (BIC -1971.0) is proposed only from other starts. Every point of the path was therefore fitted from several starts (argument starts) while this release was being prepared -- and the alternatives were then measured on six data sets and removed (see Options removed): only the warm start from the unpenalized fit survives. What remains of the idea is the registry it needed: every distinct support proposed anywhere on the path is re-estimated without penalty, and BIC is minimized over all distinct candidates together with the null and the unpenalized model. path has one row per penalty with C1.L1, start, support_id, pen.value and duplicate columns; candidates (one row per distinct support), supports and support.selected are new fields, and nmf.ffb.test() re-runs the same pipeline in its null bootstrap.

The returned object keeps every legacy field (X, C1, C2, XC1, Leontief.inv, M.model, mae, ...; SC.map and SC.cov are NULL) and adds method, Phi, psi, loglik, npar, null, full, path, C1.free, C1.L1.selected, support, LR (with LR.df), BIC, AIC, call. The likelihood-ratio statistics are returned without p-values: Theta1 >= 0 puts the null on the boundary of the parameter space and the BIC refit is a post-selection statistic, so a chi-square reference is invalid.

Two parametric bootstraps run with X fixed, and they are now two functions. nmf.ffb.test() draws from the fitted null, re-running the whole selection pipeline on each replicate, and returns LR.boot, LR.p.boot, LR.null.quantile and prob.select.null (the false-selection rate of BIC under the null). nmf.ffb.inference() draws from the selected model with the support fixed and returns the coefficients table (centred percentile intervals, support rates) that nmf.ffb.DOT() and summary() read. nmf.ffb.DOT() gains model = c("selected", "null", "full") to draw the feed-forward null or the unpenalized fit side by side with the selected model. nmf.ffb.cv() with method = "fiml" delegates to nmfkc.ecv(): column-wise CV of the equilibrium mapping cannot select Theta1 (the reduced form is the same with and without feedback), so the only tunable quantity is the stage-1 rank. summary() reports the log-likelihoods, LR statistics and BIC of the three fits. plot() does not apply to a fiml fit (L-BFGS-B leaves no objective trace) and says so.

method = "mu" is the previous estimator, moved verbatim into an internal function and verified bit-identical (identical() on a battery of fits, inference runs and CV scores before and after the change); its objects now carry method = "mu" as an additional last field. It is kept so that published analyses reproduce and will be deprecated in a later release. nmf.ffb.inference(), nmf.ffb.cv() and nmf.ffb.DOT() are unchanged for it.

nmfkc.signed(): multi-start (nstart.signed)

Signed models have many more local minima than non-negative ones, because \eqn{\Theta = C_{+} - C_{-}} takes both signs. Until now nmfkc.signed() only forwarded nstart to the non-negative warm-start fit, and the documentation asked the caller to loop over seeds. It now does the loop: with nstart.signed > 1 the whole fit is repeated from that many consecutive seeds (seed, seed + 1, ...) and the fit with the smallest $objfunc is returned, with $restarts recording the seed, objective, iteration count and convergence flag of every start. cores parallelizes the restarts. The default nstart.signed = 1 leaves the previous behaviour untouched.

nstart keeps its old meaning -- initialization of \eqn{X} inside the non-negative warm start -- so the two are independent. Since $ on a list partial-matches, both are now read with [[name, exact = TRUE]]; without that, passing nstart.signed alone would also have set nstart.

Restarts are cheap for Gram input, where \eqn{S} and \eqn{G_0} are already accumulated. A budget of 10-50 is recommended for publication-grade runs, especially when the number of classes is large. A start that stops after far fewer iterations than the others has usually failed, which is a cheap warning sign; note however that on ISOLET most of the apparent spread across starts turned out to come from stopping at epsilon = 1e-4 rather than from local minima (the signed MU there is still improving after 20,000 iterations).

Bug fix: lambda.ortho no longer leaks into C.L1

nmfkc() accepts the deprecated names lambda (now C.L1) and lambda.ortho (now X.L2.ortho) by reading them out of .... Because $ partial-matches on lists, calling nmfkc(..., lambda.ortho = x) without lambda also set C.L1 <- x, so an orthogonality penalty silently became an L1 penalty of the same size. Both names are now matched exactly.

nmfkc.rff.beta.cv(): bandwidth selection for random-feature covariates

The counterpart of nmfkc.kernel.beta.cv() for Random Fourier Features: for each candidate \eqn{\beta} (default: the seven-point median-heuristic grid) and each candidate \eqn{D}, the features are regenerated with a fixed seed and the fit is cross-validated column-wise -- nmfkc.signed.cv() for signed cosine features (type = "signed"), nmfkc.cv() for positive random features (type = "positive"). Returns the selected beta, D and the objfunc matrix. sample.size runs the selection on a random subsample of the columns, so that for large \eqn{N} the choice is made cheaply and the final fit goes through the Gram route on all \eqn{N}. cores parallelizes over candidates with identical results. (Nyström covariates need no new function: nmfkc.kernel.beta.cv(Y, rank, U = landmarks, V = data) already cross-validates \eqn{\beta} for them.)

Positive random features: nmfkc.rff.positive() and nmfkc.rff.positive.gram()

Cosine Random Fourier Features take both signs, which forces the signed solver and breaks the NMF-LAB reading of \eqn{B = \Theta A} as memberships. Positive random features (Choromanski et al. 2021, the FAVOR+ construction, transported from the softmax to the Gaussian kernel) are a non-negative, unbiased feature map for the same kernel: with \eqn{\omega \sim N(0, I_p)}, \eqn{\phi(u) = \exp(\sqrt{2\beta},\omega^\top u - 2\beta\lVert u\rVert^2)} gives \eqn{E[\phi(u)\phi(u')] = \exp(-\beta\lVert u - u'\rVert^2)}.

predict.nmfkc.signed(type = "prob"): Euclidean projection onto the simplex

With signed covariates (Random Fourier Features) the scores \eqn{\bm b_n = C\bm a_n} can be negative, so NMF-LAB's membership rule -- normalize \eqn{\bm b_n} to sum one -- is not available. type = "prob" now maps the least-squares prediction \eqn{\widehat{\bm y}n = X\bm b_n} (whose entries sum to \eqn{\sum_q b{qn}}, close to one for one-hot targets) to the probability simplex by Euclidean projection, \eqn{\max(\widehat{\bm y}_n - \tau_n\bm 1, 0)} with \eqn{\tau_n} from the sorted coordinates (Duchi, Shalev-Shwartz, Singer & Chandra 2008; Wang & Carreira-Perpiñán 2013): the closest probability vector in the Frobenius geometry the fit minimizes, \eqn{O(P\log P)} per column. The previous clip-and-renormalize rule is kept as prob.method = "clip". type = "class" is unchanged (the argmax of \eqn{\widehat{\bm y}_n}, which both rules preserve), so no reported accuracy moves; only type = "prob" values change.

Large N without the D x N covariate matrix: Gram input for nmfkc() and nmfkc.signed()

The kernel designs of NMF-LAB (Satoh 2026, JJSD) so far needed the whole \eqn{D \times N} covariate matrix in memory -- Nystr"om covariates \eqn{C^\top} (\eqn{M \times N}) or Random Fourier Features (\eqn{D \times N}, which is \eqn{D/p} times the size of the data: a low-dimensional input with \eqn{N = 10^6}, \eqn{D = 2000} is a 16 GB matrix). The Euclidean multiplicative updates never needed it: they only use \eqn{S = AA^\top} (\eqn{D \times D}) and \eqn{G_0 = YA^\top} (\eqn{P \times D}). Two constructors now accumulate those two matrices over column blocks and return a "nmfkc.gram" object that the fitters accept in place of A.

NMF-GMM family reinstated, with a formula interface and a two-stage baseline

The nmf.gmm* family returns to develop (it was removed on 2026-08-26 while the accompanying paper's publication was undecided; the paper is now being submitted, so the family comes back unchanged -- the removal commit was reverted and the restored sources are byte-identical to the archived copies). Two additions on top of the reinstated family:

New: NMF-GMM family (nmf.gmm*)

nmfkc 0.9.6 (2026-08-25)

Check time only -- no change to any computed value

CRAN's incoming pretest still reported "Overall checktime 11 min > 10 min" for 0.9.5, almost all of it checking tests ... [442s]. Uwe Ligges suggested running the less important tests conditionally on an environment variable set only on the maintainer's machine, and that is what this release does.

Measured: 1.7 s in CRAN mode against 144 s in full mode, both with zero failures.

nmfkc 0.9.5

Check time only -- no change to any computed value

CRAN's incoming pretest rejected 0.9.4 for its overall check time (48 minutes on the pretest Windows machine against a 10-minute budget; 34 of those were the tests and 11 the vignettes).

nmfkc 0.9.4

Breaking: the nmf.rrr family drops the rank / rank.encoder aliases

Breaking: nmf.rrr() renames B.prob / B.cluster to B1.prob / B1.cluster

Breaking: nmfkc() stops computing criteria nothing consumed

summary() reports a \eqn{[0,1]} factor diagnostic

nmfre() correctness fixes (identification + signed warm start)

nmfkc 0.8.8 (2026-07-13)

Removed the B.L1 penalty (and its gamma alias)

MAP penalties for nmfre()

nmfae* deprecated in favour of nmf.rrr*

nmf.sem* deprecated in favour of nmf.ffb*

C.L2 ridge for the signed families

Basis penalties extended to the signed families

by option: grouping order of coefficient tables

Classed CV objects with print / plot

Naming / API consistency pass (aligned to the nmfkc house style)

X.L2.smooth: row-smoothness penalty on the basis

Public API: nmf.rrr and nmf.ffb are the documented names

X.init = "kmeans++" basis initialization

nmf.rrr / nmfae family: rank1 / rank2 arguments

nmfre.ecv: rank selection for NMF-RE

nmfkc.DOT: signed-coefficient graphs

nmfre: marginal-NLL convergence trace

nmfre: optimization and inference fully separated

nmfre: EM/ECM algorithm and sign-free fixed effects (paper port)

nmf.rrr: NMF-RRR names for the nmfae family

nmfae(): Kullback-Leibler divergence objective

nmfkc.inference(): re-fit wild bootstrap for singular information

nmfkc 0.8.2 (2026-06-14)

nmfkc.net.DOT(): default layout is now "neato"

Bug fix: nmfkc.net.DOT() mis-detected type = "bi" as "tri"

nmfkc.bicv() / nmfkc.consensus(): leaner signatures

nmfkc.ard(): simpler, safer interface

nmfkc.ard(): better default prior scale

New nmfkc.ard(): ARD rank determination (Tan & Fevotte 2013, prototype)

New nmfkc.consensus(): consensus-clustering rank selection (Brunet 2004)

New nmfkc.bicv(): bi-cross-validation for rank selection

*.rank: eff.rank.idx shown for context (no best marker)

*.rank: broken-stick-corrected effective-rank index

*.rank results gain plot() / print() methods

New nmf.cluster.flow(): cluster-flow diagram across ranks

New nmf.cluster.criteria(): sample-clustering quality across ranks

Rank-selection functions for the other NMF families

Internal: shared element-wise CV helpers

Unified summary print blocks

Effective Rank in all five MU-family summaries

Rank-selection diagnostics: silhouette / CPCC fixed, IC removed

Breaking change: symmetric NMF removed from nmfkc()

New diagnostic: effective rank

Diagnostics cleanup: B.prob crispness metrics

Improvements

Bug Fixes

Documentation

nmfkc 0.7.3 (2026-05-13)

Documentation

nmfkc 0.7.2

Headline: NMF-FFB rebrand and full bootstrap inference

Bug Fixes

Improvements

New Functions (Signed NMF family)

New Functions (Network NMF family)

Deprecations

Parameter Renames (old names remain usable for backward compatibility)

Documentation

nmfkc 0.6.7 (2026-04-15)

Bug Fixes

Naming Unification (old names remain usable for backward compatibility)

nmfkc 0.6.6

New Functions

Parameter Renames (old names remain usable for backward compatibility)

Other Improvements

nmfkc 0.6.0

Bug Fixes

Documentation

Code Quality

nmfkc 0.5.8

Graphviz DOT Output Consolidation and Cleanup