384 lines
15 KiB
R
384 lines
15 KiB
R
# 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 root‑finding 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 1‑D 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)
|
||
} |