| Type: | Package |
| Title: | Flexible Synthetic Data for Nested, Longitudinal and Linked Multi-Table Data |
| Version: | 0.2.1 |
| Description: | 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. |
| License: | MIT + file LICENSE |
| Encoding: | UTF-8 |
| RoxygenNote: | 7.3.3 |
| Depends: | R (≥ 4.1) |
| Imports: | graphics, grDevices, parallel, stats, utils |
| Suggests: | testthat (≥ 3.0.0), rpart, ranger, partykit, data.table, knitr, litedown |
| Config/testthat/edition: | 3 |
| VignetteBuilder: | litedown |
| URL: | https://github.com/lauyeehow1986-hub/Flexsynth |
| BugReports: | https://github.com/lauyeehow1986-hub/Flexsynth/issues |
| NeedsCompilation: | no |
| Packaged: | 2026-08-19 00:32:29 UTC; lauye |
| Author: | Yee How Lau [aut, cre] |
| Maintainer: | Yee How Lau <lauyeehow1986@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-09-02 12:00:02 UTC |
flexsynth: flexible synthetic data for nested, longitudinal and linked data
Description
Generate utility-oriented synthetic data for supported flat, nested, longitudinal and tree-linked structures, natively in long format.
Two privacy tracks
-
Track A (default): high-utility sequential synthesis. No formal guarantee; ships empirical disclosure-risk diagnostics.
-
Track B (opt-in): differentially private synthesis via
dp_control(), with a person-level (\epsilon,\delta) guarantee. Seevignette("differential-privacy")for scope and accounting.
Synthetic data is not anonymisation. Track A output must never be described as differentially private.
Author(s)
Maintainer: Yee How Lau lauyeehow1986@gmail.com
See Also
Useful links:
Report bugs at https://github.com/lauyeehow1986-hub/Flexsynth/issues
Extract the synthetic data frame from a synth_result
Description
Extract the synthetic data frame from a synth_result
Usage
## S3 method for class 'synth_result'
as.data.frame(x, ...)
Arguments
x |
A |
... |
Unused. |
Value
The synthetic data.frame (for m == 1).
Extract the synthetic tables from a synth_linked_result
Description
Extract the synthetic tables from a synth_linked_result
Usage
## S3 method for class 'synth_linked_result'
as.list(x, ...)
Arguments
x |
A |
... |
Unused. |
Value
A named list of synthetic data.frames (for m == 1).
Check referential integrity of linked tables
Description
Verify that a set of linked tables is internally consistent: every table's
key identifies its rows uniquely, and every child row's foreign key resolves
to an existing parent row (no orphans). Works on a synth_linked() result or
on a raw named list of tables (supply keys in that case).
Usage
check_linkage(object, keys = NULL, verbose = TRUE)
Arguments
object |
A |
keys |
When |
verbose |
Logical; print a short summary. Defaults to |
Value
Invisibly, a report data.frame (one row per table, or per table and
collection when m > 1) with an ok attribute that is TRUE when there
are no duplicate keys and no orphan rows.
Examples
tabs <- list(
patients = data.frame(id = 1:3),
admissions = data.frame(id = c(1, 2, 9), admission_id = c(1, 1, 1))
)
check_linkage(tabs, keys = list(patients = "id",
admissions = c("id", "admission_id")))
Specific-utility comparison of an analysis on real vs synthetic data
Description
Fit the same analysis on the real data and on the synthetic data and compare
the estimates. This is the analysis-specific utility measure — "does my
regression come out the same?" — complementing the general marginal / pMSE
diagnostics of diagnose(). For each term it reports the confidence-interval
overlap (Karr et al. 2006; 1 = identical intervals, 0 = just touching,
negative = disjoint) and the standardised difference of the estimates on the
real standard-error scale (smaller is better).
Usage
compare_estimates(
real,
syn,
analysis,
rule = c("synthpop", "reiter"),
population_inference = TRUE,
conf.level = 0.95,
...
)
Arguments
real |
The real |
syn |
A |
analysis |
A function of one |
rule, population_inference |
Passed to |
conf.level |
Confidence level for both intervals (default |
... |
Reserved. |
Details
When syn is a multi-dataset synth() result its estimates are pooled with
pool_synth() (so the synthetic standard errors reflect synthesis); a single
synthetic data.frame is analysed directly. analysis uses the same contract
as pool_synth().
Value
A flexsynth_utility object; $estimates has term, est_real,
est_syn, overlap, std_diff, and both intervals.
See Also
Examples
d <- data.frame(id = 1:500, x = rnorm(500))
d$y <- 1 + 2 * d$x + rnorm(500)
res <- synth(d, ~ id, m = 5, seed = 1)
compare_estimates(d, res, function(dat) lm(y ~ x, dat))
Utility diagnostics comparing synthetic data to the real data
Description
Quantifies how faithfully synthetic data reproduces the real data's
distributions and dependence structure. Because Track A output carries no
formal privacy guarantee, these utility diagnostics (and the risk
diagnostics in disclosure_risk()) are how synthesis quality is judged.
Usage
diagnose(real, syn, vars = NULL, propensity = "logistic", ...)
Arguments
real |
The real |
syn |
The synthetic data: a |
vars |
Optional character vector restricting the variables compared; defaults to all columns present in both. |
propensity |
Propensity model for the pMSE, |
... |
Unused. |
Details
Four views are reported:
-
Univariate — per-variable marginal fit: a Kolmogorov-Smirnov statistic for numeric variables and a total-variation distance for categorical ones (both in
[0, 1]; smaller is better). -
Correlation — the Frobenius and mean absolute difference between the real and synthetic numeric correlation matrices.
-
Association — the mean and maximum absolute difference between the real and synthetic categorical association (Cramer's V) matrices, so dependence between factor variables is checked, not just numeric ones.
-
Propensity (pMSE) — a descriptive utility score: a model is fitted to tell real from synthetic records; when they are indistinguishable the propensity mean squared error sits near its null expectation, so the logistic model's
ratiois near 1 (larger means more distinguishable). This is an in-sample, row-level diagnostic: the default logistic model has main effects only, and repeated rows in longitudinal data are not treated as independent inferential evidence. CART does not have an analytic null ratio. Treat all results as descriptive rather than hypothesis tests.
If real and syn are named lists (or syn is a synth_linked_result),
each table is diagnosed and a per-table result is returned.
Value
A flexsynth_diagnostics object (or a flexsynth_diagnostics_list
for linked / list input). Has print() and plot() methods.
See Also
disclosure_risk() for privacy risk.
Examples
df <- data.frame(
id = 1:200,
age = round(rnorm(200, 60, 10)),
sbp = round(rnorm(200, 130, 15))
)
res <- synth(df, ~ id, seed = 1)
diagnose(df, res)
Empirical disclosure-risk diagnostics for synthetic data
Description
Reports how much a synthetic dataset could leak about the real records it was trained on. Four complementary measures are computed:
Usage
disclosure_risk(
real,
syn,
quasi = NULL,
target = NULL,
holdout = NULL,
max_records = 2000L,
seed = NULL,
...
)
Arguments
real |
The real |
syn |
The synthetic data: a |
quasi |
Character vector of quasi-identifier columns; defaults to all columns present in both frames. |
target |
Optional single sensitive column for the attribute-disclosure
(TCAP) measure; the quasi-identifiers minus |
holdout |
Optional |
max_records |
Cap on the number of rows used for the distance computations (each of real / synthetic is sampled down to this); keeps the pairwise distances tractable. Default 2000. |
seed |
Optional integer seed for the subsampling. |
... |
Unused. |
Details
-
Replicated uniques — records that are unique in the real data on the quasi-identifiers and are nonetheless reproduced exactly in the synthetic data. These are the classic identity-disclosure risks.
-
Distance to closest record (DCR) — for each synthetic record, the Gower distance to the nearest real record. A DCR of 0 is an exact copy. The synthetic-to-real distances are compared against the real-to-real nearest-neighbour distances: if synthetic records are not systematically closer to real records than real records are to each other, identity risk is low.
-
Membership inference — if a
holdoutof records not used for synthesis is supplied, an attacker who guesses "member" for records close to the synthetic data is simulated. The reported AUC (0.5 = no advantage) and advantage (2 * AUC - 1) measure how well training membership can be inferred. -
Attribute disclosure (TCAP) — if a sensitive
targetcolumn is named, the Target Correct Attribution Probability (Taub, Elliot et al.): an attacker who knows a real record's quasi-identifier keys reads the conditional distribution of the target off the synthetic records that share those keys.tcapis the mean synthetic probability of the true target over matched records,baselinethe marginal-only attacker evaluated on those same covered records, andliftthe excess the key-conditioning buys.baseline_unconditionalis also returned for context. Meant for categorical keys / target.
Quasi-identifiers should be the genuinely identifying columns; exclude
surrogate keys such as a regenerated unit id (a synthetic id never matches a
real one, which would understate risk). If real and syn are named lists
(or syn is a synth_linked_result), each table is assessed separately.
Value
A flexsynth_disclosure object (or flexsynth_disclosure_list) with
a print() method.
See Also
diagnose() for utility.
Examples
df <- data.frame(
id = 1:200,
age = round(rnorm(200, 60, 10)),
sex = sample(c("F", "M"), 200, replace = TRUE)
)
res <- synth(df, ~ id, seed = 1)
disclosure_risk(df, res, quasi = c("age", "sex"))
Differential-privacy controls (Track B)
Description
Opt into differentially private synthesis. Passing the result as
synth(..., privacy = dp_control(...)) selects the DP synthesiser and yields
a formal (\epsilon, \delta) guarantee. The privacy unit is
person-level by default: the guarantee protects a whole individual (all of
their rows), enforced by bounding each person's contribution before any budget
is spent.
Usage
dp_control(
epsilon,
delta = 0,
unit = c("person", "row"),
max_rows_per_person = NULL,
mechanism = c("laplace", "gaussian"),
dependence = c("tree", "independent"),
structure_frac = NULL,
degree = 1L,
select = c("fixed", "adaptive", "aim"),
treewidth = 1L,
select_frac = 0.25,
anneal = FALSE,
estimator = c("local", "pgm"),
scoring = c("auto", "independence", "model"),
cross_table = FALSE,
longitudinal = FALSE,
baseline = NULL,
transition_order = 1L,
transition_cross = 0L,
transition_parent = 0L,
bins = 12L,
bounds = NULL,
domain = c("dp", "public", "data"),
domain_frac = 0.1
)
Arguments
epsilon |
Positive privacy-loss budget. Smaller = more private, less utility. |
delta |
Failure probability for approximate DP; |
unit |
Privacy unit. |
max_rows_per_person |
Cap on how many rows one person may contribute,
used to bound sensitivity at |
mechanism |
Noise mechanism: |
dependence |
Dependence structure of the generative model: |
structure_frac |
Budget-efficient structure learning for the flat
|
degree |
Fan-in of the Bayesian network fitted for a flat |
select |
How the marginals that make up the model are chosen, for a flat
|
treewidth |
Adaptive selection only. Maximum clique size minus one in the
junction-tree model — the ceiling on interaction order the model can hold.
|
select_frac |
Adaptive selection only. Fraction of the marginal budget
spent on the private selection (the exponential-mechanism rounds); the
remaining |
anneal |
Adaptive ( |
estimator |
How the measured marginals are turned into the generative
model, for a flat |
scoring |
How |
cross_table |
Linked DP only ( |
longitudinal |
Linked DP only ( |
baseline |
Longitudinal releases. Names of subject-invariant columns —
baseline covariates that do not change across a unit's rows (e.g. birth sex, a
baseline measurement). These are held exactly constant within each
synthetic unit: they are modelled once in the initial-state model (so their
joint distribution and their correlation with the first visit are preserved)
and then broadcast to every row, rather than being stepped through a
transition matrix that would let them drift. Declaring a column baseline is
public schema knowledge, so it costs no budget; it also removes that
column's transition histogram from the release, sharpening every remaining
measurement at the same ( |
transition_order |
Longitudinal releases. Markov order of the within-unit
transition model: how many of a variable's own immediately preceding
values condition its next value. |
transition_cross |
Longitudinal releases. Number of other variables
(each at lag 1) that additionally condition each variable's transition —
moving from |
transition_parent |
Linked DP only ( |
bins |
Number of equal-width bins used to discretise each numeric
variable (default 12). Finer grids sharpen one-way marginals but make the
noisy two-way marginals used by |
bounds |
Optional named list giving |
domain |
How the discrete domain is chosen for variables without public
metadata: |
domain_frac |
Fraction of the privacy budget spent learning the domain
under |
Details
The DP engine (Track B) is a marginal-based synthesiser in the
PrivBayes / MST lineage. Continuous variables are discretised into a public
grid; low-order marginals are measured under the chosen noise mechanism with
correct budget composition; and synthetic records are drawn from the resulting
model. With dependence = "tree" a Chow-Liu tree of pairwise dependencies is
learned from the same noisy marginals (no extra budget) so second-order
structure is retained; dependence = "independent" keeps only one-way
marginals. All noise calibration and composition is reported back on the
result (see the accounting printed by synth()).
Where the bin edges come from matters, because reading them from the data (its
min / max) is itself a data-dependent step that can leak an individual's
presence. domain controls this:
"dp"(default)Rigorous and automatic. Numeric variables named in
boundsuse those public edges at no cost; any other numeric variable has its working range estimated under differential privacy (a clamp-free exponential-mechanism quantile at each end). A barecharactercolumn has its category set discovered under differential privacy by DP set-union (a stability histogram: each present category's noisy count must clear a threshold that hides any category a single person could have created; rare categories fold into an"(other)"catch-all). Both estimations spend an accounted slicedomain_fracof the budget, so the reported (\epsilon,\delta) is exact end to end. DP set-union needsdelta > 0(a threshold cannot hide a lone category's presence atdelta = 0), so a pure-\epsilonrelease still refusescharacter."public"Fully data-independent and free: every numeric variable must be given a public range in
bounds(an error is raised otherwise) and no budget is spent on the domain. The recommended mode when public ranges (codebook / physiological limits) are available."data"The non-rigorous legacy behaviour: bin edges are taken from the data range with a warning, and that step is excluded from the accounting. Kept only for benchmarking; do not use for a governed release.
Categorical variables carry their domain in their type: factor and logical
columns use their declared levels (public metadata, no leakage). In the "dp"
and "public" modes a bare character column is refused — convert it to a
factor with its full levels so the category set is public.
Value
An object of class dp_control (a validated list).
Examples
dp <- dp_control(epsilon = 1, delta = 1e-6, mechanism = "gaussian")
dp
List the available synthesis methods
Description
Return the names of every method currently registered (built-in plus any
added with register_method()).
Usage
list_methods()
Value
A character vector of method names.
Examples
list_methods()
Plot utility diagnostics
Description
Overlays the real and synthetic marginal distribution of each variable (density for numeric, side-by-side bars for categorical) using base graphics.
Usage
## S3 method for class 'flexsynth_diagnostics'
plot(x, vars = NULL, max_panels = 12L, ...)
Arguments
x |
A |
vars |
Optional subset of variables to plot. |
max_panels |
Maximum number of variables to draw (default 12). |
... |
Passed to the underlying plotting calls. |
Value
x, invisibly.
Pooled inference from synthetic data
Description
Fit an analysis on each of the m synthetic datasets in a synth() result
and combine the results into one estimate whose standard error reflects the
extra variability that synthesis introduces. An analysis of a single
synthetic dataset does not estimate between-synthesis variation, so its usual
standard errors can be miscalibrated. These functions implement published
fully-synthetic combining rules; their large-sample calibration still depends
on the estimand, synthesis model, sample size, and analysis assumptions.
Usage
pool_synth(
object,
analysis,
rule = c("synthpop", "reiter"),
population_inference = TRUE,
conf.level = 0.95,
...
)
Arguments
object |
A |
analysis |
A function of one synthetic |
rule |
Combining rule: |
population_inference |
If |
conf.level |
Confidence level for the intervals (default |
... |
Reserved for future use. |
Details
analysis is run once per synthetic dataset and must return either a fitted
model supporting coef() and vcov() (such as lm() or glm()) or a list
with numeric estimate and variance (squared standard error) vectors. The
per-dataset estimates are then combined with the chosen rule: "synthpop"
(Raab, Nowok & Dibben 2016, matching synthpop's summary.fit.synds) or
"reiter" (Reiter 2003 fully-synthetic, Tf = (1 + 1/m) b_m - vbar, which
needs m >= 2 and can be negative). Intervals are large-sample normal, as in
synthpop.
Only Track A (synth() without privacy) results are supported: differentially
private (Track B) inference must additionally account for the DP noise and is
out of scope here. Pooling of a synth_linked() result is not yet supported;
analyse a single table's synthetic frame instead.
Value
A flexsynth_pool object; $estimates is a data frame with term,
estimate, std.error, statistic, p.value, conf.low, conf.high.
See Also
synth_glm() for the common linear / generalised-linear case.
Examples
d <- data.frame(id = 1:400, x = rnorm(400))
d$y <- 1 + 2 * d$x + rnorm(400)
res <- synth(d, ~ id, m = 5, seed = 1)
pool_synth(res, function(dat) lm(y ~ x, dat))
Register a synthesis method
Description
Add a custom per-variable synthesiser to the method registry so it can be
selected by name via the method argument of synth() / synth_linked() or
the per-variable method in synth_control(). Registering a name that
already exists overwrites it, so you can override a built-in.
Usage
register_method(
name,
fit,
draw,
numeric = TRUE,
categorical = TRUE,
needs_predictors = TRUE
)
Arguments
name |
Method name (a single string) used to select it. |
fit |
A function |
draw |
A function |
numeric, categorical |
Logical flags declaring which target types the
method supports (used for validation). Both default to |
needs_predictors |
Logical; if |
Details
A method is a pair of functions:
fit(y, x, control)fits a model of the target
yon the predictordata.framex(which may have zero columns) and returns any object;draw(model, x, n, control)returns a length-
nvector of synthetic values for new predictor rowsx.
control is the synth_control() object, so a method can honour proper,
read its own hyperparameters, and so on.
Value
Invisibly, the method name.
Examples
# A trivial "mean" method for numeric variables.
register_method(
"constant_mean",
fit = function(y, x, control) mean(y),
draw = function(model, x, n, control) rep(model, n),
categorical = FALSE
)
"constant_mean" %in% list_methods()
Declare a synthesis constraint
Description
Capture a logical rule the synthetic data must satisfy. Rules are enforced by
synth() via rejection sampling at the unit grain (see Details).
Usage
rule(expr, scope = c("row", "unit"), label = NULL)
Arguments
expr |
A logical expression over the column names, e.g. |
scope |
|
label |
Optional human-readable label; defaults to the deparsed expression. |
Details
The expression is written in terms of the data's column names and is not
evaluated when rule() is called — it is captured and checked later against
the synthetic data.
Value
A flexsynth_rule object.
Examples
# A row rule and a within-unit temporal rule.
rule(dbp <= sbp)
rule(all(diff(visit_time) > 0), scope = "unit")
Synthesise a single (optionally nested / longitudinal) table
Description
Generate synthetic data for one table, working natively in long format. The
structure formula declares the nesting hierarchy (e.g.
~ id / visit / test_number) so repeated-measures and nested designs are kept
without pivoting to wide format.
Usage
synth(
data,
structure,
method = "cart",
constraints = NULL,
tuning = synth_control(),
privacy = NULL,
m = 1,
seed = NULL,
...
)
Arguments
data |
A |
structure |
A one-sided formula giving the nesting hierarchy, e.g.
|
method |
Synthesis method, |
constraints |
Optional |
tuning |
A |
privacy |
|
m |
Number of synthetic datasets to produce. |
seed |
Optional integer seed for reproducibility. |
... |
Reserved for future use. |
Details
The default (Track A) engine is sequential conditional synthesis in the
synthpop lineage: the unit identifier is regenerated by resampling whole
units (preserving realistic per-unit block sizes), and each remaining variable
is synthesised in turn from a model fitted on the real data, conditioning on
the structural columns and everything synthesised before it. With
method = "cart" each draw comes from the matching leaf of a regression /
classification tree, so marginal and conditional distributions are preserved
without distributional assumptions. Track A carries no formal privacy
guarantee.
Columns that are constant within every real unit (baseline covariates such as age or sex) are detected automatically and synthesised once per unit, then broadcast across that unit's rows, so a synthetic subject stays internally consistent. Time-varying columns are synthesised with an initial-state model (first row of each unit) plus a Markov transition model that conditions on the previous row within the unit (lag-1 predictors), so autocorrelation across visits is preserved. The number of rows per unit is drawn from the learned count distribution and the structural-index sequence is regenerated for each synthetic unit.
Any column containing NAs is given a missingness model: a companion
indicator is synthesised in sequence just before the column, the column's own
value model is fitted on the observed rows only, and the synthesised indicator
decides which rows are set back to NA. This preserves the missingness rate
and its association with the other variables, without letting missingness
cascade through predictors. The indicators are not returned.
Supplying constraints (see rule()) keeps only synthetic units whose rows
satisfy every rule, regenerating until enough valid units are collected — so
logical and within-unit temporal constraints are honoured without breaking
the nested structure.
Passing privacy = dp_control(...) selects differentially private synthesis
(Track B) with a formal (\epsilon, \delta) guarantee. A flat
structure = ~ id gives a marginal release; a nesting index
(~ id / visit) additionally engages a DP Markov model that preserves
within-unit temporal structure (length, initial state, and per-variable
transitions), for which max_rows_per_person must be set to the public
maximum rows per person. See vignette("differential-privacy").
Value
A synth_result object. Use as.data.frame() (for m == 1) or
$syn to get the synthetic data.
Examples
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, seed = 1)
head(as.data.frame(res))
Tuning controls for synthesis
Description
Collects the fine-grained knobs for the sequential synthesis engine. Strong
defaults mean beginners can ignore this; power users can tune the
utility / privacy / speed trade-off. Returns a validated synth_control
object consumed by synth() and synth_linked().
Usage
synth_control(
visit_sequence = NULL,
predictor_matrix = NULL,
method = NULL,
smoothing = NULL,
proper = FALSE,
count_model = c("marginal", "conditional"),
k = NULL,
cart = list(),
forest = list(),
constraint_max_tries = 50L,
parallel = FALSE
)
Arguments
visit_sequence |
Optional character vector giving the order in which
variables are synthesised. |
predictor_matrix |
Optional 0/1 matrix marking, for each variable, which
variables may predict it (including cross-table predictors). |
method |
Optional per-variable method override; a single string applies
globally. |
smoothing |
Numeric-variable kernel smoothing. |
proper |
Logical; use proper ( |
count_model |
How the number of rows per unit is drawn for nested /
longitudinal data. |
k |
Optional size of each synthetic dataset. |
cart, forest |
Named lists of hyperparameters passed to the CART / random
forest backends. For |
constraint_max_tries |
Integer; how many times |
parallel |
Controls parallel generation of the |
Value
An object of class synth_control (a validated list).
Examples
ctrl <- synth_control(proper = TRUE, cart = list(minbucket = 5))
ctrl
Pooled generalised-linear analysis of synthetic data
Description
Convenience wrapper over pool_synth() for the common case: fit the same
glm() on each synthetic dataset and combine. With the default
family = gaussian() this is an ordinary linear model.
Usage
synth_glm(
object,
formula,
family = stats::gaussian(),
rule = c("synthpop", "reiter"),
population_inference = TRUE,
conf.level = 0.95,
...
)
Arguments
object |
A |
formula |
A model formula (e.g. |
family |
A |
rule, population_inference, conf.level |
Passed to |
... |
Further arguments to |
Value
A flexsynth_pool object (see pool_synth()).
Examples
d <- data.frame(id = 1:400, x = rnorm(400))
d$y <- rbinom(400, 1, plogis(-0.5 + d$x))
res <- synth(d, ~ id, m = 5, seed = 1)
synth_glm(res, y ~ x, family = binomial())
Jointly synthesise multiple linked nested / longitudinal tables
Description
Synthesise several related tables together so that referential integrity and
cross-table statistical relationships are preserved. The table hierarchy is
read from the keys: a table is a child of the table whose full key equals
its own key with the last column dropped (so c("id", "admission_id") is a
child of c("id")). Root tables are synthesised first with the single-table
engine (synth()); each child table is then generated from its synthetic
parent.
Usage
synth_linked(
tables,
structures,
keys,
method = "cart",
constraints = NULL,
tuning = synth_control(),
privacy = NULL,
m = 1,
seed = NULL,
...
)
Arguments
tables |
Named list of input |
structures |
Named list of one-sided formulas, one per table, giving
each table's nesting hierarchy (e.g. |
keys |
Named list of character vectors giving each table's key columns. The last column is the table's own index; the leading columns are the foreign key into its parent. |
method |
Synthesis method applied per variable unless overridden. |
constraints |
Optional cross-table constraints / rules. Constraint
enforcement currently applies to single-table |
tuning |
A |
privacy |
|
m |
Number of synthetic dataset collections to produce. |
seed |
Optional integer seed for reproducibility. |
... |
Reserved for future use. |
Details
For every synthetic parent record the number of child rows is drawn from a
learned count distribution (including parents with no children), the
child's foreign key is copied from the parent so it always resolves
(referential integrity), the child's own structural index is regenerated, and
the child's variables are synthesised conditionally on the parent's
synthesised attributes (cross-table predictors), the own index and earlier
child variables. Conditioning is on the immediate parent; a grandparent
reaches a child only through the parent's synthesised values. Use
check_linkage() to verify the result. Track A carries no formal privacy
guarantee.
Passing privacy = dp_control(...) selects differentially private synthesis
(Track B) with a formal (\epsilon, \delta) guarantee at the
root-entity grain: adding or removing one root individual (its root row
and all descendant rows) changes the release within the budget. Contribution
is bounded hierarchically — max_rows_per_person gives the maximum children
kept per parent for each child table (a single integer for all child tables,
or a named list keyed by table name); the root cap is 1. Each table's variable
marginals and a children-per-parent count histogram are measured under one
exactly-composed budget; synthetic children copy the synthetic parent's
surrogate key so referential integrity still holds by construction. By default
a child's variables are modelled by their own within-table marginals
(referential integrity is preserved, but not cross-table statistical
dependence) and its repeated rows are treated as exchangeable. Two opt-ins in
dp_control() add structure at extra, exactly-composed budget:
cross_table = TRUE conditions each child variable on its synthetic parent's
attributes (so parent \to child dependence, not just referential
integrity, survives the noise), and longitudinal = TRUE (or a character
vector of child-table names) models a child's repeated rows as a within-unit
DP Markov trajectory — an initial-state model plus per-variable
P(v_t \mid v_{t-1}) transitions — so within-table longitudinal structure
such as visit-to-visit autocorrelation is retained; the two combine. A
longitudinally-modelled child needs a branching cap \ge 2 in
max_rows_per_person, and its recoverable temporal signal falls as the noise
grows (deeper nesting, larger caps, finer bins, or smaller \epsilon).
constraints are refused under DP. See vignette("differential-privacy").
Value
A synth_linked_result. Use as.list() (for m == 1) or $syn to
get the named list of synthetic tables.
Examples
patients <- data.frame(id = 1:20,
sex = sample(c("F", "M"), 20, TRUE),
stringsAsFactors = FALSE)
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
)
check_linkage(res)