Files
GraphonSimulation/R/graphon_distribution.R
T
2026-06-29 14:46:22 +02:00

384 lines
15 KiB
R
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Load the general quantile function, the loading can be improved later
# using the here package.
source(here::here("R", "qinf.R"))
# 0. Analytical test case for d = 1 -----------------------------
#' --------------------------------------------------------------
#' Fa(y) = ∫ Fv(y - z) fZ(z) dz with Z = a * X
#' --------------------------------------------------------------
#' Compute the graphon CDF for a scalar X and scalar coefficient a.
#'
#' @param y numeric vector of points where Fa(y) is required.
#' @param a numeric scalar (coefficient). a != 0.
#' @param fX function(z) returning the pdf of X (vectorised).
#' @param Fv function(x) returning the CDF of the noise v (vectorised).
#' @param lower,upper numeric bounds for the integration over z.
#' By default they are set to the (practical) support of X
#' transformed by a. You can tighten them for speed.
#' @param rel.tol relative tolerance passed to `integrate`.
#' @return numeric vector of the same length as y.
#' @examples
#' ## Example 1: X ~ Gamma(2,1), a = 0.8, v ~ N(0,1)
#' fX <- function(z) dgamma(z, shape = 2, rate = 1) # pdf of X
#' Fv <- function(x) pnorm(x, mean = 0, sd = 1) # cdf of v
#' a <- 0.8
#' y <- seq(-3, 6, length.out = 200)
#' Fa_vals <- Fa_one_dim(y, a, fX, Fv)
#' plot(y, Fa_vals, type = "l", col = "steelblue",
#' main = "Fa(y) for Gamma X, Normal noise",
#' xlab = "y", ylab = expression(F[a](y)))
#'
#' ## Example 2: both X and v are normal analytic check
#' fX2 <- function(z) dnorm(z, mean = 1, sd = 2)
#' Fv2 <- function(x) pnorm(x, mean = 0, sd = 1)
#' a2 <- -1.5
#' y2 <- seq(-5, 8, length.out = 100)
#' Fa2_num <- Fa_one_dim(y2, a2, fX2, Fv2)
#' ## analytic result: Y = a*X + v ~ N(a*mu_X, a^2*sd_X^2 + sd_v^2)
#' muY <- a2 * 1
#' sdY <- sqrt(a2^2 * 2^2 + 1^2)
#' Fa2_ana <- pnorm(y2, mean = muY, sd = sdY)
#' max(abs(Fa2_num - Fa2_ana)) # should be ~ 1e-6
#' --------------------------------------------------------------
pgraphon_analytical_1d <- function(y, a, fX, Fv,
lower = -Inf, upper = Inf,
rel.tol = .Machine$double.eps^0.5) {
stopifnot(is.numeric(a), length(a) == 1, a != 0)
stopifnot(is.function(fX), is.function(Fv))
stopifnot(is.numeric(y))
# pdf of Z = a*X
fZ <- function(z) {
# Jacobian |a|^{-1} and argument scaling
(1 / abs(a)) * fX(z / a)
}
# Helper that integrates for a single y
one_y <- function(yy) {
integrand <- function(z) Fv(yy - z) * fZ(z)
integrate(integrand, lower = lower, upper = upper,
rel.tol = rel.tol)$value
}
# Vectorised over the whole y vector
vapply(y, one_y, numeric(1))
}
# 1. Distribution Function -----------------------------------------------------
#' Empirical Distribution function for the graphon model.
#'
#' Computes the empirical expectation
#' \deqn{E[F_v(y - a^\top X_i)]}
#' using observed covariate samples \eqn{X_i}. This is done by evaluating the
#' supplied CDF \code{Fv} at the shifted values \eqn{y - a^\top X_i} and taking
#' their empirical average. See also Remark 2.1
#'
#' @param y Numeric scalar or vector of evaluation points.
#' @param a Numeric vector. Coefficient vector used to compute the inner products
#' \eqn{a^\top X_i}. Its length must match the number of columns in
#' \code{X_matrix}.
#' @param Fv Function. Cumulative distribution function of the noise variable
#' \eqn{v} (e.g., \code{pnorm}, \code{pexp}, etc.). Must accept a numeric vector
#' and return a numeric vector of the same length.
#' @param X_matrix Numeric matrix of dimension \eqn{n \times p}, where each row
#' corresponds to an observed covariate vector \eqn{X_i}.
#'
#' @return A numeric scalar giving the empirical expectation
#' \eqn{(1/n) \sum_{i=1}^n F_v(y - a^\top X_i)}.
#'
#' @examples
#' set.seed(1)
#' X <- matrix(rnorm(100), ncol = 2)
#' a <- c(1, -0.5)
#' y <- 0
#' pgraphon(y, a, pnorm, X)
#'
#' @export
pgraphon <- function(
y, # scalar
a, # vector (coefficient vector, can be one-dimensional)
Fv, # CDF function of the v's (e.g., pnorm, pexp, etc.)
X_matrix # n x p matrix: each row is X_i
) {
## 1.1 Check inputs ==========================================================
if (ncol(X_matrix) != length(a)) {
stop("Number of columns in X_matrix must match length of a")
}
if (!is.numeric(y)) {
stop("'y' must be numeric (scalar or vector)")
}
if (!is.numeric(a) || !is.vector(a)) {
stop("'a' must be a numeric vector")
}
if (!is.matrix(X_matrix) || !is.numeric(X_matrix)) {
stop("'X_matrix' must be a numeric matrix")
}
if (!is.function(Fv)) {
stop("'Fv' must be a function (a CDF)")
}
## 1.2 Compute the expected value ============================================
# Compute a^T X_i as in the paper. Here we switched to the R convention that each
# observation is a row. The paper assumes that each observation is a vector
inner_products <- as.vector(X_matrix %*% a) # vector of length n
# outer(y, inner_prod, "-") creates an |y| × n matrix where element (j,i)
# equals y[j] - inner_prod[i].
dev_mat <- outer(y, inner_products, "-")
# Apply CDF Fv to each deviation
cdf_vals <- Fv(dev_mat)
# Row means give the empirical expectation for each y_j
out <- rowMeans(cdf_vals)
out
}
# 2. Density Function ----------------------------------------------------------
#' Empirical Graphon Density Estimate
#'
#' Computes the empirical expectation \eqn{E[f_v(y - a^\top X_i)]} using observed
#' covariate samples \eqn{X_i}. This serves as an empirical approximation of the
#' graphon density evaluated at \eqn{y}, where \eqn{f_v} is the density of the
#' latent variable \eqn{v}. See also Remark 2.1
#'
#' @param y Numeric scalar or vector of points at which the density should be
#' evaluated.
#' @param a Numeric vector. Coefficient vector used to compute the linear
#' projection \eqn{a^\top X_i}. Its length must match the number of columns in
#' \code{X_matrix}.
#' @param fv Function. Density function of the latent variable \eqn{v} (e.g.,
#' \code{dnorm}, \code{dexp}). Must accept a numeric vector and return a
#' numeric vector of the same length.
#' @param X_matrix Numeric matrix of size \eqn{n \times p}. Each row represents
#' an observed covariate vector \eqn{X_i}. The number of columns must match the
#' length of \code{a}.
#'
#' @return A numeric scalar giving the empirical average
#' \eqn{\frac{1}{n} \sum_{i=1}^n f_v(y - a^\top X_i)}.
#'
#' @examples
#' set.seed(1)
#' X <- matrix(rnorm(50), ncol = 2)
#' a <- c(1, -0.5)
#' dgraphon(y = 0.2, a = a, fv = dnorm, X_matrix = X)
#'
#' @export
dgraphon <- function(
y, # scalar
a, # vector (coefficient vector, can be )
fv, # density function of the v's (e.g., dnorm, dexp, etc.)
X_matrix # n x p matrix: each row is X_i
) {
## 2.1 Check inputs ----------------------------------------------------------
if (!is.numeric(y)) {
stop("'y' must be numeric (scalar or vector)")
}
if (!is.numeric(a) || !is.vector(a)) {
stop("'a' must be a numeric vector")
}
if (!is.matrix(X_matrix) || !is.numeric(X_matrix)) {
stop("'X_matrix' must be a numeric matrix")
}
if (ncol(X_matrix) != length(a)) {
stop("Number of columns in X_matrix must match length of a")
}
if (!is.function(fv)) {
stop("'fv' must be a function (a density)")
}
## 1.2 Compute the expected value ============================================
# Compute a^T X_i as in the paper. Here we switched to the R convention that each
# observation is a row. The paper assumes that each observation is a vector
inner_products <- as.vector(X_matrix %*% a) # vector of length n
# outer(y, inner_prod, "-") creates an |y| × n matrix where element (j,i)
# equals y[j] - inner_prod[i].
dev_mat <- outer(y, inner_products, "-")
# Apply the density function to each y[j]
dens_vals <- fv(dev_mat)
# Row means give the empirical expectation for each y_j
out <- rowMeans(dens_vals)
out
}
# 3. Quantile Function ---------------------------------------------------------
#' Quantile of the (empirical or analytic) graphon distribution
#'
#' If the covariate matrix has a single column and the coefficient vector
#' has length 1, the function can use an *analytic* CDF (provided the user
#' supplies the pdf of the covariate via `fX`). Otherwise it falls back to the
#' original empirical estimator `pgraphon`.
#'
#' @param p Numeric vector of probabilities in \eqn{[0,1]}.
#' @param a Numeric coefficient vector (length = ncol(X_matrix)).
#' @param Fv Function. CDF of the latent variable $v$ (vectorised).
#' @param X_matrix Numeric matrix of size $n\times p$; each row is an observation.
#' @param fX Optional function(z) returning the *known* pdf of the *scalar*
#' covariate when `ncol(X_matrix)==1 && length(a)==1`. If supplied,
#' the analytic CDF `pgraphon_analytical_1d` is used.
#' @param lower,upper Numeric bounds for the rootfinding algorithm.
#' @param tol Numeric tolerance for the bisection inside `qinf`.
#' @param max.iter Maximum number of bisection iterations.
#' @param ... Additional arguments passed to the internal CDF (`pgraphon` or the
#' analytic version). For the analytic case you may want to pass
#' `lower`, `upper`, or `rel.tol`.
#' @return Numeric vector of quantiles, named by the probabilities.
#' @examples
#' ## 1. Empirical case
#' set.seed(123)
#' X <- matrix(rnorm(200), ncol = 2)
#' a <- c(0.7, -0.3)
#' qgraphon(p = c(0.25, 0.5, 0.75), a = a,
#' Fv = pnorm, X_matrix = X)
#'
#' ## 2. Analytic 1D case
#' ## X ~ Gamma(2,1), a = 0.8, v ~ N(0,1)
#' fX <- function(z) dgamma(z, shape = 2, rate = 1)
#' a1 <- 0.8
#' X1 <- matrix(rnorm(100), ncol = 1) # dummy not used by the analytic branch
#' qgraphon(p = c(0.1, 0.5, 0.9), a = a1,
#' Fv = pnorm, X_matrix = X1,
#' fX = fX, lower = -10, upper = 20)
#' @export
qgraphon <- function(p, a, Fv, X_matrix,
fX = NULL, # <- new optional argument
lower = -Inf, upper = Inf,
tol = .Machine$double.eps^0.5,
max.iter = 100, ...) {
## ---- input checks -------------------------------------------------
if (!is.numeric(p) || any(is.na(p))) {
stop("'p' must be a numeric vector without NA values")
}
if (any(p < 0 | p > 1)) {
stop("All probabilities in 'p' must lie in [0, 1]")
}
if (!is.numeric(a) || !is.vector(a)) {
stop("'a' must be a numeric vector")
}
if (!is.matrix(X_matrix) || !is.numeric(X_matrix)) {
stop("'X_matrix' must be a numeric matrix")
}
if (ncol(X_matrix) != length(a)) {
stop("Number of columns of 'X_matrix' (", ncol(X_matrix),
") must equal length of 'a' (", length(a), ")")
}
if (!is.function(Fv)) {
stop("'Fv' must be a function (the CDF of the latent variable)")
}
## ---- decide which CDF to use --------------------------------------
use_analytic <- (ncol(X_matrix) == 1L) && (length(a) == 1L) && (!is.null(fX))
if (use_analytic) {
# Build a *wrapper* that has the same signature as pgraphon()
# (y, a, Fv, X_matrix, ...) so that qinf() can call it unchanged.
analytic_cdf <- function(y, a, Fv, X_matrix, ...) {
# X_matrix is ignored the distribution of X is supplied via fX
pgraphon_analytical_1d(y = y,
a = a,
fX = fX,
Fv = Fv,
lower = lower,
upper = upper,
rel.tol = tol,
...)
}
CDF_to_use <- analytic_cdf
} else {
# Fall back to the empirical estimator
CDF_to_use <- pgraphon
}
## ---- call the generic quantile routine ----------------------------
out <- qinf(F = CDF_to_use,
p = p,
lower = lower,
upper = upper,
tol = tol,
max.iter = max.iter,
a = a,
X_matrix = X_matrix,
Fv = Fv,
...) # forward any extra arguments (e.g., rel.tol)
names(out) <- as.character(p)
out
}
# 4. Create conditional density ------------------------------------------------
#' Create a Conditional Density Function for the Graphon Model.
#'
#' This function constructs and returns another function that computes the
#' conditional density of the graphon model at specified values.
#' The returned function takes inputs \code{u} (probabilities) and \code{x}
#' (a scalar covariate value) and evaluates the graphon-based conditional
#' density using the supplied coefficient vector \code{a}, the density
#' \code{fv}, the distribution function \code{Fv}, and the empirical sample
#' \code{X_matrix}.
#'
#' @param a Numeric vector. Coefficient vector used in the graphon model; must
#' have length equal to the number of columns in \code{X_matrix}.
#' @param fv Function. Density function of the latent variable \eqn{v}
#' (e.g., \code{dnorm}, \code{dexp}).
#' @param Fv Function. Distribution function (CDF) corresponding to \code{fv}
#' (e.g., \code{pnorm}, \code{pexp}).
#' @param X_matrix Numeric matrix. An \eqn{n \times p} matrix of covariates
#' \eqn{X_i}, where each row corresponds to an observation.
#'
#' @return A function of two arguments \code{u} and \code{x}.
#' The returned function is vectorized in \code{u} and returns the conditional
#' density:
#' \deqn{ f_{U \mid X}(u \mid x) = \frac{f_v(q(u) - a^\top x)}{
#' \mathbb{E}[f_v(q(u) - a^\top X_i)] },
#' }
#' where \eqn{q(u)} is computed via \code{qgraphon()}.
#'
#' @details
#' Internally, the function constructs a scalar evaluator
#' \code{scalar_conditional_density()} that computes the conditional density for a
#' single value of \code{u}.
#' A wrapper function \code{conditional_density()} then applies this scalar
#' function to each element of the input vector \code{u}.
#'
#' Note: the function relies on the existence of \code{qgraphon()},
#' \code{pgraphon()}, and \code{dgraphon()} in the user's environment.
#'
#' @examples
#' \dontrun{
#' a <- c(1, -0.5)
#' fv <- dnorm
#' Fv <- pnorm
#' X_matrix <- matrix(rnorm(200), ncol = 2)
#'
#' cond_dens <- create_cond_density(a, fv, Fv, X_matrix)
#' cond_dens(c(0.2, 0.5, 0.8), x = 1.0)
#' }
#'
#' @export
create_cond_density <- function(
a,
fv,
Fv,
X_matrix){
scalar_conditional_density <-function(u, x) {
q_a <- qgraphon(u, a, Fv, X_matrix)
num <- fv(q_a - x %*% a)
den <- dgraphon(q_a, a, fv, X_matrix)
num / den
}
conditional_density <- function(u, x) {
vapply(u, scalar_conditional_density, numeric(1), x = x)
}
return(conditional_density)
}