Testland
Browse all skills & agents

faker-synthetic-data

Substitutes realistic replacement values for PII that a masking or de-identification step removed, nulled, or redacted, so a non-production dataset stays usable. Covers building an injective substitution map that keeps a shared identifier consistent everywhere it appears so joins survive; choosing deterministic (seeded) over random substitution, and the re-identification risk a shared or committed seed reintroduces, since a generator seed is a reproducibility control and not a cryptographic key; preserving field shape where a downstream system validates it, including check-digit values such as payment-card and national-ID numbers plus phone and postal formats; and why a value that merely looks realistic is not yet safe, leaving a residual re-identification measurement over the remaining quasi-identifiers. Use when a masking pipeline has nulled or dropped PII columns and the dataset now needs replacement values that keep cross-table joins intact.

Install with skills.sh (any agent)

npx skills add testland/qa --skill faker-synthetic-data
View source

faker-synthetic-data

The axis this skill owns

A de-identification step has already run: direct identifiers are nulled, redacted, or replaced with a placeholder. The dataset is private but broken, because joins fail and validators reject empty fields. This skill covers putting usable values back and the privacy decisions that forces: keeping a shared identifier consistent so joins survive, deterministic versus random substitution, preserving a validated field shape, and proving the result is safe rather than merely realistic.

It deliberately does not cover the generic mechanics of a fake-data library: installation, the provider catalogue, per-locale tours, or factory patterns for building fixtures from nothing. Nor does it cover detecting PII or choosing between masking operators such as hashing, tokenisation, and nulling. Both happen before this step.

Minimum library surface

The worked example below uses only Faker.seed(n) (run reproducibility, a class method) and fake.unique.<method>() (injective values, raising UniquenessException once the value space is exhausted). The full mechanic table - instance vs class seeding, JS refDate, multiple-locale weighting, and Presidio's custom operator for driving substitution from an anonymiser - is in references/library-surface-and-seeding.md.

Step 1 - Build the substitution map before substituting anything

The masking literature requires masked values to stay consistent across databases that share a data element, and to be repeatable (the same input always yields the same output) yet not reversible to the original (en.wikipedia.org/wiki/Data_masking (opens in new window)).

So do not substitute row by row while streaming. Collect the distinct real values first, build one map, apply it everywhere. Two properties must hold:

  • Injective. Two real people must never collapse onto one substitute, or a join over-matches and row counts silently inflate. fake.unique enforces this until the provider's value space runs out (faker.readthedocs.io (opens in new window)).
  • Complete. Cover every place the identifier appears: denormalised copies, free-text mentions in comment fields, filenames, exports already in the bundle. A column missed here is a column where the real value survives.

The map is the sensitive artifact. Retaining it where the recipient can reach it makes the output pseudonymised, not anonymised: GDPR Article 4(5) treats data attributable to a subject only via separately-kept additional information as pseudonymised (gdpr-info.eu (opens in new window)), and Recital 26 counts data re-attributable through such additional information as personal data (gdpr-info.eu (opens in new window)). Keeping the map is a legitimate choice. Calling the result anonymous afterwards is not.

Step 2 - What a seed guarantees, and what it does not

Does. Reproduce the same generator sequence on a later run, for the same library version. Faker warns that "as we keep updating datasets, results are not guaranteed to be consistent across patch versions" (faker.readthedocs.io (opens in new window)); the JavaScript port warns "you may get different values for the same seed" after an upgrade (fakerjs.dev (opens in new window)).

Does not. Act as a key. Python Faker draws from a random.Random (faker.readthedocs.io (opens in new window)), and the standard library warns that the module's generators are not for security use and points to the secrets module for cryptographic uses (docs.python.org (opens in new window)). A seed offers no resistance to anyone holding it.

The four seeding strategies (per-value derived seed, global seed plus a stored map, global seed with the map discarded, unseeded random) are compared by re-identification exposure in references/library-surface-and-seeding.md. The per-value derived seed is the pattern to avoid. It needs no stored map, and it fails for the reason unsalted hashing of a low-entropy field fails: the input space is enumerable, so the mapping is rebuildable. If a stored map is unacceptable, the answer is a keyed transform, not a derived seed.

Step 3 - Preserve the shape the downstream system validates

Check-digit values. Payment card numbers, and national identifiers including Canadian social insurance numbers, Israeli and South African ID numbers, and Swedish personal identity numbers, carry a Luhn ("mod 10") check digit specified in ISO/IEC 7812-1 (en.wikipedia.org/wiki/Luhn_algorithm (opens in new window)). Faker exposes credit_card_number(card_type=...) accepting 'amex', 'visa', 'mastercard', 'discover', 'diners', 'jcb', and 'maestro' among others, but does not state whether the output carries a valid check digit (faker.readthedocs.io (opens in new window)), so assert it rather than assume it:

def luhn_ok(number: str) -> bool:
    digits = [int(d) for d in number if d.isdigit()][::-1]
    return sum(d if i % 2 == 0 else sum(divmod(d * 2, 10))
               for i, d in enumerate(digits)) % 10 == 0

assert luhn_ok(fake.credit_card_number(card_type="visa"))

Luhn "was designed to protect against accidental errors, not malicious attacks" (en.wikipedia.org/wiki/Luhn_algorithm (opens in new window)), so passing it proves the field parses, not that the value is safe.

Locale-bound formats. Phone numbers and postal codes are validated against the row's country, so pin one locale per row from that country column instead of relying on multiple-locale mode's weighted random draw.

Format plus reversibility. If the receiving system needs the original back later and the shape must survive, substitution is the wrong operator: NIST SP 800-38G, "Recommendation for Block Cipher Modes of Operation: Methods for Format-Preserving Encryption", specifies FF1 and FF3 for that case (csrc.nist.gov (opens in new window)).

Step 4 - Realistic is not safe, so measure the result

Residual quasi-identifiers. Substitution removes only the direct identifiers actually substituted. Birth date, postal code, sex, and admission date usually stay because they carry the analytical value, and in combination they still identify people. NIST SP 800-188, "De-Identifying Government Datasets: Techniques and Governance" (September 2023), treats transforming quasi-identifiers and running re-identification studies as part of de-identification, not optional extras (csrc.nist.gov (opens in new window)). The exit criterion is therefore a measurement over the remaining quasi-identifier columns (smallest equivalence-class size, count of unique rows), against Recital 26's test of whether means are "reasonably likely to be used", accounting for "the costs of and the amount of time required for identification" (gdpr-info.eu (opens in new window)).

Accidental collisions. Generators draw from fixed data, described in the JavaScript port's guide as "lists of names, words etc" (fakerjs.dev (opens in new window)), so a generated value can coincide with a real person's, including one in the source file. Diff substitutes against source values and fail on any intersection.

Worked example - two joined files

customers.csv holds email; orders.csv holds billing_email and joins on it. A masking step already dropped ssn and full_name.

import csv
from faker import Faker

fake = Faker("en_US")
Faker.seed(0)  # run reproducibility only: not a secret, not derived from the data

def load(path):
    with open(path, newline="") as f:
        return list(csv.DictReader(f))

customers, orders = load("customers.csv"), load("orders.csv")
real = {c["email"] for c in customers} | {o["billing_email"] for o in orders}
email_map = {v: fake.unique.email() for v in sorted(real)}

assert len(set(email_map.values())) == len(email_map)   # injective
assert not (set(email_map.values()) & real)             # no collision with source

before = len({c["email"] for c in customers} & {o["billing_email"] for o in orders})
for c in customers:
    c["email"] = email_map[c["email"]]
for o in orders:
    o["billing_email"] = email_map[o["billing_email"]]
after = len({c["email"] for c in customers} & {o["billing_email"] for o in orders})

assert before == after, f"join key cardinality changed: {before} -> {after}"
print(f"substituted {len(email_map)} identifiers, join keys preserved: {after}")

Discard email_map, or store it under separate access control. Never ship it beside the substituted files.

Expected output shape

FileColumnBeforeAfter
customers.csvemailalice.tan@acme.examplexrogers@example.org
orders.csvbilling_emailalice.tan@acme.examplexrogers@example.org
customers.csvpostcodeSW1A 1AASW1A 1AA (quasi-identifier, unchanged)
substituted 12480 identifiers, join keys preserved: 9317
collision check vs source values: 0
residual quasi-identifiers not substituted: postcode, birth_date, sex

The third line is a handoff to a re-identification measurement: an open item, not a pass.

Anti-patterns

Anti-patternWhy it failsFix
Seeding the generator from the real value before each callThe mapping is rebuildable by anyone with the code and a candidate value listOne global seed plus an access-controlled map, or a keyed transform
Substituting row by row while streaming, with no mapThe same person gets a different substitute per occurrence and every join breaksCollect distinct values, build the map once, then apply
Calling the extract anonymous while retaining the mapThe map is the "additional information" of GDPR Article 4(5); the output is still personal dataDestroy the map, or label the output pseudonymised
Substituting a checksum-bearing field without asserting the check digitDownstream validators reject rows, so the environment looks broken rather than maskedAssert Luhn or the field's own check rule before writing
Multiple-locale mode on a dataset with a country columnLocale is a weighted random draw per call, so a French row gets a Japanese postcodeInstantiate one locale per row from that row's country
Declaring done once the direct identifiers look fakeBirth date plus postcode plus sex still identify peopleMeasure equivalence-class sizes over the remaining quasi-identifiers before release
Never diffing substitutes against source valuesFixed name lists mean a substitute can be a real person in the same fileFail the run on any intersection

Limitations

  • A seed is version-bound. Results are "not guaranteed to be consistent across patch versions" (faker.readthedocs.io (opens in new window)), so a rerun after an upgrade produces a different map and breaks joins against extracts already delivered. Pin the version beside the seed.
  • Substitution alone never reaches anonymity. Quasi-identifier transformation and the re-identification study are separate work (csrc.nist.gov (opens in new window)).
  • Injectivity has a ceiling. fake.unique raises UniquenessException once the value space is exhausted (faker.readthedocs.io (opens in new window)), so high-cardinality columns need a composed value (prefix plus counter).
  • No semantic coherence between substituted fields. A substituted address and phone number are drawn independently, so area code and region will not agree. Derive a dependent field from the substituted one instead.

faker-synthetic-data - library surface and seed-exposure detail

View source (opens in new window)

faker-synthetic-data - library surface and seed-exposure detail

Deep reference for faker-synthetic-data (opens in new window). The runnable core - build the substitution map, assert injectivity, preserve check digits, measure residual risk - stays in SKILL.md. This file holds the mechanic tables it draws on.

Minimum library surface

MechanicBehaviourSource
Faker.seed(n)Class method; seeds the shared random.Random across all internal generators. Calling .seed() on an instance raises TypeErrorfaker.readthedocs.io (opens in new window)
fake.seed_instance(n)Creates and seeds a unique random.Random for one instancefaker.readthedocs.io (opens in new window)
fake.unique.<method>()Tracks values already returned; raises UniquenessException after repeated failures to find a new onefaker.readthedocs.io (opens in new window)
Multiple-locale modeThe proxy "randomly select[s] a generator using a distribution defined by the provided weights", so locale varies per callfaker.readthedocs.io (opens in new window)
faker.seed(123) (JS)Fixes the sequence; setting the seed again resets it. Date helpers also need refDate or faker.setDefaultRefDate()fakerjs.dev (opens in new window)

To drive substitution from an anonymiser, Microsoft Presidio exposes a custom operator taking a "lambda to execute on the PII data. The lambda return type must be a string", passed as OperatorConfig("replace", {"new_value": "BIP"})-style entries in the operators dict (presidio.dataprivacystack.org (opens in new window)).

Seed exposure matrix

The four seeding strategies referenced from Step 2 of the skill, ranked by the re-identification exposure each one leaves:

MechanismJoins surviveRe-identification exposure
Seed derived per value, reseeding from the real value before each callYesHigh. Anyone with the code, the library version, and a candidate list of real values re-runs the derivation and rebuilds the map. The seed reproduces the substitute; it does not conceal the input
One global seed plus a stored mapYesEqual to the exposure of the map. Control access to the map, not the seed
One global seed, map discarded after the runYes, within the runLow from the seed alone: the substitute follows call order, not the input value, so the seed reconstructs nothing without the source data
Unseeded, random per occurrenceNoLow, but the dataset is unusable for anything relational

Related skills

data-masking-techniques-reference

Pure-reference catalog of data-masking techniques and de-identification privacy models. Enumerates the seven canonical masking operators (substitution, shuffling, number/date variance, encryption, hashing, nulling, masking-out / character-scrambling) plus tokenisation, redaction, format-preserving encryption, and Microsoft Presidio's six built-in operators. Distinguishes reversible techniques (pseudonymisation candidates per GDPR Art. 4(5)) from irreversible techniques (anonymisation candidates), and maps them to NIST SP 800-188 privacy models - k-anonymity, l-diversity, t-closeness, differential privacy (deep model definitions in references/). Cites ISO/IEC 20889:2018 for the standard taxonomy. Use to pick the right masking operator per field type and risk level.

k-anonymity-verifier

Verifies that a masked dataset satisfies k-anonymity, l-diversity, and t-closeness by computing equivalence classes over chosen quasi-identifiers and reporting re-identification risk. Covers quasi-identifier selection heuristics, threshold guidance, pycanon API (k_anonymity / l_diversity / t_closeness / report), ARX Java API and GUI workflow, SmartNoise for differential-privacy comparison, and CI-gate integration. Distinct from data-masking-techniques-reference (which catalogs masking operators but defers k-anonymity measurement to dedicated tooling) and from presidio-pii-detection (which detects PII spans but offers no equivalence-class analysis). Use when you need to confirm whether a masked dataset meets a stated k, l, or t threshold before promoting it to a non-production environment.

pii-categories-reference

Pure-reference catalog of personally identifiable information (PII) categories across GDPR, CCPA/CPRA, NIST SP 800-122, and HIPAA. Defines what counts as personal data under each regime, enumerates the explicit identifiers each regulator lists (GDPR Art. 4(1) and Art. 9 special categories; CPRA sensitive personal information; NIST direct-identifier vs linkable distinction; HIPAA Safe Harbor 18 identifiers), and maps overlapping fields across jurisdictions so a masking pipeline knows which regulator's rules apply. Use as the authoritative source when authoring or reviewing masking rules, classifying a dataset's risk level, or scoping which fields a PII detector must catch.

pii-masking-pipeline-builder

Build-an-X workflow that produces a PII masking pipeline spec from a source-data inventory. Walks the author through (1) classifying each field against pii-categories-reference, (2) picking a masking operator from data-masking-techniques-reference, (3) deciding pseudonymisation (reversible, in GDPR scope) vs anonymisation (irreversible, out of scope), (4) ordering the pipeline (detect → operator → audit), and (5) emitting a deployable config for Presidio + Faker + Synthea wrappers. Output is a YAML pipeline spec plus a per-field rationale table. Use after classifying a dataset's PII risk; this is the workflow that translates classification into runnable masking config.

presidio-pii-detection

Author and run Microsoft Presidio PII detection - wraps presidio-analyzer (PII detector) + presidio-anonymizer (replace/redact/mask/hash/encrypt operators) for scanning datasets, log streams, and free-text fields. Covers AnalyzerEngine + AnonymizerEngine setup, built-in recognizers (PERSON, EMAIL_ADDRESS, CREDIT_CARD, US_SSN, IBAN_CODE, country-specific IDs across US/UK/Spain/Italy/Poland/Singapore/Australia/India and more), custom PatternRecognizer authoring, score thresholds, and CI gating. Use when scanning *existing* data for PII (vs synthesising fresh fixtures with synthetic-pii-generator).

synthea-healthcare-data

Author and run Synthea (MITRE's open-source synthetic patient population simulator) to produce HIPAA-safe synthetic medical records for testing health IT systems. Covers Gradle build, population-size and state-specific generation, FHIR R4 / STU3 / DSTU2 / C-CDA / CSV / CPCDS output formats, disease-module customisation, and the lifecycle-simulation approach (birth-through-death patient journeys with realistic demographics). Use when testing FHIR servers, EHR integrations, claims processing, or any health IT system that needs realistic patient records without HIPAA exposure (distinct from faker-synthetic-data which is generic; this is health-domain-specific).

test-data-governance-reference

Pure-reference catalog of test-data lifecycle governance: retention schedules for test datasets, cross-environment data-sharing agreements, deletion of test data containing real PII, refresh cadence, access controls, and the legal basis for each policy under GDPR Art. 5 storage limitation and NIST SP 800-122. Use when defining a data-steward role for test environments, authoring a retention policy for a test database, scoping a data-sharing agreement before promoting a dataset from production to staging, or determining the deletion timeline for any test fixture that contains live personal data.