Skip to content

Commit a5f7c8b

Browse files
authored
Merge pull request #271 from jr-leary7/dev
Dev
2 parents f0f3927 + 6b4240b commit a5f7c8b

8 files changed

Lines changed: 74 additions & 61 deletions

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: scLANE
22
Type: Package
33
Title: Model Gene Expression Dynamics with Spline-Based NB GLMs, GEEs, & GLMMs
4-
Version: 0.8.7
4+
Version: 0.99.0
55
Authors@R: c(person(given = c("Jack", "R."), family = "Leary", email = "j.leary@ufl.edu", role = c("aut", "cre"), comment = c(ORCID = "0009-0004-8821-3269")),
66
person(given = "Rhonda", family = "Bacher", email = "rbacher@ufl.edu", role = c("ctb", "fnd"), comment = c(ORCID = "0000-0001-5787-476X")))
77
Description: Our scLANE model uses truncated power basis spline models to build flexible, interpretable models of single cell gene expression over pseudotime or latent time.

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Changes in v0.99.0
2+
3+
+ Preparing for BioConductor submission.
4+
+ Slightly adjusted `waldTestGEE()` and `scoreTestGEE()` to be more efficient.
5+
16
# Changes in v0.8.7
27

38
+ Switched GEE fitting back to use `scale.fix = FALSE` and substituted a fixed value for the Negative-binomial overdispersion parameter (instead of estimating via method-of-moments) as it improves model fits.

R/scoreTestGEE.R

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#' @author Jack R. Leary
55
#' @description Performs a basic Lagrange multiplier test to determine whether an alternate model is significantly better than a nested null model. This is the GEE equivalent (kind of) of \code{\link{modelLRT}}. Be careful with small sample sizes.
66
#' @importFrom stats model.matrix predict pchisq
7+
#' @importFrom MASS negative.binomial
78
#' @importFrom Matrix bdiag
89
#' @param mod.1 The model under the alternative hypothesis. Must be of class \code{geem}. Defaults to NULL.
910
#' @param mod.0 The model under the null hypothesis. Must be of class \code{geem}. Defaults to NULL.
@@ -52,10 +53,10 @@ scoreTestGEE <- function(mod.1 = NULL,
5253
theta <- as.numeric(gsub("\\)", "", gsub(".*\\(", "", mod.1$FunList$family)))
5354
X_null <- stats::model.matrix(mod.0, data = null.df)
5455
X_alt <- stats::model.matrix(mod.1, data = alt.df)
55-
r_null <- null.df$Y - exp(stats::predict(mod.0))
56+
r_null <- null.df$Y - fitted(mod.0)
5657
p_alt <- ncol(X_alt)
5758
groups <- unique(id.vec)
58-
W <- K <- vector("list", length = length(groups))
59+
W_list <- K_inv_list <- vector("list", length = length(groups))
5960
for (i in seq(groups)) {
6061
group_idx <- which(id.vec == groups[i])
6162
n_i <- length(group_idx)
@@ -69,39 +70,50 @@ scoreTestGEE <- function(mod.1 = NULL,
6970
R_i <- matrix(rho^abs(outer(seq(n_i), seq(n_i), "-")), nrow = n_i, ncol = n_i)
7071
}
7172
# create working covariance matrix V_i
72-
mu_i <- exp(stats::predict(mod.0)[group_idx])
73-
V_mu_i <- mu_i * (1 + mu_i / theta)
73+
mu_i <- mod.0$FunList$linkinv(mod.0$eta)[group_idx]
74+
V_mu_i <- mod.0$FunList$variance(mu_i)
7475
A_i <- diag(V_mu_i)
7576
A_i_sqrt <- sqrt(A_i) # same as taking the 1/2 power of A_i since A_i is diagonal
7677
V_i <- A_i_sqrt %*% R_i %*% A_i_sqrt
77-
# create matrices W_i and K_i
78-
K_i <- diag(mu_i)
7978
V_i_inv <- try({ eigenMapMatrixInvert(V_i) }, silent = TRUE)
8079
if (inherits(V_i_inv, "try-error")) {
8180
V_i_inv <- eigenMapPseudoInverse(V_i)
8281
}
82+
# create matrices W_i and K_i
83+
K_i <- diag(MASS::negative.binomial(theta = 1, link = "log")$mu.eta(mod.0$eta)[group_idx])
8384
W_i <- K_i %*% V_i_inv %*% K_i
84-
W[[i]] <- W_i
85-
K[[i]] <- K_i
86-
}
87-
W <- as.matrix(Matrix::bdiag(W))
88-
K <- as.matrix(Matrix::bdiag(K))
89-
K_inv <- try({ eigenMapMatrixInvert(K) }, silent = TRUE)
90-
if (inherits(K_inv, "try-error")) {
91-
K_inv <- eigenMapPseudoInverse(K)
85+
W_list[[i]] <- W_i
86+
K_inv <- try({ eigenMapMatrixInvert(K_i) }, silent = TRUE)
87+
if (inherits(K_inv, "try-error")) {
88+
K_inv <- eigenMapPseudoInverse(K_i)
89+
}
90+
K_inv_list[[i]] <- K_inv
9291
}
93-
# generate score vector
92+
W <- as.matrix(Matrix::bdiag(W_list))
93+
K_inv <- as.matrix(Matrix::bdiag(K_inv_list))
94+
# score under the null
9495
U <- phi^(-1) * t(X_alt) %*% W %*% K_inv %*% r_null
95-
# generate variance of score vector and invert it
96-
V_U <- phi^(-2) * t(X_alt) %*% W %*% X_alt
96+
# Generate variance of score vector under the null
97+
V_U <- t(X_alt) %*% W %*% X_alt
9798
V_U_inv <- try({ eigenMapMatrixInvert(V_U) }, silent = TRUE)
9899
if (inherits(V_U_inv, "try-error")) {
99100
V_U_inv <- eigenMapPseudoInverse(V_U)
100101
}
102+
VarM_hat <- phi * V_U_inv
103+
Lpmat <- diag(1, nrow = p_alt, ncol = p_alt)
104+
Lpmat <- Lpmat[-1,,drop=FALSE]
105+
Lmat <- t(Lpmat)
106+
mid <- Lpmat %*% VarM_hat %*% Lmat
107+
mid_inv <- try({ eigenMapMatrixInvert(mid) }, silent = TRUE)
108+
if (inherits(mid_inv, "try-error")) {
109+
mid_inv <- eigenMapPseudoInverse(mid)
110+
}
111+
full_mid <- VarM_hat %*% Lmat %*% mid_inv %*% Lpmat %*% VarM_hat
101112
# estimate test statistic and accompanying p-value
102-
S <- t(U) %*% V_U_inv %*% U
103-
S_df <- ncol(X_alt) - ncol(X_null)
104-
p_value <- 1 - stats::pchisq(S, df = S_df)
113+
S <- t(U) %*% full_mid %*% U
114+
S_df = p_alt - 1
115+
p_value <- 1 - stats::pchisq(S, df = S_df) # r from L matrix
116+
# format results
105117
res <- list(Score_Stat = S,
106118
DF = S_df,
107119
P_Val = p_value,

R/summary.R

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
#' Summary method for scLANE objects.
2-
#'
1+
#' Summary method for scLANE objects.
2+
#'
33
#' @name summary.scLANE
44
#' @author Jack R. Leary
5-
#' @importFrom purrr map reduce
6-
#' @importFrom stats p.adjust
7-
#' @param test.dyn.res The nested list returned by \code{\link{testDynamic}}. Defaults to NULL.
8-
#' @param (Optional) The method used to adjust \emph{p}-values for multiple hypothesis testing. Defaults to "fdr".
5+
#' @importFrom purrr map reduce
6+
#' @importFrom stats p.adjust
7+
#' @param test.dyn.res The nested list returned by \code{\link{testDynamic}}. Defaults to NULL.
8+
#' @param p.adj.method (Optional) The method used to adjust \emph{p}-values for multiple hypothesis testing. Defaults to "fdr".
99
#' @param fdr.cutoff (Optional) The FDR threshold for determining statistical significance. Defaults to 0.01.
10-
#' @return A summary list with aggregated statistics concerning the trajectory DE tests from \code{scLANE}.
10+
#' @return A summary list with aggregated statistics concerning the trajectory DE tests from \code{scLANE}.
1111
#' @export
1212
#' @examples
1313
#' data(scLANE_models)
1414
#' summary(scLANE_models)
1515

16-
summary.scLANE <- function(test.dyn.res = NULL,
17-
p.adj.method = "fdr",
16+
summary.scLANE <- function(test.dyn.res = NULL,
17+
p.adj.method = "fdr",
1818
fdr.cutoff = 0.01) {
1919
if (!inherits(test.dyn.res, "scLANE")) { stop("The input must be an object of class 'scLANE'.") }
2020
summary_stats <- list()
@@ -29,18 +29,18 @@ summary.scLANE <- function(test.dyn.res = NULL,
2929
adj_p_values <- stats::p.adjust(sort(p_values), method = p.adj.method)
3030
summary_stats$n_significant_genes <- sum(adj_p_values < fdr.cutoff, na.rm = TRUE)
3131
summary_stats$mean_adj_p_value <- mean(adj_p_values, na.rm = TRUE)
32-
summary_stats$test_type <- ifelse(test.dyn.res[[1]][[1]]$Test_Stat_Type == "LRT",
33-
"Likelihood Ratio Test",
32+
summary_stats$test_type <- ifelse(test.dyn.res[[1]][[1]]$Test_Stat_Type == "LRT",
33+
"Likelihood Ratio Test",
3434
ifelse(test.dyn.res[[1]][[1]]$Test_Stat_Type == "Wald", "Wald Test", "Score Test"))
3535
class(summary_stats) <- "summary.scLANE"
3636
return(summary_stats)
3737
}
3838

39-
#' Print method for summary.scLANE objects.
40-
#'
39+
#' Print method for summary.scLANE objects.
40+
#'
4141
#' @name print.summary.scLANE
4242
#' @author Jack R. Leary
43-
#' @param x An object of class summary.scLANE.
43+
#' @param x An object of class summary.scLANE.
4444
#' @export
4545

4646
print.summary.scLANE <- function(x) {

R/testDynamic.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
#' @param is.gee Should a GEE framework be used instead of the default GLM? Defaults to FALSE.
2626
#' @param cor.structure If the GEE framework is used, specifies the desired working correlation structure. Must be one of "ar1", "independence", or "exchangeable". Defaults to "ar1".
2727
#' @param gee.bias.correction.method (Optional) Specify which small-sample bias correction to be used on the sandwich variance-covariance matrix prior to test statistic estimation. Options are "kc" and "df". Defaults to NULL, indicating the use of the model-based variance.
28-
#' @param gee.test A string specifying the type of test used to estimate the significance of the full model. Must be one of "wald" or "score". Defaults to "score".
28+
#' @param gee.test A string specifying the type of test used to estimate the significance of the full model. Must be one of "wald" or "score". Defaults to "wald".
2929
#' @param is.glmm Should a GLMM framework be used instead of the default GLM? Defaults to FALSE.
3030
#' @param id.vec If a GEE or GLMM framework is being used, a vector of subject IDs to use as input to \code{\link[geeM]{geem}} or \code{\link[glmmTMB]{glmmTMB}}. Defaults to NULL.
3131
#' @param glmm.adaptive (Optional) Should the basis functions for the GLMM be chosen adaptively? If not, uses 4 evenly spaced knots. Defaults to TRUE.
@@ -64,7 +64,7 @@ testDynamic <- function(expr.mat = NULL,
6464
is.gee = FALSE,
6565
cor.structure = "ar1",
6666
gee.bias.correction.method = NULL,
67-
gee.test = "score",
67+
gee.test = "wald",
6868
is.glmm = FALSE,
6969
glmm.adaptive = TRUE,
7070
id.vec = NULL,

R/waldTestGEE.R

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ waldTestGEE <- function(mod.1 = NULL,
4242
if (!(correction.method %in% c("df", "kc"))) { stop("Unsupported bias correction method in waldTestGEE().") }
4343
if (correction.method == "kc" && is.null(id.vec)) { stop("The Kauermann and Carroll bias correction method requires a vector of subject IDs.") }
4444
}
45-
4645
mod.1 <- mod.1$final_mod
4746
if (!(inherits(mod.1, "geem") && inherits(mod.0, "geem"))) { stop("You must provide two geeM models to waldTestGee().") }
4847
if (length(coef(mod.0)) != 1) { stop("Null GEE model must be intercept-only.") }
@@ -53,15 +52,8 @@ waldTestGEE <- function(mod.1 = NULL,
5352
P_Val = 1,
5453
Notes = NA_character_)
5554
} else {
56-
# compute test statistic & asymptotic p-value
57-
coef_alt_mod <- names(coef(mod.1))
58-
coef_null_mod <- names(coef(mod.0))
59-
coef_diff <- setdiff(coef_alt_mod, coef_null_mod)
60-
coef_idx <- rep(0, length(coef_diff))
61-
for (i in seq_len(length(coef_diff))) {
62-
coef_idx[i] <- which(coef_diff[i] == coef_alt_mod)
63-
}
64-
coef_vals <- as.matrix(coef(mod.1)[coef_idx])
55+
# compute test statistic, optionally bias-adjust, & estimate asymptotic p-value
56+
coef_vals <- as.matrix(coef(mod.1))
6557
if (!is.null(correction.method)) {
6658
vcov_mat <- as.matrix(mod.1$var)
6759
vcov_mat <- biasCorrectGEE(mod.1,
@@ -72,24 +64,28 @@ waldTestGEE <- function(mod.1 = NULL,
7264
} else {
7365
vcov_mat <- as.matrix(mod.1$naiv.var)
7466
}
75-
vcov_mat <- vcov_mat[coef_idx, coef_idx]
76-
wald_test_stat <- try({
77-
vcov_mat_inv <- eigenMapMatrixInvert(vcov_mat, n_cores = 1L)
78-
if (inherits(vcov_mat_inv, "try-error")) {
79-
vcov_mat_inv <- eigenMapPseudoInverse(vcov_mat, n_cores = 1L)
80-
}
81-
as.numeric(crossprod(coef_vals, vcov_mat_inv) %*% coef_vals)
82-
}, silent = TRUE)
67+
p_alt <- length(coef(mod.1))
68+
Lpmat <- diag(1, nrow = p_alt, ncol = p_alt)
69+
Lpmat <- Lpmat[-1,,drop=FALSE]
70+
Lmat <- t(Lpmat)
71+
middle <- Lpmat %*% vcov_mat %*% Lmat
72+
middle_inv <- try({ eigenMapMatrixInvert(middle) }, silent = TRUE)
73+
if (inherits(middle_inv, "try-error")) {
74+
middle_inv <- eigenMapPseudoInverse(middle)
75+
}
76+
sides <- Lpmat %*% coef(mod.1)
77+
wald_test_stat <- t(sides) %*% middle_inv %*% sides
8378
if (inherits(wald_test_stat, "try-error")) {
8479
wald_note <- wald_test_stat[1] # this is the error message
8580
wald_test_stat <- 0
8681
p_value <- 1
8782
} else {
88-
p_value <- as.numeric(1 - stats::pchisq(wald_test_stat, df = length(coef_diff)))
83+
p_value <- as.numeric(1 - stats::pchisq(wald_test_stat, df = p_alt - 1))
8984
wald_note <- NA_character_
9085
}
86+
# format results
9187
res <- list(Wald_Stat = wald_test_stat,
92-
DF = length(coef_diff),
88+
DF = p_alt - 1,
9389
P_Val = p_value,
9490
Notes = wald_note)
9591
}

man/summary.scLANE.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/testDynamic.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)