Skip to content

Commit

Permalink
improve confidence interval estimation for population mean cosinor
Browse files Browse the repository at this point in the history
  • Loading branch information
Anish S. Shah committed Sep 30, 2024
1 parent e454aed commit 270ebe1
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 79 deletions.
4 changes: 0 additions & 4 deletions .Rprofile

This file was deleted.

14 changes: 7 additions & 7 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ Package: card
Type: Package
Title: Cardiovascular Applications in Research Data
Version: 0.1.0.9000
Authors@R: c(
person("Anish S. Shah",
email = "ashah282@uic.edu",
role = c("aut", "cre", "cph"),
comment = c(ORCID = "0000-0002-9729-1558"))
)
Authors@R:
person(given = "Anish S.",
family = "Shah",
role = c("aut", "cre", "cph"),
email = "shah.in.boots@gmail.com",
comment = c(ORCID = "0000-0002-9729-1558"))
Description: A collection of cardiovascular research datasets and analytical
tools, including methods for cardiovascular procedural data, such as
electrocardiography, echocardiography, and catheterization data. Additional
Expand All @@ -17,7 +17,7 @@ URL: https://cran.r-project.org/package=card
BugReports: https://github.com/shah-in-boots/card/issues
Encoding: UTF-8
LazyData: true
RoxygenNote: 7.3.1
RoxygenNote: 7.3.2
Roxygen: list(markdown = TRUE)
Depends:
R (>= 4.1),
Expand Down
4 changes: 2 additions & 2 deletions R/cosinor-constructor.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
#' linearization of the parameters to assess their statistics and
#' distribution.
#'
#' @param t Represents the _ordered_ time indices that provide the positions for the
#' cosine wave. Depending on the context:
#' @param t Represents the _ordered_ time indices that provide the positions for
#' the cosine wave. Depending on the context:
#'
#' - A `data frame` of a time-based predictor/index.
#'
Expand Down
213 changes: 152 additions & 61 deletions R/cosinor-fit.R
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ cosinor_pop_impl <- function(predictors, outcomes, tau, population) {

# Remove patients with only p observations (will cause a det ~ 0 error)
counts <- by(df, df[, "population"], nrow)
lowCounts <- as.numeric(names(counts[counts <= 2*p + 1]))
lowCounts <- as.numeric(names(counts[counts <= 2 * p + 1]))
df <- subset(df, !(population %in% lowCounts))

# Message about population count removal
Expand Down Expand Up @@ -315,71 +315,159 @@ confint.cosinor <- function(object, parm, level = 0.95, ...) {
}

switch(
object$type,
Population = {
# Message
message("Confidence intervals for amplitude and acrophase for population-mean cosinor use the methods described by Fernando et al 2004, which may not be applicable to multiple-components.")

# Freedom by number of individuals
k <- nrow(object$xmat)

# Standard errors
SE_mesor <- sd(xmat[, "mesor"]) / sqrt(k)
for(i in 1:p) {
assign(paste0("SE_amp", i), (sd(xmat[, paste0("amp", i)]) / sqrt(k)))
assign(paste0("SE_phi", i), (sd(xmat[, paste0("phi", i)]) / sqrt(k)))
assign(paste0("SE_beta", i), (sd(xmat[, paste0("beta", i)]) / sqrt(k)))
assign(paste0("SE_gamma", i), (sd(xmat[, paste0("gamma", i)]) / sqrt(k)))
}
object$type,
Population = {
# Message
message("Confidence intervals for amplitude and acrophase for population-mean cosinor use the methods described by Fernández et al. 2004.")

# Save SE
se <- list()
for(i in 1:p) {
se[[i]] <- c(
paste0("SE_amp", i),
paste0("SE_phi", i),
paste0("SE_beta", i),
paste0("SE_gamma", i)
)
}
se <- c(SE_mesor, unlist(mget(unlist(se))))
names(se)[1] <- "SE_mesor"
names(se) <- gsub("SE_", "", names(se))
# Number of individuals
k <- nrow(xmat)

# Confidence intervals
tdist <- stats::qt(1 - a/2, df = n - k)
confints <- list()
for(i in 1:p) {
confints[[i]] <-
c(
# Amp
get(paste0("amp", i)) - tdist * get(paste0("SE_amp", i)),
get(paste0("amp", i)) + tdist * get(paste0("SE_amp", i)),
# Phi
get(paste0("phi", i)) - tdist * get(paste0("SE_phi", i)),
get(paste0("phi", i)) + tdist * get(paste0("SE_phi", i))
)
}
# Extract parameters from xmat
params <- as.data.frame(xmat)

df <- rbind(
c(mesor - tdist*SE_mesor, mesor + tdist*SE_mesor),
matrix(unlist(confints), ncol = 2, byrow = TRUE)
)
rnames <- list()
for(i in 1:p) {
rnames[[i]] <- c(paste0("amp", i), paste0("phi", i))
}
rownames(df) <- c("mesor", unlist(rnames))
colnames(df) <- c(paste0(100*(a/2),"%"), paste0(100*(1-a/2), "%"))
# Convert column names to lowercase
colnames(params) <- tolower(colnames(params))

# Returned
estimates <- list(
ci = df,
se = se
)
return(estimates)
popParams <- colMeans(params)
varCovMat <- cov(params)

# Degrees of freedom
df <- k - 1

},
# Critical value from chi-squared distribution
cValue <- qchisq(level, df = 2)

# Initialize data structures for output
ciMatrix <- matrix(nrow = 0, ncol = 2)
colnames(ciMatrix) <- c(
sprintf("%.1f%%", a / 2 * 100),
sprintf("%.1f%%", (1 - a / 2) * 100)
)

seVector <- numeric()
names(seVector) <- character()

# MESOR confidence interval
seMesor <- sqrt(varCovMat["mesor", "mesor"] / k)
tValue <- qt(1 - a / 2, df)
ciMesor <- c(
popParams["mesor"] - tValue * seMesor,
popParams["mesor"] + tValue * seMesor
)

# Add MESOR to outputs
ciMatrix <- rbind(ciMatrix, ciMesor)
rownames(ciMatrix)[nrow(ciMatrix)] <- "mesor"

seVector <- c(seVector, seMesor)
names(seVector)[length(seVector)] <- "mesor"

# For each component
for (i in 1:p) {
# Get beta and gamma parameter names
betaName <- paste0("beta", i)
gammaName <- paste0("gamma", i)

# Extract individual estimates
beta_i <- params[[betaName]]
gamma_i <- params[[gammaName]]

# Compute sample means
betaBar <- mean(beta_i)
gammaBar <- mean(gamma_i)

# Compute covariance matrix for beta and gamma
covMat <- cov(cbind(beta_i, gamma_i))

# Eigen decomposition of covariance matrix
eig <- stats::eigen(covMat)
V <- eig$vectors
D <- diag(eig$values)

# Square root of covariance matrix
rootCov <- V %*% sqrt(D) %*% t(V)

# Generate ellipse points
thetaSeq <- seq(0, 2 * pi, length.out = 1000)
ellipsePoints <- sqrt(cValue) * (rootCov %*% rbind(cos(thetaSeq), sin(thetaSeq)))

betaTheta <- betaBar + ellipsePoints[1, ]
gammaTheta <- gammaBar + ellipsePoints[2, ]

# Compute amplitude and acrophase
# Make sure acrophase is within the unit circle
amplitudeTheta <- sqrt(betaTheta^2 + gammaTheta^2)
acrophaseTheta <- atan2(-gammaTheta, betaTheta)
acrophaseTheta <- (acrophaseTheta + pi) %% (2 * pi) - pi

# Compute confidence intervals for amplitude and acrophase
ampLower <- min(amplitudeTheta)
ampUpper <- max(amplitudeTheta)

phiLower <- min(acrophaseTheta)
phiUpper <- max(acrophaseTheta)

# Compute standard errors for beta and gamma
seBeta <- sqrt(var(beta_i) / k)
seGamma <- sqrt(var(gamma_i) / k)

# Compute t-value for beta and gamma
tValueBetaGamma <- qt(1 - alpha / 2, df)

# Confidence intervals for beta and gamma
betaLower <- betaBar - tValueBetaGamma * seBeta
betaUpper <- betaBar + tValueBetaGamma * seBeta

gammaLower <- gammaBar - tValueBetaGamma * seGamma
gammaUpper <- gammaBar + tValueBetaGamma * seGamma

# Add beta to outputs
ciMatrix <- rbind(ciMatrix, c(betaLower, betaUpper))
rownames(ciMatrix)[nrow(ciMatrix)] <- paste0("beta", i)

seVector <- c(seVector, seBeta)
names(seVector)[length(seVector)] <- paste0("beta", i)

# Add gamma to outputs
ciMatrix <- rbind(ciMatrix, c(gammaLower, gammaUpper))
rownames(ciMatrix)[nrow(ciMatrix)] <- paste0("gamma", i)

seVector <- c(seVector, seGamma)
names(seVector)[length(seVector)] <- paste0("gamma", i)

# Add amplitude to outputs
ciMatrix <- rbind(ciMatrix, c(ampLower, ampUpper))
rownames(ciMatrix)[nrow(ciMatrix)] <- paste0("amplitude", i)

# Standard error for amplitude
amplitude_i <- sqrt(beta_i^2 + gamma_i^2)
seAmplitude <- sqrt(var(amplitude_i) / k)
seVector <- c(seVector, seAmplitude)
names(seVector)[length(seVector)] <- paste0("amplitude", i)

# Add acrophase to outputs
ciMatrix <- rbind(ciMatrix, c(phiLower, phiUpper))
rownames(ciMatrix)[nrow(ciMatrix)] <- paste0("acrophase", i)

# Standard error for acrophase
acrophase_i <- atan2(-gamma_i, beta_i)
acrophase_i <- (acrophase_i + pi) %% (2 * pi) - pi
# Compute angular standard deviation
seAcrophase <- sqrt(-2 * log(abs(mean(exp(1i * acrophase_i)))))
seVector <- c(seVector, seAcrophase)
names(seVector)[length(seVector)] <- paste0("acrophase", i)
}

# Name the columns of ciMatrix according to confidence levels
colnames(ciMatrix) <- c(
sprintf("%.1f%%", a / 2 * 100),
sprintf("%.1f%%", (1 - a / 2) * 100)
)

# Return the output as a list
return(list(ci = ciMatrix, se = seVector))
},
Individual = {

# Nummber of parameters
Expand Down Expand Up @@ -467,6 +555,9 @@ confint.cosinor <- function(object, parm, level = 0.95, ...) {

}




## Zero Amplitude Test

#' @title Zero Amplitude Test
Expand Down
5 changes: 3 additions & 2 deletions tests/testthat/test-cosinor-fit.R
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@ test_that("models can be generally fit", {
# Harmonic checks
expect_gt(length(mcos$tau), 1)
expect_equal(max(mcos$tau) %% min(mcos$tau), 0)
expect_message(cosinor_features(mcos))
expect_warning(cosinor_features(mcos))

# Confidence intervals
expect_type(confint(scos), "list")
})

test_that("population cosinors can be fit", {

# Single population cosinor
f <- sDYX ~ hour
data <- twins
population = "patid"
cosinor(formula = f, data = data, tau = 24, population = population)
m <- cosinor(formula = f, data = data, tau = 24, population = population)


})
6 changes: 3 additions & 3 deletions tests/testthat/test-cosinor-plots.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ test_that("ggcosinor makes a ggplot", {
data("twins")
scos <- cosinor(rDYX ~ hour, twins, 24)
mcos <- cosinor(rDYX ~ hour, twins, c(24, 12))
pcos <- cosinor(rDYX ~ hour, twins, 24, "patid")
g <- ggcosinor(mcos)
pcos <- expect_message(cosinor(rDYX ~ hour, twins, 24, "patid"))
g <- expect_warning(ggcosinor(mcos))
expect_s3_class(g, "ggplot")
})
})

0 comments on commit 270ebe1

Please sign in to comment.