Nothing
# Regression tests for issues found in the external review of 0.2.0.
test_that("r_copula() rejects matrices that are not correlation matrices", {
expect_error(r_copula(10, "gaussian", corr = diag(c(4, 1))), "unit diagonal")
expect_error(r_copula(10, "gaussian", corr = matrix(c(1, 1.2, 1.2, 1), 2)), "\\[-1, 1\\]|semi-definite")
expect_error(r_copula(10, "gaussian", corr = matrix(c(1, 0.5, 0.2, 1), 2)), "symmetric")
expect_error(r_copula(10, "gaussian", corr = matrix(c(1, NA, NA, 1), 2)), "finite")
expect_error(r_copula(10, "gaussian", corr = matrix(1:6, 2)), "square")
bad_psd <- matrix(c(1, 0.9, -0.9, 0.9, 1, 0.9, -0.9, 0.9, 1), 3)
expect_error(r_copula(10, "gaussian", corr = bad_psd), "semi-definite")
# a valid matrix gives uniform margins
u <- r_copula(20000, "gaussian", corr = make_corr(2, "exchangeable", 0.6), seed = 1)
expect_lt(ks.test(u[, 1], "punif")$statistic, 0.015)
# r_mvdist() goes through the same validation
expect_error(r_mvdist(5, list("norm", "norm"), corr = diag(c(2, 1))), "unit diagonal")
})
test_that("the t copula validates its degrees of freedom", {
for (bad in list(0, -1, NA_real_, c(3, 4), "4")) {
expect_error(r_copula(5, "t", dim = 2, df = bad), "df", info = format(bad))
}
expect_error(r_mvt(5, df = NA, sigma = diag(2)), "df")
# df = Inf is the Gaussian copula
a <- r_copula(200, "t", dim = 2, df = Inf, seed = 3)
b <- r_copula(200, "gaussian", dim = 2, seed = 3)
expect_equal(unname(a), unname(b))
expect_error(r_copula(5, "clayton", dim = 2, theta = Inf), "finite")
})
test_that("row keys distinguish NA from the string '<NA>' and never collide", {
k <- SynthesizerPlus:::.stratum_key
expect_length(unique(k(data.frame(x = c(NA, "<NA>")))), 2L)
sep_clash <- data.frame(a = c("x\ry", "x"), b = c("z", "y\rz"))
expect_length(unique(k(sep_clash)), 2L)
dot_clash <- data.frame(a = c("1.2", "1"), b = c("3", "2.3"))
expect_length(unique(k(dot_clash)), 2L)
# keys are consistent across data frames, factors and characters alike
kk <- SynthesizerPlus:::.row_keys(data.frame(f = factor(c("a", "b"))),
data.frame(f = c("b", "c")))
expect_equal(kk[[1]][2], kk[[2]][1])
expect_false(kk[[2]][2] %in% kk[[1]])
# numeric values are matched exactly, not via printed digits
num <- data.frame(x = c(0.1 + 0.2, 0.3))
expect_length(unique(k(num)), 2L)
})
test_that("stratification and disclosure risk treat NA and '<NA>' separately", {
d <- data.frame(g = rep(c(NA, "<NA>"), each = 25), x = c(rnorm(25), rnorm(25, 10)))
fit <- fit_synthesizer(d, by = "g")
expect_length(fit$strata$keys, 2L)
syn <- generate(fit, n = 2000, seed = 1)
expect_gt(mean(syn$x[syn$g %in% "<NA>"]), 8)
expect_lt(abs(mean(syn$x[is.na(syn$g)])), 1)
r <- disclosure_risk(data.frame(k = c(NA, "<NA>", "a", "a"), y = 1:4),
data.frame(k = c(NA, "<NA>", "a", "a"), y = 1:4), keys = "k")
expect_equal(r$n_real_uniques, 2L)
expect_equal(r$replicated_uniques, 1)
# stratum labels for display are built from the values
st <- summary(fit_synthesizer(airquality, by = "Month"))$strata
expect_equal(st$stratum, as.character(5:9))
})
test_that("Gower distance: missingness as a category or standard Gower", {
real <- data.frame(a = c(1, NA, 10), b = c(NA, 5, 10), c = c(2, 3, 10),
d = c(NA, NA, 10), e = c(NA, NA, 10))
syn <- data.frame(a = NA_real_, b = NA_real_, c = 2.9, d = NA_real_, e = NA_real_)
g <- function(na) {
SynthesizerPlus:::.min_gower(SynthesizerPlus:::.gower_prep(syn, real),
SynthesizerPlus:::.gower_prep(real, real), na = na)
}
# category: vs row 2 -> a match, b mismatch, c 0.1/8, d e match -> 1.0125 / 5
expect_equal(g("category"), (0 + 1 + 0.1 / 8 + 0 + 0) / 5, ignore_attr = TRUE)
expect_equal(attr(g("category"), "compared"), 1)
# standard Gower: only c is observed in both -> 0.1 / 8, on 1 of 5 variables
expect_equal(g("exclude"), 0.1 / 8, ignore_attr = TRUE)
expect_equal(attr(g("exclude"), "compared"), 1 / 5)
# no variable observed in both -> no distance
none <- SynthesizerPlus:::.min_gower(
SynthesizerPlus:::.gower_prep(data.frame(a = NA_real_, b = 1), data.frame(a = 1, b = NA_real_)),
SynthesizerPlus:::.gower_prep(data.frame(a = 1, b = NA_real_), data.frame(a = 1, b = NA_real_)),
na = "exclude")
expect_true(is.na(none))
aq <- synthesize(airquality, seed = 1)
expect_true(is.finite(dcr(airquality, aq, seed = 1, na = "exclude")$ratio))
expect_error(dcr(data.frame(a = c(NA, 1), b = c(1, NA)), data.frame(a = c(NA, NA), b = c(1, 1)),
na = "exclude"), NA)
expect_error(dcr(airquality, aq, na = "drop"), "should be one of")
})
test_that("r_mvmixture() validates weights and component dimensions", {
expect_error(r_mvmixture(3, c(0, 0), list(0, 1), list(matrix(1), matrix(1))), "not all zero")
expect_error(r_mvmixture(3, c(1, NA), list(0, 1), list(matrix(1), matrix(1))), "finite")
expect_error(r_mvmixture(3, c(1, -1), list(0, 1), list(matrix(1), matrix(1))), "non-negative")
expect_error(r_mvmixture(10, c(1, 1), list(c(0, 0), c(0, 0, 0)), list(diag(2), diag(3))),
"Component 2")
expect_error(r_mvmixture(10, c(1, 1), list(c(0, 0), c(0, 0)), list(diag(2), diag(3))),
"Component 2")
expect_error(r_mvmixture(10, 1, c(0, 0), list(diag(2))), "lists")
ok <- r_mvmixture(10, c(0, 1), list(c(0, 0), c(5, 5)), list(diag(2), diag(2)), seed = 1)
expect_true(all(ok > 0)) # zero weight on the first component is allowed
})
test_that("margin helpers validate levels and probabilities", {
expect_error(margin_categorical(c("a", "a")), "unique")
expect_error(margin_categorical(c("a", NA)), "unique|non-missing")
expect_error(margin_categorical(c("a", "b"), c(1, Inf)), "finite")
q <- margin_categorical(c("a", "b"))
expect_error(q(c(0.5, 1.2)), "\\[0, 1\\]")
expect_error(q(NA_real_), "\\[0, 1\\]")
expect_error(margin_empirical(1:10)(-0.1), "\\[0, 1\\]")
})
test_that("discriminator preprocessing is learned within training folds only", {
# a variable that is constant within the training folds but not overall
# cannot be scaled by statistics of the held-out data
spec <- SynthesizerPlus:::.prep_fit(data.frame(x = c(1, 2, 3, NA), g = c("a", "b", "a", "a")))
X <- SynthesizerPlus:::.prep_apply(spec, data.frame(x = c(100, NA), g = c("zzz", "b")))
expect_equal(colnames(X), c("x..NA", "x", "g==b"))
expect_equal(X[, "x"], c((100 - 2) / sd(c(1, 2, 3, 2)), 0)) # NA imputed by training median
expect_equal(X[, "g==b"], c(0, 1)) # unseen level -> reference
# the AUC is reproducible and in a sensible range
syn <- synthesize(iris, seed = 1)
a1 <- discriminator_auc(iris, syn, seed = 2)
expect_identical(a1, discriminator_auc(iris, syn, seed = 2))
expect_true(a1 > 0.4 && a1 < 0.85)
x <- as.data.frame(r_mvnorm(400, sigma = diag(3), seed = 1))
y <- as.data.frame(r_mvnorm(400, sigma = diag(3), seed = 2))
expect_lt(abs(discriminator_auc(x, y, seed = 1) - 0.5), 0.06)
# copies of the real records push the cross-validated AUC below 0.5
expect_lt(discriminator_auc(iris[sample(150), ], iris, seed = 2), 0.4)
})
test_that("Cramer's V cannot produce NaN from empty categories", {
# tables are built from observed values only, so expected counts are never 0
v <- SynthesizerPlus:::.cramers_v(factor(c("a", "b"), levels = c("a", "b", "c")),
factor(c("x", "y"), levels = c("x", "y", "z")))
expect_false(is.nan(v))
A <- association_matrix(data.frame(f = factor(c("a", "a", "b"), levels = letters[1:5]),
g = c("x", "y", "y")))
expect_false(any(is.nan(A)))
})
test_that("date-time strings are read in the right time zone", {
tmpl <- data.frame(t = as.POSIXct("2020-01-01", tz = "Europe/Bucharest"))
x <- data.frame(t = c("2020-06-01 12:00:00", "2020-06-01T09:00:00Z",
"2020-06-01T12:00:00+03:00", "2020-06-01T05:00:00-0400",
"2020/06/01 12:00", NA), stringsAsFactors = FALSE)
y <- match_types(x, tmpl)$t
expect_equal(attr(y, "tzone"), "Europe/Bucharest")
expect_equal(format(y[1:5], "%H:%M", tz = "Europe/Bucharest"), rep("12:00", 5))
expect_true(is.na(y[6]))
# the package's own CSV output (UTC with Z) round-trips in any time zone
d <- data.frame(t = as.POSIXct(c("2021-03-28 02:30:00", "2021-10-31 03:30:00"),
tz = "Europe/Bucharest"))
f <- tempfile(fileext = ".csv")
on.exit(unlink(f))
write_data(d, f)
expect_equal(read_data(f, template = d)$t, d$t)
})
# ---- issues from the second review (0.2.1) -------------------------------------
test_that("r_dirichlet() rejects non-finite or non-positive alpha", {
for (a in list(c(1, NA), c(1, NaN), c(1, Inf), c(1, 0), c(1, -1), 1, "a")) {
expect_error(r_dirichlet(10, a), "finite positive", info = format(a))
}
})
test_that("disclosure_risk() requires a finite tolerance", {
x <- data.frame(k = 1:3, y = 1:3)
for (tol in list(NA_real_, Inf, -1, c(0.1, 0.2), "0.1")) {
expect_error(disclosure_risk(x, x, keys = "k", target = "y", tolerance = tol),
"tolerance", info = format(tol))
}
expect_s3_class(disclosure_risk(x, x, keys = "k", target = "y", tolerance = 0), "sp_disclosure")
})
test_that("seeds must be finite and in the integer range", {
for (s in list(Inf, -Inf, NaN, 1e12, c(1, 2), "1")) {
expect_error(r_copula(10, seed = s), "seed", info = format(s))
}
expect_error(generate(fit_synthesizer(iris), seed = Inf), "seed")
expect_silent(r_copula(2, seed = -.Machine$integer.max))
})
test_that("make_corr() validates rho", {
for (type in c("exchangeable", "ar1")) {
for (r in list(NA_real_, NaN, Inf, "a", numeric(0))) {
expect_error(make_corr(3, type, r), "rho", info = paste(type, format(r)))
}
}
expect_error(make_corr(3, "toeplitz", c(NA, 0)), "rho")
expect_equal(dim(make_corr(3, "random", rho = NA)), c(3L, 3L)) # rho unused for random
})
test_that("all mixture components are validated before sampling", {
bad_sigma <- matrix(c(1, 2, 2, 1), 2)
expect_error(r_mvmixture(10, c(1, 0), list(c(0, 0), c(0, 0)), list(diag(2), bad_sigma)),
"Component 2.*semi-definite")
expect_error(r_mvmixture(10, c(1, 0), list(c(0, 0), c("a", "b")), list(diag(2), diag(2))),
"Component 2.*finite")
expect_error(r_mvmixture(10, c(1, 1), list(c(0, 0), c(Inf, 0)), list(diag(2), diag(2))),
"Component 2.*finite")
expect_error(r_mvmixture(10, c(1, 1), list(c(0, 0), c(0, 0)),
list(diag(2), matrix(c(1, NA, NA, 1), 2))), "Component 2.*finite")
expect_error(r_mvmixture(10, c(1, 1), list(c(0, 0), c(0, 0)),
list(diag(2), matrix(c(1, 0.5, 0.2, 1), 2))), "Component 2.*symmetric")
})
test_that("row keys handle special values consistently", {
k <- SynthesizerPlus:::.stratum_key
num <- k(data.frame(v = c(NA, NaN, Inf, -Inf, -0, 0, 1)))
expect_equal(num[1], num[2]) # NA and NaN are both missing
expect_false(num[3] == num[4]) # Inf and -Inf differ
expect_equal(num[5], num[6]) # -0 equals 0
expect_false(num[1] == num[6])
chr <- k(data.frame(v = c(NA_character_, "NA", "<NA>", "")))
expect_length(unique(chr), 4L)
int <- k(data.frame(v = c(NA_integer_, 0L, NA_integer_)))
expect_equal(int[1], int[3])
lgl <- k(data.frame(v = c(NA, TRUE, FALSE, NA)))
expect_equal(lgl[1], lgl[4])
expect_length(unique(lgl), 3L)
dt <- k(data.frame(v = as.Date(c(NA, "2020-01-01", "2020-01-01"))))
expect_equal(dt[2], dt[3])
# consistency across data frames (real vs synthetic) for the same values
kk <- SynthesizerPlus:::.row_keys(data.frame(v = c(NaN, 1)), data.frame(v = c(NA, 1)))
expect_equal(kk[[1]], kk[[2]])
})
test_that("exact matches under na = 'exclude' require all variables", {
real <- data.frame(a = c(1, 2), b = c(5, 6), c = c(7, 8))
partial <- data.frame(a = c(1, NA), b = c(NA, NA), c = c(NA, 8)) # 1 of 3 variables each
d <- dcr(real, partial, na = "exclude")
expect_equal(d$zero_distance_rate, 1)
expect_equal(d$exact_match_rate, 0)
expect_equal(d$compared_share, 1 / 3)
full <- dcr(real, real[2:1, ], na = "exclude")
expect_equal(full$exact_match_rate, 1)
expect_equal(full$compared_share, 1)
cat_d <- dcr(real, partial, na = "category")
expect_equal(cat_d$compared_share, 1)
expect_equal(cat_d$exact_match_rate, cat_d$zero_distance_rate)
})
test_that("comparison output flags an AUC well below 0.5", {
cmp <- compare_synthetic(iris, iris[sample(150), ], metrics = "utility", seed = 2)
expect_lt(cmp$utility$auc, 0.4)
expect_output(print(cmp), "copy real")
ok <- compare_synthetic(iris, synthesize(iris, seed = 1), metrics = "utility", seed = 2)
expect_false(any(grepl("copy real", capture.output(print(ok)))))
})
# ---- 0.2.3: third external review -----------------------------------------
test_that("covariance / scale matrices are validated with clear messages", {
for (f in list(function(S) r_mvnorm(10, sigma = S),
function(S) r_mvt(10, df = 5, sigma = S),
function(S) r_mvlnorm(10, sigmalog = S),
function(S) r_mvskewnorm(10, omega = S, alpha = c(1, 1)))) {
expect_error(f(matrix(1:6, 2, 3)), "square")
expect_error(f(matrix(numeric(0), 0, 0)), "square")
expect_error(f(matrix("a", 2, 2)), "numeric")
expect_error(f(matrix(c(1, NA, NA, 1), 2)), "finite")
expect_error(f(matrix(c(1, Inf, Inf, 1), 2)), "finite")
expect_error(f(matrix(c(1, 0.5, 0.2, 1), 2)), "symmetric")
expect_error(f(matrix(c(1, 2, 2, 1), 2)), "semi-definite")
}
expect_error(r_mvmixture(10, 1, list(c(0, 0)), list(matrix(c(1, 0.5, 0.2, 1), 2))),
"Component 1: 'sigma' must be symmetric")
# singular but PSD covariances remain allowed for the normal family
x <- r_mvnorm(50, sigma = matrix(1, 2, 2), seed = 1)
expect_equal(x[, 1], x[, 2])
})
test_that("multivariate generators reject non-finite locations and shapes", {
expect_error(r_mvnorm(10, mean = c(Inf, 0), sigma = diag(2)), "finite")
expect_error(r_mvnorm(10, mean = c("a", "b"), sigma = diag(2)), "numeric")
expect_error(r_mvnorm(10, mean = 1:3, sigma = diag(2)), "incompatible dimensions")
expect_error(r_mvt(10, df = 5, mean = c(NA, 0), sigma = diag(2)), "finite")
expect_error(r_mvlnorm(10, meanlog = c(NaN, 0), sigmalog = diag(2)), "finite")
expect_error(r_mvskewnorm(10, xi = c(0, 0), omega = diag(2), alpha = c(Inf, 1)), "'alpha'.*finite")
expect_error(r_mvskewnorm(10, xi = c(NA, 0), omega = diag(2), alpha = c(1, 1)), "'xi'.*finite")
expect_error(r_mvskewnorm(10, omega = diag(2), alpha = 1), "'alpha'")
expect_error(r_mvmixture(10, 1, list(c(0, Inf)), list(diag(2))), "Component 1.*finite")
expect_error(r_mvt(-1, df = 5, sigma = diag(2)), "non-negative")
})
test_that("skew-normal requires a strictly positive scale diagonal", {
S <- matrix(c(0, 0, 0, 1), 2, 2) # PSD, but zero scale
expect_error(r_mvskewnorm(10, omega = S, alpha = c(1, 1)), "positive")
})
test_that("make_corr(d = 1) is the 1 x 1 identity whatever rho", {
expect_identical(make_corr(1, "exchangeable", rho = -5), matrix(1, 1, 1))
expect_identical(make_corr(1, "ar1", rho = 0.5, names = "a"),
matrix(1, 1, 1, dimnames = list("a", "a")))
expect_identical(make_corr(1, "random"), matrix(1, 1, 1))
})
test_that("t-copula df fallback is flagged and warned about", {
set.seed(1)
small <- data.frame(x = rnorm(8), y = rnorm(8))
expect_warning(s <- fit_synthesizer(small, copula = "t"), "df was not estimated")
expect_identical(s$pooled$copula$df, 10)
expect_false(s$pooled$copula$df_estimated)
expect_output(print(summary(s)), "fixed: too few complete rows")
big <- data.frame(x = rnorm(200), y = rnorm(200))
expect_silent(s2 <- fit_synthesizer(big, copula = "t"))
expect_true(s2$pooled$copula$df_estimated)
expect_true(s2$pooled$copula$df %in% c(2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 50, 100))
expect_true(is.na(fit_synthesizer(big)$pooled$copula$df_estimated))
})
# ---- 0.2.4: fourth external review ----------------------------------------
test_that(".check_prob() rejects every non-finite value", {
for (v in list(NaN, NA_real_, Inf, -Inf, NA)) {
expect_error(SynthesizerPlus:::.check_prob(v, "p"), "single number")
}
expect_identical(SynthesizerPlus:::.check_prob(0.3, "p"), 0.3)
})
split_modules <- function(n = 300, rho = 0.8, seed = 1) {
z <- r_mvnorm(n, sigma = make_corr(3, rho = rho), seed = seed)
d <- data.frame(a = z[, 1], b = z[, 2], c = z[, 3])
h <- n %/% 2
d$a[seq_len(h)] <- NA # a and b never observed together
d$b[(h + 1):n] <- NA
d
}
test_that("unidentified pairwise correlations are completed, counted and reported", {
d <- split_modules()
for (fam in c("gaussian", "t")) {
w <- testthat::capture_warnings(fit <- fit_synthesizer(d, copula = fam, missing = "drop"))
expect_true(any(grepl("not identified", w)))
# no complete rows at all, so the t df is the documented fallback
if (fam == "t") expect_true(any(grepl("df was not estimated", w)))
np <- copula_correlation(fit, what = "n_pair")
expect_identical(np["a", "b"], 0L)
expect_identical(np["a", "c"], 150L)
expect_identical(unname(diag(np)), c(150L, 150L, 300L))
expect_identical(fit$pooled$copula$unidentified, 1L)
R <- copula_correlation(fit)
# max-determinant completion: conditional independence of a and b given c,
# i.e. r_ab = r_ac * r_bc, instead of the arbitrary value 0
expect_equal(R["a", "b"], R["a", "c"] * R["b", "c"], tolerance = 1e-6)
expect_gt(R["a", "b"], 0.4)
expect_gt(min(eigen(R, only.values = TRUE)$values), 0)
}
expect_output(print(summary(suppressWarnings(fit_synthesizer(d, missing = "drop")))),
"Unidentified pairwise correlations")
# fully observed data: no warning, nothing unidentified
expect_silent(f2 <- fit_synthesizer(iris))
expect_identical(f2$pooled$copula$unidentified, 0L)
expect_true(all(copula_correlation(f2, what = "n_pair") == 150L))
})
test_that("a pair seen together in only 2 rows does not distort identified pairs", {
d <- split_modules()
ref <- suppressWarnings(fit_synthesizer(d, missing = "drop"))
z <- r_mvnorm(300, sigma = make_corr(3, rho = 0.8), seed = 1)
d$b[151:152] <- z[151:152, 2] # 2 joint rows: |r| = 1 trivially
expect_warning(fit <- fit_synthesizer(d, missing = "drop"), "fewer than 3 rows")
R0 <- copula_correlation(ref)
R1 <- copula_correlation(fit)
expect_equal(R1["a", "c"], R0["a", "c"], tolerance = 0.02)
expect_equal(R1["a", "b"], R1["a", "c"] * R1["b", "c"], tolerance = 1e-6)
})
test_that("completion keeps identified entries and falls back to zero if impossible", {
cc <- function(R, unid) SynthesizerPlus:::.complete_corr(R, unid)$R
R <- matrix(c(1, NA, 0.5, NA, 1, 0.4, 0.5, 0.4, 1), 3)
unid <- is.na(R)
W <- cc(R, unid)
expect_equal(W[1, 3], 0.5)
expect_equal(W[2, 3], 0.4)
expect_equal(W[1, 2], 0.2, tolerance = 1e-8)
expect_equal(solve(W)[1, 2], 0, tolerance = 1e-8) # zero partial correlation
# 4 x 4 cycle a-b-c-d-a with a-c, b-d unknown
R4 <- diag(4)
R4[cbind(c(1, 2, 3, 4), c(2, 3, 4, 1))] <- 0.5
R4 <- pmax(R4, t(R4))
u4 <- matrix(FALSE, 4, 4)
u4[cbind(c(1, 3, 2, 4), c(3, 1, 4, 2))] <- TRUE
R4[u4] <- NA
W4 <- cc(R4, u4)
expect_equal(W4[!u4], R4[!u4], tolerance = 1e-8)
expect_equal(solve(W4)[u4], rep(0, 4), tolerance = 1e-7)
# identified entries that admit no PD completion -> unknown entries set to 0
Rb <- matrix(c(1, 0.99, NA, 0.99, 1, -0.99, NA, -0.99, 1), 3)
Rb[1, 3] <- Rb[3, 1] <- NA
ub <- is.na(Rb)
expect_equal(cc(Rb, ub)[1, 3], -0.9801, tolerance = 1e-6) # r_13 = r_12 * r_23
Rx <- diag(4)
Rx[1, 2] <- Rx[2, 1] <- 0.99; Rx[2, 3] <- Rx[3, 2] <- 0.99
Rx[1, 3] <- Rx[3, 1] <- -0.99 # inconsistent triangle
Rx[1, 4] <- Rx[4, 1] <- NA
ux <- is.na(Rx)
expect_identical(cc(Rx, ux)[1, 4], 0)
# a variable with no identified partner is independent of all others
Ri <- matrix(c(1, NA, NA, NA, 1, 0.6, NA, 0.6, 1), 3)
Wi <- cc(Ri, is.na(Ri))
expect_equal(Wi[1, ], c(1, 0, 0))
expect_equal(Wi[2, 3], 0.6)
})
test_that("r_mvskewnorm() requires a positive-definite omega", {
expect_error(r_mvskewnorm(5, omega = matrix(1, 2, 2), alpha = c(1, 2)), "positive definite")
expect_error(r_mvskewnorm(5, omega = matrix(c(1, 1, 0, 1, 1, 0, 0, 0, 1), 3),
alpha = c(1, 0, 0)), "positive definite")
# PSD-but-singular sigma stays allowed for the normal and t families
expect_silent(r_mvt(5, df = 4, sigma = matrix(1, 2, 2), seed = 1))
})
# ---- 0.2.5: fifth external review -----------------------------------------
test_that("t-copula n_pair uses the full data, not the Kendall subsample", {
set.seed(123)
n <- 5000L
d <- data.frame(x = rnorm(n), y = rnorm(n), z = rnorm(n))
fit <- fit_synthesizer(d, copula = "t")
np <- copula_correlation(fit, what = "n_pair")
expect_identical(unname(np), matrix(n, 3, 3))
expect_identical(fit$pooled$copula$completion, "none")
})
test_that("t-copula identification and estimation do not depend on the subsample", {
n <- 10000L
z <- r_mvnorm(n, sigma = make_corr(3, rho = 0.7), seed = 1)
d <- data.frame(x = z[, 1], y = z[, 2], z = z[, 3])
d$x[-(1:3000)] <- NA
d$y[11:7000] <- NA # x and y jointly observed in 10 rows only
res <- vapply(1:6, function(s) {
set.seed(s)
f <- suppressWarnings(fit_synthesizer(d, copula = "t", missing = "drop"))
c(np = copula_correlation(f, what = "n_pair")["x", "y"],
unid = f$pooled$copula$unidentified,
rxy = copula_correlation(f)["x", "y"])
}, numeric(3))
expect_true(all(res["np", ] == 10))
expect_true(all(res["unid", ] == 0))
# all 10 joint rows are used, so the estimate is the same for every seed
expect_lt(diff(range(res["rxy", ])), 1e-6)
# same pattern below the subsampling size
small <- d[c(1:10, sample(11:n, 1500)), ]
f <- suppressWarnings(fit_synthesizer(small, copula = "t", missing = "drop"))
expect_identical(copula_correlation(f, what = "n_pair")["x", "y"], 10L)
})
test_that(".kendall_matrix() equals stats::cor(method = 'kendall') with missing values", {
set.seed(4)
U <- matrix(runif(600 * 5), 600, 5)
U[, 2] <- U[, 1] + 0.3 * runif(600)
U[runif(length(U)) < 0.3] <- NA
U[1:595, 5] <- NA # very sparse column
a <- SynthesizerPlus:::.kendall_matrix(U)
b <- suppressWarnings(cor(U, method = "kendall", use = "pairwise.complete.obs"))
expect_identical(unname(is.na(a)), unname(is.na(b)))
expect_equal(unname(a), unname(b), tolerance = 1e-12)
# tiny blocks give the same answer
expect_equal(SynthesizerPlus:::.kendall_matrix(U, block_cells = 1), a, tolerance = 1e-12)
# fewer than 2 joint rows: undefined
V <- cbind(c(0.1, NA, NA), c(NA, 0.2, 0.3), c(0.5, 0.6, 0.7))
expect_true(is.na(SynthesizerPlus:::.kendall_matrix(V)[1, 2]))
})
test_that("completion method and repair size are recorded and reported", {
cc <- SynthesizerPlus:::.complete_corr
R <- matrix(c(1, NA, 0.5, NA, 1, 0.4, 0.5, 0.4, 1), 3)
expect_identical(cc(R, is.na(R))$method, "maxdet")
Rx <- diag(4)
Rx[1, 2] <- Rx[2, 1] <- 0.99; Rx[2, 3] <- Rx[3, 2] <- 0.99
Rx[1, 3] <- Rx[3, 1] <- -0.99
Rx[1, 4] <- Rx[4, 1] <- NA
expect_identical(cc(Rx, is.na(Rx))$method, "zero_fallback")
d <- split_modules()
fit <- suppressWarnings(fit_synthesizer(d, missing = "drop"))
expect_identical(fit$pooled$copula$completion, "maxdet")
expect_gte(fit$pooled$copula$pd_adjustment, 0)
expect_identical(fit_synthesizer(iris)$pooled$copula$completion, "none")
expect_output(print(summary(fit)), "maximum-determinant completion")
fit$pooled$copula$completion <- "zero_fallback"
fit$pooled$copula$pd_adjustment <- 0.05
out <- capture.output(print(summary(fit)))
expect_true(any(grepl("no consistent completion; set to 0", out)))
expect_true(any(grepl("repair changed correlations by up to 0.05", out)))
# the warning mentions the later repair step
expect_warning(fit_synthesizer(d, missing = "drop"), "before any positive-definiteness repair")
})
test_that("under-represented pairs are re-estimated from at most tau_rows joint rows", {
set.seed(5)
n <- 3000L
z <- r_mvnorm(n, sigma = make_corr(3, rho = 0.6), seed = 5)
U <- apply(z, 2, function(v) rank(v) / (n + 1))
U[sample(n, 2700), 1] <- NA # column 1 sparse: 300 joint rows
obs <- !is.na(U)
np <- crossprod(obs + 0)
# subsample of 100 rows leaves ~10 joint rows for column 1 (< min(300, 80)),
# so those pairs are redone on 100 of their own 300 joint rows
R <- SynthesizerPlus:::.pairwise_kendall_corr(U, obs, np, tau_rows = 100L, tau_min_rows = 80L)
expect_true(all(is.finite(R)))
expect_equal(R[1, 2], 0.6, tolerance = 0.35)
expect_equal(R, t(R))
})
test_that("the unidentified-correlation message reports the completion method", {
# no mocking (testthat::local_mocked_bindings needs testthat >= 3.1.7):
# the message is built by an internal function from the fitted models
msg <- SynthesizerPlus:::.unidentified_message
mk <- function(k, method) list(copula = list(unidentified = k, completion = method))
expect_null(msg(list(mk(0L, "none"), mk(0L, "none"))))
expect_match(msg(list(mk(1L, "maxdet"))), "filled by maximum-determinant completion,")
m0 <- msg(list(mk(2L, "zero_fallback")))
expect_match(m0, "could not be completed consistently and were set to 0")
expect_match(m0, "^Up to 2 pair\\(s\\)")
mx <- msg(list(mk(1L, "maxdet"), mk(3L, "zero_fallback"), mk(0L, "none")))
expect_match(mx, "set to 0 in 1 model")
expect_match(mx, "Up to 3 pair\\(s\\) .*\\(affecting 2 of 3 fitted model")
# and the fit itself emits it
expect_warning(fit_synthesizer(split_modules(), missing = "drop"), "maximum-determinant completion")
})
# ---- 0.2.6: sixth external review -----------------------------------------
test_that("the unidentified-pairs warning reports a per-model maximum", {
d <- split_modules(n = 400)
d$g <- rep(c("a", "b"), times = 200)
w <- testthat::capture_warnings(fit_synthesizer(d, by = "g", missing = "drop",
min_stratum_size = 10))
w <- w[grepl("not identified", w)]
expect_length(w, 1L)
expect_match(w, "^Up to 1 pair\\(s\\) of variables per fitted model")
expect_match(w, "affecting 3 of 3 fitted model")
})
test_that("fit_synthesizer(seed = ) makes fitting reproducible", {
set.seed(1)
# x has ties (broken at random), so the fit itself is stochastic
d <- data.frame(x = round(rnorm(3000), 1), k = sample(1:4, 3000, TRUE),
f = factor(sample(letters[1:3], 3000, TRUE)))
d$x[sample(3000, 300)] <- NA
for (cop in c("gaussian", "t")) {
f1 <- fit_synthesizer(d, copula = cop, seed = 42)
f2 <- fit_synthesizer(d, copula = cop, seed = 42)
expect_identical(copula_correlation(f1, include_missing = TRUE),
copula_correlation(f2, include_missing = TRUE))
expect_identical(generate(f1, 50, seed = 1), generate(f2, 50, seed = 1))
f3 <- fit_synthesizer(d, copula = cop, seed = 43)
expect_false(identical(copula_correlation(f1), copula_correlation(f3)))
}
# the global RNG state is restored
set.seed(7); a <- runif(1)
set.seed(7); fit_synthesizer(d, seed = 99); b <- runif(1)
expect_identical(a, b)
# forwarded by the matrix, vector and stratified paths
m <- as.matrix(iris[1:4])
expect_identical(copula_correlation(fit_synthesizer(m, seed = 3)),
copula_correlation(fit_synthesizer(m, seed = 3)))
expect_identical(fit_synthesizer(iris$Sepal.Length, seed = 3)$pooled,
fit_synthesizer(iris$Sepal.Length, seed = 3)$pooled)
expect_identical(fit_synthesizer(iris, by = "Species", seed = 5)$strata,
fit_synthesizer(iris, by = "Species", seed = 5)$strata)
expect_error(fit_synthesizer(iris, seed = NA), "seed")
expect_error(fit_synthesizer(iris, seed = "a"), "seed")
})
test_that("fit_synthesizer.ts(seed = ) is reproducible for every method", {
for (m in c("copula_var", "block", "stationary", "iid")) {
f1 <- fit_synthesizer(ldeaths, method = m, seed = 8)
f2 <- fit_synthesizer(ldeaths, method = m, seed = 8)
expect_identical(generate(f1, seed = 2), generate(f2, seed = 2))
}
set.seed(7); a <- runif(1)
set.seed(7); fit_synthesizer(ldeaths, seed = 1); b <- runif(1)
expect_identical(a, b)
})
# ---- 0.2.7: seventh external review + statistical validation --------------
test_that(".pbvnorm() agrees with numerical integration", {
pb <- SynthesizerPlus:::.pbvnorm
ref <- function(a, b, r) {
integrate(function(x) dnorm(x) * pnorm((b - r * x) / sqrt(1 - r^2)), -Inf, a,
rel.tol = 1e-10)$value
}
for (r in c(-0.99, -0.6, 0, 0.3, 0.9, 0.99)) {
for (ab in list(c(0, 0), c(-1.2, 0.7), c(2, -0.5), c(1.5, 1.5))) {
expect_near(pb(ab[1], ab[2], r), ref(ab[1], ab[2], r), 1e-6)
}
}
# infinite limits reduce to univariate probabilities
expect_equal(pb(c(Inf, 0.4, -Inf), c(0.4, Inf, 1), 0.5), c(pnorm(0.4), pnorm(0.4), 0))
expect_equal(sum(SynthesizerPlus:::.gl20$w), 2)
})
test_that("polyserial and polychoric estimators recover the latent correlation", {
z <- r_mvnorm(20000, sigma = make_corr(2, rho = 0.65), seed = 1)
k <- findInterval(z[, 2], c(-Inf, -0.8, 0.2, 1.1, Inf))
tau <- qnorm(c(0, cumsum(tabulate(k, 4)) / 20000))
zz <- qnorm(rank(z[, 1]) / 20001)
expect_near(SynthesizerPlus:::.polyserial(zz, k, tau), 0.65, 0.02)
b <- as.integer(ceiling(rank(zz, ties.method = "first") * 500 / 20000))
expect_near(SynthesizerPlus:::.polyserial(zz, k, tau, bin = b),
SynthesizerPlus:::.polyserial(zz, k, tau), 1e-3)
k1 <- findInterval(z[, 1], c(-Inf, 0, 1, Inf))
N <- table(factor(k1, 1:3), factor(k, 1:4))
t1 <- qnorm(c(0, cumsum(tabulate(k1, 3)) / 20000))
expect_near(SynthesizerPlus:::.polychoric(unclass(N), t1, tau), 0.65, 0.02)
# empty cells and high-cardinality margins are handled
N2 <- unclass(N)
N2[1, 4] <- 0
expect_true(is.finite(SynthesizerPlus:::.polychoric(N2, t1, tau)))
})
test_that("a variable and its own missingness indicator are fixed at 0 and not reported", {
set.seed(2)
x <- rnorm(800)
y <- 0.5 * x + rnorm(800)
y[runif(800) < plogis(-1 + x)] <- NA
expect_silent(fit <- fit_synthesizer(data.frame(x, y)))
R <- copula_correlation(fit, include_missing = TRUE)
expect_identical(R["y", ".missing_y"], 0)
expect_gt(R["x", ".missing_y"], 0.3) # MAR dependence on x
# when r_xy^2 + r_xM^2 >= 1 a zero is infeasible and the repair moves it
y2 <- x + rnorm(800, sd = 0.3)
y2[x > 0.3] <- NA
f2 <- fit_synthesizer(data.frame(x, y2))
expect_gt(f2$pooled$copula$pd_adjustment, 0)
expect_identical(fit$pooled$copula$unidentified, 0L)
expect_identical(fit$pooled$copula$latent, "polyserial_polychoric")
expect_identical(fit_synthesizer(iris[1:4])$pooled$copula$latent, "normal_scores")
})
test_that("a single-category discrete margin has zero correlations without warnings", {
d <- data.frame(x = rnorm(50), k = factor(rep("a", 50)), y = rnorm(50))
expect_silent(fit <- fit_synthesizer(d))
expect_identical(unname(copula_correlation(fit)["k", c("x", "y")]), c(0, 0))
})
test_that("time-series arguments that the method ignores trigger warnings", {
expect_warning(fit_synthesizer(ldeaths, block_length = 12), "'block_length' is ignored")
expect_warning(fit_synthesizer(ldeaths, method = "block", order = 2), "'order' is ignored")
expect_warning(fit_synthesizer(ldeaths, method = "stationary", burn_in = 10), "'burn_in' is ignored")
expect_warning(fit_synthesizer(ldeaths, method = "iid", max_lag = 3), "'max_lag' is ignored")
expect_warning(fit_synthesizer(ldeaths, order = 12, max_lag = 3), "only used when 'order' is NULL")
# no warning when the arguments apply (also through the seed path)
expect_silent(fit_synthesizer(ldeaths, order = 12, burn_in = 50, seed = 1))
expect_silent(fit_synthesizer(ldeaths, method = "block", block_length = 6, seed = 1))
expect_silent(fit_synthesizer(ldeaths, max_lag = 4))
w <- testthat::capture_warnings(fit_synthesizer(ldeaths, method = "block", order = 2, seed = 3))
expect_length(w, 1L) # not repeated by the seed path
})
test_that("disclosure_risk(abs_tolerance = ) handles meaningful zeros", {
real <- data.frame(k = c("a", "a", "b", "b"), y = c(0, 0, 100, 100))
syn <- data.frame(k = c("a", "a", "b", "b"), y = c(0.5, 0.5, 105, 105))
r0 <- disclosure_risk(real, syn, keys = "k", target = "y")
expect_equal(r0$attribute$cap, 0.5) # 0 vs 0.5 is a miss
r1 <- disclosure_risk(real, syn, keys = "k", target = "y", abs_tolerance = 1)
expect_equal(r1$attribute$cap, 1)
expect_error(disclosure_risk(real, syn, keys = "k", target = "y", abs_tolerance = -1),
"abs_tolerance")
expect_error(disclosure_risk(real, syn, keys = "k", target = "y", abs_tolerance = NA),
"abs_tolerance")
})
test_that("missing = 'drop' keeps an entirely missing column missing", {
d <- data.frame(a = rnorm(30), b = NA_real_)
s <- generate(fit_synthesizer(d, missing = "drop"), 20, seed = 1)
expect_true(all(is.na(s$b)))
expect_false(anyNA(s$a))
})
test_that("a discrete margin with one category on the joint rows is unidentified", {
set.seed(3)
n <- 300
k <- factor(sample(c("a", "b"), n, TRUE))
g <- factor(sample(c("u", "v", "w"), n, TRUE))
x <- rnorm(n)
x[k == "b"] <- NA # x is only observed when k == "a"
g2 <- g
g2[k == "b"] <- NA # g2 also only observed when k == "a"
d <- data.frame(k, x, g2)
w <- testthat::capture_warnings(fit <- fit_synthesizer(d, missing = "drop"))
expect_true(any(grepl("not identified", w)))
expect_identical(fit$pooled$copula$unidentified, 2L) # (k, x) and (k, g2)
expect_true(all(is.finite(copula_correlation(fit))))
})
test_that("unused arguments of fit_synthesizer.ts() are reported", {
expect_warning(fit_synthesizer(ldeaths, closeness = 0.5), "Closeness control is not available")
})
test_that("time-series arguments on a non-ts input explain the ts class loss", {
m <- EuStockMarkets[1:200, ] # a matrix, no longer a ts
expect_false(is.ts(m))
expect_warning(fit_synthesizer(m, method = "stationary"), "drops the ts class")
eu <- window(EuStockMarkets, end = time(EuStockMarkets)[200])
expect_true(is.ts(eu))
expect_silent(fit <- fit_synthesizer(eu, method = "stationary", seed = 1))
expect_identical(fit$method, "stationary")
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.