Skip to content

Commit b175687

Browse files
authored
Merge pull request #58 from SebKrantz/development
Update
2 parents 7692ae0 + 741cbc6 commit b175687

File tree

15 files changed

+423
-78
lines changed

15 files changed

+423
-78
lines changed

.github/workflows/pkgdown.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2+
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
release:
8+
types: [published]
9+
workflow_dispatch:
10+
11+
name: pkgdown.yaml
12+
13+
permissions: read-all
14+
15+
jobs:
16+
pkgdown:
17+
runs-on: ubuntu-latest
18+
# Only restrict concurrency for non-PR jobs
19+
concurrency:
20+
group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }}
21+
env:
22+
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
23+
permissions:
24+
contents: write
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- uses: r-lib/actions/setup-pandoc@v2
29+
30+
- uses: r-lib/actions/setup-r@v2
31+
with:
32+
use-public-rspm: true
33+
34+
- uses: r-lib/actions/setup-r-dependencies@v2
35+
with:
36+
extra-packages: |
37+
github::SebKrantz/pkgdown
38+
local::.
39+
needs: website
40+
41+
- name: Build site
42+
run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
43+
shell: Rscript {0}
44+
45+
- name: Deploy to GitHub pages 🚀
46+
if: github.event_name != 'pull_request'
47+
uses: JamesIves/[email protected]
48+
with:
49+
clean: false
50+
branch: gh-pages
51+
folder: docs

DESCRIPTION

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Package: dfms
2-
Version: 0.2.2
2+
Version: 0.3.0
33
Title: Dynamic Factor Models
44
Authors@R: c(person("Sebastian", "Krantz", role = c("aut", "cre"), email = "[email protected]"),
55
person("Rytis", "Bagdziunas", role = "aut"))
@@ -15,8 +15,8 @@ Description: Efficient estimation of Dynamic Factor Models using the Expectation
1515
of factors are also provided - following Bai and Ng (2002) <doi:10.1111/1468-0262.00273>.
1616
URL: https://sebkrantz.github.io/dfms/
1717
BugReports: https://github.com/SebKrantz/dfms/issues
18-
Depends: R (>= 3.3.0)
19-
Imports: Rcpp (>= 1.0.1), collapse (>= 1.8.0)
18+
Depends: R (>= 3.5.0)
19+
Imports: Rcpp (>= 1.0.1), collapse (>= 2.0.0)
2020
LinkingTo: Rcpp, RcppArmadillo
2121
Suggests: xts, vars, magrittr, testthat (>= 3.0.0), knitr, rmarkdown, covr
2222
License: GPL-3

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ importFrom(collapse,"%=%")
3333
importFrom(collapse,"%r*%")
3434
importFrom(collapse,TRA.matrix)
3535
importFrom(collapse,ckmatch)
36+
importFrom(collapse,flag)
3637
importFrom(collapse,fmedian)
3738
importFrom(collapse,fmedian.default)
3839
importFrom(collapse,fmin.matrix)
@@ -54,6 +55,8 @@ importFrom(collapse,setop)
5455
importFrom(collapse,t_list)
5556
importFrom(collapse,unattrib)
5657
importFrom(collapse,unlist2d)
58+
importFrom(collapse,vec)
59+
importFrom(collapse,whichNA)
5760
importFrom(collapse,whichv)
5861
importFrom(grDevices,rainbow)
5962
importFrom(graphics,abline)

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# dfms 0.3.0
2+
3+
* Added argument `quarterly.vars`, enabling mixed-frequency estimation with monthly and quarterly data following Banbura and Modugno (2014). The data matrix should contain the quarterly variables at the end of the matrix (after the monthly ones).
4+
15
# dfms 0.2.2
26

37
* Replace Armadillo `inv_sympd()` by Armadillo `inv()` in C++ Kalman Filter to improve numerical robustness at a minor performance cost.

R/DFM.R

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
.EM_DGR <- quote(EMstepDGR(X, A, C, Q, R, F_0, P_0, cpX, n, r, sr, TT, rQi, rRi))
33
.EM_BM <- quote(EMstepBMOPT(X, A, C, Q, R, F_0, P_0, XW0, W, n, r, sr, TT, dgind, dnkron, dnkron_ind, rQi, rRi))
44
.EM_BM_idio <- quote(EMstepBMidio(X, A, C, Q, R, F_0, P_0, XW0, W, dgind, dnkron, dnkron_ind, r, p, n, sr, TT, rQi, rRi))
5+
.EMstepBMMQ <- quote(EMstepBMMQ(X, A, C, Q, R, F_0, P_0, XW0, NW, dgind, dnkron, dnkron_ind, r, p, R_mat, n, nq, sr, TT, rQi, rRi))
56
.KFS <- quote(SKFS(X, A, C, Q, R, F_0, P_0))
67

78

@@ -15,6 +16,7 @@
1516
#' @param p integer. number of lags in factor VAR.
1617
#' @param \dots (optional) arguments to \code{\link{tsnarmimp}}.
1718
#' @param idio.ar1 logical. Model observation errors as AR(1) processes: \eqn{e_t = \rho e_{t-1} + v_t}{e(t) = rho e(t-1) + v(t)}. \emph{Note} that this substantially increases computation time, and is generaly not needed if \code{n} is large (>30). See theoretical vignette for details.
19+
#' @param quarterly.vars character. Names of quarterly variables in \code{X} (if any). Monthly variables should be to the left of the quarterly variables in the data matrix and quarterly observations should be provided every 3rd period.
1820
#' @param rQ character. restrictions on the state (transition) covariance matrix (Q).
1921
#' @param rR character. restrictions on the observation (measurement) covariance matrix (R).
2022
#' @param em.method character. The implementation of the Expectation Maximization Algorithm used. The options are:
@@ -115,7 +117,7 @@
115117
#' Stock, J. H., & Watson, M. W. (2016). Dynamic Factor Models, Factor-Augmented Vector Autoregressions, and Structural Vector Autoregressions in Macroeconomics. \emph{Handbook of Macroeconomics, 2}, 415–525. https://doi.org/10.1016/bs.hesmac.2016.04.002
116118
#'
117119
#' @useDynLib dfms, .registration = TRUE
118-
#' @importFrom collapse fscale qsu fvar fmedian fmedian.default qM unattrib na_omit %=% %-=% %+=% %/=% %*=% %r*% whichv setColnames setDimnames
120+
#' @importFrom collapse fscale qsu fvar flag fmedian fmedian.default qM unattrib na_omit %=% %-=% %+=% %/=% %*=% %r*% whichv whichNA vec setColnames setDimnames
119121
#' @importFrom grDevices rainbow
120122
#' @importFrom graphics abline legend lines par
121123
#' @importFrom stats filter residuals rnorm spline ts.plot
@@ -203,7 +205,8 @@
203205
#' @export
204206

205207
DFM <- function(X, r, p = 1L, ...,
206-
idio.ar1 = FALSE, # quarterly.vars = c(), blocks = c(),
208+
idio.ar1 = FALSE,
209+
quarterly.vars = NULL, # blocks = c(),
207210
rQ = c("none", "diagonal", "identity"),
208211
rR = c("diagonal", "identity", "none"),
209212
em.method = c("auto", "DGR", "BM", "none"),
@@ -230,6 +233,7 @@ DFM <- function(X, r, p = 1L, ...,
230233
if(!is.logical(idio.ar1) || is.na(idio.ar1)) stop("idio.ar1 needs to be logical")
231234
if(!is.logical(pos.corr) || is.na(pos.corr)) stop("pos.corr needs to be logical")
232235
if(!is.logical(check.increased) || is.na(check.increased)) stop("check.increased needs to be logical")
236+
if(!is.null(quarterly.vars) && !is.character(quarterly.vars)) stop("quarterly.vars needs to be a character vector with the names of quarterly variables in X")
233237

234238
rp <- r * p
235239
sr <- seq_len(r)
@@ -242,6 +246,11 @@ DFM <- function(X, r, p = 1L, ...,
242246
Xnam <- dimnames(X)[[2L]]
243247
dimnames(X) <- NULL
244248
n <- ncol(X)
249+
if(MFl <- !is.null(quarterly.vars)) {
250+
qind <- ckmatch(quarterly.vars, Xnam)
251+
nq <- length(qind)
252+
if(!identical(sort(qind), (n-nq+1):n)) stop("Pleae order your data such that quarterly variables are to the right (after) the monthly variables")
253+
}
245254

246255
# Missing values
247256
anymiss <- anyNA(X)
@@ -261,8 +270,8 @@ DFM <- function(X, r, p = 1L, ...,
261270

262271
# This is because after removing missing rows, the data could be complete, e.g. differencing data with diff.xts() of collapse::fdiff() just gives a NA row
263272
if(anymiss && length(rm.rows)) anymiss <- any(W)
264-
BMl <- switch(tolower(em.method[1L]), auto = anymiss, dgr = FALSE, bm = TRUE, none = NA, stop("Unknown EM option:", em.method[1L]))
265-
if(idio.ar1 && !is.na(BMl)) BMl <- TRUE
273+
BMl <- switch(tolower(em.method[1L]), auto = anymiss || idio.ar1 || MFl, dgr = FALSE || idio.ar1 || MFl,
274+
bm = TRUE, none = NA, stop("Unknown EM option:", em.method[1L]))
266275

267276
# Run PCA to get initial factor estimates:
268277
# v <- svd(X_imp, nu = 0L, nv = min(as.integer(r), n, TT))$v # Not faster than eigen...
@@ -276,8 +285,13 @@ DFM <- function(X, r, p = 1L, ...,
276285
F_pc <- X_imp %*% v
277286

278287
## Initial System Matrices
279-
init <- if(idio.ar1) init_cond_idio_ar1(X, F_pc, v, n, r, p, BMl, rRi, rQi, anymiss, tol) else
280-
init_cond(X, F_pc, v, n, r, p, BMl, rRi, rQi)
288+
if(MFl) {
289+
init <- if(idio.ar1) stop("Not yet implemented") else
290+
init_cond_MQ(X, X_imp, F_pc, v, n, r, p, TT, nq, rRi, rQi)
291+
} else {
292+
init <- if(idio.ar1) init_cond_idio_ar1(X, F_pc, v, n, r, p, BMl, rRi, rQi, anymiss, tol) else
293+
init_cond(X, F_pc, v, n, r, p, BMl, rRi, rQi)
294+
}
281295
A <- init$A; C <- init$C; Q <- init$Q; R <- init$R; F_0 <- init$F_0; P_0 <- init$P_0
282296

283297
## Run standartized data through Kalman filter and smoother once
@@ -300,6 +314,7 @@ DFM <- function(X, r, p = 1L, ...,
300314
P_2s = setDimnames(kfs_res$P_smooth[sr, sr,, drop = FALSE], list(fnam, fnam, NULL)),
301315
anyNA = anymiss, # || length(rm.rows), # This is for internal missing values only
302316
rm.rows = rm.rows,
317+
quarterly.vars = quarterly.vars,
303318
em.method = if(is.na(BMl)) "none" else if(BMl) "BM" else "DGR",
304319
call = match.call())
305320

@@ -335,11 +350,22 @@ DFM <- function(X, r, p = 1L, ...,
335350
converged <- FALSE
336351

337352
if(BMl) {
338-
expr <- if(idio.ar1) .EM_BM_idio else .EM_BM
339-
dnkron <- matrix(1, r, r) %x% diag(n) # Used to be inside EMstep, taken out to speed up the algorithm
340-
dnkron_ind <- whichv(dnkron, 1)
341-
XW0 <- X_imp
353+
if(MFl) {
354+
nm <- n - nq
355+
rpC <- r * max(p, 5L)
356+
rpC1nq <- (rpC+1L):(rpC+nq)
357+
NW <- !W
358+
R_mat <- kronecker(Rcon, diag(r)) # TODO: autsource this
359+
expr <- if(idio.ar1) stop("not implemented yet") else .EMstepBMMQ
360+
dnkron <- matrix(1, r, r) %x% diag(nm) # Used to be inside EMstep, taken out to speed up the algorithm
361+
dnkron_ind <- whichv(dnkron, 1)
362+
} else {
363+
expr <- if(idio.ar1) .EM_BM_idio else .EM_BM
364+
dnkron <- matrix(1, r, r) %x% diag(n) # Used to be inside EMstep, taken out to speed up the algorithm
365+
dnkron_ind <- whichv(dnkron, 1)
366+
}
342367
dgind <- 0:(n-1) * n + 1:n
368+
XW0 <- X_imp
343369
if(anymiss) XW0[W] <- 0 else W <- is.na(X) # TODO: think about this...
344370
} else {
345371
expr <- .EM_DGR
@@ -379,7 +405,9 @@ DFM <- function(X, r, p = 1L, ...,
379405
A = setDimnames(em_res$A[sr, seq_len(rp), drop = FALSE], lagnam(fnam, p)),
380406
C = setDimnames(em_res$C[, sr, drop = FALSE], list(Xnam, fnam)),
381407
Q = setDimnames(em_res$Q[sr, sr, drop = FALSE], list(unam, unam)),
382-
R = setDimnames(if(idio.ar1) em_res$Q[-seq_len(rp), -seq_len(rp), drop = FALSE] else em_res$R, list(Xnam, Xnam)),
408+
R = setDimnames(if(MFl && idio.ar1) stop("not implemented yet") else if(MFl)
409+
`[<-`(em_res$R, (nm+1):n, (nm+1):n, value = em_res$Q[rpC1nq, rpC1nq]) else if(idio.ar1)
410+
em_res$Q[-seq_len(rp), -seq_len(rp), drop = FALSE] else em_res$R, list(Xnam, Xnam)),
383411
e = if(idio.ar1) setColnames(kfs_res$F_smooth[, -seq_len(rp), drop = FALSE], Xnam) else NULL,
384412
rho = if(idio.ar1) setNames(diag(em_res$A)[-seq_len(rp)], Xnam) else NULL,
385413
loglik = if(num_iter == max.iter) loglik_all else loglik_all[seq_len(num_iter)],

R/EMBM_MQ.R

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
2+
# Define the inputs of the function
3+
# X: a matrix with n rows and TT columns representing the observed data at each time t.
4+
# A: a transition matrix with size r*p+r*r.
5+
# C: an observation matrix with size n*r.
6+
# Q: a covariance matrix for the state equation disturbances with size r*p+r*r.
7+
# R: a covariance matrix for the observation disturbances with size n*n.
8+
# Z_0: the initial value of the state variable.
9+
# V_0: the initial value of the variance-covariance matrix of the state variable.
10+
# r: order of the state autoregression (AR) process.
11+
# p: number of lags in the state AR process.
12+
# i_idio: a vector of indicators specifying which variables are idiosyncratic noise variables.'
13+
14+
# Z_0 = F_0; V_0 = P_0
15+
EMstepBMMQ = function(X, A, C, Q, R, Z_0, V_0, XW0, NW, dgind, dnkron, dnkron_ind, r, p, R_mat, n, nq, sr, TT, rQi, rRi) {
16+
17+
# Define dimensions of the input arguments
18+
rC = 5L # ncol(Rcon)
19+
pC = max(p, rC)
20+
rp = r * p
21+
rpC = r * pC
22+
nm = n - nq
23+
rC = rC * r # Note here replacing rC!!
24+
25+
snm = seq_len(nm)
26+
srp = seq_len(rp)
27+
srpC = seq_len(rpC)
28+
rpC1nq = (rpC+1L):(rpC+nq)
29+
30+
# Compute expected sufficient statistics for a single Kalman filter sequence
31+
# Run the Kalman filter with the current parameter estimates
32+
kfs_res = SKFS(X, A, C, Q, R, Z_0, V_0, TRUE)
33+
34+
# Extract values from the Kalman Filter output
35+
Zsmooth = kfs_res$F_smooth
36+
Vsmooth = kfs_res$P_smooth
37+
VVsmooth = kfs_res$PPm_smooth
38+
Zsmooth0 = kfs_res$F_smooth_0
39+
Vsmooth0 = kfs_res$P_smooth_0
40+
loglik = kfs_res$loglik
41+
42+
# Perform algebraic operations involving E(Z_t), E(Z_{t-1}), Y_t, Y_{t-1} to update A_new, Q_new, C_new, and R_new
43+
44+
# Normal Expectations to get Z(t+1) and A(t+1).
45+
tmp = rbind(Zsmooth0[srpC], Zsmooth[-TT, srpC, drop = FALSE])
46+
tmp2 = sum3(Vsmooth[srpC, srpC, -TT, drop = FALSE])
47+
EZZ = crossprod(Zsmooth[, srpC, drop = FALSE]) %+=% (tmp2 + Vsmooth[srpC, srpC, TT]) # E(Z'Z)
48+
EZZ_BB = crossprod(tmp) %+=% (tmp2 + Vsmooth0[srpC, srpC]) # E(Z(-1)'Z(-1))
49+
EZZ_FB = crossprod(Zsmooth[, srpC, drop = FALSE], tmp) %+=% sum3(VVsmooth[srpC, srpC,, drop = FALSE]) # E(Z'Z(-1))
50+
51+
# Update matrices A and Q
52+
A_new = A
53+
Q_new = Q
54+
55+
# System matrices
56+
A_new[sr, srp] = EZZ_FB[sr, srp, drop = FALSE] %*% ainv(EZZ_BB[srp, srp, drop = FALSE])
57+
if(rQi) {
58+
Qsr = (EZZ[sr, sr] - tcrossprod(A_new[sr, srp, drop = FALSE], EZZ_FB[sr, srp, drop = FALSE])) / TT
59+
Q_new[sr, sr] = if(rQi == 2L) Qsr else diag(diag(Qsr))
60+
} else Q_new[sr, sr] = diag(r)
61+
Q_new[rpC1nq, rpC1nq] = diag(diag(crossprod(Zsmooth[, rpC1nq, drop = FALSE]) + sum3(Vsmooth[rpC1nq, rpC1nq,, drop = FALSE])) / TT)
62+
63+
# E(X'X) & E(X'Z)
64+
# Estimate matrix C using maximum likelihood approach
65+
denom = numeric(nm*r^2)
66+
nom = matrix(0, nm, r)
67+
68+
# nanYt = diagm(nanY[1:nM,t] .== 0)
69+
# denom += kron(Zsmooth[1:r,t+1] * Zsmooth[1:r,t+1]' + Vsmooth[1:r,1:r,t+1], nanYt)
70+
# nom += y[1:nM,t]*Zsmooth[1:r,t+1]';
71+
for (t in 1:TT) {
72+
tmp = Zsmooth[t, sr]
73+
nom %+=% tcrossprod(XW0[t, snm], tmp)
74+
tmp2 = tcrossprod(tmp) + Vsmooth[sr, sr, t]
75+
dim(tmp2) = NULL
76+
denom %+=% tcrossprod(tmp2, NW[t, snm])
77+
}
78+
79+
dim(denom) = c(r, r, nm)
80+
dnkron[dnkron_ind] = aperm.default(denom, c(1L, 3L, 2L))
81+
# Solve for vec_C and reshape into C_new
82+
C_new = C
83+
C_new[1:nm, sr] = solve.default(dnkron, unattrib(nom)) # ainv() -> slower...
84+
85+
# Now updating the quarterly observation matrix C
86+
for (i in (nm+1):n) {
87+
denom = numeric(rC^2)
88+
nom = numeric(rC)
89+
90+
for (t in 1:TT) {
91+
if(NW[t, i]) denom %+=% (crossprod(Zsmooth[t, 1:rC, drop = FALSE]) + Vsmooth[1:rC, 1:rC , t])
92+
nom %+=% (XW0[t, i] * Zsmooth[t, 1:rC])
93+
}
94+
dim(denom) = c(rC, rC)
95+
denom_inv = ainv(denom)
96+
C_i = denom_inv %*% nom # Is this a scalar? -> yes, otherwise the replacement with C_new doesn't make sense
97+
tmp = tcrossprod(denom_inv, R_mat)
98+
C_i_constr = C_i - tmp %*% ainv(R_mat %*% tmp) %*% R_mat %*% C_i
99+
C_new[i, 1:rC] = C_i_constr
100+
}
101+
102+
if(rRi) {
103+
R_new = matrix(0, n, n)
104+
Rdg = R[dgind]
105+
for (t in 1:TT) {
106+
nnanYt = NW[t, ]
107+
tmp = C_new * nnanYt
108+
R[dgind] = Rdg * !nnanYt # If R is not diagonal
109+
tmp2 = tmp %*% tcrossprod(Vsmooth[,, t], tmp)
110+
tmp2 %+=% tcrossprod(XW0[t, ] - tmp %*% Zsmooth[t, ])
111+
tmp2 %+=% R # If R is not diagonal
112+
# tmp2[dgind] = tmp2[dgind] + (Rdg * !nnanYt) # If R is diagonal...
113+
R_new %+=% tmp2
114+
}
115+
if(rRi == 2L) { # Unrestricted
116+
R_new %/=% TT
117+
R_new[R_new < 1e-7] = 1e-7
118+
R_new[(nm+1):n, ] = 0
119+
R_new[, (nm+1):n] = 0
120+
} else { # Diagonal
121+
RR = R_new[dgind] / TT
122+
RR[RR < 1e-7] = 1e-7 # RR(RR<1e-2) = 1e-2;
123+
R_new %*=% 0
124+
R_new[dgind[snm]] = RR[snm] # TODO: set quarterly obs to a small number?
125+
}
126+
} else {
127+
R_new = diag(n)
128+
diag(R_new)[(nm+1):n] = 0
129+
}
130+
131+
# Set initial conditions
132+
V_0_new = V_0
133+
V_0_new[srpC, srpC] = Vsmooth0[srpC, srpC]
134+
diag(V_0_new)[-srpC] = diag(Vsmooth0)[-srpC]
135+
136+
return(list(A = A_new,
137+
C = C_new,
138+
Q = Q_new,
139+
R = R_new,
140+
F_0 = drop(Zsmooth0),
141+
P_0 = V_0_new,
142+
loglik = kfs_res$loglik))
143+
144+
}

0 commit comments

Comments
 (0)