Non-Negative Matrix Factorization with Kernel Covariates
Overview
Performs Non-negative Matrix Factorization (NMF) with Kernel Covariates. Given an observation matrix and kernel covariates, it optimizes both a basis matrix and a parameter matrix. Notably, if the kernel matrix is an identity matrix, the method simplifies to standard NMF. Also provides NMF with Random Effects (NMF-RE) via nmfre(), which estimates a mixed-effects model combining covariate-driven scores with unit-specific random effects together with wild bootstrap inference, and NMF-based Structural Equation Modeling (NMF-SEM) via nmf.sem(), which fits a two-block input-output model for blind source separation and path analysis. References: Satoh (2025) doi:10.48550/arXiv.2403.05359; Satoh (2026) doi:10.1007/s42081-026-00349-x; Satoh (2025) doi:10.48550/arXiv.2512.18250; Satoh (2026) doi:10.48550/arXiv.2603.01468; Satoh and Tokuda (2026) doi:10.48550/arXiv.2607.27474; Satoh (2026) doi:10.1007/s42081-025-00314-0.
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
Show all changes in 0.9.8Hide the rest of the changes in 0.9.8
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.
- The row-sum floor pushed the pinned rows below the floor and the projection lifted them back, so the objective alternated between two values until
maxit(observed on Covertype at rank 6, 200,000 iterations). "rowSums"has the same defect in a milder form: on a 10x30 example the objective rose at 45 steps, every second step from the 49th on. The oscillation is small enough that the fit still meetsepsilonand stops, but it descends by luck rather than by construction.
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.
nmf.ffb(C1.L1 = , C2.L1 = )had **no effect at all** under the defaultmethod = "fiml": the arguments were accepted and never forwarded. They belong to the multiplicative updates; the likelihood path penalizesC1alongC1.L1.pathand leavesC2unpenalized. Passing them to a fiml fit now warns. Same innmf.ffb.inference().fit$maxitheld the **stage-2** cap while themaxitargument set the **stage-1** one, andprint()paired stage 2's iteration count with stage 1'sepsilon(87 / 3000 epsilon = 1e-06-- three numbers from two different optimizers).iter,maxit,epsilonnow describe stage 1, as the arguments of those names do; stage 2 isfiml.iter,fiml.maxit,factr,fiml.converged; andconvergedisTRUEonly if both stages converged.print()shows both.plot()on amethod = "fiml"fit failed insidesprintf(). A fiml fit has no iteration trace to draw (its optimizer is L-BFGS-B), so it now says so and names what to use instead.
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 tutorial was
vignettes/nmf-sem-with-nmfkc.Rmdand the engine fileR/nmf.sem.R; they are nownmf-ffb-with-nmfkc.RmdandR/nmf.ffb.R; - **the six S3 methods were registered on
nmf.sem, not onnmf.ffb** --summary(),plot(),coef(),fitted(),residuals()andprint.summary()-- so the deprecated name was the one dispatch resolved against and?summary.nmf.semwas the page a user landed on. They are now defined onnmf.ffb, and thenmf.semmethods are one-line aliases collected at the end ofR/nmf.sem-deprecated.R, so that removingnmf.semlater means removing one file. An object saved by a version that wrote onlyc("nmf.sem", "nmf"), and a summary object of class"summary.nmf.sem", still dispatch; nmf.ffb.DOT()tagged its resultc("nmf.sem.DOT", "nmfkc.DOT"); the leading class is now"nmf.ffb.DOT".
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:
```r
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
```
- **New**
nmf.ffb.test(): the calibrated test of the feed-forward null. It runs the null bootstrap only, and returnsLR.p.boot, the null quantiles, the null false-selection rateprob.select.nulland, for the default calibration,C1.restriction.change.rate. Replicates run in parallel withcores =as elsewhere in the package. - **New**
nmf.ffb.ecv(): choosing *Q* under the name that says what it does.nmf.ffb.cv(method = "fiml")has delegated to element-wise CV since 0.9.8; the old name still works and is kept for the multiplicative-update path. - **Breaking**
nmf.ffb.inference()no longer runs the null bootstrap and no longer returnsLR.boot,LR.p.boot,LR.null.quantile,prob.select.null,LR.boot.*,C1.restriction.change.rate,split.tableorbootstrap.calibration; itscalibrationandnsplitarguments are gone. Usenmf.ffb.test(). An intervals object that also carried a *p*-value for the presence of feedback invited the reader to treat an interval that excludes zero as evidence for the entry, which it is not. This affects only the likelihood branch (method = "fiml"), which was added after the last release. nmf.ffb.diagnostics()now also reports the three best distinct supports with their differences in BIC, the entries common to all of them, their envelope, and whether they form a chain under inclusion. A difference in BIC below about 2 is not evidence for one support over another (Kass and Raftery 1995), so the presence of feedback can be settled while its composition is not.
Options removed
Measurement, not taste, decided each of these; keeping them invited the reader
to compare procedures as if they were equally valid.
nmf.ffb(starts = )is gone. Every penalized fit is warm-started from the unpenalized full-feedback fit. The three alternative starting points measured on six data sets never uniquely attained the minimum BIC.calibration(now an argument ofnmf.ffb.test()) keeps"procedure"(the default: stage 1 and the exclusion restriction re-estimated in every null replicate) and"conditional"(valid only when the basis and the restriction come from outside the data being tested, and selected automatically in that case). The two sample-splitting levels are gone:"split"fixes the basis of the estimation half and is anti-conservative, and"split-full"is valid but strictly dominated by"procedure"-- same size, lower power, and it needs a large *N*.C1.restrictionkeeps"union"(an outcome may not feed back into a factor on which it loads) and"none", plus a user-supplied matrix. The partial rules"block"and"cross"were kept for comparison and are neither the rule of the paper nor useful on their own.
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:
"procedure"(default): stage 1 and the restriction are re-estimated on every replicate (onenmfkc()fit per replicate), so the p-value is the operating characteristic of the whole exploratory procedure. ReturnsC1.restriction.change.rate, the share of replicates whose restriction moved, andLR.boot.df."conditional": stage 2 only, basis and restriction fixed (the pre-0.9.8 behaviour). Valid only if they came from data independent ofY1; labelled as conditional inprint(), and selected automatically when the fit used a basis or a restriction supplied by the caller.
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)
`C
The NEWS section is longer than shown here.
From NEWS.md in the release
Install
Health
- OK13 OK2026-08-05
- NOTE12 OK, 1 NOTE2026-08-01
- OK3 OK2026-04-16
Documentation
- Help pages
- 147 (15 internal)
- Examples that run
- 59%46 run in a plain check · 32 only when donttest examples are enabled (R CMD check --as-cran)
- Exported functions with examples
- 85%
- Documented parameters
- 93%
- Return-value docs
- 100%
- Help pages citing references
- 29%
Downloads
Repository
Stars over time
Forks over time
Releases over time
Commits over time
6 development-tooling and community-health practices detected across 4 families in the upstream repository
Checks run against github.com/ksatohds/nmfkc on 2026-10-21.
This repository shows signs of AI coding tools.
- Claude Codefirst seen 2026-02-27Named in .Rbuildignore: .claude, CLAUDE.mdNamed in .gitignore: .claudeCommits: at least 307 crediting it in the message
- Claude Sonnet 4.656 commits · Mar to Apr 2026
- Claude Opus 4.644 commits · Feb to Mar 2026
Some of these counts are a minimum, because the search stopped before the oldest commits.
Dependencies
Installing nmfkc adds no packages beyond those that come with R.
Code & Tests
People & History
Pick two releases to compare their code metrics.
- 0.9.8Latest2026-09-22 · current release · diff ↗ ·
removed 20 exports
nmf.sem, nmf.sem.DOT, nmf.sem.cv, nmf.sem.inference, nmf.sem.split, nmfae, nmfae.DOT, nmfae.cv, nmfae.ecv, nmfae.heatmap, nmfae.inference, nmfae.kernel.beta.cv, nmfae.rank, nmfae.rename, nmfae.signed, nmfae.signed.ecv, nmfae.signed.heatmap, nmfae.signed.inference, nmfae.signed.rank, nmfae.signed.renameadded 12 exports
nmf.ffb.diagnostics, nmf.ffb.ecv, nmf.ffb.test, nmf.gmm, nmf.gmm.inference, nmf.gmm.select, nmf.gmm.twostage, nmfkc.kernel.gram, nmfkc.rff.beta.cv, nmfkc.rff.positive, nmfkc.rff.positive.gram, nmfkc.signed.rff.gram - 0.9.6
- 0.8.82026-07-13 · diff ↗ ·
removed 1 export
nmfre.dfU.scanadded 16 exports
nmf.rrr, nmf.rrr.DOT, nmf.rrr.cv, nmf.rrr.ecv, nmf.rrr.heatmap, nmf.rrr.inference, nmf.rrr.kernel.beta.cv, nmf.rrr.rank, nmf.rrr.rename, nmf.rrr.signed, nmf.rrr.signed.ecv, nmf.rrr.signed.heatmap, nmf.rrr.signed.inference, nmf.rrr.signed.rank, nmf.rrr.signed.rename, nmfre.ecv - 0.8.22026-06-14 · diff ↗ ·
added 10 exports
nmf.cluster.criteria, nmf.cluster.flow, nmfae.rank, nmfae.signed.rank, nmfkc.ard, nmfkc.bicv, nmfkc.consensus, nmfkc.net.rank, nmfkc.signed.rank, nmfkc.signed.rff - 0.7.32026-05-13 · diff ↗ ·
added 17 exports
nmf.ffb, nmf.ffb.DOT, nmf.ffb.cv, nmf.ffb.inference, nmf.ffb.split, nmfae.signed, nmfae.signed.ecv, nmfae.signed.heatmap, nmfae.signed.inference, nmfae.signed.rename, nmfkc.net, nmfkc.net.DOT, nmfkc.net.ecv, nmfkc.net.inference, nmfkc.signed, nmfkc.signed.cv, nmfkc.signed.ecv - RR 4.6.0 released · 2026-04-24
- 0.6.72026-04-15
- RR 4.5.0 released · 2025-04-11
Package metadata
- First published
- 2026-04-15
- Total releases
- 6 / 1 yrs
- License
- MIT + file LICENSE OSI
- Download size
- 545 KB
Cite
Cite this package
Run in R for the authors' entry:
citation("nmfkc")The package provides a citation file, so citation() returns the authors' own entry.
It points to 10.1007/s42081-026-00349-x, 10.1007/s42081-025-00314-0, 10.5023/jappstat.52.59.
Cite the R Observatory
For a number measured here: a download total, a coverage figure, an archival date.
From data release v2026-10-25, which the citation names so these numbers can be found later. More on citing and the projects behind them.