Package 'sffdr'

Title: Surrogate Functional False Discovery Rates for Genome-Wide Association Studies
Description: Pleiotropy-informed significance analysis of genome-wide association studies with surrogate functional false discovery rates (sfFDR). The sfFDR framework adapts the fFDR to leverage informative data from multiple sets of GWAS summary statistics to increase power in study while accommodating for linkage disequilibrium. sfFDR provides estimates of key FDR quantities in a significance analysis such as the functional local FDR and $q$-value, and uses these estimates to derive a functional $p$-value for type I error rate control and a functional local Bayes' factor for post-GWAS analyses (e.g., fine mapping and colocalization).
Authors: Andrew Bass [aut, cre], Chris Wallace [aut]
Maintainer: Andrew Bass <[email protected]>
License: MIT + file LICENSE
Version: 1.1.4
Built: 2026-10-20 19:39:31 UTC
Source: https://github.com/ajbass/sffdr

Help Index


Subset of p-values from the UK Biobank

Description

A dataset containing a subset of p-values from the UK Biobank.

Format

A data frame with 10,000 rows and 4 columns:

bmi

Body mass index

bfp

Body fat percentage

cholesterol

Cholesterol

triglycerides

Triglycerides

Examples

# Import data
data(bmi)

# Separate main p-values and informative p-values
p <- sumstats$bmi
z <- as.matrix(sumstats[, -1])

Decorrelate the informative studies from the primary study

Description

When the primary study shares samples with the informative studies, their null z-scores are correlated. Used as covariates in pi0_model, the informative p-values then predict the primary's null tests, which inflates false discoveries. decorrelate_informative() removes the primary's noise from each informative study,

uk=(zk−rkz1)/1−rk2,u_k = (z_k - r_k z_1) / \sqrt{1 - r_k^2},

where rkr_k is the null correlation of study kk with the primary. Under the primary's null, uu is independent of z1z_1 whatever the informative studies' effects, so the primary's p-values are analysed unchanged with the decorrelated p-values as covariates.

Usage

decorrelate_informative(
  z_x,
  z_y,
  p_x = NULL,
  p_y = NULL,
  rho_o = NULL,
  lfdr_x = NULL,
  indep_snps = NULL
)

Arguments

z_x

Numeric vector; the primary study's z-scores.

z_y

Numeric vector or matrix; signed z-scores of the informative studies (one column per study), in the same order as z_x.

p_x, p_y

Ignored; kept for compatibility with sffdr 1.1.2.

rho_o

Optional overlap correlations of the primary with each informative study (one number per study). If NULL (default), estimated with estimate_overlap.

lfdr_x

Ignored. sffdr 1.1.2 scaled the correction by the primary's local FDR, which left part of the overlap in the covariates.

indep_snps

Optional logical or integer vector selecting approximately independent SNPs, used to estimate rho_o.

Details

The correction needs only the overlap correlation of each informative study with the primary: cross-trait LD score regression intercepts are the recommended source for GWAS; otherwise they are estimated by estimate_overlap. Errors in them enter at second order: in simulations, a correlation of 0.5 given as 0.35 or 0.65 left the FDR within 0.005 of its value at the true correlation.

Value

A list with

z2

decorrelated z-scores of the informative studies (vector or matrix, as z_y)

p2

their p-values, for use as covariates

rho_o

the overlap correlations used

See Also

estimate_overlap

Examples

set.seed(1)
n <- 20000
e1 <- rnorm(n)
e2 <- 0.5 * e1 + sqrt(1 - 0.5^2) * rnorm(n)
z_x <- e1 + ifelse(runif(n) < 0.05, 3, 0)            # primary
z_y <- e2 + ifelse(runif(n) < 0.1, 3, 0)             # informative, same samples
dec <- decorrelate_informative(z_x, z_y, rho_o = 0.5)

p_x <- 2 * pnorm(-abs(z_x))
mpi0 <- pi0_model(as.matrix(dec$p2))
fpi0 <- fpi0est(p_x, pi0_model_obj = mpi0)
res <- sffdr(p_x, fpi0 = fpi0$fpi0)

Estimate the overlap correlation between studies

Description

Estimates the null correlation of z-scores between studies that share samples, for every pair of cbind(z, z_informative). For each pair it uses the mean product of z-scores inside the box ∣z1∣,∣z2∣<t|z_1|, |z_2| < t, which is unaffected by signal present in only one of the two studies, and inverts it through the exact truncated bivariate normal moment. Cross-trait LD score regression intercepts are preferable for GWAS when available.

Usage

estimate_overlap(z, z_informative, indep_snps = NULL, threshold = 1.5)

Arguments

z

Numeric vector; the primary study's z-scores.

z_informative

Numeric vector or matrix of the informative studies' z-scores.

indep_snps

Optional logical or integer vector of approximately independent SNPs to use.

threshold

Numeric; the box half-width tt (default 1.5). Smaller boxes are less affected by moderate effects and noisier.

Value

The (K+1)×(K+1)(K+1) \times (K+1) correlation matrix, primary first.

See Also

decorrelate_informative


Estimate Functional Proportion of Null Tests

Description

Estimates the functional proportion of null tests (pi0) using a GLM approach with a constrained binomial family. This function fits models across multiple lambda thresholds and selects the optimal estimate via MISE minimization.

Usage

fpi0est(
  p,
  pi0_model_obj = NULL,
  z = NULL,
  pi0_model = NULL,
  indep_snps = NULL,
  weights = NULL,
  lambda = seq(0.05, 0.95, 0.05),
  constrained.p = TRUE,
  tol = 1e-09,
  maxit = 200,
  ncores = 1L,
  verbose = TRUE,
  ...
)

Arguments

p

Numeric vector of p-values.

pi0_model_obj

Optional list object returned by pi0_model. Must contain fmod (formula) and zt (rank-transformed covariates). If provided, z and pi0_model are extracted automatically. Default is NULL.

z

Optional data frame or matrix of rank-transformed covariates. Required if pi0_model_obj is not provided. Default is NULL.

pi0_model

Optional formula (as character string or formula object). Required if pi0_model_obj is not provided. Default is NULL.

indep_snps

Optional independent SNPs for model fitting: a logical vector with one entry per p-value, row indices, or SNP ids matched against rownames(z). Default is NULL (all SNPs used).

weights

Optional numeric vector of weights in (0, 1], one per p-value, for the model fit. For GWAS data, this should be inverse LD scores (values above 1 are clamped to 1 with a warning). If these are available then you don't have to use the indep_snps argument, as the weights will effectively prioritize independent SNPs in the fitting process.

lambda

Numeric vector of lambda thresholds in (0, 1). Default is seq(0.05, 0.95, 0.05).

constrained.p

Logical; use constrained binomial family. Default is TRUE.

tol

Numeric; convergence tolerance. Default is 1e-9.

maxit

Integer; maximum iterations. Default is 200.

ncores

Integer; number of cores for parallel lambda fitting via parallel::mclapply. Fork-based parallelism (Unix/macOS only) - on Windows this falls back to sequential execution. Default is 1L.

verbose

Logical; print progress messages. Default is TRUE.

...

Additional arguments passed to fastglm.

Details

Algorithm:

  1. For each lambda threshold, fit a binomial GLM: P(p≥λ∣z)P(p \ge \lambda | z)

  2. Use constrained binomial family to ensure predictions in (0, 1)

  3. Select optimal lambda via MISE minimization

Usage Patterns:

Pattern 1 (Recommended): Use output from pi0_model

  mpi0 <- pi0_model(z)
  # Clean syntax:
  fpi0_out <- fpi0est(p, mpi0)

Pattern 2: Manually specify formula and covariates

  fpi0_out <- fpi0est(p, z = z_matrix, pi0_model = formula_obj)

Value

An object of class fpi0 (a list) containing:

fpi0

Numeric vector of functional pi0 estimates for each test, floored at 0.01. Tests with a missing p-value or covariate get 1.

tableLambda

A data frame with one row per lambda value: the MISE, the mean fitted pi0 and whether it was chosen.

MISE

The Mean Integrated Squared Error (MISE) at each lambda.

lambda

The selected optimal lambda value.

model

The fitted model at the selected lambda (class fastglm2; predict() gives P(p≥λ∣z)P(p \ge \lambda | z)).

Examples

# Import data
data(bmi)

# Separate main p-values and conditioning p-values
p <- sumstats$bmi
z <- as.matrix(sumstats[, -1])

# Apply pi0_model to create model (uses adaptive knot selection)
fmod <- pi0_model(z)

# Estimate functional pi0
fpi0_out <- fpi0est(p, fmod)
fpi0 <- fpi0_out$fpi0

# Apply sffdr
sffdr_out <- sffdr(p, fpi0)

Functional p-values

Description

Calculate functional p-values from functional local FDRs.

Usage

fpvalues(lfdr, p = NULL)

Arguments

lfdr

A vector of functional local FDRs in [0, 1] (values above 1 are clamped to 1).

p

A vector of p-values for ranking purposes. Default is NULL.

Details

Tests are ranked by lfdr, with ties broken by p. The functional q-value of a test is the mean lfdr of the tests ranked at or above it, and its functional p-value is the lfdr mass ranked at or above it as a fraction of the total mass. Tests with identical lfdr and p are one set: each receives the set's values, so the result does not depend on the order of the input. When p is NULL, ties are broken at random.

Value

A list containing:

fp

Functional p-values.

fq

Functional q-values.


Kernel Density Estimation for GWAS P-values

Description

Performs kernel density estimation on p-values (univariate) or joint p-value/covariate pairs (bivariate) using local regression on the probit scale. The estimator is optimized for GWAS data with linkage disequilibrium (LD) and uses adaptive downsampling to prioritize signal-rich regions while maintaining computational efficiency.

Usage

kernelEstimator(
  x,
  eval.points = x,
  epsilon = .Machine$double.xmin,
  epsilon.max = 1 - 1e-04,
  maxk = 5000,
  maxit = 200,
  target_null = 1e+05,
  trim = 0,
  nn = NULL,
  tail_threshold = -2,
  weights = NULL,
  verbose = TRUE,
  ...
)

Arguments

x

Numeric vector of p-values (for 1D density) or a 2-column matrix where the first column contains an informative covariate/surrogate and the second column contains the p-values. All p-values must be in (0, 1) and the covariate/surrogate must be rank-transformed to be (0,1].

eval.points

Points at which to evaluate the density estimate. Defaults to x. For custom evaluation points, must match the dimensionality of x (vector or 2-column matrix).

epsilon

Lower bound for p-values to prevent numerical issues. P-values below this are clamped to epsilon. Default: .Machine$double.xmin.

epsilon.max

Upper bound for p-values. P-values above this are clamped to epsilon.max. Default: 1 - 1e-4.

maxk

Space allocated for locfit's evaluation structure. It does not affect the fit: when it is too small, locfit stops with "out of vertex space" and the fit is repeated with ten times the space. Default: 5000 (about 35 MB; versions before 1.1.4 allocated 2.7 GB).

maxit

Maximum number of iterations for the bivariate local likelihood fit. Default: 200. The univariate fit uses at most 20 (locfit's default), because more iterations let it diverge on tied values.

target_null

Maximum number of null SNPs to include in the weighted fit (bivariate case only). SNPs in the signal-enriched tail (defined by tail_threshold) are always retained; null SNPs are downsampled to this target and upweighted accordingly. Default: 100000.

trim

Numeric in [0, 1); if > 0, flattens the density for p-values in (1 - trim, 1) to reduce boundary artifacts from p-values artificially clumped near 1. Only applies to the p-value dimension. Default is 0 (no trimming).

nn

Nearest-neighbor bandwidth parameter for locfit, expressed as a fraction of the rows the density is fitted on. If NULL (default), chosen so that the window spans approximately 5000 of those rows (which corresponds to multiple LD blocks in GWAS); see Details. Larger values increase smoothing.

tail_threshold

Threshold on the probit-transformed p-values (bivariate case only). SNPs with qnorm(p) < tail_threshold form the signal-enriched tail, which is never downsampled. Default: -2 (p-values below about 0.023).

weights

Optional numeric vector of prior weights in (0, 1] for the fit (values above 1 are clamped to 1).

verbose

Logical; whether to print progress messages during fitting. Default is TRUE.

...

Additional arguments passed to locfit (for example scale). The arguments nn, h, deg, kern, ev, maxk, maxit, weights and data are set by the estimator and cannot be passed here.

Details

The function implements a multi-stage density estimation procedure:

  1. Probit transformation: P-values are transformed to the normal scale via qnorm to stabilize variance and handle extreme values.

  2. Adaptive downsampling (bivariate only): To handle large GWAS datasets efficiently, the null region (p-values above the tail threshold) is downsampled to target_null SNPs, with inverse-probability weighting to preserve the density. Signal-enriched SNPs (tail) are always retained.

  3. Cascade fitting: Multiple fitting strategies are attempted in sequence, with decreasing resolution and increasing robustness, until a valid fit is obtained.

  4. Jacobian correction: Density estimates are transformed back to the original p-value scale using the Jacobian of the probit transformation.

The nearest-neighbor bandwidth nn controls smoothing and LD robustness. By default the window spans about 5000 of the rows the density is fitted on (the tail plus up to target_null null SNPs), so the estimator averages over multiple LD blocks (~30-50 blocks in European ancestry populations), reducing spurious local structure while preserving true signal-covariate relationships. Versions before 1.1.4 took the fraction relative to all SNPs, which left about 600 neighbours at one million SNPs.

In the univariate case, tied values are collapsed into one point with the corresponding multiplicity before fitting.

Value

A data frame with columns:

x

Evaluation points (original scale).

fx

Estimated density at evaluation points (original scale).

s

Evaluation points on probit scale (qnorm(x)).

fs

Estimated density on probit scale.

The returned object has an attribute "lfit" containing the fitted locfit object for diagnostics.

See Also

locfit, sffdr

Examples

## Not run: 
# 1D density estimation
p <- runif(10000, 0, 1)
dens <- kernelEstimator(p)
plot(dens$x, dens$fx, type = "l")

# 2D density: the covariate (rank-transformed) first, the p-values second
p <- runif(10000, 0, 1)
z <- runif(10000)
x_mat <- cbind(z, p)
dens <- kernelEstimator(x_mat)

## End(Not run)

Fast Conditional Monotonic Smoothing

Description

Enforces strictly non-increasing density conditional on surrogate bins. Note: Data must be sorted by (group, pvalue) before passing.

Usage

monoSmooth_conditional(pvalue, density, group)

Arguments

pvalue

Numeric vector of p-values.

density

Numeric vector of estimated densities.

group

Integer vector of surrogate bins.

Value

A numeric vector of smoothed densities.


Fast Conditional PAVA (Decreasing)

Description

Fast Conditional PAVA (Decreasing)

Usage

monoSmooth_pava(pvalue, density, group)

Arguments

pvalue

A numeric vector of p-values. Must be sorted ascendingly within each group.

density

A numeric vector of initial density estimates (e.g., from locfit).

group

An integer vector denoting the surrogate bin/stratum for each observation.

Value

A numeric vector of smoothed, monotonically non-increasing densities.


Build Model for Functional Proportion of Null Tests (fpi0)

Description

Generates a natural spline model for the functional proportion of null tests (fpi0) by adaptively selecting knots based on an FDR threshold.

Usage

pi0_model(
  z,
  indep_snps = NULL,
  weights = NULL,
  fdr_threshold = 0.25,
  min_discoveries = 150,
  min_snps_per_knot = 100,
  n_knots = 5,
  verbose = TRUE,
  seed = 2026
)

Arguments

z

Numeric matrix/data.frame of p-values (rows=tests, cols=traits).

indep_snps

Independent SNPs (the training subset): a logical vector with one entry per row of z, row indices, or SNP ids matched against rownames(z). If NULL, uses all tests. Default: NULL.

weights

Optional numeric vector of weights in (0, 1] (e.g., inverse LD scores) for weighted spline fitting. Default: NULL.

fdr_threshold

FDR threshold for signal definition. Default: 0.25.

min_discoveries

Min (LD-independent) significant hits required to place a trait's knots from its discoveries; with fewer, the trait keeps a single fallback knot. Default: 150.

min_snps_per_knot

Base minimum significant SNPs per knot interval. Default: 100. Dynamically scales up for larger datasets to prevent overfitting.

n_knots

Target knot count. Default: 5. Automatically reduced if insufficient discoveries (via min_snps_per_knot).

verbose

Print selection details. Default: TRUE.

seed

Optional seed for reproducibility. Default: 2026.

Details

Independent SNPs determine knot placement; all SNPs train the model to capture signal shape. For smaller datasets (<100K), consider reducing min_snps_per_knot and min_discoveries to improve knot placement. Every column must hold p-values: the knots of a column are placed at the quantiles of its discoveries (qvalue at fdr_threshold), and a column with values outside [0, 1] gets the fallback knot with a warning. A null anchor knot at rank 0.1 is always added.

Value

List containing:

fmod

Model formula (using splines::ns)

zt

Data frame of globally rank-transformed p-values


Plotting function for sffdr object

Description

Graphical display of the sffdr object

Usage

## S3 method for class 'sffdr'
plot(x, rng = c(0, 5e-08), ...)

Arguments

x

A sffdr object.

rng

Significance region to show (functional p-value range). Optional. If no functional p-value falls in it, the range is widened to the smallest one percent.

...

Additional arguments. Currently unused.

Value

Nothing of interest.

Author(s)

Andrew J. Bass

See Also

sffdr

Examples

## Not run: 
# import data
data(bmi)

# Separate main p-values and conditioning p-values
p <- sumstats$bmi
z <- as.matrix(sumstats[, -1])

# Apply pi0_model to create model
fmod <- pi0_model(z)

# Estimate functional pi0
fpi0_out <- fpi0est(p, z = fmod$zt, pi0_model = fmod$fmod)
fpi0 <- fpi0_out$fpi0

# Apply sffdr
sffdr_out <- sffdr(p, fpi0)

# Plot significance results
plot(sffdr_out, rng = c(0, 5e-4))

## End(Not run)

Surrogate Functional False Discovery Rate Analysis

Description

Estimate functional p-values, q-values, and local false discovery rates (lfdr) for GWAS data leveraging summary statistics from related traits. Functional p-values map from the functional q-value (FDR-based measure) to a p-value for type I error rate control, accounting for pleiotropy that impacts the prior probability of association.

Usage

sffdr(
  p.value,
  fpi0,
  surrogate = NULL,
  weights = NULL,
  epsilon = .Machine$double.xmin,
  nn = NULL,
  monotone = TRUE,
  monotone_method = c("min", "pava"),
  fp_ties = TRUE,
  seed = 2026,
  verbose = TRUE,
  ...
)

Arguments

p.value

Numeric vector of p-values to analyze, in [0, 1]. Exact zeros are clamped to the smallest positive double with a warning; negative values are an error.

fpi0

Numeric vector of functional pi0 estimates, obtained from fpi0est. Values must be in [0, 1].

surrogate

Optional numeric vector (same length as p.value) used as a surrogate variable for compression. If NULL (default), uses fpi0 as the surrogate.

weights

Ignored, with a warning (kept for compatibility with sffdr 1.1.2); see Details.

epsilon

Numeric; lower bound for p-value clamping during density estimation. Default is .Machine$double.xmin.

nn

Numeric; nearest-neighbor bandwidth for kernelEstimator, a fraction of the rows the density is fitted on. If NULL (default), the window spans ~5000 of those rows.

monotone

Logical; if TRUE, enforces monotonicity of the estimated density with respect to p-values within bins of the surrogate variable. Default is TRUE.

monotone_method

Character; method for enforcing monotonicity. Options are "min" (running minimum) or "pava" (Pool Adjacent Violators Algorithm). Default is "min". We do not recommend "pava" for GWAS data sets due to LD, but it may be better for expression data.

fp_ties

Logical; whether to break ties in functional p-values using the original p-value ordering. Default is TRUE.

seed

Integer; random seed for reproducibility of rank tie-breaking, the jitter and the null downsampling in the density fit. Default is 2026.

verbose

Logical; print progress messages. Default is TRUE.

...

Additional arguments passed to kernelEstimator.

Details

The surrogate functional FDR (sfFDR) methodology extends the functional FDR framework to leverage multiple informative variables (e.g., functional annotations, GWAS summary statistics) for increased power while controlling false discovery rates.

Workflow:

  1. Estimate functional pi0 (proportion of nulls) using fpi0est

  2. Call sffdr() with p-values and estimated functional pi0

  3. Use returned functional p-values/q-values/local FDRs for significance testing

Surrogate Variable: If not specified, the estimated functional pi0 is used as the surrogate variable.

Weights and LD: The joint density is estimated over all tested SNPs, which is the set the functional p-values count errors in, so sffdr() no longer reweights it. Inverse-LD weights change the density to that of independent signals; because associated SNPs have larger LD scores, this biases the local FDR (conservative with inverse-LD weights, anti-conservative with weights that are larger at associated SNPs). LD scores carry information about association, so their place is the functional pi0 model, as a covariate (see the vignette); as inverse weights they remain available in pi0_model and fpi0est in place of an LD-pruned training set.

Perfect-LD copies: SNPs with identical p-values are copies of one signal (perfect LD). Each set of copies contributes one point to the density fit, and the density is then evaluated for every SNP, so copies do not narrow the effective window in the tail (which inflated functional p-values below 1e-3 before 1.1.4). Rounded p-values tie between unrelated SNPs too, almost only in the bulk of the distribution where values are dense per significant digit, and collapsing those would distort the density; so identical p-values count as copies only when they are at least half as common below p = 0.023 as above it, which holds for LD copies and fails for rounding. The window itself spans about 5,000 of the rows the density is fitted on; see kernelEstimator.

Interpretation note: Functional p-values reflect the global ranking of all SNPs by the functional local FDR. A SNP with a small functional p-value but large flfdr (> 0.5) has weak individual evidence but ranks favorably relative to null SNPs. This happens when there is strong prior evidence (fpi0) of the SNP being non-null. Thus, users should also consider flfdr (and fqvalues) alongside fpvalues when assessing individual SNP evidence. SNPs with flfdr > 0.5 should be interpreted cautiously regardless of their functional p-value.

Value

An S3 object of class "sffdr" containing:

call

The function call.

pvalues

The p-values as analysed (exact zeros clamped).

fpvalues

Functional p-values.

fqvalues

Functional q-values.

flfdr

Functional local false discovery rates.

fpi0

Functional pi0 estimates.

fx

Joint density estimates at observed (p-value, surrogate) pairs.

Author(s)

Andrew J. Bass

See Also

fpi0est, plot.sffdr, kernelEstimator

Examples

## Not run: 
# Import data
data(bmi)

# Separate main p-values and conditioning p-values
p <- sumstats$bmi
z <- as.matrix(sumstats[, -1])

# Apply pi0_model to create model
# (note: use indep_snps argument to specify independent SNPs for training)
fmod <- pi0_model(z)

# Estimate functional pi0
# (note: use indep_snps argument to specify independent SNPs for training)
fpi0_out <- fpi0est(p, fmod)
fpi0 <- fpi0_out$fpi0

# Apply sffdr
sffdr_out <- sffdr(p, fpi0)

# Plot significance results
plot(sffdr_out)

# Extract functional quantities
fp <- sffdr_out$fpvalues
fq <- sffdr_out$fqvalues
flfdr <- sffdr_out$flfdr

## End(Not run)