Ahmed Doghri Logo Image
Ahmed Doghri

tabflowmini

Fake tables are easy. Fake tables that don't quietly wreck the churn model built on top of them are the actual assignment. tabflowmini reports marginal fit, category drift, churn drift, and duplicate rate. Mean KS lands at 0.056.

tabflowmini, a tabular synthetic data generator

Fake Rows Need Real Checks

A synthetic table can look completely plausible and still be worthless. It can match the column means and quietly ruin the category balance, or preserve the categories and leak entire rows, or worst of all, break the exact label relationship that made the table valuable in the first place.

tabflowmini fits a compact transport model over continuous columns, samples categorical marginals separately, regenerates the churn label from the synthetic features, and then actually checks all of that instead of declaring victory after one histogram looks close enough.

Update: "regenerates the churn label" turned out to be generous. The sampler's churn formula was the exact coefficients the real-data generator uses, copy-pasted rather than fitted — the model literally already knew the answer. A fitted churn_rate field sat right there in the model and was never read. Full writeup below.

Small Flow, No Neural Net Hiding Anywhere

The generator is inspired by flow matching, but implemented as a readable Gaussian transport instead of a hidden neural net. That keeps the repo dependency-free and makes every assumption something you can actually see.

The benchmark reports KS distance for continuous columns, plan distribution gap, churn rate gap, and rounded duplicate rate. This is a small audit harness, not a magic anonymizer, and it will tell you exactly which number to worry about.

The Number

Mean KS distance is 0.056 across continuous columns. Plan gap is 0.040. Churn gap, the number the whole exercise exists to protect, is 0.026. Duplicate rate is 0.000, meaning zero rows leaked straight out of the training set.

3 tests pass locally and on GitHub Actions across Python 3.9, 3.11, and 3.13, with metrics written to `artifacts/metrics.json` every run.

The Churn Label Was Copied, Not Learned

That 0.026 churn gap looked like proof the model had learned the churn relationship from the fitted correlations. It hadn't. The sampler's churn formula was -1.0 + 0.08 * visits - 0.015 * (income - 70) + plan_bonus, which is not derived from anything — it's a byte-for-byte copy of the coefficients the real-data generator itself uses to produce churn. fit_synth even computes a churn_rate field from the real table; the sampler never touches it.

To check whether that mattered, I built adversarial.make_table_variant: tables structurally identical to the original but with randomized true churn coefficients per seed. Run through the unmodified sampler, mean churn gap degrades from ~0.03 to 0.17–0.21 — because the sampler only ever "knows" one specific table's answer key.

sample_synth_v2 fits an actual feature-standardized logistic regression against each real table's observed visits, income, and plan columns instead of assuming the coefficients. Across a 20-seed tuning sweep and a disjoint 15-seed holdout evaluated once, the learned churn gap holds near 0.01 — both on the original table and on tables it has never seen the true relationship for. data.py and synth.py are untouched; the originally published 0.056/0.040/0.026/0.000 numbers still reproduce exactly, and the fix ships opt-in via python -m tabflowmini.eval_v2.

Tools Used

Python
Synthetic Data
Tabular Modeling
Flow Transport
Privacy Checks
Logistic Regression
pytest