--- title: "Getting started with flexsynth" output: litedown::html_format: meta: css: ["@default"] vignette: > %\VignetteEngine{litedown::vignette} %\VignetteIndexEntry{Getting started with flexsynth} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") set.seed(1) library(flexsynth) ``` ## What flexsynth does `flexsynth` generates new records intended to reproduce selected statistical structure from a real dataset. The default **Track A** engine is a utility-oriented sequential synthesiser. It may reproduce source values or even records, carries **no** formal privacy guarantee, and must not be treated as anonymisation. The package's empirical utility and disclosure-risk diagnostics inform a release review; they do not prove that a release is safe. The opt-in differentially private **Track B** and the conditions required for its formal guarantee are described in `vignette("differential-privacy")`. ## A first synthesis We start with a plain single-table dataset — one row per patient. ```{r data} n <- 300 real <- data.frame( id = seq_len(n), age = round(rnorm(n, 62, 11)), sex = sample(c("F", "M"), n, replace = TRUE, prob = c(0.45, 0.55)), smoker = sample(c(FALSE, TRUE), n, replace = TRUE, prob = c(0.7, 0.3)) ) real$sbp <- round(0.6 * real$age + ifelse(real$smoker, 8, 0) + rnorm(n, 90, 10)) head(real) ``` `synth()` takes the data and a `structure` formula naming the unit identifier. Here every row is its own unit, so the structure is just `~ id`. ```{r synth} res <- synth(real, structure = ~ id, seed = 1) res syn <- as.data.frame(res) head(syn) ``` The unit identifier is regenerated, and every other column is synthesised in turn from a model fitted on the real data (the default `method = "cart"` draws from the matching leaf of a regression / classification tree). ## Is the synthetic data any good? — `diagnose()` `diagnose()` compares real and synthetic data on four descriptive views: per-variable marginals (a Kolmogorov-Smirnov statistic for numeric variables and total-variation distance for categorical ones), numeric correlations, categorical associations (Cramer's V), and a **propensity (pMSE)** score. For the default main-effects logistic model, a `ratio` near 1 is its null benchmark; larger values indicate greater row-level distinguishability. The score is fitted and evaluated in-sample, and repeated rows in longitudinal data are not independent evidence, so do not interpret the ratio as a hypothesis test. ```{r diagnose} analysis_vars <- c("age", "sex", "smoker", "sbp") d <- diagnose(real, res, vars = analysis_vars) d ``` The `plot()` method overlays each marginal. ```{r diag-plot, fig.width = 7, fig.height = 5} plot(d) ``` ## How risky is it? — `disclosure_risk()` Synthetic data is **not** anonymisation. `disclosure_risk()` provides four empirical checks: replicated uniques; distance to the closest real record (DCR); membership inference when a genuine, synthesis-excluded holdout is supplied; and Target Correct Attribution Probability (TCAP) when a categorical sensitive `target` is named. Pass genuinely identifying columns as `quasi`, excluding surrogate keys such as the regenerated `id`. These diagnostics flag potential identity, membership, and attribute-disclosure problems; no threshold certifies a release as safe. ```{r risk} disclosure_risk(real, res, quasi = c("age", "sex", "smoker", "sbp"), seed = 1) ``` ## Choosing a method Methods live in an extensible registry. Built-ins include `sample`, `cart`, `forest` (a bagged CART ensemble), `ctree` (via `partykit`), and the parametric numeric methods `norm` and `normrank`. ```{r methods} list_methods() ``` Set one method for all variables, or per variable via `synth_control()`: ```{r per-var} ctrl <- synth_control(method = c(sbp = "norm", age = "cart")) res2 <- synth(real, ~ id, tuning = ctrl, seed = 1) ``` You can register your own with `register_method()` — supply a `fit()` and a `draw()`. ## Constraints `rule()` declares a logical constraint the synthetic data must satisfy; `synth()` enforces it by rejection sampling at the unit grain. ```{r rules} res3 <- synth(real, ~ id, constraints = rule(sbp >= 80 & sbp <= 220), seed = 1) range(as.data.frame(res3)$sbp) ``` ## Tuning `synth_control()` also exposes `smoothing` (kernel-smooth numeric draws) and `predictor_matrix` (restrict which variables may predict each target). See `?synth_control`. Track A output must never be described as differentially private. Use the diagnostics above to decide whether the utility / risk balance is acceptable for your release.