Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
fe71033
Add implementation plan and prototype for complex generalised normal …
config-i1 Sep 25, 2026
1a344df
Plan: record decisions, add IC parameter-count analysis
config-i1 Sep 25, 2026
b2389a2
Count all real parameters in AIC/BIC of clm; add multivariate AICc/BICc
config-i1 Sep 25, 2026
4fbdc96
Plan: correct note on BJsales order difference
config-i1 Sep 25, 2026
ecb4120
Bump version to 1.0.3, regenerate NAMESPACE with roxygen2 8.1.0
config-i1 Sep 25, 2026
5c1a21e
Update repository links to openforecast-org/complex
config-i1 Sep 25, 2026
3158b54
Add complex generalised normal distribution (dcgnorm) and use it in c…
config-i1 Sep 25, 2026
d4f8d7c
clm: accept a starting shape for dcgnorm as the last element of B; fi…
config-i1 Sep 25, 2026
9d1eff3
Fix cARIMA without intercept: MA sign, forecasts, AR checks, empty mo…
config-i1 Sep 25, 2026
c048948
Add carima(): pure complex ARIMA wrapper of clm() with msarima-like i…
config-i1 Sep 25, 2026
2f037f7
Add auto.carima() and stationarity/invertibility penalty in clm()
config-i1 Sep 26, 2026
4f1fcaf
Fix clm() starting values for cARIMA(0,0,q) without intercept (1:0 in…
config-i1 Sep 26, 2026
b7ccaa4
clm(): stop with an explanation for loss="CLS" with MA terms
config-i1 Sep 26, 2026
46f1ee0
clm(fast=TRUE): Hannan-Rissanen starting values for pure cARIMA
config-i1 Sep 26, 2026
f258cbb
auto.carima(): Hannan-Rissanen starts by default; fast=TRUE screening
config-i1 Sep 26, 2026
c093f8b
Add CLAUDE.md with the package layout, workflow and internals
config-i1 Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ man-roxygen
^revdep$
cran-comments.md
^CRAN-SUBMISSION$
^dev$
^CLAUDE\.md$
92 changes: 92 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# complex (R package)

Complex-valued econometrics and forecasting: complex linear models, complex ARIMA (cARIMA), complex
distributions, complex correlations. Companion to Svetunkov & Svetunkov (2024), *Complex-Valued
Econometrics with Examples in R* (Springer); Chapter 6 covers the dynamic models.
Author/maintainer: Ivan Svetunkov. Depends on greybox and legion (which loads smooth).

## Repository

- `git@github.com:openforecast-org/complex.git` (moved from config-i1/complex). Always use ssh.
There is no ssh in the Claude container, so the user does push/fetch.
- Branch `dcgnorm`: development of v1.0.3 (not yet merged to `master`). Commit as
`Ivan Svetunkov <ivan@svetunkov.com>` (`git -c user.name=... -c user.email=...`).
- Used by the wind forecasting paper project: `~/R/Projects/Experiments/cARIMA`
(git@github.com:config-i1/complex-wind.git), which has its own CLAUDE.md.

## Layout

| file | content |
|---|---|
| `R/clm.R` | `clm()` (complex linear model and cARIMA(X)) and its methods: `logLik`, `nparam`, `AICc/BICc`, `clmIC()`, `vcov`, `confint`, `summary`, `predict`, `plot` |
| `R/carima.R` | `carima()` pure cARIMA wrapper of `clm()` (msarima-like interface), `forecast.carima`, `carimaFinalise()` |
| `R/autoCarima.R` | `auto.carima()` order selection (stepwise / full / fast) |
| `R/hannanRissanen.R` | Hannan-Rissanen estimation (internal): starting values and screening |
| `R/cnorm.R`, `R/cgnorm.R` | complex normal; complex generalised normal (`dcgnorm`, `rcgnorm`, internal `cgnormConcentrated`) |
| `R/cacf.R`, `R/cvar.R` | complex ACF/PACF, direct/conjugate variances and correlations |
| `R/cTransformations.R` | `cscale`, `cdescale`, `clog`, `cexp` |
| `src/complexCode.cpp` | Rcpp: `invert`, `polyprodcomplex` |
| `tests/testthat/` | tests (none existed before v1.0.3) |
| `dev/` | plans and prototypes (in `.Rbuildignore`); `dev/PLAN-dcgnorm.md` has the design decisions |

## Workflow

```bash
cd ~/R/Projects/Packages/complex
Rscript -e 'roxygen2::roxygenise()' # roxygen2 8.1.0; never edit NAMESPACE/man by hand
R CMD INSTALL . # installs into the user library
Rscript -e 'library(complex); testthat::test_dir("tests/testthat", reporter="summary")'
R CMD build --no-build-vignettes . && R CMD check --no-manual --ignore-vignettes complex_*.tar.gz
```
- Build and check in a scratch directory, not in the package folder.
- Expected check result: `Status: 1 WARNING`, the "OS reports request to set locale to en_US.UTF-8"
warning of this machine. Anything else must be fixed.
- Add a NEWS entry (Changes / Bugfixes) for every user-visible change.
- `src/*.o` and `src/complex.so` are tracked in git (historical). Do not commit changes to them.

## Code style

Match the existing code: 4-space indentation, `;` at the end of statements, `if(...){ ... }` with
`else{` on the next line after `}`, camelCase names, `<-`, comments describing why. Use
`seq_len(n)` rather than `1:n`: several bugs here came from `1:0` selecting the first element.

## clm() internals worth knowing

- Parameters `B` are complex (intercept, regressors, AR, MA). The optimiser (nloptr, SBPLX by default)
works on `c(Re(B), Im(B))`; for `distribution="dcgnorm"` with estimated shape, log(shape) is
appended at the end. A starting shape can be passed as the last element of `B`.
- The ARI part is stored via the polynomial `(1-phi(B))(1-B)^d`; `fitter()` builds the full
parameter vector. Initial lags are extrapolated (`xregExpander(gaps="auto")`), initial MA errors
are zero; all observations enter the likelihood.
- Likelihood `dcnorm`: concentrated, `-T(log 2pi + 1 + 0.5 log det Sigma)`. `dcgnorm`: circularity
from moments, scale by exact ML given it (approximate ML for shape != 1).
- The loss returns `1E+100/min|root|` if the complex AR or MA polynomial has a root inside the unit
circle (differencing is not checked).
- `nparam()` counts per series (complex units: covariance 3/2, shape 1/2). `logLik` df and the ICs
use all real parameters (2*nparam); `clmIC()` has the formulas (AICc/BICc: Bedrick & Tsai with
two series, approximate for the restricted complex model).
- `fast=TRUE`: skips data checks and, for pure cARIMA, starts from Hannan-Rissanen estimates.
- `vcov.clm` re-estimates the model from `object$call` with `parameters` fixed and `FI=TRUE`; the
MA columns of `object$data` then act as regressors, so the parameter vector does not match the
orders there. Keep `object$call` a clm call (carima stores its own call in `carimaCall`).
- `loss="CLS"` cannot be used with MA (complex-valued loss).

## auto.carima()

- stepwise (default): for each d, neighbourhood search over p, q (IC only); constant included for
d=0, excluded for d>0, then flipped for the best model of each d and the search continued.
- Every candidate starts from Hannan-Rissanen and from the closest estimated model.
- ICs are computed on the common sample without the first `ar+i+ma` observations (models are
estimated on the full sample).
- `fast=TRUE`: Hannan-Rissanen screening of all models, likelihood refit of the best per
(d, constant, q), then neighbourhood search. About 3x faster, but approximate (least squares
underrates MA models with heavy tails).
- `search="full"`: all combinations.

## Open items

- Phase 3/4/5 of `dev/PLAN-dcgnorm.md`: joint Hessian with the shape, simulated prediction
intervals, dcgnorm diagnostics.
- Seasonal cARIMA is not supported.
- A complex portmanteau / partial autocorrelation statistic combining direct and conjugate ACFs
(discussed, not implemented).
13 changes: 7 additions & 6 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
Package: complex
Type: Package
Title: Time Series Analysis and Forecasting Using Complex Variables
Version: 1.0.2
Date: 2026-02-03
Version: 1.0.3
Date: 2026-09-25
Authors@R: person("Ivan", "Svetunkov", email = "ivan@svetunkov.com", role = c("aut", "cre"),
comment="Lecturer at Centre for Marketing Analytics and Forecasting, Lancaster University, UK")
URL: https://github.com/config-i1/complex
BugReports: https://github.com/config-i1/complex/issues
comment = c("Director of the OpenForecast consultancy company, UK",
ORCID = "0000-0001-7826-0281"))
URL: https://github.com/openforecast-org/complex
BugReports: https://github.com/openforecast-org/complex/issues
Language: en-GB
Description: Implements the instruments for complex-valued modelling,
including time series analysis and forecasting. This is based on the monograph
Expand All @@ -27,6 +28,6 @@ Suggests:
testthat,
knitr,
rmarkdown
RoxygenNote: 7.3.2
Encoding: UTF-8
Roxygen: list(old_usage = TRUE)
Config/roxygen2/version: 8.1.0
138 changes: 82 additions & 56 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Generated by roxygen2: do not edit by hand

S3method(AICc,clm)
S3method(BICc,clm)
S3method(actuals,clm)
S3method(confint,clm)
S3method(forecast,carima)
S3method(logLik,clm)
S3method(nobs,clm)
S3method(nparam,clm)
Expand All @@ -10,11 +13,15 @@ S3method(plot,clm)
S3method(plot,predict.clm)
S3method(predict,clm)
S3method(print,cacf)
S3method(print,carima)
S3method(print,summary.clm)
S3method(sigma,clm)
S3method(summary,clm)
S3method(vcov,carima)
S3method(vcov,clm)
export(auto.carima)
export(cacf)
export(carima)
export(ccor)
export(ccov)
export(ccov2cor)
Expand All @@ -29,70 +36,89 @@ export(cpacf)
export(cplot)
export(cscale)
export(cvar)
export(dcgnorm)
export(dcnorm)
export(invert)
export(mat2complex)
export(pcnorm)
export(qcnorm)
export(rcgnorm)
export(rcnorm)
export(vec2complex)
import(legion)
importFrom(grDevices,devAskNewPage)
importFrom(grDevices,grey)
importFrom(grDevices,palette)
importFrom(graphics,abline)
importFrom(graphics,axis)
importFrom(graphics,box)
importFrom(graphics,layout)
importFrom(graphics,lines)
importFrom(graphics,mtext)
importFrom(graphics,par)
importFrom(graphics,points)
importFrom(graphics,text)
importFrom(greybox,AICc)
importFrom(greybox,BICc)
importFrom(greybox,actuals)
importFrom(greybox,measures)
importFrom(greybox,nparam)
importFrom(greybox,xregExpander)
importFrom(mvtnorm,dmvnorm)
importFrom(mvtnorm,pmvnorm)
importFrom(mvtnorm,qmvnorm)
importFrom(mvtnorm,rmvnorm)
importFrom(grDevices,
devAskNewPage,
grey,
palette
)
importFrom(graphics,
abline,
axis,
box,
layout,
lines,
mtext,
par,
points,
text
)
importFrom(greybox,
AICc,
BICc,
actuals,
forecast,
measures,
nparam,
xregExpander
)
importFrom(mvtnorm,
dmvnorm,
pmvnorm,
qmvnorm,
rmvnorm
)
importFrom(nloptr,nloptr)
importFrom(pracma,hessian)
importFrom(stats,"contrasts<-")
importFrom(stats,AIC)
importFrom(stats,BIC)
importFrom(stats,as.formula)
importFrom(stats,cmdscale)
importFrom(stats,coef)
importFrom(stats,confint)
importFrom(stats,cor)
importFrom(stats,cov)
importFrom(stats,deltat)
importFrom(stats,dist)
importFrom(stats,fitted)
importFrom(stats,formula)
importFrom(stats,frequency)
importFrom(stats,logLik)
importFrom(stats,model.frame)
importFrom(stats,model.matrix)
importFrom(stats,nobs)
importFrom(stats,pacf)
importFrom(stats,qt)
importFrom(stats,resid)
importFrom(stats,residuals)
importFrom(stats,rnorm)
importFrom(stats,sd)
importFrom(stats,setNames)
importFrom(stats,sigma)
importFrom(stats,start)
importFrom(stats,terms)
importFrom(stats,time)
importFrom(stats,ts)
importFrom(stats,update.formula)
importFrom(stats,vcov)
importFrom(utils,packageVersion)
importFrom(utils,tail)
importFrom(stats,
"contrasts<-",
AIC,
BIC,
as.formula,
cmdscale,
coef,
confint,
cor,
cov,
deltat,
dist,
fitted,
formula,
frequency,
logLik,
model.frame,
model.matrix,
nobs,
optimize,
pacf,
predict,
qt,
resid,
residuals,
rgamma,
rnorm,
runif,
sd,
setNames,
sigma,
start,
terms,
time,
ts,
update.formula,
vcov
)
importFrom(utils,
packageVersion,
tail
)
useDynLib(complex)
25 changes: 25 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
complex v1.0.3 (Release data: 2026-09-25)
==============

Changes:
* carima() - pure complex ARIMA (no regressors), a wrapper of clm() with an interface similar to smooth::msarima(): orders as c(p,d,q) or list(ar,i,ma), constant (FALSE by default, TRUE or a value), arma (fixed parameters), model (reuse of an estimated model), h, holdout (with accuracy), silent. Non-seasonal only. forecast() method for carima.
* auto.carima() - automatic selection of the orders of complex ARIMA. Following auto.ssarima(), AR and MA orders are selected for each order of differencing via an IC-driven neighbourhood search (Hyndman & Khandakar, 2008), and the constant is tested at the end for the best model of each d. search="full" fits all the combinations. The information criteria are calculated on the common sample without the first ar+i+ma observations, so that models with different d are comparable. Each model is estimated from the default starting values and from the closest estimated model.
* auto.carima() starts every model from the Hannan-Rissanen estimates (via clm(fast=TRUE)) and from the closest estimated model. On wind data this reached a better optimum for the selected model (AICc lower by 6.8) with fewer fits. auto.carima(fast=TRUE) screens all the models via Hannan-Rissanen and estimates via the likelihood only the best one for each d, constant and MA order, before the neighbourhood search. It is about three times faster, but approximate.
* clm() now penalises non-stationary AR and non-invertible MA parameters in the loss (all roots of the complex AR/MA polynomials should lie outside the unit circle); the loss becomes 1E+100 divided by the smallest modulus of the roots.
* clm(fast=TRUE) for pure cARIMA (no explanatory variables, the intercept is allowed) now starts the optimiser from the Hannan-Rissanen estimates of AR and MA parameters (and, for dcgnorm, from the shape maximising the likelihood of their residuals). On wind data this reached much higher likelihoods than the default starting values (e.g. 489.8 vs 412.2 for cARIMA(1,1,2)).
* dcgnorm() and rcgnorm() - density and random generation for the complex generalised normal distribution (complex generalised Gaussian), with scale, pseudo-scale and shape.
* clm() has a new argument distribution. distribution="dcgnorm" uses the complex generalised normal likelihood, with the shape estimated (or provided via shape=..., with the starting value as the last element of B). The scale and the pseudo-scale are concentrated out: the circularity coefficient comes from the moments of the residuals and the scale is the ML estimate given it (approximate ML when shape is not 1). Standard errors are for the parameters given the shape.
* AIC() and BIC() of clm() now count all real parameters of the model (both parts of the complex variable), as done in multivariate models. nparam() and the degrees of freedom are still per series. Before, the penalty was half of what it should be. Information criteria values change, and so can the orders selected by them.
* AICc() and BICc() methods for clm() use the multivariate small-sample correction (Bedrick & Tsai, 1994) with two series, as in the legion package. The correction assumes unrestricted multivariate regression, so it is approximate for complex models.
* Added tests (testthat).

Bugfixes:
* clm() without intercept (e.g. y~-1) and with MA terms flipped the sign of the MA parameters (the likelihood was correct, the reported parameters and forecasts were not).
* predict.clm() for cARIMA without intercept used the first parameter as an intercept (1:0 indexing).
* clm() without intercept and with AR terms failed in the check of variability of differenced data.
* clm() with d=0, no intercept and no AR (e.g. cARIMA(0,0,q) with y~-1) selected the first column for the starting values (1:0 indexing) and failed.
* clm() with loss="CLS" and MA terms failed with an unclear error from the optimiser. It now stops with an explanation (the CLS loss is complex-valued and cannot be minimised numerically).
* clm() with nothing to estimate (e.g. cARIMA(0,1,0) without intercept) failed in the optimiser.
* The documentation of the ellipsis in clm() was hidden by the one of sigma.clm().


complex v1.0.2 (Release data: 2026-02-03)
==============

Expand Down
Loading
Loading