# Getting started with 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.

    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)

id | age | sex | smoker | sbp
--- | --- | --- | --- | ---
1 | 55 | F | TRUE | 128
2 | 64 | F | TRUE | 151
3 | 53 | M | FALSE | 127
4 | 80 | F | FALSE | 143
5 | 66 | F | FALSE | 128
6 | 53 | F | FALSE | 110

`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`.

    res <- synth(real, structure = ~ id, seed = 1)
    res

    #> <synth_result>
    #>   track        : A (high-utility; NOT differentially private)
    #>   datasets (m) : 1
    #>   rows each    : 300  (input: 300 )
    #>   synthesised  : age, sex, smoker, sbp
    #>   unit-level   : age, sex, smoker, sbp (once per unit)
    #>   method       : cart
    #>
    #> Get the data with as.data.frame(x)

    syn <- as.data.frame(res)
    head(syn)

id | age | sex | smoker | sbp
--- | --- | --- | --- | ---
1 | 65.578 | M | FALSE | 143.902
2 | 59.392 | M | FALSE | 130.690
3 | 53.896 | F | FALSE | 113.032
4 | 53.052 | F | FALSE | 114.245
5 | 50.355 | F | FALSE | 116.001
6 | 72.592 | M | FALSE | 139.909

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.

    analysis_vars <- c("age", "sex", "smoker", "sbp")
    d <- diagnose(real, res, vars = analysis_vars)
    d

    #> <flexsynth_diagnostics>
    #>   rows        : real 300  synthetic 300
    #>   variables   : 4
    #>
    #> Univariate fit (smaller = closer):
    #>  variable        type metric distance
    #>       age     numeric     ks   0.0600
    #>       sex categorical    tvd   0.0500
    #>    smoker     logical    tvd   0.0433
    #>       sbp     numeric     ks   0.0433
    #>   mean distance: 0.0492   worst: age (0.0600)
    #>
    #> Correlation structure (2 numeric vars):
    #>   Frobenius diff: 0.0618   mean |diff|: 0.0437   max |diff|: 0.0437
    #>
    #> Categorical association (Cramer's V, 2 vars):
    #>   mean |diff|: 0.0000   max |diff|: 0.0000
    #>
    #> Propensity utility (pMSE, logistic; descriptive, in-sample):
    #>   pMSE: 0.00123   expected: 0.00083   ratio: 1.48 (1 = indistinguishable)

The `plot()` method overlays each marginal.

    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.

    disclosure_risk(real, res, quasi = c("age", "sex", "smoker", "sbp"), seed = 1)

    #> <flexsynth_disclosure>
    #>   rows            : real 300  synthetic 300
    #>   quasi-identifiers: age, sex, smoker, sbp
    #>
    #> Replicated uniques (identity risk):
    #>   real sample-uniques : 282
    #>   reproduced in syn   : 0  (0.00% of uniques, 0.00% of real rows)
    #>   syn rows copying a real row: 0  (0.00% of syn)
    #>
    #> Distance to closest record (Gower, 0 = exact copy):
    #>   syn->real  : median 0.0075   5th pct 0.0018   exact copies 0.00%
    #>   real->real : median 0.0088   (baseline)
    #>   median syn distance is smaller than the real-neighbour baseline
    #>   descriptive only: inspect lower-tail distances and exact copies; this is not a safety guarantee
    #>
    #> Membership inference: not run (supply `holdout` of non-training records).
    #>
    #> Attribute disclosure: not run (supply `target` = a sensitive column).

## 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`.

    list_methods()

    #> [1] "cart"     "ctree"    "forest"   "norm"     "normrank" "sample"

Set one method for all variables, or per variable via `synth_control()`:

    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.

    res3 <- synth(real, ~ id,
                  constraints = rule(sbp >= 80 & sbp <= 220),
                  seed = 1)
    range(as.data.frame(res3)$sbp)

    #> [1]  92 171

## 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.