Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Existing regression expectations still fail, and invalid non-integer factor requests must be rejected before coercion.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Updates statistical.factor.model() to support universes with more assets than observations by validating feasible factor counts.
Changes:
- Replaces the
m < Nrejection with a data-supportedkbound. - Adds wide-universe, boundary, covariance, and regression tests.
- Verifies unchanged behavior for tall datasets.
File summaries
| File | Summary |
|---|---|
tests/testthat/test-factor-model-wide.R |
Adds wide-universe and factor-boundary coverage. |
R/stat.factor.model.R |
Updates factor-count validation. |
Review details
Suppressed comments (1)
R/stat.factor.model.R:70
- The new bound is applied after
khas been truncated byas.integer(), so a non-integer request such ask = 0.5becomes0, passesk > max_k, and produces anstfmwhosekis zero even though1:kstill selects a component later (makingextractCovariance()returnNULL). Validate that the original value is a finite positive integer before coercing; otherwise this guard does not enforce the contract stated by the existing error message.
max_k <- min(m - 2L, N)
if(max_k < 1L)
stop("at least three observations are needed to fit a factor model")
if(k > max_k)
stop(sprintf(paste("k = %d is more factors than the data supports:",
"%d observations and %d assets give at most %d"),
k, m, N, max_k))
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🔵 Needs a closer look
Add coverage for the asset-count (N) upper bound, including k = N and k = N + 1.
Review details
Suppressed comments (1)
tests/testthat/test-factor-model-wide.R:37
- The new
min(m - 2L, N)check'sNlimit is not exercised by this test: withwide(m = 5, N = 8), every assertion hasmax_k = 3, so evenk = 99only verifies the observation bound. Add a tall fixture withm - 2 >= Nand assert thatk = Nsucceeds whilek = N + 1receives the new validation error; otherwise regressions in the asset-count bound can pass.
test_that("k is bounded by the components the data supports, not by N", {
m <- nrow(wide)
# min(m - 2, N) = 3 here. extractCovariance() divides by m - k - 1, so
# k = m - 1 would return an infinite covariance instead of an error.
expect_no_error(statistical.factor.model(wide, k = m - 2L))
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Address the square-case compatibility change and clarify the user-facing error message.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
R/stat.factor.model.R:69
- This is a user-facing error message, but “k = %d is more factors” is grammatically incorrect and makes the failure harder to read. Please phrase it as a request/excess, for example “k = %d requests more factors than the data supports”.
R/stat.factor.model.R:64
- This bound also rejects a previously successful square case: with m = N and k = N, the old guard allowed the full PCA basis, which reconstructs x and makes extractCovariance() return the finite sample covariance (the residual numerator is zero even though the denominator is -1). That is different from the documented k = m - 1 zero-denominator/Inf case, so either preserve this compatibility case or explicitly document and test this additional breaking change.
max_k <- min(m - 2L, N)
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
Thanks for submitting a well formatted pull request. I understand what you are trying to do, and what you want. That said, just because you CAN do something doesn't mean that you SHOULD. The check is there for a reason, and follows most standard statistical guidance. Fewer observations than variables introduces much higher risks of spurious correlations looking significant, and of stationarity risks already present in financial time series being even worse. We could discuss adding an option (default FALSE) to override the current restriction, but there is no way we're going to remove it without guardrails. Adding that option would also need to come with roxygen documentation updates that discuss the option, why it is FALSE by default, and some curated guidance on when/why you would set it to TRUE, what the risks are, and citations for that guidance on both sides of the issue. Happy to continue discussing. |
statistical.factor.model() refuses to fit whenever m < N. That is the one shape a statistical factor model is normally reached for -- a universe wider than its history -- but the restriction is sound guidance and stays the default. This adds a way past it for callers who can say why. statistical.factor.model(R, k = 1, ..., allow.fewer.obs = FALSE) With the default, nothing changes: the same check, the same message, the same results, and tests/testthat/test-stat-factor-model.R is untouched and still passes unchanged on this branch. With allow.fewer.obs = TRUE the fit proceeds and k is bounded by m - 2, which is where the arithmetic binds: extractCovariance() divides the residual sums of squares by m - k - 1, so k = m - 1 would return an infinite covariance rather than an error. The bound lives inside the m < N branch, so the m >= N path is not touched in any respect. The number of assets does not enter it, because on this path there are more assets than observations. The documentation carries what the option costs, as asked: why it is FALSE by default, what a short wide panel does to sample second moments, when lifting the restriction is defensible, and citations on both sides. For: Connor and Korajczyk (1986, 1988), whose asymptotic principal components are built for a cross-section that grows against a fixed time series; Bai and Ng (2002) for choosing k in that regime; Fan, Liao and Mincheva (2013) for the covariance estimator built on it. Against: Michaud (1989) on error maximization, Ledoit and Wolf (2004) and Fan, Fan and Lv (2008) on why regularization exists, and DeMiguel, Garlappi and Uppal (2009) on losing to equal weighting out of sample. The section says plainly that the covariance is full rank by construction, which is not the same as being well estimated. Tests are a new file. Ten tests pin both sides: that the default refuses with the message it always had, with the argument absent and with it FALSE; that the opt-in path fits, and is bounded, and that every admissible k returns a finite covariance; that a positional third argument still reaches prcomp(); and that the tall and square cases are identical with the option set either way. No existing test is modified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aa9302a to
dd7b197
Compare
|
That is a fair objection, and the option you describe is the better design. I With the default nothing changes: the same check, the same message, the same With the override the fit proceeds and k is bounded by m - 2, inside the m < N The Details section carries what you asked for: why the restriction is the Deliberately left alone: everything on the m >= N path, including k = m - 1 The argument name is a guess at your conventions. Happy to rename it, or to |
|
Thanks for the updates here, and thanks for the rigor on the references. Once we get clean tests in the pipeline, I'll run it myself and merge. |
Reworked around the option you described, as a single commit. The original
version of this pull request removed the check; this one leaves it in place and
adds a way past it.
With the default, nothing changes. The same check, the same message, the
same results.
tests/testthat/test-stat-factor-model.Ris untouched and passesunchanged on this branch — that file is the evidence, and modifying it was the
thing the earlier version could not avoid.
With the override the fit proceeds and
kis bounded bym - 2. That iswhere the arithmetic binds:
extractCovariance()divides the residual sums ofsquares by
m - k - 1, sok = m - 1returns an infinite covariance ratherthan an error. The bound lives inside the
m < Nbranch, so them >= Npathis not touched in any respect. The number of assets does not enter it, because
on this path there are more assets than observations.
Documentation
A
Detailssection covering, in order: why the restriction is the default,what a short wide panel does to sample second moments, when lifting it is
defensible, and what it costs. References on both sides.
For: Connor and Korajczyk (1986, 1988), whose asymptotic principal components
are built for a cross-section that grows against a fixed time series, which is
the opposite of the usual asymptotics and the reason the method exists; Bai and
Ng (2002) for choosing
kin that regime; Fan, Liao and Mincheva (2013) forthe covariance estimator built on top of it.
Against: Michaud (1989) on error maximization, Ledoit and Wolf (2004) and Fan,
Fan and Lv (2008) on why regularization exists, DeMiguel, Garlappi and Uppal
(2009) on losing to equal weighting out of sample.
The section says plainly that the covariance returned is full rank by
construction, which is not the same as being well estimated, and that the
option is not there to obtain an invertible matrix from data that does not
support one.
Deliberately not in this change
Anything on the
m >= Npath, includingk = m - 1there, which still returnsInf. And the truncation of a non-integerkbyas.integer(), which behavesthe same on
master:k = 0.5becomes0andextractCovariance()returnsNULL. Both are separate decisions.Tests
A new file,
tests/testthat/test-factor-model-wide.R, with ten tests pinningboth sides: the default refuses with the message it always had, with the
argument absent and with it
FALSE; the opt-in path fits, is bounded atm - 2, and returns a finite covariance at every admissiblek; too fewobservations for any factor are refused;
k <= 0is refused first; apositional third argument still reaches
prcomp(); and the tall and squarecases are identical with the option set either way.
No existing test is modified.
test-stat-factor-model.R,mastertest-stat-factor-model.R, this branchtest-factor-model-wide.R,mastertest-factor-model-wide.R, this branchTest suite
The full
testthatrun on this branch is 3578 passing, with 2 failures and 2 errors, against 3550 passing onmaster, with the same two failures and two errors on both sides:chart.EfficientFrontierCompareintest-charts-roi.Randobjective()intest-objective-branches.R. Neither file is touched here.Scope
Three files, 299 insertions, 3 deletions: the function, its generated
.Rd,and the new test file.
The argument name is a guess at your conventions. Happy to rename it, or to
change what the default's message says, if you would prefer something else.
🤖 Generated with Claude Code