synthweave
Synthetic tabular data that does not require real microdata.
Most synthetic data tools fit a model to data you already have. That's no help when you have none, can't get access, or aren't 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, all without a single real row required to start.
It works the other way round too. Have real microdata? Hand it over and synthweave fits on it like any other synthesis tool. Have only published statistics, no rows at all? It works from those instead. Have neither? It can pull real reference data from a public source (the US Census) to 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.
Contents
Install
Not on PyPI yet. Install straight from GitHub:
pip install "git+https://github.com/emmanuelwunjc/synthweave.git#subdirectory=package"
Or clone and install locally for development:
git clone https://github.com/emmanuelwunjc/synthweave.git
cd synthweave/package
pip install -e .
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's the entire shape of synthweave: describe a population once
(Entity), describe a table that records it (Table), run it
(Pipeline). Everything else in this README is a variation on those three
lines.
Why synthweave
| synthweave | A GAN/VAE-based synthesizer (a deep learning model trained on your real data) | A fake-data generator (e.g. Faker) | |
|---|---|---|---|
| Needs real data to start? | No | Yes, always | No |
| Preserves relationships between columns? | Yes, if declared or learned | Yes, if you have training data | No, columns are independent |
| Cross-table IDs that actually link? | Yes, derived, no lookup table | Not built in | Not built in |
| Deterministic, reproducible output? | Yes, for a given seed, regardless of chunk size | Usually not (model training has randomness) | Usually yes |
Config transparency is a synthweave-specific feature, not a category comparison: every number in a config can be tagged with where it came from (see Every config number says where it came from below), which the other two approaches don't attempt to solve.
The gap synthweave 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 can't access real microdata, or you're only prototyping and don't 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 some columns using a statistical model, against a chosen structure source — see below | No |
| Link | Attaches identifier columns that match the same person across tables | No |
| Noise | Deliberately corrupts values (typos, blanks, scanned-form errors) | No |
Three properties make the whole thing hold together:
-
Nothing is random. Every value is a pure function of
(seed, a stable key, a salt). Same seed, same output, forever, regardless of chunk size — verified across fresh process runs, and locked in by the test suite. That's what lets two tables generated years apart still link correctly, with no lookup table involved. (One caveat for the statistically minded: the underlying model-fitting step uses scikit-learn's decision trees, which don't take an explicit seed from synthweave. This hasn't produced any observed non-determinism, but unlike everything else in synthweave, it isn't derived from the seed by construction — worth knowing if you're relying 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 — where it learns relationships between columns from:
Declared— nothing extra; learn from the relationships your ownConditionalrules already put in the data. Use this when you have no real data, only domain knowledge.Empirical— learn from a real sample you supply.Prior— learn from published statistics (percentage breakdowns), no rows needed.
See "Three ways to use it" below for all three in code.
-
Every config number says where it came from. Wrap a guess in
sw.modeled(0.08, "not yet sourced"), a real figure insw.cited(0.82, "NCES Table 219.10").result.unjustified()lists every number nobody has defended yet, so a config is auditable instead of a wall of magic numbers.
There's also a reduced-typing shorthand for the common cases (a bare list
instead of sw.Choice([...]), grain="person" instead of
sw.PerEntity("person"), and more) — see the fast path
in the full guide.
Three ways to use it
Pick the one that matches what you actually have. All three build on this shared 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])
"I have no data, only domain knowledge"
Declare the relationship yourself. Conditional puts real structure into the
data before anything is fitted, so wage genuinely depends on education
because you said so, not because a model guessed it — no synthesizer=
needed at all, this is what wages above already does once you replace its
placeholder "wage": 0.0 with:
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),
}),
})
schema = sw.Schema([people], [wages])
result = sw.Pipeline(schema).run()
"I have some real data, and want more like it"
Keep wages as shown in the shared setup (a 0.0 placeholder — the
synthesizer overwrites it), and hand your real sample straight to it. It
learns the actual statistical relationships in your data and generates new
rows that carry them, without exposing any single real row verbatim as a
whole — though see the FAQ below on what "verbatim" does and doesn't cover.
import pandas as pd
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()
"I have neither real data nor a real sample"
If you have published statistics (a report says "60% HS, 40% College"), use those directly, no rows needed:
result = sw.Pipeline(
schema,
synthesizer=sw.CARTSynthesizer(["education"], structure={"education": {"HS": 0.6, "College": 0.4}}),
).run()
That resynthesizes education in place, even though it's a carried
attribute rather than a column declared directly on wages — the
synthesizer doesn't care which, it only needs the column to exist by the
time it runs.
If you don't even have that, borrow real government survey data as a stand-in. Worth knowing: this is real data, but not your population — it's a Census survey sample for a given state/year, so treat it as a plausible reference distribution, not a guarantee that it matches your actual target population's demographics.
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)
Needs a free Census API key (sign up here)
set as CENSUS_API_KEY. Full working version of all three: examples/three_layers_data_availability.py.
FAQ
Is this like Faker? No. Faker generates realistic-looking individual values (names, addresses, emails) with no relationship between columns or rows. synthweave generates whole linked datasets: multiple tables, the same person's ID matching correctly across all of them, and real statistical relationships between columns, either declared or learned.
Does my real data ever leave my machine?
No. Empirical(df) and fetch_pums(...) both run entirely locally; nothing
is uploaded anywhere. The Census connector calls the public Census API to
fetch government-published data, it never sends anything.
Is the output actually private / safe to share?
synthweave does not (yet) include formal disclosure-risk metrics or
differential privacy. Treat v0.1 output as "structurally realistic, not
formally privacy-audited," with one specific mechanism worth naming: when a
synthesized column's structure source is Empirical (real data), each
output value for that column is sampled from the actual real values that
landed in the same decision-tree leaf as the row being synthesized, not a
resampled or perturbed value. With few real rows behind a narrow leaf, that
is a real attribute-disclosure vector if the columns you condition on are
also identifying. See CARTSynthesizer's min_samples_leaf in
the full guide to control leaf size, and Status.
Can I trust the numbers in someone else's config?
Run result.unjustified(). Anything not tagged cited with a real source is
either something the author chose deliberately (user-provided, the
default) or an unverified guess (modeled) — you can see which is which at
a glance.
How big can the output get? Memory stays flat as row count grows; see Scale. Tested to several million rows streamed to disk.
Do I need a Census API key for anything other than the ACS PUMS connector? No. Everything else (declared structure, your own real data, published aggregates) needs no external service or key at all.
What if my dataset doesn't fit any of the three built-in structure sources?
Every stage is a Protocol with a registry: implement the shape, register
it, no library edits or inheritance required. See "Extending it" in
the full 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 doesn't 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.1. Every capability described above is present and tested; several are deliberately preliminary. One implementation ships per stage; the interfaces are the real commitment, so adding implementations shouldn't require restructuring.
Not yet included: R interop, deep generative synthesizers (GAN/VAE-based), disclosure-risk metrics, differential privacy.
Known issues and the design reasoning behind them are tracked in the issue tracker, not in the repo.
Development
pip install -e ".[dev]"
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 isn't a fix.
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's new is combining them on a schema you define yourself, with no real data required to start.
Also runs in a browser: synthweave-frontend is a local front end that runs this pipeline, checks the linking guarantee against the produced data, and re-runs your config at three chunk sizes to prove the output is identical. Ships a three-table worked example and a walkthrough deck.
License
MIT
Metadata
Release files for synthweave 0.1.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.1.0.tar.gz | 80.7 kB | Without provenance | Details for synthweave-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| synthweave-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for synthweave-0.1.0-py3-none-any.whl |
Total release size: 140.1 kB
Release files / synthweave-0.1.0.tar.gz
| Download URL | synthweave-0.1.0.tar.gz |
|---|---|
| Size | 80.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
49366551c02dbfd5e6618c7ae0b9416bfb1951654ea7d257542f79f29d8aa935
|
|
BLAKE2b-256 checksum How to use checksums |
47ae1e4a2ab4619307093db4f207e4c74be90faff9b821b9862787fd4a75f7a6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Release files / synthweave-0.1.0-py3-none-any.whl
| Download URL | synthweave-0.1.0-py3-none-any.whl |
|---|---|
| Size | 59.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
17fb6fddd5225907444a308e79d4cfafb369d2ef735b68d6a43ce71e349699a4
|
|
BLAKE2b-256 checksum How to use checksums |
2a190ea0dbf6d6978cf89326afc1ec9306c18fdc9e9c28efd37661b634ad0433
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Security for synthweave 0.1.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A 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.