Skip to content

fix(basis): B-spline LS round-trip transpose + point-wise nbasis CV (GH #33) - #36

Merged
sipemu merged 1 commit into
mainfrom
fix/gh-33-bspline-basis
Aug 7, 2026
Merged

fix(basis): B-spline LS round-trip transpose + point-wise nbasis CV (GH #33)#36
sipemu merged 1 commit into
mainfrom
fix/gh-33-bspline-basis

Conversation

@sipemu

@sipemu sipemu commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Fixes #33.

Two independent defects in the 1-D B-spline basis path.

Bug 1 — fdata_to_basis / basis_to_fdata transpose the result for n > 1

Both functions built a curve-major flat buffer ([i*K + k], [i*m + j]) and passed it to FdMatrix::from_column_major, which reads it column-major ([row + col*n]). For a single curve the two layouts coincide (why a smooth 1-curve round-trip was perfect), but for multiple curves the coefficients and reconstruction were transposed/scrambled — hence residuals worse than a constant fit, flat in n_basis, and outside the data range on the 35×365 weather matrix. The least-squares solve (BᵀB)⁻¹Bᵀ itself is correct.

Fix: collect per-curve rows and scatter them into the matrix via [(i, k)] / [(i, j)] indexing (column-major-correct), preserving parallelism.

Bug 2 — basis_nbasis_cv (Cv) always selects the maximum n_basis

evaluate_nbasis_cv fit each fold's training curves but threw those coefficients away, then projected every test curve onto the basis using its own data and scored it against itself — a pure in-sample residual with no hold-out, monotone-decreasing in n_basis. (Leaving out whole curves is ill-posed for per-curve basis fitting: a held-out curve can't be predicted from other curves' coefficients.)

Fix: cross-validate over time points — fit coefficients on the retained points, predict the held-out points, score the prediction error. This is a genuine predictive criterion that penalizes overfitting and shows an interior minimum. The GCV/AIC/BIC paths were already correct and are unchanged.

Tests

  • test_bspline_roundtrip_multi_curve_is_least_squares: multi-curve round-trip beats a constant fit, improves with n_basis, stays in range.
  • test_basis_nbasis_cv_penalizes_overfitting: CV no longer selects the max n_basis (and scores aren't monotone) on smooth+noise data.
  • Full basis (149) + smooth_basis (106) + R-validation (173) + validate_new_modules (56) pass; clippy --all-targets + rustfmt clean.

🤖 Generated with Claude Code

#33)

Bug 1 — fdata_to_basis / basis_to_fdata transposed results for n > 1 curves.
Both built a curve-major (row-major) flat buffer and passed it to
FdMatrix::from_column_major, which reads it column-major — so for multiple
curves the coefficients/reconstruction were scrambled. n = 1 coincides
(row==column major), which is why a single smooth curve round-tripped
perfectly while a 35-curve matrix gave residuals worse than a constant fit,
non-monotone in n_basis, and outside the data range. The least-squares solve
itself is correct. Fix: collect per-curve rows and scatter them into the
matrix via [(i, k)] / [(i, j)] indexing.

Bug 2 — basis_nbasis_cv (Cv criterion) always selected the maximum n_basis.
evaluate_nbasis_cv fit each fold's training curves but discarded those
coefficients, then projected every TEST curve onto the basis using its own
data and scored it against itself — an in-sample residual with no hold-out,
monotone decreasing in n_basis. Leaving out curves is ill-posed for per-curve
basis fitting anyway. Fix: cross-validate over TIME POINTS — fit on the
retained points, predict the held-out points — a genuine predictive score
that penalizes overfitting and shows an interior minimum. (GCV/AIC/BIC were
already correct and are unchanged.)

Regression tests: multi-curve B-spline round-trip beats a constant fit and
improves with n_basis while staying in range; CV no longer selects the max
n_basis on smooth+noise data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sipemu
sipemu merged commit 2fb6d3c into main Aug 7, 2026
8 checks passed
@sipemu
sipemu deleted the fix/gh-33-bspline-basis branch August 7, 2026 10:37
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.57143% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.02%. Comparing base (fca0a0e) to head (e768b72).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
fdars-core/src/smooth_basis.rs 98.14% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main      #36   +/-   ##
=======================================
  Coverage   89.01%   89.02%           
=======================================
  Files         200      200           
  Lines       44099    44124   +25     
=======================================
+ Hits        39255    39281   +26     
+ Misses       4844     4843    -1     
Flag Coverage Δ
rust 89.02% <98.57%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
fdars-core/src/basis/projection.rs 95.76% <100.00%> (+0.34%) ⬆️
fdars-core/src/smooth_basis.rs 97.59% <98.14%> (+0.07%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

B-spline basis path: basis_to_fdata_1d is not a least-squares fit; basis_nbasis_cv GCV always picks max n_basis

1 participant