Pre-computes the null model (without genetic effects) for efficient repeated calls to getZ_marg_score() or getZ_marg_score_binary_SPA() with different SNP sets but the same phenotype and covariates. This optimization is critical for genome-wide analysis where the same null model is used across thousands of gene/region tests.

fit_null_model(X, Y, trait = "binary", sample_id = NULL)

Arguments

X

A numeric matrix of covariates (n x k), where k is the number of covariates. If the first column is not all 1s (intercept), it will be added automatically.

Y

A numeric vector of response variable (length n). For binary traits, must be coded as 0/1. For continuous traits, any numeric values are allowed.

trait

Character string indicating trait type. Must be either "binary" (logistic regression) or "continuous" (linear regression). Default is "binary".

sample_id

Optional character vector of sample IDs (length n). Stored in the null model for downstream sample alignment in marginal_scan(). Default is NULL.

Value

A null_model object (list) with class "glow_null_model" containing:

trait

Character, "binary" or "continuous"

n

Integer, sample size (number of observations)

X

Numeric matrix, covariate matrix with intercept included

fitted_probs

(Binary only) Numeric vector of fitted probabilities P(Y=1|X)

Y

Numeric vector, response (needed for SPA and use_lm_t)

residuals

(Continuous only) Numeric vector of residuals

s0

(Continuous only) Numeric scalar, residual standard deviation

H_half

Numeric matrix (n x k+1), precomputed projection matrix for efficient marginal scan. For binary: X_tilde (X_tilde' X_tilde)^-1/2; for continuous: X (X' X)^-1/2.

weights

(Binary only) Numeric vector of length n, sqrt(mu_0 * (1 - mu_0))

sample_id

Character vector of sample IDs (if provided)

Details

In genome-wide association studies, researchers typically test many SNP sets (genes, pathways, regions) against the same phenotype and covariates. The standard workflow repeatedly fits the null model (expensive GLM fitting), even though it's identical across all tests.

This function enables a two-step optimized workflow:

  1. Fit null model once: null_obj <- fit_null_model(X, Y, trait)

  2. Reuse for all SNP sets: getZ_marg_score(G_i, null_model = null_obj)

For Binary Traits:

Fits logistic regression \(\text{logit}(P(Y=1)) = X\beta\) and stores:

  • Fitted probabilities \(Y_0 = P(Y=1|X)\) (for weighting)

  • Covariate matrix with intercept (for projection)

  • Response vector (needed for SPA calculations)

  • Sample size (for validation)

For Continuous Traits:

Fits linear regression \(Y = X\beta + \epsilon\) and stores:

  • Residuals \(r = Y - X\hat{\beta}\)

  • Residual standard deviation \(s_0 = \text{SD}(r)\)

  • Covariate matrix with intercept (for projection)

  • Sample size (for validation)

Performance Impact:

For testing 1000 genes with 500 samples:

  • Standard mode: ~50 seconds (fit null model 1000 times)

  • Optimized mode: ~5 seconds (fit null model once, reuse 1000 times)

  • Speedup: 10x faster

Speedup increases with more tests and larger samples.

References

Zhang, H., Liu, M., Landers, J. E., and Wu, Z. (2026). Integrated weighted association test with application to genetic association studies. The Annals of Applied Statistics, 20(3), 2074-2097. doi:10.1214/26-AOAS2223

See also

getZ_marg_score for standard score calculation getZ_marg_score_binary_SPA for SPA-adjusted score calculation

Examples

# Example 1: Binary trait - standard vs. optimized workflow
set.seed(123)
n <- 500
X <- matrix(rnorm(n * 2), n, 2)
Y <- sample(c(0, 1), n, replace = TRUE)

# Create 10 different SNP sets
gene_sets <- lapply(1:10, function(i) {
  matrix(rbinom(n * 5, 2, 0.1), n, 5)
})

# Standard workflow (slow - fits null model 10 times)
if (FALSE) { # \dontrun{
results_slow <- lapply(gene_sets, function(G) {
  getZ_marg_score(G, X, Y, trait = "binary")
})
} # }

# Optimized workflow (fast - fits null model once)
null_obj <- fit_null_model(X, Y, trait = "binary")
results_fast <- lapply(gene_sets, function(G) {
  getZ_marg_score(G, null_model = null_obj)
})

# Example 2: Continuous trait
Y_cont <- rnorm(n)
null_obj_cont <- fit_null_model(X, Y_cont, trait = "continuous")
result <- getZ_marg_score(gene_sets[[1]], null_model = null_obj_cont)

# Example 3: Using with SPA-adjusted method
null_obj_spa <- fit_null_model(X, Y, trait = "binary")
result_spa <- getZ_marg_score_binary_SPA(gene_sets[[1]], null_model = null_obj_spa)