Generates utility-oriented synthetic data for supported flat, nested, longitudinal and tree-linked multi-table designs, including patients, admissions, procedures and laboratory results linked by identifiers. The default engine uses sequential conditional synthesis; an opt-in differentially private engine implements person-level (epsilon, delta) mechanisms and records their budget accounting. Synthetic output is not anonymisation. Empirical utility and disclosure-risk diagnostics are descriptive and do not by themselves establish that a release is safe.
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 record
Track 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