synthweave
Synthetic tabular data that does not require real microdata.
Quickstart · Why · How it works · Three ways to use it · FAQ · Full guide
Most synthetic data tools fit a model to data you already have. That is no help when you have none, cannot get access, or are not allowed to put what you have into a model.
synthweave generates from a schema you declare by hand, then optionally layers in statistical structure, realistic messiness, and identifiers that link correctly across tables. It works the other way round too:
| What you have | What synthweave does |
|---|---|
| Nothing but domain knowledge | Generates from rules you declare |
| Real microdata | Fits on it, like any other synthesis tool |
| Only published statistics | Expands those into a training frame |
| None of the above | Pulls public US Census data as a stand-in |
New here? Every term below is defined in the full guide: glossary, complete syntax reference, and an 8-step tutorial. This README is the fast tour.
Install
pip install synthweave
Other install options
Latest commit, ahead of the last PyPI release:
pip install "git+https://github.com/emmanuelwunjc/synthweave.git"
Local development:
git clone https://github.com/emmanuelwunjc/synthweave.git
cd synthweave
pip install -e ".[dev]"
Optional extras: [parquet] for Parquet output, [pii] for Faker-backed names
and SSNs.
Quickstart
import synthweave as sw
people = sw.Entity("person", count=10_000, attributes={"education": ["HS", "College"]})
table = sw.Table("roster", grain="person", carry="*")
result = sw.Pipeline(sw.Schema([people], [table])).run()
result["roster"].head()
Ten thousand rows. No data file, no API key, no setup beyond pip install.
That is the whole shape of synthweave: describe a population once (Entity),
describe a table that records it (Table), run it (Pipeline). Everything
below is a variation on those three lines.
Why synthweave
| synthweave | GAN/VAE synthesizer | Fake-data generator (Faker) | |
|---|---|---|---|
| Needs real data to start | No | Always | No |
| Preserves column relationships | Yes, declared or learned | Yes, with training data | No |
| Cross-table IDs that link | Yes, derived, no lookup table | Not built in | Not built in |
| Deterministic output | Yes, per seed, any chunk size | Usually not | Usually yes |
| Config provenance tracking | Yes | Not attempted | Not attempted |
The gap it targets: you need data that looks and behaves like a real linked administrative dataset (a school roster, a wage file, a benefits record, all describing the same people), and you either cannot access real microdata or are only prototyping and do not want to touch it yet.
How it works
A pipeline is up to four stages, each optional past the first:
| Stage | Does what | Needs real data |
|---|---|---|
| Generate | Draws every attribute and column from its rule | No |
| Synthesize | Refits columns with a statistical model against a chosen structure source | No |
| Link | Attaches identifiers that match the same person across tables | No |
| Noise | Corrupts values deliberately (typos, blanks, scan errors) | No |
Three properties hold it together.
Nothing is random. Every value is a pure function of (seed, stable key, salt). Same seed, same output, forever, regardless of chunk size. That is what
lets two tables generated years apart still link correctly with no lookup table
involved.
One caveat for the statistically minded
The model-fitting step uses scikit-learn's decision trees, which do not take an explicit seed from synthweave. This has never produced observed non-determinism, but unlike everything else here it is not derived from the seed by construction. Worth knowing if you rely on bit-for-bit reproducibility as a hard guarantee rather than an extremely well-tested one.
Structure has to come from somewhere. A model fit on independently generated columns learns nothing, so the synthesizer always names a structure source:
Declaredlearns from relationships your ownConditionalrules already put in the data. Use when you have domain knowledge but no data.Empiricallearns from a real sample you supply.Priorlearns from published statistics. No rows needed.
Every config number says where it came from. Wrap a guess in
sw.modeled(0.08, "not yet sourced") and a real figure in
sw.cited(0.82, "NCES Table 219.10"). Then result.unjustified() lists every
number nobody has defended yet, so a config is auditable instead of a wall of
magic numbers.
There is also a reduced-typing shorthand for common cases (a bare list instead
of sw.Choice([...]), grain="person" instead of sw.PerEntity("person")).
See the fast path.
Three ways to use it
Pick the one matching what you actually have. All three share this setup:
people = sw.Entity("person", count=10_000, attributes={"education": ["HS", "College"]})
wages = sw.Table("wages", grain="person", carry=["education"], columns={"wage": 0.0})
schema = sw.Schema([people], [wages])
1. Domain knowledge only
Declare the relationship yourself. Conditional puts real structure in before
anything is fitted, so wage depends on education because you said so, not
because a model guessed. No synthesizer= needed.
wages = sw.Table("wages", grain="person", carry=["education"], columns={
"wage": sw.Conditional("education", {
"HS": sw.Normal(38_000, 9_000, low=0),
"College": sw.Normal(64_000, 18_000, low=0),
}),
})
result = sw.Pipeline(sw.Schema([people], [wages])).run()
2. A real sample you want more of
Keep the 0.0 placeholder (the synthesizer overwrites it) and hand over your
real data. It learns the actual relationships and generates rows that carry
them.
real_sample = pd.read_csv("my_real_wages.csv") # columns: education, wage
result = sw.Pipeline(
schema,
synthesizer=sw.CARTSynthesizer(["wage"], predictors=["education"], structure=real_sample),
).run()
3. Published statistics, or nothing at all
A report says "60% HS, 40% College"? Use it directly, no rows needed:
result = sw.Pipeline(
schema,
synthesizer=sw.CARTSynthesizer(["education"], structure={"education": {"HS": 0.6, "College": 0.4}}),
).run()
If you do not even have that, borrow real government survey data as a stand-in:
from synthweave.connectors.acs_pums import fetch_pums
acs = fetch_pums(["AGEP", "PINCP"], state="NY").rename(columns={"AGEP": "age", "PINCP": "income"})
synthesizer = sw.CARTSynthesizer(["age", "income"], structure=acs)
Worth knowing: this is real data, but not your population. It is a Census survey sample for a given state and year, so treat it as a plausible reference distribution, not a guarantee it matches your target demographics. Needs a free Census API key as
CENSUS_API_KEY.
All three run end to end in examples/three_layers_data_availability.py.
FAQ
Is the output actually private, or safe to share?
synthweave does not yet include formal disclosure-risk metrics or differential privacy. Treat output as "structurally realistic, not formally privacy-audited."
One mechanism is worth naming explicitly. When a synthesized column's structure
source is Empirical, each output value is sampled from the actual real
values that landed in the same decision-tree leaf as the row being
synthesized, not a perturbed or resampled one. With few real rows behind a
narrow leaf, that is a genuine attribute-disclosure vector if the columns you
condition on are also identifying. Control leaf size with min_samples_leaf
(see the guide).
Does my real data ever leave my machine?
No. Empirical(df) and fetch_pums(...) run entirely locally. The Census
connector calls the public Census API to fetch government-published data. It
never sends anything.
How do I check synthesis preserved my real data's structure?
sw.fidelity_report(synth_df, real_df, columns=[...]).
Declared and Prior have no original to check against, since the schema
declares whatever structure it wants. Empirical is different: it fits on real
data, and the point is that output carries the same marginal distributions,
category shares, and pairwise associations the input had. The report compares
both, and with synthesizer= also reports how many rows kept a pre-synthesis
placeholder because their leaf had no donors.
Is this like Faker?
No. Faker generates realistic individual values with no relationship between columns or rows. synthweave generates whole linked datasets: multiple tables, the same person's ID matching across all of them, and real statistical relationships between columns.
Can I trust the numbers in someone else's config?
Run result.unjustified(). Anything not tagged cited with a real source is
either a deliberate choice (user-provided) or an unverified guess
(modeled), and you can see which at a glance.
What if none of the three structure sources fit my case?
Every stage is a Protocol with a registry. Implement the shape, register it,
no library edits or inheritance required. See "Extending it" in
the guide.
Scale
Stages pass chunks, not whole tables, so memory stays flat as row counts grow. Measured at roughly 200,000 rows/s with peak RSS around 0.44 GB, streamed straight to disk, on a 3.2 million row run.
The memory figure is the load-bearing one: it does not grow with row count, so tens of millions of rows is a matter of time, not RAM. Throughput varies with machine load, so treat it as an order of magnitude, not a benchmark.
sw.Pipeline(schema, chunk_size=200_000).run_to("out/", format="parquet")
Status
v0.2. Pre-1.0: the public API may still change between minor versions.
Every capability described above is present and tested. One implementation ships per stage, and the interfaces are the real commitment, so adding implementations should not require restructuring.
The API freezes at v1.0, which also requires zero known bugs in the "produces plausible-looking wrong data without raising" class. Those are tracked openly in the issue tracker along with the design reasoning behind them.
Not yet included: disclosure-risk metrics, differential privacy, R interop, deep generative synthesizers.
Development
pip install -e ".[dev]"
pre-commit install
PYTHONPATH=src python -m pytest tests/ -q
Tests run offline and go through the public API only, so internal stage code can be rewritten without touching them. Every bug fix is checked by reverting it and confirming a test goes red: a fix nothing catches is not a fix. See CONTRIBUTING.md.
Where this came from
The design generalizes two patterns proven in a real engagement building linked synthetic administrative data: deterministic hash-derived identifiers, and provenance-tagged config. It draws on pseudopeople for realistic noise models, and synthpop / tidysynthesis for sequential CART synthesis. What is new is combining them on a schema you define yourself, with no real data required to start.
Also runs in a browser. synthweave-frontend runs this pipeline locally, checks the linking guarantee against the produced data, and re-runs your config at three chunk sizes to prove the output is identical.
License
MIT
Metadata
Release files for synthweave 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| synthweave-0.2.0.tar.gz | 116.1 kB | With provenance | Details for synthweave-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| synthweave-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for synthweave-0.2.0-py3-none-any.whl |
Total release size: 182.7 kB
Release files / synthweave-0.2.0.tar.gz
| Download URL | synthweave-0.2.0.tar.gz |
|---|---|
| Size | 116.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
897bc7b5777a7f638151432404484092fae56c68b1e2ebc08a4dd7ce77f9e739
|
|
BLAKE2b-256 checksum How to use checksums |
7bebf0ab04b53efd043c40ce021e2e22137e20edb5e05f0caf418c554d7af17f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency logRelease files / synthweave-0.2.0-py3-none-any.whl
| Download URL | synthweave-0.2.0-py3-none-any.whl |
|---|---|
| Size | 66.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b600d3fc56d03f51c1cc5ca36706556405580d5949c08e57f16a12de29c8a72e
|
|
BLAKE2b-256 checksum How to use checksums |
11d0d8267963e0c3a4f4574f5ace71d0794d7509e8cb84d70cf93ef102ec85f2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency logSecurity for synthweave 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for synthweave-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.