Flexible synthetic data for nested, longitudinal and linked multi-table data.
flexsynth generates utility-oriented synthetic data for
supported flat, nested, longitudinal, and tree-linked structures,
working natively in long format — no pivoting nested or
longitudinal data to wide. It has first-class support for
tree-linked multi-table data (e.g. patients →
admissions → procedures / labs / meds), with referential integrity
preserved by construction.
It ships two engines behind one interface:
synth(..., privacy = dp_control(...)) implements a
person-level (ε, δ) mechanism and emits its
budget-accounting record. A governed release still requires independent
review of the privacy unit, public-domain assumptions, contribution
caps, and selected mechanism.Synthetic data is not anonymisation, and Track A output must never be described as differentially private.
structure formula
(~ id / visit / test).check_linkage() verifies key uniqueness and the absence of
orphans.sample,
cart, forest, ctree,
norm, normrank built in; add your own with
register_method().rule()
enforces row-wise or per-unit constraints
(e.g. dbp <= sbp, monotone length-of-stay) by unit-grain
rejection sampling.pool_synth() /
synth_glm() implement the published fully-synthetic
variance rules (synthpop / Reiter) across m synthetic
datasets. They use large-sample normal intervals; calibration still
depends on the estimand, synthesis model, sample size, and analysis
assumptions. compare_estimates() scores real-vs-synthetic
analyses by confidence-interval overlap.diagnose() (marginal fit,
correlation-matrix difference, categorical association via Cramer’s V,
propensity pMSE) and disclosure_risk() (replicated uniques,
distance-to-closest-record, membership-inference AUC, and TCAP
attribute-disclosure via target =).data.table
fast-path and parallel replicates
(synth_control(parallel = TRUE)) with reproducible L’Ecuyer
streams.# install.packages("remotes")
remotes::install_github("lauyeehow1986-hub/Flexsynth")The package installs with base-R dependencies. The default
method = "cart" requires the suggested rpart
package; ranger / partykit unlock additional
tree methods, and data.table unlocks the row-binding
fast-path. Use method = "sample" when only base R is
available.
library(flexsynth)
df <- data.frame(
id = rep(1:20, each = 2),
visit = rep(1:2, times = 20),
age = rep(round(rnorm(20, 60, 8)), each = 2),
sbp = round(rnorm(40, 130, 15))
)
res <- synth(df, structure = ~ id / visit, method = "cart", seed = 1)
head(as.data.frame(res))patients <- data.frame(id = 1:50, sex = sample(c("F", "M"), 50, TRUE))
adm <- do.call(rbind, lapply(patients$id, function(pid) {
n <- 1 + rpois(1, 0.6)
data.frame(id = pid, admission_id = seq_len(n), los = 1L + rpois(n, 3))
}))
res <- synth_linked(
tables = list(patients = patients, admissions = adm),
structures = list(patients = ~ id,
admissions = ~ id / admission_id),
keys = list(patients = "id",
admissions = c("id", "admission_id")),
seed = 1
)
syn <- as.list(res)
check_linkage(syn, keys = list(patients = "id",
admissions = c("id", "admission_id")))syn <- as.data.frame(synth(df, structure = ~ id / visit, seed = 1))
analysis_vars <- c("age", "sbp") # exclude generated structure keys
d <- diagnose(real = df, syn = syn, vars = analysis_vars)
plot(d) # overlaid marginals
disclosure_risk(real = df, syn = syn, quasi = analysis_vars)# Analyse all m synthetic sets with a published fully-synthetic pooling rule.
# A single set analysed naively generally under-states synthesis uncertainty.
res <- synth(df, structure = ~ id / visit, m = 10, seed = 1)
synth_glm(res, sbp ~ age) # pooled linear model
# any estimator works via pool_synth(res, function(d) <fit returning coef/vcov>)dp <- dp_control(epsilon = 1, delta = 1e-6, mechanism = "gaussian",
bounds = list(visit = c(1, 2), age = c(18, 100), sbp = c(60, 240)))
dp_res <- synth(df, structure = ~ id, privacy = dp, seed = 1)
dp_res$privacy # the (ε, δ) accounting recordTrack B supports three release shapes:
~ id);~ id / visit) using a bounded DP
Markov model; andsynth_linked(), using the
root entity as the privacy unit.dp_control() also exposes privately learned domains,
cross-table conditioning, higher-order transitions, adaptive marginal
selection, Private-PGM reconciliation, and AIM-style models. These
controls trade statistical fidelity, cell sparsity, runtime, and privacy
budget; they are not universally beneficial.
A governed DP release requires more than setting
epsilon: define the privacy unit and public domain
assumptions, justify contribution caps, retain the accounting record,
review utility at the intended analysis grain, and obtain independent
privacy review. See vignette("differential-privacy") for
the supported combinations, accounting model, and limitations.
vignette("getting-started") — getting startedvignette("valid-inference") — pooled inference and
attribute-disclosure (TCAP)vignette("nested-longitudinal") — repeated-measures
datavignette("linked-cardiac") — multi-table linked
synthesisvignette("differential-privacy") — Track Bdocs/roadmap.md
— phased delivery and what’s nextBundled example datasets are fully synthetic cardiac
data (see data-raw/make_toy_cardiac.R). No real patient
data ships with this package, and none should ever be committed.
MIT © 2026 flexsynth authors