BXUP Track progress & test yourself — free
QA & Testing · Hands-on lab · ~5 min read
QA & TESTING · HANDS-ON LAB

Lab 08 · Privacy-Safe Test Data Factory

Detect and mask the PII seeded through a fake production extract — the answer key lists every planted identifier, including the ones hiding in free text — then generate schema-valid synthetic data with real edge cases, and write the one-page policy that says what may enter which tool.

Nisha needs realistic order data for staging, and the shortcut everyone reaches for — "just anonymize a production dump" — is exactly how PII ends up in a prompt, a screenshot, and a vendor's training corpus. Under India's DPDP Act rules and GDPR alike, test data is personal data until proven otherwise, and "we removed the name column" has never once been proof. In this lab you run a proper pipeline: automated PII discovery with Presidio, an honest audit of what the scanner missed, masking, then a synthetic replacement generated to a validated schema — with the sensitive step kept on a local model. The extract below is fake but shaped like the real thing, planted identifiers and all.

Time, tier & cost

~90 minutes hands-on · Core tier (maps to module M5 and playbooks PB-24/PB-25). Cost: $0 (Presidio, Faker, Pydantic, Ollama are all free and local).

Setup

mkdir -p qa-lab-08 && cd qa-lab-08
python3 -m venv .venv && source .venv/bin/activate
pip install presidio-analyzer presidio-anonymizer faker pydantic
python -m spacy download en_core_web_lg
# optional local model for the sensitive step:
ollama pull llama3.1:8b

prod_extract.csv — the "production" slice. Treat it as radioactive from this line on:

order_id,customer_name,email,phone,ship_address,id_number,notes,amount_inr
9001,Rohit Sharma,rohit.sharma81@gmail.com,+91 98450 12345,"14 MG Road, Indiranagar, Bengaluru 560038",ABCPS1234F,Leave with security,2499.00
9002,Fatima Begum,fatima.b@outlook.com,9912345678,"Flat 3B, Sea Breeze Apts, Bandra West, Mumbai 400050",4321 8765 2109,Call before delivery,760.50
9003,Daniel D'Souza,dan.dsouza@yahoo.in,+91-80-2556-1234,"7 Rest House Rd, Bengaluru 560001",ABCPD9876K,Customer says neighbour Mrs. Iyer (98801 44556) can receive it,15200.00
9004,Priya Venkatesan,priya.v@company.co.in,08041234567,"221 Anna Salai, Chennai 600002",,Refund to card ending 4242 requested on call,439.99
9005,Aarav Mehta,aarav2019@gmail.com,+91 90000 11111,"5 Ridge Rd, Shimla 171001",,Minor account - father Rakesh Mehta rakesh.m@gmail.com approves,899.00
9006,GLOBAL TRADERS PVT LTD,accounts@globaltraders.in,+91 22 4001 2000,"Plot 12, MIDC, Andheri East, Mumbai 400093",27AAACG1234A1Z5,GSTIN on invoice pls,88000.00

schema.py — the Pydantic contract your synthetic data must satisfy:

from pydantic import BaseModel, EmailStr, Field, field_validator
from datetime import date

class Order(BaseModel):
    order_id: int = Field(ge=1)
    customer_name: str = Field(min_length=1, max_length=80)
    email: EmailStr
    phone: str = Field(pattern=r"^\+91[ -]?\d{5}[ -]?\d{5}$")
    ship_city: str
    ship_pin: str = Field(pattern=r"^[1-9]\d{5}$")
    order_date: date
    amount_inr: float = Field(gt=0, le=500000)

    @field_validator("order_date")
    @classmethod
    def not_future(cls, v):
        assert v <= date.today(), "order_date cannot be in the future"
        return v

Create pii-audit.md and data-policy.md empty.

Steps

  1. Run automated discovery first — and time-box it. Write scan.py: load the CSV, run AnalyzerEngine().analyze(text=..., language="en") over every cell (or row-concatenated text), and print entity type, value, confidence per hit. Dump the results into pii-audit.md under "Scanner found." Presidio will reliably flag names, emails, phones, and locations. Ten minutes, no manual reading yet.
  2. Now audit the extract by eye against a checklist. Read all six rows the way a DPO would, hunting categories, not strings: direct identifiers, contact data, government identifiers, financial traces, third-party persons, minors, and business-vs-personal data. Add a "Human found" section to pii-audit.md. The seeded extract contains at least five identifier classes the default scanner misses or mislabels — you are not done until your human list is longer than the scanner's.
  3. Write custom recognizers for the Indian identifiers. Presidio ships no default PAN or Aadhaar-pattern recognizer — add them: a PatternRecognizer for PAN ([A-Z]{5}[0-9]{4}[A-Z]), one for Aadhaar-like 12-digit groups (\d{4}\s\d{4}\s\d{4}), and one for GSTIN (\d{2}[A-Z]{5}\d{4}[A-Z]\d[A-Z]\d). Rerun the scan and record what the upgraded scanner now catches that stock Presidio didn't. This before/after is the calibrated-trust datapoint of the lab.
  4. Mask with the anonymizer — free text included. Use AnonymizerEngine to replace every detected entity (<NAME>, <PHONE>, <PAN>…) and write masked_extract.csv. Then manually verify the notes column row by row: the third-party neighbour and her phone, the card reference, and the father's email must all be gone. Free text is where masking pipelines die; the notes column is the seeded proof.
  5. Decide the GSTIN question in writing. Row 9006 is a company: GSTIN and a business address are not personal data — but the extract pattern can't know that, and sole proprietors' GSTINs can identify a person. Write your ruling and rationale in pii-audit.md (defensible either way; undefended = ungraded).
  6. Generate the synthetic replacement. Write factory.py with Faker (Faker("en_IN")) generating 50 orders validated through Order — every record passes Pydantic or the run fails. Force edge cases into the batch deliberately: names with apostrophes and Unicode (D'Souza, Ñina, a Devanagari name), a leap-day order_date (2024-02-29), boundary pins (110001, 999999), amount at exactly 500000, and an 80-char name. Emit synthetic_orders.csv plus a one-line proof: 50/50 records valid.
  7. Use the local model for the sensitive-shaped step. With Ollama, prompt llama3.1:8b to generate 10 realistic free-text delivery notes in the style of the masked ones (notes are the field whose realism you can't get from Faker). Rule being drilled: raw-shaped production text goes only to local models; the cloud model may see schemas, masked samples, and code — never the unmasked extract. Log which tool saw which artifact in pii-audit.md.
  8. Write the one-page policy.data-policy.md, four sections, one page: (1) classification — what counts as PII here, including the free-text and third-party cases you just hit; (2) the decision tree — raw extract → local-only; masked extract → approved cloud tools; synthetic → any tool; (3) the pipeline — scan with custom recognizers → mask → human free-text review → synthetic replacement, with the audit file as evidence; (4) DPDP/GDPR one-liners — purpose limitation applies to test data, and "we masked the name column" is not compliance.
Warning

The scanner is a floor, not a ceiling. Stock Presidio missed the PAN, the Aadhaar-format number, the GSTIN, and cannot rule on minors or third parties. Every scanner deployment needs the human pass and the custom-recognizer pass, and your audit file must show all three layers disagreeing and reconciling.

Verify

  • pii-audit.md has three layers: stock-scanner findings, human findings, upgraded-scanner findings — with the deltas explicit.
  • masked_extract.csv contains zero surviving identifiers, including all free-text plants.
  • Custom recognizers for PAN, Aadhaar-pattern, GSTIN exist and demonstrably fire.
  • synthetic_orders.csv: 50/50 Pydantic-valid, edge cases present and listed.
  • Tool-exposure log shows the unmasked extract touched only local tools.
  • data-policy.md fits one page and contains the decision tree.

Self-grading rubric (pass = 7/10):

Dimension012
Seeded PII found≤8 identifiers9–12All 15+, incl. every free-text plant
Scanner calibrationTrusted stock scanNoted missesBefore/after recognizer evidence
Masking completenessNotes column leaksStructured cols onlyNothing survives, verified by row
Synthetic qualitySchema violationsValid, no edges50/50 valid + forced edge cases
PolicyMissing/genericPresentOne page, decision tree, DPDP-aware
▸Answer key — every planted identifier and the expected scanner gapsoptional deep dive — click to expand

Structured-column plants (stock Presidio catches most): six customer names (9006 is an org — flagging it as a PERSON is a scanner false positive worth logging); six emails; six phones in four formats (+91 spaced, bare 10-digit, hyphenated landline, 0-prefixed landline — recognizers often miss the landline forms; check yours); six addresses with pins.

Government/financial identifiers (stock Presidio misses; custom recognizers required): PAN ABCPS1234F (row 9001) and ABCPD9876K (row 9003); Aadhaar-formatted 4321 8765 2109 (row 9002) — treat as Aadhaar-class regardless of checksum validity, the format alone is toxic in a dataset; GSTIN 27AAACG1234A1Z5 (row 9006) — your step-5 ruling decides its status, but it must be detected either way.

Free-text plants (the human layer): row 9003 — third-party person "Mrs. Iyer" plus her phone 98801 44556: personal data of someone who is not even the data subject, the single highest-risk plant in the file; row 9004 — "card ending 4242": a financial trace that must not survive masking; row 9005 — a minor's account with a parent's name and email: two extra identifiers plus a children's-data flag (DPDP has explicit children's-data obligations — your policy should mention it); row 9001 — "Leave with security" is clean: planted as a control so over-redactors notice they're deleting signal without cause.

Count for scoring: 6 names + 6 emails + 6 phones + 6 addresses/pins + 2 PAN + 1 Aadhaar-format + 1 GSTIN + 1 third-party name + 1 third-party phone + 1 card reference + 1 parent name + 1 parent email + 1 minor-flag = 15 distinct identifier classes / ~33 individual values. Pass bar: all four free-text rows correctly dispositioned and all three custom-recognizer classes firing. Typical stock-scan recall on this file is roughly 60% of values and zero of the three Indian identifier classes — if your before/after delta doesn't show that gap, your custom recognizers aren't running.

Synthetic-side checks: the phone pattern in schema.py deliberately rejects landline formats the extract contained — noticing that the schema is narrower than reality (and documenting it as an intentional test-data decision, or widening it) earns the last rubric point. The leap-day date must be 2024-02-29 (2026 is not a leap year — a factory that generated 2026-02-29 would fail validation; that failure firing is a pass signal for the validator).

Key Takeaway:

A privacy-safe data factory is three honest layers — scanner, custom recognizers, human free-text review — feeding a synthetic generator that a schema validates on every record. The scanner alone missed every Indian identifier and every third-party plant in this file; the pipeline, not the tool, is what makes test data safe to hand to any model.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in